in2-agent-server 0.20260821.185429
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 +369 -0
- package/agents/plan-requirement.md +32 -0
- package/dist/campfire/configItem.js +201 -0
- package/dist/campfire/configItemDescription.js +379 -0
- package/dist/campfire/deriveFromBundle.js +1013 -0
- package/dist/cli.js +465 -0
- package/dist/core/betaFeedback.js +493 -0
- package/dist/core/chapterInstructions.js +473 -0
- package/dist/core/claudeAssets.js +61 -0
- package/dist/core/gate.js +201 -0
- package/dist/core/ids.js +9 -0
- package/dist/core/install.js +60 -0
- package/dist/core/operatorQa.js +462 -0
- package/dist/core/orgCallMeter.js +178 -0
- package/dist/core/parcels.js +1235 -0
- package/dist/core/paths.js +105 -0
- package/dist/core/phases.js +225 -0
- package/dist/core/planFile.js +251 -0
- package/dist/core/planFragments.js +119 -0
- package/dist/core/readsLog.js +398 -0
- package/dist/core/registration.js +24 -0
- package/dist/core/result.js +38 -0
- package/dist/core/resume.js +233 -0
- package/dist/core/revisionChain.js +351 -0
- package/dist/core/richText.js +61 -0
- package/dist/core/semanticHash.js +89 -0
- package/dist/core/sessionOrg.js +60 -0
- package/dist/core/sfQuery.js +19 -0
- package/dist/core/spawn.js +182 -0
- package/dist/core/supervisorContract.js +230 -0
- package/dist/core/telemetry.js +1297 -0
- package/dist/core/telemetryContext.js +154 -0
- package/dist/core/telemetrySpool.js +244 -0
- package/dist/core/transcriptUpload.js +516 -0
- package/dist/core/update.js +363 -0
- package/dist/core/viewer.js +117 -0
- package/dist/core/workspace.js +92 -0
- package/dist/core/writeGrants.js +93 -0
- package/dist/core/writebackManifest.js +92 -0
- package/dist/direct/applyLoop.js +2516 -0
- package/dist/direct/batchReport.js +510 -0
- package/dist/direct/comparatorNoise.js +75 -0
- package/dist/direct/containers.js +85 -0
- package/dist/direct/contamination.js +221 -0
- package/dist/direct/crud.js +56 -0
- package/dist/direct/destructive.js +168 -0
- package/dist/direct/knownTokens.js +94 -0
- package/dist/direct/lint.js +382 -0
- package/dist/direct/metadataSchema.generated.json +1 -0
- package/dist/direct/metadataSchema.js +179 -0
- package/dist/direct/operatorPage.js +349 -0
- package/dist/direct/orgRepo.js +1531 -0
- package/dist/direct/orgRepoSpawnCount.js +21 -0
- package/dist/direct/plannedScope.js +119 -0
- package/dist/direct/preImageTargets.js +63 -0
- package/dist/direct/refVerify.js +191 -0
- package/dist/direct/registry.js +129 -0
- package/dist/direct/removals.js +262 -0
- package/dist/direct/rollback.js +1513 -0
- package/dist/direct/rollbackRun.js +1360 -0
- package/dist/direct/sessionProjection.js +126 -0
- package/dist/direct/snapshotComparability.js +165 -0
- package/dist/direct/sourceTree.js +113 -0
- package/dist/direct/unstatedChanges.js +106 -0
- package/dist/direct/waveAssembly.js +556 -0
- package/dist/direct/waveCounts.js +19 -0
- package/dist/direct/waveDerivation.js +152 -0
- package/dist/direct/waveGrade.js +185 -0
- package/dist/index.js +90 -0
- package/dist/orgread/customFieldSnapshot.js +48 -0
- package/dist/orgread/fieldMetadata.js +178 -0
- package/dist/orgread/genericSnapshot.js +61 -0
- package/dist/orgread/layoutSnapshot.js +151 -0
- package/dist/orgread/salesforceOrgReader.js +184 -0
- package/dist/orgread/snapshotTypes.js +9 -0
- package/dist/projection/bridge.js +26 -0
- package/dist/projection/diff.js +141 -0
- package/dist/projection/mapmd.js +213 -0
- package/dist/projection/nameAmbiguity.js +106 -0
- package/dist/projection/precedent.js +136 -0
- package/dist/projection/project.js +214 -0
- package/dist/projection/scope.js +110 -0
- package/dist/projection/types.js +5 -0
- package/dist/tools/analyze.js +322 -0
- package/dist/tools/auth.js +915 -0
- package/dist/tools/campfire.js +1742 -0
- package/dist/tools/closeBatch.js +154 -0
- package/dist/tools/closeText.js +35 -0
- package/dist/tools/direct.js +1613 -0
- package/dist/tools/feedback.js +318 -0
- package/dist/tools/operator.js +287 -0
- package/dist/tools/org.js +739 -0
- package/dist/tools/phase.js +249 -0
- package/dist/tools/projection.js +172 -0
- package/dist/tools/session.js +313 -0
- package/dist/tools/update.js +82 -0
- package/dist/tools/waves.js +793 -0
- package/package.json +48 -0
- package/reference/campfire-configuration-items-guide.md +710 -0
- package/reference/deployability-cribs.md +119 -0
- package/reference/operator-voice.md +141 -0
- package/reference/org-read-cribs.md +34 -0
- package/reference/scoped-dml-interim.md +68 -0
- package/reference/supervisor-playbook.md +313 -0
- package/reference/templates.md +233 -0
- package/skills/in2-delivery/SKILL.md +322 -0
- package/skills/in2-delivery/references/campfire-configuration-items-guide.md +710 -0
- package/skills/in2-delivery/references/deployability-cribs.md +119 -0
- package/skills/in2-delivery/references/operator-voice.md +141 -0
- package/skills/in2-delivery/references/org-read-cribs.md +34 -0
- package/skills/in2-delivery/references/scoped-dml-interim.md +68 -0
- package/skills/in2-delivery/references/templates.md +233 -0
- package/skills/plan-requirement/SKILL.md +188 -0
- package/viewer/index.html +899 -0
- package/viewer/server.mjs +433 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The `in2` CLI — setup, check, update, diagnose, from outside the MCP session.
|
|
3
|
+
//
|
|
4
|
+
// WHY THIS EXISTS SEPARATELY FROM THE MCP TOOLS
|
|
5
|
+
//
|
|
6
|
+
// update_server lives inside the server, which makes it convenient and makes it useless in exactly
|
|
7
|
+
// the situations that matter most:
|
|
8
|
+
//
|
|
9
|
+
// - a server that fails to start has no tools at all, so nothing in-session can diagnose it
|
|
10
|
+
// - a broken install needs a way back that does not depend on the thing that broke
|
|
11
|
+
//
|
|
12
|
+
// Distribution is npm (`npm install -g in2-agent-server`), so `in2 update` is a thin wrapper over
|
|
13
|
+
// `npm install -g` — the reason it exists at all is that the operator should never need to know
|
|
14
|
+
// the package name, and `in2 doctor` should be one word to say on a support call. `in2 setup` is
|
|
15
|
+
// what replaced ~1000 lines of platform-forked installer shell: everything after "get Node and run
|
|
16
|
+
// npm install" lives here, in one language, testable, shipping with the thing it installs.
|
|
17
|
+
//
|
|
18
|
+
// Claude reaches this through Bash, which means it keeps working when MCP does not.
|
|
19
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
20
|
+
import fs from "node:fs/promises";
|
|
21
|
+
import path from "node:path";
|
|
22
|
+
import { IN2_HOME, PACKAGE_NAME, SERVER_ROOT, installedVersion, isNpmInstall } from "./core/install.js";
|
|
23
|
+
import { checkForUpdate, installUpdate, npmInvocation } from "./core/update.js";
|
|
24
|
+
import { sfInvocation } from "./core/spawn.js";
|
|
25
|
+
import { WORKSPACE_ROOT } from "./core/paths.js";
|
|
26
|
+
const argv = process.argv.slice(2);
|
|
27
|
+
const command = argv[0] ?? "help";
|
|
28
|
+
const flag = (name) => {
|
|
29
|
+
const i = argv.indexOf(`--${name}`);
|
|
30
|
+
return i >= 0 ? argv[i + 1] : undefined;
|
|
31
|
+
};
|
|
32
|
+
const has = (name) => argv.includes(`--${name}`);
|
|
33
|
+
const out = (s = "") => {
|
|
34
|
+
process.stdout.write(`${s}\n`);
|
|
35
|
+
};
|
|
36
|
+
const die = (s) => {
|
|
37
|
+
process.stderr.write(`${s}\n`);
|
|
38
|
+
return process.exit(1);
|
|
39
|
+
};
|
|
40
|
+
async function cmdVersion() {
|
|
41
|
+
out(installedVersion());
|
|
42
|
+
}
|
|
43
|
+
const DEFAULT_LAUNCH_URL = "https://in2-launch.onrender.com";
|
|
44
|
+
/**
|
|
45
|
+
* Everything a fresh machine needs after `npm install -g in2-agent-server`, in one command.
|
|
46
|
+
*
|
|
47
|
+
* The npm install put THIS CLI on a PATH somewhere; what is left is the machine setup the old
|
|
48
|
+
* platform-forked installers did in ~1000 lines of bash/PowerShell:
|
|
49
|
+
* 1. the Salesforce CLI — the server shells out to `sf` for every deploy; installed into a
|
|
50
|
+
* private prefix under ~/.in2-agent/tools when absent, so no machine ever needs sudo
|
|
51
|
+
* 2. the Claude Code registration — server env is fixed at spawn, so every gate the run needs
|
|
52
|
+
* travels on the registration, not on a shell profile
|
|
53
|
+
* 3. skills + agents into ~/.claude (the server re-syncs these at every start; doing it here too
|
|
54
|
+
* means the FIRST conversation already has them)
|
|
55
|
+
* 4. a real JSON-RPC probe, because "registered" and "starts" are different claims
|
|
56
|
+
*
|
|
57
|
+
* Idempotent: re-running repairs a registration, re-syncs assets, and skips what already works.
|
|
58
|
+
*/
|
|
59
|
+
async function cmdSetup() {
|
|
60
|
+
const launchUrl = flag("launch-url") ?? DEFAULT_LAUNCH_URL;
|
|
61
|
+
const serverEntry = path.join(SERVER_ROOT, "dist", "index.js");
|
|
62
|
+
// 1. Salesforce CLI. System `sf` wins; otherwise a private npm-prefix install under IN2_HOME.
|
|
63
|
+
//
|
|
64
|
+
// The private path is probed and registered by its JS ENTRY, never through PATH. Two live-caught
|
|
65
|
+
// reasons (adversarial review, 2026-08-21): sfInvocation memoizes its Windows PATH scan, so a
|
|
66
|
+
// probe taken before the install poisons every probe after it; and a PATH mutated in THIS
|
|
67
|
+
// process reaches neither the server (spawned by the MCP client with its own env) nor the next
|
|
68
|
+
// `in2 setup` run. CC_MCP_SF_ENTRY on the registration env is the one channel that reliably
|
|
69
|
+
// reaches the server, on every platform — spawn.ts honors it ahead of any PATH resolution.
|
|
70
|
+
out("checking for the Salesforce CLI…");
|
|
71
|
+
const toolsDir = path.join(IN2_HOME, "tools");
|
|
72
|
+
const privateSfEntry = path.join(toolsDir, "node_modules", "@salesforce", "cli", "bin", "run.js");
|
|
73
|
+
const entryWorks = (entry) => {
|
|
74
|
+
try {
|
|
75
|
+
execFileSync(process.execPath, [entry, "--version"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const systemSfWorks = () => {
|
|
83
|
+
try {
|
|
84
|
+
const sf = sfInvocation(["--version"]);
|
|
85
|
+
execFileSync(sf.command, sf.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
let sfEntry;
|
|
93
|
+
if (await fs
|
|
94
|
+
.stat(privateSfEntry)
|
|
95
|
+
.then(() => entryWorks(privateSfEntry))
|
|
96
|
+
.catch(() => false)) {
|
|
97
|
+
// A previous setup already installed it privately — reuse, and re-register the entry.
|
|
98
|
+
sfEntry = privateSfEntry;
|
|
99
|
+
}
|
|
100
|
+
else if (!systemSfWorks()) {
|
|
101
|
+
out("installing @salesforce/cli (the slow step: ~150 MB down, ~576 MB on disk)…");
|
|
102
|
+
await fs.mkdir(toolsDir, { recursive: true });
|
|
103
|
+
const npm = npmInvocation(["install", "--prefix", toolsDir, "@salesforce/cli", "--no-audit", "--no-fund"]);
|
|
104
|
+
execFileSync(npm.command, npm.args, { stdio: "inherit", timeout: 1_800_000 });
|
|
105
|
+
// Verified by running the entry we just installed — not by re-resolving PATH, which would
|
|
106
|
+
// consult sfInvocation's pre-install cache and report a working install as broken.
|
|
107
|
+
if (!entryWorks(privateSfEntry)) {
|
|
108
|
+
die("the Salesforce CLI did not install cleanly — re-run `in2 setup`, or install `sf` yourself and retry.");
|
|
109
|
+
}
|
|
110
|
+
sfEntry = privateSfEntry;
|
|
111
|
+
}
|
|
112
|
+
out(` ok — sf is available${sfEntry ? " (private install under ~/.in2-agent/tools)" : ""}`);
|
|
113
|
+
// 2. Register with Claude Code, idempotently (remove-then-add; remove of a missing entry is fine).
|
|
114
|
+
//
|
|
115
|
+
// The env rides the REGISTRATION because MCP server env is fixed at spawn: CC_MCP_LAUNCH_URL is
|
|
116
|
+
// what lets activate_session redeem a code; the two write gates are capabilities whose per-org
|
|
117
|
+
// AUTHORITY still comes from activation grants; CC_MCP_BETA=1 keeps the feedback channel with
|
|
118
|
+
// the install. CC_MCP_DIRECT_ALLOWLIST is deliberately never set — on a consultant machine it
|
|
119
|
+
// would make their other clients' orgs writable.
|
|
120
|
+
//
|
|
121
|
+
// The command registered is `<this node> <absolute dist/index.js>`: the package path is stable
|
|
122
|
+
// across `npm install -g` updates (same prefix), and naming the node binary dodges every PATH
|
|
123
|
+
// difference between a login shell and however the client spawns servers.
|
|
124
|
+
out("registering with Claude Code…");
|
|
125
|
+
const resolved = await resolveClaude();
|
|
126
|
+
const claude = resolved ??
|
|
127
|
+
die("Claude Code's `claude` CLI was not found. Install it (https://claude.ai/install.sh), then re-run `in2 setup`.");
|
|
128
|
+
const env = [
|
|
129
|
+
["CC_MCP_LAUNCH_URL", launchUrl],
|
|
130
|
+
["CC_MCP_DIRECT_WRITE", "1"],
|
|
131
|
+
["CC_MCP_CAMPFIRE_WRITE", "1"],
|
|
132
|
+
["CC_MCP_BETA", "1"],
|
|
133
|
+
// On EVERY platform, not only Windows: a private sf install is off the server's PATH by
|
|
134
|
+
// construction, and this env var is how the server finds it (spawn.ts checks it first).
|
|
135
|
+
...(sfEntry ? [["CC_MCP_SF_ENTRY", sfEntry]] : [])
|
|
136
|
+
];
|
|
137
|
+
// Through cmd.exe on Windows when the resolved claude is a shim (a .cmd cannot be spawned
|
|
138
|
+
// directly); direct everywhere else. None of the argv values here carry user-typed content, so
|
|
139
|
+
// cmd.exe's re-tokenization is safe in a way it is not for sf's SOQL-bearing arguments.
|
|
140
|
+
const claudeCall = (args) => process.platform === "win32" && !claude.toLowerCase().endsWith(".exe")
|
|
141
|
+
? ["cmd.exe", ["/d", "/c", claude, ...args]]
|
|
142
|
+
: [claude, args];
|
|
143
|
+
try {
|
|
144
|
+
const [cmd, args] = claudeCall(["mcp", "remove", "in2", "-s", "user"]);
|
|
145
|
+
execFileSync(cmd, args, { stdio: "ignore" });
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
// Not registered yet — the normal first-run case.
|
|
149
|
+
}
|
|
150
|
+
{
|
|
151
|
+
const [cmd, args] = claudeCall([
|
|
152
|
+
"mcp",
|
|
153
|
+
"add",
|
|
154
|
+
"in2",
|
|
155
|
+
"-s",
|
|
156
|
+
"user",
|
|
157
|
+
...env.flatMap(([k, v]) => ["-e", `${k}=${v}`]),
|
|
158
|
+
"--",
|
|
159
|
+
process.execPath,
|
|
160
|
+
serverEntry
|
|
161
|
+
]);
|
|
162
|
+
execFileSync(cmd, args, { stdio: "inherit" });
|
|
163
|
+
}
|
|
164
|
+
// 3. Skills + agents now, not just at first server start.
|
|
165
|
+
const { installClaudeAssets } = await import("./core/claudeAssets.js");
|
|
166
|
+
await installClaudeAssets();
|
|
167
|
+
out(" ok — skills and agents installed to ~/.claude");
|
|
168
|
+
// 4. Prove it starts. Same probe as the doctor; a registration that cannot answer initialize is
|
|
169
|
+
// a setup that failed, and this is the moment to say so.
|
|
170
|
+
out("verifying the server starts…");
|
|
171
|
+
const started = await probeServer(serverEntry);
|
|
172
|
+
if (!started)
|
|
173
|
+
die("the server did not answer an MCP initialize — run `in2 doctor` for the breakdown.");
|
|
174
|
+
out(" ok — server answers");
|
|
175
|
+
out("");
|
|
176
|
+
out("Setup complete. Open a NEW Claude Code conversation and paste your activation prompt.");
|
|
177
|
+
}
|
|
178
|
+
/** The `claude` binary, probed at its real install locations — a non-interactive shell's PATH may miss it. */
|
|
179
|
+
async function resolveClaude() {
|
|
180
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "";
|
|
181
|
+
const candidates = process.platform === "win32"
|
|
182
|
+
? ["claude.exe", path.join(home, ".local", "bin", "claude.exe")]
|
|
183
|
+
: ["claude", path.join(home, ".local", "bin", "claude"), "/usr/local/bin/claude", "/opt/homebrew/bin/claude"];
|
|
184
|
+
for (const candidate of candidates) {
|
|
185
|
+
try {
|
|
186
|
+
execFileSync(candidate, ["--version"], { stdio: "ignore" });
|
|
187
|
+
return candidate;
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// keep probing
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// An npm-installed Claude Code on Windows is claude.cmd + claude.ps1, no .exe anywhere — and a
|
|
194
|
+
// cmd shim cannot be spawned directly (CVE-2024-27980, same constraint spawn.ts documents for
|
|
195
|
+
// sf.cmd). Probe it through cmd.exe; the registration itself then also runs through cmd.exe.
|
|
196
|
+
if (process.platform === "win32") {
|
|
197
|
+
try {
|
|
198
|
+
execFileSync("cmd.exe", ["/d", "/c", "claude", "--version"], { stdio: "ignore" });
|
|
199
|
+
return "claude";
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// truly absent
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
async function cmdCheck() {
|
|
208
|
+
const check = await checkForUpdate(flag("registry"));
|
|
209
|
+
if (has("json")) {
|
|
210
|
+
out(JSON.stringify(check, null, 2));
|
|
211
|
+
// Exit 1 signals "an update is available" so a script can branch without parsing prose.
|
|
212
|
+
process.exit(check.updateAvailable ? 1 : 0);
|
|
213
|
+
}
|
|
214
|
+
out(`installed : ${check.currentVersion}`);
|
|
215
|
+
out(`published : ${check.latestVersion ?? "unknown"}`);
|
|
216
|
+
out(check.updateAvailable ? "an update is available — run: in2 update" : "up to date");
|
|
217
|
+
if (check.reason)
|
|
218
|
+
out(`note : ${check.reason}`);
|
|
219
|
+
process.exit(check.updateAvailable ? 1 : 0);
|
|
220
|
+
}
|
|
221
|
+
async function cmdUpdate() {
|
|
222
|
+
const current = installedVersion();
|
|
223
|
+
const result = await installUpdate({
|
|
224
|
+
force: has("force"),
|
|
225
|
+
...(flag("registry") ? { registry: flag("registry") } : {})
|
|
226
|
+
});
|
|
227
|
+
if (!result.ok) {
|
|
228
|
+
if (result.failure === "not-managed") {
|
|
229
|
+
die(`Not an npm install (running from ${SERVER_ROOT}).\nA repo checkout updates with git pull; anything else, reinstall with: npm install -g ${PACKAGE_NAME}`);
|
|
230
|
+
}
|
|
231
|
+
die(`update failed: ${result.error ?? "unknown error"} — nothing was changed; still on ${current}.`);
|
|
232
|
+
}
|
|
233
|
+
if (!result.updated) {
|
|
234
|
+
out(`already on ${current}; published version is ${result.toVersion}. Nothing to do.`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
out(`installed ${result.toVersion} (was ${current})`);
|
|
238
|
+
// Said plainly because it is the difference between "done" and "done, and you lost your session".
|
|
239
|
+
out("An MCP server already running does NOT pick this up — a NEW conversation runs the new version.");
|
|
240
|
+
}
|
|
241
|
+
// Everything that has to be true for a session to work, checked in the order it would break.
|
|
242
|
+
// Written because the failure it diagnoses is otherwise invisible: the server registers fine and
|
|
243
|
+
// then fails on first use, in a new conversation, with no obvious cause.
|
|
244
|
+
async function cmdDoctor() {
|
|
245
|
+
const problems = [];
|
|
246
|
+
const check = (label, okay, detail = "") => {
|
|
247
|
+
out(`${okay ? " ok " : " FAIL"} ${label}${detail ? ` — ${detail}` : ""}`);
|
|
248
|
+
if (!okay)
|
|
249
|
+
problems.push(label);
|
|
250
|
+
};
|
|
251
|
+
out(`IN2 home : ${IN2_HOME}`);
|
|
252
|
+
out(`install : ${installedVersion()} from ${SERVER_ROOT}`);
|
|
253
|
+
out("");
|
|
254
|
+
check("npm-managed install", isNpmInstall(), isNpmInstall() ? "" : "a checkout updates with git, not in2 update");
|
|
255
|
+
// npm resolvable — it is the whole update path now.
|
|
256
|
+
let npmOk = false;
|
|
257
|
+
let npmVersion = "";
|
|
258
|
+
try {
|
|
259
|
+
const npm = npmInvocation(["--version"]);
|
|
260
|
+
npmVersion = execFileSync(npm.command, npm.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
261
|
+
.split("\n")[0]
|
|
262
|
+
.trim();
|
|
263
|
+
npmOk = true;
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
npmOk = false;
|
|
267
|
+
}
|
|
268
|
+
check("npm reachable", npmOk, npmVersion ? `npm ${npmVersion}` : "updates need it");
|
|
269
|
+
// The server shells out to `sf` for every deploy and every Campfire read/write.
|
|
270
|
+
let sfOk = false;
|
|
271
|
+
let sfVersion = "";
|
|
272
|
+
try {
|
|
273
|
+
const sf = sfInvocation(["--version"]);
|
|
274
|
+
sfVersion = execFileSync(sf.command, sf.args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] })
|
|
275
|
+
.split("\n")[0]
|
|
276
|
+
.trim();
|
|
277
|
+
sfOk = true;
|
|
278
|
+
}
|
|
279
|
+
catch {
|
|
280
|
+
sfOk = false;
|
|
281
|
+
}
|
|
282
|
+
check("Salesforce CLI reachable", sfOk, sfVersion || "every deploy and writeback needs it");
|
|
283
|
+
const entry = path.join(SERVER_ROOT, "dist", "index.js");
|
|
284
|
+
const entryExists = await fs
|
|
285
|
+
.stat(entry)
|
|
286
|
+
.then(() => true)
|
|
287
|
+
.catch(() => false);
|
|
288
|
+
check("server entry present", entryExists, entry);
|
|
289
|
+
if (entryExists) {
|
|
290
|
+
check("server starts and answers", await probeServer(entry));
|
|
291
|
+
}
|
|
292
|
+
const update = await checkForUpdate();
|
|
293
|
+
out("");
|
|
294
|
+
out(update.updateAvailable
|
|
295
|
+
? `An update is available: ${update.currentVersion} -> ${update.latestVersion}. Run: in2 update`
|
|
296
|
+
: `Version ${update.currentVersion}${update.latestVersion ? " (latest)" : ""}${update.reason ? ` — ${update.reason}` : ""}`);
|
|
297
|
+
if (problems.length > 0) {
|
|
298
|
+
out("");
|
|
299
|
+
die(`${problems.length} problem(s): ${problems.join("; ")}`);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// The only check that proves the server actually works rather than merely looks right: spawn it
|
|
303
|
+
// and complete an MCP initialize round-trip. stdin must STAY OPEN until the response arrives — a
|
|
304
|
+
// probe that writes the request and closes immediately loses a race on Windows (the transport sees
|
|
305
|
+
// EOF and shuts down before the async initialize dispatch flushes; live-caught in CI).
|
|
306
|
+
async function probeServer(entry) {
|
|
307
|
+
return new Promise((resolve) => {
|
|
308
|
+
const child = spawn(process.execPath, [entry], { stdio: ["pipe", "pipe", "ignore"] });
|
|
309
|
+
let buf = "";
|
|
310
|
+
let settled = false;
|
|
311
|
+
const settle = (v) => {
|
|
312
|
+
if (settled)
|
|
313
|
+
return;
|
|
314
|
+
settled = true;
|
|
315
|
+
try {
|
|
316
|
+
child.kill();
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
/* already gone */
|
|
320
|
+
}
|
|
321
|
+
resolve(v);
|
|
322
|
+
};
|
|
323
|
+
const timer = setTimeout(() => settle(false), 60_000);
|
|
324
|
+
timer.unref();
|
|
325
|
+
child.stdout.on("data", (d) => {
|
|
326
|
+
buf += String(d);
|
|
327
|
+
if (buf.includes('"result"')) {
|
|
328
|
+
clearTimeout(timer);
|
|
329
|
+
try {
|
|
330
|
+
child.stdin.end(); // now EOF — the server exits on its own
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
/* stream already closed */
|
|
334
|
+
}
|
|
335
|
+
settle(true);
|
|
336
|
+
}
|
|
337
|
+
});
|
|
338
|
+
child.on("error", () => {
|
|
339
|
+
clearTimeout(timer);
|
|
340
|
+
settle(false);
|
|
341
|
+
});
|
|
342
|
+
child.on("exit", () => {
|
|
343
|
+
clearTimeout(timer);
|
|
344
|
+
settle(buf.includes('"result"'));
|
|
345
|
+
});
|
|
346
|
+
child.stdin.write(`${JSON.stringify({
|
|
347
|
+
jsonrpc: "2.0",
|
|
348
|
+
id: 1,
|
|
349
|
+
method: "initialize",
|
|
350
|
+
params: { protocolVersion: "2024-11-05", capabilities: {}, clientInfo: { name: "in2-doctor", version: "0" } }
|
|
351
|
+
})}\n`);
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Re-materialize one org's mirror from every evidence bundle this machine can reach.
|
|
356
|
+
*
|
|
357
|
+
* OUT OF PROCESS ON PURPOSE, like everything else here: the mirror is a rebuildable cache derived
|
|
358
|
+
* from the bundles, and the case that needs rebuilding is precisely the case where the RULE for
|
|
359
|
+
* reading a bundle changed under an existing history — an install that has already committed waves
|
|
360
|
+
* under the old rule keeps them until something replays them. Nothing in a live session should be
|
|
361
|
+
* rewriting history underneath a conversation that may be quoting it.
|
|
362
|
+
*
|
|
363
|
+
* `--fetch` IS OPT-IN, and that is a deliberate choice about what this command is. Without it the
|
|
364
|
+
* command is a local repair that touches no network and needs no grant; with it, it is the
|
|
365
|
+
* convergence step (R24) — this machine's bundles united with the parcels other machines uploaded
|
|
366
|
+
* for the same org, replayed into one history. Either way the result SAYS WHICH, because a
|
|
367
|
+
* local-only history and an org-wide one look identical from the outside and only one of them
|
|
368
|
+
* supports "this is what happened in this org".
|
|
369
|
+
*
|
|
370
|
+
* Safe to run at any time and safe to run twice: same bundles in, same history out.
|
|
371
|
+
*/
|
|
372
|
+
async function cmdRebuildMirror() {
|
|
373
|
+
const orgId = argv[1];
|
|
374
|
+
if (!orgId)
|
|
375
|
+
die("usage: in2 rebuild-mirror <orgId> [--fetch [--batch <batchId>]]");
|
|
376
|
+
const { mirrorPath, rebuildFromSets, workspaceBundleDirs } = await import("./direct/orgRepo.js");
|
|
377
|
+
const bundles = await workspaceBundleDirs();
|
|
378
|
+
out(`workspace : ${WORKSPACE_ROOT}`);
|
|
379
|
+
out(`bundles : ${bundles.length} local`);
|
|
380
|
+
// The fetch runs BEFORE the "nothing to rebuild from" bail: an install that has never delivered
|
|
381
|
+
// to this org still has an org history to materialize once another machine's parcels arrive, and
|
|
382
|
+
// refusing on an empty local set would make that machine permanently unable to see it.
|
|
383
|
+
let foreign;
|
|
384
|
+
if (has("fetch")) {
|
|
385
|
+
const { fetchForeignBundles } = await import("./core/parcels.js");
|
|
386
|
+
const batchId = flag("batch");
|
|
387
|
+
const fetched = await fetchForeignBundles({
|
|
388
|
+
orgId: orgId,
|
|
389
|
+
...(batchId ? { batchId } : {}),
|
|
390
|
+
log: (line) => out(` ${line}`)
|
|
391
|
+
});
|
|
392
|
+
out(fetched.ok
|
|
393
|
+
? `parcels : ${fetched.listed} listed, ${fetched.downloaded} downloaded, ${fetched.cached} already staged, ${fetched.failed} failed`
|
|
394
|
+
: `parcels : NOT FETCHED — ${fetched.unavailable}`);
|
|
395
|
+
for (const problem of fetched.problems.slice(0, 10))
|
|
396
|
+
out(` ! ${problem}`);
|
|
397
|
+
foreign = fetched.ok
|
|
398
|
+
? { dirs: fetched.bundles.map((bundle) => bundle.bundleDir), ...(fetched.failed > 0 ? { missing: fetched.failed } : {}) }
|
|
399
|
+
: { dirs: [], unavailable: fetched.unavailable ?? "the parcel fetch did not complete" };
|
|
400
|
+
}
|
|
401
|
+
if (bundles.length === 0 && (foreign?.dirs.length ?? 0) === 0) {
|
|
402
|
+
die("no evidence bundles found — nothing to rebuild from. Check the workspace path above.");
|
|
403
|
+
}
|
|
404
|
+
const result = await rebuildFromSets(orgId, bundles, foreign);
|
|
405
|
+
out(`mirror : ${mirrorPath(orgId)}`);
|
|
406
|
+
out(`waves : ${result.local} local, ${result.foreign} foreign`);
|
|
407
|
+
// Fewer commits than waves is the rule working, not a loss: a wave that re-delivered unchanged
|
|
408
|
+
// content leaves the tree alone and records nothing (R2).
|
|
409
|
+
out(`commits : ${result.commits}`);
|
|
410
|
+
out(`skipped : ${result.skipped} bundle(s) — another org, or a verdict that never changed this one`);
|
|
411
|
+
// Its own line, never folded into `skipped`: a skip is coverage this mirror correctly does not
|
|
412
|
+
// hold, and an unreadable parcel is coverage it is MISSING. The scope note says the same thing in
|
|
413
|
+
// sentences; this says it in the count, next to the counts it must not be mistaken for.
|
|
414
|
+
if (result.unreadable > 0) {
|
|
415
|
+
out(`unreadable: ${result.unreadable} fetched parcel(s) staged but NOT readable as a wave — waves may be missing`);
|
|
416
|
+
}
|
|
417
|
+
if (result.duplicates > 0)
|
|
418
|
+
out(`duplicate : ${result.duplicates} wave(s) present both locally and as a parcel — replayed once`);
|
|
419
|
+
out(`head : ${result.head ?? "(no wave commits)"}`);
|
|
420
|
+
if (result.materializerMismatch) {
|
|
421
|
+
out(`rebuilt : the previous mirror carried ${result.priorMaterializers.join(", ")} — every commit has been re-derived under this build`);
|
|
422
|
+
}
|
|
423
|
+
out("");
|
|
424
|
+
out(`scope : ${result.scope.note}`);
|
|
425
|
+
out("");
|
|
426
|
+
out("Every commit id in this mirror is NEW — a rebuild rewrites them by design. Read history through");
|
|
427
|
+
out("direct_history and address a wave by (session, wave), never by a commit id.");
|
|
428
|
+
}
|
|
429
|
+
try {
|
|
430
|
+
switch (command) {
|
|
431
|
+
case "version":
|
|
432
|
+
await cmdVersion();
|
|
433
|
+
break;
|
|
434
|
+
case "setup":
|
|
435
|
+
await cmdSetup();
|
|
436
|
+
break;
|
|
437
|
+
case "check":
|
|
438
|
+
await cmdCheck();
|
|
439
|
+
break;
|
|
440
|
+
case "update":
|
|
441
|
+
await cmdUpdate();
|
|
442
|
+
break;
|
|
443
|
+
case "doctor":
|
|
444
|
+
await cmdDoctor();
|
|
445
|
+
break;
|
|
446
|
+
case "rebuild-mirror":
|
|
447
|
+
await cmdRebuildMirror();
|
|
448
|
+
break;
|
|
449
|
+
default:
|
|
450
|
+
out("in2 — manage the IN2 MCP server install");
|
|
451
|
+
out("");
|
|
452
|
+
out(" in2 setup [--launch-url URL] first-time machine setup: sf CLI, Claude Code registration, skills");
|
|
453
|
+
out(" in2 check [--json] is a newer version published on npm? (exit 1 = yes)");
|
|
454
|
+
out(" in2 update [--force] npm install -g the published version");
|
|
455
|
+
out(" in2 doctor check everything a session needs, and say what is broken");
|
|
456
|
+
out(" in2 version print the installed version");
|
|
457
|
+
out(" in2 rebuild-mirror <orgId> [--fetch] re-derive an org's history from its evidence bundles");
|
|
458
|
+
out(" --fetch also pulls other machines' parcels for that org");
|
|
459
|
+
out("");
|
|
460
|
+
out("An update never changes a conversation already running — start a NEW conversation to use it.");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
die(`in2 ${command}: ${error instanceof Error ? error.message : String(error)}`);
|
|
465
|
+
}
|