howtofixcli 0.7.8 → 0.7.10
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-2YYQ3OBQ.js +1 -0
- package/dist/chunk-3FYCFLNK.js +1 -0
- package/dist/chunk-3KT55SQP.js +34 -0
- package/dist/chunk-4BWDGEXH.js +5 -0
- package/dist/chunk-56SEAGAQ.js +1 -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-AGGIYXN7.js +2 -0
- package/dist/chunk-BHVBO4SC.js +1 -0
- package/dist/chunk-CGHISVXB.js +1 -0
- package/dist/chunk-FPXVDP32.js +8 -0
- package/dist/chunk-FRGMJPNO.js +2 -0
- package/dist/chunk-GXB7RZHH.js +1 -0
- package/dist/chunk-ILDDDLRC.js +1 -0
- package/dist/chunk-JFYVRWJI.js +2 -0
- package/dist/chunk-MTNY7MFG.js +6 -0
- package/dist/chunk-MVLCGU3G.js +1 -0
- package/dist/chunk-N345KWWK.js +1 -0
- package/dist/chunk-ORJ2QZBG.js +3 -0
- package/dist/chunk-PCALBZOO.js +2 -0
- package/dist/chunk-PIEAJJRE.js +5 -0
- package/dist/chunk-RDRF7DIS.js +109 -0
- package/dist/chunk-TEW62VDF.js +1 -0
- package/dist/chunk-TSEKPGK4.js +1 -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-VICEM5Y2.js +2 -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-WZKWT757.js +2 -0
- package/dist/main.js +94 -536
- package/package.json +16 -63
- 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 -120
- 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 -4757
- 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/hud-line.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { formatAge } from "./workflow-progress.js";
|
|
2
|
-
import { levelInk } from "./level-style.js";
|
|
3
|
-
import { fg } from "./theme.js";
|
|
4
|
-
const RESET = "\x1b[0m";
|
|
5
|
-
const DIM = "\x1b[2m";
|
|
6
|
-
const BOLD = "\x1b[1m";
|
|
7
|
-
const RED = "\x1b[31m";
|
|
8
|
-
const GREEN = "\x1b[32m";
|
|
9
|
-
const YELLOW = "\x1b[33m";
|
|
10
|
-
const MAGENTA = "\x1b[35m";
|
|
11
|
-
const CYAN = "\x1b[36m";
|
|
12
|
-
function paint(text, color) {
|
|
13
|
-
return `${color}${text}${RESET}`;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Paint a thinking level in its own colour.
|
|
17
|
-
*
|
|
18
|
-
* Goes through `fg()` rather than emitting an escape here, so the level obeys the
|
|
19
|
-
* same colour mode as the rest of the interface: on a terminal without 24-bit
|
|
20
|
-
* colour it becomes the nearest 256-cube index instead of an escape the terminal
|
|
21
|
-
* ignores. Split out from `sessionLine` because the caller may already have styled
|
|
22
|
-
* the level (a shimmer band sweeps it at the ceiling) and must pass that through
|
|
23
|
-
* untouched — re-colouring a string that already carries escapes nests them.
|
|
24
|
-
*/
|
|
25
|
-
export function paintLevel(text, level) {
|
|
26
|
-
return fg(text, levelInk(level));
|
|
27
|
-
}
|
|
28
|
-
function contextColor(percent) {
|
|
29
|
-
if (percent >= 85)
|
|
30
|
-
return RED;
|
|
31
|
-
if (percent >= 70)
|
|
32
|
-
return YELLOW;
|
|
33
|
-
return GREEN;
|
|
34
|
-
}
|
|
35
|
-
/**
|
|
36
|
-
* The mark before the thinking level, in place of the word "thinking".
|
|
37
|
-
*
|
|
38
|
-
* Not 🧠: U+1F9E0 carries the Unicode Emoji property and is present in **no**
|
|
39
|
-
* monospace font on this box (only Apple Color Emoji and LastResort), so it
|
|
40
|
-
* renders as a colour glyph at best and tofu at worst. `✦` (U+2726) is outside
|
|
41
|
-
* the Emoji set, comes from Menlo/Courier and the other monospace faces here, and
|
|
42
|
-
* reads as "the model is thinking" without a word.
|
|
43
|
-
*/
|
|
44
|
-
export const THINKING_MARK = "✦";
|
|
45
|
-
/**
|
|
46
|
-
* The session row: model, thinking level, project, branch.
|
|
47
|
-
*
|
|
48
|
-
* `thinking` arrives already styled. That is the caller's job because the level
|
|
49
|
-
* has two looks — its own colour, and a shimmer band when it is at the model's
|
|
50
|
-
* ceiling — and only the caller knows which one applies. What this function owns
|
|
51
|
-
* is the mark and where the level sits on the line.
|
|
52
|
-
*/
|
|
53
|
-
export function sessionLine(model, thinking, folder, git) {
|
|
54
|
-
const badge = paint(`[${model}]`, CYAN);
|
|
55
|
-
const level = `${paint(THINKING_MARK, DIM)} ${thinking}`;
|
|
56
|
-
const project = paint(folder, YELLOW);
|
|
57
|
-
const branch = git ? ` ${paint("git:(", MAGENTA)}${paint(git.replace(/^git:\(/, "").replace(/\)$/, ""), CYAN)}${paint(")", MAGENTA)}` : "";
|
|
58
|
-
return `${badge}${paint(" │ ", DIM)}${level}${paint(" │ ", DIM)}${project}${branch}`;
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Prefix a HUD row with the running spinner and how long it has been running.
|
|
62
|
-
*
|
|
63
|
-
* It sits on the row that already carries the model, so the two facts that change
|
|
64
|
-
* during a run — what the model is, and that it is working — are read together,
|
|
65
|
-
* and the spinner glyph is painted here, at draw time, from the caller's clock. A
|
|
66
|
-
* glyph captured when an event arrived is frozen exactly while the model is
|
|
67
|
-
* quiet, which is the one stretch a spinner is for.
|
|
68
|
-
*
|
|
69
|
-
* `waitedMs` is a duration, not a formatted string: the unit switch (seconds →
|
|
70
|
-
* minutes → hours) belongs with the rest of the duration formatting, and this
|
|
71
|
-
* function is about where the clock sits on the line.
|
|
72
|
-
*/
|
|
73
|
-
export function withSpinner(line, frame, waitedMs) {
|
|
74
|
-
if (!frame)
|
|
75
|
-
return line;
|
|
76
|
-
const clock = waitedMs === undefined ? frame : `${frame} ${paint(formatAge(waitedMs), DIM)}`;
|
|
77
|
-
return `${paint(clock, CYAN)} ${paint("│", DIM)} ${line}`;
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* The context meter and whatever segments belong beside it.
|
|
81
|
-
*
|
|
82
|
-
* Segments are variadic and empty ones are dropped, so a caller passes the
|
|
83
|
-
* rows it has and never has to know which of them are showing anything right
|
|
84
|
-
* now: the balance arrives asynchronously and the spend is zero until the
|
|
85
|
-
* first turn, and both would otherwise need a conditional at every call site.
|
|
86
|
-
*/
|
|
87
|
-
export function contextLine(percent, ...segments) {
|
|
88
|
-
const safe = Number.isFinite(percent) ? Math.min(100, Math.max(0, percent)) : 0;
|
|
89
|
-
const filled = Math.round(safe / 10);
|
|
90
|
-
const color = contextColor(safe);
|
|
91
|
-
const bar = `${color}${"█".repeat(filled)}${DIM}${"░".repeat(10 - filled)}${RESET}`;
|
|
92
|
-
const value = paint(`${Math.round(safe)}%`, color);
|
|
93
|
-
const balance = segments.filter(Boolean).join(paint(" │ ", DIM));
|
|
94
|
-
return `${paint("Context", DIM)} ${bar} ${value}${balance ? `${paint(" │ ", DIM)}${balance}` : ""}`;
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* The session's spend so far, as a segment of the context row.
|
|
98
|
-
*
|
|
99
|
-
* A fresh session draws nothing: "$0.000" is a number nobody asked for, and a
|
|
100
|
-
* value that is always present is a value that stops being read. The precision
|
|
101
|
-
* is the SDK footer's (`$0.123`) rather than `/stats`'s four digits - this is
|
|
102
|
-
* read at a glance while the model works, and a session rarely spends enough
|
|
103
|
-
* for the last digit to mean anything.
|
|
104
|
-
*/
|
|
105
|
-
export function costSegment(costUsd) {
|
|
106
|
-
if (!Number.isFinite(costUsd) || costUsd <= 0)
|
|
107
|
-
return "";
|
|
108
|
-
return `${paint("Spent", DIM)} ${paint(`$${costUsd.toFixed(3)}`, GREEN)}`;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* The `/usage` panel: the account's quota as a bar, the way the context line
|
|
112
|
-
* shows the window.
|
|
113
|
-
*
|
|
114
|
-
* A number alone ("6.50 USD left") makes the reader do the division before they
|
|
115
|
-
* can tell whether it matters. The bar is the answer to that question, and the
|
|
116
|
-
* percentage is its exact form; the two belong together for the same reason the
|
|
117
|
-
* context line carries both.
|
|
118
|
-
*
|
|
119
|
-
* The thresholds are `contextColor`'s, so "how full is this" reads the same on
|
|
120
|
-
* both rows: green under 70%, yellow to 84%, red at 85% and above. A quota that
|
|
121
|
-
* is 85% spent is the one worth acting on, and a colour that meant something
|
|
122
|
-
* different here would be worse than no colour.
|
|
123
|
-
*
|
|
124
|
-
* There is deliberately no reset countdown. Claude Code has one because its
|
|
125
|
-
* windows expire on a schedule; howtofix.id reports a balance and no window
|
|
126
|
-
* end, so a countdown would have to be invented rather than read.
|
|
127
|
-
*/
|
|
128
|
-
export function quotaLines(provider, limit, used, unit) {
|
|
129
|
-
const safeLimit = Number.isFinite(limit) && limit > 0 ? limit : 0;
|
|
130
|
-
const safeUsed = Number.isFinite(used) ? Math.max(0, used) : 0;
|
|
131
|
-
const remaining = Math.max(0, safeLimit - safeUsed);
|
|
132
|
-
// A zero limit is the unlimited case at the call site, but a division here
|
|
133
|
-
// would still produce NaN and paint "NaN%" on the row.
|
|
134
|
-
const percent = safeLimit > 0 ? Math.min(100, (safeUsed / safeLimit) * 100) : 0;
|
|
135
|
-
const filled = Math.round(percent / 10);
|
|
136
|
-
const color = contextColor(percent);
|
|
137
|
-
const bar = `${color}${"█".repeat(filled)}${DIM}${"░".repeat(10 - filled)}${RESET}`;
|
|
138
|
-
const money = (value) => `${value.toFixed(2)} ${unit}`;
|
|
139
|
-
// The widths are fixed so the numbers line up between the two states of the
|
|
140
|
-
// panel; a ragged column is harder to scan than a padded one.
|
|
141
|
-
return [
|
|
142
|
-
`${paint(provider, BOLD)}${paint(" account", DIM)}`,
|
|
143
|
-
` ${paint("used", DIM)} ${bar} ${paint(`${percent.toFixed(1)}%`, color)}`,
|
|
144
|
-
` ${paint("granted", DIM)} ${money(safeLimit)}`,
|
|
145
|
-
` ${paint("spent", DIM)} ${money(safeUsed)}`,
|
|
146
|
-
` ${paint("remaining", DIM)} ${paint(money(remaining), color)}`,
|
|
147
|
-
];
|
|
148
|
-
}
|
|
149
|
-
export function usageSegment(balance, unit, latencyMs) {
|
|
150
|
-
const latency = latencyMs === null ? "" : paint(` ${latencyMs}ms`, DIM);
|
|
151
|
-
const label = unit && unit !== "USD" ? `${unit} ` : "";
|
|
152
|
-
return `${paint("Balance", DIM)} ${paint(`${label}${balance}`, GREEN)}${latency}`;
|
|
153
|
-
}
|
package/dist/hwid.js
DELETED
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The machine's own id, where the OS keeps one.
|
|
3
|
-
*
|
|
4
|
-
* The panel already receives a fingerprint, but until now that fingerprint was
|
|
5
|
-
* built from the hostname, the platform, the username and the CPU model — none
|
|
6
|
-
* of which is hardware. Two different machines built from the same image
|
|
7
|
-
* produce the same value, and one machine changes it by renaming itself. That
|
|
8
|
-
* leaves the panel's device list unable to answer the one question it exists
|
|
9
|
-
* for: is this the same computer as before, or a different one?
|
|
10
|
-
*
|
|
11
|
-
* Measured on the live panel: one physical box had already produced two device
|
|
12
|
-
* sessions and two access tokens under the same fingerprint, because each
|
|
13
|
-
* sign-in mints a fresh `device_id` and nothing tied it back to the machine.
|
|
14
|
-
*
|
|
15
|
-
* So this module reads the identifier the OS keeps for the machine itself:
|
|
16
|
-
*
|
|
17
|
-
* linux /etc/machine-id, then /var/lib/dbus/machine-id, then the DMI
|
|
18
|
-
* product UUID (a firmware value, not a name)
|
|
19
|
-
* win32 HKLM\SOFTWARE\Microsoft\Cryptography\MachineGuid
|
|
20
|
-
* darwin IOPlatformUUID from `ioreg`
|
|
21
|
-
*
|
|
22
|
-
* The value never leaves the process raw. What is hashed into the fingerprint
|
|
23
|
-
* is an HMAC over it, so the panel can match a machine across runs without ever
|
|
24
|
-
* being handed the machine's serial number.
|
|
25
|
-
*
|
|
26
|
-
* The honest limits, stated where they belong:
|
|
27
|
-
*
|
|
28
|
-
* - A container that mounts no machine-id and exposes no DMI falls back to the
|
|
29
|
-
* hostname hash, and that is weak. The source is reported rather than
|
|
30
|
-
* hidden, so a weak id is visibly weak instead of silently indistinguishable
|
|
31
|
-
* from a strong one.
|
|
32
|
-
* - Anyone with root can change /etc/machine-id. This raises the cost of
|
|
33
|
-
* passing one machine off as many; it is not a wall. A wall needs a secret
|
|
34
|
-
* the panel issues once, which is what the device session already is.
|
|
35
|
-
*/
|
|
36
|
-
import { execFileSync } from "node:child_process";
|
|
37
|
-
import { createHmac } from "node:crypto";
|
|
38
|
-
import { readFileSync } from "node:fs";
|
|
39
|
-
import { arch, cpus, hostname, platform, userInfo } from "node:os";
|
|
40
|
-
/**
|
|
41
|
-
* Domain separation for the HMAC. A digest computed for one purpose must never
|
|
42
|
-
* be presentable as another, and this keeps the hwid out of the same space as
|
|
43
|
-
* the panel's other HMACs.
|
|
44
|
-
*/
|
|
45
|
-
const HWID_DOMAIN = "howtofix-cli-hwid-v1";
|
|
46
|
-
/** Read a file and return its trimmed contents, or undefined. Never throws. */
|
|
47
|
-
function readTrimmed(path) {
|
|
48
|
-
try {
|
|
49
|
-
const value = readFileSync(path, "utf8").trim();
|
|
50
|
-
return value.length > 0 ? value : undefined;
|
|
51
|
-
}
|
|
52
|
-
catch {
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
/** Run a command and return its stdout, or undefined. Never throws, never waits long. */
|
|
57
|
-
function run(command, args) {
|
|
58
|
-
try {
|
|
59
|
-
const out = execFileSync(command, args, { encoding: "utf8", timeout: 3_000, stdio: ["ignore", "pipe", "ignore"] });
|
|
60
|
-
return out.trim().length > 0 ? out.trim() : undefined;
|
|
61
|
-
}
|
|
62
|
-
catch {
|
|
63
|
-
return undefined;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* The raw machine identifier, with its source, or undefined when the OS exposes
|
|
68
|
-
* none.
|
|
69
|
-
*
|
|
70
|
-
* Order matters: a machine-id is the OS's own identity for the install, a DMI
|
|
71
|
-
* product UUID is the firmware's for the board, and both outrank a name.
|
|
72
|
-
*/
|
|
73
|
-
export function readMachineIdentity() {
|
|
74
|
-
const os = platform();
|
|
75
|
-
if (os === "linux") {
|
|
76
|
-
// The canonical install id. Present on systemd hosts and on most container
|
|
77
|
-
// images; on a host without it, dbus keeps its own copy of the same value.
|
|
78
|
-
const machineId = readTrimmed("/etc/machine-id");
|
|
79
|
-
if (machineId)
|
|
80
|
-
return { value: machineId, source: "machine-id", strong: true };
|
|
81
|
-
const dbusId = readTrimmed("/var/lib/dbus/machine-id");
|
|
82
|
-
if (dbusId)
|
|
83
|
-
return { value: dbusId, source: "dbus-machine-id", strong: true };
|
|
84
|
-
// The firmware's id for the board. Readable without root on most kernels.
|
|
85
|
-
const productUuid = readTrimmed("/sys/class/dmi/id/product_uuid");
|
|
86
|
-
if (productUuid)
|
|
87
|
-
return { value: productUuid, source: "dmi-product-uuid", strong: true };
|
|
88
|
-
return undefined;
|
|
89
|
-
}
|
|
90
|
-
if (os === "win32") {
|
|
91
|
-
// The value Windows itself uses as the machine identity. `reg query` prints
|
|
92
|
-
// " MachineGuid REG_SZ <guid>", so the guid is the last field.
|
|
93
|
-
const out = run("reg", ["query", "HKLM\\SOFTWARE\\Microsoft\\Cryptography", "/v", "MachineGuid"]);
|
|
94
|
-
const match = out?.match(/MachineGuid\s+REG_SZ\s+(\S+)/i);
|
|
95
|
-
if (match?.[1])
|
|
96
|
-
return { value: match[1], source: "windows-machine-guid", strong: true };
|
|
97
|
-
return undefined;
|
|
98
|
-
}
|
|
99
|
-
if (os === "darwin") {
|
|
100
|
-
// IOPlatformUUID is the Mac's hardware identity.
|
|
101
|
-
const out = run("ioreg", ["-rd1", "-c", "IOPlatformExpertDevice"]);
|
|
102
|
-
const match = out?.match(/"IOPlatformUUID"\s*=\s*"([^"]+)"/);
|
|
103
|
-
if (match?.[1])
|
|
104
|
-
return { value: match[1], source: "platform-uuid", strong: true };
|
|
105
|
-
return undefined;
|
|
106
|
-
}
|
|
107
|
-
return undefined;
|
|
108
|
-
}
|
|
109
|
-
/**
|
|
110
|
-
* The machine id this build reports, with the strength of its source.
|
|
111
|
-
*
|
|
112
|
-
* The fallback is the same hostname/platform/CPU tuple the fingerprint used
|
|
113
|
-
* before this module existed, so a machine with no OS identity behaves exactly
|
|
114
|
-
* as it did — but it is now labelled `strong: false` instead of being
|
|
115
|
-
* indistinguishable from a hardware id.
|
|
116
|
-
*/
|
|
117
|
-
export function resolveHwid() {
|
|
118
|
-
const identity = readMachineIdentity();
|
|
119
|
-
const raw = identity?.value ?? [hostname(), platform(), arch(), userInfo().username].join("\u0000");
|
|
120
|
-
const source = identity?.source ?? "hostname";
|
|
121
|
-
return {
|
|
122
|
-
id: createHmac("sha256", HWID_DOMAIN).update(raw).digest("hex").slice(0, 32),
|
|
123
|
-
source,
|
|
124
|
-
strong: identity?.strong ?? false,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
/** The parts the fingerprint hashes, with the machine id first so it dominates. */
|
|
128
|
-
export function fingerprintParts(hwid) {
|
|
129
|
-
return [hwid.id, hwid.source, hostname(), platform(), arch(), userInfo().username, String(cpus().length), cpus()[0]?.model ?? ""];
|
|
130
|
-
}
|
package/dist/interrupt.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Whether a raw terminal sequence is a Ctrl+C, in every form a terminal sends it.
|
|
3
|
-
*
|
|
4
|
-
* The workbench runs in raw mode and handles Ctrl+C itself, so Node never sees
|
|
5
|
-
* the SIGINT it would otherwise raise. That only holds for the form the key
|
|
6
|
-
* matcher recognises. Three forms are in use:
|
|
7
|
-
*
|
|
8
|
-
* - the raw byte 0x03, which is what a terminal sends with no keyboard
|
|
9
|
-
* protocol negotiated;
|
|
10
|
-
* - Kitty's CSI-u sequence, `\x1b[<codepoint>;<modifier>u`, where 99 is "c"
|
|
11
|
-
* and 5 is the ctrl modifier;
|
|
12
|
-
* - xterm's modifyOtherKeys, `\x1b[27;<modifier>;<codepoint>~`, same numbers.
|
|
13
|
-
*
|
|
14
|
-
* A Windows terminal that reports the enhanced form sends a sequence the key
|
|
15
|
-
* matcher does not know. The byte never matches, the handler never consumes it,
|
|
16
|
-
* and the process is left to Node's default, which is to exit. That is the
|
|
17
|
-
* close that looks like a Ctrl+C nobody pressed twice.
|
|
18
|
-
*
|
|
19
|
-
* The check is deliberately wider than the key matcher: a false positive here
|
|
20
|
-
* costs one keystroke, a miss costs the whole session.
|
|
21
|
-
*/
|
|
22
|
-
const CTRL_C_BYTE = String.fromCharCode(3);
|
|
23
|
-
/** "c" is codepoint 99, and the ctrl modifier is reported as 5. */
|
|
24
|
-
const CTRL_MODIFIER = 5;
|
|
25
|
-
const C_CODEPOINT = 99;
|
|
26
|
-
export function isInterrupt(data) {
|
|
27
|
-
if (data === CTRL_C_BYTE)
|
|
28
|
-
return true;
|
|
29
|
-
// Kitty keyboard protocol: CSI codepoint ; modifier u.
|
|
30
|
-
const kitty = /^\x1b\[(\d+);(\d+)u$/.exec(data);
|
|
31
|
-
if (kitty && Number(kitty[1]) === C_CODEPOINT && (Number(kitty[2]) & CTRL_MODIFIER) !== 0)
|
|
32
|
-
return true;
|
|
33
|
-
// xterm modifyOtherKeys: CSI 27 ; modifier ; codepoint ~.
|
|
34
|
-
const modifyOther = /^\x1b\[27;(\d+);(\d+)~$/.exec(data);
|
|
35
|
-
if (modifyOther && Number(modifyOther[2]) === C_CODEPOINT && (Number(modifyOther[1]) & CTRL_MODIFIER) !== 0)
|
|
36
|
-
return true;
|
|
37
|
-
return false;
|
|
38
|
-
}
|
|
39
|
-
/**
|
|
40
|
-
* Keep a SIGINT from killing the process outright.
|
|
41
|
-
*
|
|
42
|
-
* Raw mode is supposed to stop the terminal sending SIGINT at all, but a
|
|
43
|
-
* Windows console and a terminal that resets its mode on resize both still do.
|
|
44
|
-
* Node's default for an unhandled SIGINT is to exit immediately, which skips
|
|
45
|
-
* the shutdown path: no MCP servers are stopped, no session is disposed, and
|
|
46
|
-
* the screen is left in raw mode. Handing it to the same function a keypress
|
|
47
|
-
* uses means the close looks the same however it arrived.
|
|
48
|
-
*
|
|
49
|
-
* Idempotent: a second signal while the first shutdown is running must not
|
|
50
|
-
* start another.
|
|
51
|
-
*/
|
|
52
|
-
export function catchInterrupt(onInterrupt) {
|
|
53
|
-
let handled = false;
|
|
54
|
-
process.on("SIGINT", () => {
|
|
55
|
-
if (handled)
|
|
56
|
-
return;
|
|
57
|
-
handled = true;
|
|
58
|
-
onInterrupt();
|
|
59
|
-
});
|
|
60
|
-
}
|
package/dist/json-events.js
DELETED
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
function isAssistantMessageUpdate(event) {
|
|
2
|
-
return event.type === "message_update";
|
|
3
|
-
}
|
|
4
|
-
/**
|
|
5
|
-
* A delta event, with the cumulative `partial` removed. `toolcall_start` keeps
|
|
6
|
-
* the tool's id and name, which the delta itself does not carry.
|
|
7
|
-
*/
|
|
8
|
-
function toJsonAssistantMessageEvent(event) {
|
|
9
|
-
if (event.type === "toolcall_start") {
|
|
10
|
-
const index = event.contentIndex;
|
|
11
|
-
const part = index === undefined ? undefined : event.partial?.content[index];
|
|
12
|
-
const toolCall = part;
|
|
13
|
-
if (toolCall?.type !== "toolCall") {
|
|
14
|
-
// Dropped rather than thrown: see the module comment.
|
|
15
|
-
return undefined;
|
|
16
|
-
}
|
|
17
|
-
const { partial: _partial, ...delta } = event;
|
|
18
|
-
return { ...delta, id: toolCall.id, toolName: toolCall.name };
|
|
19
|
-
}
|
|
20
|
-
if (!("partial" in event))
|
|
21
|
-
return event;
|
|
22
|
-
const { partial: _partial, ...delta } = event;
|
|
23
|
-
return delta;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* One session event in the wire shape. Returns `undefined` for the events that
|
|
27
|
-
* cannot be represented (a malformed update), so the caller skips them instead
|
|
28
|
-
* of writing a line that is not JSON.
|
|
29
|
-
*/
|
|
30
|
-
export function toJsonEvent(event) {
|
|
31
|
-
if (!isAssistantMessageUpdate(event))
|
|
32
|
-
return event;
|
|
33
|
-
if (event.message.role !== "assistant")
|
|
34
|
-
return undefined;
|
|
35
|
-
const assistantMessageEvent = toJsonAssistantMessageEvent(event.assistantMessageEvent);
|
|
36
|
-
if (assistantMessageEvent === undefined)
|
|
37
|
-
return undefined;
|
|
38
|
-
return {
|
|
39
|
-
type: "message_update",
|
|
40
|
-
usage: event.message.usage,
|
|
41
|
-
assistantMessageEvent,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
/** The line to write for an event, or `undefined` when it has no wire shape. */
|
|
45
|
-
export function jsonLine(event) {
|
|
46
|
-
const shaped = toJsonEvent(event);
|
|
47
|
-
if (shaped === undefined)
|
|
48
|
-
return undefined;
|
|
49
|
-
return `${JSON.stringify(shaped)}\n`;
|
|
50
|
-
}
|
package/dist/level-style.js
DELETED
|
@@ -1,112 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* How a thinking level looks: its colour, and how it answers a change.
|
|
3
|
-
*
|
|
4
|
-
* The level used to be one thing — the word, painted dim, identical at `off` and
|
|
5
|
-
* at `max`. That is a reading problem, not a decoration problem: the level is the
|
|
6
|
-
* one setting that changes what every request costs, and the HUD is where it is
|
|
7
|
-
* read. Colouring it means a glance tells you where you are.
|
|
8
|
-
*
|
|
9
|
-
* Three rules hold this together, and each of them is here because the obvious
|
|
10
|
-
* alternative is wrong:
|
|
11
|
-
*
|
|
12
|
-
* 1. **The word stays.** Colour is a second cue, never the only one. The word is
|
|
13
|
-
* legible in a monochrome terminal, in `screen`, in a screenshot, and to a
|
|
14
|
-
* reader with red-green colour vision deficiency — the colour is not.
|
|
15
|
-
*
|
|
16
|
-
* 2. **The ramp is in OKHSL, not RGB.** Interpolating hue in RGB is what produces
|
|
17
|
-
* the muddy, "off" middle that makes a gradient look cheap. OKHSL keeps
|
|
18
|
-
* perceptual lightness even as hue swings from blue to orange, which is why the
|
|
19
|
-
* steps below read as one family rather than as seven unrelated colours.
|
|
20
|
-
*
|
|
21
|
-
* 3. **No new glyphs.** A gauge of dots beside the level was the obvious design
|
|
22
|
-
* and it was measured and dropped: the HUD line already reaches 89 columns with
|
|
23
|
-
* a real model name, so a four-column gauge pushes it past 80 and wraps the
|
|
24
|
-
* footer on a default terminal. The level's own width is already paid for.
|
|
25
|
-
*
|
|
26
|
-
* The 256-colour indices below were not chosen by eye — they were computed by
|
|
27
|
-
* quantising each value through the terminal's own cube, because the first
|
|
28
|
-
* hand-picked ramp collapsed to *six* distinct colours with `medium` and `high`
|
|
29
|
-
* both landing on 68. A ramp that is beautiful in truecolor and ambiguous in 256
|
|
30
|
-
* is a regression for exactly the terminals this had to stay safe on.
|
|
31
|
-
*/
|
|
32
|
-
import { parseColor } from "@netrodev/hxf-tui";
|
|
33
|
-
/**
|
|
34
|
-
* One colour per level, low to high: a desaturated slate at `off`, then blue →
|
|
35
|
-
* indigo → violet → magenta → orange.
|
|
36
|
-
*
|
|
37
|
-
* `off` is deliberately outside the ramp. It is not "the lowest amount of
|
|
38
|
-
* thinking", it is thinking switched off, and giving it the ramp's first hue
|
|
39
|
-
* would invite reading it as a low setting.
|
|
40
|
-
*/
|
|
41
|
-
const LEVEL_INK = {
|
|
42
|
-
// `off` is lifted to 46% rather than the ramp's darker end: at 36% it measured a
|
|
43
|
-
// 2.35:1 contrast ratio against the panel background, which is below the 3:1
|
|
44
|
-
// floor for legible text — the one level that is *most* often the current one was
|
|
45
|
-
// also the hardest to read.
|
|
46
|
-
off: parseColor("okhsl(230 12% 46%)"),
|
|
47
|
-
minimal: parseColor("okhsl(240 55% 49%)"),
|
|
48
|
-
low: parseColor("okhsl(252 60% 55%)"),
|
|
49
|
-
medium: parseColor("okhsl(270 64% 60%)"),
|
|
50
|
-
high: parseColor("okhsl(298 70% 64%)"),
|
|
51
|
-
xhigh: parseColor("okhsl(332 80% 66%)"),
|
|
52
|
-
max: parseColor("okhsl(42 95% 64%)"),
|
|
53
|
-
};
|
|
54
|
-
/**
|
|
55
|
-
* The same hues, lifted and desaturated: what a level flashes as the moment it
|
|
56
|
-
* changes.
|
|
57
|
-
*
|
|
58
|
-
* A change has to be visible without the word being re-read, and the change is
|
|
59
|
-
* often a single keypress. Mixing toward this rather than toward white keeps the
|
|
60
|
-
* flash inside the ramp's own character — a white flash is how several themes
|
|
61
|
-
* read an error, and raising the effort is not an error.
|
|
62
|
-
*/
|
|
63
|
-
const LEVEL_FLASH = {
|
|
64
|
-
off: parseColor("okhsl(230 14% 64%)"),
|
|
65
|
-
minimal: parseColor("okhsl(240 60% 64%)"),
|
|
66
|
-
low: parseColor("okhsl(252 64% 68%)"),
|
|
67
|
-
medium: parseColor("okhsl(270 68% 72%)"),
|
|
68
|
-
high: parseColor("okhsl(298 74% 76%)"),
|
|
69
|
-
xhigh: parseColor("okhsl(332 82% 78%)"),
|
|
70
|
-
max: parseColor("okhsl(42 96% 76%)"),
|
|
71
|
-
};
|
|
72
|
-
/** The colour a level is drawn in. Unknown names fall back to `medium`. */
|
|
73
|
-
export function levelInk(level) {
|
|
74
|
-
return LEVEL_INK[level] ?? LEVEL_INK.medium;
|
|
75
|
-
}
|
|
76
|
-
/**
|
|
77
|
-
* The ramp as a sequence, low to high, for surfaces that want to show the whole
|
|
78
|
-
* ladder at once rather than one rung of it.
|
|
79
|
-
*
|
|
80
|
-
* `off` is not in it: it is not the bottom of the ladder, it is the ladder put
|
|
81
|
-
* away, and a spectrum that started at a desaturated slate would read as a dim
|
|
82
|
-
* ramp rather than as "no thinking".
|
|
83
|
-
*/
|
|
84
|
-
export const LEVEL_RAMP = [LEVEL_INK.minimal, LEVEL_INK.low, LEVEL_INK.medium, LEVEL_INK.high, LEVEL_INK.xhigh, LEVEL_INK.max];
|
|
85
|
-
/** The colour a level flashes toward while a change is still fresh. */
|
|
86
|
-
export function levelFlash(level) {
|
|
87
|
-
return LEVEL_FLASH[level] ?? LEVEL_FLASH.medium;
|
|
88
|
-
}
|
|
89
|
-
/**
|
|
90
|
-
* How long a level change stays lit.
|
|
91
|
-
*
|
|
92
|
-
* The glint crosses a span of `word + 2 * GLIMMER_PAD` characters, one character
|
|
93
|
-
* per frame, so the travel is `span * tick`. The workbench repaints at 30 fps, so
|
|
94
|
-
* a six-letter level is 14 steps of 33 ms — about 460 ms, with the light on the
|
|
95
|
-
* word for most of it. The first version was 320 ms, set for a blurred band,
|
|
96
|
-
* which made the sharper one a flicker — long enough to see that something
|
|
97
|
-
* happened, too short to see what it was. It still finishes well inside the pause
|
|
98
|
-
* between two keystrokes of a run up the ladder.
|
|
99
|
-
*/
|
|
100
|
-
export const LEVEL_FLASH_MS = 460;
|
|
101
|
-
/**
|
|
102
|
-
* How far through the flash we are, 1 at the instant of the change down to 0.
|
|
103
|
-
*
|
|
104
|
-
* `ageMs` is passed in rather than read from the clock so the caller owns time:
|
|
105
|
-
* this is called from `render()`, which must not depend on when it was called for
|
|
106
|
-
* anything but the effect.
|
|
107
|
-
*/
|
|
108
|
-
export function flashLevel(ageMs) {
|
|
109
|
-
if (!Number.isFinite(ageMs) || ageMs < 0 || ageMs >= LEVEL_FLASH_MS)
|
|
110
|
-
return 0;
|
|
111
|
-
return 1 - ageMs / LEVEL_FLASH_MS;
|
|
112
|
-
}
|
package/dist/loop-guard.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stops an agent from repeating the same tool call until the budget is gone.
|
|
3
|
-
*
|
|
4
|
-
* Why this exists: every harness in this space has the same field report. The
|
|
5
|
-
* most-discussed agent-quality complaints on the competitor trackers are loops,
|
|
6
|
-
* not crashes — "stuck in loop of verification", "gemini stuck in thinking loop
|
|
7
|
-
* for hours", "agent repeatedly runs broad searches causing massive context
|
|
8
|
-
* overflow", "auto-compaction infinite loop". A model that re-issues an identical
|
|
9
|
-
* call is not making progress, and nothing in the SDK or the session stops it: it
|
|
10
|
-
* will happily burn the whole context and the whole budget first.
|
|
11
|
-
*
|
|
12
|
-
* The shape: guard the same failure with a small retry cap and a "no-progress
|
|
13
|
-
* dead end" that gives up instead of retrying forever. Applied to tool calls, which
|
|
14
|
-
* is where the user actually pays for it.
|
|
15
|
-
*
|
|
16
|
-
* The rule: the same tool called with the same arguments `REPEAT_LIMIT` times in
|
|
17
|
-
* a row is refused, with a reason that says what to do instead. A call with
|
|
18
|
-
* different arguments is progress and resets that tool's counter, and the whole
|
|
19
|
-
* guard resets when a new run starts, so repeating something across two prompts
|
|
20
|
-
* is fine.
|
|
21
|
-
*
|
|
22
|
-
* Deliberately exempt: `think`. It is a pure reasoning channel with no side
|
|
23
|
-
* effect, and re-using the same thought is sometimes how a model gets unstuck.
|
|
24
|
-
*
|
|
25
|
-
* Pure state, no session needed — the policy is testable on its own.
|
|
26
|
-
*/
|
|
27
|
-
/** Consecutive identical calls allowed before the guard refuses. */
|
|
28
|
-
export const REPEAT_LIMIT = 3;
|
|
29
|
-
/** Tools whose repetition is harmless and sometimes deliberate. */
|
|
30
|
-
const EXEMPT = new Set(["think"]);
|
|
31
|
-
/**
|
|
32
|
-
* The last non-exempt call, and how many times in a row it has been issued.
|
|
33
|
-
*
|
|
34
|
-
* Only *uninterrupted* repetition counts. A single counter (rather than one per
|
|
35
|
-
* tool) is deliberate: `read → edit → read` to verify an edit is a normal
|
|
36
|
-
* workflow, and a per-tool counter would see two reads and keep counting toward
|
|
37
|
-
* a refusal. Any call that differs from the last one is progress and restarts
|
|
38
|
-
* the chain.
|
|
39
|
-
*/
|
|
40
|
-
let last;
|
|
41
|
-
/**
|
|
42
|
-
* A stable key for a call. Argument key order is normalised, because the same
|
|
43
|
-
* call can arrive with its keys in a different order and must still count as a
|
|
44
|
-
* repeat.
|
|
45
|
-
*/
|
|
46
|
-
function signatureOf(input) {
|
|
47
|
-
const seen = new WeakSet();
|
|
48
|
-
const normalise = (value) => {
|
|
49
|
-
if (value === null || typeof value !== "object")
|
|
50
|
-
return value;
|
|
51
|
-
if (seen.has(value))
|
|
52
|
-
return "[circular]";
|
|
53
|
-
seen.add(value);
|
|
54
|
-
if (Array.isArray(value))
|
|
55
|
-
return value.map(normalise);
|
|
56
|
-
const record = value;
|
|
57
|
-
const sorted = {};
|
|
58
|
-
for (const key of Object.keys(record).sort())
|
|
59
|
-
sorted[key] = normalise(record[key]);
|
|
60
|
-
return sorted;
|
|
61
|
-
};
|
|
62
|
-
try {
|
|
63
|
-
return JSON.stringify(normalise(input)) ?? "";
|
|
64
|
-
}
|
|
65
|
-
catch {
|
|
66
|
-
return "";
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Record a call and report whether it has now repeated too often.
|
|
71
|
-
*
|
|
72
|
-
* Returns `undefined` while the call may proceed. Once the same call has been
|
|
73
|
-
* issued `REPEAT_LIMIT` times without anything else in between, returns the
|
|
74
|
-
* count so the caller can refuse it and say why.
|
|
75
|
-
*/
|
|
76
|
-
export function noteToolCall(toolName, input) {
|
|
77
|
-
// An exempt tool is invisible to the guard: it neither counts as a repeat nor
|
|
78
|
-
// breaks a chain, so `think → read → think → read` is still a read loop.
|
|
79
|
-
if (EXEMPT.has(toolName))
|
|
80
|
-
return undefined;
|
|
81
|
-
const signature = `${toolName}\u0000${signatureOf(input)}`;
|
|
82
|
-
const count = last && last.signature === signature ? last.count + 1 : 1;
|
|
83
|
-
last = { signature, count };
|
|
84
|
-
return count >= REPEAT_LIMIT ? { repeats: count } : undefined;
|
|
85
|
-
}
|
|
86
|
-
/** Forget the chain. Called when a new run starts, so a new prompt starts clean. */
|
|
87
|
-
export function resetLoopGuard() {
|
|
88
|
-
last = undefined;
|
|
89
|
-
}
|
|
90
|
-
/** The refusal message. It must tell the model what to do differently, not just "no". */
|
|
91
|
-
export function repeatRefusal(toolName, repeats) {
|
|
92
|
-
return (`Refused: ${toolName} has been called ${repeats} times in a row with identical arguments, ` +
|
|
93
|
-
"so the last attempt cannot have changed anything. Do not repeat it. Either use different " +
|
|
94
|
-
"arguments, use a different tool, or stop and report what you found and what is blocking you.");
|
|
95
|
-
}
|
package/dist/lsp-tools.js
DELETED
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { StringEnum } from "@netrodev/hxf-ai";
|
|
2
|
-
import { defineTool } from "@netrodev/hxf-coding-agent";
|
|
3
|
-
import { Type } from "typebox";
|
|
4
|
-
import { askLsp } from "./lsp.js";
|
|
5
|
-
export const lspTool = defineTool({
|
|
6
|
-
name: "lsp",
|
|
7
|
-
label: "lsp",
|
|
8
|
-
description: "Language-server query: definition, references, hover, symbols, or diagnostics for a file. Line is 1-based. Does not edit. If the server binary is missing the result says so; do not pretend grep is a substitute without telling the user.",
|
|
9
|
-
promptSnippet: "lsp: definition, references, hover, symbols, diagnostics",
|
|
10
|
-
parameters: Type.Object({
|
|
11
|
-
action: StringEnum(["diagnostics", "definition", "references", "hover", "symbols"], {
|
|
12
|
-
description: "Which language-server query to run",
|
|
13
|
-
}),
|
|
14
|
-
file: Type.String({ description: "File to query, relative to the project root" }),
|
|
15
|
-
line: Type.Optional(Type.Number({ description: "1-based line (definition, references, hover)" })),
|
|
16
|
-
symbol: Type.Optional(Type.String({ description: "Substring to locate on that line when column is omitted" })),
|
|
17
|
-
}),
|
|
18
|
-
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
19
|
-
const result = await askLsp({
|
|
20
|
-
action: params.action,
|
|
21
|
-
file: params.file,
|
|
22
|
-
line: params.line,
|
|
23
|
-
symbol: params.symbol,
|
|
24
|
-
// The session project root: the server's rootUri and the child's cwd.
|
|
25
|
-
root: ctx.cwd,
|
|
26
|
-
});
|
|
27
|
-
if (!result.ok) {
|
|
28
|
-
return { content: [{ type: "text", text: result.error }], details: { error: result.error }, isError: true };
|
|
29
|
-
}
|
|
30
|
-
return { content: [{ type: "text", text: result.text }], details: { action: params.action, count: result.count } };
|
|
31
|
-
},
|
|
32
|
-
});
|