@arbidocs/cli 0.3.89 → 0.3.90

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
@@ -3694,7 +3694,7 @@ function getLatestVersion(skipCache = false) {
3694
3694
  }
3695
3695
  }
3696
3696
  function getCurrentVersion() {
3697
- return "0.3.89";
3697
+ return "0.3.90";
3698
3698
  }
3699
3699
  function readChangelog(fromVersion, toVersion) {
3700
3700
  try {
@@ -3747,17 +3747,17 @@ function showChangelog(fromVersion, toVersion) {
3747
3747
  async function checkForUpdates(autoUpdate) {
3748
3748
  try {
3749
3749
  const latest = getLatestVersion();
3750
- if (!latest || latest === "0.3.89") return;
3750
+ if (!latest || latest === "0.3.90") return;
3751
3751
  if (autoUpdate) {
3752
3752
  warn(`
3753
- Your arbi version is out of date (${"0.3.89"} \u2192 ${latest}). Updating...`);
3753
+ Your arbi version is out of date (${"0.3.90"} \u2192 ${latest}). Updating...`);
3754
3754
  child_process.execSync("npm install -g @arbidocs/cli@latest", { stdio: "inherit" });
3755
- showChangelog("0.3.89", latest);
3755
+ showChangelog("0.3.90", latest);
3756
3756
  console.log(`Updated to ${latest}.`);
3757
3757
  } else {
3758
3758
  warn(
3759
3759
  `
3760
- Your arbi version is out of date (${"0.3.89"} \u2192 ${latest}).
3760
+ Your arbi version is out of date (${"0.3.90"} \u2192 ${latest}).
3761
3761
  Run "arbi update" to upgrade, or "arbi update auto" to always stay up to date.`
3762
3762
  );
3763
3763
  }
@@ -3789,10 +3789,10 @@ function markNagShown(latest) {
3789
3789
  function hintUpdateOnError() {
3790
3790
  try {
3791
3791
  const cached = readCache();
3792
- if (!cached || cached.latest === "0.3.89") return;
3792
+ if (!cached || cached.latest === "0.3.90") return;
3793
3793
  if (!shouldShowNag(cached.latest)) return;
3794
3794
  warn(
3795
- `Your arbi version is out of date (${"0.3.89"} \u2192 ${cached.latest}). Run "arbi update".`
3795
+ `Your arbi version is out of date (${"0.3.90"} \u2192 ${cached.latest}). Run "arbi update".`
3796
3796
  );
3797
3797
  markNagShown(cached.latest);
3798
3798
  } catch {
@@ -4803,6 +4803,283 @@ function registerStatusCommand(program2) {
4803
4803
  });
4804
4804
  }
4805
4805
  init_prompts();
4806
+ async function resolveRecipient(arbi, ref2) {
4807
+ const isEmail = ref2.includes("@");
4808
+ const matchKey = isEmail ? "email" : "external_id";
4809
+ const contacts = await sdk.contacts.listContacts(arbi);
4810
+ const contactMatch = contacts.find((c) => {
4811
+ if (isEmail) return c.email === ref2;
4812
+ const u = c.user;
4813
+ return u?.external_id === ref2 || c.external_id === ref2;
4814
+ });
4815
+ if (contactMatch) {
4816
+ const u = contactMatch.user;
4817
+ const extId = u?.external_id ?? contactMatch.external_id;
4818
+ const pubKey = u?.encryption_public_key ?? "";
4819
+ if (pubKey) return { extId, pubKey };
4820
+ }
4821
+ try {
4822
+ const agents = await sdk.agents.listAgents(arbi);
4823
+ const agentMatch = agents.find((a) => a[matchKey] === ref2);
4824
+ if (agentMatch) {
4825
+ return { extId: agentMatch.external_id, pubKey: agentMatch.encryption_public_key };
4826
+ }
4827
+ } catch {
4828
+ }
4829
+ try {
4830
+ const wsUsers = await sdk.workspaces.listWorkspaceUsers(arbi);
4831
+ const wsMatch = wsUsers.find((wu) => wu[matchKey] === ref2);
4832
+ if (wsMatch) {
4833
+ const u = wsMatch;
4834
+ return {
4835
+ extId: u.external_id,
4836
+ pubKey: u.encryption_public_key
4837
+ };
4838
+ }
4839
+ } catch {
4840
+ }
4841
+ return null;
4842
+ }
4843
+ function rowMatchesFilters(row, opts) {
4844
+ if (opts.unread && row.read) return false;
4845
+ const sender = row.sender;
4846
+ const recipient = row.recipient;
4847
+ if (opts.from) {
4848
+ const needle = opts.from.toLowerCase();
4849
+ const senderMatch = sender?.email?.toLowerCase() === needle || sender?.external_id === opts.from;
4850
+ const recipientMatch = recipient?.email?.toLowerCase() === needle || recipient?.external_id === opts.from;
4851
+ if (!senderMatch && !recipientMatch) return false;
4852
+ }
4853
+ if (opts.thread) {
4854
+ const needle = opts.thread.toLowerCase();
4855
+ const peer = sender?.external_id === opts.myExtId ? recipient : sender;
4856
+ const peerMatch = peer?.email?.toLowerCase() === needle || peer?.external_id === opts.thread;
4857
+ if (!peerMatch) return false;
4858
+ }
4859
+ if (opts.since) {
4860
+ const cutoff = Date.parse(opts.since);
4861
+ if (Number.isFinite(cutoff)) {
4862
+ const createdAt = typeof row.created_at === "string" ? Date.parse(row.created_at) : NaN;
4863
+ if (!Number.isFinite(createdAt) || createdAt < cutoff) return false;
4864
+ }
4865
+ }
4866
+ return true;
4867
+ }
4868
+ function registerDmCommand(program2) {
4869
+ const dm = program2.command("dm").description("Direct messages (E2E encrypted)");
4870
+ 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(
4871
+ "--thread <email-or-extid>",
4872
+ "Show the full conversation with one peer (both directions)"
4873
+ ).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(
4874
+ (opts) => runAction(async () => {
4875
+ const { arbi, crypto: crypto2 } = await resolveDmCrypto();
4876
+ const all = await sdk.dm.listDecryptedDMs(arbi, crypto2);
4877
+ const myExtId = arbi.session.getState().userExtId ?? void 0;
4878
+ const dms = all.filter((r) => r.type === "user_message");
4879
+ const filterOpts = {
4880
+ unread: Boolean(opts.unread),
4881
+ from: typeof opts.from === "string" ? opts.from : void 0,
4882
+ thread: typeof opts.thread === "string" ? opts.thread : void 0,
4883
+ since: typeof opts.since === "string" ? opts.since : void 0,
4884
+ myExtId
4885
+ };
4886
+ const filtered = dms.filter((r) => rowMatchesFilters(r, filterOpts));
4887
+ const limited = typeof opts.limit === "number" && opts.limit > 0 ? filtered.slice(0, opts.limit) : filtered;
4888
+ if (opts.json) {
4889
+ printJson(limited.map((r) => ({ ...r, encrypted_in_transit: true })));
4890
+ return;
4891
+ }
4892
+ if (limited.length === 0) {
4893
+ process.stderr.write("No messages found.\n");
4894
+ return;
4895
+ }
4896
+ const peer = (r) => {
4897
+ const s = r.sender;
4898
+ const recip = r.recipient;
4899
+ return s?.external_id === myExtId ? recip : s;
4900
+ };
4901
+ printTable(
4902
+ [
4903
+ { header: "ID", width: 16, value: (r) => r.external_id },
4904
+ {
4905
+ header: "DIR",
4906
+ width: 4,
4907
+ value: (r) => r.sender?.external_id === myExtId ? "\u2192" : "\u2190"
4908
+ },
4909
+ {
4910
+ header: "PEER",
4911
+ width: 26,
4912
+ value: (r) => {
4913
+ const p = peer(r);
4914
+ return sdk.formatUserName(p) || p?.email || "";
4915
+ }
4916
+ },
4917
+ { header: "READ", width: 6, value: (r) => r.read ? "yes" : "no" },
4918
+ {
4919
+ header: "CONTENT",
4920
+ width: 50,
4921
+ value: (r) => truncate(r.content ?? "", 49)
4922
+ }
4923
+ ],
4924
+ limited
4925
+ );
4926
+ })()
4927
+ );
4928
+ dm.command("send [recipient] [content...]").description("Send an E2E encrypted DM (interactive if no args)").action(
4929
+ (recipient, contentParts) => runAction(async () => {
4930
+ const { arbi, crypto: crypto2 } = await resolveDmCrypto();
4931
+ if (!recipient) {
4932
+ requireInteractive(
4933
+ 'Pass recipient + message as arguments: arbi dm send <email-or-id> "<message>"'
4934
+ );
4935
+ const contacts = await sdk.contacts.listContacts(arbi);
4936
+ if (contacts.length === 0) {
4937
+ error("No contacts found. Add contacts first: arbi contacts add <email>");
4938
+ process.exit(1);
4939
+ }
4940
+ recipient = await promptSelect(
4941
+ "Send to",
4942
+ contacts.map((c) => {
4943
+ const u = c.user;
4944
+ const name = sdk.formatUserName(u);
4945
+ return {
4946
+ name: name ? `${name} (${c.email})` : c.email,
4947
+ value: u?.external_id ?? c.external_id,
4948
+ description: c.email
4949
+ };
4950
+ })
4951
+ );
4952
+ }
4953
+ let content = contentParts?.length ? contentParts.join(" ") : void 0;
4954
+ if (!content) {
4955
+ requireInteractive(
4956
+ 'Pass the message as a positional argument: arbi dm send <email> "your message"'
4957
+ );
4958
+ content = await promptInput("Message");
4959
+ }
4960
+ const resolved = await resolveRecipient(arbi, recipient);
4961
+ if (!resolved) {
4962
+ error(
4963
+ `No contact, agent, or workspace member found for: ${recipient}
4964
+ Try: arbi contacts add ${recipient.includes("@") ? recipient : "<their-email>"}`
4965
+ );
4966
+ process.exit(3);
4967
+ }
4968
+ const recipientExtId = resolved.extId;
4969
+ const recipientPubKey = resolved.pubKey;
4970
+ if (!recipientPubKey) {
4971
+ error(
4972
+ "Cannot send encrypted DM \u2014 recipient public key not found.\nAdd them as a contact first: arbi contacts add <email>"
4973
+ );
4974
+ process.exit(3);
4975
+ }
4976
+ const data = await sdk.dm.sendEncryptedDM(
4977
+ arbi,
4978
+ [
4979
+ {
4980
+ recipient_ext_id: recipientExtId,
4981
+ content,
4982
+ recipient_encryption_public_key: recipientPubKey
4983
+ }
4984
+ ],
4985
+ crypto2
4986
+ );
4987
+ for (const n of data) {
4988
+ success(`Sent (encrypted): ${n.external_id} \u2192 ${n.recipient.email}`);
4989
+ }
4990
+ })()
4991
+ );
4992
+ 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(
4993
+ (ids, opts) => runAction(async () => {
4994
+ const { arbi, crypto: crypto2 } = await resolveDmCrypto();
4995
+ let msgIds = ids && ids.length > 0 ? ids : void 0;
4996
+ if (!msgIds && opts?.all) {
4997
+ const data2 = await sdk.dm.listDecryptedDMs(arbi, crypto2);
4998
+ msgIds = data2.filter((m) => !m.read).map((m) => m.external_id);
4999
+ }
5000
+ if (!msgIds) {
5001
+ const data2 = await sdk.dm.listDecryptedDMs(arbi, crypto2);
5002
+ const unread = data2.filter((m) => !m.read);
5003
+ if (unread.length === 0) {
5004
+ process.stderr.write("No unread messages.\n");
5005
+ return;
5006
+ }
5007
+ requireInteractive("Pass IDs directly or use --all to mark every unread message read.");
5008
+ msgIds = await promptCheckbox(
5009
+ "Select messages to mark as read",
5010
+ unread.map((m) => {
5011
+ const s = m.sender;
5012
+ const from = sdk.formatUserName(s) || s?.email || "";
5013
+ return {
5014
+ name: `${from}: ${(m.content ?? "").slice(0, 50)}`,
5015
+ value: m.external_id
5016
+ };
5017
+ })
5018
+ );
5019
+ if (msgIds.length === 0) return;
5020
+ }
5021
+ if (msgIds.length === 0) {
5022
+ process.stderr.write("No unread messages.\n");
5023
+ return;
5024
+ }
5025
+ const data = await sdk.dm.markRead(arbi, msgIds);
5026
+ success(`Marked ${data.length} message(s) as read.`);
5027
+ })()
5028
+ );
5029
+ 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(
5030
+ (ids, opts) => runAction(async () => {
5031
+ const { arbi, crypto: crypto2 } = await resolveDmCrypto();
5032
+ let msgIds = ids && ids.length > 0 ? ids : void 0;
5033
+ if (!msgIds && opts?.all) {
5034
+ const data = await sdk.dm.listDecryptedDMs(arbi, crypto2);
5035
+ msgIds = data.map((m) => m.external_id);
5036
+ }
5037
+ if (!msgIds) {
5038
+ const data = await sdk.dm.listDecryptedDMs(arbi, crypto2);
5039
+ if (data.length === 0) {
5040
+ process.stderr.write("No messages found.\n");
5041
+ return;
5042
+ }
5043
+ requireInteractive("Pass IDs directly or use --all to delete the whole inbox.");
5044
+ msgIds = await promptCheckbox(
5045
+ "Select messages to delete",
5046
+ data.map((m) => {
5047
+ const s = m.sender;
5048
+ const from = sdk.formatUserName(s) || s?.email || "";
5049
+ return {
5050
+ name: `${from}: ${(m.content ?? "").slice(0, 50)}`,
5051
+ value: m.external_id
5052
+ };
5053
+ })
5054
+ );
5055
+ if (msgIds.length === 0) return;
5056
+ }
5057
+ if (msgIds.length === 0) {
5058
+ process.stderr.write("No messages to delete.\n");
5059
+ return;
5060
+ }
5061
+ if (opts?.dryRun) {
5062
+ dryRun(`delete ${msgIds.length} message(s)`, msgIds);
5063
+ return;
5064
+ }
5065
+ await sdk.dm.deleteDMs(arbi, msgIds);
5066
+ success(`Deleted ${msgIds.length} message(s).`);
5067
+ })()
5068
+ );
5069
+ dm.arguments("[maybeSubcommand]").action(async (maybe) => {
5070
+ if (maybe) {
5071
+ suggestSubcommandAndExit(
5072
+ "dm",
5073
+ maybe,
5074
+ dm.commands.map((c) => c.name())
5075
+ );
5076
+ }
5077
+ await dm.commands.find((c) => c.name() === "list").parseAsync([], { from: "user" });
5078
+ });
5079
+ }
5080
+
5081
+ // src/commands/workspaces.ts
5082
+ init_prompts();
4806
5083
  function resolveWorkspaceSelector(list, selector) {
4807
5084
  const byId = list.find((w) => w.external_id === selector);
4808
5085
  if (byId) return { ok: true, id: byId.external_id, ws: byId };
@@ -5144,6 +5421,36 @@ function registerWorkspacesCommand(program2) {
5144
5421
  for (const u of data) success(`Added: ${u.user.email} as ${u.role}`);
5145
5422
  })()
5146
5423
  );
5424
+ workspace.command("request-access <owner>").description("Ask another user to grant you access to one of their workspaces").option("--note <note>", "Optional message to the workspace owner").option(
5425
+ "-w, --workspace <id>",
5426
+ "Target a specific workspace id (advanced \u2014 usually omitted; the owner picks)"
5427
+ ).action(
5428
+ (owner, opts) => runAction(async () => {
5429
+ const { arbi, crypto: crypto2, loginResult } = await resolveDmCrypto();
5430
+ const resolved = await resolveRecipient(arbi, owner);
5431
+ if (!resolved || !resolved.pubKey) {
5432
+ error(
5433
+ `No contact, agent, or workspace member found for: ${owner}
5434
+ Try: arbi contacts add ${owner.includes("@") ? owner : "<their-email>"}`
5435
+ );
5436
+ process.exit(3);
5437
+ }
5438
+ await sdk.dm.requestWorkspaceAccess(
5439
+ arbi,
5440
+ {
5441
+ recipientExtId: resolved.extId,
5442
+ recipientEncryptionPublicKey: resolved.pubKey,
5443
+ sessionPubkeyB64: arbi.crypto.bytesToBase64(loginResult.serverSessionKey),
5444
+ note: opts.note,
5445
+ workspaceExtId: opts.workspace
5446
+ },
5447
+ crypto2
5448
+ );
5449
+ success(
5450
+ `Requested workspace access from ${owner}` + (opts.workspace ? ` (workspace ${opts.workspace})` : "") + ". They\u2019ll be notified and can grant temporary or permanent access."
5451
+ );
5452
+ })()
5453
+ );
5147
5454
  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(
5148
5455
  (users, opts) => runAction(async () => {
5149
5456
  const { arbi } = await resolveWorkspace(opts.workspace);
@@ -7748,394 +8055,119 @@ Timeout (${timeoutSec}s), closing.`));
7748
8055
  await done;
7749
8056
  if (timer) clearTimeout(timer);
7750
8057
  process.removeListener("SIGINT", sigintHandler);
7751
- if (timedOut && maxCount && messageCount < maxCount) {
7752
- if (jsonMode) {
7753
- console.log(
7754
- JSON.stringify({
7755
- type: "timeout",
7756
- received: messageCount,
7757
- expected: maxCount,
7758
- timeout_seconds: timeoutSec
7759
- })
7760
- );
7761
- } else {
7762
- console.error(
7763
- chalk2__default.default.yellow(`Timed out before reaching ${maxCount} messages (got ${messageCount}).`)
7764
- );
7765
- }
7766
- process.exit(124);
7767
- }
7768
- })()
7769
- );
7770
- }
7771
- init_prompts();
7772
- function redactPictures(rows) {
7773
- return rows.map((row) => {
7774
- const user = row.user;
7775
- if (!user || typeof user.picture !== "string" || user.picture.length === 0) return row;
7776
- return {
7777
- ...row,
7778
- user: {
7779
- ...user,
7780
- has_picture: true,
7781
- picture: null
7782
- }
7783
- };
7784
- });
7785
- }
7786
- function registerContactsCommand(program2) {
7787
- const contacts = program2.command("contacts").description("Contacts: list, add, remove");
7788
- contacts.command("list").description("List all contacts").option("--json", "Output as JSON").option("--include-pictures", "Keep base64 profile-picture bytes in --json output").action(
7789
- (opts) => runAction(async () => {
7790
- const { arbi } = await resolveAuth();
7791
- const data = await sdk.contacts.listContacts(arbi);
7792
- if (opts.json) {
7793
- printJson(opts.includePictures ? data : redactPictures(data));
7794
- return;
7795
- }
7796
- if (data.length === 0) {
7797
- process.stderr.write("No contacts found.\n");
7798
- return;
7799
- }
7800
- printTable(
7801
- [
7802
- { header: "ID", width: 16, value: (r) => r.external_id },
7803
- {
7804
- header: "NAME",
7805
- width: 20,
7806
- value: (r) => sdk.formatUserName(r.user)
7807
- },
7808
- { header: "EMAIL", width: 30, value: (r) => r.email },
7809
- { header: "STATUS", width: 18, value: (r) => r.status }
7810
- ],
7811
- data
7812
- );
7813
- })()
7814
- );
7815
- contacts.command("add [emails...]").description("Add contacts by email (prompt if no emails given)").action(
7816
- (emails) => runAction(async () => {
7817
- const { arbi } = await resolveAuth();
7818
- if (!emails || emails.length === 0) {
7819
- requireInteractive("Pass email(s) as positional args: arbi contacts add foo@x.y bar@x.y");
7820
- const input2 = await promptInput("Email address(es), comma-separated");
7821
- emails = input2.split(",").map((e) => e.trim()).filter(Boolean);
7822
- if (emails.length === 0) return;
7823
- }
7824
- const data = await sdk.contacts.addContacts(arbi, emails);
7825
- for (const c of data) {
7826
- success(`Added: ${c.email} (${c.external_id}) \u2014 ${c.status}`);
7827
- }
7828
- })()
7829
- );
7830
- 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(
7831
- (ids, opts) => runAction(async () => {
7832
- const { arbi } = await resolveAuth();
7833
- let contactIds = ids && ids.length > 0 ? ids : void 0;
7834
- if (!contactIds) {
7835
- const data = await sdk.contacts.listContacts(arbi);
7836
- if (data.length === 0) {
7837
- process.stderr.write("No contacts found.\n");
7838
- return;
7839
- }
7840
- requireInteractive("Pass contact IDs directly: arbi contacts remove cnt-\u2026");
7841
- contactIds = await promptCheckbox(
7842
- "Select contacts to remove",
7843
- data.map((c) => {
7844
- const name = sdk.formatUserName(c.user);
7845
- return {
7846
- name: name ? `${name} (${c.email})` : c.email,
7847
- value: c.external_id
7848
- };
7849
- })
7850
- );
7851
- if (contactIds.length === 0) return;
7852
- }
7853
- if (opts?.dryRun) {
7854
- dryRun(`remove ${contactIds.length} contact(s)`, contactIds);
7855
- return;
7856
- }
7857
- await sdk.contacts.removeContacts(arbi, contactIds);
7858
- success(`Removed ${contactIds.length} contact(s).`);
7859
- })()
7860
- );
7861
- contacts.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
7862
- const tail = contacts.args ?? [];
7863
- await contacts.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
7864
- });
7865
- }
7866
- init_prompts();
7867
- async function resolveRecipient(arbi, ref2) {
7868
- const isEmail = ref2.includes("@");
7869
- const matchKey = isEmail ? "email" : "external_id";
7870
- const contacts = await sdk.contacts.listContacts(arbi);
7871
- const contactMatch = contacts.find((c) => {
7872
- if (isEmail) return c.email === ref2;
7873
- const u = c.user;
7874
- return u?.external_id === ref2 || c.external_id === ref2;
7875
- });
7876
- if (contactMatch) {
7877
- const u = contactMatch.user;
7878
- const extId = u?.external_id ?? contactMatch.external_id;
7879
- const pubKey = u?.encryption_public_key ?? "";
7880
- if (pubKey) return { extId, pubKey };
7881
- }
7882
- try {
7883
- const agents = await sdk.agents.listAgents(arbi);
7884
- const agentMatch = agents.find((a) => a[matchKey] === ref2);
7885
- if (agentMatch) {
7886
- return { extId: agentMatch.external_id, pubKey: agentMatch.encryption_public_key };
7887
- }
7888
- } catch {
7889
- }
7890
- try {
7891
- const wsUsers = await sdk.workspaces.listWorkspaceUsers(arbi);
7892
- const wsMatch = wsUsers.find((wu) => wu[matchKey] === ref2);
7893
- if (wsMatch) {
7894
- const u = wsMatch;
7895
- return {
7896
- extId: u.external_id,
7897
- pubKey: u.encryption_public_key
7898
- };
7899
- }
7900
- } catch {
7901
- }
7902
- return null;
7903
- }
7904
- function rowMatchesFilters(row, opts) {
7905
- if (opts.unread && row.read) return false;
7906
- const sender = row.sender;
7907
- const recipient = row.recipient;
7908
- if (opts.from) {
7909
- const needle = opts.from.toLowerCase();
7910
- const senderMatch = sender?.email?.toLowerCase() === needle || sender?.external_id === opts.from;
7911
- const recipientMatch = recipient?.email?.toLowerCase() === needle || recipient?.external_id === opts.from;
7912
- if (!senderMatch && !recipientMatch) return false;
7913
- }
7914
- if (opts.thread) {
7915
- const needle = opts.thread.toLowerCase();
7916
- const peer = sender?.external_id === opts.myExtId ? recipient : sender;
7917
- const peerMatch = peer?.email?.toLowerCase() === needle || peer?.external_id === opts.thread;
7918
- if (!peerMatch) return false;
7919
- }
7920
- if (opts.since) {
7921
- const cutoff = Date.parse(opts.since);
7922
- if (Number.isFinite(cutoff)) {
7923
- const createdAt = typeof row.created_at === "string" ? Date.parse(row.created_at) : NaN;
7924
- if (!Number.isFinite(createdAt) || createdAt < cutoff) return false;
7925
- }
7926
- }
7927
- return true;
8058
+ if (timedOut && maxCount && messageCount < maxCount) {
8059
+ if (jsonMode) {
8060
+ console.log(
8061
+ JSON.stringify({
8062
+ type: "timeout",
8063
+ received: messageCount,
8064
+ expected: maxCount,
8065
+ timeout_seconds: timeoutSec
8066
+ })
8067
+ );
8068
+ } else {
8069
+ console.error(
8070
+ chalk2__default.default.yellow(`Timed out before reaching ${maxCount} messages (got ${messageCount}).`)
8071
+ );
8072
+ }
8073
+ process.exit(124);
8074
+ }
8075
+ })()
8076
+ );
7928
8077
  }
7929
- function registerDmCommand(program2) {
7930
- const dm = program2.command("dm").description("Direct messages (E2E encrypted)");
7931
- 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(
7932
- "--thread <email-or-extid>",
7933
- "Show the full conversation with one peer (both directions)"
7934
- ).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(
8078
+ init_prompts();
8079
+ function redactPictures(rows) {
8080
+ return rows.map((row) => {
8081
+ const user = row.user;
8082
+ if (!user || typeof user.picture !== "string" || user.picture.length === 0) return row;
8083
+ return {
8084
+ ...row,
8085
+ user: {
8086
+ ...user,
8087
+ has_picture: true,
8088
+ picture: null
8089
+ }
8090
+ };
8091
+ });
8092
+ }
8093
+ function registerContactsCommand(program2) {
8094
+ const contacts = program2.command("contacts").description("Contacts: list, add, remove");
8095
+ contacts.command("list").description("List all contacts").option("--json", "Output as JSON").option("--include-pictures", "Keep base64 profile-picture bytes in --json output").action(
7935
8096
  (opts) => runAction(async () => {
7936
- const { arbi, crypto: crypto2 } = await resolveDmCrypto();
7937
- const all = await sdk.dm.listDecryptedDMs(arbi, crypto2);
7938
- const myExtId = arbi.session.getState().userExtId ?? void 0;
7939
- const dms = all.filter((r) => r.type === "user_message");
7940
- const filterOpts = {
7941
- unread: Boolean(opts.unread),
7942
- from: typeof opts.from === "string" ? opts.from : void 0,
7943
- thread: typeof opts.thread === "string" ? opts.thread : void 0,
7944
- since: typeof opts.since === "string" ? opts.since : void 0,
7945
- myExtId
7946
- };
7947
- const filtered = dms.filter((r) => rowMatchesFilters(r, filterOpts));
7948
- const limited = typeof opts.limit === "number" && opts.limit > 0 ? filtered.slice(0, opts.limit) : filtered;
8097
+ const { arbi } = await resolveAuth();
8098
+ const data = await sdk.contacts.listContacts(arbi);
7949
8099
  if (opts.json) {
7950
- printJson(limited.map((r) => ({ ...r, encrypted_in_transit: true })));
8100
+ printJson(opts.includePictures ? data : redactPictures(data));
7951
8101
  return;
7952
8102
  }
7953
- if (limited.length === 0) {
7954
- process.stderr.write("No messages found.\n");
8103
+ if (data.length === 0) {
8104
+ process.stderr.write("No contacts found.\n");
7955
8105
  return;
7956
8106
  }
7957
- const peer = (r) => {
7958
- const s = r.sender;
7959
- const recip = r.recipient;
7960
- return s?.external_id === myExtId ? recip : s;
7961
- };
7962
8107
  printTable(
7963
8108
  [
7964
8109
  { header: "ID", width: 16, value: (r) => r.external_id },
7965
8110
  {
7966
- header: "DIR",
7967
- width: 4,
7968
- value: (r) => r.sender?.external_id === myExtId ? "\u2192" : "\u2190"
7969
- },
7970
- {
7971
- header: "PEER",
7972
- width: 26,
7973
- value: (r) => {
7974
- const p = peer(r);
7975
- return sdk.formatUserName(p) || p?.email || "";
7976
- }
8111
+ header: "NAME",
8112
+ width: 20,
8113
+ value: (r) => sdk.formatUserName(r.user)
7977
8114
  },
7978
- { header: "READ", width: 6, value: (r) => r.read ? "yes" : "no" },
7979
- {
7980
- header: "CONTENT",
7981
- width: 50,
7982
- value: (r) => truncate(r.content ?? "", 49)
7983
- }
7984
- ],
7985
- limited
7986
- );
7987
- })()
7988
- );
7989
- dm.command("send [recipient] [content...]").description("Send an E2E encrypted DM (interactive if no args)").action(
7990
- (recipient, contentParts) => runAction(async () => {
7991
- const { arbi, crypto: crypto2 } = await resolveDmCrypto();
7992
- if (!recipient) {
7993
- requireInteractive(
7994
- 'Pass recipient + message as arguments: arbi dm send <email-or-id> "<message>"'
7995
- );
7996
- const contacts = await sdk.contacts.listContacts(arbi);
7997
- if (contacts.length === 0) {
7998
- error("No contacts found. Add contacts first: arbi contacts add <email>");
7999
- process.exit(1);
8000
- }
8001
- recipient = await promptSelect(
8002
- "Send to",
8003
- contacts.map((c) => {
8004
- const u = c.user;
8005
- const name = sdk.formatUserName(u);
8006
- return {
8007
- name: name ? `${name} (${c.email})` : c.email,
8008
- value: u?.external_id ?? c.external_id,
8009
- description: c.email
8010
- };
8011
- })
8012
- );
8013
- }
8014
- let content = contentParts?.length ? contentParts.join(" ") : void 0;
8015
- if (!content) {
8016
- requireInteractive(
8017
- 'Pass the message as a positional argument: arbi dm send <email> "your message"'
8018
- );
8019
- content = await promptInput("Message");
8020
- }
8021
- const resolved = await resolveRecipient(arbi, recipient);
8022
- if (!resolved) {
8023
- error(
8024
- `No contact, agent, or workspace member found for: ${recipient}
8025
- Try: arbi contacts add ${recipient.includes("@") ? recipient : "<their-email>"}`
8026
- );
8027
- process.exit(3);
8028
- }
8029
- const recipientExtId = resolved.extId;
8030
- const recipientPubKey = resolved.pubKey;
8031
- if (!recipientPubKey) {
8032
- error(
8033
- "Cannot send encrypted DM \u2014 recipient public key not found.\nAdd them as a contact first: arbi contacts add <email>"
8034
- );
8035
- process.exit(3);
8036
- }
8037
- const data = await sdk.dm.sendEncryptedDM(
8038
- arbi,
8039
- [
8040
- {
8041
- recipient_ext_id: recipientExtId,
8042
- content,
8043
- recipient_encryption_public_key: recipientPubKey
8044
- }
8115
+ { header: "EMAIL", width: 30, value: (r) => r.email },
8116
+ { header: "STATUS", width: 18, value: (r) => r.status }
8045
8117
  ],
8046
- crypto2
8118
+ data
8047
8119
  );
8048
- for (const n of data) {
8049
- success(`Sent (encrypted): ${n.external_id} \u2192 ${n.recipient.email}`);
8050
- }
8051
8120
  })()
8052
8121
  );
8053
- 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(
8054
- (ids, opts) => runAction(async () => {
8055
- const { arbi, crypto: crypto2 } = await resolveDmCrypto();
8056
- let msgIds = ids && ids.length > 0 ? ids : void 0;
8057
- if (!msgIds && opts?.all) {
8058
- const data2 = await sdk.dm.listDecryptedDMs(arbi, crypto2);
8059
- msgIds = data2.filter((m) => !m.read).map((m) => m.external_id);
8060
- }
8061
- if (!msgIds) {
8062
- const data2 = await sdk.dm.listDecryptedDMs(arbi, crypto2);
8063
- const unread = data2.filter((m) => !m.read);
8064
- if (unread.length === 0) {
8065
- process.stderr.write("No unread messages.\n");
8066
- return;
8067
- }
8068
- requireInteractive("Pass IDs directly or use --all to mark every unread message read.");
8069
- msgIds = await promptCheckbox(
8070
- "Select messages to mark as read",
8071
- unread.map((m) => {
8072
- const s = m.sender;
8073
- const from = sdk.formatUserName(s) || s?.email || "";
8074
- return {
8075
- name: `${from}: ${(m.content ?? "").slice(0, 50)}`,
8076
- value: m.external_id
8077
- };
8078
- })
8079
- );
8080
- if (msgIds.length === 0) return;
8122
+ contacts.command("add [emails...]").description("Add contacts by email (prompt if no emails given)").action(
8123
+ (emails) => runAction(async () => {
8124
+ const { arbi } = await resolveAuth();
8125
+ if (!emails || emails.length === 0) {
8126
+ requireInteractive("Pass email(s) as positional args: arbi contacts add foo@x.y bar@x.y");
8127
+ const input2 = await promptInput("Email address(es), comma-separated");
8128
+ emails = input2.split(",").map((e) => e.trim()).filter(Boolean);
8129
+ if (emails.length === 0) return;
8081
8130
  }
8082
- if (msgIds.length === 0) {
8083
- process.stderr.write("No unread messages.\n");
8084
- return;
8131
+ const data = await sdk.contacts.addContacts(arbi, emails);
8132
+ for (const c of data) {
8133
+ success(`Added: ${c.email} (${c.external_id}) \u2014 ${c.status}`);
8085
8134
  }
8086
- const data = await sdk.dm.markRead(arbi, msgIds);
8087
- success(`Marked ${data.length} message(s) as read.`);
8088
8135
  })()
8089
8136
  );
8090
- 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(
8137
+ 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(
8091
8138
  (ids, opts) => runAction(async () => {
8092
- const { arbi, crypto: crypto2 } = await resolveDmCrypto();
8093
- let msgIds = ids && ids.length > 0 ? ids : void 0;
8094
- if (!msgIds && opts?.all) {
8095
- const data = await sdk.dm.listDecryptedDMs(arbi, crypto2);
8096
- msgIds = data.map((m) => m.external_id);
8097
- }
8098
- if (!msgIds) {
8099
- const data = await sdk.dm.listDecryptedDMs(arbi, crypto2);
8139
+ const { arbi } = await resolveAuth();
8140
+ let contactIds = ids && ids.length > 0 ? ids : void 0;
8141
+ if (!contactIds) {
8142
+ const data = await sdk.contacts.listContacts(arbi);
8100
8143
  if (data.length === 0) {
8101
- process.stderr.write("No messages found.\n");
8144
+ process.stderr.write("No contacts found.\n");
8102
8145
  return;
8103
8146
  }
8104
- requireInteractive("Pass IDs directly or use --all to delete the whole inbox.");
8105
- msgIds = await promptCheckbox(
8106
- "Select messages to delete",
8107
- data.map((m) => {
8108
- const s = m.sender;
8109
- const from = sdk.formatUserName(s) || s?.email || "";
8147
+ requireInteractive("Pass contact IDs directly: arbi contacts remove cnt-\u2026");
8148
+ contactIds = await promptCheckbox(
8149
+ "Select contacts to remove",
8150
+ data.map((c) => {
8151
+ const name = sdk.formatUserName(c.user);
8110
8152
  return {
8111
- name: `${from}: ${(m.content ?? "").slice(0, 50)}`,
8112
- value: m.external_id
8153
+ name: name ? `${name} (${c.email})` : c.email,
8154
+ value: c.external_id
8113
8155
  };
8114
8156
  })
8115
8157
  );
8116
- if (msgIds.length === 0) return;
8117
- }
8118
- if (msgIds.length === 0) {
8119
- process.stderr.write("No messages to delete.\n");
8120
- return;
8158
+ if (contactIds.length === 0) return;
8121
8159
  }
8122
8160
  if (opts?.dryRun) {
8123
- dryRun(`delete ${msgIds.length} message(s)`, msgIds);
8161
+ dryRun(`remove ${contactIds.length} contact(s)`, contactIds);
8124
8162
  return;
8125
8163
  }
8126
- await sdk.dm.deleteDMs(arbi, msgIds);
8127
- success(`Deleted ${msgIds.length} message(s).`);
8164
+ await sdk.contacts.removeContacts(arbi, contactIds);
8165
+ success(`Removed ${contactIds.length} contact(s).`);
8128
8166
  })()
8129
8167
  );
8130
- dm.arguments("[maybeSubcommand]").action(async (maybe) => {
8131
- if (maybe) {
8132
- suggestSubcommandAndExit(
8133
- "dm",
8134
- maybe,
8135
- dm.commands.map((c) => c.name())
8136
- );
8137
- }
8138
- await dm.commands.find((c) => c.name() === "list").parseAsync([], { from: "user" });
8168
+ contacts.allowUnknownOption(true).allowExcessArguments(true).action(async () => {
8169
+ const tail = contacts.args ?? [];
8170
+ await contacts.commands.find((c) => c.name() === "list").parseAsync(tail, { from: "user" });
8139
8171
  });
8140
8172
  }
8141
8173
  init_prompts();
@@ -10273,7 +10305,7 @@ console.info = (...args) => {
10273
10305
  _origInfo(...args);
10274
10306
  };
10275
10307
  var program = new commander.Command();
10276
- program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.3.89").showHelpAfterError(true).showSuggestionAfterError(true);
10308
+ program.name("arbi").description("ARBI CLI \u2014 interact with ARBI from the terminal").version("0.3.90").showHelpAfterError(true).showSuggestionAfterError(true);
10277
10309
  registerConfigCommand(program);
10278
10310
  registerLoginCommand(program);
10279
10311
  registerRegisterCommand(program);