@eyeauras/insight-cli 0.3.1 → 0.4.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.
Files changed (152) hide show
  1. package/.cli-package-build.json +1 -1
  2. package/SKILL.md +4 -1
  3. package/dist/src/analysis-default.d.ts +26 -0
  4. package/dist/src/analysis-default.js +149 -0
  5. package/dist/src/analysis-storage.d.ts +39 -20
  6. package/dist/src/analysis-storage.js +112 -16
  7. package/dist/src/arguments.js +9 -4
  8. package/dist/src/cli.js +45 -37
  9. package/dist/src/code-references.d.ts +48 -0
  10. package/dist/src/code-references.js +154 -0
  11. package/dist/src/context-diagnostics.d.ts +25 -0
  12. package/dist/src/context-diagnostics.js +82 -0
  13. package/dist/src/contexts.d.ts +13 -1
  14. package/dist/src/contexts.js +38 -9
  15. package/dist/src/function-boundaries.d.ts +9 -0
  16. package/dist/src/function-boundaries.js +68 -0
  17. package/dist/src/function-entries.d.ts +33 -0
  18. package/dist/src/function-entries.js +84 -0
  19. package/dist/src/function-inspection.d.ts +4 -2
  20. package/dist/src/function-inspection.js +94 -59
  21. package/dist/src/function-results.d.ts +3 -1
  22. package/dist/src/function-results.js +9 -3
  23. package/dist/src/inline-script.d.ts +15 -0
  24. package/dist/src/inline-script.js +62 -0
  25. package/dist/src/internal-timing.d.ts +3 -0
  26. package/dist/src/internal-timing.js +52 -0
  27. package/dist/src/lifecycle.js +14 -4
  28. package/dist/src/native-inputs.d.ts +1 -1
  29. package/dist/src/native-inputs.js +1 -1
  30. package/dist/src/native-queries.js +1 -1
  31. package/dist/src/persistent-client.d.ts +30 -0
  32. package/dist/src/persistent-client.js +125 -0
  33. package/dist/src/queries.js +4 -1
  34. package/dist/src/query-policy.js +9 -1
  35. package/dist/src/research-view.d.ts +6 -0
  36. package/dist/src/research-view.js +9 -0
  37. package/dist/src/research.d.ts +38 -0
  38. package/dist/src/research.js +78 -8
  39. package/dist/src/string-anchors.d.ts +43 -0
  40. package/dist/src/string-anchors.js +176 -0
  41. package/dist/src/sync-policy.js +3 -0
  42. package/dist/src/timed-backend.d.ts +3 -0
  43. package/dist/src/timed-backend.js +16 -0
  44. package/dist/src/transport.d.ts +1 -0
  45. package/dist/src/transport.js +4 -0
  46. package/dist/src/workspace.js +5 -2
  47. package/package.json +12 -1
  48. package/references/native/InsightAnchorScan.java +135 -0
  49. package/references/native/InsightCodeReferences.java +129 -0
  50. package/references/native/InsightFunctionBoundaries.java +56 -0
  51. package/references/native/InsightInlineExecutor.java +143 -0
  52. package/references/native/InsightInlineScript.java +14 -0
  53. package/references/native/InsightPeRanges.java +92 -0
  54. package/references/native/InsightResearchQueries.java +22 -1
  55. package/references/native/InsightResearchView.java +69 -0
  56. package/references/native/InsightStringAnchors.java +49 -0
  57. package/references/persistent-client.mjs +125 -0
  58. package/references/persistent.md +50 -0
  59. package/references/query-semantics.md +12 -0
  60. package/references/recovery.md +36 -0
  61. package/references/research.md +87 -0
  62. package/references/setup.md +1 -1
  63. package/references/sparse-navigation.md +173 -0
  64. package/references/storage.md +35 -7
  65. package/runtime/GhidraMCP-7.0.0.jar +0 -0
  66. package/runtime/GhidraMCP-NOTICE +5 -0
  67. package/runtime/provenance.json +17 -3
  68. package/runtime/research-view.provenance.json +28 -0
  69. package/runtime/toolbox.json +10 -3
  70. package/skill-bundle/insight-artifacts/SKILL.md +1 -1
  71. package/skill-bundle/insight-artifacts/references/recovery.md +36 -0
  72. package/skill-bundle/insight-artifacts/references/storage.md +35 -7
  73. package/skill-bundle/insight-binary/SKILL.md +1 -1
  74. package/skill-bundle/insight-capture/SKILL.md +1 -1
  75. package/skill-bundle/insight-cli/SKILL.md +4 -1
  76. package/skill-bundle/insight-cli/references/persistent-client.mjs +125 -0
  77. package/skill-bundle/insight-cli/references/persistent.md +50 -0
  78. package/skill-bundle/insight-cli/references/recovery.md +36 -0
  79. package/skill-bundle/insight-cli/references/setup.md +1 -1
  80. package/skill-bundle/insight-cli/references/storage.md +35 -7
  81. package/skill-bundle/insight-die/SKILL.md +1 -1
  82. package/skill-bundle/insight-dnspy/SKILL.md +1 -1
  83. package/skill-bundle/insight-dotnet-dump/SKILL.md +1 -1
  84. package/skill-bundle/insight-dotnet-stack/SKILL.md +1 -1
  85. package/skill-bundle/insight-dotnet-trace/SKILL.md +1 -1
  86. package/skill-bundle/insight-frida/SKILL.md +1 -1
  87. package/skill-bundle/insight-ghidra/SKILL.md +14 -1
  88. package/skill-bundle/insight-ghidra/references/persistent-client.mjs +125 -0
  89. package/skill-bundle/insight-ghidra/references/persistent.md +50 -0
  90. package/skill-bundle/insight-ghidra/references/query-semantics.md +12 -0
  91. package/skill-bundle/insight-ghidra/references/recovery.md +36 -0
  92. package/skill-bundle/insight-ghidra/references/research.md +87 -0
  93. package/skill-bundle/insight-ghidra/references/setup.md +1 -1
  94. package/skill-bundle/insight-ghidra/references/sparse-navigation.md +173 -0
  95. package/skill-bundle/insight-ghidra/references/storage.md +35 -7
  96. package/skill-bundle/insight-ilspy/SKILL.md +1 -1
  97. package/skill-bundle/insight-interop/SKILL.md +1 -1
  98. package/skill-bundle/insight-lldb/SKILL.md +1 -1
  99. package/skill-bundle/insight-llvm/SKILL.md +1 -1
  100. package/skill-bundle/insight-managed-heap/SKILL.md +1 -1
  101. package/skill-bundle/insight-managed-metadata/SKILL.md +1 -1
  102. package/skill-bundle/insight-mitmproxy/SKILL.md +1 -1
  103. package/skill-bundle/insight-overview/SKILL.md +1 -1
  104. package/skill-bundle/insight-procmon/SKILL.md +1 -1
  105. package/skill-bundle/insight-re/SKILL.md +4 -1
  106. package/skill-bundle/insight-re/references/persistent-client.mjs +125 -0
  107. package/skill-bundle/insight-re/references/persistent.md +50 -0
  108. package/skill-bundle/insight-re/references/recovery.md +36 -0
  109. package/skill-bundle/insight-re/references/storage.md +35 -7
  110. package/skill-bundle/insight-sevenzip/SKILL.md +1 -1
  111. package/skill-bundle/insight-strace/SKILL.md +1 -1
  112. package/skill-bundle/insight-tshark/SKILL.md +1 -1
  113. package/skill-bundle/insight-windbg/SKILL.md +1 -1
  114. package/skill-bundle/manifest.json +1 -1
  115. package/skills/insight-artifacts/SKILL.md +1 -1
  116. package/skills/insight-artifacts/references/recovery.md +36 -0
  117. package/skills/insight-artifacts/references/storage.md +35 -7
  118. package/skills/insight-binary/SKILL.md +1 -1
  119. package/skills/insight-capture/SKILL.md +1 -1
  120. package/skills/insight-die/SKILL.md +1 -1
  121. package/skills/insight-dnspy/SKILL.md +1 -1
  122. package/skills/insight-dotnet-dump/SKILL.md +1 -1
  123. package/skills/insight-dotnet-stack/SKILL.md +1 -1
  124. package/skills/insight-dotnet-trace/SKILL.md +1 -1
  125. package/skills/insight-frida/SKILL.md +1 -1
  126. package/skills/insight-ghidra/SKILL.md +14 -1
  127. package/skills/insight-ghidra/references/persistent-client.mjs +125 -0
  128. package/skills/insight-ghidra/references/persistent.md +50 -0
  129. package/skills/insight-ghidra/references/query-semantics.md +12 -0
  130. package/skills/insight-ghidra/references/recovery.md +36 -0
  131. package/skills/insight-ghidra/references/research.md +87 -0
  132. package/skills/insight-ghidra/references/setup.md +1 -1
  133. package/skills/insight-ghidra/references/sparse-navigation.md +173 -0
  134. package/skills/insight-ghidra/references/storage.md +35 -7
  135. package/skills/insight-ilspy/SKILL.md +1 -1
  136. package/skills/insight-interop/SKILL.md +1 -1
  137. package/skills/insight-lldb/SKILL.md +1 -1
  138. package/skills/insight-llvm/SKILL.md +1 -1
  139. package/skills/insight-managed-heap/SKILL.md +1 -1
  140. package/skills/insight-managed-metadata/SKILL.md +1 -1
  141. package/skills/insight-mitmproxy/SKILL.md +1 -1
  142. package/skills/insight-overview/SKILL.md +1 -1
  143. package/skills/insight-procmon/SKILL.md +1 -1
  144. package/skills/insight-re/SKILL.md +4 -1
  145. package/skills/insight-re/references/persistent-client.mjs +125 -0
  146. package/skills/insight-re/references/persistent.md +50 -0
  147. package/skills/insight-re/references/recovery.md +36 -0
  148. package/skills/insight-re/references/storage.md +35 -7
  149. package/skills/insight-sevenzip/SKILL.md +1 -1
  150. package/skills/insight-strace/SKILL.md +1 -1
  151. package/skills/insight-tshark/SKILL.md +1 -1
  152. package/skills/insight-windbg/SKILL.md +1 -1
@@ -1 +1 @@
1
- {"version":1,"artifact":"197a094cb768427c71cf9790f11ce42ba4863f3892cc24e05c58e091347ffc3b"}
1
+ {"version":1,"artifact":"034bd833a87d3a9c730b486252e2db09f67b3c6da2377c195389b894b26e12f7"}
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.1
6
+ # insight-cli 0.4.0
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.
@@ -0,0 +1,26 @@
1
+ import { type CommandContext } from '@eyeauras/cli-factory';
2
+ type Context = Pick<CommandContext, 'profile' | 'appArguments'>;
3
+ export declare const analysisTrees: string[];
4
+ export interface StorageDefault {
5
+ schema: 1;
6
+ base: string;
7
+ application: string;
8
+ binding: string;
9
+ id: string;
10
+ baseInode: string;
11
+ revision: string;
12
+ }
13
+ /** Storage control metadata, independent of Factory's profile/settings document. */
14
+ export declare function storageDefault(c: Context): StorageDefault | undefined;
15
+ export declare function validateStorageDefault(c: Context, value: StorageDefault): void;
16
+ /** Preserve pre-existing local analysis until it is explicitly migrated. */
17
+ export declare function legacyAnalysis(c: Context): boolean;
18
+ export declare function inheritedStorage(c: Context): StorageDefault | undefined;
19
+ export declare function checkStorageDefaultBase(c: Context, base: string): Promise<void>;
20
+ export declare function writeStorageDefault(c: Context, base: string, revision?: string, expected?: string | null): Promise<{
21
+ state: 'configured' | 'superseded';
22
+ base: string | null;
23
+ revision: string | null;
24
+ binding?: StorageDefault;
25
+ }>;
26
+ export {};
@@ -0,0 +1,149 @@
1
+ import { CliError } from '@eyeauras/cli-factory';
2
+ import { lstatSync, readFileSync, realpathSync } from 'node:fs';
3
+ import { link, lstat, open, realpath, unlink } from 'node:fs/promises';
4
+ import { isAbsolute, join, resolve } from 'node:path';
5
+ import { randomUUID } from 'node:crypto';
6
+ import { digest } from './model.js';
7
+ import { atomicWrite, withToolLock } from './tool-runtime/store.js';
8
+ export const analysisTrees = ['workspace', 'materials', 'temp', 'runtime'];
9
+ const uuid = (value) => typeof value === 'string' && /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/.test(value);
10
+ const absent = (error) => error.code === 'ENOENT';
11
+ function fail(code, message) { throw new CliError(message, { code: 'storage.' + code }); }
12
+ const policyPath = (c) => join(c.appArguments.RoamingAppDataDirectory, '.analysis-storage-default.json');
13
+ const markerPath = (base) => join(base, '.insight-storage-root.json');
14
+ const binding = (c) => digest(realpathSync.native(c.appArguments.RoamingAppDataDirectory));
15
+ function readOwnedJson(path) {
16
+ if (!lstatSync(path).isFile())
17
+ fail('configuration', 'Storage default metadata must be an ordinary file.');
18
+ return JSON.parse(readFileSync(path, 'utf8'));
19
+ }
20
+ function valid(c, value) {
21
+ if (!value || typeof value !== 'object' || Array.isArray(value))
22
+ return false;
23
+ const v = value;
24
+ return v.schema === 1 && v.application === c.appArguments.AppName && v.binding === binding(c) && uuid(v.id) &&
25
+ typeof v.base === 'string' && isAbsolute(v.base) && v.base === resolve(v.base) && !/[\0\r\n]/.test(v.base) &&
26
+ typeof v.baseInode === 'string' && /^\d+$/.test(v.baseInode);
27
+ }
28
+ /** Storage control metadata, independent of Factory's profile/settings document. */
29
+ export function storageDefault(c) {
30
+ let value;
31
+ try {
32
+ value = readOwnedJson(policyPath(c));
33
+ }
34
+ catch (error) {
35
+ if (absent(error))
36
+ return;
37
+ fail('configuration', 'Installation storage default is unreadable; preserve its control metadata.');
38
+ }
39
+ if (!valid(c, value) || !uuid(value.revision))
40
+ fail('configuration', 'Installation storage default failed its application binding.');
41
+ return value;
42
+ }
43
+ export function validateStorageDefault(c, value) {
44
+ try {
45
+ if (!valid(c, value) || !uuid(value.revision))
46
+ throw Error('binding');
47
+ const st = lstatSync(value.base, { bigint: true }), marker = readOwnedJson(markerPath(value.base));
48
+ if (!st.isDirectory() || realpathSync.native(value.base) !== value.base || String(st.ino) !== value.baseInode ||
49
+ !valid(c, marker) || marker.id !== value.id || marker.base !== value.base || marker.baseInode !== value.baseInode)
50
+ throw Error('binding');
51
+ }
52
+ catch {
53
+ fail('unavailable', 'Installation analysis storage is missing or changed; mount its original volume. No fallback was created.');
54
+ }
55
+ }
56
+ /** Preserve pre-existing local analysis until it is explicitly migrated. */
57
+ export function legacyAnalysis(c) {
58
+ return analysisTrees.some(tree => { try {
59
+ return !lstatSync(join(c.appArguments.AppDataDirectory, tree)).isSymbolicLink();
60
+ }
61
+ catch (error) {
62
+ if (absent(error))
63
+ return false;
64
+ throw error;
65
+ } });
66
+ }
67
+ export function inheritedStorage(c) {
68
+ if (c.profile.values.analysisDirectory !== undefined || legacyAnalysis(c))
69
+ return;
70
+ return storageDefault(c);
71
+ }
72
+ export async function checkStorageDefaultBase(c, base) {
73
+ let marker;
74
+ try {
75
+ marker = readOwnedJson(markerPath(base));
76
+ }
77
+ catch (error) {
78
+ if (absent(error))
79
+ return;
80
+ fail('binding', 'Storage base ownership metadata is unreadable; preserve it.');
81
+ }
82
+ if (!valid(c, marker) || marker.base !== base)
83
+ fail('binding', 'Storage base belongs to another application/control root; no data was adopted.');
84
+ const st = await lstat(base, { bigint: true });
85
+ if (!st.isDirectory() || await realpath(base) !== base || String(st.ino) !== marker.baseInode)
86
+ fail('binding', 'Storage base directory was replaced.');
87
+ }
88
+ export async function writeStorageDefault(c, base, revision = randomUUID(), expected) {
89
+ return withToolLock(c.appArguments.RoamingAppDataDirectory, 'analysis-storage-default', async () => {
90
+ const current = storageDefault(c);
91
+ if (current?.revision === revision) {
92
+ if (current.base !== base)
93
+ fail('binding', 'Storage default intent changed.');
94
+ validateStorageDefault(c, current);
95
+ return { state: 'configured', base, revision, binding: current };
96
+ }
97
+ // A resumed migration must never overwrite a later user's default selection.
98
+ if (expected !== undefined && (current?.revision ?? null) !== expected)
99
+ return { state: 'superseded', base: current?.base ?? null, revision: current?.revision ?? null };
100
+ const st = await lstat(base, { bigint: true });
101
+ if (!st.isDirectory() || await realpath(base) !== base)
102
+ fail('path', 'Choose an existing ordinary storage base directory.');
103
+ await checkStorageDefaultBase(c, base);
104
+ let marker;
105
+ try {
106
+ marker = readOwnedJson(markerPath(base));
107
+ }
108
+ catch (error) {
109
+ if (!absent(error))
110
+ throw error;
111
+ marker = { schema: 1, base, application: c.appArguments.AppName, binding: binding(c), id: randomUUID(), baseInode: String(st.ino) };
112
+ const temporary = markerPath(base) + '.new-' + randomUUID(), file = await open(temporary, 'wx', 0o600);
113
+ try {
114
+ try {
115
+ await file.writeFile(JSON.stringify(marker) + '\n');
116
+ await file.sync();
117
+ }
118
+ finally {
119
+ await file.close();
120
+ }
121
+ try {
122
+ await link(temporary, markerPath(base));
123
+ }
124
+ catch (e) {
125
+ if (e.code !== 'EEXIST')
126
+ throw e;
127
+ }
128
+ }
129
+ finally {
130
+ await unlink(temporary);
131
+ }
132
+ await checkStorageDefaultBase(c, base);
133
+ marker = readOwnedJson(markerPath(base));
134
+ }
135
+ const next = { ...marker, revision };
136
+ validateStorageDefault(c, next);
137
+ if (process.platform !== 'win32') {
138
+ const dir = await open(base, 'r');
139
+ try {
140
+ await dir.sync();
141
+ }
142
+ finally {
143
+ await dir.close();
144
+ }
145
+ }
146
+ await atomicWrite(policyPath(c), next);
147
+ return { state: 'configured', base, revision, binding: next };
148
+ }, c.signal);
149
+ }
@@ -1,5 +1,6 @@
1
1
  import { type CommandContext, type CommandDefinition, type ProfileStoreContract } from '@eyeauras/cli-factory';
2
- export declare const analysisTrees: string[];
2
+ import { type StorageDefault } from './analysis-default.js';
3
+ export { analysisTrees } from './analysis-default.js';
3
4
  export declare function analysisDirectory(c: Pick<CommandContext, 'profile' | 'appArguments'>): string;
4
5
  /** Factory remains the only owner of profiles, settings, keyring and IPC admission. */
5
6
  export declare class AnalysisStorage {
@@ -16,6 +17,16 @@ export declare class AnalysisStorage {
16
17
  private save;
17
18
  private validate;
18
19
  wrap(entry: CommandDefinition): CommandDefinition;
20
+ private prepare;
21
+ private defaultBase;
22
+ setDefault(c: CommandContext, directory: unknown): Promise<{
23
+ defaultDirectory: string;
24
+ appliesTo: string;
25
+ state: "configured" | "superseded";
26
+ base: string | null;
27
+ revision: string | null;
28
+ binding?: StorageDefault;
29
+ }>;
19
30
  busy(directory: string): boolean;
20
31
  private exclusive;
21
32
  private preflight;
@@ -23,6 +34,7 @@ export declare class AnalysisStorage {
23
34
  profile: string;
24
35
  directory: string;
25
36
  configuration: string;
37
+ defaultBaseDirectory: string | null;
26
38
  defaultDirectory: string;
27
39
  availability: string;
28
40
  usage: unknown;
@@ -43,10 +55,20 @@ export declare class AnalysisStorage {
43
55
  progress: unknown;
44
56
  } | null;
45
57
  }>;
46
- migrate(c: CommandContext, directory: unknown, dryRun?: boolean, emptyOnly?: boolean): Promise<{
58
+ migrate(c: CommandContext, directory: unknown, dryRun?: boolean, emptyOnly?: boolean, profileOnly?: boolean, automatic?: StorageDefault): Promise<{
59
+ state: "rolled_back";
60
+ id: string;
61
+ directory: string;
62
+ destinationRetained: string;
63
+ } | {
64
+ state: "cleaned";
65
+ id: string;
66
+ compatibilityPathsRetained: boolean;
67
+ } | {
47
68
  profile: string;
48
69
  directory: string;
49
70
  configuration: string;
71
+ defaultBaseDirectory: string | null;
50
72
  defaultDirectory: string;
51
73
  availability: string;
52
74
  usage: unknown;
@@ -66,21 +88,14 @@ export declare class AnalysisStorage {
66
88
  retainedSourceBytes: number;
67
89
  progress: unknown;
68
90
  } | null;
69
- } | {
70
- state: "rolled_back";
71
- id: string;
72
- directory: string;
73
- destinationRetained: string;
74
- } | {
75
- state: "cleaned";
76
- id: string;
77
- compatibilityPathsRetained: boolean;
78
91
  } | {
79
92
  state: "committed" | "rolled_back" | "cleaned";
80
93
  id: string;
81
94
  directory: string;
82
95
  manifest: string;
83
96
  sourceRetained: boolean;
97
+ defaultBaseDirectory: string | null;
98
+ installationDefault: string;
84
99
  rollback: string;
85
100
  cleanup: string;
86
101
  } | {
@@ -96,13 +111,24 @@ export declare class AnalysisStorage {
96
111
  sourceRetained: boolean;
97
112
  compatibilityPathsRetained: boolean;
98
113
  state: string;
114
+ setsInstallationDefault: boolean;
99
115
  }>;
100
116
  private checkCopy;
101
117
  private continue;
102
118
  resume(c: CommandContext): Promise<{
119
+ state: "rolled_back";
120
+ id: string;
121
+ directory: string;
122
+ destinationRetained: string;
123
+ } | {
124
+ state: "cleaned";
125
+ id: string;
126
+ compatibilityPathsRetained: boolean;
127
+ } | {
103
128
  profile: string;
104
129
  directory: string;
105
130
  configuration: string;
131
+ defaultBaseDirectory: string | null;
106
132
  defaultDirectory: string;
107
133
  availability: string;
108
134
  usage: unknown;
@@ -122,21 +148,14 @@ export declare class AnalysisStorage {
122
148
  retainedSourceBytes: number;
123
149
  progress: unknown;
124
150
  } | null;
125
- } | {
126
- state: "rolled_back";
127
- id: string;
128
- directory: string;
129
- destinationRetained: string;
130
- } | {
131
- state: "cleaned";
132
- id: string;
133
- compatibilityPathsRetained: boolean;
134
151
  } | {
135
152
  state: "committed" | "rolled_back" | "cleaned";
136
153
  id: string;
137
154
  directory: string;
138
155
  manifest: string;
139
156
  sourceRetained: boolean;
157
+ defaultBaseDirectory: string | null;
158
+ installationDefault: string;
140
159
  rollback: string;
141
160
  cleanup: string;
142
161
  }>;
@@ -7,10 +7,11 @@ import { atomicWrite, readJson, toolProfileBusy } from './tool-runtime/store.js'
7
7
  import { listProcesses } from './tool-runtime/supervisor.js';
8
8
  import { runtimeProcesses } from './runtime-manager.js';
9
9
  import { digest } from './model.js';
10
+ import { analysisTrees, inheritedStorage, legacyAnalysis, storageDefault, validateStorageDefault, checkStorageDefaultBase, writeStorageDefault } from './analysis-default.js';
11
+ export { analysisTrees } from './analysis-default.js';
10
12
  // Move the complete runtime tree too: installation stages must share a filesystem with
11
13
  // installations, and both native checks and process runs can contain large generated DBs.
12
14
  // Stable aliases preserve references in immutable native journals, selections and results.
13
- export const analysisTrees = ['workspace', 'materials', 'temp', 'runtime'];
14
15
  const terminal = (state) => ['committed', 'rolled_back', 'cleaned'].includes(state);
15
16
  const inside = (parent, child) => { const r = relative(parent, child); return r === '' || !isAbsolute(r) && r !== '..' && !r.startsWith('..' + sep); };
16
17
  function fail(code, message) { throw new CliError(message, { code: 'storage.' + code }); }
@@ -37,8 +38,12 @@ async function sync(path, directory = false) {
37
38
  }
38
39
  export function analysisDirectory(c) {
39
40
  const value = c.profile.values.analysisDirectory;
40
- if (value === undefined || value === null)
41
+ if (value === null)
41
42
  return c.appArguments.AppDataDirectory;
43
+ if (value === undefined) {
44
+ const shared = storageDefault(c);
45
+ return shared && !legacyAnalysis(c) ? join(shared.base, c.appArguments.AppName, c.profile.name) : c.appArguments.AppDataDirectory;
46
+ }
42
47
  if (typeof value !== 'string' || !isAbsolute(value) || value !== resolve(value))
43
48
  fail('configuration', 'Invalid analysis directory in the Factory profile.');
44
49
  return value;
@@ -140,6 +145,9 @@ export class AnalysisStorage {
140
145
  !isAbsolute(j.source) || !isAbsolute(j.destination) || inside(j.source, j.destination) || inside(j.destination, j.source) ||
141
146
  !isAbsolute(j.destinationAnchor) || !inside(j.destinationAnchor, j.destination) || typeof j.sourceIdentity !== 'string' || typeof j.destinationIdentity !== 'string' ||
142
147
  !['copying', 'verified', 'activating', 'committed', 'rolling_back', 'rolled_back', 'cleaning', 'cleaned'].includes(j.state) ||
148
+ ['automatic', 'makeDefault', 'defaultApplied', 'defaultSuperseded'].some(k => { const v = j[k]; return v !== undefined && typeof v !== 'boolean'; }) ||
149
+ j.defaultPreviousRevision !== undefined && j.defaultPreviousRevision !== null && !/^[-a-f0-9]{36}$/.test(j.defaultPreviousRevision) ||
150
+ j.automatic && (!j.defaultBinding || j.makeDefault || j.files !== 0 || j.bytes !== 0 || loadEntries && j.entries.length !== 0) ||
143
151
  !Array.isArray(j.trees) || j.trees.some(t => !analysisTrees.includes(t)) || !Array.isArray(j.entries) ||
144
152
  j.entries.some(e => !j.trees.some(t => e.path === t || e.path.startsWith(t + '/')) || e.path.split('/').some(p => p === '..' || p === '.' || p === '')) ||
145
153
  !/^[a-f0-9]{64}$/.test(j.manifest) || loadEntries && j.manifest !== digest(content(j.entries)))
@@ -153,7 +161,7 @@ export class AnalysisStorage {
153
161
  const { entries, ...record } = j;
154
162
  await atomicWrite(join(this.control(c), 'migration.json'), record);
155
163
  }
156
- async validate(c) {
164
+ async validate(c, initializing = false) {
157
165
  const root = analysisDirectory(c), j = await this.journal(c);
158
166
  if (j && !terminal(j.state))
159
167
  fail('pending', 'Migration is incomplete. Use storage show, resume or rollback before research.');
@@ -161,6 +169,12 @@ export class AnalysisStorage {
161
169
  fail('configuration', 'Factory profile and committed migration differ; preserve both and inspect storage show.');
162
170
  if (root === c.appArguments.AppDataDirectory)
163
171
  return;
172
+ const shared = inheritedStorage(c);
173
+ if (shared && !j) {
174
+ if (!initializing)
175
+ validateStorageDefault(c, shared);
176
+ return;
177
+ }
164
178
  const marker = await readJson(join(root, '.insight-analysis.json'));
165
179
  if (!marker || marker.schema !== 1 || marker.binding !== await this.binding(c) || await realpath(root) !== root)
166
180
  fail('unavailable', 'Configured analysis storage is missing or changed; mount its volume. No fallback was created.');
@@ -175,12 +189,15 @@ export class AnalysisStorage {
175
189
  const run = entry.run;
176
190
  return { ...entry, ...(entry.children ? { children: entry.children.map(e => this.wrap(e)) } : {}), ...(run ? { run: async (input, c) => {
177
191
  const key = c.appArguments.AppDataDirectory;
192
+ if (this.changing.has(key))
193
+ fail('busy', 'Storage migration is in progress.');
194
+ const prepared = await this.prepare(c);
178
195
  if (this.changing.has(key))
179
196
  fail('busy', 'Storage migration is in progress.');
180
197
  this.active.set(key, (this.active.get(key) ?? 0) + 1);
181
198
  try {
182
- await this.validate(c);
183
- return await run(input, c);
199
+ await this.validate(prepared);
200
+ return await run(input, prepared);
184
201
  }
185
202
  finally {
186
203
  const n = this.active.get(key) - 1;
@@ -191,6 +208,42 @@ export class AnalysisStorage {
191
208
  }
192
209
  } } : {}) };
193
210
  }
211
+ async prepare(c) {
212
+ const j = await this.journal(c), shared = inheritedStorage(c);
213
+ if (j?.automatic && !['cleaned', 'rolled_back'].includes(j.state)) {
214
+ await this.resume(c);
215
+ c = { ...c, profile: await this.store(c).get(c.profile.name) };
216
+ }
217
+ else if (!j && shared) {
218
+ validateStorageDefault(c, shared);
219
+ await this.migrate(c, shared.base, false, true, true, shared);
220
+ c = { ...c, profile: await this.store(c).get(c.profile.name) };
221
+ }
222
+ // One command keeps its selected root even if another profile changes the default.
223
+ return { ...c, profile: { ...c.profile, values: { ...c.profile.values, analysisDirectory: analysisDirectory(c) } } };
224
+ }
225
+ async defaultBase(c, directory, existingOnly = false) {
226
+ if (typeof directory !== 'string' || !isAbsolute(directory) || /[\0\r\n]/.test(directory))
227
+ fail('path', 'Supply an absolute destination base directory.');
228
+ const base = await canonicalFuture(resolve(directory)), app = await canonicalFuture(c.appArguments.RoamingAppDataDirectory);
229
+ if (inside(app, base) || inside(base, app))
230
+ fail('path', 'The installation storage base and Factory configuration root must be separate, non-nested directories.');
231
+ for (const profile of (await this.store(c).list()).profiles) {
232
+ if (typeof profile.values.analysisDirectory !== 'string')
233
+ continue;
234
+ const root = await canonicalFuture(profile.values.analysisDirectory);
235
+ if (inside(root, base))
236
+ fail('path', 'The installation storage base cannot be nested inside a profile analysis directory.');
237
+ }
238
+ if (existingOnly && !await present(base))
239
+ fail('path', 'Choose an existing mounted storage base directory.');
240
+ await checkStorageDefaultBase(c, base);
241
+ return base;
242
+ }
243
+ async setDefault(c, directory) {
244
+ const base = await this.defaultBase(c, directory, true), result = await writeStorageDefault(c, base);
245
+ return { ...result, defaultDirectory: join(base, c.appArguments.AppName, c.profile.name), appliesTo: 'new and empty profiles; initialized profile locations are preserved' };
246
+ }
194
247
  busy(directory) { return this.changing.has(directory); }
195
248
  async exclusive(c, action) {
196
249
  const key = c.appArguments.AppDataDirectory;
@@ -212,7 +265,7 @@ export class AnalysisStorage {
212
265
  async preflight(c, directory) {
213
266
  if (typeof directory !== 'string' || !isAbsolute(directory) || /[\0\r\n]/.test(directory))
214
267
  fail('path', 'Supply an absolute destination base directory.');
215
- const source = await canonicalFuture(analysisDirectory(c)), destination = await canonicalFuture(join(resolve(directory), c.appArguments.AppName, c.profile.name));
268
+ const source = await canonicalFuture(inheritedStorage(c) ? c.appArguments.AppDataDirectory : analysisDirectory(c)), destination = await canonicalFuture(join(resolve(directory), c.appArguments.AppName, c.profile.name));
216
269
  const app = await canonicalFuture(c.appArguments.RoamingAppDataDirectory);
217
270
  if (inside(source, destination) || inside(destination, source) || inside(app, destination) || inside(destination, app))
218
271
  fail('path', 'Source, destination and profile configuration roots must be separate, non-nested directories.');
@@ -235,7 +288,7 @@ export class AnalysisStorage {
235
288
  return { source, destination, destinationAnchor: parent, destinationIdentity: await directoryIdentity(parent), bytes, entries: entries.length, availableBytes: available.toString(), requiredBytes: (BigInt(bytes) + reserve).toString(), trees: analysisTrees, sourceRetained: true, compatibilityPathsRetained: true };
236
289
  }
237
290
  async show(c) {
238
- const j = await this.journal(c), root = analysisDirectory(c);
291
+ const j = await this.journal(c), root = analysisDirectory(c), shared = storageDefault(c);
239
292
  let usage, availability = 'available';
240
293
  try {
241
294
  await this.validate(c);
@@ -245,23 +298,34 @@ export class AnalysisStorage {
245
298
  catch (error) {
246
299
  availability = error instanceof CliError ? error.code : (error.code ?? 'unavailable');
247
300
  }
248
- return { profile: c.profile.name, directory: root, configuration: 'Factory profile analysisDirectory', defaultDirectory: c.appArguments.AppDataDirectory, availability, usage,
301
+ return { profile: c.profile.name, directory: root, configuration: c.profile.values.analysisDirectory !== undefined ? 'Factory profile analysisDirectory' : shared && root !== c.appArguments.AppDataDirectory ? 'Installation storage default' : 'Factory profile AppData', defaultBaseDirectory: shared?.base ?? null, defaultDirectory: shared ? join(shared.base, c.appArguments.AppName, c.profile.name) : c.appArguments.AppDataDirectory, availability, usage,
249
302
  trees: analysisTrees, appData: c.appArguments.AppDataDirectory, included: ['snapshots, immutable inputs, native projects and local server repositories', 'tool execution/verification working data', 'runtime installations, acquisition caches, selections and process receipts'], excluded: ['profiles, permissions, IPC, logs and OS keyring secrets'],
250
303
  migration: j ? { id: j.id, state: j.state, source: j.source, destination: j.destination, manifest: j.manifest, bytes: j.bytes, files: j.files, retainedSource: join(j.source, '.storage-retained', j.id), retainedSourceBytes: j.state === 'committed' ? j.bytes : 0, progress: await readJson(join(this.control(c), 'progress.json')) } : null };
251
304
  }
252
- async migrate(c, directory, dryRun = false, emptyOnly = false) {
305
+ async migrate(c, directory, dryRun = false, emptyOnly = false, profileOnly = false, automatic) {
253
306
  return this.exclusive(c, async () => {
254
- await this.validate(c);
307
+ await this.validate(c, true);
308
+ if (automatic)
309
+ validateStorageDefault(c, automatic);
255
310
  const plan = await this.preflight(c, directory);
311
+ if (automatic) {
312
+ validateStorageDefault(c, automatic);
313
+ if (plan.destination !== join(automatic.base, c.appArguments.AppName, c.profile.name))
314
+ fail('binding', 'Automatic initialization changed its destination.');
315
+ }
316
+ if (!profileOnly)
317
+ await this.defaultBase(c, directory);
256
318
  if (dryRun)
257
- return { state: 'preflight', ...plan };
319
+ return { state: 'preflight', setsInstallationDefault: !profileOnly, ...plan };
258
320
  if (emptyOnly && plan.entries)
259
321
  fail('data', 'This profile already has analysis data. Use storage migrate --directory to preserve it.');
260
322
  const entries = await inventory(plan.source, analysisTrees, true, c.signal), id = randomUUID();
323
+ if (emptyOnly && entries.length)
324
+ fail('data', 'Analysis appeared during empty-profile initialization; no migration was started.');
261
325
  const binding = await this.binding(c);
262
326
  const j = { schema: 1, id, profile: c.profile.name, binding, source: plan.source, destination: plan.destination,
263
327
  sourceIdentity: await directoryIdentity(plan.source), destinationAnchor: plan.destinationAnchor, destinationIdentity: plan.destinationIdentity,
264
- previous: typeof c.profile.values.analysisDirectory === 'string' ? c.profile.values.analysisDirectory : null, state: 'copying', entries, trees: [...analysisTrees], manifest: digest(content(entries)), createdAt: new Date().toISOString(), bytes: entries.reduce((n, e) => n + e.size, 0), files: entries.filter(e => e.kind === 'file').length };
328
+ previous: typeof c.profile.values.analysisDirectory === 'string' ? c.profile.values.analysisDirectory : null, automatic: Boolean(automatic), defaultBinding: automatic, makeDefault: !profileOnly, defaultPreviousRevision: storageDefault(c)?.revision ?? null, state: 'copying', entries, trees: [...analysisTrees], manifest: digest(content(entries)), createdAt: new Date().toISOString(), bytes: entries.reduce((n, e) => n + e.size, 0), files: entries.filter(e => e.kind === 'file').length };
265
329
  // Reserve durably before any destination or source changes. All ordinary commands now freeze.
266
330
  await this.save(c, j);
267
331
  await this.checkpoint?.('reserved');
@@ -282,12 +346,35 @@ export class AnalysisStorage {
282
346
  fail('volume', 'A migration volume changed or disappeared. Mount the original volume before resuming; no fallback was created.');
283
347
  if (await canonicalFuture(j.destination) !== j.destination || j.targetIdentity && await directoryIdentity(j.destination) !== j.targetIdentity)
284
348
  fail('binding', 'Destination directory was replaced; no migration write was made.');
349
+ if (j.defaultBinding)
350
+ validateStorageDefault(c, j.defaultBinding);
351
+ if (j.defaultBaseIdentity && await directoryIdentity(dirname(dirname(j.destination))) !== j.defaultBaseIdentity)
352
+ fail('volume', 'The selected storage base directory was replaced.');
285
353
  if (j.state === 'rolling_back')
286
354
  return this.restore(c, j);
287
355
  if (j.state === 'cleaning')
288
356
  return this.finishCleanup(c, j);
289
- if (terminal(j.state))
357
+ if (terminal(j.state) && !(j.automatic && j.state === 'committed'))
290
358
  return this.show(c);
359
+ if (j.makeDefault && !j.defaultApplied) {
360
+ const base = dirname(dirname(j.destination));
361
+ if (await canonicalFuture(base) !== base)
362
+ fail('binding', 'Storage default base was replaced.');
363
+ await mkdir(base, { recursive: true, mode: 0o700 });
364
+ if (await directoryIdentity(j.destinationAnchor) !== j.destinationIdentity)
365
+ fail('volume', 'The default storage volume changed.');
366
+ if (!j.defaultBaseIdentity) {
367
+ j.defaultBaseIdentity = await directoryIdentity(base);
368
+ await this.save(c, j);
369
+ }
370
+ const result = await writeStorageDefault(c, base, j.id, j.defaultPreviousRevision ?? null);
371
+ await this.checkpoint?.('default-selected');
372
+ j.defaultApplied = true;
373
+ j.defaultSuperseded = result.state === 'superseded';
374
+ if (!j.defaultSuperseded)
375
+ j.defaultBinding = result.binding;
376
+ await this.save(c, j);
377
+ }
291
378
  if (j.state === 'copying') {
292
379
  await privateDirectory(j.destination);
293
380
  if (!j.targetIdentity) {
@@ -439,7 +526,14 @@ export class AnalysisStorage {
439
526
  await this.save(c, j);
440
527
  await atomicWrite(join(this.control(c), 'progress.json'), { completed: j.entries.length, total: j.entries.length, phase: 'committed' });
441
528
  }
442
- return { state: j.state, id: j.id, directory: j.destination, manifest: j.manifest, sourceRetained: true, rollback: 'storage rollback', cleanup: 'storage cleanup' };
529
+ if (j.automatic && j.state === 'committed') {
530
+ if (j.entries.length)
531
+ fail('binding', 'Automatic profile initialization cannot delete existing analysis.');
532
+ j.state = 'cleaning';
533
+ await this.save(c, j);
534
+ return this.finishCleanup(c, j);
535
+ }
536
+ return { state: j.state, id: j.id, directory: j.destination, manifest: j.manifest, sourceRetained: true, defaultBaseDirectory: storageDefault(c)?.base ?? null, installationDefault: j.makeDefault ? (j.defaultSuperseded ? 'superseded' : 'configured') : 'unchanged', rollback: 'storage rollback', cleanup: 'storage cleanup' };
443
537
  }
444
538
  async resume(c) { return this.exclusive(c, async () => { const j = await this.journal(c, true); if (!j)
445
539
  fail('missing', 'No migration journal exists.'); return this.continue(c, j); }); }
@@ -549,11 +643,13 @@ export class AnalysisStorage {
549
643
  fail('binding', 'Retained source directory was replaced; no deletion or restore was attempted.');
550
644
  }
551
645
  commands() {
646
+ const profileOnly = { flags: '--profile-only', description: 'Keep the installation default unchanged; move only this profile' };
552
647
  const directory = { flags: '--directory <path>', description: 'Absolute base directory; app name and profile are appended', required: true };
553
648
  return command('storage', 'Inspect and migrate profile analysis data', [
554
649
  command('show', 'Show effective analysis storage, usage and retained migration', (_i, c) => this.show(c), { permission: Permission.ReadOnly }),
555
- command('configure', 'Choose storage for a profile with no analysis data', ({ options: o }, c) => this.migrate(c, o.directory, false, true), { permission: Permission.Update, options: [directory] }),
556
- command('migrate', 'Copy and verify all analysis data, retain source, then switch storage', ({ options: o }, c) => this.migrate(c, o.directory, Boolean(o.dryRun)), { permission: Permission.Update, options: [directory, { flags: '--dry-run', description: 'Read-only path, capacity and quiescence preflight; create no migration' }] }),
650
+ command('default', 'Set the installation storage base for new profiles; preserve existing data locations', ({ options: o }, c) => this.setDefault(c, o.directory), { permission: Permission.Update, options: [directory] }),
651
+ command('configure', 'Choose storage for an empty profile and subsequent new profiles', ({ options: o }, c) => this.migrate(c, o.directory, false, true, Boolean(o.profileOnly)), { permission: Permission.Update, options: [directory, profileOnly] }),
652
+ command('migrate', 'Copy and verify all analysis data, retain source, then switch storage', ({ options: o }, c) => this.migrate(c, o.directory, Boolean(o.dryRun), false, Boolean(o.profileOnly)), { permission: Permission.Update, options: [directory, profileOnly, { flags: '--dry-run', description: 'Read-only path, capacity and quiescence preflight; create no migration' }] }),
557
653
  command('resume', 'Resume the retained copy/activation or rollback without replaying research', (_i, c) => this.resume(c), { permission: Permission.Update }),
558
654
  command('rollback', 'Restore retained source; refuse after any destination content changes', (_i, c) => this.rollback(c), { permission: Permission.Update }),
559
655
  command('cleanup', 'Delete verified retained source copies; retain compatibility paths permanently', (_i, c) => this.cleanup(c), { permission: 'Destructive' }),
@@ -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
  }