@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
@@ -1,200 +0,0 @@
1
- /**
2
- * Reading the workspace without pulling more of it across the boundary than the
3
- * model will be shown.
4
- *
5
- * The obvious version of each function here reads everything and throws most of
6
- * it away, which is the wrong shape against a Durable Object's 128 MB. So the
7
- * budget is applied at the source, and the offsets handed back are the *source's*
8
- * rather than the rendered subset's — which is what makes the next page exact
9
- * instead of a guess.
10
- */
11
- /**
12
- * Read a file without pulling more of it across the boundary than the model will
13
- * be shown.
14
- *
15
- * `readFile(path, "utf8")` materialises the whole file in the isolate before
16
- * `truncateOutput` throws most of it away. Harmless on a source file, and the
17
- * wrong shape against a Durable Object's 128 MB — `sb_read` is one model decision
18
- * away from a lockfile, a bundle or a captured build log. The read is
19
- * range-addressable, so the budget is enforced at the source.
20
- *
21
- * Two reads rather than one, because the budget is spent from both ends for the
22
- * reason {@link truncateOutput} documents — the first error is at the top of a
23
- * file and the summary is at the bottom.
24
- *
25
- * ## The one place the budget is spent in bytes
26
- *
27
- * `maxChars` is a character ceiling everywhere else — see
28
- * {@link ComputerConfig.maxOutputChars} — and here it is spent against
29
- * `byteOffset`/`byteLength`, which are the only units the transport has, and the
30
- * only ones that bound what crosses it. Safe in the direction that matters: a
31
- * UTF-8 byte is never more than one UTF-16 code unit, so *N* bytes decode to at
32
- * most *N* characters. Reading the character budget as bytes therefore always
33
- * honours the ceiling, and errs low on a file that is mostly non-ASCII — a CJK
34
- * source file is cut at about a third of the characters it could have shown. The
35
- * alternative is a second round trip on every read, to recover a bound the
36
- * marker already announces.
37
- *
38
- * ## Why `stat` first
39
- *
40
- * Sizing off the returned string instead would be wrong in a way that hides
41
- * itself. `byteLength` counts bytes and `String.length` counts UTF-16 code units,
42
- * so a file of two-byte characters comes back *under* a character budget while
43
- * still being over the byte budget — and the read would look complete with half
44
- * the file missing and no marker saying so. One extra round trip buys the
45
- * distinction.
46
- *
47
- * A slice boundary can land mid-codepoint, which the decoder resolves to a single
48
- * replacement character. That is one glyph of noise at a cut that already
49
- * announces itself as a cut.
50
- */
51
- export async function readBounded(fs, path, maxChars) {
52
- const { size } = await fs.stat(path);
53
- if (size <= maxChars)
54
- return fs.readFile(path, "utf8");
55
- // Names the way out, rather than only the size of the hole. Before `offset`
56
- // existed the honest answer was "use sb_exec with sed", which needs a live
57
- // container — the dependency these tools exist to remove.
58
- const marker = (dropped, at) => `\n\n… [${dropped} bytes omitted from the middle — read them with ` +
59
- `\`offset: ${at}\`] …\n\n`;
60
- const half = Math.floor((maxChars - marker(size, size).length) / 2);
61
- // No budget for two ends plus the marker: keep the head, where the first error
62
- // is. The same fallback `truncateOutput` makes at the same ceiling.
63
- if (half < 1)
64
- return fs.readFile(path, {
65
- encoding: "utf8",
66
- byteLength: Math.max(0, maxChars)
67
- });
68
- const [head, tail] = await Promise.all([
69
- fs.readFile(path, { encoding: "utf8", byteOffset: 0, byteLength: half }),
70
- fs.readFile(path, {
71
- encoding: "utf8",
72
- byteOffset: size - half,
73
- byteLength: half
74
- })
75
- ]);
76
- return head + marker(size - half * 2, half) + tail;
77
- }
78
- /**
79
- * A byte window the model asked for, with its coordinates stated.
80
- *
81
- * No middle-out here, deliberately: {@link readBounded} guesses at what matters when
82
- * nobody said, but an explicit `offset` *is* the model saying. Trimming the middle
83
- * of a region it chose would defeat the request and, worse, make the next offset
84
- * unknowable.
85
- *
86
- * The window line is what makes paging work at all — the model needs to know where
87
- * it landed and how much is left to compute the next `offset`.
88
- */
89
- export async function readWindow(fs, path, offset, length, maxChars) {
90
- const { size } = await fs.stat(path);
91
- if (offset >= size)
92
- return `(offset ${offset} is past the end of ${path}, which is ${size} bytes)`;
93
- const byteLength = Math.min(length, maxChars, size - offset);
94
- const body = await fs.readFile(path, {
95
- encoding: "utf8",
96
- byteOffset: offset,
97
- byteLength
98
- });
99
- const end = offset + byteLength;
100
- const rest = end < size ? `; ${size - end} bytes after this` : "";
101
- return `${body}\n--- bytes ${offset}–${end} of ${size}${rest} ---`;
102
- }
103
- /**
104
- * Fetch a page, drop what is off-limits, and keep the source offsets that make the
105
- * *next* page exact.
106
- *
107
- * The subtle part is why `rawIndex` exists at all. `offset` counts items at the
108
- * source; the caller renders a filtered, budget-trimmed subset of them. So the
109
- * obvious `nextOffset = offset + shown` is wrong — it silently skips or repeats
110
- * whenever anything was dropped, which is precisely when a skipped directory is
111
- * involved. Keeping each survivor's source index means the next page starts exactly
112
- * after the last one the model actually saw.
113
- *
114
- * Retries because a skipped directory can outnumber a whole page on its own — a
115
- * dependency tree runs to tens of thousands of files — so one fetch can come back
116
- * entirely excluded. `maxRounds` is the caller's: a retry re-reads and re-scans
117
- * every file the last one looked at. For a walk the store can prune instead, see
118
- * `sb_ls`, which passes exclusions to `find` and needs none of this.
119
- *
120
- * Deliberately no slicing to `want`: the caller needs the extra item to know a next
121
- * page exists, and its index to say where.
122
- */
123
- export async function collectVisible(fetch, pathOf, skip, want, offset, maxRounds) {
124
- const items = [];
125
- const rawIndex = [];
126
- let cursor = offset;
127
- let exhausted = false;
128
- let rounds = 0;
129
- // One past `want`, so a next page is detected without a further round trip.
130
- while (items.length <= want && rounds < maxRounds) {
131
- rounds += 1;
132
- const batch = await fetch(cursor, want + 1);
133
- batch.forEach((item, i) => {
134
- if (skip(pathOf(item)))
135
- return;
136
- items.push(item);
137
- rawIndex.push(cursor + i);
138
- });
139
- cursor += batch.length;
140
- if (batch.length <= want) {
141
- exhausted = true;
142
- break;
143
- }
144
- }
145
- return {
146
- items,
147
- rawIndex,
148
- rawEnd: cursor,
149
- exhausted,
150
- crowded: !exhausted && items.length <= want
151
- };
152
- }
153
- /**
154
- * The "there is more, here is where" line, with an offset that is actually correct.
155
- *
156
- * `rawIndex[shown]` rather than `offset + shown`: the first item the model did *not*
157
- * see, at its source coordinate. Anything else drifts by however many entries the
158
- * skip filter removed, and drifts silently — the next page would repeat or skip
159
- * with nothing to indicate it had.
160
- *
161
- * Narrowing is still offered, and still first-class, because it is cheaper than
162
- * paging: `offset` does not skip the walk, it only discards its early results. It is
163
- * offered as the better option rather than the only one, which is the thing the
164
- * previous wording got wrong — "narrow it" is no answer at all when the model wants
165
- * the next page of a list it is working through.
166
- */
167
- export function listingNote(page, shown, noun, narrower) {
168
- const next = shown < page.rawIndex.length
169
- ? page.rawIndex[shown]
170
- : page.exhausted
171
- ? undefined
172
- : page.rawEnd;
173
- if (next === undefined)
174
- return "";
175
- return (`\n\n… showed ${shown} ${noun}; there are more. Continue with ` +
176
- `\`offset: ${next}\`, or narrow with ${narrower} — narrowing is cheaper, ` +
177
- `since an offset still walks everything it skips.`);
178
- }
179
- /**
180
- * Does this path exist?
181
- *
182
- * `getWorkspace()` types `fs` as the in-process `WorkspaceFilesystem`, but the
183
- * value on the far side of a Durable Object boundary is a
184
- * `WorkspaceFilesystemStub`, which carries a few methods the local class does
185
- * not — `exists` among them. So the fast path is asked for rather than cast to,
186
- * and `stat` is the fallback for a host that hands back a local workspace.
187
- *
188
- * The fallback treats *any* `stat` failure as absence, which is the honest
189
- * reading for a tool whose entire answer is a boolean: there is no error channel
190
- * to distinguish "missing" from "unreadable", and the caller's next move — look
191
- * somewhere else — is the same either way.
192
- */
193
- export async function pathExists(fs, path) {
194
- // Called as a method, never through `.call`: on an RPC stub `.call` is read as
195
- // a remote method, and the stub it would pass as `this` cannot be serialised.
196
- const remote = fs;
197
- if (typeof remote.exists === "function")
198
- return remote.exists(path);
199
- return fs.stat(path).then(() => true, () => false);
200
- }
@@ -1,216 +0,0 @@
1
- import { tool } from "ai";
2
- import { z } from "zod";
3
- import { guardPath, isSkipped, skipNames, WALK_SKIPS } from "./paths.js";
4
- import { humanBytes, packBlocks, renderGrepMatches } from "./render.js";
5
- import { collectVisible, listingNote, pathExists } from "./read.js";
6
- /** Finding things: what is in a directory, what a file contains, what exists. */
7
- /**
8
- * How many directory entries one `sb_ls` returns.
9
- *
10
- * A bound on the *listing*, not on the rendered text — `maxOutputChars` still
11
- * applies on top, and the order matters. Bounding only the text means `readdir`
12
- * returns every entry and the isolate holds all of them before the ceiling
13
- * discards the tail; a dependency tree is 22,470 files, and a misbehaving build
14
- * is exactly when someone lists one. `readdir` takes a `limit`, so the bound is
15
- * applied where the entries are read.
16
- */
17
- const DEFAULT_MAX_ENTRIES = 1000;
18
- /**
19
- * How many matches one `sb_grep` returns.
20
- *
21
- * Bounded at the source like {@link DEFAULT_MAX_ENTRIES}, and for a sharper reason
22
- * than a listing: without a `limit`, `fs.grep` reads *every* file under the path
23
- * looking for more. `.git` and the dependency tree are both in the workspace, so
24
- * an unbounded search of `/workspace` streams every loose object and every
25
- * vendored file through the isolate before answering. The limit is what stops
26
- * that walk early, and the skip list is what keeps the matches worth reading.
27
- */
28
- const DEFAULT_MAX_MATCHES = 200;
29
- export function findTools(ctx) {
30
- const { cwd, maxChars, inWorkspace } = ctx;
31
- return {
32
- sb_ls: tool({
33
- description: "List files in a workspace directory, or find files by name. Without `pattern` it lists one level: directories with a trailing slash, files with their size — check that before reading a large one, since sb_read truncates. " +
34
- "`pattern` is a glob matched against paths relative to `path`, and searches the whole subtree: `*` stays within one path segment, `**/` crosses directories, `?` matches one character. So `*.ts` finds top-level TypeScript files and `**/*.ts` finds them at any depth. " +
35
- "A cut listing reports the `offset` that continues it. Subtree listings leave out `.git` and `node_modules`.",
36
- inputSchema: z.object({
37
- path: z.string().describe("Absolute directory path"),
38
- recursive: z
39
- .boolean()
40
- .optional()
41
- .describe("List the whole subtree rather than one level"),
42
- pattern: z
43
- .string()
44
- .optional()
45
- .describe("Glob relative to path, e.g. '**/*.spec.ts'. Searches the subtree, so `recursive` is not needed with it."),
46
- offset: z
47
- .number()
48
- .int()
49
- .min(0)
50
- .optional()
51
- .describe("Entries to skip — use the offset a cut listing reports")
52
- }),
53
- execute: async ({ path, recursive, pattern, offset }) => {
54
- const refusal = guardPath(path, "sb_ls");
55
- if (refusal)
56
- return refusal;
57
- const from = offset ?? 0;
58
- return inWorkspace("listing", path, async (fs) => {
59
- if (recursive || pattern) {
60
- // `find` rather than `ls`, which took no bound: a prefix scan
61
- // returned every path in the subtree and the ceiling then threw most
62
- // of them away — the same read-everything-then-discard shape the
63
- // `readdir` limit below exists to avoid. Two differences follow from
64
- // the swap, both improvements: directories appear (rendered like the
65
- // arm below renders them), and order is the walk's — pre-order, by
66
- // name — rather than one flat sort.
67
- //
68
- // `.git` and `node_modules` are pruned by the store, not filtered
69
- // here: at a repo root either one outnumbers a page on its own.
70
- const entries = await fs.find(path, pattern, {
71
- limit: DEFAULT_MAX_ENTRIES + 1,
72
- offset: from,
73
- exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
74
- });
75
- if (entries.length === 0)
76
- return pattern
77
- ? `(nothing under ${path} matches ${pattern})`
78
- : `(${path} is empty)`;
79
- const blocks = entries
80
- .slice(0, DEFAULT_MAX_ENTRIES)
81
- .map((e) => [e.type === "dir" ? `${e.path}/` : e.path]);
82
- const { body, shown } = packBlocks(blocks, maxChars);
83
- const page = {
84
- rawIndex: entries.map((_, i) => from + i),
85
- rawEnd: from + entries.length,
86
- exhausted: entries.length <= DEFAULT_MAX_ENTRIES,
87
- crowded: false
88
- };
89
- return body + listingNote(page, shown, "entries", "`pattern`");
90
- }
91
- // One over the ceiling: enough to know the listing was cut without a
92
- // second round trip to find out, and the extra entry is not shown.
93
- // Unfiltered on purpose — one line naming a directory that is really
94
- // there is honest, and costs a line rather than a page. It is the
95
- // *subtree* walk above that cannot afford to descend into them.
96
- const entries = await fs.readdir(path, {
97
- limit: DEFAULT_MAX_ENTRIES + 1,
98
- offset: from
99
- });
100
- if (entries.length === 0)
101
- return from > 0
102
- ? `(no entries in ${path} past offset ${from})`
103
- : `(${path} is empty)`;
104
- const blocks = entries
105
- .slice(0, DEFAULT_MAX_ENTRIES)
106
- .map((e) => [
107
- e.isDirectory ? `${e.name}/` : `${e.name}\t${humanBytes(e.size)}`
108
- ]);
109
- const { body, shown } = packBlocks(blocks, maxChars);
110
- const more = entries.length > shown;
111
- return (body +
112
- (more
113
- ? `\n… showed ${shown} entries; there are more. Continue with \`offset: ${from + shown}\`, or narrow with \`pattern\`.`
114
- : ""));
115
- });
116
- }
117
- }),
118
- /**
119
- * Search, without the container.
120
- *
121
- * The tool this replaces is `sb_exec("grep -rn …")`, and the case for a native
122
- * one is not that shelling out fails — it is where the search runs. `fs.grep`
123
- * reads the Durable Object's SQLite, so it answers while the container is
124
- * being replaced or an install is still running, which is exactly the window
125
- * the install gate leaves a subagent with nothing to do. It is also why this
126
- * tool is not gated: see {@link file://./context.ts awaitAdvisories}.
127
- *
128
- * Two lesser reasons that still matter. The query arrives as a value rather
129
- * than through `shellQuote` and a shell that would re-parse it. And the result
130
- * is bounded by a `limit` at the source instead of being middle-truncated
131
- * afterwards, which for a match list means losing whole files silently.
132
- */
133
- sb_grep: tool({
134
- description: "Search file contents across the workspace. Returns matching lines grouped by file, each with its line number. " +
135
- "The query is matched literally — set `regex` to interpret it as a regular expression. " +
136
- "Pass `include` to limit which files are searched, e.g. '**/*.ts' — without it every file under `path` is read, which is slower and rarely what you meant. " +
137
- "A cut result reports the `offset` that continues it. Use `context` to see the lines around a match. " +
138
- "Results from `.git` and `node_modules` are left out — search node_modules with sb_exec.",
139
- inputSchema: z.object({
140
- query: z.string().describe("Text to find, e.g. 'buildComputerTools'"),
141
- path: z
142
- .string()
143
- .optional()
144
- .describe(`Absolute file or directory to search (default: ${cwd})`),
145
- include: z
146
- .string()
147
- .optional()
148
- .describe("Glob relative to path limiting which files are searched, e.g. '**/*.ts'"),
149
- regex: z
150
- .boolean()
151
- .optional()
152
- .describe("Interpret query as a regular expression"),
153
- ignoreCase: z.boolean().optional().describe("Ignore letter case"),
154
- context: z
155
- .number()
156
- .int()
157
- .min(0)
158
- .max(3)
159
- .optional()
160
- .describe("Lines of surrounding context to include with each match"),
161
- offset: z
162
- .number()
163
- .int()
164
- .min(0)
165
- .optional()
166
- .describe("Matches to skip — use the offset a cut result reports")
167
- }),
168
- execute: async ({ query, path, include, regex, ignoreCase, context, offset }) => {
169
- const target = path ?? cwd;
170
- const refusal = guardPath(target, "sb_grep");
171
- if (refusal)
172
- return refusal;
173
- const from = offset ?? 0;
174
- return inWorkspace("searching", target, async (fs) => {
175
- // Two rounds, because a `grep` retry re-reads and re-scans every file
176
- // it already looked at. `.git` rarely floods a page here — its bulk is
177
- // compressed objects, which a text query does not match — where a
178
- // `node_modules` left in the workspace is source and matches.
179
- const skips = WALK_SKIPS;
180
- const page = await collectVisible((at, limit) => fs.grep(query, target, {
181
- include,
182
- regex,
183
- ignoreCase,
184
- context,
185
- limit,
186
- offset: at
187
- }), (m) => m.path, (p) => isSkipped(skips, p), DEFAULT_MAX_MATCHES, from, 2);
188
- if (page.items.length === 0)
189
- return page.crowded
190
- ? `every match for ${JSON.stringify(query)} from offset ${from} is inside ${skipNames(skips)}, which ${skips.length > 1 ? "are" : "is"} not searched. Add \`include\` (e.g. '**/*.ts') to search the working tree instead, or use sb_exec to search \`node_modules\`.`
191
- : `no matches for ${JSON.stringify(query)} in ${target}${include ? ` (${include})` : ""}${from > 0 ? ` past offset ${from}` : ""}`;
192
- const { body, shown, capped } = renderGrepMatches(page.items.slice(0, DEFAULT_MAX_MATCHES), maxChars);
193
- return (body +
194
- listingNote(page, shown, "matches", "`include`") +
195
- (capped
196
- ? `\n(Some lines were shortened. Read one in full with \`sb_read\` and an \`offset\`.)`
197
- : ""));
198
- });
199
- }
200
- }),
201
- sb_exists: tool({
202
- description: "Check whether a path exists in the workspace.",
203
- inputSchema: z.object({ path: z.string().describe("Absolute path") }),
204
- execute: async ({ path }) => {
205
- const refusal = guardPath(path, "sb_exists");
206
- if (refusal)
207
- return refusal;
208
- return inWorkspace("checking", path, async (fs) => {
209
- return (await pathExists(fs, path))
210
- ? `${path} exists`
211
- : `${path} does not exist`;
212
- });
213
- }
214
- })
215
- };
216
- }
@@ -1,105 +0,0 @@
1
- import type { AgentPlugin } from "@dynamicagents/core";
2
- import type { SessionMessage } from "@dynamicagents/core/agent";
3
- /**
4
- * The subset of the Vectorize binding recall drives — structurally satisfied by
5
- * `Vectorize`/`VectorizeIndex`, so nothing here names the binding type and a
6
- * test can pass a plain object.
7
- */
8
- export interface RecallIndex {
9
- query(vector: number[], options?: VectorizeQueryOptions): Promise<VectorizeMatches>;
10
- upsert(vectors: VectorizeVector[]): Promise<unknown>;
11
- }
12
- /** Embed a batch of texts. Output dimension must match the Vectorize index. */
13
- export type Embed = (texts: string[]) => Promise<number[][]>;
14
- /** A single archived message returned by a recall search. */
15
- export interface RecallResult {
16
- score: number;
17
- role: string;
18
- /** The stored message snippet (truncated to `metadataTextMax`). */
19
- text: string;
20
- /** Display name of the human speaker, when the message carried a `<turn>` wrapper. */
21
- author?: string;
22
- /** Speaker's platform user id, when present. */
23
- authorId?: string;
24
- /** Channel the turn came from, when present. */
25
- channel?: string;
26
- /** ISO-8601 instant of the turn, when present. */
27
- at?: string;
28
- }
29
- /**
30
- * Build the Vectorize vectors for a batch of archived messages. Pure:
31
- * `embeddings` are supplied 1:1 with `messages` (both already filtered to
32
- * non-empty text). The vector `id` is the message's own id, so re-archiving on a
33
- * compaction retry is an idempotent upsert; `<turn>` provenance is lifted into
34
- * metadata when present.
35
- */
36
- export declare function toRecallVectors(messages: SessionMessage[], namespace: string, embeddings: number[][], metadataTextMax?: number): VectorizeVector[];
37
- /**
38
- * Archive a range of displaced messages into Vectorize. Embeds the non-empty
39
- * texts in one batch and upserts them under `namespace`. No-op on empty input.
40
- */
41
- export declare function archiveMessages(index: RecallIndex, namespace: string, messages: SessionMessage[], embed: Embed, metadataTextMax?: number): Promise<void>;
42
- /**
43
- * Semantically search this instance's archive. Embeds the query and returns the
44
- * top matches within `namespace`, mapping each vector's metadata to a compact
45
- * {@link RecallResult}. Constrained to `namespace` — never a caller-supplied one.
46
- */
47
- export declare function recallSearch(index: RecallIndex, namespace: string, query: string, embed: Embed, topK?: number): Promise<RecallResult[]>;
48
- /**
49
- * The tuning half of {@link RecallConfig}: plain values, no bindings.
50
- *
51
- * Split out so a host can declare its settings in one place and spread them,
52
- * rather than re-typing each field at the call site:
53
- *
54
- * ```ts
55
- * // config.ts — the values, typed
56
- * export const RECALL: RecallTuning = { embeddingModelId: "@cf/baai/bge-m3", topK: 5 };
57
- *
58
- * // plugins.ts — the bindings, and the values spread
59
- * recall({ ai: host.env.AI, index: host.env.VECTORIZE,
60
- * namespace: host.callerKey, aiGatewayId: host.aiGatewayId,
61
- * agentName: host.agentName, ...RECALL })
62
- * ```
63
- *
64
- * Field-by-field re-typing is not just verbose — it silently drops any option
65
- * added later, so a new default lands everywhere except the deployments that
66
- * enumerated the fields by hand.
67
- */
68
- export interface RecallTuning {
69
- /**
70
- * AI Gateway id. Pass the host's **resolved** value (`PluginHost.aiGatewayId`),
71
- * so embedding calls stay correlated with chat calls in one gateway. Reading it
72
- * off a config-overrides object instead yields `undefined` the moment that
73
- * override is dropped in favour of core's default.
74
- */
75
- aiGatewayId?: string;
76
- /**
77
- * The `agent` key on embedding calls' AI Gateway log rows. Pass the host's
78
- * `PluginHost.agentName`, for the reason given on {@link aiGatewayId}.
79
- */
80
- agentName?: string;
81
- /** Workers-AI embedding model. Must match the index's dimension. */
82
- embeddingModelId?: string;
83
- /** Matches returned by a search. Defaults to 5. */
84
- topK?: number;
85
- /** Stored-snippet cap, guarding Vectorize's ~10 KiB/vector. Defaults to 2000. */
86
- metadataTextMax?: number;
87
- }
88
- export interface RecallConfig extends RecallTuning {
89
- /** The `AI` binding, for embeddings. Read lazily — see below. */
90
- ai: Ai;
91
- /** The `VECTORIZE` binding. Must be a 1024-dim, cosine index. */
92
- index: RecallIndex;
93
- /**
94
- * The archive namespace for this agent instance — **a thunk, deliberately.**
95
- *
96
- * It derives from the verified caller's identity, which is not known when
97
- * `plugins(host)` runs in `onStart`. The Durable Object is keyed 1:1 by that
98
- * caller, so the value is constant once known; a thunk is what lets the host
99
- * supply it late while both hooks here read the same one.
100
- */
101
- namespace: () => string;
102
- /** Test override; skips the provider entirely. */
103
- embed?: Embed;
104
- }
105
- export declare function recall(config: RecallConfig): AgentPlugin;