rea-agents 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -27
- package/bridge/hopper_bridge.py +161 -12
- package/dist/application/AnalysisSnapshotCache.js +131 -0
- package/dist/application/AnalysisSnapshotFiles.js +31 -0
- package/dist/application/ArtifactInventory.js +4 -0
- package/dist/application/AuthorizedArtifactInventory.js +16 -0
- package/dist/application/BinarySession.js +66 -36
- package/dist/application/BinarySessionPort.js +1 -0
- package/dist/application/BoundedJsonFiles.js +109 -0
- package/dist/application/CrossVersionInventory.js +98 -0
- package/dist/application/CrossVersionInvestigation.js +295 -0
- package/dist/application/DirectAnalysis.js +115 -22
- package/dist/application/Doctor.js +30 -1
- package/dist/application/EvidenceBundleFiles.js +15 -108
- package/dist/application/EvidenceLedger.js +3 -2
- package/dist/application/InvestigationProviders.js +48 -0
- package/dist/application/InvestigationWorkspaceStore.js +214 -0
- package/dist/application/LinuxHopper.js +72 -48
- package/dist/application/ProcessCaptureAuthority.js +2 -2
- package/dist/application/ProcessCaptureError.js +12 -0
- package/dist/application/ProcessCaptureLifecycle.js +19 -1
- package/dist/application/ProcessCli.js +98 -31
- package/dist/application/ProcessHarness.js +2 -2
- package/dist/application/ReferenceSourceImportEntries.js +18 -3
- package/dist/application/ReferenceSourceImportTypes.js +32 -0
- package/dist/application/Setup.js +33 -60
- package/dist/application/SetupClients.js +19 -0
- package/dist/application/SetupInstallFailure.js +53 -0
- package/dist/application/SetupPlan.js +31 -0
- package/dist/application/Uninstall.js +33 -23
- package/dist/application/UnknownEvidence.js +33 -0
- package/dist/application/Upgrade.js +1 -1
- package/dist/application/runtime.js +1 -1
- package/dist/artifacts/ArtifactProvider.js +2 -5
- package/dist/cli.js +31 -11
- package/dist/cliEvidenceCommands.js +11 -12
- package/dist/cliInvestigationCommands.js +86 -0
- package/dist/cliOutput.js +41 -0
- package/dist/cliProcessCommands.js +14 -4
- package/dist/config.js +39 -24
- package/dist/contracts/promptContracts.js +256 -0
- package/dist/contracts/sessionLifecycleInputs.js +11 -0
- package/dist/contracts/toolContractTypes.js +1 -0
- package/dist/contracts/toolContracts.js +10 -6
- package/dist/contracts/toolOutputSchemas.js +33 -4
- package/dist/domain/analysisSnapshot.js +149 -0
- package/dist/domain/changedBehavior.js +30 -7
- package/dist/domain/errors.js +180 -41
- package/dist/domain/evidence.js +4 -2
- package/dist/domain/evidenceBundle.js +28 -0
- package/dist/domain/hopperStartupFailure.js +55 -0
- package/dist/domain/investigationWorkspace.js +214 -0
- package/dist/domain/jsonValue.js +2 -0
- package/dist/domain/nativeInspection.js +3 -2
- package/dist/domain/processCapture.js +5 -4
- package/dist/domain/processComparison.js +5 -4
- package/dist/hopper/BridgeLauncher.js +47 -20
- package/dist/hopper/HopperClient.js +17 -2
- package/dist/hopper/HopperProvider.js +1 -0
- package/dist/main.js +47 -24
- package/dist/server/createServer.js +2 -0
- package/dist/server/promptCompletion.js +144 -0
- package/dist/server/registerEnhancedTools.js +2 -6
- package/dist/server/registerEvidenceTools.js +2 -8
- package/dist/server/registerFunctionComparisonTool.js +1 -1
- package/dist/server/registerInvestigationTools.js +31 -11
- package/dist/server/registerOfficialTools.js +6 -13
- package/dist/server/registerProcessComparisonTool.js +1 -7
- package/dist/server/registerPrompts.js +67 -0
- package/dist/server/registerSessionStatusTool.js +11 -0
- package/dist/server/registerSessionTools.js +52 -18
- package/dist/server/sessionEvidence.js +2 -8
- package/dist/server/sessionToolPolicies.js +1 -42
- package/dist/server/toolResult.js +7 -3
- package/install.sh +11 -11
- package/package.json +5 -3
- package/scripts/hopper-demo-x11.py +349 -0
- package/scripts/rea.mjs +6 -3
- package/skills/rea-analysis/SKILL.md +9 -2
|
@@ -6,6 +6,13 @@ import writeFileAtomic from "write-file-atomic";
|
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { PRODUCT_IDENTITY } from "../identity.js";
|
|
8
8
|
import { supportedClients } from "./Setup.js";
|
|
9
|
+
const systemFileSystem = {
|
|
10
|
+
readText: (path) => readFile(path, "utf8"),
|
|
11
|
+
copy: (source, destination) => copyFile(source, destination),
|
|
12
|
+
writeText: (path, contents) => writeFileAtomic(path, contents, { encoding: "utf8" }),
|
|
13
|
+
stat: (path) => lstat(path),
|
|
14
|
+
remove: (path) => rm(path, { recursive: true }),
|
|
15
|
+
};
|
|
9
16
|
/** Remove only REA-owned registrations and managed files, optionally including local state. */
|
|
10
17
|
export const runUninstall = async (purgeData, host = systemUninstallHost()) => {
|
|
11
18
|
const items = [];
|
|
@@ -27,26 +34,26 @@ export const runUninstall = async (purgeData, host = systemUninstallHost()) => {
|
|
|
27
34
|
};
|
|
28
35
|
};
|
|
29
36
|
/** Create uninstall effects contained to detected client configs and REA-owned paths. */
|
|
30
|
-
export const systemUninstallHost = (home = homedir()) => ({
|
|
37
|
+
export const systemUninstallHost = (home = homedir(), fileSystem = systemFileSystem) => ({
|
|
31
38
|
clients: () => Promise.resolve(supportedClients(home)),
|
|
32
|
-
removeClient,
|
|
33
|
-
removeSkill: () => removeManagedPath(join(home, ".agents/skills", PRODUCT_IDENTITY.skillName), "skill"),
|
|
39
|
+
removeClient: (client) => removeClient(client, fileSystem),
|
|
40
|
+
removeSkill: () => removeManagedPath(join(home, ".agents/skills", PRODUCT_IDENTITY.skillName), "skill", fileSystem),
|
|
34
41
|
purgeData: async () => [
|
|
35
|
-
await removeManagedPath(join(home, ".rea/cache"), "cache"),
|
|
36
|
-
await removeManagedPath(join(home, ".rea/state"), "state"),
|
|
42
|
+
await removeManagedPath(join(home, ".rea/cache"), "cache", fileSystem),
|
|
43
|
+
await removeManagedPath(join(home, ".rea/state"), "state", fileSystem),
|
|
37
44
|
],
|
|
38
45
|
});
|
|
39
|
-
const removeClient = async (client) => {
|
|
46
|
+
const removeClient = async (client, fileSystem) => {
|
|
40
47
|
if (client.format === "unsupported")
|
|
41
48
|
return item(client.name, "skipped", "This client has no documented local MCP configuration boundary.");
|
|
42
49
|
let original;
|
|
43
50
|
try {
|
|
44
|
-
original = await
|
|
51
|
+
original = await fileSystem.readText(client.configPath);
|
|
45
52
|
}
|
|
46
53
|
catch (cause) {
|
|
47
54
|
return isMissing(cause)
|
|
48
55
|
? item(client.name, "skipped", "Configuration does not exist.")
|
|
49
|
-
: item(client.name, "failed", "Configuration could not be read.");
|
|
56
|
+
: item(client.name, "failed", "Configuration could not be read. Check file permissions, then rerun uninstall.");
|
|
50
57
|
}
|
|
51
58
|
let document;
|
|
52
59
|
let key;
|
|
@@ -57,7 +64,7 @@ const removeClient = async (client) => {
|
|
|
57
64
|
servers = optionalObject(document[key]);
|
|
58
65
|
}
|
|
59
66
|
catch {
|
|
60
|
-
return item(client.name, "failed", "
|
|
67
|
+
return item(client.name, "failed", `Configuration is not valid ${client.format === "toml" ? "TOML" : "JSON"} and was not changed. Repair it, then rerun uninstall.`);
|
|
61
68
|
}
|
|
62
69
|
const registration = servers[PRODUCT_IDENTITY.mcpServerKey];
|
|
63
70
|
if (registration === undefined)
|
|
@@ -69,28 +76,31 @@ const removeClient = async (client) => {
|
|
|
69
76
|
document[key] = remaining;
|
|
70
77
|
const backupPath = `${client.configPath}.rea.backup`;
|
|
71
78
|
try {
|
|
72
|
-
await
|
|
79
|
+
await fileSystem.copy(client.configPath, backupPath);
|
|
73
80
|
}
|
|
74
81
|
catch {
|
|
75
|
-
return item(client.name, "failed", "Configuration
|
|
82
|
+
return item(client.name, "failed", "Configuration could not be backed up, so no change was made. Check file permissions, then rerun uninstall.");
|
|
76
83
|
}
|
|
77
84
|
try {
|
|
78
85
|
const encoded = client.format === "toml"
|
|
79
86
|
? stringifyToml(document)
|
|
80
87
|
: `${JSON.stringify(document, null, 2)}\n`;
|
|
81
|
-
await
|
|
88
|
+
await fileSystem.writeText(client.configPath, encoded);
|
|
82
89
|
const readback = objectSchema.parse(client.format === "toml"
|
|
83
|
-
? parseToml(await
|
|
84
|
-
: JSON.parse(await
|
|
90
|
+
? parseToml(await fileSystem.readText(client.configPath))
|
|
91
|
+
: JSON.parse(await fileSystem.readText(client.configPath)));
|
|
85
92
|
if (PRODUCT_IDENTITY.mcpServerKey in optionalObject(readback[key]))
|
|
86
93
|
throw new Error("registration readback mismatch");
|
|
87
94
|
return item(client.name, "removed", `Removed registration from ${client.configPath}.`);
|
|
88
95
|
}
|
|
89
96
|
catch {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
97
|
+
try {
|
|
98
|
+
await fileSystem.writeText(client.configPath, original);
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return item(client.name, "failed", "Configuration could not be updated or restored. Restore its `.rea.backup` manually, then rerun uninstall.");
|
|
102
|
+
}
|
|
103
|
+
return item(client.name, "failed", "Configuration could not be updated. The original was restored and its `.rea.backup` was retained. Repair the configuration or restore the backup, then rerun uninstall.");
|
|
94
104
|
}
|
|
95
105
|
};
|
|
96
106
|
/** Ownership rule for persistent MCP registrations created by REA setup. */
|
|
@@ -107,18 +117,18 @@ const isOwnedRegistration = (value) => {
|
|
|
107
117
|
JSON.stringify(args) ===
|
|
108
118
|
JSON.stringify(["-y", PRODUCT_IDENTITY.packageName, "mcp"])));
|
|
109
119
|
};
|
|
110
|
-
const removeManagedPath = async (path, name) => {
|
|
120
|
+
const removeManagedPath = async (path, name, fileSystem) => {
|
|
111
121
|
try {
|
|
112
|
-
const stats = await
|
|
122
|
+
const stats = await fileSystem.stat(path);
|
|
113
123
|
if (stats.isSymbolicLink())
|
|
114
|
-
return item(name, "retained",
|
|
115
|
-
await
|
|
124
|
+
return item(name, "retained", "REA did not remove this item because its managed path is a symbolic link. Verify the link target before removing it manually.");
|
|
125
|
+
await fileSystem.remove(path);
|
|
116
126
|
return item(name, "removed", `Removed ${path}.`);
|
|
117
127
|
}
|
|
118
128
|
catch (cause) {
|
|
119
129
|
return isMissing(cause)
|
|
120
130
|
? item(name, "skipped", `${path} does not exist.`)
|
|
121
|
-
: item(name, "failed",
|
|
131
|
+
: item(name, "failed", "This item could not be removed. Check file permissions, then rerun uninstall.");
|
|
122
132
|
}
|
|
123
133
|
};
|
|
124
134
|
const item = (name, status, detail) => ({ name, status, detail });
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { createEvidence } from "../domain/evidence.js";
|
|
2
|
+
/** Provider identity for analyst-authored residual-unknown mutations. */
|
|
3
|
+
export const UNKNOWN_REGISTRY_PROVIDER = {
|
|
4
|
+
id: "rea-unknown-registry",
|
|
5
|
+
name: "REA residual unknown registry",
|
|
6
|
+
version: "1",
|
|
7
|
+
};
|
|
8
|
+
/** Create immutable evidence for one approved residual-unknown mutation. */
|
|
9
|
+
export const unknownMutationEvidence = (target, input) => createEvidence(target, UNKNOWN_REGISTRY_PROVIDER, {
|
|
10
|
+
predicateType: "rea.residual-unknown-mutation/v1",
|
|
11
|
+
operation: "record_unknown",
|
|
12
|
+
parameters: { domain: input.domain, severity: input.severity },
|
|
13
|
+
result: {
|
|
14
|
+
action: "record",
|
|
15
|
+
question: input.question,
|
|
16
|
+
required_authority: input.required_authority,
|
|
17
|
+
required_confidence: input.required_confidence,
|
|
18
|
+
},
|
|
19
|
+
confidence: "derived",
|
|
20
|
+
authority: "analyst-inference",
|
|
21
|
+
evidenceLinks: unknownEvidenceLinks(input),
|
|
22
|
+
limitations: [
|
|
23
|
+
"Registry mutation evidence records analyst intent, not proof of the answer.",
|
|
24
|
+
],
|
|
25
|
+
});
|
|
26
|
+
/** Deduplicate evidence links carried by an unknown mutation. */
|
|
27
|
+
export const unknownEvidenceLinks = (input) => [
|
|
28
|
+
...input.supporting_evidence_ids,
|
|
29
|
+
...input.contradicting_evidence_ids,
|
|
30
|
+
...("resolution" in input && input.resolution !== null
|
|
31
|
+
? input.resolution.evidence_ids
|
|
32
|
+
: []),
|
|
33
|
+
].filter((id, index, values) => values.indexOf(id) === index);
|
|
@@ -33,7 +33,7 @@ export const runUpgrade = async (currentVersion, host = systemUpgradeHost(), out
|
|
|
33
33
|
currentVersion,
|
|
34
34
|
latestVersion: latestVersion ?? null,
|
|
35
35
|
reason: "install",
|
|
36
|
-
remediation:
|
|
36
|
+
remediation: `REA could not update through npm. Check npm registry access and global install permissions, then run: ${UPGRADE_COMMAND}`,
|
|
37
37
|
};
|
|
38
38
|
return {
|
|
39
39
|
status: "upgraded",
|
|
@@ -14,5 +14,5 @@ export const createBinarySession = (config, logger = silentLogger) => {
|
|
|
14
14
|
new ArtifactProvider(config.artifactNativeMountEnabled),
|
|
15
15
|
new NativeMacOSProvider(),
|
|
16
16
|
new HopperProvider(config, logger),
|
|
17
|
-
]));
|
|
17
|
+
]), { snapshotsEnabled: config.hopperLoaderArgs.length === 0 });
|
|
18
18
|
};
|
|
@@ -5,11 +5,8 @@ import { ARTIFACT_TOOL_CONTRACTS, artifactInventoryInputSchema, artifactExtracti
|
|
|
5
5
|
import { AnalysisCapabilityUnavailableError, ArtifactOperationError, } from "../domain/errors.js";
|
|
6
6
|
import { err, ok } from "../domain/result.js";
|
|
7
7
|
import { ArtifactReaderFailure, } from "./ArtifactReader.js";
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
name: "REA safe artifact graph provider",
|
|
11
|
-
version: "1",
|
|
12
|
-
});
|
|
8
|
+
import { ARTIFACT_GRAPH_PROVIDER } from "../application/InvestigationProviders.js";
|
|
9
|
+
const IDENTITY = Object.freeze(ARTIFACT_GRAPH_PROVIDER);
|
|
13
10
|
/** Read-only inventory and exclusively owned extraction provider. */
|
|
14
11
|
export class ArtifactProvider {
|
|
15
12
|
nativeMountEnabled;
|
package/dist/cli.js
CHANGED
|
@@ -13,6 +13,9 @@ import { parseConfig } from "./config.js";
|
|
|
13
13
|
import { importReferenceSource } from "./application/ReferenceSourceImport.js";
|
|
14
14
|
import { registerEvidenceCommands } from "./cliEvidenceCommands.js";
|
|
15
15
|
import { registerProcessCommands } from "./cliProcessCommands.js";
|
|
16
|
+
import { registerInvestigationCommands } from "./cliInvestigationCommands.js";
|
|
17
|
+
import { projectAnalysisError } from "./domain/errors.js";
|
|
18
|
+
import { projectReferenceSourceImportError } from "./application/ReferenceSourceImportTypes.js";
|
|
16
19
|
/**
|
|
17
20
|
* Build the one-shot Incur CLI without starting Hopper at import time.
|
|
18
21
|
* Analysis commands acquire and close their own sessions; bare `mcp` and
|
|
@@ -24,7 +27,7 @@ export const createCli = () => {
|
|
|
24
27
|
: parseLogLevel(process.env.REA_LOG_LEVEL));
|
|
25
28
|
const cli = Cli.create(PRODUCT_IDENTITY.cliBinary, {
|
|
26
29
|
version: process.env.REA_PACKAGE_VERSION ?? "0.0.0-development",
|
|
27
|
-
description: "Reverse engineer anything from your terminal or
|
|
30
|
+
description: "Reverse engineer anything from your terminal or agent.",
|
|
28
31
|
mcp: {
|
|
29
32
|
command: PRODUCT_IDENTITY.mcpCommand,
|
|
30
33
|
instructions: "Ask what software, artifact, protocol, or behavior the user wants to understand, then choose the available investigation capabilities that can produce evidence.",
|
|
@@ -48,6 +51,7 @@ export const createCli = () => {
|
|
|
48
51
|
registerCapabilityCommands(cli, logger);
|
|
49
52
|
registerNativeCommands(cli, logger);
|
|
50
53
|
registerArtifactCommands(cli, logger);
|
|
54
|
+
registerInvestigationCommands(cli, logger);
|
|
51
55
|
registerEvidenceCommands(cli, logger);
|
|
52
56
|
registerReferenceSourceCommand(cli, logger);
|
|
53
57
|
registerProcessCommands(cli, logger);
|
|
@@ -57,6 +61,11 @@ const registerCoreCommands = (cli, logger) => {
|
|
|
57
61
|
const overviewOptions = z.object({
|
|
58
62
|
detail: z.enum(["concise", "detailed"]).default("concise"),
|
|
59
63
|
limit: z.number().int().min(1).max(50).default(10),
|
|
64
|
+
snapshot: z
|
|
65
|
+
.string()
|
|
66
|
+
.min(1)
|
|
67
|
+
.optional()
|
|
68
|
+
.describe("Load and update a local analysis snapshot"),
|
|
60
69
|
});
|
|
61
70
|
cli.command("analyze", {
|
|
62
71
|
description: "Get an overview of an app",
|
|
@@ -64,7 +73,7 @@ const registerCoreCommands = (cli, logger) => {
|
|
|
64
73
|
path: z.string().describe("App, program, or Hopper database path"),
|
|
65
74
|
}),
|
|
66
75
|
options: overviewOptions,
|
|
67
|
-
run: ({ args, options }) => logCliCommand(logger, "analyze", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
|
|
76
|
+
run: ({ args, options }) => logCliCommand(logger, "analyze", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, { logger, snapshotPath: options.snapshot })),
|
|
68
77
|
});
|
|
69
78
|
cli.command("inspect", {
|
|
70
79
|
description: "Inspect an app overview with evidence",
|
|
@@ -72,7 +81,7 @@ const registerCoreCommands = (cli, logger) => {
|
|
|
72
81
|
path: z.string().describe("App, program, or Hopper database path"),
|
|
73
82
|
}),
|
|
74
83
|
options: overviewOptions,
|
|
75
|
-
run: ({ args, options }) => logCliCommand(logger, "inspect", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, logger)),
|
|
84
|
+
run: ({ args, options }) => logCliCommand(logger, "inspect", () => runDirectAnalysis(args.path, "binary_overview", { detail: options.detail, limit: options.limit }, { logger, snapshotPath: options.snapshot })),
|
|
76
85
|
});
|
|
77
86
|
cli.command("decompile", {
|
|
78
87
|
description: "Read one part of an app as code",
|
|
@@ -80,12 +89,13 @@ const registerCoreCommands = (cli, logger) => {
|
|
|
80
89
|
path: z.string().describe("App or program path"),
|
|
81
90
|
address: z.string().describe("Procedure address"),
|
|
82
91
|
}),
|
|
83
|
-
|
|
92
|
+
options: z.object({ snapshot: z.string().min(1).optional() }),
|
|
93
|
+
run: ({ args, options }) => logCliCommand(logger, "decompile", () => runDirectAnalysis(args.path, "procedure_pseudo_code", { procedure: args.address }, { logger, snapshotPath: options.snapshot })),
|
|
84
94
|
});
|
|
85
95
|
};
|
|
86
96
|
const registerSetupCommands = (cli, logger) => {
|
|
87
97
|
cli.command("setup", {
|
|
88
|
-
description: "Install requirements and configure
|
|
98
|
+
description: "Install requirements and configure agents",
|
|
89
99
|
options: z.object({
|
|
90
100
|
yes: z
|
|
91
101
|
.boolean()
|
|
@@ -153,7 +163,8 @@ const registerXrefsCommand = (cli, logger) => {
|
|
|
153
163
|
path: z.string().describe("App or program path"),
|
|
154
164
|
address: z.string().describe("Hexadecimal address"),
|
|
155
165
|
}),
|
|
156
|
-
|
|
166
|
+
options: z.object({ snapshot: z.string().min(1).optional() }),
|
|
167
|
+
run: ({ args, options }) => logCliCommand(logger, "xrefs", () => runDirectAnalysis(args.path, "xrefs", { address: args.address }, { logger, snapshotPath: options.snapshot })),
|
|
157
168
|
});
|
|
158
169
|
};
|
|
159
170
|
const registerTraceCommand = (cli, logger) => {
|
|
@@ -167,6 +178,7 @@ const registerTraceCommand = (cli, logger) => {
|
|
|
167
178
|
caseSensitive: z.boolean().default(false),
|
|
168
179
|
limit: z.number().int().min(1).max(100).default(20),
|
|
169
180
|
maxOperations: z.number().int().min(1).max(100).default(20),
|
|
181
|
+
snapshot: z.string().min(1).optional(),
|
|
170
182
|
}),
|
|
171
183
|
alias: {
|
|
172
184
|
caseSensitive: "case-sensitive",
|
|
@@ -177,7 +189,7 @@ const registerTraceCommand = (cli, logger) => {
|
|
|
177
189
|
case_sensitive: options.caseSensitive,
|
|
178
190
|
limit: options.limit,
|
|
179
191
|
max_operations: options.maxOperations,
|
|
180
|
-
}, logger)),
|
|
192
|
+
}, { logger, snapshotPath: options.snapshot })),
|
|
181
193
|
});
|
|
182
194
|
};
|
|
183
195
|
const registerCapabilityCommands = (cli, logger) => {
|
|
@@ -202,6 +214,7 @@ const registerFunctionCommand = (cli, logger) => {
|
|
|
202
214
|
limit: z.number().int().min(1).max(500).default(100),
|
|
203
215
|
maxPseudocodeChars: z.number().int().min(1).max(100_000).default(20_000),
|
|
204
216
|
maxInstructions: z.number().int().min(1).max(5_000).default(500),
|
|
217
|
+
snapshot: z.string().min(1).optional(),
|
|
205
218
|
}),
|
|
206
219
|
alias: {
|
|
207
220
|
includeAssembly: "include-assembly",
|
|
@@ -214,7 +227,7 @@ const registerFunctionCommand = (cli, logger) => {
|
|
|
214
227
|
limit: options.limit,
|
|
215
228
|
max_pseudocode_chars: options.maxPseudocodeChars,
|
|
216
229
|
max_instructions: options.maxInstructions,
|
|
217
|
-
}, logger)),
|
|
230
|
+
}, { logger, snapshotPath: options.snapshot })),
|
|
218
231
|
});
|
|
219
232
|
};
|
|
220
233
|
const registerSearchCommand = (cli, logger) => {
|
|
@@ -230,6 +243,7 @@ const registerSearchCommand = (cli, logger) => {
|
|
|
230
243
|
caseSensitive: z.boolean().default(false),
|
|
231
244
|
offset: z.number().int().min(0).default(0),
|
|
232
245
|
limit: z.number().int().min(1).max(100).default(100),
|
|
246
|
+
snapshot: z.string().min(1).optional(),
|
|
233
247
|
}),
|
|
234
248
|
alias: { caseSensitive: "case-sensitive" },
|
|
235
249
|
run: ({ args, options }) => logCliCommand(logger, "search", () => runDirectAnalysis(args.path, options.kind === "strings" ? "search_strings" : "search_procedures", {
|
|
@@ -238,7 +252,7 @@ const registerSearchCommand = (cli, logger) => {
|
|
|
238
252
|
case_sensitive: options.caseSensitive,
|
|
239
253
|
offset: options.offset,
|
|
240
254
|
limit: options.limit,
|
|
241
|
-
}, logger)),
|
|
255
|
+
}, { logger, snapshotPath: options.snapshot })),
|
|
242
256
|
});
|
|
243
257
|
};
|
|
244
258
|
const registerReferenceSourceCommand = (cli, logger) => {
|
|
@@ -252,7 +266,10 @@ const registerReferenceSourceCommand = (cli, logger) => {
|
|
|
252
266
|
run: ({ args }) => logCliCommand(logger, "import-reference-source", async () => {
|
|
253
267
|
const config = parseConfig(process.env);
|
|
254
268
|
if (!config.ok)
|
|
255
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
error: "Import failed",
|
|
271
|
+
...projectAnalysisError(config.error),
|
|
272
|
+
};
|
|
256
273
|
const imported = await importReferenceSource({
|
|
257
274
|
root: args.root,
|
|
258
275
|
caller: "rea-cli",
|
|
@@ -262,7 +279,10 @@ const registerReferenceSourceCommand = (cli, logger) => {
|
|
|
262
279
|
});
|
|
263
280
|
return imported.ok
|
|
264
281
|
? imported.value
|
|
265
|
-
: {
|
|
282
|
+
: {
|
|
283
|
+
error: "Import failed",
|
|
284
|
+
...projectReferenceSourceImportError(imported.error),
|
|
285
|
+
};
|
|
266
286
|
}),
|
|
267
287
|
});
|
|
268
288
|
};
|
|
@@ -2,6 +2,7 @@ import { Cli, z } from "incur";
|
|
|
2
2
|
import { compareEvidenceBundlesCommand, exportEvidenceBundleCommand, importEvidenceBundleCommand, } from "./application/EvidenceBundleCommands.js";
|
|
3
3
|
import { parseConfig } from "./config.js";
|
|
4
4
|
import { logCliCommand } from "./cliLogging.js";
|
|
5
|
+
import { projectAnalysisError } from "./domain/errors.js";
|
|
5
6
|
/** Register filesystem-gated Evidence v2 commands. */
|
|
6
7
|
export const registerEvidenceCommands = (cli, logger) => {
|
|
7
8
|
cli.command("evidence-import", {
|
|
@@ -12,11 +13,9 @@ export const registerEvidenceCommands = (cli, logger) => {
|
|
|
12
13
|
run: ({ args }) => logCliCommand(logger, "evidence-import", async () => {
|
|
13
14
|
const config = parseConfig(process.env);
|
|
14
15
|
if (!config.ok)
|
|
15
|
-
return
|
|
16
|
+
return cliError(config.error);
|
|
16
17
|
const imported = await importEvidenceBundleCommand(args.path, config.value.evidenceFilePolicy);
|
|
17
|
-
return imported.ok
|
|
18
|
-
? imported.value
|
|
19
|
-
: { error: imported.error._tag, message: imported.error.message };
|
|
18
|
+
return imported.ok ? imported.value : cliError(imported.error);
|
|
20
19
|
}),
|
|
21
20
|
});
|
|
22
21
|
cli.command("evidence-export", {
|
|
@@ -31,11 +30,9 @@ export const registerEvidenceCommands = (cli, logger) => {
|
|
|
31
30
|
run: ({ args, options }) => logCliCommand(logger, "evidence-export", async () => {
|
|
32
31
|
const config = parseConfig(process.env);
|
|
33
32
|
if (!config.ok)
|
|
34
|
-
return
|
|
33
|
+
return cliError(config.error);
|
|
35
34
|
const exported = await exportEvidenceBundleCommand(args.source, args.output, options.overwrite, config.value.evidenceFilePolicy);
|
|
36
|
-
return exported.ok
|
|
37
|
-
? exported.value
|
|
38
|
-
: { error: exported.error._tag, message: exported.error.message };
|
|
35
|
+
return exported.ok ? exported.value : cliError(exported.error);
|
|
39
36
|
}),
|
|
40
37
|
});
|
|
41
38
|
cli.command("compare", {
|
|
@@ -52,7 +49,7 @@ export const registerEvidenceCommands = (cli, logger) => {
|
|
|
52
49
|
run: ({ args, options }) => logCliCommand(logger, "compare", async () => {
|
|
53
50
|
const config = parseConfig(process.env);
|
|
54
51
|
if (!config.ok)
|
|
55
|
-
return
|
|
52
|
+
return cliError(config.error);
|
|
56
53
|
const compared = await compareEvidenceBundlesCommand({
|
|
57
54
|
leftPath: args.left,
|
|
58
55
|
rightPath: args.right,
|
|
@@ -60,9 +57,11 @@ export const registerEvidenceCommands = (cli, logger) => {
|
|
|
60
57
|
limit: options.limit,
|
|
61
58
|
policy: config.value.evidenceFilePolicy,
|
|
62
59
|
});
|
|
63
|
-
return compared.ok
|
|
64
|
-
? compared.value
|
|
65
|
-
: { error: compared.error._tag, message: compared.error.message };
|
|
60
|
+
return compared.ok ? compared.value : cliError(compared.error);
|
|
66
61
|
}),
|
|
67
62
|
});
|
|
68
63
|
};
|
|
64
|
+
const cliError = (error) => ({
|
|
65
|
+
error: "Analysis failed",
|
|
66
|
+
...projectAnalysisError(error),
|
|
67
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Cli, z } from "incur";
|
|
2
|
+
import { runCrossVersionInvestigation } from "./application/CrossVersionInvestigation.js";
|
|
3
|
+
import { logCliCommand } from "./cliLogging.js";
|
|
4
|
+
import { parseConfig } from "./config.js";
|
|
5
|
+
const investigationOptionsSchema = z.object({
|
|
6
|
+
yes: z
|
|
7
|
+
.boolean()
|
|
8
|
+
.default(false)
|
|
9
|
+
.describe("Approve persistent workspace writes"),
|
|
10
|
+
workspaceName: z.string().trim().min(1).max(200).default("default"),
|
|
11
|
+
expectedRevision: z.number().int().min(1).optional(),
|
|
12
|
+
maxEntries: z.number().int().min(1).max(50_000).default(10_000),
|
|
13
|
+
maxTotalBytes: z
|
|
14
|
+
.number()
|
|
15
|
+
.int()
|
|
16
|
+
.min(1)
|
|
17
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
18
|
+
.default(1_073_741_824),
|
|
19
|
+
maxEntryBytes: z
|
|
20
|
+
.number()
|
|
21
|
+
.int()
|
|
22
|
+
.min(1)
|
|
23
|
+
.max(Number.MAX_SAFE_INTEGER)
|
|
24
|
+
.default(268_435_456),
|
|
25
|
+
pageSize: z.number().int().min(1).max(500).default(500),
|
|
26
|
+
changeLimit: z.number().int().min(1).max(500).default(500),
|
|
27
|
+
});
|
|
28
|
+
/** Register the persistent cross-version CLI workflow. */
|
|
29
|
+
export const registerInvestigationCommands = (cli, logger) => {
|
|
30
|
+
cli.command("investigate-versions", {
|
|
31
|
+
description: "Run or resume a persistent cross-version artifact investigation",
|
|
32
|
+
args: z.object({
|
|
33
|
+
leftPath: z.string().describe("Earlier artifact or directory path"),
|
|
34
|
+
rightPath: z.string().describe("Later artifact or directory path"),
|
|
35
|
+
workspacePath: z.string().describe("Persistent workspace JSON path"),
|
|
36
|
+
}),
|
|
37
|
+
options: investigationOptionsSchema,
|
|
38
|
+
alias: {
|
|
39
|
+
workspaceName: "workspace-name",
|
|
40
|
+
expectedRevision: "expected-revision",
|
|
41
|
+
maxEntries: "max-entries",
|
|
42
|
+
maxTotalBytes: "max-total-bytes",
|
|
43
|
+
maxEntryBytes: "max-entry-bytes",
|
|
44
|
+
pageSize: "page-size",
|
|
45
|
+
changeLimit: "change-limit",
|
|
46
|
+
yes: "y",
|
|
47
|
+
},
|
|
48
|
+
run: ({ args, options }) => logCliCommand(logger, "investigate-versions", () => runInvestigation(args, options)),
|
|
49
|
+
});
|
|
50
|
+
};
|
|
51
|
+
const runInvestigation = async (args, options) => {
|
|
52
|
+
if (!options.yes)
|
|
53
|
+
return {
|
|
54
|
+
error: "ApprovalRequired",
|
|
55
|
+
message: "Persistent workspace writes require explicit --yes approval.",
|
|
56
|
+
};
|
|
57
|
+
const config = parseConfig(process.env);
|
|
58
|
+
if (!config.ok)
|
|
59
|
+
return { error: config.error._tag, message: config.error.message };
|
|
60
|
+
const investigated = await runCrossVersionInvestigation({
|
|
61
|
+
approved: true,
|
|
62
|
+
workspace_path: args.workspacePath,
|
|
63
|
+
workspace_name: options.workspaceName,
|
|
64
|
+
...(options.expectedRevision === undefined
|
|
65
|
+
? {}
|
|
66
|
+
: { expected_workspace_revision: options.expectedRevision }),
|
|
67
|
+
left_path: args.leftPath,
|
|
68
|
+
right_path: args.rightPath,
|
|
69
|
+
options: {
|
|
70
|
+
max_entries: options.maxEntries,
|
|
71
|
+
max_total_bytes: options.maxTotalBytes,
|
|
72
|
+
max_entry_bytes: options.maxEntryBytes,
|
|
73
|
+
max_compression_ratio: 1_000,
|
|
74
|
+
max_depth: 20,
|
|
75
|
+
max_path_bytes: 4_096,
|
|
76
|
+
page_size: options.pageSize,
|
|
77
|
+
change_limit: options.changeLimit,
|
|
78
|
+
},
|
|
79
|
+
}, config.value.evidenceFilePolicy, { inputRoots: config.value.investigationInputRoots });
|
|
80
|
+
return investigated.ok
|
|
81
|
+
? investigated.value.evidence
|
|
82
|
+
: {
|
|
83
|
+
error: investigated.error._tag,
|
|
84
|
+
message: investigated.error.message,
|
|
85
|
+
};
|
|
86
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const SAFE_VALIDATION_MESSAGE = "REA could not read the command arguments. Run `rea --help`, correct the arguments, then try again.";
|
|
2
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
3
|
+
const validationError = (value) => {
|
|
4
|
+
if (!isRecord(value))
|
|
5
|
+
return undefined;
|
|
6
|
+
if (value.code === "VALIDATION_ERROR")
|
|
7
|
+
return value;
|
|
8
|
+
if (value.ok === false && isRecord(value.error))
|
|
9
|
+
return value.error.code === "VALIDATION_ERROR" ? value.error : undefined;
|
|
10
|
+
return undefined;
|
|
11
|
+
};
|
|
12
|
+
/** Remove validator internals from Incur's caller-visible CLI output. */
|
|
13
|
+
export const sanitizeCliOutput = (output) => {
|
|
14
|
+
const trimmed = output.trimStart();
|
|
15
|
+
if (trimmed.startsWith("{")) {
|
|
16
|
+
try {
|
|
17
|
+
const parsed = JSON.parse(trimmed);
|
|
18
|
+
const error = validationError(parsed);
|
|
19
|
+
if (error !== undefined) {
|
|
20
|
+
const safeError = {
|
|
21
|
+
code: "VALIDATION_ERROR",
|
|
22
|
+
message: SAFE_VALIDATION_MESSAGE,
|
|
23
|
+
};
|
|
24
|
+
if (error === parsed)
|
|
25
|
+
return `${JSON.stringify(safeError)}\n`;
|
|
26
|
+
if (!isRecord(parsed))
|
|
27
|
+
return output;
|
|
28
|
+
return `${JSON.stringify({ ...parsed, error: safeError })}\n`;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return output;
|
|
33
|
+
}
|
|
34
|
+
return output;
|
|
35
|
+
}
|
|
36
|
+
if (/^ok: false\r?\nerror:\r?\n code: VALIDATION_ERROR(?:\r?\n|$)/u.test(trimmed))
|
|
37
|
+
return `ok: false\nerror:\n code: VALIDATION_ERROR\n message: "${SAFE_VALIDATION_MESSAGE}"\n`;
|
|
38
|
+
if (!/^code: VALIDATION_ERROR(?:\r?\n|$)/u.test(trimmed))
|
|
39
|
+
return output;
|
|
40
|
+
return `code: VALIDATION_ERROR\nmessage: "${SAFE_VALIDATION_MESSAGE}"\n`;
|
|
41
|
+
};
|
|
@@ -1,12 +1,22 @@
|
|
|
1
|
-
import { Cli, z } from "incur";
|
|
2
|
-
import { captureProcessScenarioFile, compareProcessEvidenceFiles, } from "./application/ProcessCli.js";
|
|
1
|
+
import { Cli, Errors, z } from "incur";
|
|
2
|
+
import { captureProcessScenarioFile, compareProcessEvidenceFiles, isProcessCliErrorOutput, } from "./application/ProcessCli.js";
|
|
3
3
|
import { logCliCommand } from "./cliLogging.js";
|
|
4
|
+
const requireProcessCommandSuccess = async (execute) => {
|
|
5
|
+
const value = await execute();
|
|
6
|
+
if (isProcessCliErrorOutput(value))
|
|
7
|
+
throw new Errors.IncurError({
|
|
8
|
+
code: "PROCESS_COMMAND_FAILED",
|
|
9
|
+
message: value.message,
|
|
10
|
+
retryable: false,
|
|
11
|
+
});
|
|
12
|
+
return value;
|
|
13
|
+
};
|
|
4
14
|
/** Register Process Capture v4 one-shot commands through shared application services. */
|
|
5
15
|
export const registerProcessCommands = (cli, logger) => {
|
|
6
16
|
cli.command("capture-process", {
|
|
7
17
|
description: "Capture one approved Process Capture v4 JSON scenario",
|
|
8
18
|
args: z.object({ scenario: z.string().describe("Scenario JSON path") }),
|
|
9
|
-
run: ({ args }) => logCliCommand(logger, "capture-process", () => captureProcessScenarioFile(args.scenario)),
|
|
19
|
+
run: ({ args }) => logCliCommand(logger, "capture-process", () => requireProcessCommandSuccess(() => captureProcessScenarioFile(args.scenario))),
|
|
10
20
|
});
|
|
11
21
|
cli.command("compare-process-captures", {
|
|
12
22
|
description: "Compare two Process Capture v4 Evidence JSON files",
|
|
@@ -14,6 +24,6 @@ export const registerProcessCommands = (cli, logger) => {
|
|
|
14
24
|
left: z.string().describe("Left capture Evidence JSON path"),
|
|
15
25
|
right: z.string().describe("Right capture Evidence JSON path"),
|
|
16
26
|
}),
|
|
17
|
-
run: ({ args }) => logCliCommand(logger, "compare-process-captures", () => compareProcessEvidenceFiles(args.left, args.right)),
|
|
27
|
+
run: ({ args }) => logCliCommand(logger, "compare-process-captures", () => requireProcessCommandSuccess(() => compareProcessEvidenceFiles(args.left, args.right))),
|
|
18
28
|
});
|
|
19
29
|
};
|