@adhdev/daemon-core 0.9.82-rc.360 → 0.9.82-rc.361
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/commands/cli-manager.d.ts +13 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/types.d.ts +16 -0
- package/dist/commands/router.d.ts +0 -1
- package/dist/index.js +2074 -2008
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2073 -2007
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events-utils.d.ts +0 -2
- package/package.json +2 -2
- package/src/commands/cli-manager.ts +69 -4
- package/src/commands/low-family/index.ts +23 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +274 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/types.ts +19 -0
- package/src/commands/router.ts +15 -555
- package/src/mesh/contracts.ts +12 -3
- package/src/mesh/mesh-events-coordinator.ts +1 -1
- package/src/mesh/mesh-events-utils.ts +0 -20
- package/src/providers/cli-provider-instance.ts +16 -3
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER LOW family — provider-dev / spec debug commands.
|
|
3
|
+
*
|
|
4
|
+
* Powers the dev console's spec editor: read/write a session's spec.json, validate
|
|
5
|
+
* an in-progress spec, and preview condition/section resolution against a live
|
|
6
|
+
* session's screen. Extracted verbatim from executeDaemonCommand; reads only
|
|
7
|
+
* ctx.deps.sessionRegistry + ctx.deps.cliManager. resolveSpecPathInProviders (a
|
|
8
|
+
* `this`-free module helper) is relocated here unchanged.
|
|
9
|
+
*/
|
|
10
|
+
import type { LowFamilyContext, LowFamilyHandler } from './types.js';
|
|
11
|
+
|
|
12
|
+
function resolveSpecPathInProviders(
|
|
13
|
+
specPath: string,
|
|
14
|
+
fsm: typeof import('node:fs'),
|
|
15
|
+
pathm: typeof import('node:path'),
|
|
16
|
+
osm: typeof import('node:os'),
|
|
17
|
+
): { ok: true; path: string } | { ok: false; error: string } {
|
|
18
|
+
let rootReal: string;
|
|
19
|
+
try {
|
|
20
|
+
rootReal = fsm.realpathSync(pathm.join(osm.homedir(), '.adhdev', 'providers'));
|
|
21
|
+
} catch (e) {
|
|
22
|
+
return { ok: false, error: `providers root unavailable: ${(e as Error).message}` };
|
|
23
|
+
}
|
|
24
|
+
const resolved = pathm.resolve(specPath);
|
|
25
|
+
const base = pathm.basename(resolved);
|
|
26
|
+
if (!/^[\w.-]+\.json$/.test(base)) {
|
|
27
|
+
return { ok: false, error: 'refused: spec file must be a *.json basename' };
|
|
28
|
+
}
|
|
29
|
+
let parentReal: string;
|
|
30
|
+
try {
|
|
31
|
+
parentReal = fsm.realpathSync(pathm.dirname(resolved));
|
|
32
|
+
} catch (e) {
|
|
33
|
+
return { ok: false, error: `spec directory not found: ${(e as Error).message}` };
|
|
34
|
+
}
|
|
35
|
+
if (parentReal !== rootReal && !parentReal.startsWith(rootReal + pathm.sep)) {
|
|
36
|
+
return { ok: false, error: 'refused: spec path must be under the providers root' };
|
|
37
|
+
}
|
|
38
|
+
const safe = pathm.join(parentReal, base);
|
|
39
|
+
// Reject if the final file itself is a symlink pointing elsewhere.
|
|
40
|
+
try {
|
|
41
|
+
const st = fsm.lstatSync(safe);
|
|
42
|
+
if (st.isSymbolicLink()) return { ok: false, error: 'refused: spec path is a symlink' };
|
|
43
|
+
} catch { /* file may not exist yet (write case) — fine */ }
|
|
44
|
+
return { ok: true, path: safe };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const specProviderDevHandlers: Record<string, LowFamilyHandler> = {
|
|
48
|
+
get_spec_debug: async (ctx: LowFamilyContext, args: any) => {
|
|
49
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
|
|
50
|
+
: typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
51
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
52
|
+
const target = ctx.deps.sessionRegistry.get(sessionId);
|
|
53
|
+
if (!target) return { success: false, error: 'Session not found', sessionId };
|
|
54
|
+
const adapterObj = ctx.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter;
|
|
55
|
+
const snapshot = adapterObj
|
|
56
|
+
? (typeof (adapterObj as any).getDebugSnapshot === 'function'
|
|
57
|
+
? (adapterObj as any).getDebugSnapshot()
|
|
58
|
+
: typeof (adapterObj as any).getDebugState === 'function'
|
|
59
|
+
? (adapterObj as any).getDebugState()
|
|
60
|
+
: null)
|
|
61
|
+
: null;
|
|
62
|
+
return {
|
|
63
|
+
success: true,
|
|
64
|
+
sessionId,
|
|
65
|
+
providerType: target.providerType,
|
|
66
|
+
isSpecProvider: snapshot !== null,
|
|
67
|
+
snapshot,
|
|
68
|
+
};
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// ── Spec source read/write for the debug panel's live editor.
|
|
72
|
+
// Lets the dashboard load a session's spec.json, edit it, and save it back —
|
|
73
|
+
// the driver's fs.watch picks up the change and hot-reloads the FSM with no
|
|
74
|
+
// restart. Writes are confined to files under ~/.adhdev/providers.
|
|
75
|
+
get_spec_source: async (ctx: LowFamilyContext, args: any) => {
|
|
76
|
+
const fsm = await import('node:fs');
|
|
77
|
+
const pathm = await import('node:path');
|
|
78
|
+
const osm = await import('node:os');
|
|
79
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
|
|
80
|
+
: typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
81
|
+
let specPath = typeof args?.specPath === 'string' ? args.specPath : '';
|
|
82
|
+
if (!specPath && sessionId) {
|
|
83
|
+
const target = ctx.deps.sessionRegistry.get(sessionId);
|
|
84
|
+
const adapterObj = target ? ctx.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter : null;
|
|
85
|
+
const snap = adapterObj && typeof (adapterObj as any).getDebugSnapshot === 'function' ? (adapterObj as any).getDebugSnapshot() : null;
|
|
86
|
+
specPath = snap?.specPath ?? '';
|
|
87
|
+
}
|
|
88
|
+
if (!specPath) return { success: false, error: 'specPath or resolvable targetSessionId required' };
|
|
89
|
+
// Confine reads to the providers tree, resolving symlinks so a crafted path
|
|
90
|
+
// can't escape via a symlinked spec file.
|
|
91
|
+
const safe = resolveSpecPathInProviders(specPath, fsm, pathm, osm);
|
|
92
|
+
if (!safe.ok) return { success: false, error: safe.error, specPath };
|
|
93
|
+
try {
|
|
94
|
+
const content = fsm.readFileSync(safe.path, 'utf8');
|
|
95
|
+
return { success: true, specPath: safe.path, content };
|
|
96
|
+
} catch (e) {
|
|
97
|
+
return { success: false, error: `read failed: ${(e as Error).message}`, specPath };
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
write_spec_source: async (_ctx: LowFamilyContext, args: any) => {
|
|
102
|
+
const fsm = await import('node:fs');
|
|
103
|
+
const pathm = await import('node:path');
|
|
104
|
+
const osm = await import('node:os');
|
|
105
|
+
const specPath = typeof args?.specPath === 'string' ? args.specPath : '';
|
|
106
|
+
const content = typeof args?.content === 'string' ? args.content : '';
|
|
107
|
+
if (!specPath) return { success: false, error: 'specPath required' };
|
|
108
|
+
if (!content) return { success: false, error: 'content required' };
|
|
109
|
+
// Confine writes to the providers tree (symlink-safe — see helper).
|
|
110
|
+
const safe = resolveSpecPathInProviders(specPath, fsm, pathm, osm);
|
|
111
|
+
if (!safe.ok) return { success: false, error: safe.error };
|
|
112
|
+
// Validate JSON + (if v4) FSM structure before writing so a bad edit can't
|
|
113
|
+
// break the live session — return precise errors.
|
|
114
|
+
let parsed: unknown;
|
|
115
|
+
try { parsed = JSON.parse(content); }
|
|
116
|
+
catch (e) { return { success: false, error: `invalid JSON: ${(e as Error).message}` }; }
|
|
117
|
+
if ((parsed as any)?.$schema === 'adhdev:cli/spec@4') {
|
|
118
|
+
const { validateFsmSpec } = await import('../../providers/spec/fsm-loader.js');
|
|
119
|
+
const errs = validateFsmSpec(parsed);
|
|
120
|
+
if (errs.length) return { success: false, error: 'spec invalid', validationErrors: errs };
|
|
121
|
+
}
|
|
122
|
+
try {
|
|
123
|
+
fsm.writeFileSync(safe.path, content, 'utf8');
|
|
124
|
+
return { success: true, specPath: safe.path };
|
|
125
|
+
} catch (e) {
|
|
126
|
+
return { success: false, error: `write failed: ${(e as Error).message}` };
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// ── Validate an in-progress spec (string or object) without writing. The form
|
|
131
|
+
// builder calls this on every change so Save can stay disabled while there
|
|
132
|
+
// are structural / reference / regex errors.
|
|
133
|
+
validate_spec: async (_ctx: LowFamilyContext, args: any) => {
|
|
134
|
+
let parsed: unknown = args?.spec;
|
|
135
|
+
if (typeof args?.content === 'string') {
|
|
136
|
+
try { parsed = JSON.parse(args.content); }
|
|
137
|
+
catch (e) { return { success: true, valid: false, errors: [`invalid JSON: ${(e as Error).message}`] }; }
|
|
138
|
+
}
|
|
139
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
140
|
+
return { success: true, valid: false, errors: ['spec must be an object or content string'] };
|
|
141
|
+
}
|
|
142
|
+
const schema = (parsed as any).$schema;
|
|
143
|
+
if (schema === 'adhdev:cli/spec@4') {
|
|
144
|
+
const { validateFsmSpec } = await import('../../providers/spec/fsm-loader.js');
|
|
145
|
+
const errors = validateFsmSpec(parsed);
|
|
146
|
+
return { success: true, valid: errors.length === 0, errors };
|
|
147
|
+
}
|
|
148
|
+
// v1/v3 left to the legacy loader path; the builder is v4-only.
|
|
149
|
+
return { success: true, valid: false, errors: [`unsupported $schema "${schema}" — form builder is v4-only`] };
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
// ── Evaluate a single condition against a live session's current screen —
|
|
153
|
+
// powers the editor's "does this match right now?" preview. Returns the
|
|
154
|
+
// recursive match tree.
|
|
155
|
+
eval_condition_preview: async (ctx: LowFamilyContext, args: any) => {
|
|
156
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
|
|
157
|
+
: typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
158
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
159
|
+
if (!args?.condition || typeof args.condition !== 'object') return { success: false, error: 'condition required' };
|
|
160
|
+
const target = ctx.deps.sessionRegistry.get(sessionId);
|
|
161
|
+
if (!target) return { success: false, error: 'Session not found', sessionId };
|
|
162
|
+
const adapterObj = ctx.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter as any;
|
|
163
|
+
const snap = adapterObj && typeof adapterObj.getDebugSnapshot === 'function' ? adapterObj.getDebugSnapshot() : null;
|
|
164
|
+
if (!snap?.screen) return { success: false, error: 'no live screen for session' };
|
|
165
|
+
// Reconstruct the sections map the spec would resolve. We pass the spec's
|
|
166
|
+
// sections so section-scoped regexes resolve the same way they do at
|
|
167
|
+
// runtime; fall back to the on-disk spec.
|
|
168
|
+
let sectionsDef: Record<string, unknown> | undefined;
|
|
169
|
+
try {
|
|
170
|
+
const fsm2 = await import('node:fs');
|
|
171
|
+
if (snap.specPath) {
|
|
172
|
+
const raw = JSON.parse(fsm2.readFileSync(snap.specPath, 'utf8'));
|
|
173
|
+
sectionsDef = raw?.sections;
|
|
174
|
+
}
|
|
175
|
+
} catch { /* fall back to whole-screen matching */ }
|
|
176
|
+
const { evaluateConditionPreview } = await import('../../providers/spec/fsm-evaluator.js');
|
|
177
|
+
try {
|
|
178
|
+
const result = evaluateConditionPreview(
|
|
179
|
+
args.condition,
|
|
180
|
+
sectionsDef as any,
|
|
181
|
+
snap.screen,
|
|
182
|
+
snap.cursorPosition ?? undefined,
|
|
183
|
+
);
|
|
184
|
+
return { success: true, result, sections: snap.sections ?? null };
|
|
185
|
+
} catch (e) {
|
|
186
|
+
return { success: false, error: `eval failed: ${(e as Error).message}` };
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
|
|
190
|
+
// ── Resolve a sections map against a live session's screen — the section
|
|
191
|
+
// editor's "test" button. Returns, for each section id, the line range + the
|
|
192
|
+
// text it captures. Accepts an in-progress sections map so it previews
|
|
193
|
+
// unsaved edits.
|
|
194
|
+
resolve_section_preview: async (ctx: LowFamilyContext, args: any) => {
|
|
195
|
+
const sessionId = typeof args?.targetSessionId === 'string' ? args.targetSessionId.trim()
|
|
196
|
+
: typeof args?.sessionId === 'string' ? args.sessionId.trim() : '';
|
|
197
|
+
if (!sessionId) return { success: false, error: 'targetSessionId required' };
|
|
198
|
+
if (!args?.sections || typeof args.sections !== 'object') return { success: false, error: 'sections map required' };
|
|
199
|
+
const target = ctx.deps.sessionRegistry.get(sessionId);
|
|
200
|
+
if (!target) return { success: false, error: 'Session not found', sessionId };
|
|
201
|
+
const adapterObj = ctx.deps.cliManager.findAdapter(target.providerType, { instanceKey: sessionId })?.adapter as any;
|
|
202
|
+
const snap = adapterObj && typeof adapterObj.getDebugSnapshot === 'function' ? adapterObj.getDebugSnapshot() : null;
|
|
203
|
+
if (!snap?.screen) return { success: false, error: 'no live screen for session' };
|
|
204
|
+
const { resolveSections } = await import('../../providers/spec/evaluator.js');
|
|
205
|
+
try {
|
|
206
|
+
const lines = String(snap.screen).split('\n').map((l: string) => l.endsWith('\r') ? l.slice(0, -1) : l);
|
|
207
|
+
const resolved = resolveSections(args.sections as any, lines);
|
|
208
|
+
return {
|
|
209
|
+
success: true,
|
|
210
|
+
screenLineCount: lines.length,
|
|
211
|
+
sections: resolved.map(s => ({ id: s.id, fromLine: s.fromLine, toLine: s.toLine, text: s.text })),
|
|
212
|
+
};
|
|
213
|
+
} catch (e) {
|
|
214
|
+
return { success: false, error: `resolve failed: ${(e as Error).message}` };
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RF-ROUTER LOW family — shared types for the extracted low-coupling command
|
|
3
|
+
* handlers. Each handler is a pure function of (context, args): it reads only the
|
|
4
|
+
* router deps it needs and returns the exact CommandRouterResult the original
|
|
5
|
+
* `executeDaemonCommand` switch case returned, so the router facade is unchanged.
|
|
6
|
+
*
|
|
7
|
+
* Registry dispatch: DaemonCommandRouter.executeDaemonCommand looks up the cmd in
|
|
8
|
+
* lowFamilyRegistry BEFORE its switch; a hit returns the handler result, a miss
|
|
9
|
+
* falls through to the remaining switch (and ultimately CommandHandler delegation).
|
|
10
|
+
*/
|
|
11
|
+
import type { CommandRouterDeps, CommandRouterResult } from '../router.js';
|
|
12
|
+
|
|
13
|
+
export interface LowFamilyContext {
|
|
14
|
+
deps: CommandRouterDeps;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export type LowFamilyHandler = (ctx: LowFamilyContext, args: any) => Promise<CommandRouterResult>;
|
|
18
|
+
|
|
19
|
+
export type LowFamilyRegistry = Map<string, LowFamilyHandler>;
|