create-snipara 1.3.36 → 1.3.38

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 +6 -1
  2. package/dist/index.js +176 -90
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -106,7 +106,8 @@ That distinction matters:
106
106
  - installs optional `snipara-orchestrator` for proof gates, drift checks, and htask workflows when requested
107
107
  - configures `.mcp.json`
108
108
  - generates merge-ready AI agent templates in `.snipara/templates/`, including Pro+ symbol-card, code-impact, and managed workflow guidance for paid Context plans
109
- - creates `AGENTS.md`, `CLAUDE.md`, `.cursor/rules/snipara.mdc`, and `.codex/config.toml` when those files do not already exist
109
+ - creates `AGENTS.md` and `CLAUDE.md` when missing, or appends a marked Snipara section when those files already exist
110
+ - creates `.cursor/rules/snipara.mdc` and `.codex/config.toml` when those files do not already exist
110
111
  - writes client-specific companion packs such as `.snipara/companion/CODEX.md` or `.snipara/companion/GEMINI.md`
111
112
  - updates `.env`, `.env.local`, and `.env.example`
112
113
  - generates `.snipara/companion/README.md`
@@ -162,6 +163,10 @@ The generated agent templates cover:
162
163
  - ChatGPT/OpenAI HTTP MCP snippet
163
164
  - Gemini, VS Code, Continue, and custom HTTP MCP reference snippets
164
165
 
166
+ Existing root `AGENTS.md` and `CLAUDE.md` files are not replaced. The installer
167
+ adds or refreshes a marked Snipara section so project-specific instructions stay
168
+ intact.
169
+
165
170
  When `snipara-companion` is installed, the companion pack also includes the compaction-safe managed workflow:
166
171
 
167
172
  ```bash
package/dist/index.js CHANGED
@@ -13,7 +13,7 @@ import { homedir } from "os";
13
13
  // package.json
14
14
  var package_default = {
15
15
  name: "create-snipara",
16
- version: "1.3.36",
16
+ version: "1.3.38",
17
17
  description: "Onboard Snipara Hosted MCP Context + Memory with auth, companion workflows, and optional Snipara Sandbox",
18
18
  main: "dist/index.js",
19
19
  type: "module",
@@ -163,10 +163,19 @@ function getCompanionInitClientFlag(aiClient) {
163
163
  const companionClient = supportedClientMap[aiClient];
164
164
  return companionClient ? ` --client ${companionClient}` : "";
165
165
  }
166
+ function getAutomationClientId(aiClient) {
167
+ if (aiClient === "other" || aiClient === "claude-desktop") {
168
+ return "custom";
169
+ }
170
+ return aiClient;
171
+ }
166
172
  function buildProgram() {
167
173
  const program = new Command();
168
174
  program.name("create-snipara").description("Scaffold hosted Snipara MCP config plus companion workflow helpers").version(VERSION);
169
- 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("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option(
175
+ 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(
176
+ "--client <client>",
177
+ "AI client: claude-code, cursor, windsurf, codex, gemini, chatgpt, vscode, continue, claude-desktop, other"
178
+ ).option("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option(
170
179
  "--profile <profile>",
171
180
  "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
172
181
  ).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("--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) => {
@@ -200,7 +209,10 @@ function buildProgram() {
200
209
  return program;
201
210
  }
202
211
  function addSharedRootOptions(program) {
203
- program.option("--api-key <key>", "Snipara API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option(
212
+ program.option("--api-key <key>", "Snipara API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
213
+ "--client <client>",
214
+ "AI client: claude-code, cursor, windsurf, codex, gemini, chatgpt, vscode, continue, claude-desktop, other"
215
+ ).option("--advanced", "Show advanced profile, project id, hooks, GitHub, and sandbox prompts").option(
204
216
  "--profile <profile>",
205
217
  "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
206
218
  ).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("--runtime-only", "Install only Snipara Sandbox").addOption(new Option("--full", "Legacy alias for --profile full-stack").hideHelp()).option("-y, --yes", "Accept defaults without prompting");
@@ -233,6 +245,7 @@ function parseInitOptionsFromArgv(args) {
233
245
  "--api-key": "apiKey",
234
246
  "--slug": "slug",
235
247
  "--project-id": "projectId",
248
+ "--client": "client",
236
249
  "--profile": "profile",
237
250
  "--github-path": "githubPath"
238
251
  };
@@ -275,7 +288,8 @@ function parseInitOptionsFromArgv(args) {
275
288
  return parsed;
276
289
  }
277
290
  async function runInit(options) {
278
- const answers = await gatherConfiguration(normalizeInitOptions(options));
291
+ const normalizedOptions = normalizeInitOptions(options);
292
+ const answers = await gatherConfiguration(normalizedOptions);
279
293
  if (answers.installMcp || answers.installRuntime || answers.installOrchestrator) {
280
294
  await installPythonPackages(answers, false);
281
295
  }
@@ -295,8 +309,9 @@ async function runInit(options) {
295
309
  }
296
310
  await updateEnvFiles(answers);
297
311
  await createCompanionConfig(answers);
312
+ await syncRemoteAutomationClient(answers, { enabled: !normalizedOptions.skipTest });
298
313
  await createCompanionPack(answers);
299
- const validationReport = await runValidation(answers, !options.skipTest);
314
+ const validationReport = await runValidation(answers, !normalizedOptions.skipTest);
300
315
  writeDoctorReport(validationReport);
301
316
  printSuccess(answers, validationReport);
302
317
  if (answers.configureGitHub && answers.githubRepo) {
@@ -454,6 +469,7 @@ function printBanner() {
454
469
  async function gatherConfiguration(options) {
455
470
  console.log(chalk.bold("\n\u{1F4CB} Project Configuration\n"));
456
471
  const detectedClient = detectAiClient();
472
+ const requestedClient = normalizeClientOption(options.client);
457
473
  const existingConfig = getExistingConfig();
458
474
  const projectSlugResolution = resolveProjectSlug(existingConfig);
459
475
  let detectedSlug = projectSlugResolution.slug;
@@ -486,7 +502,7 @@ async function gatherConfiguration(options) {
486
502
  apiKey: providedApiKey || "YOUR_API_KEY_HERE",
487
503
  apiKeyType: providedApiKey ? "api" : "none",
488
504
  profile: initialProfile,
489
- aiClient: detectedClient,
505
+ aiClient: requestedClient || detectedClient,
490
506
  skipCompanion: !!options.skipCompanion,
491
507
  skipMcp: !!options.skipMcp,
492
508
  skipRuntime: !!options.skipRuntime,
@@ -553,7 +569,9 @@ async function gatherConfiguration(options) {
553
569
  message: "Which AI client are you using?",
554
570
  initial: Math.max(
555
571
  0,
556
- AI_CLIENT_CHOICES.findIndex((choice) => choice.value === detectedClient)
572
+ AI_CLIENT_CHOICES.findIndex(
573
+ (choice) => choice.value === (requestedClient || detectedClient)
574
+ )
557
575
  ),
558
576
  choices: AI_CLIENT_CHOICES
559
577
  },
@@ -604,7 +622,7 @@ async function gatherConfiguration(options) {
604
622
  apiKey: apiKey || "",
605
623
  apiKeyType,
606
624
  profile: response.profile || initialProfile,
607
- aiClient: response.aiClient || detectedClient,
625
+ aiClient: requestedClient || response.aiClient || detectedClient,
608
626
  skipCompanion: !!options.skipCompanion,
609
627
  skipMcp: !!options.skipMcp,
610
628
  skipRuntime: !!options.skipRuntime,
@@ -702,7 +720,8 @@ function normalizeClientOption(value) {
702
720
  chatgpt: "chatgpt",
703
721
  vscode: "vscode",
704
722
  continue: "continue",
705
- other: "other"
723
+ other: "other",
724
+ custom: "other"
706
725
  };
707
726
  const client = aliases[normalized];
708
727
  if (!client) {
@@ -870,10 +889,14 @@ async function runDeviceAuthorization() {
870
889
  spinner.fail("Authorization expired");
871
890
  throw new Error("device code expired");
872
891
  }
873
- async function postJson(url, body, allowErrorResponse = false) {
892
+ async function postJson(url, body, allowErrorResponse = false, init = {}) {
893
+ const headers = {
894
+ ...init.headers ?? {},
895
+ "Content-Type": "application/json"
896
+ };
874
897
  const response = await fetch(url, {
875
- method: "POST",
876
- headers: { "Content-Type": "application/json" },
898
+ method: init.method ?? "POST",
899
+ headers,
877
900
  body: JSON.stringify(body)
878
901
  });
879
902
  const data = await response.json().catch(() => ({}));
@@ -1115,31 +1138,10 @@ async function createClaudeCodeConfig(answers) {
1115
1138
  if (answers.profile === "runtime-only") {
1116
1139
  return;
1117
1140
  }
1118
- const claudeDir = path.join(homedir(), ".claude");
1119
- const settingsPath = path.join(claudeDir, "settings.json");
1120
- console.log(chalk.bold("\n\u{1F916} Configuring Claude Code MCP\n"));
1121
- try {
1122
- fs.mkdirSync(claudeDir, { recursive: true });
1123
- let settings = {};
1124
- if (fs.existsSync(settingsPath)) {
1125
- settings = asObjectRecord(JSON.parse(fs.readFileSync(settingsPath, "utf-8")));
1126
- }
1127
- const mcpServers = asObjectRecord(settings.mcpServers);
1128
- settings.mcpServers = {
1129
- ...mcpServers,
1130
- snipara: buildHostedMcpServer(answers)
1131
- };
1132
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 384 });
1133
- try {
1134
- fs.chmodSync(settingsPath, 384);
1135
- } catch {
1136
- }
1137
- console.log(chalk.green(" \u2713 Updated ~/.claude/settings.json with Snipara MCP"));
1138
- } catch (error) {
1139
- const detail = error instanceof Error ? error.message : String(error);
1140
- console.log(chalk.yellow(` \u26A0\uFE0F Could not update Claude Code MCP config: ${detail}`));
1141
- console.log(chalk.dim(" Run create-snipara doctor --client claude after manual setup."));
1142
- }
1141
+ console.log(chalk.bold("\n\u{1F916} Claude Code MCP\n"));
1142
+ console.log(chalk.green(" \u2713 Project .mcp.json contains the Snipara Hosted MCP server"));
1143
+ console.log(chalk.dim(" Claude Code loads project-scoped MCP servers from .mcp.json."));
1144
+ console.log(chalk.dim(` Verify with: claude mcp list && claude mcp test snipara`));
1143
1145
  }
1144
1146
  async function createProjectBinding(answers) {
1145
1147
  if (answers.profile === "runtime-only") {
@@ -1447,14 +1449,8 @@ async function createClaudeDesktopConfig(answers) {
1447
1449
  }
1448
1450
  async function installClaudeCodeHooks(answers) {
1449
1451
  console.log(chalk.bold("\n\u{1FA9D} Installing Claude Code Hooks\n"));
1450
- const claudeDir = path.join(homedir(), ".claude");
1452
+ const claudeDir = path.join(process.cwd(), ".claude");
1451
1453
  const hooksDir = path.join(claudeDir, "hooks");
1452
- if (!fs.existsSync(claudeDir)) {
1453
- console.log(
1454
- chalk.yellow(" \u26A0\uFE0F ~/.claude not found. Install Claude Code first, then rerun repair.\n")
1455
- );
1456
- return;
1457
- }
1458
1454
  if (!fs.existsSync(hooksDir)) {
1459
1455
  fs.mkdirSync(hooksDir, { recursive: true });
1460
1456
  }
@@ -1491,41 +1487,88 @@ async function installClaudeCodeHooks(answers) {
1491
1487
  settings = {};
1492
1488
  }
1493
1489
  }
1494
- settings.hooks = settings.hooks || {};
1490
+ settings.hooks = asObjectRecord(settings.hooks);
1495
1491
  const hooksConfig = settings.hooks;
1496
1492
  const hookEntries = [
1497
1493
  {
1498
1494
  event: "PreToolUse",
1499
- script: path.join(hooksDir, "snipara-stuck-guard.sh"),
1500
- matcher: { tool_name: "Bash|Read|Grep|Glob|Edit|MultiEdit|Write" }
1495
+ entry: {
1496
+ matcher: "Bash|Read|Grep|Glob|Edit|MultiEdit|Write",
1497
+ hooks: [
1498
+ {
1499
+ type: "command",
1500
+ command: createClaudeProjectHookCommand("snipara-stuck-guard.sh"),
1501
+ timeout: 10
1502
+ }
1503
+ ]
1504
+ }
1501
1505
  },
1502
1506
  {
1503
- event: "PreToolUse",
1504
- script: path.join(hooksDir, "snipara-startup.sh"),
1505
- matcher: { tool_name: "TodoWrite" }
1507
+ event: "SessionStart",
1508
+ entry: {
1509
+ matcher: "startup|resume|compact|clear",
1510
+ hooks: [
1511
+ {
1512
+ type: "command",
1513
+ command: createClaudeProjectHookCommand("snipara-startup.sh"),
1514
+ timeout: 30
1515
+ }
1516
+ ]
1517
+ }
1506
1518
  },
1507
1519
  {
1508
1520
  event: "PostToolUse",
1509
- script: path.join(hooksDir, "snipara-session.sh"),
1510
- matcher: { tool_name: "Bash|Read|Grep|Glob|Edit|MultiEdit|Write" }
1521
+ entry: {
1522
+ matcher: "Bash|Read|Grep|Glob|Edit|MultiEdit|Write",
1523
+ hooks: [
1524
+ {
1525
+ type: "command",
1526
+ command: createClaudeProjectHookCommand("snipara-session.sh"),
1527
+ timeout: 10
1528
+ }
1529
+ ]
1530
+ }
1511
1531
  },
1512
1532
  {
1513
1533
  event: "PreCompact",
1514
- script: path.join(hooksDir, "snipara-compact.sh")
1534
+ entry: {
1535
+ matcher: "manual|auto",
1536
+ hooks: [
1537
+ {
1538
+ type: "command",
1539
+ command: createClaudeProjectHookCommand("snipara-compact.sh"),
1540
+ timeout: 30
1541
+ }
1542
+ ]
1543
+ }
1515
1544
  }
1516
1545
  ];
1517
- for (const entry of hookEntries) {
1518
- const eventHooks = hooksConfig[entry.event] || [];
1519
- const exists = eventHooks.some(
1520
- (hook) => typeof hook === "object" && hook !== null && "script" in hook && hook.script === entry.script
1521
- );
1546
+ for (const { event, entry } of hookEntries) {
1547
+ const eventHooks = Array.isArray(hooksConfig[event]) ? hooksConfig[event] : [];
1548
+ const expectedCommand = entry.hooks[0].command;
1549
+ const exists = eventHooks.some((hook) => hookConfigEntryHasCommand(hook, expectedCommand));
1522
1550
  if (!exists) {
1523
1551
  eventHooks.push(entry);
1524
- hooksConfig[entry.event] = eventHooks;
1552
+ hooksConfig[event] = eventHooks;
1525
1553
  }
1526
1554
  }
1527
1555
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1528
- console.log(chalk.green(" \u2713 Updated ~/.claude/settings.json"));
1556
+ console.log(chalk.green(" \u2713 Updated .claude/settings.json hooks"));
1557
+ }
1558
+ function createClaudeProjectHookCommand(scriptName) {
1559
+ return `bash -lc 'cd "\${CLAUDE_PROJECT_DIR:-$PWD}" && ./.claude/hooks/${scriptName}'`;
1560
+ }
1561
+ function hookConfigEntryHasCommand(entry, expectedCommand) {
1562
+ if (!entry || typeof entry !== "object") {
1563
+ return false;
1564
+ }
1565
+ const hooks = entry.hooks;
1566
+ if (!Array.isArray(hooks)) {
1567
+ return false;
1568
+ }
1569
+ return hooks.some(
1570
+ (hook) => hook && typeof hook === "object" && hook.command === expectedCommand
1571
+ );
1529
1572
  }
1530
1573
  function createStuckGuardHook(answers) {
1531
1574
  return `#!/usr/bin/env bash
@@ -1679,31 +1722,49 @@ async function createCompanionConfig(answers) {
1679
1722
  }
1680
1723
  const configDir = path.join(process.cwd(), ".snipara", "companion");
1681
1724
  const configPath = path.join(configDir, "config.json");
1682
- const globalConfigPath = getGlobalCompanionConfigPath();
1683
1725
  fs.mkdirSync(configDir, { recursive: true });
1684
- fs.mkdirSync(path.dirname(globalConfigPath), { recursive: true });
1685
- const existingGlobalConfig = readCompanionConfigFile(globalConfigPath);
1686
1726
  const existingWorkspaceConfig = readCompanionConfigFile(configPath);
1687
- const globalConfig = {
1688
- ...existingGlobalConfig,
1689
- apiKey: answers.apiKey,
1690
- apiUrl: API_URL
1691
- };
1692
1727
  const workspaceConfig = {
1693
- projectId: answers.projectSlug,
1694
- sessionId: typeof existingWorkspaceConfig.sessionId === "string" && existingWorkspaceConfig.sessionId ? existingWorkspaceConfig.sessionId : `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`
1728
+ ...existingWorkspaceConfig,
1729
+ apiKey: answers.apiKey,
1730
+ apiUrl: API_URL,
1731
+ projectId: answers.projectId || answers.projectSlug,
1732
+ sessionId: typeof existingWorkspaceConfig.sessionId === "string" && existingWorkspaceConfig.sessionId ? existingWorkspaceConfig.sessionId : `sess_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
1733
+ client: getAutomationClientId(answers.aiClient)
1695
1734
  };
1696
- fs.writeFileSync(globalConfigPath, JSON.stringify(globalConfig, null, 2) + "\n");
1697
1735
  fs.writeFileSync(configPath, JSON.stringify(workspaceConfig, null, 2) + "\n");
1698
- ensureGitignoreEntry(
1699
- ".snipara/companion/config.json",
1700
- "# Snipara local companion workspace config"
1701
- );
1702
- console.log(chalk.green(" \u2713 Configured global companion auth"));
1703
- console.log(chalk.green(" \u2713 Configured local companion workspace"));
1736
+ ensureGitignoreEntry(".snipara/companion/config.json", "# Snipara local companion config");
1737
+ console.log(chalk.green(" \u2713 Configured local companion config"));
1704
1738
  }
1705
- function getGlobalCompanionConfigPath() {
1706
- return path.join(homedir(), ".rlmsaas", "config.json");
1739
+ async function syncRemoteAutomationClient(answers, options) {
1740
+ if (!options.enabled || answers.apiKeyType === "none" || !answers.apiKey || answers.apiKey.includes("YOUR_") || !answers.projectSlug) {
1741
+ return;
1742
+ }
1743
+ const automationClient = getAutomationClientId(answers.aiClient);
1744
+ const url = `${BASE_URL}/api/cli/projects/${encodeURIComponent(
1745
+ answers.projectSlug
1746
+ )}/automation-client`;
1747
+ try {
1748
+ const response = await postJson(
1749
+ url,
1750
+ { automationClient },
1751
+ false,
1752
+ {
1753
+ method: "PATCH",
1754
+ headers: { "X-API-Key": answers.apiKey }
1755
+ }
1756
+ );
1757
+ if (response.success === false) {
1758
+ throw new Error(response.message || "dashboard rejected automation client update");
1759
+ }
1760
+ console.log(chalk.green(` \u2713 Synced dashboard automation client: ${automationClient}`));
1761
+ } catch (error) {
1762
+ console.log(
1763
+ chalk.yellow(
1764
+ ` \u26A0\uFE0F Could not sync dashboard automation client: ${error instanceof Error ? error.message : "unknown error"}`
1765
+ )
1766
+ );
1767
+ }
1707
1768
  }
1708
1769
  function readCompanionConfigFile(filePath) {
1709
1770
  if (!fs.existsSync(filePath)) {
@@ -1938,7 +1999,7 @@ function getClientPackFilename(client) {
1938
1999
  }
1939
2000
  function getClientConfigArtifacts(client) {
1940
2001
  if (client === "claude-code") {
1941
- return ["AGENTS.md", "CLAUDE.md", "~/.claude/settings.json MCP server"];
2002
+ return ["AGENTS.md", "CLAUDE.md", ".mcp.json MCP server", ".claude/settings.json hooks"];
1942
2003
  }
1943
2004
  if (client === "cursor") {
1944
2005
  return [".mcp.json", ".cursor/rules/snipara.mdc", "dashboard automation bundle when installed"];
@@ -2197,11 +2258,15 @@ function getClaudeClientChecks(answers, detection) {
2197
2258
  const checks = [];
2198
2259
  const claudePaths = getClaudeConfigPaths();
2199
2260
  const projectConfig = readJsonFile(path.join(process.cwd(), ".mcp.json"));
2261
+ const projectSettings = readJsonFile(path.join(process.cwd(), ".claude", "settings.json"));
2200
2262
  const codeSettings = readJsonFile(claudePaths.codeSettingsPath);
2201
2263
  const desktopConfig = readJsonFile(claudePaths.desktopConfigPath);
2202
2264
  const projectHasSnipara = containsSniparaMcpReference(projectConfig);
2265
+ const projectSettingsHasSnipara = containsSniparaMcpReference(projectSettings);
2203
2266
  const codeSettingsHasSnipara = containsSniparaMcpReference(codeSettings);
2267
+ const hasLegacyCodeSettingsMcp = projectSettingsHasSnipara || codeSettingsHasSnipara;
2204
2268
  const desktopHasSnipara = containsSniparaMcpReference(desktopConfig);
2269
+ const projectHooksConfigured = containsClaudeHooksReference(projectSettings);
2205
2270
  checks.push({
2206
2271
  name: "claude_project_config",
2207
2272
  status: projectHasSnipara ? "pass" : "warn",
@@ -2210,8 +2275,13 @@ function getClaudeClientChecks(answers, detection) {
2210
2275
  if (answers.aiClient === "claude-code") {
2211
2276
  checks.push({
2212
2277
  name: "claude_code_settings",
2213
- status: detection.files.claudeSettings ? codeSettingsHasSnipara ? "pass" : "warn" : "warn",
2214
- detail: detection.files.claudeSettings ? codeSettingsHasSnipara ? "~/.claude/settings.json contains a Snipara MCP server" : "~/.claude/settings.json exists but no Snipara MCP server was found" : "~/.claude/settings.json not found; Claude may require Settings -> MCP UI setup"
2278
+ status: hasLegacyCodeSettingsMcp ? "warn" : "pass",
2279
+ detail: hasLegacyCodeSettingsMcp ? "Legacy Claude settings contain an MCP server entry; Claude Code project MCP should live in .mcp.json or be managed by claude mcp add --scope project" : "Claude settings are not being used as the MCP destination; .claude/settings.json is hooks-only"
2280
+ });
2281
+ checks.push({
2282
+ name: "claude_code_hooks",
2283
+ status: !answers.installHooks || projectHooksConfigured ? "pass" : "warn",
2284
+ detail: answers.installHooks ? projectHooksConfigured ? "Project .claude/settings.json contains Snipara hook commands" : "Project .claude/settings.json hooks missing; run create-snipara repair --with-hooks" : "Claude hooks not requested"
2215
2285
  });
2216
2286
  }
2217
2287
  if (answers.aiClient === "claude-desktop") {
@@ -2258,6 +2328,15 @@ function containsSniparaMcpReference(value) {
2258
2328
  }
2259
2329
  return false;
2260
2330
  }
2331
+ function containsClaudeHooksReference(value) {
2332
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
2333
+ return false;
2334
+ }
2335
+ const hooks = value.hooks;
2336
+ return Boolean(
2337
+ hooks && typeof hooks === "object" && JSON.stringify(hooks).includes(COMPANION_COMMAND_NAME)
2338
+ );
2339
+ }
2261
2340
  function getCreateSniparaPackageFreshnessCheck() {
2262
2341
  if (!commandExists("npm")) {
2263
2342
  return {
@@ -2377,8 +2456,8 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
2377
2456
  });
2378
2457
  checks.push({
2379
2458
  name: "hooks",
2380
- status: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeDir ? "pass" : "warn" : "pass",
2381
- detail: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeDir ? "~/.claude detected" : "~/.claude missing" : "Hooks not requested"
2459
+ status: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeProjectSettings ? "pass" : "warn" : "pass",
2460
+ detail: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeProjectSettings ? "Project .claude/settings.json hooks present" : "Project .claude/settings.json hooks missing" : "Hooks not requested"
2382
2461
  });
2383
2462
  if (targetClient === "claude-code" || targetClient === "claude-desktop") {
2384
2463
  checks.push(...getClaudeClientChecks(effectiveAnswers, detection));
@@ -2639,7 +2718,7 @@ function printSuccess(answers, report) {
2639
2718
  }
2640
2719
  if (answers.aiClient === "claude-code" && answers.profile !== "runtime-only") {
2641
2720
  console.log(
2642
- chalk.green(" \u2713 ~/.claude/settings.json") + chalk.dim(" - Claude Code Hosted MCP server")
2721
+ chalk.green(" \u2713 Claude Code") + chalk.dim(" - .mcp.json MCP config, .claude/settings.json hooks when enabled")
2643
2722
  );
2644
2723
  }
2645
2724
  console.log(chalk.green(" \u2713 .snipara/companion") + chalk.dim(" - Local pack + doctor report"));
@@ -2759,6 +2838,8 @@ function detectEnvironment() {
2759
2838
  sniparaProject: fs.existsSync(path.join(process.cwd(), ".snipara", "project")),
2760
2839
  claudeDir: fs.existsSync(path.join(homedir(), ".claude")),
2761
2840
  claudeSettings: fs.existsSync(claudePaths.codeSettingsPath),
2841
+ claudeProjectDir: fs.existsSync(path.join(process.cwd(), ".claude")),
2842
+ claudeProjectSettings: fs.existsSync(path.join(process.cwd(), ".claude", "settings.json")),
2762
2843
  claudeDesktopConfig: fs.existsSync(claudePaths.desktopConfigPath),
2763
2844
  cursorDir: fs.existsSync(path.join(homedir(), ".cursor")),
2764
2845
  sniparaCompanionPack: fs.existsSync(path.join(process.cwd(), ".snipara", "companion")),
@@ -2984,7 +3065,8 @@ function detectRepoProjectSlug() {
2984
3065
  try {
2985
3066
  const remote = execSync("git remote get-url origin", {
2986
3067
  encoding: "utf-8",
2987
- stdio: ["ignore", "pipe", "ignore"]
3068
+ stdio: ["ignore", "pipe", "ignore"],
3069
+ timeout: 500
2988
3070
  }).trim();
2989
3071
  const match = remote.match(/[/:]([^/]+?)(?:\.git)?$/);
2990
3072
  if (match) {
@@ -3001,7 +3083,8 @@ function detectGitRepository() {
3001
3083
  try {
3002
3084
  const remoteUrl = execSync("git remote get-url origin", {
3003
3085
  encoding: "utf-8",
3004
- stdio: ["ignore", "pipe", "ignore"]
3086
+ stdio: ["ignore", "pipe", "ignore"],
3087
+ timeout: 500
3005
3088
  }).trim();
3006
3089
  const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/) || remoteUrl.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
3007
3090
  if (!match) {
@@ -3025,7 +3108,8 @@ function detectGitBranch() {
3025
3108
  try {
3026
3109
  const branch = execSync("git branch --show-current", {
3027
3110
  encoding: "utf-8",
3028
- stdio: ["ignore", "pipe", "ignore"]
3111
+ stdio: ["ignore", "pipe", "ignore"],
3112
+ timeout: 500
3029
3113
  }).trim();
3030
3114
  if (branch) {
3031
3115
  return branch;
@@ -3035,7 +3119,8 @@ function detectGitBranch() {
3035
3119
  try {
3036
3120
  const branch = execSync("git rev-parse --abbrev-ref HEAD", {
3037
3121
  encoding: "utf-8",
3038
- stdio: ["ignore", "pipe", "ignore"]
3122
+ stdio: ["ignore", "pipe", "ignore"],
3123
+ timeout: 500
3039
3124
  }).trim();
3040
3125
  return branch && branch !== "HEAD" ? branch : "main";
3041
3126
  } catch {
@@ -3045,7 +3130,8 @@ function detectGitBranch() {
3045
3130
  function isGitRepo() {
3046
3131
  try {
3047
3132
  execSync("git rev-parse --is-inside-work-tree", {
3048
- stdio: "ignore"
3133
+ stdio: "ignore",
3134
+ timeout: 500
3049
3135
  });
3050
3136
  return true;
3051
3137
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-snipara",
3
- "version": "1.3.36",
3
+ "version": "1.3.38",
4
4
  "description": "Onboard Snipara Hosted MCP Context + Memory with auth, companion workflows, and optional Snipara Sandbox",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",