@arbidocs/cli 0.4.108 → 0.4.110

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
@@ -3695,7 +3695,7 @@ function getLatestVersion(skipCache = false) {
3695
3695
  }
3696
3696
  }
3697
3697
  function getCurrentVersion() {
3698
- return "0.4.108";
3698
+ return "0.4.110";
3699
3699
  }
3700
3700
  function readChangelog(fromVersion, toVersion) {
3701
3701
  try {
@@ -3748,17 +3748,17 @@ function showChangelog(fromVersion, toVersion) {
3748
3748
  async function checkForUpdates(autoUpdate) {
3749
3749
  try {
3750
3750
  const latest = getLatestVersion();
3751
- if (!latest || latest === "0.4.108") return;
3751
+ if (!latest || latest === "0.4.110") return;
3752
3752
  if (autoUpdate) {
3753
3753
  warn(`
3754
- Your arbi version is out of date (${"0.4.108"} \u2192 ${latest}). Updating...`);
3754
+ Your arbi version is out of date (${"0.4.110"} \u2192 ${latest}). Updating...`);
3755
3755
  child_process.execSync("npm install -g @arbidocs/cli@latest", { stdio: "inherit" });
3756
- showChangelog("0.4.108", latest);
3756
+ showChangelog("0.4.110", latest);
3757
3757
  console.log(`Updated to ${latest}.`);
3758
3758
  } else {
3759
3759
  warn(
3760
3760
  `
3761
- Your arbi version is out of date (${"0.4.108"} \u2192 ${latest}).
3761
+ Your arbi version is out of date (${"0.4.110"} \u2192 ${latest}).
3762
3762
  Run "arbi update" to upgrade, or "arbi update auto" to always stay up to date.`
3763
3763
  );
3764
3764
  }
@@ -3790,10 +3790,10 @@ function markNagShown(latest) {
3790
3790
  function hintUpdateOnError() {
3791
3791
  try {
3792
3792
  const cached = readCache();
3793
- if (!cached || cached.latest === "0.4.108") return;
3793
+ if (!cached || cached.latest === "0.4.110") return;
3794
3794
  if (!shouldShowNag(cached.latest)) return;
3795
3795
  warn(
3796
- `Your arbi version is out of date (${"0.4.108"} \u2192 ${cached.latest}). Run "arbi update".`
3796
+ `Your arbi version is out of date (${"0.4.110"} \u2192 ${cached.latest}). Run "arbi update".`
3797
3797
  );
3798
3798
  markNagShown(cached.latest);
3799
3799
  } catch {
@@ -4020,15 +4020,15 @@ async function resolveWorkspace(workspaceOpt, { skipNotifications = false } = {}
4020
4020
  throw err;
4021
4021
  }
4022
4022
  }
4023
- async function resolveDmCrypto() {
4023
+ async function resolveChatCrypto() {
4024
4024
  const authCtx = await resolveAuth();
4025
4025
  const { arbi, loginResult } = authCtx;
4026
4026
  const userExtId = arbi.session.getState().userExtId;
4027
4027
  if (!userExtId) {
4028
- error("No user ID in session \u2014 cannot set up DM encryption.");
4028
+ error("No user ID in session \u2014 cannot set up chat encryption.");
4029
4029
  process.exit(1);
4030
4030
  }
4031
- const crypto = sdk.dm.createDmCryptoContext(arbi, loginResult.signingPrivateKey, userExtId);
4031
+ const crypto = sdk.chats.createChatCryptoContext(loginResult.signingPrivateKey, userExtId);
4032
4032
  return { ...authCtx, crypto };
4033
4033
  }
4034
4034
  function truncate(value, width) {
@@ -4261,7 +4261,7 @@ async function startListening(agentName, config) {
4261
4261
  process.exit(1);
4262
4262
  }
4263
4263
  dim("Starting DM listener...");
4264
- const { arbi, crypto } = await resolveDmCrypto();
4264
+ const { arbi, crypto } = await resolveChatCrypto();
4265
4265
  const fullConfig = resolveConfig();
4266
4266
  const sessionId = arbi.session.getState().userExtId ?? "default";
4267
4267
  const agentId = backend === "openclaw" ? sessionId.toLowerCase() : void 0;
@@ -4876,7 +4876,7 @@ function readAttachmentFiles(paths) {
4876
4876
  }
4877
4877
  return { resolved, name: path6__default.default.basename(resolved), size: fs7__default.default.statSync(resolved).size };
4878
4878
  });
4879
- sdk.dm.checkDmAttachmentLimits(stats);
4879
+ sdk.chats.checkChatAttachmentLimits(stats);
4880
4880
  return stats.map(({ resolved, name }) => ({
4881
4881
  name,
4882
4882
  type: typeFromName(name),
@@ -4902,8 +4902,36 @@ function attachmentLabel(files) {
4902
4902
  }
4903
4903
 
4904
4904
  // src/commands/dm.ts
4905
+ var PER_CHAT = 100;
4905
4906
  async function resolveRecipient(arbi, ref2) {
4906
- return sdk.dm.resolveRecipient(arbi, ref2);
4907
+ return sdk.chats.resolveRecipient(arbi, ref2);
4908
+ }
4909
+ async function listDmRows(arbi, crypto, options) {
4910
+ const direct = await sdk.chats.listChats(arbi, "direct");
4911
+ const rows = [];
4912
+ for (const chat of direct) {
4913
+ if (!chat.last_message) continue;
4914
+ const ring = await sdk.chats.openChatKeyRing(chat, crypto);
4915
+ const raw = await sdk.chats.listChatMessages(arbi, chat.external_id, {
4916
+ limit: PER_CHAT
4917
+ });
4918
+ const byId = new Map(chat.members.map((m) => [m.user.external_id, m]));
4919
+ const me = byId.get(crypto.userExtId);
4920
+ const peer = sdk.chats.directPeer(chat, crypto.userExtId);
4921
+ const peerMember = peer ? byId.get(peer.external_id) : me;
4922
+ for (const m of await sdk.chats.decryptChatMessages(ring, raw)) {
4923
+ const mine = m.sender_ext_id === crypto.userExtId;
4924
+ const sender = (mine ? me?.user : peer) ?? null;
4925
+ const recipient = (mine ? peer ?? me?.user : me?.user) ?? null;
4926
+ rows.push({
4927
+ ...m,
4928
+ sender,
4929
+ recipient,
4930
+ read: sdk.chats.hasRead(mine ? peerMember : me, m)
4931
+ });
4932
+ }
4933
+ }
4934
+ return rows.sort((a, b) => Date.parse(b.created_at) - Date.parse(a.created_at));
4907
4935
  }
4908
4936
  function rowMatchesFilters(row, opts) {
4909
4937
  if (opts.unread && row.read) return false;
@@ -4930,17 +4958,17 @@ function rowMatchesFilters(row, opts) {
4930
4958
  }
4931
4959
  return true;
4932
4960
  }
4961
+ var unreadForMe = (rows, me) => rows.filter((r) => !r.read && r.sender_ext_id !== me);
4933
4962
  function registerDmCommand(program2) {
4934
- const dm = program2.command("dm").description("Direct messages (E2E encrypted)");
4963
+ const dm = program2.command("dm").description("Direct messages (E2E encrypted 1:1 chats)");
4935
4964
  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(
4936
4965
  "--thread <email-or-extid>",
4937
4966
  "Show the full conversation with one peer (both directions)"
4938
4967
  ).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(
4939
4968
  (opts) => runAction(async () => {
4940
- const { arbi, crypto } = await resolveDmCrypto();
4941
- const all = await sdk.dm.listDecryptedDMs(arbi, crypto);
4942
- const myExtId = arbi.session.getState().userExtId ?? void 0;
4943
- const dms = all.filter((r) => r.type === "user_message");
4969
+ const { arbi, crypto } = await resolveChatCrypto();
4970
+ const myExtId = crypto.userExtId;
4971
+ const all = await listDmRows(arbi, crypto);
4944
4972
  const filterOpts = {
4945
4973
  unread: Boolean(opts.unread),
4946
4974
  from: typeof opts.from === "string" ? opts.from : void 0,
@@ -4948,17 +4976,14 @@ function registerDmCommand(program2) {
4948
4976
  since: typeof opts.since === "string" ? opts.since : void 0,
4949
4977
  myExtId
4950
4978
  };
4951
- const filtered = dms.filter((r) => rowMatchesFilters(r, filterOpts));
4979
+ const filtered = all.filter((r) => rowMatchesFilters(r, filterOpts)).filter((r) => !filterOpts.unread || r.sender_ext_id !== myExtId);
4952
4980
  const limited = typeof opts.limit === "number" && opts.limit > 0 ? filtered.slice(0, opts.limit) : filtered;
4953
4981
  if (opts.json) {
4954
4982
  printJson(
4955
- limited.map(({ attachmentFiles, ...r }) => ({
4983
+ limited.map(({ attachmentFiles, decryptFailed, ...r }) => ({
4956
4984
  ...r,
4957
- ...attachmentFiles ? {
4958
- attachment_files: publicAttachmentFiles(
4959
- attachmentFiles
4960
- )
4961
- } : {},
4985
+ ...attachmentFiles ? { attachment_files: publicAttachmentFiles(attachmentFiles) } : {},
4986
+ ...decryptFailed ? { decrypt_failed: true } : {},
4962
4987
  encrypted_in_transit: true
4963
4988
  }))
4964
4989
  );
@@ -4979,7 +5004,7 @@ function registerDmCommand(program2) {
4979
5004
  {
4980
5005
  header: "DIR",
4981
5006
  width: 4,
4982
- value: (r) => r.sender?.external_id === myExtId ? "\u2192" : "\u2190"
5007
+ value: (r) => r.sender_ext_id === myExtId ? "\u2192" : "\u2190"
4983
5008
  },
4984
5009
  {
4985
5010
  header: "PEER",
@@ -5003,14 +5028,14 @@ function registerDmCommand(program2) {
5003
5028
  );
5004
5029
  })()
5005
5030
  );
5006
- dm.command("send [recipient] [content...]").description("Send an E2E encrypted DM (interactive if no args)").option(
5031
+ dm.command("send [recipient] [content...]").description("Send an E2E encrypted direct message (interactive if no args)").option(
5007
5032
  "-a, --attach <path>",
5008
5033
  "Attach a file, encrypted end to end (repeatable; up to 10 files of 15 MB each)",
5009
5034
  collectPaths
5010
5035
  ).action(
5011
5036
  (recipient, contentParts, opts) => runAction(async () => {
5012
5037
  const attachmentInputs = readAttachmentFiles(opts?.attach ?? []);
5013
- const { arbi, crypto } = await resolveDmCrypto();
5038
+ const { arbi, crypto } = await resolveChatCrypto();
5014
5039
  if (!recipient) {
5015
5040
  requireInteractive(
5016
5041
  'Pass recipient + message as arguments: arbi dm send <email-or-id> "<message>"'
@@ -5049,30 +5074,11 @@ Try: arbi contacts add ${recipient.includes("@") ? recipient : "<their-email>"}`
5049
5074
  );
5050
5075
  process.exit(3);
5051
5076
  }
5052
- const recipientExtId = resolved.extId;
5053
- const recipientPubKey = resolved.pubKey;
5054
- if (!recipientPubKey) {
5055
- error(
5056
- "Cannot send encrypted DM \u2014 recipient public key not found.\nAdd them as a contact first: arbi contacts add <email>"
5057
- );
5058
- process.exit(3);
5059
- }
5060
- const attachments = await sdk.dm.uploadDmAttachments(arbi, attachmentInputs);
5061
- const data = await sdk.dm.sendEncryptedDM(
5062
- arbi,
5063
- [
5064
- {
5065
- recipient_ext_id: recipientExtId,
5066
- content,
5067
- recipient_encryption_public_key: recipientPubKey
5068
- }
5069
- ],
5070
- crypto,
5071
- { attachments }
5072
- );
5073
- for (const n of data) {
5074
- success(`Sent (encrypted): ${n.external_id} \u2192 ${n.recipient.email}`);
5075
- }
5077
+ const attachments = await sdk.chats.uploadChatAttachments(arbi, attachmentInputs);
5078
+ const sent = await sdk.chats.sendDirectMessage(arbi, crypto, resolved.extId, content, {
5079
+ attachments
5080
+ });
5081
+ success(`Sent (encrypted): ${sent.external_id} \u2192 ${recipient} (chat ${sent.chat_ext_id})`);
5076
5082
  for (const f of attachments) {
5077
5083
  success(`Attached (encrypted): ${f.id} ${f.name} (${f.size} bytes)`);
5078
5084
  }
@@ -5085,18 +5091,19 @@ Try: arbi contacts add ${recipient.includes("@") ? recipient : "<their-email>"}`
5085
5091
  "Save this attachment of the message (ids from `arbi dm list --json`) instead"
5086
5092
  ).option("-o, --output <path>", "Where to save the attachment (default: its file name)").action(
5087
5093
  (ids, opts) => runAction(async () => {
5088
- const { arbi, crypto } = await resolveDmCrypto();
5094
+ const { arbi, crypto } = await resolveChatCrypto();
5095
+ const me = crypto.userExtId;
5089
5096
  let msgIds = ids && ids.length > 0 ? ids : void 0;
5097
+ const rows = await listDmRows(arbi, crypto);
5090
5098
  if (typeof opts?.attachment === "string") {
5091
5099
  if (msgIds?.length !== 1) {
5092
- throw new Error("Name exactly one message: arbi dm read <ntf-id> --attachment <att-id>");
5100
+ throw new Error("Name exactly one message: arbi dm read <chm-id> --attachment <att-id>");
5093
5101
  }
5094
- const rows = await sdk.dm.listDecryptedDMs(arbi, crypto);
5095
5102
  const row = rows.find((m) => m.external_id === msgIds[0]);
5096
5103
  if (!row) throw new Error(`No message ${msgIds[0]}`);
5097
5104
  if (row.attachmentsError) throw new Error(row.attachmentsError);
5098
5105
  const file = findAttachment(row, opts.attachment);
5099
- const bytes = await sdk.dm.downloadDmAttachment(arbi, file);
5106
+ const bytes = await sdk.chats.downloadChatAttachment(arbi, file);
5100
5107
  const target = attachmentOutputPath(
5101
5108
  file,
5102
5109
  typeof opts.output === "string" ? opts.output : void 0
@@ -5105,13 +5112,9 @@ Try: arbi contacts add ${recipient.includes("@") ? recipient : "<their-email>"}`
5105
5112
  success(`Saved ${target} (${bytes.length} bytes, decrypted)`);
5106
5113
  return;
5107
5114
  }
5108
- if (!msgIds && opts?.all) {
5109
- const data2 = await sdk.dm.listDecryptedDMs(arbi, crypto);
5110
- msgIds = data2.filter((m) => !m.read).map((m) => m.external_id);
5111
- }
5115
+ const unread = unreadForMe(rows, me);
5116
+ if (!msgIds && opts?.all) msgIds = unread.map((m) => m.external_id);
5112
5117
  if (!msgIds) {
5113
- const data2 = await sdk.dm.listDecryptedDMs(arbi, crypto);
5114
- const unread = data2.filter((m) => !m.read);
5115
5118
  if (unread.length === 0) {
5116
5119
  process.stderr.write("No unread messages.\n");
5117
5120
  return;
@@ -5120,8 +5123,7 @@ Try: arbi contacts add ${recipient.includes("@") ? recipient : "<their-email>"}`
5120
5123
  msgIds = await promptCheckbox(
5121
5124
  "Select messages to mark as read",
5122
5125
  unread.map((m) => {
5123
- const s = m.sender;
5124
- const from = sdk.formatUserName(s) || s?.email || "";
5126
+ const from = sdk.formatUserName(m.sender) || m.sender?.email || "";
5125
5127
  return {
5126
5128
  name: `${from}: ${(m.content ?? "").slice(0, 50)}`,
5127
5129
  value: m.external_id
@@ -5134,32 +5136,42 @@ Try: arbi contacts add ${recipient.includes("@") ? recipient : "<their-email>"}`
5134
5136
  process.stderr.write("No unread messages.\n");
5135
5137
  return;
5136
5138
  }
5137
- const data = await sdk.dm.markRead(arbi, msgIds);
5138
- success(`Marked ${data.length} message(s) as read.`);
5139
+ const wanted = new Set(msgIds);
5140
+ const targets = rows.filter((r) => wanted.has(r.external_id));
5141
+ const missing = msgIds.filter((id) => !targets.some((r) => r.external_id === id));
5142
+ if (missing.length) throw new Error(`No message ${missing.join(", ")}`);
5143
+ const newestPerChat = /* @__PURE__ */ new Map();
5144
+ for (const r of targets) {
5145
+ const cur = newestPerChat.get(r.chat_ext_id);
5146
+ if (!cur || Date.parse(r.created_at) > Date.parse(cur.created_at)) {
5147
+ newestPerChat.set(r.chat_ext_id, r);
5148
+ }
5149
+ }
5150
+ for (const [chatExtId, r] of newestPerChat) {
5151
+ await sdk.chats.markChatRead(arbi, chatExtId, r.external_id);
5152
+ }
5153
+ success(`Marked ${targets.length} message(s) as read.`);
5139
5154
  })()
5140
5155
  );
5141
- 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(
5156
+ dm.command("delete [ids...]").description("Delete your own messages (interactive picker if no IDs given)").option("--all", "Delete every message you sent in direct chats (no picker, no prompt)").option("--dry-run", "Preview which messages would be deleted (no SDK call)").action(
5142
5157
  (ids, opts) => runAction(async () => {
5143
- const { arbi, crypto } = await resolveDmCrypto();
5158
+ const { arbi, crypto } = await resolveChatCrypto();
5159
+ const rows = await listDmRows(arbi, crypto);
5160
+ const own = rows.filter((r) => r.sender_ext_id === crypto.userExtId);
5144
5161
  let msgIds = ids && ids.length > 0 ? ids : void 0;
5145
- if (!msgIds && opts?.all) {
5146
- const data = await sdk.dm.listDecryptedDMs(arbi, crypto);
5147
- msgIds = data.map((m) => m.external_id);
5148
- }
5162
+ if (!msgIds && opts?.all) msgIds = own.map((m) => m.external_id);
5149
5163
  if (!msgIds) {
5150
- const data = await sdk.dm.listDecryptedDMs(arbi, crypto);
5151
- if (data.length === 0) {
5164
+ if (own.length === 0) {
5152
5165
  process.stderr.write("No messages found.\n");
5153
5166
  return;
5154
5167
  }
5155
- requireInteractive("Pass IDs directly or use --all to delete the whole inbox.");
5168
+ requireInteractive("Pass IDs directly or use --all to delete all your messages.");
5156
5169
  msgIds = await promptCheckbox(
5157
5170
  "Select messages to delete",
5158
- data.map((m) => {
5159
- const s = m.sender;
5160
- const from = sdk.formatUserName(s) || s?.email || "";
5171
+ own.map((m) => {
5172
+ const to = sdk.formatUserName(m.recipient) || m.recipient?.email || "";
5161
5173
  return {
5162
- name: `${from}: ${(m.content ?? "").slice(0, 50)}`,
5174
+ name: `\u2192 ${to}: ${(m.content ?? "").slice(0, 50)}`,
5163
5175
  value: m.external_id
5164
5176
  };
5165
5177
  })
@@ -5170,11 +5182,16 @@ Try: arbi contacts add ${recipient.includes("@") ? recipient : "<their-email>"}`
5170
5182
  process.stderr.write("No messages to delete.\n");
5171
5183
  return;
5172
5184
  }
5185
+ const chatOf = new Map(rows.map((r) => [r.external_id, r.chat_ext_id]));
5186
+ const unknown = msgIds.filter((id) => !chatOf.has(id));
5187
+ if (unknown.length) throw new Error(`No message ${unknown.join(", ")}`);
5173
5188
  if (opts?.dryRun) {
5174
5189
  dryRun(`delete ${msgIds.length} message(s)`, msgIds);
5175
5190
  return;
5176
5191
  }
5177
- await sdk.dm.deleteDMs(arbi, msgIds);
5192
+ for (const id of msgIds) {
5193
+ await sdk.chats.deleteChatMessage(arbi, chatOf.get(id), id);
5194
+ }
5178
5195
  success(`Deleted ${msgIds.length} message(s).`);
5179
5196
  })()
5180
5197
  );
@@ -5538,7 +5555,7 @@ function registerWorkspacesCommand(program2) {
5538
5555
  "Target a specific workspace id (advanced \u2014 usually omitted; the owner picks)"
5539
5556
  ).action(
5540
5557
  (owner, opts) => runAction(async () => {
5541
- const { arbi, crypto, loginResult } = await resolveDmCrypto();
5558
+ const { arbi, crypto, loginResult } = await resolveChatCrypto();
5542
5559
  const resolved = await resolveRecipient(arbi, owner);
5543
5560
  if (!resolved || !resolved.pubKey) {
5544
5561
  error(
@@ -5547,7 +5564,7 @@ Try: arbi contacts add ${owner.includes("@") ? owner : "<their-email>"}`
5547
5564
  );
5548
5565
  process.exit(3);
5549
5566
  }
5550
- await sdk.dm.requestWorkspaceAccess(
5567
+ await sdk.notifications.requestWorkspaceAccess(
5551
5568
  arbi,
5552
5569
  {
5553
5570
  recipientExtId: resolved.extId,
@@ -8302,6 +8319,10 @@ Timeout (${timeoutSec}s), closing.`));
8302
8319
  );
8303
8320
  }
8304
8321
  init_prompts();
8322
+ var asContact = (row) => row;
8323
+ function contactLabel(c) {
8324
+ return c.card?.name || sdk.formatUserName(c.user) || "";
8325
+ }
8305
8326
  function redactPictures(rows) {
8306
8327
  return rows.map((row) => {
8307
8328
  const user = row.user;
@@ -8317,7 +8338,7 @@ function redactPictures(rows) {
8317
8338
  });
8318
8339
  }
8319
8340
  function registerContactsCommand(program2) {
8320
- const contacts = program2.command("contacts").description("Contacts: list, add, remove");
8341
+ const contacts = program2.command("contacts").description("Contacts: list, add, search, remove");
8321
8342
  contacts.command("list").description("List all contacts").option("--json", "Output as JSON").option("--include-pictures", "Keep base64 profile-picture bytes in --json output").action(
8322
8343
  (opts) => runAction(async () => {
8323
8344
  const { arbi } = await resolveAuth();
@@ -8332,20 +8353,20 @@ function registerContactsCommand(program2) {
8332
8353
  }
8333
8354
  printTable(
8334
8355
  [
8335
- { header: "ID", width: 16, value: (r) => r.external_id },
8356
+ { header: "ID", width: 16, value: (r) => asContact(r).external_id ?? "" },
8357
+ { header: "NAME", width: 20, value: (r) => contactLabel(asContact(r)) },
8336
8358
  {
8337
- header: "NAME",
8338
- width: 20,
8339
- value: (r) => sdk.formatUserName(r.user)
8359
+ header: "EMAIL",
8360
+ width: 30,
8361
+ value: (r) => asContact(r).email ?? asContact(r).card?.emails?.[0]?.email ?? ""
8340
8362
  },
8341
- { header: "EMAIL", width: 30, value: (r) => r.email },
8342
- { header: "STATUS", width: 18, value: (r) => r.status }
8363
+ { header: "STATUS", width: 18, value: (r) => asContact(r).status }
8343
8364
  ],
8344
8365
  data
8345
8366
  );
8346
8367
  })()
8347
8368
  );
8348
- contacts.command("add [emails...]").description("Add contacts by email (prompt if no emails given)").action(
8369
+ contacts.command("add [emails...]").description("Invite or link people by email (prompt if no emails given)").action(
8349
8370
  (emails) => runAction(async () => {
8350
8371
  const { arbi } = await resolveAuth();
8351
8372
  if (!emails || emails.length === 0) {
@@ -8354,12 +8375,39 @@ function registerContactsCommand(program2) {
8354
8375
  emails = input2.split(",").map((e) => e.trim()).filter(Boolean);
8355
8376
  if (emails.length === 0) return;
8356
8377
  }
8357
- const data = await sdk.contacts.addContacts(arbi, emails);
8378
+ const data = await sdk.contacts.inviteContacts(arbi, emails);
8358
8379
  for (const c of data) {
8359
- success(`Added: ${c.email} (${c.external_id}) \u2014 ${c.status}`);
8380
+ if (c.status === "failed") {
8381
+ process.stderr.write(`Failed: ${c.email} \u2014 ${c.outcome_detail ?? "not invited"}
8382
+ `);
8383
+ } else {
8384
+ success(`Added: ${c.email} (${c.external_id}) \u2014 ${c.outcome ?? c.status}`);
8385
+ }
8360
8386
  }
8361
8387
  })()
8362
8388
  );
8389
+ contacts.command("search <query>").description("Search your contacts and people who share a workspace with you").option("--json", "Output as JSON").action(
8390
+ (query, opts) => runAction(async () => {
8391
+ const { arbi } = await resolveAuth();
8392
+ const data = await sdk.contacts.searchContacts(arbi, query);
8393
+ if (opts.json) {
8394
+ printJson(data);
8395
+ return;
8396
+ }
8397
+ if (data.length === 0) {
8398
+ process.stderr.write("No matches.\n");
8399
+ return;
8400
+ }
8401
+ printTable(
8402
+ [
8403
+ { header: "NAME", width: 24, value: (r) => r.name ?? "" },
8404
+ { header: "EMAIL", width: 30, value: (r) => r.email },
8405
+ { header: "SOURCE", width: 10, value: (r) => r.source }
8406
+ ],
8407
+ data
8408
+ );
8409
+ })()
8410
+ );
8363
8411
  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(
8364
8412
  (ids, opts) => runAction(async () => {
8365
8413
  const { arbi } = await resolveAuth();
@@ -8373,10 +8421,11 @@ function registerContactsCommand(program2) {
8373
8421
  requireInteractive("Pass contact IDs directly: arbi contacts remove cnt-\u2026");
8374
8422
  contactIds = await promptCheckbox(
8375
8423
  "Select contacts to remove",
8376
- data.map((c) => {
8377
- const name = sdk.formatUserName(c.user);
8424
+ data.filter((c) => c.external_id).map((c) => {
8425
+ const name = contactLabel(c);
8426
+ const email = c.email ?? "";
8378
8427
  return {
8379
- name: name ? `${name} (${c.email})` : c.email,
8428
+ name: name && email ? `${name} (${email})` : name || email || "(no name)",
8380
8429
  value: c.external_id
8381
8430
  };
8382
8431
  })
@@ -8387,7 +8436,7 @@ function registerContactsCommand(program2) {
8387
8436
  dryRun(`remove ${contactIds.length} contact(s)`, contactIds);
8388
8437
  return;
8389
8438
  }
8390
- await sdk.contacts.removeContacts(arbi, contactIds);
8439
+ for (const id of contactIds) await sdk.contacts.deleteContact(arbi, id);
8391
8440
  success(`Removed ${contactIds.length} contact(s).`);
8392
8441
  })()
8393
8442
  );
@@ -8396,6 +8445,190 @@ function registerContactsCommand(program2) {
8396
8445
  await contacts.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
8397
8446
  });
8398
8447
  }
8448
+ async function resolveMembers(arbi, refs) {
8449
+ const out = [];
8450
+ for (const ref2 of refs) {
8451
+ const r = await resolveRecipient(arbi, ref2);
8452
+ if (!r) {
8453
+ error(`No contact or workspace member found for: ${ref2}`);
8454
+ process.exit(3);
8455
+ }
8456
+ if (sdk.chats.isAgentExtId(r.extId)) {
8457
+ error(`${ref2} is a personal agent: agents cannot join a group chat`);
8458
+ process.exit(3);
8459
+ }
8460
+ out.push({ external_id: r.extId, encryption_public_key: r.pubKey });
8461
+ }
8462
+ return out;
8463
+ }
8464
+ async function readyChat(arbi, crypto, chatExtId) {
8465
+ return sdk.chats.ensureChatKeyCurrent(arbi, await sdk.chats.getChat(arbi, chatExtId), crypto);
8466
+ }
8467
+ function registerChatCommand(program2) {
8468
+ const chat = program2.command("chat").description("Chats: direct and group (E2E encrypted)");
8469
+ chat.command("list").description("List your chats, most recently active first").option("--kind <kind>", "direct | group | meeting").option("--json", "Output as JSON").action(
8470
+ (opts) => runAction(async () => {
8471
+ const { arbi, crypto } = await resolveChatCrypto();
8472
+ const list = await sdk.chats.listChats(arbi, opts.kind);
8473
+ const rows = await Promise.all(
8474
+ list.map(async (c) => {
8475
+ const ring = await sdk.chats.openChatKeyRing(c, crypto);
8476
+ const peer = sdk.chats.directPeer(c, crypto.userExtId);
8477
+ const name = c.kind === "direct" ? sdk.formatUserName(peer) || peer?.email || "(yourself)" : await sdk.chats.decryptChatTitle(ring, c) ?? c.members.filter((m) => m.user.external_id !== crypto.userExtId).map((m) => sdk.formatUserName(m.user) || m.user.email).join(", ");
8478
+ return {
8479
+ external_id: c.external_id,
8480
+ kind: c.kind,
8481
+ name,
8482
+ members: c.members.length,
8483
+ unread: c.unread_count,
8484
+ last_message_at: c.last_message_at ?? null
8485
+ };
8486
+ })
8487
+ );
8488
+ if (opts.json) {
8489
+ printJson(rows);
8490
+ return;
8491
+ }
8492
+ if (rows.length === 0) {
8493
+ process.stderr.write("No chats.\n");
8494
+ return;
8495
+ }
8496
+ printTable(
8497
+ [
8498
+ { header: "ID", width: 14, value: (r) => r.external_id },
8499
+ { header: "KIND", width: 8, value: (r) => r.kind },
8500
+ { header: "NAME", width: 36, value: (r) => truncate(r.name, 35) },
8501
+ { header: "MEMBERS", width: 8, value: (r) => String(r.members) },
8502
+ { header: "UNREAD", width: 7, value: (r) => String(r.unread) }
8503
+ ],
8504
+ rows
8505
+ );
8506
+ })()
8507
+ );
8508
+ chat.command("create").description("Create a group chat with at least one other person (no agents)").requiredOption(
8509
+ "-m, --member <email-or-id>",
8510
+ "A member (repeatable)",
8511
+ (v, prev = []) => [...prev, v]
8512
+ ).option("-t, --title <title>", "Group title (encrypted)").action(
8513
+ (opts) => runAction(async () => {
8514
+ const { arbi, crypto } = await resolveChatCrypto();
8515
+ const members = await resolveMembers(arbi, opts.member);
8516
+ const created = await sdk.chats.createGroupChat(arbi, crypto, {
8517
+ title: opts.title,
8518
+ members
8519
+ });
8520
+ success(
8521
+ `Created group (encrypted): ${created.external_id} (${created.members.length} members)`
8522
+ );
8523
+ })()
8524
+ );
8525
+ chat.command("send <chatId> <text...>").description("Post a message to a chat").action(
8526
+ (chatId, textParts) => runAction(async () => {
8527
+ const { arbi, crypto } = await resolveChatCrypto();
8528
+ const c = await readyChat(arbi, crypto, chatId);
8529
+ const sent = await sdk.chats.sendChatMessage(
8530
+ arbi,
8531
+ await sdk.chats.openChatKeyRing(c, crypto),
8532
+ textParts.join(" ")
8533
+ );
8534
+ success(`Sent (encrypted): ${sent.external_id} \u2192 ${chatId}`);
8535
+ })()
8536
+ );
8537
+ chat.command("show <chatId>").description("Show the newest messages of a chat, decrypted").option("-l, --limit <n>", "How many messages", (v) => parseInt(v, 10)).option("--json", "Output as JSON").action(
8538
+ (chatId, opts) => runAction(async () => {
8539
+ const { arbi, crypto } = await resolveChatCrypto();
8540
+ const loaded = await sdk.chats.loadDecryptedChat(
8541
+ arbi,
8542
+ crypto,
8543
+ await sdk.chats.getChat(arbi, chatId),
8544
+ { limit: opts.limit }
8545
+ );
8546
+ const names = new Map(
8547
+ loaded.chat.members.map((m) => [
8548
+ m.user.external_id,
8549
+ sdk.formatUserName(m.user) || m.user.email
8550
+ ])
8551
+ );
8552
+ const rows = loaded.messages.map(({ attachmentFiles, ...m }) => ({
8553
+ ...m,
8554
+ ...attachmentFiles ? { attachment_files: publicAttachmentFiles(attachmentFiles) } : {},
8555
+ sender: m.sender_ext_id ? names.get(m.sender_ext_id) ?? m.sender_ext_id : ""
8556
+ }));
8557
+ if (opts.json) {
8558
+ printJson({ ...loaded.chat, title: loaded.title, messages: rows });
8559
+ return;
8560
+ }
8561
+ printTable(
8562
+ [
8563
+ { header: "ID", width: 14, value: (r) => r.external_id },
8564
+ { header: "FROM", width: 24, value: (r) => truncate(r.sender, 23) },
8565
+ {
8566
+ header: "CONTENT",
8567
+ width: 60,
8568
+ value: (r) => truncate(r.content ?? "", 59)
8569
+ }
8570
+ ],
8571
+ rows
8572
+ );
8573
+ })()
8574
+ );
8575
+ chat.command("members <chatId>").description("List the members of a chat").action(
8576
+ (chatId) => runAction(async () => {
8577
+ const { arbi } = await resolveChatCrypto();
8578
+ const c = await sdk.chats.getChat(arbi, chatId);
8579
+ printTable(
8580
+ [
8581
+ { header: "ID", width: 14, value: (r) => r.id },
8582
+ { header: "NAME", width: 28, value: (r) => r.name },
8583
+ { header: "ROLE", width: 8, value: (r) => r.role }
8584
+ ],
8585
+ c.members.map((m) => ({
8586
+ id: m.user.external_id,
8587
+ name: sdk.formatUserName(m.user) || m.user.email,
8588
+ role: m.role
8589
+ }))
8590
+ );
8591
+ })()
8592
+ );
8593
+ chat.command("add <chatId> <refs...>").description("Add people to a group (they can read the current epoch)").action(
8594
+ (chatId, refs) => runAction(async () => {
8595
+ const { arbi, crypto } = await resolveChatCrypto();
8596
+ const members = await resolveMembers(arbi, refs);
8597
+ const c = await readyChat(arbi, crypto, chatId);
8598
+ const updated = await sdk.chats.addChatMembers(
8599
+ arbi,
8600
+ await sdk.chats.openChatKeyRing(c, crypto),
8601
+ members
8602
+ );
8603
+ success(`Added ${members.length} member(s) to ${chatId} (${updated.members.length} now)`);
8604
+ })()
8605
+ );
8606
+ chat.command("remove <chatId> <userId>").description("Remove a member (owners only); a new epoch key is sealed to the rest").action(
8607
+ (chatId, userId) => runAction(async () => {
8608
+ const { arbi, crypto } = await resolveChatCrypto();
8609
+ await sdk.chats.removeChatMember(arbi, chatId, userId);
8610
+ await readyChat(arbi, crypto, chatId);
8611
+ success(`Removed ${userId} from ${chatId}; rotated the chat key`);
8612
+ })()
8613
+ );
8614
+ chat.command("leave <chatId>").description("Leave a group chat").action(
8615
+ (chatId) => runAction(async () => {
8616
+ const { arbi, crypto } = await resolveChatCrypto();
8617
+ await sdk.chats.removeChatMember(arbi, chatId, crypto.userExtId);
8618
+ success(`Left ${chatId}`);
8619
+ })()
8620
+ );
8621
+ chat.arguments("[maybeSubcommand]").action(async (maybe) => {
8622
+ if (maybe) {
8623
+ suggestSubcommandAndExit(
8624
+ "chat",
8625
+ maybe,
8626
+ chat.commands.map((c) => c.name())
8627
+ );
8628
+ }
8629
+ await chat.commands.find((c) => c.name() === "list").parseAsync([], { from: "user" });
8630
+ });
8631
+ }
8399
8632
  init_prompts();
8400
8633
  function formatSpan(startIso, endIso) {
8401
8634
  const start = new Date(startIso);
@@ -11359,16 +11592,15 @@ ${message.text}`);
11359
11592
  });
11360
11593
  }
11361
11594
  function registerNotificationsCommand(program2) {
11362
- const notif = program2.command("notifications").enablePositionalOptions().description("Non-DM notifications");
11363
- 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(
11595
+ const notif = program2.command("notifications").enablePositionalOptions().description("Notifications (workspace invites, contact accepts, \u2026)");
11596
+ notif.command("list").description("List 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(
11364
11597
  (opts) => runAction(async () => {
11365
- const { arbi, crypto } = await resolveDmCrypto();
11366
- const all = await sdk.dm.listDecryptedDMs(arbi, crypto);
11367
- const nonDm = all.filter((r) => r.type !== "user_message");
11598
+ const { arbi } = await resolveAuth();
11599
+ const all = await sdk.notifications.listNotifications(arbi);
11368
11600
  const typeFilter = opts.type ? new Set(
11369
11601
  opts.type.split(",").map((t) => t.trim()).filter(Boolean)
11370
11602
  ) : null;
11371
- const filtered = nonDm.filter((r) => {
11603
+ const filtered = all.filter((r) => {
11372
11604
  if (opts.unread && r.read) return false;
11373
11605
  if (typeFilter && !typeFilter.has(r.type ?? "")) return false;
11374
11606
  return true;
@@ -11390,7 +11622,7 @@ function registerNotificationsCommand(program2) {
11390
11622
  {
11391
11623
  header: "CONTENT",
11392
11624
  width: 50,
11393
- // Non-DM rows usually carry a JSON-encoded payload in
11625
+ // Rows usually carry a JSON-encoded payload in
11394
11626
  // `content`. Show the first slice and let the JSON path
11395
11627
  // expose the full thing for agents that want to introspect.
11396
11628
  value: (r) => truncate(r.content ?? "", 49)
@@ -11400,30 +11632,30 @@ function registerNotificationsCommand(program2) {
11400
11632
  );
11401
11633
  })()
11402
11634
  );
11403
- 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(
11635
+ notif.command("mark-read [ids...]").description("Mark notifications as read (use --all to drain the unread queue)").option("--all", "Mark every unread notification as read").action(
11404
11636
  (ids, opts) => runAction(async () => {
11405
- const { arbi, crypto } = await resolveDmCrypto();
11637
+ const { arbi } = await resolveAuth();
11406
11638
  let toMark = ids && ids.length > 0 ? ids : void 0;
11407
11639
  if (!toMark && opts.all) {
11408
- const all = await sdk.dm.listDecryptedDMs(arbi, crypto);
11409
- toMark = all.filter((r) => r.type !== "user_message" && !r.read).map((r) => r.external_id);
11640
+ const all = await sdk.notifications.listNotifications(arbi);
11641
+ toMark = all.filter((r) => !r.read).map((r) => r.external_id);
11410
11642
  }
11411
11643
  if (!toMark || toMark.length === 0) {
11412
11644
  process.stderr.write("Nothing to mark read. (Pass IDs or --all.)\n");
11413
11645
  return;
11414
11646
  }
11415
- const data = await sdk.dm.markRead(arbi, toMark);
11647
+ const data = await sdk.notifications.markNotificationsRead(arbi, toMark);
11416
11648
  process.stdout.write(`Marked ${data.length} notification(s) as read.
11417
11649
  `);
11418
11650
  })()
11419
11651
  );
11420
- 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(
11652
+ notif.command("delete [ids...]").description("Delete notifications by ID (or use --all to clear them all)").option("--all", "Delete every notification (workspace invites, contact accepts, ...)").option("--dry-run", "Preview which notifications would be deleted (no SDK call)").action(
11421
11653
  (ids, opts) => runAction(async () => {
11422
- const { arbi, crypto } = await resolveDmCrypto();
11654
+ const { arbi } = await resolveAuth();
11423
11655
  let toDelete = ids && ids.length > 0 ? ids : void 0;
11424
11656
  if (!toDelete && opts.all) {
11425
- const all = await sdk.dm.listDecryptedDMs(arbi, crypto);
11426
- toDelete = all.filter((r) => r.type !== "user_message").map((r) => r.external_id);
11657
+ const all = await sdk.notifications.listNotifications(arbi);
11658
+ toDelete = all.map((r) => r.external_id);
11427
11659
  }
11428
11660
  if (!toDelete || toDelete.length === 0) {
11429
11661
  process.stderr.write("Nothing to delete. (Pass IDs or --all.)\n");
@@ -11433,7 +11665,7 @@ function registerNotificationsCommand(program2) {
11433
11665
  dryRun(`delete ${toDelete.length} notification(s)`, toDelete);
11434
11666
  return;
11435
11667
  }
11436
- await sdk.dm.deleteDMs(arbi, toDelete);
11668
+ await sdk.notifications.deleteNotifications(arbi, toDelete);
11437
11669
  process.stdout.write(`Deleted ${toDelete.length} notification(s).
11438
11670
  `);
11439
11671
  })()
@@ -12272,7 +12504,7 @@ console.info = (...args) => {
12272
12504
  _origInfo(...args);
12273
12505
  };
12274
12506
  var program = new commander.Command();
12275
- program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.4.108").showHelpAfterError(true).showSuggestionAfterError(true);
12507
+ program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.4.110").showHelpAfterError(true).showSuggestionAfterError(true);
12276
12508
  registerConfigCommand(program);
12277
12509
  registerLoginCommand(program);
12278
12510
  registerRegisterCommand(program);
@@ -12289,6 +12521,7 @@ registerFindCommand(program);
12289
12521
  registerWatchCommand(program);
12290
12522
  registerContactsCommand(program);
12291
12523
  registerDmCommand(program);
12524
+ registerChatCommand(program);
12292
12525
  registerTagsCommand(program);
12293
12526
  registerTagKindsCommand(program);
12294
12527
  registerTimelineCommand(program);