create-snipara 1.3.3 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +157 -13
  2. package/dist/index.js +254 -31
  3. package/package.json +3 -3
package/README.md CHANGED
@@ -1,14 +1,25 @@
1
1
  # create-snipara
2
2
 
3
- One-command scaffolding and maintenance CLI for **Hosted MCP config + optional snipara-companion + optional RLM-Runtime**.
3
+ One-command onboarding and maintenance CLI for **Snipara Hosted MCP Context + Memory** with optional `snipara-companion` and optional RLM-Runtime.
4
4
 
5
5
  ```bash
6
6
  npx create-snipara
7
7
  ```
8
8
 
9
- `create-snipara` is onboarding and scaffolding. It is not a product connection surface. The product surfaces remain:
9
+ `create-snipara` is the simple onboarding package for the agent path. It creates or reuses auth, writes Hosted MCP config, and prepares the local project so an LLM client can use Snipara context and reviewed memory.
10
10
 
11
- - Hosted MCP for LLM agents
11
+ Use it for:
12
+
13
+ - Hosted MCP onboarding for Codex, Claude Code, Cursor, ChatGPT Desktop, and similar clients
14
+ - free account signup with reviewed Context + Memory
15
+ - local project wiring, `doctor`, `repair`, and `upgrade`
16
+ - GitHub repository sync approval when the current folder is a GitHub repo
17
+
18
+ Do not use it to choose business documents one by one. For local folders, mounted Drive/SharePoint exports, old offers, or PowerPoints, use `snipara-business`.
19
+
20
+ The product surfaces remain:
21
+
22
+ - Hosted MCP for LLM agents, context, and reviewed memory
12
23
  - HTTP API / SDK for apps, integrators, backends, and pipelines
13
24
 
14
25
  The CLI can wire local helpers around those surfaces:
@@ -31,10 +42,37 @@ The CLI now works around explicit install profiles:
31
42
 
32
43
  Default profile: `hosted-only`
33
44
 
45
+ ## Free Account Fit
46
+
47
+ Free onboarding is now **Context + Memory**, not Context-only:
48
+
49
+ - Context Free: one project and 100 context queries/month
50
+ - Memory Free: reviewed hosted memories for continuity across sessions
51
+ - Hosted MCP: direct connection for Claude Code, Codex, Cursor, ChatGPT Desktop, and other MCP clients
52
+
53
+ The CLI opens a browser device-flow login when the user does not already have a key, auto-provisions the free Context + Memory account when needed, then writes the returned project key into the local MCP config.
54
+
55
+ ## The Two Approval Paths
56
+
57
+ `create-snipara` deliberately splits setup into two user-friendly surfaces:
58
+
59
+ 1. **Terminal**
60
+ Connects the agent, writes local config, and prepares the workstation.
61
+ 2. **Browser**
62
+ Handles account login and GitHub approval because those source permissions must be chosen explicitly by the user.
63
+
64
+ That distinction matters:
65
+
66
+ - the terminal is for local setup
67
+ - the browser is for identity and source approval
68
+ - the MCP is for agent usage after setup, not for initial GitHub permission selection
69
+
34
70
  ## What It Does
35
71
 
36
72
  - detects local environment and project state
73
+ - opens the browser device-flow free Context + Memory account flow when needed
37
74
  - writes Hosted MCP configuration
75
+ - detects a GitHub remote and can open the Snipara GitHub App automation path
38
76
  - installs `snipara-companion` when the selected profile needs local helper workflows
39
77
  - installs optional `rlm-runtime`
40
78
  - configures `.mcp.json`
@@ -45,6 +83,17 @@ Default profile: `hosted-only`
45
83
  - optionally installs Claude Code hooks
46
84
  - supports `doctor`, `repair`, `upgrade`, and `print-config`
47
85
 
86
+ ## Product Boundary
87
+
88
+ Keep the package roles simple:
89
+
90
+ - `create-snipara` = connect the user to Hosted MCP Context + Memory
91
+ - `create-snipara --github` = connect the current GitHub repository through the browser approval flow
92
+ - `snipara-business` = prepare business context from local or mounted folders
93
+ - Hosted MCP = the runtime surface the LLM actually uses after setup
94
+
95
+ This avoids mixing repo sync, business document review, and agent runtime into one unclear workflow.
96
+
48
97
  ## Quick Start
49
98
 
50
99
  ```bash
@@ -56,9 +105,10 @@ The interactive flow now asks for:
56
105
  - install profile
57
106
  - project slug
58
107
  - optional project id
59
- - API key type and key when needed
108
+ - API key path: project key, team key, free signup, or add key later
60
109
  - AI client
61
110
  - whether hooks should be generated
111
+ - whether to open GitHub repository automation when a GitHub remote is detected
62
112
  - optional runtime provider config when `rlm-runtime` is installed
63
113
 
64
114
  ## Main Commands
@@ -70,6 +120,8 @@ npx create-snipara
70
120
  npx create-snipara init
71
121
  ```
72
122
 
123
+ Use this when the goal is "connect my LLM client to Snipara."
124
+
73
125
  ### Inspect local setup
74
126
 
75
127
  ```bash
@@ -112,6 +164,42 @@ npx create-snipara print-config
112
164
  npx create-snipara print-config --json
113
165
  ```
114
166
 
167
+ ## Source Selection Rules
168
+
169
+ ### GitHub repositories
170
+
171
+ For source-code repositories, let the user approve access in the browser:
172
+
173
+ ```bash
174
+ npx create-snipara --github
175
+ ```
176
+
177
+ The CLI detects the current repo and can prefill:
178
+
179
+ - `owner/repo`
180
+ - current branch
181
+ - optional path filter such as `docs/` or `README.md`
182
+
183
+ The browser flow is where the user:
184
+
185
+ - signs in to Snipara if needed
186
+ - approves the Snipara GitHub App
187
+ - chooses the GitHub account or organization
188
+ - chooses which repositories Snipara may read
189
+
190
+ This is the correct product boundary. Repository access is explicit, auditable, and revocable.
191
+
192
+ ### Local business folders
193
+
194
+ For business documents, do not ask the user to pick files through the MCP setup flow.
195
+ Instead:
196
+
197
+ 1. connect Hosted MCP with `create-snipara`
198
+ 2. materialize the relevant local or mounted folder
199
+ 3. run `snipara-business` to build a reviewable manifest
200
+
201
+ That keeps business truth review separate from agent onboarding.
202
+
115
203
  ## Common Flags
116
204
 
117
205
  ```bash
@@ -126,11 +214,16 @@ npx create-snipara --runtime-only
126
214
  # Non-interactive setup
127
215
  npx create-snipara -y --api-key rlm_xxx --slug my-project
128
216
 
217
+ # Open GitHub automation after setup when a GitHub remote is detected
218
+ npx create-snipara --github
219
+ npx create-snipara --github-path docs/
220
+
129
221
  # Skip selected layers
130
222
  npx create-snipara --skip-companion
131
223
  npx create-snipara --skip-mcp # Skip local snipara-mcp package if explicitly requested
132
224
  npx create-snipara --skip-runtime
133
225
  npx create-snipara --skip-hooks
226
+ npx create-snipara --skip-github
134
227
 
135
228
  # Force extra layers during maintenance
136
229
  npx create-snipara repair --with-hooks
@@ -141,7 +234,7 @@ npx create-snipara upgrade --with-runtime
141
234
 
142
235
  The intended split is:
143
236
 
144
- - `create-snipara`: install, inspect, repair, and upgrade the local project setup
237
+ - `create-snipara`: Context + Memory onboarding, auth, config, inspect, repair, and upgrade
145
238
  - `snipara-companion`: optional ongoing local runtime workflows through `rlm-hook`
146
239
  - `snipara-mcp`: local stdio bridge, client-package development, compatibility, and testing
147
240
  - `rlm-runtime`: optional local execution engine
@@ -181,15 +274,37 @@ Use this for dashboardless business-context imports after an LLM client has
181
274
  materialized Drive, Notion, Gmail, or local files into a folder. For source-code
182
275
  repositories, use the GitHub OAuth/code onboarding path instead.
183
276
 
277
+ ## GitHub Automation
278
+
279
+ When run inside a GitHub-backed repository, `create-snipara` can open the Snipara GitHub App flow with the detected `owner/repo` and branch already carried through:
280
+
281
+ ```bash
282
+ npx create-snipara --github
283
+ ```
284
+
285
+ The browser flow lets the user:
286
+
287
+ - install or update the Snipara GitHub App
288
+ - pick the detected repo
289
+ - create or reuse the matching Snipara project
290
+ - enable push sync
291
+ - start the first documentation sync
292
+
293
+ Hosted memory stays attached to the same Snipara account.
294
+
295
+ Recommended explanation to users:
296
+
297
+ > `create-snipara` connects your LLM from the terminal. GitHub sync is approved in the browser because GitHub requires the user to choose which account, organization, and repositories Snipara can read.
298
+
184
299
  ## API Key Requirements
185
300
 
186
- | Tooling | Snipara API key | LLM API key |
187
- | ------------------------------------ | -------------------- | ----------- |
188
- | Hosted MCP queries | Required | Not needed |
189
- | Memory / planning tools | Required | Not needed |
190
- | `snipara-companion` hosted workflows | Optional | Not needed |
191
- | `rlm-runtime` `execute_python` MCP | Not needed | Not needed |
192
- | `rlm run` / `rlm agent` CLI | Optional for context | Required |
301
+ | Tooling | Snipara API key | LLM API key |
302
+ | ------------------------------------ | -------------------------- | ----------- |
303
+ | Hosted MCP queries | Required | Not needed |
304
+ | Memory / planning tools | Required with memory scope | Not needed |
305
+ | `snipara-companion` hosted workflows | Optional | Not needed |
306
+ | `rlm-runtime` `execute_python` MCP | Not needed | Not needed |
307
+ | `rlm run` / `rlm agent` CLI | Optional for context | Required |
193
308
 
194
309
  ## Example Flows
195
310
 
@@ -199,12 +314,41 @@ repositories, use the GitHub OAuth/code onboarding path instead.
199
314
  npx create-snipara --profile hosted-only
200
315
  ```
201
316
 
317
+ Best when the user only wants Hosted MCP Context + Memory.
318
+
202
319
  ### Full local stack
203
320
 
204
321
  ```bash
205
322
  npx create-snipara --profile full-stack
206
323
  ```
207
324
 
325
+ Best when the user also wants `snipara-companion` and `rlm-runtime`.
326
+
327
+ ### Connect the current repository
328
+
329
+ ```bash
330
+ npx create-snipara --github
331
+ ```
332
+
333
+ Best when the current folder is a GitHub repository and the user wants code or docs sync through the GitHub App.
334
+
335
+ ### Connect the current repository with a path filter
336
+
337
+ ```bash
338
+ npx create-snipara --github --github-path docs/
339
+ ```
340
+
341
+ Best when the user wants a focused first sync instead of the whole repository.
342
+
343
+ ### Continue with business documents
344
+
345
+ ```bash
346
+ npx create-snipara
347
+ npx snipara-business init
348
+ ```
349
+
350
+ Best when the agent connection is done and the next step is business context onboarding from local or mounted folders.
351
+
208
352
  ### Repair after a broken workstation or new shell
209
353
 
210
354
  ```bash
@@ -223,7 +367,7 @@ npx create-snipara print-config --json
223
367
  When testing a locally packed tarball, use `npm exec --package` instead of `npx`, for example:
224
368
 
225
369
  ```bash
226
- npm exec --package ./create-snipara-1.3.3.tgz create-snipara -- --help
370
+ npm exec --package ./create-snipara-1.3.6.tgz create-snipara -- --help
227
371
  ```
228
372
 
229
373
  This avoids `npx` resolution issues with unpublished tarballs.
package/dist/index.js CHANGED
@@ -9,12 +9,15 @@ 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.3.3";
12
+ var VERSION = "1.3.6";
13
13
  var BASE_URL = "https://www.snipara.com";
14
14
  var API_URL = "https://api.snipara.com";
15
15
  var COMPANION_PACKAGE_NAME = "snipara-companion";
16
16
  var MCP_PACKAGE_NAME = "snipara-mcp";
17
17
  var RUNTIME_PACKAGE_NAME = "rlm-runtime[all]";
18
+ var DEVICE_CLIENT_ID = "create-snipara";
19
+ var DeviceAuthorizationAborted = class extends Error {
20
+ };
18
21
  function getCompanionInitClientFlag(aiClient) {
19
22
  if (aiClient === "cursor") {
20
23
  return " --client cursor";
@@ -27,7 +30,7 @@ function buildProgram() {
27
30
  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
31
  "--profile <profile>",
29
32
  "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
30
- ).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip local snipara-mcp package 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) => {
33
+ ).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip local snipara-mcp package installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").option("--github", "Open GitHub repository automation setup when a git remote is detected").option("--skip-github", "Skip the GitHub automation prompt").option("--github-path <path>", "Optional GitHub path filter for the initial repository sync").option("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting").action(async (options) => {
31
34
  printBanner();
32
35
  await runInit(options);
33
36
  });
@@ -58,7 +61,7 @@ function addSharedRootOptions(program) {
58
61
  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
62
  "--profile <profile>",
60
63
  "Install profile: hosted-only, hosted-companion, full-stack, runtime-only"
61
- ).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip local snipara-mcp package 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");
64
+ ).option("--skip-companion", "Skip local companion CLI installation").option("--skip-mcp", "Skip local snipara-mcp package installation").option("--skip-runtime", "Skip rlm-runtime installation").option("--skip-hooks", "Skip hook generation").option("--skip-test", "Skip hosted connection test").option("--github", "Open GitHub repository automation setup when a git remote is detected").option("--skip-github", "Skip the GitHub automation prompt").option("--github-path <path>", "Optional GitHub path filter for the initial repository sync").option("--runtime-only", "Install only RLM-Runtime").option("-y, --yes", "Accept defaults without prompting");
62
65
  }
63
66
  async function main() {
64
67
  const program = buildProgram();
@@ -83,6 +86,9 @@ async function runInit(options) {
83
86
  const validationReport = await runValidation(answers, !options.skipTest);
84
87
  writeDoctorReport(validationReport);
85
88
  printSuccess(answers, validationReport);
89
+ if (answers.configureGitHub && answers.githubRepo) {
90
+ await openGitHubAutomation(answers);
91
+ }
86
92
  if (answers.runRlmInit && answers.installRuntime) {
87
93
  await runRlmInit();
88
94
  }
@@ -119,20 +125,21 @@ async function runRepair(options) {
119
125
  async function runUpgrade(options) {
120
126
  const detection = detectEnvironment();
121
127
  const answers = loadExistingAnswers(options);
128
+ const upgradedAnswers = {
129
+ ...answers,
130
+ installMcp: options.withMcp || answers.installMcp,
131
+ installRuntime: options.withRuntime || answers.installRuntime
132
+ };
122
133
  console.log(chalk.bold("\n\u2B06\uFE0F Upgrading Local Components\n"));
123
134
  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
- );
135
+ await installPythonPackages(upgradedAnswers, true);
132
136
  }
133
137
  if (options.withCompanion || detection.tooling.companionCli || answers.installCompanion) {
134
138
  await installNodeCompanion(true);
135
139
  }
140
+ if (options.withMcp || options.withRuntime || detection.files.mcpConfig) {
141
+ await createMcpConfig(upgradedAnswers);
142
+ }
136
143
  if (options.withHooks || answers.installHooks) {
137
144
  await installClaudeCodeHooks({ ...answers, installHooks: true });
138
145
  }
@@ -169,12 +176,13 @@ function printBanner() {
169
176
  chalk.cyan(`
170
177
  \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
171
178
  \u2551 \u2551
172
- \u2551 ${chalk.bold("Snipara")} - Hosted MCP + API / SDK setup \u2551
179
+ \u2551 ${chalk.bold("Snipara")} - Context + Memory onboarding \u2551
173
180
  \u2551 \u2551
174
181
  \u2551 \u2022 Hosted MCP for LLM agents \u2551
182
+ \u2551 \u2022 Free account path with hosted context and reviewed memory \u2551
175
183
  \u2551 \u2022 API / SDK for product integrations \u2551
176
184
  \u2551 \u2022 Optional local companion and runtime helpers \u2551
177
- \u2551 \u2022 One CLI for scaffolding, doctor, repair, and upgrade \u2551
185
+ \u2551 \u2022 One CLI for auth, config, doctor, repair, and upgrade \u2551
178
186
  \u2551 \u2551
179
187
  \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
180
188
  `)
@@ -186,6 +194,14 @@ async function gatherConfiguration(options) {
186
194
  let detectedProjectId = "";
187
195
  const detectedClient = detectAiClient();
188
196
  const existingConfig = getExistingConfig();
197
+ const detectedGitRepo = detectGitRepository();
198
+ if (options.github && !detectedGitRepo) {
199
+ console.log(
200
+ chalk.yellow(
201
+ " \u26A0\uFE0F --github was requested, but no GitHub origin remote was detected in this folder."
202
+ )
203
+ );
204
+ }
189
205
  if (existingConfig) {
190
206
  console.log(chalk.dim(" Found existing .mcp.json configuration"));
191
207
  detectedSlug = existingConfig.slug || detectedSlug;
@@ -203,7 +219,10 @@ async function gatherConfiguration(options) {
203
219
  skipCompanion: !!options.skipCompanion,
204
220
  skipMcp: !!options.skipMcp,
205
221
  skipRuntime: !!options.skipRuntime,
206
- skipHooks: !!options.skipHooks
222
+ skipHooks: !!options.skipHooks,
223
+ configureGitHub: Boolean(options.github && detectedGitRepo && !options.skipGithub),
224
+ githubRepo: detectedGitRepo || void 0,
225
+ githubPath: options.githubPath
207
226
  });
208
227
  }
209
228
  const response = await prompts([
@@ -243,8 +262,9 @@ async function gatherConfiguration(options) {
243
262
  description: "Access to all projects in your team"
244
263
  },
245
264
  {
246
- title: "Sign up for free (100 queries/mo)",
247
- value: "signup"
265
+ title: "Create free Context + Memory account",
266
+ value: "signup",
267
+ description: "Hosted MCP with Context Free and reviewed Memory Free"
248
268
  },
249
269
  {
250
270
  title: "I'll add the API key later",
@@ -287,13 +307,27 @@ async function gatherConfiguration(options) {
287
307
  name: "installHooks",
288
308
  message: "Generate local hooks when supported?",
289
309
  initial: detectedClient === "claude-code"
310
+ },
311
+ {
312
+ type: (_, values) => detectedGitRepo && !options.skipGithub && values.profile !== "runtime-only" ? "confirm" : null,
313
+ name: "configureGitHub",
314
+ message: detectedGitRepo ? `Open GitHub automation setup for ${detectedGitRepo.fullName} after local config?` : "Open GitHub automation setup after local config?",
315
+ initial: true
316
+ },
317
+ {
318
+ type: (_, values) => values.configureGitHub && detectedGitRepo ? "text" : null,
319
+ name: "githubPath",
320
+ message: "GitHub path filter (optional, press Enter for Snipara defaults):",
321
+ initial: options.githubPath || ""
290
322
  }
291
323
  ]);
292
324
  let apiKey = options.teamKey || options.apiKey;
293
325
  let apiKeyType = response.profile === "runtime-only" ? "none" : options.teamKey ? "team" : options.apiKey ? "project" : "none";
326
+ let signupResult = null;
294
327
  if (!apiKey && response.profile !== "runtime-only") {
295
328
  if (response.apiKeyType === "signup") {
296
- apiKey = await handleSignup();
329
+ signupResult = await handleSignup();
330
+ apiKey = signupResult.apiKey;
297
331
  apiKeyType = "project";
298
332
  } else if (response.apiKeyType === "project" || response.apiKeyType === "team") {
299
333
  apiKey = response.apiKey;
@@ -304,8 +338,8 @@ async function gatherConfiguration(options) {
304
338
  }
305
339
  }
306
340
  const answers = buildAnswersFromProfile({
307
- projectSlug: response.projectSlug,
308
- projectId: response.projectId || "",
341
+ projectSlug: signupResult?.projectSlug || response.projectSlug,
342
+ projectId: signupResult?.projectId || response.projectId || "",
309
343
  apiKey: apiKey || "",
310
344
  apiKeyType,
311
345
  profile: response.profile || initialProfile,
@@ -313,7 +347,12 @@ async function gatherConfiguration(options) {
313
347
  skipCompanion: !!options.skipCompanion,
314
348
  skipMcp: !!options.skipMcp,
315
349
  skipRuntime: !!options.skipRuntime,
316
- skipHooks: !!options.skipHooks || !response.installHooks
350
+ skipHooks: !!options.skipHooks || !response.installHooks,
351
+ configureGitHub: Boolean(
352
+ (response.configureGitHub || options.github) && detectedGitRepo && !options.skipGithub
353
+ ),
354
+ githubRepo: detectedGitRepo || void 0,
355
+ githubPath: response.githubPath || options.githubPath
317
356
  });
318
357
  if (answers.installRuntime) {
319
358
  const runtimeResponse = await prompts([
@@ -440,29 +479,124 @@ function buildAnswersFromProfile(input) {
440
479
  installMcp: selected.installMcp && !input.skipMcp,
441
480
  installRuntime: selected.installRuntime && !input.skipRuntime,
442
481
  installHooks: selected.installHooks && !input.skipHooks,
443
- aiClient: input.aiClient
482
+ aiClient: input.aiClient,
483
+ configureGitHub: Boolean(input.configureGitHub && input.githubRepo),
484
+ githubRepo: input.githubRepo,
485
+ githubPath: input.githubPath
444
486
  };
445
487
  }
446
488
  async function handleSignup() {
447
- const signupUrl = `${BASE_URL}/signup`;
489
+ console.log(chalk.bold("\n\u{1F510} Browser Account Authorization\n"));
490
+ try {
491
+ return await runDeviceAuthorization();
492
+ } catch (error) {
493
+ if (error instanceof DeviceAuthorizationAborted) {
494
+ throw error;
495
+ }
496
+ console.log(
497
+ chalk.yellow(
498
+ ` \u26A0\uFE0F Browser authorization did not complete: ${error instanceof Error ? error.message : "unknown error"}`
499
+ )
500
+ );
501
+ console.log(chalk.dim(" Falling back to manual key capture.\n"));
502
+ return handleManualSignup();
503
+ }
504
+ }
505
+ async function handleManualSignup() {
506
+ const signupUrl = `${BASE_URL}/register?plan=free&source=create-snipara`;
448
507
  console.log(chalk.cyan(`
449
508
  Opening ${signupUrl} in your browser...
450
509
  `));
451
- try {
452
- const platform = process.platform;
453
- const openCommand = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
454
- execSync(`${openCommand} "${signupUrl}"`, { stdio: "ignore", shell: "/bin/sh" });
455
- } catch {
456
- console.log(chalk.dim(` Please open ${signupUrl} manually.
457
- `));
458
- }
510
+ openBrowser(signupUrl);
459
511
  const response = await prompts({
460
512
  type: "text",
461
513
  name: "apiKey",
462
514
  message: "Paste your Project API key when ready:",
463
515
  validate: (value) => value.startsWith("rlm_") ? true : "API key should start with rlm_"
464
516
  });
465
- return response.apiKey;
517
+ return { apiKey: response.apiKey };
518
+ }
519
+ async function runDeviceAuthorization() {
520
+ const codeResponse = await postJson(`${BASE_URL}/api/oauth/device/code`, {
521
+ client_id: DEVICE_CLIENT_ID,
522
+ scope: "mcp:read mcp:write",
523
+ auto_provision: true
524
+ });
525
+ if (codeResponse.error) {
526
+ throw new Error(codeResponse.error_description || codeResponse.error);
527
+ }
528
+ console.log(
529
+ chalk.cyan(` Opening ${codeResponse.verification_uri_complete} in your browser...`)
530
+ );
531
+ console.log(chalk.dim(` Device code: ${codeResponse.user_code}`));
532
+ openBrowser(codeResponse.verification_uri_complete);
533
+ const spinner = ora("Waiting for Snipara authorization in the browser...").start();
534
+ const startedAt = Date.now();
535
+ let intervalMs = Math.max(1, codeResponse.interval || 5) * 1e3;
536
+ const expiresAt = startedAt + Math.max(60, codeResponse.expires_in || 900) * 1e3;
537
+ while (Date.now() < expiresAt) {
538
+ await sleep(intervalMs);
539
+ const tokenResponse = await postJson(
540
+ `${BASE_URL}/api/oauth/device/token`,
541
+ {
542
+ grant_type: "urn:ietf:params:oauth:grant-type:device_code",
543
+ device_code: codeResponse.device_code,
544
+ client_id: DEVICE_CLIENT_ID
545
+ },
546
+ true
547
+ );
548
+ if (!tokenResponse.error) {
549
+ if (!tokenResponse.api_key) {
550
+ throw new Error("Device flow completed but no project API key was returned");
551
+ }
552
+ spinner.succeed(
553
+ tokenResponse.project_name ? `Authorized ${tokenResponse.project_name}` : "Authorized Snipara account"
554
+ );
555
+ return {
556
+ apiKey: tokenResponse.api_key,
557
+ projectId: tokenResponse.project_id,
558
+ projectSlug: tokenResponse.project_slug
559
+ };
560
+ }
561
+ if (tokenResponse.error === "authorization_pending") {
562
+ continue;
563
+ }
564
+ if (tokenResponse.error === "slow_down") {
565
+ intervalMs += 5e3;
566
+ continue;
567
+ }
568
+ if (tokenResponse.error === "access_denied") {
569
+ throw new DeviceAuthorizationAborted("Authorization denied in browser");
570
+ }
571
+ throw new Error(tokenResponse.error_description || tokenResponse.error);
572
+ }
573
+ spinner.fail("Authorization expired");
574
+ throw new Error("device code expired");
575
+ }
576
+ async function postJson(url, body, allowErrorResponse = false) {
577
+ const response = await fetch(url, {
578
+ method: "POST",
579
+ headers: { "Content-Type": "application/json" },
580
+ body: JSON.stringify(body)
581
+ });
582
+ const data = await response.json().catch(() => ({}));
583
+ if (!response.ok && !(allowErrorResponse && data.error)) {
584
+ throw new Error(data.error_description || data.error || `HTTP ${response.status}`);
585
+ }
586
+ return data;
587
+ }
588
+ function openBrowser(url) {
589
+ try {
590
+ const platform = process.platform;
591
+ const openCommand = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
592
+ execSync(`${openCommand} "${url}"`, { stdio: "ignore", shell: "/bin/sh" });
593
+ } catch {
594
+ console.log(chalk.dim(` Please open ${url} manually.
595
+ `));
596
+ }
597
+ }
598
+ function sleep(ms) {
599
+ return new Promise((resolve) => setTimeout(resolve, ms));
466
600
  }
467
601
  async function installPythonPackages(answers, upgrade) {
468
602
  console.log(chalk.bold(`
@@ -987,6 +1121,16 @@ Apply only after reviewing the manifest:
987
1121
 
988
1122
  For source-code repositories, use the GitHub OAuth/code onboarding path instead of \`onboard-folder\`.
989
1123
 
1124
+ ## GitHub Repository Automation
1125
+
1126
+ ${answers.githubRepo ? `Detected repository: \`${answers.githubRepo.fullName}\` on branch \`${answers.githubRepo.branch}\`.
1127
+
1128
+ Open the Snipara GitHub App setup and connect this repository:
1129
+
1130
+ \`${buildGitHubAutomationUrl(answers)}\`
1131
+
1132
+ After connection, Snipara creates or reuses the repository project, enables push sync, starts the first documentation sync, and keeps hosted memory available through the same account.` : `No GitHub repository was detected for this folder. Run \`npx create-snipara --github\` from a GitHub-backed git repository to open the repo automation path.`}
1133
+
990
1134
  ## Suggested Commands
991
1135
 
992
1136
  ${commands.map(
@@ -1011,6 +1155,8 @@ ${item.purpose}`
1011
1155
  ai_client: answers.aiClient,
1012
1156
  profile: answers.profile,
1013
1157
  llm_provider: answers.llmProvider || null,
1158
+ github_repository: answers.githubRepo || null,
1159
+ github_automation_url: answers.githubRepo ? buildGitHubAutomationUrl(answers) : null,
1014
1160
  commands,
1015
1161
  guidance,
1016
1162
  detection
@@ -1048,6 +1194,36 @@ async function runRlmInit() {
1048
1194
  });
1049
1195
  });
1050
1196
  }
1197
+ async function openGitHubAutomation(answers) {
1198
+ if (!answers.githubRepo) {
1199
+ return;
1200
+ }
1201
+ const url = buildGitHubAutomationUrl(answers);
1202
+ console.log(chalk.bold("\n\u{1F419} GitHub Automation\n"));
1203
+ console.log(
1204
+ ` Opening Snipara GitHub setup for ${chalk.cyan(answers.githubRepo.fullName)} on branch ${chalk.cyan(
1205
+ answers.githubRepo.branch
1206
+ )}.`
1207
+ );
1208
+ console.log(chalk.dim(` ${url}
1209
+ `));
1210
+ openBrowser(url);
1211
+ }
1212
+ function buildGitHubAutomationUrl(answers) {
1213
+ const url = new URL("/dashboard/projects/connect", BASE_URL);
1214
+ url.searchParams.set("source", "create-snipara");
1215
+ if (answers.githubRepo) {
1216
+ url.searchParams.set("repo", answers.githubRepo.fullName);
1217
+ url.searchParams.set("branch", answers.githubRepo.branch);
1218
+ }
1219
+ if (answers.githubPath) {
1220
+ url.searchParams.set("path", answers.githubPath);
1221
+ }
1222
+ if (answers.projectSlug) {
1223
+ url.searchParams.set("project_slug", answers.projectSlug);
1224
+ }
1225
+ return url.toString();
1226
+ }
1051
1227
  async function runValidation(answers, includeHostedCheck) {
1052
1228
  console.log(chalk.bold("\n\u{1FA7A} Running Validation\n"));
1053
1229
  const checks = [];
@@ -1229,6 +1405,11 @@ function printSuccess(answers, report) {
1229
1405
  if (answers.installRuntime && !answers.runRlmInit) {
1230
1406
  printNextStep("Run rlm init when you want to choose sandbox/docker/local execution.");
1231
1407
  }
1408
+ if (answers.githubRepo) {
1409
+ printNextStep(
1410
+ `Connect GitHub automation for ${answers.githubRepo.fullName} from the generated Snipara link.`
1411
+ );
1412
+ }
1232
1413
  if (failedChecks.length > 0 || warnedChecks.length > 0) {
1233
1414
  console.log(chalk.bold("\nValidation summary:\n"));
1234
1415
  for (const check of [...failedChecks, ...warnedChecks]) {
@@ -1354,6 +1535,48 @@ function detectProjectSlug() {
1354
1535
  }
1355
1536
  return path.basename(process.cwd()).toLowerCase().replace(/[^a-z0-9-]/g, "-");
1356
1537
  }
1538
+ function detectGitRepository() {
1539
+ try {
1540
+ const remoteUrl = execSync("git remote get-url origin", {
1541
+ encoding: "utf-8"
1542
+ }).trim();
1543
+ const match = remoteUrl.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/) || remoteUrl.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
1544
+ if (!match) {
1545
+ return null;
1546
+ }
1547
+ const owner = match[1];
1548
+ const repo = match[2];
1549
+ const branch = detectGitBranch();
1550
+ return {
1551
+ owner,
1552
+ repo,
1553
+ fullName: `${owner}/${repo}`,
1554
+ branch,
1555
+ remoteUrl
1556
+ };
1557
+ } catch {
1558
+ return null;
1559
+ }
1560
+ }
1561
+ function detectGitBranch() {
1562
+ try {
1563
+ const branch = execSync("git branch --show-current", {
1564
+ encoding: "utf-8"
1565
+ }).trim();
1566
+ if (branch) {
1567
+ return branch;
1568
+ }
1569
+ } catch {
1570
+ }
1571
+ try {
1572
+ const branch = execSync("git rev-parse --abbrev-ref HEAD", {
1573
+ encoding: "utf-8"
1574
+ }).trim();
1575
+ return branch && branch !== "HEAD" ? branch : "main";
1576
+ } catch {
1577
+ return "main";
1578
+ }
1579
+ }
1357
1580
  function isGitRepo() {
1358
1581
  try {
1359
1582
  execSync("git rev-parse --is-inside-work-tree", {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "create-snipara",
3
- "version": "1.3.3",
4
- "description": "Scaffold hosted Snipara MCP config plus optional local helper and runtime workflows",
3
+ "version": "1.3.6",
4
+ "description": "Onboard Snipara Hosted MCP Context + Memory with auth plus optional local helper and runtime workflows",
5
5
  "main": "dist/index.js",
6
6
  "type": "module",
7
7
  "bin": {
@@ -35,7 +35,7 @@
35
35
  "type": "git",
36
36
  "url": "git+https://github.com/Snipara/create-snipara.git"
37
37
  },
38
- "homepage": "https://snipara.com/docs/getting-started",
38
+ "homepage": "https://snipara.com/docs/integration/create-snipara",
39
39
  "dependencies": {
40
40
  "chalk": "^5.3.0",
41
41
  "commander": "^12.1.0",