create-snipara 2.0.17 → 2.1.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.
Files changed (3) hide show
  1. package/README.md +63 -13
  2. package/dist/index.js +407 -102
  3. package/package.json +7 -3
package/README.md CHANGED
@@ -1,6 +1,6 @@
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
@@ -10,10 +10,30 @@ impact tooling instead of starting cold every session.
10
10
  npx create-snipara
11
11
  ```
12
12
 
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.
13
+ For a local or private server, use the same activation flow:
14
+
15
+ ```bash
16
+ export SNIPARA_LOCAL_API_KEY="your-server-operator-key"
17
+ npx create-snipara@latest --self-hosted \
18
+ --server-url http://localhost:8000/mcp/local \
19
+ --api-key "$SNIPARA_LOCAL_API_KEY" \
20
+ --client claude-code
21
+ ```
22
+
23
+ Both modes aim to finish with the same visible proof:
24
+
25
+ ```text
26
+ ✓ Endpoint connected
27
+ ✓ Repository README/docs seeded
28
+ ✓ First project-grounded answer verified
29
+ ✓ AI client configuration written
30
+ ```
31
+
32
+ `create-snipara` is the single activation engine for the agent path. It detects
33
+ or asks where Snipara should run, writes the right MCP config, seeds a small
34
+ README/docs corpus, and automatically verifies a first project-grounded answer.
35
+ The default path is Cloud because it is the fastest first experience; the same
36
+ flow works with an existing self-hosted Snipara Server.
17
37
  It is also the canonical activation engine behind editor setup docs: VS Code can
18
38
  surface activation as a native command, Cursor can pair OpenVSX UI with Hosted
19
39
  MCP, but both paths should land on the same project context.
@@ -25,7 +45,7 @@ behind the scenes.
25
45
 
26
46
  Use it for:
27
47
 
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
48
+ - 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
49
  - free account signup with reviewed Context + Memory
30
50
  - local project wiring, `doctor`, `repair`, and `upgrade`
31
51
  - 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 +60,22 @@ normalizes that setup.
40
60
  ```mermaid
41
61
  flowchart LR
42
62
  Project["Local project"] --> CLI["npx create-snipara"]
43
- CLI --> Auth["Snipara auth and project key"]
44
- CLI --> MCP["Hosted MCP config"]
63
+ CLI --> Auth["Cloud auth or self-hosted operator key"]
64
+ CLI --> MCP["Configured MCP endpoint"]
45
65
  CLI --> Templates["AGENTS.md, CLAUDE.md, Cursor, Codex, MCP references"]
46
66
  MCP --> Agents["Claude Code, Cursor, Codex, Gemini, Mistral, VS Code, ChatGPT, Orca, LM Studio stacks"]
47
67
  Templates --> Agents
48
68
  Agents --> Context["Snipara context and memory"]
49
69
  ```
50
70
 
51
- The CLI does not become the runtime brain. After setup, the agent talks to
52
- Hosted MCP and keeps using its own LLM.
71
+ The CLI does not become the runtime brain. After setup, the agent talks to the
72
+ configured MCP endpoint and keeps using its own LLM.
53
73
 
54
74
  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
75
 
56
76
  The product surfaces remain:
57
77
 
58
- - Hosted MCP for LLM agents, context, and reviewed memory
78
+ - Snipara Cloud or self-hosted Snipara Server for LLM agents, context, and reviewed memory
59
79
  - HTTP API / SDK for apps, integrators, backends, and pipelines
60
80
 
61
81
  Advanced flags can wire local helpers around those surfaces:
@@ -67,6 +87,24 @@ Advanced flags can wire local helpers around those surfaces:
67
87
  - local config, env files, hook scripts, and a project companion pack
68
88
  - `snipara-mcp` only for local stdio compatibility, development, or testing
69
89
 
90
+ ## Deployment Modes
91
+
92
+ The deployment mode and install profile are independent:
93
+
94
+ | Mode | Command | Data boundary | First proof |
95
+ | -------------------------- | ------------------------------------------------------------------------------------------------------------------------- | ------------------------------ | ------------------------------------------------------------------ |
96
+ | Snipara Cloud | `npx create-snipara@latest` | Snipara-managed hosted project | Cloud MCP connection, README/docs seed, grounded answer |
97
+ | 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 |
98
+
99
+ Self-hosted runs use the server's `X-API-Key` operator key, the public
100
+ `snipara-server-oss-v2` contract, and the server's `rlm_*` compatibility tools.
101
+ They never open Cloud signup or call the Cloud dashboard automation API.
102
+
103
+ Every successful live setup writes `.snipara/activation/activation-manifest.json`
104
+ with the selected endpoint, seed status, proof tool, and a non-sensitive proof
105
+ receipt such as `answer_pack_chars=...`. Raw API keys are never included in the
106
+ activation report or JSON summary.
107
+
70
108
  ## Install Profiles
71
109
 
72
110
  The CLI now works around explicit install profiles:
@@ -199,11 +237,13 @@ npx create-snipara
199
237
 
200
238
  The interactive flow now asks for:
201
239
 
240
+ - deployment mode when `--advanced` is used
241
+ - a self-hosted MCP endpoint when self-hosted mode is selected
202
242
  - project slug
203
- - API key path: existing API key, free signup, or add key later
243
+ - API key path: Cloud key/signup, self-hosted operator key, or add key later
204
244
  - AI client
205
245
 
206
- By default, this uses the minimal `hosted-only` setup: hosted MCP config and a
246
+ By default, this uses the minimal `hosted-only` setup: configured MCP config and a
207
247
  doctor report, with no local Companion install, hooks, GitHub approval prompt, or
208
248
  sandbox prompt. Add `--advanced` when you want profile selection, project id
209
249
  entry, hook prompts, GitHub automation prompts, or sandbox provider prompts.
@@ -414,9 +454,19 @@ npx create-snipara --runtime-only
414
454
  # Safer non-interactive setup with an existing API key
415
455
  SNIPARA_API_KEY=snp-xxx npx create-snipara -y --slug my-project
416
456
 
457
+ # Self-hosted non-interactive setup; the same command works with any MCP client
458
+ SNIPARA_LOCAL_API_KEY=local-operator-key npx create-snipara -y \
459
+ --self-hosted --server-url http://localhost:8000/mcp/local \
460
+ --api-key "$SNIPARA_LOCAL_API_KEY" --slug my-project
461
+
417
462
  # Compatibility path; prints a warning because CLI args can leak
418
463
  npx create-snipara -y --api-key snp-xxx --slug my-project
419
464
 
465
+ # Explicitly choose the deployment mode
466
+ npx create-snipara -y --deployment hosted --api-key snp-xxx --slug my-project
467
+ npx create-snipara -y --deployment self-hosted \
468
+ --server-url http://localhost:8000/mcp/local --api-key local-operator-key
469
+
420
470
  # Open GitHub automation after setup when a GitHub remote is detected
421
471
  npx create-snipara --github
422
472
  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.17",
17
- description: "Onboard Snipara Hosted MCP Context + Memory with project bootstrap intelligence and optional local workflows",
17
+ version: "2.1.0",
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",
@@ -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);
@@ -3874,15 +4080,19 @@ async function updateEnvFiles(answers) {
3874
4080
  const envPath = path.join(process.cwd(), ".env");
3875
4081
  let envVars = "";
3876
4082
  let envExampleVars = "";
3877
- if (answers.apiKeyType !== "none") {
4083
+ if (answers.profile !== "runtime-only") {
3878
4084
  envVars += `
3879
4085
  # Snipara Configuration
4086
+ SNIPARA_DEPLOYMENT=${answers.deployment}
4087
+ SNIPARA_MCP_URL=${answers.serverUrl}
3880
4088
  SNIPARA_API_KEY=${answers.apiKey.includes("YOUR_") ? "your_api_key_here" : answers.apiKey}
3881
4089
  SNIPARA_PROJECT_SLUG=${answers.projectSlug}
3882
4090
  ${answers.projectId ? `SNIPARA_PROJECT_ID=${answers.projectId}` : ""}
3883
4091
  `.trim();
3884
4092
  envExampleVars += `
3885
4093
  # Snipara Configuration
4094
+ SNIPARA_DEPLOYMENT=${answers.deployment}
4095
+ SNIPARA_MCP_URL=${answers.serverUrl}
3886
4096
  SNIPARA_API_KEY=your_api_key_here
3887
4097
  SNIPARA_PROJECT_SLUG=${answers.projectSlug || "your_project_slug"}
3888
4098
  ${answers.projectId ? `SNIPARA_PROJECT_ID=${answers.projectId}` : "SNIPARA_PROJECT_ID=your_project_id"}
@@ -3907,10 +4117,14 @@ ${llmVarName}=your_${answers.llmProvider}_api_key_here`;
3907
4117
  }
3908
4118
  if (envExampleVars && fs.existsSync(envExamplePath)) {
3909
4119
  const content = fs.readFileSync(envExamplePath, "utf-8");
3910
- if (!content.includes("SNIPARA_API_KEY") && !content.includes("SNIPARA_PROJECT_SLUG")) {
4120
+ const missingLines = envExampleVars.split("\n").filter((line) => {
4121
+ const match = line.match(/^([A-Z0-9_]+)=/);
4122
+ return Boolean(match && !content.includes(`${match[1]}=`));
4123
+ });
4124
+ if (missingLines.length > 0) {
3911
4125
  fs.appendFileSync(envExamplePath, `
3912
4126
 
3913
- ${envExampleVars}
4127
+ ${missingLines.join("\n")}
3914
4128
  `);
3915
4129
  console.log(chalk.green(" \u2713 Updated .env.example"));
3916
4130
  }
@@ -3921,16 +4135,26 @@ ${envExampleVars}
3921
4135
  const targetEnvPath = fs.existsSync(envLocalPath) ? envLocalPath : envPath;
3922
4136
  if (fs.existsSync(targetEnvPath)) {
3923
4137
  const content = fs.readFileSync(targetEnvPath, "utf-8");
3924
- if (!content.includes("SNIPARA_API_KEY") && !answers.apiKey.includes("YOUR_")) {
4138
+ const missingLines = envVars.split("\n").filter((line) => {
4139
+ const match = line.match(/^([A-Z0-9_]+)=/);
4140
+ if (!match) {
4141
+ return false;
4142
+ }
4143
+ if (match[1] === "SNIPARA_API_KEY") {
4144
+ return !content.includes("SNIPARA_API_KEY=") && !answers.apiKey.includes("YOUR_");
4145
+ }
4146
+ return !content.includes(`${match[1]}=`);
4147
+ });
4148
+ if (missingLines.length > 0) {
3925
4149
  fs.appendFileSync(targetEnvPath, `
3926
4150
 
3927
- ${envVars}
4151
+ ${missingLines.join("\n")}
3928
4152
  `);
3929
4153
  console.log(chalk.green(` \u2713 Updated ${path.basename(targetEnvPath)}`));
3930
4154
  }
3931
4155
  return;
3932
4156
  }
3933
- if (!answers.apiKey.includes("YOUR_") || answers.llmApiKey) {
4157
+ if (!answers.apiKey.includes("YOUR_") && answers.profile !== "runtime-only" || answers.llmApiKey) {
3934
4158
  fs.writeFileSync(targetEnvPath, envVars + "\n");
3935
4159
  console.log(chalk.green(` \u2713 Created ${path.basename(targetEnvPath)}`));
3936
4160
  }
@@ -3946,7 +4170,7 @@ async function createCompanionConfig(answers) {
3946
4170
  const workspaceConfig = {
3947
4171
  ...existingWorkspaceConfig,
3948
4172
  apiKey: answers.apiKey,
3949
- apiUrl: API_URL,
4173
+ apiUrl: answers.deployment === "hosted" ? API_URL : answers.serverUrl,
3950
4174
  projectId: answers.projectId || answers.projectSlug,
3951
4175
  sessionId: getCorrelationSessionId(answers),
3952
4176
  client: getAutomationClientId(answers.aiClient)
@@ -3991,7 +4215,7 @@ async function installLocalCodeOverlayHooks(answers) {
3991
4215
  }
3992
4216
  }
3993
4217
  async function syncRemoteAutomationClient(answers, options) {
3994
- if (!options.enabled || answers.apiKeyType === "none" || !answers.apiKey || answers.apiKey.includes("YOUR_") || !answers.projectSlug) {
4218
+ if (answers.deployment !== "hosted" || !options.enabled || answers.apiKeyType === "none" || !answers.apiKey || answers.apiKey.includes("YOUR_") || !answers.projectSlug) {
3995
4219
  return;
3996
4220
  }
3997
4221
  const automationClient = getAutomationClientId(answers.aiClient);
@@ -4035,7 +4259,7 @@ function buildActivationSurfaceContract(client) {
4035
4259
  const shared = {
4036
4260
  doctrine: "one_activation_engine_multiple_entry_points",
4037
4261
  engine: "create-snipara",
4038
- firstSuccessState: "Hosted MCP connected, local workspace context seeded, and a First Work Brief ready for the next agent session.",
4262
+ firstSuccessState: "The configured Snipara endpoint connected, local workspace context was seeded, and a First Work Brief is ready for the next agent session.",
4039
4263
  mustNotDuplicate: [
4040
4264
  "Local source scan heuristics",
4041
4265
  "First Work Brief generation",
@@ -4266,6 +4490,11 @@ function getCompanionCommands(answers) {
4266
4490
  title: "Refresh local source context",
4267
4491
  command: `${COMPANION_NPX_COMMAND} source sync --json`,
4268
4492
  purpose: "Refresh the local snapshot, document dry-run, and code overlay cache after files change."
4493
+ },
4494
+ {
4495
+ title: "Generate a reviewable Project Brief",
4496
+ command: `${COMPANION_NPX_COMMAND} docs bootstrap --preview`,
4497
+ purpose: "Preview a safe, evidence-linked project brief when the repository has little or no documentation; apply it only after review."
4269
4498
  }
4270
4499
  ] : [];
4271
4500
  const businessOnboardingCommands = answers.installCompanion ? [
@@ -4472,6 +4701,8 @@ function getClientAutomationCommand(client) {
4472
4701
  function buildClientPackMarkdown(answers, commands, guidance) {
4473
4702
  const clientName = getClientDisplayName(answers.aiClient);
4474
4703
  const automationCommand = getClientAutomationCommand(answers.aiClient);
4704
+ const deploymentLabel = getDeploymentLabel(answers.deployment);
4705
+ const toolNamespace = answers.deployment === "self-hosted" ? "rlm_*" : "snipara_*";
4475
4706
  const bootstrapCommand = commands.find(
4476
4707
  (command) => command.title === "Bootstrap local companion"
4477
4708
  );
@@ -4491,9 +4722,9 @@ Use \`AGENTS.md\` as the shared source of agent behavior, then apply the client-
4491
4722
  - Next surface action: ${surfaceContract.nextSurfaceAction}
4492
4723
 
4493
4724
  The client-specific files in this pack should not reimplement local source scanning,
4494
- First Work Brief generation, memory-candidate extraction, or Hosted MCP config semantics.
4725
+ First Work Brief generation, memory-candidate extraction, or MCP config semantics.
4495
4726
 
4496
- ## Hosted MCP
4727
+ ## ${deploymentLabel}
4497
4728
 
4498
4729
  - Endpoint: \`${buildHostedMcpEndpoint(answers)}\`
4499
4730
  - Auth: \`SNIPARA_API_KEY\` from the environment or local client secret store
@@ -4506,7 +4737,7 @@ ${getClientConfigArtifacts(answers.aiClient).map((item) => `- \`${item}\``).join
4506
4737
  ## Recommended Setup
4507
4738
 
4508
4739
  ${bootstrapCommand ? `1. Bootstrap companion: \`${bootstrapCommand.command}\`` : `1. Bootstrap companion with \`${COMPANION_NPX_COMMAND} init\`.`}
4509
- 2. Confirm the client can list or call the \`snipara_*\` MCP tools.
4740
+ 2. Confirm the client can list or call the \`${toolNamespace}\` MCP tools.
4510
4741
  3. Keep exact edits, tests, and dirty working-tree checks local; use Snipara for memory, source truth, and indexed code graph context.
4511
4742
  ${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."}
4512
4743
 
@@ -4537,10 +4768,11 @@ async function createCompanionPack(answers) {
4537
4768
  const guidance = [UNIVERSAL_CLIENT_GUIDANCE, ...getClientGuidance(answers.aiClient)];
4538
4769
  const detection = detectEnvironment();
4539
4770
  const clientPackFilename = getClientPackFilename(answers.aiClient);
4771
+ const deploymentLabel = getDeploymentLabel(answers.deployment);
4540
4772
  fs.mkdirSync(packDir, { recursive: true });
4541
4773
  const readme = `# Snipara Companion Pack
4542
4774
 
4543
- This project uses Snipara as a hosted-first project intelligence layer with source-backed context, reviewed memory, and local companion workflow helpers.
4775
+ This project uses ${deploymentLabel} as a project intelligence layer with source-backed context, reviewed memory, and local companion workflow helpers.
4544
4776
  The activation doctrine is: one shared activation engine, multiple entry points.
4545
4777
  \`create-snipara\` is the canonical bootstrap; editor extensions and plugins should provide UI, rules, or handoff affordances without forking the activation engine.
4546
4778
 
@@ -4552,7 +4784,7 @@ The activation doctrine is: one shared activation engine, multiple entry points.
4552
4784
  - AI client: \`${answers.aiClient}\`
4553
4785
  - Client pack: \`${clientPackFilename}\`
4554
4786
  - Installed local companion: \`${answers.installCompanion ? "yes" : "no"}\`
4555
- - Generated hosted MCP config: \`${answers.profile !== "runtime-only" ? "yes" : "no"}\`
4787
+ - Generated ${deploymentLabel} MCP config: \`${answers.profile !== "runtime-only" ? "yes" : "no"}\`
4556
4788
  - Installed local snipara-mcp package: \`${answers.installMcp ? "yes" : "no"}\`
4557
4789
  - Installed Snipara Sandbox: \`${answers.installRuntime ? "yes" : "no"}\`
4558
4790
  - Installed Snipara Orchestrator: \`${answers.installOrchestrator ? "yes" : "no"}\`
@@ -4612,15 +4844,25 @@ Refresh after file changes:
4612
4844
 
4613
4845
  \`${COMPANION_NPX_COMMAND} source sync --json\`
4614
4846
 
4847
+ If the folder has little or no documentation, preview a Project Brief before writing it:
4848
+
4849
+ \`${COMPANION_NPX_COMMAND} docs bootstrap --preview\`
4850
+
4851
+ After reviewing the generated brief, apply and index it:
4852
+
4853
+ \`${COMPANION_NPX_COMMAND} docs bootstrap --apply\`
4854
+
4855
+ \`${COMPANION_NPX_COMMAND} source sync --apply --reindex\`
4856
+
4615
4857
  Inspect local drift:
4616
4858
 
4617
4859
  \`${COMPANION_NPX_COMMAND} source status --json\`` : `This setup did not install snipara-companion. Enable it before local source activation:
4618
4860
 
4619
4861
  \`${CREATE_SNIPARA_NPX_COMMAND} repair --with-companion\``}
4620
4862
 
4621
- GitHub automation remains useful after first value: it gives Snipara a canonical hosted repository
4622
- sync and PR Answer Packs. Local source activation is the fallback, not a replacement for explicit
4623
- provider approval when a team wants shared hosted code context.
4863
+ GitHub automation remains a Cloud-only option after first value: it gives Snipara a canonical
4864
+ hosted repository sync and PR Answer Packs. In self-hosted mode, keep source activation local and
4865
+ do not send repository data to the Cloud unless the user explicitly starts a separate Cloud setup.
4624
4866
 
4625
4867
  ## GitHub PR Answer Packs
4626
4868
 
@@ -4962,10 +5204,10 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
4962
5204
  detail: detection.files.sniparaSkillPack ? ".snipara/templates/skills contains the Snipara skill pack" : "Snipara skill pack missing from .snipara/templates/skills"
4963
5205
  });
4964
5206
  if (targetClient === "codex") {
4965
- checks.push(getCodexConfigCheck());
5207
+ checks.push(getCodexConfigCheck(effectiveAnswers));
4966
5208
  }
4967
5209
  if (targetClient === "mistral") {
4968
- checks.push(getMistralConfigCheck());
5210
+ checks.push(getMistralConfigCheck(effectiveAnswers));
4969
5211
  }
4970
5212
  checks.push({
4971
5213
  name: "env_files",
@@ -4995,7 +5237,7 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
4995
5237
  checks.push({
4996
5238
  name: "hosted_connection",
4997
5239
  status: "warn",
4998
- detail: "Skipped hosted connectivity test because no live API key was available"
5240
+ detail: `Skipped ${getDeploymentLabel(answers.deployment)} connectivity test because no live API key was available`
4999
5241
  });
5000
5242
  }
5001
5243
  for (const check of checks) {
@@ -5014,7 +5256,7 @@ function writeDoctorReport(report) {
5014
5256
  fs.writeFileSync(path.join(reportDir, "doctor.json"), JSON.stringify(report, null, 2) + "\n");
5015
5257
  console.log(chalk.green(" \u2713 Wrote .snipara/companion/doctor.json"));
5016
5258
  }
5017
- function getCodexConfigCheck() {
5259
+ function getCodexConfigCheck(answers) {
5018
5260
  const candidates = [
5019
5261
  {
5020
5262
  label: ".codex/config.toml",
@@ -5054,8 +5296,8 @@ function getCodexConfigCheck() {
5054
5296
  const nonSniparaBearerEnvVars = configuredBearerEnvVars.filter(
5055
5297
  (candidate) => candidate.value !== "SNIPARA_API_KEY"
5056
5298
  );
5057
- const hasHostedEndpoint = present.some(
5058
- (candidate) => candidate.content.includes(`${API_URL}/mcp/`)
5299
+ const hasConfiguredEndpoint = present.some(
5300
+ (candidate) => candidate.content.includes(buildMcpEndpoint(answers))
5059
5301
  );
5060
5302
  const labels = present.map((candidate) => candidate.label).join(", ");
5061
5303
  if (nonSniparaBearerEnvVars.length > 0) {
@@ -5066,20 +5308,20 @@ function getCodexConfigCheck() {
5066
5308
  detail: `Codex config uses a non-Snipara bearer env var (${details}); use SNIPARA_API_KEY and restart Codex`
5067
5309
  };
5068
5310
  }
5069
- if (!usesSniparaEnv || !hasHostedEndpoint) {
5311
+ if (!usesSniparaEnv || !hasConfiguredEndpoint) {
5070
5312
  return {
5071
5313
  name: "codex_config",
5072
5314
  status: "warn",
5073
- detail: `Codex config found in ${labels}, but expected hosted MCP endpoint plus bearer_token_env_var = "SNIPARA_API_KEY"`
5315
+ detail: `Codex config found in ${labels}, but expected the configured ${getDeploymentLabel(answers.deployment)} endpoint plus bearer_token_env_var = "SNIPARA_API_KEY"`
5074
5316
  };
5075
5317
  }
5076
5318
  return {
5077
5319
  name: "codex_config",
5078
5320
  status: "pass",
5079
- detail: `Codex config uses hosted MCP and SNIPARA_API_KEY (${labels}); restart Codex after config changes`
5321
+ detail: `Codex config uses ${getDeploymentLabel(answers.deployment)} and SNIPARA_API_KEY (${labels}); restart Codex after config changes`
5080
5322
  };
5081
5323
  }
5082
- function getMistralConfigCheck() {
5324
+ function getMistralConfigCheck(answers) {
5083
5325
  const candidates = [
5084
5326
  {
5085
5327
  label: ".vibe/config.toml",
@@ -5105,24 +5347,24 @@ function getMistralConfigCheck() {
5105
5347
  detail: "No Mistral Vibe config found; use .snipara/templates/mistral-vibe-config.toml or add Snipara as a Le Chat Custom MCP Connector"
5106
5348
  };
5107
5349
  }
5108
- const hasHostedEndpoint = present.some(
5109
- (candidate) => candidate.content.includes(`${API_URL}/mcp/`)
5350
+ const hasConfiguredEndpoint = present.some(
5351
+ (candidate) => candidate.content.includes(buildMcpEndpoint(answers))
5110
5352
  );
5111
5353
  const hasSniparaEnv = present.some(
5112
5354
  (candidate) => candidate.content.includes('api_key_env = "SNIPARA_API_KEY"')
5113
5355
  );
5114
5356
  const labels = present.map((candidate) => candidate.label).join(", ");
5115
- if (!hasHostedEndpoint || !hasSniparaEnv) {
5357
+ if (!hasConfiguredEndpoint || !hasSniparaEnv) {
5116
5358
  return {
5117
5359
  name: "mistral_config",
5118
5360
  status: "warn",
5119
- detail: `Mistral config found in ${labels}, but expected hosted MCP endpoint plus api_key_env = "SNIPARA_API_KEY"`
5361
+ detail: `Mistral config found in ${labels}, but expected the configured ${getDeploymentLabel(answers.deployment)} endpoint plus api_key_env = "SNIPARA_API_KEY"`
5120
5362
  };
5121
5363
  }
5122
5364
  return {
5123
5365
  name: "mistral_config",
5124
5366
  status: "pass",
5125
- detail: `Mistral Vibe config uses hosted MCP and SNIPARA_API_KEY (${labels}); restart Vibe after config changes`
5367
+ detail: `Mistral Vibe config uses ${getDeploymentLabel(answers.deployment)} and SNIPARA_API_KEY (${labels}); restart Vibe after config changes`
5126
5368
  };
5127
5369
  }
5128
5370
  function printDoctorReport(detection, report) {
@@ -5144,7 +5386,8 @@ function printDoctorReport(detection, report) {
5144
5386
  console.log("");
5145
5387
  }
5146
5388
  async function testHostedConnection(answers) {
5147
- const spinner = ora("Connecting to Snipara Hosted MCP...").start();
5389
+ const deploymentLabel = getDeploymentLabel(answers.deployment);
5390
+ const spinner = ora(`Connecting to ${deploymentLabel}...`).start();
5148
5391
  try {
5149
5392
  const toolsResponse = await callHostedMcp(answers, "tools/list", {});
5150
5393
  if (!toolsResponse.ok) {
@@ -5167,7 +5410,9 @@ async function testHostedConnection(answers) {
5167
5410
  }
5168
5411
  ];
5169
5412
  }
5170
- spinner.succeed("Connected to Snipara Hosted MCP.");
5413
+ spinner.succeed(
5414
+ answers.deployment === "hosted" ? "Connected to Snipara Hosted MCP." : `Connected to ${deploymentLabel}.`
5415
+ );
5171
5416
  const toolNames = extractMcpToolNames(toolsResponse.data);
5172
5417
  const hasSniparaTools = toolNames.some((name) => name.startsWith("snipara_"));
5173
5418
  const advertisesLegacyTools = toolNames.some((name) => name.startsWith("rlm_"));
@@ -5179,29 +5424,32 @@ async function testHostedConnection(answers) {
5179
5424
  },
5180
5425
  {
5181
5426
  name: "hosted_snipara_tools",
5182
- status: hasSniparaTools ? "pass" : "warn",
5183
- detail: hasSniparaTools ? "snipara_* tools are advertised by Hosted MCP" : "tools/list succeeded, but expected snipara_* tools were not detected"
5427
+ status: hasSniparaTools || advertisesLegacyTools ? "pass" : "warn",
5428
+ 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"
5184
5429
  },
5185
5430
  {
5186
5431
  name: "hosted_legacy_tools_hidden",
5187
- status: advertisesLegacyTools ? "warn" : "pass",
5188
- detail: advertisesLegacyTools ? "tools/list still advertises legacy rlm_* tools; update or redeploy the hosted MCP surface" : "tools/list does not advertise legacy rlm_* duplicates"
5432
+ status: answers.deployment === "hosted" && advertisesLegacyTools ? "warn" : "pass",
5433
+ 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"
5189
5434
  }
5190
5435
  ];
5191
- const settingsTool = "snipara_settings";
5436
+ if (answers.deployment === "self-hosted") {
5437
+ checks.push(...await testSelfHostedCapabilities(answers));
5438
+ }
5439
+ const settingsTool = answers.deployment === "self-hosted" ? "rlm_settings" : "snipara_settings";
5192
5440
  const settingsResponse = await callHostedMcp(answers, "tools/call", {
5193
5441
  name: settingsTool,
5194
5442
  arguments: {}
5195
5443
  });
5196
5444
  if (settingsResponse.ok && !settingsResponse.data.error) {
5197
5445
  checks.push({
5198
- name: "hosted_settings",
5446
+ name: "deployment_settings",
5199
5447
  status: "pass",
5200
5448
  detail: `${settingsTool} tool call succeeded`
5201
5449
  });
5202
5450
  } else {
5203
5451
  checks.push({
5204
- name: "hosted_settings",
5452
+ name: "deployment_settings",
5205
5453
  status: "warn",
5206
5454
  detail: settingsResponse.ok ? describeJsonRpcError(settingsResponse.data) : describeHostedHttpFailure(settingsResponse.status, settingsResponse.bodyText)
5207
5455
  });
@@ -5213,7 +5461,40 @@ async function testHostedConnection(answers) {
5213
5461
  {
5214
5462
  name: "hosted_connection",
5215
5463
  status: "warn",
5216
- detail: "Could not reach Snipara Hosted MCP from this machine"
5464
+ detail: `Could not reach ${deploymentLabel} from this machine`
5465
+ }
5466
+ ];
5467
+ }
5468
+ }
5469
+ async function testSelfHostedCapabilities(answers) {
5470
+ try {
5471
+ const endpoint = new URL(answers.serverUrl);
5472
+ const response = await fetch(`${endpoint.origin}/capabilities`, {
5473
+ headers: { "X-API-Key": answers.apiKey }
5474
+ });
5475
+ const body = await response.json();
5476
+ if (!response.ok) {
5477
+ return [
5478
+ {
5479
+ name: "self_hosted_contract",
5480
+ status: "warn",
5481
+ detail: `Could not verify /capabilities (HTTP ${response.status})`
5482
+ }
5483
+ ];
5484
+ }
5485
+ return [
5486
+ {
5487
+ name: "self_hosted_contract",
5488
+ status: body.contract === "snipara-server-oss-v2" ? "pass" : "warn",
5489
+ detail: body.contract === "snipara-server-oss-v2" ? "snipara-server-oss-v2 contract verified" : `Unexpected self-hosted contract${body.contract ? `: ${body.contract}` : ""}`
5490
+ }
5491
+ ];
5492
+ } catch {
5493
+ return [
5494
+ {
5495
+ name: "self_hosted_contract",
5496
+ status: "warn",
5497
+ detail: "Self-hosted /capabilities endpoint could not be verified"
5217
5498
  }
5218
5499
  ];
5219
5500
  }
@@ -5228,7 +5509,7 @@ function extractMcpToolNames(data) {
5228
5509
  ).filter(Boolean);
5229
5510
  }
5230
5511
  async function callHostedMcp(answers, method, params) {
5231
- const response = await fetch(`${API_URL}/mcp/${answers.projectSlug}`, {
5512
+ const response = await fetch(buildMcpEndpoint(answers), {
5232
5513
  method: "POST",
5233
5514
  headers: {
5234
5515
  "Content-Type": "application/json",
@@ -5300,8 +5581,24 @@ function printActivationSummary(manifest) {
5300
5581
  ` ${formatActivationStatus(manifest.lanes.connection.status)} Connected - ${manifest.lanes.connection.detail}`
5301
5582
  );
5302
5583
  console.log(` ${formatActivationStatus(contextLane.status)} Context - ${contextLane.detail}`);
5303
- console.log(` ${chalk.dim("\u25CB pending")} First grounded answer`);
5304
- console.log(chalk.yellow("\n Activation pending: first grounded answer needed."));
5584
+ if (manifest.valueProof.status === "ready") {
5585
+ console.log(
5586
+ ` ${chalk.green("\u2713 ready")} Value proof - ${manifest.valueProof.detail} (${manifest.valueProof.evidence || "evidence recorded"})`
5587
+ );
5588
+ console.log(chalk.green("\n Activation ready: First grounded answer verified."));
5589
+ } else if (manifest.valueProof.status === "failed") {
5590
+ console.log(
5591
+ ` ${chalk.red("\u2717 failed")} Value proof - ${manifest.valueProof.error || manifest.valueProof.detail}`
5592
+ );
5593
+ console.log(
5594
+ chalk.yellow(
5595
+ "\n Activation needs attention: run create-snipara doctor after checking the MCP endpoint."
5596
+ )
5597
+ );
5598
+ } else {
5599
+ console.log(` ${chalk.dim("\u25CB pending")} First grounded answer`);
5600
+ console.log(chalk.yellow("\n Activation pending: first grounded answer needed."));
5601
+ }
5305
5602
  console.log(
5306
5603
  chalk.dim(
5307
5604
  `
@@ -5425,6 +5722,7 @@ function detectEnvironment() {
5425
5722
  existingConfig: {
5426
5723
  slug: existingConfig?.slug,
5427
5724
  projectId: existingConfig?.projectId,
5725
+ serverUrl: existingConfig?.serverUrl,
5428
5726
  hasSniparaServer: existingConfig?.hasSniparaServer ?? false,
5429
5727
  hasRuntimeServer: existingConfig?.hasRuntimeServer ?? false
5430
5728
  }
@@ -5433,19 +5731,22 @@ function detectEnvironment() {
5433
5731
  function loadExistingAnswers(options) {
5434
5732
  const detection = detectEnvironment();
5435
5733
  const effectiveProfile = resolveMaintenanceProfile(detection, options.profile);
5734
+ const deployment = resolveRequestedDeployment(options);
5436
5735
  const providedApiKey = options.apiKey?.trim();
5437
- if (providedApiKey && !isManualApiKey(providedApiKey)) {
5438
- throw new Error(manualApiKeyValidationMessage());
5736
+ if (providedApiKey && !isValidApiKey(providedApiKey, deployment)) {
5737
+ throw new Error(manualApiKeyValidationMessage(deployment));
5439
5738
  }
5440
5739
  if (providedApiKey) {
5441
5740
  warnApiKeyCliExposure();
5442
5741
  }
5443
- const envApiKey = readApiKeyFromEnvFiles();
5742
+ const envApiKey = readApiKeyFromEnvFiles(deployment);
5444
5743
  const apiKey = providedApiKey || envApiKey || "YOUR_API_KEY_HERE";
5445
5744
  const apiKeyType = effectiveProfile === "runtime-only" ? "none" : providedApiKey || envApiKey ? "api" : "none";
5446
5745
  return buildAnswersFromProfile({
5447
5746
  projectSlug: options.slug || detection.projectSlug || "my-project",
5448
5747
  projectId: options.projectId || detection.existingConfig.projectId || "",
5748
+ deployment,
5749
+ serverUrl: options.serverUrl || process.env.SNIPARA_MCP_URL || getExistingConfig()?.serverUrl || "http://localhost:8000/mcp/local",
5449
5750
  apiKey,
5450
5751
  apiKeyType,
5451
5752
  profile: effectiveProfile,
@@ -5472,10 +5773,13 @@ function resolveMaintenanceProfile(detection, explicitProfile) {
5472
5773
  }
5473
5774
  return "hosted-only";
5474
5775
  }
5475
- function readApiKeyFromEnvFiles() {
5776
+ function readApiKeyFromEnvFiles(deployment = "hosted") {
5476
5777
  const candidates = [".env.local", ".env"];
5477
5778
  for (const file of candidates) {
5478
- const value = readEnvValueFromFile(file, ["SNIPARA_API_KEY"]);
5779
+ const value = readEnvValueFromFile(
5780
+ file,
5781
+ deployment === "self-hosted" ? ["SNIPARA_LOCAL_API_KEY", "SNIPARA_API_KEY"] : ["SNIPARA_API_KEY"]
5782
+ );
5479
5783
  if (value) {
5480
5784
  return value;
5481
5785
  }
@@ -5782,6 +6086,7 @@ function getExistingConfig() {
5782
6086
  return {
5783
6087
  slug: extractSlugFromMcpEndpoint(sniparaServer?.url) || void 0,
5784
6088
  projectId: readExistingCompanionProjectId(),
6089
+ serverUrl: typeof sniparaServer?.url === "string" ? normalizeMcpEndpoint(sniparaServer.url) : void 0,
5785
6090
  hasSniparaServer: Boolean(sniparaServer),
5786
6091
  hasRuntimeServer: Boolean(runtimeServer)
5787
6092
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-snipara",
3
- "version": "2.0.17",
4
- "description": "Onboard Snipara Hosted MCP Context + Memory with project bootstrap intelligence and optional local workflows",
3
+ "version": "2.1.0",
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",