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,594 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: typeclaw-plugins
|
|
3
|
+
description: TypeClaw plugin authoring and operation guide. Use when writing, editing, configuring, debugging, or installing a TypeClaw plugin — including any work with definePlugin, defineTool, defineSubagent, plugin hooks (session.start/end/idle/prompt, tool.before/after), plugin cron jobs, plugin skills, the typeclaw/plugin import path, or per-plugin config blocks in typeclaw.json. Triggers on mentions of 'TypeClaw plugin', 'definePlugin', 'plugin hook', 'plugin cron', 'plugins[]', 'typeclaw-plugin-', or any file under src/plugin/ or plugins/.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# TypeClaw Plugins
|
|
7
|
+
|
|
8
|
+
A plugin is a TypeScript module with **one default export** — a call to `definePlugin({ ... })`. The factory returns a contributions object that the runtime translates into tools, subagents, cron jobs, skills, and event hooks. Plugins import only from `typeclaw/plugin` and `zod`.
|
|
9
|
+
|
|
10
|
+
This skill covers BOTH authoring new plugins AND operating existing ones (config layout, debugging failures, lifecycle).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. The Architectural Boundary (read first)
|
|
15
|
+
|
|
16
|
+
Three layers, sharply separated:
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
Plugin API (typeclaw/plugin) ← plugins live here. NO @mariozechner/* imports.
|
|
20
|
+
↓
|
|
21
|
+
TypeClaw runtime (src/plugin, src/agent, src/run, src/server, src/cron)
|
|
22
|
+
↓
|
|
23
|
+
Engine (@mariozechner/pi-coding-agent) ← never visible to plugins
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
**MUST NOT** import anything from `@mariozechner/*` in plugin code. The single bridge file is `src/agent/plugin-tools.ts` (runtime layer, not plugin layer). The boundary is enforced by convention — no lint rule today, but `grep` confirms no `src/plugin/**` file imports `@mariozechner/*`.
|
|
27
|
+
|
|
28
|
+
**Allowed plugin imports**: `typeclaw/plugin`, `zod`, Node built-ins, your own modules.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 2. Minimum Viable Plugin
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// my-plugin.ts
|
|
36
|
+
import { definePlugin } from 'typeclaw/plugin'
|
|
37
|
+
|
|
38
|
+
export default definePlugin({
|
|
39
|
+
plugin: async (ctx) => ({
|
|
40
|
+
hooks: {
|
|
41
|
+
'session.prompt': (event) => {
|
|
42
|
+
event.prompt += `\n\n[plugin: ${ctx.name}]`
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
}),
|
|
46
|
+
})
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
That's it. No manifest. No `name`. No `version`. The plugin's name is **derived** at load time (see §4).
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 3. Plugin with Config (typed `ctx.config`)
|
|
54
|
+
|
|
55
|
+
`definePlugin` infers `TConfig` from the literal `configSchema`. **You never write the generic.**
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
import { z } from 'zod'
|
|
59
|
+
import { definePlugin, defineTool } from 'typeclaw/plugin'
|
|
60
|
+
|
|
61
|
+
export default definePlugin({
|
|
62
|
+
configSchema: z.object({
|
|
63
|
+
schedule: z.string().default('0 9 * * 1'),
|
|
64
|
+
journalDir: z.string().default('journal'),
|
|
65
|
+
}),
|
|
66
|
+
plugin: async (ctx) => {
|
|
67
|
+
// ctx.config is typed: { schedule: string; journalDir: string }
|
|
68
|
+
return {
|
|
69
|
+
cronJobs: {
|
|
70
|
+
'weekly-digest': {
|
|
71
|
+
schedule: ctx.config.schedule,
|
|
72
|
+
kind: 'prompt',
|
|
73
|
+
prompt: 'Compile this past week into a digest.',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
tools: {
|
|
77
|
+
lookup: defineTool({
|
|
78
|
+
description: 'Look up a journal entry by date.',
|
|
79
|
+
parameters: z.object({ date: z.string() }),
|
|
80
|
+
async execute(args, toolCtx) {
|
|
81
|
+
return { content: [{ type: 'text', text: `looked up ${args.date}` }] }
|
|
82
|
+
},
|
|
83
|
+
}),
|
|
84
|
+
},
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Without `configSchema`, `ctx.config` is `never` and any reference is a type error.
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## 4. Loading & Naming (typeclaw.json)
|
|
95
|
+
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"$schema": "./node_modules/typeclaw/typeclaw.schema.json",
|
|
99
|
+
"model": "fireworks/...",
|
|
100
|
+
"plugins": ["typeclaw-plugin-standup-log", "@acme/typeclaw-plugin-foo", "./plugins/local-thing"],
|
|
101
|
+
"standup-log": { "schedule": "0 17 * * 5" },
|
|
102
|
+
"foo": { "...": "..." },
|
|
103
|
+
"local-thing": { "...": "..." }
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Plugin name derivation (you do NOT declare it)
|
|
108
|
+
|
|
109
|
+
| Source | Rule | Example → Name |
|
|
110
|
+
| ----------- | --------------------------------------------------------- | --------------------------------------------- |
|
|
111
|
+
| NPM package | strip leading scope, then strip `typeclaw-plugin-` prefix | `@acme/typeclaw-plugin-foo` → `foo` |
|
|
112
|
+
| NPM package | strip `typeclaw-plugin-` prefix | `typeclaw-plugin-standup-log` → `standup-log` |
|
|
113
|
+
| NPM package | no prefix → use as-is | `my-cool-pkg` → `my-cool-pkg` |
|
|
114
|
+
| Local path | basename, strip extension | `./plugins/local-thing.ts` → `local-thing` |
|
|
115
|
+
|
|
116
|
+
The **derived name is the key** for the per-plugin config block at the top level of `typeclaw.json`. Two plugins with the same derived name are a boot error.
|
|
117
|
+
|
|
118
|
+
### Local path safety
|
|
119
|
+
|
|
120
|
+
Local plugin paths **must resolve inside `agentDir`**. Absolute paths (`/etc/...`) and parent-traversing paths (`../../foo`) are rejected with:
|
|
121
|
+
|
|
122
|
+
```
|
|
123
|
+
plugin path escapes agent directory: <entry> (resolved to <abs-path>)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
This is why `./plugins/x.ts` works and `/Users/me/x.ts` does not.
|
|
127
|
+
|
|
128
|
+
### Recommended location: `packages/<plugin-name>/`
|
|
129
|
+
|
|
130
|
+
The agent folder is a **bun monorepo**, and `packages/` is its workspace root. **Custom plugins go there.** A `./packages/standup-log/` plugin is a real workspace package — bun installs its dependencies, the workspace symlink machinery makes it importable, and it lands in git like any other reusable code. Concretely:
|
|
131
|
+
|
|
132
|
+
```
|
|
133
|
+
packages/
|
|
134
|
+
standup-log/
|
|
135
|
+
package.json
|
|
136
|
+
index.ts # exports default definePlugin({ ... })
|
|
137
|
+
index.test.ts
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
// typeclaw.json
|
|
142
|
+
{
|
|
143
|
+
"plugins": ["./packages/standup-log"],
|
|
144
|
+
"standup-log": { "schedule": "0 17 * * 5" }
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The derived plugin name is `standup-log` (basename of the path), so the per-plugin config block uses that key. Read the `typeclaw-monorepo` skill for the full package layout, dependency wiring (`workspace:*`), root-script conventions, and how to share code between multiple workspace packages.
|
|
149
|
+
|
|
150
|
+
Putting plugins anywhere else (a top-level `./plugins/` folder, a script under `workspace/`, an absolute path) works — but loses the workspace's dependency hoisting, gets you no `bun install` integration, and (for `workspace/`) silently disappears on the next clone because `workspace/` is gitignored.
|
|
151
|
+
|
|
152
|
+
### Boot-time effects
|
|
153
|
+
|
|
154
|
+
- `plugins` is a **`restart-required`** field. Editing the array (add/remove/reorder) needs `typeclaw restart` to take effect — `reload` won't pick it up.
|
|
155
|
+
- A factory throw, a `configSchema` rejection, a duplicate plugin name, or a duplicate tool/subagent/skill/cron name → **boot fails**. All registrations from the offending plugin are atomically rolled back.
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
## 5. The Contributions Object
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
type PluginExports = {
|
|
163
|
+
tools?: Record<string, Tool>
|
|
164
|
+
subagents?: Record<string, Subagent>
|
|
165
|
+
cronJobs?: Record<string, PluginCronJob>
|
|
166
|
+
skills?: Record<string, PluginSkill> // string-form
|
|
167
|
+
skillsDirs?: string[] // file-form (absolute paths)
|
|
168
|
+
hooks?: Hooks
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Every key is optional. The runtime reads each and wires it in.
|
|
173
|
+
|
|
174
|
+
### 5.1 `tools` — global names
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
import { z } from 'zod'
|
|
178
|
+
import { defineTool } from 'typeclaw/plugin'
|
|
179
|
+
|
|
180
|
+
tools: {
|
|
181
|
+
standup_query: defineTool({
|
|
182
|
+
description: 'Read past journal entries.',
|
|
183
|
+
parameters: z.object({ date: z.string().optional() }),
|
|
184
|
+
async execute(args, toolCtx) {
|
|
185
|
+
// toolCtx: { signal, sessionId, agentDir, logger }
|
|
186
|
+
return { content: [{ type: 'text', text: '...' }] }
|
|
187
|
+
},
|
|
188
|
+
}),
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
- Tool names are **global**. Two plugins cannot register the same name.
|
|
193
|
+
- `parameters` is a **Zod schema**. The runtime converts to JSON Schema via `z.toJSONSchema(schema, { io: 'input', reused: 'inline' })`.
|
|
194
|
+
- Args are **validated once** before `tool.before` hooks see them — no double-parse. Hooks receive `event.args` as a **mutable bag** (`Record<string, unknown>`); mutations propagate to later hooks and to `execute`.
|
|
195
|
+
- `ToolContext` is **stripped down** to `{ signal, sessionId, agentDir, logger }`. It does NOT expose the engine's `ExtensionContext`. If your tool wants `read`/`bash`/etc., it cannot call them — declare a subagent with `tools: [readTool, ...]` instead.
|
|
196
|
+
- `ToolResult.content` uses TypeClaw's `ContentPart` union: `{ type: 'text'; text }` or `{ type: 'image'; mimeType; data }`.
|
|
197
|
+
|
|
198
|
+
### 5.2 `subagents` — declarative
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import { z } from 'zod'
|
|
202
|
+
import { readTool, defineSubagent } from 'typeclaw/plugin'
|
|
203
|
+
|
|
204
|
+
subagents: {
|
|
205
|
+
'journal-writer': defineSubagent({
|
|
206
|
+
systemPrompt: 'You are a journal writer.',
|
|
207
|
+
tools: [readTool], // built-in refs (re-exported)
|
|
208
|
+
customTools: [appendTool], // plugin-defined tools, scoped to this subagent
|
|
209
|
+
payloadSchema: z.object({
|
|
210
|
+
parentSessionId: z.string(),
|
|
211
|
+
agentDir: z.string(),
|
|
212
|
+
}),
|
|
213
|
+
async handler(ctx, runSession) {
|
|
214
|
+
// ctx: { userPrompt, agentDir, payload }
|
|
215
|
+
await runSession({ userPrompt: buildPrompt(ctx.payload) })
|
|
216
|
+
},
|
|
217
|
+
}),
|
|
218
|
+
}
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
| Field | Required | Notes |
|
|
222
|
+
| --------------- | -------- | ---------------------------------------------------------------------------- |
|
|
223
|
+
| `systemPrompt` | yes | Replaces the main agent's system prompt entirely for the subagent's session. |
|
|
224
|
+
| `tools` | no | `BuiltinToolRef[]` — re-exported refs only. |
|
|
225
|
+
| `customTools` | no | `Tool[]` — visible only to this subagent, NOT to the main agent. |
|
|
226
|
+
| `payloadSchema` | no | Validated on every invocation. |
|
|
227
|
+
| `handler` | no | If absent, the runtime calls `runSession()` with the original user prompt. |
|
|
228
|
+
|
|
229
|
+
**Built-in tool refs** re-exported from `typeclaw/plugin`:
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
import { readTool, writeTool, editTool, bashTool, grepTool, findTool, lsTool } from 'typeclaw/plugin'
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
Subagent names are global; the runtime uses the name **verbatim** (not prefixed). Pick discriminating names (`journal-writer`, not `worker`).
|
|
236
|
+
|
|
237
|
+
`runSession({ userPrompt? })` resolves when the spawned session completes one prompt. The session is created and disposed inside the call.
|
|
238
|
+
|
|
239
|
+
### 5.3 `cronJobs` — prefixed global ids
|
|
240
|
+
|
|
241
|
+
```ts
|
|
242
|
+
cronJobs: {
|
|
243
|
+
'weekly-digest': {
|
|
244
|
+
schedule: '0 9 * * 1',
|
|
245
|
+
kind: 'prompt',
|
|
246
|
+
prompt: 'Compile this past week into a digest.',
|
|
247
|
+
subagent: 'journal-writer', // optional; routes through subagent registry
|
|
248
|
+
payload: { /* validated by journal-writer's payloadSchema at boot */ },
|
|
249
|
+
},
|
|
250
|
+
'log-rotate': {
|
|
251
|
+
schedule: '0 0 * * *',
|
|
252
|
+
kind: 'exec',
|
|
253
|
+
command: ['bun', 'run', 'scripts/rotate.ts'],
|
|
254
|
+
},
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
- The map key is a **suffix**. The runtime constructs the global cron id as `__plugin_<plugin-name>_<key>` (e.g., `__plugin_standup-log_weekly-digest`).
|
|
259
|
+
- `cron.json` user job ids cannot start with underscore, so collision is impossible by construction.
|
|
260
|
+
- A `prompt` job's `subagent` and `payload` are **validated against the registry at boot** — bad references fail loudly on disk, not 6 hours later when the job fires.
|
|
261
|
+
- Only two kinds: `prompt` and `exec`. Plugins do not extend the schema.
|
|
262
|
+
|
|
263
|
+
### 5.4 `skills` — string-form (per-session tmpdir)
|
|
264
|
+
|
|
265
|
+
```ts
|
|
266
|
+
skills: {
|
|
267
|
+
'standup-log': {
|
|
268
|
+
description: 'How to use the standup log.',
|
|
269
|
+
content: '# Standup log\n\n...',
|
|
270
|
+
frontmatter: { 'allowed-tools': ['standup_query'] },
|
|
271
|
+
},
|
|
272
|
+
}
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
- Materializes to a per-session tmpdir as `<sanitized-name>/SKILL.md` at session start. Disposed on websocket close.
|
|
276
|
+
- The map key becomes the skill's `name`. Names are **global** across plugins.
|
|
277
|
+
- Sanitization: lowercase, non-`[a-z0-9_-]` chars become `-`. Duplicate sanitized names throw at registration.
|
|
278
|
+
|
|
279
|
+
### 5.5 `skillsDirs` — file-form (paths)
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
import { join } from 'node:path'
|
|
283
|
+
|
|
284
|
+
skillsDirs: [join(import.meta.dir, 'skills')]
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Each path is added to the resource loader's skill paths verbatim. Discovery walks for `SKILL.md` files. **No collision check** on directory paths (intentional — multiple plugins can contribute different skills from the same dir).
|
|
288
|
+
|
|
289
|
+
### 5.6 `hooks`
|
|
290
|
+
|
|
291
|
+
```ts
|
|
292
|
+
hooks: {
|
|
293
|
+
'session.start': async (event, ctx) => { /* { sessionId, agentDir } */ },
|
|
294
|
+
'session.end': async (event, ctx) => { /* { sessionId } */ },
|
|
295
|
+
'session.idle': async (event, ctx) => { /* { sessionId, parentTranscriptPath, idleMs } */ },
|
|
296
|
+
'session.prompt': async (event, ctx) => {
|
|
297
|
+
event.prompt += `\n\n${await readToday(ctx.agentDir)}` // mutate by reassign
|
|
298
|
+
},
|
|
299
|
+
'tool.before': async (event, ctx) => {
|
|
300
|
+
// event.args is a MUTABLE BAG — mutate to rewrite, or:
|
|
301
|
+
if (event.args.danger === true) return { block: true, reason: 'unsafe' }
|
|
302
|
+
},
|
|
303
|
+
'tool.after': async (event, ctx) => {
|
|
304
|
+
// observe or transform event.result
|
|
305
|
+
},
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
| Hook | Direction | Payload | Notes |
|
|
310
|
+
| ---------------- | ------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
311
|
+
| `session.start` | observe | `{ sessionId, agentDir }` | Awaited before TUI gets `connected`. |
|
|
312
|
+
| `session.end` | observe | `{ sessionId }` | Awaited before close handler resolves. |
|
|
313
|
+
| `session.idle` | observe | `{ sessionId, parentTranscriptPath, idleMs }` | Fires **after every prompt completion** (success or error). The agent is "idle" the moment it stops responding. Plugins owning idle-debounced work (e.g. memory-logger spawn) install their own `setTimeout` and reset it on each event. `idleMs` is reserved (currently `0`). |
|
|
314
|
+
| `session.prompt` | intervene | `{ prompt, sessionId, agentDir }` | Reassign `event.prompt`. Runs once per session start, in plugin-load order. |
|
|
315
|
+
| `tool.before` | intervene | `{ tool, sessionId, callId, args }` | Fires for plugin-defined tools and TypeClaw-exposed system tools, including built-in pi tools when plugins are wired. Mutate `event.args`, or return `{ block: true, reason }`. First block short-circuits. |
|
|
316
|
+
| `tool.after` | observe / transform | `{ tool, sessionId, callId, result }` | Fires after plugin-defined tools and TypeClaw-exposed system tools. Observe `event.result`; tool result mutation is best-effort and tool-specific. |
|
|
317
|
+
|
|
318
|
+
**Multiple plugins** for the same hook run **in plugin-load order**. For `session.prompt`, the next plugin sees the previous plugin's mutated string.
|
|
319
|
+
|
|
320
|
+
#### CRITICAL: `session.prompt` and provider prompt caching
|
|
321
|
+
|
|
322
|
+
Provider prompt caching makes the **prefix** of the system prompt 5–10× cheaper on subsequent calls. Cache hits require **byte-identical prefixes**.
|
|
323
|
+
|
|
324
|
+
- **Append** to `event.prompt` → cache-safe. Always prefer this.
|
|
325
|
+
- **Prepend** or **replace** → invalidates the cache for every LLM call until the prompt changes again.
|
|
326
|
+
|
|
327
|
+
If your content varies per session, **append**. If it's stable across sessions, prepending is fine but understand the cost.
|
|
328
|
+
|
|
329
|
+
---
|
|
330
|
+
|
|
331
|
+
## 6. PluginContext
|
|
332
|
+
|
|
333
|
+
```ts
|
|
334
|
+
type PluginContext<TConfig = never> = {
|
|
335
|
+
readonly name: string // derived
|
|
336
|
+
readonly version: string | undefined // package.json (npm only)
|
|
337
|
+
readonly agentDir: string // absolute, agent folder root
|
|
338
|
+
readonly config: TConfig // inferred from configSchema
|
|
339
|
+
readonly logger: PluginLogger // prefixed: [plugin:<name>]
|
|
340
|
+
spawnSubagent: (name: string, payload?: unknown) => Promise<void>
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
### `spawnSubagent` boot gate
|
|
345
|
+
|
|
346
|
+
`spawnSubagent` is **gated until boot completes**. Calling it from inside the `plugin` factory throws:
|
|
347
|
+
|
|
348
|
+
```
|
|
349
|
+
plugin <name>: spawnSubagent("<x>") called before boot completed; subagent registry is not yet wired
|
|
350
|
+
```
|
|
351
|
+
|
|
352
|
+
Safe call sites: event handlers, tool `execute`, subagent handlers (subagents can spawn other subagents).
|
|
353
|
+
|
|
354
|
+
### What's NOT on `ctx`
|
|
355
|
+
|
|
356
|
+
No `ctx.stream`, no `ctx.server`, no `ctx.reloadRegistry`, no `ctx.registerX(...)`. **Everything contributed is in the returned object. Everything read is on `ctx`.** That's the entire surface.
|
|
357
|
+
|
|
358
|
+
---
|
|
359
|
+
|
|
360
|
+
## 7. Failure Modes (verbatim error messages)
|
|
361
|
+
|
|
362
|
+
When something goes wrong, you'll see one of these. Memorize the patterns.
|
|
363
|
+
|
|
364
|
+
| Trigger | Error |
|
|
365
|
+
| -------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
366
|
+
| Local path escapes agentDir | `plugin path escapes agent directory: <entry> (resolved to <abs>)` |
|
|
367
|
+
| Local path doesn't exist | `plugin path does not exist: <entry> (resolved to <abs>)` |
|
|
368
|
+
| Two plugins resolve to same name | `plugin name conflict: <name> (entry <entry>) already loaded` |
|
|
369
|
+
| Config doesn't match schema | `plugin <name>: config invalid: <zod issues>` |
|
|
370
|
+
| Config block exists but plugin has no schema | `plugin <name>: config block "<name>" present in typeclaw.json but plugin declares no configSchema` |
|
|
371
|
+
| Factory threw | `plugin <name>: factory threw: <error message>` |
|
|
372
|
+
| Tool name collision | `plugin <name>: tool "<tool>" already registered by plugin <other>` |
|
|
373
|
+
| Subagent name collision | `plugin <name>: subagent "<sub>" already registered by plugin <other>` |
|
|
374
|
+
| Skill name collision | `plugin <name>: skill "<skill>" already registered by plugin <other>` |
|
|
375
|
+
| Cron id collision | `plugin <name>: cron job "<id>" globalId "<global>" conflicts with plugin <other>` |
|
|
376
|
+
| Empty identifier | `plugin <name>: empty <kind>` (kind: tool name / subagent name / cron job id / skill name) |
|
|
377
|
+
| Skill name dup after sanitization | `plugin <name>: duplicate skill name after sanitization: <localName>` |
|
|
378
|
+
| `spawnSubagent` called too early | `plugin <name>: spawnSubagent("<x>") called before boot completed; subagent registry is not yet wired` |
|
|
379
|
+
|
|
380
|
+
**Atomic rollback**: on any of these (during load), every contribution from the offending plugin — tools, subagents, cron jobs, skills, skillsDirs, **and hooks** — is discarded before the error bubbles up. There is no partial state.
|
|
381
|
+
|
|
382
|
+
---
|
|
383
|
+
|
|
384
|
+
## 8. Common Pitfalls
|
|
385
|
+
|
|
386
|
+
### ❌ Importing the engine
|
|
387
|
+
|
|
388
|
+
```ts
|
|
389
|
+
// WRONG — boundary violation
|
|
390
|
+
import { something } from '@mariozechner/pi-coding-agent'
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Plugins use `typeclaw/plugin` only.** The runtime translates to engine types behind the scenes.
|
|
394
|
+
|
|
395
|
+
### ❌ Calling `spawnSubagent` from the factory
|
|
396
|
+
|
|
397
|
+
```ts
|
|
398
|
+
// WRONG — throws "called before boot completed"
|
|
399
|
+
plugin: async (ctx) => {
|
|
400
|
+
await ctx.spawnSubagent('worker', {}) // TOO EARLY
|
|
401
|
+
return {
|
|
402
|
+
/* ... */
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
```ts
|
|
408
|
+
// CORRECT — call it from a hook or tool
|
|
409
|
+
plugin: async (ctx) => ({
|
|
410
|
+
hooks: {
|
|
411
|
+
'session.idle': async () => {
|
|
412
|
+
await ctx.spawnSubagent('worker', {
|
|
413
|
+
/* ... */
|
|
414
|
+
}) // OK after boot
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
})
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
### ❌ Prepending in `session.prompt`
|
|
421
|
+
|
|
422
|
+
```ts
|
|
423
|
+
// WRONG — invalidates provider prompt cache on every call
|
|
424
|
+
'session.prompt': (event) => {
|
|
425
|
+
event.prompt = `[CONTEXT]\n${dynamicData}\n${event.prompt}`
|
|
426
|
+
}
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
```ts
|
|
430
|
+
// CORRECT — append (cache-safe)
|
|
431
|
+
'session.prompt': (event) => {
|
|
432
|
+
event.prompt += `\n\n[CONTEXT]\n${dynamicData}`
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### ❌ Assuming `tool.before/after` only cover plugin tools
|
|
437
|
+
|
|
438
|
+
`tool.before` / `tool.after` also intercept TypeClaw-exposed system tools, including `read`, `bash`, `edit`, `write`, etc. when plugins are wired into the session. Scope your hook by `event.tool` before mutating args or blocking.
|
|
439
|
+
|
|
440
|
+
### ❌ Forgetting plugin name derivation
|
|
441
|
+
|
|
442
|
+
```json
|
|
443
|
+
// WRONG — config block uses package name verbatim
|
|
444
|
+
{
|
|
445
|
+
"plugins": ["typeclaw-plugin-standup-log"],
|
|
446
|
+
"typeclaw-plugin-standup-log": { ... } // ignored! plugin sees empty config
|
|
447
|
+
}
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
```json
|
|
451
|
+
// CORRECT — config block uses DERIVED name
|
|
452
|
+
{
|
|
453
|
+
"plugins": ["typeclaw-plugin-standup-log"],
|
|
454
|
+
"standup-log": { ... }
|
|
455
|
+
}
|
|
456
|
+
```
|
|
457
|
+
|
|
458
|
+
### ❌ Editing `plugins[]` and expecting `reload` to apply it
|
|
459
|
+
|
|
460
|
+
`plugins` is `restart-required`. Run `typeclaw restart` after changing the array. The reload diff will tell you, but watch for it.
|
|
461
|
+
|
|
462
|
+
### ❌ Two plugins declaring the same global tool/subagent/skill name
|
|
463
|
+
|
|
464
|
+
Boot fails. Pick discriminating names. The runtime does NOT auto-prefix tool/subagent/skill names with the plugin name (only cron ids are prefixed with `__plugin_<name>_`).
|
|
465
|
+
|
|
466
|
+
### ❌ Calling built-in tools from inside a plugin tool's `execute`
|
|
467
|
+
|
|
468
|
+
Plugin `ToolContext` is `{ signal, sessionId, agentDir, logger }`. There is no `ctx.read()`, no `ctx.bash()`. Plugin tools are leaf operations. If your tool needs to chain built-ins, declare a subagent with `tools: [readTool, ...]` and let the LLM orchestrate.
|
|
469
|
+
|
|
470
|
+
---
|
|
471
|
+
|
|
472
|
+
## 9. Operational Reference
|
|
473
|
+
|
|
474
|
+
### Where things live
|
|
475
|
+
|
|
476
|
+
- **Plugin module source**: `src/plugin/` (types, define, loader, manager, registry, hooks, skills, context)
|
|
477
|
+
- **Engine bridge**: `src/agent/plugin-tools.ts` (the ONLY file that imports both plugin and engine types)
|
|
478
|
+
- **Plugin wiring at boot**: `src/run/index.ts` (`startAgent` calls `loadPlugins`, merges into registries)
|
|
479
|
+
- **Hook fire sites**:
|
|
480
|
+
- `session.prompt`: `src/agent/index.ts` `createResourceLoader` (after default prompt assembly)
|
|
481
|
+
- `session.idle`: `src/server/index.ts` `drain()` — fires immediately after every `session.prompt()` resolves (success or error)
|
|
482
|
+
- `session.start`/`session.end`: `src/server/index.ts` ws open/close
|
|
483
|
+
- `tool.before`/`tool.after`: `src/agent/plugin-tools.ts` `wrapPluginTool`, `wrapSystemTool`, and `wrapSystemAgentTool`
|
|
484
|
+
- **Schema additions**: `src/config/config.ts` (`plugins` array, `.catchall(z.unknown())` for per-plugin blocks, `extractPluginConfigs`)
|
|
485
|
+
|
|
486
|
+
### Audit log on boot
|
|
487
|
+
|
|
488
|
+
After successful load, the runtime emits to stdout:
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
[plugin] loaded N plugin(s): standup-log v0.1.0, foo (local)
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
Local plugins have no version. Use this to confirm what's actually loaded.
|
|
495
|
+
|
|
496
|
+
### Debugging a missing config
|
|
497
|
+
|
|
498
|
+
If `ctx.config.foo` is unexpectedly missing or default:
|
|
499
|
+
|
|
500
|
+
1. Verify the **derived plugin name** matches the top-level config block key in `typeclaw.json`.
|
|
501
|
+
2. Verify `configSchema` is on `definePlugin({ ... })`, not on the inner `plugin` function.
|
|
502
|
+
3. Check audit log for `plugin <name>: config invalid: ...` — defaults don't apply if the block fails validation.
|
|
503
|
+
|
|
504
|
+
### Debugging a not-firing hook
|
|
505
|
+
|
|
506
|
+
1. `session.start` / `session.end` are tied to **websocket** open/close. They don't fire during cron-only invocations.
|
|
507
|
+
2. `tool.before` / `tool.after` fire for plugin-defined and TypeClaw-exposed system tools only when plugins are wired into the session. Confirm the session loaded your plugin and check `event.tool` matches the expected tool name.
|
|
508
|
+
3. Hooks that throw are logged (`reportHookError`) and do NOT abort the loop. Check the plugin logger output.
|
|
509
|
+
|
|
510
|
+
### Restart vs reload
|
|
511
|
+
|
|
512
|
+
| Change | Effect |
|
|
513
|
+
| ---------------------------------- | --------------------------------------------------------- |
|
|
514
|
+
| Edit a hook handler body | Container restart (new code) |
|
|
515
|
+
| Edit a tool's `execute` body | Container restart |
|
|
516
|
+
| Add/remove an entry in `plugins[]` | Container restart (`restart-required`) |
|
|
517
|
+
| Change a per-plugin config value | Container restart (factory only runs at boot) |
|
|
518
|
+
| Edit `cron.json` (non-plugin) | Reload picks it up (existing `cron.json` reload pipeline) |
|
|
519
|
+
|
|
520
|
+
When in doubt: `typeclaw restart`.
|
|
521
|
+
|
|
522
|
+
---
|
|
523
|
+
|
|
524
|
+
## 10. Anti-Goals (intentionally NOT supported)
|
|
525
|
+
|
|
526
|
+
If you find yourself wanting any of these, the design has gone wrong somewhere — file an issue rather than working around it:
|
|
527
|
+
|
|
528
|
+
- **Plugin sandboxing**. Plugins run with full Bun privileges. The container is the sandbox.
|
|
529
|
+
- **Hot plugin reload**. `typeclaw restart` to pick up plugin code or config changes.
|
|
530
|
+
- **Stream subscriptions**. Plugins observe through the typed `hooks` surface; they cannot subscribe to the in-process pub/sub directly.
|
|
531
|
+
- **Server-side TUI push notifications** from plugin code. Tool calls reach the TUI via existing `tool_start`/`tool_end` events.
|
|
532
|
+
- **Dockerfile fragments** contributed by plugins. The Dockerfile is core-managed.
|
|
533
|
+
- **New cron job kinds** beyond `prompt` and `exec`. (Subagent invocation is a `prompt` variant, not a separate kind.)
|
|
534
|
+
- **Reload-registry scopes** for plugin-owned state.
|
|
535
|
+
- **Host-stage CLI commands** registered by plugins. Plugins are container-stage only.
|
|
536
|
+
- **`extendConfig`** for arbitrary top-level fields outside the plugin's own config block.
|
|
537
|
+
- **Per-LLM-call hooks** (`llm.params` / `llm.headers`). Wait until a real plugin needs them.
|
|
538
|
+
|
|
539
|
+
---
|
|
540
|
+
|
|
541
|
+
## 11. Quick Reference Card
|
|
542
|
+
|
|
543
|
+
```ts
|
|
544
|
+
import { z } from 'zod'
|
|
545
|
+
import {
|
|
546
|
+
definePlugin, // wrap module
|
|
547
|
+
defineTool, // (optional, identity helper for type inference)
|
|
548
|
+
defineSubagent, // (optional, identity helper)
|
|
549
|
+
// built-in tool refs:
|
|
550
|
+
readTool,
|
|
551
|
+
writeTool,
|
|
552
|
+
editTool,
|
|
553
|
+
bashTool,
|
|
554
|
+
grepTool,
|
|
555
|
+
findTool,
|
|
556
|
+
lsTool,
|
|
557
|
+
// types:
|
|
558
|
+
type PluginContext,
|
|
559
|
+
type PluginExports,
|
|
560
|
+
type Tool,
|
|
561
|
+
type Subagent,
|
|
562
|
+
type ToolContext,
|
|
563
|
+
type ToolResult,
|
|
564
|
+
type ContentPart,
|
|
565
|
+
type Hooks,
|
|
566
|
+
type SessionPromptEvent,
|
|
567
|
+
type ToolBeforeEvent,
|
|
568
|
+
} from 'typeclaw/plugin'
|
|
569
|
+
```
|
|
570
|
+
|
|
571
|
+
**Plugin shape**:
|
|
572
|
+
|
|
573
|
+
```ts
|
|
574
|
+
export default definePlugin({
|
|
575
|
+
configSchema: z.object({
|
|
576
|
+
/* ... */
|
|
577
|
+
}), // optional
|
|
578
|
+
plugin: async (ctx) => ({
|
|
579
|
+
// required
|
|
580
|
+
tools,
|
|
581
|
+
subagents,
|
|
582
|
+
cronJobs,
|
|
583
|
+
skills,
|
|
584
|
+
skillsDirs,
|
|
585
|
+
hooks, // all optional
|
|
586
|
+
}),
|
|
587
|
+
})
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
**Cron global id**: `__plugin_<plugin-name>_<key>`
|
|
591
|
+
|
|
592
|
+
**Plugin name = derived**: scope-stripped, `typeclaw-plugin-` prefix stripped (npm), or basename minus extension (local).
|
|
593
|
+
|
|
594
|
+
**Boundary**: `src/plugin/**` MUST NOT import `@mariozechner/*`.
|