@ken-jo/agent-connector 0.1.0
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/LICENSE +202 -0
- package/NOTICE +6 -0
- package/README.md +348 -0
- package/dist/amp-QSFH2V7Y.js +217 -0
- package/dist/amp-QSFH2V7Y.js.map +1 -0
- package/dist/amp-TULUPXXL.js +237 -0
- package/dist/amp-TULUPXXL.js.map +1 -0
- package/dist/antigravity-CX2YLSY7.js +17 -0
- package/dist/antigravity-CX2YLSY7.js.map +1 -0
- package/dist/antigravity-STHZWMP3.js +171 -0
- package/dist/antigravity-STHZWMP3.js.map +1 -0
- package/dist/antigravity-cli-P6HADOAO.js +121 -0
- package/dist/antigravity-cli-P6HADOAO.js.map +1 -0
- package/dist/antigravity-cli-YQDFVV4U.js +86 -0
- package/dist/antigravity-cli-YQDFVV4U.js.map +1 -0
- package/dist/chunk-54EMKTTX.js +191 -0
- package/dist/chunk-54EMKTTX.js.map +1 -0
- package/dist/chunk-7L6ITC4T.js +54 -0
- package/dist/chunk-7L6ITC4T.js.map +1 -0
- package/dist/chunk-7MSDGHJW.js +316 -0
- package/dist/chunk-7MSDGHJW.js.map +1 -0
- package/dist/chunk-BCHWFTV4.js +196 -0
- package/dist/chunk-BCHWFTV4.js.map +1 -0
- package/dist/chunk-DBQMX75U.js +36 -0
- package/dist/chunk-DBQMX75U.js.map +1 -0
- package/dist/chunk-EXLS7BSI.js +29 -0
- package/dist/chunk-EXLS7BSI.js.map +1 -0
- package/dist/chunk-JNM3TPLX.js +593 -0
- package/dist/chunk-JNM3TPLX.js.map +1 -0
- package/dist/chunk-JWNT5KW7.js +197 -0
- package/dist/chunk-JWNT5KW7.js.map +1 -0
- package/dist/chunk-KHO5WNTP.js +30 -0
- package/dist/chunk-KHO5WNTP.js.map +1 -0
- package/dist/chunk-LTT56GI3.js +155 -0
- package/dist/chunk-LTT56GI3.js.map +1 -0
- package/dist/chunk-MEH7GC7R.js +111 -0
- package/dist/chunk-MEH7GC7R.js.map +1 -0
- package/dist/chunk-MQPNU2CY.js +64 -0
- package/dist/chunk-MQPNU2CY.js.map +1 -0
- package/dist/chunk-OPXAP3AC.js +12 -0
- package/dist/chunk-OPXAP3AC.js.map +1 -0
- package/dist/chunk-RK6VBLXP.js +50 -0
- package/dist/chunk-RK6VBLXP.js.map +1 -0
- package/dist/chunk-TIIP3RG2.js +420 -0
- package/dist/chunk-TIIP3RG2.js.map +1 -0
- package/dist/chunk-TRKWUNO4.js +702 -0
- package/dist/chunk-TRKWUNO4.js.map +1 -0
- package/dist/chunk-TRQKHP7D.js +93 -0
- package/dist/chunk-TRQKHP7D.js.map +1 -0
- package/dist/chunk-WCETQWWK.js +59 -0
- package/dist/chunk-WCETQWWK.js.map +1 -0
- package/dist/chunk-WV63X7IK.js +357 -0
- package/dist/chunk-WV63X7IK.js.map +1 -0
- package/dist/chunk-XD7MY6HB.js +343 -0
- package/dist/chunk-XD7MY6HB.js.map +1 -0
- package/dist/chunk-XNAM7F35.js +276 -0
- package/dist/chunk-XNAM7F35.js.map +1 -0
- package/dist/chunk-XUCWLJY5.js +238 -0
- package/dist/chunk-XUCWLJY5.js.map +1 -0
- package/dist/chunk-YNZU6PAV.js +800 -0
- package/dist/chunk-YNZU6PAV.js.map +1 -0
- package/dist/chunk-YT43MUSJ.js +146 -0
- package/dist/chunk-YT43MUSJ.js.map +1 -0
- package/dist/claude-code-HKDV7T32.js +637 -0
- package/dist/claude-code-HKDV7T32.js.map +1 -0
- package/dist/claude-code-W74MNTAC.js +652 -0
- package/dist/claude-code-W74MNTAC.js.map +1 -0
- package/dist/cli/sdk.d.ts +54 -0
- package/dist/cli/sdk.js +82 -0
- package/dist/cli/sdk.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +15 -0
- package/dist/cli.js.map +1 -0
- package/dist/codebuff-3PMZJJHP.js +307 -0
- package/dist/codebuff-3PMZJJHP.js.map +1 -0
- package/dist/codebuff-QX57FJDN.js +220 -0
- package/dist/codebuff-QX57FJDN.js.map +1 -0
- package/dist/codex-JFQTS6LU.js +593 -0
- package/dist/codex-JFQTS6LU.js.map +1 -0
- package/dist/codex-Q3ATRRX3.js +364 -0
- package/dist/codex-Q3ATRRX3.js.map +1 -0
- package/dist/copilot-cli-MTCGHZ2T.js +622 -0
- package/dist/copilot-cli-MTCGHZ2T.js.map +1 -0
- package/dist/copilot-cli-SJGIXVSD.js +369 -0
- package/dist/copilot-cli-SJGIXVSD.js.map +1 -0
- package/dist/crush-I7PK3FGI.js +360 -0
- package/dist/crush-I7PK3FGI.js.map +1 -0
- package/dist/crush-LN5QFFR2.js +183 -0
- package/dist/crush-LN5QFFR2.js.map +1 -0
- package/dist/cursor-4TMXKJOZ.js +603 -0
- package/dist/cursor-4TMXKJOZ.js.map +1 -0
- package/dist/cursor-QZ6MTO74.js +194 -0
- package/dist/cursor-QZ6MTO74.js.map +1 -0
- package/dist/detect-WIAGVYS4.js +68 -0
- package/dist/detect-WIAGVYS4.js.map +1 -0
- package/dist/doctor-EZW2K4NI.js +372 -0
- package/dist/doctor-EZW2K4NI.js.map +1 -0
- package/dist/droid-KN6VRMMT.js +424 -0
- package/dist/droid-KN6VRMMT.js.map +1 -0
- package/dist/droid-W5WF5XEX.js +224 -0
- package/dist/droid-W5WF5XEX.js.map +1 -0
- package/dist/gemini-cli-CIRW37TU.js +369 -0
- package/dist/gemini-cli-CIRW37TU.js.map +1 -0
- package/dist/gemini-cli-Y35FFK4M.js +705 -0
- package/dist/gemini-cli-Y35FFK4M.js.map +1 -0
- package/dist/goose-A3M5GODH.js +212 -0
- package/dist/goose-A3M5GODH.js.map +1 -0
- package/dist/goose-ZTGTSUFJ.js +480 -0
- package/dist/goose-ZTGTSUFJ.js.map +1 -0
- package/dist/hermes-H2KV44GK.js +470 -0
- package/dist/hermes-H2KV44GK.js.map +1 -0
- package/dist/hermes-ZYLMOFLZ.js +196 -0
- package/dist/hermes-ZYLMOFLZ.js.map +1 -0
- package/dist/hook-YVDILAMZ.js +75 -0
- package/dist/hook-YVDILAMZ.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/install-HQRYZOQ2.js +60 -0
- package/dist/install-HQRYZOQ2.js.map +1 -0
- package/dist/jetbrains-copilot-HWA3DMH7.js +633 -0
- package/dist/jetbrains-copilot-HWA3DMH7.js.map +1 -0
- package/dist/kilo-NSWAVP6P.js +235 -0
- package/dist/kilo-NSWAVP6P.js.map +1 -0
- package/dist/kilo-QXJRCSLT.js +384 -0
- package/dist/kilo-QXJRCSLT.js.map +1 -0
- package/dist/kilo-cli-EAQTHXUW.js +587 -0
- package/dist/kilo-cli-EAQTHXUW.js.map +1 -0
- package/dist/kilo-cli-EVBKUODA.js +160 -0
- package/dist/kilo-cli-EVBKUODA.js.map +1 -0
- package/dist/kimi-EI3UM3ZV.js +155 -0
- package/dist/kimi-EI3UM3ZV.js.map +1 -0
- package/dist/kimi-GQ7VXNYT.js +423 -0
- package/dist/kimi-GQ7VXNYT.js.map +1 -0
- package/dist/kiro-UCLSLBW2.js +217 -0
- package/dist/kiro-UCLSLBW2.js.map +1 -0
- package/dist/kiro-VQTT6LO2.js +451 -0
- package/dist/kiro-VQTT6LO2.js.map +1 -0
- package/dist/leaderboard-K6ON3SWW.js +137 -0
- package/dist/leaderboard-K6ON3SWW.js.map +1 -0
- package/dist/mux-T2TV4CIQ.js +211 -0
- package/dist/mux-T2TV4CIQ.js.map +1 -0
- package/dist/mux-ZMLVAXOO.js +96 -0
- package/dist/mux-ZMLVAXOO.js.map +1 -0
- package/dist/omp-PB4VDKS6.js +586 -0
- package/dist/omp-PB4VDKS6.js.map +1 -0
- package/dist/openclaw-ETN2M63O.js +751 -0
- package/dist/openclaw-ETN2M63O.js.map +1 -0
- package/dist/openclaw-IYA7ASL6.js +150 -0
- package/dist/openclaw-IYA7ASL6.js.map +1 -0
- package/dist/opencode-FZ33JKUZ.js +734 -0
- package/dist/opencode-FZ33JKUZ.js.map +1 -0
- package/dist/opencode-HWT3RSMN.js +216 -0
- package/dist/opencode-HWT3RSMN.js.map +1 -0
- package/dist/package-DLOVE2E4.js +1132 -0
- package/dist/package-DLOVE2E4.js.map +1 -0
- package/dist/pi-CIU2QHT4.js +208 -0
- package/dist/pi-CIU2QHT4.js.map +1 -0
- package/dist/pi-UFX2OYFV.js +139 -0
- package/dist/pi-UFX2OYFV.js.map +1 -0
- package/dist/qwen-code-5POCNU6P.js +573 -0
- package/dist/qwen-code-5POCNU6P.js.map +1 -0
- package/dist/qwen-code-W7NUCDEN.js +134 -0
- package/dist/qwen-code-W7NUCDEN.js.map +1 -0
- package/dist/roo-code-IOIUXRDE.js +242 -0
- package/dist/roo-code-IOIUXRDE.js.map +1 -0
- package/dist/roo-code-ZLKH4A7N.js +229 -0
- package/dist/roo-code-ZLKH4A7N.js.map +1 -0
- package/dist/runtime/index.d.ts +157 -0
- package/dist/runtime/index.js +20 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/serve-HVWWY4B6.js +65 -0
- package/dist/serve-HVWWY4B6.js.map +1 -0
- package/dist/status-FE7TECZD.js +128 -0
- package/dist/status-FE7TECZD.js.map +1 -0
- package/dist/synthetic-UKR6SMGS.js +178 -0
- package/dist/synthetic-UKR6SMGS.js.map +1 -0
- package/dist/telemetry-36GESKQQ.js +349 -0
- package/dist/telemetry-36GESKQQ.js.map +1 -0
- package/dist/trae-JSNDZFKL.js +204 -0
- package/dist/trae-JSNDZFKL.js.map +1 -0
- package/dist/trae-PA26DPBW.js +157 -0
- package/dist/trae-PA26DPBW.js.map +1 -0
- package/dist/types-CYUB1OTr.d.ts +427 -0
- package/dist/uninstall-IGCKNOSF.js +72 -0
- package/dist/uninstall-IGCKNOSF.js.map +1 -0
- package/dist/upgrade-JV2VK242.js +104 -0
- package/dist/upgrade-JV2VK242.js.map +1 -0
- package/dist/usage-6ER6EMEY.js +324 -0
- package/dist/usage-6ER6EMEY.js.map +1 -0
- package/dist/usage-event-JKBI6XCF.js +48 -0
- package/dist/usage-event-JKBI6XCF.js.map +1 -0
- package/dist/vscode-copilot-CK3NO54M.js +715 -0
- package/dist/vscode-copilot-CK3NO54M.js.map +1 -0
- package/dist/warp-JNQA7PBU.js +126 -0
- package/dist/warp-JNQA7PBU.js.map +1 -0
- package/dist/warp-QNVDXOG4.js +199 -0
- package/dist/warp-QNVDXOG4.js.map +1 -0
- package/dist/zed-NNNX5MNT.js +272 -0
- package/dist/zed-NNNX5MNT.js.map +1 -0
- package/dist/zed-VRJKWYUR.js +209 -0
- package/dist/zed-VRJKWYUR.js.map +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
resolveEnvRefsDeep
|
|
4
|
+
} from "./chunk-DBQMX75U.js";
|
|
5
|
+
import {
|
|
6
|
+
BaseAdapter
|
|
7
|
+
} from "./chunk-TIIP3RG2.js";
|
|
8
|
+
import "./chunk-7L6ITC4T.js";
|
|
9
|
+
import {
|
|
10
|
+
buildHomeBinHookCommand,
|
|
11
|
+
buildServeWrapperCommand,
|
|
12
|
+
isHomeBinHookCommand,
|
|
13
|
+
shouldWrapForTelemetry
|
|
14
|
+
} from "./chunk-MEH7GC7R.js";
|
|
15
|
+
import "./chunk-TRQKHP7D.js";
|
|
16
|
+
|
|
17
|
+
// src/adapters/crush/index.ts
|
|
18
|
+
import { existsSync } from "fs";
|
|
19
|
+
import { homedir } from "os";
|
|
20
|
+
import { join, resolve } from "path";
|
|
21
|
+
var HOST = "crush";
|
|
22
|
+
var MCP_ROOT_KEY = "mcp";
|
|
23
|
+
var STDIO_TIMEOUT_SECONDS = 120;
|
|
24
|
+
var CRUSH_HOOK_EVENTS = ["PreToolUse"];
|
|
25
|
+
var CrushAdapter = class extends BaseAdapter {
|
|
26
|
+
id = HOST;
|
|
27
|
+
name = "Crush";
|
|
28
|
+
paradigm = "json-stdio";
|
|
29
|
+
capabilities = {
|
|
30
|
+
// PreToolUse deny is the only hook decision Crush honors.
|
|
31
|
+
preToolUse: true,
|
|
32
|
+
postToolUse: false,
|
|
33
|
+
preCompact: false,
|
|
34
|
+
sessionStart: false,
|
|
35
|
+
sessionEnd: false,
|
|
36
|
+
userPromptSubmit: false,
|
|
37
|
+
stop: false,
|
|
38
|
+
notification: false,
|
|
39
|
+
// Crush is deny-only: it cannot rewrite tool args/output nor inject context.
|
|
40
|
+
canModifyArgs: false,
|
|
41
|
+
canModifyOutput: false,
|
|
42
|
+
canInjectSessionContext: false,
|
|
43
|
+
transports: ["stdio", "http"]
|
|
44
|
+
};
|
|
45
|
+
// ── Detection ────────────────────────────────────────────────────────────
|
|
46
|
+
detectInstalled(projectDir) {
|
|
47
|
+
const userDir = this.userConfigDir();
|
|
48
|
+
const userConfig = join(userDir, "crush.json");
|
|
49
|
+
const projectDot = join(projectDir, ".crush.json");
|
|
50
|
+
const projectBare = join(projectDir, "crush.json");
|
|
51
|
+
const userInstalled = existsSync(userDir) || existsSync(userConfig);
|
|
52
|
+
const projInstalled = existsSync(projectDot) || existsSync(projectBare);
|
|
53
|
+
const installed = userInstalled || projInstalled;
|
|
54
|
+
const scope = projInstalled && !userInstalled ? "project" : "user";
|
|
55
|
+
const projectPath = existsSync(projectBare) && !existsSync(projectDot) ? projectBare : projectDot;
|
|
56
|
+
const configPath = scope === "project" ? projectPath : userConfig;
|
|
57
|
+
return {
|
|
58
|
+
id: this.id,
|
|
59
|
+
name: this.name,
|
|
60
|
+
installed,
|
|
61
|
+
paradigm: this.paradigm,
|
|
62
|
+
capabilities: this.capabilities,
|
|
63
|
+
configPath,
|
|
64
|
+
scope,
|
|
65
|
+
reason: installed ? scope === "project" ? `found project Crush config at ${configPath}` : `found Crush config under ${userDir}` : `no Crush config at ${userConfig}`,
|
|
66
|
+
confidence: installed ? "high" : "low"
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
// ── Native paths ─────────────────────────────────────────────────────────
|
|
70
|
+
getConfigDir(ctx) {
|
|
71
|
+
return ctx.scope === "project" ? ctx.projectDir : this.userConfigDir();
|
|
72
|
+
}
|
|
73
|
+
/** MCP servers and hooks share ONE crush.json file. */
|
|
74
|
+
getServerConfigPath(ctx) {
|
|
75
|
+
if (ctx.scope === "project") {
|
|
76
|
+
const dot = join(ctx.projectDir, ".crush.json");
|
|
77
|
+
const bare = join(ctx.projectDir, "crush.json");
|
|
78
|
+
if (existsSync(bare) && !existsSync(dot)) return bare;
|
|
79
|
+
return dot;
|
|
80
|
+
}
|
|
81
|
+
return join(this.userConfigDir(), "crush.json");
|
|
82
|
+
}
|
|
83
|
+
/** Hooks live in the SAME crush.json under the top-level "hooks" key. */
|
|
84
|
+
getHookConfigPath(ctx) {
|
|
85
|
+
return this.getServerConfigPath(ctx);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Crush's user config dir:
|
|
89
|
+
* - Windows: %LOCALAPPDATA%\crush (Local; falls back to AppData/Local)
|
|
90
|
+
* - macOS / Linux: ~/.config/crush
|
|
91
|
+
*/
|
|
92
|
+
userConfigDir() {
|
|
93
|
+
if (process.platform === "win32") {
|
|
94
|
+
const localAppData = process.env.LOCALAPPDATA && process.env.LOCALAPPDATA.trim() !== "" ? process.env.LOCALAPPDATA : resolve(homedir(), "AppData", "Local");
|
|
95
|
+
return join(localAppData, "crush");
|
|
96
|
+
}
|
|
97
|
+
return join(homedir(), ".config", "crush");
|
|
98
|
+
}
|
|
99
|
+
// ── MCP server install / uninstall (crush.json → mcp.<id>) ───────────────
|
|
100
|
+
installServer(ctx) {
|
|
101
|
+
const { connector } = ctx;
|
|
102
|
+
const override = connector.platforms[HOST]?.server;
|
|
103
|
+
if (!connector.server || override === false) {
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
platform: this.id,
|
|
107
|
+
action: "skip",
|
|
108
|
+
detail: connector.server ? "server registration disabled for crush" : "connector declares no MCP server"
|
|
109
|
+
}
|
|
110
|
+
];
|
|
111
|
+
}
|
|
112
|
+
const server = override && typeof override === "object" ? { ...connector.server, ...override } : connector.server;
|
|
113
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
114
|
+
const entry = this.renderServerEntry(ctx, server);
|
|
115
|
+
return [
|
|
116
|
+
this.upsertServerInJson(serverPath, MCP_ROOT_KEY, connector.id, entry, ctx.dryRun)
|
|
117
|
+
];
|
|
118
|
+
}
|
|
119
|
+
uninstallServer(ctx) {
|
|
120
|
+
const serverPath = this.getServerConfigPath(ctx);
|
|
121
|
+
return [
|
|
122
|
+
this.removeServerFromJson(serverPath, MCP_ROOT_KEY, ctx.connector.id, ctx.dryRun)
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Render a normalized ServerDef into Crush's native "mcp" entry. Crush has no
|
|
127
|
+
* documented native `${env:VAR}` interpolation — and it expands `$(...)` at
|
|
128
|
+
* load time — so refs are resolved to LITERALS at install time (the safe
|
|
129
|
+
* posture for a host without native interpolation, matching Codex/Kimi).
|
|
130
|
+
*/
|
|
131
|
+
renderServerEntry(ctx, server) {
|
|
132
|
+
const transport = server.transport;
|
|
133
|
+
const disabled = server.enabled === false;
|
|
134
|
+
if (transport === "stdio") {
|
|
135
|
+
let command = server.command ?? "";
|
|
136
|
+
let args = [...server.args ?? []];
|
|
137
|
+
if (shouldWrapForTelemetry(server, ctx.connector.telemetry)) {
|
|
138
|
+
const wrapped = buildServeWrapperCommand(
|
|
139
|
+
ctx.homeBinPath,
|
|
140
|
+
ctx.connector.id,
|
|
141
|
+
command,
|
|
142
|
+
args,
|
|
143
|
+
ctx.scope,
|
|
144
|
+
this.id
|
|
145
|
+
);
|
|
146
|
+
command = wrapped.command;
|
|
147
|
+
args = wrapped.args;
|
|
148
|
+
}
|
|
149
|
+
command = resolveEnvRefsDeep(command);
|
|
150
|
+
args = resolveEnvRefsDeep(args);
|
|
151
|
+
const entry2 = {
|
|
152
|
+
type: "stdio",
|
|
153
|
+
command,
|
|
154
|
+
args,
|
|
155
|
+
timeout: STDIO_TIMEOUT_SECONDS,
|
|
156
|
+
disabled
|
|
157
|
+
};
|
|
158
|
+
const env = this.renderEnv(server.env);
|
|
159
|
+
if (env) entry2.env = env;
|
|
160
|
+
return entry2;
|
|
161
|
+
}
|
|
162
|
+
const entry = {
|
|
163
|
+
type: "http",
|
|
164
|
+
url: resolveEnvRefsDeep(server.url ?? ""),
|
|
165
|
+
timeout: STDIO_TIMEOUT_SECONDS,
|
|
166
|
+
disabled
|
|
167
|
+
};
|
|
168
|
+
const headers = this.renderEnv(server.headers);
|
|
169
|
+
if (headers) entry.headers = headers;
|
|
170
|
+
return entry;
|
|
171
|
+
}
|
|
172
|
+
/** Resolve env/header values to literals (no native interpolation on Crush). */
|
|
173
|
+
renderEnv(env) {
|
|
174
|
+
if (!env || Object.keys(env).length === 0) return void 0;
|
|
175
|
+
const out = {};
|
|
176
|
+
for (const [k, v] of Object.entries(resolveEnvRefsDeep({ ...env }))) {
|
|
177
|
+
out[k] = String(v);
|
|
178
|
+
}
|
|
179
|
+
return out;
|
|
180
|
+
}
|
|
181
|
+
// ── Hook install / uninstall (crush.json → hooks.PreToolUse[]) ───────────
|
|
182
|
+
installHooks(ctx) {
|
|
183
|
+
const { connector } = ctx;
|
|
184
|
+
if (connector.platforms[HOST]?.hooks === false) {
|
|
185
|
+
return [{ platform: this.id, action: "skip", detail: "hooks disabled for crush" }];
|
|
186
|
+
}
|
|
187
|
+
const events = this.effectiveHookEvents(ctx);
|
|
188
|
+
const path = this.getHookConfigPath(ctx);
|
|
189
|
+
if (events.length === 0) {
|
|
190
|
+
return [{ platform: this.id, action: "skip", path, detail: "no hooks declared" }];
|
|
191
|
+
}
|
|
192
|
+
const cfg = this.readJson(path) ?? {};
|
|
193
|
+
const hooks = cfg.hooks ??= {};
|
|
194
|
+
const changes = [];
|
|
195
|
+
let mutated = false;
|
|
196
|
+
for (const event of events) {
|
|
197
|
+
const command = buildHomeBinHookCommand(ctx.homeBinPath, HOST, event, connector.id);
|
|
198
|
+
const matcher = connector.hooks[event]?.matcher ?? "";
|
|
199
|
+
const entry = { matcher, command };
|
|
200
|
+
const bucket = hooks[event] ??= [];
|
|
201
|
+
const existingIdx = bucket.findIndex((e) => this.isOurHook(e, ctx));
|
|
202
|
+
if (existingIdx >= 0) {
|
|
203
|
+
if (JSON.stringify(bucket[existingIdx]) === JSON.stringify(entry)) {
|
|
204
|
+
changes.push({
|
|
205
|
+
platform: this.id,
|
|
206
|
+
action: "skip",
|
|
207
|
+
path,
|
|
208
|
+
detail: `hooks.${event} already registered`
|
|
209
|
+
});
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
bucket[existingIdx] = entry;
|
|
213
|
+
changes.push({ platform: this.id, action: "update", path, detail: `hooks.${event}` });
|
|
214
|
+
} else {
|
|
215
|
+
bucket.push(entry);
|
|
216
|
+
changes.push({ platform: this.id, action: "create", path, detail: `hooks.${event}` });
|
|
217
|
+
}
|
|
218
|
+
mutated = true;
|
|
219
|
+
}
|
|
220
|
+
if (mutated) this.writeJson(path, cfg, ctx.dryRun);
|
|
221
|
+
return changes;
|
|
222
|
+
}
|
|
223
|
+
uninstallHooks(ctx) {
|
|
224
|
+
const path = this.getHookConfigPath(ctx);
|
|
225
|
+
const cfg = this.readJson(path);
|
|
226
|
+
const hooks = cfg?.hooks;
|
|
227
|
+
if (!cfg || !hooks) {
|
|
228
|
+
return [
|
|
229
|
+
{ platform: this.id, action: "skip", path, detail: "no hooks section present" }
|
|
230
|
+
];
|
|
231
|
+
}
|
|
232
|
+
const changes = [];
|
|
233
|
+
let mutated = false;
|
|
234
|
+
for (const event of Object.keys(hooks)) {
|
|
235
|
+
const bucket = hooks[event];
|
|
236
|
+
if (!Array.isArray(bucket)) continue;
|
|
237
|
+
const kept = bucket.filter((e) => !this.isOurHook(e, ctx));
|
|
238
|
+
const removed = bucket.length - kept.length;
|
|
239
|
+
if (removed > 0) {
|
|
240
|
+
if (kept.length > 0) hooks[event] = kept;
|
|
241
|
+
else delete hooks[event];
|
|
242
|
+
changes.push({
|
|
243
|
+
platform: this.id,
|
|
244
|
+
action: "remove",
|
|
245
|
+
path,
|
|
246
|
+
detail: `hooks.${event} (${removed})`
|
|
247
|
+
});
|
|
248
|
+
mutated = true;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (mutated) this.writeJson(path, cfg, ctx.dryRun);
|
|
252
|
+
if (changes.length === 0) {
|
|
253
|
+
changes.push({
|
|
254
|
+
platform: this.id,
|
|
255
|
+
action: "skip",
|
|
256
|
+
path,
|
|
257
|
+
detail: "no matching hook entries"
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
return changes;
|
|
261
|
+
}
|
|
262
|
+
// ── Internal helpers ─────────────────────────────────────────────────────
|
|
263
|
+
/** Which canonical hook events to register for Crush, honoring overrides. */
|
|
264
|
+
effectiveHookEvents(ctx) {
|
|
265
|
+
if (ctx.connector.platforms[HOST]?.hooks === false) return [];
|
|
266
|
+
return CRUSH_HOOK_EVENTS.filter((e) => ctx.connector.hookEvents.includes(e));
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* Does this hook entry belong to this connector? Anchored on the home-bin +
|
|
270
|
+
* connector-id token (isHomeBinHookCommand) so uninstalling a shared-prefix
|
|
271
|
+
* connector id never strips a sibling's hook.
|
|
272
|
+
*/
|
|
273
|
+
isOurHook(entry, ctx) {
|
|
274
|
+
if (!entry || typeof entry !== "object") return false;
|
|
275
|
+
return isHomeBinHookCommand(entry.command, ctx.homeBinPath, ctx.connector.id);
|
|
276
|
+
}
|
|
277
|
+
// ── Diagnostics ──────────────────────────────────────────────────────────
|
|
278
|
+
getHealthChecks(ctx) {
|
|
279
|
+
const configPath = this.getServerConfigPath(ctx);
|
|
280
|
+
const connectorId = ctx.connector.id;
|
|
281
|
+
const homeBin = ctx.homeBinPath;
|
|
282
|
+
const hookEvents = this.effectiveHookEvents(ctx);
|
|
283
|
+
return [
|
|
284
|
+
{
|
|
285
|
+
name: `${this.name}: crush.json present`,
|
|
286
|
+
check: () => existsSync(configPath) ? { status: "OK", detail: configPath } : { status: "FAIL", detail: `not found: ${configPath}` }
|
|
287
|
+
},
|
|
288
|
+
{
|
|
289
|
+
name: `${this.name}: mcp.${connectorId} registered`,
|
|
290
|
+
check: () => {
|
|
291
|
+
if (!ctx.connector.server) return { status: "OK", detail: "no MCP server declared" };
|
|
292
|
+
const cfg = this.readJson(configPath);
|
|
293
|
+
const bucket = cfg?.[MCP_ROOT_KEY];
|
|
294
|
+
const present = typeof bucket === "object" && bucket !== null && connectorId in bucket;
|
|
295
|
+
return present ? { status: "OK", detail: `mcp.${connectorId}` } : { status: "FAIL", detail: `mcp.${connectorId} not found in ${configPath}` };
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
name: `${this.name}: hook command registered`,
|
|
300
|
+
check: () => {
|
|
301
|
+
if (hookEvents.length === 0) {
|
|
302
|
+
return { status: "OK", detail: "no hooks declared" };
|
|
303
|
+
}
|
|
304
|
+
const cfg = this.readJson(configPath);
|
|
305
|
+
if (!cfg) return { status: "FAIL", detail: `cannot read ${configPath}` };
|
|
306
|
+
const hooks = cfg.hooks ?? {};
|
|
307
|
+
const registered = Object.values(hooks).some(
|
|
308
|
+
(entries) => (entries ?? []).some((e) => isHomeBinHookCommand(e?.command, homeBin, connectorId))
|
|
309
|
+
);
|
|
310
|
+
return registered ? { status: "OK", detail: "hook command present" } : { status: "FAIL", detail: `no hook for ${connectorId} in ${configPath}` };
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
];
|
|
314
|
+
}
|
|
315
|
+
// ── Runtime: parse Crush stdin JSON → normalized event ───────────────────
|
|
316
|
+
parseEvent(event, raw) {
|
|
317
|
+
const input = raw ?? {};
|
|
318
|
+
const connectorId = typeof input.connector === "string" ? input.connector : "";
|
|
319
|
+
const sessionId = typeof input.session_id === "string" ? input.session_id : "";
|
|
320
|
+
const projectDir = typeof input.cwd === "string" ? input.cwd : void 0;
|
|
321
|
+
const base = {
|
|
322
|
+
hostPlatform: HOST,
|
|
323
|
+
connectorId,
|
|
324
|
+
sessionId,
|
|
325
|
+
raw,
|
|
326
|
+
...projectDir !== void 0 ? { projectDir } : {}
|
|
327
|
+
};
|
|
328
|
+
if (event === "PreToolUse") {
|
|
329
|
+
const ev = {
|
|
330
|
+
...base,
|
|
331
|
+
toolName: input.tool_name ?? "",
|
|
332
|
+
toolInput: input.tool_input ?? {}
|
|
333
|
+
};
|
|
334
|
+
return ev;
|
|
335
|
+
}
|
|
336
|
+
throw new Error(`unsupported crush hook event: ${String(event)}`);
|
|
337
|
+
}
|
|
338
|
+
// ── Runtime: normalized response → Crush native hook reply ───────────────
|
|
339
|
+
formatReply(event, response) {
|
|
340
|
+
const decision = response.decision ?? "allow";
|
|
341
|
+
if (decision === "deny" && event === "PreToolUse") {
|
|
342
|
+
return this.stdout({
|
|
343
|
+
decision: "deny",
|
|
344
|
+
reason: response.reason ?? "Blocked by hook"
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
return { exitCode: 0 };
|
|
348
|
+
}
|
|
349
|
+
stdout(payload) {
|
|
350
|
+
return { exitCode: 0, stdout: JSON.stringify(payload) };
|
|
351
|
+
}
|
|
352
|
+
};
|
|
353
|
+
var adapter = new CrushAdapter();
|
|
354
|
+
var crush_default = adapter;
|
|
355
|
+
export {
|
|
356
|
+
CrushAdapter,
|
|
357
|
+
adapter,
|
|
358
|
+
crush_default as default
|
|
359
|
+
};
|
|
360
|
+
//# sourceMappingURL=crush-I7PK3FGI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/crush/index.ts"],"sourcesContent":["/**\n * adapters/crush — Crush (Charm) platform adapter for agent-connector.\n *\n * Crush is a json-stdio host. A single JSON config file holds BOTH the MCP\n * server registrations and the hook registrations, and agent-connector MERGES\n * into it (never replaces it — unrelated keys are preserved verbatim):\n * - User scope: ~/.config/crush/crush.json\n * (Windows: %LOCALAPPDATA%\\crush\\crush.json)\n * - Project scope: ./.crush.json (preferred) or ./crush.json\n *\n * MCP servers live under the ROOT KEY \"mcp\" (NOT \"mcpServers\"). A stdio entry is\n * { type:\"stdio\", command, args, env, timeout:120, disabled:false }.\n * Hooks live under the top-level \"hooks\" key. Crush supports PreToolUse ONLY;\n * the hook reads the host's JSON on stdin and writes a JSON object on stdout\n * carrying a `decision` field. A deny is `{ decision:\"deny\", reason }` on\n * stdout (exit 0); an allow is an empty stdout (exit 0). Crush cannot rewrite\n * tool args/output nor inject session context — every non-deny decision (and\n * every non-PreToolUse event) degrades to a silent allow.\n *\n * SECURITY NOTE: Crush expands `$(...)` command substitution in its config file\n * at load time. We therefore NEVER emit shell-substitution syntax into the\n * command/args/env we write — every value is a literal path/string built by the\n * spawn helpers (which quote, not interpolate). Connector ids are kebab-case and\n * the home-bin path is an absolute literal, so nothing we write can be coerced\n * into a `$(...)` substitution.\n */\n\nimport { existsSync } from \"node:fs\";\nimport { homedir } from \"node:os\";\nimport { join, resolve } from \"node:path\";\n\nimport { BaseAdapter } from \"../base.js\";\nimport type { Adapter, HookReply, InstallContext, NormalizedEvent } from \"../spi.js\";\nimport type {\n ChangeRecord,\n DetectedPlatform,\n HealthCheck,\n HookEventName,\n HookParadigm,\n HookResponse,\n PlatformCapabilities,\n PlatformId,\n PreToolUseEvent,\n ServerDef,\n Transport,\n} from \"../../core/types.js\";\nimport { resolveEnvRefsDeep } from \"../../core/interpolate.js\";\nimport {\n buildHomeBinHookCommand,\n buildServeWrapperCommand,\n isHomeBinHookCommand,\n shouldWrapForTelemetry,\n} from \"../../core/spawn.js\";\n\nconst HOST: PlatformId = \"crush\";\n/** Crush registers MCP servers under \"mcp\" — NOT the \"mcpServers\" of Claude/Gemini. */\nconst MCP_ROOT_KEY = \"mcp\";\n/** Default per-server timeout (seconds) Crush expects on a stdio entry. */\nconst STDIO_TIMEOUT_SECONDS = 120;\n\n/**\n * Crush hook events agent-connector can register. Crush honors PreToolUse ONLY,\n * and only its deny decision is meaningful; every other normalized event has no\n * Crush equivalent and is skipped at install time.\n */\nconst CRUSH_HOOK_EVENTS = [\"PreToolUse\"] as const;\ntype CrushHookEventName = (typeof CRUSH_HOOK_EVENTS)[number];\n\n// ─────────────────────────────────────────────────────────────────────────\n// Native config shapes (only the parts we touch; rest preserved verbatim)\n// ─────────────────────────────────────────────────────────────────────────\n\n/** One hook registration entry as Crush stores it under top-level \"hooks\". */\ninterface CrushHookEntry {\n matcher?: string;\n command: string;\n [key: string]: unknown;\n}\n\n/** Crush's crush.json shape — only the keys we read/merge. */\ninterface CrushConfigFile {\n mcp?: Record<string, unknown>;\n hooks?: Record<string, CrushHookEntry[]>;\n [key: string]: unknown;\n}\n\n/** Native stdio MCP server entry under \"mcp\". */\ninterface CrushStdioServer {\n type: \"stdio\";\n command: string;\n args: string[];\n env?: Record<string, string>;\n timeout: number;\n disabled: boolean;\n}\n\n/** Native HTTP MCP server entry under \"mcp\". */\ninterface CrushHttpServer {\n type: \"http\";\n url: string;\n headers?: Record<string, string>;\n timeout: number;\n disabled: boolean;\n}\n\n/** Raw Crush hook stdin payload (Claude-style: PascalCase event, snake_case fields). */\ninterface CrushHookInput {\n tool_name?: string;\n tool_input?: Record<string, unknown>;\n session_id?: string;\n cwd?: string;\n hook_event_name?: string;\n connector?: unknown;\n}\n\nexport class CrushAdapter extends BaseAdapter implements Adapter {\n readonly id: PlatformId = HOST;\n readonly name = \"Crush\";\n readonly paradigm: HookParadigm = \"json-stdio\";\n\n readonly capabilities: PlatformCapabilities = {\n // PreToolUse deny is the only hook decision Crush honors.\n preToolUse: true,\n postToolUse: false,\n preCompact: false,\n sessionStart: false,\n sessionEnd: false,\n userPromptSubmit: false,\n stop: false,\n notification: false,\n // Crush is deny-only: it cannot rewrite tool args/output nor inject context.\n canModifyArgs: false,\n canModifyOutput: false,\n canInjectSessionContext: false,\n transports: [\"stdio\", \"http\"],\n };\n\n // ── Detection ────────────────────────────────────────────────────────────\n\n detectInstalled(projectDir: string): DetectedPlatform {\n const userDir = this.userConfigDir();\n const userConfig = join(userDir, \"crush.json\");\n const projectDot = join(projectDir, \".crush.json\");\n const projectBare = join(projectDir, \"crush.json\");\n const userInstalled = existsSync(userDir) || existsSync(userConfig);\n const projInstalled = existsSync(projectDot) || existsSync(projectBare);\n const installed = userInstalled || projInstalled;\n // Report the scope/path that actually matched so a project-only install is\n // not misreported as a (non-existent) user install.\n const scope = projInstalled && !userInstalled ? \"project\" : \"user\";\n const projectPath = existsSync(projectBare) && !existsSync(projectDot) ? projectBare : projectDot;\n const configPath = scope === \"project\" ? projectPath : userConfig;\n return {\n id: this.id,\n name: this.name,\n installed,\n paradigm: this.paradigm,\n capabilities: this.capabilities,\n configPath,\n scope,\n reason: installed\n ? scope === \"project\"\n ? `found project Crush config at ${configPath}`\n : `found Crush config under ${userDir}`\n : `no Crush config at ${userConfig}`,\n confidence: installed ? \"high\" : \"low\",\n };\n }\n\n // ── Native paths ─────────────────────────────────────────────────────────\n\n getConfigDir(ctx: InstallContext): string {\n return ctx.scope === \"project\" ? ctx.projectDir : this.userConfigDir();\n }\n\n /** MCP servers and hooks share ONE crush.json file. */\n getServerConfigPath(ctx: InstallContext): string {\n if (ctx.scope === \"project\") {\n // Prefer ./.crush.json, but MERGE into an existing ./crush.json if that is\n // the file already present (so we don't fork the user's config in two).\n const dot = join(ctx.projectDir, \".crush.json\");\n const bare = join(ctx.projectDir, \"crush.json\");\n if (existsSync(bare) && !existsSync(dot)) return bare;\n return dot;\n }\n return join(this.userConfigDir(), \"crush.json\");\n }\n\n /** Hooks live in the SAME crush.json under the top-level \"hooks\" key. */\n getHookConfigPath(ctx: InstallContext): string {\n return this.getServerConfigPath(ctx);\n }\n\n /**\n * Crush's user config dir:\n * - Windows: %LOCALAPPDATA%\\crush (Local; falls back to AppData/Local)\n * - macOS / Linux: ~/.config/crush\n */\n private userConfigDir(): string {\n if (process.platform === \"win32\") {\n const localAppData =\n process.env.LOCALAPPDATA && process.env.LOCALAPPDATA.trim() !== \"\"\n ? process.env.LOCALAPPDATA\n : resolve(homedir(), \"AppData\", \"Local\");\n return join(localAppData, \"crush\");\n }\n return join(homedir(), \".config\", \"crush\");\n }\n\n // ── MCP server install / uninstall (crush.json → mcp.<id>) ───────────────\n\n installServer(ctx: InstallContext): ChangeRecord[] {\n const { connector } = ctx;\n const override = connector.platforms[HOST]?.server;\n if (!connector.server || override === false) {\n return [\n {\n platform: this.id,\n action: \"skip\",\n detail: connector.server\n ? \"server registration disabled for crush\"\n : \"connector declares no MCP server\",\n },\n ];\n }\n\n // Shallow-merge any per-platform server override into the base ServerDef.\n const server: ServerDef =\n override && typeof override === \"object\"\n ? { ...connector.server, ...override }\n : connector.server;\n\n const serverPath = this.getServerConfigPath(ctx);\n const entry = this.renderServerEntry(ctx, server);\n\n return [\n this.upsertServerInJson(serverPath, MCP_ROOT_KEY, connector.id, entry, ctx.dryRun),\n ];\n }\n\n uninstallServer(ctx: InstallContext): ChangeRecord[] {\n const serverPath = this.getServerConfigPath(ctx);\n return [\n this.removeServerFromJson(serverPath, MCP_ROOT_KEY, ctx.connector.id, ctx.dryRun),\n ];\n }\n\n /**\n * Render a normalized ServerDef into Crush's native \"mcp\" entry. Crush has no\n * documented native `${env:VAR}` interpolation — and it expands `$(...)` at\n * load time — so refs are resolved to LITERALS at install time (the safe\n * posture for a host without native interpolation, matching Codex/Kimi).\n */\n private renderServerEntry(\n ctx: InstallContext,\n server: ServerDef,\n ): CrushStdioServer | CrushHttpServer {\n const transport: Transport = server.transport;\n const disabled = server.enabled === false;\n\n if (transport === \"stdio\") {\n let command = server.command ?? \"\";\n let args = [...(server.args ?? [])];\n\n // Transparent telemetry wrapping: route the real command through\n // `<homeBin> serve --connector <id> -- <command> <args...>`.\n if (shouldWrapForTelemetry(server, ctx.connector.telemetry)) {\n const wrapped = buildServeWrapperCommand(\n ctx.homeBinPath,\n ctx.connector.id,\n command,\n args,\n ctx.scope,\n this.id,\n );\n command = wrapped.command;\n args = wrapped.args;\n }\n\n // Resolve to literals — never emit `$(...)` Crush would expand at load.\n command = resolveEnvRefsDeep(command);\n args = resolveEnvRefsDeep(args);\n\n const entry: CrushStdioServer = {\n type: \"stdio\",\n command,\n args,\n timeout: STDIO_TIMEOUT_SECONDS,\n disabled,\n };\n const env = this.renderEnv(server.env);\n if (env) entry.env = env;\n return entry;\n }\n\n // http (and any other remote transport we surface) — Crush registers a URL.\n const entry: CrushHttpServer = {\n type: \"http\",\n url: resolveEnvRefsDeep(server.url ?? \"\"),\n timeout: STDIO_TIMEOUT_SECONDS,\n disabled,\n };\n const headers = this.renderEnv(server.headers);\n if (headers) entry.headers = headers;\n return entry;\n }\n\n /** Resolve env/header values to literals (no native interpolation on Crush). */\n private renderEnv(\n env: Record<string, string> | undefined,\n ): Record<string, string> | undefined {\n if (!env || Object.keys(env).length === 0) return undefined;\n const out: Record<string, string> = {};\n for (const [k, v] of Object.entries(resolveEnvRefsDeep({ ...env }))) {\n out[k] = String(v);\n }\n return out;\n }\n\n // ── Hook install / uninstall (crush.json → hooks.PreToolUse[]) ───────────\n\n installHooks(ctx: InstallContext): ChangeRecord[] {\n const { connector } = ctx;\n if (connector.platforms[HOST]?.hooks === false) {\n return [{ platform: this.id, action: \"skip\", detail: \"hooks disabled for crush\" }];\n }\n\n const events = this.effectiveHookEvents(ctx);\n const path = this.getHookConfigPath(ctx);\n\n if (events.length === 0) {\n return [{ platform: this.id, action: \"skip\", path, detail: \"no hooks declared\" }];\n }\n\n const cfg = this.readJson<CrushConfigFile>(path) ?? {};\n const hooks = (cfg.hooks ??= {});\n\n const changes: ChangeRecord[] = [];\n let mutated = false;\n\n for (const event of events) {\n const command = buildHomeBinHookCommand(ctx.homeBinPath, HOST, event, connector.id);\n const matcher = connector.hooks[event]?.matcher ?? \"\";\n const entry: CrushHookEntry = { matcher, command };\n\n const bucket = (hooks[event] ??= []);\n const existingIdx = bucket.findIndex((e) => this.isOurHook(e, ctx));\n\n if (existingIdx >= 0) {\n if (JSON.stringify(bucket[existingIdx]) === JSON.stringify(entry)) {\n changes.push({\n platform: this.id,\n action: \"skip\",\n path,\n detail: `hooks.${event} already registered`,\n });\n continue;\n }\n bucket[existingIdx] = entry;\n changes.push({ platform: this.id, action: \"update\", path, detail: `hooks.${event}` });\n } else {\n bucket.push(entry);\n changes.push({ platform: this.id, action: \"create\", path, detail: `hooks.${event}` });\n }\n mutated = true;\n }\n\n if (mutated) this.writeJson(path, cfg, ctx.dryRun);\n return changes;\n }\n\n uninstallHooks(ctx: InstallContext): ChangeRecord[] {\n const path = this.getHookConfigPath(ctx);\n const cfg = this.readJson<CrushConfigFile>(path);\n const hooks = cfg?.hooks;\n if (!cfg || !hooks) {\n return [\n { platform: this.id, action: \"skip\", path, detail: \"no hooks section present\" },\n ];\n }\n\n const changes: ChangeRecord[] = [];\n let mutated = false;\n\n for (const event of Object.keys(hooks)) {\n const bucket = hooks[event];\n if (!Array.isArray(bucket)) continue;\n\n // Keep every entry that is NOT ours (anchored match) so a shared-prefix\n // connector id — or the user's own hook — is never stripped.\n const kept = bucket.filter((e) => !this.isOurHook(e, ctx));\n const removed = bucket.length - kept.length;\n if (removed > 0) {\n if (kept.length > 0) hooks[event] = kept;\n else delete hooks[event];\n changes.push({\n platform: this.id,\n action: \"remove\",\n path,\n detail: `hooks.${event} (${removed})`,\n });\n mutated = true;\n }\n }\n\n if (mutated) this.writeJson(path, cfg, ctx.dryRun);\n if (changes.length === 0) {\n changes.push({\n platform: this.id,\n action: \"skip\",\n path,\n detail: \"no matching hook entries\",\n });\n }\n return changes;\n }\n\n // ── Internal helpers ─────────────────────────────────────────────────────\n\n /** Which canonical hook events to register for Crush, honoring overrides. */\n private effectiveHookEvents(ctx: InstallContext): CrushHookEventName[] {\n if (ctx.connector.platforms[HOST]?.hooks === false) return [];\n return CRUSH_HOOK_EVENTS.filter((e) => ctx.connector.hookEvents.includes(e));\n }\n\n /**\n * Does this hook entry belong to this connector? Anchored on the home-bin +\n * connector-id token (isHomeBinHookCommand) so uninstalling a shared-prefix\n * connector id never strips a sibling's hook.\n */\n private isOurHook(entry: CrushHookEntry | undefined, ctx: InstallContext): boolean {\n if (!entry || typeof entry !== \"object\") return false;\n return isHomeBinHookCommand(entry.command, ctx.homeBinPath, ctx.connector.id);\n }\n\n // ── Diagnostics ──────────────────────────────────────────────────────────\n\n override getHealthChecks(ctx: InstallContext): readonly HealthCheck[] {\n const configPath = this.getServerConfigPath(ctx);\n const connectorId = ctx.connector.id;\n const homeBin = ctx.homeBinPath;\n const hookEvents = this.effectiveHookEvents(ctx);\n return [\n {\n name: `${this.name}: crush.json present`,\n check: () =>\n existsSync(configPath)\n ? { status: \"OK\", detail: configPath }\n : { status: \"FAIL\", detail: `not found: ${configPath}` },\n },\n {\n name: `${this.name}: mcp.${connectorId} registered`,\n check: () => {\n if (!ctx.connector.server) return { status: \"OK\", detail: \"no MCP server declared\" };\n const cfg = this.readJson<CrushConfigFile>(configPath);\n const bucket = cfg?.[MCP_ROOT_KEY];\n const present =\n typeof bucket === \"object\" && bucket !== null && connectorId in bucket;\n return present\n ? { status: \"OK\", detail: `mcp.${connectorId}` }\n : { status: \"FAIL\", detail: `mcp.${connectorId} not found in ${configPath}` };\n },\n },\n {\n name: `${this.name}: hook command registered`,\n check: () => {\n if (hookEvents.length === 0) {\n return { status: \"OK\", detail: \"no hooks declared\" };\n }\n const cfg = this.readJson<CrushConfigFile>(configPath);\n if (!cfg) return { status: \"FAIL\", detail: `cannot read ${configPath}` };\n const hooks = cfg.hooks ?? {};\n const registered = Object.values(hooks).some((entries) =>\n (entries ?? []).some((e) => isHomeBinHookCommand(e?.command, homeBin, connectorId)),\n );\n return registered\n ? { status: \"OK\", detail: \"hook command present\" }\n : { status: \"FAIL\", detail: `no hook for ${connectorId} in ${configPath}` };\n },\n },\n ];\n }\n\n // ── Runtime: parse Crush stdin JSON → normalized event ───────────────────\n\n parseEvent(event: HookEventName, raw: unknown): NormalizedEvent {\n const input = (raw ?? {}) as CrushHookInput;\n const connectorId = typeof input.connector === \"string\" ? input.connector : \"\";\n const sessionId = typeof input.session_id === \"string\" ? input.session_id : \"\";\n const projectDir = typeof input.cwd === \"string\" ? input.cwd : undefined;\n\n const base = {\n hostPlatform: HOST,\n connectorId,\n sessionId,\n raw,\n ...(projectDir !== undefined ? { projectDir } : {}),\n } as const;\n\n if (event === \"PreToolUse\") {\n const ev: PreToolUseEvent = {\n ...base,\n toolName: input.tool_name ?? \"\",\n toolInput: input.tool_input ?? {},\n };\n return ev;\n }\n\n // Crush only ever delivers PreToolUse. Surface anything else loudly rather\n // than silently mis-parsing a payload the host cannot actually produce.\n throw new Error(`unsupported crush hook event: ${String(event)}`);\n }\n\n // ── Runtime: normalized response → Crush native hook reply ───────────────\n\n formatReply(event: HookEventName, response: HookResponse): HookReply {\n const decision = response.decision ?? \"allow\";\n\n // deny → Crush blocks the pending tool call via a stdout JSON object with a\n // `decision:\"deny\"` (+ reason). Only PreToolUse is honored; every other\n // event/decision degrades to a silent allow.\n if (decision === \"deny\" && event === \"PreToolUse\") {\n return this.stdout({\n decision: \"deny\",\n reason: response.reason ?? \"Blocked by hook\",\n });\n }\n\n // allow / modify / context / ask / unsupported → pass through (exit 0, empty\n // stdout). Crush cannot rewrite args/output or inject context, so those are\n // dropped fail-open here (the deny path above is the only fail-safe lever).\n return { exitCode: 0 };\n }\n\n private stdout(payload: unknown): HookReply {\n return { exitCode: 0, stdout: JSON.stringify(payload) };\n }\n}\n\nexport const adapter = new CrushAdapter();\nexport default adapter;\n"],"mappings":"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","names":["entry"]}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
openSqlite
|
|
4
|
+
} from "./chunk-YT43MUSJ.js";
|
|
5
|
+
import {
|
|
6
|
+
firstExistingRoot
|
|
7
|
+
} from "./chunk-JWNT5KW7.js";
|
|
8
|
+
import {
|
|
9
|
+
emptyTokens
|
|
10
|
+
} from "./chunk-XD7MY6HB.js";
|
|
11
|
+
|
|
12
|
+
// src/usage/readers/crush.ts
|
|
13
|
+
var PLATFORM_ID = "crush";
|
|
14
|
+
var CRUSH_MODEL_ID = "session-total";
|
|
15
|
+
var CRUSH_PROVIDER_ID = "crush";
|
|
16
|
+
var ROOT_SESSIONS_QUERY = `
|
|
17
|
+
SELECT id, cost, created_at, updated_at
|
|
18
|
+
FROM sessions
|
|
19
|
+
WHERE parent_session_id IS NULL
|
|
20
|
+
AND (COALESCE(message_count, 0) > 0 OR COALESCE(cost, 0) > 0)
|
|
21
|
+
ORDER BY created_at ASC
|
|
22
|
+
`;
|
|
23
|
+
var ASSISTANT_BUCKETS_QUERY = `
|
|
24
|
+
WITH RECURSIVE session_tree(root_session_id, session_id) AS (
|
|
25
|
+
SELECT id, id
|
|
26
|
+
FROM sessions
|
|
27
|
+
WHERE parent_session_id IS NULL
|
|
28
|
+
|
|
29
|
+
UNION ALL
|
|
30
|
+
|
|
31
|
+
SELECT st.root_session_id, s.id
|
|
32
|
+
FROM sessions s
|
|
33
|
+
JOIN session_tree st ON s.parent_session_id = st.session_id
|
|
34
|
+
)
|
|
35
|
+
SELECT st.root_session_id, m.created_at
|
|
36
|
+
FROM session_tree st
|
|
37
|
+
JOIN messages m ON m.session_id = st.session_id
|
|
38
|
+
WHERE m.role = 'assistant'
|
|
39
|
+
ORDER BY st.root_session_id ASC, m.created_at ASC
|
|
40
|
+
`;
|
|
41
|
+
function toNumberOrNull(v) {
|
|
42
|
+
if (v === null || v === void 0) return null;
|
|
43
|
+
const n = typeof v === "number" ? v : typeof v === "string" ? Number(v) : NaN;
|
|
44
|
+
return Number.isFinite(n) ? n : null;
|
|
45
|
+
}
|
|
46
|
+
function toInt(v) {
|
|
47
|
+
const n = toNumberOrNull(v);
|
|
48
|
+
return n === null ? 0 : Math.trunc(n);
|
|
49
|
+
}
|
|
50
|
+
function normalizeCrushTimestampMs(raw) {
|
|
51
|
+
if (raw <= 0) return null;
|
|
52
|
+
if (raw >= 1e11) return raw;
|
|
53
|
+
return raw * 1e3;
|
|
54
|
+
}
|
|
55
|
+
function localDayKey(timestampMs) {
|
|
56
|
+
const d = new Date(timestampMs);
|
|
57
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
58
|
+
const year = d.getFullYear();
|
|
59
|
+
const month = d.getMonth() + 1;
|
|
60
|
+
const day = d.getDate();
|
|
61
|
+
return `${year.toString().padStart(4, "0")}-${month.toString().padStart(2, "0")}-${day.toString().padStart(2, "0")}`;
|
|
62
|
+
}
|
|
63
|
+
function fallbackSessionTimestampMs(updatedAt, createdAt) {
|
|
64
|
+
return normalizeCrushTimestampMs(updatedAt) ?? normalizeCrushTimestampMs(createdAt);
|
|
65
|
+
}
|
|
66
|
+
function loadRootSessions(db) {
|
|
67
|
+
const out = [];
|
|
68
|
+
for (const row of db.all(ROOT_SESSIONS_QUERY)) {
|
|
69
|
+
const id = row.id;
|
|
70
|
+
if (typeof id !== "string") continue;
|
|
71
|
+
out.push({
|
|
72
|
+
id,
|
|
73
|
+
cost: toNumberOrNull(row.cost) ?? 0,
|
|
74
|
+
createdAt: toInt(row.created_at),
|
|
75
|
+
updatedAt: toInt(row.updated_at)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
function loadAssistantBuckets(db) {
|
|
81
|
+
const sessionDays = /* @__PURE__ */ new Map();
|
|
82
|
+
for (const row of db.all(ASSISTANT_BUCKETS_QUERY)) {
|
|
83
|
+
const sessionId = row.root_session_id;
|
|
84
|
+
if (typeof sessionId !== "string") continue;
|
|
85
|
+
const createdAt = toInt(row.created_at);
|
|
86
|
+
const timestampMs = normalizeCrushTimestampMs(createdAt);
|
|
87
|
+
if (timestampMs === null) continue;
|
|
88
|
+
const day = localDayKey(timestampMs);
|
|
89
|
+
if (day === null) continue;
|
|
90
|
+
let dayMap = sessionDays.get(sessionId);
|
|
91
|
+
if (dayMap === void 0) {
|
|
92
|
+
dayMap = /* @__PURE__ */ new Map();
|
|
93
|
+
sessionDays.set(sessionId, dayMap);
|
|
94
|
+
}
|
|
95
|
+
const existing = dayMap.get(day);
|
|
96
|
+
if (existing === void 0) {
|
|
97
|
+
dayMap.set(day, { timestampMs, messageCount: 1 });
|
|
98
|
+
} else {
|
|
99
|
+
existing.timestampMs = Math.min(existing.timestampMs, timestampMs);
|
|
100
|
+
existing.messageCount += 1;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
const out = /* @__PURE__ */ new Map();
|
|
104
|
+
for (const [sessionId, dayMap] of sessionDays) {
|
|
105
|
+
const sortedDays = [...dayMap.keys()].sort();
|
|
106
|
+
out.set(
|
|
107
|
+
sessionId,
|
|
108
|
+
sortedDays.map((day) => dayMap.get(day))
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return out;
|
|
112
|
+
}
|
|
113
|
+
var crushReader = {
|
|
114
|
+
platformId: PLATFORM_ID,
|
|
115
|
+
kind: "local",
|
|
116
|
+
async read({ sinceMs }) {
|
|
117
|
+
const dbPath = firstExistingRoot(PLATFORM_ID);
|
|
118
|
+
if (dbPath === void 0) return [];
|
|
119
|
+
const db = await openSqlite(dbPath);
|
|
120
|
+
if (db === null) return [];
|
|
121
|
+
try {
|
|
122
|
+
const rootSessions = loadRootSessions(db);
|
|
123
|
+
if (rootSessions.length === 0) return [];
|
|
124
|
+
const assistantBuckets = loadAssistantBuckets(db);
|
|
125
|
+
const records = [];
|
|
126
|
+
for (const session of rootSessions) {
|
|
127
|
+
const sessionKey = `${dbPath}:${session.id}`;
|
|
128
|
+
const dayBuckets = assistantBuckets.get(session.id);
|
|
129
|
+
if (dayBuckets !== void 0 && dayBuckets.length > 0) {
|
|
130
|
+
const totalAssistantMessages = dayBuckets.reduce(
|
|
131
|
+
(sum, bucket) => sum + bucket.messageCount,
|
|
132
|
+
0
|
|
133
|
+
);
|
|
134
|
+
const safeCost = Math.max(0, session.cost);
|
|
135
|
+
let allocatedCost = 0;
|
|
136
|
+
for (let index = 0; index < dayBuckets.length; index++) {
|
|
137
|
+
const bucket = dayBuckets[index];
|
|
138
|
+
const bucketCost = index + 1 === dayBuckets.length ? Math.max(0, safeCost - allocatedCost) : totalAssistantMessages > 0 ? safeCost * bucket.messageCount / totalAssistantMessages : 0;
|
|
139
|
+
allocatedCost += bucketCost;
|
|
140
|
+
const tokens = emptyTokens();
|
|
141
|
+
const record2 = {
|
|
142
|
+
platformId: PLATFORM_ID,
|
|
143
|
+
modelId: CRUSH_MODEL_ID,
|
|
144
|
+
providerId: CRUSH_PROVIDER_ID,
|
|
145
|
+
sessionId: sessionKey,
|
|
146
|
+
tokens,
|
|
147
|
+
cost: bucketCost,
|
|
148
|
+
ts: bucket.timestampMs,
|
|
149
|
+
messageCount: Math.max(0, bucket.messageCount),
|
|
150
|
+
confidence: "host-estimated"
|
|
151
|
+
};
|
|
152
|
+
if (sinceMs === void 0 || record2.ts >= sinceMs) records.push(record2);
|
|
153
|
+
}
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (session.cost <= 0) continue;
|
|
157
|
+
const ts = fallbackSessionTimestampMs(session.updatedAt, session.createdAt);
|
|
158
|
+
if (ts === null) continue;
|
|
159
|
+
const record = {
|
|
160
|
+
platformId: PLATFORM_ID,
|
|
161
|
+
modelId: CRUSH_MODEL_ID,
|
|
162
|
+
providerId: CRUSH_PROVIDER_ID,
|
|
163
|
+
sessionId: sessionKey,
|
|
164
|
+
tokens: emptyTokens(),
|
|
165
|
+
cost: Math.max(0, session.cost),
|
|
166
|
+
ts,
|
|
167
|
+
messageCount: 0,
|
|
168
|
+
confidence: "host-estimated"
|
|
169
|
+
};
|
|
170
|
+
if (sinceMs === void 0 || record.ts >= sinceMs) records.push(record);
|
|
171
|
+
}
|
|
172
|
+
records.sort((a, b) => a.ts - b.ts || a.sessionId.localeCompare(b.sessionId));
|
|
173
|
+
return records;
|
|
174
|
+
} finally {
|
|
175
|
+
db.close();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
var crush_default = crushReader;
|
|
180
|
+
export {
|
|
181
|
+
crush_default as default
|
|
182
|
+
};
|
|
183
|
+
//# sourceMappingURL=crush-LN5QFFR2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/usage/readers/crush.ts"],"sourcesContent":["/**\n * usage/readers/crush — Crush CLI usage reader (SQLite, cost-only).\n *\n * Faithful port of tokscale sessions/crush.rs. Crush persists usage in a single\n * SQLite database at ~/.cache/crush/crush.db (the host root, incl. the\n * AGENT_CONNECTOR_CRUSH_DIR override, is resolved by paths.ts hostRoots(\"crush\")).\n *\n * Crush stores reliable session-level COST but NOT a stable per-message token\n * breakdown. Tokscale v1 therefore preserves cost and assistant-message counts\n * without fabricating token precision:\n * - assistant messages (across the whole session tree) are grouped by LOCAL day;\n * - the root session's cost is allocated across those day buckets in proportion\n * to each bucket's assistant-message count (the last bucket absorbs the\n * rounding remainder so the per-day costs sum back to the stored total);\n * - all five token dimensions stay 0;\n * - confidence is \"host-estimated\" (cost is real, tokens are not reported).\n * A costed root session with no assistant messages emits a single zero-count,\n * cost-bearing record at its updated_at (else created_at) timestamp.\n *\n * Two SELECTs:\n * 1. root sessions — SELECT id, cost, created_at, updated_at FROM sessions\n * WHERE parent_session_id IS NULL\n * AND (COALESCE(message_count,0) > 0 OR COALESCE(cost,0) > 0)\n * ORDER BY created_at ASC;\n * 2. assistant message timestamps, attributed to their ROOT session via a\n * RECURSIVE CTE over parent_session_id (so a descendant session's assistant\n * messages count toward its root), filtered to role = 'assistant'.\n *\n * Timestamps tolerate both second-precision (auto ×1000) and millisecond-precision\n * values; non-positive timestamps are dropped. Model/provider are the constants\n * \"session-total\" / \"crush\". Session id = \"<db_path>:<root_session_id>\". No\n * project/workspace in the schema.\n *\n * Fail-open: db missing/locked/unreadable → openSqlite returns null → []; no root\n * sessions → []; a bad row is skipped, never thrown.\n */\n\nimport type { TokenBreakdown, UsageReader, UsageRecord } from \"../types.js\";\nimport { emptyTokens } from \"../aggregate.js\";\nimport { firstExistingRoot } from \"../paths.js\";\nimport { openSqlite } from \"../sqlite.js\";\n\nconst PLATFORM_ID = \"crush\" as const;\nconst CRUSH_MODEL_ID = \"session-total\";\nconst CRUSH_PROVIDER_ID = \"crush\";\n\n/** Root session row (cost lives here; tokens are not trustworthy per-message). */\ninterface CrushSession {\n id: string;\n cost: number;\n createdAt: number;\n updatedAt: number;\n}\n\n/** One local-day bucket of assistant messages within a root session. */\ninterface DayBucket {\n timestampMs: number;\n messageCount: number;\n}\n\nconst ROOT_SESSIONS_QUERY = `\n SELECT id, cost, created_at, updated_at\n FROM sessions\n WHERE parent_session_id IS NULL\n AND (COALESCE(message_count, 0) > 0 OR COALESCE(cost, 0) > 0)\n ORDER BY created_at ASC\n`;\n\nconst ASSISTANT_BUCKETS_QUERY = `\n WITH RECURSIVE session_tree(root_session_id, session_id) AS (\n SELECT id, id\n FROM sessions\n WHERE parent_session_id IS NULL\n\n UNION ALL\n\n SELECT st.root_session_id, s.id\n FROM sessions s\n JOIN session_tree st ON s.parent_session_id = st.session_id\n )\n SELECT st.root_session_id, m.created_at\n FROM session_tree st\n JOIN messages m ON m.session_id = st.session_id\n WHERE m.role = 'assistant'\n ORDER BY st.root_session_id ASC, m.created_at ASC\n`;\n\n/** Coerce an unknown SQLite cell to a finite number, or null when absent/garbage. */\nfunction toNumberOrNull(v: unknown): number | null {\n if (v === null || v === undefined) return null;\n const n = typeof v === \"number\" ? v : typeof v === \"string\" ? Number(v) : NaN;\n return Number.isFinite(n) ? n : null;\n}\n\n/** Coerce an unknown SQLite cell to an integer (0 on absence/garbage). */\nfunction toInt(v: unknown): number {\n const n = toNumberOrNull(v);\n return n === null ? 0 : Math.trunc(n);\n}\n\n/**\n * Normalize a raw Crush timestamp to epoch ms (port of normalize_crush_timestamp_ms):\n * non-positive → null; values >= 100_000_000_000 are already ms; smaller values are\n * seconds and are multiplied by 1000.\n */\nfunction normalizeCrushTimestampMs(raw: number): number | null {\n if (raw <= 0) return null;\n if (raw >= 100_000_000_000) return raw;\n return raw * 1000;\n}\n\n/**\n * Local-day key \"YYYY-MM-DD\" for an epoch-ms timestamp (port of local_day_key):\n * uses the host LOCAL timezone (matches chrono::Local), so buckets follow the\n * machine's calendar day. Returns null when the timestamp is not a valid date.\n */\nfunction localDayKey(timestampMs: number): string | null {\n const d = new Date(timestampMs);\n if (Number.isNaN(d.getTime())) return null;\n const year = d.getFullYear();\n const month = d.getMonth() + 1;\n const day = d.getDate();\n return `${year.toString().padStart(4, \"0\")}-${month\n .toString()\n .padStart(2, \"0\")}-${day.toString().padStart(2, \"0\")}`;\n}\n\n/**\n * Fallback session timestamp (port of fallback_session_timestamp_ms): updated_at,\n * else created_at, each run through the s↔ms normalization.\n */\nfunction fallbackSessionTimestampMs(updatedAt: number, createdAt: number): number | null {\n return normalizeCrushTimestampMs(updatedAt) ?? normalizeCrushTimestampMs(createdAt);\n}\n\n/** Load root sessions carrying a message count or cost (port of load_root_sessions). */\nfunction loadRootSessions(db: { all(sql: string): Array<Record<string, unknown>> }): CrushSession[] {\n const out: CrushSession[] = [];\n for (const row of db.all(ROOT_SESSIONS_QUERY)) {\n const id = row.id;\n if (typeof id !== \"string\") continue;\n out.push({\n id,\n cost: toNumberOrNull(row.cost) ?? 0,\n createdAt: toInt(row.created_at),\n updatedAt: toInt(row.updated_at),\n });\n }\n return out;\n}\n\n/**\n * Bucket assistant messages by root session + local day (port of\n * load_assistant_buckets). The recursive CTE attributes every descendant\n * session's assistant messages to its root; within a root the per-day bucket\n * keeps the MIN timestamp and counts the messages, and buckets are emitted in\n * ascending day order (BTreeMap → insertion-ordered Map keyed by day).\n */\nfunction loadAssistantBuckets(\n db: { all(sql: string): Array<Record<string, unknown>> },\n): Map<string, DayBucket[]> {\n // root session id → (local day → bucket), days kept sorted ascending.\n const sessionDays = new Map<string, Map<string, DayBucket>>();\n\n for (const row of db.all(ASSISTANT_BUCKETS_QUERY)) {\n const sessionId = row.root_session_id;\n if (typeof sessionId !== \"string\") continue;\n\n const createdAt = toInt(row.created_at);\n const timestampMs = normalizeCrushTimestampMs(createdAt);\n if (timestampMs === null) continue;\n const day = localDayKey(timestampMs);\n if (day === null) continue;\n\n let dayMap = sessionDays.get(sessionId);\n if (dayMap === undefined) {\n dayMap = new Map<string, DayBucket>();\n sessionDays.set(sessionId, dayMap);\n }\n const existing = dayMap.get(day);\n if (existing === undefined) {\n dayMap.set(day, { timestampMs, messageCount: 1 });\n } else {\n existing.timestampMs = Math.min(existing.timestampMs, timestampMs);\n existing.messageCount += 1;\n }\n }\n\n const out = new Map<string, DayBucket[]>();\n for (const [sessionId, dayMap] of sessionDays) {\n // Sort by day key ascending to mirror the Rust BTreeMap iteration order.\n const sortedDays = [...dayMap.keys()].sort();\n out.set(\n sessionId,\n sortedDays.map((day) => dayMap.get(day) as DayBucket),\n );\n }\n return out;\n}\n\n/** The Crush CLI usage reader singleton. */\nconst crushReader: UsageReader = {\n platformId: PLATFORM_ID,\n kind: \"local\",\n async read({ sinceMs }: { sinceMs?: number }): Promise<UsageRecord[]> {\n const dbPath = firstExistingRoot(PLATFORM_ID);\n if (dbPath === undefined) return []; // no crush.db → fail-open\n\n const db = await openSqlite(dbPath);\n if (db === null) return []; // missing / locked / unreadable / non-sqlite → fail-open\n\n try {\n const rootSessions = loadRootSessions(db);\n if (rootSessions.length === 0) return [];\n\n const assistantBuckets = loadAssistantBuckets(db);\n const records: UsageRecord[] = [];\n\n for (const session of rootSessions) {\n const sessionKey = `${dbPath}:${session.id}`;\n const dayBuckets = assistantBuckets.get(session.id);\n\n if (dayBuckets !== undefined && dayBuckets.length > 0) {\n const totalAssistantMessages = dayBuckets.reduce(\n (sum, bucket) => sum + bucket.messageCount,\n 0,\n );\n const safeCost = Math.max(0, session.cost);\n let allocatedCost = 0;\n\n for (let index = 0; index < dayBuckets.length; index++) {\n const bucket = dayBuckets[index] as DayBucket;\n // Last bucket absorbs the remainder so per-day costs sum to the total.\n const bucketCost =\n index + 1 === dayBuckets.length\n ? Math.max(0, safeCost - allocatedCost)\n : totalAssistantMessages > 0\n ? (safeCost * bucket.messageCount) / totalAssistantMessages\n : 0;\n allocatedCost += bucketCost;\n\n const tokens: TokenBreakdown = emptyTokens(); // tokens stay all-zero\n const record: UsageRecord = {\n platformId: PLATFORM_ID,\n modelId: CRUSH_MODEL_ID,\n providerId: CRUSH_PROVIDER_ID,\n sessionId: sessionKey,\n tokens,\n cost: bucketCost,\n ts: bucket.timestampMs,\n messageCount: Math.max(0, bucket.messageCount),\n confidence: \"host-estimated\",\n };\n if (sinceMs === undefined || record.ts >= sinceMs) records.push(record);\n }\n\n continue;\n }\n\n // No assistant messages: emit a single cost-only record (when costed).\n if (session.cost <= 0) continue;\n\n const ts = fallbackSessionTimestampMs(session.updatedAt, session.createdAt);\n if (ts === null) continue;\n\n const record: UsageRecord = {\n platformId: PLATFORM_ID,\n modelId: CRUSH_MODEL_ID,\n providerId: CRUSH_PROVIDER_ID,\n sessionId: sessionKey,\n tokens: emptyTokens(),\n cost: Math.max(0, session.cost),\n ts,\n messageCount: 0,\n confidence: \"host-estimated\",\n };\n if (sinceMs === undefined || record.ts >= sinceMs) records.push(record);\n }\n\n // Sort by timestamp asc, then session id (port of the Rust final sort).\n records.sort((a, b) => a.ts - b.ts || a.sessionId.localeCompare(b.sessionId));\n return records;\n } finally {\n db.close();\n }\n },\n};\n\nexport default crushReader;\n"],"mappings":"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","names":["record"]}
|