create-snipara 2.1.0 → 2.2.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/dist/index.js CHANGED
@@ -5,16 +5,16 @@ import { Command, Option } from "commander";
5
5
  import prompts from "prompts";
6
6
  import chalk from "chalk";
7
7
  import ora from "ora";
8
- import { randomUUID } from "crypto";
9
- import fs from "fs";
10
- import path from "path";
11
- import { execSync, spawn } from "child_process";
12
- import { homedir } from "os";
8
+ import { randomUUID as randomUUID3 } from "crypto";
9
+ import fs4 from "fs";
10
+ import path4 from "path";
11
+ import { execFileSync as execFileSync3, execSync, spawn as spawn2 } from "child_process";
12
+ import { homedir as homedir2 } from "os";
13
13
 
14
14
  // package.json
15
15
  var package_default = {
16
16
  name: "create-snipara",
17
- version: "2.1.0",
17
+ version: "2.2.0",
18
18
  description: "Onboard Snipara Cloud or self-hosted MCP with an automatic project-grounded proof of value",
19
19
  main: "dist/index.js",
20
20
  type: "module",
@@ -22,8 +22,8 @@ var package_default = {
22
22
  "create-snipara": "dist/index.js"
23
23
  },
24
24
  scripts: {
25
- build: "tsup src/index.ts --format esm --dts --clean",
26
- dev: "tsup src/index.ts --format esm --watch",
25
+ build: "tsup --config tsup.config.ts",
26
+ dev: "tsup --config tsup.config.ts --watch",
27
27
  prepack: "npm run build",
28
28
  pretest: "pnpm build",
29
29
  test: "node --test test/*.test.cjs",
@@ -74,6 +74,7 @@ var package_default = {
74
74
  },
75
75
  devDependencies: {
76
76
  "@eslint/js": "^9.39.3",
77
+ "@snipara/project-intelligence-contracts": "workspace:*",
77
78
  "@types/node": "^20.17.12",
78
79
  "@types/prompts": "^2.4.9",
79
80
  "@typescript-eslint/eslint-plugin": "^8.56.0",
@@ -90,6 +91,781 @@ var package_default = {
90
91
  ]
91
92
  };
92
93
 
94
+ // src/managed-toolchain.ts
95
+ import fs from "fs";
96
+ import path from "path";
97
+ import { homedir } from "os";
98
+ import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
99
+ import { spawn } from "child_process";
100
+
101
+ // ../project-intelligence-contracts/src/worker-profile.ts
102
+ import { createHash } from "crypto";
103
+
104
+ // ../project-intelligence-contracts/src/producer-loop.ts
105
+ import { createHash as createHash2 } from "crypto";
106
+
107
+ // ../project-intelligence-contracts/src/decision-request.ts
108
+ import { createHash as createHash3 } from "crypto";
109
+
110
+ // ../project-intelligence-contracts/src/why-capture-confidence-v1.json
111
+ var why_capture_confidence_v1_default = {
112
+ version: "why-capture-confidence-v1",
113
+ base: 0.72,
114
+ decisionRationaleBonus: 0.1,
115
+ nonManualSourceBonus: 0.04,
116
+ maximum: 0.9,
117
+ decimalPlaces: 2,
118
+ validatedExecutionOutcomes: ["completed"],
119
+ vectors: [
120
+ {
121
+ candidateType: "CONTEXT",
122
+ hasDecision: false,
123
+ hasRationale: false,
124
+ sourceKind: "manual",
125
+ executionOutcome: "completed",
126
+ expected: 0.72
127
+ },
128
+ {
129
+ candidateType: "DECISION",
130
+ hasDecision: true,
131
+ hasRationale: true,
132
+ sourceKind: "manual",
133
+ executionOutcome: "completed",
134
+ expected: 0.82
135
+ },
136
+ {
137
+ candidateType: "DECISION",
138
+ hasDecision: true,
139
+ hasRationale: true,
140
+ sourceKind: "phase_commit",
141
+ executionOutcome: "completed",
142
+ expected: 0.86
143
+ },
144
+ {
145
+ candidateType: "DECISION",
146
+ hasDecision: true,
147
+ hasRationale: true,
148
+ sourceKind: "phase_commit",
149
+ executionOutcome: "partial",
150
+ expected: 0.76
151
+ },
152
+ {
153
+ candidateType: "DECISION",
154
+ hasDecision: true,
155
+ hasRationale: true,
156
+ sourceKind: "phase_commit",
157
+ executionOutcome: "blocked",
158
+ expected: 0.76
159
+ },
160
+ {
161
+ candidateType: "DECISION",
162
+ hasDecision: true,
163
+ hasRationale: true,
164
+ sourceKind: "phase_commit",
165
+ executionOutcome: "abandoned",
166
+ expected: 0.76
167
+ },
168
+ {
169
+ candidateType: "DECISION",
170
+ hasDecision: true,
171
+ hasRationale: false,
172
+ sourceKind: "final_commit",
173
+ executionOutcome: "completed",
174
+ expected: 0.76
175
+ }
176
+ ]
177
+ };
178
+
179
+ // ../project-intelligence-contracts/src/why-capture-confidence.ts
180
+ var WHY_CAPTURE_CONFIDENCE_VERSION = why_capture_confidence_v1_default.version;
181
+ var WHY_CAPTURE_CONFIDENCE_TEST_VECTORS = why_capture_confidence_v1_default.vectors;
182
+
183
+ // ../project-intelligence-contracts/src/judgment-integrity.ts
184
+ import { randomUUID } from "crypto";
185
+
186
+ // ../project-intelligence-contracts/src/agent-memory-pilot.ts
187
+ import { createHash as createHash4 } from "crypto";
188
+
189
+ // ../project-intelligence-contracts/src/managed-toolchain.ts
190
+ var MANAGED_TOOLCHAIN_VERSION = "snipara.toolchain.v1";
191
+ var MANAGED_TOOLCHAIN_COMPONENTS = ["companion", "orchestrator", "sandbox"];
192
+ var object = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
193
+ var bounded = (value, max) => typeof value === "string" && value.length > 0 && value.length <= max && !Array.from(value).some((char) => char.charCodeAt(0) < 32 || char.charCodeAt(0) === 127);
194
+ function canonicalAbsolute(value) {
195
+ return bounded(value, 4096) && value.startsWith("/") && value !== "/" && !value.endsWith("/") && !value.split("/").some((part, index) => index > 0 && ["", ".", ".."].includes(part));
196
+ }
197
+ function parseManagedToolchainManifest(value) {
198
+ if (!object(value) || value.schemaVersion !== MANAGED_TOOLCHAIN_VERSION || !bounded(value.bundleId, 128) || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(value.bundleId) || !canonicalAbsolute(value.root) || !object(value.components) || !object(value.contracts) || value.contracts.taskReference !== 1 || value.contracts.validation !== 1 || !Array.isArray(value.capabilities) || value.capabilities.length > 32 || !value.capabilities.every((item) => bounded(item, 64) && /^[a-z][a-z0-9_-]*$/.test(item)) || new Set(value.capabilities).size !== value.capabilities.length) {
199
+ throw new Error("Unsupported or invalid managed toolchain manifest");
200
+ }
201
+ const root = value.root;
202
+ const components = {};
203
+ for (const name of MANAGED_TOOLCHAIN_COMPONENTS) {
204
+ const component = value.components[name];
205
+ if (!object(component) || !bounded(component.version, 64) || !/^\d+\.\d+\.\d+(?:-[A-Za-z0-9.-]+)?$/.test(component.version) || !canonicalAbsolute(component.command) || !component.command.startsWith(root + "/") || typeof component.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(component.sha256)) {
206
+ throw new Error("Invalid managed component identity");
207
+ }
208
+ components[name] = { version: component.version, command: component.command, sha256: component.sha256 };
209
+ }
210
+ return {
211
+ schemaVersion: MANAGED_TOOLCHAIN_VERSION,
212
+ bundleId: value.bundleId,
213
+ root,
214
+ components,
215
+ contracts: { taskReference: 1, validation: 1 },
216
+ capabilities: [...value.capabilities]
217
+ };
218
+ }
219
+
220
+ // src/managed-versions.json
221
+ var managed_versions_default = {
222
+ companion: "4.2.0",
223
+ orchestrator: "1.16.0",
224
+ sandbox: "3.0.0"
225
+ };
226
+
227
+ // src/managed-toolchain.ts
228
+ var MANAGED_VERSIONS = Object.freeze(managed_versions_default);
229
+ function assertManagedRuntimeVersions(nodeVersion, pythonVersion) {
230
+ const node = /^v?(\d+)\.\d+\.\d+(?:[-+].*)?$/.exec(nodeVersion);
231
+ const python = /^Python (\d+)\.(\d+)\.\d+(?:[a-z0-9.+-]*)?$/i.exec(pythonVersion.trim());
232
+ if (!node || Number(node[1]) < 18 || !python || Number(python[1]) !== 3 || Number(python[2]) < 10)
233
+ throw new Error("Local work requires existing Node.js 18+ and Python 3.10+. Update your system runtimes and retry; no operating-system runtime was installed");
234
+ }
235
+ var ManagedInstallerCleanupError = class extends Error {
236
+ };
237
+ var sha = (bytes2) => createHash5("sha256").update(bytes2).digest("hex");
238
+ var uid = () => {
239
+ if (!process.getuid || process.platform === "win32") throw new Error("Managed local work requires macOS or Linux");
240
+ return process.getuid();
241
+ };
242
+ function exists(file) {
243
+ try {
244
+ fs.lstatSync(file);
245
+ return true;
246
+ } catch (error) {
247
+ if (error.code === "ENOENT") return false;
248
+ throw error;
249
+ }
250
+ }
251
+ function owned(file, directory, privateMode = false) {
252
+ const info = fs.lstatSync(file);
253
+ if (info.uid !== uid() || info.isSymbolicLink() || (directory ? !info.isDirectory() : !info.isFile()) || !directory && info.nlink !== 1 || (info.mode & 18) !== 0 || privateMode && (info.mode & 63) !== 0)
254
+ throw new Error("Managed installation path is not owned and protected");
255
+ return info;
256
+ }
257
+ function privateDirectory(directory) {
258
+ if (!exists(directory)) {
259
+ privateDirectory(path.dirname(directory));
260
+ fs.mkdirSync(directory, { mode: 448 });
261
+ }
262
+ owned(directory, true);
263
+ }
264
+ function location(options) {
265
+ const home = fs.realpathSync(options.home ?? homedir());
266
+ owned(home, true);
267
+ return path.join(home, ".local/share/snipara/toolchains");
268
+ }
269
+ function bytes(file, limit = 65536) {
270
+ owned(file, false);
271
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW | fs.constants.O_NONBLOCK);
272
+ try {
273
+ const info = fs.fstatSync(fd);
274
+ if (!info.isFile() || info.uid !== uid() || info.size > limit || info.mode & 18)
275
+ throw new Error("Managed installation artifact is invalid");
276
+ const value = fs.readFileSync(fd);
277
+ if (value.length > limit) throw new Error("Managed installation artifact exceeds its bound");
278
+ return value;
279
+ } finally {
280
+ fs.closeSync(fd);
281
+ }
282
+ }
283
+ function writePrivate(file, value) {
284
+ fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", { mode: 384, flag: "wx" });
285
+ }
286
+ function protectedCommand(root, command) {
287
+ const real = fs.realpathSync(command);
288
+ if (!real.startsWith(root + path.sep)) throw new Error("Managed command escapes its bundle");
289
+ for (let current = path.dirname(real); current !== root; current = path.dirname(current)) owned(current, true);
290
+ const info = owned(real, false);
291
+ if (!(info.mode & 73)) throw new Error("Managed command is not executable");
292
+ return real;
293
+ }
294
+ function readManagedManifest(file, options = {}) {
295
+ const base = location(options);
296
+ owned(base, true, true);
297
+ const link = fs.lstatSync(file);
298
+ let target = file;
299
+ if (link.isSymbolicLink()) {
300
+ if (link.uid !== uid()) throw new Error("Managed pointer has a foreign owner");
301
+ target = path.resolve(path.dirname(file), fs.readlinkSync(file));
302
+ }
303
+ if (path.basename(target) !== "toolchain.json" || path.dirname(path.dirname(target)) !== base)
304
+ throw new Error("Managed manifest is outside the private installation");
305
+ const root = path.dirname(target);
306
+ owned(root, true, true);
307
+ if (fs.realpathSync(root) !== root) throw new Error("Managed bundle cannot use linked directories");
308
+ owned(target, false, true);
309
+ const manifest = parseManagedToolchainManifest(JSON.parse(bytes(target).toString("utf8")));
310
+ if (manifest.root !== root || manifest.bundleId !== path.basename(root) || !manifest.capabilities.includes("validation") || !manifest.capabilities.includes("coordination"))
311
+ throw new Error("Managed bundle identity or required capabilities differ");
312
+ for (const component of Object.values(manifest.components)) {
313
+ if (protectedCommand(root, component.command) !== component.command || sha(bytes(component.command, 8e6)) !== component.sha256)
314
+ throw new Error("Managed command identity changed; prepare a replacement bundle");
315
+ }
316
+ return manifest;
317
+ }
318
+ function verifyRetainedCompanionBootstrap(command, active, options = {}) {
319
+ try {
320
+ const current = readManagedManifest(path.join(active.root, "toolchain.json"), options);
321
+ const base = location(options);
322
+ if (!path.isAbsolute(command) || !command.startsWith(base + path.sep)) return false;
323
+ const bundleId = command.slice(base.length + 1).split(path.sep)[0], root = path.join(base, bundleId);
324
+ owned(root, true, true);
325
+ if (fs.realpathSync(root) !== root || protectedCommand(root, command) !== command) return false;
326
+ const manifestFile = path.join(root, "toolchain.json");
327
+ owned(manifestFile, false, true);
328
+ const body = bytes(manifestFile), observedHash = sha(bytes(command, 8e6));
329
+ let previous;
330
+ try {
331
+ previous = parseManagedToolchainManifest(JSON.parse(body.toString("utf8")));
332
+ } catch {
333
+ return command === path.join(root, path.relative(current.root, current.components.companion.command)) && observedHash === current.components.companion.sha256;
334
+ }
335
+ return previous.root === root && previous.bundleId === bundleId && previous.components.companion.command === command && previous.components.companion.sha256 === observedHash;
336
+ } catch {
337
+ return false;
338
+ }
339
+ }
340
+ function managedToolchainStatus(options = {}) {
341
+ const active = path.join(location(options), "active.json");
342
+ if (!exists(active)) return { status: "absent", manifest: null, manifestPath: null };
343
+ return { status: "ready", manifest: readManagedManifest(active, options), manifestPath: fs.realpathSync(active) };
344
+ }
345
+ function program(name) {
346
+ for (const directory of (process.env.PATH ?? "").split(path.delimiter)) {
347
+ if (!directory || !path.isAbsolute(directory)) continue;
348
+ const candidate = path.join(directory, name);
349
+ try {
350
+ fs.accessSync(candidate, fs.constants.X_OK);
351
+ if (fs.statSync(candidate).isFile()) return fs.realpathSync(candidate);
352
+ } catch {
353
+ }
354
+ }
355
+ throw new Error(`${name} is required; no operating-system runtime will be installed`);
356
+ }
357
+ function cleanEnvironment(root) {
358
+ return {
359
+ PATH: [path.dirname(process.execPath), "/usr/local/bin", "/opt/homebrew/bin", "/usr/bin", "/bin"].join(path.delimiter),
360
+ HOME: path.join(root, "home"),
361
+ TMPDIR: path.join(root, "tmp"),
362
+ LANG: "C.UTF-8",
363
+ LC_ALL: "C.UTF-8",
364
+ PYTHONNOUSERSITE: "1",
365
+ PYTHONDONTWRITEBYTECODE: "1",
366
+ PIP_DISABLE_PIP_VERSION_CHECK: "1",
367
+ PIP_NO_INPUT: "1",
368
+ PIP_CONFIG_FILE: "/dev/null",
369
+ NPM_CONFIG_USERCONFIG: "/dev/null",
370
+ NPM_CONFIG_GLOBALCONFIG: "/dev/null",
371
+ NPM_CONFIG_IGNORE_SCRIPTS: "true",
372
+ NPM_CONFIG_AUDIT: "false",
373
+ NPM_CONFIG_FUND: "false",
374
+ NO_COLOR: "1"
375
+ };
376
+ }
377
+ var runManagedInstaller = async (input) => new Promise((resolve2, reject) => {
378
+ const child = spawn(input.command, input.args, {
379
+ cwd: input.cwd,
380
+ env: input.env,
381
+ shell: false,
382
+ stdio: ["ignore", "pipe", "pipe"],
383
+ detached: process.platform !== "win32"
384
+ });
385
+ let output = "", size = 0, stopped = false, stopReason = null;
386
+ let reapTimer;
387
+ const kill = () => {
388
+ try {
389
+ if (child.pid) process.kill(-child.pid, "SIGKILL");
390
+ } catch {
391
+ child.kill("SIGKILL");
392
+ }
393
+ };
394
+ const interrupted = () => stop("Managed preparation interrupted; the prior selection was preserved");
395
+ const exiting = () => kill();
396
+ const cleanup = () => {
397
+ clearTimeout(timer);
398
+ if (reapTimer) clearTimeout(reapTimer);
399
+ process.removeListener("SIGINT", interrupted);
400
+ process.removeListener("SIGTERM", interrupted);
401
+ process.removeListener("exit", exiting);
402
+ };
403
+ const stop = (message) => {
404
+ if (stopped || stopReason) return;
405
+ stopReason = message;
406
+ kill();
407
+ reapTimer = setTimeout(() => {
408
+ if (!stopped) {
409
+ stopped = true;
410
+ cleanup();
411
+ reject(new ManagedInstallerCleanupError("Managed installer cleanup could not be observed within its deadline; installation lock retained"));
412
+ }
413
+ }, 2e3);
414
+ };
415
+ const timer = setTimeout(() => stop("Managed preparation exceeded its bounded deadline"), input.timeoutMs);
416
+ process.on("SIGINT", interrupted);
417
+ process.on("SIGTERM", interrupted);
418
+ process.on("exit", exiting);
419
+ child.stdout.on("data", (value) => {
420
+ size += value.length;
421
+ if (size > 1e6) stop("Managed preparation output exceeded its bound");
422
+ else output += value.toString("utf8");
423
+ });
424
+ child.stderr.on("data", (value) => {
425
+ size += value.length;
426
+ if (size > 1e6) stop("Managed preparation output exceeded its bound");
427
+ });
428
+ child.on("error", () => {
429
+ cleanup();
430
+ if (!stopped) {
431
+ stopped = true;
432
+ reject(new Error("Managed preparation executable was unavailable"));
433
+ }
434
+ });
435
+ child.on("close", (code) => {
436
+ cleanup();
437
+ if (stopped) return;
438
+ stopped = true;
439
+ if (stopReason) reject(new Error(stopReason));
440
+ else if (code !== 0) reject(new Error("Managed preparation step failed; the previous active bundle was preserved"));
441
+ else resolve2({ stdout: output });
442
+ });
443
+ });
444
+ function switchPointer(base, name, target) {
445
+ const current = path.join(base, name);
446
+ if (exists(current)) {
447
+ const stat = fs.lstatSync(current);
448
+ if (!stat.isSymbolicLink() || stat.uid !== uid()) throw new Error("Managed active pointer must be an owned symlink");
449
+ }
450
+ const temporary = path.join(base, `.pointer-${randomUUID2()}`);
451
+ fs.symlinkSync(target, temporary);
452
+ try {
453
+ fs.renameSync(temporary, current);
454
+ } finally {
455
+ if (exists(temporary)) fs.unlinkSync(temporary);
456
+ }
457
+ }
458
+ async function locked(options, action) {
459
+ const base = location(options);
460
+ privateDirectory(base);
461
+ owned(base, true, true);
462
+ const lock = path.join(base, ".install.lock");
463
+ const fd = fs.openSync(lock, "wx", 384);
464
+ let release = true;
465
+ try {
466
+ fs.writeFileSync(fd, JSON.stringify({ pid: process.pid, startedAt: (/* @__PURE__ */ new Date()).toISOString() }));
467
+ return await action(base);
468
+ } catch (error) {
469
+ if (error instanceof ManagedInstallerCleanupError) release = false;
470
+ throw error;
471
+ } finally {
472
+ fs.closeSync(fd);
473
+ if (release) fs.unlinkSync(lock);
474
+ }
475
+ }
476
+ async function prepareManagedToolchain(options = {}) {
477
+ if (Number(process.versions.node.split(".")[0]) < 18)
478
+ throw new Error("Local work requires Node.js 18+; update your system runtime and retry");
479
+ return locked(options, async (base) => {
480
+ const active = path.join(base, "active.json");
481
+ let prior = null;
482
+ let recovery = null;
483
+ const preserveInvalidSelection = () => {
484
+ const pointer = fs.lstatSync(active);
485
+ if (!pointer.isSymbolicLink() || pointer.uid !== uid()) throw new Error("Repair refuses a foreign or non-symlink active pointer");
486
+ const target = path.resolve(base, fs.readlinkSync(active));
487
+ if (path.basename(target) !== "toolchain.json" || path.dirname(path.dirname(target)) !== base)
488
+ throw new Error("Repair refuses an active pointer outside the owned installation");
489
+ const root2 = path.dirname(target);
490
+ owned(root2, true, true);
491
+ if (fs.realpathSync(root2) !== root2) throw new Error("Repair refuses a linked bundle directory");
492
+ let previousHash = null;
493
+ if (exists(target)) {
494
+ owned(target, false, true);
495
+ previousHash = sha(bytes(target));
496
+ }
497
+ return { priorManifestPath: target, priorManifestSha256: previousHash, priorSelection: "invalid_owned_bundle" };
498
+ };
499
+ if (exists(active)) {
500
+ try {
501
+ prior = readManagedManifest(active, options);
502
+ } catch (error) {
503
+ if (!options.repair) throw error;
504
+ recovery = preserveInvalidSelection();
505
+ prior = null;
506
+ }
507
+ }
508
+ const runner = options.runner ?? runManagedInstaller;
509
+ async function handshake(manifest) {
510
+ for (const [name, component] of Object.entries(manifest.components)) {
511
+ const command = name === "companion" ? process.execPath : component.command;
512
+ const args = name === "companion" ? [component.command, "--version"] : ["--version"];
513
+ const observed = await runner({ command, args, cwd: manifest.root, env: cleanEnvironment(manifest.root), timeoutMs: 15e3 });
514
+ if (!new RegExp(`^(?:snipara-${name}[ ,]+)?${component.version.replaceAll(".", "\\.")}$`, "i").test(observed.stdout.trim()))
515
+ throw new Error("Managed component version handshake did not match the pinned bundle");
516
+ }
517
+ }
518
+ const selectedPrior = prior;
519
+ if (selectedPrior && Object.entries(MANAGED_VERSIONS).every(([name, version]) => selectedPrior.components[name].version === version)) {
520
+ try {
521
+ await handshake(selectedPrior);
522
+ readManagedManifest(active, options);
523
+ return selectedPrior;
524
+ } catch (error) {
525
+ if (!options.repair) throw error;
526
+ recovery = preserveInvalidSelection();
527
+ prior = null;
528
+ }
529
+ }
530
+ const bundleId = `bundle-${MANAGED_VERSIONS.companion}-${MANAGED_VERSIONS.orchestrator}-${MANAGED_VERSIONS.sandbox}-${randomUUID2()}`;
531
+ const root = path.join(base, bundleId);
532
+ fs.mkdirSync(root, { mode: 448 });
533
+ for (const directory of ["home", "tmp"]) fs.mkdirSync(path.join(root, directory), { mode: 448 });
534
+ const env = cleanEnvironment(root);
535
+ const run = (command, args, timeoutMs = 3e5) => runner({ command, args, cwd: root, env, timeoutMs });
536
+ try {
537
+ if (recovery) writePrivate(path.join(root, "recovery.json"), { schemaVersion: "snipara.toolchain_recovery.v1", ...recovery, originalBundlePreserved: true });
538
+ const python = options.python ?? program("python3"), npm = options.npm ?? program("npm");
539
+ if (![python, npm].every((value) => path.isAbsolute(value))) throw new Error("Managed package managers need absolute executable paths");
540
+ const runtime = await run(python, ["--version"], 1e4);
541
+ assertManagedRuntimeVersions(process.versions.node, runtime.stdout);
542
+ const venv = path.join(root, "python"), prefix = path.join(root, "node");
543
+ await run(python, ["-I", "-m", "venv", venv], 6e4);
544
+ await run(path.join(venv, "bin/python"), ["-I", "-m", "pip", "install", "--no-input", "--disable-pip-version-check", "snipara-sandbox[validation,mcp]==" + MANAGED_VERSIONS.sandbox, "snipara-orchestrator==" + MANAGED_VERSIONS.orchestrator]);
545
+ await run(npm, ["install", "--prefix", prefix, "--ignore-scripts", "--no-audit", "--no-fund", "--save-exact", "snipara-companion@" + MANAGED_VERSIONS.companion]);
546
+ const manifest = {
547
+ schemaVersion: "snipara.toolchain.v1",
548
+ bundleId,
549
+ root,
550
+ components: {
551
+ companion: { version: MANAGED_VERSIONS.companion, command: fs.realpathSync(path.join(prefix, "node_modules/.bin/snipara-companion")), sha256: "" },
552
+ orchestrator: { version: MANAGED_VERSIONS.orchestrator, command: path.join(venv, "bin/snipara-orchestrator"), sha256: "" },
553
+ sandbox: { version: MANAGED_VERSIONS.sandbox, command: path.join(venv, "bin/snipara-sandbox"), sha256: "" }
554
+ },
555
+ contracts: { taskReference: 1, validation: 1 },
556
+ capabilities: ["validation", "coordination"]
557
+ };
558
+ for (const component of Object.values(manifest.components)) {
559
+ component.command = protectedCommand(root, component.command);
560
+ component.sha256 = sha(bytes(component.command, 8e6));
561
+ }
562
+ const manifestFile = path.join(root, "toolchain.json");
563
+ writePrivate(manifestFile, parseManagedToolchainManifest(manifest));
564
+ await handshake(manifest);
565
+ readManagedManifest(manifestFile, options);
566
+ writePrivate(path.join(root, "prepared.json"), { schemaVersion: "snipara.toolchain_preparation.v1", status: "prepared", manifestSha256: sha(bytes(manifestFile)), observedAt: (/* @__PURE__ */ new Date()).toISOString(), activationObserved: false });
567
+ if (prior) switchPointer(base, "previous.json", path.join(prior.root, "toolchain.json"));
568
+ switchPointer(base, "active.json", manifestFile);
569
+ return manifest;
570
+ } catch (error) {
571
+ writePrivate(path.join(root, "failed.json"), { schemaVersion: "snipara.toolchain_preparation.v1", status: "failed", observedAt: (/* @__PURE__ */ new Date()).toISOString(), reason: "preparation_or_identity_check_failed", priorActivePreserved: true });
572
+ throw error;
573
+ }
574
+ });
575
+ }
576
+ async function rollbackManagedToolchain(options = {}) {
577
+ return locked(options, async (base) => {
578
+ const previous = readManagedManifest(path.join(base, "previous.json"), options);
579
+ let active = null;
580
+ try {
581
+ active = readManagedManifest(path.join(base, "active.json"), options);
582
+ } catch {
583
+ }
584
+ switchPointer(base, "active.json", path.join(previous.root, "toolchain.json"));
585
+ if (active) switchPointer(base, "previous.json", path.join(active.root, "toolchain.json"));
586
+ return previous;
587
+ });
588
+ }
589
+
590
+ // src/managed-config.ts
591
+ import fs2 from "fs";
592
+ import path2 from "path";
593
+ import { execFileSync } from "child_process";
594
+ import { TextDecoder } from "util";
595
+ function mergeManagedMcpServers(existing, native, hosted, sourceBound = false) {
596
+ return { ...existing, ...sourceBound || existing["snipara-repository"] ? {} : { snipara: hosted }, "snipara-local": native };
597
+ }
598
+ var PARSE = "import json,sys\ntry:\n import tomllib\nexcept ImportError:\n import tomli as tomllib\nvalue=tomllib.loads(sys.stdin.read())\nservers=value.get('mcp_servers',{})\nif not isinstance(servers,dict): raise ValueError('invalid MCP table')\nprint(json.dumps(servers.get('snipara-local'),ensure_ascii=True))";
599
+ function parse(content, options) {
600
+ try {
601
+ return JSON.parse(execFileSync(options.python, ["-I", "-B", "-c", PARSE], {
602
+ input: content,
603
+ env: options.env,
604
+ encoding: "utf8",
605
+ timeout: 5e3,
606
+ maxBuffer: 1e6,
607
+ stdio: ["pipe", "pipe", "pipe"]
608
+ }));
609
+ } catch {
610
+ throw new Error("Existing Codex configuration is malformed or its parser is unavailable; file preserved");
611
+ }
612
+ }
613
+ function object2(value) {
614
+ return value !== null && typeof value === "object" && !Array.isArray(value);
615
+ }
616
+ function managedCodexSnippet(server) {
617
+ const env = server.env && Object.keys(server.env).length ? `
618
+ env = { ${Object.entries(server.env).map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`).join(", ")} }` : "";
619
+ return `[mcp_servers.snipara-local]
620
+ command = ${JSON.stringify(server.command)}
621
+ args = ${JSON.stringify(server.args)}${env}
622
+ `;
623
+ }
624
+ function mergeManagedCodexConfig(options) {
625
+ const directory = path2.dirname(options.file);
626
+ if (!fs2.existsSync(directory)) fs2.mkdirSync(directory, { mode: 448 });
627
+ const parent = fs2.lstatSync(directory);
628
+ if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== process.getuid?.() || parent.mode & 18)
629
+ throw new Error("Codex configuration directory is not safely owned; preserved");
630
+ let previous = null;
631
+ try {
632
+ const fd2 = fs2.openSync(options.file, fs2.constants.O_RDONLY | fs2.constants.O_NOFOLLOW | fs2.constants.O_NONBLOCK);
633
+ try {
634
+ const info = fs2.fstatSync(fd2);
635
+ if (!info.isFile() || info.uid !== process.getuid?.() || info.nlink !== 1 || info.size > 1e6 || info.mode & 18)
636
+ throw new Error("Existing Codex configuration is not safely owned; preserved");
637
+ previous = fs2.readFileSync(fd2);
638
+ if (previous.length > 1e6) throw new Error("Existing Codex configuration exceeds its bound; preserved");
639
+ } finally {
640
+ fs2.closeSync(fd2);
641
+ }
642
+ } catch (error) {
643
+ if (error.code !== "ENOENT") throw error;
644
+ }
645
+ const content = previous ? new TextDecoder("utf8", { fatal: true }).decode(previous) : options.initialContent ?? "";
646
+ const selected = parse(content, options);
647
+ if (selected !== null) {
648
+ if (!object2(selected) || Object.keys(selected).some((key) => !["command", "args", "env"].includes(key)) || typeof selected.command !== "string" || !Array.isArray(selected.args) || !selected.args.every((value) => typeof value === "string") || selected.env !== void 0 && (!object2(selected.env) || !Object.values(selected.env).every((value) => typeof value === "string")) || !options.validateExisting(selected))
649
+ throw new Error("Existing managed Codex connection conflicts with setup; file preserved and activation incomplete");
650
+ return "unchanged";
651
+ }
652
+ const result = content + (content ? content.endsWith("\n") ? "\n" : "\n\n" : "") + managedCodexSnippet(options.server);
653
+ parse(result, options);
654
+ if (previous && !fs2.readFileSync(options.file).equals(previous))
655
+ throw new Error("Codex configuration changed during setup; file preserved");
656
+ if (!previous) {
657
+ fs2.writeFileSync(options.file, result, { flag: "wx", mode: 384 });
658
+ return "created";
659
+ }
660
+ const fd = fs2.openSync(options.file, fs2.constants.O_WRONLY | fs2.constants.O_APPEND | fs2.constants.O_NOFOLLOW);
661
+ try {
662
+ const current = fs2.fstatSync(fd), named = fs2.lstatSync(options.file);
663
+ if (current.ino !== named.ino || current.dev !== named.dev || current.size !== previous.length || !current.isFile())
664
+ throw new Error("Codex configuration changed during setup; file preserved");
665
+ fs2.writeFileSync(fd, result.slice(content.length), "utf8");
666
+ } finally {
667
+ fs2.closeSync(fd);
668
+ }
669
+ return "appended";
670
+ }
671
+
672
+ // src/source-connection.ts
673
+ import fs3 from "fs";
674
+ import path3 from "path";
675
+ import { execFileSync as execFileSync2 } from "child_process";
676
+ import { TextDecoder as TextDecoder2 } from "util";
677
+
678
+ // src/source-binding.mjs
679
+ import { createHash as createHash6 } from "crypto";
680
+ import {
681
+ existsSync,
682
+ lstatSync,
683
+ mkdirSync,
684
+ readFileSync,
685
+ realpathSync,
686
+ writeFileSync
687
+ } from "fs";
688
+ import { spawnSync } from "child_process";
689
+ import { dirname, isAbsolute, join, resolve } from "path";
690
+ var fingerprint = (value) => createHash6("sha256").update(value).digest("hex");
691
+ var SourceBindingError = class extends Error {
692
+ constructor(code, message) {
693
+ super(message);
694
+ this.name = "SourceBindingError";
695
+ this.code = code;
696
+ }
697
+ };
698
+ function fail(code, message) {
699
+ throw new SourceBindingError(code, message);
700
+ }
701
+ function normalizeRepositoryRemote(remote) {
702
+ if (typeof remote !== "string" || !remote || remote.length > 2048 || /[\\%\s\x00-\x1f\x7f]/.test(remote) || /\/\.{1,2}(?:\/|$)/.test(remote))
703
+ fail("unsafe_remote", "Repository remote is unsupported or contains unsafe credentials.");
704
+ let hostname, port = "", repositoryPath;
705
+ try {
706
+ if (/^[a-z][a-z0-9+.-]*:\/\//i.test(remote)) {
707
+ const url = new URL(remote);
708
+ if (!["https:", "http:", "ssh:"].includes(url.protocol) || url.password || url.search || url.hash || url.protocol !== "ssh:" && url.username) throw new Error();
709
+ if (url.username && !/^[A-Za-z0-9_.-]+$/.test(url.username)) throw new Error();
710
+ hostname = url.hostname.toLowerCase();
711
+ port = url.port && !(url.protocol === "ssh:" && url.port === "22") ? `:${url.port}` : "";
712
+ repositoryPath = url.pathname;
713
+ } else {
714
+ const match = remote.match(/^(?:([A-Za-z0-9_.-]+)@)?([A-Za-z0-9.-]+):([^:]+)$/);
715
+ if (!match) throw new Error();
716
+ hostname = match[2].toLowerCase();
717
+ repositoryPath = match[3];
718
+ }
719
+ repositoryPath = repositoryPath.replace(/^\/+|\/+$/g, "").replace(/\.git$/i, "");
720
+ if (!hostname || !/^[A-Za-z0-9.-]+$/.test(hostname) || !repositoryPath || !/^[A-Za-z0-9_./~-]+$/.test(repositoryPath) || repositoryPath.split("/").some((part) => !part || part === "." || part === "..")) throw new Error();
721
+ } catch {
722
+ fail("unsafe_remote", "Repository remote is unsupported or contains unsafe credentials.");
723
+ }
724
+ if (["github.com", "gitlab.com", "bitbucket.org"].includes(hostname)) repositoryPath = repositoryPath.toLowerCase();
725
+ return `${hostname}${port}/${repositoryPath}`;
726
+ }
727
+ function git(cwd, args) {
728
+ const names = ["PATH", "HOME", "USERPROFILE", "SystemRoot", "WINDIR", "TMPDIR", "TMP", "TEMP"];
729
+ const env = Object.fromEntries(names.filter((key) => typeof process.env[key] === "string").map((key) => [key, process.env[key]]));
730
+ const result = spawnSync("git", ["--no-optional-locks", "-C", cwd, ...args], {
731
+ encoding: "utf8",
732
+ timeout: 5e3,
733
+ maxBuffer: 16384,
734
+ shell: false,
735
+ env: { ...env, LC_ALL: "C", GIT_TERMINAL_PROMPT: "0" }
736
+ });
737
+ if (result.error || result.signal) fail("source_unavailable", "Cannot verify the current repository. Reconnect before using Snipara.");
738
+ return result;
739
+ }
740
+ function observeSource(workspacePath) {
741
+ if (typeof workspacePath !== "string" || !workspacePath || !isAbsolute(workspacePath))
742
+ fail("source_unavailable", "Choose an explicit absolute workspace folder.");
743
+ let folder;
744
+ try {
745
+ folder = realpathSync(workspacePath);
746
+ if (!lstatSync(folder).isDirectory()) throw new Error();
747
+ } catch {
748
+ fail("source_unavailable", "Workspace folder is unavailable.");
749
+ }
750
+ const rootResult = git(folder, ["rev-parse", "--show-toplevel"]);
751
+ if (rootResult.status !== 0) {
752
+ if (rootResult.status !== 128 || !rootResult.stderr.includes("not a git repository"))
753
+ fail("source_unavailable", "Cannot verify the current repository. Reconnect before using Snipara.");
754
+ return { root: folder, kind: "folder", fingerprint: fingerprint(`folder:${folder}`) };
755
+ }
756
+ let root;
757
+ try {
758
+ root = realpathSync(rootResult.stdout.trim());
759
+ } catch {
760
+ fail("source_unavailable", "Repository root is unavailable.");
761
+ }
762
+ const remote = git(root, ["config", "--includes", "--get", "remote.origin.url"]);
763
+ if (remote.status === 0) {
764
+ const repository = normalizeRepositoryRemote(remote.stdout.replace(/\r?\n$/, ""));
765
+ return { root, kind: "repository", fingerprint: fingerprint(`repository:${repository}`), repository };
766
+ }
767
+ if (remote.status !== 1) fail("source_unavailable", "Cannot verify the repository remote.");
768
+ const commonResult = git(root, ["rev-parse", "--git-common-dir"]);
769
+ if (commonResult.status !== 0) fail("source_unavailable", "Cannot verify the local repository identity.");
770
+ let common;
771
+ try {
772
+ common = realpathSync(resolve(root, commonResult.stdout.trim()));
773
+ } catch {
774
+ fail("source_unavailable", "Cannot verify the local repository identity.");
775
+ }
776
+ return { root, kind: "local-repository", fingerprint: fingerprint(`local-repository:${common}`) };
777
+ }
778
+
779
+ // src/source-connection.ts
780
+ var object3 = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
781
+ function boundedConfiguration(file) {
782
+ try {
783
+ const parent = fs3.lstatSync(path3.dirname(file));
784
+ if (!parent.isDirectory() || parent.isSymbolicLink() || parent.uid !== process.getuid?.())
785
+ throw new Error("Native configuration directory is not safely owned; preserve and repair it first");
786
+ } catch (error) {
787
+ if (error.code === "ENOENT") return null;
788
+ throw error;
789
+ }
790
+ let fd;
791
+ try {
792
+ fd = fs3.openSync(file, fs3.constants.O_RDONLY | fs3.constants.O_NOFOLLOW | fs3.constants.O_NONBLOCK);
793
+ } catch (error) {
794
+ if (error.code === "ENOENT") return null;
795
+ throw new Error("Existing native configuration is not a readable regular file; preserve and repair it first");
796
+ }
797
+ try {
798
+ const info = fs3.fstatSync(fd);
799
+ if (!info.isFile() || info.uid !== process.getuid?.() || info.size > 1e6 || info.mode & 18)
800
+ throw new Error("Existing native configuration is not safely owned; preserve and repair it first");
801
+ const value = fs3.readFileSync(fd);
802
+ if (value.length > 1e6) throw new Error("Native configuration exceeds its read bound");
803
+ return new TextDecoder2("utf8", { fatal: true }).decode(value);
804
+ } finally {
805
+ fs3.closeSync(fd);
806
+ }
807
+ }
808
+ function operatorPython() {
809
+ for (const directory of (process.env.PATH ?? "").split(path3.delimiter)) {
810
+ if (!directory || !path3.isAbsolute(directory)) continue;
811
+ const candidate = path3.join(directory, "python3");
812
+ try {
813
+ fs3.accessSync(candidate, fs3.constants.X_OK);
814
+ if (fs3.statSync(candidate).isFile()) return fs3.realpathSync(candidate);
815
+ } catch {
816
+ }
817
+ }
818
+ throw new Error("Reading existing Codex configuration requires Python 3.11+, or Python 3.10 with tomli; no configuration was changed");
819
+ }
820
+ function sourceServer(value, observed) {
821
+ if (!object3(value) || Object.keys(value).some((key) => !["command", "args"].includes(key)) || typeof value.command !== "string" || !path3.isAbsolute(value.command) || !Array.isArray(value.args) || value.args.length !== 5 || !value.args.every((item) => typeof item === "string") || value.args[1] !== "--source-fingerprint" || value.args[3] !== "--source-kind" || value.args[2] !== observed.fingerprint || value.args[4] !== observed.kind || !path3.isAbsolute(value.args[0]) || path3.basename(value.args[0]) !== "native-mcp.mjs")
822
+ throw new Error("Existing Snipara repository connection differs from this workspace; reconnect it explicitly before local-work setup");
823
+ try {
824
+ const script = value.args[0], info = fs3.lstatSync(script);
825
+ if (!info.isFile() || info.isSymbolicLink() || info.uid !== process.getuid?.() || info.mode & 18 || fs3.realpathSync(script) !== script || info.size > 256e3 || !fs3.statSync(value.command).isFile()) throw new Error();
826
+ } catch {
827
+ throw new Error("Existing Snipara repository launcher is unavailable or unsafe; preserve and repair its connection");
828
+ }
829
+ }
830
+ function detectSourceConnection(workspace, options = {}) {
831
+ const observed = observeSource(fs3.realpathSync(workspace));
832
+ const servers = [];
833
+ const json = boundedConfiguration(path3.join(observed.root, ".mcp.json"));
834
+ if (json !== null) {
835
+ let document;
836
+ try {
837
+ document = JSON.parse(json);
838
+ } catch {
839
+ throw new Error("Existing MCP JSON is malformed; preserve and repair it before setup");
840
+ }
841
+ if (!object3(document) || document.mcpServers !== void 0 && !object3(document.mcpServers))
842
+ throw new Error("Existing MCP server configuration is invalid; preserve and repair it before setup");
843
+ servers.push(document.mcpServers ?? {});
844
+ }
845
+ const toml = boundedConfiguration(path3.join(observed.root, ".codex/config.toml"));
846
+ if (toml !== null) {
847
+ const script = "import json,sys\ntry:\n import tomllib\nexcept ImportError:\n import tomli as tomllib\nvalue=tomllib.loads(sys.stdin.read()).get('mcp_servers',{})\nif not isinstance(value,dict): raise ValueError('invalid servers')\nprint(json.dumps({key:value[key] for key in ['snipara-repository','snipara','snipara-plugin'] if key in value},ensure_ascii=True))";
848
+ try {
849
+ servers.push(JSON.parse(execFileSync2(options.python ?? operatorPython(), ["-I", "-B", "-c", script], {
850
+ input: toml,
851
+ encoding: "utf8",
852
+ timeout: 5e3,
853
+ maxBuffer: 1e6,
854
+ stdio: ["pipe", "pipe", "pipe"],
855
+ env: { PATH: process.env.PATH, HOME: process.env.HOME, PYTHONNOUSERSITE: "1" }
856
+ })));
857
+ } catch {
858
+ throw new Error("Existing Codex configuration could not be parsed. Use Python 3.11+, or Python 3.10 with tomli, and repair malformed TOML before retrying; configuration was preserved");
859
+ }
860
+ }
861
+ const selected = servers.filter((entries) => entries["snipara-repository"] !== void 0);
862
+ if (!selected.length) return null;
863
+ if (servers.some((entries) => entries.snipara !== void 0 || entries["snipara-plugin"] !== void 0))
864
+ throw new Error("An existing direct Snipara connection conflicts with the repository connection; preserve and review it before setup");
865
+ for (const entries of selected) sourceServer(entries["snipara-repository"], observed);
866
+ return observed;
867
+ }
868
+
93
869
  // src/index.ts
94
870
  var VERSION = package_default.version;
95
871
  var BASE_URL = "https://www.snipara.com";
@@ -98,7 +874,7 @@ var COMPANION_PACKAGE_NAME = "snipara-companion";
98
874
  var COMPANION_COMMAND_NAME = "snipara-companion";
99
875
  var COMPANION_NPX_COMMAND = "npx -y snipara-companion@latest";
100
876
  var CREATE_SNIPARA_NPX_COMMAND = "npx -y create-snipara@latest";
101
- var MIN_COMPANION_VERSION = "3.0.0";
877
+ var MIN_COMPANION_VERSION = "3.8.0";
102
878
  var GITHUB_ANSWER_PACKS_LABEL = "GitHub PR Answer Packs";
103
879
  var MCP_PACKAGE_NAME = "snipara-mcp";
104
880
  var SANDBOX_PACKAGE_NAME = "snipara-sandbox[all]";
@@ -139,37 +915,37 @@ var DECISION_COMMIT_TERMS = [
139
915
  "switch"
140
916
  ];
141
917
  function buildActivationId() {
142
- return `act_${Date.now().toString(36)}_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
918
+ return `act_${Date.now().toString(36)}_${randomUUID3().replace(/-/g, "").slice(0, 12)}`;
143
919
  }
144
920
  function getActivationDir() {
145
- return path.join(process.cwd(), ".snipara", "activation");
921
+ return path4.join(process.cwd(), ".snipara", "activation");
146
922
  }
147
923
  function getActivationManifestPath() {
148
- return path.join(getActivationDir(), "manifest.json");
924
+ return path4.join(getActivationDir(), "manifest.json");
149
925
  }
150
926
  function getStableActivationManifestPath() {
151
- return path.join(getActivationDir(), "activation-manifest.json");
927
+ return path4.join(getActivationDir(), "activation-manifest.json");
152
928
  }
153
929
  function getActivationHandoffPath() {
154
- return path.join(getActivationDir(), "handoff.md");
930
+ return path4.join(getActivationDir(), "handoff.md");
155
931
  }
156
932
  function getFirstWorkBriefPath() {
157
- return path.join(getActivationDir(), "first-brief.md");
933
+ return path4.join(getActivationDir(), "first-brief.md");
158
934
  }
159
935
  function getMemoryCandidatesPath() {
160
- return path.join(getActivationDir(), "memory-candidates.json");
936
+ return path4.join(getActivationDir(), "memory-candidates.json");
161
937
  }
162
938
  function getProjectBootstrapJsonPath() {
163
- return path.join(getActivationDir(), "project-bootstrap.json");
939
+ return path4.join(getActivationDir(), "project-bootstrap.json");
164
940
  }
165
941
  function getProjectBootstrapMarkdownPath() {
166
- return path.join(getActivationDir(), "project-bootstrap.md");
942
+ return path4.join(getActivationDir(), "project-bootstrap.md");
167
943
  }
168
944
  function writeActivationManifest(manifest) {
169
- fs.mkdirSync(getActivationDir(), { recursive: true });
945
+ fs4.mkdirSync(getActivationDir(), { recursive: true });
170
946
  const payload = JSON.stringify(manifest, null, 2) + "\n";
171
- fs.writeFileSync(getActivationManifestPath(), payload, "utf8");
172
- fs.writeFileSync(getStableActivationManifestPath(), payload, "utf8");
947
+ fs4.writeFileSync(getActivationManifestPath(), payload, "utf8");
948
+ fs4.writeFileSync(getStableActivationManifestPath(), payload, "utf8");
173
949
  }
174
950
  function updateActivationManifest(manifest) {
175
951
  manifest.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -188,12 +964,12 @@ function failActivationLane(detail, command, error) {
188
964
  };
189
965
  }
190
966
  function readPackageJsonScripts() {
191
- const packageJsonPath = path.join(process.cwd(), "package.json");
192
- if (!fs.existsSync(packageJsonPath)) {
967
+ const packageJsonPath = path4.join(process.cwd(), "package.json");
968
+ if (!fs4.existsSync(packageJsonPath)) {
193
969
  return [];
194
970
  }
195
971
  try {
196
- const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
972
+ const parsed = JSON.parse(fs4.readFileSync(packageJsonPath, "utf8"));
197
973
  return Object.keys(parsed.scripts ?? {}).sort();
198
974
  } catch {
199
975
  return [];
@@ -254,11 +1030,11 @@ function readDecisionCommitSubjectLines() {
254
1030
  }
255
1031
  }
256
1032
  function readMemoryCandidateLines(relativePath) {
257
- const absolutePath = path.join(process.cwd(), relativePath);
258
- if (!fs.existsSync(absolutePath) || fs.statSync(absolutePath).isDirectory()) {
1033
+ const absolutePath = path4.join(process.cwd(), relativePath);
1034
+ if (!fs4.existsSync(absolutePath) || fs4.statSync(absolutePath).isDirectory()) {
259
1035
  return [];
260
1036
  }
261
- const content = fs.readFileSync(absolutePath, "utf8").slice(0, 24e3);
1037
+ const content = fs4.readFileSync(absolutePath, "utf8").slice(0, 24e3);
262
1038
  return content.split(/\r?\n/).map((line) => line.trim()).filter((line) => /decision|decided|must|should|always|never|convention|rule/i.test(line)).slice(0, 4).map((line) => ({
263
1039
  source: relativePath,
264
1040
  text: line.slice(0, 280),
@@ -268,12 +1044,12 @@ function readMemoryCandidateLines(relativePath) {
268
1044
  }));
269
1045
  }
270
1046
  function estimateFileTokens(relativePath) {
271
- const absolutePath = path.join(process.cwd(), relativePath);
272
- if (!fs.existsSync(absolutePath) || fs.statSync(absolutePath).isDirectory()) {
1047
+ const absolutePath = path4.join(process.cwd(), relativePath);
1048
+ if (!fs4.existsSync(absolutePath) || fs4.statSync(absolutePath).isDirectory()) {
273
1049
  return 0;
274
1050
  }
275
1051
  try {
276
- const content = fs.readFileSync(absolutePath, "utf8");
1052
+ const content = fs4.readFileSync(absolutePath, "utf8");
277
1053
  return Math.ceil(content.length / 4);
278
1054
  } catch {
279
1055
  return 0;
@@ -302,12 +1078,12 @@ function buildMemoryCandidatePreviews(scan) {
302
1078
  }).slice(0, 20);
303
1079
  }
304
1080
  function readTextFileSnippet(relativePath, maxChars = 24e3) {
305
- const absolutePath = path.join(process.cwd(), relativePath);
306
- if (!fs.existsSync(absolutePath) || fs.statSync(absolutePath).isDirectory()) {
1081
+ const absolutePath = path4.join(process.cwd(), relativePath);
1082
+ if (!fs4.existsSync(absolutePath) || fs4.statSync(absolutePath).isDirectory()) {
307
1083
  return "";
308
1084
  }
309
1085
  try {
310
- return fs.readFileSync(absolutePath, "utf8").slice(0, maxChars);
1086
+ return fs4.readFileSync(absolutePath, "utf8").slice(0, maxChars);
311
1087
  } catch {
312
1088
  return "";
313
1089
  }
@@ -316,7 +1092,7 @@ function collectDayZeroCorpusDocuments(scan) {
316
1092
  const documents = [];
317
1093
  let totalChars = 0;
318
1094
  const cwd = process.cwd();
319
- const rootReadmes = fs.existsSync(cwd) ? fs.readdirSync(cwd).filter((entry) => /^readme(?:\.[a-z0-9_-]+)?\.mdx?$/i.test(entry)).sort() : [];
1095
+ const rootReadmes = fs4.existsSync(cwd) ? fs4.readdirSync(cwd).filter((entry) => /^readme(?:\.[a-z0-9_-]+)?\.mdx?$/i.test(entry)).sort() : [];
320
1096
  const sources = [
321
1097
  ...rootReadmes,
322
1098
  ...listFilesUnder("docs", DAY_ZERO_CORPUS_MAX_FILES * 8),
@@ -429,15 +1205,15 @@ function buildAgentReadinessReport(scan) {
429
1205
  };
430
1206
  }
431
1207
  function writeMemoryCandidates(candidates) {
432
- fs.mkdirSync(getActivationDir(), { recursive: true });
433
- fs.writeFileSync(
1208
+ fs4.mkdirSync(getActivationDir(), { recursive: true });
1209
+ fs4.writeFileSync(
434
1210
  getMemoryCandidatesPath(),
435
1211
  JSON.stringify({ candidates, note: "Review required before durable memory import." }, null, 2) + "\n",
436
1212
  "utf8"
437
1213
  );
438
1214
  }
439
1215
  function writeInitialHandoff(answers, manifest) {
440
- fs.mkdirSync(getActivationDir(), { recursive: true });
1216
+ fs4.mkdirSync(getActivationDir(), { recursive: true });
441
1217
  const statusLines = readGitStatusPorcelain();
442
1218
  const recentCommits = readRecentCommitSubjects();
443
1219
  const lines = [
@@ -470,10 +1246,10 @@ function writeInitialHandoff(answers, manifest) {
470
1246
  "- Connect GitHub later for PRs, reviews, team sync, and continuous sync.",
471
1247
  ""
472
1248
  ];
473
- fs.writeFileSync(getActivationHandoffPath(), lines.join("\n"), "utf8");
1249
+ fs4.writeFileSync(getActivationHandoffPath(), lines.join("\n"), "utf8");
474
1250
  }
475
1251
  function writeFirstWorkBrief(manifest) {
476
- fs.mkdirSync(getActivationDir(), { recursive: true });
1252
+ fs4.mkdirSync(getActivationDir(), { recursive: true });
477
1253
  const lines = [
478
1254
  `# ${manifest.firstBrief.title}`,
479
1255
  "",
@@ -488,40 +1264,40 @@ function writeFirstWorkBrief(manifest) {
488
1264
  ""
489
1265
  ])
490
1266
  ];
491
- fs.writeFileSync(getFirstWorkBriefPath(), lines.join("\n"), "utf8");
1267
+ fs4.writeFileSync(getFirstWorkBriefPath(), lines.join("\n"), "utf8");
492
1268
  }
493
1269
  function detectLocalActivationScan(detection) {
494
1270
  const cwd = process.cwd();
495
- const exists = (relativePath) => fs.existsSync(path.join(cwd, relativePath));
1271
+ const exists2 = (relativePath) => fs4.existsSync(path4.join(cwd, relativePath));
496
1272
  const candidateDocs = findDecisionArchaeologySources(cwd);
497
1273
  const lockfiles = ["pnpm-lock.yaml", "package-lock.json", "yarn.lock", "bun.lockb"].filter(
498
- exists
1274
+ exists2
499
1275
  );
500
1276
  const packageManagers = [
501
- exists("pnpm-lock.yaml") ? "pnpm" : null,
502
- exists("package-lock.json") ? "npm" : null,
503
- exists("yarn.lock") ? "yarn" : null,
504
- exists("bun.lockb") ? "bun" : null
1277
+ exists2("pnpm-lock.yaml") ? "pnpm" : null,
1278
+ exists2("package-lock.json") ? "npm" : null,
1279
+ exists2("yarn.lock") ? "yarn" : null,
1280
+ exists2("bun.lockb") ? "bun" : null
505
1281
  ].filter((value) => Boolean(value));
506
- const rootEntries = fs.existsSync(cwd) ? fs.readdirSync(cwd).slice(0, 250) : [];
1282
+ const rootEntries = fs4.existsSync(cwd) ? fs4.readdirSync(cwd).slice(0, 250) : [];
507
1283
  const rootExtensions = new Set(
508
- rootEntries.map((entry) => path.extname(entry).replace(/^\./, "")).filter(Boolean)
1284
+ rootEntries.map((entry) => path4.extname(entry).replace(/^\./, "")).filter(Boolean)
509
1285
  );
510
1286
  const languages = [
511
- exists("package.json") || rootExtensions.has("ts") || rootExtensions.has("tsx") ? "typescript" : null,
512
- exists("package.json") || rootExtensions.has("js") || rootExtensions.has("jsx") ? "javascript" : null,
513
- exists("pyproject.toml") || exists("requirements.txt") || rootExtensions.has("py") ? "python" : null,
514
- exists("go.mod") || rootExtensions.has("go") ? "go" : null,
515
- exists("Cargo.toml") || rootExtensions.has("rs") ? "rust" : null
1287
+ exists2("package.json") || rootExtensions.has("ts") || rootExtensions.has("tsx") ? "typescript" : null,
1288
+ exists2("package.json") || rootExtensions.has("js") || rootExtensions.has("jsx") ? "javascript" : null,
1289
+ exists2("pyproject.toml") || exists2("requirements.txt") || rootExtensions.has("py") ? "python" : null,
1290
+ exists2("go.mod") || rootExtensions.has("go") ? "go" : null,
1291
+ exists2("Cargo.toml") || rootExtensions.has("rs") ? "rust" : null
516
1292
  ].filter((value) => Boolean(value));
517
1293
  return {
518
1294
  files: {
519
1295
  candidateDocs,
520
1296
  lockfiles,
521
- hasPackageJson: exists("package.json"),
522
- hasPyproject: exists("pyproject.toml"),
523
- hasRequirements: exists("requirements.txt"),
524
- hasDockerfile: exists("Dockerfile")
1297
+ hasPackageJson: exists2("package.json"),
1298
+ hasPyproject: exists2("pyproject.toml"),
1299
+ hasRequirements: exists2("requirements.txt"),
1300
+ hasDockerfile: exists2("Dockerfile")
525
1301
  },
526
1302
  packageManagers,
527
1303
  languages: [...new Set(languages)],
@@ -547,8 +1323,8 @@ function classifyDecisionSource(relativePath) {
547
1323
  return "project_document";
548
1324
  }
549
1325
  function listFilesUnder(relativeDir, maxFiles) {
550
- const root = path.join(process.cwd(), relativeDir);
551
- if (!fs.existsSync(root) || !fs.statSync(root).isDirectory()) {
1326
+ const root = path4.join(process.cwd(), relativeDir);
1327
+ if (!fs4.existsSync(root) || !fs4.statSync(root).isDirectory()) {
552
1328
  return [];
553
1329
  }
554
1330
  const results = [];
@@ -556,13 +1332,13 @@ function listFilesUnder(relativeDir, maxFiles) {
556
1332
  if (results.length >= maxFiles) {
557
1333
  return;
558
1334
  }
559
- for (const entry of fs.readdirSync(dir).sort()) {
560
- const absolutePath = path.join(dir, entry);
561
- const stat = fs.statSync(absolutePath);
1335
+ for (const entry of fs4.readdirSync(dir).sort()) {
1336
+ const absolutePath = path4.join(dir, entry);
1337
+ const stat = fs4.statSync(absolutePath);
562
1338
  if (stat.isDirectory()) {
563
1339
  visit(absolutePath);
564
1340
  } else if (/\.(md|mdx|txt|mdc)$/i.test(entry)) {
565
- results.push(path.relative(process.cwd(), absolutePath));
1341
+ results.push(path4.relative(process.cwd(), absolutePath));
566
1342
  }
567
1343
  if (results.length >= maxFiles) {
568
1344
  return;
@@ -577,7 +1353,7 @@ function isCreateSniparaGeneratedInstructionSource(relativePath) {
577
1353
  return false;
578
1354
  }
579
1355
  try {
580
- const prefix = fs.readFileSync(path.join(process.cwd(), relativePath), "utf8").slice(0, 4096);
1356
+ const prefix = fs4.readFileSync(path4.join(process.cwd(), relativePath), "utf8").slice(0, 4096);
581
1357
  return prefix.includes(
582
1358
  "## Snipara Context Workflow\n\nThis project uses Snipara Hosted MCP for project context and reviewed memory."
583
1359
  ) || prefix.includes(
@@ -590,15 +1366,15 @@ function isCreateSniparaGeneratedInstructionSource(relativePath) {
590
1366
  }
591
1367
  }
592
1368
  function findDecisionArchaeologySources(cwd) {
593
- const exists = (relativePath) => fs.existsSync(path.join(cwd, relativePath));
1369
+ const exists2 = (relativePath) => fs4.existsSync(path4.join(cwd, relativePath));
594
1370
  const directSources = [
595
1371
  "README.md",
596
1372
  "CHANGELOG.md",
597
1373
  "MEMORY.md",
598
1374
  ...["AGENTS.md", "CLAUDE.md", ".github/copilot-instructions.md"].filter(
599
- (source) => exists(source)
1375
+ (source) => exists2(source)
600
1376
  )
601
- ].filter(exists);
1377
+ ].filter(exists2);
602
1378
  const archaeologySources = [
603
1379
  ...listFilesUnder("docs/adr", 20),
604
1380
  ...listFilesUnder("docs/adrs", 20),
@@ -630,7 +1406,7 @@ function findBoundedProjectFiles(predicate, maxMatches = PROJECT_BOOTSTRAP_MAX_M
630
1406
  }
631
1407
  let entries;
632
1408
  try {
633
- entries = fs.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
1409
+ entries = fs4.readdirSync(directory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name));
634
1410
  } catch {
635
1411
  return;
636
1412
  }
@@ -638,14 +1414,14 @@ function findBoundedProjectFiles(predicate, maxMatches = PROJECT_BOOTSTRAP_MAX_M
638
1414
  if (entry.isSymbolicLink()) {
639
1415
  continue;
640
1416
  }
641
- const absolutePath = path.join(directory, entry.name);
1417
+ const absolutePath = path4.join(directory, entry.name);
642
1418
  if (entry.isDirectory()) {
643
1419
  if (!ignoredDirectories.has(entry.name)) {
644
1420
  visit(absolutePath);
645
1421
  }
646
1422
  } else if (entry.isFile()) {
647
1423
  walkedFiles += 1;
648
- const relativePath = path.relative(process.cwd(), absolutePath).replace(/\\/g, "/");
1424
+ const relativePath = path4.relative(process.cwd(), absolutePath).replace(/\\/g, "/");
649
1425
  if (predicate(relativePath)) {
650
1426
  results.push(relativePath);
651
1427
  }
@@ -659,12 +1435,12 @@ function findBoundedProjectFiles(predicate, maxMatches = PROJECT_BOOTSTRAP_MAX_M
659
1435
  return results;
660
1436
  }
661
1437
  function readProjectPackageMetadata() {
662
- const packageJsonPath = path.join(process.cwd(), "package.json");
663
- if (!fs.existsSync(packageJsonPath)) {
1438
+ const packageJsonPath = path4.join(process.cwd(), "package.json");
1439
+ if (!fs4.existsSync(packageJsonPath)) {
664
1440
  return { scripts: {}, dependencies: [], workspaces: false };
665
1441
  }
666
1442
  try {
667
- const parsed = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
1443
+ const parsed = JSON.parse(fs4.readFileSync(packageJsonPath, "utf8"));
668
1444
  const scripts = Object.fromEntries(
669
1445
  Object.entries(parsed.scripts ?? {}).filter(
670
1446
  (entry) => typeof entry[1] === "string"
@@ -722,7 +1498,7 @@ function createBootstrapItem(section, title, status, detail, evidence) {
722
1498
  }
723
1499
  function buildProjectBootstrap(scan) {
724
1500
  const cwd = process.cwd();
725
- const exists = (relativePath) => fs.existsSync(path.join(cwd, relativePath));
1501
+ const exists2 = (relativePath) => fs4.existsSync(path4.join(cwd, relativePath));
726
1502
  const packageMetadata = readProjectPackageMetadata();
727
1503
  const allDetectedFiles = findBoundedProjectFiles(() => true, PROJECT_BOOTSTRAP_MAX_WALKED_FILES);
728
1504
  const testFiles = allDetectedFiles.filter(
@@ -738,14 +1514,14 @@ function buildProjectBootstrap(scan) {
738
1514
  (relativePath) => /^\.github\/workflows\/[^/]+\.ya?ml$/i.test(relativePath)
739
1515
  ),
740
1516
  ...[".gitlab-ci.yml", "azure-pipelines.yml", "Jenkinsfile", ".circleci/config.yml"].filter(
741
- exists
1517
+ exists2
742
1518
  )
743
1519
  ].filter((value, index, values) => values.indexOf(value) === index);
744
1520
  const instructionSources = scan.files.candidateDocs.filter(
745
1521
  (source) => classifyDecisionSource(source) === "agent_instruction"
746
1522
  );
747
1523
  const documentationSources = [
748
- ...["README.md", "CHANGELOG.md", "MEMORY.md"].filter(exists),
1524
+ ...["README.md", "CHANGELOG.md", "MEMORY.md"].filter(exists2),
749
1525
  ...listFilesUnder("docs", 40)
750
1526
  ].filter((value, index, values) => values.indexOf(value) === index);
751
1527
  const releaseTags = scan.git.detected ? readBootstrapReleaseTags() : [];
@@ -774,7 +1550,7 @@ function buildProjectBootstrap(scan) {
774
1550
  );
775
1551
  }
776
1552
  const workspaceEvidence = ["pnpm-workspace.yaml", "turbo.json", "nx.json", "lerna.json"].filter(
777
- exists
1553
+ exists2
778
1554
  );
779
1555
  if (packageMetadata.workspaces || workspaceEvidence.length > 0) {
780
1556
  architecture.push(
@@ -790,7 +1566,7 @@ function buildProjectBootstrap(scan) {
790
1566
  )
791
1567
  );
792
1568
  }
793
- for (const directory of ["apps", "packages", "services", "libs"].filter(exists)) {
1569
+ for (const directory of ["apps", "packages", "services", "libs"].filter(exists2)) {
794
1570
  architecture.push(
795
1571
  createBootstrapItem(
796
1572
  "architecture",
@@ -871,7 +1647,7 @@ function buildProjectBootstrap(scan) {
871
1647
  procedures.push(
872
1648
  createBootstrapItem(
873
1649
  "procedures",
874
- `CI workflow: ${path.basename(source)}`,
1650
+ `CI workflow: ${path4.basename(source)}`,
875
1651
  "observed",
876
1652
  "Automation configuration detected; individual jobs were not interpreted during the bounded scan.",
877
1653
  [{ source, kind: "ci_workflow" }]
@@ -881,7 +1657,7 @@ function buildProjectBootstrap(scan) {
881
1657
  const skills = skillFiles.slice(0, 12).map(
882
1658
  (source) => createBootstrapItem(
883
1659
  "skills",
884
- `Existing skill: ${path.basename(path.dirname(source)) || "root"}`,
1660
+ `Existing skill: ${path4.basename(path4.dirname(source)) || "root"}`,
885
1661
  "observed",
886
1662
  "An explicit SKILL.md workflow was found.",
887
1663
  [{ source, kind: "skill_definition" }]
@@ -1048,11 +1824,11 @@ function buildProjectBootstrap(scan) {
1048
1824
  testFiles.slice(0, 20)
1049
1825
  ),
1050
1826
  releaseHistory: createBootstrapCoverage(
1051
- releaseTags.length > 0 || exists("CHANGELOG.md") ? "partial" : "unavailable",
1052
- releaseTags.length > 0 || exists("CHANGELOG.md") ? `Detected ${releaseTags.length} recent tag(s)${exists("CHANGELOG.md") ? " and CHANGELOG.md" : ""}.` : "No Git tags or root CHANGELOG.md were detected.",
1827
+ releaseTags.length > 0 || exists2("CHANGELOG.md") ? "partial" : "unavailable",
1828
+ releaseTags.length > 0 || exists2("CHANGELOG.md") ? `Detected ${releaseTags.length} recent tag(s)${exists2("CHANGELOG.md") ? " and CHANGELOG.md" : ""}.` : "No Git tags or root CHANGELOG.md were detected.",
1053
1829
  [
1054
1830
  ...releaseTags.map((tag) => `git tag ${tag}`),
1055
- ...exists("CHANGELOG.md") ? ["CHANGELOG.md"] : []
1831
+ ...exists2("CHANGELOG.md") ? ["CHANGELOG.md"] : []
1056
1832
  ]
1057
1833
  ),
1058
1834
  codeGraph: createBootstrapCoverage(
@@ -1079,8 +1855,8 @@ function createBootstrapCoverage(status, detail, sources) {
1079
1855
  return { status, detail, sources };
1080
1856
  }
1081
1857
  function writeProjectBootstrap(report) {
1082
- fs.mkdirSync(getActivationDir(), { recursive: true });
1083
- fs.writeFileSync(getProjectBootstrapJsonPath(), JSON.stringify(report, null, 2) + "\n", "utf8");
1858
+ fs4.mkdirSync(getActivationDir(), { recursive: true });
1859
+ fs4.writeFileSync(getProjectBootstrapJsonPath(), JSON.stringify(report, null, 2) + "\n", "utf8");
1084
1860
  const lines = [
1085
1861
  "# PROJECT BOOTSTRAP",
1086
1862
  "",
@@ -1110,7 +1886,7 @@ function writeProjectBootstrap(report) {
1110
1886
  `**${report.reviewPrompt}**`,
1111
1887
  ""
1112
1888
  ];
1113
- fs.writeFileSync(getProjectBootstrapMarkdownPath(), lines.join("\n"), "utf8");
1889
+ fs4.writeFileSync(getProjectBootstrapMarkdownPath(), lines.join("\n"), "utf8");
1114
1890
  }
1115
1891
  function addProjectBootstrapToFirstWorkBrief(manifest, report) {
1116
1892
  manifest.firstBrief.sections.splice(1, 0, {
@@ -1200,6 +1976,7 @@ function createActivationManifest(answers, detection, mode = "default", scan) {
1200
1976
  const surfaceContract = buildActivationSurfaceContract(answers.aiClient);
1201
1977
  return {
1202
1978
  activationId: buildActivationId(),
1979
+ profile: answers.profile,
1203
1980
  mode,
1204
1981
  deployment: answers.deployment,
1205
1982
  serverUrl: answers.serverUrl,
@@ -1283,8 +2060,8 @@ function createActivationManifest(answers, detection, mode = "default", scan) {
1283
2060
  )
1284
2061
  },
1285
2062
  artifacts: {
1286
- activationManifestPath: path.relative(process.cwd(), getStableActivationManifestPath()),
1287
- legacyManifestPath: path.relative(process.cwd(), getActivationManifestPath()),
2063
+ activationManifestPath: path4.relative(process.cwd(), getStableActivationManifestPath()),
2064
+ legacyManifestPath: path4.relative(process.cwd(), getActivationManifestPath()),
1288
2065
  firstBriefPath: null,
1289
2066
  handoffPath: null,
1290
2067
  memoryCandidatesPath: null,
@@ -1302,7 +2079,8 @@ function runLocalSourceActivation(answers, manifest) {
1302
2079
  updateActivationManifest(manifest);
1303
2080
  return;
1304
2081
  }
1305
- const companionCommand = detectCompanionCommand();
2082
+ const managed = answers.profile === "local-work" ? answers.managedToolchain ?? managedToolchainStatus().manifest : null;
2083
+ const companionCommand = managed?.components.companion.command ?? detectCompanionCommand();
1306
2084
  if (!companionCommand) {
1307
2085
  const detail = `${COMPANION_COMMAND_NAME} ${MIN_COMPANION_VERSION}+ is not on PATH after setup.`;
1308
2086
  manifest.lanes.localSnapshot = createActivationLane("skipped", detail);
@@ -1315,12 +2093,14 @@ function runLocalSourceActivation(answers, manifest) {
1315
2093
  const syncCommand = `${companionCommand} source sync --json`;
1316
2094
  const spinner = ora("Activating local Project Intelligence").start();
1317
2095
  try {
1318
- execSync(initCommand, {
2096
+ const runOptions = {
1319
2097
  cwd: process.cwd(),
1320
- encoding: "utf-8",
2098
+ encoding: "utf8",
1321
2099
  stdio: ["ignore", "pipe", "pipe"],
1322
2100
  timeout: 3e4
1323
- });
2101
+ };
2102
+ if (managed) execFileSync3(process.execPath, [companionCommand, "source", "init", "."], runOptions);
2103
+ else execSync(initCommand, runOptions);
1324
2104
  manifest.lanes.localSnapshot = createActivationLane(
1325
2105
  "ready",
1326
2106
  "Local source snapshot created.",
@@ -1337,12 +2117,14 @@ function runLocalSourceActivation(answers, manifest) {
1337
2117
  return;
1338
2118
  }
1339
2119
  try {
1340
- execSync(syncCommand, {
2120
+ const runOptions = {
1341
2121
  cwd: process.cwd(),
1342
- encoding: "utf-8",
2122
+ encoding: "utf8",
1343
2123
  stdio: ["ignore", "pipe", "pipe"],
1344
2124
  timeout: 45e3
1345
- });
2125
+ };
2126
+ if (managed) execFileSync3(process.execPath, [companionCommand, "source", "sync", "--json"], runOptions);
2127
+ else execSync(syncCommand, runOptions);
1346
2128
  manifest.lanes.contextIndex = createActivationLane(
1347
2129
  "indexing",
1348
2130
  "Local documents submitted for context indexing.",
@@ -1564,11 +2346,11 @@ function extractMcpText(data) {
1564
2346
  function createActivationArtifacts(answers, manifest) {
1565
2347
  const projectBootstrap = buildProjectBootstrap(manifest.localScan);
1566
2348
  writeProjectBootstrap(projectBootstrap);
1567
- manifest.artifacts.projectBootstrapJsonPath = path.relative(
2349
+ manifest.artifacts.projectBootstrapJsonPath = path4.relative(
1568
2350
  process.cwd(),
1569
2351
  getProjectBootstrapJsonPath()
1570
2352
  );
1571
- manifest.artifacts.projectBootstrapMarkdownPath = path.relative(
2353
+ manifest.artifacts.projectBootstrapMarkdownPath = path4.relative(
1572
2354
  process.cwd(),
1573
2355
  getProjectBootstrapMarkdownPath()
1574
2356
  );
@@ -1580,21 +2362,21 @@ function createActivationArtifacts(answers, manifest) {
1580
2362
  addProjectBootstrapToFirstWorkBrief(manifest, projectBootstrap);
1581
2363
  const candidates = buildMemoryCandidatePreviews(manifest.localScan);
1582
2364
  writeMemoryCandidates(candidates);
1583
- manifest.artifacts.memoryCandidatesPath = path.relative(process.cwd(), getMemoryCandidatesPath());
2365
+ manifest.artifacts.memoryCandidatesPath = path4.relative(process.cwd(), getMemoryCandidatesPath());
1584
2366
  manifest.lanes.memoryCandidates = createActivationLane(
1585
2367
  candidates.length > 0 ? "needs_review" : "skipped",
1586
2368
  candidates.length > 0 ? `${candidates.length} memory candidates prepared for review; none were imported.` : "No memory candidates found in local docs.",
1587
2369
  manifest.artifacts.memoryCandidatesPath
1588
2370
  );
1589
2371
  writeInitialHandoff(answers, manifest);
1590
- manifest.artifacts.handoffPath = path.relative(process.cwd(), getActivationHandoffPath());
2372
+ manifest.artifacts.handoffPath = path4.relative(process.cwd(), getActivationHandoffPath());
1591
2373
  manifest.lanes.handoff = createActivationLane(
1592
2374
  "ready",
1593
2375
  "Initial local handoff created.",
1594
2376
  manifest.artifacts.handoffPath
1595
2377
  );
1596
2378
  writeFirstWorkBrief(manifest);
1597
- manifest.artifacts.firstBriefPath = path.relative(process.cwd(), getFirstWorkBriefPath());
2379
+ manifest.artifacts.firstBriefPath = path4.relative(process.cwd(), getFirstWorkBriefPath());
1598
2380
  manifest.lanes.firstBrief = createActivationLane(
1599
2381
  "ready",
1600
2382
  "First Work Brief generated from local setup and scan signals.",
@@ -1683,61 +2465,71 @@ function shouldInstallDefaultHooks(aiClient) {
1683
2465
  return aiClient === "claude-code" || aiClient === "codex";
1684
2466
  }
1685
2467
  function buildProgram() {
1686
- const program = new Command();
1687
- program.name("create-snipara").description("Connect Snipara Cloud or a self-hosted Snipara Server with a fast proof of value").version(VERSION);
1688
- program.command("init").description("Install and configure Snipara for this project").option("--api-key <key>", "Snipara API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
2468
+ const program2 = new Command();
2469
+ program2.name("create-snipara").description("Connect Snipara Cloud or a self-hosted Snipara Server with a fast proof of value").version(VERSION);
2470
+ const toolchain = program2.command("toolchain").description("Prepare or inspect private local-work components");
2471
+ toolchain.command("prepare").description("Prepare the compatible local-work bundle and switch only after checks").action(async () => {
2472
+ console.log(JSON.stringify(await prepareManagedToolchain(), null, 2));
2473
+ });
2474
+ toolchain.command("status").description("Inspect the active bundle without installing or activating").action(() => {
2475
+ console.log(JSON.stringify(managedToolchainStatus(), null, 2));
2476
+ });
2477
+ toolchain.command("rollback").description("Select the previous valid bundle; retain task state and evidence").action(async () => {
2478
+ console.log(JSON.stringify(await rollbackManagedToolchain(), null, 2));
2479
+ });
2480
+ program2.command("init").description("Install and configure Snipara for this project").option("--api-key <key>", "Snipara API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
1689
2481
  "--client <client>",
1690
2482
  "AI client: claude-code, cursor, kimi, codex, gemini, mistral, chatgpt, vscode, continue, claude-desktop, other"
1691
- ).option("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option(
2483
+ ).option("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option("--connect-context", "Optionally connect project context during local-work setup").option(
1692
2484
  "--profile <profile>",
1693
- "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
2485
+ "Install profile: hosted-only (Context), local-work, hosted-companion, full-stack, runtime-only"
1694
2486
  ).option("--deployment <deployment>", "Deployment: hosted or self-hosted").option("--self-hosted", "Use an existing self-hosted Snipara MCP server").option("--server-url <url>", "Self-hosted MCP endpoint URL").option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip local snipara-mcp package installation").option("--skip-runtime", "Skip snipara-sandbox installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip MCP connection and value-proof checks").option("--with-orchestrator", "Install optional snipara-orchestrator package").option("--github", "Open GitHub repository automation setup when a git remote is detected").option("--skip-github", "Skip the GitHub automation prompt").option("--github-path <path>", "Optional GitHub path filter for the initial repository sync").option("--starter", "Mark this run as a starter activation and print dashboard-first guidance").option("--json", "Print a machine-readable activation summary after setup").option("--runtime-only", "Install only Snipara Sandbox").addOption(new Option("--full", "Legacy alias for --profile full-stack").hideHelp()).option("-y, --yes", "Accept defaults without prompting").action(async (options) => {
1695
2487
  printBanner();
1696
2488
  await runInit(options);
1697
2489
  });
1698
- program.command("doctor").description("Inspect the current project and write a validation report").option(
2490
+ program2.command("doctor").description("Inspect the current project and write a validation report").option(
1699
2491
  "--client <client>",
1700
2492
  "Run client-specific diagnostics: claude, claude-code, claude-desktop, cursor, codex, etc."
1701
2493
  ).option("--skip-npm-check", "Skip npm latest-version freshness check").option("--json", "Print the report as JSON").action(async (options) => {
1702
2494
  printBanner();
1703
2495
  await runDoctor(options);
1704
2496
  });
1705
- program.command("repair").description("Rebuild local Snipara config and re-run validation").option("--api-key <key>", "Snipara API key override").option("--slug <slug>", "Project slug override").option("--project-id <id>", "Project ID override").option("--profile <profile>", "Target install profile").option("--deployment <deployment>", "Deployment: hosted or self-hosted").option("--self-hosted", "Use an existing self-hosted Snipara MCP server").option("--server-url <url>", "Self-hosted MCP endpoint URL").option("--with-hooks", "Force hook generation").option("--with-runtime", "Force Snipara Sandbox server/config").option("--with-mcp", "Force MCP config").option("--with-companion", "Force companion pack generation").option("--with-orchestrator", "Install optional snipara-orchestrator package").action(async (options) => {
2497
+ program2.command("repair").description("Rebuild local Snipara config and re-run validation").option("--api-key <key>", "Snipara API key override").option("--slug <slug>", "Project slug override").option("--project-id <id>", "Project ID override").option("--profile <profile>", "Target install profile").option("--deployment <deployment>", "Deployment: hosted or self-hosted").option("--self-hosted", "Use an existing self-hosted Snipara MCP server").option("--server-url <url>", "Self-hosted MCP endpoint URL").option("--with-hooks", "Force hook generation").option("--with-runtime", "Force Snipara Sandbox server/config").option("--with-mcp", "Force MCP config").option("--with-companion", "Force companion pack generation").option("--with-orchestrator", "Install optional snipara-orchestrator package").action(async (options) => {
1706
2498
  printBanner();
1707
2499
  await runRepair(options);
1708
2500
  });
1709
- program.command("demo").description("Open the guided Snipara activation demo without changing this repository").option("--no-open", "Print demo links without opening a browser").action((options) => {
2501
+ program2.command("demo").description("Open the guided Snipara activation demo without changing this repository").option("--no-open", "Print demo links without opening a browser").action((options) => {
1710
2502
  printBanner();
1711
2503
  runDemo(options);
1712
2504
  });
1713
- program.command("upgrade").description("Upgrade installed Snipara local components").option("--slug <slug>", "Project slug override").option("--with-hooks", "Refresh hooks after upgrade").option("--with-runtime", "Upgrade Snipara Sandbox even if not detected").option("--with-mcp", "Upgrade snipara-mcp even if not detected").option("--with-companion", "Upgrade companion even if not detected").option("--with-orchestrator", "Upgrade snipara-orchestrator even if not detected").action(async (options) => {
2505
+ program2.command("upgrade").description("Upgrade installed Snipara local components").option("--slug <slug>", "Project slug override").option("--with-hooks", "Refresh hooks after upgrade").option("--with-runtime", "Upgrade Snipara Sandbox even if not detected").option("--with-mcp", "Upgrade snipara-mcp even if not detected").option("--with-companion", "Upgrade companion even if not detected").option("--with-orchestrator", "Upgrade snipara-orchestrator even if not detected").action(async (options) => {
1714
2506
  printBanner();
1715
2507
  await runUpgrade(options);
1716
2508
  });
1717
- program.command("print-config").description("Print detected local setup and inferred install profile").option("--json", "Print JSON instead of a formatted summary").action(async (options) => {
2509
+ program2.command("print-config").description("Print detected local setup and inferred install profile").option("--json", "Print JSON instead of a formatted summary").action(async (options) => {
1718
2510
  printBanner();
1719
2511
  await runPrintConfig(options);
1720
2512
  });
1721
- addSharedRootOptions(program);
1722
- program.action(async (options) => {
2513
+ addSharedRootOptions(program2);
2514
+ program2.action(async (options) => {
1723
2515
  printBanner();
1724
2516
  await runInit(options);
1725
2517
  });
1726
- return program;
2518
+ return program2;
1727
2519
  }
1728
- function addSharedRootOptions(program) {
1729
- program.option("--api-key <key>", "Snipara API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
2520
+ function addSharedRootOptions(program2) {
2521
+ program2.option("--api-key <key>", "Snipara API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
1730
2522
  "--client <client>",
1731
2523
  "AI client: claude-code, cursor, kimi, codex, gemini, mistral, chatgpt, vscode, continue, claude-desktop, other"
1732
- ).option("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option(
2524
+ ).option("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option("--connect-context", "Optionally connect project context during local-work setup").option(
1733
2525
  "--profile <profile>",
1734
- "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
2526
+ "Install profile: hosted-only (Context), local-work, hosted-companion, full-stack, runtime-only"
1735
2527
  ).option("--deployment <deployment>", "Deployment: hosted or self-hosted").option("--self-hosted", "Use an existing self-hosted Snipara MCP server").option("--server-url <url>", "Self-hosted MCP endpoint URL").option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip local snipara-mcp package installation").option("--skip-runtime", "Skip snipara-sandbox installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip MCP connection and value-proof checks").option("--with-orchestrator", "Install optional snipara-orchestrator package").option("--github", "Open GitHub repository automation setup when a git remote is detected").option("--skip-github", "Skip the GitHub automation prompt").option("--github-path <path>", "Optional GitHub path filter for the initial repository sync").option("--starter", "Mark this run as a starter activation and print dashboard-first guidance").option("--json", "Print a machine-readable activation summary after setup").option("--runtime-only", "Install only Snipara Sandbox").addOption(new Option("--full", "Legacy alias for --profile full-stack").hideHelp()).option("-y, --yes", "Accept defaults without prompting");
1736
2528
  }
1737
2529
  async function main() {
1738
- const program = buildProgram();
2530
+ const program2 = buildProgram();
1739
2531
  normalizeLegacyArgAliases(process.argv);
1740
- await program.parseAsync(process.argv);
2532
+ await program2.parseAsync(process.argv);
1741
2533
  }
1742
2534
  function normalizeLegacyArgAliases(argv) {
1743
2535
  for (let index = 2; index < argv.length; index += 1) {
@@ -1822,6 +2614,7 @@ function parseInitOptionsFromArgv(args) {
1822
2614
  };
1823
2615
  const booleanFlags = {
1824
2616
  "--advanced": "advanced",
2617
+ "--connect-context": "connectContext",
1825
2618
  "--skip-companion": "skipCompanion",
1826
2619
  "--skip-mcp": "skipMcp",
1827
2620
  "--skip-runtime": "skipRuntime",
@@ -1864,12 +2657,15 @@ function parseInitOptionsFromArgv(args) {
1864
2657
  async function runInit(options) {
1865
2658
  const normalizedOptions = normalizeInitOptions(options);
1866
2659
  const answers = await gatherConfiguration(normalizedOptions);
1867
- const activationDetection = detectEnvironment();
1868
- const activationScan = detectLocalActivationScan(activationDetection);
1869
- if (answers.installMcp || answers.installRuntime || answers.installOrchestrator) {
2660
+ const activationDetection = detectEnvironment(answers.profile === "local-work");
2661
+ const activationScan = managedSetupOnly(answers) ? void 0 : detectLocalActivationScan(activationDetection);
2662
+ if (answers.profile === "local-work") {
2663
+ assertManagedMcpConfig();
2664
+ answers.managedToolchain = await prepareManagedToolchain();
2665
+ } else if (answers.installMcp || answers.installRuntime || answers.installOrchestrator) {
1870
2666
  await installPythonPackages(answers, false);
1871
2667
  }
1872
- if (answers.installCompanion) {
2668
+ if (answers.installCompanion && answers.profile !== "local-work") {
1873
2669
  await installNodeCompanion(false);
1874
2670
  }
1875
2671
  if (answers.profile !== "runtime-only" || answers.installRuntime) {
@@ -1878,6 +2674,10 @@ async function runInit(options) {
1878
2674
  if (answers.aiClient === "claude-code" && answers.profile !== "runtime-only") {
1879
2675
  await createClaudeCodeConfig(answers);
1880
2676
  }
2677
+ if (managedSetupOnly(answers)) {
2678
+ await finishManagedLocalWork(answers, normalizedOptions.json);
2679
+ return;
2680
+ }
1881
2681
  await createProjectBinding(answers);
1882
2682
  await createAgentTemplates(answers);
1883
2683
  if (answers.installHooks && answers.aiClient === "claude-code") {
@@ -1965,16 +2765,23 @@ async function runDoctor(options) {
1965
2765
  async function runRepair(options) {
1966
2766
  const answers = loadExistingAnswers(options);
1967
2767
  console.log(chalk.bold("\n\u{1F6E0}\uFE0F Repairing Local Setup\n"));
1968
- if (answers.installMcp || answers.installRuntime || answers.installOrchestrator) {
2768
+ if (answers.profile === "local-work") {
2769
+ assertManagedMcpConfig();
2770
+ answers.managedToolchain = await prepareManagedToolchain({ repair: true });
2771
+ } else if (answers.installMcp || answers.installRuntime || answers.installOrchestrator) {
1969
2772
  await installPythonPackages(answers, true);
1970
2773
  }
1971
- if (answers.installCompanion) {
2774
+ if (answers.installCompanion && answers.profile !== "local-work") {
1972
2775
  await installNodeCompanion(true);
1973
2776
  }
1974
2777
  await createMcpConfig(answers);
1975
2778
  if (answers.aiClient === "claude-code" && answers.profile !== "runtime-only") {
1976
2779
  await createClaudeCodeConfig(answers);
1977
2780
  }
2781
+ if (managedSetupOnly(answers)) {
2782
+ await finishManagedLocalWork(answers);
2783
+ return;
2784
+ }
1978
2785
  await createProjectBinding(answers);
1979
2786
  await createAgentTemplates(answers);
1980
2787
  await updateEnvFiles(answers);
@@ -1989,7 +2796,7 @@ async function runRepair(options) {
1989
2796
  printDoctorReport(detectEnvironment(), report);
1990
2797
  }
1991
2798
  async function runUpgrade(options) {
1992
- const detection = detectEnvironment();
2799
+ const detection = detectEnvironment(hasLocalWorkSelection());
1993
2800
  const answers = loadExistingAnswers(options);
1994
2801
  const upgradedAnswers = {
1995
2802
  ...answers,
@@ -1998,18 +2805,25 @@ async function runUpgrade(options) {
1998
2805
  installOrchestrator: options.withOrchestrator || answers.installOrchestrator
1999
2806
  };
2000
2807
  console.log(chalk.bold("\n\u2B06\uFE0F Upgrading Local Components\n"));
2001
- if (options.withMcp || options.withRuntime || options.withOrchestrator || detection.existingConfig.hasRuntimeServer || detection.tooling.orchestratorCli) {
2808
+ if (answers.profile === "local-work") {
2809
+ assertManagedMcpConfig();
2810
+ upgradedAnswers.managedToolchain = await prepareManagedToolchain();
2811
+ } else if (options.withMcp || options.withRuntime || options.withOrchestrator || detection.existingConfig.hasRuntimeServer || detection.tooling.orchestratorCli) {
2002
2812
  await installPythonPackages(upgradedAnswers, true);
2003
2813
  }
2004
- if (options.withCompanion || detection.tooling.companionCli || answers.installCompanion) {
2814
+ if (answers.profile !== "local-work" && (options.withCompanion || detection.tooling.companionCli || answers.installCompanion)) {
2005
2815
  await installNodeCompanion(true);
2006
2816
  }
2007
- if (options.withMcp || options.withRuntime || detection.files.mcpConfig) {
2817
+ if (answers.profile === "local-work" || options.withMcp || options.withRuntime || detection.files.mcpConfig) {
2008
2818
  await createMcpConfig(upgradedAnswers);
2009
2819
  }
2010
2820
  if (upgradedAnswers.aiClient === "claude-code" && upgradedAnswers.profile !== "runtime-only") {
2011
2821
  await createClaudeCodeConfig(upgradedAnswers);
2012
2822
  }
2823
+ if (managedSetupOnly(upgradedAnswers)) {
2824
+ await finishManagedLocalWork(upgradedAnswers);
2825
+ return;
2826
+ }
2013
2827
  await createProjectBinding(upgradedAnswers);
2014
2828
  await createAgentTemplates(upgradedAnswers);
2015
2829
  await createCompanionConfig(upgradedAnswers);
@@ -2119,13 +2933,13 @@ async function gatherConfiguration(options) {
2119
2933
  }
2120
2934
  const initialProfile = resolveRequestedProfile(options);
2121
2935
  const initialDeployment = resolveRequestedDeployment(options);
2122
- const initialServerUrl = normalizeMcpEndpoint(
2936
+ const initialServerUrl = initialProfile === "local-work" && (!options.connectContext || hasBoundLocalWorkSource("local-work")) ? "http://localhost:8000/mcp/local" : normalizeMcpEndpoint(
2123
2937
  options.serverUrl || process.env.SNIPARA_MCP_URL || getExistingConfig()?.serverUrl || "http://localhost:8000/mcp/local"
2124
2938
  );
2125
2939
  const defaultProjectSlug = options.slug || detectedSlug || (initialDeployment === "self-hosted" ? extractSlugFromMcpEndpoint(initialServerUrl) || "local" : "");
2126
2940
  const optionApiKey = options.apiKey?.trim();
2127
2941
  const environmentApiKey = options.yes ? initialDeployment === "self-hosted" ? process.env.SNIPARA_LOCAL_API_KEY?.trim() || process.env.SNIPARA_API_KEY?.trim() : process.env.SNIPARA_API_KEY?.trim() : void 0;
2128
- const providedApiKey = optionApiKey || environmentApiKey;
2942
+ const providedApiKey = initialProfile === "local-work" && (!options.connectContext || hasBoundLocalWorkSource("local-work")) ? void 0 : optionApiKey || environmentApiKey;
2129
2943
  if (providedApiKey && !isValidApiKey(providedApiKey, initialDeployment)) {
2130
2944
  throw new Error(manualApiKeyValidationMessage(initialDeployment));
2131
2945
  }
@@ -2141,6 +2955,7 @@ async function gatherConfiguration(options) {
2141
2955
  apiKey: providedApiKey || "YOUR_API_KEY_HERE",
2142
2956
  apiKeyType: providedApiKey ? "api" : "none",
2143
2957
  profile: initialProfile,
2958
+ connectContext: options.connectContext,
2144
2959
  aiClient: requestedClient || detectedClient,
2145
2960
  skipCompanion: !!options.skipCompanion,
2146
2961
  skipMcp: !!options.skipMcp,
@@ -2188,27 +3003,27 @@ async function gatherConfiguration(options) {
2188
3003
  }
2189
3004
  },
2190
3005
  {
2191
- type: showAdvancedPrompts ? "select" : null,
3006
+ type: options.profile ? null : "select",
2192
3007
  name: "profile",
2193
- message: "Choose an install profile:",
2194
- initial: installProfileChoices().findIndex((choice) => choice.value === initialProfile),
2195
- choices: installProfileChoices()
3008
+ message: "How will you use Snipara?",
3009
+ initial: Math.max(0, installProfileChoices(showAdvancedPrompts).findIndex((choice) => choice.value === initialProfile)),
3010
+ choices: installProfileChoices(showAdvancedPrompts)
2196
3011
  },
2197
3012
  {
2198
- type: "text",
3013
+ type: (_, values) => (values.profile || initialProfile) === "local-work" && (!options.connectContext || hasBoundLocalWorkSource("local-work")) ? null : "text",
2199
3014
  name: "projectSlug",
2200
3015
  message: "Project slug:",
2201
3016
  initial: defaultProjectSlug,
2202
3017
  validate: (value) => value.length > 0 ? true : "Project slug is required (for example my-project)"
2203
3018
  },
2204
3019
  {
2205
- type: showAdvancedPrompts ? "text" : null,
3020
+ type: showAdvancedPrompts && (initialProfile !== "local-work" || options.connectContext) ? "text" : null,
2206
3021
  name: "projectId",
2207
3022
  message: "Project ID (optional, press Enter to skip):",
2208
3023
  initial: options.projectId || detectedProjectId || ""
2209
3024
  },
2210
3025
  {
2211
- type: (_, values) => values.profile === "runtime-only" || providedApiKey ? null : "select",
3026
+ type: (_, values) => values.profile === "runtime-only" || providedApiKey || (values.profile || initialProfile) === "local-work" && (!options.connectContext || hasBoundLocalWorkSource("local-work")) ? null : "select",
2212
3027
  name: "apiKeyType",
2213
3028
  message: (_, values) => values.deployment === "self-hosted" || initialDeployment === "self-hosted" ? "How do you want to add the self-hosted operator API key?" : "How do you want to add your Snipara API key?",
2214
3029
  choices: (_, values) => values.deployment === "self-hosted" || initialDeployment === "self-hosted" ? [
@@ -2308,13 +3123,14 @@ async function gatherConfiguration(options) {
2308
3123
  }
2309
3124
  }
2310
3125
  const answers = buildAnswersFromProfile({
2311
- projectSlug: signupResult?.projectSlug || response.projectSlug,
3126
+ projectSlug: signupResult?.projectSlug || response.projectSlug || defaultProjectSlug || "existing-source",
2312
3127
  projectId: signupResult?.projectId || response.projectId || options.projectId || "",
2313
3128
  deployment: selectedDeployment,
2314
3129
  serverUrl: selectedServerUrl,
2315
3130
  apiKey: apiKey || "",
2316
3131
  apiKeyType,
2317
3132
  profile: response.profile || initialProfile,
3133
+ connectContext: options.connectContext,
2318
3134
  aiClient: requestedClient || response.aiClient || detectedClient,
2319
3135
  skipCompanion: !!options.skipCompanion,
2320
3136
  skipMcp: !!options.skipMcp,
@@ -2327,7 +3143,7 @@ async function gatherConfiguration(options) {
2327
3143
  githubRepo: detectedGitRepo || void 0,
2328
3144
  githubPath: response.githubPath || options.githubPath
2329
3145
  });
2330
- if (answers.installRuntime) {
3146
+ if (answers.installRuntime && answers.profile !== "local-work") {
2331
3147
  const runtimeResponse = await prompts([
2332
3148
  {
2333
3149
  type: "select",
@@ -2418,7 +3234,7 @@ function getDeploymentLabel(deployment) {
2418
3234
  return deployment === "self-hosted" ? "self-hosted Snipara Server" : "Snipara Cloud";
2419
3235
  }
2420
3236
  function isInstallProfile(value) {
2421
- return ["hosted-only", "hosted-companion", "full-stack", "runtime-only"].includes(value);
3237
+ return ["hosted-only", "local-work", "hosted-companion", "full-stack", "runtime-only"].includes(value);
2422
3238
  }
2423
3239
  function normalizeClientOption(value) {
2424
3240
  if (!value) {
@@ -2457,13 +3273,21 @@ function normalizeClientOption(value) {
2457
3273
  }
2458
3274
  return client;
2459
3275
  }
2460
- function installProfileChoices() {
2461
- return [
3276
+ function installProfileChoices(advanced = true) {
3277
+ const normal = [
2462
3278
  {
2463
- title: "Hosted only",
3279
+ title: "Context",
2464
3280
  value: "hosted-only",
2465
- description: "Default: hosted MCP config, no local companion, hooks, or sandbox"
3281
+ description: "Connect hosted project knowledge to your agent"
2466
3282
  },
3283
+ {
3284
+ title: "Local work",
3285
+ value: "local-work",
3286
+ description: "Work locally; automatically reuse a connected repository's optional context"
3287
+ }
3288
+ ];
3289
+ return advanced ? [
3290
+ ...normal,
2467
3291
  {
2468
3292
  title: "Hosted + Companion",
2469
3293
  value: "hosted-companion",
@@ -2479,9 +3303,11 @@ function installProfileChoices() {
2479
3303
  value: "runtime-only",
2480
3304
  description: "Snipara Sandbox execution only, no hosted API key required"
2481
3305
  }
2482
- ];
3306
+ ] : normal;
2483
3307
  }
2484
3308
  function buildAnswersFromProfile(input) {
3309
+ if (input.profile === "local-work" && (input.skipCompanion || input.skipRuntime))
3310
+ throw new Error("local-work requires the complete managed bundle; choose hosted-only to skip local components");
2485
3311
  const defaults = {
2486
3312
  "hosted-only": {
2487
3313
  installCompanion: false,
@@ -2489,6 +3315,12 @@ function buildAnswersFromProfile(input) {
2489
3315
  installRuntime: false,
2490
3316
  installHooks: false
2491
3317
  },
3318
+ "local-work": {
3319
+ installCompanion: true,
3320
+ installMcp: false,
3321
+ installRuntime: true,
3322
+ installHooks: false
3323
+ },
2492
3324
  "hosted-companion": {
2493
3325
  installCompanion: true,
2494
3326
  installMcp: false,
@@ -2513,14 +3345,15 @@ function buildAnswersFromProfile(input) {
2513
3345
  projectSlug: input.projectSlug,
2514
3346
  projectId: input.projectId,
2515
3347
  deployment: input.deployment,
2516
- serverUrl: input.deployment === "hosted" ? `${API_URL}/mcp/${encodeURIComponent(input.projectSlug)}` : normalizeMcpEndpoint(input.serverUrl),
3348
+ serverUrl: input.profile === "local-work" && (!input.connectContext || hasBoundLocalWorkSource("local-work")) ? "" : input.deployment === "hosted" ? `${API_URL}/mcp/${encodeURIComponent(input.projectSlug)}` : normalizeMcpEndpoint(input.serverUrl),
2517
3349
  apiKey: input.apiKey || "YOUR_API_KEY_HERE",
2518
3350
  apiKeyType: input.profile === "runtime-only" ? "none" : input.apiKeyType,
2519
3351
  profile: input.profile,
3352
+ connectContext: input.profile === "local-work" ? Boolean(input.connectContext) : true,
2520
3353
  installCompanion: selected.installCompanion && !input.skipCompanion,
2521
3354
  installMcp: selected.installMcp && !input.skipMcp,
2522
3355
  installRuntime: selected.installRuntime && !input.skipRuntime,
2523
- installOrchestrator: input.withOrchestrator,
3356
+ installOrchestrator: input.profile === "local-work" || input.withOrchestrator,
2524
3357
  installHooks: selected.installHooks && !input.skipHooks,
2525
3358
  aiClient: input.aiClient,
2526
3359
  configureGitHub: Boolean(input.configureGitHub && input.githubRepo),
@@ -2686,7 +3519,7 @@ function runDemo(options) {
2686
3519
  }
2687
3520
  }
2688
3521
  function sleep(ms) {
2689
- return new Promise((resolve) => setTimeout(resolve, ms));
3522
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
2690
3523
  }
2691
3524
  function isValidApiKey(value, deployment) {
2692
3525
  const trimmed = value.trim();
@@ -2890,36 +3723,126 @@ async function installNodeCompanion(upgrade) {
2890
3723
  console.log(chalk.gray(` npm install -g ${COMPANION_PACKAGE_NAME}@latest
2891
3724
  `));
2892
3725
  }
3726
+ function assertManagedMcpConfig(file = path4.join(process.cwd(), ".mcp.json")) {
3727
+ sourceConnectionObservation = { workspace: fs4.realpathSync(process.cwd()), source: detectSourceConnection(process.cwd()) };
3728
+ if (!fs4.existsSync(file)) {
3729
+ try {
3730
+ fs4.lstatSync(file);
3731
+ throw new Error("Managed configuration cannot replace a linked path");
3732
+ } catch (error) {
3733
+ if (error.code !== "ENOENT") throw error;
3734
+ }
3735
+ return;
3736
+ }
3737
+ const info = fs4.lstatSync(file);
3738
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 1e6)
3739
+ throw new Error("Existing .mcp.json is not a bounded regular configuration; preserve and repair it first");
3740
+ const value = JSON.parse(fs4.readFileSync(file, "utf8"));
3741
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("Existing .mcp.json is invalid");
3742
+ const servers = value.mcpServers;
3743
+ if (!servers || typeof servers !== "object" || Array.isArray(servers)) throw new Error("Existing MCP servers are invalid");
3744
+ const native = servers["snipara-local"];
3745
+ if (native !== void 0 && !isManagedNativeServer(native))
3746
+ throw new Error("Existing snipara-local connection was preserved; choose another name before managed setup");
3747
+ }
3748
+ function isManagedNativeServer(value) {
3749
+ const server = asObjectRecord(value);
3750
+ return typeof server.command === "string" && Array.isArray(server.args) && server.args[1] === "toolchain" && server.args[2] === "mcp" && server.args[3] === "--workspace-root" && server.args.length === 5 && typeof server.args[0] === "string" && typeof server.args[4] === "string";
3751
+ }
3752
+ function validManagedBootstrap(server, manifest) {
3753
+ if (!isManagedNativeServer(server) || server.command !== process.execPath || server.args[4] !== observeSource(fs4.realpathSync(process.cwd())).root || Object.keys(server.env ?? {}).length > 0)
3754
+ return false;
3755
+ const command = server.args[0], base = path4.dirname(manifest.root);
3756
+ if (!path4.isAbsolute(command) || !command.startsWith(base + path4.sep)) return false;
3757
+ return verifyRetainedCompanionBootstrap(command, manifest);
3758
+ }
3759
+ function managedNativeServer(answers) {
3760
+ const manifest = answers.managedToolchain ?? managedToolchainStatus().manifest;
3761
+ if (!manifest) throw new Error("Prepare the local-work toolchain before configuring this workspace");
3762
+ const existing = asObjectRecord(asObjectRecord(readJsonFile(path4.join(process.cwd(), ".mcp.json"))).mcpServers)["snipara-local"];
3763
+ if (isManagedNativeServer(existing)) {
3764
+ const selected = existing;
3765
+ if (!validManagedBootstrap(selected, manifest)) throw new Error("Existing managed bootstrap differs from its retained bundle; configuration was preserved");
3766
+ return selected;
3767
+ }
3768
+ return {
3769
+ type: "stdio",
3770
+ command: process.execPath,
3771
+ args: [manifest.components.companion.command, "toolchain", "mcp", "--workspace-root", observeSource(fs4.realpathSync(process.cwd())).root]
3772
+ };
3773
+ }
3774
+ function hasLocalWorkSelection() {
3775
+ const native = asObjectRecord(asObjectRecord(readJsonFile(path4.join(process.cwd(), ".mcp.json"))).mcpServers)["snipara-local"];
3776
+ const activation = asObjectRecord(readJsonFile(getStableActivationManifestPath()));
3777
+ return isManagedNativeServer(native) || activation.profile === "local-work";
3778
+ }
3779
+ function hasBoundLocalWorkSource(profile) {
3780
+ if (profile !== "local-work") return false;
3781
+ const workspace = fs4.realpathSync(process.cwd());
3782
+ if (!sourceConnectionObservation || sourceConnectionObservation.workspace !== workspace)
3783
+ sourceConnectionObservation = { workspace, source: detectSourceConnection(workspace) };
3784
+ return sourceConnectionObservation.source !== null;
3785
+ }
3786
+ var sourceConnectionObservation = null;
3787
+ function managedSetupOnly(answers) {
3788
+ return answers.profile === "local-work" && (!answers.connectContext || hasBoundLocalWorkSource(answers.profile));
3789
+ }
3790
+ async function finishManagedLocalWork(answers, json = false) {
3791
+ sourceConnectionObservation = { workspace: fs4.realpathSync(process.cwd()), source: detectSourceConnection(process.cwd()) };
3792
+ await createAgentTemplates(answers);
3793
+ const report = await runValidation({ ...answers, apiKeyType: "none" }, false);
3794
+ writeDoctorReport(report);
3795
+ printDoctorReport(detectEnvironment(), report);
3796
+ const existingSource = hasBoundLocalWorkSource(answers.profile);
3797
+ console.log(chalk.green(existingSource ? "Local work prepared. Existing repository context is reused automatically when its connection is authorized." : "Local work prepared. An account or hosted context connection is optional."));
3798
+ if (json) console.log(JSON.stringify({
3799
+ ok: true,
3800
+ profile: "local-work",
3801
+ localWork: { prepared: true },
3802
+ context: { configured: existingSource, authorized: null, mode: existingSource ? "existing_source" : "local_only" },
3803
+ activationObserved: false,
3804
+ validation: report
3805
+ }, null, 2));
3806
+ }
2893
3807
  async function createMcpConfig(answers) {
2894
3808
  console.log(chalk.bold("\n\u2699\uFE0F Creating Configuration\n"));
2895
- const mcpConfigPath = path.join(process.cwd(), ".mcp.json");
3809
+ const mcpConfigPath = path4.join(process.cwd(), ".mcp.json");
2896
3810
  let mcpConfig = { mcpServers: {} };
2897
- if (fs.existsSync(mcpConfigPath)) {
3811
+ if (answers.profile === "local-work") assertManagedMcpConfig();
3812
+ if (fs4.existsSync(mcpConfigPath)) {
2898
3813
  try {
2899
- mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
3814
+ mcpConfig = JSON.parse(fs4.readFileSync(mcpConfigPath, "utf-8"));
2900
3815
  console.log(chalk.dim(" Found existing .mcp.json, merging..."));
2901
3816
  } catch {
3817
+ if (answers.profile === "local-work") throw new Error("Existing .mcp.json changed or became invalid; configuration preserved");
2902
3818
  console.log(chalk.yellow(" Could not parse existing .mcp.json, creating new..."));
2903
3819
  }
2904
3820
  }
2905
- if (answers.profile !== "runtime-only") {
3821
+ if (answers.profile !== "runtime-only" && answers.profile !== "local-work") {
2906
3822
  mcpConfig.mcpServers.snipara = buildHostedMcpServer(answers);
2907
3823
  }
2908
- if (answers.installRuntime) {
3824
+ if (answers.profile === "local-work") {
3825
+ mcpConfig.mcpServers = mergeManagedMcpServers(
3826
+ mcpConfig.mcpServers,
3827
+ managedNativeServer(answers),
3828
+ managedSetupOnly(answers) ? { type: "http" } : buildHostedMcpServer(answers),
3829
+ managedSetupOnly(answers)
3830
+ );
3831
+ } else if (answers.installRuntime) {
2909
3832
  mcpConfig.mcpServers[SANDBOX_SERVER_NAME] = {
2910
3833
  type: "http",
2911
3834
  url: "http://localhost:8765/mcp",
2912
3835
  headers: {}
2913
3836
  };
2914
3837
  }
2915
- fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n", { mode: 384 });
3838
+ fs4.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n", { mode: 384 });
2916
3839
  try {
2917
- fs.chmodSync(mcpConfigPath, 384);
3840
+ fs4.chmodSync(mcpConfigPath, 384);
2918
3841
  } catch {
2919
3842
  }
2920
3843
  console.log(chalk.green(" \u2713 Created .mcp.json"));
2921
3844
  ensureGitignoreEntry(".mcp.json", "# Snipara MCP config (contains API key)");
2922
- if (answers.aiClient === "kimi" && answers.profile !== "runtime-only") {
3845
+ if (answers.aiClient === "kimi" && answers.profile !== "runtime-only" && !managedSetupOnly(answers)) {
2923
3846
  createKimiMcpConfig(answers);
2924
3847
  }
2925
3848
  if (answers.aiClient === "claude-desktop") {
@@ -2927,8 +3850,8 @@ async function createMcpConfig(answers) {
2927
3850
  }
2928
3851
  }
2929
3852
  function createKimiMcpConfig(answers) {
2930
- const kimiDir = path.join(process.cwd(), ".kimi-code");
2931
- const configPath = path.join(kimiDir, "mcp.json");
3853
+ const kimiDir = path4.join(process.cwd(), ".kimi-code");
3854
+ const configPath = path4.join(kimiDir, "mcp.json");
2932
3855
  const existing = asObjectRecord(readJsonFile(configPath));
2933
3856
  const existingServers = asObjectRecord(existing.mcpServers);
2934
3857
  const config = {
@@ -2944,8 +3867,8 @@ function createKimiMcpConfig(answers) {
2944
3867
  }
2945
3868
  }
2946
3869
  };
2947
- fs.mkdirSync(kimiDir, { recursive: true });
2948
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
3870
+ fs4.mkdirSync(kimiDir, { recursive: true });
3871
+ fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
2949
3872
  console.log(
2950
3873
  chalk.green(" \u2713 Created .kimi-code/mcp.json") + chalk.dim(" - SNIPARA_API_KEY stays in the launch environment")
2951
3874
  );
@@ -2970,13 +3893,13 @@ function normalizeCorrelationSessionId(value) {
2970
3893
  }
2971
3894
  function readExistingCorrelationSessionId() {
2972
3895
  const companionConfig = readCompanionConfigFile(
2973
- path.join(process.cwd(), ".snipara", "companion", "config.json")
3896
+ path4.join(process.cwd(), ".snipara", "companion", "config.json")
2974
3897
  );
2975
- const projectMcpConfig = asObjectRecord(readJsonFile(path.join(process.cwd(), ".mcp.json")));
3898
+ const projectMcpConfig = asObjectRecord(readJsonFile(path4.join(process.cwd(), ".mcp.json")));
2976
3899
  const projectMcpServer = asObjectRecord(asObjectRecord(projectMcpConfig.mcpServers).snipara);
2977
3900
  const projectMcpHeaders = asObjectRecord(projectMcpServer.headers);
2978
3901
  const kimiConfig = asObjectRecord(
2979
- readJsonFile(path.join(process.cwd(), ".kimi-code", "mcp.json"))
3902
+ readJsonFile(path4.join(process.cwd(), ".kimi-code", "mcp.json"))
2980
3903
  );
2981
3904
  const kimiServer = asObjectRecord(asObjectRecord(kimiConfig.mcpServers).snipara);
2982
3905
  const kimiHeaders = asObjectRecord(kimiServer.headers);
@@ -2984,7 +3907,7 @@ function readExistingCorrelationSessionId() {
2984
3907
  }
2985
3908
  function getCorrelationSessionId(answers) {
2986
3909
  const existing = normalizeCorrelationSessionId(answers.correlationSessionId) || readExistingCorrelationSessionId();
2987
- const sessionId = existing || `sess_${Date.now()}_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
3910
+ const sessionId = existing || `sess_${Date.now()}_${randomUUID3().replace(/-/g, "").slice(0, 12)}`;
2988
3911
  answers.correlationSessionId = sessionId;
2989
3912
  return sessionId;
2990
3913
  }
@@ -2995,6 +3918,10 @@ async function createClaudeCodeConfig(answers) {
2995
3918
  if (answers.profile === "runtime-only") {
2996
3919
  return;
2997
3920
  }
3921
+ if (managedSetupOnly(answers)) {
3922
+ console.log(chalk.green(" Local connection prepared in .mcp.json; reload and approve it in Claude Code."));
3923
+ return;
3924
+ }
2998
3925
  console.log(chalk.bold("\n\u{1F916} Claude Code MCP\n"));
2999
3926
  console.log(
3000
3927
  chalk.green(
@@ -3013,112 +3940,163 @@ async function createProjectBinding(answers) {
3013
3940
  if (answers.profile === "runtime-only") {
3014
3941
  return;
3015
3942
  }
3016
- const sniparaDir = path.join(process.cwd(), ".snipara");
3017
- const projectBindingPath = path.join(sniparaDir, "project");
3943
+ const sniparaDir = path4.join(process.cwd(), ".snipara");
3944
+ const projectBindingPath = path4.join(sniparaDir, "project");
3018
3945
  const projectBinding = `${answers.projectSlug}
3019
3946
  `;
3020
- const action = fs.existsSync(projectBindingPath) ? "Updated" : "Created";
3021
- fs.mkdirSync(sniparaDir, { recursive: true });
3022
- fs.writeFileSync(projectBindingPath, projectBinding);
3947
+ const action = fs4.existsSync(projectBindingPath) ? "Updated" : "Created";
3948
+ fs4.mkdirSync(sniparaDir, { recursive: true });
3949
+ fs4.writeFileSync(projectBindingPath, projectBinding);
3023
3950
  console.log(chalk.green(` \u2713 ${action} .snipara/project`));
3024
3951
  }
3025
3952
  async function createAgentTemplates(answers) {
3026
3953
  if (answers.profile === "runtime-only") {
3027
3954
  return;
3028
3955
  }
3956
+ if (managedSetupOnly(answers)) {
3957
+ createManagedLocalTemplates(answers);
3958
+ return;
3959
+ }
3029
3960
  console.log(chalk.bold("\nGenerating Agent Templates\n"));
3030
- const templatesDir = path.join(process.cwd(), ".snipara", "templates");
3031
- fs.mkdirSync(templatesDir, { recursive: true });
3961
+ const templatesDir = path4.join(process.cwd(), ".snipara", "templates");
3962
+ fs4.mkdirSync(templatesDir, { recursive: true });
3032
3963
  const templates = [
3033
- { path: path.join(templatesDir, "AGENTS.md"), content: buildAgentsMdTemplate(answers) },
3034
- { path: path.join(templatesDir, "CLAUDE.md"), content: buildClaudeMdTemplate(answers) },
3964
+ { path: path4.join(templatesDir, "AGENTS.md"), content: buildAgentsMdTemplate(answers) },
3965
+ { path: path4.join(templatesDir, "CLAUDE.md"), content: buildClaudeMdTemplate(answers) },
3035
3966
  {
3036
- path: path.join(templatesDir, "cursor-snipara.mdc"),
3967
+ path: path4.join(templatesDir, "cursor-snipara.mdc"),
3037
3968
  content: buildCursorRuleTemplate(answers)
3038
3969
  },
3039
3970
  {
3040
- path: path.join(templatesDir, "codex-config.toml"),
3971
+ path: path4.join(templatesDir, "codex-config.toml"),
3041
3972
  content: buildCodexConfigTemplate(answers)
3042
3973
  },
3043
3974
  {
3044
- path: path.join(templatesDir, "chatgpt-openai-mcp.json"),
3975
+ path: path4.join(templatesDir, "chatgpt-openai-mcp.json"),
3045
3976
  content: buildChatGptMcpTemplate(answers)
3046
3977
  },
3047
3978
  {
3048
- path: path.join(templatesDir, "gemini-mcp-reference.json"),
3979
+ path: path4.join(templatesDir, "gemini-mcp-reference.json"),
3049
3980
  content: buildGenericMcpReferenceTemplate(answers, "gemini")
3050
3981
  },
3051
3982
  {
3052
- path: path.join(templatesDir, "mistral-vibe-config.toml"),
3983
+ path: path4.join(templatesDir, "mistral-vibe-config.toml"),
3053
3984
  content: buildMistralVibeConfigTemplate(answers)
3054
3985
  },
3055
3986
  {
3056
- path: path.join(templatesDir, "mistral-le-chat-mcp.json"),
3987
+ path: path4.join(templatesDir, "mistral-le-chat-mcp.json"),
3057
3988
  content: buildMistralLeChatTemplate(answers)
3058
3989
  },
3059
3990
  {
3060
- path: path.join(templatesDir, "mistral-langchain-tools.ts"),
3991
+ path: path4.join(templatesDir, "mistral-langchain-tools.ts"),
3061
3992
  content: buildMistralLangChainToolsTemplate(answers)
3062
3993
  },
3063
3994
  {
3064
- path: path.join(templatesDir, "vscode-mcp-reference.json"),
3995
+ path: path4.join(templatesDir, "vscode-mcp-reference.json"),
3065
3996
  content: buildGenericMcpReferenceTemplate(answers, "vscode")
3066
3997
  },
3067
3998
  {
3068
- path: path.join(templatesDir, "continue-mcp-reference.json"),
3999
+ path: path4.join(templatesDir, "continue-mcp-reference.json"),
3069
4000
  content: buildGenericMcpReferenceTemplate(answers, "continue")
3070
4001
  },
3071
4002
  {
3072
- path: path.join(templatesDir, "custom-mcp-reference.json"),
4003
+ path: path4.join(templatesDir, "custom-mcp-reference.json"),
3073
4004
  content: buildGenericMcpReferenceTemplate(answers, "custom")
3074
4005
  }
3075
4006
  ];
3076
4007
  for (const template of templates) {
3077
- fs.writeFileSync(template.path, template.content);
4008
+ fs4.writeFileSync(template.path, template.content);
3078
4009
  }
3079
4010
  console.log(chalk.green(" Created .snipara/templates agent snippets"));
3080
- writeAgentSkillPackTemplates(path.join(templatesDir, "skills"), answers);
4011
+ writeAgentSkillPackTemplates(path4.join(templatesDir, "skills"), answers);
3081
4012
  writeIfMissing(
3082
- path.join(process.cwd(), "AGENTS.md"),
4013
+ path4.join(process.cwd(), "AGENTS.md"),
3083
4014
  buildAgentsMdTemplate(answers),
3084
4015
  "AGENTS.md"
3085
4016
  );
3086
4017
  writeIfMissing(
3087
- path.join(process.cwd(), "CLAUDE.md"),
4018
+ path4.join(process.cwd(), "CLAUDE.md"),
3088
4019
  buildClaudeMdTemplate(answers),
3089
4020
  "CLAUDE.md"
3090
4021
  );
3091
- const cursorRulesDir = path.join(process.cwd(), ".cursor", "rules");
3092
- fs.mkdirSync(cursorRulesDir, { recursive: true });
4022
+ const cursorRulesDir = path4.join(process.cwd(), ".cursor", "rules");
4023
+ fs4.mkdirSync(cursorRulesDir, { recursive: true });
3093
4024
  writeIfMissing(
3094
- path.join(cursorRulesDir, "snipara.mdc"),
4025
+ path4.join(cursorRulesDir, "snipara.mdc"),
3095
4026
  buildCursorRuleTemplate(answers),
3096
4027
  ".cursor/rules/snipara.mdc"
3097
4028
  );
3098
- const codexDir = path.join(process.cwd(), ".codex");
3099
- fs.mkdirSync(codexDir, { recursive: true });
3100
- writeIfMissing(
3101
- path.join(codexDir, "config.toml"),
3102
- buildCodexConfigTemplate(answers),
3103
- ".codex/config.toml"
3104
- );
4029
+ const codexDir = path4.join(process.cwd(), ".codex");
4030
+ if (answers.profile === "local-work") {
4031
+ configureManagedCodex(answers);
4032
+ } else {
4033
+ fs4.mkdirSync(codexDir, { recursive: true });
4034
+ writeIfMissing(path4.join(codexDir, "config.toml"), buildCodexConfigTemplate(answers), ".codex/config.toml");
4035
+ }
3105
4036
  if (answers.aiClient === "mistral") {
3106
- const vibeDir = path.join(process.cwd(), ".vibe");
3107
- fs.mkdirSync(vibeDir, { recursive: true });
4037
+ const vibeDir = path4.join(process.cwd(), ".vibe");
4038
+ fs4.mkdirSync(vibeDir, { recursive: true });
3108
4039
  writeIfMissing(
3109
- path.join(vibeDir, "config.toml"),
4040
+ path4.join(vibeDir, "config.toml"),
3110
4041
  buildMistralVibeConfigTemplate(answers),
3111
4042
  ".vibe/config.toml"
3112
4043
  );
3113
4044
  }
3114
4045
  ensureGitignoreEntry(".snipara/companion/doctor.json", "# Snipara generated validation output");
3115
4046
  }
4047
+ function configureManagedCodex(answers) {
4048
+ const manifest = answers.managedToolchain ?? managedToolchainStatus().manifest;
4049
+ if (!manifest) throw new Error("The managed local-work bundle must be prepared before configuring Codex");
4050
+ const native = managedNativeServer(answers);
4051
+ mergeManagedCodexConfig({
4052
+ file: path4.join(process.cwd(), ".codex/config.toml"),
4053
+ python: path4.join(manifest.root, "python/bin/python"),
4054
+ server: { command: native.command, args: native.args, ...native.env ? { env: native.env } : {} },
4055
+ initialContent: buildCodexHostedConfigTemplate(answers),
4056
+ env: { PATH: path4.dirname(process.execPath) + path4.delimiter + "/usr/bin:/bin", HOME: path4.join(manifest.root, "home"), PYTHONNOUSERSITE: "1" },
4057
+ validateExisting: (server) => validManagedBootstrap({ type: "stdio", ...server }, manifest)
4058
+ });
4059
+ }
4060
+ function createManagedLocalTemplates(answers) {
4061
+ const manifest = answers.managedToolchain ?? managedToolchainStatus().manifest;
4062
+ if (!manifest) throw new Error("Prepare local work before generating its agent instructions");
4063
+ const guide = `# Snipara local work
4064
+
4065
+ Use the user's existing agent and the local Snipara task workflow. No account or
4066
+ cloud subscription is required for local tasks and deterministic validation.
4067
+ Local CLI argv prefix: ${JSON.stringify([process.execPath, manifest.components.companion.command])}.
4068
+
4069
+ Bind each task to the current repository/worktree, explicit acceptance criteria
4070
+ and scope. Reuse its existing task and recorded progress rather than creating
4071
+ duplicate work. Run relevant authorized checks through the native local
4072
+ validation connection. Saved running markers, declarations and historical
4073
+ closure do not establish observed execution or verified completion.
4074
+
4075
+ Context is optional. If an existing repository connection reports connected,
4076
+ automatically use its authorized project context. A configured or pending
4077
+ connection alone does not establish authorization. Otherwise continue with local
4078
+ files and local work. Offer context setup when requested; never infer a hosted
4079
+ project from a directory name or a global API key. Do not read credential stores,
4080
+ copy provider/API secrets or automatically upload repository documents during setup.
4081
+ `;
4082
+ const templates = path4.join(process.cwd(), ".snipara/templates");
4083
+ fs4.mkdirSync(templates, { recursive: true });
4084
+ for (const name of ["AGENTS.md", "CLAUDE.md"]) {
4085
+ fs4.writeFileSync(path4.join(templates, name), guide);
4086
+ writeIfMissing(path4.join(process.cwd(), name), guide, name);
4087
+ }
4088
+ const cursor = path4.join(process.cwd(), ".cursor/rules");
4089
+ fs4.mkdirSync(cursor, { recursive: true });
4090
+ writeIfMissing(path4.join(cursor, "snipara.mdc"), guide, ".cursor/rules/snipara.mdc");
4091
+ fs4.writeFileSync(path4.join(templates, "codex-config.toml"), buildCodexConfigTemplate(answers));
4092
+ configureManagedCodex(answers);
4093
+ }
3116
4094
  function writeIfMissing(filePath, content, label) {
3117
- if (fs.existsSync(filePath)) {
4095
+ if (fs4.existsSync(filePath)) {
3118
4096
  console.log(chalk.dim(` ${label} already exists; merge from .snipara/templates if needed`));
3119
4097
  return;
3120
4098
  }
3121
- fs.writeFileSync(filePath, content);
4099
+ fs4.writeFileSync(filePath, content);
3122
4100
  console.log(chalk.green(` Created ${label}`));
3123
4101
  }
3124
4102
  function buildMcpEndpoint(answers) {
@@ -3133,11 +4111,11 @@ function buildHostedMcpEndpoint(answers) {
3133
4111
  function writeAgentSkillPackTemplates(skillsDir, answers) {
3134
4112
  const skills = buildSniparaSkillPackTemplates(answers);
3135
4113
  for (const skill of skills) {
3136
- const skillDir = path.join(skillsDir, skill.name);
3137
- const agentsDir = path.join(skillDir, "agents");
3138
- fs.mkdirSync(agentsDir, { recursive: true });
3139
- fs.writeFileSync(path.join(skillDir, "SKILL.md"), skill.skillMd);
3140
- fs.writeFileSync(path.join(agentsDir, "openai.yaml"), buildSkillOpenAiYaml(skill, answers));
4114
+ const skillDir = path4.join(skillsDir, skill.name);
4115
+ const agentsDir = path4.join(skillDir, "agents");
4116
+ fs4.mkdirSync(agentsDir, { recursive: true });
4117
+ fs4.writeFileSync(path4.join(skillDir, "SKILL.md"), skill.skillMd);
4118
+ fs4.writeFileSync(path4.join(agentsDir, "openai.yaml"), buildSkillOpenAiYaml(skill, answers));
3141
4119
  }
3142
4120
  console.log(chalk.green(" Created .snipara/templates/skills Snipara skill pack"));
3143
4121
  }
@@ -3510,8 +4488,7 @@ Agent task lifecycle:
3510
4488
  commit SHA, indexing time, included file sample, coverage, freshness warnings,
3511
4489
  and sync guidance before relying on it. If \`git status --short\` is dirty,
3512
4490
  warn that the indexed graph may not include local edits.
3513
- 12. Use Snipara Sandbox only when sandboxed execution, repeatable validation, or
3514
- isolated transformations materially help.
4491
+ 12. Before executable repository tests, probe the required Snipara Sandbox capabilities using \`validation gate\` with an explicit request and workflow/lot scope. If absent, inaccessible or incompatible, ask the user to choose another test environment, install/configure Sandbox, or continue without Sandbox (local tests versus no tests). Never fall back silently or treat no response as approval; preserve the scoped choice and proof limits across resume. Static reads and documentation do not require a container.
3515
4492
  13. If a broad query is slow, retry once with a narrow keyword query before
3516
4493
  falling back to local search.
3517
4494
  14. When generated hooks are enabled, \`snipara-companion pre-tool\`, \`post-tool\`,
@@ -3562,7 +4539,7 @@ ${EXPERIENCE_GOAL_SECTION}
3562
4539
  - If a broad query is slow, retry once with a narrow keyword query before falling back to local search.
3563
4540
  - When the plan is visible and \`snipara-companion\` is installed, keep the machine plan in JSON, use \`snipara-companion workflow start\`, \`phase-start\`, and \`phase-commit\` per phase, and after \`workflow resume\` rerun \`workflow phase-start\` before editing again.
3564
4541
  - If generated hooks are enabled, \`snipara-companion pre-tool\`, \`post-tool\`, and \`stuck-guard status\` provide runtime Rescue Pack checks when the agent loops on failures or empty searches.
3565
- - Use Snipara Sandbox only when sandboxed execution or repeatable validation materially helps. For runtime-bound phases, capture compact rehydratable state with \`workflow runtime-checkpoint <phase_id> --summary "<state>" --rehydrate-file <state.json>\`. Then \`workflow resume\` restores workflow and memory continuity plus the recorded Sandbox binding and prints a reattach or rehydrate plan. It does not snapshot or exactly restore a live Snipara Sandbox or REPL process.
4542
+ - Before executable repository tests, run \`validation gate\` with an explicit request and workflow/lot scope. If unavailable or incompatible, ask the user to choose another environment, install/configure Sandbox, or continue without Sandbox (local tests versus no tests). Never fall back silently or infer approval. Preserve the scoped choice and proof requirements across resume. For runtime-bound phases, capture compact rehydratable state with \`workflow runtime-checkpoint <phase_id> --summary "<state>" --rehydrate-file <state.json>\`. Then \`workflow resume\` restores workflow and memory continuity plus the recorded Sandbox binding and prints a reattach or rehydrate plan. It does not snapshot or exactly restore a live Snipara Sandbox or REPL process.
3566
4543
  - End substantial work with \`snipara_end_of_task_commit\` when available; for managed workflows, end every phase with \`snipara-companion workflow phase-commit\` and the task with \`snipara-companion final-commit\`.
3567
4544
  - Use \`snipara_remember_if_novel\` or \`snipara_remember\` only for narrow durable memories.
3568
4545
  - When remembered knowledge is obsolete or replaced and \`snipara-companion\` is installed, use \`snipara-companion memory invalidate <memory-id>\` or \`snipara-companion memory supersede <old-memory-id> <new-memory-id>\`; otherwise use hosted \`snipara_memory_invalidate\` or \`snipara_memory_supersede\`.
@@ -3603,18 +4580,24 @@ ${EXPERIENCE_GOAL_SECTION}
3603
4580
  - If a broad query is slow, retry once with a narrow keyword query before falling back to local search.
3604
4581
  - When \`snipara-companion\` is installed and the LLM has a visible multi-phase plan, keep the machine plan in JSON, use \`snipara-companion workflow start\`, \`phase-start\`, and \`phase-commit\` per phase, and after \`workflow resume\` rerun \`workflow phase-start\` before editing again.
3605
4582
  - Generated hooks can call \`snipara-companion pre-tool\`, \`post-tool\`, and \`stuck-guard status\` so Rescue Packs are runtime-triggered rather than relying only on instructions.
3606
- - Use Snipara Sandbox only when sandboxed execution or repeatable validation materially helps. For runtime-bound phases, capture compact rehydratable state with \`workflow runtime-checkpoint <phase_id> --summary "<state>" --rehydrate-file <state.json>\`. Then \`workflow resume\` restores workflow and memory continuity plus the recorded Sandbox binding and prints a reattach or rehydrate plan. It does not snapshot or exactly restore a live Snipara Sandbox or REPL process.
4583
+ - Before executable repository tests, run \`validation gate\` with an explicit request and workflow/lot scope. If unavailable or incompatible, ask the user to choose another environment, install/configure Sandbox, or continue without Sandbox (local tests versus no tests). Never fall back silently or infer approval. Preserve the scoped choice and proof requirements across resume. For runtime-bound phases, capture compact rehydratable state with \`workflow runtime-checkpoint <phase_id> --summary "<state>" --rehydrate-file <state.json>\`. Then \`workflow resume\` restores workflow and memory continuity plus the recorded Sandbox binding and prints a reattach or rehydrate plan. It does not snapshot or exactly restore a live Snipara Sandbox or REPL process.
3607
4584
  - End substantial work with \`snipara_end_of_task_commit\` when available; for managed workflows, end every phase with \`snipara-companion workflow phase-commit\` and the task with \`snipara-companion final-commit\`. Use \`snipara_remember_if_novel\` or \`snipara_remember\` only for narrow durable memories. When remembered knowledge is obsolete or replaced, use \`snipara-companion memory invalidate <memory-id>\` or \`snipara-companion memory supersede <old-memory-id> <new-memory-id>\` when companion is installed; otherwise use hosted \`snipara_memory_invalidate\` or \`snipara_memory_supersede\`. Never store secrets.
3608
4585
  `;
3609
4586
  }
3610
- function buildCodexConfigTemplate(answers) {
3611
- return `[mcp_servers.snipara]
4587
+ function buildCodexHostedConfigTemplate(answers) {
4588
+ return managedSetupOnly(answers) ? "" : `[mcp_servers.snipara]
3612
4589
  type = "streamable_http"
3613
4590
  url = "${buildHostedMcpEndpoint(answers)}"
3614
4591
  bearer_token_env_var = "SNIPARA_API_KEY"
3615
4592
  http_headers = { "X-Snipara-Session-Id" = "${getCorrelationSessionId(answers)}" }
3616
4593
  `;
3617
4594
  }
4595
+ function buildCodexConfigTemplate(answers) {
4596
+ const hosted = buildCodexHostedConfigTemplate(answers);
4597
+ if (answers.profile !== "local-work") return hosted;
4598
+ const native = managedNativeServer(answers);
4599
+ return hosted + "\n" + managedCodexSnippet({ command: native.command, args: native.args });
4600
+ }
3618
4601
  function buildChatGptMcpTemplate(answers) {
3619
4602
  return `${JSON.stringify(
3620
4603
  {
@@ -3782,11 +4765,11 @@ function buildGenericMcpReferenceTemplate(answers, client) {
3782
4765
  `;
3783
4766
  }
3784
4767
  function ensureGitignoreEntry(entry, comment) {
3785
- const gitignorePath = path.join(process.cwd(), ".gitignore");
3786
- if (fs.existsSync(gitignorePath)) {
3787
- const gitignore = fs.readFileSync(gitignorePath, "utf-8");
4768
+ const gitignorePath = path4.join(process.cwd(), ".gitignore");
4769
+ if (fs4.existsSync(gitignorePath)) {
4770
+ const gitignore = fs4.readFileSync(gitignorePath, "utf-8");
3788
4771
  if (!gitignore.includes(entry)) {
3789
- fs.appendFileSync(gitignorePath, `
4772
+ fs4.appendFileSync(gitignorePath, `
3790
4773
  ${comment}
3791
4774
  ${entry}
3792
4775
  `);
@@ -3794,27 +4777,31 @@ ${entry}
3794
4777
  }
3795
4778
  return;
3796
4779
  }
3797
- fs.writeFileSync(gitignorePath, `${comment}
4780
+ fs4.writeFileSync(gitignorePath, `${comment}
3798
4781
  ${entry}
3799
4782
  `);
3800
4783
  console.log(chalk.green(` \u2713 Created .gitignore with ${entry}`));
3801
4784
  }
3802
4785
  async function createClaudeDesktopConfig(answers) {
3803
- const configDir = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path.join(homedir(), "AppData", "Roaming", "Claude") : path.join(homedir(), ".config", "claude");
3804
- const configPath = path.join(configDir, "claude_desktop_config.json");
4786
+ const configDir = process.platform === "darwin" ? path4.join(homedir2(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path4.join(homedir2(), "AppData", "Roaming", "Claude") : path4.join(homedir2(), ".config", "claude");
4787
+ const configPath = path4.join(configDir, "claude_desktop_config.json");
3805
4788
  console.log(chalk.dim(`
3806
4789
  Claude Desktop config: ${configPath}`));
3807
4790
  try {
3808
- if (!fs.existsSync(configDir)) {
3809
- fs.mkdirSync(configDir, { recursive: true });
4791
+ if (answers.profile === "local-work") assertManagedMcpConfig(configPath);
4792
+ if (!fs4.existsSync(configDir)) {
4793
+ fs4.mkdirSync(configDir, { recursive: true });
3810
4794
  }
3811
4795
  let config = {};
3812
- if (fs.existsSync(configPath)) {
3813
- config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
4796
+ if (fs4.existsSync(configPath)) {
4797
+ config = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
3814
4798
  }
3815
4799
  config.mcpServers = config.mcpServers || {};
3816
- config.mcpServers.snipara = buildHostedMcpServer(answers);
3817
- if (answers.installRuntime) {
4800
+ if (!(answers.profile === "local-work" && (managedSetupOnly(answers) || config.mcpServers["snipara-repository"] || asObjectRecord(asObjectRecord(readJsonFile(path4.join(process.cwd(), ".mcp.json"))).mcpServers)["snipara-repository"])))
4801
+ config.mcpServers.snipara = buildHostedMcpServer(answers);
4802
+ if (answers.profile === "local-work") {
4803
+ config.mcpServers["snipara-local"] = managedNativeServer(answers);
4804
+ } else if (answers.installRuntime) {
3818
4805
  config.mcpServers[SANDBOX_SERVER_NAME] = {
3819
4806
  command: detectSandboxCommand() || SANDBOX_COMMAND_NAME,
3820
4807
  args: ["mcp-serve"],
@@ -3824,18 +4811,19 @@ async function createClaudeDesktopConfig(answers) {
3824
4811
  }
3825
4812
  };
3826
4813
  }
3827
- fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
4814
+ fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
3828
4815
  console.log(chalk.green(" \u2713 Updated Claude Desktop config"));
3829
4816
  } catch (error) {
4817
+ if (answers.profile === "local-work") throw error;
3830
4818
  console.log(chalk.yellow(` \u26A0\uFE0F Could not update Claude Desktop config: ${error}`));
3831
4819
  }
3832
4820
  }
3833
4821
  async function installClaudeCodeHooks(answers) {
3834
4822
  console.log(chalk.bold("\n\u{1FA9D} Installing Claude Code Hooks\n"));
3835
- const claudeDir = path.join(process.cwd(), ".claude");
3836
- const hooksDir = path.join(claudeDir, "hooks");
3837
- if (!fs.existsSync(hooksDir)) {
3838
- fs.mkdirSync(hooksDir, { recursive: true });
4823
+ const claudeDir = path4.join(process.cwd(), ".claude");
4824
+ const hooksDir = path4.join(claudeDir, "hooks");
4825
+ if (!fs4.existsSync(hooksDir)) {
4826
+ fs4.mkdirSync(hooksDir, { recursive: true });
3839
4827
  }
3840
4828
  const hooks = [
3841
4829
  {
@@ -3856,16 +4844,16 @@ async function installClaudeCodeHooks(answers) {
3856
4844
  }
3857
4845
  ];
3858
4846
  for (const hook of hooks) {
3859
- const hookPath = path.join(hooksDir, hook.name);
3860
- fs.writeFileSync(hookPath, hook.content);
3861
- fs.chmodSync(hookPath, "755");
4847
+ const hookPath = path4.join(hooksDir, hook.name);
4848
+ fs4.writeFileSync(hookPath, hook.content);
4849
+ fs4.chmodSync(hookPath, "755");
3862
4850
  console.log(chalk.green(` \u2713 Created ${hook.name}`));
3863
4851
  }
3864
- const settingsPath = path.join(claudeDir, "settings.json");
4852
+ const settingsPath = path4.join(claudeDir, "settings.json");
3865
4853
  let settings = {};
3866
- if (fs.existsSync(settingsPath)) {
4854
+ if (fs4.existsSync(settingsPath)) {
3867
4855
  try {
3868
- settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
4856
+ settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
3869
4857
  } catch {
3870
4858
  settings = {};
3871
4859
  }
@@ -3929,13 +4917,13 @@ async function installClaudeCodeHooks(answers) {
3929
4917
  for (const { event, entry } of hookEntries) {
3930
4918
  const eventHooks = Array.isArray(hooksConfig[event]) ? hooksConfig[event] : [];
3931
4919
  const expectedCommand = entry.hooks[0].command;
3932
- const exists = eventHooks.some((hook) => hookConfigEntryHasCommand(hook, expectedCommand));
3933
- if (!exists) {
4920
+ const exists2 = eventHooks.some((hook) => hookConfigEntryHasCommand(hook, expectedCommand));
4921
+ if (!exists2) {
3934
4922
  eventHooks.push(entry);
3935
4923
  hooksConfig[event] = eventHooks;
3936
4924
  }
3937
4925
  }
3938
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
4926
+ fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
3939
4927
  console.log(chalk.green(" \u2713 Updated .claude/settings.json hooks"));
3940
4928
  }
3941
4929
  function createClaudeProjectHookCommand(scriptName) {
@@ -4075,9 +5063,9 @@ fi
4075
5063
  }
4076
5064
  async function updateEnvFiles(answers) {
4077
5065
  console.log(chalk.bold("\n\u{1F4DD} Updating Environment Files\n"));
4078
- const envExamplePath = path.join(process.cwd(), ".env.example");
4079
- const envLocalPath = path.join(process.cwd(), ".env.local");
4080
- const envPath = path.join(process.cwd(), ".env");
5066
+ const envExamplePath = path4.join(process.cwd(), ".env.example");
5067
+ const envLocalPath = path4.join(process.cwd(), ".env.local");
5068
+ const envPath = path4.join(process.cwd(), ".env");
4081
5069
  let envVars = "";
4082
5070
  let envExampleVars = "";
4083
5071
  if (answers.profile !== "runtime-only") {
@@ -4115,26 +5103,26 @@ ${llmVarName}=your_${answers.llmProvider}_api_key_here`;
4115
5103
  console.log(chalk.dim(" No environment variables to save"));
4116
5104
  return;
4117
5105
  }
4118
- if (envExampleVars && fs.existsSync(envExamplePath)) {
4119
- const content = fs.readFileSync(envExamplePath, "utf-8");
5106
+ if (envExampleVars && fs4.existsSync(envExamplePath)) {
5107
+ const content = fs4.readFileSync(envExamplePath, "utf-8");
4120
5108
  const missingLines = envExampleVars.split("\n").filter((line) => {
4121
5109
  const match = line.match(/^([A-Z0-9_]+)=/);
4122
5110
  return Boolean(match && !content.includes(`${match[1]}=`));
4123
5111
  });
4124
5112
  if (missingLines.length > 0) {
4125
- fs.appendFileSync(envExamplePath, `
5113
+ fs4.appendFileSync(envExamplePath, `
4126
5114
 
4127
5115
  ${missingLines.join("\n")}
4128
5116
  `);
4129
5117
  console.log(chalk.green(" \u2713 Updated .env.example"));
4130
5118
  }
4131
5119
  } else if (envExampleVars) {
4132
- fs.writeFileSync(envExamplePath, envExampleVars + "\n");
5120
+ fs4.writeFileSync(envExamplePath, envExampleVars + "\n");
4133
5121
  console.log(chalk.green(" \u2713 Created .env.example"));
4134
5122
  }
4135
- const targetEnvPath = fs.existsSync(envLocalPath) ? envLocalPath : envPath;
4136
- if (fs.existsSync(targetEnvPath)) {
4137
- const content = fs.readFileSync(targetEnvPath, "utf-8");
5123
+ const targetEnvPath = fs4.existsSync(envLocalPath) ? envLocalPath : envPath;
5124
+ if (fs4.existsSync(targetEnvPath)) {
5125
+ const content = fs4.readFileSync(targetEnvPath, "utf-8");
4138
5126
  const missingLines = envVars.split("\n").filter((line) => {
4139
5127
  const match = line.match(/^([A-Z0-9_]+)=/);
4140
5128
  if (!match) {
@@ -4146,26 +5134,26 @@ ${missingLines.join("\n")}
4146
5134
  return !content.includes(`${match[1]}=`);
4147
5135
  });
4148
5136
  if (missingLines.length > 0) {
4149
- fs.appendFileSync(targetEnvPath, `
5137
+ fs4.appendFileSync(targetEnvPath, `
4150
5138
 
4151
5139
  ${missingLines.join("\n")}
4152
5140
  `);
4153
- console.log(chalk.green(` \u2713 Updated ${path.basename(targetEnvPath)}`));
5141
+ console.log(chalk.green(` \u2713 Updated ${path4.basename(targetEnvPath)}`));
4154
5142
  }
4155
5143
  return;
4156
5144
  }
4157
5145
  if (!answers.apiKey.includes("YOUR_") && answers.profile !== "runtime-only" || answers.llmApiKey) {
4158
- fs.writeFileSync(targetEnvPath, envVars + "\n");
4159
- console.log(chalk.green(` \u2713 Created ${path.basename(targetEnvPath)}`));
5146
+ fs4.writeFileSync(targetEnvPath, envVars + "\n");
5147
+ console.log(chalk.green(` \u2713 Created ${path4.basename(targetEnvPath)}`));
4160
5148
  }
4161
5149
  }
4162
5150
  async function createCompanionConfig(answers) {
4163
5151
  if (!answers.installCompanion || answers.apiKeyType === "none" || !answers.apiKey || answers.apiKey.includes("YOUR_")) {
4164
5152
  return;
4165
5153
  }
4166
- const configDir = path.join(process.cwd(), ".snipara", "companion");
4167
- const configPath = path.join(configDir, "config.json");
4168
- fs.mkdirSync(configDir, { recursive: true });
5154
+ const configDir = path4.join(process.cwd(), ".snipara", "companion");
5155
+ const configPath = path4.join(configDir, "config.json");
5156
+ fs4.mkdirSync(configDir, { recursive: true });
4169
5157
  const existingWorkspaceConfig = readCompanionConfigFile(configPath);
4170
5158
  const workspaceConfig = {
4171
5159
  ...existingWorkspaceConfig,
@@ -4175,7 +5163,7 @@ async function createCompanionConfig(answers) {
4175
5163
  sessionId: getCorrelationSessionId(answers),
4176
5164
  client: getAutomationClientId(answers.aiClient)
4177
5165
  };
4178
- fs.writeFileSync(configPath, JSON.stringify(workspaceConfig, null, 2) + "\n");
5166
+ fs4.writeFileSync(configPath, JSON.stringify(workspaceConfig, null, 2) + "\n");
4179
5167
  ensureGitignoreEntry(".snipara/companion/config.json", "# Snipara local companion config");
4180
5168
  console.log(chalk.green(" \u2713 Configured local companion config"));
4181
5169
  }
@@ -4245,11 +5233,11 @@ async function syncRemoteAutomationClient(answers, options) {
4245
5233
  }
4246
5234
  }
4247
5235
  function readCompanionConfigFile(filePath) {
4248
- if (!fs.existsSync(filePath)) {
5236
+ if (!fs4.existsSync(filePath)) {
4249
5237
  return {};
4250
5238
  }
4251
5239
  try {
4252
- const parsed = JSON.parse(fs.readFileSync(filePath, "utf-8"));
5240
+ const parsed = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
4253
5241
  return typeof parsed === "object" && parsed !== null ? parsed : {};
4254
5242
  } catch {
4255
5243
  return {};
@@ -4763,13 +5751,13 @@ ${guidance.map((line) => `- ${line}`).join("\n")}
4763
5751
  }
4764
5752
  async function createCompanionPack(answers) {
4765
5753
  console.log(chalk.bold("\n\u{1F5C2}\uFE0F Generating Companion Pack\n"));
4766
- const packDir = path.join(process.cwd(), ".snipara", "companion");
5754
+ const packDir = path4.join(process.cwd(), ".snipara", "companion");
4767
5755
  const commands = getCompanionCommands(answers);
4768
5756
  const guidance = [UNIVERSAL_CLIENT_GUIDANCE, ...getClientGuidance(answers.aiClient)];
4769
5757
  const detection = detectEnvironment();
4770
5758
  const clientPackFilename = getClientPackFilename(answers.aiClient);
4771
5759
  const deploymentLabel = getDeploymentLabel(answers.deployment);
4772
- fs.mkdirSync(packDir, { recursive: true });
5760
+ fs4.mkdirSync(packDir, { recursive: true });
4773
5761
  const readme = `# Snipara Companion Pack
4774
5762
 
4775
5763
  This project uses ${deploymentLabel} as a project intelligence layer with source-backed context, reviewed memory, and local companion workflow helpers.
@@ -4916,12 +5904,12 @@ ${item.purpose}`
4916
5904
  guidance,
4917
5905
  detection
4918
5906
  };
4919
- fs.writeFileSync(path.join(packDir, "README.md"), readme + "\n");
4920
- fs.writeFileSync(
4921
- path.join(packDir, clientPackFilename),
5907
+ fs4.writeFileSync(path4.join(packDir, "README.md"), readme + "\n");
5908
+ fs4.writeFileSync(
5909
+ path4.join(packDir, clientPackFilename),
4922
5910
  buildClientPackMarkdown(answers, commands, guidance) + "\n"
4923
5911
  );
4924
- fs.writeFileSync(path.join(packDir, "commands.json"), JSON.stringify(pack, null, 2) + "\n");
5912
+ fs4.writeFileSync(path4.join(packDir, "commands.json"), JSON.stringify(pack, null, 2) + "\n");
4925
5913
  console.log(chalk.green(" \u2713 Created .snipara/companion/README.md"));
4926
5914
  console.log(chalk.green(` \u2713 Created .snipara/companion/${clientPackFilename}`));
4927
5915
  console.log(chalk.green(" \u2713 Created .snipara/companion/commands.json"));
@@ -4935,8 +5923,8 @@ async function runSandboxInit() {
4935
5923
  `
4936
5924
  )
4937
5925
  );
4938
- return new Promise((resolve) => {
4939
- const child = spawn(sandboxCommand, ["init"], {
5926
+ return new Promise((resolve2) => {
5927
+ const child = spawn2(sandboxCommand, ["init"], {
4940
5928
  stdio: "inherit",
4941
5929
  shell: true
4942
5930
  });
@@ -4948,7 +5936,7 @@ async function runSandboxInit() {
4948
5936
  \u26A0\uFE0F ${sandboxCommand} init exited with code ${code}`));
4949
5937
  console.log(chalk.dim(` You can run '${sandboxCommand} init' manually later.`));
4950
5938
  }
4951
- resolve();
5939
+ resolve2();
4952
5940
  });
4953
5941
  child.on("error", (error) => {
4954
5942
  console.log(chalk.yellow(`
@@ -4956,7 +5944,7 @@ async function runSandboxInit() {
4956
5944
  console.log(
4957
5945
  chalk.dim(` Run '${SANDBOX_COMMAND_NAME} init' manually after installation completes.`)
4958
5946
  );
4959
- resolve();
5947
+ resolve2();
4960
5948
  });
4961
5949
  });
4962
5950
  }
@@ -4993,8 +5981,8 @@ function buildGitHubAutomationUrl(answers) {
4993
5981
  function getClaudeClientChecks(answers, detection) {
4994
5982
  const checks = [];
4995
5983
  const claudePaths = getClaudeConfigPaths();
4996
- const projectConfig = readJsonFile(path.join(process.cwd(), ".mcp.json"));
4997
- const projectSettings = readJsonFile(path.join(process.cwd(), ".claude", "settings.json"));
5984
+ const projectConfig = readJsonFile(path4.join(process.cwd(), ".mcp.json"));
5985
+ const projectSettings = readJsonFile(path4.join(process.cwd(), ".claude", "settings.json"));
4998
5986
  const codeSettings = readJsonFile(claudePaths.codeSettingsPath);
4999
5987
  const desktopConfig = readJsonFile(claudePaths.desktopConfigPath);
5000
5988
  const projectHasSnipara = containsSniparaMcpReference(projectConfig);
@@ -5048,11 +6036,11 @@ function getClaudeClientChecks(answers, detection) {
5048
6036
  return checks;
5049
6037
  }
5050
6038
  function readJsonFile(filePath) {
5051
- if (!fs.existsSync(filePath)) {
6039
+ if (!fs4.existsSync(filePath)) {
5052
6040
  return null;
5053
6041
  }
5054
6042
  try {
5055
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
6043
+ return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
5056
6044
  } catch {
5057
6045
  return null;
5058
6046
  }
@@ -5145,13 +6133,35 @@ function compareSemver(left, right) {
5145
6133
  async function runValidation(answers, includeHostedCheck, options = {}) {
5146
6134
  console.log(chalk.bold("\n\u{1FA7A} Running Validation\n"));
5147
6135
  const checks = [];
5148
- const detection = detectEnvironment();
6136
+ const detection = detectEnvironment(answers.profile === "local-work");
6137
+ if (answers.profile === "local-work") {
6138
+ try {
6139
+ const managed = managedToolchainStatus();
6140
+ if (!managed.manifest) throw new Error("Managed local-work bundle is absent");
6141
+ answers.managedToolchain = managed.manifest;
6142
+ detection.tooling.companionCli = true;
6143
+ detection.tooling.runtimeCli = true;
6144
+ detection.tooling.orchestratorCli = true;
6145
+ checks.push({ name: "managed_toolchain", status: "pass", detail: `Private compatible bundle ${managed.manifest.bundleId}; installation is not project activation` });
6146
+ const native = asObjectRecord(asObjectRecord(readJsonFile(path4.join(process.cwd(), ".mcp.json"))).mcpServers)["snipara-local"];
6147
+ checks.push({
6148
+ name: "managed_native_config",
6149
+ status: isManagedNativeServer(native) && validManagedBootstrap(native, managed.manifest) ? "pass" : "fail",
6150
+ detail: "Native launcher configuration is checked against retained owned bundles; live client connectivity is separate"
6151
+ });
6152
+ } catch {
6153
+ detection.tooling.companionCli = false;
6154
+ detection.tooling.runtimeCli = false;
6155
+ detection.tooling.orchestratorCli = false;
6156
+ checks.push({ name: "managed_toolchain", status: "fail", detail: "Managed bundle is absent, changed or incompatible; existing state was preserved" });
6157
+ }
6158
+ }
5149
6159
  const targetClient = options.client || answers.aiClient;
5150
6160
  const effectiveAnswers = { ...answers, aiClient: targetClient };
5151
6161
  checks.push({
5152
6162
  name: "project_slug",
5153
- status: answers.projectSlug ? "pass" : "fail",
5154
- detail: answers.projectSlug || "Project slug missing"
6163
+ status: managedSetupOnly(answers) || answers.projectSlug ? "pass" : "fail",
6164
+ detail: managedSetupOnly(answers) ? "A hosted project slug is optional for local work" : answers.projectSlug || "Project slug missing"
5155
6165
  });
5156
6166
  const configuredSlugSignals = collectConfiguredProjectSlugSignals(detection.existingConfig);
5157
6167
  const presentSlugSignals = configuredSlugSignals.filter((signal) => signal.slug);
@@ -5176,7 +6186,7 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
5176
6186
  checks.push({
5177
6187
  name: "companion_cli",
5178
6188
  status: answers.installCompanion ? detection.tooling.companionCli ? "pass" : "fail" : "pass",
5179
- detail: answers.installCompanion && !detection.tooling.companionCli ? getCompanionRequirementMessage() : "Companion CLI ready or not requested"
6189
+ detail: answers.installCompanion && !detection.tooling.companionCli ? answers.profile === "local-work" ? "Managed Companion unavailable; repair local-work" : getCompanionRequirementMessage() : "Companion CLI ready or not requested"
5180
6190
  });
5181
6191
  checks.push({
5182
6192
  name: "sandbox_cli",
@@ -5219,10 +6229,12 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
5219
6229
  status: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeProjectSettings ? "pass" : "warn" : "pass",
5220
6230
  detail: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeProjectSettings ? "Project .claude/settings.json hooks present" : "Project .claude/settings.json hooks missing" : "Hooks not requested"
5221
6231
  });
5222
- if (targetClient === "claude-code" || targetClient === "claude-desktop") {
6232
+ if (managedSetupOnly(answers)) {
6233
+ checks.push({ name: "repository_context", status: "pass", detail: hasBoundLocalWorkSource(answers.profile) ? "Existing source connection preserved; authorization is observed by its native broker" : "Optional project context is not configured; local work needs no account" });
6234
+ } else if (targetClient === "claude-code" || targetClient === "claude-desktop") {
5223
6235
  checks.push(...getClaudeClientChecks(effectiveAnswers, detection));
5224
6236
  }
5225
- if (options.includeNpmFreshness) {
6237
+ if (options.includeNpmFreshness && !managedSetupOnly(answers)) {
5226
6238
  checks.push(getCreateSniparaPackageFreshnessCheck());
5227
6239
  }
5228
6240
  const githubRepo = answers.githubRepo || detectGitRepository();
@@ -5231,7 +6243,9 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
5231
6243
  status: answers.configureGitHub ? "pass" : githubRepo ? "warn" : "pass",
5232
6244
  detail: answers.configureGitHub ? `GitHub App setup link generated for ${githubRepo?.fullName || "detected repo"}; PR Answer Packs are enabled after browser approval` : githubRepo ? `GitHub repo ${githubRepo.fullName} detected; run ${CREATE_SNIPARA_NPX_COMMAND} --github to connect PR Answer Packs` : "No GitHub repo detected; PR Answer Packs require the GitHub App setup path"
5233
6245
  });
5234
- if (includeHostedCheck && answers.apiKeyType !== "none" && !answers.apiKey.includes("YOUR_")) {
6246
+ if (includeHostedCheck && managedSetupOnly(answers)) {
6247
+ checks.push({ name: "hosted_connection", status: "pass", detail: hasBoundLocalWorkSource(answers.profile) ? "Existing native broker automatically reuses authorized project context; authorization was not probed by setup" : "No hosted connection is required for local work" });
6248
+ } else if (includeHostedCheck && answers.apiKeyType !== "none" && !answers.apiKey.includes("YOUR_")) {
5235
6249
  checks.push(...await testHostedConnection(answers));
5236
6250
  } else if (includeHostedCheck) {
5237
6251
  checks.push({
@@ -5251,29 +6265,34 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
5251
6265
  };
5252
6266
  }
5253
6267
  function writeDoctorReport(report) {
5254
- const reportDir = path.join(process.cwd(), ".snipara", "companion");
5255
- fs.mkdirSync(reportDir, { recursive: true });
5256
- fs.writeFileSync(path.join(reportDir, "doctor.json"), JSON.stringify(report, null, 2) + "\n");
6268
+ const reportDir = path4.join(process.cwd(), ".snipara", "companion");
6269
+ fs4.mkdirSync(reportDir, { recursive: true });
6270
+ fs4.writeFileSync(path4.join(reportDir, "doctor.json"), JSON.stringify(report, null, 2) + "\n");
5257
6271
  console.log(chalk.green(" \u2713 Wrote .snipara/companion/doctor.json"));
5258
6272
  }
5259
6273
  function getCodexConfigCheck(answers) {
6274
+ if (managedSetupOnly(answers)) return {
6275
+ name: "codex_config",
6276
+ status: "warn",
6277
+ detail: "Managed local launcher is configured; restart Codex and observe connection status. Optional context authorization remains separate"
6278
+ };
5260
6279
  const candidates = [
5261
6280
  {
5262
6281
  label: ".codex/config.toml",
5263
- filePath: path.join(process.cwd(), ".codex", "config.toml")
6282
+ filePath: path4.join(process.cwd(), ".codex", "config.toml")
5264
6283
  },
5265
6284
  {
5266
6285
  label: ".snipara/templates/codex-config.toml",
5267
- filePath: path.join(process.cwd(), ".snipara", "templates", "codex-config.toml")
6286
+ filePath: path4.join(process.cwd(), ".snipara", "templates", "codex-config.toml")
5268
6287
  },
5269
6288
  {
5270
6289
  label: "~/.codex/config.toml",
5271
- filePath: path.join(homedir(), ".codex", "config.toml")
6290
+ filePath: path4.join(homedir2(), ".codex", "config.toml")
5272
6291
  }
5273
6292
  ];
5274
- const present = candidates.filter((candidate) => fs.existsSync(candidate.filePath)).map((candidate) => ({
6293
+ const present = candidates.filter((candidate) => fs4.existsSync(candidate.filePath)).map((candidate) => ({
5275
6294
  label: candidate.label,
5276
- content: fs.readFileSync(candidate.filePath, "utf-8")
6295
+ content: fs4.readFileSync(candidate.filePath, "utf-8")
5277
6296
  }));
5278
6297
  if (present.length === 0) {
5279
6298
  return {
@@ -5325,20 +6344,20 @@ function getMistralConfigCheck(answers) {
5325
6344
  const candidates = [
5326
6345
  {
5327
6346
  label: ".vibe/config.toml",
5328
- filePath: path.join(process.cwd(), ".vibe", "config.toml")
6347
+ filePath: path4.join(process.cwd(), ".vibe", "config.toml")
5329
6348
  },
5330
6349
  {
5331
6350
  label: ".snipara/templates/mistral-vibe-config.toml",
5332
- filePath: path.join(process.cwd(), ".snipara", "templates", "mistral-vibe-config.toml")
6351
+ filePath: path4.join(process.cwd(), ".snipara", "templates", "mistral-vibe-config.toml")
5333
6352
  },
5334
6353
  {
5335
6354
  label: "~/.vibe/config.toml",
5336
- filePath: path.join(homedir(), ".vibe", "config.toml")
6355
+ filePath: path4.join(homedir2(), ".vibe", "config.toml")
5337
6356
  }
5338
6357
  ];
5339
- const present = candidates.filter((candidate) => fs.existsSync(candidate.filePath)).map((candidate) => ({
6358
+ const present = candidates.filter((candidate) => fs4.existsSync(candidate.filePath)).map((candidate) => ({
5340
6359
  label: candidate.label,
5341
- content: fs.readFileSync(candidate.filePath, "utf-8")
6360
+ content: fs4.readFileSync(candidate.filePath, "utf-8")
5342
6361
  }));
5343
6362
  if (present.length === 0) {
5344
6363
  return {
@@ -5602,7 +6621,7 @@ function printActivationSummary(manifest) {
5602
6621
  console.log(
5603
6622
  chalk.dim(
5604
6623
  `
5605
- Activation manifest: ${path.relative(process.cwd(), getActivationManifestPath())}`
6624
+ Activation manifest: ${path4.relative(process.cwd(), getActivationManifestPath())}`
5606
6625
  )
5607
6626
  );
5608
6627
  if (manifest.artifacts.handoffPath) {
@@ -5668,13 +6687,25 @@ function commandExists(command) {
5668
6687
  }
5669
6688
  }
5670
6689
  function getClaudeConfigPaths() {
5671
- const desktopDir = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path.join(homedir(), "AppData", "Roaming", "Claude") : path.join(homedir(), ".config", "claude");
6690
+ const desktopDir = process.platform === "darwin" ? path4.join(homedir2(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path4.join(homedir2(), "AppData", "Roaming", "Claude") : path4.join(homedir2(), ".config", "claude");
5672
6691
  return {
5673
- codeSettingsPath: path.join(homedir(), ".claude", "settings.json"),
5674
- desktopConfigPath: path.join(desktopDir, "claude_desktop_config.json")
6692
+ codeSettingsPath: path4.join(homedir2(), ".claude", "settings.json"),
6693
+ desktopConfigPath: path4.join(desktopDir, "claude_desktop_config.json")
5675
6694
  };
5676
6695
  }
5677
- function detectEnvironment() {
6696
+ function executableOnPath(command) {
6697
+ return (process.env.PATH ?? "").split(path4.delimiter).some((directory) => {
6698
+ if (!directory || !path4.isAbsolute(directory)) return false;
6699
+ try {
6700
+ const file = path4.join(directory, command);
6701
+ fs4.accessSync(file, fs4.constants.X_OK);
6702
+ return fs4.statSync(file).isFile();
6703
+ } catch {
6704
+ return false;
6705
+ }
6706
+ });
6707
+ }
6708
+ function detectEnvironment(managed = hasLocalWorkSelection()) {
5678
6709
  const existingConfig = getExistingConfig();
5679
6710
  const projectSlugResolution = resolveProjectSlug(existingConfig);
5680
6711
  const claudePaths = getClaudeConfigPaths();
@@ -5686,38 +6717,38 @@ function detectEnvironment() {
5686
6717
  aiClient: detectAiClient(),
5687
6718
  inGitRepo: isGitRepo(),
5688
6719
  files: {
5689
- mcpConfig: fs.existsSync(path.join(process.cwd(), ".mcp.json")),
5690
- env: fs.existsSync(path.join(process.cwd(), ".env")),
5691
- envLocal: fs.existsSync(path.join(process.cwd(), ".env.local")),
5692
- envExample: fs.existsSync(path.join(process.cwd(), ".env.example")),
5693
- agentsMd: fs.existsSync(path.join(process.cwd(), "AGENTS.md")),
5694
- claudeMd: fs.existsSync(path.join(process.cwd(), "CLAUDE.md")),
5695
- codexConfig: fs.existsSync(path.join(process.cwd(), ".codex", "config.toml")),
5696
- cursorRule: fs.existsSync(path.join(process.cwd(), ".cursor", "rules", "snipara.mdc")),
5697
- sniparaProject: fs.existsSync(path.join(process.cwd(), ".snipara", "project")),
5698
- claudeDir: fs.existsSync(path.join(homedir(), ".claude")),
5699
- claudeSettings: fs.existsSync(claudePaths.codeSettingsPath),
5700
- claudeProjectDir: fs.existsSync(path.join(process.cwd(), ".claude")),
5701
- claudeProjectSettings: fs.existsSync(path.join(process.cwd(), ".claude", "settings.json")),
5702
- claudeDesktopConfig: fs.existsSync(claudePaths.desktopConfigPath),
5703
- cursorDir: fs.existsSync(path.join(homedir(), ".cursor")),
5704
- mistralVibeConfig: fs.existsSync(path.join(process.cwd(), ".vibe", "config.toml")) || fs.existsSync(path.join(homedir(), ".vibe", "config.toml")),
5705
- sniparaCompanionPack: fs.existsSync(path.join(process.cwd(), ".snipara", "companion")),
5706
- sniparaTemplates: fs.existsSync(path.join(process.cwd(), ".snipara", "templates")),
5707
- sniparaSkillPack: fs.existsSync(path.join(process.cwd(), ".snipara", "templates", "skills"))
6720
+ mcpConfig: fs4.existsSync(path4.join(process.cwd(), ".mcp.json")),
6721
+ env: fs4.existsSync(path4.join(process.cwd(), ".env")),
6722
+ envLocal: fs4.existsSync(path4.join(process.cwd(), ".env.local")),
6723
+ envExample: fs4.existsSync(path4.join(process.cwd(), ".env.example")),
6724
+ agentsMd: fs4.existsSync(path4.join(process.cwd(), "AGENTS.md")),
6725
+ claudeMd: fs4.existsSync(path4.join(process.cwd(), "CLAUDE.md")),
6726
+ codexConfig: fs4.existsSync(path4.join(process.cwd(), ".codex", "config.toml")),
6727
+ cursorRule: fs4.existsSync(path4.join(process.cwd(), ".cursor", "rules", "snipara.mdc")),
6728
+ sniparaProject: fs4.existsSync(path4.join(process.cwd(), ".snipara", "project")),
6729
+ claudeDir: fs4.existsSync(path4.join(homedir2(), ".claude")),
6730
+ claudeSettings: fs4.existsSync(claudePaths.codeSettingsPath),
6731
+ claudeProjectDir: fs4.existsSync(path4.join(process.cwd(), ".claude")),
6732
+ claudeProjectSettings: fs4.existsSync(path4.join(process.cwd(), ".claude", "settings.json")),
6733
+ claudeDesktopConfig: fs4.existsSync(claudePaths.desktopConfigPath),
6734
+ cursorDir: fs4.existsSync(path4.join(homedir2(), ".cursor")),
6735
+ mistralVibeConfig: fs4.existsSync(path4.join(process.cwd(), ".vibe", "config.toml")) || fs4.existsSync(path4.join(homedir2(), ".vibe", "config.toml")),
6736
+ sniparaCompanionPack: fs4.existsSync(path4.join(process.cwd(), ".snipara", "companion")),
6737
+ sniparaTemplates: fs4.existsSync(path4.join(process.cwd(), ".snipara", "templates")),
6738
+ sniparaSkillPack: fs4.existsSync(path4.join(process.cwd(), ".snipara", "templates", "skills"))
5708
6739
  },
5709
6740
  tooling: {
5710
- node: commandExists("node"),
5711
- npm: commandExists("npm"),
5712
- pnpm: commandExists("pnpm"),
5713
- python: commandExists("python3") || commandExists("python"),
5714
- pip: commandExists("pip3") || commandExists("pip"),
5715
- uv: commandExists("uv"),
5716
- docker: commandExists("docker"),
5717
- git: commandExists("git"),
5718
- companionCli: Boolean(detectCompanionCommand()),
5719
- runtimeCli: Boolean(detectSandboxCommand()),
5720
- orchestratorCli: Boolean(detectOrchestratorCommand())
6741
+ node: managed ? true : commandExists("node"),
6742
+ npm: (managed ? executableOnPath : commandExists)("npm"),
6743
+ pnpm: (managed ? executableOnPath : commandExists)("pnpm"),
6744
+ python: (managed ? executableOnPath : commandExists)("python3") || (managed ? executableOnPath : commandExists)("python"),
6745
+ pip: (managed ? executableOnPath : commandExists)("pip3") || (managed ? executableOnPath : commandExists)("pip"),
6746
+ uv: (managed ? executableOnPath : commandExists)("uv"),
6747
+ docker: (managed ? executableOnPath : commandExists)("docker"),
6748
+ git: (managed ? executableOnPath : commandExists)("git"),
6749
+ companionCli: managed ? false : Boolean(detectCompanionCommand()),
6750
+ runtimeCli: managed ? false : Boolean(detectSandboxCommand()),
6751
+ orchestratorCli: managed ? false : Boolean(detectOrchestratorCommand())
5721
6752
  },
5722
6753
  existingConfig: {
5723
6754
  slug: existingConfig?.slug,
@@ -5729,7 +6760,7 @@ function detectEnvironment() {
5729
6760
  };
5730
6761
  }
5731
6762
  function loadExistingAnswers(options) {
5732
- const detection = detectEnvironment();
6763
+ const detection = detectEnvironment(options.profile === "local-work" || hasLocalWorkSelection());
5733
6764
  const effectiveProfile = resolveMaintenanceProfile(detection, options.profile);
5734
6765
  const deployment = resolveRequestedDeployment(options);
5735
6766
  const providedApiKey = options.apiKey?.trim();
@@ -5751,9 +6782,9 @@ function loadExistingAnswers(options) {
5751
6782
  apiKeyType,
5752
6783
  profile: effectiveProfile,
5753
6784
  aiClient: detection.aiClient,
5754
- skipCompanion: options.withCompanion === true ? false : !detection.tooling.companionCli && effectiveProfile !== "hosted-companion" && effectiveProfile !== "full-stack",
6785
+ skipCompanion: effectiveProfile === "local-work" ? false : options.withCompanion === true ? false : !detection.tooling.companionCli && effectiveProfile !== "hosted-companion" && effectiveProfile !== "full-stack",
5755
6786
  skipMcp: options.withMcp === true ? false : !detection.existingConfig.hasSniparaServer && effectiveProfile === "runtime-only",
5756
- skipRuntime: options.withRuntime === true ? false : !detection.existingConfig.hasRuntimeServer && effectiveProfile !== "full-stack" && effectiveProfile !== "runtime-only",
6787
+ skipRuntime: effectiveProfile === "local-work" ? false : options.withRuntime === true ? false : !detection.existingConfig.hasRuntimeServer && effectiveProfile !== "full-stack" && effectiveProfile !== "runtime-only",
5757
6788
  withOrchestrator: Boolean(options.withOrchestrator || detection.tooling.orchestratorCli),
5758
6789
  skipHooks: options.withHooks ? false : !shouldInstallDefaultHooks(detection.aiClient)
5759
6790
  });
@@ -5762,6 +6793,7 @@ function resolveMaintenanceProfile(detection, explicitProfile) {
5762
6793
  if (explicitProfile && isInstallProfile(explicitProfile)) {
5763
6794
  return explicitProfile;
5764
6795
  }
6796
+ if (hasLocalWorkSelection()) return "local-work";
5765
6797
  if (detection.existingConfig.hasRuntimeServer && detection.existingConfig.hasSniparaServer) {
5766
6798
  return detection.tooling.companionCli ? "full-stack" : "full-stack";
5767
6799
  }
@@ -5806,22 +6838,22 @@ function collectConfiguredProjectSlugSignals(existingConfig = getExistingConfig(
5806
6838
  { source: ".snipara/project", slug: readProjectSlugFromProjectBinding() },
5807
6839
  {
5808
6840
  source: ".codex/config.toml",
5809
- slug: readProjectSlugFromTextFile(path.join(process.cwd(), ".codex", "config.toml"))
6841
+ slug: readProjectSlugFromTextFile(path4.join(process.cwd(), ".codex", "config.toml"))
5810
6842
  },
5811
6843
  {
5812
6844
  source: ".snipara/templates/codex-config.toml",
5813
6845
  slug: readProjectSlugFromTextFile(
5814
- path.join(process.cwd(), ".snipara", "templates", "codex-config.toml")
6846
+ path4.join(process.cwd(), ".snipara", "templates", "codex-config.toml")
5815
6847
  )
5816
6848
  },
5817
6849
  {
5818
6850
  source: ".vibe/config.toml",
5819
- slug: readProjectSlugFromTextFile(path.join(process.cwd(), ".vibe", "config.toml"))
6851
+ slug: readProjectSlugFromTextFile(path4.join(process.cwd(), ".vibe", "config.toml"))
5820
6852
  },
5821
6853
  {
5822
6854
  source: ".snipara/templates/mistral-vibe-config.toml",
5823
6855
  slug: readProjectSlugFromTextFile(
5824
- path.join(process.cwd(), ".snipara", "templates", "mistral-vibe-config.toml")
6856
+ path4.join(process.cwd(), ".snipara", "templates", "mistral-vibe-config.toml")
5825
6857
  )
5826
6858
  },
5827
6859
  { source: ".env.local", slug: readProjectSlugFromEnvFile(".env.local") },
@@ -5859,22 +6891,22 @@ function extractSlugFromText(content) {
5859
6891
  return normalizeSlugCandidate(match?.[1] ? decodeURIComponent(match[1]) : null);
5860
6892
  }
5861
6893
  function readProjectSlugFromTextFile(filePath) {
5862
- if (!fs.existsSync(filePath)) {
6894
+ if (!fs4.existsSync(filePath)) {
5863
6895
  return null;
5864
6896
  }
5865
6897
  try {
5866
- return extractSlugFromText(fs.readFileSync(filePath, "utf-8"));
6898
+ return extractSlugFromText(fs4.readFileSync(filePath, "utf-8"));
5867
6899
  } catch {
5868
6900
  return null;
5869
6901
  }
5870
6902
  }
5871
6903
  function readProjectSlugFromProjectBinding() {
5872
- const filePath = path.join(process.cwd(), ".snipara", "project");
5873
- if (!fs.existsSync(filePath)) {
6904
+ const filePath = path4.join(process.cwd(), ".snipara", "project");
6905
+ if (!fs4.existsSync(filePath)) {
5874
6906
  return null;
5875
6907
  }
5876
6908
  try {
5877
- const content = fs.readFileSync(filePath, "utf-8").trim();
6909
+ const content = fs4.readFileSync(filePath, "utf-8").trim();
5878
6910
  if (!content) {
5879
6911
  return null;
5880
6912
  }
@@ -5910,12 +6942,12 @@ function readProjectSlugFromEnvFile(file) {
5910
6942
  return null;
5911
6943
  }
5912
6944
  function readEnvValueFromFile(file, names) {
5913
- const filePath = path.join(process.cwd(), file);
5914
- if (!fs.existsSync(filePath)) {
6945
+ const filePath = path4.join(process.cwd(), file);
6946
+ if (!fs4.existsSync(filePath)) {
5915
6947
  return null;
5916
6948
  }
5917
6949
  try {
5918
- const content = fs.readFileSync(filePath, "utf-8");
6950
+ const content = fs4.readFileSync(filePath, "utf-8");
5919
6951
  for (const line of content.split(/\r?\n/)) {
5920
6952
  const match = line.trim().match(/^(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/);
5921
6953
  if (!match || !names.includes(match[1])) {
@@ -5955,7 +6987,7 @@ function detectRepoProjectSlug() {
5955
6987
  return null;
5956
6988
  }
5957
6989
  function detectDirectoryProjectSlug() {
5958
- return slugifyFallback(path.basename(process.cwd()));
6990
+ return slugifyFallback(path4.basename(process.cwd()));
5959
6991
  }
5960
6992
  function detectGitRepository() {
5961
6993
  try {
@@ -6017,50 +7049,50 @@ function isGitRepo() {
6017
7049
  }
6018
7050
  }
6019
7051
  function detectAiClient() {
6020
- if (fs.existsSync(path.join(process.cwd(), ".codex"))) {
7052
+ if (fs4.existsSync(path4.join(process.cwd(), ".codex"))) {
6021
7053
  return "codex";
6022
7054
  }
6023
- if (fs.existsSync(path.join(process.cwd(), ".cursor"))) {
7055
+ if (fs4.existsSync(path4.join(process.cwd(), ".cursor"))) {
6024
7056
  return "cursor";
6025
7057
  }
6026
- if (fs.existsSync(path.join(process.cwd(), ".kimi-code"))) {
7058
+ if (fs4.existsSync(path4.join(process.cwd(), ".kimi-code"))) {
6027
7059
  return "kimi";
6028
7060
  }
6029
- if (fs.existsSync(path.join(process.cwd(), ".gemini"))) {
7061
+ if (fs4.existsSync(path4.join(process.cwd(), ".gemini"))) {
6030
7062
  return "gemini";
6031
7063
  }
6032
- if (fs.existsSync(path.join(process.cwd(), ".vibe"))) {
7064
+ if (fs4.existsSync(path4.join(process.cwd(), ".vibe"))) {
6033
7065
  return "mistral";
6034
7066
  }
6035
- if (fs.existsSync(path.join(process.cwd(), ".continue"))) {
7067
+ if (fs4.existsSync(path4.join(process.cwd(), ".continue"))) {
6036
7068
  return "continue";
6037
7069
  }
6038
- if (fs.existsSync(path.join(process.cwd(), ".vscode"))) {
7070
+ if (fs4.existsSync(path4.join(process.cwd(), ".vscode"))) {
6039
7071
  return "vscode";
6040
7072
  }
6041
- if (fs.existsSync(path.join(homedir(), ".claude")) || process.env.CLAUDE_CODE) {
7073
+ if (fs4.existsSync(path4.join(homedir2(), ".claude")) || process.env.CLAUDE_CODE) {
6042
7074
  return "claude-code";
6043
7075
  }
6044
- if (fs.existsSync(path.join(homedir(), ".cursor")) || process.env.CURSOR_EDITOR) {
7076
+ if (fs4.existsSync(path4.join(homedir2(), ".cursor")) || process.env.CURSOR_EDITOR) {
6045
7077
  return "cursor";
6046
7078
  }
6047
- if (fs.existsSync(path.join(homedir(), ".kimi-code")) || process.env.KIMI_CODE_HOME) {
7079
+ if (fs4.existsSync(path4.join(homedir2(), ".kimi-code")) || process.env.KIMI_CODE_HOME) {
6048
7080
  return "kimi";
6049
7081
  }
6050
- if (fs.existsSync(path.join(homedir(), ".gemini")) || process.env.GEMINI_CLI) {
7082
+ if (fs4.existsSync(path4.join(homedir2(), ".gemini")) || process.env.GEMINI_CLI) {
6051
7083
  return "gemini";
6052
7084
  }
6053
- if (fs.existsSync(path.join(homedir(), ".vibe")) || process.env.MISTRAL_VIBE) {
7085
+ if (fs4.existsSync(path4.join(homedir2(), ".vibe")) || process.env.MISTRAL_VIBE) {
6054
7086
  return "mistral";
6055
7087
  }
6056
- if (fs.existsSync(path.join(homedir(), ".continue")) || process.env.CONTINUE_GLOBAL_DIR) {
7088
+ if (fs4.existsSync(path4.join(homedir2(), ".continue")) || process.env.CONTINUE_GLOBAL_DIR) {
6057
7089
  return "continue";
6058
7090
  }
6059
- const claudeDesktopPath = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path.join(homedir(), "AppData", "Roaming", "Claude") : path.join(homedir(), ".config", "claude");
6060
- if (fs.existsSync(claudeDesktopPath)) {
7091
+ const claudeDesktopPath = process.platform === "darwin" ? path4.join(homedir2(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path4.join(homedir2(), "AppData", "Roaming", "Claude") : path4.join(homedir2(), ".config", "claude");
7092
+ if (fs4.existsSync(claudeDesktopPath)) {
6061
7093
  return "claude-desktop";
6062
7094
  }
6063
- if (fs.existsSync(path.join(homedir(), ".codex"))) {
7095
+ if (fs4.existsSync(path4.join(homedir2(), ".codex"))) {
6064
7096
  return "codex";
6065
7097
  }
6066
7098
  return "other";
@@ -6075,12 +7107,12 @@ function detectPipCommand() {
6075
7107
  return null;
6076
7108
  }
6077
7109
  function getExistingConfig() {
6078
- const mcpConfigPath = path.join(process.cwd(), ".mcp.json");
6079
- if (!fs.existsSync(mcpConfigPath)) {
7110
+ const mcpConfigPath = path4.join(process.cwd(), ".mcp.json");
7111
+ if (!fs4.existsSync(mcpConfigPath)) {
6080
7112
  return null;
6081
7113
  }
6082
7114
  try {
6083
- const config = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
7115
+ const config = JSON.parse(fs4.readFileSync(mcpConfigPath, "utf-8"));
6084
7116
  const sniparaServer = config.mcpServers?.snipara;
6085
7117
  const runtimeServer = config.mcpServers?.[SANDBOX_SERVER_NAME] || config.mcpServers?.[LEGACY_RUNTIME_SERVER_NAME];
6086
7118
  return {
@@ -6100,12 +7132,12 @@ function getExistingConfig() {
6100
7132
  }
6101
7133
  }
6102
7134
  function readExistingCompanionProjectId() {
6103
- const configPath = path.join(process.cwd(), ".snipara", "companion", "config.json");
6104
- if (!fs.existsSync(configPath)) {
7135
+ const configPath = path4.join(process.cwd(), ".snipara", "companion", "config.json");
7136
+ if (!fs4.existsSync(configPath)) {
6105
7137
  return void 0;
6106
7138
  }
6107
7139
  try {
6108
- const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
7140
+ const parsed = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
6109
7141
  return typeof parsed.projectId === "string" && parsed.projectId ? parsed.projectId : void 0;
6110
7142
  } catch {
6111
7143
  return void 0;