@arbidocs/cli 0.4.75 → 0.4.77

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
@@ -12,6 +12,7 @@ var child_process = require('child_process');
12
12
  var client = require('@arbidocs/client');
13
13
  var url = require('url');
14
14
  var crypto = require('crypto');
15
+ var yaml = require('yaml');
15
16
  var module$1 = require('module');
16
17
 
17
18
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -3694,7 +3695,7 @@ function getLatestVersion(skipCache = false) {
3694
3695
  }
3695
3696
  }
3696
3697
  function getCurrentVersion() {
3697
- return "0.4.75";
3698
+ return "0.4.77";
3698
3699
  }
3699
3700
  function readChangelog(fromVersion, toVersion) {
3700
3701
  try {
@@ -3747,17 +3748,17 @@ function showChangelog(fromVersion, toVersion) {
3747
3748
  async function checkForUpdates(autoUpdate) {
3748
3749
  try {
3749
3750
  const latest = getLatestVersion();
3750
- if (!latest || latest === "0.4.75") return;
3751
+ if (!latest || latest === "0.4.77") return;
3751
3752
  if (autoUpdate) {
3752
3753
  warn(`
3753
- Your arbi version is out of date (${"0.4.75"} \u2192 ${latest}). Updating...`);
3754
+ Your arbi version is out of date (${"0.4.77"} \u2192 ${latest}). Updating...`);
3754
3755
  child_process.execSync("npm install -g @arbidocs/cli@latest", { stdio: "inherit" });
3755
- showChangelog("0.4.75", latest);
3756
+ showChangelog("0.4.77", latest);
3756
3757
  console.log(`Updated to ${latest}.`);
3757
3758
  } else {
3758
3759
  warn(
3759
3760
  `
3760
- Your arbi version is out of date (${"0.4.75"} \u2192 ${latest}).
3761
+ Your arbi version is out of date (${"0.4.77"} \u2192 ${latest}).
3761
3762
  Run "arbi update" to upgrade, or "arbi update auto" to always stay up to date.`
3762
3763
  );
3763
3764
  }
@@ -3789,10 +3790,10 @@ function markNagShown(latest) {
3789
3790
  function hintUpdateOnError() {
3790
3791
  try {
3791
3792
  const cached = readCache();
3792
- if (!cached || cached.latest === "0.4.75") return;
3793
+ if (!cached || cached.latest === "0.4.77") return;
3793
3794
  if (!shouldShowNag(cached.latest)) return;
3794
3795
  warn(
3795
- `Your arbi version is out of date (${"0.4.75"} \u2192 ${cached.latest}). Run "arbi update".`
3796
+ `Your arbi version is out of date (${"0.4.77"} \u2192 ${cached.latest}). Run "arbi update".`
3796
3797
  );
3797
3798
  markNagShown(cached.latest);
3798
3799
  } catch {
@@ -3963,6 +3964,10 @@ function stringifyApiDetail(value) {
3963
3964
  const rec = value;
3964
3965
  if (typeof rec.detail === "string") return rec.detail;
3965
3966
  if (Array.isArray(rec.detail)) return stringifyApiDetail(rec.detail);
3967
+ if (rec.fields && typeof rec.fields === "object" && !Array.isArray(rec.fields)) {
3968
+ const fields = Object.entries(rec.fields).map(([name, problem]) => `${name}: ${String(problem)}`).join("; ");
3969
+ return typeof rec.message === "string" ? `${rec.message} (${fields})` : fields;
3970
+ }
3966
3971
  if (typeof rec.message === "string") return rec.message;
3967
3972
  if (typeof rec.error === "string") return rec.error;
3968
3973
  try {
@@ -6322,10 +6327,10 @@ function registerDocsCommand(program2) {
6322
6327
  if (!opts.quiet && statusIntervalMs > 0) {
6323
6328
  statusTimer = setInterval(() => printStatus(" "), statusIntervalMs);
6324
6329
  }
6325
- const sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
6330
+ const sleep2 = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
6326
6331
  const backoff = async (attempt) => {
6327
6332
  const delayMs = Math.min(1e3 * Math.pow(2, attempt), 3e4);
6328
- await sleep(delayMs);
6333
+ await sleep2(delayMs);
6329
6334
  };
6330
6335
  const submitBatch = async (batchIds) => {
6331
6336
  const updates = batchIds.map((id) => ({
@@ -6437,12 +6442,12 @@ function registerDocsCommand(program2) {
6437
6442
  }
6438
6443
  submitted += 1;
6439
6444
  if (intervalMs > 0 && j < batch.length - 1) {
6440
- await sleep(intervalMs);
6445
+ await sleep2(intervalMs);
6441
6446
  }
6442
6447
  }
6443
6448
  }
6444
6449
  if (intervalMs > 0 && i + batchSize < docIds.length) {
6445
- await sleep(intervalMs);
6450
+ await sleep2(intervalMs);
6446
6451
  }
6447
6452
  }
6448
6453
  } finally {
@@ -6520,19 +6525,19 @@ Processing complete: completed=${procCompleted} failed=0 elapsed=${totalElapsed
6520
6525
  }
6521
6526
 
6522
6527
  // src/commands/upload-shared.ts
6523
- function printSummary(summary) {
6528
+ function printSummary(summary2) {
6524
6529
  console.log("");
6525
6530
  console.log("Upload summary:");
6526
- console.log(` Files found: ${summary.totalFiles}`);
6527
- success(` Uploaded: ${summary.uploaded}`);
6528
- if (summary.skipped > 0) {
6529
- warn(` Skipped: ${summary.skipped}`);
6531
+ console.log(` Files found: ${summary2.totalFiles}`);
6532
+ success(` Uploaded: ${summary2.uploaded}`);
6533
+ if (summary2.skipped > 0) {
6534
+ warn(` Skipped: ${summary2.skipped}`);
6530
6535
  }
6531
- console.log(` Folders: ${summary.folders}`);
6532
- if (summary.skippedDetails.length > 0) {
6536
+ console.log(` Folders: ${summary2.folders}`);
6537
+ if (summary2.skippedDetails.length > 0) {
6533
6538
  console.log("");
6534
6539
  warn("Skipped files:");
6535
- for (const s of summary.skippedDetails) {
6540
+ for (const s of summary2.skippedDetails) {
6536
6541
  warn(` ${s.file_name}: ${s.reason}`);
6537
6542
  }
6538
6543
  }
@@ -6550,10 +6555,10 @@ function countSupportedFiles(dirPath) {
6550
6555
  }
6551
6556
  return count;
6552
6557
  }
6553
- function mergeBatchResult(summary, uploadedDocs, result, sourceLabel, opts) {
6558
+ function mergeBatchResult(summary2, uploadedDocs, result, sourceLabel, opts) {
6554
6559
  for (const [folder, info] of result.folders) {
6555
- summary.folders += 1;
6556
- summary.totalFiles += info.fileCount;
6560
+ summary2.folders += 1;
6561
+ summary2.totalFiles += info.fileCount;
6557
6562
  if (!opts.json) {
6558
6563
  success(
6559
6564
  ` ${folder || "<root>"}: ${info.fileCount} file(s) \u2192 ${info.doc_ext_ids.length} uploaded`
@@ -6564,10 +6569,10 @@ function mergeBatchResult(summary, uploadedDocs, result, sourceLabel, opts) {
6564
6569
  }
6565
6570
  for (const docId of info.doc_ext_ids) uploadedDocs.set(docId, sourceLabel);
6566
6571
  }
6567
- summary.uploaded += result.doc_ext_ids.length;
6568
- summary.skipped += result.skipped.length;
6569
- summary.docIds.push(...result.doc_ext_ids);
6570
- summary.skippedDetails.push(...result.skipped);
6572
+ summary2.uploaded += result.doc_ext_ids.length;
6573
+ summary2.skipped += result.skipped.length;
6574
+ summary2.docIds.push(...result.doc_ext_ids);
6575
+ summary2.skippedDetails.push(...result.skipped);
6571
6576
  }
6572
6577
  function classifyDirectInputs(inputPaths) {
6573
6578
  const out = [];
@@ -6632,7 +6637,7 @@ async function runDirectUploadMode(inputPaths, opts) {
6632
6637
  });
6633
6638
  const creds = store.requireCredentials();
6634
6639
  const workspaceKey = await sdk.getRawWorkspaceKey(arbi, workspaceId, creds.signingPrivateKeyBase64);
6635
- const summary = {
6640
+ const summary2 = {
6636
6641
  totalFiles: 0,
6637
6642
  uploaded: 0,
6638
6643
  skipped: 0,
@@ -6669,7 +6674,7 @@ async function runDirectUploadMode(inputPaths, opts) {
6669
6674
  warn(`No supported files found in directory: ${inp.originalPath}`);
6670
6675
  continue;
6671
6676
  }
6672
- mergeBatchResult(summary, uploadedDocs, result2, inp.originalPath, opts);
6677
+ mergeBatchResult(summary2, uploadedDocs, result2, inp.originalPath, opts);
6673
6678
  if (!opts.json) {
6674
6679
  success(
6675
6680
  `Uploaded directory: ${inp.originalPath} (${result2.doc_ext_ids.length} document(s) total)`
@@ -6688,7 +6693,7 @@ async function runDirectUploadMode(inputPaths, opts) {
6688
6693
  warn(`No supported files found in zip: ${inp.originalPath}`);
6689
6694
  continue;
6690
6695
  }
6691
- mergeBatchResult(summary, uploadedDocs, result2, inp.originalPath, opts);
6696
+ mergeBatchResult(summary2, uploadedDocs, result2, inp.originalPath, opts);
6692
6697
  if (!opts.json) {
6693
6698
  success(
6694
6699
  `Uploaded zip: ${inp.originalPath} (${result2.doc_ext_ids.length} document(s) total)`
@@ -6706,7 +6711,7 @@ async function runDirectUploadMode(inputPaths, opts) {
6706
6711
  warn(`No supported files found in archive: ${inp.originalPath}`);
6707
6712
  continue;
6708
6713
  }
6709
- mergeBatchResult(summary, uploadedDocs, result, inp.originalPath, opts);
6714
+ mergeBatchResult(summary2, uploadedDocs, result, inp.originalPath, opts);
6710
6715
  if (!opts.json) {
6711
6716
  success(
6712
6717
  `Uploaded archive: ${inp.originalPath} (${result.doc_ext_ids.length} document(s) total)`
@@ -6723,11 +6728,11 @@ async function runDirectUploadMode(inputPaths, opts) {
6723
6728
  folder: opts.folder,
6724
6729
  configExtId: opts.config
6725
6730
  });
6726
- summary.totalFiles += plainFiles.length;
6727
- summary.uploaded += result.doc_ext_ids.length;
6728
- summary.skipped += result.skipped.length;
6729
- summary.docIds.push(...result.doc_ext_ids);
6730
- summary.skippedDetails.push(...result.skipped);
6731
+ summary2.totalFiles += plainFiles.length;
6732
+ summary2.uploaded += result.doc_ext_ids.length;
6733
+ summary2.skipped += result.skipped.length;
6734
+ summary2.docIds.push(...result.doc_ext_ids);
6735
+ summary2.skippedDetails.push(...result.skipped);
6731
6736
  for (let i = 0; i < result.doc_ext_ids.length && i < plainFiles.length; i++) {
6732
6737
  uploadedDocs.set(result.doc_ext_ids[i], path6__default.default.basename(plainFiles[i]));
6733
6738
  }
@@ -6738,9 +6743,9 @@ async function runDirectUploadMode(inputPaths, opts) {
6738
6743
  }
6739
6744
  }
6740
6745
  if (opts.json) {
6741
- console.log(JSON.stringify(summary));
6746
+ console.log(JSON.stringify(summary2));
6742
6747
  } else {
6743
- printSummary(summary);
6748
+ printSummary(summary2);
6744
6749
  }
6745
6750
  if (willWatch && uploadedDocs.size > 0) {
6746
6751
  const pending = new Set(uploadedDocs.keys());
@@ -6849,7 +6854,7 @@ function registerUploadCommand(program2) {
6849
6854
  });
6850
6855
  const uploadedDocs = /* @__PURE__ */ new Map();
6851
6856
  const auth = { baseUrl: config.baseUrl, accessToken };
6852
- const summary = {
6857
+ const summary2 = {
6853
6858
  totalFiles: 0,
6854
6859
  uploaded: 0,
6855
6860
  skipped: 0,
@@ -6883,7 +6888,7 @@ function registerUploadCommand(program2) {
6883
6888
  if (opts.dryRun) {
6884
6889
  const count = countSupportedFiles2(filePath);
6885
6890
  console.log(`${filePath}: ${count} supported file(s) would be uploaded`);
6886
- summary.totalFiles += count;
6891
+ summary2.totalFiles += count;
6887
6892
  continue;
6888
6893
  }
6889
6894
  const result = await sdk.documentsNode.uploadDirectory(auth, filePath, progressCallbacks);
@@ -6906,7 +6911,7 @@ function registerUploadCommand(program2) {
6906
6911
  `Uploaded directory: ${filePath} (${result.doc_ext_ids.length} document(s) total)`
6907
6912
  );
6908
6913
  }
6909
- updateSummary(summary, result);
6914
+ updateSummary(summary2, result);
6910
6915
  for (const id of result.doc_ext_ids) uploadedDocs.set(id, filePath);
6911
6916
  } else if (filePath.toLowerCase().endsWith(".zip")) {
6912
6917
  if (opts.dryRun) {
@@ -6931,7 +6936,7 @@ function registerUploadCommand(program2) {
6931
6936
  }
6932
6937
  success(`Uploaded zip: ${filePath} (${result.doc_ext_ids.length} document(s) total)`);
6933
6938
  }
6934
- updateSummary(summary, result);
6939
+ updateSummary(summary2, result);
6935
6940
  for (const id of result.doc_ext_ids) uploadedDocs.set(id, filePath);
6936
6941
  } else if (sdk.documentsNode.ARCHIVE_EXTENSIONS.has(
6937
6942
  filePath.slice(filePath.lastIndexOf(".")).toLowerCase()
@@ -6960,12 +6965,12 @@ function registerUploadCommand(program2) {
6960
6965
  `Uploaded archive: ${filePath} (${result.doc_ext_ids.length} document(s) total)`
6961
6966
  );
6962
6967
  }
6963
- updateSummary(summary, result);
6968
+ updateSummary(summary2, result);
6964
6969
  for (const id of result.doc_ext_ids) uploadedDocs.set(id, filePath);
6965
6970
  } else {
6966
6971
  if (opts.dryRun) {
6967
6972
  console.log(`${filePath}: single file would be uploaded`);
6968
- summary.totalFiles += 1;
6973
+ summary2.totalFiles += 1;
6969
6974
  continue;
6970
6975
  }
6971
6976
  const result = await sdk.documentsNode.uploadLocalFile(auth, filePath, {
@@ -6981,12 +6986,12 @@ function registerUploadCommand(program2) {
6981
6986
  );
6982
6987
  }
6983
6988
  }
6984
- summary.totalFiles += 1;
6985
- summary.uploaded += result.doc_ext_ids?.length ?? 0;
6986
- summary.docIds.push(...result.doc_ext_ids ?? []);
6989
+ summary2.totalFiles += 1;
6990
+ summary2.uploaded += result.doc_ext_ids?.length ?? 0;
6991
+ summary2.docIds.push(...result.doc_ext_ids ?? []);
6987
6992
  if (result.skipped) {
6988
- summary.skipped += result.skipped.length;
6989
- summary.skippedDetails.push(...result.skipped);
6993
+ summary2.skipped += result.skipped.length;
6994
+ summary2.skippedDetails.push(...result.skipped);
6990
6995
  }
6991
6996
  for (const id of result.doc_ext_ids ?? []) uploadedDocs.set(id, result.fileName);
6992
6997
  }
@@ -6995,9 +7000,9 @@ function registerUploadCommand(program2) {
6995
7000
  return;
6996
7001
  }
6997
7002
  if (opts.json) {
6998
- console.log(JSON.stringify(summary));
7003
+ console.log(JSON.stringify(summary2));
6999
7004
  } else {
7000
- printSummary(summary);
7005
+ printSummary(summary2);
7001
7006
  }
7002
7007
  if (willWatch && uploadedDocs.size > 0) {
7003
7008
  const pending = new Set(uploadedDocs.keys());
@@ -7059,17 +7064,17 @@ Connection closed. ${pending.size} document(s) still processing.`);
7059
7064
  })()
7060
7065
  );
7061
7066
  }
7062
- function updateSummary(summary, result) {
7067
+ function updateSummary(summary2, result) {
7063
7068
  let totalFiles = 0;
7064
7069
  for (const [, info] of result.folders) {
7065
7070
  totalFiles += info.fileCount;
7066
- summary.folders += 1;
7071
+ summary2.folders += 1;
7067
7072
  }
7068
- summary.totalFiles += totalFiles;
7069
- summary.uploaded += result.doc_ext_ids.length;
7070
- summary.skipped += result.skipped.length;
7071
- summary.docIds.push(...result.doc_ext_ids);
7072
- summary.skippedDetails.push(...result.skipped);
7073
+ summary2.totalFiles += totalFiles;
7074
+ summary2.uploaded += result.doc_ext_ids.length;
7075
+ summary2.skipped += result.skipped.length;
7076
+ summary2.docIds.push(...result.doc_ext_ids);
7077
+ summary2.skippedDetails.push(...result.skipped);
7073
7078
  }
7074
7079
  function countSupportedFiles2(dirPath) {
7075
7080
  let count = 0;
@@ -7752,10 +7757,10 @@ Error: ${message}`))
7752
7757
  saveLastMetadata(result.metadata);
7753
7758
  }
7754
7759
  const refs = sdk.countCitations(result.metadata);
7755
- const summary = opts.quiet !== true ? sdk.formatStreamSummary(result, elapsedTime) : "";
7756
- if (summary) {
7760
+ const summary2 = opts.quiet !== true ? sdk.formatStreamSummary(result, elapsedTime) : "";
7761
+ if (summary2) {
7757
7762
  const refSuffix = refs > 0 ? ` \xB7 ${refs} ref${refs === 1 ? "" : "s"}` : "";
7758
- const text = `[${summary}${refSuffix}]`;
7763
+ const text = `[${summary2}${refSuffix}]`;
7759
7764
  const cols = process.stderr.columns || 80;
7760
7765
  const pad = Math.max(0, cols - text.length);
7761
7766
  console.error(chalk2__default.default.dim(" ".repeat(pad) + text));
@@ -8472,6 +8477,174 @@ function registerTimelineCommand(program2) {
8472
8477
  );
8473
8478
  }
8474
8479
  init_prompts();
8480
+ function readStructured(path8) {
8481
+ let text;
8482
+ try {
8483
+ text = fs7.readFileSync(path8, "utf8");
8484
+ } catch (err) {
8485
+ throw new Error(`Cannot read ${path8}: ${err.message}`);
8486
+ }
8487
+ const ext = path6.extname(path8).toLowerCase();
8488
+ try {
8489
+ return ext === ".json" ? JSON.parse(text) : yaml.parse(text);
8490
+ } catch (err) {
8491
+ throw new Error(
8492
+ `${path8} is not valid ${ext === ".json" ? "JSON" : "YAML"}: ${err.message}`
8493
+ );
8494
+ }
8495
+ }
8496
+ function readStepsFile(path8) {
8497
+ try {
8498
+ return sdk.workflowSteps.parsePlanSteps(readStructured(path8));
8499
+ } catch (err) {
8500
+ if (err instanceof sdk.PlanStepsError) throw new Error(`${path8}: ${err.message}`);
8501
+ throw err;
8502
+ }
8503
+ }
8504
+ function readInputsFile(path8) {
8505
+ const value = readStructured(path8);
8506
+ if (typeof value !== "object" || value === null || Array.isArray(value))
8507
+ throw new Error(`${path8}: expected an object of input values, e.g. {"client": "Acme"}`);
8508
+ return value;
8509
+ }
8510
+ function collect(value, previous = []) {
8511
+ return [...previous, value];
8512
+ }
8513
+ var GAP = "\u2014";
8514
+ var isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
8515
+ function parseFieldDefinitions(arg) {
8516
+ let value;
8517
+ const trimmed = arg.trim();
8518
+ if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
8519
+ try {
8520
+ value = JSON.parse(trimmed);
8521
+ } catch (err) {
8522
+ throw new Error(`--define is not valid JSON: ${err.message}`);
8523
+ }
8524
+ } else if (fs7.existsSync(arg)) value = readStructured(arg);
8525
+ else
8526
+ throw new Error(
8527
+ `--define takes JSON (e.g. '[{"name":"budget","type":"number"}]') or a .json/.yaml file; no file "${arg}"`
8528
+ );
8529
+ const list = isRecord(value) ? value.fields : value;
8530
+ if (!Array.isArray(list))
8531
+ throw new Error('--define: expected a list of fields, or an object with a "fields" list');
8532
+ const problems = [];
8533
+ const names = /* @__PURE__ */ new Set();
8534
+ const out = list.map((raw, i) => {
8535
+ const at = `fields[${i}]`;
8536
+ if (!isRecord(raw)) {
8537
+ problems.push(`${at}: must be an object with a name and a type`);
8538
+ return { name: "", type: "text" };
8539
+ }
8540
+ for (const key of Object.keys(raw))
8541
+ if (!["id", "name", "type", "options"].includes(key))
8542
+ problems.push(`${at}.${key}: unknown field (allowed: name, type, options, id)`);
8543
+ const name = typeof raw.name === "string" ? raw.name.trim() : "";
8544
+ if (!name || name.length > 200)
8545
+ problems.push(`${at}.name: required text (up to 200 characters)`);
8546
+ else if (names.has(name.toLowerCase())) problems.push(`${at}.name: "${name}" is used twice`);
8547
+ names.add(name.toLowerCase());
8548
+ const type = raw.type ?? "text";
8549
+ if (!sdk.tags.TAG_FIELD_TYPES.includes(type))
8550
+ problems.push(
8551
+ `${at}.type: must be one of ${sdk.tags.TAG_FIELD_TYPES.join(", ")} (got ${JSON.stringify(type)})`
8552
+ );
8553
+ const options = raw.options;
8554
+ if (options !== void 0 && options !== null) {
8555
+ if (!Array.isArray(options) || options.some((o) => typeof o !== "string"))
8556
+ problems.push(`${at}.options: a list of text, e.g. ["low", "high"]`);
8557
+ else if (type !== "select") problems.push(`${at}.options: only a select field has options`);
8558
+ } else if (type === "select") problems.push(`${at}.options: a select field needs its options`);
8559
+ if (raw.id !== void 0 && (typeof raw.id !== "string" || !/^fld-[a-f0-9]{6}$/.test(raw.id)))
8560
+ problems.push(`${at}.id: an existing field id (fld-\u2026), or leave it out`);
8561
+ return {
8562
+ ...typeof raw.id === "string" ? { id: raw.id } : {},
8563
+ name,
8564
+ type,
8565
+ ...Array.isArray(options) ? { options } : {}
8566
+ };
8567
+ });
8568
+ if (problems.length)
8569
+ throw new Error(`Invalid field definitions:
8570
+ ${problems.map((p) => ` - ${p}`).join("\n")}`);
8571
+ return out;
8572
+ }
8573
+ function parseFieldValues(pairs, fields) {
8574
+ const out = {};
8575
+ for (const pair of pairs) {
8576
+ const eq = pair.indexOf("=");
8577
+ if (eq <= 0) throw new Error(`Invalid --set "${pair}": use name=value (name= clears it)`);
8578
+ const name = pair.slice(0, eq).trim();
8579
+ const value = pair.slice(eq + 1);
8580
+ const field = fields.find((f) => f.name.toLowerCase() === name.toLowerCase() || f.id === name);
8581
+ if (!field)
8582
+ throw new Error(
8583
+ `No field "${name}" on this tag. Its fields: ${fields.map((f) => f.name).join(", ") || "(none: add some with --define)"}`
8584
+ );
8585
+ if (value.trim() === "") out[field.name] = null;
8586
+ else if (field.type === "select" && field.options?.length) {
8587
+ const option = field.options.find((o) => o.toLowerCase() === value.trim().toLowerCase());
8588
+ if (!option)
8589
+ throw new Error(`"${field.name}" is one of ${field.options.join(", ")}; got "${value}"`);
8590
+ out[field.name] = option;
8591
+ } else out[field.name] = value;
8592
+ }
8593
+ return out;
8594
+ }
8595
+ function formatFieldValue(value) {
8596
+ if (value === void 0 || value === null || value === "") return GAP;
8597
+ if (typeof value === "boolean") return value ? "yes" : "no";
8598
+ if (typeof value === "object") return JSON.stringify(value);
8599
+ return String(value);
8600
+ }
8601
+ function fieldRows(tag) {
8602
+ return (tag.fields ?? []).map((f) => ({
8603
+ id: f.id,
8604
+ name: f.name,
8605
+ type: f.type === "select" && f.options?.length ? `select: ${f.options.join("|")}` : f.type,
8606
+ value: formatFieldValue(tag.field_values?.[f.id]),
8607
+ from: f.tag_ext_id === tag.external_id ? "own" : `inherited (${f.tag_ext_id})`
8608
+ }));
8609
+ }
8610
+ function matrixTable(matrix) {
8611
+ const columns = sdk.tags.tagMatrixColumns(matrix);
8612
+ const headers = ["TAG", ...columns.map((c) => c.name.toUpperCase()), "DOCS", "FACTS"];
8613
+ const rows = [];
8614
+ const gaps = [];
8615
+ for (const row of matrix.rows ?? []) {
8616
+ const has = new Set((row.tag.fields ?? []).map((f) => f.id));
8617
+ const cells = columns.map(
8618
+ (c) => has.has(c.id) ? formatFieldValue(row.tag.field_values?.[c.id]) : "n/a"
8619
+ );
8620
+ rows.push([
8621
+ row.tag.name,
8622
+ ...cells,
8623
+ row.doc_count ? String(row.doc_count) : GAP,
8624
+ row.fact_count ? String(row.fact_count) : GAP
8625
+ ]);
8626
+ const missing = columns.filter((c, i) => has.has(c.id) && cells[i] === GAP).map((c) => c.name);
8627
+ if (!row.doc_count) missing.push("documents");
8628
+ if (!row.fact_count) missing.push("facts");
8629
+ if (missing.length) gaps.push(`${row.tag.name}: no ${missing.join(", no ")}`);
8630
+ }
8631
+ return { headers, rows, gaps };
8632
+ }
8633
+ function renderTable(headers, rows, max = 28) {
8634
+ const clip = (s) => s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
8635
+ const widths = headers.map(
8636
+ (h, i) => Math.min(max, Math.max(h.length, ...rows.map((r) => clip(r[i] ?? "").length)))
8637
+ );
8638
+ const line = (cells) => cells.map((c, i) => clip(c ?? "").padEnd(widths[i])).join(" ").trimEnd();
8639
+ return [line(headers), ...rows.map(line)];
8640
+ }
8641
+
8642
+ // src/commands/tags.ts
8643
+ async function resolveTag(arbi, selector) {
8644
+ const res = resolveByIdOrName(await sdk.tags.listTags(arbi), selector);
8645
+ if (!res.ok) failResolveAndExit("Tag", selector, res);
8646
+ return res.item;
8647
+ }
8475
8648
  async function fetchTagChoices(arbi) {
8476
8649
  const data = await sdk.tags.listTags(arbi);
8477
8650
  if (data.length === 0) {
@@ -8488,7 +8661,7 @@ async function fetchTagChoices(arbi) {
8488
8661
  };
8489
8662
  }
8490
8663
  function registerTagsCommand(program2) {
8491
- const tags = program2.command("tags").description("Tags: list, create, delete, update, share, unshare, rename");
8664
+ const tags = program2.command("tags").description("Tags: list, create, delete, update, share, unshare, rename, fields, matrix");
8492
8665
  tags.command("list").description("List tags in the active workspace").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").option("--json", "Output as JSON").action(
8493
8666
  (opts) => runAction(async () => {
8494
8667
  const { arbi } = await resolveWorkspace(opts.workspace);
@@ -8511,7 +8684,8 @@ function registerTagsCommand(program2) {
8511
8684
  value: (r) => r.tag_type?.type ?? ""
8512
8685
  },
8513
8686
  { header: "DOCS", width: 6, value: (r) => String(r.doctag_count) },
8514
- { header: "SHARED", width: 8, value: (r) => r.shared ? "yes" : "no" }
8687
+ { header: "SHARED", width: 8, value: (r) => r.shared ? "yes" : "no" },
8688
+ { header: "PARENT", width: 24, value: (r) => r.parent_ext_id ?? "" }
8515
8689
  ],
8516
8690
  data
8517
8691
  );
@@ -8521,9 +8695,10 @@ function registerTagsCommand(program2) {
8521
8695
  "-t, --type <type>",
8522
8696
  "Tag type (checkbox, text, number, select, folder, search, date, assistant)",
8523
8697
  "checkbox"
8524
- ).option("-i, --instruction <text>", "Tag instruction").option("--shared", "Make tag shared", false).action(
8698
+ ).option("-i, --instruction <text>", "Tag instruction").option("--shared", "Make tag shared", false).option("--parent <tag>", "Make it a child of this tag (id or name): it inherits its fields").option("--json", "Output as JSON").action(
8525
8699
  (name, opts) => runAction(async () => {
8526
8700
  const { arbi } = await resolveWorkspace(opts.workspace);
8701
+ const parent = opts.parent ? await resolveTag(arbi, opts.parent) : void 0;
8527
8702
  const interactive = !name;
8528
8703
  if (!name) {
8529
8704
  name = await promptInput("Tag name");
@@ -8547,9 +8722,16 @@ function registerTagsCommand(program2) {
8547
8722
  options: []
8548
8723
  },
8549
8724
  instruction,
8550
- shared
8725
+ shared,
8726
+ parentId: parent?.external_id ?? null
8551
8727
  });
8552
- success(`Created: ${data.name} (${ref(data.external_id)})`);
8728
+ if (opts.json) {
8729
+ printJson(data);
8730
+ return;
8731
+ }
8732
+ success(
8733
+ `Created: ${data.name} (${ref(data.external_id)})${parent ? ` under ${parent.name}` : ""}`
8734
+ );
8553
8735
  })()
8554
8736
  );
8555
8737
  tags.command("delete [id]").description("Delete a tag (interactive picker if no ID given)").option("--dry-run", "Preview which tag would be deleted (no SDK call)").action(
@@ -8614,6 +8796,95 @@ function registerTagsCommand(program2) {
8614
8796
  success(`Updated: ${data.name} (${ref(data.external_id)})`);
8615
8797
  })()
8616
8798
  );
8799
+ tags.command("fields <tag>").description(
8800
+ "Show a tag's fields and values; --define its own fields (a child inherits its parent's), --set values"
8801
+ ).option(
8802
+ "--define <json|file>",
8803
+ `Its own field definitions: JSON or a .json/.yaml file, e.g. '[{"name":"budget","type":"number"},{"name":"risk","type":"select","options":["low","high"]}]'. A field defined again by name keeps its values; ones left out are dropped unless --keep-others`
8804
+ ).option("--keep-others", "With --define: keep its other own fields").option("--set <name=value>", "Set a field value (repeatable; name= clears it)", collect, []).option("--json", "Output as JSON").action(
8805
+ (selector, opts) => runAction(async () => {
8806
+ const defs = opts.define ? parseFieldDefinitions(opts.define) : void 0;
8807
+ const { arbi } = await resolveWorkspace();
8808
+ let tag = await resolveTag(arbi, selector);
8809
+ if (defs || opts.set.length) {
8810
+ const fields = defs ? sdk.tags.mergeTagFieldDefinitions(tag, defs, { keepOthers: !!opts.keepOthers }) : void 0;
8811
+ const known = fields ? [
8812
+ ...(tag.fields ?? []).filter((f) => f.tag_ext_id !== tag.external_id),
8813
+ ...fields.map((f) => ({
8814
+ id: f.id ?? "",
8815
+ name: f.name,
8816
+ type: f.type,
8817
+ options: f.options ?? null,
8818
+ tag_ext_id: tag.external_id
8819
+ }))
8820
+ ] : tag.fields ?? [];
8821
+ const values = opts.set.length ? parseFieldValues(opts.set, known) : void 0;
8822
+ tag = await sdk.tags.updateTagFields(arbi, tag.external_id, { fields, values });
8823
+ }
8824
+ if (opts.json) {
8825
+ printJson(tag);
8826
+ return;
8827
+ }
8828
+ if (defs || opts.set.length)
8829
+ success(`Updated the fields of ${tag.name} (${ref(tag.external_id)})`);
8830
+ else bold(`${tag.name} (${tag.external_id})`);
8831
+ const rows = fieldRows(tag);
8832
+ if (!rows.length) {
8833
+ dim(
8834
+ `No fields. Define some: arbi tags fields ${tag.external_id} --define '[{"name":"budget","type":"number"}]'`
8835
+ );
8836
+ return;
8837
+ }
8838
+ printTable(
8839
+ [
8840
+ { header: "FIELD", width: 22, value: (r) => r.name },
8841
+ { header: "TYPE", width: 24, value: (r) => r.type },
8842
+ { header: "VALUE", width: 28, value: (r) => r.value },
8843
+ { header: "DEFINED", width: 30, value: (r) => r.from }
8844
+ ],
8845
+ rows
8846
+ );
8847
+ })()
8848
+ );
8849
+ tags.command("matrix").description(
8850
+ "Tags as a table: rows are a parent's children (--parent) or the given tags (--tag), columns their fields, then document and fact counts; gaps marked \u2014"
8851
+ ).option("--parent <tag>", "Rows are this tag\u2019s children (id or name)").option("--tag <tag>", "A row (id or name; repeatable, in this order)", collect, []).option("--links <n>", "Most documents and facts listed per row (JSON)").option("--json", "Output as JSON").action(
8852
+ (opts) => runAction(async () => {
8853
+ if (!opts.parent === !opts.tag.length)
8854
+ throw new Error("Pass --parent <tag>, or one or more --tag <tag>");
8855
+ const links = opts.links === void 0 ? void 0 : Number(opts.links);
8856
+ if (links !== void 0 && (!Number.isInteger(links) || links < 0))
8857
+ throw new Error("--links must be a whole number");
8858
+ const { arbi } = await resolveWorkspace();
8859
+ const all = await sdk.tags.listTags(arbi);
8860
+ const pick = (selector) => {
8861
+ const res = resolveByIdOrName(all, selector);
8862
+ if (!res.ok) failResolveAndExit("Tag", selector, res);
8863
+ return res.item;
8864
+ };
8865
+ const selection = opts.parent ? { parentId: pick(opts.parent).external_id } : { tagIds: opts.tag.map((t) => pick(t).external_id) };
8866
+ const matrix = await sdk.tags.getTagMatrix(arbi, selection, links);
8867
+ if (opts.json) {
8868
+ printJson(matrix);
8869
+ return;
8870
+ }
8871
+ const table = matrixTable(matrix);
8872
+ if (matrix.parent) bold(`${matrix.parent.name} (${matrix.parent.external_id})`);
8873
+ if (!table.rows.length) {
8874
+ process.stderr.write(
8875
+ opts.parent ? `No child tags. Add one: arbi tags create <name> --parent ${opts.parent}
8876
+ ` : "No tags.\n"
8877
+ );
8878
+ return;
8879
+ }
8880
+ for (const line of renderTable(table.headers, table.rows)) console.log(line);
8881
+ if (table.gaps.length) {
8882
+ console.log("");
8883
+ dim("Gaps:");
8884
+ for (const g of table.gaps) dim(` ${g}`);
8885
+ }
8886
+ })()
8887
+ );
8617
8888
  tags.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
8618
8889
  const tail = tags.args ?? [];
8619
8890
  await tags.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
@@ -9427,6 +9698,9 @@ async function runQuickstart(url) {
9427
9698
  console.log(` Workspace: ${workspaceName} (${ref(workspaceId)})`);
9428
9699
  console.log("");
9429
9700
  dim('Try: arbi ask "hello"');
9701
+ dim(
9702
+ "Workflows with steps: arbi workflow create --name <n> --steps steps.yaml (see arbi run --help)"
9703
+ );
9430
9704
  } catch (err) {
9431
9705
  if (isExitPromptError(err)) throw err;
9432
9706
  error(`Login failed: ${sdk.getErrorMessage(err)}`);
@@ -9793,14 +10067,59 @@ function registerTaskCommand(program2) {
9793
10067
  })()
9794
10068
  );
9795
10069
  }
10070
+ function signalTrigger(name, eventKind, eventFilter) {
10071
+ if (name === void 0) return void 0;
10072
+ if (!sdk.workflowSteps.SIGNAL_NAME_PATTERN.test(name))
10073
+ throw new Error(
10074
+ `Invalid --on-signal "${name}": lowercase letters, digits, ".", "_" or "-", starting with a letter or digit (e.g. risk-high)`
10075
+ );
10076
+ if (eventKind && eventKind !== "workflow_signal")
10077
+ throw new Error(`--on-signal is an event trigger: use it instead of --event-kind ${eventKind}`);
10078
+ let filter = {};
10079
+ if (eventFilter) {
10080
+ try {
10081
+ filter = JSON.parse(eventFilter);
10082
+ } catch {
10083
+ throw new Error("--event-filter must be a JSON object");
10084
+ }
10085
+ }
10086
+ return { event_kind: "workflow_signal", event_filter: { ...filter, signal: name } };
10087
+ }
10088
+ function triggerText(w) {
10089
+ const signal = w.event_filter?.signal;
10090
+ if (w.event_kind === "workflow_signal" && typeof signal === "string") return `signal ${signal}`;
10091
+ if (w.event_kind) return `event ${w.event_kind}`;
10092
+ if (w.rrule) return w.rrule;
10093
+ if (w.run_at) return `once ${w.run_at}`;
10094
+ if (w.after_conversation_ext_id) return `after ${w.after_conversation_ext_id}`;
10095
+ return "by hand";
10096
+ }
9796
10097
  function registerWorkflowCommand(program2) {
9797
- const workflow = program2.command("workflow").alias("automation").description("Workflows (/v1/workflow): create, list, resume, pause, run, delete");
9798
- workflow.command("create").description("Create a workflow with exactly one trigger").requiredOption("--name <name>", "Workflow name (required)").option("--rrule <RRULE>", "Trigger: RFC-5545 recurrence rule").option("--run-at <ISO>", "Trigger: run once at this time (ISO-8601)").option("--event-kind <kind>", "Trigger: a notification type to react to").option("--after <con-id>", "Trigger: when this task (conversation) is done").option("--timezone <tz>", "IANA timezone for --rrule", "UTC").option("--action <type>", "Action: annotate_documents or email_documents").option("--documents <ids>", "Comma-separated document IDs").option("--labels <ids>", "Select documents matching these label IDs at execution time").option("--latest", "Select documents uploaded since the previous successful run").option("--tags <ids>", "Annotation field IDs for AI review").option("--overwrite", "Regenerate existing annotation answers").option(
10098
+ const workflow = program2.command("workflow").alias("automation").description(
10099
+ "Workflows (/v1/workflow): create (optionally with steps), list, update, resume, pause, run, delete"
10100
+ );
10101
+ workflow.command("create").description(
10102
+ "Create a workflow: at most one trigger (none = started by hand) and what it runs: --steps, --prompt, --skill or --action"
10103
+ ).requiredOption("--name <name>", "Workflow name (required)").option(
10104
+ "--steps <file>",
10105
+ "Named steps (form/upload/agent/review) as a TaskPlanStep list in .json, .yaml or .yml"
10106
+ ).option("--rrule <RRULE>", "Trigger: RFC-5545 recurrence rule").option("--run-at <ISO>", "Trigger: run once at this time (ISO-8601)").option("--event-kind <kind>", "Trigger: a notification type to react to").option(
10107
+ "--on-signal <name>",
10108
+ "Trigger: when a step of another run raises this signal (event_kind=workflow_signal, filter {signal})"
10109
+ ).option("--after <con-id>", "Trigger: when this task (conversation) is done").option("--timezone <tz>", "IANA timezone for --rrule", "UTC").option("--action <type>", "Action: annotate_documents or email_documents").option("--documents <ids>", "Comma-separated document IDs").option("--labels <ids>", "Select documents matching these label IDs at execution time").option("--latest", "Select documents uploaded since the previous successful run").option("--tags <ids>", "Annotation field IDs for AI review").option("--overwrite", "Regenerate existing annotation answers").option(
9799
10110
  "--auth-mode <mode>",
9800
10111
  "scheduled_require_auth or scheduled_permanent",
9801
10112
  "scheduled_require_auth"
9802
10113
  ).option("--event-filter <json>", "Notification attribute filters as a JSON object").option("--prompt <text>", "Instruction the agent runs").option("--skill <ref>", "Skill reference to run").option("--agent <agt-id>", "Agent to run it as (external id)").option("--workspace <wrk-id>", "Workspace to scope the workflow to (default: the current one)").option("--shared", "Share it with the workspace").option("--json", "Output as JSON").action(
9803
10114
  (opts) => runAction(async () => {
10115
+ if (opts.steps && (opts.prompt || opts.skill || opts.action))
10116
+ throw new Error("Use --steps, or --prompt / --skill / --action, not both");
10117
+ const signal = signalTrigger(opts.onSignal, opts.eventKind, opts.eventFilter);
10118
+ if (signal) {
10119
+ opts.eventKind = signal.event_kind;
10120
+ opts.eventFilter = JSON.stringify(signal.event_filter);
10121
+ }
10122
+ const steps = opts.steps ? readStepsFile(opts.steps) : void 0;
9804
10123
  const { arbi, workspaceId } = await resolveWorkspace();
9805
10124
  const body = {
9806
10125
  name: opts.name,
@@ -9842,12 +10161,27 @@ function registerWorkflowCommand(program2) {
9842
10161
  if (opts.after) body.after_conversation_ext_id = opts.after;
9843
10162
  if (opts.skill) body.skill_ref = opts.skill;
9844
10163
  if (opts.agent) body.agent_ext_id = opts.agent;
10164
+ if (steps) body.plan = sdk.workflowSteps.stepsPlan(steps);
9845
10165
  const data = await sdk.workflows.createWorkflow(arbi, body);
9846
10166
  if (opts.json) {
9847
10167
  printJson(data);
9848
10168
  return;
9849
10169
  }
9850
- success(`Created workflow: ${data.name} (${ref(data.external_id)})`);
10170
+ success(
10171
+ `Created workflow: ${data.name} (${ref(data.external_id)})${steps ? ` with ${steps.length} steps` : ""}`
10172
+ );
10173
+ if (opts.onSignal)
10174
+ dim(
10175
+ `It starts when a step of a run raises "${opts.onSignal}" (a step's signals: [{name: ${opts.onSignal}, when: \u2026}]).`
10176
+ );
10177
+ const raised = [
10178
+ ...new Set((steps ?? []).flatMap((s) => (s.signals ?? []).map((x) => x.name)))
10179
+ ];
10180
+ if (raised.length)
10181
+ dim(
10182
+ `Its steps raise ${raised.map((n) => `"${n}"`).join(", ")}: a workflow made with --on-signal <name> starts on each.`
10183
+ );
10184
+ if (steps) dim(`Run it: arbi workflow run ${data.external_id}`);
9851
10185
  })()
9852
10186
  );
9853
10187
  workflow.command("list").description("List your workflows").option("--json", "Output as JSON").action(
@@ -9861,6 +10195,11 @@ function registerWorkflowCommand(program2) {
9861
10195
  { header: "NAME", width: 28, value: (r) => r.name ?? "" },
9862
10196
  { header: "ACTIVE", width: 7, value: (r) => r.active ? "yes" : "no" },
9863
10197
  { header: "SHARED", width: 8, value: (r) => r.shared ? "yes" : "no" },
10198
+ {
10199
+ header: "TRIGGER",
10200
+ width: 26,
10201
+ value: (r) => triggerText(r)
10202
+ },
9864
10203
  { header: "NEXT RUN", width: 26, value: (r) => r.next_fire_at ?? "" },
9865
10204
  { header: "LAST RUN", width: 26, value: (r) => r.last_fired_at ?? "" }
9866
10205
  ],
@@ -9880,7 +10219,9 @@ function registerWorkflowCommand(program2) {
9880
10219
  })()
9881
10220
  );
9882
10221
  }
9883
- workflow.command("run <id>").description("Start the saved action now (document actions do not create a thread)").option("--json", "Output as JSON").action(
10222
+ workflow.command("run <id>").description(
10223
+ "Run a workflow now: a thread (a stepped one waits for its steps), or a document action without one"
10224
+ ).option("--json", "Output as JSON").action(
9884
10225
  (id, opts) => runAction(async () => {
9885
10226
  const { arbi } = await resolveAuth();
9886
10227
  const data = await sdk.workflows.runWorkflow(arbi, id);
@@ -9890,11 +10231,17 @@ function registerWorkflowCommand(program2) {
9890
10231
  }
9891
10232
  if (data.error) throw new Error(data.error);
9892
10233
  success(
9893
- `${data.started ? "Started" : "Submitted"} automation ${ref(id)}${data.execution_id ? ` \u2014 execution ${data.execution_id}` : ""}${data.conversation_ext_id ? ` \u2014 thread ${ref(data.conversation_ext_id)}` : ""}`
10234
+ `${data.started ? "Started" : "Submitted"} workflow ${ref(id)}${data.execution_id ? ` \u2014 execution ${data.execution_id}` : ""}${data.conversation_ext_id ? ` \u2014 thread ${ref(data.conversation_ext_id)}` : ""}`
9894
10235
  );
10236
+ if (!data.conversation_ext_id) return;
10237
+ const saved = await sdk.workflows.getWorkflow(arbi, id);
10238
+ if (saved.plan?.steps?.length) {
10239
+ dim(`Its steps: arbi run steps ${data.conversation_ext_id}`);
10240
+ dim(`Follow it: arbi run steps ${data.conversation_ext_id} --watch --answers`);
10241
+ }
9895
10242
  })()
9896
10243
  );
9897
- workflow.command("get <id>").description("Read the complete saved automation configuration").action(
10244
+ workflow.command("get <id>").description("Read the complete saved workflow configuration").action(
9898
10245
  (id) => runAction(async () => {
9899
10246
  const { arbi } = await resolveAuth();
9900
10247
  printJson(await sdk.workflows.getWorkflow(arbi, id));
@@ -9904,7 +10251,7 @@ function registerWorkflowCommand(program2) {
9904
10251
  (id, opts) => runAction(async () => {
9905
10252
  const { arbi } = await resolveAuth();
9906
10253
  const data = await sdk.workflows.listWorkflowRuns(arbi, id);
9907
- if (!emitListOrTable(data, opts, "This automation has not run yet.")) return;
10254
+ if (!emitListOrTable(data, opts, "This workflow has not run yet.")) return;
9908
10255
  printTable(
9909
10256
  [
9910
10257
  { header: "RUN", width: 10, value: (r) => String(r.id) },
@@ -9922,13 +10269,25 @@ function registerWorkflowCommand(program2) {
9922
10269
  );
9923
10270
  })()
9924
10271
  );
9925
- workflow.command("update <id>").description("Update any saved automation settings from a JSON file").requiredOption("--file <path>", "JSON UpdateWorkflowRequest file").option("--json", "Output as JSON").action(
10272
+ workflow.command("update <id>").description("Update a saved workflow: its steps (--steps), and any setting from a JSON file").option("--steps <file>", "Replace its steps (TaskPlanStep list in .json, .yaml or .yml)").option("--file <path>", "JSON UpdateWorkflowRequest file").option("--on-signal <name>", "Start it when a run raises this signal (replaces its trigger)").option("--json", "Output as JSON").action(
9926
10273
  (id, opts) => runAction(async () => {
10274
+ if (!opts.file && !opts.steps && !opts.onSignal)
10275
+ throw new Error("Pass --steps <file>, --on-signal <name> and/or --file <path>");
10276
+ const steps = opts.steps ? readStepsFile(opts.steps) : void 0;
10277
+ const body = opts.file ? JSON.parse(fs7.readFileSync(opts.file, "utf8")) : {};
10278
+ const signal = signalTrigger(opts.onSignal, void 0, void 0);
10279
+ if (signal) Object.assign(body, signal, { on_demand: false });
10280
+ if (steps)
10281
+ Object.assign(body, {
10282
+ plan: sdk.workflowSteps.stepsPlan(steps),
10283
+ prompt: "",
10284
+ skill_ref: null,
10285
+ skill_inputs: null
10286
+ });
9927
10287
  const { arbi } = await resolveAuth();
9928
- const body = JSON.parse(fs7.readFileSync(opts.file, "utf8"));
9929
10288
  const data = await sdk.workflows.updateWorkflow(arbi, id, body);
9930
10289
  if (opts.json) printJson(data);
9931
- else success(`Updated automation ${ref(id)}`);
10290
+ else success(`Updated workflow ${ref(id)}${steps ? ` \u2014 ${steps.length} steps` : ""}`);
9932
10291
  })()
9933
10292
  );
9934
10293
  workflow.command("delete <id>").description("Delete a workflow (tasks it already created stay)").action(
@@ -9939,6 +10298,367 @@ function registerWorkflowCommand(program2) {
9939
10298
  })()
9940
10299
  );
9941
10300
  }
10301
+ function runJson(progress, answers = {}) {
10302
+ const c = progress.conversation;
10303
+ return {
10304
+ conversation_ext_id: c.external_id,
10305
+ title: c.title,
10306
+ workflow_ext_id: c.workflow_ext_id ?? null,
10307
+ parent_conversation_ext_id: c.parent_conversation_ext_id ?? null,
10308
+ waiting_on: progress.waitingOn,
10309
+ in_progress: c.in_progress,
10310
+ pending_auth_request_id: c.pending_auth_request_id ?? null,
10311
+ last_error: c.last_error ?? null,
10312
+ steps: progress.steps.map((s) => ({
10313
+ id: s.id,
10314
+ title: s.title,
10315
+ kind: s.kind,
10316
+ state: s.phase,
10317
+ after: s.after,
10318
+ answer_id: s.responseId,
10319
+ error: s.error,
10320
+ assignee: s.assignee,
10321
+ announced_to: s.announcedTo,
10322
+ sent_back: s.sentBack,
10323
+ sent_back_note: s.hasNote,
10324
+ times_sent_back: s.timesSentBack,
10325
+ signals: s.signals,
10326
+ linked_runs: s.linkedRuns.map(linkedJson),
10327
+ ...s.responseId && answers[s.responseId] !== void 0 ? { answer: answers[s.responseId] } : {}
10328
+ })),
10329
+ signals: (progress.signals ?? []).map((x) => ({ step: x.step, signal: x.signal, at: x.at })),
10330
+ linked_runs: (progress.linkedRuns ?? []).map(linkedJson),
10331
+ send_backs: (progress.sendBacks ?? []).map((b) => ({
10332
+ step: b.step,
10333
+ to: b.to,
10334
+ reopened: b.reopened,
10335
+ note: b.hasNote,
10336
+ by: b.by,
10337
+ at: b.at
10338
+ }))
10339
+ };
10340
+ }
10341
+ var linkedJson = (l) => ({
10342
+ step: l.step,
10343
+ signal: l.signal,
10344
+ conversation_ext_id: l.conversationId,
10345
+ workflow_ext_id: l.workflowId,
10346
+ at: l.at
10347
+ });
10348
+ function stepNotes(s) {
10349
+ const notes = [];
10350
+ if (s.sentBack) notes.push(`sent back${s.hasNote ? " (note)" : ""}`);
10351
+ else if (s.timesSentBack) notes.push(`sent back \xD7${s.timesSentBack}`);
10352
+ if (s.signals.length) notes.push(`\u2691 ${s.signals.join(", ")}`);
10353
+ if (s.linkedRuns.length)
10354
+ notes.push(
10355
+ s.linkedRuns.length === 1 ? `\u2192 ${s.linkedRuns[0].conversationId}` : `\u2192 ${s.linkedRuns.length} linked runs`
10356
+ );
10357
+ return notes.join("; ");
10358
+ }
10359
+ function runEventLines(progress) {
10360
+ const lines = [
10361
+ ...(progress.signals ?? []).map((x) => ({
10362
+ at: x.at ?? "",
10363
+ line: `\u2691 ${x.step} raised "${x.signal}"`
10364
+ })),
10365
+ ...(progress.linkedRuns ?? []).map((l) => ({
10366
+ at: l.at ?? "",
10367
+ line: `\u2192 "${l.signal ?? "?"}" started ${l.conversationId}${l.workflowId ? ` (workflow ${l.workflowId})` : ""}: arbi run steps ${l.conversationId}`
10368
+ })),
10369
+ ...(progress.sendBacks ?? []).map((b) => ({
10370
+ at: b.at ?? "",
10371
+ line: `\u21A9 ${b.step} sent the work back to ${b.to}${b.hasNote ? " with a note" : ""}${b.by ? ` (${b.by})` : ""}; reopened ${b.reopened.join(", ")}`
10372
+ }))
10373
+ ];
10374
+ return lines.sort((a, b) => a.at && b.at ? a.at.localeCompare(b.at) : 0).map((l) => l.line);
10375
+ }
10376
+ function nextHint(progress, planSteps = []) {
10377
+ const id = progress.conversation.external_id;
10378
+ if (progress.conversation.pending_auth_request_id)
10379
+ return "The run is waiting for access to the workspace: approve the request in ARBI.";
10380
+ switch (progress.waitingOn) {
10381
+ case "finished":
10382
+ return "Every step is done or skipped.";
10383
+ case "agent": {
10384
+ const next = progress.steps.find((s) => s.phase === "running") ?? progress.steps.find((s) => s.kind === "agent" && s.phase === "ready");
10385
+ return `Step ${next ? `"${next.id}" ` : ""}is on its way: arbi run steps ${id} --watch`;
10386
+ }
10387
+ case "person": {
10388
+ const step = progress.steps.find((s) => s.kind !== "agent" && s.phase === "ready");
10389
+ const planned = planSteps.find((p) => p.id === step.id);
10390
+ const have = progress.conversation.skill_inputs ?? {};
10391
+ const fields = (planned ? sdk.workflowSteps.stepFields(planned) : []).filter(
10392
+ (f) => have[f.name] === void 0 || have[f.name] === null || have[f.name] === ""
10393
+ );
10394
+ const inputs = fields.map((f) => ` --input ${f.name}=\u2026`).join("");
10395
+ const forWhom = step.assignee ? ` (for ${sdk.workflowSteps.describeAssignee(step.assignee)})` : "";
10396
+ const hint = `Waiting on ${step.kind} step "${step.id}"${forWhom}: arbi run step ${id} ${step.id} complete${inputs}`;
10397
+ const back = step.kind === "review" && step.after.length > 0;
10398
+ return back ? `${hint} (or: arbi run step ${id} ${step.id} send-back${step.after.length > 1 ? " --to <step>" : ""} --note "\u2026")` : hint;
10399
+ }
10400
+ default: {
10401
+ const failed = progress.steps.find((s) => s.phase === "failed");
10402
+ return failed ? `Step "${failed.id}" failed${failed.error ? ` (${failed.error})` : ""}: arbi run step ${id} ${failed.id} skip` : "Nothing can move: a step it waits for has not finished.";
10403
+ }
10404
+ }
10405
+ }
10406
+ function printRun(progress, answers) {
10407
+ const c = progress.conversation;
10408
+ bold(`Run ${c.external_id}${c.title ? ` \u2014 ${c.title}` : ""}`);
10409
+ if (c.parent_conversation_ext_id)
10410
+ dim(
10411
+ `Started from ${c.parent_conversation_ext_id}${c.workflow_ext_id ? ` by workflow ${c.workflow_ext_id}` : ""}: arbi run steps ${c.parent_conversation_ext_id}`
10412
+ );
10413
+ printTable(
10414
+ [
10415
+ { header: "STEP", width: 16, value: (r) => r.id },
10416
+ { header: "KIND", width: 8, value: (r) => r.kind },
10417
+ { header: "STATE", width: 9, value: (r) => r.phase },
10418
+ {
10419
+ header: "FOR",
10420
+ width: 15,
10421
+ value: (r) => sdk.workflowSteps.describeAssignee(r.assignee)
10422
+ },
10423
+ { header: "ANSWER", width: 16, value: (r) => r.responseId ?? "" },
10424
+ { header: "NOTES", width: 40, value: (r) => stepNotes(r) },
10425
+ { header: "TITLE", width: 32, value: (r) => r.title }
10426
+ ],
10427
+ progress.steps
10428
+ );
10429
+ for (const line of runEventLines(progress)) dim(line);
10430
+ for (const s of progress.steps) {
10431
+ if (s.phase === "failed" && s.error) warn(`${s.id}: ${s.error}`);
10432
+ if (!s.responseId || answers[s.responseId] === void 0) continue;
10433
+ console.log("");
10434
+ bold(`\u2500\u2500 ${s.id}: ${s.title} (${s.responseId})`);
10435
+ console.log(answers[s.responseId]);
10436
+ }
10437
+ console.log("");
10438
+ dim(nextHint(progress, c.plan?.steps ?? []));
10439
+ }
10440
+ async function answersFor(arbi, progress) {
10441
+ const ids = progress.steps.filter((s) => s.kind === "agent" && s.phase === "done" && s.responseId).map((s) => s.responseId);
10442
+ return sdk.work.getStepAnswers(arbi, progress.conversation.external_id, ids);
10443
+ }
10444
+ var sleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
10445
+ var summary = (p) => p.steps.map((s) => `${s.id}:${s.phase}`).join(" ");
10446
+ async function watchRun(arbi, conversationId, { intervalMs, timeoutMs, quiet }) {
10447
+ const deadline = Date.now() + timeoutMs;
10448
+ let progress = await sdk.work.getRunProgress(arbi, conversationId);
10449
+ let last = "";
10450
+ const report = () => {
10451
+ const now = summary(progress);
10452
+ if (!quiet && now !== last) process.stderr.write(`${(/* @__PURE__ */ new Date()).toISOString()} ${now}
10453
+ `);
10454
+ last = now;
10455
+ };
10456
+ report();
10457
+ while (progress.waitingOn === "agent" && !progress.conversation.pending_auth_request_id) {
10458
+ if (Date.now() >= deadline) return { progress, timedOut: true };
10459
+ await sleep(intervalMs);
10460
+ progress = await sdk.work.getRunProgress(arbi, conversationId);
10461
+ report();
10462
+ }
10463
+ return { progress, timedOut: false };
10464
+ }
10465
+ function seconds(value, flag) {
10466
+ const n = Number(value);
10467
+ if (!Number.isFinite(n) || n <= 0) throw new Error(`${flag} must be a positive number of seconds`);
10468
+ return n * 1e3;
10469
+ }
10470
+ var STEP_ACTIONS = {
10471
+ complete: "complete",
10472
+ skip: "skip",
10473
+ "send-back": "send_back",
10474
+ send_back: "send_back"
10475
+ };
10476
+ function fieldsHint(fields) {
10477
+ const one = (f) => {
10478
+ const options = f.options?.length ? `: ${f.options.map((o) => o.value).join("|")}` : "";
10479
+ const range = f.min !== null && f.min !== void 0 && f.max !== null && f.max !== void 0 ? ` ${f.min}\u2013${f.max}` : "";
10480
+ return `${f.name} (${f.type ?? "text"}${options}${range}${f.required ? ", required" : ""})`;
10481
+ };
10482
+ return `Its fields: ${fields.map(one).join(", ")}`;
10483
+ }
10484
+ function upstreamOf(plan, id) {
10485
+ const found = /* @__PURE__ */ new Set();
10486
+ const stack = sdk.workflowSteps.stepDependencies(plan, id);
10487
+ while (stack.length) {
10488
+ const d = stack.pop();
10489
+ if (found.has(d)) continue;
10490
+ found.add(d);
10491
+ stack.push(...sdk.workflowSteps.stepDependencies(plan, d));
10492
+ }
10493
+ return found;
10494
+ }
10495
+ function checkSendBack(plan, stepId, to) {
10496
+ const upstream = upstreamOf(plan, stepId);
10497
+ if (to === void 0) {
10498
+ const direct = sdk.workflowSteps.stepDependencies(plan, stepId);
10499
+ if (direct.length !== 1)
10500
+ throw new Error(
10501
+ `"${stepId}" waits for ${direct.length ? direct.join(", ") : "no step"}: say which to send it back to with --to <step>`
10502
+ );
10503
+ return;
10504
+ }
10505
+ if (!plan.some((s) => s.id === to))
10506
+ throw new Error(`No step "${to}". Its steps: ${plan.map((s) => s.id).join(", ")}`);
10507
+ if (!upstream.has(to))
10508
+ throw new Error(
10509
+ `"${to}" is not a step "${stepId}" waits for: send it back to one of ${[...upstream].join(", ") || "(none)"}`
10510
+ );
10511
+ }
10512
+ function gatherInputs(pairs, file, fields) {
10513
+ return {
10514
+ ...file ? readInputsFile(file) : {},
10515
+ ...sdk.workflowSteps.parseInputPairs(pairs, fields)
10516
+ };
10517
+ }
10518
+ function registerRunCommand(program2) {
10519
+ const run = program2.command("run").description(
10520
+ "Stepped workflow runs: start one from a steps file, see its steps, complete or skip a step"
10521
+ );
10522
+ run.command("start").description("Start a one-off stepped run as a work thread, without saving a workflow").requiredOption("--steps <file>", "Plan steps (TaskPlanStep list) as .json, .yaml or .yml").requiredOption("--title <title>", "The run thread's title").option("--input <name=value>", "A run input, e.g. a form field (repeatable)", collect, []).option("--inputs-file <file>", "Run inputs as a JSON or YAML object").option("--agent <agt-id>", "Agent to run the agent steps as (external id)").option("--shared", "Share the thread with the workspace").option("--json", "Output as JSON").action(
10523
+ (opts) => runAction(async () => {
10524
+ const steps = readStepsFile(opts.steps);
10525
+ const inputs = gatherInputs(
10526
+ opts.input,
10527
+ opts.inputsFile,
10528
+ steps.flatMap((s) => sdk.workflowSteps.stepFields(s))
10529
+ );
10530
+ const { arbi } = await resolveWorkspace();
10531
+ const created = await sdk.work.createWork(arbi, {
10532
+ title: opts.title,
10533
+ plan: sdk.workflowSteps.stepsPlan(steps),
10534
+ skill_inputs: Object.keys(inputs).length ? inputs : null,
10535
+ agent_ext_id: opts.agent ?? null,
10536
+ shared: opts.shared ?? null,
10537
+ auth_mode: "run_now_temp"
10538
+ });
10539
+ const progress = await sdk.work.getRunProgress(arbi, created.external_id);
10540
+ if (opts.json) {
10541
+ printJson(runJson(progress));
10542
+ return;
10543
+ }
10544
+ success(`Started run ${ref(created.external_id)} \u2014 ${opts.title}`);
10545
+ dim(`Follow it: arbi run steps ${created.external_id} --watch`);
10546
+ dim(nextHint(progress, steps));
10547
+ })()
10548
+ );
10549
+ run.command("steps <con-id>").description(
10550
+ "A run's steps and their state (pending, ready, running, done, skipped, failed), with each done agent step's answer id"
10551
+ ).option("--answers", "Print the done agent steps' answers from the thread").option("--watch", "Poll until the run stops waiting on an agent step").option("--interval <seconds>", "Poll interval for --watch", "3").option("--timeout <seconds>", "Stop watching after this long", "600").option("--json", "Output as JSON").action(
10552
+ (conversationId, opts) => runAction(async () => {
10553
+ const intervalMs = seconds(opts.interval, "--interval");
10554
+ const timeoutMs = seconds(opts.timeout, "--timeout");
10555
+ const { arbi } = await resolveWorkspace();
10556
+ let progress;
10557
+ let timedOut = false;
10558
+ if (opts.watch) {
10559
+ ;
10560
+ ({ progress, timedOut } = await watchRun(arbi, conversationId, {
10561
+ intervalMs,
10562
+ timeoutMs,
10563
+ quiet: !!opts.json
10564
+ }));
10565
+ } else progress = await sdk.work.getRunProgress(arbi, conversationId);
10566
+ const answers = opts.answers ? await answersFor(arbi, progress) : {};
10567
+ if (opts.json) printJson({ ...runJson(progress, answers), timed_out: timedOut });
10568
+ else {
10569
+ printRun(progress, answers);
10570
+ if (timedOut) warn(`Stopped watching after ${opts.timeout}s; the run is still going.`);
10571
+ }
10572
+ })()
10573
+ );
10574
+ run.command("step <con-id> <step-id> <action>").description(
10575
+ "Act on a step: complete a person step (a form, or a step with fields, takes --input), skip any step, or send-back the work to an earlier step (--to, --note)"
10576
+ ).option(
10577
+ "--input <name=value>",
10578
+ "An input: a form field or a step field (repeatable)",
10579
+ collect,
10580
+ []
10581
+ ).option("--inputs-file <file>", "Inputs as a JSON or YAML object").option(
10582
+ "--to <step-id>",
10583
+ "send-back: the step to reopen (default: the one this step waits for)"
10584
+ ).option("--note <text>", "send-back: what to change (an agent step runs again with it)").option("--json", "Output as JSON").action(
10585
+ (conversationId, stepId, rawAction, opts) => runAction(async () => {
10586
+ const action = STEP_ACTIONS[rawAction];
10587
+ if (!action)
10588
+ throw new Error(`Unknown action "${rawAction}": use complete, skip or send-back`);
10589
+ if (action !== "send_back" && (opts.to !== void 0 || opts.note !== void 0))
10590
+ throw new Error("--to and --note go with send-back only");
10591
+ const { arbi } = await resolveWorkspace();
10592
+ const before = await sdk.work.getRunProgress(arbi, conversationId);
10593
+ const plan = before.conversation.plan?.steps ?? [];
10594
+ const step = plan.find((s) => s.id === stepId);
10595
+ if (!step)
10596
+ throw new Error(
10597
+ `No step "${stepId}" in ${conversationId}. Its steps: ${plan.map((s) => s.id).join(", ")}`
10598
+ );
10599
+ const fields = sdk.workflowSteps.stepFields(step);
10600
+ const hasInputs = opts.input.length > 0 || !!opts.inputsFile;
10601
+ if (hasInputs && (action !== "complete" || fields.length === 0))
10602
+ throw new Error(
10603
+ "Only completing a form step, or a step with fields, takes --input / --inputs-file"
10604
+ );
10605
+ const here = before.steps.find((s) => s.id === stepId);
10606
+ if (action !== "skip") {
10607
+ if (step.kind === "agent")
10608
+ throw new Error(
10609
+ action === "complete" ? `"${stepId}" is an agent step: it runs itself. Skip it with: arbi run step ${conversationId} ${stepId} skip` : `"${stepId}" is an agent step: send the work back to it from a person step after it (--to ${stepId})`
10610
+ );
10611
+ if (here.phase === "pending")
10612
+ throw new Error(`"${stepId}" waits for ${here.after.join(", ")} to finish first`);
10613
+ if (here.phase === "done" || here.phase === "skipped")
10614
+ throw new Error(`"${stepId}" is already ${here.phase}`);
10615
+ }
10616
+ let inputs;
10617
+ if (action === "complete" && fields.length) {
10618
+ inputs = gatherInputs(opts.input, opts.inputsFile, fields);
10619
+ if (Array.isArray(step.fields)) {
10620
+ const problems = sdk.workflowSteps.checkStepInputs(step, inputs);
10621
+ if (Object.keys(problems).length)
10622
+ throw new Error(
10623
+ `Cannot complete "${stepId}": ${sdk.workflowSteps.describeInputProblems(step, problems)}. ${fieldsHint(fields)}`
10624
+ );
10625
+ } else {
10626
+ const missing = sdk.workflowSteps.missingRequiredFields(step, {
10627
+ ...before.conversation.skill_inputs ?? {},
10628
+ ...inputs
10629
+ });
10630
+ if (missing.length)
10631
+ throw new Error(
10632
+ `Missing required input${missing.length > 1 ? "s" : ""}: ${missing.join(", ")}. Pass ${missing.map((m) => `--input ${m}=\u2026`).join(" ")}`
10633
+ );
10634
+ }
10635
+ }
10636
+ if (action === "send_back") checkSendBack(plan, stepId, opts.to);
10637
+ await sdk.work.actOnStep(arbi, conversationId, {
10638
+ step: stepId,
10639
+ action,
10640
+ ...inputs && Object.keys(inputs).length ? { inputs } : {},
10641
+ ...action === "send_back" && opts.to ? { to: opts.to } : {},
10642
+ ...action === "send_back" && opts.note?.trim() ? { note: opts.note.trim() } : {}
10643
+ });
10644
+ const progress = await sdk.work.getRunProgress(arbi, conversationId);
10645
+ if (opts.json) {
10646
+ printJson(runJson(progress));
10647
+ return;
10648
+ }
10649
+ if (action === "send_back") {
10650
+ const back = progress.sendBacks.at(-1);
10651
+ success(
10652
+ `Sent "${stepId}" of ${ref(conversationId)} back to "${back?.to ?? opts.to ?? "?"}"${opts.note?.trim() ? " with a note" : ""}; reopened ${back?.reopened.join(", ") ?? ""}`
10653
+ );
10654
+ } else
10655
+ success(
10656
+ `${action === "complete" ? "Completed" : "Skipped"} step "${stepId}" of ${ref(conversationId)}`
10657
+ );
10658
+ dim(nextHint(progress, plan));
10659
+ })()
10660
+ );
10661
+ }
9942
10662
  var MARKER_START = "# arbi-cli completion start";
9943
10663
  var MARKER_END = "# arbi-cli completion end";
9944
10664
  var BASH_COMPLETION = `
@@ -10172,12 +10892,12 @@ function printTree(dir, prefix, maxDepth, depth) {
10172
10892
  }
10173
10893
  }
10174
10894
  }
10175
- function humanizeTtl(seconds) {
10176
- if (!Number.isFinite(seconds) || seconds < 0) return "";
10177
- if (seconds < 60) return `${seconds}s`;
10178
- if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
10179
- if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
10180
- return `${Math.round(seconds / 86400)}d`;
10895
+ function humanizeTtl(seconds2) {
10896
+ if (!Number.isFinite(seconds2) || seconds2 < 0) return "";
10897
+ if (seconds2 < 60) return `${seconds2}s`;
10898
+ if (seconds2 < 3600) return `${Math.round(seconds2 / 60)}m`;
10899
+ if (seconds2 < 86400) return `${Math.round(seconds2 / 3600)}h`;
10900
+ return `${Math.round(seconds2 / 86400)}d`;
10181
10901
  }
10182
10902
  function registerSessionCommand(program2) {
10183
10903
  const session = program2.command("session").description("List and manage sessions");
@@ -10239,7 +10959,7 @@ function parseAddress(value) {
10239
10959
  const name = match?.[1].replace(/^"|"$/g, "");
10240
10960
  return name ? { email, name } : { email };
10241
10961
  }
10242
- function collect(value, previous = []) {
10962
+ function collect2(value, previous = []) {
10243
10963
  return [...previous, ...value.split(",").filter((v) => v.trim())];
10244
10964
  }
10245
10965
  var readTextFile = (path8) => fs7__default.default.readFileSync(path8 === "-" ? 0 : path8, "utf8");
@@ -10265,7 +10985,7 @@ function buildCompose(options) {
10265
10985
  };
10266
10986
  }
10267
10987
  function addComposeOptions(command) {
10268
- return command.option("--to <addresses>", "Recipient(s), comma-separated or repeated", collect).option("--cc <addresses>", "Cc recipient(s)", collect).option("--bcc <addresses>", "Bcc recipient(s)", collect).option("-s, --subject <subject>", "Subject").option("--text <text>", "Plain-text body").option("--text-file <path>", "Read the plain-text body from a file (- for stdin)").option("--html <html>", "HTML body (the plain text stays as the fallback)").option("--html-file <path>", "Read the HTML body from a file (- for stdin)").option("-a, --attach <path>", "Attach a file (repeatable)", (v, prev = []) => [
10988
+ return command.option("--to <addresses>", "Recipient(s), comma-separated or repeated", collect2).option("--cc <addresses>", "Cc recipient(s)", collect2).option("--bcc <addresses>", "Bcc recipient(s)", collect2).option("-s, --subject <subject>", "Subject").option("--text <text>", "Plain-text body").option("--text-file <path>", "Read the plain-text body from a file (- for stdin)").option("--html <html>", "HTML body (the plain text stays as the fallback)").option("--html-file <path>", "Read the HTML body from a file (- for stdin)").option("-a, --attach <path>", "Attach a file (repeatable)", (v, prev = []) => [
10269
10989
  ...prev,
10270
10990
  v
10271
10991
  ]).option("--json", "Output as JSON (recommended for scripting / agents)");
@@ -10532,19 +11252,95 @@ async function resolveProjectId(arbi, explicit) {
10532
11252
  }
10533
11253
  return first;
10534
11254
  }
11255
+ var orNull = (value) => value.trim().toLowerCase() === "none" ? null : value;
11256
+ function projectPatch(opts) {
11257
+ const body = {};
11258
+ if (opts.budget !== void 0)
11259
+ body.budget = orNull(opts.budget) === null ? null : sdk.projects.parseBudget(opts.budget);
11260
+ if (opts.client !== void 0) body.client = orNull(opts.client);
11261
+ if (opts.clientRef !== void 0) body.client_ref = orNull(opts.clientRef);
11262
+ if (opts.stage !== void 0) body.stage = orNull(opts.stage);
11263
+ if (opts.field?.length) {
11264
+ const fields = {};
11265
+ for (const pair of opts.field) {
11266
+ const eq = pair.indexOf("=");
11267
+ const key = eq > 0 ? pair.slice(0, eq).trim() : "";
11268
+ if (!/^[a-z][a-z0-9_]{0,63}$/.test(key))
11269
+ throw new Error(
11270
+ `Invalid --field "${pair}": use key=value with a key like "matter_type" (lowercase, digits, "_")`
11271
+ );
11272
+ const value = pair.slice(eq + 1);
11273
+ fields[key] = value.trim() === "" ? null : value;
11274
+ }
11275
+ body.fields = fields;
11276
+ }
11277
+ return body;
11278
+ }
11279
+ async function resolveProject(arbi, selector) {
11280
+ const res = resolveByIdOrName(await sdk.projects.listProjects(arbi), selector);
11281
+ if (!res.ok) failResolveAndExit("Project", selector, res);
11282
+ return res.item;
11283
+ }
11284
+ var PERSON_ID = /^(?:usr|agt)-[a-zA-Z0-9]{8}$/;
11285
+ async function resolveLead(value) {
11286
+ if (orNull(value) === null) return null;
11287
+ if (PERSON_ID.test(value)) return value;
11288
+ if (!value.includes("@"))
11289
+ throw new Error(`--lead takes a person id (usr-\u2026), an email, or none; got "${value}"`);
11290
+ const { arbi } = await resolveWorkspace();
11291
+ const users = await sdk.workspaces.listWorkspaceUsers(arbi);
11292
+ const found = users.find((u) => u.user.email.toLowerCase() === value.toLowerCase());
11293
+ if (!found) {
11294
+ error(
11295
+ `No one with the email ${value} in the open workspace. Pass their id (usr-\u2026), or open a workspace they are in.`
11296
+ );
11297
+ process.exit(3);
11298
+ }
11299
+ return found.user.external_id;
11300
+ }
11301
+ function eventValue(event, side) {
11302
+ const detail = event.detail ?? {};
11303
+ const value = detail[side === "old" ? "from" : "to"];
11304
+ if (value === null || value === void 0) return "\u2014";
11305
+ if (typeof value === "object")
11306
+ return sdk.projects.formatBudget(value) || JSON.stringify(value);
11307
+ return String(value);
11308
+ }
11309
+ function printProjectDetails(p) {
11310
+ label("Budget:", sdk.projects.formatBudget(p.budget) || "\u2014");
11311
+ label("Client:", p.client ?? "\u2014");
11312
+ label("Client ref:", p.client_ref ?? "\u2014");
11313
+ label("Stage:", `${p.stage ?? "\u2014"}${p.stage_changed_at ? ` (since ${p.stage_changed_at})` : ""}`);
11314
+ label("Lead:", p.lead_user ?? "\u2014");
11315
+ const fields = Object.entries(p.fields ?? {});
11316
+ label("Fields:", fields.length ? fields.map(([k, v]) => `${k}=${String(v)}`).join(", ") : "\u2014");
11317
+ }
10535
11318
  function registerProjectCommand(program2) {
10536
- const project = program2.command("project").description("Project tenancy: list, create, rename, update, delete, refresh");
10537
- project.command("list").description("List your projects").option("--json", "Output as JSON").action(
11319
+ const project = program2.command("project").description(
11320
+ "Project tenancy: list, create, rename, update, set (budget, client, stage, lead, fields), show, events, delete, refresh"
11321
+ );
11322
+ project.command("list").description("List your projects (--stage, --lead to filter)").option("--stage <label>", "Only projects at this stage").option("--lead <usr-id|email>", "Only projects led by this person").option("--json", "Output as JSON").action(
10538
11323
  (opts) => runAction(async () => {
10539
11324
  const { arbi } = await resolveAuth();
10540
- const data = await sdk.projects.listProjects(arbi);
11325
+ const leadUser = opts.lead ? await resolveLead(opts.lead) ?? void 0 : void 0;
11326
+ const data = await sdk.projects.listProjects(arbi, { stage: opts.stage, leadUser });
10541
11327
  if (!emitListOrTable(data, opts, "No projects found.")) return;
10542
11328
  printTable(
10543
11329
  [
10544
11330
  { header: "ID", width: 18, value: (r) => r.external_id },
10545
- { header: "NAME", width: 30, value: (r) => r.name ?? "" },
10546
- { header: "PLAN", width: 14, value: (r) => r.plan ?? "" },
10547
- { header: "ROLE", width: 14, value: (r) => r.role ?? "" }
11331
+ { header: "NAME", width: 26, value: (r) => r.name ?? "" },
11332
+ {
11333
+ header: "PLAN",
11334
+ width: 10,
11335
+ value: (r) => r.plan ?? r.subscription ?? ""
11336
+ },
11337
+ { header: "STAGE", width: 16, value: (r) => r.stage ?? "" },
11338
+ {
11339
+ header: "BUDGET",
11340
+ width: 16,
11341
+ value: (r) => sdk.projects.formatBudget(r.budget)
11342
+ },
11343
+ { header: "LEAD", width: 14, value: (r) => r.lead_user ?? "" }
10548
11344
  ],
10549
11345
  data
10550
11346
  );
@@ -10587,6 +11383,69 @@ function registerProjectCommand(program2) {
10587
11383
  success(`Updated project ${ref(id)} (${data.name})`);
10588
11384
  })()
10589
11385
  );
11386
+ project.command("set <project>").description(
11387
+ "Set a project's details: budget, client, client ref, stage, lead and fields (owner only; stage and budget changes become project events)"
11388
+ ).option("--budget <amount+currency>", "Budget, e.g. 6000GBP (none to clear)").option("--client <name>", "Client name (none to clear)").option("--client-ref <id>", "Client record id, e.g. a CRM id (none to clear)").option("--stage <label>", "Stage, e.g. Discovery (none to clear)").option(
11389
+ "--lead <usr-id|email>",
11390
+ "Lead: a member of one of its workspaces, by id or email (none to clear)"
11391
+ ).option("--field <key=value>", "A field (repeatable; key= removes it)", collect, []).option("--json", "Output as JSON").action(
11392
+ (selector, opts) => runAction(async () => {
11393
+ const body = projectPatch(opts);
11394
+ if (Object.keys(body).length === 0 && !opts.lead)
11395
+ throw new Error(
11396
+ "Nothing to set. Pass --budget, --client, --client-ref, --stage, --lead and/or --field"
11397
+ );
11398
+ const { arbi } = await resolveAuth();
11399
+ const target = await resolveProject(arbi, selector);
11400
+ if (opts.lead !== void 0) body.lead_user = await resolveLead(opts.lead);
11401
+ const data = await sdk.projects.updateProject(arbi, target.external_id, body);
11402
+ if (opts.json) {
11403
+ printJson(data);
11404
+ return;
11405
+ }
11406
+ success(`Updated project ${data.name} (${ref(data.external_id)})`);
11407
+ printProjectDetails(data);
11408
+ })()
11409
+ );
11410
+ project.command("show <project>").description("A project's details: budget, client, stage, lead and fields").option("--json", "Output as JSON").action(
11411
+ (selector, opts) => runAction(async () => {
11412
+ const { arbi } = await resolveAuth();
11413
+ const data = await resolveProject(arbi, selector);
11414
+ if (opts.json) {
11415
+ printJson(data);
11416
+ return;
11417
+ }
11418
+ bold(`${data.name} (${data.external_id})`);
11419
+ printProjectDetails(data);
11420
+ })()
11421
+ );
11422
+ project.command("events <project>").description("A project's stage and budget changes, newest first").option("--kind <kind>", "Only stage_changed or budget_changed").option("--limit <n>", "At most this many (default 100)").option("--json", "Output as JSON").action(
11423
+ (selector, opts) => runAction(async () => {
11424
+ if (opts.kind && !["stage_changed", "budget_changed"].includes(opts.kind))
11425
+ throw new Error("--kind must be stage_changed or budget_changed");
11426
+ const limit = opts.limit === void 0 ? void 0 : Number(opts.limit);
11427
+ if (limit !== void 0 && (!Number.isInteger(limit) || limit < 1 || limit > 1e3))
11428
+ throw new Error("--limit must be a whole number from 1 to 1000");
11429
+ const { arbi } = await resolveAuth();
11430
+ const target = await resolveProject(arbi, selector);
11431
+ const events = await sdk.projects.listProjectEvents(arbi, target.external_id, {
11432
+ kind: opts.kind,
11433
+ limit
11434
+ });
11435
+ if (!emitListOrTable(events, opts, `No stage or budget changes on ${target.name} yet.`))
11436
+ return;
11437
+ printTable(
11438
+ [
11439
+ { header: "WHEN", width: 26, value: (r) => r.created_at },
11440
+ { header: "CHANGE", width: 16, value: (r) => r.kind },
11441
+ { header: "FROM", width: 22, value: (r) => eventValue(r, "old") },
11442
+ { header: "TO", width: 22, value: (r) => eventValue(r, "new") },
11443
+ { header: "BY", width: 14, value: (r) => r.actor ?? "" }
11444
+ ],
11445
+ events
11446
+ );
11447
+ })()
11448
+ );
10590
11449
  project.command("delete <id>").description("Delete a project (irreversible; requires an explicit id)").action(
10591
11450
  (id) => runAction(async () => {
10592
11451
  const { arbi } = await resolveAuth();
@@ -10718,10 +11577,104 @@ function registerFilesCommand(program2) {
10718
11577
  await files.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
10719
11578
  });
10720
11579
  }
10721
-
10722
- // src/commands/usage.ts
11580
+ var credits = (n) => `${n.toLocaleString("en", { maximumFractionDigits: 2 })} credits`;
11581
+ function breakdownRows(breakdown, name = (k) => k) {
11582
+ return Object.entries(breakdown).sort(([, a], [, b]) => b.spend - a.spend).map(([key, b]) => [
11583
+ name(key),
11584
+ credits(b.spend),
11585
+ b.total_tokens.toLocaleString("en"),
11586
+ String(b.requests)
11587
+ ]);
11588
+ }
11589
+ function printBreakdown(title, breakdown, name) {
11590
+ console.log("");
11591
+ bold(title);
11592
+ const rows = breakdownRows(breakdown, name);
11593
+ if (!rows.length) {
11594
+ dim(" (nothing recorded)");
11595
+ return;
11596
+ }
11597
+ for (const line of renderTable(["", "SPEND", "TOKENS", "REQUESTS"], rows, 40))
11598
+ console.log(` ${line}`);
11599
+ }
10723
11600
  function registerUsageCommand(program2) {
10724
- const usage = program2.command("usage").description("Show today's usage against the daily cap");
11601
+ const usage = program2.command("usage").description(
11602
+ "Usage and spend: today's against the daily cap, a conversation's by step, model and agent, a project's by conversation"
11603
+ );
11604
+ usage.command("conversation <con-id>").description(
11605
+ "What a conversation's AI work cost this billing period: by step, model and agent (also by run and feature in --json)"
11606
+ ).option("--json", "Output as JSON").action(
11607
+ (conversationId, opts) => runAction(async () => {
11608
+ const { arbi } = await resolveWorkspace();
11609
+ const data = await sdk.work.getConversationUsage(arbi, conversationId);
11610
+ if (opts.json) {
11611
+ printJson(data);
11612
+ return;
11613
+ }
11614
+ bold(`Conversation ${data.conversation_ext_id} (project ${data.project_ext_id})`);
11615
+ label("Period:", `${data.period_start} \u2192 ${data.period_end}`);
11616
+ label("Spend:", credits(data.total_spend));
11617
+ label("Tokens:", data.total_tokens.toLocaleString("en"));
11618
+ label("Requests:", String(data.total_requests));
11619
+ printBreakdown("By step", data.by_step);
11620
+ printBreakdown(
11621
+ "By run (each run of the thread: a send-back runs a step again)",
11622
+ data.by_run
11623
+ );
11624
+ printBreakdown("By model", data.by_model);
11625
+ printBreakdown("By agent", data.by_agent);
11626
+ })()
11627
+ );
11628
+ usage.command("project <project>").description(
11629
+ "A project's spend this billing period (--by-conversation: per conversation, plus what none accounts for)"
11630
+ ).option("--by-conversation", "Break the spend down by conversation").option("--months-back <n>", "0 = this billing period (default), 1 = the one before, \u2026").option("--json", "Output as JSON").action(
11631
+ (selector, opts) => runAction(async () => {
11632
+ const monthsBack = opts.monthsBack === void 0 ? void 0 : Number(opts.monthsBack);
11633
+ if (monthsBack !== void 0 && (!Number.isInteger(monthsBack) || monthsBack < 0))
11634
+ throw new Error("--months-back must be a whole number, 0 or more");
11635
+ const { arbi } = await resolveAuth();
11636
+ const res = resolveByIdOrName(await sdk.projects.listProjects(arbi), selector);
11637
+ if (!res.ok) failResolveAndExit("Project", selector, res);
11638
+ const projectId = res.item.external_id;
11639
+ if (!opts.byConversation) {
11640
+ const data2 = await sdk.projects.getProjectUsage(arbi, projectId, monthsBack);
11641
+ if (opts.json) {
11642
+ printJson(data2);
11643
+ return;
11644
+ }
11645
+ bold(`Project ${res.item.name} (${projectId})`);
11646
+ label("Period:", `${data2.period_start} \u2192 ${data2.period_end}`);
11647
+ label("Spend:", credits(data2.total_spend));
11648
+ label("Tokens:", data2.total_tokens.toLocaleString("en"));
11649
+ dim(`By conversation: arbi usage project ${projectId} --by-conversation`);
11650
+ return;
11651
+ }
11652
+ const data = await sdk.projects.getProjectConversationUsage(arbi, projectId, monthsBack);
11653
+ if (opts.json) {
11654
+ printJson(data);
11655
+ return;
11656
+ }
11657
+ const titles = /* @__PURE__ */ new Map();
11658
+ if (getConfig()?.selectedWorkspaceId)
11659
+ try {
11660
+ const { arbi: ws } = await resolveWorkspace();
11661
+ for (const c of await sdk.conversations.listConversations(ws))
11662
+ if (c.title) titles.set(c.external_id, c.title);
11663
+ } catch {
11664
+ }
11665
+ bold(`Project ${res.item.name} (${projectId})`);
11666
+ label("Period:", `${data.period_start} \u2192 ${data.period_end}`);
11667
+ label("Spend:", credits(data.total_spend));
11668
+ printBreakdown(
11669
+ "By conversation",
11670
+ data.by_conversation,
11671
+ (id) => titles.has(id) ? `${id} ${titles.get(id)}` : id
11672
+ );
11673
+ const u = data.unattributed;
11674
+ if (u.spend || u.requests)
11675
+ dim(`Not in any conversation: ${credits(u.spend)}, ${u.requests} requests`);
11676
+ })()
11677
+ );
10725
11678
  usage.command("today").description("Today's tokens used / remaining (for the current user)").option("--json", "Output as JSON").action(
10726
11679
  (opts) => runAction(async () => {
10727
11680
  const { arbi } = await resolveAuth();
@@ -10997,7 +11950,7 @@ console.info = (...args) => {
10997
11950
  _origInfo(...args);
10998
11951
  };
10999
11952
  var program = new commander.Command();
11000
- program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.4.75").showHelpAfterError(true).showSuggestionAfterError(true);
11953
+ program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.4.77").showHelpAfterError(true).showSuggestionAfterError(true);
11001
11954
  registerConfigCommand(program);
11002
11955
  registerLoginCommand(program);
11003
11956
  registerRegisterCommand(program);
@@ -11027,6 +11980,7 @@ registerAgentCreateCommand(program);
11027
11980
  registerAgentCommand(program);
11028
11981
  registerTaskCommand(program);
11029
11982
  registerWorkflowCommand(program);
11983
+ registerRunCommand(program);
11030
11984
  registerCompletionCommand(program);
11031
11985
  registerListenCommand(program);
11032
11986
  registerSessionCommand(program);
@@ -11053,7 +12007,7 @@ applyHelpGroups(program, {
11053
12007
  "Search & ask:": ["ask", "find", "cite", "watch"],
11054
12008
  "Tagging:": ["tags", "doctags"],
11055
12009
  "Conversations & messaging:": ["convo", "dm", "listen", "notifications", "contacts"],
11056
- "Agents & tasks:": ["agent", "agent-create", "task", "workflow", "agentconfig", "session"],
12010
+ "Agents & tasks:": ["agent", "agent-create", "task", "workflow", "run", "agentconfig", "session"],
11057
12011
  "Account:": ["auth", "usage", "files"],
11058
12012
  "System:": ["health", "models", "settings", "tui", "local", "update"]
11059
12013
  });