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.
- package/README.md +134 -148
- package/dist/index.js +842 -317
- 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 {
|
|
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.
|
|
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
|
|
17
|
-
|
|
18
|
-
|
|
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
|
-
|
|
29
|
-
|
|
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
|
|
44
|
-
\u2551 \u2022
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
85
|
-
apiKeyType:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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: (
|
|
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: (
|
|
253
|
+
type: (_, values) => values.profile === "runtime-only" ? null : values.apiKeyType === "project" || values.apiKeyType === "team" ? "text" : null,
|
|
133
254
|
name: "apiKey",
|
|
134
|
-
message: (
|
|
135
|
-
validate: (
|
|
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: "
|
|
267
|
+
description: "Best hooks support"
|
|
149
268
|
},
|
|
150
269
|
{
|
|
151
270
|
title: "Cursor",
|
|
152
271
|
value: "cursor",
|
|
153
|
-
description: "MCP
|
|
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
|
|
277
|
+
description: "Desktop MCP config flow"
|
|
159
278
|
},
|
|
160
279
|
{ title: "Other", value: "other" }
|
|
161
280
|
]
|
|
162
281
|
},
|
|
163
282
|
{
|
|
164
|
-
type: "
|
|
165
|
-
name: "
|
|
166
|
-
message: "
|
|
167
|
-
|
|
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
|
|
298
|
+
} else {
|
|
200
299
|
apiKey = "YOUR_API_KEY_HERE";
|
|
201
300
|
apiKeyType = "none";
|
|
202
301
|
}
|
|
203
302
|
}
|
|
204
|
-
const
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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?
|
|
320
|
+
message: "Will you use rlm run/agent CLI too?",
|
|
217
321
|
choices: [
|
|
218
322
|
{
|
|
219
|
-
title: "No -
|
|
323
|
+
title: "No - execute_python only",
|
|
220
324
|
value: "none",
|
|
221
|
-
description: "No
|
|
325
|
+
description: "No extra LLM API key needed"
|
|
222
326
|
},
|
|
223
327
|
{
|
|
224
|
-
title: "Yes - OpenAI
|
|
328
|
+
title: "Yes - OpenAI",
|
|
225
329
|
value: "openai",
|
|
226
330
|
description: "Requires OPENAI_API_KEY"
|
|
227
331
|
},
|
|
228
332
|
{
|
|
229
|
-
title: "Yes - Anthropic
|
|
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
|
|
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: (
|
|
240
|
-
if (values.llmProvider === "openai" && !
|
|
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" && !
|
|
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 =
|
|
251
|
-
llmApiKey =
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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:
|
|
269
|
-
projectId:
|
|
270
|
-
apiKey: apiKey || "YOUR_API_KEY_HERE",
|
|
271
|
-
apiKeyType,
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
|
288
|
-
|
|
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
|
-
|
|
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: "
|
|
304
|
-
message: "Paste your API key
|
|
305
|
-
validate: (
|
|
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.
|
|
462
|
+
return response.apiKey;
|
|
308
463
|
}
|
|
309
|
-
async function installPythonPackages(answers) {
|
|
310
|
-
console.log(chalk.bold(
|
|
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(
|
|
472
|
+
console.log(chalk.gray(` pip install ${upgrade ? "-U " : ""}${MCP_PACKAGE_NAME}`));
|
|
320
473
|
}
|
|
321
474
|
if (answers.installRuntime) {
|
|
322
|
-
console.log(chalk.gray(
|
|
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(
|
|
482
|
+
packages.push(MCP_PACKAGE_NAME);
|
|
330
483
|
}
|
|
331
484
|
if (answers.installRuntime) {
|
|
332
|
-
packages.push(
|
|
485
|
+
packages.push(RUNTIME_PACKAGE_NAME);
|
|
333
486
|
}
|
|
334
487
|
if (packages.length === 0) return;
|
|
335
|
-
const spinner = ora(
|
|
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(
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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(
|
|
384
|
-
fs.appendFileSync(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
console.log(chalk.green(
|
|
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
|
-
|
|
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
|
|
642
|
+
config.mcpServers.snipara = {
|
|
416
643
|
command: "uvx",
|
|
417
|
-
args: [
|
|
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
|
-
(
|
|
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
|
-
|
|
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
|
-
|
|
529
|
-
|
|
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
|
-
|
|
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
|
-
|
|
550
|
-
|
|
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
|
-
|
|
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
|
-
|
|
572
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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
|
-
|
|
685
|
-
|
|
686
|
-
|
|
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("
|
|
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
|
|
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
|
|
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
|
|
1146
|
+
console.log(chalk.green(" \u2713 hooks") + chalk.dim(" - Local session lifecycle scripts"));
|
|
712
1147
|
}
|
|
713
|
-
|
|
714
|
-
|
|
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.
|
|
717
|
-
console.log(
|
|
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
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
console.log(chalk.dim("
|
|
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(
|
|
735
|
-
|
|
736
|
-
|
|
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
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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
|
-
|
|
756
|
-
|
|
757
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
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);
|