create-snipara 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/dist/index.js +1400 -368
- 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";
|
|
@@ -98,7 +874,7 @@ var COMPANION_PACKAGE_NAME = "snipara-companion";
|
|
|
98
874
|
var COMPANION_COMMAND_NAME = "snipara-companion";
|
|
99
875
|
var COMPANION_NPX_COMMAND = "npx -y snipara-companion@latest";
|
|
100
876
|
var CREATE_SNIPARA_NPX_COMMAND = "npx -y create-snipara@latest";
|
|
101
|
-
var MIN_COMPANION_VERSION = "3.
|
|
877
|
+
var MIN_COMPANION_VERSION = "3.8.0";
|
|
102
878
|
var GITHUB_ANSWER_PACKS_LABEL = "GitHub PR Answer Packs";
|
|
103
879
|
var MCP_PACKAGE_NAME = "snipara-mcp";
|
|
104
880
|
var SANDBOX_PACKAGE_NAME = "snipara-sandbox[all]";
|
|
@@ -139,37 +915,37 @@ var DECISION_COMMIT_TERMS = [
|
|
|
139
915
|
"switch"
|
|
140
916
|
];
|
|
141
917
|
function buildActivationId() {
|
|
142
|
-
return `act_${Date.now().toString(36)}_${
|
|
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
|
}
|
|
@@ -3510,8 +4488,7 @@ Agent task lifecycle:
|
|
|
3510
4488
|
commit SHA, indexing time, included file sample, coverage, freshness warnings,
|
|
3511
4489
|
and sync guidance before relying on it. If \`git status --short\` is dirty,
|
|
3512
4490
|
warn that the indexed graph may not include local edits.
|
|
3513
|
-
12.
|
|
3514
|
-
isolated transformations materially help.
|
|
4491
|
+
12. Before executable repository tests, probe the required Snipara Sandbox capabilities using \`validation gate\` with an explicit request and workflow/lot scope. If absent, inaccessible or incompatible, ask the user to choose another test environment, install/configure Sandbox, or continue without Sandbox (local tests versus no tests). Never fall back silently or treat no response as approval; preserve the scoped choice and proof limits across resume. Static reads and documentation do not require a container.
|
|
3515
4492
|
13. If a broad query is slow, retry once with a narrow keyword query before
|
|
3516
4493
|
falling back to local search.
|
|
3517
4494
|
14. When generated hooks are enabled, \`snipara-companion pre-tool\`, \`post-tool\`,
|
|
@@ -3562,7 +4539,7 @@ ${EXPERIENCE_GOAL_SECTION}
|
|
|
3562
4539
|
- If a broad query is slow, retry once with a narrow keyword query before falling back to local search.
|
|
3563
4540
|
- When the plan is visible and \`snipara-companion\` is installed, keep the machine plan in JSON, use \`snipara-companion workflow start\`, \`phase-start\`, and \`phase-commit\` per phase, and after \`workflow resume\` rerun \`workflow phase-start\` before editing again.
|
|
3564
4541
|
- If generated hooks are enabled, \`snipara-companion pre-tool\`, \`post-tool\`, and \`stuck-guard status\` provide runtime Rescue Pack checks when the agent loops on failures or empty searches.
|
|
3565
|
-
-
|
|
4542
|
+
- Before executable repository tests, run \`validation gate\` with an explicit request and workflow/lot scope. If unavailable or incompatible, ask the user to choose another environment, install/configure Sandbox, or continue without Sandbox (local tests versus no tests). Never fall back silently or infer approval. Preserve the scoped choice and proof requirements across resume. For runtime-bound phases, capture compact rehydratable state with \`workflow runtime-checkpoint <phase_id> --summary "<state>" --rehydrate-file <state.json>\`. Then \`workflow resume\` restores workflow and memory continuity plus the recorded Sandbox binding and prints a reattach or rehydrate plan. It does not snapshot or exactly restore a live Snipara Sandbox or REPL process.
|
|
3566
4543
|
- End substantial work with \`snipara_end_of_task_commit\` when available; for managed workflows, end every phase with \`snipara-companion workflow phase-commit\` and the task with \`snipara-companion final-commit\`.
|
|
3567
4544
|
- Use \`snipara_remember_if_novel\` or \`snipara_remember\` only for narrow durable memories.
|
|
3568
4545
|
- When remembered knowledge is obsolete or replaced and \`snipara-companion\` is installed, use \`snipara-companion memory invalidate <memory-id>\` or \`snipara-companion memory supersede <old-memory-id> <new-memory-id>\`; otherwise use hosted \`snipara_memory_invalidate\` or \`snipara_memory_supersede\`.
|
|
@@ -3603,18 +4580,24 @@ ${EXPERIENCE_GOAL_SECTION}
|
|
|
3603
4580
|
- If a broad query is slow, retry once with a narrow keyword query before falling back to local search.
|
|
3604
4581
|
- When \`snipara-companion\` is installed and the LLM has a visible multi-phase plan, keep the machine plan in JSON, use \`snipara-companion workflow start\`, \`phase-start\`, and \`phase-commit\` per phase, and after \`workflow resume\` rerun \`workflow phase-start\` before editing again.
|
|
3605
4582
|
- Generated hooks can call \`snipara-companion pre-tool\`, \`post-tool\`, and \`stuck-guard status\` so Rescue Packs are runtime-triggered rather than relying only on instructions.
|
|
3606
|
-
-
|
|
4583
|
+
- Before executable repository tests, run \`validation gate\` with an explicit request and workflow/lot scope. If unavailable or incompatible, ask the user to choose another environment, install/configure Sandbox, or continue without Sandbox (local tests versus no tests). Never fall back silently or infer approval. Preserve the scoped choice and proof requirements across resume. For runtime-bound phases, capture compact rehydratable state with \`workflow runtime-checkpoint <phase_id> --summary "<state>" --rehydrate-file <state.json>\`. Then \`workflow resume\` restores workflow and memory continuity plus the recorded Sandbox binding and prints a reattach or rehydrate plan. It does not snapshot or exactly restore a live Snipara Sandbox or REPL process.
|
|
3607
4584
|
- End substantial work with \`snipara_end_of_task_commit\` when available; for managed workflows, end every phase with \`snipara-companion workflow phase-commit\` and the task with \`snipara-companion final-commit\`. Use \`snipara_remember_if_novel\` or \`snipara_remember\` only for narrow durable memories. When remembered knowledge is obsolete or replaced, use \`snipara-companion memory invalidate <memory-id>\` or \`snipara-companion memory supersede <old-memory-id> <new-memory-id>\` when companion is installed; otherwise use hosted \`snipara_memory_invalidate\` or \`snipara_memory_supersede\`. Never store secrets.
|
|
3608
4585
|
`;
|
|
3609
4586
|
}
|
|
3610
|
-
function
|
|
3611
|
-
return `[mcp_servers.snipara]
|
|
4587
|
+
function buildCodexHostedConfigTemplate(answers) {
|
|
4588
|
+
return managedSetupOnly(answers) ? "" : `[mcp_servers.snipara]
|
|
3612
4589
|
type = "streamable_http"
|
|
3613
4590
|
url = "${buildHostedMcpEndpoint(answers)}"
|
|
3614
4591
|
bearer_token_env_var = "SNIPARA_API_KEY"
|
|
3615
4592
|
http_headers = { "X-Snipara-Session-Id" = "${getCorrelationSessionId(answers)}" }
|
|
3616
4593
|
`;
|
|
3617
4594
|
}
|
|
4595
|
+
function buildCodexConfigTemplate(answers) {
|
|
4596
|
+
const hosted = buildCodexHostedConfigTemplate(answers);
|
|
4597
|
+
if (answers.profile !== "local-work") return hosted;
|
|
4598
|
+
const native = managedNativeServer(answers);
|
|
4599
|
+
return hosted + "\n" + managedCodexSnippet({ command: native.command, args: native.args });
|
|
4600
|
+
}
|
|
3618
4601
|
function buildChatGptMcpTemplate(answers) {
|
|
3619
4602
|
return `${JSON.stringify(
|
|
3620
4603
|
{
|
|
@@ -3782,11 +4765,11 @@ function buildGenericMcpReferenceTemplate(answers, client) {
|
|
|
3782
4765
|
`;
|
|
3783
4766
|
}
|
|
3784
4767
|
function ensureGitignoreEntry(entry, comment) {
|
|
3785
|
-
const gitignorePath =
|
|
3786
|
-
if (
|
|
3787
|
-
const gitignore =
|
|
4768
|
+
const gitignorePath = path4.join(process.cwd(), ".gitignore");
|
|
4769
|
+
if (fs4.existsSync(gitignorePath)) {
|
|
4770
|
+
const gitignore = fs4.readFileSync(gitignorePath, "utf-8");
|
|
3788
4771
|
if (!gitignore.includes(entry)) {
|
|
3789
|
-
|
|
4772
|
+
fs4.appendFileSync(gitignorePath, `
|
|
3790
4773
|
${comment}
|
|
3791
4774
|
${entry}
|
|
3792
4775
|
`);
|
|
@@ -3794,27 +4777,31 @@ ${entry}
|
|
|
3794
4777
|
}
|
|
3795
4778
|
return;
|
|
3796
4779
|
}
|
|
3797
|
-
|
|
4780
|
+
fs4.writeFileSync(gitignorePath, `${comment}
|
|
3798
4781
|
${entry}
|
|
3799
4782
|
`);
|
|
3800
4783
|
console.log(chalk.green(` \u2713 Created .gitignore with ${entry}`));
|
|
3801
4784
|
}
|
|
3802
4785
|
async function createClaudeDesktopConfig(answers) {
|
|
3803
|
-
const configDir = process.platform === "darwin" ?
|
|
3804
|
-
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");
|
|
3805
4788
|
console.log(chalk.dim(`
|
|
3806
4789
|
Claude Desktop config: ${configPath}`));
|
|
3807
4790
|
try {
|
|
3808
|
-
if (
|
|
3809
|
-
|
|
4791
|
+
if (answers.profile === "local-work") assertManagedMcpConfig(configPath);
|
|
4792
|
+
if (!fs4.existsSync(configDir)) {
|
|
4793
|
+
fs4.mkdirSync(configDir, { recursive: true });
|
|
3810
4794
|
}
|
|
3811
4795
|
let config = {};
|
|
3812
|
-
if (
|
|
3813
|
-
config = JSON.parse(
|
|
4796
|
+
if (fs4.existsSync(configPath)) {
|
|
4797
|
+
config = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
3814
4798
|
}
|
|
3815
4799
|
config.mcpServers = config.mcpServers || {};
|
|
3816
|
-
config.mcpServers
|
|
3817
|
-
|
|
4800
|
+
if (!(answers.profile === "local-work" && (managedSetupOnly(answers) || config.mcpServers["snipara-repository"] || asObjectRecord(asObjectRecord(readJsonFile(path4.join(process.cwd(), ".mcp.json"))).mcpServers)["snipara-repository"])))
|
|
4801
|
+
config.mcpServers.snipara = buildHostedMcpServer(answers);
|
|
4802
|
+
if (answers.profile === "local-work") {
|
|
4803
|
+
config.mcpServers["snipara-local"] = managedNativeServer(answers);
|
|
4804
|
+
} else if (answers.installRuntime) {
|
|
3818
4805
|
config.mcpServers[SANDBOX_SERVER_NAME] = {
|
|
3819
4806
|
command: detectSandboxCommand() || SANDBOX_COMMAND_NAME,
|
|
3820
4807
|
args: ["mcp-serve"],
|
|
@@ -3824,18 +4811,19 @@ async function createClaudeDesktopConfig(answers) {
|
|
|
3824
4811
|
}
|
|
3825
4812
|
};
|
|
3826
4813
|
}
|
|
3827
|
-
|
|
4814
|
+
fs4.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
3828
4815
|
console.log(chalk.green(" \u2713 Updated Claude Desktop config"));
|
|
3829
4816
|
} catch (error) {
|
|
4817
|
+
if (answers.profile === "local-work") throw error;
|
|
3830
4818
|
console.log(chalk.yellow(` \u26A0\uFE0F Could not update Claude Desktop config: ${error}`));
|
|
3831
4819
|
}
|
|
3832
4820
|
}
|
|
3833
4821
|
async function installClaudeCodeHooks(answers) {
|
|
3834
4822
|
console.log(chalk.bold("\n\u{1FA9D} Installing Claude Code Hooks\n"));
|
|
3835
|
-
const claudeDir =
|
|
3836
|
-
const hooksDir =
|
|
3837
|
-
if (!
|
|
3838
|
-
|
|
4823
|
+
const claudeDir = path4.join(process.cwd(), ".claude");
|
|
4824
|
+
const hooksDir = path4.join(claudeDir, "hooks");
|
|
4825
|
+
if (!fs4.existsSync(hooksDir)) {
|
|
4826
|
+
fs4.mkdirSync(hooksDir, { recursive: true });
|
|
3839
4827
|
}
|
|
3840
4828
|
const hooks = [
|
|
3841
4829
|
{
|
|
@@ -3856,16 +4844,16 @@ async function installClaudeCodeHooks(answers) {
|
|
|
3856
4844
|
}
|
|
3857
4845
|
];
|
|
3858
4846
|
for (const hook of hooks) {
|
|
3859
|
-
const hookPath =
|
|
3860
|
-
|
|
3861
|
-
|
|
4847
|
+
const hookPath = path4.join(hooksDir, hook.name);
|
|
4848
|
+
fs4.writeFileSync(hookPath, hook.content);
|
|
4849
|
+
fs4.chmodSync(hookPath, "755");
|
|
3862
4850
|
console.log(chalk.green(` \u2713 Created ${hook.name}`));
|
|
3863
4851
|
}
|
|
3864
|
-
const settingsPath =
|
|
4852
|
+
const settingsPath = path4.join(claudeDir, "settings.json");
|
|
3865
4853
|
let settings = {};
|
|
3866
|
-
if (
|
|
4854
|
+
if (fs4.existsSync(settingsPath)) {
|
|
3867
4855
|
try {
|
|
3868
|
-
settings = JSON.parse(
|
|
4856
|
+
settings = JSON.parse(fs4.readFileSync(settingsPath, "utf-8"));
|
|
3869
4857
|
} catch {
|
|
3870
4858
|
settings = {};
|
|
3871
4859
|
}
|
|
@@ -3929,13 +4917,13 @@ async function installClaudeCodeHooks(answers) {
|
|
|
3929
4917
|
for (const { event, entry } of hookEntries) {
|
|
3930
4918
|
const eventHooks = Array.isArray(hooksConfig[event]) ? hooksConfig[event] : [];
|
|
3931
4919
|
const expectedCommand = entry.hooks[0].command;
|
|
3932
|
-
const
|
|
3933
|
-
if (!
|
|
4920
|
+
const exists2 = eventHooks.some((hook) => hookConfigEntryHasCommand(hook, expectedCommand));
|
|
4921
|
+
if (!exists2) {
|
|
3934
4922
|
eventHooks.push(entry);
|
|
3935
4923
|
hooksConfig[event] = eventHooks;
|
|
3936
4924
|
}
|
|
3937
4925
|
}
|
|
3938
|
-
|
|
4926
|
+
fs4.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
3939
4927
|
console.log(chalk.green(" \u2713 Updated .claude/settings.json hooks"));
|
|
3940
4928
|
}
|
|
3941
4929
|
function createClaudeProjectHookCommand(scriptName) {
|
|
@@ -4075,9 +5063,9 @@ fi
|
|
|
4075
5063
|
}
|
|
4076
5064
|
async function updateEnvFiles(answers) {
|
|
4077
5065
|
console.log(chalk.bold("\n\u{1F4DD} Updating Environment Files\n"));
|
|
4078
|
-
const envExamplePath =
|
|
4079
|
-
const envLocalPath =
|
|
4080
|
-
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");
|
|
4081
5069
|
let envVars = "";
|
|
4082
5070
|
let envExampleVars = "";
|
|
4083
5071
|
if (answers.profile !== "runtime-only") {
|
|
@@ -4115,26 +5103,26 @@ ${llmVarName}=your_${answers.llmProvider}_api_key_here`;
|
|
|
4115
5103
|
console.log(chalk.dim(" No environment variables to save"));
|
|
4116
5104
|
return;
|
|
4117
5105
|
}
|
|
4118
|
-
if (envExampleVars &&
|
|
4119
|
-
const content =
|
|
5106
|
+
if (envExampleVars && fs4.existsSync(envExamplePath)) {
|
|
5107
|
+
const content = fs4.readFileSync(envExamplePath, "utf-8");
|
|
4120
5108
|
const missingLines = envExampleVars.split("\n").filter((line) => {
|
|
4121
5109
|
const match = line.match(/^([A-Z0-9_]+)=/);
|
|
4122
5110
|
return Boolean(match && !content.includes(`${match[1]}=`));
|
|
4123
5111
|
});
|
|
4124
5112
|
if (missingLines.length > 0) {
|
|
4125
|
-
|
|
5113
|
+
fs4.appendFileSync(envExamplePath, `
|
|
4126
5114
|
|
|
4127
5115
|
${missingLines.join("\n")}
|
|
4128
5116
|
`);
|
|
4129
5117
|
console.log(chalk.green(" \u2713 Updated .env.example"));
|
|
4130
5118
|
}
|
|
4131
5119
|
} else if (envExampleVars) {
|
|
4132
|
-
|
|
5120
|
+
fs4.writeFileSync(envExamplePath, envExampleVars + "\n");
|
|
4133
5121
|
console.log(chalk.green(" \u2713 Created .env.example"));
|
|
4134
5122
|
}
|
|
4135
|
-
const targetEnvPath =
|
|
4136
|
-
if (
|
|
4137
|
-
const content =
|
|
5123
|
+
const targetEnvPath = fs4.existsSync(envLocalPath) ? envLocalPath : envPath;
|
|
5124
|
+
if (fs4.existsSync(targetEnvPath)) {
|
|
5125
|
+
const content = fs4.readFileSync(targetEnvPath, "utf-8");
|
|
4138
5126
|
const missingLines = envVars.split("\n").filter((line) => {
|
|
4139
5127
|
const match = line.match(/^([A-Z0-9_]+)=/);
|
|
4140
5128
|
if (!match) {
|
|
@@ -4146,26 +5134,26 @@ ${missingLines.join("\n")}
|
|
|
4146
5134
|
return !content.includes(`${match[1]}=`);
|
|
4147
5135
|
});
|
|
4148
5136
|
if (missingLines.length > 0) {
|
|
4149
|
-
|
|
5137
|
+
fs4.appendFileSync(targetEnvPath, `
|
|
4150
5138
|
|
|
4151
5139
|
${missingLines.join("\n")}
|
|
4152
5140
|
`);
|
|
4153
|
-
console.log(chalk.green(` \u2713 Updated ${
|
|
5141
|
+
console.log(chalk.green(` \u2713 Updated ${path4.basename(targetEnvPath)}`));
|
|
4154
5142
|
}
|
|
4155
5143
|
return;
|
|
4156
5144
|
}
|
|
4157
5145
|
if (!answers.apiKey.includes("YOUR_") && answers.profile !== "runtime-only" || answers.llmApiKey) {
|
|
4158
|
-
|
|
4159
|
-
console.log(chalk.green(` \u2713 Created ${
|
|
5146
|
+
fs4.writeFileSync(targetEnvPath, envVars + "\n");
|
|
5147
|
+
console.log(chalk.green(` \u2713 Created ${path4.basename(targetEnvPath)}`));
|
|
4160
5148
|
}
|
|
4161
5149
|
}
|
|
4162
5150
|
async function createCompanionConfig(answers) {
|
|
4163
5151
|
if (!answers.installCompanion || answers.apiKeyType === "none" || !answers.apiKey || answers.apiKey.includes("YOUR_")) {
|
|
4164
5152
|
return;
|
|
4165
5153
|
}
|
|
4166
|
-
const configDir =
|
|
4167
|
-
const configPath =
|
|
4168
|
-
|
|
5154
|
+
const configDir = path4.join(process.cwd(), ".snipara", "companion");
|
|
5155
|
+
const configPath = path4.join(configDir, "config.json");
|
|
5156
|
+
fs4.mkdirSync(configDir, { recursive: true });
|
|
4169
5157
|
const existingWorkspaceConfig = readCompanionConfigFile(configPath);
|
|
4170
5158
|
const workspaceConfig = {
|
|
4171
5159
|
...existingWorkspaceConfig,
|
|
@@ -4175,7 +5163,7 @@ async function createCompanionConfig(answers) {
|
|
|
4175
5163
|
sessionId: getCorrelationSessionId(answers),
|
|
4176
5164
|
client: getAutomationClientId(answers.aiClient)
|
|
4177
5165
|
};
|
|
4178
|
-
|
|
5166
|
+
fs4.writeFileSync(configPath, JSON.stringify(workspaceConfig, null, 2) + "\n");
|
|
4179
5167
|
ensureGitignoreEntry(".snipara/companion/config.json", "# Snipara local companion config");
|
|
4180
5168
|
console.log(chalk.green(" \u2713 Configured local companion config"));
|
|
4181
5169
|
}
|
|
@@ -4245,11 +5233,11 @@ async function syncRemoteAutomationClient(answers, options) {
|
|
|
4245
5233
|
}
|
|
4246
5234
|
}
|
|
4247
5235
|
function readCompanionConfigFile(filePath) {
|
|
4248
|
-
if (!
|
|
5236
|
+
if (!fs4.existsSync(filePath)) {
|
|
4249
5237
|
return {};
|
|
4250
5238
|
}
|
|
4251
5239
|
try {
|
|
4252
|
-
const parsed = JSON.parse(
|
|
5240
|
+
const parsed = JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
4253
5241
|
return typeof parsed === "object" && parsed !== null ? parsed : {};
|
|
4254
5242
|
} catch {
|
|
4255
5243
|
return {};
|
|
@@ -4763,13 +5751,13 @@ ${guidance.map((line) => `- ${line}`).join("\n")}
|
|
|
4763
5751
|
}
|
|
4764
5752
|
async function createCompanionPack(answers) {
|
|
4765
5753
|
console.log(chalk.bold("\n\u{1F5C2}\uFE0F Generating Companion Pack\n"));
|
|
4766
|
-
const packDir =
|
|
5754
|
+
const packDir = path4.join(process.cwd(), ".snipara", "companion");
|
|
4767
5755
|
const commands = getCompanionCommands(answers);
|
|
4768
5756
|
const guidance = [UNIVERSAL_CLIENT_GUIDANCE, ...getClientGuidance(answers.aiClient)];
|
|
4769
5757
|
const detection = detectEnvironment();
|
|
4770
5758
|
const clientPackFilename = getClientPackFilename(answers.aiClient);
|
|
4771
5759
|
const deploymentLabel = getDeploymentLabel(answers.deployment);
|
|
4772
|
-
|
|
5760
|
+
fs4.mkdirSync(packDir, { recursive: true });
|
|
4773
5761
|
const readme = `# Snipara Companion Pack
|
|
4774
5762
|
|
|
4775
5763
|
This project uses ${deploymentLabel} as a project intelligence layer with source-backed context, reviewed memory, and local companion workflow helpers.
|
|
@@ -4916,12 +5904,12 @@ ${item.purpose}`
|
|
|
4916
5904
|
guidance,
|
|
4917
5905
|
detection
|
|
4918
5906
|
};
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
5907
|
+
fs4.writeFileSync(path4.join(packDir, "README.md"), readme + "\n");
|
|
5908
|
+
fs4.writeFileSync(
|
|
5909
|
+
path4.join(packDir, clientPackFilename),
|
|
4922
5910
|
buildClientPackMarkdown(answers, commands, guidance) + "\n"
|
|
4923
5911
|
);
|
|
4924
|
-
|
|
5912
|
+
fs4.writeFileSync(path4.join(packDir, "commands.json"), JSON.stringify(pack, null, 2) + "\n");
|
|
4925
5913
|
console.log(chalk.green(" \u2713 Created .snipara/companion/README.md"));
|
|
4926
5914
|
console.log(chalk.green(` \u2713 Created .snipara/companion/${clientPackFilename}`));
|
|
4927
5915
|
console.log(chalk.green(" \u2713 Created .snipara/companion/commands.json"));
|
|
@@ -4935,8 +5923,8 @@ async function runSandboxInit() {
|
|
|
4935
5923
|
`
|
|
4936
5924
|
)
|
|
4937
5925
|
);
|
|
4938
|
-
return new Promise((
|
|
4939
|
-
const child =
|
|
5926
|
+
return new Promise((resolve2) => {
|
|
5927
|
+
const child = spawn2(sandboxCommand, ["init"], {
|
|
4940
5928
|
stdio: "inherit",
|
|
4941
5929
|
shell: true
|
|
4942
5930
|
});
|
|
@@ -4948,7 +5936,7 @@ async function runSandboxInit() {
|
|
|
4948
5936
|
\u26A0\uFE0F ${sandboxCommand} init exited with code ${code}`));
|
|
4949
5937
|
console.log(chalk.dim(` You can run '${sandboxCommand} init' manually later.`));
|
|
4950
5938
|
}
|
|
4951
|
-
|
|
5939
|
+
resolve2();
|
|
4952
5940
|
});
|
|
4953
5941
|
child.on("error", (error) => {
|
|
4954
5942
|
console.log(chalk.yellow(`
|
|
@@ -4956,7 +5944,7 @@ async function runSandboxInit() {
|
|
|
4956
5944
|
console.log(
|
|
4957
5945
|
chalk.dim(` Run '${SANDBOX_COMMAND_NAME} init' manually after installation completes.`)
|
|
4958
5946
|
);
|
|
4959
|
-
|
|
5947
|
+
resolve2();
|
|
4960
5948
|
});
|
|
4961
5949
|
});
|
|
4962
5950
|
}
|
|
@@ -4993,8 +5981,8 @@ function buildGitHubAutomationUrl(answers) {
|
|
|
4993
5981
|
function getClaudeClientChecks(answers, detection) {
|
|
4994
5982
|
const checks = [];
|
|
4995
5983
|
const claudePaths = getClaudeConfigPaths();
|
|
4996
|
-
const projectConfig = readJsonFile(
|
|
4997
|
-
const projectSettings = readJsonFile(
|
|
5984
|
+
const projectConfig = readJsonFile(path4.join(process.cwd(), ".mcp.json"));
|
|
5985
|
+
const projectSettings = readJsonFile(path4.join(process.cwd(), ".claude", "settings.json"));
|
|
4998
5986
|
const codeSettings = readJsonFile(claudePaths.codeSettingsPath);
|
|
4999
5987
|
const desktopConfig = readJsonFile(claudePaths.desktopConfigPath);
|
|
5000
5988
|
const projectHasSnipara = containsSniparaMcpReference(projectConfig);
|
|
@@ -5048,11 +6036,11 @@ function getClaudeClientChecks(answers, detection) {
|
|
|
5048
6036
|
return checks;
|
|
5049
6037
|
}
|
|
5050
6038
|
function readJsonFile(filePath) {
|
|
5051
|
-
if (!
|
|
6039
|
+
if (!fs4.existsSync(filePath)) {
|
|
5052
6040
|
return null;
|
|
5053
6041
|
}
|
|
5054
6042
|
try {
|
|
5055
|
-
return JSON.parse(
|
|
6043
|
+
return JSON.parse(fs4.readFileSync(filePath, "utf-8"));
|
|
5056
6044
|
} catch {
|
|
5057
6045
|
return null;
|
|
5058
6046
|
}
|
|
@@ -5145,13 +6133,35 @@ function compareSemver(left, right) {
|
|
|
5145
6133
|
async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
5146
6134
|
console.log(chalk.bold("\n\u{1FA7A} Running Validation\n"));
|
|
5147
6135
|
const checks = [];
|
|
5148
|
-
const detection = detectEnvironment();
|
|
6136
|
+
const detection = detectEnvironment(answers.profile === "local-work");
|
|
6137
|
+
if (answers.profile === "local-work") {
|
|
6138
|
+
try {
|
|
6139
|
+
const managed = managedToolchainStatus();
|
|
6140
|
+
if (!managed.manifest) throw new Error("Managed local-work bundle is absent");
|
|
6141
|
+
answers.managedToolchain = managed.manifest;
|
|
6142
|
+
detection.tooling.companionCli = true;
|
|
6143
|
+
detection.tooling.runtimeCli = true;
|
|
6144
|
+
detection.tooling.orchestratorCli = true;
|
|
6145
|
+
checks.push({ name: "managed_toolchain", status: "pass", detail: `Private compatible bundle ${managed.manifest.bundleId}; installation is not project activation` });
|
|
6146
|
+
const native = asObjectRecord(asObjectRecord(readJsonFile(path4.join(process.cwd(), ".mcp.json"))).mcpServers)["snipara-local"];
|
|
6147
|
+
checks.push({
|
|
6148
|
+
name: "managed_native_config",
|
|
6149
|
+
status: isManagedNativeServer(native) && validManagedBootstrap(native, managed.manifest) ? "pass" : "fail",
|
|
6150
|
+
detail: "Native launcher configuration is checked against retained owned bundles; live client connectivity is separate"
|
|
6151
|
+
});
|
|
6152
|
+
} catch {
|
|
6153
|
+
detection.tooling.companionCli = false;
|
|
6154
|
+
detection.tooling.runtimeCli = false;
|
|
6155
|
+
detection.tooling.orchestratorCli = false;
|
|
6156
|
+
checks.push({ name: "managed_toolchain", status: "fail", detail: "Managed bundle is absent, changed or incompatible; existing state was preserved" });
|
|
6157
|
+
}
|
|
6158
|
+
}
|
|
5149
6159
|
const targetClient = options.client || answers.aiClient;
|
|
5150
6160
|
const effectiveAnswers = { ...answers, aiClient: targetClient };
|
|
5151
6161
|
checks.push({
|
|
5152
6162
|
name: "project_slug",
|
|
5153
|
-
status: answers.projectSlug ? "pass" : "fail",
|
|
5154
|
-
detail: answers.projectSlug || "Project slug missing"
|
|
6163
|
+
status: managedSetupOnly(answers) || answers.projectSlug ? "pass" : "fail",
|
|
6164
|
+
detail: managedSetupOnly(answers) ? "A hosted project slug is optional for local work" : answers.projectSlug || "Project slug missing"
|
|
5155
6165
|
});
|
|
5156
6166
|
const configuredSlugSignals = collectConfiguredProjectSlugSignals(detection.existingConfig);
|
|
5157
6167
|
const presentSlugSignals = configuredSlugSignals.filter((signal) => signal.slug);
|
|
@@ -5176,7 +6186,7 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
|
5176
6186
|
checks.push({
|
|
5177
6187
|
name: "companion_cli",
|
|
5178
6188
|
status: answers.installCompanion ? detection.tooling.companionCli ? "pass" : "fail" : "pass",
|
|
5179
|
-
detail: answers.installCompanion && !detection.tooling.companionCli ? getCompanionRequirementMessage() : "Companion CLI ready or not requested"
|
|
6189
|
+
detail: answers.installCompanion && !detection.tooling.companionCli ? answers.profile === "local-work" ? "Managed Companion unavailable; repair local-work" : getCompanionRequirementMessage() : "Companion CLI ready or not requested"
|
|
5180
6190
|
});
|
|
5181
6191
|
checks.push({
|
|
5182
6192
|
name: "sandbox_cli",
|
|
@@ -5219,10 +6229,12 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
|
5219
6229
|
status: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeProjectSettings ? "pass" : "warn" : "pass",
|
|
5220
6230
|
detail: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeProjectSettings ? "Project .claude/settings.json hooks present" : "Project .claude/settings.json hooks missing" : "Hooks not requested"
|
|
5221
6231
|
});
|
|
5222
|
-
if (
|
|
6232
|
+
if (managedSetupOnly(answers)) {
|
|
6233
|
+
checks.push({ name: "repository_context", status: "pass", detail: hasBoundLocalWorkSource(answers.profile) ? "Existing source connection preserved; authorization is observed by its native broker" : "Optional project context is not configured; local work needs no account" });
|
|
6234
|
+
} else if (targetClient === "claude-code" || targetClient === "claude-desktop") {
|
|
5223
6235
|
checks.push(...getClaudeClientChecks(effectiveAnswers, detection));
|
|
5224
6236
|
}
|
|
5225
|
-
if (options.includeNpmFreshness) {
|
|
6237
|
+
if (options.includeNpmFreshness && !managedSetupOnly(answers)) {
|
|
5226
6238
|
checks.push(getCreateSniparaPackageFreshnessCheck());
|
|
5227
6239
|
}
|
|
5228
6240
|
const githubRepo = answers.githubRepo || detectGitRepository();
|
|
@@ -5231,7 +6243,9 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
|
5231
6243
|
status: answers.configureGitHub ? "pass" : githubRepo ? "warn" : "pass",
|
|
5232
6244
|
detail: answers.configureGitHub ? `GitHub App setup link generated for ${githubRepo?.fullName || "detected repo"}; PR Answer Packs are enabled after browser approval` : githubRepo ? `GitHub repo ${githubRepo.fullName} detected; run ${CREATE_SNIPARA_NPX_COMMAND} --github to connect PR Answer Packs` : "No GitHub repo detected; PR Answer Packs require the GitHub App setup path"
|
|
5233
6245
|
});
|
|
5234
|
-
if (includeHostedCheck && answers
|
|
6246
|
+
if (includeHostedCheck && managedSetupOnly(answers)) {
|
|
6247
|
+
checks.push({ name: "hosted_connection", status: "pass", detail: hasBoundLocalWorkSource(answers.profile) ? "Existing native broker automatically reuses authorized project context; authorization was not probed by setup" : "No hosted connection is required for local work" });
|
|
6248
|
+
} else if (includeHostedCheck && answers.apiKeyType !== "none" && !answers.apiKey.includes("YOUR_")) {
|
|
5235
6249
|
checks.push(...await testHostedConnection(answers));
|
|
5236
6250
|
} else if (includeHostedCheck) {
|
|
5237
6251
|
checks.push({
|
|
@@ -5251,29 +6265,34 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
|
|
|
5251
6265
|
};
|
|
5252
6266
|
}
|
|
5253
6267
|
function writeDoctorReport(report) {
|
|
5254
|
-
const reportDir =
|
|
5255
|
-
|
|
5256
|
-
|
|
6268
|
+
const reportDir = path4.join(process.cwd(), ".snipara", "companion");
|
|
6269
|
+
fs4.mkdirSync(reportDir, { recursive: true });
|
|
6270
|
+
fs4.writeFileSync(path4.join(reportDir, "doctor.json"), JSON.stringify(report, null, 2) + "\n");
|
|
5257
6271
|
console.log(chalk.green(" \u2713 Wrote .snipara/companion/doctor.json"));
|
|
5258
6272
|
}
|
|
5259
6273
|
function getCodexConfigCheck(answers) {
|
|
6274
|
+
if (managedSetupOnly(answers)) return {
|
|
6275
|
+
name: "codex_config",
|
|
6276
|
+
status: "warn",
|
|
6277
|
+
detail: "Managed local launcher is configured; restart Codex and observe connection status. Optional context authorization remains separate"
|
|
6278
|
+
};
|
|
5260
6279
|
const candidates = [
|
|
5261
6280
|
{
|
|
5262
6281
|
label: ".codex/config.toml",
|
|
5263
|
-
filePath:
|
|
6282
|
+
filePath: path4.join(process.cwd(), ".codex", "config.toml")
|
|
5264
6283
|
},
|
|
5265
6284
|
{
|
|
5266
6285
|
label: ".snipara/templates/codex-config.toml",
|
|
5267
|
-
filePath:
|
|
6286
|
+
filePath: path4.join(process.cwd(), ".snipara", "templates", "codex-config.toml")
|
|
5268
6287
|
},
|
|
5269
6288
|
{
|
|
5270
6289
|
label: "~/.codex/config.toml",
|
|
5271
|
-
filePath:
|
|
6290
|
+
filePath: path4.join(homedir2(), ".codex", "config.toml")
|
|
5272
6291
|
}
|
|
5273
6292
|
];
|
|
5274
|
-
const present = candidates.filter((candidate) =>
|
|
6293
|
+
const present = candidates.filter((candidate) => fs4.existsSync(candidate.filePath)).map((candidate) => ({
|
|
5275
6294
|
label: candidate.label,
|
|
5276
|
-
content:
|
|
6295
|
+
content: fs4.readFileSync(candidate.filePath, "utf-8")
|
|
5277
6296
|
}));
|
|
5278
6297
|
if (present.length === 0) {
|
|
5279
6298
|
return {
|
|
@@ -5325,20 +6344,20 @@ function getMistralConfigCheck(answers) {
|
|
|
5325
6344
|
const candidates = [
|
|
5326
6345
|
{
|
|
5327
6346
|
label: ".vibe/config.toml",
|
|
5328
|
-
filePath:
|
|
6347
|
+
filePath: path4.join(process.cwd(), ".vibe", "config.toml")
|
|
5329
6348
|
},
|
|
5330
6349
|
{
|
|
5331
6350
|
label: ".snipara/templates/mistral-vibe-config.toml",
|
|
5332
|
-
filePath:
|
|
6351
|
+
filePath: path4.join(process.cwd(), ".snipara", "templates", "mistral-vibe-config.toml")
|
|
5333
6352
|
},
|
|
5334
6353
|
{
|
|
5335
6354
|
label: "~/.vibe/config.toml",
|
|
5336
|
-
filePath:
|
|
6355
|
+
filePath: path4.join(homedir2(), ".vibe", "config.toml")
|
|
5337
6356
|
}
|
|
5338
6357
|
];
|
|
5339
|
-
const present = candidates.filter((candidate) =>
|
|
6358
|
+
const present = candidates.filter((candidate) => fs4.existsSync(candidate.filePath)).map((candidate) => ({
|
|
5340
6359
|
label: candidate.label,
|
|
5341
|
-
content:
|
|
6360
|
+
content: fs4.readFileSync(candidate.filePath, "utf-8")
|
|
5342
6361
|
}));
|
|
5343
6362
|
if (present.length === 0) {
|
|
5344
6363
|
return {
|
|
@@ -5602,7 +6621,7 @@ function printActivationSummary(manifest) {
|
|
|
5602
6621
|
console.log(
|
|
5603
6622
|
chalk.dim(
|
|
5604
6623
|
`
|
|
5605
|
-
Activation manifest: ${
|
|
6624
|
+
Activation manifest: ${path4.relative(process.cwd(), getActivationManifestPath())}`
|
|
5606
6625
|
)
|
|
5607
6626
|
);
|
|
5608
6627
|
if (manifest.artifacts.handoffPath) {
|
|
@@ -5668,13 +6687,25 @@ function commandExists(command) {
|
|
|
5668
6687
|
}
|
|
5669
6688
|
}
|
|
5670
6689
|
function getClaudeConfigPaths() {
|
|
5671
|
-
const desktopDir = process.platform === "darwin" ?
|
|
6690
|
+
const desktopDir = process.platform === "darwin" ? path4.join(homedir2(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path4.join(homedir2(), "AppData", "Roaming", "Claude") : path4.join(homedir2(), ".config", "claude");
|
|
5672
6691
|
return {
|
|
5673
|
-
codeSettingsPath:
|
|
5674
|
-
desktopConfigPath:
|
|
6692
|
+
codeSettingsPath: path4.join(homedir2(), ".claude", "settings.json"),
|
|
6693
|
+
desktopConfigPath: path4.join(desktopDir, "claude_desktop_config.json")
|
|
5675
6694
|
};
|
|
5676
6695
|
}
|
|
5677
|
-
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()) {
|
|
5678
6709
|
const existingConfig = getExistingConfig();
|
|
5679
6710
|
const projectSlugResolution = resolveProjectSlug(existingConfig);
|
|
5680
6711
|
const claudePaths = getClaudeConfigPaths();
|
|
@@ -5686,38 +6717,38 @@ function detectEnvironment() {
|
|
|
5686
6717
|
aiClient: detectAiClient(),
|
|
5687
6718
|
inGitRepo: isGitRepo(),
|
|
5688
6719
|
files: {
|
|
5689
|
-
mcpConfig:
|
|
5690
|
-
env:
|
|
5691
|
-
envLocal:
|
|
5692
|
-
envExample:
|
|
5693
|
-
agentsMd:
|
|
5694
|
-
claudeMd:
|
|
5695
|
-
codexConfig:
|
|
5696
|
-
cursorRule:
|
|
5697
|
-
sniparaProject:
|
|
5698
|
-
claudeDir:
|
|
5699
|
-
claudeSettings:
|
|
5700
|
-
claudeProjectDir:
|
|
5701
|
-
claudeProjectSettings:
|
|
5702
|
-
claudeDesktopConfig:
|
|
5703
|
-
cursorDir:
|
|
5704
|
-
mistralVibeConfig:
|
|
5705
|
-
sniparaCompanionPack:
|
|
5706
|
-
sniparaTemplates:
|
|
5707
|
-
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"))
|
|
5708
6739
|
},
|
|
5709
6740
|
tooling: {
|
|
5710
|
-
node: commandExists("node"),
|
|
5711
|
-
npm: commandExists("npm"),
|
|
5712
|
-
pnpm: commandExists("pnpm"),
|
|
5713
|
-
python: commandExists("python3") || commandExists("python"),
|
|
5714
|
-
pip: commandExists("pip3") || commandExists("pip"),
|
|
5715
|
-
uv: commandExists("uv"),
|
|
5716
|
-
docker: commandExists("docker"),
|
|
5717
|
-
git: commandExists("git"),
|
|
5718
|
-
companionCli: Boolean(detectCompanionCommand()),
|
|
5719
|
-
runtimeCli: Boolean(detectSandboxCommand()),
|
|
5720
|
-
orchestratorCli: Boolean(detectOrchestratorCommand())
|
|
6741
|
+
node: managed ? true : commandExists("node"),
|
|
6742
|
+
npm: (managed ? executableOnPath : commandExists)("npm"),
|
|
6743
|
+
pnpm: (managed ? executableOnPath : commandExists)("pnpm"),
|
|
6744
|
+
python: (managed ? executableOnPath : commandExists)("python3") || (managed ? executableOnPath : commandExists)("python"),
|
|
6745
|
+
pip: (managed ? executableOnPath : commandExists)("pip3") || (managed ? executableOnPath : commandExists)("pip"),
|
|
6746
|
+
uv: (managed ? executableOnPath : commandExists)("uv"),
|
|
6747
|
+
docker: (managed ? executableOnPath : commandExists)("docker"),
|
|
6748
|
+
git: (managed ? executableOnPath : commandExists)("git"),
|
|
6749
|
+
companionCli: managed ? false : Boolean(detectCompanionCommand()),
|
|
6750
|
+
runtimeCli: managed ? false : Boolean(detectSandboxCommand()),
|
|
6751
|
+
orchestratorCli: managed ? false : Boolean(detectOrchestratorCommand())
|
|
5721
6752
|
},
|
|
5722
6753
|
existingConfig: {
|
|
5723
6754
|
slug: existingConfig?.slug,
|
|
@@ -5729,7 +6760,7 @@ function detectEnvironment() {
|
|
|
5729
6760
|
};
|
|
5730
6761
|
}
|
|
5731
6762
|
function loadExistingAnswers(options) {
|
|
5732
|
-
const detection = detectEnvironment();
|
|
6763
|
+
const detection = detectEnvironment(options.profile === "local-work" || hasLocalWorkSelection());
|
|
5733
6764
|
const effectiveProfile = resolveMaintenanceProfile(detection, options.profile);
|
|
5734
6765
|
const deployment = resolveRequestedDeployment(options);
|
|
5735
6766
|
const providedApiKey = options.apiKey?.trim();
|
|
@@ -5751,9 +6782,9 @@ function loadExistingAnswers(options) {
|
|
|
5751
6782
|
apiKeyType,
|
|
5752
6783
|
profile: effectiveProfile,
|
|
5753
6784
|
aiClient: detection.aiClient,
|
|
5754
|
-
skipCompanion: options.withCompanion === true ? false : !detection.tooling.companionCli && effectiveProfile !== "hosted-companion" && effectiveProfile !== "full-stack",
|
|
6785
|
+
skipCompanion: effectiveProfile === "local-work" ? false : options.withCompanion === true ? false : !detection.tooling.companionCli && effectiveProfile !== "hosted-companion" && effectiveProfile !== "full-stack",
|
|
5755
6786
|
skipMcp: options.withMcp === true ? false : !detection.existingConfig.hasSniparaServer && effectiveProfile === "runtime-only",
|
|
5756
|
-
skipRuntime: options.withRuntime === true ? false : !detection.existingConfig.hasRuntimeServer && effectiveProfile !== "full-stack" && effectiveProfile !== "runtime-only",
|
|
6787
|
+
skipRuntime: effectiveProfile === "local-work" ? false : options.withRuntime === true ? false : !detection.existingConfig.hasRuntimeServer && effectiveProfile !== "full-stack" && effectiveProfile !== "runtime-only",
|
|
5757
6788
|
withOrchestrator: Boolean(options.withOrchestrator || detection.tooling.orchestratorCli),
|
|
5758
6789
|
skipHooks: options.withHooks ? false : !shouldInstallDefaultHooks(detection.aiClient)
|
|
5759
6790
|
});
|
|
@@ -5762,6 +6793,7 @@ function resolveMaintenanceProfile(detection, explicitProfile) {
|
|
|
5762
6793
|
if (explicitProfile && isInstallProfile(explicitProfile)) {
|
|
5763
6794
|
return explicitProfile;
|
|
5764
6795
|
}
|
|
6796
|
+
if (hasLocalWorkSelection()) return "local-work";
|
|
5765
6797
|
if (detection.existingConfig.hasRuntimeServer && detection.existingConfig.hasSniparaServer) {
|
|
5766
6798
|
return detection.tooling.companionCli ? "full-stack" : "full-stack";
|
|
5767
6799
|
}
|
|
@@ -5806,22 +6838,22 @@ function collectConfiguredProjectSlugSignals(existingConfig = getExistingConfig(
|
|
|
5806
6838
|
{ source: ".snipara/project", slug: readProjectSlugFromProjectBinding() },
|
|
5807
6839
|
{
|
|
5808
6840
|
source: ".codex/config.toml",
|
|
5809
|
-
slug: readProjectSlugFromTextFile(
|
|
6841
|
+
slug: readProjectSlugFromTextFile(path4.join(process.cwd(), ".codex", "config.toml"))
|
|
5810
6842
|
},
|
|
5811
6843
|
{
|
|
5812
6844
|
source: ".snipara/templates/codex-config.toml",
|
|
5813
6845
|
slug: readProjectSlugFromTextFile(
|
|
5814
|
-
|
|
6846
|
+
path4.join(process.cwd(), ".snipara", "templates", "codex-config.toml")
|
|
5815
6847
|
)
|
|
5816
6848
|
},
|
|
5817
6849
|
{
|
|
5818
6850
|
source: ".vibe/config.toml",
|
|
5819
|
-
slug: readProjectSlugFromTextFile(
|
|
6851
|
+
slug: readProjectSlugFromTextFile(path4.join(process.cwd(), ".vibe", "config.toml"))
|
|
5820
6852
|
},
|
|
5821
6853
|
{
|
|
5822
6854
|
source: ".snipara/templates/mistral-vibe-config.toml",
|
|
5823
6855
|
slug: readProjectSlugFromTextFile(
|
|
5824
|
-
|
|
6856
|
+
path4.join(process.cwd(), ".snipara", "templates", "mistral-vibe-config.toml")
|
|
5825
6857
|
)
|
|
5826
6858
|
},
|
|
5827
6859
|
{ source: ".env.local", slug: readProjectSlugFromEnvFile(".env.local") },
|
|
@@ -5859,22 +6891,22 @@ function extractSlugFromText(content) {
|
|
|
5859
6891
|
return normalizeSlugCandidate(match?.[1] ? decodeURIComponent(match[1]) : null);
|
|
5860
6892
|
}
|
|
5861
6893
|
function readProjectSlugFromTextFile(filePath) {
|
|
5862
|
-
if (!
|
|
6894
|
+
if (!fs4.existsSync(filePath)) {
|
|
5863
6895
|
return null;
|
|
5864
6896
|
}
|
|
5865
6897
|
try {
|
|
5866
|
-
return extractSlugFromText(
|
|
6898
|
+
return extractSlugFromText(fs4.readFileSync(filePath, "utf-8"));
|
|
5867
6899
|
} catch {
|
|
5868
6900
|
return null;
|
|
5869
6901
|
}
|
|
5870
6902
|
}
|
|
5871
6903
|
function readProjectSlugFromProjectBinding() {
|
|
5872
|
-
const filePath =
|
|
5873
|
-
if (!
|
|
6904
|
+
const filePath = path4.join(process.cwd(), ".snipara", "project");
|
|
6905
|
+
if (!fs4.existsSync(filePath)) {
|
|
5874
6906
|
return null;
|
|
5875
6907
|
}
|
|
5876
6908
|
try {
|
|
5877
|
-
const content =
|
|
6909
|
+
const content = fs4.readFileSync(filePath, "utf-8").trim();
|
|
5878
6910
|
if (!content) {
|
|
5879
6911
|
return null;
|
|
5880
6912
|
}
|
|
@@ -5910,12 +6942,12 @@ function readProjectSlugFromEnvFile(file) {
|
|
|
5910
6942
|
return null;
|
|
5911
6943
|
}
|
|
5912
6944
|
function readEnvValueFromFile(file, names) {
|
|
5913
|
-
const filePath =
|
|
5914
|
-
if (!
|
|
6945
|
+
const filePath = path4.join(process.cwd(), file);
|
|
6946
|
+
if (!fs4.existsSync(filePath)) {
|
|
5915
6947
|
return null;
|
|
5916
6948
|
}
|
|
5917
6949
|
try {
|
|
5918
|
-
const content =
|
|
6950
|
+
const content = fs4.readFileSync(filePath, "utf-8");
|
|
5919
6951
|
for (const line of content.split(/\r?\n/)) {
|
|
5920
6952
|
const match = line.trim().match(/^(?:export\s+)?([A-Z0-9_]+)\s*=\s*(.*)$/);
|
|
5921
6953
|
if (!match || !names.includes(match[1])) {
|
|
@@ -5955,7 +6987,7 @@ function detectRepoProjectSlug() {
|
|
|
5955
6987
|
return null;
|
|
5956
6988
|
}
|
|
5957
6989
|
function detectDirectoryProjectSlug() {
|
|
5958
|
-
return slugifyFallback(
|
|
6990
|
+
return slugifyFallback(path4.basename(process.cwd()));
|
|
5959
6991
|
}
|
|
5960
6992
|
function detectGitRepository() {
|
|
5961
6993
|
try {
|
|
@@ -6017,50 +7049,50 @@ function isGitRepo() {
|
|
|
6017
7049
|
}
|
|
6018
7050
|
}
|
|
6019
7051
|
function detectAiClient() {
|
|
6020
|
-
if (
|
|
7052
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".codex"))) {
|
|
6021
7053
|
return "codex";
|
|
6022
7054
|
}
|
|
6023
|
-
if (
|
|
7055
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".cursor"))) {
|
|
6024
7056
|
return "cursor";
|
|
6025
7057
|
}
|
|
6026
|
-
if (
|
|
7058
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".kimi-code"))) {
|
|
6027
7059
|
return "kimi";
|
|
6028
7060
|
}
|
|
6029
|
-
if (
|
|
7061
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".gemini"))) {
|
|
6030
7062
|
return "gemini";
|
|
6031
7063
|
}
|
|
6032
|
-
if (
|
|
7064
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".vibe"))) {
|
|
6033
7065
|
return "mistral";
|
|
6034
7066
|
}
|
|
6035
|
-
if (
|
|
7067
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".continue"))) {
|
|
6036
7068
|
return "continue";
|
|
6037
7069
|
}
|
|
6038
|
-
if (
|
|
7070
|
+
if (fs4.existsSync(path4.join(process.cwd(), ".vscode"))) {
|
|
6039
7071
|
return "vscode";
|
|
6040
7072
|
}
|
|
6041
|
-
if (
|
|
7073
|
+
if (fs4.existsSync(path4.join(homedir2(), ".claude")) || process.env.CLAUDE_CODE) {
|
|
6042
7074
|
return "claude-code";
|
|
6043
7075
|
}
|
|
6044
|
-
if (
|
|
7076
|
+
if (fs4.existsSync(path4.join(homedir2(), ".cursor")) || process.env.CURSOR_EDITOR) {
|
|
6045
7077
|
return "cursor";
|
|
6046
7078
|
}
|
|
6047
|
-
if (
|
|
7079
|
+
if (fs4.existsSync(path4.join(homedir2(), ".kimi-code")) || process.env.KIMI_CODE_HOME) {
|
|
6048
7080
|
return "kimi";
|
|
6049
7081
|
}
|
|
6050
|
-
if (
|
|
7082
|
+
if (fs4.existsSync(path4.join(homedir2(), ".gemini")) || process.env.GEMINI_CLI) {
|
|
6051
7083
|
return "gemini";
|
|
6052
7084
|
}
|
|
6053
|
-
if (
|
|
7085
|
+
if (fs4.existsSync(path4.join(homedir2(), ".vibe")) || process.env.MISTRAL_VIBE) {
|
|
6054
7086
|
return "mistral";
|
|
6055
7087
|
}
|
|
6056
|
-
if (
|
|
7088
|
+
if (fs4.existsSync(path4.join(homedir2(), ".continue")) || process.env.CONTINUE_GLOBAL_DIR) {
|
|
6057
7089
|
return "continue";
|
|
6058
7090
|
}
|
|
6059
|
-
const claudeDesktopPath = process.platform === "darwin" ?
|
|
6060
|
-
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)) {
|
|
6061
7093
|
return "claude-desktop";
|
|
6062
7094
|
}
|
|
6063
|
-
if (
|
|
7095
|
+
if (fs4.existsSync(path4.join(homedir2(), ".codex"))) {
|
|
6064
7096
|
return "codex";
|
|
6065
7097
|
}
|
|
6066
7098
|
return "other";
|
|
@@ -6075,12 +7107,12 @@ function detectPipCommand() {
|
|
|
6075
7107
|
return null;
|
|
6076
7108
|
}
|
|
6077
7109
|
function getExistingConfig() {
|
|
6078
|
-
const mcpConfigPath =
|
|
6079
|
-
if (!
|
|
7110
|
+
const mcpConfigPath = path4.join(process.cwd(), ".mcp.json");
|
|
7111
|
+
if (!fs4.existsSync(mcpConfigPath)) {
|
|
6080
7112
|
return null;
|
|
6081
7113
|
}
|
|
6082
7114
|
try {
|
|
6083
|
-
const config = JSON.parse(
|
|
7115
|
+
const config = JSON.parse(fs4.readFileSync(mcpConfigPath, "utf-8"));
|
|
6084
7116
|
const sniparaServer = config.mcpServers?.snipara;
|
|
6085
7117
|
const runtimeServer = config.mcpServers?.[SANDBOX_SERVER_NAME] || config.mcpServers?.[LEGACY_RUNTIME_SERVER_NAME];
|
|
6086
7118
|
return {
|
|
@@ -6100,12 +7132,12 @@ function getExistingConfig() {
|
|
|
6100
7132
|
}
|
|
6101
7133
|
}
|
|
6102
7134
|
function readExistingCompanionProjectId() {
|
|
6103
|
-
const configPath =
|
|
6104
|
-
if (!
|
|
7135
|
+
const configPath = path4.join(process.cwd(), ".snipara", "companion", "config.json");
|
|
7136
|
+
if (!fs4.existsSync(configPath)) {
|
|
6105
7137
|
return void 0;
|
|
6106
7138
|
}
|
|
6107
7139
|
try {
|
|
6108
|
-
const parsed = JSON.parse(
|
|
7140
|
+
const parsed = JSON.parse(fs4.readFileSync(configPath, "utf-8"));
|
|
6109
7141
|
return typeof parsed.projectId === "string" && parsed.projectId ? parsed.projectId : void 0;
|
|
6110
7142
|
} catch {
|
|
6111
7143
|
return void 0;
|