@arbidocs/cli 0.3.62 → 0.3.64

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
@@ -1,16 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ var prompts = require('@inquirer/prompts');
4
5
  var commander = require('commander');
5
6
  var fs5 = require('fs');
6
7
  var path5 = require('path');
7
- var os = require('os');
8
+ var os2 = require('os');
8
9
  var chalk2 = require('chalk');
9
10
  var sdk = require('@arbidocs/sdk');
10
- var prompts = require('@inquirer/prompts');
11
11
  var child_process = require('child_process');
12
12
  var client = require('@arbidocs/client');
13
13
  var url = require('url');
14
+ var crypto$1 = require('crypto');
14
15
  var module$1 = require('module');
15
16
 
16
17
  var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
@@ -18,11 +19,73 @@ function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
18
19
 
19
20
  var fs5__default = /*#__PURE__*/_interopDefault(fs5);
20
21
  var path5__default = /*#__PURE__*/_interopDefault(path5);
21
- var os__default = /*#__PURE__*/_interopDefault(os);
22
+ var os2__default = /*#__PURE__*/_interopDefault(os2);
22
23
  var chalk2__default = /*#__PURE__*/_interopDefault(chalk2);
23
24
 
25
+ var __defProp = Object.defineProperty;
26
+ var __getOwnPropNames = Object.getOwnPropertyNames;
27
+ var __esm = (fn, res) => function __init() {
28
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
29
+ };
30
+ var __export = (target, all) => {
31
+ for (var name in all)
32
+ __defProp(target, name, { get: all[name], enumerable: true });
33
+ };
34
+
35
+ // src/prompts.ts
36
+ var prompts_exports = {};
37
+ __export(prompts_exports, {
38
+ checkbox: () => prompts.checkbox,
39
+ confirm: () => prompts.confirm,
40
+ input: () => prompts.input,
41
+ password: () => prompts.password,
42
+ promptCheckbox: () => promptCheckbox,
43
+ promptConfirm: () => promptConfirm,
44
+ promptInput: () => promptInput,
45
+ promptPassword: () => promptPassword,
46
+ promptSearch: () => promptSearch,
47
+ promptSelect: () => promptSelect,
48
+ search: () => prompts.search,
49
+ select: () => prompts.select
50
+ });
51
+ async function promptSelect(message, choices) {
52
+ return prompts.select({ message, choices });
53
+ }
54
+ async function promptCheckbox(message, choices) {
55
+ return prompts.checkbox({ message, choices });
56
+ }
57
+ async function promptSearch(message, choices) {
58
+ return prompts.search({
59
+ message,
60
+ source: async (term) => {
61
+ if (!term) return choices;
62
+ const lower = term.toLowerCase();
63
+ return choices.filter((c) => c.name.toLowerCase().includes(lower));
64
+ }
65
+ });
66
+ }
67
+ async function promptInput(message, required = true) {
68
+ return prompts.input({
69
+ message,
70
+ validate: required ? (v) => v.trim() ? true : "Required" : void 0
71
+ });
72
+ }
73
+ async function promptPassword(message) {
74
+ return prompts.password({
75
+ message,
76
+ mask: "*",
77
+ validate: (v) => v ? true : "Required"
78
+ });
79
+ }
80
+ async function promptConfirm(message, defaultValue = true) {
81
+ return prompts.confirm({ message, default: defaultValue });
82
+ }
83
+ var init_prompts = __esm({
84
+ "src/prompts.ts"() {
85
+ }
86
+ });
24
87
  function getCacheFile() {
25
- const configDir = process.env.ARBI_CONFIG_DIR ?? path5__default.default.join(os__default.default.homedir(), ".arbi");
88
+ const configDir = process.env.ARBI_CONFIG_DIR ?? path5__default.default.join(os2__default.default.homedir(), ".arbi");
26
89
  return path5__default.default.join(configDir, "completions.json");
27
90
  }
28
91
  function ensureDir(filePath) {
@@ -40,6 +103,16 @@ function getCachedWorkspaceIds() {
40
103
  return [];
41
104
  }
42
105
  }
106
+ function getCachedWorkspaceName(id) {
107
+ try {
108
+ const content = fs5__default.default.readFileSync(getCacheFile(), "utf-8");
109
+ const cache = JSON.parse(content);
110
+ const hit = cache.workspaces.find((w) => w.id === id);
111
+ return hit?.name?.trim() ? hit.name : null;
112
+ } catch {
113
+ return null;
114
+ }
115
+ }
43
116
  function updateCompletionCache(workspaces3) {
44
117
  const cache = {
45
118
  workspaces: workspaces3.filter((w) => w.external_id).map((w) => ({ id: w.external_id, name: w.name ?? "" })),
@@ -194,8 +267,8 @@ var ALIAS_LINE = 'alias A="arbi ask"';
194
267
  var ALIAS_MARKER = "# arbi-cli alias";
195
268
  function getShellRcPath() {
196
269
  const shell = process.env.SHELL || "";
197
- if (shell.includes("zsh")) return path5.join(os.homedir(), ".zshrc");
198
- return path5.join(os.homedir(), ".bashrc");
270
+ if (shell.includes("zsh")) return path5.join(os2.homedir(), ".zshrc");
271
+ return path5.join(os2.homedir(), ".bashrc");
199
272
  }
200
273
  function isAliasInstalled(rcPath) {
201
274
  if (!fs5.existsSync(rcPath)) return false;
@@ -203,7 +276,7 @@ function isAliasInstalled(rcPath) {
203
276
  return content.includes(ALIAS_LINE) || content.includes(ALIAS_MARKER);
204
277
  }
205
278
  function registerConfigCommand(program2) {
206
- const config = program2.command("config").description("Manage CLI configuration");
279
+ const config = program2.command("config").description("CLI configuration: set-url, show, notifications, verbose, watch, alias");
207
280
  config.command("set-url <url>").description("Set the ARBI server URL").action((url) => {
208
281
  try {
209
282
  const parsed = new URL(url);
@@ -219,7 +292,17 @@ function registerConfigCommand(program2) {
219
292
  config.command("show").description("Show current configuration").option("--json", "Output as JSON").action((opts) => {
220
293
  const cfg = getConfig();
221
294
  if (opts.json) {
222
- console.log(JSON.stringify(cfg ?? {}, null, 2));
295
+ const projection = cfg ? {
296
+ baseUrl: cfg.baseUrl ?? null,
297
+ deploymentDomain: cfg.deploymentDomain ?? null,
298
+ autoUpdate: cfg.autoUpdate ?? false,
299
+ verbose: cfg.verbose !== false,
300
+ watch: cfg.watch !== false,
301
+ notifications: cfg.notifications !== false,
302
+ orchestrator: cfg.orchestrator ?? null,
303
+ selectedWorkspaceId: cfg.selectedWorkspaceId ?? null
304
+ } : {};
305
+ console.log(JSON.stringify(projection, null, 2));
223
306
  return;
224
307
  }
225
308
  if (!cfg) {
@@ -232,6 +315,7 @@ function registerConfigCommand(program2) {
232
315
  label("Verbose:", cfg.verbose !== false ? "on" : "off");
233
316
  label("Watch:", cfg.watch !== false ? "on" : "off");
234
317
  label("Notifications:", cfg.notifications !== false ? "on" : "off");
318
+ if (cfg.orchestrator) label("Orchestrator:", cfg.orchestrator);
235
319
  if (cfg.selectedWorkspaceId) {
236
320
  label("Workspace:", cfg.selectedWorkspaceId);
237
321
  }
@@ -275,7 +359,7 @@ function registerConfigCommand(program2) {
275
359
  updateConfig({ watch: toggle === "on" });
276
360
  success(`Watch: ${toggle}`);
277
361
  });
278
- config.command("alias").description('Set up shell alias A for "arbi ask"').action(() => {
362
+ config.command("alias").description('Append `alias A="arbi ask"` to your shell rc file (idempotent, reversible)').action(() => {
279
363
  const rcPath = getShellRcPath();
280
364
  if (isAliasInstalled(rcPath)) {
281
365
  success(`Alias already set up in ${rcPath}`);
@@ -3255,7 +3339,7 @@ var waitForOthersClosedDelete = (databases, name, openDatabases, cb) => {
3255
3339
  };
3256
3340
  var deleteDatabase = (databases, connectionQueues, name, request, cb) => {
3257
3341
  const deleteDBTask = () => {
3258
- return new Promise((resolve3) => {
3342
+ return new Promise((resolve5) => {
3259
3343
  const db = databases.get(name);
3260
3344
  const oldVersion = db !== void 0 ? db.version : 0;
3261
3345
  const onComplete = (err) => {
@@ -3266,7 +3350,7 @@ var deleteDatabase = (databases, connectionQueues, name, request, cb) => {
3266
3350
  cb(null, oldVersion);
3267
3351
  }
3268
3352
  } finally {
3269
- resolve3();
3353
+ resolve5();
3270
3354
  }
3271
3355
  };
3272
3356
  try {
@@ -3414,7 +3498,7 @@ var runVersionchangeTransaction = (connection, version, request, cb) => {
3414
3498
  };
3415
3499
  var openDatabase = (databases, connectionQueues, name, version, request, cb) => {
3416
3500
  const openDBTask = () => {
3417
- return new Promise((resolve3) => {
3501
+ return new Promise((resolve5) => {
3418
3502
  const onComplete = (err) => {
3419
3503
  try {
3420
3504
  if (err) {
@@ -3423,7 +3507,7 @@ var openDatabase = (databases, connectionQueues, name, version, request, cb) =>
3423
3507
  cb(null, connection);
3424
3508
  }
3425
3509
  } finally {
3426
- resolve3();
3510
+ resolve5();
3427
3511
  }
3428
3512
  };
3429
3513
  let db = databases.get(name);
@@ -3572,39 +3656,8 @@ Object.defineProperties(globalVar, {
3572
3656
  IDBTransaction: createPropertyDescriptor(FDBTransaction_default),
3573
3657
  IDBVersionChangeEvent: createPropertyDescriptor(FDBVersionChangeEvent_default)
3574
3658
  });
3575
- async function promptSelect(message, choices) {
3576
- return prompts.select({ message, choices });
3577
- }
3578
- async function promptCheckbox(message, choices) {
3579
- return prompts.checkbox({ message, choices });
3580
- }
3581
- async function promptSearch(message, choices) {
3582
- return prompts.search({
3583
- message,
3584
- source: async (term) => {
3585
- if (!term) return choices;
3586
- const lower = term.toLowerCase();
3587
- return choices.filter((c) => c.name.toLowerCase().includes(lower));
3588
- }
3589
- });
3590
- }
3591
- async function promptInput(message, required = true) {
3592
- return prompts.input({
3593
- message,
3594
- validate: required ? (v) => v.trim() ? true : "Required" : void 0
3595
- });
3596
- }
3597
- async function promptPassword(message) {
3598
- return prompts.password({
3599
- message,
3600
- mask: "*",
3601
- validate: (v) => v ? true : "Required"
3602
- });
3603
- }
3604
- async function promptConfirm(message, defaultValue = true) {
3605
- return prompts.confirm({ message, default: defaultValue });
3606
- }
3607
- var CACHE_FILE = path5__default.default.join(os__default.default.homedir(), ".arbi", "version-cache.json");
3659
+ init_prompts();
3660
+ var CACHE_FILE = path5__default.default.join(os2__default.default.homedir(), ".arbi", "version-cache.json");
3608
3661
  var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
3609
3662
  function readCache() {
3610
3663
  try {
@@ -3641,7 +3694,7 @@ function getLatestVersion(skipCache = false) {
3641
3694
  }
3642
3695
  }
3643
3696
  function getCurrentVersion() {
3644
- return "0.3.62";
3697
+ return "0.3.64";
3645
3698
  }
3646
3699
  function readChangelog(fromVersion, toVersion) {
3647
3700
  try {
@@ -3694,38 +3747,61 @@ function showChangelog(fromVersion, toVersion) {
3694
3747
  async function checkForUpdates(autoUpdate) {
3695
3748
  try {
3696
3749
  const latest = getLatestVersion();
3697
- if (!latest || latest === "0.3.62") return;
3750
+ if (!latest || latest === "0.3.64") return;
3698
3751
  if (autoUpdate) {
3699
3752
  warn(`
3700
- Your arbi version is out of date (${"0.3.62"} \u2192 ${latest}). Updating...`);
3753
+ Your arbi version is out of date (${"0.3.64"} \u2192 ${latest}). Updating...`);
3701
3754
  child_process.execSync("npm install -g @arbidocs/cli@latest", { stdio: "inherit" });
3702
- showChangelog("0.3.62", latest);
3755
+ showChangelog("0.3.64", latest);
3703
3756
  console.log(`Updated to ${latest}.`);
3704
3757
  } else {
3705
3758
  warn(
3706
3759
  `
3707
- Your arbi version is out of date (${"0.3.62"} \u2192 ${latest}).
3760
+ Your arbi version is out of date (${"0.3.64"} \u2192 ${latest}).
3708
3761
  Run "arbi update" to upgrade, or "arbi update auto" to always stay up to date.`
3709
3762
  );
3710
3763
  }
3711
3764
  } catch {
3712
3765
  }
3713
3766
  }
3767
+ var NAG_FILE = path5__default.default.join(os2__default.default.homedir(), ".arbi", "last-update-hint.json");
3768
+ var NAG_INTERVAL_MS = 24 * 60 * 60 * 1e3;
3769
+ function shouldShowNag(latest) {
3770
+ if (process.env.ARBI_JSON === "1") return false;
3771
+ if (process.argv.includes("--json")) return false;
3772
+ try {
3773
+ const data = JSON.parse(fs5__default.default.readFileSync(NAG_FILE, "utf8"));
3774
+ if (data.latest === latest && typeof data.shownAt === "number" && Date.now() - data.shownAt < NAG_INTERVAL_MS) {
3775
+ return false;
3776
+ }
3777
+ } catch {
3778
+ }
3779
+ return true;
3780
+ }
3781
+ function markNagShown(latest) {
3782
+ try {
3783
+ const dir = path5__default.default.dirname(NAG_FILE);
3784
+ if (!fs5__default.default.existsSync(dir)) fs5__default.default.mkdirSync(dir, { recursive: true, mode: 448 });
3785
+ fs5__default.default.writeFileSync(NAG_FILE, JSON.stringify({ latest, shownAt: Date.now() }) + "\n");
3786
+ } catch {
3787
+ }
3788
+ }
3714
3789
  function hintUpdateOnError() {
3715
3790
  try {
3716
3791
  const cached = readCache();
3717
- if (cached && cached.latest !== "0.3.62") {
3718
- warn(
3719
- `Your arbi version is out of date (${"0.3.62"} \u2192 ${cached.latest}). Run "arbi update".`
3720
- );
3721
- }
3792
+ if (!cached || cached.latest === "0.3.64") return;
3793
+ if (!shouldShowNag(cached.latest)) return;
3794
+ warn(
3795
+ `Your arbi version is out of date (${"0.3.64"} \u2192 ${cached.latest}). Run "arbi update".`
3796
+ );
3797
+ markNagShown(cached.latest);
3722
3798
  } catch {
3723
3799
  }
3724
3800
  }
3725
3801
  var MAX_TASKS = 50;
3726
3802
  var MAX_AGE_MS = 7 * 24 * 60 * 60 * 1e3;
3727
3803
  function getTasksFile() {
3728
- const configDir = process.env.ARBI_CONFIG_DIR ?? path5__default.default.join(os__default.default.homedir(), ".arbi");
3804
+ const configDir = process.env.ARBI_CONFIG_DIR ?? path5__default.default.join(os2__default.default.homedir(), ".arbi");
3729
3805
  return path5__default.default.join(configDir, "tasks.json");
3730
3806
  }
3731
3807
  function ensureDir2(filePath) {
@@ -3858,14 +3934,46 @@ function formatCliError(err) {
3858
3934
  if (err instanceof sdk.ArbiApiError && err.apiError && typeof err.apiError === "object") {
3859
3935
  const base = err.message;
3860
3936
  const apiErr = err.apiError;
3861
- const detail = apiErr.detail ?? apiErr.message ?? apiErr.error;
3862
- if (typeof detail === "string" && detail && !base.includes(detail)) {
3937
+ const detail = stringifyApiDetail(apiErr.detail ?? apiErr.message ?? apiErr.error ?? apiErr);
3938
+ if (detail && !base.includes(detail)) {
3863
3939
  return `${base} \u2014 ${detail}`;
3864
3940
  }
3865
3941
  return base;
3866
3942
  }
3867
3943
  return sdk.getErrorMessage(err);
3868
3944
  }
3945
+ function stringifyApiDetail(value) {
3946
+ if (!value) return "";
3947
+ if (typeof value === "string") return value;
3948
+ if (Array.isArray(value)) {
3949
+ const parts = value.map((v) => {
3950
+ if (typeof v === "string") return v;
3951
+ if (v && typeof v === "object") {
3952
+ const rec = v;
3953
+ const loc = Array.isArray(rec.loc) ? rec.loc.join(".") : void 0;
3954
+ const msg = typeof rec.msg === "string" ? rec.msg : void 0;
3955
+ if (loc && msg) return `${loc}: ${msg}`;
3956
+ return stringifyApiDetail(v);
3957
+ }
3958
+ return "";
3959
+ }).filter(Boolean);
3960
+ return parts.join("; ");
3961
+ }
3962
+ if (typeof value === "object") {
3963
+ const rec = value;
3964
+ if (typeof rec.detail === "string") return rec.detail;
3965
+ if (Array.isArray(rec.detail)) return stringifyApiDetail(rec.detail);
3966
+ if (typeof rec.message === "string") return rec.message;
3967
+ if (typeof rec.error === "string") return rec.error;
3968
+ try {
3969
+ const s = JSON.stringify(value);
3970
+ return s === "{}" ? "" : s;
3971
+ } catch {
3972
+ return "";
3973
+ }
3974
+ }
3975
+ return String(value);
3976
+ }
3869
3977
  function runAction(fn) {
3870
3978
  return async () => {
3871
3979
  try {
@@ -3918,13 +4026,19 @@ async function resolveDmCrypto() {
3918
4026
  const crypto2 = sdk.dm.createDmCryptoContext(arbi, loginResult.signingPrivateKey, userExtId);
3919
4027
  return { ...authCtx, crypto: crypto2 };
3920
4028
  }
4029
+ function truncate(value, width) {
4030
+ if (width <= 0) return "";
4031
+ if (value.length <= width) return value;
4032
+ if (width === 1) return "\u2026";
4033
+ return value.slice(0, width - 1) + "\u2026";
4034
+ }
3921
4035
  function printTable(columns, rows) {
3922
4036
  console.log(chalk2__default.default.bold(columns.map((c) => c.header.padEnd(c.width)).join("")));
3923
4037
  for (const row of rows) {
3924
4038
  console.log(
3925
4039
  columns.map((c) => {
3926
4040
  const val = c.value(row) ?? "";
3927
- return val.slice(0, c.width - 2).padEnd(c.width);
4041
+ return truncate(val, c.width - 1).padEnd(c.width);
3928
4042
  }).join("")
3929
4043
  );
3930
4044
  }
@@ -3937,6 +4051,91 @@ function parseJsonArg(input2, example) {
3937
4051
  process.exit(1);
3938
4052
  }
3939
4053
  }
4054
+ function isInteractive() {
4055
+ return Boolean(process.stdin.isTTY);
4056
+ }
4057
+ function requireInteractive(hint) {
4058
+ if (isInteractive()) return;
4059
+ error(`Cannot prompt: not running in a terminal. ${hint}`);
4060
+ process.exit(1);
4061
+ }
4062
+ function printJson(value) {
4063
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
4064
+ }
4065
+ function nearestMatch(input2, candidates) {
4066
+ if (!input2 || candidates.length === 0) return null;
4067
+ const lower = input2.toLowerCase();
4068
+ const prefix = candidates.find((c) => c.startsWith(lower));
4069
+ if (prefix) return prefix;
4070
+ let best = null;
4071
+ for (const c of candidates) {
4072
+ let score = 0;
4073
+ for (let i = 0; i < Math.min(c.length, lower.length); i++) {
4074
+ if (c[i] === lower[i]) score++;
4075
+ else break;
4076
+ }
4077
+ if (!best || score > best.score) best = { name: c, score };
4078
+ }
4079
+ return best && best.score >= 1 ? best.name : null;
4080
+ }
4081
+ function suggestSubcommandAndExit(parentName, attempted, candidates) {
4082
+ const suggestion = nearestMatch(attempted, candidates);
4083
+ const hint = suggestion ? ` (Did you mean ${suggestion}?)` : "";
4084
+ error(
4085
+ `unknown subcommand '${attempted}' for '${parentName}'${hint}
4086
+ Available: ${candidates.join(", ")}`
4087
+ );
4088
+ process.exit(1);
4089
+ }
4090
+ function emitListOrTable(data, opts, emptyMessage) {
4091
+ if (opts.json) {
4092
+ printJson(data);
4093
+ return false;
4094
+ }
4095
+ if (data.length === 0) {
4096
+ process.stderr.write(`${emptyMessage}
4097
+ `);
4098
+ return false;
4099
+ }
4100
+ return true;
4101
+ }
4102
+ function resolveByIdOrName(list, selector) {
4103
+ const byId = list.find((w) => w.external_id === selector);
4104
+ if (byId) return { ok: true, item: byId, id: byId.external_id };
4105
+ const lower = selector.toLowerCase();
4106
+ const byName = list.filter((w) => (w.name ?? "").toLowerCase() === lower);
4107
+ if (byName.length === 1) return { ok: true, item: byName[0], id: byName[0].external_id };
4108
+ if (byName.length > 1) return { ok: false, reason: "ambiguous", matches: byName };
4109
+ return { ok: false, reason: "not-found", matches: [] };
4110
+ }
4111
+ function failResolveAndExit(thing, selector, res) {
4112
+ if (res.reason === "ambiguous") {
4113
+ error(
4114
+ `Ambiguous ${thing} "${selector}" \u2014 matches ${res.matches.map((m) => m.external_id).join(", ")}. Use the ID.`
4115
+ );
4116
+ } else {
4117
+ error(`${thing} "${selector}" not found.`);
4118
+ }
4119
+ process.exit(3);
4120
+ }
4121
+ function dryRun(verb, targets) {
4122
+ const list = Array.isArray(targets) ? targets : [targets];
4123
+ process.stdout.write(`[dry-run] Would ${verb}:
4124
+ `);
4125
+ for (const t of list) process.stdout.write(` - ${t}
4126
+ `);
4127
+ process.stdout.write("[dry-run] (no changes made; drop --dry-run to execute)\n");
4128
+ }
4129
+ async function pickFromList(items, toChoice, options) {
4130
+ if (items.length === 0) {
4131
+ process.stderr.write(`${options.emptyMessage}
4132
+ `);
4133
+ process.exit(0);
4134
+ }
4135
+ requireInteractive(options.nonTtyHint);
4136
+ const { promptSelect: promptSelect2 } = await Promise.resolve().then(() => (init_prompts(), prompts_exports));
4137
+ return promptSelect2(options.message, items.map(toChoice));
4138
+ }
3940
4139
  var AGENT_BACKENDS = ["claude", "openclaw"];
3941
4140
  var AGENT_MIN_VERSIONS = {
3942
4141
  claude: {
@@ -3995,7 +4194,7 @@ function installSkill(backend) {
3995
4194
  return;
3996
4195
  }
3997
4196
  if (backend === "claude") {
3998
- const dir = path5.join(os.homedir(), ".claude", "commands", "arbi");
4197
+ const dir = path5.join(os2.homedir(), ".claude", "commands", "arbi");
3999
4198
  const target = path5.join(dir, "SKILL.md");
4000
4199
  try {
4001
4200
  fs5.mkdirSync(dir, { recursive: true });
@@ -4006,7 +4205,7 @@ function installSkill(backend) {
4006
4205
  } catch {
4007
4206
  }
4008
4207
  } else if (backend === "openclaw") {
4009
- const workspace = path5.join(os.homedir(), ".arbi", "openclaw-workspace");
4208
+ const workspace = path5.join(os2.homedir(), ".arbi", "openclaw-workspace");
4010
4209
  const bootstrap = path5.join(workspace, "BOOTSTRAP.md");
4011
4210
  try {
4012
4211
  fs5.mkdirSync(workspace, { recursive: true });
@@ -4091,7 +4290,9 @@ async function startListening(agentName, config) {
4091
4290
  });
4092
4291
  }
4093
4292
  function registerListenCommand(program2) {
4094
- program2.command("listen").description("Start DM listener (connect agent backend to ARBI messaging)").option("--agent <name>", `Agent backend: ${AGENT_BACKENDS.join(", ")}`).action(
4293
+ program2.command("listen").description(
4294
+ "Start DM listener (foreground; runs until Ctrl-C \u2014 use `& disown` or systemd for background)"
4295
+ ).option("--agent <name>", `Agent backend: ${AGENT_BACKENDS.join(", ")}`).action(
4095
4296
  (opts) => (async () => {
4096
4297
  const config = resolveConfig();
4097
4298
  await setupAgent(opts.agent, config);
@@ -4118,7 +4319,8 @@ function registerLoginCommand(program2) {
4118
4319
  }
4119
4320
  const passwordFromFlagOrEnv = opts.password || process.env.ARBI_PASSWORD;
4120
4321
  if (!isTty && !passwordFromFlagOrEnv && !opts.signingKey) {
4121
- const msg = "Password is required when stdin is not a TTY. Use --password <password> or set ARBI_PASSWORD.";
4322
+ const why = opts.sso ? "\nNote: --sso still requires --password \u2014 the master password is the E2E key seed,\nthe SSO token only proves identity. ARBI has no passwordless mode." : "";
4323
+ const msg = "Password is required when stdin is not a TTY. Use --password <password> or set ARBI_PASSWORD." + why;
4122
4324
  if (opts.json) console.log(JSON.stringify({ ok: false, error: msg }));
4123
4325
  else error(msg);
4124
4326
  process.exit(1);
@@ -4221,6 +4423,7 @@ Open this URL in your browser:
4221
4423
  })()
4222
4424
  );
4223
4425
  }
4426
+ init_prompts();
4224
4427
  var CENTRAL_API_URL = "https://central.arbi.work";
4225
4428
  var RegisterHintError = class extends Error {
4226
4429
  constructor(message, kind) {
@@ -4360,6 +4563,17 @@ async function nonInteractiveRegister(config, opts) {
4360
4563
  const password2 = opts.password || process.env.ARBI_PASSWORD;
4361
4564
  const supportApiKey = process.env.SUPPORT_API_KEY;
4362
4565
  if (!email) {
4566
+ if (opts.json) {
4567
+ console.log(
4568
+ JSON.stringify({
4569
+ ok: false,
4570
+ stage: "preflight",
4571
+ error: "Email required",
4572
+ hint: "Use --email <email> or set ARBI_EMAIL"
4573
+ })
4574
+ );
4575
+ process.exit(1);
4576
+ }
4363
4577
  error("Email required. Use --email <email> or set ARBI_EMAIL");
4364
4578
  process.exit(1);
4365
4579
  }
@@ -4368,6 +4582,17 @@ async function nonInteractiveRegister(config, opts) {
4368
4582
  if (!opts.json) console.log(`Using email: ${email}`);
4369
4583
  }
4370
4584
  if (!password2) {
4585
+ if (opts.json) {
4586
+ console.log(
4587
+ JSON.stringify({
4588
+ ok: false,
4589
+ stage: "preflight",
4590
+ error: "Password required",
4591
+ hint: "Use --password <password> or set ARBI_PASSWORD"
4592
+ })
4593
+ );
4594
+ process.exit(1);
4595
+ }
4371
4596
  error("Password required. Use --password <password> or set ARBI_PASSWORD");
4372
4597
  process.exit(1);
4373
4598
  }
@@ -4388,6 +4613,17 @@ async function nonInteractiveRegister(config, opts) {
4388
4613
  verificationCode = opts.verificationCode;
4389
4614
  } else {
4390
4615
  if (!supportApiKey) {
4616
+ if (opts.json) {
4617
+ console.log(
4618
+ JSON.stringify({
4619
+ ok: false,
4620
+ stage: "preflight",
4621
+ error: "Verification code required",
4622
+ hint: "Use --verification-code <code> or set SUPPORT_API_KEY for CI mode"
4623
+ })
4624
+ );
4625
+ process.exit(1);
4626
+ }
4391
4627
  error(
4392
4628
  "Verification code required. Use --verification-code <code> or set SUPPORT_API_KEY for CI mode"
4393
4629
  );
@@ -4532,6 +4768,8 @@ function registerStatusCommand(program2) {
4532
4768
  program2.command("status").description("Show current configuration and login status").option("--json", "Output as JSON for scripting").action((opts) => {
4533
4769
  const config = getConfig();
4534
4770
  const creds = getCredentials();
4771
+ const wsId = config?.selectedWorkspaceId ?? null;
4772
+ const wsName = wsId ? getCachedWorkspaceName(wsId) : null;
4535
4773
  if (opts.json) {
4536
4774
  console.log(
4537
4775
  JSON.stringify(
@@ -4540,7 +4778,8 @@ function registerStatusCommand(program2) {
4540
4778
  server: config?.baseUrl ?? null,
4541
4779
  logged_in: !!creds,
4542
4780
  user: creds?.email ?? null,
4543
- workspace: config?.selectedWorkspaceId ?? null
4781
+ workspace: wsId,
4782
+ workspace_name: wsName
4544
4783
  },
4545
4784
  null,
4546
4785
  2
@@ -4558,13 +4797,14 @@ function registerStatusCommand(program2) {
4558
4797
  } else {
4559
4798
  label("User:", "(not logged in)");
4560
4799
  }
4561
- if (config.selectedWorkspaceId) {
4562
- label("Workspace:", config.selectedWorkspaceId);
4800
+ if (wsId) {
4801
+ label("Workspace:", wsName ? `${wsName} (${wsId})` : wsId);
4563
4802
  } else {
4564
4803
  label("Workspace:", "(none selected)");
4565
4804
  }
4566
4805
  });
4567
4806
  }
4807
+ init_prompts();
4568
4808
  function resolveWorkspaceSelector(list, selector) {
4569
4809
  const byId = list.find((w) => w.external_id === selector);
4570
4810
  if (byId) return { ok: true, id: byId.external_id, ws: byId };
@@ -4587,35 +4827,55 @@ function printResolveError(list, selector, res) {
4587
4827
  for (const w of list) error(` ${w.external_id} ${w.name}`);
4588
4828
  }
4589
4829
  function registerWorkspacesCommand(program2) {
4590
- program2.command("workspaces").description("List workspaces").option("--json", "Output as JSON").option("--ids", "Output only workspace IDs (one per line)").action(
4830
+ program2.command("workspaces").description("List workspaces").option("--json", "Output as JSON").option("--ids", "Output only workspace IDs (one per line)").option(
4831
+ "--mine",
4832
+ "Only workspaces where you have a membership role (hide deployment-wide common ones)"
4833
+ ).action(
4591
4834
  (opts) => runAction(async () => {
4592
4835
  const { arbi } = await resolveAuth();
4593
- const data = await sdk.workspaces.listWorkspaces(arbi);
4594
- updateCompletionCache(data);
4836
+ const allWorkspaces = await sdk.workspaces.listWorkspaces(arbi);
4837
+ updateCompletionCache(allWorkspaces);
4838
+ const data = opts.mine ? allWorkspaces.filter((w) => w.users?.length > 0) : allWorkspaces;
4595
4839
  if (opts.ids) {
4596
4840
  for (const w of data) console.log(w.external_id);
4597
4841
  return;
4598
4842
  }
4599
4843
  if (opts.json) {
4600
- const selectedId = getConfig()?.selectedWorkspaceId ?? null;
4601
- const out = data.map((w) => ({
4602
- id: w.external_id,
4603
- name: w.name,
4604
- docs: w.shared_document_count + w.private_document_count,
4605
- role: w.users?.[0]?.role ?? null,
4606
- is_selected: w.external_id === selectedId
4607
- }));
4844
+ const selectedId2 = getConfig()?.selectedWorkspaceId ?? null;
4845
+ const out = data.map((w) => {
4846
+ const role = w.users?.[0]?.role ?? null;
4847
+ return {
4848
+ id: w.external_id,
4849
+ name: w.name,
4850
+ docs: w.shared_document_count + w.private_document_count,
4851
+ role,
4852
+ is_common: role === null,
4853
+ is_selected: w.external_id === selectedId2
4854
+ };
4855
+ });
4608
4856
  console.log(JSON.stringify(out, null, 2));
4609
4857
  return;
4610
4858
  }
4611
4859
  if (data.length === 0) {
4612
- console.log("No workspaces found.");
4860
+ process.stderr.write(
4861
+ opts.mine ? "No workspaces you are a member of. (Drop --mine to see deployment-wide ones.)\n" : "No workspaces found.\n"
4862
+ );
4613
4863
  return;
4614
4864
  }
4865
+ const selectedId = getConfig()?.selectedWorkspaceId ?? null;
4615
4866
  printTable(
4616
4867
  [
4617
4868
  { header: "ID", width: 24, value: (r) => r.external_id },
4618
- { header: "NAME", width: 30, value: (r) => r.name },
4869
+ {
4870
+ header: "NAME",
4871
+ width: 30,
4872
+ // Asterisk-mark the active workspace so a glance at the table
4873
+ // tells you which one `arbi docs / ask / upload` will hit.
4874
+ value: (r) => {
4875
+ const name = r.name ?? "";
4876
+ return r.external_id === selectedId ? `* ${name}` : ` ${name}`;
4877
+ }
4878
+ },
4619
4879
  {
4620
4880
  header: "DOCS",
4621
4881
  width: 6,
@@ -4623,8 +4883,11 @@ function registerWorkspacesCommand(program2) {
4623
4883
  },
4624
4884
  {
4625
4885
  header: "ROLE",
4626
- width: 10,
4627
- value: (r) => r.users?.[0]?.role ?? ""
4886
+ width: 12,
4887
+ // `common` (italics dimmed) for memberless rows the caller can
4888
+ // see only because it's a deployment-wide workspace. The blank
4889
+ // ROLE that used to render here read like missing data.
4890
+ value: (r) => r.users?.[0]?.role ?? "common"
4628
4891
  }
4629
4892
  ],
4630
4893
  data
@@ -4699,7 +4962,7 @@ function registerWorkspacesCommand(program2) {
4699
4962
  success(`Selected: ${ws.name} (${ref(selectedId)})`);
4700
4963
  })()
4701
4964
  );
4702
- workspace.command("create <name>").description("Create a new workspace").option("-d, --description <text>", "Workspace description").option("--public", "Make workspace public", false).option("--select", "Set the new workspace as the active selection", false).option("--json", "Output the new workspace as JSON").action(
4965
+ workspace.command("create <name>").description("Create a new workspace").option("-d, --description <text>", "Workspace description").option("--public", "Make workspace public", false).option("--select", "Set the new workspace as the active selection", false).action(
4703
4966
  (name, opts) => runAction(async () => {
4704
4967
  const { arbi, loginResult } = await resolveAuth();
4705
4968
  const userProjects = await sdk.projects.listProjects(arbi);
@@ -4718,25 +4981,11 @@ function registerWorkspacesCommand(program2) {
4718
4981
  updateConfig({ selectedWorkspaceId: data.external_id });
4719
4982
  clearChatSession();
4720
4983
  }
4721
- if (opts.json) {
4722
- console.log(
4723
- JSON.stringify(
4724
- {
4725
- id: data.external_id,
4726
- name: data.name,
4727
- selected: opts.select ?? false
4728
- },
4729
- null,
4730
- 2
4731
- )
4732
- );
4733
- return;
4734
- }
4735
4984
  success(`Created: ${data.name} (${ref(data.external_id)})`);
4736
4985
  if (opts.select) success(`Selected: ${data.name} (${ref(data.external_id)})`);
4737
4986
  })()
4738
4987
  );
4739
- workspace.command("delete [id-or-name]").description("Delete a workspace (defaults to selected workspace)").option("-y, --yes", "Skip confirmation prompt (required in non-interactive shells)", false).option("--json", "Output the result as JSON").action(
4988
+ workspace.command("delete [id-or-name]").description("Delete a workspace (defaults to selected workspace)").option("-y, --yes", "Skip confirmation prompt (required in non-interactive shells)", false).option("--dry-run", "Preview which workspace would be deleted (no SDK call)").action(
4740
4989
  (idOrName, opts) => runAction(async () => {
4741
4990
  const { arbi } = await resolveAuth();
4742
4991
  const config = getConfig();
@@ -4758,9 +5007,14 @@ function registerWorkspacesCommand(program2) {
4758
5007
  process.exit(1);
4759
5008
  }
4760
5009
  }
4761
- const isInteractive = process.stdin.isTTY === true && process.stdout.isTTY === true;
5010
+ if (opts.dryRun) {
5011
+ const label2 = targetName ? `${targetId} ("${targetName}")` : targetId;
5012
+ dryRun("delete workspace (along with all its documents, conversations, and tags)", label2);
5013
+ return;
5014
+ }
5015
+ const isInteractive2 = process.stdin.isTTY === true && process.stdout.isTTY === true;
4762
5016
  if (!opts.yes) {
4763
- if (!isInteractive) {
5017
+ if (!isInteractive2) {
4764
5018
  error(
4765
5019
  `Refusing to delete ${targetId} without confirmation. Re-run with --yes (non-interactive shell).`
4766
5020
  );
@@ -4784,22 +5038,42 @@ function registerWorkspacesCommand(program2) {
4784
5038
  if (session.workspaceId === targetId) {
4785
5039
  clearChatSession();
4786
5040
  }
4787
- if (opts.json) {
4788
- console.log(JSON.stringify({ id: targetId, deleted: true }, null, 2));
4789
- return;
4790
- }
4791
5041
  success(`Deleted workspace ${targetId}`);
4792
5042
  })()
4793
5043
  );
4794
- workspace.command("update <json>").description("Update workspace properties (pass JSON)").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").option("--json", "Output the updated workspace as JSON").action(
5044
+ workspace.command("rename <name>").description("Rename the active workspace").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").action(
5045
+ (name, opts) => runAction(async () => {
5046
+ const { arbi } = await resolveWorkspace(opts.workspace);
5047
+ const data = await sdk.workspaces.updateWorkspace(arbi, { name });
5048
+ success(`Renamed: ${data.name} (${ref(data.external_id)})`);
5049
+ })()
5050
+ );
5051
+ workspace.command("describe <description>").description("Update the workspace's description").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").action(
5052
+ (description, opts) => runAction(async () => {
5053
+ const { arbi } = await resolveWorkspace(opts.workspace);
5054
+ const data = await sdk.workspaces.updateWorkspace(arbi, {
5055
+ description
5056
+ });
5057
+ success(`Updated description: ${data.name} (${ref(data.external_id)})`);
5058
+ })()
5059
+ );
5060
+ workspace.command("public <on-or-off>").description("Toggle workspace visibility (on = public, off = private)").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").action(
5061
+ (onOff, opts) => runAction(async () => {
5062
+ const isPublic = onOff === "on" || onOff === "true" || onOff === "public";
5063
+ const { arbi } = await resolveWorkspace(opts.workspace);
5064
+ const data = await sdk.workspaces.updateWorkspace(arbi, {
5065
+ is_public: isPublic
5066
+ });
5067
+ success(
5068
+ `Visibility: ${isPublic ? "public" : "private"} \u2014 ${data.name} (${ref(data.external_id)})`
5069
+ );
5070
+ })()
5071
+ );
5072
+ workspace.command("update <json>").description("Update workspace properties (pass JSON; prefer `rename`/`describe`/`public`)").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").action(
4795
5073
  (json, opts) => runAction(async () => {
4796
5074
  const body = parseJsonArg(json, `arbi workspace update '{"name": "New Name"}'`);
4797
5075
  const { arbi } = await resolveWorkspace(opts.workspace);
4798
5076
  const data = await sdk.workspaces.updateWorkspace(arbi, body);
4799
- if (opts.json) {
4800
- console.log(JSON.stringify({ id: data.external_id, name: data.name }, null, 2));
4801
- return;
4802
- }
4803
5077
  success(`Updated: ${data.name} (${ref(data.external_id)})`);
4804
5078
  })()
4805
5079
  );
@@ -4862,13 +5136,72 @@ function registerWorkspacesCommand(program2) {
4862
5136
  for (const u of data) success(`Added: ${u.user.email} as ${u.role}`);
4863
5137
  })()
4864
5138
  );
4865
- workspace.command("remove-user <user-ids...>").description("Remove users from the active workspace").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").action(
4866
- (userIds, opts) => runAction(async () => {
5139
+ workspace.command("remove-user <users...>").description("Remove users from the active workspace (accepts usr-ids or emails)").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").option("--dry-run", "Preview which users would be removed (no SDK call)").action(
5140
+ (users, opts) => runAction(async () => {
4867
5141
  const { arbi } = await resolveWorkspace(opts.workspace);
5142
+ const needsLookup = users.some((u) => u.includes("@"));
5143
+ let userIds = users;
5144
+ if (needsLookup) {
5145
+ const members = await sdk.workspaces.listWorkspaceUsers(arbi);
5146
+ userIds = users.map((u) => {
5147
+ if (!u.includes("@")) return u;
5148
+ const hit = members.find((m) => m.email === u);
5149
+ if (!hit) {
5150
+ error(`Workspace member ${u} not found. Try: arbi workspace users`);
5151
+ process.exit(3);
5152
+ }
5153
+ return hit.external_id;
5154
+ });
5155
+ }
5156
+ if (opts.dryRun) {
5157
+ dryRun(`remove ${userIds.length} user(s) from this workspace`, userIds);
5158
+ return;
5159
+ }
4868
5160
  await sdk.workspaces.removeWorkspaceUsers(arbi, userIds);
4869
5161
  success(`Removed ${userIds.length} user(s).`);
4870
5162
  })()
4871
5163
  );
5164
+ workspace.command("leave [id-or-name]").description("Leave a workspace (defaults to the active one)").option("-y, --yes", "Skip confirmation (required in non-TTY shells)").option("--dry-run", "Preview which workspace would be left (no SDK call)").action(
5165
+ (selector, opts) => runAction(async () => {
5166
+ const { arbi: userArbi } = await resolveAuth();
5167
+ const wsList = await sdk.workspaces.listWorkspaces(userArbi);
5168
+ const target = selector ? resolveWorkspaceSelector(wsList, selector) : (() => {
5169
+ const activeId = getConfig()?.selectedWorkspaceId;
5170
+ const hit = wsList.find((w) => w.external_id === activeId);
5171
+ return hit ? { ok: true, id: hit.external_id, ws: hit } : { ok: false, reason: "not-found" };
5172
+ })();
5173
+ if (!target.ok) {
5174
+ printResolveError(wsList, selector ?? "(active workspace)", target);
5175
+ process.exit(3);
5176
+ }
5177
+ const selfExtId = userArbi.session.getState().userExtId;
5178
+ if (!selfExtId) {
5179
+ error("No user ID in session \u2014 cannot resolve self for leave.");
5180
+ process.exit(1);
5181
+ }
5182
+ if (opts.dryRun) {
5183
+ dryRun("leave workspace", `${target.id} ("${target.ws.name}")`);
5184
+ return;
5185
+ }
5186
+ if (!opts?.yes) {
5187
+ requireInteractive("Pass -y/--yes to confirm in non-TTY shells.");
5188
+ const confirmed = await promptConfirm(
5189
+ `Leave workspace "${target.ws.name}" (${target.id})?`,
5190
+ false
5191
+ );
5192
+ if (!confirmed) {
5193
+ console.log("Cancelled.");
5194
+ return;
5195
+ }
5196
+ }
5197
+ const { arbi } = await resolveWorkspace(target.id);
5198
+ await sdk.workspaces.removeWorkspaceUsers(arbi, [selfExtId]);
5199
+ if (getConfig()?.selectedWorkspaceId === target.id) {
5200
+ updateConfig({ selectedWorkspaceId: "" });
5201
+ }
5202
+ success(`Left workspace "${target.ws.name}" (${target.id}).`);
5203
+ })()
5204
+ );
4872
5205
  workspace.command("set-role <role> <user-ids...>").description("Update user roles (owner, collaborator, guest)").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").action(
4873
5206
  (role, userIds, opts) => runAction(async () => {
4874
5207
  const { arbi } = await resolveWorkspace(opts.workspace);
@@ -4880,15 +5213,29 @@ function registerWorkspacesCommand(program2) {
4880
5213
  for (const u of data) success(`Updated: ${u.user.email} \u2192 ${u.role}`);
4881
5214
  })()
4882
5215
  );
4883
- workspace.command("copy <target-workspace-id> <doc-ids...>").description("Copy documents to another workspace").option("-w, --workspace <id>", "Source workspace ID (defaults to selected workspace)").action(
4884
- (targetId, docIds, opts) => runAction(async () => {
5216
+ workspace.command("copy <target-workspace> <doc-ids...>").description("Copy documents to another workspace (target accepts ID or unique name)").option("-w, --workspace <id>", "Source workspace ID (defaults to selected workspace)").action(
5217
+ (targetRef, docIds, opts) => runAction(async () => {
4885
5218
  const { arbi: userArbi } = await resolveAuth();
4886
5219
  const wsList = await sdk.workspaces.listWorkspaces(userArbi);
4887
- const targetWs = wsList.find((w) => w.external_id === targetId);
5220
+ let targetWs = wsList.find((w) => w.external_id === targetRef);
5221
+ if (!targetWs) {
5222
+ const nameMatches = wsList.filter(
5223
+ (w) => w.name?.toLowerCase() === targetRef.toLowerCase()
5224
+ );
5225
+ if (nameMatches.length === 1) {
5226
+ targetWs = nameMatches[0];
5227
+ } else if (nameMatches.length > 1) {
5228
+ error(
5229
+ `Ambiguous target workspace "${targetRef}" \u2014 matches ${nameMatches.map((w) => w.external_id).join(", ")}. Use the ID.`
5230
+ );
5231
+ process.exit(3);
5232
+ }
5233
+ }
4888
5234
  if (!targetWs || !targetWs.wrapped_key) {
4889
- error(`Target workspace ${targetId} not found or has no encryption key`);
4890
- process.exit(1);
5235
+ error(`Target workspace ${targetRef} not found or has no encryption key`);
5236
+ process.exit(3);
4891
5237
  }
5238
+ const targetId = targetWs.external_id;
4892
5239
  const { arbi, loginResult } = await resolveWorkspace(opts.workspace);
4893
5240
  const signingPrivateKeyBase64 = arbi.crypto.bytesToBase64(loginResult.signingPrivateKey);
4894
5241
  const targetKey = await sdk.generateEncryptedWorkspaceKey(
@@ -4913,6 +5260,7 @@ function registerWorkspacesCommand(program2) {
4913
5260
  })()
4914
5261
  );
4915
5262
  }
5263
+ init_prompts();
4916
5264
  function statusSymbol(status2) {
4917
5265
  switch (status2) {
4918
5266
  case "completed":
@@ -5235,8 +5583,8 @@ function registerDocsCommand(program2) {
5235
5583
  }
5236
5584
  })()
5237
5585
  );
5238
- doc.command("delete [ids...]").description("Delete documents (interactive picker if no IDs given)").action(
5239
- (ids) => runAction(async () => {
5586
+ doc.command("delete [ids...]").description("Delete documents (interactive picker if no IDs given)").option("--dry-run", "Preview which docs would be deleted (no SDK call)").action(
5587
+ (ids, opts) => runAction(async () => {
5240
5588
  const { arbi } = await resolveWorkspace();
5241
5589
  let docIds = ids && ids.length > 0 ? ids : void 0;
5242
5590
  if (!docIds) {
@@ -5245,26 +5593,94 @@ function registerDocsCommand(program2) {
5245
5593
  if (selected.length === 0) return;
5246
5594
  docIds = selected;
5247
5595
  }
5596
+ if (opts?.dryRun) {
5597
+ dryRun(`delete ${docIds.length} document(s)`, docIds);
5598
+ return;
5599
+ }
5248
5600
  await sdk.documents.deleteDocuments(arbi, docIds);
5249
5601
  success(`Deleted ${docIds.length} document(s).`);
5250
5602
  })()
5251
5603
  );
5252
- doc.command("update [json]").description("Update documents (interactive form if no JSON given)").action(
5253
- (json) => runAction(async () => {
5254
- if (json) {
5255
- const parsed = parseJsonArg(json, `arbi doc update '[{"external_id": "doc-123", "shared": true}]'`);
5256
- const docs = Array.isArray(parsed) ? parsed : parsed.documents;
5257
- const { arbi } = await resolveWorkspace();
5258
- const data = await sdk.documents.updateDocuments(
5259
- arbi,
5260
- docs
5261
- );
5262
- success(`Updated ${data.length} document(s).`);
5263
- } else {
5264
- const { arbi } = await resolveWorkspace();
5265
- const choices = await fetchDocChoices(arbi);
5266
- const docId = await promptSearch("Select document to update", choices);
5267
- const field = await promptSelect("Field to update", [
5604
+ doc.command("share <ids...>").description("Mark documents as shared (visible to all workspace members)").action(
5605
+ (ids) => runAction(async () => {
5606
+ const { arbi } = await resolveWorkspace();
5607
+ await sdk.documents.updateDocuments(
5608
+ arbi,
5609
+ ids.map((external_id) => ({ external_id, shared: true }))
5610
+ );
5611
+ success(`Shared ${ids.length} document(s).`);
5612
+ })()
5613
+ );
5614
+ doc.command("unshare <ids...>").description("Mark documents as private (owner-only)").action(
5615
+ (ids) => runAction(async () => {
5616
+ const { arbi } = await resolveWorkspace();
5617
+ await sdk.documents.updateDocuments(
5618
+ arbi,
5619
+ ids.map((external_id) => ({ external_id, shared: false }))
5620
+ );
5621
+ success(`Unshared ${ids.length} document(s).`);
5622
+ })()
5623
+ );
5624
+ doc.command("similar [doc-id]").description("Find near-duplicate documents (cached similarity pairs)").option(
5625
+ "--threshold <n>",
5626
+ "Minimum cosine similarity score, 0.0\u20131.0 (default 0.0 = all stored pairs)",
5627
+ (v) => parseFloat(v),
5628
+ 0
5629
+ ).option("--json", "Output as JSON").action(
5630
+ (docId, opts) => runAction(async () => {
5631
+ const { arbi } = await resolveWorkspace();
5632
+ const res = await arbi.fetch.GET("/v1/document/similar", {
5633
+ params: {
5634
+ query: {
5635
+ ...docId ? { doc_ext_id: docId } : {},
5636
+ threshold: opts.threshold
5637
+ }
5638
+ }
5639
+ });
5640
+ if (res.error) throw new Error(`similar failed: ${JSON.stringify(res.error)}`);
5641
+ const envelope = res.data;
5642
+ const pairs = envelope.pairs ?? [];
5643
+ if (opts.json) {
5644
+ console.log(JSON.stringify(envelope, null, 2));
5645
+ return;
5646
+ }
5647
+ if (pairs.length === 0) {
5648
+ process.stderr.write(
5649
+ docId ? `No similar documents found above threshold ${opts.threshold}.
5650
+ ` : "No stored similarity pairs.\n"
5651
+ );
5652
+ return;
5653
+ }
5654
+ printTable(
5655
+ [
5656
+ { header: "DOC A", width: 18, value: (r) => r.doc_a_ext_id ?? "" },
5657
+ { header: "DOC B", width: 18, value: (r) => r.doc_b_ext_id ?? "" },
5658
+ {
5659
+ header: "SCORE",
5660
+ width: 8,
5661
+ value: (r) => (r.score ?? 0).toFixed(3)
5662
+ }
5663
+ ],
5664
+ pairs
5665
+ );
5666
+ })()
5667
+ );
5668
+ doc.command("update [json]").description("Update documents (interactive form if no JSON given)").action(
5669
+ (json) => runAction(async () => {
5670
+ if (json) {
5671
+ const parsed = parseJsonArg(json, `arbi doc update '[{"external_id": "doc-123", "shared": true}]'`);
5672
+ const docs = Array.isArray(parsed) ? parsed : parsed.documents;
5673
+ const { arbi } = await resolveWorkspace();
5674
+ const data = await sdk.documents.updateDocuments(
5675
+ arbi,
5676
+ docs
5677
+ );
5678
+ success(`Updated ${data.length} document(s).`);
5679
+ } else {
5680
+ const { arbi } = await resolveWorkspace();
5681
+ const choices = await fetchDocChoices(arbi);
5682
+ const docId = await promptSearch("Select document to update", choices);
5683
+ const field = await promptSelect("Field to update", [
5268
5684
  { name: "Title", value: "title" },
5269
5685
  { name: "Shared", value: "shared" },
5270
5686
  { name: "Author", value: "author" },
@@ -5344,9 +5760,9 @@ function registerDocsCommand(program2) {
5344
5760
  "5"
5345
5761
  ).option("-v, --verbose", "Print a log line after each batch in addition to periodic stats.").option("-q, --quiet", "Suppress periodic progress lines \u2014 only print the final summary.").option("-W, --watch", "Watch document processing progress after submission").option("--no-watch", "Skip watching document processing").action(
5346
5762
  (ids, opts) => runAction(async () => {
5347
- const isInteractive = process.stdout.isTTY === true;
5763
+ const isInteractive2 = process.stdout.isTTY === true;
5348
5764
  const watchPref = getConfig()?.watch !== false;
5349
- const willWatch = opts.watch === false ? false : opts.watch === true || watchPref && isInteractive;
5765
+ const willWatch = opts.watch === false ? false : opts.watch === true || watchPref && isInteractive2;
5350
5766
  const { arbi, config, accessToken } = await resolveWorkspace(void 0, {
5351
5767
  skipNotifications: true
5352
5768
  });
@@ -5440,7 +5856,7 @@ function registerDocsCommand(program2) {
5440
5856
  if (!opts.quiet && statusIntervalMs > 0) {
5441
5857
  statusTimer = setInterval(() => printStatus(" "), statusIntervalMs);
5442
5858
  }
5443
- const sleep = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
5859
+ const sleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
5444
5860
  const backoff = async (attempt) => {
5445
5861
  const delayMs = Math.min(1e3 * Math.pow(2, attempt), 3e4);
5446
5862
  await sleep(delayMs);
@@ -5718,9 +6134,9 @@ function classifyDirectInputs(inputPaths) {
5718
6134
  return out;
5719
6135
  }
5720
6136
  async function runDirectUploadMode(inputPaths, opts) {
5721
- const isInteractive = process.stdout.isTTY === true;
6137
+ const isInteractive2 = process.stdout.isTTY === true;
5722
6138
  const watchPref = getConfig()?.watch !== false;
5723
- const willWatch = opts.watch === false ? false : opts.watch === true || watchPref && isInteractive;
6139
+ const willWatch = opts.watch === false ? false : opts.watch === true || watchPref && isInteractive2;
5724
6140
  const inputs = classifyDirectInputs(inputPaths);
5725
6141
  if (inputs.length === 0) {
5726
6142
  warn("No supported files found to upload.");
@@ -5949,9 +6365,9 @@ function registerUploadCommand(program2) {
5949
6365
  process.exit(1);
5950
6366
  }
5951
6367
  }
5952
- const isInteractive = process.stdout.isTTY === true;
6368
+ const isInteractive2 = process.stdout.isTTY === true;
5953
6369
  const watchPref = getConfig()?.watch !== false;
5954
- const willWatch = opts.watch === false ? false : opts.watch === true || watchPref && isInteractive;
6370
+ const willWatch = opts.watch === false ? false : opts.watch === true || watchPref && isInteractive2;
5955
6371
  const { config, accessToken } = await resolveWorkspace(opts.workspace, {
5956
6372
  skipNotifications: willWatch
5957
6373
  });
@@ -6127,7 +6543,7 @@ Watching ${pending.size} document(s)...`);
6127
6543
  pending.delete(msg.doc_ext_id);
6128
6544
  } else {
6129
6545
  console.log(` ${docName}: ${status(msg.status)} (${msg.progress}%)`);
6130
- if (msg.status === "completed") {
6546
+ if (sdk.DOC_TERMINAL_STATUSES.has(msg.status)) {
6131
6547
  pending.delete(msg.doc_ext_id);
6132
6548
  }
6133
6549
  }
@@ -6415,7 +6831,7 @@ async function runManifestMode(inputPaths, opts) {
6415
6831
  pathsToUpload = pathsToUpload.slice(0, n);
6416
6832
  }
6417
6833
  }
6418
- const isInteractive = process.stdout.isTTY === true;
6834
+ const isInteractive2 = process.stdout.isTTY === true;
6419
6835
  const humanOutput = !opts.json;
6420
6836
  if (humanOutput) {
6421
6837
  console.log(
@@ -6441,7 +6857,7 @@ async function runManifestMode(inputPaths, opts) {
6441
6857
  return;
6442
6858
  }
6443
6859
  const watchPref = getConfig()?.watch !== false;
6444
- const willWatch = opts.watch === false ? false : opts.watch === true || watchPref && isInteractive;
6860
+ const willWatch = opts.watch === false ? false : opts.watch === true || watchPref && isInteractive2;
6445
6861
  const { config, accessToken, workspaceId } = await resolveWorkspace(opts.workspace, {
6446
6862
  skipNotifications: willWatch
6447
6863
  });
@@ -6489,13 +6905,13 @@ async function runManifestMode(inputPaths, opts) {
6489
6905
  let errorCount = 0;
6490
6906
  let skippedCount = 0;
6491
6907
  const renderProgress = () => {
6492
- if (!humanOutput || !isInteractive) return;
6908
+ if (!humanOutput || !isInteractive2) return;
6493
6909
  const pct = total > 0 ? Math.floor(done / total * 100) : 100;
6494
6910
  const line = ` ${done}/${total} (${pct}%) ok=${uploadedCount}` + (duplicateCount > 0 ? ` dup=${duplicateCount}` : "") + (rejectedCount > 0 ? ` rej=${rejectedCount}` : "") + (skippedCount > 0 ? ` skip=${skippedCount}` : "") + (errorCount > 0 ? ` err=${errorCount}` : "");
6495
6911
  process.stderr.write("\r" + line.padEnd(80, " "));
6496
6912
  };
6497
6913
  const clearProgress = () => {
6498
- if (humanOutput && isInteractive) process.stderr.write("\r" + " ".repeat(80) + "\r");
6914
+ if (humanOutput && isInteractive2) process.stderr.write("\r" + " ".repeat(80) + "\r");
6499
6915
  };
6500
6916
  const printInlineOutcome = (o) => {
6501
6917
  if (!humanOutput) return;
@@ -6628,7 +7044,7 @@ Watching ${pending.size} document(s)...`);
6628
7044
  pending.delete(msg.doc_ext_id);
6629
7045
  } else {
6630
7046
  console.log(` ${docName}: ${status(msg.status)} (${msg.progress}%)`);
6631
- if (msg.status === "completed") {
7047
+ if (sdk.DOC_TERMINAL_STATUSES.has(msg.status)) {
6632
7048
  pending.delete(msg.doc_ext_id);
6633
7049
  }
6634
7050
  }
@@ -6658,6 +7074,7 @@ Connection closed. ${pending.size} document(s) still processing.`);
6658
7074
  await done2;
6659
7075
  }
6660
7076
  }
7077
+ init_prompts();
6661
7078
  function registerDownloadCommand(program2) {
6662
7079
  program2.command("download [doc-id]").description("Download a document (interactive picker if no ID given)").option(
6663
7080
  "-o, --output <path>",
@@ -6716,8 +7133,17 @@ function registerDownloadCommand(program2) {
6716
7133
  );
6717
7134
  }
6718
7135
  function registerAskCommand(program2) {
6719
- program2.command("ask <question...>").description("Ask the RAG assistant a question (no quotes needed)").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").option("-c, --continue <msg-id>", "Continue from a specific message ID").option("--config <id>", "Config ext_id to use (e.g. cfg-xxx)").option("-n, --new", "Start a new conversation (ignore previous context)").option("-b, --background", "Submit as background task (fire and forget)").option("-q, --quiet", "Suppress agent steps and tool calls").option("--json", "Output in JSON format (background mode only)").action(
7136
+ program2.command("ask <question...>").description("Ask the RAG assistant a question (no quotes needed)").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").option("-c, --continue <msg-id>", "Continue from a specific message ID").option("--config <id>", "Config ext_id to use (e.g. cfg-xxx)").option("-n, --new", "Start a new conversation (ignore previous context)").option("-b, --background", "Submit as background task \u2014 fetch result with `arbi task result`").option("-q, --quiet", "Suppress agent steps and tool calls").option("--json", "Output in JSON format (background mode only)").option(
7137
+ "--raw-citations",
7138
+ "Keep raw `[text](#cite-N)` markdown links instead of the cleaner `text[N]` form"
7139
+ ).action(
6720
7140
  (words, opts) => runAction(async () => {
7141
+ if (opts.json && !opts.background) {
7142
+ console.error(
7143
+ "Error: --json only works with -b/--background. Streaming mode emits prose, not JSON.\nRun: arbi ask -b --json <question> (then `arbi task result`)"
7144
+ );
7145
+ process.exit(1);
7146
+ }
6721
7147
  const question = words.join(" ");
6722
7148
  const { arbi, accessToken, workspaceId, config } = await resolveWorkspace(opts.workspace);
6723
7149
  let previousResponseId = null;
@@ -6792,13 +7218,23 @@ function registerAskCommand(program2) {
6792
7218
  const verbose = opts.quiet === true ? false : getConfig()?.verbose !== false;
6793
7219
  let elapsedTime = null;
6794
7220
  let firstToken = true;
7221
+ let buf = "";
7222
+ const transform = opts.rawCitations ? (s) => s : sdk.stripCitationMarkdown;
7223
+ const flush = (final) => {
7224
+ const cut = final ? buf.length : Math.max(0, buf.length - 32);
7225
+ if (cut === 0) return;
7226
+ const out = transform(buf.slice(0, cut));
7227
+ buf = buf.slice(cut);
7228
+ if (out) process.stdout.write(out);
7229
+ };
6795
7230
  const result = await sdk.streamSSE(res, {
6796
7231
  onToken: (content) => {
6797
7232
  if (firstToken) {
6798
7233
  process.stderr.write(chalk2__default.default.dim("[ARBI] "));
6799
7234
  firstToken = false;
6800
7235
  }
6801
- process.stdout.write(content);
7236
+ buf += content;
7237
+ flush(false);
6802
7238
  },
6803
7239
  onAgentStep: (data) => {
6804
7240
  if (verbose) {
@@ -6815,6 +7251,7 @@ function registerAskCommand(program2) {
6815
7251
  onError: (message) => console.error(chalk2__default.default.red(`
6816
7252
  Error: ${message}`))
6817
7253
  });
7254
+ flush(true);
6818
7255
  process.stdout.write("\n");
6819
7256
  if (result.metadata) {
6820
7257
  saveLastMetadata(result.metadata);
@@ -6833,9 +7270,10 @@ Error: ${message}`))
6833
7270
  chalk2__default.default.dim(`[${refs} citation${refs === 1 ? "" : "s"} \u2014 arbi cite to browse]`)
6834
7271
  );
6835
7272
  }
6836
- if (result.assistantMessageExtId) {
7273
+ const assistantId = result.assistantMessageExtId ?? result.metadata?.external_id;
7274
+ if (assistantId) {
6837
7275
  const updates = {
6838
- lastMessageExtId: result.assistantMessageExtId,
7276
+ lastMessageExtId: assistantId,
6839
7277
  workspaceId
6840
7278
  };
6841
7279
  const conversationExtId = result.userMessage?.conversation_ext_id ?? result.metadata?.conversation_ext_id;
@@ -6843,96 +7281,202 @@ Error: ${message}`))
6843
7281
  updates.conversationExtId = conversationExtId;
6844
7282
  }
6845
7283
  updateChatSession(updates);
7284
+ } else if (opts.quiet !== true) {
7285
+ console.error(
7286
+ chalk2__default.default.yellow(
7287
+ "[warn] No assistant message ID received \u2014 session not saved. The next `arbi ask` will start a new conversation."
7288
+ )
7289
+ );
6846
7290
  }
6847
7291
  })()
6848
7292
  );
6849
7293
  }
6850
- var MAX_PASSAGE_LINES = 40;
7294
+ function hashUtf8(s) {
7295
+ return crypto$1.createHash("sha256").update(s, "utf8").digest("hex");
7296
+ }
6851
7297
  function registerCiteCommand(program2) {
6852
- program2.command("cite [number]").description("Browse citations from the last response").option("-a, --all", "Show all citations with full passages").action(
6853
- (number, opts) => runAction(async () => {
6854
- const raw = loadLastMetadata();
6855
- if (!raw) {
6856
- console.error(chalk2__default.default.dim("No citation data available. Run `arbi ask` first."));
6857
- return;
6858
- }
6859
- const metadata = raw;
6860
- const count = sdk.countCitations(metadata);
6861
- if (count === 0) {
6862
- console.log(chalk2__default.default.dim("The last response contained no citations."));
6863
- return;
6864
- }
6865
- const resolved = sdk.resolveCitations(metadata);
6866
- if (opts.all) {
6867
- for (const r of resolved) {
6868
- printCitationDetail(r.citationNum, resolved);
6869
- console.log();
7298
+ program2.command("cite [number]").description("Browse citations from the last `arbi ask` response in this shell").allowExcessArguments(true).option("-a, --all", "Show all citations as markdown blocks").option("--json", "Output citations as JSON (full structured payload)").action(
7299
+ (numberOrVerb, opts) => runAction(async () => {
7300
+ if (numberOrVerb === "verify") {
7301
+ const argv = process.argv;
7302
+ const i = argv.indexOf("verify");
7303
+ const target = i !== -1 ? argv[i + 1] : void 0;
7304
+ if (!target) {
7305
+ console.error("Usage: arbi cite verify <number>");
7306
+ process.exit(1);
6870
7307
  }
7308
+ await verifyOne(target, opts);
6871
7309
  return;
6872
7310
  }
6873
- if (number) {
6874
- printCitationDetail(number, resolved);
6875
- return;
6876
- }
6877
- const summaries = sdk.summarizeCitations(resolved);
6878
- console.log(chalk2__default.default.bold(`Citations (${summaries.length}):
6879
- `));
6880
- for (const s of summaries) {
6881
- const page = s.pageNumber != null ? `, p${s.pageNumber}` : "";
6882
- const chunks = s.chunkCount > 1 ? ` (${s.chunkCount} passages)` : "";
6883
- console.log(
6884
- ` ${chalk2__default.default.cyan(`[${s.citationNum}]`)} ${chalk2__default.default.bold(s.docTitle)}${page}${chunks}`
6885
- );
6886
- const truncated = s.statement.length > 120 ? s.statement.slice(0, 120).replace(/\n/g, " ").trim() + "..." : s.statement.replace(/\n/g, " ").trim();
6887
- console.log(` ${chalk2__default.default.dim(truncated)}`);
6888
- }
6889
- console.log(chalk2__default.default.dim(`
6890
- Use \`arbi cite <N>\` to view full passage.`));
7311
+ await listOrShow(numberOrVerb, opts);
6891
7312
  })()
6892
7313
  );
6893
7314
  }
6894
- function printCitationDetail(num, resolved) {
6895
- const citation = resolved.find((r) => r.citationNum === num);
6896
- if (!citation) {
6897
- console.error(chalk2__default.default.red(`Citation [${num}] not found.`));
6898
- return;
6899
- }
6900
- const firstChunk = citation.chunks[0];
7315
+ function renderCitation(r) {
7316
+ const firstChunk = r.chunks[0];
6901
7317
  const docTitle = firstChunk?.metadata?.doc_title ?? "Unknown document";
6902
7318
  const page = firstChunk?.metadata?.page_number;
6903
- console.log(chalk2__default.default.bold.cyan(`[Citation ${citation.citationNum}]`) + " " + chalk2__default.default.bold(docTitle));
6904
- if (page != null) {
6905
- console.log(chalk2__default.default.dim(`Page ${page}`));
6906
- }
6907
- console.log();
6908
- console.log(chalk2__default.default.bold("Statement:"));
6909
- console.log(` ${citation.citationData.statement}`);
6910
- console.log();
6911
- for (let i = 0; i < citation.chunks.length; i++) {
6912
- const chunk = citation.chunks[i];
6913
- if (citation.chunks.length > 1) {
6914
- console.log(chalk2__default.default.bold(`Passage ${i + 1}/${citation.chunks.length}:`));
6915
- } else {
6916
- console.log(chalk2__default.default.bold("Passage:"));
7319
+ const docId = firstChunk?.metadata?.doc_ext_id ?? null;
7320
+ const chunkId = firstChunk?.metadata?.chunk_ext_id ?? null;
7321
+ const passage = r.chunks.map((c) => c.content).join("\n\n");
7322
+ const sha = hashUtf8(passage);
7323
+ const lines = [];
7324
+ lines.push(chalk2__default.default.bold(`## Citation ${r.citationNum}`));
7325
+ lines.push("");
7326
+ lines.push(`${chalk2__default.default.bold("**Document:**")} ${docTitle}`);
7327
+ if (page != null) lines.push(`${chalk2__default.default.bold("**Page:**")} ${page}`);
7328
+ if (docId) lines.push(`${chalk2__default.default.bold("**Doc ID:**")} ${docId}`);
7329
+ if (chunkId) lines.push(`${chalk2__default.default.bold("**Chunk ID:**")} ${chunkId}`);
7330
+ lines.push(`${chalk2__default.default.bold("**SHA256:**")} ${sha}`);
7331
+ lines.push("");
7332
+ lines.push(chalk2__default.default.bold("### Claim"));
7333
+ lines.push(r.citationData.statement ?? "");
7334
+ lines.push("");
7335
+ lines.push(chalk2__default.default.bold("### Passage"));
7336
+ lines.push("```");
7337
+ lines.push(passage);
7338
+ lines.push("```");
7339
+ return lines.join("\n");
7340
+ }
7341
+ async function listOrShow(number, opts) {
7342
+ const raw = loadLastMetadata();
7343
+ if (!raw) {
7344
+ if (opts.json) {
7345
+ printJson({ count: 0, citations: [] });
7346
+ return;
6917
7347
  }
6918
- const lines = chunk.content.split("\n");
6919
- if (lines.length > MAX_PASSAGE_LINES) {
6920
- console.log(lines.slice(0, MAX_PASSAGE_LINES).join("\n"));
6921
- console.log(chalk2__default.default.dim(` ... (${lines.length - MAX_PASSAGE_LINES} more lines)`));
6922
- } else {
6923
- console.log(chunk.content);
7348
+ process.stderr.write("No citation data available. Run `arbi ask` first.\n");
7349
+ return;
7350
+ }
7351
+ const metadata = raw;
7352
+ if (sdk.countCitations(metadata) === 0) {
7353
+ if (opts.json) {
7354
+ printJson({ count: 0, citations: [] });
7355
+ return;
7356
+ }
7357
+ process.stderr.write("The last response contained no citations.\n");
7358
+ return;
7359
+ }
7360
+ const resolved = sdk.resolveCitations(metadata);
7361
+ if (opts.json) {
7362
+ const projection = resolved.map((r) => {
7363
+ const passage = r.chunks.map((c) => c.content).join("\n\n");
7364
+ return {
7365
+ num: r.citationNum,
7366
+ statement: r.citationData.statement ?? "",
7367
+ doc_title: r.chunks[0]?.metadata?.doc_title ?? null,
7368
+ page_number: r.chunks[0]?.metadata?.page_number ?? null,
7369
+ chunk_ext_ids: r.chunks.map((c) => c.metadata?.chunk_ext_id ?? null),
7370
+ doc_ext_ids: [...new Set(r.chunks.map((c) => c.metadata?.doc_ext_id).filter(Boolean))],
7371
+ passages: r.chunks.map((c) => c.content),
7372
+ passage_hashes: r.chunks.map((c) => hashUtf8(c.content)),
7373
+ combined_passage_hash: hashUtf8(passage)
7374
+ };
7375
+ });
7376
+ const filtered = number ? projection.filter((p) => p.num === number) : projection;
7377
+ printJson({ count: filtered.length, citations: filtered });
7378
+ return;
7379
+ }
7380
+ if (number) {
7381
+ const hit = resolved.find((r) => r.citationNum === number);
7382
+ if (!hit) {
7383
+ console.error(chalk2__default.default.red(`Citation [${number}] not found.`));
7384
+ process.exit(3);
7385
+ }
7386
+ console.log(renderCitation(hit));
7387
+ return;
7388
+ }
7389
+ if (opts.all) {
7390
+ for (let i = 0; i < resolved.length; i++) {
7391
+ if (i > 0) console.log("\n---\n");
7392
+ console.log(renderCitation(resolved[i]));
7393
+ }
7394
+ return;
7395
+ }
7396
+ console.log(chalk2__default.default.bold(`# Citations (${resolved.length})`));
7397
+ console.log("");
7398
+ for (const r of resolved) {
7399
+ const docTitle = r.chunks[0]?.metadata?.doc_title ?? "Unknown document";
7400
+ const page = r.chunks[0]?.metadata?.page_number;
7401
+ const pageSuffix = page != null ? `, p${page}` : "";
7402
+ const docId = r.chunks[0]?.metadata?.doc_ext_id;
7403
+ const statement = (r.citationData.statement ?? "").replace(/\s+/g, " ").trim();
7404
+ const trimmed = statement.length > 140 ? statement.slice(0, 140) + "\u2026" : statement;
7405
+ console.log(
7406
+ `- ${chalk2__default.default.bold(`[${r.citationNum}]`)} ${docTitle}${pageSuffix}${docId ? ` (${docId})` : ""}`
7407
+ );
7408
+ console.log(` ${trimmed}`);
7409
+ }
7410
+ console.log("");
7411
+ console.log(
7412
+ chalk2__default.default.dim("`arbi cite <N>` for the full markdown block; `--json` for the structured payload.")
7413
+ );
7414
+ }
7415
+ async function verifyOne(number, opts) {
7416
+ const raw = loadLastMetadata();
7417
+ if (!raw) {
7418
+ if (opts.json) {
7419
+ printJson({ verified: false, reason: "no-last-metadata" });
7420
+ return;
7421
+ }
7422
+ process.stderr.write("No citation data available. Run `arbi ask` first.\n");
7423
+ process.exit(1);
7424
+ }
7425
+ const metadata = raw;
7426
+ const resolved = sdk.resolveCitations(metadata);
7427
+ const citation = resolved.find((r) => r.citationNum === number);
7428
+ if (!citation) {
7429
+ if (opts.json) {
7430
+ printJson({ verified: false, reason: "not-found", number });
7431
+ return;
6924
7432
  }
6925
- if (i < citation.chunks.length - 1) console.log();
7433
+ console.error(chalk2__default.default.red(`Citation [${number}] not found in last response.`));
7434
+ process.exit(3);
7435
+ }
7436
+ const chunks = citation.chunks.map((c) => ({
7437
+ chunk_ext_id: c.metadata?.chunk_ext_id ?? null,
7438
+ doc_ext_id: c.metadata?.doc_ext_id ?? null,
7439
+ doc_title: c.metadata?.doc_title ?? null,
7440
+ page_number: c.metadata?.page_number ?? null,
7441
+ passage_length: c.content.length,
7442
+ passage_sha256: hashUtf8(c.content)
7443
+ }));
7444
+ if (opts.json) {
7445
+ printJson({
7446
+ verified: chunks.length > 0,
7447
+ number,
7448
+ statement: citation.citationData.statement ?? "",
7449
+ chunks,
7450
+ followup_hint: "For independent re-verification, run `arbi doc get <doc_ext_id> --json` and confirm the passage hash matches the doc content."
7451
+ });
7452
+ return;
6926
7453
  }
6927
- if (citation.chunks.length === 0) {
6928
- console.log(chalk2__default.default.dim(" (no passage data available)"));
7454
+ console.log(chalk2__default.default.bold(`## Verification: Citation ${number}`));
7455
+ console.log("");
7456
+ console.log(`${chalk2__default.default.bold("**Status:**")} ${chunks.length > 0 ? "verified" : "no chunks"}`);
7457
+ console.log(`${chalk2__default.default.bold("**Claim:**")} ${citation.citationData.statement ?? ""}`);
7458
+ console.log("");
7459
+ for (let i = 0; i < chunks.length; i++) {
7460
+ const v = chunks[i];
7461
+ console.log(chalk2__default.default.bold(`### Chunk ${i + 1}/${chunks.length}`));
7462
+ console.log(`${chalk2__default.default.bold("**Doc:**")} ${v.doc_title ?? "Unknown"}`);
7463
+ if (v.page_number != null) console.log(`${chalk2__default.default.bold("**Page:**")} ${v.page_number}`);
7464
+ if (v.doc_ext_id) console.log(`${chalk2__default.default.bold("**Doc ID:**")} ${v.doc_ext_id}`);
7465
+ if (v.chunk_ext_id) console.log(`${chalk2__default.default.bold("**Chunk ID:**")} ${v.chunk_ext_id}`);
7466
+ console.log(`${chalk2__default.default.bold("**Length:**")} ${v.passage_length} chars`);
7467
+ console.log(`${chalk2__default.default.bold("**SHA256:**")} ${v.passage_sha256}`);
7468
+ if (i < chunks.length - 1) console.log("");
6929
7469
  }
7470
+ console.log("");
7471
+ console.log(
7472
+ chalk2__default.default.dim("Re-verify: `arbi doc get <Doc ID> --json` and compare the passage SHA256.")
7473
+ );
6930
7474
  }
6931
7475
  var MAX_INDIVIDUALLY_SELECTED_DOCS = 5e3;
6932
7476
  function chunkScore(chunk) {
6933
7477
  return chunk.metadata.rerank_score ?? chunk.metadata.score ?? 0;
6934
7478
  }
6935
- function truncate(text, max) {
7479
+ function truncate2(text, max) {
6936
7480
  const oneLine = text.replace(/\n/g, " ").trim();
6937
7481
  return oneLine.length > max ? oneLine.slice(0, max - 1) + "\u2026" : oneLine;
6938
7482
  }
@@ -6987,11 +7531,22 @@ Narrow your selection, or omit -d to search the whole workspace.`
6987
7531
  const filtered = allChunks.filter((c) => chunkScore(c) >= minScore).sort((a, b) => chunkScore(b) - chunkScore(a)).slice(0, limit);
6988
7532
  const uniqueDocs = new Set(filtered.map((c) => c.metadata.doc_ext_id));
6989
7533
  if (opts.json) {
6990
- console.log(JSON.stringify(result, null, 2));
7534
+ console.log(
7535
+ JSON.stringify(
7536
+ {
7537
+ results: filtered,
7538
+ count: filtered.length,
7539
+ unique_documents: uniqueDocs.size,
7540
+ raw: result
7541
+ },
7542
+ null,
7543
+ 2
7544
+ )
7545
+ );
6991
7546
  return;
6992
7547
  }
6993
7548
  if (filtered.length === 0) {
6994
- console.log("No results found.");
7549
+ process.stderr.write("No results found.\n");
6995
7550
  return;
6996
7551
  }
6997
7552
  console.log(
@@ -7005,7 +7560,7 @@ Found ${chalk2__default.default.bold(String(filtered.length))} result${filtered.
7005
7560
  const score = fmtScore(chunkScore(chunk));
7006
7561
  const doc = chunk.metadata.doc_title ?? chunk.metadata.doc_ext_id ?? "";
7007
7562
  const page = chunk.metadata.page_number ? `p.${chunk.metadata.page_number}` : "";
7008
- const preview = truncate(chunk.content, 80);
7563
+ const preview = truncate2(chunk.content, 80);
7009
7564
  console.log(
7010
7565
  ` ${chalk2__default.default.dim(String(i + 1).padStart(2, " "))}. ${chalk2__default.default.yellow(score)} ${chalk2__default.default.cyan(doc)} ${chalk2__default.default.dim(page)}`
7011
7566
  );
@@ -7030,7 +7585,7 @@ Found ${chalk2__default.default.bold(String(filtered.length))} result${filtered.
7030
7585
  for (const chunk of chunks) {
7031
7586
  const score = fmtScore(chunkScore(chunk));
7032
7587
  const page = chunk.metadata.page_number ? `p.${chunk.metadata.page_number}` : "";
7033
- const preview = truncate(chunk.content, 72);
7588
+ const preview = truncate2(chunk.content, 72);
7034
7589
  console.log(
7035
7590
  ` ${chalk2__default.default.yellow(score)} ${chalk2__default.default.dim(page.padEnd(6))} ${chalk2__default.default.dim(preview)}`
7036
7591
  );
@@ -7046,8 +7601,19 @@ function colorize2(level, text) {
7046
7601
  if (level === "warning") return chalk2__default.default.yellow(text);
7047
7602
  return text;
7048
7603
  }
7604
+ var KNOWN_WATCH_TYPES = [
7605
+ "task_update",
7606
+ "batch_complete",
7607
+ "presence_update",
7608
+ "response_complete",
7609
+ "notification",
7610
+ "error"
7611
+ ];
7049
7612
  function registerWatchCommand(program2) {
7050
- program2.command("watch").description("Watch workspace activity in real time").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").option("-t, --timeout <seconds>", "Auto-close after N seconds").option("-n, --count <n>", "Stop after N messages").option("--json", "Output NDJSON (one JSON object per line)").action(
7613
+ program2.command("watch").description("Watch workspace activity in real time").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").option("-t, --timeout <seconds>", "Auto-close after N seconds").option("-n, --count <n>", "Stop after N messages").option(
7614
+ "--type <comma-list>",
7615
+ `Only emit events of these types (default: all). Known: ${KNOWN_WATCH_TYPES.join(",")}`
7616
+ ).option("--json", "Output NDJSON (one JSON object per line)").action(
7051
7617
  (opts) => runAction(async () => {
7052
7618
  const { config, accessToken, workspaceId } = await resolveWorkspace(opts.workspace, {
7053
7619
  skipNotifications: true
@@ -7055,6 +7621,21 @@ function registerWatchCommand(program2) {
7055
7621
  const timeoutSec = opts.timeout ? parseInt(opts.timeout, 10) : void 0;
7056
7622
  const maxCount = opts.count ? parseInt(opts.count, 10) : void 0;
7057
7623
  const jsonMode = opts.json ?? false;
7624
+ const typeFilter = opts.type ? new Set(
7625
+ opts.type.split(",").map((t) => t.trim()).filter(Boolean)
7626
+ ) : null;
7627
+ if (typeFilter) {
7628
+ const unknown = [...typeFilter].filter(
7629
+ (t) => !KNOWN_WATCH_TYPES.includes(t)
7630
+ );
7631
+ if (unknown.length > 0) {
7632
+ console.error(
7633
+ `Error: unknown event type(s): ${unknown.join(", ")}
7634
+ Known: ${KNOWN_WATCH_TYPES.join(", ")}`
7635
+ );
7636
+ process.exit(1);
7637
+ }
7638
+ }
7058
7639
  if (!jsonMode) {
7059
7640
  const parts = [`Watching workspace ${workspaceId}...`];
7060
7641
  if (timeoutSec) parts.push(`(timeout: ${timeoutSec}s)`);
@@ -7063,6 +7644,7 @@ function registerWatchCommand(program2) {
7063
7644
  console.log(parts.join(" "));
7064
7645
  }
7065
7646
  let messageCount = 0;
7647
+ let timedOut = false;
7066
7648
  let onDone;
7067
7649
  const done = new Promise((r) => {
7068
7650
  onDone = r;
@@ -7071,6 +7653,10 @@ function registerWatchCommand(program2) {
7071
7653
  baseUrl: config.baseUrl,
7072
7654
  accessToken,
7073
7655
  onMessage: (msg) => {
7656
+ if (typeFilter) {
7657
+ const type = msg.type;
7658
+ if (!type || !typeFilter.has(type)) return;
7659
+ }
7074
7660
  messageCount++;
7075
7661
  if (jsonMode) {
7076
7662
  console.log(JSON.stringify(msg));
@@ -7097,6 +7683,7 @@ Connection closed (code ${code}${reason ? ": " + reason : ""})`)
7097
7683
  let timer;
7098
7684
  if (timeoutSec) {
7099
7685
  timer = setTimeout(() => {
7686
+ timedOut = true;
7100
7687
  if (!jsonMode) console.log(chalk2__default.default.dim(`
7101
7688
  Timeout (${timeoutSec}s), closing.`));
7102
7689
  conn.close();
@@ -7110,21 +7697,53 @@ Timeout (${timeoutSec}s), closing.`));
7110
7697
  await done;
7111
7698
  if (timer) clearTimeout(timer);
7112
7699
  process.removeListener("SIGINT", sigintHandler);
7700
+ if (timedOut && maxCount && messageCount < maxCount) {
7701
+ if (jsonMode) {
7702
+ console.log(
7703
+ JSON.stringify({
7704
+ type: "timeout",
7705
+ received: messageCount,
7706
+ expected: maxCount,
7707
+ timeout_seconds: timeoutSec
7708
+ })
7709
+ );
7710
+ } else {
7711
+ console.error(
7712
+ chalk2__default.default.yellow(`Timed out before reaching ${maxCount} messages (got ${messageCount}).`)
7713
+ );
7714
+ }
7715
+ process.exit(124);
7716
+ }
7113
7717
  })()
7114
7718
  );
7115
7719
  }
7720
+ init_prompts();
7721
+ function redactPictures(rows) {
7722
+ return rows.map((row) => {
7723
+ const user = row.user;
7724
+ if (!user || typeof user.picture !== "string" || user.picture.length === 0) return row;
7725
+ return {
7726
+ ...row,
7727
+ user: {
7728
+ ...user,
7729
+ has_picture: true,
7730
+ picture: null
7731
+ }
7732
+ };
7733
+ });
7734
+ }
7116
7735
  function registerContactsCommand(program2) {
7117
- const contacts = program2.command("contacts").description("Manage contacts");
7118
- contacts.command("list").description("List all contacts").option("--json", "Output as JSON").action(
7736
+ const contacts = program2.command("contacts").description("Contacts: list, add, remove");
7737
+ contacts.command("list").description("List all contacts").option("--json", "Output as JSON").option("--include-pictures", "Keep base64 profile-picture bytes in --json output").action(
7119
7738
  (opts) => runAction(async () => {
7120
7739
  const { arbi } = await resolveAuth();
7121
7740
  const data = await sdk.contacts.listContacts(arbi);
7122
7741
  if (opts.json) {
7123
- console.log(JSON.stringify(data, null, 2));
7742
+ printJson(opts.includePictures ? data : redactPictures(data));
7124
7743
  return;
7125
7744
  }
7126
7745
  if (data.length === 0) {
7127
- console.log("No contacts found.");
7746
+ process.stderr.write("No contacts found.\n");
7128
7747
  return;
7129
7748
  }
7130
7749
  printTable(
@@ -7146,6 +7765,7 @@ function registerContactsCommand(program2) {
7146
7765
  (emails) => runAction(async () => {
7147
7766
  const { arbi } = await resolveAuth();
7148
7767
  if (!emails || emails.length === 0) {
7768
+ requireInteractive("Pass email(s) as positional args: arbi contacts add foo@x.y bar@x.y");
7149
7769
  const input2 = await promptInput("Email address(es), comma-separated");
7150
7770
  emails = input2.split(",").map((e) => e.trim()).filter(Boolean);
7151
7771
  if (emails.length === 0) return;
@@ -7156,16 +7776,17 @@ function registerContactsCommand(program2) {
7156
7776
  }
7157
7777
  })()
7158
7778
  );
7159
- contacts.command("remove [ids...]").description("Remove contacts (interactive picker if no IDs given)").action(
7160
- (ids) => runAction(async () => {
7779
+ contacts.command("remove [ids...]").description("Remove contacts (interactive picker if no IDs given)").option("--dry-run", "Preview which contacts would be removed (no SDK call)").action(
7780
+ (ids, opts) => runAction(async () => {
7161
7781
  const { arbi } = await resolveAuth();
7162
7782
  let contactIds = ids && ids.length > 0 ? ids : void 0;
7163
7783
  if (!contactIds) {
7164
7784
  const data = await sdk.contacts.listContacts(arbi);
7165
7785
  if (data.length === 0) {
7166
- console.log("No contacts found.");
7786
+ process.stderr.write("No contacts found.\n");
7167
7787
  return;
7168
7788
  }
7789
+ requireInteractive("Pass contact IDs directly: arbi contacts remove cnt-\u2026");
7169
7790
  contactIds = await promptCheckbox(
7170
7791
  "Select contacts to remove",
7171
7792
  data.map((c) => {
@@ -7178,57 +7799,157 @@ function registerContactsCommand(program2) {
7178
7799
  );
7179
7800
  if (contactIds.length === 0) return;
7180
7801
  }
7802
+ if (opts?.dryRun) {
7803
+ dryRun(`remove ${contactIds.length} contact(s)`, contactIds);
7804
+ return;
7805
+ }
7181
7806
  await sdk.contacts.removeContacts(arbi, contactIds);
7182
7807
  success(`Removed ${contactIds.length} contact(s).`);
7183
7808
  })()
7184
7809
  );
7185
- contacts.option("--json", "Output as JSON").action(async (opts) => {
7186
- const args = [];
7187
- if (opts.json) args.push("--json");
7188
- await contacts.commands.find((c) => c.name() === "list").parseAsync(args, { from: "user" });
7810
+ contacts.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
7811
+ const tail = contacts.args ?? [];
7812
+ await contacts.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
7813
+ });
7814
+ }
7815
+ init_prompts();
7816
+ async function resolveRecipient(arbi, ref2) {
7817
+ const isEmail = ref2.includes("@");
7818
+ const matchKey = isEmail ? "email" : "external_id";
7819
+ const contacts = await sdk.contacts.listContacts(arbi);
7820
+ const contactMatch = contacts.find((c) => {
7821
+ if (isEmail) return c.email === ref2;
7822
+ const u = c.user;
7823
+ return u?.external_id === ref2 || c.external_id === ref2;
7189
7824
  });
7825
+ if (contactMatch) {
7826
+ const u = contactMatch.user;
7827
+ const extId = u?.external_id ?? contactMatch.external_id;
7828
+ const pubKey = u?.encryption_public_key ?? "";
7829
+ if (pubKey) return { extId, pubKey };
7830
+ }
7831
+ try {
7832
+ const agents = await sdk.agents.listAgents(arbi);
7833
+ const agentMatch = agents.find((a) => a[matchKey] === ref2);
7834
+ if (agentMatch) {
7835
+ return { extId: agentMatch.external_id, pubKey: agentMatch.encryption_public_key };
7836
+ }
7837
+ } catch {
7838
+ }
7839
+ try {
7840
+ const wsUsers = await sdk.workspaces.listWorkspaceUsers(arbi);
7841
+ const wsMatch = wsUsers.find((wu) => wu[matchKey] === ref2);
7842
+ if (wsMatch) {
7843
+ const u = wsMatch;
7844
+ return {
7845
+ extId: u.external_id,
7846
+ pubKey: u.encryption_public_key
7847
+ };
7848
+ }
7849
+ } catch {
7850
+ }
7851
+ return null;
7852
+ }
7853
+ function rowMatchesFilters(row, opts) {
7854
+ if (opts.unread && row.read) return false;
7855
+ const sender = row.sender;
7856
+ const recipient = row.recipient;
7857
+ if (opts.from) {
7858
+ const needle = opts.from.toLowerCase();
7859
+ const senderMatch = sender?.email?.toLowerCase() === needle || sender?.external_id === opts.from;
7860
+ const recipientMatch = recipient?.email?.toLowerCase() === needle || recipient?.external_id === opts.from;
7861
+ if (!senderMatch && !recipientMatch) return false;
7862
+ }
7863
+ if (opts.thread) {
7864
+ const needle = opts.thread.toLowerCase();
7865
+ const peer = sender?.external_id === opts.myExtId ? recipient : sender;
7866
+ const peerMatch = peer?.email?.toLowerCase() === needle || peer?.external_id === opts.thread;
7867
+ if (!peerMatch) return false;
7868
+ }
7869
+ if (opts.since) {
7870
+ const cutoff = Date.parse(opts.since);
7871
+ if (Number.isFinite(cutoff)) {
7872
+ const createdAt = typeof row.created_at === "string" ? Date.parse(row.created_at) : NaN;
7873
+ if (!Number.isFinite(createdAt) || createdAt < cutoff) return false;
7874
+ }
7875
+ }
7876
+ return true;
7190
7877
  }
7191
7878
  function registerDmCommand(program2) {
7192
7879
  const dm = program2.command("dm").description("Direct messages (E2E encrypted)");
7193
- dm.command("list").description("List all DMs and notifications (decrypted)").action(
7194
- runAction(async () => {
7880
+ dm.command("list").description("List direct messages (decrypted, newest first)").option("--unread", "Only show unread messages").option("--from <email-or-extid>", "Filter by sender OR recipient (email or usr- id)").option(
7881
+ "--thread <email-or-extid>",
7882
+ "Show the full conversation with one peer (both directions)"
7883
+ ).option("--since <iso>", "Only messages newer than this ISO timestamp").option("-l, --limit <n>", "Cap to N most-recent messages", (v) => parseInt(v, 10)).option("--json", "Output as JSON (recommended for scripting / agents)").action(
7884
+ (opts) => runAction(async () => {
7195
7885
  const { arbi, crypto: crypto2 } = await resolveDmCrypto();
7196
- const data = await sdk.dm.listDecryptedDMs(arbi, crypto2);
7197
- if (data.length === 0) {
7198
- console.log("No messages found.");
7886
+ const all = await sdk.dm.listDecryptedDMs(arbi, crypto2);
7887
+ const myExtId = arbi.session.getState().userExtId;
7888
+ const dms = all.filter((r) => r.type === "user_message");
7889
+ const filterOpts = {
7890
+ unread: Boolean(opts.unread),
7891
+ from: typeof opts.from === "string" ? opts.from : void 0,
7892
+ thread: typeof opts.thread === "string" ? opts.thread : void 0,
7893
+ since: typeof opts.since === "string" ? opts.since : void 0,
7894
+ myExtId
7895
+ };
7896
+ const filtered = dms.filter((r) => rowMatchesFilters(r, filterOpts));
7897
+ const limited = typeof opts.limit === "number" && opts.limit > 0 ? filtered.slice(0, opts.limit) : filtered;
7898
+ if (opts.json) {
7899
+ printJson(limited.map((r) => ({ ...r, encrypted_in_transit: true })));
7900
+ return;
7901
+ }
7902
+ if (limited.length === 0) {
7903
+ process.stderr.write("No messages found.\n");
7199
7904
  return;
7200
7905
  }
7906
+ const peer = (r) => {
7907
+ const s = r.sender;
7908
+ const recip = r.recipient;
7909
+ return s?.external_id === myExtId ? recip : s;
7910
+ };
7201
7911
  printTable(
7202
7912
  [
7203
7913
  { header: "ID", width: 16, value: (r) => r.external_id },
7204
- { header: "TYPE", width: 18, value: (r) => r.type },
7205
7914
  {
7206
- header: "FROM",
7207
- width: 22,
7915
+ header: "DIR",
7916
+ width: 4,
7917
+ value: (r) => r.sender?.external_id === myExtId ? "\u2192" : "\u2190"
7918
+ },
7919
+ {
7920
+ header: "PEER",
7921
+ width: 26,
7208
7922
  value: (r) => {
7209
- const s = r.sender;
7210
- return sdk.formatUserName(s) || s?.email || "";
7923
+ const p = peer(r);
7924
+ return sdk.formatUserName(p) || p?.email || "";
7211
7925
  }
7212
7926
  },
7213
7927
  { header: "READ", width: 6, value: (r) => r.read ? "yes" : "no" },
7214
- { header: "CONTENT", width: 40, value: (r) => r.content ?? "" }
7928
+ {
7929
+ header: "CONTENT",
7930
+ width: 50,
7931
+ value: (r) => truncate(r.content ?? "", 49)
7932
+ }
7215
7933
  ],
7216
- data
7934
+ limited
7217
7935
  );
7218
- })
7936
+ })()
7219
7937
  );
7220
7938
  dm.command("send [recipient] [content...]").description("Send an E2E encrypted DM (interactive if no args)").action(
7221
7939
  (recipient, contentParts) => runAction(async () => {
7222
7940
  const { arbi, crypto: crypto2 } = await resolveDmCrypto();
7223
7941
  if (!recipient) {
7224
- const contacts2 = await sdk.contacts.listContacts(arbi);
7225
- if (contacts2.length === 0) {
7942
+ requireInteractive(
7943
+ 'Pass recipient + message as arguments: arbi dm send <email-or-id> "<message>"'
7944
+ );
7945
+ const contacts = await sdk.contacts.listContacts(arbi);
7946
+ if (contacts.length === 0) {
7226
7947
  error("No contacts found. Add contacts first: arbi contacts add <email>");
7227
7948
  process.exit(1);
7228
7949
  }
7229
7950
  recipient = await promptSelect(
7230
7951
  "Send to",
7231
- contacts2.map((c) => {
7952
+ contacts.map((c) => {
7232
7953
  const u = c.user;
7233
7954
  const name = sdk.formatUserName(u);
7234
7955
  return {
@@ -7241,79 +7962,26 @@ function registerDmCommand(program2) {
7241
7962
  }
7242
7963
  let content = contentParts?.length ? contentParts.join(" ") : void 0;
7243
7964
  if (!content) {
7965
+ requireInteractive(
7966
+ 'Pass the message as a positional argument: arbi dm send <email> "your message"'
7967
+ );
7244
7968
  content = await promptInput("Message");
7245
7969
  }
7246
- const contacts = await sdk.contacts.listContacts(arbi);
7247
- let recipientExtId = recipient;
7248
- let recipientPubKey;
7249
- if (recipient.includes("@")) {
7250
- const match = contacts.find((c) => c.email === recipient);
7251
- if (match) {
7252
- const u = match.user;
7253
- recipientExtId = u?.external_id ?? match.external_id;
7254
- recipientPubKey = u?.encryption_public_key;
7255
- } else {
7256
- try {
7257
- const agentsList = await sdk.agents.listAgents(arbi);
7258
- const agentMatch = agentsList.find((a) => a.email === recipient);
7259
- if (agentMatch) {
7260
- recipientExtId = agentMatch.external_id;
7261
- recipientPubKey = agentMatch.encryption_public_key;
7262
- }
7263
- } catch {
7264
- }
7265
- if (!recipientPubKey) {
7266
- try {
7267
- const wsUsers = await sdk.workspaces.listWorkspaceUsers(arbi);
7268
- const wsMatch = wsUsers.find(
7269
- (wu) => wu.email === recipient
7270
- );
7271
- if (wsMatch) {
7272
- const u = wsMatch;
7273
- recipientExtId = u.external_id;
7274
- recipientPubKey = u.encryption_public_key;
7275
- }
7276
- } catch {
7277
- }
7278
- }
7279
- if (!recipientExtId || recipientExtId === recipient) {
7280
- error(`No contact, agent, or workspace member found with email: ${recipient}`);
7281
- process.exit(1);
7282
- }
7283
- }
7284
- } else {
7285
- const match = contacts.find((c) => {
7286
- const u = c.user;
7287
- return u?.external_id === recipient || c.external_id === recipient;
7288
- });
7289
- recipientPubKey = match?.user?.encryption_public_key;
7290
- if (!recipientPubKey) {
7291
- try {
7292
- const agentsList = await sdk.agents.listAgents(arbi);
7293
- const agentMatch = agentsList.find((a) => a.external_id === recipientExtId);
7294
- if (agentMatch) {
7295
- recipientPubKey = agentMatch.encryption_public_key;
7296
- }
7297
- } catch {
7298
- }
7299
- }
7300
- if (!recipientPubKey) {
7301
- try {
7302
- const wsUsers = await sdk.workspaces.listWorkspaceUsers(arbi);
7303
- const wsMatch = wsUsers.find(
7304
- (wu) => wu.external_id === recipientExtId
7305
- );
7306
- if (wsMatch) {
7307
- recipientPubKey = wsMatch.encryption_public_key;
7308
- }
7309
- } catch {
7310
- }
7311
- }
7970
+ const resolved = await resolveRecipient(arbi, recipient);
7971
+ if (!resolved) {
7972
+ error(
7973
+ `No contact, agent, or workspace member found for: ${recipient}
7974
+ Try: arbi contacts add ${recipient.includes("@") ? recipient : "<their-email>"}`
7975
+ );
7976
+ process.exit(3);
7312
7977
  }
7978
+ const recipientExtId = resolved.extId;
7979
+ const recipientPubKey = resolved.pubKey;
7313
7980
  if (!recipientPubKey) {
7314
- error("Cannot send encrypted DM \u2014 recipient public key not found.");
7315
- error("The recipient is not in your contacts or workspace.");
7316
- process.exit(1);
7981
+ error(
7982
+ "Cannot send encrypted DM \u2014 recipient public key not found.\nAdd them as a contact first: arbi contacts add <email>"
7983
+ );
7984
+ process.exit(3);
7317
7985
  }
7318
7986
  const data = await sdk.dm.sendEncryptedDM(
7319
7987
  arbi,
@@ -7327,21 +7995,26 @@ function registerDmCommand(program2) {
7327
7995
  crypto2
7328
7996
  );
7329
7997
  for (const n of data) {
7330
- success(`Sent: ${n.external_id} \u2192 ${n.recipient.email}`);
7998
+ success(`Sent (encrypted): ${n.external_id} \u2192 ${n.recipient.email}`);
7331
7999
  }
7332
8000
  })()
7333
8001
  );
7334
- dm.command("read [ids...]").description("Mark messages as read (interactive picker if no IDs given)").action(
7335
- (ids) => runAction(async () => {
8002
+ dm.command("read [ids...]").description("Mark messages as read (interactive picker if no IDs given)").option("--all", "Mark every unread message as read (no picker)").action(
8003
+ (ids, opts) => runAction(async () => {
7336
8004
  const { arbi, crypto: crypto2 } = await resolveDmCrypto();
7337
8005
  let msgIds = ids && ids.length > 0 ? ids : void 0;
8006
+ if (!msgIds && opts?.all) {
8007
+ const data2 = await sdk.dm.listDecryptedDMs(arbi, crypto2);
8008
+ msgIds = data2.filter((m) => !m.read).map((m) => m.external_id);
8009
+ }
7338
8010
  if (!msgIds) {
7339
8011
  const data2 = await sdk.dm.listDecryptedDMs(arbi, crypto2);
7340
8012
  const unread = data2.filter((m) => !m.read);
7341
8013
  if (unread.length === 0) {
7342
- console.log("No unread messages.");
8014
+ process.stderr.write("No unread messages.\n");
7343
8015
  return;
7344
8016
  }
8017
+ requireInteractive("Pass IDs directly or use --all to mark every unread message read.");
7345
8018
  msgIds = await promptCheckbox(
7346
8019
  "Select messages to mark as read",
7347
8020
  unread.map((m) => {
@@ -7355,20 +8028,29 @@ function registerDmCommand(program2) {
7355
8028
  );
7356
8029
  if (msgIds.length === 0) return;
7357
8030
  }
8031
+ if (msgIds.length === 0) {
8032
+ process.stderr.write("No unread messages.\n");
8033
+ return;
8034
+ }
7358
8035
  const data = await sdk.dm.markRead(arbi, msgIds);
7359
8036
  success(`Marked ${data.length} message(s) as read.`);
7360
8037
  })()
7361
8038
  );
7362
- dm.command("delete [ids...]").description("Delete messages (interactive picker if no IDs given)").action(
7363
- (ids) => runAction(async () => {
8039
+ dm.command("delete [ids...]").description("Delete messages (interactive picker if no IDs given)").option("--all", "Delete every message in your DM history (no picker, no prompt)").option("--dry-run", "Preview which messages would be deleted (no SDK call)").action(
8040
+ (ids, opts) => runAction(async () => {
7364
8041
  const { arbi, crypto: crypto2 } = await resolveDmCrypto();
7365
8042
  let msgIds = ids && ids.length > 0 ? ids : void 0;
8043
+ if (!msgIds && opts?.all) {
8044
+ const data = await sdk.dm.listDecryptedDMs(arbi, crypto2);
8045
+ msgIds = data.map((m) => m.external_id);
8046
+ }
7366
8047
  if (!msgIds) {
7367
8048
  const data = await sdk.dm.listDecryptedDMs(arbi, crypto2);
7368
8049
  if (data.length === 0) {
7369
- console.log("No messages found.");
8050
+ process.stderr.write("No messages found.\n");
7370
8051
  return;
7371
8052
  }
8053
+ requireInteractive("Pass IDs directly or use --all to delete the whole inbox.");
7372
8054
  msgIds = await promptCheckbox(
7373
8055
  "Select messages to delete",
7374
8056
  data.map((m) => {
@@ -7382,18 +8064,34 @@ function registerDmCommand(program2) {
7382
8064
  );
7383
8065
  if (msgIds.length === 0) return;
7384
8066
  }
8067
+ if (msgIds.length === 0) {
8068
+ process.stderr.write("No messages to delete.\n");
8069
+ return;
8070
+ }
8071
+ if (opts?.dryRun) {
8072
+ dryRun(`delete ${msgIds.length} message(s)`, msgIds);
8073
+ return;
8074
+ }
7385
8075
  await sdk.dm.deleteDMs(arbi, msgIds);
7386
8076
  success(`Deleted ${msgIds.length} message(s).`);
7387
8077
  })()
7388
8078
  );
7389
- dm.action(async () => {
8079
+ dm.arguments("[maybeSubcommand]").action(async (maybe) => {
8080
+ if (maybe) {
8081
+ suggestSubcommandAndExit(
8082
+ "dm",
8083
+ maybe,
8084
+ dm.commands.map((c) => c.name())
8085
+ );
8086
+ }
7390
8087
  await dm.commands.find((c) => c.name() === "list").parseAsync([], { from: "user" });
7391
8088
  });
7392
8089
  }
8090
+ init_prompts();
7393
8091
  async function fetchTagChoices(arbi) {
7394
8092
  const data = await sdk.tags.listTags(arbi);
7395
8093
  if (data.length === 0) {
7396
- console.log("No tags found.");
8094
+ process.stderr.write("No tags found.\n");
7397
8095
  process.exit(0);
7398
8096
  }
7399
8097
  return {
@@ -7406,17 +8104,17 @@ async function fetchTagChoices(arbi) {
7406
8104
  };
7407
8105
  }
7408
8106
  function registerTagsCommand(program2) {
7409
- const tags = program2.command("tags").description("Manage tags");
8107
+ const tags = program2.command("tags").description("Tags: list, create, delete, update, share, unshare, rename");
7410
8108
  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(
7411
8109
  (opts) => runAction(async () => {
7412
8110
  const { arbi } = await resolveWorkspace(opts.workspace);
7413
8111
  const data = await sdk.tags.listTags(arbi);
7414
8112
  if (opts.json) {
7415
- console.log(JSON.stringify(data, null, 2));
8113
+ printJson(data);
7416
8114
  return;
7417
8115
  }
7418
8116
  if (data.length === 0) {
7419
- console.log("No tags found.");
8117
+ process.stderr.write("No tags found.\n");
7420
8118
  return;
7421
8119
  }
7422
8120
  printTable(
@@ -7470,17 +8168,42 @@ function registerTagsCommand(program2) {
7470
8168
  success(`Created: ${data.name} (${ref(data.external_id)})`);
7471
8169
  })()
7472
8170
  );
7473
- tags.command("delete [id]").description("Delete a tag (interactive picker if no ID given)").action(
7474
- (id) => runAction(async () => {
8171
+ 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(
8172
+ (id, opts) => runAction(async () => {
7475
8173
  const { arbi } = await resolveWorkspace();
7476
8174
  if (!id) {
7477
8175
  const { choices } = await fetchTagChoices(arbi);
7478
8176
  id = await promptSelect("Select tag to delete", choices);
7479
8177
  }
8178
+ if (opts?.dryRun) {
8179
+ dryRun("delete tag", id);
8180
+ return;
8181
+ }
7480
8182
  const data = await sdk.tags.deleteTag(arbi, id);
7481
8183
  success(data?.detail ?? `Deleted tag ${id}`);
7482
8184
  })()
7483
8185
  );
8186
+ tags.command("share <id>").description("Mark a tag as shared (visible to other workspace members)").action(
8187
+ (id) => runAction(async () => {
8188
+ const { arbi } = await resolveWorkspace();
8189
+ const data = await sdk.tags.updateTag(arbi, id, { shared: true });
8190
+ success(`Shared: ${data.name} (${ref(data.external_id)})`);
8191
+ })()
8192
+ );
8193
+ tags.command("unshare <id>").description("Mark a tag as private (owner-only)").action(
8194
+ (id) => runAction(async () => {
8195
+ const { arbi } = await resolveWorkspace();
8196
+ const data = await sdk.tags.updateTag(arbi, id, { shared: false });
8197
+ success(`Unshared: ${data.name} (${ref(data.external_id)})`);
8198
+ })()
8199
+ );
8200
+ tags.command("rename <id> <name>").description("Rename a tag").action(
8201
+ (id, name) => runAction(async () => {
8202
+ const { arbi } = await resolveWorkspace();
8203
+ const data = await sdk.tags.updateTag(arbi, id, { name });
8204
+ success(`Renamed: ${data.name} (${ref(data.external_id)})`);
8205
+ })()
8206
+ );
7484
8207
  tags.command("update [id] [json]").description("Update a tag (interactive picker + form if no args)").action(
7485
8208
  (id, json) => runAction(async () => {
7486
8209
  const { arbi } = await resolveWorkspace();
@@ -7507,34 +8230,43 @@ function registerTagsCommand(program2) {
7507
8230
  success(`Updated: ${data.name} (${ref(data.external_id)})`);
7508
8231
  })()
7509
8232
  );
7510
- tags.option("--json", "Output as JSON").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").action(async (opts, cmd) => {
7511
- const args = [];
7512
- if (opts.json) args.push("--json");
7513
- if (opts.workspace) args.push("-w", opts.workspace);
7514
- await cmd.commands.find((c) => c.name() === "list").parseAsync(args, { from: "user" });
8233
+ tags.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
8234
+ const tail = tags.args ?? [];
8235
+ await tags.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
7515
8236
  });
7516
8237
  }
7517
- async function pickTag(arbi, workspaceId, message = "Select tag") {
8238
+ init_prompts();
8239
+ async function resolveTagRef(arbi, ref2) {
8240
+ if (ref2.startsWith("tag-")) return ref2;
7518
8241
  const data = await sdk.tags.listTags(arbi);
7519
- if (data.length === 0) {
7520
- console.log("No tags found.");
7521
- process.exit(0);
7522
- }
7523
- return promptSelect(
7524
- message,
7525
- data.map((t) => ({
7526
- name: `${t.name} (${t.tag_type?.type ?? ""})`,
7527
- value: t.external_id,
7528
- description: t.external_id
7529
- }))
8242
+ const res = resolveByIdOrName(data, ref2);
8243
+ if (!res.ok) failResolveAndExit("tag", ref2, res);
8244
+ return res.id;
8245
+ }
8246
+ function pickTag(arbi, message = "Select tag") {
8247
+ return sdk.tags.listTags(arbi).then(
8248
+ (data) => pickFromList(
8249
+ data,
8250
+ (t) => ({
8251
+ name: `${t.name} (${t.tag_type?.type ?? ""})`,
8252
+ value: t.external_id,
8253
+ description: t.external_id
8254
+ }),
8255
+ {
8256
+ message,
8257
+ emptyMessage: "No tags found.",
8258
+ nonTtyHint: "Pass <tag-id> (or unique tag name) and <doc-ids...> directly."
8259
+ }
8260
+ )
7530
8261
  );
7531
8262
  }
7532
- async function pickDocs(arbi, workspaceId, message = "Select documents") {
8263
+ async function pickDocs(arbi, message = "Select documents") {
7533
8264
  const data = await sdk.documents.listDocuments(arbi);
7534
8265
  if (data.length === 0) {
7535
- console.log("No documents found.");
8266
+ process.stderr.write("No documents found.\n");
7536
8267
  process.exit(0);
7537
8268
  }
8269
+ requireInteractive("Pass <doc-ids...> directly: arbi docs --ids to list them.");
7538
8270
  return promptCheckbox(
7539
8271
  message,
7540
8272
  data.map((d) => ({
@@ -7544,32 +8276,37 @@ async function pickDocs(arbi, workspaceId, message = "Select documents") {
7544
8276
  );
7545
8277
  }
7546
8278
  function registerDoctagsCommand(program2) {
7547
- const doctagsCmd = program2.command("doctags").description("Manage document tags (doctags)");
7548
- doctagsCmd.command("create [tag-id] [doc-ids...]").description("Assign a tag to documents (interactive pickers if no args)").option("-n, --note <text>", "Note for the doctag").action(
7549
- (tagId, docIds, opts) => runAction(async () => {
7550
- const { arbi, workspaceId } = await resolveWorkspace();
7551
- if (!tagId) tagId = await pickTag(arbi, workspaceId, "Select tag to assign");
7552
- if (!docIds || docIds.length === 0)
7553
- docIds = await pickDocs(arbi, workspaceId, "Select documents to tag");
8279
+ const doctagsCmd = program2.command("doctags").description("Doctags (tag\u2194doc links): create, delete, generate");
8280
+ doctagsCmd.command("create [tag] [doc-ids...]").description("Assign a tag to documents (tag accepts ID or unique name; pickers if no args)").option("-n, --note <text>", "Note for the doctag").action(
8281
+ (tagRef, docIds, opts) => runAction(async () => {
8282
+ const { arbi } = await resolveWorkspace();
8283
+ let tagId = tagRef ? await resolveTagRef(arbi, tagRef) : void 0;
8284
+ if (!tagId) tagId = await pickTag(arbi, "Select tag to assign");
8285
+ if (!docIds || docIds.length === 0) docIds = await pickDocs(arbi, "Select documents to tag");
7554
8286
  if (docIds.length === 0) return;
7555
8287
  const data = await sdk.doctags.assignDocTags(arbi, tagId, docIds, opts?.note);
7556
8288
  success(`Created ${data.length} doctag(s) for tag ${tagId}.`);
7557
8289
  })()
7558
8290
  );
7559
- doctagsCmd.command("delete [tag-id] [doc-ids...]").description("Remove a tag from documents (interactive pickers if no args)").action(
7560
- (tagId, docIds) => runAction(async () => {
7561
- const { arbi, workspaceId } = await resolveWorkspace();
7562
- if (!tagId) tagId = await pickTag(arbi, workspaceId, "Select tag to remove");
8291
+ doctagsCmd.command("delete [tag] [doc-ids...]").description("Remove a tag from documents (tag accepts ID or unique name; pickers if no args)").option("--dry-run", "Preview which doctag links would be removed (no SDK call)").action(
8292
+ (tagRef, docIds, opts) => runAction(async () => {
8293
+ const { arbi } = await resolveWorkspace();
8294
+ let tagId = tagRef ? await resolveTagRef(arbi, tagRef) : void 0;
8295
+ if (!tagId) tagId = await pickTag(arbi, "Select tag to remove");
7563
8296
  if (!docIds || docIds.length === 0)
7564
- docIds = await pickDocs(arbi, workspaceId, "Select documents to untag");
8297
+ docIds = await pickDocs(arbi, "Select documents to untag");
7565
8298
  if (docIds.length === 0) return;
8299
+ if (opts?.dryRun) {
8300
+ dryRun(`remove tag ${tagId} from ${docIds.length} document(s)`, docIds);
8301
+ return;
8302
+ }
7566
8303
  await sdk.doctags.removeDocTags(arbi, tagId, docIds);
7567
8304
  success(`Removed tag ${tagId} from ${docIds.length} document(s).`);
7568
8305
  })()
7569
8306
  );
7570
8307
  doctagsCmd.command("generate").description("AI-generate doctags (interactive pickers if no flags)").option("--tags <ids>", "Comma-separated tag IDs").option("--docs <ids>", "Comma-separated document IDs").action(
7571
8308
  (opts) => runAction(async () => {
7572
- const { arbi, workspaceId } = await resolveWorkspace();
8309
+ const { arbi } = await resolveWorkspace();
7573
8310
  let tagIds;
7574
8311
  if (opts.tags) {
7575
8312
  tagIds = opts.tags.split(",").map((s) => s.trim());
@@ -7592,7 +8329,7 @@ function registerDoctagsCommand(program2) {
7592
8329
  if (opts.docs) {
7593
8330
  docIds = opts.docs.split(",").map((s) => s.trim());
7594
8331
  } else {
7595
- docIds = await pickDocs(arbi, workspaceId, "Select documents for AI tagging");
8332
+ docIds = await pickDocs(arbi, "Select documents for AI tagging");
7596
8333
  if (docIds.length === 0) return;
7597
8334
  }
7598
8335
  const data = await sdk.doctags.generateDocTags(arbi, tagIds, docIds);
@@ -7602,10 +8339,11 @@ function registerDoctagsCommand(program2) {
7602
8339
  })()
7603
8340
  );
7604
8341
  }
8342
+ init_prompts();
7605
8343
  async function pickConversation(arbi, workspaceId, message = "Select conversation") {
7606
8344
  const data = await sdk.conversations.listConversations(arbi);
7607
8345
  if (data.length === 0) {
7608
- console.log("No conversations found.");
8346
+ process.stderr.write("No conversations found.\n");
7609
8347
  process.exit(0);
7610
8348
  }
7611
8349
  return promptSelect(
@@ -7618,17 +8356,17 @@ async function pickConversation(arbi, workspaceId, message = "Select conversatio
7618
8356
  );
7619
8357
  }
7620
8358
  function registerConversationsCommand(program2) {
7621
- const conv = program2.command("conversations").description("Manage conversations");
8359
+ const conv = program2.command("convo").description("Conversations: list, switch, threads, delete, share, title, message, trace");
7622
8360
  conv.command("list").description("List conversations in the active workspace").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").option("--json", "Output as JSON").action(
7623
8361
  (opts) => runAction(async () => {
7624
8362
  const { arbi } = await resolveWorkspace(opts.workspace);
7625
8363
  const data = await sdk.conversations.listConversations(arbi);
7626
8364
  if (opts.json) {
7627
- console.log(JSON.stringify(data, null, 2));
8365
+ printJson(data);
7628
8366
  return;
7629
8367
  }
7630
8368
  if (data.length === 0) {
7631
- console.log("No conversations found.");
8369
+ process.stderr.write("No conversations found.\n");
7632
8370
  return;
7633
8371
  }
7634
8372
  printTable(
@@ -7650,8 +8388,39 @@ function registerConversationsCommand(program2) {
7650
8388
  console.log(JSON.stringify(data, null, 2));
7651
8389
  })()
7652
8390
  );
7653
- conv.command("delete [conversation-id]").description("Delete a conversation (interactive picker if no ID given)").action(
8391
+ conv.command("switch <conversation-id>").description(
8392
+ "Continue a previous conversation \u2014 next `arbi ask` resumes from its latest message"
8393
+ ).action(
7654
8394
  (conversationId) => runAction(async () => {
8395
+ const { arbi, workspaceId } = await resolveWorkspace();
8396
+ const data = await sdk.conversations.getConversationThreads(arbi, conversationId);
8397
+ const threads = data.threads ?? [];
8398
+ if (threads.length === 0) {
8399
+ process.stderr.write(`Conversation ${conversationId} has no threads.
8400
+ `);
8401
+ process.exit(3);
8402
+ }
8403
+ const primary = threads[0];
8404
+ const leafId = primary.leaf_message_ext_id;
8405
+ const lastAssistant = [...primary.history ?? []].reverse().find((m) => m.role === "assistant");
8406
+ const resumeId = leafId ?? lastAssistant?.external_id;
8407
+ if (!resumeId) {
8408
+ process.stderr.write(
8409
+ `No resumable message in conversation ${conversationId}. (Empty thread or never reached an assistant turn.)
8410
+ `
8411
+ );
8412
+ process.exit(3);
8413
+ }
8414
+ updateChatSession({
8415
+ conversationExtId: conversationId,
8416
+ lastMessageExtId: resumeId,
8417
+ workspaceId
8418
+ });
8419
+ success(`Switched to conversation ${conversationId} (resume from ${resumeId}).`);
8420
+ })()
8421
+ );
8422
+ conv.command("delete [conversation-id]").description("Delete a conversation (interactive picker if no ID given)").option("--dry-run", "Preview which conversation would be deleted (no SDK call)").action(
8423
+ (conversationId, opts) => runAction(async () => {
7655
8424
  const { arbi, workspaceId } = await resolveWorkspace();
7656
8425
  if (!conversationId)
7657
8426
  conversationId = await pickConversation(
@@ -7659,6 +8428,10 @@ function registerConversationsCommand(program2) {
7659
8428
  workspaceId,
7660
8429
  "Select conversation to delete"
7661
8430
  );
8431
+ if (opts?.dryRun) {
8432
+ dryRun("delete conversation", conversationId);
8433
+ return;
8434
+ }
7662
8435
  const data = await sdk.conversations.deleteConversation(arbi, conversationId);
7663
8436
  success(data?.detail ?? `Deleted conversation ${conversationId}`);
7664
8437
  })()
@@ -7681,11 +8454,18 @@ function registerConversationsCommand(program2) {
7681
8454
  success(data?.detail ?? `Updated title to: ${title}`);
7682
8455
  })()
7683
8456
  );
7684
- conv.command("message <message-id>").description("Get message details").action(
8457
+ conv.command("message <message-id>").description("Get message details (full decrypted JSON, useful for debugging)").action(
7685
8458
  (messageId) => runAction(async () => {
7686
8459
  const { arbi } = await resolveWorkspace();
7687
8460
  const data = await sdk.conversations.getMessage(arbi, messageId);
7688
- console.log(JSON.stringify(data, null, 2));
8461
+ printJson(data);
8462
+ })()
8463
+ );
8464
+ conv.command("trace <message-id>").description("Decrypted execution trace for one agent response (alias for `message`)").action(
8465
+ (messageId) => runAction(async () => {
8466
+ const { arbi } = await resolveWorkspace();
8467
+ const data = await sdk.conversations.getMessage(arbi, messageId);
8468
+ printJson(data);
7689
8469
  })()
7690
8470
  );
7691
8471
  conv.command("delete-message <message-id>").description("Delete a message and its descendants").action(
@@ -7695,20 +8475,18 @@ function registerConversationsCommand(program2) {
7695
8475
  success(data?.detail ?? `Deleted message ${messageId}`);
7696
8476
  })()
7697
8477
  );
7698
- conv.option("--json", "Output as JSON").option("-w, --workspace <id>", "Workspace ID (defaults to selected workspace)").action(async (opts, cmd) => {
7699
- const args = [];
7700
- if (opts.json) args.push("--json");
7701
- if (opts.workspace) args.push("-w", opts.workspace);
7702
- await cmd.commands.find((c) => c.name() === "list").parseAsync(args, { from: "user" });
8478
+ conv.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
8479
+ const tail = conv.args ?? [];
8480
+ await conv.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
7703
8481
  });
7704
8482
  }
7705
8483
  function registerSettingsCommand(program2) {
7706
- const settings = program2.command("settings").description("Manage user settings");
8484
+ const settings = program2.command("settings").description("User settings: get, set");
7707
8485
  settings.command("get").description("Show current user settings (always JSON; --json accepted for consistency)").option("--json", "Output as JSON (default \u2014 always JSON)").action(
7708
8486
  () => runAction(async () => {
7709
8487
  const { arbi } = await resolveAuth();
7710
8488
  const data = await sdk.settings.getSettings(arbi);
7711
- console.log(JSON.stringify(data, null, 2));
8489
+ printJson(data);
7712
8490
  })()
7713
8491
  );
7714
8492
  settings.command("set <json>").description("Update user settings (pass JSON object)").action(
@@ -7723,6 +8501,7 @@ function registerSettingsCommand(program2) {
7723
8501
  await settings.commands.find((c) => c.name() === "get").parseAsync([], { from: "user" });
7724
8502
  });
7725
8503
  }
8504
+ init_prompts();
7726
8505
  var MODEL_SECTIONS = [
7727
8506
  "Agents",
7728
8507
  "QueryLLM",
@@ -7866,7 +8645,7 @@ async function buildConfigInteractively(arbi) {
7866
8645
  return body;
7867
8646
  }
7868
8647
  function registerAgentconfigCommand(program2) {
7869
- const ac = program2.command("agentconfig").description("Manage agent configurations");
8648
+ const ac = program2.command("agentconfig").description("Agent configurations: list, get, save, delete, schema");
7870
8649
  ac.command("list").description("List saved configuration versions").action(
7871
8650
  runAction(async () => {
7872
8651
  const { arbi } = await resolveAuth();
@@ -8013,12 +8792,29 @@ function registerTuiCommand(program2) {
8013
8792
  });
8014
8793
  }
8015
8794
  function registerUpdateCommand(program2) {
8016
- const update = program2.command("update").description("Update ARBI CLI to the latest version").action(() => {
8795
+ const update = program2.command("update").description("Update ARBI CLI to the latest version (use --check to only probe)").option("--check", "Only report whether an update is available \u2014 never install anything").option("--json", "Output {current, latest, update_available} as JSON (implies --check)").action((opts) => {
8017
8796
  const current = getCurrentVersion();
8018
- label("Current version:", current);
8019
- console.log("Checking for updates...\n");
8797
+ const checkOnly = Boolean(opts.check || opts.json);
8798
+ if (!checkOnly) {
8799
+ label("Current version:", current);
8800
+ console.log("Checking for updates...\n");
8801
+ }
8020
8802
  try {
8021
8803
  const latest = getLatestVersion(true) || child_process.execSync("npm view @arbidocs/cli version", { encoding: "utf8" }).trim();
8804
+ if (checkOnly) {
8805
+ const updateAvailable = latest !== current;
8806
+ if (opts.json) {
8807
+ printJson({ current, latest, update_available: updateAvailable });
8808
+ } else if (updateAvailable) {
8809
+ label("Current version:", current);
8810
+ label("Latest version:", latest);
8811
+ console.log("Update available. Run `arbi update` to install.");
8812
+ } else {
8813
+ label("Current version:", current);
8814
+ success("Already up to date.");
8815
+ }
8816
+ process.exit(updateAvailable ? 1 : 0);
8817
+ }
8022
8818
  if (latest === current) {
8023
8819
  success("Already up to date.");
8024
8820
  return;
@@ -8031,6 +8827,10 @@ function registerUpdateCommand(program2) {
8031
8827
  Updated to ${latest}.`);
8032
8828
  } catch (err) {
8033
8829
  const msg = err instanceof Error ? err.message : String(err);
8830
+ if (opts.json) {
8831
+ printJson({ current, latest: null, update_available: null, error: msg });
8832
+ process.exit(1);
8833
+ }
8034
8834
  error(`Update failed: ${msg}`);
8035
8835
  error("\nYou can update manually with:");
8036
8836
  dim(" npm install -g @arbidocs/cli@latest");
@@ -8042,6 +8842,7 @@ Updated to ${latest}.`);
8042
8842
  success("Auto-update enabled. ARBI CLI will update automatically on login.");
8043
8843
  });
8044
8844
  }
8845
+ init_prompts();
8045
8846
  function isExitPromptError(err) {
8046
8847
  return err instanceof Error && err.name === "ExitPromptError";
8047
8848
  }
@@ -8215,7 +9016,9 @@ async function getVerificationCode2(email, apiKey) {
8215
9016
  return Array.isArray(words) ? words.join(" ") : String(words);
8216
9017
  }
8217
9018
  function registerAgentCreateCommand(program2) {
8218
- program2.command("agent-create").description("Create a bot/test account (requires SUPPORT_API_KEY)").argument("[url]", "Server URL (auto-detected if omitted)").option("-p, --password <password>", "Account password", DEFAULT_PASSWORD).option("--workspace-name <name>", "Workspace name", "Agent Workspace").option("--email <email>", "Custom email (default: agent-{timestamp}@{domain})").action(
9019
+ program2.command("agent-create").description(
9020
+ "Create a standalone bot/test ACCOUNT via SUPPORT_API_KEY (different from `arbi agent create`, which makes a child agent under the current user)"
9021
+ ).argument("[url]", "Server URL (auto-detected if omitted)").option("-p, --password <password>", "Account password", DEFAULT_PASSWORD).option("--workspace-name <name>", "Workspace name", "Agent Workspace").option("--email <email>", "Custom email (default: agent-{timestamp}@{domain})").action(
8219
9022
  async (url, opts) => {
8220
9023
  const { config, source } = store.resolveConfigWithFallbacks();
8221
9024
  if (url) {
@@ -8306,6 +9109,7 @@ function registerAgentCreateCommand(program2) {
8306
9109
  }
8307
9110
  );
8308
9111
  }
9112
+ init_prompts();
8309
9113
  function generatePassword() {
8310
9114
  const chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-_";
8311
9115
  const bytes = new Uint8Array(20);
@@ -8313,8 +9117,12 @@ function generatePassword() {
8313
9117
  return Array.from(bytes, (b) => chars[b % chars.length]).join("");
8314
9118
  }
8315
9119
  function registerAgentCommand(program2) {
8316
- const agent = program2.command("agent").description("Manage persistent agents");
8317
- agent.command("create").description("Create a persistent agent with its own login credentials").option("--name <name>", "Agent name (letters, digits, hyphens, underscores)").option("-w, --workspace <ids...>", "Workspace IDs to share with the agent").option("--role <role>", "Workspace role: collaborator or guest", "collaborator").action(
9120
+ const agent = program2.command("agent").description(
9121
+ "Child agents under your user: list, create, delete (for SUPPORT_API_KEY bot accounts, use `arbi agent-create`)"
9122
+ );
9123
+ agent.command("create").description(
9124
+ "Create a child persistent agent owned by the current user (different from `arbi agent-create`, which is a SUPPORT_API_KEY bot-account flow)"
9125
+ ).option("--name <name>", "Agent name (letters, digits, hyphens, underscores)").option("-w, --workspace <ids...>", "Workspace IDs to share with the agent").option("--role <role>", "Workspace role: collaborator or guest", "collaborator").action(
8318
9126
  (opts) => runAction(async () => {
8319
9127
  const { arbi, loginResult } = await resolveAuth();
8320
9128
  const config = resolveConfig();
@@ -8390,12 +9198,16 @@ function registerAgentCommand(program2) {
8390
9198
  dim(`On the agent machine: arbi login --email ${agent2.email} --password <password>`);
8391
9199
  })()
8392
9200
  );
8393
- agent.command("list").description("List your persistent agents").action(
8394
- runAction(async () => {
9201
+ agent.command("list").description("List your persistent agents").option("--json", "Output as JSON (recommended for scripting / agents)").action(
9202
+ (opts) => runAction(async () => {
8395
9203
  const { arbi } = await resolveAuth();
8396
9204
  const data = await sdk.agents.listAgents(arbi);
9205
+ if (opts?.json) {
9206
+ printJson(data);
9207
+ return;
9208
+ }
8397
9209
  if (data.length === 0) {
8398
- console.log("No agents found.");
9210
+ process.stderr.write("No agents found.\n");
8399
9211
  return;
8400
9212
  }
8401
9213
  printTable(
@@ -8410,17 +9222,18 @@ function registerAgentCommand(program2) {
8410
9222
  ],
8411
9223
  data
8412
9224
  );
8413
- })
9225
+ })()
8414
9226
  );
8415
- agent.command("delete [agent-id]").description("Delete a persistent agent (picker if no ID)").action(
8416
- (agentId) => runAction(async () => {
9227
+ agent.command("delete [agent-id]").description("Delete a persistent agent (picker if no ID)").option("-y, --yes", "Skip confirmation (required in non-TTY shells)").option("--dry-run", "Preview which agent would be deleted (no SDK call)").action(
9228
+ (agentId, opts) => runAction(async () => {
8417
9229
  const { arbi } = await resolveAuth();
8418
9230
  if (!agentId) {
8419
9231
  const data = await sdk.agents.listAgents(arbi);
8420
9232
  if (data.length === 0) {
8421
- console.log("No agents to delete.");
9233
+ process.stderr.write("No agents to delete.\n");
8422
9234
  return;
8423
9235
  }
9236
+ requireInteractive("Pass <agent-id> directly: arbi agent delete <id>");
8424
9237
  agentId = await promptSelect(
8425
9238
  "Select agent to delete",
8426
9239
  data.map((a) => ({
@@ -8429,16 +9242,30 @@ function registerAgentCommand(program2) {
8429
9242
  }))
8430
9243
  );
8431
9244
  }
8432
- const confirmed = await promptConfirm(`Delete agent ${agentId}?`, false);
8433
- if (!confirmed) {
8434
- console.log("Cancelled.");
9245
+ if (opts?.dryRun) {
9246
+ dryRun("delete agent", agentId);
8435
9247
  return;
8436
9248
  }
9249
+ if (!opts?.yes) {
9250
+ requireInteractive("Pass -y/--yes to confirm in non-TTY shells.");
9251
+ const confirmed = await promptConfirm(`Delete agent ${agentId}?`, false);
9252
+ if (!confirmed) {
9253
+ console.log("Cancelled.");
9254
+ return;
9255
+ }
9256
+ }
8437
9257
  await sdk.agents.deleteAgents(arbi, [agentId]);
8438
9258
  success(`Agent ${agentId} deleted.`);
8439
9259
  })()
8440
9260
  );
8441
- agent.action(async () => {
9261
+ agent.arguments("[maybeSubcommand]").action(async (maybe) => {
9262
+ if (maybe) {
9263
+ suggestSubcommandAndExit(
9264
+ "agent",
9265
+ maybe,
9266
+ agent.commands.map((c) => c.name())
9267
+ );
9268
+ }
8442
9269
  await agent.commands.find((c) => c.name() === "list").parseAsync([], { from: "user" });
8443
9270
  });
8444
9271
  }
@@ -8465,15 +9292,33 @@ function statusColor2(s) {
8465
9292
  return s;
8466
9293
  }
8467
9294
  function registerTaskCommand(program2) {
8468
- const task = program2.command("task").description("Manage background tasks");
8469
- task.action(async (_opts, cmd) => {
8470
- await cmd.commands.find((c) => c.name() === "list").parseAsync([], { from: "user" });
9295
+ const task = program2.command("task").description("Background tasks (from `arbi ask -b`): list, status, result");
9296
+ task.arguments("[maybeSubcommand]").action(async (maybe) => {
9297
+ if (maybe) {
9298
+ suggestSubcommandAndExit(
9299
+ "task",
9300
+ maybe,
9301
+ task.commands.map((c) => c.name())
9302
+ );
9303
+ }
9304
+ await task.commands.find((c) => c.name() === "list").parseAsync([], { from: "user" });
8471
9305
  });
8472
- task.command("list").description("List background tasks").action(
8473
- () => runAction(async () => {
9306
+ task.command("list").description("List background tasks").option("--json", "Output as JSON (recommended for scripting / agents)").action(
9307
+ (opts) => runAction(async () => {
8474
9308
  const tasks = getTasks();
9309
+ if (opts?.json) {
9310
+ const out = tasks.map((t) => ({
9311
+ id: t.id,
9312
+ status: t.status,
9313
+ question: t.question,
9314
+ age_seconds: Math.round((Date.now() - new Date(t.submittedAt).getTime()) / 1e3),
9315
+ submitted_at: t.submittedAt
9316
+ }));
9317
+ printJson(out);
9318
+ return;
9319
+ }
8475
9320
  if (tasks.length === 0) {
8476
- console.log('No tasks. Submit one with: arbi ask -b "your question"');
9321
+ process.stderr.write('No tasks. Submit one with: arbi ask -b "your question"\n');
8477
9322
  return;
8478
9323
  }
8479
9324
  printTable(
@@ -8556,8 +9401,8 @@ compdef _arbi_completions arbi
8556
9401
  ${MARKER_END}`;
8557
9402
  function getShellRcPath2() {
8558
9403
  const shell = process.env.SHELL || "";
8559
- if (shell.includes("zsh")) return path5.join(os.homedir(), ".zshrc");
8560
- return path5.join(os.homedir(), ".bashrc");
9404
+ if (shell.includes("zsh")) return path5.join(os2.homedir(), ".zshrc");
9405
+ return path5.join(os2.homedir(), ".bashrc");
8561
9406
  }
8562
9407
  function isZsh() {
8563
9408
  return (process.env.SHELL || "").includes("zsh");
@@ -8576,7 +9421,7 @@ function removeCompletionBlock(content) {
8576
9421
  return before + after;
8577
9422
  }
8578
9423
  function registerCompletionCommand(program2) {
8579
- const completion = program2.command("completion").description("Manage shell tab completion");
9424
+ const completion = program2.command("completion").description("Shell tab completion: install, uninstall, refresh");
8580
9425
  completion.command("install").description("Install shell tab completion (auto-detects bash or zsh \u2014 no shell arg needed)").action(() => {
8581
9426
  const rcPath = getShellRcPath2();
8582
9427
  if (isCompletionInstalled(rcPath)) {
@@ -8614,7 +9459,7 @@ function registerCompletionCommand(program2) {
8614
9459
  }
8615
9460
  function resolvePath(p) {
8616
9461
  const raw = p ?? ".";
8617
- const expanded = raw.startsWith("~") ? raw.replace("~", os.homedir()) : raw;
9462
+ const expanded = raw.startsWith("~") ? raw.replace("~", os2.homedir()) : raw;
8618
9463
  return path5.resolve(expanded);
8619
9464
  }
8620
9465
  function formatSize2(bytes) {
@@ -8623,9 +9468,20 @@ function formatSize2(bytes) {
8623
9468
  if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)}M`;
8624
9469
  return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)}G`;
8625
9470
  }
9471
+ function entryToRow(dir, name) {
9472
+ try {
9473
+ const stat = fs5.statSync(path5.join(dir, name));
9474
+ if (stat.isDirectory()) return { name, type: "dir", size: null };
9475
+ if (stat.isSymbolicLink()) return { name, type: "symlink", size: stat.size };
9476
+ if (stat.isFile()) return { name, type: "file", size: stat.size };
9477
+ return { name, type: "other", size: stat.size };
9478
+ } catch {
9479
+ return { name, type: "other", size: null };
9480
+ }
9481
+ }
8626
9482
  function registerLocalCommand(program2) {
8627
9483
  const local = program2.command("local").description("Local filesystem operations");
8628
- local.command("ls [path]").description("List files in a directory").option("-l, --long", "Show file sizes and types").action((path7, opts) => {
9484
+ local.command("ls [path]").description("List files in a directory").option("-l, --long", "Show file sizes and types (ignored when --json is set)").option("-a, --all", "Include dotfiles").option("--json", "Output as JSON array of {name, type, size}").action((path7, opts) => {
8629
9485
  const dir = resolvePath(path7);
8630
9486
  let entries;
8631
9487
  try {
@@ -8634,33 +9490,46 @@ function registerLocalCommand(program2) {
8634
9490
  console.error(`Cannot read directory: ${dir}`);
8635
9491
  process.exit(1);
8636
9492
  }
8637
- for (const entry of entries.filter((e) => !e.startsWith(".")).sort()) {
9493
+ const sorted = entries.filter((e) => opts?.all || !e.startsWith(".")).sort();
9494
+ if (opts?.json) {
9495
+ const rows = sorted.map((name) => entryToRow(dir, name));
9496
+ printJson(rows);
9497
+ return;
9498
+ }
9499
+ for (const entry of sorted) {
8638
9500
  if (opts?.long) {
8639
- try {
8640
- const stat = fs5.statSync(path5.join(dir, entry));
8641
- const type = stat.isDirectory() ? "dir" : "file";
8642
- const size = stat.isFile() ? formatSize2(stat.size) : "-";
8643
- console.log(`${type.padEnd(5)} ${size.padStart(10)} ${entry}`);
8644
- } catch {
8645
- console.log(`? ${"-".padStart(10)} ${entry}`);
8646
- }
9501
+ const row = entryToRow(dir, entry);
9502
+ const size = row.size !== null && row.type === "file" ? formatSize2(row.size) : "-";
9503
+ console.log(`${row.type.padEnd(5)} ${size.padStart(10)} ${entry}`);
8647
9504
  } else {
8648
9505
  console.log(entry);
8649
9506
  }
8650
9507
  }
8651
9508
  });
8652
- local.command("find <pattern>").description('Find files matching a glob (e.g. "**/*.pdf")').option("-d, --dir <path>", "Directory to search in", ".").action((pattern, opts) => {
9509
+ local.command("find <pattern>").description('Find files matching a glob (e.g. "**/*.pdf")').option("-d, --dir <path>", "Directory to search in", ".").option("--json", "Output as JSON array of matched paths").action((pattern, opts) => {
8653
9510
  const dir = resolvePath(opts.dir);
8654
9511
  const results = fs5.globSync(pattern, { cwd: dir });
9512
+ if (opts.json) {
9513
+ if (results.length === 0) {
9514
+ process.stderr.write(`No files matching "${pattern}" in ${dir}
9515
+ `);
9516
+ }
9517
+ printJson(results);
9518
+ return;
9519
+ }
8655
9520
  if (results.length === 0) {
8656
- console.log(`No files matching "${pattern}" in ${dir}`);
9521
+ process.stderr.write(`No files matching "${pattern}" in ${dir}
9522
+ `);
8657
9523
  return;
8658
9524
  }
8659
9525
  for (const file of results) {
8660
9526
  console.log(file);
8661
9527
  }
8662
9528
  });
8663
- local.command("cat <file>").description("Print file contents").option("--head <lines>", "Only show first N lines").action((file, opts) => {
9529
+ local.command("cat <file>").description("Print file contents").option("--head <lines>", "Only show first N lines").option(
9530
+ "--json",
9531
+ "Output as {path, bytes, content} \u2014 useful when content has newlines an agent needs to handle"
9532
+ ).action((file, opts) => {
8664
9533
  const filePath = resolvePath(file);
8665
9534
  let content;
8666
9535
  try {
@@ -8671,18 +9540,51 @@ function registerLocalCommand(program2) {
8671
9540
  }
8672
9541
  if (opts.head) {
8673
9542
  const n = parseInt(opts.head, 10);
8674
- console.log(content.split("\n").slice(0, n).join("\n"));
8675
- } else {
8676
- console.log(content);
9543
+ content = content.split("\n").slice(0, n).join("\n");
8677
9544
  }
9545
+ if (opts.json) {
9546
+ printJson({ path: filePath, bytes: Buffer.byteLength(content, "utf-8"), content });
9547
+ return;
9548
+ }
9549
+ console.log(content);
8678
9550
  });
8679
- local.command("tree [path]").description("Show directory tree").option("-d, --depth <n>", "Maximum depth", "3").action((path7, opts) => {
9551
+ local.command("tree [path]").description("Show directory tree").option("-d, --depth <n>", "Maximum depth", "3").option("--json", "Output as nested {name, type, children} tree").action((path7, opts) => {
8680
9552
  const dir = resolvePath(path7);
8681
9553
  const maxDepth = parseInt(opts?.depth ?? "3", 10);
9554
+ if (opts?.json) {
9555
+ printJson(buildTree(dir, maxDepth, 0));
9556
+ return;
9557
+ }
8682
9558
  console.log(path5.basename(dir) + "/");
8683
9559
  printTree(dir, "", maxDepth, 0);
8684
9560
  });
8685
9561
  }
9562
+ function buildTree(dir, maxDepth, depth) {
9563
+ const node = { name: path5.basename(dir), type: "dir", children: [] };
9564
+ if (depth >= maxDepth) return node;
9565
+ let entries;
9566
+ try {
9567
+ entries = fs5.readdirSync(dir).filter((e) => !e.startsWith(".")).sort();
9568
+ } catch {
9569
+ return node;
9570
+ }
9571
+ for (const entry of entries) {
9572
+ const full = path5.join(dir, entry);
9573
+ let isDir = false;
9574
+ try {
9575
+ isDir = fs5.statSync(full).isDirectory();
9576
+ } catch {
9577
+ node.children.push({ name: entry, type: "other" });
9578
+ continue;
9579
+ }
9580
+ if (isDir) {
9581
+ node.children.push(buildTree(full, maxDepth, depth + 1));
9582
+ } else {
9583
+ node.children.push({ name: entry, type: "file" });
9584
+ }
9585
+ }
9586
+ return node;
9587
+ }
8686
9588
  function printTree(dir, prefix, maxDepth, depth) {
8687
9589
  if (depth >= maxDepth) return;
8688
9590
  let entries;
@@ -8709,14 +9611,25 @@ function printTree(dir, prefix, maxDepth, depth) {
8709
9611
  }
8710
9612
  }
8711
9613
  }
9614
+ function humanizeTtl(seconds) {
9615
+ if (!Number.isFinite(seconds) || seconds < 0) return "";
9616
+ if (seconds < 60) return `${seconds}s`;
9617
+ if (seconds < 3600) return `${Math.round(seconds / 60)}m`;
9618
+ if (seconds < 86400) return `${Math.round(seconds / 3600)}h`;
9619
+ return `${Math.round(seconds / 86400)}d`;
9620
+ }
8712
9621
  function registerSessionCommand(program2) {
8713
9622
  const session = program2.command("session").description("List and manage sessions");
8714
- session.command("list").description("List active sessions").action(
8715
- runAction(async () => {
9623
+ session.command("list").description("List active sessions").option("--json", "Output as JSON (recommended for scripting / agents)").action(
9624
+ (opts) => runAction(async () => {
8716
9625
  const { arbi } = await resolveAuth();
8717
9626
  const data = await sdk.sessions.listSessions(arbi);
9627
+ if (opts?.json) {
9628
+ printJson(data);
9629
+ return;
9630
+ }
8718
9631
  if (data.length === 0) {
8719
- console.log("No active sessions.");
9632
+ process.stderr.write("No active sessions.\n");
8720
9633
  return;
8721
9634
  }
8722
9635
  printTable(
@@ -8728,7 +9641,13 @@ function registerSessionCommand(program2) {
8728
9641
  },
8729
9642
  { header: "NAME", width: 20, value: (r) => r.name || "" },
8730
9643
  { header: "STATUS", width: 12, value: (r) => r.status },
8731
- { header: "TTL", width: 10, value: (r) => `${r.ttl}s` },
9644
+ {
9645
+ header: "TTL",
9646
+ width: 10,
9647
+ // Raw `<n>s` was unreadable past a few hours; humanize once the
9648
+ // window grows beyond a minute.
9649
+ value: (r) => humanizeTtl(r.ttl)
9650
+ },
8732
9651
  {
8733
9652
  header: "WORKSPACES",
8734
9653
  width: 12,
@@ -8737,12 +9656,423 @@ function registerSessionCommand(program2) {
8737
9656
  ],
8738
9657
  data
8739
9658
  );
8740
- })
9659
+ })()
8741
9660
  );
8742
- session.action(async () => {
9661
+ session.arguments("[maybeSubcommand]").action(async (maybe) => {
9662
+ if (maybe) {
9663
+ suggestSubcommandAndExit(
9664
+ "session",
9665
+ maybe,
9666
+ session.commands.map((c) => c.name())
9667
+ );
9668
+ }
8743
9669
  await session.commands.find((c) => c.name() === "list").parseAsync([], { from: "user" });
8744
9670
  });
8745
9671
  }
9672
+ function registerNotificationsCommand(program2) {
9673
+ const notif = program2.command("notifications").enablePositionalOptions().description("Non-DM notifications");
9674
+ notif.command("list").description("List non-DM notifications (workspace invites, contact accepts, \u2026)").option("--unread", "Only show unread notifications").option("--type <comma-list>", "Filter to specific type(s), e.g. workspaceuser_invited").option("-l, --limit <n>", "Cap to N most-recent", (v) => parseInt(v, 10)).option("--json", "Output as JSON (recommended for scripting / agents)").action(
9675
+ (opts) => runAction(async () => {
9676
+ const { arbi, crypto: crypto2 } = await resolveDmCrypto();
9677
+ const all = await sdk.dm.listDecryptedDMs(arbi, crypto2);
9678
+ const nonDm = all.filter((r) => r.type !== "user_message");
9679
+ const typeFilter = opts.type ? new Set(
9680
+ opts.type.split(",").map((t) => t.trim()).filter(Boolean)
9681
+ ) : null;
9682
+ const filtered = nonDm.filter((r) => {
9683
+ if (opts.unread && r.read) return false;
9684
+ if (typeFilter && !typeFilter.has(r.type ?? "")) return false;
9685
+ return true;
9686
+ });
9687
+ const limited = opts.limit && opts.limit > 0 ? filtered.slice(0, opts.limit) : filtered;
9688
+ if (opts.json) {
9689
+ printJson(limited);
9690
+ return;
9691
+ }
9692
+ if (limited.length === 0) {
9693
+ process.stderr.write("No notifications.\n");
9694
+ return;
9695
+ }
9696
+ printTable(
9697
+ [
9698
+ { header: "ID", width: 16, value: (r) => r.external_id },
9699
+ { header: "TYPE", width: 24, value: (r) => r.type ?? "" },
9700
+ { header: "READ", width: 6, value: (r) => r.read ? "yes" : "no" },
9701
+ {
9702
+ header: "CONTENT",
9703
+ width: 50,
9704
+ // Non-DM rows usually carry a JSON-encoded payload in
9705
+ // `content`. Show the first slice and let the JSON path
9706
+ // expose the full thing for agents that want to introspect.
9707
+ value: (r) => truncate(r.content ?? "", 49)
9708
+ }
9709
+ ],
9710
+ limited
9711
+ );
9712
+ })()
9713
+ );
9714
+ notif.command("mark-read [ids...]").description("Mark notifications as read (use --all to drain the unread queue)").option("--all", "Mark every unread non-DM notification as read").action(
9715
+ (ids, opts) => runAction(async () => {
9716
+ const { arbi, crypto: crypto2 } = await resolveDmCrypto();
9717
+ let toMark = ids && ids.length > 0 ? ids : void 0;
9718
+ if (!toMark && opts.all) {
9719
+ const all = await sdk.dm.listDecryptedDMs(arbi, crypto2);
9720
+ toMark = all.filter((r) => r.type !== "user_message" && !r.read).map((r) => r.external_id);
9721
+ }
9722
+ if (!toMark || toMark.length === 0) {
9723
+ process.stderr.write("Nothing to mark read. (Pass IDs or --all.)\n");
9724
+ return;
9725
+ }
9726
+ const data = await sdk.dm.markRead(arbi, toMark);
9727
+ process.stdout.write(`Marked ${data.length} notification(s) as read.
9728
+ `);
9729
+ })()
9730
+ );
9731
+ notif.command("delete [ids...]").description("Delete notifications by ID (or use --all to clear non-DM noise)").option("--all", "Delete every non-DM notification (workspace invites, contact accepts, ...)").option("--dry-run", "Preview which notifications would be deleted (no SDK call)").action(
9732
+ (ids, opts) => runAction(async () => {
9733
+ const { arbi, crypto: crypto2 } = await resolveDmCrypto();
9734
+ let toDelete = ids && ids.length > 0 ? ids : void 0;
9735
+ if (!toDelete && opts.all) {
9736
+ const all = await sdk.dm.listDecryptedDMs(arbi, crypto2);
9737
+ toDelete = all.filter((r) => r.type !== "user_message").map((r) => r.external_id);
9738
+ }
9739
+ if (!toDelete || toDelete.length === 0) {
9740
+ process.stderr.write("Nothing to delete. (Pass IDs or --all.)\n");
9741
+ return;
9742
+ }
9743
+ if (opts?.dryRun) {
9744
+ dryRun(`delete ${toDelete.length} notification(s)`, toDelete);
9745
+ return;
9746
+ }
9747
+ await sdk.dm.deleteDMs(arbi, toDelete);
9748
+ process.stdout.write(`Deleted ${toDelete.length} notification(s).
9749
+ `);
9750
+ })()
9751
+ );
9752
+ notif.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
9753
+ const tail = notif.args ?? [];
9754
+ const subcommands = notif.commands.map((c) => c.name());
9755
+ const firstNonFlag = tail.find((a) => !a.startsWith("-"));
9756
+ if (firstNonFlag && !subcommands.includes(firstNonFlag)) {
9757
+ suggestSubcommandAndExit("notifications", firstNonFlag, subcommands);
9758
+ }
9759
+ await notif.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
9760
+ });
9761
+ }
9762
+ function registerProjectCommand(program2) {
9763
+ const project = program2.command("project").description("Project tenancy: list, create, refresh");
9764
+ project.command("list").description("List your projects").option("--json", "Output as JSON").action(
9765
+ (opts) => runAction(async () => {
9766
+ const { arbi } = await resolveAuth();
9767
+ const data = await sdk.projects.listProjects(arbi);
9768
+ if (!emitListOrTable(data, opts, "No projects found.")) return;
9769
+ printTable(
9770
+ [
9771
+ { header: "ID", width: 18, value: (r) => r.external_id },
9772
+ { header: "NAME", width: 30, value: (r) => r.name ?? "" },
9773
+ { header: "PLAN", width: 14, value: (r) => r.plan ?? "" },
9774
+ { header: "ROLE", width: 14, value: (r) => r.role ?? "" }
9775
+ ],
9776
+ data
9777
+ );
9778
+ })()
9779
+ );
9780
+ project.command("create <name>").description("Create a new project").action(
9781
+ (name) => runAction(async () => {
9782
+ const { arbi } = await resolveAuth();
9783
+ const data = await sdk.projects.createProject(arbi, name);
9784
+ success(`Created project: ${data.name} (${ref(data.external_id)})`);
9785
+ })()
9786
+ );
9787
+ project.command("refresh <id>").description("Refresh a project (force plan/usage sync from billing)").action(
9788
+ (id) => runAction(async () => {
9789
+ const { arbi } = await resolveAuth();
9790
+ await sdk.projects.refreshProject(arbi, id);
9791
+ success(`Refreshed project: ${ref(id)}`);
9792
+ })()
9793
+ );
9794
+ project.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
9795
+ const tail = project.args ?? [];
9796
+ const subcommands = project.commands.map((c) => c.name());
9797
+ const firstNonFlag = tail.find((a) => !a.startsWith("-"));
9798
+ if (firstNonFlag && !subcommands.includes(firstNonFlag)) {
9799
+ suggestSubcommandAndExit("project", firstNonFlag, subcommands);
9800
+ }
9801
+ await project.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
9802
+ });
9803
+ }
9804
+ function formatBytes(n) {
9805
+ if (n < 1024) return `${n}B`;
9806
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)}K`;
9807
+ if (n < 1024 * 1024 * 1024) return `${(n / (1024 * 1024)).toFixed(1)}M`;
9808
+ return `${(n / (1024 * 1024 * 1024)).toFixed(1)}G`;
9809
+ }
9810
+ function registerFilesCommand(program2) {
9811
+ const files = program2.command("files").description("Ephemeral files: list, upload, get, delete, fetch content");
9812
+ files.command("list").description("List ephemeral files attached to your account").option("--purpose <name>", "Filter by purpose (default: assistants)").option("-l, --limit <n>", "Cap to N entries", (v) => parseInt(v, 10)).option("--json", "Output as JSON").action(
9813
+ (opts) => runAction(async () => {
9814
+ const { arbi } = await resolveWorkspace();
9815
+ const data = await sdk.files.listFiles(arbi, {
9816
+ purpose: opts.purpose ?? null,
9817
+ limit: opts.limit
9818
+ });
9819
+ const items = data.data ?? [];
9820
+ if (opts.json) {
9821
+ printJson(data);
9822
+ return;
9823
+ }
9824
+ if (!emitListOrTable(items, opts, "No files found.")) return;
9825
+ printTable(
9826
+ [
9827
+ { header: "ID", width: 30, value: (r) => r.id },
9828
+ {
9829
+ header: "NAME",
9830
+ width: 30,
9831
+ value: (r) => truncate(r.filename ?? "", 29)
9832
+ },
9833
+ { header: "BYTES", width: 10, value: (r) => formatBytes(r.bytes ?? 0) },
9834
+ { header: "PURPOSE", width: 14, value: (r) => r.purpose ?? "" }
9835
+ ],
9836
+ items
9837
+ );
9838
+ })()
9839
+ );
9840
+ files.command("upload <path>").description("Upload a file (file ID is returned for use with arbi ask -b --attach)").option("--purpose <name>", "OpenAI purpose tag (default: assistants)", "assistants").action(
9841
+ (path7, opts) => runAction(async () => {
9842
+ const { arbi, loginResult } = await resolveWorkspace();
9843
+ const filePath = path5.resolve(path7);
9844
+ let bytes;
9845
+ try {
9846
+ bytes = fs5.readFileSync(filePath);
9847
+ } catch {
9848
+ error(`Cannot read file: ${filePath}`);
9849
+ process.exit(3);
9850
+ }
9851
+ const blob = new Blob([new Uint8Array(bytes)]);
9852
+ const data = await sdk.files.uploadFile(
9853
+ {
9854
+ baseUrl: arbi.fetch.baseUrl,
9855
+ accessToken: loginResult.accessToken
9856
+ },
9857
+ blob,
9858
+ path5.basename(filePath),
9859
+ opts.purpose ?? "assistants"
9860
+ );
9861
+ success(`Uploaded: ${ref(data.id)} (${formatBytes(data.bytes ?? 0)})`);
9862
+ })()
9863
+ );
9864
+ files.command("get <id>").description("Show a file's metadata (always JSON \u2014 the metadata IS the data)").action(
9865
+ (id) => runAction(async () => {
9866
+ const { arbi } = await resolveWorkspace();
9867
+ const data = await sdk.files.getFile(arbi, id);
9868
+ printJson(data);
9869
+ })()
9870
+ );
9871
+ files.command("delete <id>").description("Delete an ephemeral file").option("--dry-run", "Preview which file would be deleted (no SDK call)").action(
9872
+ (id, opts) => runAction(async () => {
9873
+ if (opts?.dryRun) {
9874
+ dryRun("delete file", id);
9875
+ return;
9876
+ }
9877
+ const { arbi } = await resolveWorkspace();
9878
+ await sdk.files.deleteFile(arbi, id);
9879
+ success(`Deleted: ${ref(id)}`);
9880
+ })()
9881
+ );
9882
+ files.command("content <id>").description("Fetch a file's raw bytes (writes to disk or stdout)").option("-o, --output <path>", 'Output file path (use "-" for stdout)').action(
9883
+ (id, opts) => runAction(async () => {
9884
+ const { arbi, loginResult } = await resolveWorkspace();
9885
+ const res = await sdk.files.getFileContent(
9886
+ {
9887
+ baseUrl: arbi.fetch.baseUrl,
9888
+ accessToken: loginResult.accessToken
9889
+ },
9890
+ id
9891
+ );
9892
+ const buf = Buffer.from(await res.arrayBuffer());
9893
+ if (!opts.output || opts.output === "-") {
9894
+ process.stdout.write(buf);
9895
+ return;
9896
+ }
9897
+ fs5.writeFileSync(path5.resolve(opts.output), buf);
9898
+ success(`Wrote ${formatBytes(buf.length)} \u2192 ${opts.output}`);
9899
+ })()
9900
+ );
9901
+ files.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
9902
+ const tail = files.args ?? [];
9903
+ const subcommands = files.commands.map((c) => c.name());
9904
+ const firstNonFlag = tail.find((a) => !a.startsWith("-"));
9905
+ if (firstNonFlag && !subcommands.includes(firstNonFlag)) {
9906
+ suggestSubcommandAndExit("files", firstNonFlag, subcommands);
9907
+ }
9908
+ await files.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
9909
+ });
9910
+ }
9911
+
9912
+ // src/commands/usage.ts
9913
+ function registerUsageCommand(program2) {
9914
+ const usage = program2.command("usage").description("Show today's usage against the daily cap");
9915
+ usage.command("today").description("Today's tokens used / remaining (for the current user)").option("--json", "Output as JSON").action(
9916
+ (opts) => runAction(async () => {
9917
+ const { arbi } = await resolveAuth();
9918
+ const res = await arbi.fetch.GET("/v1/user/me/usage/today");
9919
+ if (res.error) {
9920
+ throw new Error(`Failed to fetch usage: ${JSON.stringify(res.error)}`);
9921
+ }
9922
+ const data = res.data;
9923
+ if (opts.json) {
9924
+ printJson(data);
9925
+ return;
9926
+ }
9927
+ for (const [k, v] of Object.entries(data)) {
9928
+ label(`${k}:`, typeof v === "object" ? JSON.stringify(v) : String(v));
9929
+ }
9930
+ dim("\nPass --json for machine-readable output.");
9931
+ })()
9932
+ );
9933
+ usage.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
9934
+ const tail = usage.args ?? [];
9935
+ const subcommands = usage.commands.map((c) => c.name());
9936
+ const firstNonFlag = tail.find((a) => !a.startsWith("-"));
9937
+ if (firstNonFlag && !subcommands.includes(firstNonFlag)) {
9938
+ suggestSubcommandAndExit("usage", firstNonFlag, subcommands);
9939
+ }
9940
+ await usage.commands.find((c) => c.name() === "today").parseAsync(tail, { from: "user" });
9941
+ });
9942
+ }
9943
+ init_prompts();
9944
+ function registerAuthCommand(program2) {
9945
+ const auth = program2.command("auth").description("Self-service account ops: profile, change-email, delete-account, sso-config");
9946
+ auth.command("profile").description("Show or update your display profile (given/family name, picture)").option("--name <given>", "New given name").option("--family <family>", "New family name (empty string clears it)").option(
9947
+ "--picture <path-or-url>",
9948
+ "Profile picture (local path \u2192 base64 inline; URL \u2192 stored verbatim)"
9949
+ ).action(
9950
+ (opts) => runAction(async () => {
9951
+ const { arbi } = await resolveAuth();
9952
+ const noUpdates = !opts.name && opts.family === void 0 && !opts.picture;
9953
+ if (noUpdates) {
9954
+ const myExtId = arbi.session.getState().userExtId;
9955
+ const res2 = await arbi.fetch.GET("/v1/workspace/users");
9956
+ if (res2.error || !res2.data) {
9957
+ error(
9958
+ "No profile-read endpoint exists. To inspect: arbi status --json or arbi contacts --json"
9959
+ );
9960
+ process.exit(1);
9961
+ }
9962
+ const me = res2.data.find(
9963
+ (u) => u.external_id === myExtId
9964
+ );
9965
+ const profile = me ?? { external_id: myExtId };
9966
+ for (const [k, v] of Object.entries(profile)) {
9967
+ label(`${k}:`, typeof v === "object" ? JSON.stringify(v) : String(v));
9968
+ }
9969
+ dim("\nPass --name/--family/--picture to update.");
9970
+ return;
9971
+ }
9972
+ let picture = opts.picture;
9973
+ if (picture && !/^https?:|^data:/.test(picture)) {
9974
+ try {
9975
+ const bytes = fs5.readFileSync(path5.resolve(picture));
9976
+ const ext = picture.toLowerCase().split(".").pop();
9977
+ const mime = ext === "png" ? "image/png" : ext === "gif" ? "image/gif" : "image/jpeg";
9978
+ picture = `data:${mime};base64,${bytes.toString("base64")}`;
9979
+ } catch {
9980
+ error(`Cannot read picture file: ${picture}`);
9981
+ process.exit(3);
9982
+ }
9983
+ }
9984
+ const body = {};
9985
+ if (opts.name !== void 0) body.given_name = opts.name;
9986
+ if (opts.family !== void 0) body.family_name = opts.family;
9987
+ if (picture !== void 0) body.picture = picture;
9988
+ const res = await arbi.fetch.PATCH("/v1/user/profile", { body });
9989
+ if (res.error) throw new Error(`Profile update failed: ${JSON.stringify(res.error)}`);
9990
+ success("Profile updated.");
9991
+ })()
9992
+ );
9993
+ auth.command("change-email <new-email>").description("Request a verification code for an email change (SSO users update at IdP)").action(
9994
+ (newEmail) => runAction(async () => {
9995
+ const { arbi } = await resolveAuth();
9996
+ const res = await arbi.fetch.POST("/v1/user/request-email-change", {
9997
+ body: { new_email: newEmail }
9998
+ });
9999
+ if (res.error) throw new Error(`request-email-change failed: ${JSON.stringify(res.error)}`);
10000
+ success(`Verification code sent to ${newEmail}.`);
10001
+ dim(`Confirm with: arbi auth confirm-email <code>`);
10002
+ })()
10003
+ );
10004
+ auth.command("confirm-email <code>").description("Confirm an email change with the code from change-email").option("--new-email <email>", "The new email being confirmed (required)").action(
10005
+ (code, opts) => runAction(async () => {
10006
+ if (!opts.newEmail) {
10007
+ error("Missing --new-email. The new address is required to complete the change.");
10008
+ process.exit(1);
10009
+ }
10010
+ const { arbi } = await resolveAuth();
10011
+ const res = await arbi.fetch.POST("/v1/user/confirm-email-change", {
10012
+ body: { new_email: opts.newEmail, verification_code: code }
10013
+ });
10014
+ if (res.error) throw new Error(`confirm-email-change failed: ${JSON.stringify(res.error)}`);
10015
+ success(`Email changed to ${opts.newEmail}.`);
10016
+ })()
10017
+ );
10018
+ auth.command("delete-account").description("Request account deletion (30-day grace window \u2014 use cancel-deletion to undo)").option("-y, --yes", "Skip confirmation (required in non-TTY shells)").option("--dry-run", "Preview the deletion request (no SDK call)").action(
10019
+ (opts) => runAction(async () => {
10020
+ const { arbi } = await resolveAuth();
10021
+ if (opts.dryRun) {
10022
+ const email = arbi.session.getState().userExtId ?? "(unknown)";
10023
+ dryRun("request account deletion (30-day grace)", email);
10024
+ return;
10025
+ }
10026
+ if (!opts.yes) {
10027
+ requireInteractive("Pass -y/--yes to confirm in non-TTY shells.");
10028
+ const confirmed = await promptConfirm(
10029
+ "Request deletion of this account (30-day grace)?",
10030
+ false
10031
+ );
10032
+ if (!confirmed) {
10033
+ console.log("Cancelled.");
10034
+ return;
10035
+ }
10036
+ }
10037
+ const res = await arbi.fetch.POST("/v1/user/request-deletion", { body: {} });
10038
+ if (res.error) throw new Error(`request-deletion failed: ${JSON.stringify(res.error)}`);
10039
+ success("Deletion requested. You have 30 days to cancel with: arbi auth cancel-deletion");
10040
+ })()
10041
+ );
10042
+ auth.command("cancel-deletion").description("Cancel an in-flight account deletion request").action(
10043
+ () => runAction(async () => {
10044
+ const { arbi } = await resolveAuth();
10045
+ const res = await arbi.fetch.POST("/v1/user/cancel-deletion", { body: {} });
10046
+ if (res.error) throw new Error(`cancel-deletion failed: ${JSON.stringify(res.error)}`);
10047
+ success("Deletion cancelled. Account is active.");
10048
+ })()
10049
+ );
10050
+ auth.command("sso-config").description("Show SSO config for this deployment (sso_enabled, Auth0 domain, audience)").option("--json", "Output as JSON").action(
10051
+ (opts) => runAction(async () => {
10052
+ const { arbi } = await resolveAuth();
10053
+ const res = await arbi.fetch.GET("/v1/user/sso-config");
10054
+ if (res.error) throw new Error(`sso-config failed: ${JSON.stringify(res.error)}`);
10055
+ if (opts.json) {
10056
+ printJson(res.data);
10057
+ return;
10058
+ }
10059
+ for (const [k, v] of Object.entries(res.data)) {
10060
+ label(`${k}:`, typeof v === "object" ? JSON.stringify(v) : String(v));
10061
+ }
10062
+ dim("\nTo log in: arbi login --sso -e <email> -p <master-password>");
10063
+ dim("(--sso still needs --password \u2014 the master password is the E2E key seed.)");
10064
+ })()
10065
+ );
10066
+ auth.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
10067
+ const tail = auth.args ?? [];
10068
+ const subcommands = auth.commands.map((c) => c.name());
10069
+ const firstNonFlag = tail.find((a) => !a.startsWith("-"));
10070
+ if (firstNonFlag && !subcommands.includes(firstNonFlag)) {
10071
+ suggestSubcommandAndExit("auth", firstNonFlag, subcommands);
10072
+ }
10073
+ await auth.commands.find((c) => c.name() === "profile").parseAsync(tail, { from: "user" });
10074
+ });
10075
+ }
8746
10076
 
8747
10077
  // src/index.ts
8748
10078
  console.debug = () => {
@@ -8753,7 +10083,7 @@ console.info = (...args) => {
8753
10083
  _origInfo(...args);
8754
10084
  };
8755
10085
  var program = new commander.Command();
8756
- program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.3.62");
10086
+ program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.3.64").showHelpAfterError(true).showSuggestionAfterError(true);
8757
10087
  registerConfigCommand(program);
8758
10088
  registerLoginCommand(program);
8759
10089
  registerRegisterCommand(program);
@@ -8785,6 +10115,41 @@ registerCompletionCommand(program);
8785
10115
  registerListenCommand(program);
8786
10116
  registerSessionCommand(program);
8787
10117
  registerLocalCommand(program);
10118
+ registerNotificationsCommand(program);
10119
+ registerProjectCommand(program);
10120
+ registerFilesCommand(program);
10121
+ registerUsageCommand(program);
10122
+ registerAuthCommand(program);
10123
+ applyHelpGroups(program, {
10124
+ "Getting started:": [
10125
+ "quickstart",
10126
+ "login",
10127
+ "register",
10128
+ "logout",
10129
+ "status",
10130
+ "config",
10131
+ "completion"
10132
+ ],
10133
+ "Workspaces:": ["workspaces", "workspace", "project"],
10134
+ "Documents:": ["docs", "doc", "add", "upload", "download"],
10135
+ "Search & ask:": ["ask", "find", "cite", "watch"],
10136
+ "Tagging:": ["tags", "doctags"],
10137
+ "Conversations & messaging:": ["convo", "dm", "listen", "notifications", "contacts"],
10138
+ "Agents & tasks:": ["agent", "agent-create", "task", "agentconfig", "session"],
10139
+ "Account:": ["auth", "usage", "files"],
10140
+ "System:": ["health", "models", "settings", "tui", "local", "update"]
10141
+ });
10142
+ function applyHelpGroups(prog, groups) {
10143
+ for (const [groupLabel, names] of Object.entries(groups)) {
10144
+ for (const name of names) {
10145
+ const cmd = prog.commands.find((c) => c.name() === name);
10146
+ const hg = cmd?.helpGroup;
10147
+ if (cmd && typeof hg === "function") {
10148
+ hg.call(cmd, groupLabel);
10149
+ }
10150
+ }
10151
+ }
10152
+ }
8788
10153
  var completionIdx = process.argv.indexOf("--get-completions");
8789
10154
  if (completionIdx !== -1) {
8790
10155
  const line = process.argv[completionIdx + 1] ?? "";