@eyeauras/insight-cli 0.3.2 → 0.4.1

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.
Files changed (141) hide show
  1. package/.cli-package-build.json +1 -1
  2. package/README.md +34 -62
  3. package/SKILL.md +4 -1
  4. package/dist/src/arguments.js +9 -4
  5. package/dist/src/cli.js +45 -37
  6. package/dist/src/code-references.d.ts +48 -0
  7. package/dist/src/code-references.js +154 -0
  8. package/dist/src/context-diagnostics.d.ts +25 -0
  9. package/dist/src/context-diagnostics.js +82 -0
  10. package/dist/src/contexts.d.ts +13 -1
  11. package/dist/src/contexts.js +38 -9
  12. package/dist/src/function-boundaries.d.ts +9 -0
  13. package/dist/src/function-boundaries.js +68 -0
  14. package/dist/src/function-entries.d.ts +33 -0
  15. package/dist/src/function-entries.js +84 -0
  16. package/dist/src/function-inspection.d.ts +4 -2
  17. package/dist/src/function-inspection.js +94 -59
  18. package/dist/src/function-results.d.ts +3 -1
  19. package/dist/src/function-results.js +9 -3
  20. package/dist/src/inline-script.d.ts +15 -0
  21. package/dist/src/inline-script.js +62 -0
  22. package/dist/src/internal-timing.d.ts +3 -0
  23. package/dist/src/internal-timing.js +52 -0
  24. package/dist/src/lifecycle.js +14 -4
  25. package/dist/src/native-inputs.d.ts +1 -1
  26. package/dist/src/native-inputs.js +1 -1
  27. package/dist/src/native-queries.js +1 -1
  28. package/dist/src/persistent-client.d.ts +30 -0
  29. package/dist/src/persistent-client.js +125 -0
  30. package/dist/src/queries.js +4 -1
  31. package/dist/src/query-policy.js +9 -1
  32. package/dist/src/research-view.d.ts +6 -0
  33. package/dist/src/research-view.js +9 -0
  34. package/dist/src/research.d.ts +38 -0
  35. package/dist/src/research.js +78 -8
  36. package/dist/src/string-anchors.d.ts +43 -0
  37. package/dist/src/string-anchors.js +176 -0
  38. package/dist/src/sync-policy.js +3 -0
  39. package/dist/src/timed-backend.d.ts +3 -0
  40. package/dist/src/timed-backend.js +16 -0
  41. package/dist/src/transport.d.ts +1 -0
  42. package/dist/src/transport.js +4 -0
  43. package/dist/src/workspace.js +5 -2
  44. package/package.json +12 -1
  45. package/references/native/InsightAnchorScan.java +135 -0
  46. package/references/native/InsightCodeReferences.java +129 -0
  47. package/references/native/InsightFunctionBoundaries.java +56 -0
  48. package/references/native/InsightInlineExecutor.java +143 -0
  49. package/references/native/InsightInlineScript.java +14 -0
  50. package/references/native/InsightPeRanges.java +92 -0
  51. package/references/native/InsightResearchQueries.java +22 -1
  52. package/references/native/InsightResearchView.java +69 -0
  53. package/references/native/InsightStringAnchors.java +49 -0
  54. package/references/persistent-client.mjs +125 -0
  55. package/references/persistent.md +50 -0
  56. package/references/query-semantics.md +12 -0
  57. package/references/recovery.md +36 -0
  58. package/references/research.md +87 -0
  59. package/references/setup.md +1 -1
  60. package/references/sparse-navigation.md +173 -0
  61. package/runtime/GhidraMCP-7.0.0.jar +0 -0
  62. package/runtime/GhidraMCP-NOTICE +5 -0
  63. package/runtime/provenance.json +17 -3
  64. package/runtime/research-view.provenance.json +28 -0
  65. package/runtime/toolbox.json +10 -3
  66. package/skill-bundle/insight-artifacts/SKILL.md +1 -1
  67. package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
  68. package/skill-bundle/insight-binary/SKILL.md +1 -1
  69. package/skill-bundle/insight-capture/SKILL.md +1 -1
  70. package/skill-bundle/insight-cli/SKILL.md +4 -1
  71. package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
  72. package/skill-bundle/insight-cli/references/persistent.md +50 -0
  73. package/skill-bundle/insight-cli/references/recovery.md +36 -0
  74. package/skill-bundle/insight-cli/references/setup.md +1 -1
  75. package/skill-bundle/insight-die/SKILL.md +1 -1
  76. package/skill-bundle/insight-dnspy/SKILL.md +1 -1
  77. package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
  78. package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
  79. package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
  80. package/skill-bundle/insight-frida/SKILL.md +1 -1
  81. package/skill-bundle/insight-ghidra/SKILL.md +14 -1
  82. package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
  83. package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
  84. package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
  85. package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
  86. package/skill-bundle/insight-ghidra/references/research.md +87 -0
  87. package/skill-bundle/insight-ghidra/references/setup.md +1 -1
  88. package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
  89. package/skill-bundle/insight-ilspy/SKILL.md +1 -1
  90. package/skill-bundle/insight-interop/SKILL.md +1 -1
  91. package/skill-bundle/insight-lldb/SKILL.md +1 -1
  92. package/skill-bundle/insight-llvm/SKILL.md +1 -1
  93. package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
  94. package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
  95. package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
  96. package/skill-bundle/insight-overview/SKILL.md +1 -1
  97. package/skill-bundle/insight-procmon/SKILL.md +1 -1
  98. package/skill-bundle/insight-re/SKILL.md +4 -1
  99. package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
  100. package/skill-bundle/insight-re/references/persistent.md +50 -0
  101. package/skill-bundle/insight-re/references/recovery.md +36 -0
  102. package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
  103. package/skill-bundle/insight-strace/SKILL.md +1 -1
  104. package/skill-bundle/insight-tshark/SKILL.md +1 -1
  105. package/skill-bundle/insight-windbg/SKILL.md +1 -1
  106. package/skill-bundle/manifest.json +1 -1
  107. package/skills/insight-artifacts/SKILL.md +1 -1
  108. package/skills/insight-artifacts/references/recovery.md +36 -0
  109. package/skills/insight-binary/SKILL.md +1 -1
  110. package/skills/insight-capture/SKILL.md +1 -1
  111. package/skills/insight-die/SKILL.md +1 -1
  112. package/skills/insight-dnspy/SKILL.md +1 -1
  113. package/skills/insight-dotnet-dump/SKILL.md +1 -1
  114. package/skills/insight-dotnet-stack/SKILL.md +1 -1
  115. package/skills/insight-dotnet-trace/SKILL.md +1 -1
  116. package/skills/insight-frida/SKILL.md +1 -1
  117. package/skills/insight-ghidra/SKILL.md +14 -1
  118. package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
  119. package/skills/insight-ghidra/references/persistent.md +50 -0
  120. package/skills/insight-ghidra/references/query-semantics.md +12 -0
  121. package/skills/insight-ghidra/references/recovery.md +36 -0
  122. package/skills/insight-ghidra/references/research.md +87 -0
  123. package/skills/insight-ghidra/references/setup.md +1 -1
  124. package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
  125. package/skills/insight-ilspy/SKILL.md +1 -1
  126. package/skills/insight-interop/SKILL.md +1 -1
  127. package/skills/insight-lldb/SKILL.md +1 -1
  128. package/skills/insight-llvm/SKILL.md +1 -1
  129. package/skills/insight-managed-heap/SKILL.md +1 -1
  130. package/skills/insight-managed-metadata/SKILL.md +1 -1
  131. package/skills/insight-mitmproxy/SKILL.md +1 -1
  132. package/skills/insight-overview/SKILL.md +1 -1
  133. package/skills/insight-procmon/SKILL.md +1 -1
  134. package/skills/insight-re/SKILL.md +4 -1
  135. package/skills/insight-re/references/persistent-client.mjs +125 -0
  136. package/skills/insight-re/references/persistent.md +50 -0
  137. package/skills/insight-re/references/recovery.md +36 -0
  138. package/skills/insight-sevenzip/SKILL.md +1 -1
  139. package/skills/insight-strace/SKILL.md +1 -1
  140. package/skills/insight-tshark/SKILL.md +1 -1
  141. package/skills/insight-windbg/SKILL.md +1 -1
@@ -1 +1 @@
1
- {"version":1,"artifact":"0e495563091ce0b79877fc3050a2c51dca9631c45e07010a464f315bb3d56b58"}
1
+ {"version":1,"artifact":"89b2f68ac98cf2bf71c2fc63c4af3a2014054f9b492eb3483ff41990092fb956"}
package/README.md CHANGED
@@ -1,86 +1,58 @@
1
1
  # insight-cli
2
2
 
3
- CLI/API for reverse engineering with durable sessions, isolated Ghidra contexts,
4
- explicit merge/refresh and ordinary saved files. Agents choose the investigation.
3
+ A CLI/API for reverse-engineering agents. Spend more time understanding a program and
4
+ less time managing Ghidra processes or writing scripts around missing analysis metadata.
5
+
6
+ - Search raw strings and code references, and inspect functions in sparse Ghidra databases.
7
+ - Give each agent an isolated research context, with saved work and explicit merge/refresh.
8
+ - Use structured JSON results and a persistent client for sequences of research calls.
5
9
 
6
10
  ## Install
7
11
 
8
- Use Node.js 24 or newer:
12
+ Requires **Node.js 24+**:
9
13
 
10
14
  ```sh
11
15
  npm install --global @eyeauras/insight-cli
12
- insight-cli --help
13
- insight-cli skill insight-re
14
- ```
15
-
16
- The executable is `insight-cli`. Its global npm executable directory must be on PATH.
17
- No source checkout, TypeScript build or separate Factory installation is required.
18
- The pinned Factory IPC runtime is a normal npm dependency fetched from an immutable
19
- GitHub tarball URL; installation therefore needs npm registry and GitHub access.
20
- Use `insight-cli ghidra install` to deliver the pinned Ghidra/JDK/native/server runtime.
21
- External Ghidra launchers and Java paths are also supported.
22
- `insight-cli ilspy install` delivers ILSpy and its private .NET runtime for managed-code
23
- research. Tool downloads use pinned assets retained in our GitHub Releases.
24
- Use `insight-cli tools processes` to inspect owned executions across tools.
25
- Help, skill discovery and saving ordinary files do not need Ghidra or Java.
26
-
27
- Downloading a tool does not expose it in an agent's tool list. Connect the packaged skills
28
- to your agent host, or explicitly give the agent the path returned by `insight-cli skill
29
- insight-re --json`. See [agent setup](references/agent-setup.md) for tested project-local
30
- Codex setup and a practical usability check. Installation leaves agent settings unchanged.
31
-
32
- ## Discover the toolbox
33
-
34
- ```sh
35
- insight-cli tools list --json
36
- insight-cli tools show die --json
37
- insight-cli skill insight-die --json
16
+ insight-cli profile create local
17
+ insight-cli profile set-default local
18
+ insight-cli permissions grant Update
19
+ insight-cli ghidra install
38
20
  ```
39
21
 
40
- The catalog describes the adapters and their packaged skills before any runtime is downloaded.
41
- Use each tool's `status` to check the selected profile, then follow its skill and generated help.
42
- The toolbox includes Ghidra, ILSpy, 7-Zip, LLVM utilities, DiE, LLDB, Frida, WinDbg, strace and .NET dump/stack/trace; platform
43
- and capability limits are explicit per tool. A partial classification or a failed native process
44
- is not a complete finding. Preserve useful output in ordinary project files and checkpoint it
45
- with `artifacts save` when useful. Existing project RE instructions retain precedence.
22
+ The last command downloads and verifies compatible Ghidra and JDK runtimes. No source
23
+ checkout or separate Java installation is needed. See [runtime setup](https://github.com/iXab3r/insight-cli/blob/main/skills/ghidra-coordinator/references/managed-runtime.md)
24
+ for existing installations and offline setup.
46
25
 
47
- ## Start
26
+ For an AI agent, [connect the packaged skills](https://github.com/iXab3r/insight-cli/blob/main/skills/ghidra-coordinator/references/agent-setup.md).
27
+ For repeated API calls, use the [persistent client](https://github.com/iXab3r/insight-cli/blob/main/skills/ghidra-coordinator/references/persistent.md).
48
28
 
49
- ```sh
50
- insight-cli profile create local
51
- insight-cli --profile local permissions grant Update
52
- insight-cli --profile local ghidra install
53
- insight-cli --profile local ghidra status
54
- insight-cli --profile local ghidra open ./client.exe --session new --json
55
- ```
29
+ ## Quick example
56
30
 
57
- Retain the returned `session` and `target` and supply both on research calls:
31
+ Open a binary in a new private research context:
58
32
 
59
33
  ```sh
60
- insight-cli --profile local ghidra strings search --session <session> --target <target> --contains config.json --json
61
- insight-cli --profile local ghidra functions decompile --session <session> --target <target> --address <entry> --json
62
- insight-cli --profile local ghidra merge --session <session> --target <target> --json
34
+ insight-cli ghidra open ./client.exe --session new --json
63
35
  ```
64
36
 
65
- Another agent uses `--session new` for its own context on the same input. It receives
66
- published changes with `ghidra refresh`; local work and conflicts remain explicit.
67
- Managed local startup needs no separately configured Ghidra Server.
68
-
69
- Save a completed project file or folder when a retained checkpoint is useful:
37
+ Reuse the returned session and target IDs in place of `SESSION` and `TARGET`:
70
38
 
71
39
  ```sh
72
- insight-cli --profile local artifacts save ./analysis --json
40
+ insight-cli ghidra strings anchors --session SESSION --target TARGET \
41
+ --text config --encodings ascii,utf16le --json
42
+ insight-cli sessions status SESSION --target TARGET --json
73
43
  ```
74
44
 
75
- The response returns an ordinary saved path and digest. Follow existing project RE
76
- rules, formats and evidence locations; the packaged RE skill fills unspecified gaps.
77
- Installation does not register skills or change agent settings. Use `insight-cli skill`
78
- and `insight-cli skill insight-re` to locate packaged instructions and references.
45
+ The search reads raw literal bytes, so it does not require defined Ghidra strings.
46
+ Results include scan coverage and a continuation cursor when incomplete. New imports
47
+ skip full automatic analysis by default; add `--analysis` to the first `open` to opt in.
48
+ Continue with the [research guide](https://github.com/iXab3r/insight-cli/blob/main/skills/ghidra-coordinator/references/research.md).
79
49
 
80
- Full documentation and verification: [GitHub](https://github.com/iXab3r/insight-cli).
81
- License: MIT. The bundled GhidraMCP bridge has its own Apache-2.0 license and notice.
50
+ ## Benchmarks
82
51
 
83
- Runtime management, local archives and external paths: [managed runtime](references/managed-runtime.md).
84
- The host debugger and OS permissions are checked separately by `ghidra doctor`.
52
+ In a fresh-agent PoE 2 investigation, installed insight-cli 0.4.0 with its persistent
53
+ client finished in **16:35**, versus **19:43** through Ghidra MCP: **15.9% less wall time**,
54
+ with comparable core findings. This is one adaptive research pair, not a general speed guarantee.
85
55
 
86
- Static .NET commands and ordinary output files: [ILSpy](references/ilspy.md).
56
+ [Benchmarks](https://github.com/iXab3r/insight-cli/blob/main/BENCHMARKS.md) includes phases, earlier runs and comparison limits.
57
+ [Development history and detailed reports](https://github.com/iXab3r/insight-cli/blob/main/docs/PROJECT-HISTORY.md) covers architecture,
58
+ acceptance, other experiments and releases. See [Contributing](https://github.com/iXab3r/insight-cli/blob/main/CONTRIBUTING.md) for development.
package/SKILL.md CHANGED
@@ -3,7 +3,7 @@ name: insight-cli
3
3
  description: Choose and prepare reverse-engineering capabilities from the insight-cli toolbox for binary, managed-code and runtime investigations. Use to discover the appropriate tool or its packaged skill before installing native runtimes.
4
4
  ---
5
5
 
6
- # insight-cli 0.3.2
6
+ # insight-cli 0.4.1
7
7
 
8
8
  If local help/status reports AppData or IPC access denied, retry that **read-only probe**
9
9
  with the agent host's normal permission escalation, then use that route for later authorized
@@ -15,6 +15,9 @@ request/status recovery, never blind retry.
15
15
  Read the project's RE instructions and existing findings first. They own tool choices,
16
16
  evidence layout, formats and source of truth. These skills fill the remaining gaps.
17
17
 
18
+ For repeated research calls, retain one [persistent CLI client](references/persistent.md)
19
+ in the agent's long-lived runtime. Reuse it across calls to avoid repeated frontend startup.
20
+
18
21
  1. Run `insight-cli tools list --json` to discover the capabilities shipped in this version.
19
22
  The catalog works before profile configuration, downloads or IPC startup. A downloaded
20
23
  runtime is not needed to discover what a tool does.
@@ -23,8 +23,10 @@ export function parsed(p, value) {
23
23
  fail('invalid', `${flag(p.name)} is outside its supported query range.`);
24
24
  if (p.name === 'timeout_seconds' && (result < 1 || result > 1800))
25
25
  fail('invalid', 'timeout-seconds must be between 1 and 1800.');
26
- if (p.name === 'max_scan' && (result < 1 || result > 200000) || p.name === 'max_bytes' && (result < 1 || result > 16777216))
27
- fail('invalid', `${flag(p.name)} is outside its supported work budget.`);
26
+ if (p.name === 'max_scan' && (result < 1 || result > 200000))
27
+ fail('invalid', 'max-scan must be between 1 and 200000 visited inputs; result offset does not advance scan coverage.');
28
+ if (p.name === 'max_bytes' && (result < 1 || result > 16777216))
29
+ fail('invalid', 'max-bytes must be between 1 and 16777216 bytes.');
28
30
  return result;
29
31
  }
30
32
  if (['json', 'object', 'array', 'any'].includes(p.type)) {
@@ -55,8 +57,11 @@ export function option(p) {
55
57
  const value = p.default;
56
58
  const defaultValue = value === undefined || value === '' && !p.allowEmpty ? undefined : parsed(p, value);
57
59
  return { flags: `--${flag(p.name)} <${p.type === 'json' ? 'json' : p.type}>`,
58
- description: p.semantic === 'function_ref' ? 'Function name or explicit address' : p.semantic === 'address' ? 'Explicit hexadecimal address' :
59
- `${flag(p.name)} (${p.type}${p.name === 'limit' ? ', 1–1000' : p.name === 'length' ? ', 1–65536 bytes' : ''})`,
60
+ description: p.name === 'max_scan' ? 'Maximum visited inputs, 1–200000; independent of result offset' :
61
+ p.name === 'max_bytes' ? 'Maximum searched bytes, 1–16777216' :
62
+ p.name === 'offset' && p.default !== '-1' ? 'Skip matching result rows; does not continue an incomplete scan' :
63
+ p.semantic === 'function_ref' ? 'Function name or explicit address' : p.semantic === 'address' ? 'Explicit hexadecimal address' :
64
+ `${flag(p.name)} (${p.type}${p.name === 'limit' ? ', 1–1000' : p.name === 'length' ? ', 1–65536 bytes' : ''})`,
60
65
  ...(p.required ? { required: true } : {}), ...(defaultValue === undefined ? {} : { defaultValue }),
61
66
  ...(['json', 'object', 'array', 'any'].includes(p.type) ? {} : { parse: (value) => parsed(p, value) }) };
62
67
  }
package/dist/src/cli.js CHANGED
@@ -52,6 +52,11 @@ import { RuntimeManager, runtimeCommands, runtimeProcesses } from './runtime-man
52
52
  import { packageVersion } from './package-version.js';
53
53
  import { runtimeSelection } from './runtime-selection.js';
54
54
  import { AnalysisStorage, analysisDirectory } from './analysis-storage.js';
55
+ import { codeReferenceCommands } from './code-references.js';
56
+ import { stringAnchorCommands } from './string-anchors.js';
57
+ import { contextDiagnosticCommands } from './context-diagnostics.js';
58
+ import { traceCommand } from './internal-timing.js';
59
+ import { timedBackend } from './timed-backend.js';
55
60
  import { readInitialProgress } from './initial-progress.js';
56
61
  const bindingFields = ['java', 'ghidraHome', 'mcpJar', 'mcpJarSha256', 'server', 'repository', 'readerUser', 'publisherUser', 'maxContexts', 'debuggerPython', 'windbgDir', 'archiveUrl'];
57
62
  class Owner {
@@ -109,7 +114,7 @@ class Owner {
109
114
  const result = await selected.backend;
110
115
  if (result instanceof GhidraContexts)
111
116
  await managedExtensions(selected.workspace.directory);
112
- return result instanceof GhidraContexts ? result.fork(c) : result;
117
+ return timedBackend(result instanceof GhidraContexts ? result.fork(c) : result);
113
118
  };
114
119
  const research = new Research(profile.workspace, backend, profile.tasks);
115
120
  return { workspace: profile.workspace, research, synchronization: new Synchronization(profile.workspace, research, async () => {
@@ -231,47 +236,50 @@ function scopedFailures(entry, owner, parents = []) {
231
236
  const path = [...parents, entry.name.split(' ')[0]], name = path.join(' ');
232
237
  const run = entry.run;
233
238
  return { ...entry, ...(entry.children ? { children: entry.children.map(child => scopedFailures(child, owner, path)) } : {}), ...(run ? { run: async (input, c) => {
234
- try {
235
- let selected;
239
+ const execute = async () => {
236
240
  try {
237
- selected = scope(input.options);
238
- if (selected.session === 'new')
239
- selected = undefined;
241
+ let selected;
242
+ try {
243
+ selected = scope(input.options);
244
+ if (selected.session === 'new')
245
+ selected = undefined;
246
+ }
247
+ catch { }
248
+ if (!selected)
249
+ return await run(input, c);
250
+ const services = owner.selected(c);
251
+ // First open of an already known target reserves its context inside Research.open.
252
+ // Existing contexts still use the command boundary to order automatic sync phases.
253
+ if (path[1] === 'open')
254
+ try {
255
+ services.workspace.context(selected);
256
+ }
257
+ catch (error) {
258
+ if (error instanceof CliError && error.code === 'coordinator.notFound')
259
+ return await run(input, c);
260
+ throw error;
261
+ }
262
+ return await services.research.boundary(selected, async () => {
263
+ const excluded = ['open', 'merge', 'refresh', 'sync-policy', 'close', 'changes', 'debugger', 'ui', 'version-tracking', 'archive', 'projects', 'repository', 'server', 'formats', 'libraries', 'plugins'];
264
+ const eligible = entry.permission !== Permission.ReadOnly && entry.permission !== undefined && !excluded.includes(path[1]) && !['ghidra types archive-export', 'ghidra documentation transfer'].includes(name);
265
+ return eligible ? services.synchronization.execute(selected, name, { ...input.options, commandArguments: input.args }, async (requestId) => run({ ...input, options: { ...input.options, ...(requestId ? { requestId } : {}) } }, c)) : run(input, c);
266
+ });
240
267
  }
241
- catch { }
242
- if (!selected)
243
- return await run(input, c);
244
- const services = owner.selected(c);
245
- // First open of an already known target reserves its context inside Research.open.
246
- // Existing contexts still use the command boundary to order automatic sync phases.
247
- if (path[1] === 'open')
268
+ catch (error) {
269
+ let selected;
248
270
  try {
249
- services.workspace.context(selected);
271
+ selected = scope(input.options);
250
272
  }
251
- catch (error) {
252
- if (error instanceof CliError && error.code === 'coordinator.notFound')
253
- return await run(input, c);
273
+ catch {
254
274
  throw error;
255
275
  }
256
- return await services.research.boundary(selected, async () => {
257
- const excluded = ['open', 'merge', 'refresh', 'sync-policy', 'close', 'changes', 'debugger', 'ui', 'version-tracking', 'archive', 'projects', 'repository', 'server', 'formats', 'libraries', 'plugins'];
258
- const eligible = entry.permission !== Permission.ReadOnly && entry.permission !== undefined && !excluded.includes(path[1]) && !['ghidra types archive-export', 'ghidra documentation transfer'].includes(name);
259
- return eligible ? services.synchronization.execute(selected, name, { ...input.options, commandArguments: input.args }, async (requestId) => run({ ...input, options: { ...input.options, ...(requestId ? { requestId } : {}) } }, c)) : run(input, c);
260
- });
261
- }
262
- catch (error) {
263
- let selected;
264
- try {
265
- selected = scope(input.options);
266
- }
267
- catch {
268
- throw error;
276
+ if (!(error instanceof CliError))
277
+ throw error;
278
+ const result = error.result && typeof error.result === 'object' && !Array.isArray(error.result) ? error.result : {};
279
+ throw new CliError(error.message, { code: error.code, exitCode: error.exitCode, next: error.next, result: { ...result, ...selected } });
269
280
  }
270
- if (!(error instanceof CliError))
271
- throw error;
272
- const result = error.result && typeof error.result === 'object' && !Array.isArray(error.result) ? error.result : {};
273
- throw new CliError(error.message, { code: error.code, exitCode: error.exitCode, next: error.next, result: { ...result, ...selected } });
274
- }
281
+ };
282
+ return path[1] === 'open' || input.options.session && input.options.target ? traceCommand(join(analysisDirectory(c), 'workspace'), name, execute) : execute();
275
283
  } } : {}) };
276
284
  }
277
285
  export function createDefinition(options = {}) {
@@ -424,7 +432,7 @@ export function createDefinition(options = {}) {
424
432
  { flags: '--loader <name>', description: 'Explicit installed loader name/ID from formats loaders' },
425
433
  { flags: '--loader-options <json>', description: 'Named primitive SDK loader options, requiring --loader' }] }),
426
434
  command('strings', 'Read defined strings', [
427
- command('search', 'Find literal text and usable addresses', ({ options: o }, c) => owner.selected(c).research.strings(scope(o), text(o.contains, 4096), number(o.limit, 100, 1), number(o.offset, 2147483647)), { permission: Permission.ReadOnly, options: [...scoped, { flags: '--contains <text>', description: 'Literal case-insensitive substring', required: true }, ...paged] }),
435
+ command('search', 'Find literal text in defined string data; undefined literals: analysis search-byte-patterns', ({ options: o }, c) => owner.selected(c).research.strings(scope(o), text(o.contains, 4096), number(o.limit, 100, 1), number(o.offset, 2147483647)), { permission: Permission.ReadOnly, options: [...scoped, { flags: '--contains <text>', description: 'Literal case-insensitive substring', required: true }, ...paged] }),
428
436
  ]),
429
437
  command('xrefs', 'Follow cross references', [
430
438
  command('to', 'Find incoming references and their functions', ({ options: o }, c) => owner.selected(c).research.xrefs(scope(o), address(o.address), number(o.limit, 100, 1), number(o.offset, 2147483647)), { permission: Permission.ReadOnly, options: [...scoped, at, ...paged] }),
@@ -508,7 +516,7 @@ export function createDefinition(options = {}) {
508
516
  ],
509
517
  };
510
518
  const commands = definition.commands?.map(entry => entry.name === 'ghidra' ?
511
- scopedFailures({ ...entry, children: extendQueries(extendQueries(extendQueries(entry.children ?? [], queryCommands(c => owner.selected(c).research)), mutationCommands(c => owner.selected(c).research)), [...sdkCommands(c => owner.selected(c).research), ...sdkFileCommands(c => owner.selected(c).sdkFiles), ...trackingCommands(c => owner.selected(c).tracking), ...crossCommands(c => owner.selected(c).research), ...administrationCommands(c => owner.selected(c).administration, options.backend ? undefined : async (c) => { const status = await new RuntimeManager(c).status(); return status.state === 'not_ready' ? { ...status.server, connected: false, state: 'runtime_not_ready', missing: status.missing } : undefined; }),
519
+ scopedFailures({ ...entry, children: extendQueries(extendQueries(extendQueries(entry.children ?? [], queryCommands(c => owner.selected(c).research)), mutationCommands(c => owner.selected(c).research)), [...codeReferenceCommands(c => owner.selected(c).research), ...stringAnchorCommands(c => owner.selected(c).research), ...contextDiagnosticCommands(c => owner.selected(c).research, c => owner.selected(c).workspace), ...sdkCommands(c => owner.selected(c).research), ...sdkFileCommands(c => owner.selected(c).sdkFiles), ...trackingCommands(c => owner.selected(c).tracking), ...crossCommands(c => owner.selected(c).research), ...administrationCommands(c => owner.selected(c).administration, options.backend ? undefined : async (c) => { const status = await new RuntimeManager(c).status(); return status.state === 'not_ready' ? { ...status.server, connected: false, state: 'runtime_not_ready', missing: status.missing } : undefined; }),
512
520
  ...extensionCommands(c => new Extensions(join(analysisDirectory(c), 'workspace'))), ...extensionInventoryCommands(c => owner.selected(c).backend), ...pluginCommands(c => owner.selected(c).ui), ...synchronizationCommands(c => owner.selected(c).synchronization), ...utilityCommands(), ...runtimeCommands((c, action, quiescent) => owner.runtime(c, action, quiescent)), ...lifecycleCommands(c => owner.selected(c).lifecycle, c => new RuntimeManager(c).status()), ...projectCommands(c => owner.selected(c).projects), ...uiCommands(c => owner.selected(c).ui), ...debuggerCommands(c => owner.selected(c).debugger), ...archiveCommands(c => owner.selected(c).archive)]) }, owner) : entry);
513
521
  return { ...definition, commands: [...(commands ?? []).map(e => storage.wrap(e)), storage.commands()] };
514
522
  }
@@ -0,0 +1,48 @@
1
+ import { type CommandContext } from '@eyeauras/cli-factory';
2
+ import { type Research, type ResearchBackend } from './research.js';
3
+ import type { Scope } from './workspace.js';
4
+ export declare const referenceAlgorithm = "x86-64-code-reference-candidates-v1";
5
+ export declare const referenceForms: readonly ["call-rel32", "lea-rip", "mov-rip", "call-rip-slot"];
6
+ type Form = typeof referenceForms[number];
7
+ export interface ReferenceQuery {
8
+ targets: string[];
9
+ forms: Form[];
10
+ start: string;
11
+ end: string;
12
+ }
13
+ export interface ReferenceBudgets {
14
+ max_bytes: number;
15
+ max_candidates: number;
16
+ timeout_seconds: number;
17
+ limit: number;
18
+ }
19
+ export interface ReferenceRequest {
20
+ query: ReferenceQuery;
21
+ from: string;
22
+ budgets: ReferenceBudgets;
23
+ expectedView?: string;
24
+ }
25
+ export declare function referenceRequest(o: Record<string, unknown>, who: Scope): ReferenceRequest;
26
+ /** The deliberately small encoding contract, independently rechecked in the facade. */
27
+ export declare function referenceEncoding(input: Buffer): {
28
+ form: "call-rel32" | "lea-rip" | "mov-rip" | "call-rip-slot";
29
+ length: number;
30
+ displacement: number;
31
+ operand: number;
32
+ bits: number;
33
+ delta: number;
34
+ role: string;
35
+ } | undefined;
36
+ export declare function referenceResult(raw: unknown, r: ReferenceRequest): Record<string, unknown>;
37
+ export declare function inspectCodeReferences(backend: ResearchBackend, who: Scope, r: ReferenceRequest, view: string, observation: unknown): Promise<{
38
+ value: {
39
+ view: {
40
+ identity: string;
41
+ };
42
+ cursor: string | null;
43
+ };
44
+ session: string;
45
+ target: string;
46
+ }>;
47
+ export declare function codeReferenceCommands(research: (c: CommandContext) => Research): import("@eyeauras/cli-factory").CommandDefinition[];
48
+ export {};
@@ -0,0 +1,154 @@
1
+ import { command, Permission } from '@eyeauras/cli-factory';
2
+ import { scope, number, address } from './research.js';
3
+ import { fail, object, digest, stable } from './model.js';
4
+ import { parseDomainJson } from './json.js';
5
+ import { selectedOptions } from './arguments.js';
6
+ export const referenceAlgorithm = 'x86-64-code-reference-candidates-v1';
7
+ export const referenceForms = ['call-rel32', 'lea-rip', 'mov-rip', 'call-rip-slot'];
8
+ const defaults = { max_bytes: 4194304, max_candidates: 200000, timeout_seconds: 5, limit: 100 };
9
+ const hex = (v) => { const s = address(v); if (!/^[a-f0-9]{1,16}$/.test(s))
10
+ fail('invalid', 'Code-reference V1 requires default-space unsigned 64-bit addresses.'); return s; };
11
+ const big = (s) => BigInt('0x' + s);
12
+ function query(v) {
13
+ const q = object(v);
14
+ if (!Array.isArray(q.targets) || q.targets.length < 1 || q.targets.length > 64 || !Array.isArray(q.forms) || !q.forms.length || q.forms.length > 4)
15
+ fail('invalid', 'Select 1–64 targets and 1–4 reference forms.');
16
+ const targets = q.targets.map(hex).sort((a, b) => big(a) < big(b) ? -1 : big(a) > big(b) ? 1 : 0), forms = referenceForms.filter(f => q.forms.includes(f));
17
+ if (new Set(targets).size !== targets.length || forms.length !== q.forms.length)
18
+ fail('invalid', 'Targets/forms must be distinct supported values.');
19
+ const start = hex(q.start), end = hex(q.end);
20
+ if (big(start) > big(end))
21
+ fail('invalid', 'scan-through must not precede scan-from.');
22
+ return { targets, forms, start, end };
23
+ }
24
+ function budgets(v, base = defaults) { return { max_bytes: number(v.maxBytes ?? base.max_bytes, 16777216, 15), max_candidates: number(v.maxCandidates ?? base.max_candidates, 1000000, 1), timeout_seconds: number(v.timeoutSeconds ?? base.timeout_seconds, 30, 1), limit: number(v.limit ?? base.limit, 1000, 1) }; }
25
+ export function referenceRequest(o, who) {
26
+ if (o.cursor !== undefined) {
27
+ if (['to', 'forms', 'scanFrom', 'scanThrough'].some(k => o[k] !== undefined))
28
+ fail('invalid', 'Cursor continuation cannot change targets, forms or scan bounds.');
29
+ if (typeof o.cursor !== 'string' || o.cursor.length > 16384 || !/^cr1\.[A-Za-z0-9_-]+$/.test(o.cursor))
30
+ fail('invalid', 'Malformed code-reference cursor.');
31
+ let c;
32
+ try {
33
+ const encoded = o.cursor.slice(4), bytes = Buffer.from(encoded, 'base64url');
34
+ if (bytes.toString('base64url') !== encoded)
35
+ throw Error();
36
+ c = object(parseDomainJson(bytes.toString('utf8')));
37
+ }
38
+ catch {
39
+ fail('invalid', 'Malformed code-reference cursor.');
40
+ }
41
+ if (c.version !== 1 || c.algorithm !== referenceAlgorithm || stable(c.scope) !== stable(who) || typeof c.view !== 'string' || !/^[a-f0-9]{64}$/.test(c.view))
42
+ fail('invalid', 'Cursor version, scope or view is incompatible.');
43
+ const q = query(c.query), from = hex(c.next), b = object(c.budgets);
44
+ if (big(from) < big(q.start) || big(from) > big(q.end))
45
+ fail('invalid', 'Cursor frontier is outside its range.');
46
+ if (['max_bytes', 'max_candidates', 'timeout_seconds', 'limit'].some(k => !Number.isSafeInteger(b[k])))
47
+ fail('invalid', 'Malformed cursor budgets.');
48
+ const inherited = budgets({ maxBytes: b.max_bytes, maxCandidates: b.max_candidates, timeoutSeconds: b.timeout_seconds, limit: b.limit });
49
+ return { query: q, from, budgets: budgets(o, inherited), expectedView: c.view };
50
+ }
51
+ if (typeof o.to !== 'string' || typeof o.scanFrom !== 'string' || typeof o.scanThrough !== 'string')
52
+ fail('invalid', 'Supply --to, --scan-from and --scan-through, or --cursor.');
53
+ const q = query({ targets: o.to.split(',').map(x => x.trim()), forms: o.forms === undefined ? [...referenceForms] : String(o.forms).split(',').map(x => x.trim()), start: o.scanFrom, end: o.scanThrough });
54
+ return { query: q, from: q.start, budgets: budgets(o) };
55
+ }
56
+ /** The deliberately small encoding contract, independently rechecked in the facade. */
57
+ export function referenceEncoding(input) {
58
+ let p = 0, rex = 0, word = false;
59
+ while (p < input.length && p < 15) {
60
+ const b = input[p];
61
+ if ([0x66, 0x26, 0x2e, 0x36, 0x3e].includes(b)) {
62
+ word ||= b === 0x66;
63
+ rex = 0;
64
+ p++;
65
+ }
66
+ else if (b >= 0x40 && b <= 0x4f) {
67
+ rex = b;
68
+ p++;
69
+ }
70
+ else
71
+ break;
72
+ }
73
+ const op = input[p], m = input[p + 1];
74
+ let form, displacement, length, operand, bits;
75
+ if (op === 0xe8) {
76
+ form = 'call-rel32';
77
+ length = p + 5;
78
+ displacement = p + 1;
79
+ operand = 0;
80
+ bits = 64;
81
+ }
82
+ else {
83
+ if (m === undefined || (m & 0xc7) !== 5)
84
+ return;
85
+ if (op === 0x8d) {
86
+ form = 'lea-rip';
87
+ operand = 1;
88
+ }
89
+ else if (op !== undefined && [0x88, 0x89, 0x8a, 0x8b].includes(op)) {
90
+ form = 'mov-rip';
91
+ operand = op === 0x88 || op === 0x89 ? 0 : 1;
92
+ }
93
+ else if (op === 0xff && ((m >> 3) & 7) === 2) {
94
+ form = 'call-rip-slot';
95
+ operand = 0;
96
+ }
97
+ else
98
+ return;
99
+ length = p + 6;
100
+ displacement = p + 2;
101
+ bits = form === 'call-rip-slot' ? 64 : op === 0x88 || op === 0x8a ? 8 : rex & 8 ? 64 : word ? 16 : 32;
102
+ }
103
+ if (length > 15 || input.length < length)
104
+ return;
105
+ return { form, length, displacement, operand, bits, delta: input.readInt32LE(displacement), role: form === 'call-rel32' ? 'direct-call-target' : form === 'call-rip-slot' ? 'pointer-slot' : 'effective-address' };
106
+ }
107
+ export function referenceResult(raw, r) {
108
+ const v = object(raw), s = object(v.scan), q = r.query, b = r.budgets;
109
+ if (v.algorithm !== referenceAlgorithm || typeof v.language !== 'string' || !v.language.startsWith('x86:LE:64:') || stable(v.forms) !== stable(q.forms) || !Array.isArray(v.matches) || v.matches.length > b.limit || object(v.read_only ?? {}).modification_unchanged !== true)
110
+ fail('partial', 'Malformed code-reference result.');
111
+ for (const k of ['bytes_read', 'tested_starts', 'visited_candidates'])
112
+ if (!Number.isSafeInteger(s[k]) || Number(s[k]) < 0)
113
+ fail('partial', 'Malformed reference scan counters.');
114
+ const complete = s.complete;
115
+ if (typeof complete !== 'boolean' || Number(s.bytes_read) > b.max_bytes || Number(s.tested_starts) > Number(s.bytes_read) || Number(s.visited_candidates) > b.max_candidates || Number(s.visited_candidates) < v.matches.length || Number(s.visited_candidates) > Number(s.tested_starts) || !['exhausted', 'byte_limit', 'candidate_limit', 'timeout', 'result_limit'].includes(String(s.reason)) || complete !== (s.reason === 'exhausted') || !Number.isFinite(s.elapsed_ms) || Number(s.elapsed_ms) < 0 || hex(s.from) !== r.from || hex(s.through) !== q.end)
116
+ fail('partial', 'Inconsistent code-reference coverage.');
117
+ const next = complete ? null : hex(s.next_start);
118
+ if (complete && s.next_start !== null || next !== null && (big(next) < big(r.from) || big(next) > big(q.end)) || next !== null && BigInt(Number(s.tested_starts)) > big(next) - big(r.from))
119
+ fail('partial', 'Reference frontier changed.');
120
+ let previous = -1n;
121
+ for (const rawRow of v.matches) {
122
+ const row = object(rawRow), from = hex(row.from), to = hex(row.to), bytes = row.bytes;
123
+ if (typeof bytes !== 'string' || !/^(?:[a-f0-9]{2}){1,15}$/.test(bytes))
124
+ fail('partial', 'Reference omitted its instruction bytes.');
125
+ const encoding = referenceEncoding(Buffer.from(bytes, 'hex'));
126
+ if (!encoding || row.form !== encoding.form || !q.forms.includes(encoding.form) || row.length !== encoding.length || bytes.length !== encoding.length * 2 || row.operand_index !== encoding.operand || row.operand_bits !== encoding.bits || row.target_role !== encoding.role || !q.targets.includes(to) || big(to) !== BigInt.asUintN(64, big(from) + BigInt(encoding.length + encoding.delta)) || big(from) <= previous || big(from) < big(r.from) || big(from) + BigInt(encoding.length - 1) > big(q.end) || next !== null && big(from) >= big(next) || !['listing-confirmed', 'listing-conflict', 'unconfirmed'].includes(String(row.boundary)) || row.validation !== 'decoded' || typeof row.instruction !== 'string' || row.instruction.length > 512)
127
+ fail('partial', 'Reference bytes, target, form or frontier are inconsistent.');
128
+ previous = big(from);
129
+ }
130
+ if (Buffer.byteLength(JSON.stringify(v)) > 1048576)
131
+ fail('limit', 'Code-reference response exceeds one MiB.');
132
+ return v;
133
+ }
134
+ export async function inspectCodeReferences(backend, who, r, view, observation) {
135
+ if (r.expectedView !== undefined && r.expectedView !== view)
136
+ fail('binding', 'Code-reference continuation view changed.', { reason: 'changed_view' });
137
+ const q = r.query, result = await backend.call(who, '/code_references', { targets: q.targets, forms: q.forms, scan_from: r.from, scan_through: q.end, ...r.budgets });
138
+ if (digest(observation) !== digest(await backend.observation(who)))
139
+ fail('binding', 'Native view changed during code-reference scan.', { reason: 'changed_view' });
140
+ const value = referenceResult(result, r), s = object(value.scan);
141
+ const cursor = s.complete ? null : 'cr1.' + Buffer.from(stable({ version: 1, algorithm: referenceAlgorithm, scope: who, query: q, next: hex(s.next_start), view, budgets: r.budgets })).toString('base64url');
142
+ return { ...who, value: { ...value, view: { identity: view }, cursor } };
143
+ }
144
+ export function codeReferenceCommands(research) {
145
+ return [command('analysis', 'Static analysis', [command('code-references', 'Find bounded x86-64 byte reference candidates without changing Listing or xrefs', ({ options: o }, c) => { const who = scope(o); return research(c).codeReferences(who, referenceRequest(o, who)); }, { permission: Permission.ReadOnly, options: [...selectedOptions,
146
+ { flags: '--to <addresses>', description: '1–64 distinct exact default-space targets, comma separated' },
147
+ { flags: '--forms <forms>', description: 'call-rel32,lea-rip,mov-rip,call-rip-slot; defaults to all four' },
148
+ { flags: '--scan-from <address>', description: 'First candidate start (initial request)' }, { flags: '--scan-through <address>', description: 'Inclusive final byte; matches must fit completely' },
149
+ { flags: '--cursor <token>', description: 'Stateless continuation; excludes targets/forms/range selectors' },
150
+ { flags: '--max-bytes <count>', description: '15–16777216 bytes read including lookahead; default 4194304' },
151
+ { flags: '--max-candidates <count>', description: '1–1000000 prefiltered starts including rejected candidates; default 200000' },
152
+ { flags: '--timeout-seconds <count>', description: '1–30 second cooperative native scan deadline; default 5' },
153
+ { flags: '--limit <count>', description: '1–1000 matches per call; default 100' }] })])];
154
+ }
@@ -0,0 +1,25 @@
1
+ import { type CommandContext, type CommandDefinition } from '@eyeauras/cli-factory';
2
+ import { type Research } from './research.js';
3
+ import type { Workspace, Scope } from './workspace.js';
4
+ export type StopReason = 'explicit_close' | 'view_replaced' | 'mode_changed' | 'owner_dispose' | 'startup_failed' | 'temporary_complete' | 'legacy_cleanup' | 'unknown';
5
+ type Event = {
6
+ at: string;
7
+ event: 'start' | 'restore' | 'stop';
8
+ reason: string;
9
+ pid: number;
10
+ };
11
+ export declare function recordLifecycle(workspace: Workspace, context: string, event: Event): Promise<void>;
12
+ export declare function lifecycleDiagnostic(workspace: Workspace, selected: Scope): Promise<{
13
+ lifecycle: {
14
+ ownerShutdownCause: string;
15
+ coverage: string;
16
+ latest?: Record<string, unknown> | undefined;
17
+ events: Event[];
18
+ maximumEvents: number;
19
+ };
20
+ session: string;
21
+ target: string;
22
+ }>;
23
+ export declare function navigationResult(raw: unknown, maximum: number): Record<string, unknown>;
24
+ export declare function contextDiagnosticCommands(research: (c: CommandContext) => Research, workspace: (c: CommandContext) => Workspace): CommandDefinition[];
25
+ export {};
@@ -0,0 +1,82 @@
1
+ import { command, Permission } from '@eyeauras/cli-factory';
2
+ import { mkdir, readFile, writeFile, rename, realpath } from 'node:fs/promises';
3
+ import { join, resolve, sep } from 'node:path';
4
+ import { randomUUID } from 'node:crypto';
5
+ import { scope, number } from './research.js';
6
+ import { object, fail, stable } from './model.js';
7
+ import { selectedOptions } from './arguments.js';
8
+ const writes = new Map();
9
+ const journal = (workspace, context) => join(workspace.directory, 'diagnostics', 'lifecycle', context + '.json');
10
+ const reasons = new Set(['explicit_close', 'view_replaced', 'mode_changed', 'owner_dispose', 'startup_failed', 'temporary_complete', 'legacy_cleanup', 'unknown', 'native_launch', 'saved_snapshot']);
11
+ async function events(path) {
12
+ try {
13
+ const rows = JSON.parse(await readFile(path, 'utf8'));
14
+ if (!Array.isArray(rows) || rows.length > 128 || rows.some(row => !row || typeof row.at !== 'string' || row.at.length > 64 || !Number.isFinite(Date.parse(row.at)) || !['start', 'restore', 'stop'].includes(row.event) || !reasons.has(row.reason) || !Number.isSafeInteger(row.pid) || row.pid < 1))
15
+ fail('binding', 'Retained lifecycle observations are malformed.');
16
+ return rows.map(({ at, event, reason, pid }) => ({ at, event, reason, pid }));
17
+ }
18
+ catch (error) {
19
+ if (error.code === 'ENOENT')
20
+ return [];
21
+ throw error;
22
+ }
23
+ }
24
+ export async function recordLifecycle(workspace, context, event) {
25
+ const path = journal(workspace, context), previous = writes.get(path) ?? Promise.resolve();
26
+ const pending = previous.catch(() => { }).then(async () => { const rows = [...await events(path), event].slice(-128), directory = join(workspace.directory, 'diagnostics', 'lifecycle'); await mkdir(directory, { recursive: true, mode: 0o700 }); const temporary = path + '.new-' + randomUUID(); await writeFile(temporary, JSON.stringify(rows) + '\n', { mode: 0o600, flag: 'wx' }); await rename(temporary, path); });
27
+ writes.set(path, pending);
28
+ try {
29
+ await pending;
30
+ }
31
+ catch { }
32
+ finally {
33
+ if (writes.get(path) === pending)
34
+ writes.delete(path);
35
+ }
36
+ }
37
+ export async function lifecycleDiagnostic(workspace, selected) {
38
+ const context = workspace.context(selected), root = join(workspace.directory, 'ghidra-native', 'context', context.id);
39
+ let latest;
40
+ try {
41
+ const binding = object(JSON.parse(await readFile(join(root, 'current.json'), 'utf8')));
42
+ if (binding.context !== context.id || binding.target !== selected.target || binding.session !== undefined && binding.session !== selected.session || typeof binding.root !== 'string' || !Number.isSafeInteger(binding.pid) || typeof binding.tokenSecret !== 'string')
43
+ fail('binding', 'Lifecycle receipt belongs to another context.');
44
+ const physical = await realpath(binding.root);
45
+ if (physical !== resolve(binding.root) || !physical.startsWith(resolve(root) + sep))
46
+ fail('binding', 'Lifecycle receipt moved outside its context.');
47
+ const persisted = object(JSON.parse(await readFile(join(physical, 'process.json'), 'utf8')));
48
+ if (stable(persisted) !== stable(binding))
49
+ fail('binding', 'Lifecycle process receipt changed.');
50
+ latest = { pid: binding.pid, state: 'unobserved', reason: 'unknown' };
51
+ try {
52
+ const stopped = object(JSON.parse(await readFile(join(physical, 'stopped.json'), 'utf8')));
53
+ if (stopped.pid === binding.pid && stopped.tokenSecret === binding.tokenSecret)
54
+ latest = { pid: binding.pid, state: 'stopped', reason: reasons.has(String(stopped.reason)) ? stopped.reason : 'unknown', ...(typeof stopped.at === 'string' ? { at: stopped.at } : {}) };
55
+ }
56
+ catch (error) {
57
+ if (error.code !== 'ENOENT')
58
+ throw error;
59
+ }
60
+ }
61
+ catch (error) {
62
+ if (error.code !== 'ENOENT')
63
+ throw error;
64
+ }
65
+ return { ...selected, lifecycle: { events: await events(journal(workspace, context.id)), maximumEvents: 128, ...(latest ? { latest } : {}), ownerShutdownCause: 'unknown', coverage: 'Retained facade observations, at most the latest 128 events; absent events and historical causes remain unknown. This read neither starts nor probes a native process.' } };
66
+ }
67
+ export function navigationResult(raw, maximum) {
68
+ const value = object(raw), scan = object(value.scan), functions = object(value.functions);
69
+ const samples = ['functions', 'instructions', 'references', 'definedData'].map(key => object(value[key]));
70
+ if (samples.some(row => !Number.isSafeInteger(row.sampled) || Number(row.sampled) < 0 || Number(row.sampled) > 128 || typeof row.complete !== 'boolean') ||
71
+ !Number.isSafeInteger(scan.visited) || Number(scan.visited) !== samples.reduce((n, row) => n + Number(row.sampled), 0) || Number(scan.visited) > maximum || scan.maximum !== maximum || scan.sampleLimitPerCategory !== 128 || typeof scan.complete !== 'boolean' || scan.complete !== samples.every(row => row.complete) || !['budget', 'sample_limit', 'exhausted'].includes(String(scan.reason)) ||
72
+ !Number.isSafeInteger(functions.oneByteBodies) || Number(functions.oneByteBodies) < 0 || Number(functions.oneByteBodies) > Number(functions.sampled) || !Number.isSafeInteger(functions.entriesWithInstructions) || Number(functions.entriesWithInstructions) < 0 || Number(functions.entriesWithInstructions) > Number(functions.sampled) || !Number.isSafeInteger(functions.bodyBytes) || Number(functions.bodyBytes) < 0 ||
73
+ !Number.isSafeInteger(samples[3].strings) || Number(samples[3].strings) < 0 || Number(samples[3].strings) > Number(samples[3].sampled) || !['sparse-metadata', 'no-code-observed', 'observed-metadata'].includes(String(value.assessment)) || !Array.isArray(value.recommendations) || value.recommendations.some(v => typeof v !== 'string') || object(value.read_only ?? {}).modification_unchanged !== true)
74
+ fail('partial', 'Malformed navigation diagnostic.');
75
+ return value;
76
+ }
77
+ export function contextDiagnosticCommands(research, workspace) {
78
+ return [
79
+ command('analysis', 'Analysis observations', [command('navigation', 'Inspect bounded navigation metadata; ready is not analysis completeness', ({ options: o }, c) => research(c).navigation(scope(o), number(o.maxScan ?? 10000, 200000, 1)), { permission: Permission.ReadOnly, options: [...selectedOptions, { flags: '--max-scan <count>', description: 'Maximum visited metadata inputs across all samples, 1–200000', defaultValue: 10000 }] })]),
80
+ command('diagnostics', 'Inspect retained context diagnostics', [command('lifecycle', 'Read retained start/restore/stop reasons without starting Ghidra', ({ options: o }, c) => lifecycleDiagnostic(workspace(c), scope(o)), { permission: Permission.ReadOnly, options: selectedOptions })]),
81
+ ];
82
+ }