create-snipara 1.2.0 → 1.3.1

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