howtofixcli 0.7.14 → 0.7.15

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.
Files changed (165) hide show
  1. package/dist/chunk-27TRKD65.js +2 -0
  2. package/dist/chunk-3FYCFLNK.js +1 -0
  3. package/dist/chunk-56SEAGAQ.js +1 -0
  4. package/dist/chunk-6FUWMLWO.js +1 -0
  5. package/dist/chunk-7CHDNTH6.js +1 -0
  6. package/dist/chunk-7DTQKDO4.js +2 -0
  7. package/dist/chunk-AGGIYXN7.js +2 -0
  8. package/dist/chunk-AUDMRGWF.js +189 -0
  9. package/dist/chunk-CPJKKHGH.js +1 -0
  10. package/dist/chunk-DAPXGSGD.js +12 -0
  11. package/dist/chunk-DOD7MUGX.js +1 -0
  12. package/dist/chunk-DV2BBBBJ.js +2 -0
  13. package/dist/chunk-EIBOS5KO.js +5 -0
  14. package/dist/chunk-EYWOVCSS.js +6 -0
  15. package/dist/chunk-FPXVDP32.js +8 -0
  16. package/dist/chunk-GXB7RZHH.js +1 -0
  17. package/dist/chunk-HT3MFLAV.js +34 -0
  18. package/dist/chunk-ILDDDLRC.js +1 -0
  19. package/dist/chunk-JFYVRWJI.js +2 -0
  20. package/dist/chunk-JMKK26AT.js +2 -0
  21. package/dist/chunk-LAHVHRCS.js +2 -0
  22. package/dist/chunk-LOOFOBTS.js +1 -0
  23. package/dist/chunk-LSJC73DG.js +1 -0
  24. package/dist/chunk-MOKUBKE7.js +1 -0
  25. package/dist/chunk-N345KWWK.js +1 -0
  26. package/dist/chunk-ONZLNFQ2.js +3 -0
  27. package/dist/chunk-ORJ2QZBG.js +3 -0
  28. package/dist/chunk-PCALBZOO.js +2 -0
  29. package/dist/chunk-SX5VE6XA.js +110 -0
  30. package/dist/chunk-TSEKPGK4.js +1 -0
  31. package/dist/chunk-UNK5HRQR.js +1 -0
  32. package/dist/chunk-UTYFLLSM.js +1 -0
  33. package/dist/chunk-V2RPZMLK.js +1 -0
  34. package/dist/chunk-V3R4FYTJ.js +1 -0
  35. package/dist/chunk-V656XVXV.js +8 -0
  36. package/dist/chunk-VLMQSD25.js +1 -0
  37. package/dist/chunk-WKUJ7CFO.js +1 -0
  38. package/dist/chunk-WOT6VMZA.js +1 -0
  39. package/dist/chunk-WUPU6LTD.js +4 -0
  40. package/dist/chunk-WX3V5P7U.js +5 -0
  41. package/dist/chunk-Y7YM5R75.js +5 -0
  42. package/dist/chunk-YW7NORH3.js +2 -0
  43. package/dist/main.js +99 -551
  44. package/package.json +17 -64
  45. package/dist/account-credential.js +0 -136
  46. package/dist/account-credential.test.js +0 -38
  47. package/dist/acp.js +0 -234
  48. package/dist/activity.js +0 -124
  49. package/dist/agent-defs.js +0 -237
  50. package/dist/agent-limiter.js +0 -68
  51. package/dist/agents.js +0 -554
  52. package/dist/anim.js +0 -361
  53. package/dist/args.js +0 -250
  54. package/dist/at-file.js +0 -28
  55. package/dist/brand-extension.js +0 -87
  56. package/dist/brand.js +0 -76
  57. package/dist/browser.js +0 -58
  58. package/dist/cache-report.js +0 -139
  59. package/dist/checkpoint.js +0 -437
  60. package/dist/claims.js +0 -167
  61. package/dist/cli-auth.js +0 -142
  62. package/dist/cli-checkin.js +0 -120
  63. package/dist/cli-credentials.js +0 -254
  64. package/dist/clipboard.js +0 -149
  65. package/dist/collapse.js +0 -129
  66. package/dist/commands.js +0 -294
  67. package/dist/compaction-guard.js +0 -81
  68. package/dist/context-extension.js +0 -17
  69. package/dist/dcp/commands.js +0 -172
  70. package/dist/dcp/compress-state.js +0 -139
  71. package/dist/dcp/compress.js +0 -329
  72. package/dist/dcp/dcp-extension.js +0 -295
  73. package/dist/dcp/dedup.js +0 -93
  74. package/dist/dcp/ids.js +0 -189
  75. package/dist/dcp/index.js +0 -25
  76. package/dist/dcp/middle-prune.js +0 -76
  77. package/dist/dcp/prompts.js +0 -68
  78. package/dist/dcp/protected-content.js +0 -221
  79. package/dist/dcp/protected.js +0 -161
  80. package/dist/dcp/prune.js +0 -418
  81. package/dist/dcp/state.js +0 -176
  82. package/dist/device-id.js +0 -90
  83. package/dist/device-id.test.js +0 -23
  84. package/dist/errors.js +0 -63
  85. package/dist/extensions.js +0 -48
  86. package/dist/fatal.js +0 -35
  87. package/dist/feed.js +0 -498
  88. package/dist/fs-atomic.js +0 -54
  89. package/dist/gateway-errors.js +0 -90
  90. package/dist/git.js +0 -230
  91. package/dist/goal-extension.js +0 -88
  92. package/dist/goal.js +0 -222
  93. package/dist/hooks-extension.js +0 -176
  94. package/dist/hooks.js +0 -443
  95. package/dist/howtofix-account.js +0 -735
  96. package/dist/hud-line.js +0 -153
  97. package/dist/hwid.js +0 -130
  98. package/dist/interrupt.js +0 -60
  99. package/dist/json-events.js +0 -50
  100. package/dist/level-style.js +0 -112
  101. package/dist/loop-guard.js +0 -95
  102. package/dist/lsp-tools.js +0 -32
  103. package/dist/lsp.js +0 -429
  104. package/dist/mcp-config.js +0 -85
  105. package/dist/mcp-lifecycle.js +0 -147
  106. package/dist/model-catalog.js +0 -217
  107. package/dist/model-facts.js +0 -35
  108. package/dist/model-resolve.js +0 -65
  109. package/dist/notes.js +0 -78
  110. package/dist/oauth-login.js +0 -568
  111. package/dist/paths.js +0 -61
  112. package/dist/permissions.js +0 -298
  113. package/dist/plan-mode.js +0 -363
  114. package/dist/preset-command.js +0 -227
  115. package/dist/preset-extension.js +0 -35
  116. package/dist/preset-mcp.js +0 -292
  117. package/dist/presets.js +0 -727
  118. package/dist/prompt-files.js +0 -103
  119. package/dist/prompt-text.js +0 -41
  120. package/dist/proxy.js +0 -137
  121. package/dist/quality/ast.js +0 -196
  122. package/dist/quality/cognitive.js +0 -83
  123. package/dist/quality/cyclomatic.js +0 -30
  124. package/dist/quality/halstead.js +0 -83
  125. package/dist/quality/index.js +0 -18
  126. package/dist/quality/length.js +0 -3
  127. package/dist/quality/maintainability.js +0 -15
  128. package/dist/quality/nesting.js +0 -57
  129. package/dist/quality/report.js +0 -803
  130. package/dist/quality/smells.js +0 -67
  131. package/dist/quality-tool.js +0 -63
  132. package/dist/read-only-commands.js +0 -597
  133. package/dist/repo-map.js +0 -292
  134. package/dist/roblox-connect.js +0 -125
  135. package/dist/rpc.js +0 -91
  136. package/dist/rtk.js +0 -193
  137. package/dist/sandbox.js +0 -105
  138. package/dist/schema.js +0 -214
  139. package/dist/serve.js +0 -50
  140. package/dist/session-lifecycle.js +0 -19
  141. package/dist/session-retention.js +0 -111
  142. package/dist/session.js +0 -447
  143. package/dist/sessions.js +0 -100
  144. package/dist/settings.js +0 -148
  145. package/dist/skills.js +0 -35
  146. package/dist/slop-tool.js +0 -96
  147. package/dist/spend-cap.js +0 -43
  148. package/dist/subagent.js +0 -171
  149. package/dist/tar.js +0 -146
  150. package/dist/theme.js +0 -79
  151. package/dist/thinking.js +0 -45
  152. package/dist/todo.js +0 -114
  153. package/dist/tool-loadout.js +0 -97
  154. package/dist/tool-render.js +0 -769
  155. package/dist/tools.js +0 -176
  156. package/dist/ui.js +0 -4814
  157. package/dist/update.js +0 -551
  158. package/dist/usage-segment.js +0 -69
  159. package/dist/web-tools.js +0 -46
  160. package/dist/web.js +0 -265
  161. package/dist/welcome.js +0 -179
  162. package/dist/workflow-engine.js +0 -246
  163. package/dist/workflow-progress.js +0 -446
  164. package/dist/workflow-view.js +0 -195
  165. package/dist/workflow.js +0 -349
@@ -1,217 +0,0 @@
1
- /**
2
- * Real context windows and reasoning support, from models.dev.
3
- *
4
- * `GET /v1/models` on the gateway returns ids and nothing else — verified against
5
- * the live service, whose response carries only `id`, `object`, `created`,
6
- * `owned_by` and `supported_endpoint_types`. So a synced model has no declared
7
- * window and no declared capability, and `/connect` filled the gap with two
8
- * guesses that were both wrong in the same direction:
9
- *
10
- * - `contextWindow: 128_000` for every model. The gateway serves models with
11
- * 1,000,000-token windows (deepseek-v4.1-flash, kimi-k3, mimo-v2.5), so the
12
- * context meter read ~8x too full and the CLI compacted a conversation that
13
- * had barely started.
14
- * - no `reasoning` field at all. The SDK gates thinking on it — the schema is
15
- * `reasoning: boolean`, and `getSupportedThinkingLevels` returns `["off"]`
16
- * the moment it is falsy — so every synced model lost its thinking levels
17
- * even though the model reasons. That is the bug a user reports as "thinking
18
- * is missing on a model that has it".
19
- *
20
- * models.dev publishes exactly these facts per model (`limit.context`,
21
- * `limit.output`, `reasoning`). It is a public catalogue, so the CLI can read it
22
- * instead of guessing. Nothing here is bundled: the catalogue is fetched once,
23
- * slimmed to the three fields that are used, and cached next to the agent state,
24
- * so a later start can repair models without a network call.
25
- *
26
- * The pure half (index, lookup) is separated from the network half so the
27
- * matching rules are testable without a fetch.
28
- */
29
- /** Where the catalogue lives. One document, every provider. */
30
- export const MODELS_DEV_URL = "https://models.dev/api.json";
31
- /** How long to wait for the catalogue. It is ~5 MB, so this is generous. */
32
- export const CATALOG_TIMEOUT_MS = 20_000;
33
- /**
34
- * Providers whose entry wins when the same model is listed by several.
35
- *
36
- * The catalogue is community-maintained and mirrors every reseller, so one model
37
- * id resolves to a dozen near-identical entries. The first-party listing is the
38
- * one whose numbers describe the model rather than a middleman's limits, so the
39
- * search prefers it. Order matters only among the preferred set; everything else
40
- * is ordered alphabetically so the result never depends on object iteration.
41
- */
42
- const PREFERRED_PROVIDERS = ["anthropic", "openai", "google", "deepseek", "zai", "xai", "moonshotai", "minimax", "alibaba", "xiaomi", "meta", "tencent", "nvidia", "venice", "openrouter"];
43
- /** The comparison form of a model id: case and punctuation are not meaningful. */
44
- export function normalizeModelId(id) {
45
- return id.toLowerCase().replace(/[^a-z0-9]+/g, "");
46
- }
47
- /** The last path segment of an id, normalized: `z-ai/glm-5.3-fast` -> `glm53fast`. */
48
- function lastSegment(id) {
49
- const parts = id.split("/");
50
- return normalizeModelId(parts[parts.length - 1] ?? id);
51
- }
52
- /**
53
- * Index every model in the catalogue document.
54
- *
55
- * One entry per normalized id; when several providers list the same model, the
56
- * preferred provider wins (see {@link PREFERRED_PROVIDERS}). A model whose entry
57
- * carries no `limit.context` is skipped: without a window there is nothing to
58
- * correct, and indexing it would let a bad entry shadow a good one.
59
- */
60
- export function buildCatalogIndex(raw) {
61
- const index = new Map();
62
- const owners = new Map();
63
- if (!raw || typeof raw !== "object")
64
- return index;
65
- const providers = raw;
66
- const priority = (provider) => {
67
- const at = PREFERRED_PROVIDERS.indexOf(provider);
68
- return at === -1 ? PREFERRED_PROVIDERS.length : at;
69
- };
70
- // Sorted so the winner among equally-preferred providers is decided by name,
71
- // not by the insertion order of a parsed JSON object — this number feeds a
72
- // token budget, so it must not depend on how the document happened to be built.
73
- for (const provider of Object.keys(providers).sort()) {
74
- const models = providers[provider]?.models;
75
- if (!models || typeof models !== "object")
76
- continue;
77
- for (const id of Object.keys(models)) {
78
- const model = models[id];
79
- const context = Number(model?.limit?.context);
80
- if (!Number.isFinite(context) || context <= 0)
81
- continue;
82
- const key = normalizeModelId(id);
83
- const rank = priority(provider);
84
- const held = owners.get(key);
85
- // A preferred provider always wins; otherwise the first (alphabetical) one
86
- // keeps the slot. Never let a later, less-preferred listing overwrite it.
87
- if (held !== undefined && held <= rank)
88
- continue;
89
- const output = Number(model?.limit?.output);
90
- index.set(key, {
91
- contextWindow: Math.floor(context),
92
- maxTokens: Number.isFinite(output) && output > 0 ? Math.floor(output) : undefined,
93
- reasoning: model?.reasoning === true,
94
- vision: Array.isArray(model?.modalities?.input) && model.modalities.input.includes("image"),
95
- });
96
- owners.set(key, rank);
97
- }
98
- }
99
- return index;
100
- }
101
- /**
102
- * The catalogue entry for a gateway model id, or undefined when it is unknown.
103
- *
104
- * Three passes, in order of how literal the match is: the whole id, the last path
105
- * segment, then a suffix. The gateway renames models (`deepseek-v4.1-flash` is
106
- * `DeepSeek-V4.1-Flash` in the catalogue, `glm-5.3-flash` is `zai/glm-5.3-flash`)
107
- * and drops vendor prefixes (`anthropic/claude-sonnet-5` keeps its own), so a
108
- * single exact match finds less than half. A miss is not a failure: the caller
109
- * keeps its conservative default, which is the behaviour before this module.
110
- */
111
- export function lookupModel(index, id) {
112
- const wanted = normalizeModelId(id);
113
- if (!wanted)
114
- return undefined;
115
- const exact = index.get(wanted);
116
- if (exact)
117
- return exact;
118
- const wantedTail = lastSegment(id);
119
- for (const [key, entry] of index) {
120
- if (lastSegment(key) === wantedTail)
121
- return entry;
122
- }
123
- for (const [key, entry] of index) {
124
- if (key.endsWith(wanted) || wanted.endsWith(key))
125
- return entry;
126
- }
127
- return undefined;
128
- }
129
- /** Where the slimmed catalogue is cached, so a later start needs no network. */
130
- export function catalogCachePath(agentDir) {
131
- return `${agentDir}/models-dev-cache.json`;
132
- }
133
- /**
134
- * The serialized form of an index: `{ id: [context, output, reasoning, vision] }`.
135
- *
136
- * An array rather than an object per entry because the catalogue is 8,000+ rows
137
- * and repeating the keys eight thousand times triples the file for no reader's
138
- * benefit. The cache is internal — nothing outside this module parses it.
139
- *
140
- * `vision` was added after the first release, so a cache written by the older
141
- * build is a 3-tuple. {@link deserializeCatalog} reads the fourth slot as
142
- * optional and defaults it to `false`, which is the behaviour that build had;
143
- * the next fetch fills it in. Bumping a format number would instead throw the
144
- * whole cache away and cost every user a 5 MB download on the next start.
145
- */
146
- export function serializeCatalog(index) {
147
- const out = {};
148
- for (const [key, entry] of index) {
149
- out[key] = [entry.contextWindow, entry.maxTokens ?? 0, entry.reasoning ? 1 : 0, entry.vision ? 1 : 0];
150
- }
151
- return JSON.stringify(out);
152
- }
153
- /** Read back {@link serializeCatalog}. Returns an empty index for anything unusable. */
154
- export function deserializeCatalog(text) {
155
- const index = new Map();
156
- let parsed;
157
- try {
158
- parsed = JSON.parse(text);
159
- }
160
- catch {
161
- return index;
162
- }
163
- if (!parsed || typeof parsed !== "object")
164
- return index;
165
- for (const [key, value] of Object.entries(parsed)) {
166
- if (!Array.isArray(value) || value.length < 3)
167
- continue;
168
- const context = Number(value[0]);
169
- const output = Number(value[1]);
170
- if (!Number.isFinite(context) || context <= 0)
171
- continue;
172
- index.set(key, {
173
- contextWindow: Math.floor(context),
174
- maxTokens: Number.isFinite(output) && output > 0 ? Math.floor(output) : undefined,
175
- reasoning: value[2] === 1 || value[2] === true,
176
- // Absent in a cache written before this field existed: read it as "no
177
- // vision", which is what that build assumed, and let the next fetch correct it.
178
- vision: value[3] === 1 || value[3] === true,
179
- });
180
- }
181
- return index;
182
- }
183
- /**
184
- * Fetch the catalogue and index it.
185
- *
186
- * Never throws: a catalogue that cannot be fetched is a reason to keep the
187
- * conservative defaults, not a reason for `/connect` to fail. The caller reports
188
- * which of the two happened.
189
- */
190
- export async function fetchCatalog(options) {
191
- const url = options?.url ?? MODELS_DEV_URL;
192
- const doFetch = options?.fetchImpl ?? fetch;
193
- const timeoutMs = options?.timeoutMs ?? CATALOG_TIMEOUT_MS;
194
- const controller = new AbortController();
195
- const timer = setTimeout(() => controller.abort(), timeoutMs);
196
- try {
197
- const response = await doFetch(url, { signal: controller.signal, headers: { Accept: "application/json" } });
198
- if (!response.ok) {
199
- return { ok: false, message: `the model catalogue answered ${response.status}` };
200
- }
201
- const index = buildCatalogIndex(await response.json());
202
- if (index.size === 0) {
203
- return { ok: false, message: "the model catalogue was empty or in an unexpected shape" };
204
- }
205
- return { ok: true, index };
206
- }
207
- catch (cause) {
208
- const aborted = cause instanceof Error && cause.name === "AbortError";
209
- return {
210
- ok: false,
211
- message: aborted ? `the model catalogue did not answer within ${Math.round(timeoutMs / 1000)}s` : "could not reach the model catalogue",
212
- };
213
- }
214
- finally {
215
- clearTimeout(timer);
216
- }
217
- }
@@ -1,35 +0,0 @@
1
- /**
2
- * The two facts a model picker is useless without: how much context it takes and
3
- * what it costs.
4
- *
5
- * Every model entry already carries `contextWindow` and `cost`, but the picker
6
- * showed only the provider, so a free model and an expensive one looked the
7
- * same. These helpers turn those numbers into one short line for the list.
8
- */
9
- /** 200000 -> "200K", 1000000 -> "1M", 1500000 -> "1.5M". Empty when unknown. */
10
- export function formatContextWindow(tokens) {
11
- if (!tokens || !Number.isFinite(tokens) || tokens <= 0)
12
- return "";
13
- if (tokens >= 1_000_000) {
14
- // Rounded to one decimal, then trimmed: 1_048_576 is "1M", not "1.0M".
15
- return `${Number((tokens / 1_000_000).toFixed(1))}M`;
16
- }
17
- if (tokens >= 1_000)
18
- return `${Math.round(tokens / 1_000)}K`;
19
- return String(tokens);
20
- }
21
- /** Price per million tokens for input and output, or "free" when both are zero. */
22
- export function formatModelPrice(cost) {
23
- if (!cost)
24
- return "";
25
- if (cost.input <= 0 && cost.output <= 0)
26
- return "free";
27
- const money = (value) => (value === 0 ? "0" : String(Number(value >= 1 ? value.toFixed(2) : value.toFixed(3))));
28
- return `$${money(cost.input)}/$${money(cost.output)} per M`;
29
- }
30
- /** "1M ctx · free" — the summary shown beside a model in the picker. */
31
- export function describeModelFacts(model) {
32
- const context = formatContextWindow(model.contextWindow);
33
- const price = formatModelPrice(model.cost);
34
- return [context ? `${context} ctx` : "", price].filter(Boolean).join(" · ");
35
- }
@@ -1,65 +0,0 @@
1
- /**
2
- * Resolve a model spec the way the CLI's own `--model` flag does.
3
- *
4
- * Exact `provider/id` or a bare `id` first, then a case-insensitive substring
5
- * match, so both the name `/model` prints and a shorthand work. Lifted out of
6
- * `session.ts` when subagents gained a model of their own: two resolvers would
7
- * drift, and a subagent picking a different model than the parent asked for
8
- * would read as the option being broken.
9
- */
10
- export function resolveModel(models, spec) {
11
- const exact = models.find((model) => `${model.provider}/${model.id}` === spec || model.id === spec);
12
- if (exact)
13
- return exact;
14
- const needle = spec.toLowerCase();
15
- return models.find((model) => model.id.toLowerCase().includes(needle) || `${model.provider}/${model.id}`.toLowerCase().includes(needle));
16
- }
17
- /**
18
- * Order a catalogue for choosing a default model, best first.
19
- *
20
- * A gateway lists its catalogue in no useful order — `anthropic/claude-opus-4.8`
21
- * can come first while the fast general-purpose models are far down the list — so
22
- * "use whatever is first" hands a new user the most expensive thing on the
23
- * account, or an image model that cannot hold a conversation.
24
- *
25
- * Image, embedding, audio and video models are pushed to the end: they cannot
26
- * answer a coding prompt, so selecting one would look like a broken install. The
27
- * caller then probes the first few and takes the first that answers.
28
- */
29
- const PREFERRED_FAMILIES = ["deepseek", "qwen", "claude", "gpt-5", "gpt-4", "gemini", "glm", "mimo", "kimi", "llama"];
30
- const NOT_CHAT = /(image|embedding|embed|whisper|tts|audio|video|rerank|moderation|dall-e|stable-diffusion|sora)/i;
31
- export function orderModelsForDefault(ids) {
32
- const rank = (id) => {
33
- if (NOT_CHAT.test(id))
34
- return 100;
35
- const family = PREFERRED_FAMILIES.findIndex((name) => id.toLowerCase().includes(name));
36
- return family === -1 ? 50 : family;
37
- };
38
- // Stable within a rank, so the gateway's own order decides ties.
39
- return ids
40
- .map((id, index) => ({ id, index }))
41
- .sort((a, b) => rank(a.id) - rank(b.id) || a.index - b.index)
42
- .map((entry) => entry.id);
43
- }
44
- /**
45
- * The model a session should start on when nothing was asked for.
46
- *
47
- * The model runtime knows every model in the catalog it ships, including
48
- * providers this account holds no credential for, and its own default is the
49
- * catalog's — `opencode/kimi-k2.6` — not the account's. Starting there is not a
50
- * cosmetic problem: the first prompt goes to a provider that cannot answer and
51
- * comes back as an empty assistant message, with nothing in the transcript to
52
- * explain why it looks hung.
53
- *
54
- * So the default comes from the models that are actually available, and a
55
- * HowToFix provider wins over any other reachable one: an install that signed in
56
- * to HowToFix should run HowToFix's models.
57
- */
58
- export function defaultModelFor(available, ownProviders = []) {
59
- if (available.length === 0)
60
- return undefined;
61
- const own = available.filter((model) => ownProviders.includes(model.provider));
62
- const pool = own.length > 0 ? own : available;
63
- const first = orderModelsForDefault(pool.map((model) => model.id))[0];
64
- return pool.find((model) => model.id === first) ?? pool[0];
65
- }
package/dist/notes.js DELETED
@@ -1,78 +0,0 @@
1
- /**
2
- * A shared scratchpad for facts that must survive context compaction and reach
3
- * every agent working on the task.
4
- *
5
- * A shared notes store for the session.
6
- * Why it matters: the `task` tool spawns subagents with their own context
7
- * window, so anything the main agent learned while scouting is invisible to them,
8
- * and anything they discover dies with their run. A note written once is visible
9
- * to the parent and to every sibling, and it survives a compaction that would
10
- * otherwise drop the discovery from the transcript.
11
- *
12
- * The store is process-wide on purpose: `agents.ts` opens subagent sessions in
13
- * this same process, so they share it. It is in-memory only — notes are working
14
- * state for one session, not project documentation (that is `AGENTS.md`).
15
- */
16
- import { StringEnum } from "@netrodev/hxf-ai";
17
- import { defineTool } from "@netrodev/hxf-coding-agent";
18
- import { Type } from "typebox";
19
- const notes = new Map();
20
- /** Longest note accepted, so one call cannot flood every later request. */
21
- const MAX_VALUE = 2000;
22
- /** Longest key accepted; keys are the lookup handle, so they stay short. */
23
- const MAX_KEY = 80;
24
- export function clearNotes() {
25
- notes.clear();
26
- }
27
- export function noteCount() {
28
- return notes.size;
29
- }
30
- /** The block injected into the prompt, or an empty string when there is nothing. */
31
- export function renderNotes() {
32
- if (notes.size === 0)
33
- return "";
34
- const lines = [...notes].map(([key, value]) => `- ${key}: ${value}`);
35
- return ["<shared-notes>", "Working notes recorded with the `context` tool. They survive compaction and are visible to every agent on this task.", ...lines, "</shared-notes>"].join("\n");
36
- }
37
- export function applyNote(op, key, value) {
38
- if (op === "list") {
39
- if (notes.size === 0)
40
- return { text: "No notes." };
41
- return { text: [...notes].map(([k, v]) => `${k} = ${v}`).join("\n") };
42
- }
43
- if (!key)
44
- return { text: "Error: key is required", error: "key required" };
45
- if (key.length > MAX_KEY)
46
- return { text: `Error: key longer than ${MAX_KEY} characters`, error: "key too long" };
47
- if (op === "forget") {
48
- const existed = notes.delete(key);
49
- return { text: existed ? `Removed ${key}` : `No note named ${key}` };
50
- }
51
- if (op === "set") {
52
- if (!value)
53
- return { text: "Error: value is required for set", error: "value required" };
54
- const clipped = value.length > MAX_VALUE ? `${value.slice(0, MAX_VALUE)}…` : value;
55
- notes.set(key, clipped);
56
- return { text: `Recorded ${key} (${notes.size} note${notes.size === 1 ? "" : "s"} held)` };
57
- }
58
- return { text: `Error: unknown op ${JSON.stringify(op)}`, error: "unknown op" };
59
- }
60
- export const contextTool = defineTool({
61
- name: "context",
62
- label: "context",
63
- description: "Record a durable working note (a decision, a file's role, a fact you would otherwise re-derive). Notes survive context compaction and are visible to every agent on this task, including subagents. Use `set` to write, `list` to read, `forget` to remove.",
64
- promptSnippet: "context: durable notes shared with every agent on this task",
65
- parameters: Type.Object({
66
- op: StringEnum(["set", "list", "forget"], { description: "set writes a note, list reads all, forget removes one" }),
67
- key: Type.Optional(Type.String({ description: "Short lookup key, e.g. 'auth-entrypoint'" })),
68
- value: Type.Optional(Type.String({ description: "The fact to keep (for set)" })),
69
- }),
70
- async execute(_toolCallId, params) {
71
- const { text, error } = applyNote(params.op, params.key, params.value);
72
- return {
73
- content: [{ type: "text", text }],
74
- details: { op: params.op, notes: Object.fromEntries(notes) },
75
- ...(error ? { isError: true } : {}),
76
- };
77
- },
78
- });