create-snipara 1.3.8 → 1.3.9

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 +40 -6
  2. package/dist/index.js +200 -61
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -1,12 +1,18 @@
1
1
  # create-snipara
2
2
 
3
- One-command onboarding and maintenance CLI for **Snipara Hosted MCP Context + Memory** with optional `snipara-companion` and optional RLM-Runtime.
3
+ **One-command onboarding for Snipara Hosted MCP Context + Memory.**
4
+
5
+ `create-snipara` connects a local project to Snipara so AI agents can start
6
+ with project-owned context, reviewed memory, shared guidance, and optional code
7
+ impact tooling instead of starting cold every session.
4
8
 
5
9
  ```bash
6
10
  npx create-snipara
7
11
  ```
8
12
 
9
- `create-snipara` is the simple onboarding package for the agent path. It creates or reuses auth, writes Hosted MCP config, and prepares the local project so an LLM client can use Snipara context and reviewed memory.
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.
10
16
 
11
17
  Use it for:
12
18
 
@@ -15,6 +21,26 @@ Use it for:
15
21
  - local project wiring, `doctor`, `repair`, and `upgrade`
16
22
  - GitHub repository sync approval when the current folder is a GitHub repo
17
23
 
24
+ ## Why It Exists
25
+
26
+ AI clients all have slightly different config formats, but the project need is
27
+ the same: connect the agent to the same durable project memory. `create-snipara`
28
+ normalizes that setup.
29
+
30
+ ```mermaid
31
+ flowchart LR
32
+ Project["Local project"] --> CLI["npx create-snipara"]
33
+ CLI --> Auth["Snipara auth and project key"]
34
+ CLI --> MCP["Hosted MCP config"]
35
+ CLI --> Templates["AGENTS.md, CLAUDE.md, Cursor, Codex snippets"]
36
+ MCP --> Agents["Codex, Claude Code, Cursor, ChatGPT Desktop"]
37
+ Templates --> Agents
38
+ Agents --> Context["Snipara context and memory"]
39
+ ```
40
+
41
+ The CLI does not become the runtime brain. After setup, the agent talks to
42
+ Hosted MCP and keeps using its own LLM.
43
+
18
44
  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`.
19
45
 
20
46
  The product surfaces remain:
@@ -104,11 +130,18 @@ npx create-snipara
104
130
 
105
131
  The interactive flow now asks for:
106
132
 
107
- - install profile
108
133
  - project slug
109
- - optional project id
110
134
  - API key path: project key, team key, free signup, or add key later
111
135
  - AI client
136
+
137
+ By default, this stays on the recommended `hosted-only` setup. Add
138
+ `--advanced` when you want profile selection, project id entry, hook prompts,
139
+ GitHub automation prompts, or runtime provider prompts.
140
+
141
+ The advanced flow asks for:
142
+
143
+ - install profile
144
+ - optional project id
112
145
  - whether hooks should be generated
113
146
  - whether to open GitHub repository automation when a GitHub remote is detected
114
147
  - optional runtime provider config when `rlm-runtime` is installed
@@ -217,6 +250,7 @@ That keeps business truth review separate from agent onboarding.
217
250
  npx create-snipara --profile hosted-only
218
251
  npx create-snipara --profile hosted-companion
219
252
  npx create-snipara --profile full-stack
253
+ npx create-snipara --advanced
220
254
 
221
255
  # Runtime-only local execution
222
256
  npx create-snipara --runtime-only
@@ -329,7 +363,7 @@ Best when the user only wants Hosted MCP Context + Memory.
329
363
  ### Full local stack
330
364
 
331
365
  ```bash
332
- npx create-snipara --profile full-stack
366
+ npx create-snipara --profile full-stack --advanced
333
367
  ```
334
368
 
335
369
  Best when the user also wants `snipara-companion` and `rlm-runtime`.
@@ -377,7 +411,7 @@ npx create-snipara print-config --json
377
411
  When testing a locally packed tarball, use `npm exec --package` instead of `npx`, for example:
378
412
 
379
413
  ```bash
380
- npm exec --package ./create-snipara-1.3.8.tgz create-snipara -- --help
414
+ npm exec --package ./create-snipara-1.3.9.tgz create-snipara -- --help
381
415
  ```
382
416
 
383
417
  This avoids `npx` resolution issues with unpublished tarballs.
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import fs from "fs";
9
9
  import path from "path";
10
10
  import { execSync, spawn } from "child_process";
11
11
  import { homedir } from "os";
12
- var VERSION = "1.3.8";
12
+ var VERSION = "1.3.9";
13
13
  var BASE_URL = "https://www.snipara.com";
14
14
  var API_URL = "https://api.snipara.com";
15
15
  var COMPANION_PACKAGE_NAME = "snipara-companion";
@@ -27,7 +27,7 @@ function getCompanionInitClientFlag(aiClient) {
27
27
  function buildProgram() {
28
28
  const program = new Command();
29
29
  program.name("create-snipara").description("Scaffold hosted Snipara MCP config plus optional local helper workflows").version(VERSION);
30
- program.command("init").description("Install and configure Snipara for this project").option("--api-key <key>", "Snipara project API key").option("--team-key <key>", "Snipara team API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
30
+ program.command("init").description("Install and configure Snipara for this project").option("--api-key <key>", "Snipara project API key").option("--team-key <key>", "Snipara team API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option("--advanced", "Show advanced profile, project id, hooks, GitHub, and runtime prompts").option(
31
31
  "--profile <profile>",
32
32
  "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
33
33
  ).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip local snipara-mcp package installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").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("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting").action(async (options) => {
@@ -58,7 +58,7 @@ function buildProgram() {
58
58
  return program;
59
59
  }
60
60
  function addSharedRootOptions(program) {
61
- program.option("--api-key <key>", "Snipara project API key").option("--team-key <key>", "Snipara team API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
61
+ program.option("--api-key <key>", "Snipara project API key").option("--team-key <key>", "Snipara team API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option("--advanced", "Show advanced profile, project id, hooks, GitHub, and runtime prompts").option(
62
62
  "--profile <profile>",
63
63
  "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
64
64
  ).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip local snipara-mcp package installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").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("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting");
@@ -67,8 +67,63 @@ async function main() {
67
67
  const program = buildProgram();
68
68
  await program.parseAsync(process.argv);
69
69
  }
70
+ function normalizeInitOptions(options) {
71
+ const parsed = parseInitOptionsFromArgv(process.argv.slice(2));
72
+ const normalized = { ...parsed };
73
+ for (const [key, value] of Object.entries(options)) {
74
+ if (value !== void 0) {
75
+ normalized[key] = value;
76
+ }
77
+ }
78
+ return normalized;
79
+ }
80
+ function parseInitOptionsFromArgv(args) {
81
+ const parsed = {};
82
+ const valueFlags = {
83
+ "--api-key": "apiKey",
84
+ "--team-key": "teamKey",
85
+ "--slug": "slug",
86
+ "--project-id": "projectId",
87
+ "--profile": "profile",
88
+ "--github-path": "githubPath"
89
+ };
90
+ const booleanFlags = {
91
+ "--advanced": "advanced",
92
+ "--skip-companion": "skipCompanion",
93
+ "--skip-mcp": "skipMcp",
94
+ "--skip-runtime": "skipRuntime",
95
+ "--skip-hooks": "skipHooks",
96
+ "--skip-test": "skipTest",
97
+ "--runtime-only": "runtimeOnly",
98
+ "--github": "github",
99
+ "--skip-github": "skipGithub",
100
+ "--yes": "yes",
101
+ "-y": "yes"
102
+ };
103
+ for (let index = 0; index < args.length; index += 1) {
104
+ const arg = args[index];
105
+ if (arg === "init") {
106
+ continue;
107
+ }
108
+ const [flag, inlineValue] = arg.split("=", 2);
109
+ if (flag in valueFlags) {
110
+ const value = inlineValue ?? args[index + 1];
111
+ if (value && !value.startsWith("-")) {
112
+ parsed[valueFlags[flag]] = value;
113
+ if (inlineValue === void 0) {
114
+ index += 1;
115
+ }
116
+ }
117
+ continue;
118
+ }
119
+ if (arg in booleanFlags) {
120
+ parsed[booleanFlags[arg]] = true;
121
+ }
122
+ }
123
+ return parsed;
124
+ }
70
125
  async function runInit(options) {
71
- const answers = await gatherConfiguration(options);
126
+ const answers = await gatherConfiguration(normalizeInitOptions(options));
72
127
  if (answers.installMcp || answers.installRuntime) {
73
128
  await installPythonPackages(answers, false);
74
129
  }
@@ -198,6 +253,7 @@ async function gatherConfiguration(options) {
198
253
  const detectedClient = detectAiClient();
199
254
  const existingConfig = getExistingConfig();
200
255
  const detectedGitRepo = detectGitRepository();
256
+ const showAdvancedPrompts = Boolean(options.advanced);
201
257
  if (options.github && !detectedGitRepo) {
202
258
  console.log(
203
259
  chalk.yellow(
@@ -230,7 +286,7 @@ async function gatherConfiguration(options) {
230
286
  }
231
287
  const response = await prompts([
232
288
  {
233
- type: "select",
289
+ type: showAdvancedPrompts ? "select" : null,
234
290
  name: "profile",
235
291
  message: "Choose an install profile:",
236
292
  initial: installProfileChoices().findIndex((choice) => choice.value === initialProfile),
@@ -244,7 +300,7 @@ async function gatherConfiguration(options) {
244
300
  validate: (value) => value.length > 0 ? true : "Project slug is required (for example my-project)"
245
301
  },
246
302
  {
247
- type: "text",
303
+ type: showAdvancedPrompts ? "text" : null,
248
304
  name: "projectId",
249
305
  message: "Project ID (optional, press Enter to skip):",
250
306
  initial: options.projectId || detectedProjectId || ""
@@ -318,19 +374,19 @@ async function gatherConfiguration(options) {
318
374
  ]
319
375
  },
320
376
  {
321
- type: "confirm",
377
+ type: showAdvancedPrompts ? "confirm" : null,
322
378
  name: "installHooks",
323
379
  message: "Generate local hooks when supported?",
324
380
  initial: detectedClient === "claude-code"
325
381
  },
326
382
  {
327
- type: (_, values) => detectedGitRepo && !options.skipGithub && values.profile !== "runtime-only" ? "confirm" : null,
383
+ type: (_, values) => detectedGitRepo && showAdvancedPrompts && !options.skipGithub && (values.profile || initialProfile) !== "runtime-only" ? "confirm" : null,
328
384
  name: "configureGitHub",
329
385
  message: detectedGitRepo ? `Open GitHub automation setup for ${detectedGitRepo.fullName} after local config?` : "Open GitHub automation setup after local config?",
330
386
  initial: true
331
387
  },
332
388
  {
333
- type: (_, values) => values.configureGitHub && detectedGitRepo ? "text" : null,
389
+ type: (_, values) => showAdvancedPrompts && values.configureGitHub && detectedGitRepo ? "text" : null,
334
390
  name: "githubPath",
335
391
  message: "GitHub path filter (optional, press Enter for Snipara defaults):",
336
392
  initial: options.githubPath || ""
@@ -354,7 +410,7 @@ async function gatherConfiguration(options) {
354
410
  }
355
411
  const answers = buildAnswersFromProfile({
356
412
  projectSlug: signupResult?.projectSlug || response.projectSlug,
357
- projectId: signupResult?.projectId || response.projectId || "",
413
+ projectId: signupResult?.projectId || response.projectId || options.projectId || "",
358
414
  apiKey: apiKey || "",
359
415
  apiKeyType,
360
416
  profile: response.profile || initialProfile,
@@ -837,15 +893,16 @@ Agent task lifecycle:
837
893
 
838
894
  1. Start substantial work with \`rlm_recall\` and a targeted \`rlm_context_query\`.
839
895
  2. Use \`rlm_context_query\` for docs, business context, client/project truth, architecture notes, runbooks, and narrative source material.
840
- 3. Use \`rlm_get_chunk\` to load cited source sections returned by reference-based queries before relying on precise wording.
841
- 4. Use \`rlm_code_callers\`, \`rlm_code_imports\`, \`rlm_code_neighbors\`, or \`rlm_code_shortest_path\` for structural code questions.
842
- 5. On paid Context plans, use \`rlm_code_symbol_card\` for agent-ready symbol context and \`rlm_code_impact\` before risky multi-file changes or PR reviews.
843
- 6. Use \`rlm_plan\` or \`rlm_decompose\` only for complex work and only when the hosted server exposes them.
844
- 7. Use local file reads, \`rg\`, git commands, and test commands for exact edits, current working-tree state, and verification.
845
- 8. Use RLM Runtime only when sandboxed execution, repeatable validation, or isolated transformations materially help.
846
- 9. If a broad query is slow, retry once with a narrow keyword query before falling back to local search.
847
- 10. End substantial work with \`rlm_end_of_task_commit\` when available. Use \`rlm_remember_if_novel\` for one reusable memory while avoiding duplicates, and use \`rlm_remember\` for explicit direct memory writes.
848
- 11. Do not store secrets, tokens, passwords, private keys, raw logs, transient command output, or unreviewed guesses in memory.
896
+ 3. When \`rlm_context_query\` returns \`answer_pack\`, treat it as the first-pass response plan: use its source facts, caveats, source map, and verification checklist before drafting claims.
897
+ 4. Use \`rlm_get_chunk\` to load cited source sections returned by reference-based queries before relying on precise wording.
898
+ 5. Use \`rlm_code_callers\`, \`rlm_code_imports\`, \`rlm_code_neighbors\`, or \`rlm_code_shortest_path\` for structural code questions.
899
+ 6. On paid Context plans, use \`rlm_code_symbol_card\` for agent-ready symbol context and \`rlm_code_impact\` before risky multi-file changes or PR reviews.
900
+ 7. Use \`rlm_plan\` or \`rlm_decompose\` only for complex work and only when the hosted server exposes them.
901
+ 8. Use local file reads, \`rg\`, git commands, and test commands for exact edits, current working-tree state, and verification.
902
+ 9. Use RLM Runtime only when sandboxed execution, repeatable validation, or isolated transformations materially help.
903
+ 10. If a broad query is slow, retry once with a narrow keyword query before falling back to local search.
904
+ 11. End substantial work with \`rlm_end_of_task_commit\` when available. Use \`rlm_remember_if_novel\` for one reusable memory while avoiding duplicates, and use \`rlm_remember\` for explicit direct memory writes.
905
+ 12. Do not store secrets, tokens, passwords, private keys, raw logs, transient command output, or unreviewed guesses in memory.
849
906
  `;
850
907
  }
851
908
  function buildClaudeMdTemplate(answers) {
@@ -860,6 +917,7 @@ Use Snipara Hosted MCP before answering project-specific questions.
860
917
  - Endpoint: \`${buildHostedMcpEndpoint(answers)}\`
861
918
  - Start substantial work with \`rlm_recall\` and a targeted \`rlm_context_query\`.
862
919
  - Use \`rlm_context_query\` for source truth and \`rlm_get_chunk\` for exact cited sections.
920
+ - When \`rlm_context_query\` returns \`answer_pack\`, use its facts, caveats, source map, and verification checklist before drafting claims.
863
921
  - Use \`rlm_code_callers\`, \`rlm_code_imports\`, \`rlm_code_neighbors\`, or \`rlm_code_shortest_path\` for structural code questions.
864
922
  - On paid Context plans, use \`rlm_code_symbol_card\` before editing important symbols and \`rlm_code_impact\` before risky multi-file changes or PR reviews.
865
923
  - Use RLM Runtime only when sandboxed execution or repeatable validation materially helps.
@@ -882,6 +940,7 @@ This workspace uses Snipara Hosted MCP for current project context, business/cli
882
940
  - Endpoint: \`${buildHostedMcpEndpoint(answers)}\`
883
941
  - Use \`rlm_recall\` for durable decisions, preferences, and validated learnings.
884
942
  - Use \`rlm_context_query\` for docs, business context, current client/project truth, and narrative source material.
943
+ - When \`rlm_context_query\` returns \`answer_pack\`, use its facts, caveats, source map, and verification checklist before drafting claims.
885
944
  - Use \`rlm_get_chunk\` before relying on exact cited wording from reference-based results.
886
945
  - Use \`rlm_code_callers\`, \`rlm_code_imports\`, \`rlm_code_neighbors\`, or \`rlm_code_shortest_path\` for structural code questions.
887
946
  - On paid Context plans, use \`rlm_code_symbol_card\` before editing important symbols and \`rlm_code_impact\` before risky multi-file changes or PR reviews.
@@ -1149,7 +1208,7 @@ function getClientGuidance(client) {
1149
1208
  if (client === "codex") {
1150
1209
  return [
1151
1210
  "Codex should use the hosted MCP surface plus AGENTS.md as the canonical project instruction file.",
1152
- "The generated .snipara/templates/codex-config.toml can be merged into ~/.codex/config.toml."
1211
+ "The generated .snipara/templates/codex-config.toml is a merge-ready snippet for ~/.codex/config.toml if your Codex app does not read project .codex/config.toml."
1153
1212
  ];
1154
1213
  }
1155
1214
  if (client === "chatgpt") {
@@ -1449,7 +1508,7 @@ async function runValidation(answers, includeHostedCheck) {
1449
1508
  detail: answers.installHooks && answers.aiClient === "claude-code" ? detection.files.claudeDir ? "~/.claude detected" : "~/.claude missing" : "Hooks not requested"
1450
1509
  });
1451
1510
  if (includeHostedCheck && answers.apiKeyType !== "none" && !answers.apiKey.includes("YOUR_")) {
1452
- checks.push(await testConnection(answers));
1511
+ checks.push(...await testHostedConnection(answers));
1453
1512
  } else if (includeHostedCheck) {
1454
1513
  checks.push({
1455
1514
  name: "hosted_connection",
@@ -1488,48 +1547,119 @@ function printDoctorReport(detection, report) {
1488
1547
  }
1489
1548
  console.log("");
1490
1549
  }
1491
- async function testConnection(answers) {
1492
- const spinner = ora("Connecting to Snipara API...").start();
1550
+ async function testHostedConnection(answers) {
1551
+ const spinner = ora("Connecting to Snipara Hosted MCP...").start();
1493
1552
  try {
1494
- const response = await fetch(`${API_URL}/mcp/${answers.projectSlug}`, {
1495
- method: "POST",
1496
- headers: {
1497
- "Content-Type": "application/json",
1498
- "X-API-Key": answers.apiKey
1499
- },
1500
- body: JSON.stringify({
1501
- jsonrpc: "2.0",
1502
- id: 1,
1503
- method: "tools/list",
1504
- params: {}
1505
- })
1506
- });
1507
- if (response.ok) {
1508
- const data = await response.json();
1509
- const toolCount = data.result?.tools?.length || 0;
1510
- spinner.succeed(`Connected! ${toolCount} MCP tools available.`);
1511
- return {
1553
+ const toolsResponse = await callHostedMcp(answers, "tools/list", {});
1554
+ if (!toolsResponse.ok) {
1555
+ spinner.warn(`Hosted MCP returned HTTP ${toolsResponse.status}`);
1556
+ return [
1557
+ {
1558
+ name: "hosted_connection",
1559
+ status: toolsResponse.status === 401 || toolsResponse.status === 403 ? "fail" : "warn",
1560
+ detail: describeHostedHttpFailure(toolsResponse.status, toolsResponse.bodyText)
1561
+ }
1562
+ ];
1563
+ }
1564
+ if (toolsResponse.data.error) {
1565
+ spinner.warn("Hosted MCP tools/list returned a JSON-RPC error");
1566
+ return [
1567
+ {
1568
+ name: "hosted_connection",
1569
+ status: "warn",
1570
+ detail: describeJsonRpcError(toolsResponse.data)
1571
+ }
1572
+ ];
1573
+ }
1574
+ const toolCount = toolsResponse.data.result?.tools?.length || 0;
1575
+ spinner.succeed(`Connected! ${toolCount} MCP tools available.`);
1576
+ const checks = [
1577
+ {
1512
1578
  name: "hosted_connection",
1513
1579
  status: "pass",
1514
- detail: `Connected successfully (${toolCount} tools reported)`
1515
- };
1580
+ detail: `tools/list succeeded (${toolCount} tools reported)`
1581
+ }
1582
+ ];
1583
+ const settingsResponse = await callHostedMcp(answers, "tools/call", {
1584
+ name: "rlm_settings",
1585
+ arguments: {}
1586
+ });
1587
+ if (settingsResponse.ok && !settingsResponse.data.error) {
1588
+ checks.push({
1589
+ name: "hosted_settings",
1590
+ status: "pass",
1591
+ detail: "rlm_settings tool call succeeded"
1592
+ });
1593
+ } else {
1594
+ checks.push({
1595
+ name: "hosted_settings",
1596
+ status: "warn",
1597
+ detail: settingsResponse.ok ? describeJsonRpcError(settingsResponse.data) : describeHostedHttpFailure(settingsResponse.status, settingsResponse.bodyText)
1598
+ });
1516
1599
  }
1517
- const errorText = await response.text();
1518
- spinner.warn(`Connection returned ${response.status}`);
1519
- return {
1520
- name: "hosted_connection",
1521
- status: "warn",
1522
- detail: `HTTP ${response.status}: ${errorText.slice(0, 120)}`
1523
- };
1600
+ return checks;
1524
1601
  } catch (error) {
1525
1602
  spinner.warn(`Could not connect: ${error}`);
1526
- return {
1527
- name: "hosted_connection",
1528
- status: "warn",
1529
- detail: "Could not reach Snipara API from this machine"
1530
- };
1603
+ return [
1604
+ {
1605
+ name: "hosted_connection",
1606
+ status: "warn",
1607
+ detail: "Could not reach Snipara Hosted MCP from this machine"
1608
+ }
1609
+ ];
1531
1610
  }
1532
1611
  }
1612
+ async function callHostedMcp(answers, method, params) {
1613
+ const response = await fetch(`${API_URL}/mcp/${answers.projectSlug}`, {
1614
+ method: "POST",
1615
+ headers: {
1616
+ "Content-Type": "application/json",
1617
+ "X-API-Key": answers.apiKey
1618
+ },
1619
+ body: JSON.stringify({
1620
+ jsonrpc: "2.0",
1621
+ id: 1,
1622
+ method,
1623
+ params
1624
+ })
1625
+ });
1626
+ const bodyText = await response.text();
1627
+ let data = {};
1628
+ try {
1629
+ data = bodyText ? JSON.parse(bodyText) : {};
1630
+ } catch {
1631
+ data = {};
1632
+ }
1633
+ return {
1634
+ ok: response.ok,
1635
+ status: response.status,
1636
+ data,
1637
+ bodyText
1638
+ };
1639
+ }
1640
+ function describeHostedHttpFailure(status, bodyText) {
1641
+ const detail = bodyText.trim().slice(0, 120);
1642
+ if (status === 401 || status === 403) {
1643
+ return `Auth failed (${status}). Check the project/team API key and project slug.`;
1644
+ }
1645
+ if (status === 404) {
1646
+ return "Endpoint or project slug was not found. Check the hosted MCP URL.";
1647
+ }
1648
+ if (status === 429) {
1649
+ return "Rate limit reached. Retry later or check plan limits.";
1650
+ }
1651
+ if (status >= 500) {
1652
+ return `Hosted MCP server error (${status}). Retry later or check Snipara status.`;
1653
+ }
1654
+ return detail ? `HTTP ${status}: ${detail}` : `HTTP ${status}`;
1655
+ }
1656
+ function describeJsonRpcError(data) {
1657
+ if (!data.error) {
1658
+ return "Unexpected hosted MCP response";
1659
+ }
1660
+ const code = data.error.code !== void 0 ? ` ${data.error.code}` : "";
1661
+ return `JSON-RPC error${code}: ${data.error.message || "unknown error"}`;
1662
+ }
1533
1663
  function printSuccess(answers, report) {
1534
1664
  console.log(
1535
1665
  chalk.green(`
@@ -1572,6 +1702,11 @@ function printSuccess(answers, report) {
1572
1702
  nextStep += 1;
1573
1703
  };
1574
1704
  printNextStep("Restart your AI client if you created or changed hosted MCP config.");
1705
+ if (answers.aiClient === "codex") {
1706
+ printNextStep(
1707
+ "For Codex, merge .snipara/templates/codex-config.toml into ~/.codex/config.toml if the app does not read project .codex/config.toml."
1708
+ );
1709
+ }
1575
1710
  printNextStep("Open .snipara/companion/README.md for local commands and repair flow.");
1576
1711
  printNextStep("Run create-snipara doctor anytime you want a fresh local validation report.");
1577
1712
  if (answers.installCompanion) {
@@ -1697,7 +1832,7 @@ function resolveMaintenanceProfile(detection, explicitProfile) {
1697
1832
  if (detection.existingConfig.hasSniparaServer) {
1698
1833
  return "hosted-only";
1699
1834
  }
1700
- return detection.tooling.runtimeCli ? "runtime-only" : "hosted-only";
1835
+ return "hosted-only";
1701
1836
  }
1702
1837
  function readApiKeyFromEnvFiles() {
1703
1838
  const candidates = [".env.local", ".env"];
@@ -1705,7 +1840,7 @@ function readApiKeyFromEnvFiles() {
1705
1840
  const filePath = path.join(process.cwd(), file);
1706
1841
  if (!fs.existsSync(filePath)) continue;
1707
1842
  const content = fs.readFileSync(filePath, "utf-8");
1708
- const match = content.match(/^SNIPARA_API_KEY=(.+)$/m);
1843
+ const match = content.match(/^(?:SNIPARA_API_KEY|RLM_API_KEY)=(.+)$/m);
1709
1844
  if (match?.[1]) {
1710
1845
  return match[1].trim();
1711
1846
  }
@@ -1715,7 +1850,8 @@ function readApiKeyFromEnvFiles() {
1715
1850
  function detectProjectSlug() {
1716
1851
  try {
1717
1852
  const remote = execSync("git remote get-url origin", {
1718
- encoding: "utf-8"
1853
+ encoding: "utf-8",
1854
+ stdio: ["ignore", "pipe", "ignore"]
1719
1855
  }).trim();
1720
1856
  const match = remote.match(/[/:]([^/]+?)(?:\.git)?$/);
1721
1857
  if (match) {
@@ -1728,7 +1864,8 @@ function detectProjectSlug() {
1728
1864
  function detectGitRepository() {
1729
1865
  try {
1730
1866
  const remoteUrl = execSync("git remote get-url origin", {
1731
- encoding: "utf-8"
1867
+ encoding: "utf-8",
1868
+ stdio: ["ignore", "pipe", "ignore"]
1732
1869
  }).trim();
1733
1870
  const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/) || remoteUrl.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
1734
1871
  if (!match) {
@@ -1751,7 +1888,8 @@ function detectGitRepository() {
1751
1888
  function detectGitBranch() {
1752
1889
  try {
1753
1890
  const branch = execSync("git branch --show-current", {
1754
- encoding: "utf-8"
1891
+ encoding: "utf-8",
1892
+ stdio: ["ignore", "pipe", "ignore"]
1755
1893
  }).trim();
1756
1894
  if (branch) {
1757
1895
  return branch;
@@ -1760,7 +1898,8 @@ function detectGitBranch() {
1760
1898
  }
1761
1899
  try {
1762
1900
  const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1763
- encoding: "utf-8"
1901
+ encoding: "utf-8",
1902
+ stdio: ["ignore", "pipe", "ignore"]
1764
1903
  }).trim();
1765
1904
  return branch && branch !== "HEAD" ? branch : "main";
1766
1905
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-snipara",
3
- "version": "1.3.8",
3
+ "version": "1.3.9",
4
4
  "description": "Onboard Snipara Hosted MCP Context + Memory with auth plus optional local helper and runtime workflows",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
@@ -11,6 +11,8 @@
11
11
  "build": "tsup src/index.ts --format esm --dts --clean",
12
12
  "dev": "tsup src/index.ts --format esm --watch",
13
13
  "prepublishOnly": "npm run build",
14
+ "pretest": "pnpm build",
15
+ "test": "node --test test/*.test.cjs",
14
16
  "pack:smoke": "bash ../../scripts/smoke-node-package.sh packages/create-snipara create-snipara create-snipara --help",
15
17
  "type-check": "tsc --noEmit",
16
18
  "lint": "eslint src --ext .ts"