@adhdev/daemon-core 0.9.82-rc.17 → 0.9.82-rc.170
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/boot/process-hardening.d.ts +50 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +22 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/commands/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +72 -1
- package/dist/commands/router.d.ts +22 -0
- package/dist/config/chat-history.d.ts +7 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +71 -1
- package/dist/git/git-commands.d.ts +5 -1
- package/dist/index.d.ts +35 -6
- package/dist/index.js +18621 -3760
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +18525 -3732
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +72 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +30 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-events.d.ts +96 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +58 -1
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-work-queue.d.ts +44 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +113 -0
- package/dist/providers/approval-utils.d.ts +13 -0
- package/dist/providers/cli-provider-instance.d.ts +9 -3
- package/dist/providers/contracts.d.ts +98 -5
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/dispatcher.d.ts +24 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance.d.ts +3 -0
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +169 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +56 -0
- package/dist/providers/spec/cli-adapter.d.ts +95 -0
- package/dist/providers/spec/driver.d.ts +149 -0
- package/dist/providers/spec/evaluator.d.ts +47 -0
- package/dist/providers/spec/loader.d.ts +14 -0
- package/dist/providers/spec/native-history-executor.d.ts +39 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +511 -0
- package/dist/providers/spec/types.d.ts +212 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +48 -0
- package/dist/repo-mesh-types.d.ts +100 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -2
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +17 -10
- package/src/boot/process-hardening.ts +89 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +14 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +21 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1086 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +59 -11
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- package/src/commands/chat-commands.ts +1597 -60
- package/src/commands/cli-manager.ts +214 -3
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +231 -127
- package/src/commands/router.ts +3559 -502
- package/src/config/chat-history.ts +87 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +264 -2
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +17 -5
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +113 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/beads-db.ts +479 -0
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +204 -30
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-events.ts +879 -61
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +457 -104
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-work-queue.ts +216 -158
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +247 -0
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +452 -54
- package/src/providers/contracts.ts +105 -5
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +19 -5
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/dispatcher.ts +227 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance.ts +3 -0
- package/src/providers/provider-loader.ts +585 -50
- package/src/providers/provider-schema.ts +31 -13
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +458 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +210 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +168 -0
- package/src/providers/spec/cli-adapter.ts +430 -0
- package/src/providers/spec/driver.ts +502 -0
- package/src/providers/spec/evaluator.ts +298 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +623 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +511 -0
- package/src/providers/spec/schema.json +211 -0
- package/src/providers/spec/types.ts +231 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +297 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +110 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -6
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +84 -25
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
|
@@ -0,0 +1,419 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-cli-transcript — Daemon-side built-in native history adapter for codex-cli.
|
|
3
|
+
*
|
|
4
|
+
* Reads Codex CLI rollout JSONL session files directly without shelling out to a JS override.
|
|
5
|
+
* Files are stored at ~/.codex/sessions/<uuid>[-<slug>].jsonl.
|
|
6
|
+
*
|
|
7
|
+
* Format — each line is a JSON object of one of these types:
|
|
8
|
+
* { type: 'session_meta', timestamp: number, payload: { id: string, cwd: string, ... } }
|
|
9
|
+
* { type: 'response_item', timestamp: number, payload: { type: 'message', role: 'user'|'assistant', content: ... } }
|
|
10
|
+
* { type: 'response_item', timestamp: number, payload: { type: 'function_call' | 'custom_tool_call', name?: string, arguments?: string, input?: unknown } }
|
|
11
|
+
* { type: 'response_item', timestamp: number, payload: { type: 'function_call_output' | 'custom_tool_call_output', output?: string, result?: string } }
|
|
12
|
+
*
|
|
13
|
+
* watchPath (from provider.v1.json): ~/.codex/sessions
|
|
14
|
+
*
|
|
15
|
+
* OSS code (AGPL-3.0). Must not import from packages/ (proprietary).
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import * as fs from 'fs';
|
|
19
|
+
import * as path from 'path';
|
|
20
|
+
import * as os from 'os';
|
|
21
|
+
|
|
22
|
+
// ─── Types ─────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
export type NativeHistoryRole = 'user' | 'assistant' | 'system';
|
|
25
|
+
export type NativeHistoryKind = 'standard' | 'tool' | 'session_start';
|
|
26
|
+
|
|
27
|
+
export interface NativeHistoryMessage {
|
|
28
|
+
ts: string;
|
|
29
|
+
receivedAt: number;
|
|
30
|
+
role: NativeHistoryRole;
|
|
31
|
+
content: string;
|
|
32
|
+
kind: NativeHistoryKind;
|
|
33
|
+
senderName?: string;
|
|
34
|
+
agent: 'codex-cli';
|
|
35
|
+
historySessionId: string;
|
|
36
|
+
workspace?: string;
|
|
37
|
+
/** Stable per-message identity (v2 contract). */
|
|
38
|
+
providerUnitKey?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface NativeHistorySession {
|
|
42
|
+
messages: NativeHistoryMessage[];
|
|
43
|
+
providerSessionId: string;
|
|
44
|
+
source: 'provider-native';
|
|
45
|
+
sourcePath: string;
|
|
46
|
+
sourceMtimeMs: number;
|
|
47
|
+
nativeHistoryCoverage: 'full';
|
|
48
|
+
workspace?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface NativeHistorySessionMeta {
|
|
52
|
+
historySessionId: string;
|
|
53
|
+
sessionId: string;
|
|
54
|
+
sourcePath: string;
|
|
55
|
+
sourceMtimeMs: number;
|
|
56
|
+
messageCount: number;
|
|
57
|
+
firstMessageAt: number;
|
|
58
|
+
lastMessageAt: number;
|
|
59
|
+
sessionTitle?: string;
|
|
60
|
+
preview?: string;
|
|
61
|
+
workspace?: string;
|
|
62
|
+
agent: 'codex-cli';
|
|
63
|
+
source: 'provider-native';
|
|
64
|
+
nativeHistoryCoverage: 'full';
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Internal helpers ───────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function extractTimestampValue(value: unknown): number {
|
|
70
|
+
if (typeof value === 'number' && Number.isFinite(value) && value > 0) return value;
|
|
71
|
+
if (typeof value === 'string') {
|
|
72
|
+
const numeric = Number(value);
|
|
73
|
+
if (Number.isFinite(numeric) && numeric > 0) return numeric;
|
|
74
|
+
const parsed = Date.parse(value);
|
|
75
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
76
|
+
}
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function statMtimeMs(filePath: string): number {
|
|
81
|
+
try { return fs.statSync(filePath).mtimeMs; } catch { return 0; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isUuidLikeSessionId(sessionId: string): boolean {
|
|
85
|
+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(sessionId);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isSafeFilename(name: string): boolean {
|
|
89
|
+
return /^[A-Za-z0-9._:-]+$/.test(name) && !name.includes('..');
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function codexSessionsRoot(): string {
|
|
93
|
+
return path.join(os.homedir(), '.codex', 'sessions');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Flatten Codex content (string, array, or nested object with text/output/message) into plain text.
|
|
98
|
+
*/
|
|
99
|
+
function flattenCodexContent(content: unknown): string {
|
|
100
|
+
if (typeof content === 'string') return content.trim();
|
|
101
|
+
if (content == null) return '';
|
|
102
|
+
if (Array.isArray(content)) {
|
|
103
|
+
return (content as unknown[]).map(flattenCodexContent).filter(Boolean).join('\n').trim();
|
|
104
|
+
}
|
|
105
|
+
if (typeof content === 'object') {
|
|
106
|
+
const obj = content as Record<string, unknown>;
|
|
107
|
+
if (typeof obj.text === 'string') return obj.text.trim();
|
|
108
|
+
if (typeof obj.content === 'string' || Array.isArray(obj.content)) return flattenCodexContent(obj.content);
|
|
109
|
+
if (typeof obj.output === 'string') return obj.output.trim();
|
|
110
|
+
if (typeof obj.message === 'string') return obj.message.trim();
|
|
111
|
+
}
|
|
112
|
+
return '';
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Summarize tool call arguments into a compact string.
|
|
117
|
+
*/
|
|
118
|
+
function summarizeToolArguments(value: unknown): string {
|
|
119
|
+
if (typeof value === 'string') return value.trim();
|
|
120
|
+
if (Array.isArray(value)) return (value as unknown[]).map(String).join(' ').trim();
|
|
121
|
+
if (!value || typeof value !== 'object') return '';
|
|
122
|
+
const obj = value as Record<string, unknown>;
|
|
123
|
+
const direct = obj.command ?? obj.cmd ?? obj.query ?? obj.path ?? obj.prompt;
|
|
124
|
+
if (typeof direct === 'string') return direct.trim();
|
|
125
|
+
if (Array.isArray(direct)) return (direct as unknown[]).map(String).join(' ').trim();
|
|
126
|
+
try { return JSON.stringify(value).trim(); } catch { return ''; }
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Build a human-readable summary of a Codex tool call payload.
|
|
131
|
+
*/
|
|
132
|
+
function summarizeToolCall(payload: Record<string, unknown>): string {
|
|
133
|
+
const name = String(payload.name ?? payload.type ?? 'tool').trim() || 'tool';
|
|
134
|
+
const rawArguments = payload.arguments ?? payload.input;
|
|
135
|
+
let argumentValue = '';
|
|
136
|
+
if (typeof rawArguments === 'string') {
|
|
137
|
+
const trimmed = rawArguments.trim();
|
|
138
|
+
try {
|
|
139
|
+
argumentValue = summarizeToolArguments(JSON.parse(trimmed));
|
|
140
|
+
} catch {
|
|
141
|
+
argumentValue = trimmed;
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
argumentValue = summarizeToolArguments(rawArguments);
|
|
145
|
+
}
|
|
146
|
+
return argumentValue ? `${name}: ${argumentValue}` : name;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Extract text content from a tool output payload.
|
|
151
|
+
*/
|
|
152
|
+
function extractToolOutputContent(payload: Record<string, unknown>): string {
|
|
153
|
+
const output = payload.output ?? payload.result ?? payload.content;
|
|
154
|
+
const text = flattenCodexContent(output);
|
|
155
|
+
if (text) return text;
|
|
156
|
+
if (output && typeof output === 'object') {
|
|
157
|
+
try { return JSON.stringify(output).trim(); } catch { return ''; }
|
|
158
|
+
}
|
|
159
|
+
return '';
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Read the first line of a Codex JSONL session file and parse the session_meta record.
|
|
164
|
+
* Returns the payload object (containing id, cwd, etc.) or null.
|
|
165
|
+
*/
|
|
166
|
+
function readSessionMeta(filePath: string): Record<string, unknown> | null {
|
|
167
|
+
try {
|
|
168
|
+
const firstLine = fs.readFileSync(filePath, 'utf-8').split('\n').find(Boolean);
|
|
169
|
+
if (!firstLine) return null;
|
|
170
|
+
const parsed = JSON.parse(firstLine) as Record<string, unknown>;
|
|
171
|
+
if (String(parsed.type ?? '') !== 'session_meta') return null;
|
|
172
|
+
return parsed.payload && typeof parsed.payload === 'object'
|
|
173
|
+
? (parsed.payload as Record<string, unknown>)
|
|
174
|
+
: null;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Parse a Codex session JSONL file into NativeHistoryMessages.
|
|
182
|
+
* The sessionId must be the UUID from the session_meta row.
|
|
183
|
+
*/
|
|
184
|
+
function parseSessionFile(
|
|
185
|
+
filePath: string,
|
|
186
|
+
sessionId: string,
|
|
187
|
+
workspaceFallback?: string,
|
|
188
|
+
): NativeHistoryMessage[] {
|
|
189
|
+
let raw: string;
|
|
190
|
+
try { raw = fs.readFileSync(filePath, 'utf-8'); } catch { return []; }
|
|
191
|
+
|
|
192
|
+
const lines = raw.split('\n').filter(Boolean);
|
|
193
|
+
const records: NativeHistoryMessage[] = [];
|
|
194
|
+
let fallbackTs = Date.now();
|
|
195
|
+
let detectedWorkspace = typeof workspaceFallback === 'string' ? workspaceFallback.trim() : '';
|
|
196
|
+
|
|
197
|
+
for (const line of lines) {
|
|
198
|
+
let parsed: unknown = null;
|
|
199
|
+
try { parsed = JSON.parse(line); } catch { continue; }
|
|
200
|
+
if (!parsed || typeof parsed !== 'object') continue;
|
|
201
|
+
|
|
202
|
+
const record = parsed as Record<string, unknown>;
|
|
203
|
+
const receivedAt = extractTimestampValue(record.timestamp) || fallbackTs;
|
|
204
|
+
fallbackTs = receivedAt + 1;
|
|
205
|
+
|
|
206
|
+
const type = String(record.type ?? '').trim();
|
|
207
|
+
const payload = record.payload && typeof record.payload === 'object'
|
|
208
|
+
? (record.payload as Record<string, unknown>)
|
|
209
|
+
: null;
|
|
210
|
+
if (!payload) continue;
|
|
211
|
+
|
|
212
|
+
if (type === 'session_meta') {
|
|
213
|
+
// Validate session identity — if the meta reports a different id, bail out
|
|
214
|
+
const metaId = String(payload.id ?? '').trim();
|
|
215
|
+
if (metaId && metaId !== sessionId) return [];
|
|
216
|
+
|
|
217
|
+
const metaWorkspace = String(payload.cwd ?? '').trim();
|
|
218
|
+
if (!detectedWorkspace && metaWorkspace) detectedWorkspace = metaWorkspace;
|
|
219
|
+
|
|
220
|
+
// Emit session_start system record once
|
|
221
|
+
if (records.length === 0 && detectedWorkspace) {
|
|
222
|
+
records.push({
|
|
223
|
+
ts: new Date(receivedAt).toISOString(),
|
|
224
|
+
receivedAt,
|
|
225
|
+
role: 'system',
|
|
226
|
+
content: detectedWorkspace,
|
|
227
|
+
kind: 'session_start',
|
|
228
|
+
agent: 'codex-cli',
|
|
229
|
+
historySessionId: sessionId,
|
|
230
|
+
workspace: detectedWorkspace,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (type !== 'response_item') continue;
|
|
237
|
+
|
|
238
|
+
const payloadType = String(payload.type ?? '').trim();
|
|
239
|
+
|
|
240
|
+
if (payloadType === 'message') {
|
|
241
|
+
const role = String(payload.role ?? '').trim();
|
|
242
|
+
if (role !== 'user' && role !== 'assistant') continue;
|
|
243
|
+
|
|
244
|
+
const content = flattenCodexContent(payload.content);
|
|
245
|
+
if (!content) continue;
|
|
246
|
+
|
|
247
|
+
const msg: NativeHistoryMessage = {
|
|
248
|
+
ts: new Date(receivedAt).toISOString(),
|
|
249
|
+
receivedAt,
|
|
250
|
+
role: role as 'user' | 'assistant',
|
|
251
|
+
content,
|
|
252
|
+
kind: 'standard',
|
|
253
|
+
agent: 'codex-cli',
|
|
254
|
+
historySessionId: sessionId,
|
|
255
|
+
};
|
|
256
|
+
if (detectedWorkspace) msg.workspace = detectedWorkspace;
|
|
257
|
+
records.push(msg);
|
|
258
|
+
} else if (payloadType === 'function_call' || payloadType === 'custom_tool_call') {
|
|
259
|
+
const content = summarizeToolCall(payload);
|
|
260
|
+
if (!content) continue;
|
|
261
|
+
|
|
262
|
+
const msg: NativeHistoryMessage = {
|
|
263
|
+
ts: new Date(receivedAt).toISOString(),
|
|
264
|
+
receivedAt,
|
|
265
|
+
role: 'assistant',
|
|
266
|
+
content,
|
|
267
|
+
kind: 'tool',
|
|
268
|
+
senderName: 'Tool',
|
|
269
|
+
agent: 'codex-cli',
|
|
270
|
+
historySessionId: sessionId,
|
|
271
|
+
};
|
|
272
|
+
if (detectedWorkspace) msg.workspace = detectedWorkspace;
|
|
273
|
+
records.push(msg);
|
|
274
|
+
} else if (payloadType === 'function_call_output' || payloadType === 'custom_tool_call_output') {
|
|
275
|
+
const content = extractToolOutputContent(payload);
|
|
276
|
+
if (!content) continue;
|
|
277
|
+
|
|
278
|
+
const msg: NativeHistoryMessage = {
|
|
279
|
+
ts: new Date(receivedAt).toISOString(),
|
|
280
|
+
receivedAt,
|
|
281
|
+
role: 'assistant',
|
|
282
|
+
content,
|
|
283
|
+
kind: 'tool',
|
|
284
|
+
senderName: 'Tool',
|
|
285
|
+
agent: 'codex-cli',
|
|
286
|
+
historySessionId: sessionId,
|
|
287
|
+
};
|
|
288
|
+
if (detectedWorkspace) msg.workspace = detectedWorkspace;
|
|
289
|
+
records.push(msg);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return records;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// ─── Public API ─────────────────────────────────────────────────────────────
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Read a single Codex CLI session by its JSONL file path.
|
|
300
|
+
*
|
|
301
|
+
* `sessionPath` is the absolute path to a `<uuid>[-<slug>].jsonl` file
|
|
302
|
+
* under ~/.codex/sessions/.
|
|
303
|
+
* Returns `null` when the file is missing, empty, or yields no parseable messages.
|
|
304
|
+
*/
|
|
305
|
+
export function readSession(sessionPath: string): NativeHistorySession | null {
|
|
306
|
+
if (!sessionPath || !path.isAbsolute(sessionPath)) return null;
|
|
307
|
+
if (!fs.existsSync(sessionPath)) return null;
|
|
308
|
+
|
|
309
|
+
// Derive the session UUID from the file.
|
|
310
|
+
// The meta row's id is authoritative; the filename may also carry a UUID.
|
|
311
|
+
// When both are present they must agree — a mismatch means the file is corrupt
|
|
312
|
+
// or belongs to a different session, so we reject it.
|
|
313
|
+
const meta = readSessionMeta(sessionPath);
|
|
314
|
+
const metaId = String(meta?.id ?? '').trim();
|
|
315
|
+
|
|
316
|
+
// Extract UUID from filename (filename may include a slug suffix like <uuid>-name)
|
|
317
|
+
const basename = path.basename(sessionPath, '.jsonl');
|
|
318
|
+
const uuidMatch = basename.match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i);
|
|
319
|
+
const filenameUuid = uuidMatch ? uuidMatch[1] : '';
|
|
320
|
+
|
|
321
|
+
// Reject when both IDs are present but don't match
|
|
322
|
+
if (metaId && filenameUuid && metaId !== filenameUuid) return null;
|
|
323
|
+
|
|
324
|
+
const sessionId = metaId || filenameUuid;
|
|
325
|
+
if (!sessionId || !isUuidLikeSessionId(sessionId)) return null;
|
|
326
|
+
|
|
327
|
+
const workspaceFallback = typeof meta?.cwd === 'string' ? meta.cwd : undefined;
|
|
328
|
+
const sourceMtimeMs = statMtimeMs(sessionPath);
|
|
329
|
+
const messages = parseSessionFile(sessionPath, sessionId, workspaceFallback);
|
|
330
|
+
if (messages.length === 0) return null;
|
|
331
|
+
|
|
332
|
+
const firstSystem = messages.find((m) => m.kind === 'session_start');
|
|
333
|
+
const workspace = firstSystem?.workspace || firstSystem?.content || undefined;
|
|
334
|
+
|
|
335
|
+
return {
|
|
336
|
+
messages,
|
|
337
|
+
providerSessionId: sessionId,
|
|
338
|
+
source: 'provider-native',
|
|
339
|
+
sourcePath: sessionPath,
|
|
340
|
+
sourceMtimeMs,
|
|
341
|
+
nativeHistoryCoverage: 'full',
|
|
342
|
+
workspace,
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* List all Codex CLI sessions under the given watchPath base dir.
|
|
348
|
+
*
|
|
349
|
+
* `watchPath` is the value from provider.v1.json (`~/.codex/sessions`).
|
|
350
|
+
* Expands the home directory and scans the directory recursively,
|
|
351
|
+
* collecting all `.jsonl` files.
|
|
352
|
+
*
|
|
353
|
+
* Returns summary metadata for each session, sorted by most recently updated first.
|
|
354
|
+
*/
|
|
355
|
+
export async function listSessions(watchPath: string): Promise<NativeHistorySessionMeta[]> {
|
|
356
|
+
const expandedBase = watchPath.startsWith('~/')
|
|
357
|
+
? path.join(os.homedir(), watchPath.slice(2).split('/**')[0].split('/*')[0])
|
|
358
|
+
: watchPath.split('/**')[0].split('/*')[0];
|
|
359
|
+
|
|
360
|
+
const root = fs.existsSync(expandedBase) ? expandedBase : codexSessionsRoot();
|
|
361
|
+
if (!fs.existsSync(root)) return [];
|
|
362
|
+
|
|
363
|
+
const results: NativeHistorySessionMeta[] = [];
|
|
364
|
+
const uuidPattern = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
|
|
365
|
+
|
|
366
|
+
const stack: string[] = [root];
|
|
367
|
+
while (stack.length > 0) {
|
|
368
|
+
const current = stack.pop()!;
|
|
369
|
+
let entries: fs.Dirent[] = [];
|
|
370
|
+
try { entries = fs.readdirSync(current, { withFileTypes: true }); } catch { continue; }
|
|
371
|
+
|
|
372
|
+
for (const entry of entries) {
|
|
373
|
+
const entryPath = path.join(current, entry.name);
|
|
374
|
+
if (entry.isDirectory()) {
|
|
375
|
+
stack.push(entryPath);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (!entry.isFile() || !entry.name.endsWith('.jsonl')) continue;
|
|
379
|
+
if (!isSafeFilename(entry.name.replace('.jsonl', ''))) continue;
|
|
380
|
+
|
|
381
|
+
// Derive session UUID from meta or filename
|
|
382
|
+
const meta = readSessionMeta(entryPath);
|
|
383
|
+
const metaId = String(meta?.id ?? '').trim();
|
|
384
|
+
const filenameMatch = entry.name.replace('.jsonl', '').match(uuidPattern);
|
|
385
|
+
const sessionId = metaId || (filenameMatch ? filenameMatch[1] : '');
|
|
386
|
+
if (!sessionId || !isUuidLikeSessionId(sessionId)) continue;
|
|
387
|
+
|
|
388
|
+
const workspaceFallback = typeof meta?.cwd === 'string' ? meta.cwd : undefined;
|
|
389
|
+
const sourceMtimeMs = statMtimeMs(entryPath);
|
|
390
|
+
const messages = parseSessionFile(entryPath, sessionId, workspaceFallback);
|
|
391
|
+
const visible = messages.filter((m) => m.kind !== 'session_start');
|
|
392
|
+
if (visible.length === 0) continue;
|
|
393
|
+
|
|
394
|
+
const firstSystem = messages.find((m) => m.kind === 'session_start');
|
|
395
|
+
const workspace = firstSystem?.workspace || firstSystem?.content || undefined;
|
|
396
|
+
const firstMsg = visible[0];
|
|
397
|
+
const lastMsg = visible[visible.length - 1];
|
|
398
|
+
|
|
399
|
+
results.push({
|
|
400
|
+
historySessionId: sessionId,
|
|
401
|
+
sessionId,
|
|
402
|
+
sourcePath: entryPath,
|
|
403
|
+
sourceMtimeMs,
|
|
404
|
+
messageCount: visible.length,
|
|
405
|
+
firstMessageAt: firstMsg.receivedAt || sourceMtimeMs,
|
|
406
|
+
lastMessageAt: lastMsg.receivedAt || sourceMtimeMs,
|
|
407
|
+
sessionTitle: lastMsg.content,
|
|
408
|
+
preview: lastMsg.content,
|
|
409
|
+
workspace,
|
|
410
|
+
agent: 'codex-cli',
|
|
411
|
+
source: 'provider-native',
|
|
412
|
+
nativeHistoryCoverage: 'full',
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
results.sort((a, b) => (b.lastMessageAt || 0) - (a.lastMessageAt || 0));
|
|
418
|
+
return results;
|
|
419
|
+
}
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec-aware dispatcher for native conversation history.
|
|
3
|
+
*
|
|
4
|
+
* The four existing readers (claude/codex/antigravity/hermes) already know
|
|
5
|
+
* how to parse each agent's on-disk format. This dispatcher just resolves
|
|
6
|
+
* the right file given a workspace + sessionId hint, then hands it off.
|
|
7
|
+
*
|
|
8
|
+
* Wired into ProviderModule.scripts.readNativeHistory by provider-loader
|
|
9
|
+
* when spec.json declares native_history.reader. The script signature
|
|
10
|
+
* matches what chat-history.ts expects (see callProviderNativeHistoryRead).
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as os from 'node:os';
|
|
16
|
+
import * as path from 'node:path';
|
|
17
|
+
import { readSession as readClaudeCliSession } from './claude-cli-transcript.js';
|
|
18
|
+
import { readSession as readCodexCliSession } from './codex-cli-transcript.js';
|
|
19
|
+
import { readSession as readAntigravityCliSession } from './antigravity-cli-transcript.js';
|
|
20
|
+
import { readSession as readHermesCliSession } from './hermes-cli-transcript.js';
|
|
21
|
+
|
|
22
|
+
export type ReaderId = 'claude-cli' | 'codex-cli' | 'antigravity-cli' | 'hermes-cli';
|
|
23
|
+
|
|
24
|
+
export interface NativeHistoryInput {
|
|
25
|
+
agentType?: string;
|
|
26
|
+
sessionId?: string;
|
|
27
|
+
providerSessionId?: string;
|
|
28
|
+
historySessionId?: string;
|
|
29
|
+
workspace?: string;
|
|
30
|
+
format?: string;
|
|
31
|
+
watchPath?: string;
|
|
32
|
+
args?: Record<string, unknown>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface NativeHistoryResult {
|
|
36
|
+
messages: Array<{ role: string; content: string; receivedAt?: number; kind?: string }>;
|
|
37
|
+
providerSessionId?: string;
|
|
38
|
+
sourcePath: string;
|
|
39
|
+
sourceMtimeMs: number;
|
|
40
|
+
nativeHistoryCoverage?: 'full' | 'partial' | 'best-effort';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createNativeHistoryDispatcher(reader: ReaderId): (input: NativeHistoryInput) => NativeHistoryResult | null {
|
|
44
|
+
return (input: NativeHistoryInput) => {
|
|
45
|
+
const workspace = input.workspace || '';
|
|
46
|
+
const sessionId = input.sessionId || input.historySessionId || '';
|
|
47
|
+
// Caller may pass a providerSessionId (the *provider's own* id, e.g.
|
|
48
|
+
// the uuid claude writes into the jsonl basename). When present, the
|
|
49
|
+
// dispatcher only returns a transcript whose file id matches —
|
|
50
|
+
// otherwise the newest_by_mtime fallback can surface a different
|
|
51
|
+
// session's chat (round 9 part b: the user reported "previous chat
|
|
52
|
+
// shows up before I type anything" on every provider).
|
|
53
|
+
const requestedProviderSid = input.providerSessionId || '';
|
|
54
|
+
|
|
55
|
+
const sourcePath = resolveSourcePath(reader, workspace, sessionId);
|
|
56
|
+
if (!sourcePath) return null;
|
|
57
|
+
|
|
58
|
+
const session = readByReader(reader, sourcePath, sessionId, workspace);
|
|
59
|
+
if (!session) return null;
|
|
60
|
+
|
|
61
|
+
if (requestedProviderSid && session.providerSessionId && session.providerSessionId !== requestedProviderSid) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
messages: session.messages.map((m: any) => ({
|
|
67
|
+
role: normalizeRole(m.role),
|
|
68
|
+
content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
|
|
69
|
+
receivedAt: typeof m.receivedAt === 'number' ? m.receivedAt : Date.parse(m.timestamp || '') || Date.now(),
|
|
70
|
+
kind: typeof m.kind === 'string' ? m.kind : 'standard',
|
|
71
|
+
})),
|
|
72
|
+
providerSessionId: session.providerSessionId,
|
|
73
|
+
sourcePath: session.sourcePath,
|
|
74
|
+
sourceMtimeMs: session.sourceMtimeMs,
|
|
75
|
+
nativeHistoryCoverage: (session as any).nativeHistoryCoverage || 'full',
|
|
76
|
+
};
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// Per-provider path resolution
|
|
82
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function resolveSourcePath(reader: ReaderId, workspace: string, sessionId: string): string | null {
|
|
85
|
+
switch (reader) {
|
|
86
|
+
case 'claude-cli': return resolveClaudePath(workspace, sessionId);
|
|
87
|
+
case 'codex-cli': return resolveCodexPath(workspace);
|
|
88
|
+
case 'antigravity-cli': return resolveAntigravityPath(workspace);
|
|
89
|
+
case 'hermes-cli': return resolveHermesPath(workspace, sessionId);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function resolveClaudePath(workspace: string, sessionId: string): string | null {
|
|
94
|
+
// claude stores per-cwd: ~/.claude/projects/<cwd-as-dashes>/<uuid>.jsonl
|
|
95
|
+
const dir = path.join(os.homedir(), '.claude', 'projects', cwdAsDashes(workspace));
|
|
96
|
+
if (!fs.existsSync(dir)) return null;
|
|
97
|
+
if (sessionId) {
|
|
98
|
+
const candidate = path.join(dir, `${sessionId}.jsonl`);
|
|
99
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
100
|
+
}
|
|
101
|
+
// No exact match: return null. The newest_by_mtime fallback used to
|
|
102
|
+
// grab whichever transcript was touched most recently, but that
|
|
103
|
+
// surfaced a different (often the user's *external* claude session
|
|
104
|
+
// in the same workspace) chat in the dashboard before the user
|
|
105
|
+
// typed anything. The right answer is to show nothing until either
|
|
106
|
+
// the daemon's sessionId actually maps to a file or the caller
|
|
107
|
+
// passes the correct providerSessionId (round 9 part b).
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function resolveCodexPath(workspace: string): string | null {
|
|
112
|
+
void workspace;
|
|
113
|
+
// codex stores by UTC date: ~/.codex/sessions/<year>/<month>/<day>/<file>.jsonl
|
|
114
|
+
const now = new Date();
|
|
115
|
+
const dir = path.join(
|
|
116
|
+
os.homedir(), '.codex', 'sessions',
|
|
117
|
+
String(now.getUTCFullYear()),
|
|
118
|
+
String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
119
|
+
String(now.getUTCDate()).padStart(2, '0'),
|
|
120
|
+
);
|
|
121
|
+
if (fs.existsSync(dir)) {
|
|
122
|
+
const f = newestRecentFile(dir, /\.jsonl$/);
|
|
123
|
+
if (f) return f;
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function resolveAntigravityPath(workspace: string): string | null {
|
|
129
|
+
void workspace;
|
|
130
|
+
// agy brain/<uuid>/.system_generated/logs/transcript.jsonl
|
|
131
|
+
const brainRoot = path.join(os.homedir(), '.gemini', 'antigravity-cli', 'brain');
|
|
132
|
+
if (!fs.existsSync(brainRoot)) return null;
|
|
133
|
+
const cutoff = Date.now() - RECENT_WINDOW_MS;
|
|
134
|
+
const entries = fs.readdirSync(brainRoot, { withFileTypes: true })
|
|
135
|
+
.filter(e => e.isDirectory())
|
|
136
|
+
.map(e => ({ p: path.join(brainRoot, e.name), mtime: safeMtime(path.join(brainRoot, e.name)) }))
|
|
137
|
+
.filter(e => e.mtime >= cutoff)
|
|
138
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
139
|
+
for (const e of entries) {
|
|
140
|
+
const t = path.join(e.p, '.system_generated', 'logs', 'transcript.jsonl');
|
|
141
|
+
if (fs.existsSync(t)) return t;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function resolveHermesPath(workspace: string, sessionId: string): string | null {
|
|
147
|
+
void workspace; void sessionId;
|
|
148
|
+
// Hermes ≥ 0.14 persists all chat to ~/.hermes/state.db (SQLite). The
|
|
149
|
+
// db file is the "path" we hand to the reader — it pulls the newest
|
|
150
|
+
// source='cli' session inside readSession.
|
|
151
|
+
const dbPath = path.join(os.homedir(), '.hermes', 'state.db');
|
|
152
|
+
if (fs.existsSync(dbPath)) return dbPath;
|
|
153
|
+
// Legacy fallback: pre-db hermes wrote per-session JSON dumps.
|
|
154
|
+
const dir = path.join(os.homedir(), '.hermes', 'sessions');
|
|
155
|
+
if (!fs.existsSync(dir)) return null;
|
|
156
|
+
return newestRecentFile(dir, /^session_.*\.json$/);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
160
|
+
// Reader dispatch
|
|
161
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
162
|
+
|
|
163
|
+
function readByReader(
|
|
164
|
+
reader: ReaderId,
|
|
165
|
+
sourcePath: string,
|
|
166
|
+
sessionId: string,
|
|
167
|
+
workspace: string,
|
|
168
|
+
): any | null {
|
|
169
|
+
switch (reader) {
|
|
170
|
+
case 'claude-cli': return readClaudeCliSession(sourcePath);
|
|
171
|
+
case 'codex-cli': return readCodexCliSession(sourcePath);
|
|
172
|
+
case 'antigravity-cli': return readAntigravityCliSession(sourcePath, sessionId || undefined, workspace || undefined);
|
|
173
|
+
case 'hermes-cli': return readHermesCliSession(sourcePath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
178
|
+
// Helpers
|
|
179
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
function cwdAsDashes(cwd: string): string {
|
|
182
|
+
if (!cwd) return '';
|
|
183
|
+
return cwd.replace(/\//g, '-');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function newestFile(dir: string, pattern: RegExp): string | null {
|
|
187
|
+
try {
|
|
188
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
189
|
+
.filter(e => e.isFile() && pattern.test(e.name))
|
|
190
|
+
.map(e => ({ p: path.join(dir, e.name), mtime: safeMtime(path.join(dir, e.name)) }))
|
|
191
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
192
|
+
return entries[0]?.p ?? null;
|
|
193
|
+
} catch { return null; }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Like newestFile but only returns a candidate when its mtime is within
|
|
198
|
+
* the recent activity window. Prevents the dashboard from surfacing a
|
|
199
|
+
* prior session's transcript when the daemon's sessionId doesn't match
|
|
200
|
+
* any file on disk (e.g. claude allocates its own uuid; daemon and
|
|
201
|
+
* agent disagree about what the "current" session is).
|
|
202
|
+
*/
|
|
203
|
+
const RECENT_WINDOW_MS = 5 * 60 * 1000;
|
|
204
|
+
function newestRecentFile(dir: string, pattern: RegExp): string | null {
|
|
205
|
+
try {
|
|
206
|
+
const cutoff = Date.now() - RECENT_WINDOW_MS;
|
|
207
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
|
208
|
+
.filter(e => e.isFile() && pattern.test(e.name))
|
|
209
|
+
.map(e => ({ p: path.join(dir, e.name), mtime: safeMtime(path.join(dir, e.name)) }))
|
|
210
|
+
.filter(e => e.mtime >= cutoff)
|
|
211
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
212
|
+
return entries[0]?.p ?? null;
|
|
213
|
+
} catch { return null; }
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function safeMtime(p: string): number {
|
|
217
|
+
try { return Math.floor(fs.statSync(p).mtimeMs); } catch { return 0; }
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function normalizeRole(r: any): 'user' | 'assistant' | 'system' {
|
|
221
|
+
const s = String(r ?? '').toLowerCase();
|
|
222
|
+
if (s === 'user' || s === 'human') return 'user';
|
|
223
|
+
if (s === 'assistant' || s === 'ai' || s === 'model') return 'assistant';
|
|
224
|
+
// daemon's chat schema rejects 'tool'/'function' — surface as assistant.
|
|
225
|
+
if (s === 'tool' || s === 'tool_result' || s === 'function') return 'assistant';
|
|
226
|
+
return 'system';
|
|
227
|
+
}
|