howtofixcli 0.7.24 → 0.7.26
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/dist/account-credential.js +136 -0
- package/dist/acp.js +234 -0
- package/dist/activity.js +124 -0
- package/dist/agent-defs.js +237 -0
- package/dist/agent-limiter.js +68 -0
- package/dist/agents.js +554 -0
- package/dist/anim.js +361 -0
- package/dist/args.js +250 -0
- package/dist/at-file.js +28 -0
- package/dist/brand-extension.js +87 -0
- package/dist/brand.js +76 -0
- package/dist/browser.js +58 -0
- package/dist/cache-report.js +139 -0
- package/dist/checkpoint.js +437 -0
- package/dist/claims.js +167 -0
- package/dist/cli-auth.js +142 -0
- package/dist/cli-checkin.js +121 -0
- package/dist/cli-credentials.js +254 -0
- package/dist/clipboard.js +149 -0
- package/dist/collapse.js +129 -0
- package/dist/commands.js +381 -0
- package/dist/compaction-guard.js +81 -0
- package/dist/connect-prompt.js +22 -0
- package/dist/context-extension.js +17 -0
- package/dist/dcp/commands.js +172 -0
- package/dist/dcp/compress-state.js +139 -0
- package/dist/dcp/compress.js +329 -0
- package/dist/dcp/dcp-extension.js +295 -0
- package/dist/dcp/dedup.js +93 -0
- package/dist/dcp/ids.js +189 -0
- package/dist/dcp/index.js +25 -0
- package/dist/dcp/middle-prune.js +76 -0
- package/dist/dcp/prompts.js +68 -0
- package/dist/dcp/protected-content.js +221 -0
- package/dist/dcp/protected.js +161 -0
- package/dist/dcp/prune.js +418 -0
- package/dist/dcp/state.js +176 -0
- package/dist/errors.js +63 -0
- package/dist/extensions.js +48 -0
- package/dist/fatal.js +35 -0
- package/dist/feed.js +498 -0
- package/dist/fs-atomic.js +54 -0
- package/dist/gateway-errors.js +107 -0
- package/dist/git.js +230 -0
- package/dist/goal-extension.js +88 -0
- package/dist/goal.js +222 -0
- package/dist/hooks-extension.js +176 -0
- package/dist/hooks.js +443 -0
- package/dist/howtofix-account.js +834 -0
- package/dist/hud-line.js +158 -0
- package/dist/hwid.js +130 -0
- package/dist/interrupt.js +60 -0
- package/dist/json-events.js +50 -0
- package/dist/level-style.js +112 -0
- package/dist/loop-guard.js +95 -0
- package/dist/lsp-tools.js +32 -0
- package/dist/lsp.js +429 -0
- package/dist/main.js +573 -98
- package/dist/mcp-config.js +85 -0
- package/dist/mcp-lifecycle.js +147 -0
- package/dist/model-catalog.js +217 -0
- package/dist/model-facts.js +35 -0
- package/dist/model-resolve.js +65 -0
- package/dist/notes.js +78 -0
- package/dist/oauth-login.js +568 -0
- package/dist/paths.js +61 -0
- package/dist/permissions.js +298 -0
- package/dist/plan-mode.js +363 -0
- package/dist/preset-command.js +227 -0
- package/dist/preset-extension.js +35 -0
- package/dist/preset-mcp.js +292 -0
- package/dist/presets/mcp/rea/preset.json +28 -0
- package/dist/presets/mcp/rea/prompt.md +5 -0
- package/dist/presets/mcp/rea/skills/mcp-rea/SKILL.md +6 -0
- package/dist/presets/mcp/remotion/preset.json +27 -0
- package/dist/presets/mcp/remotion/prompt.md +7 -0
- package/dist/presets/mcp/remotion/skills/mcp-remotion/SKILL.md +6 -0
- package/dist/presets.js +727 -0
- package/dist/prompt-files.js +103 -0
- package/dist/prompt-text.js +41 -0
- package/dist/proxy.js +137 -0
- package/dist/quality/ast.js +196 -0
- package/dist/quality/cognitive.js +83 -0
- package/dist/quality/cyclomatic.js +30 -0
- package/dist/quality/halstead.js +83 -0
- package/dist/quality/index.js +18 -0
- package/dist/quality/length.js +3 -0
- package/dist/quality/maintainability.js +15 -0
- package/dist/quality/nesting.js +57 -0
- package/dist/quality/report.js +803 -0
- package/dist/quality/smells.js +67 -0
- package/dist/quality-tool.js +63 -0
- package/dist/read-only-commands.js +597 -0
- package/dist/repo-map.js +292 -0
- package/dist/roblox-connect.js +125 -0
- package/dist/rpc.js +91 -0
- package/dist/rtk.js +193 -0
- package/dist/sandbox.js +105 -0
- package/dist/schema.js +214 -0
- package/dist/serve.js +50 -0
- package/dist/session-lifecycle.js +19 -0
- package/dist/session-retention.js +111 -0
- package/dist/session.js +460 -0
- package/dist/sessions.js +100 -0
- package/dist/settings.js +148 -0
- package/dist/skills.js +35 -0
- package/dist/slop-tool.js +96 -0
- package/dist/spend-cap.js +43 -0
- package/dist/subagent.js +171 -0
- package/dist/tar.js +146 -0
- package/dist/theme.js +79 -0
- package/dist/thinking.js +45 -0
- package/dist/todo.js +114 -0
- package/dist/tool-loadout.js +102 -0
- package/dist/tool-render.js +769 -0
- package/dist/tools.js +176 -0
- package/dist/ui.js +4931 -0
- package/dist/update.js +551 -0
- package/dist/usage-segment.js +74 -0
- package/dist/web-tools.js +46 -0
- package/dist/web.js +265 -0
- package/dist/welcome.js +179 -0
- package/dist/workflow-engine.js +246 -0
- package/dist/workflow-progress.js +446 -0
- package/dist/workflow-view.js +195 -0
- package/dist/workflow.js +349 -0
- package/package.json +62 -15
- package/dist/chunk-27TRKD65.js +0 -2
- package/dist/chunk-3FYCFLNK.js +0 -1
- package/dist/chunk-3LZR7OEU.js +0 -1
- package/dist/chunk-56SEAGAQ.js +0 -1
- package/dist/chunk-5AWTL34S.js +0 -1
- package/dist/chunk-75P2SCOT.js +0 -1
- package/dist/chunk-7CHDNTH6.js +0 -1
- package/dist/chunk-7PHRNXLN.js +0 -34
- package/dist/chunk-AGGIYXN7.js +0 -2
- package/dist/chunk-AUDMRGWF.js +0 -189
- package/dist/chunk-C5E3SFOQ.js +0 -2
- package/dist/chunk-CMTKCUL5.js +0 -1
- package/dist/chunk-DAPXGSGD.js +0 -12
- package/dist/chunk-DOD7MUGX.js +0 -1
- package/dist/chunk-FPXVDP32.js +0 -8
- package/dist/chunk-GXB7RZHH.js +0 -1
- package/dist/chunk-I7KVLBMF.js +0 -2
- package/dist/chunk-ILDDDLRC.js +0 -1
- package/dist/chunk-JFYVRWJI.js +0 -2
- package/dist/chunk-LAHVHRCS.js +0 -2
- package/dist/chunk-LSJC73DG.js +0 -1
- package/dist/chunk-MNDKS6UH.js +0 -6
- package/dist/chunk-N345KWWK.js +0 -1
- package/dist/chunk-N3J2J7EQ.js +0 -5
- package/dist/chunk-ONZLNFQ2.js +0 -3
- package/dist/chunk-ORJ2QZBG.js +0 -3
- package/dist/chunk-PCALBZOO.js +0 -2
- package/dist/chunk-QPHSHL7H.js +0 -2
- package/dist/chunk-TKROS3BE.js +0 -5
- package/dist/chunk-TSEKPGK4.js +0 -1
- package/dist/chunk-UP7ENLEL.js +0 -117
- package/dist/chunk-UTYFLLSM.js +0 -1
- package/dist/chunk-V3R4FYTJ.js +0 -1
- package/dist/chunk-V656XVXV.js +0 -8
- package/dist/chunk-WKUJ7CFO.js +0 -1
- package/dist/chunk-WOT6VMZA.js +0 -1
- package/dist/chunk-WUPU6LTD.js +0 -4
- package/dist/chunk-WX3V5P7U.js +0 -5
- package/dist/chunk-XW6LMQGX.js +0 -1
- package/dist/chunk-YW7NORH3.js +0 -2
package/dist/rtk.js
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output compression for tool results (the "read the tool, keep the token" layer).
|
|
3
|
+
*
|
|
4
|
+
* A coding agent spends most of its context on tool output, and most of that output is
|
|
5
|
+
* not information: a `git diff` where one line in forty changed, a `grep` that matched
|
|
6
|
+
* the same import in fifty files, an `ls -R` of a `node_modules` nobody asked about.
|
|
7
|
+
* Paying full price for it shortens the effective context and, on a metered gateway,
|
|
8
|
+
* costs money for text the model will never reason about.
|
|
9
|
+
*
|
|
10
|
+
* This runs as a `tool_result` handler, which is the right seam: the result has already
|
|
11
|
+
* been produced and stored, so compression changes only what the model is asked to read,
|
|
12
|
+
* never what the tool actually returned. That distinction matters — a `bash` command that
|
|
13
|
+
* wrote a file must never be re-run or altered because its *report* was shortened.
|
|
14
|
+
*
|
|
15
|
+
* Two rules keep it honest:
|
|
16
|
+
*
|
|
17
|
+
* - **Never invent content.** Every line the model sees came from the tool. A summary
|
|
18
|
+
* says how many lines were dropped and where they went; it never paraphrases them.
|
|
19
|
+
* - **Never hide a failure.** An error result, or a result the tool marked truncated,
|
|
20
|
+
* is passed through untouched. Losing the one line that explains why a command failed
|
|
21
|
+
* costs far more than the tokens it saves.
|
|
22
|
+
*
|
|
23
|
+
* Off by default. Compression is a trade the caller makes with knowledge of their task:
|
|
24
|
+
* a search whose whole point is a count needs every line, and a run that is skimming a
|
|
25
|
+
* large diff does not want any.
|
|
26
|
+
*/
|
|
27
|
+
/** Tools whose output is worth compressing. Reads and writes are already exact. */
|
|
28
|
+
const COMPRESSIBLE = new Set(["bash", "grep", "find", "ls", "bash_output", "powershell"]);
|
|
29
|
+
/**
|
|
30
|
+
* Tools whose lines are *progress* rather than *findings*, and so may be run-collapsed.
|
|
31
|
+
*
|
|
32
|
+
* The distinction is what the numbers mean. In `Compiling module 10 done in 30ms` the
|
|
33
|
+
* numbers are a counter and the lines are interchangeable — keeping two loses nothing.
|
|
34
|
+
* In `src/auth.ts:42: const x = 1` the numbers identify *which* finding this is, and
|
|
35
|
+
* collapsing a hundred of them into a notice deletes the answer to the search the user
|
|
36
|
+
* just ran. A grep must therefore keep every line it matched; only its redundant blanks
|
|
37
|
+
* and an elision of the middle may go.
|
|
38
|
+
*/
|
|
39
|
+
const RUN_COLLAPSIBLE = new Set(["bash", "bash_output", "powershell"]);
|
|
40
|
+
/** Below this many lines there is nothing worth summarising. */
|
|
41
|
+
export const MIN_LINES = 40;
|
|
42
|
+
/** How many lines of a long run to keep at each end. */
|
|
43
|
+
export const KEEP_HEAD = 12;
|
|
44
|
+
export const KEEP_TAIL = 8;
|
|
45
|
+
/** A run must be at least this long before it is worth collapsing. */
|
|
46
|
+
export const MIN_RUN = 6;
|
|
47
|
+
/**
|
|
48
|
+
* True when a line carries no information beyond its neighbours' shape.
|
|
49
|
+
*
|
|
50
|
+
* Deliberately narrow. A "useful" test that guessed would eventually drop the one line
|
|
51
|
+
* that mattered, so only plainly redundant lines qualify: repeats of the line above,
|
|
52
|
+
* separators, and blank runs.
|
|
53
|
+
*/
|
|
54
|
+
function isRedundant(line, previous) {
|
|
55
|
+
const trimmed = line.trim();
|
|
56
|
+
if (trimmed === "")
|
|
57
|
+
return true;
|
|
58
|
+
// A rule of only punctuation or dashes is a separator, not content.
|
|
59
|
+
if (/^[-=*_~+#]{3,}$/.test(trimmed))
|
|
60
|
+
return true;
|
|
61
|
+
if (previous !== undefined && line === previous)
|
|
62
|
+
return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The shape of a line, with every number run replaced by a placeholder.
|
|
67
|
+
*
|
|
68
|
+
* Numbers move; the text around them is what makes two lines the same kind of line. A
|
|
69
|
+
* trailing-only match misses the common case — `Compiling module 10 done in 30ms` has
|
|
70
|
+
* its counter in the middle, so every line looked unique and none were collapsed.
|
|
71
|
+
*
|
|
72
|
+
* Collapsing is defensible for tool output in a way it is not for, say, a claim: the
|
|
73
|
+
* output is *reproducible*. The tool can be run again, and the notice below always says
|
|
74
|
+
* how many lines went and what shape they had, so nothing is lost that cannot be
|
|
75
|
+
* recovered by re-running the command.
|
|
76
|
+
*/
|
|
77
|
+
function shapeOf(line) {
|
|
78
|
+
return line.replace(/\d+/g, "#").trimEnd();
|
|
79
|
+
}
|
|
80
|
+
function collapseNumericRuns(lines) {
|
|
81
|
+
const out = [];
|
|
82
|
+
let collapsed = 0;
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (i < lines.length) {
|
|
85
|
+
const key = shapeOf(lines[i]);
|
|
86
|
+
// A line with no digits has no counter to move, so a run of them is content that
|
|
87
|
+
// happens to repeat — not progress. Only a changing number makes a run collapsible.
|
|
88
|
+
if (key === "" || !/\d/.test(lines[i])) {
|
|
89
|
+
out.push(lines[i]);
|
|
90
|
+
i += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
let j = i + 1;
|
|
94
|
+
while (j < lines.length && shapeOf(lines[j]) === key && /\d/.test(lines[j]))
|
|
95
|
+
j += 1;
|
|
96
|
+
const run = j - i;
|
|
97
|
+
if (run >= MIN_RUN) {
|
|
98
|
+
out.push(lines[i]);
|
|
99
|
+
out.push(` … ${run - 2} more line${run - 2 === 1 ? "" : "s"} of the same shape …`);
|
|
100
|
+
out.push(lines[j - 1]);
|
|
101
|
+
collapsed += 1;
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
for (let k = i; k < j; k += 1)
|
|
105
|
+
out.push(lines[k]);
|
|
106
|
+
}
|
|
107
|
+
i = j;
|
|
108
|
+
}
|
|
109
|
+
return { out, collapsed };
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Compress one tool result, or return undefined to leave it exactly as it was.
|
|
113
|
+
*
|
|
114
|
+
* Returning `undefined` rather than the original content is the difference between
|
|
115
|
+
* "nothing to do" and "replace it with itself": the handler contract treats an omitted
|
|
116
|
+
* field as untouched, and re-sending identical content burns the work for no gain.
|
|
117
|
+
*/
|
|
118
|
+
export function compressToolResult(text, details, options = {}) {
|
|
119
|
+
// A result the tool already truncated has been shaped by a rule that understands the
|
|
120
|
+
// tool; second-guessing it here only risks dropping the tail it deliberately kept.
|
|
121
|
+
if (details?.truncated)
|
|
122
|
+
return undefined;
|
|
123
|
+
const lines = text.split("\n");
|
|
124
|
+
if (lines.length < MIN_LINES)
|
|
125
|
+
return undefined;
|
|
126
|
+
// 1. Drop plainly redundant lines, keeping count so the notice can be truthful.
|
|
127
|
+
const kept = [];
|
|
128
|
+
let droppedRedundant = 0;
|
|
129
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
130
|
+
if (isRedundant(lines[i], kept[kept.length - 1])) {
|
|
131
|
+
// Keep one blank line as a paragraph break; collapse only the runs.
|
|
132
|
+
if (lines[i].trim() === "" && kept[kept.length - 1]?.trim() !== "") {
|
|
133
|
+
kept.push("");
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
droppedRedundant += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
kept.push(lines[i]);
|
|
140
|
+
}
|
|
141
|
+
// 2. Collapse repeated-shape runs — but only for tools whose numbers are progress
|
|
142
|
+
// rather than content. A grep's line numbers identify findings; collapsing them
|
|
143
|
+
// would delete the answer. See RUN_COLLAPSIBLE.
|
|
144
|
+
const mayCollapseRuns = options.toolName === undefined || RUN_COLLAPSIBLE.has(options.toolName);
|
|
145
|
+
const { out: collapsedLines, collapsed } = mayCollapseRuns ? collapseNumericRuns(kept) : { out: kept, collapsed: 0 };
|
|
146
|
+
// 3. Keep both ends if it is still too long. The head carries the command and its
|
|
147
|
+
// first results; the tail carries the failure or the summary, which is usually
|
|
148
|
+
// why anyone reads a long output at all.
|
|
149
|
+
const head = options.head ?? KEEP_HEAD;
|
|
150
|
+
const tail = options.tail ?? KEEP_TAIL;
|
|
151
|
+
let final = collapsedLines;
|
|
152
|
+
let elided = 0;
|
|
153
|
+
if (collapsedLines.length > head + tail + MIN_RUN) {
|
|
154
|
+
elided = collapsedLines.length - head - tail;
|
|
155
|
+
final = [...collapsedLines.slice(0, head), ` … ${elided} line${elided === 1 ? "" : "s"} omitted (${head} kept from the top, ${tail} from the end) …`, ...collapsedLines.slice(collapsedLines.length - tail)];
|
|
156
|
+
}
|
|
157
|
+
const saved = droppedRedundant + collapsed;
|
|
158
|
+
if (saved === 0 && elided === 0)
|
|
159
|
+
return undefined;
|
|
160
|
+
return {
|
|
161
|
+
text: final.join("\n"),
|
|
162
|
+
stats: { before: lines.length, after: final.length, collapsed },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Build the extension that applies compression to tool results.
|
|
167
|
+
*
|
|
168
|
+
* `enabled` is read per call rather than captured, so toggling it mid-session takes
|
|
169
|
+
* effect on the next tool result instead of the next process.
|
|
170
|
+
*/
|
|
171
|
+
export function createRtkExtension(options) {
|
|
172
|
+
return (hxf) => {
|
|
173
|
+
hxf.on("tool_result", (event) => {
|
|
174
|
+
if (!options.enabled())
|
|
175
|
+
return undefined;
|
|
176
|
+
// A failure is never compressed. The line that explains it is exactly the
|
|
177
|
+
// line a heuristic is most likely to decide is redundant.
|
|
178
|
+
if (event.isError)
|
|
179
|
+
return undefined;
|
|
180
|
+
if (!COMPRESSIBLE.has(event.toolName))
|
|
181
|
+
return undefined;
|
|
182
|
+
const textPart = event.content.find((c) => c.type === "text");
|
|
183
|
+
if (!textPart || textPart.type !== "text")
|
|
184
|
+
return undefined;
|
|
185
|
+
const details = event.details;
|
|
186
|
+
const result = compressToolResult(textPart.text, details, { toolName: event.toolName });
|
|
187
|
+
if (!result)
|
|
188
|
+
return undefined;
|
|
189
|
+
const content = event.content.map((c) => (c === textPart ? { type: "text", text: result.text } : c));
|
|
190
|
+
return { content };
|
|
191
|
+
});
|
|
192
|
+
};
|
|
193
|
+
}
|
package/dist/sandbox.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function isExecutable(path) {
|
|
5
|
+
try {
|
|
6
|
+
accessSync(path, constants.X_OK);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
/** Which sandbox this machine can actually provide, if any. */
|
|
14
|
+
export function detectSandbox(platform = process.platform, pathEnv = process.env.PATH ?? "") {
|
|
15
|
+
const dirs = pathEnv.split(":").filter(Boolean);
|
|
16
|
+
const find = (name) => dirs.map((dir) => join(dir, name)).find(isExecutable);
|
|
17
|
+
if (platform === "linux") {
|
|
18
|
+
const bwrap = find("bwrap");
|
|
19
|
+
// Installed is not the same as usable: a container or a kernel with
|
|
20
|
+
// unprivileged user namespaces disabled has the binary and still refuses to
|
|
21
|
+
// create a namespace. Probing costs a few milliseconds and turns a confusing
|
|
22
|
+
// runtime failure into an honest "not available here".
|
|
23
|
+
if (!bwrap)
|
|
24
|
+
return undefined;
|
|
25
|
+
const probe = spawnSync(bwrap, ["--ro-bind", "/", "/", "--", "true"], { stdio: "ignore", timeout: 3000 });
|
|
26
|
+
return probe.status === 0 ? "bubblewrap" : undefined;
|
|
27
|
+
}
|
|
28
|
+
if (platform === "darwin")
|
|
29
|
+
return find("sandbox-exec") ? "seatbelt" : undefined;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The argv that re-runs `argv` inside the sandbox.
|
|
34
|
+
*
|
|
35
|
+
* The bind list is the security boundary: `/` is mounted read-only, then the
|
|
36
|
+
* project and the CLI's state directory are mounted writable over it. `/tmp` is
|
|
37
|
+
* a fresh tmpfs, so a build cannot leave anything behind outside the project.
|
|
38
|
+
* Network stays on — a coding agent that cannot reach a registry is not usable —
|
|
39
|
+
* and `--unshare-pid` is left off because some toolchains need to see the
|
|
40
|
+
* process tree.
|
|
41
|
+
*/
|
|
42
|
+
export function sandboxArgv(kind, paths) {
|
|
43
|
+
if (kind === "bubblewrap") {
|
|
44
|
+
return [
|
|
45
|
+
"bwrap",
|
|
46
|
+
"--die-with-parent",
|
|
47
|
+
"--new-session",
|
|
48
|
+
"--ro-bind",
|
|
49
|
+
"/",
|
|
50
|
+
"/",
|
|
51
|
+
"--dev",
|
|
52
|
+
"/dev",
|
|
53
|
+
"--proc",
|
|
54
|
+
"/proc",
|
|
55
|
+
"--tmpfs",
|
|
56
|
+
"/tmp",
|
|
57
|
+
"--bind",
|
|
58
|
+
paths.cwd,
|
|
59
|
+
paths.cwd,
|
|
60
|
+
"--bind",
|
|
61
|
+
paths.agentDir,
|
|
62
|
+
paths.agentDir,
|
|
63
|
+
"--chdir",
|
|
64
|
+
paths.cwd,
|
|
65
|
+
"--",
|
|
66
|
+
...paths.argv,
|
|
67
|
+
];
|
|
68
|
+
}
|
|
69
|
+
const profile = [
|
|
70
|
+
"(version 1)",
|
|
71
|
+
"(deny default)",
|
|
72
|
+
"(allow process*)",
|
|
73
|
+
"(allow signal)",
|
|
74
|
+
"(allow sysctl-read)",
|
|
75
|
+
"(allow mach-lookup)",
|
|
76
|
+
"(allow network*)",
|
|
77
|
+
"(allow file-read*)",
|
|
78
|
+
`(allow file-write* (subpath "${paths.cwd}") (subpath "${paths.agentDir}") (subpath "/tmp") (subpath "/private/tmp") (subpath "/private/var/folders"))`,
|
|
79
|
+
"",
|
|
80
|
+
].join("\n");
|
|
81
|
+
return ["sandbox-exec", "-p", profile, ...paths.argv];
|
|
82
|
+
}
|
|
83
|
+
/** A sentence for the banner, so the mode is never a secret. */
|
|
84
|
+
export function describeSandbox(kind) {
|
|
85
|
+
return kind === "bubblewrap"
|
|
86
|
+
? "sandbox: bubblewrap — project writable, rest of the filesystem read-only"
|
|
87
|
+
: "sandbox: seatbelt — project writable, rest of the filesystem read-only";
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Re-execute this process inside the sandbox and return its exit code.
|
|
91
|
+
*
|
|
92
|
+
* Called once, before any session exists, and guarded by HOWTOFIX_SANDBOXED so
|
|
93
|
+
* the re-executed process does not try to wrap itself again.
|
|
94
|
+
*/
|
|
95
|
+
export function enterSandbox(kind, paths) {
|
|
96
|
+
const result = spawnSync(sandboxArgv(kind, paths)[0], sandboxArgv(kind, paths).slice(1), {
|
|
97
|
+
stdio: "inherit",
|
|
98
|
+
env: { ...process.env, HOWTOFIX_SANDBOXED: "1" },
|
|
99
|
+
});
|
|
100
|
+
if (result.error) {
|
|
101
|
+
console.error(`sandbox: could not start ${kind}: ${result.error.message}`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
return result.status ?? 1;
|
|
105
|
+
}
|
package/dist/schema.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small JSON Schema validator, sufficient for the `schema` option of
|
|
3
|
+
* `agent()` in a workflow script.
|
|
4
|
+
*
|
|
5
|
+
* Why not a library: `ajv` is not installed and the SDK does not export a
|
|
6
|
+
* validator, so a dependency would have to be vendored into a repo whose rule is
|
|
7
|
+
* not to re-vendor the SDK. The supported subset is the one the workflow spec names
|
|
8
|
+
* for structured output — `type`, `properties`, `required`, `items`, `enum`,
|
|
9
|
+
* `anyOf`/`oneOf`/`allOf`, `$defs`/`definitions`, `additionalProperties` — which
|
|
10
|
+
* is small enough to implement honestly, and the failure messages matter more
|
|
11
|
+
* than breadth here: they are fed back to the model as a repair turn.
|
|
12
|
+
*
|
|
13
|
+
* Validation is intentionally permissive about keywords it does not understand
|
|
14
|
+
* (they are ignored rather than failing the value), because the contract is that
|
|
15
|
+
* a reply validating against the authored schema is accepted.
|
|
16
|
+
*/
|
|
17
|
+
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
18
|
+
function typeOf(value) {
|
|
19
|
+
if (value === null)
|
|
20
|
+
return "null";
|
|
21
|
+
if (Array.isArray(value))
|
|
22
|
+
return "array";
|
|
23
|
+
if (Number.isInteger(value))
|
|
24
|
+
return "integer";
|
|
25
|
+
return typeof value;
|
|
26
|
+
}
|
|
27
|
+
function matchesType(value, want) {
|
|
28
|
+
const actual = typeOf(value);
|
|
29
|
+
if (want === "number")
|
|
30
|
+
return actual === "number" || actual === "integer";
|
|
31
|
+
if (want === "integer")
|
|
32
|
+
return actual === "integer";
|
|
33
|
+
return actual === want;
|
|
34
|
+
}
|
|
35
|
+
/** Resolve a `$ref` against the document root (`#/$defs/name` or `#/definitions/name`). */
|
|
36
|
+
function resolveRef(root, ref) {
|
|
37
|
+
if (typeof ref !== "string" || !ref.startsWith("#/"))
|
|
38
|
+
return undefined;
|
|
39
|
+
let node = root;
|
|
40
|
+
for (const rawPart of ref.slice(2).split("/")) {
|
|
41
|
+
const part = rawPart.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
42
|
+
if (!isObject(node) && !Array.isArray(node))
|
|
43
|
+
return undefined;
|
|
44
|
+
node = node[part];
|
|
45
|
+
}
|
|
46
|
+
return isObject(node) ? node : undefined;
|
|
47
|
+
}
|
|
48
|
+
function validateNode(value, schema, root, path, issues) {
|
|
49
|
+
if (!isObject(schema))
|
|
50
|
+
return;
|
|
51
|
+
if (schema.$ref !== undefined) {
|
|
52
|
+
const target = resolveRef(root, schema.$ref);
|
|
53
|
+
if (target)
|
|
54
|
+
validateNode(value, target, root, path, issues);
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Combinators. `allOf` must hold everywhere; `anyOf`/`oneOf` need a passing branch.
|
|
58
|
+
if (Array.isArray(schema.allOf)) {
|
|
59
|
+
for (const branch of schema.allOf)
|
|
60
|
+
validateNode(value, branch, root, path, issues);
|
|
61
|
+
}
|
|
62
|
+
for (const keyword of ["anyOf", "oneOf"]) {
|
|
63
|
+
const branches = schema[keyword];
|
|
64
|
+
if (!Array.isArray(branches) || branches.length === 0)
|
|
65
|
+
continue;
|
|
66
|
+
const passes = branches.filter((branch) => {
|
|
67
|
+
const scratch = [];
|
|
68
|
+
validateNode(value, branch, root, path, scratch);
|
|
69
|
+
return scratch.length === 0;
|
|
70
|
+
}).length;
|
|
71
|
+
if (keyword === "anyOf" && passes === 0)
|
|
72
|
+
issues.push({ path, message: "does not match any allowed shape" });
|
|
73
|
+
if (keyword === "oneOf" && passes !== 1)
|
|
74
|
+
issues.push({ path, message: `must match exactly one shape (matched ${passes})` });
|
|
75
|
+
}
|
|
76
|
+
const declaredType = schema.type;
|
|
77
|
+
if (declaredType !== undefined) {
|
|
78
|
+
const wanted = Array.isArray(declaredType) ? declaredType.map(String) : [String(declaredType)];
|
|
79
|
+
if (!wanted.some((want) => matchesType(value, want))) {
|
|
80
|
+
issues.push({ path, message: `expected ${wanted.join(" or ")}, got ${typeOf(value)}` });
|
|
81
|
+
return; // A wrong type makes the remaining checks noise.
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(schema.enum) && !schema.enum.some((option) => JSON.stringify(option) === JSON.stringify(value))) {
|
|
85
|
+
issues.push({ path, message: `must be one of ${schema.enum.map((option) => JSON.stringify(option)).join(", ")}` });
|
|
86
|
+
}
|
|
87
|
+
if (isObject(value)) {
|
|
88
|
+
const properties = isObject(schema.properties) ? schema.properties : {};
|
|
89
|
+
if (Array.isArray(schema.required)) {
|
|
90
|
+
for (const key of schema.required.map(String)) {
|
|
91
|
+
if (!(key in value))
|
|
92
|
+
issues.push({ path: path ? `${path}.${key}` : key, message: "is required" });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
for (const [key, propertySchema] of Object.entries(properties)) {
|
|
96
|
+
if (key in value)
|
|
97
|
+
validateNode(value[key], propertySchema, root, path ? `${path}.${key}` : key, issues);
|
|
98
|
+
}
|
|
99
|
+
if (schema.additionalProperties === false) {
|
|
100
|
+
for (const key of Object.keys(value)) {
|
|
101
|
+
if (!(key in properties))
|
|
102
|
+
issues.push({ path: path ? `${path}.${key}` : key, message: "is not an allowed property" });
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
else if (isObject(schema.additionalProperties)) {
|
|
106
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
107
|
+
if (!(key in properties))
|
|
108
|
+
validateNode(entry, schema.additionalProperties, root, path ? `${path}.${key}` : key, issues);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (Array.isArray(value) && schema.items !== undefined) {
|
|
113
|
+
const items = schema.items;
|
|
114
|
+
if (Array.isArray(items)) {
|
|
115
|
+
// Tuple form: validate positionally.
|
|
116
|
+
value.forEach((entry, index) => {
|
|
117
|
+
if (index < items.length)
|
|
118
|
+
validateNode(entry, items[index], root, `${path}[${index}]`, issues);
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
value.forEach((entry, index) => validateNode(entry, items, root, `${path}[${index}]`, issues));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (typeof value === "string") {
|
|
126
|
+
if (typeof schema.minLength === "number" && value.length < schema.minLength) {
|
|
127
|
+
issues.push({ path, message: `is shorter than ${schema.minLength} characters` });
|
|
128
|
+
}
|
|
129
|
+
if (typeof schema.pattern === "string") {
|
|
130
|
+
try {
|
|
131
|
+
if (!new RegExp(schema.pattern).test(value))
|
|
132
|
+
issues.push({ path, message: `does not match ${schema.pattern}` });
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
// An invalid pattern is the schema author's problem, not the value's.
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (typeof value === "number") {
|
|
140
|
+
if (typeof schema.minimum === "number" && value < schema.minimum)
|
|
141
|
+
issues.push({ path, message: `is below ${schema.minimum}` });
|
|
142
|
+
if (typeof schema.maximum === "number" && value > schema.maximum)
|
|
143
|
+
issues.push({ path, message: `is above ${schema.maximum}` });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
/** Every way `value` fails `schema`. Empty means it validates. */
|
|
147
|
+
export function validateSchema(value, schema) {
|
|
148
|
+
if (!isObject(schema))
|
|
149
|
+
return [];
|
|
150
|
+
const issues = [];
|
|
151
|
+
validateNode(value, schema, schema, "", issues);
|
|
152
|
+
return issues;
|
|
153
|
+
}
|
|
154
|
+
/** One line per issue, for a repair turn. */
|
|
155
|
+
export function formatIssues(issues) {
|
|
156
|
+
return issues.map((issue) => `- ${issue.path || "(root)"}: ${issue.message}`).join("\n");
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Pull a JSON value out of a model reply.
|
|
160
|
+
*
|
|
161
|
+
* Models wrap JSON in prose or fences often enough that a bare `JSON.parse` is
|
|
162
|
+
* not a fair test of whether they followed the schema. Only the first complete
|
|
163
|
+
* value is taken; anything before or after it is ignored.
|
|
164
|
+
*/
|
|
165
|
+
export function extractJson(text) {
|
|
166
|
+
const trimmed = text.trim();
|
|
167
|
+
const fenced = /```(?:json)?\s*([\s\S]*?)```/.exec(trimmed);
|
|
168
|
+
const candidates = [trimmed, fenced?.[1]?.trim()].filter((candidate) => Boolean(candidate));
|
|
169
|
+
for (const candidate of candidates) {
|
|
170
|
+
try {
|
|
171
|
+
return JSON.parse(candidate);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
/* try to find a balanced value inside */
|
|
175
|
+
}
|
|
176
|
+
for (let start = 0; start < candidate.length; start++) {
|
|
177
|
+
const opener = candidate[start];
|
|
178
|
+
if (opener !== "{" && opener !== "[")
|
|
179
|
+
continue;
|
|
180
|
+
const closer = opener === "{" ? "}" : "]";
|
|
181
|
+
let depth = 0;
|
|
182
|
+
let inString = false;
|
|
183
|
+
let escaped = false;
|
|
184
|
+
for (let index = start; index < candidate.length; index++) {
|
|
185
|
+
const char = candidate[index];
|
|
186
|
+
if (inString) {
|
|
187
|
+
if (escaped)
|
|
188
|
+
escaped = false;
|
|
189
|
+
else if (char === "\\")
|
|
190
|
+
escaped = true;
|
|
191
|
+
else if (char === '"')
|
|
192
|
+
inString = false;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (char === '"')
|
|
196
|
+
inString = true;
|
|
197
|
+
else if (char === opener)
|
|
198
|
+
depth++;
|
|
199
|
+
else if (char === closer) {
|
|
200
|
+
depth--;
|
|
201
|
+
if (depth === 0) {
|
|
202
|
+
try {
|
|
203
|
+
return JSON.parse(candidate.slice(start, index + 1));
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
package/dist/serve.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { APP_VERSION } from "./brand.js";
|
|
2
|
+
/** Adapt an opened session to the slice of it the protocol layers use. */
|
|
3
|
+
export function agentFromSession(opened) {
|
|
4
|
+
return {
|
|
5
|
+
prompt: (text) => opened.session.prompt(text),
|
|
6
|
+
abort: () => opened.session.abort(),
|
|
7
|
+
subscribe: (listener) => opened.session.subscribe((event) => listener(event)),
|
|
8
|
+
getLastAssistantText: () => opened.session.getLastAssistantText(),
|
|
9
|
+
lastStopReason: () => {
|
|
10
|
+
const last = opened.session.state.messages.at(-1);
|
|
11
|
+
return last?.role === "assistant" ? last.stopReason : undefined;
|
|
12
|
+
},
|
|
13
|
+
dispose: () => opened.dispose(),
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
/** Run the chosen protocol on stdin/stdout until stdin closes. */
|
|
17
|
+
export async function serveProtocol(mode, options) {
|
|
18
|
+
const { openSession } = await import("./session.js");
|
|
19
|
+
const open = (cwd) => openSession({
|
|
20
|
+
cwd,
|
|
21
|
+
model: options.model,
|
|
22
|
+
thinking: options.thinking,
|
|
23
|
+
continueSession: false,
|
|
24
|
+
noSession: false,
|
|
25
|
+
sessionPath: undefined,
|
|
26
|
+
// No terminal: a skill that asks a question must say so instead of waiting
|
|
27
|
+
// on a person who is not there.
|
|
28
|
+
interactive: false,
|
|
29
|
+
});
|
|
30
|
+
if (mode === "acp") {
|
|
31
|
+
const { runAcp } = await import("./acp.js");
|
|
32
|
+
await runAcp({
|
|
33
|
+
input: process.stdin,
|
|
34
|
+
output: process.stdout,
|
|
35
|
+
version: APP_VERSION,
|
|
36
|
+
createAgent: async (cwd) => agentFromSession(await open(cwd)),
|
|
37
|
+
});
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const { runRpc } = await import("./rpc.js");
|
|
41
|
+
const { jsonLine } = await import("./json-events.js");
|
|
42
|
+
const opened = await open(options.cwd);
|
|
43
|
+
await runRpc({
|
|
44
|
+
input: process.stdin,
|
|
45
|
+
output: process.stdout,
|
|
46
|
+
agent: agentFromSession(opened),
|
|
47
|
+
encodeEvent: (event) => jsonLine(event),
|
|
48
|
+
state: () => ({ sessionId: opened.session.sessionId, model: opened.modelLabel, cwd: options.cwd }),
|
|
49
|
+
});
|
|
50
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Close extension-owned resources before disposing an agent session.
|
|
3
|
+
*
|
|
4
|
+
* AgentSession.dispose() disconnects the agent itself, but extension resources
|
|
5
|
+
* (including MCP transports) are released by the session_shutdown event. The
|
|
6
|
+
* SDK exposes the runner so callers can deliver that lifecycle event even when
|
|
7
|
+
* a session is replaced outside the interactive mode.
|
|
8
|
+
*
|
|
9
|
+
* The returned promise settles once the shutdown has been delivered and the
|
|
10
|
+
* session is disposed. Callers must await it: dropping the promise leaves the
|
|
11
|
+
* MCP processes and tool registrations alive after the configuration that
|
|
12
|
+
* created them is gone.
|
|
13
|
+
*/
|
|
14
|
+
export function disposeAgentSession(session) {
|
|
15
|
+
return session.extensionRunner
|
|
16
|
+
.emit({ type: "session_shutdown", reason: "quit" })
|
|
17
|
+
.catch(() => undefined)
|
|
18
|
+
.finally(() => session.dispose());
|
|
19
|
+
}
|