create-snipara 2.1.1 → 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/README.md +54 -0
- package/dist/index.js +1396 -363
- package/dist/managed-config.d.ts +19 -0
- package/dist/managed-config.js +86 -0
- package/dist/managed-toolchain.d.ts +38 -0
- package/dist/managed-toolchain.js +505 -0
- package/dist/source-connection.d.ts +12 -0
- package/dist/source-connection.js +199 -0
- package/package.json +4 -3
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
|
|
10
|
-
import
|
|
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.
|
|
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
|
|
26
|
-
dev: "tsup
|
|
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";
|
|
@@ -139,37 +915,37 @@ var DECISION_COMMIT_TERMS = [
|
|
|
139
915
|
"switch"
|
|
140
916
|
];
|
|
141
917
|
function buildActivationId() {
|
|
142
|
-
return `act_${Date.now().toString(36)}_${
|
|
918
|
+
return `act_${Date.now().toString(36)}_${randomUUID3().replace(/-/g, "").slice(0, 12)}`;
|
|
143
919
|
}
|
|
144
920
|
function getActivationDir() {
|
|
145
|
-
return
|
|
921
|
+
return path4.join(process.cwd(), ".snipara", "activation");
|
|
146
922
|
}
|
|
147
923
|
function getActivationManifestPath() {
|
|
148
|
-
return
|
|
924
|
+
return path4.join(getActivationDir(), "manifest.json");
|
|
149
925
|
}
|
|
150
926
|
function getStableActivationManifestPath() {
|
|
151
|
-
return
|
|
927
|
+
return path4.join(getActivationDir(), "activation-manifest.json");
|
|
152
928
|
}
|
|
153
929
|
function getActivationHandoffPath() {
|
|
154
|
-
return
|
|
930
|
+
return path4.join(getActivationDir(), "handoff.md");
|
|
155
931
|
}
|
|
156
932
|
function getFirstWorkBriefPath() {
|
|
157
|
-
return
|
|
933
|
+
return path4.join(getActivationDir(), "first-brief.md");
|
|
158
934
|
}
|
|
159
935
|
function getMemoryCandidatesPath() {
|
|
160
|
-
return
|
|
936
|
+
return path4.join(getActivationDir(), "memory-candidates.json");
|
|
161
937
|
}
|
|
162
938
|
function getProjectBootstrapJsonPath() {
|
|
163
|
-
return
|
|
939
|
+
return path4.join(getActivationDir(), "project-bootstrap.json");
|
|
164
940
|
}
|
|
165
941
|
function getProjectBootstrapMarkdownPath() {
|
|
166
|
-
return
|
|
942
|
+
return path4.join(getActivationDir(), "project-bootstrap.md");
|
|
167
943
|
}
|
|
168
944
|
function writeActivationManifest(manifest) {
|
|
169
|
-
|
|
945
|
+
fs4.mkdirSync(getActivationDir(), { recursive: true });
|
|
170
946
|
const payload = JSON.stringify(manifest, null, 2) + "\n";
|
|
171
|
-
|
|
172
|
-
|
|
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 =
|
|
192
|
-
if (!
|
|
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(
|
|
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 =
|
|
258
|
-
if (!
|
|
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 =
|
|
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 =
|
|
272
|
-
if (!
|
|
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 =
|
|
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 =
|
|
306
|
-
if (!
|
|
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
|
|
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 =
|
|
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
|
-
|
|
433
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1249
|
+
fs4.writeFileSync(getActivationHandoffPath(), lines.join("\n"), "utf8");
|
|
474
1250
|
}
|
|
475
1251
|
function writeFirstWorkBrief(manifest) {
|
|
476
|
-
|
|
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
|
-
|
|
1267
|
+
fs4.writeFileSync(getFirstWorkBriefPath(), lines.join("\n"), "utf8");
|
|
492
1268
|
}
|
|
493
1269
|
function detectLocalActivationScan(detection) {
|
|
494
1270
|
const cwd = process.cwd();
|
|
495
|
-
const
|
|
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
|
-
|
|
1274
|
+
exists2
|
|
499
1275
|
);
|
|
500
1276
|
const packageManagers = [
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
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 =
|
|
1282
|
+
const rootEntries = fs4.existsSync(cwd) ? fs4.readdirSync(cwd).slice(0, 250) : [];
|
|
507
1283
|
const rootExtensions = new Set(
|
|
508
|
-
rootEntries.map((entry) =>
|
|
1284
|
+
rootEntries.map((entry) => path4.extname(entry).replace(/^\./, "")).filter(Boolean)
|
|
509
1285
|
);
|
|
510
1286
|
const languages = [
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
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:
|
|
522
|
-
hasPyproject:
|
|
523
|
-
hasRequirements:
|
|
524
|
-
hasDockerfile:
|
|
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 =
|
|
551
|
-
if (!
|
|
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
|
|
560
|
-
const absolutePath =
|
|
561
|
-
const stat =
|
|
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(
|
|
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 =
|
|
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
|
|
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) =>
|
|
1375
|
+
(source) => exists2(source)
|
|
600
1376
|
)
|
|
601
|
-
].filter(
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
663
|
-
if (!
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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(
|
|
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: ${
|
|
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: ${
|
|
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 ||
|
|
1052
|
-
releaseTags.length > 0 ||
|
|
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
|
-
...
|
|
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
|
-
|
|
1083
|
-
|
|
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
|
-
|
|
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:
|
|
1287
|
-
legacyManifestPath:
|
|
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
|
|
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
|
-
|
|
2096
|
+
const runOptions = {
|
|
1319
2097
|
cwd: process.cwd(),
|
|
1320
|
-
encoding: "
|
|
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
|
-
|
|
2120
|
+
const runOptions = {
|
|
1341
2121
|
cwd: process.cwd(),
|
|
1342
|
-
encoding: "
|
|
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 =
|
|
2349
|
+
manifest.artifacts.projectBootstrapJsonPath = path4.relative(
|
|
1568
2350
|
process.cwd(),
|
|
1569
2351
|
getProjectBootstrapJsonPath()
|
|
1570
2352
|
);
|
|
1571
|
-
manifest.artifacts.projectBootstrapMarkdownPath =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
1687
|
-
|
|
1688
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
1722
|
-
|
|
2513
|
+
addSharedRootOptions(program2);
|
|
2514
|
+
program2.action(async (options) => {
|
|
1723
2515
|
printBanner();
|
|
1724
2516
|
await runInit(options);
|
|
1725
2517
|
});
|
|
1726
|
-
return
|
|
2518
|
+
return program2;
|
|
1727
2519
|
}
|
|
1728
|
-
function addSharedRootOptions(
|
|
1729
|
-
|
|
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
|
|
2530
|
+
const program2 = buildProgram();
|
|
1739
2531
|
normalizeLegacyArgAliases(process.argv);
|
|
1740
|
-
await
|
|
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.
|
|
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.
|
|
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 (
|
|
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:
|
|
3006
|
+
type: options.profile ? null : "select",
|
|
2192
3007
|
name: "profile",
|
|
2193
|
-
message: "
|
|
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
|
-
|
|
3276
|
+
function installProfileChoices(advanced = true) {
|
|
3277
|
+
const normal = [
|
|
2462
3278
|
{
|
|
2463
|
-
title: "
|
|
3279
|
+
title: "Context",
|
|
2464
3280
|
value: "hosted-only",
|
|
2465
|
-
description: "
|
|
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((
|
|
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 =
|
|
3809
|
+
const mcpConfigPath = path4.join(process.cwd(), ".mcp.json");
|
|
2896
3810
|
let mcpConfig = { mcpServers: {} };
|
|
2897
|
-
if (
|
|
3811
|
+
if (answers.profile === "local-work") assertManagedMcpConfig();
|
|
3812
|
+
if (fs4.existsSync(mcpConfigPath)) {
|
|
2898
3813
|
try {
|
|
2899
|
-
mcpConfig = JSON.parse(
|
|
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.
|
|
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
|
-
|
|
3838
|
+
fs4.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n", { mode: 384 });
|
|
2916
3839
|
try {
|
|
2917
|
-
|
|
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 =
|
|
2931
|
-
const configPath =
|
|
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
|
-
|
|
2948
|
-
|
|
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
|
-
|
|
3896
|
+
path4.join(process.cwd(), ".snipara", "companion", "config.json")
|
|
2974
3897
|
);
|
|
2975
|
-
const projectMcpConfig = asObjectRecord(readJsonFile(
|
|
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(
|
|
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()}_${
|
|
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 =
|
|
3017
|
-
const projectBindingPath =
|
|
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 =
|
|
3021
|
-
|
|
3022
|
-
|
|
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 =
|
|
3031
|
-
|
|
3961
|
+
const templatesDir = path4.join(process.cwd(), ".snipara", "templates");
|
|
3962
|
+
fs4.mkdirSync(templatesDir, { recursive: true });
|
|
3032
3963
|
const templates = [
|
|
3033
|
-
{ path:
|
|
3034
|
-
{ path:
|
|
3964
|
+
{ path: path4.join(templatesDir, "AGENTS.md"), content: buildAgentsMdTemplate(answers) },
|
|
3965
|
+
{ path: path4.join(templatesDir, "CLAUDE.md"), content: buildClaudeMdTemplate(answers) },
|
|
3035
3966
|
{
|
|
3036
|
-
path:
|
|
3967
|
+
path: path4.join(templatesDir, "cursor-snipara.mdc"),
|
|
3037
3968
|
content: buildCursorRuleTemplate(answers)
|
|
3038
3969
|
},
|
|
3039
3970
|
{
|
|
3040
|
-
path:
|
|
3971
|
+
path: path4.join(templatesDir, "codex-config.toml"),
|
|
3041
3972
|
content: buildCodexConfigTemplate(answers)
|
|
3042
3973
|
},
|
|
3043
3974
|
{
|
|
3044
|
-
path:
|
|
3975
|
+
path: path4.join(templatesDir, "chatgpt-openai-mcp.json"),
|
|
3045
3976
|
content: buildChatGptMcpTemplate(answers)
|
|
3046
3977
|
},
|
|
3047
3978
|
{
|
|
3048
|
-
path:
|
|
3979
|
+
path: path4.join(templatesDir, "gemini-mcp-reference.json"),
|
|
3049
3980
|
content: buildGenericMcpReferenceTemplate(answers, "gemini")
|
|
3050
3981
|
},
|
|
3051
3982
|
{
|
|
3052
|
-
path:
|
|
3983
|
+
path: path4.join(templatesDir, "mistral-vibe-config.toml"),
|
|
3053
3984
|
content: buildMistralVibeConfigTemplate(answers)
|
|
3054
3985
|
},
|
|
3055
3986
|
{
|
|
3056
|
-
path:
|
|
3987
|
+
path: path4.join(templatesDir, "mistral-le-chat-mcp.json"),
|
|
3057
3988
|
content: buildMistralLeChatTemplate(answers)
|
|
3058
3989
|
},
|
|
3059
3990
|
{
|
|
3060
|
-
path:
|
|
3991
|
+
path: path4.join(templatesDir, "mistral-langchain-tools.ts"),
|
|
3061
3992
|
content: buildMistralLangChainToolsTemplate(answers)
|
|
3062
3993
|
},
|
|
3063
3994
|
{
|
|
3064
|
-
path:
|
|
3995
|
+
path: path4.join(templatesDir, "vscode-mcp-reference.json"),
|
|
3065
3996
|
content: buildGenericMcpReferenceTemplate(answers, "vscode")
|
|
3066
3997
|
},
|
|
3067
3998
|
{
|
|
3068
|
-
path:
|
|
3999
|
+
path: path4.join(templatesDir, "continue-mcp-reference.json"),
|
|
3069
4000
|
content: buildGenericMcpReferenceTemplate(answers, "continue")
|
|
3070
4001
|
},
|
|
3071
4002
|
{
|
|
3072
|
-
path:
|
|
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
|
-
|
|
4008
|
+
fs4.writeFileSync(template.path, template.content);
|
|
3078
4009
|
}
|
|
3079
4010
|
console.log(chalk.green(" Created .snipara/templates agent snippets"));
|
|
3080
|
-
writeAgentSkillPackTemplates(
|
|
4011
|
+
writeAgentSkillPackTemplates(path4.join(templatesDir, "skills"), answers);
|
|
3081
4012
|
writeIfMissing(
|
|
3082
|
-
|
|
4013
|
+
path4.join(process.cwd(), "AGENTS.md"),
|
|
3083
4014
|
buildAgentsMdTemplate(answers),
|
|
3084
4015
|
"AGENTS.md"
|
|
3085
4016
|
);
|
|
3086
4017
|
writeIfMissing(
|
|
3087
|
-
|
|
4018
|
+
path4.join(process.cwd(), "CLAUDE.md"),
|
|
3088
4019
|
buildClaudeMdTemplate(answers),
|
|
3089
4020
|
"CLAUDE.md"
|
|
3090
4021
|
);
|
|
3091
|
-
const cursorRulesDir =
|
|
3092
|
-
|
|
4022
|
+
const cursorRulesDir = path4.join(process.cwd(), ".cursor", "rules");
|
|
4023
|
+
fs4.mkdirSync(cursorRulesDir, { recursive: true });
|
|
3093
4024
|
writeIfMissing(
|
|
3094
|
-
|
|
4025
|
+
path4.join(cursorRulesDir, "snipara.mdc"),
|
|
3095
4026
|
buildCursorRuleTemplate(answers),
|
|
3096
4027
|
".cursor/rules/snipara.mdc"
|
|
3097
4028
|
);
|
|
3098
|
-
const codexDir =
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
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 =
|
|
3107
|
-
|
|
4037
|
+
const vibeDir = path4.join(process.cwd(), ".vibe");
|
|
4038
|
+
fs4.mkdirSync(vibeDir, { recursive: true });
|
|
3108
4039
|
writeIfMissing(
|
|
3109
|
-
|
|
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 (
|
|
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
|
-
|
|
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 =
|
|
3137
|
-
const agentsDir =
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
|
|
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
|
}
|
|
@@ -3606,14 +4584,20 @@ ${EXPERIENCE_GOAL_SECTION}
|
|
|
3606
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.
|
|
3607
4585
|
`;
|
|
3608
4586
|
}
|
|
3609
|
-
function
|
|
3610
|
-
return `[mcp_servers.snipara]
|
|
4587
|
+
function buildCodexHostedConfigTemplate(answers) {
|
|
4588
|
+
return managedSetupOnly(answers) ? "" : `[mcp_servers.snipara]
|
|
3611
4589
|
type = "streamable_http"
|
|
3612
4590
|
url = "${buildHostedMcpEndpoint(answers)}"
|
|
3613
4591
|
bearer_token_env_var = "SNIPARA_API_KEY"
|
|
3614
4592
|
http_headers = { "X-Snipara-Session-Id" = "${getCorrelationSessionId(answers)}" }
|
|
3615
4593
|
`;
|
|
3616
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
|
+
}
|
|
3617
4601
|
function buildChatGptMcpTemplate(answers) {
|
|
3618
4602
|
return `${JSON.stringify(
|
|
3619
4603
|
{
|
|
@@ -3781,11 +4765,11 @@ function buildGenericMcpReferenceTemplate(answers, client) {
|
|
|
3781
4765
|
`;
|
|
3782
4766
|
}
|
|
3783
4767
|
function ensureGitignoreEntry(entry, comment) {
|
|
3784
|
-
const gitignorePath =
|
|
3785
|
-
if (
|
|
3786
|
-
const gitignore =
|
|
4768
|
+
const gitignorePath = path4.join(process.cwd(), ".gitignore");
|
|
4769
|
+
if (fs4.existsSync(gitignorePath)) {
|
|
4770
|
+
const gitignore = fs4.readFileSync(gitignorePath, "utf-8");
|
|
3787
4771
|
if (!gitignore.includes(entry)) {
|
|
3788
|
-
|
|
4772
|
+
fs4.appendFileSync(gitignorePath, `
|
|
3789
4773
|
${comment}
|
|
3790
4774
|
${entry}
|
|
3791
4775
|
`);
|
|
@@ -3793,27 +4777,31 @@ ${entry}
|
|
|
3793
4777
|
}
|
|
3794
4778
|
return;
|
|
3795
4779
|
}
|
|
3796
|
-
|
|
4780
|
+
fs4.writeFileSync(gitignorePath, `${comment}
|
|
3797
4781
|
${entry}
|
|
3798
4782
|
`);
|
|
3799
4783
|
console.log(chalk.green(` \u2713 Created .gitignore with ${entry}`));
|
|
3800
4784
|
}
|
|
3801
4785
|
async function createClaudeDesktopConfig(answers) {
|
|
3802
|
-
const configDir = process.platform === "darwin" ?
|
|
3803
|
-
const configPath =
|
|
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");
|
|
3804
4788
|
console.log(chalk.dim(`
|
|
3805
4789
|
Claude Desktop config: ${configPath}`));
|
|
3806
4790
|
try {
|
|
3807
|
-
if (
|
|
3808
|
-
|
|
4791
|
+
if (answers.profile === "local-work") assertManagedMcpConfig(configPath);
|
|
4792
|
+
if (!fs4.existsSync(configDir)) {
|
|
4793
|
+
fs4.mkdirSync(configDir, { recursive: true });
|
|
3809
4794
|
}
|
|
3810
4795
|
let config = {};
|
|
3811
|
-
if (
|
|
3812
|
-
config = JSON.parse(
|
|
4796
|
+
if (fs4.existsSync(configPath)) {
|
|
4797
|
+
config = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
3813
4798
|
}
|
|
3814
4799
|
config.mcpServers = config.mcpServers || {};
|
|
3815
|
-
config.mcpServers
|
|
3816
|
-
|
|
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) {
|
|
3817
4805
|
config.mcpServers[SANDBOX_SERVER_NAME] = {
|
|
3818
4806
|
command: detectSandboxCommand() || SANDBOX_COMMAND_NAME,
|
|
3819
4807
|
args: ["mcp-serve"],
|
|
@@ -3823,18 +4811,19 @@ async function createClaudeDesktopConfig(answers) {
|
|
|
3823
4811
|
}
|
|
3824
4812
|
};
|
|
3825
4813
|
}
|
|
3826
|
-
|
|
4814
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
3827
4815
|
console.log(chalk.green(" \u2713 Updated Claude Desktop config"));
|
|
3828
4816
|
} catch (error) {
|
|
4817
|
+
if (answers.profile === "local-work") throw error;
|
|
3829
4818
|
console.log(chalk.yellow(` \u26A0\uFE0F Could not update Claude Desktop config: ${error}`));
|
|
3830
4819
|
}
|
|
3831
4820
|
}
|
|
3832
4821
|
async function installClaudeCodeHooks(answers) {
|
|
3833
4822
|
console.log(chalk.bold("\n\u{1FA9D} Installing Claude Code Hooks\n"));
|
|
3834
|
-
const claudeDir =
|
|
3835
|
-
const hooksDir =
|
|
3836
|
-
if (!
|
|
3837
|
-
|
|
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 });
|
|
3838
4827
|
}
|
|
3839
4828
|
const hooks = [
|
|
3840
4829
|
{
|
|
@@ -3855,16 +4844,16 @@ async function installClaudeCodeHooks(answers) {
|
|
|
3855
4844
|
}
|
|
3856
4845
|
];
|
|
3857
4846
|
for (const hook of hooks) {
|
|
3858
|
-
const hookPath =
|
|
3859
|
-
|
|
3860
|
-
|
|
4847
|
+
const hookPath = path4.join(hooksDir, hook.name);
|
|
4848
|
+
fs4.writeFileSync(hookPath, hook.content);
|
|
4849
|
+
fs4.chmodSync(hookPath, "755");
|
|
3861
4850
|
console.log(chalk.green(` \u2713 Created ${hook.name}`));
|
|
3862
4851
|
}
|
|
3863
|
-
const settingsPath =
|
|
4852
|
+
const settingsPath = path4.join(claudeDir, "settings.json");
|
|
3864
4853
|
let settings = {};
|
|
3865
|
-
if (
|
|
4854
|
+
if (fs4.existsSync(settingsPath)) {
|
|
3866
4855
|
try {
|
|
3867
|
-
settings = JSON.parse(
|
|
4856
|
+
settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
|
|
3868
4857
|
} catch {
|
|
3869
4858
|
settings = {};
|
|
3870
4859
|
}
|
|
@@ -3928,13 +4917,13 @@ async function installClaudeCodeHooks(answers) {
|
|
|
3928
4917
|
for (const { event, entry } of hookEntries) {
|
|
3929
4918
|
const eventHooks = Array.isArray(hooksConfig[event]) ? hooksConfig[event] : [];
|
|
3930
4919
|
const expectedCommand = entry.hooks[0].command;
|
|
3931
|
-
const
|
|
3932
|
-
if (!
|
|
4920
|
+
const exists2 = eventHooks.some((hook) => hookConfigEntryHasCommand(hook, expectedCommand));
|
|
4921
|
+
if (!exists2) {
|
|
3933
4922
|
eventHooks.push(entry);
|
|
3934
4923
|
hooksConfig[event] = eventHooks;
|
|
3935
4924
|
}
|
|
3936
4925
|
}
|
|
3937
|
-
|
|
4926
|
+
fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3938
4927
|
console.log(chalk.green(" \u2713 Updated .claude/settings.json hooks"));
|
|
3939
4928
|
}
|
|
3940
4929
|
function createClaudeProjectHookCommand(scriptName) {
|
|
@@ -4074,9 +5063,9 @@ fi
|
|
|
4074
5063
|
}
|
|
4075
5064
|
async function updateEnvFiles(answers) {
|
|
4076
5065
|
console.log(chalk.bold("\n\u{1F4DD} Updating Environment Files\n"));
|
|
4077
|
-
const envExamplePath =
|
|
4078
|
-
const envLocalPath =
|
|
4079
|
-
const envPath =
|
|
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");
|
|
4080
5069
|
let envVars = "";
|
|
4081
5070
|
let envExampleVars = "";
|
|
4082
5071
|
if (answers.profile !== "runtime-only") {
|
|
@@ -4114,26 +5103,26 @@ ${llmVarName}=your_${answers.llmProvider}_api_key_here`;
|
|
|
4114
5103
|
console.log(chalk.dim(" No environment variables to save"));
|
|
4115
5104
|
return;
|
|
4116
5105
|
}
|
|
4117
|
-
if (envExampleVars &&
|
|
4118
|
-
const content =
|
|
5106
|
+
if (envExampleVars && fs4.existsSync(envExamplePath)) {
|
|
5107
|
+
const content = fs4.readFileSync(envExamplePath, "utf-8");
|
|
4119
5108
|
const missingLines = envExampleVars.split("\n").filter((line) => {
|
|
4120
5109
|
const match = line.match(/^([A-Z0-9_]+)=/);
|
|
4121
5110
|
return Boolean(match && !content.includes(`${match[1]}=`));
|
|
4122
5111
|
});
|
|
4123
5112
|
if (missingLines.length > 0) {
|
|
4124
|
-
|
|
5113
|
+
fs4.appendFileSync(envExamplePath, `
|
|
4125
5114
|
|
|
4126
5115
|
${missingLines.join("\n")}
|
|
4127
5116
|
`);
|
|
4128
5117
|
console.log(chalk.green(" \u2713 Updated .env.example"));
|
|
4129
5118
|
}
|
|
4130
5119
|
} else if (envExampleVars) {
|
|
4131
|
-
|
|
5120
|
+
fs4.writeFileSync(envExamplePath, envExampleVars + "\n");
|
|
4132
5121
|
console.log(chalk.green(" \u2713 Created .env.example"));
|
|
4133
5122
|
}
|
|
4134
|
-
const targetEnvPath =
|
|
4135
|
-
if (
|
|
4136
|
-
const content =
|
|
5123
|
+
const targetEnvPath = fs4.existsSync(envLocalPath) ? envLocalPath : envPath;
|
|
5124
|
+
if (fs4.existsSync(targetEnvPath)) {
|
|
5125
|
+
const content = fs4.readFileSync(targetEnvPath, "utf-8");
|
|
4137
5126
|
const missingLines = envVars.split("\n").filter((line) => {
|
|
4138
5127
|
const match = line.match(/^([A-Z0-9_]+)=/);
|
|
4139
5128
|
if (!match) {
|
|
@@ -4145,26 +5134,26 @@ ${missingLines.join("\n")}
|
|
|
4145
5134
|
return !content.includes(`${match[1]}=`);
|
|
4146
5135
|
});
|
|
4147
5136
|
if (missingLines.length > 0) {
|
|
4148
|
-
|
|
5137
|
+
fs4.appendFileSync(targetEnvPath, `
|
|
4149
5138
|
|
|
4150
5139
|
${missingLines.join("\n")}
|
|
4151
5140
|
`);
|
|
4152
|
-
console.log(chalk.green(` \u2713 Updated ${
|
|
5141
|
+
console.log(chalk.green(` \u2713 Updated ${path4.basename(targetEnvPath)}`));
|
|
4153
5142
|
}
|
|
4154
5143
|
return;
|
|
4155
5144
|
}
|
|
4156
5145
|
if (!answers.apiKey.includes("YOUR_") && answers.profile !== "runtime-only" || answers.llmApiKey) {
|
|
4157
|
-
|
|
4158
|
-
console.log(chalk.green(` \u2713 Created ${
|
|
5146
|
+
fs4.writeFileSync(targetEnvPath, envVars + "\n");
|
|
5147
|
+
console.log(chalk.green(` \u2713 Created ${path4.basename(targetEnvPath)}`));
|
|
4159
5148
|
}
|
|
4160
5149
|
}
|
|
4161
5150
|
async function createCompanionConfig(answers) {
|
|
4162
5151
|
if (!answers.installCompanion || answers.apiKeyType === "none" || !answers.apiKey || answers.apiKey.includes("YOUR_")) {
|
|
4163
5152
|
return;
|
|
4164
5153
|
}
|
|
4165
|
-
const configDir =
|
|
4166
|
-
const configPath =
|
|
4167
|
-
|
|
5154
|
+
const configDir = path4.join(process.cwd(), ".snipara", "companion");
|
|
5155
|
+
const configPath = path4.join(configDir, "config.json");
|
|
5156
|
+
fs4.mkdirSync(configDir, { recursive: true });
|
|
4168
5157
|
const existingWorkspaceConfig = readCompanionConfigFile(configPath);
|
|
4169
5158
|
const workspaceConfig = {
|
|
4170
5159
|
...existingWorkspaceConfig,
|
|
@@ -4174,7 +5163,7 @@ async function createCompanionConfig(answers) {
|
|
|
4174
5163
|
sessionId: getCorrelationSessionId(answers),
|
|
4175
5164
|
client: getAutomationClientId(answers.aiClient)
|
|
4176
5165
|
};
|
|
4177
|
-
|
|
5166
|
+
fs4.writeFileSync(configPath, JSON.stringify(workspaceConfig, null, 2) + "\n");
|
|
4178
5167
|
ensureGitignoreEntry(".snipara/companion/config.json", "# Snipara local companion config");
|
|
4179
5168
|
console.log(chalk.green(" \u2713 Configured local companion config"));
|
|
4180
5169
|
}
|
|
@@ -4244,11 +5233,11 @@ async function syncRemoteAutomationClient(answers, options) {
|
|
|
4244
5233
|
}
|
|
4245
5234
|
}
|
|
4246
5235
|
function readCompanionConfigFile(filePath) {
|
|
4247
|
-
if (!
|
|
5236
|
+
if (!fs4.existsSync(filePath)) {
|
|
4248
5237
|
return {};
|
|
4249
5238
|
}
|
|
4250
5239
|
try {
|
|
4251
|
-
const parsed = JSON.parse(
|
|
5240
|
+
const parsed = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
4252
5241
|
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
4253
5242
|
} catch {
|
|
4254
5243
|
return {};
|
|
@@ -4762,13 +5751,13 @@ ${guidance.map((line) => `- ${line}`).join("\n")}
|
|
|
4762
5751
|
}
|
|
4763
5752
|
async function createCompanionPack(answers) {
|
|
4764
5753
|
console.log(chalk.bold("\n\u{1F5C2}\uFE0F Generating Companion Pack\n"));
|
|
4765
|
-
const packDir =
|
|
5754
|
+
const packDir = path4.join(process.cwd(), ".snipara", "companion");
|
|
4766
5755
|
const commands = getCompanionCommands(answers);
|
|
4767
5756
|
const guidance = [UNIVERSAL_CLIENT_GUIDANCE, ...getClientGuidance(answers.aiClient)];
|
|
4768
5757
|
const detection = detectEnvironment();
|
|
4769
5758
|
const clientPackFilename = getClientPackFilename(answers.aiClient);
|
|
4770
5759
|
const deploymentLabel = getDeploymentLabel(answers.deployment);
|
|
4771
|
-
|
|
5760
|
+
fs4.mkdirSync(packDir, { recursive: true });
|
|
4772
5761
|
const readme = `# Snipara Companion Pack
|
|
4773
5762
|
|
|
4774
5763
|
This project uses ${deploymentLabel} as a project intelligence layer with source-backed context, reviewed memory, and local companion workflow helpers.
|
|
@@ -4915,12 +5904,12 @@ ${item.purpose}`
|
|
|
4915
5904
|
guidance,
|
|
4916
5905
|
detection
|
|
4917
5906
|
};
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
5907
|
+
fs4.writeFileSync(path4.join(packDir, "README.md"), readme + "\n");
|
|
5908
|
+
fs4.writeFileSync(
|
|
5909
|
+
path4.join(packDir, clientPackFilename),
|
|
4921
5910
|
buildClientPackMarkdown(answers, commands, guidance) + "\n"
|
|
4922
5911
|
);
|
|
4923
|
-
|
|
5912
|
+
fs4.writeFileSync(path4.join(packDir, "commands.json"), JSON.stringify(pack, null, 2) + "\n");
|
|
4924
5913
|
console.log(chalk.green(" \u2713 Created .snipara/companion/README.md"));
|
|
4925
5914
|
console.log(chalk.green(` \u2713 Created .snipara/companion/${clientPackFilename}`));
|
|
4926
5915
|
console.log(chalk.green(" \u2713 Created .snipara/companion/commands.json"));
|
|
@@ -4934,8 +5923,8 @@ async function runSandboxInit() {
|
|
|
4934
5923
|
`
|
|
4935
5924
|
)
|
|
4936
5925
|
);
|
|
4937
|
-
return new Promise((
|
|
4938
|
-
const child =
|
|
5926
|
+
return new Promise((resolve2) => {
|
|
5927
|
+
const child = spawn2(sandboxCommand, ["init"], {
|
|
4939
5928
|
stdio: "inherit",
|
|
4940
5929
|
shell: true
|
|
4941
5930
|
});
|
|
@@ -4947,7 +5936,7 @@ async function runSandboxInit() {
|
|
|
4947
5936
|
\u26A0\uFE0F ${sandboxCommand} init exited with code ${code}`));
|
|
4948
5937
|
console.log(chalk.dim(` You can run '${sandboxCommand} init' manually later.`));
|
|
4949
5938
|
}
|
|
4950
|
-
|
|
5939
|
+
resolve2();
|
|
4951
5940
|
});
|
|
4952
5941
|
child.on("error", (error) => {
|
|
4953
5942
|
console.log(chalk.yellow(`
|
|
@@ -4955,7 +5944,7 @@ async function runSandboxInit() {
|
|
|
4955
5944
|
console.log(
|
|
4956
5945
|
chalk.dim(` Run '${SANDBOX_COMMAND_NAME} init' manually after installation completes.`)
|
|
4957
5946
|
);
|
|
4958
|
-
|
|
5947
|
+
resolve2();
|
|
4959
5948
|
});
|
|
4960
5949
|
});
|
|
4961
5950
|
}
|
|
@@ -4992,8 +5981,8 @@ function buildGitHubAutomationUrl(answers) {
|
|
|
4992
5981
|
function getClaudeClientChecks(answers, detection) {
|
|
4993
5982
|
const checks = [];
|
|
4994
5983
|
const claudePaths = getClaudeConfigPaths();
|
|
4995
|
-
const projectConfig = readJsonFile(
|
|
4996
|
-
const projectSettings = readJsonFile(
|
|
5984
|
+
const projectConfig = readJsonFile(path4.join(process.cwd(), ".mcp.json"));
|
|
5985
|
+
const projectSettings = readJsonFile(path4.join(process.cwd(), ".claude", "settings.json"));
|
|
4997
5986
|
const codeSettings = readJsonFile(claudePaths.codeSettingsPath);
|
|
4998
5987
|
const desktopConfig = readJsonFile(claudePaths.desktopConfigPath);
|
|
4999
5988
|
const projectHasSnipara = containsSniparaMcpReference(projectConfig);
|
|
@@ -5047,11 +6036,11 @@ function getClaudeClientChecks(answers, detection) {
|
|
|
5047
6036
|
return checks;
|
|
5048
6037
|
}
|
|
5049
6038
|
function readJsonFile(filePath) {
|
|
5050
|
-
if (!
|
|
6039
|
+
if (!fs4.existsSync(filePath)) {
|
|
5051
6040
|
return null;
|
|
5052
6041
|
}
|
|
5053
6042
|
try {
|
|
5054
|
-
return JSON.parse(
|
|
6043
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
5055
6044
|
} catch {
|
|
5056
6045
|
return null;
|
|
5057
6046
|
}
|
|
@@ -5144,13 +6133,35 @@ function compareSemver(left, right) {
|
|
|
5144
6133
|
async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
5145
6134
|
console.log(chalk.bold("\n\u{1FA7A} Running Validation\n"));
|
|
5146
6135
|
const checks = [];
|
|
5147
|
-
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
|
+
}
|
|
5148
6159
|
const targetClient = options.client || answers.aiClient;
|
|
5149
6160
|
const effectiveAnswers = { ...answers, aiClient: targetClient };
|
|
5150
6161
|
checks.push({
|
|
5151
6162
|
name: "project_slug",
|
|
5152
|
-
status: answers.projectSlug ? "pass" : "fail",
|
|
5153
|
-
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"
|
|
5154
6165
|
});
|
|
5155
6166
|
const configuredSlugSignals = collectConfiguredProjectSlugSignals(detection.existingConfig);
|
|
5156
6167
|
const presentSlugSignals = configuredSlugSignals.filter((signal) => signal.slug);
|
|
@@ -5175,7 +6186,7 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
|
5175
6186
|
checks.push({
|
|
5176
6187
|
name: "companion_cli",
|
|
5177
6188
|
status: answers.installCompanion ? detection.tooling.companionCli ? "pass" : "fail" : "pass",
|
|
5178
|
-
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"
|
|
5179
6190
|
});
|
|
5180
6191
|
checks.push({
|
|
5181
6192
|
name: "sandbox_cli",
|
|
@@ -5218,10 +6229,12 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
|
5218
6229
|
status: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeProjectSettings ? "pass" : "warn" : "pass",
|
|
5219
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"
|
|
5220
6231
|
});
|
|
5221
|
-
if (
|
|
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") {
|
|
5222
6235
|
checks.push(...getClaudeClientChecks(effectiveAnswers, detection));
|
|
5223
6236
|
}
|
|
5224
|
-
if (options.includeNpmFreshness) {
|
|
6237
|
+
if (options.includeNpmFreshness && !managedSetupOnly(answers)) {
|
|
5225
6238
|
checks.push(getCreateSniparaPackageFreshnessCheck());
|
|
5226
6239
|
}
|
|
5227
6240
|
const githubRepo = answers.githubRepo || detectGitRepository();
|
|
@@ -5230,7 +6243,9 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
|
5230
6243
|
status: answers.configureGitHub ? "pass" : githubRepo ? "warn" : "pass",
|
|
5231
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"
|
|
5232
6245
|
});
|
|
5233
|
-
if (includeHostedCheck && answers
|
|
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_")) {
|
|
5234
6249
|
checks.push(...await testHostedConnection(answers));
|
|
5235
6250
|
} else if (includeHostedCheck) {
|
|
5236
6251
|
checks.push({
|
|
@@ -5250,29 +6265,34 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
|
5250
6265
|
};
|
|
5251
6266
|
}
|
|
5252
6267
|
function writeDoctorReport(report) {
|
|
5253
|
-
const reportDir =
|
|
5254
|
-
|
|
5255
|
-
|
|
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");
|
|
5256
6271
|
console.log(chalk.green(" \u2713 Wrote .snipara/companion/doctor.json"));
|
|
5257
6272
|
}
|
|
5258
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
|
+
};
|
|
5259
6279
|
const candidates = [
|
|
5260
6280
|
{
|
|
5261
6281
|
label: ".codex/config.toml",
|
|
5262
|
-
filePath:
|
|
6282
|
+
filePath: path4.join(process.cwd(), ".codex", "config.toml")
|
|
5263
6283
|
},
|
|
5264
6284
|
{
|
|
5265
6285
|
label: ".snipara/templates/codex-config.toml",
|
|
5266
|
-
filePath:
|
|
6286
|
+
filePath: path4.join(process.cwd(), ".snipara", "templates", "codex-config.toml")
|
|
5267
6287
|
},
|
|
5268
6288
|
{
|
|
5269
6289
|
label: "~/.codex/config.toml",
|
|
5270
|
-
filePath:
|
|
6290
|
+
filePath: path4.join(homedir2(), ".codex", "config.toml")
|
|
5271
6291
|
}
|
|
5272
6292
|
];
|
|
5273
|
-
const present = candidates.filter((candidate) =>
|
|
6293
|
+
const present = candidates.filter((candidate) => fs4.existsSync(candidate.filePath)).map((candidate) => ({
|
|
5274
6294
|
label: candidate.label,
|
|
5275
|
-
content:
|
|
6295
|
+
content: fs4.readFileSync(candidate.filePath, "utf-8")
|
|
5276
6296
|
}));
|
|
5277
6297
|
if (present.length === 0) {
|
|
5278
6298
|
return {
|
|
@@ -5324,20 +6344,20 @@ function getMistralConfigCheck(answers) {
|
|
|
5324
6344
|
const candidates = [
|
|
5325
6345
|
{
|
|
5326
6346
|
label: ".vibe/config.toml",
|
|
5327
|
-
filePath:
|
|
6347
|
+
filePath: path4.join(process.cwd(), ".vibe", "config.toml")
|
|
5328
6348
|
},
|
|
5329
6349
|
{
|
|
5330
6350
|
label: ".snipara/templates/mistral-vibe-config.toml",
|
|
5331
|
-
filePath:
|
|
6351
|
+
filePath: path4.join(process.cwd(), ".snipara", "templates", "mistral-vibe-config.toml")
|
|
5332
6352
|
},
|
|
5333
6353
|
{
|
|
5334
6354
|
label: "~/.vibe/config.toml",
|
|
5335
|
-
filePath:
|
|
6355
|
+
filePath: path4.join(homedir2(), ".vibe", "config.toml")
|
|
5336
6356
|
}
|
|
5337
6357
|
];
|
|
5338
|
-
const present = candidates.filter((candidate) =>
|
|
6358
|
+
const present = candidates.filter((candidate) => fs4.existsSync(candidate.filePath)).map((candidate) => ({
|
|
5339
6359
|
label: candidate.label,
|
|
5340
|
-
content:
|
|
6360
|
+
content: fs4.readFileSync(candidate.filePath, "utf-8")
|
|
5341
6361
|
}));
|
|
5342
6362
|
if (present.length === 0) {
|
|
5343
6363
|
return {
|
|
@@ -5601,7 +6621,7 @@ function printActivationSummary(manifest) {
|
|
|
5601
6621
|
console.log(
|
|
5602
6622
|
chalk.dim(
|
|
5603
6623
|
`
|
|
5604
|
-
Activation manifest: ${
|
|
6624
|
+
Activation manifest: ${path4.relative(process.cwd(), getActivationManifestPath())}`
|
|
5605
6625
|
)
|
|
5606
6626
|
);
|
|
5607
6627
|
if (manifest.artifacts.handoffPath) {
|
|
@@ -5667,13 +6687,25 @@ function commandExists(command) {
|
|
|
5667
6687
|
}
|
|
5668
6688
|
}
|
|
5669
6689
|
function getClaudeConfigPaths() {
|
|
5670
|
-
const desktopDir = process.platform === "darwin" ?
|
|
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");
|
|
5671
6691
|
return {
|
|
5672
|
-
codeSettingsPath:
|
|
5673
|
-
desktopConfigPath:
|
|
6692
|
+
codeSettingsPath: path4.join(homedir2(), ".claude", "settings.json"),
|
|
6693
|
+
desktopConfigPath: path4.join(desktopDir, "claude_desktop_config.json")
|
|
5674
6694
|
};
|
|
5675
6695
|
}
|
|
5676
|
-
function
|
|
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()) {
|
|
5677
6709
|
const existingConfig = getExistingConfig();
|
|
5678
6710
|
const projectSlugResolution = resolveProjectSlug(existingConfig);
|
|
5679
6711
|
const claudePaths = getClaudeConfigPaths();
|
|
@@ -5685,38 +6717,38 @@ function detectEnvironment() {
|
|
|
5685
6717
|
aiClient: detectAiClient(),
|
|
5686
6718
|
inGitRepo: isGitRepo(),
|
|
5687
6719
|
files: {
|
|
5688
|
-
mcpConfig:
|
|
5689
|
-
env:
|
|
5690
|
-
envLocal:
|
|
5691
|
-
envExample:
|
|
5692
|
-
agentsMd:
|
|
5693
|
-
claudeMd:
|
|
5694
|
-
codexConfig:
|
|
5695
|
-
cursorRule:
|
|
5696
|
-
sniparaProject:
|
|
5697
|
-
claudeDir:
|
|
5698
|
-
claudeSettings:
|
|
5699
|
-
claudeProjectDir:
|
|
5700
|
-
claudeProjectSettings:
|
|
5701
|
-
claudeDesktopConfig:
|
|
5702
|
-
cursorDir:
|
|
5703
|
-
mistralVibeConfig:
|
|
5704
|
-
sniparaCompanionPack:
|
|
5705
|
-
sniparaTemplates:
|
|
5706
|
-
sniparaSkillPack:
|
|
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"))
|
|
5707
6739
|
},
|
|
5708
6740
|
tooling: {
|
|
5709
|
-
node: commandExists("node"),
|
|
5710
|
-
npm: commandExists("npm"),
|
|
5711
|
-
pnpm: commandExists("pnpm"),
|
|
5712
|
-
python: commandExists("python3") || commandExists("python"),
|
|
5713
|
-
pip: commandExists("pip3") || commandExists("pip"),
|
|
5714
|
-
uv: commandExists("uv"),
|
|
5715
|
-
docker: commandExists("docker"),
|
|
5716
|
-
git: commandExists("git"),
|
|
5717
|
-
companionCli: Boolean(detectCompanionCommand()),
|
|
5718
|
-
runtimeCli: Boolean(detectSandboxCommand()),
|
|
5719
|
-
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())
|
|
5720
6752
|
},
|
|
5721
6753
|
existingConfig: {
|
|
5722
6754
|
slug: existingConfig?.slug,
|
|
@@ -5728,7 +6760,7 @@ function detectEnvironment() {
|
|
|
5728
6760
|
};
|
|
5729
6761
|
}
|
|
5730
6762
|
function loadExistingAnswers(options) {
|
|
5731
|
-
const detection = detectEnvironment();
|
|
6763
|
+
const detection = detectEnvironment(options.profile === "local-work" || hasLocalWorkSelection());
|
|
5732
6764
|
const effectiveProfile = resolveMaintenanceProfile(detection, options.profile);
|
|
5733
6765
|
const deployment = resolveRequestedDeployment(options);
|
|
5734
6766
|
const providedApiKey = options.apiKey?.trim();
|
|
@@ -5750,9 +6782,9 @@ function loadExistingAnswers(options) {
|
|
|
5750
6782
|
apiKeyType,
|
|
5751
6783
|
profile: effectiveProfile,
|
|
5752
6784
|
aiClient: detection.aiClient,
|
|
5753
|
-
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",
|
|
5754
6786
|
skipMcp: options.withMcp === true ? false : !detection.existingConfig.hasSniparaServer && effectiveProfile === "runtime-only",
|
|
5755
|
-
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",
|
|
5756
6788
|
withOrchestrator: Boolean(options.withOrchestrator || detection.tooling.orchestratorCli),
|
|
5757
6789
|
skipHooks: options.withHooks ? false : !shouldInstallDefaultHooks(detection.aiClient)
|
|
5758
6790
|
});
|
|
@@ -5761,6 +6793,7 @@ function resolveMaintenanceProfile(detection, explicitProfile) {
|
|
|
5761
6793
|
if (explicitProfile && isInstallProfile(explicitProfile)) {
|
|
5762
6794
|
return explicitProfile;
|
|
5763
6795
|
}
|
|
6796
|
+
if (hasLocalWorkSelection()) return "local-work";
|
|
5764
6797
|
if (detection.existingConfig.hasRuntimeServer && detection.existingConfig.hasSniparaServer) {
|
|
5765
6798
|
return detection.tooling.companionCli ? "full-stack" : "full-stack";
|
|
5766
6799
|
}
|
|
@@ -5805,22 +6838,22 @@ function collectConfiguredProjectSlugSignals(existingConfig = getExistingConfig(
|
|
|
5805
6838
|
{ source: ".snipara/project", slug: readProjectSlugFromProjectBinding() },
|
|
5806
6839
|
{
|
|
5807
6840
|
source: ".codex/config.toml",
|
|
5808
|
-
slug: readProjectSlugFromTextFile(
|
|
6841
|
+
slug: readProjectSlugFromTextFile(path4.join(process.cwd(), ".codex", "config.toml"))
|
|
5809
6842
|
},
|
|
5810
6843
|
{
|
|
5811
6844
|
source: ".snipara/templates/codex-config.toml",
|
|
5812
6845
|
slug: readProjectSlugFromTextFile(
|
|
5813
|
-
|
|
6846
|
+
path4.join(process.cwd(), ".snipara", "templates", "codex-config.toml")
|
|
5814
6847
|
)
|
|
5815
6848
|
},
|
|
5816
6849
|
{
|
|
5817
6850
|
source: ".vibe/config.toml",
|
|
5818
|
-
slug: readProjectSlugFromTextFile(
|
|
6851
|
+
slug: readProjectSlugFromTextFile(path4.join(process.cwd(), ".vibe", "config.toml"))
|
|
5819
6852
|
},
|
|
5820
6853
|
{
|
|
5821
6854
|
source: ".snipara/templates/mistral-vibe-config.toml",
|
|
5822
6855
|
slug: readProjectSlugFromTextFile(
|
|
5823
|
-
|
|
6856
|
+
path4.join(process.cwd(), ".snipara", "templates", "mistral-vibe-config.toml")
|
|
5824
6857
|
)
|
|
5825
6858
|
},
|
|
5826
6859
|
{ source: ".env.local", slug: readProjectSlugFromEnvFile(".env.local") },
|
|
@@ -5858,22 +6891,22 @@ function extractSlugFromText(content) {
|
|
|
5858
6891
|
return normalizeSlugCandidate(match?.[1] ? decodeURIComponent(match[1]) : null);
|
|
5859
6892
|
}
|
|
5860
6893
|
function readProjectSlugFromTextFile(filePath) {
|
|
5861
|
-
if (!
|
|
6894
|
+
if (!fs4.existsSync(filePath)) {
|
|
5862
6895
|
return null;
|
|
5863
6896
|
}
|
|
5864
6897
|
try {
|
|
5865
|
-
return extractSlugFromText(
|
|
6898
|
+
return extractSlugFromText(fs4.readFileSync(filePath, "utf-8"));
|
|
5866
6899
|
} catch {
|
|
5867
6900
|
return null;
|
|
5868
6901
|
}
|
|
5869
6902
|
}
|
|
5870
6903
|
function readProjectSlugFromProjectBinding() {
|
|
5871
|
-
const filePath =
|
|
5872
|
-
if (!
|
|
6904
|
+
const filePath = path4.join(process.cwd(), ".snipara", "project");
|
|
6905
|
+
if (!fs4.existsSync(filePath)) {
|
|
5873
6906
|
return null;
|
|
5874
6907
|
}
|
|
5875
6908
|
try {
|
|
5876
|
-
const content =
|
|
6909
|
+
const content = fs4.readFileSync(filePath, "utf-8").trim();
|
|
5877
6910
|
if (!content) {
|
|
5878
6911
|
return null;
|
|
5879
6912
|
}
|
|
@@ -5909,12 +6942,12 @@ function readProjectSlugFromEnvFile(file) {
|
|
|
5909
6942
|
return null;
|
|
5910
6943
|
}
|
|
5911
6944
|
function readEnvValueFromFile(file, names) {
|
|
5912
|
-
const filePath =
|
|
5913
|
-
if (!
|
|
6945
|
+
const filePath = path4.join(process.cwd(), file);
|
|
6946
|
+
if (!fs4.existsSync(filePath)) {
|
|
5914
6947
|
return null;
|
|
5915
6948
|
}
|
|
5916
6949
|
try {
|
|
5917
|
-
const content =
|
|
6950
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
5918
6951
|
for (const line of content.split(/\r?\n/)) {
|
|
5919
6952
|
const match = line.trim().match(/^(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/);
|
|
5920
6953
|
if (!match || !names.includes(match[1])) {
|
|
@@ -5954,7 +6987,7 @@ function detectRepoProjectSlug() {
|
|
|
5954
6987
|
return null;
|
|
5955
6988
|
}
|
|
5956
6989
|
function detectDirectoryProjectSlug() {
|
|
5957
|
-
return slugifyFallback(
|
|
6990
|
+
return slugifyFallback(path4.basename(process.cwd()));
|
|
5958
6991
|
}
|
|
5959
6992
|
function detectGitRepository() {
|
|
5960
6993
|
try {
|
|
@@ -6016,50 +7049,50 @@ function isGitRepo() {
|
|
|
6016
7049
|
}
|
|
6017
7050
|
}
|
|
6018
7051
|
function detectAiClient() {
|
|
6019
|
-
if (
|
|
7052
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".codex"))) {
|
|
6020
7053
|
return "codex";
|
|
6021
7054
|
}
|
|
6022
|
-
if (
|
|
7055
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".cursor"))) {
|
|
6023
7056
|
return "cursor";
|
|
6024
7057
|
}
|
|
6025
|
-
if (
|
|
7058
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".kimi-code"))) {
|
|
6026
7059
|
return "kimi";
|
|
6027
7060
|
}
|
|
6028
|
-
if (
|
|
7061
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".gemini"))) {
|
|
6029
7062
|
return "gemini";
|
|
6030
7063
|
}
|
|
6031
|
-
if (
|
|
7064
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".vibe"))) {
|
|
6032
7065
|
return "mistral";
|
|
6033
7066
|
}
|
|
6034
|
-
if (
|
|
7067
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".continue"))) {
|
|
6035
7068
|
return "continue";
|
|
6036
7069
|
}
|
|
6037
|
-
if (
|
|
7070
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".vscode"))) {
|
|
6038
7071
|
return "vscode";
|
|
6039
7072
|
}
|
|
6040
|
-
if (
|
|
7073
|
+
if (fs4.existsSync(path4.join(homedir2(), ".claude")) || process.env.CLAUDE_CODE) {
|
|
6041
7074
|
return "claude-code";
|
|
6042
7075
|
}
|
|
6043
|
-
if (
|
|
7076
|
+
if (fs4.existsSync(path4.join(homedir2(), ".cursor")) || process.env.CURSOR_EDITOR) {
|
|
6044
7077
|
return "cursor";
|
|
6045
7078
|
}
|
|
6046
|
-
if (
|
|
7079
|
+
if (fs4.existsSync(path4.join(homedir2(), ".kimi-code")) || process.env.KIMI_CODE_HOME) {
|
|
6047
7080
|
return "kimi";
|
|
6048
7081
|
}
|
|
6049
|
-
if (
|
|
7082
|
+
if (fs4.existsSync(path4.join(homedir2(), ".gemini")) || process.env.GEMINI_CLI) {
|
|
6050
7083
|
return "gemini";
|
|
6051
7084
|
}
|
|
6052
|
-
if (
|
|
7085
|
+
if (fs4.existsSync(path4.join(homedir2(), ".vibe")) || process.env.MISTRAL_VIBE) {
|
|
6053
7086
|
return "mistral";
|
|
6054
7087
|
}
|
|
6055
|
-
if (
|
|
7088
|
+
if (fs4.existsSync(path4.join(homedir2(), ".continue")) || process.env.CONTINUE_GLOBAL_DIR) {
|
|
6056
7089
|
return "continue";
|
|
6057
7090
|
}
|
|
6058
|
-
const claudeDesktopPath = process.platform === "darwin" ?
|
|
6059
|
-
if (
|
|
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)) {
|
|
6060
7093
|
return "claude-desktop";
|
|
6061
7094
|
}
|
|
6062
|
-
if (
|
|
7095
|
+
if (fs4.existsSync(path4.join(homedir2(), ".codex"))) {
|
|
6063
7096
|
return "codex";
|
|
6064
7097
|
}
|
|
6065
7098
|
return "other";
|
|
@@ -6074,12 +7107,12 @@ function detectPipCommand() {
|
|
|
6074
7107
|
return null;
|
|
6075
7108
|
}
|
|
6076
7109
|
function getExistingConfig() {
|
|
6077
|
-
const mcpConfigPath =
|
|
6078
|
-
if (!
|
|
7110
|
+
const mcpConfigPath = path4.join(process.cwd(), ".mcp.json");
|
|
7111
|
+
if (!fs4.existsSync(mcpConfigPath)) {
|
|
6079
7112
|
return null;
|
|
6080
7113
|
}
|
|
6081
7114
|
try {
|
|
6082
|
-
const config = JSON.parse(
|
|
7115
|
+
const config = JSON.parse(fs4.readFileSync(mcpConfigPath, "utf-8"));
|
|
6083
7116
|
const sniparaServer = config.mcpServers?.snipara;
|
|
6084
7117
|
const runtimeServer = config.mcpServers?.[SANDBOX_SERVER_NAME] || config.mcpServers?.[LEGACY_RUNTIME_SERVER_NAME];
|
|
6085
7118
|
return {
|
|
@@ -6099,12 +7132,12 @@ function getExistingConfig() {
|
|
|
6099
7132
|
}
|
|
6100
7133
|
}
|
|
6101
7134
|
function readExistingCompanionProjectId() {
|
|
6102
|
-
const configPath =
|
|
6103
|
-
if (!
|
|
7135
|
+
const configPath = path4.join(process.cwd(), ".snipara", "companion", "config.json");
|
|
7136
|
+
if (!fs4.existsSync(configPath)) {
|
|
6104
7137
|
return void 0;
|
|
6105
7138
|
}
|
|
6106
7139
|
try {
|
|
6107
|
-
const parsed = JSON.parse(
|
|
7140
|
+
const parsed = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
6108
7141
|
return typeof parsed.projectId === "string" && parsed.projectId ? parsed.projectId : void 0;
|
|
6109
7142
|
} catch {
|
|
6110
7143
|
return void 0;
|