@sixb/cli-core 0.1.7 → 0.1.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -11,6 +11,7 @@ import { pipeline } from "stream/promises";
11
11
  var INSTANCE_CLI_VERSION = "1";
12
12
  var EXIT_USAGE = 2;
13
13
  var EXIT_API = 3;
14
+ var EXIT_AWAITING_APPROVAL = 4;
14
15
 
15
16
  class CliError extends Error {
16
17
  body;
@@ -29,12 +30,29 @@ function writeJson(value) {
29
30
  process.stdout.write(`${JSON.stringify(value)}
30
31
  `);
31
32
  }
33
+
34
+ class CliExit extends Error {
35
+ exitCode;
36
+ constructor(exitCode) {
37
+ super(`The Sixb CLI exited with code ${exitCode}.`);
38
+ this.exitCode = exitCode;
39
+ this.name = "CliExit";
40
+ }
41
+ }
42
+ function writeRequestResult(value) {
43
+ writeJson(value);
44
+ const status = typeof value === "object" && value !== null ? Reflect.get(value, "status") : null;
45
+ if (status === "awaiting_approval")
46
+ throw new CliExit(EXIT_AWAITING_APPROVAL);
47
+ }
32
48
  function writeText(value) {
33
49
  process.stdout.write(value.endsWith(`
34
50
  `) ? value : `${value}
35
51
  `);
36
52
  }
37
53
  function reportError(error) {
54
+ if (error instanceof CliExit)
55
+ return error.exitCode;
38
56
  const cliError = error instanceof CliError ? error : new CliError({
39
57
  code: "internal_error",
40
58
  message: error instanceof Error ? error.message : "The Sixb CLI failed unexpectedly."
@@ -62,16 +80,18 @@ class ApiClient {
62
80
  const url = this.url(path, query);
63
81
  return this.json(url, { method: "GET" });
64
82
  }
65
- async post(path, body) {
83
+ async post(path, body, options = {}) {
66
84
  return this.json(this.url(path), {
67
85
  method: "POST",
68
86
  headers: { "content-type": "application/json" },
69
- body: JSON.stringify(body)
87
+ body: JSON.stringify(body),
88
+ ...options.signal ? { signal: options.signal } : {}
70
89
  });
71
90
  }
72
- async upload(path, source, logicalPath) {
91
+ async upload(path, source, logicalPath, mediaType) {
73
92
  const form = new FormData;
74
- form.append("file", new Blob([new Uint8Array(await readFile(source))]), basename(source));
93
+ const bytes = new Uint8Array(await readFile(source));
94
+ form.append("file", new Blob([bytes], mediaType ? { type: mediaType } : undefined), basename(source));
75
95
  if (logicalPath)
76
96
  form.append("logicalPath", logicalPath);
77
97
  return this.json(this.url(path), { method: "POST", body: form });
@@ -117,7 +137,9 @@ class ApiClient {
117
137
  if (this.authorization)
118
138
  headers.set("authorization", this.authorization);
119
139
  response = await fetch(url, { ...init, headers });
120
- } catch {
140
+ } catch (error) {
141
+ if (init.signal?.aborted)
142
+ throw error;
121
143
  throw new CliError({
122
144
  code: "api_unreachable",
123
145
  message: this.unavailableMessage,
@@ -190,8 +212,9 @@ function parseCommandArgs(args, kinds, command, positionalCount = 0) {
190
212
  const flag = equals < 0 ? argument : argument.slice(0, equals);
191
213
  if (!Object.hasOwn(kinds, flag))
192
214
  fail(`Unknown ${command} option '${flag}'.`);
193
- if (Object.hasOwn(options, flag))
215
+ if (Object.hasOwn(options, flag) && kinds[flag] !== "strings") {
194
216
  fail(`${flag} may only be provided once.`);
217
+ }
195
218
  if (kinds[flag] === "boolean") {
196
219
  if (equals >= 0)
197
220
  fail(`${flag} does not accept a value.`);
@@ -201,7 +224,8 @@ function parseCommandArgs(args, kinds, command, positionalCount = 0) {
201
224
  if (!value || equals < 0 && value.startsWith("--")) {
202
225
  fail(`${flag} requires a value.`);
203
226
  }
204
- options[flag] = value;
227
+ const previous = options[flag];
228
+ options[flag] = kinds[flag] === "strings" ? [...Array.isArray(previous) ? previous : [], value] : value;
205
229
  }
206
230
  }
207
231
  const [minimum, maximum] = typeof positionalCount === "number" ? [positionalCount, positionalCount] : positionalCount;
@@ -265,8 +289,13 @@ function enumValue(flag, value, allowed) {
265
289
  function formatAlternatives(values) {
266
290
  if (values.length < 2)
267
291
  return values[0] ?? "a supported value";
292
+ if (values.length === 2)
293
+ return `${values[0]} or ${values[1]}`;
268
294
  return `${values.slice(0, -1).join(", ")}, or ${values.at(-1)}`;
269
295
  }
296
+ // src/commands/actions.ts
297
+ import { randomUUID as randomUUID2 } from "crypto";
298
+
270
299
  // src/policies.ts
271
300
  var CLI_LIMITS = {
272
301
  list: { default: 20, maximum: 1000 },
@@ -274,6 +303,7 @@ var CLI_LIMITS = {
274
303
  vectorSearchText: { maximum: 8000 },
275
304
  telemetryHistory: { default: 100, maximum: 1000 },
276
305
  linkPage: { default: 100, maximum: 1000 },
306
+ users: { default: 20, maximum: 200 },
277
307
  inspect: {
278
308
  depth: { default: 2, maximum: 3 },
279
309
  objects: { default: 20, maximum: 100 },
@@ -309,6 +339,8 @@ Objects:
309
339
  sixb objects inspect <type> <id> Inspect an object and its related graph in one command
310
340
  sixb objects list [options] Browse materialized objects
311
341
  sixb objects get <type> <id>... Exact lookup through opaque object references
342
+ sixb objects get-by <type> <property> <value>
343
+ The object holding a value of a unique property
312
344
  sixb objects search <text> Search visible objects by id prefix and full text
313
345
  sixb objects search <text> --type <type>
314
346
  Semantic search through the type's vector profile
@@ -332,6 +364,13 @@ Actions and workflows:
332
364
  Files:
333
365
  sixb files upload|download ...
334
366
 
367
+ Members:
368
+ sixb users search <text> Find members by name or email
369
+ sixb users list|get ...
370
+
371
+ A user parameter or property is {"type":"user","id":"<user-id>"}. Find the id with
372
+ \`sixb users search <name>\`.
373
+
335
374
  Run \`sixb <group> --help\` or \`sixb <group> <command> --help\` for exact arguments.
336
375
  For query IR, run \`sixb objects query --help\` and \`sixb objects query --example list\`.`;
337
376
  var LOCAL_MAIN_HELP = SANDBOX_MAIN_HELP.replace("Sixb agent CLI", "Sixb instance CLI").replace("The CLI uses the run-scoped SIXB_API_BASE_URL. Do not configure authentication or another origin.", "The CLI uses the selected local profile and its API credentials.").replace(` sixb doctor Check the sandbox and API gateway
@@ -339,7 +378,16 @@ var LOCAL_MAIN_HELP = SANDBOX_MAIN_HELP.replace("Sixb agent CLI", "Sixb instance
339
378
  `, "").replace(`
340
379
  When you do not know a type's properties or links, read its file under $SIXB_ONTOLOGY_DIR. Use
341
380
  \`sixb ontology get\` for a type that has no file there.
342
- `, "");
381
+ `, "").replace(`Files:
382
+ sixb files upload|download ...
383
+ `, `Files:
384
+ sixb files upload|download ...
385
+
386
+ Models:
387
+ sixb models list [--kind <kind>] Configured models of every kind, with prices
388
+ sixb models get <model> One model's options and rate card
389
+ sixb models text|image|speech|transcribe ... Call a model; see sixb models --help
390
+ `);
343
391
  function renderInstanceHelp(mode) {
344
392
  return mode === "sandbox" ? SANDBOX_MAIN_HELP : LOCAL_MAIN_HELP;
345
393
  }
@@ -356,10 +404,18 @@ var SEARCH_HELP = `Usage:
356
404
  profiles.
357
405
 
358
406
  ${SEARCH_DETAILS}`;
407
+ var GET_BY_HELP = `Usage:
408
+ sixb objects get-by <object-type> <property> <value> [--json]
409
+
410
+ Prints the object of exactly <object-type> holding <value> for a property marked unique in its
411
+ ontology doc, or null when no visible object holds it. Values match exactly, case included.
412
+ The value is a string; with --json it is parsed as JSON, for an integer (42) or a user
413
+ reference ({"type":"user","id":"usr_1"}).`;
359
414
  var OBJECTS_HELP = `Usage:
360
415
  sixb objects inspect <object-type> <primary-id> [options]
361
416
  sixb objects list [options]
362
417
  sixb objects get <object-type> <primary-id>...
418
+ sixb objects get-by <object-type> <property> <value> [--json]
363
419
  sixb objects search <text> [--type <object-type> [--vector-profile <name>]] [--limit <n>]
364
420
  sixb objects query --file <path|-> [--include-total|--no-total]
365
421
  sixb objects query --example <name>
@@ -426,7 +482,7 @@ Query nodes compose through input:
426
482
  limit {"kind":"limit","input":<query>,"limit":${CLI_LIMITS.list.default}}
427
483
  page {"kind":"page","input":<query>,"pageSize":${CLI_LIMITS.list.default},"pageToken":"from nextPageToken"}
428
484
  project {"kind":"project","input":<query>,"properties":["name","status"]}
429
- expand {"kind":"expand","input":<query>,"expansions":[{"linkId":"link","direction":"outgoing","limit":5}]}
485
+ expand {"kind":"expand","input":<query>,"expansions":[{"linkId":"link","direction":"outgoing","limit":5}],"users":["assignee"]}
430
486
  vector {"kind":"vector","input":<start|filter>,"vector":"text","profile":"name","k":10}
431
487
  rerank {"kind":"rerank","input":<vector>,"model":{"provider":"provider","modelId":"model"}}
432
488
 
@@ -437,11 +493,16 @@ Predicates use op, not kind:
437
493
  {"op":"in","propertyId":"status","values":["open","blocked"]}
438
494
  {"op":"exists","propertyId":"dueDate","value":true}
439
495
  {"op":"contains","propertyId":"tags","value":"urgent"}
496
+ {"op":"eq","propertyId":"assignee","value":{"type":"currentUser"}} the caller, on a user reference: neq, in, contains too
440
497
 
441
498
  Every field shown is required except includeSubtypes, fields, pageToken, properties, a sort
442
- direction, and an expansion's limit. Sort by relevance after text or vector with
499
+ direction, an expansion's limit, and users. Sort by relevance after text or vector with
443
500
  {"kind":"relevance"}. Wrap the outermost node in project to return only the properties you need.
444
501
 
502
+ An expand's users, or an expansion's, lists user-reference properties: each row then carries the
503
+ members' profiles under users (id, email, displayName, avatarUrl, status). Without access to the
504
+ member directory, users is empty.
505
+
445
506
  Traversal and expansion directions are outgoing or incoming. For incoming relationships, the link
446
507
  is declared on the child/source type; add sourceObjectTypeId when needed to disambiguate it.
447
508
 
@@ -471,11 +532,13 @@ History options:
471
532
  actions: `Usage:
472
533
  sixb actions list [--type <object-type>]
473
534
  sixb actions get <action-id>
474
- sixb actions request <action-id> [--subject-type <type> --subject-id <id>] [--file <path|->] [--run-id <id>] [--wait]
535
+ sixb actions request <action-id> [--subject-type <type> --subject-id <id>] [--file <path|->] [--run-id <id>]
475
536
 
476
537
  \`actions get\` includes inputSchema, the exact JSON shape accepted by the Action. The JSON file
477
- contains that parameter object; use - to read standard input. --wait returns the terminal Action
478
- run and waits at most 25 seconds.`,
538
+ contains that parameter object; use - to read standard input. \`actions request\` runs the Action
539
+ and prints its finished run, with its status and any error. It waits at most 25 seconds: a run that
540
+ takes longer keeps running, and the error names the run to read with \`action-runs get\`. A request
541
+ that waits for the user's approval prints \`"status": "awaiting_approval"\` and exits with code 4.`,
479
542
  "action-runs": `Usage:
480
543
  sixb action-runs list [options]
481
544
  sixb action-runs get <run-id>
@@ -484,7 +547,7 @@ List options:
484
547
  --action <action-id>
485
548
  --type <object-type>
486
549
  --id <primary-id>
487
- --status queued|running|succeeded|failed|cancelled
550
+ --status succeeded|failed
488
551
  --started-after|--started-before <RFC3339>
489
552
  --limit <1-${CLI_LIMITS.list.maximum}> Defaults to ${CLI_LIMITS.list.default}
490
553
  --offset <n> Non-negative; defaults to 0
@@ -496,9 +559,48 @@ List options:
496
559
  workflows: `Usage:
497
560
  sixb workflows list
498
561
  sixb workflows get <workflow-id>
499
- sixb workflows start <workflow-id> [--file <path|->]
562
+ sixb workflows start <workflow-id> [--file <path|->] [--run-id <id>]
563
+
564
+ The JSON file contains the workflow input object. Use - to read standard input. A start that waits
565
+ for the user's approval prints \`"status": "awaiting_approval"\` and exits with code 4.`,
566
+ users: `Usage:
567
+ sixb users list [options]
568
+ sixb users search <text> [options]
569
+ sixb users get <user-id>
570
+
571
+ Options:
572
+ --status active|suspended|all Defaults to active
573
+ --limit <1-${CLI_LIMITS.users.maximum}> Defaults to ${CLI_LIMITS.users.default}
574
+ --offset <n> Non-negative; defaults to 0
500
575
 
501
- The JSON file contains the workflow input object. Use - to read standard input.`,
576
+ \`users search\` matches part of a display name or email, ignoring case. Each member is
577
+ {"id","email","displayName","avatarUrl","status"}; a user parameter takes
578
+ {"type":"user","id":"<id>"}. Reading members requires can.view(project.users).`,
579
+ models: `Usage:
580
+ sixb models list [--kind language|image|speech|transcription]
581
+ sixb models get <model> [--kind <kind>]
582
+ sixb models text "<prompt>"|--file <path|-> [--model <tier|model>] [--instructions <text>]
583
+ [--max-output-tokens <n>] [--reasoning <level>]
584
+ sixb models image "<prompt>" --output <path> [--model <model>] [--n <n>]
585
+ [--size <size>] [--aspect-ratio <ratio>] [--quality <q>] [--style <s>]
586
+ [--seed <n>] [--image <file>]...
587
+ sixb models speech "<text>" --output <path> [--model <model>] [--voice <voice>]
588
+ [--format mp3|wav|ogg|aac|flac] [--instructions <text>]
589
+ [--speed <n>] [--language <code>]
590
+ sixb models transcribe <file> [--model <model>]
591
+ sixb models calls files <call-id> [--output <path>]
592
+
593
+ <model> is a model id, or provider:model-id when two providers share one; without --model, the
594
+ kind's default answers. \`list\` shows each model's kind, default, description and price; \`get\`
595
+ adds the sizes, voices and other options a request can use. Prices are in USD.
596
+
597
+ \`text\` also takes a tier (light, standard or heavy) for --model, and reads a long prompt from
598
+ --file: a path, or - for standard input.
599
+
600
+ --output is a file, numbered when a call returns several, or a directory ending in /. The
601
+ extension follows the file's type, and the result lists the paths written. --image uploads a
602
+ reference image; repeat it for several. \`calls files\` lists a past call's files, or downloads
603
+ them again.`,
502
604
  "workflow-runs": `Usage:
503
605
  sixb workflow-runs list [options]
504
606
  sixb workflow-runs get <run-id>
@@ -518,9 +620,11 @@ var QUERY_EXAMPLES = {
518
620
  not: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Customer"},"predicate":{"op":"not","item":{"op":"eq","propertyId":"status","value":"churned"}}},"limit":${CLI_LIMITS.list.default}}`,
519
621
  in: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Customer"},"predicate":{"op":"in","propertyId":"status","values":["active","trial"]}},"limit":${CLI_LIMITS.list.default}}`,
520
622
  exists: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Customer"},"predicate":{"op":"exists","propertyId":"accountManager","value":false}},"limit":${CLI_LIMITS.list.default}}`,
623
+ mine: `{"kind":"limit","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Task"},"predicate":{"op":"eq","propertyId":"assignee","value":{"type":"currentUser"}}},"limit":${CLI_LIMITS.list.default}}`,
521
624
  project: `{"kind":"project","input":{"kind":"limit","input":{"kind":"start","objectTypeId":"Customer"},"limit":${CLI_LIMITS.list.default}},"properties":["name","status"]}`,
522
625
  incoming: '{"kind":"traverse","input":{"kind":"refs","refs":[{"objectTypeId":"RepositoryIssue","primaryId":"github:issue:owner/repo#297"}]},"linkId":"issue","direction":"incoming","sourceObjectTypeId":"RepositoryComment"}',
523
626
  expand: `{"kind":"expand","input":{"kind":"refs","refs":[{"objectTypeId":"RepositoryIssue","primaryId":"github:issue:owner/repo#297"}]},"expansions":[{"linkId":"issue","direction":"incoming","sourceObjectTypeId":"RepositoryComment","limit":${CLI_LIMITS.list.default}}]}`,
627
+ users: `{"kind":"expand","input":{"kind":"limit","input":{"kind":"start","objectTypeId":"Task"},"limit":${CLI_LIMITS.list.default}},"expansions":[],"users":["assignee"]}`,
524
628
  sort: `{"kind":"limit","input":{"kind":"sort","input":{"kind":"start","objectTypeId":"Customer"},"fields":[{"kind":"property","propertyId":"name","direction":"asc"}]},"limit":${CLI_LIMITS.list.default}}`,
525
629
  page: `{"kind":"page","input":{"kind":"start","objectTypeId":"Customer"},"pageSize":${CLI_LIMITS.list.default}}`,
526
630
  vector: `{"kind":"vector","input":{"kind":"filter","input":{"kind":"start","objectTypeId":"Product"},"predicate":{"op":"eq","propertyId":"status","value":"active"}},"vector":"waterproof trail shoes","profile":"content","k":${CLI_LIMITS.search.default}}`
@@ -558,20 +662,22 @@ function singleFileOption(args, command) {
558
662
  return options["--file"];
559
663
  }
560
664
  async function readJson(source) {
561
- let text;
562
- try {
563
- text = source === "-" ? await readStdin() : await readFile2(source, "utf8");
564
- } catch (error) {
565
- if (isFileError(error, "ENOENT"))
566
- fail(`JSON file '${source}' does not exist.`);
567
- throw error;
568
- }
665
+ const text = await readText(source, "JSON file");
569
666
  try {
570
667
  return JSON.parse(text);
571
668
  } catch {
572
669
  fail(source === "-" ? "Standard input is not valid JSON." : `JSON file '${source}' is not valid JSON.`, "invalid_json");
573
670
  }
574
671
  }
672
+ async function readText(source, noun) {
673
+ try {
674
+ return source === "-" ? await readStdin() : await readFile2(source, "utf8");
675
+ } catch (error) {
676
+ if (isFileError(error, "ENOENT"))
677
+ fail(`${noun} '${source}' does not exist.`);
678
+ throw error;
679
+ }
680
+ }
575
681
  function asRecord2(value) {
576
682
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : {};
577
683
  }
@@ -593,7 +699,8 @@ async function readStdin() {
593
699
  }
594
700
 
595
701
  // src/commands/actions.ts
596
- async function actions(api, args) {
702
+ var ACTION_REQUEST_WAIT_MS = 25000;
703
+ async function actions(api, args, requestWaitMs = ACTION_REQUEST_WAIT_MS) {
597
704
  const [sub, ...rest] = args;
598
705
  if (!sub || isHelp(sub) || requestsHelp(rest))
599
706
  return writeText(GROUP_HELP.actions);
@@ -615,15 +722,13 @@ async function actions(api, args) {
615
722
  "--subject-type": "string",
616
723
  "--subject-id": "string",
617
724
  "--file": "string",
618
- "--run-id": "string",
619
- "--wait": "boolean"
725
+ "--run-id": "string"
620
726
  }, "actions request", 1);
621
727
  const actionId = positionals[0] ?? "";
622
728
  const subjectType = options["--subject-type"];
623
729
  const subjectId = options["--subject-id"];
624
730
  const paramsSource = options["--file"];
625
731
  const runId = options["--run-id"];
626
- const wait = options["--wait"];
627
732
  if (Boolean(subjectType) !== Boolean(subjectId)) {
628
733
  fail("--subject-type and --subject-id must be provided together.");
629
734
  }
@@ -631,42 +736,26 @@ async function actions(api, args) {
631
736
  if (Array.isArray(params) || typeof params !== "object" || params === null) {
632
737
  fail("Action params must be a JSON object.");
633
738
  }
634
- const requested = await api.post(`/api/actions/${encodeURIComponent(actionId)}`, {
635
- params,
636
- ...subjectType && subjectId ? { subject: { kind: "object", objectTypeId: subjectType, primaryId: subjectId } } : {},
637
- ...runId ? { runId } : {}
638
- });
639
- if (!wait)
640
- return writeJson(requested);
641
- const requestedRunId = asRecord2(requested).runId;
642
- if (typeof requestedRunId !== "string" || requestedRunId.length === 0) {
739
+ const requestedRunId = runId ?? `act_${randomUUID2()}`;
740
+ const waiting = AbortSignal.timeout(requestWaitMs);
741
+ try {
742
+ return writeRequestResult(await api.post(`/api/actions/${encodeURIComponent(actionId)}`, {
743
+ params,
744
+ ...subjectType && subjectId ? { subject: { kind: "object", objectTypeId: subjectType, primaryId: subjectId } } : {},
745
+ runId: requestedRunId
746
+ }, { signal: waiting }));
747
+ } catch (error) {
748
+ if (!waiting.aborted)
749
+ throw error;
643
750
  throw new CliError({
644
- code: "invalid_api_response",
645
- message: "The Action request response did not contain a run id."
751
+ code: "action_wait_timeout",
752
+ message: `Action run '${requestedRunId}' did not finish within ${requestWaitMs / 1000} ` + "seconds. It was not cancelled.",
753
+ hint: `Read it with 'sixb action-runs get ${requestedRunId}': its record exists once it ` + `has ended. A repeated request must pass '--run-id ${requestedRunId}', or the ` + "Action runs twice."
646
754
  }, EXIT_API);
647
755
  }
648
- return writeJson(await waitForActionRun(api, requestedRunId));
649
756
  }
650
757
  fail(`Unknown actions command '${sub}'.`);
651
758
  }
652
- var ACTION_WAIT_TIMEOUT_MS = 25000;
653
- var ACTION_WAIT_POLL_MS = 250;
654
- var TERMINAL_ACTION_STATUSES = new Set(["succeeded", "failed", "cancelled"]);
655
- async function waitForActionRun(api, runId) {
656
- const startedAt = Date.now();
657
- while (Date.now() - startedAt < ACTION_WAIT_TIMEOUT_MS) {
658
- const run = await api.get(`/api/action-runs/${encodeURIComponent(runId)}`);
659
- const status = asRecord2(run).status;
660
- if (typeof status === "string" && TERMINAL_ACTION_STATUSES.has(status))
661
- return run;
662
- await new Promise((resolve) => setTimeout(resolve, ACTION_WAIT_POLL_MS));
663
- }
664
- throw new CliError({
665
- code: "action_wait_timeout",
666
- message: `Action run '${runId}' did not finish within ${ACTION_WAIT_TIMEOUT_MS / 1000} seconds.`,
667
- hint: `Inspect it with 'sixb action-runs get ${runId}'.`
668
- }, EXIT_API);
669
- }
670
759
 
671
760
  // src/commands/files.ts
672
761
  import { access } from "fs/promises";
@@ -711,6 +800,340 @@ async function files(api, args) {
711
800
  fail(`Unknown files command '${sub}'.`);
712
801
  }
713
802
 
803
+ // src/commands/models.ts
804
+ import { access as access2, mkdir, stat } from "fs/promises";
805
+ import { dirname as dirname2, extname, join as join2, parse } from "path";
806
+ var MODEL_KINDS = ["language", "image", "speech", "transcription"];
807
+ async function models(api, args) {
808
+ const [sub, ...rest] = args;
809
+ if (!sub || isHelp(sub) || requestsHelp(rest))
810
+ return writeText(GROUP_HELP.models);
811
+ if (sub === "list") {
812
+ const { options } = parseCommandArgs(rest, { "--kind": "string" }, "models list", 0);
813
+ const kind = options["--kind"] === undefined ? undefined : modelKind(options["--kind"]);
814
+ const entries = catalogEntries(await api.get("/api/models"));
815
+ return writeJson(entries.filter((entry) => kind === undefined || entry.kind === kind).map(({ kind, entry }) => ({
816
+ kind,
817
+ model: entry.modelId,
818
+ provider: entry.provider,
819
+ ...entry.tier ? { tier: entry.tier } : {},
820
+ ...entry.isDefault ? { default: true } : {},
821
+ ...entry.description ? { description: entry.description } : {},
822
+ price: describePrice(entry.rateCard)
823
+ })));
824
+ }
825
+ if (sub === "get") {
826
+ const { positionals, options } = parseCommandArgs(rest, { "--kind": "string" }, "models get", 1);
827
+ const kind = options["--kind"] === undefined ? undefined : modelKind(options["--kind"]);
828
+ const entries = catalogEntries(await api.get("/api/models"));
829
+ const { kind: found, entry } = findModel(entries, positionals[0] ?? "", kind);
830
+ const { provider, modelId, isDefault, rateCard, ...details } = entry;
831
+ return writeJson({
832
+ kind: found,
833
+ model: modelId,
834
+ provider,
835
+ default: isDefault,
836
+ ...details,
837
+ price: describePrice(rateCard),
838
+ ...rateCard ? { rateCard } : {}
839
+ });
840
+ }
841
+ if (sub === "text")
842
+ return generateText(api, rest);
843
+ if (sub === "image")
844
+ return generateImages(api, rest);
845
+ if (sub === "speech")
846
+ return generateSpeech(api, rest);
847
+ if (sub === "transcribe")
848
+ return transcribe(api, rest);
849
+ if (sub === "calls")
850
+ return calls(api, rest);
851
+ fail(`Unknown models command '${sub}'.`);
852
+ }
853
+ var SPEECH_FORMATS = {
854
+ mp3: "audio/mpeg",
855
+ wav: "audio/wav",
856
+ ogg: "audio/ogg",
857
+ aac: "audio/aac",
858
+ flac: "audio/flac"
859
+ };
860
+ var SPEECH_FORMAT_NAMES = Object.keys(SPEECH_FORMATS);
861
+ var MEDIA_TYPES = {
862
+ ".png": "image/png",
863
+ ".jpg": "image/jpeg",
864
+ ".jpeg": "image/jpeg",
865
+ ".webp": "image/webp",
866
+ ".gif": "image/gif",
867
+ ".mp3": "audio/mpeg",
868
+ ".wav": "audio/wav",
869
+ ".ogg": "audio/ogg",
870
+ ".aac": "audio/aac",
871
+ ".flac": "audio/flac",
872
+ ".opus": "audio/opus",
873
+ ".m4a": "audio/mp4",
874
+ ".webm": "audio/webm"
875
+ };
876
+ var EXTENSIONS = Object.fromEntries(Object.entries(MEDIA_TYPES).filter(([extension]) => extension !== ".jpeg").map(([extension, mediaType]) => [mediaType, extension]));
877
+ async function generateText(api, args) {
878
+ const { positionals, options } = parseCommandArgs(args, {
879
+ "--model": "string",
880
+ "--instructions": "string",
881
+ "--max-output-tokens": "string",
882
+ "--reasoning": "string",
883
+ "--file": "string"
884
+ }, "models text", [0, 1]);
885
+ const prompt = await readPrompt(positionals[0], options["--file"]);
886
+ const maxOutputTokens = options["--max-output-tokens"];
887
+ const result = await api.post("/api/models/language/generations", {
888
+ ...await languageModelRef(api, options["--model"]),
889
+ prompt,
890
+ ...options["--instructions"] === undefined ? {} : { instructions: options["--instructions"] },
891
+ ...maxOutputTokens === undefined ? {} : { maxOutputTokens: integerInRange("--max-output-tokens", maxOutputTokens, 1, 1e6) },
892
+ ...options["--reasoning"] === undefined ? {} : { reasoning: options["--reasoning"] }
893
+ });
894
+ writeJson({
895
+ callId: result.callId,
896
+ model: result.model.modelId,
897
+ text: result.text,
898
+ finishReason: result.finishReason,
899
+ cost: describeCost(result.cost)
900
+ });
901
+ }
902
+ async function generateImages(api, args) {
903
+ const { positionals, options } = parseCommandArgs(args, {
904
+ "--output": "string",
905
+ "--model": "string",
906
+ "--n": "string",
907
+ "--size": "string",
908
+ "--aspect-ratio": "string",
909
+ "--quality": "string",
910
+ "--style": "string",
911
+ "--seed": "string",
912
+ "--image": "strings"
913
+ }, "models image", 1);
914
+ const output = requireOutput(options["--output"], "models image");
915
+ const seed = options["--seed"];
916
+ if (seed !== undefined && !/^-?[0-9]+$/.test(seed))
917
+ fail("--seed must be an integer.");
918
+ const model = await modelRef(api, options["--model"], "image");
919
+ const images = [];
920
+ for (const path of options["--image"] ?? [])
921
+ images.push(await uploadInput(api, path, "image"));
922
+ const result = await api.post("/api/models/image/generations", {
923
+ ...model,
924
+ prompt: positionals[0],
925
+ ...options["--n"] === undefined ? {} : { n: integerInRange("--n", options["--n"], 1, 100) },
926
+ ...optional("size", options["--size"]),
927
+ ...optional("aspectRatio", options["--aspect-ratio"]),
928
+ ...optional("quality", options["--quality"]),
929
+ ...optional("style", options["--style"]),
930
+ ...seed === undefined ? {} : { seed: Number(seed) },
931
+ ...images.length > 0 ? { images } : {}
932
+ });
933
+ writeJson(await savedResult(api, result, output));
934
+ }
935
+ async function generateSpeech(api, args) {
936
+ const { positionals, options } = parseCommandArgs(args, {
937
+ "--output": "string",
938
+ "--model": "string",
939
+ "--voice": "string",
940
+ "--format": "string",
941
+ "--instructions": "string",
942
+ "--speed": "string",
943
+ "--language": "string"
944
+ }, "models speech", 1);
945
+ const output = requireOutput(options["--output"], "models speech");
946
+ const format = options["--format"];
947
+ const speed = options["--speed"];
948
+ if (speed !== undefined && !(Number(speed) > 0))
949
+ fail("--speed must be a positive number.");
950
+ const result = await api.post("/api/models/audio/speech", {
951
+ ...await modelRef(api, options["--model"], "speech"),
952
+ text: positionals[0],
953
+ ...optional("voice", options["--voice"]),
954
+ ...format === undefined ? {} : { mediaType: SPEECH_FORMATS[enumValue("--format", format, SPEECH_FORMAT_NAMES)] },
955
+ ...optional("instructions", options["--instructions"]),
956
+ ...speed === undefined ? {} : { speed: Number(speed) },
957
+ ...optional("language", options["--language"])
958
+ });
959
+ writeJson(await savedResult(api, result, output));
960
+ }
961
+ async function transcribe(api, args) {
962
+ const { positionals, options } = parseCommandArgs(args, { "--model": "string" }, "models transcribe", 1);
963
+ const model = await modelRef(api, options["--model"], "transcription");
964
+ const audio = await uploadInput(api, positionals[0] ?? "", "audio");
965
+ const result = await api.post("/api/models/audio/transcriptions", {
966
+ ...model,
967
+ audio
968
+ });
969
+ writeJson({
970
+ callId: result.callId,
971
+ model: result.model.modelId,
972
+ text: result.text,
973
+ cost: describeCost(result.cost)
974
+ });
975
+ }
976
+ async function calls(api, args) {
977
+ const [sub, ...rest] = args;
978
+ if (sub !== "files")
979
+ fail(`Unknown models calls command '${sub ?? ""}'. Use 'models calls files'.`);
980
+ const { positionals, options } = parseCommandArgs(rest, { "--output": "string" }, "models calls files", 1);
981
+ const callId = positionals[0] ?? "";
982
+ const call = await api.get(`/api/ai/model-calls/${encodeURIComponent(callId)}/files`);
983
+ const output = options["--output"];
984
+ if (output === undefined)
985
+ return writeJson(call);
986
+ writeJson({ callId: call.callId, files: await saveFiles(api, call.callId, call.files, output) });
987
+ }
988
+ async function modelRef(api, reference, kind) {
989
+ if (reference === undefined)
990
+ return {};
991
+ const { entry } = findModel(catalogEntries(await api.get("/api/models")), reference, kind);
992
+ return { model: { provider: entry.provider, modelId: entry.modelId } };
993
+ }
994
+ var LANGUAGE_TIERS = ["light", "standard", "heavy"];
995
+ async function languageModelRef(api, reference) {
996
+ const tier = LANGUAGE_TIERS.find((candidate) => candidate === reference);
997
+ if (tier !== undefined)
998
+ return { model: tier };
999
+ return modelRef(api, reference, "language");
1000
+ }
1001
+ async function readPrompt(argument, source) {
1002
+ if (argument !== undefined && source !== undefined) {
1003
+ fail("models text takes its prompt as an argument or from --file, not both.");
1004
+ }
1005
+ const prompt = source === undefined ? argument : await readText(source, "Prompt file");
1006
+ if (prompt === undefined) {
1007
+ fail('models text requires a prompt: "<prompt>", or --file <path|-> to read it.');
1008
+ }
1009
+ if (!prompt.trim())
1010
+ fail("The prompt is empty.");
1011
+ return prompt;
1012
+ }
1013
+ function requireOutput(output, command) {
1014
+ if (!output)
1015
+ fail(`${command} requires --output <path>: a file, or a directory ending in /.`);
1016
+ return output;
1017
+ }
1018
+ function optional(field, value) {
1019
+ return value === undefined ? {} : { [field]: value };
1020
+ }
1021
+ async function uploadInput(api, path, kind) {
1022
+ try {
1023
+ await access2(path);
1024
+ } catch {
1025
+ fail(`Input file '${path}' does not exist.`);
1026
+ }
1027
+ const mediaType = MEDIA_TYPES[extname(path).toLowerCase()];
1028
+ if (!mediaType?.startsWith(`${kind}/`)) {
1029
+ const accepted = Object.entries(MEDIA_TYPES).filter(([, type]) => type.startsWith(`${kind}/`)).map(([extension]) => extension);
1030
+ fail(`'${path}' is not an ${kind} file. Use ${accepted.join(", ")}.`);
1031
+ }
1032
+ return api.upload("/api/files", path, undefined, mediaType);
1033
+ }
1034
+ async function savedResult(api, result, output) {
1035
+ return {
1036
+ callId: result.callId,
1037
+ model: result.model.modelId,
1038
+ files: await saveFiles(api, result.callId, result.files ?? [], output),
1039
+ ...result.warnings?.length ? { warnings: result.warnings } : {},
1040
+ cost: describeCost(result.cost)
1041
+ };
1042
+ }
1043
+ async function saveFiles(api, callId, files, output) {
1044
+ const directory = output.endsWith("/") || (await stat(output).catch(() => null))?.isDirectory();
1045
+ const paths = files.map((file, index) => {
1046
+ const number = files.length > 1 ? `-${index + 1}` : "";
1047
+ const extension = (file.mediaType && EXTENSIONS[file.mediaType]) ?? "";
1048
+ if (directory)
1049
+ return join2(output, `${callId}${number}${extension || ".bin"}`);
1050
+ const { dir, name, ext } = parse(output);
1051
+ return join2(dir, `${name}${number}${extension || ext}`);
1052
+ });
1053
+ for (const [index, path] of paths.entries()) {
1054
+ await mkdir(dirname2(path), { recursive: true });
1055
+ await api.download(`/api/ai/model-calls/${encodeURIComponent(callId)}/files/content`, path, {
1056
+ path: `/files/${index}`
1057
+ });
1058
+ }
1059
+ return paths;
1060
+ }
1061
+ function describeCost(cost) {
1062
+ if (!cost.money)
1063
+ return "unknown";
1064
+ const nanos = BigInt(cost.money.amountNanos);
1065
+ const whole = nanos / 1000000000n;
1066
+ const fraction = (nanos % 1000000000n).toString().padStart(9, "0").replace(/0+$/, "");
1067
+ return `$${whole}${fraction ? `.${fraction}` : ""}`;
1068
+ }
1069
+ function modelKind(value) {
1070
+ const kind = MODEL_KINDS.find((candidate) => candidate === value);
1071
+ if (!kind)
1072
+ fail(`Unknown model kind '${value}'. Use one of: ${MODEL_KINDS.join(", ")}.`);
1073
+ return kind;
1074
+ }
1075
+ function catalogEntries(catalog) {
1076
+ return [
1077
+ ...catalog.language.map((entry) => ({ kind: "language", entry })),
1078
+ ...catalog.image.map((entry) => ({ kind: "image", entry })),
1079
+ ...catalog.audio.speech.map((entry) => ({ kind: "speech", entry })),
1080
+ ...catalog.audio.transcription.map((entry) => ({ kind: "transcription", entry }))
1081
+ ];
1082
+ }
1083
+ function findModel(entries, reference, kind) {
1084
+ const ofKind = entries.filter((candidate) => kind === undefined || candidate.kind === kind);
1085
+ let matches = ofKind.filter((candidate) => candidate.entry.modelId === reference);
1086
+ const separator = reference.indexOf(":");
1087
+ if (matches.length === 0 && separator > 0) {
1088
+ const provider = reference.slice(0, separator);
1089
+ const modelId = reference.slice(separator + 1);
1090
+ matches = ofKind.filter((candidate) => candidate.entry.provider === provider && candidate.entry.modelId === modelId);
1091
+ }
1092
+ const [match] = matches;
1093
+ if (!match) {
1094
+ fail(`No ${kind === undefined ? "" : `${kind} `}model '${reference}' is configured. Run 'sixb models list'.`);
1095
+ }
1096
+ if (matches.length > 1) {
1097
+ const kinds = new Set(matches.map((candidate) => candidate.kind));
1098
+ fail(kinds.size > 1 ? `'${reference}' is configured as ${[...kinds].join(" and ")}. Add --kind <kind>.` : `'${reference}' is configured by ${matches.map((candidate) => candidate.entry.provider).join(" and ")}. Use provider:${reference}.`);
1099
+ }
1100
+ return match;
1101
+ }
1102
+ function describePrice(card) {
1103
+ if (!card)
1104
+ return "price unknown";
1105
+ const parts = [
1106
+ variantPrice(card.images, "per image", "at"),
1107
+ variantPrice(card.characters, "per 1M characters", "for"),
1108
+ variantPrice(card.audioInputSeconds, "per second of audio", "for"),
1109
+ card.input !== undefined && card.output !== undefined ? `${dollars(tokenPrice(card.input))} input \xB7 ${dollars(tokenPrice(card.output))} output per 1M tokens` : undefined,
1110
+ card.audioOutput === undefined ? undefined : `${dollars(tokenPrice(card.audioOutput))} per 1M audio tokens`
1111
+ ].filter((part) => part !== undefined);
1112
+ return parts.length === 0 ? "price unknown" : parts.join(" \xB7 ");
1113
+ }
1114
+ function variantPrice(price, unit, joiner) {
1115
+ if (price === undefined)
1116
+ return;
1117
+ if (typeof price === "string")
1118
+ return `${dollars(price)} ${unit}`;
1119
+ const only = price.default;
1120
+ if (only !== undefined && price.variants.every((variant) => variant.price === only)) {
1121
+ return `${dollars(only)} ${unit}`;
1122
+ }
1123
+ const variants = price.variants.map(({ price: amount, ...dimensions }) => {
1124
+ const label = Object.values(dimensions).join(" ");
1125
+ return `${dollars(amount)} ${joiner} ${label}`;
1126
+ });
1127
+ const otherwise = price.default === undefined ? [] : [`otherwise ${dollars(price.default)}`];
1128
+ return `${unit}: ${[...variants, ...otherwise].join(", ")}`;
1129
+ }
1130
+ function tokenPrice(price) {
1131
+ return typeof price === "string" ? price : price.default;
1132
+ }
1133
+ function dollars(amount) {
1134
+ return `$${amount}`;
1135
+ }
1136
+
714
1137
  // src/graph.ts
715
1138
  async function inspectGraph(api, objectTypeId, primaryId, options) {
716
1139
  if (options.depth === 0)
@@ -976,6 +1399,8 @@ async function objects(api, args) {
976
1399
  return objectsList(api, rest);
977
1400
  case "get":
978
1401
  return objectsGet(api, rest);
1402
+ case "get-by":
1403
+ return objectsGetBy(api, rest);
979
1404
  case "search":
980
1405
  return objectsSearch(api, rest);
981
1406
  case "query":
@@ -1066,6 +1491,31 @@ async function objectsGet(api, args) {
1066
1491
  includeTotal: false
1067
1492
  }));
1068
1493
  }
1494
+ async function objectsGetBy(api, args) {
1495
+ if (requestsHelp(args))
1496
+ return writeText(GET_BY_HELP);
1497
+ const { positionals, options } = parseCommandArgs(args, { "--json": "boolean" }, "objects get-by", 3);
1498
+ const [objectTypeId = "", propertyId = "", text = ""] = positionals;
1499
+ const value = options["--json"] ? parseJsonValue(text) : text;
1500
+ const path = `/api/objects/${encodeURIComponent(objectTypeId)}/by/${encodeURIComponent(propertyId)}`;
1501
+ try {
1502
+ writeJson(await api.post(path, { value }));
1503
+ } catch (error) {
1504
+ if (!isObjectNotFound(error))
1505
+ throw error;
1506
+ writeJson(null);
1507
+ }
1508
+ }
1509
+ function parseJsonValue(text) {
1510
+ try {
1511
+ return JSON.parse(text);
1512
+ } catch {
1513
+ fail(`--json needs the value as JSON, such as 42 or '{"type":"user","id":"usr_1"}'.`);
1514
+ }
1515
+ }
1516
+ function isObjectNotFound(error) {
1517
+ return error instanceof CliError && error.body.status === 404 && error.body.code === "object.not_found";
1518
+ }
1069
1519
  async function objectsSearch(api, args) {
1070
1520
  if (requestsHelp(args))
1071
1521
  return writeText(SEARCH_HELP);
@@ -1263,7 +1713,7 @@ async function project(api, args) {
1263
1713
  }
1264
1714
 
1265
1715
  // src/commands/runs.ts
1266
- var ACTION_RUN_STATUSES = ["queued", "running", "succeeded", "failed", "cancelled"];
1716
+ var ACTION_RUN_STATUSES = ["succeeded", "failed"];
1267
1717
  var WORKFLOW_RUN_STATUSES = [
1268
1718
  "queued",
1269
1719
  "running",
@@ -1387,7 +1837,45 @@ function normalizeTelemetryQueryInput(input) {
1387
1837
  return body;
1388
1838
  }
1389
1839
 
1840
+ // src/commands/users.ts
1841
+ var USER_STATUSES = ["active", "suspended", "all"];
1842
+ var LIST_FLAGS = {
1843
+ "--status": "string",
1844
+ "--limit": "string",
1845
+ "--offset": "string"
1846
+ };
1847
+ async function users(api, args) {
1848
+ const [sub, ...rest] = args;
1849
+ if (!sub || isHelp(sub) || requestsHelp(rest))
1850
+ return writeText(GROUP_HELP.users);
1851
+ if (sub === "get") {
1852
+ const { positionals } = parseCommandArgs(rest, {}, "users get", 1);
1853
+ return writeJson(await api.get(`/api/users/${encodeURIComponent(positionals[0] ?? "")}`));
1854
+ }
1855
+ if (sub === "list") {
1856
+ const { options } = parseCommandArgs(rest, LIST_FLAGS, "users list");
1857
+ return writeJson(await api.get("/api/users", listQuery(options)));
1858
+ }
1859
+ if (sub === "search") {
1860
+ const { positionals, options } = parseCommandArgs(rest, LIST_FLAGS, "users search", 1);
1861
+ const search = positionals[0]?.trim();
1862
+ if (!search)
1863
+ fail("users search requires the text to find in a name or email.");
1864
+ return writeJson(await api.get("/api/users", { ...listQuery(options), search }));
1865
+ }
1866
+ fail(`Unknown users command '${sub}'.`);
1867
+ }
1868
+ function listQuery(options) {
1869
+ const limit = integerInRange("--limit", options["--limit"] ?? String(CLI_LIMITS.users.default), 1, CLI_LIMITS.users.maximum);
1870
+ return {
1871
+ status: enumValue("--status", options["--status"] ?? "active", USER_STATUSES),
1872
+ limit: String(limit),
1873
+ ...options["--offset"] === undefined ? {} : { offset: String(nonNegativeInteger("--offset", options["--offset"])) }
1874
+ };
1875
+ }
1876
+
1390
1877
  // src/commands/workflows.ts
1878
+ import { randomUUID as randomUUID3 } from "crypto";
1391
1879
  async function workflows(api, args) {
1392
1880
  const [sub, ...rest] = args;
1393
1881
  if (!sub || isHelp(sub) || requestsHelp(rest))
@@ -1406,12 +1894,16 @@ async function workflows(api, args) {
1406
1894
  const {
1407
1895
  positionals: [workflowId],
1408
1896
  options
1409
- } = parseCommandArgs(rest, { "--file": "string" }, "workflows start", 1);
1897
+ } = parseCommandArgs(rest, { "--file": "string", "--run-id": "string" }, "workflows start", 1);
1410
1898
  const input = options["--file"] ? await readJson(options["--file"]) : {};
1411
1899
  if (Array.isArray(input) || typeof input !== "object" || input === null) {
1412
1900
  fail("Workflow input must be a JSON object.");
1413
1901
  }
1414
- return writeJson(await api.post(`/api/workflows/${encodeURIComponent(workflowId ?? "")}/runs`, { input }));
1902
+ const runId = options["--run-id"] ?? `run_${randomUUID3()}`;
1903
+ return writeRequestResult(await api.post(`/api/workflows/${encodeURIComponent(workflowId ?? "")}/runs`, {
1904
+ input,
1905
+ runId
1906
+ }));
1415
1907
  }
1416
1908
  fail(`Unknown workflows command '${sub}'.`);
1417
1909
  }
@@ -1426,7 +1918,9 @@ var INSTANCE_COMMANDS = [
1426
1918
  "action-runs",
1427
1919
  "files",
1428
1920
  "workflows",
1429
- "workflow-runs"
1921
+ "workflow-runs",
1922
+ "users",
1923
+ "models"
1430
1924
  ];
1431
1925
  function isInstanceCommand(command) {
1432
1926
  return INSTANCE_COMMANDS.includes(command);
@@ -1451,6 +1945,10 @@ async function dispatch(api, command, args) {
1451
1945
  return workflows(api, args);
1452
1946
  case "workflow-runs":
1453
1947
  return runs(api, "workflow", args);
1948
+ case "users":
1949
+ return users(api, args);
1950
+ case "models":
1951
+ return models(api, args);
1454
1952
  default:
1455
1953
  fail(`Unknown command '${command}'. Run 'sixb --help'.`);
1456
1954
  }
@@ -1490,4 +1988,4 @@ export {
1490
1988
  writeText
1491
1989
  };
1492
1990
 
1493
- //# debugId=F1EB880C95B058F464756E2164756E21
1991
+ //# debugId=7F5CD0F40A1B96CA64756E2164756E21