arp-tui 0.0.5 → 0.0.7

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
@@ -5,7 +5,7 @@ import { parseArgs as nodeParseArgs } from "util";
5
5
  import { render } from "ink";
6
6
 
7
7
  // src/app.tsx
8
- import { Box as Box8, Text as Text10, useApp, useInput, useStdin } from "ink";
8
+ import { Box as Box9, Text as Text11, useApp, useInput, useStdin } from "ink";
9
9
  import { useEffect, useSyncExternalStore } from "react";
10
10
 
11
11
  // src/state/store.ts
@@ -621,7 +621,7 @@ function chooseRefreshToken(args2) {
621
621
  const { memoryRt, diskRt, memorySub, diskSub, knownNewerInMemory } = args2;
622
622
  const memNewerThanDisk = knownNewerInMemory && diskRt !== memoryRt;
623
623
  if (memNewerThanDisk) {
624
- if (diskRt === null && diskSub && memorySub && diskSub !== memorySub) return null;
624
+ if (diskSub && memorySub && diskSub !== memorySub) return null;
625
625
  return memoryRt;
626
626
  }
627
627
  const sameIdentity = diskRt === memoryRt || !!diskSub && diskSub === memorySub;
@@ -780,6 +780,239 @@ async function logout(instance2, log = console.error) {
780
780
  return true;
781
781
  }
782
782
 
783
+ // src/commands/result.ts
784
+ var EXIT_CODE = {
785
+ success: 0,
786
+ internal_error: 1,
787
+ usage: 2,
788
+ cancelled: 3,
789
+ authentication_required: 4,
790
+ policy_denied: 5,
791
+ bridge_unavailable: 6,
792
+ relay_unavailable: 7,
793
+ configured_offline: 8,
794
+ outcome_unknown: 9,
795
+ local_integrity_error: 10
796
+ };
797
+ var MAX_SAFE_MESSAGE_SCALARS = 256;
798
+ var SAFE_CODE = /^[a-z][a-z0-9_]{0,63}$/;
799
+ function boundedText(value) {
800
+ const plain = value.replace(/\r\n?|\n/g, " ").replace(/[\x00-\x1f\x7f-\x9f]/g, "").normalize("NFC");
801
+ return Array.from(plain).slice(0, MAX_SAFE_MESSAGE_SCALARS).join("");
802
+ }
803
+ var CommandFailure = class extends Error {
804
+ exitCode;
805
+ stableCode;
806
+ operationId;
807
+ requestId;
808
+ constructor(symbol, stableCode, message, refs) {
809
+ if (!SAFE_CODE.test(stableCode)) throw new TypeError("invalid stable error code");
810
+ super(boundedText(message));
811
+ this.name = "CommandFailure";
812
+ this.exitCode = EXIT_CODE[symbol];
813
+ this.stableCode = stableCode;
814
+ this.operationId = refs?.operationId;
815
+ this.requestId = refs?.requestId;
816
+ }
817
+ toJSON() {
818
+ return Object.freeze({
819
+ schemaVersion: 1,
820
+ ok: false,
821
+ code: this.stableCode,
822
+ message: this.message,
823
+ ...this.operationId ? { operationId: this.operationId } : {},
824
+ ...this.requestId ? { requestId: this.requestId } : {}
825
+ });
826
+ }
827
+ };
828
+ var OutcomeUnknown = class extends CommandFailure {
829
+ operation;
830
+ reconcile;
831
+ constructor(args2) {
832
+ super(
833
+ "outcome_unknown",
834
+ "outcome_unknown",
835
+ "The relay outcome is unknown. Reconcile this operation before trying again.",
836
+ { operationId: args2.operationId, requestId: args2.requestId }
837
+ );
838
+ this.name = "OutcomeUnknown";
839
+ this.operation = args2.operation;
840
+ this.reconcile = args2.reconcile;
841
+ }
842
+ toJSON() {
843
+ return Object.freeze({ ...super.toJSON(), operation: boundedText(this.operation), reconcile: boundedText(this.reconcile) });
844
+ }
845
+ };
846
+
847
+ // src/util/sanitize.ts
848
+ var ESCAPE_SEQ = (
849
+ // eslint-disable-next-line no-control-regex
850
+ /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:\x07|\x1b\\)|[PX^_][\s\S]*?\x1b\\|[@-Z\\-_])/g
851
+ );
852
+ var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
853
+ function sanitizeForTty(input) {
854
+ return input.replace(ESCAPE_SEQ, "").replace(CONTROL_CHARS, "");
855
+ }
856
+ function sanitizeLabel(input, max = 40) {
857
+ const clean = sanitizeForTty(input);
858
+ return clean.length > max ? clean.slice(0, max - 1) + "\u2026" : clean;
859
+ }
860
+ var FORMAT_CONTROLS = /[\u00ad\u034f\u061c\u115f-\u1160\u17b4-\u17b5\u180b-\u180f\u200b-\u200f\u202a-\u202e\u2060-\u206f\u3164\ufe00-\ufe0f\ufeff\uffa0\ufff0-\ufff8\u{1bca0}-\u{1bca3}\u{1d173}-\u{1d17a}\u{e0000}-\u{e0fff}]/gu;
861
+ function stripTerminalSequences(input) {
862
+ let out = "";
863
+ for (let i = 0; i < input.length; ) {
864
+ const cp = input.codePointAt(i);
865
+ const width = cp > 65535 ? 2 : 1;
866
+ const esc = cp === 27;
867
+ const csi = cp === 155;
868
+ const osc = cp === 157;
869
+ const stringControl = cp === 144 || cp === 152 || cp === 158 || cp === 159;
870
+ if (esc || csi || osc || stringControl) {
871
+ let kind = cp;
872
+ i += width;
873
+ if (esc && i < input.length) {
874
+ kind = input.codePointAt(i);
875
+ i += kind > 65535 ? 2 : 1;
876
+ }
877
+ if (kind === 91 || kind === 155) {
878
+ while (i < input.length) {
879
+ const n = input.codePointAt(i);
880
+ i += n > 65535 ? 2 : 1;
881
+ if (n >= 64 && n <= 126) break;
882
+ }
883
+ } else if (kind === 93 || kind === 157 || kind === 80 || kind === 88 || kind === 94 || kind === 95 || stringControl) {
884
+ while (i < input.length) {
885
+ const n = input.codePointAt(i);
886
+ i += n > 65535 ? 2 : 1;
887
+ if ((kind === 93 || kind === 157) && n === 7) break;
888
+ if (n === 156) break;
889
+ if (n === 27 && input.codePointAt(i) === 92) {
890
+ i++;
891
+ break;
892
+ }
893
+ }
894
+ }
895
+ continue;
896
+ }
897
+ if (cp < 32 && cp !== 9 && cp !== 10 && cp !== 13 || cp >= 127 && cp <= 159) {
898
+ i += width;
899
+ continue;
900
+ }
901
+ out += String.fromCodePoint(cp);
902
+ i += width;
903
+ }
904
+ return out.replace(FORMAT_CONTROLS, "");
905
+ }
906
+ function clampScalars(value, max) {
907
+ const points = Array.from(value);
908
+ return points.length <= max ? value : points.slice(0, max).join("");
909
+ }
910
+ function sanitizeRemoteMultiline(input, maxScalars = 8192) {
911
+ const normalizedLines = stripTerminalSequences(input).replace(/\r\n?/g, "\n").replace(/\t/g, " ").split("\n").slice(0, 64).map((line) => clampScalars(line, 512)).join("\n").normalize("NFC");
912
+ return clampScalars(normalizedLines, maxScalars);
913
+ }
914
+ function sanitizeRemoteSingleLine(input, maxScalars = 256) {
915
+ return clampScalars(
916
+ stripTerminalSequences(input).replace(/\r\n?|\n|\t/g, " ").normalize("NFC"),
917
+ maxScalars
918
+ );
919
+ }
920
+
921
+ // src/api/attentionMutations.ts
922
+ var UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
923
+ var ATTENTION_LEVELS = /* @__PURE__ */ new Set(["info", "action_required", "critical"]);
924
+ function requireImmutableId(value, label) {
925
+ if (!UUID.test(value)) throw new CommandFailure("usage", "invalid_immutable_id", `${label} must be an immutable UUID from a list result.`);
926
+ return value;
927
+ }
928
+ function invalid(label) {
929
+ throw new CommandFailure("relay_unavailable", "invalid_relay_response", `The relay returned an invalid ${label}.`);
930
+ }
931
+ function record(value, label) {
932
+ if (!value || typeof value !== "object" || Array.isArray(value)) invalid(label);
933
+ return value;
934
+ }
935
+ function string(value, label, maxBytes) {
936
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > maxBytes) invalid(label);
937
+ return value;
938
+ }
939
+ function uuid(value, label) {
940
+ const id = string(value, label, 36);
941
+ if (!UUID.test(id)) invalid(label);
942
+ return id;
943
+ }
944
+ function timestamp(value, label) {
945
+ const stamp = string(value, label, 64);
946
+ if (!/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/.test(stamp)) invalid(label);
947
+ return stamp;
948
+ }
949
+ function count(value, label) {
950
+ if (!Number.isSafeInteger(value) || value < 0 || value > 1e6) invalid(label);
951
+ return value;
952
+ }
953
+ function level(value) {
954
+ if (typeof value !== "string" || !ATTENTION_LEVELS.has(value)) invalid("attention level");
955
+ return value;
956
+ }
957
+ function optionalActorType(value) {
958
+ if (value === void 0 || value === null || value === "") return void 0;
959
+ if (value !== "human" && value !== "bot" && value !== "system") invalid("attention attribution type");
960
+ return value;
961
+ }
962
+ function decodeAttention(value) {
963
+ const item = record(value, "attention item");
964
+ const raisedByType = optionalActorType(item.raisedByType);
965
+ const raisedBy = item.raisedBy === void 0 || item.raisedBy === null ? void 0 : sanitizeRemoteSingleLine(string(item.raisedBy, "attention attribution", 256));
966
+ return {
967
+ id: uuid(item.id, "attention id"),
968
+ channelId: uuid(item.channelId, "attention channel id"),
969
+ level: level(item.level),
970
+ reason: sanitizeRemoteMultiline(string(item.reason, "attention reason", 1024)),
971
+ count: count(item.count, "attention count"),
972
+ raisedAt: timestamp(item.raisedAt, "attention raised timestamp"),
973
+ updatedAt: timestamp(item.updatedAt, "attention updated timestamp"),
974
+ ...raisedBy ? { raisedBy } : {},
975
+ ...raisedByType ? { raisedByType } : {}
976
+ };
977
+ }
978
+ function envelope(value, field) {
979
+ const outer = record(value, "response envelope");
980
+ if (outer.ok !== true || !(field in outer)) invalid("response envelope");
981
+ return outer[field];
982
+ }
983
+ var AttentionMutations = class {
984
+ constructor(transport) {
985
+ this.transport = transport;
986
+ }
987
+ transport;
988
+ async listChannel(channelId) {
989
+ const id = requireImmutableId(channelId, "channel id");
990
+ return this.transport.request({ method: "GET", path: `/channels/${id}/attention`, operation: "attention.list-channel", decode: (value) => {
991
+ const items = envelope(value, "attention");
992
+ if (!Array.isArray(items) || items.length > 64) invalid("attention list");
993
+ return items.map(decodeAttention);
994
+ } });
995
+ }
996
+ raise(args2) {
997
+ const channelId = requireImmutableId(args2.channelId, "channel id");
998
+ return this.transport.request({ method: "POST", path: `/channels/${channelId}/attention`, operation: "attention.raise", operationId: args2.operationId, reconcile: `GET /channels/${channelId}/attention`, body: { level: args2.level, reason: args2.reason, count: 1, sourceTaskIds: [] }, decode: (value) => {
999
+ const attention = decodeAttention(envelope(value, "attention"));
1000
+ if (attention.channelId !== channelId || attention.level !== args2.level || attention.reason !== sanitizeRemoteMultiline(args2.reason)) invalid("attention receipt");
1001
+ return { attention };
1002
+ } });
1003
+ }
1004
+ ack(args2) {
1005
+ const channelId = requireImmutableId(args2.channelId, "channel id");
1006
+ return this.transport.request({ method: "POST", path: `/channels/${channelId}/attention/ack`, operation: "attention.ack", operationId: args2.operationId, reconcile: `GET /channels/${channelId}/attention`, body: args2.level ? { level: args2.level } : {}, decode: (value) => ({ acked: count(envelope(value, "acked"), "acknowledged count") }) });
1007
+ }
1008
+ };
1009
+ function normalizeReason(value) {
1010
+ if (Buffer.byteLength(value, "utf8") > 1024) throw new CommandFailure("usage", "reason_too_long", "Attention reason must be at most 1,024 UTF-8 bytes.");
1011
+ const reason = sanitizeRemoteSingleLine(value, 256).trim();
1012
+ if (!reason) throw new CommandFailure("usage", "reason_required", "Attention reason is required.");
1013
+ return reason;
1014
+ }
1015
+
783
1016
  // src/api/clerkRefresh.ts
784
1017
  var CACHE_TTL_MS = 45e3;
785
1018
  var FAPI_TIMEOUT_MS = 1e4;
@@ -838,15 +1071,158 @@ function createClerkTokenSupplier(clientToken, opts) {
838
1071
  };
839
1072
  }
840
1073
 
1074
+ // src/api/lifecycleTransport.ts
1075
+ var DEFAULT_READ_TIMEOUT_MS = 15e3;
1076
+ var DEFAULT_MUTATION_TIMEOUT_MS = 2e4;
1077
+ var DEFAULT_RESPONSE_LIMIT = 64 * 1024;
1078
+ var DEFAULT_REQUEST_LIMIT = 64 * 1024;
1079
+ var MAX_JSON_DEPTH = 32;
1080
+ var CANONICAL_UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/;
1081
+ function assertJsonDepth(value, depth = 0) {
1082
+ if (depth > MAX_JSON_DEPTH) throw new CommandFailure("relay_unavailable", "invalid_relay_response", "The relay response exceeded the accepted JSON depth.");
1083
+ if (Array.isArray(value)) for (const item of value) assertJsonDepth(item, depth + 1);
1084
+ else if (value && typeof value === "object") for (const item of Object.values(value)) assertJsonDepth(item, depth + 1);
1085
+ }
1086
+ function requestId(headers) {
1087
+ const value = headers.get("x-request-id") ?? void 0;
1088
+ return value && /^[A-Za-z0-9._:-]{1,128}$/.test(value) ? value : void 0;
1089
+ }
1090
+ async function readBounded(response, limit) {
1091
+ if (!response.body) return new Uint8Array();
1092
+ const reader = response.body.getReader();
1093
+ const chunks = [];
1094
+ let total = 0;
1095
+ try {
1096
+ for (; ; ) {
1097
+ const { done, value } = await reader.read();
1098
+ if (done) break;
1099
+ total += value.byteLength;
1100
+ if (total > limit) throw new CommandFailure("relay_unavailable", "response_too_large", "The relay response exceeded the accepted size.", { requestId: requestId(response.headers) });
1101
+ chunks.push(value);
1102
+ }
1103
+ } finally {
1104
+ await reader.cancel().catch(() => {
1105
+ });
1106
+ }
1107
+ const bytes = new Uint8Array(total);
1108
+ let offset = 0;
1109
+ for (const chunk of chunks) {
1110
+ bytes.set(chunk, offset);
1111
+ offset += chunk.byteLength;
1112
+ }
1113
+ return bytes;
1114
+ }
1115
+ var LifecycleTransport = class {
1116
+ constructor(cfg) {
1117
+ this.cfg = cfg;
1118
+ this.origin = new URL(cfg.relayUrl);
1119
+ if (this.origin.protocol !== "https:" || this.origin.username || this.origin.password || this.origin.search || this.origin.hash || this.origin.pathname !== "/") {
1120
+ throw new CommandFailure("local_integrity_error", "invalid_relay_origin", "The selected relay profile has an invalid origin.");
1121
+ }
1122
+ this.fetchImpl = cfg.fetch ?? globalThis.fetch;
1123
+ }
1124
+ cfg;
1125
+ origin;
1126
+ fetchImpl;
1127
+ async request(req) {
1128
+ if (!req.path.startsWith("/") || req.path.startsWith("//") || req.path.length > 2048 || /[\x00-\x20\x7f-\x9f]/u.test(req.path)) throw new CommandFailure("internal_error", "invalid_route", "The lifecycle route is invalid.");
1129
+ const target = new URL(req.path, this.origin);
1130
+ if (target.origin !== this.origin.origin) throw new CommandFailure("internal_error", "cross_origin_route", "The lifecycle route crossed the selected relay origin.");
1131
+ const mutation = req.method !== "GET";
1132
+ if (mutation && (!req.operationId || !CANONICAL_UUID.test(req.operationId) || !req.reconcile)) throw new CommandFailure("internal_error", "missing_operation_identity", "A mutation requires a canonical operation ID and reconciliation method.");
1133
+ const outcomeUnknown = (rid2) => new OutcomeUnknown({
1134
+ operation: req.operation,
1135
+ operationId: req.operationId,
1136
+ requestId: rid2,
1137
+ reconcile: req.reconcile
1138
+ });
1139
+ let token;
1140
+ try {
1141
+ token = await this.cfg.getToken();
1142
+ } catch {
1143
+ throw new CommandFailure("authentication_required", "authentication_required", "Sign in with `arp-tui login`.");
1144
+ }
1145
+ if (!token) throw new CommandFailure("authentication_required", "authentication_required", "Sign in with `arp-tui login`.");
1146
+ let serializedBody;
1147
+ if (req.body !== void 0) {
1148
+ try {
1149
+ serializedBody = JSON.stringify(req.body);
1150
+ } catch {
1151
+ throw new CommandFailure("internal_error", "invalid_request_body", "The lifecycle request body could not be encoded.");
1152
+ }
1153
+ if (Buffer.byteLength(serializedBody, "utf8") > DEFAULT_REQUEST_LIMIT) throw new CommandFailure("internal_error", "request_too_large", "The lifecycle request exceeded the accepted size.");
1154
+ }
1155
+ let response;
1156
+ try {
1157
+ response = await this.fetchImpl(target, {
1158
+ method: req.method,
1159
+ redirect: "manual",
1160
+ headers: {
1161
+ accept: "application/json",
1162
+ authorization: `Bearer ${token}`,
1163
+ ...req.body === void 0 ? {} : { "content-type": "application/json" },
1164
+ ...req.operationId ? { "ARP-Operation-Id": req.operationId } : {}
1165
+ },
1166
+ body: serializedBody,
1167
+ signal: AbortSignal.timeout(mutation ? this.cfg.mutationTimeoutMs ?? DEFAULT_MUTATION_TIMEOUT_MS : this.cfg.readTimeoutMs ?? DEFAULT_READ_TIMEOUT_MS)
1168
+ });
1169
+ } catch {
1170
+ if (mutation) throw outcomeUnknown();
1171
+ throw new CommandFailure("relay_unavailable", "relay_unavailable", "The relay did not return a definitive response.");
1172
+ }
1173
+ const rid = requestId(response.headers);
1174
+ if (response.status >= 300 && response.status < 400) {
1175
+ if (mutation) throw outcomeUnknown(rid);
1176
+ throw new CommandFailure("relay_unavailable", "redirect_refused", "The relay returned a redirect, which was refused.", { requestId: rid });
1177
+ }
1178
+ const contentLength = Number(response.headers.get("content-length") ?? "0");
1179
+ const limit = this.cfg.responseLimitBytes ?? DEFAULT_RESPONSE_LIMIT;
1180
+ if (Number.isFinite(contentLength) && contentLength > limit) {
1181
+ if (mutation) throw outcomeUnknown(rid);
1182
+ throw new CommandFailure("relay_unavailable", "response_too_large", "The relay response exceeded the accepted size.", { requestId: rid });
1183
+ }
1184
+ let bytes;
1185
+ try {
1186
+ bytes = await readBounded(response, limit);
1187
+ } catch (error) {
1188
+ if (mutation) throw outcomeUnknown(rid);
1189
+ throw error;
1190
+ }
1191
+ if (!response.ok) {
1192
+ if (mutation && response.status >= 500) throw outcomeUnknown(rid);
1193
+ const symbol = response.status === 401 ? "authentication_required" : response.status === 403 ? "policy_denied" : "relay_unavailable";
1194
+ const code = response.status === 401 ? "authentication_required" : response.status === 403 ? "policy_denied" : "relay_rejected";
1195
+ throw new CommandFailure(symbol, code, response.status === 401 ? "Sign in with `arp-tui login`." : response.status === 403 ? "The relay policy denied this operation." : "The relay rejected the request.", { operationId: req.operationId, requestId: rid });
1196
+ }
1197
+ if (!(response.headers.get("content-type") ?? "").toLowerCase().startsWith("application/json")) {
1198
+ if (mutation) throw outcomeUnknown(rid);
1199
+ throw new CommandFailure("relay_unavailable", "invalid_content_type", "The relay returned an unsupported response type.", { requestId: rid });
1200
+ }
1201
+ let parsed;
1202
+ try {
1203
+ parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
1204
+ assertJsonDepth(parsed);
1205
+ return req.decode(parsed);
1206
+ } catch (error) {
1207
+ if (mutation) throw outcomeUnknown(rid);
1208
+ if (error instanceof CommandFailure) throw error;
1209
+ throw new CommandFailure("relay_unavailable", "invalid_relay_response", "The relay returned invalid or unexpected JSON.", { requestId: rid });
1210
+ }
1211
+ }
1212
+ };
1213
+
841
1214
  // src/api/types.ts
842
1215
  function normalizeMember(raw, prev) {
843
1216
  const id = String(raw?.id ?? raw?.memberId ?? raw?.userId ?? prev?.id ?? "");
844
1217
  const rawType = raw?.type ?? raw?.memberType;
1218
+ const type = rawType === "bot" ? "bot" : rawType === "human" ? "human" : prev?.type ?? "human";
845
1219
  const status = raw?.status === "active" || raw?.status === "online" || raw?.isOnline === true ? "online" : raw?.status === "away" ? "away" : raw?.status === "offline" || raw?.isOnline === false ? "offline" : prev?.status ?? "offline";
846
1220
  return {
847
1221
  id,
848
- type: rawType === "bot" ? "bot" : rawType === "human" ? "human" : prev?.type ?? "human",
849
- name: String(raw?.name ?? raw?.displayName ?? raw?.email ?? prev?.name ?? id.slice(0, 8)),
1222
+ type,
1223
+ // Name fallback: a bot's id IS its full agent name never shorten it.
1224
+ // Only a human's uuid id gets the 8-char short form for display.
1225
+ name: String(raw?.name ?? raw?.displayName ?? raw?.email ?? prev?.name ?? (type === "bot" ? id : id.slice(0, 8))),
850
1226
  role: raw?.role === "owner" ? "owner" : "member",
851
1227
  status
852
1228
  };
@@ -921,6 +1297,58 @@ var RelayRest = class {
921
1297
  const data = await this.req("GET", "/channels");
922
1298
  return (data.channels ?? []).map((c) => ({ ...c, members: normalizeMembers(c.members) }));
923
1299
  }
1300
+ /**
1301
+ * GET /channels?archived=true — the caller's ARCHIVED channels only (the
1302
+ * default listing is active-only; ListChannelsHandler channels.go:274).
1303
+ */
1304
+ async listArchivedChannels() {
1305
+ const data = await this.req("GET", "/channels?archived=true");
1306
+ return (data.channels ?? []).map((c) => ({ ...c, members: normalizeMembers(c.members) }));
1307
+ }
1308
+ /**
1309
+ * POST /channels — create a channel (Braid S3). Body per CreateChannelHandler
1310
+ * (channels.go:66): only {name}; the creator is resolved server-side and
1311
+ * added as owner; visibility defaults to "private".
1312
+ */
1313
+ async createChannel(name) {
1314
+ const data = await this.req("POST", "/channels", { name });
1315
+ const ch = data.channel ?? {};
1316
+ return { ...ch, members: normalizeMembers(ch?.members) };
1317
+ }
1318
+ /** POST /channels/{id}/archive — reversible retire (channels are archive-only; no delete exists). */
1319
+ async archiveChannel(channelId) {
1320
+ await this.req("POST", `/channels/${encodeURIComponent(channelId)}/archive`, {});
1321
+ }
1322
+ /** POST /channels/{id}/unarchive — restore an archived channel (idempotent). */
1323
+ async unarchiveChannel(channelId) {
1324
+ await this.req("POST", `/channels/${encodeURIComponent(channelId)}/unarchive`, {});
1325
+ }
1326
+ /**
1327
+ * POST /channels/{id}/agents/{agentId} — add an already-registered agent to
1328
+ * the channel via the PORTABILITY route (AddExistingAgentToChannelHandler,
1329
+ * agent_add_to_channel.go:35; route router.go:364). agentId is UUID-or-name;
1330
+ * we always send the UUID (unambiguous). This is the ONLY add path that
1331
+ * works for PERSONAL agents: AddMemberHandler's org gate (channels.go:789-797)
1332
+ * COALESCEs a personal agent's NULL org to uuid.Nil → 404 "agent not found".
1333
+ * Owner-only for personal agents; 403 guest_agents_disabled when the channel
1334
+ * org disallows guests. Idempotent; no request body; responds {ok:true}.
1335
+ */
1336
+ async addAgentToChannel(channelId, agentId) {
1337
+ await this.req(
1338
+ "POST",
1339
+ `/channels/${encodeURIComponent(channelId)}/agents/${encodeURIComponent(agentId)}`
1340
+ );
1341
+ }
1342
+ /**
1343
+ * DELETE /channels/{id}/members/{memberId} — memberId is the member row's
1344
+ * member_id: agent name for a bot, user id for a human (channels.go:951,995).
1345
+ */
1346
+ async removeChannelMember(channelId, memberId) {
1347
+ await this.req(
1348
+ "DELETE",
1349
+ `/channels/${encodeURIComponent(channelId)}/members/${encodeURIComponent(memberId)}`
1350
+ );
1351
+ }
924
1352
  /** GET /channels/{id} */
925
1353
  async getChannel(channelId) {
926
1354
  const data = await this.req("GET", `/channels/${encodeURIComponent(channelId)}`);
@@ -966,6 +1394,108 @@ var RelayRest = class {
966
1394
  async dismissNotification(id) {
967
1395
  await this.req("POST", `/notifications/${encodeURIComponent(id)}/dismiss`, {});
968
1396
  }
1397
+ /** POST /agents — mint a personal agent + one-time connect invite (Braid S1 `agent add`) */
1398
+ async createPersonalAgent(name) {
1399
+ return this.req("POST", "/agents", { name });
1400
+ }
1401
+ /** GET /agents/mine — my agents with live presence (Braid S1 `agents`) */
1402
+ async listMyAgents() {
1403
+ const data = await this.req("GET", "/agents/mine");
1404
+ return data.agents ?? [];
1405
+ }
1406
+ /**
1407
+ * POST /agents/{id}/reissue — mint a fresh one-time connect invite for an
1408
+ * existing personal agent (Braid S2 `agent reissue`). Owner-only; 429 when
1409
+ * the relay's register rate limit trips.
1410
+ */
1411
+ async reissueAgent(agentId) {
1412
+ return this.req("POST", `/agents/${encodeURIComponent(agentId)}/reissue`, {});
1413
+ }
1414
+ /** POST /agents/{id}/credential/revoke — revoke the durable key family + close the live WS (Braid S2). */
1415
+ async revokeAgentCredential(agentId) {
1416
+ const data = await this.req(
1417
+ "POST",
1418
+ `/agents/${encodeURIComponent(agentId)}/credential/revoke`,
1419
+ {}
1420
+ );
1421
+ return data.credential ?? {};
1422
+ }
1423
+ /** DELETE /agents/{id} — permanently delete the agent (row, memberships, Clerk machine). */
1424
+ async deleteAgent(agentId) {
1425
+ await this.req("DELETE", `/agents/${encodeURIComponent(agentId)}`);
1426
+ }
1427
+ /**
1428
+ * POST /channels/{id}/agents/invite — mint a PENDING channel-scoped agent
1429
+ * invite (CreateAgentInviteHandler, agent_invite.go:47; route router.go:353,
1430
+ * RequireChannelMembership + RequireOrgAdmin). Body requires {name}
1431
+ * (agent_invite.go:75-77); provisioning happens at redeem. The relay stores
1432
+ * only the secret's SHA-256 — the response is the ONLY copy of the secret.
1433
+ */
1434
+ async createChannelAgentInvite(channelId, name) {
1435
+ return this.req(
1436
+ "POST",
1437
+ `/channels/${encodeURIComponent(channelId)}/agents/invite`,
1438
+ { name }
1439
+ );
1440
+ }
1441
+ /** GET /channels/{id}/agents/invites — the channel's invites, newest first (agent_invites_manage.go:17). */
1442
+ async listChannelAgentInvites(channelId) {
1443
+ const data = await this.req(
1444
+ "GET",
1445
+ `/channels/${encodeURIComponent(channelId)}/agents/invites`
1446
+ );
1447
+ return data.invites ?? [];
1448
+ }
1449
+ /**
1450
+ * DELETE /channels/{id}/agents/invites/{inviteId} — soft-revoke a
1451
+ * channel-scoped invite (agent_invites_manage.go:55). Idempotent 200;
1452
+ * unknown/cross-channel id → 404 "invite not found".
1453
+ */
1454
+ async revokeChannelAgentInvite(channelId, inviteId) {
1455
+ await this.req(
1456
+ "DELETE",
1457
+ `/channels/${encodeURIComponent(channelId)}/agents/invites/${encodeURIComponent(inviteId)}`
1458
+ );
1459
+ }
1460
+ /**
1461
+ * GET /agents/invites — the caller's org's channel-less ORG-kind invites,
1462
+ * newest first, same row shape as the channel listing (org_invites.go:20;
1463
+ * route router.go:236, members.manage-gated → 403 admin_required).
1464
+ */
1465
+ async listOrgAgentInvites() {
1466
+ const data = await this.req("GET", "/agents/invites");
1467
+ return data.invites ?? [];
1468
+ }
1469
+ /** DELETE /agents/invites/{inviteId} — soft-revoke an ORG-kind invite (org_invites.go:65); 404 hides cross-org/channel-kind ids. */
1470
+ async revokeOrgAgentInvite(inviteId) {
1471
+ await this.req("DELETE", `/agents/invites/${encodeURIComponent(inviteId)}`);
1472
+ }
1473
+ /**
1474
+ * POST /channels/{id}/humans/invite — invite a human by email
1475
+ * (InviteHumanHandler, humans_invite.go:26; route router.go:360, org-admin).
1476
+ * Hybrid Clerk+relay: sends a Clerk org invitation and adds a pending user to
1477
+ * the channel; an EXISTING cross-org user joins this channel only, as a guest.
1478
+ */
1479
+ async inviteHumanToChannel(channelId, email) {
1480
+ return this.req(
1481
+ "POST",
1482
+ `/channels/${encodeURIComponent(channelId)}/humans/invite`,
1483
+ { email }
1484
+ );
1485
+ }
1486
+ /** GET /organizations/me — caller's own org id + guest-agent policy (organizations.go:16; 404 when no active org). */
1487
+ async getMyOrganization() {
1488
+ const data = await this.req("GET", "/organizations/me");
1489
+ return data.organization;
1490
+ }
1491
+ /**
1492
+ * PATCH /organizations/{id} {allowGuestAgents} — admin-only, own-org-only
1493
+ * (PatchOrganizationHandler, organizations.go:36-59; route router.go:151,
1494
+ * RequireOrgAdmin → 403 admin_required; cross-org id → 404).
1495
+ */
1496
+ async patchOrganization(orgId, patch) {
1497
+ await this.req("PATCH", `/organizations/${encodeURIComponent(orgId)}`, patch);
1498
+ }
969
1499
  };
970
1500
 
971
1501
  // src/state/activityFold.ts
@@ -1037,10 +1567,10 @@ function foldActivityEvents(events) {
1037
1567
 
1038
1568
  // src/state/priority.ts
1039
1569
  function channelPriority(cid, p) {
1040
- const level = p.attention[cid]?.highestLevel;
1041
- if (level === "critical") return 0;
1042
- if (level === "action_required") return 1;
1043
- if (level === "info") return 2;
1570
+ const level2 = p.attention[cid]?.highestLevel;
1571
+ if (level2 === "critical") return 0;
1572
+ if (level2 === "action_required") return 1;
1573
+ if (level2 === "info") return 2;
1044
1574
  if ((p.unread[cid] ?? 0) > 0) return 3;
1045
1575
  return 4;
1046
1576
  }
@@ -1342,6 +1872,10 @@ function createStore(opts) {
1342
1872
  unread: { ...config.unread ?? {} },
1343
1873
  unreadCapNotice: null,
1344
1874
  attention: {},
1875
+ channelAttention: [],
1876
+ attentionSelected: 0,
1877
+ attentionDraft: "",
1878
+ attentionBusy: false,
1345
1879
  notifications: [],
1346
1880
  notifSelected: 0,
1347
1881
  activityByTurn: {},
@@ -1498,6 +2032,9 @@ function createStore(opts) {
1498
2032
  }
1499
2033
  const rest = new RelayRest({ relayUrl, getToken });
1500
2034
  const ws = new RelayWs({ relayUrl, getToken });
2035
+ const attentionApi = () => opts.attentionApi ?? new AttentionMutations(new LifecycleTransport({ relayUrl, getToken }));
2036
+ let pendingAttentionLevel = null;
2037
+ let pendingAttentionAck = null;
1501
2038
  function pushDivider(label) {
1502
2039
  const id = `divider-${++dividerCounter}`;
1503
2040
  seen.add(id);
@@ -1595,6 +2132,141 @@ function createStore(opts) {
1595
2132
  const next = Math.max(0, Math.min(state.notifSelected + delta, len - 1));
1596
2133
  set({ notifSelected: next });
1597
2134
  }
2135
+ async function refreshChannelAttention() {
2136
+ const channelId = state.activeChannelId;
2137
+ if (!channelId) return;
2138
+ try {
2139
+ const items = await attentionApi().listChannel(channelId);
2140
+ if (state.activeChannelId !== channelId) return;
2141
+ setThrottled({
2142
+ channelAttention: items,
2143
+ attentionSelected: Math.max(0, Math.min(state.attentionSelected, items.length - 1))
2144
+ });
2145
+ } catch (error) {
2146
+ if (error instanceof CommandFailure && state.mode.startsWith("attention")) {
2147
+ set({ error: `${error.stableCode}: ${error.message}` });
2148
+ }
2149
+ }
2150
+ }
2151
+ function finishAttentionMutation(error) {
2152
+ pendingAttentionLevel = null;
2153
+ pendingAttentionAck = null;
2154
+ set({
2155
+ mode: "attention",
2156
+ attentionDraft: "",
2157
+ attentionBusy: false,
2158
+ error: error instanceof CommandFailure ? `${error.stableCode}: ${error.message}` : error ? "attention operation failed safely" : null
2159
+ });
2160
+ void refreshChannelAttention();
2161
+ void pollAttention();
2162
+ }
2163
+ async function performAttentionRaise(level2, reason) {
2164
+ const channelId = state.activeChannelId;
2165
+ if (!channelId || state.attentionBusy) return;
2166
+ set({ mode: "attention", attentionDraft: "", attentionBusy: true, error: null });
2167
+ const operationId = (opts.newOperationId ?? randomUUID)();
2168
+ try {
2169
+ const api = attentionApi();
2170
+ const before = await api.listChannel(channelId);
2171
+ const previousCount = before.find((item) => item.level === level2)?.count ?? 0;
2172
+ try {
2173
+ await api.raise({ channelId, level: level2, reason, operationId });
2174
+ } catch (error) {
2175
+ if (!(error instanceof OutcomeUnknown)) throw error;
2176
+ const current = (await api.listChannel(channelId)).find((item) => item.level === level2);
2177
+ if (!current || current.reason !== reason || current.count <= previousCount) throw error;
2178
+ }
2179
+ finishAttentionMutation();
2180
+ } catch (error) {
2181
+ finishAttentionMutation(error);
2182
+ }
2183
+ }
2184
+ async function performAttentionAck(scope) {
2185
+ const channelId = state.activeChannelId;
2186
+ if (!channelId || state.attentionBusy) return;
2187
+ set({ mode: "attention", attentionDraft: "", attentionBusy: true, error: null });
2188
+ const operationId = (opts.newOperationId ?? randomUUID)();
2189
+ try {
2190
+ const api = attentionApi();
2191
+ try {
2192
+ await api.ack({ channelId, ...scope === "all" ? {} : { level: scope }, operationId });
2193
+ } catch (error) {
2194
+ if (!(error instanceof OutcomeUnknown)) throw error;
2195
+ const current = await api.listChannel(channelId);
2196
+ const reconciled = scope === "all" ? current.length === 0 : !current.some((item) => item.level === scope);
2197
+ if (!reconciled) throw error;
2198
+ }
2199
+ finishAttentionMutation();
2200
+ } catch (error) {
2201
+ finishAttentionMutation(error);
2202
+ }
2203
+ }
2204
+ function openAttention() {
2205
+ if (!state.activeChannelId || state.attentionBusy) return;
2206
+ set({ mode: "attention", attentionDraft: "", attentionSelected: 0, error: null });
2207
+ void refreshChannelAttention();
2208
+ }
2209
+ function closeAttention() {
2210
+ if (state.attentionBusy) return;
2211
+ pendingAttentionLevel = null;
2212
+ pendingAttentionAck = null;
2213
+ set({
2214
+ mode: state.mode === "attention" ? "normal" : "attention",
2215
+ attentionDraft: "",
2216
+ error: null
2217
+ });
2218
+ }
2219
+ function moveAttentionSelection(delta) {
2220
+ if (state.channelAttention.length === 0 || state.attentionBusy) return;
2221
+ set({ attentionSelected: Math.max(0, Math.min(state.attentionSelected + delta, state.channelAttention.length - 1)) });
2222
+ }
2223
+ function beginAttentionRaise() {
2224
+ if (state.attentionBusy) return;
2225
+ pendingAttentionLevel = null;
2226
+ pendingAttentionAck = null;
2227
+ set({ mode: "attention_level", attentionDraft: "", error: null });
2228
+ }
2229
+ function beginAttentionAck(all) {
2230
+ if (state.attentionBusy) return;
2231
+ if (all) pendingAttentionAck = "all";
2232
+ else pendingAttentionAck = state.channelAttention[state.attentionSelected]?.level ?? null;
2233
+ if (!pendingAttentionAck) return;
2234
+ set({ mode: "attention_confirm", attentionDraft: "", error: null });
2235
+ }
2236
+ function setAttentionDraft(value) {
2237
+ const max = state.mode === "attention_level" ? 32 : state.mode === "attention_confirm" ? 36 : 1024;
2238
+ if (Buffer.byteLength(value, "utf8") <= max) set({ attentionDraft: value });
2239
+ }
2240
+ function submitAttentionInput() {
2241
+ if (state.attentionBusy) return;
2242
+ if (state.mode === "attention_level") {
2243
+ const value = state.attentionDraft.trim();
2244
+ if (!ATTENTION_LEVELS.has(value)) {
2245
+ set({ error: "level must be info, action_required, or critical" });
2246
+ return;
2247
+ }
2248
+ pendingAttentionLevel = value;
2249
+ set({ mode: "attention_reason", attentionDraft: "", error: null });
2250
+ return;
2251
+ }
2252
+ if (state.mode === "attention_reason") {
2253
+ if (!pendingAttentionLevel) return;
2254
+ try {
2255
+ const reason = normalizeReason(state.attentionDraft);
2256
+ void performAttentionRaise(pendingAttentionLevel, reason);
2257
+ } catch (error) {
2258
+ if (error instanceof CommandFailure) set({ error: `${error.stableCode}: ${error.message}` });
2259
+ }
2260
+ return;
2261
+ }
2262
+ if (state.mode === "attention_confirm") {
2263
+ if (state.attentionDraft !== state.activeChannelId) {
2264
+ set({ error: "confirmation did not match the channel UUID" });
2265
+ return;
2266
+ }
2267
+ if (pendingAttentionAck) void performAttentionAck(pendingAttentionAck);
2268
+ }
2269
+ }
1598
2270
  function seedPresence(members) {
1599
2271
  const presence = {};
1600
2272
  for (const m of members) presence[m.id] = m.status === "online" ? "active" : m.status;
@@ -1811,6 +2483,16 @@ function createStore(opts) {
1811
2483
  pushDivider("channel archived");
1812
2484
  return;
1813
2485
  }
2486
+ case "channel.attention.raised":
2487
+ case "channel.attention.updated":
2488
+ case "channel.attention.cleared":
2489
+ case "channel.attention.acked": {
2490
+ void pollAttention();
2491
+ if (frame.channelId === state.activeChannelId && state.mode.startsWith("attention")) {
2492
+ void refreshChannelAttention();
2493
+ }
2494
+ return;
2495
+ }
1814
2496
  case "activity_event": {
1815
2497
  if (frame.channelId !== state.activeChannelId) return;
1816
2498
  if (!frame.turnId || !frame.toolCallId) return;
@@ -2093,6 +2775,13 @@ function createStore(opts) {
2093
2775
  dismissNotification: (id) => void dismissNotification(id),
2094
2776
  markAllNotificationsRead: () => void markAllNotificationsRead(),
2095
2777
  moveNotifSelection,
2778
+ openAttention,
2779
+ closeAttention,
2780
+ moveAttentionSelection,
2781
+ beginAttentionRaise,
2782
+ beginAttentionAck,
2783
+ setAttentionDraft,
2784
+ submitAttentionInput,
2096
2785
  jumpNextActive: () => {
2097
2786
  const s = state;
2098
2787
  const target = nextActiveChannel(s.channels, s.activeChannelId, { attention: s.attention, unread: s.unread });
@@ -2105,22 +2794,6 @@ function createStore(opts) {
2105
2794
 
2106
2795
  // src/ui/ActivityLine.tsx
2107
2796
  import { Text } from "ink";
2108
-
2109
- // src/util/sanitize.ts
2110
- var ESCAPE_SEQ = (
2111
- // eslint-disable-next-line no-control-regex
2112
- /\x1b(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:\x07|\x1b\\)|[PX^_][\s\S]*?\x1b\\|[@-Z\\-_])/g
2113
- );
2114
- var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
2115
- function sanitizeForTty(input) {
2116
- return input.replace(ESCAPE_SEQ, "").replace(CONTROL_CHARS, "");
2117
- }
2118
- function sanitizeLabel(input, max = 40) {
2119
- const clean = sanitizeForTty(input);
2120
- return clean.length > max ? clean.slice(0, max - 1) + "\u2026" : clean;
2121
- }
2122
-
2123
- // src/ui/ActivityLine.tsx
2124
2797
  import { jsx, jsxs } from "react/jsx-runtime";
2125
2798
  var MAX_SHOWN = 3;
2126
2799
  function ActivityLine({ activities }) {
@@ -2212,7 +2885,7 @@ function ActivitySurface(props) {
2212
2885
  pinned && call.locations && call.locations.length > 0 && /* @__PURE__ */ jsx2(Box, { flexDirection: "column", paddingLeft: 2, children: call.locations.map((loc, i) => /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
2213
2886
  sanitizeLabel(loc.path, 60),
2214
2887
  loc.line != null ? `:${loc.line}` : ""
2215
- ] }, i)) })
2888
+ ] }, `${loc.path}:${loc.line ?? ""}#${i}`)) })
2216
2889
  ] }, call.toolCallId))
2217
2890
  ] }),
2218
2891
  plan.length > 0 && /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
@@ -2221,15 +2894,71 @@ function ActivitySurface(props) {
2221
2894
  planGlyph(entry.status),
2222
2895
  " ",
2223
2896
  sanitizeForTty(entry.content)
2224
- ] }, i))
2897
+ ] }, `${entry.content}#${i}`))
2225
2898
  ] }),
2226
2899
  /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pinHint })
2227
2900
  ] });
2228
2901
  }
2229
2902
 
2230
- // src/ui/ChannelSwitcher.tsx
2903
+ // src/ui/AttentionPanel.tsx
2231
2904
  import { Box as Box2, Text as Text3 } from "ink";
2232
2905
  import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
2906
+ var LEVEL_COLOR = {
2907
+ info: "blue",
2908
+ action_required: "yellow",
2909
+ critical: "red"
2910
+ };
2911
+ function AttentionPanel({
2912
+ channelId,
2913
+ items,
2914
+ selected,
2915
+ mode,
2916
+ draft,
2917
+ busy
2918
+ }) {
2919
+ const prompt = mode === "attention_level" ? "level (info | action_required | critical)" : mode === "attention_reason" ? "reason" : mode === "attention_confirm" ? `type ${channelId} to confirm` : null;
2920
+ return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2921
+ /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
2922
+ "ATTENTION channel:",
2923
+ sanitizeForTty(channelId)
2924
+ ] }),
2925
+ items.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "no active attention" }) : items.map((item, index) => /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
2926
+ /* @__PURE__ */ jsxs3(Text3, { children: [
2927
+ /* @__PURE__ */ jsxs3(Text3, { color: index === selected ? "cyan" : void 0, children: [
2928
+ index === selected ? ">" : " ",
2929
+ " "
2930
+ ] }),
2931
+ /* @__PURE__ */ jsx3(Text3, { bold: true, color: LEVEL_COLOR[item.level], children: item.level }),
2932
+ /* @__PURE__ */ jsxs3(Text3, { children: [
2933
+ " count:",
2934
+ item.count,
2935
+ " ",
2936
+ item.raisedByType ?? "unknown",
2937
+ ":",
2938
+ sanitizeForTty(item.raisedBy ?? "unknown")
2939
+ ] })
2940
+ ] }),
2941
+ /* @__PURE__ */ jsxs3(Text3, { wrap: "wrap", children: [
2942
+ " ",
2943
+ sanitizeForTty(item.reason)
2944
+ ] }),
2945
+ /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2946
+ " updated ",
2947
+ sanitizeForTty(item.updatedAt)
2948
+ ] })
2949
+ ] }, item.id)),
2950
+ prompt ? /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
2951
+ prompt,
2952
+ ": ",
2953
+ sanitizeForTty(draft),
2954
+ "\u258C"
2955
+ ] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: busy ? "working\u2026" : "j/k select \xB7 r raise \xB7 x ack selected \xB7 X ack all \xB7 Esc close" })
2956
+ ] });
2957
+ }
2958
+
2959
+ // src/ui/ChannelSwitcher.tsx
2960
+ import { Box as Box3, Text as Text4 } from "ink";
2961
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
2233
2962
  function channelLabel(ch) {
2234
2963
  return `${channelSigil(ch.kind)}${sanitizeLabel(ch.name)}`;
2235
2964
  }
@@ -2244,7 +2973,7 @@ function ChannelBadge({
2244
2973
  if (att) {
2245
2974
  const color = att.highestLevel === "critical" ? "red" : att.highestLevel === "action_required" ? "yellow" : "cyan";
2246
2975
  const sigil = att.highestLevel === "critical" ? "!" : att.highestLevel === "action_required" ? "\u25B2" : "\u2022";
2247
- return /* @__PURE__ */ jsxs3(Text3, { color, children: [
2976
+ return /* @__PURE__ */ jsxs4(Text4, { color, children: [
2248
2977
  " ",
2249
2978
  "[",
2250
2979
  sigil,
@@ -2252,11 +2981,11 @@ function ChannelBadge({
2252
2981
  "]"
2253
2982
  ] });
2254
2983
  }
2255
- const count = unread[channelId] ?? 0;
2256
- if (count > 0) {
2257
- return /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
2984
+ const count2 = unread[channelId] ?? 0;
2985
+ if (count2 > 0) {
2986
+ return /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2258
2987
  " (",
2259
- count,
2988
+ count2,
2260
2989
  ")"
2261
2990
  ] });
2262
2991
  }
@@ -2273,11 +3002,11 @@ function ChannelSwitcher({
2273
3002
  const sorted = sortChannelsByPriority(filtered, { attention, unread });
2274
3003
  const visible = sorted.slice(0, 9);
2275
3004
  const hidden = sorted.length - visible.length;
2276
- return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2277
- /* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
3005
+ return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
3006
+ /* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
2278
3007
  "channels",
2279
3008
  " ",
2280
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3009
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2281
3010
  "(",
2282
3011
  sorted.length,
2283
3012
  "/",
@@ -2285,23 +3014,23 @@ function ChannelSwitcher({
2285
3014
  ")"
2286
3015
  ] })
2287
3016
  ] }),
2288
- /* @__PURE__ */ jsxs3(Text3, { children: [
2289
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "filter: " }),
2290
- /* @__PURE__ */ jsx3(Text3, { children: sanitizeLabel(filter, 40) }),
2291
- /* @__PURE__ */ jsx3(Text3, { inverse: true, children: " " })
3017
+ /* @__PURE__ */ jsxs4(Text4, { children: [
3018
+ /* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "filter: " }),
3019
+ /* @__PURE__ */ jsx4(Text4, { children: sanitizeLabel(filter, 40) }),
3020
+ /* @__PURE__ */ jsx4(Text4, { inverse: true, children: " " })
2292
3021
  ] }),
2293
- visible.map((ch, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
2294
- /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
3022
+ visible.map((ch, i) => /* @__PURE__ */ jsxs4(Text4, { children: [
3023
+ /* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
2295
3024
  i + 1,
2296
3025
  " "
2297
3026
  ] }),
2298
- /* @__PURE__ */ jsx3(Text3, { bold: ch.id === activeChannelId, children: channelLabel(ch) }),
2299
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3027
+ /* @__PURE__ */ jsx4(Text4, { bold: ch.id === activeChannelId, children: channelLabel(ch) }),
3028
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2300
3029
  " \xB7 ",
2301
3030
  ch.members.length,
2302
3031
  " members"
2303
3032
  ] }),
2304
- /* @__PURE__ */ jsx3(
3033
+ /* @__PURE__ */ jsx4(
2305
3034
  ChannelBadge,
2306
3035
  {
2307
3036
  channelId: ch.id,
@@ -2311,36 +3040,36 @@ function ChannelSwitcher({
2311
3040
  }
2312
3041
  )
2313
3042
  ] }, ch.id)),
2314
- hidden > 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3043
+ hidden > 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2315
3044
  "\u2026",
2316
3045
  hidden,
2317
3046
  " more \u2014 keep typing to narrow"
2318
3047
  ] }) : null,
2319
- sorted.length === 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3048
+ sorted.length === 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2320
3049
  'no channels match "',
2321
3050
  sanitizeLabel(filter, 20),
2322
3051
  '"'
2323
3052
  ] }) : null,
2324
- /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: filter.length === 0 ? "type to filter \xB7 1\u20139 quick-select \xB7 n jump \xB7 Esc cancel" : "keep typing to narrow \xB7 Enter opens top match \xB7 Esc cancel" })
3053
+ /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: filter.length === 0 ? "type to filter \xB7 1\u20139 quick-select \xB7 n jump \xB7 Esc cancel" : "keep typing to narrow \xB7 Enter opens top match \xB7 Esc cancel" })
2325
3054
  ] });
2326
3055
  }
2327
3056
 
2328
3057
  // src/ui/Composer.tsx
2329
- import { Box as Box3, Text as Text4 } from "ink";
2330
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
3058
+ import { Box as Box4, Text as Text5 } from "ink";
3059
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
2331
3060
  function Composer({ draft, focused }) {
2332
3061
  const width = Math.max((process.stdout.columns ?? 80) - 8, 20);
2333
3062
  const visible = draft.length > width ? "\u2026" + draft.slice(-(width - 1)) : draft;
2334
- return /* @__PURE__ */ jsx4(Box3, { borderStyle: "round", borderColor: focused ? "cyan" : "gray", paddingX: 1, width: "100%", children: /* @__PURE__ */ jsxs4(Text4, { wrap: "truncate", children: [
2335
- /* @__PURE__ */ jsx4(Text4, { color: focused ? "cyan" : "gray", children: "> " }),
2336
- draft.length === 0 && !focused ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "i to compose" }) : /* @__PURE__ */ jsx4(Text4, { children: visible }),
2337
- focused ? /* @__PURE__ */ jsx4(Text4, { inverse: true, children: " " }) : null
3063
+ return /* @__PURE__ */ jsx5(Box4, { borderStyle: "round", borderColor: focused ? "cyan" : "gray", paddingX: 1, width: "100%", children: /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
3064
+ /* @__PURE__ */ jsx5(Text5, { color: focused ? "cyan" : "gray", children: "> " }),
3065
+ draft.length === 0 && !focused ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "i to compose" }) : /* @__PURE__ */ jsx5(Text5, { children: visible }),
3066
+ focused ? /* @__PURE__ */ jsx5(Text5, { inverse: true, children: " " }) : null
2338
3067
  ] }) });
2339
3068
  }
2340
3069
 
2341
3070
  // src/ui/Header.tsx
2342
- import { Box as Box4, Text as Text5 } from "ink";
2343
- import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
3071
+ import { Box as Box5, Text as Text6 } from "ink";
3072
+ import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
2344
3073
  var STATUS_DOT = {
2345
3074
  online: { glyph: "\u25CF", color: "green" },
2346
3075
  away: { glyph: "\u25D0", color: "yellow" },
@@ -2357,32 +3086,32 @@ function Header({
2357
3086
  members,
2358
3087
  presence
2359
3088
  }) {
2360
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
2361
- /* @__PURE__ */ jsxs5(Box4, { justifyContent: "space-between", children: [
2362
- /* @__PURE__ */ jsxs5(Text5, { children: [
2363
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "arp-tui" }),
2364
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \u25B8 " }),
2365
- /* @__PURE__ */ jsxs5(Text5, { bold: true, children: [
3089
+ return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", children: [
3090
+ /* @__PURE__ */ jsxs6(Box5, { justifyContent: "space-between", children: [
3091
+ /* @__PURE__ */ jsxs6(Text6, { children: [
3092
+ /* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: "arp-tui" }),
3093
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " \u25B8 " }),
3094
+ /* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
2366
3095
  channelSigil(channelKind),
2367
3096
  sanitizeLabel(channelName)
2368
3097
  ] })
2369
3098
  ] }),
2370
- /* @__PURE__ */ jsx5(Text5, { children: members.map((m) => {
3099
+ /* @__PURE__ */ jsx6(Text6, { children: members.map((m) => {
2371
3100
  const dot = STATUS_DOT[effectiveStatus(m, presence)];
2372
- return /* @__PURE__ */ jsxs5(Text5, { children: [
2373
- /* @__PURE__ */ jsxs5(Text5, { color: dot.color, children: [
3101
+ return /* @__PURE__ */ jsxs6(Text6, { children: [
3102
+ /* @__PURE__ */ jsxs6(Text6, { color: dot.color, children: [
2374
3103
  dot.glyph,
2375
3104
  " "
2376
3105
  ] }),
2377
- /* @__PURE__ */ jsxs5(Text5, { dimColor: effectiveStatus(m, presence) === "offline", children: [
3106
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: effectiveStatus(m, presence) === "offline", children: [
2378
3107
  m.type === "bot" ? "\u2699" : "",
2379
3108
  sanitizeLabel(m.name, 20)
2380
3109
  ] }),
2381
- /* @__PURE__ */ jsx5(Text5, { children: " " })
3110
+ /* @__PURE__ */ jsx6(Text6, { children: " " })
2382
3111
  ] }, m.id);
2383
3112
  }) })
2384
3113
  ] }),
2385
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "\u2500".repeat(Math.min(process.stdout.columns ?? 80, 100)) })
3114
+ /* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2500".repeat(Math.min(process.stdout.columns ?? 80, 100)) })
2386
3115
  ] });
2387
3116
  }
2388
3117
 
@@ -2390,8 +3119,8 @@ function Header({
2390
3119
  import { Static } from "ink";
2391
3120
 
2392
3121
  // src/ui/MessageRow.tsx
2393
- import { Box as Box5, Text as Text6 } from "ink";
2394
- import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
3122
+ import { Box as Box6, Text as Text7 } from "ink";
3123
+ import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
2395
3124
  function hhmm(rfc3339) {
2396
3125
  const d = new Date(rfc3339);
2397
3126
  return isNaN(d.getTime()) ? "--:--" : `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
@@ -2404,7 +3133,7 @@ function TokenChip({ message }) {
2404
3133
  if (message.messageType !== "agent" || !message.tokensUsed) return null;
2405
3134
  const parts = [`${formatTokens(message.tokensUsed)} tok`];
2406
3135
  if (message.model) parts.push(sanitizeLabel(message.model, 16));
2407
- return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3136
+ return /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2408
3137
  message.verified ? "\u2713 " : "",
2409
3138
  parts.join(" \xB7 ")
2410
3139
  ] });
@@ -2414,41 +3143,41 @@ function MessageRow({ message }) {
2414
3143
  const isSystem = message.messageType === "system";
2415
3144
  const name = sanitizeLabel(message.agentName, 20);
2416
3145
  if (isSystem) {
2417
- return /* @__PURE__ */ jsx6(Box5, { children: /* @__PURE__ */ jsxs6(Text6, { dimColor: true, italic: true, children: [
3146
+ return /* @__PURE__ */ jsx7(Box6, { children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, italic: true, children: [
2418
3147
  " ",
2419
3148
  "\u2500\u2500 ",
2420
3149
  sanitizeForTty(message.content),
2421
3150
  " \u2500\u2500"
2422
3151
  ] }) });
2423
3152
  }
2424
- return /* @__PURE__ */ jsxs6(Box5, { justifyContent: "space-between", gap: 2, children: [
2425
- /* @__PURE__ */ jsx6(Box5, { flexShrink: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
2426
- /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3153
+ return /* @__PURE__ */ jsxs7(Box6, { justifyContent: "space-between", gap: 2, children: [
3154
+ /* @__PURE__ */ jsx7(Box6, { flexShrink: 1, children: /* @__PURE__ */ jsxs7(Text7, { children: [
3155
+ /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2427
3156
  hhmm(message.createdAt),
2428
3157
  " "
2429
3158
  ] }),
2430
- /* @__PURE__ */ jsxs6(Text6, { color: isBot ? "magenta" : "cyan", bold: !isBot, children: [
3159
+ /* @__PURE__ */ jsxs7(Text7, { color: isBot ? "magenta" : "cyan", bold: !isBot, children: [
2431
3160
  isBot ? "\u2699" : "",
2432
3161
  name.padEnd(12)
2433
3162
  ] }),
2434
- /* @__PURE__ */ jsxs6(Text6, { children: [
3163
+ /* @__PURE__ */ jsxs7(Text7, { children: [
2435
3164
  " ",
2436
3165
  sanitizeForTty(message.content)
2437
3166
  ] })
2438
3167
  ] }) }),
2439
- /* @__PURE__ */ jsx6(Box5, { flexShrink: 0, children: /* @__PURE__ */ jsx6(TokenChip, { message }) })
3168
+ /* @__PURE__ */ jsx7(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx7(TokenChip, { message }) })
2440
3169
  ] });
2441
3170
  }
2442
3171
 
2443
3172
  // src/ui/MessageList.tsx
2444
- import { jsx as jsx7 } from "react/jsx-runtime";
3173
+ import { jsx as jsx8 } from "react/jsx-runtime";
2445
3174
  function MessageList({ messages }) {
2446
- return /* @__PURE__ */ jsx7(Static, { items: messages, children: (m) => /* @__PURE__ */ jsx7(MessageRow, { message: m }, m.id) });
3175
+ return /* @__PURE__ */ jsx8(Static, { items: messages, children: (m) => /* @__PURE__ */ jsx8(MessageRow, { message: m }, m.id) });
2447
3176
  }
2448
3177
 
2449
3178
  // src/ui/NotificationsPanel.tsx
2450
- import { Box as Box6, Text as Text7 } from "ink";
2451
- import { jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
3179
+ import { Box as Box7, Text as Text8 } from "ink";
3180
+ import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
2452
3181
  function relativeAge(createdAt) {
2453
3182
  const ms = Date.now() - new Date(createdAt).getTime();
2454
3183
  if (!Number.isFinite(ms) || ms < 0) return "\u2014";
@@ -2466,11 +3195,11 @@ function NotificationsPanel({
2466
3195
  notifSelected
2467
3196
  }) {
2468
3197
  const unreadCount = notifications.filter((n) => !n.readAt).length;
2469
- return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2470
- /* @__PURE__ */ jsxs7(Text7, { bold: true, children: [
3198
+ return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
3199
+ /* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
2471
3200
  "alerts",
2472
3201
  " ",
2473
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3202
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2474
3203
  "(",
2475
3204
  unreadCount,
2476
3205
  " unread \xB7 ",
@@ -2478,38 +3207,42 @@ function NotificationsPanel({
2478
3207
  " total)"
2479
3208
  ] })
2480
3209
  ] }),
2481
- notifications.length === 0 ? /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: "no notifications" }) : notifications.map((n, i) => {
3210
+ notifications.length === 0 ? /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "no notifications" }) : notifications.map((n, i) => {
2482
3211
  const isSelected = i === notifSelected;
2483
3212
  const isUnread = n.readAt === null;
2484
3213
  const type = sanitizeLabel(n.type, 20);
2485
3214
  const ref = shortRef(sanitizeForTty(n.entityRef));
2486
3215
  const age = relativeAge(n.createdAt);
2487
- return /* @__PURE__ */ jsxs7(Text7, { inverse: isSelected, children: [
2488
- isUnread ? /* @__PURE__ */ jsx8(Text7, { color: "yellow", children: "\u2022 " }) : /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " " }),
2489
- /* @__PURE__ */ jsx8(Text7, { bold: isSelected, children: type }),
2490
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3216
+ return /* @__PURE__ */ jsxs8(Text8, { inverse: isSelected, children: [
3217
+ isUnread ? /* @__PURE__ */ jsx9(Text8, { color: "yellow", children: "\u2022 " }) : /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " " }),
3218
+ /* @__PURE__ */ jsx9(Text8, { bold: isSelected, children: type }),
3219
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2491
3220
  " ",
2492
3221
  ref
2493
3222
  ] }),
2494
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3223
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2495
3224
  " ",
2496
3225
  age
2497
3226
  ] })
2498
3227
  ] }, n.id);
2499
3228
  }),
2500
- /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: "J/K move \xB7 d dismiss \xB7 a mark all read \xB7 Esc close" })
3229
+ /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "J/K move \xB7 d dismiss \xB7 a mark all read \xB7 Esc close" })
2501
3230
  ] });
2502
3231
  }
2503
3232
 
2504
3233
  // src/ui/StatusBar.tsx
2505
- import { Text as Text8 } from "ink";
2506
- import { Fragment, jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
3234
+ import { Text as Text9 } from "ink";
3235
+ import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
2507
3236
  var MODE_LABEL = {
2508
3237
  normal: "NORMAL",
2509
3238
  insert: "INSERT",
2510
3239
  switcher: "CHANNELS",
2511
3240
  token: "TOKEN",
2512
- notifications: "ALERTS"
3241
+ notifications: "ALERTS",
3242
+ attention: "ATTENTION",
3243
+ attention_level: "ATTENTION LEVEL",
3244
+ attention_reason: "ATTENTION REASON",
3245
+ attention_confirm: "CONFIRM ATTENTION"
2513
3246
  };
2514
3247
  function ageLabel(obtainedAt) {
2515
3248
  if (!obtainedAt) return "\u2014";
@@ -2560,41 +3293,41 @@ function StatusBar({
2560
3293
  color: stale ? "yellow" : "green"
2561
3294
  };
2562
3295
  }
2563
- return /* @__PURE__ */ jsxs8(Text8, { children: [
2564
- /* @__PURE__ */ jsxs8(Text8, { bold: true, color: mode === "insert" ? "cyan" : mode === "token" ? "yellow" : "white", children: [
3296
+ return /* @__PURE__ */ jsxs9(Text9, { children: [
3297
+ /* @__PURE__ */ jsxs9(Text9, { bold: true, color: mode === "insert" ? "cyan" : mode === "token" ? "yellow" : "white", children: [
2565
3298
  " ",
2566
3299
  MODE_LABEL[mode],
2567
3300
  " "
2568
3301
  ] }),
2569
- /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "\u2502 " }),
2570
- /* @__PURE__ */ jsx9(Text8, { color: connColor, children: connectionLabel(connection, reconnectAt, reconnectAttempt) }),
2571
- /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " \u2502 " }),
2572
- /* @__PURE__ */ jsx9(Text8, { color: tokenSeg.color, children: tokenSeg.text }),
2573
- /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
3302
+ /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: "\u2502 " }),
3303
+ /* @__PURE__ */ jsx10(Text9, { color: connColor, children: connectionLabel(connection, reconnectAt, reconnectAttempt) }),
3304
+ /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 " }),
3305
+ /* @__PURE__ */ jsx10(Text9, { color: tokenSeg.color, children: tokenSeg.text }),
3306
+ /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
2574
3307
  " \u2502 seq ",
2575
3308
  lastSeq ?? "\u2014"
2576
3309
  ] }),
2577
- notifCount > 0 ? /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
3310
+ notifCount > 0 ? /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
2578
3311
  " \u2502 alerts:",
2579
3312
  notifCount
2580
3313
  ] }) : null,
2581
3314
  error ? (
2582
3315
  // BL-222: an error replaces the full hint line but must never strand
2583
3316
  // the user — keep the exit hint visible alongside it
2584
- /* @__PURE__ */ jsxs8(Fragment, { children: [
2585
- /* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
3317
+ /* @__PURE__ */ jsxs9(Fragment, { children: [
3318
+ /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
2586
3319
  " \u2502 ",
2587
3320
  sanitizeForTty(error)
2588
3321
  ] }),
2589
- /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " \u2502 q quit" })
3322
+ /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 q quit" })
2590
3323
  ] })
2591
- ) : /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " \u2502 c channels \xB7 a alerts \xB7 n next \xB7 v watch \xB7 i compose \xB7 r reconnect \xB7 Esc normal \xB7 q quit" })
3324
+ ) : /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 c channels \xB7 a alerts \xB7 t attention \xB7 n next \xB7 v watch \xB7 i compose \xB7 r reconnect \xB7 Esc normal \xB7 q quit" })
2592
3325
  ] });
2593
3326
  }
2594
3327
 
2595
3328
  // src/ui/TokenPrompt.tsx
2596
- import { Box as Box7, Text as Text9 } from "ink";
2597
- import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
3329
+ import { Box as Box8, Text as Text10 } from "ink";
3330
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2598
3331
  function TokenPrompt({
2599
3332
  value,
2600
3333
  reason,
@@ -2602,26 +3335,26 @@ function TokenPrompt({
2602
3335
  jwtTemplate
2603
3336
  }) {
2604
3337
  const masked = value.length === 0 ? "" : value.length <= 16 ? value : `${value.slice(0, 12)}\u2026 (${value.length} chars)`;
2605
- return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2606
- /* @__PURE__ */ jsx10(Text9, { bold: true, color: "yellow", children: "token needed" }),
2607
- reason ? /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: sanitizeForTty(reason) }) : null,
2608
- /* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
3338
+ return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
3339
+ /* @__PURE__ */ jsx11(Text10, { bold: true, color: "yellow", children: "token needed" }),
3340
+ reason ? /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: sanitizeForTty(reason) }) : null,
3341
+ /* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
2609
3342
  "mint one: open ",
2610
3343
  webUrl,
2611
3344
  " logged in, browser console:"
2612
3345
  ] }),
2613
- /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: ` await window.Clerk.session.getToken({template:'${jwtTemplate}'})` }),
2614
- /* @__PURE__ */ jsxs9(Text9, { children: [
2615
- /* @__PURE__ */ jsx10(Text9, { color: "yellow", children: "> " }),
2616
- /* @__PURE__ */ jsx10(Text9, { children: masked }),
2617
- /* @__PURE__ */ jsx10(Text9, { inverse: true, children: " " })
3346
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: ` await window.Clerk.session.getToken({template:'${jwtTemplate}'})` }),
3347
+ /* @__PURE__ */ jsxs10(Text10, { children: [
3348
+ /* @__PURE__ */ jsx11(Text10, { color: "yellow", children: "> " }),
3349
+ /* @__PURE__ */ jsx11(Text10, { children: masked }),
3350
+ /* @__PURE__ */ jsx11(Text10, { inverse: true, children: " " })
2618
3351
  ] }),
2619
- /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: "paste, then Enter \xB7 Ctrl+C quits" })
3352
+ /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "paste, then Enter \xB7 Ctrl+C quits" })
2620
3353
  ] });
2621
3354
  }
2622
3355
 
2623
3356
  // src/app.tsx
2624
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
3357
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2625
3358
  var ACTIVITY_SETTLE_MS2 = 4e3;
2626
3359
  function cleanInput(input) {
2627
3360
  return input.replace(/\r\n|[\r\n]/g, " ").replace(/[\x00-\x1f\x7f]/g, "");
@@ -2668,6 +3401,22 @@ function App({ store: store2 }) {
2668
3401
  } else if (input === "a") store2.markAllNotificationsRead();
2669
3402
  return;
2670
3403
  }
3404
+ if (s.mode.startsWith("attention")) {
3405
+ if (s.mode !== "attention") {
3406
+ if (key.escape) store2.closeAttention();
3407
+ else if (key.return) store2.submitAttentionInput();
3408
+ else if (key.backspace || key.delete) store2.setAttentionDraft(s.attentionDraft.slice(0, -1));
3409
+ else if (input) store2.setAttentionDraft(s.attentionDraft + cleanInput(input));
3410
+ return;
3411
+ }
3412
+ if (key.escape) store2.closeAttention();
3413
+ else if (input === "j" || key.downArrow) store2.moveAttentionSelection(1);
3414
+ else if (input === "k" || key.upArrow) store2.moveAttentionSelection(-1);
3415
+ else if (input === "r") store2.beginAttentionRaise();
3416
+ else if (input === "x") store2.beginAttentionAck(false);
3417
+ else if (input === "X") store2.beginAttentionAck(true);
3418
+ return;
3419
+ }
2671
3420
  if (s.mode === "switcher") {
2672
3421
  const filtered = filterChannels(s.channels, s.switcherFilter);
2673
3422
  const sorted = sortChannelsByPriority(filtered, { attention: s.attention, unread: s.unread });
@@ -2691,6 +3440,7 @@ function App({ store: store2 }) {
2691
3440
  exit();
2692
3441
  } else if (input === "c") store2.setMode("switcher");
2693
3442
  else if (input === "a") store2.setMode("notifications");
3443
+ else if (input === "t") store2.openAttention();
2694
3444
  else if (input === "i" || key.return) store2.setMode("insert");
2695
3445
  else if (input === "r") store2.reconnectNow();
2696
3446
  else if (input === "n") store2.jumpNextActive();
@@ -2708,8 +3458,8 @@ function App({ store: store2 }) {
2708
3458
  const activityLive = activityRows.some((r) => r.status === "pending" || r.status === "in_progress") || activityPlan.some((e) => e.status !== "completed");
2709
3459
  const activityWithinSettle = state.activityIdleSince !== null && Date.now() - state.activityIdleSince < ACTIVITY_SETTLE_MS2;
2710
3460
  const activityVisible = state.activityPinned || activityLive || activityWithinSettle;
2711
- return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
2712
- /* @__PURE__ */ jsx11(
3461
+ return /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
3462
+ /* @__PURE__ */ jsx12(
2713
3463
  Header,
2714
3464
  {
2715
3465
  channelName: activeChannel?.name ?? "(connecting\u2026)",
@@ -2718,9 +3468,9 @@ function App({ store: store2 }) {
2718
3468
  presence: state.presence
2719
3469
  }
2720
3470
  ),
2721
- /* @__PURE__ */ jsx11(MessageList, { messages: state.messages }),
2722
- /* @__PURE__ */ jsx11(ActivitySurface, { toolCalls: activityRows, plan: activityPlan, visible: activityVisible, pinned: state.activityPinned }),
2723
- state.mode === "switcher" ? /* @__PURE__ */ jsx11(
3471
+ /* @__PURE__ */ jsx12(MessageList, { messages: state.messages }),
3472
+ /* @__PURE__ */ jsx12(ActivitySurface, { toolCalls: activityRows, plan: activityPlan, visible: activityVisible, pinned: state.activityPinned }),
3473
+ state.mode === "switcher" ? /* @__PURE__ */ jsx12(
2724
3474
  ChannelSwitcher,
2725
3475
  {
2726
3476
  channels: state.channels,
@@ -2730,14 +3480,25 @@ function App({ store: store2 }) {
2730
3480
  unread: state.unread
2731
3481
  }
2732
3482
  ) : null,
2733
- state.mode === "notifications" ? /* @__PURE__ */ jsx11(
3483
+ state.mode === "notifications" ? /* @__PURE__ */ jsx12(
2734
3484
  NotificationsPanel,
2735
3485
  {
2736
3486
  notifications: state.notifications,
2737
3487
  notifSelected: state.notifSelected
2738
3488
  }
2739
3489
  ) : null,
2740
- state.mode === "token" ? /* @__PURE__ */ jsx11(
3490
+ state.mode.startsWith("attention") && state.activeChannelId ? /* @__PURE__ */ jsx12(
3491
+ AttentionPanel,
3492
+ {
3493
+ channelId: state.activeChannelId,
3494
+ items: state.channelAttention,
3495
+ selected: state.attentionSelected,
3496
+ mode: state.mode,
3497
+ draft: state.attentionDraft,
3498
+ busy: state.attentionBusy
3499
+ }
3500
+ ) : null,
3501
+ state.mode === "token" ? /* @__PURE__ */ jsx12(
2741
3502
  TokenPrompt,
2742
3503
  {
2743
3504
  value: state.tokenPrompt,
@@ -2746,9 +3507,9 @@ function App({ store: store2 }) {
2746
3507
  jwtTemplate: store2.instance.clerkJwtTemplate
2747
3508
  }
2748
3509
  ) : null,
2749
- /* @__PURE__ */ jsx11(ActivityLine, { activities: state.activities }),
2750
- /* @__PURE__ */ jsx11(Composer, { draft: state.draft, focused: state.mode === "insert" }),
2751
- /* @__PURE__ */ jsx11(
3510
+ /* @__PURE__ */ jsx12(ActivityLine, { activities: state.activities }),
3511
+ /* @__PURE__ */ jsx12(Composer, { draft: state.draft, focused: state.mode === "insert" }),
3512
+ /* @__PURE__ */ jsx12(
2752
3513
  StatusBar,
2753
3514
  {
2754
3515
  mode: state.mode,
@@ -2764,7 +3525,7 @@ function App({ store: store2 }) {
2764
3525
  error: state.error
2765
3526
  }
2766
3527
  ),
2767
- !interactive ? /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "(non-interactive terminal: keys disabled \u2014 tail is read-only here)" }) : null
3528
+ !interactive ? /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "(non-interactive terminal: keys disabled \u2014 tail is read-only here)" }) : null
2768
3529
  ] });
2769
3530
  }
2770
3531
 
@@ -2847,8 +3608,1144 @@ unknown profile command "${args2.sub}".`);
2847
3608
  }
2848
3609
  }
2849
3610
 
3611
+ // src/api/bridgeProcess.ts
3612
+ import { spawn as spawn2 } from "child_process";
3613
+ var DEFAULT_BRIDGE_CMD = "npx -y @snowyroad/braid";
3614
+ var ENROLL_TIMEOUT_MS = 12e4;
3615
+ var STDERR_TAIL_CHARS = 600;
3616
+ function resolveBridgeCmd(flag, env = process.env) {
3617
+ return flag ?? env["ARP_TUI_BRIDGE_CMD"] ?? DEFAULT_BRIDGE_CMD;
3618
+ }
3619
+ function splitCmd(cmd) {
3620
+ return cmd.trim().split(/\s+/).filter(Boolean);
3621
+ }
3622
+ function enrollArgv(bridgeCmd) {
3623
+ return [...splitCmd(bridgeCmd), "enroll", "--invite-stdin", "--json"];
3624
+ }
3625
+ function startArgv(bridgeCmd, name, provider) {
3626
+ return [...splitCmd(bridgeCmd), "start", name, ...provider ? ["--provider", provider] : []];
3627
+ }
3628
+ function enrollAgent(opts) {
3629
+ const [cmd, ...argv] = enrollArgv(opts.bridgeCmd);
3630
+ if (!cmd) {
3631
+ return Promise.resolve({ ok: false, exitCode: null, message: "empty bridge command", stderrTail: "" });
3632
+ }
3633
+ return new Promise((resolve) => {
3634
+ const child = spawn2(cmd, argv, { stdio: ["pipe", "pipe", "pipe"] });
3635
+ let stdout = "";
3636
+ let stderr = "";
3637
+ let settled = false;
3638
+ const timeoutMs = opts.timeoutMs ?? ENROLL_TIMEOUT_MS;
3639
+ const finish = (r) => {
3640
+ if (settled) return;
3641
+ settled = true;
3642
+ clearTimeout(timer);
3643
+ resolve(r);
3644
+ };
3645
+ const timer = setTimeout(() => {
3646
+ finish({
3647
+ ok: false,
3648
+ exitCode: null,
3649
+ message: `bridge enrollment timed out after ${Math.round(timeoutMs / 1e3)}s`,
3650
+ stderrTail: tail(stderr)
3651
+ });
3652
+ child.kill("SIGKILL");
3653
+ }, timeoutMs);
3654
+ child.on("error", (err) => {
3655
+ finish({
3656
+ ok: false,
3657
+ exitCode: null,
3658
+ message: `could not run the bridge (${cmd}): ${sanitizeForTty(err.message)}`,
3659
+ stderrTail: ""
3660
+ });
3661
+ });
3662
+ child.stdout.setEncoding("utf8");
3663
+ child.stdout.on("data", (d) => stdout += d);
3664
+ child.stderr.setEncoding("utf8");
3665
+ child.stderr.on("data", (d) => stderr += d);
3666
+ child.stdin.on("error", () => {
3667
+ });
3668
+ child.stdin.write(opts.connectBlob);
3669
+ child.stdin.end();
3670
+ child.on("close", (code) => {
3671
+ if (code !== 0) {
3672
+ finish({
3673
+ ok: false,
3674
+ exitCode: code,
3675
+ message: `bridge enroll exited ${code ?? "on a signal"}`,
3676
+ stderrTail: tail(stderr)
3677
+ });
3678
+ return;
3679
+ }
3680
+ const receipt = parseReceipt(stdout);
3681
+ if (!receipt) {
3682
+ finish({
3683
+ ok: false,
3684
+ exitCode: 0,
3685
+ message: "the bridge exited 0 but printed no parseable enrollment receipt",
3686
+ stderrTail: tail(stderr)
3687
+ });
3688
+ return;
3689
+ }
3690
+ finish({ ok: true, receipt });
3691
+ });
3692
+ });
3693
+ }
3694
+ function tail(stderr) {
3695
+ return sanitizeForTty(stderr).trim().slice(-STDERR_TAIL_CHARS);
3696
+ }
3697
+ function parseReceipt(stdout) {
3698
+ const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
3699
+ for (let i = lines.length - 1; i >= 0; i--) {
3700
+ const line = lines[i];
3701
+ if (!line.startsWith("{")) continue;
3702
+ try {
3703
+ const data = JSON.parse(line);
3704
+ if (data && typeof data === "object" && data["ok"] === true && typeof data["agentName"] === "string" && typeof data["agentUuid"] === "string") {
3705
+ return {
3706
+ ok: true,
3707
+ agentName: data["agentName"],
3708
+ agentUuid: data["agentUuid"],
3709
+ relayUrl: typeof data["relayUrl"] === "string" ? data["relayUrl"] : "",
3710
+ keystoreFile: typeof data["keystoreFile"] === "string" ? data["keystoreFile"] : ""
3711
+ };
3712
+ }
3713
+ } catch {
3714
+ }
3715
+ }
3716
+ return null;
3717
+ }
3718
+ function startAgentProcess(opts) {
3719
+ const [cmd, ...argv] = startArgv(opts.bridgeCmd, opts.name, opts.provider);
3720
+ if (!cmd) return Promise.resolve(1);
3721
+ return new Promise((resolve) => {
3722
+ const child = spawn2(cmd, argv, { stdio: "inherit" });
3723
+ child.on("error", () => resolve(1));
3724
+ child.on("close", (code) => resolve(code ?? 1));
3725
+ });
3726
+ }
3727
+
3728
+ // src/commands/agent.ts
3729
+ import readline from "readline/promises";
3730
+ var AGENT_USAGE = `usage: braid agent <add|status|reissue|revoke|remove> <name> [options]
3731
+
3732
+ add <name> create a personal agent on Braid, enroll it on this machine
3733
+ via the local bridge, and start it (Ctrl-C stops it)
3734
+ status <name> detail card: presence, credential, health, service mode
3735
+ reissue <name> mint a fresh credential and re-enroll here \u2014 recovers an
3736
+ agent whose credential is broken or revoked
3737
+ revoke <name> revoke the agent's credential \u2014 it cannot connect until reissued
3738
+ remove <name> permanently delete the agent from Braid
3739
+
3740
+ --provider <p> agent CLI the bridge runs (claude-code, codex, gemini, \u2026)
3741
+ --no-start enroll only \u2014 prints the exact start command instead
3742
+ --bridge-cmd <cmd> bridge command to spawn (default: npx -y arp-bridge@latest;
3743
+ also ARP_TUI_BRIDGE_CMD env)
3744
+ --yes skip the confirmation prompt (required with --json on
3745
+ revoke/remove)
3746
+ --json machine-readable result on stdout`;
3747
+ async function promptOnTty(question) {
3748
+ if (!process.stdin.isTTY || !process.stderr.isTTY) return null;
3749
+ const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
3750
+ try {
3751
+ return await rl.question(question);
3752
+ } finally {
3753
+ rl.close();
3754
+ }
3755
+ }
3756
+ async function ensureSignedIn(deps) {
3757
+ const existing = deps.resolveCreds();
3758
+ if (existing?.token) return existing;
3759
+ deps.err("you're not signed in yet \u2014 opening your browser to sign in to Braid\u2026");
3760
+ return deps.runLogin();
3761
+ }
3762
+ function oneShotTokenSupplier(instance2, initial, refresh = refreshGrant) {
3763
+ let current = initial;
3764
+ return async () => {
3765
+ const refreshable = !!current.refreshToken && boundToFapi(current.clerkFapiUrl, instance2.clerkFapiUrl);
3766
+ if (refreshable && needsRefresh(current.expiresAt)) {
3767
+ const consumed = current.refreshToken;
3768
+ try {
3769
+ const tokens = await refresh({
3770
+ endpoints: oauthEndpoints(instance2.clerkFapiUrl),
3771
+ clientId: requireOAuthClientId(instance2),
3772
+ refreshToken: consumed
3773
+ });
3774
+ const fields = {
3775
+ token: tokens.accessToken,
3776
+ tokenType: "oauth",
3777
+ // a slot holding a refresh token is ALWAYS an oauth slot
3778
+ obtainedAt: (/* @__PURE__ */ new Date()).toISOString(),
3779
+ expiresAt: tokens.expiresAt ?? jwtExpiry(tokens.accessToken),
3780
+ refreshToken: tokens.refreshToken ?? consumed
3781
+ };
3782
+ try {
3783
+ updateCredentials(instance2.relayUrl, (onDisk) => mergeRotation(onDisk, fields, consumed));
3784
+ } catch {
3785
+ }
3786
+ current = { ...current, ...fields };
3787
+ } catch {
3788
+ }
3789
+ }
3790
+ return current.token;
3791
+ };
3792
+ }
3793
+ async function runAgentCli(instance2, args2, overrides = {}) {
3794
+ const deps = {
3795
+ resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
3796
+ runLogin: async () => (await login(instance2)).credentials,
3797
+ enroll: enrollAgent,
3798
+ startAgent: startAgentProcess,
3799
+ env: process.env,
3800
+ out: (line) => console.log(line),
3801
+ err: (line) => console.error(line),
3802
+ prompt: promptOnTty,
3803
+ ...overrides
3804
+ };
3805
+ try {
3806
+ if (args2.cmd === "agents") return await runAgentsList(instance2, args2, deps);
3807
+ switch (args2.sub) {
3808
+ case "add":
3809
+ return await runAgentAdd(instance2, args2, deps);
3810
+ case "status":
3811
+ return await runAgentStatus(instance2, args2, deps);
3812
+ case "reissue":
3813
+ return await runAgentReissue(instance2, args2, deps);
3814
+ case "revoke":
3815
+ return await runAgentRevoke(instance2, args2, deps);
3816
+ case "remove":
3817
+ return await runAgentRemove(instance2, args2, deps);
3818
+ default:
3819
+ deps.err(AGENT_USAGE);
3820
+ if (args2.sub) deps.err(`
3821
+ unknown agent command "${sanitizeLabel(args2.sub)}".`);
3822
+ return 1;
3823
+ }
3824
+ } catch (err) {
3825
+ deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
3826
+ return 1;
3827
+ }
3828
+ }
3829
+ function oneShotRest(instance2, creds) {
3830
+ return new RelayRest({
3831
+ relayUrl: instance2.relayUrl,
3832
+ getToken: oneShotTokenSupplier(instance2, creds)
3833
+ });
3834
+ }
3835
+ function requireName2(args2, deps) {
3836
+ const name = args2.name?.trim();
3837
+ if (!name) {
3838
+ deps.err(`agent ${args2.sub} requires a name \u2014 usage: braid agent ${args2.sub} <name>`);
3839
+ return null;
3840
+ }
3841
+ return name;
3842
+ }
3843
+ async function findMyAgent(rest, name, args2, deps) {
3844
+ const agents = await rest.listMyAgents();
3845
+ const found = agents.find((a) => a.name === name);
3846
+ if (found) return found;
3847
+ const msg = `no agent named "${sanitizeLabel(name)}"`;
3848
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
3849
+ const known = agents.map((a) => sanitizeLabel(a.name ?? "")).filter(Boolean);
3850
+ deps.err(
3851
+ `\u2717 ${msg}${known.length > 0 ? ` \u2014 your agents: ${known.join(", ")}` : " \u2014 you have no agents yet (run `braid agent add <name>`)"}`
3852
+ );
3853
+ return null;
3854
+ }
3855
+ async function confirmDestructive(args2, deps, name, actionLine) {
3856
+ if (args2.yes) return true;
3857
+ if (args2.json) {
3858
+ deps.out(JSON.stringify({ ok: false, error: "--json runs non-interactively \u2014 add --yes to confirm" }));
3859
+ deps.err(`\u2717 refusing: --json runs non-interactively \u2014 add --yes to confirm`);
3860
+ return false;
3861
+ }
3862
+ const typed = await deps.prompt(`${actionLine}
3863
+ type the agent name to confirm: `);
3864
+ if (typed === null) {
3865
+ deps.err("\u2717 refusing: no terminal to confirm on \u2014 re-run with --yes");
3866
+ return false;
3867
+ }
3868
+ if (typed.trim() !== name) {
3869
+ deps.err("\u2717 confirmation did not match \u2014 nothing was changed");
3870
+ return false;
3871
+ }
3872
+ return true;
3873
+ }
3874
+ async function enrollAndMaybeStart(args2, deps, opts) {
3875
+ const result = await deps.enroll({ bridgeCmd: opts.bridgeCmd, connectBlob: opts.connectBlob });
3876
+ if (!result.ok) {
3877
+ const msg = sanitizeForTty(result.message);
3878
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
3879
+ deps.err(`\u2717 enrollment failed: ${msg}`);
3880
+ if (result.stderrTail) deps.err(result.stderrTail);
3881
+ return 1;
3882
+ }
3883
+ const agentName = result.receipt.agentName;
3884
+ const started = !args2.noStart;
3885
+ if (args2.json) {
3886
+ deps.out(
3887
+ JSON.stringify({
3888
+ ok: true,
3889
+ agentName: sanitizeForTty(agentName),
3890
+ agentUuid: sanitizeForTty(result.receipt.agentUuid),
3891
+ enrolled: true,
3892
+ started
3893
+ })
3894
+ );
3895
+ }
3896
+ deps.err(opts.successLine(sanitizeLabel(agentName)));
3897
+ if (!started) {
3898
+ deps.err(` start it with: ${sanitizeForTty(startArgv(opts.bridgeCmd, agentName, args2.provider).join(" "))}`);
3899
+ return 0;
3900
+ }
3901
+ deps.err(`\u2192 starting ${sanitizeLabel(agentName)} now \u2014 press Ctrl-C to stop it`);
3902
+ return deps.startAgent({ bridgeCmd: opts.bridgeCmd, name: agentName, provider: args2.provider });
3903
+ }
3904
+ async function runAgentAdd(instance2, args2, deps) {
3905
+ const name = requireName2(args2, deps);
3906
+ if (!name) return 1;
3907
+ const bridgeCmd = resolveBridgeCmd(args2.bridgeCmd, deps.env);
3908
+ const creds = await ensureSignedIn(deps);
3909
+ const rest = oneShotRest(instance2, creds);
3910
+ let invite;
3911
+ try {
3912
+ invite = await rest.createPersonalAgent(name);
3913
+ } catch (err) {
3914
+ const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
3915
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
3916
+ deps.err(`\u2717 Braid could not create "${sanitizeLabel(name)}": ${msg}`);
3917
+ return 1;
3918
+ }
3919
+ deps.err(`\u2713 ${sanitizeLabel(name)} created on Braid \u2014 enrolling on this machine\u2026`);
3920
+ return enrollAndMaybeStart(args2, deps, {
3921
+ bridgeCmd,
3922
+ connectBlob: invite.connectBlob,
3923
+ successLine: (n) => `\u2714 ${n} is enrolled with Braid`
3924
+ });
3925
+ }
3926
+ async function runAgentStatus(instance2, args2, deps) {
3927
+ const name = requireName2(args2, deps);
3928
+ if (!name) return 1;
3929
+ const creds = await ensureSignedIn(deps);
3930
+ const agent = await findMyAgent(oneShotRest(instance2, creds), name, args2, deps);
3931
+ if (!agent) return 1;
3932
+ if (args2.json) {
3933
+ deps.out(JSON.stringify(agent, null, 2));
3934
+ return 0;
3935
+ }
3936
+ const row = (label, value) => deps.out(`${(label + ":").padEnd(14)}${value}`);
3937
+ row("agent", sanitizeLabel(agent.name ?? "", 64));
3938
+ row("status", agent.status === "online" ? "online" : "offline");
3939
+ const cred = sanitizeLabel(agent.credentialState ?? "none", 32);
3940
+ row(
3941
+ "credential",
3942
+ agent.credentialLastUsedAt ? `${cred} (last used ${sanitizeLabel(agent.credentialLastUsedAt, 40)})` : cred
3943
+ );
3944
+ row("health", sanitizeLabel(agent.healthState ?? "unknown", 32));
3945
+ if (agent.healthDetail) row("detail", sanitizeLabel(agent.healthDetail, 200));
3946
+ if (agent.healthFixHint) row("fix", sanitizeLabel(agent.healthFixHint, 200));
3947
+ row(
3948
+ "service mode",
3949
+ typeof agent.serviceMode === "boolean" ? agent.serviceMode ? "on" : "off" : sanitizeLabel(agent.serviceMode ?? "off", 20)
3950
+ );
3951
+ if (agent.runtimeVersion) row("runtime", sanitizeLabel(agent.runtimeVersion, 40));
3952
+ if (agent.daemonPosture) row("daemon", sanitizeLabel(agent.daemonPosture, 40));
3953
+ return 0;
3954
+ }
3955
+ async function runAgentReissue(instance2, args2, deps) {
3956
+ const name = requireName2(args2, deps);
3957
+ if (!name) return 1;
3958
+ const bridgeCmd = resolveBridgeCmd(args2.bridgeCmd, deps.env);
3959
+ const creds = await ensureSignedIn(deps);
3960
+ const rest = oneShotRest(instance2, creds);
3961
+ const agent = await findMyAgent(rest, name, args2, deps);
3962
+ if (!agent) return 1;
3963
+ let invite;
3964
+ try {
3965
+ invite = await rest.reissueAgent(agent.id);
3966
+ } catch (err) {
3967
+ const msg = err instanceof RelayError && err.status === 429 ? "reissue is rate-limited \u2014 wait a minute and try again" : sanitizeForTty(err instanceof Error ? err.message : String(err));
3968
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
3969
+ deps.err(`\u2717 Braid could not reissue "${sanitizeLabel(name)}": ${msg}`);
3970
+ return 1;
3971
+ }
3972
+ deps.err(`\u2713 fresh credential minted \u2014 re-enrolling ${sanitizeLabel(name)} on this machine\u2026`);
3973
+ return enrollAndMaybeStart(args2, deps, {
3974
+ bridgeCmd,
3975
+ connectBlob: invite.connectBlob,
3976
+ successLine: (n) => `\u2714 ${n} reconnected to Braid`
3977
+ });
3978
+ }
3979
+ async function runAgentRevoke(instance2, args2, deps) {
3980
+ const name = requireName2(args2, deps);
3981
+ if (!name) return 1;
3982
+ const creds = await ensureSignedIn(deps);
3983
+ const rest = oneShotRest(instance2, creds);
3984
+ const agent = await findMyAgent(rest, name, args2, deps);
3985
+ if (!agent) return 1;
3986
+ const confirmed = await confirmDestructive(
3987
+ args2,
3988
+ deps,
3989
+ name,
3990
+ `this revokes "${sanitizeLabel(name)}"'s credential and disconnects it from Braid.`
3991
+ );
3992
+ if (!confirmed) return 1;
3993
+ let credential;
3994
+ try {
3995
+ credential = await rest.revokeAgentCredential(agent.id);
3996
+ } catch (err) {
3997
+ const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
3998
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
3999
+ deps.err(`\u2717 Braid could not revoke "${sanitizeLabel(name)}": ${msg}`);
4000
+ return 1;
4001
+ }
4002
+ if (args2.json) {
4003
+ deps.out(
4004
+ JSON.stringify({
4005
+ ok: true,
4006
+ agentName: sanitizeForTty(name),
4007
+ credentialState: sanitizeForTty(credential.state ?? "revoked_owner")
4008
+ })
4009
+ );
4010
+ }
4011
+ deps.err(
4012
+ `\u2714 credential revoked \u2014 ${sanitizeLabel(name)} can no longer connect to Braid until you run \`braid agent reissue ${sanitizeLabel(name)}\``
4013
+ );
4014
+ return 0;
4015
+ }
4016
+ async function runAgentRemove(instance2, args2, deps) {
4017
+ const name = requireName2(args2, deps);
4018
+ if (!name) return 1;
4019
+ const creds = await ensureSignedIn(deps);
4020
+ const rest = oneShotRest(instance2, creds);
4021
+ const agent = await findMyAgent(rest, name, args2, deps);
4022
+ if (!agent) return 1;
4023
+ const confirmed = await confirmDestructive(
4024
+ args2,
4025
+ deps,
4026
+ name,
4027
+ `this permanently deletes agent "${sanitizeLabel(name)}" from Braid \u2014 identity, credential, and channel memberships.`
4028
+ );
4029
+ if (!confirmed) return 1;
4030
+ try {
4031
+ await rest.deleteAgent(agent.id);
4032
+ } catch (err) {
4033
+ const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
4034
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4035
+ deps.err(`\u2717 Braid could not remove "${sanitizeLabel(name)}": ${msg}`);
4036
+ return 1;
4037
+ }
4038
+ if (args2.json) deps.out(JSON.stringify({ ok: true, agentName: sanitizeForTty(name), removed: true }));
4039
+ deps.err(
4040
+ `\u2714 ${sanitizeLabel(name)} removed from Braid \u2014 its identity, credential, and channel memberships are deleted; messages it already posted stay in channel history`
4041
+ );
4042
+ return 0;
4043
+ }
4044
+ async function runAgentsList(instance2, args2, deps) {
4045
+ const creds = await ensureSignedIn(deps);
4046
+ const rest = oneShotRest(instance2, creds);
4047
+ let agents;
4048
+ try {
4049
+ agents = await rest.listMyAgents();
4050
+ } catch (err) {
4051
+ deps.err(
4052
+ `\u2717 Braid could not list your agents: ${sanitizeForTty(err instanceof Error ? err.message : String(err))}`
4053
+ );
4054
+ return 1;
4055
+ }
4056
+ if (args2.json) {
4057
+ deps.out(JSON.stringify(agents, null, 2));
4058
+ return 0;
4059
+ }
4060
+ if (agents.length === 0) {
4061
+ deps.out("(no agents yet \u2014 run `braid agent add <name>` to create one)");
4062
+ return 0;
4063
+ }
4064
+ deps.out(`${"NAME".padEnd(24)} ${"STATUS".padEnd(8)} CREDENTIAL`);
4065
+ for (const a of agents) {
4066
+ const status = a.status === "online" ? "online" : "offline";
4067
+ deps.out(
4068
+ `${sanitizeLabel(a.name ?? "", 24).padEnd(24)} ${status.padEnd(8)} ${sanitizeLabel(a.credentialState ?? "-", 20)}`
4069
+ );
4070
+ }
4071
+ return 0;
4072
+ }
4073
+
4074
+ // src/commands/channel.ts
4075
+ var CHANNEL_USAGE = `usage: braid channels [--all] [--json]
4076
+ braid channel <create|archive|unarchive|members|member> \u2026
4077
+
4078
+ channels list your channels (--all includes archived)
4079
+ create <name> create a channel on Braid (you become its owner)
4080
+ archive <name-or-id> archive a channel \u2014 it leaves everyone's sidebar
4081
+ (reversible; y/N confirm, --yes skips)
4082
+ unarchive <name-or-id> restore an archived channel
4083
+ members <name-or-id> list a channel's members (humans and agents)
4084
+ member add <channel> <agent>
4085
+ add one of your agents to a channel
4086
+ member remove <channel> <agent>
4087
+ remove an agent from a channel (y/N confirm, --yes)
4088
+
4089
+ --all include archived channels when listing
4090
+ --yes skip the confirmation prompt (required with --json on
4091
+ archive/member remove)
4092
+ --json machine-readable result on stdout
4093
+
4094
+ channels are archive-only \u2014 there is no delete. archive to retire one.`;
4095
+ async function runChannelCli(instance2, args2, overrides = {}) {
4096
+ const deps = {
4097
+ resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
4098
+ runLogin: async () => (await login(instance2)).credentials,
4099
+ out: (line) => console.log(line),
4100
+ err: (line) => console.error(line),
4101
+ prompt: promptOnTty,
4102
+ ...overrides
4103
+ };
4104
+ try {
4105
+ if (args2.cmd === "channels") return await runChannelsList(instance2, args2, deps);
4106
+ switch (args2.sub) {
4107
+ case "create":
4108
+ return await runChannelCreate(instance2, args2, deps);
4109
+ case "archive":
4110
+ return await runChannelArchive(instance2, args2, deps);
4111
+ case "unarchive":
4112
+ return await runChannelUnarchive(instance2, args2, deps);
4113
+ case "members":
4114
+ return await runChannelMembers(instance2, args2, deps);
4115
+ case "member":
4116
+ return await runChannelMember(instance2, args2, deps);
4117
+ default:
4118
+ deps.err(CHANNEL_USAGE);
4119
+ if (args2.sub) deps.err(`
4120
+ unknown channel command "${sanitizeLabel(args2.sub)}".`);
4121
+ return 1;
4122
+ }
4123
+ } catch (err) {
4124
+ deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
4125
+ return 1;
4126
+ }
4127
+ }
4128
+ async function fetchChannels(rest, scope) {
4129
+ const rows = [];
4130
+ if (scope !== "archived") {
4131
+ rows.push(...(await rest.listChannels()).map((c) => ({ ...c, archived: false })));
4132
+ }
4133
+ if (scope !== "active") {
4134
+ rows.push(...(await rest.listArchivedChannels()).map((c) => ({ ...c, archived: true })));
4135
+ }
4136
+ return rows;
4137
+ }
4138
+ async function resolveChannel(rest, ref, scope, args2, deps) {
4139
+ const rows = await fetchChannels(rest, scope);
4140
+ const byId = rows.find((c) => c.id === ref);
4141
+ if (byId) return byId;
4142
+ const byName = rows.filter((c) => sanitizeForTty(c.name ?? "") === ref);
4143
+ if (byName.length === 1) return byName[0];
4144
+ if (byName.length > 1) {
4145
+ const msg2 = `channel name "${sanitizeLabel(ref)}" is ambiguous \u2014 use the id`;
4146
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg2 }));
4147
+ deps.err(`\u2717 ${msg2}:`);
4148
+ for (const c of byName) {
4149
+ deps.err(
4150
+ ` ${sanitizeLabel(c.name ?? "", 24).padEnd(24)} ${sanitizeLabel(c.id ?? "", 40)}${c.archived ? " (archived)" : ""}`
4151
+ );
4152
+ }
4153
+ return null;
4154
+ }
4155
+ const noun = scope === "archived" ? "archived channel" : "channel";
4156
+ const msg = `no ${noun} named "${sanitizeLabel(ref)}"`;
4157
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4158
+ const known = rows.map((c) => sanitizeLabel(c.name ?? "")).filter(Boolean);
4159
+ deps.err(
4160
+ `\u2717 ${msg}${known.length > 0 ? ` \u2014 your ${scope === "archived" ? "archived channels" : "channels"}: ${known.join(", ")}` : scope === "archived" ? " \u2014 you have no archived channels" : " \u2014 you have no channels yet (run `braid channel create <name>`)"}`
4161
+ );
4162
+ return null;
4163
+ }
4164
+ async function confirmSimple(args2, deps, actionLine) {
4165
+ if (args2.yes) return true;
4166
+ if (args2.json) {
4167
+ deps.out(JSON.stringify({ ok: false, error: "--json runs non-interactively \u2014 add --yes to confirm" }));
4168
+ deps.err("\u2717 refusing: --json runs non-interactively \u2014 add --yes to confirm");
4169
+ return false;
4170
+ }
4171
+ const typed = await deps.prompt(`${actionLine} [y/N] `);
4172
+ if (typed === null) {
4173
+ deps.err("\u2717 refusing: no terminal to confirm on \u2014 re-run with --yes");
4174
+ return false;
4175
+ }
4176
+ if (!/^y(es)?$/i.test(typed.trim())) {
4177
+ deps.err("\u2717 not confirmed \u2014 nothing was changed");
4178
+ return false;
4179
+ }
4180
+ return true;
4181
+ }
4182
+ function shortId(id) {
4183
+ return sanitizeLabel(id ?? "", 40).slice(0, 8);
4184
+ }
4185
+ async function runChannelsList(instance2, args2, deps) {
4186
+ const creds = await ensureSignedIn(deps);
4187
+ const rest = oneShotRest(instance2, creds);
4188
+ const rows = await fetchChannels(rest, args2.all ? "all" : "active");
4189
+ if (args2.json) {
4190
+ deps.out(JSON.stringify(rows, null, 2));
4191
+ return 0;
4192
+ }
4193
+ if (rows.length === 0) {
4194
+ deps.out("(no channels yet \u2014 run `braid channel create <name>` to make one)");
4195
+ return 0;
4196
+ }
4197
+ deps.out(`${"NAME".padEnd(24)} ${"ID".padEnd(10)}${args2.all ? " STATE" : ""}`);
4198
+ for (const c of rows) {
4199
+ deps.out(
4200
+ `${sanitizeLabel(c.name ?? "", 24).padEnd(24)} ${shortId(c.id).padEnd(10)}${args2.all ? ` ${c.archived ? "archived" : "active"}` : ""}`
4201
+ );
4202
+ }
4203
+ return 0;
4204
+ }
4205
+ async function runChannelCreate(instance2, args2, deps) {
4206
+ const name = args2.rest[0]?.trim();
4207
+ if (!name) {
4208
+ deps.err("channel create requires a name \u2014 usage: braid channel create <name>");
4209
+ return 1;
4210
+ }
4211
+ const creds = await ensureSignedIn(deps);
4212
+ const rest = oneShotRest(instance2, creds);
4213
+ let ch;
4214
+ try {
4215
+ ch = await rest.createChannel(name);
4216
+ } catch (err) {
4217
+ const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
4218
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4219
+ deps.err(`\u2717 Braid could not create channel "${sanitizeLabel(name)}": ${msg}`);
4220
+ return 1;
4221
+ }
4222
+ if (args2.json) {
4223
+ deps.out(JSON.stringify({ ok: true, id: sanitizeForTty(ch.id ?? ""), name: sanitizeForTty(ch.name ?? "") }));
4224
+ }
4225
+ deps.err(
4226
+ `\u2714 #${sanitizeLabel(ch.name ?? name)} created on Braid (id ${shortId(ch.id)}) \u2014 add an agent with \`braid channel member add ${sanitizeLabel(ch.name ?? name)} <agent>\``
4227
+ );
4228
+ return 0;
4229
+ }
4230
+ async function runChannelArchive(instance2, args2, deps) {
4231
+ const ref = args2.rest[0]?.trim();
4232
+ if (!ref) {
4233
+ deps.err("channel archive requires a name \u2014 usage: braid channel archive <name-or-id>");
4234
+ return 1;
4235
+ }
4236
+ const creds = await ensureSignedIn(deps);
4237
+ const rest = oneShotRest(instance2, creds);
4238
+ const ch = await resolveChannel(rest, ref, "active", args2, deps);
4239
+ if (!ch) return 1;
4240
+ const name = sanitizeLabel(ch.name ?? "");
4241
+ const confirmed = await confirmSimple(
4242
+ args2,
4243
+ deps,
4244
+ `archive #${name}? it leaves everyone's sidebar (\`braid channel unarchive ${name}\` restores it).`
4245
+ );
4246
+ if (!confirmed) return 1;
4247
+ try {
4248
+ await rest.archiveChannel(ch.id);
4249
+ } catch (err) {
4250
+ const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
4251
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4252
+ deps.err(`\u2717 Braid could not archive #${name}: ${msg}`);
4253
+ return 1;
4254
+ }
4255
+ if (args2.json) {
4256
+ deps.out(
4257
+ JSON.stringify({ ok: true, id: sanitizeForTty(ch.id), name: sanitizeForTty(ch.name ?? ""), archived: true })
4258
+ );
4259
+ }
4260
+ deps.err(`\u2714 #${name} archived \u2014 bring it back any time with \`braid channel unarchive ${name}\``);
4261
+ return 0;
4262
+ }
4263
+ async function runChannelUnarchive(instance2, args2, deps) {
4264
+ const ref = args2.rest[0]?.trim();
4265
+ if (!ref) {
4266
+ deps.err("channel unarchive requires a name \u2014 usage: braid channel unarchive <name-or-id>");
4267
+ return 1;
4268
+ }
4269
+ const creds = await ensureSignedIn(deps);
4270
+ const rest = oneShotRest(instance2, creds);
4271
+ const ch = await resolveChannel(rest, ref, "archived", args2, deps);
4272
+ if (!ch) return 1;
4273
+ const name = sanitizeLabel(ch.name ?? "");
4274
+ try {
4275
+ await rest.unarchiveChannel(ch.id);
4276
+ } catch (err) {
4277
+ const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
4278
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4279
+ deps.err(`\u2717 Braid could not unarchive #${name}: ${msg}`);
4280
+ return 1;
4281
+ }
4282
+ if (args2.json) {
4283
+ deps.out(
4284
+ JSON.stringify({ ok: true, id: sanitizeForTty(ch.id), name: sanitizeForTty(ch.name ?? ""), archived: false })
4285
+ );
4286
+ }
4287
+ deps.err(`\u2714 #${name} is back \u2014 restored from the archive`);
4288
+ return 0;
4289
+ }
4290
+ async function runChannelMembers(instance2, args2, deps) {
4291
+ const ref = args2.rest[0]?.trim();
4292
+ if (!ref) {
4293
+ deps.err("channel members requires a channel \u2014 usage: braid channel members <name-or-id>");
4294
+ return 1;
4295
+ }
4296
+ const creds = await ensureSignedIn(deps);
4297
+ const rest = oneShotRest(instance2, creds);
4298
+ const ch = await resolveChannel(rest, ref, "all", args2, deps);
4299
+ if (!ch) return 1;
4300
+ if (args2.json) {
4301
+ deps.out(JSON.stringify(ch.members, null, 2));
4302
+ return 0;
4303
+ }
4304
+ if (ch.members.length === 0) {
4305
+ deps.out(`(no members in #${sanitizeLabel(ch.name ?? "")})`);
4306
+ return 0;
4307
+ }
4308
+ deps.out(`${"NAME".padEnd(24)} ${"TYPE".padEnd(6)} ${"ROLE".padEnd(7)} STATUS`);
4309
+ for (const m of ch.members) {
4310
+ deps.out(
4311
+ `${sanitizeLabel(m.name, 24).padEnd(24)} ${m.type.padEnd(6)} ${sanitizeLabel(m.role, 7).padEnd(7)} ${m.status}`
4312
+ );
4313
+ }
4314
+ return 0;
4315
+ }
4316
+ async function runChannelMember(instance2, args2, deps) {
4317
+ const [action, channelRef, agentName] = args2.rest.map((s) => s?.trim());
4318
+ const usage = "usage: braid channel member <add|remove> <channel> <agent-name>";
4319
+ if (action !== "add" && action !== "remove" || !channelRef || !agentName) {
4320
+ deps.err(usage);
4321
+ if (action && action !== "add" && action !== "remove") {
4322
+ deps.err(`
4323
+ unknown member action "${sanitizeLabel(action)}".`);
4324
+ }
4325
+ return 1;
4326
+ }
4327
+ const creds = await ensureSignedIn(deps);
4328
+ const rest = oneShotRest(instance2, creds);
4329
+ const ch = await resolveChannel(rest, channelRef, "active", args2, deps);
4330
+ if (!ch) return 1;
4331
+ return action === "add" ? runMemberAdd(rest, ch, agentName, args2, deps) : runMemberRemove(rest, ch, agentName, args2, deps);
4332
+ }
4333
+ async function runMemberAdd(rest, ch, agentName, args2, deps) {
4334
+ const chName = sanitizeLabel(ch.name ?? "");
4335
+ const agent = await findMyAgent(rest, agentName, args2, deps);
4336
+ if (!agent) return 1;
4337
+ try {
4338
+ await rest.addAgentToChannel(ch.id, agent.id);
4339
+ } catch (err) {
4340
+ let msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
4341
+ if (msg.includes("guest_agents_disabled")) {
4342
+ msg = "this organization does not allow guest agents \u2014 enable it in org settings or ask an admin";
4343
+ }
4344
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4345
+ deps.err(`\u2717 Braid could not add "${sanitizeLabel(agentName)}" to #${chName}: ${msg}`);
4346
+ return 1;
4347
+ }
4348
+ if (args2.json) {
4349
+ deps.out(
4350
+ JSON.stringify({
4351
+ ok: true,
4352
+ channelId: sanitizeForTty(ch.id),
4353
+ channel: sanitizeForTty(ch.name ?? ""),
4354
+ agent: sanitizeForTty(agent.name),
4355
+ added: true
4356
+ })
4357
+ );
4358
+ }
4359
+ deps.err(`\u2714 ${sanitizeLabel(agent.name)} joined #${chName} \u2014 it now sees the channel and can post there`);
4360
+ return 0;
4361
+ }
4362
+ async function runMemberRemove(rest, ch, agentName, args2, deps) {
4363
+ const chName = sanitizeLabel(ch.name ?? "");
4364
+ const member = ch.members.find((m) => m.type === "bot" && m.id === agentName);
4365
+ if (!member) {
4366
+ const msg = `"${sanitizeLabel(agentName)}" is not a member of #${chName}`;
4367
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4368
+ const bots = ch.members.filter((m) => m.type === "bot").map((m) => sanitizeLabel(m.name));
4369
+ deps.err(`\u2717 ${msg}${bots.length > 0 ? ` \u2014 its agents: ${bots.join(", ")}` : " \u2014 it has no agents"}`);
4370
+ return 1;
4371
+ }
4372
+ const confirmed = await confirmSimple(
4373
+ args2,
4374
+ deps,
4375
+ `remove ${sanitizeLabel(agentName)} from #${chName}? it stops seeing the channel (member add restores it).`
4376
+ );
4377
+ if (!confirmed) return 1;
4378
+ try {
4379
+ await rest.removeChannelMember(ch.id, member.id);
4380
+ } catch (err) {
4381
+ const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
4382
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4383
+ deps.err(`\u2717 Braid could not remove "${sanitizeLabel(agentName)}" from #${chName}: ${msg}`);
4384
+ return 1;
4385
+ }
4386
+ if (args2.json) {
4387
+ deps.out(
4388
+ JSON.stringify({
4389
+ ok: true,
4390
+ channelId: sanitizeForTty(ch.id),
4391
+ channel: sanitizeForTty(ch.name ?? ""),
4392
+ agent: sanitizeForTty(agentName),
4393
+ removed: true
4394
+ })
4395
+ );
4396
+ }
4397
+ deps.err(
4398
+ `\u2714 ${sanitizeLabel(agentName)} removed from #${chName} \u2014 add it back any time with \`braid channel member add ${chName} ${sanitizeLabel(agentName)}\``
4399
+ );
4400
+ return 0;
4401
+ }
4402
+
4403
+ // src/commands/invite.ts
4404
+ var INVITE_USAGE = `usage: braid invites [<channel>] [--all] [--json]
4405
+ braid invite <agent|human|revoke> \u2026
4406
+
4407
+ invites list your org's channel-less agent invites
4408
+ invites <channel> list a channel's agent invites
4409
+ (active only; --all includes revoked/expired/used)
4410
+ agent <channel> <name> mint an invite for a NEW agent named <name> on the
4411
+ channel \u2014 prints the join command ONCE (org admin)
4412
+ human <channel> <email> invite a human by email: they get a Braid email and
4413
+ land in the channel (org admin; a user from another
4414
+ org joins this channel only, as a guest)
4415
+ revoke <invite-id> revoke an org-wide invite so it cannot be redeemed
4416
+ (y/N confirm, --yes skips)
4417
+ --channel <channel> revoke a CHANNEL invite instead (the id alone
4418
+ targets the org-wide route)
4419
+
4420
+ --all include revoked/expired/exhausted invites when listing
4421
+ --yes skip the confirmation prompt (required with --json on revoke)
4422
+ --json machine-readable result on stdout
4423
+
4424
+ minting and managing invites needs org admin on Braid.`;
4425
+ async function runInviteCli(instance2, args2, overrides = {}) {
4426
+ const deps = {
4427
+ resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
4428
+ runLogin: async () => (await login(instance2)).credentials,
4429
+ out: (line) => console.log(line),
4430
+ err: (line) => console.error(line),
4431
+ prompt: promptOnTty,
4432
+ ...overrides
4433
+ };
4434
+ try {
4435
+ if (args2.cmd === "invites") return await runInvitesList(instance2, args2, deps);
4436
+ switch (args2.sub) {
4437
+ case "agent":
4438
+ return await runInviteAgent(instance2, args2, deps);
4439
+ case "human":
4440
+ return await runInviteHuman(instance2, args2, deps);
4441
+ case "revoke":
4442
+ return await runInviteRevoke(instance2, args2, deps);
4443
+ default:
4444
+ deps.err(INVITE_USAGE);
4445
+ if (args2.sub) deps.err(`
4446
+ unknown invite command "${sanitizeLabel(args2.sub)}".`);
4447
+ return 1;
4448
+ }
4449
+ } catch (err) {
4450
+ deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
4451
+ return 1;
4452
+ }
4453
+ }
4454
+ function inviteErrorMessage(err) {
4455
+ if (err instanceof RelayError) {
4456
+ if (err.status === 403 && err.message === "admin_required") {
4457
+ return "you need to be an org admin on Braid to do this \u2014 ask an admin, or have them grant you the role";
4458
+ }
4459
+ if (err.status === 403 && err.message === "guest_agents_disabled") {
4460
+ return "this organization does not allow guest agents \u2014 enable it in org settings or ask an admin";
4461
+ }
4462
+ if (err.status === 402 && err.message === "seat_limit_reached") {
4463
+ return "the organization is out of member seats \u2014 upgrade the plan to invite more humans";
4464
+ }
4465
+ if (err.status === 402 && err.message === "guest_limit_reached") {
4466
+ return "the organization is at its guest limit \u2014 upgrade the plan to add more guests";
4467
+ }
4468
+ }
4469
+ return sanitizeForTty(err instanceof Error ? err.message : String(err));
4470
+ }
4471
+ async function runInviteAgent(instance2, args2, deps) {
4472
+ const [channelRef, name] = args2.rest.map((s) => s?.trim());
4473
+ if (!channelRef || !name) {
4474
+ deps.err("usage: braid invite agent <channel> <agent-name>");
4475
+ return 1;
4476
+ }
4477
+ const creds = await ensureSignedIn(deps);
4478
+ const rest = oneShotRest(instance2, creds);
4479
+ const ch = await resolveChannel(rest, channelRef, "active", args2, deps);
4480
+ if (!ch) return 1;
4481
+ const chName = sanitizeLabel(ch.name ?? "");
4482
+ let mint;
4483
+ try {
4484
+ mint = await rest.createChannelAgentInvite(ch.id, name);
4485
+ } catch (err) {
4486
+ const msg = inviteErrorMessage(err);
4487
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4488
+ deps.err(`\u2717 Braid could not mint an invite for "${sanitizeLabel(name)}" on #${chName}: ${msg}`);
4489
+ return 1;
4490
+ }
4491
+ if (args2.json) {
4492
+ deps.out(JSON.stringify(mint));
4493
+ return 0;
4494
+ }
4495
+ deps.err(`\u2714 invite minted for agent "${sanitizeLabel(name)}" on #${chName} (invite ${shortId(mint.inviteId)})`);
4496
+ deps.err(" run this on the agent's machine to join:");
4497
+ deps.out(sanitizeForTty(mint.joinCommand));
4498
+ deps.err(
4499
+ `\u26A0 shown once \u2014 Braid keeps only a hash and this terminal will not show it again` + (mint.expiresAt ? ` (expires ${sanitizeLabel(mint.expiresAt)}` : ` (never expires`) + `${mint.oneTime ? ", single use)" : ")"}`
4500
+ );
4501
+ return 0;
4502
+ }
4503
+ function usesCell(inv) {
4504
+ return `${inv.uses}/${inv.maxUses === 0 ? "\u221E" : inv.maxUses}`;
4505
+ }
4506
+ async function runInvitesList(instance2, args2, deps) {
4507
+ const channelRef = args2.rest[0]?.trim();
4508
+ const creds = await ensureSignedIn(deps);
4509
+ const rest = oneShotRest(instance2, creds);
4510
+ let ch = null;
4511
+ let invites;
4512
+ try {
4513
+ if (channelRef) {
4514
+ ch = await resolveChannel(rest, channelRef, "all", args2, deps);
4515
+ if (!ch) return 1;
4516
+ invites = await rest.listChannelAgentInvites(ch.id);
4517
+ } else {
4518
+ invites = await rest.listOrgAgentInvites();
4519
+ }
4520
+ } catch (err) {
4521
+ const msg = inviteErrorMessage(err);
4522
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4523
+ deps.err(`\u2717 Braid could not list invites: ${msg}`);
4524
+ return 1;
4525
+ }
4526
+ if (!args2.all) invites = invites.filter((i) => i.status === "active");
4527
+ if (args2.json) {
4528
+ deps.out(JSON.stringify(invites, null, 2));
4529
+ return 0;
4530
+ }
4531
+ const where = ch ? `#${sanitizeLabel(ch.name ?? "")}` : "your organization (channel-less invites)";
4532
+ if (invites.length === 0) {
4533
+ deps.out(`(no ${args2.all ? "" : "active "}invites for ${where})`);
4534
+ return 0;
4535
+ }
4536
+ deps.err(`invites for ${where}:`);
4537
+ deps.out(
4538
+ `${"ID".padEnd(10)} ${"AGENT".padEnd(24)} ${"CHANNEL".padEnd(16)} ${"CREATED".padEnd(21)} ${"EXPIRES".padEnd(21)} ${"USES".padEnd(5)} STATUS`
4539
+ );
4540
+ for (const inv of invites) {
4541
+ const channelCell = ch ? sanitizeLabel(ch.name ?? "", 16) : "-";
4542
+ deps.out(
4543
+ `${shortId(inv.id).padEnd(10)} ${sanitizeLabel(inv.agentName ?? "", 24).padEnd(24)} ${channelCell.padEnd(16)} ${sanitizeLabel(inv.createdAt ?? "", 21).padEnd(21)} ${sanitizeLabel(inv.expiresAt ?? "never", 21).padEnd(21)} ${usesCell(inv).padEnd(5)} ` + sanitizeLabel(inv.status ?? "", 12)
4544
+ );
4545
+ }
4546
+ return 0;
4547
+ }
4548
+ async function runInviteRevoke(instance2, args2, deps) {
4549
+ const inviteId = args2.rest[0]?.trim();
4550
+ if (!inviteId) {
4551
+ deps.err("invite revoke requires an id \u2014 usage: braid invite revoke <invite-id> [--channel <channel>]");
4552
+ return 1;
4553
+ }
4554
+ const creds = await ensureSignedIn(deps);
4555
+ const rest = oneShotRest(instance2, creds);
4556
+ let ch = null;
4557
+ if (args2.channel) {
4558
+ ch = await resolveChannel(rest, args2.channel.trim(), "all", args2, deps);
4559
+ if (!ch) return 1;
4560
+ }
4561
+ const where = ch ? `#${sanitizeLabel(ch.name ?? "")}` : "your organization";
4562
+ const confirmed = await confirmSimple(
4563
+ args2,
4564
+ deps,
4565
+ `revoke invite ${sanitizeLabel(inviteId, 40)} on ${where}? it can no longer be redeemed (an agent that already joined stays connected).`
4566
+ );
4567
+ if (!confirmed) return 1;
4568
+ try {
4569
+ if (ch) await rest.revokeChannelAgentInvite(ch.id, inviteId);
4570
+ else await rest.revokeOrgAgentInvite(inviteId);
4571
+ } catch (err) {
4572
+ let msg = inviteErrorMessage(err);
4573
+ if (err instanceof RelayError && err.status === 404 && !ch) {
4574
+ msg = `invite not found org-wide \u2014 a channel invite needs --channel <channel>`;
4575
+ }
4576
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4577
+ deps.err(`\u2717 Braid could not revoke invite ${sanitizeLabel(inviteId, 40)}: ${msg}`);
4578
+ return 1;
4579
+ }
4580
+ if (args2.json) {
4581
+ deps.out(
4582
+ JSON.stringify({
4583
+ ok: true,
4584
+ inviteId: sanitizeForTty(inviteId),
4585
+ ...ch ? { channelId: sanitizeForTty(ch.id), channel: sanitizeForTty(ch.name ?? "") } : {},
4586
+ revoked: true
4587
+ })
4588
+ );
4589
+ }
4590
+ deps.err(`\u2714 invite ${sanitizeLabel(inviteId, 40)} revoked \u2014 it can no longer be redeemed`);
4591
+ return 0;
4592
+ }
4593
+ function looksLikeEmail(email) {
4594
+ return /^[^\s@]+@[^\s@]+$/.test(email);
4595
+ }
4596
+ async function runInviteHuman(instance2, args2, deps) {
4597
+ const [channelRef, emailRaw] = args2.rest.map((s) => s?.trim());
4598
+ if (!channelRef || !emailRaw) {
4599
+ deps.err("usage: braid invite human <channel> <email>");
4600
+ return 1;
4601
+ }
4602
+ const email = emailRaw.toLowerCase();
4603
+ if (!looksLikeEmail(email)) {
4604
+ const msg = `"${sanitizeLabel(emailRaw)}" does not look like an email address`;
4605
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4606
+ deps.err(`\u2717 ${msg}`);
4607
+ return 1;
4608
+ }
4609
+ const creds = await ensureSignedIn(deps);
4610
+ const rest = oneShotRest(instance2, creds);
4611
+ const ch = await resolveChannel(rest, channelRef, "active", args2, deps);
4612
+ if (!ch) return 1;
4613
+ const chName = sanitizeLabel(ch.name ?? "");
4614
+ let result;
4615
+ try {
4616
+ result = await rest.inviteHumanToChannel(ch.id, email);
4617
+ } catch (err) {
4618
+ const msg = inviteErrorMessage(err);
4619
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4620
+ deps.err(`\u2717 Braid could not invite ${sanitizeLabel(email)} to #${chName}: ${msg}`);
4621
+ return 1;
4622
+ }
4623
+ if (args2.json) {
4624
+ deps.out(JSON.stringify(result));
4625
+ return 0;
4626
+ }
4627
+ if (result.note) {
4628
+ deps.err(`\u2714 ${sanitizeLabel(email)} \u2014 ${sanitizeLabel(result.note, 80)}`);
4629
+ } else if (result.guest) {
4630
+ deps.err(
4631
+ `\u2714 ${sanitizeLabel(email)} added to #${chName} as a cross-org guest \u2014 they see this channel only, not your org directory`
4632
+ );
4633
+ } else {
4634
+ deps.err(
4635
+ `\u2714 ${sanitizeLabel(email)} invited \u2014 Braid emailed them; they'll land in #${chName} when they accept`
4636
+ );
4637
+ }
4638
+ return 0;
4639
+ }
4640
+
4641
+ // src/commands/org.ts
4642
+ var ORG_USAGE = `usage: braid org [--json]
4643
+ braid org guest-agents <on|off> [--yes] [--json]
4644
+
4645
+ org show your organization: id and whether guest agents
4646
+ (agents owned outside the org) are allowed in
4647
+ guest-agents on|off allow or block guest agents org-wide \u2014 this changes
4648
+ live cross-org access, so it asks y/N (--yes skips;
4649
+ org admin only)
4650
+
4651
+ --yes skip the confirmation prompt (required with --json on guest-agents)
4652
+ --json machine-readable result on stdout`;
4653
+ async function runOrgCli(instance2, args2, overrides = {}) {
4654
+ const deps = {
4655
+ resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
4656
+ runLogin: async () => (await login(instance2)).credentials,
4657
+ out: (line) => console.log(line),
4658
+ err: (line) => console.error(line),
4659
+ prompt: promptOnTty,
4660
+ ...overrides
4661
+ };
4662
+ try {
4663
+ if (!args2.sub) return await runOrgShow(instance2, args2, deps);
4664
+ if (args2.sub === "guest-agents") return await runOrgGuestAgents(instance2, args2, deps);
4665
+ deps.err(ORG_USAGE);
4666
+ deps.err(`
4667
+ unknown org command "${sanitizeLabel(args2.sub)}".`);
4668
+ return 1;
4669
+ } catch (err) {
4670
+ deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
4671
+ return 1;
4672
+ }
4673
+ }
4674
+ function orgErrorMessage(err) {
4675
+ if (err instanceof RelayError) {
4676
+ if (err.status === 403 && err.message === "admin_required") {
4677
+ return "you need to be an org admin on Braid to change org settings";
4678
+ }
4679
+ if (err.status === 404) {
4680
+ return "no active organization \u2014 sign in on Braid web and pick an organization first";
4681
+ }
4682
+ }
4683
+ return sanitizeForTty(err instanceof Error ? err.message : String(err));
4684
+ }
4685
+ async function fetchMyOrg(rest, args2, deps) {
4686
+ try {
4687
+ return await rest.getMyOrganization();
4688
+ } catch (err) {
4689
+ const msg = orgErrorMessage(err);
4690
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4691
+ deps.err(`\u2717 Braid could not read your organization: ${msg}`);
4692
+ return null;
4693
+ }
4694
+ }
4695
+ async function runOrgShow(instance2, args2, deps) {
4696
+ const creds = await ensureSignedIn(deps);
4697
+ const org = await fetchMyOrg(oneShotRest(instance2, creds), args2, deps);
4698
+ if (!org) return 1;
4699
+ if (args2.json) {
4700
+ deps.out(JSON.stringify(org, null, 2));
4701
+ return 0;
4702
+ }
4703
+ const row = (label, value) => deps.out(`${(label + ":").padEnd(14)}${value}`);
4704
+ row("org id", sanitizeLabel(org.id ?? "", 40));
4705
+ row("guest agents", org.allowGuestAgents ? "allowed \u2014 agents owned outside the org may join channels" : "blocked");
4706
+ return 0;
4707
+ }
4708
+ async function runOrgGuestAgents(instance2, args2, deps) {
4709
+ const value = args2.rest[0]?.trim();
4710
+ if (value !== "on" && value !== "off") {
4711
+ deps.err("usage: braid org guest-agents <on|off>");
4712
+ if (value) deps.err(`
4713
+ unknown value "${sanitizeLabel(value)}" \u2014 say on or off.`);
4714
+ return 1;
4715
+ }
4716
+ const allow = value === "on";
4717
+ const creds = await ensureSignedIn(deps);
4718
+ const rest = oneShotRest(instance2, creds);
4719
+ const org = await fetchMyOrg(rest, args2, deps);
4720
+ if (!org) return 1;
4721
+ if (org.allowGuestAgents === allow) {
4722
+ if (args2.json) deps.out(JSON.stringify({ ok: true, allowGuestAgents: allow, changed: false }));
4723
+ deps.err(`\u2714 guest agents are already ${allow ? "allowed" : "blocked"} \u2014 nothing to change`);
4724
+ return 0;
4725
+ }
4726
+ const confirmed = await confirmSimple(
4727
+ args2,
4728
+ deps,
4729
+ allow ? "allow guest agents? agents owned OUTSIDE your org can then be invited into your channels." : "block guest agents? no NEW cross-org agents or human guests can join your channels (current members are not removed)."
4730
+ );
4731
+ if (!confirmed) return 1;
4732
+ try {
4733
+ await rest.patchOrganization(org.id, { allowGuestAgents: allow });
4734
+ } catch (err) {
4735
+ const msg = orgErrorMessage(err);
4736
+ if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
4737
+ deps.err(`\u2717 Braid could not update the guest-agent policy: ${msg}`);
4738
+ return 1;
4739
+ }
4740
+ if (args2.json) deps.out(JSON.stringify({ ok: true, allowGuestAgents: allow, changed: true }));
4741
+ deps.err(
4742
+ allow ? "\u2714 guest agents allowed \u2014 cross-org agents can now be invited into your channels" : "\u2714 guest agents blocked \u2014 new cross-org agents and guests can no longer join your channels"
4743
+ );
4744
+ return 0;
4745
+ }
4746
+
2850
4747
  // src/index.tsx
2851
- import { jsx as jsx12 } from "react/jsx-runtime";
4748
+ import { jsx as jsx13 } from "react/jsx-runtime";
2852
4749
  function helpText(profileName2, base2, effective) {
2853
4750
  const relayOverridden = effective.relayUrl !== base2.relayUrl;
2854
4751
  const overrideNote = relayOverridden ? `
@@ -2857,7 +4754,7 @@ function helpText(profileName2, base2, effective) {
2857
4754
  instance \u2014 a JWT minted there will NOT work against the override)` : "";
2858
4755
  return `arp-tui \u2014 tail + post for Agent Relay Protocol channels (as the HUMAN)
2859
4756
 
2860
- usage: arp-tui [tail|login|logout|status|profile] [--profile <name>] [--relay <url>] [--token <jwt>]
4757
+ usage: arp-tui [tail|login|logout|status|profile|agent <verb>|agents|channel <verb>|channels|invite <verb>|invites|org] [--profile <name>] [--relay <url>] [--token <jwt>]
2861
4758
 
2862
4759
  commands:
2863
4760
  tail live channel tail + post (default)
@@ -2866,6 +4763,34 @@ commands:
2866
4763
  logout revoke the OAuth grant and delete this instance's stored tokens
2867
4764
  status show the stored credential state for the active instance
2868
4765
  profile manage saved instances (list|add|switch|remove; run \`arp-tui profile\`)
4766
+ agent add <name> create a personal agent on Braid, enroll it on this machine
4767
+ via the local bridge, and start it (--no-start to skip;
4768
+ --bridge-cmd/--provider/--json \u2014 run \`arp-tui agent\` for details)
4769
+ agent status|reissue|revoke|remove <name>
4770
+ inspect or recover an agent: detail card, fresh credential +
4771
+ re-enroll, credential revoke, permanent delete (revoke/remove
4772
+ confirm on the TTY; --yes with --json)
4773
+ agents list your agents: name, online/offline, credential state (--json)
4774
+ channels list your channels (--all includes archived; --json)
4775
+ channel create|archive|unarchive|members <name-or-id>
4776
+ channel lifecycle: create, archive (reversible; y/N confirm,
4777
+ --yes skips), restore, roster (run \`arp-tui channel\` for details)
4778
+ channel member add|remove <channel> <agent>
4779
+ add or remove one of your agents on a channel
4780
+ invites [<channel>]
4781
+ list agent invites: the org's channel-less invites, or one
4782
+ channel's (--all includes revoked/expired; --json)
4783
+ invite agent <channel> <name>
4784
+ mint a channel invite for a NEW agent \u2014 prints the join
4785
+ command ONCE (org admin; run \`arp-tui invite\` for details)
4786
+ invite human <channel> <email>
4787
+ invite a human by email into the channel (org admin)
4788
+ invite revoke <invite-id> [--channel <ch>]
4789
+ revoke an invite (y/N confirm, --yes skips)
4790
+ org show your organization: id + guest-agent policy (--json)
4791
+ org guest-agents on|off
4792
+ allow or block cross-org guest agents org-wide (org admin;
4793
+ y/N confirm, --yes skips)
2869
4794
 
2870
4795
  options:
2871
4796
  --profile <name> instance profile from ~/.arp-tui/config.json
@@ -2899,6 +4824,12 @@ function parseCliArgs(argv) {
2899
4824
  "clerk-fapi": { type: "string" },
2900
4825
  "clerk-template": { type: "string" },
2901
4826
  "clerk-oauth-client-id": { type: "string" },
4827
+ provider: { type: "string" },
4828
+ "no-start": { type: "boolean" },
4829
+ "bridge-cmd": { type: "string" },
4830
+ channel: { type: "string" },
4831
+ all: { type: "boolean" },
4832
+ yes: { type: "boolean" },
2902
4833
  json: { type: "boolean" },
2903
4834
  help: { type: "boolean", short: "h" }
2904
4835
  },
@@ -2912,6 +4843,7 @@ function parseCliArgs(argv) {
2912
4843
  cmd: parsed.positionals[0] ?? "tail",
2913
4844
  sub: parsed.positionals[1],
2914
4845
  name: parsed.positionals[2],
4846
+ positionals: parsed.positionals,
2915
4847
  profile: parsed.values.profile,
2916
4848
  relay: parsed.values.relay,
2917
4849
  token: parsed.values.token,
@@ -2919,6 +4851,12 @@ function parseCliArgs(argv) {
2919
4851
  clerkFapi: parsed.values["clerk-fapi"],
2920
4852
  clerkTemplate: parsed.values["clerk-template"],
2921
4853
  clerkOAuthClientId: parsed.values["clerk-oauth-client-id"],
4854
+ provider: parsed.values.provider,
4855
+ noStart: parsed.values["no-start"] === true,
4856
+ bridgeCmd: parsed.values["bridge-cmd"],
4857
+ channel: parsed.values.channel,
4858
+ all: parsed.values.all === true,
4859
+ yes: parsed.values.yes === true,
2922
4860
  json: parsed.values.json === true,
2923
4861
  help: parsed.values.help === true
2924
4862
  };
@@ -3026,6 +4964,53 @@ if (args.cmd === "login") {
3026
4964
  }
3027
4965
  }
3028
4966
  process.exit(0);
4967
+ } else if (args.cmd === "agent" || args.cmd === "agents") {
4968
+ const code = await runAgentCli(instance, {
4969
+ cmd: args.cmd,
4970
+ sub: args.sub,
4971
+ name: args.name,
4972
+ provider: args.provider,
4973
+ noStart: args.noStart,
4974
+ bridgeCmd: args.bridgeCmd,
4975
+ yes: args.yes,
4976
+ json: args.json,
4977
+ flagToken: args.token
4978
+ });
4979
+ process.exit(code);
4980
+ } else if (args.cmd === "channel" || args.cmd === "channels") {
4981
+ const code = await runChannelCli(instance, {
4982
+ cmd: args.cmd,
4983
+ sub: args.sub,
4984
+ rest: args.positionals.slice(2),
4985
+ all: args.all,
4986
+ yes: args.yes,
4987
+ json: args.json,
4988
+ flagToken: args.token
4989
+ });
4990
+ process.exit(code);
4991
+ } else if (args.cmd === "invite" || args.cmd === "invites") {
4992
+ const code = await runInviteCli(instance, {
4993
+ cmd: args.cmd,
4994
+ sub: args.sub,
4995
+ // `invites [<channel>]` has no subcommand — its positionals start right after the verb
4996
+ rest: args.cmd === "invites" ? args.positionals.slice(1) : args.positionals.slice(2),
4997
+ channel: args.channel,
4998
+ all: args.all,
4999
+ yes: args.yes,
5000
+ json: args.json,
5001
+ flagToken: args.token
5002
+ });
5003
+ process.exit(code);
5004
+ } else if (args.cmd === "org") {
5005
+ const code = await runOrgCli(instance, {
5006
+ cmd: args.cmd,
5007
+ sub: args.sub,
5008
+ rest: args.positionals.slice(2),
5009
+ yes: args.yes,
5010
+ json: args.json,
5011
+ flagToken: args.token
5012
+ });
5013
+ process.exit(code);
3029
5014
  } else if (args.cmd !== "tail") {
3030
5015
  console.error(`unknown command "${args.cmd}" \u2014 try --help.`);
3031
5016
  process.exit(1);
@@ -3043,4 +5028,4 @@ if (credentials?.tokenType === "dev-jwt" && credentials.token && !credentials.cl
3043
5028
  );
3044
5029
  }
3045
5030
  var store = createStore({ instance, credentials });
3046
- render(/* @__PURE__ */ jsx12(App, { store }));
5031
+ render(/* @__PURE__ */ jsx13(App, { store }));