@dynamicagents/plugins 0.16.0 → 0.17.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 (38) hide show
  1. package/README.md +1 -1
  2. package/dist/claude-code/cards.d.ts +6 -1
  3. package/dist/claude-code/cards.js +21 -2
  4. package/dist/claude-code/model.d.ts +4 -0
  5. package/dist/claude-code/model.js +74 -7
  6. package/dist/claude-code/verified.d.ts +1 -1
  7. package/dist/claude-code/verified.js +1 -1
  8. package/dist/computer/client.d.ts +9 -1
  9. package/dist/computer/context.js +1 -1
  10. package/dist/computer/index.d.ts +10 -6
  11. package/dist/computer/index.js +34 -17
  12. package/dist/computer/paths.d.ts +26 -2
  13. package/dist/computer/paths.js +51 -5
  14. package/dist/computer/proxy.d.ts +10 -3
  15. package/dist/computer/proxy.js +77 -87
  16. package/dist/computer/render.d.ts +6 -35
  17. package/dist/computer/render.js +5 -43
  18. package/dist/computer/sandbox.d.ts +4 -3
  19. package/dist/computer/sandbox.js +9 -2
  20. package/dist/computer/shell.d.ts +8 -7
  21. package/dist/computer/shell.js +8 -7
  22. package/dist/computer/tools-exec.d.ts +39 -0
  23. package/dist/computer/tools-exec.js +357 -228
  24. package/dist/computer/tools-grep.js +10 -4
  25. package/dist/repo-github/checkout.d.ts +16 -0
  26. package/dist/repo-github/checkout.js +16 -7
  27. package/dist/repo-github/context.d.ts +11 -0
  28. package/dist/repo-github/context.js +1 -0
  29. package/dist/repo-github/skills.d.ts +167 -0
  30. package/dist/repo-github/skills.js +414 -0
  31. package/dist/repo-github/tools-clone.js +25 -4
  32. package/dist/sandbox/header.d.ts +13 -0
  33. package/dist/sandbox/header.js +19 -4
  34. package/dist/sandbox/install-plan.d.ts +1 -1
  35. package/dist/sandbox/install-plan.js +2 -4
  36. package/dist/sandbox/object.d.ts +1 -0
  37. package/dist/sandbox/object.js +21 -13
  38. package/package.json +9 -7
@@ -1,6 +1,7 @@
1
1
  import { tool } from "ai";
2
2
  import { z } from "zod";
3
- import { guardPath, WALK_SKIPS } from "./paths.js";
3
+ import { SANDBOX_ROOT } from "../sandbox/header.js";
4
+ import { guardPath, walkSkips } from "./paths.js";
4
5
  import { renderGrepMatches } from "./render.js";
5
6
  /**
6
7
  * Searching file contents, under Think's name so it replaces Think's `grep` —
@@ -31,7 +32,7 @@ export function grepTools(ctx) {
31
32
  /**
32
33
  * Search, without the container.
33
34
  *
34
- * The alternative is `bash("grep -rn …")`, and the case for a native one is
35
+ * The alternative is `exec("grep -rn …")`, and the case for a native one is
35
36
  * not that shelling out fails — it is where the search runs. `fs.grep` reads
36
37
  * the Durable Object's SQLite, so it answers while the container is being
37
38
  * replaced or an install is still running, which is exactly the window the
@@ -51,7 +52,7 @@ export function grepTools(ctx) {
51
52
  "The query is matched literally — set `regex` to interpret it as a regular expression. " +
52
53
  "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. " +
53
54
  "A cut result reports the `offset` that continues it. Use `context` to see the lines around a match. " +
54
- "Results from `.git` and `node_modules` are left out — search node_modules with bash.",
55
+ "Results from `.git` and `node_modules` are left out — search node_modules with exec.",
55
56
  inputSchema: z.object({
56
57
  query: z.string().describe("Text to find, e.g. 'buildComputerTools'"),
57
58
  path: z
@@ -102,7 +103,12 @@ export function grepTools(ctx) {
102
103
  context: around,
103
104
  limit: DEFAULT_MAX_MATCHES + 1,
104
105
  offset: from,
105
- exclude: WALK_SKIPS.map((segment) => `**/${segment}`)
106
+ // The same skips the workspace's own walks hand on, anchored to
107
+ // the served root rather than to the target: this tool replaces
108
+ // Think's `grep`, so a search of the root that kept the agent's
109
+ // own directories would fill a bounded page with them before the
110
+ // checkout's matches. See `src/computer/paths.ts`.
111
+ exclude: walkSkips(target, SANDBOX_ROOT)
106
112
  });
107
113
  if (matches.length === 0)
108
114
  return `no matches for ${JSON.stringify(query)} in ${target}${include ? ` (${include})` : ""}${from > 0 ? ` past offset ${from}` : ""}`;
@@ -17,6 +17,22 @@
17
17
  interface RefreshOutcome {
18
18
  message: string;
19
19
  branch?: string;
20
+ /**
21
+ * Set once this directory is known to hold a checkout of the repository that
22
+ * was asked for, with a tree git could read.
23
+ *
24
+ * Weaker than `branch` on purpose, and the difference is the dirty tree: that
25
+ * checkout is there and is the right repository, so an answer may describe
26
+ * what is in it, while nothing may *act* on it.
27
+ *
28
+ * The returns above the status read leave it unset, and so does the status
29
+ * read's own failure: a tree git could not read is not one to read anything
30
+ * else out of, and the doubt that stops the reset below stops a description of
31
+ * what is on the disk under it. A caller cannot tell any of that from the
32
+ * message alone, which is why this is a flag and not a reading of the
33
+ * sentence.
34
+ */
35
+ checkout?: true;
20
36
  }
21
37
  /**
22
38
  * What this module needs back from a git command.
@@ -119,6 +119,7 @@ export async function refreshCheckout({ dir, url, branch, author, plain, fetchOr
119
119
  // call established — kicking off an install over it would be acting on a
120
120
  // state the model has not looked at yet.
121
121
  return {
122
+ checkout: true,
122
123
  message: `${dir} already has this repository checked out, with uncommitted changes:\n` +
123
124
  `${dirty.stdout.trim()}\n\n` +
124
125
  `Left untouched — these may be unfinished work from an earlier task. ` +
@@ -140,10 +141,11 @@ export async function refreshCheckout({ dir, url, branch, author, plain, fetchOr
140
141
  const current = await plain("symbolic-ref --quiet --short HEAD", dir);
141
142
  const on = current.success ? current.stdout.trim() : "";
142
143
  if (!on)
143
- return { message: failure };
144
+ return { message: failure, checkout: true };
144
145
  return {
145
146
  message: `${failure}\nThe checkout was left on ${on}, clean but not updated from the remote.`,
146
- branch: on
147
+ branch: on,
148
+ checkout: true
147
149
  };
148
150
  }
149
151
  // The branch to land on: the one asked for, else the remote's own default.
@@ -155,12 +157,16 @@ export async function refreshCheckout({ dir, url, branch, author, plain, fetchOr
155
157
  // way.
156
158
  if (head.unreachable)
157
159
  return {
158
- message: `could not read the default branch for ${url}: ${head.unreachable}`
160
+ message: `could not read the default branch for ${url}: ${head.unreachable}`,
161
+ checkout: true
159
162
  };
160
163
  target = head.branch;
161
164
  }
162
165
  if (!target)
163
- return { message: `could not determine a default branch for ${url}` };
166
+ return {
167
+ message: `could not determine a default branch for ${url}`,
168
+ checkout: true
169
+ };
164
170
  // The one place in this plugin where a model-authored value lands in git's
165
171
  // *operand* position — everywhere else it is prefixed (`origin/…`,
166
172
  // `refs/heads/…`) or is a flag's value. Quoting is not enough: it stops
@@ -172,17 +178,20 @@ export async function refreshCheckout({ dir, url, branch, author, plain, fetchOr
172
178
  });
173
179
  if (!checkout.success)
174
180
  return {
175
- message: `could not check out ${target}: ${checkout.stderr || checkout.stdout}`
181
+ message: `could not check out ${target}: ${checkout.stderr || checkout.stdout}`,
182
+ checkout: true
176
183
  };
177
184
  const reset = await plain(`reset --hard "origin/$REPO_BRANCH"`, dir, {
178
185
  REPO_BRANCH: target
179
186
  });
180
187
  if (!reset.success)
181
188
  return {
182
- message: `could not reset to origin/${target}: ${reset.stderr || reset.stdout}`
189
+ message: `could not reset to origin/${target}: ${reset.stderr || reset.stdout}`,
190
+ checkout: true
183
191
  };
184
192
  return {
185
193
  message: `reused the existing checkout at ${dir}, fetched and reset to origin/${target}`,
186
- branch: target
194
+ branch: target,
195
+ checkout: true
187
196
  };
188
197
  }
@@ -47,6 +47,17 @@ export interface RepoContext {
47
47
  stdout?: string;
48
48
  }) => void;
49
49
  plain: (args: string, cwd: string, vars?: Record<string, string>) => Promise<RunResult>;
50
+ /**
51
+ * The same runner without git in front of it, for the one thing these tools
52
+ * read that git cannot answer: what a checkout's own files say.
53
+ * {@link file://./skills.ts skillInventory} is what runs on it.
54
+ *
55
+ * Published alongside `plain` rather than instead of it. Every command this
56
+ * plugin runs is still one of two shapes, and a reader can tell which from the
57
+ * call: `plain` is a git subcommand, `shell` is a script whose text is this
58
+ * package's own constant.
59
+ */
60
+ shell: (script: string, cwd: string, vars?: Record<string, string>) => Promise<RunResult>;
50
61
  runGit: (op: () => Promise<RepoGitResult>) => Promise<RunResult>;
51
62
  fetchOrigin: (dir: string, url: string) => Promise<RunResult>;
52
63
  pushBranch: (dir: string, url: string, branch: string) => Promise<RunResult>;
@@ -532,6 +532,7 @@ runtime = () => undefined) {
532
532
  notifyCheckout,
533
533
  logFailure,
534
534
  plain,
535
+ shell,
535
536
  runGit,
536
537
  fetchOrigin,
537
538
  pushBranch,
@@ -0,0 +1,167 @@
1
+ /**
2
+ * What skills a checkout carries, and how a prompt may reach each one.
3
+ *
4
+ * A Claude Code session reads the skills of the repository it is running in, and
5
+ * the agent that briefs that session has no other way to learn they are there:
6
+ * the session is a separate process with a context of its own, so a skill it
7
+ * should follow has to be named in the prompt it is given. This is what lets
8
+ * `repo_clone` answer with the list.
9
+ *
10
+ * **Why it is here rather than in the plugin that owns a session.** This package
11
+ * fails any subpath whose module graph reaches a sibling's, so the plugin that
12
+ * holds the checkout is the only one that can read it. What belongs to the
13
+ * session's own domain is the *policy*, which is to say when a skill should be
14
+ * named, and that stays with the host writing the prompt. This module reports
15
+ * what is in the checkout and what each skill's own frontmatter says about
16
+ * reaching it.
17
+ *
18
+ * The split inside the module is the one {@link file://./checkout.ts checkout.ts}
19
+ * uses: a runner arrives as a parameter and everything that reads is pure, so
20
+ * the parsing and the rendering are tested without a container.
21
+ */
22
+ /**
23
+ * Where Claude Code reads a project's skills from, relative to the checkout.
24
+ *
25
+ * One level of it: `<dir>/<name>/SKILL.md` and no deeper, which is the layout a
26
+ * session loads. A skill a repository keeps somewhere else of its own is not
27
+ * collected and is not reported as missing either, because nothing here can tell
28
+ * such a directory from any other.
29
+ */
30
+ export declare const SKILLS_DIR = ".claude/skills";
31
+ /**
32
+ * How many skills the answer lists, and how long a description it carries.
33
+ *
34
+ * Both bound an answer the model pays for on every clone, and neither is a
35
+ * judgement about which skills matter. They are set against a real checkout
36
+ * rather than guessed: this workspace's own set of skills renders whole at these
37
+ * values, in 5,731 characters, inside the sixteen thousand `maxOutputChars`
38
+ * defaults to. A cap tight enough to bite would drop skills in ordinal order,
39
+ * and the one dropped could be the one somebody has to name, which is the
40
+ * failure these numbers are chosen to stay clear of.
41
+ *
42
+ * A description here is a line that helps decide whether to open the skill, not
43
+ * a substitute for reading it.
44
+ *
45
+ * Exported for the specs, which assert the bound rather than restate the
46
+ * number. Nothing outside this package reads them: `./index.ts` is what the
47
+ * subpath publishes, and it exports neither.
48
+ */
49
+ export declare const MAX_SKILLS_LISTED = 40;
50
+ export declare const MAX_DESCRIPTION_CHARS = 200;
51
+ /**
52
+ * What one command this module runs answers with.
53
+ *
54
+ * A structural subset of `context.ts`'s `RunResult`, for the reason
55
+ * {@link file://./checkout.ts GitAnswer} is the home of: the dependency runs one
56
+ * way, so the tools know about this file and this file knows only the shape of
57
+ * an answer. Narrower than that one by `unreachable`, which nothing here reads:
58
+ * a read that never ran and a read that ran and failed cost the same line.
59
+ */
60
+ export interface ShellAnswer {
61
+ success: boolean;
62
+ stdout: string;
63
+ stderr: string;
64
+ }
65
+ /** The runner {@link skillInventory} borrows from `repoContext`. */
66
+ export type ShellRunner = (script: string, cwd: string) => Promise<ShellAnswer>;
67
+ /**
68
+ * Collect every skill's frontmatter block, each file's lines indented by one tab
69
+ * under the line naming its directory.
70
+ *
71
+ * **The tab is what makes the stream unambiguous.** A line at column zero names
72
+ * a skill's directory and every line of that skill's file is indented by one, so
73
+ * nothing in a file can be read as the start of another record. The obvious
74
+ * alternative, a separator line of its own, is a string a folded description
75
+ * could legally contain, and the failure would be a skill silently merged into
76
+ * its neighbour. It is written by `awk`'s own string escape, which is portable
77
+ * where `sed`'s `\t` is GNU's, and a published package does not get to assume a
78
+ * host's image.
79
+ *
80
+ * **`awk` stops at the closing delimiter**, so what arrives is the whole block
81
+ * and not a guess at how long one is. {@link readFrontmatter} reports a block
82
+ * that arrived without it, and {@link invocationOf} is why that matters: a
83
+ * restriction sits in whichever line of the block its author put it on, so a
84
+ * read that stops early is a read that cannot speak for the skill. The `NR == 1`
85
+ * test is the other half of the saving: a file with no block at all costs one
86
+ * line rather than the bound.
87
+ *
88
+ * **A file this cannot read costs its own record and no other.** The loop's exit
89
+ * status is the last `printf`'s, so a failed read leaves the directory line with
90
+ * nothing indented under it, which {@link readFrontmatter} reads as a block
91
+ * nobody has seen. Failing the whole read instead would spend every other
92
+ * skill's inventory on one unreadable file.
93
+ *
94
+ * It reads through whatever `<dir>/<name>` is. A checkout may commit each entry
95
+ * as a symlink to a directory it keeps elsewhere, which is how this workspace
96
+ * holds its own, and the glob and `head` both follow one.
97
+ *
98
+ * A glob that matches nothing stays literal in a POSIX shell, so the `-f` test
99
+ * is what makes an absent `.claude/skills` an empty answer rather than an error.
100
+ *
101
+ * The blank line after each record is the stream's own shape rather than a
102
+ * property of the tool above it: a name starts a line whatever the file below it
103
+ * ended with. No model input reaches any of this; every path is this module's
104
+ * own constant.
105
+ */
106
+ export declare const READ_SKILLS = "for d in .claude/skills/*/; do\n [ -f \"$d/SKILL.md\" ] || continue\n printf '%s\\n' \"${d%/}\"\n head -c 4000 \"$d/SKILL.md\" | awk -v lines=120 '\n { print \"\\t\" $0 }\n NR == 1 { if ($0 !~ /^---[[:space:]]*$/) exit; next }\n /^---[[:space:]]*$/ { exit }\n NR >= lines { exit }\n '\n printf '\\n'\ndone";
107
+ /**
108
+ * How a prompt may reach a skill, as its frontmatter decides.
109
+ *
110
+ * The distinction is not cosmetic, and it is the reason this module reports more
111
+ * than a name and a description. `disable-model-invocation` takes the skill away
112
+ * from the model's own skill tool, which leaves naming it as `/<name>` the only
113
+ * way in; `user-invocable: false` removes that command, which leaves prose. A
114
+ * host that cannot tell the two apart writes a prompt that silently does
115
+ * nothing.
116
+ *
117
+ * `unknown` is the one that is not a reading of the frontmatter but a statement
118
+ * about the read: the block did not arrive whole, so the fields that decide this
119
+ * are fields nobody has seen. It is reported rather than guessed because both
120
+ * guesses are wrong in a case that happens. The permissive one advertises a
121
+ * prompt-only skill as one a model picks up by itself, which is the silent
122
+ * failure above; the strict one takes a way in away from a skill that has it.
123
+ */
124
+ export type SkillInvocation = "either" | "command" | "prose" | "neither" | "unknown";
125
+ /** One skill a checkout carries. */
126
+ export interface RepoSkill {
127
+ /** What a prompt names it by: its frontmatter `name`, else its directory. */
128
+ name: string;
129
+ description?: string;
130
+ invocation: SkillInvocation;
131
+ }
132
+ /**
133
+ * The skills in a checkout, as a sentence the model reads, or why they could not
134
+ * be read.
135
+ *
136
+ * A failed read is reported and never raised: the clone it is appended to
137
+ * succeeded, and a missing inventory is worth a line rather than an error that
138
+ * sends the model to investigate git.
139
+ */
140
+ export declare function skillInventory(shell: ShellRunner, dir: string): Promise<string>;
141
+ /**
142
+ * {@link READ_SKILLS}'s output, read back.
143
+ *
144
+ * Unindented lines name directories and indented ones belong to the file above
145
+ * them, so the shape of the stream is what separates the records. A blank
146
+ * unindented line is the trailing newline of the stream, never the end of a
147
+ * record.
148
+ *
149
+ * **A record starts only on a path under {@link SKILLS_DIR}**, which is the one
150
+ * thing the indentation alone does not settle. Directory names come from a
151
+ * cloned repository, and one carrying a newline writes a second line at column
152
+ * zero: without the path, that line opens a record of its own and the skill
153
+ * above it loses the frontmatter that follows to a name nobody wrote. A
154
+ * directory name holds no `/`, so what the second line cannot be is a path,
155
+ * which is what closes it.
156
+ */
157
+ export declare function readSkillInventory(stdout: string): RepoSkill[];
158
+ /**
159
+ * The inventory as the model reads it: where this looked, a line per skill, and
160
+ * what the list left out.
161
+ *
162
+ * **The counts are unconditional**, the empty answer included. "No skills here"
163
+ * and "this tool does not report skills" are different facts, and a repository
164
+ * that keeps its skills somewhere Claude Code does not read them is the case
165
+ * that makes the difference worth a sentence.
166
+ */
167
+ export declare function skillInventoryText(skills: readonly RepoSkill[]): string;