@orquentra/codebuddy 2.8.0 → 2.14.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/CLAUDE_CODEX.md +33 -0
- package/README.md +27 -3
- package/dist/{adapters-S4HHTWJW.js → adapters-OJRSVFV3.js} +2 -2
- package/dist/{chunk-GKNYMNFM.js → chunk-4DKYKDHI.js} +6 -1
- package/dist/chunk-4DKYKDHI.js.map +1 -0
- package/dist/{chunk-6KGYAFG7.js → chunk-7QMHIJBA.js} +30 -3
- package/dist/chunk-7QMHIJBA.js.map +1 -0
- package/dist/{chunk-6NZEZJYD.js → chunk-ESC4Y3L7.js} +3 -5
- package/dist/{chunk-6NZEZJYD.js.map → chunk-ESC4Y3L7.js.map} +1 -1
- package/dist/chunk-GQKWUHMA.js +20 -0
- package/dist/chunk-GQKWUHMA.js.map +1 -0
- package/dist/{chunk-J4T6HHI4.js → chunk-L6BRYZ6G.js} +537 -316
- package/dist/chunk-L6BRYZ6G.js.map +1 -0
- package/dist/{chunk-UK6XTS2Y.js → chunk-LRQNPYCK.js} +13 -19
- package/dist/chunk-LRQNPYCK.js.map +1 -0
- package/dist/{chunk-GXOML4UZ.js → chunk-LW75SKZR.js} +2 -2
- package/dist/{chunk-EJQVGFH2.js → chunk-UOTKHSQ5.js} +26 -9
- package/dist/chunk-UOTKHSQ5.js.map +1 -0
- package/dist/{chunk-W7GAXL6C.js → chunk-V7PT6ER5.js} +6 -5
- package/dist/chunk-V7PT6ER5.js.map +1 -0
- package/dist/{chunk-ZNLSCV3J.js → chunk-XSNW6Y3X.js} +1253 -315
- package/dist/chunk-XSNW6Y3X.js.map +1 -0
- package/dist/{chunk-6DONJW3W.js → chunk-YJPCS2GP.js} +274 -69
- package/dist/chunk-YJPCS2GP.js.map +1 -0
- package/dist/{chunk-XL2HLACE.js → chunk-YN7TEIUH.js} +1 -1
- package/dist/chunk-YN7TEIUH.js.map +1 -0
- package/dist/{chunk-TLWGZVPC.js → chunk-YNSTIMCF.js} +746 -66
- package/dist/chunk-YNSTIMCF.js.map +1 -0
- package/dist/{chunk-NY2YXP3R.js → chunk-YWDH6DQ6.js} +70 -4
- package/dist/chunk-YWDH6DQ6.js.map +1 -0
- package/dist/cli/index.js +23 -9
- package/dist/cli/index.js.map +1 -1
- package/dist/{codebuddy-DO7ZhoWn.d.ts → codebuddy-CHqMptZB.d.ts} +35 -0
- package/dist/index.d.ts +6 -4
- package/dist/index.js +9 -9
- package/dist/install-EU35ERK5.js +30 -0
- package/dist/langgraph/index.d.ts +1 -1
- package/dist/lean-43ADYTBW.js +28 -0
- package/dist/{main-QUASSFP7.js → main-SFC5LV4E.js} +1333 -437
- package/dist/main-SFC5LV4E.js.map +1 -0
- package/dist/{operations-WJR7AZZF.js → operations-6Z6ZDVBP.js} +3 -3
- package/dist/{pretool-command-JTKX6G3M.js → pretool-command-B5GY5T6E.js} +3 -3
- package/dist/prompt-gate-TDTFFQW2.js +8 -0
- package/dist/{runtime-OQQ22VJF.js → runtime-W5AW32SD.js} +15 -7
- package/dist/{summaries-ML53R7EL.js → summaries-32PHOB6L.js} +3 -4
- package/dist/{zvec-XDZNZ2OL.js → zvec-24HLAFGN.js} +3 -4
- package/dist/zvec-24HLAFGN.js.map +1 -0
- package/package.json +11 -2
- package/dist/chunk-6DONJW3W.js.map +0 -1
- package/dist/chunk-6KGYAFG7.js.map +0 -1
- package/dist/chunk-EJQVGFH2.js.map +0 -1
- package/dist/chunk-GKNYMNFM.js.map +0 -1
- package/dist/chunk-ITQO632W.js +0 -673
- package/dist/chunk-ITQO632W.js.map +0 -1
- package/dist/chunk-J4T6HHI4.js.map +0 -1
- package/dist/chunk-NY2YXP3R.js.map +0 -1
- package/dist/chunk-TLWGZVPC.js.map +0 -1
- package/dist/chunk-UK6XTS2Y.js.map +0 -1
- package/dist/chunk-W7GAXL6C.js.map +0 -1
- package/dist/chunk-XL2HLACE.js.map +0 -1
- package/dist/chunk-ZNLSCV3J.js.map +0 -1
- package/dist/install-BXM56QCD.js +0 -19
- package/dist/lean-command-7DFW32MA.js +0 -19
- package/dist/main-QUASSFP7.js.map +0 -1
- /package/dist/{adapters-S4HHTWJW.js.map → adapters-OJRSVFV3.js.map} +0 -0
- /package/dist/{chunk-GXOML4UZ.js.map → chunk-LW75SKZR.js.map} +0 -0
- /package/dist/{install-BXM56QCD.js.map → install-EU35ERK5.js.map} +0 -0
- /package/dist/{lean-command-7DFW32MA.js.map → lean-43ADYTBW.js.map} +0 -0
- /package/dist/{operations-WJR7AZZF.js.map → operations-6Z6ZDVBP.js.map} +0 -0
- /package/dist/{pretool-command-JTKX6G3M.js.map → pretool-command-B5GY5T6E.js.map} +0 -0
- /package/dist/{runtime-OQQ22VJF.js.map → prompt-gate-TDTFFQW2.js.map} +0 -0
- /package/dist/{summaries-ML53R7EL.js.map → runtime-W5AW32SD.js.map} +0 -0
- /package/dist/{zvec-XDZNZ2OL.js.map → summaries-32PHOB6L.js.map} +0 -0
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
isDockerDaemonRunning,
|
|
3
|
+
tryStartDockerDesktop,
|
|
4
|
+
waitForDockerDaemon
|
|
5
|
+
} from "./chunk-5UH3BEQR.js";
|
|
6
|
+
import {
|
|
7
|
+
postgresDown,
|
|
8
|
+
postgresStatus,
|
|
9
|
+
postgresUp
|
|
10
|
+
} from "./chunk-CSRD5OQA.js";
|
|
5
11
|
import {
|
|
6
12
|
runPreToolHookCommand
|
|
7
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-UOTKHSQ5.js";
|
|
8
14
|
import {
|
|
9
15
|
failSoft,
|
|
10
16
|
readHookInput,
|
|
@@ -13,6 +19,7 @@ import {
|
|
|
13
19
|
import {
|
|
14
20
|
WORKFLOW_CLIENTS,
|
|
15
21
|
clientDisplayName,
|
|
22
|
+
clientHookCapabilities,
|
|
16
23
|
clientHooksInstalled,
|
|
17
24
|
defaultRulesMode,
|
|
18
25
|
detectClient,
|
|
@@ -29,10 +36,12 @@ import {
|
|
|
29
36
|
removeCodexEntry,
|
|
30
37
|
renderManagedBlock,
|
|
31
38
|
renderWorkflowTemplate,
|
|
39
|
+
runBridgeHook,
|
|
40
|
+
runCodexHook,
|
|
32
41
|
setHooks,
|
|
33
42
|
uninstallClient,
|
|
34
43
|
writeProjectMcpConfig
|
|
35
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-L6BRYZ6G.js";
|
|
36
45
|
import {
|
|
37
46
|
AUTOMATION_MODES,
|
|
38
47
|
CLIENT_ADAPTERS,
|
|
@@ -42,19 +51,14 @@ import {
|
|
|
42
51
|
parseClient,
|
|
43
52
|
parseMode,
|
|
44
53
|
supportedModes
|
|
45
|
-
} from "./chunk-
|
|
46
|
-
import {
|
|
47
|
-
isDockerDaemonRunning,
|
|
48
|
-
tryStartDockerDesktop,
|
|
49
|
-
waitForDockerDaemon
|
|
50
|
-
} from "./chunk-5UH3BEQR.js";
|
|
54
|
+
} from "./chunk-V7PT6ER5.js";
|
|
51
55
|
import {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
} from "./chunk-CSRD5OQA.js";
|
|
56
|
+
renderLeanBlock,
|
|
57
|
+
runLeanHook
|
|
58
|
+
} from "./chunk-LRQNPYCK.js";
|
|
56
59
|
import {
|
|
57
60
|
TELEMETRY_NOTICE,
|
|
61
|
+
findReuse,
|
|
58
62
|
flushTelemetry,
|
|
59
63
|
generateSuggestions,
|
|
60
64
|
migrateMemoryToFiles,
|
|
@@ -65,15 +69,17 @@ import {
|
|
|
65
69
|
telemetryBlocked,
|
|
66
70
|
telemetryStatePath,
|
|
67
71
|
telemetryUrl,
|
|
72
|
+
traceCliCommand,
|
|
68
73
|
watchRepository,
|
|
69
74
|
writeTelemetryState
|
|
70
|
-
} from "./chunk-
|
|
75
|
+
} from "./chunk-YJPCS2GP.js";
|
|
71
76
|
import {
|
|
72
77
|
DEFAULT_ZVEC_MODEL,
|
|
73
78
|
buildZvecIndex,
|
|
74
79
|
zvecStatus
|
|
75
|
-
} from "./chunk-
|
|
80
|
+
} from "./chunk-LW75SKZR.js";
|
|
76
81
|
import {
|
|
82
|
+
HookStagingStore,
|
|
77
83
|
PlanFileStore,
|
|
78
84
|
PlanLifecycle,
|
|
79
85
|
ReviewStore,
|
|
@@ -82,28 +88,40 @@ import {
|
|
|
82
88
|
assessRisk,
|
|
83
89
|
buildBeforeEditContext,
|
|
84
90
|
buildBootstrapContext,
|
|
91
|
+
findCodexRollouts,
|
|
92
|
+
forgetJournal,
|
|
93
|
+
importCodexRollout,
|
|
94
|
+
journalLag,
|
|
95
|
+
latestUnfinished,
|
|
96
|
+
listJournals,
|
|
85
97
|
openHookSemantics,
|
|
98
|
+
readCheckpoint,
|
|
99
|
+
renderCheckpointNow,
|
|
86
100
|
renderContextBlock,
|
|
87
101
|
resolveRiskPaths,
|
|
88
102
|
runCaptureHook,
|
|
103
|
+
runCheckpointHook,
|
|
89
104
|
runContextHook,
|
|
90
105
|
runResetHook,
|
|
106
|
+
runResumeHook,
|
|
91
107
|
runStageHook,
|
|
92
|
-
runSyncHook
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
SessionCapsuleLedger,
|
|
96
|
-
detectTestCommand,
|
|
97
|
-
estimateTokens
|
|
98
|
-
} from "./chunk-GKNYMNFM.js";
|
|
108
|
+
runSyncHook,
|
|
109
|
+
syncReuseVectors
|
|
110
|
+
} from "./chunk-XSNW6Y3X.js";
|
|
99
111
|
import {
|
|
100
112
|
parseTraceLines,
|
|
101
113
|
readTraces,
|
|
102
114
|
tracePath
|
|
103
|
-
} from "./chunk-
|
|
115
|
+
} from "./chunk-YN7TEIUH.js";
|
|
116
|
+
import {
|
|
117
|
+
SessionCapsuleLedger,
|
|
118
|
+
detectTestCommand,
|
|
119
|
+
estimateTokens
|
|
120
|
+
} from "./chunk-4DKYKDHI.js";
|
|
104
121
|
import {
|
|
105
122
|
hookRoot
|
|
106
123
|
} from "./chunk-TGFTKWOR.js";
|
|
124
|
+
import "./chunk-GQKWUHMA.js";
|
|
107
125
|
import "./chunk-DFV2SK6C.js";
|
|
108
126
|
import {
|
|
109
127
|
bootstrapDatabase,
|
|
@@ -116,11 +134,11 @@ import {
|
|
|
116
134
|
pingDatabase,
|
|
117
135
|
runDoctor,
|
|
118
136
|
runMigrations
|
|
119
|
-
} from "./chunk-
|
|
137
|
+
} from "./chunk-YWDH6DQ6.js";
|
|
120
138
|
import {
|
|
121
139
|
MemoryFileStore,
|
|
122
140
|
VERSION
|
|
123
|
-
} from "./chunk-
|
|
141
|
+
} from "./chunk-7QMHIJBA.js";
|
|
124
142
|
import {
|
|
125
143
|
computeFactHash,
|
|
126
144
|
generateEntityId
|
|
@@ -133,25 +151,24 @@ import {
|
|
|
133
151
|
import {
|
|
134
152
|
loadLlmSummaries,
|
|
135
153
|
summarizeRepository
|
|
136
|
-
} from "./chunk-
|
|
154
|
+
} from "./chunk-ESC4Y3L7.js";
|
|
137
155
|
import {
|
|
138
156
|
chatJson,
|
|
139
157
|
coolingDown,
|
|
140
158
|
usageToday
|
|
141
159
|
} from "./chunk-ESZXCZIN.js";
|
|
142
|
-
import {
|
|
143
|
-
generateGraphifyGraph,
|
|
144
|
-
loadGraphifyMap,
|
|
145
|
-
setupGraphify,
|
|
146
|
-
syncFileVectors
|
|
147
|
-
} from "./chunk-TLWGZVPC.js";
|
|
148
160
|
import {
|
|
149
161
|
buildArchitectureMap,
|
|
162
|
+
generateGraphifyGraph,
|
|
150
163
|
indexRepository,
|
|
151
164
|
loadFreshIndex,
|
|
165
|
+
loadGraphifyMap,
|
|
166
|
+
loadProjectArchitectureMap,
|
|
152
167
|
neighbours,
|
|
153
|
-
queryMap
|
|
154
|
-
|
|
168
|
+
queryMap,
|
|
169
|
+
setupGraphify,
|
|
170
|
+
syncFileVectors
|
|
171
|
+
} from "./chunk-YNSTIMCF.js";
|
|
155
172
|
import {
|
|
156
173
|
extractSymbolTable,
|
|
157
174
|
supportsSymbols
|
|
@@ -186,12 +203,14 @@ import {
|
|
|
186
203
|
setZvecEnabled,
|
|
187
204
|
writeConfigFile
|
|
188
205
|
} from "./chunk-KNR2P2UY.js";
|
|
189
|
-
import
|
|
206
|
+
import {
|
|
207
|
+
writeAtomic
|
|
208
|
+
} from "./chunk-CG5B3LYG.js";
|
|
190
209
|
import "./chunk-PZ5AY32C.js";
|
|
191
210
|
|
|
192
211
|
// src/cli/main.ts
|
|
193
212
|
import { Command } from "commander";
|
|
194
|
-
import
|
|
213
|
+
import pc25 from "picocolors";
|
|
195
214
|
|
|
196
215
|
// src/integrations/github.ts
|
|
197
216
|
import { constants } from "fs";
|
|
@@ -769,6 +788,7 @@ var CONFORMANCE = {
|
|
|
769
788
|
const servers = json(result.stdout);
|
|
770
789
|
if (!Array.isArray(servers)) return [fail("mcp", "codex mcp list --json returned no JSON")];
|
|
771
790
|
return [
|
|
791
|
+
await clientHooksInstalled(root, "codex") ? pass("hooks", ".codex/hooks.json is installed; review trust in Codex /hooks") : skip("hooks", "not installed in this repo"),
|
|
772
792
|
servers.some((server) => server.name === name2) ? pass("mcp", `codex mcp list has ${name2}`) : skip("mcp", `no ${name2} entry (clients install codex --global adds it)`)
|
|
773
793
|
];
|
|
774
794
|
},
|
|
@@ -862,7 +882,7 @@ function registerClientsCommands(program, runSafely2) {
|
|
|
862
882
|
console.log(pc2.dim("Per-turn token cost per client: codebuddy clients cost"));
|
|
863
883
|
});
|
|
864
884
|
});
|
|
865
|
-
clients.command("install").argument("<client>", `One of: ${CLIENT_IDS.join(", ")}.`).requiredOption("--mode <mode>", `One of: ${AUTOMATION_MODES.join(", ")}.`).option("--namespace <name>", "Namespace for the MCP entry (defaults to config/folder).").option("--global", "Also add the MCP entry to the global config (Codex, Hermes).").description("Apply one automation mode: rules block, hooks, and MCP config.").action(
|
|
885
|
+
clients.command("install").argument("<client>", `One of: ${CLIENT_IDS.join(", ")}.`).requiredOption("--mode <mode>", `One of: ${AUTOMATION_MODES.join(", ")}.`).option("--namespace <name>", "Namespace for the MCP entry (defaults to config/folder).").option("--global", "Also add the MCP entry to the global config (Codex, Hermes).").option("--capture", "Capture turns into memory even in lean mode (Claude Code).").option("--no-capture", "Inject context without capturing turns (Claude Code).").description("Apply one automation mode: rules block, hooks, and MCP config.").action(
|
|
866
886
|
async (client, opts) => {
|
|
867
887
|
await runSafely2(async () => {
|
|
868
888
|
const mode = parseMode(opts.mode);
|
|
@@ -870,7 +890,8 @@ function registerClientsCommands(program, runSafely2) {
|
|
|
870
890
|
client: parseClient(client),
|
|
871
891
|
mode,
|
|
872
892
|
...opts.namespace ? { namespace: opts.namespace } : {},
|
|
873
|
-
...opts.global ? { global: true } : {}
|
|
893
|
+
...opts.global ? { global: true } : {},
|
|
894
|
+
...opts.capture !== void 0 ? { capture: opts.capture } : {}
|
|
874
895
|
});
|
|
875
896
|
printReport2(report, `${MODE_LABELS[mode]} mode`);
|
|
876
897
|
});
|
|
@@ -982,6 +1003,30 @@ function registerContextCommands(program, runSafely2) {
|
|
|
982
1003
|
printBootstrap(result);
|
|
983
1004
|
});
|
|
984
1005
|
});
|
|
1006
|
+
context.command("reuse").argument("<task...>", "What you are about to implement.").option("--json", "Print machine-readable JSON.").description(
|
|
1007
|
+
"Existing helpers to reuse for a task: signature, a call site and the covering test."
|
|
1008
|
+
).action(async (taskParts, opts) => {
|
|
1009
|
+
await runSafely2(async () => {
|
|
1010
|
+
const root = process.cwd();
|
|
1011
|
+
const { manifest } = await loadFreshIndex(root);
|
|
1012
|
+
const picks = await findReuse(
|
|
1013
|
+
root,
|
|
1014
|
+
taskParts.join(" "),
|
|
1015
|
+
manifest,
|
|
1016
|
+
await loadProjectArchitectureMap(root, manifest)
|
|
1017
|
+
);
|
|
1018
|
+
if (opts.json) {
|
|
1019
|
+
console.log(JSON.stringify(picks, null, 2));
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
if (picks.length === 0) console.log(pc3.dim("no existing helper clearly matches"));
|
|
1023
|
+
for (const pick of picks) {
|
|
1024
|
+
console.log(`${pick.path}:${pick.startLine}-${pick.endLine} ${pick.signature}`);
|
|
1025
|
+
if (pick.usage) console.log(pc3.dim(` used at ${pick.usage}`));
|
|
1026
|
+
if (pick.test) console.log(pc3.dim(` test ${pick.test}`));
|
|
1027
|
+
}
|
|
1028
|
+
});
|
|
1029
|
+
});
|
|
985
1030
|
context.command("preview").argument("[task...]", "The task you are about to work on.").option("--paths <paths>", "Comma-separated repo-relative paths to target.").option("--plan <id>", "Resolve target files from a CodeBuddy plan.").option("--git", "Resolve target files from current git changes.").option("--json", "Print machine-readable JSON.").description("Show the context CodeBuddy would assemble before editing the given files.").action(
|
|
986
1031
|
async (taskParts, opts) => {
|
|
987
1032
|
await runSafely2(async () => {
|
|
@@ -1199,7 +1244,7 @@ th.n { text-align: right; }
|
|
|
1199
1244
|
.chip { display: inline-block; font: 600 10px var(--mono); text-transform: uppercase; letter-spacing: .06em; padding: 2px 6px; border-radius: 4px; }
|
|
1200
1245
|
.chip.recall { background: var(--recall-soft); color: var(--recall); }
|
|
1201
1246
|
.chip.tool { background: var(--tool-soft); color: var(--tool); }
|
|
1202
|
-
.chip.capture, .chip.sync, .chip.guard, .chip.kw { background: var(--line); color: var(--capture); }
|
|
1247
|
+
.chip.capture, .chip.sync, .chip.guard, .chip.cli, .chip.kw { background: var(--line); color: var(--capture); }
|
|
1203
1248
|
.chip.err { background: var(--bad-soft); color: var(--bad); }
|
|
1204
1249
|
.feed { display: grid; gap: 10px; max-height: 520px; overflow: auto; }
|
|
1205
1250
|
.prompt { border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; }
|
|
@@ -1719,15 +1764,22 @@ function registerGitHubCommands(program, runSafely2) {
|
|
|
1719
1764
|
import { execFile as execFile2, spawn as spawn3 } from "child_process";
|
|
1720
1765
|
import { relative as relative2 } from "path";
|
|
1721
1766
|
import pc7 from "picocolors";
|
|
1767
|
+
|
|
1768
|
+
// src/hooks/lean-command.ts
|
|
1769
|
+
async function runLeanHookCommand() {
|
|
1770
|
+
await failSoft(async () => {
|
|
1771
|
+
await runLeanHook(await readHookInput(), writeStdout);
|
|
1772
|
+
});
|
|
1773
|
+
}
|
|
1774
|
+
|
|
1775
|
+
// src/cli/commands/hooks.ts
|
|
1722
1776
|
function registerHooksCommands(program, runSafely2) {
|
|
1723
1777
|
const hooks = program.command("hooks").description("Enforce automatic recall/capture via client lifecycle hooks.");
|
|
1724
1778
|
hooks.command("install").option(
|
|
1725
1779
|
"--client <client>",
|
|
1726
|
-
"Client with a CodeBuddy hook adapter (claude, opencode, hermes).",
|
|
1780
|
+
"Client with a CodeBuddy hook adapter (claude, codex, opencode, hermes).",
|
|
1727
1781
|
"claude"
|
|
1728
|
-
).description(
|
|
1729
|
-
"Add CodeBuddy's recall/capture hooks (.claude/settings.json, .opencode/plugins/codebuddy.js or .hermes/plugins/codebuddy/)."
|
|
1730
|
-
).action(async (opts) => {
|
|
1782
|
+
).description("Add CodeBuddy's recall/capture hooks for the selected client.").action(async (opts) => {
|
|
1731
1783
|
await runSafely2(async () => {
|
|
1732
1784
|
const client = assertHookClient(opts.client);
|
|
1733
1785
|
const step = await setHooks(process.cwd(), true, client);
|
|
@@ -1749,7 +1801,7 @@ function registerHooksCommands(program, runSafely2) {
|
|
|
1749
1801
|
});
|
|
1750
1802
|
hooks.command("uninstall").option(
|
|
1751
1803
|
"--client <client>",
|
|
1752
|
-
"Client with a CodeBuddy hook adapter (claude, opencode, hermes).",
|
|
1804
|
+
"Client with a CodeBuddy hook adapter (claude, codex, opencode, hermes).",
|
|
1753
1805
|
"claude"
|
|
1754
1806
|
).description("Remove CodeBuddy's hooks for a client (user hooks untouched).").action(async (opts) => {
|
|
1755
1807
|
await runSafely2(async () => {
|
|
@@ -1763,14 +1815,43 @@ function registerHooksCommands(program, runSafely2) {
|
|
|
1763
1815
|
});
|
|
1764
1816
|
hooks.command("status").option(
|
|
1765
1817
|
"--client <client>",
|
|
1766
|
-
"Client with a CodeBuddy hook adapter (claude, opencode, hermes).",
|
|
1818
|
+
"Client with a CodeBuddy hook adapter (claude, codex, opencode, hermes).",
|
|
1767
1819
|
"claude"
|
|
1768
|
-
).description("Report
|
|
1820
|
+
).description("Report which CodeBuddy hooks are installed and what they do.").action(async (opts) => {
|
|
1769
1821
|
await runSafely2(async () => {
|
|
1770
|
-
const
|
|
1822
|
+
const caps = await clientHookCapabilities(process.cwd(), parseClient(opts.client));
|
|
1823
|
+
if (!caps?.injection) {
|
|
1824
|
+
console.log(pc7.dim("CodeBuddy hooks: not installed"));
|
|
1825
|
+
return;
|
|
1826
|
+
}
|
|
1827
|
+
console.log(pc7.green("CodeBuddy hooks: installed"));
|
|
1828
|
+
console.log(` injection: ${caps.injection}`);
|
|
1829
|
+
console.log(
|
|
1830
|
+
` capture: ${caps.capture ? "on (edits staged, turn end captured)" : "off (turns are not captured into memory)"}`
|
|
1831
|
+
);
|
|
1771
1832
|
console.log(
|
|
1772
|
-
|
|
1833
|
+
` recovery: ${caps.recovery ? "on (pre-compaction checkpoint, unfinished-task resume)" : "off"}`
|
|
1773
1834
|
);
|
|
1835
|
+
console.log(` compaction reset: ${caps.reset ? "on" : "off"}`);
|
|
1836
|
+
console.log(` read guard: ${caps.guard ? "on" : "off"}`);
|
|
1837
|
+
});
|
|
1838
|
+
});
|
|
1839
|
+
hooks.command("codex").option("--injection <mode>", "full or lean", "full").option("--capture", "Capture visible lifecycle events", false).description("[hook] Native Codex event adapter (JSON output, no permission changes).").action(async (opts) => {
|
|
1840
|
+
let result = {};
|
|
1841
|
+
await failSoft(async () => {
|
|
1842
|
+
if (opts.injection !== "full" && opts.injection !== "lean")
|
|
1843
|
+
throw new Error("injection must be full or lean");
|
|
1844
|
+
result = await runCodexHook(
|
|
1845
|
+
await readHookInput(),
|
|
1846
|
+
opts.injection,
|
|
1847
|
+
opts.capture
|
|
1848
|
+
);
|
|
1849
|
+
});
|
|
1850
|
+
await writeStdout(JSON.stringify(result));
|
|
1851
|
+
});
|
|
1852
|
+
hooks.command("bridge").description("[hook] Capture normalized host events and restore checkpoint context.").action(async () => {
|
|
1853
|
+
await failSoft(async () => {
|
|
1854
|
+
await runBridgeHook(await readHookInput(), writeStdout);
|
|
1774
1855
|
});
|
|
1775
1856
|
});
|
|
1776
1857
|
hooks.command("context").description("[hook] Emit compact project context for injection at prompt submit.").action(async () => {
|
|
@@ -1823,6 +1904,16 @@ function registerHooksCommands(program, runSafely2) {
|
|
|
1823
1904
|
await runResetHook(await readHookInput(), writeStdout);
|
|
1824
1905
|
});
|
|
1825
1906
|
});
|
|
1907
|
+
hooks.command("checkpoint").description("[hook] Journal the transcript before compaction (PreCompact).").action(async () => {
|
|
1908
|
+
await failSoft(async () => {
|
|
1909
|
+
await runCheckpointHook(await readHookInput());
|
|
1910
|
+
});
|
|
1911
|
+
});
|
|
1912
|
+
hooks.command("resume").description("[hook] Print another session's unfinished task checkpoint, if any.").action(async () => {
|
|
1913
|
+
await failSoft(async () => {
|
|
1914
|
+
await runResumeHook(await readHookInput(), writeStdout);
|
|
1915
|
+
});
|
|
1916
|
+
});
|
|
1826
1917
|
}
|
|
1827
1918
|
function assertHookClient(value) {
|
|
1828
1919
|
const client = parseClient(value);
|
|
@@ -2009,9 +2100,21 @@ function registerIndexCommands(program, runSafely2) {
|
|
|
2009
2100
|
await runSafely2(async () => {
|
|
2010
2101
|
const result = await indexRepository({ ...opts.full ? { full: true } : {} });
|
|
2011
2102
|
const local = opts.vectors && (await loadRuntimeConfig(process.cwd(), { requireDatabase: false }).catch(() => null))?.provider.type === "local";
|
|
2103
|
+
const vectorStarted = Date.now();
|
|
2012
2104
|
const vectors = local ? await syncFileVectors(process.cwd(), VECTOR_BUDGET_MS) : null;
|
|
2105
|
+
const reuseVectors = local ? await syncReuseVectors(process.cwd(), VECTOR_BUDGET_MS - (Date.now() - vectorStarted)) : null;
|
|
2013
2106
|
if (opts.json) {
|
|
2014
|
-
console.log(
|
|
2107
|
+
console.log(
|
|
2108
|
+
JSON.stringify(
|
|
2109
|
+
{
|
|
2110
|
+
...result,
|
|
2111
|
+
...vectors ? { vectors } : {},
|
|
2112
|
+
...reuseVectors ? { reuseVectors } : {}
|
|
2113
|
+
},
|
|
2114
|
+
null,
|
|
2115
|
+
2
|
|
2116
|
+
)
|
|
2117
|
+
);
|
|
2015
2118
|
return;
|
|
2016
2119
|
}
|
|
2017
2120
|
printResult(result);
|
|
@@ -2201,8 +2304,59 @@ async function runInitWizard() {
|
|
|
2201
2304
|
}
|
|
2202
2305
|
}
|
|
2203
2306
|
|
|
2204
|
-
// src/cli/commands/
|
|
2307
|
+
// src/cli/commands/journal.ts
|
|
2205
2308
|
import pc10 from "picocolors";
|
|
2309
|
+
function registerJournalCommands(program, runSafely2) {
|
|
2310
|
+
const journal = program.command("journal").description("Inspect or delete session journals and active-task checkpoints.");
|
|
2311
|
+
journal.command("list", { isDefault: true }).description("List sessions with a checkpoint, newest first.").action(async () => {
|
|
2312
|
+
await runSafely2(async () => {
|
|
2313
|
+
const sessions = await listJournals(process.cwd());
|
|
2314
|
+
if (sessions.length === 0) console.log(pc10.dim("no session journals yet"));
|
|
2315
|
+
for (const { session, updatedAt } of sessions) console.log(`${updatedAt} ${session}`);
|
|
2316
|
+
});
|
|
2317
|
+
});
|
|
2318
|
+
journal.command("show").argument("<session>").description("Print a session's active-task checkpoint as JSON.").action(async (session) => {
|
|
2319
|
+
await runSafely2(async () => {
|
|
2320
|
+
const checkpoint = await readCheckpoint(process.cwd(), session);
|
|
2321
|
+
console.log(checkpoint ? JSON.stringify(checkpoint, null, 2) : pc10.dim("no checkpoint"));
|
|
2322
|
+
});
|
|
2323
|
+
});
|
|
2324
|
+
journal.command("resume").description("Print the newest unfinished task checkpoint (any session or agent).").action(async () => {
|
|
2325
|
+
await runSafely2(async () => {
|
|
2326
|
+
const checkpoint = await latestUnfinished(process.cwd(), void 0);
|
|
2327
|
+
console.log(
|
|
2328
|
+
checkpoint ? await renderCheckpointNow(
|
|
2329
|
+
process.cwd(),
|
|
2330
|
+
checkpoint,
|
|
2331
|
+
`unfinished, from session ${checkpoint.session}`
|
|
2332
|
+
) : pc10.dim("no unfinished task in the last 3 days")
|
|
2333
|
+
);
|
|
2334
|
+
});
|
|
2335
|
+
});
|
|
2336
|
+
journal.command("import-codex").option("--all", "Import every Codex rollout for this repository, not only the newest.").description("Journal Codex rollouts (~/.codex/sessions) whose working directory is this repo.").action(async (opts) => {
|
|
2337
|
+
await runSafely2(async () => {
|
|
2338
|
+
const rollouts = await findCodexRollouts(process.cwd());
|
|
2339
|
+
if (rollouts.length === 0) {
|
|
2340
|
+
console.log(pc10.dim("no Codex rollout for this repository"));
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2343
|
+
for (const rollout of opts.all ? rollouts : rollouts.slice(0, 1)) {
|
|
2344
|
+
const result = await importCodexRollout(process.cwd(), rollout);
|
|
2345
|
+
console.log(`codex-${rollout.id}: ${result ? `${result.added} new events` : "busy"}`);
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
2348
|
+
});
|
|
2349
|
+
journal.command("forget").argument("[session]").option("--all", "Delete every session's journal.").description("Delete a session's journal, cursor and checkpoint.").action(async (session, opts) => {
|
|
2350
|
+
await runSafely2(async () => {
|
|
2351
|
+
if (!session && !opts.all) throw new Error("Name a session or pass --all.");
|
|
2352
|
+
await forgetJournal(process.cwd(), opts.all ? void 0 : session);
|
|
2353
|
+
console.log(pc10.yellow(opts.all ? "deleted all session journals" : `deleted ${session}`));
|
|
2354
|
+
});
|
|
2355
|
+
});
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
// src/cli/commands/learn.ts
|
|
2359
|
+
import pc11 from "picocolors";
|
|
2206
2360
|
|
|
2207
2361
|
// src/learning/outcomes.ts
|
|
2208
2362
|
async function recordChangeOutcome(input) {
|
|
@@ -2263,15 +2417,15 @@ function registerLearnCommand(program, runSafely2) {
|
|
|
2263
2417
|
...opts.commit ? { commit: opts.commit } : {},
|
|
2264
2418
|
...opts.plan ? { planId: opts.plan } : {}
|
|
2265
2419
|
});
|
|
2266
|
-
console.log(`${
|
|
2267
|
-
console.log(
|
|
2420
|
+
console.log(`${pc11.green("recorded")} ${result.id} (${result.category})`);
|
|
2421
|
+
console.log(pc11.dim(result.path));
|
|
2268
2422
|
});
|
|
2269
2423
|
}
|
|
2270
2424
|
);
|
|
2271
2425
|
}
|
|
2272
2426
|
|
|
2273
2427
|
// src/cli/commands/map.ts
|
|
2274
|
-
import
|
|
2428
|
+
import pc12 from "picocolors";
|
|
2275
2429
|
function registerMapCommands(program, runSafely2) {
|
|
2276
2430
|
const map = program.command("map").description("Query the lightweight architecture map.");
|
|
2277
2431
|
map.command("build").description("Scan the current repository and summarize the module graph.").action(async () => {
|
|
@@ -2291,7 +2445,7 @@ function registerMapCommands(program, runSafely2) {
|
|
|
2291
2445
|
return;
|
|
2292
2446
|
}
|
|
2293
2447
|
if (edges.length === 0) {
|
|
2294
|
-
console.log(
|
|
2448
|
+
console.log(pc12.dim("no dependencies found"));
|
|
2295
2449
|
return;
|
|
2296
2450
|
}
|
|
2297
2451
|
for (const edge of edges) console.log(`${edge.from} -> ${edge.to}`);
|
|
@@ -2306,7 +2460,7 @@ function registerMapCommands(program, runSafely2) {
|
|
|
2306
2460
|
return;
|
|
2307
2461
|
}
|
|
2308
2462
|
if (result.edges.length === 0) {
|
|
2309
|
-
console.log(
|
|
2463
|
+
console.log(pc12.dim("no dependents found"));
|
|
2310
2464
|
return;
|
|
2311
2465
|
}
|
|
2312
2466
|
for (const edge of result.edges) console.log(`${edge.from} -> ${edge.to}`);
|
|
@@ -2317,7 +2471,7 @@ function registerMapCommands(program, runSafely2) {
|
|
|
2317
2471
|
// src/cli/commands/memory-review.ts
|
|
2318
2472
|
import { spawn as spawn4 } from "child_process";
|
|
2319
2473
|
import { relative as relative3 } from "path";
|
|
2320
|
-
import
|
|
2474
|
+
import pc13 from "picocolors";
|
|
2321
2475
|
function registerMemoryCommands(program, runSafely2) {
|
|
2322
2476
|
const memory = program.command("memory").description("Inspect and curate CodeBuddy memory.");
|
|
2323
2477
|
const review = memory.command("review").description("Review memory candidates captured automatically after turns.");
|
|
@@ -2329,11 +2483,11 @@ function registerMemoryCommands(program, runSafely2) {
|
|
|
2329
2483
|
return;
|
|
2330
2484
|
}
|
|
2331
2485
|
if (items.length === 0) {
|
|
2332
|
-
console.log(
|
|
2486
|
+
console.log(pc13.dim("no pending review items"));
|
|
2333
2487
|
return;
|
|
2334
2488
|
}
|
|
2335
2489
|
for (const item of items) console.log(renderRow(item));
|
|
2336
|
-
console.log(
|
|
2490
|
+
console.log(pc13.dim(`
|
|
2337
2491
|
${items.length} pending. approve <id> | reject <id> | show <id>`));
|
|
2338
2492
|
});
|
|
2339
2493
|
});
|
|
@@ -2347,7 +2501,7 @@ ${items.length} pending. approve <id> | reject <id> | show <id>`));
|
|
|
2347
2501
|
const factId = await approveReviewItem(id, {
|
|
2348
2502
|
...opts.incident !== void 0 ? { incidentId: opts.incident } : {}
|
|
2349
2503
|
});
|
|
2350
|
-
console.log(`${
|
|
2504
|
+
console.log(`${pc13.green("approved")} ${id} \u2192 ${factId}`);
|
|
2351
2505
|
});
|
|
2352
2506
|
});
|
|
2353
2507
|
review.command("reject").argument("<id>").description("Discard a review candidate without saving it.").action(async (id) => {
|
|
@@ -2355,7 +2509,7 @@ ${items.length} pending. approve <id> | reject <id> | show <id>`));
|
|
|
2355
2509
|
const store = new ReviewStore();
|
|
2356
2510
|
await store.read(id);
|
|
2357
2511
|
await store.delete(id);
|
|
2358
|
-
console.log(`${
|
|
2512
|
+
console.log(`${pc13.yellow("rejected")} ${id}`);
|
|
2359
2513
|
});
|
|
2360
2514
|
});
|
|
2361
2515
|
review.command("edit").argument("<id>").description("Open a review candidate in $EDITOR and re-validate on save.").action(async (id) => {
|
|
@@ -2364,44 +2518,44 @@ ${items.length} pending. approve <id> | reject <id> | show <id>`));
|
|
|
2364
2518
|
const item = await store.read(id);
|
|
2365
2519
|
await openEditor(item.path);
|
|
2366
2520
|
await store.read(id);
|
|
2367
|
-
console.log(
|
|
2521
|
+
console.log(pc13.dim("saved and validated"));
|
|
2368
2522
|
});
|
|
2369
2523
|
});
|
|
2370
2524
|
}
|
|
2371
2525
|
function renderRow(item) {
|
|
2372
|
-
const flag = item.sensitive ?
|
|
2373
|
-
return `${classColor(item.class)} ${
|
|
2526
|
+
const flag = item.sensitive ? pc13.red(" \u26A0 sensitive") : "";
|
|
2527
|
+
return `${classColor(item.class)} ${pc13.dim(item.id)} ${truncate(item.content, 70)}${flag}`;
|
|
2374
2528
|
}
|
|
2375
2529
|
function renderItem(item) {
|
|
2376
2530
|
const lines = [
|
|
2377
|
-
`${classColor(item.class)} ${
|
|
2378
|
-
`${
|
|
2379
|
-
`${
|
|
2380
|
-
`${
|
|
2531
|
+
`${classColor(item.class)} ${pc13.dim(item.id)}`,
|
|
2532
|
+
`${pc13.bold("reason:")} ${item.reason}`,
|
|
2533
|
+
`${pc13.bold("confidence:")} ${item.confidence.toFixed(2)}`,
|
|
2534
|
+
`${pc13.bold("triple:")} ${item.subject} \xB7 ${item.predicate} \xB7 ${item.object}`
|
|
2381
2535
|
];
|
|
2382
|
-
if (item.paths.length > 0) lines.push(`${
|
|
2383
|
-
if (item.severity) lines.push(`${
|
|
2536
|
+
if (item.paths.length > 0) lines.push(`${pc13.bold("paths:")} ${item.paths.join(", ")}`);
|
|
2537
|
+
if (item.severity) lines.push(`${pc13.bold("severity:")} ${item.severity}`);
|
|
2384
2538
|
if (item.resolvesIncidents) {
|
|
2385
2539
|
lines.push(
|
|
2386
|
-
`${
|
|
2540
|
+
`${pc13.bold("resolves:")} ${item.resolvesIncidents.join(", ") || "(no candidate; approve with --incident <id>)"}`
|
|
2387
2541
|
);
|
|
2388
2542
|
}
|
|
2389
|
-
if (item.sensitive) lines.push(
|
|
2390
|
-
lines.push("", item.content, "",
|
|
2543
|
+
if (item.sensitive) lines.push(pc13.red(`\u26A0 sensitive: ${item.sensitiveReasons.join(", ")}`));
|
|
2544
|
+
lines.push("", item.content, "", pc13.dim(relative3(process.cwd(), item.path)));
|
|
2391
2545
|
return lines.join("\n");
|
|
2392
2546
|
}
|
|
2393
2547
|
function classColor(memoryClass) {
|
|
2394
2548
|
switch (memoryClass) {
|
|
2395
2549
|
case "incident":
|
|
2396
|
-
return
|
|
2550
|
+
return pc13.red(memoryClass);
|
|
2397
2551
|
case "decision":
|
|
2398
|
-
return
|
|
2552
|
+
return pc13.cyan(memoryClass);
|
|
2399
2553
|
case "fact":
|
|
2400
|
-
return
|
|
2554
|
+
return pc13.green(memoryClass);
|
|
2401
2555
|
case "note":
|
|
2402
|
-
return
|
|
2556
|
+
return pc13.yellow(memoryClass);
|
|
2403
2557
|
default:
|
|
2404
|
-
return
|
|
2558
|
+
return pc13.dim(memoryClass);
|
|
2405
2559
|
}
|
|
2406
2560
|
}
|
|
2407
2561
|
function truncate(value, max) {
|
|
@@ -2421,7 +2575,7 @@ function openEditor(path) {
|
|
|
2421
2575
|
import { existsSync } from "fs";
|
|
2422
2576
|
import { join as join5 } from "path";
|
|
2423
2577
|
import { spinner as spinner2 } from "@clack/prompts";
|
|
2424
|
-
import
|
|
2578
|
+
import pc14 from "picocolors";
|
|
2425
2579
|
|
|
2426
2580
|
// evals/fact-relevance.json
|
|
2427
2581
|
var fact_relevance_default = {
|
|
@@ -2685,9 +2839,9 @@ async function calibrateEmbedder(embed) {
|
|
|
2685
2839
|
var EMBEDDERS = ["local", "huggingface", "openrouter"];
|
|
2686
2840
|
var RERANKERS = ["off", "local", "huggingface", "openrouter"];
|
|
2687
2841
|
var KEY_ENV2 = { huggingface: "HF_TOKEN", openrouter: "OPENROUTER_API_KEY" };
|
|
2688
|
-
var ok = (text3) => `${
|
|
2689
|
-
var warn = (text3) => `${
|
|
2690
|
-
var row = (label, value) => ` ${
|
|
2842
|
+
var ok = (text3) => `${pc14.green("\u2713")} ${text3}`;
|
|
2843
|
+
var warn = (text3) => `${pc14.yellow("!")} ${text3}`;
|
|
2844
|
+
var row = (label, value) => ` ${pc14.dim(label.padEnd(11))}${value}`;
|
|
2691
2845
|
async function withSpinner(label, work, done) {
|
|
2692
2846
|
const progress = spinner2();
|
|
2693
2847
|
progress.start(label);
|
|
@@ -2696,7 +2850,7 @@ async function withSpinner(label, work, done) {
|
|
|
2696
2850
|
progress.stop(done(value));
|
|
2697
2851
|
return value;
|
|
2698
2852
|
} catch (error) {
|
|
2699
|
-
progress.error(
|
|
2853
|
+
progress.error(pc14.red(`\u2717 ${label}`));
|
|
2700
2854
|
throw error;
|
|
2701
2855
|
}
|
|
2702
2856
|
}
|
|
@@ -2735,8 +2889,8 @@ async function printStatus() {
|
|
|
2735
2889
|
const model = provider.type === "local" ? LOCAL_EMBEDDING_MODEL.id : provider.model ?? DEFAULT_CLOUD_EMBEDDING_MODELS[provider.type];
|
|
2736
2890
|
const floor = provider.relevanceFloor ?? getModelDefinition(model)?.relevanceFloor;
|
|
2737
2891
|
const next = [];
|
|
2738
|
-
console.log(
|
|
2739
|
-
console.log(row("provider", `${provider.type} ${
|
|
2892
|
+
console.log(pc14.bold("Embeddings"));
|
|
2893
|
+
console.log(row("provider", `${provider.type} ${pc14.dim(model)}`));
|
|
2740
2894
|
if (provider.type === "local" && !localRuntimeInstalled()) {
|
|
2741
2895
|
console.log(
|
|
2742
2896
|
row("runtime", warn("@huggingface/transformers is not installed: dense recall is off"))
|
|
@@ -2747,7 +2901,7 @@ async function printStatus() {
|
|
|
2747
2901
|
console.log(
|
|
2748
2902
|
row(
|
|
2749
2903
|
"model",
|
|
2750
|
-
ready ? ok(`downloaded ${
|
|
2904
|
+
ready ? ok(`downloaded ${pc14.dim(modelCacheDir())}`) : warn("not downloaded yet (~90 MB)")
|
|
2751
2905
|
)
|
|
2752
2906
|
);
|
|
2753
2907
|
if (!ready) next.push("codebuddy models pull");
|
|
@@ -2764,21 +2918,21 @@ async function printStatus() {
|
|
|
2764
2918
|
console.log(
|
|
2765
2919
|
row(
|
|
2766
2920
|
"floor",
|
|
2767
|
-
floor !== void 0 ? `${floor} ${
|
|
2921
|
+
floor !== void 0 ? `${floor} ${pc14.dim(provider.relevanceFloor !== void 0 ? "(calibrated for this project)" : "(built-in)")}` : warn("not calibrated: dense recall is off for this model")
|
|
2768
2922
|
)
|
|
2769
2923
|
);
|
|
2770
2924
|
if (floor === void 0) next.push("codebuddy models calibrate");
|
|
2771
2925
|
const rerank = file.rerank?.provider ?? "off";
|
|
2772
|
-
console.log(
|
|
2926
|
+
console.log(pc14.bold("Reranker"));
|
|
2773
2927
|
if (rerank === "off") {
|
|
2774
2928
|
console.log(
|
|
2775
|
-
row("provider", `off ${
|
|
2929
|
+
row("provider", `off ${pc14.dim("(default: no reranker beat dense order in evals)")}`)
|
|
2776
2930
|
);
|
|
2777
2931
|
} else {
|
|
2778
2932
|
console.log(
|
|
2779
2933
|
row(
|
|
2780
2934
|
"provider",
|
|
2781
|
-
`${rerank} ${
|
|
2935
|
+
`${rerank} ${pc14.dim(file.rerank?.model ?? DEFAULT_RERANK_MODELS[rerank].id)}`
|
|
2782
2936
|
)
|
|
2783
2937
|
);
|
|
2784
2938
|
if (rerank !== "local") {
|
|
@@ -2791,7 +2945,7 @@ async function printStatus() {
|
|
|
2791
2945
|
);
|
|
2792
2946
|
}
|
|
2793
2947
|
}
|
|
2794
|
-
console.log(
|
|
2948
|
+
console.log(pc14.bold("Memory index"));
|
|
2795
2949
|
if (process.env.CODEBUDDY_SEMANTIC === "0") {
|
|
2796
2950
|
console.log(row("recall", warn("CODEBUDDY_SEMANTIC=0: lexical only")));
|
|
2797
2951
|
}
|
|
@@ -2812,7 +2966,7 @@ async function printStatus() {
|
|
|
2812
2966
|
console.log(
|
|
2813
2967
|
row(
|
|
2814
2968
|
"vectors",
|
|
2815
|
-
`${counts.ready} ready` + (counts.pending ? `, ${counts.pending} pending` : "") + (counts.failed ?
|
|
2969
|
+
`${counts.ready} ready` + (counts.pending ? `, ${counts.pending} pending` : "") + (counts.failed ? pc14.red(`, ${counts.failed} failed`) : "") + (counts.other ? pc14.yellow(`, ${counts.other} from another model`) : "")
|
|
2816
2970
|
)
|
|
2817
2971
|
);
|
|
2818
2972
|
if (counts.pending || counts.other)
|
|
@@ -2821,9 +2975,9 @@ async function printStatus() {
|
|
|
2821
2975
|
}
|
|
2822
2976
|
console.log(
|
|
2823
2977
|
next.length > 0 ? `
|
|
2824
|
-
${
|
|
2825
|
-
${next.map((step) => ` ${
|
|
2826
|
-
${ok("Semantic recall is ready.")} ${
|
|
2978
|
+
${pc14.bold("Next")}
|
|
2979
|
+
${next.map((step) => ` ${pc14.cyan(step)}`).join("\n")}` : `
|
|
2980
|
+
${ok("Semantic recall is ready.")} ${pc14.dim('Try: codebuddy context preview "<task>"')}`
|
|
2827
2981
|
);
|
|
2828
2982
|
}
|
|
2829
2983
|
function registerModelsCommands(program, runSafely2) {
|
|
@@ -2860,27 +3014,27 @@ Examples:
|
|
|
2860
3014
|
...opts.floor !== void 0 ? { relevanceFloor: opts.floor } : {}
|
|
2861
3015
|
});
|
|
2862
3016
|
const model = type === "local" ? LOCAL_EMBEDDING_MODEL.id : opts.model ?? DEFAULT_CLOUD_EMBEDDING_MODELS[type];
|
|
2863
|
-
console.log(ok(`Embeddings: ${type} ${
|
|
3017
|
+
console.log(ok(`Embeddings: ${type} ${pc14.dim(model)}`));
|
|
2864
3018
|
const missingKey = type !== "local" && !keyPresent(type, current?.apiKey);
|
|
2865
3019
|
if (missingKey) {
|
|
2866
3020
|
console.log(
|
|
2867
3021
|
warn(
|
|
2868
|
-
`Set ${KEY_ENV2[type]} in your shell (keys are never stored in config), then run ${
|
|
3022
|
+
`Set ${KEY_ENV2[type]} in your shell (keys are never stored in config), then run ${pc14.cyan("codebuddy hooks sync")}.`
|
|
2869
3023
|
)
|
|
2870
3024
|
);
|
|
2871
|
-
console.log(
|
|
3025
|
+
console.log(pc14.dim(" Existing vectors keep working until the new model can embed."));
|
|
2872
3026
|
}
|
|
2873
3027
|
if (opts.floor === void 0 && getModelDefinition(model)?.relevanceFloor === void 0) {
|
|
2874
3028
|
console.log(
|
|
2875
3029
|
warn(
|
|
2876
|
-
`No floor for ${model} yet: run ${
|
|
3030
|
+
`No floor for ${model} yet: run ${pc14.cyan("codebuddy models calibrate")} to turn on dense recall.`
|
|
2877
3031
|
)
|
|
2878
3032
|
);
|
|
2879
3033
|
}
|
|
2880
3034
|
if (!missingKey) {
|
|
2881
3035
|
syncInBackground(process.cwd(), true);
|
|
2882
3036
|
console.log(
|
|
2883
|
-
|
|
3037
|
+
pc14.dim(" Re-embedding existing memory in the background (codebuddy hooks sync).")
|
|
2884
3038
|
);
|
|
2885
3039
|
}
|
|
2886
3040
|
});
|
|
@@ -2907,13 +3061,13 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
|
|
|
2907
3061
|
}
|
|
2908
3062
|
const reranker = choice;
|
|
2909
3063
|
console.log(
|
|
2910
|
-
ok(`Reranker: ${reranker} ${
|
|
3064
|
+
ok(`Reranker: ${reranker} ${pc14.dim(opts.model ?? DEFAULT_RERANK_MODELS[reranker].id)}`)
|
|
2911
3065
|
);
|
|
2912
3066
|
if (reranker !== "local" && !keyPresent(reranker, (await readConfigFile()).provider?.apiKey)) {
|
|
2913
3067
|
console.log(warn(`Set ${KEY_ENV2[reranker]}; without it reranking is skipped.`));
|
|
2914
3068
|
}
|
|
2915
3069
|
if (reranker === "local")
|
|
2916
|
-
console.log(
|
|
3070
|
+
console.log(pc14.dim(" Run codebuddy models pull to download it now."));
|
|
2917
3071
|
});
|
|
2918
3072
|
});
|
|
2919
3073
|
models.command("pull").description("Download the local models now instead of on first use.").action(async () => {
|
|
@@ -2924,7 +3078,7 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
|
|
|
2924
3078
|
await withSpinner(
|
|
2925
3079
|
`Downloading ${LOCAL_EMBEDDING_MODEL.id}`,
|
|
2926
3080
|
() => new LocalModelProvider().embed({ input: "warm up" }),
|
|
2927
|
-
() => ok(`${LOCAL_EMBEDDING_MODEL.id} ready ${
|
|
3081
|
+
() => ok(`${LOCAL_EMBEDDING_MODEL.id} ready ${pc14.dim(`${Date.now() - started} ms`)}`)
|
|
2928
3082
|
);
|
|
2929
3083
|
if (file.rerank?.provider === "local") {
|
|
2930
3084
|
const id = file.rerank.model ?? DEFAULT_RERANK_MODELS.local.id;
|
|
@@ -2933,10 +3087,10 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
|
|
|
2933
3087
|
await withSpinner(
|
|
2934
3088
|
`Downloading ${id}`,
|
|
2935
3089
|
() => rerankLocally({ id, ...revision ? { revision } : {} }, "warm", ["up"]),
|
|
2936
|
-
() => ok(`${id} ready ${
|
|
3090
|
+
() => ok(`${id} ready ${pc14.dim(`${Date.now() - started} ms`)}`)
|
|
2937
3091
|
);
|
|
2938
3092
|
}
|
|
2939
|
-
console.log(
|
|
3093
|
+
console.log(pc14.dim(` Cache: ${modelCacheDir()}`));
|
|
2940
3094
|
});
|
|
2941
3095
|
});
|
|
2942
3096
|
models.command("calibrate").option("--dry-run", "Measure only; do not save the floor.").description(
|
|
@@ -2954,19 +3108,19 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
|
|
|
2954
3108
|
console.log(
|
|
2955
3109
|
row(
|
|
2956
3110
|
"ranking",
|
|
2957
|
-
`AUC ${result.auc.toFixed(3)} \xB7 right fact first ${pct(result.recallAt1)} ${
|
|
3111
|
+
`AUC ${result.auc.toFixed(3)} \xB7 right fact first ${pct(result.recallAt1)} ${pc14.dim("(held-out tasks)")}`
|
|
2958
3112
|
)
|
|
2959
3113
|
);
|
|
2960
3114
|
console.log(
|
|
2961
3115
|
row(
|
|
2962
3116
|
"floor",
|
|
2963
|
-
`${result.best.floor}: precision ${pct(result.test.precision)}, recall ${pct(result.test.recall)}, silent on ${pct(result.test.abstain)} of unrelated tasks ${
|
|
3117
|
+
`${result.best.floor}: precision ${pct(result.test.precision)}, recall ${pct(result.test.recall)}, silent on ${pct(result.test.abstain)} of unrelated tasks ${pc14.dim("(chosen on dev, measured on test)")}`
|
|
2964
3118
|
)
|
|
2965
3119
|
);
|
|
2966
3120
|
console.log(
|
|
2967
3121
|
row(
|
|
2968
3122
|
"recall floor",
|
|
2969
|
-
`${result.recall.dev.floor}: precision ${pct(result.recall.test.precision)}, recall ${pct(result.recall.test.recall)} ` +
|
|
3123
|
+
`${result.recall.dev.floor}: precision ${pct(result.recall.test.precision)}, recall ${pct(result.recall.test.recall)} ` + pc14.dim("(explicit recall: best F2 on dev, measured on test)")
|
|
2970
3124
|
)
|
|
2971
3125
|
);
|
|
2972
3126
|
if (opts.dryRun) return;
|
|
@@ -2985,10 +3139,238 @@ before keeping one on. A reranker never admits a fact, it only orders them.`
|
|
|
2985
3139
|
});
|
|
2986
3140
|
}
|
|
2987
3141
|
|
|
2988
|
-
// src/cli/commands/
|
|
3142
|
+
// src/cli/commands/output.ts
|
|
2989
3143
|
import { spawn as spawn5 } from "child_process";
|
|
3144
|
+
import { constants as constants2 } from "os";
|
|
3145
|
+
import pc15 from "picocolors";
|
|
3146
|
+
|
|
3147
|
+
// src/output/reduce.ts
|
|
3148
|
+
import { createHash } from "crypto";
|
|
3149
|
+
import { mkdir as mkdir2, readdir, readFile as readFile3, rm as rm2 } from "fs/promises";
|
|
3150
|
+
import { join as join6 } from "path";
|
|
3151
|
+
var OUTPUT_LIMITS = {
|
|
3152
|
+
/** At or below either, output is never reduced. */
|
|
3153
|
+
minLines: 80,
|
|
3154
|
+
minBytes: 6e3,
|
|
3155
|
+
head: 20,
|
|
3156
|
+
tail: 30,
|
|
3157
|
+
contextBefore: 2,
|
|
3158
|
+
contextAfter: 4,
|
|
3159
|
+
/** A reduction must save at least this share, or the original is printed. */
|
|
3160
|
+
minSaving: 0.3,
|
|
3161
|
+
/** Stored bytes per run; beyond this the stored copy says it was cut. */
|
|
3162
|
+
storeBytes: 2e7,
|
|
3163
|
+
retentionDays: 7,
|
|
3164
|
+
/** Lines one `output show` may print. */
|
|
3165
|
+
showLines: 400
|
|
3166
|
+
};
|
|
3167
|
+
var SIGNAL = /\b(error|errors|fail|failed|failure|failing|exception|traceback|panic|fatal|assert(?:ion)?|warn(?:ing)?|denied|not found|undefined|segfault|timeout|timed out)\b|✗|×|✖|FAIL|ERR!/i;
|
|
3168
|
+
function outputDir(root) {
|
|
3169
|
+
return join6(root, ".codebuddy", "cache", "outputs");
|
|
3170
|
+
}
|
|
3171
|
+
var shape = (line) => line.replace(/\d+/g, "#").trim();
|
|
3172
|
+
function reduceOutput(text3, id) {
|
|
3173
|
+
const lines = text3.split("\n");
|
|
3174
|
+
if (lines.length <= OUTPUT_LIMITS.minLines || Buffer.byteLength(text3) <= OUTPUT_LIMITS.minBytes) {
|
|
3175
|
+
return null;
|
|
3176
|
+
}
|
|
3177
|
+
if (text3.includes("\0")) return null;
|
|
3178
|
+
const keep = /* @__PURE__ */ new Set();
|
|
3179
|
+
for (let i = 0; i < Math.min(OUTPUT_LIMITS.head, lines.length); i++) keep.add(i);
|
|
3180
|
+
for (let i = Math.max(0, lines.length - OUTPUT_LIMITS.tail); i < lines.length; i++) keep.add(i);
|
|
3181
|
+
lines.forEach((line, index2) => {
|
|
3182
|
+
if (!SIGNAL.test(line)) return;
|
|
3183
|
+
const from = Math.max(0, index2 - OUTPUT_LIMITS.contextBefore);
|
|
3184
|
+
const to = Math.min(lines.length - 1, index2 + OUTPUT_LIMITS.contextAfter);
|
|
3185
|
+
for (let i = from; i <= to; i++) keep.add(i);
|
|
3186
|
+
});
|
|
3187
|
+
const out = [];
|
|
3188
|
+
let index = 0;
|
|
3189
|
+
while (index < lines.length) {
|
|
3190
|
+
if (!keep.has(index)) {
|
|
3191
|
+
const start = index;
|
|
3192
|
+
while (index < lines.length && !keep.has(index)) index++;
|
|
3193
|
+
out.push(
|
|
3194
|
+
`\u2026 ${index - start} lines omitted (codebuddy output show ${id} --range ${start + 1}:${index})`
|
|
3195
|
+
);
|
|
3196
|
+
continue;
|
|
3197
|
+
}
|
|
3198
|
+
const line = lines[index];
|
|
3199
|
+
const same = SIGNAL.test(line) ? (other) => other === line : (other) => shape(other) === shape(line);
|
|
3200
|
+
let run = index + 1;
|
|
3201
|
+
while (run < lines.length && keep.has(run) && same(lines[run])) run++;
|
|
3202
|
+
out.push(line);
|
|
3203
|
+
if (run - index > 2) {
|
|
3204
|
+
out.push(
|
|
3205
|
+
`\u2026 ${run - index - 1} similar lines (codebuddy output show ${id} --range ${index + 2}:${run})`
|
|
3206
|
+
);
|
|
3207
|
+
index = run;
|
|
3208
|
+
} else index++;
|
|
3209
|
+
}
|
|
3210
|
+
const reduced = out.join("\n");
|
|
3211
|
+
return Buffer.byteLength(reduced) <= Buffer.byteLength(text3) * (1 - OUTPUT_LIMITS.minSaving) ? reduced : null;
|
|
3212
|
+
}
|
|
3213
|
+
async function storeOutput(root, command, output, exitCode, now = /* @__PURE__ */ new Date()) {
|
|
3214
|
+
const truncated = Buffer.byteLength(output) > OUTPUT_LIMITS.storeBytes;
|
|
3215
|
+
const body = truncated ? `${output.slice(0, OUTPUT_LIMITS.storeBytes)}
|
|
3216
|
+
[codebuddy: stored copy cut at ${OUTPUT_LIMITS.storeBytes} bytes]` : output;
|
|
3217
|
+
const id = createHash("sha256").update(`${command}\0${now.toISOString()}\0${body}`).digest("hex").slice(0, 12);
|
|
3218
|
+
const meta = {
|
|
3219
|
+
id,
|
|
3220
|
+
command,
|
|
3221
|
+
exitCode,
|
|
3222
|
+
createdAt: now.toISOString(),
|
|
3223
|
+
expiresAt: new Date(now.getTime() + OUTPUT_LIMITS.retentionDays * 864e5).toISOString(),
|
|
3224
|
+
lines: body.split("\n").length,
|
|
3225
|
+
bytes: Buffer.byteLength(body),
|
|
3226
|
+
truncated
|
|
3227
|
+
};
|
|
3228
|
+
const dir = outputDir(root);
|
|
3229
|
+
await mkdir2(dir, { recursive: true, mode: 448 });
|
|
3230
|
+
await writeAtomic(join6(dir, `${id}.log`), body, { mode: 384 });
|
|
3231
|
+
await writeAtomic(join6(dir, `${id}.json`), `${JSON.stringify(meta)}
|
|
3232
|
+
`, { mode: 384 });
|
|
3233
|
+
await pruneOutputs(root, now).catch(() => void 0);
|
|
3234
|
+
return meta;
|
|
3235
|
+
}
|
|
3236
|
+
async function pruneOutputs(root, now = /* @__PURE__ */ new Date()) {
|
|
3237
|
+
const dir = outputDir(root);
|
|
3238
|
+
let removed = 0;
|
|
3239
|
+
for (const name2 of (await readdir(dir).catch(() => [])).filter(
|
|
3240
|
+
(n) => n.endsWith(".json")
|
|
3241
|
+
)) {
|
|
3242
|
+
const meta = await readMeta(root, name2.slice(0, -5));
|
|
3243
|
+
if (meta && Date.parse(meta.expiresAt) <= now.getTime()) {
|
|
3244
|
+
await rm2(join6(dir, `${meta.id}.log`), { force: true });
|
|
3245
|
+
await rm2(join6(dir, name2), { force: true });
|
|
3246
|
+
removed++;
|
|
3247
|
+
}
|
|
3248
|
+
}
|
|
3249
|
+
return removed;
|
|
3250
|
+
}
|
|
3251
|
+
async function readMeta(root, id) {
|
|
3252
|
+
if (!/^[a-f0-9]{12}$/.test(id)) return null;
|
|
3253
|
+
try {
|
|
3254
|
+
return JSON.parse(await readFile3(join6(outputDir(root), `${id}.json`), "utf8"));
|
|
3255
|
+
} catch {
|
|
3256
|
+
return null;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
async function showOutput(root, id, options = {}) {
|
|
3260
|
+
const meta = await readMeta(root, id);
|
|
3261
|
+
if (!meta) return { status: "missing", id };
|
|
3262
|
+
if (Date.parse(meta.expiresAt) <= (options.now ?? /* @__PURE__ */ new Date()).getTime()) {
|
|
3263
|
+
return { status: "expired", id };
|
|
3264
|
+
}
|
|
3265
|
+
const body = await readFile3(join6(outputDir(root), `${id}.log`), "utf8").catch(() => null);
|
|
3266
|
+
if (body === null) return { status: "missing", id };
|
|
3267
|
+
const lines = body.split("\n");
|
|
3268
|
+
let picked;
|
|
3269
|
+
if (options.grep) {
|
|
3270
|
+
const pattern = new RegExp(options.grep, "i");
|
|
3271
|
+
const set = /* @__PURE__ */ new Set();
|
|
3272
|
+
lines.forEach((line, index) => {
|
|
3273
|
+
if (!pattern.test(line)) return;
|
|
3274
|
+
for (let i = Math.max(0, index - 2); i <= Math.min(lines.length - 1, index + 2); i++)
|
|
3275
|
+
set.add(i);
|
|
3276
|
+
});
|
|
3277
|
+
picked = [...set].sort((a, b) => a - b);
|
|
3278
|
+
} else {
|
|
3279
|
+
const [from, to] = options.range ?? [1, lines.length];
|
|
3280
|
+
picked = [];
|
|
3281
|
+
for (let i = Math.max(1, from); i <= Math.min(lines.length, to); i++) picked.push(i - 1);
|
|
3282
|
+
}
|
|
3283
|
+
const shown = picked.slice(0, OUTPUT_LIMITS.showLines);
|
|
3284
|
+
return {
|
|
3285
|
+
status: "ok",
|
|
3286
|
+
meta,
|
|
3287
|
+
text: shown.map((i) => `${i + 1}: ${lines[i]}`).join("\n"),
|
|
3288
|
+
more: picked.length > shown.length
|
|
3289
|
+
};
|
|
3290
|
+
}
|
|
3291
|
+
async function listOutputs(root) {
|
|
3292
|
+
const names = await readdir(outputDir(root)).catch(() => []);
|
|
3293
|
+
const metas = await Promise.all(
|
|
3294
|
+
names.filter((n) => n.endsWith(".json")).map((n) => readMeta(root, n.slice(0, -5)))
|
|
3295
|
+
);
|
|
3296
|
+
return metas.filter((meta) => meta !== null).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
3297
|
+
}
|
|
3298
|
+
|
|
3299
|
+
// src/cli/commands/output.ts
|
|
3300
|
+
function registerOutputCommands(program, runSafely2) {
|
|
3301
|
+
program.command("run").argument("<command...>", "The command and its arguments (after --).").description(
|
|
3302
|
+
"Run a command; print a reduced view of long output and keep the original for `codebuddy output show`."
|
|
3303
|
+
).action(async (parts) => {
|
|
3304
|
+
await runSafely2(async () => {
|
|
3305
|
+
const command = parts.join(" ");
|
|
3306
|
+
const { output: output2, exitCode } = await capture(parts);
|
|
3307
|
+
const root = process.cwd();
|
|
3308
|
+
const provisional = reduceOutput(output2, "xxxxxxxxxxxx");
|
|
3309
|
+
if (provisional === null) {
|
|
3310
|
+
process.stdout.write(output2);
|
|
3311
|
+
} else {
|
|
3312
|
+
const stored = await storeOutput(root, command, output2, exitCode);
|
|
3313
|
+
const reduced = reduceOutput(output2, stored.id) ?? output2;
|
|
3314
|
+
process.stdout.write(`${reduced}
|
|
3315
|
+
`);
|
|
3316
|
+
process.stdout.write(
|
|
3317
|
+
`[codebuddy run: exit ${exitCode ?? "signal"}; ${stored.lines} lines shown as ${reduced.split("\n").length}; full output: codebuddy output show ${stored.id} [--grep RE | --range A:B], kept until ${stored.expiresAt.slice(0, 10)}]
|
|
3318
|
+
`
|
|
3319
|
+
);
|
|
3320
|
+
}
|
|
3321
|
+
process.exitCode = exitCode ?? 1;
|
|
3322
|
+
});
|
|
3323
|
+
});
|
|
3324
|
+
const output = program.command("output").description("Stored originals of `codebuddy run` output.");
|
|
3325
|
+
output.command("list", { isDefault: true }).description("List stored outputs, newest first.").action(async () => {
|
|
3326
|
+
await runSafely2(async () => {
|
|
3327
|
+
const stored = await listOutputs(process.cwd());
|
|
3328
|
+
if (stored.length === 0) console.log(pc15.dim("no stored outputs"));
|
|
3329
|
+
for (const meta of stored) {
|
|
3330
|
+
console.log(
|
|
3331
|
+
`${meta.id} exit ${meta.exitCode ?? "-"} ${meta.lines} lines until ${meta.expiresAt.slice(0, 10)} ${meta.command.slice(0, 80)}`
|
|
3332
|
+
);
|
|
3333
|
+
}
|
|
3334
|
+
});
|
|
3335
|
+
});
|
|
3336
|
+
output.command("show").argument("<id>").option("--range <a:b>", "1-based inclusive line range.").option("--grep <pattern>", "Lines matching a regular expression, with 2 lines of context.").description("Print exact stored lines (at most 400 per call).").action(async (id, opts) => {
|
|
3337
|
+
await runSafely2(async () => {
|
|
3338
|
+
const range = opts.range?.match(/^(\d+):(\d+)$/);
|
|
3339
|
+
if (opts.range && !range) throw new Error("--range must look like 120:180");
|
|
3340
|
+
const result = await showOutput(process.cwd(), id, {
|
|
3341
|
+
...range ? { range: [Number(range[1]), Number(range[2])] } : {},
|
|
3342
|
+
...opts.grep ? { grep: opts.grep } : {}
|
|
3343
|
+
});
|
|
3344
|
+
if (result.status !== "ok") {
|
|
3345
|
+
throw new Error(`stored output ${id} is ${result.status}`);
|
|
3346
|
+
}
|
|
3347
|
+
console.log(result.text);
|
|
3348
|
+
if (result.more) console.log(pc15.dim("\u2026 more lines; narrow --range or --grep"));
|
|
3349
|
+
});
|
|
3350
|
+
});
|
|
3351
|
+
}
|
|
3352
|
+
function capture(parts) {
|
|
3353
|
+
return new Promise((resolve6, reject) => {
|
|
3354
|
+
const [file, ...args] = parts;
|
|
3355
|
+
const child = parts.length === 1 ? spawn5(file, { shell: true, stdio: ["inherit", "pipe", "pipe"] }) : spawn5(file, args, { stdio: ["inherit", "pipe", "pipe"] });
|
|
3356
|
+
const chunks = [];
|
|
3357
|
+
child.stdout.on("data", (chunk) => chunks.push(chunk));
|
|
3358
|
+
child.stderr.on("data", (chunk) => chunks.push(chunk));
|
|
3359
|
+
child.on("error", reject);
|
|
3360
|
+
child.on(
|
|
3361
|
+
"close",
|
|
3362
|
+
(code, signal) => resolve6({
|
|
3363
|
+
output: Buffer.concat(chunks).toString("utf8"),
|
|
3364
|
+
exitCode: code ?? (signal ? 128 + (constants2.signals[signal] ?? 0) : null)
|
|
3365
|
+
})
|
|
3366
|
+
);
|
|
3367
|
+
});
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
// src/cli/commands/plan.ts
|
|
3371
|
+
import { spawn as spawn6 } from "child_process";
|
|
2990
3372
|
import { relative as relative4 } from "path";
|
|
2991
|
-
import
|
|
3373
|
+
import pc16 from "picocolors";
|
|
2992
3374
|
function makeLifecycle(cwd = process.cwd()) {
|
|
2993
3375
|
const store = new PlanFileStore(cwd);
|
|
2994
3376
|
return { store, lifecycle: new PlanLifecycle(store) };
|
|
@@ -2996,67 +3378,67 @@ function makeLifecycle(cwd = process.cwd()) {
|
|
|
2996
3378
|
function statusColor(status) {
|
|
2997
3379
|
switch (status) {
|
|
2998
3380
|
case "draft":
|
|
2999
|
-
return
|
|
3381
|
+
return pc16.dim(status);
|
|
3000
3382
|
case "approved":
|
|
3001
|
-
return
|
|
3383
|
+
return pc16.cyan(status);
|
|
3002
3384
|
case "executing":
|
|
3003
|
-
return
|
|
3385
|
+
return pc16.yellow(status);
|
|
3004
3386
|
case "complete":
|
|
3005
|
-
return
|
|
3387
|
+
return pc16.green(status);
|
|
3006
3388
|
case "abandoned":
|
|
3007
|
-
return
|
|
3389
|
+
return pc16.red(status);
|
|
3008
3390
|
}
|
|
3009
3391
|
}
|
|
3010
3392
|
function renderPlan(plan) {
|
|
3011
3393
|
const lines = [];
|
|
3012
|
-
lines.push(`${
|
|
3013
|
-
lines.push(
|
|
3394
|
+
lines.push(`${pc16.bold(plan.title)} ${statusColor(plan.status)}`);
|
|
3395
|
+
lines.push(pc16.dim(plan.id));
|
|
3014
3396
|
lines.push("");
|
|
3015
|
-
lines.push(`${
|
|
3397
|
+
lines.push(`${pc16.bold("Brief:")} ${plan.brief}`);
|
|
3016
3398
|
if (plan.filesToTouch.length > 0) {
|
|
3017
3399
|
lines.push("");
|
|
3018
|
-
lines.push(
|
|
3400
|
+
lines.push(pc16.bold("Files to touch:"));
|
|
3019
3401
|
for (const file of plan.filesToTouch) {
|
|
3020
3402
|
lines.push(` ${file.action.padEnd(6)} ${file.path}${file.notes ? ` \u2014 ${file.notes}` : ""}`);
|
|
3021
3403
|
}
|
|
3022
3404
|
}
|
|
3023
3405
|
if (plan.testsToAdd.length > 0) {
|
|
3024
3406
|
lines.push("");
|
|
3025
|
-
lines.push(
|
|
3407
|
+
lines.push(pc16.bold("Tests to add:"));
|
|
3026
3408
|
for (const test of plan.testsToAdd) {
|
|
3027
3409
|
lines.push(` ${test.path}${test.description ? ` \u2014 ${test.description}` : ""}`);
|
|
3028
3410
|
}
|
|
3029
3411
|
}
|
|
3030
3412
|
if (plan.outOfScope.length > 0) {
|
|
3031
3413
|
lines.push("");
|
|
3032
|
-
lines.push(
|
|
3414
|
+
lines.push(pc16.bold("Out of scope:"));
|
|
3033
3415
|
for (const item of plan.outOfScope) lines.push(` - ${item}`);
|
|
3034
3416
|
}
|
|
3035
3417
|
if (plan.risks.length > 0) {
|
|
3036
3418
|
lines.push("");
|
|
3037
|
-
lines.push(
|
|
3419
|
+
lines.push(pc16.bold("Risks:"));
|
|
3038
3420
|
for (const item of plan.risks) lines.push(` - ${item}`);
|
|
3039
3421
|
}
|
|
3040
3422
|
if (plan.commitSha) {
|
|
3041
3423
|
lines.push("");
|
|
3042
|
-
lines.push(`${
|
|
3424
|
+
lines.push(`${pc16.bold("Commit:")} ${plan.commitSha}`);
|
|
3043
3425
|
}
|
|
3044
3426
|
lines.push("");
|
|
3045
|
-
lines.push(
|
|
3427
|
+
lines.push(pc16.dim("\u2500".repeat(40)));
|
|
3046
3428
|
lines.push(plan.body);
|
|
3047
3429
|
return lines.join("\n");
|
|
3048
3430
|
}
|
|
3049
3431
|
function openEditor2(path) {
|
|
3050
3432
|
const editor = process.env.EDITOR ?? process.env.VISUAL ?? "vi";
|
|
3051
3433
|
return new Promise((resolve6, reject) => {
|
|
3052
|
-
const child =
|
|
3434
|
+
const child = spawn6(editor, [path], { stdio: "inherit" });
|
|
3053
3435
|
child.on("error", reject);
|
|
3054
3436
|
child.on("close", () => resolve6());
|
|
3055
3437
|
});
|
|
3056
3438
|
}
|
|
3057
3439
|
function runGit(args) {
|
|
3058
3440
|
return new Promise((resolve6, reject) => {
|
|
3059
|
-
const child =
|
|
3441
|
+
const child = spawn6("git", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
3060
3442
|
let stdout = "";
|
|
3061
3443
|
let stderr = "";
|
|
3062
3444
|
child.stdout.on("data", (c) => {
|
|
@@ -3081,14 +3463,14 @@ function registerPlanCommands(program, runSafely2) {
|
|
|
3081
3463
|
title: opts.title ?? brief,
|
|
3082
3464
|
brief
|
|
3083
3465
|
});
|
|
3084
|
-
console.log(`${
|
|
3466
|
+
console.log(`${pc16.green("created")} ${created.id} (${statusColor(created.status)})`);
|
|
3085
3467
|
if (opts.edit !== false && process.stdin.isTTY) {
|
|
3086
3468
|
await openEditor2(created.path);
|
|
3087
3469
|
const { store } = makeLifecycle();
|
|
3088
3470
|
await store.readPlan(created.id);
|
|
3089
|
-
console.log(
|
|
3471
|
+
console.log(pc16.dim("saved and validated"));
|
|
3090
3472
|
}
|
|
3091
|
-
console.log(
|
|
3473
|
+
console.log(pc16.dim(relative4(process.cwd(), created.path)));
|
|
3092
3474
|
});
|
|
3093
3475
|
});
|
|
3094
3476
|
plan.command("list").option("--status <status>", "Filter by status.").option("--all", "Include completed and abandoned plans.").description("List plans in the current namespace.").action(async (opts) => {
|
|
@@ -3100,11 +3482,11 @@ function registerPlanCommands(program, runSafely2) {
|
|
|
3100
3482
|
else if (!opts.all)
|
|
3101
3483
|
plans = plans.filter((p) => p.status !== "complete" && p.status !== "abandoned");
|
|
3102
3484
|
if (plans.length === 0) {
|
|
3103
|
-
console.log(
|
|
3485
|
+
console.log(pc16.dim("no plans"));
|
|
3104
3486
|
return;
|
|
3105
3487
|
}
|
|
3106
3488
|
for (const p of plans) {
|
|
3107
|
-
console.log(`${statusColor(p.status).padEnd(20)} ${
|
|
3489
|
+
console.log(`${statusColor(p.status).padEnd(20)} ${pc16.bold(p.title)} ${pc16.dim(p.id)}`);
|
|
3108
3490
|
}
|
|
3109
3491
|
});
|
|
3110
3492
|
});
|
|
@@ -3120,7 +3502,7 @@ function registerPlanCommands(program, runSafely2) {
|
|
|
3120
3502
|
const existing = await store.readPlan(id);
|
|
3121
3503
|
await openEditor2(existing.path);
|
|
3122
3504
|
await store.readPlan(id);
|
|
3123
|
-
console.log(
|
|
3505
|
+
console.log(pc16.dim("saved and validated"));
|
|
3124
3506
|
});
|
|
3125
3507
|
});
|
|
3126
3508
|
plan.command("approve").argument("<id>").description("Promote a draft to approved (one active plan per namespace).").action(async (id) => {
|
|
@@ -3144,7 +3526,7 @@ function registerPlanCommands(program, runSafely2) {
|
|
|
3144
3526
|
...opts.commit ? { commitSha: opts.commit } : {},
|
|
3145
3527
|
...opts.notes ? { notes: opts.notes } : {}
|
|
3146
3528
|
});
|
|
3147
|
-
if (!opts.commit) console.log(
|
|
3529
|
+
if (!opts.commit) console.log(pc16.yellow("note: no --commit supplied"));
|
|
3148
3530
|
console.log(`${next.id} \u2192 ${statusColor(next.status)}`);
|
|
3149
3531
|
});
|
|
3150
3532
|
});
|
|
@@ -3163,7 +3545,7 @@ function registerPlanCommands(program, runSafely2) {
|
|
|
3163
3545
|
const args = ["diff", opts.from, ...opts.to ? [opts.to] : [], "--", relPath];
|
|
3164
3546
|
const result = await runGit(args);
|
|
3165
3547
|
if (result.stdout.trim().length === 0) {
|
|
3166
|
-
console.log(
|
|
3548
|
+
console.log(pc16.dim("no changes"));
|
|
3167
3549
|
} else {
|
|
3168
3550
|
process.stdout.write(result.stdout);
|
|
3169
3551
|
}
|
|
@@ -3180,13 +3562,13 @@ function registerPlanCommands(program, runSafely2) {
|
|
|
3180
3562
|
throw new Error(`Invalid policy "${mode}". Use one of: ${PLAN_POLICIES.join(", ")}.`);
|
|
3181
3563
|
}
|
|
3182
3564
|
await setPlanPolicy(mode);
|
|
3183
|
-
console.log(`${
|
|
3565
|
+
console.log(`${pc16.green("plan policy")} = ${mode}`);
|
|
3184
3566
|
});
|
|
3185
3567
|
});
|
|
3186
3568
|
}
|
|
3187
3569
|
|
|
3188
3570
|
// src/cli/commands/risk.ts
|
|
3189
|
-
import
|
|
3571
|
+
import pc17 from "picocolors";
|
|
3190
3572
|
function registerRiskCommands(program, runSafely2) {
|
|
3191
3573
|
const risk = program.command("risk").description("Assess evidence-backed change risk.");
|
|
3192
3574
|
risk.command("assess").option("--plan <id>", "Assess paths listed in a CodeBuddy plan.").option("--paths <paths>", "Comma-separated repo-relative paths to assess.").option("--git", "Assess staged, unstaged, and untracked git changes.").option("--limit <n>", "Maximum risk items to show.", Number).option("--json", "Print machine-readable JSON.").option("--explain", "Show evidence for each risk item.").description("Assess risk for a plan, explicit paths, or current git changes.").action(
|
|
@@ -3214,31 +3596,31 @@ function parsePaths3(value) {
|
|
|
3214
3596
|
}
|
|
3215
3597
|
function printAssessment(assessment, explain) {
|
|
3216
3598
|
if (assessment.items.length === 0) {
|
|
3217
|
-
console.log(
|
|
3599
|
+
console.log(pc17.dim("no risk evidence found"));
|
|
3218
3600
|
return;
|
|
3219
3601
|
}
|
|
3220
3602
|
for (const item of assessment.items) {
|
|
3221
3603
|
console.log(
|
|
3222
|
-
`${
|
|
3604
|
+
`${pc17.bold(item.path)} score ${scoreColor2(item.score)} category ${pc17.cyan(item.category)}`
|
|
3223
3605
|
);
|
|
3224
3606
|
console.log(` ${item.reason}`);
|
|
3225
3607
|
if (explain) {
|
|
3226
3608
|
for (const evidence of item.evidence) console.log(` \u21B3 ${formatEvidence(evidence)}`);
|
|
3227
3609
|
} else {
|
|
3228
|
-
console.log(
|
|
3610
|
+
console.log(pc17.dim(` ${item.evidence.length} evidence item(s)`));
|
|
3229
3611
|
}
|
|
3230
3612
|
}
|
|
3231
3613
|
console.log(
|
|
3232
|
-
|
|
3614
|
+
pc17.dim(
|
|
3233
3615
|
`
|
|
3234
3616
|
${assessment.stats.itemsProduced} item(s), ${assessment.stats.signalsRun} signal(s), ${assessment.stats.pathsConsidered} path(s) considered`
|
|
3235
3617
|
)
|
|
3236
3618
|
);
|
|
3237
3619
|
}
|
|
3238
3620
|
function scoreColor2(score) {
|
|
3239
|
-
if (score >= 75) return
|
|
3240
|
-
if (score >= 50) return
|
|
3241
|
-
return
|
|
3621
|
+
if (score >= 75) return pc17.red(String(score));
|
|
3622
|
+
if (score >= 50) return pc17.yellow(String(score));
|
|
3623
|
+
return pc17.green(String(score));
|
|
3242
3624
|
}
|
|
3243
3625
|
function formatEvidence(evidence) {
|
|
3244
3626
|
switch (evidence.kind) {
|
|
@@ -3260,7 +3642,7 @@ function truncate2(value, max) {
|
|
|
3260
3642
|
|
|
3261
3643
|
// src/cli/commands/rules.ts
|
|
3262
3644
|
import { relative as relative5 } from "path";
|
|
3263
|
-
import
|
|
3645
|
+
import pc18 from "picocolors";
|
|
3264
3646
|
async function resolveMode(client, value) {
|
|
3265
3647
|
if (value) {
|
|
3266
3648
|
const mode = parseMode(value);
|
|
@@ -3300,7 +3682,7 @@ function registerRulesCommands(program, runSafely2) {
|
|
|
3300
3682
|
`${actionLabel(result.action)} ${clientDisplayName(client)} workflow in ${where}`
|
|
3301
3683
|
);
|
|
3302
3684
|
if (result.action !== "unchanged") {
|
|
3303
|
-
console.log(
|
|
3685
|
+
console.log(pc18.dim(" managed block between codebuddy:workflow markers; safe to re-run"));
|
|
3304
3686
|
}
|
|
3305
3687
|
});
|
|
3306
3688
|
});
|
|
@@ -3308,20 +3690,20 @@ function registerRulesCommands(program, runSafely2) {
|
|
|
3308
3690
|
function actionLabel(action) {
|
|
3309
3691
|
switch (action) {
|
|
3310
3692
|
case "created":
|
|
3311
|
-
return
|
|
3693
|
+
return pc18.green("created");
|
|
3312
3694
|
case "added":
|
|
3313
|
-
return
|
|
3695
|
+
return pc18.green("added");
|
|
3314
3696
|
case "updated":
|
|
3315
|
-
return
|
|
3697
|
+
return pc18.cyan("updated");
|
|
3316
3698
|
case "removed":
|
|
3317
|
-
return
|
|
3699
|
+
return pc18.yellow("removed");
|
|
3318
3700
|
default:
|
|
3319
|
-
return
|
|
3701
|
+
return pc18.dim("unchanged");
|
|
3320
3702
|
}
|
|
3321
3703
|
}
|
|
3322
3704
|
|
|
3323
3705
|
// src/cli/commands/suggest.ts
|
|
3324
|
-
import
|
|
3706
|
+
import pc19 from "picocolors";
|
|
3325
3707
|
function parsePaths4(value) {
|
|
3326
3708
|
return value.split(",").map((path) => path.trim()).filter(Boolean);
|
|
3327
3709
|
}
|
|
@@ -3343,7 +3725,7 @@ function registerSuggestCommand(program, runSafely2) {
|
|
|
3343
3725
|
}
|
|
3344
3726
|
printSuggestions(result.suggestions);
|
|
3345
3727
|
console.log(
|
|
3346
|
-
|
|
3728
|
+
pc19.dim(
|
|
3347
3729
|
`
|
|
3348
3730
|
${result.stats.produced} suggestion(s) across ${result.stats.pathsConsidered} path(s)`
|
|
3349
3731
|
)
|
|
@@ -3354,72 +3736,72 @@ ${result.stats.produced} suggestion(s) across ${result.stats.pathsConsidered} pa
|
|
|
3354
3736
|
}
|
|
3355
3737
|
function printSuggestions(suggestions) {
|
|
3356
3738
|
if (suggestions.length === 0) {
|
|
3357
|
-
console.log(
|
|
3739
|
+
console.log(pc19.dim("no suggestions \u2014 nothing high-signal to report"));
|
|
3358
3740
|
return;
|
|
3359
3741
|
}
|
|
3360
3742
|
for (const suggestion of suggestions) {
|
|
3361
|
-
console.log(`${severityBadge2(suggestion.severity)} ${
|
|
3743
|
+
console.log(`${severityBadge2(suggestion.severity)} ${pc19.bold(suggestion.title)}`);
|
|
3362
3744
|
console.log(` ${suggestion.detail}`);
|
|
3363
3745
|
for (const evidence of suggestion.evidence) {
|
|
3364
|
-
console.log(
|
|
3746
|
+
console.log(pc19.dim(` \u21B3 ${evidence.kind}: ${evidence.detail}`));
|
|
3365
3747
|
}
|
|
3366
|
-
if (suggestion.commandHint) console.log(
|
|
3748
|
+
if (suggestion.commandHint) console.log(pc19.dim(` \u2192 ${suggestion.commandHint}`));
|
|
3367
3749
|
}
|
|
3368
3750
|
}
|
|
3369
3751
|
function severityBadge2(severity) {
|
|
3370
3752
|
switch (severity) {
|
|
3371
3753
|
case "high":
|
|
3372
|
-
return
|
|
3754
|
+
return pc19.red("[high]");
|
|
3373
3755
|
case "medium":
|
|
3374
|
-
return
|
|
3756
|
+
return pc19.yellow("[med ]");
|
|
3375
3757
|
case "low":
|
|
3376
|
-
return
|
|
3758
|
+
return pc19.cyan("[low ]");
|
|
3377
3759
|
default:
|
|
3378
|
-
return
|
|
3760
|
+
return pc19.dim("[info]");
|
|
3379
3761
|
}
|
|
3380
3762
|
}
|
|
3381
3763
|
|
|
3382
3764
|
// src/cli/commands/symbols.ts
|
|
3383
|
-
import { readFile as
|
|
3765
|
+
import { readFile as readFile4 } from "fs/promises";
|
|
3384
3766
|
import { resolve as resolve5 } from "path";
|
|
3385
|
-
import
|
|
3767
|
+
import pc20 from "picocolors";
|
|
3386
3768
|
function registerSymbolsCommand(program, runSafely2) {
|
|
3387
3769
|
program.command("symbols").argument("<path>", "Repo-relative or absolute path to a code file.").option("--json", "Print machine-readable JSON.").description("List a file's top-level symbols (functions, classes, types, exports).").action(async (path, opts) => {
|
|
3388
3770
|
await runSafely2(async () => {
|
|
3389
3771
|
if (!supportsSymbols(path)) {
|
|
3390
|
-
console.log(
|
|
3772
|
+
console.log(pc20.dim(`no symbol extraction for ${path} (unsupported language)`));
|
|
3391
3773
|
return;
|
|
3392
3774
|
}
|
|
3393
|
-
const content = await
|
|
3775
|
+
const content = await readFile4(resolve5(path), "utf8");
|
|
3394
3776
|
const table = extractSymbolTable(content, path);
|
|
3395
3777
|
if (opts.json) {
|
|
3396
3778
|
console.log(JSON.stringify({ path, symbols: table }, null, 2));
|
|
3397
3779
|
return;
|
|
3398
3780
|
}
|
|
3399
3781
|
if (table.length === 0) {
|
|
3400
|
-
console.log(
|
|
3782
|
+
console.log(pc20.dim("no top-level symbols found"));
|
|
3401
3783
|
return;
|
|
3402
3784
|
}
|
|
3403
3785
|
for (const symbol of table) {
|
|
3404
|
-
const badge = symbol.exported ?
|
|
3786
|
+
const badge = symbol.exported ? pc20.green("export") : pc20.dim("local ");
|
|
3405
3787
|
console.log(
|
|
3406
|
-
`${badge} ${
|
|
3788
|
+
`${badge} ${pc20.cyan(symbol.kind.padEnd(9))} ${pc20.bold(symbol.name)} ${pc20.dim(`L${symbol.startLine}-${symbol.endLine}`)}`
|
|
3407
3789
|
);
|
|
3408
3790
|
}
|
|
3409
|
-
console.log(
|
|
3791
|
+
console.log(pc20.dim(`
|
|
3410
3792
|
${table.length} symbol(s)`));
|
|
3411
3793
|
});
|
|
3412
3794
|
});
|
|
3413
3795
|
}
|
|
3414
3796
|
|
|
3415
3797
|
// src/cli/commands/telemetry.ts
|
|
3416
|
-
import
|
|
3798
|
+
import pc21 from "picocolors";
|
|
3417
3799
|
|
|
3418
3800
|
// src/telemetry/collector.ts
|
|
3419
|
-
import { createHash, timingSafeEqual } from "crypto";
|
|
3420
|
-
import { appendFile, mkdir as
|
|
3801
|
+
import { createHash as createHash2, timingSafeEqual } from "crypto";
|
|
3802
|
+
import { appendFile, mkdir as mkdir3, readdir as readdir2, readFile as readFile5 } from "fs/promises";
|
|
3421
3803
|
import { createServer as createServer2 } from "http";
|
|
3422
|
-
import { join as
|
|
3804
|
+
import { join as join7 } from "path";
|
|
3423
3805
|
import { z as z2 } from "zod";
|
|
3424
3806
|
|
|
3425
3807
|
// src/telemetry/admin-page.ts
|
|
@@ -3431,101 +3813,174 @@ var ADMIN_HTML = `<!doctype html>
|
|
|
3431
3813
|
<title>CodeBuddy Usage</title>
|
|
3432
3814
|
<style>
|
|
3433
3815
|
:root {
|
|
3434
|
-
--bg: #f4f4f2; --panel: #fcfcfb; --fg: #0b0b0b; --muted: #52514e; --line: #
|
|
3435
|
-
--bar: #2a78d6; --bar-soft: #e3edfa; --
|
|
3816
|
+
--bg: #f4f4f2; --panel: #fcfcfb; --fg: #0b0b0b; --muted: #52514e; --faint: #8a8984; --line: #e4e3de;
|
|
3817
|
+
--bar: #2a78d6; --bar-soft: #e3edfa; --hook: #eb6834; --cli: #1baf7a;
|
|
3818
|
+
--good: #0a7d0a; --bad: #c23030; --bad-soft: #fbeaea;
|
|
3436
3819
|
--sans: ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
|
|
3437
3820
|
--mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
|
3438
3821
|
}
|
|
3439
3822
|
@media (prefers-color-scheme: dark) { :root {
|
|
3440
|
-
--bg: #
|
|
3441
|
-
--bar: #3987e5; --bar-soft: #1d2b3d; --
|
|
3823
|
+
--bg: #111110; --panel: #1a1a19; --fg: #f0efec; --muted: #c3c2b7; --faint: #8f8e86; --line: #2e2e2b;
|
|
3824
|
+
--bar: #3987e5; --bar-soft: #1d2b3d; --hook: #d95926; --cli: #199e70;
|
|
3825
|
+
--good: #3fbf3f; --bad: #f07070; --bad-soft: #3a1a1a; color-scheme: dark } }
|
|
3442
3826
|
* { box-sizing: border-box; }
|
|
3443
3827
|
body { margin: 0; background: var(--bg); color: var(--fg); font: 14px/1.5 var(--sans); }
|
|
3444
|
-
|
|
3828
|
+
a { color: inherit; }
|
|
3829
|
+
.wrap { max-width: 1240px; margin: 0 auto; padding: 0 16px 56px; display: grid; gap: 16px; }
|
|
3445
3830
|
.wrap > * { min-width: 0; }
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3831
|
+
.top { position: sticky; top: 0; z-index: 5; background: color-mix(in srgb, var(--bg) 88%, transparent); backdrop-filter: blur(8px); border-bottom: 1px solid var(--line); }
|
|
3832
|
+
.bar-inner { max-width: 1240px; margin: 0 auto; padding: 12px 16px; display: flex; flex-wrap: wrap; align-items: center; gap: 10px 18px; }
|
|
3833
|
+
.brand { display: flex; align-items: center; gap: 10px; font-weight: 650; font-size: 16px; letter-spacing: -.01em; }
|
|
3834
|
+
.mark { width: 22px; height: 22px; border-radius: 6px; background: var(--bar); display: grid; place-items: center; color: #fff; font: 700 12px var(--mono); }
|
|
3835
|
+
.updated { font: 12px var(--mono); color: var(--faint); }
|
|
3836
|
+
.tabs { display: flex; gap: 4px; }
|
|
3837
|
+
.tabs a { text-decoration: none; color: var(--muted); font-weight: 500; font-size: 13px; padding: 6px 10px; border-radius: 7px; }
|
|
3838
|
+
.tabs a[aria-current="page"] { background: var(--panel); color: var(--fg); box-shadow: inset 0 0 0 1px var(--line); }
|
|
3839
|
+
.tabs a:focus-visible, .seg button:focus-visible, .btn:focus-visible, input:focus-visible, th button:focus-visible, tr.link:focus-visible { outline: 2px solid var(--bar); outline-offset: 1px; }
|
|
3450
3840
|
.spacer { flex: 1; }
|
|
3451
3841
|
.seg { display: inline-flex; border: 1px solid var(--line); border-radius: 8px; overflow: hidden; background: var(--panel); }
|
|
3452
|
-
.seg button { border: 0; background: none; color: var(--muted); font: 500 12px var(--sans); padding: 6px
|
|
3842
|
+
.seg button { border: 0; background: none; color: var(--muted); font: 500 12px var(--sans); padding: 6px 11px; cursor: pointer; }
|
|
3453
3843
|
.seg button + button { border-left: 1px solid var(--line); }
|
|
3454
3844
|
.seg button[aria-pressed="true"] { background: var(--fg); color: var(--bg); }
|
|
3455
|
-
|
|
3456
|
-
.
|
|
3457
|
-
.
|
|
3845
|
+
h2 { font-size: 12px; margin: 0; text-transform: uppercase; letter-spacing: .08em; color: var(--muted); font-weight: 600; }
|
|
3846
|
+
.head { display: flex; align-items: baseline; flex-wrap: wrap; gap: 6px 12px; margin-bottom: 12px; }
|
|
3847
|
+
.head .note { font-size: 12px; color: var(--faint); }
|
|
3848
|
+
.hero { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(190px, 100%), 1fr)); gap: 12px; padding-top: 16px; }
|
|
3849
|
+
.kpi { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 14px 16px; }
|
|
3458
3850
|
.kpi .label { font-size: 12px; color: var(--muted); }
|
|
3459
|
-
.kpi .value { font:
|
|
3460
|
-
.kpi .sub { font-size: 12px; color: var(--muted); margin-top:
|
|
3461
|
-
.
|
|
3462
|
-
.
|
|
3463
|
-
.
|
|
3464
|
-
.
|
|
3465
|
-
.
|
|
3466
|
-
svg
|
|
3467
|
-
|
|
3468
|
-
.
|
|
3469
|
-
.
|
|
3470
|
-
.
|
|
3471
|
-
.
|
|
3851
|
+
.kpi .value { font: 650 32px/1.15 var(--mono); font-variant-numeric: tabular-nums; margin-top: 6px; letter-spacing: -.02em; }
|
|
3852
|
+
.kpi .sub { font-size: 12px; color: var(--muted); margin-top: 4px; }
|
|
3853
|
+
.up { color: var(--good); font-weight: 600; }
|
|
3854
|
+
.down { color: var(--bad); font-weight: 600; }
|
|
3855
|
+
.panel { background: var(--panel); border: 1px solid var(--line); border-radius: 12px; padding: 16px; min-width: 0; }
|
|
3856
|
+
.grid2 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(340px, 100%), 1fr)); gap: 16px; }
|
|
3857
|
+
.grid3 { display: grid; grid-template-columns: repeat(auto-fit, minmax(min(260px, 100%), 1fr)); gap: 16px; }
|
|
3858
|
+
.chart svg { display: block; width: 100%; height: 190px; overflow: visible; }
|
|
3859
|
+
svg text { fill: var(--faint); font: 11px var(--mono); }
|
|
3860
|
+
.hit { fill: transparent; cursor: crosshair; }
|
|
3861
|
+
.hit:hover { fill: color-mix(in srgb, var(--fg) 6%, transparent); }
|
|
3862
|
+
.tip { position: absolute; pointer-events: none; background: var(--fg); color: var(--bg); font: 12px var(--mono); padding: 5px 8px; border-radius: 6px; white-space: nowrap; display: none; z-index: 10; }
|
|
3863
|
+
.rows { display: grid; gap: 8px; }
|
|
3864
|
+
.row { display: grid; grid-template-columns: minmax(0, 1fr) auto; gap: 3px 12px; font-size: 13px; }
|
|
3865
|
+
.row .track { grid-column: 1 / -1; height: 8px; background: var(--bar-soft); border-radius: 4px; overflow: hidden; }
|
|
3866
|
+
.row .fill { height: 8px; background: var(--bar); border-radius: 4px; }
|
|
3472
3867
|
.row .n { font-family: var(--mono); font-variant-numeric: tabular-nums; color: var(--muted); }
|
|
3868
|
+
.funnel .row { font-size: 14px; }
|
|
3869
|
+
.funnel .track { height: 22px; border-radius: 6px; }
|
|
3870
|
+
.funnel .fill { height: 22px; border-radius: 6px; }
|
|
3473
3871
|
.scroll { overflow-x: auto; }
|
|
3474
3872
|
table { width: 100%; border-collapse: collapse; font-size: 13px; }
|
|
3475
3873
|
th { text-align: left; font: 600 11px var(--sans); text-transform: uppercase; letter-spacing: .06em; color: var(--muted); padding: 6px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
3476
|
-
|
|
3874
|
+
th button { all: unset; cursor: pointer; }
|
|
3875
|
+
th button::after { content: attr(data-dir); margin-left: 4px; color: var(--faint); }
|
|
3876
|
+
td { padding: 7px 8px; border-bottom: 1px solid var(--line); white-space: nowrap; }
|
|
3477
3877
|
tr:last-child td { border-bottom: 0; }
|
|
3478
3878
|
td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric: tabular-nums; }
|
|
3879
|
+
tr.link { cursor: pointer; }
|
|
3880
|
+
tr.link:hover td { background: var(--bar-soft); }
|
|
3881
|
+
.inline { display: inline-block; height: 6px; background: var(--bar); border-radius: 3px; vertical-align: middle; margin-left: 8px; }
|
|
3882
|
+
.heat td { text-align: center; font: 12px var(--mono); padding: 6px 4px; border: 2px solid var(--panel); border-radius: 6px; min-width: 44px; }
|
|
3883
|
+
.heat td.lab { text-align: left; color: var(--muted); font-family: var(--sans); white-space: nowrap; }
|
|
3884
|
+
.heat td.empty { background: transparent; color: var(--faint); }
|
|
3479
3885
|
.bad { color: var(--bad); }
|
|
3480
3886
|
.muted { color: var(--muted); }
|
|
3481
3887
|
.mono { font-family: var(--mono); }
|
|
3482
|
-
.
|
|
3483
|
-
.
|
|
3484
|
-
|
|
3485
|
-
.
|
|
3486
|
-
.
|
|
3888
|
+
.empty-state { padding: 36px 16px; text-align: center; color: var(--muted); }
|
|
3889
|
+
.toolbar { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; margin-bottom: 12px; }
|
|
3890
|
+
input[type="search"] { font: 13px var(--sans); padding: 7px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg); color: var(--fg); min-width: 0; flex: 1 1 200px; }
|
|
3891
|
+
.btn { border: 1px solid var(--line); background: var(--panel); color: var(--fg); font: 500 12px var(--sans); padding: 7px 12px; border-radius: 8px; cursor: pointer; text-decoration: none; }
|
|
3892
|
+
.chips { display: flex; flex-wrap: wrap; gap: 6px; }
|
|
3893
|
+
.chip { display: inline-flex; align-items: center; gap: 6px; font: 600 11px var(--mono); padding: 2px 8px; border-radius: 999px; background: var(--bar-soft); color: var(--fg); }
|
|
3894
|
+
.sw { width: 10px; height: 10px; border-radius: 3px; display: inline-block; }
|
|
3895
|
+
.sessions { display: grid; gap: 6px; max-height: 520px; overflow: auto; }
|
|
3896
|
+
.session { all: unset; display: grid; gap: 2px; padding: 8px 10px; border: 1px solid var(--line); border-radius: 8px; cursor: pointer; font-size: 12px; }
|
|
3897
|
+
.session[aria-pressed="true"] { border-color: var(--bar); box-shadow: inset 3px 0 0 var(--bar); }
|
|
3898
|
+
.session:focus-visible { outline: 2px solid var(--bar); }
|
|
3899
|
+
.split { display: grid; grid-template-columns: minmax(220px, 300px) minmax(0, 1fr); gap: 16px; }
|
|
3900
|
+
@media (max-width: 820px) { .split { grid-template-columns: 1fr; } .wf .step { grid-template-columns: 120px 52px minmax(0, 1fr) 56px; } }
|
|
3901
|
+
.wf { display: grid; gap: 3px; font-size: 12px; }
|
|
3902
|
+
.wf .step { display: grid; grid-template-columns: 170px 64px minmax(0, 1fr) 70px; gap: 10px; align-items: center; }
|
|
3903
|
+
.wf .lane { position: relative; height: 14px; background: var(--bg); border-radius: 3px; }
|
|
3904
|
+
.wf .seg-bar { position: absolute; top: 2px; height: 10px; min-width: 3px; border-radius: 3px; }
|
|
3905
|
+
.wf .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-family: var(--mono); }
|
|
3906
|
+
.wf .at { font-family: var(--mono); color: var(--faint); text-align: right; }
|
|
3907
|
+
.wf .t { text-align: right; font-family: var(--mono); color: var(--muted); }
|
|
3908
|
+
.legend { display: flex; gap: 14px; font-size: 12px; color: var(--muted); margin-bottom: 10px; flex-wrap: wrap; }
|
|
3909
|
+
.legend span { display: inline-flex; align-items: center; gap: 6px; }
|
|
3910
|
+
footer { font-size: 12px; color: var(--faint); }
|
|
3911
|
+
[hidden] { display: none !important; }
|
|
3487
3912
|
</style>
|
|
3488
3913
|
</head>
|
|
3489
3914
|
<body>
|
|
3915
|
+
<div class="top"><div class="bar-inner">
|
|
3916
|
+
<div class="brand"><span class="mark">cb</span>CodeBuddy usage</div>
|
|
3917
|
+
<nav class="tabs" aria-label="Views">
|
|
3918
|
+
<a href="#overview" data-view="overview">Overview</a>
|
|
3919
|
+
<a href="#users" data-view="users">Users</a>
|
|
3920
|
+
<a href="#events" data-view="events">Events</a>
|
|
3921
|
+
</nav>
|
|
3922
|
+
<span class="spacer"></span>
|
|
3923
|
+
<span class="updated" id="updated">loading\u2026</span>
|
|
3924
|
+
<div class="seg" role="group" aria-label="Time range" id="range">
|
|
3925
|
+
<button data-days="7">7d</button><button data-days="30" aria-pressed="true">30d</button><button data-days="90">90d</button><button data-days="365">1y</button>
|
|
3926
|
+
</div>
|
|
3927
|
+
</div></div>
|
|
3928
|
+
|
|
3490
3929
|
<div class="wrap">
|
|
3491
|
-
<
|
|
3492
|
-
<
|
|
3493
|
-
<
|
|
3494
|
-
|
|
3495
|
-
<
|
|
3496
|
-
<button data-days="30" aria-pressed="true">30 days</button>
|
|
3497
|
-
<button data-days="90">90 days</button>
|
|
3498
|
-
<button data-days="365">1 year</button>
|
|
3930
|
+
<section id="view-overview">
|
|
3931
|
+
<div class="hero" id="hero" aria-label="Headline numbers"></div>
|
|
3932
|
+
<div class="grid2" style="margin-top:16px">
|
|
3933
|
+
<section class="panel"><div class="head"><h2>Total installs</h2><span class="note">all-time, by first-seen day</span></div><div class="chart" id="cumulative"></div></section>
|
|
3934
|
+
<section class="panel"><div class="head"><h2>Daily active installs</h2><span class="note">installs that sent any event</span></div><div class="chart" id="active"></div></section>
|
|
3499
3935
|
</div>
|
|
3500
|
-
|
|
3501
|
-
|
|
3502
|
-
|
|
3503
|
-
|
|
3504
|
-
<section class="panel"><h2>
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
<section class="panel"><h2>
|
|
3511
|
-
</
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
<div class="
|
|
3936
|
+
<div class="grid2" style="margin-top:16px">
|
|
3937
|
+
<section class="panel"><div class="head"><h2>Activation</h2><span class="note">installs active in range reaching each step</span></div><div class="rows funnel" id="funnel"></div></section>
|
|
3938
|
+
<section class="panel"><div class="head"><h2>Retention by weekly cohort</h2><span class="note">share of each week's new installs active in later weeks</span></div><div class="scroll" id="cohorts"></div></section>
|
|
3939
|
+
</div>
|
|
3940
|
+
<section class="panel" style="margin-top:16px"><div class="head"><h2>Feature usage</h2><span class="note">MCP tools, hook steps (recall, capture\u2026) and CLI commands</span></div><div class="scroll" id="features"></div></section>
|
|
3941
|
+
<div class="grid3" style="margin-top:16px">
|
|
3942
|
+
<section class="panel"><div class="head"><h2>Agent clients</h2></div><div class="rows" id="clients"></div></section>
|
|
3943
|
+
<section class="panel"><div class="head"><h2>Versions</h2></div><div class="rows" id="versions"></div></section>
|
|
3944
|
+
<section class="panel"><div class="head"><h2>Platforms</h2></div><div class="rows" id="platforms"></div></section>
|
|
3945
|
+
</div>
|
|
3946
|
+
<section class="panel" style="margin-top:16px"><div class="head"><h2>npm downloads per day</h2><span class="note" id="npmNote"></span></div><div class="chart" id="npm"></div></section>
|
|
3947
|
+
</section>
|
|
3948
|
+
|
|
3949
|
+
<section id="view-users" class="panel" style="margin-top:16px" hidden>
|
|
3950
|
+
<div class="toolbar">
|
|
3951
|
+
<h2>Users</h2><span class="muted" id="userCount" style="font-size:12px"></span><span class="spacer"></span>
|
|
3952
|
+
<input type="search" id="userSearch" placeholder="Filter by id, client, version, platform\u2026" aria-label="Filter users">
|
|
3953
|
+
<button class="btn" id="csv">Download CSV</button>
|
|
3954
|
+
</div>
|
|
3955
|
+
<div class="scroll" id="users"></div>
|
|
3515
3956
|
</section>
|
|
3516
|
-
|
|
3517
|
-
<
|
|
3957
|
+
|
|
3958
|
+
<section id="view-install" style="margin-top:16px" hidden></section>
|
|
3959
|
+
|
|
3960
|
+
<section id="view-events" class="panel" style="margin-top:16px" hidden>
|
|
3961
|
+
<div class="toolbar"><h2>Latest events</h2><span class="spacer"></span><div class="chips" id="kindFilter"></div></div>
|
|
3962
|
+
<div class="scroll" id="recent"></div>
|
|
3963
|
+
</section>
|
|
3964
|
+
|
|
3965
|
+
<footer>Anonymous telemetry from installs that did not opt out: random install ids, tool and command names, timings, versions. No prompts, code, paths or repository names are collected. Shares count each install once, from its latest batch.</footer>
|
|
3518
3966
|
</div>
|
|
3519
|
-
<div class="tip" id="tip"></div>
|
|
3967
|
+
<div class="tip" id="tip" role="status"></div>
|
|
3520
3968
|
<script>
|
|
3521
3969
|
(function () {
|
|
3522
3970
|
var days = 30;
|
|
3523
|
-
|
|
3524
|
-
var
|
|
3525
|
-
var
|
|
3526
|
-
var
|
|
3971
|
+
var summary = null, npm = null, install = null;
|
|
3972
|
+
var userSort = { key: "lastSeen", dir: -1 };
|
|
3973
|
+
var kindShown = "all";
|
|
3974
|
+
var selectedSession = null;
|
|
3975
|
+
var path = location.pathname;
|
|
3976
|
+
var base = path.charAt(path.length - 1) === "/" ? path : path + "/";
|
|
3527
3977
|
var tip = document.getElementById("tip");
|
|
3978
|
+
var NL = String.fromCharCode(10);
|
|
3979
|
+
var KIND_COLOR = { tool: "var(--bar)", cli: "var(--cli)" };
|
|
3980
|
+
function kindColor(kind) { return KIND_COLOR[kind] || "var(--hook)"; }
|
|
3981
|
+
function kindGroup(kind) { return kind === "tool" ? "MCP tool" : kind === "cli" ? "CLI command" : "hook step"; }
|
|
3528
3982
|
|
|
3983
|
+
function $(id) { return document.getElementById(id); }
|
|
3529
3984
|
function esc(value) {
|
|
3530
3985
|
return String(value == null ? "" : value).replace(/[&<>"']/g, function (c) {
|
|
3531
3986
|
return { "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c];
|
|
@@ -3533,36 +3988,57 @@ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric:
|
|
|
3533
3988
|
}
|
|
3534
3989
|
function num(n) { return Math.round(n).toLocaleString(); }
|
|
3535
3990
|
function compact(n) { return n >= 1e6 ? (n / 1e6).toFixed(1) + "M" : n >= 1e4 ? (n / 1e3).toFixed(1) + "k" : num(n); }
|
|
3536
|
-
function pct(n) { return (n * 100).toFixed(n < 0.1 ? 1 : 0) + "%"; }
|
|
3537
|
-
function
|
|
3991
|
+
function pct(n) { return (n * 100).toFixed(n > 0 && n < 0.1 ? 1 : 0) + "%"; }
|
|
3992
|
+
function plural(n, word) { return num(n) + " " + word + (n === 1 ? "" : "s"); }
|
|
3993
|
+
function offset(n) { var s = Math.round(n / 1000); return "+" + (s >= 60 ? Math.floor(s / 60) + "m " + (s % 60) + "s" : s + "s"); }
|
|
3994
|
+
function ms(n) { return n >= 60000 ? (n / 60000).toFixed(1) + " min" : n >= 1000 ? (n / 1000).toFixed(1) + " s" : Math.round(n) + " ms"; }
|
|
3995
|
+
function date(iso) { return new Date(iso).toLocaleDateString(undefined, { month: "short", day: "numeric" }); }
|
|
3996
|
+
function ago(iso) {
|
|
3997
|
+
var s = Math.max(0, (Date.now() - Date.parse(iso)) / 1000);
|
|
3998
|
+
return s < 60 ? "just now" : s < 3600 ? Math.floor(s / 60) + "m ago" : s < 86400 ? Math.floor(s / 3600) + "h ago" : Math.floor(s / 86400) + "d ago";
|
|
3999
|
+
}
|
|
4000
|
+
function get(url) { return fetch(base + url).then(function (r) { return r.ok ? r.json() : null; }); }
|
|
3538
4001
|
|
|
3539
4002
|
function kpi(label, value, sub) {
|
|
3540
|
-
return '<div class="kpi"><div class="label">' + esc(label) + '</div><div class="value">' + esc(value) +
|
|
3541
|
-
|
|
4003
|
+
return '<div class="kpi"><div class="label">' + esc(label) + '</div><div class="value">' + esc(value) + '</div><div class="sub">' + (sub || "") + "</div></div>";
|
|
4004
|
+
}
|
|
4005
|
+
function delta(now, before) {
|
|
4006
|
+
if (!before) return now ? '<span class="up">\u25B2 new</span> vs previous 7 days' : "no activity yet";
|
|
4007
|
+
var change = (now - before) / before;
|
|
4008
|
+
return '<span class="' + (change >= 0 ? "up" : "down") + '">' + (change >= 0 ? "\u25B2 " : "\u25BC ") + pct(Math.abs(change)) + "</span> vs previous 7 days";
|
|
3542
4009
|
}
|
|
3543
4010
|
|
|
3544
|
-
/** Bars
|
|
3545
|
-
|
|
3546
|
-
|
|
3547
|
-
var
|
|
4011
|
+
/** Bars or a filled line over days, one measure; every point has a hover target. */
|
|
4012
|
+
/** Draw into a container at its own pixel width, so labels never stretch. */
|
|
4013
|
+
function draw(id, points, label, line) {
|
|
4014
|
+
var el = $(id);
|
|
4015
|
+
if (el) el.innerHTML = chart(points, label, line, el.clientWidth);
|
|
4016
|
+
}
|
|
4017
|
+
function chart(points, label, line, width) {
|
|
4018
|
+
if (!points.length) return '<div class="empty-state">No data yet.</div>';
|
|
4019
|
+
var w = Math.max(280, width || 640), h = 190, top = 10, bottom = 22, left = 40, plot = h - top - bottom;
|
|
3548
4020
|
var max = Math.max.apply(null, points.map(function (p) { return p.value; }).concat([1]));
|
|
3549
4021
|
var step = (w - left) / points.length;
|
|
3550
|
-
var
|
|
4022
|
+
var y = function (v) { return top + plot * (1 - v / max); };
|
|
3551
4023
|
var out = '<svg viewBox="0 0 ' + w + " " + h + '" preserveAspectRatio="none" role="img" aria-label="' + esc(label) + '">';
|
|
3552
4024
|
[0, 0.5, 1].forEach(function (f) {
|
|
3553
|
-
|
|
3554
|
-
out += '<
|
|
3555
|
-
out += '<text x="' + (left - 6) + '" y="' + (y + 4) + '" text-anchor="end">' + esc(compact(max * f)) + "</text>";
|
|
4025
|
+
out += '<line x1="' + left + '" x2="' + w + '" y1="' + y(max * f) + '" y2="' + y(max * f) + '" stroke="var(--line)"/>';
|
|
4026
|
+
out += '<text x="' + (left - 6) + '" y="' + (y(max * f) + 4) + '" text-anchor="end">' + esc(compact(max * f)) + "</text>";
|
|
3556
4027
|
});
|
|
4028
|
+
if (line) {
|
|
4029
|
+
var pts = points.map(function (p, i) { return (left + i * step + step / 2).toFixed(1) + "," + y(p.value).toFixed(1); });
|
|
4030
|
+
out += '<path d="M' + (left + step / 2) + "," + (top + plot) + " L" + pts.join(" L") + " L" + (left + (points.length - 0.5) * step) + "," + (top + plot) + ' Z" fill="var(--bar-soft)"/>';
|
|
4031
|
+
out += '<polyline points="' + pts.join(" ") + '" fill="none" stroke="var(--bar)" stroke-width="2" vector-effect="non-scaling-stroke" stroke-linejoin="round"/>';
|
|
4032
|
+
}
|
|
3557
4033
|
points.forEach(function (p, i) {
|
|
3558
|
-
var
|
|
3559
|
-
|
|
3560
|
-
|
|
3561
|
-
|
|
3562
|
-
|
|
4034
|
+
var x = left + i * step;
|
|
4035
|
+
if (!line) {
|
|
4036
|
+
var bh = plot * (p.value / max), bw = Math.max(1, step - 2);
|
|
4037
|
+
out += '<rect x="' + (x + 1) + '" y="' + (top + plot - bh) + '" width="' + bw + '" height="' + bh + '" rx="' + Math.min(2, bw / 2) + '" fill="var(--bar)"/>';
|
|
4038
|
+
}
|
|
4039
|
+
out += '<rect class="hit" x="' + x + '" y="' + top + '" width="' + step + '" height="' + plot + '" data-tip="' + esc(p.date + " \xB7 " + num(p.value) + " " + label) + '"/>';
|
|
3563
4040
|
});
|
|
3564
|
-
|
|
3565
|
-
out += '<text x="' + left + '" y="' + (h - 6) + '">' + esc(first) + '</text><text x="' + w + '" y="' + (h - 6) + '" text-anchor="end">' + esc(last) + "</text>";
|
|
4041
|
+
out += '<text x="' + left + '" y="' + (h - 5) + '">' + esc(points[0].date) + '</text><text x="' + w + '" y="' + (h - 5) + '" text-anchor="end">' + esc(points[points.length - 1].date) + "</text>";
|
|
3566
4042
|
return out + "</svg>";
|
|
3567
4043
|
}
|
|
3568
4044
|
|
|
@@ -3570,85 +4046,230 @@ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric:
|
|
|
3570
4046
|
if (!items.length) return '<div class="muted">No data yet.</div>';
|
|
3571
4047
|
return items.slice(0, 8).map(function (item) {
|
|
3572
4048
|
var share = total ? item.installs / total : 0;
|
|
3573
|
-
return '<div class="row"><span>' + esc(item.label) + '</span><span class="n">' + num(item.installs) + " \xB7 " + pct(share) +
|
|
3574
|
-
'</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
|
|
4049
|
+
return '<div class="row"><span>' + esc(item.label) + '</span><span class="n">' + num(item.installs) + " \xB7 " + pct(share) + '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
|
|
3575
4050
|
}).join("");
|
|
3576
4051
|
}
|
|
3577
4052
|
|
|
3578
|
-
function
|
|
3579
|
-
|
|
3580
|
-
|
|
4053
|
+
function renderOverview() {
|
|
4054
|
+
var t = summary.totals;
|
|
4055
|
+
var npm30 = npmTotal(30);
|
|
4056
|
+
$("hero").innerHTML =
|
|
4057
|
+
kpi("Weekly active installs", num(t.wau), delta(t.wau, t.wauPrevious)) +
|
|
4058
|
+
kpi("Total installs", num(t.allTimeInstalls), "+" + num(t.newInRange) + " in the last " + summary.days + " days \xB7 " + num(t.newToday) + " today") +
|
|
4059
|
+
kpi("Week-1 retention", t.week1Retention == null ? "\u2013" : pct(t.week1Retention), t.week1Retention == null ? "needs a cohort older than 7 days" : "new installs active again the next week") +
|
|
4060
|
+
kpi("Agent sessions", compact(t.sessions), num(t.dau) + " installs active today \xB7 " + num(t.mau) + " this month") +
|
|
4061
|
+
kpi("npm downloads \xB7 30 days", npm30 == null ? "\u2013" : num(npm30), npm && npm.error ? esc(npm.error) : num(npmTotal(365) || 0) + " in the last year");
|
|
4062
|
+
draw("cumulative", summary.daily.map(function (d) { return { date: d.date, value: d.totalInstalls }; }), "total installs", true);
|
|
4063
|
+
draw("active", summary.daily.map(function (d) { return { date: d.date, value: d.installs }; }), "active installs", false);
|
|
4064
|
+
|
|
4065
|
+
var active = summary.funnel[0] ? summary.funnel[0].installs : 0;
|
|
4066
|
+
$("funnel").innerHTML = active ? summary.funnel.map(function (step) {
|
|
4067
|
+
var share = active ? step.installs / active : 0;
|
|
4068
|
+
return '<div class="row"><span>' + esc(step.label) + '</span><span class="n">' + num(step.installs) + " \xB7 " + pct(share) + '</span><div class="track"><div class="fill" style="width:' + (share * 100).toFixed(1) + '%"></div></div></div>';
|
|
4069
|
+
}).join("") : '<div class="empty-state">No active installs in this range.</div>';
|
|
4070
|
+
|
|
4071
|
+
var cohorts = summary.cohorts.filter(function (c) { return c.size > 0; });
|
|
4072
|
+
var weeks = summary.cohorts.length;
|
|
4073
|
+
$("cohorts").innerHTML = cohorts.length
|
|
4074
|
+
? '<table class="heat"><thead><tr><th>Cohort</th><th class="n">Installs</th>' + Array.from({ length: weeks }, function (_, i) { return '<th class="n">W' + i + "</th>"; }).join("") + "</tr></thead><tbody>" +
|
|
4075
|
+
cohorts.map(function (c) {
|
|
4076
|
+
var cells = "";
|
|
4077
|
+
for (var i = 0; i < weeks; i += 1) {
|
|
4078
|
+
var v = c.weeks[i];
|
|
4079
|
+
if (v == null) { cells += '<td class="empty">\xB7</td>'; continue; }
|
|
4080
|
+
var strength = Math.round(12 + v * 88);
|
|
4081
|
+
cells += '<td style="background:color-mix(in oklab, var(--bar) ' + strength + '%, var(--panel));color:' + (v > 0.5 ? "#fff" : "var(--fg)") + '" title="' + esc(c.start + " cohort, week " + i + ": " + pct(v)) + '">' + pct(v) + "</td>";
|
|
4082
|
+
}
|
|
4083
|
+
return '<tr><td class="lab">' + esc(date(c.start)) + '</td><td class="n">' + num(c.size) + "</td>" + cells + "</tr>";
|
|
4084
|
+
}).join("") + "</tbody></table>"
|
|
4085
|
+
: '<div class="empty-state">Cohorts appear once new installs report in.</div>';
|
|
4086
|
+
|
|
4087
|
+
var maxCalls = Math.max.apply(null, summary.features.map(function (f) { return f.calls; }).concat([1]));
|
|
4088
|
+
$("features").innerHTML = summary.features.length
|
|
4089
|
+
? '<table><thead><tr><th>Feature</th><th class="n">Calls</th><th class="n">Installs</th><th class="n">Median</th><th class="n">p95</th><th class="n">Errors</th></tr></thead><tbody>' +
|
|
4090
|
+
summary.features.map(function (f) {
|
|
4091
|
+
return '<tr><td class="mono">' + esc(f.label) + '<span class="inline" style="width:' + Math.max(2, (f.calls / maxCalls) * 120).toFixed(0) + 'px"></span></td><td class="n">' + num(f.calls) + '</td><td class="n">' + num(f.installs) +
|
|
4092
|
+
'</td><td class="n">' + ms(f.p50ms) + '</td><td class="n">' + ms(f.p95ms) + '</td><td class="n' + (f.errorRate > 0.05 ? " bad" : "") + '">' + pct(f.errorRate) + "</td></tr>";
|
|
4093
|
+
}).join("") + "</tbody></table>"
|
|
4094
|
+
: '<div class="empty-state">No events yet. Installs send a batch at most every 10 minutes.</div>';
|
|
4095
|
+
var installs = summary.versions.reduce(function (sum, v) { return sum + v.installs; }, 0);
|
|
4096
|
+
$("clients").innerHTML = rows(summary.clients, installs);
|
|
4097
|
+
$("versions").innerHTML = rows(summary.versions, installs);
|
|
4098
|
+
$("platforms").innerHTML = rows(summary.platforms, installs);
|
|
4099
|
+
renderNpm();
|
|
3581
4100
|
}
|
|
4101
|
+
|
|
3582
4102
|
function npmTotal(n) {
|
|
3583
4103
|
if (!npm || !npm.downloads) return null;
|
|
3584
4104
|
return npm.downloads.slice(-n).reduce(function (sum, d) { return sum + d.downloads; }, 0);
|
|
3585
4105
|
}
|
|
4106
|
+
function renderNpm() {
|
|
4107
|
+
if (!npm) return;
|
|
4108
|
+
if (npm.error) { $("npm").innerHTML = '<div class="empty-state">' + esc(npm.error) + "</div>"; return; }
|
|
4109
|
+
$("npmNote").textContent = npm.package + " \xB7 public npm registry";
|
|
4110
|
+
draw("npm", npm.downloads.slice(-Math.min(days, 365)).map(function (d) { return { date: d.day, value: d.downloads }; }), "downloads", false);
|
|
4111
|
+
}
|
|
3586
4112
|
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
return "<
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
document.getElementById("versions").innerHTML = rows(summary.versions, installs);
|
|
3612
|
-
document.getElementById("platforms").innerHTML = rows(summary.platforms, installs);
|
|
3613
|
-
document.getElementById("userCount").textContent = num(summary.users.length) + " installs seen in " + summary.days + " days";
|
|
3614
|
-
document.getElementById("users").innerHTML = summary.users.length
|
|
3615
|
-
? "<table><thead><tr><th>Install</th><th>First seen</th><th>Last active</th><th class=\\"n\\">Events</th><th class=\\"n\\">Sessions</th><th class=\\"n\\">Projects</th><th>Client</th><th>Version</th><th>Platform</th><th>Top feature</th></tr></thead><tbody>" +
|
|
3616
|
-
summary.users.map(function (u) {
|
|
3617
|
-
return '<tr><td class="mono">' + esc(u.install) + "</td><td>" + esc(new Date(u.firstSeen).toLocaleDateString()) + "</td><td>" + esc(new Date(u.lastSeen).toLocaleString()) +
|
|
3618
|
-
'</td><td class="n">' + num(u.events) + '</td><td class="n">' + num(u.sessions) + '</td><td class="n">' + num(u.projects) + "</td><td>" + esc(u.client) +
|
|
3619
|
-
'</td><td class="mono">' + esc(u.version) + "</td><td>" + esc(u.platform) + '</td><td class="mono">' + esc(u.topFeature) + "</td></tr>";
|
|
4113
|
+
var USER_COLS = [
|
|
4114
|
+
["install", "Install"], ["firstSeen", "First seen"], ["lastSeen", "Last active"], ["activeDays", "Active days", 1],
|
|
4115
|
+
["sessions", "Sessions", 1], ["events", "Events", 1], ["errors", "Errors", 1], ["client", "Client"], ["version", "Version"], ["platform", "Platform"], ["topFeature", "Top feature"]
|
|
4116
|
+
];
|
|
4117
|
+
function filteredUsers() {
|
|
4118
|
+
var q = $("userSearch").value.trim().toLowerCase();
|
|
4119
|
+
return summary.users.filter(function (u) {
|
|
4120
|
+
return !q || [u.install, u.client, u.version, u.platform, u.topFeature].join(" ").toLowerCase().indexOf(q) !== -1;
|
|
4121
|
+
}).sort(function (a, b) {
|
|
4122
|
+
var x = a[userSort.key], y = b[userSort.key];
|
|
4123
|
+
return (x < y ? -1 : x > y ? 1 : 0) * userSort.dir;
|
|
4124
|
+
});
|
|
4125
|
+
}
|
|
4126
|
+
function renderUsers() {
|
|
4127
|
+
var list = filteredUsers();
|
|
4128
|
+
$("userCount").textContent = num(summary.users.length) + " installs active in the last " + summary.days + " days";
|
|
4129
|
+
$("users").innerHTML = list.length
|
|
4130
|
+
? "<table><thead><tr>" + USER_COLS.map(function (c) {
|
|
4131
|
+
return "<th" + (c[2] ? ' class="n"' : "") + '><button data-sort="' + c[0] + '" data-dir="' + (userSort.key === c[0] ? (userSort.dir > 0 ? "\u2191" : "\u2193") : "") + '">' + c[1] + "</button></th>";
|
|
4132
|
+
}).join("") + "</tr></thead><tbody>" +
|
|
4133
|
+
list.map(function (u) {
|
|
4134
|
+
return '<tr class="link" tabindex="0" data-install="' + esc(u.install) + '"><td class="mono">' + esc(u.install) + "</td><td>" + esc(date(u.firstSeen)) + "</td><td>" + esc(ago(u.lastSeen)) +
|
|
4135
|
+
'</td><td class="n">' + num(u.activeDays) + '</td><td class="n">' + num(u.sessions) + '</td><td class="n">' + num(u.events) + '</td><td class="n' + (u.errors ? " bad" : "") + '">' + num(u.errors) +
|
|
4136
|
+
"</td><td>" + esc(u.client) + '</td><td class="mono">' + esc(u.version) + "</td><td>" + esc(u.platform) + '</td><td class="mono">' + esc(u.topFeature) + "</td></tr>";
|
|
3620
4137
|
}).join("") + "</tbody></table>"
|
|
3621
|
-
: '<div class="empty">No users yet
|
|
3622
|
-
|
|
3623
|
-
|
|
3624
|
-
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
4138
|
+
: '<div class="empty-state">' + (summary.users.length ? "No installs match this filter." : "No users yet.") + "</div>";
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
function renderInstall() {
|
|
4142
|
+
var el = $("view-install");
|
|
4143
|
+
if (!install) { el.innerHTML = '<div class="panel empty-state">Loading\u2026</div>'; return; }
|
|
4144
|
+
if (install.missing) { el.innerHTML = '<div class="panel empty-state">No events for this install in the last 90 days. <a href="#users">Back to users</a></div>'; return; }
|
|
4145
|
+
var errors = install.events.filter(function (e) { return !e.ok; }).length;
|
|
4146
|
+
var perDay = {};
|
|
4147
|
+
install.perDay.forEach(function (d) { perDay[d.date] = d.events; });
|
|
4148
|
+
var points = [];
|
|
4149
|
+
for (var i = 89; i >= 0; i -= 1) {
|
|
4150
|
+
var d = new Date(Date.now() - i * 86400000).toISOString().slice(0, 10);
|
|
4151
|
+
points.push({ date: d, value: perDay[d] || 0 });
|
|
4152
|
+
}
|
|
4153
|
+
if (!selectedSession || !install.sessions.some(function (s) { return s.id === selectedSession; })) selectedSession = install.sessions[0] ? install.sessions[0].id : null;
|
|
4154
|
+
el.innerHTML =
|
|
4155
|
+
'<div class="toolbar"><a class="btn" href="#users">\u2190 Users</a><h2 style="font-size:14px;color:var(--fg);text-transform:none;letter-spacing:0">Install <span class="mono">' + esc(install.install) + "</span></h2>" +
|
|
4156
|
+
'<span class="chips"><span class="chip">' + esc(install.client) + '</span><span class="chip">v' + esc(install.version) + '</span><span class="chip">' + esc(install.platform) + '</span><span class="chip">node ' + esc(install.node) + "</span></span></div>" +
|
|
4157
|
+
'<div class="hero" style="padding-top:0">' + kpi("Events \xB7 90 days", num(install.events.length), errors ? '<span class="bad">' + plural(errors, "error") + "</span>" : "no errors") +
|
|
4158
|
+
kpi("Sessions", num(install.sessions.length), "agent sessions with CodeBuddy") + kpi("Active days", num(install.perDay.length), "in the last 90 days") + kpi("Projects", num(install.projects), "hashed, never named") + "</div>" +
|
|
4159
|
+
'<section class="panel" style="margin-top:16px"><div class="head"><h2>Activity</h2><span class="note">events per day, last 90 days</span></div><div class="chart" id="installActivity"></div></section>' +
|
|
4160
|
+
'<section class="panel" style="margin-top:16px"><div class="head"><h2>Session traces</h2><span class="note">each step ends at its recorded time; bar length is its duration</span></div>' +
|
|
4161
|
+
'<div class="split"><div class="sessions" id="sessionList">' + install.sessions.map(function (s) {
|
|
4162
|
+
return '<button class="session" data-session="' + esc(s.id) + '" aria-pressed="' + (s.id === selectedSession) + '"><strong class="mono">' + esc(s.id === "none" ? "no session id" : s.id.slice(0, 8)) + "</strong>" +
|
|
4163
|
+
'<span class="muted">' + esc(new Date(s.start).toLocaleString()) + " \xB7 " + num(s.events) + " steps" + (s.errors ? ' \xB7 <span class="bad">' + plural(s.errors, "error") + "</span>" : "") + "</span></button>";
|
|
4164
|
+
}).join("") + '</div><div id="waterfall"></div></div></section>';
|
|
4165
|
+
draw("installActivity", points, "events", false);
|
|
4166
|
+
renderWaterfall();
|
|
4167
|
+
}
|
|
4168
|
+
|
|
4169
|
+
function renderWaterfall() {
|
|
4170
|
+
var target = $("waterfall");
|
|
4171
|
+
if (!target || !install) return;
|
|
4172
|
+
var steps = install.events.filter(function (e) { return e.session === selectedSession; })
|
|
4173
|
+
.sort(function (a, b) { return a.ts < b.ts ? -1 : a.ts > b.ts ? 1 : 0; }).slice(-200);
|
|
4174
|
+
if (!steps.length) { target.innerHTML = '<div class="empty-state">Pick a session.</div>'; return; }
|
|
4175
|
+
var start = Math.min.apply(null, steps.map(function (e) { return Date.parse(e.ts) - e.ms; }));
|
|
4176
|
+
var end = Math.max.apply(null, steps.map(function (e) { return Date.parse(e.ts); }));
|
|
4177
|
+
var span = Math.max(1, end - start);
|
|
4178
|
+
target.innerHTML = '<div class="legend"><span><i class="sw" style="background:var(--bar)"></i>MCP tool</span><span><i class="sw" style="background:var(--hook)"></i>hook step</span><span><i class="sw" style="background:var(--cli)"></i>CLI command</span><span class="spacer"></span><span>' + esc(ms(span)) + " total</span></div>" +
|
|
4179
|
+
'<div class="wf">' + steps.map(function (e) {
|
|
4180
|
+
var s = Date.parse(e.ts) - e.ms;
|
|
4181
|
+
var left = ((s - start) / span) * 100, width = Math.max(0.8, (e.ms / span) * 100);
|
|
4182
|
+
return '<div class="step"><span class="name' + (e.ok ? "" : " bad") + '" title="' + esc(kindGroup(e.kind) + ": " + e.name) + '">' + (e.ok ? "" : "\u2715 ") + esc(e.feature) + '</span><span class="at">' + esc(offset(s - start)) + '</span><span class="lane"><span class="seg-bar" style="left:' + left.toFixed(2) + "%;width:" + width.toFixed(2) + "%;background:" + kindColor(e.kind) + '" data-tip="' + esc(e.feature + " \xB7 " + ms(e.ms) + (e.ok ? "" : " \xB7 error") + (e.tokensOut ? " \xB7 " + num(e.tokensOut) + " tokens out" : "")) + '"></span></span><span class="t">' + esc(ms(e.ms)) + "</span></div>";
|
|
4183
|
+
}).join("") + "</div>";
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
function renderEvents() {
|
|
4187
|
+
var kinds = ["all"];
|
|
4188
|
+
summary.recent.forEach(function (e) { if (kinds.indexOf(e.kind) === -1) kinds.push(e.kind); });
|
|
4189
|
+
$("kindFilter").innerHTML = '<div class="seg" role="group" aria-label="Event kind">' + kinds.map(function (k) {
|
|
4190
|
+
return '<button data-kind="' + esc(k) + '" aria-pressed="' + (k === kindShown) + '">' + esc(k) + "</button>";
|
|
4191
|
+
}).join("") + "</div>";
|
|
4192
|
+
var list = summary.recent.filter(function (e) { return kindShown === "all" || e.kind === kindShown; });
|
|
4193
|
+
$("recent").innerHTML = list.length
|
|
4194
|
+
? '<table><thead><tr><th>Received</th><th>Install</th><th>Kind</th><th>Name</th><th>Client</th><th>Version</th><th class="n">Time</th><th>Result</th></tr></thead><tbody>' +
|
|
4195
|
+
list.map(function (e) {
|
|
4196
|
+
return '<tr class="link" tabindex="0" data-install="' + esc(e.install) + '"><td>' + esc(new Date(e.receivedAt).toLocaleString()) + '</td><td class="mono">' + esc(e.install) + '</td><td><span class="sw" style="background:' + kindColor(e.kind) + '"></span> ' + esc(e.kind) +
|
|
4197
|
+
'</td><td class="mono">' + esc(e.name) + "</td><td>" + esc(e.client) + '</td><td class="mono">' + esc(e.version) + '</td><td class="n">' + ms(e.ms) + '</td><td class="' + (e.ok ? "" : "bad") + '">' + (e.ok ? "ok" : "\u2715 error") + "</td></tr>";
|
|
3628
4198
|
}).join("") + "</tbody></table>"
|
|
3629
|
-
: '<div class="empty">No events yet.</div>';
|
|
4199
|
+
: '<div class="empty-state">No events yet.</div>';
|
|
3630
4200
|
}
|
|
3631
4201
|
|
|
3632
|
-
function
|
|
3633
|
-
|
|
4202
|
+
function route() {
|
|
4203
|
+
var hash = location.hash.replace("#", "") || "overview";
|
|
4204
|
+
var view = hash.indexOf("install/") === 0 ? "install" : ["overview", "users", "events"].indexOf(hash) !== -1 ? hash : "overview";
|
|
4205
|
+
["overview", "users", "install", "events"].forEach(function (v) { $("view-" + v).hidden = v !== view; });
|
|
4206
|
+
document.querySelectorAll(".tabs a").forEach(function (a) {
|
|
4207
|
+
var current = a.dataset.view === view || (view === "install" && a.dataset.view === "users");
|
|
4208
|
+
if (current) a.setAttribute("aria-current", "page"); else a.removeAttribute("aria-current");
|
|
4209
|
+
});
|
|
4210
|
+
$("range").hidden = view === "install";
|
|
4211
|
+
if (view === "install") {
|
|
4212
|
+
var id = hash.slice(8);
|
|
4213
|
+
if (!install || install.install !== id) {
|
|
4214
|
+
install = null;
|
|
4215
|
+
renderInstall();
|
|
4216
|
+
get("api/install?id=" + encodeURIComponent(id)).then(function (data) { install = data || { install: id, missing: true }; renderInstall(); });
|
|
4217
|
+
} else renderInstall();
|
|
4218
|
+
} else render();
|
|
3634
4219
|
}
|
|
3635
|
-
fetch(base + "api/npm").then(function (r) { return r.json(); }).then(function (n) { npm = n; render(); });
|
|
3636
4220
|
|
|
3637
|
-
|
|
4221
|
+
function render() {
|
|
4222
|
+
if (!summary) return;
|
|
4223
|
+
$("updated").textContent = "updated " + new Date(summary.generatedAt).toLocaleTimeString();
|
|
4224
|
+
var hash = location.hash.replace("#", "") || "overview";
|
|
4225
|
+
if (hash === "users") renderUsers();
|
|
4226
|
+
else if (hash === "events") renderEvents();
|
|
4227
|
+
else if (hash.indexOf("install/") !== 0) renderOverview();
|
|
4228
|
+
}
|
|
4229
|
+
|
|
4230
|
+
function load() { get("api/summary?days=" + days).then(function (s) { if (s) { summary = s; render(); } }); }
|
|
4231
|
+
get("api/npm").then(function (n) { npm = n; if (summary) render(); });
|
|
4232
|
+
|
|
4233
|
+
$("range").addEventListener("click", function (event) {
|
|
3638
4234
|
var button = event.target.closest("button");
|
|
3639
4235
|
if (!button) return;
|
|
3640
4236
|
days = Number(button.dataset.days);
|
|
3641
4237
|
document.querySelectorAll("#range button").forEach(function (b) { b.setAttribute("aria-pressed", String(b === button)); });
|
|
3642
4238
|
load();
|
|
3643
4239
|
});
|
|
3644
|
-
|
|
4240
|
+
$("userSearch").addEventListener("input", function () { if (summary) renderUsers(); });
|
|
4241
|
+
document.addEventListener("click", function (event) {
|
|
4242
|
+
var sort = event.target.closest("[data-sort]");
|
|
4243
|
+
if (sort) {
|
|
4244
|
+
var key = sort.dataset.sort;
|
|
4245
|
+
userSort = { key: key, dir: userSort.key === key ? -userSort.dir : -1 };
|
|
4246
|
+
renderUsers();
|
|
4247
|
+
return;
|
|
4248
|
+
}
|
|
4249
|
+
var row = event.target.closest("tr[data-install]");
|
|
4250
|
+
if (row) { location.hash = "install/" + row.dataset.install; return; }
|
|
4251
|
+
var session = event.target.closest("[data-session]");
|
|
4252
|
+
if (session) {
|
|
4253
|
+
selectedSession = session.dataset.session;
|
|
4254
|
+
document.querySelectorAll("#sessionList .session").forEach(function (b) { b.setAttribute("aria-pressed", String(b === session)); });
|
|
4255
|
+
renderWaterfall();
|
|
4256
|
+
return;
|
|
4257
|
+
}
|
|
4258
|
+
var kind = event.target.closest("[data-kind]");
|
|
4259
|
+
if (kind) { kindShown = kind.dataset.kind; renderEvents(); }
|
|
4260
|
+
});
|
|
4261
|
+
document.addEventListener("keydown", function (event) {
|
|
4262
|
+
var row = event.target.closest && event.target.closest("tr[data-install]");
|
|
4263
|
+
if (row && (event.key === "Enter" || event.key === " ")) { event.preventDefault(); location.hash = "install/" + row.dataset.install; }
|
|
4264
|
+
});
|
|
4265
|
+
$("csv").addEventListener("click", function () {
|
|
3645
4266
|
if (!summary) return;
|
|
3646
|
-
var cols =
|
|
3647
|
-
var lines = [cols.join(",")].concat(
|
|
4267
|
+
var cols = USER_COLS.map(function (c) { return c[0]; });
|
|
4268
|
+
var lines = [cols.join(",")].concat(filteredUsers().map(function (u) {
|
|
3648
4269
|
return cols.map(function (c) { return '"' + String(u[c]).replace(/"/g, '""') + '"'; }).join(",");
|
|
3649
4270
|
}));
|
|
3650
4271
|
var link = document.createElement("a");
|
|
3651
|
-
link.href = URL.createObjectURL(new Blob([lines.join(
|
|
4272
|
+
link.href = URL.createObjectURL(new Blob([lines.join(NL)], { type: "text/csv" }));
|
|
3652
4273
|
link.download = "codebuddy-users-" + new Date().toISOString().slice(0, 10) + ".csv";
|
|
3653
4274
|
link.click();
|
|
3654
4275
|
});
|
|
@@ -3657,10 +4278,16 @@ td.n, th.n { text-align: right; font-family: var(--mono); font-variant-numeric:
|
|
|
3657
4278
|
if (!text) { tip.style.display = "none"; return; }
|
|
3658
4279
|
tip.textContent = text;
|
|
3659
4280
|
tip.style.display = "block";
|
|
3660
|
-
|
|
3661
|
-
tip.style.
|
|
3662
|
-
|
|
4281
|
+
tip.style.left = Math.min(event.pageX + 12, document.documentElement.scrollWidth - tip.offsetWidth - 8) + "px";
|
|
4282
|
+
tip.style.top = (event.pageY - 34) + "px";
|
|
4283
|
+
});
|
|
4284
|
+
var resizeTimer = 0;
|
|
4285
|
+
window.addEventListener("resize", function () {
|
|
4286
|
+
clearTimeout(resizeTimer);
|
|
4287
|
+
resizeTimer = setTimeout(function () { if (location.hash.indexOf("#install/") === 0) renderInstall(); else render(); }, 150);
|
|
3663
4288
|
});
|
|
4289
|
+
window.addEventListener("hashchange", route);
|
|
4290
|
+
route();
|
|
3664
4291
|
load();
|
|
3665
4292
|
setInterval(load, 60000);
|
|
3666
4293
|
})();
|
|
@@ -3699,7 +4326,8 @@ var batchSchema = z2.object({
|
|
|
3699
4326
|
});
|
|
3700
4327
|
async function startCollector(options) {
|
|
3701
4328
|
const { dataDir } = options;
|
|
3702
|
-
await
|
|
4329
|
+
await mkdir3(dataDir, { recursive: true });
|
|
4330
|
+
const registry = await loadRegistry(dataDir);
|
|
3703
4331
|
let summaryCache;
|
|
3704
4332
|
let npmCache;
|
|
3705
4333
|
const perInstall = /* @__PURE__ */ new Map();
|
|
@@ -3743,10 +4371,18 @@ async function startCollector(options) {
|
|
|
3743
4371
|
return;
|
|
3744
4372
|
}
|
|
3745
4373
|
const receivedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
4374
|
+
if (!registry.has(batch.installId)) {
|
|
4375
|
+
registry.set(batch.installId, receivedAt);
|
|
4376
|
+
await appendFile(
|
|
4377
|
+
join7(dataDir, "installs.jsonl"),
|
|
4378
|
+
`${JSON.stringify({ installId: batch.installId, firstSeen: receivedAt })}
|
|
4379
|
+
`
|
|
4380
|
+
);
|
|
4381
|
+
}
|
|
3746
4382
|
const { events, ...meta } = batch;
|
|
3747
4383
|
const lines = events.map((event) => JSON.stringify({ ...meta, ...event, receivedAt }));
|
|
3748
4384
|
await appendFile(
|
|
3749
|
-
|
|
4385
|
+
join7(dataDir, `events-${receivedAt.slice(0, 10)}.jsonl`),
|
|
3750
4386
|
`${lines.join("\n")}
|
|
3751
4387
|
`
|
|
3752
4388
|
);
|
|
@@ -3790,12 +4426,29 @@ async function startCollector(options) {
|
|
|
3790
4426
|
summaryCache = {
|
|
3791
4427
|
at: Date.now(),
|
|
3792
4428
|
days,
|
|
3793
|
-
value: summarize(
|
|
4429
|
+
value: summarize(
|
|
4430
|
+
await readEvents(dataDir, Math.max(days, COHORT_WEEKS * 7)),
|
|
4431
|
+
days,
|
|
4432
|
+
Date.now(),
|
|
4433
|
+
registry
|
|
4434
|
+
)
|
|
3794
4435
|
};
|
|
3795
4436
|
}
|
|
3796
4437
|
json2(summaryCache.value);
|
|
3797
4438
|
return;
|
|
3798
4439
|
}
|
|
4440
|
+
if (url.pathname === "/api/install") {
|
|
4441
|
+
const trace = installTrace(
|
|
4442
|
+
await readEvents(dataDir, 90),
|
|
4443
|
+
(url.searchParams.get("id") ?? "").toLowerCase()
|
|
4444
|
+
);
|
|
4445
|
+
if (!trace) {
|
|
4446
|
+
response.writeHead(404, { "content-type": "application/json" }).end("null");
|
|
4447
|
+
return;
|
|
4448
|
+
}
|
|
4449
|
+
json2(trace);
|
|
4450
|
+
return;
|
|
4451
|
+
}
|
|
3799
4452
|
if (url.pathname === "/api/npm") {
|
|
3800
4453
|
if (!npmCache || Date.now() - npmCache.at > 36e5) {
|
|
3801
4454
|
npmCache = { at: Date.now(), value: await npmDownloads(options.fetchImpl ?? fetch) };
|
|
@@ -3826,7 +4479,7 @@ function passwordMatches(request, password3) {
|
|
|
3826
4479
|
if (!header.startsWith("Basic ")) return false;
|
|
3827
4480
|
const decoded = Buffer.from(header.slice(6), "base64").toString("utf8");
|
|
3828
4481
|
const given = decoded.slice(decoded.indexOf(":") + 1);
|
|
3829
|
-
const digest = (value) =>
|
|
4482
|
+
const digest = (value) => createHash2("sha256").update(value).digest();
|
|
3830
4483
|
return timingSafeEqual(digest(given), digest(password3));
|
|
3831
4484
|
}
|
|
3832
4485
|
function readBody(request) {
|
|
@@ -3850,10 +4503,10 @@ function listen2(server, port) {
|
|
|
3850
4503
|
}
|
|
3851
4504
|
async function readEvents(dataDir, days, now = Date.now()) {
|
|
3852
4505
|
const since = new Date(now - (days - 1) * 864e5).toISOString().slice(0, 10);
|
|
3853
|
-
const files = (await
|
|
4506
|
+
const files = (await readdir2(dataDir).catch(() => [])).filter((file) => /^events-\d{4}-\d{2}-\d{2}\.jsonl$/.test(file) && file.slice(7, 17) >= since).sort();
|
|
3854
4507
|
const events = [];
|
|
3855
4508
|
for (const file of files) {
|
|
3856
|
-
for (const line of (await
|
|
4509
|
+
for (const line of (await readFile5(join7(dataDir, file), "utf8")).split("\n")) {
|
|
3857
4510
|
if (!line) continue;
|
|
3858
4511
|
try {
|
|
3859
4512
|
events.push(JSON.parse(line));
|
|
@@ -3863,64 +4516,125 @@ async function readEvents(dataDir, days, now = Date.now()) {
|
|
|
3863
4516
|
}
|
|
3864
4517
|
return events;
|
|
3865
4518
|
}
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
4519
|
+
var DAY_MS = 864e5;
|
|
4520
|
+
var dayOf = (iso) => iso.slice(0, 10);
|
|
4521
|
+
var dayNumber = (iso) => Math.floor(Date.parse(iso) / DAY_MS);
|
|
4522
|
+
var COHORT_WEEKS = 8;
|
|
4523
|
+
function featureOf(event) {
|
|
4524
|
+
if (event.kind === "tool") return event.name;
|
|
4525
|
+
if (event.kind === "cli") return `cli:${event.name}`;
|
|
4526
|
+
return event.kind;
|
|
4527
|
+
}
|
|
4528
|
+
var GOT_CONTEXT = (e) => e.kind === "recall" || e.kind === "tool" && /^(context_(pack|bootstrap|before_edit)|recall)$/.test(e.name);
|
|
4529
|
+
var SAVED_MEMORY = (e) => e.kind === "capture" || e.kind === "tool" && /^(context_after_turn|remember)$/.test(e.name);
|
|
4530
|
+
function summarize(events, days, now = Date.now(), registry = /* @__PURE__ */ new Map()) {
|
|
4531
|
+
const today = Math.floor(now / DAY_MS);
|
|
4532
|
+
const rangeStart = today - days + 1;
|
|
4533
|
+
const firstSeenOf = (install, fallback) => registry.get(install) ?? fallback;
|
|
4534
|
+
const activeDays = /* @__PURE__ */ new Map();
|
|
4535
|
+
for (const event of events) {
|
|
4536
|
+
const set = activeDays.get(event.installId) ?? /* @__PURE__ */ new Set();
|
|
4537
|
+
set.add(dayNumber(event.receivedAt));
|
|
4538
|
+
activeDays.set(event.installId, set);
|
|
4539
|
+
}
|
|
4540
|
+
const activeBetween = (from, to) => {
|
|
4541
|
+
let count = 0;
|
|
4542
|
+
for (const set of activeDays.values()) {
|
|
4543
|
+
for (const d of set) {
|
|
4544
|
+
if (d >= from && d <= to) {
|
|
4545
|
+
count += 1;
|
|
4546
|
+
break;
|
|
4547
|
+
}
|
|
4548
|
+
}
|
|
4549
|
+
}
|
|
4550
|
+
return count;
|
|
3872
4551
|
};
|
|
4552
|
+
const inRange = events.filter((e) => dayNumber(e.receivedAt) >= rangeStart);
|
|
3873
4553
|
const daily = /* @__PURE__ */ new Map();
|
|
3874
|
-
for (let
|
|
3875
|
-
|
|
3876
|
-
installs: /* @__PURE__ */ new Set(),
|
|
3877
|
-
events: 0
|
|
3878
|
-
});
|
|
3879
|
-
}
|
|
3880
|
-
const firstSeen = /* @__PURE__ */ new Map();
|
|
3881
|
-
const tools = /* @__PURE__ */ new Map();
|
|
3882
|
-
const kinds = /* @__PURE__ */ new Map();
|
|
4554
|
+
for (let d = rangeStart; d <= today; d += 1) daily.set(d, { installs: /* @__PURE__ */ new Set(), events: 0 });
|
|
4555
|
+
const features = /* @__PURE__ */ new Map();
|
|
3883
4556
|
const latest = /* @__PURE__ */ new Map();
|
|
3884
4557
|
const users = /* @__PURE__ */ new Map();
|
|
4558
|
+
const funnel = {
|
|
4559
|
+
active: /* @__PURE__ */ new Set(),
|
|
4560
|
+
context: /* @__PURE__ */ new Set(),
|
|
4561
|
+
memory: /* @__PURE__ */ new Set()
|
|
4562
|
+
};
|
|
3885
4563
|
let errors = 0;
|
|
3886
4564
|
let tokensOut = 0;
|
|
3887
|
-
for (const event of
|
|
3888
|
-
const
|
|
4565
|
+
for (const event of inRange) {
|
|
4566
|
+
const d = dayNumber(event.receivedAt);
|
|
4567
|
+
const bucket = daily.get(d);
|
|
3889
4568
|
if (bucket) {
|
|
3890
4569
|
bucket.installs.add(event.installId);
|
|
3891
4570
|
bucket.events += 1;
|
|
3892
4571
|
}
|
|
3893
|
-
const seen = firstSeen.get(event.installId);
|
|
3894
|
-
if (!seen || event.receivedAt < seen) firstSeen.set(event.installId, event.receivedAt);
|
|
3895
4572
|
latest.set(event.installId, event);
|
|
4573
|
+
const feature = featureOf(event);
|
|
3896
4574
|
const user = users.get(event.installId) ?? {
|
|
4575
|
+
firstEvent: event.receivedAt,
|
|
3897
4576
|
lastSeen: event.receivedAt,
|
|
3898
4577
|
events: 0,
|
|
4578
|
+
errors: 0,
|
|
3899
4579
|
sessions: /* @__PURE__ */ new Set(),
|
|
3900
4580
|
projects: /* @__PURE__ */ new Set(),
|
|
3901
|
-
features: /* @__PURE__ */ new Map()
|
|
4581
|
+
features: /* @__PURE__ */ new Map(),
|
|
4582
|
+
days: /* @__PURE__ */ new Set()
|
|
3902
4583
|
};
|
|
3903
4584
|
if (event.receivedAt > user.lastSeen) user.lastSeen = event.receivedAt;
|
|
3904
4585
|
user.events += 1;
|
|
4586
|
+
if (!event.ok) user.errors += 1;
|
|
3905
4587
|
if (event.session) user.sessions.add(event.session);
|
|
3906
4588
|
user.projects.add(event.project);
|
|
3907
|
-
const feature = event.kind === "tool" ? event.name : event.kind;
|
|
3908
4589
|
user.features.set(feature, (user.features.get(feature) ?? 0) + 1);
|
|
4590
|
+
user.days.add(d);
|
|
3909
4591
|
users.set(event.installId, user);
|
|
3910
|
-
|
|
4592
|
+
funnel.active.add(event.installId);
|
|
4593
|
+
if (GOT_CONTEXT(event)) funnel.context.add(event.installId);
|
|
4594
|
+
if (SAVED_MEMORY(event)) funnel.memory.add(event.installId);
|
|
3911
4595
|
if (!event.ok) errors += 1;
|
|
3912
4596
|
tokensOut += event.tokensOut ?? 0;
|
|
3913
|
-
const
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
4597
|
+
const stats = features.get(feature) ?? { calls: 0, errors: 0, installs: /* @__PURE__ */ new Set(), ms: [] };
|
|
4598
|
+
stats.calls += 1;
|
|
4599
|
+
if (!event.ok) stats.errors += 1;
|
|
4600
|
+
stats.installs.add(event.installId);
|
|
4601
|
+
stats.ms.push(event.ms);
|
|
4602
|
+
features.set(feature, stats);
|
|
4603
|
+
}
|
|
4604
|
+
const firstSeenDay = /* @__PURE__ */ new Map();
|
|
4605
|
+
for (const [install, seen] of registry) firstSeenDay.set(install, dayNumber(seen));
|
|
4606
|
+
for (const [install, user] of users) {
|
|
4607
|
+
if (!firstSeenDay.has(install)) firstSeenDay.set(install, dayNumber(user.firstEvent));
|
|
4608
|
+
}
|
|
4609
|
+
for (const [install, set] of activeDays) {
|
|
4610
|
+
if (!firstSeenDay.has(install)) firstSeenDay.set(install, Math.min(...set));
|
|
3920
4611
|
}
|
|
3921
4612
|
const newPerDay = /* @__PURE__ */ new Map();
|
|
3922
|
-
for (const
|
|
3923
|
-
|
|
4613
|
+
for (const d of firstSeenDay.values()) newPerDay.set(d, (newPerDay.get(d) ?? 0) + 1);
|
|
4614
|
+
let cumulative = [...firstSeenDay.values()].filter((d) => d < rangeStart).length;
|
|
4615
|
+
const cohorts = [];
|
|
4616
|
+
for (let age = COHORT_WEEKS - 1; age >= 0; age -= 1) {
|
|
4617
|
+
const from = today - (age + 1) * 7 + 1;
|
|
4618
|
+
const members = [...firstSeenDay].filter(([, d]) => d >= from && d < from + 7).map(([install]) => install);
|
|
4619
|
+
const weeks = [];
|
|
4620
|
+
for (let week = 0; week <= age; week += 1) {
|
|
4621
|
+
const wFrom = from + week * 7;
|
|
4622
|
+
const active = members.filter((install) => {
|
|
4623
|
+
const set = activeDays.get(install);
|
|
4624
|
+
if (!set) return week === 0;
|
|
4625
|
+
for (const d of set) if (d >= wFrom && d < wFrom + 7) return true;
|
|
4626
|
+
return false;
|
|
4627
|
+
}).length;
|
|
4628
|
+
weeks.push(members.length ? active / members.length : 0);
|
|
4629
|
+
}
|
|
4630
|
+
cohorts.push({
|
|
4631
|
+
start: new Date(from * DAY_MS).toISOString().slice(0, 10),
|
|
4632
|
+
size: members.length,
|
|
4633
|
+
weeks
|
|
4634
|
+
});
|
|
4635
|
+
}
|
|
4636
|
+
const week1 = cohorts.filter((c) => c.size > 0 && c.weeks.length > 1);
|
|
4637
|
+
const week1Retention = week1.length ? week1.reduce((sum, c) => sum + (c.weeks[1] ?? 0) * c.size, 0) / week1.reduce((sum, c) => sum + c.size, 0) : null;
|
|
3924
4638
|
const share = (pick) => {
|
|
3925
4639
|
const counts = /* @__PURE__ */ new Map();
|
|
3926
4640
|
for (const event of latest.values()) {
|
|
@@ -3932,47 +4646,72 @@ function summarize(events, days, now = Date.now()) {
|
|
|
3932
4646
|
const sorted = [...values].sort((a, b) => a - b);
|
|
3933
4647
|
return sorted[Math.floor(sorted.length / 2)] ?? 0;
|
|
3934
4648
|
};
|
|
4649
|
+
const p95 = (values) => {
|
|
4650
|
+
const sorted = [...values].sort((a, b) => a - b);
|
|
4651
|
+
return sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))] ?? 0;
|
|
4652
|
+
};
|
|
4653
|
+
const wau = activeBetween(today - 6, today);
|
|
4654
|
+
const wauPrevious = activeBetween(today - 13, today - 7);
|
|
4655
|
+
const returned = [...users.values()].filter((user) => user.days.size >= 2).length;
|
|
3935
4656
|
return {
|
|
3936
4657
|
generatedAt: new Date(now).toISOString(),
|
|
3937
4658
|
days,
|
|
3938
4659
|
totals: {
|
|
3939
|
-
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
4660
|
+
allTimeInstalls: firstSeenDay.size,
|
|
4661
|
+
installs: users.size,
|
|
4662
|
+
newInRange: [...firstSeenDay.values()].filter((d) => d >= rangeStart).length,
|
|
4663
|
+
newToday: newPerDay.get(today) ?? 0,
|
|
4664
|
+
projects: new Set(inRange.map((e) => `${e.installId}:${e.project}`)).size,
|
|
4665
|
+
sessions: new Set(inRange.filter((e) => e.session).map((e) => e.session)).size,
|
|
4666
|
+
events: inRange.length,
|
|
4667
|
+
errorRate: inRange.length ? errors / inRange.length : 0,
|
|
3944
4668
|
tokensOut,
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
4669
|
+
dau: activeBetween(today, today),
|
|
4670
|
+
wau,
|
|
4671
|
+
wauPrevious,
|
|
4672
|
+
mau: activeBetween(today - 29, today),
|
|
4673
|
+
week1Retention
|
|
3949
4674
|
},
|
|
3950
|
-
daily: [...daily].map(([
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
4675
|
+
daily: [...daily].map(([d, bucket]) => {
|
|
4676
|
+
cumulative += newPerDay.get(d) ?? 0;
|
|
4677
|
+
return {
|
|
4678
|
+
date: new Date(d * DAY_MS).toISOString().slice(0, 10),
|
|
4679
|
+
installs: bucket.installs.size,
|
|
4680
|
+
events: bucket.events,
|
|
4681
|
+
newInstalls: newPerDay.get(d) ?? 0,
|
|
4682
|
+
totalInstalls: cumulative
|
|
4683
|
+
};
|
|
4684
|
+
}),
|
|
4685
|
+
/** Installs active in the range that reached each step. */
|
|
4686
|
+
funnel: [
|
|
4687
|
+
{ label: "Active", installs: funnel.active.size },
|
|
4688
|
+
{ label: "Received context", installs: funnel.context.size },
|
|
4689
|
+
{ label: "Saved memory", installs: funnel.memory.size },
|
|
4690
|
+
{ label: "Came back another day", installs: returned }
|
|
4691
|
+
],
|
|
4692
|
+
cohorts,
|
|
4693
|
+
features: [...features].map(([label, stats]) => ({
|
|
3957
4694
|
label,
|
|
3958
|
-
calls:
|
|
3959
|
-
installs:
|
|
3960
|
-
errorRate:
|
|
3961
|
-
p50ms: median(
|
|
3962
|
-
|
|
3963
|
-
|
|
4695
|
+
calls: stats.calls,
|
|
4696
|
+
installs: stats.installs.size,
|
|
4697
|
+
errorRate: stats.errors / stats.calls,
|
|
4698
|
+
p50ms: median(stats.ms),
|
|
4699
|
+
p95ms: p95(stats.ms)
|
|
4700
|
+
})).sort((a, b) => b.calls - a.calls).slice(0, 25),
|
|
3964
4701
|
clients: share((e) => e.clients.length ? e.clients : ["unknown"]),
|
|
3965
4702
|
versions: share((e) => [e.version]),
|
|
3966
4703
|
platforms: share((e) => [`${e.platform}/${e.arch}`]),
|
|
3967
4704
|
nodes: share((e) => [`node ${e.node}`]),
|
|
3968
|
-
/** One row per install, most recently active first. */
|
|
4705
|
+
/** One row per install active in the range, most recently active first. */
|
|
3969
4706
|
users: [...users].map(([installId, user]) => {
|
|
3970
4707
|
const last = latest.get(installId);
|
|
3971
4708
|
return {
|
|
3972
4709
|
install: installId.slice(0, 8),
|
|
3973
|
-
firstSeen:
|
|
4710
|
+
firstSeen: firstSeenOf(installId, user.firstEvent),
|
|
3974
4711
|
lastSeen: user.lastSeen,
|
|
4712
|
+
activeDays: user.days.size,
|
|
3975
4713
|
events: user.events,
|
|
4714
|
+
errors: user.errors,
|
|
3976
4715
|
sessions: user.sessions.size,
|
|
3977
4716
|
projects: user.projects.size,
|
|
3978
4717
|
client: last?.clients.join(",") || "unknown",
|
|
@@ -3981,7 +4720,7 @@ function summarize(events, days, now = Date.now()) {
|
|
|
3981
4720
|
topFeature: [...user.features].sort((a, b) => b[1] - a[1])[0]?.[0] ?? ""
|
|
3982
4721
|
};
|
|
3983
4722
|
}).sort((a, b) => b.lastSeen.localeCompare(a.lastSeen)).slice(0, 1e3),
|
|
3984
|
-
recent:
|
|
4723
|
+
recent: inRange.slice(-50).reverse().map((e) => ({
|
|
3985
4724
|
receivedAt: e.receivedAt,
|
|
3986
4725
|
install: e.installId.slice(0, 8),
|
|
3987
4726
|
kind: e.kind,
|
|
@@ -3993,6 +4732,77 @@ function summarize(events, days, now = Date.now()) {
|
|
|
3993
4732
|
}))
|
|
3994
4733
|
};
|
|
3995
4734
|
}
|
|
4735
|
+
function installTrace(events, prefix) {
|
|
4736
|
+
const own = events.filter((e) => e.installId.startsWith(prefix));
|
|
4737
|
+
const first = own[0];
|
|
4738
|
+
if (!first || !/^[0-9a-f]{8}$/.test(prefix)) return null;
|
|
4739
|
+
const sessions = /* @__PURE__ */ new Map();
|
|
4740
|
+
const perDay = /* @__PURE__ */ new Map();
|
|
4741
|
+
for (const event of own) {
|
|
4742
|
+
perDay.set(dayOf(event.receivedAt), (perDay.get(dayOf(event.receivedAt)) ?? 0) + 1);
|
|
4743
|
+
const key = event.session ?? "none";
|
|
4744
|
+
const session = sessions.get(key) ?? {
|
|
4745
|
+
start: event.ts,
|
|
4746
|
+
end: event.ts,
|
|
4747
|
+
events: 0,
|
|
4748
|
+
errors: 0,
|
|
4749
|
+
ms: 0,
|
|
4750
|
+
features: /* @__PURE__ */ new Set()
|
|
4751
|
+
};
|
|
4752
|
+
if (event.ts < session.start) session.start = event.ts;
|
|
4753
|
+
if (event.ts > session.end) session.end = event.ts;
|
|
4754
|
+
session.events += 1;
|
|
4755
|
+
if (!event.ok) session.errors += 1;
|
|
4756
|
+
session.ms += event.ms;
|
|
4757
|
+
session.features.add(featureOf(event));
|
|
4758
|
+
sessions.set(key, session);
|
|
4759
|
+
}
|
|
4760
|
+
const last = own[own.length - 1] ?? first;
|
|
4761
|
+
return {
|
|
4762
|
+
install: prefix,
|
|
4763
|
+
client: last.clients.join(",") || "unknown",
|
|
4764
|
+
version: last.version,
|
|
4765
|
+
platform: `${last.platform}/${last.arch}`,
|
|
4766
|
+
node: last.node,
|
|
4767
|
+
projects: new Set(own.map((e) => e.project)).size,
|
|
4768
|
+
perDay: [...perDay].map(([date, count]) => ({ date, events: count })),
|
|
4769
|
+
sessions: [...sessions].map(([id, session]) => ({ id, ...session, features: [...session.features] })).sort((a, b) => b.end.localeCompare(a.end)).slice(0, 50),
|
|
4770
|
+
events: own.slice(-1e3).map((e) => ({
|
|
4771
|
+
ts: e.ts,
|
|
4772
|
+
kind: e.kind,
|
|
4773
|
+
name: e.name,
|
|
4774
|
+
feature: featureOf(e),
|
|
4775
|
+
ms: e.ms,
|
|
4776
|
+
ok: e.ok,
|
|
4777
|
+
session: e.session ?? "none",
|
|
4778
|
+
tokensOut: e.tokensOut ?? 0
|
|
4779
|
+
}))
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
async function loadRegistry(dataDir) {
|
|
4783
|
+
const registry = /* @__PURE__ */ new Map();
|
|
4784
|
+
const path = join7(dataDir, "installs.jsonl");
|
|
4785
|
+
const raw = await readFile5(path, "utf8").catch(() => null);
|
|
4786
|
+
if (raw !== null) {
|
|
4787
|
+
for (const line of raw.split("\n")) {
|
|
4788
|
+
try {
|
|
4789
|
+
const entry = JSON.parse(line);
|
|
4790
|
+
if (!registry.has(entry.installId)) registry.set(entry.installId, entry.firstSeen);
|
|
4791
|
+
} catch {
|
|
4792
|
+
}
|
|
4793
|
+
}
|
|
4794
|
+
return registry;
|
|
4795
|
+
}
|
|
4796
|
+
for (const event of await readEvents(dataDir, 3650)) {
|
|
4797
|
+
if (!registry.has(event.installId)) registry.set(event.installId, event.receivedAt);
|
|
4798
|
+
}
|
|
4799
|
+
const lines = [...registry].map(
|
|
4800
|
+
([installId, firstSeen]) => JSON.stringify({ installId, firstSeen })
|
|
4801
|
+
);
|
|
4802
|
+
await appendFile(path, lines.length ? `${lines.join("\n")}
|
|
4803
|
+
` : "");
|
|
4804
|
+
return registry;
|
|
4805
|
+
}
|
|
3996
4806
|
async function npmDownloads(fetchImpl) {
|
|
3997
4807
|
const to = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
3998
4808
|
const from = new Date(Date.now() - 364 * 864e5).toISOString().slice(0, 10);
|
|
@@ -4017,10 +4827,10 @@ function registerTelemetryCommands(program, runSafely2) {
|
|
|
4017
4827
|
const state = await readTelemetryState();
|
|
4018
4828
|
const blocked = telemetryBlocked(state);
|
|
4019
4829
|
console.log(
|
|
4020
|
-
blocked ? `${
|
|
4830
|
+
blocked ? `${pc21.yellow("\u25CB")} telemetry off (${blocked})` : `${pc21.green("\u25CF")} telemetry on`
|
|
4021
4831
|
);
|
|
4022
|
-
console.log(
|
|
4023
|
-
if (telemetryUrl()) console.log(
|
|
4832
|
+
console.log(pc21.dim(` install id ${state.installId} (${telemetryStatePath()})`));
|
|
4833
|
+
if (telemetryUrl()) console.log(pc21.dim(` collector ${telemetryUrl()}`));
|
|
4024
4834
|
});
|
|
4025
4835
|
});
|
|
4026
4836
|
telemetry.command("enable").description("Allow anonymous usage data from this machine.").action(async () => {
|
|
@@ -4031,7 +4841,7 @@ function registerTelemetryCommands(program, runSafely2) {
|
|
|
4031
4841
|
noticeAt: state.noticeAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
4032
4842
|
});
|
|
4033
4843
|
console.log(TELEMETRY_NOTICE);
|
|
4034
|
-
console.log(
|
|
4844
|
+
console.log(pc21.green("Telemetry enabled."));
|
|
4035
4845
|
});
|
|
4036
4846
|
});
|
|
4037
4847
|
telemetry.command("disable").description("Stop sending anonymous usage data from this machine.").action(async () => {
|
|
@@ -4059,14 +4869,14 @@ function registerTelemetryCommands(program, runSafely2) {
|
|
|
4059
4869
|
adminPort: opts.adminPort,
|
|
4060
4870
|
...process.env.CODEBUDDY_ADMIN_PASSWORD ? { adminPassword: process.env.CODEBUDDY_ADMIN_PASSWORD } : {}
|
|
4061
4871
|
});
|
|
4062
|
-
console.log(`${
|
|
4063
|
-
console.log(`${
|
|
4872
|
+
console.log(`${pc21.green("\u25CF")} ingest ${pc21.bold(collector.ingestUrl)}`);
|
|
4873
|
+
console.log(`${pc21.green("\u25CF")} admin ${pc21.bold(collector.adminUrl)}`);
|
|
4064
4874
|
console.log(
|
|
4065
|
-
|
|
4875
|
+
pc21.dim(
|
|
4066
4876
|
process.env.CODEBUDDY_ADMIN_PASSWORD ? " admin panel is password-protected (CODEBUDDY_ADMIN_PASSWORD)." : " admin panel has no password: keep it private, or set CODEBUDDY_ADMIN_PASSWORD."
|
|
4067
4877
|
)
|
|
4068
4878
|
);
|
|
4069
|
-
console.log(
|
|
4879
|
+
console.log(pc21.dim(` events in ${opts.data}. Ctrl+C to stop.`));
|
|
4070
4880
|
await new Promise((resolve6) => {
|
|
4071
4881
|
const stop = () => void collector.close().then(resolve6);
|
|
4072
4882
|
process.once("SIGINT", stop);
|
|
@@ -4079,12 +4889,12 @@ function registerTelemetryCommands(program, runSafely2) {
|
|
|
4079
4889
|
// src/cli/commands/use-command.ts
|
|
4080
4890
|
import { basename as basename2 } from "path";
|
|
4081
4891
|
import { confirm as confirm2, isCancel as isCancel2, log as log2, note as note2, password as password2, select, spinner as spinner3, text as text2 } from "@clack/prompts";
|
|
4082
|
-
import
|
|
4892
|
+
import pc22 from "picocolors";
|
|
4083
4893
|
|
|
4084
4894
|
// src/core/global-config.ts
|
|
4085
|
-
import { chmod, mkdir as
|
|
4895
|
+
import { chmod, mkdir as mkdir4, readFile as readFile6, writeFile as writeFile3 } from "fs/promises";
|
|
4086
4896
|
import { homedir } from "os";
|
|
4087
|
-
import { dirname as dirname3, join as
|
|
4897
|
+
import { dirname as dirname3, join as join8 } from "path";
|
|
4088
4898
|
import { z as z3 } from "zod";
|
|
4089
4899
|
var GLOBAL_DIR = ".codebuddy";
|
|
4090
4900
|
var GLOBAL_FILE = "global.json";
|
|
@@ -4097,12 +4907,12 @@ var globalConfigSchema = z3.object({
|
|
|
4097
4907
|
installPath: z3.string().optional()
|
|
4098
4908
|
});
|
|
4099
4909
|
function globalConfigPath(home = homedir()) {
|
|
4100
|
-
return
|
|
4910
|
+
return join8(home, GLOBAL_DIR, GLOBAL_FILE);
|
|
4101
4911
|
}
|
|
4102
4912
|
async function readGlobalConfig(home = homedir()) {
|
|
4103
4913
|
const path = globalConfigPath(home);
|
|
4104
4914
|
try {
|
|
4105
|
-
const raw = await
|
|
4915
|
+
const raw = await readFile6(path, "utf8");
|
|
4106
4916
|
return globalConfigSchema.parse(JSON.parse(raw));
|
|
4107
4917
|
} catch (error) {
|
|
4108
4918
|
if (error.code === "ENOENT") return {};
|
|
@@ -4111,7 +4921,7 @@ async function readGlobalConfig(home = homedir()) {
|
|
|
4111
4921
|
}
|
|
4112
4922
|
async function writeGlobalConfig(next, home = homedir()) {
|
|
4113
4923
|
const path = globalConfigPath(home);
|
|
4114
|
-
await
|
|
4924
|
+
await mkdir4(dirname3(path), { recursive: true, mode: 448 });
|
|
4115
4925
|
await writeFile3(path, `${JSON.stringify(next, null, 2)}
|
|
4116
4926
|
`, { mode: 384 });
|
|
4117
4927
|
await chmod(path, 384);
|
|
@@ -4163,15 +4973,15 @@ async function runUseCommand(options = {}) {
|
|
|
4163
4973
|
})
|
|
4164
4974
|
));
|
|
4165
4975
|
note2(
|
|
4166
|
-
`Project folder: ${
|
|
4167
|
-
Namespace: ${
|
|
4976
|
+
`Project folder: ${pc22.bold(process.cwd())}
|
|
4977
|
+
Namespace: ${pc22.bold(namespace)}`,
|
|
4168
4978
|
"codebuddy use"
|
|
4169
4979
|
);
|
|
4170
4980
|
const stored = await readGlobalConfig();
|
|
4171
4981
|
const projectConfig = await readConfigFile();
|
|
4172
4982
|
let embeddings = projectConfig.provider?.type;
|
|
4173
4983
|
if (embeddings) {
|
|
4174
|
-
log2.info(`Embeddings: ${embeddings} (change with ${
|
|
4984
|
+
log2.info(`Embeddings: ${embeddings} (change with ${pc22.cyan("codebuddy models use")}).`);
|
|
4175
4985
|
} else if (!interactive) {
|
|
4176
4986
|
embeddings = "local";
|
|
4177
4987
|
} else {
|
|
@@ -4212,7 +5022,7 @@ Namespace: ${pc20.bold(namespace)}`,
|
|
|
4212
5022
|
log2.info("Using HF_TOKEN from environment.");
|
|
4213
5023
|
} else if (stored.hfToken) {
|
|
4214
5024
|
hfToken = stored.hfToken;
|
|
4215
|
-
log2.info(`Reusing saved Hugging Face token (${
|
|
5025
|
+
log2.info(`Reusing saved Hugging Face token (${pc22.dim("~/.codebuddy/global.json")}).`);
|
|
4216
5026
|
} else if (!interactive) {
|
|
4217
5027
|
throw new Error(
|
|
4218
5028
|
"Hugging Face embeddings need a token: set HF_TOKEN (no terminal to prompt for it)."
|
|
@@ -4346,7 +5156,7 @@ Namespace: ${pc20.bold(namespace)}`,
|
|
|
4346
5156
|
databaseUrl
|
|
4347
5157
|
});
|
|
4348
5158
|
log2.success(
|
|
4349
|
-
`Registered ${
|
|
5159
|
+
`Registered ${pc22.bold(install.serverKey)} in Claude Desktop. Restart with \u2318Q to activate.`
|
|
4350
5160
|
);
|
|
4351
5161
|
} catch (error) {
|
|
4352
5162
|
log2.warn(`Claude Desktop registration skipped: ${error.message}`);
|
|
@@ -4361,7 +5171,7 @@ Namespace: ${pc20.bold(namespace)}`,
|
|
|
4361
5171
|
databaseUrl
|
|
4362
5172
|
});
|
|
4363
5173
|
log2.success(
|
|
4364
|
-
`Registered ${
|
|
5174
|
+
`Registered ${pc22.bold(install.serverKey)} in Codex. Restart Codex to activate.`
|
|
4365
5175
|
);
|
|
4366
5176
|
} catch (error) {
|
|
4367
5177
|
log2.warn(`Codex registration skipped: ${error.message}`);
|
|
@@ -4369,24 +5179,24 @@ Namespace: ${pc20.bold(namespace)}`,
|
|
|
4369
5179
|
}
|
|
4370
5180
|
note2(
|
|
4371
5181
|
[
|
|
4372
|
-
`${
|
|
4373
|
-
`${
|
|
4374
|
-
`${
|
|
4375
|
-
`${
|
|
5182
|
+
`${pc22.green("\u2713")} CodeBuddy is ready for ${pc22.bold(folderName)}.`,
|
|
5183
|
+
`${pc22.green("\u2713")} Scaffolded ${pc22.cyan(".codebuddy/policies.yaml")} for project-specific guardrails.`,
|
|
5184
|
+
`${pc22.green("\u2713")} ${graphifyMessage}`,
|
|
5185
|
+
`${pc22.green("\u2713")} Added agent workflow instructions to ${pc22.cyan("CLAUDE.md")} and ${pc22.cyan("AGENTS.md")}.`,
|
|
4376
5186
|
``,
|
|
4377
5187
|
`Next:`,
|
|
4378
5188
|
` 1. Restart Claude Desktop and Codex.`,
|
|
4379
|
-
` 2. Ask Claude or Codex to use the ${
|
|
5189
|
+
` 2. Ask Claude or Codex to use the ${pc22.cyan("context_pack")} tool for each coding task.`,
|
|
4380
5190
|
` 3. Indexing, Graphify, Markdown memory import, and embeddings now update automatically while the MCP server is running.`,
|
|
4381
|
-
` 4. Run ${
|
|
5191
|
+
` 4. Run ${pc22.cyan("codebuddy doctor")} or ${pc22.cyan("codebuddy db doctor")} if you want a full health check.`,
|
|
4382
5192
|
``,
|
|
4383
5193
|
`Manage:`,
|
|
4384
|
-
` codebuddy claude list ${
|
|
4385
|
-
` codebuddy codex list ${
|
|
4386
|
-
` codebuddy db test ${
|
|
5194
|
+
` codebuddy claude list ${pc22.dim("# see every project wired up")}`,
|
|
5195
|
+
` codebuddy codex list ${pc22.dim("# see Codex entries")}`,
|
|
5196
|
+
` codebuddy db test ${pc22.dim("# quick Postgres connectivity check")}`,
|
|
4387
5197
|
` codebuddy claude remove codebuddy-${namespace}`,
|
|
4388
5198
|
` codebuddy codex remove codebuddy-${namespace}`,
|
|
4389
|
-
` codebuddy postgres down ${
|
|
5199
|
+
` codebuddy postgres down ${pc22.dim("# stop the DB (data preserved)")}`
|
|
4390
5200
|
].join("\n"),
|
|
4391
5201
|
"Done"
|
|
4392
5202
|
);
|
|
@@ -4397,7 +5207,7 @@ Namespace: ${pc20.bold(namespace)}`,
|
|
|
4397
5207
|
|
|
4398
5208
|
// src/cli/commands/worklog.ts
|
|
4399
5209
|
import { relative as relative6 } from "path";
|
|
4400
|
-
import
|
|
5210
|
+
import pc23 from "picocolors";
|
|
4401
5211
|
function registerWorklogCommands(program, runSafely2) {
|
|
4402
5212
|
const worklog = program.command("worklog").description("Inspect the cross-chat worklog of completed turns.");
|
|
4403
5213
|
worklog.command("list", { isDefault: true }).option("--limit <n>", "Maximum entries to show.", "20").option("--json", "Print machine-readable JSON.").description("List worklog entries, newest first.").action(async (opts) => {
|
|
@@ -4408,7 +5218,7 @@ function registerWorklogCommands(program, runSafely2) {
|
|
|
4408
5218
|
return;
|
|
4409
5219
|
}
|
|
4410
5220
|
if (entries.length === 0) {
|
|
4411
|
-
console.log(
|
|
5221
|
+
console.log(pc23.dim("no worklog entries yet"));
|
|
4412
5222
|
return;
|
|
4413
5223
|
}
|
|
4414
5224
|
for (const entry of entries) console.log(renderRow2(entry));
|
|
@@ -4422,36 +5232,36 @@ function registerWorklogCommands(program, runSafely2) {
|
|
|
4422
5232
|
worklog.command("latest").description("Show the most recent worklog entry.").action(async () => {
|
|
4423
5233
|
await runSafely2(async () => {
|
|
4424
5234
|
const [entry] = await new WorklogStore().list();
|
|
4425
|
-
console.log(entry ? renderEntry(entry) :
|
|
5235
|
+
console.log(entry ? renderEntry(entry) : pc23.dim("no worklog entries yet"));
|
|
4426
5236
|
});
|
|
4427
5237
|
});
|
|
4428
5238
|
}
|
|
4429
5239
|
function renderRow2(entry) {
|
|
4430
5240
|
const summary = entry.summary.replace(/\s+/g, " ");
|
|
4431
5241
|
const short2 = summary.length <= 70 ? summary : `${summary.slice(0, 69)}\u2026`;
|
|
4432
|
-
return `${
|
|
5242
|
+
return `${pc23.dim(entry.createdAt.slice(0, 16))} ${pc23.dim(entry.id)} ${short2} ${pc23.dim(`(${entry.files.length} file(s))`)}`;
|
|
4433
5243
|
}
|
|
4434
5244
|
function renderEntry(entry) {
|
|
4435
|
-
const lines = [`${
|
|
4436
|
-
if (entry.sourceSessionId) lines.push(`${
|
|
4437
|
-
if (entry.sourcePlanId) lines.push(`${
|
|
4438
|
-
if (entry.commitSha) lines.push(`${
|
|
4439
|
-
if (entry.files.length > 0) lines.push(`${
|
|
5245
|
+
const lines = [`${pc23.bold(entry.id)} ${pc23.dim(entry.createdAt)} ${pc23.dim(entry.namespace)}`];
|
|
5246
|
+
if (entry.sourceSessionId) lines.push(`${pc23.bold("session:")} ${entry.sourceSessionId}`);
|
|
5247
|
+
if (entry.sourcePlanId) lines.push(`${pc23.bold("plan:")} ${entry.sourcePlanId}`);
|
|
5248
|
+
if (entry.commitSha) lines.push(`${pc23.bold("commit:")} ${entry.commitSha}`);
|
|
5249
|
+
if (entry.files.length > 0) lines.push(`${pc23.bold("files:")} ${entry.files.join(", ")}`);
|
|
4440
5250
|
if (entry.verification.length > 0) {
|
|
4441
|
-
lines.push(`${
|
|
5251
|
+
lines.push(`${pc23.bold("verification:")}`, ...entry.verification.map((line) => ` - ${line}`));
|
|
4442
5252
|
}
|
|
4443
5253
|
if (entry.savedMemory.length > 0) {
|
|
4444
|
-
lines.push(`${
|
|
5254
|
+
lines.push(`${pc23.bold("saved memory:")} ${entry.savedMemory.join(", ")}`);
|
|
4445
5255
|
}
|
|
4446
5256
|
if (entry.queuedForReview.length > 0) {
|
|
4447
|
-
lines.push(`${
|
|
5257
|
+
lines.push(`${pc23.bold("queued for review:")} ${entry.queuedForReview.join(", ")}`);
|
|
4448
5258
|
}
|
|
4449
|
-
lines.push("", entry.summary, "",
|
|
5259
|
+
lines.push("", entry.summary, "", pc23.dim(relative6(process.cwd(), entry.path)));
|
|
4450
5260
|
return lines.join("\n");
|
|
4451
5261
|
}
|
|
4452
5262
|
|
|
4453
5263
|
// src/cli/commands/zvec.ts
|
|
4454
|
-
import
|
|
5264
|
+
import pc24 from "picocolors";
|
|
4455
5265
|
function registerZvecCommands(program, runSafely2) {
|
|
4456
5266
|
const zvec = program.command("zvec").description("Optional zvec-grep semantic file discovery for the context tools.");
|
|
4457
5267
|
zvec.command("status", { isDefault: true }).option("--json", "Print machine-readable JSON.").description("Show whether zvec-grep is enabled, installed, supported and indexed.").action(async (opts) => {
|
|
@@ -4461,22 +5271,22 @@ function registerZvecCommands(program, runSafely2) {
|
|
|
4461
5271
|
console.log(JSON.stringify(report, null, 2));
|
|
4462
5272
|
return;
|
|
4463
5273
|
}
|
|
4464
|
-
const mark = (ok2) => ok2 ?
|
|
5274
|
+
const mark = (ok2) => ok2 ? pc24.green("yes") : pc24.yellow("no");
|
|
4465
5275
|
console.log(`enabled: ${mark(report.enabled)}`);
|
|
4466
5276
|
console.log(`binary: ${report.binary}`);
|
|
4467
5277
|
console.log(
|
|
4468
|
-
`version: ${report.version ??
|
|
5278
|
+
`version: ${report.version ?? pc24.yellow("not found")}${report.version && !report.supported ? pc24.yellow(" (unsupported; need 0.2.x)") : ""}`
|
|
4469
5279
|
);
|
|
4470
5280
|
console.log(`index ready: ${mark(report.indexReady)}`);
|
|
4471
5281
|
console.log(
|
|
4472
|
-
`zg server: ${report.server} ${
|
|
5282
|
+
`zg server: ${report.server} ${pc24.dim("(running = faster queries; optional)")}`
|
|
4473
5283
|
);
|
|
4474
5284
|
console.log(`status: ${report.reason}`);
|
|
4475
|
-
if (!report.version) console.log(
|
|
5285
|
+
if (!report.version) console.log(pc24.dim("install: npm install -g @zvec/zvec-grep"));
|
|
4476
5286
|
else if (report.supported && !report.indexReady)
|
|
4477
|
-
console.log(
|
|
5287
|
+
console.log(pc24.dim("next: codebuddy zvec index"));
|
|
4478
5288
|
else if (report.indexReady && !report.enabled)
|
|
4479
|
-
console.log(
|
|
5289
|
+
console.log(pc24.dim("next: codebuddy zvec enable"));
|
|
4480
5290
|
});
|
|
4481
5291
|
});
|
|
4482
5292
|
zvec.command("index").option("--model <model>", "Embedding model for a new index.", DEFAULT_ZVEC_MODEL).option("--rebuild", "Rebuild the index from scratch (e.g. after changing model).").description("Build or refresh the zg index for this repository (explicit; never automatic).").action(async (opts) => {
|
|
@@ -4486,21 +5296,21 @@ function registerZvecCommands(program, runSafely2) {
|
|
|
4486
5296
|
...opts.rebuild ? { rebuild: true } : {}
|
|
4487
5297
|
});
|
|
4488
5298
|
if (code !== 0) throw new Error(`zg index exited with code ${code}`);
|
|
4489
|
-
console.log(
|
|
5299
|
+
console.log(pc24.dim("next: codebuddy zvec enable (.zvec-grep/ ignores itself in git)"));
|
|
4490
5300
|
});
|
|
4491
5301
|
});
|
|
4492
5302
|
zvec.command("enable").description("Use zvec-grep candidates in context_pack / context_before_edit.").action(async () => {
|
|
4493
5303
|
await runSafely2(async () => {
|
|
4494
5304
|
await setZvecEnabled(true);
|
|
4495
5305
|
console.log(
|
|
4496
|
-
`${
|
|
5306
|
+
`${pc24.green("enabled")} zvec-grep retrieval (falls back to keywords if unavailable)`
|
|
4497
5307
|
);
|
|
4498
5308
|
});
|
|
4499
5309
|
});
|
|
4500
5310
|
zvec.command("disable").description("Stop using zvec-grep; keyword ranking only.").action(async () => {
|
|
4501
5311
|
await runSafely2(async () => {
|
|
4502
5312
|
await setZvecEnabled(false);
|
|
4503
|
-
console.log(`${
|
|
5313
|
+
console.log(`${pc24.yellow("disabled")} zvec-grep retrieval`);
|
|
4504
5314
|
});
|
|
4505
5315
|
});
|
|
4506
5316
|
}
|
|
@@ -4773,6 +5583,36 @@ function createCli() {
|
|
|
4773
5583
|
console.log(JSON.stringify(await runtime.repository.getStats(), null, 2));
|
|
4774
5584
|
});
|
|
4775
5585
|
});
|
|
5586
|
+
program.command("storage").option("--json", "Print machine-readable JSON.").description(
|
|
5587
|
+
"Memory storage health: Markdown/database drift, backup coverage, embedding backlog and spaces, capture lag."
|
|
5588
|
+
).action(async (opts) => {
|
|
5589
|
+
await runSafely(async () => {
|
|
5590
|
+
const root = process.cwd();
|
|
5591
|
+
const capture2 = {
|
|
5592
|
+
stagedNotCaptured: await new HookStagingStore(root).pending(),
|
|
5593
|
+
journalBehind: (await journalLag(root)).filter((entry) => entry.behindBytes !== 0)
|
|
5594
|
+
};
|
|
5595
|
+
let database;
|
|
5596
|
+
try {
|
|
5597
|
+
const runtime = await createRuntime(await loadRuntimeConfig());
|
|
5598
|
+
try {
|
|
5599
|
+
database = await runtime.memory.storageReport();
|
|
5600
|
+
} finally {
|
|
5601
|
+
await runtime.close();
|
|
5602
|
+
}
|
|
5603
|
+
} catch (error) {
|
|
5604
|
+
database = { unavailable: error.message };
|
|
5605
|
+
}
|
|
5606
|
+
if (opts.json) {
|
|
5607
|
+
console.log(JSON.stringify({ database, capture: capture2 }, null, 2));
|
|
5608
|
+
return;
|
|
5609
|
+
}
|
|
5610
|
+
printStorage(database, capture2);
|
|
5611
|
+
if ("unavailable" in database || database.markdownOnly.length + database.dbOnly.length > 0) {
|
|
5612
|
+
process.exitCode = 1;
|
|
5613
|
+
}
|
|
5614
|
+
});
|
|
5615
|
+
});
|
|
4776
5616
|
program.command("doctor").description("Check CodeBuddy environment health.").option("--skip-model-check", "Do not call Hugging Face models.").action(async (opts) => {
|
|
4777
5617
|
await runSafely(async () => {
|
|
4778
5618
|
const report = await runDoctor({ skipModelCheck: opts.skipModelCheck ?? true });
|
|
@@ -4799,6 +5639,8 @@ function createCli() {
|
|
|
4799
5639
|
registerLearnCommand(program, runSafely);
|
|
4800
5640
|
registerMemoryCommands(program, runSafely);
|
|
4801
5641
|
registerWorklogCommands(program, runSafely);
|
|
5642
|
+
registerJournalCommands(program, runSafely);
|
|
5643
|
+
registerOutputCommands(program, runSafely);
|
|
4802
5644
|
registerZvecCommands(program, runSafely);
|
|
4803
5645
|
registerLlmCommands(program, runSafely);
|
|
4804
5646
|
registerSuggestCommand(program, runSafely);
|
|
@@ -4810,7 +5652,12 @@ function createCli() {
|
|
|
4810
5652
|
registerGitHubCommands(program, runSafely);
|
|
4811
5653
|
registerSymbolsCommand(program, runSafely);
|
|
4812
5654
|
registerTelemetryCommands(program, runSafely);
|
|
4813
|
-
|
|
5655
|
+
let started = Date.now();
|
|
5656
|
+
program.hook("preAction", () => {
|
|
5657
|
+
started = Date.now();
|
|
5658
|
+
return showTelemetryNoticeOnce();
|
|
5659
|
+
});
|
|
5660
|
+
program.hook("postAction", (_program, command) => traceCliCommand(command, started));
|
|
4814
5661
|
program.action(async () => {
|
|
4815
5662
|
await runSafely(() => runUseCommand());
|
|
4816
5663
|
});
|
|
@@ -4821,7 +5668,20 @@ var HELP_GROUPS = [
|
|
|
4821
5668
|
["Get started:", ["use", "init", "doctor", "hooks", "clients", "models", "dashboard"]],
|
|
4822
5669
|
[
|
|
4823
5670
|
"Daily work:",
|
|
4824
|
-
[
|
|
5671
|
+
[
|
|
5672
|
+
"context",
|
|
5673
|
+
"plan",
|
|
5674
|
+
"change",
|
|
5675
|
+
"risk",
|
|
5676
|
+
"suggest",
|
|
5677
|
+
"memory",
|
|
5678
|
+
"worklog",
|
|
5679
|
+
"journal",
|
|
5680
|
+
"run",
|
|
5681
|
+
"output",
|
|
5682
|
+
"map",
|
|
5683
|
+
"symbols"
|
|
5684
|
+
]
|
|
4825
5685
|
],
|
|
4826
5686
|
["Indexing and search:", ["index", "watch", "reindex", "zvec", "graphify", "llm"]],
|
|
4827
5687
|
[
|
|
@@ -4831,6 +5691,7 @@ var HELP_GROUPS = [
|
|
|
4831
5691
|
"db",
|
|
4832
5692
|
"migrate",
|
|
4833
5693
|
"stats",
|
|
5694
|
+
"storage",
|
|
4834
5695
|
"inspect",
|
|
4835
5696
|
"namespaces",
|
|
4836
5697
|
"list-facts",
|
|
@@ -4881,7 +5742,7 @@ async function runSafely(fn) {
|
|
|
4881
5742
|
try {
|
|
4882
5743
|
await fn();
|
|
4883
5744
|
} catch (error) {
|
|
4884
|
-
console.error(
|
|
5745
|
+
console.error(pc25.red(String(redactSecrets(error.message))));
|
|
4885
5746
|
process.exitCode = 1;
|
|
4886
5747
|
}
|
|
4887
5748
|
}
|
|
@@ -4895,9 +5756,9 @@ function parseDuration(value) {
|
|
|
4895
5756
|
return amount * 60 * 1e3;
|
|
4896
5757
|
}
|
|
4897
5758
|
function printDoctor(report) {
|
|
4898
|
-
const ok2 =
|
|
4899
|
-
const bad =
|
|
4900
|
-
const warn2 =
|
|
5759
|
+
const ok2 = pc25.green("ok");
|
|
5760
|
+
const bad = pc25.red("fail");
|
|
5761
|
+
const warn2 = pc25.yellow("warn");
|
|
4901
5762
|
console.log(`scaffold root: ${report.scaffold.codebuddyDir.ok ? ok2 : bad}`);
|
|
4902
5763
|
if (report.scaffold.codebuddyDir.warning)
|
|
4903
5764
|
console.log(` ${report.scaffold.codebuddyDir.warning}`);
|
|
@@ -4923,7 +5784,7 @@ function printDoctor(report) {
|
|
|
4923
5784
|
);
|
|
4924
5785
|
console.log(`estimated daily cap remaining: ${report.usage.estimatedDailyCapRemaining}`);
|
|
4925
5786
|
if (report.usage.warning80Percent)
|
|
4926
|
-
console.log(
|
|
5787
|
+
console.log(pc25.yellow("usage warning: daily cap is over 80% used"));
|
|
4927
5788
|
console.log(
|
|
4928
5789
|
`config permissions: ${report.config.exists ? report.config.mode : "missing"} ${report.config.ok ? ok2 : warn2}`
|
|
4929
5790
|
);
|
|
@@ -4933,9 +5794,9 @@ function printDoctor(report) {
|
|
|
4933
5794
|
}
|
|
4934
5795
|
}
|
|
4935
5796
|
function printDbDoctor(report) {
|
|
4936
|
-
const ok2 =
|
|
4937
|
-
const bad =
|
|
4938
|
-
const warn2 =
|
|
5797
|
+
const ok2 = pc25.green("ok");
|
|
5798
|
+
const bad = pc25.red("fail");
|
|
5799
|
+
const warn2 = pc25.yellow("warn");
|
|
4939
5800
|
console.log(`DB connectivity: ${report.db.ok ? ok2 : bad}`);
|
|
4940
5801
|
if (report.db.postgresUrlSource) console.log(` url source: ${report.db.postgresUrlSource}`);
|
|
4941
5802
|
if (report.db.error) console.log(` ${report.db.error}`);
|
|
@@ -4948,7 +5809,42 @@ function printDbDoctor(report) {
|
|
|
4948
5809
|
}
|
|
4949
5810
|
if (report.namespace.warning) console.log(` ${report.namespace.warning}`);
|
|
4950
5811
|
}
|
|
5812
|
+
function printStorage(database, capture2) {
|
|
5813
|
+
if ("unavailable" in database) {
|
|
5814
|
+
console.log(pc25.yellow(`database: unavailable (${database.unavailable})`));
|
|
5815
|
+
} else {
|
|
5816
|
+
const drift = database.markdownOnly.length + database.dbOnly.length;
|
|
5817
|
+
console.log(
|
|
5818
|
+
`facts: ${database.markdownFacts} Markdown, ${database.databaseFacts} database${database.invalidFiles > 0 ? `, ${database.invalidFiles} unreadable file(s)` : ""}`
|
|
5819
|
+
);
|
|
5820
|
+
console.log(
|
|
5821
|
+
drift === 0 ? pc25.green("sync: Markdown and database agree") : pc25.yellow(
|
|
5822
|
+
`sync: ${database.markdownOnly.length} file(s) not in the database, ${database.dbOnly.length} row(s) without a file (run \`codebuddy reindex\`)`
|
|
5823
|
+
)
|
|
5824
|
+
);
|
|
5825
|
+
if (database.unbacked.length > 0) {
|
|
5826
|
+
console.log(
|
|
5827
|
+
pc25.yellow(
|
|
5828
|
+
`backup: ${database.unbacked.length} database-only fact(s) have no Markdown copy`
|
|
5829
|
+
)
|
|
5830
|
+
);
|
|
5831
|
+
}
|
|
5832
|
+
const e = database.embeddings;
|
|
5833
|
+
console.log(
|
|
5834
|
+
`embeddings: ${e.ready} ready, ${e.pending} pending, ${e.processing} processing, ${e.failed} failed${database.oldestPendingAt ? ` (oldest pending ${database.oldestPendingAt})` : ""}`
|
|
5835
|
+
);
|
|
5836
|
+
for (const space of database.spaces) {
|
|
5837
|
+
console.log(` space ${space.model} ${space.space ?? "(legacy)"}: ${space.count}`);
|
|
5838
|
+
}
|
|
5839
|
+
if (database.spaces.length > 1) {
|
|
5840
|
+
console.log(pc25.yellow(" more than one embedding space: run `codebuddy reindex --respace`"));
|
|
5841
|
+
}
|
|
5842
|
+
}
|
|
5843
|
+
console.log(
|
|
5844
|
+
capture2.stagedNotCaptured.length === 0 && capture2.journalBehind.length === 0 ? pc25.green("capture: nothing pending") : `capture: ${capture2.stagedNotCaptured.length} session(s) with uncaptured edits, ${capture2.journalBehind.length} journal(s) behind their transcript`
|
|
5845
|
+
);
|
|
5846
|
+
}
|
|
4951
5847
|
export {
|
|
4952
5848
|
createCli
|
|
4953
5849
|
};
|
|
4954
|
-
//# sourceMappingURL=main-
|
|
5850
|
+
//# sourceMappingURL=main-SFC5LV4E.js.map
|