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/collapse.js
DELETED
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Collapse a run of consecutive read/search calls into one summary line.
|
|
3
|
-
*
|
|
4
|
-
* The shape is Claude Code's (`src/utils/collapseReadSearch.ts`): consecutive
|
|
5
|
-
* Read/Grep/Glob calls become `Read 3 files, searched for 2 patterns`, and the
|
|
6
|
-
* run breaks when anything else appears — assistant text, an edit, a shell
|
|
7
|
-
* command that is not a search. A model exploring a repository issues those
|
|
8
|
-
* calls in bursts, and a feed that lists each one buries the two lines that
|
|
9
|
-
* matter under twenty that do not.
|
|
10
|
-
*
|
|
11
|
-
* Three details are taken from the reference rather than invented:
|
|
12
|
-
*
|
|
13
|
-
* 1. **Plural per category, not per group.** `1 file` / `2 files` is decided
|
|
14
|
-
* next to the count it belongs to, because a group can hold one read and
|
|
15
|
-
* three greps and the two need different words.
|
|
16
|
-
* 2. **Active and finished read differently.** `Reading 2 files` while the run
|
|
17
|
-
* is live, `Read 2 files` once it settles. That is the same information the
|
|
18
|
-
* per-call spinner carries, one level up.
|
|
19
|
-
* 3. **The break is on assistant text, not on a message boundary.** Two calls
|
|
20
|
-
* from the same assistant turn that sit either side of a paragraph are two
|
|
21
|
-
* thoughts, and merging them would claim a grouping the model did not make.
|
|
22
|
-
*
|
|
23
|
-
* Pure over a list of calls: no clock, no session, no rendering. What it
|
|
24
|
-
* decides is which calls belong together and what the line says; whether that
|
|
25
|
-
* line is drawn is the feed's business.
|
|
26
|
-
*/
|
|
27
|
-
/** The categories the summary counts, one word pair each. */
|
|
28
|
-
const CATEGORIES = {
|
|
29
|
-
read: { active: "Reading", done: "Read", one: "file", many: "files" },
|
|
30
|
-
search: { active: "Searching for", done: "Searched for", one: "pattern", many: "patterns" },
|
|
31
|
-
list: { active: "Listing", done: "Listed", one: "directory", many: "directories" },
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Which category a tool belongs to, or `undefined` when it does not collapse.
|
|
35
|
-
*
|
|
36
|
-
* `read` reads a file; `grep` and `find` search; `ls` lists. Everything else
|
|
37
|
-
* breaks the run — `edit` and `write` change the tree, and a `bash` call is not
|
|
38
|
-
* classified here because this repository's bash tool is a general shell, not
|
|
39
|
-
* the search/read primitive Claude Code splits its own out into.
|
|
40
|
-
*/
|
|
41
|
-
function categoryOf(toolName) {
|
|
42
|
-
if (toolName === "read")
|
|
43
|
-
return "read";
|
|
44
|
-
if (toolName === "grep" || toolName === "find" || toolName === "glob")
|
|
45
|
-
return "search";
|
|
46
|
-
if (toolName === "ls")
|
|
47
|
-
return "list";
|
|
48
|
-
return undefined;
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Find every run worth collapsing, in order.
|
|
52
|
-
*
|
|
53
|
-
* A run of one is NOT a group: `Read(src/ui.ts)` says more than `Read 1 file`,
|
|
54
|
-
* and replacing a single call with a summary loses the path for no gain. That
|
|
55
|
-
* threshold is the whole reason this returns a list of runs rather than a
|
|
56
|
-
* rewritten transcript.
|
|
57
|
-
*/
|
|
58
|
-
export function collapseGroups(calls, live = false) {
|
|
59
|
-
const groups = [];
|
|
60
|
-
let run;
|
|
61
|
-
const flush = () => {
|
|
62
|
-
if (!run || run.indices.length < 2) {
|
|
63
|
-
run = undefined;
|
|
64
|
-
return;
|
|
65
|
-
}
|
|
66
|
-
groups.push({
|
|
67
|
-
start: run.indices[0],
|
|
68
|
-
indices: run.indices,
|
|
69
|
-
counts: run.counts,
|
|
70
|
-
summary: summaryOf(run.counts, live),
|
|
71
|
-
});
|
|
72
|
-
run = undefined;
|
|
73
|
-
};
|
|
74
|
-
for (const [index, call] of calls.entries()) {
|
|
75
|
-
const category = categoryOf(call.toolName);
|
|
76
|
-
// Anything that is not a read/search/list BREAKS the run, which is the
|
|
77
|
-
// reference's rule: an edit, a shell command or a paragraph of assistant
|
|
78
|
-
// text sits between two explorations and says they were separate.
|
|
79
|
-
if (category === undefined) {
|
|
80
|
-
flush();
|
|
81
|
-
continue;
|
|
82
|
-
}
|
|
83
|
-
if (!run)
|
|
84
|
-
run = { indices: [], counts: {} };
|
|
85
|
-
run.indices.push(index);
|
|
86
|
-
run.counts[category] = (run.counts[category] ?? 0) + 1;
|
|
87
|
-
}
|
|
88
|
-
flush();
|
|
89
|
-
return groups;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* `Reading 2 files, searching for 3 patterns`.
|
|
93
|
-
*
|
|
94
|
-
* Only the first clause is capitalised, which is how the reference builds it:
|
|
95
|
-
* the verb is chosen by whether it is the first part being pushed, so the line
|
|
96
|
-
* reads as one sentence rather than a list of headings.
|
|
97
|
-
*/
|
|
98
|
-
/**
|
|
99
|
-
* The tense is a parameter rather than an options object: there is one call
|
|
100
|
-
* site and it forwards the same flag the group was built with, so a named bag
|
|
101
|
-
* would add a layer without adding a distinction. The gate flags the boolean
|
|
102
|
-
* anyway, and it is in the baseline - the rule is right about the general case
|
|
103
|
-
* and this is the case it does not fit.
|
|
104
|
-
*/
|
|
105
|
-
function summaryOf(counts, live) {
|
|
106
|
-
const parts = [];
|
|
107
|
-
// Fixed clause order, taken from the reference (search, read, list, repl):
|
|
108
|
-
// the summary reads the same for the same work whatever order the model
|
|
109
|
-
// happened to issue it in, which is the point of a summary line.
|
|
110
|
-
for (const category of ["search", "read", "list"]) {
|
|
111
|
-
const count = counts[category];
|
|
112
|
-
if (count === undefined)
|
|
113
|
-
continue;
|
|
114
|
-
const spec = CATEGORIES[category];
|
|
115
|
-
const verb = live ? spec.active : spec.done;
|
|
116
|
-
const word = count === 1 ? spec.one : spec.many;
|
|
117
|
-
parts.push(`${parts.length === 0 ? verb : verb.toLowerCase()} ${count} ${word}`);
|
|
118
|
-
}
|
|
119
|
-
return parts.join(", ");
|
|
120
|
-
}
|
|
121
|
-
// `collapseSummary` was exported here and used by nothing: `collapseGroups`
|
|
122
|
-
// already puts the finished line in `CollapseGroup.summary`, which is where
|
|
123
|
-
// every caller reads it from. `check-wiring.mjs` caught the dead export, and the
|
|
124
|
-
// fix is to delete it rather than to add a caller that exists to justify it.
|
|
125
|
-
//
|
|
126
|
-
// The count is spelled out rather than shown as `×3` because the categories
|
|
127
|
-
// differ: `Read ×3` is fine for one kind of call and meaningless for a mixed
|
|
128
|
-
// run, and a summary that only works for the easy case is one that has to be
|
|
129
|
-
// special-cased later.
|
package/dist/commands.js
DELETED
|
@@ -1,301 +0,0 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, unlinkSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { AGENT_DIR } from "./paths.js";
|
|
5
|
-
import { APP_TITLE } from "./brand.js";
|
|
6
|
-
import { reportError } from "./errors.js";
|
|
7
|
-
import { detectSandbox } from "./sandbox.js";
|
|
8
|
-
const MIN_NODE = [22, 19];
|
|
9
|
-
/** Everything the CLI needs from the machine, checked without a model call. */
|
|
10
|
-
export async function collectChecks(gatewayUrl = "https://api.howtofix.id/api/status") {
|
|
11
|
-
const checks = [];
|
|
12
|
-
const [major, minor] = process.versions.node.split(".").map(Number);
|
|
13
|
-
const nodeOk = major > MIN_NODE[0] || (major === MIN_NODE[0] && minor >= MIN_NODE[1]);
|
|
14
|
-
checks.push({
|
|
15
|
-
name: "node",
|
|
16
|
-
ok: nodeOk,
|
|
17
|
-
detail: `${process.versions.node} (needs >= ${MIN_NODE[0]}.${MIN_NODE[1]})`,
|
|
18
|
-
});
|
|
19
|
-
const git = spawnSync("git", ["--version"], { encoding: "utf8" });
|
|
20
|
-
checks.push({
|
|
21
|
-
name: "git",
|
|
22
|
-
ok: git.status === 0,
|
|
23
|
-
detail: git.status === 0 ? git.stdout.trim() : "not found on PATH",
|
|
24
|
-
});
|
|
25
|
-
const sandbox = detectSandbox();
|
|
26
|
-
checks.push({
|
|
27
|
-
name: "sandbox",
|
|
28
|
-
ok: Boolean(sandbox),
|
|
29
|
-
detail: sandbox ?? "no working bubblewrap (Linux) or sandbox-exec (macOS); --sandbox unavailable",
|
|
30
|
-
optional: true,
|
|
31
|
-
});
|
|
32
|
-
let stateOk = false;
|
|
33
|
-
let stateDetail = AGENT_DIR;
|
|
34
|
-
try {
|
|
35
|
-
const probe = join(AGENT_DIR, `.doctor-${process.pid}`);
|
|
36
|
-
writeFileSync(probe, "ok");
|
|
37
|
-
unlinkSync(probe);
|
|
38
|
-
stateOk = true;
|
|
39
|
-
}
|
|
40
|
-
catch (error) {
|
|
41
|
-
stateDetail = `${AGENT_DIR} is not writable: ${error instanceof Error ? error.message : String(error)}`;
|
|
42
|
-
}
|
|
43
|
-
checks.push({ name: "state dir", ok: stateOk, detail: stateDetail });
|
|
44
|
-
const authPath = join(AGENT_DIR, "auth.json");
|
|
45
|
-
let authDetail = "no auth.json — run `howtofix` and sign in";
|
|
46
|
-
let authOk = false;
|
|
47
|
-
try {
|
|
48
|
-
const parsed = JSON.parse(readFileSync(authPath, "utf8"));
|
|
49
|
-
const providers = Object.keys(parsed).length;
|
|
50
|
-
authOk = providers > 0;
|
|
51
|
-
authDetail = authOk ? `${providers} provider credential${providers === 1 ? "" : "s"}` : authDetail;
|
|
52
|
-
}
|
|
53
|
-
catch {
|
|
54
|
-
// Missing or unreadable: the sentence above already says what to do.
|
|
55
|
-
}
|
|
56
|
-
checks.push({ name: "auth", ok: authOk, detail: authDetail, optional: true });
|
|
57
|
-
try {
|
|
58
|
-
const started = Date.now();
|
|
59
|
-
const response = await fetch(gatewayUrl, { signal: AbortSignal.timeout(5000) });
|
|
60
|
-
checks.push({
|
|
61
|
-
name: "gateway",
|
|
62
|
-
ok: response.ok,
|
|
63
|
-
detail: `${gatewayUrl} → ${response.status} in ${Date.now() - started}ms`,
|
|
64
|
-
optional: true,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
catch (error) {
|
|
68
|
-
checks.push({
|
|
69
|
-
name: "gateway",
|
|
70
|
-
ok: false,
|
|
71
|
-
detail: `${gatewayUrl} unreachable: ${error instanceof Error ? error.message : String(error)}`,
|
|
72
|
-
optional: true,
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
return checks;
|
|
76
|
-
}
|
|
77
|
-
export async function runDoctor() {
|
|
78
|
-
const checks = await collectChecks();
|
|
79
|
-
const width = Math.max(...checks.map((check) => check.name.length));
|
|
80
|
-
for (const check of checks) {
|
|
81
|
-
const mark = check.ok ? "ok " : check.optional ? "warn" : "FAIL";
|
|
82
|
-
console.log(` ${mark} ${check.name.padEnd(width)} ${check.detail}`);
|
|
83
|
-
}
|
|
84
|
-
const failed = checks.filter((check) => !check.ok && !check.optional);
|
|
85
|
-
if (failed.length > 0) {
|
|
86
|
-
console.log(`\n${failed.length} problem${failed.length === 1 ? "" : "s"} must be fixed before a session will work.`);
|
|
87
|
-
return 1;
|
|
88
|
-
}
|
|
89
|
-
return 0;
|
|
90
|
-
}
|
|
91
|
-
/** Configured providers and their models, straight from models.json. */
|
|
92
|
-
export function runModels(root = AGENT_DIR) {
|
|
93
|
-
const path = join(root, "models.json");
|
|
94
|
-
let raw;
|
|
95
|
-
try {
|
|
96
|
-
raw = readFileSync(path, "utf8");
|
|
97
|
-
}
|
|
98
|
-
catch (error) {
|
|
99
|
-
const code = error.code;
|
|
100
|
-
if (code === "ENOENT") {
|
|
101
|
-
console.log(` no models.json at ${path} — run \`howtofix /connect\` to pull the catalogue`);
|
|
102
|
-
}
|
|
103
|
-
else {
|
|
104
|
-
reportError(APP_TITLE, "models", error);
|
|
105
|
-
}
|
|
106
|
-
return 1;
|
|
107
|
-
}
|
|
108
|
-
let parsed;
|
|
109
|
-
try {
|
|
110
|
-
parsed = JSON.parse(raw);
|
|
111
|
-
}
|
|
112
|
-
catch (error) {
|
|
113
|
-
// A file that exists but does not parse is a different problem from a
|
|
114
|
-
// missing one: /connect will not fix it, and saying "no models.json" sends
|
|
115
|
-
// the user looking for a file that is already there.
|
|
116
|
-
console.error(`${APP_TITLE}: models: ${path} is not valid JSON (${error instanceof Error ? error.message : String(error)})`);
|
|
117
|
-
console.error(" fix the file, or delete it and run `howtofix /connect` to write a new one");
|
|
118
|
-
return 1;
|
|
119
|
-
}
|
|
120
|
-
const providers = parsed.providers ?? {};
|
|
121
|
-
let total = 0;
|
|
122
|
-
for (const [name, provider] of Object.entries(providers)) {
|
|
123
|
-
const ids = (provider.models ?? []).map((model) => model.id).filter(Boolean);
|
|
124
|
-
total += ids.length;
|
|
125
|
-
console.log(` ${name} (${ids.length})`);
|
|
126
|
-
for (const id of ids)
|
|
127
|
-
console.log(` ${id}`);
|
|
128
|
-
}
|
|
129
|
-
if (total === 0) {
|
|
130
|
-
console.log(" no models configured");
|
|
131
|
-
return 1;
|
|
132
|
-
}
|
|
133
|
-
return 0;
|
|
134
|
-
}
|
|
135
|
-
/** Saved sessions, newest first. */
|
|
136
|
-
export function listSessionFiles(limit = 15, root = AGENT_DIR) {
|
|
137
|
-
const sessionsRoot = join(root, "sessions");
|
|
138
|
-
let projects;
|
|
139
|
-
try {
|
|
140
|
-
projects = readdirSync(sessionsRoot);
|
|
141
|
-
}
|
|
142
|
-
catch {
|
|
143
|
-
return [];
|
|
144
|
-
}
|
|
145
|
-
const rows = [];
|
|
146
|
-
for (const project of projects) {
|
|
147
|
-
const dir = join(sessionsRoot, project);
|
|
148
|
-
let files;
|
|
149
|
-
try {
|
|
150
|
-
files = readdirSync(dir);
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
for (const file of files) {
|
|
156
|
-
if (!file.endsWith(".jsonl"))
|
|
157
|
-
continue;
|
|
158
|
-
const full = join(dir, file);
|
|
159
|
-
try {
|
|
160
|
-
const info = statSync(full);
|
|
161
|
-
rows.push({ file: full, project, modified: info.mtimeMs, bytes: info.size });
|
|
162
|
-
}
|
|
163
|
-
catch {
|
|
164
|
-
// A session removed mid-walk is not an error worth reporting.
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return rows.sort((a, b) => b.modified - a.modified).slice(0, limit);
|
|
169
|
-
}
|
|
170
|
-
export function runSessions(argv) {
|
|
171
|
-
const searchIndex = argv.indexOf("--search");
|
|
172
|
-
if (searchIndex >= 0) {
|
|
173
|
-
const query = argv[searchIndex + 1]?.trim();
|
|
174
|
-
if (!query) {
|
|
175
|
-
console.error(`${APP_TITLE}: sessions: --search needs a word or phrase`);
|
|
176
|
-
return 2;
|
|
177
|
-
}
|
|
178
|
-
const hits = searchSessions(query);
|
|
179
|
-
if (hits.length === 0) {
|
|
180
|
-
console.log(` no session mentions "${query}"`);
|
|
181
|
-
return 0;
|
|
182
|
-
}
|
|
183
|
-
for (const hit of hits) {
|
|
184
|
-
const when = new Date(hit.row.modified).toISOString().replace("T", " ").slice(0, 16);
|
|
185
|
-
console.log(` ${when} ${hit.row.project} (${hit.matches} hit${hit.matches === 1 ? "" : "s"})`);
|
|
186
|
-
console.log(` ${hit.row.file}`);
|
|
187
|
-
console.log(` ${hit.snippet}`);
|
|
188
|
-
}
|
|
189
|
-
return 0;
|
|
190
|
-
}
|
|
191
|
-
const limitIndex = argv.indexOf("--limit");
|
|
192
|
-
const limit = limitIndex >= 0 ? Number(argv[limitIndex + 1]) || 15 : 15;
|
|
193
|
-
const rows = listSessionFiles(limit);
|
|
194
|
-
if (rows.length === 0) {
|
|
195
|
-
console.log(` no saved sessions under ${join(AGENT_DIR, "sessions")}`);
|
|
196
|
-
return 0;
|
|
197
|
-
}
|
|
198
|
-
const exportIndex = argv.indexOf("--export");
|
|
199
|
-
if (exportIndex >= 0) {
|
|
200
|
-
const which = Number(argv[exportIndex + 1]);
|
|
201
|
-
if (!Number.isInteger(which) || which < 1 || which > rows.length) {
|
|
202
|
-
console.error(`${APP_TITLE}: sessions: --export needs a number from the list (1-${rows.length})`);
|
|
203
|
-
return 2;
|
|
204
|
-
}
|
|
205
|
-
const row = rows[which - 1];
|
|
206
|
-
try {
|
|
207
|
-
console.log(exportSessionMarkdown(row.file));
|
|
208
|
-
}
|
|
209
|
-
catch (error) {
|
|
210
|
-
reportError(APP_TITLE, "sessions", error);
|
|
211
|
-
return 1;
|
|
212
|
-
}
|
|
213
|
-
return 0;
|
|
214
|
-
}
|
|
215
|
-
rows.forEach((row, index) => {
|
|
216
|
-
const when = new Date(row.modified).toISOString().replace("T", " ").slice(0, 16);
|
|
217
|
-
const kb = Math.round(row.bytes / 1024);
|
|
218
|
-
console.log(` ${String(index + 1).padStart(2)} ${when} ${String(kb).padStart(6)}k ${row.project}`);
|
|
219
|
-
console.log(` ${row.file}`);
|
|
220
|
-
});
|
|
221
|
-
return 0;
|
|
222
|
-
}
|
|
223
|
-
/** Text of one JSONL line, whether its content is a string or content blocks. */
|
|
224
|
-
function lineText(line) {
|
|
225
|
-
try {
|
|
226
|
-
const entry = JSON.parse(line);
|
|
227
|
-
const content = entry.message?.content;
|
|
228
|
-
if (typeof content === "string")
|
|
229
|
-
return content.replace(/\s+/g, " ").trim();
|
|
230
|
-
if (Array.isArray(content)) {
|
|
231
|
-
return content
|
|
232
|
-
.map((block) => (block && typeof block === "object" && "text" in block ? String(block.text) : ""))
|
|
233
|
-
.join(" ")
|
|
234
|
-
.replace(/\s+/g, " ")
|
|
235
|
-
.trim();
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
catch {
|
|
239
|
-
// A line that does not parse is still searchable as raw text.
|
|
240
|
-
}
|
|
241
|
-
return line.replace(/\s+/g, " ").trim();
|
|
242
|
-
}
|
|
243
|
-
/** Sessions whose text mentions `query`, with the first matching line as evidence. */
|
|
244
|
-
export function searchSessions(query, limit = 10, root = AGENT_DIR) {
|
|
245
|
-
const needle = query.toLowerCase();
|
|
246
|
-
const hits = [];
|
|
247
|
-
for (const row of listSessionFiles(500, root)) {
|
|
248
|
-
let text;
|
|
249
|
-
try {
|
|
250
|
-
text = readFileSync(row.file, "utf8");
|
|
251
|
-
}
|
|
252
|
-
catch {
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
let matches = 0;
|
|
256
|
-
let snippet = "";
|
|
257
|
-
for (const line of text.split("\n")) {
|
|
258
|
-
if (!line.toLowerCase().includes(needle))
|
|
259
|
-
continue;
|
|
260
|
-
matches++;
|
|
261
|
-
if (!snippet)
|
|
262
|
-
snippet = lineText(line).slice(0, 160);
|
|
263
|
-
}
|
|
264
|
-
if (matches > 0)
|
|
265
|
-
hits.push({ row, matches, snippet });
|
|
266
|
-
if (hits.length >= limit)
|
|
267
|
-
break;
|
|
268
|
-
}
|
|
269
|
-
return hits;
|
|
270
|
-
}
|
|
271
|
-
/** A session as markdown, for pasting into an issue or a chat. */
|
|
272
|
-
export function exportSessionMarkdown(file) {
|
|
273
|
-
const out = [`# Session ${file.split(/[\\/]/).pop() ?? file}`, ""];
|
|
274
|
-
for (const line of readFileSync(file, "utf8").split("\n")) {
|
|
275
|
-
if (!line.trim())
|
|
276
|
-
continue;
|
|
277
|
-
let entry;
|
|
278
|
-
try {
|
|
279
|
-
entry = JSON.parse(line);
|
|
280
|
-
}
|
|
281
|
-
catch {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
if (entry.type !== "message")
|
|
285
|
-
continue;
|
|
286
|
-
const text = lineText(line);
|
|
287
|
-
if (!text)
|
|
288
|
-
continue;
|
|
289
|
-
out.push(`## ${entry.message?.role ?? "unknown"}`, "", text, "");
|
|
290
|
-
}
|
|
291
|
-
return out.join("\n").trimEnd();
|
|
292
|
-
}
|
|
293
|
-
/** True when the file exists and is a directory — used by doctor's callers. */
|
|
294
|
-
export function stateDirExists() {
|
|
295
|
-
try {
|
|
296
|
-
return existsSync(AGENT_DIR) && statSync(AGENT_DIR).isDirectory();
|
|
297
|
-
}
|
|
298
|
-
catch {
|
|
299
|
-
return false;
|
|
300
|
-
}
|
|
301
|
-
}
|
package/dist/compaction-guard.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Consecutive failures before the breaker trips.
|
|
3
|
-
*
|
|
4
|
-
* Same value as the reference. Three is enough to distinguish a broken
|
|
5
|
-
* compaction from a transient API error, and small enough that the wasted calls
|
|
6
|
-
* stay negligible.
|
|
7
|
-
*/
|
|
8
|
-
export const MAX_CONSECUTIVE_FAILURES = 3;
|
|
9
|
-
export function emptyGuardState() {
|
|
10
|
-
return { consecutiveFailures: 0, tripped: false };
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Whether a compaction should be refused.
|
|
14
|
-
*
|
|
15
|
-
* Pure, so the policy is testable without a session: manual compaction is always
|
|
16
|
-
* allowed, and an untripped breaker allows everything.
|
|
17
|
-
*/
|
|
18
|
-
export function shouldSkipCompaction(state, reason) {
|
|
19
|
-
if (reason === "manual")
|
|
20
|
-
return false;
|
|
21
|
-
return state.consecutiveFailures >= MAX_CONSECUTIVE_FAILURES;
|
|
22
|
-
}
|
|
23
|
-
/** Fold one failure into the state. Aborts are not failures. */
|
|
24
|
-
export function noteFailure(state, aborted) {
|
|
25
|
-
if (aborted)
|
|
26
|
-
return state;
|
|
27
|
-
const consecutiveFailures = state.consecutiveFailures + 1;
|
|
28
|
-
return {
|
|
29
|
-
consecutiveFailures,
|
|
30
|
-
tripped: state.tripped || consecutiveFailures >= MAX_CONSECUTIVE_FAILURES,
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
/** Fold one success into the state: the compaction worked, so the count resets. */
|
|
34
|
-
export function noteSuccess() {
|
|
35
|
-
return emptyGuardState();
|
|
36
|
-
}
|
|
37
|
-
/** The message the user sees once, when the breaker trips. */
|
|
38
|
-
export function breakerMessage(state) {
|
|
39
|
-
return [
|
|
40
|
-
`Auto-compaction failed ${state.consecutiveFailures} times in a row, so it is switched off for this session.`,
|
|
41
|
-
"The context is over the limit and compaction cannot recover it. Start a new session, or switch to a model with a larger context window.",
|
|
42
|
-
"Manual /compact is still allowed.",
|
|
43
|
-
].join(" ");
|
|
44
|
-
}
|
|
45
|
-
let state = emptyGuardState();
|
|
46
|
-
export function compactionGuardState() {
|
|
47
|
-
return state;
|
|
48
|
-
}
|
|
49
|
-
/** Test seam: forget the counts. */
|
|
50
|
-
export function resetCompactionGuard() {
|
|
51
|
-
state = emptyGuardState();
|
|
52
|
-
}
|
|
53
|
-
/**
|
|
54
|
-
* Wire the breaker into the session.
|
|
55
|
-
*
|
|
56
|
-
* The refusal happens in `session_before_compact`, which the SDK consults before
|
|
57
|
-
* it runs the summariser — so a refused attempt costs no API call at all.
|
|
58
|
-
*/
|
|
59
|
-
export function createCompactionGuardExtension() {
|
|
60
|
-
return (hxf) => {
|
|
61
|
-
hxf.on("session_start", async () => {
|
|
62
|
-
state = emptyGuardState();
|
|
63
|
-
});
|
|
64
|
-
hxf.on("session_compact", async () => {
|
|
65
|
-
state = noteSuccess();
|
|
66
|
-
});
|
|
67
|
-
hxf.on("session_compact_failed", async (event, ctx) => {
|
|
68
|
-
const before = state;
|
|
69
|
-
state = noteFailure(state, event.aborted);
|
|
70
|
-
// Report the trip once, when it happens, rather than on every later turn.
|
|
71
|
-
if (!before.tripped && state.tripped && ctx?.ui) {
|
|
72
|
-
ctx.ui.notify(breakerMessage(state), "warning");
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
hxf.on("session_before_compact", async (event) => {
|
|
76
|
-
if (!shouldSkipCompaction(state, event.reason))
|
|
77
|
-
return undefined;
|
|
78
|
-
return { cancel: true };
|
|
79
|
-
});
|
|
80
|
-
};
|
|
81
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { readMessageText, writeMessageText } from "./prompt-text.js";
|
|
2
|
-
import { renderNotes } from "./notes.js";
|
|
3
|
-
export function createContextExtension() {
|
|
4
|
-
return (hxf) => {
|
|
5
|
-
hxf.on("context_with_system", async (event) => {
|
|
6
|
-
const notes = renderNotes();
|
|
7
|
-
if (!notes)
|
|
8
|
-
return undefined;
|
|
9
|
-
const head = event.messages[0];
|
|
10
|
-
if (!head || head.role !== "system")
|
|
11
|
-
return undefined;
|
|
12
|
-
const combined = `${readMessageText(head.content)}\n\n${notes}`;
|
|
13
|
-
const messages = event.messages.map((message) => (message === head ? { ...head, content: writeMessageText(head.content, combined) } : message));
|
|
14
|
-
return { messages };
|
|
15
|
-
});
|
|
16
|
-
};
|
|
17
|
-
}
|