@mininglamp-oss/cc-channel-octo 1.0.1-dev.0ac574a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +361 -0
  2. package/LICENSE +191 -0
  3. package/README.md +577 -0
  4. package/config.bot.example.json +15 -0
  5. package/config.example.json +33 -0
  6. package/dist/agent-bridge.d.ts +91 -0
  7. package/dist/agent-bridge.js +397 -0
  8. package/dist/agent-bridge.js.map +1 -0
  9. package/dist/cli.d.ts +109 -0
  10. package/dist/cli.js +467 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands.d.ts +57 -0
  13. package/dist/commands.js +121 -0
  14. package/dist/commands.js.map +1 -0
  15. package/dist/config.d.ts +294 -0
  16. package/dist/config.js +344 -0
  17. package/dist/config.js.map +1 -0
  18. package/dist/configure.d.ts +11 -0
  19. package/dist/configure.js +106 -0
  20. package/dist/configure.js.map +1 -0
  21. package/dist/cron-evaluator.d.ts +53 -0
  22. package/dist/cron-evaluator.js +191 -0
  23. package/dist/cron-evaluator.js.map +1 -0
  24. package/dist/cron-fire-marker.d.ts +24 -0
  25. package/dist/cron-fire-marker.js +25 -0
  26. package/dist/cron-fire-marker.js.map +1 -0
  27. package/dist/cron-scheduler.d.ts +46 -0
  28. package/dist/cron-scheduler.js +114 -0
  29. package/dist/cron-scheduler.js.map +1 -0
  30. package/dist/cron-store.d.ts +62 -0
  31. package/dist/cron-store.js +63 -0
  32. package/dist/cron-store.js.map +1 -0
  33. package/dist/cron-tool.d.ts +44 -0
  34. package/dist/cron-tool.js +151 -0
  35. package/dist/cron-tool.js.map +1 -0
  36. package/dist/cwd-resolver.d.ts +72 -0
  37. package/dist/cwd-resolver.js +166 -0
  38. package/dist/cwd-resolver.js.map +1 -0
  39. package/dist/db-adapter.d.ts +21 -0
  40. package/dist/db-adapter.js +64 -0
  41. package/dist/db-adapter.js.map +1 -0
  42. package/dist/file-inline-wrap.d.ts +94 -0
  43. package/dist/file-inline-wrap.js +243 -0
  44. package/dist/file-inline-wrap.js.map +1 -0
  45. package/dist/gateway.d.ts +105 -0
  46. package/dist/gateway.js +425 -0
  47. package/dist/gateway.js.map +1 -0
  48. package/dist/group-config.d.ts +41 -0
  49. package/dist/group-config.js +104 -0
  50. package/dist/group-config.js.map +1 -0
  51. package/dist/group-context.d.ts +81 -0
  52. package/dist/group-context.js +466 -0
  53. package/dist/group-context.js.map +1 -0
  54. package/dist/inbound.d.ts +136 -0
  55. package/dist/inbound.js +667 -0
  56. package/dist/inbound.js.map +1 -0
  57. package/dist/index.d.ts +65 -0
  58. package/dist/index.js +1026 -0
  59. package/dist/index.js.map +1 -0
  60. package/dist/media-inbound.d.ts +38 -0
  61. package/dist/media-inbound.js +131 -0
  62. package/dist/media-inbound.js.map +1 -0
  63. package/dist/mention-utils.d.ts +108 -0
  64. package/dist/mention-utils.js +199 -0
  65. package/dist/mention-utils.js.map +1 -0
  66. package/dist/octo/api.d.ts +148 -0
  67. package/dist/octo/api.js +320 -0
  68. package/dist/octo/api.js.map +1 -0
  69. package/dist/octo/socket.d.ts +102 -0
  70. package/dist/octo/socket.js +793 -0
  71. package/dist/octo/socket.js.map +1 -0
  72. package/dist/octo/types.d.ts +126 -0
  73. package/dist/octo/types.js +35 -0
  74. package/dist/octo/types.js.map +1 -0
  75. package/dist/prompt-safety.d.ts +78 -0
  76. package/dist/prompt-safety.js +148 -0
  77. package/dist/prompt-safety.js.map +1 -0
  78. package/dist/session-router.d.ts +144 -0
  79. package/dist/session-router.js +490 -0
  80. package/dist/session-router.js.map +1 -0
  81. package/dist/session-store.d.ts +89 -0
  82. package/dist/session-store.js +297 -0
  83. package/dist/session-store.js.map +1 -0
  84. package/dist/skill-linker.d.ts +31 -0
  85. package/dist/skill-linker.js +160 -0
  86. package/dist/skill-linker.js.map +1 -0
  87. package/dist/stream-relay.d.ts +42 -0
  88. package/dist/stream-relay.js +243 -0
  89. package/dist/stream-relay.js.map +1 -0
  90. package/dist/url-policy.d.ts +103 -0
  91. package/dist/url-policy.js +290 -0
  92. package/dist/url-policy.js.map +1 -0
  93. package/package.json +79 -0
package/README.md ADDED
@@ -0,0 +1,577 @@
1
+ <h1 align="center">cc-channel-octo</h1>
2
+
3
+ <p align="center">
4
+ Bridge <a href="https://docs.anthropic.com/en/docs/claude-code">Claude Code</a> to <a href="https://github.com/nicco-io/octo">Octo</a> IM — an independent Node.js gateway powered by the <a href="https://www.npmjs.com/package/@anthropic-ai/claude-agent-sdk">Claude Agent SDK</a>.
5
+ </p>
6
+
7
+ <p align="center">
8
+ <a href="https://github.com/Mininglamp-OSS/cc-channel-octo/actions"><img src="https://github.com/Mininglamp-OSS/cc-channel-octo/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
9
+ <a href="https://www.npmjs.com/package/@mininglamp-oss/cc-channel-octo"><img src="https://img.shields.io/npm/v/@mininglamp-oss/cc-channel-octo" alt="npm version"></a>
10
+ <a href="./LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="License"></a>
11
+ <img src="https://img.shields.io/node/v/@mininglamp-oss/cc-channel-octo" alt="Node.js version">
12
+ </p>
13
+
14
+ ---
15
+
16
+ Users talk to a bot in Octo (DM or group @mention). The bot sends messages to Claude Code via the Agent SDK, streams the response back in real time, and persists conversation history in SQLite — all as a single self-contained process.
17
+
18
+ ## Features
19
+
20
+ - **Streaming output** — Real-time response delivery via Octo's stream API with 800 ms throttled flushes, typing indicators, and automatic fallback to plain messages.
21
+ - **Tool progress** *(opt-in)* — With `sdk.toolProgress` enabled, the bot posts brief `🔧 Running <tool>(<params>)…` notices as the agent invokes tools (params are a truncated one-liner), so users see activity during long tool-heavy turns (deduped + capped per turn).
22
+ - **Group chat awareness** — Responds only to @mentions. Injects recent group conversation as context so Claude understands the discussion.
23
+ - **Session persistence** — SQLite-backed conversation history (40-message sliding window) with automatic 7-day expiry.
24
+ - **In-chat commands** — `/reset` clears your own session's stored history (not the shared recent-group-context cache), `/config` shows the active settings, `/help` lists commands. Scoped per-user (even in groups); subject to the same per-session rate limit as normal messages.
25
+ - **Rate limiting** — Per-session token bucket (default 5 req/min) with debounced rejection notices.
26
+ - **Security by configuration** — `allowedTools` whitelist + per-session workspace isolation. No runtime permission prompts (headless mode).
27
+ - **Multi-bot** — Run several independent bots in one process via a `bots[]` config array; each gets its own token, data directory, and sandbox root (no shared history).
28
+ - **Zero infrastructure** — Single process, single SQLite file, `npm start` and go.
29
+
30
+ ## Quick Start
31
+
32
+ ### Prerequisites
33
+
34
+ - Node.js ≥ 22
35
+ - An Octo bot token (`bf_*`)
36
+ - Claude Code CLI installed (`npm i -g @anthropic-ai/claude-code`)
37
+ - `ANTHROPIC_API_KEY` set in your environment
38
+
39
+ ### Install & Run
40
+
41
+ **Option A — install from npm (recommended):**
42
+
43
+ ```bash
44
+ npm install -g @mininglamp-oss/cc-channel-octo
45
+ # or run without installing:
46
+ npx @mininglamp-oss/cc-channel-octo
47
+ ```
48
+
49
+ This installs the `cc-channel-octo` command (prebuilt — no compile step). Skip
50
+ straight to creating the config files below, then run `cc-channel-octo`.
51
+
52
+ To use it as a library instead, `npm install @mininglamp-oss/cc-channel-octo`
53
+ and import from it; the Claude Agent SDK is a peer dependency, so install
54
+ `@anthropic-ai/claude-agent-sdk` alongside it.
55
+
56
+ **Option B — build from source:**
57
+
58
+ ```bash
59
+ git clone https://github.com/Mininglamp-OSS/cc-channel-octo.git
60
+ cd cc-channel-octo
61
+ npm install
62
+ npm run build
63
+ ```
64
+
65
+ cc-channel-octo uses a fixed, bot-first directory layout under **`~/.cc-channel-octo/`**:
66
+
67
+ ```
68
+ ~/.cc-channel-octo/
69
+ ├── config.json ← GLOBAL: shared defaults + the list of bots (no token)
70
+ ├── skills/ ← optional: skills shared by ALL bots (see Agent skills)
71
+ └── <botId>/ ← one self-contained subtree per bot ("default" for a single bot)
72
+ ├── config.json ← THIS bot: botToken (required) + per-bot overrides
73
+ ├── SOUL.md ← optional personality (overrides sdk.systemPrompt)
74
+ ├── skills/ ← optional: skills for THIS bot (override same-named global)
75
+ ├── data/ ← SQLite history
76
+ ├── workspace/ ← per-session cwd sandboxes (auto-created)
77
+ └── memory/ ← long-term auto-memory (auto-created)
78
+ ```
79
+
80
+ The directory holding the global `config.json` is the **baseDir**; every bot's
81
+ `data`/`workspace`/`memory` are **derived** from `<baseDir>/<id>/…` and are not
82
+ configurable, so a bot can never escape its own subtree.
83
+
84
+ Create the two config files:
85
+
86
+ ```bash
87
+ mkdir -p ~/.cc-channel-octo/default
88
+ cp config.example.json ~/.cc-channel-octo/config.json # global (shared + bots list)
89
+ cp config.bot.example.json ~/.cc-channel-octo/default/config.json # the bot (token + overrides)
90
+ chmod 600 ~/.cc-channel-octo/config.json ~/.cc-channel-octo/default/config.json
91
+ ```
92
+
93
+ Global `~/.cc-channel-octo/config.json` (shared; **no token**):
94
+
95
+ ```jsonc
96
+ {
97
+ "apiUrl": "https://your-octo-instance.com",
98
+ "bots": [{ "id": "default" }]
99
+ }
100
+ ```
101
+
102
+ Per-bot `~/.cc-channel-octo/default/config.json`:
103
+
104
+ ```jsonc
105
+ {
106
+ "botToken": "bf_YOUR_BOT_TOKEN",
107
+ "sdk": { "model": "vertexai/claude-opus-4-8" }
108
+ }
109
+ ```
110
+
111
+ Start the gateway:
112
+
113
+ ```bash
114
+ npm start # from source (Option B)
115
+ # or, if installed from npm (Option A):
116
+ cc-channel-octo
117
+ ```
118
+
119
+ The bot is now online. Send it a DM or @mention it in a group.
120
+
121
+ ### Install via Claude Code CLI (copy-paste prompts)
122
+
123
+ Prefer to let Claude Code do the setup? Run `claude` in an empty working
124
+ directory and paste **one prompt per step**. The two steps are deliberately
125
+ separate: **Install** clones + builds the code (no secrets), then **Configure**
126
+ writes your token(s) and starts the gateway. Both steps support single- and
127
+ multi-bot deployments.
128
+
129
+ > **Heads up — these are agentic prompts, not scripts.** Claude Code will run
130
+ > `git`, `npm`, `mkdir`, and write files under `~/.cc-channel-octo/`. Read what
131
+ > it proposes before approving. Your bot token(s) go only into
132
+ > `~/.cc-channel-octo/<id>/config.json` (chmod `600`) — never into the repo,
133
+ > shell history, or this prompt's output.
134
+
135
+ #### Step 1 — Install (clone + build, no secrets)
136
+
137
+ ```text
138
+ Install the cc-channel-octo gateway from source.
139
+
140
+ 1. Verify Node.js >= 22 is on PATH (`node -v`); stop and tell me if it is older.
141
+ 2. Clone https://github.com/Mininglamp-OSS/cc-channel-octo.git into the current
142
+ directory (skip the clone if a cc-channel-octo/ checkout is already here — just
143
+ `git pull` it instead) and cd into it.
144
+ 3. Run `npm install` then `npm run build`.
145
+ 4. Run `npm test` and confirm it passes.
146
+ 5. Print the absolute path of the repo and tell me to run the Configure prompt
147
+ next. Do NOT create any config files or ask me for a token in this step.
148
+ ```
149
+
150
+ #### Step 2 — Configure & run (writes token, starts gateway)
151
+
152
+ Edit the bracketed values first, then paste. For **multi-bot**, list more than
153
+ one entry under "Bots".
154
+
155
+ ```text
156
+ Configure and start the cc-channel-octo gateway you just installed.
157
+
158
+ Octo API URL: https://your-octo-instance.com
159
+ Bots (one line each — "id = token"; ids are slugs [a-z0-9._-]):
160
+ default = bf_YOUR_BOT_TOKEN
161
+ # add more lines for multi-bot, e.g.:
162
+ # support = bf_TOKEN_A
163
+ # ops = bf_TOKEN_B
164
+ Claude model: vertexai/claude-opus-4-8 # or leave blank for the SDK default
165
+
166
+ Do this:
167
+ 1. Create the fixed layout under ~/.cc-channel-octo/ : a GLOBAL config.json
168
+ holding { "apiUrl": "<the URL>", "bots": [ { "id": "<id>" }, ... ] } with one
169
+ entry per bot above and NO tokens in it.
170
+ 2. For EACH bot, create ~/.cc-channel-octo/<id>/config.json containing that bot's
171
+ { "botToken": "<its token>", "sdk": { "model": "<model, if given>" } }.
172
+ 3. `chmod 600` the global config and every per-bot config.json. Tokens must
173
+ appear ONLY in the per-bot files — never echo a token back to me or put one in
174
+ the global file.
175
+ 4. Validate: the global `bots[]` ids must exactly match the per-bot directory
176
+ names, and each per-bot file must have a non-empty botToken. Fix mismatches.
177
+ 5. From the repo dir, start the gateway with `npm start` and watch the logs until
178
+ you see "Ready — listening for messages" (multi-bot also logs "Multi-bot mode:
179
+ starting N bots" and one "Bot connected" per bot). Report success, or surface
180
+ the first error if a bot fails to register/connect.
181
+ ```
182
+
183
+ After "Ready", DM the bot or @mention it in a group. To change config later,
184
+ edit the JSON files and restart (`npm start`); see [Configuration](#configuration)
185
+ for every available field.
186
+
187
+ ## Configuration
188
+
189
+ All configuration comes from JSON files — there are **no environment-variable
190
+ overrides**. Two layers: a **global** `~/.cc-channel-octo/config.json` (shared
191
+ defaults + the `bots` list) and one **per-bot** `~/.cc-channel-octo/<id>/config.json`
192
+ (its token + overrides). Per-bot fields win over the global layer; per-bot
193
+ directories are derived from `baseDir` (see the tree above) and are not
194
+ configurable.
195
+
196
+ | Field | Default | Description |
197
+ |-------|---------|-------------|
198
+ | `botToken` | *(required, per-bot)* | Octo bot token (`bf_*`). Lives in `<baseDir>/<id>/config.json`, not the global file. |
199
+ | `apiUrl` | *(required)* | Octo API base URL (shared; a bot may override). |
200
+ | `bots` | `[{id:"default"}]` | Which bots to run; each `id` selects its subtree + per-bot config. |
201
+ | *(dirs)* | *(derived)* | `data`/`workspace`/`memory`/`skills` are always `<baseDir>/<id>/…` — not configurable. |
202
+ | `sdk.model` | *(SDK default)* | Claude model override |
203
+ | `sdk.allowedTools` | `"*"` | Either `"*"` (allow every tool the SDK exposes) or an explicit string array whitelist. |
204
+ | `sdk.permissionMode` | `bypassPermissions` | SDK permission mode |
205
+ | `sdk.maxTurns` | *(SDK default)* | Max agentic turns per query |
206
+ | `sdk.systemPrompt` | *(built-in)* | Custom system prompt (a `<baseDir>/<id>/SOUL.md` overrides this). |
207
+ | `sdk.toolProgress` | `false` | When true, post `🔧 Running <tool>(<params>)…` notices as the agent invokes tools (params truncated; deduped, capped per turn) |
208
+ | `sdk.settingSources` | `['project']` | Filesystem settings sources the SDK loads. Default `['project']` so it discovers skills symlinked into the session sandbox's `.claude/skills/` (see [Agent skills](#agent-skills)). Memory stays isolated regardless (inline auto-memory dir pin). Add `'user'` only to deliberately load the operator's real `~/.claude`. |
209
+ | `groupConfigDir` | *(unset)* | Directory of per-group instruction files (`<groupId>.md`). A match is injected into the system prompt as trusted custom instructions for that group. See [Per-group instructions](#per-group-instructions). |
210
+ | `sdk.anthropicBaseUrl` | *(unset)* | Override the upstream Claude API endpoint. See [Self-hosted gateway](#self-hosted-gateway) below. |
211
+ | `sdk.env` | *(unset)* | Extra environment variables (`{ "KEY": "value" }`) injected verbatim into the agent's tool subprocess. Per-bot. Use to give a bot's skills what their CLIs need — e.g. `{ "OCTO_BOT_ID": "<robotId>" }` so a multi-bot deploy's `octo-cli` selects the right stored profile. See [Agent skills](#agent-skills). |
212
+ | `sdk.skills` | *(SDK default)* | Which skills this bot enables: `'all'` or a `string[]` of skill names. Per-bot **selection** over the centrally-maintained skill library (maintain once, each bot picks its subset). Omit to use the SDK default. See [Agent skills](#agent-skills). |
213
+ | `sdk.cron` | `false` | When true, give the agent a `cron` tool set to register per-bot scheduled tasks (persisted to `<baseDir>/<id>/cron.json`, fired through the normal pipeline). Creation is **owner-gated**. See [Scheduled tasks](#scheduled-tasks). |
214
+ | `rateLimit.maxPerMinute` | `5` | Max requests per minute per session |
215
+ | `context.maxContextChars` | `6000` | Max characters of group context injected into prompts |
216
+ | `context.historyLimit` | `40` | Max messages in session history window |
217
+ | `botBlocklist` | `[]` | Bot UIDs to ignore in DMs (prevents bot loops) |
218
+ | `mentionFreeGroups` | `[]` | Group channel IDs where the bot responds to every text message without requiring an `@bot` mention (G12). |
219
+
220
+ ### Self-hosted gateway
221
+
222
+ If you proxy the Claude API through your own gateway (corporate egress, regional
223
+ endpoint, model router, etc.), set `sdk.anthropicBaseUrl` to the gateway origin.
224
+ The value is forwarded to the Claude Agent SDK subprocess as the standard
225
+ `ANTHROPIC_BASE_URL` environment variable (scoped to the subprocess — it does
226
+ not mutate the gateway's own environment), so any deployment that already speaks
227
+ the Anthropic protocol will Just Work — no code changes required.
228
+
229
+ Because this endpoint receives the Anthropic API key and all prompt/response
230
+ content, it is SSRF-validated at boot exactly like `apiUrl`: it must be `https://`
231
+ (or `http://localhost` / `http://127.0.0.1` for local development), and may not
232
+ resolve to a private/link-local address. An unsafe value fails fast at startup.
233
+
234
+ ```jsonc
235
+ {
236
+ "sdk": {
237
+ "anthropicBaseUrl": "https://claude-gw.internal.example.com"
238
+ }
239
+ }
240
+ ```
241
+
242
+ Leave the field unset to talk to Anthropic's public endpoint directly.
243
+
244
+ ### Per-group instructions
245
+
246
+ Give a specific group its own persona or rules without code changes: set
247
+ `groupConfigDir` to a directory you control, and drop a `<groupId>.md` file in
248
+ it (the group's channel id, e.g. `s12_345.md`). Its contents are injected into
249
+ that group's system prompt as a trusted, **unsanitized** `[Group instructions]`
250
+ block. Only groups use this; DMs key on the peer uid. The key is the channel id,
251
+ so all topics under one `CommunityTopic` channel id share the same file.
252
+
253
+ ```jsonc
254
+ // ~/.cc-channel-octo/config.json
255
+ { "groupConfigDir": "/home/deploy/cc-octo-groups" }
256
+ // /home/deploy/cc-octo-groups/s12_345.md:
257
+ // Always answer in formal English and cite sources.
258
+ ```
259
+
260
+ > ⚠️ **Security — this is a trusted, unsanitized prompt-injection sink.** Its
261
+ > safety depends entirely on the files being writable **only** by the operator.
262
+ > Putting `groupConfigDir` outside every bot's `workspace/` is required (the
263
+ > gateway refuses otherwise) but **not sufficient**: under the shipped defaults
264
+ > (`allowedTools: "*"` + `bypassPermissions`) the agent has `Bash`/`Write` and
265
+ > can write **absolute** paths outside its sandbox — the workspace is a starting
266
+ > dir, not a chroot (see [Security Model](#security-model)). A malicious user
267
+ > could then have the agent write `<groupConfigDir>/<otherGroup>.md` and inject
268
+ > persistent, trusted instructions into another group. So you **must**:
269
+ > - make `groupConfigDir` and its files **non-writable by the gateway process
270
+ > user** (e.g. root-owned, mode `0755`/`0644`), and/or
271
+ > - harden the deployment (drop `Bash` from `allowedTools`, run unprivileged,
272
+ > sandbox the filesystem).
273
+ >
274
+ > As defense-in-depth the gateway refuses to inject a group/world-writable file,
275
+ > but that is a backstop, not the guarantee. Files larger than 16 KiB are
276
+ > truncated; an unsafe group id (path separators, `..`) is ignored.
277
+
278
+ ### Agent skills
279
+
280
+ cc supports external tooling generically through **Claude skills**. Drop a skill
281
+ into a `skills/` directory and the agent can use it — there is **no per-tool code
282
+ in cc**. A skill is a standard directory with a `SKILL.md` (plus optional
283
+ `references/` and `scripts/`); it teaches the agent how to drive some external
284
+ CLI (`octo-cli`, `gh`, anything on `PATH`).
285
+
286
+ **Two layers** (mirroring the config model):
287
+
288
+ | Location | Scope |
289
+ |----------|-------|
290
+ | `~/.cc-channel-octo/skills/<name>/` | shared by **all** bots |
291
+ | `~/.cc-channel-octo/<id>/skills/<name>/` | **one** bot (overrides a same-named global skill) |
292
+
293
+ cc symlinks both layers into each session sandbox's `.claude/skills/` on every
294
+ turn, and the SDK discovers them because `sdk.settingSources` defaults to
295
+ `['project']`. (Memory isolation is unaffected — the auto-memory directory is
296
+ pinned via inline settings, which the SDK ranks above any project-level value.)
297
+
298
+ **Installing a skill.** Copy or symlink any `SKILL.md` directory into a `skills/`
299
+ folder. For octo operations, octo-cli ships ready-made skills:
300
+
301
+ ```bash
302
+ mkdir -p ~/.cc-channel-octo/skills
303
+ octo-cli skills --install ~/.cc-channel-octo/skills # octo-shared, octo-messaging, …
304
+ ```
305
+
306
+ **Per-bot skill selection.** The library is maintained once; each bot picks its
307
+ subset via `sdk.skills` in its per-bot config.json — `'all'`, or a list of skill
308
+ names:
309
+
310
+ ```jsonc
311
+ // ~/.cc-channel-octo/issue-triage/config.json
312
+ { "sdk": { "skills": ["octo-messaging", "github-issue-triage"] } }
313
+ ```
314
+
315
+ Omit it for the SDK default. So a `triage` bot can enable the triage + messaging
316
+ skills while another bot enables only messaging — from the same shared library.
317
+
318
+ **Per-bot identity.** Each bot's persona/rules are independent:
319
+
320
+ - `<id>/SOUL.md` — persona (overrides `sdk.systemPrompt`).
321
+ - `<id>/CLAUDE.md` — behavior rules. Discovered because the `project` source
322
+ walks up from the session sandbox to the bot subtree.
323
+ - `~/.cc-channel-octo/CLAUDE.md` (optional) — an all-bots baseline (the same
324
+ upward walk reaches it).
325
+
326
+ > ⚠️ **CLAUDE.md upward-walk has no project boundary.** The walk continues past
327
+ > the bot subtree all the way up the filesystem — so a `CLAUDE.md` in the host
328
+ > HOME or any ancestor directory **leaks into every bot's context**. Keep the
329
+ > deploy machine's `$HOME` (and ancestors) free of `CLAUDE.md`; put bot rules in
330
+ > `<id>/CLAUDE.md` and shared rules in `~/.cc-channel-octo/CLAUDE.md`. (This is
331
+ > also why `settingSources` stays `['project']`, not `['user']` — `user` would
332
+ > additionally pull in the host's personal `~/.claude` config/skills.)
333
+
334
+ **Prerequisites are the operator's responsibility, out-of-band:** install the
335
+ CLI a skill needs (`npm i -g @mininglamp-oss/octo-cli`, etc.) and authenticate it
336
+ (`octo-cli auth login`, `gh auth login`). **cc never handles credentials** — the
337
+ agent only runs the already-authenticated CLI. Skills are operator-owned and
338
+ trusted (like `SOUL.md`/`GROUP.md`); since their contents are visible to the
339
+ model, **never put secrets in a skill file**.
340
+
341
+ **Multi-bot tool identity.** When several bots share one CLI whose credential
342
+ store keys by identity (e.g. `octo-cli`, which needs `--bot-id`/`OCTO_BOT_ID` to
343
+ pick among ≥2 stored profiles), give each bot its selector via `sdk.env` in its
344
+ per-bot config.json — e.g. `{ "sdk": { "env": { "OCTO_BOT_ID": "<robotId>" } } }`.
345
+ cc injects it into that bot's tool subprocess so the CLI acts as the right bot.
346
+
347
+ ### Scheduled tasks
348
+
349
+ Enable `sdk.cron: true` to give the agent a `cron` tool set so it can schedule
350
+ work — the missing "non-IM trigger" that makes a bot more than purely reactive.
351
+
352
+ ```jsonc
353
+ // ~/.cc-channel-octo/<id>/config.json
354
+ { "sdk": { "cron": true } }
355
+ ```
356
+
357
+ The agent calls:
358
+ - `cron_create(schedule, prompt, recurring?)` — `schedule` is a 5-field cron
359
+ expression (`"0 9 * * 1-5"` = weekdays 9am) or a one-shot ISO datetime
360
+ (`"2026-06-09T09:00:00Z"`). Cron fields use the **gateway's local timezone**
361
+ (set `TZ` on the process); ISO datetimes are absolute instants.
362
+ - `cron_list` / `cron_delete(id)`.
363
+
364
+ Tasks persist to `<baseDir>/<id>/cron.json` and survive restarts. When a task is
365
+ due, the gateway scheduler re-runs its `prompt` **through the normal message
366
+ pipeline, bound to the chat that created it** — so the result posts back in that
367
+ same channel, exactly as if the prompt had arrived as a message.
368
+
369
+ > **Security.** The `cron_create`/`cron_delete` **owner check** (`registerBot.owner_uid`)
370
+ > stops the agent from *casually* registering a task for a non-owner — but it is
371
+ > **not a hard boundary**: under the default `bypassPermissions` + `allowedTools: "*"`
372
+ > the agent can `Write` `cron.json` directly. That's inherent to a full-tool bot
373
+ > (it can already run any command), so **only enable `sdk.cron` for bots you'd
374
+ > already trust with full tools** (your own DM, trusted-team rooms). For an
375
+ > untrusted-input bot, restrict `allowedTools` instead — a cron-specific lock
376
+ > would be false assurance. A fired task bypasses the group @mention gate
377
+ > (authenticated by a per-process nonce so a real inbound payload can't forge it)
378
+ > and is still rate-limited; it is itself offered the cron tools, so it can
379
+ > self-schedule.
380
+
381
+ ### Multi-bot
382
+
383
+ To run several bots from one process, list them in the global config's `bots`
384
+ array. Each `id` is a slug (letters, digits, `.`, `_`, `-`) that names the bot's
385
+ subtree under `baseDir`; each bot's **token + overrides live in its own
386
+ `<baseDir>/<id>/config.json`** (the highest-priority layer):
387
+
388
+ Global `~/.cc-channel-octo/config.json`:
389
+
390
+ ```jsonc
391
+ {
392
+ "apiUrl": "https://your-octo-instance.com",
393
+ "bots": [
394
+ { "id": "support" },
395
+ { "id": "ops", "model": "vertexai/claude-opus-4-8" }
396
+ ]
397
+ }
398
+ ```
399
+
400
+ Per-bot `~/.cc-channel-octo/support/config.json` and `~/.cc-channel-octo/ops/config.json`:
401
+
402
+ ```jsonc
403
+ { "botToken": "bf_AAA" }
404
+ ```
405
+
406
+ Each bot runs a fully independent stack (gateway, router, store). Its
407
+ `data`/`workspace`/`memory` are derived as `<baseDir>/<id>/…`, so **bots never
408
+ share conversation history, working directories, or memory** — the isolation is
409
+ structural and not overridable. A bot may override shared fields (`apiUrl`,
410
+ `model`, `systemPrompt`, `botBlocklist`, mention lists)
411
+ in its inline `bots[]` entry or, with higher priority, its per-bot config.json.
412
+ A single bot is just one entry (conventionally `id: "default"`).
413
+
414
+ ## Security Model
415
+
416
+ cc-channel-octo runs Claude Code in **headless automation mode**. There is no terminal for interactive permission prompts, so `bypassPermissions` is the default. Security relies on two mechanisms:
417
+
418
+ ### 1. `allowedTools` Whitelist
419
+
420
+ The `allowedTools` field accepts either the wildcard `"*"` (allow every tool
421
+ the SDK exposes — the default) or an explicit string array whitelist. Reduce
422
+ the list to reduce risk:
423
+
424
+ | Profile | `allowedTools` | Risk Level |
425
+ |---------|---------------|------------|
426
+ | **Full** (default) | `"*"` | High — every SDK tool available |
427
+ | **Explicit full** | `["Read", "Write", "Edit", "Bash", "Glob", "Grep", "WebFetch", "WebSearch"]` | High — same surface, pinned list |
428
+ | **No network** | `["Read", "Write", "Edit", "Bash", "Glob", "Grep"]` | Medium — no SSRF risk |
429
+ | **No shell** | `["Read", "Write", "Edit", "Glob", "Grep"]` | Medium — no arbitrary commands |
430
+ | **Read-only** | `["Read", "Glob", "Grep"]` | Low — code reading only |
431
+
432
+ ### 2. Workspace Isolation (derived `workspace/`)
433
+
434
+ **Each bot's `workspace/` (`<baseDir>/<botId>/workspace`) is the parent under
435
+ which each session gets its own hashed sandbox.** A 16-hex SHA-256 subdirectory
436
+ is derived from the same per-session key used for conversation history, so
437
+ isolation matches the session granularity: **per DM peer**, and **per group
438
+ channel** — a whole group shares one sandbox (all members work in the same tree,
439
+ by design; a group is a shared workspace). Different DM peers, and different
440
+ groups, cannot read or mutate each other's working trees, and different **bots**
441
+ are fully isolated by their separate subtrees. Subdirectories idle for more than
442
+ 7 days (from the last inbound message) are cleaned up automatically every 6 hours.
443
+
444
+ **Limitation — cwd is a starting directory, not a chroot.** With `Bash`/`Read`
445
+ in the tool set and `bypassPermissions`, the agent can still be instructed to
446
+ read absolute paths outside the sandbox (e.g. `/etc/passwd`). Per-session
447
+ sandboxing partitions sessions from *each other*; it does not confine a single
448
+ session to its directory. For a hard boundary, run the gateway as an
449
+ unprivileged user in a container/VM and tighten `allowedTools` (drop `Bash`).
450
+
451
+ Because the layout is fixed under `~/.cc-channel-octo/`, the bot's own
452
+ `config.json` (with the token) lives in the bot subtree root, a **sibling** of
453
+ `workspace/` — never inside it. Keep other secrets out of the bot subtree too;
454
+ treat `workspace/` as untrusted ground that any user who can message the bot can
455
+ read within their own session sandbox.
456
+
457
+ ### Built-in System Prompt
458
+
459
+ A default system prompt instructs Claude to treat input as untrusted and decline requests for sensitive file reads or credential exfiltration. This is a **soft constraint** (model-level guidance), not a security boundary. The `allowedTools` whitelist and per-session workspace isolation are the real security controls.
460
+
461
+ ### Bot Loop Prevention
462
+
463
+ - The bot ignores its own messages (by `robot_id`).
464
+ - Configure `botBlocklist` with UIDs of other bots to prevent DM ping-pong loops.
465
+ - In group chats, bot messages are cached as context but do not trigger AI processing (unless explicitly @mentioned).
466
+
467
+ ## Architecture
468
+
469
+ ```
470
+ Octo User
471
+ ↓ WuKongIM WebSocket (binary, AES-CBC encrypted)
472
+ Gateway ─── bot registration, token refresh, heartbeat, process lock
473
+
474
+ SessionRouter ─── session key routing, mention gating, rate limiting
475
+
476
+ AgentBridge ─── prompt construction → Claude Agent SDK query()
477
+ ↓ AsyncIterable<string>
478
+ StreamRelay ─── 800ms throttled flush, typing heartbeat, message splitting
479
+
480
+ Octo REST API
481
+
482
+ Octo User
483
+ ```
484
+
485
+ The gateway connects to Octo via the WuKongIM binary protocol (DH key exchange + AES-CBC encryption). Inbound messages are routed through the session router, which enforces @mention gating for groups and per-session rate limiting. The agent bridge constructs prompts from conversation history and group context, then calls the Claude Agent SDK. Responses stream back through the stream relay, which throttles output and handles Octo's stream API lifecycle.
486
+
487
+ See [ARCHITECTURE.md](./ARCHITECTURE.md) for the full design document, and
488
+ [RUNTIME.md](./RUNTIME.md) for how cc provides an openclaw-style agent runtime on
489
+ top of the Claude Agent SDK (identity, memory, sessions, skills — and the gaps).
490
+
491
+ ## Development
492
+
493
+ ```bash
494
+ # Install dependencies (sets up husky pre-commit hook via `prepare` script)
495
+ npm install
496
+
497
+ # Type-check
498
+ npm run type-check
499
+
500
+ # Lint (zero warnings enforced)
501
+ npm run lint
502
+
503
+ # Run tests
504
+ npm test
505
+
506
+ # Watch mode
507
+ npm run test:watch
508
+
509
+ # Coverage report (HTML + lcov in coverage/)
510
+ npm run test:coverage
511
+
512
+ # Build
513
+ npm run build
514
+
515
+ # Start (after build)
516
+ npm start
517
+ ```
518
+
519
+ ### Quality gates
520
+
521
+ This repo enforces three layers of automated checks:
522
+
523
+ 1. **Pre-commit (`.husky/pre-commit`)** — runs `lint-staged` (ESLint on staged files with `--max-warnings 0`) and `tsc --noEmit`. Set up automatically by `npm install`.
524
+ 2. **CI (`.github/workflows/ci.yml`)** — every PR runs `type-check`, `lint`, `test`, and `test:coverage` as separate jobs. PRs cannot merge if any job fails.
525
+ 3. **Strict TypeScript** — `noUnusedLocals`, `noUnusedParameters`, and full `strict` mode are on. Dead code fails the build.
526
+
527
+ Coverage artifacts are uploaded per CI run (retained 14 days). No hard threshold yet — baselines are being established.
528
+
529
+ Reviewers must follow [`docs/REVIEW_CHECKLIST.md`](./docs/REVIEW_CHECKLIST.md)
530
+ on security-adjacent PRs. The 9-item checklist is distilled from Stage 6
531
+ review-process failures and codifies hard-won rules like "reproduction test
532
+ before APPROVED", "refresh reviews state before clicking APPROVED",
533
+ "enumerate canonical-equivalent forms for attacker-input validation", etc.
534
+
535
+ ### Project Structure
536
+
537
+ ```
538
+ src/
539
+ ├── index.ts # Entry point — orchestrates all modules
540
+ ├── config.ts # Three-level config loading (env > file > defaults)
541
+ ├── gateway.ts # WKSocket lifecycle, bot registration, token refresh
542
+ ├── session-router.ts # Session routing, mention gating, rate limiting
543
+ ├── agent-bridge.ts # Claude Agent SDK integration
544
+ ├── session-store.ts # SQLite session + message persistence
545
+ ├── group-context.ts # Group message cache, member mapping, mention resolution
546
+ ├── stream-relay.ts # Throttled streaming output + fallback
547
+ ├── db-adapter.ts # SQLite adapter interface (better-sqlite3)
548
+ └── octo/
549
+ ├── socket.ts # WuKongIM binary protocol (forked from openclaw-channel-octo)
550
+ ├── api.ts # Octo Bot REST API client
551
+ └── types.ts # Protocol type definitions
552
+ ```
553
+
554
+ ## Known Limitations (v1.0)
555
+
556
+ - **Per-session workspace isolation** — Each session gets its own SHA-256 hex sandbox under the bot's `workspace/` (`<baseDir>/<botId>/workspace`), partitioned by the same key as conversation history — **per DM peer** and **per group channel** (a whole group shares one sandbox by design). Idle sandboxes (>7d) are auto-cleaned every 6h. Note: it separates sessions from each other but does not confine a session to its directory (absolute-path reads via Bash/Read remain possible) — see the Security Model section.
557
+ - **Groups are a shared workspace** — All members of a group share one history, one sandbox, and one auto-memory store (the session key is the channel id). There is **no member-to-member isolation within a group**; DM sessions remain private per peer.
558
+ - **Auto-memory is not TTL-reclaimed** — Long-term memory lives at `<baseDir>/<botId>/memory` (a sibling of `workspace/`) and is never swept by the cwd janitor, so it persists across `/reset` and grows unbounded on long-lived deploys.
559
+ - **SDK session owns conversation history** — every turn resumes the stored SDK session (the source of truth for history + workspace state); the system prompt is frozen (no per-turn history/context, so the prompt cache hits). A session's first turn — or a migration from existing SQLite history — injects prior history once into the user message; later turns rely on resume. A stale/expired session id is recovered automatically (cleared + retried with history re-injected). SQLite keeps a durable record for migration/recovery, not live prompt history.
560
+
561
+ ## Roadmap
562
+
563
+ | Version | Scope |
564
+ |---------|-------|
565
+ | **v0.1** | Text messaging, streaming, session persistence, rate limiting, security model |
566
+ | **v0.2** | Media reception & sending (image/file/RichText), @mention, group context, per-session `cwdBase` isolation, self-hosted gateway, SSRF/prompt-injection hardening |
567
+ | **v1.0** *(current)* | Slash commands, tool progress, multi-bot, SDK-session-owned history, GROUP.md per-group instructions, scheduled tasks (cron), skill-as-data external tooling, JSON-only config |
568
+
569
+ ## Contributing
570
+
571
+ See [CONTRIBUTING.md](./CONTRIBUTING.md) for development setup, coding standards, and the PR process.
572
+
573
+ ## License
574
+
575
+ [Apache-2.0](./LICENSE)
576
+
577
+ Copyright (c) 2026 Mininglamp-OSS
@@ -0,0 +1,15 @@
1
+ {
2
+ "_comment": "PER-BOT config — install at ~/.cc-channel-octo/<id>/config.json (the <id> must match an entry in the global config's `bots` list; for a single bot use id 'default'). This file is the highest-priority layer: it overrides both the inline bots[] entry and the global shared fields. It holds the bot's REQUIRED botToken. The bot's directories are derived automatically as siblings of this file: ./data, ./workspace, ./memory. Drop a ./SOUL.md next to this file to give the bot a personality (it overrides sdk.systemPrompt).",
3
+ "botToken": "bf_YOUR_BOT_TOKEN",
4
+
5
+ "_comment_overrides": "Any of these optional fields override the global config for THIS bot only:",
6
+ "_comment_apiUrl": "apiUrl — override the Octo API base for this bot.",
7
+
8
+ "sdk": {
9
+ "_comment_model": "Override the model for this bot, e.g. 'vertexai/claude-opus-4-8'.",
10
+ "_comment_systemPrompt": "Inline personality string. A ./SOUL.md file next to this config overrides it.",
11
+ "_comment_skills": "Per-bot skill selection: 'all' or a string[] of skill names from the shared library (~/.cc-channel-octo/skills + this bot's ./skills).",
12
+ "_comment_env": "Extra env injected into the agent's tool subprocess, e.g. {\"OCTO_BOT_ID\":\"<robotId>\"} so a shared CLI acts as this bot.",
13
+ "_comment_cron": "Set cron=true to let the agent schedule tasks (cron_create/list/delete → ./cron.json, owner-gated)."
14
+ }
15
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "_comment": "GLOBAL config — install at ~/.cc-channel-octo/config.json. Holds SHARED defaults + the list of bots to run. It does NOT hold a botToken: each bot's token lives in ~/.cc-channel-octo/<id>/config.json (see config.bot.example.json). The directory containing THIS file is the baseDir; every bot is a self-contained subtree at <baseDir>/<id>/{config.json, SOUL.md, data/, workspace/, memory/}, all auto-created. Per-bot dirs are NOT configurable — they are always derived from baseDir + id, so a bot can never escape its subtree.",
3
+ "apiUrl": "https://your-octo-instance.com",
4
+
5
+ "_comment_bots": "Which bots to run. Each entry's `id` selects its subtree <baseDir>/<id>/ and its per-bot config.json. A single bot is just one entry, conventionally id 'default'. Per-bot config.json overrides anything here (and these shared fields). You MAY put shared inline overrides on an entry (model, etc.), but the token belongs in the per-bot file.",
6
+ "bots": [
7
+ { "id": "default" }
8
+ ],
9
+
10
+ "_comment_group_config_dir": "v1.0: optional directory of per-group instruction files (<groupId>.md), injected as trusted custom instructions. Operator-controlled — keep it OUTSIDE every bot's workspace (the agent can write there) AND non-writable by the gateway user. Omit to disable.",
11
+
12
+ "sdk": {
13
+ "_comment_anthropic_base_url": "Q1: Optional self-hosted Claude API gateway base URL. Forwarded to the SDK subprocess (scoped). Must be https:// (or http://localhost for dev) — SSRF-validated at boot. Omit to use Anthropic's public endpoint.",
14
+ "_comment_allowed_tools": "Q2: either '*' (allow every tool the SDK exposes) or an explicit string[] whitelist.",
15
+ "allowedTools": "*",
16
+ "permissionMode": "bypassPermissions",
17
+ "_comment_tool_progress": "v0.3: set toolProgress=true to post '🔧 Running <tool>(<params>)…' notices (params truncated). Off by default.",
18
+ "_comment_setting_sources": "#100: which host filesystem settings the SDK loads (user=~/.claude, project=.claude, local=.claude/*.local). Default [\"project\"] so the SDK discovers skills symlinked into each session sandbox's .claude/skills/. Memory stays isolated regardless (the auto-memory dir is pinned via inline settings, ranked above projectSettings). Add \"user\" only to deliberately load the operator's real ~/.claude.",
19
+ "settingSources": ["project"]
20
+ },
21
+
22
+ "rateLimit": {
23
+ "maxPerMinute": 5
24
+ },
25
+
26
+ "context": {
27
+ "maxContextChars": 6000,
28
+ "historyLimit": 40
29
+ },
30
+
31
+ "_comment_mention_free": "G12: group_ids listed here process every text message without requiring an @bot mention.",
32
+ "mentionFreeGroups": []
33
+ }