@adhdev/daemon-core 0.9.82-rc.16 → 0.9.82-rc.161
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 +19 -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 +104 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +27 -0
- package/dist/commands/handler.d.ts +66 -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 +31 -6
- package/dist/index.js +17210 -3575
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +17127 -3551
- 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 +77 -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 +9 -0
- package/dist/providers/cli-provider-instance.d.ts +7 -2
- package/dist/providers/contracts.d.ts +98 -5
- 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-loader.d.ts +19 -1
- 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 +64 -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 +76 -0
- package/dist/providers/spec/driver.d.ts +148 -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 +507 -0
- package/dist/providers/spec/types.d.ts +211 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/repo-mesh-types.d.ts +100 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/dist/shared-types.d.ts +12 -0
- 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/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 +18 -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 +664 -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 +57 -11
- 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 +195 -3
- package/src/commands/handler.ts +547 -2
- package/src/commands/mesh-coordinator.ts +231 -127
- package/src/commands/router.ts +3547 -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 +91 -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 +820 -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.ts +39 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +388 -47
- package/src/providers/contracts.ts +105 -5
- package/src/providers/ide-provider-instance.ts +17 -3
- 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-loader.ts +420 -31
- package/src/providers/provider-schema.ts +31 -13
- 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 +245 -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 +457 -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 +119 -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 +153 -0
- package/src/providers/spec/cli-adapter.ts +318 -0
- package/src/providers/spec/driver.ts +498 -0
- package/src/providers/spec/evaluator.ts +268 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +612 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +507 -0
- package/src/providers/spec/schema.json +210 -0
- package/src/providers/spec/types.ts +230 -0
- package/src/providers/transcript-v2.ts +567 -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 +9 -0
- package/src/status/builders.ts +23 -6
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +35 -11
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
|
@@ -0,0 +1,612 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Declarative native-history executor.
|
|
3
|
+
*
|
|
4
|
+
* Reads a spec.json's `native_history.source` block and turns it into a
|
|
5
|
+
* NativeHistoryResult by talking directly to the on-disk store (jsonl
|
|
6
|
+
* file or sqlite db). No per-provider TypeScript reader required —
|
|
7
|
+
* adding a new provider is just authoring spec.json.
|
|
8
|
+
*
|
|
9
|
+
* Two source kinds:
|
|
10
|
+
* - jsonl — newest matching file inside a path (with variable expansion)
|
|
11
|
+
* + jsonpath-lite map for each record
|
|
12
|
+
* - sqlite — read-only better-sqlite3 handle, two queries (session pick
|
|
13
|
+
* + message fetch) + jsonpath-lite map for each row
|
|
14
|
+
*
|
|
15
|
+
* Exotic formats can still ship a provider-local reader via
|
|
16
|
+
* `native_history.override_path` — the dispatcher in provider-loader picks
|
|
17
|
+
* that path instead of calling this executor.
|
|
18
|
+
*/
|
|
19
|
+
'use strict';
|
|
20
|
+
|
|
21
|
+
import * as fs from 'node:fs';
|
|
22
|
+
import * as os from 'node:os';
|
|
23
|
+
import * as path from 'node:path';
|
|
24
|
+
import type {
|
|
25
|
+
NativeHistoryConfig,
|
|
26
|
+
NativeHistoryJsonlSource,
|
|
27
|
+
NativeHistoryMessageMap,
|
|
28
|
+
NativeHistorySqliteSource,
|
|
29
|
+
} from './types.js';
|
|
30
|
+
|
|
31
|
+
export interface NativeHistoryInput {
|
|
32
|
+
agentType?: string;
|
|
33
|
+
sessionId?: string;
|
|
34
|
+
providerSessionId?: string;
|
|
35
|
+
historySessionId?: string;
|
|
36
|
+
workspace?: string;
|
|
37
|
+
/** Daemon-side wall clock at the moment the session was registered.
|
|
38
|
+
* Native-history file lookups use this as the lower bound: any file
|
|
39
|
+
* whose mtime is before the current session started can't be from
|
|
40
|
+
* this session, so it's excluded from newest-recent matching. The
|
|
41
|
+
* caller (chat-history pipeline) populates this from the session
|
|
42
|
+
* registry; specs/executor never need to know how it's sourced. */
|
|
43
|
+
sessionStartedAtMs?: number;
|
|
44
|
+
/** Env overrides the daemon set on the spawned CLI. The mesh
|
|
45
|
+
* coordinator points hermes at a per-coordinator HERMES_HOME so
|
|
46
|
+
* the hermes process writes its state.db into a tmp directory
|
|
47
|
+
* instead of ~/.hermes. expandPath consults this map before
|
|
48
|
+
* process.env so the native-history reader follows the spawned
|
|
49
|
+
* child's view of HERMES_HOME / similar overrides; without it
|
|
50
|
+
* the reader would always look at ~/.hermes and miss every
|
|
51
|
+
* coordinator-session transcript. */
|
|
52
|
+
envOverrides?: Record<string, string>;
|
|
53
|
+
args?: Record<string, unknown>;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface NativeHistoryMessage {
|
|
57
|
+
role: 'user' | 'assistant' | 'system';
|
|
58
|
+
content: string;
|
|
59
|
+
receivedAt: number;
|
|
60
|
+
kind?: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface NativeHistoryResult {
|
|
64
|
+
messages: NativeHistoryMessage[];
|
|
65
|
+
providerSessionId?: string;
|
|
66
|
+
sourcePath: string;
|
|
67
|
+
sourceMtimeMs: number;
|
|
68
|
+
nativeHistoryCoverage?: 'full' | 'partial' | 'best-effort';
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const UUID_RE = /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})/i;
|
|
72
|
+
|
|
73
|
+
export function executeNativeHistory(cfg: NativeHistoryConfig, input: NativeHistoryInput): NativeHistoryResult | null {
|
|
74
|
+
if (!cfg?.source) return null;
|
|
75
|
+
if (cfg.source.kind === 'jsonl') return executeJsonl(cfg.source, input);
|
|
76
|
+
if (cfg.source.kind === 'sqlite') return executeSqlite(cfg.source, input);
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
81
|
+
// JSONL
|
|
82
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
function executeJsonl(src: NativeHistoryJsonlSource, input: NativeHistoryInput): NativeHistoryResult | null {
|
|
85
|
+
const resolved = expandPath(src.path, input);
|
|
86
|
+
if (!resolved) return null;
|
|
87
|
+
|
|
88
|
+
const windowMs = typeof src.recent_window_ms === 'number' ? src.recent_window_ms : 5 * 60_000;
|
|
89
|
+
const filePat = src.file_pattern ? globToRegex(src.file_pattern) : /.*\.jsonl$/;
|
|
90
|
+
|
|
91
|
+
// path can be:
|
|
92
|
+
// - a concrete file → used as-is
|
|
93
|
+
// - a concrete dir → newest matching file inside recent_window_ms
|
|
94
|
+
// - a path with `*` or `**` segments → walk all dirs that match the
|
|
95
|
+
// glob, pick the newest matching file across all matches. Lets
|
|
96
|
+
// specs like ~/.gemini/antigravity-cli/brain/*/.system_generated/logs
|
|
97
|
+
// resolve transparently without a per-provider override.
|
|
98
|
+
// The session-start cutoff guarantees a fresh dashboard view can't
|
|
99
|
+
// pick up a transcript file from a session that ended before this
|
|
100
|
+
// one started. recent_window_ms only controls how far back we'd
|
|
101
|
+
// otherwise look for a matching file; the session-start floor wins
|
|
102
|
+
// when it's later.
|
|
103
|
+
const sessionFloor = typeof input.sessionStartedAtMs === 'number' ? input.sessionStartedAtMs : 0;
|
|
104
|
+
let sourcePath: string | null = null;
|
|
105
|
+
if (resolved.includes('*')) {
|
|
106
|
+
sourcePath = newestRecentFileAcrossGlob(resolved, filePat, windowMs, sessionFloor);
|
|
107
|
+
} else {
|
|
108
|
+
let stat: fs.Stats | null = null;
|
|
109
|
+
try { stat = fs.statSync(resolved); } catch { return null; }
|
|
110
|
+
if (stat.isFile()) {
|
|
111
|
+
sourcePath = resolved;
|
|
112
|
+
} else if (stat.isDirectory()) {
|
|
113
|
+
sourcePath = newestRecentFile(resolved, filePat, windowMs, sessionFloor);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (!sourcePath) return null;
|
|
117
|
+
|
|
118
|
+
const mtime = safeMtimeMs(sourcePath);
|
|
119
|
+
const lines = readJsonlLines(sourcePath);
|
|
120
|
+
if (lines.length === 0) return null;
|
|
121
|
+
|
|
122
|
+
// session id: filename uuid or extracted from first record
|
|
123
|
+
let providerSessionId: string | undefined;
|
|
124
|
+
if (src.session_id_from === 'first_record' && src.session_id_path) {
|
|
125
|
+
const v = jsonPathGet(lines[0], src.session_id_path);
|
|
126
|
+
if (typeof v === 'string' && v) providerSessionId = v;
|
|
127
|
+
} else if (src.session_id_from === 'filename_uuid' || !src.session_id_from) {
|
|
128
|
+
const m = path.basename(sourcePath).match(UUID_RE);
|
|
129
|
+
if (m) providerSessionId = m[1];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const requested = input.providerSessionId || '';
|
|
133
|
+
if (requested && providerSessionId && providerSessionId !== requested) return null;
|
|
134
|
+
|
|
135
|
+
const filter = src.message_filter ? compileWhere(src.message_filter.where) : null;
|
|
136
|
+
const messages: NativeHistoryMessage[] = [];
|
|
137
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
138
|
+
const rec = lines[i];
|
|
139
|
+
if (filter && !filter(rec)) continue;
|
|
140
|
+
const msg = projectMessage(rec, src.message_map, i, lines.length, mtime);
|
|
141
|
+
if (msg) messages.push(msg);
|
|
142
|
+
}
|
|
143
|
+
if (messages.length === 0) return null;
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
messages,
|
|
147
|
+
providerSessionId,
|
|
148
|
+
sourcePath,
|
|
149
|
+
sourceMtimeMs: mtime,
|
|
150
|
+
nativeHistoryCoverage: 'full',
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function readJsonlLines(p: string): any[] {
|
|
155
|
+
let text: string;
|
|
156
|
+
try { text = fs.readFileSync(p, 'utf8'); } catch { return []; }
|
|
157
|
+
const out: any[] = [];
|
|
158
|
+
for (const line of text.split('\n')) {
|
|
159
|
+
const trimmed = line.trim();
|
|
160
|
+
if (!trimmed) continue;
|
|
161
|
+
try { out.push(JSON.parse(trimmed)); } catch { /* skip malformed line */ }
|
|
162
|
+
}
|
|
163
|
+
return out;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
167
|
+
// SQLite
|
|
168
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function executeSqlite(src: NativeHistorySqliteSource, input: NativeHistoryInput): NativeHistoryResult | null {
|
|
171
|
+
const resolved = expandPath(src.path, input);
|
|
172
|
+
if (!resolved || !fs.existsSync(resolved)) return null;
|
|
173
|
+
|
|
174
|
+
let Database: any;
|
|
175
|
+
try {
|
|
176
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
177
|
+
Database = require('better-sqlite3');
|
|
178
|
+
} catch { return null; }
|
|
179
|
+
|
|
180
|
+
let db: any;
|
|
181
|
+
try { db = new Database(resolved, { readonly: true, fileMustExist: true }); }
|
|
182
|
+
catch { return null; }
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
let sessionRow: any;
|
|
186
|
+
try {
|
|
187
|
+
// session_query may reference `?` to receive the session's
|
|
188
|
+
// start-time floor in seconds (e.g. WHERE started_at >= ?).
|
|
189
|
+
// That gives spec authors a robust way to keep prior-session
|
|
190
|
+
// rows out of a fresh dashboard view without inventing their
|
|
191
|
+
// own time arithmetic in SQL. When the caller didn't pass a
|
|
192
|
+
// session floor (i.e. no live session is associated with the
|
|
193
|
+
// call), we use 0 so spec queries that bind `?` still produce
|
|
194
|
+
// a sane result rather than choking the whole executor.
|
|
195
|
+
const sessionFloorSeconds = typeof input.sessionStartedAtMs === 'number'
|
|
196
|
+
? Math.floor(input.sessionStartedAtMs / 1000)
|
|
197
|
+
: 0;
|
|
198
|
+
const stmt = db.prepare(src.session_query);
|
|
199
|
+
try {
|
|
200
|
+
sessionRow = stmt.get(sessionFloorSeconds);
|
|
201
|
+
} catch {
|
|
202
|
+
sessionRow = stmt.get();
|
|
203
|
+
}
|
|
204
|
+
} catch { return null; }
|
|
205
|
+
if (!sessionRow) return null;
|
|
206
|
+
// First column of the first row is the session id.
|
|
207
|
+
const sessionIdRaw = Object.values(sessionRow)[0];
|
|
208
|
+
const sessionId = sessionIdRaw == null ? '' : String(sessionIdRaw);
|
|
209
|
+
if (!sessionId) return null;
|
|
210
|
+
|
|
211
|
+
const requested = input.providerSessionId || '';
|
|
212
|
+
if (requested && sessionId !== requested) return null;
|
|
213
|
+
|
|
214
|
+
const messageRows: any[] = db.prepare(src.message_query).all(sessionId);
|
|
215
|
+
if (!messageRows || messageRows.length === 0) return null;
|
|
216
|
+
|
|
217
|
+
const mtime = safeMtimeMs(resolved);
|
|
218
|
+
const messages: NativeHistoryMessage[] = [];
|
|
219
|
+
for (let i = 0; i < messageRows.length; i += 1) {
|
|
220
|
+
const msg = projectMessage(messageRows[i], src.message_map, i, messageRows.length, mtime);
|
|
221
|
+
if (msg) messages.push(msg);
|
|
222
|
+
}
|
|
223
|
+
if (messages.length === 0) return null;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
messages,
|
|
227
|
+
providerSessionId: sessionId,
|
|
228
|
+
sourcePath: resolved,
|
|
229
|
+
sourceMtimeMs: mtime,
|
|
230
|
+
nativeHistoryCoverage: 'full',
|
|
231
|
+
};
|
|
232
|
+
} finally {
|
|
233
|
+
try { db.close(); } catch { /* ignore */ }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
238
|
+
// Path expansion + globbing
|
|
239
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
240
|
+
|
|
241
|
+
function expandPath(template: string, input: NativeHistoryInput): string | null {
|
|
242
|
+
if (!template) return null;
|
|
243
|
+
let out = template;
|
|
244
|
+
if (out.startsWith('~/') || out === '~') {
|
|
245
|
+
out = path.join(os.homedir(), out.slice(2));
|
|
246
|
+
}
|
|
247
|
+
// ${VAR} expands from envOverrides (the spawned child's view) first,
|
|
248
|
+
// then process.env. ${VAR:-fallback} keeps the bash-style default
|
|
249
|
+
// so spec authors can say e.g. ${HERMES_HOME:-~/.hermes}/state.db
|
|
250
|
+
// and have it work both for coordinator-launched sessions (where
|
|
251
|
+
// HERMES_HOME is set to a tmpdir) and normal sessions.
|
|
252
|
+
out = out.replace(/\$\{([A-Z_][A-Z0-9_]*)(?::-(.*?))?\}/g, (_m, name, fallback) => {
|
|
253
|
+
const v = input.envOverrides?.[name] ?? process.env[name];
|
|
254
|
+
return v != null && v !== '' ? v : (fallback ?? '');
|
|
255
|
+
});
|
|
256
|
+
// Re-expand ~ in case the fallback used it.
|
|
257
|
+
if (out.startsWith('~/')) out = path.join(os.homedir(), out.slice(2));
|
|
258
|
+
const now = new Date();
|
|
259
|
+
const vars: Record<string, string> = {
|
|
260
|
+
cwd: input.workspace ?? '',
|
|
261
|
+
cwd_dashed: (input.workspace ?? '').replace(/\//g, '-'),
|
|
262
|
+
session_id: input.providerSessionId || input.sessionId || input.historySessionId || '',
|
|
263
|
+
yyyy: String(now.getUTCFullYear()),
|
|
264
|
+
mm: String(now.getUTCMonth() + 1).padStart(2, '0'),
|
|
265
|
+
dd: String(now.getUTCDate()).padStart(2, '0'),
|
|
266
|
+
};
|
|
267
|
+
// Replace {var}. If a referenced variable is empty (e.g. session_id
|
|
268
|
+
// before the agent has allocated one), return null so the caller
|
|
269
|
+
// doesn't accidentally fall through to an unrelated newest-file
|
|
270
|
+
// match. Wildcards (`*`) are explicitly allowed to pass — the dir
|
|
271
|
+
// glob walker handles them separately.
|
|
272
|
+
let missing = false;
|
|
273
|
+
out = out.replace(/\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g, (_m, name) => {
|
|
274
|
+
const v = vars[name] ?? '';
|
|
275
|
+
if (!v) missing = true;
|
|
276
|
+
return v;
|
|
277
|
+
});
|
|
278
|
+
if (missing) return null;
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function globToRegex(pattern: string): RegExp {
|
|
283
|
+
// Minimal glob: `*` → `[^/]*`, `?` → `[^/]`, `.` → `\.`. Anchored.
|
|
284
|
+
const re = pattern
|
|
285
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
286
|
+
.replace(/\*/g, '[^/]*')
|
|
287
|
+
.replace(/\?/g, '[^/]');
|
|
288
|
+
return new RegExp(`^${re}$`);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Resolve a path with `*` segments to all concrete directories that match,
|
|
293
|
+
* then pick the newest file inside any of them. `*` matches one path
|
|
294
|
+
* component (no slashes); `**` matches zero or more components. Filenames
|
|
295
|
+
* are matched against `pattern`, not the glob — file_pattern is the right
|
|
296
|
+
* place for the leaf match.
|
|
297
|
+
*/
|
|
298
|
+
function expandDirGlob(template: string): string[] {
|
|
299
|
+
const parts = template.split('/');
|
|
300
|
+
let dirs: string[] = parts[0] === '' ? ['/'] : [parts[0]];
|
|
301
|
+
for (let i = 1; i < parts.length; i += 1) {
|
|
302
|
+
const seg = parts[i];
|
|
303
|
+
if (!seg) continue;
|
|
304
|
+
const next: string[] = [];
|
|
305
|
+
if (seg === '**') {
|
|
306
|
+
for (const d of dirs) walkAllDirs(d, next);
|
|
307
|
+
dirs = next;
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (seg.includes('*') || seg.includes('?')) {
|
|
311
|
+
const re = globToRegex(seg);
|
|
312
|
+
for (const d of dirs) {
|
|
313
|
+
let entries: fs.Dirent[];
|
|
314
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
315
|
+
for (const e of entries) {
|
|
316
|
+
if (e.isDirectory() && re.test(e.name)) next.push(path.join(d, e.name));
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
for (const d of dirs) {
|
|
321
|
+
const candidate = path.join(d, seg);
|
|
322
|
+
let stat: fs.Stats | null = null;
|
|
323
|
+
try { stat = fs.statSync(candidate); } catch { continue; }
|
|
324
|
+
if (stat.isDirectory()) next.push(candidate);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
dirs = next;
|
|
328
|
+
}
|
|
329
|
+
return dirs;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function walkAllDirs(root: string, out: string[]): void {
|
|
333
|
+
let entries: fs.Dirent[];
|
|
334
|
+
try { entries = fs.readdirSync(root, { withFileTypes: true }); } catch { return; }
|
|
335
|
+
out.push(root);
|
|
336
|
+
for (const e of entries) {
|
|
337
|
+
if (e.isDirectory()) walkAllDirs(path.join(root, e.name), out);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function newestRecentFileAcrossGlob(template: string, pattern: RegExp, windowMs: number, sessionFloorMs = 0): string | null {
|
|
342
|
+
const dirs = expandDirGlob(template);
|
|
343
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs);
|
|
344
|
+
let best: { p: string; mtime: number } | null = null;
|
|
345
|
+
for (const d of dirs) {
|
|
346
|
+
let entries: fs.Dirent[];
|
|
347
|
+
try { entries = fs.readdirSync(d, { withFileTypes: true }); } catch { continue; }
|
|
348
|
+
for (const e of entries) {
|
|
349
|
+
if (!e.isFile() || !pattern.test(e.name)) continue;
|
|
350
|
+
const p = path.join(d, e.name);
|
|
351
|
+
const mtime = safeMtimeMs(p);
|
|
352
|
+
if (mtime < cutoff) continue;
|
|
353
|
+
if (!best || mtime > best.mtime) best = { p, mtime };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return best ? best.p : null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function newestRecentFile(dir: string, pattern: RegExp, windowMs: number, sessionFloorMs = 0): string | null {
|
|
360
|
+
let entries: fs.Dirent[];
|
|
361
|
+
try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return null; }
|
|
362
|
+
const cutoff = Math.max(Date.now() - windowMs, sessionFloorMs);
|
|
363
|
+
let best: { p: string; mtime: number } | null = null;
|
|
364
|
+
for (const e of entries) {
|
|
365
|
+
if (!e.isFile() || !pattern.test(e.name)) continue;
|
|
366
|
+
const p = path.join(dir, e.name);
|
|
367
|
+
const mtime = safeMtimeMs(p);
|
|
368
|
+
if (mtime < cutoff) continue;
|
|
369
|
+
if (!best || mtime > best.mtime) best = { p, mtime };
|
|
370
|
+
}
|
|
371
|
+
return best ? best.p : null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function safeMtimeMs(p: string): number {
|
|
375
|
+
try { return Math.floor(fs.statSync(p).mtimeMs); } catch { return 0; }
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
379
|
+
// jsonpath-lite + projection
|
|
380
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Resolve `$.a.b[0].c` against a record. Strings without leading `$` are
|
|
384
|
+
* literals. Supports `||` fallback between paths so a single message_map
|
|
385
|
+
* entry can pick the first non-empty value across alternative locations
|
|
386
|
+
* (e.g. agy's content vs. thinking).
|
|
387
|
+
*/
|
|
388
|
+
function jsonPathGet(record: any, expr: string): unknown {
|
|
389
|
+
if (typeof expr !== 'string') return undefined;
|
|
390
|
+
if (expr.includes('||')) {
|
|
391
|
+
for (const alt of expr.split('||')) {
|
|
392
|
+
const v = jsonPathGet(record, alt.trim());
|
|
393
|
+
if (v != null && v !== '') return v;
|
|
394
|
+
}
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
if (!expr.startsWith('$')) return expr;
|
|
398
|
+
let cur: any = record;
|
|
399
|
+
let i = 1;
|
|
400
|
+
while (i < expr.length && cur != null) {
|
|
401
|
+
const ch = expr[i];
|
|
402
|
+
if (ch === '.') { i += 1; continue; }
|
|
403
|
+
if (ch === '[') {
|
|
404
|
+
const close = expr.indexOf(']', i);
|
|
405
|
+
if (close < 0) return undefined;
|
|
406
|
+
const idx = Number(expr.slice(i + 1, close));
|
|
407
|
+
if (!Number.isInteger(idx)) return undefined;
|
|
408
|
+
cur = cur[idx];
|
|
409
|
+
i = close + 1;
|
|
410
|
+
continue;
|
|
411
|
+
}
|
|
412
|
+
let end = i;
|
|
413
|
+
while (end < expr.length && expr[end] !== '.' && expr[end] !== '[') end += 1;
|
|
414
|
+
const key = expr.slice(i, end);
|
|
415
|
+
cur = cur[key];
|
|
416
|
+
i = end;
|
|
417
|
+
}
|
|
418
|
+
return cur;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function projectMessage(record: any, map: NativeHistoryMessageMap, index: number, total: number, sourceMtimeMs: number): NativeHistoryMessage | null {
|
|
422
|
+
const roleRaw = jsonPathGet(record, map.role);
|
|
423
|
+
const contentRaw = jsonPathGet(record, map.content);
|
|
424
|
+
const role = normalizeRole(roleRaw);
|
|
425
|
+
let content = stringifyContent(contentRaw);
|
|
426
|
+
if (content && map.content_strip) {
|
|
427
|
+
for (const tag of map.content_strip) {
|
|
428
|
+
const safeTag = tag.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
429
|
+
const re = new RegExp(`<${safeTag}\\b[^>]*>[\\s\\S]*?<\\/${safeTag}\\s*>`, 'gi');
|
|
430
|
+
content = content.replace(re, '');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (content && map.content_unwrap) {
|
|
434
|
+
for (const tag of map.content_unwrap) {
|
|
435
|
+
const safeTag = tag.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
436
|
+
const open = new RegExp(`<${safeTag}\\b[^>]*>`, 'gi');
|
|
437
|
+
const close = new RegExp(`<\\/${safeTag}\\s*>`, 'gi');
|
|
438
|
+
content = content.replace(open, '').replace(close, '');
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
if (content) content = content.trim();
|
|
442
|
+
if (!content) return null;
|
|
443
|
+
|
|
444
|
+
// Records are passed in chronological order (oldest → newest), so the
|
|
445
|
+
// last record's receivedAt should be ~sourceMtimeMs (when the file was
|
|
446
|
+
// last touched) and earlier records should walk backwards. Earlier
|
|
447
|
+
// version had this inverted, which made the dashboard render bubbles
|
|
448
|
+
// in reverse order and produce the "chat jumping" effect.
|
|
449
|
+
let receivedAt = sourceMtimeMs - ((total - 1 - index) * 1000);
|
|
450
|
+
if (map.timestamp_ms) {
|
|
451
|
+
const tsRaw = jsonPathGet(record, map.timestamp_ms);
|
|
452
|
+
const parsed = parseTimestamp(tsRaw);
|
|
453
|
+
if (parsed != null) receivedAt = parsed;
|
|
454
|
+
}
|
|
455
|
+
const kindRaw = map.kind ? jsonPathGet(record, map.kind) : undefined;
|
|
456
|
+
const kind = typeof kindRaw === 'string' && kindRaw ? kindRaw : 'standard';
|
|
457
|
+
return { role, content, receivedAt, kind };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* Coerce a timestamp value to epoch milliseconds. Accepts:
|
|
462
|
+
* - number (ms or seconds — heuristic: < 1e12 means seconds)
|
|
463
|
+
* - ISO 8601 string ("2026-06-05T01:25:28Z")
|
|
464
|
+
* - numeric string
|
|
465
|
+
*
|
|
466
|
+
* Returns null when the value can't be parsed; caller falls back to the
|
|
467
|
+
* monotonic-by-index estimate.
|
|
468
|
+
*/
|
|
469
|
+
function parseTimestamp(v: unknown): number | null {
|
|
470
|
+
if (v == null) return null;
|
|
471
|
+
if (typeof v === 'number' && Number.isFinite(v) && v > 0) {
|
|
472
|
+
return v < 1e12 ? Math.floor(v * 1000) : Math.floor(v);
|
|
473
|
+
}
|
|
474
|
+
if (typeof v === 'string' && v) {
|
|
475
|
+
const trimmed = v.trim();
|
|
476
|
+
const asNum = Number(trimmed);
|
|
477
|
+
if (Number.isFinite(asNum) && asNum > 0) {
|
|
478
|
+
return asNum < 1e12 ? Math.floor(asNum * 1000) : Math.floor(asNum);
|
|
479
|
+
}
|
|
480
|
+
const parsed = Date.parse(trimmed);
|
|
481
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
482
|
+
}
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function normalizeRole(r: unknown): 'user' | 'assistant' | 'system' {
|
|
487
|
+
const s = String(r ?? '').toLowerCase();
|
|
488
|
+
if (s === 'user' || s === 'human' || s === 'user_explicit') return 'user';
|
|
489
|
+
if (s === 'assistant' || s === 'ai' || s === 'model') return 'assistant';
|
|
490
|
+
if (s === 'tool' || s === 'tool_result' || s === 'function') return 'assistant';
|
|
491
|
+
return 'system';
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Coerce a content value to a plain string the dashboard can render.
|
|
496
|
+
*
|
|
497
|
+
* Many providers ship structured content (claude messages are arrays of
|
|
498
|
+
* typed blocks: text / tool_use / tool_result). We collapse those to
|
|
499
|
+
* their text-bearing parts so the dashboard doesn't show raw JSON
|
|
500
|
+
* fragments in the transcript. Tool calls/results are intentionally
|
|
501
|
+
* dropped — the daemon's chat schema is for user-visible turns.
|
|
502
|
+
*
|
|
503
|
+
* Order of attempts:
|
|
504
|
+
* 1. string → as-is
|
|
505
|
+
* 2. array of blocks → join the `text` field of each
|
|
506
|
+
* block that has one; if none have
|
|
507
|
+
* a text field, fall through
|
|
508
|
+
* 3. object with a top-level `text` → that string
|
|
509
|
+
* 4. last resort → JSON.stringify
|
|
510
|
+
*/
|
|
511
|
+
function stringifyContent(v: unknown): string {
|
|
512
|
+
if (v == null) return '';
|
|
513
|
+
if (typeof v === 'string') return v;
|
|
514
|
+
if (Array.isArray(v)) {
|
|
515
|
+
const parts: string[] = [];
|
|
516
|
+
for (const block of v) {
|
|
517
|
+
if (block == null) continue;
|
|
518
|
+
if (typeof block === 'string') { parts.push(block); continue; }
|
|
519
|
+
if (typeof block === 'object') {
|
|
520
|
+
const t = (block as any).text;
|
|
521
|
+
if (typeof t === 'string' && t) { parts.push(t); continue; }
|
|
522
|
+
// tool_use / tool_result / image / etc — skip from the
|
|
523
|
+
// user-facing transcript. They re-surface via the
|
|
524
|
+
// adapter's tool-event channel if/when that's wired.
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
if (parts.length > 0) return parts.join('\n');
|
|
528
|
+
return '';
|
|
529
|
+
}
|
|
530
|
+
if (typeof v === 'object') {
|
|
531
|
+
const t = (v as any).text;
|
|
532
|
+
if (typeof t === 'string') return t;
|
|
533
|
+
}
|
|
534
|
+
try { return JSON.stringify(v); } catch { return String(v); }
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
538
|
+
// where-clause mini-language
|
|
539
|
+
//
|
|
540
|
+
// Grammar (intentionally tiny — keep spec readable):
|
|
541
|
+
// expr := term (('&&' | '||') term)*
|
|
542
|
+
// term := path op literal
|
|
543
|
+
// op := '==' | '!=' | '>' | '<' | '>=' | '<='
|
|
544
|
+
// path := jsonpath like '$.foo.bar[0].baz'
|
|
545
|
+
// literal := string ('"…"' or "'…'") | number | true | false | null
|
|
546
|
+
//
|
|
547
|
+
// No grouping, no negation. If you need more, write a real reader file
|
|
548
|
+
// behind `native_history.override_path`.
|
|
549
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
550
|
+
|
|
551
|
+
interface WhereTerm { path: string; op: string; lit: unknown; negate?: boolean }
|
|
552
|
+
|
|
553
|
+
function compileWhere(src: string): (record: any) => boolean {
|
|
554
|
+
const ors: WhereTerm[][] = [];
|
|
555
|
+
for (const orChunk of src.split('||')) {
|
|
556
|
+
const ands: WhereTerm[] = [];
|
|
557
|
+
for (const andChunk of orChunk.split('&&')) {
|
|
558
|
+
const term = parseTerm(andChunk.trim());
|
|
559
|
+
if (term) ands.push(term);
|
|
560
|
+
}
|
|
561
|
+
if (ands.length > 0) ors.push(ands);
|
|
562
|
+
}
|
|
563
|
+
return (record: any) => ors.some(ands => ands.every(t => evalTerm(t, record)));
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
function parseTerm(src: string): WhereTerm | null {
|
|
567
|
+
let s = src.trim();
|
|
568
|
+
let negate = false;
|
|
569
|
+
if (s.startsWith('!')) { negate = true; s = s.slice(1).trim(); }
|
|
570
|
+
// Function-call form: startsWith($.x, "y") / endsWith($.x, "y") / contains($.x, "y")
|
|
571
|
+
const fnMatch = s.match(/^(startsWith|endsWith|contains)\s*\(\s*(.+?)\s*,\s*(.+?)\s*\)$/);
|
|
572
|
+
if (fnMatch) {
|
|
573
|
+
const [, op, pathExpr, litExpr] = fnMatch;
|
|
574
|
+
return { path: pathExpr, op, lit: parseLiteral(litExpr), negate };
|
|
575
|
+
}
|
|
576
|
+
// Binary comparison: <path> <op> <literal>
|
|
577
|
+
const opMatch = s.match(/^(.+?)\s*(==|!=|>=|<=|>|<)\s*(.+)$/);
|
|
578
|
+
if (!opMatch) return null;
|
|
579
|
+
const [, lhs, op, rhsRaw] = opMatch;
|
|
580
|
+
return { path: lhs.trim(), op, lit: parseLiteral(rhsRaw.trim()), negate };
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function parseLiteral(src: string): unknown {
|
|
584
|
+
if (src === 'true') return true;
|
|
585
|
+
if (src === 'false') return false;
|
|
586
|
+
if (src === 'null') return null;
|
|
587
|
+
if ((src.startsWith('"') && src.endsWith('"')) || (src.startsWith("'") && src.endsWith("'"))) {
|
|
588
|
+
return src.slice(1, -1);
|
|
589
|
+
}
|
|
590
|
+
const n = Number(src);
|
|
591
|
+
if (!Number.isNaN(n)) return n;
|
|
592
|
+
return src; // bareword — treated as string
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function evalTerm(t: WhereTerm, record: any): boolean {
|
|
596
|
+
const lhs = jsonPathGet(record, t.path);
|
|
597
|
+
const lit = t.lit;
|
|
598
|
+
let result: boolean;
|
|
599
|
+
switch (t.op) {
|
|
600
|
+
case '==': result = lhs === lit; break;
|
|
601
|
+
case '!=': result = lhs !== lit; break;
|
|
602
|
+
case '>': result = Number(lhs) > Number(lit); break;
|
|
603
|
+
case '<': result = Number(lhs) < Number(lit); break;
|
|
604
|
+
case '>=': result = Number(lhs) >= Number(lit); break;
|
|
605
|
+
case '<=': result = Number(lhs) <= Number(lit); break;
|
|
606
|
+
case 'startsWith': result = typeof lhs === 'string' && typeof lit === 'string' && lhs.startsWith(lit); break;
|
|
607
|
+
case 'endsWith': result = typeof lhs === 'string' && typeof lit === 'string' && lhs.endsWith(lit); break;
|
|
608
|
+
case 'contains': result = typeof lhs === 'string' && typeof lit === 'string' && lhs.includes(lit); break;
|
|
609
|
+
default: result = false;
|
|
610
|
+
}
|
|
611
|
+
return t.negate ? !result : result;
|
|
612
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Routing — given a CLI provider definition, decide whether to drive
|
|
3
|
+
* it via the legacy ProviderCliAdapter (scripts/v1/*.js parsers +
|
|
4
|
+
* cli-state-engine) or via SpecCliAdapter (single spec.json +
|
|
5
|
+
* SpecDriver). The gate is the presence of spec.json in the resolved
|
|
6
|
+
* provider directory.
|
|
7
|
+
*
|
|
8
|
+
* This is the only place in the daemon that knows which path a given
|
|
9
|
+
* provider takes. Providers can be migrated one at a time by adding
|
|
10
|
+
* a spec.json next to their scripts/v1/, then deleting scripts/v1/.
|
|
11
|
+
*/
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
import * as fs from 'node:fs';
|
|
15
|
+
import * as path from 'node:path';
|
|
16
|
+
import { ProviderCliAdapter } from '../../cli-adapters/provider-cli-adapter.js';
|
|
17
|
+
import type { CliProviderModule } from '../../cli-adapters/provider-cli-adapter.js';
|
|
18
|
+
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
19
|
+
import type { CliAdapter } from '../../cli-adapter-types.js';
|
|
20
|
+
import { SpecCliAdapter } from './cli-adapter.js';
|
|
21
|
+
import { LOG } from '../../logging/logger.js';
|
|
22
|
+
|
|
23
|
+
export function createCliAdapter(
|
|
24
|
+
provider: CliProviderModule,
|
|
25
|
+
workingDir: string,
|
|
26
|
+
cliArgs: string[],
|
|
27
|
+
extraEnv: Record<string, string>,
|
|
28
|
+
transportFactory?: PtyTransportFactory,
|
|
29
|
+
): CliAdapter {
|
|
30
|
+
// Prefer the path provider-loader already resolved (it walks the
|
|
31
|
+
// compatibility[i].spec → specs/default.json → spec.json chain).
|
|
32
|
+
// Fall back to the legacy spec.json-in-provider-dir lookup if the
|
|
33
|
+
// loader didn't attach one — keeps out-of-tree providers that still
|
|
34
|
+
// use the original layout working.
|
|
35
|
+
const resolvedSpecPath = (provider as unknown as { _resolvedSpecPath?: string })._resolvedSpecPath;
|
|
36
|
+
const dir = (provider as unknown as { _resolvedProviderDir?: string })._resolvedProviderDir;
|
|
37
|
+
let specPath: string | undefined = resolvedSpecPath && fs.existsSync(resolvedSpecPath) ? resolvedSpecPath : undefined;
|
|
38
|
+
if (!specPath && dir) {
|
|
39
|
+
const legacy = path.join(dir, 'spec.json');
|
|
40
|
+
if (fs.existsSync(legacy)) specPath = legacy;
|
|
41
|
+
}
|
|
42
|
+
if (specPath) {
|
|
43
|
+
try {
|
|
44
|
+
LOG.info('spec-route', `[${provider.type}] routing through SpecCliAdapter (${path.relative(dir || '', specPath) || specPath})`);
|
|
45
|
+
return new SpecCliAdapter(specPath, workingDir, cliArgs, extraEnv, transportFactory);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
LOG.warn('spec-route', `[${provider.type}] spec invalid, falling back to ProviderCliAdapter: ${(err as Error).message}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return new ProviderCliAdapter(provider, workingDir, cliArgs, extraEnv, transportFactory) as unknown as CliAdapter;
|
|
51
|
+
}
|