howtofixcli 0.7.7 → 0.7.9
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/chunk-27TRKD65.js +2 -0
- package/dist/chunk-3FYCFLNK.js +1 -0
- package/dist/chunk-56SEAGAQ.js +1 -0
- package/dist/chunk-63ZCQDXH.js +6 -0
- package/dist/chunk-6FUWMLWO.js +1 -0
- package/dist/chunk-7CHDNTH6.js +1 -0
- package/dist/chunk-ACOMNDUL.js +185 -0
- package/dist/chunk-AEVPET7X.js +2 -0
- package/dist/chunk-AGGIYXN7.js +2 -0
- package/dist/chunk-BHVBO4SC.js +1 -0
- package/dist/chunk-FPXVDP32.js +8 -0
- package/dist/chunk-GIUYIKH3.js +109 -0
- package/dist/chunk-GQCAJ56I.js +5 -0
- package/dist/chunk-GXB7RZHH.js +1 -0
- package/dist/chunk-IGEY3AWF.js +1 -0
- package/dist/chunk-ILDDDLRC.js +1 -0
- package/dist/chunk-JFYVRWJI.js +2 -0
- package/dist/chunk-MVLCGU3G.js +1 -0
- package/dist/chunk-N345KWWK.js +1 -0
- package/dist/chunk-NBQFPCRT.js +1 -0
- package/dist/chunk-OIPSLIZE.js +5 -0
- package/dist/chunk-ORJ2QZBG.js +3 -0
- package/dist/chunk-PCALBZOO.js +2 -0
- package/dist/chunk-PDNBGUYP.js +2 -0
- package/dist/chunk-S656RE4A.js +1 -0
- package/dist/chunk-TSEKPGK4.js +1 -0
- package/dist/chunk-U6CI6KXN.js +34 -0
- package/dist/chunk-UTYFLLSM.js +1 -0
- package/dist/chunk-V3R4FYTJ.js +1 -0
- package/dist/chunk-V656XVXV.js +8 -0
- package/dist/chunk-WKUJ7CFO.js +1 -0
- package/dist/chunk-WOT6VMZA.js +1 -0
- package/dist/chunk-WUPU6LTD.js +4 -0
- package/dist/chunk-WX3V5P7U.js +5 -0
- package/dist/chunk-ZKZ6WCI3.js +2 -0
- package/dist/main.js +94 -536
- package/package.json +15 -62
- package/dist/account-credential.js +0 -136
- package/dist/account-credential.test.js +0 -38
- package/dist/activity.js +0 -124
- package/dist/agent-defs.js +0 -237
- package/dist/agent-limiter.js +0 -68
- package/dist/agents.js +0 -554
- package/dist/anim.js +0 -361
- package/dist/args.js +0 -228
- package/dist/at-file.js +0 -28
- package/dist/brand-extension.js +0 -87
- package/dist/brand.js +0 -76
- package/dist/browser.js +0 -58
- package/dist/cache-report.js +0 -139
- package/dist/checkpoint.js +0 -437
- package/dist/claims.js +0 -167
- package/dist/cli-auth.js +0 -142
- package/dist/cli-checkin.js +0 -116
- package/dist/cli-credentials.js +0 -254
- package/dist/clipboard.js +0 -149
- package/dist/collapse.js +0 -129
- package/dist/commands.js +0 -301
- package/dist/compaction-guard.js +0 -81
- package/dist/context-extension.js +0 -17
- package/dist/dcp/commands.js +0 -172
- package/dist/dcp/compress-state.js +0 -139
- package/dist/dcp/compress.js +0 -329
- package/dist/dcp/dcp-extension.js +0 -288
- package/dist/dcp/dedup.js +0 -93
- package/dist/dcp/ids.js +0 -189
- package/dist/dcp/index.js +0 -25
- package/dist/dcp/prompts.js +0 -68
- package/dist/dcp/protected-content.js +0 -221
- package/dist/dcp/protected.js +0 -161
- package/dist/dcp/prune.js +0 -418
- package/dist/dcp/state.js +0 -176
- package/dist/device-id.js +0 -90
- package/dist/device-id.test.js +0 -23
- package/dist/errors.js +0 -63
- package/dist/extensions.js +0 -48
- package/dist/fatal.js +0 -35
- package/dist/feed.js +0 -498
- package/dist/fs-atomic.js +0 -54
- package/dist/gateway-errors.js +0 -90
- package/dist/git.js +0 -230
- package/dist/goal-extension.js +0 -88
- package/dist/goal.js +0 -222
- package/dist/hooks-extension.js +0 -176
- package/dist/hooks.js +0 -443
- package/dist/howtofix-account.js +0 -715
- package/dist/hud-line.js +0 -153
- package/dist/hwid.js +0 -130
- package/dist/interrupt.js +0 -60
- package/dist/json-events.js +0 -50
- package/dist/level-style.js +0 -112
- package/dist/loop-guard.js +0 -95
- package/dist/lsp-tools.js +0 -32
- package/dist/lsp.js +0 -347
- package/dist/mcp-config.js +0 -85
- package/dist/mcp-lifecycle.js +0 -147
- package/dist/model-catalog.js +0 -217
- package/dist/model-resolve.js +0 -65
- package/dist/notes.js +0 -78
- package/dist/oauth-login.js +0 -568
- package/dist/paths.js +0 -61
- package/dist/permissions.js +0 -298
- package/dist/plan-mode.js +0 -363
- package/dist/preset-command.js +0 -227
- package/dist/preset-extension.js +0 -35
- package/dist/preset-mcp.js +0 -292
- package/dist/presets.js +0 -727
- package/dist/prompt-files.js +0 -103
- package/dist/prompt-text.js +0 -41
- package/dist/proxy.js +0 -137
- package/dist/quality/ast.js +0 -196
- package/dist/quality/cognitive.js +0 -83
- package/dist/quality/cyclomatic.js +0 -30
- package/dist/quality/halstead.js +0 -83
- package/dist/quality/index.js +0 -18
- package/dist/quality/length.js +0 -3
- package/dist/quality/maintainability.js +0 -15
- package/dist/quality/nesting.js +0 -57
- package/dist/quality/report.js +0 -803
- package/dist/quality/smells.js +0 -67
- package/dist/quality-tool.js +0 -63
- package/dist/read-only-commands.js +0 -597
- package/dist/repo-map.js +0 -292
- package/dist/rtk.js +0 -193
- package/dist/sandbox.js +0 -105
- package/dist/schema.js +0 -214
- package/dist/session-lifecycle.js +0 -19
- package/dist/session-retention.js +0 -111
- package/dist/session.js +0 -446
- package/dist/sessions.js +0 -100
- package/dist/settings.js +0 -148
- package/dist/skills.js +0 -35
- package/dist/slop-tool.js +0 -96
- package/dist/spend-cap.js +0 -43
- package/dist/subagent.js +0 -171
- package/dist/tar.js +0 -146
- package/dist/theme.js +0 -79
- package/dist/thinking.js +0 -45
- package/dist/todo.js +0 -114
- package/dist/tool-loadout.js +0 -97
- package/dist/tool-render.js +0 -769
- package/dist/tools.js +0 -176
- package/dist/ui.js +0 -4751
- package/dist/update.js +0 -533
- package/dist/usage-segment.js +0 -69
- package/dist/web-tools.js +0 -46
- package/dist/web.js +0 -265
- package/dist/welcome.js +0 -179
- package/dist/workflow-engine.js +0 -246
- package/dist/workflow-progress.js +0 -446
- package/dist/workflow-view.js +0 -195
- package/dist/workflow.js +0 -349
package/dist/settings.js
DELETED
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
-
import { dirname, join } from "node:path";
|
|
3
|
-
import { writeFileAtomicSync } from "./fs-atomic.js";
|
|
4
|
-
export const DEFAULT_WORKBENCH_SETTINGS = {
|
|
5
|
-
skipPermissions: false,
|
|
6
|
-
autoUpdate: true,
|
|
7
|
-
rightClickPaste: "auto",
|
|
8
|
-
welcomeSeen: false,
|
|
9
|
-
compressToolOutput: false,
|
|
10
|
-
};
|
|
11
|
-
function workbenchSettingsPath(agentDir) {
|
|
12
|
-
return join(agentDir, "settings.json");
|
|
13
|
-
}
|
|
14
|
-
/**
|
|
15
|
-
* The stored preferences, with anything missing filled from the defaults.
|
|
16
|
-
*
|
|
17
|
-
* A non-boolean `skipPermissions` is ignored rather than coerced: `"false"` is
|
|
18
|
-
* truthy, and a value that did not come from this file is not a decision the
|
|
19
|
-
* user made here. `rightClickPaste` is read the same way — only the three
|
|
20
|
-
* spellings this CLI writes are accepted, because an unrecognised one is not a
|
|
21
|
-
* preference and guessing at it would be inventing a setting.
|
|
22
|
-
*/
|
|
23
|
-
export function readWorkbenchSettings(agentDir) {
|
|
24
|
-
try {
|
|
25
|
-
const parsed = JSON.parse(readFileSync(workbenchSettingsPath(agentDir), "utf8"));
|
|
26
|
-
const boolean = (key) => {
|
|
27
|
-
const value = parsed[key];
|
|
28
|
-
return typeof value === "boolean" ? value : DEFAULT_WORKBENCH_SETTINGS[key];
|
|
29
|
-
};
|
|
30
|
-
const rightClick = parsed.rightClickPaste;
|
|
31
|
-
return {
|
|
32
|
-
skipPermissions: boolean("skipPermissions"),
|
|
33
|
-
autoUpdate: boolean("autoUpdate"),
|
|
34
|
-
rightClickPaste: rightClick === "on" || rightClick === "off" || rightClick === "auto" ? rightClick : DEFAULT_WORKBENCH_SETTINGS.rightClickPaste,
|
|
35
|
-
welcomeSeen: boolean("welcomeSeen"),
|
|
36
|
-
compressToolOutput: boolean("compressToolOutput"),
|
|
37
|
-
// Only a positive integer is a preference. Anything else is an
|
|
38
|
-
// unrecognised value, and guessing at it would be inventing a setting.
|
|
39
|
-
...(typeof parsed.agents === "number" && Number.isInteger(parsed.agents) && parsed.agents > 0 ? { agents: parsed.agents } : {}),
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return { ...DEFAULT_WORKBENCH_SETTINGS };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
/**
|
|
47
|
-
* Store one preference, leaving every other key in the file untouched.
|
|
48
|
-
*
|
|
49
|
-
* Returns false when the file exists but cannot be parsed, so the caller can
|
|
50
|
-
* report it instead of silently discarding the change.
|
|
51
|
-
*/
|
|
52
|
-
export function writeWorkbenchSetting(agentDir, key, value) {
|
|
53
|
-
return writeSetting(agentDir, key, value);
|
|
54
|
-
}
|
|
55
|
-
/**
|
|
56
|
-
* Record that the first-run greeting has been shown.
|
|
57
|
-
*
|
|
58
|
-
* Takes no value: the only writer is the welcome screen, which sets this once
|
|
59
|
-
* and never clears it, so "already seen" has a single spelling and there is no
|
|
60
|
-
* parameter through which it could ever be un-set.
|
|
61
|
-
*/
|
|
62
|
-
export function markWelcomeSeen(agentDir) {
|
|
63
|
-
return writeSetting(agentDir, "welcomeSeen", true);
|
|
64
|
-
}
|
|
65
|
-
/**
|
|
66
|
-
* Store the right-click-paste preference.
|
|
67
|
-
*
|
|
68
|
-
* `"auto"` is stored by *removing* the key, so a file that never mentions it and
|
|
69
|
-
* a file that says `"auto"` mean the same thing and there is one spelling of the
|
|
70
|
-
* default rather than two.
|
|
71
|
-
*/
|
|
72
|
-
export function writeRightClickPasteSetting(agentDir, value) {
|
|
73
|
-
return writeSetting(agentDir, "rightClickPaste", value === "auto" ? undefined : value);
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* Whether the CLI should paste on a right click.
|
|
77
|
-
*
|
|
78
|
-
* On Windows it must not: Windows Terminal and the legacy console both paste on
|
|
79
|
-
* a right click themselves, and the SDK's own mouse handler passes the click
|
|
80
|
-
* through to them, so handling it here as well pasted the clipboard twice. That
|
|
81
|
-
* is the bug this setting exists for — the CLI used to answer this question with
|
|
82
|
-
* a bare `process.platform !== "win32"` and the SDK with `platform === "win32"`,
|
|
83
|
-
* so between the two of them nothing handled it.
|
|
84
|
-
*
|
|
85
|
-
* Elsewhere it must: mouse tracking has already stopped the terminal from
|
|
86
|
-
* pasting, so the click reaches the app and a right click would do nothing at
|
|
87
|
-
* all without this.
|
|
88
|
-
*
|
|
89
|
-
* `"on"` and `"off"` are the user overruling that, for a terminal configured
|
|
90
|
-
* unlike its platform's default.
|
|
91
|
-
*/
|
|
92
|
-
export function rightClickPastesHere(preference) {
|
|
93
|
-
if (preference === "on")
|
|
94
|
-
return true;
|
|
95
|
-
if (preference === "off")
|
|
96
|
-
return false;
|
|
97
|
-
return process.platform !== "win32";
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Store a string preference, or clear it when the value is empty.
|
|
101
|
-
*
|
|
102
|
-
* A separate entry point from the boolean one because the two are not the same
|
|
103
|
-
* kind of change: a cleared string is a key that should stop existing, so
|
|
104
|
-
* `/proxy off` removes it rather than writing `""` — a file that says
|
|
105
|
-
* `"httpsProxy": ""` invites the reader to wonder whether an empty proxy is a
|
|
106
|
-
* proxy.
|
|
107
|
-
*/
|
|
108
|
-
/**
|
|
109
|
-
* Store the agent-concurrency preference.
|
|
110
|
-
*
|
|
111
|
-
* `undefined` (or anything that is not a positive integer) means "size it to the
|
|
112
|
-
* machine", so the key is removed — one spelling of the default rather than two.
|
|
113
|
-
*/
|
|
114
|
-
export function writeAgentsSetting(agentDir, value) {
|
|
115
|
-
const wanted = typeof value === "number" && Number.isInteger(value) && value > 0 ? value : undefined;
|
|
116
|
-
return writeSetting(agentDir, "agents", wanted);
|
|
117
|
-
}
|
|
118
|
-
export function writeStringSetting(agentDir, key, value) {
|
|
119
|
-
const trimmed = value.trim();
|
|
120
|
-
return writeSetting(agentDir, key, trimmed === "" ? undefined : trimmed);
|
|
121
|
-
}
|
|
122
|
-
function writeSetting(agentDir, key, value) {
|
|
123
|
-
const path = workbenchSettingsPath(agentDir);
|
|
124
|
-
let parsed = {};
|
|
125
|
-
try {
|
|
126
|
-
const candidate = JSON.parse(readFileSync(path, "utf8"));
|
|
127
|
-
if (candidate && typeof candidate === "object" && !Array.isArray(candidate))
|
|
128
|
-
parsed = candidate;
|
|
129
|
-
}
|
|
130
|
-
catch {
|
|
131
|
-
// A file that exists but does not parse is the user's, and is not ours to
|
|
132
|
-
// overwrite. A missing file is the normal first write.
|
|
133
|
-
if (existsSync(path))
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
if (value === undefined)
|
|
137
|
-
delete parsed[key];
|
|
138
|
-
else
|
|
139
|
-
parsed[key] = value;
|
|
140
|
-
try {
|
|
141
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
142
|
-
writeFileAtomicSync(path, `${JSON.stringify(parsed, null, 2)}\n`);
|
|
143
|
-
}
|
|
144
|
-
catch {
|
|
145
|
-
return false;
|
|
146
|
-
}
|
|
147
|
-
return true;
|
|
148
|
-
}
|
package/dist/skills.js
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import { USER_SKILLS_DIR, projectSkillsDir } from "./paths.js";
|
|
2
|
-
export function skillSearchPaths(cwd) {
|
|
3
|
-
return [USER_SKILLS_DIR, projectSkillsDir(cwd)];
|
|
4
|
-
}
|
|
5
|
-
/**
|
|
6
|
-
* Recognise a skill invocation and normalise it to `/skill:<name> [args]`.
|
|
7
|
-
*
|
|
8
|
-
* Accepts both `/skill:name` and the friendlier `/skill name`. The SDK's
|
|
9
|
-
* expander only looks for `/skill:`, so the space form has to be rewritten
|
|
10
|
-
* before it reaches `session.prompt`. Returns `undefined` for an unknown
|
|
11
|
-
* skill name or any other leading slash — those stay with `runSlash`.
|
|
12
|
-
*/
|
|
13
|
-
export function skillInvocation(text, names) {
|
|
14
|
-
const trimmed = text.trim();
|
|
15
|
-
const match = /^\/skill:([^\s]+)(?:\s+([\s\S]*))?$/.exec(trimmed) ?? /^\/skill\s+([^\s]+)(?:\s+([\s\S]*))?$/.exec(trimmed);
|
|
16
|
-
if (!match)
|
|
17
|
-
return undefined;
|
|
18
|
-
const name = match[1] ?? "";
|
|
19
|
-
if (!names.includes(name))
|
|
20
|
-
return undefined;
|
|
21
|
-
const args = (match[2] ?? "").trim();
|
|
22
|
-
return args ? `/skill:${name} ${args}` : `/skill:${name}`;
|
|
23
|
-
}
|
|
24
|
-
export function formatSkillList(skills) {
|
|
25
|
-
if (skills.length === 0) {
|
|
26
|
-
const [userDir, projectDir] = skillSearchPaths(process.cwd());
|
|
27
|
-
return ["no skills loaded.", `put a SKILL.md in ${userDir}`, `or in ${projectDir}`, "invoke with /skill:<name>"].join("\n");
|
|
28
|
-
}
|
|
29
|
-
return skills
|
|
30
|
-
.map((skill) => {
|
|
31
|
-
const flag = skill.disableModelInvocation ? " (invoke only)" : "";
|
|
32
|
-
return `${skill.name}${flag}\n ${skill.description}\n ${skill.filePath}`;
|
|
33
|
-
})
|
|
34
|
-
.join("\n");
|
|
35
|
-
}
|
package/dist/slop-tool.js
DELETED
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The `slop` tool — the gate, callable by the agent on itself.
|
|
3
|
-
*
|
|
4
|
-
* Read-only by construction: it analyses and reports, it never edits. That is
|
|
5
|
-
* what makes it worth offering. A run that ends with "done" can be checked
|
|
6
|
-
* against a list instead of a claim, and the list names a rule and a line, so
|
|
7
|
-
* the agent can act on it before reporting rather than after being asked.
|
|
8
|
-
*
|
|
9
|
-
* It runs `scripts/slop.mjs` as a child process rather than importing it. The
|
|
10
|
-
* script is the gate: it is what pre-commit runs and what CI would run, and a
|
|
11
|
-
* second in-process implementation would be a different checker wearing the
|
|
12
|
-
* same name. The contract is the CLI's own `--json` output.
|
|
13
|
-
*
|
|
14
|
-
* Defaults to the changed files. A whole-project scan takes a few seconds and
|
|
15
|
-
* buries the one line the agent just wrote under 66 accepted findings, which is
|
|
16
|
-
* the opposite of useful when the question is "did I introduce something".
|
|
17
|
-
*/
|
|
18
|
-
import { spawnSync } from "node:child_process";
|
|
19
|
-
import { join } from "node:path";
|
|
20
|
-
import { defineTool } from "@netrodev/hxf-coding-agent";
|
|
21
|
-
import { Type } from "typebox";
|
|
22
|
-
/** Longest report handed back, so one call cannot flood the context. */
|
|
23
|
-
const MAX_REPORT_CHARS = 6000;
|
|
24
|
-
export const slopTool = defineTool({
|
|
25
|
-
name: "slop",
|
|
26
|
-
label: "slop",
|
|
27
|
-
description: "Check code against the project's slop gate: naming, comments, placeholder values, swallowed errors, and the LLM tells that appear in model-written code. Read-only. Use it before reporting a task finished; it returns each finding as a rule id, a line, and what is wrong, so a claim of clean code becomes something the user can verify. Defaults to the changed files; pass a path to narrow it.",
|
|
28
|
-
promptSnippet: "slop: naming, comment and placeholder findings for changed code",
|
|
29
|
-
parameters: Type.Object({
|
|
30
|
-
path: Type.Optional(Type.String({ description: "One file or directory, relative to the project root. Omit to scan the changed set." })),
|
|
31
|
-
changes: Type.Optional(Type.Boolean({ description: "Scan only files changed against the base ref. Default true when path is omitted." })),
|
|
32
|
-
base: Type.Optional(Type.String({ description: "Git ref for `changes`. Defaults to HEAD." })),
|
|
33
|
-
}),
|
|
34
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
35
|
-
const cwd = ctx.cwd ?? process.cwd();
|
|
36
|
-
const script = join(cwd, "scripts", "slop.mjs");
|
|
37
|
-
const args = [script, "--json"];
|
|
38
|
-
if (params.path !== undefined)
|
|
39
|
-
args.push(params.path);
|
|
40
|
-
else if (params.changes ?? true)
|
|
41
|
-
args.push("--changes");
|
|
42
|
-
if (params.base !== undefined)
|
|
43
|
-
args.push("--base", params.base);
|
|
44
|
-
const run = spawnSync("node", args, { cwd, encoding: "utf8", timeout: 120_000 });
|
|
45
|
-
// The gate reports "off" through its own switch, and its exit code carries
|
|
46
|
-
// meaning: 0 is advisory, 1 would be a strict failure this mode never asks
|
|
47
|
-
// for. An unparseable body is the failure to report, not the exit code.
|
|
48
|
-
let parsed;
|
|
49
|
-
try {
|
|
50
|
-
parsed = JSON.parse(run.stdout);
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
parsed = undefined;
|
|
54
|
-
}
|
|
55
|
-
if (!parsed) {
|
|
56
|
-
// A spawn that never started, or one the timeout killed, leaves `stdout`
|
|
57
|
-
// and `stderr` null and puts the reason in `error`/`signal` instead. Reading
|
|
58
|
-
// only the streams reported "no output" for both — the one message that
|
|
59
|
-
// tells the reader nothing, while the cause (`spawnSync node ENOENT`, a
|
|
60
|
-
// 120 s timeout) was right there and discarded.
|
|
61
|
-
const detail = (run.stderr || run.stdout || "").trim() || run.error?.message || (run.signal ? `killed by ${run.signal} (timed out after 120 s?)` : "no output");
|
|
62
|
-
return {
|
|
63
|
-
content: [{ type: "text", text: `slop gate produced no report: ${detail.slice(0, 400)}` }],
|
|
64
|
-
details: { error: detail.slice(0, 400) },
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
if (parsed.off) {
|
|
68
|
-
return {
|
|
69
|
-
content: [{ type: "text", text: "slop gate is off" }],
|
|
70
|
-
details: { off: true },
|
|
71
|
-
};
|
|
72
|
-
}
|
|
73
|
-
const files = parsed.files ?? {};
|
|
74
|
-
const lines = [];
|
|
75
|
-
let findings = 0;
|
|
76
|
-
for (const [file, list] of Object.entries(files)) {
|
|
77
|
-
lines.push(`${file}`);
|
|
78
|
-
for (const finding of list) {
|
|
79
|
-
findings += 1;
|
|
80
|
-
lines.push(` ${finding.rule} L${finding.line} ${finding.message}`);
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
if (findings === 0) {
|
|
84
|
-
return {
|
|
85
|
-
content: [{ type: "text", text: "slop gate: no findings in the scanned files" }],
|
|
86
|
-
details: { files: Object.keys(files).length, findings: 0 },
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
const report = lines.join("\n").slice(0, MAX_REPORT_CHARS);
|
|
90
|
-
const tail = report.length < lines.join("\n").length ? "\n… truncated" : "";
|
|
91
|
-
return {
|
|
92
|
-
content: [{ type: "text", text: `${findings} finding${findings === 1 ? "" : "s"}:\n\n${report}${tail}` }],
|
|
93
|
-
details: { files: Object.keys(files).length, findings },
|
|
94
|
-
};
|
|
95
|
-
},
|
|
96
|
-
});
|
package/dist/spend-cap.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
export function createSpendCap(limit) {
|
|
2
|
-
let carried = 0;
|
|
3
|
-
let hit;
|
|
4
|
-
return {
|
|
5
|
-
limit,
|
|
6
|
-
close(cost) {
|
|
7
|
-
carried += cost;
|
|
8
|
-
},
|
|
9
|
-
total(liveCost) {
|
|
10
|
-
return carried + liveCost;
|
|
11
|
-
},
|
|
12
|
-
check(liveCost) {
|
|
13
|
-
if (limit === undefined || hit !== undefined)
|
|
14
|
-
return hit;
|
|
15
|
-
const spent = carried + liveCost;
|
|
16
|
-
// A carried total is a sum of doubles, so a session meant to land exactly
|
|
17
|
-
// on the limit can come out a few ulps over it (0.9 + 0.05 is already
|
|
18
|
-
// 0.9500000000000001) and fire a budget the user never overspent. A tenth
|
|
19
|
-
// of a nano-dollar is far below any charge the gateway reports, and it is
|
|
20
|
-
// what keeps "spent exactly the budget" from counting as over.
|
|
21
|
-
if (spent - limit > 1e-9)
|
|
22
|
-
hit = { spent, limit };
|
|
23
|
-
return hit;
|
|
24
|
-
},
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* The sentence both modes say, so `--print` and the TUI cannot drift into two
|
|
29
|
-
* different descriptions of the same stop.
|
|
30
|
-
*/
|
|
31
|
-
export function spendCapMessage(spent, limit) {
|
|
32
|
-
return `stopped — spent $${spent.toFixed(4)} of a $${limit.toFixed(2)} budget.`;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* The refusal for a cap pointed at a model that declares no prices: every turn
|
|
36
|
-
* reports $0, so the cap can never fire and only *looks* enforced. Refusing up
|
|
37
|
-
* front is the honest version. Shared by both modes for the same reason.
|
|
38
|
-
*/
|
|
39
|
-
export function unpricedBudgetMessage(modelLabel, stateDir) {
|
|
40
|
-
return (`--max-budget-usd needs a model with prices, and ${modelLabel} declares none ` +
|
|
41
|
-
`(its rates are zero, so every turn costs $0 and the budget would never fire). ` +
|
|
42
|
-
`Add a cost to that model in ${stateDir}/models.json, or drop the flag.`);
|
|
43
|
-
}
|
package/dist/subagent.js
DELETED
|
@@ -1,171 +0,0 @@
|
|
|
1
|
-
import { Type } from "@netrodev/hxf-ai";
|
|
2
|
-
import { defineTool } from "@netrodev/hxf-coding-agent";
|
|
3
|
-
import { agents, MAX_TASKS_PER_CALL, drain, modelRef, spawn, startBackgroundDrain, summarizeRun } from "./agents.js";
|
|
4
|
-
import { createAgentWorktree } from "./git.js";
|
|
5
|
-
export { listRuns, runs, showRun } from "./agents.js";
|
|
6
|
-
/** The agents a caller can name, as one clause. */
|
|
7
|
-
function describeAgents() {
|
|
8
|
-
return [...agents.values()].map((agent) => `${agent.name} (${agent.description})`).join(", ");
|
|
9
|
-
}
|
|
10
|
-
/**
|
|
11
|
-
* Build the `task` tool.
|
|
12
|
-
*
|
|
13
|
-
* A factory rather than a constant because the description has to list the
|
|
14
|
-
* agents, and definition files are read after this module is imported. A
|
|
15
|
-
* constant would advertise the four built-ins for the life of the process, so a
|
|
16
|
-
* user's own agent would exist while the model was never told it is there —
|
|
17
|
-
* which is the same as it not existing.
|
|
18
|
-
*/
|
|
19
|
-
export function createTaskTool() {
|
|
20
|
-
return defineTool({
|
|
21
|
-
name: "task",
|
|
22
|
-
label: "task",
|
|
23
|
-
description: `Spawn subagents. Available: ${describeAgents()}. One call can carry tasks[]; each item may set agent, model, effort and isolation. Agents run concurrently, park when done, and can be resumed or messaged with /agents. Pass background: true to return as soon as they start instead of waiting for them. Use architect to design an approach before a large change. Set isolation: "worktree" on every task that writes files.`,
|
|
24
|
-
promptSnippet: "task: spawn background agents",
|
|
25
|
-
parameters: Type.Object({
|
|
26
|
-
agent: Type.Optional(Type.String()),
|
|
27
|
-
effort: Type.Optional(Type.String()),
|
|
28
|
-
model: Type.Optional(Type.String()),
|
|
29
|
-
task: Type.Optional(Type.String()),
|
|
30
|
-
/**
|
|
31
|
-
* `"worktree"` gives the tasks in this call their own checkouts. Off by
|
|
32
|
-
* default: a copy costs a directory and a `git worktree add`, which buys
|
|
33
|
-
* nothing for a read-only lookup.
|
|
34
|
-
*/
|
|
35
|
-
isolation: Type.Optional(Type.String()),
|
|
36
|
-
/** Start them and return, instead of waiting for the results. */
|
|
37
|
-
background: Type.Optional(Type.Boolean()),
|
|
38
|
-
tasks: Type.Optional(Type.Array(Type.Object({
|
|
39
|
-
agent: Type.Optional(Type.String()),
|
|
40
|
-
effort: Type.Optional(Type.String()),
|
|
41
|
-
model: Type.Optional(Type.String()),
|
|
42
|
-
task: Type.String(),
|
|
43
|
-
isolation: Type.Optional(Type.String()),
|
|
44
|
-
}))),
|
|
45
|
-
}),
|
|
46
|
-
executionMode: "parallel",
|
|
47
|
-
async execute(_toolCallId, params, signal, onUpdate, ctx) {
|
|
48
|
-
const asked = params.tasks && params.tasks.length > 0
|
|
49
|
-
? params.tasks.map((item) => ({
|
|
50
|
-
agent: item.agent ?? params.agent ?? "task",
|
|
51
|
-
task: item.task,
|
|
52
|
-
effort: item.effort ?? params.effort,
|
|
53
|
-
model: item.model ?? params.model,
|
|
54
|
-
isolation: item.isolation ?? params.isolation,
|
|
55
|
-
}))
|
|
56
|
-
: [
|
|
57
|
-
{
|
|
58
|
-
agent: params.agent ?? "task",
|
|
59
|
-
task: params.task ?? "",
|
|
60
|
-
effort: params.effort,
|
|
61
|
-
model: params.model,
|
|
62
|
-
isolation: params.isolation,
|
|
63
|
-
},
|
|
64
|
-
];
|
|
65
|
-
// One call cannot queue more than the host can carry. Saying so beats
|
|
66
|
-
// silently dropping the extras, which would read as children that never ran.
|
|
67
|
-
const items = asked.slice(0, MAX_TASKS_PER_CALL);
|
|
68
|
-
const dropped = asked.length - items.length;
|
|
69
|
-
const background = Boolean(params.background);
|
|
70
|
-
const mine = [];
|
|
71
|
-
// One checkout per task, created before the run exists so `spawn` can be
|
|
72
|
-
// pointed at it. `openAgentSession` resolves `run.worktree || run.cwd`, so a
|
|
73
|
-
// run that never gets one behaves exactly as it did before this option.
|
|
74
|
-
//
|
|
75
|
-
// A task that asked for isolation and did not get it (not a git repository, or
|
|
76
|
-
// `worktree add` refused) still runs, in the shared tree. It has to: refusing
|
|
77
|
-
// would turn an unavailable optimisation into a failed task. The note says so,
|
|
78
|
-
// because a silent fallback to the shared tree is the collision this option
|
|
79
|
-
// exists to prevent, and the caller is the only one who can decide to retry.
|
|
80
|
-
const created = [];
|
|
81
|
-
const unisolated = [];
|
|
82
|
-
// Directories too large to copy into a worktree. Reported rather than left
|
|
83
|
-
// implicit: an agent that cannot install or build will otherwise report a pass it
|
|
84
|
-
// never checked, and the caller would have no way to know why.
|
|
85
|
-
const unshared = [];
|
|
86
|
-
try {
|
|
87
|
-
for (const item of items) {
|
|
88
|
-
if (!item.task.trim())
|
|
89
|
-
continue;
|
|
90
|
-
let worktree;
|
|
91
|
-
if (item.isolation === "worktree") {
|
|
92
|
-
const made = await createAgentWorktree(ctx?.cwd ?? process.cwd(), item.agent);
|
|
93
|
-
if (made) {
|
|
94
|
-
created.push(made);
|
|
95
|
-
worktree = made.path;
|
|
96
|
-
for (const name of made.skipped)
|
|
97
|
-
unshared.push(`${item.agent}:${name}`);
|
|
98
|
-
}
|
|
99
|
-
else {
|
|
100
|
-
unisolated.push(item.agent);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
// The workspace the tool call runs in, so the child writes into the
|
|
104
|
-
// session's project rather than the CLI process's own directory. The
|
|
105
|
-
// parent's model rides along so a child that names none runs the model
|
|
106
|
-
// the user is actually paying for, not the runtime's default.
|
|
107
|
-
mine.push(await spawn({ ...item, cwd: ctx?.cwd, background, parentModel: modelRef(ctx?.model), worktree }));
|
|
108
|
-
onUpdate?.({
|
|
109
|
-
content: [],
|
|
110
|
-
details: { agents: mine.map((run) => ({ id: run.id, agent: run.agent, status: run.status })) },
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
if (mine.length === 0)
|
|
114
|
-
throw new Error("task or tasks is required");
|
|
115
|
-
const overflow = dropped > 0 ? `(only the first ${MAX_TASKS_PER_CALL} of ${asked.length} tasks ran)` : "";
|
|
116
|
-
const isolated = mine.filter((run) => run.worktree).length;
|
|
117
|
-
const notes = [
|
|
118
|
-
overflow,
|
|
119
|
-
unisolated.length > 0 ? `isolation: "worktree" was requested but not available for: ${unisolated.join(", ")}. Those agents ran in the shared working tree — check their edits for collisions before trusting them.` : "",
|
|
120
|
-
unshared.length > 0 ? `too large to copy into the isolated worktree, so it was left out: ${unshared.join(", ")}. An agent that needs it cannot install or build — treat any "done" from those runs as unverified.` : "",
|
|
121
|
-
].filter(Boolean);
|
|
122
|
-
const details = {
|
|
123
|
-
agents: mine.map((run) => ({ id: run.id, status: run.status, isolated: Boolean(run.worktree) })),
|
|
124
|
-
};
|
|
125
|
-
if (background) {
|
|
126
|
-
// The pump is started, not awaited: this turn ends here and the session
|
|
127
|
-
// is free for the next one. Each run announces itself through the
|
|
128
|
-
// finished-run handler as it settles.
|
|
129
|
-
startBackgroundDrain();
|
|
130
|
-
const ids = mine.map((run) => `${run.agent}#${run.id}`).join(", ");
|
|
131
|
-
return {
|
|
132
|
-
content: [
|
|
133
|
-
{
|
|
134
|
-
type: "text",
|
|
135
|
-
text: `started ${mine.length} agent${mine.length === 1 ? "" : "s"} in the background: ${ids}.` +
|
|
136
|
-
(isolated > 0 ? ` ${isolated} of them in their own worktree.` : "") +
|
|
137
|
-
"\nTheir results are not part of this turn — keep working. Read them with /agents, or message one with /agents <id> <text>." +
|
|
138
|
-
(notes.length > 0 ? `\n${notes.join("\n")}` : ""),
|
|
139
|
-
},
|
|
140
|
-
],
|
|
141
|
-
details,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
await drain(signal);
|
|
145
|
-
const parts = mine.map((run) => `${summarizeRun(run)}${run.worktree ? " [isolated]" : ""}\n${run.result ?? ""}`);
|
|
146
|
-
if (notes.length > 0)
|
|
147
|
-
parts.push(notes.join("\n"));
|
|
148
|
-
return { content: [{ type: "text", text: parts.join("\n\n") }], details };
|
|
149
|
-
}
|
|
150
|
-
finally {
|
|
151
|
-
// The checkouts go when the call is done with them, successes and failures
|
|
152
|
-
// alike. A worktree left behind is invisible to the project's own
|
|
153
|
-
// `git status` (it lives in the temp dir) and would accumulate silently.
|
|
154
|
-
//
|
|
155
|
-
// Removal is safe here even though it looks like it discards work: a
|
|
156
|
-
// detached worktree holds no branch, so the only edits that exist are
|
|
157
|
-
// uncommitted ones, and they were made by an agent whose result is
|
|
158
|
-
// already in `run.result`. `createAgentWorktree` places the tree at HEAD,
|
|
159
|
-
// and the run's output — not the directory — is the deliverable.
|
|
160
|
-
for (const worktree of created) {
|
|
161
|
-
try {
|
|
162
|
-
await worktree.remove();
|
|
163
|
-
}
|
|
164
|
-
catch {
|
|
165
|
-
// Never throws by contract, but a throw here must not mask the result.
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
},
|
|
170
|
-
});
|
|
171
|
-
}
|
package/dist/tar.js
DELETED
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A minimal tar reader.
|
|
3
|
-
*
|
|
4
|
-
* Presets are installed from a GitHub tarball, and Node ships `zlib` but not a
|
|
5
|
-
* tar parser. Adding a dependency for this would put a third-party package on
|
|
6
|
-
* the path that runs whenever a user installs a preset — a large amount of
|
|
7
|
-
* trust for reading a format that has not changed since 1988. The subset below
|
|
8
|
-
* is the whole of what GitHub emits: ustar headers, pax extended headers for
|
|
9
|
-
* long paths, and GNU long-name entries.
|
|
10
|
-
*
|
|
11
|
-
* Nothing here writes to disk. `parseTar` returns entries and the caller decides
|
|
12
|
-
* what to do with them, so the path rules are in one place and testable without
|
|
13
|
-
* a filesystem.
|
|
14
|
-
*/
|
|
15
|
-
import { gunzipSync } from "node:zlib";
|
|
16
|
-
const BLOCK = 512;
|
|
17
|
-
function readCString(buf, start, length) {
|
|
18
|
-
const slice = buf.subarray(start, start + length);
|
|
19
|
-
const end = slice.indexOf(0);
|
|
20
|
-
return slice
|
|
21
|
-
.subarray(0, end === -1 ? slice.length : end)
|
|
22
|
-
.toString("utf8")
|
|
23
|
-
.trim();
|
|
24
|
-
}
|
|
25
|
-
function readOctal(buf, start, length) {
|
|
26
|
-
const text = readCString(buf, start, length);
|
|
27
|
-
if (!text)
|
|
28
|
-
return 0;
|
|
29
|
-
const value = Number.parseInt(text, 8);
|
|
30
|
-
return Number.isFinite(value) ? value : 0;
|
|
31
|
-
}
|
|
32
|
-
/** Apply a pax extended header's records to the next entry. */
|
|
33
|
-
function paxOverrides(data) {
|
|
34
|
-
const out = {};
|
|
35
|
-
let offset = 0;
|
|
36
|
-
while (offset < data.length) {
|
|
37
|
-
const space = data.indexOf(0x20, offset);
|
|
38
|
-
if (space === -1)
|
|
39
|
-
break;
|
|
40
|
-
const length = Number.parseInt(data.toString("utf8", offset, space), 10);
|
|
41
|
-
if (!Number.isFinite(length) || length <= 0)
|
|
42
|
-
break;
|
|
43
|
-
const record = data.toString("utf8", space + 1, offset + length - 1);
|
|
44
|
-
const eq = record.indexOf("=");
|
|
45
|
-
if (eq > 0)
|
|
46
|
-
out[record.slice(0, eq)] = record.slice(eq + 1);
|
|
47
|
-
offset += length;
|
|
48
|
-
}
|
|
49
|
-
return out;
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Parse a gzipped tar. Returns entries in archive order.
|
|
53
|
-
*
|
|
54
|
-
* The leading directory component is dropped, so paths are relative to the
|
|
55
|
-
* repository root. GitHub names that component `<repo>-<sha>`, which would
|
|
56
|
-
* otherwise have to be guessed by every caller.
|
|
57
|
-
*/
|
|
58
|
-
export function parseTarGz(archive) {
|
|
59
|
-
const tar = gunzipSync(archive);
|
|
60
|
-
const entries = [];
|
|
61
|
-
let offset = 0;
|
|
62
|
-
let pendingName;
|
|
63
|
-
let pendingPax = {};
|
|
64
|
-
while (offset + BLOCK <= tar.length) {
|
|
65
|
-
const header = tar.subarray(offset, offset + BLOCK);
|
|
66
|
-
// Two zero blocks end the archive; a single one is enough to stop reading.
|
|
67
|
-
if (header.every((b) => b === 0))
|
|
68
|
-
break;
|
|
69
|
-
const name = readCString(header, 0, 100);
|
|
70
|
-
const prefix = readCString(header, 345, 155);
|
|
71
|
-
const size = readOctal(header, 124, 12);
|
|
72
|
-
const mode = readOctal(header, 100, 8);
|
|
73
|
-
const typeFlag = String.fromCharCode(header[156] ?? 0x30);
|
|
74
|
-
const linkName = readCString(header, 157, 100);
|
|
75
|
-
const data = size > 0 ? tar.subarray(offset + BLOCK, offset + BLOCK + size) : Buffer.alloc(0);
|
|
76
|
-
offset += BLOCK + Math.ceil(size / BLOCK) * BLOCK;
|
|
77
|
-
if (typeFlag === "x" || typeFlag === "g") {
|
|
78
|
-
pendingPax = paxOverrides(data);
|
|
79
|
-
continue;
|
|
80
|
-
}
|
|
81
|
-
if (typeFlag === "L") {
|
|
82
|
-
pendingName = data.toString("utf8").replace(/\0.*$/s, "").trim();
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
let full = pendingPax.path ?? pendingName ?? (prefix ? `${prefix}/${name}` : name);
|
|
86
|
-
pendingName = undefined;
|
|
87
|
-
pendingPax = {};
|
|
88
|
-
// Drop the archive's own root component: `<repo>-<sha>/…` → `…`
|
|
89
|
-
const slash = full.indexOf("/");
|
|
90
|
-
if (slash === -1)
|
|
91
|
-
continue;
|
|
92
|
-
full = full.slice(slash + 1);
|
|
93
|
-
// Tar marks a directory with a trailing slash. Normalising here means every
|
|
94
|
-
// consumer sees one spelling of a path: without this, a directory entry and a
|
|
95
|
-
// path comparison disagree (`reference/` vs `reference`), and `subtreeEntries`
|
|
96
|
-
// prefix matching has to guess which form it was given.
|
|
97
|
-
full = full.replace(/\/+$/, "");
|
|
98
|
-
if (!full)
|
|
99
|
-
continue;
|
|
100
|
-
const kind = typeFlag === "5" ? "directory" : typeFlag === "2" ? "symlink" : typeFlag === "0" || typeFlag === "\0" || typeFlag === "" ? "file" : "other";
|
|
101
|
-
entries.push({
|
|
102
|
-
path: full,
|
|
103
|
-
kind,
|
|
104
|
-
size,
|
|
105
|
-
mode: mode & 0o777,
|
|
106
|
-
...(kind === "file" ? { data: Buffer.from(data) } : {}),
|
|
107
|
-
...(kind === "symlink" ? { link: linkName } : {}),
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
return entries;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* True when a path from an archive may be written under a destination.
|
|
114
|
-
*
|
|
115
|
-
* A tarball is attacker-controlled once a preset points at someone else's
|
|
116
|
-
* repository, and `../../.ssh/authorized_keys` is a valid tar path. Absolute
|
|
117
|
-
* paths, any `..` component, and drive letters are all refused.
|
|
118
|
-
*/
|
|
119
|
-
export function isSafeRelativePath(path) {
|
|
120
|
-
if (!path || path.startsWith("/") || path.startsWith("\\"))
|
|
121
|
-
return false;
|
|
122
|
-
if (/^[a-zA-Z]:/.test(path))
|
|
123
|
-
return false;
|
|
124
|
-
// A trailing slash marks a directory entry in tar; it is not an empty
|
|
125
|
-
// component. Dropping it is what makes `reference/` (which every archive
|
|
126
|
-
// contains) pass, while `a//b` still fails below.
|
|
127
|
-
const trimmed = path.replace(/\/+$/, "");
|
|
128
|
-
if (!trimmed)
|
|
129
|
-
return false;
|
|
130
|
-
const parts = trimmed.split("/");
|
|
131
|
-
return parts.every((part) => part !== "" && part !== "." && part !== "..");
|
|
132
|
-
}
|
|
133
|
-
/** Every entry under `subtree` (a repo-relative directory), with the prefix removed. */
|
|
134
|
-
export function subtreeEntries(entries, subtree) {
|
|
135
|
-
const prefix = `${subtree.replace(/\/+$/, "")}/`;
|
|
136
|
-
const out = [];
|
|
137
|
-
for (const entry of entries) {
|
|
138
|
-
if (!entry.path.startsWith(prefix))
|
|
139
|
-
continue;
|
|
140
|
-
const path = entry.path.slice(prefix.length);
|
|
141
|
-
if (!path)
|
|
142
|
-
continue;
|
|
143
|
-
out.push({ ...entry, path });
|
|
144
|
-
}
|
|
145
|
-
return out;
|
|
146
|
-
}
|