@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.
- package/README.md +1 -1
- package/dist/claude-code/cards.d.ts +6 -1
- package/dist/claude-code/cards.js +21 -2
- package/dist/claude-code/model.d.ts +4 -0
- package/dist/claude-code/model.js +74 -7
- package/dist/claude-code/verified.d.ts +1 -1
- package/dist/claude-code/verified.js +1 -1
- package/dist/computer/client.d.ts +9 -1
- package/dist/computer/context.js +1 -1
- package/dist/computer/index.d.ts +10 -6
- package/dist/computer/index.js +34 -17
- package/dist/computer/paths.d.ts +26 -2
- package/dist/computer/paths.js +51 -5
- package/dist/computer/proxy.d.ts +10 -3
- package/dist/computer/proxy.js +77 -87
- package/dist/computer/render.d.ts +6 -35
- package/dist/computer/render.js +5 -43
- package/dist/computer/sandbox.d.ts +4 -3
- package/dist/computer/sandbox.js +9 -2
- package/dist/computer/shell.d.ts +8 -7
- package/dist/computer/shell.js +8 -7
- package/dist/computer/tools-exec.d.ts +39 -0
- package/dist/computer/tools-exec.js +357 -228
- package/dist/computer/tools-grep.js +10 -4
- package/dist/repo-github/checkout.d.ts +16 -0
- package/dist/repo-github/checkout.js +16 -7
- package/dist/repo-github/context.d.ts +11 -0
- package/dist/repo-github/context.js +1 -0
- package/dist/repo-github/skills.d.ts +167 -0
- package/dist/repo-github/skills.js +414 -0
- package/dist/repo-github/tools-clone.js +25 -4
- package/dist/sandbox/header.d.ts +13 -0
- package/dist/sandbox/header.js +19 -4
- package/dist/sandbox/install-plan.d.ts +1 -1
- package/dist/sandbox/install-plan.js +2 -4
- package/dist/sandbox/object.d.ts +1 -0
- package/dist/sandbox/object.js +21 -13
- package/package.json +9 -7
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { tool } from "ai";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
-
import {
|
|
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 `
|
|
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
|
|
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
|
-
|
|
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 {
|
|
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>;
|
|
@@ -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;
|