@dynamicagents/plugins 0.12.0 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (132) hide show
  1. package/README.md +58 -77
  2. package/dist/browser/index.d.ts +0 -2
  3. package/dist/browser/index.js +21 -27
  4. package/dist/claude-code/cards.d.ts +55 -0
  5. package/dist/claude-code/cards.js +283 -0
  6. package/dist/claude-code/config.d.ts +28 -63
  7. package/dist/claude-code/config.js +0 -11
  8. package/dist/claude-code/egress.d.ts +8 -1
  9. package/dist/claude-code/egress.js +42 -4
  10. package/dist/claude-code/events.d.ts +106 -9
  11. package/dist/claude-code/events.js +201 -56
  12. package/dist/claude-code/github.d.ts +39 -0
  13. package/dist/claude-code/github.js +82 -0
  14. package/dist/claude-code/index.d.ts +16 -90
  15. package/dist/claude-code/index.js +14 -276
  16. package/dist/claude-code/model.d.ts +152 -0
  17. package/dist/claude-code/model.js +366 -0
  18. package/dist/claude-code/run.d.ts +230 -116
  19. package/dist/claude-code/run.js +380 -167
  20. package/dist/claude-code/session.d.ts +131 -0
  21. package/dist/claude-code/session.js +288 -0
  22. package/dist/claude-code/spec.d.ts +46 -0
  23. package/dist/claude-code/spec.js +83 -0
  24. package/dist/claude-code/verified.d.ts +15 -0
  25. package/dist/claude-code/verified.js +15 -0
  26. package/dist/computer/context.d.ts +1 -5
  27. package/dist/computer/context.js +7 -10
  28. package/dist/computer/file-lock.js +5 -4
  29. package/dist/computer/gate.d.ts +4 -4
  30. package/dist/computer/gate.js +5 -5
  31. package/dist/computer/index.d.ts +40 -258
  32. package/dist/computer/index.js +71 -324
  33. package/dist/computer/paths.d.ts +36 -16
  34. package/dist/computer/paths.js +42 -29
  35. package/dist/computer/proxy.d.ts +45 -0
  36. package/dist/computer/proxy.js +234 -0
  37. package/dist/computer/render.d.ts +1 -40
  38. package/dist/computer/render.js +2 -68
  39. package/dist/computer/tools-exec.js +12 -13
  40. package/dist/computer/tools-file.d.ts +9 -1
  41. package/dist/computer/tools-file.js +22 -82
  42. package/dist/computer/{tools-find.d.ts → tools-grep.d.ts} +1 -1
  43. package/dist/computer/tools-grep.js +129 -0
  44. package/dist/repo/checkout.d.ts +4 -5
  45. package/dist/repo/checkout.js +31 -8
  46. package/dist/repo/context.d.ts +35 -9
  47. package/dist/repo/context.js +101 -49
  48. package/dist/repo/index.d.ts +73 -28
  49. package/dist/repo/index.js +112 -96
  50. package/dist/repo/tools-checks.d.ts +3 -0
  51. package/dist/repo/tools-checks.js +112 -0
  52. package/dist/repo/tools-clone.js +1 -1
  53. package/dist/repo/tools-pr.d.ts +14 -0
  54. package/dist/repo/{tools-forge.js → tools-pr.js} +53 -36
  55. package/dist/repo/tools-review.d.ts +9 -0
  56. package/dist/repo/tools-review.js +67 -47
  57. package/dist/repo/tools-worktree.js +144 -60
  58. package/dist/repo/tools-worktrees.d.ts +12 -0
  59. package/dist/repo/tools-worktrees.js +48 -0
  60. package/dist/repo/url.d.ts +2 -2
  61. package/dist/repo/url.js +7 -7
  62. package/dist/scratch/index.d.ts +4 -4
  63. package/dist/scratch/index.js +12 -11
  64. package/dist/{computer → workspace}/advisory.d.ts +4 -4
  65. package/dist/{computer → workspace}/advisory.js +2 -2
  66. package/dist/{computer/host → workspace}/ca-trust.d.ts +8 -1
  67. package/dist/{computer/host → workspace}/ca-trust.js +12 -3
  68. package/dist/{computer/host → workspace}/container-deps.d.ts +4 -0
  69. package/dist/{computer/host → workspace}/container-deps.js +26 -1
  70. package/dist/{computer/host → workspace}/container-fault.d.ts +3 -3
  71. package/dist/{computer/host → workspace}/container-fault.js +23 -8
  72. package/dist/workspace/deps-snapshot.d.ts +110 -0
  73. package/dist/workspace/deps-snapshot.js +424 -0
  74. package/dist/workspace/exec.d.ts +94 -0
  75. package/dist/workspace/exec.js +153 -0
  76. package/dist/workspace/format.d.ts +36 -0
  77. package/dist/workspace/format.js +57 -0
  78. package/dist/{computer/host → workspace}/git-host.d.ts +21 -4
  79. package/dist/{computer/host → workspace}/git-host.js +72 -2
  80. package/dist/{computer/host → workspace}/git-identity.d.ts +1 -1
  81. package/dist/{computer/host → workspace}/git-identity.js +1 -1
  82. package/dist/workspace/index.d.ts +44 -47
  83. package/dist/workspace/index.js +52 -112
  84. package/dist/{computer/host → workspace}/install-job.d.ts +53 -4
  85. package/dist/{computer/host → workspace}/install-job.js +346 -178
  86. package/dist/{computer → workspace}/install.d.ts +17 -8
  87. package/dist/{computer → workspace}/install.js +24 -6
  88. package/dist/{computer/host/workspace.d.ts → workspace/object.d.ts} +125 -27
  89. package/dist/{computer/host/workspace.js → workspace/object.js} +341 -58
  90. package/dist/workspace/open.d.ts +95 -0
  91. package/dist/workspace/open.js +63 -0
  92. package/dist/workspace/read.d.ts +16 -0
  93. package/dist/workspace/read.js +22 -0
  94. package/dist/{computer → workspace}/shell.d.ts +5 -5
  95. package/dist/{computer → workspace}/shell.js +6 -6
  96. package/dist/{computer/host → workspace}/wake.d.ts +3 -1
  97. package/package.json +17 -37
  98. package/dist/arc-agi/analysis.d.ts +0 -322
  99. package/dist/arc-agi/analysis.js +0 -802
  100. package/dist/arc-agi/client.d.ts +0 -82
  101. package/dist/arc-agi/client.js +0 -113
  102. package/dist/arc-agi/game-tools.d.ts +0 -24
  103. package/dist/arc-agi/game-tools.js +0 -26
  104. package/dist/arc-agi/index.d.ts +0 -40
  105. package/dist/arc-agi/index.js +0 -175
  106. package/dist/arc-agi/main-agent.d.ts +0 -24
  107. package/dist/arc-agi/main-agent.js +0 -62
  108. package/dist/arc-agi/recipe.d.ts +0 -48
  109. package/dist/arc-agi/recipe.js +0 -97
  110. package/dist/arc-agi/scorecard.d.ts +0 -118
  111. package/dist/arc-agi/scorecard.js +0 -163
  112. package/dist/arc-agi/soul.d.ts +0 -29
  113. package/dist/arc-agi/soul.js +0 -79
  114. package/dist/arc-agi/store.d.ts +0 -153
  115. package/dist/arc-agi/store.js +0 -215
  116. package/dist/arc-agi/tools.d.ts +0 -36
  117. package/dist/arc-agi/tools.js +0 -574
  118. package/dist/arc-agi/types.d.ts +0 -211
  119. package/dist/arc-agi/types.js +0 -19
  120. package/dist/claude-code/recipe.d.ts +0 -53
  121. package/dist/claude-code/recipe.js +0 -100
  122. package/dist/computer/read.d.ts +0 -133
  123. package/dist/computer/read.js +0 -200
  124. package/dist/computer/tools-find.js +0 -216
  125. package/dist/recall/index.d.ts +0 -105
  126. package/dist/recall/index.js +0 -181
  127. package/dist/repo/tools-forge.d.ts +0 -4
  128. package/dist/triage/index.d.ts +0 -360
  129. package/dist/triage/index.js +0 -311
  130. /package/dist/{computer/host → workspace}/sync.d.ts +0 -0
  131. /package/dist/{computer/host → workspace}/sync.js +0 -0
  132. /package/dist/{computer/host → workspace}/wake.js +0 -0
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/plugins.ts
23
- import { arcAgi } from "@dynamicagents/plugins/arc-agi";
25
+ // src/agents/coder/agent.ts
26
+ import { StepAgent } from "@dynamicagents/core/agent";
24
27
  import { browser } from "@dynamicagents/plugins/browser";
25
- import { recall } from "@dynamicagents/plugins/recall";
26
-
27
- export interface PluginHost {
28
- env: Env;
29
- storage: DurableObjectStorage;
30
- /** The verified caller this Durable Object belongs to. See below. */
31
- callerKey: () => string;
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
- `plugins` is a function, not a module-level array: on Workers `env` does not exist at module
47
- scope, and core's registry is built per Durable Object instance in `onStart()`.
48
-
49
- ```ts
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
- That deferral is the whole reason `/recall` takes `namespace` as a function. Anything else
77
- needing per-caller state takes it the same way.
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 | Needs |
84
- | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------- |
85
- | [`/arc-agi`](src/arc-agi/) | Play ARC-AGI-3 games — a delegable subtask type, a catalogue tool, a scorecard ledger | `ARC_API_KEY` |
86
- | [`/browser`](src/browser/) | Read web pages via Browser Rendering Quick Actions | `BROWSER` (paid plan) |
87
- | [`/claude-code`](src/claude-code/) | Delegate a coding task to a Claude Code session in the workspace container | one or more `claude setup-token` credentials |
88
- | [`/computer`](src/computer/) | A Linux container whose filesystem outlives it — shell, package manager, network, and the Durable Object it runs in | `@cloudflare/computer`, `@platformatic/vfs` (paid) |
89
- | [`/recall`](src/recall/) | Episodic memory over Vectorize — search history that compaction folded away | `VECTORIZE` (1024-dim/cosine) |
90
- | [`/repo`](src/repo/) | Clone, commit, push a branch, open a pull request — over any container | `GITHUB_TOKEN` |
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
- key: "scraper",
108
- mainAgentTools: () => ({ fetchPage: /* … */ }),
109
- capability: "You can fetch and summarize a page.",
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
- Three rules the whole design rests on:
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
- - **Declare a capability block in exactly one place.** If your plugin has a `subtaskType`, put
124
- it there; otherwise on the plugin. Both are rendered, by different call sites.
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
 
@@ -152,18 +145,6 @@ gives it its own copy of every peer. Two copies of `agents` in one bundle breaks
152
145
  `Session` types and every `instanceof`, at runtime rather than at the type level.
153
146
  Nothing is written to `package.json`, so CI never builds against a local checkout.
154
147
 
155
- `test/arc-agi/recorded.spec.ts` drives the **real** ARC API and replays a committed
156
- cassette, so it needs no key either. Re-record it against the live API with:
157
-
158
- ```bash
159
- npm run test:record # real ARC_API_KEY in .env.test (see .env.test.example)
160
- ```
161
-
162
- The key reaches the live ARC API and nothing else: the recorder excludes the auth header, so
163
- it never lands in the committed cassette. It reaches the spec only as a Miniflare binding —
164
- `.env.test` is loaded into **Node's** `process.env`, which a spec running in workerd cannot
165
- see, so `vitest.config.ts` hands it across explicitly.
166
-
167
148
  ## License
168
149
 
169
- GPL-3.0-only
150
+ Apache-2.0
@@ -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
@@ -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`), offered to the **main agent** and available to a subagent
9
- * recipe as the `browser` tool family. Both, because both wanted them: a main
10
- * agent reads a page to answer a question directly, and a delegated research
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
- /** This plugin's tool-family name, as a recipe's `toolFamilies` lists it. */
19
- export const BROWSER_FAMILY = "browser";
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
- key: "browser",
33
- mainAgentTools: () => tools(),
34
- toolFamilies: { [BROWSER_FAMILY]: () => ({ tools: tools() }) },
35
- capability: [
36
- "You can read web pages directly:",
37
- "- `browser_markdown` fetches a page and returns it as Markdown. This is the default — use it first.",
38
- "- `browser_extract` pulls structured fields out of a page when you know what you are looking for.",
39
- "- `browser_links` lists a page's links, for following a trail.",
40
- "- `browser_scrape` returns the text of specific CSS selectors.",
41
- "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."
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,55 @@
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
+ /** A session its run gave up on before reading its end, and why. */
54
+ export declare function lostCard(why: string): Card;
55
+ export {};
@@ -0,0 +1,283 @@
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
+ /** A session its run gave up on before reading its end, and why. */
265
+ export function lostCard(why) {
266
+ return {
267
+ text: "Session ended before its run read it to the end",
268
+ detail: {
269
+ title: "Session",
270
+ status: "error",
271
+ sections: [section("Why", why, "text")]
272
+ }
273
+ };
274
+ }
275
+ function duration(ms) {
276
+ const s = Math.round(ms / 1000);
277
+ if (s < 60)
278
+ return `${s}s`;
279
+ const m = Math.floor(s / 60);
280
+ if (m < 60)
281
+ return `${m}m ${s % 60}s`;
282
+ return `${Math.floor(m / 60)}h ${m % 60}m`;
283
+ }