zozul-cli 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 (139) hide show
  1. package/.env.example +44 -0
  2. package/.github/workflows/publish.yml +26 -0
  3. package/DEVELOPMENT.md +288 -0
  4. package/LICENSE +201 -0
  5. package/README.md +178 -0
  6. package/dist/cli/commands.d.ts +3 -0
  7. package/dist/cli/commands.d.ts.map +1 -0
  8. package/dist/cli/commands.js +307 -0
  9. package/dist/cli/commands.js.map +1 -0
  10. package/dist/cli/format.d.ts +5 -0
  11. package/dist/cli/format.d.ts.map +1 -0
  12. package/dist/cli/format.js +115 -0
  13. package/dist/cli/format.js.map +1 -0
  14. package/dist/context/index.d.ts +8 -0
  15. package/dist/context/index.d.ts.map +1 -0
  16. package/dist/context/index.js +37 -0
  17. package/dist/context/index.js.map +1 -0
  18. package/dist/dashboard/html.d.ts +17 -0
  19. package/dist/dashboard/html.d.ts.map +1 -0
  20. package/dist/dashboard/html.js +79 -0
  21. package/dist/dashboard/html.js.map +1 -0
  22. package/dist/dashboard/index.html +1245 -0
  23. package/dist/hooks/config.d.ts +19 -0
  24. package/dist/hooks/config.d.ts.map +1 -0
  25. package/dist/hooks/config.js +106 -0
  26. package/dist/hooks/config.js.map +1 -0
  27. package/dist/hooks/git.d.ts +6 -0
  28. package/dist/hooks/git.d.ts.map +1 -0
  29. package/dist/hooks/git.js +73 -0
  30. package/dist/hooks/git.js.map +1 -0
  31. package/dist/hooks/index.d.ts +4 -0
  32. package/dist/hooks/index.d.ts.map +1 -0
  33. package/dist/hooks/index.js +3 -0
  34. package/dist/hooks/index.js.map +1 -0
  35. package/dist/hooks/server.d.ts +16 -0
  36. package/dist/hooks/server.d.ts.map +1 -0
  37. package/dist/hooks/server.js +349 -0
  38. package/dist/hooks/server.js.map +1 -0
  39. package/dist/index.d.ts +3 -0
  40. package/dist/index.d.ts.map +1 -0
  41. package/dist/index.js +6 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/otel/config.d.ts +36 -0
  44. package/dist/otel/config.d.ts.map +1 -0
  45. package/dist/otel/config.js +109 -0
  46. package/dist/otel/config.js.map +1 -0
  47. package/dist/otel/index.d.ts +4 -0
  48. package/dist/otel/index.d.ts.map +1 -0
  49. package/dist/otel/index.js +3 -0
  50. package/dist/otel/index.js.map +1 -0
  51. package/dist/otel/receiver.d.ts +10 -0
  52. package/dist/otel/receiver.d.ts.map +1 -0
  53. package/dist/otel/receiver.js +155 -0
  54. package/dist/otel/receiver.js.map +1 -0
  55. package/dist/parser/index.d.ts +4 -0
  56. package/dist/parser/index.d.ts.map +1 -0
  57. package/dist/parser/index.js +3 -0
  58. package/dist/parser/index.js.map +1 -0
  59. package/dist/parser/ingest.d.ts +20 -0
  60. package/dist/parser/ingest.d.ts.map +1 -0
  61. package/dist/parser/ingest.js +98 -0
  62. package/dist/parser/ingest.js.map +1 -0
  63. package/dist/parser/jsonl.d.ts +14 -0
  64. package/dist/parser/jsonl.d.ts.map +1 -0
  65. package/dist/parser/jsonl.js +202 -0
  66. package/dist/parser/jsonl.js.map +1 -0
  67. package/dist/parser/types.d.ts +81 -0
  68. package/dist/parser/types.d.ts.map +1 -0
  69. package/dist/parser/types.js +9 -0
  70. package/dist/parser/types.js.map +1 -0
  71. package/dist/parser/watcher.d.ts +16 -0
  72. package/dist/parser/watcher.d.ts.map +1 -0
  73. package/dist/parser/watcher.js +103 -0
  74. package/dist/parser/watcher.js.map +1 -0
  75. package/dist/pricing/index.d.ts +2 -0
  76. package/dist/pricing/index.d.ts.map +1 -0
  77. package/dist/pricing/index.js +37 -0
  78. package/dist/pricing/index.js.map +1 -0
  79. package/dist/service/index.d.ts +31 -0
  80. package/dist/service/index.d.ts.map +1 -0
  81. package/dist/service/index.js +252 -0
  82. package/dist/service/index.js.map +1 -0
  83. package/dist/storage/db.d.ts +75 -0
  84. package/dist/storage/db.d.ts.map +1 -0
  85. package/dist/storage/db.js +117 -0
  86. package/dist/storage/db.js.map +1 -0
  87. package/dist/storage/index.d.ts +4 -0
  88. package/dist/storage/index.d.ts.map +1 -0
  89. package/dist/storage/index.js +3 -0
  90. package/dist/storage/index.js.map +1 -0
  91. package/dist/storage/repo.d.ts +162 -0
  92. package/dist/storage/repo.d.ts.map +1 -0
  93. package/dist/storage/repo.js +472 -0
  94. package/dist/storage/repo.js.map +1 -0
  95. package/dist/sync/client.d.ts +24 -0
  96. package/dist/sync/client.d.ts.map +1 -0
  97. package/dist/sync/client.js +41 -0
  98. package/dist/sync/client.js.map +1 -0
  99. package/dist/sync/index.d.ts +18 -0
  100. package/dist/sync/index.d.ts.map +1 -0
  101. package/dist/sync/index.js +135 -0
  102. package/dist/sync/index.js.map +1 -0
  103. package/dist/sync/sync.test.d.ts +2 -0
  104. package/dist/sync/sync.test.d.ts.map +1 -0
  105. package/dist/sync/sync.test.js +412 -0
  106. package/dist/sync/sync.test.js.map +1 -0
  107. package/dist/sync/transform.d.ts +80 -0
  108. package/dist/sync/transform.d.ts.map +1 -0
  109. package/dist/sync/transform.js +90 -0
  110. package/dist/sync/transform.js.map +1 -0
  111. package/package.json +50 -0
  112. package/src/cli/commands.ts +332 -0
  113. package/src/cli/format.ts +133 -0
  114. package/src/context/index.ts +42 -0
  115. package/src/dashboard/html.ts +97 -0
  116. package/src/dashboard/index.html +1245 -0
  117. package/src/hooks/config.ts +119 -0
  118. package/src/hooks/git.ts +77 -0
  119. package/src/hooks/index.ts +7 -0
  120. package/src/hooks/server.ts +397 -0
  121. package/src/index.ts +6 -0
  122. package/src/otel/config.ts +141 -0
  123. package/src/otel/index.ts +8 -0
  124. package/src/otel/receiver.ts +183 -0
  125. package/src/parser/index.ts +3 -0
  126. package/src/parser/ingest.ts +119 -0
  127. package/src/parser/jsonl.ts +241 -0
  128. package/src/parser/types.ts +89 -0
  129. package/src/parser/watcher.ts +116 -0
  130. package/src/pricing/index.ts +51 -0
  131. package/src/service/index.ts +272 -0
  132. package/src/storage/db.ts +198 -0
  133. package/src/storage/index.ts +3 -0
  134. package/src/storage/repo.ts +601 -0
  135. package/src/sync/client.ts +63 -0
  136. package/src/sync/index.ts +207 -0
  137. package/src/sync/sync.test.ts +447 -0
  138. package/src/sync/transform.ts +184 -0
  139. package/tsconfig.json +19 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.d.ts","sourceRoot":"","sources":["../../src/sync/transform.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ // ── Safe JSON parse ──
2
+ function safeJsonParse(value) {
3
+ if (value == null)
4
+ return null;
5
+ try {
6
+ return JSON.parse(value);
7
+ }
8
+ catch {
9
+ return value;
10
+ }
11
+ }
12
+ // ── Transform functions ──
13
+ export function transformSession(row) {
14
+ return {
15
+ id: row.id,
16
+ project_path: row.project_path,
17
+ started_at: row.started_at,
18
+ ended_at: row.ended_at,
19
+ total_input_tokens: row.total_input_tokens,
20
+ total_output_tokens: row.total_output_tokens,
21
+ total_cache_read_tokens: row.total_cache_read_tokens,
22
+ total_cache_creation_tokens: row.total_cache_creation_tokens,
23
+ total_cost_usd: row.total_cost_usd,
24
+ total_turns: row.total_turns,
25
+ total_duration_ms: row.total_duration_ms,
26
+ model: row.model,
27
+ };
28
+ }
29
+ export function transformTurn(row) {
30
+ return {
31
+ turn_index: row.turn_index,
32
+ role: row.role,
33
+ timestamp: row.timestamp,
34
+ input_tokens: row.input_tokens,
35
+ output_tokens: row.output_tokens,
36
+ cache_read_tokens: row.cache_read_tokens,
37
+ cache_creation_tokens: row.cache_creation_tokens,
38
+ cost_usd: row.cost_usd,
39
+ duration_ms: row.duration_ms,
40
+ model: row.model,
41
+ content_text: row.content_text,
42
+ tool_calls: safeJsonParse(row.tool_calls),
43
+ is_real_user: row.is_real_user === 1,
44
+ };
45
+ }
46
+ export function transformToolUse(row, turnLookup) {
47
+ return {
48
+ turn_index: row.turn_id != null ? (turnLookup.get(row.turn_id) ?? null) : null,
49
+ tool_name: row.tool_name,
50
+ tool_input: safeJsonParse(row.tool_input),
51
+ tool_result: row.tool_result,
52
+ success: row.success == null ? null : row.success === 1,
53
+ duration_ms: row.duration_ms,
54
+ timestamp: row.timestamp,
55
+ };
56
+ }
57
+ export function transformHookEvent(row) {
58
+ return {
59
+ event_name: row.event_name,
60
+ timestamp: row.timestamp,
61
+ payload: safeJsonParse(row.payload) ?? row.payload,
62
+ };
63
+ }
64
+ export function transformTaskTag(row, turnLookup) {
65
+ return {
66
+ turn_index: turnLookup.get(row.turn_id) ?? null,
67
+ task: row.task,
68
+ tagged_at: row.tagged_at,
69
+ };
70
+ }
71
+ export function transformOtelMetric(row) {
72
+ return {
73
+ name: row.name,
74
+ value: row.value,
75
+ attributes: safeJsonParse(row.attributes),
76
+ session_id: row.session_id,
77
+ model: row.model,
78
+ timestamp: row.timestamp,
79
+ };
80
+ }
81
+ export function transformOtelEvent(row) {
82
+ return {
83
+ event_name: row.event_name,
84
+ attributes: safeJsonParse(row.attributes),
85
+ session_id: row.session_id,
86
+ prompt_id: row.prompt_id,
87
+ timestamp: row.timestamp,
88
+ };
89
+ }
90
+ //# sourceMappingURL=transform.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transform.js","sourceRoot":"","sources":["../../src/sync/transform.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "zozul-cli",
3
+ "version": "0.1.0",
4
+ "description": "Observability for Claude Code — track token usage, costs, turns, and conversation history",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "bin": {
8
+ "zozul": "dist/index.js"
9
+ },
10
+ "scripts": {
11
+ "build": "./node_modules/.bin/tsc && cp src/dashboard/index.html dist/dashboard/index.html",
12
+ "dev": "tsx src/index.ts",
13
+ "start": "node dist/index.js",
14
+ "test": "vitest run"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/tuesday-coder/zozul-cli.git"
19
+ },
20
+ "keywords": [
21
+ "claude-code",
22
+ "observability",
23
+ "telemetry",
24
+ "opentelemetry"
25
+ ],
26
+ "author": "",
27
+ "license": "Apache-2.0",
28
+ "bugs": {
29
+ "url": "https://github.com/tuesday-coder/zozul-cli/issues"
30
+ },
31
+ "homepage": "https://github.com/tuesday-coder/zozul-cli#readme",
32
+ "dependencies": {
33
+ "@opentelemetry/api": "^1.9.0",
34
+ "@opentelemetry/exporter-logs-otlp-grpc": "^0.213.0",
35
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.213.0",
36
+ "@opentelemetry/exporter-prometheus": "^0.213.0",
37
+ "@opentelemetry/otlp-grpc-exporter-base": "^0.213.0",
38
+ "@opentelemetry/resources": "^2.6.0",
39
+ "@opentelemetry/sdk-logs": "^0.213.0",
40
+ "@opentelemetry/sdk-metrics": "^2.6.0",
41
+ "@types/better-sqlite3": "^7.6.13",
42
+ "@types/node": "^25.5.0",
43
+ "better-sqlite3": "^12.8.0",
44
+ "commander": "^14.0.3",
45
+ "dotenv": "^17.3.1",
46
+ "tsx": "^4.21.0",
47
+ "typescript": "^6.0.2",
48
+ "vitest": "^3.2.4"
49
+ }
50
+ }
@@ -0,0 +1,332 @@
1
+ import { Command } from "commander";
2
+ import { getDb } from "../storage/db.js";
3
+ import { SessionRepo } from "../storage/repo.js";
4
+ import { createHookServer } from "../hooks/server.js";
5
+ import { installHooksToSettings, uninstallHooksFromSettings, generateHooksConfig } from "../hooks/config.js";
6
+ import { installOtelToSettings, uninstallOtelFromSettings, generateOtelShellExports } from "../otel/config.js";
7
+ import { ingestAllSessions } from "../parser/ingest.js";
8
+ import { watchSessionFiles } from "../parser/watcher.js";
9
+ import { installService, uninstallService, serviceStatus, restartService } from "../service/index.js";
10
+ import { getActiveContext, setActiveContext, clearActiveContext } from "../context/index.js";
11
+ import { installGitHook, uninstallGitHook } from "../hooks/git.js";
12
+ import { runSync } from "../sync/index.js";
13
+ import { ZozulApiClient } from "../sync/client.js";
14
+
15
+ function envPort(): string {
16
+ return process.env.ZOZUL_PORT ?? "7890";
17
+ }
18
+ function envOtelEndpoint(): string {
19
+ return process.env.OTEL_ENDPOINT ?? "http://localhost:7890";
20
+ }
21
+ function envOtelProtocol(): string {
22
+ return process.env.OTEL_PROTOCOL ?? "http/json";
23
+ }
24
+ function envVerbose(): boolean {
25
+ return process.env.ZOZUL_VERBOSE === "true" || process.env.ZOZUL_VERBOSE === "1";
26
+ }
27
+ function envDbPath(): string | undefined {
28
+ return process.env.ZOZUL_DB_PATH || undefined;
29
+ }
30
+
31
+ export function buildCli(): Command {
32
+ const program = new Command();
33
+
34
+ program
35
+ .name("zozul")
36
+ .description("Observability for Claude Code — track tokens, costs, turns, and conversations")
37
+ .version("0.1.0");
38
+
39
+ program
40
+ .command("serve")
41
+ .description("Start the hooks HTTP server to receive real-time events from Claude Code")
42
+ .option("-p, --port <port>", "Port to listen on", envPort())
43
+ .option("-v, --verbose", "Print events to stderr as they arrive")
44
+ .action(async (opts) => {
45
+ const port = parseInt(opts.port, 10);
46
+ const verbose = opts.verbose || envVerbose();
47
+ const db = getDb(envDbPath());
48
+ const repo = new SessionRepo(db);
49
+ const server = createHookServer({ port, repo, verbose });
50
+
51
+ server.on("error", (err: NodeJS.ErrnoException) => {
52
+ if (err.code === "EADDRINUSE") {
53
+ console.error(`Port ${port} is already in use. Is zozul already running?`);
54
+ console.error(" Check with: lsof -ti :" + port);
55
+ db.close();
56
+ process.exit(0); // clean exit so launchd/systemd won't respawn
57
+ }
58
+ throw err;
59
+ });
60
+
61
+ server.listen(port, async () => {
62
+ console.log(`zozul listening on http://localhost:${port}`);
63
+ console.log(` Dashboard: http://localhost:${port}/dashboard`);
64
+ console.log(` Hooks: http://localhost:${port}/hook/<event>`);
65
+ console.log(` OTLP receiver: http://localhost:${port}/v1/metrics & /v1/logs`);
66
+ console.log(` API: http://localhost:${port}/api/*`);
67
+ console.log("\nPress Ctrl+C to stop.\n");
68
+
69
+ const stopWatcher = await watchSessionFiles({ repo, verbose, catchUp: true });
70
+
71
+ process.on("SIGINT", () => {
72
+ stopWatcher();
73
+ server.close();
74
+ db.close();
75
+ process.exit(0);
76
+ });
77
+ });
78
+ });
79
+
80
+ program
81
+ .command("install")
82
+ .description("Install hooks, OTEL config, and optionally the background service")
83
+ .option("-p, --port <port>", "Hook server port", envPort())
84
+ .option("--otel-endpoint <endpoint>", "OTLP endpoint", envOtelEndpoint())
85
+ .option("--otel-protocol <protocol>", "OTLP protocol", envOtelProtocol())
86
+ .option("--no-otel", "Skip OTEL configuration")
87
+ .option("--no-hooks", "Skip hooks configuration")
88
+ .option("--service", "Also install zozul as a background service (starts on login)")
89
+ .option("--status", "Show background service status")
90
+ .option("--restart", "Restart the background service")
91
+ .option("--dry-run", "Print the config that would be installed without writing")
92
+ .action((opts) => {
93
+ if (opts.status) {
94
+ console.log(`Service status: ${serviceStatus()}`);
95
+ return;
96
+ }
97
+
98
+ if (opts.restart) {
99
+ try {
100
+ restartService();
101
+ console.log("Service restarted.");
102
+ console.log(` Status: ${serviceStatus()}`);
103
+ } catch (err) {
104
+ console.error(`Restart failed: ${err instanceof Error ? err.message : err}`);
105
+ process.exit(1);
106
+ }
107
+ return;
108
+ }
109
+
110
+ const port = parseInt(opts.port, 10);
111
+
112
+ if (opts.dryRun) {
113
+ console.log("── Hooks Config (for ~/.claude/settings.json) ──\n");
114
+ console.log(JSON.stringify(generateHooksConfig({ port }), null, 2));
115
+ console.log("\n── OTEL Environment Variables ──\n");
116
+ console.log(generateOtelShellExports({ endpoint: opts.otelEndpoint }));
117
+ return;
118
+ }
119
+
120
+ if (opts.hooks !== false) {
121
+ const result = installHooksToSettings({ port });
122
+ console.log(`Hooks installed to ${result.path}`);
123
+ if (result.merged) console.log(" (merged with existing hooks)");
124
+ }
125
+
126
+ if (opts.otel !== false) {
127
+ const result = installOtelToSettings({
128
+ endpoint: opts.otelEndpoint,
129
+ protocol: opts.otelProtocol,
130
+ });
131
+ console.log(`OTEL config installed to ${result.path}`);
132
+ }
133
+
134
+ if (opts.service) {
135
+ try {
136
+ const result = installService({ port, dbPath: envDbPath() });
137
+ console.log(`Service installed: ${result.servicePath}`);
138
+ console.log(" zozul is now running in the background and will start automatically on login.");
139
+ } catch (err) {
140
+ console.error(`Service install failed: ${err instanceof Error ? err.message : err}`);
141
+ console.error(" Run 'zozul serve' manually as a fallback.");
142
+ }
143
+ } else {
144
+ console.log("\nDone. Start the server with: zozul serve");
145
+ console.log("Or install as a background service with: zozul install --service");
146
+ }
147
+
148
+ const gitResult = installGitHook();
149
+ if (gitResult) {
150
+ if (gitResult.created) {
151
+ console.log(`Git post-commit hook installed: ${gitResult.path}`);
152
+ console.log(" (auto-clears task context on commit)");
153
+ } else {
154
+ console.log("Git post-commit hook already installed.");
155
+ }
156
+ }
157
+
158
+ console.log("Launch Claude Code normally with: claude");
159
+ });
160
+
161
+ program
162
+ .command("uninstall")
163
+ .description("Remove zozul hooks, OTEL config, and background service")
164
+ .option("-p, --port <port>", "Hook server port (to match installed hooks)", envPort())
165
+ .action((opts) => {
166
+ const port = parseInt(opts.port, 10);
167
+
168
+ const hooksRemoved = uninstallHooksFromSettings({ port });
169
+ const otelRemoved = uninstallOtelFromSettings();
170
+ const gitRemoved = uninstallGitHook();
171
+
172
+ if (hooksRemoved) console.log("Hooks removed from Claude Code settings.");
173
+ if (otelRemoved) console.log("OTEL config removed from Claude Code settings.");
174
+ if (gitRemoved) console.log("Git post-commit hook removed.");
175
+
176
+ const serviceResult = uninstallService();
177
+ if (serviceResult.removed) {
178
+ console.log("Background service stopped and removed.");
179
+ }
180
+
181
+ if (!hooksRemoved && !otelRemoved && !gitRemoved && !serviceResult.removed) {
182
+ console.log("Nothing to remove.");
183
+ }
184
+ });
185
+
186
+ program
187
+ .command("context [tags...]")
188
+ .description("Set, view, or clear the active task tags for tagging turns")
189
+ .option("--clear", "Clear the active task context")
190
+ .option("--list", "List all tasks that have been used")
191
+ .action((tags: string[], opts: { clear?: boolean; list?: boolean }) => {
192
+ if (opts.clear) {
193
+ clearActiveContext();
194
+ console.log("Task context cleared.");
195
+ return;
196
+ }
197
+
198
+ if (opts.list) {
199
+ const db = getDb(envDbPath());
200
+ const repo = new SessionRepo(db);
201
+ const tasks = repo.listTasks();
202
+ if (tasks.length === 0) {
203
+ console.log("No tasks found.");
204
+ } else {
205
+ for (const t of tasks) {
206
+ console.log(` ${t.task} (${t.turn_count} turns, last tagged: ${t.last_tagged})`);
207
+ }
208
+ }
209
+ db.close();
210
+ return;
211
+ }
212
+
213
+ if (tags.length > 0) {
214
+ const ctx = setActiveContext(tags);
215
+ console.log(`Active tags: ${ctx.active.join(", ")}`);
216
+ console.log(` Set at: ${ctx.set_at}`);
217
+ return;
218
+ }
219
+
220
+ // No arguments: show current context
221
+ const ctx = getActiveContext();
222
+ if (ctx?.active && ctx.active.length > 0) {
223
+ console.log(`Active tags: ${ctx.active.join(", ")}`);
224
+ console.log(` Set at: ${ctx.set_at}`);
225
+ } else {
226
+ console.log("No active task context.");
227
+ console.log('Set one with: zozul context "UI" "Feature"');
228
+ }
229
+ });
230
+
231
+ program
232
+ .command("sync")
233
+ .description("Sync local data to the remote zozul backend")
234
+ .option("--dry-run", "Show what would be synced without sending data")
235
+ .option("-v, --verbose", "Print detailed progress")
236
+ .action(async (opts) => {
237
+ const apiUrl = process.env.ZOZUL_API_URL;
238
+ const apiKey = process.env.ZOZUL_API_KEY;
239
+
240
+ if (!apiUrl || !apiKey) {
241
+ console.error("Missing required environment variables:");
242
+ if (!apiUrl) console.error(" ZOZUL_API_URL — base URL of the zozul backend");
243
+ if (!apiKey) console.error(" ZOZUL_API_KEY — API key for authentication");
244
+ console.error("\nSet them in .env or export them in your shell.");
245
+ process.exit(1);
246
+ }
247
+
248
+ const db = getDb(envDbPath());
249
+ const repo = new SessionRepo(db);
250
+ const client = new ZozulApiClient({ apiUrl, apiKey });
251
+
252
+ if (opts.dryRun) {
253
+ console.log(`Dry run — checking what would sync to ${apiUrl}...\n`);
254
+ } else {
255
+ console.log(`Syncing to ${apiUrl}...\n`);
256
+ }
257
+
258
+ const result = await runSync(repo, client, {
259
+ verbose: opts.verbose || envVerbose(),
260
+ dryRun: opts.dryRun,
261
+ });
262
+
263
+ console.log("── Sync Summary ──");
264
+ const label = opts.dryRun ? "pending" : "synced";
265
+ for (const [table, counts] of Object.entries(result)) {
266
+ const status = counts.failed > 0 ? "PARTIAL" : "OK";
267
+ console.log(` ${table.padEnd(15)} ${counts.synced} ${label}, ${counts.failed} failed [${status}]`);
268
+ }
269
+
270
+ const totalFailed = Object.values(result).reduce((s, c) => s + c.failed, 0);
271
+ if (totalFailed > 0) {
272
+ console.error(`\n${totalFailed} items failed to sync. Re-run 'zozul sync' to retry.`);
273
+ db.close();
274
+ process.exit(1);
275
+ }
276
+
277
+ db.close();
278
+ });
279
+
280
+ // ── Hidden maintenance commands ──
281
+
282
+ program
283
+ .command("ingest", { hidden: true })
284
+ .description("Re-ingest all Claude Code session files (backfill)")
285
+ .option("-f, --force", "Re-ingest sessions that already exist in the database")
286
+ .option("--no-tag", "Skip tagging turns with the active task context")
287
+ .action(async (opts) => {
288
+ const db = getDb(envDbPath());
289
+ const repo = new SessionRepo(db);
290
+
291
+ console.log("Scanning for Claude Code session files...");
292
+ const result = await ingestAllSessions(repo, { force: opts.force, noTag: opts.tag === false });
293
+ console.log(`Ingested: ${result.ingested} Skipped: ${result.skipped}`);
294
+ db.close();
295
+ });
296
+
297
+ program
298
+ .command("db-clean", { hidden: true })
299
+ .description("Remove invalid/test rows from the database")
300
+ .option("--session <id>", "Remove all data for a specific session ID")
301
+ .action((opts) => {
302
+ const db = getDb(envDbPath());
303
+
304
+ if (opts.session) {
305
+ const id: string = opts.session;
306
+ db.transaction(() => {
307
+ db.prepare(`DELETE FROM task_tags WHERE turn_id IN (SELECT id FROM turns WHERE session_id = ?)`).run(id);
308
+ for (const table of ["otel_metrics", "otel_events", "hook_events", "tool_uses", "turns"] as const) {
309
+ db.prepare(`DELETE FROM ${table} WHERE session_id = ?`).run(id);
310
+ }
311
+ db.prepare(`DELETE FROM sessions WHERE id = ?`).run(id);
312
+ })();
313
+ console.log(`Removed all data for session: ${id}`);
314
+ } else {
315
+ const minDate = "2025-01-01";
316
+ const result = db.prepare(`
317
+ SELECT COUNT(*) as n FROM otel_metrics WHERE timestamp < ?
318
+ `).get(minDate) as { n: number };
319
+ if (result.n === 0) {
320
+ console.log("Nothing to clean.");
321
+ } else {
322
+ db.prepare(`DELETE FROM otel_metrics WHERE timestamp < ?`).run(minDate);
323
+ db.prepare(`DELETE FROM otel_events WHERE timestamp < ?`).run(minDate);
324
+ console.log(`Removed ${result.n} row(s) with timestamps before ${minDate}.`);
325
+ }
326
+ }
327
+
328
+ db.close();
329
+ });
330
+
331
+ return program;
332
+ }
@@ -0,0 +1,133 @@
1
+ import type { SessionRow, TurnRow } from "../storage/db.js";
2
+
3
+ export function formatSessionList(sessions: SessionRow[]): string {
4
+ if (sessions.length === 0) return "No sessions found.";
5
+
6
+ const lines: string[] = [];
7
+ lines.push(
8
+ pad("SESSION ID", 40) +
9
+ pad("STARTED", 22) +
10
+ pad("TURNS", 7) +
11
+ pad("TOKENS (in/out)", 20) +
12
+ pad("COST", 10) +
13
+ "MODEL",
14
+ );
15
+ lines.push("─".repeat(120));
16
+
17
+ for (const s of sessions) {
18
+ const started = formatTimestamp(s.started_at);
19
+ const tokens = `${fmtNum(s.total_input_tokens)}/${fmtNum(s.total_output_tokens)}`;
20
+ const cost = `$${s.total_cost_usd.toFixed(4)}`;
21
+ const id = s.id.length > 36 ? s.id.slice(0, 36) + "…" : s.id;
22
+
23
+ lines.push(
24
+ pad(id, 40) +
25
+ pad(started, 22) +
26
+ pad(String(s.total_turns), 7) +
27
+ pad(tokens, 20) +
28
+ pad(cost, 10) +
29
+ (s.model ?? "—"),
30
+ );
31
+ }
32
+
33
+ return lines.join("\n");
34
+ }
35
+
36
+ export function formatSessionDetail(session: SessionRow, turns: TurnRow[]): string {
37
+ const lines: string[] = [];
38
+
39
+ lines.push(`Session: ${session.id}`);
40
+ lines.push(`Project: ${session.project_path ?? "—"}`);
41
+ lines.push(`Started: ${formatTimestamp(session.started_at)}`);
42
+ lines.push(`Ended: ${session.ended_at ? formatTimestamp(session.ended_at) : "—"}`);
43
+ lines.push(`Model: ${session.model ?? "—"}`);
44
+ lines.push("");
45
+ lines.push("── Token Usage ──");
46
+ lines.push(` Input tokens: ${fmtNum(session.total_input_tokens)}`);
47
+ lines.push(` Output tokens: ${fmtNum(session.total_output_tokens)}`);
48
+ lines.push(` Cache read tokens: ${fmtNum(session.total_cache_read_tokens)}`);
49
+ lines.push(` Cache creation tokens: ${fmtNum(session.total_cache_creation_tokens)}`);
50
+ lines.push(` Total cost: $${session.total_cost_usd.toFixed(4)}`);
51
+ lines.push(` Total turns: ${session.total_turns}`);
52
+ lines.push(` Total duration: ${formatDuration(session.total_duration_ms)}`);
53
+
54
+ if (turns.length > 0) {
55
+ lines.push("");
56
+ lines.push("── Conversation ──");
57
+
58
+ for (const turn of turns) {
59
+ const role = turn.role === "assistant" ? "🤖 Assistant" : "👤 User";
60
+ lines.push("");
61
+ lines.push(`[${turn.turn_index}] ${role} (${formatTimestamp(turn.timestamp)})`);
62
+
63
+ if (turn.input_tokens > 0 || turn.output_tokens > 0) {
64
+ lines.push(
65
+ ` tokens: ${fmtNum(turn.input_tokens)} in / ${fmtNum(turn.output_tokens)} out` +
66
+ (turn.cost_usd > 0 ? ` | cost: $${turn.cost_usd.toFixed(4)}` : "") +
67
+ (turn.model ? ` | model: ${turn.model}` : ""),
68
+ );
69
+ }
70
+
71
+ if (turn.content_text) {
72
+ const preview = turn.content_text.length > 500
73
+ ? turn.content_text.slice(0, 500) + "…"
74
+ : turn.content_text;
75
+ lines.push(` ${preview.replace(/\n/g, "\n ")}`);
76
+ }
77
+
78
+ if (turn.tool_calls) {
79
+ try {
80
+ const calls = JSON.parse(turn.tool_calls);
81
+ for (const call of calls) {
82
+ lines.push(` 🔧 ${call.toolName}`);
83
+ }
84
+ } catch {
85
+ // skip
86
+ }
87
+ }
88
+ }
89
+ }
90
+
91
+ return lines.join("\n");
92
+ }
93
+
94
+ export function formatStats(stats: Record<string, unknown>): string {
95
+ const lines: string[] = [];
96
+ lines.push("── Aggregate Statistics ──");
97
+ lines.push(` Total sessions: ${stats.total_sessions ?? 0}`);
98
+ lines.push(` Total input tokens: ${fmtNum(Number(stats.total_input_tokens ?? 0))}`);
99
+ lines.push(` Total output tokens: ${fmtNum(Number(stats.total_output_tokens ?? 0))}`);
100
+ lines.push(` Total cache read: ${fmtNum(Number(stats.total_cache_read_tokens ?? 0))}`);
101
+ lines.push(` Total cost: $${Number(stats.total_cost_usd ?? 0).toFixed(4)}`);
102
+ lines.push(` Total turns: ${stats.total_turns ?? 0}`);
103
+ lines.push(` Total active time: ${formatDuration(Number(stats.total_duration_ms ?? 0))}`);
104
+ return lines.join("\n");
105
+ }
106
+
107
+ function pad(str: string, len: number): string {
108
+ return str.padEnd(len);
109
+ }
110
+
111
+ function fmtNum(n: number): string {
112
+ return n.toLocaleString("en-US");
113
+ }
114
+
115
+ function formatTimestamp(ts: string): string {
116
+ try {
117
+ return new Date(ts).toLocaleString();
118
+ } catch {
119
+ return ts;
120
+ }
121
+ }
122
+
123
+ function formatDuration(ms: number): string {
124
+ if (ms < 1000) return `${ms}ms`;
125
+ const seconds = Math.floor(ms / 1000);
126
+ if (seconds < 60) return `${seconds}s`;
127
+ const minutes = Math.floor(seconds / 60);
128
+ const remainingSeconds = seconds % 60;
129
+ if (minutes < 60) return `${minutes}m ${remainingSeconds}s`;
130
+ const hours = Math.floor(minutes / 60);
131
+ const remainingMinutes = minutes % 60;
132
+ return `${hours}h ${remainingMinutes}m`;
133
+ }
@@ -0,0 +1,42 @@
1
+ import path from "node:path";
2
+ import os from "node:os";
3
+ import fs from "node:fs";
4
+
5
+ const CONTEXT_PATH = path.join(os.homedir(), ".zozul", "context.json");
6
+
7
+ export interface TaskContext {
8
+ active: string[];
9
+ set_at: string;
10
+ }
11
+
12
+ export function getActiveContext(): TaskContext | null {
13
+ if (!fs.existsSync(CONTEXT_PATH)) return null;
14
+ try {
15
+ const data = JSON.parse(fs.readFileSync(CONTEXT_PATH, "utf-8"));
16
+ // Support legacy single-string format
17
+ if (typeof data.active === "string") {
18
+ return { active: [data.active], set_at: data.set_at ?? "" };
19
+ }
20
+ if (Array.isArray(data.active) && data.active.length > 0) {
21
+ return { active: data.active, set_at: data.set_at ?? "" };
22
+ }
23
+ return null;
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+
29
+ export function setActiveContext(tasks: string[]): TaskContext {
30
+ const tags = tasks.map(t => t.trim()).filter(Boolean);
31
+ if (tags.length === 0) throw new Error("At least one tag is required");
32
+ const ctx: TaskContext = { active: tags, set_at: new Date().toISOString() };
33
+ fs.mkdirSync(path.dirname(CONTEXT_PATH), { recursive: true });
34
+ fs.writeFileSync(CONTEXT_PATH, JSON.stringify(ctx, null, 2) + "\n");
35
+ return ctx;
36
+ }
37
+
38
+ export function clearActiveContext(): void {
39
+ if (fs.existsSync(CONTEXT_PATH)) {
40
+ fs.unlinkSync(CONTEXT_PATH);
41
+ }
42
+ }