create-snipara 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -148
- package/dist/index.js +814 -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.0";
|
|
13
13
|
var BASE_URL = "https://www.snipara.com";
|
|
14
14
|
var API_URL = "https://api.snipara.com";
|
|
15
|
+
var COMPANION_PACKAGE_NAME = "snipara-companion";
|
|
16
|
+
var MCP_PACKAGE_NAME = "snipara-mcp";
|
|
17
|
+
var RUNTIME_PACKAGE_NAME = "rlm-runtime[all]";
|
|
18
|
+
function getCompanionInitClientFlag(aiClient) {
|
|
19
|
+
if (aiClient === "cursor") {
|
|
20
|
+
return " --client cursor";
|
|
21
|
+
}
|
|
22
|
+
return aiClient === "claude-code" ? " --client claude-code" : "";
|
|
23
|
+
}
|
|
24
|
+
function buildProgram() {
|
|
25
|
+
const program = new Command();
|
|
26
|
+
program.name("create-snipara").description("Initialize and maintain Snipara MCP + local companion workflows").version(VERSION);
|
|
27
|
+
program.command("init").description("Install and configure Snipara for this project").option("--api-key <key>", "Snipara project API key").option("--team-key <key>", "Snipara team API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
|
|
28
|
+
"--profile <profile>",
|
|
29
|
+
"Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
|
|
30
|
+
).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip snipara-mcp installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").option("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting").action(async (options) => {
|
|
31
|
+
printBanner();
|
|
32
|
+
await runInit(options);
|
|
33
|
+
});
|
|
34
|
+
program.command("doctor").description("Inspect the current project and write a validation report").option("--json", "Print the report as JSON").action(async (options) => {
|
|
35
|
+
printBanner();
|
|
36
|
+
await runDoctor(options);
|
|
37
|
+
});
|
|
38
|
+
program.command("repair").description("Rebuild local Snipara config and re-run validation").option("--api-key <key>", "Snipara project API key override").option("--team-key <key>", "Snipara team API key override").option("--slug <slug>", "Project slug override").option("--project-id <id>", "Project ID override").option("--profile <profile>", "Target install profile").option("--with-hooks", "Force hook generation").option("--with-runtime", "Force runtime server/config").option("--with-mcp", "Force MCP config").option("--with-companion", "Force companion pack generation").action(async (options) => {
|
|
39
|
+
printBanner();
|
|
40
|
+
await runRepair(options);
|
|
41
|
+
});
|
|
42
|
+
program.command("upgrade").description("Upgrade installed Snipara local components").option("--with-hooks", "Refresh hooks after upgrade").option("--with-runtime", "Upgrade runtime even if not detected").option("--with-mcp", "Upgrade snipara-mcp even if not detected").option("--with-companion", "Upgrade companion even if not detected").action(async (options) => {
|
|
43
|
+
printBanner();
|
|
44
|
+
await runUpgrade(options);
|
|
45
|
+
});
|
|
46
|
+
program.command("print-config").description("Print detected local setup and inferred install profile").option("--json", "Print JSON instead of a formatted summary").action(async (options) => {
|
|
47
|
+
printBanner();
|
|
48
|
+
await runPrintConfig(options);
|
|
49
|
+
});
|
|
50
|
+
addSharedRootOptions(program);
|
|
51
|
+
program.action(async (options) => {
|
|
52
|
+
printBanner();
|
|
53
|
+
await runInit(options);
|
|
54
|
+
});
|
|
55
|
+
return program;
|
|
56
|
+
}
|
|
57
|
+
function addSharedRootOptions(program) {
|
|
58
|
+
program.option("--api-key <key>", "Snipara project API key").option("--team-key <key>", "Snipara team API key").option("--slug <slug>", "Project slug").option("--project-id <id>", "Project ID").option(
|
|
59
|
+
"--profile <profile>",
|
|
60
|
+
"Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
|
|
61
|
+
).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip snipara-mcp installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").option("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting");
|
|
62
|
+
}
|
|
15
63
|
async function main() {
|
|
16
|
-
program
|
|
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
|
-
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;
|
|
266
363
|
}
|
|
364
|
+
return answers;
|
|
365
|
+
}
|
|
366
|
+
function resolveRequestedProfile(options) {
|
|
367
|
+
if ("runtimeOnly" in options && options.runtimeOnly) {
|
|
368
|
+
return "runtime-only";
|
|
369
|
+
}
|
|
370
|
+
if (options.profile && isInstallProfile(options.profile)) {
|
|
371
|
+
return options.profile;
|
|
372
|
+
}
|
|
373
|
+
return "hosted-companion";
|
|
374
|
+
}
|
|
375
|
+
function isInstallProfile(value) {
|
|
376
|
+
return ["hosted-only", "hosted-companion", "full-stack", "runtime-only"].includes(value);
|
|
377
|
+
}
|
|
378
|
+
function installProfileChoices() {
|
|
379
|
+
return [
|
|
380
|
+
{
|
|
381
|
+
title: "Hosted only",
|
|
382
|
+
value: "hosted-only",
|
|
383
|
+
description: "Snipara MCP only, no local companion or runtime"
|
|
384
|
+
},
|
|
385
|
+
{
|
|
386
|
+
title: "Hosted + Companion",
|
|
387
|
+
value: "hosted-companion",
|
|
388
|
+
description: "Recommended default: hosted MCP plus snipara-companion"
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
title: "Hosted + Companion + Runtime",
|
|
392
|
+
value: "full-stack",
|
|
393
|
+
description: "Add local runtime execution as well"
|
|
394
|
+
},
|
|
395
|
+
{
|
|
396
|
+
title: "Runtime only",
|
|
397
|
+
value: "runtime-only",
|
|
398
|
+
description: "Local execution only, no hosted API key required"
|
|
399
|
+
}
|
|
400
|
+
];
|
|
401
|
+
}
|
|
402
|
+
function buildAnswersFromProfile(input) {
|
|
403
|
+
const defaults = {
|
|
404
|
+
"hosted-only": {
|
|
405
|
+
installCompanion: false,
|
|
406
|
+
installMcp: true,
|
|
407
|
+
installRuntime: false,
|
|
408
|
+
installHooks: false
|
|
409
|
+
},
|
|
410
|
+
"hosted-companion": {
|
|
411
|
+
installCompanion: true,
|
|
412
|
+
installMcp: true,
|
|
413
|
+
installRuntime: false,
|
|
414
|
+
installHooks: input.aiClient === "claude-code"
|
|
415
|
+
},
|
|
416
|
+
"full-stack": {
|
|
417
|
+
installCompanion: true,
|
|
418
|
+
installMcp: true,
|
|
419
|
+
installRuntime: true,
|
|
420
|
+
installHooks: input.aiClient === "claude-code"
|
|
421
|
+
},
|
|
422
|
+
"runtime-only": {
|
|
423
|
+
installCompanion: false,
|
|
424
|
+
installMcp: false,
|
|
425
|
+
installRuntime: true,
|
|
426
|
+
installHooks: false
|
|
427
|
+
}
|
|
428
|
+
};
|
|
429
|
+
const selected = defaults[input.profile];
|
|
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,69 +446,104 @@ 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(" ")}
|
|
347
504
|
`));
|
|
348
505
|
}
|
|
349
506
|
}
|
|
507
|
+
function stripExtras(name) {
|
|
508
|
+
return name.replace(/\[.*\]$/, "");
|
|
509
|
+
}
|
|
510
|
+
function detectNodePackageManager() {
|
|
511
|
+
if (commandExists("pnpm")) return "pnpm";
|
|
512
|
+
if (commandExists("npm")) return "npm";
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
async function installNodeCompanion(upgrade) {
|
|
516
|
+
console.log(chalk.bold(`
|
|
517
|
+
\u{1F9F0} ${upgrade ? "Upgrading" : "Installing"} Local Companion CLI
|
|
518
|
+
`));
|
|
519
|
+
const packageManager = detectNodePackageManager();
|
|
520
|
+
if (!packageManager) {
|
|
521
|
+
console.log(chalk.yellow(" \u26A0\uFE0F npm/pnpm not found. Please install manually:\n"));
|
|
522
|
+
console.log(chalk.gray(` npm install -g ${COMPANION_PACKAGE_NAME}
|
|
523
|
+
`));
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
const spinner = ora(
|
|
527
|
+
`${upgrade ? "Upgrading" : "Installing"} ${COMPANION_PACKAGE_NAME}...`
|
|
528
|
+
).start();
|
|
529
|
+
try {
|
|
530
|
+
const installCmd = packageManager === "pnpm" ? `pnpm add -g ${upgrade ? "--latest " : ""}${COMPANION_PACKAGE_NAME}` : `npm install -g ${upgrade ? COMPANION_PACKAGE_NAME + "@latest" : COMPANION_PACKAGE_NAME}`;
|
|
531
|
+
execSync(installCmd, {
|
|
532
|
+
stdio: "pipe",
|
|
533
|
+
encoding: "utf-8"
|
|
534
|
+
});
|
|
535
|
+
spinner.succeed(`${upgrade ? "Upgraded" : "Installed"} ${COMPANION_PACKAGE_NAME}`);
|
|
536
|
+
} catch {
|
|
537
|
+
spinner.fail(`Failed to ${upgrade ? "upgrade" : "install"} ${COMPANION_PACKAGE_NAME}`);
|
|
538
|
+
console.log(chalk.yellow("\n Install manually with:"));
|
|
539
|
+
console.log(
|
|
540
|
+
chalk.gray(
|
|
541
|
+
` npm install -g ${upgrade ? COMPANION_PACKAGE_NAME + "@latest" : COMPANION_PACKAGE_NAME}
|
|
542
|
+
`
|
|
543
|
+
)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
350
547
|
async function createMcpConfig(answers) {
|
|
351
548
|
console.log(chalk.bold("\n\u2699\uFE0F Creating Configuration\n"));
|
|
352
549
|
const mcpConfigPath = path.join(process.cwd(), ".mcp.json");
|
|
@@ -356,18 +553,18 @@ async function createMcpConfig(answers) {
|
|
|
356
553
|
mcpConfig = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
|
|
357
554
|
console.log(chalk.dim(" Found existing .mcp.json, merging..."));
|
|
358
555
|
} catch {
|
|
359
|
-
console.log(
|
|
360
|
-
chalk.yellow(" Could not parse existing .mcp.json, creating new...")
|
|
361
|
-
);
|
|
556
|
+
console.log(chalk.yellow(" Could not parse existing .mcp.json, creating new..."));
|
|
362
557
|
}
|
|
363
558
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
559
|
+
if (answers.apiKeyType !== "none" || answers.installMcp) {
|
|
560
|
+
mcpConfig.mcpServers.snipara = {
|
|
561
|
+
type: "http",
|
|
562
|
+
url: `${API_URL}/mcp/${answers.projectSlug}`,
|
|
563
|
+
headers: {
|
|
564
|
+
"X-API-Key": answers.apiKey || "YOUR_API_KEY_HERE"
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
}
|
|
371
568
|
if (answers.installRuntime) {
|
|
372
569
|
mcpConfig.mcpServers["rlm-runtime"] = {
|
|
373
570
|
type: "http",
|
|
@@ -377,26 +574,28 @@ async function createMcpConfig(answers) {
|
|
|
377
574
|
}
|
|
378
575
|
fs.writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2) + "\n");
|
|
379
576
|
console.log(chalk.green(" \u2713 Created .mcp.json"));
|
|
577
|
+
ensureGitignoreEntry(".mcp.json", "# Snipara MCP config (contains API key)");
|
|
578
|
+
if (answers.aiClient === "claude-desktop") {
|
|
579
|
+
await createClaudeDesktopConfig(answers);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
function ensureGitignoreEntry(entry, comment) {
|
|
380
583
|
const gitignorePath = path.join(process.cwd(), ".gitignore");
|
|
381
584
|
if (fs.existsSync(gitignorePath)) {
|
|
382
585
|
const gitignore = fs.readFileSync(gitignorePath, "utf-8");
|
|
383
|
-
if (!gitignore.includes(
|
|
384
|
-
fs.appendFileSync(
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
console.log(chalk.green(
|
|
586
|
+
if (!gitignore.includes(entry)) {
|
|
587
|
+
fs.appendFileSync(gitignorePath, `
|
|
588
|
+
${comment}
|
|
589
|
+
${entry}
|
|
590
|
+
`);
|
|
591
|
+
console.log(chalk.green(` \u2713 Added ${entry} to .gitignore`));
|
|
389
592
|
}
|
|
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);
|
|
593
|
+
return;
|
|
399
594
|
}
|
|
595
|
+
fs.writeFileSync(gitignorePath, `${comment}
|
|
596
|
+
${entry}
|
|
597
|
+
`);
|
|
598
|
+
console.log(chalk.green(` \u2713 Created .gitignore with ${entry}`));
|
|
400
599
|
}
|
|
401
600
|
async function createClaudeDesktopConfig(answers) {
|
|
402
601
|
const configDir = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path.join(homedir(), "AppData", "Roaming", "Claude") : path.join(homedir(), ".config", "claude");
|
|
@@ -412,9 +611,9 @@ async function createClaudeDesktopConfig(answers) {
|
|
|
412
611
|
config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
413
612
|
}
|
|
414
613
|
config.mcpServers = config.mcpServers || {};
|
|
415
|
-
config.mcpServers
|
|
614
|
+
config.mcpServers.snipara = {
|
|
416
615
|
command: "uvx",
|
|
417
|
-
args: [
|
|
616
|
+
args: [MCP_PACKAGE_NAME],
|
|
418
617
|
env: {
|
|
419
618
|
SNIPARA_API_KEY: answers.apiKey,
|
|
420
619
|
SNIPARA_PROJECT_SLUG: answers.projectSlug
|
|
@@ -433,9 +632,7 @@ async function createClaudeDesktopConfig(answers) {
|
|
|
433
632
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
434
633
|
console.log(chalk.green(" \u2713 Updated Claude Desktop config"));
|
|
435
634
|
} catch (error) {
|
|
436
|
-
console.log(
|
|
437
|
-
chalk.yellow(` \u26A0\uFE0F Could not update Claude Desktop config: ${error}`)
|
|
438
|
-
);
|
|
635
|
+
console.log(chalk.yellow(` \u26A0\uFE0F Could not update Claude Desktop config: ${error}`));
|
|
439
636
|
}
|
|
440
637
|
}
|
|
441
638
|
async function installClaudeCodeHooks(answers) {
|
|
@@ -444,11 +641,8 @@ async function installClaudeCodeHooks(answers) {
|
|
|
444
641
|
const hooksDir = path.join(claudeDir, "hooks");
|
|
445
642
|
if (!fs.existsSync(claudeDir)) {
|
|
446
643
|
console.log(
|
|
447
|
-
chalk.yellow(
|
|
448
|
-
" \u26A0\uFE0F ~/.claude not found. Install Claude Code first, then run:\n"
|
|
449
|
-
)
|
|
644
|
+
chalk.yellow(" \u26A0\uFE0F ~/.claude not found. Install Claude Code first, then rerun repair.\n")
|
|
450
645
|
);
|
|
451
|
-
console.log(chalk.gray(" create-snipara --skip-mcp --skip-runtime\n"));
|
|
452
646
|
return;
|
|
453
647
|
}
|
|
454
648
|
if (!fs.existsSync(hooksDir)) {
|
|
@@ -457,17 +651,14 @@ async function installClaudeCodeHooks(answers) {
|
|
|
457
651
|
const hooks = [
|
|
458
652
|
{
|
|
459
653
|
name: "snipara-startup.sh",
|
|
460
|
-
event: "PreToolUse",
|
|
461
654
|
content: createStartupHook(answers)
|
|
462
655
|
},
|
|
463
656
|
{
|
|
464
657
|
name: "snipara-session.sh",
|
|
465
|
-
event: "PostToolUse",
|
|
466
658
|
content: createSessionHook(answers)
|
|
467
659
|
},
|
|
468
660
|
{
|
|
469
661
|
name: "snipara-compact.sh",
|
|
470
|
-
event: "PreCompact",
|
|
471
662
|
content: createCompactHook(answers)
|
|
472
663
|
}
|
|
473
664
|
];
|
|
@@ -483,6 +674,7 @@ async function installClaudeCodeHooks(answers) {
|
|
|
483
674
|
try {
|
|
484
675
|
settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
|
|
485
676
|
} catch {
|
|
677
|
+
settings = {};
|
|
486
678
|
}
|
|
487
679
|
}
|
|
488
680
|
settings.hooks = settings.hooks || {};
|
|
@@ -506,75 +698,50 @@ async function installClaudeCodeHooks(answers) {
|
|
|
506
698
|
for (const entry of hookEntries) {
|
|
507
699
|
const eventHooks = hooksConfig[entry.event] || [];
|
|
508
700
|
const exists = eventHooks.some(
|
|
509
|
-
(
|
|
701
|
+
(hook) => typeof hook === "object" && hook !== null && "script" in hook && hook.script === entry.script
|
|
510
702
|
);
|
|
511
703
|
if (!exists) {
|
|
512
704
|
eventHooks.push(entry);
|
|
513
705
|
hooksConfig[entry.event] = eventHooks;
|
|
514
706
|
}
|
|
515
707
|
}
|
|
516
|
-
settings.hooks = hooksConfig;
|
|
517
708
|
fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n");
|
|
518
709
|
console.log(chalk.green(" \u2713 Updated ~/.claude/settings.json"));
|
|
519
710
|
}
|
|
520
711
|
function createStartupHook(answers) {
|
|
521
|
-
return `#!/bin/bash
|
|
522
|
-
|
|
523
|
-
# Event: PreToolUse (TodoWrite)
|
|
712
|
+
return `#!/usr/bin/env bash
|
|
713
|
+
set -euo pipefail
|
|
524
714
|
|
|
525
|
-
export SNIPARA_API_KEY="${answers.apiKey}"
|
|
526
715
|
export SNIPARA_PROJECT_SLUG="${answers.projectSlug}"
|
|
716
|
+
${answers.projectId ? `export SNIPARA_PROJECT_ID="${answers.projectId}"` : ""}
|
|
527
717
|
|
|
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
|
|
718
|
+
if command -v rlm-hook >/dev/null 2>&1; then
|
|
719
|
+
rlm-hook session-bootstrap || true
|
|
536
720
|
fi
|
|
537
|
-
|
|
538
|
-
exit 0
|
|
539
721
|
`;
|
|
540
722
|
}
|
|
541
723
|
function createSessionHook(answers) {
|
|
542
|
-
return `#!/bin/bash
|
|
543
|
-
|
|
544
|
-
# Event: PostToolUse (Bash)
|
|
724
|
+
return `#!/usr/bin/env bash
|
|
725
|
+
set -euo pipefail
|
|
545
726
|
|
|
546
|
-
export SNIPARA_API_KEY="${answers.apiKey}"
|
|
547
727
|
export SNIPARA_PROJECT_SLUG="${answers.projectSlug}"
|
|
728
|
+
${answers.projectId ? `export SNIPARA_PROJECT_ID="${answers.projectId}"` : ""}
|
|
548
729
|
|
|
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
|
|
730
|
+
if command -v rlm-hook >/dev/null 2>&1; then
|
|
731
|
+
rlm-hook emit-event --event-type tool_result --json '{"source":"claude-code-hook"}' || true
|
|
558
732
|
fi
|
|
559
|
-
|
|
560
|
-
exit 0
|
|
561
733
|
`;
|
|
562
734
|
}
|
|
563
735
|
function createCompactHook(answers) {
|
|
564
|
-
return `#!/bin/bash
|
|
565
|
-
|
|
566
|
-
# Event: PreCompact
|
|
736
|
+
return `#!/usr/bin/env bash
|
|
737
|
+
set -euo pipefail
|
|
567
738
|
|
|
568
|
-
export SNIPARA_API_KEY="${answers.apiKey}"
|
|
569
739
|
export SNIPARA_PROJECT_SLUG="${answers.projectSlug}"
|
|
740
|
+
${answers.projectId ? `export SNIPARA_PROJECT_ID="${answers.projectId}"` : ""}
|
|
570
741
|
|
|
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
|
|
742
|
+
if command -v rlm-hook >/dev/null 2>&1; then
|
|
743
|
+
rlm-hook task-commit --summary "Claude Code compacted local session context" --outcome partial || true
|
|
575
744
|
fi
|
|
576
|
-
|
|
577
|
-
exit 0
|
|
578
745
|
`;
|
|
579
746
|
}
|
|
580
747
|
async function updateEnvFiles(answers) {
|
|
@@ -605,7 +772,7 @@ ${llmVarName}=${answers.llmApiKey}`;
|
|
|
605
772
|
}
|
|
606
773
|
if (fs.existsSync(envExamplePath)) {
|
|
607
774
|
const content = fs.readFileSync(envExamplePath, "utf-8");
|
|
608
|
-
if (!content.includes("SNIPARA_API_KEY")) {
|
|
775
|
+
if (!content.includes("SNIPARA_API_KEY") && !content.includes("SNIPARA_PROJECT_SLUG")) {
|
|
609
776
|
fs.appendFileSync(envExamplePath, `
|
|
610
777
|
|
|
611
778
|
${envVars}
|
|
@@ -626,11 +793,159 @@ ${envVars}
|
|
|
626
793
|
`);
|
|
627
794
|
console.log(chalk.green(` \u2713 Updated ${path.basename(targetEnvPath)}`));
|
|
628
795
|
}
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
if (!answers.apiKey.includes("YOUR_") || answers.llmApiKey) {
|
|
799
|
+
fs.writeFileSync(targetEnvPath, envVars + "\n");
|
|
800
|
+
console.log(chalk.green(` \u2713 Created ${path.basename(targetEnvPath)}`));
|
|
629
801
|
}
|
|
630
802
|
}
|
|
803
|
+
function getClientGuidance(client) {
|
|
804
|
+
if (client === "claude-code") {
|
|
805
|
+
return [
|
|
806
|
+
"Claude Code can use both the hosted MCP surface and the local rlm-hook companion flows.",
|
|
807
|
+
"If hooks are enabled, the edge layer forwards lifecycle signals without owning durable memory locally."
|
|
808
|
+
];
|
|
809
|
+
}
|
|
810
|
+
if (client === "cursor") {
|
|
811
|
+
return [
|
|
812
|
+
"Cursor reads the generated .mcp.json directly, so the hosted Snipara MCP surface is available after restart.",
|
|
813
|
+
"Use the companion CLI for explicit upload, planning, chunk lookup, doctor, and task commit workflows outside the editor UI."
|
|
814
|
+
];
|
|
815
|
+
}
|
|
816
|
+
if (client === "claude-desktop") {
|
|
817
|
+
return [
|
|
818
|
+
"Claude Desktop uses the generated desktop MCP config plus the same hosted Snipara tool contract.",
|
|
819
|
+
"Keep the companion CLI nearby for bootstrap, upload, and task summaries that are easier to run from a terminal."
|
|
820
|
+
];
|
|
821
|
+
}
|
|
822
|
+
return [
|
|
823
|
+
"The generated pack assumes a hosted-first Snipara setup with optional local companion workflows.",
|
|
824
|
+
"Use the companion CLI whenever your client does not expose an equivalent UX for planning, upload, doctor, or task commit flows."
|
|
825
|
+
];
|
|
826
|
+
}
|
|
827
|
+
function getCompanionCommands(answers) {
|
|
828
|
+
const projectIdFlag = answers.projectId ? ` --project-id ${answers.projectId}` : "";
|
|
829
|
+
const apiKeyFlag = answers.apiKeyType === "none" || answers.apiKey.includes("YOUR_") ? " --api-key <rlm_key>" : ` --api-key ${answers.apiKey}`;
|
|
830
|
+
const clientFlag = getCompanionInitClientFlag(answers.aiClient);
|
|
831
|
+
return [
|
|
832
|
+
{
|
|
833
|
+
title: "Bootstrap local companion",
|
|
834
|
+
command: `rlm-hook init${clientFlag}${projectIdFlag}${apiKeyFlag}`,
|
|
835
|
+
purpose: "Write local companion config for this workstation."
|
|
836
|
+
},
|
|
837
|
+
{
|
|
838
|
+
title: "Run doctor for this project",
|
|
839
|
+
command: "create-snipara doctor",
|
|
840
|
+
purpose: "Inspect local wiring, hosted reachability, and generated assets."
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
title: "Repair local setup",
|
|
844
|
+
command: "create-snipara repair",
|
|
845
|
+
purpose: "Rebuild config files, hooks, and the companion pack."
|
|
846
|
+
},
|
|
847
|
+
{
|
|
848
|
+
title: "Run a focused hosted query",
|
|
849
|
+
command: 'rlm-hook query --query "recent auth decisions" --max-tokens 4000',
|
|
850
|
+
purpose: "Pull ranked context before editing or reviewing code."
|
|
851
|
+
},
|
|
852
|
+
{
|
|
853
|
+
title: "Generate a hosted plan",
|
|
854
|
+
command: 'rlm-hook plan --query "Implement webhook retry hardening" --max-tokens 3000',
|
|
855
|
+
purpose: "Produce a structured execution plan against indexed project knowledge."
|
|
856
|
+
},
|
|
857
|
+
{
|
|
858
|
+
title: "Upload or refresh a doc",
|
|
859
|
+
command: "rlm-hook upload --path docs/local-playbook.md --file ./docs/local-playbook.md",
|
|
860
|
+
purpose: "Push local notes or docs into hosted Snipara context."
|
|
861
|
+
},
|
|
862
|
+
{
|
|
863
|
+
title: "Inspect a specific chunk",
|
|
864
|
+
command: "rlm-hook chunk get --chunk-id <chunk_id>",
|
|
865
|
+
purpose: "Open an exact chunk returned by references-based search."
|
|
866
|
+
},
|
|
867
|
+
{
|
|
868
|
+
title: "Bootstrap the session memory view",
|
|
869
|
+
command: "rlm-hook session-bootstrap",
|
|
870
|
+
purpose: "Load critical and daily memories at session start."
|
|
871
|
+
},
|
|
872
|
+
{
|
|
873
|
+
title: "Commit durable task memory",
|
|
874
|
+
command: 'rlm-hook task-commit --summary "Hardened webhook retries and updated tests" --category engineering --outcome completed --files src/api/webhooks.ts tests/webhooks.test.ts',
|
|
875
|
+
purpose: "Persist reusable task outcomes back into hosted memory."
|
|
876
|
+
}
|
|
877
|
+
];
|
|
878
|
+
}
|
|
879
|
+
async function createCompanionPack(answers) {
|
|
880
|
+
console.log(chalk.bold("\n\u{1F5C2}\uFE0F Generating Companion Pack\n"));
|
|
881
|
+
const packDir = path.join(process.cwd(), ".snipara", "companion");
|
|
882
|
+
const commands = getCompanionCommands(answers);
|
|
883
|
+
const guidance = getClientGuidance(answers.aiClient);
|
|
884
|
+
const detection = detectEnvironment();
|
|
885
|
+
fs.mkdirSync(packDir, { recursive: true });
|
|
886
|
+
const readme = `# Snipara Companion Pack
|
|
887
|
+
|
|
888
|
+
This project uses Snipara as a hosted-first context and memory layer with an optional local companion CLI.
|
|
889
|
+
|
|
890
|
+
## Install Profile
|
|
891
|
+
|
|
892
|
+
- Profile: \`${answers.profile}\`
|
|
893
|
+
- Project slug: \`${answers.projectSlug}\`
|
|
894
|
+
- Project id: \`${answers.projectId || "not-set"}\`
|
|
895
|
+
- AI client: \`${answers.aiClient}\`
|
|
896
|
+
- Installed local companion: \`${answers.installCompanion ? "yes" : "no"}\`
|
|
897
|
+
- Installed MCP server: \`${answers.installMcp ? "yes" : "no"}\`
|
|
898
|
+
- Installed runtime: \`${answers.installRuntime ? "yes" : "no"}\`
|
|
899
|
+
- Generated hooks: \`${answers.installHooks ? "yes" : "no"}\`
|
|
900
|
+
|
|
901
|
+
## Local Detection Snapshot
|
|
902
|
+
|
|
903
|
+
- Git repo: \`${detection.inGitRepo ? "yes" : "no"}\`
|
|
904
|
+
- .mcp.json present: \`${detection.files.mcpConfig ? "yes" : "no"}\`
|
|
905
|
+
- .env.local present: \`${detection.files.envLocal ? "yes" : "no"}\`
|
|
906
|
+
- Companion CLI on PATH: \`${detection.tooling.companionCli ? "yes" : "no"}\`
|
|
907
|
+
- Runtime CLI on PATH: \`${detection.tooling.runtimeCli ? "yes" : "no"}\`
|
|
908
|
+
|
|
909
|
+
## Client Guidance
|
|
910
|
+
|
|
911
|
+
${guidance.map((line) => `- ${line}`).join("\n")}
|
|
912
|
+
|
|
913
|
+
## Suggested Commands
|
|
914
|
+
|
|
915
|
+
${commands.map(
|
|
916
|
+
(item) => `### ${item.title}
|
|
917
|
+
|
|
918
|
+
\`${item.command}\`
|
|
919
|
+
|
|
920
|
+
${item.purpose}`
|
|
921
|
+
).join("\n\n")}
|
|
922
|
+
|
|
923
|
+
## Design Rule
|
|
924
|
+
|
|
925
|
+
- Durable memory, policy, and orchestration stay hosted in Snipara.
|
|
926
|
+
- The local companion is for capture, bootstrap, upload, exploration, doctor, repair, and explicit task summaries.
|
|
927
|
+
`;
|
|
928
|
+
const pack = {
|
|
929
|
+
generated_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
930
|
+
project_slug: answers.projectSlug,
|
|
931
|
+
project_id: answers.projectId || null,
|
|
932
|
+
ai_client: answers.aiClient,
|
|
933
|
+
profile: answers.profile,
|
|
934
|
+
llm_provider: answers.llmProvider || null,
|
|
935
|
+
commands,
|
|
936
|
+
guidance,
|
|
937
|
+
detection
|
|
938
|
+
};
|
|
939
|
+
fs.writeFileSync(path.join(packDir, "README.md"), readme + "\n");
|
|
940
|
+
fs.writeFileSync(path.join(packDir, "commands.json"), JSON.stringify(pack, null, 2) + "\n");
|
|
941
|
+
console.log(chalk.green(" \u2713 Created .snipara/companion/README.md"));
|
|
942
|
+
console.log(chalk.green(" \u2713 Created .snipara/companion/commands.json"));
|
|
943
|
+
}
|
|
631
944
|
async function runRlmInit() {
|
|
632
945
|
console.log(chalk.bold("\n\u{1F527} Running RLM-Runtime Configuration\n"));
|
|
633
|
-
console.log(
|
|
946
|
+
console.log(
|
|
947
|
+
chalk.dim(" Starting 'rlm init' - follow the prompts to configure execution environment...\n")
|
|
948
|
+
);
|
|
634
949
|
return new Promise((resolve) => {
|
|
635
950
|
const child = spawn("rlm", ["init"], {
|
|
636
951
|
stdio: "inherit",
|
|
@@ -642,7 +957,7 @@ async function runRlmInit() {
|
|
|
642
957
|
} else if (code !== null) {
|
|
643
958
|
console.log(chalk.yellow(`
|
|
644
959
|
\u26A0\uFE0F rlm init exited with code ${code}`));
|
|
645
|
-
console.log(chalk.dim(" You can run 'rlm init' manually later
|
|
960
|
+
console.log(chalk.dim(" You can run 'rlm init' manually later."));
|
|
646
961
|
}
|
|
647
962
|
resolve();
|
|
648
963
|
});
|
|
@@ -654,8 +969,86 @@ async function runRlmInit() {
|
|
|
654
969
|
});
|
|
655
970
|
});
|
|
656
971
|
}
|
|
972
|
+
async function runValidation(answers, includeHostedCheck) {
|
|
973
|
+
console.log(chalk.bold("\n\u{1FA7A} Running Validation\n"));
|
|
974
|
+
const checks = [];
|
|
975
|
+
const detection = detectEnvironment();
|
|
976
|
+
checks.push({
|
|
977
|
+
name: "project_slug",
|
|
978
|
+
status: answers.projectSlug ? "pass" : "fail",
|
|
979
|
+
detail: answers.projectSlug || "Project slug missing"
|
|
980
|
+
});
|
|
981
|
+
checks.push({
|
|
982
|
+
name: "mcp_config",
|
|
983
|
+
status: detection.files.mcpConfig ? "pass" : "warn",
|
|
984
|
+
detail: detection.files.mcpConfig ? ".mcp.json present" : ".mcp.json missing"
|
|
985
|
+
});
|
|
986
|
+
checks.push({
|
|
987
|
+
name: "companion_cli",
|
|
988
|
+
status: answers.installCompanion ? detection.tooling.companionCli ? "pass" : "fail" : "pass",
|
|
989
|
+
detail: answers.installCompanion && !detection.tooling.companionCli ? `${COMPANION_PACKAGE_NAME} not found on PATH` : "Companion CLI ready or not requested"
|
|
990
|
+
});
|
|
991
|
+
checks.push({
|
|
992
|
+
name: "runtime_cli",
|
|
993
|
+
status: answers.installRuntime ? detection.tooling.runtimeCli ? "pass" : "warn" : "pass",
|
|
994
|
+
detail: answers.installRuntime && !detection.tooling.runtimeCli ? "rlm CLI not found on PATH" : "Runtime CLI ready or not requested"
|
|
995
|
+
});
|
|
996
|
+
checks.push({
|
|
997
|
+
name: "companion_pack",
|
|
998
|
+
status: detection.files.sniparaCompanionPack ? "pass" : "warn",
|
|
999
|
+
detail: detection.files.sniparaCompanionPack ? ".snipara/companion present" : ".snipara/companion missing"
|
|
1000
|
+
});
|
|
1001
|
+
checks.push({
|
|
1002
|
+
name: "env_files",
|
|
1003
|
+
status: detection.files.env || detection.files.envLocal || detection.files.envExample ? "pass" : "warn",
|
|
1004
|
+
detail: detection.files.env || detection.files.envLocal || detection.files.envExample ? "Environment file present" : "No environment file found"
|
|
1005
|
+
});
|
|
1006
|
+
checks.push({
|
|
1007
|
+
name: "hooks",
|
|
1008
|
+
status: answers.installHooks && answers.aiClient === "claude-code" ? detection.files.claudeDir ? "pass" : "warn" : "pass",
|
|
1009
|
+
detail: answers.installHooks && answers.aiClient === "claude-code" ? detection.files.claudeDir ? "~/.claude detected" : "~/.claude missing" : "Hooks not requested"
|
|
1010
|
+
});
|
|
1011
|
+
if (includeHostedCheck && answers.apiKeyType !== "none" && !answers.apiKey.includes("YOUR_")) {
|
|
1012
|
+
checks.push(await testConnection(answers));
|
|
1013
|
+
} else if (includeHostedCheck) {
|
|
1014
|
+
checks.push({
|
|
1015
|
+
name: "hosted_connection",
|
|
1016
|
+
status: "warn",
|
|
1017
|
+
detail: "Skipped hosted connectivity test because no live API key was available"
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
1020
|
+
for (const check of checks) {
|
|
1021
|
+
const symbol = check.status === "pass" ? chalk.green("\u2713") : check.status === "warn" ? chalk.yellow("!") : chalk.red("\u2717");
|
|
1022
|
+
console.log(` ${symbol} ${check.name}: ${check.detail}`);
|
|
1023
|
+
}
|
|
1024
|
+
return {
|
|
1025
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1026
|
+
profile: answers.profile,
|
|
1027
|
+
checks
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
function writeDoctorReport(report) {
|
|
1031
|
+
const reportDir = path.join(process.cwd(), ".snipara", "companion");
|
|
1032
|
+
fs.mkdirSync(reportDir, { recursive: true });
|
|
1033
|
+
fs.writeFileSync(path.join(reportDir, "doctor.json"), JSON.stringify(report, null, 2) + "\n");
|
|
1034
|
+
console.log(chalk.green(" \u2713 Wrote .snipara/companion/doctor.json"));
|
|
1035
|
+
}
|
|
1036
|
+
function printDoctorReport(detection, report) {
|
|
1037
|
+
console.log(chalk.bold("\n\u{1FA7A} Snipara Doctor\n"));
|
|
1038
|
+
console.log(
|
|
1039
|
+
` Project slug: ${detection.existingConfig.slug || detection.projectSlug || "unknown"}`
|
|
1040
|
+
);
|
|
1041
|
+
console.log(` AI client: ${detection.aiClient}`);
|
|
1042
|
+
console.log(` In git repo: ${detection.inGitRepo ? "yes" : "no"}`);
|
|
1043
|
+
console.log(` Profile: ${report.profile}`);
|
|
1044
|
+
console.log("");
|
|
1045
|
+
for (const check of report.checks) {
|
|
1046
|
+
const symbol = check.status === "pass" ? chalk.green("\u2713") : check.status === "warn" ? chalk.yellow("!") : chalk.red("\u2717");
|
|
1047
|
+
console.log(` ${symbol} ${check.name}: ${check.detail}`);
|
|
1048
|
+
}
|
|
1049
|
+
console.log("");
|
|
1050
|
+
}
|
|
657
1051
|
async function testConnection(answers) {
|
|
658
|
-
console.log(chalk.bold("\n\u{1F50C} Testing Connection\n"));
|
|
659
1052
|
const spinner = ora("Connecting to Snipara API...").start();
|
|
660
1053
|
try {
|
|
661
1054
|
const response = await fetch(`${API_URL}/mcp/${answers.projectSlug}`, {
|
|
@@ -675,22 +1068,29 @@ async function testConnection(answers) {
|
|
|
675
1068
|
const data = await response.json();
|
|
676
1069
|
const toolCount = data.result?.tools?.length || 0;
|
|
677
1070
|
spinner.succeed(`Connected! ${toolCount} MCP tools available.`);
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
1071
|
+
return {
|
|
1072
|
+
name: "hosted_connection",
|
|
1073
|
+
status: "pass",
|
|
1074
|
+
detail: `Connected successfully (${toolCount} tools reported)`
|
|
1075
|
+
};
|
|
681
1076
|
}
|
|
1077
|
+
const errorText = await response.text();
|
|
1078
|
+
spinner.warn(`Connection returned ${response.status}`);
|
|
1079
|
+
return {
|
|
1080
|
+
name: "hosted_connection",
|
|
1081
|
+
status: "warn",
|
|
1082
|
+
detail: `HTTP ${response.status}: ${errorText.slice(0, 120)}`
|
|
1083
|
+
};
|
|
682
1084
|
} catch (error) {
|
|
683
1085
|
spinner.warn(`Could not connect: ${error}`);
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
console.log(chalk.dim(` Create your project at: ${BASE_URL}/dashboard
|
|
690
|
-
`));
|
|
1086
|
+
return {
|
|
1087
|
+
name: "hosted_connection",
|
|
1088
|
+
status: "warn",
|
|
1089
|
+
detail: "Could not reach Snipara API from this machine"
|
|
1090
|
+
};
|
|
691
1091
|
}
|
|
692
1092
|
}
|
|
693
|
-
function printSuccess(answers) {
|
|
1093
|
+
function printSuccess(answers, report) {
|
|
694
1094
|
console.log(
|
|
695
1095
|
chalk.green(`
|
|
696
1096
|
\u2554\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2557
|
|
@@ -700,70 +1100,153 @@ function printSuccess(answers) {
|
|
|
700
1100
|
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u2550\u255D
|
|
701
1101
|
`)
|
|
702
1102
|
);
|
|
703
|
-
console.log(chalk.bold("
|
|
1103
|
+
console.log(chalk.bold("Installed profile:\n"));
|
|
1104
|
+
console.log(` ${chalk.green("\u2713")} ${answers.profile}`);
|
|
1105
|
+
console.log(chalk.bold("\nWhat was installed:\n"));
|
|
704
1106
|
if (answers.installMcp) {
|
|
705
|
-
console.log(chalk.green(" \u2713 snipara-mcp") + chalk.dim(" - MCP server
|
|
1107
|
+
console.log(chalk.green(" \u2713 snipara-mcp") + chalk.dim(" - Hosted MCP server wiring"));
|
|
706
1108
|
}
|
|
707
1109
|
if (answers.installRuntime) {
|
|
708
|
-
console.log(chalk.green(" \u2713 rlm-runtime") + chalk.dim(" - Safe code execution
|
|
1110
|
+
console.log(chalk.green(" \u2713 rlm-runtime") + chalk.dim(" - Safe code execution"));
|
|
1111
|
+
}
|
|
1112
|
+
if (answers.installCompanion) {
|
|
1113
|
+
console.log(
|
|
1114
|
+
chalk.green(` \u2713 ${COMPANION_PACKAGE_NAME}`) + chalk.dim(" - Local companion workflows and hook bootstrap")
|
|
1115
|
+
);
|
|
709
1116
|
}
|
|
710
1117
|
if (answers.installHooks) {
|
|
711
|
-
console.log(chalk.green(" \u2713
|
|
1118
|
+
console.log(chalk.green(" \u2713 hooks") + chalk.dim(" - Local session lifecycle scripts"));
|
|
712
1119
|
}
|
|
713
|
-
|
|
714
|
-
|
|
1120
|
+
console.log(chalk.green(" \u2713 .snipara/companion") + chalk.dim(" - Local pack + doctor report"));
|
|
1121
|
+
const failedChecks = report.checks.filter((check) => check.status === "fail");
|
|
1122
|
+
const warnedChecks = report.checks.filter((check) => check.status === "warn");
|
|
1123
|
+
console.log(chalk.bold("\nNext steps:\n"));
|
|
1124
|
+
console.log(" 1. Restart your AI client if you installed or changed MCP config.");
|
|
1125
|
+
console.log(" 2. Open .snipara/companion/README.md for local commands and repair flow.");
|
|
1126
|
+
console.log(" 3. Run create-snipara doctor anytime you want a fresh local validation report.");
|
|
1127
|
+
if (answers.installCompanion) {
|
|
1128
|
+
console.log(
|
|
1129
|
+
" 4. Use rlm-hook query / plan / upload / task-commit for explicit local workflows."
|
|
1130
|
+
);
|
|
715
1131
|
}
|
|
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}`));
|
|
1132
|
+
if (answers.installRuntime && !answers.runRlmInit) {
|
|
1133
|
+
console.log(" 5. Run rlm init when you want to choose sandbox/docker/local execution.");
|
|
721
1134
|
}
|
|
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");
|
|
1135
|
+
if (failedChecks.length > 0 || warnedChecks.length > 0) {
|
|
1136
|
+
console.log(chalk.bold("\nValidation summary:\n"));
|
|
1137
|
+
for (const check of [...failedChecks, ...warnedChecks]) {
|
|
1138
|
+
const symbol = check.status === "fail" ? chalk.red("\u2717") : chalk.yellow("!");
|
|
1139
|
+
console.log(` ${symbol} ${check.name}: ${check.detail}`);
|
|
1140
|
+
}
|
|
1141
|
+
console.log(chalk.dim("\n Run create-snipara repair to rebuild local wiring.\n"));
|
|
733
1142
|
} else {
|
|
734
|
-
console.log(
|
|
735
|
-
|
|
736
|
-
|
|
1143
|
+
console.log(
|
|
1144
|
+
chalk.dim(
|
|
1145
|
+
"\n Validation passed cleanly. Doctor report saved in .snipara/companion/doctor.json\n"
|
|
1146
|
+
)
|
|
1147
|
+
);
|
|
737
1148
|
}
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
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"));
|
|
1149
|
+
console.log(chalk.dim(`Docs: ${BASE_URL}/docs`));
|
|
1150
|
+
console.log(chalk.dim("Support: hello@starbox-group.com\n"));
|
|
1151
|
+
}
|
|
1152
|
+
function commandExists(command) {
|
|
1153
|
+
try {
|
|
1154
|
+
execSync(`${command} --version`, { stdio: "ignore", shell: "/bin/sh" });
|
|
1155
|
+
return true;
|
|
1156
|
+
} catch {
|
|
1157
|
+
try {
|
|
1158
|
+
execSync(`command -v ${command}`, { stdio: "ignore", shell: "/bin/sh" });
|
|
1159
|
+
return true;
|
|
1160
|
+
} catch {
|
|
1161
|
+
return false;
|
|
754
1162
|
}
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
function detectEnvironment() {
|
|
1166
|
+
const existingConfig = getExistingConfig();
|
|
1167
|
+
return {
|
|
1168
|
+
cwd: process.cwd(),
|
|
1169
|
+
projectSlug: detectProjectSlug(),
|
|
1170
|
+
aiClient: detectAiClient(),
|
|
1171
|
+
inGitRepo: isGitRepo(),
|
|
1172
|
+
files: {
|
|
1173
|
+
mcpConfig: fs.existsSync(path.join(process.cwd(), ".mcp.json")),
|
|
1174
|
+
env: fs.existsSync(path.join(process.cwd(), ".env")),
|
|
1175
|
+
envLocal: fs.existsSync(path.join(process.cwd(), ".env.local")),
|
|
1176
|
+
envExample: fs.existsSync(path.join(process.cwd(), ".env.example")),
|
|
1177
|
+
claudeDir: fs.existsSync(path.join(homedir(), ".claude")),
|
|
1178
|
+
cursorDir: fs.existsSync(path.join(homedir(), ".cursor")),
|
|
1179
|
+
sniparaCompanionPack: fs.existsSync(path.join(process.cwd(), ".snipara", "companion"))
|
|
1180
|
+
},
|
|
1181
|
+
tooling: {
|
|
1182
|
+
node: commandExists("node"),
|
|
1183
|
+
npm: commandExists("npm"),
|
|
1184
|
+
pnpm: commandExists("pnpm"),
|
|
1185
|
+
python: commandExists("python3") || commandExists("python"),
|
|
1186
|
+
pip: commandExists("pip3") || commandExists("pip"),
|
|
1187
|
+
uv: commandExists("uv"),
|
|
1188
|
+
docker: commandExists("docker"),
|
|
1189
|
+
git: commandExists("git"),
|
|
1190
|
+
companionCli: commandExists("rlm-hook"),
|
|
1191
|
+
runtimeCli: commandExists("rlm")
|
|
1192
|
+
},
|
|
1193
|
+
existingConfig: {
|
|
1194
|
+
slug: existingConfig?.slug,
|
|
1195
|
+
projectId: existingConfig?.projectId,
|
|
1196
|
+
hasSniparaServer: existingConfig?.hasSniparaServer ?? false,
|
|
1197
|
+
hasRuntimeServer: existingConfig?.hasRuntimeServer ?? false
|
|
758
1198
|
}
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
function loadExistingAnswers(options) {
|
|
1202
|
+
const detection = detectEnvironment();
|
|
1203
|
+
const effectiveProfile = resolveMaintenanceProfile(detection, options.profile);
|
|
1204
|
+
const apiKey = options.teamKey || options.apiKey || readApiKeyFromEnvFiles() || "YOUR_API_KEY_HERE";
|
|
1205
|
+
const apiKeyType = options.teamKey ? "team" : options.apiKey || readApiKeyFromEnvFiles() ? "project" : effectiveProfile === "runtime-only" ? "none" : "none";
|
|
1206
|
+
return buildAnswersFromProfile({
|
|
1207
|
+
projectSlug: options.slug || detection.existingConfig.slug || detection.projectSlug || "my-project",
|
|
1208
|
+
projectId: options.projectId || detection.existingConfig.projectId || "",
|
|
1209
|
+
apiKey,
|
|
1210
|
+
apiKeyType,
|
|
1211
|
+
profile: effectiveProfile,
|
|
1212
|
+
aiClient: detection.aiClient,
|
|
1213
|
+
skipCompanion: options.withCompanion === true ? false : !detection.tooling.companionCli && effectiveProfile !== "hosted-companion" && effectiveProfile !== "full-stack",
|
|
1214
|
+
skipMcp: options.withMcp === true ? false : !detection.existingConfig.hasSniparaServer && effectiveProfile === "runtime-only",
|
|
1215
|
+
skipRuntime: options.withRuntime === true ? false : !detection.existingConfig.hasRuntimeServer && effectiveProfile !== "full-stack" && effectiveProfile !== "runtime-only",
|
|
1216
|
+
skipHooks: options.withHooks ? false : detection.aiClient !== "claude-code"
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
function resolveMaintenanceProfile(detection, explicitProfile) {
|
|
1220
|
+
if (explicitProfile && isInstallProfile(explicitProfile)) {
|
|
1221
|
+
return explicitProfile;
|
|
759
1222
|
}
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
1223
|
+
if (detection.existingConfig.hasRuntimeServer && detection.existingConfig.hasSniparaServer) {
|
|
1224
|
+
return detection.tooling.companionCli ? "full-stack" : "full-stack";
|
|
1225
|
+
}
|
|
1226
|
+
if (detection.existingConfig.hasSniparaServer && detection.tooling.companionCli) {
|
|
1227
|
+
return "hosted-companion";
|
|
1228
|
+
}
|
|
1229
|
+
if (detection.existingConfig.hasSniparaServer) {
|
|
1230
|
+
return "hosted-only";
|
|
1231
|
+
}
|
|
1232
|
+
return detection.tooling.runtimeCli ? "runtime-only" : "hosted-companion";
|
|
1233
|
+
}
|
|
1234
|
+
function readApiKeyFromEnvFiles() {
|
|
1235
|
+
const candidates = [".env.local", ".env"];
|
|
1236
|
+
for (const file of candidates) {
|
|
1237
|
+
const filePath = path.join(process.cwd(), file);
|
|
1238
|
+
if (!fs.existsSync(filePath)) continue;
|
|
1239
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
1240
|
+
const match = content.match(/^SNIPARA_API_KEY=(.+)$/m);
|
|
1241
|
+
if (match?.[1]) {
|
|
1242
|
+
return match[1].trim();
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
return null;
|
|
763
1246
|
}
|
|
764
1247
|
function detectProjectSlug() {
|
|
765
1248
|
try {
|
|
766
|
-
const remote = execSync("git remote get-url origin
|
|
1249
|
+
const remote = execSync("git remote get-url origin", {
|
|
767
1250
|
encoding: "utf-8"
|
|
768
1251
|
}).trim();
|
|
769
1252
|
const match = remote.match(/[/:]([^/]+?)(?:\.git)?$/);
|
|
@@ -774,6 +1257,16 @@ function detectProjectSlug() {
|
|
|
774
1257
|
}
|
|
775
1258
|
return path.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
776
1259
|
}
|
|
1260
|
+
function isGitRepo() {
|
|
1261
|
+
try {
|
|
1262
|
+
execSync("git rev-parse --is-inside-work-tree", {
|
|
1263
|
+
stdio: "ignore"
|
|
1264
|
+
});
|
|
1265
|
+
return true;
|
|
1266
|
+
} catch {
|
|
1267
|
+
return false;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
777
1270
|
function detectAiClient() {
|
|
778
1271
|
if (fs.existsSync(path.join(homedir(), ".claude")) || process.env.CLAUDE_CODE) {
|
|
779
1272
|
return "claude-code";
|
|
@@ -781,7 +1274,7 @@ function detectAiClient() {
|
|
|
781
1274
|
if (fs.existsSync(path.join(homedir(), ".cursor")) || process.env.CURSOR_EDITOR) {
|
|
782
1275
|
return "cursor";
|
|
783
1276
|
}
|
|
784
|
-
const claudeDesktopPath = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : path.join(homedir(), "AppData", "Roaming", "Claude");
|
|
1277
|
+
const claudeDesktopPath = process.platform === "darwin" ? path.join(homedir(), "Library", "Application Support", "Claude") : process.platform === "win32" ? path.join(homedir(), "AppData", "Roaming", "Claude") : path.join(homedir(), ".config", "claude");
|
|
785
1278
|
if (fs.existsSync(claudeDesktopPath)) {
|
|
786
1279
|
return "claude-desktop";
|
|
787
1280
|
}
|
|
@@ -789,33 +1282,37 @@ function detectAiClient() {
|
|
|
789
1282
|
}
|
|
790
1283
|
function detectPipCommand() {
|
|
791
1284
|
const commands = ["uv", "pip3", "pip"];
|
|
792
|
-
for (const
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
return cmd;
|
|
796
|
-
} catch {
|
|
1285
|
+
for (const command of commands) {
|
|
1286
|
+
if (commandExists(command)) {
|
|
1287
|
+
return command;
|
|
797
1288
|
}
|
|
798
1289
|
}
|
|
799
1290
|
return null;
|
|
800
1291
|
}
|
|
801
1292
|
function getExistingConfig() {
|
|
802
1293
|
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
|
-
}
|
|
1294
|
+
if (!fs.existsSync(mcpConfigPath)) {
|
|
1295
|
+
return null;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
const config = JSON.parse(fs.readFileSync(mcpConfigPath, "utf-8"));
|
|
1299
|
+
const sniparaServer = config.mcpServers?.snipara;
|
|
1300
|
+
const runtimeServer = config.mcpServers?.["rlm-runtime"];
|
|
1301
|
+
const match = sniparaServer?.url?.match(/\/mcp\/([^/]+)$/);
|
|
1302
|
+
return {
|
|
1303
|
+
slug: match?.[1],
|
|
1304
|
+
projectId: "",
|
|
1305
|
+
hasSniparaServer: Boolean(sniparaServer),
|
|
1306
|
+
hasRuntimeServer: Boolean(runtimeServer)
|
|
1307
|
+
};
|
|
1308
|
+
} catch {
|
|
1309
|
+
return {
|
|
1310
|
+
slug: void 0,
|
|
1311
|
+
projectId: "",
|
|
1312
|
+
hasSniparaServer: false,
|
|
1313
|
+
hasRuntimeServer: false
|
|
1314
|
+
};
|
|
817
1315
|
}
|
|
818
|
-
return null;
|
|
819
1316
|
}
|
|
820
1317
|
main().catch((error) => {
|
|
821
1318
|
console.error(chalk.red("Error:"), error.message);
|