@basou/core 0.4.0 → 0.6.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/dist/index.js CHANGED
@@ -21,27 +21,6 @@ function summarizeAdapterOutput(_stream, _raw) {
21
21
  throw new Error("adapter_output summary is not implemented in this release");
22
22
  }
23
23
 
24
- // src/approval/approval-store.ts
25
- import { readdir } from "fs/promises";
26
- import { join } from "path";
27
-
28
- // src/lib/error-codes.ts
29
- function findErrorCode(error, code, depth = 4) {
30
- let cur = error;
31
- for (let i = 0; i < depth && cur instanceof Error; i++) {
32
- const c = cur.code;
33
- if (typeof c === "string" && c === code) return true;
34
- cur = cur.cause;
35
- }
36
- return false;
37
- }
38
-
39
- // src/schemas/approval.schema.ts
40
- import { z as z2 } from "zod";
41
-
42
- // src/schemas/shared.schema.ts
43
- import { z } from "zod";
44
-
45
24
  // src/ids/ulid.ts
46
25
  import { isValid as isValidUlid, monotonicFactory } from "ulid";
47
26
  var ID_PREFIXES = Object.freeze(["ws", "task", "ses", "evt", "appr", "decision"]);
@@ -67,12 +46,565 @@ function isValidPrefixedId(value) {
67
46
  return isValidUlid(ulidPart);
68
47
  }
69
48
 
49
+ // src/stats/active-time.ts
50
+ var ACTIVE_GAP_CAP_MS = 5 * 60 * 1e3;
51
+ var ENGAGED_TURNS_METHOD = "engaged-turns";
52
+ var TURN_INTERVALS_METHOD = "turn-intervals";
53
+ function activeTimeFromTimestamps(timestampsMs, capMs) {
54
+ const sorted = timestampsMs.filter((t) => Number.isFinite(t)).sort((a, b) => a - b);
55
+ const raw = [];
56
+ for (let i = 1; i < sorted.length; i++) {
57
+ const prev = sorted[i - 1];
58
+ const curr = sorted[i];
59
+ if (prev === void 0 || curr === void 0) continue;
60
+ const gap = curr - prev;
61
+ if (gap <= 0) continue;
62
+ raw.push([prev, prev + Math.min(gap, capMs)]);
63
+ }
64
+ const intervals = mergeIntervals(raw);
65
+ return { ms: sumDurations(intervals), intervals };
66
+ }
67
+ function mergeIntervals(intervals) {
68
+ const sorted = [...intervals].sort((a, b) => a[0] - b[0]);
69
+ const merged = [];
70
+ for (const [start, end] of sorted) {
71
+ const last = merged[merged.length - 1];
72
+ if (last !== void 0 && start <= last[1]) {
73
+ if (end > last[1]) last[1] = end;
74
+ } else {
75
+ merged.push([start, end]);
76
+ }
77
+ }
78
+ return merged;
79
+ }
80
+ function unionDurationMs(intervals) {
81
+ const merged = mergeIntervals(intervals);
82
+ return { ms: sumDurations(merged), merged };
83
+ }
84
+ function intervalsMsToIso(intervals) {
85
+ return intervals.map(([start, end]) => ({
86
+ start: new Date(start).toISOString(),
87
+ end: new Date(end).toISOString()
88
+ }));
89
+ }
90
+ function intervalsIsoToMs(intervals) {
91
+ const out = [];
92
+ for (const { start, end } of intervals) {
93
+ const s = Date.parse(start);
94
+ const e = Date.parse(end);
95
+ if (Number.isFinite(s) && Number.isFinite(e) && e >= s) out.push([s, e]);
96
+ }
97
+ return out;
98
+ }
99
+ function sumDurations(intervals) {
100
+ let total = 0;
101
+ for (const [start, end] of intervals) total += end - start;
102
+ return total;
103
+ }
104
+
105
+ // src/adapters/claude-code/transcript-importer.ts
106
+ var CLAUDE_IMPORT_SOURCE = "claude-code-import";
107
+ function claudeTranscriptToImportPayload(records, options) {
108
+ const placeholderSessionId = prefixedUlid("ses");
109
+ const askAnswers = indexAskAnswers(records);
110
+ const derived = [];
111
+ const relatedFiles = /* @__PURE__ */ new Set();
112
+ let minTs;
113
+ let maxTs;
114
+ let workingDir;
115
+ let claudeSessionId;
116
+ let outputTokens = 0;
117
+ let inputTokens = 0;
118
+ let cachedInputTokens = 0;
119
+ const seenMessageIds = /* @__PURE__ */ new Set();
120
+ const engagementTsMs = [];
121
+ const seenEngagementMessageIds = /* @__PURE__ */ new Set();
122
+ for (const record of records) {
123
+ const ts = readString(record.timestamp);
124
+ if (ts === void 0) continue;
125
+ if (minTs === void 0 || Date.parse(ts) < Date.parse(minTs)) minTs = ts;
126
+ if (maxTs === void 0 || Date.parse(ts) > Date.parse(maxTs)) maxTs = ts;
127
+ if (workingDir === void 0) workingDir = readString(record.cwd);
128
+ if (claudeSessionId === void 0) claudeSessionId = readString(record.sessionId);
129
+ if (record.isSidechain !== true) {
130
+ const tsMs = Date.parse(ts);
131
+ if (Number.isFinite(tsMs)) {
132
+ const recType = readString(record.type);
133
+ if (recType === "user") {
134
+ if (isHumanUserMessage(record)) engagementTsMs.push(tsMs);
135
+ } else if (recType === "assistant") {
136
+ const msg = isObject(record.message) ? record.message : void 0;
137
+ const mid = msg !== void 0 ? readString(msg.id) : void 0;
138
+ if (mid === void 0 || !seenEngagementMessageIds.has(mid)) {
139
+ if (mid !== void 0) seenEngagementMessageIds.add(mid);
140
+ engagementTsMs.push(tsMs);
141
+ }
142
+ }
143
+ }
144
+ }
145
+ if (readString(record.type) !== "assistant") continue;
146
+ const message = isObject(record.message) ? record.message : void 0;
147
+ const usage = message !== void 0 && isObject(message.usage) ? message.usage : void 0;
148
+ if (usage !== void 0) {
149
+ const messageId = message !== void 0 ? readString(message.id) : void 0;
150
+ const alreadyCounted = messageId !== void 0 && seenMessageIds.has(messageId);
151
+ if (!alreadyCounted) {
152
+ if (messageId !== void 0) seenMessageIds.add(messageId);
153
+ outputTokens += readNonNegInt(usage.output_tokens);
154
+ inputTokens += readNonNegInt(usage.input_tokens);
155
+ cachedInputTokens += readNonNegInt(usage.cache_read_input_tokens);
156
+ }
157
+ }
158
+ const cwd = readString(record.cwd) ?? workingDir ?? ".";
159
+ for (const item of toolUses(record)) {
160
+ const name = readString(item.name);
161
+ const input = isObject(item.input) ? item.input : void 0;
162
+ if (input === void 0) continue;
163
+ if (name === "Bash") {
164
+ const command = readString(input.command);
165
+ if (command !== void 0) {
166
+ derived.push(commandExecutedEvent(ts, placeholderSessionId, command, cwd));
167
+ }
168
+ continue;
169
+ }
170
+ if (name === "AskUserQuestion") {
171
+ const useId = readString(item.id);
172
+ const answers = useId !== void 0 ? askAnswers.get(useId) : void 0;
173
+ if (answers !== void 0) {
174
+ for (const [question, answer] of Object.entries(answers)) {
175
+ if (question.length === 0) continue;
176
+ const answerStr = typeof answer === "string" && answer.length > 0 ? answer : void 0;
177
+ const title = answerStr !== void 0 ? `${question} -> ${answerStr}` : question;
178
+ derived.push(decisionRecordedEvent(ts, placeholderSessionId, title));
179
+ }
180
+ }
181
+ continue;
182
+ }
183
+ if (name === "Edit" || name === "Write" || name === "NotebookEdit") {
184
+ const path2 = readString(input.file_path) ?? readString(input.notebook_path);
185
+ if (path2 !== void 0) {
186
+ const changeType = name === "Write" ? "added" : "modified";
187
+ relatedFiles.add(path2);
188
+ derived.push(fileChangedEvent(ts, placeholderSessionId, path2, changeType));
189
+ }
190
+ }
191
+ }
192
+ }
193
+ if (minTs === void 0 || maxTs === void 0) return null;
194
+ if (derived.length === 0) return null;
195
+ derived.sort((a, b) => Date.parse(a.occurred_at) - Date.parse(b.occurred_at));
196
+ const events = [
197
+ sessionStartedEvent(minTs, placeholderSessionId),
198
+ ...derived,
199
+ sessionEndedEvent(maxTs, placeholderSessionId)
200
+ ];
201
+ const externalId = options.externalId ?? claudeSessionId;
202
+ const commandCount = derived.reduce((n, e) => e.type === "command_executed" ? n + 1 : n, 0);
203
+ const fileCount = relatedFiles.size;
204
+ const date = minTs.slice(0, 10);
205
+ const label = `claude-code ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}, ${fileCount} ${fileCount === 1 ? "file" : "files"}`;
206
+ const active = engagementTsMs.length >= 2 ? activeTimeFromTimestamps(engagementTsMs, ACTIVE_GAP_CAP_MS) : void 0;
207
+ const metricsFields = {
208
+ ...outputTokens > 0 ? { output_tokens: outputTokens } : {},
209
+ ...inputTokens > 0 ? { input_tokens: inputTokens } : {},
210
+ ...cachedInputTokens > 0 ? { cached_input_tokens: cachedInputTokens } : {},
211
+ ...active !== void 0 && active.ms > 0 ? {
212
+ active_time_ms: active.ms,
213
+ active_intervals: intervalsMsToIso(active.intervals),
214
+ active_gap_cap_ms: ACTIVE_GAP_CAP_MS,
215
+ active_time_method: ENGAGED_TURNS_METHOD
216
+ } : {}
217
+ };
218
+ const metrics = Object.keys(metricsFields).length > 0 ? metricsFields : void 0;
219
+ const payload = {
220
+ schema_version: "0.1.0",
221
+ session: {
222
+ label,
223
+ workspace_id: options.workspaceId,
224
+ source: {
225
+ kind: CLAUDE_IMPORT_SOURCE,
226
+ version: "0.1.0",
227
+ ...externalId !== void 0 ? { external_id: externalId } : {}
228
+ },
229
+ started_at: minTs,
230
+ ended_at: maxTs,
231
+ // Validated against the canonical enum here; importSessionFromJson
232
+ // overwrites it with the literal "imported" regardless.
233
+ status: "imported",
234
+ working_directory: workingDir ?? ".",
235
+ invocation: { command: "claude", args: [], exit_code: null },
236
+ related_files: [...relatedFiles].sort(),
237
+ summary: null,
238
+ ...metrics !== void 0 ? { metrics } : {}
239
+ },
240
+ events
241
+ };
242
+ return payload;
243
+ }
244
+ function baseEvent(occurredAt, sessionId) {
245
+ return {
246
+ schema_version: "0.1.0",
247
+ id: prefixedUlid("evt"),
248
+ session_id: sessionId,
249
+ occurred_at: occurredAt,
250
+ source: CLAUDE_IMPORT_SOURCE
251
+ };
252
+ }
253
+ function sessionStartedEvent(occurredAt, sessionId) {
254
+ return { ...baseEvent(occurredAt, sessionId), type: "session_started" };
255
+ }
256
+ function sessionEndedEvent(occurredAt, sessionId) {
257
+ return { ...baseEvent(occurredAt, sessionId), type: "session_ended" };
258
+ }
259
+ function commandExecutedEvent(occurredAt, sessionId, command, cwd) {
260
+ return {
261
+ ...baseEvent(occurredAt, sessionId),
262
+ type: "command_executed",
263
+ command: "bash",
264
+ args: ["-c", command],
265
+ cwd,
266
+ exit_code: null,
267
+ duration_ms: 0
268
+ };
269
+ }
270
+ function fileChangedEvent(occurredAt, sessionId, path2, changeType) {
271
+ return {
272
+ ...baseEvent(occurredAt, sessionId),
273
+ type: "file_changed",
274
+ path: path2,
275
+ change_type: changeType
276
+ };
277
+ }
278
+ function decisionRecordedEvent(occurredAt, sessionId, title) {
279
+ return {
280
+ ...baseEvent(occurredAt, sessionId),
281
+ type: "decision_recorded",
282
+ decision_id: prefixedUlid("decision"),
283
+ title
284
+ };
285
+ }
286
+ function readString(value) {
287
+ return typeof value === "string" && value.length > 0 ? value : void 0;
288
+ }
289
+ function readNonNegInt(value) {
290
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
291
+ }
292
+ function isObject(value) {
293
+ return typeof value === "object" && value !== null && !Array.isArray(value);
294
+ }
295
+ function isHumanUserMessage(record) {
296
+ const message = isObject(record.message) ? record.message : void 0;
297
+ if (message === void 0) return false;
298
+ const content = message.content;
299
+ if (typeof content === "string") return content.length > 0;
300
+ if (Array.isArray(content)) {
301
+ return content.some((block) => {
302
+ if (!isObject(block)) return false;
303
+ const type = readString(block.type);
304
+ return type !== void 0 && type !== "tool_result";
305
+ });
306
+ }
307
+ return false;
308
+ }
309
+ function toolUses(record) {
310
+ const message = isObject(record.message) ? record.message : void 0;
311
+ const content = message !== void 0 && Array.isArray(message.content) ? message.content : [];
312
+ const result = [];
313
+ for (const item of content) {
314
+ if (isObject(item) && readString(item.type) === "tool_use") {
315
+ result.push(item);
316
+ }
317
+ }
318
+ return result;
319
+ }
320
+ function indexAskAnswers(records) {
321
+ const byId = /* @__PURE__ */ new Map();
322
+ for (const record of records) {
323
+ const result = record.toolUseResult;
324
+ if (!isObject(result)) continue;
325
+ const answers = result.answers;
326
+ if (!isObject(answers)) continue;
327
+ const message = isObject(record.message) ? record.message : void 0;
328
+ const content = message !== void 0 && Array.isArray(message.content) ? message.content : [];
329
+ for (const item of content) {
330
+ if (isObject(item) && readString(item.type) === "tool_result") {
331
+ const id = readString(item.tool_use_id);
332
+ if (id !== void 0) byId.set(id, answers);
333
+ }
334
+ }
335
+ }
336
+ return byId;
337
+ }
338
+
339
+ // src/adapters/codex/rollout-importer.ts
340
+ var CODEX_IMPORT_SOURCE = "codex-import";
341
+ function codexRolloutToImportPayload(records, options) {
342
+ const placeholderSessionId = prefixedUlid("ses");
343
+ const outputsByCallId = indexOutputs(records);
344
+ const turnStartMsByTurnId = indexTaskStarts(records);
345
+ const derived = [];
346
+ let minTs;
347
+ let maxTs;
348
+ let workingDir;
349
+ let codexSessionId;
350
+ let lastTokenTotals;
351
+ const engagementTsMs = [];
352
+ const completions = [];
353
+ const completedTurnIds = /* @__PURE__ */ new Set();
354
+ for (const record of records) {
355
+ const ts = readString2(record.timestamp);
356
+ if (ts === void 0) continue;
357
+ if (minTs === void 0 || Date.parse(ts) < Date.parse(minTs)) minTs = ts;
358
+ if (maxTs === void 0 || Date.parse(ts) > Date.parse(maxTs)) maxTs = ts;
359
+ const payload2 = isObject2(record.payload) ? record.payload : void 0;
360
+ if (payload2 === void 0) continue;
361
+ if (readString2(record.type) === "session_meta") {
362
+ if (workingDir === void 0) workingDir = readString2(payload2.cwd);
363
+ if (codexSessionId === void 0) codexSessionId = readString2(payload2.id);
364
+ continue;
365
+ }
366
+ if (readString2(record.type) === "event_msg" && readString2(payload2.type) === "token_count") {
367
+ const info = isObject2(payload2.info) ? payload2.info : void 0;
368
+ const totals = info !== void 0 && isObject2(info.total_token_usage) ? info.total_token_usage : void 0;
369
+ if (totals !== void 0) lastTokenTotals = totals;
370
+ continue;
371
+ }
372
+ if (readString2(record.type) === "event_msg") {
373
+ const pt = readString2(payload2.type);
374
+ if (pt === "user_message" || pt === "agent_message" || pt === "task_started" || pt === "task_complete") {
375
+ const tsMs = Date.parse(ts);
376
+ if (Number.isFinite(tsMs)) engagementTsMs.push(tsMs);
377
+ }
378
+ if (pt === "task_complete") {
379
+ const turnId = readString2(payload2.turn_id);
380
+ if (turnId === void 0 || !completedTurnIds.has(turnId)) {
381
+ if (turnId !== void 0) completedTurnIds.add(turnId);
382
+ completions.push({
383
+ interval: turnIntervalFromComplete(ts, payload2, turnStartMsByTurnId),
384
+ durationMs: readNonNegInt2(payload2.duration_ms)
385
+ });
386
+ }
387
+ }
388
+ continue;
389
+ }
390
+ if (readString2(record.type) !== "response_item") continue;
391
+ if (readString2(payload2.type) !== "function_call") continue;
392
+ if (readString2(payload2.name) !== "exec_command") continue;
393
+ const command = readExecCommand(payload2.arguments);
394
+ if (command === void 0) continue;
395
+ const cwd = command.workdir ?? workingDir ?? ".";
396
+ const output = readCallId(payload2.call_id, outputsByCallId);
397
+ const execTsMs = Date.parse(ts);
398
+ if (Number.isFinite(execTsMs)) engagementTsMs.push(execTsMs);
399
+ derived.push(
400
+ commandExecutedEvent2(ts, placeholderSessionId, command.cmd, cwd, {
401
+ exitCode: parseExitCode(output),
402
+ durationMs: parseWallTimeMs(output)
403
+ })
404
+ );
405
+ }
406
+ if (minTs === void 0 || maxTs === void 0) return null;
407
+ if (derived.length === 0) return null;
408
+ derived.sort((a, b) => Date.parse(a.occurred_at) - Date.parse(b.occurred_at));
409
+ const events = [
410
+ sessionStartedEvent2(minTs, placeholderSessionId),
411
+ ...derived,
412
+ sessionEndedEvent2(maxTs, placeholderSessionId)
413
+ ];
414
+ const externalId = options.externalId ?? codexSessionId;
415
+ const commandCount = derived.length;
416
+ const date = minTs.slice(0, 10);
417
+ const label = `codex ${date}: ${commandCount} ${commandCount === 1 ? "command" : "commands"}`;
418
+ const minTsMs = Date.parse(minTs);
419
+ const turnIntervals = [];
420
+ let machineActiveMs = 0;
421
+ let allCompletedTurnsHaveDuration = true;
422
+ for (const { interval, durationMs } of completions) {
423
+ if (durationMs <= 0) allCompletedTurnsHaveDuration = false;
424
+ if (interval === void 0) continue;
425
+ const start = Number.isFinite(minTsMs) ? Math.max(interval[0], minTsMs) : interval[0];
426
+ const end = interval[1];
427
+ if (!(start < end)) continue;
428
+ turnIntervals.push([start, end]);
429
+ machineActiveMs += Math.min(durationMs, end - start);
430
+ }
431
+ const pointResult = activeTimeFromTimestamps(engagementTsMs, ACTIVE_GAP_CAP_MS);
432
+ const active = turnIntervals.length > 0 || pointResult.intervals.length > 0 ? unionDurationMs([...turnIntervals, ...pointResult.intervals]) : void 0;
433
+ const activeMethod = turnIntervals.length > 0 ? TURN_INTERVALS_METHOD : ENGAGED_TURNS_METHOD;
434
+ const machineActive = allCompletedTurnsHaveDuration ? machineActiveMs : 0;
435
+ const tokenFields = lastTokenTotals === void 0 ? {} : {
436
+ ...readNonNegInt2(lastTokenTotals.output_tokens) > 0 ? { output_tokens: readNonNegInt2(lastTokenTotals.output_tokens) } : {},
437
+ ...readNonNegInt2(lastTokenTotals.input_tokens) > 0 ? { input_tokens: readNonNegInt2(lastTokenTotals.input_tokens) } : {},
438
+ ...readNonNegInt2(lastTokenTotals.cached_input_tokens) > 0 ? { cached_input_tokens: readNonNegInt2(lastTokenTotals.cached_input_tokens) } : {},
439
+ ...readNonNegInt2(lastTokenTotals.reasoning_output_tokens) > 0 ? { reasoning_output_tokens: readNonNegInt2(lastTokenTotals.reasoning_output_tokens) } : {}
440
+ };
441
+ const metricsFields = {
442
+ ...tokenFields,
443
+ ...active !== void 0 && active.ms > 0 ? {
444
+ active_time_ms: active.ms,
445
+ active_intervals: intervalsMsToIso(active.merged),
446
+ active_gap_cap_ms: ACTIVE_GAP_CAP_MS,
447
+ active_time_method: activeMethod
448
+ } : {},
449
+ ...machineActive > 0 ? { machine_active_time_ms: machineActive } : {}
450
+ };
451
+ const metrics = Object.keys(metricsFields).length > 0 ? metricsFields : void 0;
452
+ const payload = {
453
+ schema_version: "0.1.0",
454
+ session: {
455
+ label,
456
+ workspace_id: options.workspaceId,
457
+ source: {
458
+ kind: CODEX_IMPORT_SOURCE,
459
+ version: "0.1.0",
460
+ ...externalId !== void 0 ? { external_id: externalId } : {}
461
+ },
462
+ started_at: minTs,
463
+ ended_at: maxTs,
464
+ // Validated against the canonical enum here; importSessionFromJson
465
+ // overwrites it with the literal "imported" regardless.
466
+ status: "imported",
467
+ working_directory: workingDir ?? ".",
468
+ invocation: { command: "codex", args: [], exit_code: null },
469
+ related_files: [],
470
+ summary: null,
471
+ ...metrics !== void 0 ? { metrics } : {}
472
+ },
473
+ events
474
+ };
475
+ return payload;
476
+ }
477
+ function baseEvent2(occurredAt, sessionId) {
478
+ return {
479
+ schema_version: "0.1.0",
480
+ id: prefixedUlid("evt"),
481
+ session_id: sessionId,
482
+ occurred_at: occurredAt,
483
+ source: CODEX_IMPORT_SOURCE
484
+ };
485
+ }
486
+ function sessionStartedEvent2(occurredAt, sessionId) {
487
+ return { ...baseEvent2(occurredAt, sessionId), type: "session_started" };
488
+ }
489
+ function sessionEndedEvent2(occurredAt, sessionId) {
490
+ return { ...baseEvent2(occurredAt, sessionId), type: "session_ended" };
491
+ }
492
+ function commandExecutedEvent2(occurredAt, sessionId, command, cwd, outcome) {
493
+ return {
494
+ ...baseEvent2(occurredAt, sessionId),
495
+ type: "command_executed",
496
+ command: "bash",
497
+ args: ["-c", command],
498
+ cwd,
499
+ exit_code: outcome.exitCode,
500
+ duration_ms: outcome.durationMs
501
+ };
502
+ }
503
+ function readString2(value) {
504
+ return typeof value === "string" && value.length > 0 ? value : void 0;
505
+ }
506
+ function readNonNegInt2(value) {
507
+ return typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : 0;
508
+ }
509
+ function isObject2(value) {
510
+ return typeof value === "object" && value !== null && !Array.isArray(value);
511
+ }
512
+ function readExecCommand(value) {
513
+ const raw = readString2(value);
514
+ if (raw === void 0) return void 0;
515
+ let parsed;
516
+ try {
517
+ parsed = JSON.parse(raw);
518
+ } catch {
519
+ return void 0;
520
+ }
521
+ if (!isObject2(parsed)) return void 0;
522
+ const cmd = readString2(parsed.cmd);
523
+ if (cmd === void 0) return void 0;
524
+ return { cmd, workdir: readString2(parsed.workdir) };
525
+ }
526
+ function readCallId(value, outputs) {
527
+ const callId = readString2(value);
528
+ return callId !== void 0 ? outputs.get(callId) : void 0;
529
+ }
530
+ function turnIntervalFromComplete(endTs, payload, startMsByTurnId) {
531
+ const endMs = Date.parse(endTs);
532
+ if (!Number.isFinite(endMs)) return void 0;
533
+ const turnId = readString2(payload.turn_id);
534
+ const indexedStart = turnId !== void 0 ? startMsByTurnId.get(turnId) : void 0;
535
+ const durationMs = readNonNegInt2(payload.duration_ms);
536
+ const startMs = indexedStart !== void 0 ? indexedStart : durationMs > 0 ? endMs - durationMs : void 0;
537
+ if (startMs === void 0 || !(startMs < endMs)) return void 0;
538
+ return [startMs, endMs];
539
+ }
540
+ function indexTaskStarts(records) {
541
+ const byTurnId = /* @__PURE__ */ new Map();
542
+ for (const record of records) {
543
+ if (readString2(record.type) !== "event_msg") continue;
544
+ const payload = isObject2(record.payload) ? record.payload : void 0;
545
+ if (payload === void 0 || readString2(payload.type) !== "task_started") continue;
546
+ const turnId = readString2(payload.turn_id);
547
+ const startMs = Date.parse(readString2(record.timestamp) ?? "");
548
+ if (turnId !== void 0 && Number.isFinite(startMs) && !byTurnId.has(turnId)) {
549
+ byTurnId.set(turnId, startMs);
550
+ }
551
+ }
552
+ return byTurnId;
553
+ }
554
+ function parseExitCode(output) {
555
+ if (output === void 0) return null;
556
+ const match = output.match(/Process exited with code (-?\d+)/);
557
+ return match?.[1] !== void 0 ? Number.parseInt(match[1], 10) : null;
558
+ }
559
+ function parseWallTimeMs(output) {
560
+ if (output === void 0) return 0;
561
+ const match = output.match(/Wall time:\s*([\d.]+)\s*seconds/);
562
+ if (match?.[1] === void 0) return 0;
563
+ const seconds = Number.parseFloat(match[1]);
564
+ return Number.isFinite(seconds) ? Math.round(seconds * 1e3) : 0;
565
+ }
566
+ function indexOutputs(records) {
567
+ const byId = /* @__PURE__ */ new Map();
568
+ for (const record of records) {
569
+ if (readString2(record.type) !== "response_item") continue;
570
+ const payload = isObject2(record.payload) ? record.payload : void 0;
571
+ if (payload === void 0) continue;
572
+ if (readString2(payload.type) !== "function_call_output") continue;
573
+ const callId = readString2(payload.call_id);
574
+ const output = readString2(payload.output);
575
+ if (callId !== void 0 && output !== void 0) byId.set(callId, output);
576
+ }
577
+ return byId;
578
+ }
579
+
580
+ // src/approval/approval-store.ts
581
+ import { readdir } from "fs/promises";
582
+ import { join } from "path";
583
+
584
+ // src/lib/error-codes.ts
585
+ function findErrorCode(error, code, depth = 4) {
586
+ let cur = error;
587
+ for (let i = 0; i < depth && cur instanceof Error; i++) {
588
+ const c = cur.code;
589
+ if (typeof c === "string" && c === code) return true;
590
+ cur = cur.cause;
591
+ }
592
+ return false;
593
+ }
594
+
595
+ // src/schemas/approval.schema.ts
596
+ import { z as z2 } from "zod";
597
+
70
598
  // src/schemas/shared.schema.ts
599
+ import { z } from "zod";
71
600
  var SchemaVersionSchema = z.literal("0.1.0");
72
601
  var IsoTimestampSchema = z.string().datetime({ offset: true });
73
602
  var createPrefixedIdSchema = (prefix) => {
74
603
  const refiner = (value) => isValidPrefixedId(value) && value.startsWith(`${prefix}_`);
75
- return z.string().refine(refiner, { message: `Expected ${prefix}_<ULID>` });
604
+ return z.string().refine(refiner, { message: `Expected ${prefix}_<ULID>` }).meta({
605
+ pattern: `^${prefix}_[0-7][0-9A-HJKMNP-TV-Z]{25}$`,
606
+ description: `Basou ${prefix} id: \`${prefix}_\` followed by a 26-character Crockford Base32 ULID.`
607
+ });
76
608
  };
77
609
  var WorkspaceIdSchema = createPrefixedIdSchema("ws");
78
610
  var TaskIdSchema = createPrefixedIdSchema("task");
@@ -488,13 +1020,19 @@ var SessionStatusSchema = z4.enum([
488
1020
  ]);
489
1021
  var SessionSourceKindSchema = z4.enum([
490
1022
  "claude-code-adapter",
1023
+ "claude-code-import",
1024
+ "codex-import",
491
1025
  "human",
492
1026
  "import",
493
1027
  "terminal"
494
1028
  ]);
495
1029
  var SessionSourceSchema = z4.object({
496
1030
  kind: SessionSourceKindSchema,
497
- version: z4.literal("0.1.0")
1031
+ version: z4.literal("0.1.0"),
1032
+ // Optional id of the originating session in the SOURCE tool's own
1033
+ // namespace (e.g. the Claude Code session UUID for a `claude-code-import`).
1034
+ // Lets re-imports of the same source be deduplicated; absent for live runs.
1035
+ external_id: z4.string().optional()
498
1036
  });
499
1037
  var InvocationSchema = z4.object({
500
1038
  command: z4.string().min(1),
@@ -503,6 +1041,17 @@ var InvocationSchema = z4.object({
503
1041
  // code; the same nullability is mirrored in CommandExecutedEventSchema.
504
1042
  exit_code: z4.number().int().nullable()
505
1043
  });
1044
+ var SessionMetricsSchema = z4.object({
1045
+ output_tokens: z4.number().int().nonnegative().optional(),
1046
+ input_tokens: z4.number().int().nonnegative().optional(),
1047
+ cached_input_tokens: z4.number().int().nonnegative().optional(),
1048
+ reasoning_output_tokens: z4.number().int().nonnegative().optional(),
1049
+ active_time_ms: z4.number().int().nonnegative().optional(),
1050
+ active_intervals: z4.array(z4.object({ start: IsoTimestampSchema, end: IsoTimestampSchema })).optional(),
1051
+ active_gap_cap_ms: z4.number().int().nonnegative().optional(),
1052
+ active_time_method: z4.string().optional(),
1053
+ machine_active_time_ms: z4.number().int().nonnegative().optional()
1054
+ });
506
1055
  var SessionInnerSchema = z4.object({
507
1056
  id: SessionIdSchema,
508
1057
  label: z4.string().optional(),
@@ -517,7 +1066,8 @@ var SessionInnerSchema = z4.object({
517
1066
  invocation: InvocationSchema,
518
1067
  related_files: z4.array(z4.string()).default([]),
519
1068
  events_log: z4.string().default("events.jsonl"),
520
- summary: z4.string().nullable().optional()
1069
+ summary: z4.string().nullable().optional(),
1070
+ metrics: SessionMetricsSchema.optional()
521
1071
  });
522
1072
  var SessionSchema = z4.object({
523
1073
  schema_version: SchemaVersionSchema,
@@ -2905,18 +3455,14 @@ async function renderHandoff(input) {
2905
3455
  const latestSession = [...liveEntries].sort(
2906
3456
  (a, b) => Date.parse(b.session.session.started_at) - Date.parse(a.session.session.started_at)
2907
3457
  )[0];
2908
- const allFiles = /* @__PURE__ */ new Set();
2909
- for (const e of entries) {
2910
- if (e.session.session.source.kind === "import") continue;
2911
- for (const f of e.session.session.related_files) allFiles.add(f);
2912
- }
2913
- const sortedFiles = [...allFiles].sort();
3458
+ const latestFiles = latestSession?.session.session.related_files ?? [];
3459
+ const sortedFiles = [...new Set(latestFiles)].sort();
2914
3460
  const displayedFiles = sortedFiles.slice(0, limit);
2915
3461
  const overflow = Math.max(0, sortedFiles.length - limit);
2916
3462
  const suspectCount = entries.filter((e) => e.suspect).length;
2917
3463
  const firstEntry = entries[0];
2918
3464
  const lastEntry = entries[entries.length - 1];
2919
- const sessionRange = firstEntry !== void 0 && lastEntry !== void 0 ? `${firstEntry.sessionId}..${lastEntry.sessionId}` : "";
3465
+ const sessionRange = firstEntry !== void 0 && lastEntry !== void 0 ? `${shortIdWithPrefix(firstEntry.sessionId)}..${shortIdWithPrefix(lastEntry.sessionId)}` : "";
2920
3466
  const body = formatHandoffBody({
2921
3467
  nowIso: input.nowIso,
2922
3468
  sessionRange,
@@ -2956,18 +3502,23 @@ function formatHandoffBody(args) {
2956
3502
  lines.push("## \u73FE\u5728\u306E\u72B6\u614B");
2957
3503
  lines.push("");
2958
3504
  if (args.latestSession !== void 0) {
2959
- const sid = args.latestSession.sessionId;
2960
3505
  const status = args.latestSession.session.session.status;
2961
- lines.push(`- \u6700\u7D42 session: ${sid} (${status})`);
3506
+ const label = args.latestSession.session.session.label;
3507
+ const shortId = shortIdWithPrefix(args.latestSession.sessionId);
3508
+ if (label !== void 0 && label !== "") {
3509
+ lines.push(`- \u6700\u7D42 session: ${label} (${status}) [${shortId}]`);
3510
+ } else {
3511
+ lines.push(`- \u6700\u7D42 session: ${shortId} (${status})`);
3512
+ }
2962
3513
  } else {
2963
3514
  lines.push("- \u6700\u7D42 session: (no live sessions)");
2964
3515
  }
2965
3516
  if (args.latestActivityRecord !== void 0) {
2966
3517
  const statusLabel = args.latestTaskDoc !== void 0 ? args.latestTaskDoc.task.task.status : "status unknown \u2014 task.md missing or invalid";
2967
3518
  const linkedCount = args.latestTaskDoc?.task.task.linked_sessions?.length;
2968
- const linkedSuffix = linkedCount !== void 0 && linkedCount > 1 ? ` (linked_sessions: ${linkedCount})` : "";
3519
+ const linkedSuffix = linkedCount !== void 0 && linkedCount > 1 ? `, linked_sessions: ${linkedCount}` : "";
2969
3520
  lines.push(
2970
- `- \u6700\u7D42 task: ${args.latestActivityRecord.taskId} (${statusLabel}): ${args.latestActivityRecord.title}${linkedSuffix}`
3521
+ `- \u6700\u7D42 task: ${args.latestActivityRecord.title} (${statusLabel}${linkedSuffix}) [${shortIdWithPrefix(args.latestActivityRecord.taskId)}]`
2971
3522
  );
2972
3523
  } else {
2973
3524
  lines.push("- \u6700\u7D42 task: (no tasks recorded yet)");
@@ -2988,7 +3539,7 @@ function formatHandoffBody(args) {
2988
3539
  lines.push("(no decisions recorded yet)");
2989
3540
  } else {
2990
3541
  const last = args.decisions[args.decisions.length - 1];
2991
- lines.push(`- ${last.decisionId}: ${last.title}`);
3542
+ lines.push(`- ${last.title} [${shortIdWithPrefix(last.decisionId)}]`);
2992
3543
  lines.push("");
2993
3544
  lines.push(`(${args.decisions.length} decisions total \u2014 see decisions.md)`);
2994
3545
  }
@@ -3016,7 +3567,9 @@ function formatHandoffBody(args) {
3016
3567
  lines.push("(no pending tasks)");
3017
3568
  } else {
3018
3569
  for (const t of args.pendingTasks) {
3019
- lines.push(`- ${t.task.task.id} (${t.task.task.status}): ${t.task.task.title}`);
3570
+ lines.push(
3571
+ `- ${t.task.task.title} (${t.task.task.status}) [${shortIdWithPrefix(t.task.task.id)}]`
3572
+ );
3020
3573
  }
3021
3574
  }
3022
3575
  lines.push("");
@@ -3085,6 +3638,11 @@ function shortHandoffId(sessionId) {
3085
3638
  if (sessionId.startsWith(SES)) return sessionId.slice(SES.length, SES.length + 10);
3086
3639
  return sessionId.slice(0, 10);
3087
3640
  }
3641
+ function shortIdWithPrefix(id) {
3642
+ const sep = id.indexOf("_");
3643
+ if (sep === -1) return id.slice(0, 10);
3644
+ return id.slice(0, sep + 1) + id.slice(sep + 1, sep + 1 + 10);
3645
+ }
3088
3646
 
3089
3647
  // src/lib/duration.ts
3090
3648
  var DURATION_RE = /^([1-9]\d*)(ms|s|m|h)$/;
@@ -3300,6 +3858,9 @@ function classifySpawnError(error) {
3300
3858
  return new Error("Failed to spawn child process", { cause: error });
3301
3859
  }
3302
3860
 
3861
+ // src/schemas/json-schema.ts
3862
+ import { z as z11 } from "zod";
3863
+
3303
3864
  // src/schemas/manifest.schema.ts
3304
3865
  import { z as z9 } from "zod";
3305
3866
  var ProjectSchema = z9.object({
@@ -3350,7 +3911,10 @@ var SessionInnerImportSchema = z10.object({
3350
3911
  workspace_id: WorkspaceIdSchema,
3351
3912
  source: z10.object({
3352
3913
  kind: SessionSourceKindSchema,
3353
- version: z10.literal("0.1.0")
3914
+ version: z10.literal("0.1.0"),
3915
+ // Source-tool-native id (e.g. Claude Code session UUID), retained so
3916
+ // re-imports of the same source can be deduplicated.
3917
+ external_id: z10.string().optional()
3354
3918
  }),
3355
3919
  started_at: IsoTimestampSchema,
3356
3920
  ended_at: IsoTimestampSchema.optional(),
@@ -3363,7 +3927,8 @@ var SessionInnerImportSchema = z10.object({
3363
3927
  }),
3364
3928
  related_files: z10.array(z10.string()).default([]),
3365
3929
  events_log: z10.string().optional(),
3366
- summary: z10.string().nullable().optional()
3930
+ summary: z10.string().nullable().optional(),
3931
+ metrics: SessionMetricsSchema.optional()
3367
3932
  }).strict();
3368
3933
  var SessionImportPayloadSchema = z10.object({
3369
3934
  schema_version: z10.string(),
@@ -3371,29 +3936,388 @@ var SessionImportPayloadSchema = z10.object({
3371
3936
  events: z10.array(EventSchema)
3372
3937
  }).strict();
3373
3938
 
3939
+ // src/schemas/json-schema.ts
3940
+ var JSON_SCHEMA_VERSION = "0.1.0";
3941
+ var ID_BASE = `https://basou.dev/schemas/${JSON_SCHEMA_VERSION}`;
3942
+ var JSON_SCHEMA_DIALECT = "https://json-schema.org/draft/2020-12/schema";
3943
+ var DOCUMENTS = [
3944
+ {
3945
+ name: "manifest",
3946
+ schema: ManifestSchema,
3947
+ title: "Basou Manifest",
3948
+ description: "The `.basou/manifest.yaml` workspace manifest."
3949
+ },
3950
+ {
3951
+ name: "session",
3952
+ schema: SessionSchema,
3953
+ title: "Basou Session",
3954
+ description: "A `.basou/sessions/<id>/session.yaml` session record."
3955
+ },
3956
+ {
3957
+ name: "event",
3958
+ schema: EventSchema,
3959
+ title: "Basou Event",
3960
+ description: "One line of a `.basou/sessions/<id>/events.jsonl` stream (a discriminated union over the event `type`)."
3961
+ },
3962
+ {
3963
+ name: "task",
3964
+ schema: TaskSchema,
3965
+ title: "Basou Task",
3966
+ description: "The YAML front matter of a `.basou/tasks/<id>.md` task document."
3967
+ },
3968
+ {
3969
+ name: "approval",
3970
+ schema: ApprovalSchema,
3971
+ title: "Basou Approval",
3972
+ description: "A `.basou/approvals/{pending,resolved}/<id>.yaml` approval record."
3973
+ },
3974
+ {
3975
+ name: "status",
3976
+ schema: StatusSchema,
3977
+ title: "Basou Status",
3978
+ description: "The `.basou/status.json` workspace status snapshot."
3979
+ },
3980
+ {
3981
+ name: "task-index",
3982
+ schema: TaskIndexSchema,
3983
+ title: "Basou Task Index",
3984
+ description: "The `.basou/tasks/index.json` task lookup index."
3985
+ },
3986
+ {
3987
+ name: "session-import",
3988
+ schema: SessionImportPayloadSchema,
3989
+ title: "Basou Session Import Payload",
3990
+ description: "The portable session payload consumed by `basou session import` (and produced by `basou session export`)."
3991
+ }
3992
+ ];
3993
+ function buildJsonSchemas() {
3994
+ return DOCUMENTS.map((doc) => {
3995
+ const generated = z11.toJSONSchema(doc.schema, { io: "input" });
3996
+ const { $schema, ...rest } = generated;
3997
+ const schema = {
3998
+ $schema: typeof $schema === "string" ? $schema : JSON_SCHEMA_DIALECT,
3999
+ $id: `${ID_BASE}/${doc.name}.schema.json`,
4000
+ title: doc.title,
4001
+ description: doc.description,
4002
+ ...rest
4003
+ };
4004
+ return { name: doc.name, schema };
4005
+ });
4006
+ }
4007
+ function serializeJsonSchema(schema) {
4008
+ return `${JSON.stringify(schema, null, 2)}
4009
+ `;
4010
+ }
4011
+
4012
+ // src/stats/work-stats.ts
4013
+ import { join as join11 } from "path";
4014
+ function resolveTimeZone(timeZone) {
4015
+ if (timeZone !== void 0 && timeZone.length > 0) return timeZone;
4016
+ return Intl.DateTimeFormat().resolvedOptions().timeZone;
4017
+ }
4018
+ var STATUS_ORDER = [
4019
+ "completed",
4020
+ "failed",
4021
+ "running",
4022
+ "interrupted",
4023
+ "waiting_approval",
4024
+ "initialized",
4025
+ "imported",
4026
+ "archived"
4027
+ ];
4028
+ async function computeWorkStats(input) {
4029
+ const { now } = input;
4030
+ const timeZone = resolveTimeZone(input.timeZone);
4031
+ const unreadableEmitted = /* @__PURE__ */ new Set();
4032
+ const wrappedSkip = (sid, reason) => {
4033
+ if (reason === "events_jsonl_unreadable") unreadableEmitted.add(sid);
4034
+ input.onSessionSkip?.(sid, reason);
4035
+ };
4036
+ const loadOpts = { now, onSkip: wrappedSkip };
4037
+ if (input.onWarning !== void 0) loadOpts.onWarning = input.onWarning;
4038
+ const entries = await loadSessionEntries(input.paths, loadOpts);
4039
+ const sessions = [];
4040
+ for (const entry of entries) {
4041
+ const events = [];
4042
+ let eventsUnreadable = false;
4043
+ try {
4044
+ for await (const ev of replayEvents(join11(input.paths.sessions, entry.sessionId), {
4045
+ onWarning: (w) => input.onWarning?.(w, entry.sessionId)
4046
+ })) {
4047
+ events.push(ev);
4048
+ }
4049
+ } catch {
4050
+ eventsUnreadable = true;
4051
+ if (!unreadableEmitted.has(entry.sessionId)) {
4052
+ wrappedSkip(entry.sessionId, "events_jsonl_unreadable");
4053
+ }
4054
+ }
4055
+ sessions.push(
4056
+ sessionWorkStatsFromEvents(
4057
+ entry.sessionId,
4058
+ entry.session.session,
4059
+ events,
4060
+ now,
4061
+ eventsUnreadable
4062
+ )
4063
+ );
4064
+ }
4065
+ const allIntervals = [];
4066
+ for (const s of sessions) allIntervals.push(...intervalsIsoToMs(s.activeIntervals));
4067
+ const union = unionDurationMs(allIntervals);
4068
+ return {
4069
+ generatedAt: now.toISOString(),
4070
+ activeGapCapMs: ACTIVE_GAP_CAP_MS,
4071
+ timeZone,
4072
+ totals: computeTotals(sessions, union.ms),
4073
+ sessions,
4074
+ bySource: computeBySource(sessions),
4075
+ byStatus: computeByStatus(sessions),
4076
+ byDay: computeByDay(sessions, union.merged, timeZone)
4077
+ };
4078
+ }
4079
+ function sessionWorkStatsFromEvents(sessionId, inner, events, now, eventsUnreadable = false) {
4080
+ let commandCount = 0;
4081
+ let fileChangedCount = 0;
4082
+ let decisionCount = 0;
4083
+ let commandTimeMs = 0;
4084
+ const timestamps = [];
4085
+ for (const ev of events) {
4086
+ const t = Date.parse(ev.occurred_at);
4087
+ if (Number.isFinite(t)) timestamps.push(t);
4088
+ if (ev.type === "command_executed") {
4089
+ commandCount++;
4090
+ commandTimeMs += ev.duration_ms;
4091
+ } else if (ev.type === "file_changed") {
4092
+ fileChangedCount++;
4093
+ } else if (ev.type === "decision_recorded") {
4094
+ decisionCount++;
4095
+ }
4096
+ }
4097
+ const span = computeSpan(inner.started_at, inner.ended_at, now);
4098
+ const tokens = readTokens(inner.metrics);
4099
+ const active = resolveActiveTime(inner.metrics, timestamps);
4100
+ const machineActiveTimeMs = inner.metrics?.machine_active_time_ms ?? 0;
4101
+ return {
4102
+ sessionId,
4103
+ label: inner.label,
4104
+ status: inner.status,
4105
+ sourceKind: inner.source.kind,
4106
+ startedAt: inner.started_at,
4107
+ endedAt: inner.ended_at,
4108
+ open: inner.ended_at === void 0,
4109
+ sessionSpanMs: span.ms,
4110
+ commandTimeMs,
4111
+ activeTimeMs: active.ms,
4112
+ activeTimeBasis: active.basis,
4113
+ activeIntervals: intervalsMsToIso(active.intervals),
4114
+ machineActiveTimeMs,
4115
+ activeTimeMethod: inner.metrics?.active_time_method,
4116
+ commandCount,
4117
+ fileChangedCount,
4118
+ decisionCount,
4119
+ eventCount: events.length,
4120
+ tokens,
4121
+ availability: {
4122
+ span: true,
4123
+ commandTime: inner.source.kind !== "claude-code-import",
4124
+ activeTime: active.intervals.length > 0,
4125
+ tokens: hasTokens(tokens),
4126
+ machineActive: machineActiveTimeMs > 0
4127
+ },
4128
+ spanClamped: span.clamped,
4129
+ eventsUnreadable
4130
+ };
4131
+ }
4132
+ function resolveActiveTime(metrics, eventTimestamps) {
4133
+ const stored = metrics?.active_intervals;
4134
+ if (stored !== void 0 && stored.length > 0) {
4135
+ const intervals = intervalsIsoToMs(stored);
4136
+ const ms = intervals.reduce((n, [start, end]) => n + (end - start), 0);
4137
+ return { ms, intervals, basis: "engaged-turns" };
4138
+ }
4139
+ const derived = activeTimeFromTimestamps(eventTimestamps, ACTIVE_GAP_CAP_MS);
4140
+ return { ms: derived.ms, intervals: derived.intervals, basis: "events" };
4141
+ }
4142
+ function computeSpan(startedAt, endedAt, now) {
4143
+ const start = Date.parse(startedAt);
4144
+ const end = endedAt !== void 0 ? Date.parse(endedAt) : now.getTime();
4145
+ if (!Number.isFinite(start) || !Number.isFinite(end)) return { ms: 0, clamped: true };
4146
+ const raw = end - start;
4147
+ return raw < 0 ? { ms: 0, clamped: true } : { ms: raw, clamped: false };
4148
+ }
4149
+ function readTokens(metrics) {
4150
+ return {
4151
+ output: metrics?.output_tokens ?? 0,
4152
+ input: metrics?.input_tokens ?? 0,
4153
+ cached: metrics?.cached_input_tokens ?? 0,
4154
+ reasoning: metrics?.reasoning_output_tokens ?? 0
4155
+ };
4156
+ }
4157
+ function hasTokens(t) {
4158
+ return t.output > 0 || t.input > 0 || t.cached > 0 || t.reasoning > 0;
4159
+ }
4160
+ function emptyTokens() {
4161
+ return { output: 0, input: 0, cached: 0, reasoning: 0 };
4162
+ }
4163
+ function addTokens(a, b) {
4164
+ a.output += b.output;
4165
+ a.input += b.input;
4166
+ a.cached += b.cached;
4167
+ a.reasoning += b.reasoning;
4168
+ }
4169
+ function computeTotals(sessions, billableActiveTimeMs) {
4170
+ const tokens = emptyTokens();
4171
+ const totals = {
4172
+ sessionCount: sessions.length,
4173
+ openSessionCount: 0,
4174
+ sessionSpanMs: 0,
4175
+ commandTimeMs: 0,
4176
+ activeTimeMs: 0,
4177
+ billableActiveTimeMs,
4178
+ machineActiveTimeMs: 0,
4179
+ commandCount: 0,
4180
+ fileChangedCount: 0,
4181
+ decisionCount: 0,
4182
+ eventCount: 0,
4183
+ tokens,
4184
+ commandTimeReliable: true,
4185
+ tokensAvailable: false,
4186
+ machineActiveAvailable: false
4187
+ };
4188
+ for (const s of sessions) {
4189
+ if (s.open) totals.openSessionCount++;
4190
+ totals.sessionSpanMs += s.sessionSpanMs;
4191
+ totals.commandTimeMs += s.commandTimeMs;
4192
+ totals.activeTimeMs += s.activeTimeMs;
4193
+ totals.machineActiveTimeMs += s.machineActiveTimeMs;
4194
+ totals.commandCount += s.commandCount;
4195
+ totals.fileChangedCount += s.fileChangedCount;
4196
+ totals.decisionCount += s.decisionCount;
4197
+ totals.eventCount += s.eventCount;
4198
+ addTokens(tokens, s.tokens);
4199
+ if (!s.availability.commandTime) totals.commandTimeReliable = false;
4200
+ if (s.availability.tokens) totals.tokensAvailable = true;
4201
+ if (s.availability.machineActive) totals.machineActiveAvailable = true;
4202
+ }
4203
+ return totals;
4204
+ }
4205
+ function computeBySource(sessions) {
4206
+ const map = /* @__PURE__ */ new Map();
4207
+ for (const s of sessions) {
4208
+ let row = map.get(s.sourceKind);
4209
+ if (row === void 0) {
4210
+ row = {
4211
+ sourceKind: s.sourceKind,
4212
+ sessionCount: 0,
4213
+ sessionSpanMs: 0,
4214
+ commandTimeMs: 0,
4215
+ activeTimeMs: 0,
4216
+ machineActiveTimeMs: 0,
4217
+ commandCount: 0,
4218
+ fileChangedCount: 0,
4219
+ decisionCount: 0,
4220
+ eventCount: 0,
4221
+ tokens: emptyTokens(),
4222
+ commandTimeReliable: true,
4223
+ tokensAvailable: false,
4224
+ machineActiveAvailable: false
4225
+ };
4226
+ map.set(s.sourceKind, row);
4227
+ }
4228
+ row.sessionCount++;
4229
+ row.sessionSpanMs += s.sessionSpanMs;
4230
+ row.commandTimeMs += s.commandTimeMs;
4231
+ row.activeTimeMs += s.activeTimeMs;
4232
+ row.machineActiveTimeMs += s.machineActiveTimeMs;
4233
+ row.commandCount += s.commandCount;
4234
+ row.fileChangedCount += s.fileChangedCount;
4235
+ row.decisionCount += s.decisionCount;
4236
+ row.eventCount += s.eventCount;
4237
+ addTokens(row.tokens, s.tokens);
4238
+ if (!s.availability.commandTime) row.commandTimeReliable = false;
4239
+ if (s.availability.tokens) row.tokensAvailable = true;
4240
+ if (s.availability.machineActive) row.machineActiveAvailable = true;
4241
+ }
4242
+ return [...map.values()].sort((a, b) => a.sourceKind.localeCompare(b.sourceKind));
4243
+ }
4244
+ function computeByStatus(sessions) {
4245
+ const counts = /* @__PURE__ */ new Map();
4246
+ for (const s of sessions) counts.set(s.status, (counts.get(s.status) ?? 0) + 1);
4247
+ const ordered = [];
4248
+ for (const status of STATUS_ORDER) {
4249
+ const count = counts.get(status);
4250
+ if (count !== void 0 && count > 0) ordered.push({ status, count });
4251
+ }
4252
+ return ordered;
4253
+ }
4254
+ function computeByDay(sessions, unionMerged, timeZone) {
4255
+ const days = /* @__PURE__ */ new Map();
4256
+ const ensure = (date) => {
4257
+ let day = days.get(date);
4258
+ if (day === void 0) {
4259
+ day = {
4260
+ date,
4261
+ billableActiveTimeMs: 0,
4262
+ machineActiveTimeMs: 0,
4263
+ sessionCount: 0,
4264
+ commandCount: 0,
4265
+ fileChangedCount: 0,
4266
+ decisionCount: 0,
4267
+ tokens: emptyTokens()
4268
+ };
4269
+ days.set(date, day);
4270
+ }
4271
+ return day;
4272
+ };
4273
+ for (const [start, end] of unionMerged) {
4274
+ ensure(tzDate(start, timeZone)).billableActiveTimeMs += end - start;
4275
+ }
4276
+ for (const s of sessions) {
4277
+ const startedMs = Date.parse(s.startedAt);
4278
+ if (!Number.isFinite(startedMs)) continue;
4279
+ const day = ensure(tzDate(startedMs, timeZone));
4280
+ day.sessionCount++;
4281
+ day.machineActiveTimeMs += s.machineActiveTimeMs;
4282
+ day.commandCount += s.commandCount;
4283
+ day.fileChangedCount += s.fileChangedCount;
4284
+ day.decisionCount += s.decisionCount;
4285
+ addTokens(day.tokens, s.tokens);
4286
+ }
4287
+ return [...days.values()].sort((a, b) => a.date.localeCompare(b.date));
4288
+ }
4289
+ function tzDate(ms, timeZone) {
4290
+ return new Intl.DateTimeFormat("en-CA", {
4291
+ timeZone,
4292
+ year: "numeric",
4293
+ month: "2-digit",
4294
+ day: "2-digit"
4295
+ }).format(new Date(ms));
4296
+ }
4297
+
3374
4298
  // src/storage/basou-dir.ts
3375
4299
  import { lstat as lstat3, mkdir as mkdir3 } from "fs/promises";
3376
- import { join as join11 } from "path";
4300
+ import { join as join12 } from "path";
3377
4301
  function basouPaths(repositoryRoot) {
3378
- const root = join11(repositoryRoot, ".basou");
3379
- const approvalsBase = join11(root, "approvals");
4302
+ const root = join12(repositoryRoot, ".basou");
4303
+ const approvalsBase = join12(root, "approvals");
3380
4304
  return {
3381
4305
  root,
3382
- sessions: join11(root, "sessions"),
3383
- tasks: join11(root, "tasks"),
4306
+ sessions: join12(root, "sessions"),
4307
+ tasks: join12(root, "tasks"),
3384
4308
  approvals: {
3385
- pending: join11(approvalsBase, "pending"),
3386
- resolved: join11(approvalsBase, "resolved")
4309
+ pending: join12(approvalsBase, "pending"),
4310
+ resolved: join12(approvalsBase, "resolved")
3387
4311
  },
3388
- locks: join11(root, "locks"),
3389
- logs: join11(root, "logs"),
3390
- raw: join11(root, "raw"),
3391
- tmp: join11(root, "tmp"),
4312
+ locks: join12(root, "locks"),
4313
+ logs: join12(root, "logs"),
4314
+ raw: join12(root, "raw"),
4315
+ tmp: join12(root, "tmp"),
3392
4316
  files: {
3393
- manifest: join11(root, "manifest.yaml"),
3394
- status: join11(root, "status.json"),
3395
- handoff: join11(root, "handoff.md"),
3396
- decisions: join11(root, "decisions.md")
4317
+ manifest: join12(root, "manifest.yaml"),
4318
+ status: join12(root, "status.json"),
4319
+ handoff: join12(root, "handoff.md"),
4320
+ decisions: join12(root, "decisions.md")
3397
4321
  }
3398
4322
  };
3399
4323
  }
@@ -3450,11 +4374,11 @@ function hasErrorCode3(error) {
3450
4374
 
3451
4375
  // src/storage/gitignore.ts
3452
4376
  import { readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
3453
- import { join as join12 } from "path";
4377
+ import { join as join13 } from "path";
3454
4378
  var MARKER = "# Basou - default ignore";
3455
4379
  var BASOU_GITIGNORE_BLOCK = "# Basou - default ignore\n.basou/logs/\n.basou/raw/\n.basou/tmp/\n.basou/locks/\n.basou/status.json\n.basou/sessions/*/events.jsonl\n.basou/sessions/*/artifacts/\n.basou/approvals/pending/\n.basou/approvals/resolved/\n\n# Basou - default commit\n# .basou/manifest.yaml\n# .basou/handoff.md\n# .basou/decisions.md\n# .basou/tasks/\n# .basou/sessions/*/session.yaml\n# .basou/sessions/*/transcript.md\n# .basou/sessions/*/changed-files.json\n";
3456
4380
  async function appendBasouGitignore(repositoryRoot) {
3457
- const gitignorePath = join12(repositoryRoot, ".gitignore");
4381
+ const gitignorePath = join13(repositoryRoot, ".gitignore");
3458
4382
  let body;
3459
4383
  let existed;
3460
4384
  try {
@@ -3668,7 +4592,7 @@ function hasErrorCode6(error) {
3668
4592
  // src/storage/session-import.ts
3669
4593
  import { mkdir as mkdir4, rm as rm2 } from "fs/promises";
3670
4594
  import { homedir as homedir2 } from "os";
3671
- import { join as join13 } from "path";
4595
+ import { join as join14 } from "path";
3672
4596
  async function importSessionFromJson(paths, manifest, payload, options) {
3673
4597
  if (options.taskIdOverride !== void 0 && !TaskIdSchema.safeParse(options.taskIdOverride).success) {
3674
4598
  throw new Error(`Invalid task_id: ${options.taskIdOverride}`);
@@ -3693,7 +4617,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
3693
4617
  pathSanitizeReport
3694
4618
  };
3695
4619
  }
3696
- const sessionDir = join13(paths.sessions, newSessionId);
4620
+ const sessionDir = join14(paths.sessions, newSessionId);
3697
4621
  try {
3698
4622
  await mkdir4(sessionDir, { recursive: true });
3699
4623
  } catch (error) {
@@ -3706,7 +4630,7 @@ async function importSessionFromJson(paths, manifest, payload, options) {
3706
4630
  throw error;
3707
4631
  }
3708
4632
  try {
3709
- const sessionYamlPath = join13(sessionDir, "session.yaml");
4633
+ const sessionYamlPath = join14(sessionDir, "session.yaml");
3710
4634
  await linkYamlFile(sessionYamlPath, sessionRecord);
3711
4635
  } catch (error) {
3712
4636
  await rm2(sessionDir, { recursive: true, force: true }).catch(() => void 0);
@@ -3787,7 +4711,8 @@ function buildSessionRecord(input, manifest, newSessionId, options) {
3787
4711
  invocation: input.invocation,
3788
4712
  related_files: relatedSanitized.sanitized,
3789
4713
  events_log: "events.jsonl",
3790
- summary: input.summary ?? null
4714
+ summary: input.summary ?? null,
4715
+ ...input.metrics !== void 0 ? { metrics: input.metrics } : {}
3791
4716
  };
3792
4717
  return {
3793
4718
  record: { schema_version: "0.1.0", session: inner },
@@ -3801,10 +4726,13 @@ function buildSessionRecord(input, manifest, newSessionId, options) {
3801
4726
  // src/index.ts
3802
4727
  var BASOU_CORE_VERSION = "0.1.0";
3803
4728
  export {
4729
+ ACTIVE_GAP_CAP_MS,
3804
4730
  ApprovalIdSchema,
3805
4731
  ApprovalSchema,
3806
4732
  ApprovalStatusSchema,
3807
4733
  BASOU_CORE_VERSION,
4734
+ CLAUDE_IMPORT_SOURCE,
4735
+ CODEX_IMPORT_SOURCE,
3808
4736
  ChildProcessRunner,
3809
4737
  DecisionIdSchema,
3810
4738
  EventIdSchema,
@@ -3815,6 +4743,7 @@ export {
3815
4743
  GENERATED_START,
3816
4744
  ID_PREFIXES,
3817
4745
  IsoTimestampSchema,
4746
+ JSON_SCHEMA_VERSION,
3818
4747
  ManifestSchema,
3819
4748
  RiskLevelSchema,
3820
4749
  STUCK_THRESHOLD_MS,
@@ -3822,6 +4751,7 @@ export {
3822
4751
  SessionIdSchema,
3823
4752
  SessionImportPayloadSchema,
3824
4753
  SessionInnerImportSchema,
4754
+ SessionMetricsSchema,
3825
4755
  SessionSchema,
3826
4756
  SessionSourceKindSchema,
3827
4757
  SessionStatusSchema,
@@ -3838,9 +4768,13 @@ export {
3838
4768
  archiveTask,
3839
4769
  assertBasouRootSafe,
3840
4770
  basouPaths,
4771
+ buildJsonSchemas,
3841
4772
  buildStatusSnapshot,
3842
4773
  classifySuspect,
3843
4774
  claudeCodeAdapterMetadata,
4775
+ claudeTranscriptToImportPayload,
4776
+ codexRolloutToImportPayload,
4777
+ computeWorkStats,
3844
4778
  createAdHocSessionWithEvent,
3845
4779
  createManifest,
3846
4780
  createTaskWithEvent,
@@ -3887,6 +4821,8 @@ export {
3887
4821
  sanitizePath,
3888
4822
  sanitizeRelatedFiles,
3889
4823
  sanitizeWorkingDirectory,
4824
+ serializeJsonSchema,
4825
+ sessionWorkStatsFromEvents,
3890
4826
  summarizeAdapterOutput,
3891
4827
  tryRemoteUrl,
3892
4828
  ulid,