@adhdev/daemon-core 0.9.82-rc.2 → 0.9.82-rc.200
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/daemon-lifecycle.d.ts +3 -0
- 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 +23 -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/cli-manager.d.ts +2 -1
- package/dist/commands/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +29 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +72 -1
- package/dist/git/git-commands.d.ts +8 -1
- package/dist/git/git-status.d.ts +5 -0
- package/dist/git/git-types.d.ts +12 -0
- package/dist/index.d.ts +39 -10
- package/dist/index.js +29425 -12026
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +25700 -8378
- 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/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-delivery-policy.d.ts +126 -0
- package/dist/mesh/mesh-events.d.ts +167 -6
- package/dist/mesh/mesh-fast-forward.d.ts +41 -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-runtime-store.d.ts +149 -0
- package/dist/mesh/mesh-work-queue.d.ts +55 -6
- 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/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +39 -3
- package/dist/providers/contracts.d.ts +130 -6
- 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 +27 -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-manager.d.ts +12 -0
- package/dist/providers/provider-instance.d.ts +13 -1
- 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 +203 -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 +60 -0
- package/dist/providers/spec/cli-adapter.d.ts +98 -0
- package/dist/providers/spec/driver.d.ts +190 -0
- package/dist/providers/spec/evaluator.d.ts +55 -0
- package/dist/providers/spec/loader.d.ts +14 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/schema.gen.d.ts +599 -0
- package/dist/providers/spec/types.d.ts +282 -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 +230 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/dist/shared-types-extra.d.ts +1 -1
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -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 +22 -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 +20 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +22 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1138 -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 +1787 -60
- package/src/commands/cli-manager.ts +283 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +331 -122
- package/src/commands/router.ts +3915 -546
- package/src/config/chat-history.ts +95 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +280 -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 +38 -8
- package/src/git/git-status.ts +132 -12
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +13 -0
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +127 -9
- 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/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-delivery-policy.ts +298 -0
- package/src/mesh/mesh-events.ts +1352 -109
- package/src/mesh/mesh-fast-forward.ts +438 -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-runtime-store.ts +769 -0
- package/src/mesh/mesh-work-queue.ts +267 -159
- 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 +7 -12
- package/src/providers/cli-provider-instance.ts +806 -64
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- 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 +340 -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-manager.ts +30 -0
- package/src/providers/provider-instance.ts +10 -1
- package/src/providers/provider-loader.ts +582 -50
- package/src/providers/provider-schema.ts +87 -14
- 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 +504 -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 +229 -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 +176 -0
- package/src/providers/spec/cli-adapter.ts +538 -0
- package/src/providers/spec/driver.ts +725 -0
- package/src/providers/spec/evaluator.ts +373 -0
- package/src/providers/spec/loader.ts +130 -0
- package/src/providers/spec/native-history-executor.ts +939 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +559 -0
- package/src/providers/spec/schema.json +237 -0
- package/src/providers/spec/types.ts +301 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +425 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +250 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types-extra.ts +1 -1
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -6
- package/src/status/normalize.ts +2 -0
- 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
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spec evaluator — pure function.
|
|
3
|
+
*
|
|
4
|
+
* Given a visible screen text and a CliSpec, returns:
|
|
5
|
+
* - which sections cover which line ranges
|
|
6
|
+
* - which state matched and why
|
|
7
|
+
* - extracted modal title + buttons (if any)
|
|
8
|
+
* - which control_bar entries are visible
|
|
9
|
+
* - which notifications/delegates this evaluation activates
|
|
10
|
+
* - a trace object that explains every decision (for the inspector)
|
|
11
|
+
*
|
|
12
|
+
* No I/O, no state.
|
|
13
|
+
*/
|
|
14
|
+
'use strict';
|
|
15
|
+
|
|
16
|
+
import type {
|
|
17
|
+
CliSpec, Section, SectionRegex, SectionPattern, SpecState,
|
|
18
|
+
ControlAction, NotificationRule, DelegateTrigger,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
|
|
21
|
+
export interface ResolvedSection {
|
|
22
|
+
id: string;
|
|
23
|
+
fromLine: number; // inclusive
|
|
24
|
+
toLine: number; // exclusive
|
|
25
|
+
text: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ModalSnapshot {
|
|
29
|
+
title: string | null;
|
|
30
|
+
buttons: { index: number; label: string; key: string }[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface VisibleControl {
|
|
34
|
+
id: string;
|
|
35
|
+
label: string;
|
|
36
|
+
actionType: 'send_keys' | 'open_picker' | 'attach_image';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface FiredNotification {
|
|
40
|
+
id: string;
|
|
41
|
+
title: string;
|
|
42
|
+
body: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface FiredDelegate {
|
|
46
|
+
id: string;
|
|
47
|
+
task: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TraceEntry {
|
|
51
|
+
kind: 'section' | 'state_match' | 'state_skip' | 'modal' | 'control' | 'notification' | 'delegate';
|
|
52
|
+
text: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface SpecEvaluation {
|
|
56
|
+
state: { id: string; label: string; title: string | null };
|
|
57
|
+
modal: ModalSnapshot | null;
|
|
58
|
+
controls: VisibleControl[];
|
|
59
|
+
notifications: FiredNotification[];
|
|
60
|
+
delegates: FiredDelegate[];
|
|
61
|
+
sections: ResolvedSection[];
|
|
62
|
+
trace: TraceEntry[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
// Layout
|
|
67
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
function resolveSize(size: number | string | undefined, total: number): number {
|
|
70
|
+
if (size === undefined) return 0;
|
|
71
|
+
if (typeof size === 'number') return Math.max(0, Math.min(total, size));
|
|
72
|
+
const m = /^(\d+(?:\.\d+)?)%$/.exec(size);
|
|
73
|
+
if (!m) return 0;
|
|
74
|
+
const pct = Number(m[1]);
|
|
75
|
+
return Math.max(0, Math.min(total, Math.round((total * pct) / 100)));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function resolveSections(spec: CliSpec, lines: string[]): ResolvedSection[] {
|
|
79
|
+
const total = lines.length;
|
|
80
|
+
const resolved: ResolvedSection[] = [];
|
|
81
|
+
// Two-pass: first resolve from_top / from_bottom without `until`.
|
|
82
|
+
const anchored = new Map<string, { fromLine: number; toLine: number }>();
|
|
83
|
+
|
|
84
|
+
for (const sec of spec.layout.sections) {
|
|
85
|
+
let from = 0;
|
|
86
|
+
let to = total;
|
|
87
|
+
if (sec.anchor_regex !== undefined) {
|
|
88
|
+
try {
|
|
89
|
+
const re = new RegExp(sec.anchor_regex, sec.anchor_flags ?? '');
|
|
90
|
+
const prevRe = sec.anchor_context?.prev !== undefined
|
|
91
|
+
? new RegExp(sec.anchor_context.prev, sec.anchor_context.prev_flags ?? '') : null;
|
|
92
|
+
const nextRe = sec.anchor_context?.next !== undefined
|
|
93
|
+
? new RegExp(sec.anchor_context.next, sec.anchor_context.next_flags ?? '') : null;
|
|
94
|
+
const matches = (i: number) => re.test(lines[i])
|
|
95
|
+
&& (prevRe === null || (i > 0 && prevRe.test(lines[i - 1])))
|
|
96
|
+
&& (nextRe === null || (i < total - 1 && nextRe.test(lines[i + 1])));
|
|
97
|
+
let idx = -1;
|
|
98
|
+
if (sec.anchor_last) {
|
|
99
|
+
for (let i = total - 1; i >= 0; i--) { if (matches(i)) { idx = i; break; } }
|
|
100
|
+
} else {
|
|
101
|
+
for (let i = 0; i < total; i++) { if (matches(i)) { idx = i; break; } }
|
|
102
|
+
}
|
|
103
|
+
if (idx !== -1) {
|
|
104
|
+
from = idx;
|
|
105
|
+
to = total;
|
|
106
|
+
if (sec.until_regex !== undefined) {
|
|
107
|
+
try {
|
|
108
|
+
const ure = new RegExp(sec.until_regex, sec.until_regex_flags ?? '');
|
|
109
|
+
const end = lines.findIndex((l, i) => i > idx && ure.test(l));
|
|
110
|
+
if (end !== -1) to = end;
|
|
111
|
+
} catch { /* bad until_regex — extend to end */ }
|
|
112
|
+
} else if (sec.lines !== undefined) {
|
|
113
|
+
to = Math.min(total, from + sec.lines);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
} catch { /* bad anchor_regex — fall through to defaults */ }
|
|
117
|
+
} else if (sec.from_top !== undefined) {
|
|
118
|
+
from = resolveSize(sec.from_top, total);
|
|
119
|
+
} else if (sec.from_bottom !== undefined) {
|
|
120
|
+
const sz = resolveSize(sec.from_bottom, total);
|
|
121
|
+
from = total - sz;
|
|
122
|
+
to = total;
|
|
123
|
+
}
|
|
124
|
+
anchored.set(sec.id, { fromLine: from, toLine: to });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Second pass: apply `until` references.
|
|
128
|
+
for (const sec of spec.layout.sections) {
|
|
129
|
+
let { fromLine, toLine } = anchored.get(sec.id)!;
|
|
130
|
+
if (sec.until) {
|
|
131
|
+
const target = anchored.get(sec.until.section);
|
|
132
|
+
if (target) toLine = target.fromLine;
|
|
133
|
+
}
|
|
134
|
+
if (toLine < fromLine) toLine = fromLine;
|
|
135
|
+
const text = lines.slice(fromLine, toLine).join('\n');
|
|
136
|
+
resolved.push({ id: sec.id, fromLine, toLine, text });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return resolved;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function sectionText(sections: ResolvedSection[], sectionId: string | undefined, fullScreen: string): string {
|
|
143
|
+
if (!sectionId) return fullScreen;
|
|
144
|
+
const found = sections.find(s => s.id === sectionId);
|
|
145
|
+
return found ? found.text : '';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function compileRegex(ref: { regex: string; flags?: string }): RegExp {
|
|
149
|
+
return new RegExp(ref.regex, ref.flags ?? 'i');
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function compilePattern(ref: { pattern: string; flags?: string }): RegExp {
|
|
153
|
+
const flags = ref.flags ?? 'gm';
|
|
154
|
+
return new RegExp(ref.pattern, flags.includes('g') ? flags : flags + 'g');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function compileLinePattern(ref: { pattern: string; flags?: string }): RegExp {
|
|
158
|
+
const flags = (ref.flags ?? 'm').replace(/g/g, '');
|
|
159
|
+
return new RegExp(ref.pattern, flags);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
163
|
+
// State matching
|
|
164
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
function matchState(
|
|
167
|
+
state: SpecState,
|
|
168
|
+
sections: ResolvedSection[],
|
|
169
|
+
fullScreen: string,
|
|
170
|
+
trace: TraceEntry[],
|
|
171
|
+
cursor?: { row: number; col: number },
|
|
172
|
+
): { matched: boolean; title: string | null } {
|
|
173
|
+
const haystack = sectionText(sections, state.when.section, fullScreen);
|
|
174
|
+
const re = compileRegex(state.when);
|
|
175
|
+
if (!re.test(haystack)) {
|
|
176
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] when ${state.when.section ?? '*'}~/${state.when.regex}/ no match` });
|
|
177
|
+
return { matched: false, title: null };
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Cursor-position guards: check row/col bounds when the state declares them.
|
|
181
|
+
// Guards are skipped entirely when the caller did not supply a cursor position
|
|
182
|
+
// (cursor === undefined) so existing pure-text evaluation is unaffected.
|
|
183
|
+
if (cursor !== undefined) {
|
|
184
|
+
const w = state.when;
|
|
185
|
+
if (w.cursor_row_min !== undefined && cursor.row < w.cursor_row_min) {
|
|
186
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor row ${cursor.row} < cursor_row_min ${w.cursor_row_min}` });
|
|
187
|
+
return { matched: false, title: null };
|
|
188
|
+
}
|
|
189
|
+
if (w.cursor_row_max !== undefined && cursor.row > w.cursor_row_max) {
|
|
190
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor row ${cursor.row} > cursor_row_max ${w.cursor_row_max}` });
|
|
191
|
+
return { matched: false, title: null };
|
|
192
|
+
}
|
|
193
|
+
if (w.cursor_col_min !== undefined && cursor.col < w.cursor_col_min) {
|
|
194
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor col ${cursor.col} < cursor_col_min ${w.cursor_col_min}` });
|
|
195
|
+
return { matched: false, title: null };
|
|
196
|
+
}
|
|
197
|
+
if (w.cursor_col_max !== undefined && cursor.col > w.cursor_col_max) {
|
|
198
|
+
trace.push({ kind: 'state_skip', text: `state[${state.id}] cursor col ${cursor.col} > cursor_col_max ${w.cursor_col_max}` });
|
|
199
|
+
return { matched: false, title: null };
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
trace.push({ kind: 'state_match', text: `state[${state.id}] matched via ${state.when.section ?? '*'}~/${state.when.regex}/${cursor !== undefined ? ` cursor=(${cursor.row},${cursor.col})` : ''}` });
|
|
204
|
+
|
|
205
|
+
let title: string | null = null;
|
|
206
|
+
if (state.extract_title) {
|
|
207
|
+
const titleHay = sectionText(sections, state.extract_title.section, fullScreen);
|
|
208
|
+
const m = compileRegex(state.extract_title).exec(titleHay);
|
|
209
|
+
if (m) title = (m[1] ?? m[0]).trim();
|
|
210
|
+
trace.push({ kind: 'state_match', text: `state[${state.id}] extract_title → ${title ?? '(none)'}` });
|
|
211
|
+
}
|
|
212
|
+
return { matched: true, title };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function extractModal(
|
|
216
|
+
state: SpecState,
|
|
217
|
+
sections: ResolvedSection[],
|
|
218
|
+
fullScreen: string,
|
|
219
|
+
title: string | null,
|
|
220
|
+
trace: TraceEntry[],
|
|
221
|
+
): ModalSnapshot | null {
|
|
222
|
+
if (!state.modal_buttons) return null;
|
|
223
|
+
const hay = sectionText(sections, state.modal_buttons.section, fullScreen);
|
|
224
|
+
const buttons: { index: number; label: string; key: string }[] = [];
|
|
225
|
+
if (state.modal_buttons.continuation_lines) {
|
|
226
|
+
const re = compileLinePattern(state.modal_buttons);
|
|
227
|
+
const lines = hay.split('\n');
|
|
228
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
229
|
+
const m = re.exec(lines[i]);
|
|
230
|
+
if (!m) continue;
|
|
231
|
+
const idx = Number(m[1]);
|
|
232
|
+
let label = String(m[2] ?? '').trim();
|
|
233
|
+
if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
|
|
234
|
+
let j = i + 1;
|
|
235
|
+
while (j < lines.length) {
|
|
236
|
+
const next = lines[j];
|
|
237
|
+
if (!next.trim()) break;
|
|
238
|
+
if (re.test(next)) break;
|
|
239
|
+
if (!/^\s+/.test(next)) break;
|
|
240
|
+
label += ' ' + next.trim();
|
|
241
|
+
j += 1;
|
|
242
|
+
}
|
|
243
|
+
if (buttons.some(b => b.index === idx)) continue;
|
|
244
|
+
const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
|
|
245
|
+
buttons.push({ index: idx, label, key });
|
|
246
|
+
i = j - 1;
|
|
247
|
+
}
|
|
248
|
+
} else {
|
|
249
|
+
const re = compilePattern(state.modal_buttons);
|
|
250
|
+
let m: RegExpExecArray | null;
|
|
251
|
+
while ((m = re.exec(hay)) !== null) {
|
|
252
|
+
const idx = Number(m[1]);
|
|
253
|
+
const label = String(m[2] ?? '').trim();
|
|
254
|
+
if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
|
|
255
|
+
if (buttons.some(b => b.index === idx)) continue;
|
|
256
|
+
const key = state.modal_buttons.key_for_index.replace(/\{index\}/g, String(idx));
|
|
257
|
+
buttons.push({ index: idx, label, key });
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
buttons.sort((a, b) => a.index - b.index);
|
|
261
|
+
const minCount = state.modal_buttons.min_count ?? 2;
|
|
262
|
+
if (buttons.length < minCount) {
|
|
263
|
+
trace.push({ kind: 'modal', text: `modal_buttons matched ${buttons.length}/${minCount} required — discarded` });
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
trace.push({ kind: 'modal', text: `modal_buttons matched ${buttons.length} choices` });
|
|
267
|
+
return { title, buttons };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
271
|
+
// Public evaluator
|
|
272
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
273
|
+
|
|
274
|
+
export function evaluate(
|
|
275
|
+
spec: CliSpec,
|
|
276
|
+
screenText: string,
|
|
277
|
+
/** Optional cursor position (0-based row and col). When supplied, states
|
|
278
|
+
* with cursor_row_min/max or cursor_col_min/max predicates are filtered.
|
|
279
|
+
* When omitted, cursor predicates are ignored and evaluation is text-only
|
|
280
|
+
* (backward-compatible with all existing specs and call sites). */
|
|
281
|
+
cursor?: { row: number; col: number },
|
|
282
|
+
): SpecEvaluation {
|
|
283
|
+
const trace: TraceEntry[] = [];
|
|
284
|
+
const lines = screenText.split('\n');
|
|
285
|
+
const sections = resolveSections(spec, lines);
|
|
286
|
+
for (const s of sections) {
|
|
287
|
+
trace.push({ kind: 'section', text: `section[${s.id}] lines [${s.fromLine}, ${s.toLine}) (${s.toLine - s.fromLine} lines)` });
|
|
288
|
+
}
|
|
289
|
+
if (cursor !== undefined) {
|
|
290
|
+
trace.push({ kind: 'section', text: `cursor (${cursor.row}, ${cursor.col})` });
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
let activeState: { id: string; label: string; title: string | null } | null = null;
|
|
294
|
+
let modal: ModalSnapshot | null = null;
|
|
295
|
+
|
|
296
|
+
for (const st of spec.states) {
|
|
297
|
+
const { matched, title } = matchState(st, sections, screenText, trace, cursor);
|
|
298
|
+
if (!matched) continue;
|
|
299
|
+
const extractedModal = extractModal(st, sections, screenText, title, trace);
|
|
300
|
+
// If the state declares modal_buttons but extraction failed (button
|
|
301
|
+
// count below min_count, or text-was-mistaken-for-modal), do not
|
|
302
|
+
// promote the state. Otherwise we would surface a phantom approval
|
|
303
|
+
// built from arbitrary screen text — see claude-cli numbered-list
|
|
304
|
+
// false-positive on 2026-06-07.
|
|
305
|
+
if (st.modal_buttons && !extractedModal) {
|
|
306
|
+
trace.push({ kind: 'state_skip', text: `state[${st.id}] matched but modal_buttons extraction failed — not promoting` });
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
activeState = { id: st.id, label: st.label, title };
|
|
310
|
+
modal = extractedModal;
|
|
311
|
+
break;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!activeState) {
|
|
315
|
+
const def = spec.states.find(s => s.id === spec.default_state);
|
|
316
|
+
if (def) {
|
|
317
|
+
activeState = { id: def.id, label: def.label, title: null };
|
|
318
|
+
trace.push({ kind: 'state_match', text: `(no state matched — fallback to default_state ${def.id})` });
|
|
319
|
+
} else {
|
|
320
|
+
activeState = { id: spec.default_state, label: spec.default_state, title: null };
|
|
321
|
+
trace.push({ kind: 'state_match', text: `(no state matched, no default_state defined — using id "${spec.default_state}")` });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const controls: VisibleControl[] = [];
|
|
326
|
+
for (const c of spec.control_bar ?? []) {
|
|
327
|
+
const visible = !c.visible_when_state || c.visible_when_state.includes(activeState.id);
|
|
328
|
+
trace.push({ kind: 'control', text: `control[${c.id}] visible=${visible}${c.visible_when_state ? ` (when_state ${c.visible_when_state.join('|')})` : ''}` });
|
|
329
|
+
if (visible) controls.push({ id: c.id, label: c.label, actionType: c.action.type });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const notifications: FiredNotification[] = [];
|
|
333
|
+
for (const n of spec.notifications ?? []) {
|
|
334
|
+
if (n.when_state !== activeState.id) continue;
|
|
335
|
+
const body = interpolate(n.body ?? '', activeState, sections);
|
|
336
|
+
notifications.push({ id: n.id, title: n.title, body });
|
|
337
|
+
trace.push({ kind: 'notification', text: `notification[${n.id}] fired (when_state=${n.when_state})` });
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const delegates: FiredDelegate[] = [];
|
|
341
|
+
for (const d of spec.delegate ?? []) {
|
|
342
|
+
if (d.when_state !== activeState.id) continue;
|
|
343
|
+
// after_duration_ms is enforced by the driver, not the evaluator
|
|
344
|
+
// (the evaluator is pure / stateless). The driver bundles a
|
|
345
|
+
// timer per delegate trigger and emits when it fires.
|
|
346
|
+
delegates.push({ id: d.id, task: interpolate(d.task_template, activeState, sections) });
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return {
|
|
350
|
+
state: activeState,
|
|
351
|
+
modal,
|
|
352
|
+
controls,
|
|
353
|
+
notifications,
|
|
354
|
+
delegates,
|
|
355
|
+
sections,
|
|
356
|
+
trace,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function interpolate(
|
|
361
|
+
template: string,
|
|
362
|
+
state: { id: string; label: string; title: string | null },
|
|
363
|
+
sections: ResolvedSection[],
|
|
364
|
+
): string {
|
|
365
|
+
return template
|
|
366
|
+
.replace(/\{state\.label\}/g, state.label)
|
|
367
|
+
.replace(/\{state\.title\}/g, state.title ?? '')
|
|
368
|
+
.replace(/\{state\.id\}/g, state.id)
|
|
369
|
+
.replace(/\{screen\.([a-z][a-z0-9_]*)\}/g, (_, id: string) => {
|
|
370
|
+
const s = sections.find(s => s.id === id);
|
|
371
|
+
return s ? s.text : '';
|
|
372
|
+
});
|
|
373
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* spec.json loader + strict validator. Rejects spec files that don't
|
|
3
|
+
* conform to schema.json — unknown fields, missing required fields,
|
|
4
|
+
* wrong types, regex strings that don't compile, state references
|
|
5
|
+
* that don't exist, etc.
|
|
6
|
+
*
|
|
7
|
+
* Validation runs once at load. Hot reload re-runs validation.
|
|
8
|
+
*/
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
import * as fs from 'node:fs';
|
|
12
|
+
import * as path from 'node:path';
|
|
13
|
+
import Ajv from 'ajv';
|
|
14
|
+
import type { CliSpec } from './types.js';
|
|
15
|
+
import { SCHEMA as schema } from './schema.gen.js';
|
|
16
|
+
|
|
17
|
+
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
18
|
+
const validate = ajv.compile<CliSpec>(schema);
|
|
19
|
+
|
|
20
|
+
export interface SpecLoadResult {
|
|
21
|
+
ok: true;
|
|
22
|
+
spec: CliSpec;
|
|
23
|
+
sourcePath: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface SpecLoadError {
|
|
27
|
+
ok: false;
|
|
28
|
+
errors: string[];
|
|
29
|
+
sourcePath: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function loadSpec(sourcePath: string): SpecLoadResult | SpecLoadError {
|
|
33
|
+
let raw: unknown;
|
|
34
|
+
try {
|
|
35
|
+
const text = fs.readFileSync(sourcePath, 'utf8');
|
|
36
|
+
raw = JSON.parse(text);
|
|
37
|
+
} catch (err) {
|
|
38
|
+
return { ok: false, errors: [`Failed to read spec: ${(err as Error).message}`], sourcePath };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!validate(raw)) {
|
|
42
|
+
const errors = (validate.errors || []).map(e => `${e.instancePath || '(root)'} ${e.message ?? 'invalid'}`);
|
|
43
|
+
return { ok: false, errors, sourcePath };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const spec = raw as CliSpec;
|
|
47
|
+
const extra = validateRefs(spec);
|
|
48
|
+
if (extra.length > 0) return { ok: false, errors: extra, sourcePath };
|
|
49
|
+
|
|
50
|
+
return { ok: true, spec, sourcePath };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Cross-field validation that JSON Schema can't express on its own:
|
|
55
|
+
* - section references actually point to a defined section
|
|
56
|
+
* - state references in control_bar/notifications/delegate exist
|
|
57
|
+
* - default_state exists
|
|
58
|
+
* - regex / pattern strings compile
|
|
59
|
+
*/
|
|
60
|
+
function validateRefs(spec: CliSpec): string[] {
|
|
61
|
+
const errs: string[] = [];
|
|
62
|
+
const sectionIds = new Set(spec.layout.sections.map(s => s.id));
|
|
63
|
+
const stateIds = new Set(spec.states.map(s => s.id));
|
|
64
|
+
|
|
65
|
+
if (!stateIds.has(spec.default_state)) {
|
|
66
|
+
errs.push(`default_state "${spec.default_state}" is not defined in states[]`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
for (const s of spec.states) {
|
|
70
|
+
checkSectionRef(s.when, `states[${s.id}].when`, sectionIds, errs);
|
|
71
|
+
compileRegex(s.when.regex, s.when.flags, `states[${s.id}].when.regex`, errs);
|
|
72
|
+
if (s.extract_title) {
|
|
73
|
+
checkSectionRef(s.extract_title, `states[${s.id}].extract_title`, sectionIds, errs);
|
|
74
|
+
compileRegex(s.extract_title.regex, s.extract_title.flags, `states[${s.id}].extract_title.regex`, errs);
|
|
75
|
+
}
|
|
76
|
+
if (s.modal_buttons) {
|
|
77
|
+
if (s.modal_buttons.section && !sectionIds.has(s.modal_buttons.section)) {
|
|
78
|
+
errs.push(`states[${s.id}].modal_buttons.section "${s.modal_buttons.section}" unknown`);
|
|
79
|
+
}
|
|
80
|
+
compileRegex(s.modal_buttons.pattern, s.modal_buttons.flags ?? 'm', `states[${s.id}].modal_buttons.pattern`, errs);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
for (const c of spec.control_bar ?? []) {
|
|
85
|
+
for (const stId of c.visible_when_state ?? []) {
|
|
86
|
+
if (!stateIds.has(stId)) errs.push(`control_bar[${c.id}].visible_when_state references unknown state "${stId}"`);
|
|
87
|
+
}
|
|
88
|
+
if (c.action.type === 'open_picker') {
|
|
89
|
+
checkSectionRef(c.action.wait_for, `control_bar[${c.id}].action.wait_for`, sectionIds, errs);
|
|
90
|
+
compileRegex(c.action.wait_for.regex, c.action.wait_for.flags, `control_bar[${c.id}].action.wait_for.regex`, errs);
|
|
91
|
+
if (c.action.extract_choices.section && !sectionIds.has(c.action.extract_choices.section)) {
|
|
92
|
+
errs.push(`control_bar[${c.id}].action.extract_choices.section "${c.action.extract_choices.section}" unknown`);
|
|
93
|
+
}
|
|
94
|
+
compileRegex(c.action.extract_choices.pattern, c.action.extract_choices.flags ?? 'm', `control_bar[${c.id}].action.extract_choices.pattern`, errs);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
for (const n of spec.notifications ?? []) {
|
|
99
|
+
if (!stateIds.has(n.when_state)) errs.push(`notifications[${n.id}].when_state "${n.when_state}" unknown`);
|
|
100
|
+
}
|
|
101
|
+
for (const d of spec.delegate ?? []) {
|
|
102
|
+
if (!stateIds.has(d.when_state)) errs.push(`delegate[${d.id}].when_state "${d.when_state}" unknown`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// native_history: exactly one of {reader, source, override_path}.
|
|
106
|
+
const nh = spec.native_history;
|
|
107
|
+
if (nh) {
|
|
108
|
+
const modes = (['reader', 'source', 'override_path'] as const).filter(k => (nh as Record<string, unknown>)[k] !== undefined);
|
|
109
|
+
if (modes.length === 0) {
|
|
110
|
+
errs.push('native_history must set exactly one of {reader, source, override_path}');
|
|
111
|
+
} else if (modes.length > 1) {
|
|
112
|
+
errs.push(`native_history sets ${modes.length} modes (${modes.join(', ')}); pick exactly one`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return errs;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function checkSectionRef(ref: { section?: string }, where: string, valid: Set<string>, errs: string[]): void {
|
|
120
|
+
if (ref.section && !valid.has(ref.section)) errs.push(`${where}.section "${ref.section}" unknown`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function compileRegex(source: string, flags: string | undefined, where: string, errs: string[]): void {
|
|
124
|
+
try { new RegExp(source, flags ?? ''); } catch (err) { errs.push(`${where} regex invalid: ${(err as Error).message}`); }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Convenience: look up a provider's spec.json next to its provider dir. */
|
|
128
|
+
export function resolveSpecPath(providerDir: string): string {
|
|
129
|
+
return path.join(providerDir, 'spec.json');
|
|
130
|
+
}
|