create-snipara 1.3.37 → 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 (2) hide show
  1. package/dist/index.js +109 -63
  2. package/package.json +1 -1
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.37",
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",
@@ -288,7 +288,8 @@ function parseInitOptionsFromArgv(args) {
288
288
  return parsed;
289
289
  }
290
290
  async function runInit(options) {
291
- const answers = await gatherConfiguration(normalizeInitOptions(options));
291
+ const normalizedOptions = normalizeInitOptions(options);
292
+ const answers = await gatherConfiguration(normalizedOptions);
292
293
  if (answers.installMcp || answers.installRuntime || answers.installOrchestrator) {
293
294
  await installPythonPackages(answers, false);
294
295
  }
@@ -308,9 +309,9 @@ async function runInit(options) {
308
309
  }
309
310
  await updateEnvFiles(answers);
310
311
  await createCompanionConfig(answers);
311
- await syncRemoteAutomationClient(answers, { enabled: !options.skipTest });
312
+ await syncRemoteAutomationClient(answers, { enabled: !normalizedOptions.skipTest });
312
313
  await createCompanionPack(answers);
313
- const validationReport = await runValidation(answers, !options.skipTest);
314
+ const validationReport = await runValidation(answers, !normalizedOptions.skipTest);
314
315
  writeDoctorReport(validationReport);
315
316
  printSuccess(answers, validationReport);
316
317
  if (answers.configureGitHub && answers.githubRepo) {
@@ -1137,31 +1138,10 @@ async function createClaudeCodeConfig(answers) {
1137
1138
  if (answers.profile === "runtime-only") {
1138
1139
  return;
1139
1140
  }
1140
- const claudeDir = path.join(homedir(), ".claude");
1141
- const settingsPath = path.join(claudeDir, "settings.json");
1142
- console.log(chalk.bold("\n\u{1F916} Configuring Claude Code MCP\n"));
1143
- try {
1144
- fs.mkdirSync(claudeDir, { recursive: true });
1145
- let settings = {};
1146
- if (fs.existsSync(settingsPath)) {
1147
- settings = asObjectRecord(JSON.parse(fs.readFileSync(settingsPath, "utf-8")));
1148
- }
1149
- const mcpServers = asObjectRecord(settings.mcpServers);
1150
- settings.mcpServers = {
1151
- ...mcpServers,
1152
- snipara: buildHostedMcpServer(answers)
1153
- };
1154
- fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", { mode: 384 });
1155
- try {
1156
- fs.chmodSync(settingsPath, 384);
1157
- } catch {
1158
- }
1159
- console.log(chalk.green(" \u2713 Updated ~/.claude/settings.json with Snipara MCP"));
1160
- } catch (error) {
1161
- const detail = error instanceof Error ? error.message : String(error);
1162
- console.log(chalk.yellow(` \u26A0\uFE0F Could not update Claude Code MCP config: ${detail}`));
1163
- console.log(chalk.dim(" Run create-snipara doctor --client claude after manual setup."));
1164
- }
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`));
1165
1145
  }
1166
1146
  async function createProjectBinding(answers) {
1167
1147
  if (answers.profile === "runtime-only") {
@@ -1469,14 +1449,8 @@ async function createClaudeDesktopConfig(answers) {
1469
1449
  }
1470
1450
  async function installClaudeCodeHooks(answers) {
1471
1451
  console.log(chalk.bold("\n\u{1FA9D} Installing Claude Code Hooks\n"));
1472
- const claudeDir = path.join(homedir(), ".claude");
1452
+ const claudeDir = path.join(process.cwd(), ".claude");
1473
1453
  const hooksDir = path.join(claudeDir, "hooks");
1474
- if (!fs.existsSync(claudeDir)) {
1475
- console.log(
1476
- chalk.yellow(" \u26A0\uFE0F ~/.claude not found. Install Claude Code first, then rerun repair.\n")
1477
- );
1478
- return;
1479
- }
1480
1454
  if (!fs.existsSync(hooksDir)) {
1481
1455
  fs.mkdirSync(hooksDir, { recursive: true });
1482
1456
  }
@@ -1513,41 +1487,88 @@ async function installClaudeCodeHooks(answers) {
1513
1487
  settings = {};
1514
1488
  }
1515
1489
  }
1516
- settings.hooks = settings.hooks || {};
1490
+ settings.hooks = asObjectRecord(settings.hooks);
1517
1491
  const hooksConfig = settings.hooks;
1518
1492
  const hookEntries = [
1519
1493
  {
1520
1494
  event: "PreToolUse",
1521
- script: path.join(hooksDir, "snipara-stuck-guard.sh"),
1522
- 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
+ }
1523
1505
  },
1524
1506
  {
1525
- event: "PreToolUse",
1526
- script: path.join(hooksDir, "snipara-startup.sh"),
1527
- 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
+ }
1528
1518
  },
1529
1519
  {
1530
1520
  event: "PostToolUse",
1531
- script: path.join(hooksDir, "snipara-session.sh"),
1532
- 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
+ }
1533
1531
  },
1534
1532
  {
1535
1533
  event: "PreCompact",
1536
- 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
+ }
1537
1544
  }
1538
1545
  ];
1539
- for (const entry of hookEntries) {
1540
- const eventHooks = hooksConfig[entry.event] || [];
1541
- const exists = eventHooks.some(
1542
- (hook) => typeof hook === "object" && hook !== null && "script" in hook && hook.script === entry.script
1543
- );
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));
1544
1550
  if (!exists) {
1545
1551
  eventHooks.push(entry);
1546
- hooksConfig[entry.event] = eventHooks;
1552
+ hooksConfig[event] = eventHooks;
1547
1553
  }
1548
1554
  }
1549
1555
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
1550
- 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
+ );
1551
1572
  }
1552
1573
  function createStuckGuardHook(answers) {
1553
1574
  return `#!/usr/bin/env bash
@@ -1978,7 +1999,7 @@ function getClientPackFilename(client) {
1978
1999
  }
1979
2000
  function getClientConfigArtifacts(client) {
1980
2001
  if (client === "claude-code") {
1981
- return ["AGENTS.md", "CLAUDE.md", "~/.claude/settings.json MCP server"];
2002
+ return ["AGENTS.md", "CLAUDE.md", ".mcp.json MCP server", ".claude/settings.json hooks"];
1982
2003
  }
1983
2004
  if (client === "cursor") {
1984
2005
  return [".mcp.json", ".cursor/rules/snipara.mdc", "dashboard automation bundle when installed"];
@@ -2237,11 +2258,15 @@ function getClaudeClientChecks(answers, detection) {
2237
2258
  const checks = [];
2238
2259
  const claudePaths = getClaudeConfigPaths();
2239
2260
  const projectConfig = readJsonFile(path.join(process.cwd(), ".mcp.json"));
2261
+ const projectSettings = readJsonFile(path.join(process.cwd(), ".claude", "settings.json"));
2240
2262
  const codeSettings = readJsonFile(claudePaths.codeSettingsPath);
2241
2263
  const desktopConfig = readJsonFile(claudePaths.desktopConfigPath);
2242
2264
  const projectHasSnipara = containsSniparaMcpReference(projectConfig);
2265
+ const projectSettingsHasSnipara = containsSniparaMcpReference(projectSettings);
2243
2266
  const codeSettingsHasSnipara = containsSniparaMcpReference(codeSettings);
2267
+ const hasLegacyCodeSettingsMcp = projectSettingsHasSnipara || codeSettingsHasSnipara;
2244
2268
  const desktopHasSnipara = containsSniparaMcpReference(desktopConfig);
2269
+ const projectHooksConfigured = containsClaudeHooksReference(projectSettings);
2245
2270
  checks.push({
2246
2271
  name: "claude_project_config",
2247
2272
  status: projectHasSnipara ? "pass" : "warn",
@@ -2250,8 +2275,13 @@ function getClaudeClientChecks(answers, detection) {
2250
2275
  if (answers.aiClient === "claude-code") {
2251
2276
  checks.push({
2252
2277
  name: "claude_code_settings",
2253
- status: detection.files.claudeSettings ? codeSettingsHasSnipara ? "pass" : "warn" : "warn",
2254
- 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"
2255
2285
  });
2256
2286
  }
2257
2287
  if (answers.aiClient === "claude-desktop") {
@@ -2298,6 +2328,15 @@ function containsSniparaMcpReference(value) {
2298
2328
  }
2299
2329
  return false;
2300
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
+ }
2301
2340
  function getCreateSniparaPackageFreshnessCheck() {
2302
2341
  if (!commandExists("npm")) {
2303
2342
  return {
@@ -2417,8 +2456,8 @@ async function runValidation(answers, includeHostedCheck, options = {}) {
2417
2456
  });
2418
2457
  checks.push({
2419
2458
  name: "hooks",
2420
- status: answers.installHooks && targetClient === "claude-code" ? detection.files.claudeDir ? "pass" : "warn" : "pass",
2421
- 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"
2422
2461
  });
2423
2462
  if (targetClient === "claude-code" || targetClient === "claude-desktop") {
2424
2463
  checks.push(...getClaudeClientChecks(effectiveAnswers, detection));
@@ -2679,7 +2718,7 @@ function printSuccess(answers, report) {
2679
2718
  }
2680
2719
  if (answers.aiClient === "claude-code" && answers.profile !== "runtime-only") {
2681
2720
  console.log(
2682
- 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")
2683
2722
  );
2684
2723
  }
2685
2724
  console.log(chalk.green(" \u2713 .snipara/companion") + chalk.dim(" - Local pack + doctor report"));
@@ -2799,6 +2838,8 @@ function detectEnvironment() {
2799
2838
  sniparaProject: fs.existsSync(path.join(process.cwd(), ".snipara", "project")),
2800
2839
  claudeDir: fs.existsSync(path.join(homedir(), ".claude")),
2801
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")),
2802
2843
  claudeDesktopConfig: fs.existsSync(claudePaths.desktopConfigPath),
2803
2844
  cursorDir: fs.existsSync(path.join(homedir(), ".cursor")),
2804
2845
  sniparaCompanionPack: fs.existsSync(path.join(process.cwd(), ".snipara", "companion")),
@@ -3024,7 +3065,8 @@ function detectRepoProjectSlug() {
3024
3065
  try {
3025
3066
  const remote = execSync("git remote get-url origin", {
3026
3067
  encoding: "utf-8",
3027
- stdio: ["ignore", "pipe", "ignore"]
3068
+ stdio: ["ignore", "pipe", "ignore"],
3069
+ timeout: 500
3028
3070
  }).trim();
3029
3071
  const match = remote.match(/[/:]([^/]+?)(?:\.git)?$/);
3030
3072
  if (match) {
@@ -3041,7 +3083,8 @@ function detectGitRepository() {
3041
3083
  try {
3042
3084
  const remoteUrl = execSync("git remote get-url origin", {
3043
3085
  encoding: "utf-8",
3044
- stdio: ["ignore", "pipe", "ignore"]
3086
+ stdio: ["ignore", "pipe", "ignore"],
3087
+ timeout: 500
3045
3088
  }).trim();
3046
3089
  const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/) || remoteUrl.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
3047
3090
  if (!match) {
@@ -3065,7 +3108,8 @@ function detectGitBranch() {
3065
3108
  try {
3066
3109
  const branch = execSync("git branch --show-current", {
3067
3110
  encoding: "utf-8",
3068
- stdio: ["ignore", "pipe", "ignore"]
3111
+ stdio: ["ignore", "pipe", "ignore"],
3112
+ timeout: 500
3069
3113
  }).trim();
3070
3114
  if (branch) {
3071
3115
  return branch;
@@ -3075,7 +3119,8 @@ function detectGitBranch() {
3075
3119
  try {
3076
3120
  const branch = execSync("git rev-parse --abbrev-ref HEAD", {
3077
3121
  encoding: "utf-8",
3078
- stdio: ["ignore", "pipe", "ignore"]
3122
+ stdio: ["ignore", "pipe", "ignore"],
3123
+ timeout: 500
3079
3124
  }).trim();
3080
3125
  return branch && branch !== "HEAD" ? branch : "main";
3081
3126
  } catch {
@@ -3085,7 +3130,8 @@ function detectGitBranch() {
3085
3130
  function isGitRepo() {
3086
3131
  try {
3087
3132
  execSync("git rev-parse --is-inside-work-tree", {
3088
- stdio: "ignore"
3133
+ stdio: "ignore",
3134
+ timeout: 500
3089
3135
  });
3090
3136
  return true;
3091
3137
  } catch {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-snipara",
3
- "version": "1.3.37",
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",