@synap-core/cli 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +250 -0
  2. package/dist/commands/connect.d.ts +11 -0
  3. package/dist/commands/connect.js +96 -0
  4. package/dist/commands/connect.js.map +1 -0
  5. package/dist/commands/finish.d.ts +16 -0
  6. package/dist/commands/finish.js +82 -0
  7. package/dist/commands/finish.js.map +1 -0
  8. package/dist/commands/init.d.ts +21 -0
  9. package/dist/commands/init.js +865 -0
  10. package/dist/commands/init.js.map +1 -0
  11. package/dist/commands/security-audit.d.ts +12 -0
  12. package/dist/commands/security-audit.js +100 -0
  13. package/dist/commands/security-audit.js.map +1 -0
  14. package/dist/commands/status.d.ts +6 -0
  15. package/dist/commands/status.js +216 -0
  16. package/dist/commands/status.js.map +1 -0
  17. package/dist/commands/update.d.ts +6 -0
  18. package/dist/commands/update.js +34 -0
  19. package/dist/commands/update.js.map +1 -0
  20. package/dist/index.d.ts +16 -0
  21. package/dist/index.js +138 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/lib/auth.d.ts +57 -0
  24. package/dist/lib/auth.js +322 -0
  25. package/dist/lib/auth.js.map +1 -0
  26. package/dist/lib/hardening.d.ts +18 -0
  27. package/dist/lib/hardening.js +203 -0
  28. package/dist/lib/hardening.js.map +1 -0
  29. package/dist/lib/openclaw.d.ts +28 -0
  30. package/dist/lib/openclaw.js +106 -0
  31. package/dist/lib/openclaw.js.map +1 -0
  32. package/dist/lib/pod.d.ts +91 -0
  33. package/dist/lib/pod.js +305 -0
  34. package/dist/lib/pod.js.map +1 -0
  35. package/dist/lib/seed.d.ts +13 -0
  36. package/dist/lib/seed.js +135 -0
  37. package/dist/lib/seed.js.map +1 -0
  38. package/dist/lib/templates.d.ts +11 -0
  39. package/dist/lib/templates.js +13 -0
  40. package/dist/lib/templates.js.map +1 -0
  41. package/dist/templates/agent-os.json +3090 -0
  42. package/dist/utils/logger.d.ts +11 -0
  43. package/dist/utils/logger.js +31 -0
  44. package/dist/utils/logger.js.map +1 -0
  45. package/package.json +45 -0
@@ -0,0 +1,865 @@
1
+ /**
2
+ * synap init
3
+ *
4
+ * Three paths based on environment detection:
5
+ * A: OpenClaw found → connect mode (primary funnel, 250K users)
6
+ * B: Server, no OpenClaw → bundle mode (fresh setup)
7
+ * C: Laptop/desktop → need hosting
8
+ */
9
+ import prompts from "prompts";
10
+ import ora from "ora";
11
+ import chalk from "chalk";
12
+ import { execSync } from "child_process";
13
+ import { log, banner } from "../utils/logger.js";
14
+ import { detectOpenClaw, readOpenClawConfig, writeOpenClawConfig, setConfigValue, } from "../lib/openclaw.js";
15
+ import { runSecurityChecks, computeScore } from "../lib/hardening.js";
16
+ import { checkPodHealth, setupAgent, setupAgentViaCp, provisionUserOnPod, installSynapSkill, enableOpenClawAddonManaged, saveLocalPodConfig, checkServerResources, startOpenClawOnServer, getLocalPodConfig, } from "../lib/pod.js";
17
+ import { seedAgentEntities } from "../lib/seed.js";
18
+ import { login, isLoggedIn, listPods, getStoredToken, waitForPodCallback } from "../lib/auth.js";
19
+ export async function init(opts) {
20
+ banner();
21
+ // ── Auto-detect environment ─────────────────────────────────────────────
22
+ const oc = detectOpenClaw();
23
+ const isServer = detectServer();
24
+ if (oc.found) {
25
+ log.info(`OpenClaw detected${oc.version ? ` v${oc.version}` : ""} — connect mode`);
26
+ await pathA(opts, oc);
27
+ }
28
+ else if (isServer) {
29
+ log.info("Server detected, no OpenClaw — fresh setup mode");
30
+ await pathB(opts);
31
+ }
32
+ else {
33
+ log.info("Running on desktop — hosting mode");
34
+ await pathC(opts);
35
+ }
36
+ }
37
+ // =============================================================================
38
+ // PATH A: Existing OpenClaw (primary funnel)
39
+ // =============================================================================
40
+ async function pathA(opts, oc) {
41
+ // ── Report ──────────────────────────────────────────────────────────────
42
+ log.heading("Step 1: OpenClaw Detected");
43
+ log.success(`Version: ${oc.version ?? "unknown"}`);
44
+ log.success(`Gateway: ${oc.gatewayRunning ? "running" : "stopped"} (port ${oc.gatewayPort ?? 18789})`);
45
+ // ── Security ────────────────────────────────────────────────────────────
46
+ if (!opts.skipSecurity) {
47
+ await securityStep(oc.version);
48
+ }
49
+ // ── Pod ─────────────────────────────────────────────────────────────────
50
+ const podUrl = await podChoiceStep(opts);
51
+ if (!podUrl)
52
+ return;
53
+ // ── Connect ─────────────────────────────────────────────────────────────
54
+ const apiKey = await connectStep(podUrl, opts, true);
55
+ if (!apiKey)
56
+ return;
57
+ // ── Skill ───────────────────────────────────────────────────────────────
58
+ await skillStep(true);
59
+ // ── Seed ────────────────────────────────────────────────────────────────
60
+ await seedStep(podUrl, apiKey, oc);
61
+ // ── IS ──────────────────────────────────────────────────────────────────
62
+ if (!opts.skipIs) {
63
+ await isStep(podUrl, apiKey, true);
64
+ }
65
+ printSummary(podUrl, true);
66
+ }
67
+ // =============================================================================
68
+ // PATH B: Fresh server (no OpenClaw)
69
+ // =============================================================================
70
+ async function pathB(opts) {
71
+ log.heading("Step 1: Server Setup");
72
+ // Resource check
73
+ const resources = checkServerResources();
74
+ log.info(`RAM: ${resources.ramTotal}MB total, ${resources.ramFree}MB free`);
75
+ log.info(`Disk: ${resources.diskFree} free`);
76
+ if (resources.ramFree < 1500) {
77
+ log.warn("Low RAM — Synap + OpenClaw need ~1.5GB. Performance may be affected.");
78
+ }
79
+ // What to install
80
+ const { installChoice } = await prompts({
81
+ type: "select",
82
+ name: "installChoice",
83
+ message: "What would you like to set up?",
84
+ choices: [
85
+ {
86
+ title: "Synap pod + OpenClaw (full stack)",
87
+ description: "Recommended — everything on this server",
88
+ value: "bundle",
89
+ },
90
+ {
91
+ title: "Synap pod only",
92
+ description: "Add OpenClaw later",
93
+ value: "pod-only",
94
+ },
95
+ ],
96
+ });
97
+ if (!installChoice)
98
+ return;
99
+ // Install pod
100
+ const podUrl = await podInstallLocalStep(opts);
101
+ if (!podUrl)
102
+ return;
103
+ // Get the API key once
104
+ const apiKey = await connectStep(podUrl, opts, false);
105
+ if (!apiKey)
106
+ return;
107
+ // If bundle mode, start the OpenClaw Docker container using the key we just got
108
+ if (installChoice === "bundle") {
109
+ const spinner = ora("Starting OpenClaw container...").start();
110
+ try {
111
+ const localConfig = getLocalPodConfig();
112
+ startOpenClawOnServer(apiKey, localConfig?.agentUserId ?? "", localConfig?.workspaceId ?? "", podUrl);
113
+ spinner.succeed("OpenClaw started and healthy");
114
+ }
115
+ catch (err) {
116
+ spinner.fail(err instanceof Error ? err.message : String(err));
117
+ log.dim("Start manually: docker compose --profile openclaw up -d openclaw");
118
+ }
119
+ }
120
+ await skillStep(installChoice === "bundle");
121
+ // Detect OpenClaw after potential install
122
+ const oc = detectOpenClaw();
123
+ await seedStep(podUrl, apiKey, oc);
124
+ if (!opts.skipIs) {
125
+ await isStep(podUrl, apiKey, oc.found);
126
+ }
127
+ printSummary(podUrl, installChoice === "bundle");
128
+ }
129
+ // =============================================================================
130
+ // PATH C: Laptop/desktop user
131
+ // =============================================================================
132
+ async function pathC(opts) {
133
+ log.heading("Step 1: Connect to Your Pod");
134
+ const { hostChoice } = await prompts({
135
+ type: "select",
136
+ name: "hostChoice",
137
+ message: "How do you want to connect?",
138
+ choices: [
139
+ {
140
+ title: "Login to Synap (managed pods)",
141
+ description: "Sign in via browser — auto-detect your pods",
142
+ value: "login",
143
+ },
144
+ {
145
+ title: "Connect to an existing pod (enter URL)",
146
+ description: "Self-hosted or managed — enter the URL directly",
147
+ value: "existing",
148
+ },
149
+ {
150
+ title: "I don't have a pod yet",
151
+ value: "none",
152
+ },
153
+ ],
154
+ });
155
+ if (hostChoice === "login") {
156
+ const podResult = await loginAndSelectPod();
157
+ if (!podResult)
158
+ return;
159
+ await connectExistingPod(podResult.url, opts, "managed", podResult.podId);
160
+ return;
161
+ }
162
+ if (hostChoice === "none") {
163
+ const { createChoice } = await prompts({
164
+ type: "select",
165
+ name: "createChoice",
166
+ message: "Create a pod:",
167
+ choices: [
168
+ {
169
+ title: "Managed by Synap — €15/mo",
170
+ description: "We host it, zero ops",
171
+ value: "managed",
172
+ },
173
+ {
174
+ title: "Self-hosted on my VPS — FREE",
175
+ value: "vps",
176
+ },
177
+ ],
178
+ });
179
+ if (createChoice === "managed") {
180
+ log.blank();
181
+ log.info("Opening synap.live to provision your pod...");
182
+ log.info(chalk.dim("(Waiting up to 5 minutes for provisioning to complete)"));
183
+ log.blank();
184
+ const spinner = ora("Waiting for pod provisioning...").start();
185
+ const result = await waitForPodCallback();
186
+ if (result) {
187
+ spinner.succeed(`Pod provisioned at ${chalk.cyan(result.podUrl)}`);
188
+ await connectExistingPod(result.podUrl, opts, "managed");
189
+ }
190
+ else {
191
+ spinner.fail("Pod provisioning timed out or was cancelled.");
192
+ log.blank();
193
+ log.info("You can resume at any time by running: " + chalk.cyan("synap init"));
194
+ log.info("Or connect manually: " + chalk.cyan(`synap connect --pod-url <your-pod-url>`));
195
+ }
196
+ }
197
+ else if (createChoice === "vps") {
198
+ log.blank();
199
+ log.info("SSH into your server and run:");
200
+ console.log(chalk.cyan("\n curl -fsSL https://raw.githubusercontent.com/Synap-core/backend/main/install.sh | bash\n"));
201
+ log.info("Then on that server: npx @synap/cli init");
202
+ }
203
+ return;
204
+ }
205
+ if (hostChoice === "existing") {
206
+ const { url } = await prompts({
207
+ type: "text",
208
+ name: "url",
209
+ message: "Pod URL:",
210
+ initial: "https://pod.synap.live",
211
+ });
212
+ if (!url)
213
+ return;
214
+ const spinner = ora("Checking pod health...").start();
215
+ const status = await checkPodHealth(url);
216
+ if (!status.healthy) {
217
+ spinner.fail(`Pod not reachable at ${url}`);
218
+ return;
219
+ }
220
+ spinner.succeed(`Pod healthy at ${url}`);
221
+ // Detect if self-hosted or managed by checking URL
222
+ const isSynapLive = url.includes("synap.live");
223
+ await connectExistingPod(url, opts, isSynapLive ? "managed" : "self-hosted");
224
+ }
225
+ }
226
+ /**
227
+ * Connect to an existing pod — handles both self-hosted and managed.
228
+ */
229
+ async function connectExistingPod(podUrl, opts, podType, podId) {
230
+ // Check for local OpenClaw
231
+ const oc = detectOpenClaw();
232
+ if (oc.found) {
233
+ log.success(`OpenClaw detected${oc.version ? ` v${oc.version}` : ""}`);
234
+ if (!opts.skipSecurity)
235
+ await securityStep(oc.version);
236
+ }
237
+ // Get API key
238
+ const apiKey = await connectStep(podUrl, opts, oc.found, podId);
239
+ if (!apiKey)
240
+ return;
241
+ // OpenClaw handling
242
+ if (!oc.found) {
243
+ log.heading("OpenClaw");
244
+ const { ocChoice } = await prompts({
245
+ type: "select",
246
+ name: "ocChoice",
247
+ message: "OpenClaw not detected locally. What would you like to do?",
248
+ choices: [
249
+ {
250
+ title: "Enable OpenClaw on my pod server (free addon)",
251
+ description: "Runs alongside your pod via Docker — zero extra cost",
252
+ value: "addon",
253
+ },
254
+ {
255
+ title: "Install OpenClaw on this computer",
256
+ description: "npm i -g openclaw",
257
+ value: "local",
258
+ },
259
+ { title: "Skip OpenClaw for now", value: "skip" },
260
+ ],
261
+ });
262
+ if (ocChoice === "addon") {
263
+ if (podType === "self-hosted") {
264
+ log.blank();
265
+ log.info("SSH into your pod server and run:");
266
+ log.blank();
267
+ console.log(chalk.cyan(" cd /srv/synap && ./deploy/setup-openclaw.sh"));
268
+ log.blank();
269
+ log.info("This will start OpenClaw as a Docker addon on your pod.");
270
+ log.info("Then re-run: synap init --pod-url " + podUrl);
271
+ }
272
+ else {
273
+ // Managed pod — activate via CP (requires user session, not PROVISIONING_TOKEN)
274
+ log.info("Activating OpenClaw addon on your managed pod...");
275
+ const creds = getStoredToken();
276
+ try {
277
+ if (!creds)
278
+ throw new Error("Not logged in to CP");
279
+ await enableOpenClawAddonManaged(creds.token, podUrl);
280
+ log.success("OpenClaw addon provisioning started — may take a minute");
281
+ }
282
+ catch (err) {
283
+ const msg = err instanceof Error ? err.message : String(err);
284
+ if (msg.includes("422") || msg.includes("provisioned pod server")) {
285
+ // Pod doesn't have a managed server — OpenClaw can't run server-side
286
+ log.blank();
287
+ log.info("Your pod doesn't have a managed server for addons.");
288
+ log.info("Install OpenClaw locally instead:");
289
+ log.dim(" npm i -g openclaw && openclaw onboard");
290
+ log.info("Then re-run: " + chalk.cyan(`synap init --pod-url ${podUrl}`));
291
+ }
292
+ else {
293
+ log.warn("Could not activate OpenClaw automatically.");
294
+ log.dim(msg);
295
+ log.info("Enable it from: https://synap.live/account/pods");
296
+ }
297
+ }
298
+ }
299
+ }
300
+ else if (ocChoice === "local") {
301
+ log.blank();
302
+ log.info("Install OpenClaw:");
303
+ log.dim(" npm i -g openclaw && openclaw onboard");
304
+ log.info("Then re-run: synap init --pod-url " + podUrl);
305
+ return;
306
+ }
307
+ }
308
+ // Install skill + seed (if OpenClaw available)
309
+ const ocAfter = detectOpenClaw();
310
+ if (ocAfter.found) {
311
+ await skillStep(true);
312
+ await seedStep(podUrl, apiKey, ocAfter);
313
+ if (!opts.skipIs)
314
+ await isStep(podUrl, apiKey, true);
315
+ }
316
+ else {
317
+ log.blank();
318
+ log.info("Pod connected. Once OpenClaw is running, install the skill:");
319
+ log.dim(" openclaw skills install synap");
320
+ }
321
+ printSummary(podUrl, ocAfter.found);
322
+ }
323
+ // =============================================================================
324
+ // SHARED STEPS
325
+ // =============================================================================
326
+ export async function securityStep(version) {
327
+ log.heading("Security Audit");
328
+ const checks = runSecurityChecks(version);
329
+ const failed = checks.filter((c) => !c.passed);
330
+ const score = computeScore(checks);
331
+ const passed = checks.filter((c) => c.passed).length;
332
+ console.log(` ${passed}/${checks.length} passed — Score: ${score === "A" ? chalk.green.bold("A") : score === "B" ? chalk.yellow.bold("B") : chalk.red.bold(score)}`);
333
+ if (failed.length > 0) {
334
+ const fixable = failed.filter((c) => c.fixable && c.fix);
335
+ if (fixable.length > 0) {
336
+ const { doFix } = await prompts({
337
+ type: "confirm",
338
+ name: "doFix",
339
+ message: `Auto-fix ${fixable.length} issue(s)?`,
340
+ initial: true,
341
+ });
342
+ if (doFix) {
343
+ for (const check of fixable) {
344
+ check.fix();
345
+ log.success(`Fixed: ${check.name}`);
346
+ }
347
+ }
348
+ }
349
+ for (const check of failed.filter((c) => !c.fixable)) {
350
+ log.warn(`${check.name} — ${check.message}`);
351
+ }
352
+ }
353
+ else {
354
+ log.success("All checks passed");
355
+ }
356
+ }
357
+ async function podChoiceStep(opts) {
358
+ log.heading("Synap Pod");
359
+ if (opts.podUrl) {
360
+ const status = await checkPodHealth(opts.podUrl);
361
+ if (status.healthy) {
362
+ log.success(`Pod healthy at ${opts.podUrl}`);
363
+ return opts.podUrl;
364
+ }
365
+ log.error(`Pod not reachable at ${opts.podUrl}`);
366
+ return null;
367
+ }
368
+ // Check if pod already running locally
369
+ const localStatus = await checkPodHealth("http://localhost:4000");
370
+ if (localStatus.healthy) {
371
+ log.success("Pod already running at http://localhost:4000");
372
+ return "http://localhost:4000";
373
+ }
374
+ const { podChoice } = await prompts({
375
+ type: "select",
376
+ name: "podChoice",
377
+ message: "Where should your Synap pod run?",
378
+ choices: [
379
+ {
380
+ title: "Login to Synap — connect to your existing pod",
381
+ description: "Sign in via browser and find your pod automatically",
382
+ value: "login",
383
+ },
384
+ {
385
+ title: "This machine (docker-compose) — FREE",
386
+ description: "Runs alongside OpenClaw, ~1.5GB RAM",
387
+ value: "local",
388
+ },
389
+ {
390
+ title: "Managed by Synap — €15/mo",
391
+ description: "We host it, you connect",
392
+ value: "managed",
393
+ },
394
+ {
395
+ title: "I already have a pod (enter URL)",
396
+ value: "existing",
397
+ },
398
+ ],
399
+ });
400
+ if (podChoice === "login") {
401
+ const podResult = await loginAndSelectPod();
402
+ return podResult?.url ?? null;
403
+ }
404
+ if (podChoice === "local") {
405
+ return await podInstallLocalStep(opts);
406
+ }
407
+ if (podChoice === "managed") {
408
+ log.blank();
409
+ log.info("Sign up at: " + chalk.cyan("https://synap.live"));
410
+ log.info("After provisioning, re-run:");
411
+ log.dim(" synap init --pod-url https://your-pod.synap.live");
412
+ return null;
413
+ }
414
+ if (podChoice === "existing") {
415
+ const { url } = await prompts({
416
+ type: "text",
417
+ name: "url",
418
+ message: "Pod URL:",
419
+ });
420
+ if (!url)
421
+ return null;
422
+ const spinner = ora("Checking pod health...").start();
423
+ const status = await checkPodHealth(url);
424
+ if (status.healthy) {
425
+ spinner.succeed(`Pod healthy at ${url}`);
426
+ return url;
427
+ }
428
+ spinner.fail(`Pod not reachable at ${url}`);
429
+ return null;
430
+ }
431
+ return null;
432
+ }
433
+ async function podInstallLocalStep(opts) {
434
+ // Check resources first
435
+ const resources = checkServerResources();
436
+ if (resources.ramFree < 1500) {
437
+ log.warn(`Low RAM (${resources.ramFree}MB free). Synap needs ~1.5GB. Consider managed hosting.`);
438
+ }
439
+ log.blank();
440
+ log.info("Install Synap pod with:");
441
+ log.blank();
442
+ console.log(chalk.cyan(" curl -fsSL https://raw.githubusercontent.com/Synap-core/backend/main/install.sh | bash"));
443
+ log.blank();
444
+ const { proceed } = await prompts({
445
+ type: "confirm",
446
+ name: "proceed",
447
+ message: "Run the installer now?",
448
+ initial: true,
449
+ });
450
+ if (proceed) {
451
+ try {
452
+ execSync("curl -fsSL https://raw.githubusercontent.com/Synap-core/backend/main/install.sh | bash", { stdio: "inherit" });
453
+ return "http://localhost:4000";
454
+ }
455
+ catch {
456
+ log.error("Installation failed. Check the output above.");
457
+ return null;
458
+ }
459
+ }
460
+ log.dim("Run the command above manually, then: synap init");
461
+ return null;
462
+ }
463
+ async function connectStep(podUrl, opts, openclawFound, podId) {
464
+ log.heading("Connect to Pod");
465
+ let apiKey = opts.apiKey;
466
+ if (!apiKey) {
467
+ // Check if user is authenticated via CP — if so, auto-generate key
468
+ const creds = getStoredToken();
469
+ const isAuthenticated = creds && new Date(creds.expiresAt) > new Date();
470
+ if (isAuthenticated) {
471
+ // User is logged in — generate API key automatically via CP session
472
+ // The pod trusts the user's session to create agent credentials
473
+ const spinner = ora("Generating API key for OpenClaw agent...").start();
474
+ try {
475
+ // Provision the user on the pod (creates Kratos identity + pod user account)
476
+ // This is idempotent — safe to call on every init
477
+ try {
478
+ await provisionUserOnPod(podUrl, creds.token);
479
+ }
480
+ catch (err) {
481
+ // Non-fatal: log warning, proceed anyway (user may already exist from Browser/Relay login)
482
+ log.warn(`Could not provision user on pod: ${err instanceof Error ? err.message : String(err)}`);
483
+ }
484
+ // Try using the user's CP session token to call the pod directly
485
+ // The pod's setup/agent endpoint accepts PROVISIONING_TOKEN,
486
+ // but for managed pods we can also use the CP to relay the request
487
+ const result = await setupAgentViaCp(podUrl, creds.token, "openclaw");
488
+ apiKey = result.hubApiKey;
489
+ opts.apiKey = apiKey;
490
+ spinner.succeed("API key generated");
491
+ log.dim(`Agent user: ${result.agentUserId}`);
492
+ log.dim(`Workspace: ${result.workspaceId}`);
493
+ log.blank();
494
+ log.info("This key lets OpenClaw read/write your knowledge graph.");
495
+ log.info("It's scoped to Hub Protocol operations only.");
496
+ // Always save to ~/.synap/pod-config.json (works even without OpenClaw)
497
+ saveLocalPodConfig({
498
+ podUrl,
499
+ podId: podId ?? undefined,
500
+ workspaceId: result.workspaceId,
501
+ agentUserId: result.agentUserId,
502
+ hubApiKey: result.hubApiKey,
503
+ savedAt: new Date().toISOString(),
504
+ });
505
+ if (openclawFound) {
506
+ const config = readOpenClawConfig() ?? {};
507
+ setConfigValue(config, "synap.podUrl", podUrl);
508
+ setConfigValue(config, "synap.workspaceId", result.workspaceId);
509
+ setConfigValue(config, "synap.agentUserId", result.agentUserId);
510
+ writeOpenClawConfig(config);
511
+ }
512
+ }
513
+ catch (err) {
514
+ const msg = err instanceof Error ? err.message : String(err);
515
+ spinner.fail(`Auto-generation failed: ${msg}`);
516
+ // Fall through to manual options below
517
+ }
518
+ }
519
+ if (!apiKey) {
520
+ // Manual path — not authenticated or auto-gen failed
521
+ const { keyChoice } = await prompts({
522
+ type: "select",
523
+ name: "keyChoice",
524
+ message: "How to get an API key?",
525
+ choices: [
526
+ {
527
+ title: "Generate via PROVISIONING_TOKEN",
528
+ description: "Use the token from your pod's .env file",
529
+ value: "generate",
530
+ },
531
+ {
532
+ title: "Paste an existing API key",
533
+ description: "From pod Settings → API Keys",
534
+ value: "paste",
535
+ },
536
+ ],
537
+ });
538
+ if (keyChoice === "paste") {
539
+ const { key } = await prompts({
540
+ type: "password",
541
+ name: "key",
542
+ message: "Hub Protocol API key:",
543
+ });
544
+ apiKey = key;
545
+ }
546
+ else {
547
+ const { token } = await prompts({
548
+ type: "password",
549
+ name: "token",
550
+ message: "PROVISIONING_TOKEN (from pod .env):",
551
+ });
552
+ if (token) {
553
+ const spinner = ora("Creating agent credentials...").start();
554
+ try {
555
+ const result = await setupAgent(podUrl, token, "openclaw");
556
+ apiKey = result.hubApiKey;
557
+ opts.apiKey = apiKey;
558
+ spinner.succeed("Credentials created");
559
+ log.dim(`Agent: ${result.agentUserId}`);
560
+ log.dim(`Workspace: ${result.workspaceId}`);
561
+ saveLocalPodConfig({
562
+ podUrl,
563
+ podId: podId ?? undefined,
564
+ workspaceId: result.workspaceId,
565
+ agentUserId: result.agentUserId,
566
+ hubApiKey: result.hubApiKey,
567
+ savedAt: new Date().toISOString(),
568
+ });
569
+ if (openclawFound) {
570
+ const config = readOpenClawConfig() ?? {};
571
+ setConfigValue(config, "synap.podUrl", podUrl);
572
+ setConfigValue(config, "synap.workspaceId", result.workspaceId);
573
+ setConfigValue(config, "synap.agentUserId", result.agentUserId);
574
+ writeOpenClawConfig(config);
575
+ }
576
+ }
577
+ catch (err) {
578
+ spinner.fail(err instanceof Error ? err.message : String(err));
579
+ return null;
580
+ }
581
+ }
582
+ }
583
+ }
584
+ }
585
+ if (apiKey) {
586
+ log.success(`API Key: ${apiKey}`);
587
+ log.warn("Save this key — it will not be shown again.");
588
+ }
589
+ return apiKey ?? null;
590
+ }
591
+ export async function skillStep(openclawFound) {
592
+ if (!openclawFound)
593
+ return;
594
+ log.heading("Install Skill");
595
+ const spinner = ora("Installing synap skill...").start();
596
+ try {
597
+ installSynapSkill();
598
+ spinner.succeed("Synap skill installed");
599
+ }
600
+ catch (err) {
601
+ spinner.fail(err instanceof Error ? err.message : "Failed");
602
+ log.dim("Install manually: openclaw skills install synap");
603
+ }
604
+ }
605
+ export async function seedStep(podUrl, apiKey, oc) {
606
+ log.heading("Seed Workspace");
607
+ const spinner = ora("Creating entities from OpenClaw config...").start();
608
+ try {
609
+ const count = await seedAgentEntities(podUrl, apiKey, oc);
610
+ spinner.succeed(`${count} entities created`);
611
+ }
612
+ catch (err) {
613
+ spinner.fail(err instanceof Error ? err.message : "Seed failed");
614
+ }
615
+ }
616
+ export async function isStep(podUrl, apiKey, openclawFound) {
617
+ log.heading("Intelligence Service");
618
+ log.dim("Route AI through Synap — 90% on free models, save 50-80%");
619
+ // 1. Check current IS status on the pod
620
+ const spinner = ora("Checking Intelligence Service status...").start();
621
+ let isActive = false;
622
+ let needsProvision = false;
623
+ try {
624
+ const statusRes = await fetch(`${podUrl}/api/provision/status`, {
625
+ signal: AbortSignal.timeout(5000),
626
+ }).catch(() => null);
627
+ if (statusRes?.ok) {
628
+ const data = (await statusRes.json());
629
+ isActive = data?.intelligenceService?.status === "active";
630
+ }
631
+ }
632
+ catch {
633
+ // Pod may not support this endpoint — continue
634
+ }
635
+ if (isActive) {
636
+ spinner.succeed("Intelligence Service is already active on this pod");
637
+ }
638
+ else {
639
+ spinner.info("Intelligence Service is not active on this pod");
640
+ needsProvision = true;
641
+ }
642
+ // 2. If IS is not active, try to provision via CP (if user is logged in)
643
+ if (needsProvision) {
644
+ const creds = getStoredToken();
645
+ if (creds) {
646
+ // Check CP subscription status
647
+ const cpUrl = process.env.SYNAP_CP_URL ?? "https://api.synap.live";
648
+ try {
649
+ // Find the pod ID from CP
650
+ const pods = await listPods(creds.token);
651
+ const matchingPod = pods.find((p) => (p.podUrl ?? p.url ?? "").replace(/\/+$/, "") === podUrl.replace(/\/+$/, ""));
652
+ if (matchingPod) {
653
+ const podId = matchingPod.id;
654
+ // Check IS provision status on CP
655
+ const provStatus = await fetch(`${cpUrl}/intelligence/provision/status/${podId}`, { headers: { Authorization: `Bearer ${creds.token}` } }).catch(() => null);
656
+ const provData = provStatus?.ok
657
+ ? (await provStatus.json())
658
+ : null;
659
+ if (provData?.subscribed && !provData?.cpProvisioned) {
660
+ // User has subscription but IS not provisioned — offer to provision
661
+ const { provision } = await prompts({
662
+ type: "confirm",
663
+ name: "provision",
664
+ message: "Your subscription includes AI. Provision Intelligence Service on this pod?",
665
+ initial: true,
666
+ });
667
+ if (provision) {
668
+ const provSpinner = ora("Provisioning Intelligence Service...").start();
669
+ try {
670
+ const res = await fetch(`${cpUrl}/intelligence/provision/${podId}`, {
671
+ method: "POST",
672
+ headers: {
673
+ Authorization: `Bearer ${creds.token}`,
674
+ "Content-Type": "application/json",
675
+ },
676
+ });
677
+ if (res.ok) {
678
+ provSpinner.succeed("Intelligence Service provisioned successfully");
679
+ isActive = true;
680
+ }
681
+ else {
682
+ const err = (await res.json().catch(() => null));
683
+ provSpinner.fail(`Provisioning failed: ${err?.error ?? res.status}`);
684
+ }
685
+ }
686
+ catch (e) {
687
+ provSpinner.fail(`Provisioning error: ${e instanceof Error ? e.message : "unknown"}`);
688
+ }
689
+ }
690
+ }
691
+ else if (!provData?.subscribed) {
692
+ log.dim("No AI subscription found. Subscribe at https://synap.live/pricing");
693
+ log.dim("Or use --skip-is to skip this step");
694
+ }
695
+ else if (provData?.cpProvisioned) {
696
+ log.dim("IS is provisioned on CP but pod may need reconnection.");
697
+ log.dim("Try reprovisioning from the Browser app: Settings > Add-ons > Intelligence");
698
+ }
699
+ }
700
+ }
701
+ catch {
702
+ log.dim("Could not check CP subscription status");
703
+ }
704
+ }
705
+ else {
706
+ log.dim("Sign in with 'synap login' to check your AI subscription and auto-provision IS");
707
+ }
708
+ }
709
+ // 3. Configure OpenClaw provider (if found and IS is active)
710
+ if (openclawFound && isActive) {
711
+ const { configureOc } = await prompts({
712
+ type: "confirm",
713
+ name: "configureOc",
714
+ message: "Configure Synap IS as OpenClaw AI provider?",
715
+ initial: true,
716
+ });
717
+ if (configureOc) {
718
+ const config = readOpenClawConfig() ?? {};
719
+ setConfigValue(config, "models.providers.synap", {
720
+ baseUrl: `${podUrl}/v1`,
721
+ api: "openai-completions",
722
+ apiKey,
723
+ models: [
724
+ { id: "synap/auto", name: "Synap Auto", contextWindow: 200000, maxTokens: 8192 },
725
+ { id: "synap/balanced", name: "Synap Balanced", contextWindow: 131072, maxTokens: 8192 },
726
+ { id: "synap/advanced", name: "Synap Advanced", contextWindow: 200000, maxTokens: 8192 },
727
+ ],
728
+ });
729
+ writeOpenClawConfig(config);
730
+ log.success("Synap IS configured as OpenClaw provider — restart OpenClaw to apply");
731
+ }
732
+ }
733
+ else if (openclawFound && !isActive) {
734
+ log.dim("Skipping OpenClaw provider configuration (IS not active)");
735
+ log.dim("Once IS is provisioned, run 'synap init' again to configure the provider");
736
+ }
737
+ }
738
+ function printSummary(podUrl, openclawConnected) {
739
+ log.blank();
740
+ console.log(chalk.green("═══════════════════════════════════════════"));
741
+ console.log(chalk.green.bold(" Synap Setup Complete"));
742
+ console.log(chalk.green("═══════════════════════════════════════════"));
743
+ log.blank();
744
+ log.info(`Pod: ${podUrl}`);
745
+ if (openclawConnected)
746
+ log.info("Skill: synap (knowledge graph + relay)");
747
+ log.blank();
748
+ if (openclawConnected) {
749
+ log.info("Try it now:");
750
+ log.dim(' Ask your agent: "remember that Marc prefers email"');
751
+ log.dim(' Then later: "what do I know about Marc?"');
752
+ }
753
+ log.blank();
754
+ log.dim(" synap status — health check");
755
+ log.dim(" synap security-audit — verify security");
756
+ log.blank();
757
+ if (!openclawConnected) {
758
+ log.info("OpenClaw is provisioning on your pod server (2-5 min).");
759
+ log.info("Once it's ready, run:");
760
+ log.blank();
761
+ console.log(chalk.cyan(" synap finish"));
762
+ log.blank();
763
+ log.dim("This will install the skill, seed your workspace, and configure AI routing.");
764
+ log.dim("Check progress: synap status");
765
+ }
766
+ }
767
+ async function loginAndSelectPod() {
768
+ // Check if already logged in
769
+ const authStatus = await isLoggedIn();
770
+ if (!authStatus.valid) {
771
+ log.info("Opening browser to sign in...");
772
+ const spinner = ora("Waiting for browser authentication...").start();
773
+ const creds = await login();
774
+ if (!creds) {
775
+ spinner.fail("Authentication timed out or failed");
776
+ log.dim("Try again or use --pod-url to connect directly");
777
+ return null;
778
+ }
779
+ spinner.succeed(`Authenticated as ${creds.email}`);
780
+ }
781
+ else {
782
+ log.success(`Already logged in as ${authStatus.email}`);
783
+ }
784
+ const token = getStoredToken();
785
+ if (!token)
786
+ return null;
787
+ // List pods
788
+ const podsSpinner = ora("Fetching your pods...").start();
789
+ try {
790
+ const pods = await listPods(token.token);
791
+ if (pods.length === 0) {
792
+ podsSpinner.info("No pods found on your account");
793
+ log.blank();
794
+ log.info("Create a pod at: " + chalk.cyan("https://synap.live"));
795
+ log.info("Then re-run: " + chalk.dim("synap init"));
796
+ return null;
797
+ }
798
+ podsSpinner.succeed(`Found ${pods.length} pod(s)`);
799
+ if (pods.length === 1) {
800
+ const pod = pods[0];
801
+ const podUrl = pod.url || `https://${pod.subdomain}.synap.live`;
802
+ const { connect } = await prompts({
803
+ type: "confirm",
804
+ name: "connect",
805
+ message: `Connect to ${podUrl}?`,
806
+ initial: true,
807
+ });
808
+ if (!connect)
809
+ return null;
810
+ const healthSpinner = ora("Checking pod health...").start();
811
+ const status = await checkPodHealth(podUrl);
812
+ if (status.healthy) {
813
+ healthSpinner.succeed(`Pod healthy at ${podUrl}`);
814
+ return { url: podUrl, podId: pod.id };
815
+ }
816
+ healthSpinner.fail(`Pod not reachable at ${podUrl}`);
817
+ return null;
818
+ }
819
+ // Multiple pods — let user choose
820
+ const { selectedPodUrl } = await prompts({
821
+ type: "select",
822
+ name: "selectedPodUrl",
823
+ message: "Which pod do you want to connect to?",
824
+ choices: pods.map((pod) => {
825
+ const podUrl = pod.url || `https://${pod.subdomain}.synap.live`;
826
+ return {
827
+ title: `${pod.subdomain} (${pod.status}) — ${pod.region}`,
828
+ description: podUrl,
829
+ value: podUrl,
830
+ };
831
+ }),
832
+ });
833
+ if (!selectedPodUrl)
834
+ return null;
835
+ const selectedPod = pods.find((p) => (p.url || `https://${p.subdomain}.synap.live`) === selectedPodUrl);
836
+ const healthSpinner = ora("Checking pod health...").start();
837
+ const status = await checkPodHealth(selectedPodUrl);
838
+ if (status.healthy) {
839
+ healthSpinner.succeed(`Pod healthy at ${selectedPodUrl}`);
840
+ return { url: selectedPodUrl, podId: selectedPod?.id ?? "" };
841
+ }
842
+ healthSpinner.fail(`Pod not reachable at ${selectedPodUrl}`);
843
+ return null;
844
+ }
845
+ catch (err) {
846
+ podsSpinner.fail(err instanceof Error ? err.message : "Failed to fetch pods");
847
+ return null;
848
+ }
849
+ }
850
+ // =============================================================================
851
+ // HELPERS
852
+ // =============================================================================
853
+ function detectServer() {
854
+ try {
855
+ const platform = process.platform;
856
+ if (platform !== "linux")
857
+ return false;
858
+ execSync("docker info >/dev/null 2>&1", { timeout: 5000 });
859
+ return true;
860
+ }
861
+ catch {
862
+ return false;
863
+ }
864
+ }
865
+ //# sourceMappingURL=init.js.map