@arbidocs/cli 0.4.76 → 0.4.78

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,8 +12,8 @@ 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 module$1 = require('module');
16
15
  var yaml = require('yaml');
16
+ var module$1 = require('module');
17
17
 
18
18
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
19
19
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
@@ -3695,7 +3695,7 @@ function getLatestVersion(skipCache = false) {
3695
3695
  }
3696
3696
  }
3697
3697
  function getCurrentVersion() {
3698
- return "0.4.76";
3698
+ return "0.4.78";
3699
3699
  }
3700
3700
  function readChangelog(fromVersion, toVersion) {
3701
3701
  try {
@@ -3748,17 +3748,17 @@ function showChangelog(fromVersion, toVersion) {
3748
3748
  async function checkForUpdates(autoUpdate) {
3749
3749
  try {
3750
3750
  const latest = getLatestVersion();
3751
- if (!latest || latest === "0.4.76") return;
3751
+ if (!latest || latest === "0.4.78") return;
3752
3752
  if (autoUpdate) {
3753
3753
  warn(`
3754
- Your arbi version is out of date (${"0.4.76"} \u2192 ${latest}). Updating...`);
3754
+ Your arbi version is out of date (${"0.4.78"} \u2192 ${latest}). Updating...`);
3755
3755
  child_process.execSync("npm install -g @arbidocs/cli@latest", { stdio: "inherit" });
3756
- showChangelog("0.4.76", latest);
3756
+ showChangelog("0.4.78", latest);
3757
3757
  console.log(`Updated to ${latest}.`);
3758
3758
  } else {
3759
3759
  warn(
3760
3760
  `
3761
- Your arbi version is out of date (${"0.4.76"} \u2192 ${latest}).
3761
+ Your arbi version is out of date (${"0.4.78"} \u2192 ${latest}).
3762
3762
  Run "arbi update" to upgrade, or "arbi update auto" to always stay up to date.`
3763
3763
  );
3764
3764
  }
@@ -3790,10 +3790,10 @@ function markNagShown(latest) {
3790
3790
  function hintUpdateOnError() {
3791
3791
  try {
3792
3792
  const cached = readCache();
3793
- if (!cached || cached.latest === "0.4.76") return;
3793
+ if (!cached || cached.latest === "0.4.78") return;
3794
3794
  if (!shouldShowNag(cached.latest)) return;
3795
3795
  warn(
3796
- `Your arbi version is out of date (${"0.4.76"} \u2192 ${cached.latest}). Run "arbi update".`
3796
+ `Your arbi version is out of date (${"0.4.78"} \u2192 ${cached.latest}). Run "arbi update".`
3797
3797
  );
3798
3798
  markNagShown(cached.latest);
3799
3799
  } catch {
@@ -3964,6 +3964,10 @@ function stringifyApiDetail(value) {
3964
3964
  const rec = value;
3965
3965
  if (typeof rec.detail === "string") return rec.detail;
3966
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
+ }
3967
3971
  if (typeof rec.message === "string") return rec.message;
3968
3972
  if (typeof rec.error === "string") return rec.error;
3969
3973
  try {
@@ -8473,6 +8477,174 @@ function registerTimelineCommand(program2) {
8473
8477
  );
8474
8478
  }
8475
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
+ }
8476
8648
  async function fetchTagChoices(arbi) {
8477
8649
  const data = await sdk.tags.listTags(arbi);
8478
8650
  if (data.length === 0) {
@@ -8489,7 +8661,7 @@ async function fetchTagChoices(arbi) {
8489
8661
  };
8490
8662
  }
8491
8663
  function registerTagsCommand(program2) {
8492
- 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");
8493
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(
8494
8666
  (opts) => runAction(async () => {
8495
8667
  const { arbi } = await resolveWorkspace(opts.workspace);
@@ -8512,7 +8684,8 @@ function registerTagsCommand(program2) {
8512
8684
  value: (r) => r.tag_type?.type ?? ""
8513
8685
  },
8514
8686
  { header: "DOCS", width: 6, value: (r) => String(r.doctag_count) },
8515
- { 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 ?? "" }
8516
8689
  ],
8517
8690
  data
8518
8691
  );
@@ -8522,9 +8695,10 @@ function registerTagsCommand(program2) {
8522
8695
  "-t, --type <type>",
8523
8696
  "Tag type (checkbox, text, number, select, folder, search, date, assistant)",
8524
8697
  "checkbox"
8525
- ).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(
8526
8699
  (name, opts) => runAction(async () => {
8527
8700
  const { arbi } = await resolveWorkspace(opts.workspace);
8701
+ const parent = opts.parent ? await resolveTag(arbi, opts.parent) : void 0;
8528
8702
  const interactive = !name;
8529
8703
  if (!name) {
8530
8704
  name = await promptInput("Tag name");
@@ -8548,9 +8722,16 @@ function registerTagsCommand(program2) {
8548
8722
  options: []
8549
8723
  },
8550
8724
  instruction,
8551
- shared
8725
+ shared,
8726
+ parentId: parent?.external_id ?? null
8552
8727
  });
8553
- 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
+ );
8554
8735
  })()
8555
8736
  );
8556
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(
@@ -8615,6 +8796,95 @@ function registerTagsCommand(program2) {
8615
8796
  success(`Updated: ${data.name} (${ref(data.external_id)})`);
8616
8797
  })()
8617
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
+ );
8618
8888
  tags.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
8619
8889
  const tail = tags.args ?? [];
8620
8890
  await tags.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
@@ -9797,41 +10067,33 @@ function registerTaskCommand(program2) {
9797
10067
  })()
9798
10068
  );
9799
10069
  }
9800
- function readStructured(path8) {
9801
- let text;
9802
- try {
9803
- text = fs7.readFileSync(path8, "utf8");
9804
- } catch (err) {
9805
- throw new Error(`Cannot read ${path8}: ${err.message}`);
9806
- }
9807
- const ext = path6.extname(path8).toLowerCase();
9808
- try {
9809
- return ext === ".json" ? JSON.parse(text) : yaml.parse(text);
9810
- } catch (err) {
10070
+ function signalTrigger(name, eventKind, eventFilter) {
10071
+ if (name === void 0) return void 0;
10072
+ if (!sdk.workflowSteps.SIGNAL_NAME_PATTERN.test(name))
9811
10073
  throw new Error(
9812
- `${path8} is not valid ${ext === ".json" ? "JSON" : "YAML"}: ${err.message}`
10074
+ `Invalid --on-signal "${name}": lowercase letters, digits, ".", "_" or "-", starting with a letter or digit (e.g. risk-high)`
9813
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
+ }
9814
10085
  }
10086
+ return { event_kind: "workflow_signal", event_filter: { ...filter, signal: name } };
9815
10087
  }
9816
- function readStepsFile(path8) {
9817
- try {
9818
- return sdk.workflowSteps.parsePlanSteps(readStructured(path8));
9819
- } catch (err) {
9820
- if (err instanceof sdk.PlanStepsError) throw new Error(`${path8}: ${err.message}`);
9821
- throw err;
9822
- }
9823
- }
9824
- function readInputsFile(path8) {
9825
- const value = readStructured(path8);
9826
- if (typeof value !== "object" || value === null || Array.isArray(value))
9827
- throw new Error(`${path8}: expected an object of input values, e.g. {"client": "Acme"}`);
9828
- return value;
9829
- }
9830
- function collect(value, previous = []) {
9831
- return [...previous, value];
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";
9832
10096
  }
9833
-
9834
- // src/commands/workflow.ts
9835
10097
  function registerWorkflowCommand(program2) {
9836
10098
  const workflow = program2.command("workflow").alias("automation").description(
9837
10099
  "Workflows (/v1/workflow): create (optionally with steps), list, update, resume, pause, run, delete"
@@ -9841,7 +10103,10 @@ function registerWorkflowCommand(program2) {
9841
10103
  ).requiredOption("--name <name>", "Workflow name (required)").option(
9842
10104
  "--steps <file>",
9843
10105
  "Named steps (form/upload/agent/review) as a TaskPlanStep list in .json, .yaml or .yml"
9844
- ).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(
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(
9845
10110
  "--auth-mode <mode>",
9846
10111
  "scheduled_require_auth or scheduled_permanent",
9847
10112
  "scheduled_require_auth"
@@ -9849,6 +10114,11 @@ function registerWorkflowCommand(program2) {
9849
10114
  (opts) => runAction(async () => {
9850
10115
  if (opts.steps && (opts.prompt || opts.skill || opts.action))
9851
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
+ }
9852
10122
  const steps = opts.steps ? readStepsFile(opts.steps) : void 0;
9853
10123
  const { arbi, workspaceId } = await resolveWorkspace();
9854
10124
  const body = {
@@ -9900,6 +10170,17 @@ function registerWorkflowCommand(program2) {
9900
10170
  success(
9901
10171
  `Created workflow: ${data.name} (${ref(data.external_id)})${steps ? ` with ${steps.length} steps` : ""}`
9902
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
+ );
9903
10184
  if (steps) dim(`Run it: arbi workflow run ${data.external_id}`);
9904
10185
  })()
9905
10186
  );
@@ -9914,6 +10195,11 @@ function registerWorkflowCommand(program2) {
9914
10195
  { header: "NAME", width: 28, value: (r) => r.name ?? "" },
9915
10196
  { header: "ACTIVE", width: 7, value: (r) => r.active ? "yes" : "no" },
9916
10197
  { header: "SHARED", width: 8, value: (r) => r.shared ? "yes" : "no" },
10198
+ {
10199
+ header: "TRIGGER",
10200
+ width: 26,
10201
+ value: (r) => triggerText(r)
10202
+ },
9917
10203
  { header: "NEXT RUN", width: 26, value: (r) => r.next_fire_at ?? "" },
9918
10204
  { header: "LAST RUN", width: 26, value: (r) => r.last_fired_at ?? "" }
9919
10205
  ],
@@ -9983,11 +10269,14 @@ function registerWorkflowCommand(program2) {
9983
10269
  );
9984
10270
  })()
9985
10271
  );
9986
- 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("--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(
9987
10273
  (id, opts) => runAction(async () => {
9988
- if (!opts.file && !opts.steps) throw new Error("Pass --steps <file> and/or --file <path>");
10274
+ if (!opts.file && !opts.steps && !opts.onSignal)
10275
+ throw new Error("Pass --steps <file>, --on-signal <name> and/or --file <path>");
9989
10276
  const steps = opts.steps ? readStepsFile(opts.steps) : void 0;
9990
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 });
9991
10280
  if (steps)
9992
10281
  Object.assign(body, {
9993
10282
  plan: sdk.workflowSteps.stepsPlan(steps),
@@ -10014,6 +10303,8 @@ function runJson(progress, answers = {}) {
10014
10303
  return {
10015
10304
  conversation_ext_id: c.external_id,
10016
10305
  title: c.title,
10306
+ workflow_ext_id: c.workflow_ext_id ?? null,
10307
+ parent_conversation_ext_id: c.parent_conversation_ext_id ?? null,
10017
10308
  waiting_on: progress.waitingOn,
10018
10309
  in_progress: c.in_progress,
10019
10310
  pending_auth_request_id: c.pending_auth_request_id ?? null,
@@ -10026,10 +10317,62 @@ function runJson(progress, answers = {}) {
10026
10317
  after: s.after,
10027
10318
  answer_id: s.responseId,
10028
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),
10029
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
10030
10338
  }))
10031
10339
  };
10032
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
+ }
10033
10376
  function nextHint(progress, planSteps = []) {
10034
10377
  const id = progress.conversation.external_id;
10035
10378
  if (progress.conversation.pending_auth_request_id)
@@ -10049,7 +10392,10 @@ function nextHint(progress, planSteps = []) {
10049
10392
  (f) => have[f.name] === void 0 || have[f.name] === null || have[f.name] === ""
10050
10393
  );
10051
10394
  const inputs = fields.map((f) => ` --input ${f.name}=\u2026`).join("");
10052
- return `Waiting on ${step.kind} step "${step.id}": arbi run step ${id} ${step.id} complete${inputs}`;
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;
10053
10399
  }
10054
10400
  default: {
10055
10401
  const failed = progress.steps.find((s) => s.phase === "failed");
@@ -10060,16 +10406,27 @@ function nextHint(progress, planSteps = []) {
10060
10406
  function printRun(progress, answers) {
10061
10407
  const c = progress.conversation;
10062
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
+ );
10063
10413
  printTable(
10064
10414
  [
10065
10415
  { header: "STEP", width: 16, value: (r) => r.id },
10066
10416
  { header: "KIND", width: 8, value: (r) => r.kind },
10067
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
+ },
10068
10423
  { header: "ANSWER", width: 16, value: (r) => r.responseId ?? "" },
10069
- { header: "TITLE", width: 40, value: (r) => r.title }
10424
+ { header: "NOTES", width: 40, value: (r) => stepNotes(r) },
10425
+ { header: "TITLE", width: 32, value: (r) => r.title }
10070
10426
  ],
10071
10427
  progress.steps
10072
10428
  );
10429
+ for (const line of runEventLines(progress)) dim(line);
10073
10430
  for (const s of progress.steps) {
10074
10431
  if (s.phase === "failed" && s.error) warn(`${s.id}: ${s.error}`);
10075
10432
  if (!s.responseId || answers[s.responseId] === void 0) continue;
@@ -10110,6 +10467,48 @@ function seconds(value, flag) {
10110
10467
  if (!Number.isFinite(n) || n <= 0) throw new Error(`${flag} must be a positive number of seconds`);
10111
10468
  return n * 1e3;
10112
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
+ }
10113
10512
  function gatherInputs(pairs, file, fields) {
10114
10513
  return {
10115
10514
  ...file ? readInputsFile(file) : {},
@@ -10173,11 +10572,22 @@ function registerRunCommand(program2) {
10173
10572
  })()
10174
10573
  );
10175
10574
  run.command("step <con-id> <step-id> <action>").description(
10176
- "Act on a step: complete a form, upload or review step (a form takes its inputs), or skip any step"
10177
- ).option("--input <name=value>", "A form input (repeatable)", collect, []).option("--inputs-file <file>", "Form inputs as a JSON or YAML object").option("--json", "Output as JSON").action(
10178
- (conversationId, stepId, action, opts) => runAction(async () => {
10179
- if (action !== "complete" && action !== "skip")
10180
- throw new Error(`Unknown action "${action}": use complete or skip`);
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");
10181
10591
  const { arbi } = await resolveWorkspace();
10182
10592
  const before = await sdk.work.getRunProgress(arbi, conversationId);
10183
10593
  const plan = before.conversation.plan?.steps ?? [];
@@ -10186,22 +10596,33 @@ function registerRunCommand(program2) {
10186
10596
  throw new Error(
10187
10597
  `No step "${stepId}" in ${conversationId}. Its steps: ${plan.map((s) => s.id).join(", ")}`
10188
10598
  );
10599
+ const fields = sdk.workflowSteps.stepFields(step);
10189
10600
  const hasInputs = opts.input.length > 0 || !!opts.inputsFile;
10190
- if (hasInputs && (action === "skip" || step.kind !== "form"))
10191
- throw new Error("Only completing a form step takes --input / --inputs-file");
10192
- let inputs;
10193
- if (action === "complete") {
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") {
10194
10607
  if (step.kind === "agent")
10195
10608
  throw new Error(
10196
- `"${stepId}" is an agent step: it runs itself. Skip it with: arbi run step ${conversationId} ${stepId} skip`
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})`
10197
10610
  );
10198
- const phase = before.steps.find((s) => s.id === stepId)?.phase;
10199
- if (phase === "pending") {
10200
- const waits = before.steps.find((s) => s.id === stepId)?.after ?? [];
10201
- throw new Error(`"${stepId}" waits for ${waits.join(", ")} to finish first`);
10202
- }
10203
- if (step.kind === "form") {
10204
- inputs = gatherInputs(opts.input, opts.inputsFile, sdk.workflowSteps.stepFields(step));
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 {
10205
10626
  const missing = sdk.workflowSteps.missingRequiredFields(step, {
10206
10627
  ...before.conversation.skill_inputs ?? {},
10207
10628
  ...inputs
@@ -10212,19 +10633,28 @@ function registerRunCommand(program2) {
10212
10633
  );
10213
10634
  }
10214
10635
  }
10636
+ if (action === "send_back") checkSendBack(plan, stepId, opts.to);
10215
10637
  await sdk.work.actOnStep(arbi, conversationId, {
10216
10638
  step: stepId,
10217
10639
  action,
10218
- ...inputs && Object.keys(inputs).length ? { inputs } : {}
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() } : {}
10219
10643
  });
10220
10644
  const progress = await sdk.work.getRunProgress(arbi, conversationId);
10221
10645
  if (opts.json) {
10222
10646
  printJson(runJson(progress));
10223
10647
  return;
10224
10648
  }
10225
- success(
10226
- `${action === "complete" ? "Completed" : "Skipped"} step "${stepId}" of ${ref(conversationId)}`
10227
- );
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
+ );
10228
10658
  dim(nextHint(progress, plan));
10229
10659
  })()
10230
10660
  );
@@ -10822,19 +11252,95 @@ async function resolveProjectId(arbi, explicit) {
10822
11252
  }
10823
11253
  return first;
10824
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
+ }
10825
11318
  function registerProjectCommand(program2) {
10826
- const project = program2.command("project").description("Project tenancy: list, create, rename, update, delete, refresh");
10827
- 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(
10828
11323
  (opts) => runAction(async () => {
10829
11324
  const { arbi } = await resolveAuth();
10830
- 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 });
10831
11327
  if (!emitListOrTable(data, opts, "No projects found.")) return;
10832
11328
  printTable(
10833
11329
  [
10834
11330
  { header: "ID", width: 18, value: (r) => r.external_id },
10835
- { header: "NAME", width: 30, value: (r) => r.name ?? "" },
10836
- { header: "PLAN", width: 14, value: (r) => r.plan ?? "" },
10837
- { 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 ?? "" }
10838
11344
  ],
10839
11345
  data
10840
11346
  );
@@ -10877,6 +11383,69 @@ function registerProjectCommand(program2) {
10877
11383
  success(`Updated project ${ref(id)} (${data.name})`);
10878
11384
  })()
10879
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
+ );
10880
11449
  project.command("delete <id>").description("Delete a project (irreversible; requires an explicit id)").action(
10881
11450
  (id) => runAction(async () => {
10882
11451
  const { arbi } = await resolveAuth();
@@ -11008,10 +11577,104 @@ function registerFilesCommand(program2) {
11008
11577
  await files.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
11009
11578
  });
11010
11579
  }
11011
-
11012
- // 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
+ }
11013
11600
  function registerUsageCommand(program2) {
11014
- 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
+ );
11015
11678
  usage.command("today").description("Today's tokens used / remaining (for the current user)").option("--json", "Output as JSON").action(
11016
11679
  (opts) => runAction(async () => {
11017
11680
  const { arbi } = await resolveAuth();
@@ -11287,7 +11950,7 @@ console.info = (...args) => {
11287
11950
  _origInfo(...args);
11288
11951
  };
11289
11952
  var program = new commander.Command();
11290
- program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.4.76").showHelpAfterError(true).showSuggestionAfterError(true);
11953
+ program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.4.78").showHelpAfterError(true).showSuggestionAfterError(true);
11291
11954
  registerConfigCommand(program);
11292
11955
  registerLoginCommand(program);
11293
11956
  registerRegisterCommand(program);