create-snipara 1.1.0 → 1.3.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 +148 -89
  2. package/dist/index.js +842 -297
  3. package/package.json +6 -4
package/dist/index.js CHANGED
@@ -1,34 +1,165 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { program } from "commander";
4
+ import { Command } from "commander";
5
5
  import prompts from "prompts";
6
6
  import chalk from "chalk";
7
7
  import ora from "ora";
8
8
  import fs from "fs";
9
9
  import path from "path";
10
- import { execSync } from "child_process";
10
+ import { execSync, spawn } from "child_process";
11
11
  import { homedir } from "os";
12
- var VERSION = "1.1.0";
12
+ var VERSION = "1.3.0";
13
13
  var BASE_URL = "https://www.snipara.com";
14
14
  var API_URL = "https://api.snipara.com";
15
+ var COMPANION_PACKAGE_NAME = "snipara-companion";
16
+ var MCP_PACKAGE_NAME = "snipara-mcp";
17
+ var RUNTIME_PACKAGE_NAME = "rlm-runtime[all]";
18
+ function getCompanionInitClientFlag(aiClient) {
19
+ if (aiClient === "cursor") {
20
+ return " --client cursor";
21
+ }
22
+ return aiClient === "claude-code" ? " --client claude-code" : "";
23
+ }
24
+ function buildProgram() {
25
+ const program = new Command();
26
+ program.name("create-snipara").description("Initialize and maintain Snipara MCP + local companion workflows").version(VERSION);
27
+ 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(
28
+ "--profile <profile>",
29
+ "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
30
+ ).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip snipara-mcp installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").option("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting").action(async (options) => {
31
+ printBanner();
32
+ await runInit(options);
33
+ });
34
+ program.command("doctor").description("Inspect the current project and write a validation report").option("--json", "Print the report as JSON").action(async (options) => {
35
+ printBanner();
36
+ await runDoctor(options);
37
+ });
38
+ program.command("repair").description("Rebuild local Snipara config and re-run validation").option("--api-key <key>", "Snipara project API key override").option("--team-key <key>", "Snipara team 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 runtime server/config").option("--with-mcp", "Force MCP config").option("--with-companion", "Force companion pack generation").action(async (options) => {
39
+ printBanner();
40
+ await runRepair(options);
41
+ });
42
+ program.command("upgrade").description("Upgrade installed Snipara local components").option("--with-hooks", "Refresh hooks after upgrade").option("--with-runtime", "Upgrade runtime even if not detected").option("--with-mcp", "Upgrade snipara-mcp even if not detected").option("--with-companion", "Upgrade companion even if not detected").action(async (options) => {
43
+ printBanner();
44
+ await runUpgrade(options);
45
+ });
46
+ program.command("print-config").description("Print detected local setup and inferred install profile").option("--json", "Print JSON instead of a formatted summary").action(async (options) => {
47
+ printBanner();
48
+ await runPrintConfig(options);
49
+ });
50
+ addSharedRootOptions(program);
51
+ program.action(async (options) => {
52
+ printBanner();
53
+ await runInit(options);
54
+ });
55
+ return program;
56
+ }
57
+ function addSharedRootOptions(program) {
58
+ 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(
59
+ "--profile <profile>",
60
+ "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
61
+ ).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip snipara-mcp installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").option("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting");
62
+ }
15
63
  async function main() {
16
- program.name("create-snipara").description("Initialize Snipara MCP + RLM-Runtime with a single command").version(VERSION).option("--api-key <key>", "Snipara API key (project or team)").option("--team-key <key>", "Team API key (access all team projects)").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID (optional, for advanced use)").option("--skip-mcp", "Skip snipara-mcp installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hooks installation").option("--skip-test", "Skip connection test").option("--runtime-only", "Install only RLM-Runtime (no API key needed for local execution)").option("-y, --yes", "Accept defaults without prompting").parse();
17
- const options = program.opts();
18
- printBanner();
64
+ const program = buildProgram();
65
+ await program.parseAsync(process.argv);
66
+ }
67
+ async function runInit(options) {
19
68
  const answers = await gatherConfiguration(options);
20
69
  if (answers.installMcp || answers.installRuntime) {
21
- await installPythonPackages(answers);
70
+ await installPythonPackages(answers, false);
71
+ }
72
+ if (answers.installCompanion) {
73
+ await installNodeCompanion(false);
74
+ }
75
+ if (answers.apiKeyType !== "none" || answers.installRuntime || answers.installMcp) {
76
+ await createMcpConfig(answers);
22
77
  }
23
- await createMcpConfig(answers);
24
78
  if (answers.installHooks && answers.aiClient === "claude-code") {
25
79
  await installClaudeCodeHooks(answers);
26
80
  }
27
81
  await updateEnvFiles(answers);
28
- if (!options.skipTest && answers.apiKey && !answers.apiKey.includes("YOUR_")) {
29
- await testConnection(answers);
82
+ await createCompanionPack(answers);
83
+ const validationReport = await runValidation(answers, !options.skipTest);
84
+ writeDoctorReport(validationReport);
85
+ printSuccess(answers, validationReport);
86
+ if (answers.runRlmInit && answers.installRuntime) {
87
+ await runRlmInit();
30
88
  }
31
- printSuccess(answers);
89
+ }
90
+ async function runDoctor(options) {
91
+ const answers = loadExistingAnswers(options);
92
+ const report = await runValidation(answers, true);
93
+ writeDoctorReport(report);
94
+ if (options.json) {
95
+ console.log(JSON.stringify({ detection: detectEnvironment(), validation: report }, null, 2));
96
+ return;
97
+ }
98
+ printDoctorReport(detectEnvironment(), report);
99
+ }
100
+ async function runRepair(options) {
101
+ const answers = loadExistingAnswers(options);
102
+ console.log(chalk.bold("\n\u{1F6E0}\uFE0F Repairing Local Setup\n"));
103
+ if (answers.installMcp || answers.installRuntime) {
104
+ await installPythonPackages(answers, true);
105
+ }
106
+ if (answers.installCompanion) {
107
+ await installNodeCompanion(true);
108
+ }
109
+ await createMcpConfig(answers);
110
+ await updateEnvFiles(answers);
111
+ await createCompanionPack(answers);
112
+ if (answers.installHooks && answers.aiClient === "claude-code") {
113
+ await installClaudeCodeHooks(answers);
114
+ }
115
+ const report = await runValidation(answers, true);
116
+ writeDoctorReport(report);
117
+ printDoctorReport(detectEnvironment(), report);
118
+ }
119
+ async function runUpgrade(options) {
120
+ const detection = detectEnvironment();
121
+ const answers = loadExistingAnswers(options);
122
+ console.log(chalk.bold("\n\u2B06\uFE0F Upgrading Local Components\n"));
123
+ if (options.withMcp || options.withRuntime || detection.existingConfig.hasRuntimeServer) {
124
+ await installPythonPackages(
125
+ {
126
+ ...answers,
127
+ installMcp: options.withMcp || answers.installMcp,
128
+ installRuntime: options.withRuntime || answers.installRuntime
129
+ },
130
+ true
131
+ );
132
+ }
133
+ if (options.withCompanion || detection.tooling.companionCli || answers.installCompanion) {
134
+ await installNodeCompanion(true);
135
+ }
136
+ if (options.withHooks || answers.installHooks) {
137
+ await installClaudeCodeHooks({ ...answers, installHooks: true });
138
+ }
139
+ await createCompanionPack(answers);
140
+ const report = await runValidation(answers, true);
141
+ writeDoctorReport(report);
142
+ printDoctorReport(detectEnvironment(), report);
143
+ }
144
+ async function runPrintConfig(options) {
145
+ const detection = detectEnvironment();
146
+ const answers = loadExistingAnswers(options);
147
+ if (options.json) {
148
+ console.log(JSON.stringify({ detection, inferredSetup: answers }, null, 2));
149
+ return;
150
+ }
151
+ console.log(chalk.bold("\n\u{1F4E6} Current Setup\n"));
152
+ console.log(` Project slug: ${answers.projectSlug}`);
153
+ console.log(` Project id: ${answers.projectId || "not-set"}`);
154
+ console.log(` Profile: ${answers.profile}`);
155
+ console.log(` AI client: ${answers.aiClient}`);
156
+ console.log(` Companion: ${answers.installCompanion ? "enabled" : "disabled"}`);
157
+ console.log(` Hosted MCP: ${answers.installMcp ? "enabled" : "disabled"}`);
158
+ console.log(` Runtime: ${answers.installRuntime ? "enabled" : "disabled"}`);
159
+ console.log(` Hooks: ${answers.installHooks ? "enabled" : "disabled"}`);
160
+ console.log(` Existing .mcp.json: ${detection.files.mcpConfig ? "yes" : "no"}`);
161
+ console.log(` Companion pack: ${detection.files.sniparaCompanionPack ? "yes" : "no"}`);
162
+ console.log("");
32
163
  }
33
164
  function printBanner() {
34
165
  console.log(
@@ -37,10 +168,10 @@ function printBanner() {
37
168
  \u2551 \u2551
38
169
  \u2551 ${chalk.bold("Snipara")} - Context Optimization + RLM-Runtime \u2551
39
170
  \u2551 \u2551
40
- \u2551 \u2022 90% context reduction for LLM queries \u2551
41
- \u2551 \u2022 Semantic memory across sessions \u2551
171
+ \u2551 \u2022 Hosted MCP, memory, and orchestration \u2551
172
+ \u2551 \u2022 Optional local companion workflows \u2551
42
173
  \u2551 \u2022 Safe code execution with Docker isolation \u2551
43
- \u2551 \u2022 Multi-agent coordination \u2551
174
+ \u2551 \u2022 One CLI for install, doctor, repair, and upgrade \u2551
44
175
  \u2551 \u2551
45
176
  \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
46
177
  `)
@@ -57,42 +188,35 @@ async function gatherConfiguration(options) {
57
188
  detectedSlug = existingConfig.slug || detectedSlug;
58
189
  detectedProjectId = existingConfig.projectId || "";
59
190
  }
60
- if (options.runtimeOnly) {
61
- console.log(chalk.cyan("\n \u2139\uFE0F Runtime-only mode: No API key needed for local code execution\n"));
62
- console.log(chalk.dim(" execute_python works without any API key when used in Claude Code."));
63
- console.log(chalk.dim(" For rlm run/agent CLI, you'll need OPENAI_API_KEY or ANTHROPIC_API_KEY.\n"));
64
- return {
65
- projectSlug: options.slug || detectedSlug || "my-project",
66
- projectId: "",
67
- apiKey: "",
68
- apiKeyType: "none",
69
- installMcp: false,
70
- installRuntime: true,
71
- installHooks: false,
72
- aiClient: detectedClient
73
- };
74
- }
191
+ const initialProfile = resolveRequestedProfile(options);
75
192
  if (options.yes) {
76
- const apiKey2 = options.teamKey || options.apiKey || "YOUR_API_KEY_HERE";
77
- const apiKeyType2 = options.teamKey ? "team" : "project";
78
- return {
193
+ return buildAnswersFromProfile({
79
194
  projectSlug: options.slug || detectedSlug || "my-project",
80
195
  projectId: options.projectId || detectedProjectId,
81
- apiKey: apiKey2,
82
- apiKeyType: apiKeyType2,
83
- installMcp: !options.skipMcp,
84
- installRuntime: !options.skipRuntime,
85
- installHooks: !options.skipHooks && detectedClient === "claude-code",
86
- aiClient: detectedClient
87
- };
196
+ apiKey: options.teamKey || options.apiKey || "YOUR_API_KEY_HERE",
197
+ apiKeyType: options.teamKey ? "team" : options.apiKey ? "project" : "none",
198
+ profile: initialProfile,
199
+ aiClient: detectedClient,
200
+ skipCompanion: !!options.skipCompanion,
201
+ skipMcp: !!options.skipMcp,
202
+ skipRuntime: !!options.skipRuntime,
203
+ skipHooks: !!options.skipHooks
204
+ });
88
205
  }
89
206
  const response = await prompts([
207
+ {
208
+ type: "select",
209
+ name: "profile",
210
+ message: "Choose an install profile:",
211
+ initial: installProfileChoices().findIndex((choice) => choice.value === initialProfile),
212
+ choices: installProfileChoices()
213
+ },
90
214
  {
91
215
  type: "text",
92
216
  name: "projectSlug",
93
217
  message: "Project slug:",
94
218
  initial: options.slug || detectedSlug || "",
95
- validate: (v) => v.length > 0 ? true : "Project slug is required (e.g., my-project)"
219
+ validate: (value) => value.length > 0 ? true : "Project slug is required (for example my-project)"
96
220
  },
97
221
  {
98
222
  type: "text",
@@ -101,7 +225,7 @@ async function gatherConfiguration(options) {
101
225
  initial: options.projectId || detectedProjectId || ""
102
226
  },
103
227
  {
104
- type: options.apiKey || options.teamKey ? null : "select",
228
+ type: (_, values) => values.profile === "runtime-only" || options.apiKey || options.teamKey ? null : "select",
105
229
  name: "apiKeyType",
106
230
  message: "What type of API key do you have?",
107
231
  choices: [
@@ -126,138 +250,194 @@ async function gatherConfiguration(options) {
126
250
  ]
127
251
  },
128
252
  {
129
- type: (prev, values) => values.apiKeyType === "project" || values.apiKeyType === "team" ? "text" : null,
253
+ type: (_, values) => values.profile === "runtime-only" ? null : values.apiKeyType === "project" || values.apiKeyType === "team" ? "text" : null,
130
254
  name: "apiKey",
131
- message: (prev, values) => values.apiKeyType === "team" ? "Enter your Team API key:" : "Enter your Project API key:",
132
- validate: (v) => v.startsWith("rlm_") ? true : "API key should start with rlm_"
255
+ message: (_, values) => values.apiKeyType === "team" ? "Enter your Team API key:" : "Enter your Project API key:",
256
+ validate: (value) => value.startsWith("rlm_") ? true : "API key should start with rlm_"
133
257
  },
134
258
  {
135
259
  type: "select",
136
260
  name: "aiClient",
137
261
  message: "Which AI client are you using?",
138
- initial: ["claude-code", "cursor", "claude-desktop", "other"].indexOf(
139
- detectedClient
140
- ),
262
+ initial: ["claude-code", "cursor", "claude-desktop", "other"].indexOf(detectedClient),
141
263
  choices: [
142
264
  {
143
265
  title: "Claude Code (CLI/VS Code extension)",
144
266
  value: "claude-code",
145
- description: "Full hooks support"
267
+ description: "Best hooks support"
146
268
  },
147
269
  {
148
270
  title: "Cursor",
149
271
  value: "cursor",
150
- description: "MCP support via .mcp.json"
272
+ description: "MCP via .mcp.json plus companion terminal workflows"
151
273
  },
152
274
  {
153
275
  title: "Claude Desktop",
154
276
  value: "claude-desktop",
155
- description: "MCP support via config file"
277
+ description: "Desktop MCP config flow"
156
278
  },
157
279
  { title: "Other", value: "other" }
158
280
  ]
159
281
  },
160
282
  {
161
- type: "multiselect",
162
- name: "packages",
163
- message: "What would you like to install?",
164
- choices: [
165
- {
166
- title: "snipara-mcp",
167
- value: "mcp",
168
- selected: !options.skipMcp,
169
- description: "MCP server for context queries (requires API key)"
170
- },
171
- {
172
- title: "rlm-runtime",
173
- value: "runtime",
174
- selected: !options.skipRuntime,
175
- description: "Safe code execution (no API key for execute_python)"
176
- },
177
- {
178
- title: "Claude Code hooks",
179
- value: "hooks",
180
- selected: !options.skipHooks,
181
- description: "Session memory & automation (Claude Code only)"
182
- }
183
- ],
184
- hint: "Space to toggle, Enter to confirm"
283
+ type: "confirm",
284
+ name: "installHooks",
285
+ message: "Generate local hooks when supported?",
286
+ initial: detectedClient === "claude-code"
185
287
  }
186
288
  ]);
187
289
  let apiKey = options.teamKey || options.apiKey;
188
- let apiKeyType = options.teamKey ? "team" : "project";
189
- if (!apiKey) {
290
+ let apiKeyType = response.profile === "runtime-only" ? "none" : options.teamKey ? "team" : options.apiKey ? "project" : "none";
291
+ if (!apiKey && response.profile !== "runtime-only") {
190
292
  if (response.apiKeyType === "signup") {
191
293
  apiKey = await handleSignup();
192
294
  apiKeyType = "project";
193
295
  } else if (response.apiKeyType === "project" || response.apiKeyType === "team") {
194
296
  apiKey = response.apiKey;
195
297
  apiKeyType = response.apiKeyType;
196
- } else if (response.apiKeyType === "skip") {
298
+ } else {
197
299
  apiKey = "YOUR_API_KEY_HERE";
198
300
  apiKeyType = "none";
199
301
  }
200
302
  }
201
- const packages = response.packages || [];
202
- let llmProvider;
203
- let llmApiKey;
204
- if (packages.includes("runtime")) {
205
- console.log(chalk.cyan("\n \u2139\uFE0F RLM-Runtime API Key Info:\n"));
206
- console.log(chalk.dim(" \u2022 execute_python MCP tool: No LLM API key needed"));
207
- console.log(chalk.dim(" (Your AI client - Claude, GPT, etc. - provides the LLM)"));
208
- console.log(chalk.dim(" \u2022 rlm run / rlm agent CLI: Requires an LLM API key\n"));
209
- const llmResponse = await prompts([
303
+ const answers = buildAnswersFromProfile({
304
+ projectSlug: response.projectSlug,
305
+ projectId: response.projectId || "",
306
+ apiKey: apiKey || "",
307
+ apiKeyType,
308
+ profile: response.profile || initialProfile,
309
+ aiClient: response.aiClient || detectedClient,
310
+ skipCompanion: !!options.skipCompanion,
311
+ skipMcp: !!options.skipMcp,
312
+ skipRuntime: !!options.skipRuntime,
313
+ skipHooks: !!options.skipHooks || !response.installHooks
314
+ });
315
+ if (answers.installRuntime) {
316
+ const runtimeResponse = await prompts([
210
317
  {
211
318
  type: "select",
212
319
  name: "llmProvider",
213
- message: "Will you use rlm run/agent CLI? (requires LLM API key)",
320
+ message: "Will you use rlm run/agent CLI too?",
214
321
  choices: [
215
322
  {
216
- title: "No - I'll only use execute_python in my AI client",
323
+ title: "No - execute_python only",
217
324
  value: "none",
218
- description: "No additional API key needed"
325
+ description: "No extra LLM API key needed"
219
326
  },
220
327
  {
221
- title: "Yes - OpenAI (GPT-4, etc.)",
328
+ title: "Yes - OpenAI",
222
329
  value: "openai",
223
330
  description: "Requires OPENAI_API_KEY"
224
331
  },
225
332
  {
226
- title: "Yes - Anthropic (Claude)",
333
+ title: "Yes - Anthropic",
227
334
  value: "anthropic",
228
335
  description: "Requires ANTHROPIC_API_KEY"
229
336
  }
230
337
  ]
231
338
  },
232
339
  {
233
- type: (prev) => prev !== "none" ? "text" : null,
340
+ type: (prev) => prev === "none" ? null : "text",
234
341
  name: "llmApiKey",
235
342
  message: (prev) => prev === "openai" ? "Enter your OpenAI API key (sk-...):" : "Enter your Anthropic API key (sk-ant-...):",
236
- validate: (v, values) => {
237
- if (values.llmProvider === "openai" && !v.startsWith("sk-")) {
343
+ validate: (value, values) => {
344
+ if (values.llmProvider === "openai" && !value.startsWith("sk-")) {
238
345
  return "OpenAI key should start with sk-";
239
346
  }
240
- if (values.llmProvider === "anthropic" && !v.startsWith("sk-ant-")) {
347
+ if (values.llmProvider === "anthropic" && !value.startsWith("sk-ant-")) {
241
348
  return "Anthropic key should start with sk-ant-";
242
349
  }
243
350
  return true;
244
351
  }
352
+ },
353
+ {
354
+ type: "confirm",
355
+ name: "runRlmInit",
356
+ message: "Run 'rlm init' now to choose sandbox/docker/local?",
357
+ initial: false
245
358
  }
246
359
  ]);
247
- llmProvider = llmResponse.llmProvider;
248
- llmApiKey = llmResponse.llmApiKey;
360
+ answers.llmProvider = runtimeResponse.llmProvider;
361
+ answers.llmApiKey = runtimeResponse.llmApiKey;
362
+ answers.runRlmInit = runtimeResponse.runRlmInit || false;
249
363
  }
364
+ return answers;
365
+ }
366
+ function resolveRequestedProfile(options) {
367
+ if ("runtimeOnly" in options && options.runtimeOnly) {
368
+ return "runtime-only";
369
+ }
370
+ if (options.profile && isInstallProfile(options.profile)) {
371
+ return options.profile;
372
+ }
373
+ return "hosted-companion";
374
+ }
375
+ function isInstallProfile(value) {
376
+ return ["hosted-only", "hosted-companion", "full-stack", "runtime-only"].includes(value);
377
+ }
378
+ function installProfileChoices() {
379
+ return [
380
+ {
381
+ title: "Hosted only",
382
+ value: "hosted-only",
383
+ description: "Snipara MCP only, no local companion or runtime"
384
+ },
385
+ {
386
+ title: "Hosted + Companion",
387
+ value: "hosted-companion",
388
+ description: "Recommended default: hosted MCP plus snipara-companion"
389
+ },
390
+ {
391
+ title: "Hosted + Companion + Runtime",
392
+ value: "full-stack",
393
+ description: "Add local runtime execution as well"
394
+ },
395
+ {
396
+ title: "Runtime only",
397
+ value: "runtime-only",
398
+ description: "Local execution only, no hosted API key required"
399
+ }
400
+ ];
401
+ }
402
+ function buildAnswersFromProfile(input) {
403
+ const defaults = {
404
+ "hosted-only": {
405
+ installCompanion: false,
406
+ installMcp: true,
407
+ installRuntime: false,
408
+ installHooks: false
409
+ },
410
+ "hosted-companion": {
411
+ installCompanion: true,
412
+ installMcp: true,
413
+ installRuntime: false,
414
+ installHooks: input.aiClient === "claude-code"
415
+ },
416
+ "full-stack": {
417
+ installCompanion: true,
418
+ installMcp: true,
419
+ installRuntime: true,
420
+ installHooks: input.aiClient === "claude-code"
421
+ },
422
+ "runtime-only": {
423
+ installCompanion: false,
424
+ installMcp: false,
425
+ installRuntime: true,
426
+ installHooks: false
427
+ }
428
+ };
429
+ const selected = defaults[input.profile];
250
430
  return {
251
- projectSlug: response.projectSlug,
252
- projectId: response.projectId || "",
253
- apiKey: apiKey || "YOUR_API_KEY_HERE",
254
- apiKeyType,
255
- installMcp: packages.includes("mcp"),
256
- installRuntime: packages.includes("runtime"),
257
- installHooks: packages.includes("hooks"),
258
- aiClient: response.aiClient || detectedClient,
259
- llmProvider,
260
- llmApiKey
431
+ projectSlug: input.projectSlug,
432
+ projectId: input.projectId,
433
+ apiKey: input.apiKey || "YOUR_API_KEY_HERE",
434
+ apiKeyType: input.profile === "runtime-only" ? "none" : input.apiKeyType,
435
+ profile: input.profile,
436
+ installCompanion: selected.installCompanion && !input.skipCompanion,
437
+ installMcp: selected.installMcp && !input.skipMcp,
438
+ installRuntime: selected.installRuntime && !input.skipRuntime,
439
+ installHooks: selected.installHooks && !input.skipHooks,
440
+ aiClient: input.aiClient
261
441
  };
262
442
  }
263
443
  async function handleSignup() {
@@ -266,67 +446,102 @@ async function handleSignup() {
266
446
  Opening ${signupUrl} in your browser...
267
447
  `));
268
448
  try {
269
- const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
270
- execSync(`${openCmd} "${signupUrl}"`, { stdio: "ignore" });
449
+ const platform = process.platform;
450
+ const openCommand = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
451
+ execSync(`${openCommand} "${signupUrl}"`, { stdio: "ignore", shell: "/bin/sh" });
271
452
  } catch {
272
- console.log(
273
- chalk.yellow(` Could not open browser. Please visit:
274
- ${signupUrl}
275
- `)
276
- );
453
+ console.log(chalk.dim(` Please open ${signupUrl} manually.
454
+ `));
277
455
  }
278
- console.log(
279
- chalk.dim(
280
- " After signing up, create a project and copy your API key.\n"
281
- )
282
- );
283
456
  const response = await prompts({
284
457
  type: "text",
285
- name: "key",
286
- message: "Paste your API key here:",
287
- validate: (v) => v.startsWith("rlm_") ? true : "API key should start with rlm_"
458
+ name: "apiKey",
459
+ message: "Paste your Project API key when ready:",
460
+ validate: (value) => value.startsWith("rlm_") ? true : "API key should start with rlm_"
288
461
  });
289
- return response.key;
462
+ return response.apiKey;
290
463
  }
291
- async function installPythonPackages(answers) {
292
- console.log(chalk.bold("\n\u{1F4E6} Installing Python Packages\n"));
464
+ async function installPythonPackages(answers, upgrade) {
465
+ console.log(chalk.bold(`
466
+ \u{1F4E6} ${upgrade ? "Upgrading" : "Installing"} Python Packages
467
+ `));
293
468
  const pipCmd = detectPipCommand();
294
469
  if (!pipCmd) {
295
- console.log(
296
- chalk.yellow(
297
- " \u26A0\uFE0F Python pip not found. Please install packages manually:\n"
298
- )
299
- );
470
+ console.log(chalk.yellow(" \u26A0\uFE0F Python pip not found. Please install packages manually:\n"));
300
471
  if (answers.installMcp) {
301
- console.log(chalk.gray(" pip install snipara-mcp"));
472
+ console.log(chalk.gray(` pip install ${upgrade ? "-U " : ""}${MCP_PACKAGE_NAME}`));
302
473
  }
303
474
  if (answers.installRuntime) {
304
- console.log(chalk.gray(" pip install rlm-runtime[all]"));
475
+ console.log(chalk.gray(` pip install ${upgrade ? "-U " : ""}${RUNTIME_PACKAGE_NAME}`));
305
476
  }
306
477
  console.log("");
307
478
  return;
308
479
  }
309
480
  const packages = [];
310
481
  if (answers.installMcp) {
311
- packages.push("snipara-mcp");
482
+ packages.push(MCP_PACKAGE_NAME);
312
483
  }
313
484
  if (answers.installRuntime) {
314
- packages.push("rlm-runtime[all]");
485
+ packages.push(RUNTIME_PACKAGE_NAME);
315
486
  }
316
487
  if (packages.length === 0) return;
317
- const spinner = ora(`Installing ${packages.join(", ")}...`).start();
488
+ const spinner = ora(
489
+ `${upgrade ? "Upgrading" : "Installing"} ${packages.map(stripExtras).join(", ")}...`
490
+ ).start();
318
491
  try {
319
- const installCmd = pipCmd === "uv" ? `uv pip install ${packages.join(" ")}` : `${pipCmd} install ${packages.join(" ")}`;
492
+ const installCmd = pipCmd === "uv" ? `uv pip install ${upgrade ? "--upgrade " : ""}${packages.join(" ")}` : `${pipCmd} install ${upgrade ? "--upgrade " : ""}${packages.join(" ")}`;
320
493
  execSync(installCmd, {
321
494
  stdio: "pipe",
322
495
  encoding: "utf-8"
323
496
  });
324
- spinner.succeed(`Installed ${packages.join(", ")}`);
325
- } catch (error) {
326
- spinner.fail(`Failed to install packages`);
497
+ spinner.succeed(
498
+ `${upgrade ? "Upgraded" : "Installed"} ${packages.map(stripExtras).join(", ")}`
499
+ );
500
+ } catch {
501
+ spinner.fail(`Failed to ${upgrade ? "upgrade" : "install"} Python packages`);
327
502
  console.log(chalk.yellow("\n Install manually with:"));
328
- console.log(chalk.gray(` pip install ${packages.join(" ")}
503
+ console.log(chalk.gray(` pip install ${upgrade ? "--upgrade " : ""}${packages.join(" ")}
504
+ `));
505
+ }
506
+ }
507
+ function stripExtras(name) {
508
+ return name.replace(/\[.*\]$/, "");
509
+ }
510
+ function detectNodePackageManager() {
511
+ if (commandExists("pnpm")) return "pnpm";
512
+ if (commandExists("npm")) return "npm";
513
+ return null;
514
+ }
515
+ async function installNodeCompanion(upgrade) {
516
+ console.log(chalk.bold(`
517
+ \u{1F9F0} ${upgrade ? "Upgrading" : "Installing"} Local Companion CLI
329
518
  `));
519
+ const packageManager = detectNodePackageManager();
520
+ if (!packageManager) {
521
+ console.log(chalk.yellow(" \u26A0\uFE0F npm/pnpm not found. Please install manually:\n"));
522
+ console.log(chalk.gray(` npm install -g ${COMPANION_PACKAGE_NAME}
523
+ `));
524
+ return;
525
+ }
526
+ const spinner = ora(
527
+ `${upgrade ? "Upgrading" : "Installing"} ${COMPANION_PACKAGE_NAME}...`
528
+ ).start();
529
+ try {
530
+ const installCmd = packageManager === "pnpm" ? `pnpm add -g ${upgrade ? "--latest " : ""}${COMPANION_PACKAGE_NAME}` : `npm install -g ${upgrade ? COMPANION_PACKAGE_NAME + "@latest" : COMPANION_PACKAGE_NAME}`;
531
+ execSync(installCmd, {
532
+ stdio: "pipe",
533
+ encoding: "utf-8"
534
+ });
535
+ spinner.succeed(`${upgrade ? "Upgraded" : "Installed"} ${COMPANION_PACKAGE_NAME}`);
536
+ } catch {
537
+ spinner.fail(`Failed to ${upgrade ? "upgrade" : "install"} ${COMPANION_PACKAGE_NAME}`);
538
+ console.log(chalk.yellow("\n Install manually with:"));
539
+ console.log(
540
+ chalk.gray(
541
+ ` npm install -g ${upgrade ? COMPANION_PACKAGE_NAME + "@latest" : COMPANION_PACKAGE_NAME}
542
+ `
543
+ )
544
+ );
330
545
  }
331
546
  }
332
547
  async function createMcpConfig(answers) {
@@ -338,18 +553,18 @@ async function createMcpConfig(answers) {
338
553
  mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
339
554
  console.log(chalk.dim(" Found existing .mcp.json, merging..."));
340
555
  } catch {
341
- console.log(
342
- chalk.yellow(" Could not parse existing .mcp.json, creating new...")
343
- );
556
+ console.log(chalk.yellow(" Could not parse existing .mcp.json, creating new..."));
344
557
  }
345
558
  }
346
- mcpConfig.mcpServers["snipara"] = {
347
- type: "http",
348
- url: `${API_URL}/mcp/${answers.projectSlug}`,
349
- headers: {
350
- "X-API-Key": answers.apiKey
351
- }
352
- };
559
+ if (answers.apiKeyType !== "none" || answers.installMcp) {
560
+ mcpConfig.mcpServers.snipara = {
561
+ type: "http",
562
+ url: `${API_URL}/mcp/${answers.projectSlug}`,
563
+ headers: {
564
+ "X-API-Key": answers.apiKey || "YOUR_API_KEY_HERE"
565
+ }
566
+ };
567
+ }
353
568
  if (answers.installRuntime) {
354
569
  mcpConfig.mcpServers["rlm-runtime"] = {
355
570
  type: "http",
@@ -359,26 +574,28 @@ async function createMcpConfig(answers) {
359
574
  }
360
575
  fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
361
576
  console.log(chalk.green(" \u2713 Created .mcp.json"));
577
+ ensureGitignoreEntry(".mcp.json", "# Snipara MCP config (contains API key)");
578
+ if (answers.aiClient === "claude-desktop") {
579
+ await createClaudeDesktopConfig(answers);
580
+ }
581
+ }
582
+ function ensureGitignoreEntry(entry, comment) {
362
583
  const gitignorePath = path.join(process.cwd(), ".gitignore");
363
584
  if (fs.existsSync(gitignorePath)) {
364
585
  const gitignore = fs.readFileSync(gitignorePath, "utf-8");
365
- if (!gitignore.includes(".mcp.json")) {
366
- fs.appendFileSync(
367
- gitignorePath,
368
- "\n# Snipara MCP config (contains API key)\n.mcp.json\n"
369
- );
370
- console.log(chalk.green(" \u2713 Added .mcp.json to .gitignore"));
586
+ if (!gitignore.includes(entry)) {
587
+ fs.appendFileSync(gitignorePath, `
588
+ ${comment}
589
+ ${entry}
590
+ `);
591
+ console.log(chalk.green(` \u2713 Added ${entry} to .gitignore`));
371
592
  }
372
- } else {
373
- fs.writeFileSync(
374
- gitignorePath,
375
- "# Snipara MCP config (contains API key)\n.mcp.json\n"
376
- );
377
- console.log(chalk.green(" \u2713 Created .gitignore with .mcp.json"));
378
- }
379
- if (answers.aiClient === "claude-desktop") {
380
- await createClaudeDesktopConfig(answers);
593
+ return;
381
594
  }
595
+ fs.writeFileSync(gitignorePath, `${comment}
596
+ ${entry}
597
+ `);
598
+ console.log(chalk.green(` \u2713 Created .gitignore with ${entry}`));
382
599
  }
383
600
  async function createClaudeDesktopConfig(answers) {
384
601
  const configDir = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path.join(homedir(), "AppData", "Roaming", "Claude") : path.join(homedir(), ".config", "claude");
@@ -394,9 +611,9 @@ async function createClaudeDesktopConfig(answers) {
394
611
  config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
395
612
  }
396
613
  config.mcpServers = config.mcpServers || {};
397
- config.mcpServers["snipara"] = {
614
+ config.mcpServers.snipara = {
398
615
  command: "uvx",
399
- args: ["snipara-mcp"],
616
+ args: [MCP_PACKAGE_NAME],
400
617
  env: {
401
618
  SNIPARA_API_KEY: answers.apiKey,
402
619
  SNIPARA_PROJECT_SLUG: answers.projectSlug
@@ -415,9 +632,7 @@ async function createClaudeDesktopConfig(answers) {
415
632
  fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
416
633
  console.log(chalk.green(" \u2713 Updated Claude Desktop config"));
417
634
  } catch (error) {
418
- console.log(
419
- chalk.yellow(` \u26A0\uFE0F Could not update Claude Desktop config: ${error}`)
420
- );
635
+ console.log(chalk.yellow(` \u26A0\uFE0F Could not update Claude Desktop config: ${error}`));
421
636
  }
422
637
  }
423
638
  async function installClaudeCodeHooks(answers) {
@@ -426,11 +641,8 @@ async function installClaudeCodeHooks(answers) {
426
641
  const hooksDir = path.join(claudeDir, "hooks");
427
642
  if (!fs.existsSync(claudeDir)) {
428
643
  console.log(
429
- chalk.yellow(
430
- " \u26A0\uFE0F ~/.claude not found. Install Claude Code first, then run:\n"
431
- )
644
+ chalk.yellow(" \u26A0\uFE0F ~/.claude not found. Install Claude Code first, then rerun repair.\n")
432
645
  );
433
- console.log(chalk.gray(" create-snipara --skip-mcp --skip-runtime\n"));
434
646
  return;
435
647
  }
436
648
  if (!fs.existsSync(hooksDir)) {
@@ -439,17 +651,14 @@ async function installClaudeCodeHooks(answers) {
439
651
  const hooks = [
440
652
  {
441
653
  name: "snipara-startup.sh",
442
- event: "PreToolUse",
443
654
  content: createStartupHook(answers)
444
655
  },
445
656
  {
446
657
  name: "snipara-session.sh",
447
- event: "PostToolUse",
448
658
  content: createSessionHook(answers)
449
659
  },
450
660
  {
451
661
  name: "snipara-compact.sh",
452
- event: "PreCompact",
453
662
  content: createCompactHook(answers)
454
663
  }
455
664
  ];
@@ -465,6 +674,7 @@ async function installClaudeCodeHooks(answers) {
465
674
  try {
466
675
  settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
467
676
  } catch {
677
+ settings = {};
468
678
  }
469
679
  }
470
680
  settings.hooks = settings.hooks || {};
@@ -488,75 +698,50 @@ async function installClaudeCodeHooks(answers) {
488
698
  for (const entry of hookEntries) {
489
699
  const eventHooks = hooksConfig[entry.event] || [];
490
700
  const exists = eventHooks.some(
491
- (h) => typeof h === "object" && h !== null && "script" in h && h.script === entry.script
701
+ (hook) => typeof hook === "object" && hook !== null && "script" in hook && hook.script === entry.script
492
702
  );
493
703
  if (!exists) {
494
704
  eventHooks.push(entry);
495
705
  hooksConfig[entry.event] = eventHooks;
496
706
  }
497
707
  }
498
- settings.hooks = hooksConfig;
499
708
  fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
500
709
  console.log(chalk.green(" \u2713 Updated ~/.claude/settings.json"));
501
710
  }
502
711
  function createStartupHook(answers) {
503
- return `#!/bin/bash
504
- # Snipara Startup Hook - Restores session context
505
- # Event: PreToolUse (TodoWrite)
712
+ return `#!/usr/bin/env bash
713
+ set -euo pipefail
506
714
 
507
- export SNIPARA_API_KEY="${answers.apiKey}"
508
715
  export SNIPARA_PROJECT_SLUG="${answers.projectSlug}"
716
+ ${answers.projectId ? `export SNIPARA_PROJECT_ID="${answers.projectId}"` : ""}
509
717
 
510
- # Recall recent context on first TodoWrite
511
- if [ -z "$SNIPARA_SESSION_STARTED" ]; then
512
- export SNIPARA_SESSION_STARTED=1
513
-
514
- # Use snipara-mcp to recall context
515
- if command -v snipara-mcp &> /dev/null; then
516
- snipara-mcp recall --query "session progress recent work" --limit 3 2>/dev/null || true
517
- fi
718
+ if command -v rlm-hook >/dev/null 2>&1; then
719
+ rlm-hook session-bootstrap || true
518
720
  fi
519
-
520
- exit 0
521
721
  `;
522
722
  }
523
723
  function createSessionHook(answers) {
524
- return `#!/bin/bash
525
- # Snipara Session Hook - Auto-remembers significant commits
526
- # Event: PostToolUse (Bash)
724
+ return `#!/usr/bin/env bash
725
+ set -euo pipefail
527
726
 
528
- export SNIPARA_API_KEY="${answers.apiKey}"
529
727
  export SNIPARA_PROJECT_SLUG="${answers.projectSlug}"
728
+ ${answers.projectId ? `export SNIPARA_PROJECT_ID="${answers.projectId}"` : ""}
530
729
 
531
- # Check if this was a git commit
532
- if echo "$TOOL_INPUT" | grep -q "git commit"; then
533
- COMMIT_MSG=$(git log -1 --pretty=%B 2>/dev/null || echo "")
534
-
535
- if [ -n "$COMMIT_MSG" ]; then
536
- if command -v snipara-mcp &> /dev/null; then
537
- snipara-mcp remember --type "context" --content "Committed: $COMMIT_MSG" --ttl 7 2>/dev/null || true
538
- fi
539
- fi
730
+ if command -v rlm-hook >/dev/null 2>&1; then
731
+ rlm-hook emit-event --event-type tool_result --json '{"source":"claude-code-hook"}' || true
540
732
  fi
541
-
542
- exit 0
543
733
  `;
544
734
  }
545
735
  function createCompactHook(answers) {
546
- return `#!/bin/bash
547
- # Snipara Compact Hook - Saves session context before compaction
548
- # Event: PreCompact
736
+ return `#!/usr/bin/env bash
737
+ set -euo pipefail
549
738
 
550
- export SNIPARA_API_KEY="${answers.apiKey}"
551
739
  export SNIPARA_PROJECT_SLUG="${answers.projectSlug}"
740
+ ${answers.projectId ? `export SNIPARA_PROJECT_ID="${answers.projectId}"` : ""}
552
741
 
553
- # Save session context before compaction
554
- if command -v snipara-mcp &> /dev/null; then
555
- # Extract key context from conversation (simplified)
556
- snipara-mcp remember --type "context" --content "Session compacted. Resume point saved." --ttl 7 2>/dev/null || true
742
+ if command -v rlm-hook >/dev/null 2>&1; then
743
+ rlm-hook task-commit --summary "Claude Code compacted local session context" --outcome partial || true
557
744
  fi
558
-
559
- exit 0
560
745
  `;
561
746
  }
562
747
  async function updateEnvFiles(answers) {
@@ -587,7 +772,7 @@ ${llmVarName}=${answers.llmApiKey}`;
587
772
  }
588
773
  if (fs.existsSync(envExamplePath)) {
589
774
  const content = fs.readFileSync(envExamplePath, "utf-8");
590
- if (!content.includes("SNIPARA_API_KEY")) {
775
+ if (!content.includes("SNIPARA_API_KEY") && !content.includes("SNIPARA_PROJECT_SLUG")) {
591
776
  fs.appendFileSync(envExamplePath, `
592
777
 
593
778
  ${envVars}
@@ -608,10 +793,262 @@ ${envVars}
608
793
  `);
609
794
  console.log(chalk.green(` \u2713 Updated ${path.basename(targetEnvPath)}`));
610
795
  }
796
+ return;
797
+ }
798
+ if (!answers.apiKey.includes("YOUR_") || answers.llmApiKey) {
799
+ fs.writeFileSync(targetEnvPath, envVars + "\n");
800
+ console.log(chalk.green(` \u2713 Created ${path.basename(targetEnvPath)}`));
801
+ }
802
+ }
803
+ function getClientGuidance(client) {
804
+ if (client === "claude-code") {
805
+ return [
806
+ "Claude Code can use both the hosted MCP surface and the local rlm-hook companion flows.",
807
+ "If hooks are enabled, the edge layer forwards lifecycle signals without owning durable memory locally."
808
+ ];
809
+ }
810
+ if (client === "cursor") {
811
+ return [
812
+ "Cursor reads the generated .mcp.json directly, so the hosted Snipara MCP surface is available after restart.",
813
+ "Use the companion CLI for explicit upload, planning, chunk lookup, doctor, and task commit workflows outside the editor UI."
814
+ ];
815
+ }
816
+ if (client === "claude-desktop") {
817
+ return [
818
+ "Claude Desktop uses the generated desktop MCP config plus the same hosted Snipara tool contract.",
819
+ "Keep the companion CLI nearby for bootstrap, upload, and task summaries that are easier to run from a terminal."
820
+ ];
821
+ }
822
+ return [
823
+ "The generated pack assumes a hosted-first Snipara setup with optional local companion workflows.",
824
+ "Use the companion CLI whenever your client does not expose an equivalent UX for planning, upload, doctor, or task commit flows."
825
+ ];
826
+ }
827
+ function getCompanionCommands(answers) {
828
+ const projectIdFlag = answers.projectId ? ` --project-id ${answers.projectId}` : "";
829
+ const apiKeyFlag = answers.apiKeyType === "none" || answers.apiKey.includes("YOUR_") ? " --api-key <rlm_key>" : ` --api-key ${answers.apiKey}`;
830
+ const clientFlag = getCompanionInitClientFlag(answers.aiClient);
831
+ return [
832
+ {
833
+ title: "Bootstrap local companion",
834
+ command: `rlm-hook init${clientFlag}${projectIdFlag}${apiKeyFlag}`,
835
+ purpose: "Write local companion config for this workstation."
836
+ },
837
+ {
838
+ title: "Run doctor for this project",
839
+ command: "create-snipara doctor",
840
+ purpose: "Inspect local wiring, hosted reachability, and generated assets."
841
+ },
842
+ {
843
+ title: "Repair local setup",
844
+ command: "create-snipara repair",
845
+ purpose: "Rebuild config files, hooks, and the companion pack."
846
+ },
847
+ {
848
+ title: "Run a focused hosted query",
849
+ command: 'rlm-hook query --query "recent auth decisions" --max-tokens 4000',
850
+ purpose: "Pull ranked context before editing or reviewing code."
851
+ },
852
+ {
853
+ title: "Generate a hosted plan",
854
+ command: 'rlm-hook plan --query "Implement webhook retry hardening" --max-tokens 3000',
855
+ purpose: "Produce a structured execution plan against indexed project knowledge."
856
+ },
857
+ {
858
+ title: "Upload or refresh a doc",
859
+ command: "rlm-hook upload --path docs/local-playbook.md --file ./docs/local-playbook.md",
860
+ purpose: "Push local notes or docs into hosted Snipara context."
861
+ },
862
+ {
863
+ title: "Inspect a specific chunk",
864
+ command: "rlm-hook chunk get --chunk-id <chunk_id>",
865
+ purpose: "Open an exact chunk returned by references-based search."
866
+ },
867
+ {
868
+ title: "Bootstrap the session memory view",
869
+ command: "rlm-hook session-bootstrap",
870
+ purpose: "Load critical and daily memories at session start."
871
+ },
872
+ {
873
+ title: "Commit durable task memory",
874
+ command: 'rlm-hook task-commit --summary "Hardened webhook retries and updated tests" --category engineering --outcome completed --files src/api/webhooks.ts tests/webhooks.test.ts',
875
+ purpose: "Persist reusable task outcomes back into hosted memory."
876
+ }
877
+ ];
878
+ }
879
+ async function createCompanionPack(answers) {
880
+ console.log(chalk.bold("\n\u{1F5C2}\uFE0F Generating Companion Pack\n"));
881
+ const packDir = path.join(process.cwd(), ".snipara", "companion");
882
+ const commands = getCompanionCommands(answers);
883
+ const guidance = getClientGuidance(answers.aiClient);
884
+ const detection = detectEnvironment();
885
+ fs.mkdirSync(packDir, { recursive: true });
886
+ const readme = `# Snipara Companion Pack
887
+
888
+ This project uses Snipara as a hosted-first context and memory layer with an optional local companion CLI.
889
+
890
+ ## Install Profile
891
+
892
+ - Profile: \`${answers.profile}\`
893
+ - Project slug: \`${answers.projectSlug}\`
894
+ - Project id: \`${answers.projectId || "not-set"}\`
895
+ - AI client: \`${answers.aiClient}\`
896
+ - Installed local companion: \`${answers.installCompanion ? "yes" : "no"}\`
897
+ - Installed MCP server: \`${answers.installMcp ? "yes" : "no"}\`
898
+ - Installed runtime: \`${answers.installRuntime ? "yes" : "no"}\`
899
+ - Generated hooks: \`${answers.installHooks ? "yes" : "no"}\`
900
+
901
+ ## Local Detection Snapshot
902
+
903
+ - Git repo: \`${detection.inGitRepo ? "yes" : "no"}\`
904
+ - .mcp.json present: \`${detection.files.mcpConfig ? "yes" : "no"}\`
905
+ - .env.local present: \`${detection.files.envLocal ? "yes" : "no"}\`
906
+ - Companion CLI on PATH: \`${detection.tooling.companionCli ? "yes" : "no"}\`
907
+ - Runtime CLI on PATH: \`${detection.tooling.runtimeCli ? "yes" : "no"}\`
908
+
909
+ ## Client Guidance
910
+
911
+ ${guidance.map((line) => `- ${line}`).join("\n")}
912
+
913
+ ## Suggested Commands
914
+
915
+ ${commands.map(
916
+ (item) => `### ${item.title}
917
+
918
+ \`${item.command}\`
919
+
920
+ ${item.purpose}`
921
+ ).join("\n\n")}
922
+
923
+ ## Design Rule
924
+
925
+ - Durable memory, policy, and orchestration stay hosted in Snipara.
926
+ - The local companion is for capture, bootstrap, upload, exploration, doctor, repair, and explicit task summaries.
927
+ `;
928
+ const pack = {
929
+ generated_at: (/* @__PURE__ */ new Date()).toISOString(),
930
+ project_slug: answers.projectSlug,
931
+ project_id: answers.projectId || null,
932
+ ai_client: answers.aiClient,
933
+ profile: answers.profile,
934
+ llm_provider: answers.llmProvider || null,
935
+ commands,
936
+ guidance,
937
+ detection
938
+ };
939
+ fs.writeFileSync(path.join(packDir, "README.md"), readme + "\n");
940
+ fs.writeFileSync(path.join(packDir, "commands.json"), JSON.stringify(pack, null, 2) + "\n");
941
+ console.log(chalk.green(" \u2713 Created .snipara/companion/README.md"));
942
+ console.log(chalk.green(" \u2713 Created .snipara/companion/commands.json"));
943
+ }
944
+ async function runRlmInit() {
945
+ console.log(chalk.bold("\n\u{1F527} Running RLM-Runtime Configuration\n"));
946
+ console.log(
947
+ chalk.dim(" Starting 'rlm init' - follow the prompts to configure execution environment...\n")
948
+ );
949
+ return new Promise((resolve) => {
950
+ const child = spawn("rlm", ["init"], {
951
+ stdio: "inherit",
952
+ shell: true
953
+ });
954
+ child.on("close", (code) => {
955
+ if (code === 0) {
956
+ console.log(chalk.green("\n \u2713 RLM-Runtime configured successfully"));
957
+ } else if (code !== null) {
958
+ console.log(chalk.yellow(`
959
+ \u26A0\uFE0F rlm init exited with code ${code}`));
960
+ console.log(chalk.dim(" You can run 'rlm init' manually later."));
961
+ }
962
+ resolve();
963
+ });
964
+ child.on("error", (error) => {
965
+ console.log(chalk.yellow(`
966
+ \u26A0\uFE0F Could not run rlm init: ${error.message}`));
967
+ console.log(chalk.dim(" Run 'rlm init' manually after installation completes."));
968
+ resolve();
969
+ });
970
+ });
971
+ }
972
+ async function runValidation(answers, includeHostedCheck) {
973
+ console.log(chalk.bold("\n\u{1FA7A} Running Validation\n"));
974
+ const checks = [];
975
+ const detection = detectEnvironment();
976
+ checks.push({
977
+ name: "project_slug",
978
+ status: answers.projectSlug ? "pass" : "fail",
979
+ detail: answers.projectSlug || "Project slug missing"
980
+ });
981
+ checks.push({
982
+ name: "mcp_config",
983
+ status: detection.files.mcpConfig ? "pass" : "warn",
984
+ detail: detection.files.mcpConfig ? ".mcp.json present" : ".mcp.json missing"
985
+ });
986
+ checks.push({
987
+ name: "companion_cli",
988
+ status: answers.installCompanion ? detection.tooling.companionCli ? "pass" : "fail" : "pass",
989
+ detail: answers.installCompanion && !detection.tooling.companionCli ? `${COMPANION_PACKAGE_NAME} not found on PATH` : "Companion CLI ready or not requested"
990
+ });
991
+ checks.push({
992
+ name: "runtime_cli",
993
+ status: answers.installRuntime ? detection.tooling.runtimeCli ? "pass" : "warn" : "pass",
994
+ detail: answers.installRuntime && !detection.tooling.runtimeCli ? "rlm CLI not found on PATH" : "Runtime CLI ready or not requested"
995
+ });
996
+ checks.push({
997
+ name: "companion_pack",
998
+ status: detection.files.sniparaCompanionPack ? "pass" : "warn",
999
+ detail: detection.files.sniparaCompanionPack ? ".snipara/companion present" : ".snipara/companion missing"
1000
+ });
1001
+ checks.push({
1002
+ name: "env_files",
1003
+ status: detection.files.env || detection.files.envLocal || detection.files.envExample ? "pass" : "warn",
1004
+ detail: detection.files.env || detection.files.envLocal || detection.files.envExample ? "Environment file present" : "No environment file found"
1005
+ });
1006
+ checks.push({
1007
+ name: "hooks",
1008
+ status: answers.installHooks && answers.aiClient === "claude-code" ? detection.files.claudeDir ? "pass" : "warn" : "pass",
1009
+ detail: answers.installHooks && answers.aiClient === "claude-code" ? detection.files.claudeDir ? "~/.claude detected" : "~/.claude missing" : "Hooks not requested"
1010
+ });
1011
+ if (includeHostedCheck && answers.apiKeyType !== "none" && !answers.apiKey.includes("YOUR_")) {
1012
+ checks.push(await testConnection(answers));
1013
+ } else if (includeHostedCheck) {
1014
+ checks.push({
1015
+ name: "hosted_connection",
1016
+ status: "warn",
1017
+ detail: "Skipped hosted connectivity test because no live API key was available"
1018
+ });
1019
+ }
1020
+ for (const check of checks) {
1021
+ const symbol = check.status === "pass" ? chalk.green("\u2713") : check.status === "warn" ? chalk.yellow("!") : chalk.red("\u2717");
1022
+ console.log(` ${symbol} ${check.name}: ${check.detail}`);
1023
+ }
1024
+ return {
1025
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1026
+ profile: answers.profile,
1027
+ checks
1028
+ };
1029
+ }
1030
+ function writeDoctorReport(report) {
1031
+ const reportDir = path.join(process.cwd(), ".snipara", "companion");
1032
+ fs.mkdirSync(reportDir, { recursive: true });
1033
+ fs.writeFileSync(path.join(reportDir, "doctor.json"), JSON.stringify(report, null, 2) + "\n");
1034
+ console.log(chalk.green(" \u2713 Wrote .snipara/companion/doctor.json"));
1035
+ }
1036
+ function printDoctorReport(detection, report) {
1037
+ console.log(chalk.bold("\n\u{1FA7A} Snipara Doctor\n"));
1038
+ console.log(
1039
+ ` Project slug: ${detection.existingConfig.slug || detection.projectSlug || "unknown"}`
1040
+ );
1041
+ console.log(` AI client: ${detection.aiClient}`);
1042
+ console.log(` In git repo: ${detection.inGitRepo ? "yes" : "no"}`);
1043
+ console.log(` Profile: ${report.profile}`);
1044
+ console.log("");
1045
+ for (const check of report.checks) {
1046
+ const symbol = check.status === "pass" ? chalk.green("\u2713") : check.status === "warn" ? chalk.yellow("!") : chalk.red("\u2717");
1047
+ console.log(` ${symbol} ${check.name}: ${check.detail}`);
611
1048
  }
1049
+ console.log("");
612
1050
  }
613
1051
  async function testConnection(answers) {
614
- console.log(chalk.bold("\n\u{1F50C} Testing Connection\n"));
615
1052
  const spinner = ora("Connecting to Snipara API...").start();
616
1053
  try {
617
1054
  const response = await fetch(`${API_URL}/mcp/${answers.projectSlug}`, {
@@ -631,22 +1068,29 @@ async function testConnection(answers) {
631
1068
  const data = await response.json();
632
1069
  const toolCount = data.result?.tools?.length || 0;
633
1070
  spinner.succeed(`Connected! ${toolCount} MCP tools available.`);
634
- } else {
635
- const errorText = await response.text();
636
- spinner.warn(`Connection returned ${response.status}: ${errorText}`);
1071
+ return {
1072
+ name: "hosted_connection",
1073
+ status: "pass",
1074
+ detail: `Connected successfully (${toolCount} tools reported)`
1075
+ };
637
1076
  }
1077
+ const errorText = await response.text();
1078
+ spinner.warn(`Connection returned ${response.status}`);
1079
+ return {
1080
+ name: "hosted_connection",
1081
+ status: "warn",
1082
+ detail: `HTTP ${response.status}: ${errorText.slice(0, 120)}`
1083
+ };
638
1084
  } catch (error) {
639
1085
  spinner.warn(`Could not connect: ${error}`);
640
- console.log(
641
- chalk.dim(
642
- "\n This is normal if you haven't created the project yet."
643
- )
644
- );
645
- console.log(chalk.dim(` Create your project at: ${BASE_URL}/dashboard
646
- `));
1086
+ return {
1087
+ name: "hosted_connection",
1088
+ status: "warn",
1089
+ detail: "Could not reach Snipara API from this machine"
1090
+ };
647
1091
  }
648
1092
  }
649
- function printSuccess(answers) {
1093
+ function printSuccess(answers, report) {
650
1094
  console.log(
651
1095
  chalk.green(`
652
1096
  \u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
@@ -656,66 +1100,153 @@ function printSuccess(answers) {
656
1100
  \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
657
1101
  `)
658
1102
  );
659
- console.log(chalk.bold("What was installed:\n"));
1103
+ console.log(chalk.bold("Installed profile:\n"));
1104
+ console.log(` ${chalk.green("\u2713")} ${answers.profile}`);
1105
+ console.log(chalk.bold("\nWhat was installed:\n"));
660
1106
  if (answers.installMcp) {
661
- console.log(chalk.green(" \u2713 snipara-mcp") + chalk.dim(" - MCP server for context queries"));
1107
+ console.log(chalk.green(" \u2713 snipara-mcp") + chalk.dim(" - Hosted MCP server wiring"));
662
1108
  }
663
1109
  if (answers.installRuntime) {
664
- console.log(chalk.green(" \u2713 rlm-runtime") + chalk.dim(" - Safe code execution with Docker"));
1110
+ console.log(chalk.green(" \u2713 rlm-runtime") + chalk.dim(" - Safe code execution"));
1111
+ }
1112
+ if (answers.installCompanion) {
1113
+ console.log(
1114
+ chalk.green(` \u2713 ${COMPANION_PACKAGE_NAME}`) + chalk.dim(" - Local companion workflows and hook bootstrap")
1115
+ );
665
1116
  }
666
1117
  if (answers.installHooks) {
667
- console.log(chalk.green(" \u2713 Claude Code hooks") + chalk.dim(" - Session memory automation"));
1118
+ console.log(chalk.green(" \u2713 hooks") + chalk.dim(" - Local session lifecycle scripts"));
668
1119
  }
669
- if (answers.apiKeyType !== "none") {
670
- console.log(chalk.green(" \u2713 .mcp.json") + chalk.dim(" - MCP server configuration"));
1120
+ console.log(chalk.green(" \u2713 .snipara/companion") + chalk.dim(" - Local pack + doctor report"));
1121
+ const failedChecks = report.checks.filter((check) => check.status === "fail");
1122
+ const warnedChecks = report.checks.filter((check) => check.status === "warn");
1123
+ console.log(chalk.bold("\nNext steps:\n"));
1124
+ console.log(" 1. Restart your AI client if you installed or changed MCP config.");
1125
+ console.log(" 2. Open .snipara/companion/README.md for local commands and repair flow.");
1126
+ console.log(" 3. Run create-snipara doctor anytime you want a fresh local validation report.");
1127
+ if (answers.installCompanion) {
1128
+ console.log(
1129
+ " 4. Use rlm-hook query / plan / upload / task-commit for explicit local workflows."
1130
+ );
671
1131
  }
672
- if (answers.apiKeyType === "team") {
673
- console.log(chalk.cyan("\n \u{1F511} Using Team API key - access to all team projects"));
674
- } else if (answers.apiKeyType === "project") {
675
- console.log(chalk.dim(`
676
- \u{1F511} Using Project API key for: ${answers.projectSlug}`));
1132
+ if (answers.installRuntime && !answers.runRlmInit) {
1133
+ console.log(" 5. Run rlm init when you want to choose sandbox/docker/local execution.");
677
1134
  }
678
- console.log(chalk.bold("\nNext steps:\n"));
679
- if (answers.apiKeyType === "none" && answers.installRuntime) {
680
- console.log(" 1. Restart your AI client (Claude Code, Cursor, etc.)");
681
- console.log(" 2. Use execute_python MCP tool - no API key needed!");
682
- console.log(chalk.dim("\n For rlm run/agent CLI, set one of these env vars:"));
683
- console.log(chalk.dim(" export OPENAI_API_KEY=sk-..."));
684
- console.log(chalk.dim(" export ANTHROPIC_API_KEY=sk-ant-..."));
685
- } else if (answers.apiKey.includes("YOUR_") || answers.apiKeyType === "none") {
686
- console.log(` 1. Get your API key at ${chalk.cyan(`${BASE_URL}/signup`)}`);
687
- console.log(" 2. Update SNIPARA_API_KEY in .mcp.json");
688
- console.log(" 3. Restart your AI client");
1135
+ if (failedChecks.length > 0 || warnedChecks.length > 0) {
1136
+ console.log(chalk.bold("\nValidation summary:\n"));
1137
+ for (const check of [...failedChecks, ...warnedChecks]) {
1138
+ const symbol = check.status === "fail" ? chalk.red("\u2717") : chalk.yellow("!");
1139
+ console.log(` ${symbol} ${check.name}: ${check.detail}`);
1140
+ }
1141
+ console.log(chalk.dim("\n Run create-snipara repair to rebuild local wiring.\n"));
689
1142
  } else {
690
- console.log(" 1. Restart your AI client (Claude Code, Cursor, etc.)");
691
- console.log(` 2. Upload docs at ${chalk.cyan(`${BASE_URL}/dashboard`)}`);
692
- console.log(" 3. Start using MCP tools like rlm_context_query");
1143
+ console.log(
1144
+ chalk.dim(
1145
+ "\n Validation passed cleanly. Doctor report saved in .snipara/companion/doctor.json\n"
1146
+ )
1147
+ );
693
1148
  }
694
- if (answers.installRuntime) {
695
- console.log(chalk.bold("\nRLM-Runtime:\n"));
696
- console.log(chalk.dim(" MCP tools (no LLM API key needed - your AI client provides the LLM):"));
697
- console.log(chalk.dim(" \u2022 execute_python - Run Python in sandbox"));
698
- console.log(chalk.dim(" \u2022 get_repl_context - Get session variables"));
699
- if (answers.llmProvider && answers.llmProvider !== "none") {
700
- const providerName = answers.llmProvider === "openai" ? "OpenAI" : "Anthropic";
701
- console.log(chalk.dim(`
702
- CLI commands (using ${providerName}):`));
703
- console.log(chalk.dim(" \u2022 rlm run - Run with LLM orchestration"));
704
- console.log(chalk.dim(" \u2022 rlm agent - Autonomous agent mode"));
705
- console.log(chalk.dim(" \u2022 rlm visualize - Trajectory dashboard"));
706
- } else {
707
- console.log(chalk.dim("\n CLI commands (not configured - set LLM API key later if needed):"));
708
- console.log(chalk.dim(" \u2022 rlm run - Requires OPENAI_API_KEY or ANTHROPIC_API_KEY"));
709
- console.log(chalk.dim(" \u2022 rlm agent - Requires OPENAI_API_KEY or ANTHROPIC_API_KEY"));
1149
+ console.log(chalk.dim(`Docs: ${BASE_URL}/docs`));
1150
+ console.log(chalk.dim("Support: hello@starbox-group.com\n"));
1151
+ }
1152
+ function commandExists(command) {
1153
+ try {
1154
+ execSync(`${command} --version`, { stdio: "ignore", shell: "/bin/sh" });
1155
+ return true;
1156
+ } catch {
1157
+ try {
1158
+ execSync(`command -v ${command}`, { stdio: "ignore", shell: "/bin/sh" });
1159
+ return true;
1160
+ } catch {
1161
+ return false;
710
1162
  }
711
1163
  }
712
- console.log(chalk.dim(`
713
- Docs: ${BASE_URL}/docs`));
714
- console.log(chalk.dim(" Support: hello@snipara.com\n"));
1164
+ }
1165
+ function detectEnvironment() {
1166
+ const existingConfig = getExistingConfig();
1167
+ return {
1168
+ cwd: process.cwd(),
1169
+ projectSlug: detectProjectSlug(),
1170
+ aiClient: detectAiClient(),
1171
+ inGitRepo: isGitRepo(),
1172
+ files: {
1173
+ mcpConfig: fs.existsSync(path.join(process.cwd(), ".mcp.json")),
1174
+ env: fs.existsSync(path.join(process.cwd(), ".env")),
1175
+ envLocal: fs.existsSync(path.join(process.cwd(), ".env.local")),
1176
+ envExample: fs.existsSync(path.join(process.cwd(), ".env.example")),
1177
+ claudeDir: fs.existsSync(path.join(homedir(), ".claude")),
1178
+ cursorDir: fs.existsSync(path.join(homedir(), ".cursor")),
1179
+ sniparaCompanionPack: fs.existsSync(path.join(process.cwd(), ".snipara", "companion"))
1180
+ },
1181
+ tooling: {
1182
+ node: commandExists("node"),
1183
+ npm: commandExists("npm"),
1184
+ pnpm: commandExists("pnpm"),
1185
+ python: commandExists("python3") || commandExists("python"),
1186
+ pip: commandExists("pip3") || commandExists("pip"),
1187
+ uv: commandExists("uv"),
1188
+ docker: commandExists("docker"),
1189
+ git: commandExists("git"),
1190
+ companionCli: commandExists("rlm-hook"),
1191
+ runtimeCli: commandExists("rlm")
1192
+ },
1193
+ existingConfig: {
1194
+ slug: existingConfig?.slug,
1195
+ projectId: existingConfig?.projectId,
1196
+ hasSniparaServer: existingConfig?.hasSniparaServer ?? false,
1197
+ hasRuntimeServer: existingConfig?.hasRuntimeServer ?? false
1198
+ }
1199
+ };
1200
+ }
1201
+ function loadExistingAnswers(options) {
1202
+ const detection = detectEnvironment();
1203
+ const effectiveProfile = resolveMaintenanceProfile(detection, options.profile);
1204
+ const apiKey = options.teamKey || options.apiKey || readApiKeyFromEnvFiles() || "YOUR_API_KEY_HERE";
1205
+ const apiKeyType = options.teamKey ? "team" : options.apiKey || readApiKeyFromEnvFiles() ? "project" : effectiveProfile === "runtime-only" ? "none" : "none";
1206
+ return buildAnswersFromProfile({
1207
+ projectSlug: options.slug || detection.existingConfig.slug || detection.projectSlug || "my-project",
1208
+ projectId: options.projectId || detection.existingConfig.projectId || "",
1209
+ apiKey,
1210
+ apiKeyType,
1211
+ profile: effectiveProfile,
1212
+ aiClient: detection.aiClient,
1213
+ skipCompanion: options.withCompanion === true ? false : !detection.tooling.companionCli && effectiveProfile !== "hosted-companion" && effectiveProfile !== "full-stack",
1214
+ skipMcp: options.withMcp === true ? false : !detection.existingConfig.hasSniparaServer && effectiveProfile === "runtime-only",
1215
+ skipRuntime: options.withRuntime === true ? false : !detection.existingConfig.hasRuntimeServer && effectiveProfile !== "full-stack" && effectiveProfile !== "runtime-only",
1216
+ skipHooks: options.withHooks ? false : detection.aiClient !== "claude-code"
1217
+ });
1218
+ }
1219
+ function resolveMaintenanceProfile(detection, explicitProfile) {
1220
+ if (explicitProfile && isInstallProfile(explicitProfile)) {
1221
+ return explicitProfile;
1222
+ }
1223
+ if (detection.existingConfig.hasRuntimeServer && detection.existingConfig.hasSniparaServer) {
1224
+ return detection.tooling.companionCli ? "full-stack" : "full-stack";
1225
+ }
1226
+ if (detection.existingConfig.hasSniparaServer && detection.tooling.companionCli) {
1227
+ return "hosted-companion";
1228
+ }
1229
+ if (detection.existingConfig.hasSniparaServer) {
1230
+ return "hosted-only";
1231
+ }
1232
+ return detection.tooling.runtimeCli ? "runtime-only" : "hosted-companion";
1233
+ }
1234
+ function readApiKeyFromEnvFiles() {
1235
+ const candidates = [".env.local", ".env"];
1236
+ for (const file of candidates) {
1237
+ const filePath = path.join(process.cwd(), file);
1238
+ if (!fs.existsSync(filePath)) continue;
1239
+ const content = fs.readFileSync(filePath, "utf-8");
1240
+ const match = content.match(/^SNIPARA_API_KEY=(.+)$/m);
1241
+ if (match?.[1]) {
1242
+ return match[1].trim();
1243
+ }
1244
+ }
1245
+ return null;
715
1246
  }
716
1247
  function detectProjectSlug() {
717
1248
  try {
718
- const remote = execSync("git remote get-url origin 2>/dev/null", {
1249
+ const remote = execSync("git remote get-url origin", {
719
1250
  encoding: "utf-8"
720
1251
  }).trim();
721
1252
  const match = remote.match(/[/:]([^/]+?)(?:\.git)?$/);
@@ -726,6 +1257,16 @@ function detectProjectSlug() {
726
1257
  }
727
1258
  return path.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
728
1259
  }
1260
+ function isGitRepo() {
1261
+ try {
1262
+ execSync("git rev-parse --is-inside-work-tree", {
1263
+ stdio: "ignore"
1264
+ });
1265
+ return true;
1266
+ } catch {
1267
+ return false;
1268
+ }
1269
+ }
729
1270
  function detectAiClient() {
730
1271
  if (fs.existsSync(path.join(homedir(), ".claude")) || process.env.CLAUDE_CODE) {
731
1272
  return "claude-code";
@@ -733,7 +1274,7 @@ function detectAiClient() {
733
1274
  if (fs.existsSync(path.join(homedir(), ".cursor")) || process.env.CURSOR_EDITOR) {
734
1275
  return "cursor";
735
1276
  }
736
- const claudeDesktopPath = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : path.join(homedir(), "AppData", "Roaming", "Claude");
1277
+ const claudeDesktopPath = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path.join(homedir(), "AppData", "Roaming", "Claude") : path.join(homedir(), ".config", "claude");
737
1278
  if (fs.existsSync(claudeDesktopPath)) {
738
1279
  return "claude-desktop";
739
1280
  }
@@ -741,33 +1282,37 @@ function detectAiClient() {
741
1282
  }
742
1283
  function detectPipCommand() {
743
1284
  const commands = ["uv", "pip3", "pip"];
744
- for (const cmd of commands) {
745
- try {
746
- execSync(`${cmd} --version`, { stdio: "pipe" });
747
- return cmd;
748
- } catch {
1285
+ for (const command of commands) {
1286
+ if (commandExists(command)) {
1287
+ return command;
749
1288
  }
750
1289
  }
751
1290
  return null;
752
1291
  }
753
1292
  function getExistingConfig() {
754
1293
  const mcpConfigPath = path.join(process.cwd(), ".mcp.json");
755
- if (fs.existsSync(mcpConfigPath)) {
756
- try {
757
- const config = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
758
- const sniparaServer = config.mcpServers?.snipara;
759
- if (sniparaServer?.url) {
760
- const match = sniparaServer.url.match(/\/mcp\/([^/]+)$/);
761
- return {
762
- slug: match?.[1],
763
- projectId: ""
764
- // Could extract from headers if stored
765
- };
766
- }
767
- } catch {
768
- }
1294
+ if (!fs.existsSync(mcpConfigPath)) {
1295
+ return null;
1296
+ }
1297
+ try {
1298
+ const config = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
1299
+ const sniparaServer = config.mcpServers?.snipara;
1300
+ const runtimeServer = config.mcpServers?.["rlm-runtime"];
1301
+ const match = sniparaServer?.url?.match(/\/mcp\/([^/]+)$/);
1302
+ return {
1303
+ slug: match?.[1],
1304
+ projectId: "",
1305
+ hasSniparaServer: Boolean(sniparaServer),
1306
+ hasRuntimeServer: Boolean(runtimeServer)
1307
+ };
1308
+ } catch {
1309
+ return {
1310
+ slug: void 0,
1311
+ projectId: "",
1312
+ hasSniparaServer: false,
1313
+ hasRuntimeServer: false
1314
+ };
769
1315
  }
770
- return null;
771
1316
  }
772
1317
  main().catch((error) => {
773
1318
  console.error(chalk.red("Error:"), error.message);