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/session.js
DELETED
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
import { join } from "node:path";
|
|
2
|
-
import { hostname } from "node:os";
|
|
3
|
-
import { createAgentSession, createCodemodeExtension, createMcpExtension, DefaultResourceLoader, SessionManager, SettingsManager } from "@netrodev/hxf-coding-agent";
|
|
4
|
-
import { APP_VERSION, SYSTEM_APPEND, scrubHarnessMessage } from "./brand.js";
|
|
5
|
-
import { promptAppendBlocks } from "./prompt-files.js";
|
|
6
|
-
import { createBrandExtension } from "./brand-extension.js";
|
|
7
|
-
import { createHooksExtension } from "./hooks-extension.js";
|
|
8
|
-
import { AGENT_DIR, additionalSkillPaths } from "./paths.js";
|
|
9
|
-
import { readCliCredential, renewCliCredentialIfNeeded } from "./cli-credentials.js";
|
|
10
|
-
import { createPermissionExtension } from "./permissions.js";
|
|
11
|
-
import { loadMcpConfiguration } from "./mcp-config.js";
|
|
12
|
-
/** Session entry type the running build stamps itself under. */
|
|
13
|
-
export const BUILD_ENTRY = "howtofix.build";
|
|
14
|
-
import { createRtkExtension } from "./rtk.js";
|
|
15
|
-
import { readWorkbenchSettings } from "./settings.js";
|
|
16
|
-
import { askTool, claimTool, sessionsTool, setClaimsContext, thinkTool } from "./tools.js";
|
|
17
|
-
import { repoMapTool, setRepoRoot } from "./repo-map.js";
|
|
18
|
-
import { SESSIONS_DIR_NAME, PRESENCE_TTL_MS, announce, withdraw } from "./sessions.js";
|
|
19
|
-
import { todoTool } from "./todo.js";
|
|
20
|
-
import { contextTool } from "./notes.js";
|
|
21
|
-
import { goalTool } from "./goal.js";
|
|
22
|
-
import { createPlanGateExtension, createPlanPromptExtension, enterPlanModeTool, exitPlanModeTool } from "./plan-mode.js";
|
|
23
|
-
import { createCompactionGuardExtension } from "./compaction-guard.js";
|
|
24
|
-
import { createGoalExtension } from "./goal-extension.js";
|
|
25
|
-
import { createContextExtension } from "./context-extension.js";
|
|
26
|
-
import { createPresetExtension } from "./preset-extension.js";
|
|
27
|
-
import { createDcpExtension, createCompressTool, getDcpState, activeDcpSession, snapshotFor } from "./dcp/index.js";
|
|
28
|
-
import { createWorkflowTool } from "./workflow.js";
|
|
29
|
-
import { createTaskTool } from "./subagent.js";
|
|
30
|
-
import { loadAgentDefs } from "./agent-defs.js";
|
|
31
|
-
import { agents } from "./agents.js";
|
|
32
|
-
import { defaultModelFor, resolveModel } from "./model-resolve.js";
|
|
33
|
-
import { HOWTOFIX_PROVIDERS } from "./howtofix-account.js";
|
|
34
|
-
import { activateExtraBuiltins, settleExtensionTools } from "./tool-loadout.js";
|
|
35
|
-
import { webFetchTool, webSearchTool } from "./web-tools.js";
|
|
36
|
-
import { lspTool } from "./lsp-tools.js";
|
|
37
|
-
import { qualityTool } from "./quality-tool.js";
|
|
38
|
-
import { slopTool } from "./slop-tool.js";
|
|
39
|
-
import { disposeAllLsp } from "./lsp.js";
|
|
40
|
-
import { disposeAgentSession } from "./session-lifecycle.js";
|
|
41
|
-
import { sweepStaleSessions } from "./session-retention.js";
|
|
42
|
-
/**
|
|
43
|
-
* Every tool this product offers, as documentation of the surface.
|
|
44
|
-
*
|
|
45
|
-
* The loadout is NOT applied from here: it is the exclusion in `toolLoadout()`
|
|
46
|
-
* plus the activation in `tool-loadout.ts`. An allowlist was tried and removed —
|
|
47
|
-
* it applies to *every* registered tool including extension ones, so it discarded
|
|
48
|
-
* `mcp__<server>__<tool>` names that cannot be known in advance and undid the MCP
|
|
49
|
-
* extension's `codemode` activation. See the decisions log.
|
|
50
|
-
*
|
|
51
|
-
* It is deliberately not exported or referenced: if it ever becomes the thing
|
|
52
|
-
* that decides the loadout again, the MCP regressions come back. The active set is
|
|
53
|
-
* asserted by `scripts/check-tool-loadout.mjs`.
|
|
54
|
-
*/
|
|
55
|
-
export const MAIN_TOOLS = [
|
|
56
|
-
"read",
|
|
57
|
-
"bash",
|
|
58
|
-
"edit",
|
|
59
|
-
"write",
|
|
60
|
-
"grep",
|
|
61
|
-
"find",
|
|
62
|
-
"ls",
|
|
63
|
-
"bash_output",
|
|
64
|
-
"kill_shell",
|
|
65
|
-
"task",
|
|
66
|
-
"think",
|
|
67
|
-
"ask",
|
|
68
|
-
"claim",
|
|
69
|
-
"sessions",
|
|
70
|
-
"todo",
|
|
71
|
-
"context",
|
|
72
|
-
"goal",
|
|
73
|
-
"enter_plan_mode",
|
|
74
|
-
"exit_plan_mode",
|
|
75
|
-
"workflow",
|
|
76
|
-
"web_search",
|
|
77
|
-
"web_fetch",
|
|
78
|
-
"lsp",
|
|
79
|
-
"quality",
|
|
80
|
-
"compress",
|
|
81
|
-
"slop",
|
|
82
|
-
];
|
|
83
|
-
/**
|
|
84
|
-
* The tool loadout for this platform, expressed as an exclusion rather than an
|
|
85
|
-
* allowlist.
|
|
86
|
-
*
|
|
87
|
-
* `tools:` is an allowlist that filters *every* registered tool, extension ones
|
|
88
|
-
* included (`_refreshToolRegistry` → `_isAllowedTool`), so naming only our own
|
|
89
|
-
* tools silently drops any tool an extension registers later under a name we
|
|
90
|
-
* cannot know — `mcp__<server>__<tool>` above all. Measured: an MCP server
|
|
91
|
-
* connected, `/mcp` reported "14 tools", and the model could call none of them
|
|
92
|
-
* because the allowlist had already discarded every one.
|
|
93
|
-
*
|
|
94
|
-
* `codemode` was the same bug in miniature. The MCP extension activates it at
|
|
95
|
-
* `session_start` whenever a server's tools are only reachable from scripts (the
|
|
96
|
-
* default `exposure: "codemode"`), but the allowlist filter runs when the tool
|
|
97
|
-
* registry is rebuilt, after that activation, so a static list undid it — and the
|
|
98
|
-
* user got "MCP tools are only reachable from the codemode or tool_search tool,
|
|
99
|
-
* but neither is active" on a fully connected server.
|
|
100
|
-
*
|
|
101
|
-
* Without `tools:`, the SDK enables its default set plus everything extensions
|
|
102
|
-
* register, and activation sticks. The one piece of policy left — the SDK's
|
|
103
|
-
* `powershell` tool, which throws "only available on Windows" everywhere else,
|
|
104
|
-
* while `bash` resolves to Git Bash that Windows does not ship — becomes an
|
|
105
|
-
* exclusion.
|
|
106
|
-
*/
|
|
107
|
-
const toolLoadout = () => ({
|
|
108
|
-
exclude: process.platform === "win32" ? [] : ["powershell"],
|
|
109
|
-
});
|
|
110
|
-
/**
|
|
111
|
-
* Every way the CLI can start — the workbench, `--print`, `-c`, `--session-path`
|
|
112
|
-
* — reaches `openSession`, so the session-file retention sweep hangs here rather
|
|
113
|
-
* than in the workbench alone: a scripted run is exactly the thing that used to
|
|
114
|
-
* leave a transcript behind. Once per process, because a `/new` or `/resume`
|
|
115
|
-
* swap opens another session in the same process and the store does not need a
|
|
116
|
-
* second walk to know it is stale.
|
|
117
|
-
*/
|
|
118
|
-
let sessionsSwept = false;
|
|
119
|
-
export async function openSession(options) {
|
|
120
|
-
const settingsManager = SettingsManager.create(options.cwd, AGENT_DIR, { projectTrusted: true });
|
|
121
|
-
// Read once at open and refreshed by the slash command. The extension consults this
|
|
122
|
-
// variable per tool result, so a change applies immediately without reopening the
|
|
123
|
-
// session — which is what a toggle should do.
|
|
124
|
-
let rtkEnabled = readWorkbenchSettings(AGENT_DIR).compressToolOutput;
|
|
125
|
-
const sessionManager = options.sessionPath ? SessionManager.open(options.sessionPath) : options.noSession ? SessionManager.inMemory(options.cwd) : options.continueSession ? SessionManager.continueRecent(options.cwd) : SessionManager.create(options.cwd);
|
|
126
|
-
// A session written by a different build is a compatibility question the user
|
|
127
|
-
// should not have to guess at. The file header records the *format* version
|
|
128
|
-
// (the SDK's), not the product's, so a session from another release loads
|
|
129
|
-
// silently and any behaviour that changed between them reads as a bug. Read
|
|
130
|
-
// the stamp the previous build left and say when it differs.
|
|
131
|
-
// Read the stamp off the session that was opened, whatever route opened it:
|
|
132
|
-
// `--continue` and `/resume` reach the same file as `--session`, and keying
|
|
133
|
-
// this on `sessionPath` alone meant the warning never fired for the two ways
|
|
134
|
-
// a user actually returns to an old session.
|
|
135
|
-
const buildNote = (() => {
|
|
136
|
-
try {
|
|
137
|
-
const previous = sessionManager.getEntries().find((entry) => entry.type === "custom" && entry.customType === BUILD_ENTRY);
|
|
138
|
-
const was = previous?.data?.version;
|
|
139
|
-
if (!was || was === APP_VERSION)
|
|
140
|
-
return undefined;
|
|
141
|
-
return `this session was written by HowToFix ${was}; you are running ${APP_VERSION}`;
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
// An unreadable or older file is not a reason to refuse to open it.
|
|
145
|
-
return undefined;
|
|
146
|
-
}
|
|
147
|
-
})();
|
|
148
|
-
const resourceLoader = new DefaultResourceLoader({
|
|
149
|
-
cwd: options.cwd,
|
|
150
|
-
agentDir: AGENT_DIR,
|
|
151
|
-
settingsManager,
|
|
152
|
-
appendSystemPrompt: promptAppendBlocks(SYSTEM_APPEND, options.interactive ?? true),
|
|
153
|
-
// The SDK discovers `<agentDir>/skills` itself through the package manager,
|
|
154
|
-
// and the project dir is its own `.hxf`, so both the user's
|
|
155
|
-
// `.howtofix/skills` and the bundled suite are handed over here. The user's
|
|
156
|
-
// dir comes first (first-wins on a name collision), and the bundled suite
|
|
157
|
-
// drops out of the list entirely when the antislop switch resolves off.
|
|
158
|
-
additionalSkillPaths: additionalSkillPaths(options.cwd),
|
|
159
|
-
extensionFactories: [
|
|
160
|
-
// The plan gate must come first: the SDK returns on the first `block`, so
|
|
161
|
-
// registering it ahead of the approval gate is what makes plan mode hold in
|
|
162
|
-
// every approval mode, `full-auto` included.
|
|
163
|
-
createPlanGateExtension(),
|
|
164
|
-
createPermissionExtension(),
|
|
165
|
-
createBrandExtension(),
|
|
166
|
-
createHooksExtension(),
|
|
167
|
-
// DCP sits between brand and the notes, and both neighbours matter.
|
|
168
|
-
// Before brand and the id tags it writes into the prompt are scrubbed
|
|
169
|
-
// back out; after context-extension and the shared notes become keep-set
|
|
170
|
-
// candidates and get pruned. `check-dcp.mjs` asserts the order with three
|
|
171
|
-
// handlers that overwrite each other — acceptance covers it, but if you
|
|
172
|
-
// move this line, that check fails and it is right to fail.
|
|
173
|
-
createDcpExtension(),
|
|
174
|
-
createContextExtension(),
|
|
175
|
-
// After the notes so a preset's rules are appended last, and after brand
|
|
176
|
-
// so the harness scrub has already run over the sections it owns.
|
|
177
|
-
createPresetExtension(options.cwd),
|
|
178
|
-
createGoalExtension(),
|
|
179
|
-
createPlanPromptExtension(),
|
|
180
|
-
createCompactionGuardExtension(),
|
|
181
|
-
createCodemodeExtension(),
|
|
182
|
-
// Reads the setting per tool result, so `/rtk on` takes effect on the next tool
|
|
183
|
-
// call rather than on the next process.
|
|
184
|
-
createRtkExtension({ enabled: () => rtkEnabled }),
|
|
185
|
-
// A loader that re-reads `mcp.json` each time the extension asks, so a
|
|
186
|
-
// server removed from the preset menu is seen as gone and its transport is
|
|
187
|
-
// closed. The SDK reads the file once on `session_start` otherwise, which
|
|
188
|
-
// left the removed server's process running until the CLI exited.
|
|
189
|
-
createMcpExtension({
|
|
190
|
-
loadConfig: (ctx) => loadMcpConfiguration({
|
|
191
|
-
cwd: ctx.cwd,
|
|
192
|
-
projectTrusted: ctx.isProjectTrusted?.() ?? false,
|
|
193
|
-
}),
|
|
194
|
-
}),
|
|
195
|
-
],
|
|
196
|
-
});
|
|
197
|
-
await resourceLoader.reload();
|
|
198
|
-
// Subagent definitions the user wrote, read before the session is built so the
|
|
199
|
-
// `task` tool's description can name them: a definition the model is never told
|
|
200
|
-
// about is a definition that does not exist. Problems are reported as notes
|
|
201
|
-
// rather than thrown — a typo in one file must not stop the CLI from opening.
|
|
202
|
-
const agentDefs = loadAgentDefs(options.cwd, agents);
|
|
203
|
-
// Built after the definitions are in the map, so the description names them.
|
|
204
|
-
const taskTool = createTaskTool();
|
|
205
|
-
// Claims are held per session and written under the session's workspace. The
|
|
206
|
-
// holder id has to be stable for the whole session — a claim that cannot be
|
|
207
|
-
// renewed or released by the agent that took it is worse than no claim — so it
|
|
208
|
-
// is derived from the session the manager opened, not from the process.
|
|
209
|
-
const sessionId = sessionManager.getSessionId();
|
|
210
|
-
setRepoRoot(() => options.cwd);
|
|
211
|
-
const presenceDir = join(AGENT_DIR, SESSIONS_DIR_NAME);
|
|
212
|
-
setClaimsContext({
|
|
213
|
-
root: () => options.cwd,
|
|
214
|
-
holder: () => `session:${sessionId}`,
|
|
215
|
-
presenceDir: () => presenceDir,
|
|
216
|
-
selfId: () => sessionId,
|
|
217
|
-
});
|
|
218
|
-
// Announce this session so another one in the same workspace can see it and ask
|
|
219
|
-
// for a file it holds. Heartbeat, not a one-shot write: liveness is decided by the
|
|
220
|
-
// freshness of the file, so a session that is killed drops out on its own without
|
|
221
|
-
// anything having to reap it.
|
|
222
|
-
const beat = () => {
|
|
223
|
-
try {
|
|
224
|
-
announce({ dir: presenceDir }, { id: sessionId, cwd: options.cwd, host: hostname(), pid: process.pid, at: Date.now() });
|
|
225
|
-
}
|
|
226
|
-
catch {
|
|
227
|
-
// A session that cannot write its presence still works; it is simply invisible
|
|
228
|
-
// to the others. Failing the session over this would be the worse trade.
|
|
229
|
-
}
|
|
230
|
-
};
|
|
231
|
-
beat();
|
|
232
|
-
const heartbeat = setInterval(beat, Math.max(5_000, Math.floor(PRESENCE_TTL_MS / 3)));
|
|
233
|
-
// A background interval must not hold the process open on its own.
|
|
234
|
-
heartbeat.unref?.();
|
|
235
|
-
const result = await createAgentSession({
|
|
236
|
-
cwd: options.cwd,
|
|
237
|
-
agentDir: AGENT_DIR,
|
|
238
|
-
settingsManager,
|
|
239
|
-
sessionManager,
|
|
240
|
-
resourceLoader,
|
|
241
|
-
excludeTools: toolLoadout().exclude,
|
|
242
|
-
customTools: [
|
|
243
|
-
taskTool,
|
|
244
|
-
repoMapTool,
|
|
245
|
-
thinkTool,
|
|
246
|
-
askTool,
|
|
247
|
-
claimTool,
|
|
248
|
-
sessionsTool,
|
|
249
|
-
todoTool,
|
|
250
|
-
contextTool,
|
|
251
|
-
goalTool,
|
|
252
|
-
enterPlanModeTool,
|
|
253
|
-
exitPlanModeTool,
|
|
254
|
-
createWorkflowTool(),
|
|
255
|
-
webSearchTool,
|
|
256
|
-
webFetchTool,
|
|
257
|
-
lspTool,
|
|
258
|
-
qualityTool,
|
|
259
|
-
slopTool,
|
|
260
|
-
// The compress tool resolves its boundaries against the ref snapshot the
|
|
261
|
-
// handler published on the last prompt pass. Registering it here rather
|
|
262
|
-
// than inside the extension is the pattern this file uses for every
|
|
263
|
-
// custom tool, and the SDK enables a custom tool only when it is named in
|
|
264
|
-
// `tools` too - which is why `compress` also appears in MAIN_TOOLS.
|
|
265
|
-
createCompressTool({
|
|
266
|
-
state: () => getDcpState(activeDcpSession() ?? "dcp-sessionless"),
|
|
267
|
-
messages: () => snapshotFor(activeDcpSession() ?? "dcp-sessionless"),
|
|
268
|
-
}),
|
|
269
|
-
],
|
|
270
|
-
thinkingLevel: options.thinking,
|
|
271
|
-
});
|
|
272
|
-
// Activate the built-in search tools the product relies on.
|
|
273
|
-
//
|
|
274
|
-
// `grep`, `find` and `ls` are registered by the SDK but are not in its default
|
|
275
|
-
// active set (`read, bash, edit, write`), so without this they are dead weight:
|
|
276
|
-
// `permissions.ts` carries approval tiers for them and `agents.ts` tells every
|
|
277
|
-
// subagent to use them, while the model cannot call them. Measured before the
|
|
278
|
-
// fix: the model reported `read, bash, edit, write, task, …` and answered
|
|
279
|
-
// "NO GREP TOOL".
|
|
280
|
-
//
|
|
281
|
-
// This has to happen here, through the public session API, and NOT in an
|
|
282
|
-
// extension. An extension's `session_start` handler runs inside
|
|
283
|
-
// `bindExtensions()`, which the SDK follows with `_refreshToolRegistry()` — so
|
|
284
|
-
// anything `setActiveTools` did there is silently overwritten before the first
|
|
285
|
-
// turn. Measured: the extension ran, `setActiveTools` was called, and the
|
|
286
|
-
// active set was still `bash, edit, read, write`.
|
|
287
|
-
//
|
|
288
|
-
// Bind the extensions here, once, for every way the CLI can be started.
|
|
289
|
-
//
|
|
290
|
-
// `bindExtensions()` is what emits `session_start`, and `session_start` is where
|
|
291
|
-
// extensions do their work: the MCP extension connects its servers and turns on
|
|
292
|
-
// `codemode`, the hooks extension installs its handlers. It used to be called
|
|
293
|
-
// only from the workbench's `bindUI`, so a `--print` run (`-p`, which is what
|
|
294
|
-
// scripts and CI use) never emitted `session_start` at all: MCP servers were
|
|
295
|
-
// never connected, hooks never ran, and `codemode` was inactive.
|
|
296
|
-
//
|
|
297
|
-
// The mode is `"print"` so an extension asks through text instead of opening an
|
|
298
|
-
// overlay the workbench cannot host. `uiContext` is passed when a caller has
|
|
299
|
-
// one (the workbench does, after this returns, via `bindUI`); here it is the
|
|
300
|
-
// dialog-free context the SDK expects for non-interactive runs.
|
|
301
|
-
//
|
|
302
|
-
// This runs BEFORE `activateExtraBuiltins`: `bindExtensions` ends in the SDK's
|
|
303
|
-
// `_refreshToolRegistry()`, which resets the active set, so activating first
|
|
304
|
-
// would be undone.
|
|
305
|
-
await result.session.bindExtensions({ mode: "print", onError: () => { } });
|
|
306
|
-
// A short-lived access token has to be renewed before it is used, and this is
|
|
307
|
-
// the one place every way of starting the CLI goes through — the workbench
|
|
308
|
-
// renews again before each prompt, but `--print` never touches the workbench,
|
|
309
|
-
// so without this a scripted run on an expired token fails on its first call
|
|
310
|
-
// with nothing to explain it.
|
|
311
|
-
//
|
|
312
|
-
// A terminal refusal is reported as a note rather than thrown: the stored key
|
|
313
|
-
// may still work (the panel could be briefly unreachable, or the local clock
|
|
314
|
-
// could be ahead), and refusing to start would turn a recoverable state into a
|
|
315
|
-
// dead CLI.
|
|
316
|
-
const storedCredential = readCliCredential(AGENT_DIR);
|
|
317
|
-
const credentialNotes = [];
|
|
318
|
-
if (storedCredential) {
|
|
319
|
-
const renewed = await renewCliCredentialIfNeeded(AGENT_DIR, storedCredential);
|
|
320
|
-
if (renewed.renewed) {
|
|
321
|
-
await result.session.modelRuntime.setRuntimeApiKey("howtofix", renewed.credential.accessToken);
|
|
322
|
-
}
|
|
323
|
-
else if (renewed.terminal && renewed.message) {
|
|
324
|
-
credentialNotes.push(renewed.message);
|
|
325
|
-
}
|
|
326
|
-
else if (renewed.message) {
|
|
327
|
-
credentialNotes.push(renewed.message);
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
// `session_start` handlers are async and the runner does not await them, so
|
|
331
|
-
// give the ones that register tools (MCP's `codemode`) a moment to land before
|
|
332
|
-
// the first turn reads the loadout. This is bounded and only waits while the
|
|
333
|
-
// work is still in flight.
|
|
334
|
-
await settleExtensionTools(result.session);
|
|
335
|
-
// Only now, on the settled set, add the built-ins the SDK leaves inactive.
|
|
336
|
-
activateExtraBuiltins(result.session);
|
|
337
|
-
let model = result.session.model;
|
|
338
|
-
if (options.model) {
|
|
339
|
-
const available = await result.session.modelRuntime.getAvailable();
|
|
340
|
-
const requested = resolveModel(available, options.model);
|
|
341
|
-
if (!requested) {
|
|
342
|
-
await disposeAgentSession(result.session);
|
|
343
|
-
throw new Error(`unknown model '${options.model}'`);
|
|
344
|
-
}
|
|
345
|
-
if (model?.id !== requested.id || model.provider !== requested.provider) {
|
|
346
|
-
await result.session.setModel(requested);
|
|
347
|
-
model = result.session.model;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
else {
|
|
351
|
-
// Nothing was asked for, so the runtime's own default stands — and that
|
|
352
|
-
// default is the catalogue's, not this account's. The runtime ships a
|
|
353
|
-
// built-in provider catalogue, so a fresh session can start on
|
|
354
|
-
// `opencode/kimi-k2.6`, a provider with no credential here: the first prompt
|
|
355
|
-
// then goes nowhere and comes back as an empty assistant message, with
|
|
356
|
-
// nothing in the transcript to say why it looks hung. Start on a model the
|
|
357
|
-
// account can actually reach instead, preferring HowToFix's own providers.
|
|
358
|
-
//
|
|
359
|
-
// Available is not the same as reachable — the catalogue lists every provider
|
|
360
|
-
// it knows — so each provider is asked for its credential, once.
|
|
361
|
-
const available = await result.session.modelRuntime.getAvailable();
|
|
362
|
-
const withKey = new Map();
|
|
363
|
-
const reachable = [];
|
|
364
|
-
for (const entry of available) {
|
|
365
|
-
let ok = withKey.get(entry.provider);
|
|
366
|
-
if (ok === undefined) {
|
|
367
|
-
ok = Boolean((await result.session.modelRuntime.getAuth(entry.provider))?.auth?.apiKey);
|
|
368
|
-
withKey.set(entry.provider, ok);
|
|
369
|
-
}
|
|
370
|
-
if (ok)
|
|
371
|
-
reachable.push(entry);
|
|
372
|
-
}
|
|
373
|
-
// A credential in the runtime is not proof the provider answers: the built-in
|
|
374
|
-
// catalogue carries providers whose key is absent at request time, which is
|
|
375
|
-
// how a session ends up on `opencode/kimi-k2.6` and gets a 401 with nothing
|
|
376
|
-
// in the transcript. So the account's own provider wins whenever it has any
|
|
377
|
-
// model at all — an install signed in to HowToFix should run HowToFix models.
|
|
378
|
-
const ownProviders = HOWTOFIX_PROVIDERS;
|
|
379
|
-
const startedOnOwn = ownProviders.includes(model?.provider ?? "");
|
|
380
|
-
if (!startedOnOwn) {
|
|
381
|
-
const chosen = defaultModelFor(reachable.filter((entry) => ownProviders.includes(entry.provider)), ownProviders);
|
|
382
|
-
if (chosen) {
|
|
383
|
-
await result.session.setModel(chosen);
|
|
384
|
-
model = result.session.model;
|
|
385
|
-
}
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
// Stamp this build into a session it is allowed to write. A read-only open
|
|
389
|
-
// (`--print`, a resumed file) is left untouched: the stamp belongs to the
|
|
390
|
-
// build that owns the session, not to whoever glanced at it.
|
|
391
|
-
if (!options.noSession && sessionManager.getEntries().every((entry) => entry.type !== "custom" || entry.customType !== BUILD_ENTRY)) {
|
|
392
|
-
try {
|
|
393
|
-
sessionManager.appendCustomEntry(BUILD_ENTRY, { version: APP_VERSION });
|
|
394
|
-
}
|
|
395
|
-
catch {
|
|
396
|
-
// A session that cannot be written (read-only file, full disk) still works.
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
// Retention sweep, once per process and off the critical path: session files
|
|
400
|
-
// under `~/.howtofix/agent/sessions/` only ever grow — the SDK appends and
|
|
401
|
-
// never prunes, and the TUI's delete is one session a person picked — so files
|
|
402
|
-
// idle for longer than RETENTION_DAYS are unlinked for *every* project, not
|
|
403
|
-
// just this one. The file just opened is kept whatever its age: `--continue`
|
|
404
|
-
// and `/resume` exist to bring an old session back, and a sweep that ate its
|
|
405
|
-
// own argument would break them. Not awaited — startup must not pay for a walk
|
|
406
|
-
// of every session directory, and a failed sweep leaves the store merely
|
|
407
|
-
// larger. See session-retention.ts.
|
|
408
|
-
if (!sessionsSwept) {
|
|
409
|
-
sessionsSwept = true;
|
|
410
|
-
void sweepStaleSessions({ keep: result.session.sessionFile }).catch(() => {
|
|
411
|
-
// Housekeeping: the store is larger, not wrong.
|
|
412
|
-
});
|
|
413
|
-
}
|
|
414
|
-
const modelLabel = model ? `${model.provider}/${model.id}` : "no model";
|
|
415
|
-
// A model with no declared rates reports $0 forever, which is what makes a
|
|
416
|
-
// budget unenforceable on it. See `priced` on `OpenedSession`.
|
|
417
|
-
const priced = Boolean(model && model.cost && (model.cost.input > 0 || model.cost.output > 0));
|
|
418
|
-
const notes = [scrubHarnessMessage(result.modelFallbackMessage), ...credentialNotes, ...agentDefs.notes, ...agentDefs.problems].filter((note) => Boolean(note));
|
|
419
|
-
if (!model) {
|
|
420
|
-
notes.push(`no model configured. run \`howtofix\` and use /login, or edit ${join(AGENT_DIR, "models.json")}`);
|
|
421
|
-
}
|
|
422
|
-
return {
|
|
423
|
-
session: result.session,
|
|
424
|
-
dispose: async () => {
|
|
425
|
-
// The lsp client pool is process-wide and shared with subagents; the main
|
|
426
|
-
// session is the owner that tears it down (process `exit` is the backstop).
|
|
427
|
-
disposeAllLsp();
|
|
428
|
-
// Await the extension shutdown so MCP servers stop before the session
|
|
429
|
-
// that hosts their tools goes away.
|
|
430
|
-
await disposeAgentSession(result.session);
|
|
431
|
-
// Take this session out of the registry on a clean exit. A crash is covered
|
|
432
|
-
// by the heartbeat going stale, so this only makes the common case immediate.
|
|
433
|
-
clearInterval(heartbeat);
|
|
434
|
-
withdraw({ dir: presenceDir }, sessionId);
|
|
435
|
-
},
|
|
436
|
-
modelLabel,
|
|
437
|
-
priced,
|
|
438
|
-
// Exposed so the `/rtk` command can flip the flag the extension reads. Passing a
|
|
439
|
-
// setter rather than the variable keeps the two in one place: there is no way to
|
|
440
|
-
// change the setting without changing what the extension sees.
|
|
441
|
-
setCompressToolOutput: (on) => {
|
|
442
|
-
rtkEnabled = on;
|
|
443
|
-
},
|
|
444
|
-
note: [buildNote, ...notes].filter(Boolean).join("\n") || undefined,
|
|
445
|
-
};
|
|
446
|
-
}
|
package/dist/sessions.js
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A registry of other HowToFix CLI sessions on this machine.
|
|
3
|
-
*
|
|
4
|
-
* One process can only see the agents it started. Two sessions open on the same
|
|
5
|
-
* project — the user in one terminal and a long run in another — are invisible to each
|
|
6
|
-
* other, so neither can see what the other is holding, and a file claim taken by one
|
|
7
|
-
* cannot be explained to the other. This module is the smallest thing that closes that
|
|
8
|
-
* gap: each session writes one file describing itself, and any session can read them
|
|
9
|
-
* all.
|
|
10
|
-
*
|
|
11
|
-
* File-based for the same reason claims are: the sessions are separate processes, so
|
|
12
|
-
* there is no shared object to read and a broker would need something running. A file
|
|
13
|
-
* per session is what all of them can agree on without a daemon.
|
|
14
|
-
*
|
|
15
|
-
* A session that crashed must not appear as live. Liveness is the freshness of the
|
|
16
|
-
* heartbeat, not the presence of the file, so a killed process drops out on its own
|
|
17
|
-
* after {@link PRESENCE_TTL_MS} without anything having to clean up.
|
|
18
|
-
*/
|
|
19
|
-
import { mkdirSync, readdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from "node:fs";
|
|
20
|
-
import { join } from "node:path";
|
|
21
|
-
/** How long a session stays "live" without refreshing its file. */
|
|
22
|
-
export const PRESENCE_TTL_MS = 3 * 60 * 1000;
|
|
23
|
-
/**
|
|
24
|
-
* Directory under the agent dir holding one file per live session.
|
|
25
|
-
*
|
|
26
|
-
* Deliberately not `sessions`: `session-retention.ts` owns that one and sweeps it,
|
|
27
|
-
* so a presence file written there would sit among per-project session files and — far
|
|
28
|
-
* worse — be a candidate for its sweep. A live session whose presence is deleted looks
|
|
29
|
-
* to every other session like one that died.
|
|
30
|
-
*/
|
|
31
|
-
export const SESSIONS_DIR_NAME = "presence";
|
|
32
|
-
function file(options, id) {
|
|
33
|
-
// The id comes from the session, not from user input, but a path separator in it
|
|
34
|
-
// would still write the file somewhere else, so it is stripped rather than trusted.
|
|
35
|
-
return join(options.dir, `${id.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
|
|
36
|
-
}
|
|
37
|
-
/**
|
|
38
|
-
* Read every session's file, dropping the ones that are stale or unreadable.
|
|
39
|
-
*
|
|
40
|
-
* Unreadable means "not there": a half-written file from a session that is starting up
|
|
41
|
-
* must not make a reader fail, and a reader that cannot list sessions is worse than one
|
|
42
|
-
* that lists fewer.
|
|
43
|
-
*/
|
|
44
|
-
export function listSessions(options, now = Date.now()) {
|
|
45
|
-
let names;
|
|
46
|
-
try {
|
|
47
|
-
names = readdirSync(options.dir);
|
|
48
|
-
}
|
|
49
|
-
catch {
|
|
50
|
-
return [];
|
|
51
|
-
}
|
|
52
|
-
const out = [];
|
|
53
|
-
for (const name of names) {
|
|
54
|
-
if (!name.endsWith(".json"))
|
|
55
|
-
continue;
|
|
56
|
-
try {
|
|
57
|
-
const raw = JSON.parse(readFileSync(join(options.dir, name), "utf8"));
|
|
58
|
-
if (typeof raw?.id !== "string" || typeof raw?.cwd !== "string" || typeof raw?.at !== "number")
|
|
59
|
-
continue;
|
|
60
|
-
if (now - raw.at >= PRESENCE_TTL_MS)
|
|
61
|
-
continue;
|
|
62
|
-
out.push({
|
|
63
|
-
id: raw.id,
|
|
64
|
-
cwd: raw.cwd,
|
|
65
|
-
host: typeof raw.host === "string" ? raw.host : "",
|
|
66
|
-
pid: typeof raw.pid === "number" ? raw.pid : 0,
|
|
67
|
-
at: raw.at,
|
|
68
|
-
...(typeof raw.activity === "string" && raw.activity ? { activity: raw.activity } : {}),
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
catch {
|
|
72
|
-
// Starting up, or dying. Either way there is nothing to report yet.
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
// Newest first: a caller showing the list wants the busy sessions at the top.
|
|
76
|
-
return out.sort((a, b) => b.at - a.at);
|
|
77
|
-
}
|
|
78
|
-
/** Write or refresh one session's presence, replacing the file atomically. */
|
|
79
|
-
export function announce(options, presence) {
|
|
80
|
-
mkdirSync(options.dir, { recursive: true });
|
|
81
|
-
const target = file(options, presence.id);
|
|
82
|
-
const tmp = `${target}.${process.pid}.tmp`;
|
|
83
|
-
writeFileSync(tmp, JSON.stringify(presence, null, 2));
|
|
84
|
-
// A reader can be listing while this runs; rename means it sees either the old
|
|
85
|
-
// file or the new one, never a partial write it would have to skip.
|
|
86
|
-
renameSync(tmp, target);
|
|
87
|
-
}
|
|
88
|
-
/** Remove a session's file, on a clean exit. */
|
|
89
|
-
export function withdraw(options, id) {
|
|
90
|
-
try {
|
|
91
|
-
unlinkSync(file(options, id));
|
|
92
|
-
}
|
|
93
|
-
catch {
|
|
94
|
-
// Already gone, or never written. Nothing to undo.
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
/** The sessions working in one workspace, excluding this one if asked. */
|
|
98
|
-
export function sessionsIn(options, cwd, exceptId, now = Date.now()) {
|
|
99
|
-
return listSessions(options, now).filter((s) => s.cwd === cwd && s.id !== exceptId);
|
|
100
|
-
}
|