create-snipara 2.0.18 → 2.1.1

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.
Files changed (3) hide show
  1. package/README.md +67 -13
  2. package/dist/index.js +396 -107
  3. package/package.json +7 -3
package/README.md CHANGED
@@ -1,19 +1,43 @@
1
1
  # create-snipara
2
2
 
3
- **One-command onboarding for Snipara Hosted MCP Context + Memory.**
3
+ **One-command onboarding for Snipara Cloud or self-hosted Snipara Server.**
4
4
 
5
5
  `create-snipara` connects a local project to Snipara so AI agents can start
6
6
  with project-owned context, reviewed memory, shared guidance, and optional code
7
7
  impact tooling instead of starting cold every session.
8
8
 
9
+ The shortest path is three moments: connect the folder your agent already uses,
10
+ let Snipara index its project context, then ask one question that only the
11
+ project can answer.
12
+
9
13
  ```bash
10
14
  npx create-snipara
11
15
  ```
12
16
 
13
- `create-snipara` is the simple onboarding package for the agent path. It
14
- creates or reuses auth, writes Hosted MCP config, and prepares the local project
15
- so an LLM client can use Snipara context and reviewed memory. The default path is
16
- hosted-only so the first success is a grounded answer, not a local tooling tour.
17
+ For a local or private server, use the same activation flow:
18
+
19
+ ```bash
20
+ export SNIPARA_LOCAL_API_KEY="your-server-operator-key"
21
+ npx create-snipara@latest --self-hosted \
22
+ --server-url http://localhost:8000/mcp/local \
23
+ --api-key "$SNIPARA_LOCAL_API_KEY" \
24
+ --client claude-code
25
+ ```
26
+
27
+ Both modes aim to finish with the same visible proof:
28
+
29
+ ```text
30
+ ✓ Endpoint connected
31
+ ✓ Repository README/docs seeded
32
+ ✓ First project-grounded answer verified
33
+ ✓ AI client configuration written
34
+ ```
35
+
36
+ `create-snipara` is the single activation engine for the agent path. It detects
37
+ or asks where Snipara should run, writes the right MCP config, seeds a small
38
+ README/docs corpus, and automatically verifies a first project-grounded answer.
39
+ The default path is Cloud because it is the fastest first experience; the same
40
+ flow works with an existing self-hosted Snipara Server.
17
41
  It is also the canonical activation engine behind editor setup docs: VS Code can
18
42
  surface activation as a native command, Cursor can pair OpenVSX UI with Hosted
19
43
  MCP, but both paths should land on the same project context.
@@ -25,7 +49,7 @@ behind the scenes.
25
49
 
26
50
  Use it for:
27
51
 
28
- - Hosted MCP onboarding for Claude Code, Cursor, Kimi Code CLI, Codex, Gemini, Mistral, VS Code/Copilot, Continue, ChatGPT Desktop, Orca-style MCP clients, LM Studio-backed local stacks, and similar clients
52
+ - Snipara Cloud or self-hosted MCP onboarding for Claude Code, Cursor, Kimi Code CLI, Codex, Gemini, Mistral, VS Code/Copilot, Continue, ChatGPT Desktop, Orca-style MCP clients, LM Studio-backed local stacks, and similar clients
29
53
  - free account signup with reviewed Context + Memory
30
54
  - local project wiring, `doctor`, `repair`, and `upgrade`
31
55
  - local source activation without GitHub, plus optional GitHub repository sync and PR Answer Packs approval when the current folder is a GitHub repo
@@ -40,22 +64,22 @@ normalizes that setup.
40
64
  ```mermaid
41
65
  flowchart LR
42
66
  Project["Local project"] --> CLI["npx create-snipara"]
43
- CLI --> Auth["Snipara auth and project key"]
44
- CLI --> MCP["Hosted MCP config"]
67
+ CLI --> Auth["Cloud auth or self-hosted operator key"]
68
+ CLI --> MCP["Configured MCP endpoint"]
45
69
  CLI --> Templates["AGENTS.md, CLAUDE.md, Cursor, Codex, MCP references"]
46
70
  MCP --> Agents["Claude Code, Cursor, Codex, Gemini, Mistral, VS Code, ChatGPT, Orca, LM Studio stacks"]
47
71
  Templates --> Agents
48
72
  Agents --> Context["Snipara context and memory"]
49
73
  ```
50
74
 
51
- The CLI does not become the runtime brain. After setup, the agent talks to
52
- Hosted MCP and keeps using its own LLM.
75
+ The CLI does not become the runtime brain. After setup, the agent talks to the
76
+ configured MCP endpoint and keeps using its own LLM.
53
77
 
54
78
  Do not use it to choose business documents one by one. For local folders, mounted Drive/SharePoint exports, old offers, or PowerPoints, use `snipara-business`.
55
79
 
56
80
  The product surfaces remain:
57
81
 
58
- - Hosted MCP for LLM agents, context, and reviewed memory
82
+ - Snipara Cloud or self-hosted Snipara Server for LLM agents, context, and reviewed memory
59
83
  - HTTP API / SDK for apps, integrators, backends, and pipelines
60
84
 
61
85
  Advanced flags can wire local helpers around those surfaces:
@@ -67,6 +91,24 @@ Advanced flags can wire local helpers around those surfaces:
67
91
  - local config, env files, hook scripts, and a project companion pack
68
92
  - `snipara-mcp` only for local stdio compatibility, development, or testing
69
93
 
94
+ ## Deployment Modes
95
+
96
+ The deployment mode and install profile are independent:
97
+
98
+ | Mode | Command | Data boundary | First proof |
99
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------ |
100
+ | Snipara Cloud | `npx create-snipara@latest` | Snipara-managed hosted project | Cloud MCP connection, README/docs seed, grounded answer |
101
+ | Self-hosted Snipara Server | `npx create-snipara@latest --self-hosted --server-url http://localhost:8000/mcp/local --api-key "$SNIPARA_LOCAL_API_KEY"` | Your server and storage | Server contract, MCP connection, README/docs seed, grounded answer |
102
+
103
+ Self-hosted runs use the server's `X-API-Key` operator key, the public
104
+ `snipara-server-oss-v2` contract, and the server's `rlm_*` compatibility tools.
105
+ They never open Cloud signup or call the Cloud dashboard automation API.
106
+
107
+ Every successful live setup writes `.snipara/activation/activation-manifest.json`
108
+ with the selected endpoint, seed status, proof tool, and a non-sensitive proof
109
+ receipt such as `answer_pack_chars=...`. Raw API keys are never included in the
110
+ activation report or JSON summary.
111
+
70
112
  ## Install Profiles
71
113
 
72
114
  The CLI now works around explicit install profiles:
@@ -199,11 +241,13 @@ npx create-snipara
199
241
 
200
242
  The interactive flow now asks for:
201
243
 
244
+ - deployment mode when `--advanced` is used
245
+ - a self-hosted MCP endpoint when self-hosted mode is selected
202
246
  - project slug
203
- - API key path: existing API key, free signup, or add key later
247
+ - API key path: Cloud key/signup, self-hosted operator key, or add key later
204
248
  - AI client
205
249
 
206
- By default, this uses the minimal `hosted-only` setup: hosted MCP config and a
250
+ By default, this uses the minimal `hosted-only` setup: configured MCP config and a
207
251
  doctor report, with no local Companion install, hooks, GitHub approval prompt, or
208
252
  sandbox prompt. Add `--advanced` when you want profile selection, project id
209
253
  entry, hook prompts, GitHub automation prompts, or sandbox provider prompts.
@@ -414,9 +458,19 @@ npx create-snipara --runtime-only
414
458
  # Safer non-interactive setup with an existing API key
415
459
  SNIPARA_API_KEY=snp-xxx npx create-snipara -y --slug my-project
416
460
 
461
+ # Self-hosted non-interactive setup; the same command works with any MCP client
462
+ SNIPARA_LOCAL_API_KEY=local-operator-key npx create-snipara -y \
463
+ --self-hosted --server-url http://localhost:8000/mcp/local \
464
+ --api-key "$SNIPARA_LOCAL_API_KEY" --slug my-project
465
+
417
466
  # Compatibility path; prints a warning because CLI args can leak
418
467
  npx create-snipara -y --api-key snp-xxx --slug my-project
419
468
 
469
+ # Explicitly choose the deployment mode
470
+ npx create-snipara -y --deployment hosted --api-key snp-xxx --slug my-project
471
+ npx create-snipara -y --deployment self-hosted \
472
+ --server-url http://localhost:8000/mcp/local --api-key local-operator-key
473
+
420
474
  # Open GitHub automation after setup when a GitHub remote is detected
421
475
  npx create-snipara --github
422
476
  npx create-snipara --github-path docs/
package/dist/index.js CHANGED
@@ -5,6 +5,7 @@ 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";
8
9
  import fs from "fs";
9
10
  import path from "path";
10
11
  import { execSync, spawn } from "child_process";
@@ -13,8 +14,8 @@ import { homedir } from "os";
13
14
  // package.json
14
15
  var package_default = {
15
16
  name: "create-snipara",
16
- version: "2.0.18",
17
- description: "Onboard Snipara Hosted MCP Context + Memory with project bootstrap intelligence and optional local workflows",
17
+ version: "2.1.1",
18
+ description: "Onboard Snipara Cloud or self-hosted MCP with an automatic project-grounded proof of value",
18
19
  main: "dist/index.js",
19
20
  type: "module",
20
21
  bin: {
@@ -52,7 +53,11 @@ var package_default = {
52
53
  "lm-studio",
53
54
  "vscode",
54
55
  "continue",
55
- "claude-desktop"
56
+ "claude-desktop",
57
+ "self-hosted",
58
+ "snipara-server",
59
+ "project-intelligence",
60
+ "persistent-memory"
56
61
  ],
57
62
  author: "Snipara",
58
63
  license: "MIT",
@@ -93,7 +98,7 @@ var COMPANION_PACKAGE_NAME = "snipara-companion";
93
98
  var COMPANION_COMMAND_NAME = "snipara-companion";
94
99
  var COMPANION_NPX_COMMAND = "npx -y snipara-companion@latest";
95
100
  var CREATE_SNIPARA_NPX_COMMAND = "npx -y create-snipara@latest";
96
- var MIN_COMPANION_VERSION = "3.0.0";
101
+ var MIN_COMPANION_VERSION = "3.8.0";
97
102
  var GITHUB_ANSWER_PACKS_LABEL = "GitHub PR Answer Packs";
98
103
  var MCP_PACKAGE_NAME = "snipara-mcp";
99
104
  var SANDBOX_PACKAGE_NAME = "snipara-sandbox[all]";
@@ -134,7 +139,7 @@ var DECISION_COMMIT_TERMS = [
134
139
  "switch"
135
140
  ];
136
141
  function buildActivationId() {
137
- return `act_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
142
+ return `act_${Date.now().toString(36)}_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
138
143
  }
139
144
  function getActivationDir() {
140
145
  return path.join(process.cwd(), ".snipara", "activation");
@@ -1188,7 +1193,7 @@ function createInitialFirstWorkBrief(answers, detection, scan) {
1188
1193
  }
1189
1194
  function createActivationManifest(answers, detection, mode = "default", scan) {
1190
1195
  const now = (/* @__PURE__ */ new Date()).toISOString();
1191
- const hasHostedConfig = answers.profile !== "runtime-only";
1196
+ const hasMcpConfig = answers.profile !== "runtime-only";
1192
1197
  const hasCompanion = answers.installCompanion;
1193
1198
  const localScan = scan ?? detectLocalActivationScan(detection);
1194
1199
  const dayZeroQuery = "What is this project, what is currently in flight, and what should an agent know before making the first edit?";
@@ -1196,6 +1201,8 @@ function createActivationManifest(answers, detection, mode = "default", scan) {
1196
1201
  return {
1197
1202
  activationId: buildActivationId(),
1198
1203
  mode,
1204
+ deployment: answers.deployment,
1205
+ serverUrl: answers.serverUrl,
1199
1206
  surfaceContract,
1200
1207
  consumerContract: buildEditorActivationConsumerContract(answers.aiClient),
1201
1208
  activationMetric: {
@@ -1204,15 +1211,22 @@ function createActivationManifest(answers, detection, mode = "default", scan) {
1204
1211
  proof: "A correct, non-obvious statement about this repository from the first answer pack."
1205
1212
  },
1206
1213
  dayZeroQuery: {
1207
- tool: "snipara_context_query",
1214
+ tool: answers.deployment === "self-hosted" ? "rlm_context_query" : "snipara_context_query",
1208
1215
  query: dayZeroQuery,
1209
1216
  expectedOutcome: "First own-repo answer pack before the user reads product docs."
1210
1217
  },
1211
1218
  dayZeroCorpus: createDayZeroCorpusReport(
1212
1219
  "queued",
1213
1220
  collectDayZeroCorpusDocuments(localScan),
1214
- "README and docs Markdown will be submitted to hosted context before the first day-zero query when live hosted checks are enabled."
1221
+ `README and docs Markdown will be submitted to ${getDeploymentLabel(answers.deployment)} before the first day-zero query when live checks are enabled.`
1215
1222
  ),
1223
+ valueProof: {
1224
+ status: "skipped",
1225
+ deployment: answers.deployment,
1226
+ query: dayZeroQuery,
1227
+ detail: "Waiting for the configured MCP connection and source seeding.",
1228
+ observedAt: now
1229
+ },
1216
1230
  agentReadiness: buildAgentReadinessReport(localScan),
1217
1231
  contextEconomy: buildContextEconomyReport(localScan, dayZeroQuery),
1218
1232
  projectSlug: answers.projectSlug,
@@ -1228,20 +1242,20 @@ function createActivationManifest(answers, detection, mode = "default", scan) {
1228
1242
  localScan,
1229
1243
  lanes: {
1230
1244
  connection: createActivationLane(
1231
- hasHostedConfig ? "ready" : "skipped",
1232
- hasHostedConfig ? "Hosted MCP config written." : "Runtime-only profile skipped Hosted MCP config."
1245
+ hasMcpConfig ? "ready" : "skipped",
1246
+ hasMcpConfig ? `${getDeploymentLabel(answers.deployment)} MCP config written.` : "Runtime-only profile skipped MCP config."
1233
1247
  ),
1234
1248
  clientConfig: createActivationLane(
1235
- hasHostedConfig ? "ready" : "skipped",
1236
- hasHostedConfig ? `${getClientDisplayName(answers.aiClient)} project files generated.` : "No AI client config generated for runtime-only profile."
1249
+ hasMcpConfig ? "ready" : "skipped",
1250
+ hasMcpConfig ? `${getClientDisplayName(answers.aiClient)} project files generated.` : "No AI client config generated for runtime-only profile."
1237
1251
  ),
1238
1252
  localSnapshot: createActivationLane(
1239
1253
  hasCompanion ? "queued" : "skipped",
1240
1254
  hasCompanion ? "Local source activation will create a repository snapshot." : "Install companion later to activate local source context."
1241
1255
  ),
1242
1256
  contextIndex: createActivationLane(
1243
- hasHostedConfig || hasCompanion ? "queued" : "skipped",
1244
- hasCompanion ? "Context indexing starts from local docs and instruction files." : hasHostedConfig ? "Day-zero README/docs sync will seed hosted context when live hosted checks run." : "Context indexing requires companion or hosted provider sync."
1257
+ hasMcpConfig || hasCompanion ? "queued" : "skipped",
1258
+ hasCompanion ? "Context indexing starts from local docs and instruction files." : hasMcpConfig ? `Day-zero README/docs sync will seed ${getDeploymentLabel(answers.deployment)} when live checks run.` : "Context indexing requires companion or hosted provider sync."
1245
1259
  ),
1246
1260
  codeGraph: createActivationLane(
1247
1261
  hasCompanion ? "queued" : "skipped",
@@ -1360,7 +1374,7 @@ async function runHostedDayZeroCorpusSync(answers, manifest, enabled) {
1360
1374
  manifest.dayZeroCorpus = createDayZeroCorpusReport(
1361
1375
  "skipped",
1362
1376
  [],
1363
- "Runtime-only profile skipped Hosted MCP config and hosted document sync."
1377
+ "Runtime-only profile skipped MCP config and document sync."
1364
1378
  );
1365
1379
  updateActivationManifest(manifest);
1366
1380
  return;
@@ -1370,7 +1384,7 @@ async function runHostedDayZeroCorpusSync(answers, manifest, enabled) {
1370
1384
  manifest.dayZeroCorpus = createDayZeroCorpusReport(
1371
1385
  "skipped",
1372
1386
  documents2,
1373
- "Skipped because hosted validation was disabled with --skip-test."
1387
+ `Skipped because ${getDeploymentLabel(answers.deployment)} validation was disabled with --skip-test.`
1374
1388
  );
1375
1389
  updateActivationManifest(manifest);
1376
1390
  return;
@@ -1379,7 +1393,7 @@ async function runHostedDayZeroCorpusSync(answers, manifest, enabled) {
1379
1393
  manifest.dayZeroCorpus = createDayZeroCorpusReport(
1380
1394
  "skipped",
1381
1395
  [],
1382
- "Skipped because no live hosted API key was available."
1396
+ `Skipped because no live ${getDeploymentLabel(answers.deployment)} API key was available.`
1383
1397
  );
1384
1398
  updateActivationManifest(manifest);
1385
1399
  return;
@@ -1389,18 +1403,18 @@ async function runHostedDayZeroCorpusSync(answers, manifest, enabled) {
1389
1403
  manifest.dayZeroCorpus = createDayZeroCorpusReport(
1390
1404
  "skipped",
1391
1405
  [],
1392
- "No README or docs Markdown files were found to seed hosted context."
1406
+ `No README or docs Markdown files were found to seed ${getDeploymentLabel(answers.deployment)}.`
1393
1407
  );
1394
1408
  if (manifest.lanes.contextIndex.status === "queued") {
1395
1409
  manifest.lanes.contextIndex = createActivationLane(
1396
1410
  "skipped",
1397
- "No README or docs Markdown files were found to seed hosted context."
1411
+ `No README or docs Markdown files were found to seed ${getDeploymentLabel(answers.deployment)}.`
1398
1412
  );
1399
1413
  }
1400
1414
  updateActivationManifest(manifest);
1401
1415
  return;
1402
1416
  }
1403
- const syncTool = "snipara_sync_documents";
1417
+ const syncTool = answers.deployment === "self-hosted" ? "rlm_sync_documents" : "snipara_sync_documents";
1404
1418
  const syncResponse = await callHostedMcp(answers, "tools/call", {
1405
1419
  name: syncTool,
1406
1420
  arguments: {
@@ -1412,18 +1426,18 @@ async function runHostedDayZeroCorpusSync(answers, manifest, enabled) {
1412
1426
  manifest.dayZeroCorpus = createDayZeroCorpusReport(
1413
1427
  "submitted",
1414
1428
  documents,
1415
- `Submitted ${documents.length} README/docs files to hosted context for the first day-zero answer pack.`,
1429
+ `Submitted ${documents.length} README/docs files to ${getDeploymentLabel(answers.deployment)} for the first day-zero answer pack.`,
1416
1430
  { tool: syncTool }
1417
1431
  );
1418
1432
  manifest.lanes.contextIndex = createActivationLane(
1419
1433
  "indexing",
1420
- "Day-zero README/docs corpus submitted to hosted context indexing.",
1434
+ `Day-zero README/docs corpus submitted to ${getDeploymentLabel(answers.deployment)} indexing.`,
1421
1435
  syncTool
1422
1436
  );
1423
1437
  updateActivationManifest(manifest);
1424
1438
  return;
1425
1439
  }
1426
- const uploadTool = "snipara_upload_document";
1440
+ const uploadTool = answers.deployment === "self-hosted" ? "rlm_upload_document" : "snipara_upload_document";
1427
1441
  const uploadErrors = [];
1428
1442
  for (const document of documents) {
1429
1443
  const uploadResponse = await callHostedMcp(answers, "tools/call", {
@@ -1441,12 +1455,12 @@ async function runHostedDayZeroCorpusSync(answers, manifest, enabled) {
1441
1455
  manifest.dayZeroCorpus = createDayZeroCorpusReport(
1442
1456
  "submitted",
1443
1457
  documents,
1444
- `Submitted ${documents.length} README/docs files to hosted context for the first day-zero answer pack.`,
1458
+ `Submitted ${documents.length} README/docs files to ${getDeploymentLabel(answers.deployment)} for the first day-zero answer pack.`,
1445
1459
  { tool: uploadTool }
1446
1460
  );
1447
1461
  manifest.lanes.contextIndex = createActivationLane(
1448
1462
  "indexing",
1449
- "Day-zero README/docs corpus submitted to hosted context indexing.",
1463
+ `Day-zero README/docs corpus submitted to ${getDeploymentLabel(answers.deployment)} indexing.`,
1450
1464
  uploadTool
1451
1465
  );
1452
1466
  } else {
@@ -1457,13 +1471,96 @@ async function runHostedDayZeroCorpusSync(answers, manifest, enabled) {
1457
1471
  error: detail
1458
1472
  });
1459
1473
  manifest.lanes.contextIndex = failActivationLane(
1460
- "Hosted day-zero README/docs sync failed; the first answer may need GitHub, companion, or a later sync.",
1474
+ `${getDeploymentLabel(answers.deployment)} day-zero README/docs sync failed; the first answer may need GitHub, companion, or a later sync.`,
1461
1475
  syncTool,
1462
1476
  new Error(detail)
1463
1477
  );
1464
1478
  }
1465
1479
  updateActivationManifest(manifest);
1466
1480
  }
1481
+ async function runDayZeroValueProof(answers, manifest, enabled) {
1482
+ const observedAt = (/* @__PURE__ */ new Date()).toISOString();
1483
+ if (!enabled || answers.profile === "runtime-only") {
1484
+ manifest.valueProof = {
1485
+ status: "skipped",
1486
+ deployment: answers.deployment,
1487
+ query: manifest.dayZeroQuery.query,
1488
+ detail: "Skipped because live MCP validation was disabled or the profile is runtime-only.",
1489
+ observedAt
1490
+ };
1491
+ updateActivationManifest(manifest);
1492
+ return;
1493
+ }
1494
+ if (answers.apiKeyType === "none" || answers.apiKey.includes("YOUR_")) {
1495
+ manifest.valueProof = {
1496
+ status: "skipped",
1497
+ deployment: answers.deployment,
1498
+ query: manifest.dayZeroQuery.query,
1499
+ detail: "Skipped because no live API key was available.",
1500
+ observedAt
1501
+ };
1502
+ updateActivationManifest(manifest);
1503
+ return;
1504
+ }
1505
+ const queryTool = answers.deployment === "self-hosted" ? "rlm_context_query" : "snipara_context_query";
1506
+ const spinner = ora(`Running ${getDeploymentLabel(answers.deployment)} value proof...`).start();
1507
+ try {
1508
+ const response = await callHostedMcp(answers, "tools/call", {
1509
+ name: queryTool,
1510
+ arguments: {
1511
+ query: manifest.dayZeroQuery.query,
1512
+ max_tokens: 1200,
1513
+ search_mode: "keyword",
1514
+ return_references: true,
1515
+ auto_decompose: false
1516
+ }
1517
+ });
1518
+ if (!response.ok || response.data.error) {
1519
+ const detail = response.ok ? describeJsonRpcError(response.data) : describeHostedHttpFailure(response.status, response.bodyText);
1520
+ throw new Error(detail);
1521
+ }
1522
+ const evidenceChars = extractMcpText(response.data).length;
1523
+ if (evidenceChars === 0) {
1524
+ throw new Error("The MCP query returned no answer content");
1525
+ }
1526
+ manifest.valueProof = {
1527
+ status: "ready",
1528
+ deployment: answers.deployment,
1529
+ query: manifest.dayZeroQuery.query,
1530
+ tool: queryTool,
1531
+ detail: "A non-empty project answer pack was returned from the configured MCP endpoint.",
1532
+ evidence: `answer_pack_chars=${evidenceChars}`,
1533
+ observedAt
1534
+ };
1535
+ spinner.succeed("Value proof ready: Snipara returned a project-grounded answer.");
1536
+ } catch (error) {
1537
+ const detail = error instanceof Error ? error.message : "unknown MCP value-proof error";
1538
+ manifest.valueProof = {
1539
+ status: "failed",
1540
+ deployment: answers.deployment,
1541
+ query: manifest.dayZeroQuery.query,
1542
+ tool: queryTool,
1543
+ detail: "The connection succeeded, but the first project answer could not be verified.",
1544
+ observedAt,
1545
+ error: detail.slice(0, 300)
1546
+ };
1547
+ spinner.warn(`Value proof needs attention: ${detail}`);
1548
+ }
1549
+ updateActivationManifest(manifest);
1550
+ }
1551
+ function extractMcpText(data) {
1552
+ const content = data.result?.content;
1553
+ if (!Array.isArray(content)) {
1554
+ return "";
1555
+ }
1556
+ return content.map((item) => {
1557
+ if (!item || typeof item !== "object") {
1558
+ return "";
1559
+ }
1560
+ const record = item;
1561
+ return typeof record.text === "string" ? record.text : "";
1562
+ }).filter(Boolean).join("\n");
1563
+ }
1467
1564
  function createActivationArtifacts(answers, manifest) {
1468
1565
  const projectBootstrap = buildProjectBootstrap(manifest.localScan);
1469
1566
  writeProjectBootstrap(projectBootstrap);
@@ -1587,14 +1684,14 @@ function shouldInstallDefaultHooks(aiClient) {
1587
1684
  }
1588
1685
  function buildProgram() {
1589
1686
  const program = new Command();
1590
- program.name("create-snipara").description("Scaffold hosted Snipara MCP config; advanced flags add local helpers").version(VERSION);
1687
+ program.name("create-snipara").description("Connect Snipara Cloud or a self-hosted Snipara Server with a fast proof of value").version(VERSION);
1591
1688
  program.command("init").description("Install and configure Snipara for this project").option("--api-key <key>", "Snipara API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
1592
1689
  "--client <client>",
1593
1690
  "AI client: claude-code, cursor, kimi, codex, gemini, mistral, chatgpt, vscode, continue, claude-desktop, other"
1594
1691
  ).option("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option(
1595
1692
  "--profile <profile>",
1596
1693
  "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
1597
- ).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 hosted connection test").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) => {
1694
+ ).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) => {
1598
1695
  printBanner();
1599
1696
  await runInit(options);
1600
1697
  });
@@ -1605,7 +1702,7 @@ function buildProgram() {
1605
1702
  printBanner();
1606
1703
  await runDoctor(options);
1607
1704
  });
1608
- program.command("repair").description("Rebuild local Snipara config and re-run validation").option("--api-key <key>", "Snipara API key override").option("--slug <slug>", "Project slug override").option("--project-id <id>", "Project ID override").option("--profile <profile>", "Target install profile").option("--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) => {
1705
+ program.command("repair").description("Rebuild local Snipara config and re-run validation").option("--api-key <key>", "Snipara API key override").option("--slug <slug>", "Project slug override").option("--project-id <id>", "Project ID override").option("--profile <profile>", "Target install profile").option("--deployment <deployment>", "Deployment: hosted or self-hosted").option("--self-hosted", "Use an existing self-hosted Snipara MCP server").option("--server-url <url>", "Self-hosted MCP endpoint URL").option("--with-hooks", "Force hook generation").option("--with-runtime", "Force Snipara Sandbox server/config").option("--with-mcp", "Force MCP config").option("--with-companion", "Force companion pack generation").option("--with-orchestrator", "Install optional snipara-orchestrator package").action(async (options) => {
1609
1706
  printBanner();
1610
1707
  await runRepair(options);
1611
1708
  });
@@ -1635,7 +1732,7 @@ function addSharedRootOptions(program) {
1635
1732
  ).option("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option(
1636
1733
  "--profile <profile>",
1637
1734
  "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
1638
- ).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 hosted connection test").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");
1735
+ ).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");
1639
1736
  }
1640
1737
  async function main() {
1641
1738
  const program = buildProgram();
@@ -1719,6 +1816,8 @@ function parseInitOptionsFromArgv(args) {
1719
1816
  "--project-id": "projectId",
1720
1817
  "--client": "client",
1721
1818
  "--profile": "profile",
1819
+ "--deployment": "deployment",
1820
+ "--server-url": "serverUrl",
1722
1821
  "--github-path": "githubPath"
1723
1822
  };
1724
1823
  const booleanFlags = {
@@ -1732,6 +1831,7 @@ function parseInitOptionsFromArgv(args) {
1732
1831
  "--runtime-only": "runtimeOnly",
1733
1832
  "--full": "full",
1734
1833
  "--Full": "full",
1834
+ "--self-hosted": "selfHosted",
1735
1835
  "--github": "github",
1736
1836
  "--skip-github": "skipGithub",
1737
1837
  "--starter": "starter",
@@ -1797,6 +1897,7 @@ async function runInit(options) {
1797
1897
  activationScan
1798
1898
  );
1799
1899
  await runHostedDayZeroCorpusSync(answers, activationManifest, !normalizedOptions.skipTest);
1900
+ await runDayZeroValueProof(answers, activationManifest, !normalizedOptions.skipTest);
1800
1901
  runLocalSourceActivation(answers, activationManifest);
1801
1902
  createActivationArtifacts(answers, activationManifest);
1802
1903
  printSuccess(answers, validationReport, activationManifest);
@@ -1816,14 +1917,17 @@ function buildActivationJsonSummary(manifest, validation) {
1816
1917
  const laneEntries = Object.entries(manifest.lanes);
1817
1918
  const failedLanes = laneEntries.filter(([, lane]) => lane.status === "failed").map(([name]) => name);
1818
1919
  const pendingLanes = laneEntries.filter(([, lane]) => lane.status === "queued" || lane.status === "indexing").map(([name]) => name);
1819
- const activationStatus = failedLanes.length > 0 ? "partial" : pendingLanes.length > 0 ? "partial" : "complete";
1920
+ const activationStatus = failedLanes.length > 0 || manifest.valueProof.status === "failed" ? "partial" : pendingLanes.length > 0 ? "partial" : "complete";
1820
1921
  return {
1821
1922
  ok: true,
1822
1923
  activationStatus,
1823
1924
  activationId: manifest.activationId,
1824
1925
  projectSlug: manifest.projectSlug,
1926
+ deployment: manifest.deployment,
1927
+ serverUrl: manifest.serverUrl,
1825
1928
  aiClient: manifest.aiClient,
1826
1929
  profile: validation.profile,
1930
+ valueProof: manifest.valueProof,
1827
1931
  artifacts: manifest.artifacts,
1828
1932
  lanes: Object.fromEntries(
1829
1933
  laneEntries.map(([name, lane]) => [
@@ -1981,8 +2085,8 @@ function printBanner() {
1981
2085
  \u2551 \u2551
1982
2086
  \u2551 ${chalk.bold("Snipara")} - Context + Memory onboarding \u2551
1983
2087
  \u2551 \u2551
1984
- \u2551 \u2022 Hosted MCP for LLM agents \u2551
1985
- \u2551 \u2022 Free account path with hosted context and reviewed memory \u2551
2088
+ \u2551 \u2022 Cloud or self-hosted MCP for LLM agents \u2551
2089
+ \u2551 \u2022 First project-grounded answer in minutes \u2551
1986
2090
  \u2551 \u2022 First grounded answer before local workflow setup \u2551
1987
2091
  \u2551 \u2022 API / SDK for product integrations \u2551
1988
2092
  \u2551 \u2022 Optional advanced companion, hooks, and Snipara Sandbox helpers \u2551
@@ -2014,17 +2118,26 @@ async function gatherConfiguration(options) {
2014
2118
  detectedProjectId = existingConfig.projectId || "";
2015
2119
  }
2016
2120
  const initialProfile = resolveRequestedProfile(options);
2017
- const providedApiKey = options.apiKey?.trim();
2018
- if (providedApiKey && !isManualApiKey(providedApiKey)) {
2019
- throw new Error(manualApiKeyValidationMessage());
2020
- }
2021
- if (providedApiKey) {
2121
+ const initialDeployment = resolveRequestedDeployment(options);
2122
+ const initialServerUrl = normalizeMcpEndpoint(
2123
+ options.serverUrl || process.env.SNIPARA_MCP_URL || getExistingConfig()?.serverUrl || "http://localhost:8000/mcp/local"
2124
+ );
2125
+ const defaultProjectSlug = options.slug || detectedSlug || (initialDeployment === "self-hosted" ? extractSlugFromMcpEndpoint(initialServerUrl) || "local" : "");
2126
+ const optionApiKey = options.apiKey?.trim();
2127
+ 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;
2129
+ if (providedApiKey && !isValidApiKey(providedApiKey, initialDeployment)) {
2130
+ throw new Error(manualApiKeyValidationMessage(initialDeployment));
2131
+ }
2132
+ if (optionApiKey) {
2022
2133
  warnApiKeyCliExposure();
2023
2134
  }
2024
2135
  if (options.yes) {
2025
2136
  return buildAnswersFromProfile({
2026
- projectSlug: options.slug || detectedSlug || "my-project",
2137
+ projectSlug: defaultProjectSlug || "my-project",
2027
2138
  projectId: options.projectId || detectedProjectId,
2139
+ deployment: initialDeployment,
2140
+ serverUrl: initialServerUrl,
2028
2141
  apiKey: providedApiKey || "YOUR_API_KEY_HERE",
2029
2142
  apiKeyType: providedApiKey ? "api" : "none",
2030
2143
  profile: initialProfile,
@@ -2035,13 +2148,45 @@ async function gatherConfiguration(options) {
2035
2148
  withOrchestrator: !!options.withOrchestrator,
2036
2149
  skipHooks: !!options.skipHooks,
2037
2150
  configureGitHub: Boolean(
2038
- (options.github || options.githubPath) && detectedGitRepo && !options.skipGithub
2151
+ initialDeployment === "hosted" && (options.github || options.githubPath) && detectedGitRepo && !options.skipGithub
2039
2152
  ),
2040
2153
  githubRepo: detectedGitRepo || void 0,
2041
2154
  githubPath: options.githubPath
2042
2155
  });
2043
2156
  }
2044
2157
  const response = await prompts([
2158
+ {
2159
+ type: showAdvancedPrompts ? "select" : null,
2160
+ name: "deployment",
2161
+ message: "Where should this project connect?",
2162
+ initial: initialDeployment === "self-hosted" ? 1 : 0,
2163
+ choices: [
2164
+ {
2165
+ title: "Snipara Cloud (fastest first experience)",
2166
+ value: "hosted",
2167
+ description: "Hosted MCP, account signup, and automatic day-zero proof"
2168
+ },
2169
+ {
2170
+ title: "Existing self-hosted Snipara Server",
2171
+ value: "self-hosted",
2172
+ description: "Your MCP endpoint, operator API key, and local data boundary"
2173
+ }
2174
+ ]
2175
+ },
2176
+ {
2177
+ type: (_, values) => initialDeployment === "self-hosted" || values.deployment === "self-hosted" ? "text" : null,
2178
+ name: "serverUrl",
2179
+ message: "Self-hosted MCP endpoint URL:",
2180
+ initial: options.serverUrl || process.env.SNIPARA_MCP_URL || initialServerUrl,
2181
+ validate: (value) => {
2182
+ try {
2183
+ normalizeMcpEndpoint(value);
2184
+ return true;
2185
+ } catch {
2186
+ return "Enter a valid http:// or https:// MCP endpoint URL";
2187
+ }
2188
+ }
2189
+ },
2045
2190
  {
2046
2191
  type: showAdvancedPrompts ? "select" : null,
2047
2192
  name: "profile",
@@ -2053,7 +2198,7 @@ async function gatherConfiguration(options) {
2053
2198
  type: "text",
2054
2199
  name: "projectSlug",
2055
2200
  message: "Project slug:",
2056
- initial: options.slug || detectedSlug || "",
2201
+ initial: defaultProjectSlug,
2057
2202
  validate: (value) => value.length > 0 ? true : "Project slug is required (for example my-project)"
2058
2203
  },
2059
2204
  {
@@ -2065,8 +2210,18 @@ async function gatherConfiguration(options) {
2065
2210
  {
2066
2211
  type: (_, values) => values.profile === "runtime-only" || providedApiKey ? null : "select",
2067
2212
  name: "apiKeyType",
2068
- message: "How do you want to add your Snipara API key?",
2069
- choices: [
2213
+ 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
+ choices: (_, values) => values.deployment === "self-hosted" || initialDeployment === "self-hosted" ? [
2215
+ {
2216
+ title: "I have an operator API key",
2217
+ value: "api",
2218
+ description: "Use the X-API-Key configured for your server"
2219
+ },
2220
+ {
2221
+ title: "I'll add the API key later",
2222
+ value: "skip"
2223
+ }
2224
+ ] : [
2070
2225
  {
2071
2226
  title: "I have an API key",
2072
2227
  value: "api",
@@ -2087,7 +2242,10 @@ async function gatherConfiguration(options) {
2087
2242
  type: (_, values) => values.profile === "runtime-only" ? null : values.apiKeyType === "api" ? "text" : null,
2088
2243
  name: "apiKey",
2089
2244
  message: (_, values) => values.apiKeyType === "api" ? "Enter your Snipara API key:" : "Enter your API key:",
2090
- validate: (value) => isManualApiKey(value) ? true : manualApiKeyValidationMessage()
2245
+ validate: (value, values) => {
2246
+ const deployment = normalizeDeployment(values.deployment) || initialDeployment;
2247
+ return isValidApiKey(value, deployment) ? true : manualApiKeyValidationMessage(deployment);
2248
+ }
2091
2249
  },
2092
2250
  {
2093
2251
  type: "select",
@@ -2126,11 +2284,15 @@ async function gatherConfiguration(options) {
2126
2284
  initial: options.githubPath || ""
2127
2285
  }
2128
2286
  ]);
2287
+ const selectedDeployment = normalizeDeployment(response.deployment) || initialDeployment;
2288
+ const selectedServerUrl = normalizeMcpEndpoint(
2289
+ options.serverUrl || response.serverUrl || initialServerUrl
2290
+ );
2129
2291
  let apiKey = providedApiKey;
2130
2292
  let apiKeyType = response.profile === "runtime-only" ? "none" : providedApiKey ? "api" : "none";
2131
2293
  let signupResult = null;
2132
2294
  if (!apiKey && response.profile !== "runtime-only") {
2133
- if (response.apiKeyType === "signup") {
2295
+ if (response.apiKeyType === "signup" && selectedDeployment === "hosted") {
2134
2296
  signupResult = await handleSignup({
2135
2297
  projectHint: response.projectSlug,
2136
2298
  repoHint: detectedGitRepo?.fullName
@@ -2148,6 +2310,8 @@ async function gatherConfiguration(options) {
2148
2310
  const answers = buildAnswersFromProfile({
2149
2311
  projectSlug: signupResult?.projectSlug || response.projectSlug,
2150
2312
  projectId: signupResult?.projectId || response.projectId || options.projectId || "",
2313
+ deployment: selectedDeployment,
2314
+ serverUrl: selectedServerUrl,
2151
2315
  apiKey: apiKey || "",
2152
2316
  apiKeyType,
2153
2317
  profile: response.profile || initialProfile,
@@ -2158,7 +2322,7 @@ async function gatherConfiguration(options) {
2158
2322
  withOrchestrator: Boolean(options.withOrchestrator || response.installOrchestrator),
2159
2323
  skipHooks: !!options.skipHooks || !(typeof response.installHooks === "boolean" ? response.installHooks : shouldInstallDefaultHooks(requestedClient || response.aiClient || detectedClient)),
2160
2324
  configureGitHub: Boolean(
2161
- (response.configureGitHub || options.github || options.githubPath) && detectedGitRepo && !options.skipGithub
2325
+ selectedDeployment === "hosted" && (response.configureGitHub || options.github || options.githubPath) && detectedGitRepo && !options.skipGithub
2162
2326
  ),
2163
2327
  githubRepo: detectedGitRepo || void 0,
2164
2328
  githubPath: response.githubPath || options.githubPath
@@ -2226,6 +2390,33 @@ function resolveRequestedProfile(options) {
2226
2390
  }
2227
2391
  return "hosted-only";
2228
2392
  }
2393
+ function normalizeDeployment(value) {
2394
+ if (value === "hosted" || value === "self-hosted") {
2395
+ return value;
2396
+ }
2397
+ return null;
2398
+ }
2399
+ function resolveRequestedDeployment(options) {
2400
+ if (options.selfHosted || options.serverUrl) {
2401
+ return "self-hosted";
2402
+ }
2403
+ if (normalizeDeployment(options.deployment)) {
2404
+ return normalizeDeployment(options.deployment);
2405
+ }
2406
+ const existingEndpoint = getExistingConfig()?.serverUrl;
2407
+ return existingEndpoint && !existingEndpoint.startsWith(API_URL) ? "self-hosted" : "hosted";
2408
+ }
2409
+ function normalizeMcpEndpoint(value) {
2410
+ const normalized = value.trim().replace(/\/+$/, "");
2411
+ const parsed = new URL(normalized);
2412
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
2413
+ throw new Error("MCP endpoint must use http:// or https://");
2414
+ }
2415
+ return normalized;
2416
+ }
2417
+ function getDeploymentLabel(deployment) {
2418
+ return deployment === "self-hosted" ? "self-hosted Snipara Server" : "Snipara Cloud";
2419
+ }
2229
2420
  function isInstallProfile(value) {
2230
2421
  return ["hosted-only", "hosted-companion", "full-stack", "runtime-only"].includes(value);
2231
2422
  }
@@ -2321,6 +2512,8 @@ function buildAnswersFromProfile(input) {
2321
2512
  return {
2322
2513
  projectSlug: input.projectSlug,
2323
2514
  projectId: input.projectId,
2515
+ deployment: input.deployment,
2516
+ serverUrl: input.deployment === "hosted" ? `${API_URL}/mcp/${encodeURIComponent(input.projectSlug)}` : normalizeMcpEndpoint(input.serverUrl),
2324
2517
  apiKey: input.apiKey || "YOUR_API_KEY_HERE",
2325
2518
  apiKeyType: input.profile === "runtime-only" ? "none" : input.apiKeyType,
2326
2519
  profile: input.profile,
@@ -2495,17 +2688,20 @@ function runDemo(options) {
2495
2688
  function sleep(ms) {
2496
2689
  return new Promise((resolve) => setTimeout(resolve, ms));
2497
2690
  }
2498
- function isManualApiKey(value) {
2691
+ function isValidApiKey(value, deployment) {
2499
2692
  const trimmed = value.trim();
2693
+ if (deployment === "self-hosted") {
2694
+ return trimmed.length >= 8 && !/\s/.test(trimmed);
2695
+ }
2500
2696
  return trimmed.startsWith("snp-");
2501
2697
  }
2502
- function manualApiKeyValidationMessage() {
2503
- return "API key should start with snp-";
2698
+ function manualApiKeyValidationMessage(deployment = "hosted") {
2699
+ return deployment === "self-hosted" ? "Self-hosted API key should be at least 8 characters and contain no spaces" : "API key should start with snp-";
2504
2700
  }
2505
2701
  function warnApiKeyCliExposure() {
2506
2702
  console.log(
2507
2703
  chalk.yellow(
2508
- " \u26A0\uFE0F --api-key can be exposed in shell history or process lists. Prefer SNIPARA_API_KEY or the interactive prompt for future runs."
2704
+ " \u26A0\uFE0F --api-key can be exposed in shell history or process lists. Prefer SNIPARA_API_KEY, SNIPARA_LOCAL_API_KEY, or the interactive prompt for future runs."
2509
2705
  )
2510
2706
  );
2511
2707
  }
@@ -2788,7 +2984,7 @@ function readExistingCorrelationSessionId() {
2788
2984
  }
2789
2985
  function getCorrelationSessionId(answers) {
2790
2986
  const existing = normalizeCorrelationSessionId(answers.correlationSessionId) || readExistingCorrelationSessionId();
2791
- const sessionId = existing || `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
2987
+ const sessionId = existing || `sess_${Date.now()}_${randomUUID().replace(/-/g, "").slice(0, 12)}`;
2792
2988
  answers.correlationSessionId = sessionId;
2793
2989
  return sessionId;
2794
2990
  }
@@ -2800,7 +2996,11 @@ async function createClaudeCodeConfig(answers) {
2800
2996
  return;
2801
2997
  }
2802
2998
  console.log(chalk.bold("\n\u{1F916} Claude Code MCP\n"));
2803
- console.log(chalk.green(" \u2713 Project .mcp.json contains the Snipara Hosted MCP server"));
2999
+ console.log(
3000
+ chalk.green(
3001
+ ` \u2713 Project .mcp.json contains the ${getDeploymentLabel(answers.deployment)} MCP server`
3002
+ )
3003
+ );
2804
3004
  console.log(chalk.dim(" Claude Code loads project-scoped MCP servers from .mcp.json."));
2805
3005
  console.log(
2806
3006
  chalk.dim(
@@ -2921,8 +3121,14 @@ function writeIfMissing(filePath, content, label) {
2921
3121
  fs.writeFileSync(filePath, content);
2922
3122
  console.log(chalk.green(` Created ${label}`));
2923
3123
  }
3124
+ function buildMcpEndpoint(answers) {
3125
+ if (answers.deployment === "self-hosted") {
3126
+ return answers.serverUrl;
3127
+ }
3128
+ return `${API_URL}/mcp/${encodeURIComponent(answers.projectSlug)}`;
3129
+ }
2924
3130
  function buildHostedMcpEndpoint(answers) {
2925
- return `${API_URL}/mcp/${answers.projectSlug}`;
3131
+ return buildMcpEndpoint(answers);
2926
3132
  }
2927
3133
  function writeAgentSkillPackTemplates(skillsDir, answers) {
2928
3134
  const skills = buildSniparaSkillPackTemplates(answers);
@@ -3304,8 +3510,7 @@ Agent task lifecycle:
3304
3510
  commit SHA, indexing time, included file sample, coverage, freshness warnings,
3305
3511
  and sync guidance before relying on it. If \`git status --short\` is dirty,
3306
3512
  warn that the indexed graph may not include local edits.
3307
- 12. Use Snipara Sandbox only when sandboxed execution, repeatable validation, or
3308
- isolated transformations materially help.
3513
+ 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.
3309
3514
  13. If a broad query is slow, retry once with a narrow keyword query before
3310
3515
  falling back to local search.
3311
3516
  14. When generated hooks are enabled, \`snipara-companion pre-tool\`, \`post-tool\`,
@@ -3356,7 +3561,7 @@ ${EXPERIENCE_GOAL_SECTION}
3356
3561
  - If a broad query is slow, retry once with a narrow keyword query before falling back to local search.
3357
3562
  - 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.
3358
3563
  - 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.
3359
- - Use Snipara Sandbox only when sandboxed execution or repeatable validation materially helps. For runtime-bound phases, capture compact rehydratable state with \`workflow runtime-checkpoint <phase_id> --summary "<state>" --rehydrate-file <state.json>\`. Then \`workflow resume\` restores workflow and memory continuity plus the recorded Sandbox binding and prints a reattach or rehydrate plan. It does not snapshot or exactly restore a live Snipara Sandbox or REPL process.
3564
+ - 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.
3360
3565
  - 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\`.
3361
3566
  - Use \`snipara_remember_if_novel\` or \`snipara_remember\` only for narrow durable memories.
3362
3567
  - 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\`.
@@ -3397,7 +3602,7 @@ ${EXPERIENCE_GOAL_SECTION}
3397
3602
  - If a broad query is slow, retry once with a narrow keyword query before falling back to local search.
3398
3603
  - 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.
3399
3604
  - 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.
3400
- - Use Snipara Sandbox only when sandboxed execution or repeatable validation materially helps. For runtime-bound phases, capture compact rehydratable state with \`workflow runtime-checkpoint <phase_id> --summary "<state>" --rehydrate-file <state.json>\`. Then \`workflow resume\` restores workflow and memory continuity plus the recorded Sandbox binding and prints a reattach or rehydrate plan. It does not snapshot or exactly restore a live Snipara Sandbox or REPL process.
3605
+ - 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.
3401
3606
  - 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.
3402
3607
  `;
3403
3608
  }
@@ -3874,15 +4079,19 @@ async function updateEnvFiles(answers) {
3874
4079
  const envPath = path.join(process.cwd(), ".env");
3875
4080
  let envVars = "";
3876
4081
  let envExampleVars = "";
3877
- if (answers.apiKeyType !== "none") {
4082
+ if (answers.profile !== "runtime-only") {
3878
4083
  envVars += `
3879
4084
  # Snipara Configuration
4085
+ SNIPARA_DEPLOYMENT=${answers.deployment}
4086
+ SNIPARA_MCP_URL=${answers.serverUrl}
3880
4087
  SNIPARA_API_KEY=${answers.apiKey.includes("YOUR_") ? "your_api_key_here" : answers.apiKey}
3881
4088
  SNIPARA_PROJECT_SLUG=${answers.projectSlug}
3882
4089
  ${answers.projectId ? `SNIPARA_PROJECT_ID=${answers.projectId}` : ""}
3883
4090
  `.trim();
3884
4091
  envExampleVars += `
3885
4092
  # Snipara Configuration
4093
+ SNIPARA_DEPLOYMENT=${answers.deployment}
4094
+ SNIPARA_MCP_URL=${answers.serverUrl}
3886
4095
  SNIPARA_API_KEY=your_api_key_here
3887
4096
  SNIPARA_PROJECT_SLUG=${answers.projectSlug || "your_project_slug"}
3888
4097
  ${answers.projectId ? `SNIPARA_PROJECT_ID=${answers.projectId}` : "SNIPARA_PROJECT_ID=your_project_id"}
@@ -3907,10 +4116,14 @@ ${llmVarName}=your_${answers.llmProvider}_api_key_here`;
3907
4116
  }
3908
4117
  if (envExampleVars && fs.existsSync(envExamplePath)) {
3909
4118
  const content = fs.readFileSync(envExamplePath, "utf-8");
3910
- if (!content.includes("SNIPARA_API_KEY") && !content.includes("SNIPARA_PROJECT_SLUG")) {
4119
+ const missingLines = envExampleVars.split("\n").filter((line) => {
4120
+ const match = line.match(/^([A-Z0-9_]+)=/);
4121
+ return Boolean(match && !content.includes(`${match[1]}=`));
4122
+ });
4123
+ if (missingLines.length > 0) {
3911
4124
  fs.appendFileSync(envExamplePath, `
3912
4125
 
3913
- ${envExampleVars}
4126
+ ${missingLines.join("\n")}
3914
4127
  `);
3915
4128
  console.log(chalk.green(" \u2713 Updated .env.example"));
3916
4129
  }
@@ -3921,16 +4134,26 @@ ${envExampleVars}
3921
4134
  const targetEnvPath = fs.existsSync(envLocalPath) ? envLocalPath : envPath;
3922
4135
  if (fs.existsSync(targetEnvPath)) {
3923
4136
  const content = fs.readFileSync(targetEnvPath, "utf-8");
3924
- if (!content.includes("SNIPARA_API_KEY") && !answers.apiKey.includes("YOUR_")) {
4137
+ const missingLines = envVars.split("\n").filter((line) => {
4138
+ const match = line.match(/^([A-Z0-9_]+)=/);
4139
+ if (!match) {
4140
+ return false;
4141
+ }
4142
+ if (match[1] === "SNIPARA_API_KEY") {
4143
+ return !content.includes("SNIPARA_API_KEY=") && !answers.apiKey.includes("YOUR_");
4144
+ }
4145
+ return !content.includes(`${match[1]}=`);
4146
+ });
4147
+ if (missingLines.length > 0) {
3925
4148
  fs.appendFileSync(targetEnvPath, `
3926
4149
 
3927
- ${envVars}
4150
+ ${missingLines.join("\n")}
3928
4151
  `);
3929
4152
  console.log(chalk.green(` \u2713 Updated ${path.basename(targetEnvPath)}`));
3930
4153
  }
3931
4154
  return;
3932
4155
  }
3933
- if (!answers.apiKey.includes("YOUR_") || answers.llmApiKey) {
4156
+ if (!answers.apiKey.includes("YOUR_") && answers.profile !== "runtime-only" || answers.llmApiKey) {
3934
4157
  fs.writeFileSync(targetEnvPath, envVars + "\n");
3935
4158
  console.log(chalk.green(` \u2713 Created ${path.basename(targetEnvPath)}`));
3936
4159
  }
@@ -3946,7 +4169,7 @@ async function createCompanionConfig(answers) {
3946
4169
  const workspaceConfig = {
3947
4170
  ...existingWorkspaceConfig,
3948
4171
  apiKey: answers.apiKey,
3949
- apiUrl: API_URL,
4172
+ apiUrl: answers.deployment === "hosted" ? API_URL : answers.serverUrl,
3950
4173
  projectId: answers.projectId || answers.projectSlug,
3951
4174
  sessionId: getCorrelationSessionId(answers),
3952
4175
  client: getAutomationClientId(answers.aiClient)
@@ -3991,7 +4214,7 @@ async function installLocalCodeOverlayHooks(answers) {
3991
4214
  }
3992
4215
  }
3993
4216
  async function syncRemoteAutomationClient(answers, options) {
3994
- if (!options.enabled || answers.apiKeyType === "none" || !answers.apiKey || answers.apiKey.includes("YOUR_") || !answers.projectSlug) {
4217
+ if (answers.deployment !== "hosted" || !options.enabled || answers.apiKeyType === "none" || !answers.apiKey || answers.apiKey.includes("YOUR_") || !answers.projectSlug) {
3995
4218
  return;
3996
4219
  }
3997
4220
  const automationClient = getAutomationClientId(answers.aiClient);
@@ -4035,7 +4258,7 @@ function buildActivationSurfaceContract(client) {
4035
4258
  const shared = {
4036
4259
  doctrine: "one_activation_engine_multiple_entry_points",
4037
4260
  engine: "create-snipara",
4038
- firstSuccessState: "Hosted MCP connected, local workspace context seeded, and a First Work Brief ready for the next agent session.",
4261
+ firstSuccessState: "The configured Snipara endpoint connected, local workspace context was seeded, and a First Work Brief is ready for the next agent session.",
4039
4262
  mustNotDuplicate: [
4040
4263
  "Local source scan heuristics",
4041
4264
  "First Work Brief generation",
@@ -4477,6 +4700,8 @@ function getClientAutomationCommand(client) {
4477
4700
  function buildClientPackMarkdown(answers, commands, guidance) {
4478
4701
  const clientName = getClientDisplayName(answers.aiClient);
4479
4702
  const automationCommand = getClientAutomationCommand(answers.aiClient);
4703
+ const deploymentLabel = getDeploymentLabel(answers.deployment);
4704
+ const toolNamespace = answers.deployment === "self-hosted" ? "rlm_*" : "snipara_*";
4480
4705
  const bootstrapCommand = commands.find(
4481
4706
  (command) => command.title === "Bootstrap local companion"
4482
4707
  );
@@ -4496,9 +4721,9 @@ Use \`AGENTS.md\` as the shared source of agent behavior, then apply the client-
4496
4721
  - Next surface action: ${surfaceContract.nextSurfaceAction}
4497
4722
 
4498
4723
  The client-specific files in this pack should not reimplement local source scanning,
4499
- First Work Brief generation, memory-candidate extraction, or Hosted MCP config semantics.
4724
+ First Work Brief generation, memory-candidate extraction, or MCP config semantics.
4500
4725
 
4501
- ## Hosted MCP
4726
+ ## ${deploymentLabel}
4502
4727
 
4503
4728
  - Endpoint: \`${buildHostedMcpEndpoint(answers)}\`
4504
4729
  - Auth: \`SNIPARA_API_KEY\` from the environment or local client secret store
@@ -4511,7 +4736,7 @@ ${getClientConfigArtifacts(answers.aiClient).map((item) => `- \`${item}\``).join
4511
4736
  ## Recommended Setup
4512
4737
 
4513
4738
  ${bootstrapCommand ? `1. Bootstrap companion: \`${bootstrapCommand.command}\`` : `1. Bootstrap companion with \`${COMPANION_NPX_COMMAND} init\`.`}
4514
- 2. Confirm the client can list or call the \`snipara_*\` MCP tools.
4739
+ 2. Confirm the client can list or call the \`${toolNamespace}\` MCP tools.
4515
4740
  3. Keep exact edits, tests, and dirty working-tree checks local; use Snipara for memory, source truth, and indexed code graph context.
4516
4741
  ${automationCommand ? `4. For generated local automations, run \`${automationCommand}\` when that client exposes usable automation files.` : "4. This client is treated as MCP-first; no local hook surface is assumed."}
4517
4742
 
@@ -4542,10 +4767,11 @@ async function createCompanionPack(answers) {
4542
4767
  const guidance = [UNIVERSAL_CLIENT_GUIDANCE, ...getClientGuidance(answers.aiClient)];
4543
4768
  const detection = detectEnvironment();
4544
4769
  const clientPackFilename = getClientPackFilename(answers.aiClient);
4770
+ const deploymentLabel = getDeploymentLabel(answers.deployment);
4545
4771
  fs.mkdirSync(packDir, { recursive: true });
4546
4772
  const readme = `# Snipara Companion Pack
4547
4773
 
4548
- This project uses Snipara as a hosted-first project intelligence layer with source-backed context, reviewed memory, and local companion workflow helpers.
4774
+ This project uses ${deploymentLabel} as a project intelligence layer with source-backed context, reviewed memory, and local companion workflow helpers.
4549
4775
  The activation doctrine is: one shared activation engine, multiple entry points.
4550
4776
  \`create-snipara\` is the canonical bootstrap; editor extensions and plugins should provide UI, rules, or handoff affordances without forking the activation engine.
4551
4777
 
@@ -4557,7 +4783,7 @@ The activation doctrine is: one shared activation engine, multiple entry points.
4557
4783
  - AI client: \`${answers.aiClient}\`
4558
4784
  - Client pack: \`${clientPackFilename}\`
4559
4785
  - Installed local companion: \`${answers.installCompanion ? "yes" : "no"}\`
4560
- - Generated hosted MCP config: \`${answers.profile !== "runtime-only" ? "yes" : "no"}\`
4786
+ - Generated ${deploymentLabel} MCP config: \`${answers.profile !== "runtime-only" ? "yes" : "no"}\`
4561
4787
  - Installed local snipara-mcp package: \`${answers.installMcp ? "yes" : "no"}\`
4562
4788
  - Installed Snipara Sandbox: \`${answers.installRuntime ? "yes" : "no"}\`
4563
4789
  - Installed Snipara Orchestrator: \`${answers.installOrchestrator ? "yes" : "no"}\`
@@ -4633,9 +4859,9 @@ Inspect local drift:
4633
4859
 
4634
4860
  \`${CREATE_SNIPARA_NPX_COMMAND} repair --with-companion\``}
4635
4861
 
4636
- GitHub automation remains useful after first value: it gives Snipara a canonical hosted repository
4637
- sync and PR Answer Packs. Local source activation is the fallback, not a replacement for explicit
4638
- provider approval when a team wants shared hosted code context.
4862
+ GitHub automation remains a Cloud-only option after first value: it gives Snipara a canonical
4863
+ hosted repository sync and PR Answer Packs. In self-hosted mode, keep source activation local and
4864
+ do not send repository data to the Cloud unless the user explicitly starts a separate Cloud setup.
4639
4865
 
4640
4866
  ## GitHub PR Answer Packs
4641
4867
 
@@ -4977,10 +5203,10 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
4977
5203
  detail: detection.files.sniparaSkillPack ? ".snipara/templates/skills contains the Snipara skill pack" : "Snipara skill pack missing from .snipara/templates/skills"
4978
5204
  });
4979
5205
  if (targetClient === "codex") {
4980
- checks.push(getCodexConfigCheck());
5206
+ checks.push(getCodexConfigCheck(effectiveAnswers));
4981
5207
  }
4982
5208
  if (targetClient === "mistral") {
4983
- checks.push(getMistralConfigCheck());
5209
+ checks.push(getMistralConfigCheck(effectiveAnswers));
4984
5210
  }
4985
5211
  checks.push({
4986
5212
  name: "env_files",
@@ -5010,7 +5236,7 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
5010
5236
  checks.push({
5011
5237
  name: "hosted_connection",
5012
5238
  status: "warn",
5013
- detail: "Skipped hosted connectivity test because no live API key was available"
5239
+ detail: `Skipped ${getDeploymentLabel(answers.deployment)} connectivity test because no live API key was available`
5014
5240
  });
5015
5241
  }
5016
5242
  for (const check of checks) {
@@ -5029,7 +5255,7 @@ function writeDoctorReport(report) {
5029
5255
  fs.writeFileSync(path.join(reportDir, "doctor.json"), JSON.stringify(report, null, 2) + "\n");
5030
5256
  console.log(chalk.green(" \u2713 Wrote .snipara/companion/doctor.json"));
5031
5257
  }
5032
- function getCodexConfigCheck() {
5258
+ function getCodexConfigCheck(answers) {
5033
5259
  const candidates = [
5034
5260
  {
5035
5261
  label: ".codex/config.toml",
@@ -5069,8 +5295,8 @@ function getCodexConfigCheck() {
5069
5295
  const nonSniparaBearerEnvVars = configuredBearerEnvVars.filter(
5070
5296
  (candidate) => candidate.value !== "SNIPARA_API_KEY"
5071
5297
  );
5072
- const hasHostedEndpoint = present.some(
5073
- (candidate) => candidate.content.includes(`${API_URL}/mcp/`)
5298
+ const hasConfiguredEndpoint = present.some(
5299
+ (candidate) => candidate.content.includes(buildMcpEndpoint(answers))
5074
5300
  );
5075
5301
  const labels = present.map((candidate) => candidate.label).join(", ");
5076
5302
  if (nonSniparaBearerEnvVars.length > 0) {
@@ -5081,20 +5307,20 @@ function getCodexConfigCheck() {
5081
5307
  detail: `Codex config uses a non-Snipara bearer env var (${details}); use SNIPARA_API_KEY and restart Codex`
5082
5308
  };
5083
5309
  }
5084
- if (!usesSniparaEnv || !hasHostedEndpoint) {
5310
+ if (!usesSniparaEnv || !hasConfiguredEndpoint) {
5085
5311
  return {
5086
5312
  name: "codex_config",
5087
5313
  status: "warn",
5088
- detail: `Codex config found in ${labels}, but expected hosted MCP endpoint plus bearer_token_env_var = "SNIPARA_API_KEY"`
5314
+ detail: `Codex config found in ${labels}, but expected the configured ${getDeploymentLabel(answers.deployment)} endpoint plus bearer_token_env_var = "SNIPARA_API_KEY"`
5089
5315
  };
5090
5316
  }
5091
5317
  return {
5092
5318
  name: "codex_config",
5093
5319
  status: "pass",
5094
- detail: `Codex config uses hosted MCP and SNIPARA_API_KEY (${labels}); restart Codex after config changes`
5320
+ detail: `Codex config uses ${getDeploymentLabel(answers.deployment)} and SNIPARA_API_KEY (${labels}); restart Codex after config changes`
5095
5321
  };
5096
5322
  }
5097
- function getMistralConfigCheck() {
5323
+ function getMistralConfigCheck(answers) {
5098
5324
  const candidates = [
5099
5325
  {
5100
5326
  label: ".vibe/config.toml",
@@ -5120,24 +5346,24 @@ function getMistralConfigCheck() {
5120
5346
  detail: "No Mistral Vibe config found; use .snipara/templates/mistral-vibe-config.toml or add Snipara as a Le Chat Custom MCP Connector"
5121
5347
  };
5122
5348
  }
5123
- const hasHostedEndpoint = present.some(
5124
- (candidate) => candidate.content.includes(`${API_URL}/mcp/`)
5349
+ const hasConfiguredEndpoint = present.some(
5350
+ (candidate) => candidate.content.includes(buildMcpEndpoint(answers))
5125
5351
  );
5126
5352
  const hasSniparaEnv = present.some(
5127
5353
  (candidate) => candidate.content.includes('api_key_env = "SNIPARA_API_KEY"')
5128
5354
  );
5129
5355
  const labels = present.map((candidate) => candidate.label).join(", ");
5130
- if (!hasHostedEndpoint || !hasSniparaEnv) {
5356
+ if (!hasConfiguredEndpoint || !hasSniparaEnv) {
5131
5357
  return {
5132
5358
  name: "mistral_config",
5133
5359
  status: "warn",
5134
- detail: `Mistral config found in ${labels}, but expected hosted MCP endpoint plus api_key_env = "SNIPARA_API_KEY"`
5360
+ detail: `Mistral config found in ${labels}, but expected the configured ${getDeploymentLabel(answers.deployment)} endpoint plus api_key_env = "SNIPARA_API_KEY"`
5135
5361
  };
5136
5362
  }
5137
5363
  return {
5138
5364
  name: "mistral_config",
5139
5365
  status: "pass",
5140
- detail: `Mistral Vibe config uses hosted MCP and SNIPARA_API_KEY (${labels}); restart Vibe after config changes`
5366
+ detail: `Mistral Vibe config uses ${getDeploymentLabel(answers.deployment)} and SNIPARA_API_KEY (${labels}); restart Vibe after config changes`
5141
5367
  };
5142
5368
  }
5143
5369
  function printDoctorReport(detection, report) {
@@ -5159,7 +5385,8 @@ function printDoctorReport(detection, report) {
5159
5385
  console.log("");
5160
5386
  }
5161
5387
  async function testHostedConnection(answers) {
5162
- const spinner = ora("Connecting to Snipara Hosted MCP...").start();
5388
+ const deploymentLabel = getDeploymentLabel(answers.deployment);
5389
+ const spinner = ora(`Connecting to ${deploymentLabel}...`).start();
5163
5390
  try {
5164
5391
  const toolsResponse = await callHostedMcp(answers, "tools/list", {});
5165
5392
  if (!toolsResponse.ok) {
@@ -5182,7 +5409,9 @@ async function testHostedConnection(answers) {
5182
5409
  }
5183
5410
  ];
5184
5411
  }
5185
- spinner.succeed("Connected to Snipara Hosted MCP.");
5412
+ spinner.succeed(
5413
+ answers.deployment === "hosted" ? "Connected to Snipara Hosted MCP." : `Connected to ${deploymentLabel}.`
5414
+ );
5186
5415
  const toolNames = extractMcpToolNames(toolsResponse.data);
5187
5416
  const hasSniparaTools = toolNames.some((name) => name.startsWith("snipara_"));
5188
5417
  const advertisesLegacyTools = toolNames.some((name) => name.startsWith("rlm_"));
@@ -5194,29 +5423,32 @@ async function testHostedConnection(answers) {
5194
5423
  },
5195
5424
  {
5196
5425
  name: "hosted_snipara_tools",
5197
- status: hasSniparaTools ? "pass" : "warn",
5198
- detail: hasSniparaTools ? "snipara_* tools are advertised by Hosted MCP" : "tools/list succeeded, but expected snipara_* tools were not detected"
5426
+ status: hasSniparaTools || advertisesLegacyTools ? "pass" : "warn",
5427
+ detail: hasSniparaTools || advertisesLegacyTools ? `${toolNames.filter((name) => name.startsWith("snipara_") || name.startsWith("rlm_")).length} Snipara MCP tools are advertised` : "tools/list succeeded, but no Snipara tool namespace was detected"
5199
5428
  },
5200
5429
  {
5201
5430
  name: "hosted_legacy_tools_hidden",
5202
- status: advertisesLegacyTools ? "warn" : "pass",
5203
- detail: advertisesLegacyTools ? "tools/list still advertises legacy rlm_* tools; update or redeploy the hosted MCP surface" : "tools/list does not advertise legacy rlm_* duplicates"
5431
+ status: answers.deployment === "hosted" && advertisesLegacyTools ? "warn" : "pass",
5432
+ detail: answers.deployment === "hosted" && advertisesLegacyTools ? "Hosted MCP still advertises legacy rlm_* tools; update or redeploy the hosted surface" : answers.deployment === "self-hosted" ? "Self-hosted deployment may expose the public rlm_* compatibility namespace" : "Hosted MCP does not advertise legacy rlm_* duplicates"
5204
5433
  }
5205
5434
  ];
5206
- const settingsTool = "snipara_settings";
5435
+ if (answers.deployment === "self-hosted") {
5436
+ checks.push(...await testSelfHostedCapabilities(answers));
5437
+ }
5438
+ const settingsTool = answers.deployment === "self-hosted" ? "rlm_settings" : "snipara_settings";
5207
5439
  const settingsResponse = await callHostedMcp(answers, "tools/call", {
5208
5440
  name: settingsTool,
5209
5441
  arguments: {}
5210
5442
  });
5211
5443
  if (settingsResponse.ok && !settingsResponse.data.error) {
5212
5444
  checks.push({
5213
- name: "hosted_settings",
5445
+ name: "deployment_settings",
5214
5446
  status: "pass",
5215
5447
  detail: `${settingsTool} tool call succeeded`
5216
5448
  });
5217
5449
  } else {
5218
5450
  checks.push({
5219
- name: "hosted_settings",
5451
+ name: "deployment_settings",
5220
5452
  status: "warn",
5221
5453
  detail: settingsResponse.ok ? describeJsonRpcError(settingsResponse.data) : describeHostedHttpFailure(settingsResponse.status, settingsResponse.bodyText)
5222
5454
  });
@@ -5228,7 +5460,40 @@ async function testHostedConnection(answers) {
5228
5460
  {
5229
5461
  name: "hosted_connection",
5230
5462
  status: "warn",
5231
- detail: "Could not reach Snipara Hosted MCP from this machine"
5463
+ detail: `Could not reach ${deploymentLabel} from this machine`
5464
+ }
5465
+ ];
5466
+ }
5467
+ }
5468
+ async function testSelfHostedCapabilities(answers) {
5469
+ try {
5470
+ const endpoint = new URL(answers.serverUrl);
5471
+ const response = await fetch(`${endpoint.origin}/capabilities`, {
5472
+ headers: { "X-API-Key": answers.apiKey }
5473
+ });
5474
+ const body = await response.json();
5475
+ if (!response.ok) {
5476
+ return [
5477
+ {
5478
+ name: "self_hosted_contract",
5479
+ status: "warn",
5480
+ detail: `Could not verify /capabilities (HTTP ${response.status})`
5481
+ }
5482
+ ];
5483
+ }
5484
+ return [
5485
+ {
5486
+ name: "self_hosted_contract",
5487
+ status: body.contract === "snipara-server-oss-v2" ? "pass" : "warn",
5488
+ detail: body.contract === "snipara-server-oss-v2" ? "snipara-server-oss-v2 contract verified" : `Unexpected self-hosted contract${body.contract ? `: ${body.contract}` : ""}`
5489
+ }
5490
+ ];
5491
+ } catch {
5492
+ return [
5493
+ {
5494
+ name: "self_hosted_contract",
5495
+ status: "warn",
5496
+ detail: "Self-hosted /capabilities endpoint could not be verified"
5232
5497
  }
5233
5498
  ];
5234
5499
  }
@@ -5243,7 +5508,7 @@ function extractMcpToolNames(data) {
5243
5508
  ).filter(Boolean);
5244
5509
  }
5245
5510
  async function callHostedMcp(answers, method, params) {
5246
- const response = await fetch(`${API_URL}/mcp/${answers.projectSlug}`, {
5511
+ const response = await fetch(buildMcpEndpoint(answers), {
5247
5512
  method: "POST",
5248
5513
  headers: {
5249
5514
  "Content-Type": "application/json",
@@ -5315,8 +5580,24 @@ function printActivationSummary(manifest) {
5315
5580
  ` ${formatActivationStatus(manifest.lanes.connection.status)} Connected - ${manifest.lanes.connection.detail}`
5316
5581
  );
5317
5582
  console.log(` ${formatActivationStatus(contextLane.status)} Context - ${contextLane.detail}`);
5318
- console.log(` ${chalk.dim("\u25CB pending")} First grounded answer`);
5319
- console.log(chalk.yellow("\n Activation pending: first grounded answer needed."));
5583
+ if (manifest.valueProof.status === "ready") {
5584
+ console.log(
5585
+ ` ${chalk.green("\u2713 ready")} Value proof - ${manifest.valueProof.detail} (${manifest.valueProof.evidence || "evidence recorded"})`
5586
+ );
5587
+ console.log(chalk.green("\n Activation ready: First grounded answer verified."));
5588
+ } else if (manifest.valueProof.status === "failed") {
5589
+ console.log(
5590
+ ` ${chalk.red("\u2717 failed")} Value proof - ${manifest.valueProof.error || manifest.valueProof.detail}`
5591
+ );
5592
+ console.log(
5593
+ chalk.yellow(
5594
+ "\n Activation needs attention: run create-snipara doctor after checking the MCP endpoint."
5595
+ )
5596
+ );
5597
+ } else {
5598
+ console.log(` ${chalk.dim("\u25CB pending")} First grounded answer`);
5599
+ console.log(chalk.yellow("\n Activation pending: first grounded answer needed."));
5600
+ }
5320
5601
  console.log(
5321
5602
  chalk.dim(
5322
5603
  `
@@ -5440,6 +5721,7 @@ function detectEnvironment() {
5440
5721
  existingConfig: {
5441
5722
  slug: existingConfig?.slug,
5442
5723
  projectId: existingConfig?.projectId,
5724
+ serverUrl: existingConfig?.serverUrl,
5443
5725
  hasSniparaServer: existingConfig?.hasSniparaServer ?? false,
5444
5726
  hasRuntimeServer: existingConfig?.hasRuntimeServer ?? false
5445
5727
  }
@@ -5448,19 +5730,22 @@ function detectEnvironment() {
5448
5730
  function loadExistingAnswers(options) {
5449
5731
  const detection = detectEnvironment();
5450
5732
  const effectiveProfile = resolveMaintenanceProfile(detection, options.profile);
5733
+ const deployment = resolveRequestedDeployment(options);
5451
5734
  const providedApiKey = options.apiKey?.trim();
5452
- if (providedApiKey && !isManualApiKey(providedApiKey)) {
5453
- throw new Error(manualApiKeyValidationMessage());
5735
+ if (providedApiKey && !isValidApiKey(providedApiKey, deployment)) {
5736
+ throw new Error(manualApiKeyValidationMessage(deployment));
5454
5737
  }
5455
5738
  if (providedApiKey) {
5456
5739
  warnApiKeyCliExposure();
5457
5740
  }
5458
- const envApiKey = readApiKeyFromEnvFiles();
5741
+ const envApiKey = readApiKeyFromEnvFiles(deployment);
5459
5742
  const apiKey = providedApiKey || envApiKey || "YOUR_API_KEY_HERE";
5460
5743
  const apiKeyType = effectiveProfile === "runtime-only" ? "none" : providedApiKey || envApiKey ? "api" : "none";
5461
5744
  return buildAnswersFromProfile({
5462
5745
  projectSlug: options.slug || detection.projectSlug || "my-project",
5463
5746
  projectId: options.projectId || detection.existingConfig.projectId || "",
5747
+ deployment,
5748
+ serverUrl: options.serverUrl || process.env.SNIPARA_MCP_URL || getExistingConfig()?.serverUrl || "http://localhost:8000/mcp/local",
5464
5749
  apiKey,
5465
5750
  apiKeyType,
5466
5751
  profile: effectiveProfile,
@@ -5487,10 +5772,13 @@ function resolveMaintenanceProfile(detection, explicitProfile) {
5487
5772
  }
5488
5773
  return "hosted-only";
5489
5774
  }
5490
- function readApiKeyFromEnvFiles() {
5775
+ function readApiKeyFromEnvFiles(deployment = "hosted") {
5491
5776
  const candidates = [".env.local", ".env"];
5492
5777
  for (const file of candidates) {
5493
- const value = readEnvValueFromFile(file, ["SNIPARA_API_KEY"]);
5778
+ const value = readEnvValueFromFile(
5779
+ file,
5780
+ deployment === "self-hosted" ? ["SNIPARA_LOCAL_API_KEY", "SNIPARA_API_KEY"] : ["SNIPARA_API_KEY"]
5781
+ );
5494
5782
  if (value) {
5495
5783
  return value;
5496
5784
  }
@@ -5797,6 +6085,7 @@ function getExistingConfig() {
5797
6085
  return {
5798
6086
  slug: extractSlugFromMcpEndpoint(sniparaServer?.url) || void 0,
5799
6087
  projectId: readExistingCompanionProjectId(),
6088
+ serverUrl: typeof sniparaServer?.url === "string" ? normalizeMcpEndpoint(sniparaServer.url) : void 0,
5800
6089
  hasSniparaServer: Boolean(sniparaServer),
5801
6090
  hasRuntimeServer: Boolean(runtimeServer)
5802
6091
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-snipara",
3
- "version": "2.0.18",
4
- "description": "Onboard Snipara Hosted MCP Context + Memory with project bootstrap intelligence and optional local workflows",
3
+ "version": "2.1.1",
4
+ "description": "Onboard Snipara Cloud or self-hosted MCP with an automatic project-grounded proof of value",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "bin": {
@@ -39,7 +39,11 @@
39
39
  "lm-studio",
40
40
  "vscode",
41
41
  "continue",
42
- "claude-desktop"
42
+ "claude-desktop",
43
+ "self-hosted",
44
+ "snipara-server",
45
+ "project-intelligence",
46
+ "persistent-memory"
43
47
  ],
44
48
  "author": "Snipara",
45
49
  "license": "MIT",