@eyeauras/insight-cli 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.cli-package-build.json +1 -0
- package/LICENSE +21 -0
- package/README.md +60 -2
- package/SKILL.md +58 -0
- package/dist/src/administration-results.d.ts +1 -0
- package/dist/src/administration-results.js +84 -0
- package/dist/src/administration.d.ts +14 -0
- package/dist/src/administration.js +134 -0
- package/dist/src/archive-contract.d.ts +6 -0
- package/dist/src/archive-contract.js +108 -0
- package/dist/src/archive.d.ts +12 -0
- package/dist/src/archive.js +116 -0
- package/dist/src/arguments.d.ts +14 -0
- package/dist/src/arguments.js +62 -0
- package/dist/src/artifacts.d.ts +17 -0
- package/dist/src/artifacts.js +115 -0
- package/dist/src/bin.d.ts +2 -0
- package/dist/src/bin.js +4 -0
- package/dist/src/cli.d.ts +10 -0
- package/dist/src/cli.js +419 -0
- package/dist/src/comment-effects.d.ts +3 -0
- package/dist/src/comment-effects.js +55 -0
- package/dist/src/contexts.d.ts +115 -0
- package/dist/src/contexts.js +1078 -0
- package/dist/src/coordinator.d.ts +41 -0
- package/dist/src/coordinator.js +160 -0
- package/dist/src/cross-results.d.ts +14 -0
- package/dist/src/cross-results.js +71 -0
- package/dist/src/cross.d.ts +3 -0
- package/dist/src/cross.js +43 -0
- package/dist/src/data-effects.d.ts +3 -0
- package/dist/src/data-effects.js +103 -0
- package/dist/src/debugger-results.d.ts +4 -0
- package/dist/src/debugger-results.js +151 -0
- package/dist/src/debugger-supervisor.d.ts +1 -0
- package/dist/src/debugger-supervisor.js +136 -0
- package/dist/src/debugger.d.ts +21 -0
- package/dist/src/debugger.js +267 -0
- package/dist/src/documentation-effects.d.ts +5 -0
- package/dist/src/documentation-effects.js +56 -0
- package/dist/src/extensions.d.ts +64 -0
- package/dist/src/extensions.js +289 -0
- package/dist/src/function-effects.d.ts +4 -0
- package/dist/src/function-effects.js +112 -0
- package/dist/src/ghidra.d.ts +50 -0
- package/dist/src/ghidra.js +468 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +1 -0
- package/dist/src/initial-model.d.ts +6 -0
- package/dist/src/initial-model.js +85 -0
- package/dist/src/json.d.ts +2 -0
- package/dist/src/json.js +8 -0
- package/dist/src/lifecycle.d.ts +49 -0
- package/dist/src/lifecycle.js +154 -0
- package/dist/src/memory-inputs.d.ts +8 -0
- package/dist/src/memory-inputs.js +56 -0
- package/dist/src/merge.d.ts +29 -0
- package/dist/src/merge.js +66 -0
- package/dist/src/model.d.ts +74 -0
- package/dist/src/model.js +67 -0
- package/dist/src/mutation-effects.d.ts +4 -0
- package/dist/src/mutation-effects.js +321 -0
- package/dist/src/mutation-inputs.d.ts +2 -0
- package/dist/src/mutation-inputs.js +71 -0
- package/dist/src/mutations.d.ts +4 -0
- package/dist/src/mutations.js +53 -0
- package/dist/src/native-inputs.d.ts +28 -0
- package/dist/src/native-inputs.js +182 -0
- package/dist/src/native-queries.d.ts +2 -0
- package/dist/src/native-queries.js +29 -0
- package/dist/src/projects.d.ts +31 -0
- package/dist/src/projects.js +301 -0
- package/dist/src/prototype-cli.d.ts +8 -0
- package/dist/src/prototype-cli.js +155 -0
- package/dist/src/queries.d.ts +5 -0
- package/dist/src/queries.js +84 -0
- package/dist/src/query-policy.d.ts +6 -0
- package/dist/src/query-policy.js +72 -0
- package/dist/src/query-results.d.ts +2 -0
- package/dist/src/query-results.js +224 -0
- package/dist/src/research.d.ts +184 -0
- package/dist/src/research.js +808 -0
- package/dist/src/runtime-archives.d.ts +5 -0
- package/dist/src/runtime-archives.js +127 -0
- package/dist/src/runtime-catalog.d.ts +18 -0
- package/dist/src/runtime-catalog.js +27 -0
- package/dist/src/runtime-manager.d.ts +101 -0
- package/dist/src/runtime-manager.js +444 -0
- package/dist/src/runtime-selection.d.ts +13 -0
- package/dist/src/runtime-selection.js +25 -0
- package/dist/src/saved-materials.d.ts +12 -0
- package/dist/src/saved-materials.js +196 -0
- package/dist/src/scripts.d.ts +19 -0
- package/dist/src/scripts.js +79 -0
- package/dist/src/sdk-contracts.d.ts +13 -0
- package/dist/src/sdk-contracts.js +37 -0
- package/dist/src/sdk-files.d.ts +29 -0
- package/dist/src/sdk-files.js +249 -0
- package/dist/src/sdk.d.ts +4 -0
- package/dist/src/sdk.js +125 -0
- package/dist/src/server.d.ts +46 -0
- package/dist/src/server.js +460 -0
- package/dist/src/store.d.ts +33 -0
- package/dist/src/store.js +178 -0
- package/dist/src/sync-policy.d.ts +22 -0
- package/dist/src/sync-policy.js +123 -0
- package/dist/src/transport.d.ts +23 -0
- package/dist/src/transport.js +135 -0
- package/dist/src/ui-results.d.ts +2 -0
- package/dist/src/ui-results.js +80 -0
- package/dist/src/ui.d.ts +17 -0
- package/dist/src/ui.js +88 -0
- package/dist/src/utility.d.ts +14 -0
- package/dist/src/utility.js +43 -0
- package/dist/src/version-tracking.d.ts +57 -0
- package/dist/src/version-tracking.js +200 -0
- package/dist/src/workspace.d.ts +199 -0
- package/dist/src/workspace.js +564 -0
- package/package.json +47 -4
- package/references/InsightRepositoryProbe.java +55 -0
- package/references/administration.md +34 -0
- package/references/archive.md +32 -0
- package/references/artifacts.md +47 -0
- package/references/debugger.md +40 -0
- package/references/extensions-and-sync.md +124 -0
- package/references/managed-runtime.md +116 -0
- package/references/mutations.json +1424 -0
- package/references/native/InsightApplyDocumentation.java +48 -0
- package/references/native/InsightArchivePayloads.java +36 -0
- package/references/native/InsightBlockTransfer.java +57 -0
- package/references/native/InsightEmulation.java +93 -0
- package/references/native/InsightExtensions.java +36 -0
- package/references/native/InsightFunctionDatabases.java +98 -0
- package/references/native/InsightGuiMain.java +32 -0
- package/references/native/InsightHeadlessMain.java +10 -0
- package/references/native/InsightInitialImport.java +42 -0
- package/references/native/InsightInitialJournal.java +48 -0
- package/references/native/InsightInitialPublication.java +142 -0
- package/references/native/InsightKeytool.java +16 -0
- package/references/native/InsightNativeReceipt.java +31 -0
- package/references/native/InsightPlugins.java +34 -0
- package/references/native/InsightProjectFiles.java +92 -0
- package/references/native/InsightPublicationRecovery.java +63 -0
- package/references/native/InsightPythonWheels.java +31 -0
- package/references/native/InsightResearchQueries.java +188 -0
- package/references/native/InsightRuntimeDoctor.java +33 -0
- package/references/native/InsightSdkFiles.java +142 -0
- package/references/native/InsightSdkScript.java +31 -0
- package/references/native/InsightSdkTools.java +338 -0
- package/references/native/InsightSdkTypes.java +132 -0
- package/references/native/InsightServerAdministration.java +82 -0
- package/references/native/InsightServerInit.java +62 -0
- package/references/native/InsightServerMain.java +43 -0
- package/references/native/InsightServerProbe.java +29 -0
- package/references/native/InsightSnapshotFields.java +68 -0
- package/references/native/InsightSnapshotInspect.java +189 -0
- package/references/native/InsightSnapshotPublisher.java +123 -0
- package/references/native/InsightStartDebugger.java +28 -0
- package/references/native/InsightStopDebugger.java +20 -0
- package/references/native/InsightTraceInspect.java +24 -0
- package/references/native/InsightTraceState.java +130 -0
- package/references/native/InsightTraceTools.java +114 -0
- package/references/native/InsightVersionTracking.java +91 -0
- package/references/native-workflows.md +112 -0
- package/references/queries.json +1761 -0
- package/references/query-semantics.md +96 -0
- package/references/recovery.md +53 -0
- package/references/research.md +101 -0
- package/references/setup.md +49 -0
- package/runtime/GhidraMCP-7.0.0.jar +0 -0
- package/runtime/GhidraMCP-LICENSE +202 -0
- package/runtime/GhidraMCP-NOTICE +10 -0
- package/runtime/provenance.json +11 -0
- package/skills/insight-re/SKILL.md +108 -0
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { type Batch, type Binding, type FindingRecord, type State, type Target } from './model.js';
|
|
2
|
+
export declare class Store {
|
|
3
|
+
readonly directory: string;
|
|
4
|
+
readonly profile: string;
|
|
5
|
+
readonly server: string;
|
|
6
|
+
static pending(directory: string): boolean;
|
|
7
|
+
private db;
|
|
8
|
+
constructor(directory: string, profile: string, server: string);
|
|
9
|
+
close(): void;
|
|
10
|
+
private transaction;
|
|
11
|
+
register(value: unknown): Target;
|
|
12
|
+
target(id: string): Target;
|
|
13
|
+
targets(): Target[];
|
|
14
|
+
submit(value: unknown): FindingRecord;
|
|
15
|
+
getFinding(id: string): FindingRecord;
|
|
16
|
+
findings(targetId?: string): FindingRecord[];
|
|
17
|
+
begin(targetId: string): Batch | undefined;
|
|
18
|
+
batch(id: string): Batch;
|
|
19
|
+
batches(): Batch[];
|
|
20
|
+
update(id: string, state: State, patch?: Partial<Omit<Batch, 'id' | 'targetId' | 'findings'>>): Batch;
|
|
21
|
+
conflicts(id: string, conflicts: unknown): void;
|
|
22
|
+
private event;
|
|
23
|
+
events(id: string): {
|
|
24
|
+
seq: import("node:sqlite").SQLOutputValue | undefined;
|
|
25
|
+
state: import("node:sqlite").SQLOutputValue | undefined;
|
|
26
|
+
data: any;
|
|
27
|
+
}[];
|
|
28
|
+
worker(id: string): Binding;
|
|
29
|
+
putWorker(id: string, targetId: string, binding: Binding): void;
|
|
30
|
+
updateReader(id: string, binding: Binding): void;
|
|
31
|
+
workers(): any[];
|
|
32
|
+
removeWorker(id: string): void;
|
|
33
|
+
}
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
import { mkdirSync, chmodSync, existsSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { randomUUID } from 'node:crypto';
|
|
5
|
+
import { assertIdentity, digest, fail, finding, identity, stable } from './model.js';
|
|
6
|
+
export class Store {
|
|
7
|
+
directory;
|
|
8
|
+
profile;
|
|
9
|
+
server;
|
|
10
|
+
static pending(directory) {
|
|
11
|
+
const path = join(directory, 'coordinator.sqlite');
|
|
12
|
+
if (!existsSync(path))
|
|
13
|
+
return false;
|
|
14
|
+
const db = new DatabaseSync(path, { readOnly: true });
|
|
15
|
+
try {
|
|
16
|
+
return Number(db.prepare("SELECT COUNT(*) AS count FROM findings WHERE state != 'published_verified'").get()?.count) > 0;
|
|
17
|
+
}
|
|
18
|
+
finally {
|
|
19
|
+
db.close();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
db;
|
|
23
|
+
constructor(directory, profile, server) {
|
|
24
|
+
this.directory = directory;
|
|
25
|
+
this.profile = profile;
|
|
26
|
+
this.server = server;
|
|
27
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
28
|
+
const path = join(directory, 'coordinator.sqlite');
|
|
29
|
+
this.db = new DatabaseSync(path);
|
|
30
|
+
chmodSync(path, 0o600);
|
|
31
|
+
this.db.exec(`PRAGMA journal_mode=WAL; PRAGMA synchronous=FULL; PRAGMA foreign_keys=ON; PRAGMA busy_timeout=5000;
|
|
32
|
+
CREATE TABLE IF NOT EXISTS targets(id TEXT PRIMARY KEY, lane TEXT NOT NULL UNIQUE, data TEXT NOT NULL);
|
|
33
|
+
CREATE TABLE IF NOT EXISTS findings(id TEXT PRIMARY KEY, request_id TEXT NOT NULL UNIQUE, hash TEXT NOT NULL, target_id TEXT NOT NULL REFERENCES targets(id), state TEXT NOT NULL, batch_id TEXT, data TEXT NOT NULL, conflict TEXT);
|
|
34
|
+
CREATE TABLE IF NOT EXISTS batches(id TEXT PRIMARY KEY, target_id TEXT NOT NULL REFERENCES targets(id), state TEXT NOT NULL, owner_pid INTEGER NOT NULL, data TEXT NOT NULL);
|
|
35
|
+
CREATE UNIQUE INDEX IF NOT EXISTS active_lane ON batches(target_id) WHERE state IN ('applying','staged','checking_in','publication_unknown','failed');
|
|
36
|
+
CREATE TABLE IF NOT EXISTS events(seq INTEGER PRIMARY KEY AUTOINCREMENT, batch_id TEXT NOT NULL, state TEXT NOT NULL, data TEXT NOT NULL);
|
|
37
|
+
CREATE TABLE IF NOT EXISTS workers(id TEXT PRIMARY KEY, target_id TEXT NOT NULL, data TEXT NOT NULL);
|
|
38
|
+
PRAGMA user_version=1;`);
|
|
39
|
+
}
|
|
40
|
+
close() { this.db.close(); }
|
|
41
|
+
transaction(action) {
|
|
42
|
+
this.db.exec('BEGIN IMMEDIATE');
|
|
43
|
+
try {
|
|
44
|
+
const result = action();
|
|
45
|
+
this.db.exec('COMMIT');
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
this.db.exec('ROLLBACK');
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
register(value) {
|
|
54
|
+
const selected = identity(value);
|
|
55
|
+
if (selected.profile !== this.profile || selected.server !== this.server)
|
|
56
|
+
fail('binding', 'Target must match the selected profile and server.');
|
|
57
|
+
const lane = digest([selected.profile, selected.server, selected.repository, selected.programPath]);
|
|
58
|
+
const target = { ...selected, id: 't-' + digest(selected).slice(0, 24), lane };
|
|
59
|
+
return this.transaction(() => {
|
|
60
|
+
const existing = this.db.prepare('SELECT data FROM targets WHERE lane=?').get(lane);
|
|
61
|
+
if (existing) {
|
|
62
|
+
const t = JSON.parse(String(existing.data));
|
|
63
|
+
assertIdentity(t, target);
|
|
64
|
+
return t;
|
|
65
|
+
}
|
|
66
|
+
this.db.prepare('INSERT INTO targets VALUES(?,?,?)').run(target.id, lane, stable(target));
|
|
67
|
+
return target;
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
target(id) {
|
|
71
|
+
const row = this.db.prepare('SELECT data FROM targets WHERE id=?').get(id);
|
|
72
|
+
if (!row)
|
|
73
|
+
fail('notFound', 'Target was not found in the selected profile.');
|
|
74
|
+
const target = JSON.parse(String(row.data));
|
|
75
|
+
if (target.profile !== this.profile || target.server !== this.server)
|
|
76
|
+
fail('binding', 'Registered target belongs to a different profile or server.');
|
|
77
|
+
return target;
|
|
78
|
+
}
|
|
79
|
+
targets() { return this.db.prepare('SELECT data FROM targets ORDER BY id LIMIT 100').all().map(r => this.target(JSON.parse(String(r.data)).id)); }
|
|
80
|
+
submit(value) {
|
|
81
|
+
const f = finding(value);
|
|
82
|
+
assertIdentity(f.identity, this.target(f.targetId));
|
|
83
|
+
return this.transaction(() => {
|
|
84
|
+
const previous = this.db.prepare('SELECT id,hash FROM findings WHERE request_id=? OR id=?').all(f.requestId, f.findingId);
|
|
85
|
+
if (previous.length) {
|
|
86
|
+
if (previous.length !== 1 || previous[0]?.hash !== digest(f))
|
|
87
|
+
fail('idempotency', 'Request or finding ID was already used with different content.');
|
|
88
|
+
return this.getFinding(String(previous[0].id));
|
|
89
|
+
}
|
|
90
|
+
const worker = this.worker(f.workerId);
|
|
91
|
+
assertIdentity(worker.identity, f.identity);
|
|
92
|
+
if (worker.role !== 'reader' || worker.version !== f.baselineVersion)
|
|
93
|
+
fail('binding', 'Finding does not match its registered reader baseline.');
|
|
94
|
+
if (f.resolves) {
|
|
95
|
+
const previous = this.getFinding(f.resolves);
|
|
96
|
+
if (previous.state !== 'conflict_pending' || previous.targetId !== f.targetId || previous.address !== f.address || previous.kind !== f.kind)
|
|
97
|
+
fail('conflict', 'Resolution must reference a conflicting finding for the same scoped comment.');
|
|
98
|
+
}
|
|
99
|
+
this.db.prepare('INSERT INTO findings(id,request_id,hash,target_id,state,data) VALUES(?,?,?,?,?,?)').run(f.findingId, f.requestId, digest(f), f.targetId, 'queued', stable(f));
|
|
100
|
+
return this.getFinding(f.findingId);
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
getFinding(id) {
|
|
104
|
+
const row = this.db.prepare('SELECT * FROM findings WHERE id=?').get(id);
|
|
105
|
+
if (!row)
|
|
106
|
+
fail('notFound', 'Finding was not found.');
|
|
107
|
+
return { ...JSON.parse(String(row.data)), state: row.state, ...(row.batch_id ? { batchId: row.batch_id } : {}), ...(row.conflict ? { conflict: JSON.parse(String(row.conflict)) } : {}) };
|
|
108
|
+
}
|
|
109
|
+
findings(targetId) {
|
|
110
|
+
if (targetId)
|
|
111
|
+
this.target(targetId);
|
|
112
|
+
const rows = targetId ? this.db.prepare('SELECT id FROM findings WHERE target_id=? ORDER BY rowid LIMIT 100').all(targetId) : this.db.prepare('SELECT id FROM findings ORDER BY rowid LIMIT 100').all();
|
|
113
|
+
return rows.map(r => this.getFinding(String(r.id)));
|
|
114
|
+
}
|
|
115
|
+
begin(targetId) {
|
|
116
|
+
this.target(targetId);
|
|
117
|
+
return this.transaction(() => {
|
|
118
|
+
const active = this.db.prepare("SELECT id FROM batches WHERE target_id=? AND state IN ('applying','staged','checking_in','publication_unknown','failed')").get(targetId);
|
|
119
|
+
if (active)
|
|
120
|
+
fail('blocked', 'Publication lane requires recovery before new integration.', this.batch(String(active.id)));
|
|
121
|
+
const rows = this.db.prepare("SELECT id FROM findings WHERE target_id=? AND state='queued' ORDER BY rowid LIMIT 100").all(targetId);
|
|
122
|
+
if (!rows.length)
|
|
123
|
+
return undefined;
|
|
124
|
+
const id = 'b-' + randomUUID();
|
|
125
|
+
const batch = { id, targetId, state: 'applying', ownerPid: process.pid, findings: rows.map(r => this.getFinding(String(r.id))) };
|
|
126
|
+
this.db.prepare('INSERT INTO batches VALUES(?,?,?,?,?)').run(id, targetId, batch.state, process.pid, stable({}));
|
|
127
|
+
for (const row of rows)
|
|
128
|
+
this.db.prepare("UPDATE findings SET state='applying',batch_id=? WHERE id=?").run(id, row.id);
|
|
129
|
+
this.event(id, 'applying', {});
|
|
130
|
+
return this.batch(id);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
batch(id) {
|
|
134
|
+
const row = this.db.prepare('SELECT * FROM batches WHERE id=?').get(id);
|
|
135
|
+
if (!row)
|
|
136
|
+
fail('notFound', 'Publication batch was not found.');
|
|
137
|
+
this.target(String(row.target_id));
|
|
138
|
+
return { ...JSON.parse(String(row.data)), id: String(row.id), targetId: String(row.target_id), state: String(row.state), ownerPid: Number(row.owner_pid), findings: this.db.prepare('SELECT id FROM findings WHERE batch_id=? ORDER BY rowid').all(id).map(r => this.getFinding(String(r.id))) };
|
|
139
|
+
}
|
|
140
|
+
batches() { return this.db.prepare('SELECT id FROM batches ORDER BY rowid DESC LIMIT 100').all().map(r => this.batch(String(r.id))); }
|
|
141
|
+
update(id, state, patch = {}) {
|
|
142
|
+
return this.transaction(() => {
|
|
143
|
+
const current = this.batch(id);
|
|
144
|
+
const { findings: _findings, id: _id, targetId: _targetId, ...data } = { ...current, ...patch, state };
|
|
145
|
+
this.db.prepare('UPDATE batches SET state=?,data=? WHERE id=?').run(state, stable(data), id);
|
|
146
|
+
this.db.prepare('UPDATE findings SET state=? WHERE batch_id=?').run(state, id);
|
|
147
|
+
this.event(id, state, patch);
|
|
148
|
+
return this.batch(id);
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
conflicts(id, conflicts) {
|
|
152
|
+
this.db.prepare('UPDATE findings SET conflict=? WHERE batch_id=?').run(stable(conflicts), id);
|
|
153
|
+
}
|
|
154
|
+
event(id, state, data) { this.db.prepare('INSERT INTO events(batch_id,state,data) VALUES(?,?,?)').run(id, state, stable(data)); }
|
|
155
|
+
events(id) { return this.db.prepare('SELECT seq,state,data FROM events WHERE batch_id=? ORDER BY seq').all(id).map(r => ({ seq: r.seq, state: r.state, data: JSON.parse(String(r.data)) })); }
|
|
156
|
+
worker(id) {
|
|
157
|
+
const row = this.db.prepare('SELECT data,target_id FROM workers WHERE id=?').get(id);
|
|
158
|
+
if (!row)
|
|
159
|
+
fail('notFound', 'Worker was not found.');
|
|
160
|
+
this.target(String(row.target_id));
|
|
161
|
+
return JSON.parse(String(row.data));
|
|
162
|
+
}
|
|
163
|
+
putWorker(id, targetId, binding) {
|
|
164
|
+
assertIdentity(binding.identity, this.target(targetId));
|
|
165
|
+
if (this.db.prepare('SELECT id FROM workers WHERE id=?').get(id))
|
|
166
|
+
fail('workerExists', 'Worker ID is already registered.');
|
|
167
|
+
this.db.prepare('INSERT INTO workers VALUES(?,?,?)').run(id, targetId, stable(binding));
|
|
168
|
+
}
|
|
169
|
+
updateReader(id, binding) {
|
|
170
|
+
const previous = this.worker(id);
|
|
171
|
+
assertIdentity(binding.identity, previous.identity);
|
|
172
|
+
if (previous.pid !== binding.pid || previous.endpoint !== binding.endpoint || previous.version !== binding.version || previous.projectDirectory !== binding.projectDirectory || previous.role !== 'reader' || binding.role !== 'reader' || previous.readEpoch && previous.readEpoch !== binding.readEpoch)
|
|
173
|
+
fail('binding', 'Reader update changed its immutable binding.');
|
|
174
|
+
this.db.prepare('UPDATE workers SET data=? WHERE id=?').run(stable(binding), id);
|
|
175
|
+
}
|
|
176
|
+
workers() { return this.db.prepare('SELECT id,target_id,data FROM workers ORDER BY id LIMIT 100').all().map(r => ({ id: r.id, targetId: r.target_id, ...JSON.parse(String(r.data)) })); }
|
|
177
|
+
removeWorker(id) { this.db.prepare('DELETE FROM workers WHERE id=?').run(id); }
|
|
178
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type CommandDefinition, type CommandContext } from '@eyeauras/cli-factory';
|
|
2
|
+
import { Research } from './research.js';
|
|
3
|
+
import { Workspace, type Scope, type SyncPolicy } from './workspace.js';
|
|
4
|
+
export declare const manualPolicy: SyncPolicy;
|
|
5
|
+
export declare function syncPolicy(value: unknown): SyncPolicy;
|
|
6
|
+
export declare class Synchronization {
|
|
7
|
+
readonly workspace: Workspace;
|
|
8
|
+
readonly research: Research;
|
|
9
|
+
private readonly authorize;
|
|
10
|
+
constructor(workspace: Workspace, research: Research, authorize?: () => Promise<void>);
|
|
11
|
+
get(selected: Scope): {
|
|
12
|
+
publish: "manual" | "after-edit" | "after-save";
|
|
13
|
+
refresh: "manual" | "before-edit";
|
|
14
|
+
session: string;
|
|
15
|
+
target: string;
|
|
16
|
+
};
|
|
17
|
+
set(selected: Scope, policy: SyncPolicy, callerId?: string): Promise<unknown>;
|
|
18
|
+
/** The outer command queue spans refresh, the durable local intent and publication. Inner
|
|
19
|
+
* research queues remain unchanged, including their cross-context ordering and shared lane. */
|
|
20
|
+
execute(selected: Scope, path: string, input: Record<string, unknown>, run: (requestId: string) => Promise<unknown>): Promise<unknown>;
|
|
21
|
+
}
|
|
22
|
+
export declare function synchronizationCommands(service: (c: CommandContext) => Synchronization): CommandDefinition[];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { scope } from './research.js';
|
|
3
|
+
import { fail, id, object, text } from './model.js';
|
|
4
|
+
import { selectedOptions } from './arguments.js';
|
|
5
|
+
export const manualPolicy = { publish: 'manual', refresh: 'manual' };
|
|
6
|
+
export function syncPolicy(value) {
|
|
7
|
+
const p = object(value);
|
|
8
|
+
if (Object.keys(p).some(k => !['publish', 'refresh'].includes(k)) || !['manual', 'after-edit', 'after-save'].includes(String(p.publish)) || !['manual', 'before-edit'].includes(String(p.refresh)))
|
|
9
|
+
fail('invalid', 'Policy requires publish manual/after-edit/after-save and refresh manual/before-edit.');
|
|
10
|
+
return { publish: p.publish, refresh: p.refresh };
|
|
11
|
+
}
|
|
12
|
+
export class Synchronization {
|
|
13
|
+
workspace;
|
|
14
|
+
research;
|
|
15
|
+
authorize;
|
|
16
|
+
constructor(workspace, research, authorize = async () => { }) {
|
|
17
|
+
this.workspace = workspace;
|
|
18
|
+
this.research = research;
|
|
19
|
+
this.authorize = authorize;
|
|
20
|
+
}
|
|
21
|
+
get(selected) { return { ...selected, ...(this.workspace.context(selected).syncPolicy ?? manualPolicy) }; }
|
|
22
|
+
async set(selected, policy, callerId) {
|
|
23
|
+
const reserved = this.workspace.reserveRequest(selected, 'ghidra.sync-policy.set', policy, callerId);
|
|
24
|
+
return this.research.ordered(selected, async () => {
|
|
25
|
+
const request = this.workspace.request(reserved.request.id);
|
|
26
|
+
if (reserved.replay && request.state === 'applied')
|
|
27
|
+
return request.result;
|
|
28
|
+
if (request.state !== 'accepted')
|
|
29
|
+
fail('unknown', 'Policy change has a retained outcome.');
|
|
30
|
+
if (this.workspace.unresolved(selected).length)
|
|
31
|
+
fail('blocked', 'Reconcile the context outcome before changing its synchronization policy.');
|
|
32
|
+
const current = this.workspace.context(selected), result = { ...selected, state: 'configured', ...policy };
|
|
33
|
+
this.workspace.startRequest(request.id);
|
|
34
|
+
this.workspace.completeSavedRequest(selected, request.id, current.generation, { syncPolicy: policy }, result);
|
|
35
|
+
return result;
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
/** The outer command queue spans refresh, the durable local intent and publication. Inner
|
|
39
|
+
* research queues remain unchanged, including their cross-context ordering and shared lane. */
|
|
40
|
+
async execute(selected, path, input, run) {
|
|
41
|
+
const configured = this.workspace.context(selected).syncPolicy ?? manualPolicy;
|
|
42
|
+
const caller = input.requestId === undefined ? undefined : id(input.requestId);
|
|
43
|
+
const previous = caller ? this.workspace.administrationByCaller(caller) : undefined;
|
|
44
|
+
if (!previous && (configured.publish === 'manual' && configured.refresh === 'manual' || input.requestId !== undefined && this.workspace.requestByCaller(id(input.requestId))))
|
|
45
|
+
return run(input.requestId === undefined ? '' : id(input.requestId));
|
|
46
|
+
if (previous && previous.operation !== 'ghidra.automatic')
|
|
47
|
+
fail('idempotency', 'Request ID already identifies another profile administration operation.');
|
|
48
|
+
const policy = previous ? syncPolicy(object(object(previous.intent).parameters).policy) : configured;
|
|
49
|
+
const reserved = this.workspace.reserveAdministration('ghidra.automatic', { ...selected, path, input, policy }, caller);
|
|
50
|
+
const request = this.workspace.administration(reserved.request.id), requestId = 'auto-local-' + request.id;
|
|
51
|
+
if (reserved.replay && request.state === 'applied')
|
|
52
|
+
return request.result;
|
|
53
|
+
if (reserved.replay && request.state === 'failed')
|
|
54
|
+
fail(String(object(request.result ?? {}).failureCode ?? 'coordinator.conflict').replace(/^coordinator\./, ''), 'This automatic workflow retains its original failed result.', request.result);
|
|
55
|
+
if (policy.refresh === 'before-edit' && path !== 'ghidra save' || policy.publish === 'after-edit' || policy.publish === 'after-save' && path === 'ghidra save')
|
|
56
|
+
await this.authorize();
|
|
57
|
+
let progress = object(request.result ?? { phase: 'accepted' });
|
|
58
|
+
const remember = (patch) => { progress = { ...progress, ...patch }; this.workspace.administrationState(request.id, 'unknown', progress); };
|
|
59
|
+
try {
|
|
60
|
+
// Re-entering a dispatched local command always uses its original request identity.
|
|
61
|
+
// A lost merge reply is reconciled by merge itself; no check-in is re-dispatched.
|
|
62
|
+
if (progress.phase === 'accepted' && policy.refresh === 'before-edit' && path !== 'ghidra save') {
|
|
63
|
+
remember({ phase: 'refresh' });
|
|
64
|
+
const incoming = object(await this.research.refresh(selected, 'auto-refresh-' + request.id));
|
|
65
|
+
remember({ incoming, phase: 'ready' });
|
|
66
|
+
if (incoming.state === 'conflict') {
|
|
67
|
+
const result = { ...selected, state: 'conflict', phase: 'refresh', localApplied: false, automatic: { refresh: incoming } };
|
|
68
|
+
this.workspace.administrationState(request.id, 'failed', result);
|
|
69
|
+
fail('conflict', 'Automatic refresh retained conflicts; the requested edit was not started.', result);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (progress.phase === 'refresh') {
|
|
73
|
+
// Refresh's candidate/context commit is already durable or explicitly unresolved.
|
|
74
|
+
const incoming = object(await this.research.refresh(selected, 'auto-refresh-' + request.id));
|
|
75
|
+
remember({ incoming, phase: 'ready' });
|
|
76
|
+
if (incoming.state === 'conflict')
|
|
77
|
+
fail('conflict', 'Automatic refresh retained conflicts; the edit was not started.', { ...selected, state: 'conflict', localApplied: false, automatic: { refresh: incoming } });
|
|
78
|
+
}
|
|
79
|
+
let local = progress.local === undefined ? undefined : object(progress.local);
|
|
80
|
+
if (!local) {
|
|
81
|
+
remember({ phase: 'local' });
|
|
82
|
+
local = object(await run(requestId));
|
|
83
|
+
if (!['applied', 'saved', 'preview'].includes(String(local.state)))
|
|
84
|
+
fail('unknown', 'The local command has no verified saved acknowledgment.', local);
|
|
85
|
+
remember({ local, phase: 'saved' });
|
|
86
|
+
}
|
|
87
|
+
const automatic = { ...(progress.incoming ? { refresh: progress.incoming } : {}) };
|
|
88
|
+
const publish = (policy.publish === 'after-save' && path === 'ghidra save' || policy.publish === 'after-edit') &&
|
|
89
|
+
(path === 'ghidra save' || local.sharedState === 'pending') && Number(local.pendingChanges) > 0;
|
|
90
|
+
if (publish) {
|
|
91
|
+
remember({ phase: 'publish' });
|
|
92
|
+
automatic.publication = await this.research.merge(selected, 'auto-merge-' + request.id);
|
|
93
|
+
}
|
|
94
|
+
const current = this.workspace.context(selected), result = { ...local, pendingChanges: current.scalars.length + current.complexChanges.length,
|
|
95
|
+
...(local.sharedState !== undefined ? { sharedState: current.scalars.length || current.complexChanges.length ? 'pending' : 'shared' } : {}), automatic };
|
|
96
|
+
this.workspace.administrationState(request.id, 'applied', result);
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (this.workspace.administration(request.id).state === 'failed')
|
|
101
|
+
throw error;
|
|
102
|
+
const code = String(error.code ?? 'coordinator.unknown'), detail = error.result, child = this.workspace.requestByCaller(requestId);
|
|
103
|
+
const notStarted = progress.phase === 'local' && (!child || child.state === 'accepted'), rejected = progress.phase === 'local' && child?.state === 'failed' && object(child.result ?? {}).state === 'rejected';
|
|
104
|
+
const knownRejection = rejected || notStarted && code.endsWith('.invalid');
|
|
105
|
+
// Retain the local acknowledgment alongside a failed/uncertain publication. Its caller
|
|
106
|
+
// can distinguish successful editing from a publication that needs intervention.
|
|
107
|
+
const result = { ...selected, state: code.endsWith('.conflict') ? 'conflict' : knownRejection ? 'rejected' : 'unknown', failureCode: code, phase: progress.phase,
|
|
108
|
+
...(progress.local ? { localApplied: true, local: progress.local } : notStarted || rejected ? { localApplied: false } : { localOutcome: progress.phase === 'local' ? 'unknown' : 'not_started' }), automatic: { ...(progress.incoming ? { refresh: progress.incoming } : {}), ...(detail ? { outcome: detail } : {}) } };
|
|
109
|
+
if (code.endsWith('.conflict') || knownRejection)
|
|
110
|
+
this.workspace.administrationState(request.id, 'failed', result);
|
|
111
|
+
else
|
|
112
|
+
this.workspace.administrationState(request.id, 'unknown', { ...progress, outcome: result });
|
|
113
|
+
fail(code.endsWith('.conflict') ? 'conflict' : knownRejection ? code.replace(/^coordinator\./, '') : 'unknown', 'Automatic synchronization did not complete; retained local effects are not replayed.', result);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export function synchronizationCommands(service) {
|
|
118
|
+
return [command('sync-policy', 'Configure opt-in synchronization for one saved tool context', [
|
|
119
|
+
command('get', 'Read the persisted policy; default is manual', ({ options: o }, c) => service(c).get(scope(o)), { permission: Permission.ReadOnly, options: selectedOptions }),
|
|
120
|
+
command('set', 'Choose manual, publication after edit/save and optional refresh before edit', ({ options: o }, c) => service(c).set(scope(o), syncPolicy({ publish: text(o.publish), refresh: text(o.refresh) }), o.requestId === undefined ? undefined : id(o.requestId)), { permission: Permission.Update, options: [...selectedOptions,
|
|
121
|
+
{ flags: '--publish <mode>', description: 'manual, after-edit or after-save', required: true }, { flags: '--refresh <mode>', description: 'manual or before-edit', required: true }, { flags: '--request-id <id>', description: 'Retrieve the recorded policy change' }] }),
|
|
122
|
+
])];
|
|
123
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Descriptor {
|
|
2
|
+
path: string;
|
|
3
|
+
method: string;
|
|
4
|
+
params: {
|
|
5
|
+
name: string;
|
|
6
|
+
source: string;
|
|
7
|
+
}[];
|
|
8
|
+
}
|
|
9
|
+
/** The pinned GhidraMCP HTTP side of a dedicated bridge. No discovery by project name. */
|
|
10
|
+
export declare class HttpBridge {
|
|
11
|
+
readonly endpoint: string;
|
|
12
|
+
private fetch;
|
|
13
|
+
private signal?;
|
|
14
|
+
private token?;
|
|
15
|
+
private timeoutMilliseconds?;
|
|
16
|
+
private schema?;
|
|
17
|
+
constructor(endpoint: string, fetch?: typeof globalThis.fetch, signal?: AbortSignal | undefined, token?: string | undefined, timeoutMilliseconds?: number | undefined);
|
|
18
|
+
private request;
|
|
19
|
+
instance(): Promise<Record<string, unknown>>;
|
|
20
|
+
descriptor(path: string): Promise<Descriptor>;
|
|
21
|
+
call(path: string, params?: Record<string, unknown>): Promise<unknown>;
|
|
22
|
+
}
|
|
23
|
+
export declare function decodeMcp(result: unknown): unknown;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { fail, object } from './model.js';
|
|
2
|
+
import { parseDomainJson } from './json.js';
|
|
3
|
+
import { CliError } from '@eyeauras/cli-factory';
|
|
4
|
+
/** The pinned GhidraMCP HTTP side of a dedicated bridge. No discovery by project name. */
|
|
5
|
+
export class HttpBridge {
|
|
6
|
+
endpoint;
|
|
7
|
+
fetch;
|
|
8
|
+
signal;
|
|
9
|
+
token;
|
|
10
|
+
timeoutMilliseconds;
|
|
11
|
+
schema;
|
|
12
|
+
constructor(endpoint, fetch = globalThis.fetch, signal, token, timeoutMilliseconds) {
|
|
13
|
+
this.endpoint = endpoint;
|
|
14
|
+
this.fetch = fetch;
|
|
15
|
+
this.signal = signal;
|
|
16
|
+
this.token = token;
|
|
17
|
+
this.timeoutMilliseconds = timeoutMilliseconds;
|
|
18
|
+
const url = new URL(endpoint);
|
|
19
|
+
if (url.protocol !== 'http:' || url.hostname !== '127.0.0.1' || !url.port || url.username || url.password || url.pathname !== '/')
|
|
20
|
+
fail('binding', 'Worker bridge must use an explicit loopback HTTP port.');
|
|
21
|
+
if (timeoutMilliseconds !== undefined && (!Number.isSafeInteger(timeoutMilliseconds) || timeoutMilliseconds < 1 || timeoutMilliseconds > 1800000))
|
|
22
|
+
fail('invalid', 'Bridge timeout is outside the supported bound.');
|
|
23
|
+
}
|
|
24
|
+
async request(path, method, query, body) {
|
|
25
|
+
const url = new URL(path, this.endpoint);
|
|
26
|
+
// Import includes synchronous auto-analysis. Keep discovery/ordinary requests short,
|
|
27
|
+
// while allowing a real executable to finish its first analysis without losing the reply.
|
|
28
|
+
const timeoutMilliseconds = this.timeoutMilliseconds ?? (path === '/import_file' ? 600000 : 30000);
|
|
29
|
+
const signal = this.signal ? AbortSignal.any([this.signal, AbortSignal.timeout(timeoutMilliseconds)]) : AbortSignal.timeout(timeoutMilliseconds);
|
|
30
|
+
const transportFailure = () => {
|
|
31
|
+
if (this.signal?.aborted)
|
|
32
|
+
fail('cancelled', `Ghidra request ${path} was cancelled; its outcome may require reconciliation.`, { operation: path });
|
|
33
|
+
if (signal.aborted)
|
|
34
|
+
fail('timeout', `Ghidra request ${path} exceeded ${timeoutMilliseconds / 1000} seconds; its outcome may require reconciliation.`, { operation: path, timeoutMilliseconds });
|
|
35
|
+
fail('transport', `Ghidra request ${path} did not complete; its outcome may require reconciliation.`, { operation: path });
|
|
36
|
+
};
|
|
37
|
+
for (const [name, value] of Object.entries(query))
|
|
38
|
+
url.searchParams.set(name, String(value));
|
|
39
|
+
let response;
|
|
40
|
+
try {
|
|
41
|
+
response = await this.fetch(url, { method, redirect: 'error', signal, headers: { ...(this.token ? { Authorization: 'Bearer ' + this.token } : {}), ...(method === 'POST' ? { 'Content-Type': 'application/json' } : {}) }, ...(method === 'POST' ? { body: JSON.stringify(body) } : {}) });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return transportFailure();
|
|
45
|
+
}
|
|
46
|
+
if (!response.ok)
|
|
47
|
+
fail('remote', 'Ghidra bridge rejected the operation.');
|
|
48
|
+
// Bound even a chunked/malformed service response; never persist raw payloads as logs.
|
|
49
|
+
const reader = response.body?.getReader();
|
|
50
|
+
if (!reader)
|
|
51
|
+
fail('malformed', 'Ghidra bridge returned an empty body.');
|
|
52
|
+
const chunks = [];
|
|
53
|
+
let length = 0;
|
|
54
|
+
try {
|
|
55
|
+
for (;;) {
|
|
56
|
+
const next = await reader.read();
|
|
57
|
+
if (next.done)
|
|
58
|
+
break;
|
|
59
|
+
length += next.value.byteLength;
|
|
60
|
+
if (length > 1024 * 1024)
|
|
61
|
+
fail('limit', 'Ghidra response exceeded one MiB.');
|
|
62
|
+
chunks.push(next.value);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
if (error instanceof CliError)
|
|
67
|
+
throw error;
|
|
68
|
+
return transportFailure();
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
await reader.cancel().catch(() => { });
|
|
72
|
+
}
|
|
73
|
+
const text = Buffer.concat(chunks).toString('utf8');
|
|
74
|
+
let result;
|
|
75
|
+
try {
|
|
76
|
+
result = parseDomainJson(text);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
result = text;
|
|
80
|
+
}
|
|
81
|
+
if (result && typeof result === 'object' && !Array.isArray(result)) {
|
|
82
|
+
const o = object(result);
|
|
83
|
+
if (o.error || o.success === false || o.status === 'error') {
|
|
84
|
+
const reason = typeof o.error === 'string' && o.error.length <= 1024 && !o.error.includes('\0') ? o.error : undefined;
|
|
85
|
+
fail('remote', reason ? 'Ghidra: ' + reason : 'Ghidra operation failed.');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return result;
|
|
89
|
+
}
|
|
90
|
+
async instance() { return object(await this.request('/mcp/instance_info', 'GET', {}, {})); }
|
|
91
|
+
async descriptor(path) {
|
|
92
|
+
if (!this.schema) {
|
|
93
|
+
const schema = object(await this.request('/mcp/schema', 'GET', {}, {}));
|
|
94
|
+
if (!Array.isArray(schema.tools))
|
|
95
|
+
fail('malformed', 'Ghidra schema omitted its tool descriptors.');
|
|
96
|
+
this.schema = new Map(schema.tools.map(value => { const d = object(value); return [d.path, d]; }));
|
|
97
|
+
}
|
|
98
|
+
const tool = this.schema.get(path);
|
|
99
|
+
if (!tool || !['GET', 'POST'].includes(tool.method) || !Array.isArray(tool.params))
|
|
100
|
+
fail('schema', 'Required Ghidra operation is missing from the observed schema.');
|
|
101
|
+
return tool;
|
|
102
|
+
}
|
|
103
|
+
async call(path, params = {}) {
|
|
104
|
+
const tool = await this.descriptor(path);
|
|
105
|
+
const query = {}, body = {};
|
|
106
|
+
for (const [name, value] of Object.entries(params)) {
|
|
107
|
+
const param = tool.params.find(p => p.name === name);
|
|
108
|
+
if (!param)
|
|
109
|
+
fail('schema', 'Required Ghidra operation parameter is missing from the observed schema.');
|
|
110
|
+
(param.source === 'body' ? body : query)[name] = value;
|
|
111
|
+
}
|
|
112
|
+
return this.request(path, tool.method, query, body);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
export function decodeMcp(result) {
|
|
116
|
+
const r = object(result);
|
|
117
|
+
if (r.isError === true || !Array.isArray(r.content))
|
|
118
|
+
fail('remote', 'MCP bridge reported failure or malformed content.');
|
|
119
|
+
const content = r.content.filter(x => x && typeof x === 'object' && x.type === 'text');
|
|
120
|
+
if (content.length !== 1 || typeof content[0]?.text !== 'string' || content[0].text.length > 1024 * 1024)
|
|
121
|
+
fail('malformed', 'Expected one bounded MCP text result.');
|
|
122
|
+
let value;
|
|
123
|
+
try {
|
|
124
|
+
value = parseDomainJson(content[0].text);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
value = content[0].text;
|
|
128
|
+
}
|
|
129
|
+
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
130
|
+
const o = object(value);
|
|
131
|
+
if (o.error || o.success === false)
|
|
132
|
+
fail('remote', 'MCP result content reports failure.');
|
|
133
|
+
}
|
|
134
|
+
return value;
|
|
135
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { object, fail, text } from './model.js';
|
|
2
|
+
import { address, publicAddress } from './research.js';
|
|
3
|
+
export function uiResult(operation, p, raw, selected, program, label, nativeName) {
|
|
4
|
+
const value = object(raw), check = (value) => { if (value !== program && value !== program.slice(program.lastIndexOf('/') + 1) && !(nativeName !== undefined && value === nativeName))
|
|
5
|
+
fail('binding', 'UI selected an undeclared program.'); };
|
|
6
|
+
if (operation === 'tools') {
|
|
7
|
+
if (!Array.isArray(value.tools) || value.tools.length > 100 || value.count !== value.tools.length)
|
|
8
|
+
fail('partial', 'UI tool inventory has inconsistent counts.');
|
|
9
|
+
const tools = value.tools.map(raw => {
|
|
10
|
+
const row = object(raw);
|
|
11
|
+
text(row.name);
|
|
12
|
+
if (typeof row.has_program_manager !== 'boolean')
|
|
13
|
+
fail('partial', 'UI tool omitted its program-manager capability.');
|
|
14
|
+
if (row.current_program !== undefined)
|
|
15
|
+
check(row.current_program);
|
|
16
|
+
if (row.has_program_manager && (!Array.isArray(row.open_programs) || row.open_programs.length > 100))
|
|
17
|
+
fail('partial', 'UI tool omitted its open programs.');
|
|
18
|
+
const open = Array.isArray(row.open_programs) ? row.open_programs.map(value => { check(value); return selected; }) : [];
|
|
19
|
+
return { name: row.name, hasProgramManager: row.has_program_manager, current: row.current_program === undefined ? null : selected, open };
|
|
20
|
+
});
|
|
21
|
+
return { tools, count: tools.length };
|
|
22
|
+
}
|
|
23
|
+
if (operation === 'goto') {
|
|
24
|
+
if (value.success !== true || address(value.address) !== address(p.address))
|
|
25
|
+
fail('partial', 'Navigation did not acknowledge the requested address.');
|
|
26
|
+
return { address: publicAddress(value.address), ...(typeof value.function === 'string' ? { function: value.function } : {}) };
|
|
27
|
+
}
|
|
28
|
+
if (operation === 'prompt_policy') {
|
|
29
|
+
if (typeof value.enabled !== 'boolean' || !Number.isSafeInteger(value.remaining_ms) || Number(value.remaining_ms) < 0 || typeof value.reason !== 'string' || !Array.isArray(value.actions) || value.actions.length > 1000)
|
|
30
|
+
fail('partial', 'Prompt policy status is incomplete.');
|
|
31
|
+
if (p.action === 'enable' && value.enabled !== true || p.action === 'disable' && value.enabled !== false)
|
|
32
|
+
fail('partial', 'Prompt policy did not establish the requested state.');
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
if (operation === 'cursor') {
|
|
36
|
+
const result = {};
|
|
37
|
+
for (const facet of ['address', 'function', 'selection', 'program']) {
|
|
38
|
+
if (!Object.hasOwn(value, facet) && typeof value[facet + '_unavailable'] !== 'string')
|
|
39
|
+
fail('partial', 'Cursor omitted a requested facet.');
|
|
40
|
+
if (value[facet] === null || !Object.hasOwn(value, facet)) {
|
|
41
|
+
if (typeof value[facet + '_unavailable'] !== 'string')
|
|
42
|
+
fail('partial', 'Unavailable cursor facet omitted its reason.');
|
|
43
|
+
result[facet] = null;
|
|
44
|
+
result[facet + '_unavailable'] = value[facet + '_unavailable'];
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
const row = object(value[facet]);
|
|
48
|
+
if (facet === 'program') {
|
|
49
|
+
check(row.path);
|
|
50
|
+
const { path, executable_path, name, ...metadata } = row;
|
|
51
|
+
for (const field of ['language', 'compiler', 'executable_format'])
|
|
52
|
+
text(metadata[field]);
|
|
53
|
+
for (const field of ['function_count', 'symbol_count', 'data_type_count', 'memory_size'])
|
|
54
|
+
if (!Number.isSafeInteger(metadata[field]) || Number(metadata[field]) < 0)
|
|
55
|
+
fail('partial', 'Focused program metadata is incomplete.');
|
|
56
|
+
result.program = { ...metadata, ...selected, name: label };
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
check(row.program);
|
|
60
|
+
const { program: route, ...data } = row;
|
|
61
|
+
if (facet === 'address')
|
|
62
|
+
data.address = publicAddress(row.address);
|
|
63
|
+
if (facet === 'function') {
|
|
64
|
+
text(row.function_name);
|
|
65
|
+
text(row.signature);
|
|
66
|
+
data.address = publicAddress(row.address);
|
|
67
|
+
}
|
|
68
|
+
if (facet === 'selection') {
|
|
69
|
+
if (typeof row.is_empty !== 'boolean' || !Array.isArray(row.ranges) || row.ranges.length > 10000)
|
|
70
|
+
fail('partial', 'Cursor selection is incomplete.');
|
|
71
|
+
data.ranges = row.ranges.map(raw => { const range = object(raw); if (!Number.isSafeInteger(range.length) || Number(range.length) < 1)
|
|
72
|
+
fail('partial', 'Cursor selection range is invalid.'); return { ...range, start: publicAddress(range.start), end: publicAddress(range.end) }; });
|
|
73
|
+
}
|
|
74
|
+
result[facet] = { ...data, ...selected };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return result;
|
|
78
|
+
}
|
|
79
|
+
fail('schema', 'UI operation lacks a result contract.');
|
|
80
|
+
}
|
package/dist/src/ui.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
import { Workspace, type Scope } from './workspace.js';
|
|
3
|
+
import { Research, type ResearchBackend } from './research.js';
|
|
4
|
+
export declare class UserInterface {
|
|
5
|
+
private readonly workspace;
|
|
6
|
+
private readonly research;
|
|
7
|
+
private readonly backend;
|
|
8
|
+
constructor(workspace: Workspace, research: Research, backend: () => Promise<ResearchBackend>);
|
|
9
|
+
read(selected: Scope, operation: 'cursor' | 'tools' | 'plugins_list', facet?: string): Promise<{
|
|
10
|
+
value: Record<string, unknown>;
|
|
11
|
+
session: string;
|
|
12
|
+
target: string;
|
|
13
|
+
}>;
|
|
14
|
+
change(selected: Scope, operation: 'goto' | 'prompt_policy' | 'plugins_set', p: Record<string, unknown>, callerId?: string): Promise<unknown>;
|
|
15
|
+
}
|
|
16
|
+
export declare function uiCommands(service: (c: CommandContext) => UserInterface): CommandDefinition[];
|
|
17
|
+
export declare function pluginCommands(service: (c: CommandContext) => UserInterface): CommandDefinition[];
|