@dynamicagents/plugins 0.11.1 → 0.13.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/README.md +59 -79
- package/dist/browser/index.d.ts +0 -2
- package/dist/browser/index.js +21 -27
- package/dist/claude-code/cards.d.ts +53 -0
- package/dist/claude-code/cards.js +272 -0
- package/dist/claude-code/config.d.ts +73 -31
- package/dist/claude-code/config.js +0 -2
- package/dist/claude-code/credentials.d.ts +21 -9
- package/dist/claude-code/credentials.js +43 -9
- package/dist/claude-code/egress.d.ts +15 -14
- package/dist/claude-code/egress.js +51 -14
- package/dist/claude-code/events.d.ts +153 -9
- package/dist/claude-code/events.js +289 -58
- package/dist/claude-code/github.d.ts +39 -0
- package/dist/claude-code/github.js +82 -0
- package/dist/claude-code/index.d.ts +24 -80
- package/dist/claude-code/index.js +19 -234
- package/dist/claude-code/model.d.ts +152 -0
- package/dist/claude-code/model.js +342 -0
- package/dist/claude-code/run.d.ts +283 -92
- package/dist/claude-code/run.js +620 -114
- package/dist/claude-code/session.d.ts +131 -0
- package/dist/claude-code/session.js +285 -0
- package/dist/claude-code/spec.d.ts +46 -0
- package/dist/claude-code/spec.js +83 -0
- package/dist/claude-code/verified.d.ts +15 -0
- package/dist/claude-code/verified.js +15 -0
- package/dist/computer/context.d.ts +19 -0
- package/dist/computer/context.js +222 -0
- package/dist/computer/file-lock.d.ts +5 -0
- package/dist/computer/file-lock.js +40 -0
- package/dist/computer/gate.d.ts +12 -12
- package/dist/computer/gate.js +12 -12
- package/dist/computer/index.d.ts +57 -255
- package/dist/computer/index.js +98 -1039
- package/dist/computer/paths.d.ts +47 -22
- package/dist/computer/paths.js +64 -49
- package/dist/computer/proxy.d.ts +45 -0
- package/dist/computer/proxy.js +234 -0
- package/dist/computer/render.d.ts +27 -38
- package/dist/computer/render.js +32 -65
- package/dist/computer/tools-exec.d.ts +3 -0
- package/dist/computer/tools-exec.js +268 -0
- package/dist/computer/tools-file.d.ts +12 -0
- package/dist/computer/tools-file.js +64 -0
- package/dist/computer/tools-grep.d.ts +3 -0
- package/dist/computer/tools-grep.js +129 -0
- package/dist/repo/checkout.d.ts +35 -6
- package/dist/repo/checkout.js +64 -6
- package/dist/repo/context.d.ts +135 -0
- package/dist/repo/context.js +582 -0
- package/dist/repo/index.d.ts +79 -57
- package/dist/repo/index.js +118 -1112
- package/dist/repo/tools-checks.d.ts +3 -0
- package/dist/repo/tools-checks.js +112 -0
- package/dist/repo/tools-clone.d.ts +4 -0
- package/dist/repo/tools-clone.js +192 -0
- package/dist/repo/tools-pr.d.ts +14 -0
- package/dist/repo/tools-pr.js +196 -0
- package/dist/repo/tools-review.d.ts +12 -0
- package/dist/repo/tools-review.js +393 -0
- package/dist/repo/tools-worktree.d.ts +4 -0
- package/dist/repo/tools-worktree.js +305 -0
- package/dist/repo/tools-worktrees.d.ts +12 -0
- package/dist/repo/tools-worktrees.js +48 -0
- package/dist/repo/url.d.ts +2 -2
- package/dist/repo/url.js +7 -7
- package/dist/scratch/index.d.ts +4 -4
- package/dist/scratch/index.js +12 -11
- package/dist/{computer → workspace}/advisory.d.ts +33 -20
- package/dist/{computer → workspace}/advisory.js +32 -7
- package/dist/workspace/ca-trust.d.ts +95 -0
- package/dist/workspace/ca-trust.js +312 -0
- package/dist/workspace/container-deps.d.ts +54 -0
- package/dist/workspace/container-deps.js +224 -0
- package/dist/workspace/container-fault.d.ts +38 -0
- package/dist/workspace/container-fault.js +73 -0
- package/dist/workspace/exec.d.ts +94 -0
- package/dist/workspace/exec.js +153 -0
- package/dist/workspace/format.d.ts +36 -0
- package/dist/workspace/format.js +57 -0
- package/dist/workspace/git-host.d.ts +79 -0
- package/dist/workspace/git-host.js +226 -0
- package/dist/workspace/git-identity.d.ts +57 -0
- package/dist/workspace/git-identity.js +147 -0
- package/dist/workspace/index.d.ts +43 -47
- package/dist/workspace/index.js +52 -112
- package/dist/workspace/install-job.d.ts +121 -0
- package/dist/workspace/install-job.js +866 -0
- package/dist/{computer → workspace}/install.d.ts +15 -19
- package/dist/{computer → workspace}/install.js +8 -11
- package/dist/workspace/object.d.ts +502 -0
- package/dist/workspace/object.js +1427 -0
- package/dist/workspace/open.d.ts +95 -0
- package/dist/workspace/open.js +63 -0
- package/dist/workspace/read.d.ts +16 -0
- package/dist/workspace/read.js +22 -0
- package/dist/workspace/shell.d.ts +45 -0
- package/dist/workspace/shell.js +80 -0
- package/dist/workspace/sync.d.ts +111 -0
- package/dist/workspace/sync.js +124 -0
- package/dist/workspace/wake.d.ts +19 -0
- package/dist/workspace/wake.js +10 -0
- package/package.json +28 -42
- package/dist/arc-agi/analysis.d.ts +0 -336
- package/dist/arc-agi/analysis.js +0 -829
- package/dist/arc-agi/client.d.ts +0 -82
- package/dist/arc-agi/client.js +0 -113
- package/dist/arc-agi/game-tools.d.ts +0 -25
- package/dist/arc-agi/game-tools.js +0 -26
- package/dist/arc-agi/index.d.ts +0 -42
- package/dist/arc-agi/index.js +0 -175
- package/dist/arc-agi/main-agent.d.ts +0 -27
- package/dist/arc-agi/main-agent.js +0 -65
- package/dist/arc-agi/recipe.d.ts +0 -51
- package/dist/arc-agi/recipe.js +0 -100
- package/dist/arc-agi/scorecard.d.ts +0 -118
- package/dist/arc-agi/scorecard.js +0 -163
- package/dist/arc-agi/soul.d.ts +0 -40
- package/dist/arc-agi/soul.js +0 -90
- package/dist/arc-agi/store.d.ts +0 -154
- package/dist/arc-agi/store.js +0 -216
- package/dist/arc-agi/tools.d.ts +0 -36
- package/dist/arc-agi/tools.js +0 -576
- package/dist/arc-agi/types.d.ts +0 -212
- package/dist/arc-agi/types.js +0 -19
- package/dist/claude-code/recipe.d.ts +0 -53
- package/dist/claude-code/recipe.js +0 -100
- package/dist/computer/read.d.ts +0 -137
- package/dist/computer/read.js +0 -203
- package/dist/recall/index.d.ts +0 -99
- package/dist/recall/index.js +0 -171
- package/dist/triage/index.d.ts +0 -355
- package/dist/triage/index.js +0 -302
package/README.md
CHANGED
|
@@ -18,24 +18,32 @@ npm install @dynamicagents/plugins
|
|
|
18
18
|
|
|
19
19
|
## The one file you edit
|
|
20
20
|
|
|
21
|
+
An agent installs plugins by listing them in `getPlugins()`, on a `StepAgent` from
|
|
22
|
+
`@dynamicagents/core/agent` or a `SubAgent` from `@dynamicagents/core/subagent`:
|
|
23
|
+
|
|
21
24
|
```ts
|
|
22
|
-
// src/
|
|
23
|
-
import {
|
|
25
|
+
// src/agents/coder/agent.ts
|
|
26
|
+
import { StepAgent } from "@dynamicagents/core/agent";
|
|
24
27
|
import { browser } from "@dynamicagents/plugins/browser";
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
28
|
+
import { computer, computerWorkspace } from "@dynamicagents/plugins/computer";
|
|
29
|
+
// `workspaceConfig` names the `/workspace` object this agent works in.
|
|
30
|
+
import { repo } from "@dynamicagents/plugins/repo";
|
|
31
|
+
|
|
32
|
+
export class Coder extends StepAgent<Env> {
|
|
33
|
+
// `computer` runs commands in a container, so the agent's own workspace —
|
|
34
|
+
// what Think's `read` and `write` work on — has to be that container's.
|
|
35
|
+
override workspace = computerWorkspace(workspaceConfig(this.env), () =>
|
|
36
|
+
this.pluginContext().runtime()
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
override getPlugins() {
|
|
40
|
+
return [
|
|
41
|
+
browser({ binding: this.env.BROWSER }),
|
|
42
|
+
computer(workspaceConfig(this.env)),
|
|
43
|
+
repo(repoConfig(this.env))
|
|
44
|
+
];
|
|
45
|
+
}
|
|
32
46
|
}
|
|
33
|
-
|
|
34
|
-
export const plugins = ({ env, storage, callerKey }: PluginHost) => [
|
|
35
|
-
arcAgi({ apiKey: env.ARC_API_KEY, storage }),
|
|
36
|
-
browser({ binding: env.BROWSER }),
|
|
37
|
-
recall({ ai: env.AI, index: env.VECTORIZE, namespace: callerKey })
|
|
38
|
-
];
|
|
39
47
|
```
|
|
40
48
|
|
|
41
49
|
Delete a line and that module leaves your bundle entirely. Nothing in core imports a plugin,
|
|
@@ -43,54 +51,27 @@ and there is **no root barrel** — `@dynamicagents/plugins` on its own does not
|
|
|
43
51
|
guarantee is structural rather than a tree-shaker's opinion. `npm run verify:exports` asserts
|
|
44
52
|
it on the built graph before every publish.
|
|
45
53
|
|
|
46
|
-
`
|
|
47
|
-
scope
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
export class MyAgent extends Agent<Env> {
|
|
51
|
-
/** Set on the first verified request; constant thereafter. See below. */
|
|
52
|
-
private identity?: string;
|
|
53
|
-
|
|
54
|
-
async onStart() {
|
|
55
|
-
this.runtime = createAgentRuntime({
|
|
56
|
-
config,
|
|
57
|
-
plugins: plugins({
|
|
58
|
-
env: this.env,
|
|
59
|
-
storage: this.ctx.storage,
|
|
60
|
-
// A thunk, not a value: `onStart` runs before any request, so the caller
|
|
61
|
-
// is not known yet. The DO is keyed 1:1 by that caller, so it is constant
|
|
62
|
-
// once it is — this just defers reading it until it exists.
|
|
63
|
-
callerKey: () => this.identity ?? ""
|
|
64
|
-
}),
|
|
65
|
-
env: this.env // verify every plugin's declared bindings exist, at startup
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async onTurn(turn: AgentTurn, identity: GatekeeperIdentity) {
|
|
70
|
-
this.identity ??= identity.key!;
|
|
71
|
-
// …
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
```
|
|
54
|
+
`getPlugins()` is a method, not a module-level array: on Workers `env` does not exist at
|
|
55
|
+
module scope. Core checks the list when the agent starts — every declared binding present,
|
|
56
|
+
every tool name offered once — so a wiring fault fails the start with a sentence naming the
|
|
57
|
+
plugin, not the first turn.
|
|
75
58
|
|
|
76
|
-
|
|
77
|
-
|
|
59
|
+
A plugin's tools are the same wherever it is installed, so what a sub-agent must not have is
|
|
60
|
+
left out of its own `getPlugins()`, and `restrictTools` from `@dynamicagents/core` narrows a
|
|
61
|
+
plugin to the tools an install names.
|
|
78
62
|
|
|
79
63
|
---
|
|
80
64
|
|
|
81
65
|
## The plugins
|
|
82
66
|
|
|
83
|
-
| Subpath | What it adds
|
|
84
|
-
| ---------------------------------- |
|
|
85
|
-
| [`/
|
|
86
|
-
| [`/
|
|
87
|
-
| [`/
|
|
88
|
-
| [`/
|
|
89
|
-
| [`/
|
|
90
|
-
| [`/
|
|
91
|
-
| [`/scratch`](src/scratch/) | A throwaway git repository with no remote, for work that needs a container but no checkout | — |
|
|
92
|
-
| [`/triage`](src/triage/) | A pre-turn gate: is this message even for me? | — |
|
|
93
|
-
| [`/workspace`](src/workspace/) | A durable file store for long subagent runs, plus tools over it | `@cloudflare/shell` |
|
|
67
|
+
| Subpath | What it adds | Needs |
|
|
68
|
+
| ---------------------------------- | ---------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
|
|
69
|
+
| [`/browser`](src/browser/) | Read web pages via Browser Rendering Quick Actions | `BROWSER` (paid plan) |
|
|
70
|
+
| [`/claude-code`](src/claude-code/) | Sub-agents whose model is a Claude Code session in the workspace container | one or more `claude setup-token` credentials |
|
|
71
|
+
| [`/computer`](src/computer/) | An agent's tools over a workspace — `bash`, `grep`, `edit` — and the workspace Think's file tools use | a `/workspace` object |
|
|
72
|
+
| [`/repo`](src/repo/) | Clone, commit, push a branch, open a pull request — over any container | `GITHUB_TOKEN` |
|
|
73
|
+
| [`/scratch`](src/scratch/) | A throwaway git repository with no remote, for work that needs a container but no checkout | — |
|
|
74
|
+
| [`/workspace`](src/workspace/) | The Durable Object a container workspace lives in — its container, credentialed git, dependency install and sync | `@cloudflare/computer`, `@platformatic/vfs` (paid) |
|
|
94
75
|
|
|
95
76
|
Each directory has its own README with the config shape and a paste-ready `wrangler.jsonc`
|
|
96
77
|
snippet — a plugin cannot add its own binding, which is why it declares what it needs.
|
|
@@ -101,18 +82,21 @@ snippet — a plugin cannot add its own binding, which is why it declares what i
|
|
|
101
82
|
|
|
102
83
|
```ts
|
|
103
84
|
import { definePlugin, type AgentPlugin } from "@dynamicagents/core";
|
|
85
|
+
import { tool } from "ai";
|
|
104
86
|
|
|
105
|
-
export function scraper(config: { apiKey: string }): AgentPlugin {
|
|
87
|
+
export function scraper(config: { apiKey: () => string }): AgentPlugin {
|
|
106
88
|
return definePlugin({
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
89
|
+
name: "scraper",
|
|
90
|
+
tools: (ctx) => ({ fetch_page: tool({/* … */}) }),
|
|
91
|
+
context: [
|
|
92
|
+
{ provider: { get: async () => "You can fetch and summarize a page." } }
|
|
93
|
+
],
|
|
110
94
|
requires: { secrets: ["SCRAPER_API_KEY"] }
|
|
111
95
|
});
|
|
112
96
|
}
|
|
113
97
|
```
|
|
114
98
|
|
|
115
|
-
|
|
99
|
+
The rules the whole design rests on:
|
|
116
100
|
|
|
117
101
|
- **Never name a consumer's `Env`.** It is an ambient interface `wrangler types` generates
|
|
118
102
|
into _their_ app. Take bindings and secrets as config, which is also the only thing that
|
|
@@ -120,8 +104,17 @@ Three rules the whole design rests on:
|
|
|
120
104
|
- **`definePlugin` sets `contractVersion`** from the core you compiled against. Never write
|
|
121
105
|
that number as a literal — the point is that it moves, so a version train that leaves one
|
|
122
106
|
repo behind fails at startup with a sentence instead of a structural-type error.
|
|
123
|
-
- **
|
|
124
|
-
|
|
107
|
+
- **Give a context block a get-only provider.** A block with no provider is wired as a
|
|
108
|
+
writable one, which the model can overwrite with `set_context`.
|
|
109
|
+
- **Read `ctx.runtime()` inside `execute`, never while building tools.** It is what a
|
|
110
|
+
sub-agent's spec prepared for the running dispatch, and it belongs to the turn.
|
|
111
|
+
|
|
112
|
+
A write that must not happen twice — a comment, a reply — is a Think `action()` in
|
|
113
|
+
`actions(ctx)`, with an idempotency key, so a recovered turn replays it rather than
|
|
114
|
+
repeating it. [`/repo`](src/repo/) has two.
|
|
115
|
+
|
|
116
|
+
A plugin that owns a domain some work is delegated into exports a `SubAgentSpec` as data,
|
|
117
|
+
and the agent binds it to a `SubAgent` class — see [`/claude-code`](src/claude-code/).
|
|
125
118
|
|
|
126
119
|
## Testing
|
|
127
120
|
|
|
@@ -130,8 +123,7 @@ Specs run inside real workerd via `@cloudflare/vitest-plugin`, with the harness
|
|
|
130
123
|
|
|
131
124
|
```bash
|
|
132
125
|
npm test # the whole suite, no credentials and no network
|
|
133
|
-
npm run check # prettier + eslint + tsc + build
|
|
134
|
-
npm run verify:exports
|
|
126
|
+
npm run check # prettier + eslint + tsc + build, then verify:exports over it
|
|
135
127
|
```
|
|
136
128
|
|
|
137
129
|
### Working against an unpublished core
|
|
@@ -153,18 +145,6 @@ gives it its own copy of every peer. Two copies of `agents` in one bundle breaks
|
|
|
153
145
|
`Session` types and every `instanceof`, at runtime rather than at the type level.
|
|
154
146
|
Nothing is written to `package.json`, so CI never builds against a local checkout.
|
|
155
147
|
|
|
156
|
-
`test/arc-agi/recorded.spec.ts` drives the **real** ARC API and replays a committed
|
|
157
|
-
cassette, so it needs no key either. Re-record it against the live API with:
|
|
158
|
-
|
|
159
|
-
```bash
|
|
160
|
-
npm run test:record # real ARC_API_KEY in .env.test (see .env.test.example)
|
|
161
|
-
```
|
|
162
|
-
|
|
163
|
-
The key reaches the live ARC API and nothing else: the recorder excludes the auth header, so
|
|
164
|
-
it never lands in the committed cassette. It reaches the spec only as a Miniflare binding —
|
|
165
|
-
`.env.test` is loaded into **Node's** `process.env`, which a spec running in workerd cannot
|
|
166
|
-
see, so `vitest.config.ts` hands it across explicitly.
|
|
167
|
-
|
|
168
148
|
## License
|
|
169
149
|
|
|
170
|
-
|
|
150
|
+
Apache-2.0
|
package/dist/browser/index.d.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { AgentPlugin } from "@dynamicagents/core";
|
|
2
2
|
import type { QuickActionToolName } from "agents/browser/ai";
|
|
3
3
|
import type { QuickActionBinding, QuickActionCommonOptions } from "agents/browser";
|
|
4
|
-
/** This plugin's tool-family name, as a recipe's `toolFamilies` lists it. */
|
|
5
|
-
export declare const BROWSER_FAMILY = "browser";
|
|
6
4
|
export interface BrowserConfig {
|
|
7
5
|
/**
|
|
8
6
|
* The `BROWSER` binding. Browser Rendering is a **paid-plan** feature, and
|
package/dist/browser/index.js
CHANGED
|
@@ -5,41 +5,35 @@ import { createQuickActionTools } from "agents/browser/ai";
|
|
|
5
5
|
* Quick Actions.
|
|
6
6
|
*
|
|
7
7
|
* Four tools (`browser_markdown`, `browser_extract`, `browser_links`,
|
|
8
|
-
* `browser_scrape`),
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* subtask reads a dozen of them. Nothing about the tools differs between the two
|
|
12
|
-
* — only who is holding them.
|
|
8
|
+
* `browser_scrape`), the same for whichever agent installs the plugin: a parent
|
|
9
|
+
* reads a page to answer a question directly, and a research sub-agent reads a
|
|
10
|
+
* dozen of them.
|
|
13
11
|
*
|
|
14
12
|
* The binding is closed over at instantiation, so it is never model input.
|
|
15
13
|
*/
|
|
16
14
|
/** How many characters of a page result the model is allowed to see. */
|
|
17
15
|
const DEFAULT_MAX_CHARS = 20_000;
|
|
18
|
-
|
|
19
|
-
|
|
16
|
+
const CONTEXT = [
|
|
17
|
+
"You can read web pages directly:",
|
|
18
|
+
"- `browser_markdown` fetches a page and returns it as Markdown. This is the default — use it first.",
|
|
19
|
+
"- `browser_extract` pulls structured fields out of a page when you know what you are looking for.",
|
|
20
|
+
"- `browser_links` lists a page's links, for following a trail.",
|
|
21
|
+
"- `browser_scrape` returns the text of specific CSS selectors.",
|
|
22
|
+
"Results are truncated to protect your context window, so prefer a specific page over a search-results page, and extract rather than reading in full when you already know the fields you need."
|
|
23
|
+
].join("\n");
|
|
20
24
|
export function browser(config) {
|
|
21
25
|
const { binding, maxChars = DEFAULT_MAX_CHARS, actions, options } = config;
|
|
22
|
-
// One builder, called per surface rather than once here. The SDK returns a
|
|
23
|
-
// fresh ToolSet each call and a tool object is not obviously safe to share
|
|
24
|
-
// across two concurrent executions; building twice costs nothing.
|
|
25
|
-
const tools = () => createQuickActionTools({
|
|
26
|
-
browser: binding,
|
|
27
|
-
maxChars,
|
|
28
|
-
...(actions ? { actions } : {}),
|
|
29
|
-
...(options ? { options } : {})
|
|
30
|
-
});
|
|
31
26
|
return definePlugin({
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
].join("\n"),
|
|
27
|
+
name: "browser",
|
|
28
|
+
// Built per call: the SDK returns a fresh ToolSet each time, and a tool
|
|
29
|
+
// object is not obviously safe to share across two concurrent executions.
|
|
30
|
+
tools: () => createQuickActionTools({
|
|
31
|
+
browser: binding,
|
|
32
|
+
maxChars,
|
|
33
|
+
...(actions ? { actions } : {}),
|
|
34
|
+
...(options ? { options } : {})
|
|
35
|
+
}),
|
|
36
|
+
context: [{ provider: { get: async () => CONTEXT } }],
|
|
43
37
|
// A plugin cannot add its own wrangler binding, so declaring it turns a
|
|
44
38
|
// missing one into a startup error naming this plugin, rather than a failed
|
|
45
39
|
// tool call inside a request someone is waiting on.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type ArtifactEntryDetail } from "@dynamicagents/core/artifacts";
|
|
2
|
+
import type { ClaudeCodeEvent, ClaudeCodeResult } from "./events.js";
|
|
3
|
+
/**
|
|
4
|
+
* A Claude Code session's stream as cards on its parent's transcript — the
|
|
5
|
+
* only place in this package that knows what each of the CLI's tools is.
|
|
6
|
+
*
|
|
7
|
+
* Modelled on how the CLI shows them itself: a tool and the thing it acted on
|
|
8
|
+
* on one line, the input and the output folded beneath. A tool's call and its
|
|
9
|
+
* result are two cards sharing the tool-use id as their `ref`, and the
|
|
10
|
+
* transcript folds the second into the first — see core's
|
|
11
|
+
* `ArtifactEntryDetail`. Both are pure functions of one event, because the
|
|
12
|
+
* drain that calls them is re-entrant (see `./events.ts`): a result is
|
|
13
|
+
* formatted from its own line, never from the call it answers.
|
|
14
|
+
*
|
|
15
|
+
* Every section is clipped here, before the note is persisted anywhere — see
|
|
16
|
+
* core's `ENTRY_SECTION_MAX_CHARS` for the ceiling that protects.
|
|
17
|
+
*/
|
|
18
|
+
/** A note's text and its card. */
|
|
19
|
+
export interface Card {
|
|
20
|
+
text: string;
|
|
21
|
+
detail: ArtifactEntryDetail;
|
|
22
|
+
}
|
|
23
|
+
type ToolResultEvent = Extract<ClaudeCodeEvent, {
|
|
24
|
+
kind: "toolResult";
|
|
25
|
+
}>;
|
|
26
|
+
type InitEvent = Extract<ClaudeCodeEvent, {
|
|
27
|
+
kind: "init";
|
|
28
|
+
}>;
|
|
29
|
+
/** A tool call: the card its result will complete. */
|
|
30
|
+
export declare function toolCallCard(id: string, name: string, raw: unknown): Card;
|
|
31
|
+
/**
|
|
32
|
+
* A tool's result: the half that completes its call's card.
|
|
33
|
+
*
|
|
34
|
+
* Read off the tool's own record (`structured`) where it has one, and off the
|
|
35
|
+
* text the model was given where it does not. The record's shape is what
|
|
36
|
+
* names the tool — the result line carries only the call's id.
|
|
37
|
+
*/
|
|
38
|
+
export declare function toolResultCard(event: ToolResultEvent): Card;
|
|
39
|
+
/**
|
|
40
|
+
* A thinking block, as the CLI says it: "Thought for 5s", or "Thought" when it
|
|
41
|
+
* was not timed. What it thought is never on the card — see the `thinking`
|
|
42
|
+
* event in `./events.ts`.
|
|
43
|
+
*
|
|
44
|
+
* Its message is its `ref`, so a message's blocks are one card: a message
|
|
45
|
+
* that thinks twice ends on a second block of a few milliseconds, and the CLI
|
|
46
|
+
* shows the two as one thought.
|
|
47
|
+
*/
|
|
48
|
+
export declare function thinkingCard(durationMs: number | undefined, messageId: string | undefined): Card;
|
|
49
|
+
/** A session starting — every exec, a resume or a follow-up included. */
|
|
50
|
+
export declare function initCard(event: InitEvent): Card;
|
|
51
|
+
/** A session's end, with what its `result` line says about it. */
|
|
52
|
+
export declare function resultCard(result: ClaudeCodeResult): Card;
|
|
53
|
+
export {};
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { clipEntryBody } from "@dynamicagents/core/artifacts";
|
|
2
|
+
/** The most of a summary line a card's head shows. */
|
|
3
|
+
const SUMMARY_MAX_CHARS = 120;
|
|
4
|
+
function record(value) {
|
|
5
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
6
|
+
? value
|
|
7
|
+
: {};
|
|
8
|
+
}
|
|
9
|
+
function str(value) {
|
|
10
|
+
return typeof value === "string" && value ? value : undefined;
|
|
11
|
+
}
|
|
12
|
+
/** The first line of `text`, clipped to a card's head. */
|
|
13
|
+
function oneLine(text) {
|
|
14
|
+
const line = text.trim().split("\n")[0]?.trim() ?? "";
|
|
15
|
+
return line.length <= SUMMARY_MAX_CHARS
|
|
16
|
+
? line
|
|
17
|
+
: `${line.slice(0, SUMMARY_MAX_CHARS - 1)}…`;
|
|
18
|
+
}
|
|
19
|
+
/** The most of a path's end a card's head keeps. */
|
|
20
|
+
const PATH_SEGMENTS = 3;
|
|
21
|
+
/**
|
|
22
|
+
* A path as a card's head shows it: its last segments, since the head is one
|
|
23
|
+
* line and clips at its end — where the file's name is. A checkout's absolute
|
|
24
|
+
* prefix is the same on every card and tells a reader nothing.
|
|
25
|
+
*/
|
|
26
|
+
function shortPath(path) {
|
|
27
|
+
if (!path?.startsWith("/"))
|
|
28
|
+
return path;
|
|
29
|
+
const parts = path.split("/").filter(Boolean);
|
|
30
|
+
return parts.length > PATH_SEGMENTS + 1
|
|
31
|
+
? `…/${parts.slice(-PATH_SEGMENTS).join("/")}`
|
|
32
|
+
: path;
|
|
33
|
+
}
|
|
34
|
+
function section(label, body, format) {
|
|
35
|
+
return { label, body: clipEntryBody(body), ...(format ? { format } : {}) };
|
|
36
|
+
}
|
|
37
|
+
function json(value) {
|
|
38
|
+
try {
|
|
39
|
+
return JSON.stringify(value, null, 2) ?? String(value);
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return String(value);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/** The input, minus fields already on the head, or nothing when that is all. */
|
|
46
|
+
function rest(input, shown) {
|
|
47
|
+
const left = Object.fromEntries(Object.entries(input).filter(([key]) => !shown.includes(key)));
|
|
48
|
+
return Object.keys(left).length > 0 ? json(left) : "";
|
|
49
|
+
}
|
|
50
|
+
/** `old` replaced by `next`, as the lines a diff section colours. */
|
|
51
|
+
function replacement(old, next) {
|
|
52
|
+
const minus = old ? old.split("\n").map((line) => `-${line}`) : [];
|
|
53
|
+
const plus = next ? next.split("\n").map((line) => `+${line}`) : [];
|
|
54
|
+
return [...minus, ...plus].join("\n");
|
|
55
|
+
}
|
|
56
|
+
const TODO_STATES = {
|
|
57
|
+
completed: "done",
|
|
58
|
+
in_progress: "active",
|
|
59
|
+
pending: "pending"
|
|
60
|
+
};
|
|
61
|
+
/** A `TodoWrite` list as a checklist. The active item reads as its `activeForm`. */
|
|
62
|
+
function todos(input) {
|
|
63
|
+
const list = Array.isArray(input.todos) ? input.todos : [];
|
|
64
|
+
const items = [];
|
|
65
|
+
for (const raw of list) {
|
|
66
|
+
const todo = record(raw);
|
|
67
|
+
const state = TODO_STATES[str(todo.status) ?? ""];
|
|
68
|
+
const text = (state === "active" ? str(todo.activeForm) : undefined) ??
|
|
69
|
+
str(todo.content);
|
|
70
|
+
if (state && text)
|
|
71
|
+
items.push({ text, state });
|
|
72
|
+
}
|
|
73
|
+
return items;
|
|
74
|
+
}
|
|
75
|
+
/** A tool call: the card its result will complete. */
|
|
76
|
+
export function toolCallCard(id, name, raw) {
|
|
77
|
+
const input = record(raw);
|
|
78
|
+
const open = (title, text, extra = {}) => ({
|
|
79
|
+
text: oneLine(text ?? "") || title,
|
|
80
|
+
detail: { ref: id, status: "running", title, ...extra }
|
|
81
|
+
});
|
|
82
|
+
const path = shortPath(str(input.file_path) ?? str(input.notebook_path));
|
|
83
|
+
switch (name) {
|
|
84
|
+
case "Bash": {
|
|
85
|
+
const command = str(input.command) ?? "";
|
|
86
|
+
return open("Bash", str(input.description) ?? command, {
|
|
87
|
+
sections: [section("Command", command, "code")]
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
case "Read": {
|
|
91
|
+
const offset = typeof input.offset === "number" ? input.offset : 0;
|
|
92
|
+
const limit = typeof input.limit === "number" ? input.limit : 0;
|
|
93
|
+
const range = offset || limit
|
|
94
|
+
? ` · lines ${offset || 1}–${limit ? (offset || 1) + limit - 1 : "end"}`
|
|
95
|
+
: "";
|
|
96
|
+
return open("Read", path && `${path}${range}`);
|
|
97
|
+
}
|
|
98
|
+
case "Edit":
|
|
99
|
+
return open("Update", path, {
|
|
100
|
+
sections: [
|
|
101
|
+
section("Diff", replacement(str(input.old_string) ?? "", str(input.new_string) ?? ""), "diff")
|
|
102
|
+
]
|
|
103
|
+
});
|
|
104
|
+
case "MultiEdit": {
|
|
105
|
+
const edits = Array.isArray(input.edits) ? input.edits : [];
|
|
106
|
+
const diff = edits
|
|
107
|
+
.map((edit, n) => {
|
|
108
|
+
const one = record(edit);
|
|
109
|
+
return `@@ edit ${n + 1} @@\n${replacement(str(one.old_string) ?? "", str(one.new_string) ?? "")}`;
|
|
110
|
+
})
|
|
111
|
+
.join("\n");
|
|
112
|
+
return open("Update", path, {
|
|
113
|
+
sections: [section("Diff", diff, "diff")]
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
case "Write":
|
|
117
|
+
return open("Write", path, {
|
|
118
|
+
sections: [section("Content", str(input.content) ?? "", "code")]
|
|
119
|
+
});
|
|
120
|
+
case "NotebookEdit":
|
|
121
|
+
return open("NotebookEdit", path, {
|
|
122
|
+
sections: [section("Source", str(input.new_source) ?? "", "code")]
|
|
123
|
+
});
|
|
124
|
+
case "Grep":
|
|
125
|
+
case "Glob": {
|
|
126
|
+
const pattern = str(input.pattern) ?? "";
|
|
127
|
+
const where = shortPath(str(input.path));
|
|
128
|
+
const extra = rest(input, ["pattern", "path"]);
|
|
129
|
+
return open(name === "Grep" ? "Search" : "Glob", `${pattern}${where ? ` in ${where}` : ""}`, extra ? { sections: [section("Options", extra, "code")] } : {});
|
|
130
|
+
}
|
|
131
|
+
case "WebFetch":
|
|
132
|
+
return open("Fetch", str(input.url), {
|
|
133
|
+
sections: [section("Prompt", str(input.prompt) ?? "", "text")]
|
|
134
|
+
});
|
|
135
|
+
case "WebSearch":
|
|
136
|
+
return open("Web Search", str(input.query));
|
|
137
|
+
case "Task":
|
|
138
|
+
case "Agent":
|
|
139
|
+
return open(str(input.subagent_type) ?? "Task", str(input.description), {
|
|
140
|
+
sections: [section("Prompt", str(input.prompt) ?? "", "markdown")]
|
|
141
|
+
});
|
|
142
|
+
case "TodoWrite": {
|
|
143
|
+
const items = todos(input);
|
|
144
|
+
const done = items.filter((item) => item.state === "done").length;
|
|
145
|
+
const active = items.find((item) => item.state === "active");
|
|
146
|
+
return open("Todos", `${active ? `${active.text} · ` : ""}${done}/${items.length} done`, { checklist: items });
|
|
147
|
+
}
|
|
148
|
+
case "ExitPlanMode":
|
|
149
|
+
return open("Plan", "the plan, for approval", {
|
|
150
|
+
sections: [section("Plan", str(input.plan) ?? "", "markdown")]
|
|
151
|
+
});
|
|
152
|
+
default: {
|
|
153
|
+
const first = Object.values(input).find((value) => typeof value === "string");
|
|
154
|
+
const body = Object.keys(input).length > 0 ? json(input) : "";
|
|
155
|
+
return open(name, first, {
|
|
156
|
+
...(body ? { sections: [section("Input", body, "code")] } : {})
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* A tool's result: the half that completes its call's card.
|
|
163
|
+
*
|
|
164
|
+
* Read off the tool's own record (`structured`) where it has one, and off the
|
|
165
|
+
* text the model was given where it does not. The record's shape is what
|
|
166
|
+
* names the tool — the result line carries only the call's id.
|
|
167
|
+
*/
|
|
168
|
+
export function toolResultCard(event) {
|
|
169
|
+
const done = (sections) => ({
|
|
170
|
+
text: oneLine(event.content) || "result",
|
|
171
|
+
detail: {
|
|
172
|
+
ref: event.id,
|
|
173
|
+
status: event.isError ? "error" : "ok",
|
|
174
|
+
...(sections.length > 0 ? { sections } : {})
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
if (event.isError)
|
|
178
|
+
return done([section("Error", event.content, "text")]);
|
|
179
|
+
const result = record(event.structured);
|
|
180
|
+
// Bash.
|
|
181
|
+
if ("stdout" in result || "stderr" in result) {
|
|
182
|
+
const stdout = str(result.stdout) ?? "";
|
|
183
|
+
const stderr = str(result.stderr) ?? "";
|
|
184
|
+
const sections = [];
|
|
185
|
+
if (stdout)
|
|
186
|
+
sections.push(section("Output", stdout, "code"));
|
|
187
|
+
if (stderr)
|
|
188
|
+
sections.push(section("Stderr", stderr, "code"));
|
|
189
|
+
if (result.interrupted === true)
|
|
190
|
+
sections.push(section("Note", "interrupted", "text"));
|
|
191
|
+
return done(sections.length > 0
|
|
192
|
+
? sections
|
|
193
|
+
: [section("Output", "(no output)", "text")]);
|
|
194
|
+
}
|
|
195
|
+
// Read.
|
|
196
|
+
const file = record(result.file);
|
|
197
|
+
if (typeof file.content === "string") {
|
|
198
|
+
const lines = typeof file.numLines === "number"
|
|
199
|
+
? `${file.numLines} line${file.numLines === 1 ? "" : "s"}`
|
|
200
|
+
: "Output";
|
|
201
|
+
return done([section(lines, file.content, "code")]);
|
|
202
|
+
}
|
|
203
|
+
// Edit, MultiEdit, Write and TodoWrite: the call's card already shows what
|
|
204
|
+
// changed, and the result only says that it did.
|
|
205
|
+
if ("structuredPatch" in result || "newTodos" in result)
|
|
206
|
+
return done([]);
|
|
207
|
+
// Task: the subagent's answer.
|
|
208
|
+
if (Array.isArray(result.content)) {
|
|
209
|
+
const text = result.content
|
|
210
|
+
.map((block) => str(record(block).text) ?? "")
|
|
211
|
+
.filter(Boolean)
|
|
212
|
+
.join("\n\n");
|
|
213
|
+
if (text)
|
|
214
|
+
return done([section("Result", text, "markdown")]);
|
|
215
|
+
}
|
|
216
|
+
return done(event.content ? [section("Output", event.content, "text")] : []);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* A thinking block, as the CLI says it: "Thought for 5s", or "Thought" when it
|
|
220
|
+
* was not timed. What it thought is never on the card — see the `thinking`
|
|
221
|
+
* event in `./events.ts`.
|
|
222
|
+
*
|
|
223
|
+
* Its message is its `ref`, so a message's blocks are one card: a message
|
|
224
|
+
* that thinks twice ends on a second block of a few milliseconds, and the CLI
|
|
225
|
+
* shows the two as one thought.
|
|
226
|
+
*/
|
|
227
|
+
export function thinkingCard(durationMs, messageId) {
|
|
228
|
+
return {
|
|
229
|
+
text: durationMs === undefined
|
|
230
|
+
? "Thought"
|
|
231
|
+
: `Thought for ${duration(Math.max(durationMs, 1000))}`,
|
|
232
|
+
detail: { title: "Thinking", ...(messageId ? { ref: messageId } : {}) }
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
/** A session starting — every exec, a resume or a follow-up included. */
|
|
236
|
+
export function initCard(event) {
|
|
237
|
+
const parts = [
|
|
238
|
+
"Session started",
|
|
239
|
+
event.model,
|
|
240
|
+
event.permissionMode && `${event.permissionMode} mode`
|
|
241
|
+
].filter(Boolean);
|
|
242
|
+
return { text: parts.join(" · "), detail: { title: "Session" } };
|
|
243
|
+
}
|
|
244
|
+
/** A session's end, with what its `result` line says about it. */
|
|
245
|
+
export function resultCard(result) {
|
|
246
|
+
const failed = result.isError || result.subtype !== "success";
|
|
247
|
+
const parts = [
|
|
248
|
+
failed ? `Session failed (${result.subtype})` : "Session finished",
|
|
249
|
+
result.numTurns
|
|
250
|
+
? `${result.numTurns} turn${result.numTurns === 1 ? "" : "s"}`
|
|
251
|
+
: undefined,
|
|
252
|
+
result.durationMs ? duration(result.durationMs) : undefined
|
|
253
|
+
].filter(Boolean);
|
|
254
|
+
const errors = result.errors?.join("\n");
|
|
255
|
+
return {
|
|
256
|
+
text: parts.join(" · "),
|
|
257
|
+
detail: {
|
|
258
|
+
title: "Session",
|
|
259
|
+
status: failed ? "error" : "ok",
|
|
260
|
+
...(errors ? { sections: [section("Errors", errors, "text")] } : {})
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
function duration(ms) {
|
|
265
|
+
const s = Math.round(ms / 1000);
|
|
266
|
+
if (s < 60)
|
|
267
|
+
return `${s}s`;
|
|
268
|
+
const m = Math.floor(s / 60);
|
|
269
|
+
if (m < 60)
|
|
270
|
+
return `${m}m ${s % 60}s`;
|
|
271
|
+
return `${Math.floor(m / 60)}h ${m % 60}m`;
|
|
272
|
+
}
|