@recursiv/cli 0.1.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.
@@ -0,0 +1,2001 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/bin/recursiv.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import { existsSync as existsSync2 } from "fs";
8
+ import { mkdir } from "fs/promises";
9
+ import { resolve } from "path";
10
+ import { execSync } from "child_process";
11
+ import prompts2 from "prompts";
12
+ import pc3 from "picocolors";
13
+ import ora2 from "ora";
14
+
15
+ // src/lib/logger.ts
16
+ import pc from "picocolors";
17
+ var log = {
18
+ info(msg) {
19
+ console.log(pc.cyan("info") + " " + msg);
20
+ },
21
+ success(msg) {
22
+ console.log(pc.green("ok") + " " + msg);
23
+ },
24
+ warn(msg) {
25
+ console.log(pc.yellow("warn") + " " + msg);
26
+ },
27
+ error(msg) {
28
+ console.error(pc.red("error") + " " + msg);
29
+ },
30
+ step(n, total, msg) {
31
+ console.log(pc.dim(`[${n}/${total}]`) + " " + msg);
32
+ },
33
+ blank() {
34
+ console.log();
35
+ }
36
+ };
37
+ function banner() {
38
+ console.log();
39
+ console.log(pc.bold("Recursiv") + pc.dim(" \u2014 build and ship apps with AI"));
40
+ console.log();
41
+ }
42
+
43
+ // src/lib/config.ts
44
+ import { readFile, writeFile } from "fs/promises";
45
+ import { join } from "path";
46
+ var CONFIG_FILE = ".recursiv.json";
47
+ function configPath(dir) {
48
+ return join(dir, CONFIG_FILE);
49
+ }
50
+ async function readConfig(dir) {
51
+ try {
52
+ const raw = await readFile(configPath(dir), "utf-8");
53
+ return JSON.parse(raw);
54
+ } catch {
55
+ return null;
56
+ }
57
+ }
58
+ async function writeConfig(dir, config) {
59
+ await writeFile(configPath(dir), JSON.stringify(config, null, 2) + "\n", "utf-8");
60
+ }
61
+ function createConfig(opts) {
62
+ return {
63
+ version: 1,
64
+ project: {
65
+ name: opts.name,
66
+ template: opts.template,
67
+ framework: opts.framework
68
+ },
69
+ api: {
70
+ baseUrl: "https://recursiv.io/api/v1"
71
+ },
72
+ dev: {
73
+ port: opts.port ?? 3e3,
74
+ command: opts.devCommand ?? "npm run dev"
75
+ }
76
+ };
77
+ }
78
+
79
+ // src/lib/env.ts
80
+ import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
81
+ import { join as join2 } from "path";
82
+ async function writeEnvFile(dir, apiKey) {
83
+ const envPath = join2(dir, ".env");
84
+ const content = [
85
+ "# Recursiv API key",
86
+ `RECURSIV_API_KEY=${apiKey}`,
87
+ "",
88
+ "# Framework-specific public keys (uncomment as needed)",
89
+ `# NEXT_PUBLIC_RECURSIV_API_KEY=${apiKey}`,
90
+ `# VITE_RECURSIV_API_KEY=${apiKey}`,
91
+ ""
92
+ ].join("\n");
93
+ await writeFile2(envPath, content, "utf-8");
94
+ }
95
+ async function readApiKeyFromEnv(dir) {
96
+ try {
97
+ const raw = await readFile2(join2(dir, ".env"), "utf-8");
98
+ const match = raw.match(/^RECURSIV_API_KEY=(.+)$/m);
99
+ return match?.[1]?.trim() ?? null;
100
+ } catch {
101
+ return null;
102
+ }
103
+ }
104
+ async function updateApiKeyInEnv(dir, apiKey) {
105
+ const envPath = join2(dir, ".env");
106
+ let content;
107
+ try {
108
+ content = await readFile2(envPath, "utf-8");
109
+ if (content.includes("RECURSIV_API_KEY=")) {
110
+ content = content.replace(/^RECURSIV_API_KEY=.+$/m, `RECURSIV_API_KEY=${apiKey}`);
111
+ } else {
112
+ content += `
113
+ RECURSIV_API_KEY=${apiKey}
114
+ `;
115
+ }
116
+ } catch {
117
+ content = `RECURSIV_API_KEY=${apiKey}
118
+ `;
119
+ }
120
+ await writeFile2(envPath, content, "utf-8");
121
+ }
122
+
123
+ // src/lib/templates.ts
124
+ import degit from "degit";
125
+ import ora from "ora";
126
+ async function cloneTemplate(template, dest) {
127
+ const spinner = ora(`Cloning ${template.name} template...`).start();
128
+ try {
129
+ const emitter = degit(template.repo, {
130
+ cache: false,
131
+ force: true,
132
+ verbose: false
133
+ });
134
+ await emitter.clone(dest);
135
+ spinner.succeed(`Template cloned successfully`);
136
+ } catch (err) {
137
+ spinner.fail("Failed to clone template");
138
+ const message = err instanceof Error ? err.message : String(err);
139
+ throw new Error(
140
+ `Could not clone template from ${template.repo}.
141
+ Make sure you have internet access and the repo exists.
142
+ Details: ${message}`
143
+ );
144
+ }
145
+ }
146
+
147
+ // src/lib/auth-flow.ts
148
+ import prompts from "prompts";
149
+ import pc2 from "picocolors";
150
+ var API_KEY_PATTERN = /^sk_(live|test)_[a-zA-Z0-9]{20,}$/;
151
+ var DASHBOARD_URL = "https://recursiv.io/dashboard/api-keys";
152
+ var DEFAULT_SCOPES = [
153
+ "posts:read",
154
+ "posts:write",
155
+ "chat:read",
156
+ "chat:write",
157
+ "agents:read",
158
+ "agents:write",
159
+ "projects:read",
160
+ "projects:write",
161
+ "communities:read",
162
+ "communities:write",
163
+ "users:read"
164
+ ];
165
+ function isValidKeyFormat(key) {
166
+ return API_KEY_PATTERN.test(key);
167
+ }
168
+ async function validateApiKey(apiKey, baseUrl) {
169
+ try {
170
+ const res = await fetch(`${baseUrl}/users/me`, {
171
+ headers: {
172
+ Authorization: `Bearer ${apiKey}`,
173
+ Accept: "application/json"
174
+ }
175
+ });
176
+ return res.ok;
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+ async function terminalSignup(baseUrl) {
182
+ const rootUrl = baseUrl.replace(/\/api\/v1\/?$/, "");
183
+ console.log();
184
+ console.log(pc2.bold("Create a free Recursiv account"));
185
+ console.log(pc2.dim("Free tier: 1,000 API calls/day, 1 agent, 3 projects"));
186
+ console.log();
187
+ const { email } = await prompts(
188
+ {
189
+ type: "text",
190
+ name: "email",
191
+ message: "Email",
192
+ validate: (v) => {
193
+ if (!v || !v.includes("@")) return "Valid email required";
194
+ return true;
195
+ }
196
+ },
197
+ { onCancel: () => process.exit(1) }
198
+ );
199
+ const { password } = await prompts(
200
+ {
201
+ type: "password",
202
+ name: "password",
203
+ message: "Password",
204
+ validate: (v) => {
205
+ if (!v || v.length < 8) return "Password must be at least 8 characters";
206
+ if (!/[a-zA-Z]/.test(v) || !/[0-9]/.test(v))
207
+ return "Password must contain both letters and numbers";
208
+ return true;
209
+ }
210
+ },
211
+ { onCancel: () => process.exit(1) }
212
+ );
213
+ let sessionCookie = null;
214
+ let isNewAccount = false;
215
+ try {
216
+ const signupRes = await fetch(`${rootUrl}/api/auth/sign-up/email`, {
217
+ method: "POST",
218
+ headers: { "Content-Type": "application/json" },
219
+ body: JSON.stringify({
220
+ email,
221
+ password,
222
+ name: email.split("@")[0]
223
+ }),
224
+ redirect: "manual"
225
+ });
226
+ if (signupRes.ok || signupRes.status === 302) {
227
+ sessionCookie = extractSessionCookie(signupRes);
228
+ isNewAccount = true;
229
+ } else if (signupRes.status === 409 || signupRes.status === 422) {
230
+ const signinRes = await fetch(`${rootUrl}/api/auth/sign-in/email`, {
231
+ method: "POST",
232
+ headers: { "Content-Type": "application/json" },
233
+ body: JSON.stringify({ email, password }),
234
+ redirect: "manual"
235
+ });
236
+ if (signinRes.ok || signinRes.status === 302) {
237
+ sessionCookie = extractSessionCookie(signinRes);
238
+ } else {
239
+ const errBody = await signinRes.text().catch(() => "");
240
+ log.error(`Login failed (${signinRes.status}): ${errBody || "Invalid credentials"}`);
241
+ return null;
242
+ }
243
+ } else {
244
+ const errBody = await signupRes.text().catch(() => "");
245
+ log.error(`Signup failed (${signupRes.status}): ${errBody || "Unknown error"}`);
246
+ return null;
247
+ }
248
+ } catch (err) {
249
+ log.error(
250
+ `Could not reach ${rootUrl} \u2014 ${err instanceof Error ? err.message : "network error"}`
251
+ );
252
+ log.info(`You can create an API key manually at ${pc2.underline(DASHBOARD_URL)}`);
253
+ return null;
254
+ }
255
+ if (!sessionCookie) {
256
+ log.error("No session returned from server");
257
+ log.info(`Create an API key manually at ${pc2.underline(DASHBOARD_URL)}`);
258
+ return null;
259
+ }
260
+ if (isNewAccount) {
261
+ log.success("Account created");
262
+ } else {
263
+ log.success("Signed in");
264
+ }
265
+ try {
266
+ const keyRes = await fetch(`${baseUrl}/api-keys`, {
267
+ method: "POST",
268
+ headers: {
269
+ "Content-Type": "application/json",
270
+ Cookie: sessionCookie
271
+ },
272
+ body: JSON.stringify({
273
+ name: "CLI (auto-created)",
274
+ scopes: DEFAULT_SCOPES
275
+ })
276
+ });
277
+ if (!keyRes.ok) {
278
+ const errBody = await keyRes.text().catch(() => "");
279
+ log.error(`Failed to create API key (${keyRes.status}): ${errBody}`);
280
+ log.info(`Create one manually at ${pc2.underline(DASHBOARD_URL)}`);
281
+ return null;
282
+ }
283
+ const { data } = await keyRes.json();
284
+ log.success(`API key created (free tier: 1,000 calls/day)`);
285
+ return data.key;
286
+ } catch (err) {
287
+ log.error(
288
+ `Failed to create API key: ${err instanceof Error ? err.message : "unknown error"}`
289
+ );
290
+ log.info(`Create one manually at ${pc2.underline(DASHBOARD_URL)}`);
291
+ return null;
292
+ }
293
+ }
294
+ function extractSessionCookie(res) {
295
+ const setCookieHeaders = "getSetCookie" in res.headers ? res.headers.getSetCookie() : [];
296
+ if (setCookieHeaders.length === 0) {
297
+ const raw = res.headers.get("set-cookie");
298
+ if (raw) {
299
+ return raw.split(",").map((c) => c.split(";")[0].trim()).join("; ");
300
+ }
301
+ return null;
302
+ }
303
+ return setCookieHeaders.map((c) => c.split(";")[0].trim()).join("; ");
304
+ }
305
+ async function promptApiKey() {
306
+ console.log(
307
+ pc2.dim(
308
+ `Get your API key from ${pc2.underline(DASHBOARD_URL)}
309
+ Keys start with sk_live_ or sk_test_`
310
+ )
311
+ );
312
+ console.log();
313
+ const { apiKey } = await prompts(
314
+ {
315
+ type: "text",
316
+ name: "apiKey",
317
+ message: "API key (or press Enter to skip)",
318
+ validate: (value) => {
319
+ if (!value) return true;
320
+ if (!isValidKeyFormat(value)) {
321
+ return "Invalid key format. Keys start with sk_live_ or sk_test_";
322
+ }
323
+ return true;
324
+ }
325
+ },
326
+ { onCancel: () => process.exit(1) }
327
+ );
328
+ if (!apiKey) {
329
+ log.warn("No API key provided \u2014 you can add one later in .env");
330
+ return null;
331
+ }
332
+ return apiKey;
333
+ }
334
+ async function promptAndValidateApiKey(baseUrl) {
335
+ const apiKey = await promptApiKey();
336
+ if (!apiKey) return null;
337
+ const valid = await validateApiKey(apiKey, baseUrl);
338
+ if (!valid) {
339
+ log.warn("Could not validate API key (server unreachable or invalid key)");
340
+ log.info("Key saved to .env \u2014 you can update it later");
341
+ } else {
342
+ log.success("API key validated");
343
+ }
344
+ return apiKey;
345
+ }
346
+ async function getOrCreateApiKey(baseUrl) {
347
+ console.log();
348
+ const { method } = await prompts(
349
+ {
350
+ type: "select",
351
+ name: "method",
352
+ message: "How would you like to authenticate?",
353
+ choices: [
354
+ {
355
+ title: "Create a free account",
356
+ description: "Sign up with email + password (no browser needed)",
357
+ value: "signup"
358
+ },
359
+ {
360
+ title: "Enter an existing API key",
361
+ description: "Paste a key from your dashboard",
362
+ value: "paste"
363
+ },
364
+ {
365
+ title: "Skip for now",
366
+ description: "Add an API key later in .env",
367
+ value: "skip"
368
+ }
369
+ ]
370
+ },
371
+ { onCancel: () => process.exit(1) }
372
+ );
373
+ if (method === "signup") {
374
+ return terminalSignup(baseUrl);
375
+ }
376
+ if (method === "paste") {
377
+ return promptAndValidateApiKey(baseUrl);
378
+ }
379
+ log.warn("No API key \u2014 you can add one later in .env");
380
+ return null;
381
+ }
382
+
383
+ // src/lib/package-manager.ts
384
+ import { existsSync } from "fs";
385
+ import { join as join3 } from "path";
386
+ function detectPackageManager(dir) {
387
+ if (existsSync(join3(dir, "bun.lockb")) || existsSync(join3(dir, "bun.lock"))) return "bun";
388
+ if (existsSync(join3(dir, "pnpm-lock.yaml"))) return "pnpm";
389
+ if (existsSync(join3(dir, "yarn.lock"))) return "yarn";
390
+ return "npm";
391
+ }
392
+ function detectFromUserAgent() {
393
+ const ua = process.env.npm_config_user_agent ?? "";
394
+ if (ua.startsWith("bun")) return "bun";
395
+ if (ua.startsWith("pnpm")) return "pnpm";
396
+ if (ua.startsWith("yarn")) return "yarn";
397
+ return "npm";
398
+ }
399
+ function installCommand(pm) {
400
+ return pm === "yarn" ? "yarn" : `${pm} install`;
401
+ }
402
+ function runCommand(pm, script) {
403
+ if (pm === "npm") return `npm run ${script}`;
404
+ return `${pm} ${script}`;
405
+ }
406
+
407
+ // src/templates/registry.ts
408
+ var templates = [
409
+ {
410
+ id: "nextjs",
411
+ name: "Next.js + Recursiv",
412
+ description: "Full-stack Next.js app with feeds, chat, and agents",
413
+ repo: "socialdotdev/template-nextjs",
414
+ framework: "nextjs",
415
+ devCommand: "next dev",
416
+ devPort: 3e3,
417
+ recommended: true
418
+ },
419
+ {
420
+ id: "node",
421
+ name: "Node.js + Express",
422
+ description: "Express API server with Recursiv SDK",
423
+ repo: "socialdotdev/template-node",
424
+ framework: "express",
425
+ devCommand: "node --watch src/index.js",
426
+ devPort: 4e3
427
+ },
428
+ {
429
+ id: "vite",
430
+ name: "Vite + React",
431
+ description: "React SPA with social feed components",
432
+ repo: "socialdotdev/template-vite",
433
+ framework: "vite",
434
+ devCommand: "vite",
435
+ devPort: 5173
436
+ }
437
+ ];
438
+ function getTemplate(id) {
439
+ return templates.find((t) => t.id === id);
440
+ }
441
+
442
+ // src/commands/init.ts
443
+ import { writeFile as writeFile3 } from "fs/promises";
444
+ async function initCommand(nameArg) {
445
+ banner();
446
+ const totalSteps = 7;
447
+ log.step(1, totalSteps, "Project setup");
448
+ let projectName;
449
+ if (nameArg) {
450
+ projectName = nameArg;
451
+ } else {
452
+ const { name } = await prompts2(
453
+ {
454
+ type: "text",
455
+ name: "name",
456
+ message: "Project name",
457
+ initial: "my-recursiv-app",
458
+ validate: (v) => v.trim() ? true : "Name is required"
459
+ },
460
+ { onCancel: () => process.exit(1) }
461
+ );
462
+ projectName = name;
463
+ }
464
+ const projectDir = resolve(process.cwd(), projectName);
465
+ if (existsSync2(projectDir)) {
466
+ log.error(`Directory ${pc3.bold(projectName)} already exists`);
467
+ process.exit(1);
468
+ }
469
+ log.step(2, totalSteps, "Choose a template");
470
+ const templateChoices = templates.map((t) => ({
471
+ title: t.recommended ? `${t.name} ${pc3.dim("(recommended)")}` : t.name,
472
+ description: t.description,
473
+ value: t.id
474
+ }));
475
+ const { templateId } = await prompts2(
476
+ {
477
+ type: "select",
478
+ name: "templateId",
479
+ message: "Template",
480
+ choices: templateChoices,
481
+ initial: 0
482
+ },
483
+ { onCancel: () => process.exit(1) }
484
+ );
485
+ const template = templates.find((t) => t.id === templateId);
486
+ log.step(3, totalSteps, "Authentication");
487
+ const apiKey = await getOrCreateApiKey("https://recursiv.io/api/v1");
488
+ log.step(4, totalSteps, "Creating project");
489
+ await mkdir(projectDir, { recursive: true });
490
+ await cloneTemplate(template, projectDir);
491
+ log.step(5, totalSteps, "Writing config");
492
+ const pm = detectFromUserAgent();
493
+ const config = createConfig({
494
+ name: projectName,
495
+ template: template.id,
496
+ framework: template.framework,
497
+ port: template.devPort,
498
+ devCommand: runCommand(pm, "dev")
499
+ });
500
+ await writeConfig(projectDir, config);
501
+ if (apiKey) {
502
+ await writeEnvFile(projectDir, apiKey);
503
+ }
504
+ const gitignorePath = resolve(projectDir, ".gitignore");
505
+ if (!existsSync2(gitignorePath)) {
506
+ await writeFile3(
507
+ gitignorePath,
508
+ ["node_modules", "dist", ".env", ".env.local", ".next", ".turbo", ""].join("\n"),
509
+ "utf-8"
510
+ );
511
+ }
512
+ log.step(6, totalSteps, "Installing dependencies");
513
+ const installSpinner = ora2(`Running ${pc3.bold(installCommand(pm))}...`).start();
514
+ try {
515
+ execSync(installCommand(pm), {
516
+ cwd: projectDir,
517
+ stdio: "pipe",
518
+ timeout: 12e4
519
+ });
520
+ installSpinner.succeed("Dependencies installed");
521
+ } catch {
522
+ installSpinner.warn("Could not install dependencies \u2014 run install manually");
523
+ }
524
+ log.step(7, totalSteps, "Initializing git");
525
+ try {
526
+ execSync("git init", { cwd: projectDir, stdio: "pipe" });
527
+ execSync("git add -A", { cwd: projectDir, stdio: "pipe" });
528
+ execSync('git commit -m "Initial commit from create-recursiv-app"', {
529
+ cwd: projectDir,
530
+ stdio: "pipe"
531
+ });
532
+ log.success("Git repository initialized");
533
+ } catch {
534
+ log.warn("Could not initialize git repository");
535
+ }
536
+ log.blank();
537
+ console.log(pc3.bold(pc3.green("Your Recursiv app is ready!")));
538
+ log.blank();
539
+ console.log(" Next steps:");
540
+ console.log();
541
+ console.log(` ${pc3.dim("$")} cd ${projectName}`);
542
+ if (!apiKey) {
543
+ console.log(` ${pc3.dim("$")} ${pc3.dim("# Add your API key to .env")}`);
544
+ }
545
+ console.log(` ${pc3.dim("$")} ${runCommand(pm, "dev")}`);
546
+ log.blank();
547
+ console.log(pc3.dim("Docs: https://docs.recursiv.io"));
548
+ console.log(pc3.dim("Dashboard: https://recursiv.io/dashboard"));
549
+ log.blank();
550
+ }
551
+
552
+ // src/commands/dev.ts
553
+ import { spawn } from "child_process";
554
+ import pc4 from "picocolors";
555
+ async function devCommand(opts) {
556
+ const cwd = process.cwd();
557
+ const config = await readConfig(cwd);
558
+ if (!config) {
559
+ log.error("No .recursiv.json found \u2014 are you in a Recursiv project?");
560
+ log.info(`Run ${pc4.bold("create-recursiv-app")} to create a new project`);
561
+ process.exit(1);
562
+ }
563
+ const apiKey = await readApiKeyFromEnv(cwd);
564
+ if (!apiKey) {
565
+ log.warn("No RECURSIV_API_KEY found in .env \u2014 API calls will fail");
566
+ }
567
+ const pm = detectPackageManager(cwd);
568
+ const port = opts.port ?? String(config.dev.port);
569
+ const devCmd = config.dev.command || runCommand(pm, "dev");
570
+ const env = {
571
+ ...process.env,
572
+ PORT: port
573
+ };
574
+ if (apiKey) {
575
+ env.RECURSIV_API_KEY = apiKey;
576
+ if (config.project.framework === "nextjs") {
577
+ env.NEXT_PUBLIC_RECURSIV_API_KEY = apiKey;
578
+ } else if (config.project.framework === "vite") {
579
+ env.VITE_RECURSIV_API_KEY = apiKey;
580
+ }
581
+ }
582
+ log.info(`Starting dev server on port ${pc4.bold(port)}`);
583
+ log.info(`Running: ${pc4.dim(devCmd)}`);
584
+ console.log();
585
+ const [cmd, ...args] = devCmd.split(" ");
586
+ const child = spawn(cmd, args, {
587
+ cwd,
588
+ env,
589
+ stdio: "inherit",
590
+ shell: true
591
+ });
592
+ child.on("close", (code) => {
593
+ process.exit(code ?? 0);
594
+ });
595
+ const cleanup = () => {
596
+ child.kill("SIGTERM");
597
+ };
598
+ process.on("SIGINT", cleanup);
599
+ process.on("SIGTERM", cleanup);
600
+ }
601
+
602
+ // src/commands/deploy.ts
603
+ import { writeFile as writeFile4 } from "fs/promises";
604
+ import { resolve as resolve2 } from "path";
605
+ import pc5 from "picocolors";
606
+ import ora3 from "ora";
607
+ var targets = [
608
+ {
609
+ id: "vercel",
610
+ name: "Vercel",
611
+ generate: (name, framework) => ({
612
+ file: "vercel.json",
613
+ content: JSON.stringify(
614
+ {
615
+ $schema: "https://openapi.vercel.sh/vercel.json",
616
+ projectSettings: {
617
+ framework: framework === "nextjs" ? "nextjs" : framework === "vite" ? "vite" : null
618
+ },
619
+ env: {
620
+ RECURSIV_API_KEY: "@recursiv-api-key"
621
+ }
622
+ },
623
+ null,
624
+ 2
625
+ )
626
+ })
627
+ },
628
+ {
629
+ id: "docker",
630
+ name: "Docker",
631
+ generate: (name, framework) => ({
632
+ file: "Dockerfile",
633
+ content: [
634
+ "FROM node:20-slim AS base",
635
+ "WORKDIR /app",
636
+ "COPY package*.json ./",
637
+ "RUN npm ci --omit=dev",
638
+ "COPY . .",
639
+ framework === "nextjs" ? "RUN npm run build" : "",
640
+ framework === "nextjs" ? "EXPOSE 3000" : "EXPOSE 4000",
641
+ framework === "nextjs" ? 'CMD ["npm", "start"]' : 'CMD ["node", "src/index.js"]',
642
+ ""
643
+ ].filter(Boolean).join("\n")
644
+ })
645
+ },
646
+ {
647
+ id: "render",
648
+ name: "Render",
649
+ generate: (name, framework) => ({
650
+ file: "render.yaml",
651
+ content: [
652
+ "services:",
653
+ ` - type: web`,
654
+ ` name: ${name}`,
655
+ ` runtime: node`,
656
+ ` buildCommand: npm install && npm run build`,
657
+ framework === "nextjs" ? " startCommand: npm start" : " startCommand: node src/index.js",
658
+ " envVars:",
659
+ " - key: RECURSIV_API_KEY",
660
+ " sync: false",
661
+ ""
662
+ ].join("\n")
663
+ })
664
+ },
665
+ {
666
+ id: "railway",
667
+ name: "Railway",
668
+ generate: (name, framework) => ({
669
+ file: "railway.toml",
670
+ content: [
671
+ "[build]",
672
+ 'builder = "nixpacks"',
673
+ "",
674
+ "[deploy]",
675
+ framework === "nextjs" ? 'startCommand = "npm start"' : 'startCommand = "node src/index.js"',
676
+ 'healthcheckPath = "/"',
677
+ ""
678
+ ].join("\n")
679
+ })
680
+ }
681
+ ];
682
+ async function deployCommand(target) {
683
+ const cwd = process.cwd();
684
+ const config = await readConfig(cwd);
685
+ if (!config) {
686
+ log.error("No .recursiv.json found \u2014 are you in a Recursiv project?");
687
+ process.exit(1);
688
+ }
689
+ if (target === "cloud") {
690
+ log.info("Cloud deployment via Recursiv is coming soon");
691
+ log.info(`For now, deploy to Vercel, Render, or Railway with ${pc5.bold("recursiv deploy <target>")}`);
692
+ return;
693
+ }
694
+ const deployTarget = targets.find((t) => t.id === target);
695
+ if (!deployTarget) {
696
+ log.error(`Unknown target: ${pc5.bold(target)}`);
697
+ log.info(`Available targets: ${targets.map((t) => t.id).join(", ")}, cloud`);
698
+ process.exit(1);
699
+ }
700
+ const spinner = ora3(`Generating ${deployTarget.name} config...`).start();
701
+ const { file, content } = deployTarget.generate(
702
+ config.project.name,
703
+ config.project.framework
704
+ );
705
+ await writeFile4(resolve2(cwd, file), content + "\n", "utf-8");
706
+ spinner.succeed(`Created ${pc5.bold(file)}`);
707
+ log.blank();
708
+ log.info(`Remember to set ${pc5.bold("RECURSIV_API_KEY")} in your ${deployTarget.name} environment`);
709
+ }
710
+
711
+ // src/commands/auth.ts
712
+ import pc6 from "picocolors";
713
+ async function loginCommand() {
714
+ const cwd = process.cwd();
715
+ const config = await readConfig(cwd);
716
+ const baseUrl = config?.api.baseUrl ?? "https://recursiv.io/api/v1";
717
+ const existing = await readApiKeyFromEnv(cwd);
718
+ if (existing) {
719
+ log.info(`Existing key found: ${pc6.dim(maskKey(existing))}`);
720
+ log.info("Enter a new key to replace it, or press Enter to keep it");
721
+ }
722
+ const apiKey = await getOrCreateApiKey(baseUrl);
723
+ if (apiKey) {
724
+ await updateApiKeyInEnv(cwd, apiKey);
725
+ log.success("API key saved to .env");
726
+ }
727
+ }
728
+ async function logoutCommand() {
729
+ const cwd = process.cwd();
730
+ const existing = await readApiKeyFromEnv(cwd);
731
+ if (!existing) {
732
+ log.info("No API key found in .env");
733
+ return;
734
+ }
735
+ await updateApiKeyInEnv(cwd, "");
736
+ log.success("API key removed from .env");
737
+ }
738
+ async function whoamiCommand() {
739
+ const cwd = process.cwd();
740
+ const config = await readConfig(cwd);
741
+ const baseUrl = config?.api.baseUrl ?? "https://recursiv.io/api/v1";
742
+ const apiKey = await readApiKeyFromEnv(cwd);
743
+ if (!apiKey) {
744
+ log.error("No API key found in .env");
745
+ log.info(`Run ${pc6.bold("recursiv auth login")} to set one`);
746
+ return;
747
+ }
748
+ log.info(`Key: ${pc6.dim(maskKey(apiKey))}`);
749
+ try {
750
+ const res = await fetch(`${baseUrl}/users/me`, {
751
+ headers: {
752
+ Authorization: `Bearer ${apiKey}`,
753
+ Accept: "application/json"
754
+ }
755
+ });
756
+ if (!res.ok) {
757
+ log.error("API key is invalid or expired");
758
+ return;
759
+ }
760
+ const body = await res.json();
761
+ if (body.data.username) log.info(`User: ${pc6.bold(body.data.username)}`);
762
+ if (body.data.email) log.info(`Email: ${body.data.email}`);
763
+ } catch {
764
+ log.warn("Could not reach API \u2014 check your network");
765
+ }
766
+ }
767
+ function maskKey(key) {
768
+ if (key.length <= 12) return "****";
769
+ return key.slice(0, 8) + "..." + key.slice(-4);
770
+ }
771
+
772
+ // src/commands/info.ts
773
+ import pc7 from "picocolors";
774
+ async function infoCommand() {
775
+ const cwd = process.cwd();
776
+ const config = await readConfig(cwd);
777
+ if (!config) {
778
+ log.error("No .recursiv.json found \u2014 are you in a Recursiv project?");
779
+ log.info(`Run ${pc7.bold("create-recursiv-app")} to create a new project`);
780
+ process.exit(1);
781
+ }
782
+ const apiKey = await readApiKeyFromEnv(cwd);
783
+ const pm = detectPackageManager(cwd);
784
+ const template = getTemplate(config.project.template);
785
+ console.log();
786
+ console.log(pc7.bold("Project Info"));
787
+ console.log();
788
+ console.log(` Name: ${pc7.bold(config.project.name)}`);
789
+ console.log(` Template: ${template?.name ?? config.project.template}`);
790
+ console.log(` Framework: ${config.project.framework}`);
791
+ console.log(` Dev port: ${config.dev.port}`);
792
+ console.log(` Dev cmd: ${pc7.dim(config.dev.command)}`);
793
+ console.log(` Pkg mgr: ${pm}`);
794
+ console.log(` API URL: ${pc7.dim(config.api.baseUrl)}`);
795
+ console.log(` API key: ${apiKey ? pc7.green("configured") : pc7.yellow("not set")}`);
796
+ console.log(` Config: ${pc7.dim(".recursiv.json v" + config.version)}`);
797
+ console.log();
798
+ if (apiKey) {
799
+ try {
800
+ const res = await fetch(`${config.api.baseUrl}/users/me`, {
801
+ headers: { Authorization: `Bearer ${apiKey}`, Accept: "application/json" }
802
+ });
803
+ if (res.ok) {
804
+ log.success("API connection OK");
805
+ } else {
806
+ log.warn(`API returned ${res.status} \u2014 key may be invalid`);
807
+ }
808
+ } catch {
809
+ log.warn("Could not reach API");
810
+ }
811
+ }
812
+ }
813
+
814
+ // src/commands/brain.tsx
815
+ import { render } from "ink";
816
+
817
+ // src/ui/app.tsx
818
+ import { useEffect, useMemo, useState } from "react";
819
+ import { Box, Text, useApp, useInput, useStdout } from "ink";
820
+ import { jsx, jsxs } from "react/jsx-runtime";
821
+ var TABS = [
822
+ { key: "overview", label: "Overview" },
823
+ { key: "commands", label: "Commands" },
824
+ { key: "stack", label: "App" }
825
+ ];
826
+ var THEME = {
827
+ accent: "#1FE4C6",
828
+ accentWarm: "#F6D47A",
829
+ accentCool: "#4DB6FF",
830
+ muted: "#6F7C8A",
831
+ bright: "#E9FFFA",
832
+ danger: "#FF6B6B"
833
+ };
834
+ var WORDMARK_COLORS = ["cyan", "blue", "yellow"];
835
+ var WORDMARK_LINES = [
836
+ "#### #### ### ##### # #",
837
+ "# # # # # # # ## #",
838
+ "#### #### ##### # # # #",
839
+ "# # # # # # # # ##",
840
+ "#### # # # # ##### # #"
841
+ ];
842
+ var HERO_LINES = [
843
+ "Neurons, particles, waves.",
844
+ "The brain behind Recursiv.",
845
+ "Signals in motion, products alive.",
846
+ "A living map of your platform.",
847
+ "Bioluminescent systems in sync."
848
+ ];
849
+ var COPY = {
850
+ overview: `# Social.dev Brain
851
+ A neural CLI reflection of your product surface with neurons, particles, and waves.
852
+
853
+ ## What it shows
854
+ - Branded networks, communities, posts, and reactions
855
+ - Real-time chat with human + AI agent threads
856
+ - Tool-enabled agents with sandboxes and integrations
857
+ - Deployments, storage buckets, and databases
858
+
859
+ ## Flow
860
+ 1. Create a project
861
+ 2. Run dev locally
862
+ 3. Deploy anywhere`,
863
+ commands: `# Command Deck
864
+ - \`recursiv\` runs Brain when interactive
865
+ - \`recursiv brain\`
866
+ - \`RECURSIV_BRAIN=1 recursiv\`
867
+ - \`RECURSIV_BRAIN=0 recursiv\`
868
+ - \`recursiv create my-app\`
869
+ - \`recursiv dev --port 3000\`
870
+ - \`recursiv deploy <target>\`
871
+ - \`recursiv auth login | logout | whoami\`
872
+ - \`recursiv info\`
873
+ - \`recursiv users me\`
874
+ - \`recursiv communities list\`
875
+ - \`recursiv posts list\`
876
+ - \`recursiv agents list\``,
877
+ stack: `# App Reflection
878
+ ## Client
879
+ - React Native + Expo
880
+ - NativeWind + Tailwind
881
+
882
+ ## Backend
883
+ - Hono + tRPC
884
+ - Better Auth
885
+
886
+ ## Data + Realtime
887
+ - PostgreSQL + Drizzle ORM
888
+ - Kafka + Socket.IO
889
+
890
+ ## Agents
891
+ - Composio + E2B + OpenRouter`
892
+ };
893
+ var PULSE_FRAMES = ["spark", "pulse", "sync", "bloom"];
894
+ var DOT_FRAMES = [".", "..", "..."];
895
+ var BAR_FRAMES = ["[~ ]", "[~~ ]", "[~~~ ]", "[ ~~~~ ]", "[ ~~~~]", "[ ~~~]", "[ ~~]", "[ ~]"];
896
+ var AURORA_COLORS = ["#1FE4C6", "#4DB6FF", "#8AF0FF", "#F6D47A"];
897
+ var AURORA_COLORS_DEEP = ["#0C2236", "#12314C", "#1E3A5F"];
898
+ var QUICK_ACTIONS = [
899
+ "recursiv brain",
900
+ "create-recursiv-app my-app",
901
+ "recursiv dev --port 3000",
902
+ "recursiv users me",
903
+ "recursiv deploy vercel"
904
+ ];
905
+ var HINTS = ["Tab or arrows to switch tabs.", "1/2/3 to jump instantly.", "q or Esc to exit."];
906
+ var INTEGRATIONS = ["Composio", "E2B", "OpenRouter"];
907
+ var INTRO_DURATION_MS = 5200;
908
+ var INTRO_STEPS = [
909
+ "Igniting cortex",
910
+ "Mapping synapses",
911
+ "Charging particles",
912
+ "Calibrating agents",
913
+ "Locking signal"
914
+ ];
915
+ var INTRO_HINTS = ["Press Space to skip", "Press q to exit"];
916
+ function App(props) {
917
+ const { snapshot, animate } = props;
918
+ const { exit } = useApp();
919
+ const { stdout } = useStdout();
920
+ const [tab, setTab] = useState("overview");
921
+ const [introActive, setIntroActive] = useState(animate);
922
+ const columns = stdout?.columns ?? process.stdout.columns ?? 100;
923
+ const isNarrow = columns < 100;
924
+ const fieldWidth = isNarrow ? Math.max(12, columns - 6) : 26;
925
+ const fieldHeight = isNarrow ? 6 : 10;
926
+ const navWidth = isNarrow ? void 0 : 26;
927
+ const infoWidth = isNarrow ? void 0 : 34;
928
+ const pulseIndex = useTicker(animate, 700, PULSE_FRAMES.length);
929
+ const dotIndex = useTicker(animate, 450, DOT_FRAMES.length);
930
+ const heroIndex = useTicker(animate, 2200, HERO_LINES.length);
931
+ const barIndex = useTicker(animate, 140, BAR_FRAMES.length);
932
+ const sweepIndex = useTicker(animate, 60, Math.max(10, Math.min(columns - 4, 120)));
933
+ const scanIndex = useTicker(animate, 320, 9);
934
+ const introProgress = useProgress(introActive && animate, INTRO_DURATION_MS, 80);
935
+ const introFrame = useTicker(introActive && animate, 60, Math.max(10, columns - 6));
936
+ const pulse = PULSE_FRAMES[pulseIndex];
937
+ const dots = DOT_FRAMES[dotIndex];
938
+ const hero = HERO_LINES[heroIndex];
939
+ const bar = BAR_FRAMES[barIndex];
940
+ useEffect(() => {
941
+ if (!animate) {
942
+ setIntroActive(false);
943
+ return;
944
+ }
945
+ setIntroActive(true);
946
+ const id = setTimeout(() => setIntroActive(false), INTRO_DURATION_MS);
947
+ return () => clearTimeout(id);
948
+ }, [animate]);
949
+ useInput((input, key) => {
950
+ if (input === "q" || key.escape) {
951
+ exit();
952
+ return;
953
+ }
954
+ if (introActive) {
955
+ if (input === " " || key.return || input === "s") {
956
+ setIntroActive(false);
957
+ }
958
+ return;
959
+ }
960
+ if (key.tab || key.rightArrow) {
961
+ const next = (tabIndex(tab) + 1) % TABS.length;
962
+ setTab(TABS[next].key);
963
+ return;
964
+ }
965
+ if (key.leftArrow) {
966
+ const prev = (tabIndex(tab) - 1 + TABS.length) % TABS.length;
967
+ setTab(TABS[prev].key);
968
+ return;
969
+ }
970
+ if (input === "1") setTab("overview");
971
+ if (input === "2") setTab("commands");
972
+ if (input === "3") setTab("stack");
973
+ });
974
+ if (introActive) {
975
+ return /* @__PURE__ */ jsx(
976
+ IntroScreen,
977
+ {
978
+ width: columns,
979
+ height: Math.max(14, fieldHeight + 6),
980
+ progress: introProgress,
981
+ frame: introFrame
982
+ }
983
+ );
984
+ }
985
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
986
+ /* @__PURE__ */ jsx(
987
+ Header,
988
+ {
989
+ hero,
990
+ pulse,
991
+ dots,
992
+ bar,
993
+ sweep: sweepIndex,
994
+ width: columns,
995
+ animate
996
+ }
997
+ ),
998
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: isNarrow ? "column" : "row", children: [
999
+ /* @__PURE__ */ jsx(
1000
+ Box,
1001
+ {
1002
+ flexDirection: "column",
1003
+ width: navWidth,
1004
+ marginRight: isNarrow ? 0 : 2,
1005
+ marginBottom: isNarrow ? 1 : 0,
1006
+ children: /* @__PURE__ */ jsxs(Panel, { title: "Brain Map", accent: THEME.accent, borderStyle: "round", children: [
1007
+ /* @__PURE__ */ jsx(NeuralField, { width: fieldWidth, height: fieldHeight, frame: sweepIndex, animate }),
1008
+ /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: "* neuron o synapse + particle ~ wave" }),
1009
+ /* @__PURE__ */ jsx(Divider, { width: fieldWidth }),
1010
+ /* @__PURE__ */ jsx(SectionTitle, { label: "Navigator" }),
1011
+ /* @__PURE__ */ jsx(Navigation, { active: tab }),
1012
+ /* @__PURE__ */ jsx(Divider, {}),
1013
+ /* @__PURE__ */ jsx(SectionTitle, { label: "Quick Actions" }),
1014
+ QUICK_ACTIONS.map((item) => /* @__PURE__ */ jsxs(Text, { color: THEME.muted, children: [
1015
+ "- ",
1016
+ item
1017
+ ] }, item)),
1018
+ /* @__PURE__ */ jsx(Divider, {}),
1019
+ /* @__PURE__ */ jsx(SectionTitle, { label: "Hints" }),
1020
+ HINTS.map((item) => /* @__PURE__ */ jsxs(Text, { color: THEME.muted, children: [
1021
+ "- ",
1022
+ item
1023
+ ] }, item))
1024
+ ] })
1025
+ }
1026
+ ),
1027
+ /* @__PURE__ */ jsx(
1028
+ Box,
1029
+ {
1030
+ flexDirection: "column",
1031
+ flexGrow: 1,
1032
+ marginRight: isNarrow ? 0 : 2,
1033
+ marginBottom: isNarrow ? 1 : 0,
1034
+ children: /* @__PURE__ */ jsx(Panel, { title: tabTitle(tab), accent: THEME.accentWarm, borderStyle: "double", children: /* @__PURE__ */ jsx(AnimatedMarkdown, { content: COPY[tab], animate }) })
1035
+ }
1036
+ ),
1037
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", width: infoWidth, children: /* @__PURE__ */ jsx(Panel, { title: "Synapses", accent: THEME.accentCool, borderStyle: "round", children: /* @__PURE__ */ jsx(ProjectPanel, { snapshot, animate, dots, frame: sweepIndex, scanIndex }) }) })
1038
+ ] }),
1039
+ /* @__PURE__ */ jsx(Footer, { snapshot, animate })
1040
+ ] });
1041
+ }
1042
+ function Header(props) {
1043
+ const { hero, pulse, dots, bar, sweep, width, animate } = props;
1044
+ const lineWidth = Math.max(32, Math.min(width - 4, 120));
1045
+ const pulseLabel = animate ? `${pulse}${dots}` : "ready";
1046
+ const time = (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit" });
1047
+ const wordmarkPalette = rotatePalette(WORDMARK_COLORS, sweep);
1048
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
1049
+ /* @__PURE__ */ jsx(Wordmark, { lines: WORDMARK_LINES, palette: wordmarkPalette }),
1050
+ /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: hero }),
1051
+ /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: "by Recursiv" }),
1052
+ /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "row", children: [
1053
+ /* @__PURE__ */ jsx(Box, { marginRight: 2, children: /* @__PURE__ */ jsx(StatusPill, { label: "brain", value: pulseLabel, tone: THEME.accent }) }),
1054
+ /* @__PURE__ */ jsx(Box, { marginRight: 2, children: /* @__PURE__ */ jsx(StatusPill, { label: "clock", value: time, tone: THEME.accentWarm }) }),
1055
+ /* @__PURE__ */ jsx(StatusPill, { label: "waves", value: bar, tone: THEME.accentCool })
1056
+ ] }),
1057
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: /* @__PURE__ */ jsx(Aurora, { width: lineWidth, frame: sweep }) }),
1058
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(AccentLine, { width: lineWidth, frame: sweep }) })
1059
+ ] });
1060
+ }
1061
+ function Footer(props) {
1062
+ const { snapshot, animate } = props;
1063
+ const projectHint = snapshot ? `${snapshot.name}` : "no project loaded";
1064
+ const status = animate ? "brain live" : "brain ready";
1065
+ return /* @__PURE__ */ jsxs(Box, { marginTop: 1, flexDirection: "row", justifyContent: "space-between", children: [
1066
+ /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: "Tab to switch | 1/2/3 to jump | q to quit" }),
1067
+ /* @__PURE__ */ jsxs(Text, { color: THEME.muted, children: [
1068
+ status,
1069
+ " | ",
1070
+ projectHint
1071
+ ] })
1072
+ ] });
1073
+ }
1074
+ function Panel(props) {
1075
+ const { title, accent, borderStyle = "round", children } = props;
1076
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", borderStyle, borderColor: accent, paddingX: 1, paddingY: 1, children: [
1077
+ /* @__PURE__ */ jsx(Text, { color: accent, children: title }),
1078
+ /* @__PURE__ */ jsx(Box, { flexDirection: "column", marginTop: 1, children })
1079
+ ] });
1080
+ }
1081
+ function SectionTitle(props) {
1082
+ return /* @__PURE__ */ jsx(Text, { color: THEME.accentWarm, children: props.label });
1083
+ }
1084
+ function Navigation(props) {
1085
+ const { active } = props;
1086
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: TABS.map((tab, index) => {
1087
+ const isActive = tab.key === active;
1088
+ const marker = isActive ? ">" : " ";
1089
+ const color = isActive ? THEME.accent : THEME.muted;
1090
+ return /* @__PURE__ */ jsxs(Text, { color, children: [
1091
+ marker,
1092
+ " ",
1093
+ index + 1,
1094
+ ". ",
1095
+ tab.label
1096
+ ] }, tab.key);
1097
+ }) });
1098
+ }
1099
+ function NeuralField(props) {
1100
+ const { width, height, frame, animate } = props;
1101
+ const lines = useMemo(() => buildNeuralField(width, height, animate ? frame : 0), [width, height, frame, animate]);
1102
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: lines.map((line, index) => {
1103
+ const tone = line.includes("~") || line.includes("*") || line.includes("+") || line.includes("o") || line.includes("O") ? THEME.accentCool : THEME.muted;
1104
+ return /* @__PURE__ */ jsx(Text, { color: tone, children: line }, index);
1105
+ }) });
1106
+ }
1107
+ function ProjectPanel(props) {
1108
+ const { snapshot, animate, dots, frame, scanIndex } = props;
1109
+ if (!snapshot) {
1110
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1111
+ /* @__PURE__ */ jsx(Text, { color: THEME.accentWarm, children: "Not in a Recursiv project." }),
1112
+ /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: "Run create-recursiv-app to start." })
1113
+ ] });
1114
+ }
1115
+ const status = animate ? `firing${dots}` : "quiet";
1116
+ const apiStatus = snapshot.apiKeyConfigured ? "linked" : "missing";
1117
+ const apiTone = snapshot.apiKeyConfigured ? THEME.accentCool : THEME.danger;
1118
+ const meter = buildPulseMeter(animate ? frame : 0, 16);
1119
+ const fields = [
1120
+ { label: "Name", value: snapshot.name },
1121
+ { label: "Template", value: snapshot.template },
1122
+ { label: "Framework", value: snapshot.framework },
1123
+ { label: "Dev port", value: String(snapshot.port) },
1124
+ { label: "Pkg mgr", value: snapshot.packageManager },
1125
+ { label: "API base", value: snapshot.apiBaseUrl, dim: true },
1126
+ { label: "API key", value: apiStatus, tone: apiTone },
1127
+ { label: "Status", value: status, tone: THEME.accent },
1128
+ { label: "Pulse", value: meter, tone: THEME.accentWarm }
1129
+ ];
1130
+ const activeIndex = fields.length > 0 ? scanIndex % fields.length : 0;
1131
+ const scanBar = buildScanBar(activeIndex, 18);
1132
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1133
+ fields.map((field, index) => {
1134
+ const distance = Math.min(
1135
+ Math.abs(index - activeIndex),
1136
+ fields.length - Math.abs(index - activeIndex)
1137
+ );
1138
+ const glow = animate && distance === 0 ? "strong" : animate && distance === 1 ? "soft" : "none";
1139
+ return /* @__PURE__ */ jsx(
1140
+ Field,
1141
+ {
1142
+ label: field.label,
1143
+ value: field.value,
1144
+ dim: field.dim,
1145
+ tone: field.tone,
1146
+ glow
1147
+ },
1148
+ field.label
1149
+ );
1150
+ }),
1151
+ /* @__PURE__ */ jsxs(Text, { color: THEME.accentCool, children: [
1152
+ "Neural scan ",
1153
+ scanBar
1154
+ ] }),
1155
+ /* @__PURE__ */ jsx(Divider, {}),
1156
+ /* @__PURE__ */ jsx(SectionTitle, { label: "Integrations" }),
1157
+ INTEGRATIONS.map((item) => /* @__PURE__ */ jsxs(Text, { color: THEME.muted, children: [
1158
+ "- ",
1159
+ item
1160
+ ] }, item)),
1161
+ /* @__PURE__ */ jsx(Divider, {}),
1162
+ /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: "Dev cmd:" }),
1163
+ /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: snapshot.devCommand })
1164
+ ] });
1165
+ }
1166
+ function Field(props) {
1167
+ const { label, value, dim, tone, glow = "none" } = props;
1168
+ const marker = glow === "strong" ? ">" : glow === "soft" ? "-" : " ";
1169
+ const paddedLabel = `${marker} ${label.padEnd(9, " ")}`;
1170
+ const labelColor = glow === "strong" ? THEME.accentWarm : glow === "soft" ? THEME.accent : THEME.muted;
1171
+ const valueColor = glow === "strong" ? THEME.bright : glow === "soft" ? THEME.accentCool : tone ?? (dim ? THEME.muted : void 0);
1172
+ return /* @__PURE__ */ jsxs(Text, { children: [
1173
+ /* @__PURE__ */ jsx(Text, { color: labelColor, children: paddedLabel }),
1174
+ /* @__PURE__ */ jsx(Text, { color: valueColor, children: value })
1175
+ ] });
1176
+ }
1177
+ function StatusPill(props) {
1178
+ const { label, value, tone } = props;
1179
+ return /* @__PURE__ */ jsxs(Text, { color: tone, children: [
1180
+ "[",
1181
+ label,
1182
+ ": ",
1183
+ value,
1184
+ "]"
1185
+ ] });
1186
+ }
1187
+ function Wordmark(props) {
1188
+ const { lines, palette } = props;
1189
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", alignItems: "center", children: lines.map((line, index) => /* @__PURE__ */ jsx(Text, { color: palette[index % palette.length] ?? THEME.accent, children: line }, index)) });
1190
+ }
1191
+ function ColorLine(props) {
1192
+ return /* @__PURE__ */ jsx(Text, { color: props.color, children: props.text });
1193
+ }
1194
+ function Aurora(props) {
1195
+ const { width, frame } = props;
1196
+ const lineWidth = Math.max(18, width);
1197
+ const lineA = buildAuroraLine(lineWidth, frame, 0);
1198
+ const lineB = buildInterferenceLine(lineWidth, frame + 7, 1);
1199
+ const lineC = buildAuroraLine(lineWidth, frame + 13, 2);
1200
+ const lineD = buildNeuronHalo(lineWidth, frame);
1201
+ const palette = rotatePalette(AURORA_COLORS, frame);
1202
+ const deepPalette = rotatePalette(AURORA_COLORS_DEEP, frame);
1203
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
1204
+ /* @__PURE__ */ jsx(ColorLine, { text: lineA, color: palette[0] }),
1205
+ /* @__PURE__ */ jsx(ColorLine, { text: lineB, color: deepPalette[0] }),
1206
+ /* @__PURE__ */ jsx(ColorLine, { text: lineC, color: palette[1] ?? palette[0] }),
1207
+ /* @__PURE__ */ jsx(ColorLine, { text: lineD, color: palette[2] ?? palette[0] })
1208
+ ] });
1209
+ }
1210
+ function Divider(props = {}) {
1211
+ const { width } = props;
1212
+ const lineWidth = Math.max(12, Math.min(32, width ?? 22));
1213
+ return /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: "-".repeat(lineWidth) });
1214
+ }
1215
+ function AccentLine(props) {
1216
+ const { width, frame } = props;
1217
+ const lineWidth = Math.max(10, width);
1218
+ const head = Math.min(frame % lineWidth, lineWidth - 1);
1219
+ const left = "-".repeat(head);
1220
+ const right = "-".repeat(Math.max(0, lineWidth - head - 1));
1221
+ return /* @__PURE__ */ jsxs(Text, { color: THEME.muted, children: [
1222
+ left,
1223
+ /* @__PURE__ */ jsx(Text, { color: THEME.accent, children: "*" }),
1224
+ right
1225
+ ] });
1226
+ }
1227
+ function AnimatedMarkdown(props) {
1228
+ const { content, animate } = props;
1229
+ const safe = useMemo(() => content.replace(/\r\n/g, "\n"), [content]);
1230
+ const lines = useMemo(() => safe.split("\n"), [safe]);
1231
+ const [visibleLines, setVisibleLines] = useState(animate ? 1 : lines.length);
1232
+ useEffect(() => {
1233
+ if (!animate) {
1234
+ setVisibleLines(lines.length);
1235
+ return;
1236
+ }
1237
+ setVisibleLines(1);
1238
+ const id = setInterval(() => {
1239
+ setVisibleLines((current) => current >= lines.length ? current : current + 1);
1240
+ }, 55);
1241
+ return () => clearInterval(id);
1242
+ }, [animate, lines.length, safe]);
1243
+ const sliceCount = Math.max(1, visibleLines);
1244
+ const text = animate ? lines.slice(0, sliceCount).join("\n") : safe;
1245
+ return /* @__PURE__ */ jsx(MarkdownBlock, { content: text });
1246
+ }
1247
+ function MarkdownBlock(props) {
1248
+ const lines = props.content.split("\n");
1249
+ return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: lines.map((line, index) => {
1250
+ if (line.startsWith("### ")) {
1251
+ return /* @__PURE__ */ jsx(Text, { color: THEME.accentCool, children: line.slice(4) }, index);
1252
+ }
1253
+ if (line.startsWith("## ")) {
1254
+ return /* @__PURE__ */ jsx(Text, { color: THEME.accentWarm, children: line.slice(3) }, index);
1255
+ }
1256
+ if (line.startsWith("# ")) {
1257
+ return /* @__PURE__ */ jsx(Text, { color: THEME.accent, children: line.slice(2) }, index);
1258
+ }
1259
+ if (line.startsWith("- ")) {
1260
+ return /* @__PURE__ */ jsxs(Text, { color: THEME.muted, children: [
1261
+ "- ",
1262
+ line.slice(2)
1263
+ ] }, index);
1264
+ }
1265
+ if (/^\d+\.\s/.test(line)) {
1266
+ return /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: line }, index);
1267
+ }
1268
+ if (line.trim().length === 0) {
1269
+ return /* @__PURE__ */ jsx(Text, { children: " " }, index);
1270
+ }
1271
+ return /* @__PURE__ */ jsx(Text, { color: THEME.bright, children: line }, index);
1272
+ }) });
1273
+ }
1274
+ function useTicker(animate, intervalMs, length) {
1275
+ const [tick, setTick] = useState(0);
1276
+ useEffect(() => {
1277
+ if (!animate || length <= 0) {
1278
+ setTick(0);
1279
+ return;
1280
+ }
1281
+ const id = setInterval(() => {
1282
+ setTick((current) => (current + 1) % length);
1283
+ }, intervalMs);
1284
+ return () => clearInterval(id);
1285
+ }, [animate, intervalMs, length]);
1286
+ return tick;
1287
+ }
1288
+ function tabIndex(tab) {
1289
+ return TABS.findIndex((item) => item.key === tab);
1290
+ }
1291
+ function tabTitle(tab) {
1292
+ const active = TABS.find((item) => item.key === tab);
1293
+ return active ? active.label : "Overview";
1294
+ }
1295
+ function buildNeuralField(width, height, frame) {
1296
+ const safeWidth = Math.max(8, width);
1297
+ const safeHeight = Math.max(4, height);
1298
+ const grid = Array.from({ length: safeHeight }, () => Array(safeWidth).fill(" "));
1299
+ const phase = Math.floor(frame / 6);
1300
+ const rng = makeRng(phase * 109 + 97);
1301
+ const nodes = [];
1302
+ const place = (x, y, char, priority = 0) => {
1303
+ if (x < 0 || x >= safeWidth || y < 0 || y >= safeHeight) return;
1304
+ const current = grid[y][x];
1305
+ const currentPriority = current === "*" || current === "O" ? 3 : current === "o" ? 2 : current === "~" ? 1 : 0;
1306
+ if (priority >= currentPriority || current === " ") {
1307
+ grid[y][x] = char;
1308
+ }
1309
+ };
1310
+ for (let y = 0; y < safeHeight; y += 1) {
1311
+ for (let x = 0; x < safeWidth; x += 1) {
1312
+ const roll = rng();
1313
+ if (roll < 0.018) {
1314
+ grid[y][x] = "*";
1315
+ nodes.push({ x, y });
1316
+ } else if (roll < 0.04) {
1317
+ grid[y][x] = "o";
1318
+ nodes.push({ x, y });
1319
+ } else if (roll < 0.12) {
1320
+ grid[y][x] = ".";
1321
+ }
1322
+ }
1323
+ }
1324
+ const mid = Math.floor(safeHeight / 2);
1325
+ const amp = Math.max(1, Math.floor(safeHeight / 3));
1326
+ for (let x = 0; x < safeWidth; x += 1) {
1327
+ const y = Math.round(mid + Math.sin((x + frame * 0.6) / 3) * amp);
1328
+ place(x, y, "~", 1);
1329
+ if ((x + frame) % 11 === 0) place(x, y, "*", 3);
1330
+ }
1331
+ const mid2 = Math.floor(safeHeight / 3);
1332
+ const amp2 = Math.max(1, Math.floor(safeHeight / 4));
1333
+ for (let x = 0; x < safeWidth; x += 1) {
1334
+ const y = Math.round(mid2 + Math.sin((x + frame * 0.4) / 2.6) * amp2);
1335
+ place(x, y, "~", 1);
1336
+ if ((x + frame * 2) % 17 === 0) place(x, y, "o", 2);
1337
+ }
1338
+ const particleCount = Math.max(2, Math.floor(safeWidth / 12));
1339
+ for (let i = 0; i < particleCount; i += 1) {
1340
+ const particleX = (frame * (i + 2) + i * 7) % safeWidth;
1341
+ const particleY = (frame * (i + 3) + i * 5) % safeHeight;
1342
+ place(particleX, particleY, "+", 2);
1343
+ }
1344
+ nodes.forEach((node) => {
1345
+ if (rng() < 0.35) {
1346
+ const length = 2 + Math.floor(rng() * 6);
1347
+ const drift = rng() < 0.5 ? 1 : -1;
1348
+ for (let step = 1; step <= length; step += 1) {
1349
+ const x = node.x + step;
1350
+ const y = node.y + (rng() < 0.3 ? drift : 0);
1351
+ place(x, y, rng() < 0.5 ? "-" : "=", 0);
1352
+ }
1353
+ }
1354
+ if ((node.x + frame) % 14 === 0) {
1355
+ place(node.x, node.y, "O", 3);
1356
+ }
1357
+ });
1358
+ return grid.map((row) => row.join(""));
1359
+ }
1360
+ function buildAuroraLine(width, frame, offset) {
1361
+ const safeWidth = Math.max(10, width);
1362
+ const chars = [" ", " ", ".", ":", "-", "=", "+", "*"];
1363
+ let line = "";
1364
+ for (let x = 0; x < safeWidth; x += 1) {
1365
+ const phase = (x + frame * 0.9 + offset * 7) / 4;
1366
+ const wave = Math.sin(phase) * 0.6 + Math.sin(phase / 2 + offset) * 0.4 + 1;
1367
+ const normalized = Math.max(0, Math.min(1, wave / 2));
1368
+ const idx = Math.min(chars.length - 1, Math.floor(normalized * (chars.length - 1)));
1369
+ line += chars[idx];
1370
+ }
1371
+ return line;
1372
+ }
1373
+ function buildInterferenceLine(width, frame, offset) {
1374
+ const safeWidth = Math.max(10, width);
1375
+ const chars = [" ", ".", ":", "-", "=", "+", "*", "#"];
1376
+ let line = "";
1377
+ for (let x = 0; x < safeWidth; x += 1) {
1378
+ const waveA = Math.sin((x + frame * 0.6 + offset * 5) / 3);
1379
+ const waveB = Math.sin((x * 0.6 + frame * 0.3) / 2.1);
1380
+ const waveC = Math.sin((x * 1.2 + frame * 0.9) / 4.6);
1381
+ const mix = (waveA + waveB + waveC + 3) / 6;
1382
+ const idx = Math.min(chars.length - 1, Math.floor(mix * (chars.length - 1)));
1383
+ line += chars[idx];
1384
+ }
1385
+ return line;
1386
+ }
1387
+ function buildNeuronHalo(width, frame) {
1388
+ const safeWidth = Math.max(10, width);
1389
+ const rng = makeRng((Math.floor(frame / 4) + 11) * 233);
1390
+ let line = "";
1391
+ for (let x = 0; x < safeWidth; x += 1) {
1392
+ const roll = rng();
1393
+ if (roll < 0.04) line += "*";
1394
+ else if (roll < 0.08) line += "o";
1395
+ else if (roll < 0.11) line += ".";
1396
+ else line += " ";
1397
+ }
1398
+ return line;
1399
+ }
1400
+ function buildPulseMeter(frame, width) {
1401
+ const safeWidth = Math.max(8, width);
1402
+ const head = frame % safeWidth;
1403
+ let line = "";
1404
+ for (let i = 0; i < safeWidth; i += 1) {
1405
+ if (i === head) {
1406
+ line += "*";
1407
+ } else if (Math.abs(i - head) === 1) {
1408
+ line += "+";
1409
+ } else if ((i + frame) % 5 === 0) {
1410
+ line += "-";
1411
+ } else {
1412
+ line += ".";
1413
+ }
1414
+ }
1415
+ return line;
1416
+ }
1417
+ function buildScanBar(position, width) {
1418
+ const safeWidth = Math.max(8, width);
1419
+ if (position < 0) return "[" + ".".repeat(safeWidth) + "]";
1420
+ const head = position % safeWidth;
1421
+ let line = "";
1422
+ for (let i = 0; i < safeWidth; i += 1) {
1423
+ if (i === head) line += ">";
1424
+ else if (Math.abs(i - head) === 1) line += "=";
1425
+ else line += ".";
1426
+ }
1427
+ return `[${line}]`;
1428
+ }
1429
+ function makeRng(seed) {
1430
+ let state = seed >>> 0;
1431
+ return () => {
1432
+ state = state * 1664525 + 1013904223 >>> 0;
1433
+ return state / 4294967295;
1434
+ };
1435
+ }
1436
+ function rotatePalette(palette, frame) {
1437
+ if (palette.length === 0) return palette;
1438
+ const offset = frame % palette.length;
1439
+ return [...palette.slice(offset), ...palette.slice(0, offset)];
1440
+ }
1441
+ function useProgress(active, durationMs, stepMs) {
1442
+ const [progress, setProgress] = useState(active ? 0 : 1);
1443
+ useEffect(() => {
1444
+ if (!active) {
1445
+ setProgress(1);
1446
+ return;
1447
+ }
1448
+ setProgress(0);
1449
+ const start = Date.now();
1450
+ const id = setInterval(() => {
1451
+ const value = Math.min(1, (Date.now() - start) / durationMs);
1452
+ setProgress(value);
1453
+ if (value >= 1) clearInterval(id);
1454
+ }, stepMs);
1455
+ return () => clearInterval(id);
1456
+ }, [active, durationMs, stepMs]);
1457
+ return progress;
1458
+ }
1459
+ function IntroScreen(props) {
1460
+ const { width, height, progress, frame } = props;
1461
+ const fieldWidth = Math.max(24, width - 6);
1462
+ const fieldHeight = Math.max(10, Math.min(14, height));
1463
+ const steps = INTRO_STEPS.map((label, index) => ({
1464
+ label,
1465
+ done: progress >= (index + 1) / INTRO_STEPS.length
1466
+ }));
1467
+ const bar = buildProgressBar(progress, Math.max(16, Math.min(34, Math.floor(width / 3))));
1468
+ const palette = rotatePalette(WORDMARK_COLORS, frame);
1469
+ return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: [
1470
+ /* @__PURE__ */ jsxs(Box, { flexDirection: "column", alignItems: "center", children: [
1471
+ /* @__PURE__ */ jsx(Wordmark, { lines: WORDMARK_LINES, palette }),
1472
+ /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: "Neural ignition sequence" })
1473
+ ] }),
1474
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", alignItems: "center", children: /* @__PURE__ */ jsx(ColorLine, { text: bar, color: THEME.accentWarm }) }),
1475
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, flexDirection: "column", children: steps.map((step, index) => /* @__PURE__ */ jsxs(Text, { color: step.done ? THEME.accent : THEME.muted, children: [
1476
+ "[",
1477
+ step.done ? "x" : " ",
1478
+ "] ",
1479
+ step.label
1480
+ ] }, index)) }),
1481
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: /* @__PURE__ */ jsx(NeuralField, { width: fieldWidth, height: fieldHeight, frame, animate: true }) }),
1482
+ /* @__PURE__ */ jsx(Box, { marginTop: 1, children: INTRO_HINTS.map((hint) => /* @__PURE__ */ jsx(Text, { color: THEME.muted, children: hint }, hint)) })
1483
+ ] });
1484
+ }
1485
+ function buildProgressBar(progress, width) {
1486
+ const safeWidth = Math.max(10, width);
1487
+ const filled = Math.round(progress * safeWidth);
1488
+ const left = "=".repeat(filled);
1489
+ const right = ".".repeat(Math.max(0, safeWidth - filled));
1490
+ return `[${left}${right}]`;
1491
+ }
1492
+
1493
+ // src/commands/brain.tsx
1494
+ import { jsx as jsx2 } from "react/jsx-runtime";
1495
+ async function brainCommand(opts) {
1496
+ const cwd = process.cwd();
1497
+ const config = await readConfig(cwd);
1498
+ const apiKey = await readApiKeyFromEnv(cwd);
1499
+ const packageManager = detectPackageManager(cwd);
1500
+ const snapshot = config ? {
1501
+ name: config.project.name,
1502
+ template: config.project.template,
1503
+ framework: config.project.framework,
1504
+ port: config.dev.port,
1505
+ devCommand: config.dev.command,
1506
+ apiBaseUrl: config.api.baseUrl,
1507
+ apiKeyConfigured: Boolean(apiKey),
1508
+ packageManager
1509
+ } : null;
1510
+ const app = render(/* @__PURE__ */ jsx2(App, { snapshot, animate: opts.anim !== false }));
1511
+ await app.waitUntilExit();
1512
+ }
1513
+
1514
+ // src/commands/studio.tsx
1515
+ async function studioCommand(opts) {
1516
+ await brainCommand(opts);
1517
+ }
1518
+
1519
+ // src/commands/api.ts
1520
+ import pc9 from "picocolors";
1521
+
1522
+ // src/lib/api.ts
1523
+ import pc8 from "picocolors";
1524
+ async function getApiContext(cwd) {
1525
+ const config = await readConfig(cwd);
1526
+ const apiKey = await readApiKeyFromEnv(cwd);
1527
+ if (!apiKey) {
1528
+ log.error("No API key configured.");
1529
+ log.info(`Run ${pc8.bold("recursiv auth login")} to set your key.`);
1530
+ process.exit(1);
1531
+ }
1532
+ return {
1533
+ baseUrl: config?.api.baseUrl ?? "https://recursiv.io/api/v1",
1534
+ apiKey
1535
+ };
1536
+ }
1537
+ async function apiRequest(ctx, opts) {
1538
+ const url = new URL(opts.path, ctx.baseUrl.endsWith("/") ? ctx.baseUrl : ctx.baseUrl + "/");
1539
+ if (opts.query) {
1540
+ Object.entries(opts.query).forEach(([key, value]) => {
1541
+ if (value === void 0 || value === null || value === "") return;
1542
+ url.searchParams.set(key, String(value));
1543
+ });
1544
+ }
1545
+ const res = await fetch(url, {
1546
+ method: opts.method,
1547
+ headers: {
1548
+ Authorization: `Bearer ${ctx.apiKey}`,
1549
+ Accept: "application/json",
1550
+ ...opts.body ? { "Content-Type": "application/json" } : {}
1551
+ },
1552
+ body: opts.body ? JSON.stringify(opts.body) : void 0
1553
+ });
1554
+ const text = await res.text();
1555
+ const payload = safeJson(text);
1556
+ if (!res.ok) {
1557
+ const message = payload?.error?.message || payload?.message || `Request failed (${res.status} ${res.statusText})`;
1558
+ throw new Error(message);
1559
+ }
1560
+ return payload ?? {};
1561
+ }
1562
+ function safeJson(text) {
1563
+ if (!text) return null;
1564
+ try {
1565
+ return JSON.parse(text);
1566
+ } catch {
1567
+ return null;
1568
+ }
1569
+ }
1570
+
1571
+ // src/commands/api.ts
1572
+ async function usersMeCommand(opts) {
1573
+ const ctx = await getApiContext(process.cwd());
1574
+ const res = await apiRequest(ctx, { method: "GET", path: "users/me" });
1575
+ output(res, opts.json, (data) => {
1576
+ log.blank();
1577
+ console.log(pc9.bold("User"));
1578
+ console.log(` ${pc9.dim("id")}: ${data.id}`);
1579
+ console.log(` ${pc9.dim("name")}: ${data.name ?? "-"}`);
1580
+ console.log(` ${pc9.dim("username")}: ${data.username ?? "-"}`);
1581
+ console.log(` ${pc9.dim("email")}: ${data.email ?? "-"}`);
1582
+ console.log(` ${pc9.dim("followers")}: ${data.followers_count ?? 0}`);
1583
+ console.log(` ${pc9.dim("following")}: ${data.following_count ?? 0}`);
1584
+ console.log(` ${pc9.dim("posts")}: ${data.posts_count ?? 0}`);
1585
+ log.blank();
1586
+ });
1587
+ }
1588
+ async function communitiesListCommand(opts) {
1589
+ const ctx = await getApiContext(process.cwd());
1590
+ const res = await apiRequest(ctx, {
1591
+ method: "GET",
1592
+ path: "communities",
1593
+ query: {
1594
+ limit: opts.limit,
1595
+ offset: opts.offset
1596
+ }
1597
+ });
1598
+ output(res, opts.json, (data, meta) => {
1599
+ log.blank();
1600
+ console.log(pc9.bold("Communities"));
1601
+ data.forEach((comm) => {
1602
+ const name = pc9.bold(comm.name);
1603
+ const slug = comm.slug ? pc9.dim(`/${comm.slug}`) : "";
1604
+ const members = pc9.dim(`members ${comm.member_count ?? 0}`);
1605
+ console.log(` ${name} ${slug} ${members}`);
1606
+ if (comm.description) console.log(` ${pc9.dim(comm.description)}`);
1607
+ });
1608
+ printMeta(meta);
1609
+ });
1610
+ }
1611
+ async function communitiesCreateCommand(opts) {
1612
+ if (!opts.name || !opts.slug) {
1613
+ log.error("Missing required flags: --name and --slug");
1614
+ process.exit(1);
1615
+ }
1616
+ const ctx = await getApiContext(process.cwd());
1617
+ const res = await apiRequest(ctx, {
1618
+ method: "POST",
1619
+ path: "communities",
1620
+ body: {
1621
+ name: opts.name,
1622
+ slug: opts.slug,
1623
+ description: opts.description,
1624
+ privacy: opts.privacy ?? "public"
1625
+ }
1626
+ });
1627
+ output(res, opts.json, (data) => {
1628
+ log.success(`Community created: ${pc9.bold(data.name)} (${data.id})`);
1629
+ });
1630
+ }
1631
+ async function postsListCommand(opts) {
1632
+ const ctx = await getApiContext(process.cwd());
1633
+ const res = await apiRequest(ctx, {
1634
+ method: "GET",
1635
+ path: "posts",
1636
+ query: {
1637
+ limit: opts.limit,
1638
+ offset: opts.offset,
1639
+ community_id: opts.communityId,
1640
+ author_id: opts.authorId
1641
+ }
1642
+ });
1643
+ output(res, opts.json, (data, meta) => {
1644
+ log.blank();
1645
+ console.log(pc9.bold("Posts"));
1646
+ data.forEach((post) => {
1647
+ const author = post.author?.username ? `@${post.author.username}` : "unknown";
1648
+ const content = truncate(post.content ?? "", 100);
1649
+ console.log(` ${pc9.bold(author)} ${pc9.dim(post.id)}`);
1650
+ console.log(` ${content}`);
1651
+ });
1652
+ printMeta(meta);
1653
+ });
1654
+ }
1655
+ async function postsCreateCommand(opts) {
1656
+ if (!opts.content) {
1657
+ log.error("Missing required flag: --content");
1658
+ process.exit(1);
1659
+ }
1660
+ const ctx = await getApiContext(process.cwd());
1661
+ const tagIds = opts.tagIds ? opts.tagIds.split(",").map((id) => id.trim()).filter(Boolean) : void 0;
1662
+ const res = await apiRequest(ctx, {
1663
+ method: "POST",
1664
+ path: "posts",
1665
+ body: {
1666
+ content: opts.content,
1667
+ content_format: opts.markdown ? "markdown" : "plain",
1668
+ community_id: opts.communityId,
1669
+ organization_id: opts.organizationId,
1670
+ reply_to_id: opts.replyToId,
1671
+ tag_ids: tagIds
1672
+ }
1673
+ });
1674
+ output(res, opts.json, (data) => {
1675
+ log.success(`Post created: ${pc9.dim(data.id)}`);
1676
+ });
1677
+ }
1678
+ async function projectsListCommand(opts) {
1679
+ const ctx = await getApiContext(process.cwd());
1680
+ const res = await apiRequest(ctx, {
1681
+ method: "GET",
1682
+ path: "projects",
1683
+ query: {
1684
+ limit: opts.limit,
1685
+ offset: opts.offset,
1686
+ organization_id: opts.organizationId
1687
+ }
1688
+ });
1689
+ output(res, opts.json, (data, meta) => {
1690
+ log.blank();
1691
+ console.log(pc9.bold("Projects"));
1692
+ data.forEach((proj) => {
1693
+ const name = pc9.bold(proj.name);
1694
+ const slug = proj.slug ? pc9.dim(`/${proj.slug}`) : "";
1695
+ const org = proj.organization?.slug ? pc9.dim(`org ${proj.organization.slug}`) : "";
1696
+ console.log(` ${name} ${slug} ${org}`);
1697
+ });
1698
+ printMeta(meta);
1699
+ });
1700
+ }
1701
+ async function projectsCreateCommand(opts) {
1702
+ if (!opts.organizationId || !opts.name) {
1703
+ log.error("Missing required flags: --org and --name");
1704
+ process.exit(1);
1705
+ }
1706
+ const ctx = await getApiContext(process.cwd());
1707
+ const res = await apiRequest(ctx, {
1708
+ method: "POST",
1709
+ path: "projects",
1710
+ body: {
1711
+ organization_id: opts.organizationId,
1712
+ name: opts.name,
1713
+ slug: opts.slug,
1714
+ repo_url: opts.repoUrl,
1715
+ template_source: opts.templateSource ?? "default",
1716
+ custom_template_url: opts.templateUrl
1717
+ }
1718
+ });
1719
+ output(res, opts.json, (data) => {
1720
+ log.success(`Project created: ${pc9.bold(data.name)} (${data.id})`);
1721
+ });
1722
+ }
1723
+ async function agentsListCommand(opts) {
1724
+ const ctx = await getApiContext(process.cwd());
1725
+ const res = await apiRequest(ctx, {
1726
+ method: "GET",
1727
+ path: "agents",
1728
+ query: {
1729
+ limit: opts.limit,
1730
+ offset: opts.offset
1731
+ }
1732
+ });
1733
+ output(res, opts.json, (data, meta) => {
1734
+ log.blank();
1735
+ console.log(pc9.bold("Agents"));
1736
+ data.forEach((agent) => {
1737
+ const name = pc9.bold(agent.name);
1738
+ const handle = agent.username ? pc9.dim(`@${agent.username}`) : "";
1739
+ const model = agent.model ? pc9.dim(agent.model) : "";
1740
+ console.log(` ${name} ${handle} ${model}`);
1741
+ });
1742
+ printMeta(meta);
1743
+ });
1744
+ }
1745
+ async function agentsCreateCommand(opts) {
1746
+ if (!opts.name || !opts.username) {
1747
+ log.error("Missing required flags: --name and --username");
1748
+ process.exit(1);
1749
+ }
1750
+ const ctx = await getApiContext(process.cwd());
1751
+ const res = await apiRequest(ctx, {
1752
+ method: "POST",
1753
+ path: "agents",
1754
+ body: {
1755
+ name: opts.name,
1756
+ username: opts.username,
1757
+ bio: opts.bio,
1758
+ model: opts.model,
1759
+ system_prompt: opts.systemPrompt,
1760
+ social_mode: opts.socialMode,
1761
+ post_frequency: opts.postFrequency,
1762
+ tool_mode: opts.toolMode,
1763
+ daily_request_limit: opts.dailyRequestLimit ? Number(opts.dailyRequestLimit) : void 0,
1764
+ organization_id: opts.organizationId
1765
+ }
1766
+ });
1767
+ output(res, opts.json, (data) => {
1768
+ log.success(`Agent created: ${pc9.bold(data.name)} (${data.id})`);
1769
+ });
1770
+ }
1771
+ function output(res, json, render2) {
1772
+ if (json) {
1773
+ console.log(JSON.stringify(res, null, 2));
1774
+ return;
1775
+ }
1776
+ render2(res.data, res.meta);
1777
+ }
1778
+ function printMeta(meta) {
1779
+ if (!meta) return;
1780
+ log.blank();
1781
+ console.log(pc9.dim(`limit ${meta.limit ?? 0} offset ${meta.offset ?? 0} has_more ${meta.has_more ?? false}`));
1782
+ log.blank();
1783
+ }
1784
+ function truncate(value, max) {
1785
+ if (value.length <= max) return value;
1786
+ return value.slice(0, max - 1) + "\u2026";
1787
+ }
1788
+
1789
+ // src/bin/recursiv.ts
1790
+ var program = new Command();
1791
+ program.name("recursiv").description("Recursiv CLI \u2014 build and ship apps with AI").version("0.1.0");
1792
+ program.command("create [name]").description("Create a new Recursiv application").action(async (name) => {
1793
+ try {
1794
+ await initCommand(name);
1795
+ } catch (err) {
1796
+ console.error(err instanceof Error ? err.message : err);
1797
+ process.exit(1);
1798
+ }
1799
+ });
1800
+ program.command("dev").description("Start the development server").option("-p, --port <port>", "port to run on").action(async (opts) => {
1801
+ try {
1802
+ await devCommand(opts);
1803
+ } catch (err) {
1804
+ console.error(err instanceof Error ? err.message : err);
1805
+ process.exit(1);
1806
+ }
1807
+ });
1808
+ program.command("deploy <target>").description("Generate deployment config (vercel, docker, render, railway, cloud)").action(async (target) => {
1809
+ try {
1810
+ await deployCommand(target);
1811
+ } catch (err) {
1812
+ console.error(err instanceof Error ? err.message : err);
1813
+ process.exit(1);
1814
+ }
1815
+ });
1816
+ var auth = program.command("auth").description("Manage API authentication");
1817
+ auth.command("login").description("Set or update your API key").action(async () => {
1818
+ try {
1819
+ await loginCommand();
1820
+ } catch (err) {
1821
+ console.error(err instanceof Error ? err.message : err);
1822
+ process.exit(1);
1823
+ }
1824
+ });
1825
+ auth.command("logout").description("Remove API key from .env").action(async () => {
1826
+ try {
1827
+ await logoutCommand();
1828
+ } catch (err) {
1829
+ console.error(err instanceof Error ? err.message : err);
1830
+ process.exit(1);
1831
+ }
1832
+ });
1833
+ auth.command("whoami").description("Show current authenticated user").action(async () => {
1834
+ try {
1835
+ await whoamiCommand();
1836
+ } catch (err) {
1837
+ console.error(err instanceof Error ? err.message : err);
1838
+ process.exit(1);
1839
+ }
1840
+ });
1841
+ program.command("info").description("Display project information").action(async () => {
1842
+ try {
1843
+ await infoCommand();
1844
+ } catch (err) {
1845
+ console.error(err instanceof Error ? err.message : err);
1846
+ process.exit(1);
1847
+ }
1848
+ });
1849
+ var users = program.command("users").description("Users API");
1850
+ users.command("me").description("Show current authenticated user").option("--json", "Output JSON").action(async (opts) => {
1851
+ try {
1852
+ await usersMeCommand(opts);
1853
+ } catch (err) {
1854
+ console.error(err instanceof Error ? err.message : err);
1855
+ process.exit(1);
1856
+ }
1857
+ });
1858
+ var communities = program.command("communities").description("Communities API");
1859
+ communities.command("list").description("List communities").option("--limit <limit>", "number of results").option("--offset <offset>", "pagination offset").option("--json", "Output JSON").action(async (opts) => {
1860
+ try {
1861
+ await communitiesListCommand(opts);
1862
+ } catch (err) {
1863
+ console.error(err instanceof Error ? err.message : err);
1864
+ process.exit(1);
1865
+ }
1866
+ });
1867
+ communities.command("create").description("Create a community").requiredOption("--name <name>", "community name").requiredOption("--slug <slug>", "community slug").option("--description <description>", "description").option("--privacy <privacy>", "public | private | hidden", "public").option("--json", "Output JSON").action(async (opts) => {
1868
+ try {
1869
+ await communitiesCreateCommand(opts);
1870
+ } catch (err) {
1871
+ console.error(err instanceof Error ? err.message : err);
1872
+ process.exit(1);
1873
+ }
1874
+ });
1875
+ var posts = program.command("posts").description("Posts API");
1876
+ posts.command("list").description("List posts").option("--community-id <id>", "filter by community").option("--author-id <id>", "filter by author").option("--limit <limit>", "number of results").option("--offset <offset>", "pagination offset").option("--json", "Output JSON").action(async (opts) => {
1877
+ try {
1878
+ await postsListCommand(opts);
1879
+ } catch (err) {
1880
+ console.error(err instanceof Error ? err.message : err);
1881
+ process.exit(1);
1882
+ }
1883
+ });
1884
+ posts.command("create").description("Create a post").requiredOption("--content <content>", "post content").option("--markdown", "content is markdown").option("--community-id <id>", "community id").option("--organization-id <id>", "organization id").option("--reply-to-id <id>", "reply to post id").option("--tag-ids <ids>", "comma-separated tag ids").option("--json", "Output JSON").action(async (opts) => {
1885
+ try {
1886
+ await postsCreateCommand(opts);
1887
+ } catch (err) {
1888
+ console.error(err instanceof Error ? err.message : err);
1889
+ process.exit(1);
1890
+ }
1891
+ });
1892
+ var projects = program.command("projects").description("Projects API");
1893
+ projects.command("list").description("List projects").option("--org <id>", "organization id").option("--limit <limit>", "number of results").option("--offset <offset>", "pagination offset").option("--json", "Output JSON").action(async (opts) => {
1894
+ try {
1895
+ await projectsListCommand({
1896
+ organizationId: opts.org,
1897
+ limit: opts.limit,
1898
+ offset: opts.offset,
1899
+ json: opts.json
1900
+ });
1901
+ } catch (err) {
1902
+ console.error(err instanceof Error ? err.message : err);
1903
+ process.exit(1);
1904
+ }
1905
+ });
1906
+ projects.command("create").description("Create a project").requiredOption("--org <id>", "organization id").requiredOption("--name <name>", "project name").option("--slug <slug>", "project slug").option("--repo <url>", "repo URL").option("--template <source>", "default | custom", "default").option("--template-url <url>", "custom template URL").option("--json", "Output JSON").action(async (opts) => {
1907
+ try {
1908
+ await projectsCreateCommand({
1909
+ organizationId: opts.org,
1910
+ name: opts.name,
1911
+ slug: opts.slug,
1912
+ repoUrl: opts.repo,
1913
+ templateSource: opts.template,
1914
+ templateUrl: opts.templateUrl,
1915
+ json: opts.json
1916
+ });
1917
+ } catch (err) {
1918
+ console.error(err instanceof Error ? err.message : err);
1919
+ process.exit(1);
1920
+ }
1921
+ });
1922
+ var agents = program.command("agents").description("Agents API");
1923
+ agents.command("list").description("List agents").option("--limit <limit>", "number of results").option("--offset <offset>", "pagination offset").option("--json", "Output JSON").action(async (opts) => {
1924
+ try {
1925
+ await agentsListCommand(opts);
1926
+ } catch (err) {
1927
+ console.error(err instanceof Error ? err.message : err);
1928
+ process.exit(1);
1929
+ }
1930
+ });
1931
+ agents.command("create").description("Create an AI agent").requiredOption("--name <name>", "agent name").requiredOption("--username <username>", "agent username").option("--bio <bio>", "agent bio").option("--model <model>", "model id").option("--system-prompt <prompt>", "system prompt").option("--social-mode <mode>", "chat_only | chat_post").option("--post-frequency <freq>", "never | light | medium | heavy").option("--tool-mode <mode>", "chat_only | permission | autonomous").option("--daily-request-limit <n>", "daily request limit").option("--org <id>", "organization id").option("--json", "Output JSON").action(async (opts) => {
1932
+ try {
1933
+ await agentsCreateCommand({
1934
+ name: opts.name,
1935
+ username: opts.username,
1936
+ bio: opts.bio,
1937
+ model: opts.model,
1938
+ systemPrompt: opts.systemPrompt,
1939
+ socialMode: opts.socialMode,
1940
+ postFrequency: opts.postFrequency,
1941
+ toolMode: opts.toolMode,
1942
+ dailyRequestLimit: opts.dailyRequestLimit,
1943
+ organizationId: opts.org,
1944
+ json: opts.json
1945
+ });
1946
+ } catch (err) {
1947
+ console.error(err instanceof Error ? err.message : err);
1948
+ process.exit(1);
1949
+ }
1950
+ });
1951
+ program.command("brain").description("Launch the interactive Recursiv brain").option("--no-anim", "Disable animations").action(async (opts) => {
1952
+ try {
1953
+ await brainCommand(opts);
1954
+ } catch (err) {
1955
+ console.error(err instanceof Error ? err.message : err);
1956
+ process.exit(1);
1957
+ }
1958
+ });
1959
+ program.command("studio").description("Legacy alias for brain").option("--no-anim", "Disable animations").action(async (opts) => {
1960
+ try {
1961
+ await studioCommand(opts);
1962
+ } catch (err) {
1963
+ console.error(err instanceof Error ? err.message : err);
1964
+ process.exit(1);
1965
+ }
1966
+ });
1967
+ async function runDefaultBrain() {
1968
+ try {
1969
+ await brainCommand({});
1970
+ } catch (err) {
1971
+ console.error(err instanceof Error ? err.message : err);
1972
+ process.exit(1);
1973
+ }
1974
+ }
1975
+ function shouldAutoLaunchBrain(args) {
1976
+ if (process.env.RECURSIV_BRAIN === "1") return true;
1977
+ if (process.env.RECURSIV_BRAIN === "0") return false;
1978
+ if (process.env.RECURSIV_STUDIO === "1") return true;
1979
+ if (process.env.RECURSIV_STUDIO === "0") return false;
1980
+ if (args.length > 0) return false;
1981
+ if (!process.stdout.isTTY) return false;
1982
+ if (process.env.CI) return false;
1983
+ return true;
1984
+ }
1985
+ async function main() {
1986
+ const args = process.argv.slice(2);
1987
+ if (args.includes("--brain") || args.includes("--studio")) {
1988
+ const filtered = args.filter((arg) => arg !== "--brain" && arg !== "--studio");
1989
+ process.argv = [process.argv[0], process.argv[1], ...filtered];
1990
+ const noAnim = filtered.includes("--no-anim");
1991
+ await brainCommand({ anim: !noAnim });
1992
+ return;
1993
+ }
1994
+ if (shouldAutoLaunchBrain(args)) {
1995
+ await runDefaultBrain();
1996
+ return;
1997
+ }
1998
+ program.parse();
1999
+ }
2000
+ void main();
2001
+ //# sourceMappingURL=recursiv.js.map