@eyeauras/insight-cli 0.0.0-stage → 0.1.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 +54 -2
- package/SKILL.md +55 -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 +120 -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 +387 -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 +1068 -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 +148 -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 +171 -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/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 +44 -0
- package/dist/src/server.js +455 -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 +45 -4
- package/references/InsightRepositoryProbe.java +55 -0
- package/references/administration.md +33 -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/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/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 +47 -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,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[];
|
package/dist/src/ui.js
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { scope, address, number } from './research.js';
|
|
3
|
+
import { id, text, object, fail } from './model.js';
|
|
4
|
+
import { selectedOptions } from './arguments.js';
|
|
5
|
+
export class UserInterface {
|
|
6
|
+
workspace;
|
|
7
|
+
research;
|
|
8
|
+
backend;
|
|
9
|
+
constructor(workspace, research, backend) {
|
|
10
|
+
this.workspace = workspace;
|
|
11
|
+
this.research = research;
|
|
12
|
+
this.backend = backend;
|
|
13
|
+
}
|
|
14
|
+
async read(selected, operation, facet = 'all') {
|
|
15
|
+
return this.research.ordered(selected, async () => {
|
|
16
|
+
const backend = await this.backend();
|
|
17
|
+
await backend.open(selected);
|
|
18
|
+
if (operation === 'tools' || operation === 'plugins_list')
|
|
19
|
+
await backend.mode(selected, 'gui');
|
|
20
|
+
if (!backend.ui)
|
|
21
|
+
fail('configuration', 'Selected backend lacks the UI adapter.');
|
|
22
|
+
const value = await backend.ui(selected, operation, {});
|
|
23
|
+
return { ...selected, value: operation === 'cursor' && facet !== 'all' ? { [facet]: value[facet], ...(value[facet] === null ? { reason: value[facet + '_unavailable'] } : {}) } : value };
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
async change(selected, operation, p, callerId) {
|
|
27
|
+
const reserved = this.workspace.reserveRequest(selected, 'ghidra.ui.' + operation, p, callerId);
|
|
28
|
+
return this.research.ordered(selected, async () => {
|
|
29
|
+
const request = this.workspace.request(reserved.request.id);
|
|
30
|
+
if (reserved.replay && request.state !== 'accepted') {
|
|
31
|
+
if (request.state === 'applied')
|
|
32
|
+
return request.result;
|
|
33
|
+
fail('unknown', 'UI effect is retained and is not repeated.', request.result);
|
|
34
|
+
}
|
|
35
|
+
const backend = await this.backend();
|
|
36
|
+
await backend.open(selected);
|
|
37
|
+
await backend.mode(selected, 'gui');
|
|
38
|
+
if (!backend.ui)
|
|
39
|
+
fail('configuration', 'Selected backend lacks the UI adapter.');
|
|
40
|
+
const context = this.workspace.context(selected);
|
|
41
|
+
this.workspace.startRequest(request.id, { kind: 'ui', operation, generation: context.generation });
|
|
42
|
+
try {
|
|
43
|
+
const value = await backend.ui(selected, operation, p);
|
|
44
|
+
if (operation === 'goto') {
|
|
45
|
+
const cursor = await backend.ui(selected, 'cursor', {}), location = object(cursor.address);
|
|
46
|
+
if (location.session !== selected.session || location.target !== selected.target || address(location.address) !== address(p.address))
|
|
47
|
+
fail('partial', 'Cursor did not reach the requested context and address.');
|
|
48
|
+
}
|
|
49
|
+
const plugin = operation === 'plugins_set', snapshot = plugin ? await backend.saved(selected) : undefined;
|
|
50
|
+
const complexChanges = plugin ? [...context.complexChanges, request.id] : context.complexChanges;
|
|
51
|
+
const result = { ...selected, state: 'applied', value, ...(plugin ? { sharedState: 'pending', pendingChanges: context.scalars.length + complexChanges.length } : {}) };
|
|
52
|
+
this.workspace.completeSavedRequest(selected, request.id, context.generation, plugin ? { workingSnapshot: snapshot, complexChanges } : {}, result);
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
this.workspace.recordRequest(request.id, 'unknown', { ...selected, state: 'unknown', operation });
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
export function uiCommands(service) {
|
|
63
|
+
const request = { flags: '--request-id <id>', description: 'Retrieve the recorded UI result without repeating it' };
|
|
64
|
+
return [command('ui', 'Inspect and control the selected context interface', [
|
|
65
|
+
command('cursor', 'Read the focused address, function, selection and program', ({ options: o }, c) => {
|
|
66
|
+
const type = text(o.type);
|
|
67
|
+
if (!['all', 'address', 'function', 'selection', 'program'].includes(type))
|
|
68
|
+
fail('invalid', 'Cursor type must be all, address, function, selection or program.');
|
|
69
|
+
return service(c).read(scope(o), 'cursor', type);
|
|
70
|
+
}, { permission: Permission.ReadOnly, options: [...selectedOptions, { flags: '--type <facet>', description: 'Cursor facet or all', defaultValue: 'all' }] }),
|
|
71
|
+
command('tools', 'Read tools in the owned GUI context', ({ options: o }, c) => service(c).read(scope(o), 'tools'), { permission: Permission.ReadOnly, options: selectedOptions }),
|
|
72
|
+
command('goto', 'Navigate the owned browser and verify the cursor', ({ options: o }, c) => service(c).change(scope(o), 'goto', { address: address(o.address) }, o.requestId === undefined ? undefined : id(o.requestId)), { permission: Permission.Update, options: [...selectedOptions, { flags: '--address <address>', description: 'Explicit address', required: true }, request] }),
|
|
73
|
+
command('prompt-policy', 'Temporarily control the owned GUI prompt policy', ({ options: o }, c) => {
|
|
74
|
+
const action = text(o.action);
|
|
75
|
+
if (!['status', 'enable', 'disable'].includes(action))
|
|
76
|
+
fail('invalid', 'Prompt policy action must be status, enable or disable.');
|
|
77
|
+
return service(c).change(scope(o), 'prompt_policy', { action, reason: text(o.reason, 512), seconds: number(o.seconds, 300, 1) }, o.requestId === undefined ? undefined : id(o.requestId));
|
|
78
|
+
}, { permission: Permission.Update, options: [...selectedOptions, { flags: '--action <action>', description: 'status, enable or disable', defaultValue: 'status' },
|
|
79
|
+
{ flags: '--reason <text>', description: 'Recorded policy reason', defaultValue: 'automation' }, { flags: '--seconds <count>', description: 'Requested duration (1–300 seconds)', defaultValue: 120 }, request] }),
|
|
80
|
+
])];
|
|
81
|
+
}
|
|
82
|
+
export function pluginCommands(service) {
|
|
83
|
+
return [command('plugins', 'Configure plugins from enabled managed modules in an owned GUI', [
|
|
84
|
+
command('list', 'Read enabled managed plugins in this GUI tool', ({ options: o }, c) => service(c).read(scope(o), 'plugins_list'), { permission: Permission.ReadOnly, options: selectedOptions }),
|
|
85
|
+
command('set', 'Enable or disable one installed managed plugin with immediate readback', ({ options: o }, c) => { if (o.enabled !== 'true' && o.enabled !== 'false')
|
|
86
|
+
fail('invalid', 'enabled must be true or false.'); return service(c).change(scope(o), 'plugins_set', { plugin: text(o.plugin, 512), enabled: o.enabled === 'true' }, o.requestId === undefined ? undefined : id(o.requestId)); }, { permission: 'Destructive', options: [...selectedOptions, { flags: '--plugin <class>', description: 'Managed plugin ID from extensions capabilities', required: true }, { flags: '--enabled <boolean>', description: 'true or false', required: true }, { flags: '--request-id <id>', description: 'Retrieve the original plugin outcome without replay' }] })
|
|
87
|
+
])];
|
|
88
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CommandDefinition } from '@eyeauras/cli-factory';
|
|
2
|
+
/** Equivalent of the pinned unsigned 64-bit number conversion, without a tool process. */
|
|
3
|
+
export declare function convertNumber(input: unknown, size?: unknown): {
|
|
4
|
+
input: string;
|
|
5
|
+
input_type: string;
|
|
6
|
+
size: number;
|
|
7
|
+
decimal_unsigned: string;
|
|
8
|
+
decimal_signed: string;
|
|
9
|
+
hexadecimal: string;
|
|
10
|
+
binary: string;
|
|
11
|
+
octal: string;
|
|
12
|
+
hex_padded: string;
|
|
13
|
+
};
|
|
14
|
+
export declare function utilityCommands(): CommandDefinition[];
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { command, Permission } from '@eyeauras/cli-factory';
|
|
2
|
+
import { fail, text } from './model.js';
|
|
3
|
+
/** Equivalent of the pinned unsigned 64-bit number conversion, without a tool process. */
|
|
4
|
+
export function convertNumber(input, size = 4) {
|
|
5
|
+
const source = text(input, 128), width = Number(size);
|
|
6
|
+
if (!Number.isSafeInteger(width) || width < 1 || width > 8)
|
|
7
|
+
fail('invalid', 'Size must be 1–8 bytes.');
|
|
8
|
+
let value, inputType;
|
|
9
|
+
try {
|
|
10
|
+
if (/^0[xX][a-fA-F0-9]+$/.test(source)) {
|
|
11
|
+
value = BigInt(source);
|
|
12
|
+
inputType = 'hexadecimal';
|
|
13
|
+
}
|
|
14
|
+
else if (/^0[bB][01]+$/.test(source)) {
|
|
15
|
+
value = BigInt(source);
|
|
16
|
+
inputType = 'binary';
|
|
17
|
+
}
|
|
18
|
+
else if (/^0[0-7]+$/.test(source) && source.length > 1) {
|
|
19
|
+
value = BigInt('0o' + source);
|
|
20
|
+
inputType = 'octal';
|
|
21
|
+
}
|
|
22
|
+
else if (/^\+?\d+$/.test(source)) {
|
|
23
|
+
value = BigInt(source);
|
|
24
|
+
inputType = 'decimal';
|
|
25
|
+
}
|
|
26
|
+
else
|
|
27
|
+
throw Error();
|
|
28
|
+
if (value < 0n || value > 0xffffffffffffffffn)
|
|
29
|
+
throw Error();
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
fail('invalid', 'Number must be an unsigned 64-bit decimal, hexadecimal, binary or octal integer.');
|
|
33
|
+
}
|
|
34
|
+
const bits = BigInt(width * 8), masked = value & ((1n << bits) - 1n), signed = masked >= (1n << (bits - 1n)) ? masked - (1n << bits) : masked, hex = masked.toString(16).toUpperCase();
|
|
35
|
+
return { input: source, input_type: inputType, size: width, decimal_unsigned: masked.toString(), decimal_signed: signed.toString(),
|
|
36
|
+
hexadecimal: '0x' + hex, binary: '0b' + masked.toString(2), octal: '0' + masked.toString(8), hex_padded: '0x' + hex.padStart(width * 2, '0') };
|
|
37
|
+
}
|
|
38
|
+
export function utilityCommands() {
|
|
39
|
+
return [command('utility', 'Stateless research utilities', [
|
|
40
|
+
command('convert-number', 'Convert an unsigned 64-bit integer at an explicit byte width', ({ options: o }) => convertNumber(o.text, o.size), { permission: Permission.ReadOnly, options: [{ flags: '--text <number>', description: 'Decimal, 0x hexadecimal, 0b binary or leading-zero octal', required: true },
|
|
41
|
+
{ flags: '--size <bytes>', description: 'Output width, 1–8 bytes', defaultValue: 4 }] })
|
|
42
|
+
])];
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
/** A native VT object belongs to an existing research session; it is not another agent session. */
|
|
5
|
+
export declare class VersionTracking {
|
|
6
|
+
private readonly workspace;
|
|
7
|
+
private readonly research;
|
|
8
|
+
private readonly backend;
|
|
9
|
+
private readonly tasks;
|
|
10
|
+
constructor(workspace: Workspace, research: Research, backend: () => Promise<ResearchBackend>, tasks: Map<string, Promise<unknown>>);
|
|
11
|
+
private ordered;
|
|
12
|
+
private root;
|
|
13
|
+
private owner;
|
|
14
|
+
private response;
|
|
15
|
+
private native;
|
|
16
|
+
list(selected: Scope, offset: number, limit: number): Promise<{
|
|
17
|
+
items: never[];
|
|
18
|
+
complete: boolean;
|
|
19
|
+
session: string;
|
|
20
|
+
target: string;
|
|
21
|
+
} | {
|
|
22
|
+
nextOffset?: number | undefined;
|
|
23
|
+
items: {
|
|
24
|
+
tracking: string;
|
|
25
|
+
source: Scope;
|
|
26
|
+
destination: Scope;
|
|
27
|
+
sourceSnapshotSha256: string;
|
|
28
|
+
destinationSnapshotSha256: string;
|
|
29
|
+
}[];
|
|
30
|
+
visited: number;
|
|
31
|
+
complete: boolean;
|
|
32
|
+
session: string;
|
|
33
|
+
target: string;
|
|
34
|
+
}>;
|
|
35
|
+
private decisionResult;
|
|
36
|
+
create(selected: Scope, source: Scope, callerId?: string): Promise<unknown>;
|
|
37
|
+
read(selected: Scope, tracking: string, operation: 'matches' | 'markup', p: Record<string, unknown>): Promise<{
|
|
38
|
+
tracking: string;
|
|
39
|
+
source: {
|
|
40
|
+
snapshotSha256: string;
|
|
41
|
+
session: string;
|
|
42
|
+
target: string;
|
|
43
|
+
};
|
|
44
|
+
destination: {
|
|
45
|
+
snapshotSha256: string;
|
|
46
|
+
session: string;
|
|
47
|
+
target: string;
|
|
48
|
+
};
|
|
49
|
+
sourceContextChanged: boolean;
|
|
50
|
+
destinationContextChanged: boolean;
|
|
51
|
+
value: unknown;
|
|
52
|
+
session: string;
|
|
53
|
+
target: string;
|
|
54
|
+
}>;
|
|
55
|
+
decide(selected: Scope, tracking: string, p: Record<string, unknown>, callerId?: string): Promise<unknown>;
|
|
56
|
+
}
|
|
57
|
+
export declare function trackingCommands(service: (c: CommandContext) => VersionTracking): CommandDefinition[];
|