typeclaw 0.1.0

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 (213) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +134 -0
  3. package/auth.schema.json +63 -0
  4. package/cron.schema.json +96 -0
  5. package/package.json +72 -0
  6. package/scripts/emit-base-dockerfile.ts +5 -0
  7. package/scripts/generate-schema.ts +34 -0
  8. package/secrets.schema.json +63 -0
  9. package/src/agent/auth.ts +119 -0
  10. package/src/agent/compaction.ts +35 -0
  11. package/src/agent/git-nudge.ts +95 -0
  12. package/src/agent/index.ts +451 -0
  13. package/src/agent/plugin-tools.ts +269 -0
  14. package/src/agent/reload-tool.ts +71 -0
  15. package/src/agent/self.ts +45 -0
  16. package/src/agent/session-origin.ts +288 -0
  17. package/src/agent/subagents.ts +253 -0
  18. package/src/agent/system-prompt.ts +68 -0
  19. package/src/agent/tools/channel-fetch-attachment.ts +118 -0
  20. package/src/agent/tools/channel-history.ts +119 -0
  21. package/src/agent/tools/channel-reply.ts +182 -0
  22. package/src/agent/tools/channel-send.ts +212 -0
  23. package/src/agent/tools/ddg.ts +218 -0
  24. package/src/agent/tools/restart.ts +122 -0
  25. package/src/agent/tools/stream-snapshot.ts +181 -0
  26. package/src/agent/tools/webfetch/fetch.ts +102 -0
  27. package/src/agent/tools/webfetch/index.ts +1 -0
  28. package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
  29. package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
  30. package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
  31. package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
  32. package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
  33. package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
  34. package/src/agent/tools/webfetch/tool.ts +281 -0
  35. package/src/agent/tools/webfetch/types.ts +33 -0
  36. package/src/agent/tools/websearch.ts +96 -0
  37. package/src/agent/tools/wikipedia.ts +52 -0
  38. package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
  39. package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
  40. package/src/bundled-plugins/agent-browser/index.ts +179 -0
  41. package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
  42. package/src/bundled-plugins/agent-browser/shim.ts +152 -0
  43. package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
  44. package/src/bundled-plugins/guard/index.ts +26 -0
  45. package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
  46. package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
  47. package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
  48. package/src/bundled-plugins/guard/policy.ts +18 -0
  49. package/src/bundled-plugins/memory/README.md +71 -0
  50. package/src/bundled-plugins/memory/append-tool.ts +84 -0
  51. package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
  52. package/src/bundled-plugins/memory/dreaming.ts +470 -0
  53. package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
  54. package/src/bundled-plugins/memory/index.ts +238 -0
  55. package/src/bundled-plugins/memory/load-memory.ts +122 -0
  56. package/src/bundled-plugins/memory/memory-logger.ts +257 -0
  57. package/src/bundled-plugins/memory/secret-detector.ts +49 -0
  58. package/src/bundled-plugins/memory/watermark.ts +15 -0
  59. package/src/bundled-plugins/security/index.ts +35 -0
  60. package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
  61. package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
  62. package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
  63. package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
  64. package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
  65. package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
  66. package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
  67. package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
  68. package/src/bundled-plugins/security/policy.ts +9 -0
  69. package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
  70. package/src/channels/adapters/discord-bot-classify.ts +148 -0
  71. package/src/channels/adapters/discord-bot.ts +640 -0
  72. package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
  73. package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
  74. package/src/channels/adapters/kakaotalk-classify.ts +77 -0
  75. package/src/channels/adapters/kakaotalk.ts +622 -0
  76. package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
  77. package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
  78. package/src/channels/adapters/slack-bot-classify.ts +213 -0
  79. package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
  80. package/src/channels/adapters/slack-bot-time.ts +10 -0
  81. package/src/channels/adapters/slack-bot.ts +881 -0
  82. package/src/channels/adapters/telegram-bot-classify.ts +155 -0
  83. package/src/channels/adapters/telegram-bot-format.ts +309 -0
  84. package/src/channels/adapters/telegram-bot.ts +604 -0
  85. package/src/channels/engagement.ts +227 -0
  86. package/src/channels/index.ts +21 -0
  87. package/src/channels/manager.ts +292 -0
  88. package/src/channels/membership-cache.ts +116 -0
  89. package/src/channels/membership-from-history.ts +53 -0
  90. package/src/channels/membership.ts +30 -0
  91. package/src/channels/participants.ts +47 -0
  92. package/src/channels/persistence.ts +209 -0
  93. package/src/channels/reloadable.ts +28 -0
  94. package/src/channels/router.ts +1570 -0
  95. package/src/channels/schema.ts +273 -0
  96. package/src/channels/types.ts +160 -0
  97. package/src/cli/channel.ts +403 -0
  98. package/src/cli/compose-status.ts +95 -0
  99. package/src/cli/compose.ts +240 -0
  100. package/src/cli/hostd.ts +163 -0
  101. package/src/cli/index.ts +27 -0
  102. package/src/cli/init.ts +592 -0
  103. package/src/cli/logs.ts +38 -0
  104. package/src/cli/reload.ts +68 -0
  105. package/src/cli/restart.ts +66 -0
  106. package/src/cli/run.ts +77 -0
  107. package/src/cli/shell.ts +33 -0
  108. package/src/cli/start.ts +57 -0
  109. package/src/cli/status.ts +178 -0
  110. package/src/cli/stop.ts +31 -0
  111. package/src/cli/tui.ts +35 -0
  112. package/src/cli/ui.ts +110 -0
  113. package/src/commands/index.ts +74 -0
  114. package/src/compose/discover.ts +43 -0
  115. package/src/compose/index.ts +25 -0
  116. package/src/compose/logs.ts +162 -0
  117. package/src/compose/restart.ts +69 -0
  118. package/src/compose/start.ts +62 -0
  119. package/src/compose/status.ts +28 -0
  120. package/src/compose/stop.ts +43 -0
  121. package/src/config/config.ts +424 -0
  122. package/src/config/index.ts +25 -0
  123. package/src/config/providers.ts +234 -0
  124. package/src/config/reloadable.ts +47 -0
  125. package/src/container/index.ts +27 -0
  126. package/src/container/logs.ts +37 -0
  127. package/src/container/port.ts +137 -0
  128. package/src/container/shared.ts +290 -0
  129. package/src/container/shell.ts +58 -0
  130. package/src/container/start.ts +670 -0
  131. package/src/container/status.ts +76 -0
  132. package/src/container/stop.ts +120 -0
  133. package/src/container/verify-running.ts +149 -0
  134. package/src/cron/consumer.ts +138 -0
  135. package/src/cron/index.ts +54 -0
  136. package/src/cron/reloadable.ts +64 -0
  137. package/src/cron/scheduler.ts +200 -0
  138. package/src/cron/schema.ts +96 -0
  139. package/src/hostd/client.ts +113 -0
  140. package/src/hostd/daemon.ts +587 -0
  141. package/src/hostd/index.ts +25 -0
  142. package/src/hostd/paths.ts +82 -0
  143. package/src/hostd/portbroker-manager.ts +101 -0
  144. package/src/hostd/protocol.ts +48 -0
  145. package/src/hostd/spawn.ts +224 -0
  146. package/src/hostd/supervisor.ts +60 -0
  147. package/src/hostd/tailscale.ts +172 -0
  148. package/src/hostd/version.ts +115 -0
  149. package/src/init/dockerfile.ts +327 -0
  150. package/src/init/ensure-deps.ts +152 -0
  151. package/src/init/gitignore.ts +46 -0
  152. package/src/init/hatching.ts +60 -0
  153. package/src/init/index.ts +786 -0
  154. package/src/init/kakaotalk-auth.ts +114 -0
  155. package/src/init/models-dev.ts +130 -0
  156. package/src/init/oauth-login.ts +74 -0
  157. package/src/init/packagejson.ts +94 -0
  158. package/src/init/paths.ts +2 -0
  159. package/src/init/run-bun-install.ts +20 -0
  160. package/src/markdown/chunk.ts +299 -0
  161. package/src/markdown/index.ts +1 -0
  162. package/src/plugin/context.ts +40 -0
  163. package/src/plugin/define.ts +35 -0
  164. package/src/plugin/hooks.ts +204 -0
  165. package/src/plugin/index.ts +63 -0
  166. package/src/plugin/loader.ts +111 -0
  167. package/src/plugin/manager.ts +136 -0
  168. package/src/plugin/registry.ts +145 -0
  169. package/src/plugin/skills.ts +62 -0
  170. package/src/plugin/types.ts +172 -0
  171. package/src/portbroker/bind-with-forward.ts +102 -0
  172. package/src/portbroker/container-server.ts +305 -0
  173. package/src/portbroker/forward-result-bus.ts +36 -0
  174. package/src/portbroker/hostd-client.ts +443 -0
  175. package/src/portbroker/index.ts +33 -0
  176. package/src/portbroker/policy.ts +24 -0
  177. package/src/portbroker/proc-net-tcp.ts +72 -0
  178. package/src/portbroker/protocol.ts +39 -0
  179. package/src/reload/client.ts +59 -0
  180. package/src/reload/index.ts +3 -0
  181. package/src/reload/registry.ts +60 -0
  182. package/src/reload/types.ts +13 -0
  183. package/src/run/bundled-plugins.ts +24 -0
  184. package/src/run/channel-session-factory.ts +105 -0
  185. package/src/run/index.ts +432 -0
  186. package/src/run/plugin-runtime.ts +43 -0
  187. package/src/run/schema-with-plugins.ts +14 -0
  188. package/src/secrets/index.ts +13 -0
  189. package/src/secrets/migrate.ts +95 -0
  190. package/src/secrets/schema.ts +75 -0
  191. package/src/secrets/storage.ts +231 -0
  192. package/src/server/index.ts +436 -0
  193. package/src/sessions/index.ts +23 -0
  194. package/src/shared/index.ts +9 -0
  195. package/src/shared/local-time.ts +21 -0
  196. package/src/shared/protocol.ts +25 -0
  197. package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
  198. package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
  199. package/src/skills/typeclaw-config/SKILL.md +643 -0
  200. package/src/skills/typeclaw-cron/SKILL.md +159 -0
  201. package/src/skills/typeclaw-git/SKILL.md +89 -0
  202. package/src/skills/typeclaw-memory/SKILL.md +174 -0
  203. package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
  204. package/src/skills/typeclaw-plugins/SKILL.md +594 -0
  205. package/src/skills/typeclaw-skills/SKILL.md +246 -0
  206. package/src/stream/broker.ts +161 -0
  207. package/src/stream/index.ts +16 -0
  208. package/src/stream/types.ts +69 -0
  209. package/src/tui/client.ts +45 -0
  210. package/src/tui/format.ts +317 -0
  211. package/src/tui/index.ts +225 -0
  212. package/src/tui/theme.ts +41 -0
  213. package/typeclaw.schema.json +826 -0
@@ -0,0 +1,299 @@
1
+ // Splits a Markdown document into chunks of bounded length while preserving
2
+ // the integrity of structural blocks that would render incorrectly if cut
3
+ // in the middle: code fences, tables, and (best-effort) blockquotes. The
4
+ // algorithm is intentionally a line-walker rather than a full Markdown
5
+ // parser — round-tripping through an AST normalizes whitespace and list
6
+ // markers, which would change the user-visible text and break byte-for-byte
7
+ // fidelity.
8
+ //
9
+ // Atomicity rules:
10
+ // - Code fence (``` or ~~~): one atomic block. If a single fence exceeds
11
+ // `maxLen`, it splits with the fence reopened on the next chunk so each
12
+ // half is independently parseable.
13
+ // - Pipe table (|---| separator on second line): one atomic block. If a
14
+ // single table exceeds `maxLen`, it is emitted whole as one oversize
15
+ // chunk — splitting a table is worse than downstream rejecting one
16
+ // oversize message.
17
+ // - Blockquote: one atomic block. Partial blockquotes look visually wrong
18
+ // even when syntactically fine.
19
+ // - Lists: packed as one block but splittable BETWEEN items if a single
20
+ // list exceeds `maxLen`.
21
+ // - Plain paragraphs: split on `\n\n` → `\n` → sentence → hard cut.
22
+
23
+ const FENCE_RE = /^(\s*)(```+|~~~+)(.*)$/
24
+ const TABLE_SEP_RE = /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/
25
+ const BLOCKQUOTE_RE = /^\s{0,3}>\s?/
26
+ const LIST_ITEM_RE = /^\s{0,3}(?:[-*+]|\d+[.)])\s+/
27
+ const FENCE_OVERHEAD_NEWLINES = 2
28
+
29
+ type Block = { kind: 'atomic' | 'list' | 'prose'; text: string }
30
+
31
+ export function chunkMarkdown(text: string, maxLen: number): string[] {
32
+ if (!Number.isFinite(maxLen) || maxLen <= 0) {
33
+ throw new Error(`chunkMarkdown: maxLen must be a positive finite number, got ${maxLen}`)
34
+ }
35
+ if (text === '') return ['']
36
+ if (text.length <= maxLen) return [text]
37
+
38
+ const blocks = tokenize(text)
39
+ return packBlocks(blocks, maxLen)
40
+ }
41
+
42
+ function tokenize(text: string): Block[] {
43
+ const lines = text.split('\n')
44
+ const blocks: Block[] = []
45
+ let i = 0
46
+
47
+ while (i < lines.length) {
48
+ const line = lines[i]!
49
+
50
+ // Close fence must use the same character (` vs ~) and be at least as
51
+ // long as the opener; shorter closes are not closes per CommonMark.
52
+ const fenceOpen = FENCE_RE.exec(line)
53
+ if (fenceOpen !== undefined && fenceOpen !== null) {
54
+ const fenceChar = fenceOpen[2]![0]!
55
+ const fenceLen = fenceOpen[2]!.length
56
+ const start = i
57
+ i++
58
+ while (i < lines.length) {
59
+ const close = FENCE_RE.exec(lines[i]!)
60
+ if (close && close[2]![0] === fenceChar && close[2]!.length >= fenceLen) {
61
+ i++
62
+ break
63
+ }
64
+ i++
65
+ }
66
+ blocks.push({ kind: 'atomic', text: lines.slice(start, i).join('\n') })
67
+ continue
68
+ }
69
+
70
+ // We require the alignment row on line N+1 to disambiguate from prose
71
+ // paragraphs that happen to contain a leading `|`.
72
+ if (line.includes('|') && i + 1 < lines.length && TABLE_SEP_RE.test(lines[i + 1]!)) {
73
+ const start = i
74
+ i += 2
75
+ while (i < lines.length && lines[i]!.includes('|') && lines[i]!.trim() !== '') {
76
+ i++
77
+ }
78
+ blocks.push({ kind: 'atomic', text: lines.slice(start, i).join('\n') })
79
+ continue
80
+ }
81
+
82
+ if (BLOCKQUOTE_RE.test(line)) {
83
+ const start = i
84
+ while (i < lines.length && (BLOCKQUOTE_RE.test(lines[i]!) || lines[i]!.trim() === '')) {
85
+ // A blank line followed by a non-blockquote line ends the quote.
86
+ if (lines[i]!.trim() === '' && (i + 1 >= lines.length || !BLOCKQUOTE_RE.test(lines[i + 1]!))) {
87
+ break
88
+ }
89
+ i++
90
+ }
91
+ blocks.push({ kind: 'atomic', text: lines.slice(start, i).join('\n') })
92
+ continue
93
+ }
94
+
95
+ if (LIST_ITEM_RE.test(line)) {
96
+ const start = i
97
+ while (i < lines.length) {
98
+ const cur = lines[i]!
99
+ if (LIST_ITEM_RE.test(cur)) {
100
+ i++
101
+ continue
102
+ }
103
+ // Allow a single blank line inside a list (loose list); double blank ends.
104
+ if (cur.trim() === '') {
105
+ if (i + 1 < lines.length && LIST_ITEM_RE.test(lines[i + 1]!)) {
106
+ i++
107
+ continue
108
+ }
109
+ break
110
+ }
111
+ if (/^\s{2,}/.test(cur)) {
112
+ i++
113
+ continue
114
+ }
115
+ break
116
+ }
117
+ blocks.push({ kind: 'list', text: lines.slice(start, i).join('\n') })
118
+ continue
119
+ }
120
+
121
+ const start = i
122
+ while (i < lines.length && lines[i]!.trim() !== '') {
123
+ i++
124
+ }
125
+ blocks.push({ kind: 'prose', text: lines.slice(start, i).join('\n') })
126
+ while (i < lines.length && lines[i]!.trim() === '') {
127
+ i++
128
+ }
129
+ }
130
+
131
+ return blocks.filter((b) => b.text !== '')
132
+ }
133
+
134
+ function packBlocks(blocks: Block[], maxLen: number): string[] {
135
+ const chunks: string[] = []
136
+ let current = ''
137
+
138
+ const flush = () => {
139
+ if (current !== '') {
140
+ chunks.push(current.replace(/\n+$/, ''))
141
+ current = ''
142
+ }
143
+ }
144
+
145
+ for (const block of blocks) {
146
+ const sep = current === '' ? '' : '\n\n'
147
+ const candidateLen = current.length + sep.length + block.text.length
148
+
149
+ if (candidateLen <= maxLen) {
150
+ current += sep + block.text
151
+ continue
152
+ }
153
+
154
+ flush()
155
+
156
+ if (block.text.length <= maxLen) {
157
+ current = block.text
158
+ continue
159
+ }
160
+
161
+ const split = splitOversize(block, maxLen)
162
+ for (let i = 0; i < split.length - 1; i++) {
163
+ chunks.push(split[i]!)
164
+ }
165
+ current = split[split.length - 1] ?? ''
166
+ }
167
+
168
+ flush()
169
+ return chunks.length === 0 ? [''] : chunks
170
+ }
171
+
172
+ function splitOversize(block: Block, maxLen: number): string[] {
173
+ if (block.kind === 'atomic') {
174
+ return splitOversizeAtomic(block.text, maxLen)
175
+ }
176
+ if (block.kind === 'list') {
177
+ return splitList(block.text, maxLen)
178
+ }
179
+ return splitProse(block.text, maxLen)
180
+ }
181
+
182
+ function splitOversizeAtomic(text: string, maxLen: number): string[] {
183
+ // Fence splitting: re-emit the language tag on each chunk so syntax
184
+ // highlighting survives the split.
185
+ const openMatch = FENCE_RE.exec(text.split('\n')[0] ?? '')
186
+ if (openMatch !== undefined && openMatch !== null) {
187
+ const fenceMarker = openMatch[2]!
188
+ const lang = openMatch[3] ?? ''
189
+ const open = `${fenceMarker}${lang}`
190
+ const close = fenceMarker
191
+
192
+ const lines = text.split('\n')
193
+ const innerStart = 1
194
+ let innerEnd = lines.length
195
+ if (innerEnd > innerStart) {
196
+ const lastLine = lines[innerEnd - 1]!
197
+ const lastClose = FENCE_RE.exec(lastLine)
198
+ if (
199
+ lastClose !== undefined &&
200
+ lastClose !== null &&
201
+ lastClose[2]![0] === fenceMarker[0] &&
202
+ lastClose[2]!.length >= fenceMarker.length
203
+ ) {
204
+ innerEnd--
205
+ }
206
+ }
207
+ const inner = lines.slice(innerStart, innerEnd).join('\n')
208
+ const overhead = open.length + close.length + FENCE_OVERHEAD_NEWLINES
209
+ if (overhead >= maxLen) {
210
+ return [text]
211
+ }
212
+ const innerBudget = maxLen - overhead
213
+ const innerChunks = splitProse(inner, innerBudget)
214
+ return innerChunks.map((c) => `${open}\n${c}\n${close}`)
215
+ }
216
+
217
+ // Tables and other atomic blocks: keep whole, accept oversize.
218
+ return [text]
219
+ }
220
+
221
+ function splitList(text: string, maxLen: number): string[] {
222
+ const lines = text.split('\n')
223
+ const items: string[] = []
224
+ let current: string[] = []
225
+ for (const line of lines) {
226
+ if (LIST_ITEM_RE.test(line) && current.length > 0) {
227
+ items.push(current.join('\n'))
228
+ current = []
229
+ }
230
+ current.push(line)
231
+ }
232
+ if (current.length > 0) items.push(current.join('\n'))
233
+
234
+ const chunks: string[] = []
235
+ let buffer = ''
236
+ for (const item of items) {
237
+ const sep = buffer === '' ? '' : '\n'
238
+ const candidateLen = buffer.length + sep.length + item.length
239
+ if (candidateLen <= maxLen) {
240
+ buffer += sep + item
241
+ continue
242
+ }
243
+ if (buffer !== '') chunks.push(buffer)
244
+ if (item.length <= maxLen) {
245
+ buffer = item
246
+ } else {
247
+ const proseChunks = splitProse(item, maxLen)
248
+ for (let i = 0; i < proseChunks.length - 1; i++) {
249
+ chunks.push(proseChunks[i]!)
250
+ }
251
+ buffer = proseChunks[proseChunks.length - 1] ?? ''
252
+ }
253
+ }
254
+ if (buffer !== '') chunks.push(buffer)
255
+ return chunks
256
+ }
257
+
258
+ function splitProse(text: string, maxLen: number): string[] {
259
+ if (text.length <= maxLen) return [text]
260
+
261
+ // Splitter array uses capturing groups so separators stay in the output
262
+ // and we can rejoin without losing whitespace structure.
263
+ const splitters: Array<(s: string) => string[]> = [
264
+ (s) => s.split(/(\n\n+)/),
265
+ (s) => s.split(/(\n)/),
266
+ (s) => s.split(/(?<=[.!?])\s+/),
267
+ ]
268
+
269
+ for (const splitter of splitters) {
270
+ const pieces = splitter(text)
271
+ const merged = mergeWithBudget(pieces, maxLen)
272
+ if (merged !== null) return merged
273
+ }
274
+
275
+ // Hard cut last resort: only fires when a single unbroken token exceeds maxLen.
276
+ const out: string[] = []
277
+ for (let i = 0; i < text.length; i += maxLen) {
278
+ out.push(text.slice(i, i + maxLen))
279
+ }
280
+ return out
281
+ }
282
+
283
+ function mergeWithBudget(pieces: string[], maxLen: number): string[] | null {
284
+ for (const p of pieces) {
285
+ if (p.length > maxLen) return null
286
+ }
287
+ const chunks: string[] = []
288
+ let buffer = ''
289
+ for (const piece of pieces) {
290
+ if (buffer.length + piece.length <= maxLen) {
291
+ buffer += piece
292
+ continue
293
+ }
294
+ if (buffer !== '') chunks.push(buffer)
295
+ buffer = piece
296
+ }
297
+ if (buffer !== '') chunks.push(buffer)
298
+ return chunks.map((c) => c.replace(/^\n+|\n+$/g, '')).filter((c) => c !== '')
299
+ }
@@ -0,0 +1 @@
1
+ export { chunkMarkdown } from './chunk'
@@ -0,0 +1,40 @@
1
+ import type { PluginContext, PluginLogger } from './types'
2
+
3
+ export type SpawnSubagentFn = (name: string, payload?: unknown) => Promise<void>
4
+
5
+ export type CreatePluginContextOptions<TConfig> = {
6
+ name: string
7
+ version: string | undefined
8
+ agentDir: string
9
+ config: TConfig
10
+ logger: PluginLogger
11
+ spawnSubagent: SpawnSubagentFn
12
+ isBooted: () => boolean
13
+ }
14
+
15
+ export function createPluginContext<TConfig>(opts: CreatePluginContextOptions<TConfig>): PluginContext<TConfig> {
16
+ return Object.freeze({
17
+ name: opts.name,
18
+ version: opts.version,
19
+ agentDir: opts.agentDir,
20
+ config: opts.config,
21
+ logger: opts.logger,
22
+ spawnSubagent: async (name: string, payload?: unknown) => {
23
+ if (!opts.isBooted()) {
24
+ throw new Error(
25
+ `plugin ${opts.name}: spawnSubagent("${name}") called before boot completed; subagent registry is not yet wired`,
26
+ )
27
+ }
28
+ await opts.spawnSubagent(name, payload)
29
+ },
30
+ })
31
+ }
32
+
33
+ export function createPluginLogger(name: string): PluginLogger {
34
+ const prefix = `[plugin:${name}]`
35
+ return {
36
+ info: (m) => console.log(`${prefix} ${m}`),
37
+ warn: (m) => console.warn(`${prefix} ${m}`),
38
+ error: (m) => console.error(`${prefix} ${m}`),
39
+ }
40
+ }
@@ -0,0 +1,35 @@
1
+ import type { z } from 'zod'
2
+
3
+ import type { BuiltinToolRef, DefinedPlugin, PluginContext, PluginExports, Subagent, Tool } from './types'
4
+
5
+ type DefinePluginSpec<S extends z.ZodType<unknown> | undefined> =
6
+ S extends z.ZodType<infer T>
7
+ ? {
8
+ configSchema: S
9
+ plugin: (ctx: PluginContext<T>) => Promise<PluginExports>
10
+ }
11
+ : {
12
+ plugin: (ctx: PluginContext<unknown>) => Promise<PluginExports>
13
+ }
14
+
15
+ export function definePlugin<S extends z.ZodType<unknown> | undefined = undefined>(
16
+ spec: DefinePluginSpec<S>,
17
+ ): DefinedPlugin<S extends z.ZodType<infer T> ? T : unknown> {
18
+ return spec as DefinedPlugin<S extends z.ZodType<infer T> ? T : unknown>
19
+ }
20
+
21
+ export function defineTool<P>(tool: Tool<P>): Tool<P> {
22
+ return tool
23
+ }
24
+
25
+ export function defineSubagent<P>(subagent: Subagent<P>): Subagent<P> {
26
+ return subagent
27
+ }
28
+
29
+ export const readTool: BuiltinToolRef = { __builtinTool: 'read' }
30
+ export const bashTool: BuiltinToolRef = { __builtinTool: 'bash' }
31
+ export const editTool: BuiltinToolRef = { __builtinTool: 'edit' }
32
+ export const writeTool: BuiltinToolRef = { __builtinTool: 'write' }
33
+ export const grepTool: BuiltinToolRef = { __builtinTool: 'grep' }
34
+ export const findTool: BuiltinToolRef = { __builtinTool: 'find' }
35
+ export const lsTool: BuiltinToolRef = { __builtinTool: 'ls' }
@@ -0,0 +1,204 @@
1
+ import type {
2
+ HookContext,
3
+ Hooks,
4
+ PluginLogger,
5
+ SessionEndEvent,
6
+ SessionIdleEvent,
7
+ SessionPromptEvent,
8
+ SessionStartEvent,
9
+ ToolAfterEvent,
10
+ ToolBeforeEvent,
11
+ ToolBeforeResult,
12
+ } from './types'
13
+
14
+ // Per-handler ceiling for session.idle. The channels-side router wraps the
15
+ // whole chain at SESSION_IDLE_TIMEOUT_MS = 30s; this inner bound fires
16
+ // first so the offending plugin gets named in the logs instead of the
17
+ // chain-level timeout swallowing attribution. A handler that legitimately
18
+ // needs longer (large transcript replay, dreaming subagent) is rare — and
19
+ // `setTimeout`-driven plugins like memory-logger normally return in
20
+ // milliseconds. 25s leaves headroom under the chain watchdog.
21
+ export const IDLE_HANDLER_TIMEOUT_MS = 25_000
22
+
23
+ // Per-handler ceiling for session.end. Cron consumer's runPrompt and the
24
+ // subagent runner both await session.hooks.runSessionEnd inside their
25
+ // finally blocks — a hung handler wedges inFlight forever, so the next
26
+ // scheduler fire is silently coalesced and the cron job appears dead.
27
+ // The memory plugin's session.end awaits a serialized memory-logger chain
28
+ // that can stall on a half-open LLM stream; 60s is generous headroom for
29
+ // legitimate transcript flush while still bounding the failure mode.
30
+ export const END_HANDLER_TIMEOUT_MS = 60_000
31
+
32
+ export type RegisteredHook<K extends keyof Hooks> = {
33
+ pluginName: string
34
+ agentDir: string
35
+ logger: PluginLogger
36
+ handler: NonNullable<Hooks[K]>
37
+ }
38
+
39
+ export type HookBus = {
40
+ registerAll: (pluginName: string, agentDir: string, logger: PluginLogger, hooks: Hooks) => void
41
+ unregisterAll: (pluginName: string) => void
42
+ runSessionStart: (event: SessionStartEvent) => Promise<void>
43
+ runSessionEnd: (event: SessionEndEvent) => Promise<void>
44
+ runSessionIdle: (event: SessionIdleEvent) => Promise<void>
45
+ runSessionPrompt: (event: SessionPromptEvent) => Promise<void>
46
+ runToolBefore: (event: ToolBeforeEvent) => Promise<{ block: true; reason: string } | undefined>
47
+ runToolAfter: (event: ToolAfterEvent) => Promise<void>
48
+ count: (name: keyof Hooks) => number
49
+ }
50
+
51
+ export type CreateHookBusOptions = {
52
+ // Test seam: per-handler ceiling for session.idle invocations. Lets the
53
+ // timeout path be exercised in tens of milliseconds instead of the 25s
54
+ // production default.
55
+ idleHandlerTimeoutMs?: number
56
+ // Test seam: per-handler ceiling for session.end invocations.
57
+ endHandlerTimeoutMs?: number
58
+ }
59
+
60
+ type Registries = {
61
+ 'session.start': RegisteredHook<'session.start'>[]
62
+ 'session.end': RegisteredHook<'session.end'>[]
63
+ 'session.idle': RegisteredHook<'session.idle'>[]
64
+ 'session.prompt': RegisteredHook<'session.prompt'>[]
65
+ 'tool.before': RegisteredHook<'tool.before'>[]
66
+ 'tool.after': RegisteredHook<'tool.after'>[]
67
+ }
68
+
69
+ export function createHookBus(options: CreateHookBusOptions = {}): HookBus {
70
+ const idleHandlerTimeoutMs = options.idleHandlerTimeoutMs ?? IDLE_HANDLER_TIMEOUT_MS
71
+ const endHandlerTimeoutMs = options.endHandlerTimeoutMs ?? END_HANDLER_TIMEOUT_MS
72
+ const r: Registries = {
73
+ 'session.start': [],
74
+ 'session.end': [],
75
+ 'session.idle': [],
76
+ 'session.prompt': [],
77
+ 'tool.before': [],
78
+ 'tool.after': [],
79
+ }
80
+
81
+ function ctx(reg: { pluginName: string; agentDir: string; logger: PluginLogger }): HookContext {
82
+ return { agentDir: reg.agentDir, pluginName: reg.pluginName, logger: reg.logger }
83
+ }
84
+
85
+ return {
86
+ registerAll(pluginName, agentDir, logger, hooks) {
87
+ const base = { pluginName, agentDir, logger }
88
+ if (hooks['session.start']) r['session.start'].push({ ...base, handler: hooks['session.start'] })
89
+ if (hooks['session.end']) r['session.end'].push({ ...base, handler: hooks['session.end'] })
90
+ if (hooks['session.idle']) r['session.idle'].push({ ...base, handler: hooks['session.idle'] })
91
+ if (hooks['session.prompt']) r['session.prompt'].push({ ...base, handler: hooks['session.prompt'] })
92
+ if (hooks['tool.before']) r['tool.before'].push({ ...base, handler: hooks['tool.before'] })
93
+ if (hooks['tool.after']) r['tool.after'].push({ ...base, handler: hooks['tool.after'] })
94
+ },
95
+
96
+ unregisterAll(pluginName) {
97
+ r['session.start'] = r['session.start'].filter((h) => h.pluginName !== pluginName)
98
+ r['session.end'] = r['session.end'].filter((h) => h.pluginName !== pluginName)
99
+ r['session.idle'] = r['session.idle'].filter((h) => h.pluginName !== pluginName)
100
+ r['session.prompt'] = r['session.prompt'].filter((h) => h.pluginName !== pluginName)
101
+ r['tool.before'] = r['tool.before'].filter((h) => h.pluginName !== pluginName)
102
+ r['tool.after'] = r['tool.after'].filter((h) => h.pluginName !== pluginName)
103
+ },
104
+
105
+ async runSessionStart(event) {
106
+ for (const reg of r['session.start']) {
107
+ try {
108
+ await reg.handler(event, ctx(reg))
109
+ } catch (err) {
110
+ reportHookError(reg, 'session.start', err)
111
+ }
112
+ }
113
+ },
114
+
115
+ async runSessionEnd(event) {
116
+ for (const reg of r['session.end']) {
117
+ try {
118
+ await raceWithTimeout(
119
+ Promise.resolve(reg.handler(event, ctx(reg))),
120
+ endHandlerTimeoutMs,
121
+ `plugin ${reg.pluginName} session.end`,
122
+ )
123
+ } catch (err) {
124
+ reportHookError(reg, 'session.end', err)
125
+ }
126
+ }
127
+ },
128
+
129
+ async runSessionIdle(event) {
130
+ for (const reg of r['session.idle']) {
131
+ try {
132
+ await raceWithTimeout(
133
+ Promise.resolve(reg.handler(event, ctx(reg))),
134
+ idleHandlerTimeoutMs,
135
+ `plugin ${reg.pluginName} session.idle`,
136
+ )
137
+ } catch (err) {
138
+ reportHookError(reg, 'session.idle', err)
139
+ }
140
+ }
141
+ },
142
+
143
+ async runSessionPrompt(event) {
144
+ for (const reg of r['session.prompt']) {
145
+ try {
146
+ await reg.handler(event, ctx(reg))
147
+ } catch (err) {
148
+ reportHookError(reg, 'session.prompt', err)
149
+ }
150
+ }
151
+ },
152
+
153
+ // First plugin to return `{ block: true, reason }` short-circuits. Earlier
154
+ // plugins' arg mutations remain visible to later plugins via the shared
155
+ // event.args object.
156
+ async runToolBefore(event) {
157
+ for (const reg of r['tool.before']) {
158
+ let result: ToolBeforeResult
159
+ try {
160
+ result = await reg.handler(event, ctx(reg))
161
+ } catch (err) {
162
+ reportHookError(reg, 'tool.before', err)
163
+ continue
164
+ }
165
+ if (result && typeof result === 'object' && (result as { block?: unknown }).block === true) {
166
+ const reason = (result as { reason?: unknown }).reason
167
+ return { block: true, reason: typeof reason === 'string' ? reason : 'blocked by plugin' }
168
+ }
169
+ }
170
+ return undefined
171
+ },
172
+
173
+ async runToolAfter(event) {
174
+ for (const reg of r['tool.after']) {
175
+ try {
176
+ await reg.handler(event, ctx(reg))
177
+ } catch (err) {
178
+ reportHookError(reg, 'tool.after', err)
179
+ }
180
+ }
181
+ },
182
+
183
+ count(name) {
184
+ return r[name].length
185
+ },
186
+ }
187
+ }
188
+
189
+ function reportHookError(reg: { logger: PluginLogger }, hook: keyof Hooks, err: unknown): void {
190
+ const message = err instanceof Error ? err.message : String(err)
191
+ reg.logger.error(`hook ${hook} threw: ${message}`)
192
+ }
193
+
194
+ async function raceWithTimeout<T>(work: Promise<T>, ms: number, label: string): Promise<T> {
195
+ let timer: ReturnType<typeof setTimeout> | null = null
196
+ const timeout = new Promise<never>((_, reject) => {
197
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms)
198
+ })
199
+ try {
200
+ return await Promise.race([work, timeout])
201
+ } finally {
202
+ if (timer !== null) clearTimeout(timer)
203
+ }
204
+ }
@@ -0,0 +1,63 @@
1
+ export {
2
+ bashTool,
3
+ defineTool,
4
+ definePlugin,
5
+ defineSubagent,
6
+ editTool,
7
+ findTool,
8
+ grepTool,
9
+ lsTool,
10
+ readTool,
11
+ writeTool,
12
+ } from './define'
13
+
14
+ export type {
15
+ BuiltinToolRef,
16
+ ContentPart,
17
+ DefinedPlugin,
18
+ HookContext,
19
+ HookName,
20
+ Hooks,
21
+ PluginContext,
22
+ PluginCronJob,
23
+ PluginExecCronJob,
24
+ PluginExports,
25
+ PluginLogger,
26
+ PluginPromptCronJob,
27
+ PluginSkill,
28
+ RunSession,
29
+ SessionEndEvent,
30
+ SessionIdleEvent,
31
+ SessionPromptEvent,
32
+ SessionStartEvent,
33
+ Subagent,
34
+ SubagentContext,
35
+ Tool,
36
+ ToolAfterEvent,
37
+ ToolBeforeEvent,
38
+ ToolBeforeResult,
39
+ ToolContext,
40
+ ToolLogger,
41
+ ToolResult,
42
+ } from './types'
43
+
44
+ export {
45
+ loadPlugins,
46
+ summarizeLoaded,
47
+ pluginCronJobs,
48
+ type LoadPluginsOptions,
49
+ type LoadPluginsResult,
50
+ } from './manager'
51
+ export type { LoadPluginEntryFn, ResolvedPlugin } from './loader'
52
+ export { loadPluginEntry, derivePluginNameFromPackage } from './loader'
53
+ export { materializeSkills, type MaterializedSkills, type SkillEntry } from './skills'
54
+ export {
55
+ buildPluginCronGlobalId,
56
+ type PluginRegistry,
57
+ type RegisteredCronJob,
58
+ type RegisteredSubagent,
59
+ type RegisteredTool,
60
+ type RegisteredSkillEntry,
61
+ type RegisteredSkillDir,
62
+ } from './registry'
63
+ export { createHookBus, type HookBus } from './hooks'