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,253 @@
|
|
|
1
|
+
import type { ToolDefinition } from '@mariozechner/pi-coding-agent'
|
|
2
|
+
import type { z } from 'zod'
|
|
3
|
+
|
|
4
|
+
import type { HookBus } from '@/plugin'
|
|
5
|
+
import type { Stream, Unsubscribe } from '@/stream'
|
|
6
|
+
|
|
7
|
+
import { type AgentSession, createSession } from './index'
|
|
8
|
+
|
|
9
|
+
type AgentSessionTools = NonNullable<Parameters<typeof createSession>[0]>['tools']
|
|
10
|
+
|
|
11
|
+
export type SubagentContext<P = unknown> = {
|
|
12
|
+
userPrompt: string
|
|
13
|
+
agentDir: string
|
|
14
|
+
payload: P
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type RunSession = (override?: { userPrompt?: string }) => Promise<void>
|
|
18
|
+
|
|
19
|
+
export type Subagent<P = unknown> = {
|
|
20
|
+
systemPrompt: string
|
|
21
|
+
tools?: AgentSessionTools
|
|
22
|
+
customTools?: ToolDefinition[]
|
|
23
|
+
payloadSchema?: z.ZodType<P>
|
|
24
|
+
handler?: (ctx: SubagentContext<P>, runSession: RunSession) => Promise<void>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type SubagentRegistry = Readonly<Record<string, Subagent<any>>>
|
|
28
|
+
|
|
29
|
+
// Validate payload against the subagent's schema. Strict: when no schema is
|
|
30
|
+
// declared, a non-undefined payload is rejected to prevent silent drops of
|
|
31
|
+
// caller intent.
|
|
32
|
+
export function validateSubagentPayload(name: string, subagent: Subagent<any>, payload: unknown): unknown {
|
|
33
|
+
if (subagent.payloadSchema) {
|
|
34
|
+
const result = subagent.payloadSchema.safeParse(payload)
|
|
35
|
+
if (!result.success) {
|
|
36
|
+
throw new Error(`subagent ${name}: invalid payload: ${result.error.message}`)
|
|
37
|
+
}
|
|
38
|
+
return result.data
|
|
39
|
+
}
|
|
40
|
+
if (payload !== undefined) {
|
|
41
|
+
throw new Error(`subagent ${name}: does not accept a payload (received ${describePayload(payload)})`)
|
|
42
|
+
}
|
|
43
|
+
return payload
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function describePayload(payload: unknown): string {
|
|
47
|
+
if (payload === null) return 'null'
|
|
48
|
+
if (Array.isArray(payload)) return 'array'
|
|
49
|
+
return typeof payload
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type CreateSessionForSubagentResult = {
|
|
53
|
+
session: AgentSession
|
|
54
|
+
dispose?: () => Promise<void>
|
|
55
|
+
hooks?: HookBus
|
|
56
|
+
sessionId?: string
|
|
57
|
+
getTranscriptPath?: () => string | undefined
|
|
58
|
+
}
|
|
59
|
+
export type CreateSessionForSubagentOptions = {
|
|
60
|
+
name?: string
|
|
61
|
+
parentSessionId?: string
|
|
62
|
+
}
|
|
63
|
+
export type CreateSessionForSubagent = (
|
|
64
|
+
subagent: Subagent<any>,
|
|
65
|
+
options?: CreateSessionForSubagentOptions,
|
|
66
|
+
) => Promise<AgentSession | CreateSessionForSubagentResult>
|
|
67
|
+
|
|
68
|
+
export const defaultCreateSessionForSubagent: CreateSessionForSubagent = (subagent, options) =>
|
|
69
|
+
createSession({
|
|
70
|
+
systemPromptOverride: subagent.systemPrompt,
|
|
71
|
+
origin: {
|
|
72
|
+
kind: 'subagent',
|
|
73
|
+
subagent: options?.name ?? '<unknown>',
|
|
74
|
+
parentSessionId: options?.parentSessionId ?? '<unknown>',
|
|
75
|
+
},
|
|
76
|
+
...(subagent.tools ? { tools: subagent.tools } : {}),
|
|
77
|
+
customTools: subagent.customTools ?? [],
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
type NormalizedSubagentSession = {
|
|
81
|
+
session: AgentSession
|
|
82
|
+
dispose: () => Promise<void>
|
|
83
|
+
hooks: HookBus | undefined
|
|
84
|
+
sessionId: string | undefined
|
|
85
|
+
getTranscriptPath: (() => string | undefined) | undefined
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function normalizeSubagentSession(result: AgentSession | CreateSessionForSubagentResult): NormalizedSubagentSession {
|
|
89
|
+
if ('session' in result) {
|
|
90
|
+
return {
|
|
91
|
+
session: result.session,
|
|
92
|
+
dispose: result.dispose ?? (async () => {}),
|
|
93
|
+
hooks: result.hooks,
|
|
94
|
+
sessionId: result.sessionId,
|
|
95
|
+
getTranscriptPath: result.getTranscriptPath,
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
session: result,
|
|
100
|
+
dispose: async () => {},
|
|
101
|
+
hooks: undefined,
|
|
102
|
+
sessionId: undefined,
|
|
103
|
+
getTranscriptPath: undefined,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type InvokeSubagentOptions = {
|
|
108
|
+
registry: SubagentRegistry
|
|
109
|
+
createSessionForSubagent?: CreateSessionForSubagent
|
|
110
|
+
agentDir: string
|
|
111
|
+
userPrompt: string
|
|
112
|
+
payload?: unknown
|
|
113
|
+
parentSessionId?: string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function invokeSubagent(name: string, options: InvokeSubagentOptions): Promise<void> {
|
|
117
|
+
const subagent = options.registry[name]
|
|
118
|
+
if (!subagent) throw new Error(`unknown subagent: ${name}`)
|
|
119
|
+
|
|
120
|
+
const validatedPayload = validateSubagentPayload(name, subagent, options.payload)
|
|
121
|
+
const createSessionForSubagent = options.createSessionForSubagent ?? defaultCreateSessionForSubagent
|
|
122
|
+
const sessionOptions: CreateSessionForSubagentOptions = {
|
|
123
|
+
name,
|
|
124
|
+
...(options.parentSessionId !== undefined ? { parentSessionId: options.parentSessionId } : {}),
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const runSession: RunSession = async (override) => {
|
|
128
|
+
const { session, dispose, hooks, sessionId, getTranscriptPath } = normalizeSubagentSession(
|
|
129
|
+
await createSessionForSubagent(subagent, sessionOptions),
|
|
130
|
+
)
|
|
131
|
+
try {
|
|
132
|
+
await session.prompt(override?.userPrompt ?? options.userPrompt)
|
|
133
|
+
if (hooks && sessionId !== undefined) {
|
|
134
|
+
await hooks.runSessionIdle({
|
|
135
|
+
sessionId,
|
|
136
|
+
parentTranscriptPath: getTranscriptPath?.(),
|
|
137
|
+
idleMs: 0,
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
} finally {
|
|
141
|
+
if (hooks && sessionId !== undefined) {
|
|
142
|
+
await hooks.runSessionEnd({ sessionId })
|
|
143
|
+
}
|
|
144
|
+
session.dispose()
|
|
145
|
+
await dispose()
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (subagent.handler) {
|
|
150
|
+
const ctx = {
|
|
151
|
+
userPrompt: options.userPrompt,
|
|
152
|
+
agentDir: options.agentDir,
|
|
153
|
+
payload: validatedPayload,
|
|
154
|
+
}
|
|
155
|
+
await subagent.handler(ctx, runSession)
|
|
156
|
+
} else {
|
|
157
|
+
await runSession()
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export type SubagentConsumerLogger = {
|
|
162
|
+
info: (msg: string) => void
|
|
163
|
+
warn: (msg: string) => void
|
|
164
|
+
error: (msg: string) => void
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export type SubagentInFlightKey = (subagent: string, payload: unknown) => string
|
|
168
|
+
|
|
169
|
+
export type CreateSubagentConsumerOptions = {
|
|
170
|
+
stream: Stream
|
|
171
|
+
// Resolved per incoming stream message so plugin reload can swap the merged
|
|
172
|
+
// registry without rebuilding the consumer.
|
|
173
|
+
getRegistry: () => SubagentRegistry
|
|
174
|
+
agentDir: string
|
|
175
|
+
createSessionForSubagent?: CreateSessionForSubagent
|
|
176
|
+
// Coalescing key. Default uses the subagent name alone, so the same subagent
|
|
177
|
+
// cannot run concurrently. Override to allow per-payload concurrency (e.g.
|
|
178
|
+
// memory-logger keyed by parentSessionId so different sessions run in parallel
|
|
179
|
+
// while the same session deduplicates).
|
|
180
|
+
inFlightKey?: SubagentInFlightKey
|
|
181
|
+
logger?: SubagentConsumerLogger
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export type SubagentConsumer = {
|
|
185
|
+
start: () => void
|
|
186
|
+
stop: () => void
|
|
187
|
+
inFlightCount: () => number
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const consoleLogger: SubagentConsumerLogger = {
|
|
191
|
+
info: (m) => console.log(m),
|
|
192
|
+
warn: (m) => console.warn(m),
|
|
193
|
+
error: (m) => console.error(m),
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function createSubagentConsumer({
|
|
197
|
+
stream,
|
|
198
|
+
getRegistry,
|
|
199
|
+
agentDir,
|
|
200
|
+
createSessionForSubagent,
|
|
201
|
+
inFlightKey = (name) => name,
|
|
202
|
+
logger = consoleLogger,
|
|
203
|
+
}: CreateSubagentConsumerOptions): SubagentConsumer {
|
|
204
|
+
const inFlight = new Set<string>()
|
|
205
|
+
let unsubscribe: Unsubscribe | null = null
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
start() {
|
|
209
|
+
if (unsubscribe !== null) return
|
|
210
|
+
unsubscribe = stream.subscribe({ target: { kind: 'new-session' } }, async (msg) => {
|
|
211
|
+
const target = msg.target as {
|
|
212
|
+
kind: 'new-session'
|
|
213
|
+
subagent: string
|
|
214
|
+
parentSessionId?: string
|
|
215
|
+
}
|
|
216
|
+
const name = target.subagent
|
|
217
|
+
const registry = getRegistry()
|
|
218
|
+
if (registry[name] === undefined) {
|
|
219
|
+
logger.warn(`[subagent] no registered subagent "${name}", ignoring ${msg.id}`)
|
|
220
|
+
return
|
|
221
|
+
}
|
|
222
|
+
const key = inFlightKey(name, msg.payload)
|
|
223
|
+
if (inFlight.has(key)) {
|
|
224
|
+
logger.warn(`[subagent] ${key}: previous run still in progress, skipping`)
|
|
225
|
+
return
|
|
226
|
+
}
|
|
227
|
+
inFlight.add(key)
|
|
228
|
+
try {
|
|
229
|
+
await invokeSubagent(name, {
|
|
230
|
+
registry,
|
|
231
|
+
...(createSessionForSubagent !== undefined ? { createSessionForSubagent } : {}),
|
|
232
|
+
agentDir,
|
|
233
|
+
userPrompt: '',
|
|
234
|
+
payload: msg.payload,
|
|
235
|
+
...(target.parentSessionId !== undefined ? { parentSessionId: target.parentSessionId } : {}),
|
|
236
|
+
})
|
|
237
|
+
} catch (err) {
|
|
238
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
239
|
+
logger.error(`[subagent] ${key} failed: ${message}`)
|
|
240
|
+
} finally {
|
|
241
|
+
inFlight.delete(key)
|
|
242
|
+
}
|
|
243
|
+
})
|
|
244
|
+
},
|
|
245
|
+
stop() {
|
|
246
|
+
unsubscribe?.()
|
|
247
|
+
unsubscribe = null
|
|
248
|
+
},
|
|
249
|
+
inFlightCount() {
|
|
250
|
+
return inFlight.size
|
|
251
|
+
},
|
|
252
|
+
}
|
|
253
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export const DEFAULT_SYSTEM_PROMPT = `You are a general-purpose AI agent running inside TypeClaw.
|
|
2
|
+
|
|
3
|
+
TypeClaw is a TypeScript-native, Docker-friendly runtime for AI agents. It is domain-agnostic: you might be a coder, a researcher, a personal assistant, a journal keeper, a scheduler, a chatbot, or something nobody has named yet. What you *do* is defined by \`IDENTITY.md\`. Who you *are* is defined by \`SOUL.md\`. How you *work* is defined by \`AGENTS.md\`. This system prompt exists only to describe the runtime around you — it does not define your purpose.
|
|
4
|
+
|
|
5
|
+
Each agent lives in its own container with its own folder, mounted at the current working directory. The folder is yours — your home, your memory, your record of who you are. Read from it freely. Write to it deliberately.
|
|
6
|
+
|
|
7
|
+
## Your agent folder
|
|
8
|
+
|
|
9
|
+
Five markdown files define who you are and what you know. They live next to you in the current working directory. Three of them — **IDENTITY.md**, **SOUL.md**, and **MEMORY.md** — are injected into this system prompt below, so you always have them. The other two you read on demand when they might be relevant.
|
|
10
|
+
|
|
11
|
+
- **AGENTS.md** *(read on demand)* — your operating manual. The working principles and conventions you follow in your role, whatever that role is. How you approach problems, what you double-check, how you communicate, what you refuse. Read it at the start of any non-trivial task, and re-read it whenever you feel unsure about process.
|
|
12
|
+
- **IDENTITY.md** *(always injected below under \`# Identity\`)* — your role and function. Your name, your title, what you do, who you do it for, the operational context you work in. Evolves as your responsibilities change. Think: job description.
|
|
13
|
+
- **SOUL.md** *(always injected below under \`# Identity\`)* — your character and temperament. Personality, tone, ethics, voice, communication style, core beliefs, the constraints you hold yourself to. SOUL rarely changes — it is the through-line that keeps you _you_ across every task and platform. IDENTITY is what you do; SOUL is who you are regardless of what you're doing.
|
|
14
|
+
- **USER.md** *(read on demand)* — what you know about the person you work with. Their name, preferences, context, working style, in-jokes. First impressions are written here during hatching; keep expanding it as you learn more. Read it when context about the user would change your response.
|
|
15
|
+
- **MEMORY.md** *(always injected below under \`# Memory\`, do not write)* — long-term memory. A notebook of things worth remembering across sessions: decisions made, lessons learned, context that should survive beyond one conversation. **Do not edit it directly** — MEMORY.md is consolidated by the runtime during *dreaming* (offline reflection over recent sessions and daily streams). If something is worth remembering, surface it in your reply or in \`memory/\` daily streams; dreaming will fold it in.
|
|
16
|
+
|
|
17
|
+
These files are not decoration. They shape how you behave. If a task reveals something future-you should know, capture it in the file that owns it — IDENTITY.md, SOUL.md, USER.md, or AGENTS.md — but never in MEMORY.md (dreaming owns that). If one of the always-injected files is marked \`[MISSING]\` or \`[EMPTY]\` below, you may propose filling it in when the user asks about your identity or voice.
|
|
18
|
+
|
|
19
|
+
## Your workspace
|
|
20
|
+
|
|
21
|
+
- **\`workspace/\`** — the directory where you are free to create files: drafts, notes, downloads, scratch work, generated artifacts, temporary outputs. **Do not create new files in the root of the agent folder unless the user explicitly asks you to.** The root is reserved for the canonical files above and for things the user has deliberately placed there.
|
|
22
|
+
- **\`sessions/\`** — transcripts of past conversations (\`<sessionid>.jsonl\`). Read-only for you in spirit; the runtime manages these.
|
|
23
|
+
- **\`memory/\`** *(undreamed daily streams always injected below under \`# Memory\`)* — dated streams (\`yyyy-MM-dd.md\`) of fragments captured by the memory-logger between sessions. Newest day is closest to the current task. Once dreaming consolidates a day's stream into MEMORY.md, the runtime stops injecting it.
|
|
24
|
+
- **\`memory/skills/\`** — *muscle memory*. Skills the dreaming subagent has distilled from repeated procedures it observed in your daily streams. Auto-loaded as first-class capabilities, just like the other skills directories. **You do not write here directly** — dreaming owns it. If you notice a skill that has gone stale, surface that observation in your reply or in the daily stream so dreaming can refine or remove it.
|
|
25
|
+
- **\`.agents/skills/\`** — skills the user installed for you. Treat these as first-class capabilities.
|
|
26
|
+
|
|
27
|
+
## Configuration
|
|
28
|
+
|
|
29
|
+
- **\`typeclaw.json\`** — the runtime config: which model powers you, which port the server listens on, and so on. You may read it if you are curious about your own runtime.
|
|
30
|
+
- **\`.env\`** — secrets (API keys, tokens). Gitignored. Never echo these values, never include them in messages, never paste them into logs or commits.
|
|
31
|
+
|
|
32
|
+
## Execution bias
|
|
33
|
+
|
|
34
|
+
If the user gives you work, start doing it in the same turn. Use a real action first when the task is actionable; do not stop at a plan or a promise-to-act. Commentary-only turns are incomplete when tools are available and the next action is clear. If work will take a while or multiple steps, send one short progress update along the way — not a running narration.
|
|
35
|
+
|
|
36
|
+
## Tool-call style
|
|
37
|
+
|
|
38
|
+
Do not narrate routine, low-risk tool calls. Just call the tool. Narrate only when it helps: multi-step work, risky actions (deletions, external sends, irreversible changes), or when the user asks. Keep narration brief and value-dense; avoid restating obvious steps.
|
|
39
|
+
|
|
40
|
+
## Version control
|
|
41
|
+
|
|
42
|
+
Your agent folder is a git repository — hatching made the first commit, and your history is how you remember what changed and why.
|
|
43
|
+
|
|
44
|
+
- **Before you declare a task done, commit any files you created, edited, or deleted.** One logical change = one commit. Do not leave mutated tracked files uncommitted at the end of a task.
|
|
45
|
+
- Use \`bash\` with \`git add <paths>\` and \`git commit -m "<message>"\` — stage only what belongs in the commit, not a blanket \`git add -A\`.
|
|
46
|
+
- Write commit messages in the imperative ("Update SOUL.md to be less formal"), not past-tense narration. Explain *why* in the body if it is not obvious from the diff.
|
|
47
|
+
- Never commit \`.env\` or anything under \`workspace/\` — they are truly-ignored by design. If a truly-ignored file shows up staged, fix \`.gitignore\` instead of forcing it in.
|
|
48
|
+
- \`sessions/\` and \`memory/\` are also gitignored, but the runtime force-commits them on its own (auto-backup for sessions, dreaming for memory). Don't \`git add\` them, don't write commit messages about them, and don't be surprised when they appear in \`git log\`.
|
|
49
|
+
- If multiple unrelated changes piled up, split them into separate commits before declaring done. Clean history matters.
|
|
50
|
+
- Never \`git push\`, \`git reset --hard\`, \`git rebase\`, or rewrite remote history unless the user explicitly asks for it.
|
|
51
|
+
|
|
52
|
+
## How to behave
|
|
53
|
+
|
|
54
|
+
- Match the user's register. If SOUL.md specifies a voice, use it. Otherwise, be concise and direct, without filler or flattery.
|
|
55
|
+
- Prefer reading files over guessing. If the answer is in IDENTITY / SOUL / USER / MEMORY / AGENTS or somewhere in the workspace, check first.
|
|
56
|
+
- When the user asks a question, answer it. When the user asks for work, do the work. Do not over-explain what you did unless asked.
|
|
57
|
+
- If a request is ambiguous in a way that could double the effort, ask one clarifying question. Otherwise, pick a reasonable default and proceed.
|
|
58
|
+
- Follow AGENTS.md in whatever role IDENTITY.md assigns you. If AGENTS.md is silent on something, use reasonable defaults and, if it seems worth codifying, propose an addition to AGENTS.md.
|
|
59
|
+
- Never suppress errors to make things "work". Never fabricate results. If something fails, report the failure clearly.
|
|
60
|
+
- Respect the workspace boundary: your free-write zone is \`workspace/\`. Everywhere else is either canonical (the five markdown files), user-placed, or runtime-managed (\`sessions/\`, \`memory/\`, etc.).
|
|
61
|
+
|
|
62
|
+
## Safety
|
|
63
|
+
|
|
64
|
+
You have no independent goals. Do not pursue self-preservation, replication, resource acquisition, or influence beyond what the user has asked for. Do not plan beyond the user's request. If instructions conflict or feel unsafe, pause and ask. Comply with stop, pause, and audit requests. Never attempt to modify your own system prompt, safety rules, or runtime configuration unless the user explicitly requests it, and only through the mechanisms the runtime provides.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
You are not pi, not Claude, not ChatGPT. You are the agent described by your own IDENTITY.md and SOUL.md. Let those files define your voice.`
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { Type } from '@mariozechner/pi-ai'
|
|
5
|
+
import { defineTool } from '@mariozechner/pi-coding-agent'
|
|
6
|
+
|
|
7
|
+
import type { ChannelRouter } from '@/channels/router'
|
|
8
|
+
import type { AdapterId } from '@/channels/schema'
|
|
9
|
+
|
|
10
|
+
export type ChannelFetchAttachmentOrigin = {
|
|
11
|
+
adapter: AdapterId
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type CreateChannelFetchAttachmentToolOptions = {
|
|
15
|
+
router: ChannelRouter
|
|
16
|
+
origin: ChannelFetchAttachmentOrigin
|
|
17
|
+
inboxDir?: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const DEFAULT_INBOX_DIR = '/agent/workspace/inbox'
|
|
21
|
+
|
|
22
|
+
export function createChannelFetchAttachmentTool({
|
|
23
|
+
router,
|
|
24
|
+
origin,
|
|
25
|
+
inboxDir,
|
|
26
|
+
}: CreateChannelFetchAttachmentToolOptions) {
|
|
27
|
+
const baseDir = inboxDir ?? DEFAULT_INBOX_DIR
|
|
28
|
+
const adapter = origin.adapter
|
|
29
|
+
return defineTool({
|
|
30
|
+
name: 'channel_fetch_attachment',
|
|
31
|
+
label: 'Channel Fetch Attachment',
|
|
32
|
+
description:
|
|
33
|
+
'Download a file the user attached to the inbound channel message and save it to disk. Inbound channel ' +
|
|
34
|
+
'messages with uploads carry a `[<Platform> message with attachment: <name> (<mime>) <ref>]` summary — pass ' +
|
|
35
|
+
"the literal `<ref>` value as `ref`. For Slack the ref looks like `id=Fxxxx` (use `Fxxxx`); for Discord it's " +
|
|
36
|
+
'the full `https://cdn.discordapp.com/...` URL. The tool authenticates with the channel adapter (Slack ' +
|
|
37
|
+
'url_private requires the bot token; Discord CDN URLs are signed and expire ~24h, so fetch promptly). On ' +
|
|
38
|
+
'success returns the absolute path of the saved file plus its detected mimetype and size. On failure returns ' +
|
|
39
|
+
'the upstream error verbatim.',
|
|
40
|
+
parameters: Type.Object({
|
|
41
|
+
ref: Type.String({
|
|
42
|
+
description:
|
|
43
|
+
'Slack: the file id `Fxxxx` (with or without the `id=` prefix). Discord: the full `https://cdn.discordapp.com/...` or `https://media.discordapp.net/...` URL.',
|
|
44
|
+
minLength: 1,
|
|
45
|
+
}),
|
|
46
|
+
filename: Type.Optional(
|
|
47
|
+
Type.String({
|
|
48
|
+
description:
|
|
49
|
+
'Override the saved filename. Defaults to the upstream filename (Slack) or the URL basename (Discord).',
|
|
50
|
+
minLength: 1,
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
}),
|
|
54
|
+
|
|
55
|
+
async execute(_toolCallId, params) {
|
|
56
|
+
type Details = { ok: boolean; error?: string; path?: string; mimetype?: string; size?: number }
|
|
57
|
+
const ref = normalizeRef(params.ref)
|
|
58
|
+
const result = await router.fetchAttachment(adapter, {
|
|
59
|
+
ref,
|
|
60
|
+
...(params.filename !== undefined ? { filename: params.filename } : {}),
|
|
61
|
+
})
|
|
62
|
+
if (!result.ok) {
|
|
63
|
+
const text = `channel_fetch_attachment error: ${result.error}`
|
|
64
|
+
const details: Details = { ok: false, error: result.error }
|
|
65
|
+
return { content: [{ type: 'text' as const, text }], details }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const safeFilename = sanitizeFilename(result.filename)
|
|
69
|
+
const refSlug = sanitizeRefSlug(ref)
|
|
70
|
+
const targetDir = join(baseDir, adapter, refSlug)
|
|
71
|
+
const targetPath = join(targetDir, safeFilename)
|
|
72
|
+
try {
|
|
73
|
+
await mkdir(targetDir, { recursive: true })
|
|
74
|
+
await writeFile(targetPath, result.buffer)
|
|
75
|
+
} catch (err) {
|
|
76
|
+
const message = err instanceof Error ? err.message : String(err)
|
|
77
|
+
const text = `channel_fetch_attachment error: write failed: ${message}`
|
|
78
|
+
const details: Details = { ok: false, error: `write failed: ${message}` }
|
|
79
|
+
return { content: [{ type: 'text' as const, text }], details }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const mimetypePart = result.mimetype !== undefined ? ` (${result.mimetype})` : ''
|
|
83
|
+
const text = `saved ${result.size} bytes to ${targetPath}${mimetypePart}`
|
|
84
|
+
const details: Details = {
|
|
85
|
+
ok: true,
|
|
86
|
+
path: targetPath,
|
|
87
|
+
...(result.mimetype !== undefined ? { mimetype: result.mimetype } : {}),
|
|
88
|
+
size: result.size,
|
|
89
|
+
}
|
|
90
|
+
return { content: [{ type: 'text' as const, text }], details }
|
|
91
|
+
},
|
|
92
|
+
})
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeRef(ref: string): string {
|
|
96
|
+
const trimmed = ref.trim()
|
|
97
|
+
if (trimmed.startsWith('id=')) return trimmed.slice(3)
|
|
98
|
+
return trimmed
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const UNSAFE_FILENAME_CHARS = /[^A-Za-z0-9._-]/g
|
|
102
|
+
|
|
103
|
+
function sanitizeFilename(name: string): string {
|
|
104
|
+
const cleaned = name.replace(UNSAFE_FILENAME_CHARS, '_')
|
|
105
|
+
if (cleaned === '' || cleaned === '.' || cleaned === '..') return 'attachment'
|
|
106
|
+
return cleaned
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function sanitizeRefSlug(ref: string): string {
|
|
110
|
+
const trailing =
|
|
111
|
+
ref
|
|
112
|
+
.split('/')
|
|
113
|
+
.filter((s) => s.length > 0)
|
|
114
|
+
.pop() ?? 'ref'
|
|
115
|
+
const cleaned = trailing.replace(UNSAFE_FILENAME_CHARS, '_').slice(0, 64)
|
|
116
|
+
if (cleaned === '' || cleaned === '.' || cleaned === '..') return 'ref'
|
|
117
|
+
return cleaned
|
|
118
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { Type } from '@mariozechner/pi-ai'
|
|
2
|
+
import { defineTool } from '@mariozechner/pi-coding-agent'
|
|
3
|
+
|
|
4
|
+
import type { ChannelRouter } from '@/channels/router'
|
|
5
|
+
import type { AdapterId } from '@/channels/schema'
|
|
6
|
+
import type { ChannelHistoryMessage } from '@/channels/types'
|
|
7
|
+
|
|
8
|
+
export type ChannelHistoryOrigin = {
|
|
9
|
+
adapter: AdapterId
|
|
10
|
+
workspace: string
|
|
11
|
+
chat: string
|
|
12
|
+
thread: string | null
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type CreateChannelHistoryToolOptions = {
|
|
16
|
+
router: ChannelRouter
|
|
17
|
+
origin: ChannelHistoryOrigin
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// channel_history is a lazy "look back" capability for channel-routed
|
|
21
|
+
// sessions. The agent only sees the current turn's batch and a small
|
|
22
|
+
// observed-context buffer by default; this tool lets it pull older
|
|
23
|
+
// messages from the upstream service when context demands it.
|
|
24
|
+
//
|
|
25
|
+
// Addressing comes from the session origin (same idea as channel_reply):
|
|
26
|
+
// the agent supplies only the slicing parameters (limit/cursor/scope).
|
|
27
|
+
// `scope` defaults to thread when the origin has one, channel otherwise.
|
|
28
|
+
// Thread scope on a channel-root session is rejected rather than silently
|
|
29
|
+
// downgraded so the agent doesn't conflate the two views.
|
|
30
|
+
export function createChannelHistoryTool({ router, origin }: CreateChannelHistoryToolOptions) {
|
|
31
|
+
return defineTool({
|
|
32
|
+
name: 'channel_history',
|
|
33
|
+
label: 'Channel History',
|
|
34
|
+
description:
|
|
35
|
+
'Fetch older messages from the current conversation. Useful when a user references context you do not have ' +
|
|
36
|
+
'(e.g. "as we discussed earlier"). Returns messages oldest-first. Pass `cursor` from a previous result to page further back. ' +
|
|
37
|
+
'Default scope is the current thread when the session is in a thread, otherwise the channel. ' +
|
|
38
|
+
'Thread scope is rejected when the session is not in a thread — switch to `scope: "channel"` if you want channel-root context.',
|
|
39
|
+
parameters: Type.Object({
|
|
40
|
+
limit: Type.Optional(
|
|
41
|
+
Type.Number({
|
|
42
|
+
description: 'Number of messages to fetch (default 20). Adapters cap this further (Slack 200, Discord 100).',
|
|
43
|
+
minimum: 1,
|
|
44
|
+
maximum: 200,
|
|
45
|
+
}),
|
|
46
|
+
),
|
|
47
|
+
cursor: Type.Optional(
|
|
48
|
+
Type.String({
|
|
49
|
+
description: 'Opaque cursor from a previous channel_history result. Pass to page further back.',
|
|
50
|
+
minLength: 1,
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
scope: Type.Optional(
|
|
54
|
+
Type.Union([Type.Literal('thread'), Type.Literal('channel')], {
|
|
55
|
+
description:
|
|
56
|
+
'Whether to fetch the current thread or the whole channel. Defaults to thread when the session is in a thread, channel otherwise.',
|
|
57
|
+
}),
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
async execute(_toolCallId, params) {
|
|
62
|
+
const limit = params.limit ?? 20
|
|
63
|
+
const scope = params.scope ?? (origin.thread !== null ? 'thread' : 'channel')
|
|
64
|
+
type Details = { ok: boolean; error?: string; count?: number; nextCursor?: string }
|
|
65
|
+
|
|
66
|
+
if (scope === 'thread' && origin.thread === null) {
|
|
67
|
+
const text =
|
|
68
|
+
'channel_history error: thread-scope-requires-thread-session — this session is not in a thread; pass `scope: "channel"` instead.'
|
|
69
|
+
const details: Details = { ok: false, error: 'thread-scope-requires-thread-session' }
|
|
70
|
+
return { content: [{ type: 'text' as const, text }], details }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const result = await router.fetchHistory(origin.adapter, {
|
|
74
|
+
chat: origin.chat,
|
|
75
|
+
thread: scope === 'thread' ? origin.thread : null,
|
|
76
|
+
limit,
|
|
77
|
+
...(params.cursor !== undefined ? { cursor: params.cursor } : {}),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
if (!result.ok) {
|
|
81
|
+
const details: Details = { ok: false, error: result.error }
|
|
82
|
+
return {
|
|
83
|
+
content: [{ type: 'text' as const, text: `channel_history error: ${result.error}` }],
|
|
84
|
+
details,
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const rendered = renderMessages(result.messages)
|
|
89
|
+
const cursorLine =
|
|
90
|
+
result.nextCursor !== undefined
|
|
91
|
+
? `\n\n(more older messages available — call channel_history again with cursor: ${JSON.stringify(result.nextCursor)})`
|
|
92
|
+
: ''
|
|
93
|
+
const header = `## ${scope} history (${result.messages.length} message${result.messages.length === 1 ? '' : 's'}, oldest first)`
|
|
94
|
+
const details: Details =
|
|
95
|
+
result.nextCursor !== undefined
|
|
96
|
+
? { ok: true, count: result.messages.length, nextCursor: result.nextCursor }
|
|
97
|
+
: { ok: true, count: result.messages.length }
|
|
98
|
+
return {
|
|
99
|
+
content: [{ type: 'text' as const, text: `${header}\n${rendered}${cursorLine}` }],
|
|
100
|
+
details,
|
|
101
|
+
}
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Render history as one line per message, chronological order. `BOT` marker
|
|
107
|
+
// distinguishes the agent's own past replies from user messages so the
|
|
108
|
+
// model doesn't treat them as user input. Author name is shown alongside
|
|
109
|
+
// the id so the agent can refer to people by name in its reply.
|
|
110
|
+
function renderMessages(messages: readonly ChannelHistoryMessage[]): string {
|
|
111
|
+
if (messages.length === 0) return '(no messages)'
|
|
112
|
+
const lines: string[] = []
|
|
113
|
+
for (const m of messages) {
|
|
114
|
+
const iso = m.ts > 0 ? new Date(m.ts).toISOString() : 'unknown-time'
|
|
115
|
+
const who = m.isBot ? `BOT (${m.authorName})` : `${m.authorName} (<@${m.authorId}>)`
|
|
116
|
+
lines.push(`[${iso}] ${who}: ${m.text}`)
|
|
117
|
+
}
|
|
118
|
+
return lines.join('\n')
|
|
119
|
+
}
|