@fml-inc/panopticon 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (124) hide show
  1. package/.claude-plugin/plugin.json +10 -0
  2. package/LICENSE +5 -0
  3. package/README.md +363 -0
  4. package/bin/hook-handler +3 -0
  5. package/bin/mcp-server +3 -0
  6. package/bin/panopticon +3 -0
  7. package/bin/proxy +3 -0
  8. package/bin/server +3 -0
  9. package/dist/api/client.d.ts +67 -0
  10. package/dist/api/client.js +48 -0
  11. package/dist/api/client.js.map +1 -0
  12. package/dist/chunk-3BUJ7URA.js +387 -0
  13. package/dist/chunk-3BUJ7URA.js.map +1 -0
  14. package/dist/chunk-3TZAKV3M.js +158 -0
  15. package/dist/chunk-3TZAKV3M.js.map +1 -0
  16. package/dist/chunk-4SM2H22C.js +169 -0
  17. package/dist/chunk-4SM2H22C.js.map +1 -0
  18. package/dist/chunk-7Q3BJMLG.js +62 -0
  19. package/dist/chunk-7Q3BJMLG.js.map +1 -0
  20. package/dist/chunk-BVOE7A2Z.js +412 -0
  21. package/dist/chunk-BVOE7A2Z.js.map +1 -0
  22. package/dist/chunk-CF4GPWLI.js +170 -0
  23. package/dist/chunk-CF4GPWLI.js.map +1 -0
  24. package/dist/chunk-DZ5HJFB4.js +467 -0
  25. package/dist/chunk-DZ5HJFB4.js.map +1 -0
  26. package/dist/chunk-HQCY722C.js +428 -0
  27. package/dist/chunk-HQCY722C.js.map +1 -0
  28. package/dist/chunk-HRCEIYKU.js +134 -0
  29. package/dist/chunk-HRCEIYKU.js.map +1 -0
  30. package/dist/chunk-K7YUPLES.js +76 -0
  31. package/dist/chunk-K7YUPLES.js.map +1 -0
  32. package/dist/chunk-L7G27XWF.js +130 -0
  33. package/dist/chunk-L7G27XWF.js.map +1 -0
  34. package/dist/chunk-LWXF7YRG.js +626 -0
  35. package/dist/chunk-LWXF7YRG.js.map +1 -0
  36. package/dist/chunk-NXH7AONS.js +1120 -0
  37. package/dist/chunk-NXH7AONS.js.map +1 -0
  38. package/dist/chunk-QK5442ZP.js +55 -0
  39. package/dist/chunk-QK5442ZP.js.map +1 -0
  40. package/dist/chunk-QVK6VGCV.js +1703 -0
  41. package/dist/chunk-QVK6VGCV.js.map +1 -0
  42. package/dist/chunk-RX2RXHBH.js +1699 -0
  43. package/dist/chunk-RX2RXHBH.js.map +1 -0
  44. package/dist/chunk-SEXU2WYG.js +788 -0
  45. package/dist/chunk-SEXU2WYG.js.map +1 -0
  46. package/dist/chunk-SUGSQ4YI.js +264 -0
  47. package/dist/chunk-SUGSQ4YI.js.map +1 -0
  48. package/dist/chunk-TGXFVAID.js +138 -0
  49. package/dist/chunk-TGXFVAID.js.map +1 -0
  50. package/dist/chunk-WLBNFVIG.js +447 -0
  51. package/dist/chunk-WLBNFVIG.js.map +1 -0
  52. package/dist/chunk-XLTCUH5A.js +1072 -0
  53. package/dist/chunk-XLTCUH5A.js.map +1 -0
  54. package/dist/chunk-YVRWVDIA.js +146 -0
  55. package/dist/chunk-YVRWVDIA.js.map +1 -0
  56. package/dist/chunk-ZEC4LRKS.js +176 -0
  57. package/dist/chunk-ZEC4LRKS.js.map +1 -0
  58. package/dist/cli.d.ts +1 -0
  59. package/dist/cli.js +1084 -0
  60. package/dist/cli.js.map +1 -0
  61. package/dist/config-NwoZC-GM.d.ts +20 -0
  62. package/dist/db.d.ts +46 -0
  63. package/dist/db.js +15 -0
  64. package/dist/db.js.map +1 -0
  65. package/dist/doctor.d.ts +37 -0
  66. package/dist/doctor.js +14 -0
  67. package/dist/doctor.js.map +1 -0
  68. package/dist/hooks/handler.d.ts +23 -0
  69. package/dist/hooks/handler.js +295 -0
  70. package/dist/hooks/handler.js.map +1 -0
  71. package/dist/index.d.ts +57 -0
  72. package/dist/index.js +101 -0
  73. package/dist/index.js.map +1 -0
  74. package/dist/mcp/server.d.ts +1 -0
  75. package/dist/mcp/server.js +243 -0
  76. package/dist/mcp/server.js.map +1 -0
  77. package/dist/otlp/server.d.ts +7 -0
  78. package/dist/otlp/server.js +17 -0
  79. package/dist/otlp/server.js.map +1 -0
  80. package/dist/permissions.d.ts +33 -0
  81. package/dist/permissions.js +14 -0
  82. package/dist/permissions.js.map +1 -0
  83. package/dist/pricing.d.ts +29 -0
  84. package/dist/pricing.js +13 -0
  85. package/dist/pricing.js.map +1 -0
  86. package/dist/proxy/server.d.ts +10 -0
  87. package/dist/proxy/server.js +20 -0
  88. package/dist/proxy/server.js.map +1 -0
  89. package/dist/prune.d.ts +18 -0
  90. package/dist/prune.js +13 -0
  91. package/dist/prune.js.map +1 -0
  92. package/dist/query.d.ts +56 -0
  93. package/dist/query.js +27 -0
  94. package/dist/query.js.map +1 -0
  95. package/dist/reparse-636YZCE3.js +14 -0
  96. package/dist/reparse-636YZCE3.js.map +1 -0
  97. package/dist/repo.d.ts +17 -0
  98. package/dist/repo.js +9 -0
  99. package/dist/repo.js.map +1 -0
  100. package/dist/scanner.d.ts +73 -0
  101. package/dist/scanner.js +15 -0
  102. package/dist/scanner.js.map +1 -0
  103. package/dist/sdk.d.ts +82 -0
  104. package/dist/sdk.js +208 -0
  105. package/dist/sdk.js.map +1 -0
  106. package/dist/server.d.ts +5 -0
  107. package/dist/server.js +25 -0
  108. package/dist/server.js.map +1 -0
  109. package/dist/setup.d.ts +35 -0
  110. package/dist/setup.js +19 -0
  111. package/dist/setup.js.map +1 -0
  112. package/dist/sync/index.d.ts +29 -0
  113. package/dist/sync/index.js +32 -0
  114. package/dist/sync/index.js.map +1 -0
  115. package/dist/targets.d.ts +279 -0
  116. package/dist/targets.js +20 -0
  117. package/dist/targets.js.map +1 -0
  118. package/dist/types-D-MYCBol.d.ts +128 -0
  119. package/dist/types.d.ts +164 -0
  120. package/dist/types.js +1 -0
  121. package/dist/types.js.map +1 -0
  122. package/hooks/hooks.json +274 -0
  123. package/package.json +124 -0
  124. package/skills/panopticon-optimize/SKILL.md +222 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1084 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ permissionsApply,
4
+ permissionsShow
5
+ } from "./chunk-TGXFVAID.js";
6
+ import {
7
+ activitySummary,
8
+ costBreakdown,
9
+ dbStats,
10
+ listPlans,
11
+ listSessions,
12
+ print,
13
+ pruneEstimate,
14
+ pruneExecute,
15
+ rawQuery,
16
+ refreshPricing,
17
+ search,
18
+ sessionTimeline,
19
+ syncPending,
20
+ syncReset,
21
+ syncTargetAdd,
22
+ syncTargetList,
23
+ syncTargetRemove,
24
+ syncWatermarkGet,
25
+ syncWatermarkSet
26
+ } from "./chunk-4SM2H22C.js";
27
+ import {
28
+ DAEMON_NAMES,
29
+ LOG_DIR,
30
+ logPaths,
31
+ openLogFd
32
+ } from "./chunk-7Q3BJMLG.js";
33
+ import {
34
+ refreshPricing as refreshPricing2
35
+ } from "./chunk-3TZAKV3M.js";
36
+ import {
37
+ loadUnifiedConfig
38
+ } from "./chunk-QK5442ZP.js";
39
+ import "./chunk-ZEC4LRKS.js";
40
+ import {
41
+ allTargets,
42
+ getTarget,
43
+ targetIds
44
+ } from "./chunk-QVK6VGCV.js";
45
+ import {
46
+ closeDb,
47
+ getDb
48
+ } from "./chunk-DZ5HJFB4.js";
49
+ import {
50
+ config,
51
+ ensureDataDir
52
+ } from "./chunk-K7YUPLES.js";
53
+
54
+ // src/cli.ts
55
+ import { execFileSync, spawn } from "child_process";
56
+ import fs2 from "fs";
57
+ import os from "os";
58
+ import path2 from "path";
59
+ import readline from "readline";
60
+ import { fileURLToPath } from "url";
61
+ import { Command } from "commander";
62
+
63
+ // src/toml.ts
64
+ import fs from "fs";
65
+ import path from "path";
66
+ import { parse, stringify } from "smol-toml";
67
+ function readTomlFile(filePath) {
68
+ try {
69
+ return parse(fs.readFileSync(filePath, "utf-8"));
70
+ } catch {
71
+ return {};
72
+ }
73
+ }
74
+ function writeTomlFile(filePath, data) {
75
+ fs.mkdirSync(path.dirname(filePath), { recursive: true });
76
+ fs.writeFileSync(filePath, `${stringify(data)}
77
+ `);
78
+ }
79
+
80
+ // src/cli.ts
81
+ function output(data) {
82
+ console.log(JSON.stringify(data, null, 2));
83
+ }
84
+ function getPluginRoot() {
85
+ let dir = path2.dirname(fileURLToPath(import.meta.url));
86
+ dir = path2.resolve(dir, "..");
87
+ return dir;
88
+ }
89
+ function stopExistingDaemons() {
90
+ const pidsKilled = /* @__PURE__ */ new Set();
91
+ for (const pidFile of [config.serverPidFile, config.pidFile]) {
92
+ try {
93
+ const pid = parseInt(fs2.readFileSync(pidFile, "utf-8").trim(), 10);
94
+ process.kill(pid, "SIGTERM");
95
+ pidsKilled.add(pid);
96
+ } catch {
97
+ }
98
+ try {
99
+ fs2.unlinkSync(pidFile);
100
+ } catch {
101
+ }
102
+ }
103
+ try {
104
+ const out = execFileSync("lsof", ["-ti", `tcp:${config.port}`], {
105
+ encoding: "utf-8",
106
+ timeout: 3e3,
107
+ stdio: ["ignore", "pipe", "ignore"]
108
+ }).trim();
109
+ for (const line of out.split("\n")) {
110
+ const pid = parseInt(line, 10);
111
+ if (pid && !pidsKilled.has(pid)) {
112
+ try {
113
+ process.kill(pid, "SIGTERM");
114
+ pidsKilled.add(pid);
115
+ } catch {
116
+ }
117
+ }
118
+ }
119
+ } catch {
120
+ }
121
+ if (pidsKilled.size > 0) {
122
+ const deadline = Date.now() + 3e3;
123
+ while (Date.now() < deadline) {
124
+ try {
125
+ execFileSync("lsof", ["-ti", `tcp:${config.port}`], {
126
+ encoding: "utf-8",
127
+ timeout: 1e3,
128
+ stdio: ["ignore", "pipe", "ignore"]
129
+ });
130
+ } catch {
131
+ break;
132
+ }
133
+ const waitMs = Math.min(200, deadline - Date.now());
134
+ if (waitMs > 0)
135
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs);
136
+ }
137
+ }
138
+ }
139
+ function readJsonFile(filePath) {
140
+ try {
141
+ return JSON.parse(fs2.readFileSync(filePath, "utf-8"));
142
+ } catch {
143
+ return null;
144
+ }
145
+ }
146
+ function writeJsonFile(filePath, data) {
147
+ fs2.mkdirSync(path2.dirname(filePath), { recursive: true });
148
+ fs2.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}
149
+ `);
150
+ }
151
+ function isProcessRunning(pidFile) {
152
+ if (!fs2.existsSync(pidFile)) return { running: false, pid: null };
153
+ const pid = parseInt(fs2.readFileSync(pidFile, "utf-8").trim(), 10);
154
+ try {
155
+ process.kill(pid, 0);
156
+ return { running: true, pid };
157
+ } catch {
158
+ return { running: false, pid };
159
+ }
160
+ }
161
+ function parseAge(value) {
162
+ const match = value.match(/^(\d+)\s*(d|h|m)$/);
163
+ if (!match) {
164
+ console.error(
165
+ `Invalid --older-than value: ${value} (use e.g. 30d, 24h, 60m)`
166
+ );
167
+ process.exit(1);
168
+ }
169
+ const n = parseInt(match[1], 10);
170
+ const unit = match[2];
171
+ const multipliers = {
172
+ d: 864e5,
173
+ h: 36e5,
174
+ m: 6e4
175
+ };
176
+ return n * multipliers[unit];
177
+ }
178
+ function promptUser(question) {
179
+ const rl = readline.createInterface({
180
+ input: process.stdin,
181
+ output: process.stdout
182
+ });
183
+ return new Promise((resolve) => {
184
+ rl.question(question, (answer) => {
185
+ rl.close();
186
+ resolve(answer.trim());
187
+ });
188
+ });
189
+ }
190
+ function tailLines(filePath, n) {
191
+ const CHUNK_SIZE = 64 * 1024;
192
+ const fd = fs2.openSync(filePath, "r");
193
+ try {
194
+ const { size } = fs2.fstatSync(fd);
195
+ if (size === 0) return [];
196
+ const readStart = Math.max(0, size - CHUNK_SIZE);
197
+ const buf = Buffer.alloc(size - readStart);
198
+ fs2.readSync(fd, buf, 0, buf.length, readStart);
199
+ const chunk = buf.toString("utf-8");
200
+ const lines = chunk.split("\n");
201
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
202
+ if (readStart > 0 && lines.length > 0) lines.shift();
203
+ return lines.slice(-n);
204
+ } finally {
205
+ fs2.closeSync(fd);
206
+ }
207
+ }
208
+ function readStdin() {
209
+ const chunks = [];
210
+ return new Promise((resolve, reject) => {
211
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
212
+ process.stdin.on(
213
+ "end",
214
+ () => resolve(Buffer.concat(chunks).toString("utf-8"))
215
+ );
216
+ process.stdin.on("error", reject);
217
+ });
218
+ }
219
+ function configureShellEnv(force, target = "claude", proxy = false) {
220
+ const shellRc = path2.join(
221
+ os.homedir(),
222
+ process.env.SHELL?.includes("zsh") ? ".zshrc" : ".bashrc"
223
+ );
224
+ const rcContent = fs2.existsSync(shellRc) ? fs2.readFileSync(shellRc, "utf-8") : "";
225
+ const allTargetVarNames = /* @__PURE__ */ new Set();
226
+ for (const v of allTargets()) {
227
+ for (const [varName] of v.shellEnv.envVars(config.port, true)) {
228
+ allTargetVarNames.add(varName);
229
+ }
230
+ }
231
+ const PANOPTICON_VARS = [
232
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
233
+ "OTEL_EXPORTER_OTLP_PROTOCOL",
234
+ "OTEL_METRICS_EXPORTER",
235
+ "OTEL_LOGS_EXPORTER",
236
+ "OTEL_LOG_TOOL_DETAILS",
237
+ "OTEL_LOG_USER_PROMPTS",
238
+ "OTEL_METRIC_EXPORT_INTERVAL",
239
+ ...allTargetVarNames
240
+ ];
241
+ const PANOPTICON_COMMENTS = ["# >>> panopticon", "# <<< panopticon"];
242
+ const isPanopticonLine = (line) => {
243
+ const trimmed = line.trim();
244
+ if (PANOPTICON_COMMENTS.some((c) => trimmed.startsWith(c))) return true;
245
+ for (const v of PANOPTICON_VARS) {
246
+ if (trimmed === `export ${v}` || trimmed.startsWith(`export ${v}=`))
247
+ return true;
248
+ }
249
+ return false;
250
+ };
251
+ const wantedLines = [
252
+ ["# >>> panopticon >>>", "# >>> panopticon >>>"],
253
+ [
254
+ "OTEL_EXPORTER_OTLP_ENDPOINT",
255
+ `export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:${config.port}`
256
+ ],
257
+ [
258
+ "OTEL_EXPORTER_OTLP_PROTOCOL",
259
+ "export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf"
260
+ ],
261
+ ["OTEL_METRICS_EXPORTER", "export OTEL_METRICS_EXPORTER=otlp"],
262
+ ["OTEL_LOGS_EXPORTER", "export OTEL_LOGS_EXPORTER=otlp"],
263
+ ["OTEL_LOG_TOOL_DETAILS", "export OTEL_LOG_TOOL_DETAILS=1"],
264
+ ["OTEL_LOG_USER_PROMPTS", "export OTEL_LOG_USER_PROMPTS=1"],
265
+ ["OTEL_METRIC_EXPORT_INTERVAL", "export OTEL_METRIC_EXPORT_INTERVAL=10000"]
266
+ ];
267
+ const selectedTargetList = target === "all" ? allTargets() : allTargets().filter((v) => v.id === target);
268
+ for (const t of selectedTargetList) {
269
+ for (const [varName, value] of t.shellEnv.envVars(config.port, proxy)) {
270
+ wantedLines.push([varName, `export ${varName}=${value}`]);
271
+ }
272
+ }
273
+ wantedLines.push(["# <<< panopticon <<<", "# <<< panopticon <<<"]);
274
+ const lines = rcContent.split("\n");
275
+ const seen = /* @__PURE__ */ new Set();
276
+ let lastPanopticonIdx = -1;
277
+ for (let i = 0; i < lines.length; i++) {
278
+ if (!isPanopticonLine(lines[i])) continue;
279
+ lastPanopticonIdx = i;
280
+ const match = wantedLines.find(([key]) => {
281
+ if (key.startsWith("#")) return lines[i].trim().startsWith(key);
282
+ return lines[i].trim() === `export ${key}` || lines[i].trim().startsWith(`export ${key}=`);
283
+ });
284
+ if (match) {
285
+ if (!force && lines[i].trim() !== match[1] && !match[0].startsWith("#")) {
286
+ console.log(` \u26A0 Keeping existing value: ${lines[i].trim()}`);
287
+ console.log(` (default would be: ${match[1]})`);
288
+ console.log(" (use --force to overwrite)");
289
+ } else {
290
+ lines[i] = match[1];
291
+ }
292
+ seen.add(match[0]);
293
+ } else {
294
+ lines[i] = "";
295
+ }
296
+ }
297
+ const newLines = wantedLines.filter(([key]) => !seen.has(key)).map(([, val]) => val);
298
+ if (newLines.length > 0) {
299
+ if (lastPanopticonIdx >= 0) {
300
+ lines.splice(lastPanopticonIdx + 1, 0, ...newLines);
301
+ } else {
302
+ lines.push("", ...newLines, "");
303
+ }
304
+ }
305
+ fs2.writeFileSync(shellRc, lines.join("\n"));
306
+ console.log(
307
+ ` ${lastPanopticonIdx >= 0 ? "Updated" : "Added"} env vars in ${shellRc}
308
+ `
309
+ );
310
+ }
311
+ function removeShellEnv() {
312
+ const shellRc = path2.join(
313
+ os.homedir(),
314
+ process.env.SHELL?.includes("zsh") ? ".zshrc" : ".bashrc"
315
+ );
316
+ if (!fs2.existsSync(shellRc)) return;
317
+ const content = fs2.readFileSync(shellRc, "utf-8");
318
+ const lines = content.split("\n");
319
+ let inBlock = false;
320
+ const filtered = lines.filter((line) => {
321
+ if (line.trim().startsWith("# >>> panopticon")) {
322
+ inBlock = true;
323
+ return false;
324
+ }
325
+ if (line.trim().startsWith("# <<< panopticon")) {
326
+ inBlock = false;
327
+ return false;
328
+ }
329
+ return !inBlock;
330
+ });
331
+ fs2.writeFileSync(shellRc, filtered.join("\n"));
332
+ console.log(` Removed panopticon env vars from ${shellRc}
333
+ `);
334
+ }
335
+ var program = new Command();
336
+ program.name("panopticon").description("Observability for Claude Code").version(
337
+ true ? "0.1.0+2aee981" : "dev"
338
+ );
339
+ program.command("install").alias("setup").description("Build, register plugin, init DB, configure shell").option(
340
+ "--target <target>",
341
+ `Target CLI: ${targetIds().join(", ")}, all`,
342
+ "all"
343
+ ).option("--proxy", "Also route API traffic through the panopticon proxy").option("--force", "Overwrite customized env vars with defaults").action(async (opts) => {
344
+ const validTargets = [...targetIds(), "all"];
345
+ if (!validTargets.includes(opts.target)) {
346
+ console.error(
347
+ `Invalid target: ${opts.target}. Must be ${validTargets.join(", ")}.`
348
+ );
349
+ process.exit(1);
350
+ }
351
+ const pluginRoot = getPluginRoot();
352
+ await install(pluginRoot, opts);
353
+ });
354
+ program.command("uninstall").description("Remove panopticon hooks, shell config, and optionally all data").option(
355
+ "--target <target>",
356
+ `Target CLI: ${targetIds().join(", ")}, all`,
357
+ "all"
358
+ ).option("--purge", "Also remove database and all data").action(async (opts) => {
359
+ const validTargets = [...targetIds(), "all"];
360
+ if (!validTargets.includes(opts.target)) {
361
+ console.error(
362
+ `Invalid target: ${opts.target}. Must be ${validTargets.join(", ")}.`
363
+ );
364
+ process.exit(1);
365
+ }
366
+ const targetId = opts.target ?? "all";
367
+ const purge = !!opts.purge;
368
+ console.log("Uninstalling panopticon...\n");
369
+ console.log("[1/6] Stopping daemons...");
370
+ stopExistingDaemons();
371
+ console.log();
372
+ console.log("[2/6] Uninstalling MCP plugin...");
373
+ if (targetId === "all" || targetId === "claude") {
374
+ try {
375
+ execFileSync(
376
+ "claude",
377
+ ["plugin", "uninstall", "panopticon@local-plugins"],
378
+ {
379
+ stdio: "ignore",
380
+ timeout: 1e4
381
+ }
382
+ );
383
+ console.log(" Uninstalled plugin via Claude Code CLI");
384
+ } catch {
385
+ }
386
+ } else {
387
+ console.log(" Skipped (target-specific uninstall)");
388
+ }
389
+ console.log();
390
+ const selectedTargets = targetId === "all" ? allTargets() : allTargets().filter((t) => t.id === targetId);
391
+ for (const t of selectedTargets) {
392
+ console.log(`[3/6] Removing panopticon from ${t.detect.displayName}...`);
393
+ let existing;
394
+ if (t.config.configFormat === "toml") {
395
+ existing = readTomlFile(t.config.configPath);
396
+ } else {
397
+ existing = readJsonFile(t.config.configPath) ?? {};
398
+ }
399
+ const updated = t.hooks.removeInstallConfig(existing);
400
+ if (t.config.configFormat === "toml") {
401
+ writeTomlFile(t.config.configPath, updated);
402
+ } else {
403
+ writeJsonFile(t.config.configPath, updated);
404
+ }
405
+ console.log(` ${t.config.configPath}
406
+ `);
407
+ }
408
+ console.log("[4/6] Cleaning shell environment...");
409
+ removeShellEnv();
410
+ if (targetId === "all") {
411
+ console.log("[5/6] Removing marketplace and plugin cache...");
412
+ try {
413
+ fs2.rmSync(config.marketplaceDir, { recursive: true, force: true });
414
+ console.log(` Removed ${config.marketplaceDir}`);
415
+ } catch {
416
+ }
417
+ try {
418
+ fs2.rmSync(config.pluginCacheDir, { recursive: true, force: true });
419
+ console.log(` Removed ${config.pluginCacheDir}`);
420
+ } catch {
421
+ }
422
+ console.log();
423
+ console.log("[6/6] Removing skills...");
424
+ const pluginRoot = getPluginRoot();
425
+ const skillsSource = path2.join(pluginRoot, "skills");
426
+ const skillsTarget = path2.join(os.homedir(), ".claude", "skills");
427
+ if (fs2.existsSync(skillsSource)) {
428
+ for (const name of fs2.readdirSync(skillsSource)) {
429
+ const dest = path2.join(skillsTarget, name);
430
+ try {
431
+ fs2.rmSync(dest, { recursive: true, force: true });
432
+ console.log(` Removed ${dest}`);
433
+ } catch {
434
+ }
435
+ }
436
+ }
437
+ console.log();
438
+ } else {
439
+ console.log("[5/6] Skipping marketplace (target-specific uninstall)");
440
+ console.log("[6/6] Skipping skills (target-specific uninstall)\n");
441
+ }
442
+ if (purge) {
443
+ console.log("Purging data...");
444
+ closeDb();
445
+ try {
446
+ fs2.rmSync(config.dataDir, { recursive: true, force: true });
447
+ console.log(` Removed ${config.dataDir}`);
448
+ } catch {
449
+ }
450
+ try {
451
+ fs2.rmSync(LOG_DIR, { recursive: true, force: true });
452
+ console.log(` Removed ${LOG_DIR}`);
453
+ } catch {
454
+ }
455
+ console.log();
456
+ }
457
+ console.log("Done! Panopticon has been uninstalled.");
458
+ if (!purge) {
459
+ console.log(
460
+ `Database preserved at ${config.dataDir} (use --purge to remove)`
461
+ );
462
+ }
463
+ });
464
+ program.command("update").description("Update panopticon to the latest version").action(async () => {
465
+ const currentVersion = true ? "0.1.0+2aee981" : "unknown";
466
+ console.log(`Current: ${currentVersion}`);
467
+ console.log(
468
+ "To update, re-run the install command for your package manager:\n"
469
+ );
470
+ console.log(" pnpm install -g @fml-inc/panopticon@latest");
471
+ console.log(" # or: npm install -g @fml-inc/panopticon@latest\n");
472
+ console.log("Then run: panopticon install");
473
+ });
474
+ async function install(pluginRoot, opts) {
475
+ const force = opts.force ?? false;
476
+ const target = opts.target ?? "claude";
477
+ console.log("Installing panopticon...\n");
478
+ const pkgJson = readJsonFile(path2.join(pluginRoot, "package.json"));
479
+ const version = pkgJson?.version ?? "0.0.0-dev";
480
+ console.log("[1/5] Initializing database and log directory...");
481
+ ensureDataDir();
482
+ const logDir = path2.dirname(logPaths.server);
483
+ fs2.mkdirSync(logDir, { recursive: true });
484
+ getDb();
485
+ closeDb();
486
+ console.log(` ${config.dbPath}`);
487
+ console.log(` ${logDir}`);
488
+ const pricing = await refreshPricing2();
489
+ console.log(
490
+ pricing ? ` Cached pricing for ${Object.keys(pricing.models).length} models
491
+ ` : " Could not fetch pricing (will use defaults)\n"
492
+ );
493
+ console.log("[2/5] Setting up local marketplace...");
494
+ fs2.mkdirSync(path2.join(config.marketplaceDir, ".claude-plugin"), {
495
+ recursive: true
496
+ });
497
+ const manifest = readJsonFile(config.marketplaceManifest) ?? {
498
+ name: "local-plugins",
499
+ owner: { name: os.userInfo().username },
500
+ plugins: []
501
+ };
502
+ const plugins = manifest.plugins ?? [];
503
+ const existing = plugins.findIndex((p) => p.name === "panopticon");
504
+ const entry = {
505
+ name: "panopticon",
506
+ source: "./panopticon",
507
+ description: pkgJson?.description ?? "Observability for Claude Code"
508
+ };
509
+ if (existing >= 0) {
510
+ plugins[existing] = entry;
511
+ } else {
512
+ plugins.push(entry);
513
+ }
514
+ manifest.plugins = plugins;
515
+ writeJsonFile(config.marketplaceManifest, manifest);
516
+ const symlinkType = process.platform === "win32" ? "junction" : "dir";
517
+ const marketplaceLink = path2.join(config.marketplaceDir, "panopticon");
518
+ try {
519
+ fs2.unlinkSync(marketplaceLink);
520
+ } catch {
521
+ }
522
+ fs2.symlinkSync(pluginRoot, marketplaceLink, symlinkType);
523
+ const cacheDir = path2.join(config.pluginCacheDir, version);
524
+ fs2.mkdirSync(cacheDir, { recursive: true });
525
+ const filesToSync = [
526
+ ".claude-plugin",
527
+ "hooks",
528
+ "bin",
529
+ "dist",
530
+ "skills",
531
+ "node_modules",
532
+ "package.json",
533
+ "package-lock.json"
534
+ ];
535
+ for (const name of filesToSync) {
536
+ const src = path2.join(pluginRoot, name);
537
+ const dest = path2.join(cacheDir, name);
538
+ if (fs2.existsSync(src)) {
539
+ fs2.rmSync(dest, { recursive: true, force: true });
540
+ fs2.cpSync(src, dest, { recursive: true, dereference: true });
541
+ }
542
+ }
543
+ const binDir = path2.join(pluginRoot, "bin");
544
+ if (fs2.existsSync(binDir)) {
545
+ for (const file of fs2.readdirSync(binDir)) {
546
+ const binPath = path2.join(binDir, file);
547
+ if (fs2.statSync(binPath).isFile()) {
548
+ fs2.chmodSync(binPath, 493);
549
+ }
550
+ }
551
+ const cachedBinDir = path2.join(cacheDir, "bin");
552
+ if (fs2.existsSync(cachedBinDir)) {
553
+ for (const file of fs2.readdirSync(cachedBinDir)) {
554
+ const binPath = path2.join(cachedBinDir, file);
555
+ if (fs2.statSync(binPath).isFile()) {
556
+ fs2.chmodSync(binPath, 493);
557
+ }
558
+ }
559
+ }
560
+ }
561
+ console.log(` Marketplace: ${config.marketplaceDir}`);
562
+ console.log(` Cache: ${cacheDir}
563
+ `);
564
+ const selectedTargets = target === "all" ? allTargets() : [getTarget(target)].filter(
565
+ Boolean
566
+ );
567
+ for (const t of selectedTargets) {
568
+ console.log(`[3/5] Registering panopticon in ${t.detect.displayName}...`);
569
+ let existingConfig;
570
+ if (t.config.configFormat === "toml") {
571
+ existingConfig = readTomlFile(t.config.configPath);
572
+ } else {
573
+ existingConfig = readJsonFile(t.config.configPath) ?? {};
574
+ }
575
+ const updatedConfig = t.hooks.applyInstallConfig(existingConfig, {
576
+ pluginRoot,
577
+ port: config.port,
578
+ proxy: !!opts.proxy
579
+ });
580
+ if (t.config.configFormat === "toml") {
581
+ writeTomlFile(t.config.configPath, updatedConfig);
582
+ } else {
583
+ writeJsonFile(t.config.configPath, updatedConfig);
584
+ }
585
+ if (opts.proxy && t.id === "codex") {
586
+ console.log(" API proxy enabled (--proxy)");
587
+ }
588
+ console.log(` ${t.config.configPath}
589
+ `);
590
+ }
591
+ const skippedTargets = allTargets().filter(
592
+ (v) => !selectedTargets.some((st) => st.id === v.id)
593
+ );
594
+ for (const t of skippedTargets) {
595
+ console.log(`[3/5] Skipping ${t.detect.displayName} settings...
596
+ `);
597
+ }
598
+ console.log("[4/5] Installing skills...");
599
+ const skillsSource = path2.join(pluginRoot, "skills");
600
+ const skillsTarget = path2.join(os.homedir(), ".claude", "skills");
601
+ if (fs2.existsSync(skillsSource)) {
602
+ for (const skillName of fs2.readdirSync(skillsSource)) {
603
+ const src = path2.join(skillsSource, skillName);
604
+ if (!fs2.statSync(src).isDirectory()) continue;
605
+ const dest = path2.join(skillsTarget, skillName);
606
+ fs2.mkdirSync(dest, { recursive: true });
607
+ for (const file of fs2.readdirSync(src)) {
608
+ fs2.cpSync(path2.join(src, file), path2.join(dest, file), {
609
+ recursive: true
610
+ });
611
+ }
612
+ console.log(` ${skillName} -> ${dest}`);
613
+ }
614
+ }
615
+ console.log();
616
+ console.log("[5/5] Configuring shell environment...");
617
+ configureShellEnv(force, target, !!opts.proxy);
618
+ stopExistingDaemons();
619
+ const serverScript = path2.resolve(
620
+ path2.dirname(fileURLToPath(import.meta.url)),
621
+ "server.js"
622
+ );
623
+ const logFd = openLogFd("server");
624
+ const child = spawn("node", [serverScript], {
625
+ detached: true,
626
+ stdio: ["ignore", logFd, logFd],
627
+ env: { ...process.env, PANOPTICON_PORT: String(config.port) }
628
+ });
629
+ if (child.pid) {
630
+ fs2.writeFileSync(config.serverPidFile, String(child.pid));
631
+ console.log(`
632
+ Server started (PID ${child.pid}) on :${config.port}`);
633
+ }
634
+ child.unref();
635
+ fs2.closeSync(logFd);
636
+ const assistant = target === "all" ? allTargets().map((v) => v.detect.displayName).join(", ") : getTarget(target)?.detect.displayName ?? target;
637
+ console.log(`Done! Start a new ${assistant} session to activate.
638
+ `);
639
+ console.log("Verify with: panopticon status");
640
+ }
641
+ program.command("start").description("Start panopticon server (background)").action(async () => {
642
+ ensureDataDir();
643
+ if (fs2.existsSync(config.serverPidFile)) {
644
+ const pid = parseInt(
645
+ fs2.readFileSync(config.serverPidFile, "utf-8").trim(),
646
+ 10
647
+ );
648
+ try {
649
+ process.kill(pid, 0);
650
+ console.log(`Panopticon already running (PID ${pid})`);
651
+ return;
652
+ } catch {
653
+ fs2.unlinkSync(config.serverPidFile);
654
+ }
655
+ }
656
+ for (const legacyPid of [config.pidFile, config.proxyPidFile]) {
657
+ if (fs2.existsSync(legacyPid)) {
658
+ try {
659
+ const pid = parseInt(fs2.readFileSync(legacyPid, "utf-8").trim(), 10);
660
+ process.kill(pid, "SIGTERM");
661
+ } catch {
662
+ }
663
+ try {
664
+ fs2.unlinkSync(legacyPid);
665
+ } catch {
666
+ }
667
+ }
668
+ }
669
+ const serverScript = path2.resolve(
670
+ path2.dirname(fileURLToPath(import.meta.url)),
671
+ "server.js"
672
+ );
673
+ const logFd = openLogFd("server");
674
+ const child = spawn("node", [serverScript], {
675
+ detached: true,
676
+ stdio: ["ignore", logFd, logFd],
677
+ env: {
678
+ ...process.env,
679
+ PANOPTICON_PORT: String(config.port)
680
+ }
681
+ });
682
+ await new Promise((resolve, reject) => {
683
+ child.on("error", (err) => {
684
+ reject(new Error(`Failed to start: ${err.message}`));
685
+ });
686
+ setTimeout(() => {
687
+ if (child.pid) {
688
+ fs2.writeFileSync(config.serverPidFile, String(child.pid));
689
+ child.unref();
690
+ fs2.closeSync(logFd);
691
+ console.log(
692
+ `Panopticon started (PID ${child.pid}) on :${config.port}`
693
+ );
694
+ console.log(`Log: ${logPaths.server}`);
695
+ resolve();
696
+ } else {
697
+ fs2.closeSync(logFd);
698
+ reject(new Error("Failed to start panopticon server"));
699
+ }
700
+ }, 500);
701
+ });
702
+ });
703
+ program.command("stop").description("Stop panopticon server").action(() => {
704
+ if (!fs2.existsSync(config.serverPidFile)) {
705
+ console.log("Panopticon is not running (no PID file)");
706
+ return;
707
+ }
708
+ const pid = parseInt(
709
+ fs2.readFileSync(config.serverPidFile, "utf-8").trim(),
710
+ 10
711
+ );
712
+ try {
713
+ process.kill(pid, "SIGTERM");
714
+ fs2.unlinkSync(config.serverPidFile);
715
+ console.log(`Panopticon stopped (PID ${pid})`);
716
+ } catch {
717
+ fs2.unlinkSync(config.serverPidFile);
718
+ console.log("Panopticon was not running (stale PID file removed)");
719
+ }
720
+ });
721
+ program.command("doctor").description("Check system health, server, database, and configuration").option("--json", "Output as JSON").action(async (opts) => {
722
+ const { doctor } = await import("./doctor.js");
723
+ const result = await doctor();
724
+ if (opts.json) {
725
+ output(result);
726
+ return;
727
+ }
728
+ console.log(
729
+ `System: ${result.system.os} \xB7 Node ${result.system.node}${result.system.sandbox ? " \xB7 Sandbox" : ""}`
730
+ );
731
+ console.log();
732
+ for (const check of result.checks) {
733
+ const icon = check.status === "ok" ? "\x1B[32m\u2713\x1B[0m" : check.status === "warn" ? "\x1B[33m!\x1B[0m" : "\x1B[31m\u2717\x1B[0m";
734
+ console.log(` ${icon} ${check.label.padEnd(12)} ${check.detail}`);
735
+ }
736
+ console.log();
737
+ const passed = result.checks.filter((c) => c.status === "ok").length;
738
+ const warned = result.checks.filter((c) => c.status === "warn").length;
739
+ const failed = result.checks.filter((c) => c.status === "fail").length;
740
+ const parts = [];
741
+ if (passed > 0) parts.push(`\x1B[32m${passed} passed\x1B[0m`);
742
+ if (warned > 0)
743
+ parts.push(`\x1B[33m${warned} warning${warned > 1 ? "s" : ""}\x1B[0m`);
744
+ if (failed > 0) parts.push(`\x1B[31m${failed} failed\x1B[0m`);
745
+ console.log(` ${parts.join(", ")}`);
746
+ if (result.recentErrors.length > 0) {
747
+ console.log();
748
+ console.log(" Recent errors:");
749
+ for (const err of result.recentErrors) {
750
+ console.log(` [${err.id}] ${err.body}`);
751
+ }
752
+ }
753
+ if (result.recentEvents.length > 0) {
754
+ console.log();
755
+ console.log(" Recent events:");
756
+ for (const evt of result.recentEvents) {
757
+ const tool = evt.toolName ? ` (${evt.toolName})` : "";
758
+ console.log(` ${evt.eventType}${tool} \u2014 ${evt.timestamp}`);
759
+ }
760
+ }
761
+ console.log();
762
+ });
763
+ program.command("status").description("Show server status and database stats").action(async () => {
764
+ const server = isProcessRunning(config.serverPidFile);
765
+ console.log("Panopticon Status");
766
+ console.log("=================");
767
+ console.log();
768
+ console.log(
769
+ `Server: ${server.running ? `running (PID ${server.pid}, port ${config.port})` : "stopped"}`
770
+ );
771
+ console.log(`Database: ${config.dbPath}`);
772
+ console.log();
773
+ console.log("Log files:");
774
+ for (const name of DAEMON_NAMES) {
775
+ const logPath = logPaths[name];
776
+ let sizeStr = "not created";
777
+ try {
778
+ const stat = fs2.statSync(logPath);
779
+ sizeStr = stat.size < 1024 ? `${stat.size} B` : `${(stat.size / 1024).toFixed(1)} KB`;
780
+ } catch {
781
+ }
782
+ console.log(` ${name}: ${logPath} (${sizeStr})`);
783
+ }
784
+ if (fs2.existsSync(config.dbPath)) {
785
+ const stat = fs2.statSync(config.dbPath);
786
+ console.log(`Database size: ${(stat.size / 1024).toFixed(1)} KB`);
787
+ if (server.running) {
788
+ try {
789
+ const stats = await dbStats();
790
+ console.log();
791
+ console.log("Row counts:");
792
+ console.log(` sessions: ${stats.sessions}`);
793
+ console.log(` messages: ${stats.messages}`);
794
+ console.log(` tool_calls: ${stats.tool_calls}`);
795
+ console.log(` scanner_turns: ${stats.scanner_turns}`);
796
+ console.log(` scanner_events: ${stats.scanner_events}`);
797
+ console.log(` hook_events: ${stats.hook_events}`);
798
+ console.log(` otel_logs: ${stats.otel_logs}`);
799
+ console.log(` otel_metrics: ${stats.otel_metrics}`);
800
+ } catch {
801
+ console.log(" (could not read database)");
802
+ }
803
+ }
804
+ } else {
805
+ console.log("Database: not initialized (run 'panopticon install')");
806
+ }
807
+ try {
808
+ const cfg = loadUnifiedConfig();
809
+ const targets = cfg.sync.targets;
810
+ if (targets.length > 0) {
811
+ console.log();
812
+ console.log("Sync targets:");
813
+ for (const t of targets) {
814
+ console.log(` ${t.name} \u2192 ${t.url}`);
815
+ if (server.running) {
816
+ try {
817
+ const result = await syncPending(t.name);
818
+ if (result.totalPending === 0) {
819
+ console.log(" status: up to date");
820
+ } else {
821
+ console.log(` pending: ${result.totalPending} total`);
822
+ for (const [table, info] of Object.entries(result.tables)) {
823
+ console.log(
824
+ ` ${table}: ${info.pending} (${info.watermark} / ${info.maxId})`
825
+ );
826
+ }
827
+ }
828
+ } catch {
829
+ }
830
+ }
831
+ }
832
+ }
833
+ } catch {
834
+ }
835
+ });
836
+ program.command("logs").alias("log").description("View daemon logs (otlp, mcp)").argument("[daemon]", "Daemon name (otlp, mcp)", "otlp").option("-f, --follow", "Follow log output (like tail -f)").option("-n, --lines <count>", "Number of lines to show", "50").action(async (daemon, opts) => {
837
+ if (!DAEMON_NAMES.includes(daemon)) {
838
+ console.error(`Unknown daemon: ${daemon}`);
839
+ console.log(`Available: ${DAEMON_NAMES.join(", ")}`);
840
+ process.exit(1);
841
+ }
842
+ const logPath = logPaths[daemon];
843
+ const numLines = parseInt(opts.lines, 10);
844
+ if (!fs2.existsSync(logPath)) {
845
+ console.log(`No logs yet for ${daemon} (${logPath})`);
846
+ return;
847
+ }
848
+ const lines = tailLines(logPath, numLines);
849
+ for (const line of lines) {
850
+ console.log(line);
851
+ }
852
+ if (opts.follow) {
853
+ let pos = fs2.statSync(logPath).size;
854
+ fs2.watchFile(logPath, { interval: 200 }, () => {
855
+ const stat = fs2.statSync(logPath);
856
+ if (stat.size > pos) {
857
+ const fd = fs2.openSync(logPath, "r");
858
+ const buf = Buffer.alloc(stat.size - pos);
859
+ fs2.readSync(fd, buf, 0, buf.length, pos);
860
+ fs2.closeSync(fd);
861
+ process.stdout.write(buf.toString("utf-8"));
862
+ pos = stat.size;
863
+ } else if (stat.size < pos) {
864
+ pos = 0;
865
+ }
866
+ });
867
+ await new Promise(() => {
868
+ });
869
+ }
870
+ });
871
+ program.command("prune").description("Delete old data from the database").option("--older-than <age>", "Max age (e.g. 30d, 24h, 60m)", "30d").option("--dry-run", "Show estimate without deleting").option("--vacuum", "Reclaim disk space after pruning").option("--yes", "Skip confirmation prompt").action(async (opts) => {
872
+ const ageMs = parseAge(opts.olderThan);
873
+ const cutoffMs = Date.now() - ageMs;
874
+ const cutoffDate = new Date(cutoffMs).toISOString();
875
+ console.log(
876
+ `Pruning rows older than ${opts.olderThan} (before ${cutoffDate})`
877
+ );
878
+ console.log();
879
+ const estimate = await pruneEstimate(cutoffMs);
880
+ const total = Object.values(estimate).reduce((a, b) => a + b, 0);
881
+ console.log("Rows to delete:");
882
+ for (const [key, count] of Object.entries(estimate)) {
883
+ if (count > 0) console.log(` ${key}: ${count}`);
884
+ }
885
+ console.log(` total: ${total}`);
886
+ console.log();
887
+ if (total === 0) {
888
+ console.log("Nothing to prune.");
889
+ return;
890
+ }
891
+ if (opts.dryRun) {
892
+ console.log("Dry run \u2014 no rows deleted.");
893
+ return;
894
+ }
895
+ if (!opts.yes) {
896
+ const answer = await promptUser("Proceed with deletion? [y/N] ");
897
+ if (answer.toLowerCase() !== "y") {
898
+ console.log("Aborted.");
899
+ return;
900
+ }
901
+ }
902
+ const result = await pruneExecute(cutoffMs, {
903
+ vacuum: opts.vacuum
904
+ });
905
+ console.log("Deleted:");
906
+ for (const [key, count] of Object.entries(result)) {
907
+ if (count > 0) console.log(` ${key}: ${count}`);
908
+ }
909
+ if (opts.vacuum) {
910
+ console.log("\nDisk space reclaimed.");
911
+ }
912
+ });
913
+ program.command("sync").description("Manage sync targets (OTLP export)").addCommand(
914
+ new Command("add").description("Add or update a sync target").argument("<name>", "Target name").argument("<url>", "OTLP endpoint base URL").option("--token <token>", "Bearer token for auth").option(
915
+ "--token-command <command>",
916
+ "Shell command that returns a token (e.g. 'gh auth token')"
917
+ ).action(async (name, url, opts) => {
918
+ await syncTargetAdd({
919
+ name,
920
+ url,
921
+ token: opts.token ?? void 0,
922
+ tokenCommand: opts.tokenCommand ?? void 0
923
+ });
924
+ console.log(`Added sync target "${name}" \u2192 ${url}`);
925
+ console.log("Restart panopticon to activate.");
926
+ })
927
+ ).addCommand(
928
+ new Command("remove").description("Remove a sync target").argument("<name>", "Target name").action(async (name) => {
929
+ const result = await syncTargetRemove(name);
930
+ if (result.ok) {
931
+ console.log(`Removed sync target "${name}"`);
932
+ console.log("Restart panopticon to apply.");
933
+ } else {
934
+ console.log(`No target named "${name}"`);
935
+ }
936
+ })
937
+ ).addCommand(
938
+ new Command("list").description("List sync targets").action(async () => {
939
+ const result = await syncTargetList();
940
+ if (result.targets.length === 0) {
941
+ console.log("No sync targets configured.");
942
+ return;
943
+ }
944
+ for (const t of result.targets) {
945
+ const auth = t.token ? " (token)" : t.tokenCommand ? ` (token-command: ${t.tokenCommand})` : "";
946
+ console.log(` ${t.name} \u2192 ${t.url}${auth}`);
947
+ }
948
+ })
949
+ ).addCommand(
950
+ new Command("reset").description("Reset sync watermarks (re-syncs all data)").argument("[target]", "Reset only this sync target (default: all)").action(async (targetName) => {
951
+ await syncReset(targetName);
952
+ console.log(
953
+ targetName ? `Reset sync watermarks for "${targetName}"` : "Reset all sync watermarks"
954
+ );
955
+ console.log("Restart panopticon to re-sync.");
956
+ })
957
+ ).addCommand(
958
+ new Command("watermark").description("Get or set sync watermarks").argument("<target>", "Sync target name").argument("[table]", "Table name (omit to show all)").option("--set <value>", "Set watermark to this value", parseInt).action(async (target, table, opts) => {
959
+ if (opts?.set !== void 0) {
960
+ if (!table) {
961
+ console.error("Table name is required when setting a watermark");
962
+ process.exit(1);
963
+ }
964
+ const result = await syncWatermarkSet(target, table, opts.set);
965
+ console.log(`${result.key} = ${result.value}`);
966
+ } else {
967
+ const result = await syncWatermarkGet(target, table);
968
+ if (table) {
969
+ const r = result;
970
+ console.log(`${r.key} = ${r.value}`);
971
+ } else {
972
+ const r = result;
973
+ console.log(`Watermarks for "${r.target}":`);
974
+ for (const [tbl, value] of Object.entries(r.watermarks)) {
975
+ console.log(` ${tbl}: ${value}`);
976
+ }
977
+ }
978
+ }
979
+ })
980
+ );
981
+ program.command("sessions").description("List recent sessions with stats (event count, tools, cost)").option("--limit <n>", "Max sessions to return (default 20)", parseInt).option(
982
+ "--since <duration>",
983
+ 'Time filter: ISO date or relative like "24h", "7d", "30m"'
984
+ ).action(async (opts) => {
985
+ output(await listSessions({ limit: opts.limit, since: opts.since }));
986
+ });
987
+ program.command("timeline").description("Get messages and tool calls for a session").argument("<session-id>", "The session ID to query").option("--limit <n>", "Max messages to return (default 50)", parseInt).option("--offset <n>", "Number of messages to skip", parseInt).option("--full", "Return full content instead of truncated").action(async (sessionId, opts) => {
988
+ const result = await sessionTimeline({
989
+ sessionId,
990
+ limit: opts.limit,
991
+ offset: opts.offset,
992
+ fullPayloads: opts.full
993
+ });
994
+ output(result);
995
+ });
996
+ program.command("costs").description("Token usage and cost breakdowns").option(
997
+ "--since <duration>",
998
+ 'Time filter: ISO date or relative like "24h", "7d"'
999
+ ).option("--group-by <key>", "Group by: session, model, or day").action(async (opts) => {
1000
+ output(await costBreakdown({ since: opts.since, groupBy: opts.groupBy }));
1001
+ });
1002
+ program.command("summary").description("Activity summary \u2014 sessions, prompts, tools, files, costs").option(
1003
+ "--since <duration>",
1004
+ 'Time window (default "24h"). ISO date or relative like "24h", "7d"'
1005
+ ).action(async (opts) => {
1006
+ output(await activitySummary({ since: opts.since }));
1007
+ });
1008
+ program.command("plans").description("List plans created by Claude Code (from ExitPlanMode events)").option("--session <id>", "Filter to a specific session").option(
1009
+ "--since <duration>",
1010
+ 'Time filter: ISO date or relative like "24h", "7d"'
1011
+ ).option("--limit <n>", "Max plans to return (default 20)", parseInt).action(async (opts) => {
1012
+ output(
1013
+ await listPlans({
1014
+ session_id: opts.session,
1015
+ since: opts.since,
1016
+ limit: opts.limit
1017
+ })
1018
+ );
1019
+ });
1020
+ program.command("search").description("Full-text search across events and messages").argument("<query>", "Text to search for").option("--types <types...>", "Filter to specific event types").option(
1021
+ "--since <duration>",
1022
+ 'Time filter: ISO date or relative like "24h", "7d"'
1023
+ ).option("--limit <n>", "Max results (default 20)", parseInt).option("--offset <n>", "Number of results to skip", parseInt).option("--full", "Return full payloads instead of truncated").action(async (query, opts) => {
1024
+ const result = await search({
1025
+ query,
1026
+ eventTypes: opts.types,
1027
+ since: opts.since,
1028
+ limit: opts.limit,
1029
+ offset: opts.offset,
1030
+ fullPayloads: opts.full
1031
+ });
1032
+ output(result);
1033
+ });
1034
+ program.command("print").alias("event").description("Get full details for a record by source and ID").argument("<source>", "Source: hook, otel, or message").argument("<id>", "Record ID from search/timeline results").action(async (source, id) => {
1035
+ if (source !== "hook" && source !== "otel" && source !== "message") {
1036
+ console.error(
1037
+ `Invalid source: ${source} (must be "hook", "otel", or "message")`
1038
+ );
1039
+ process.exit(1);
1040
+ }
1041
+ const result = await print({ source, id: parseInt(id, 10) });
1042
+ if (!result) {
1043
+ console.error(`No ${source} record found with id ${id}`);
1044
+ process.exit(1);
1045
+ }
1046
+ output(result);
1047
+ });
1048
+ program.command("query").description("Execute a read-only SQL query against the database").argument("<sql>", "SQL query (SELECT/WITH/PRAGMA only)").action(async (sql) => {
1049
+ try {
1050
+ output(await rawQuery(sql));
1051
+ } catch (err) {
1052
+ console.error(`Error: ${err.message}`);
1053
+ process.exit(1);
1054
+ }
1055
+ });
1056
+ program.command("db-stats").description("Show database row counts for each table").action(async () => {
1057
+ output(await dbStats());
1058
+ });
1059
+ program.command("refresh-pricing").description("Fetch latest model pricing from LiteLLM").action(async () => {
1060
+ console.log("Fetching pricing from LiteLLM...");
1061
+ const result = await refreshPricing();
1062
+ if (result && typeof result === "object" && "models" in result) {
1063
+ const models = result.models;
1064
+ console.log(`Cached pricing for ${Object.keys(models).length} models`);
1065
+ } else if (result && typeof result === "object" && "ok" in result) {
1066
+ console.log("Pricing refreshed.");
1067
+ } else {
1068
+ console.error("Failed to fetch pricing");
1069
+ process.exit(1);
1070
+ }
1071
+ });
1072
+ var permissions = program.command("permissions").description("Show or apply permission rules");
1073
+ permissions.command("show", { isDefault: true }).description("Show current approvals and allowed tools/commands").action(() => {
1074
+ output(permissionsShow());
1075
+ });
1076
+ permissions.command("apply").description("Apply permission rules (reads JSON payload from stdin)").action(async () => {
1077
+ const input = JSON.parse(await readStdin());
1078
+ output(permissionsApply(input));
1079
+ });
1080
+ program.parseAsync().catch((err) => {
1081
+ console.error("Error:", err.message);
1082
+ process.exit(1);
1083
+ });
1084
+ //# sourceMappingURL=cli.js.map