arp-tui 0.0.4 → 0.0.6

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 }) {
@@ -2191,8 +2864,12 @@ function fallbackTitle(call) {
2191
2864
  function ActivitySurface(props) {
2192
2865
  const { toolCalls, plan, visible, pinned } = props;
2193
2866
  if (!visible) return null;
2867
+ const pinHint = pinned ? "pinned \xB7 press v to unpin" : "press v to keep open";
2194
2868
  if (toolCalls.length === 0 && plan.length === 0) {
2195
- return /* @__PURE__ */ jsx2(Box, { paddingLeft: 1, children: /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "no activity yet" }) });
2869
+ return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingLeft: 1, children: [
2870
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: "no activity yet" }),
2871
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pinHint })
2872
+ ] });
2196
2873
  }
2197
2874
  return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", paddingLeft: 1, children: [
2198
2875
  toolCalls.length > 0 && /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
@@ -2208,7 +2885,7 @@ function ActivitySurface(props) {
2208
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: [
2209
2886
  sanitizeLabel(loc.path, 60),
2210
2887
  loc.line != null ? `:${loc.line}` : ""
2211
- ] }, i)) })
2888
+ ] }, `${loc.path}:${loc.line ?? ""}#${i}`)) })
2212
2889
  ] }, call.toolCallId))
2213
2890
  ] }),
2214
2891
  plan.length > 0 && /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
@@ -2217,14 +2894,71 @@ function ActivitySurface(props) {
2217
2894
  planGlyph(entry.status),
2218
2895
  " ",
2219
2896
  sanitizeForTty(entry.content)
2220
- ] }, i))
2221
- ] })
2897
+ ] }, `${entry.content}#${i}`))
2898
+ ] }),
2899
+ /* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pinHint })
2222
2900
  ] });
2223
2901
  }
2224
2902
 
2225
- // src/ui/ChannelSwitcher.tsx
2903
+ // src/ui/AttentionPanel.tsx
2226
2904
  import { Box as Box2, Text as Text3 } from "ink";
2227
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";
2228
2962
  function channelLabel(ch) {
2229
2963
  return `${channelSigil(ch.kind)}${sanitizeLabel(ch.name)}`;
2230
2964
  }
@@ -2239,7 +2973,7 @@ function ChannelBadge({
2239
2973
  if (att) {
2240
2974
  const color = att.highestLevel === "critical" ? "red" : att.highestLevel === "action_required" ? "yellow" : "cyan";
2241
2975
  const sigil = att.highestLevel === "critical" ? "!" : att.highestLevel === "action_required" ? "\u25B2" : "\u2022";
2242
- return /* @__PURE__ */ jsxs3(Text3, { color, children: [
2976
+ return /* @__PURE__ */ jsxs4(Text4, { color, children: [
2243
2977
  " ",
2244
2978
  "[",
2245
2979
  sigil,
@@ -2247,11 +2981,11 @@ function ChannelBadge({
2247
2981
  "]"
2248
2982
  ] });
2249
2983
  }
2250
- const count = unread[channelId] ?? 0;
2251
- if (count > 0) {
2252
- 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: [
2253
2987
  " (",
2254
- count,
2988
+ count2,
2255
2989
  ")"
2256
2990
  ] });
2257
2991
  }
@@ -2268,11 +3002,11 @@ function ChannelSwitcher({
2268
3002
  const sorted = sortChannelsByPriority(filtered, { attention, unread });
2269
3003
  const visible = sorted.slice(0, 9);
2270
3004
  const hidden = sorted.length - visible.length;
2271
- return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
2272
- /* @__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: [
2273
3007
  "channels",
2274
3008
  " ",
2275
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3009
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2276
3010
  "(",
2277
3011
  sorted.length,
2278
3012
  "/",
@@ -2280,23 +3014,23 @@ function ChannelSwitcher({
2280
3014
  ")"
2281
3015
  ] })
2282
3016
  ] }),
2283
- /* @__PURE__ */ jsxs3(Text3, { children: [
2284
- /* @__PURE__ */ jsx3(Text3, { color: "cyan", children: "filter: " }),
2285
- /* @__PURE__ */ jsx3(Text3, { children: sanitizeLabel(filter, 40) }),
2286
- /* @__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: " " })
2287
3021
  ] }),
2288
- visible.map((ch, i) => /* @__PURE__ */ jsxs3(Text3, { children: [
2289
- /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
3022
+ visible.map((ch, i) => /* @__PURE__ */ jsxs4(Text4, { children: [
3023
+ /* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
2290
3024
  i + 1,
2291
3025
  " "
2292
3026
  ] }),
2293
- /* @__PURE__ */ jsx3(Text3, { bold: ch.id === activeChannelId, children: channelLabel(ch) }),
2294
- /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3027
+ /* @__PURE__ */ jsx4(Text4, { bold: ch.id === activeChannelId, children: channelLabel(ch) }),
3028
+ /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2295
3029
  " \xB7 ",
2296
3030
  ch.members.length,
2297
3031
  " members"
2298
3032
  ] }),
2299
- /* @__PURE__ */ jsx3(
3033
+ /* @__PURE__ */ jsx4(
2300
3034
  ChannelBadge,
2301
3035
  {
2302
3036
  channelId: ch.id,
@@ -2306,36 +3040,36 @@ function ChannelSwitcher({
2306
3040
  }
2307
3041
  )
2308
3042
  ] }, ch.id)),
2309
- hidden > 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3043
+ hidden > 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2310
3044
  "\u2026",
2311
3045
  hidden,
2312
3046
  " more \u2014 keep typing to narrow"
2313
3047
  ] }) : null,
2314
- sorted.length === 0 ? /* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
3048
+ sorted.length === 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
2315
3049
  'no channels match "',
2316
3050
  sanitizeLabel(filter, 20),
2317
3051
  '"'
2318
3052
  ] }) : null,
2319
- /* @__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" })
2320
3054
  ] });
2321
3055
  }
2322
3056
 
2323
3057
  // src/ui/Composer.tsx
2324
- import { Box as Box3, Text as Text4 } from "ink";
2325
- 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";
2326
3060
  function Composer({ draft, focused }) {
2327
3061
  const width = Math.max((process.stdout.columns ?? 80) - 8, 20);
2328
3062
  const visible = draft.length > width ? "\u2026" + draft.slice(-(width - 1)) : draft;
2329
- return /* @__PURE__ */ jsx4(Box3, { borderStyle: "round", borderColor: focused ? "cyan" : "gray", paddingX: 1, width: "100%", children: /* @__PURE__ */ jsxs4(Text4, { wrap: "truncate", children: [
2330
- /* @__PURE__ */ jsx4(Text4, { color: focused ? "cyan" : "gray", children: "> " }),
2331
- draft.length === 0 && !focused ? /* @__PURE__ */ jsx4(Text4, { dimColor: true, children: "i to compose" }) : /* @__PURE__ */ jsx4(Text4, { children: visible }),
2332
- 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
2333
3067
  ] }) });
2334
3068
  }
2335
3069
 
2336
3070
  // src/ui/Header.tsx
2337
- import { Box as Box4, Text as Text5 } from "ink";
2338
- 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";
2339
3073
  var STATUS_DOT = {
2340
3074
  online: { glyph: "\u25CF", color: "green" },
2341
3075
  away: { glyph: "\u25D0", color: "yellow" },
@@ -2352,32 +3086,32 @@ function Header({
2352
3086
  members,
2353
3087
  presence
2354
3088
  }) {
2355
- return /* @__PURE__ */ jsxs5(Box4, { flexDirection: "column", children: [
2356
- /* @__PURE__ */ jsxs5(Box4, { justifyContent: "space-between", children: [
2357
- /* @__PURE__ */ jsxs5(Text5, { children: [
2358
- /* @__PURE__ */ jsx5(Text5, { color: "cyan", bold: true, children: "arp-tui" }),
2359
- /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: " \u25B8 " }),
2360
- /* @__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: [
2361
3095
  channelSigil(channelKind),
2362
3096
  sanitizeLabel(channelName)
2363
3097
  ] })
2364
3098
  ] }),
2365
- /* @__PURE__ */ jsx5(Text5, { children: members.map((m) => {
3099
+ /* @__PURE__ */ jsx6(Text6, { children: members.map((m) => {
2366
3100
  const dot = STATUS_DOT[effectiveStatus(m, presence)];
2367
- return /* @__PURE__ */ jsxs5(Text5, { children: [
2368
- /* @__PURE__ */ jsxs5(Text5, { color: dot.color, children: [
3101
+ return /* @__PURE__ */ jsxs6(Text6, { children: [
3102
+ /* @__PURE__ */ jsxs6(Text6, { color: dot.color, children: [
2369
3103
  dot.glyph,
2370
3104
  " "
2371
3105
  ] }),
2372
- /* @__PURE__ */ jsxs5(Text5, { dimColor: effectiveStatus(m, presence) === "offline", children: [
3106
+ /* @__PURE__ */ jsxs6(Text6, { dimColor: effectiveStatus(m, presence) === "offline", children: [
2373
3107
  m.type === "bot" ? "\u2699" : "",
2374
3108
  sanitizeLabel(m.name, 20)
2375
3109
  ] }),
2376
- /* @__PURE__ */ jsx5(Text5, { children: " " })
3110
+ /* @__PURE__ */ jsx6(Text6, { children: " " })
2377
3111
  ] }, m.id);
2378
3112
  }) })
2379
3113
  ] }),
2380
- /* @__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)) })
2381
3115
  ] });
2382
3116
  }
2383
3117
 
@@ -2385,8 +3119,8 @@ function Header({
2385
3119
  import { Static } from "ink";
2386
3120
 
2387
3121
  // src/ui/MessageRow.tsx
2388
- import { Box as Box5, Text as Text6 } from "ink";
2389
- 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";
2390
3124
  function hhmm(rfc3339) {
2391
3125
  const d = new Date(rfc3339);
2392
3126
  return isNaN(d.getTime()) ? "--:--" : `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
@@ -2399,7 +3133,7 @@ function TokenChip({ message }) {
2399
3133
  if (message.messageType !== "agent" || !message.tokensUsed) return null;
2400
3134
  const parts = [`${formatTokens(message.tokensUsed)} tok`];
2401
3135
  if (message.model) parts.push(sanitizeLabel(message.model, 16));
2402
- return /* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
3136
+ return /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
2403
3137
  message.verified ? "\u2713 " : "",
2404
3138
  parts.join(" \xB7 ")
2405
3139
  ] });
@@ -2409,41 +3143,41 @@ function MessageRow({ message }) {
2409
3143
  const isSystem = message.messageType === "system";
2410
3144
  const name = sanitizeLabel(message.agentName, 20);
2411
3145
  if (isSystem) {
2412
- 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: [
2413
3147
  " ",
2414
3148
  "\u2500\u2500 ",
2415
3149
  sanitizeForTty(message.content),
2416
3150
  " \u2500\u2500"
2417
3151
  ] }) });
2418
3152
  }
2419
- return /* @__PURE__ */ jsxs6(Box5, { justifyContent: "space-between", gap: 2, children: [
2420
- /* @__PURE__ */ jsx6(Box5, { flexShrink: 1, children: /* @__PURE__ */ jsxs6(Text6, { children: [
2421
- /* @__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: [
2422
3156
  hhmm(message.createdAt),
2423
3157
  " "
2424
3158
  ] }),
2425
- /* @__PURE__ */ jsxs6(Text6, { color: isBot ? "magenta" : "cyan", bold: !isBot, children: [
3159
+ /* @__PURE__ */ jsxs7(Text7, { color: isBot ? "magenta" : "cyan", bold: !isBot, children: [
2426
3160
  isBot ? "\u2699" : "",
2427
3161
  name.padEnd(12)
2428
3162
  ] }),
2429
- /* @__PURE__ */ jsxs6(Text6, { children: [
3163
+ /* @__PURE__ */ jsxs7(Text7, { children: [
2430
3164
  " ",
2431
3165
  sanitizeForTty(message.content)
2432
3166
  ] })
2433
3167
  ] }) }),
2434
- /* @__PURE__ */ jsx6(Box5, { flexShrink: 0, children: /* @__PURE__ */ jsx6(TokenChip, { message }) })
3168
+ /* @__PURE__ */ jsx7(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx7(TokenChip, { message }) })
2435
3169
  ] });
2436
3170
  }
2437
3171
 
2438
3172
  // src/ui/MessageList.tsx
2439
- import { jsx as jsx7 } from "react/jsx-runtime";
3173
+ import { jsx as jsx8 } from "react/jsx-runtime";
2440
3174
  function MessageList({ messages }) {
2441
- 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) });
2442
3176
  }
2443
3177
 
2444
3178
  // src/ui/NotificationsPanel.tsx
2445
- import { Box as Box6, Text as Text7 } from "ink";
2446
- 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";
2447
3181
  function relativeAge(createdAt) {
2448
3182
  const ms = Date.now() - new Date(createdAt).getTime();
2449
3183
  if (!Number.isFinite(ms) || ms < 0) return "\u2014";
@@ -2461,11 +3195,11 @@ function NotificationsPanel({
2461
3195
  notifSelected
2462
3196
  }) {
2463
3197
  const unreadCount = notifications.filter((n) => !n.readAt).length;
2464
- return /* @__PURE__ */ jsxs7(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2465
- /* @__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: [
2466
3200
  "alerts",
2467
3201
  " ",
2468
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3202
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2469
3203
  "(",
2470
3204
  unreadCount,
2471
3205
  " unread \xB7 ",
@@ -2473,38 +3207,42 @@ function NotificationsPanel({
2473
3207
  " total)"
2474
3208
  ] })
2475
3209
  ] }),
2476
- 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) => {
2477
3211
  const isSelected = i === notifSelected;
2478
3212
  const isUnread = n.readAt === null;
2479
3213
  const type = sanitizeLabel(n.type, 20);
2480
3214
  const ref = shortRef(sanitizeForTty(n.entityRef));
2481
3215
  const age = relativeAge(n.createdAt);
2482
- return /* @__PURE__ */ jsxs7(Text7, { inverse: isSelected, children: [
2483
- isUnread ? /* @__PURE__ */ jsx8(Text7, { color: "yellow", children: "\u2022 " }) : /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " " }),
2484
- /* @__PURE__ */ jsx8(Text7, { bold: isSelected, children: type }),
2485
- /* @__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: [
2486
3220
  " ",
2487
3221
  ref
2488
3222
  ] }),
2489
- /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
3223
+ /* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
2490
3224
  " ",
2491
3225
  age
2492
3226
  ] })
2493
3227
  ] }, n.id);
2494
3228
  }),
2495
- /* @__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" })
2496
3230
  ] });
2497
3231
  }
2498
3232
 
2499
3233
  // src/ui/StatusBar.tsx
2500
- import { Text as Text8 } from "ink";
2501
- 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";
2502
3236
  var MODE_LABEL = {
2503
3237
  normal: "NORMAL",
2504
3238
  insert: "INSERT",
2505
3239
  switcher: "CHANNELS",
2506
3240
  token: "TOKEN",
2507
- notifications: "ALERTS"
3241
+ notifications: "ALERTS",
3242
+ attention: "ATTENTION",
3243
+ attention_level: "ATTENTION LEVEL",
3244
+ attention_reason: "ATTENTION REASON",
3245
+ attention_confirm: "CONFIRM ATTENTION"
2508
3246
  };
2509
3247
  function ageLabel(obtainedAt) {
2510
3248
  if (!obtainedAt) return "\u2014";
@@ -2555,41 +3293,41 @@ function StatusBar({
2555
3293
  color: stale ? "yellow" : "green"
2556
3294
  };
2557
3295
  }
2558
- return /* @__PURE__ */ jsxs8(Text8, { children: [
2559
- /* @__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: [
2560
3298
  " ",
2561
3299
  MODE_LABEL[mode],
2562
3300
  " "
2563
3301
  ] }),
2564
- /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "\u2502 " }),
2565
- /* @__PURE__ */ jsx9(Text8, { color: connColor, children: connectionLabel(connection, reconnectAt, reconnectAttempt) }),
2566
- /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " \u2502 " }),
2567
- /* @__PURE__ */ jsx9(Text8, { color: tokenSeg.color, children: tokenSeg.text }),
2568
- /* @__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: [
2569
3307
  " \u2502 seq ",
2570
3308
  lastSeq ?? "\u2014"
2571
3309
  ] }),
2572
- notifCount > 0 ? /* @__PURE__ */ jsxs8(Text8, { color: "yellow", children: [
3310
+ notifCount > 0 ? /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
2573
3311
  " \u2502 alerts:",
2574
3312
  notifCount
2575
3313
  ] }) : null,
2576
3314
  error ? (
2577
3315
  // BL-222: an error replaces the full hint line but must never strand
2578
3316
  // the user — keep the exit hint visible alongside it
2579
- /* @__PURE__ */ jsxs8(Fragment, { children: [
2580
- /* @__PURE__ */ jsxs8(Text8, { color: "red", children: [
3317
+ /* @__PURE__ */ jsxs9(Fragment, { children: [
3318
+ /* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
2581
3319
  " \u2502 ",
2582
3320
  sanitizeForTty(error)
2583
3321
  ] }),
2584
- /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " \u2502 q quit" })
3322
+ /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 q quit" })
2585
3323
  ] })
2586
- ) : /* @__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" })
2587
3325
  ] });
2588
3326
  }
2589
3327
 
2590
3328
  // src/ui/TokenPrompt.tsx
2591
- import { Box as Box7, Text as Text9 } from "ink";
2592
- 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";
2593
3331
  function TokenPrompt({
2594
3332
  value,
2595
3333
  reason,
@@ -2597,26 +3335,26 @@ function TokenPrompt({
2597
3335
  jwtTemplate
2598
3336
  }) {
2599
3337
  const masked = value.length === 0 ? "" : value.length <= 16 ? value : `${value.slice(0, 12)}\u2026 (${value.length} chars)`;
2600
- return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
2601
- /* @__PURE__ */ jsx10(Text9, { bold: true, color: "yellow", children: "token needed" }),
2602
- reason ? /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: sanitizeForTty(reason) }) : null,
2603
- /* @__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: [
2604
3342
  "mint one: open ",
2605
3343
  webUrl,
2606
3344
  " logged in, browser console:"
2607
3345
  ] }),
2608
- /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: ` await window.Clerk.session.getToken({template:'${jwtTemplate}'})` }),
2609
- /* @__PURE__ */ jsxs9(Text9, { children: [
2610
- /* @__PURE__ */ jsx10(Text9, { color: "yellow", children: "> " }),
2611
- /* @__PURE__ */ jsx10(Text9, { children: masked }),
2612
- /* @__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: " " })
2613
3351
  ] }),
2614
- /* @__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" })
2615
3353
  ] });
2616
3354
  }
2617
3355
 
2618
3356
  // src/app.tsx
2619
- import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
3357
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2620
3358
  var ACTIVITY_SETTLE_MS2 = 4e3;
2621
3359
  function cleanInput(input) {
2622
3360
  return input.replace(/\r\n|[\r\n]/g, " ").replace(/[\x00-\x1f\x7f]/g, "");
@@ -2663,6 +3401,22 @@ function App({ store: store2 }) {
2663
3401
  } else if (input === "a") store2.markAllNotificationsRead();
2664
3402
  return;
2665
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
+ }
2666
3420
  if (s.mode === "switcher") {
2667
3421
  const filtered = filterChannels(s.channels, s.switcherFilter);
2668
3422
  const sorted = sortChannelsByPriority(filtered, { attention: s.attention, unread: s.unread });
@@ -2686,6 +3440,7 @@ function App({ store: store2 }) {
2686
3440
  exit();
2687
3441
  } else if (input === "c") store2.setMode("switcher");
2688
3442
  else if (input === "a") store2.setMode("notifications");
3443
+ else if (input === "t") store2.openAttention();
2689
3444
  else if (input === "i" || key.return) store2.setMode("insert");
2690
3445
  else if (input === "r") store2.reconnectNow();
2691
3446
  else if (input === "n") store2.jumpNextActive();
@@ -2703,8 +3458,8 @@ function App({ store: store2 }) {
2703
3458
  const activityLive = activityRows.some((r) => r.status === "pending" || r.status === "in_progress") || activityPlan.some((e) => e.status !== "completed");
2704
3459
  const activityWithinSettle = state.activityIdleSince !== null && Date.now() - state.activityIdleSince < ACTIVITY_SETTLE_MS2;
2705
3460
  const activityVisible = state.activityPinned || activityLive || activityWithinSettle;
2706
- return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", children: [
2707
- /* @__PURE__ */ jsx11(
3461
+ return /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
3462
+ /* @__PURE__ */ jsx12(
2708
3463
  Header,
2709
3464
  {
2710
3465
  channelName: activeChannel?.name ?? "(connecting\u2026)",
@@ -2713,9 +3468,9 @@ function App({ store: store2 }) {
2713
3468
  presence: state.presence
2714
3469
  }
2715
3470
  ),
2716
- /* @__PURE__ */ jsx11(MessageList, { messages: state.messages }),
2717
- /* @__PURE__ */ jsx11(ActivitySurface, { toolCalls: activityRows, plan: activityPlan, visible: activityVisible, pinned: state.activityPinned }),
2718
- 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(
2719
3474
  ChannelSwitcher,
2720
3475
  {
2721
3476
  channels: state.channels,
@@ -2725,14 +3480,25 @@ function App({ store: store2 }) {
2725
3480
  unread: state.unread
2726
3481
  }
2727
3482
  ) : null,
2728
- state.mode === "notifications" ? /* @__PURE__ */ jsx11(
3483
+ state.mode === "notifications" ? /* @__PURE__ */ jsx12(
2729
3484
  NotificationsPanel,
2730
3485
  {
2731
3486
  notifications: state.notifications,
2732
3487
  notifSelected: state.notifSelected
2733
3488
  }
2734
3489
  ) : null,
2735
- 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(
2736
3502
  TokenPrompt,
2737
3503
  {
2738
3504
  value: state.tokenPrompt,
@@ -2741,9 +3507,9 @@ function App({ store: store2 }) {
2741
3507
  jwtTemplate: store2.instance.clerkJwtTemplate
2742
3508
  }
2743
3509
  ) : null,
2744
- /* @__PURE__ */ jsx11(ActivityLine, { activities: state.activities }),
2745
- /* @__PURE__ */ jsx11(Composer, { draft: state.draft, focused: state.mode === "insert" }),
2746
- /* @__PURE__ */ jsx11(
3510
+ /* @__PURE__ */ jsx12(ActivityLine, { activities: state.activities }),
3511
+ /* @__PURE__ */ jsx12(Composer, { draft: state.draft, focused: state.mode === "insert" }),
3512
+ /* @__PURE__ */ jsx12(
2747
3513
  StatusBar,
2748
3514
  {
2749
3515
  mode: state.mode,
@@ -2759,7 +3525,7 @@ function App({ store: store2 }) {
2759
3525
  error: state.error
2760
3526
  }
2761
3527
  ),
2762
- !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
2763
3529
  ] });
2764
3530
  }
2765
3531
 
@@ -2842,8 +3608,1144 @@ unknown profile command "${args2.sub}".`);
2842
3608
  }
2843
3609
  }
2844
3610
 
3611
+ // src/api/bridgeProcess.ts
3612
+ import { spawn as spawn2 } from "child_process";
3613
+ var DEFAULT_BRIDGE_CMD = "npx -y arp-bridge@latest";
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
+
2845
4747
  // src/index.tsx
2846
- import { jsx as jsx12 } from "react/jsx-runtime";
4748
+ import { jsx as jsx13 } from "react/jsx-runtime";
2847
4749
  function helpText(profileName2, base2, effective) {
2848
4750
  const relayOverridden = effective.relayUrl !== base2.relayUrl;
2849
4751
  const overrideNote = relayOverridden ? `
@@ -2852,7 +4754,7 @@ function helpText(profileName2, base2, effective) {
2852
4754
  instance \u2014 a JWT minted there will NOT work against the override)` : "";
2853
4755
  return `arp-tui \u2014 tail + post for Agent Relay Protocol channels (as the HUMAN)
2854
4756
 
2855
- 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>]
2856
4758
 
2857
4759
  commands:
2858
4760
  tail live channel tail + post (default)
@@ -2861,6 +4763,34 @@ commands:
2861
4763
  logout revoke the OAuth grant and delete this instance's stored tokens
2862
4764
  status show the stored credential state for the active instance
2863
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)
2864
4794
 
2865
4795
  options:
2866
4796
  --profile <name> instance profile from ~/.arp-tui/config.json
@@ -2894,6 +4824,12 @@ function parseCliArgs(argv) {
2894
4824
  "clerk-fapi": { type: "string" },
2895
4825
  "clerk-template": { type: "string" },
2896
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" },
2897
4833
  json: { type: "boolean" },
2898
4834
  help: { type: "boolean", short: "h" }
2899
4835
  },
@@ -2907,6 +4843,7 @@ function parseCliArgs(argv) {
2907
4843
  cmd: parsed.positionals[0] ?? "tail",
2908
4844
  sub: parsed.positionals[1],
2909
4845
  name: parsed.positionals[2],
4846
+ positionals: parsed.positionals,
2910
4847
  profile: parsed.values.profile,
2911
4848
  relay: parsed.values.relay,
2912
4849
  token: parsed.values.token,
@@ -2914,6 +4851,12 @@ function parseCliArgs(argv) {
2914
4851
  clerkFapi: parsed.values["clerk-fapi"],
2915
4852
  clerkTemplate: parsed.values["clerk-template"],
2916
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,
2917
4860
  json: parsed.values.json === true,
2918
4861
  help: parsed.values.help === true
2919
4862
  };
@@ -3021,6 +4964,53 @@ if (args.cmd === "login") {
3021
4964
  }
3022
4965
  }
3023
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);
3024
5014
  } else if (args.cmd !== "tail") {
3025
5015
  console.error(`unknown command "${args.cmd}" \u2014 try --help.`);
3026
5016
  process.exit(1);
@@ -3038,4 +5028,4 @@ if (credentials?.tokenType === "dev-jwt" && credentials.token && !credentials.cl
3038
5028
  );
3039
5029
  }
3040
5030
  var store = createStore({ instance, credentials });
3041
- render(/* @__PURE__ */ jsx12(App, { store }));
5031
+ render(/* @__PURE__ */ jsx13(App, { store }));