Skip to content

Commit 1f3229b

Browse files
author
publish-kit
committed
publish-kit sync 2026-05-16
1 parent 5b95582 commit 1f3229b

54 files changed

Lines changed: 6709 additions & 627 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

CHANGELOG.md

Lines changed: 51 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,52 @@
1-
## 2026-04-16
1+
## 2026-05-16
22

3-
+ added personas/flo-rivers.md
4-
+ added personas/holly-helpdesk.md
5-
+ added personas/paige-turner.md
6-
+ added personas/stan-dardson.md
7-
+ added personas/stella-fullstack.md
8-
+ added runbooks/_template.md
9-
+ added runbooks/anthropic-api.md
10-
+ added runbooks/apollo-api.md
11-
+ added runbooks/clickup-api.md
12-
+ added runbooks/credential-vault.md
13-
+ added runbooks/github-api.md
14-
+ added runbooks/helpjuice-api.md
15-
+ added runbooks/salesforce-cli.md
16-
+ added skills/build-tool/SKILL.md
17-
+ added skills/catalog/SKILL.md
18-
+ added skills/changelog-polish/SKILL.md
19-
+ added skills/toolreview/SKILL.md
3+
+ added examples/teams-dm-bot/README.md
4+
+ added examples/teams-dm-bot/bin/fetch-conv-ref.sh
5+
+ added examples/teams-dm-bot/bin/send-dm.sh
6+
+ added examples/teams-dm-bot/functions/function_app.py
7+
+ added examples/teams-dm-bot/functions/host.json
8+
+ added examples/teams-dm-bot/functions/requirements.txt
9+
+ added examples/teams-dm-bot/manifest/color.png
10+
+ added examples/teams-dm-bot/manifest/make-zip.sh
11+
+ added examples/teams-dm-bot/manifest/manifest.json
12+
+ added examples/teams-dm-bot/manifest/outline.png
13+
+ added examples/teams-dm-bot/worker/index.js
14+
+ added examples/teams-dm-bot/worker/wrangler.toml
15+
+ added runbooks/_start-task-gotchas.md
16+
+ added runbooks/azure-container-apps.md
17+
+ added runbooks/browser-harness.md
18+
+ added runbooks/browser-use-api.md
19+
+ added runbooks/case-email-cc-preservation.md
20+
+ added runbooks/claude-code-bash.md
21+
+ added runbooks/claude-workflow.md
22+
+ added runbooks/cloudflare.md
23+
+ added runbooks/firecrawl.md
24+
+ added runbooks/fireflies-api.md
25+
+ added runbooks/github-actions-oidc.md
26+
+ added runbooks/google-workspace.md
27+
+ added runbooks/iterm-tmux.md
28+
+ added runbooks/macos-shell.md
29+
+ added runbooks/maestro-local-dispatch.md
30+
+ added runbooks/netsapiens-api.md
31+
+ added runbooks/notion-api.md
32+
+ added runbooks/playwright-cli.md
33+
+ added runbooks/postmark.md
34+
+ added runbooks/publish-kit.md
35+
+ added runbooks/simplesat-api.md
36+
+ added runbooks/slack-api.md
37+
+ added runbooks/subagent-dispatch.md
38+
+ added runbooks/talentlms.md
39+
+ added runbooks/teams-bot-dm.md
40+
+ added runbooks/unifi-network.md
41+
~ modified personas/flo-rivers.md
42+
~ modified personas/holly-helpdesk.md
43+
~ modified personas/paige-turner.md
44+
~ modified personas/stan-dardson.md
45+
~ modified personas/stella-fullstack.md
46+
~ modified runbooks/_template.md
47+
~ modified runbooks/clickup-api.md
48+
~ modified runbooks/credential-vault.md
49+
~ modified runbooks/github-api.md
50+
~ modified runbooks/helpjuice-api.md
51+
~ modified runbooks/salesforce-cli.md
52+
~ modified skills/toolreview/SKILL.md

examples/teams-dm-bot/README.md

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
# teams-dm-bot — Claude DMs you in Microsoft Teams
2+
3+
A working skeleton: deploy → sideload → say "hi" → run a command → your message lands in Teams.
4+
5+
**Full guide:** `runbooks/teams-bot-dm.md` (read this first).
6+
7+
## What's here
8+
9+
| Path | Purpose |
10+
|---|---|
11+
| `manifest/` | Teams app manifest + icons + zip helper |
12+
| `worker/` | Cloudflare Workers receiver (primary) |
13+
| `functions/` | Azure Functions Python alternative (use one or the other) |
14+
| `bin/fetch-conv-ref.sh` | One-shot: pull the cached conversation reference into `~/.config/teams-dm/` |
15+
| `bin/send-dm.sh` | Send a DM from anywhere on your machine |
16+
17+
## 60-second quickstart
18+
19+
1. Read the runbook. Seriously — it explains *why* the bootstrap dance exists.
20+
2. Create an Azure Bot resource (F0, free) + AAD app registration. Capture the App ID, tenant ID, and a client secret.
21+
3. Deploy the Worker:
22+
```bash
23+
cd worker
24+
wrangler kv:namespace create CONV_REF # paste the id into wrangler.toml
25+
wrangler secret put BOT_APP_ID
26+
wrangler secret put BOT_APP_SECRET
27+
wrangler secret put <credential-env>
28+
wrangler secret put SETUP_SECRET # any random string
29+
wrangler deploy
30+
```
31+
4. Point Bot Service messaging endpoint → `https://<your-worker>.workers.dev/api/messages`.
32+
5. Edit `manifest/manifest.json` (`{{BOT_APP_ID}}`, `{{BOT_NAME}}`) → `manifest/make-zip.sh` → sideload in Teams (Apps → Manage your apps → Upload an app).
33+
6. DM the bot once with "hi". Wrangler tail should show a 200.
34+
7. Pull the conv reference + write env file:
35+
```bash
36+
WORKER_URL=https://<your-worker>.workers.dev \
37+
SETUP_SECRET=<the value you wrangled> \
38+
./bin/fetch-conv-ref.sh
39+
40+
cat > ~/.config/teams-dm/env <<EOF
41+
BOT_APP_ID=<app id>
42+
BOT_APP_SECRET=<client secret>
43+
<credential-env>=<tenant id>
44+
EOF
45+
chmod 600 ~/.config/teams-dm/env
46+
```
47+
8. Send:
48+
```bash
49+
./bin/send-dm.sh "Claude says hi"
50+
./bin/send-dm.sh --markdown "**deploy** finished in _42s_"
51+
```
52+
53+
## When it doesn't work
54+
55+
See the runbook's gotcha catalog. The most common: messaging endpoint missing `/api/messages` suffix, `Microsoft.BotService` provider not registered in the subscription, tenant policy blocking custom app upload.
56+
57+
## Wiring it into Claude Code
58+
59+
Pick one:
60+
- Shell hook: a Claude Code `Stop` hook that calls `send-dm.sh` when a session ends
61+
- Slash command: a `/dm "..."` command that wraps `send-dm.sh`
62+
- Inline: ask Claude to run `bash <path>/send-dm.sh "message"` directly
63+
64+
The send helper has no dependencies on Claude — it works from any process with `curl`, `python3`, and the env file readable.
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
#!/usr/bin/env bash
2+
# One-shot bootstrap helper.
3+
# Fetches the cached conversation reference from the Worker (or Function)
4+
# using the setup secret and writes it to ~/.config/teams-dm/conv-ref.json.
5+
#
6+
# Usage:
7+
# WORKER_URL=https://my-bot.example.workers.dev \
8+
# SETUP_SECRET=hunter2 \
9+
# ./fetch-conv-ref.sh
10+
set -euo pipefail
11+
12+
: "${WORKER_URL:?WORKER_URL must be set to your Worker or Function URL}"
13+
: "${SETUP_SECRET:?SETUP_SECRET must be set to the value you wrangled into the worker}"
14+
15+
CONFIG_DIR="${HOME}/.config/teams-dm"
16+
mkdir -p "${CONFIG_DIR}"
17+
18+
response_file="$(mktemp)"
19+
trap 'rm -f "${response_file}"' EXIT
20+
21+
http_code="$(curl -sS -o "${response_file}" -w "%{http_code}" \
22+
-H "X-Setup-Secret: ${SETUP_SECRET}" \
23+
"${WORKER_URL%/}/conv-ref")"
24+
25+
if [[ "${http_code}" != "200" ]]; then
26+
echo "ERR: Worker returned ${http_code}" >&2
27+
cat "${response_file}" >&2
28+
exit 1
29+
fi
30+
31+
# Sanity-check that we got JSON with a conversationId.
32+
if ! python3 -c "import json,sys; d=json.load(open('${response_file}')); sys.exit(0 if d.get('conversationId') and d.get('serviceUrl') else 1)"; then
33+
echo "ERR: response did not contain conversationId + serviceUrl" >&2
34+
cat "${response_file}" >&2
35+
exit 1
36+
fi
37+
38+
mv "${response_file}" "${CONFIG_DIR}/conv-ref.json"
39+
chmod 600 "${CONFIG_DIR}/conv-ref.json"
40+
echo "Saved: ${CONFIG_DIR}/conv-ref.json"
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env bash
2+
# Send a Teams DM as the bot. Outbound-only — uses the cached conversation
3+
# reference written by fetch-conv-ref.sh.
4+
#
5+
# Usage:
6+
# ./send-dm.sh "message body" # plain text
7+
# ./send-dm.sh --markdown "**hi**" # rendered as markdown
8+
set -euo pipefail
9+
10+
CONFIG_DIR="${HOME}/.config/teams-dm"
11+
CONV_REF="${CONFIG_DIR}/conv-ref.json"
12+
ENV_FILE="${CONFIG_DIR}/env"
13+
TOKEN_CACHE="${TMPDIR:-/tmp}/teams-dm-token"
14+
15+
[[ -f "${CONV_REF}" ]] || { echo "ERR: ${CONV_REF} not found. Run fetch-conv-ref.sh first." >&2; exit 1; }
16+
[[ -f "${ENV_FILE}" ]] || { echo "ERR: ${ENV_FILE} not found. See README for required vars." >&2; exit 1; }
17+
18+
# shellcheck disable=SC1090
19+
source "${ENV_FILE}"
20+
: "${BOT_APP_ID:?BOT_APP_ID missing in ${ENV_FILE}}"
21+
: "${BOT_APP_SECRET:?BOT_APP_SECRET missing in ${ENV_FILE}}"
22+
: "${<credential-env>:?<credential-env> missing in ${ENV_FILE}}"
23+
24+
text_format="plain"
25+
if [[ "${1:-}" == "--markdown" ]]; then
26+
text_format="markdown"
27+
shift
28+
fi
29+
text="${1:?usage: send-dm.sh [--markdown] \"message\"}"
30+
31+
# Reuse cached token. Cache stores access_token + absolute expiry epoch on two
32+
# lines. Honor the token endpoint's expires_in minus a 60s clock-skew margin.
33+
# Atomic write via mktemp+mv. mkdir-based lock prevents concurrent invocations
34+
# from stampeding the token endpoint when the cache expires (mkdir is atomic
35+
# on all POSIX filesystems — no flock/portable lock-file dance needed).
36+
LOCK_DIR="${TOKEN_CACHE}.lock"
37+
acquire_lock() {
38+
local tries=0
39+
until mkdir "${LOCK_DIR}" 2>/dev/null; do
40+
tries=$((tries + 1))
41+
[[ $tries -gt 50 ]] && { echo "ERR: could not acquire token lock after 5s" >&2; return 1; }
42+
sleep 0.1
43+
done
44+
trap 'rmdir "${LOCK_DIR}" 2>/dev/null' EXIT
45+
}
46+
release_lock() {
47+
rmdir "${LOCK_DIR}" 2>/dev/null || true
48+
trap - EXIT
49+
}
50+
51+
get_token() {
52+
if [[ -f "${TOKEN_CACHE}" ]]; then
53+
local cached_exp cached_token
54+
cached_exp="$(sed -n '2p' "${TOKEN_CACHE}" 2>/dev/null || echo 0)"
55+
cached_token="$(sed -n '1p' "${TOKEN_CACHE}" 2>/dev/null || true)"
56+
if [[ -n "${cached_token}" && "${cached_exp}" =~ ^[0-9]+$ ]]; then
57+
if (( cached_exp - 60 > $(date +%s) )); then
58+
echo "${cached_token}"
59+
return
60+
fi
61+
fi
62+
fi
63+
# Cache miss or expired — serialize the refresh.
64+
acquire_lock || return 1
65+
# Re-check after acquiring lock — another process may have refreshed.
66+
if [[ -f "${TOKEN_CACHE}" ]]; then
67+
local cached_exp cached_token
68+
cached_exp="$(sed -n '2p' "${TOKEN_CACHE}" 2>/dev/null || echo 0)"
69+
cached_token="$(sed -n '1p' "${TOKEN_CACHE}" 2>/dev/null || true)"
70+
if [[ -n "${cached_token}" && "${cached_exp}" =~ ^[0-9]+$ ]] && (( cached_exp - 60 > $(date +%s) )); then
71+
release_lock
72+
echo "${cached_token}"
73+
return
74+
fi
75+
fi
76+
local response
77+
response="$(curl -sS -X POST \
78+
"https://login.microsoftonline.com/${<credential-env>}/oauth2/v2.0/token" \
79+
-d "client_id=${BOT_APP_ID}" \
80+
-d "client_secret=${BOT_APP_SECRET}" \
81+
-d "scope=https://api.botframework.com/.default" \
82+
-d "grant_type=client_credentials")"
83+
local parsed
84+
parsed="$(echo "${response}" | python3 -c "
85+
import json, sys, time
86+
d = json.load(sys.stdin)
87+
if 'access_token' not in d:
88+
sys.exit('ERR: token response missing access_token: ' + json.dumps(d))
89+
exp = int(time.time()) + int(d.get('expires_in', 3600))
90+
print(d['access_token'])
91+
print(exp)
92+
")"
93+
local tmp
94+
tmp="$(mktemp "${TOKEN_CACHE}.XXXXXX")"
95+
printf '%s\n' "${parsed}" > "${tmp}"
96+
chmod 600 "${tmp}"
97+
mv "${tmp}" "${TOKEN_CACHE}"
98+
release_lock
99+
echo "${parsed%%$'\n'*}"
100+
}
101+
102+
token="$(get_token)"
103+
conv_id="$(python3 -c "import json; print(json.load(open('${CONV_REF}'))['conversationId'])")"
104+
service_url="$(python3 -c "import json; print(json.load(open('${CONV_REF}'))['serviceUrl'].rstrip('/'))")"
105+
106+
body="$(python3 -c "
107+
import json, sys
108+
print(json.dumps({
109+
'type': 'message',
110+
'textFormat': sys.argv[1],
111+
'text': sys.argv[2],
112+
}))
113+
" "${text_format}" "${text}")"
114+
115+
http_code="$(curl -sS -o /tmp/teams-dm-resp -w "%{http_code}" \
116+
-X POST "${service_url}/v3/conversations/${conv_id}/activities" \
117+
-H "Authorization: Bearer ${token}" \
118+
-H "Content-Type: application/json" \
119+
-d "${body}")"
120+
121+
if [[ "${http_code}" == "401" ]]; then
122+
# Token may have rotated — bust cache and retry once.
123+
rm -f "${TOKEN_CACHE}"
124+
token="$(get_token)"
125+
http_code="$(curl -sS -o /tmp/teams-dm-resp -w "%{http_code}" \
126+
-X POST "${service_url}/v3/conversations/${conv_id}/activities" \
127+
-H "Authorization: Bearer ${token}" \
128+
-H "Content-Type: application/json" \
129+
-d "${body}")"
130+
fi
131+
132+
if [[ "${http_code}" != "200" && "${http_code}" != "201" ]]; then
133+
echo "ERR: send failed (HTTP ${http_code})" >&2
134+
cat /tmp/teams-dm-resp >&2
135+
exit 1
136+
fi
137+
echo "Sent."

0 commit comments

Comments
 (0)