arp-tui 0.0.5 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +2118 -133
- package/npm-shrinkwrap.json +6 -5
- package/package.json +3 -1
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
|
|
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 (
|
|
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
|
|
849
|
-
|
|
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
|
|
1041
|
-
if (
|
|
1042
|
-
if (
|
|
1043
|
-
if (
|
|
1570
|
+
const level2 = p.attention[cid]?.highestLevel;
|
|
1571
|
+
if (level2 === "critical") return 0;
|
|
1572
|
+
if (level2 === "action_required") return 1;
|
|
1573
|
+
if (level2 === "info") return 2;
|
|
1044
1574
|
if ((p.unread[cid] ?? 0) > 0) return 3;
|
|
1045
1575
|
return 4;
|
|
1046
1576
|
}
|
|
@@ -1342,6 +1872,10 @@ function createStore(opts) {
|
|
|
1342
1872
|
unread: { ...config.unread ?? {} },
|
|
1343
1873
|
unreadCapNotice: null,
|
|
1344
1874
|
attention: {},
|
|
1875
|
+
channelAttention: [],
|
|
1876
|
+
attentionSelected: 0,
|
|
1877
|
+
attentionDraft: "",
|
|
1878
|
+
attentionBusy: false,
|
|
1345
1879
|
notifications: [],
|
|
1346
1880
|
notifSelected: 0,
|
|
1347
1881
|
activityByTurn: {},
|
|
@@ -1498,6 +2032,9 @@ function createStore(opts) {
|
|
|
1498
2032
|
}
|
|
1499
2033
|
const rest = new RelayRest({ relayUrl, getToken });
|
|
1500
2034
|
const ws = new RelayWs({ relayUrl, getToken });
|
|
2035
|
+
const attentionApi = () => opts.attentionApi ?? new AttentionMutations(new LifecycleTransport({ relayUrl, getToken }));
|
|
2036
|
+
let pendingAttentionLevel = null;
|
|
2037
|
+
let pendingAttentionAck = null;
|
|
1501
2038
|
function pushDivider(label) {
|
|
1502
2039
|
const id = `divider-${++dividerCounter}`;
|
|
1503
2040
|
seen.add(id);
|
|
@@ -1595,6 +2132,141 @@ function createStore(opts) {
|
|
|
1595
2132
|
const next = Math.max(0, Math.min(state.notifSelected + delta, len - 1));
|
|
1596
2133
|
set({ notifSelected: next });
|
|
1597
2134
|
}
|
|
2135
|
+
async function refreshChannelAttention() {
|
|
2136
|
+
const channelId = state.activeChannelId;
|
|
2137
|
+
if (!channelId) return;
|
|
2138
|
+
try {
|
|
2139
|
+
const items = await attentionApi().listChannel(channelId);
|
|
2140
|
+
if (state.activeChannelId !== channelId) return;
|
|
2141
|
+
setThrottled({
|
|
2142
|
+
channelAttention: items,
|
|
2143
|
+
attentionSelected: Math.max(0, Math.min(state.attentionSelected, items.length - 1))
|
|
2144
|
+
});
|
|
2145
|
+
} catch (error) {
|
|
2146
|
+
if (error instanceof CommandFailure && state.mode.startsWith("attention")) {
|
|
2147
|
+
set({ error: `${error.stableCode}: ${error.message}` });
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
function finishAttentionMutation(error) {
|
|
2152
|
+
pendingAttentionLevel = null;
|
|
2153
|
+
pendingAttentionAck = null;
|
|
2154
|
+
set({
|
|
2155
|
+
mode: "attention",
|
|
2156
|
+
attentionDraft: "",
|
|
2157
|
+
attentionBusy: false,
|
|
2158
|
+
error: error instanceof CommandFailure ? `${error.stableCode}: ${error.message}` : error ? "attention operation failed safely" : null
|
|
2159
|
+
});
|
|
2160
|
+
void refreshChannelAttention();
|
|
2161
|
+
void pollAttention();
|
|
2162
|
+
}
|
|
2163
|
+
async function performAttentionRaise(level2, reason) {
|
|
2164
|
+
const channelId = state.activeChannelId;
|
|
2165
|
+
if (!channelId || state.attentionBusy) return;
|
|
2166
|
+
set({ mode: "attention", attentionDraft: "", attentionBusy: true, error: null });
|
|
2167
|
+
const operationId = (opts.newOperationId ?? randomUUID)();
|
|
2168
|
+
try {
|
|
2169
|
+
const api = attentionApi();
|
|
2170
|
+
const before = await api.listChannel(channelId);
|
|
2171
|
+
const previousCount = before.find((item) => item.level === level2)?.count ?? 0;
|
|
2172
|
+
try {
|
|
2173
|
+
await api.raise({ channelId, level: level2, reason, operationId });
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
if (!(error instanceof OutcomeUnknown)) throw error;
|
|
2176
|
+
const current = (await api.listChannel(channelId)).find((item) => item.level === level2);
|
|
2177
|
+
if (!current || current.reason !== reason || current.count <= previousCount) throw error;
|
|
2178
|
+
}
|
|
2179
|
+
finishAttentionMutation();
|
|
2180
|
+
} catch (error) {
|
|
2181
|
+
finishAttentionMutation(error);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
async function performAttentionAck(scope) {
|
|
2185
|
+
const channelId = state.activeChannelId;
|
|
2186
|
+
if (!channelId || state.attentionBusy) return;
|
|
2187
|
+
set({ mode: "attention", attentionDraft: "", attentionBusy: true, error: null });
|
|
2188
|
+
const operationId = (opts.newOperationId ?? randomUUID)();
|
|
2189
|
+
try {
|
|
2190
|
+
const api = attentionApi();
|
|
2191
|
+
try {
|
|
2192
|
+
await api.ack({ channelId, ...scope === "all" ? {} : { level: scope }, operationId });
|
|
2193
|
+
} catch (error) {
|
|
2194
|
+
if (!(error instanceof OutcomeUnknown)) throw error;
|
|
2195
|
+
const current = await api.listChannel(channelId);
|
|
2196
|
+
const reconciled = scope === "all" ? current.length === 0 : !current.some((item) => item.level === scope);
|
|
2197
|
+
if (!reconciled) throw error;
|
|
2198
|
+
}
|
|
2199
|
+
finishAttentionMutation();
|
|
2200
|
+
} catch (error) {
|
|
2201
|
+
finishAttentionMutation(error);
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
function openAttention() {
|
|
2205
|
+
if (!state.activeChannelId || state.attentionBusy) return;
|
|
2206
|
+
set({ mode: "attention", attentionDraft: "", attentionSelected: 0, error: null });
|
|
2207
|
+
void refreshChannelAttention();
|
|
2208
|
+
}
|
|
2209
|
+
function closeAttention() {
|
|
2210
|
+
if (state.attentionBusy) return;
|
|
2211
|
+
pendingAttentionLevel = null;
|
|
2212
|
+
pendingAttentionAck = null;
|
|
2213
|
+
set({
|
|
2214
|
+
mode: state.mode === "attention" ? "normal" : "attention",
|
|
2215
|
+
attentionDraft: "",
|
|
2216
|
+
error: null
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
function moveAttentionSelection(delta) {
|
|
2220
|
+
if (state.channelAttention.length === 0 || state.attentionBusy) return;
|
|
2221
|
+
set({ attentionSelected: Math.max(0, Math.min(state.attentionSelected + delta, state.channelAttention.length - 1)) });
|
|
2222
|
+
}
|
|
2223
|
+
function beginAttentionRaise() {
|
|
2224
|
+
if (state.attentionBusy) return;
|
|
2225
|
+
pendingAttentionLevel = null;
|
|
2226
|
+
pendingAttentionAck = null;
|
|
2227
|
+
set({ mode: "attention_level", attentionDraft: "", error: null });
|
|
2228
|
+
}
|
|
2229
|
+
function beginAttentionAck(all) {
|
|
2230
|
+
if (state.attentionBusy) return;
|
|
2231
|
+
if (all) pendingAttentionAck = "all";
|
|
2232
|
+
else pendingAttentionAck = state.channelAttention[state.attentionSelected]?.level ?? null;
|
|
2233
|
+
if (!pendingAttentionAck) return;
|
|
2234
|
+
set({ mode: "attention_confirm", attentionDraft: "", error: null });
|
|
2235
|
+
}
|
|
2236
|
+
function setAttentionDraft(value) {
|
|
2237
|
+
const max = state.mode === "attention_level" ? 32 : state.mode === "attention_confirm" ? 36 : 1024;
|
|
2238
|
+
if (Buffer.byteLength(value, "utf8") <= max) set({ attentionDraft: value });
|
|
2239
|
+
}
|
|
2240
|
+
function submitAttentionInput() {
|
|
2241
|
+
if (state.attentionBusy) return;
|
|
2242
|
+
if (state.mode === "attention_level") {
|
|
2243
|
+
const value = state.attentionDraft.trim();
|
|
2244
|
+
if (!ATTENTION_LEVELS.has(value)) {
|
|
2245
|
+
set({ error: "level must be info, action_required, or critical" });
|
|
2246
|
+
return;
|
|
2247
|
+
}
|
|
2248
|
+
pendingAttentionLevel = value;
|
|
2249
|
+
set({ mode: "attention_reason", attentionDraft: "", error: null });
|
|
2250
|
+
return;
|
|
2251
|
+
}
|
|
2252
|
+
if (state.mode === "attention_reason") {
|
|
2253
|
+
if (!pendingAttentionLevel) return;
|
|
2254
|
+
try {
|
|
2255
|
+
const reason = normalizeReason(state.attentionDraft);
|
|
2256
|
+
void performAttentionRaise(pendingAttentionLevel, reason);
|
|
2257
|
+
} catch (error) {
|
|
2258
|
+
if (error instanceof CommandFailure) set({ error: `${error.stableCode}: ${error.message}` });
|
|
2259
|
+
}
|
|
2260
|
+
return;
|
|
2261
|
+
}
|
|
2262
|
+
if (state.mode === "attention_confirm") {
|
|
2263
|
+
if (state.attentionDraft !== state.activeChannelId) {
|
|
2264
|
+
set({ error: "confirmation did not match the channel UUID" });
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
2267
|
+
if (pendingAttentionAck) void performAttentionAck(pendingAttentionAck);
|
|
2268
|
+
}
|
|
2269
|
+
}
|
|
1598
2270
|
function seedPresence(members) {
|
|
1599
2271
|
const presence = {};
|
|
1600
2272
|
for (const m of members) presence[m.id] = m.status === "online" ? "active" : m.status;
|
|
@@ -1811,6 +2483,16 @@ function createStore(opts) {
|
|
|
1811
2483
|
pushDivider("channel archived");
|
|
1812
2484
|
return;
|
|
1813
2485
|
}
|
|
2486
|
+
case "channel.attention.raised":
|
|
2487
|
+
case "channel.attention.updated":
|
|
2488
|
+
case "channel.attention.cleared":
|
|
2489
|
+
case "channel.attention.acked": {
|
|
2490
|
+
void pollAttention();
|
|
2491
|
+
if (frame.channelId === state.activeChannelId && state.mode.startsWith("attention")) {
|
|
2492
|
+
void refreshChannelAttention();
|
|
2493
|
+
}
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
1814
2496
|
case "activity_event": {
|
|
1815
2497
|
if (frame.channelId !== state.activeChannelId) return;
|
|
1816
2498
|
if (!frame.turnId || !frame.toolCallId) return;
|
|
@@ -2093,6 +2775,13 @@ function createStore(opts) {
|
|
|
2093
2775
|
dismissNotification: (id) => void dismissNotification(id),
|
|
2094
2776
|
markAllNotificationsRead: () => void markAllNotificationsRead(),
|
|
2095
2777
|
moveNotifSelection,
|
|
2778
|
+
openAttention,
|
|
2779
|
+
closeAttention,
|
|
2780
|
+
moveAttentionSelection,
|
|
2781
|
+
beginAttentionRaise,
|
|
2782
|
+
beginAttentionAck,
|
|
2783
|
+
setAttentionDraft,
|
|
2784
|
+
submitAttentionInput,
|
|
2096
2785
|
jumpNextActive: () => {
|
|
2097
2786
|
const s = state;
|
|
2098
2787
|
const target = nextActiveChannel(s.channels, s.activeChannelId, { attention: s.attention, unread: s.unread });
|
|
@@ -2105,22 +2794,6 @@ function createStore(opts) {
|
|
|
2105
2794
|
|
|
2106
2795
|
// src/ui/ActivityLine.tsx
|
|
2107
2796
|
import { Text } from "ink";
|
|
2108
|
-
|
|
2109
|
-
// src/util/sanitize.ts
|
|
2110
|
-
var ESCAPE_SEQ = (
|
|
2111
|
-
// eslint-disable-next-line no-control-regex
|
|
2112
|
-
/\x1b(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:\x07|\x1b\\)|[PX^_][\s\S]*?\x1b\\|[@-Z\\-_])/g
|
|
2113
|
-
);
|
|
2114
|
-
var CONTROL_CHARS = /[\x00-\x08\x0b-\x1f\x7f]/g;
|
|
2115
|
-
function sanitizeForTty(input) {
|
|
2116
|
-
return input.replace(ESCAPE_SEQ, "").replace(CONTROL_CHARS, "");
|
|
2117
|
-
}
|
|
2118
|
-
function sanitizeLabel(input, max = 40) {
|
|
2119
|
-
const clean = sanitizeForTty(input);
|
|
2120
|
-
return clean.length > max ? clean.slice(0, max - 1) + "\u2026" : clean;
|
|
2121
|
-
}
|
|
2122
|
-
|
|
2123
|
-
// src/ui/ActivityLine.tsx
|
|
2124
2797
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2125
2798
|
var MAX_SHOWN = 3;
|
|
2126
2799
|
function ActivityLine({ activities }) {
|
|
@@ -2212,7 +2885,7 @@ function ActivitySurface(props) {
|
|
|
2212
2885
|
pinned && call.locations && call.locations.length > 0 && /* @__PURE__ */ jsx2(Box, { flexDirection: "column", paddingLeft: 2, children: call.locations.map((loc, i) => /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
2213
2886
|
sanitizeLabel(loc.path, 60),
|
|
2214
2887
|
loc.line != null ? `:${loc.line}` : ""
|
|
2215
|
-
] }, i)) })
|
|
2888
|
+
] }, `${loc.path}:${loc.line ?? ""}#${i}`)) })
|
|
2216
2889
|
] }, call.toolCallId))
|
|
2217
2890
|
] }),
|
|
2218
2891
|
plan.length > 0 && /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", children: [
|
|
@@ -2221,15 +2894,71 @@ function ActivitySurface(props) {
|
|
|
2221
2894
|
planGlyph(entry.status),
|
|
2222
2895
|
" ",
|
|
2223
2896
|
sanitizeForTty(entry.content)
|
|
2224
|
-
] }, i))
|
|
2897
|
+
] }, `${entry.content}#${i}`))
|
|
2225
2898
|
] }),
|
|
2226
2899
|
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: pinHint })
|
|
2227
2900
|
] });
|
|
2228
2901
|
}
|
|
2229
2902
|
|
|
2230
|
-
// src/ui/
|
|
2903
|
+
// src/ui/AttentionPanel.tsx
|
|
2231
2904
|
import { Box as Box2, Text as Text3 } from "ink";
|
|
2232
2905
|
import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
|
|
2906
|
+
var LEVEL_COLOR = {
|
|
2907
|
+
info: "blue",
|
|
2908
|
+
action_required: "yellow",
|
|
2909
|
+
critical: "red"
|
|
2910
|
+
};
|
|
2911
|
+
function AttentionPanel({
|
|
2912
|
+
channelId,
|
|
2913
|
+
items,
|
|
2914
|
+
selected,
|
|
2915
|
+
mode,
|
|
2916
|
+
draft,
|
|
2917
|
+
busy
|
|
2918
|
+
}) {
|
|
2919
|
+
const prompt = mode === "attention_level" ? "level (info | action_required | critical)" : mode === "attention_reason" ? "reason" : mode === "attention_confirm" ? `type ${channelId} to confirm` : null;
|
|
2920
|
+
return /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2921
|
+
/* @__PURE__ */ jsxs3(Text3, { bold: true, children: [
|
|
2922
|
+
"ATTENTION channel:",
|
|
2923
|
+
sanitizeForTty(channelId)
|
|
2924
|
+
] }),
|
|
2925
|
+
items.length === 0 ? /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: "no active attention" }) : items.map((item, index) => /* @__PURE__ */ jsxs3(Box2, { flexDirection: "column", children: [
|
|
2926
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2927
|
+
/* @__PURE__ */ jsxs3(Text3, { color: index === selected ? "cyan" : void 0, children: [
|
|
2928
|
+
index === selected ? ">" : " ",
|
|
2929
|
+
" "
|
|
2930
|
+
] }),
|
|
2931
|
+
/* @__PURE__ */ jsx3(Text3, { bold: true, color: LEVEL_COLOR[item.level], children: item.level }),
|
|
2932
|
+
/* @__PURE__ */ jsxs3(Text3, { children: [
|
|
2933
|
+
" count:",
|
|
2934
|
+
item.count,
|
|
2935
|
+
" ",
|
|
2936
|
+
item.raisedByType ?? "unknown",
|
|
2937
|
+
":",
|
|
2938
|
+
sanitizeForTty(item.raisedBy ?? "unknown")
|
|
2939
|
+
] })
|
|
2940
|
+
] }),
|
|
2941
|
+
/* @__PURE__ */ jsxs3(Text3, { wrap: "wrap", children: [
|
|
2942
|
+
" ",
|
|
2943
|
+
sanitizeForTty(item.reason)
|
|
2944
|
+
] }),
|
|
2945
|
+
/* @__PURE__ */ jsxs3(Text3, { dimColor: true, children: [
|
|
2946
|
+
" updated ",
|
|
2947
|
+
sanitizeForTty(item.updatedAt)
|
|
2948
|
+
] })
|
|
2949
|
+
] }, item.id)),
|
|
2950
|
+
prompt ? /* @__PURE__ */ jsxs3(Text3, { color: "cyan", children: [
|
|
2951
|
+
prompt,
|
|
2952
|
+
": ",
|
|
2953
|
+
sanitizeForTty(draft),
|
|
2954
|
+
"\u258C"
|
|
2955
|
+
] }) : /* @__PURE__ */ jsx3(Text3, { dimColor: true, children: busy ? "working\u2026" : "j/k select \xB7 r raise \xB7 x ack selected \xB7 X ack all \xB7 Esc close" })
|
|
2956
|
+
] });
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
// src/ui/ChannelSwitcher.tsx
|
|
2960
|
+
import { Box as Box3, Text as Text4 } from "ink";
|
|
2961
|
+
import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
|
|
2233
2962
|
function channelLabel(ch) {
|
|
2234
2963
|
return `${channelSigil(ch.kind)}${sanitizeLabel(ch.name)}`;
|
|
2235
2964
|
}
|
|
@@ -2244,7 +2973,7 @@ function ChannelBadge({
|
|
|
2244
2973
|
if (att) {
|
|
2245
2974
|
const color = att.highestLevel === "critical" ? "red" : att.highestLevel === "action_required" ? "yellow" : "cyan";
|
|
2246
2975
|
const sigil = att.highestLevel === "critical" ? "!" : att.highestLevel === "action_required" ? "\u25B2" : "\u2022";
|
|
2247
|
-
return /* @__PURE__ */
|
|
2976
|
+
return /* @__PURE__ */ jsxs4(Text4, { color, children: [
|
|
2248
2977
|
" ",
|
|
2249
2978
|
"[",
|
|
2250
2979
|
sigil,
|
|
@@ -2252,11 +2981,11 @@ function ChannelBadge({
|
|
|
2252
2981
|
"]"
|
|
2253
2982
|
] });
|
|
2254
2983
|
}
|
|
2255
|
-
const
|
|
2256
|
-
if (
|
|
2257
|
-
return /* @__PURE__ */
|
|
2984
|
+
const count2 = unread[channelId] ?? 0;
|
|
2985
|
+
if (count2 > 0) {
|
|
2986
|
+
return /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2258
2987
|
" (",
|
|
2259
|
-
|
|
2988
|
+
count2,
|
|
2260
2989
|
")"
|
|
2261
2990
|
] });
|
|
2262
2991
|
}
|
|
@@ -2273,11 +3002,11 @@ function ChannelSwitcher({
|
|
|
2273
3002
|
const sorted = sortChannelsByPriority(filtered, { attention, unread });
|
|
2274
3003
|
const visible = sorted.slice(0, 9);
|
|
2275
3004
|
const hidden = sorted.length - visible.length;
|
|
2276
|
-
return /* @__PURE__ */
|
|
2277
|
-
/* @__PURE__ */
|
|
3005
|
+
return /* @__PURE__ */ jsxs4(Box3, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
3006
|
+
/* @__PURE__ */ jsxs4(Text4, { bold: true, children: [
|
|
2278
3007
|
"channels",
|
|
2279
3008
|
" ",
|
|
2280
|
-
/* @__PURE__ */
|
|
3009
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2281
3010
|
"(",
|
|
2282
3011
|
sorted.length,
|
|
2283
3012
|
"/",
|
|
@@ -2285,23 +3014,23 @@ function ChannelSwitcher({
|
|
|
2285
3014
|
")"
|
|
2286
3015
|
] })
|
|
2287
3016
|
] }),
|
|
2288
|
-
/* @__PURE__ */
|
|
2289
|
-
/* @__PURE__ */
|
|
2290
|
-
/* @__PURE__ */
|
|
2291
|
-
/* @__PURE__ */
|
|
3017
|
+
/* @__PURE__ */ jsxs4(Text4, { children: [
|
|
3018
|
+
/* @__PURE__ */ jsx4(Text4, { color: "cyan", children: "filter: " }),
|
|
3019
|
+
/* @__PURE__ */ jsx4(Text4, { children: sanitizeLabel(filter, 40) }),
|
|
3020
|
+
/* @__PURE__ */ jsx4(Text4, { inverse: true, children: " " })
|
|
2292
3021
|
] }),
|
|
2293
|
-
visible.map((ch, i) => /* @__PURE__ */
|
|
2294
|
-
/* @__PURE__ */
|
|
3022
|
+
visible.map((ch, i) => /* @__PURE__ */ jsxs4(Text4, { children: [
|
|
3023
|
+
/* @__PURE__ */ jsxs4(Text4, { color: "cyan", children: [
|
|
2295
3024
|
i + 1,
|
|
2296
3025
|
" "
|
|
2297
3026
|
] }),
|
|
2298
|
-
/* @__PURE__ */
|
|
2299
|
-
/* @__PURE__ */
|
|
3027
|
+
/* @__PURE__ */ jsx4(Text4, { bold: ch.id === activeChannelId, children: channelLabel(ch) }),
|
|
3028
|
+
/* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2300
3029
|
" \xB7 ",
|
|
2301
3030
|
ch.members.length,
|
|
2302
3031
|
" members"
|
|
2303
3032
|
] }),
|
|
2304
|
-
/* @__PURE__ */
|
|
3033
|
+
/* @__PURE__ */ jsx4(
|
|
2305
3034
|
ChannelBadge,
|
|
2306
3035
|
{
|
|
2307
3036
|
channelId: ch.id,
|
|
@@ -2311,36 +3040,36 @@ function ChannelSwitcher({
|
|
|
2311
3040
|
}
|
|
2312
3041
|
)
|
|
2313
3042
|
] }, ch.id)),
|
|
2314
|
-
hidden > 0 ? /* @__PURE__ */
|
|
3043
|
+
hidden > 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2315
3044
|
"\u2026",
|
|
2316
3045
|
hidden,
|
|
2317
3046
|
" more \u2014 keep typing to narrow"
|
|
2318
3047
|
] }) : null,
|
|
2319
|
-
sorted.length === 0 ? /* @__PURE__ */
|
|
3048
|
+
sorted.length === 0 ? /* @__PURE__ */ jsxs4(Text4, { dimColor: true, children: [
|
|
2320
3049
|
'no channels match "',
|
|
2321
3050
|
sanitizeLabel(filter, 20),
|
|
2322
3051
|
'"'
|
|
2323
3052
|
] }) : null,
|
|
2324
|
-
/* @__PURE__ */
|
|
3053
|
+
/* @__PURE__ */ jsx4(Text4, { dimColor: true, children: filter.length === 0 ? "type to filter \xB7 1\u20139 quick-select \xB7 n jump \xB7 Esc cancel" : "keep typing to narrow \xB7 Enter opens top match \xB7 Esc cancel" })
|
|
2325
3054
|
] });
|
|
2326
3055
|
}
|
|
2327
3056
|
|
|
2328
3057
|
// src/ui/Composer.tsx
|
|
2329
|
-
import { Box as
|
|
2330
|
-
import { jsx as
|
|
3058
|
+
import { Box as Box4, Text as Text5 } from "ink";
|
|
3059
|
+
import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
|
|
2331
3060
|
function Composer({ draft, focused }) {
|
|
2332
3061
|
const width = Math.max((process.stdout.columns ?? 80) - 8, 20);
|
|
2333
3062
|
const visible = draft.length > width ? "\u2026" + draft.slice(-(width - 1)) : draft;
|
|
2334
|
-
return /* @__PURE__ */
|
|
2335
|
-
/* @__PURE__ */
|
|
2336
|
-
draft.length === 0 && !focused ? /* @__PURE__ */
|
|
2337
|
-
focused ? /* @__PURE__ */
|
|
3063
|
+
return /* @__PURE__ */ jsx5(Box4, { borderStyle: "round", borderColor: focused ? "cyan" : "gray", paddingX: 1, width: "100%", children: /* @__PURE__ */ jsxs5(Text5, { wrap: "truncate", children: [
|
|
3064
|
+
/* @__PURE__ */ jsx5(Text5, { color: focused ? "cyan" : "gray", children: "> " }),
|
|
3065
|
+
draft.length === 0 && !focused ? /* @__PURE__ */ jsx5(Text5, { dimColor: true, children: "i to compose" }) : /* @__PURE__ */ jsx5(Text5, { children: visible }),
|
|
3066
|
+
focused ? /* @__PURE__ */ jsx5(Text5, { inverse: true, children: " " }) : null
|
|
2338
3067
|
] }) });
|
|
2339
3068
|
}
|
|
2340
3069
|
|
|
2341
3070
|
// src/ui/Header.tsx
|
|
2342
|
-
import { Box as
|
|
2343
|
-
import { jsx as
|
|
3071
|
+
import { Box as Box5, Text as Text6 } from "ink";
|
|
3072
|
+
import { jsx as jsx6, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2344
3073
|
var STATUS_DOT = {
|
|
2345
3074
|
online: { glyph: "\u25CF", color: "green" },
|
|
2346
3075
|
away: { glyph: "\u25D0", color: "yellow" },
|
|
@@ -2357,32 +3086,32 @@ function Header({
|
|
|
2357
3086
|
members,
|
|
2358
3087
|
presence
|
|
2359
3088
|
}) {
|
|
2360
|
-
return /* @__PURE__ */
|
|
2361
|
-
/* @__PURE__ */
|
|
2362
|
-
/* @__PURE__ */
|
|
2363
|
-
/* @__PURE__ */
|
|
2364
|
-
/* @__PURE__ */
|
|
2365
|
-
/* @__PURE__ */
|
|
3089
|
+
return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", children: [
|
|
3090
|
+
/* @__PURE__ */ jsxs6(Box5, { justifyContent: "space-between", children: [
|
|
3091
|
+
/* @__PURE__ */ jsxs6(Text6, { children: [
|
|
3092
|
+
/* @__PURE__ */ jsx6(Text6, { color: "cyan", bold: true, children: "arp-tui" }),
|
|
3093
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: " \u25B8 " }),
|
|
3094
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
|
|
2366
3095
|
channelSigil(channelKind),
|
|
2367
3096
|
sanitizeLabel(channelName)
|
|
2368
3097
|
] })
|
|
2369
3098
|
] }),
|
|
2370
|
-
/* @__PURE__ */
|
|
3099
|
+
/* @__PURE__ */ jsx6(Text6, { children: members.map((m) => {
|
|
2371
3100
|
const dot = STATUS_DOT[effectiveStatus(m, presence)];
|
|
2372
|
-
return /* @__PURE__ */
|
|
2373
|
-
/* @__PURE__ */
|
|
3101
|
+
return /* @__PURE__ */ jsxs6(Text6, { children: [
|
|
3102
|
+
/* @__PURE__ */ jsxs6(Text6, { color: dot.color, children: [
|
|
2374
3103
|
dot.glyph,
|
|
2375
3104
|
" "
|
|
2376
3105
|
] }),
|
|
2377
|
-
/* @__PURE__ */
|
|
3106
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: effectiveStatus(m, presence) === "offline", children: [
|
|
2378
3107
|
m.type === "bot" ? "\u2699" : "",
|
|
2379
3108
|
sanitizeLabel(m.name, 20)
|
|
2380
3109
|
] }),
|
|
2381
|
-
/* @__PURE__ */
|
|
3110
|
+
/* @__PURE__ */ jsx6(Text6, { children: " " })
|
|
2382
3111
|
] }, m.id);
|
|
2383
3112
|
}) })
|
|
2384
3113
|
] }),
|
|
2385
|
-
/* @__PURE__ */
|
|
3114
|
+
/* @__PURE__ */ jsx6(Text6, { dimColor: true, children: "\u2500".repeat(Math.min(process.stdout.columns ?? 80, 100)) })
|
|
2386
3115
|
] });
|
|
2387
3116
|
}
|
|
2388
3117
|
|
|
@@ -2390,8 +3119,8 @@ function Header({
|
|
|
2390
3119
|
import { Static } from "ink";
|
|
2391
3120
|
|
|
2392
3121
|
// src/ui/MessageRow.tsx
|
|
2393
|
-
import { Box as
|
|
2394
|
-
import { jsx as
|
|
3122
|
+
import { Box as Box6, Text as Text7 } from "ink";
|
|
3123
|
+
import { jsx as jsx7, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
2395
3124
|
function hhmm(rfc3339) {
|
|
2396
3125
|
const d = new Date(rfc3339);
|
|
2397
3126
|
return isNaN(d.getTime()) ? "--:--" : `${String(d.getHours()).padStart(2, "0")}:${String(d.getMinutes()).padStart(2, "0")}`;
|
|
@@ -2404,7 +3133,7 @@ function TokenChip({ message }) {
|
|
|
2404
3133
|
if (message.messageType !== "agent" || !message.tokensUsed) return null;
|
|
2405
3134
|
const parts = [`${formatTokens(message.tokensUsed)} tok`];
|
|
2406
3135
|
if (message.model) parts.push(sanitizeLabel(message.model, 16));
|
|
2407
|
-
return /* @__PURE__ */
|
|
3136
|
+
return /* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2408
3137
|
message.verified ? "\u2713 " : "",
|
|
2409
3138
|
parts.join(" \xB7 ")
|
|
2410
3139
|
] });
|
|
@@ -2414,41 +3143,41 @@ function MessageRow({ message }) {
|
|
|
2414
3143
|
const isSystem = message.messageType === "system";
|
|
2415
3144
|
const name = sanitizeLabel(message.agentName, 20);
|
|
2416
3145
|
if (isSystem) {
|
|
2417
|
-
return /* @__PURE__ */
|
|
3146
|
+
return /* @__PURE__ */ jsx7(Box6, { children: /* @__PURE__ */ jsxs7(Text7, { dimColor: true, italic: true, children: [
|
|
2418
3147
|
" ",
|
|
2419
3148
|
"\u2500\u2500 ",
|
|
2420
3149
|
sanitizeForTty(message.content),
|
|
2421
3150
|
" \u2500\u2500"
|
|
2422
3151
|
] }) });
|
|
2423
3152
|
}
|
|
2424
|
-
return /* @__PURE__ */
|
|
2425
|
-
/* @__PURE__ */
|
|
2426
|
-
/* @__PURE__ */
|
|
3153
|
+
return /* @__PURE__ */ jsxs7(Box6, { justifyContent: "space-between", gap: 2, children: [
|
|
3154
|
+
/* @__PURE__ */ jsx7(Box6, { flexShrink: 1, children: /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
3155
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2427
3156
|
hhmm(message.createdAt),
|
|
2428
3157
|
" "
|
|
2429
3158
|
] }),
|
|
2430
|
-
/* @__PURE__ */
|
|
3159
|
+
/* @__PURE__ */ jsxs7(Text7, { color: isBot ? "magenta" : "cyan", bold: !isBot, children: [
|
|
2431
3160
|
isBot ? "\u2699" : "",
|
|
2432
3161
|
name.padEnd(12)
|
|
2433
3162
|
] }),
|
|
2434
|
-
/* @__PURE__ */
|
|
3163
|
+
/* @__PURE__ */ jsxs7(Text7, { children: [
|
|
2435
3164
|
" ",
|
|
2436
3165
|
sanitizeForTty(message.content)
|
|
2437
3166
|
] })
|
|
2438
3167
|
] }) }),
|
|
2439
|
-
/* @__PURE__ */
|
|
3168
|
+
/* @__PURE__ */ jsx7(Box6, { flexShrink: 0, children: /* @__PURE__ */ jsx7(TokenChip, { message }) })
|
|
2440
3169
|
] });
|
|
2441
3170
|
}
|
|
2442
3171
|
|
|
2443
3172
|
// src/ui/MessageList.tsx
|
|
2444
|
-
import { jsx as
|
|
3173
|
+
import { jsx as jsx8 } from "react/jsx-runtime";
|
|
2445
3174
|
function MessageList({ messages }) {
|
|
2446
|
-
return /* @__PURE__ */
|
|
3175
|
+
return /* @__PURE__ */ jsx8(Static, { items: messages, children: (m) => /* @__PURE__ */ jsx8(MessageRow, { message: m }, m.id) });
|
|
2447
3176
|
}
|
|
2448
3177
|
|
|
2449
3178
|
// src/ui/NotificationsPanel.tsx
|
|
2450
|
-
import { Box as
|
|
2451
|
-
import { jsx as
|
|
3179
|
+
import { Box as Box7, Text as Text8 } from "ink";
|
|
3180
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2452
3181
|
function relativeAge(createdAt) {
|
|
2453
3182
|
const ms = Date.now() - new Date(createdAt).getTime();
|
|
2454
3183
|
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
@@ -2466,11 +3195,11 @@ function NotificationsPanel({
|
|
|
2466
3195
|
notifSelected
|
|
2467
3196
|
}) {
|
|
2468
3197
|
const unreadCount = notifications.filter((n) => !n.readAt).length;
|
|
2469
|
-
return /* @__PURE__ */
|
|
2470
|
-
/* @__PURE__ */
|
|
3198
|
+
return /* @__PURE__ */ jsxs8(Box7, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3199
|
+
/* @__PURE__ */ jsxs8(Text8, { bold: true, children: [
|
|
2471
3200
|
"alerts",
|
|
2472
3201
|
" ",
|
|
2473
|
-
/* @__PURE__ */
|
|
3202
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2474
3203
|
"(",
|
|
2475
3204
|
unreadCount,
|
|
2476
3205
|
" unread \xB7 ",
|
|
@@ -2478,38 +3207,42 @@ function NotificationsPanel({
|
|
|
2478
3207
|
" total)"
|
|
2479
3208
|
] })
|
|
2480
3209
|
] }),
|
|
2481
|
-
notifications.length === 0 ? /* @__PURE__ */
|
|
3210
|
+
notifications.length === 0 ? /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "no notifications" }) : notifications.map((n, i) => {
|
|
2482
3211
|
const isSelected = i === notifSelected;
|
|
2483
3212
|
const isUnread = n.readAt === null;
|
|
2484
3213
|
const type = sanitizeLabel(n.type, 20);
|
|
2485
3214
|
const ref = shortRef(sanitizeForTty(n.entityRef));
|
|
2486
3215
|
const age = relativeAge(n.createdAt);
|
|
2487
|
-
return /* @__PURE__ */
|
|
2488
|
-
isUnread ? /* @__PURE__ */
|
|
2489
|
-
/* @__PURE__ */
|
|
2490
|
-
/* @__PURE__ */
|
|
3216
|
+
return /* @__PURE__ */ jsxs8(Text8, { inverse: isSelected, children: [
|
|
3217
|
+
isUnread ? /* @__PURE__ */ jsx9(Text8, { color: "yellow", children: "\u2022 " }) : /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: " " }),
|
|
3218
|
+
/* @__PURE__ */ jsx9(Text8, { bold: isSelected, children: type }),
|
|
3219
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2491
3220
|
" ",
|
|
2492
3221
|
ref
|
|
2493
3222
|
] }),
|
|
2494
|
-
/* @__PURE__ */
|
|
3223
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2495
3224
|
" ",
|
|
2496
3225
|
age
|
|
2497
3226
|
] })
|
|
2498
3227
|
] }, n.id);
|
|
2499
3228
|
}),
|
|
2500
|
-
/* @__PURE__ */
|
|
3229
|
+
/* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "J/K move \xB7 d dismiss \xB7 a mark all read \xB7 Esc close" })
|
|
2501
3230
|
] });
|
|
2502
3231
|
}
|
|
2503
3232
|
|
|
2504
3233
|
// src/ui/StatusBar.tsx
|
|
2505
|
-
import { Text as
|
|
2506
|
-
import { Fragment, jsx as
|
|
3234
|
+
import { Text as Text9 } from "ink";
|
|
3235
|
+
import { Fragment, jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2507
3236
|
var MODE_LABEL = {
|
|
2508
3237
|
normal: "NORMAL",
|
|
2509
3238
|
insert: "INSERT",
|
|
2510
3239
|
switcher: "CHANNELS",
|
|
2511
3240
|
token: "TOKEN",
|
|
2512
|
-
notifications: "ALERTS"
|
|
3241
|
+
notifications: "ALERTS",
|
|
3242
|
+
attention: "ATTENTION",
|
|
3243
|
+
attention_level: "ATTENTION LEVEL",
|
|
3244
|
+
attention_reason: "ATTENTION REASON",
|
|
3245
|
+
attention_confirm: "CONFIRM ATTENTION"
|
|
2513
3246
|
};
|
|
2514
3247
|
function ageLabel(obtainedAt) {
|
|
2515
3248
|
if (!obtainedAt) return "\u2014";
|
|
@@ -2560,41 +3293,41 @@ function StatusBar({
|
|
|
2560
3293
|
color: stale ? "yellow" : "green"
|
|
2561
3294
|
};
|
|
2562
3295
|
}
|
|
2563
|
-
return /* @__PURE__ */
|
|
2564
|
-
/* @__PURE__ */
|
|
3296
|
+
return /* @__PURE__ */ jsxs9(Text9, { children: [
|
|
3297
|
+
/* @__PURE__ */ jsxs9(Text9, { bold: true, color: mode === "insert" ? "cyan" : mode === "token" ? "yellow" : "white", children: [
|
|
2565
3298
|
" ",
|
|
2566
3299
|
MODE_LABEL[mode],
|
|
2567
3300
|
" "
|
|
2568
3301
|
] }),
|
|
2569
|
-
/* @__PURE__ */
|
|
2570
|
-
/* @__PURE__ */
|
|
2571
|
-
/* @__PURE__ */
|
|
2572
|
-
/* @__PURE__ */
|
|
2573
|
-
/* @__PURE__ */
|
|
3302
|
+
/* @__PURE__ */ jsx10(Text9, { dimColor: true, children: "\u2502 " }),
|
|
3303
|
+
/* @__PURE__ */ jsx10(Text9, { color: connColor, children: connectionLabel(connection, reconnectAt, reconnectAttempt) }),
|
|
3304
|
+
/* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 " }),
|
|
3305
|
+
/* @__PURE__ */ jsx10(Text9, { color: tokenSeg.color, children: tokenSeg.text }),
|
|
3306
|
+
/* @__PURE__ */ jsxs9(Text9, { dimColor: true, children: [
|
|
2574
3307
|
" \u2502 seq ",
|
|
2575
3308
|
lastSeq ?? "\u2014"
|
|
2576
3309
|
] }),
|
|
2577
|
-
notifCount > 0 ? /* @__PURE__ */
|
|
3310
|
+
notifCount > 0 ? /* @__PURE__ */ jsxs9(Text9, { color: "yellow", children: [
|
|
2578
3311
|
" \u2502 alerts:",
|
|
2579
3312
|
notifCount
|
|
2580
3313
|
] }) : null,
|
|
2581
3314
|
error ? (
|
|
2582
3315
|
// BL-222: an error replaces the full hint line but must never strand
|
|
2583
3316
|
// the user — keep the exit hint visible alongside it
|
|
2584
|
-
/* @__PURE__ */
|
|
2585
|
-
/* @__PURE__ */
|
|
3317
|
+
/* @__PURE__ */ jsxs9(Fragment, { children: [
|
|
3318
|
+
/* @__PURE__ */ jsxs9(Text9, { color: "red", children: [
|
|
2586
3319
|
" \u2502 ",
|
|
2587
3320
|
sanitizeForTty(error)
|
|
2588
3321
|
] }),
|
|
2589
|
-
/* @__PURE__ */
|
|
3322
|
+
/* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 q quit" })
|
|
2590
3323
|
] })
|
|
2591
|
-
) : /* @__PURE__ */
|
|
3324
|
+
) : /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: " \u2502 c channels \xB7 a alerts \xB7 t attention \xB7 n next \xB7 v watch \xB7 i compose \xB7 r reconnect \xB7 Esc normal \xB7 q quit" })
|
|
2592
3325
|
] });
|
|
2593
3326
|
}
|
|
2594
3327
|
|
|
2595
3328
|
// src/ui/TokenPrompt.tsx
|
|
2596
|
-
import { Box as
|
|
2597
|
-
import { jsx as
|
|
3329
|
+
import { Box as Box8, Text as Text10 } from "ink";
|
|
3330
|
+
import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
|
|
2598
3331
|
function TokenPrompt({
|
|
2599
3332
|
value,
|
|
2600
3333
|
reason,
|
|
@@ -2602,26 +3335,26 @@ function TokenPrompt({
|
|
|
2602
3335
|
jwtTemplate
|
|
2603
3336
|
}) {
|
|
2604
3337
|
const masked = value.length === 0 ? "" : value.length <= 16 ? value : `${value.slice(0, 12)}\u2026 (${value.length} chars)`;
|
|
2605
|
-
return /* @__PURE__ */
|
|
2606
|
-
/* @__PURE__ */
|
|
2607
|
-
reason ? /* @__PURE__ */
|
|
2608
|
-
/* @__PURE__ */
|
|
3338
|
+
return /* @__PURE__ */ jsxs10(Box8, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
3339
|
+
/* @__PURE__ */ jsx11(Text10, { bold: true, color: "yellow", children: "token needed" }),
|
|
3340
|
+
reason ? /* @__PURE__ */ jsx11(Text10, { dimColor: true, children: sanitizeForTty(reason) }) : null,
|
|
3341
|
+
/* @__PURE__ */ jsxs10(Text10, { dimColor: true, children: [
|
|
2609
3342
|
"mint one: open ",
|
|
2610
3343
|
webUrl,
|
|
2611
3344
|
" logged in, browser console:"
|
|
2612
3345
|
] }),
|
|
2613
|
-
/* @__PURE__ */
|
|
2614
|
-
/* @__PURE__ */
|
|
2615
|
-
/* @__PURE__ */
|
|
2616
|
-
/* @__PURE__ */
|
|
2617
|
-
/* @__PURE__ */
|
|
3346
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: ` await window.Clerk.session.getToken({template:'${jwtTemplate}'})` }),
|
|
3347
|
+
/* @__PURE__ */ jsxs10(Text10, { children: [
|
|
3348
|
+
/* @__PURE__ */ jsx11(Text10, { color: "yellow", children: "> " }),
|
|
3349
|
+
/* @__PURE__ */ jsx11(Text10, { children: masked }),
|
|
3350
|
+
/* @__PURE__ */ jsx11(Text10, { inverse: true, children: " " })
|
|
2618
3351
|
] }),
|
|
2619
|
-
/* @__PURE__ */
|
|
3352
|
+
/* @__PURE__ */ jsx11(Text10, { dimColor: true, children: "paste, then Enter \xB7 Ctrl+C quits" })
|
|
2620
3353
|
] });
|
|
2621
3354
|
}
|
|
2622
3355
|
|
|
2623
3356
|
// src/app.tsx
|
|
2624
|
-
import { jsx as
|
|
3357
|
+
import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
|
|
2625
3358
|
var ACTIVITY_SETTLE_MS2 = 4e3;
|
|
2626
3359
|
function cleanInput(input) {
|
|
2627
3360
|
return input.replace(/\r\n|[\r\n]/g, " ").replace(/[\x00-\x1f\x7f]/g, "");
|
|
@@ -2668,6 +3401,22 @@ function App({ store: store2 }) {
|
|
|
2668
3401
|
} else if (input === "a") store2.markAllNotificationsRead();
|
|
2669
3402
|
return;
|
|
2670
3403
|
}
|
|
3404
|
+
if (s.mode.startsWith("attention")) {
|
|
3405
|
+
if (s.mode !== "attention") {
|
|
3406
|
+
if (key.escape) store2.closeAttention();
|
|
3407
|
+
else if (key.return) store2.submitAttentionInput();
|
|
3408
|
+
else if (key.backspace || key.delete) store2.setAttentionDraft(s.attentionDraft.slice(0, -1));
|
|
3409
|
+
else if (input) store2.setAttentionDraft(s.attentionDraft + cleanInput(input));
|
|
3410
|
+
return;
|
|
3411
|
+
}
|
|
3412
|
+
if (key.escape) store2.closeAttention();
|
|
3413
|
+
else if (input === "j" || key.downArrow) store2.moveAttentionSelection(1);
|
|
3414
|
+
else if (input === "k" || key.upArrow) store2.moveAttentionSelection(-1);
|
|
3415
|
+
else if (input === "r") store2.beginAttentionRaise();
|
|
3416
|
+
else if (input === "x") store2.beginAttentionAck(false);
|
|
3417
|
+
else if (input === "X") store2.beginAttentionAck(true);
|
|
3418
|
+
return;
|
|
3419
|
+
}
|
|
2671
3420
|
if (s.mode === "switcher") {
|
|
2672
3421
|
const filtered = filterChannels(s.channels, s.switcherFilter);
|
|
2673
3422
|
const sorted = sortChannelsByPriority(filtered, { attention: s.attention, unread: s.unread });
|
|
@@ -2691,6 +3440,7 @@ function App({ store: store2 }) {
|
|
|
2691
3440
|
exit();
|
|
2692
3441
|
} else if (input === "c") store2.setMode("switcher");
|
|
2693
3442
|
else if (input === "a") store2.setMode("notifications");
|
|
3443
|
+
else if (input === "t") store2.openAttention();
|
|
2694
3444
|
else if (input === "i" || key.return) store2.setMode("insert");
|
|
2695
3445
|
else if (input === "r") store2.reconnectNow();
|
|
2696
3446
|
else if (input === "n") store2.jumpNextActive();
|
|
@@ -2708,8 +3458,8 @@ function App({ store: store2 }) {
|
|
|
2708
3458
|
const activityLive = activityRows.some((r) => r.status === "pending" || r.status === "in_progress") || activityPlan.some((e) => e.status !== "completed");
|
|
2709
3459
|
const activityWithinSettle = state.activityIdleSince !== null && Date.now() - state.activityIdleSince < ACTIVITY_SETTLE_MS2;
|
|
2710
3460
|
const activityVisible = state.activityPinned || activityLive || activityWithinSettle;
|
|
2711
|
-
return /* @__PURE__ */
|
|
2712
|
-
/* @__PURE__ */
|
|
3461
|
+
return /* @__PURE__ */ jsxs11(Box9, { flexDirection: "column", children: [
|
|
3462
|
+
/* @__PURE__ */ jsx12(
|
|
2713
3463
|
Header,
|
|
2714
3464
|
{
|
|
2715
3465
|
channelName: activeChannel?.name ?? "(connecting\u2026)",
|
|
@@ -2718,9 +3468,9 @@ function App({ store: store2 }) {
|
|
|
2718
3468
|
presence: state.presence
|
|
2719
3469
|
}
|
|
2720
3470
|
),
|
|
2721
|
-
/* @__PURE__ */
|
|
2722
|
-
/* @__PURE__ */
|
|
2723
|
-
state.mode === "switcher" ? /* @__PURE__ */
|
|
3471
|
+
/* @__PURE__ */ jsx12(MessageList, { messages: state.messages }),
|
|
3472
|
+
/* @__PURE__ */ jsx12(ActivitySurface, { toolCalls: activityRows, plan: activityPlan, visible: activityVisible, pinned: state.activityPinned }),
|
|
3473
|
+
state.mode === "switcher" ? /* @__PURE__ */ jsx12(
|
|
2724
3474
|
ChannelSwitcher,
|
|
2725
3475
|
{
|
|
2726
3476
|
channels: state.channels,
|
|
@@ -2730,14 +3480,25 @@ function App({ store: store2 }) {
|
|
|
2730
3480
|
unread: state.unread
|
|
2731
3481
|
}
|
|
2732
3482
|
) : null,
|
|
2733
|
-
state.mode === "notifications" ? /* @__PURE__ */
|
|
3483
|
+
state.mode === "notifications" ? /* @__PURE__ */ jsx12(
|
|
2734
3484
|
NotificationsPanel,
|
|
2735
3485
|
{
|
|
2736
3486
|
notifications: state.notifications,
|
|
2737
3487
|
notifSelected: state.notifSelected
|
|
2738
3488
|
}
|
|
2739
3489
|
) : null,
|
|
2740
|
-
state.mode
|
|
3490
|
+
state.mode.startsWith("attention") && state.activeChannelId ? /* @__PURE__ */ jsx12(
|
|
3491
|
+
AttentionPanel,
|
|
3492
|
+
{
|
|
3493
|
+
channelId: state.activeChannelId,
|
|
3494
|
+
items: state.channelAttention,
|
|
3495
|
+
selected: state.attentionSelected,
|
|
3496
|
+
mode: state.mode,
|
|
3497
|
+
draft: state.attentionDraft,
|
|
3498
|
+
busy: state.attentionBusy
|
|
3499
|
+
}
|
|
3500
|
+
) : null,
|
|
3501
|
+
state.mode === "token" ? /* @__PURE__ */ jsx12(
|
|
2741
3502
|
TokenPrompt,
|
|
2742
3503
|
{
|
|
2743
3504
|
value: state.tokenPrompt,
|
|
@@ -2746,9 +3507,9 @@ function App({ store: store2 }) {
|
|
|
2746
3507
|
jwtTemplate: store2.instance.clerkJwtTemplate
|
|
2747
3508
|
}
|
|
2748
3509
|
) : null,
|
|
2749
|
-
/* @__PURE__ */
|
|
2750
|
-
/* @__PURE__ */
|
|
2751
|
-
/* @__PURE__ */
|
|
3510
|
+
/* @__PURE__ */ jsx12(ActivityLine, { activities: state.activities }),
|
|
3511
|
+
/* @__PURE__ */ jsx12(Composer, { draft: state.draft, focused: state.mode === "insert" }),
|
|
3512
|
+
/* @__PURE__ */ jsx12(
|
|
2752
3513
|
StatusBar,
|
|
2753
3514
|
{
|
|
2754
3515
|
mode: state.mode,
|
|
@@ -2764,7 +3525,7 @@ function App({ store: store2 }) {
|
|
|
2764
3525
|
error: state.error
|
|
2765
3526
|
}
|
|
2766
3527
|
),
|
|
2767
|
-
!interactive ? /* @__PURE__ */
|
|
3528
|
+
!interactive ? /* @__PURE__ */ jsx12(Text11, { dimColor: true, children: "(non-interactive terminal: keys disabled \u2014 tail is read-only here)" }) : null
|
|
2768
3529
|
] });
|
|
2769
3530
|
}
|
|
2770
3531
|
|
|
@@ -2847,8 +3608,1144 @@ unknown profile command "${args2.sub}".`);
|
|
|
2847
3608
|
}
|
|
2848
3609
|
}
|
|
2849
3610
|
|
|
3611
|
+
// src/api/bridgeProcess.ts
|
|
3612
|
+
import { spawn as spawn2 } from "child_process";
|
|
3613
|
+
var DEFAULT_BRIDGE_CMD = "npx -y @snowyroad/braid";
|
|
3614
|
+
var ENROLL_TIMEOUT_MS = 12e4;
|
|
3615
|
+
var STDERR_TAIL_CHARS = 600;
|
|
3616
|
+
function resolveBridgeCmd(flag, env = process.env) {
|
|
3617
|
+
return flag ?? env["ARP_TUI_BRIDGE_CMD"] ?? DEFAULT_BRIDGE_CMD;
|
|
3618
|
+
}
|
|
3619
|
+
function splitCmd(cmd) {
|
|
3620
|
+
return cmd.trim().split(/\s+/).filter(Boolean);
|
|
3621
|
+
}
|
|
3622
|
+
function enrollArgv(bridgeCmd) {
|
|
3623
|
+
return [...splitCmd(bridgeCmd), "enroll", "--invite-stdin", "--json"];
|
|
3624
|
+
}
|
|
3625
|
+
function startArgv(bridgeCmd, name, provider) {
|
|
3626
|
+
return [...splitCmd(bridgeCmd), "start", name, ...provider ? ["--provider", provider] : []];
|
|
3627
|
+
}
|
|
3628
|
+
function enrollAgent(opts) {
|
|
3629
|
+
const [cmd, ...argv] = enrollArgv(opts.bridgeCmd);
|
|
3630
|
+
if (!cmd) {
|
|
3631
|
+
return Promise.resolve({ ok: false, exitCode: null, message: "empty bridge command", stderrTail: "" });
|
|
3632
|
+
}
|
|
3633
|
+
return new Promise((resolve) => {
|
|
3634
|
+
const child = spawn2(cmd, argv, { stdio: ["pipe", "pipe", "pipe"] });
|
|
3635
|
+
let stdout = "";
|
|
3636
|
+
let stderr = "";
|
|
3637
|
+
let settled = false;
|
|
3638
|
+
const timeoutMs = opts.timeoutMs ?? ENROLL_TIMEOUT_MS;
|
|
3639
|
+
const finish = (r) => {
|
|
3640
|
+
if (settled) return;
|
|
3641
|
+
settled = true;
|
|
3642
|
+
clearTimeout(timer);
|
|
3643
|
+
resolve(r);
|
|
3644
|
+
};
|
|
3645
|
+
const timer = setTimeout(() => {
|
|
3646
|
+
finish({
|
|
3647
|
+
ok: false,
|
|
3648
|
+
exitCode: null,
|
|
3649
|
+
message: `bridge enrollment timed out after ${Math.round(timeoutMs / 1e3)}s`,
|
|
3650
|
+
stderrTail: tail(stderr)
|
|
3651
|
+
});
|
|
3652
|
+
child.kill("SIGKILL");
|
|
3653
|
+
}, timeoutMs);
|
|
3654
|
+
child.on("error", (err) => {
|
|
3655
|
+
finish({
|
|
3656
|
+
ok: false,
|
|
3657
|
+
exitCode: null,
|
|
3658
|
+
message: `could not run the bridge (${cmd}): ${sanitizeForTty(err.message)}`,
|
|
3659
|
+
stderrTail: ""
|
|
3660
|
+
});
|
|
3661
|
+
});
|
|
3662
|
+
child.stdout.setEncoding("utf8");
|
|
3663
|
+
child.stdout.on("data", (d) => stdout += d);
|
|
3664
|
+
child.stderr.setEncoding("utf8");
|
|
3665
|
+
child.stderr.on("data", (d) => stderr += d);
|
|
3666
|
+
child.stdin.on("error", () => {
|
|
3667
|
+
});
|
|
3668
|
+
child.stdin.write(opts.connectBlob);
|
|
3669
|
+
child.stdin.end();
|
|
3670
|
+
child.on("close", (code) => {
|
|
3671
|
+
if (code !== 0) {
|
|
3672
|
+
finish({
|
|
3673
|
+
ok: false,
|
|
3674
|
+
exitCode: code,
|
|
3675
|
+
message: `bridge enroll exited ${code ?? "on a signal"}`,
|
|
3676
|
+
stderrTail: tail(stderr)
|
|
3677
|
+
});
|
|
3678
|
+
return;
|
|
3679
|
+
}
|
|
3680
|
+
const receipt = parseReceipt(stdout);
|
|
3681
|
+
if (!receipt) {
|
|
3682
|
+
finish({
|
|
3683
|
+
ok: false,
|
|
3684
|
+
exitCode: 0,
|
|
3685
|
+
message: "the bridge exited 0 but printed no parseable enrollment receipt",
|
|
3686
|
+
stderrTail: tail(stderr)
|
|
3687
|
+
});
|
|
3688
|
+
return;
|
|
3689
|
+
}
|
|
3690
|
+
finish({ ok: true, receipt });
|
|
3691
|
+
});
|
|
3692
|
+
});
|
|
3693
|
+
}
|
|
3694
|
+
function tail(stderr) {
|
|
3695
|
+
return sanitizeForTty(stderr).trim().slice(-STDERR_TAIL_CHARS);
|
|
3696
|
+
}
|
|
3697
|
+
function parseReceipt(stdout) {
|
|
3698
|
+
const lines = stdout.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3699
|
+
for (let i = lines.length - 1; i >= 0; i--) {
|
|
3700
|
+
const line = lines[i];
|
|
3701
|
+
if (!line.startsWith("{")) continue;
|
|
3702
|
+
try {
|
|
3703
|
+
const data = JSON.parse(line);
|
|
3704
|
+
if (data && typeof data === "object" && data["ok"] === true && typeof data["agentName"] === "string" && typeof data["agentUuid"] === "string") {
|
|
3705
|
+
return {
|
|
3706
|
+
ok: true,
|
|
3707
|
+
agentName: data["agentName"],
|
|
3708
|
+
agentUuid: data["agentUuid"],
|
|
3709
|
+
relayUrl: typeof data["relayUrl"] === "string" ? data["relayUrl"] : "",
|
|
3710
|
+
keystoreFile: typeof data["keystoreFile"] === "string" ? data["keystoreFile"] : ""
|
|
3711
|
+
};
|
|
3712
|
+
}
|
|
3713
|
+
} catch {
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
return null;
|
|
3717
|
+
}
|
|
3718
|
+
function startAgentProcess(opts) {
|
|
3719
|
+
const [cmd, ...argv] = startArgv(opts.bridgeCmd, opts.name, opts.provider);
|
|
3720
|
+
if (!cmd) return Promise.resolve(1);
|
|
3721
|
+
return new Promise((resolve) => {
|
|
3722
|
+
const child = spawn2(cmd, argv, { stdio: "inherit" });
|
|
3723
|
+
child.on("error", () => resolve(1));
|
|
3724
|
+
child.on("close", (code) => resolve(code ?? 1));
|
|
3725
|
+
});
|
|
3726
|
+
}
|
|
3727
|
+
|
|
3728
|
+
// src/commands/agent.ts
|
|
3729
|
+
import readline from "readline/promises";
|
|
3730
|
+
var AGENT_USAGE = `usage: braid agent <add|status|reissue|revoke|remove> <name> [options]
|
|
3731
|
+
|
|
3732
|
+
add <name> create a personal agent on Braid, enroll it on this machine
|
|
3733
|
+
via the local bridge, and start it (Ctrl-C stops it)
|
|
3734
|
+
status <name> detail card: presence, credential, health, service mode
|
|
3735
|
+
reissue <name> mint a fresh credential and re-enroll here \u2014 recovers an
|
|
3736
|
+
agent whose credential is broken or revoked
|
|
3737
|
+
revoke <name> revoke the agent's credential \u2014 it cannot connect until reissued
|
|
3738
|
+
remove <name> permanently delete the agent from Braid
|
|
3739
|
+
|
|
3740
|
+
--provider <p> agent CLI the bridge runs (claude-code, codex, gemini, \u2026)
|
|
3741
|
+
--no-start enroll only \u2014 prints the exact start command instead
|
|
3742
|
+
--bridge-cmd <cmd> bridge command to spawn (default: npx -y arp-bridge@latest;
|
|
3743
|
+
also ARP_TUI_BRIDGE_CMD env)
|
|
3744
|
+
--yes skip the confirmation prompt (required with --json on
|
|
3745
|
+
revoke/remove)
|
|
3746
|
+
--json machine-readable result on stdout`;
|
|
3747
|
+
async function promptOnTty(question) {
|
|
3748
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) return null;
|
|
3749
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
3750
|
+
try {
|
|
3751
|
+
return await rl.question(question);
|
|
3752
|
+
} finally {
|
|
3753
|
+
rl.close();
|
|
3754
|
+
}
|
|
3755
|
+
}
|
|
3756
|
+
async function ensureSignedIn(deps) {
|
|
3757
|
+
const existing = deps.resolveCreds();
|
|
3758
|
+
if (existing?.token) return existing;
|
|
3759
|
+
deps.err("you're not signed in yet \u2014 opening your browser to sign in to Braid\u2026");
|
|
3760
|
+
return deps.runLogin();
|
|
3761
|
+
}
|
|
3762
|
+
function oneShotTokenSupplier(instance2, initial, refresh = refreshGrant) {
|
|
3763
|
+
let current = initial;
|
|
3764
|
+
return async () => {
|
|
3765
|
+
const refreshable = !!current.refreshToken && boundToFapi(current.clerkFapiUrl, instance2.clerkFapiUrl);
|
|
3766
|
+
if (refreshable && needsRefresh(current.expiresAt)) {
|
|
3767
|
+
const consumed = current.refreshToken;
|
|
3768
|
+
try {
|
|
3769
|
+
const tokens = await refresh({
|
|
3770
|
+
endpoints: oauthEndpoints(instance2.clerkFapiUrl),
|
|
3771
|
+
clientId: requireOAuthClientId(instance2),
|
|
3772
|
+
refreshToken: consumed
|
|
3773
|
+
});
|
|
3774
|
+
const fields = {
|
|
3775
|
+
token: tokens.accessToken,
|
|
3776
|
+
tokenType: "oauth",
|
|
3777
|
+
// a slot holding a refresh token is ALWAYS an oauth slot
|
|
3778
|
+
obtainedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3779
|
+
expiresAt: tokens.expiresAt ?? jwtExpiry(tokens.accessToken),
|
|
3780
|
+
refreshToken: tokens.refreshToken ?? consumed
|
|
3781
|
+
};
|
|
3782
|
+
try {
|
|
3783
|
+
updateCredentials(instance2.relayUrl, (onDisk) => mergeRotation(onDisk, fields, consumed));
|
|
3784
|
+
} catch {
|
|
3785
|
+
}
|
|
3786
|
+
current = { ...current, ...fields };
|
|
3787
|
+
} catch {
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
return current.token;
|
|
3791
|
+
};
|
|
3792
|
+
}
|
|
3793
|
+
async function runAgentCli(instance2, args2, overrides = {}) {
|
|
3794
|
+
const deps = {
|
|
3795
|
+
resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
|
|
3796
|
+
runLogin: async () => (await login(instance2)).credentials,
|
|
3797
|
+
enroll: enrollAgent,
|
|
3798
|
+
startAgent: startAgentProcess,
|
|
3799
|
+
env: process.env,
|
|
3800
|
+
out: (line) => console.log(line),
|
|
3801
|
+
err: (line) => console.error(line),
|
|
3802
|
+
prompt: promptOnTty,
|
|
3803
|
+
...overrides
|
|
3804
|
+
};
|
|
3805
|
+
try {
|
|
3806
|
+
if (args2.cmd === "agents") return await runAgentsList(instance2, args2, deps);
|
|
3807
|
+
switch (args2.sub) {
|
|
3808
|
+
case "add":
|
|
3809
|
+
return await runAgentAdd(instance2, args2, deps);
|
|
3810
|
+
case "status":
|
|
3811
|
+
return await runAgentStatus(instance2, args2, deps);
|
|
3812
|
+
case "reissue":
|
|
3813
|
+
return await runAgentReissue(instance2, args2, deps);
|
|
3814
|
+
case "revoke":
|
|
3815
|
+
return await runAgentRevoke(instance2, args2, deps);
|
|
3816
|
+
case "remove":
|
|
3817
|
+
return await runAgentRemove(instance2, args2, deps);
|
|
3818
|
+
default:
|
|
3819
|
+
deps.err(AGENT_USAGE);
|
|
3820
|
+
if (args2.sub) deps.err(`
|
|
3821
|
+
unknown agent command "${sanitizeLabel(args2.sub)}".`);
|
|
3822
|
+
return 1;
|
|
3823
|
+
}
|
|
3824
|
+
} catch (err) {
|
|
3825
|
+
deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
|
|
3826
|
+
return 1;
|
|
3827
|
+
}
|
|
3828
|
+
}
|
|
3829
|
+
function oneShotRest(instance2, creds) {
|
|
3830
|
+
return new RelayRest({
|
|
3831
|
+
relayUrl: instance2.relayUrl,
|
|
3832
|
+
getToken: oneShotTokenSupplier(instance2, creds)
|
|
3833
|
+
});
|
|
3834
|
+
}
|
|
3835
|
+
function requireName2(args2, deps) {
|
|
3836
|
+
const name = args2.name?.trim();
|
|
3837
|
+
if (!name) {
|
|
3838
|
+
deps.err(`agent ${args2.sub} requires a name \u2014 usage: braid agent ${args2.sub} <name>`);
|
|
3839
|
+
return null;
|
|
3840
|
+
}
|
|
3841
|
+
return name;
|
|
3842
|
+
}
|
|
3843
|
+
async function findMyAgent(rest, name, args2, deps) {
|
|
3844
|
+
const agents = await rest.listMyAgents();
|
|
3845
|
+
const found = agents.find((a) => a.name === name);
|
|
3846
|
+
if (found) return found;
|
|
3847
|
+
const msg = `no agent named "${sanitizeLabel(name)}"`;
|
|
3848
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3849
|
+
const known = agents.map((a) => sanitizeLabel(a.name ?? "")).filter(Boolean);
|
|
3850
|
+
deps.err(
|
|
3851
|
+
`\u2717 ${msg}${known.length > 0 ? ` \u2014 your agents: ${known.join(", ")}` : " \u2014 you have no agents yet (run `braid agent add <name>`)"}`
|
|
3852
|
+
);
|
|
3853
|
+
return null;
|
|
3854
|
+
}
|
|
3855
|
+
async function confirmDestructive(args2, deps, name, actionLine) {
|
|
3856
|
+
if (args2.yes) return true;
|
|
3857
|
+
if (args2.json) {
|
|
3858
|
+
deps.out(JSON.stringify({ ok: false, error: "--json runs non-interactively \u2014 add --yes to confirm" }));
|
|
3859
|
+
deps.err(`\u2717 refusing: --json runs non-interactively \u2014 add --yes to confirm`);
|
|
3860
|
+
return false;
|
|
3861
|
+
}
|
|
3862
|
+
const typed = await deps.prompt(`${actionLine}
|
|
3863
|
+
type the agent name to confirm: `);
|
|
3864
|
+
if (typed === null) {
|
|
3865
|
+
deps.err("\u2717 refusing: no terminal to confirm on \u2014 re-run with --yes");
|
|
3866
|
+
return false;
|
|
3867
|
+
}
|
|
3868
|
+
if (typed.trim() !== name) {
|
|
3869
|
+
deps.err("\u2717 confirmation did not match \u2014 nothing was changed");
|
|
3870
|
+
return false;
|
|
3871
|
+
}
|
|
3872
|
+
return true;
|
|
3873
|
+
}
|
|
3874
|
+
async function enrollAndMaybeStart(args2, deps, opts) {
|
|
3875
|
+
const result = await deps.enroll({ bridgeCmd: opts.bridgeCmd, connectBlob: opts.connectBlob });
|
|
3876
|
+
if (!result.ok) {
|
|
3877
|
+
const msg = sanitizeForTty(result.message);
|
|
3878
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3879
|
+
deps.err(`\u2717 enrollment failed: ${msg}`);
|
|
3880
|
+
if (result.stderrTail) deps.err(result.stderrTail);
|
|
3881
|
+
return 1;
|
|
3882
|
+
}
|
|
3883
|
+
const agentName = result.receipt.agentName;
|
|
3884
|
+
const started = !args2.noStart;
|
|
3885
|
+
if (args2.json) {
|
|
3886
|
+
deps.out(
|
|
3887
|
+
JSON.stringify({
|
|
3888
|
+
ok: true,
|
|
3889
|
+
agentName: sanitizeForTty(agentName),
|
|
3890
|
+
agentUuid: sanitizeForTty(result.receipt.agentUuid),
|
|
3891
|
+
enrolled: true,
|
|
3892
|
+
started
|
|
3893
|
+
})
|
|
3894
|
+
);
|
|
3895
|
+
}
|
|
3896
|
+
deps.err(opts.successLine(sanitizeLabel(agentName)));
|
|
3897
|
+
if (!started) {
|
|
3898
|
+
deps.err(` start it with: ${sanitizeForTty(startArgv(opts.bridgeCmd, agentName, args2.provider).join(" "))}`);
|
|
3899
|
+
return 0;
|
|
3900
|
+
}
|
|
3901
|
+
deps.err(`\u2192 starting ${sanitizeLabel(agentName)} now \u2014 press Ctrl-C to stop it`);
|
|
3902
|
+
return deps.startAgent({ bridgeCmd: opts.bridgeCmd, name: agentName, provider: args2.provider });
|
|
3903
|
+
}
|
|
3904
|
+
async function runAgentAdd(instance2, args2, deps) {
|
|
3905
|
+
const name = requireName2(args2, deps);
|
|
3906
|
+
if (!name) return 1;
|
|
3907
|
+
const bridgeCmd = resolveBridgeCmd(args2.bridgeCmd, deps.env);
|
|
3908
|
+
const creds = await ensureSignedIn(deps);
|
|
3909
|
+
const rest = oneShotRest(instance2, creds);
|
|
3910
|
+
let invite;
|
|
3911
|
+
try {
|
|
3912
|
+
invite = await rest.createPersonalAgent(name);
|
|
3913
|
+
} catch (err) {
|
|
3914
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
3915
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3916
|
+
deps.err(`\u2717 Braid could not create "${sanitizeLabel(name)}": ${msg}`);
|
|
3917
|
+
return 1;
|
|
3918
|
+
}
|
|
3919
|
+
deps.err(`\u2713 ${sanitizeLabel(name)} created on Braid \u2014 enrolling on this machine\u2026`);
|
|
3920
|
+
return enrollAndMaybeStart(args2, deps, {
|
|
3921
|
+
bridgeCmd,
|
|
3922
|
+
connectBlob: invite.connectBlob,
|
|
3923
|
+
successLine: (n) => `\u2714 ${n} is enrolled with Braid`
|
|
3924
|
+
});
|
|
3925
|
+
}
|
|
3926
|
+
async function runAgentStatus(instance2, args2, deps) {
|
|
3927
|
+
const name = requireName2(args2, deps);
|
|
3928
|
+
if (!name) return 1;
|
|
3929
|
+
const creds = await ensureSignedIn(deps);
|
|
3930
|
+
const agent = await findMyAgent(oneShotRest(instance2, creds), name, args2, deps);
|
|
3931
|
+
if (!agent) return 1;
|
|
3932
|
+
if (args2.json) {
|
|
3933
|
+
deps.out(JSON.stringify(agent, null, 2));
|
|
3934
|
+
return 0;
|
|
3935
|
+
}
|
|
3936
|
+
const row = (label, value) => deps.out(`${(label + ":").padEnd(14)}${value}`);
|
|
3937
|
+
row("agent", sanitizeLabel(agent.name ?? "", 64));
|
|
3938
|
+
row("status", agent.status === "online" ? "online" : "offline");
|
|
3939
|
+
const cred = sanitizeLabel(agent.credentialState ?? "none", 32);
|
|
3940
|
+
row(
|
|
3941
|
+
"credential",
|
|
3942
|
+
agent.credentialLastUsedAt ? `${cred} (last used ${sanitizeLabel(agent.credentialLastUsedAt, 40)})` : cred
|
|
3943
|
+
);
|
|
3944
|
+
row("health", sanitizeLabel(agent.healthState ?? "unknown", 32));
|
|
3945
|
+
if (agent.healthDetail) row("detail", sanitizeLabel(agent.healthDetail, 200));
|
|
3946
|
+
if (agent.healthFixHint) row("fix", sanitizeLabel(agent.healthFixHint, 200));
|
|
3947
|
+
row(
|
|
3948
|
+
"service mode",
|
|
3949
|
+
typeof agent.serviceMode === "boolean" ? agent.serviceMode ? "on" : "off" : sanitizeLabel(agent.serviceMode ?? "off", 20)
|
|
3950
|
+
);
|
|
3951
|
+
if (agent.runtimeVersion) row("runtime", sanitizeLabel(agent.runtimeVersion, 40));
|
|
3952
|
+
if (agent.daemonPosture) row("daemon", sanitizeLabel(agent.daemonPosture, 40));
|
|
3953
|
+
return 0;
|
|
3954
|
+
}
|
|
3955
|
+
async function runAgentReissue(instance2, args2, deps) {
|
|
3956
|
+
const name = requireName2(args2, deps);
|
|
3957
|
+
if (!name) return 1;
|
|
3958
|
+
const bridgeCmd = resolveBridgeCmd(args2.bridgeCmd, deps.env);
|
|
3959
|
+
const creds = await ensureSignedIn(deps);
|
|
3960
|
+
const rest = oneShotRest(instance2, creds);
|
|
3961
|
+
const agent = await findMyAgent(rest, name, args2, deps);
|
|
3962
|
+
if (!agent) return 1;
|
|
3963
|
+
let invite;
|
|
3964
|
+
try {
|
|
3965
|
+
invite = await rest.reissueAgent(agent.id);
|
|
3966
|
+
} catch (err) {
|
|
3967
|
+
const msg = err instanceof RelayError && err.status === 429 ? "reissue is rate-limited \u2014 wait a minute and try again" : sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
3968
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3969
|
+
deps.err(`\u2717 Braid could not reissue "${sanitizeLabel(name)}": ${msg}`);
|
|
3970
|
+
return 1;
|
|
3971
|
+
}
|
|
3972
|
+
deps.err(`\u2713 fresh credential minted \u2014 re-enrolling ${sanitizeLabel(name)} on this machine\u2026`);
|
|
3973
|
+
return enrollAndMaybeStart(args2, deps, {
|
|
3974
|
+
bridgeCmd,
|
|
3975
|
+
connectBlob: invite.connectBlob,
|
|
3976
|
+
successLine: (n) => `\u2714 ${n} reconnected to Braid`
|
|
3977
|
+
});
|
|
3978
|
+
}
|
|
3979
|
+
async function runAgentRevoke(instance2, args2, deps) {
|
|
3980
|
+
const name = requireName2(args2, deps);
|
|
3981
|
+
if (!name) return 1;
|
|
3982
|
+
const creds = await ensureSignedIn(deps);
|
|
3983
|
+
const rest = oneShotRest(instance2, creds);
|
|
3984
|
+
const agent = await findMyAgent(rest, name, args2, deps);
|
|
3985
|
+
if (!agent) return 1;
|
|
3986
|
+
const confirmed = await confirmDestructive(
|
|
3987
|
+
args2,
|
|
3988
|
+
deps,
|
|
3989
|
+
name,
|
|
3990
|
+
`this revokes "${sanitizeLabel(name)}"'s credential and disconnects it from Braid.`
|
|
3991
|
+
);
|
|
3992
|
+
if (!confirmed) return 1;
|
|
3993
|
+
let credential;
|
|
3994
|
+
try {
|
|
3995
|
+
credential = await rest.revokeAgentCredential(agent.id);
|
|
3996
|
+
} catch (err) {
|
|
3997
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
3998
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
3999
|
+
deps.err(`\u2717 Braid could not revoke "${sanitizeLabel(name)}": ${msg}`);
|
|
4000
|
+
return 1;
|
|
4001
|
+
}
|
|
4002
|
+
if (args2.json) {
|
|
4003
|
+
deps.out(
|
|
4004
|
+
JSON.stringify({
|
|
4005
|
+
ok: true,
|
|
4006
|
+
agentName: sanitizeForTty(name),
|
|
4007
|
+
credentialState: sanitizeForTty(credential.state ?? "revoked_owner")
|
|
4008
|
+
})
|
|
4009
|
+
);
|
|
4010
|
+
}
|
|
4011
|
+
deps.err(
|
|
4012
|
+
`\u2714 credential revoked \u2014 ${sanitizeLabel(name)} can no longer connect to Braid until you run \`braid agent reissue ${sanitizeLabel(name)}\``
|
|
4013
|
+
);
|
|
4014
|
+
return 0;
|
|
4015
|
+
}
|
|
4016
|
+
async function runAgentRemove(instance2, args2, deps) {
|
|
4017
|
+
const name = requireName2(args2, deps);
|
|
4018
|
+
if (!name) return 1;
|
|
4019
|
+
const creds = await ensureSignedIn(deps);
|
|
4020
|
+
const rest = oneShotRest(instance2, creds);
|
|
4021
|
+
const agent = await findMyAgent(rest, name, args2, deps);
|
|
4022
|
+
if (!agent) return 1;
|
|
4023
|
+
const confirmed = await confirmDestructive(
|
|
4024
|
+
args2,
|
|
4025
|
+
deps,
|
|
4026
|
+
name,
|
|
4027
|
+
`this permanently deletes agent "${sanitizeLabel(name)}" from Braid \u2014 identity, credential, and channel memberships.`
|
|
4028
|
+
);
|
|
4029
|
+
if (!confirmed) return 1;
|
|
4030
|
+
try {
|
|
4031
|
+
await rest.deleteAgent(agent.id);
|
|
4032
|
+
} catch (err) {
|
|
4033
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4034
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4035
|
+
deps.err(`\u2717 Braid could not remove "${sanitizeLabel(name)}": ${msg}`);
|
|
4036
|
+
return 1;
|
|
4037
|
+
}
|
|
4038
|
+
if (args2.json) deps.out(JSON.stringify({ ok: true, agentName: sanitizeForTty(name), removed: true }));
|
|
4039
|
+
deps.err(
|
|
4040
|
+
`\u2714 ${sanitizeLabel(name)} removed from Braid \u2014 its identity, credential, and channel memberships are deleted; messages it already posted stay in channel history`
|
|
4041
|
+
);
|
|
4042
|
+
return 0;
|
|
4043
|
+
}
|
|
4044
|
+
async function runAgentsList(instance2, args2, deps) {
|
|
4045
|
+
const creds = await ensureSignedIn(deps);
|
|
4046
|
+
const rest = oneShotRest(instance2, creds);
|
|
4047
|
+
let agents;
|
|
4048
|
+
try {
|
|
4049
|
+
agents = await rest.listMyAgents();
|
|
4050
|
+
} catch (err) {
|
|
4051
|
+
deps.err(
|
|
4052
|
+
`\u2717 Braid could not list your agents: ${sanitizeForTty(err instanceof Error ? err.message : String(err))}`
|
|
4053
|
+
);
|
|
4054
|
+
return 1;
|
|
4055
|
+
}
|
|
4056
|
+
if (args2.json) {
|
|
4057
|
+
deps.out(JSON.stringify(agents, null, 2));
|
|
4058
|
+
return 0;
|
|
4059
|
+
}
|
|
4060
|
+
if (agents.length === 0) {
|
|
4061
|
+
deps.out("(no agents yet \u2014 run `braid agent add <name>` to create one)");
|
|
4062
|
+
return 0;
|
|
4063
|
+
}
|
|
4064
|
+
deps.out(`${"NAME".padEnd(24)} ${"STATUS".padEnd(8)} CREDENTIAL`);
|
|
4065
|
+
for (const a of agents) {
|
|
4066
|
+
const status = a.status === "online" ? "online" : "offline";
|
|
4067
|
+
deps.out(
|
|
4068
|
+
`${sanitizeLabel(a.name ?? "", 24).padEnd(24)} ${status.padEnd(8)} ${sanitizeLabel(a.credentialState ?? "-", 20)}`
|
|
4069
|
+
);
|
|
4070
|
+
}
|
|
4071
|
+
return 0;
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
// src/commands/channel.ts
|
|
4075
|
+
var CHANNEL_USAGE = `usage: braid channels [--all] [--json]
|
|
4076
|
+
braid channel <create|archive|unarchive|members|member> \u2026
|
|
4077
|
+
|
|
4078
|
+
channels list your channels (--all includes archived)
|
|
4079
|
+
create <name> create a channel on Braid (you become its owner)
|
|
4080
|
+
archive <name-or-id> archive a channel \u2014 it leaves everyone's sidebar
|
|
4081
|
+
(reversible; y/N confirm, --yes skips)
|
|
4082
|
+
unarchive <name-or-id> restore an archived channel
|
|
4083
|
+
members <name-or-id> list a channel's members (humans and agents)
|
|
4084
|
+
member add <channel> <agent>
|
|
4085
|
+
add one of your agents to a channel
|
|
4086
|
+
member remove <channel> <agent>
|
|
4087
|
+
remove an agent from a channel (y/N confirm, --yes)
|
|
4088
|
+
|
|
4089
|
+
--all include archived channels when listing
|
|
4090
|
+
--yes skip the confirmation prompt (required with --json on
|
|
4091
|
+
archive/member remove)
|
|
4092
|
+
--json machine-readable result on stdout
|
|
4093
|
+
|
|
4094
|
+
channels are archive-only \u2014 there is no delete. archive to retire one.`;
|
|
4095
|
+
async function runChannelCli(instance2, args2, overrides = {}) {
|
|
4096
|
+
const deps = {
|
|
4097
|
+
resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
|
|
4098
|
+
runLogin: async () => (await login(instance2)).credentials,
|
|
4099
|
+
out: (line) => console.log(line),
|
|
4100
|
+
err: (line) => console.error(line),
|
|
4101
|
+
prompt: promptOnTty,
|
|
4102
|
+
...overrides
|
|
4103
|
+
};
|
|
4104
|
+
try {
|
|
4105
|
+
if (args2.cmd === "channels") return await runChannelsList(instance2, args2, deps);
|
|
4106
|
+
switch (args2.sub) {
|
|
4107
|
+
case "create":
|
|
4108
|
+
return await runChannelCreate(instance2, args2, deps);
|
|
4109
|
+
case "archive":
|
|
4110
|
+
return await runChannelArchive(instance2, args2, deps);
|
|
4111
|
+
case "unarchive":
|
|
4112
|
+
return await runChannelUnarchive(instance2, args2, deps);
|
|
4113
|
+
case "members":
|
|
4114
|
+
return await runChannelMembers(instance2, args2, deps);
|
|
4115
|
+
case "member":
|
|
4116
|
+
return await runChannelMember(instance2, args2, deps);
|
|
4117
|
+
default:
|
|
4118
|
+
deps.err(CHANNEL_USAGE);
|
|
4119
|
+
if (args2.sub) deps.err(`
|
|
4120
|
+
unknown channel command "${sanitizeLabel(args2.sub)}".`);
|
|
4121
|
+
return 1;
|
|
4122
|
+
}
|
|
4123
|
+
} catch (err) {
|
|
4124
|
+
deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
|
|
4125
|
+
return 1;
|
|
4126
|
+
}
|
|
4127
|
+
}
|
|
4128
|
+
async function fetchChannels(rest, scope) {
|
|
4129
|
+
const rows = [];
|
|
4130
|
+
if (scope !== "archived") {
|
|
4131
|
+
rows.push(...(await rest.listChannels()).map((c) => ({ ...c, archived: false })));
|
|
4132
|
+
}
|
|
4133
|
+
if (scope !== "active") {
|
|
4134
|
+
rows.push(...(await rest.listArchivedChannels()).map((c) => ({ ...c, archived: true })));
|
|
4135
|
+
}
|
|
4136
|
+
return rows;
|
|
4137
|
+
}
|
|
4138
|
+
async function resolveChannel(rest, ref, scope, args2, deps) {
|
|
4139
|
+
const rows = await fetchChannels(rest, scope);
|
|
4140
|
+
const byId = rows.find((c) => c.id === ref);
|
|
4141
|
+
if (byId) return byId;
|
|
4142
|
+
const byName = rows.filter((c) => sanitizeForTty(c.name ?? "") === ref);
|
|
4143
|
+
if (byName.length === 1) return byName[0];
|
|
4144
|
+
if (byName.length > 1) {
|
|
4145
|
+
const msg2 = `channel name "${sanitizeLabel(ref)}" is ambiguous \u2014 use the id`;
|
|
4146
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg2 }));
|
|
4147
|
+
deps.err(`\u2717 ${msg2}:`);
|
|
4148
|
+
for (const c of byName) {
|
|
4149
|
+
deps.err(
|
|
4150
|
+
` ${sanitizeLabel(c.name ?? "", 24).padEnd(24)} ${sanitizeLabel(c.id ?? "", 40)}${c.archived ? " (archived)" : ""}`
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
4153
|
+
return null;
|
|
4154
|
+
}
|
|
4155
|
+
const noun = scope === "archived" ? "archived channel" : "channel";
|
|
4156
|
+
const msg = `no ${noun} named "${sanitizeLabel(ref)}"`;
|
|
4157
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4158
|
+
const known = rows.map((c) => sanitizeLabel(c.name ?? "")).filter(Boolean);
|
|
4159
|
+
deps.err(
|
|
4160
|
+
`\u2717 ${msg}${known.length > 0 ? ` \u2014 your ${scope === "archived" ? "archived channels" : "channels"}: ${known.join(", ")}` : scope === "archived" ? " \u2014 you have no archived channels" : " \u2014 you have no channels yet (run `braid channel create <name>`)"}`
|
|
4161
|
+
);
|
|
4162
|
+
return null;
|
|
4163
|
+
}
|
|
4164
|
+
async function confirmSimple(args2, deps, actionLine) {
|
|
4165
|
+
if (args2.yes) return true;
|
|
4166
|
+
if (args2.json) {
|
|
4167
|
+
deps.out(JSON.stringify({ ok: false, error: "--json runs non-interactively \u2014 add --yes to confirm" }));
|
|
4168
|
+
deps.err("\u2717 refusing: --json runs non-interactively \u2014 add --yes to confirm");
|
|
4169
|
+
return false;
|
|
4170
|
+
}
|
|
4171
|
+
const typed = await deps.prompt(`${actionLine} [y/N] `);
|
|
4172
|
+
if (typed === null) {
|
|
4173
|
+
deps.err("\u2717 refusing: no terminal to confirm on \u2014 re-run with --yes");
|
|
4174
|
+
return false;
|
|
4175
|
+
}
|
|
4176
|
+
if (!/^y(es)?$/i.test(typed.trim())) {
|
|
4177
|
+
deps.err("\u2717 not confirmed \u2014 nothing was changed");
|
|
4178
|
+
return false;
|
|
4179
|
+
}
|
|
4180
|
+
return true;
|
|
4181
|
+
}
|
|
4182
|
+
function shortId(id) {
|
|
4183
|
+
return sanitizeLabel(id ?? "", 40).slice(0, 8);
|
|
4184
|
+
}
|
|
4185
|
+
async function runChannelsList(instance2, args2, deps) {
|
|
4186
|
+
const creds = await ensureSignedIn(deps);
|
|
4187
|
+
const rest = oneShotRest(instance2, creds);
|
|
4188
|
+
const rows = await fetchChannels(rest, args2.all ? "all" : "active");
|
|
4189
|
+
if (args2.json) {
|
|
4190
|
+
deps.out(JSON.stringify(rows, null, 2));
|
|
4191
|
+
return 0;
|
|
4192
|
+
}
|
|
4193
|
+
if (rows.length === 0) {
|
|
4194
|
+
deps.out("(no channels yet \u2014 run `braid channel create <name>` to make one)");
|
|
4195
|
+
return 0;
|
|
4196
|
+
}
|
|
4197
|
+
deps.out(`${"NAME".padEnd(24)} ${"ID".padEnd(10)}${args2.all ? " STATE" : ""}`);
|
|
4198
|
+
for (const c of rows) {
|
|
4199
|
+
deps.out(
|
|
4200
|
+
`${sanitizeLabel(c.name ?? "", 24).padEnd(24)} ${shortId(c.id).padEnd(10)}${args2.all ? ` ${c.archived ? "archived" : "active"}` : ""}`
|
|
4201
|
+
);
|
|
4202
|
+
}
|
|
4203
|
+
return 0;
|
|
4204
|
+
}
|
|
4205
|
+
async function runChannelCreate(instance2, args2, deps) {
|
|
4206
|
+
const name = args2.rest[0]?.trim();
|
|
4207
|
+
if (!name) {
|
|
4208
|
+
deps.err("channel create requires a name \u2014 usage: braid channel create <name>");
|
|
4209
|
+
return 1;
|
|
4210
|
+
}
|
|
4211
|
+
const creds = await ensureSignedIn(deps);
|
|
4212
|
+
const rest = oneShotRest(instance2, creds);
|
|
4213
|
+
let ch;
|
|
4214
|
+
try {
|
|
4215
|
+
ch = await rest.createChannel(name);
|
|
4216
|
+
} catch (err) {
|
|
4217
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4218
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4219
|
+
deps.err(`\u2717 Braid could not create channel "${sanitizeLabel(name)}": ${msg}`);
|
|
4220
|
+
return 1;
|
|
4221
|
+
}
|
|
4222
|
+
if (args2.json) {
|
|
4223
|
+
deps.out(JSON.stringify({ ok: true, id: sanitizeForTty(ch.id ?? ""), name: sanitizeForTty(ch.name ?? "") }));
|
|
4224
|
+
}
|
|
4225
|
+
deps.err(
|
|
4226
|
+
`\u2714 #${sanitizeLabel(ch.name ?? name)} created on Braid (id ${shortId(ch.id)}) \u2014 add an agent with \`braid channel member add ${sanitizeLabel(ch.name ?? name)} <agent>\``
|
|
4227
|
+
);
|
|
4228
|
+
return 0;
|
|
4229
|
+
}
|
|
4230
|
+
async function runChannelArchive(instance2, args2, deps) {
|
|
4231
|
+
const ref = args2.rest[0]?.trim();
|
|
4232
|
+
if (!ref) {
|
|
4233
|
+
deps.err("channel archive requires a name \u2014 usage: braid channel archive <name-or-id>");
|
|
4234
|
+
return 1;
|
|
4235
|
+
}
|
|
4236
|
+
const creds = await ensureSignedIn(deps);
|
|
4237
|
+
const rest = oneShotRest(instance2, creds);
|
|
4238
|
+
const ch = await resolveChannel(rest, ref, "active", args2, deps);
|
|
4239
|
+
if (!ch) return 1;
|
|
4240
|
+
const name = sanitizeLabel(ch.name ?? "");
|
|
4241
|
+
const confirmed = await confirmSimple(
|
|
4242
|
+
args2,
|
|
4243
|
+
deps,
|
|
4244
|
+
`archive #${name}? it leaves everyone's sidebar (\`braid channel unarchive ${name}\` restores it).`
|
|
4245
|
+
);
|
|
4246
|
+
if (!confirmed) return 1;
|
|
4247
|
+
try {
|
|
4248
|
+
await rest.archiveChannel(ch.id);
|
|
4249
|
+
} catch (err) {
|
|
4250
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4251
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4252
|
+
deps.err(`\u2717 Braid could not archive #${name}: ${msg}`);
|
|
4253
|
+
return 1;
|
|
4254
|
+
}
|
|
4255
|
+
if (args2.json) {
|
|
4256
|
+
deps.out(
|
|
4257
|
+
JSON.stringify({ ok: true, id: sanitizeForTty(ch.id), name: sanitizeForTty(ch.name ?? ""), archived: true })
|
|
4258
|
+
);
|
|
4259
|
+
}
|
|
4260
|
+
deps.err(`\u2714 #${name} archived \u2014 bring it back any time with \`braid channel unarchive ${name}\``);
|
|
4261
|
+
return 0;
|
|
4262
|
+
}
|
|
4263
|
+
async function runChannelUnarchive(instance2, args2, deps) {
|
|
4264
|
+
const ref = args2.rest[0]?.trim();
|
|
4265
|
+
if (!ref) {
|
|
4266
|
+
deps.err("channel unarchive requires a name \u2014 usage: braid channel unarchive <name-or-id>");
|
|
4267
|
+
return 1;
|
|
4268
|
+
}
|
|
4269
|
+
const creds = await ensureSignedIn(deps);
|
|
4270
|
+
const rest = oneShotRest(instance2, creds);
|
|
4271
|
+
const ch = await resolveChannel(rest, ref, "archived", args2, deps);
|
|
4272
|
+
if (!ch) return 1;
|
|
4273
|
+
const name = sanitizeLabel(ch.name ?? "");
|
|
4274
|
+
try {
|
|
4275
|
+
await rest.unarchiveChannel(ch.id);
|
|
4276
|
+
} catch (err) {
|
|
4277
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4278
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4279
|
+
deps.err(`\u2717 Braid could not unarchive #${name}: ${msg}`);
|
|
4280
|
+
return 1;
|
|
4281
|
+
}
|
|
4282
|
+
if (args2.json) {
|
|
4283
|
+
deps.out(
|
|
4284
|
+
JSON.stringify({ ok: true, id: sanitizeForTty(ch.id), name: sanitizeForTty(ch.name ?? ""), archived: false })
|
|
4285
|
+
);
|
|
4286
|
+
}
|
|
4287
|
+
deps.err(`\u2714 #${name} is back \u2014 restored from the archive`);
|
|
4288
|
+
return 0;
|
|
4289
|
+
}
|
|
4290
|
+
async function runChannelMembers(instance2, args2, deps) {
|
|
4291
|
+
const ref = args2.rest[0]?.trim();
|
|
4292
|
+
if (!ref) {
|
|
4293
|
+
deps.err("channel members requires a channel \u2014 usage: braid channel members <name-or-id>");
|
|
4294
|
+
return 1;
|
|
4295
|
+
}
|
|
4296
|
+
const creds = await ensureSignedIn(deps);
|
|
4297
|
+
const rest = oneShotRest(instance2, creds);
|
|
4298
|
+
const ch = await resolveChannel(rest, ref, "all", args2, deps);
|
|
4299
|
+
if (!ch) return 1;
|
|
4300
|
+
if (args2.json) {
|
|
4301
|
+
deps.out(JSON.stringify(ch.members, null, 2));
|
|
4302
|
+
return 0;
|
|
4303
|
+
}
|
|
4304
|
+
if (ch.members.length === 0) {
|
|
4305
|
+
deps.out(`(no members in #${sanitizeLabel(ch.name ?? "")})`);
|
|
4306
|
+
return 0;
|
|
4307
|
+
}
|
|
4308
|
+
deps.out(`${"NAME".padEnd(24)} ${"TYPE".padEnd(6)} ${"ROLE".padEnd(7)} STATUS`);
|
|
4309
|
+
for (const m of ch.members) {
|
|
4310
|
+
deps.out(
|
|
4311
|
+
`${sanitizeLabel(m.name, 24).padEnd(24)} ${m.type.padEnd(6)} ${sanitizeLabel(m.role, 7).padEnd(7)} ${m.status}`
|
|
4312
|
+
);
|
|
4313
|
+
}
|
|
4314
|
+
return 0;
|
|
4315
|
+
}
|
|
4316
|
+
async function runChannelMember(instance2, args2, deps) {
|
|
4317
|
+
const [action, channelRef, agentName] = args2.rest.map((s) => s?.trim());
|
|
4318
|
+
const usage = "usage: braid channel member <add|remove> <channel> <agent-name>";
|
|
4319
|
+
if (action !== "add" && action !== "remove" || !channelRef || !agentName) {
|
|
4320
|
+
deps.err(usage);
|
|
4321
|
+
if (action && action !== "add" && action !== "remove") {
|
|
4322
|
+
deps.err(`
|
|
4323
|
+
unknown member action "${sanitizeLabel(action)}".`);
|
|
4324
|
+
}
|
|
4325
|
+
return 1;
|
|
4326
|
+
}
|
|
4327
|
+
const creds = await ensureSignedIn(deps);
|
|
4328
|
+
const rest = oneShotRest(instance2, creds);
|
|
4329
|
+
const ch = await resolveChannel(rest, channelRef, "active", args2, deps);
|
|
4330
|
+
if (!ch) return 1;
|
|
4331
|
+
return action === "add" ? runMemberAdd(rest, ch, agentName, args2, deps) : runMemberRemove(rest, ch, agentName, args2, deps);
|
|
4332
|
+
}
|
|
4333
|
+
async function runMemberAdd(rest, ch, agentName, args2, deps) {
|
|
4334
|
+
const chName = sanitizeLabel(ch.name ?? "");
|
|
4335
|
+
const agent = await findMyAgent(rest, agentName, args2, deps);
|
|
4336
|
+
if (!agent) return 1;
|
|
4337
|
+
try {
|
|
4338
|
+
await rest.addAgentToChannel(ch.id, agent.id);
|
|
4339
|
+
} catch (err) {
|
|
4340
|
+
let msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4341
|
+
if (msg.includes("guest_agents_disabled")) {
|
|
4342
|
+
msg = "this organization does not allow guest agents \u2014 enable it in org settings or ask an admin";
|
|
4343
|
+
}
|
|
4344
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4345
|
+
deps.err(`\u2717 Braid could not add "${sanitizeLabel(agentName)}" to #${chName}: ${msg}`);
|
|
4346
|
+
return 1;
|
|
4347
|
+
}
|
|
4348
|
+
if (args2.json) {
|
|
4349
|
+
deps.out(
|
|
4350
|
+
JSON.stringify({
|
|
4351
|
+
ok: true,
|
|
4352
|
+
channelId: sanitizeForTty(ch.id),
|
|
4353
|
+
channel: sanitizeForTty(ch.name ?? ""),
|
|
4354
|
+
agent: sanitizeForTty(agent.name),
|
|
4355
|
+
added: true
|
|
4356
|
+
})
|
|
4357
|
+
);
|
|
4358
|
+
}
|
|
4359
|
+
deps.err(`\u2714 ${sanitizeLabel(agent.name)} joined #${chName} \u2014 it now sees the channel and can post there`);
|
|
4360
|
+
return 0;
|
|
4361
|
+
}
|
|
4362
|
+
async function runMemberRemove(rest, ch, agentName, args2, deps) {
|
|
4363
|
+
const chName = sanitizeLabel(ch.name ?? "");
|
|
4364
|
+
const member = ch.members.find((m) => m.type === "bot" && m.id === agentName);
|
|
4365
|
+
if (!member) {
|
|
4366
|
+
const msg = `"${sanitizeLabel(agentName)}" is not a member of #${chName}`;
|
|
4367
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4368
|
+
const bots = ch.members.filter((m) => m.type === "bot").map((m) => sanitizeLabel(m.name));
|
|
4369
|
+
deps.err(`\u2717 ${msg}${bots.length > 0 ? ` \u2014 its agents: ${bots.join(", ")}` : " \u2014 it has no agents"}`);
|
|
4370
|
+
return 1;
|
|
4371
|
+
}
|
|
4372
|
+
const confirmed = await confirmSimple(
|
|
4373
|
+
args2,
|
|
4374
|
+
deps,
|
|
4375
|
+
`remove ${sanitizeLabel(agentName)} from #${chName}? it stops seeing the channel (member add restores it).`
|
|
4376
|
+
);
|
|
4377
|
+
if (!confirmed) return 1;
|
|
4378
|
+
try {
|
|
4379
|
+
await rest.removeChannelMember(ch.id, member.id);
|
|
4380
|
+
} catch (err) {
|
|
4381
|
+
const msg = sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4382
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4383
|
+
deps.err(`\u2717 Braid could not remove "${sanitizeLabel(agentName)}" from #${chName}: ${msg}`);
|
|
4384
|
+
return 1;
|
|
4385
|
+
}
|
|
4386
|
+
if (args2.json) {
|
|
4387
|
+
deps.out(
|
|
4388
|
+
JSON.stringify({
|
|
4389
|
+
ok: true,
|
|
4390
|
+
channelId: sanitizeForTty(ch.id),
|
|
4391
|
+
channel: sanitizeForTty(ch.name ?? ""),
|
|
4392
|
+
agent: sanitizeForTty(agentName),
|
|
4393
|
+
removed: true
|
|
4394
|
+
})
|
|
4395
|
+
);
|
|
4396
|
+
}
|
|
4397
|
+
deps.err(
|
|
4398
|
+
`\u2714 ${sanitizeLabel(agentName)} removed from #${chName} \u2014 add it back any time with \`braid channel member add ${chName} ${sanitizeLabel(agentName)}\``
|
|
4399
|
+
);
|
|
4400
|
+
return 0;
|
|
4401
|
+
}
|
|
4402
|
+
|
|
4403
|
+
// src/commands/invite.ts
|
|
4404
|
+
var INVITE_USAGE = `usage: braid invites [<channel>] [--all] [--json]
|
|
4405
|
+
braid invite <agent|human|revoke> \u2026
|
|
4406
|
+
|
|
4407
|
+
invites list your org's channel-less agent invites
|
|
4408
|
+
invites <channel> list a channel's agent invites
|
|
4409
|
+
(active only; --all includes revoked/expired/used)
|
|
4410
|
+
agent <channel> <name> mint an invite for a NEW agent named <name> on the
|
|
4411
|
+
channel \u2014 prints the join command ONCE (org admin)
|
|
4412
|
+
human <channel> <email> invite a human by email: they get a Braid email and
|
|
4413
|
+
land in the channel (org admin; a user from another
|
|
4414
|
+
org joins this channel only, as a guest)
|
|
4415
|
+
revoke <invite-id> revoke an org-wide invite so it cannot be redeemed
|
|
4416
|
+
(y/N confirm, --yes skips)
|
|
4417
|
+
--channel <channel> revoke a CHANNEL invite instead (the id alone
|
|
4418
|
+
targets the org-wide route)
|
|
4419
|
+
|
|
4420
|
+
--all include revoked/expired/exhausted invites when listing
|
|
4421
|
+
--yes skip the confirmation prompt (required with --json on revoke)
|
|
4422
|
+
--json machine-readable result on stdout
|
|
4423
|
+
|
|
4424
|
+
minting and managing invites needs org admin on Braid.`;
|
|
4425
|
+
async function runInviteCli(instance2, args2, overrides = {}) {
|
|
4426
|
+
const deps = {
|
|
4427
|
+
resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
|
|
4428
|
+
runLogin: async () => (await login(instance2)).credentials,
|
|
4429
|
+
out: (line) => console.log(line),
|
|
4430
|
+
err: (line) => console.error(line),
|
|
4431
|
+
prompt: promptOnTty,
|
|
4432
|
+
...overrides
|
|
4433
|
+
};
|
|
4434
|
+
try {
|
|
4435
|
+
if (args2.cmd === "invites") return await runInvitesList(instance2, args2, deps);
|
|
4436
|
+
switch (args2.sub) {
|
|
4437
|
+
case "agent":
|
|
4438
|
+
return await runInviteAgent(instance2, args2, deps);
|
|
4439
|
+
case "human":
|
|
4440
|
+
return await runInviteHuman(instance2, args2, deps);
|
|
4441
|
+
case "revoke":
|
|
4442
|
+
return await runInviteRevoke(instance2, args2, deps);
|
|
4443
|
+
default:
|
|
4444
|
+
deps.err(INVITE_USAGE);
|
|
4445
|
+
if (args2.sub) deps.err(`
|
|
4446
|
+
unknown invite command "${sanitizeLabel(args2.sub)}".`);
|
|
4447
|
+
return 1;
|
|
4448
|
+
}
|
|
4449
|
+
} catch (err) {
|
|
4450
|
+
deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
|
|
4451
|
+
return 1;
|
|
4452
|
+
}
|
|
4453
|
+
}
|
|
4454
|
+
function inviteErrorMessage(err) {
|
|
4455
|
+
if (err instanceof RelayError) {
|
|
4456
|
+
if (err.status === 403 && err.message === "admin_required") {
|
|
4457
|
+
return "you need to be an org admin on Braid to do this \u2014 ask an admin, or have them grant you the role";
|
|
4458
|
+
}
|
|
4459
|
+
if (err.status === 403 && err.message === "guest_agents_disabled") {
|
|
4460
|
+
return "this organization does not allow guest agents \u2014 enable it in org settings or ask an admin";
|
|
4461
|
+
}
|
|
4462
|
+
if (err.status === 402 && err.message === "seat_limit_reached") {
|
|
4463
|
+
return "the organization is out of member seats \u2014 upgrade the plan to invite more humans";
|
|
4464
|
+
}
|
|
4465
|
+
if (err.status === 402 && err.message === "guest_limit_reached") {
|
|
4466
|
+
return "the organization is at its guest limit \u2014 upgrade the plan to add more guests";
|
|
4467
|
+
}
|
|
4468
|
+
}
|
|
4469
|
+
return sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4470
|
+
}
|
|
4471
|
+
async function runInviteAgent(instance2, args2, deps) {
|
|
4472
|
+
const [channelRef, name] = args2.rest.map((s) => s?.trim());
|
|
4473
|
+
if (!channelRef || !name) {
|
|
4474
|
+
deps.err("usage: braid invite agent <channel> <agent-name>");
|
|
4475
|
+
return 1;
|
|
4476
|
+
}
|
|
4477
|
+
const creds = await ensureSignedIn(deps);
|
|
4478
|
+
const rest = oneShotRest(instance2, creds);
|
|
4479
|
+
const ch = await resolveChannel(rest, channelRef, "active", args2, deps);
|
|
4480
|
+
if (!ch) return 1;
|
|
4481
|
+
const chName = sanitizeLabel(ch.name ?? "");
|
|
4482
|
+
let mint;
|
|
4483
|
+
try {
|
|
4484
|
+
mint = await rest.createChannelAgentInvite(ch.id, name);
|
|
4485
|
+
} catch (err) {
|
|
4486
|
+
const msg = inviteErrorMessage(err);
|
|
4487
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4488
|
+
deps.err(`\u2717 Braid could not mint an invite for "${sanitizeLabel(name)}" on #${chName}: ${msg}`);
|
|
4489
|
+
return 1;
|
|
4490
|
+
}
|
|
4491
|
+
if (args2.json) {
|
|
4492
|
+
deps.out(JSON.stringify(mint));
|
|
4493
|
+
return 0;
|
|
4494
|
+
}
|
|
4495
|
+
deps.err(`\u2714 invite minted for agent "${sanitizeLabel(name)}" on #${chName} (invite ${shortId(mint.inviteId)})`);
|
|
4496
|
+
deps.err(" run this on the agent's machine to join:");
|
|
4497
|
+
deps.out(sanitizeForTty(mint.joinCommand));
|
|
4498
|
+
deps.err(
|
|
4499
|
+
`\u26A0 shown once \u2014 Braid keeps only a hash and this terminal will not show it again` + (mint.expiresAt ? ` (expires ${sanitizeLabel(mint.expiresAt)}` : ` (never expires`) + `${mint.oneTime ? ", single use)" : ")"}`
|
|
4500
|
+
);
|
|
4501
|
+
return 0;
|
|
4502
|
+
}
|
|
4503
|
+
function usesCell(inv) {
|
|
4504
|
+
return `${inv.uses}/${inv.maxUses === 0 ? "\u221E" : inv.maxUses}`;
|
|
4505
|
+
}
|
|
4506
|
+
async function runInvitesList(instance2, args2, deps) {
|
|
4507
|
+
const channelRef = args2.rest[0]?.trim();
|
|
4508
|
+
const creds = await ensureSignedIn(deps);
|
|
4509
|
+
const rest = oneShotRest(instance2, creds);
|
|
4510
|
+
let ch = null;
|
|
4511
|
+
let invites;
|
|
4512
|
+
try {
|
|
4513
|
+
if (channelRef) {
|
|
4514
|
+
ch = await resolveChannel(rest, channelRef, "all", args2, deps);
|
|
4515
|
+
if (!ch) return 1;
|
|
4516
|
+
invites = await rest.listChannelAgentInvites(ch.id);
|
|
4517
|
+
} else {
|
|
4518
|
+
invites = await rest.listOrgAgentInvites();
|
|
4519
|
+
}
|
|
4520
|
+
} catch (err) {
|
|
4521
|
+
const msg = inviteErrorMessage(err);
|
|
4522
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4523
|
+
deps.err(`\u2717 Braid could not list invites: ${msg}`);
|
|
4524
|
+
return 1;
|
|
4525
|
+
}
|
|
4526
|
+
if (!args2.all) invites = invites.filter((i) => i.status === "active");
|
|
4527
|
+
if (args2.json) {
|
|
4528
|
+
deps.out(JSON.stringify(invites, null, 2));
|
|
4529
|
+
return 0;
|
|
4530
|
+
}
|
|
4531
|
+
const where = ch ? `#${sanitizeLabel(ch.name ?? "")}` : "your organization (channel-less invites)";
|
|
4532
|
+
if (invites.length === 0) {
|
|
4533
|
+
deps.out(`(no ${args2.all ? "" : "active "}invites for ${where})`);
|
|
4534
|
+
return 0;
|
|
4535
|
+
}
|
|
4536
|
+
deps.err(`invites for ${where}:`);
|
|
4537
|
+
deps.out(
|
|
4538
|
+
`${"ID".padEnd(10)} ${"AGENT".padEnd(24)} ${"CHANNEL".padEnd(16)} ${"CREATED".padEnd(21)} ${"EXPIRES".padEnd(21)} ${"USES".padEnd(5)} STATUS`
|
|
4539
|
+
);
|
|
4540
|
+
for (const inv of invites) {
|
|
4541
|
+
const channelCell = ch ? sanitizeLabel(ch.name ?? "", 16) : "-";
|
|
4542
|
+
deps.out(
|
|
4543
|
+
`${shortId(inv.id).padEnd(10)} ${sanitizeLabel(inv.agentName ?? "", 24).padEnd(24)} ${channelCell.padEnd(16)} ${sanitizeLabel(inv.createdAt ?? "", 21).padEnd(21)} ${sanitizeLabel(inv.expiresAt ?? "never", 21).padEnd(21)} ${usesCell(inv).padEnd(5)} ` + sanitizeLabel(inv.status ?? "", 12)
|
|
4544
|
+
);
|
|
4545
|
+
}
|
|
4546
|
+
return 0;
|
|
4547
|
+
}
|
|
4548
|
+
async function runInviteRevoke(instance2, args2, deps) {
|
|
4549
|
+
const inviteId = args2.rest[0]?.trim();
|
|
4550
|
+
if (!inviteId) {
|
|
4551
|
+
deps.err("invite revoke requires an id \u2014 usage: braid invite revoke <invite-id> [--channel <channel>]");
|
|
4552
|
+
return 1;
|
|
4553
|
+
}
|
|
4554
|
+
const creds = await ensureSignedIn(deps);
|
|
4555
|
+
const rest = oneShotRest(instance2, creds);
|
|
4556
|
+
let ch = null;
|
|
4557
|
+
if (args2.channel) {
|
|
4558
|
+
ch = await resolveChannel(rest, args2.channel.trim(), "all", args2, deps);
|
|
4559
|
+
if (!ch) return 1;
|
|
4560
|
+
}
|
|
4561
|
+
const where = ch ? `#${sanitizeLabel(ch.name ?? "")}` : "your organization";
|
|
4562
|
+
const confirmed = await confirmSimple(
|
|
4563
|
+
args2,
|
|
4564
|
+
deps,
|
|
4565
|
+
`revoke invite ${sanitizeLabel(inviteId, 40)} on ${where}? it can no longer be redeemed (an agent that already joined stays connected).`
|
|
4566
|
+
);
|
|
4567
|
+
if (!confirmed) return 1;
|
|
4568
|
+
try {
|
|
4569
|
+
if (ch) await rest.revokeChannelAgentInvite(ch.id, inviteId);
|
|
4570
|
+
else await rest.revokeOrgAgentInvite(inviteId);
|
|
4571
|
+
} catch (err) {
|
|
4572
|
+
let msg = inviteErrorMessage(err);
|
|
4573
|
+
if (err instanceof RelayError && err.status === 404 && !ch) {
|
|
4574
|
+
msg = `invite not found org-wide \u2014 a channel invite needs --channel <channel>`;
|
|
4575
|
+
}
|
|
4576
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4577
|
+
deps.err(`\u2717 Braid could not revoke invite ${sanitizeLabel(inviteId, 40)}: ${msg}`);
|
|
4578
|
+
return 1;
|
|
4579
|
+
}
|
|
4580
|
+
if (args2.json) {
|
|
4581
|
+
deps.out(
|
|
4582
|
+
JSON.stringify({
|
|
4583
|
+
ok: true,
|
|
4584
|
+
inviteId: sanitizeForTty(inviteId),
|
|
4585
|
+
...ch ? { channelId: sanitizeForTty(ch.id), channel: sanitizeForTty(ch.name ?? "") } : {},
|
|
4586
|
+
revoked: true
|
|
4587
|
+
})
|
|
4588
|
+
);
|
|
4589
|
+
}
|
|
4590
|
+
deps.err(`\u2714 invite ${sanitizeLabel(inviteId, 40)} revoked \u2014 it can no longer be redeemed`);
|
|
4591
|
+
return 0;
|
|
4592
|
+
}
|
|
4593
|
+
function looksLikeEmail(email) {
|
|
4594
|
+
return /^[^\s@]+@[^\s@]+$/.test(email);
|
|
4595
|
+
}
|
|
4596
|
+
async function runInviteHuman(instance2, args2, deps) {
|
|
4597
|
+
const [channelRef, emailRaw] = args2.rest.map((s) => s?.trim());
|
|
4598
|
+
if (!channelRef || !emailRaw) {
|
|
4599
|
+
deps.err("usage: braid invite human <channel> <email>");
|
|
4600
|
+
return 1;
|
|
4601
|
+
}
|
|
4602
|
+
const email = emailRaw.toLowerCase();
|
|
4603
|
+
if (!looksLikeEmail(email)) {
|
|
4604
|
+
const msg = `"${sanitizeLabel(emailRaw)}" does not look like an email address`;
|
|
4605
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4606
|
+
deps.err(`\u2717 ${msg}`);
|
|
4607
|
+
return 1;
|
|
4608
|
+
}
|
|
4609
|
+
const creds = await ensureSignedIn(deps);
|
|
4610
|
+
const rest = oneShotRest(instance2, creds);
|
|
4611
|
+
const ch = await resolveChannel(rest, channelRef, "active", args2, deps);
|
|
4612
|
+
if (!ch) return 1;
|
|
4613
|
+
const chName = sanitizeLabel(ch.name ?? "");
|
|
4614
|
+
let result;
|
|
4615
|
+
try {
|
|
4616
|
+
result = await rest.inviteHumanToChannel(ch.id, email);
|
|
4617
|
+
} catch (err) {
|
|
4618
|
+
const msg = inviteErrorMessage(err);
|
|
4619
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4620
|
+
deps.err(`\u2717 Braid could not invite ${sanitizeLabel(email)} to #${chName}: ${msg}`);
|
|
4621
|
+
return 1;
|
|
4622
|
+
}
|
|
4623
|
+
if (args2.json) {
|
|
4624
|
+
deps.out(JSON.stringify(result));
|
|
4625
|
+
return 0;
|
|
4626
|
+
}
|
|
4627
|
+
if (result.note) {
|
|
4628
|
+
deps.err(`\u2714 ${sanitizeLabel(email)} \u2014 ${sanitizeLabel(result.note, 80)}`);
|
|
4629
|
+
} else if (result.guest) {
|
|
4630
|
+
deps.err(
|
|
4631
|
+
`\u2714 ${sanitizeLabel(email)} added to #${chName} as a cross-org guest \u2014 they see this channel only, not your org directory`
|
|
4632
|
+
);
|
|
4633
|
+
} else {
|
|
4634
|
+
deps.err(
|
|
4635
|
+
`\u2714 ${sanitizeLabel(email)} invited \u2014 Braid emailed them; they'll land in #${chName} when they accept`
|
|
4636
|
+
);
|
|
4637
|
+
}
|
|
4638
|
+
return 0;
|
|
4639
|
+
}
|
|
4640
|
+
|
|
4641
|
+
// src/commands/org.ts
|
|
4642
|
+
var ORG_USAGE = `usage: braid org [--json]
|
|
4643
|
+
braid org guest-agents <on|off> [--yes] [--json]
|
|
4644
|
+
|
|
4645
|
+
org show your organization: id and whether guest agents
|
|
4646
|
+
(agents owned outside the org) are allowed in
|
|
4647
|
+
guest-agents on|off allow or block guest agents org-wide \u2014 this changes
|
|
4648
|
+
live cross-org access, so it asks y/N (--yes skips;
|
|
4649
|
+
org admin only)
|
|
4650
|
+
|
|
4651
|
+
--yes skip the confirmation prompt (required with --json on guest-agents)
|
|
4652
|
+
--json machine-readable result on stdout`;
|
|
4653
|
+
async function runOrgCli(instance2, args2, overrides = {}) {
|
|
4654
|
+
const deps = {
|
|
4655
|
+
resolveCreds: () => resolveCredentials(instance2, args2.flagToken),
|
|
4656
|
+
runLogin: async () => (await login(instance2)).credentials,
|
|
4657
|
+
out: (line) => console.log(line),
|
|
4658
|
+
err: (line) => console.error(line),
|
|
4659
|
+
prompt: promptOnTty,
|
|
4660
|
+
...overrides
|
|
4661
|
+
};
|
|
4662
|
+
try {
|
|
4663
|
+
if (!args2.sub) return await runOrgShow(instance2, args2, deps);
|
|
4664
|
+
if (args2.sub === "guest-agents") return await runOrgGuestAgents(instance2, args2, deps);
|
|
4665
|
+
deps.err(ORG_USAGE);
|
|
4666
|
+
deps.err(`
|
|
4667
|
+
unknown org command "${sanitizeLabel(args2.sub)}".`);
|
|
4668
|
+
return 1;
|
|
4669
|
+
} catch (err) {
|
|
4670
|
+
deps.err(sanitizeForTty(err instanceof Error ? err.message : String(err)));
|
|
4671
|
+
return 1;
|
|
4672
|
+
}
|
|
4673
|
+
}
|
|
4674
|
+
function orgErrorMessage(err) {
|
|
4675
|
+
if (err instanceof RelayError) {
|
|
4676
|
+
if (err.status === 403 && err.message === "admin_required") {
|
|
4677
|
+
return "you need to be an org admin on Braid to change org settings";
|
|
4678
|
+
}
|
|
4679
|
+
if (err.status === 404) {
|
|
4680
|
+
return "no active organization \u2014 sign in on Braid web and pick an organization first";
|
|
4681
|
+
}
|
|
4682
|
+
}
|
|
4683
|
+
return sanitizeForTty(err instanceof Error ? err.message : String(err));
|
|
4684
|
+
}
|
|
4685
|
+
async function fetchMyOrg(rest, args2, deps) {
|
|
4686
|
+
try {
|
|
4687
|
+
return await rest.getMyOrganization();
|
|
4688
|
+
} catch (err) {
|
|
4689
|
+
const msg = orgErrorMessage(err);
|
|
4690
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4691
|
+
deps.err(`\u2717 Braid could not read your organization: ${msg}`);
|
|
4692
|
+
return null;
|
|
4693
|
+
}
|
|
4694
|
+
}
|
|
4695
|
+
async function runOrgShow(instance2, args2, deps) {
|
|
4696
|
+
const creds = await ensureSignedIn(deps);
|
|
4697
|
+
const org = await fetchMyOrg(oneShotRest(instance2, creds), args2, deps);
|
|
4698
|
+
if (!org) return 1;
|
|
4699
|
+
if (args2.json) {
|
|
4700
|
+
deps.out(JSON.stringify(org, null, 2));
|
|
4701
|
+
return 0;
|
|
4702
|
+
}
|
|
4703
|
+
const row = (label, value) => deps.out(`${(label + ":").padEnd(14)}${value}`);
|
|
4704
|
+
row("org id", sanitizeLabel(org.id ?? "", 40));
|
|
4705
|
+
row("guest agents", org.allowGuestAgents ? "allowed \u2014 agents owned outside the org may join channels" : "blocked");
|
|
4706
|
+
return 0;
|
|
4707
|
+
}
|
|
4708
|
+
async function runOrgGuestAgents(instance2, args2, deps) {
|
|
4709
|
+
const value = args2.rest[0]?.trim();
|
|
4710
|
+
if (value !== "on" && value !== "off") {
|
|
4711
|
+
deps.err("usage: braid org guest-agents <on|off>");
|
|
4712
|
+
if (value) deps.err(`
|
|
4713
|
+
unknown value "${sanitizeLabel(value)}" \u2014 say on or off.`);
|
|
4714
|
+
return 1;
|
|
4715
|
+
}
|
|
4716
|
+
const allow = value === "on";
|
|
4717
|
+
const creds = await ensureSignedIn(deps);
|
|
4718
|
+
const rest = oneShotRest(instance2, creds);
|
|
4719
|
+
const org = await fetchMyOrg(rest, args2, deps);
|
|
4720
|
+
if (!org) return 1;
|
|
4721
|
+
if (org.allowGuestAgents === allow) {
|
|
4722
|
+
if (args2.json) deps.out(JSON.stringify({ ok: true, allowGuestAgents: allow, changed: false }));
|
|
4723
|
+
deps.err(`\u2714 guest agents are already ${allow ? "allowed" : "blocked"} \u2014 nothing to change`);
|
|
4724
|
+
return 0;
|
|
4725
|
+
}
|
|
4726
|
+
const confirmed = await confirmSimple(
|
|
4727
|
+
args2,
|
|
4728
|
+
deps,
|
|
4729
|
+
allow ? "allow guest agents? agents owned OUTSIDE your org can then be invited into your channels." : "block guest agents? no NEW cross-org agents or human guests can join your channels (current members are not removed)."
|
|
4730
|
+
);
|
|
4731
|
+
if (!confirmed) return 1;
|
|
4732
|
+
try {
|
|
4733
|
+
await rest.patchOrganization(org.id, { allowGuestAgents: allow });
|
|
4734
|
+
} catch (err) {
|
|
4735
|
+
const msg = orgErrorMessage(err);
|
|
4736
|
+
if (args2.json) deps.out(JSON.stringify({ ok: false, error: msg }));
|
|
4737
|
+
deps.err(`\u2717 Braid could not update the guest-agent policy: ${msg}`);
|
|
4738
|
+
return 1;
|
|
4739
|
+
}
|
|
4740
|
+
if (args2.json) deps.out(JSON.stringify({ ok: true, allowGuestAgents: allow, changed: true }));
|
|
4741
|
+
deps.err(
|
|
4742
|
+
allow ? "\u2714 guest agents allowed \u2014 cross-org agents can now be invited into your channels" : "\u2714 guest agents blocked \u2014 new cross-org agents and guests can no longer join your channels"
|
|
4743
|
+
);
|
|
4744
|
+
return 0;
|
|
4745
|
+
}
|
|
4746
|
+
|
|
2850
4747
|
// src/index.tsx
|
|
2851
|
-
import { jsx as
|
|
4748
|
+
import { jsx as jsx13 } from "react/jsx-runtime";
|
|
2852
4749
|
function helpText(profileName2, base2, effective) {
|
|
2853
4750
|
const relayOverridden = effective.relayUrl !== base2.relayUrl;
|
|
2854
4751
|
const overrideNote = relayOverridden ? `
|
|
@@ -2857,7 +4754,7 @@ function helpText(profileName2, base2, effective) {
|
|
|
2857
4754
|
instance \u2014 a JWT minted there will NOT work against the override)` : "";
|
|
2858
4755
|
return `arp-tui \u2014 tail + post for Agent Relay Protocol channels (as the HUMAN)
|
|
2859
4756
|
|
|
2860
|
-
usage: arp-tui [tail|login|logout|status|profile] [--profile <name>] [--relay <url>] [--token <jwt>]
|
|
4757
|
+
usage: arp-tui [tail|login|logout|status|profile|agent <verb>|agents|channel <verb>|channels|invite <verb>|invites|org] [--profile <name>] [--relay <url>] [--token <jwt>]
|
|
2861
4758
|
|
|
2862
4759
|
commands:
|
|
2863
4760
|
tail live channel tail + post (default)
|
|
@@ -2866,6 +4763,34 @@ commands:
|
|
|
2866
4763
|
logout revoke the OAuth grant and delete this instance's stored tokens
|
|
2867
4764
|
status show the stored credential state for the active instance
|
|
2868
4765
|
profile manage saved instances (list|add|switch|remove; run \`arp-tui profile\`)
|
|
4766
|
+
agent add <name> create a personal agent on Braid, enroll it on this machine
|
|
4767
|
+
via the local bridge, and start it (--no-start to skip;
|
|
4768
|
+
--bridge-cmd/--provider/--json \u2014 run \`arp-tui agent\` for details)
|
|
4769
|
+
agent status|reissue|revoke|remove <name>
|
|
4770
|
+
inspect or recover an agent: detail card, fresh credential +
|
|
4771
|
+
re-enroll, credential revoke, permanent delete (revoke/remove
|
|
4772
|
+
confirm on the TTY; --yes with --json)
|
|
4773
|
+
agents list your agents: name, online/offline, credential state (--json)
|
|
4774
|
+
channels list your channels (--all includes archived; --json)
|
|
4775
|
+
channel create|archive|unarchive|members <name-or-id>
|
|
4776
|
+
channel lifecycle: create, archive (reversible; y/N confirm,
|
|
4777
|
+
--yes skips), restore, roster (run \`arp-tui channel\` for details)
|
|
4778
|
+
channel member add|remove <channel> <agent>
|
|
4779
|
+
add or remove one of your agents on a channel
|
|
4780
|
+
invites [<channel>]
|
|
4781
|
+
list agent invites: the org's channel-less invites, or one
|
|
4782
|
+
channel's (--all includes revoked/expired; --json)
|
|
4783
|
+
invite agent <channel> <name>
|
|
4784
|
+
mint a channel invite for a NEW agent \u2014 prints the join
|
|
4785
|
+
command ONCE (org admin; run \`arp-tui invite\` for details)
|
|
4786
|
+
invite human <channel> <email>
|
|
4787
|
+
invite a human by email into the channel (org admin)
|
|
4788
|
+
invite revoke <invite-id> [--channel <ch>]
|
|
4789
|
+
revoke an invite (y/N confirm, --yes skips)
|
|
4790
|
+
org show your organization: id + guest-agent policy (--json)
|
|
4791
|
+
org guest-agents on|off
|
|
4792
|
+
allow or block cross-org guest agents org-wide (org admin;
|
|
4793
|
+
y/N confirm, --yes skips)
|
|
2869
4794
|
|
|
2870
4795
|
options:
|
|
2871
4796
|
--profile <name> instance profile from ~/.arp-tui/config.json
|
|
@@ -2899,6 +4824,12 @@ function parseCliArgs(argv) {
|
|
|
2899
4824
|
"clerk-fapi": { type: "string" },
|
|
2900
4825
|
"clerk-template": { type: "string" },
|
|
2901
4826
|
"clerk-oauth-client-id": { type: "string" },
|
|
4827
|
+
provider: { type: "string" },
|
|
4828
|
+
"no-start": { type: "boolean" },
|
|
4829
|
+
"bridge-cmd": { type: "string" },
|
|
4830
|
+
channel: { type: "string" },
|
|
4831
|
+
all: { type: "boolean" },
|
|
4832
|
+
yes: { type: "boolean" },
|
|
2902
4833
|
json: { type: "boolean" },
|
|
2903
4834
|
help: { type: "boolean", short: "h" }
|
|
2904
4835
|
},
|
|
@@ -2912,6 +4843,7 @@ function parseCliArgs(argv) {
|
|
|
2912
4843
|
cmd: parsed.positionals[0] ?? "tail",
|
|
2913
4844
|
sub: parsed.positionals[1],
|
|
2914
4845
|
name: parsed.positionals[2],
|
|
4846
|
+
positionals: parsed.positionals,
|
|
2915
4847
|
profile: parsed.values.profile,
|
|
2916
4848
|
relay: parsed.values.relay,
|
|
2917
4849
|
token: parsed.values.token,
|
|
@@ -2919,6 +4851,12 @@ function parseCliArgs(argv) {
|
|
|
2919
4851
|
clerkFapi: parsed.values["clerk-fapi"],
|
|
2920
4852
|
clerkTemplate: parsed.values["clerk-template"],
|
|
2921
4853
|
clerkOAuthClientId: parsed.values["clerk-oauth-client-id"],
|
|
4854
|
+
provider: parsed.values.provider,
|
|
4855
|
+
noStart: parsed.values["no-start"] === true,
|
|
4856
|
+
bridgeCmd: parsed.values["bridge-cmd"],
|
|
4857
|
+
channel: parsed.values.channel,
|
|
4858
|
+
all: parsed.values.all === true,
|
|
4859
|
+
yes: parsed.values.yes === true,
|
|
2922
4860
|
json: parsed.values.json === true,
|
|
2923
4861
|
help: parsed.values.help === true
|
|
2924
4862
|
};
|
|
@@ -3026,6 +4964,53 @@ if (args.cmd === "login") {
|
|
|
3026
4964
|
}
|
|
3027
4965
|
}
|
|
3028
4966
|
process.exit(0);
|
|
4967
|
+
} else if (args.cmd === "agent" || args.cmd === "agents") {
|
|
4968
|
+
const code = await runAgentCli(instance, {
|
|
4969
|
+
cmd: args.cmd,
|
|
4970
|
+
sub: args.sub,
|
|
4971
|
+
name: args.name,
|
|
4972
|
+
provider: args.provider,
|
|
4973
|
+
noStart: args.noStart,
|
|
4974
|
+
bridgeCmd: args.bridgeCmd,
|
|
4975
|
+
yes: args.yes,
|
|
4976
|
+
json: args.json,
|
|
4977
|
+
flagToken: args.token
|
|
4978
|
+
});
|
|
4979
|
+
process.exit(code);
|
|
4980
|
+
} else if (args.cmd === "channel" || args.cmd === "channels") {
|
|
4981
|
+
const code = await runChannelCli(instance, {
|
|
4982
|
+
cmd: args.cmd,
|
|
4983
|
+
sub: args.sub,
|
|
4984
|
+
rest: args.positionals.slice(2),
|
|
4985
|
+
all: args.all,
|
|
4986
|
+
yes: args.yes,
|
|
4987
|
+
json: args.json,
|
|
4988
|
+
flagToken: args.token
|
|
4989
|
+
});
|
|
4990
|
+
process.exit(code);
|
|
4991
|
+
} else if (args.cmd === "invite" || args.cmd === "invites") {
|
|
4992
|
+
const code = await runInviteCli(instance, {
|
|
4993
|
+
cmd: args.cmd,
|
|
4994
|
+
sub: args.sub,
|
|
4995
|
+
// `invites [<channel>]` has no subcommand — its positionals start right after the verb
|
|
4996
|
+
rest: args.cmd === "invites" ? args.positionals.slice(1) : args.positionals.slice(2),
|
|
4997
|
+
channel: args.channel,
|
|
4998
|
+
all: args.all,
|
|
4999
|
+
yes: args.yes,
|
|
5000
|
+
json: args.json,
|
|
5001
|
+
flagToken: args.token
|
|
5002
|
+
});
|
|
5003
|
+
process.exit(code);
|
|
5004
|
+
} else if (args.cmd === "org") {
|
|
5005
|
+
const code = await runOrgCli(instance, {
|
|
5006
|
+
cmd: args.cmd,
|
|
5007
|
+
sub: args.sub,
|
|
5008
|
+
rest: args.positionals.slice(2),
|
|
5009
|
+
yes: args.yes,
|
|
5010
|
+
json: args.json,
|
|
5011
|
+
flagToken: args.token
|
|
5012
|
+
});
|
|
5013
|
+
process.exit(code);
|
|
3029
5014
|
} else if (args.cmd !== "tail") {
|
|
3030
5015
|
console.error(`unknown command "${args.cmd}" \u2014 try --help.`);
|
|
3031
5016
|
process.exit(1);
|
|
@@ -3043,4 +5028,4 @@ if (credentials?.tokenType === "dev-jwt" && credentials.token && !credentials.cl
|
|
|
3043
5028
|
);
|
|
3044
5029
|
}
|
|
3045
5030
|
var store = createStore({ instance, credentials });
|
|
3046
|
-
render(/* @__PURE__ */
|
|
5031
|
+
render(/* @__PURE__ */ jsx13(App, { store }));
|