@rooaak/cli 0.1.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +121 -0
  2. package/dist/cli.d.ts +2 -0
  3. package/dist/cli.d.ts.map +1 -0
  4. package/dist/cli.js +4 -0
  5. package/dist/index.d.ts +2 -0
  6. package/dist/index.d.ts.map +1 -0
  7. package/dist/index.js +1 -0
  8. package/dist/src/commands/auth-login.d.ts +3 -0
  9. package/dist/src/commands/auth-login.d.ts.map +1 -0
  10. package/dist/src/commands/auth-login.js +97 -0
  11. package/dist/src/commands/config-profile-use.d.ts +3 -0
  12. package/dist/src/commands/config-profile-use.d.ts.map +1 -0
  13. package/dist/src/commands/config-profile-use.js +88 -0
  14. package/dist/src/commands/config-whoami.d.ts +3 -0
  15. package/dist/src/commands/config-whoami.d.ts.map +1 -0
  16. package/dist/src/commands/config-whoami.js +101 -0
  17. package/dist/src/commands/help.d.ts +3 -0
  18. package/dist/src/commands/help.d.ts.map +1 -0
  19. package/dist/src/commands/help.js +61 -0
  20. package/dist/src/commands/init/index.d.ts +3 -0
  21. package/dist/src/commands/init/index.d.ts.map +1 -0
  22. package/dist/src/commands/init/index.js +756 -0
  23. package/dist/src/commands/init/prompting.d.ts +16 -0
  24. package/dist/src/commands/init/prompting.d.ts.map +1 -0
  25. package/dist/src/commands/init/prompting.js +63 -0
  26. package/dist/src/commands/init/wait-for-message.d.ts +45 -0
  27. package/dist/src/commands/init/wait-for-message.d.ts.map +1 -0
  28. package/dist/src/commands/init/wait-for-message.js +27 -0
  29. package/dist/src/commands/version.d.ts +3 -0
  30. package/dist/src/commands/version.d.ts.map +1 -0
  31. package/dist/src/commands/version.js +14 -0
  32. package/dist/src/generated/handlers.d.ts +5 -0
  33. package/dist/src/generated/handlers.d.ts.map +1 -0
  34. package/dist/src/generated/handlers.js +17 -0
  35. package/dist/src/generated/manifest.d.ts +3 -0
  36. package/dist/src/generated/manifest.d.ts.map +1 -0
  37. package/dist/src/generated/manifest.js +56 -0
  38. package/dist/src/lib/argv.d.ts +16 -0
  39. package/dist/src/lib/argv.d.ts.map +1 -0
  40. package/dist/src/lib/argv.js +68 -0
  41. package/dist/src/lib/base-url.d.ts +7 -0
  42. package/dist/src/lib/base-url.d.ts.map +1 -0
  43. package/dist/src/lib/base-url.js +6 -0
  44. package/dist/src/lib/exit-codes.d.ts +11 -0
  45. package/dist/src/lib/exit-codes.d.ts.map +1 -0
  46. package/dist/src/lib/exit-codes.js +29 -0
  47. package/dist/src/lib/generated-command.d.ts +25 -0
  48. package/dist/src/lib/generated-command.d.ts.map +1 -0
  49. package/dist/src/lib/generated-command.js +357 -0
  50. package/dist/src/lib/normalize-error.d.ts +45 -0
  51. package/dist/src/lib/normalize-error.d.ts.map +1 -0
  52. package/dist/src/lib/normalize-error.js +128 -0
  53. package/dist/src/lib/options.d.ts +12 -0
  54. package/dist/src/lib/options.d.ts.map +1 -0
  55. package/dist/src/lib/options.js +49 -0
  56. package/dist/src/lib/package-version.d.ts +2 -0
  57. package/dist/src/lib/package-version.d.ts.map +1 -0
  58. package/dist/src/lib/package-version.js +22 -0
  59. package/dist/src/lib/paths.d.ts +2 -0
  60. package/dist/src/lib/paths.d.ts.map +1 -0
  61. package/dist/src/lib/paths.js +21 -0
  62. package/dist/src/lib/profile-name.d.ts +2 -0
  63. package/dist/src/lib/profile-name.d.ts.map +1 -0
  64. package/dist/src/lib/profile-name.js +4 -0
  65. package/dist/src/lib/profile-store.d.ts +31 -0
  66. package/dist/src/lib/profile-store.d.ts.map +1 -0
  67. package/dist/src/lib/profile-store.js +179 -0
  68. package/dist/src/lib/renderer.d.ts +10 -0
  69. package/dist/src/lib/renderer.d.ts.map +1 -0
  70. package/dist/src/lib/renderer.js +25 -0
  71. package/dist/src/lib/run.d.ts +12 -0
  72. package/dist/src/lib/run.d.ts.map +1 -0
  73. package/dist/src/lib/run.js +108 -0
  74. package/dist/src/lib/transport.d.ts +37 -0
  75. package/dist/src/lib/transport.d.ts.map +1 -0
  76. package/dist/src/lib/transport.js +65 -0
  77. package/dist/src/lib/types.d.ts +22 -0
  78. package/dist/src/lib/types.d.ts.map +1 -0
  79. package/dist/src/lib/types.js +1 -0
  80. package/package.json +46 -0
  81. package/rooaak.js +39 -0
  82. package/schemas/openapi.v1.json +6013 -0
@@ -0,0 +1,756 @@
1
+ import path from "node:path";
2
+ import { randomUUID } from "node:crypto";
3
+ import { resolveBaseUrl } from "../../lib/base-url.js";
4
+ import { ExitCode } from "../../lib/exit-codes.js";
5
+ import { createRenderer } from "../../lib/renderer.js";
6
+ import { normalizeLocalUsageError, toJsonErrorEnvelope, toJsonOkEnvelope, exitCodeForError } from "../../lib/normalize-error.js";
7
+ import { parseOptions } from "../../lib/options.js";
8
+ import { createProfileStore } from "../../lib/profile-store.js";
9
+ import { isValidProfileName } from "../../lib/profile-name.js";
10
+ import { createTransport } from "../../lib/transport.js";
11
+ import { promptConfirm, promptSecret, promptText } from "./prompting.js";
12
+ import { waitForMessageResponse } from "./wait-for-message.js";
13
+ function isPlainObject(value) {
14
+ return typeof value === "object" && value !== null && !Array.isArray(value);
15
+ }
16
+ function safeBasename(p) {
17
+ const base = path.basename(p || "");
18
+ return base && base !== "." && base !== path.sep ? base : "rooaak-project";
19
+ }
20
+ function slugToProfileName(input) {
21
+ const s = input
22
+ .trim()
23
+ .toLowerCase()
24
+ .replace(/[^a-z0-9._-]+/g, "-")
25
+ .replace(/-+/g, "-")
26
+ .replace(/^-+|-+$/g, "");
27
+ return s.length > 0 ? s : "default";
28
+ }
29
+ function normalizeInitError(input) {
30
+ return {
31
+ code: input.code,
32
+ message: input.message,
33
+ details: input.details ?? {},
34
+ meta: { status: input.meta?.status, requestId: input.meta?.requestId },
35
+ };
36
+ }
37
+ function noTtyUsageError(message, details = {}) {
38
+ return normalizeInitError({
39
+ code: "usage.validation_error",
40
+ message,
41
+ details,
42
+ });
43
+ }
44
+ async function requireAdminTransport(ctx, r, argvApiKey) {
45
+ const store = createProfileStore({ configPath: ctx.profileConfigPath });
46
+ const cfgRes = store.readConfig();
47
+ if (cfgRes.kind === "corrupt") {
48
+ // Allow remediation: if user provides an API key, we'll overwrite later. Surface a hint.
49
+ r.warn(cfgRes.message);
50
+ }
51
+ const config = cfgRes.kind === "ok" ? cfgRes.config : null;
52
+ const profile = config ? config.profiles[config.currentProfile] : null;
53
+ const baseUrl = resolveBaseUrl({
54
+ cliFlag: ctx.flags.baseUrl,
55
+ env: ctx.env.ROOAAK_BASE_URL,
56
+ profile: profile?.baseUrl,
57
+ });
58
+ async function validateAdminKey(candidate) {
59
+ const transport = createTransport({ baseUrl, apiKey: candidate, fetchFn: ctx.fetchFn });
60
+ const res = await transport.getJson("/api/v1/projects?limit=1");
61
+ if (!res.ok) {
62
+ return { ok: false, http: { status: res.status, requestId: res.requestId, err: res.error } };
63
+ }
64
+ return { ok: true, transport, meta: { status: res.status, requestId: res.requestId } };
65
+ }
66
+ if (typeof argvApiKey === "string" && argvApiKey.length > 0) {
67
+ const v = await validateAdminKey(argvApiKey);
68
+ if (v.ok)
69
+ return { ok: true, transport: v.transport, baseUrl, adminApiKey: argvApiKey, requestMeta: v.meta };
70
+ const status = v.http?.status;
71
+ if (status === 401) {
72
+ return {
73
+ ok: false,
74
+ exit: ExitCode.Auth,
75
+ err: normalizeInitError({
76
+ code: "auth.invalid_api_key",
77
+ message: "Invalid API key",
78
+ meta: { status: v.http?.status, requestId: v.http?.requestId },
79
+ }),
80
+ };
81
+ }
82
+ if (status === 403) {
83
+ return {
84
+ ok: false,
85
+ exit: ExitCode.Permission,
86
+ err: normalizeInitError({
87
+ code: "permission.admin_key_required",
88
+ message: "Admin API key required to create/select projects.",
89
+ details: { next: "Use an admin API key with scope admin:project (project_id must be NULL)." },
90
+ meta: { status: v.http?.status, requestId: v.http?.requestId },
91
+ }),
92
+ };
93
+ }
94
+ return { ok: false, exit: exitCodeForError(v.http?.err ?? normalizeInitError({ code: "server.error", message: "Server error" })), err: v.http?.err ?? normalizeInitError({ code: "server.error", message: "Server error" }) };
95
+ }
96
+ if (profile?.apiKey) {
97
+ const v = await validateAdminKey(profile.apiKey);
98
+ if (v.ok)
99
+ return { ok: true, transport: v.transport, baseUrl, adminApiKey: profile.apiKey, requestMeta: v.meta };
100
+ const status = v.http?.status;
101
+ const err = v.http?.err ?? normalizeInitError({ code: "server.error", message: "Server error" });
102
+ if (ctx.flags.nonInteractive) {
103
+ if (status === 401) {
104
+ return {
105
+ ok: false,
106
+ exit: ExitCode.Auth,
107
+ err: normalizeInitError({
108
+ code: "auth.invalid_api_key",
109
+ message: "Invalid API key",
110
+ meta: { status: v.http?.status, requestId: v.http?.requestId },
111
+ }),
112
+ };
113
+ }
114
+ if (status === 403) {
115
+ return {
116
+ ok: false,
117
+ exit: ExitCode.Permission,
118
+ err: normalizeInitError({
119
+ code: "permission.admin_key_required",
120
+ message: err.message || "Admin API key required to create/select projects.",
121
+ details: {
122
+ next: "Pass --api-key with an admin API key (scope admin:project), or switch to an admin profile.",
123
+ },
124
+ meta: { status: v.http?.status, requestId: v.http?.requestId },
125
+ }),
126
+ };
127
+ }
128
+ return { ok: false, exit: exitCodeForError(err), err };
129
+ }
130
+ // Interactive: allow recovery by prompting for an admin key on auth/permission failures.
131
+ if (status === 401) {
132
+ r.warn("Current profile API key is invalid. You'll be prompted for an admin API key.");
133
+ }
134
+ else if (status === 403) {
135
+ r.warn("Current profile API key cannot manage projects. You'll be prompted for an admin API key.");
136
+ }
137
+ else {
138
+ // Network/transient failures won't be fixed by prompting.
139
+ return { ok: false, exit: exitCodeForError(err), err };
140
+ }
141
+ }
142
+ if (ctx.flags.nonInteractive) {
143
+ return {
144
+ ok: false,
145
+ exit: ExitCode.Auth,
146
+ err: normalizeInitError({
147
+ code: "auth.not_logged_in",
148
+ message: "Not logged in. Provide --api-key or run `rooaak auth login --api-key ...`.",
149
+ }),
150
+ };
151
+ }
152
+ if (ctx.stdinIsTTY !== true) {
153
+ return {
154
+ ok: false,
155
+ exit: ExitCode.Usage,
156
+ err: noTtyUsageError("Cannot prompt for API key because stdin is not a TTY. Re-run with --non-interactive and provide --api-key, or run `rooaak auth login --api-key ...` from an interactive terminal."),
157
+ };
158
+ }
159
+ // Interactive: prompt until valid admin key is provided.
160
+ while (true) {
161
+ const entered = await promptSecret({ stderr: ctx.stderr, stdin: ctx.stdin, stdinIsTTY: ctx.stdinIsTTY }, "Admin API key", { allowEmpty: false });
162
+ const v = await validateAdminKey(entered);
163
+ if (v.ok)
164
+ return { ok: true, transport: v.transport, baseUrl, adminApiKey: entered, requestMeta: v.meta };
165
+ const status = v.http?.status;
166
+ if (status === 401) {
167
+ r.error("Invalid API key. Try again.");
168
+ continue;
169
+ }
170
+ if (status === 403) {
171
+ r.error("That key cannot manage projects. Use an admin API key with scope admin:project.");
172
+ continue;
173
+ }
174
+ r.error(v.http?.err.message ?? "Request failed. Try again.");
175
+ }
176
+ }
177
+ async function findProjectsByExactName(adminTransport, projectName) {
178
+ const matches = [];
179
+ let cursor = null;
180
+ let meta = null;
181
+ while (true) {
182
+ const qs = new URLSearchParams();
183
+ qs.set("limit", "500");
184
+ if (cursor)
185
+ qs.set("cursor", cursor);
186
+ const httpRes = await adminTransport.getJson(`/api/v1/projects?${qs.toString()}`);
187
+ if (!httpRes.ok)
188
+ return { ok: false, err: httpRes.error };
189
+ meta = { status: httpRes.status, requestId: httpRes.requestId };
190
+ const items = Array.isArray(httpRes.data.items) ? httpRes.data.items : [];
191
+ for (const p of items) {
192
+ if (p.name === projectName)
193
+ matches.push(p);
194
+ if (matches.length >= 2) {
195
+ return { ok: true, matches, meta };
196
+ }
197
+ }
198
+ const next = httpRes.data.nextCursor ?? null;
199
+ if (!next)
200
+ return { ok: true, matches, meta };
201
+ cursor = next;
202
+ }
203
+ }
204
+ async function ensureProjectByName(input) {
205
+ const findRes = await findProjectsByExactName(input.adminTransport, input.projectName);
206
+ if (!findRes.ok)
207
+ return { ok: false, err: findRes.err };
208
+ const matches = findRes.matches;
209
+ if (matches.length === 1)
210
+ return { ok: true, project: matches[0], created: false, meta: findRes.meta };
211
+ if (matches.length > 1) {
212
+ return {
213
+ ok: false,
214
+ err: normalizeInitError({
215
+ code: "conflict.duplicate_project_name",
216
+ message: `Multiple projects named '${input.projectName}' exist.`,
217
+ details: { next: "Run `rooaak init` in interactive mode to select a project." },
218
+ meta: findRes.meta,
219
+ }),
220
+ };
221
+ }
222
+ const createRes = await input.adminTransport.requestJson({
223
+ method: "POST",
224
+ path: "/api/v1/projects",
225
+ body: { name: input.projectName },
226
+ });
227
+ if (createRes.ok) {
228
+ return { ok: true, project: { id: createRes.data.id, name: createRes.data.name }, created: true, meta: { status: createRes.status, requestId: createRes.requestId } };
229
+ }
230
+ // Best-effort recovery: if the create failed but the project exists now (race, duplicate name), select it.
231
+ const findAgain = await findProjectsByExactName(input.adminTransport, input.projectName);
232
+ if (findAgain.ok) {
233
+ const againMatches = findAgain.matches;
234
+ if (againMatches.length === 1)
235
+ return { ok: true, project: againMatches[0], created: false, meta: findAgain.meta };
236
+ }
237
+ return { ok: false, err: createRes.error };
238
+ }
239
+ async function createProjectApiKey(input) {
240
+ const httpRes = await input.adminTransport.requestJson({
241
+ method: "POST",
242
+ path: `/api/v1/projects/${input.projectId}/api-keys`,
243
+ body: { name: input.name },
244
+ });
245
+ if (!httpRes.ok)
246
+ return { ok: false, err: httpRes.error };
247
+ const key = isPlainObject(httpRes.data) && typeof httpRes.data.key === "string" ? httpRes.data.key : null;
248
+ if (!key) {
249
+ return {
250
+ ok: false,
251
+ err: normalizeInitError({
252
+ code: "server.invalid_response",
253
+ message: "Server returned an unexpected API key response.",
254
+ meta: { status: httpRes.status, requestId: httpRes.requestId },
255
+ }),
256
+ };
257
+ }
258
+ return { ok: true, apiKey: key, meta: { status: httpRes.status, requestId: httpRes.requestId } };
259
+ }
260
+ function writeProjectProfile(input) {
261
+ const store = createProfileStore({ configPath: input.ctx.profileConfigPath });
262
+ const existingRes = store.readConfig();
263
+ if (existingRes.kind === "corrupt") {
264
+ // Allowed remediation path.
265
+ input.r.warn(existingRes.message);
266
+ }
267
+ const existing = existingRes.kind === "ok" ? existingRes.config : null;
268
+ let profileName = input.profileName;
269
+ if (typeof profileName === "string" && profileName.length > 0) {
270
+ if (!isValidProfileName(profileName)) {
271
+ const err = normalizeLocalUsageError("Invalid profile name", { profile: profileName });
272
+ return { ok: false, exit: ExitCode.Usage, err };
273
+ }
274
+ }
275
+ else if (!existing) {
276
+ profileName = "default";
277
+ }
278
+ else {
279
+ const base = `project-${slugToProfileName(input.projectName)}`;
280
+ profileName = base;
281
+ // Avoid clobbering an existing profile name.
282
+ if (existing.profiles[profileName]) {
283
+ let i = 2;
284
+ while (existing.profiles[`${profileName}-${i}`])
285
+ i++;
286
+ profileName = `${profileName}-${i}`;
287
+ }
288
+ }
289
+ const now = new Date().toISOString();
290
+ const next = {
291
+ version: 1,
292
+ currentProfile: profileName,
293
+ profiles: {
294
+ ...(existing?.profiles ?? {}),
295
+ [profileName]: {
296
+ apiKey: input.apiKey,
297
+ baseUrl: input.baseUrl,
298
+ updatedAt: now,
299
+ },
300
+ },
301
+ };
302
+ store.writeConfig(next);
303
+ input.r.debug(`Wrote config: ${store.getConfigPath()}`);
304
+ return { ok: true, profileName };
305
+ }
306
+ export async function initCommand(ctx, argv) {
307
+ const r = createRenderer(ctx);
308
+ const parsed = parseOptions(argv, [
309
+ { name: "--help", takesValue: false },
310
+ { name: "--api-key", takesValue: true }, // admin key
311
+ { name: "--project-name", takesValue: true },
312
+ { name: "--agent-name", takesValue: true },
313
+ { name: "--test-message", takesValue: true },
314
+ { name: "--wait-timeout-ms", takesValue: true },
315
+ { name: "--profile", takesValue: true },
316
+ { name: "--start", takesValue: false },
317
+ { name: "--no-start", takesValue: false },
318
+ ]);
319
+ if (parsed.unknownOption) {
320
+ const err = normalizeLocalUsageError(`Unknown option: ${parsed.unknownOption}`);
321
+ if (ctx.flags.json)
322
+ r.json(toJsonErrorEnvelope(err));
323
+ else
324
+ r.error(err.message);
325
+ return ExitCode.Usage;
326
+ }
327
+ if (parsed.missingValueOption) {
328
+ const err = normalizeLocalUsageError(`Missing value for: ${parsed.missingValueOption}`);
329
+ if (ctx.flags.json)
330
+ r.json(toJsonErrorEnvelope(err));
331
+ else
332
+ r.error(err.message);
333
+ return ExitCode.Usage;
334
+ }
335
+ if (parsed.positionals.length > 0) {
336
+ const err = normalizeLocalUsageError("Unexpected extra arguments", { argc: parsed.positionals.length });
337
+ if (ctx.flags.json)
338
+ r.json(toJsonErrorEnvelope(err));
339
+ else
340
+ r.error(err.message);
341
+ return ExitCode.Usage;
342
+ }
343
+ if (parsed.values["--help"]) {
344
+ if (ctx.flags.json) {
345
+ r.json(toJsonOkEnvelope({
346
+ type: "help",
347
+ command: "init",
348
+ usage: "rooaak init [--api-key <key>] [--project-name <name>] [--agent-name <name>] [--test-message <text>] [--wait-timeout-ms <ms>] [--profile <name>] [--start|--no-start] [--json] [--non-interactive]",
349
+ flags: [
350
+ { name: "--api-key <key>", description: "Admin API key (required when not logged in as admin)" },
351
+ { name: "--project-name <name>", description: "Project name to create/select" },
352
+ { name: "--agent-name <name>", description: "Agent name to create" },
353
+ { name: "--test-message <text>", description: "Message to send after creation" },
354
+ { name: "--wait-timeout-ms <ms>", description: "How long to wait for the first response (default 60000ms; overrides ROOAAK_SYNC_MESSAGE_TIMEOUT_MS)" },
355
+ { name: "--start", description: "Start the agent (default)" },
356
+ { name: "--no-start", description: "Skip starting the agent" },
357
+ { name: "--profile <name>", description: "Profile name to save the created project API key under" },
358
+ ],
359
+ }));
360
+ return ExitCode.Success;
361
+ }
362
+ ctx.stdout.write([
363
+ "Usage:",
364
+ " rooaak init [flags]",
365
+ "",
366
+ "Flags:",
367
+ " --api-key <key> Admin API key (required when not logged in as admin)",
368
+ " --project-name <name> Project name to create/select",
369
+ " --agent-name <name> Agent name to create",
370
+ " --test-message <text> Message to send after creation",
371
+ " --wait-timeout-ms <ms> How long to wait for the first response (default 60000ms; overrides ROOAAK_SYNC_MESSAGE_TIMEOUT_MS)",
372
+ " --start Start the agent (default)",
373
+ " --no-start Skip starting the agent",
374
+ " --profile <name> Profile name to save created project API key under",
375
+ "",
376
+ "Global flags:",
377
+ " --json Machine output (single JSON object on stdout)",
378
+ " --non-interactive Fail fast instead of prompting",
379
+ "",
380
+ ].join("\n"));
381
+ return ExitCode.Success;
382
+ }
383
+ const interactive = !ctx.flags.nonInteractive;
384
+ const flagProjectName = parsed.values["--project-name"];
385
+ const flagAgentName = parsed.values["--agent-name"];
386
+ const flagTestMessage = parsed.values["--test-message"];
387
+ const flagWaitTimeoutMs = parsed.values["--wait-timeout-ms"];
388
+ const flagProfileName = parsed.values["--profile"];
389
+ let projectName = typeof flagProjectName === "string" && flagProjectName.trim().length > 0 ? flagProjectName.trim() : "";
390
+ let agentName = typeof flagAgentName === "string" && flagAgentName.trim().length > 0 ? flagAgentName.trim() : "";
391
+ let testMessage = typeof flagTestMessage === "string" && flagTestMessage.trim().length > 0 ? flagTestMessage.trim() : "";
392
+ const wantsStart = Boolean(parsed.values["--start"]);
393
+ const wantsNoStart = Boolean(parsed.values["--no-start"]);
394
+ if (wantsStart && wantsNoStart) {
395
+ const err = normalizeLocalUsageError("Conflicting flags: --start and --no-start");
396
+ if (ctx.flags.json)
397
+ r.json(toJsonErrorEnvelope(err));
398
+ else
399
+ r.error(err.message);
400
+ return ExitCode.Usage;
401
+ }
402
+ let startAgent;
403
+ if (wantsStart)
404
+ startAgent = true;
405
+ else if (wantsNoStart)
406
+ startAgent = false;
407
+ else if (!interactive)
408
+ startAgent = true;
409
+ else if (ctx.stdinIsTTY !== true)
410
+ startAgent = true;
411
+ else
412
+ startAgent = await promptConfirm({ stderr: ctx.stderr, stdin: ctx.stdin, stdinIsTTY: ctx.stdinIsTTY }, "Start the agent now?", { defaultYes: true });
413
+ if (!projectName) {
414
+ if (!interactive) {
415
+ const err = normalizeLocalUsageError("Missing required flag: --project-name");
416
+ if (ctx.flags.json)
417
+ r.json(toJsonErrorEnvelope(err));
418
+ else
419
+ r.error(err.message);
420
+ return ExitCode.Usage;
421
+ }
422
+ if (ctx.stdinIsTTY !== true) {
423
+ const err = noTtyUsageError("Cannot prompt for --project-name because stdin is not a TTY. Re-run with --non-interactive and provide --project-name.");
424
+ if (ctx.flags.json)
425
+ r.json(toJsonErrorEnvelope(err));
426
+ else
427
+ r.error(err.message);
428
+ return ExitCode.Usage;
429
+ }
430
+ projectName = await promptText({ stderr: ctx.stderr, stdin: ctx.stdin, stdinIsTTY: ctx.stdinIsTTY }, "Project name", { defaultValue: safeBasename(ctx.cwd) });
431
+ projectName = projectName.trim();
432
+ }
433
+ if (!agentName) {
434
+ if (!interactive) {
435
+ const err = normalizeLocalUsageError("Missing required flag: --agent-name");
436
+ if (ctx.flags.json)
437
+ r.json(toJsonErrorEnvelope(err));
438
+ else
439
+ r.error(err.message);
440
+ return ExitCode.Usage;
441
+ }
442
+ if (ctx.stdinIsTTY !== true) {
443
+ const err = noTtyUsageError("Cannot prompt for --agent-name because stdin is not a TTY. Re-run with --non-interactive and provide --agent-name.");
444
+ if (ctx.flags.json)
445
+ r.json(toJsonErrorEnvelope(err));
446
+ else
447
+ r.error(err.message);
448
+ return ExitCode.Usage;
449
+ }
450
+ agentName = await promptText({ stderr: ctx.stderr, stdin: ctx.stdin, stdinIsTTY: ctx.stdinIsTTY }, "Agent name", { defaultValue: `${projectName}-agent` });
451
+ agentName = agentName.trim();
452
+ }
453
+ if (!testMessage) {
454
+ if (!interactive) {
455
+ const err = normalizeLocalUsageError("Missing required flag: --test-message");
456
+ if (ctx.flags.json)
457
+ r.json(toJsonErrorEnvelope(err));
458
+ else
459
+ r.error(err.message);
460
+ return ExitCode.Usage;
461
+ }
462
+ if (ctx.stdinIsTTY !== true) {
463
+ const err = noTtyUsageError("Cannot prompt for --test-message because stdin is not a TTY. Re-run with --non-interactive and provide --test-message.");
464
+ if (ctx.flags.json)
465
+ r.json(toJsonErrorEnvelope(err));
466
+ else
467
+ r.error(err.message);
468
+ return ExitCode.Usage;
469
+ }
470
+ testMessage = await promptText({ stderr: ctx.stderr, stdin: ctx.stdin, stdinIsTTY: ctx.stdinIsTTY }, "Test message", {
471
+ defaultValue: "Hello! Reply with a short greeting so I can verify you're working.",
472
+ });
473
+ testMessage = testMessage.trim();
474
+ }
475
+ // Wait timeout override: flag -> env -> default.
476
+ let waitTimeoutMs = 60_000;
477
+ const envTimeout = ctx.env.ROOAAK_SYNC_MESSAGE_TIMEOUT_MS;
478
+ if (typeof envTimeout === "string" && envTimeout.trim().length > 0) {
479
+ const n = Number(envTimeout);
480
+ if (Number.isFinite(n) && n > 0)
481
+ waitTimeoutMs = Math.floor(n);
482
+ }
483
+ if (typeof flagWaitTimeoutMs === "string" && flagWaitTimeoutMs.trim().length > 0) {
484
+ const n = Number(flagWaitTimeoutMs);
485
+ if (!Number.isFinite(n) || n <= 0) {
486
+ const err = normalizeLocalUsageError("Invalid value for --wait-timeout-ms", { value: flagWaitTimeoutMs });
487
+ if (ctx.flags.json)
488
+ r.json(toJsonErrorEnvelope(err));
489
+ else
490
+ r.error(err.message);
491
+ return ExitCode.Usage;
492
+ }
493
+ waitTimeoutMs = Math.floor(n);
494
+ }
495
+ const adminApiKey = typeof parsed.values["--api-key"] === "string" ? parsed.values["--api-key"] : undefined;
496
+ const adminRes = await requireAdminTransport(ctx, r, adminApiKey);
497
+ if (!adminRes.ok) {
498
+ if (ctx.flags.json)
499
+ r.json(toJsonErrorEnvelope(adminRes.err));
500
+ else
501
+ r.error(adminRes.err.message);
502
+ return adminRes.exit;
503
+ }
504
+ const { transport: adminTransport, baseUrl } = adminRes;
505
+ if (interactive) {
506
+ r.warn("This will create/select a project, generate a project API key, create an agent, and send a first message.");
507
+ }
508
+ // Create/select project.
509
+ let projectRes;
510
+ if (interactive) {
511
+ const findRes = await findProjectsByExactName(adminTransport, projectName);
512
+ if (!findRes.ok) {
513
+ const err = findRes.err;
514
+ if (ctx.flags.json)
515
+ r.json(toJsonErrorEnvelope(err));
516
+ else
517
+ r.error(err.message);
518
+ return exitCodeForError(err);
519
+ }
520
+ if (findRes.matches.length === 1) {
521
+ const shouldPrompt = ctx.stdinIsTTY === true;
522
+ const useExisting = shouldPrompt
523
+ ? await promptConfirm({ stderr: ctx.stderr, stdin: ctx.stdin, stdinIsTTY: ctx.stdinIsTTY }, `Project '${projectName}' exists. Use it?`, { defaultYes: true })
524
+ : true;
525
+ if (useExisting) {
526
+ projectRes = { ok: true, project: findRes.matches[0], created: false, meta: findRes.meta };
527
+ }
528
+ else {
529
+ projectName = await promptText({ stderr: ctx.stderr, stdin: ctx.stdin, stdinIsTTY: ctx.stdinIsTTY }, "Project name", { defaultValue: `${projectName}-2` });
530
+ projectName = projectName.trim();
531
+ projectRes = await ensureProjectByName({ adminTransport, projectName });
532
+ }
533
+ }
534
+ else if (findRes.matches.length > 1) {
535
+ projectRes = {
536
+ ok: false,
537
+ err: normalizeInitError({
538
+ code: "conflict.duplicate_project_name",
539
+ message: `Multiple projects named '${projectName}' exist.`,
540
+ details: { next: "Specify a unique --project-name or clean up duplicates." },
541
+ meta: findRes.meta,
542
+ }),
543
+ };
544
+ }
545
+ else {
546
+ projectRes = await ensureProjectByName({ adminTransport, projectName });
547
+ }
548
+ }
549
+ else {
550
+ projectRes = await ensureProjectByName({ adminTransport, projectName });
551
+ }
552
+ if (!projectRes.ok) {
553
+ const err = projectRes.err;
554
+ if (ctx.flags.json)
555
+ r.json(toJsonErrorEnvelope(err));
556
+ else {
557
+ r.error(err.message);
558
+ if (err.code === "permission.forbidden" || err.code.startsWith("permission.")) {
559
+ r.warn("Project management requires an admin API key with scope admin:project.");
560
+ }
561
+ }
562
+ return exitCodeForError(err);
563
+ }
564
+ const project = projectRes.project;
565
+ // Create a project-scoped API key and store it locally.
566
+ const apiKeyRes = await createProjectApiKey({
567
+ adminTransport,
568
+ projectId: project.id,
569
+ name: "rooaak-cli-init",
570
+ });
571
+ if (!apiKeyRes.ok) {
572
+ const err = apiKeyRes.err;
573
+ if (ctx.flags.json)
574
+ r.json(toJsonErrorEnvelope(err));
575
+ else {
576
+ r.error(err.message);
577
+ r.warn("Creating project API keys requires admin:project (admin key) or project:api-keys (project key).");
578
+ }
579
+ return exitCodeForError(err);
580
+ }
581
+ const projectApiKey = apiKeyRes.apiKey;
582
+ const writeRes = writeProjectProfile({
583
+ ctx,
584
+ r,
585
+ baseUrl,
586
+ apiKey: projectApiKey,
587
+ projectName: project.name,
588
+ profileName: typeof flagProfileName === "string" ? flagProfileName : undefined,
589
+ });
590
+ if (!writeRes.ok) {
591
+ const err = writeRes.err;
592
+ if (ctx.flags.json)
593
+ r.json(toJsonErrorEnvelope(err));
594
+ else
595
+ r.error(err.message);
596
+ return writeRes.exit;
597
+ }
598
+ const profileName = writeRes.profileName;
599
+ const projectTransport = createTransport({ baseUrl, apiKey: projectApiKey, fetchFn: ctx.fetchFn });
600
+ // Create agent.
601
+ const agentRes = await projectTransport.requestJson({
602
+ method: "POST",
603
+ path: "/api/v1/agents",
604
+ body: { name: agentName },
605
+ });
606
+ if (!agentRes.ok) {
607
+ const err = agentRes.error;
608
+ if (ctx.flags.json)
609
+ r.json(toJsonErrorEnvelope(err));
610
+ else {
611
+ r.error(err.message);
612
+ r.warn("Agent creation requires a project API key with scope project:agent.");
613
+ }
614
+ return exitCodeForError(err);
615
+ }
616
+ const agent = { id: agentRes.data.id, name: agentRes.data.name };
617
+ // Optional start.
618
+ let startMeta;
619
+ if (startAgent) {
620
+ const startRes = await projectTransport.requestJson({
621
+ method: "POST",
622
+ path: `/api/v1/agents/${agent.id}/start`,
623
+ });
624
+ if (!startRes.ok) {
625
+ const err = startRes.error;
626
+ if (ctx.flags.json)
627
+ r.json(toJsonErrorEnvelope(err));
628
+ else {
629
+ r.error(err.message);
630
+ r.warn(`You can retry: rooaak agents start --agent-id ${agent.id}`);
631
+ }
632
+ return exitCodeForError(err);
633
+ }
634
+ startMeta = { status: startRes.status, requestId: startRes.requestId };
635
+ }
636
+ const suggestedSessionId = `init_${randomUUID()}`;
637
+ let messageId = null;
638
+ let messageSessionId = null;
639
+ let messageStatus = "skipped";
640
+ let messageResponse = null;
641
+ let waitedMs = null;
642
+ let sendMeta;
643
+ // Send a test message only when startup is enabled.
644
+ if (startAgent) {
645
+ messageSessionId = suggestedSessionId;
646
+ const sendRes = await projectTransport.requestJson({
647
+ method: "POST",
648
+ path: `/api/v1/agents/${agent.id}/messages`,
649
+ body: { sessionId: messageSessionId, message: testMessage },
650
+ });
651
+ if (!sendRes.ok) {
652
+ const err = sendRes.error;
653
+ if (ctx.flags.json)
654
+ r.json(toJsonErrorEnvelope(err));
655
+ else {
656
+ r.error(err.message);
657
+ r.warn("Message sending requires a project API key with scope project:message.");
658
+ }
659
+ return exitCodeForError(err);
660
+ }
661
+ sendMeta = { status: sendRes.status, requestId: sendRes.requestId };
662
+ const sendData = sendRes.data;
663
+ if (!isPlainObject(sendData) || typeof sendData.messageId !== "string") {
664
+ const err = normalizeInitError({
665
+ code: "server.invalid_response",
666
+ message: "Server returned an unexpected message response.",
667
+ meta: { status: sendRes.status, requestId: sendRes.requestId },
668
+ });
669
+ if (ctx.flags.json)
670
+ r.json(toJsonErrorEnvelope(err));
671
+ else
672
+ r.error(err.message);
673
+ return exitCodeForError(err);
674
+ }
675
+ messageId = sendData.messageId;
676
+ messageStatus = "pending";
677
+ if (sendData.status === "responded" && typeof sendData.response === "string") {
678
+ messageStatus = "responded";
679
+ messageResponse = sendData.response;
680
+ }
681
+ else {
682
+ const waitRes = await waitForMessageResponse({
683
+ transport: projectTransport,
684
+ messageId,
685
+ timeoutMs: waitTimeoutMs,
686
+ });
687
+ if (!waitRes.ok) {
688
+ const err = waitRes.error;
689
+ if (ctx.flags.json)
690
+ r.json(toJsonErrorEnvelope(err));
691
+ else
692
+ r.error(err.message);
693
+ return waitRes.exit;
694
+ }
695
+ if (waitRes.kind === "responded") {
696
+ messageStatus = "responded";
697
+ messageResponse = waitRes.message.response ?? null;
698
+ }
699
+ else {
700
+ messageStatus = "timeout";
701
+ waitedMs = waitRes.waitedMs;
702
+ }
703
+ }
704
+ }
705
+ const next = {
706
+ agentStart: `rooaak agents start --agent-id ${agent.id}`,
707
+ messageSend: `rooaak messages send --agent-id ${agent.id} --session-id ${suggestedSessionId} --message "..."`,
708
+ ...(messageId ? { messageGet: `rooaak messages get --message-id ${messageId}` } : {}),
709
+ };
710
+ const output = {
711
+ baseUrl,
712
+ profile: profileName,
713
+ project: { id: project.id, name: project.name, created: projectRes.created },
714
+ agent: { id: agent.id, name: agent.name, started: startAgent },
715
+ message: {
716
+ id: messageId,
717
+ sessionId: messageSessionId,
718
+ status: messageStatus,
719
+ response: messageResponse,
720
+ ...(messageStatus === "timeout" ? { waitedMs } : {}),
721
+ },
722
+ next,
723
+ };
724
+ if (ctx.flags.json) {
725
+ // Use the final API request metadata if we have it; otherwise fall back to prior successful requests.
726
+ const meta = sendMeta ?? startMeta ?? { status: agentRes.status, requestId: agentRes.requestId };
727
+ r.json(toJsonOkEnvelope(output, meta));
728
+ return ExitCode.Success;
729
+ }
730
+ r.text(`Project: ${project.name} (${project.id})`);
731
+ r.text(`Profile saved: '${profileName}'`);
732
+ r.text(`Agent: ${agent.name} (${agent.id})`);
733
+ if (messageStatus === "responded") {
734
+ r.text("");
735
+ r.text("First response:");
736
+ r.text(messageResponse ?? "");
737
+ }
738
+ else if (messageStatus === "timeout") {
739
+ r.text("");
740
+ r.warn(`Message is still pending after waiting ${waitTimeoutMs}ms. It may finish shortly.`);
741
+ if ("messageGet" in output.next)
742
+ r.warn(`Check status: ${output.next.messageGet}`);
743
+ }
744
+ else if (messageStatus === "skipped") {
745
+ r.text("");
746
+ r.text("Skipped first message because agent startup was disabled.");
747
+ r.text(`Start later: ${output.next.agentStart}`);
748
+ }
749
+ r.text("");
750
+ r.text("Next steps:");
751
+ r.text(` ${output.next.agentStart}`);
752
+ r.text(` ${output.next.messageSend}`);
753
+ if ("messageGet" in output.next)
754
+ r.text(` ${output.next.messageGet}`);
755
+ return ExitCode.Success;
756
+ }