@spira-lab/cli 0.0.4 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +6 -0
  2. package/dist/index.cjs +887 -50
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -14,8 +14,14 @@ npm install -g @spira-lab/cli
14
14
  spira --help
15
15
  spira auth login
16
16
  spira ai models list
17
+ spira ai models list --media-type audio
18
+ spira ai jobs create --model-id nano-banana-2 --inputs-json '{"prompt":"A product photo"}' --wait
19
+ spira ai jobs create --model-id elevenlabs-music-generation --inputs-json '{"prompt":"A short ambient cue","duration":"10"}' --wait
20
+ spira workflows node-catalog --json
17
21
  spira workflows create --name "Product launch workflow"
18
22
  spira workflows update <workflowId> --workflow-definition-json '{"nodes":[],"edges":[]}'
19
23
  ```
20
24
 
21
25
  Use `spira --json` when you need machine-readable output.
26
+
27
+ `ai jobs create --wait` prints the job ID and status changes to stderr while it waits. Its final stdout is a compact result containing the job ID, model, status, result type, and result URLs. Use `spira --json ai jobs status <jobId>` when you need the full job and provider execution details.
package/dist/index.cjs CHANGED
@@ -18049,6 +18049,7 @@ var cliResultTypeSchema = external_exports.enum([
18049
18049
  "aiModelList",
18050
18050
  "aiModelDetail",
18051
18051
  "workflowList",
18052
+ "workflowNodeCatalog",
18052
18053
  "workflowDetail",
18053
18054
  "workflowRunList",
18054
18055
  "workflowRunDetail",
@@ -18058,6 +18059,10 @@ var cliResultTypeSchema = external_exports.enum([
18058
18059
  "scheduleOccurrenceList",
18059
18060
  "scheduleExecutionList",
18060
18061
  "scheduleAction",
18062
+ "dataBaseGlob",
18063
+ "dataBaseRead",
18064
+ "dataBaseGrep",
18065
+ "dataBaseSearch",
18061
18066
  "generic"
18062
18067
  ]);
18063
18068
  var cliResultSchema = external_exports.object({
@@ -20193,6 +20198,260 @@ var cliAiModelDocs = [
20193
20198
  "first_last_frames_to_video requires exactly 2 referenceImage URLs.",
20194
20199
  "extendFromTaskId requires extendProviderId=socheap-media and resolution=720p."
20195
20200
  ]
20201
+ },
20202
+ {
20203
+ id: "suno-music",
20204
+ name: "Suno Music",
20205
+ mediaType: "audio",
20206
+ description: "Suno music generation with simple prompt and custom song modes.",
20207
+ capabilities: ["text-to-audio"],
20208
+ providers: ["socheap-media"],
20209
+ variants: [
20210
+ {
20211
+ key: "v5_5",
20212
+ display: "v5_5",
20213
+ conditions: {},
20214
+ billingUnit: "per_generation",
20215
+ creditsPerUnit: 6.6
20216
+ }
20217
+ ],
20218
+ fields: [
20219
+ {
20220
+ name: "prompt",
20221
+ type: "string",
20222
+ description: "Song description in simple mode, or lyrics in custom vocal mode."
20223
+ },
20224
+ {
20225
+ name: "mode",
20226
+ type: "string",
20227
+ description: "Suno model version.",
20228
+ values: ["v5_5"],
20229
+ defaultValue: "v5_5"
20230
+ },
20231
+ {
20232
+ name: "customMode",
20233
+ type: "boolean",
20234
+ description: "Enable custom lyrics, style, and title inputs.",
20235
+ defaultValue: false
20236
+ },
20237
+ {
20238
+ name: "instrumental",
20239
+ type: "boolean",
20240
+ description: "Generate an instrumental track. Only used in custom mode.",
20241
+ defaultValue: false
20242
+ },
20243
+ {
20244
+ name: "style",
20245
+ type: "string",
20246
+ description: "Required music style in custom mode."
20247
+ },
20248
+ {
20249
+ name: "title",
20250
+ type: "string",
20251
+ description: "Required song title in custom mode."
20252
+ }
20253
+ ],
20254
+ examples: [
20255
+ {
20256
+ label: "Generate music",
20257
+ inputs: {
20258
+ prompt: "A warm upbeat electronic instrumental for a product launch",
20259
+ mode: "v5_5"
20260
+ }
20261
+ }
20262
+ ],
20263
+ notes: [
20264
+ "Simple mode requires prompt. Custom mode requires style and title; vocal custom mode also requires prompt as lyrics."
20265
+ ]
20266
+ },
20267
+ {
20268
+ id: "elevenlabs-music-generation",
20269
+ name: "Eleven Music V1",
20270
+ mediaType: "audio",
20271
+ description: "ElevenLabs prompt-to-music generation.",
20272
+ capabilities: ["text-to-audio"],
20273
+ providers: ["elevenlabs-audio"],
20274
+ variants: [
20275
+ {
20276
+ key: "default",
20277
+ display: "Per second",
20278
+ conditions: {},
20279
+ billingUnit: "per_second",
20280
+ creditsPerUnit: 0.25
20281
+ }
20282
+ ],
20283
+ fields: [
20284
+ {
20285
+ name: "prompt",
20286
+ type: "string",
20287
+ description: "Description of the music to generate.",
20288
+ required: true
20289
+ },
20290
+ {
20291
+ name: "duration",
20292
+ type: "string",
20293
+ description: "Whole number of seconds from 1 to 300.",
20294
+ defaultValue: "10"
20295
+ }
20296
+ ],
20297
+ examples: [
20298
+ {
20299
+ label: "Generate music",
20300
+ inputs: {
20301
+ prompt: "A calm ambient piano cue with a gentle ending",
20302
+ duration: "10"
20303
+ }
20304
+ }
20305
+ ]
20306
+ },
20307
+ {
20308
+ id: "elevenlabs-sound-effects",
20309
+ name: "Eleven Text To Sound V2",
20310
+ mediaType: "audio",
20311
+ description: "ElevenLabs sound-effect generation from an English prompt.",
20312
+ capabilities: ["text-to-audio"],
20313
+ providers: ["elevenlabs-audio"],
20314
+ variants: [
20315
+ {
20316
+ key: "default",
20317
+ display: "Per generation",
20318
+ conditions: {},
20319
+ billingUnit: "per_generation",
20320
+ creditsPerUnit: 12
20321
+ }
20322
+ ],
20323
+ fields: [
20324
+ {
20325
+ name: "prompt",
20326
+ type: "string",
20327
+ description: "English description of the sound effect.",
20328
+ required: true
20329
+ },
20330
+ {
20331
+ name: "duration",
20332
+ type: "string",
20333
+ description: "Duration in seconds from 0.5 to 30.",
20334
+ defaultValue: "5"
20335
+ }
20336
+ ],
20337
+ examples: [
20338
+ {
20339
+ label: "Generate a sound effect",
20340
+ inputs: {
20341
+ prompt: "A soft camera shutter in a quiet studio",
20342
+ duration: "2"
20343
+ }
20344
+ }
20345
+ ]
20346
+ },
20347
+ {
20348
+ id: "elevenlabs-text-to-speech",
20349
+ name: "ElevenLabs Text to Speech",
20350
+ mediaType: "audio",
20351
+ description: "ElevenLabs text-to-speech using an existing voice ID.",
20352
+ capabilities: ["text-to-speech"],
20353
+ providers: ["elevenlabs-audio"],
20354
+ variants: [
20355
+ {
20356
+ key: "multilingual-v2-v3",
20357
+ display: "Per character",
20358
+ conditions: {},
20359
+ billingUnit: "per_character",
20360
+ creditsPerUnit: 0.01
20361
+ }
20362
+ ],
20363
+ fields: [
20364
+ {
20365
+ name: "text",
20366
+ type: "string",
20367
+ description: "Text to speak.",
20368
+ required: true
20369
+ },
20370
+ {
20371
+ name: "voiceId",
20372
+ type: "string",
20373
+ description: "Existing ElevenLabs voice ID.",
20374
+ required: true
20375
+ },
20376
+ {
20377
+ name: "providerModelId",
20378
+ type: "string",
20379
+ description: "ElevenLabs speech model.",
20380
+ values: ["eleven_multilingual_v2", "eleven_v3"],
20381
+ defaultValue: "eleven_multilingual_v2"
20382
+ }
20383
+ ],
20384
+ examples: [
20385
+ {
20386
+ label: "Generate speech",
20387
+ inputs: {
20388
+ text: "Welcome to Spira.",
20389
+ voiceId: "<voice-id>",
20390
+ providerModelId: "eleven_multilingual_v2"
20391
+ }
20392
+ }
20393
+ ]
20394
+ },
20395
+ {
20396
+ id: "elevenlabs-voice-clone-tts",
20397
+ name: "ElevenLabs Voice TTS",
20398
+ mediaType: "audio",
20399
+ description: "Text-to-speech using either an existing voice ID or one reference audio URL.",
20400
+ capabilities: ["text-to-speech", "audio-to-speech"],
20401
+ providers: ["elevenlabs-audio"],
20402
+ variants: [
20403
+ {
20404
+ key: "voice-clone-tts",
20405
+ display: "Per character",
20406
+ conditions: {},
20407
+ billingUnit: "per_character",
20408
+ creditsPerUnit: 0.01
20409
+ }
20410
+ ],
20411
+ fields: [
20412
+ {
20413
+ name: "text",
20414
+ type: "string",
20415
+ description: "Text to speak.",
20416
+ required: true
20417
+ },
20418
+ {
20419
+ name: "voiceId",
20420
+ type: "string",
20421
+ description: "Existing ElevenLabs voice ID. Do not combine with referenceAudio."
20422
+ },
20423
+ {
20424
+ name: "referenceAudio",
20425
+ type: "string[]",
20426
+ description: "Exactly one audio URL when cloning a voice. Do not combine with voiceId."
20427
+ },
20428
+ {
20429
+ name: "providerModelId",
20430
+ type: "string",
20431
+ description: "ElevenLabs speech model.",
20432
+ values: ["eleven_multilingual_v2", "eleven_v3"],
20433
+ defaultValue: "eleven_multilingual_v2"
20434
+ },
20435
+ {
20436
+ name: "qualityMode",
20437
+ type: "string",
20438
+ description: "Voice delivery mode.",
20439
+ values: ["stable", "expressive"],
20440
+ defaultValue: "stable"
20441
+ }
20442
+ ],
20443
+ examples: [
20444
+ {
20445
+ label: "Generate speech from a reference voice",
20446
+ inputs: {
20447
+ text: "Welcome to Spira.",
20448
+ referenceAudio: ["https://example.com/reference.mp3"],
20449
+ providerModelId: "eleven_multilingual_v2",
20450
+ qualityMode: "stable"
20451
+ }
20452
+ }
20453
+ ],
20454
+ notes: ["Provide either voiceId or exactly one referenceAudio URL, never both."]
20196
20455
  }
20197
20456
  ];
20198
20457
  function listCliAiModelDocs(mediaType) {
@@ -20202,6 +20461,115 @@ function getCliAiModelDoc(modelId) {
20202
20461
  return cliAiModelDocs.find((model) => model.id === modelId);
20203
20462
  }
20204
20463
 
20464
+ // src/ai/wait.ts
20465
+ function wait(durationMs) {
20466
+ return new Promise((resolve) => setTimeout(resolve, durationMs));
20467
+ }
20468
+ function jobStatus(result) {
20469
+ if (!result.data || typeof result.data !== "object") return void 0;
20470
+ const status = result.data.status;
20471
+ return typeof status === "string" ? status : void 0;
20472
+ }
20473
+ function providerError(result) {
20474
+ if (!result.data || typeof result.data !== "object") return void 0;
20475
+ const latestExecution = result.data.latestExecution;
20476
+ if (!latestExecution || typeof latestExecution !== "object") return void 0;
20477
+ const error51 = latestExecution.error;
20478
+ if (typeof error51 === "string") return error51;
20479
+ if (error51 && typeof error51 === "object" && typeof error51.message === "string") {
20480
+ return error51.message;
20481
+ }
20482
+ return void 0;
20483
+ }
20484
+ function completedResult(result) {
20485
+ return cliResultSchema.parse({
20486
+ ...result,
20487
+ resultType: "aiJobStatus",
20488
+ command: "spira ai jobs create",
20489
+ nextActions: []
20490
+ });
20491
+ }
20492
+ function pollingError(result) {
20493
+ return cliResultSchema.parse({
20494
+ ...result,
20495
+ command: "spira ai jobs create"
20496
+ });
20497
+ }
20498
+ function terminalError(result, jobId, status) {
20499
+ const message = providerError(result) ?? `AI job ${jobId} ended with status ${status}.`;
20500
+ return cliResultSchema.parse({
20501
+ ...result,
20502
+ status: "error",
20503
+ resultType: "aiJobStatus",
20504
+ command: "spira ai jobs create",
20505
+ error: {
20506
+ code: status === "FAILED" ? "PROVIDER_FAILED" : "ASYNC_STATE",
20507
+ message,
20508
+ resource: { type: "aiJob", id: jobId },
20509
+ retryable: false,
20510
+ nextActions: [{ label: "Show job status", command: `spira ai jobs status ${jobId}` }]
20511
+ }
20512
+ });
20513
+ }
20514
+ function timeoutResult(jobId, timeoutMs, lastResult) {
20515
+ return cliResultSchema.parse({
20516
+ object: "cli.result",
20517
+ status: "error",
20518
+ resultType: "aiJobStatus",
20519
+ command: "spira ai jobs create",
20520
+ data: lastResult?.data,
20521
+ ids: { ...lastResult?.ids, jobId },
20522
+ error: {
20523
+ code: "TIMEOUT",
20524
+ message: `AI job ${jobId} did not finish within ${timeoutMs}ms.`,
20525
+ resource: { type: "aiJob", id: jobId },
20526
+ retryable: true,
20527
+ nextActions: [{ label: "Show job status", command: `spira ai jobs status ${jobId}` }]
20528
+ }
20529
+ });
20530
+ }
20531
+ function contractError(result, jobId) {
20532
+ return cliResultSchema.parse({
20533
+ ...result,
20534
+ status: "error",
20535
+ resultType: "aiJobStatus",
20536
+ command: "spira ai jobs create",
20537
+ error: {
20538
+ code: "CONTRACT_MISMATCH",
20539
+ message: `AI job ${jobId} status response did not contain a recognized status.`,
20540
+ resource: { type: "aiJob", id: jobId },
20541
+ retryable: false,
20542
+ nextActions: [{ label: "Show job status", command: `spira ai jobs status ${jobId}` }]
20543
+ }
20544
+ });
20545
+ }
20546
+ async function waitForAiJob(input) {
20547
+ const now = input.now ?? Date.now;
20548
+ const sleep = input.sleep ?? wait;
20549
+ const deadline = now() + input.timeoutMs;
20550
+ let lastResult;
20551
+ let lastStatus;
20552
+ while (true) {
20553
+ const result = await input.poll();
20554
+ lastResult = result;
20555
+ if (result.status === "error") {
20556
+ if (!result.error?.retryable) return pollingError(result);
20557
+ } else {
20558
+ const status = jobStatus(result);
20559
+ if (status && status !== lastStatus) {
20560
+ input.onStatus?.(status);
20561
+ lastStatus = status;
20562
+ }
20563
+ if (status === "COMPLETED") return completedResult(result);
20564
+ if (status === "FAILED" || status === "CANCELED") return terminalError(result, input.jobId, status);
20565
+ if (status !== "QUEUED" && status !== "RUNNING") return contractError(result, input.jobId);
20566
+ }
20567
+ const remainingMs = deadline - now();
20568
+ if (remainingMs <= 0) return timeoutResult(input.jobId, input.timeoutMs, lastResult);
20569
+ await sleep(Math.min(input.intervalMs, remainingMs));
20570
+ }
20571
+ }
20572
+
20205
20573
  // src/commands/definitions.ts
20206
20574
  var commonListOptions = [
20207
20575
  {
@@ -20385,7 +20753,10 @@ var commandDefinitions = [
20385
20753
  },
20386
20754
  resultType: "socialSubmissionList",
20387
20755
  examples: ["spira social submissions list --platform tiktok --status draft --limit 10"],
20388
- relatedCommands: ["spira social submissions get <submissionId>", "spira social submissions publish <submissionId> --confirm"]
20756
+ relatedCommands: [
20757
+ "spira social submissions get <submissionId>",
20758
+ "spira social submissions publish <submissionId> --confirm"
20759
+ ]
20389
20760
  },
20390
20761
  {
20391
20762
  path: ["social", "submissions", "get"],
@@ -20431,7 +20802,12 @@ var commandDefinitions = [
20431
20802
  defaultValue: "oauth_account"
20432
20803
  },
20433
20804
  { name: "title", flag: "--title <title>", description: "Draft title.", type: "string" },
20434
- { name: "description", flag: "--description <description>", description: "Draft description/text.", type: "string" },
20805
+ {
20806
+ name: "description",
20807
+ flag: "--description <description>",
20808
+ description: "Draft description/text.",
20809
+ type: "string"
20810
+ },
20435
20811
  { name: "mediaUrls", flag: "--media-url <url>", description: "Media URL. Repeatable.", type: "stringArray" },
20436
20812
  { name: "agentId", flag: "--agent-id <agentId>", description: "Associated agent ID.", type: "string" },
20437
20813
  {
@@ -20458,9 +20834,7 @@ var commandDefinitions = [
20458
20834
  ]
20459
20835
  },
20460
20836
  resultType: "socialSubmissionAction",
20461
- examples: [
20462
- 'spira social submissions create --platform twitter --content-kind text --description "Launch note"'
20463
- ],
20837
+ examples: ['spira social submissions create --platform twitter --content-kind text --description "Launch note"'],
20464
20838
  relatedCommands: [
20465
20839
  "spira social submissions get <submissionId>",
20466
20840
  "spira social submissions publish <submissionId> --confirm"
@@ -20471,9 +20845,20 @@ var commandDefinitions = [
20471
20845
  purpose: "Publish a social submission draft.",
20472
20846
  arguments: [{ name: "submissionId", description: "Submission ID.", required: true }],
20473
20847
  options: [
20474
- { name: "confirm", flag: "--confirm", description: "Required explicit publish confirmation.", type: "boolean", required: true },
20848
+ {
20849
+ name: "confirm",
20850
+ flag: "--confirm",
20851
+ description: "Required explicit publish confirmation.",
20852
+ type: "boolean",
20853
+ required: true
20854
+ },
20475
20855
  { name: "title", flag: "--title <title>", description: "Platform title override.", type: "string" },
20476
- { name: "description", flag: "--description <description>", description: "Platform description override.", type: "string" },
20856
+ {
20857
+ name: "description",
20858
+ flag: "--description <description>",
20859
+ description: "Platform description override.",
20860
+ type: "string"
20861
+ },
20477
20862
  { name: "text", flag: "--text <text>", description: "Text override for X or LinkedIn.", type: "string" },
20478
20863
  { name: "mediaUrls", flag: "--media-url <url>", description: "Media URL. Repeatable.", type: "stringArray" },
20479
20864
  { name: "privacyLevel", flag: "--privacy-level <value>", description: "TikTok privacy level.", type: "string" },
@@ -20514,18 +20899,18 @@ var commandDefinitions = [
20514
20899
  },
20515
20900
  {
20516
20901
  path: ["ai", "models", "list"],
20517
- purpose: "List AI image and video models available from the CLI.",
20902
+ purpose: "List AI image, video, and audio models available from the CLI.",
20518
20903
  options: [
20519
20904
  {
20520
20905
  name: "mediaType",
20521
20906
  flag: "--media-type <type>",
20522
20907
  description: "Filter by model media type.",
20523
20908
  type: "string",
20524
- enumValues: ["image", "video"]
20909
+ enumValues: ["image", "video", "audio"]
20525
20910
  }
20526
20911
  ],
20527
20912
  resultType: "aiModelList",
20528
- examples: ["spira ai models list", "spira ai models list --media-type video"],
20913
+ examples: ["spira ai models list", "spira ai models list --media-type audio"],
20529
20914
  relatedCommands: ["spira ai models get <modelId>", "spira ai jobs create"]
20530
20915
  },
20531
20916
  {
@@ -20538,11 +20923,45 @@ var commandDefinitions = [
20538
20923
  },
20539
20924
  {
20540
20925
  path: ["ai", "jobs", "create"],
20541
- purpose: "Create an AI image or video generation job.",
20926
+ purpose: "Create an AI image, video, or audio generation job.",
20542
20927
  options: [
20543
20928
  { name: "modelId", flag: "--model-id <modelId>", description: "AI model ID.", type: "string", required: true },
20544
- { name: "providerId", flag: "--provider-id <providerId>", description: "Advanced routing override.", type: "string", hidden: true },
20545
- { name: "inputsJson", flag: "--inputs-json <json>", description: "Model inputs JSON object.", type: "json", required: true }
20929
+ {
20930
+ name: "providerId",
20931
+ flag: "--provider-id <providerId>",
20932
+ description: "Advanced routing override.",
20933
+ type: "string",
20934
+ hidden: true
20935
+ },
20936
+ {
20937
+ name: "inputsJson",
20938
+ flag: "--inputs-json <json>",
20939
+ description: "Model inputs JSON object.",
20940
+ type: "json",
20941
+ required: true
20942
+ },
20943
+ {
20944
+ name: "wait",
20945
+ flag: "--wait",
20946
+ description: "Wait for the job to finish and print the final result.",
20947
+ type: "boolean"
20948
+ },
20949
+ {
20950
+ name: "waitTimeout",
20951
+ flag: "--wait-timeout <duration>",
20952
+ description: "Maximum wait duration.",
20953
+ type: "duration",
20954
+ defaultValue: 6e5,
20955
+ minimum: 1e3
20956
+ },
20957
+ {
20958
+ name: "waitInterval",
20959
+ flag: "--wait-interval <duration>",
20960
+ description: "Polling interval while waiting.",
20961
+ type: "duration",
20962
+ defaultValue: 3e3,
20963
+ minimum: 1e3
20964
+ }
20546
20965
  ],
20547
20966
  http: {
20548
20967
  method: "POST",
@@ -20555,7 +20974,10 @@ var commandDefinitions = [
20555
20974
  ]
20556
20975
  },
20557
20976
  resultType: "aiJobAction",
20558
- examples: [`spira ai jobs create --model-id nano-banana-2 --inputs-json '{"prompt":"A product photo"}'`],
20977
+ examples: [
20978
+ `spira ai jobs create --model-id nano-banana-2 --inputs-json '{"prompt":"A product photo"}'`,
20979
+ `spira ai jobs create --model-id nano-banana-2 --inputs-json '{"prompt":"A product photo"}' --wait`
20980
+ ],
20559
20981
  relatedCommands: ["spira ai jobs status <jobId>", "spira ai models get <modelId>", "spira credits balance"]
20560
20982
  },
20561
20983
  {
@@ -20592,6 +21014,33 @@ var commandDefinitions = [
20592
21014
  "spira workflows runs list <workflowId>"
20593
21015
  ]
20594
21016
  },
21017
+ {
21018
+ path: ["workflows", "node-catalog"],
21019
+ purpose: "Show the current server workflow node catalog.",
21020
+ options: [
21021
+ {
21022
+ name: "surface",
21023
+ flag: "--surface <surface>",
21024
+ description: "Workflow access surface.",
21025
+ type: "string",
21026
+ enumValues: ["dashboard", "admin"],
21027
+ defaultValue: "dashboard"
21028
+ }
21029
+ ],
21030
+ http: {
21031
+ method: "GET",
21032
+ path: "/api/cli/v1/workflows/node-catalog",
21033
+ authRequired: true,
21034
+ query: [{ name: "surface", source: "option", from: "surface" }]
21035
+ },
21036
+ resultType: "workflowNodeCatalog",
21037
+ examples: ["spira workflows node-catalog --json", "spira workflows node-catalog --surface admin --json"],
21038
+ relatedCommands: [
21039
+ "spira workflows create --name <name>",
21040
+ "spira workflows update <workflowId>",
21041
+ "spira workflows get <workflowId>"
21042
+ ]
21043
+ },
20595
21044
  {
20596
21045
  path: ["workflows", "create"],
20597
21046
  purpose: "Create a canvas workflow.",
@@ -20635,7 +21084,11 @@ var commandDefinitions = [
20635
21084
  'spira workflows create --name "Product launch workflow"',
20636
21085
  `spira workflows create --name "Product launch workflow" --workflow-definition-json '{"nodes":[],"edges":[]}'`
20637
21086
  ],
20638
- relatedCommands: ["spira workflows get <workflowId>", "spira workflows update <workflowId>", "spira workflows runs start <workflowId>"]
21087
+ relatedCommands: [
21088
+ "spira workflows get <workflowId>",
21089
+ "spira workflows update <workflowId>",
21090
+ "spira workflows runs start <workflowId>"
21091
+ ]
20639
21092
  },
20640
21093
  {
20641
21094
  path: ["workflows", "get"],
@@ -20773,7 +21226,12 @@ var commandDefinitions = [
20773
21226
  purpose: "Start a workflow run and return its run ID.",
20774
21227
  arguments: [{ name: "workflowId", description: "Workflow ID.", required: true }],
20775
21228
  options: [
20776
- { name: "startOptionsJson", flag: "--start-options-json <json>", description: "Workflow start options JSON.", type: "json" },
21229
+ {
21230
+ name: "startOptionsJson",
21231
+ flag: "--start-options-json <json>",
21232
+ description: "Workflow start options JSON.",
21233
+ type: "json"
21234
+ },
20777
21235
  {
20778
21236
  name: "surface",
20779
21237
  flag: "--surface <surface>",
@@ -20832,7 +21290,9 @@ var commandDefinitions = [
20832
21290
  path: ["schedules", "executions"],
20833
21291
  purpose: "List persisted schedule execution attempts.",
20834
21292
  arguments: [{ name: "scheduleId", description: "Schedule ID.", required: true }],
20835
- options: [{ name: "limit", flag: "--limit <number>", description: "Maximum executions to return.", type: "number" }],
21293
+ options: [
21294
+ { name: "limit", flag: "--limit <number>", description: "Maximum executions to return.", type: "number" }
21295
+ ],
20836
21296
  http: {
20837
21297
  method: "GET",
20838
21298
  path: "/api/schedules/:scheduleId/executions",
@@ -20848,11 +21308,28 @@ var commandDefinitions = [
20848
21308
  path: ["schedules", "calendar"],
20849
21309
  purpose: "List scheduled occurrences between two times.",
20850
21310
  options: [
20851
- { name: "from", flag: "--from <ms>", description: "Start timestamp in epoch milliseconds.", type: "number", required: true },
20852
- { name: "to", flag: "--to <ms>", description: "End timestamp in epoch milliseconds.", type: "number", required: true },
21311
+ {
21312
+ name: "from",
21313
+ flag: "--from <ms>",
21314
+ description: "Start timestamp in epoch milliseconds.",
21315
+ type: "number",
21316
+ required: true
21317
+ },
21318
+ {
21319
+ name: "to",
21320
+ flag: "--to <ms>",
21321
+ description: "End timestamp in epoch milliseconds.",
21322
+ type: "number",
21323
+ required: true
21324
+ },
20853
21325
  { name: "limit", flag: "--limit <number>", description: "Maximum occurrences to return.", type: "number" },
20854
21326
  { name: "offset", flag: "--offset <number>", description: "Zero-based occurrence offset.", type: "number" },
20855
- { name: "scheduleId", flag: "--schedule-id <scheduleId>", description: "Optional schedule id filter.", type: "string" }
21327
+ {
21328
+ name: "scheduleId",
21329
+ flag: "--schedule-id <scheduleId>",
21330
+ description: "Optional schedule id filter.",
21331
+ type: "string"
21332
+ }
20856
21333
  ],
20857
21334
  http: {
20858
21335
  method: "GET",
@@ -20869,6 +21346,125 @@ var commandDefinitions = [
20869
21346
  resultType: "scheduleOccurrenceList",
20870
21347
  examples: ["spira schedules calendar --from 1781884800000 --to 1782490000000"],
20871
21348
  relatedCommands: ["spira schedules get <scheduleId>"]
21349
+ },
21350
+ {
21351
+ path: ["dataBase", "glob"],
21352
+ purpose: "List files and folders in your Brand database matching a glob pattern.",
21353
+ arguments: [{ name: "pattern", description: "Glob pattern (default: /**).", required: false }],
21354
+ http: {
21355
+ method: "GET",
21356
+ path: "/api/cli/v1/data-base/glob",
21357
+ authRequired: true,
21358
+ query: [{ name: "pattern", source: "argument", from: "pattern" }]
21359
+ },
21360
+ resultType: "dataBaseGlob",
21361
+ examples: ["spira dataBase glob", "spira dataBase glob '/docs/**'", "spira dataBase glob '/**/*.md'"],
21362
+ relatedCommands: ["spira dataBase read <path>", 'spira dataBase search "<query>"']
21363
+ },
21364
+ {
21365
+ path: ["dataBase", "read"],
21366
+ purpose: "Read file content at a Brand database path with optional line/page slicing.",
21367
+ arguments: [{ name: "path", description: "Brand database path (e.g. /docs/style-guide.md).", required: true }],
21368
+ options: [
21369
+ { name: "line", flag: "--line <number>", description: "Start line (1-based).", type: "number" },
21370
+ { name: "lineEnd", flag: "--line-end <number>", description: "End line (inclusive).", type: "number" },
21371
+ { name: "page", flag: "--page <number>", description: "Start page (1-based, PDFs).", type: "number" },
21372
+ { name: "pageEnd", flag: "--page-end <number>", description: "End page (inclusive).", type: "number" },
21373
+ {
21374
+ name: "expand",
21375
+ flag: "--expand <number>",
21376
+ description: "Extra lines of context around the range.",
21377
+ type: "number"
21378
+ }
21379
+ ],
21380
+ http: {
21381
+ method: "GET",
21382
+ path: "/api/cli/v1/data-base/read",
21383
+ authRequired: true,
21384
+ query: [
21385
+ { name: "path", source: "argument", from: "path" },
21386
+ { name: "line", source: "option", from: "line" },
21387
+ { name: "lineEnd", source: "option", from: "lineEnd" },
21388
+ { name: "page", source: "option", from: "page" },
21389
+ { name: "pageEnd", source: "option", from: "pageEnd" },
21390
+ { name: "expand", source: "option", from: "expand" }
21391
+ ]
21392
+ },
21393
+ resultType: "dataBaseRead",
21394
+ examples: [
21395
+ "spira dataBase read /README.md",
21396
+ "spira dataBase read /docs/style-guide.md --line 40 --line-end 120",
21397
+ "spira dataBase read /decks/pitch.pdf --page 2 --page-end 3"
21398
+ ],
21399
+ relatedCommands: ["spira dataBase glob", 'spira dataBase search "<query>"']
21400
+ },
21401
+ {
21402
+ path: ["dataBase", "grep"],
21403
+ purpose: "Lexical (FTS + exact) search across your Brand database. Fast and deterministic.",
21404
+ arguments: [{ name: "query", description: "Search query.", required: true }],
21405
+ options: [
21406
+ { name: "scope", flag: "--scope <path>", description: "Restrict to a folder path.", type: "string" },
21407
+ { name: "limit", flag: "--limit <number>", description: "Max matches (default 20, max 100).", type: "number" },
21408
+ {
21409
+ name: "debug",
21410
+ flag: "--debug",
21411
+ description: "Include candidate scoring breakdown (JSON mode).",
21412
+ type: "boolean"
21413
+ }
21414
+ ],
21415
+ http: {
21416
+ method: "POST",
21417
+ path: "/api/cli/v1/data-base/grep",
21418
+ authRequired: true,
21419
+ body: [
21420
+ { name: "query", source: "argument", from: "query" },
21421
+ { name: "scope", source: "option", from: "scope" },
21422
+ { name: "limit", source: "option", from: "limit" },
21423
+ { name: "debug", source: "option", from: "debug" }
21424
+ ]
21425
+ },
21426
+ resultType: "dataBaseGrep",
21427
+ examples: ['spira dataBase grep "brand voice"', 'spira dataBase grep "tone" --scope /docs --limit 10'],
21428
+ relatedCommands: ["spira dataBase read <path>", 'spira dataBase search "<query>"']
21429
+ },
21430
+ {
21431
+ path: ["dataBase", "search"],
21432
+ purpose: "Semantic search (pgvector + RRF + MMR) across your Brand database.",
21433
+ arguments: [{ name: "query", description: "Natural-language query.", required: true }],
21434
+ options: [
21435
+ { name: "scope", flag: "--scope <path>", description: "Restrict to a folder path.", type: "string" },
21436
+ { name: "limit", flag: "--limit <number>", description: "Max matches (default 20, max 100).", type: "number" },
21437
+ {
21438
+ name: "rerank",
21439
+ flag: "--rerank",
21440
+ description: "Enable LLM reranking (slower, higher quality).",
21441
+ type: "boolean"
21442
+ },
21443
+ {
21444
+ name: "debug",
21445
+ flag: "--debug",
21446
+ description: "Include candidate scoring breakdown (JSON mode).",
21447
+ type: "boolean"
21448
+ }
21449
+ ],
21450
+ http: {
21451
+ method: "POST",
21452
+ path: "/api/cli/v1/data-base/search",
21453
+ authRequired: true,
21454
+ body: [
21455
+ { name: "query", source: "argument", from: "query" },
21456
+ { name: "scope", source: "option", from: "scope" },
21457
+ { name: "limit", source: "option", from: "limit" },
21458
+ { name: "rerank", source: "option", from: "rerank" },
21459
+ { name: "debug", source: "option", from: "debug" }
21460
+ ]
21461
+ },
21462
+ resultType: "dataBaseSearch",
21463
+ examples: [
21464
+ 'spira dataBase search "what is our stance on price anchoring"',
21465
+ 'spira dataBase search "founder biography" --scope /about --rerank'
21466
+ ],
21467
+ relatedCommands: ["spira dataBase read <path>", 'spira dataBase grep "<query>"']
20872
21468
  }
20873
21469
  ];
20874
21470
  function findCommandDefinition(path) {
@@ -20892,6 +21488,13 @@ function validateCommandInput(command, args, options) {
20892
21488
  return validationError(command, option.name, `Expected one of: ${option.enumValues.join(", ")}.`);
20893
21489
  }
20894
21490
  }
21491
+ if (typeof value === "number" && option.minimum !== void 0 && value < option.minimum) {
21492
+ return validationError(
21493
+ command,
21494
+ option.name,
21495
+ `${getOptionDisplayName(option.flag)} must be at least ${option.minimum}.`
21496
+ );
21497
+ }
20895
21498
  }
20896
21499
  return null;
20897
21500
  }
@@ -21445,6 +22048,43 @@ async function logout() {
21445
22048
  });
21446
22049
  }
21447
22050
 
22051
+ // src/render/ai-job-wait.ts
22052
+ function objectRecord(value) {
22053
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
22054
+ }
22055
+ function compactResult(result) {
22056
+ const data = objectRecord(result.data);
22057
+ const execution = objectRecord(data.latestExecution);
22058
+ const status = typeof data.status === "string" ? data.status.toLowerCase() : result.status;
22059
+ const resultUrls = Array.isArray(execution.resultUrls) ? execution.resultUrls.filter((url2) => typeof url2 === "string") : [];
22060
+ return {
22061
+ id: typeof data.id === "string" ? data.id : result.ids.jobId,
22062
+ modelId: typeof data.modelId === "string" ? data.modelId : void 0,
22063
+ status,
22064
+ resultType: typeof execution.resultType === "string" ? execution.resultType : void 0,
22065
+ resultUrls,
22066
+ ...result.error ? {
22067
+ error: {
22068
+ code: result.error.code,
22069
+ message: result.error.message,
22070
+ retryable: result.error.retryable
22071
+ }
22072
+ } : {}
22073
+ };
22074
+ }
22075
+ function renderAiJobWait(result, options) {
22076
+ const compact = compactResult(result);
22077
+ if (options.json) return `${JSON.stringify(compact, null, 2)}
22078
+ `;
22079
+ const lines = [`id: ${compact.id ?? "-"}`, `status: ${compact.status}`];
22080
+ if (compact.modelId) lines.push(`modelId: ${compact.modelId}`);
22081
+ if (compact.resultType) lines.push(`resultType: ${compact.resultType}`);
22082
+ lines.push(...compact.resultUrls);
22083
+ if (compact.error) lines.push(`error: ${compact.error.message}`);
22084
+ return `${lines.join("\n")}
22085
+ `;
22086
+ }
22087
+
21448
22088
  // src/render/human.ts
21449
22089
  function valueToText(value) {
21450
22090
  if (value === null || value === void 0 || value === "") return "-";
@@ -21495,11 +22135,11 @@ function renderKeyValues(data) {
21495
22135
  `;
21496
22136
  return Object.entries(data).filter(([, value]) => typeof value !== "object" || value === null).map(([key, value]) => `${key}: ${valueToText(value)}`).join("\n").concat("\n");
21497
22137
  }
21498
- function objectRecord(data) {
22138
+ function objectRecord2(data) {
21499
22139
  return data && typeof data === "object" ? data : {};
21500
22140
  }
21501
22141
  function formatWhen(value) {
21502
- const record2 = objectRecord(value);
22142
+ const record2 = objectRecord2(value);
21503
22143
  const entries = Object.entries(record2);
21504
22144
  if (!entries.length) return "";
21505
22145
  return entries.map(([key, item]) => `${key}=${valueToText(item)}`).join(", ");
@@ -21512,11 +22152,34 @@ function formatCreditOption(option) {
21512
22152
  const when = formatWhen(option.when ?? option.conditions);
21513
22153
  return ` ${key}: ${label} - ${credits} credits ${unit}${when ? ` (${when})` : ""}`;
21514
22154
  }
22155
+ function portSummary(ports) {
22156
+ if (!Array.isArray(ports) || ports.length === 0) return "-";
22157
+ return ports.map((port) => {
22158
+ const record2 = objectRecord2(port);
22159
+ const suffix = record2.multiple ? "[]" : "";
22160
+ return `${valueToText(record2.id)}:${valueToText(record2.valueType)}${suffix}`;
22161
+ }).join(", ");
22162
+ }
22163
+ function renderWorkflowNodeCatalog(data) {
22164
+ const record2 = objectRecord2(data);
22165
+ const nodes = Array.isArray(record2.nodes) ? record2.nodes : [];
22166
+ const rows = nodes.map((node) => {
22167
+ const nodeRecord = objectRecord2(node);
22168
+ const ports = objectRecord2(nodeRecord.ports);
22169
+ return {
22170
+ type: nodeRecord.type,
22171
+ inputs: portSummary(ports.inputs),
22172
+ outputs: portSummary(ports.outputs),
22173
+ requiredPermission: nodeRecord.requiredPermission
22174
+ };
22175
+ });
22176
+ return renderRows(rows, ["type", "inputs", "outputs", "requiredPermission"]);
22177
+ }
21515
22178
  function renderAccountList(data) {
21516
22179
  const channels = data && typeof data === "object" ? data.publishChannels : null;
21517
22180
  if (Array.isArray(channels)) {
21518
22181
  const rows = channels.map((channel) => {
21519
- const record2 = objectRecord(channel);
22182
+ const record2 = objectRecord2(channel);
21520
22183
  return {
21521
22184
  platform: record2.platform,
21522
22185
  connected: record2.oauthAccountsCount,
@@ -21529,12 +22192,12 @@ function renderAccountList(data) {
21529
22192
  return renderKeyValues(data);
21530
22193
  }
21531
22194
  function renderAccountConnect(data) {
21532
- const record2 = objectRecord(data);
22195
+ const record2 = objectRecord2(data);
21533
22196
  return `platform: ${valueToText(record2.platform)}
21534
22197
  `;
21535
22198
  }
21536
22199
  function renderCreditBalance(data) {
21537
- const record2 = objectRecord(data);
22200
+ const record2 = objectRecord2(data);
21538
22201
  const lines = [
21539
22202
  `availableCredits: ${formatCredits(record2.availableCredits)}`,
21540
22203
  `permanentCredits: ${formatCredits(record2.availablePermanentCredits)}`,
@@ -21561,7 +22224,7 @@ function renderUser(prefix, value) {
21561
22224
  function renderAuthWhoami(data) {
21562
22225
  if (!data || typeof data !== "object") return renderKeyValues(data);
21563
22226
  const record2 = data;
21564
- const effective = objectRecord(record2.effectiveUser);
22227
+ const effective = objectRecord2(record2.effectiveUser);
21565
22228
  const name = valueToText(effective.name);
21566
22229
  const email3 = valueToText(effective.email);
21567
22230
  const lines = [`user: ${name}${email3 === "-" ? "" : ` <${email3}>`}`];
@@ -21576,8 +22239,8 @@ function renderAuthLogin(data) {
21576
22239
  `;
21577
22240
  }
21578
22241
  function renderSocialSubmissionDetail(data) {
21579
- const raw = objectRecord(data);
21580
- const nested = objectRecord(raw.data);
22242
+ const raw = objectRecord2(data);
22243
+ const nested = objectRecord2(raw.data);
21581
22244
  const record2 = nested.submission_id || nested.postUrl || nested.tweetId ? {
21582
22245
  id: nested.submission_id,
21583
22246
  platform: raw.platform,
@@ -21611,8 +22274,8 @@ function renderSocialSubmissionDetail(data) {
21611
22274
  `;
21612
22275
  }
21613
22276
  function renderAiJobStatus(data) {
21614
- const record2 = objectRecord(data);
21615
- const latestExecution = objectRecord(record2.latestExecution);
22277
+ const record2 = objectRecord2(data);
22278
+ const latestExecution = objectRecord2(record2.latestExecution);
21616
22279
  const resultUrls = Array.isArray(latestExecution.resultUrls) ? latestExecution.resultUrls : [];
21617
22280
  const lines = [
21618
22281
  `id: ${valueToText(record2.id)}`,
@@ -21631,10 +22294,10 @@ function renderAiModelList(data) {
21631
22294
  return renderRows(dataArray(data), ["id", "mediaType", "creditOptions", "name", "capabilities"]);
21632
22295
  }
21633
22296
  function renderAiModelDetail(data) {
21634
- const record2 = objectRecord(data);
21635
- const fields = Array.isArray(record2.fields) ? record2.fields.map(objectRecord) : [];
21636
- const creditOptions = Array.isArray(record2.creditOptions) ? record2.creditOptions.map(objectRecord) : Array.isArray(record2.variants) ? record2.variants.map(objectRecord) : [];
21637
- const examples = Array.isArray(record2.examples) ? record2.examples.map(objectRecord) : [];
22297
+ const record2 = objectRecord2(data);
22298
+ const fields = Array.isArray(record2.fields) ? record2.fields.map(objectRecord2) : [];
22299
+ const creditOptions = Array.isArray(record2.creditOptions) ? record2.creditOptions.map(objectRecord2) : Array.isArray(record2.variants) ? record2.variants.map(objectRecord2) : [];
22300
+ const examples = Array.isArray(record2.examples) ? record2.examples.map(objectRecord2) : [];
21638
22301
  const notes = Array.isArray(record2.notes) ? record2.notes : [];
21639
22302
  const lines = [
21640
22303
  `id: ${valueToText(record2.id)}`,
@@ -21674,8 +22337,8 @@ function renderAiModelDetail(data) {
21674
22337
  `;
21675
22338
  }
21676
22339
  function renderWorkflowDetail(data) {
21677
- const record2 = objectRecord(data);
21678
- const definition = objectRecord(record2.workflowDefinition);
22340
+ const record2 = objectRecord2(data);
22341
+ const definition = objectRecord2(record2.workflowDefinition);
21679
22342
  const nodes = Array.isArray(definition.nodes) ? definition.nodes : [];
21680
22343
  const edges = Array.isArray(definition.edges) ? definition.edges : [];
21681
22344
  return [
@@ -21688,9 +22351,9 @@ function renderWorkflowDetail(data) {
21688
22351
  ].join("\n").concat("\n");
21689
22352
  }
21690
22353
  function renderScheduleDetail(data) {
21691
- const record2 = objectRecord(data);
21692
- const executorConfig = objectRecord(record2.executorConfig);
21693
- const currentPattern = objectRecord(record2.currentPattern);
22354
+ const record2 = objectRecord2(data);
22355
+ const executorConfig = objectRecord2(record2.executorConfig);
22356
+ const currentPattern = objectRecord2(record2.currentPattern);
21694
22357
  return [
21695
22358
  `id: ${valueToText(record2.id)}`,
21696
22359
  `executorType: ${valueToText(record2.executorType)}`,
@@ -21703,6 +22366,112 @@ function renderScheduleDetail(data) {
21703
22366
  `scheduledAt: ${valueToText(currentPattern.scheduledAt)}`
21704
22367
  ].join("\n").concat("\n");
21705
22368
  }
22369
+ function formatByteSize(value) {
22370
+ const n = typeof value === "number" && Number.isFinite(value) ? value : NaN;
22371
+ if (!Number.isFinite(n) || n < 0) return "-";
22372
+ if (n < 1024) return `${n} B`;
22373
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(n < 10 * 1024 ? 1 : 0)} KB`;
22374
+ if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)} MB`;
22375
+ return `${(n / (1024 * 1024 * 1024)).toFixed(2)} GB`;
22376
+ }
22377
+ function formatAnchor(anchor) {
22378
+ const parts = [];
22379
+ const line = anchor.line;
22380
+ const lineEnd = anchor.lineEnd;
22381
+ const page = anchor.page;
22382
+ const pageEnd = anchor.pageEnd;
22383
+ const timeStart = anchor.timeStartMs;
22384
+ const timeEnd = anchor.timeEndMs;
22385
+ if (typeof line === "number") {
22386
+ parts.push(typeof lineEnd === "number" ? `L${line}-${lineEnd}` : `L${line}`);
22387
+ }
22388
+ if (typeof page === "number") {
22389
+ parts.push(typeof pageEnd === "number" ? `p${page}-${pageEnd}` : `p${page}`);
22390
+ }
22391
+ if (typeof timeStart === "number") {
22392
+ parts.push(typeof timeEnd === "number" ? `t${timeStart}-${timeEnd}ms` : `t${timeStart}ms`);
22393
+ }
22394
+ return parts.join(", ") || "-";
22395
+ }
22396
+ function renderDataBaseGlob(data) {
22397
+ const record2 = objectRecord2(data);
22398
+ const pattern = valueToText(record2.pattern);
22399
+ const entries = Array.isArray(record2.entries) ? record2.entries.map(objectRecord2) : [];
22400
+ if (entries.length === 0) return `pattern: ${pattern}
22401
+ No entries.
22402
+ `;
22403
+ const rows = entries.map((entry) => {
22404
+ const isFolder = entry.type === "folder";
22405
+ const isVirtual = entry.virtual === true;
22406
+ return {
22407
+ type: isFolder ? "dir" : isVirtual ? "file*" : "file",
22408
+ path: entry.path,
22409
+ fileType: isFolder ? "-" : valueToText(entry.fileType),
22410
+ size: isFolder ? `${valueToText(entry.fileCount)} files` : formatByteSize(entry.size),
22411
+ status: isFolder ? "-" : valueToText(entry.processingStatus),
22412
+ updatedAt: entry.updatedAt
22413
+ };
22414
+ });
22415
+ return `pattern: ${pattern} (${entries.length} entries)
22416
+ ${renderRows(rows, ["type", "path", "fileType", "size", "status", "updatedAt"])}`;
22417
+ }
22418
+ function renderDataBaseRead(data) {
22419
+ const record2 = objectRecord2(data);
22420
+ const path = valueToText(record2.path);
22421
+ const fileType = valueToText(record2.fileType);
22422
+ const mimeType = valueToText(record2.mimeType);
22423
+ const rangeObj = objectRecord2(record2.range);
22424
+ let rangeStr = "-";
22425
+ if (Array.isArray(rangeObj.line) && rangeObj.line.length === 2) {
22426
+ rangeStr = `lines ${rangeObj.line[0]}-${rangeObj.line[1]}`;
22427
+ } else if (Array.isArray(rangeObj.page) && rangeObj.page.length === 2) {
22428
+ rangeStr = `pages ${rangeObj.page[0]}-${rangeObj.page[1]}`;
22429
+ }
22430
+ const totalLines = record2.totalLines !== void 0 ? valueToText(record2.totalLines) : "-";
22431
+ const truncated = valueToText(record2.truncated);
22432
+ const lines = [
22433
+ `path: ${path}`,
22434
+ `fileType: ${fileType}`,
22435
+ `mimeType: ${mimeType}`,
22436
+ `range: ${rangeStr}`,
22437
+ `totalLines: ${totalLines}`,
22438
+ `truncated: ${truncated}`
22439
+ ];
22440
+ if (record2.virtual === true) lines.push("virtual: true");
22441
+ if (record2.processingStatus) lines.push(`processingStatus: ${valueToText(record2.processingStatus)}`);
22442
+ if (record2.storageUrl) lines.push(`storageUrl: ${valueToText(record2.storageUrl)}`);
22443
+ const content = typeof record2.content === "string" ? record2.content : "";
22444
+ const header = `${lines.join("\n")}
22445
+ `;
22446
+ const nextLine = typeof record2.nextLine === "number" ? record2.nextLine : void 0;
22447
+ const footer = record2.truncated === true && nextLine ? `
22448
+ --
22449
+ (truncated; continue with: spira dataBase read ${path} --line ${nextLine})
22450
+ ` : "";
22451
+ return `${header}
22452
+ ${content}${footer}`;
22453
+ }
22454
+ function renderDataBaseMatches(data) {
22455
+ const record2 = objectRecord2(data);
22456
+ const query = valueToText(record2.query);
22457
+ const matches = Array.isArray(record2.matches) ? record2.matches.map(objectRecord2) : [];
22458
+ if (matches.length === 0) return `query: ${query}
22459
+ No matches.
22460
+ `;
22461
+ const blocks = matches.map((match) => {
22462
+ const anchor = formatAnchor(objectRecord2(match.anchor));
22463
+ const score = typeof match.score === "number" ? match.score.toFixed(3) : valueToText(match.score);
22464
+ const fields = Array.isArray(match.matchedFields) ? match.matchedFields.join(",") : "";
22465
+ const header = `${valueToText(match.path)}(${anchor}) [score=${score}${fields ? ` fields=${fields}` : ""}]`;
22466
+ const snippet = typeof match.snippet === "string" ? match.snippet : valueToText(match.snippet);
22467
+ const indented = snippet.split("\n").map((line) => ` ${line}`).join("\n");
22468
+ return `${header}
22469
+ ${indented}`;
22470
+ });
22471
+ return `query: ${query} (${matches.length} matches)
22472
+ ${blocks.join("\n\n")}
22473
+ `;
22474
+ }
21706
22475
  function renderSuccess(result) {
21707
22476
  switch (result.resultType) {
21708
22477
  case "authLogin":
@@ -21720,7 +22489,7 @@ function renderSuccess(result) {
21720
22489
  case "socialSubmissionList":
21721
22490
  return renderRows(
21722
22491
  dataArray(result.data).map((item) => {
21723
- const record2 = objectRecord(item);
22492
+ const record2 = objectRecord2(item);
21724
22493
  return {
21725
22494
  id: record2.id,
21726
22495
  platform: record2.platform,
@@ -21745,6 +22514,8 @@ function renderSuccess(result) {
21745
22514
  return renderAiModelDetail(result.data);
21746
22515
  case "workflowList":
21747
22516
  return renderRows(dataArray(result.data), ["id", "name", "creatorName", "nodeTypes", "updatedAt"]);
22517
+ case "workflowNodeCatalog":
22518
+ return renderWorkflowNodeCatalog(result.data);
21748
22519
  case "workflowDetail":
21749
22520
  return renderWorkflowDetail(result.data);
21750
22521
  case "workflowRunList":
@@ -21757,6 +22528,13 @@ function renderSuccess(result) {
21757
22528
  return renderRows(dataArray(result.data), ["id", "scheduleId", "scheduledAt", "status", "executorType"]);
21758
22529
  case "scheduleExecutionList":
21759
22530
  return renderRows(dataArray(result.data), ["id", "scheduleId", "status", "originalScheduledAt", "startedAt", "finishedAt"]);
22531
+ case "dataBaseGlob":
22532
+ return renderDataBaseGlob(result.data);
22533
+ case "dataBaseRead":
22534
+ return renderDataBaseRead(result.data);
22535
+ case "dataBaseGrep":
22536
+ case "dataBaseSearch":
22537
+ return renderDataBaseMatches(result.data);
21760
22538
  default:
21761
22539
  return renderKeyValues(result.data);
21762
22540
  }
@@ -21808,6 +22586,7 @@ function renderJson(result) {
21808
22586
 
21809
22587
  // src/render/index.ts
21810
22588
  function renderResult(result, options) {
22589
+ if (options.compactAiJobWait) return renderAiJobWait(result, options);
21811
22590
  return options.json ? renderJson(result) : renderHuman(result);
21812
22591
  }
21813
22592
 
@@ -21960,6 +22739,24 @@ function parseNumber(value) {
21960
22739
  if (!Number.isFinite(parsed)) throw new InvalidArgumentError("Expected a number.");
21961
22740
  return parsed;
21962
22741
  }
22742
+ function parseDuration(value) {
22743
+ const match = /^(\d+(?:\.\d+)?)(ms|s|m|h)$/.exec(value.trim());
22744
+ if (!match) throw new InvalidArgumentError("Expected a duration such as 500ms, 3s, 10m, or 1h.");
22745
+ const amount = Number(match[1]);
22746
+ const unit = match[2];
22747
+ const multiplier = unit === "ms" ? 1 : unit === "s" ? 1e3 : unit === "m" ? 6e4 : 36e5;
22748
+ const durationMs = amount * multiplier;
22749
+ if (!Number.isSafeInteger(durationMs) || durationMs <= 0) {
22750
+ throw new InvalidArgumentError("Expected a positive duration.");
22751
+ }
22752
+ return durationMs;
22753
+ }
22754
+ function formatDuration(durationMs) {
22755
+ if (durationMs % 36e5 === 0) return `${durationMs / 36e5}h`;
22756
+ if (durationMs % 6e4 === 0) return `${durationMs / 6e4}m`;
22757
+ if (durationMs % 1e3 === 0) return `${durationMs / 1e3}s`;
22758
+ return `${durationMs}ms`;
22759
+ }
21963
22760
  function parseJson(value) {
21964
22761
  try {
21965
22762
  return JSON.parse(value);
@@ -21978,6 +22775,11 @@ function addOption(command, definition) {
21978
22775
  command.addOption(option.argParser(parseNumber).default(definition.defaultValue));
21979
22776
  return;
21980
22777
  }
22778
+ if (definition.type === "duration") {
22779
+ const defaultDescription = typeof definition.defaultValue === "number" ? formatDuration(definition.defaultValue) : void 0;
22780
+ command.addOption(option.argParser(parseDuration).default(definition.defaultValue, defaultDescription));
22781
+ return;
22782
+ }
21981
22783
  if (definition.type === "json") {
21982
22784
  command.addOption(option.argParser(parseJson).default(definition.defaultValue));
21983
22785
  return;
@@ -22034,10 +22836,7 @@ function getOrCreateSubcommand(parent, name) {
22034
22836
  return next;
22035
22837
  }
22036
22838
  function renderCommandReference(definition) {
22037
- const lines = [
22038
- `Command: spira ${definition.path.join(" ")}`,
22039
- `Description: ${definition.purpose}`
22040
- ];
22839
+ const lines = [`Command: spira ${definition.path.join(" ")}`, `Description: ${definition.purpose}`];
22041
22840
  if (definition.arguments?.length) {
22042
22841
  lines.push("Arguments:");
22043
22842
  for (const argument of definition.arguments) {
@@ -22066,8 +22865,8 @@ function renderCommandIndex() {
22066
22865
  `;
22067
22866
  }
22068
22867
  async function packageVersion() {
22069
- if ("0.0.4".length > 0) {
22070
- return "0.0.4";
22868
+ if ("0.0.6".length > 0) {
22869
+ return "0.0.6";
22071
22870
  }
22072
22871
  try {
22073
22872
  const packageUrl = new URL("../package.json", import_meta.url);
@@ -22240,7 +23039,38 @@ ${url2}
22240
23039
  resultType: "aiModelDetail",
22241
23040
  command: "spira ai models get",
22242
23041
  data: publicAiModelDoc(model),
22243
- nextActions: [{ label: "Create job", command: `spira ai jobs create --model-id ${model.id} --inputs-json '<json>'` }]
23042
+ nextActions: [
23043
+ { label: "Create job", command: `spira ai jobs create --model-id ${model.id} --inputs-json '<json>'` }
23044
+ ]
23045
+ });
23046
+ }
23047
+ if (commandKey(definition.path) === "ai jobs create") {
23048
+ const created = await executeHttpCommand({ command: definition, args, options });
23049
+ if (options.wait !== true || created.status === "error") return created;
23050
+ const jobId = created.ids.jobId;
23051
+ if (!jobId) {
23052
+ return cliResultSchema.parse({
23053
+ ...created,
23054
+ status: "error",
23055
+ error: {
23056
+ code: "CONTRACT_MISMATCH",
23057
+ message: "AI job creation response did not contain a jobId.",
23058
+ retryable: false,
23059
+ nextActions: []
23060
+ }
23061
+ });
23062
+ }
23063
+ const statusCommand = findCommandDefinition(["ai", "jobs", "status"]);
23064
+ if (!statusCommand) throw new Error("AI job status command is not registered.");
23065
+ output.error(`AI job ${jobId}: SUBMITTED
23066
+ `);
23067
+ return waitForAiJob({
23068
+ jobId,
23069
+ timeoutMs: options.waitTimeout,
23070
+ intervalMs: options.waitInterval,
23071
+ onStatus: (status) => output.error(`AI job ${jobId}: ${status}
23072
+ `),
23073
+ poll: () => executeHttpCommand({ command: statusCommand, args: { jobId }, options: {} })
22244
23074
  });
22245
23075
  }
22246
23076
  return executeHttpCommand({ command: definition, args, options });
@@ -22258,12 +23088,19 @@ function addLeafCommand(root, definition, output) {
22258
23088
  for (const option of definition.options ?? []) addOption(leaf, option);
22259
23089
  leaf.action(async (...values) => {
22260
23090
  const command = values[values.length - 1];
22261
- const positional = values.slice(0, -1).map((value) => String(value));
22262
- const args = Object.fromEntries((definition.arguments ?? []).map((argument, index) => [argument.name, positional[index] ?? ""]));
23091
+ const positional = values.slice(0, -1).map((value) => value === void 0 ? "" : String(value));
23092
+ const args = Object.fromEntries(
23093
+ (definition.arguments ?? []).map((argument, index) => [argument.name, positional[index] ?? ""])
23094
+ );
22263
23095
  const options = command.optsWithGlobals();
22264
23096
  const validationResult2 = validateCommandInput(definition, args, options);
22265
23097
  const result = validationResult2 ?? await executeDefinition(definition, args, options, output);
22266
- output.write(renderResult(result, { json: Boolean(options.json) }));
23098
+ output.write(
23099
+ renderResult(result, {
23100
+ json: Boolean(options.json),
23101
+ compactAiJobWait: commandKey(definition.path) === "ai jobs create" && options.wait === true
23102
+ })
23103
+ );
22267
23104
  if (result.status === "error") process.exitCode = 1;
22268
23105
  });
22269
23106
  parent.addCommand(leaf);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spira-lab/cli",
3
- "version": "0.0.4",
3
+ "version": "0.0.6",
4
4
  "description": "Command line client for Spira.",
5
5
  "type": "module",
6
6
  "bin": {