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.
- package/LICENSE +21 -0
- package/README.md +134 -0
- package/auth.schema.json +63 -0
- package/cron.schema.json +96 -0
- package/package.json +72 -0
- package/scripts/emit-base-dockerfile.ts +5 -0
- package/scripts/generate-schema.ts +34 -0
- package/secrets.schema.json +63 -0
- package/src/agent/auth.ts +119 -0
- package/src/agent/compaction.ts +35 -0
- package/src/agent/git-nudge.ts +95 -0
- package/src/agent/index.ts +451 -0
- package/src/agent/plugin-tools.ts +269 -0
- package/src/agent/reload-tool.ts +71 -0
- package/src/agent/self.ts +45 -0
- package/src/agent/session-origin.ts +288 -0
- package/src/agent/subagents.ts +253 -0
- package/src/agent/system-prompt.ts +68 -0
- package/src/agent/tools/channel-fetch-attachment.ts +118 -0
- package/src/agent/tools/channel-history.ts +119 -0
- package/src/agent/tools/channel-reply.ts +182 -0
- package/src/agent/tools/channel-send.ts +212 -0
- package/src/agent/tools/ddg.ts +218 -0
- package/src/agent/tools/restart.ts +122 -0
- package/src/agent/tools/stream-snapshot.ts +181 -0
- package/src/agent/tools/webfetch/fetch.ts +102 -0
- package/src/agent/tools/webfetch/index.ts +1 -0
- package/src/agent/tools/webfetch/strategies/grep.ts +70 -0
- package/src/agent/tools/webfetch/strategies/jq.ts +31 -0
- package/src/agent/tools/webfetch/strategies/raw.ts +3 -0
- package/src/agent/tools/webfetch/strategies/readability.ts +30 -0
- package/src/agent/tools/webfetch/strategies/selector.ts +41 -0
- package/src/agent/tools/webfetch/strategies/snapshot.ts +135 -0
- package/src/agent/tools/webfetch/tool.ts +281 -0
- package/src/agent/tools/webfetch/types.ts +33 -0
- package/src/agent/tools/websearch.ts +96 -0
- package/src/agent/tools/wikipedia.ts +52 -0
- package/src/bundled-plugins/agent-browser/dashboard-discovery.ts +170 -0
- package/src/bundled-plugins/agent-browser/dashboard-proxy.ts +421 -0
- package/src/bundled-plugins/agent-browser/index.ts +179 -0
- package/src/bundled-plugins/agent-browser/shim-install.ts +158 -0
- package/src/bundled-plugins/agent-browser/shim.ts +152 -0
- package/src/bundled-plugins/agent-browser/skills/agent-browser/SKILL.md +113 -0
- package/src/bundled-plugins/guard/index.ts +26 -0
- package/src/bundled-plugins/guard/policies/non-workspace-write.ts +98 -0
- package/src/bundled-plugins/guard/policies/skill-authoring.ts +185 -0
- package/src/bundled-plugins/guard/policies/uncommitted-changes.ts +85 -0
- package/src/bundled-plugins/guard/policy.ts +18 -0
- package/src/bundled-plugins/memory/README.md +71 -0
- package/src/bundled-plugins/memory/append-tool.ts +84 -0
- package/src/bundled-plugins/memory/dreaming-state.ts +86 -0
- package/src/bundled-plugins/memory/dreaming.ts +470 -0
- package/src/bundled-plugins/memory/fragment-parser.ts +67 -0
- package/src/bundled-plugins/memory/index.ts +238 -0
- package/src/bundled-plugins/memory/load-memory.ts +122 -0
- package/src/bundled-plugins/memory/memory-logger.ts +257 -0
- package/src/bundled-plugins/memory/secret-detector.ts +49 -0
- package/src/bundled-plugins/memory/watermark.ts +15 -0
- package/src/bundled-plugins/security/index.ts +35 -0
- package/src/bundled-plugins/security/policies/git-exfil.ts +120 -0
- package/src/bundled-plugins/security/policies/outbound-secret-scan.ts +167 -0
- package/src/bundled-plugins/security/policies/prompt-injection.ts +488 -0
- package/src/bundled-plugins/security/policies/secret-exfil-bash.ts +99 -0
- package/src/bundled-plugins/security/policies/secret-exfil-read.ts +127 -0
- package/src/bundled-plugins/security/policies/session-search-secrets.ts +86 -0
- package/src/bundled-plugins/security/policies/ssrf.ts +196 -0
- package/src/bundled-plugins/security/policies/system-prompt-leak.ts +81 -0
- package/src/bundled-plugins/security/policy.ts +9 -0
- package/src/channels/adapters/discord-bot-channel-resolver.ts +77 -0
- package/src/channels/adapters/discord-bot-classify.ts +148 -0
- package/src/channels/adapters/discord-bot.ts +640 -0
- package/src/channels/adapters/kakaotalk-author-resolver.ts +78 -0
- package/src/channels/adapters/kakaotalk-channel-resolver.ts +105 -0
- package/src/channels/adapters/kakaotalk-classify.ts +77 -0
- package/src/channels/adapters/kakaotalk.ts +622 -0
- package/src/channels/adapters/slack-bot-author-resolver.ts +80 -0
- package/src/channels/adapters/slack-bot-channel-resolver.ts +84 -0
- package/src/channels/adapters/slack-bot-classify.ts +213 -0
- package/src/channels/adapters/slack-bot-dedupe.ts +51 -0
- package/src/channels/adapters/slack-bot-time.ts +10 -0
- package/src/channels/adapters/slack-bot.ts +881 -0
- package/src/channels/adapters/telegram-bot-classify.ts +155 -0
- package/src/channels/adapters/telegram-bot-format.ts +309 -0
- package/src/channels/adapters/telegram-bot.ts +604 -0
- package/src/channels/engagement.ts +227 -0
- package/src/channels/index.ts +21 -0
- package/src/channels/manager.ts +292 -0
- package/src/channels/membership-cache.ts +116 -0
- package/src/channels/membership-from-history.ts +53 -0
- package/src/channels/membership.ts +30 -0
- package/src/channels/participants.ts +47 -0
- package/src/channels/persistence.ts +209 -0
- package/src/channels/reloadable.ts +28 -0
- package/src/channels/router.ts +1570 -0
- package/src/channels/schema.ts +273 -0
- package/src/channels/types.ts +160 -0
- package/src/cli/channel.ts +403 -0
- package/src/cli/compose-status.ts +95 -0
- package/src/cli/compose.ts +240 -0
- package/src/cli/hostd.ts +163 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/init.ts +592 -0
- package/src/cli/logs.ts +38 -0
- package/src/cli/reload.ts +68 -0
- package/src/cli/restart.ts +66 -0
- package/src/cli/run.ts +77 -0
- package/src/cli/shell.ts +33 -0
- package/src/cli/start.ts +57 -0
- package/src/cli/status.ts +178 -0
- package/src/cli/stop.ts +31 -0
- package/src/cli/tui.ts +35 -0
- package/src/cli/ui.ts +110 -0
- package/src/commands/index.ts +74 -0
- package/src/compose/discover.ts +43 -0
- package/src/compose/index.ts +25 -0
- package/src/compose/logs.ts +162 -0
- package/src/compose/restart.ts +69 -0
- package/src/compose/start.ts +62 -0
- package/src/compose/status.ts +28 -0
- package/src/compose/stop.ts +43 -0
- package/src/config/config.ts +424 -0
- package/src/config/index.ts +25 -0
- package/src/config/providers.ts +234 -0
- package/src/config/reloadable.ts +47 -0
- package/src/container/index.ts +27 -0
- package/src/container/logs.ts +37 -0
- package/src/container/port.ts +137 -0
- package/src/container/shared.ts +290 -0
- package/src/container/shell.ts +58 -0
- package/src/container/start.ts +670 -0
- package/src/container/status.ts +76 -0
- package/src/container/stop.ts +120 -0
- package/src/container/verify-running.ts +149 -0
- package/src/cron/consumer.ts +138 -0
- package/src/cron/index.ts +54 -0
- package/src/cron/reloadable.ts +64 -0
- package/src/cron/scheduler.ts +200 -0
- package/src/cron/schema.ts +96 -0
- package/src/hostd/client.ts +113 -0
- package/src/hostd/daemon.ts +587 -0
- package/src/hostd/index.ts +25 -0
- package/src/hostd/paths.ts +82 -0
- package/src/hostd/portbroker-manager.ts +101 -0
- package/src/hostd/protocol.ts +48 -0
- package/src/hostd/spawn.ts +224 -0
- package/src/hostd/supervisor.ts +60 -0
- package/src/hostd/tailscale.ts +172 -0
- package/src/hostd/version.ts +115 -0
- package/src/init/dockerfile.ts +327 -0
- package/src/init/ensure-deps.ts +152 -0
- package/src/init/gitignore.ts +46 -0
- package/src/init/hatching.ts +60 -0
- package/src/init/index.ts +786 -0
- package/src/init/kakaotalk-auth.ts +114 -0
- package/src/init/models-dev.ts +130 -0
- package/src/init/oauth-login.ts +74 -0
- package/src/init/packagejson.ts +94 -0
- package/src/init/paths.ts +2 -0
- package/src/init/run-bun-install.ts +20 -0
- package/src/markdown/chunk.ts +299 -0
- package/src/markdown/index.ts +1 -0
- package/src/plugin/context.ts +40 -0
- package/src/plugin/define.ts +35 -0
- package/src/plugin/hooks.ts +204 -0
- package/src/plugin/index.ts +63 -0
- package/src/plugin/loader.ts +111 -0
- package/src/plugin/manager.ts +136 -0
- package/src/plugin/registry.ts +145 -0
- package/src/plugin/skills.ts +62 -0
- package/src/plugin/types.ts +172 -0
- package/src/portbroker/bind-with-forward.ts +102 -0
- package/src/portbroker/container-server.ts +305 -0
- package/src/portbroker/forward-result-bus.ts +36 -0
- package/src/portbroker/hostd-client.ts +443 -0
- package/src/portbroker/index.ts +33 -0
- package/src/portbroker/policy.ts +24 -0
- package/src/portbroker/proc-net-tcp.ts +72 -0
- package/src/portbroker/protocol.ts +39 -0
- package/src/reload/client.ts +59 -0
- package/src/reload/index.ts +3 -0
- package/src/reload/registry.ts +60 -0
- package/src/reload/types.ts +13 -0
- package/src/run/bundled-plugins.ts +24 -0
- package/src/run/channel-session-factory.ts +105 -0
- package/src/run/index.ts +432 -0
- package/src/run/plugin-runtime.ts +43 -0
- package/src/run/schema-with-plugins.ts +14 -0
- package/src/secrets/index.ts +13 -0
- package/src/secrets/migrate.ts +95 -0
- package/src/secrets/schema.ts +75 -0
- package/src/secrets/storage.ts +231 -0
- package/src/server/index.ts +436 -0
- package/src/sessions/index.ts +23 -0
- package/src/shared/index.ts +9 -0
- package/src/shared/local-time.ts +21 -0
- package/src/shared/protocol.ts +25 -0
- package/src/skills/typeclaw-channel-kakaotalk/SKILL.md +87 -0
- package/src/skills/typeclaw-channel-telegram-bot/SKILL.md +64 -0
- package/src/skills/typeclaw-config/SKILL.md +643 -0
- package/src/skills/typeclaw-cron/SKILL.md +159 -0
- package/src/skills/typeclaw-git/SKILL.md +89 -0
- package/src/skills/typeclaw-memory/SKILL.md +174 -0
- package/src/skills/typeclaw-monorepo/SKILL.md +175 -0
- package/src/skills/typeclaw-plugins/SKILL.md +594 -0
- package/src/skills/typeclaw-skills/SKILL.md +246 -0
- package/src/stream/broker.ts +161 -0
- package/src/stream/index.ts +16 -0
- package/src/stream/types.ts +69 -0
- package/src/tui/client.ts +45 -0
- package/src/tui/format.ts +317 -0
- package/src/tui/index.ts +225 -0
- package/src/tui/theme.ts +41 -0
- 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'
|