arp-tui 0.0.1 → 0.0.2
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/README.md +20 -0
- package/dist/index.js +503 -61
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -38,6 +38,7 @@ the agents and people in your ARP workspace.
|
|
|
38
38
|
- `arp-tui login` — sign in once via your browser
|
|
39
39
|
- `arp-tui logout` — revoke access and delete stored tokens
|
|
40
40
|
- `arp-tui status` — show stored credentials and their state
|
|
41
|
+
- `arp-tui profile` — manage saved instances (see [Profiles](#profiles))
|
|
41
42
|
|
|
42
43
|
## Options
|
|
43
44
|
|
|
@@ -45,6 +46,25 @@ the agents and people in your ARP workspace.
|
|
|
45
46
|
- `--profile <name>` — use a named credential profile
|
|
46
47
|
- `-h`, `--help` — full usage
|
|
47
48
|
|
|
49
|
+
## Profiles
|
|
50
|
+
|
|
51
|
+
A profile is a named ARP instance (relay + web + auth coordinates). The client
|
|
52
|
+
ships with a default profile and stores your profiles in `~/.arp-tui/config.json`.
|
|
53
|
+
Manage them without hand-editing that file:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
arp-tui profile list # show saved profiles (active marked *)
|
|
57
|
+
arp-tui profile add work --relay https://relay.example.com \
|
|
58
|
+
--web https://app.example.com \
|
|
59
|
+
--clerk-fapi https://your-instance.clerk.accounts.dev
|
|
60
|
+
arp-tui profile switch work # make "work" the active profile
|
|
61
|
+
arp-tui profile remove work # delete a profile
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Run any command against one profile without changing the active one using
|
|
65
|
+
`--profile <name>` (or the `ARP_TUI_PROFILE` environment variable). Credentials
|
|
66
|
+
are stored per instance, so switching profiles never mixes up your sign-ins.
|
|
67
|
+
|
|
48
68
|
## Requirements
|
|
49
69
|
|
|
50
70
|
Node.js >= 20.
|
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 Box7, Text as Text9, useApp, useInput, useStdin } from "ink";
|
|
9
9
|
import { useEffect, useSyncExternalStore } from "react";
|
|
10
10
|
|
|
11
11
|
// src/state/store.ts
|
|
@@ -180,7 +180,9 @@ Profiles defined: ${Object.keys(profiles).join(", ")}. Fix "profile" or add the
|
|
|
180
180
|
profile: requested,
|
|
181
181
|
profiles,
|
|
182
182
|
lastChannelId: typeof cfg["lastChannelId"] === "string" ? cfg["lastChannelId"] : void 0,
|
|
183
|
-
lastSeq: cfg["lastSeq"] && typeof cfg["lastSeq"] === "object" ? cfg["lastSeq"] : {}
|
|
183
|
+
lastSeq: cfg["lastSeq"] && typeof cfg["lastSeq"] === "object" ? cfg["lastSeq"] : {},
|
|
184
|
+
unread: cfg["unread"] && typeof cfg["unread"] === "object" ? cfg["unread"] : {},
|
|
185
|
+
lastSeen: cfg["lastSeen"] && typeof cfg["lastSeen"] === "object" ? cfg["lastSeen"] : {}
|
|
184
186
|
};
|
|
185
187
|
}
|
|
186
188
|
function resolveProfile(cfg, overrides) {
|
|
@@ -205,6 +207,61 @@ function resolveProfile(cfg, overrides) {
|
|
|
205
207
|
function saveConfig(cfg) {
|
|
206
208
|
writeFileAtomic0600(CONFIG_FILE, JSON.stringify(cfg, null, 2) + "\n");
|
|
207
209
|
}
|
|
210
|
+
var PROFILE_NAME_RE = /^[A-Za-z0-9._-]+$/;
|
|
211
|
+
function assertValidProfileName(name) {
|
|
212
|
+
if (!PROFILE_NAME_RE.test(name)) {
|
|
213
|
+
throw new ConfigError(
|
|
214
|
+
`invalid profile name "${name}" \u2014 use letters, digits, dot, dash or underscore only.`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
function listProfiles() {
|
|
219
|
+
const cfg = loadConfig();
|
|
220
|
+
return { active: cfg.profile, profiles: cfg.profiles };
|
|
221
|
+
}
|
|
222
|
+
function addProfile(name, raw) {
|
|
223
|
+
assertValidProfileName(name);
|
|
224
|
+
const cfg = loadConfig();
|
|
225
|
+
if (Object.hasOwn(cfg.profiles, name)) {
|
|
226
|
+
throw new ConfigError(
|
|
227
|
+
`profile "${name}" already exists (${cfg.profiles[name].relayUrl}).
|
|
228
|
+
Remove it first (arp-tui profile remove ${name}) or choose another name.`
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
const profile = normalizeProfile(name, raw);
|
|
232
|
+
cfg.profiles[name] = profile;
|
|
233
|
+
saveConfig(cfg);
|
|
234
|
+
return profile;
|
|
235
|
+
}
|
|
236
|
+
function switchProfile(name) {
|
|
237
|
+
const cfg = loadConfig();
|
|
238
|
+
if (!Object.hasOwn(cfg.profiles, name)) {
|
|
239
|
+
throw new ConfigError(
|
|
240
|
+
`profile "${name}" does not exist. Defined: ${Object.keys(cfg.profiles).join(", ")}.`
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
cfg.profile = name;
|
|
244
|
+
saveConfig(cfg);
|
|
245
|
+
return cfg.profiles[name];
|
|
246
|
+
}
|
|
247
|
+
function removeProfile(name) {
|
|
248
|
+
const cfg = loadConfig();
|
|
249
|
+
if (!Object.hasOwn(cfg.profiles, name)) {
|
|
250
|
+
throw new ConfigError(
|
|
251
|
+
`profile "${name}" does not exist. Defined: ${Object.keys(cfg.profiles).join(", ")}.`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
if (Object.keys(cfg.profiles).length === 1) {
|
|
255
|
+
throw new ConfigError(`cannot remove the only profile "${name}".`);
|
|
256
|
+
}
|
|
257
|
+
if (cfg.profile === name) {
|
|
258
|
+
throw new ConfigError(
|
|
259
|
+
`cannot remove the active profile "${name}" \u2014 switch to another first (arp-tui profile switch <name>).`
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
delete cfg.profiles[name];
|
|
263
|
+
saveConfig(cfg);
|
|
264
|
+
}
|
|
208
265
|
function assertStorePerms() {
|
|
209
266
|
ensureDir0700(CONFIG_DIR);
|
|
210
267
|
const problems = [
|
|
@@ -724,8 +781,6 @@ async function logout(instance2, log = console.error) {
|
|
|
724
781
|
}
|
|
725
782
|
|
|
726
783
|
// src/api/clerkRefresh.ts
|
|
727
|
-
var DEFAULT_CLERK_FAPI = "https://generous-fish-48.clerk.accounts.dev";
|
|
728
|
-
var DEFAULT_JWT_TEMPLATE = "arp";
|
|
729
784
|
var CACHE_TTL_MS = 45e3;
|
|
730
785
|
var FAPI_TIMEOUT_MS = 1e4;
|
|
731
786
|
var ClerkRefreshError = class extends Error {
|
|
@@ -734,9 +789,9 @@ var ClerkRefreshError = class extends Error {
|
|
|
734
789
|
this.name = "ClerkRefreshError";
|
|
735
790
|
}
|
|
736
791
|
};
|
|
737
|
-
function createClerkTokenSupplier(clientToken, opts
|
|
738
|
-
const fapi =
|
|
739
|
-
const template = opts.template
|
|
792
|
+
function createClerkTokenSupplier(clientToken, opts) {
|
|
793
|
+
const fapi = opts.fapiUrl.replace(/\/$/, "");
|
|
794
|
+
const template = opts.template;
|
|
740
795
|
let cached = null;
|
|
741
796
|
let sessionId = null;
|
|
742
797
|
async function fapiFetch(path3, method) {
|
|
@@ -893,8 +948,54 @@ var RelayRest = class {
|
|
|
893
948
|
async setPresence(status) {
|
|
894
949
|
await this.req("POST", "/presence", { status });
|
|
895
950
|
}
|
|
951
|
+
/** GET /attention — cross-channel unacked attention summaries (best-effort caller) */
|
|
952
|
+
async getAttention() {
|
|
953
|
+
const data = await this.req("GET", "/attention");
|
|
954
|
+
return data.attention ?? [];
|
|
955
|
+
}
|
|
956
|
+
/** GET /notifications — human-only; newest-first, <=50 */
|
|
957
|
+
async getNotifications() {
|
|
958
|
+
const data = await this.req("GET", "/notifications");
|
|
959
|
+
return data.notifications ?? [];
|
|
960
|
+
}
|
|
961
|
+
/** POST /notifications/read-all */
|
|
962
|
+
async markAllNotificationsRead() {
|
|
963
|
+
await this.req("POST", "/notifications/read-all", {});
|
|
964
|
+
}
|
|
965
|
+
/** POST /notifications/{id}/dismiss — archives one; 404 for not-yours/nonexistent (G3) */
|
|
966
|
+
async dismissNotification(id) {
|
|
967
|
+
await this.req("POST", `/notifications/${encodeURIComponent(id)}/dismiss`, {});
|
|
968
|
+
}
|
|
896
969
|
};
|
|
897
970
|
|
|
971
|
+
// src/state/priority.ts
|
|
972
|
+
function channelPriority(cid, p) {
|
|
973
|
+
const level = p.attention[cid]?.highestLevel;
|
|
974
|
+
if (level === "critical") return 0;
|
|
975
|
+
if (level === "action_required") return 1;
|
|
976
|
+
if (level === "info") return 2;
|
|
977
|
+
if ((p.unread[cid] ?? 0) > 0) return 3;
|
|
978
|
+
return 4;
|
|
979
|
+
}
|
|
980
|
+
function sortChannelsByPriority(channels, p) {
|
|
981
|
+
return channels.map((c, i) => ({ c, pri: channelPriority(c.id, p), i })).sort((a, b) => a.pri - b.pri || a.i - b.i).map((x) => x.c);
|
|
982
|
+
}
|
|
983
|
+
function nextActiveChannel(channels, activeId, p) {
|
|
984
|
+
const ordered = sortChannelsByPriority(
|
|
985
|
+
channels.filter((c) => channelPriority(c.id, p) < 4),
|
|
986
|
+
p
|
|
987
|
+
);
|
|
988
|
+
if (ordered.length === 0) return null;
|
|
989
|
+
const idx = ordered.findIndex((c) => c.id === activeId);
|
|
990
|
+
if (idx === -1) {
|
|
991
|
+
return ordered[0] ?? null;
|
|
992
|
+
}
|
|
993
|
+
const next = ordered[(idx + 1) % ordered.length];
|
|
994
|
+
if (!next) return null;
|
|
995
|
+
if (next.id === activeId) return null;
|
|
996
|
+
return next;
|
|
997
|
+
}
|
|
998
|
+
|
|
898
999
|
// src/api/ws.ts
|
|
899
1000
|
import { appendFileSync } from "fs";
|
|
900
1001
|
import WebSocket from "ws";
|
|
@@ -1166,7 +1267,12 @@ function createStore(opts) {
|
|
|
1166
1267
|
// "on" only once it actually mints (getToken flips it)
|
|
1167
1268
|
clerkRefresh: creds?.clerkClientToken && creds.tokenType !== "oauth" ? "on" : "off",
|
|
1168
1269
|
tokenPrompt: "",
|
|
1169
|
-
tokenPromptReason: null
|
|
1270
|
+
tokenPromptReason: null,
|
|
1271
|
+
unread: { ...config.unread ?? {} },
|
|
1272
|
+
unreadCapNotice: null,
|
|
1273
|
+
attention: {},
|
|
1274
|
+
notifications: [],
|
|
1275
|
+
notifSelected: 0
|
|
1170
1276
|
};
|
|
1171
1277
|
let lastNotifyAt = 0;
|
|
1172
1278
|
let notifyTimer = null;
|
|
@@ -1345,6 +1451,75 @@ function createStore(opts) {
|
|
|
1345
1451
|
});
|
|
1346
1452
|
return fresh.length;
|
|
1347
1453
|
}
|
|
1454
|
+
const WS_SUB_CAP = 256;
|
|
1455
|
+
function bumpUnread(channelId) {
|
|
1456
|
+
if (channelId === state.activeChannelId) return;
|
|
1457
|
+
const next = (state.unread[channelId] ?? 0) + 1;
|
|
1458
|
+
const unread = { ...state.unread, [channelId]: next };
|
|
1459
|
+
config = { ...config, unread };
|
|
1460
|
+
saveConfigSoon();
|
|
1461
|
+
setThrottled({ unread });
|
|
1462
|
+
}
|
|
1463
|
+
function clearUnread(channelId) {
|
|
1464
|
+
const lastSeen = { ...config.lastSeen ?? {}, [channelId]: (/* @__PURE__ */ new Date()).toISOString() };
|
|
1465
|
+
const unread = { ...state.unread };
|
|
1466
|
+
delete unread[channelId];
|
|
1467
|
+
config = { ...config, unread, lastSeen };
|
|
1468
|
+
saveConfigSoon();
|
|
1469
|
+
set({ unread });
|
|
1470
|
+
}
|
|
1471
|
+
const ATTENTION_POLL_MS = 3e4;
|
|
1472
|
+
const NOTIF_POLL_MS = 6e4;
|
|
1473
|
+
let attentionTimer = null;
|
|
1474
|
+
let notifTimer = null;
|
|
1475
|
+
async function pollAttention() {
|
|
1476
|
+
try {
|
|
1477
|
+
const list = await rest.getAttention();
|
|
1478
|
+
const map = {};
|
|
1479
|
+
for (const a of list) map[a.channelId] = a;
|
|
1480
|
+
setThrottled({ attention: map });
|
|
1481
|
+
} catch {
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
async function pollNotifications() {
|
|
1485
|
+
try {
|
|
1486
|
+
setThrottled({ notifications: await rest.getNotifications() });
|
|
1487
|
+
} catch {
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
async function dismissNotification(id) {
|
|
1491
|
+
const prev = state.notifications;
|
|
1492
|
+
const prevSel = state.notifSelected;
|
|
1493
|
+
const next = prev.filter((n) => n.id !== id);
|
|
1494
|
+
const clampedSel = Math.max(0, Math.min(prevSel, next.length - 1));
|
|
1495
|
+
setThrottled({ notifications: next, notifSelected: clampedSel });
|
|
1496
|
+
try {
|
|
1497
|
+
await rest.dismissNotification(id);
|
|
1498
|
+
} catch {
|
|
1499
|
+
setThrottled({ notifications: prev, notifSelected: prevSel });
|
|
1500
|
+
}
|
|
1501
|
+
}
|
|
1502
|
+
async function markAllNotificationsRead() {
|
|
1503
|
+
const stamped = state.notifications.map((n) => ({
|
|
1504
|
+
...n,
|
|
1505
|
+
readAt: n.readAt ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1506
|
+
}));
|
|
1507
|
+
setThrottled({ notifications: stamped });
|
|
1508
|
+
try {
|
|
1509
|
+
await rest.markAllNotificationsRead();
|
|
1510
|
+
} catch {
|
|
1511
|
+
void pollNotifications();
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
function moveNotifSelection(delta) {
|
|
1515
|
+
const len = state.notifications.length;
|
|
1516
|
+
if (len === 0) {
|
|
1517
|
+
set({ notifSelected: 0 });
|
|
1518
|
+
return;
|
|
1519
|
+
}
|
|
1520
|
+
const next = Math.max(0, Math.min(state.notifSelected + delta, len - 1));
|
|
1521
|
+
set({ notifSelected: next });
|
|
1522
|
+
}
|
|
1348
1523
|
function seedPresence(members) {
|
|
1349
1524
|
const presence = {};
|
|
1350
1525
|
for (const m of members) presence[m.id] = m.status === "online" ? "active" : m.status;
|
|
@@ -1438,6 +1613,8 @@ function createStore(opts) {
|
|
|
1438
1613
|
if (isReconnect) {
|
|
1439
1614
|
pendingGapCursor = config.lastSeq?.[state.activeChannelId ?? ""] ?? null;
|
|
1440
1615
|
void gapFill();
|
|
1616
|
+
void pollAttention();
|
|
1617
|
+
void pollNotifications();
|
|
1441
1618
|
}
|
|
1442
1619
|
});
|
|
1443
1620
|
ws.onFrame((frame) => {
|
|
@@ -1459,8 +1636,12 @@ function createStore(opts) {
|
|
|
1459
1636
|
appendMessages(frame.channelId, frame.messages ?? []);
|
|
1460
1637
|
return;
|
|
1461
1638
|
}
|
|
1462
|
-
case "new_message":
|
|
1463
|
-
|
|
1639
|
+
case "new_message":
|
|
1640
|
+
case "flow_message": {
|
|
1641
|
+
if (frame.channelId !== state.activeChannelId) {
|
|
1642
|
+
bumpUnread(frame.channelId);
|
|
1643
|
+
return;
|
|
1644
|
+
}
|
|
1464
1645
|
appendMessages(frame.channelId, [frame.message]);
|
|
1465
1646
|
const senderId = frame.message.agentId;
|
|
1466
1647
|
removeActivity(
|
|
@@ -1500,7 +1681,11 @@ function createStore(opts) {
|
|
|
1500
1681
|
return;
|
|
1501
1682
|
}
|
|
1502
1683
|
case "subscribe_error": {
|
|
1503
|
-
|
|
1684
|
+
if (frame.error === "limit") {
|
|
1685
|
+
set({ unreadCapNotice: "hit the channel-subscription cap \u2014 some channels are attention-only" });
|
|
1686
|
+
} else {
|
|
1687
|
+
set({ error: `subscribe failed for ${frame.channelId}: ${frame.error}` });
|
|
1688
|
+
}
|
|
1504
1689
|
return;
|
|
1505
1690
|
}
|
|
1506
1691
|
case "channel_archived": {
|
|
@@ -1624,7 +1809,19 @@ function createStore(opts) {
|
|
|
1624
1809
|
}
|
|
1625
1810
|
initialized = true;
|
|
1626
1811
|
set({ channels });
|
|
1812
|
+
const toSub = channels.slice(0, WS_SUB_CAP);
|
|
1813
|
+
for (const c of toSub) ws.subscribe(c.id);
|
|
1814
|
+
if (channels.length > WS_SUB_CAP)
|
|
1815
|
+
set({ unreadCapNotice: `unread tracked for ${WS_SUB_CAP} of ${channels.length} channels (attention still covers the rest)` });
|
|
1627
1816
|
ws.connect();
|
|
1817
|
+
void pollAttention();
|
|
1818
|
+
void pollNotifications();
|
|
1819
|
+
if (!attentionTimer) {
|
|
1820
|
+
attentionTimer = setInterval(() => void pollAttention(), ATTENTION_POLL_MS);
|
|
1821
|
+
}
|
|
1822
|
+
if (!notifTimer) {
|
|
1823
|
+
notifTimer = setInterval(() => void pollNotifications(), NOTIF_POLL_MS);
|
|
1824
|
+
}
|
|
1628
1825
|
reportPresence("active");
|
|
1629
1826
|
if (!idleTimer) {
|
|
1630
1827
|
idleTimer = setInterval(() => {
|
|
@@ -1645,7 +1842,6 @@ function createStore(opts) {
|
|
|
1645
1842
|
set({ mode: "normal" });
|
|
1646
1843
|
return;
|
|
1647
1844
|
}
|
|
1648
|
-
if (state.activeChannelId) ws.unsubscribe(state.activeChannelId);
|
|
1649
1845
|
const ch = state.channels.find((c) => c.id === channelId);
|
|
1650
1846
|
const members = ch?.members ?? [];
|
|
1651
1847
|
pushDivider(`${channelSigil(ch?.kind)}${ch?.name ?? channelId}`);
|
|
@@ -1659,6 +1855,7 @@ function createStore(opts) {
|
|
|
1659
1855
|
lastSeq: config.lastSeq?.[channelId] ?? null,
|
|
1660
1856
|
error: null
|
|
1661
1857
|
});
|
|
1858
|
+
clearUnread(channelId);
|
|
1662
1859
|
ws.subscribe(channelId);
|
|
1663
1860
|
config = { ...config, lastChannelId: channelId };
|
|
1664
1861
|
saveConfigSoon();
|
|
@@ -1694,6 +1891,14 @@ function createStore(opts) {
|
|
|
1694
1891
|
if (tickTimer) clearInterval(tickTimer);
|
|
1695
1892
|
if (activitySweep) clearTimeout(activitySweep);
|
|
1696
1893
|
if (countdownTimer) clearInterval(countdownTimer);
|
|
1894
|
+
if (attentionTimer) {
|
|
1895
|
+
clearInterval(attentionTimer);
|
|
1896
|
+
attentionTimer = null;
|
|
1897
|
+
}
|
|
1898
|
+
if (notifTimer) {
|
|
1899
|
+
clearInterval(notifTimer);
|
|
1900
|
+
notifTimer = null;
|
|
1901
|
+
}
|
|
1697
1902
|
if (notifyTimer) {
|
|
1698
1903
|
clearTimeout(notifyTimer);
|
|
1699
1904
|
notifyTimer = null;
|
|
@@ -1718,6 +1923,14 @@ function createStore(opts) {
|
|
|
1718
1923
|
sendDraft: () => void sendDraft(),
|
|
1719
1924
|
reconnectNow: () => ws.reconnectNow(),
|
|
1720
1925
|
noteUserActivity,
|
|
1926
|
+
dismissNotification: (id) => void dismissNotification(id),
|
|
1927
|
+
markAllNotificationsRead: () => void markAllNotificationsRead(),
|
|
1928
|
+
moveNotifSelection,
|
|
1929
|
+
jumpNextActive: () => {
|
|
1930
|
+
const s = state;
|
|
1931
|
+
const target = nextActiveChannel(s.channels, s.activeChannelId, { attention: s.attention, unread: s.unread });
|
|
1932
|
+
if (target) openChannel(target.id);
|
|
1933
|
+
},
|
|
1721
1934
|
quit
|
|
1722
1935
|
};
|
|
1723
1936
|
}
|
|
@@ -1761,21 +1974,53 @@ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
|
1761
1974
|
function channelLabel(ch) {
|
|
1762
1975
|
return `${channelSigil(ch.kind)}${sanitizeLabel(ch.name)}`;
|
|
1763
1976
|
}
|
|
1977
|
+
function ChannelBadge({
|
|
1978
|
+
channelId,
|
|
1979
|
+
activeChannelId,
|
|
1980
|
+
attention,
|
|
1981
|
+
unread
|
|
1982
|
+
}) {
|
|
1983
|
+
if (channelId === activeChannelId) return null;
|
|
1984
|
+
const att = attention[channelId];
|
|
1985
|
+
if (att) {
|
|
1986
|
+
const color = att.highestLevel === "critical" ? "red" : att.highestLevel === "action_required" ? "yellow" : "cyan";
|
|
1987
|
+
const sigil = att.highestLevel === "critical" ? "!" : att.highestLevel === "action_required" ? "\u25B2" : "\u2022";
|
|
1988
|
+
return /* @__PURE__ */ jsxs2(Text2, { color, children: [
|
|
1989
|
+
" ",
|
|
1990
|
+
"[",
|
|
1991
|
+
sigil,
|
|
1992
|
+
att.totalCount,
|
|
1993
|
+
"]"
|
|
1994
|
+
] });
|
|
1995
|
+
}
|
|
1996
|
+
const count = unread[channelId] ?? 0;
|
|
1997
|
+
if (count > 0) {
|
|
1998
|
+
return /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1999
|
+
" (",
|
|
2000
|
+
count,
|
|
2001
|
+
")"
|
|
2002
|
+
] });
|
|
2003
|
+
}
|
|
2004
|
+
return null;
|
|
2005
|
+
}
|
|
1764
2006
|
function ChannelSwitcher({
|
|
1765
2007
|
channels,
|
|
1766
2008
|
activeChannelId,
|
|
1767
|
-
filter
|
|
2009
|
+
filter,
|
|
2010
|
+
attention,
|
|
2011
|
+
unread
|
|
1768
2012
|
}) {
|
|
1769
2013
|
const filtered = filterChannels(channels, filter);
|
|
1770
|
-
const
|
|
1771
|
-
const
|
|
2014
|
+
const sorted = sortChannelsByPriority(filtered, { attention, unread });
|
|
2015
|
+
const visible = sorted.slice(0, 9);
|
|
2016
|
+
const hidden = sorted.length - visible.length;
|
|
1772
2017
|
return /* @__PURE__ */ jsxs2(Box, { flexDirection: "column", borderStyle: "round", borderColor: "cyan", paddingX: 1, children: [
|
|
1773
2018
|
/* @__PURE__ */ jsxs2(Text2, { bold: true, children: [
|
|
1774
2019
|
"channels",
|
|
1775
2020
|
" ",
|
|
1776
2021
|
/* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1777
2022
|
"(",
|
|
1778
|
-
|
|
2023
|
+
sorted.length,
|
|
1779
2024
|
"/",
|
|
1780
2025
|
channels.length,
|
|
1781
2026
|
")"
|
|
@@ -1796,19 +2041,28 @@ function ChannelSwitcher({
|
|
|
1796
2041
|
" \xB7 ",
|
|
1797
2042
|
ch.members.length,
|
|
1798
2043
|
" members"
|
|
1799
|
-
] })
|
|
2044
|
+
] }),
|
|
2045
|
+
/* @__PURE__ */ jsx2(
|
|
2046
|
+
ChannelBadge,
|
|
2047
|
+
{
|
|
2048
|
+
channelId: ch.id,
|
|
2049
|
+
activeChannelId,
|
|
2050
|
+
attention,
|
|
2051
|
+
unread
|
|
2052
|
+
}
|
|
2053
|
+
)
|
|
1800
2054
|
] }, ch.id)),
|
|
1801
2055
|
hidden > 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1802
2056
|
"\u2026",
|
|
1803
2057
|
hidden,
|
|
1804
2058
|
" more \u2014 keep typing to narrow"
|
|
1805
2059
|
] }) : null,
|
|
1806
|
-
|
|
2060
|
+
sorted.length === 0 ? /* @__PURE__ */ jsxs2(Text2, { dimColor: true, children: [
|
|
1807
2061
|
'no channels match "',
|
|
1808
2062
|
sanitizeLabel(filter, 20),
|
|
1809
2063
|
'"'
|
|
1810
2064
|
] }) : null,
|
|
1811
|
-
/* @__PURE__ */ jsx2(Text2, { dimColor: true, children: filter.length === 0 ? "type to filter \xB7 1\u20139 quick-select \xB7 Esc cancel" : "keep typing to narrow \xB7 Enter opens top match \xB7 Esc cancel" })
|
|
2065
|
+
/* @__PURE__ */ jsx2(Text2, { 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" })
|
|
1812
2066
|
] });
|
|
1813
2067
|
}
|
|
1814
2068
|
|
|
@@ -1933,14 +2187,70 @@ function MessageList({ messages }) {
|
|
|
1933
2187
|
return /* @__PURE__ */ jsx6(Static, { items: messages, children: (m) => /* @__PURE__ */ jsx6(MessageRow, { message: m }, m.id) });
|
|
1934
2188
|
}
|
|
1935
2189
|
|
|
2190
|
+
// src/ui/NotificationsPanel.tsx
|
|
2191
|
+
import { Box as Box5, Text as Text6 } from "ink";
|
|
2192
|
+
import { jsx as jsx7, jsxs as jsxs6 } from "react/jsx-runtime";
|
|
2193
|
+
function relativeAge(createdAt) {
|
|
2194
|
+
const ms = Date.now() - new Date(createdAt).getTime();
|
|
2195
|
+
if (!Number.isFinite(ms) || ms < 0) return "\u2014";
|
|
2196
|
+
if (ms < 1e4) return "just now";
|
|
2197
|
+
if (ms < 9e4) return `${Math.round(ms / 1e3)}s`;
|
|
2198
|
+
if (ms < 90 * 6e4) return `${Math.round(ms / 6e4)}m`;
|
|
2199
|
+
if (ms < 48 * 36e5) return `${Math.round(ms / 36e5)}h`;
|
|
2200
|
+
return `${Math.round(ms / 864e5)}d`;
|
|
2201
|
+
}
|
|
2202
|
+
function shortRef(entityRef) {
|
|
2203
|
+
return sanitizeLabel(entityRef, 24);
|
|
2204
|
+
}
|
|
2205
|
+
function NotificationsPanel({
|
|
2206
|
+
notifications,
|
|
2207
|
+
notifSelected
|
|
2208
|
+
}) {
|
|
2209
|
+
const unreadCount = notifications.filter((n) => !n.readAt).length;
|
|
2210
|
+
return /* @__PURE__ */ jsxs6(Box5, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2211
|
+
/* @__PURE__ */ jsxs6(Text6, { bold: true, children: [
|
|
2212
|
+
"alerts",
|
|
2213
|
+
" ",
|
|
2214
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2215
|
+
"(",
|
|
2216
|
+
unreadCount,
|
|
2217
|
+
" unread \xB7 ",
|
|
2218
|
+
notifications.length,
|
|
2219
|
+
" total)"
|
|
2220
|
+
] })
|
|
2221
|
+
] }),
|
|
2222
|
+
notifications.length === 0 ? /* @__PURE__ */ jsx7(Text6, { dimColor: true, children: "no notifications" }) : notifications.map((n, i) => {
|
|
2223
|
+
const isSelected = i === notifSelected;
|
|
2224
|
+
const isUnread = n.readAt === null;
|
|
2225
|
+
const type = sanitizeLabel(n.type, 20);
|
|
2226
|
+
const ref = shortRef(sanitizeForTty(n.entityRef));
|
|
2227
|
+
const age = relativeAge(n.createdAt);
|
|
2228
|
+
return /* @__PURE__ */ jsxs6(Text6, { inverse: isSelected, children: [
|
|
2229
|
+
isUnread ? /* @__PURE__ */ jsx7(Text6, { color: "yellow", children: "\u2022 " }) : /* @__PURE__ */ jsx7(Text6, { dimColor: true, children: " " }),
|
|
2230
|
+
/* @__PURE__ */ jsx7(Text6, { bold: isSelected, children: type }),
|
|
2231
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2232
|
+
" ",
|
|
2233
|
+
ref
|
|
2234
|
+
] }),
|
|
2235
|
+
/* @__PURE__ */ jsxs6(Text6, { dimColor: true, children: [
|
|
2236
|
+
" ",
|
|
2237
|
+
age
|
|
2238
|
+
] })
|
|
2239
|
+
] }, n.id);
|
|
2240
|
+
}),
|
|
2241
|
+
/* @__PURE__ */ jsx7(Text6, { dimColor: true, children: "J/K move \xB7 d dismiss \xB7 a mark all read \xB7 Esc close" })
|
|
2242
|
+
] });
|
|
2243
|
+
}
|
|
2244
|
+
|
|
1936
2245
|
// src/ui/StatusBar.tsx
|
|
1937
|
-
import { Text as
|
|
1938
|
-
import { Fragment, jsx as
|
|
2246
|
+
import { Text as Text7 } from "ink";
|
|
2247
|
+
import { Fragment, jsx as jsx8, jsxs as jsxs7 } from "react/jsx-runtime";
|
|
1939
2248
|
var MODE_LABEL = {
|
|
1940
2249
|
normal: "NORMAL",
|
|
1941
2250
|
insert: "INSERT",
|
|
1942
2251
|
switcher: "CHANNELS",
|
|
1943
|
-
token: "TOKEN"
|
|
2252
|
+
token: "TOKEN",
|
|
2253
|
+
notifications: "ALERTS"
|
|
1944
2254
|
};
|
|
1945
2255
|
function ageLabel(obtainedAt) {
|
|
1946
2256
|
if (!obtainedAt) return "\u2014";
|
|
@@ -1971,6 +2281,7 @@ function StatusBar({
|
|
|
1971
2281
|
tokenObtainedAt,
|
|
1972
2282
|
tokenRefreshable,
|
|
1973
2283
|
clerkRefresh,
|
|
2284
|
+
notifCount,
|
|
1974
2285
|
error
|
|
1975
2286
|
}) {
|
|
1976
2287
|
const connColor = connection === "connected" ? "green" : connection === "reconnecting" ? "yellow" : "red";
|
|
@@ -1990,37 +2301,41 @@ function StatusBar({
|
|
|
1990
2301
|
color: stale ? "yellow" : "green"
|
|
1991
2302
|
};
|
|
1992
2303
|
}
|
|
1993
|
-
return /* @__PURE__ */
|
|
1994
|
-
/* @__PURE__ */
|
|
2304
|
+
return /* @__PURE__ */ jsxs7(Text7, { children: [
|
|
2305
|
+
/* @__PURE__ */ jsxs7(Text7, { bold: true, color: mode === "insert" ? "cyan" : mode === "token" ? "yellow" : "white", children: [
|
|
1995
2306
|
" ",
|
|
1996
2307
|
MODE_LABEL[mode],
|
|
1997
2308
|
" "
|
|
1998
2309
|
] }),
|
|
1999
|
-
/* @__PURE__ */
|
|
2000
|
-
/* @__PURE__ */
|
|
2001
|
-
/* @__PURE__ */
|
|
2002
|
-
/* @__PURE__ */
|
|
2003
|
-
/* @__PURE__ */
|
|
2310
|
+
/* @__PURE__ */ jsx8(Text7, { dimColor: true, children: "\u2502 " }),
|
|
2311
|
+
/* @__PURE__ */ jsx8(Text7, { color: connColor, children: connectionLabel(connection, reconnectAt, reconnectAttempt) }),
|
|
2312
|
+
/* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " \u2502 " }),
|
|
2313
|
+
/* @__PURE__ */ jsx8(Text7, { color: tokenSeg.color, children: tokenSeg.text }),
|
|
2314
|
+
/* @__PURE__ */ jsxs7(Text7, { dimColor: true, children: [
|
|
2004
2315
|
" \u2502 seq ",
|
|
2005
2316
|
lastSeq ?? "\u2014"
|
|
2006
2317
|
] }),
|
|
2318
|
+
notifCount > 0 ? /* @__PURE__ */ jsxs7(Text7, { color: "yellow", children: [
|
|
2319
|
+
" \u2502 alerts:",
|
|
2320
|
+
notifCount
|
|
2321
|
+
] }) : null,
|
|
2007
2322
|
error ? (
|
|
2008
2323
|
// BL-222: an error replaces the full hint line but must never strand
|
|
2009
2324
|
// the user — keep the exit hint visible alongside it
|
|
2010
|
-
/* @__PURE__ */
|
|
2011
|
-
/* @__PURE__ */
|
|
2325
|
+
/* @__PURE__ */ jsxs7(Fragment, { children: [
|
|
2326
|
+
/* @__PURE__ */ jsxs7(Text7, { color: "red", children: [
|
|
2012
2327
|
" \u2502 ",
|
|
2013
2328
|
sanitizeForTty(error)
|
|
2014
2329
|
] }),
|
|
2015
|
-
/* @__PURE__ */
|
|
2330
|
+
/* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " \u2502 q quit" })
|
|
2016
2331
|
] })
|
|
2017
|
-
) : /* @__PURE__ */
|
|
2332
|
+
) : /* @__PURE__ */ jsx8(Text7, { dimColor: true, children: " \u2502 c channels \xB7 a alerts \xB7 n next \xB7 i compose \xB7 r reconnect \xB7 Esc normal \xB7 q quit" })
|
|
2018
2333
|
] });
|
|
2019
2334
|
}
|
|
2020
2335
|
|
|
2021
2336
|
// src/ui/TokenPrompt.tsx
|
|
2022
|
-
import { Box as
|
|
2023
|
-
import { jsx as
|
|
2337
|
+
import { Box as Box6, Text as Text8 } from "ink";
|
|
2338
|
+
import { jsx as jsx9, jsxs as jsxs8 } from "react/jsx-runtime";
|
|
2024
2339
|
function TokenPrompt({
|
|
2025
2340
|
value,
|
|
2026
2341
|
reason,
|
|
@@ -2028,26 +2343,26 @@ function TokenPrompt({
|
|
|
2028
2343
|
jwtTemplate
|
|
2029
2344
|
}) {
|
|
2030
2345
|
const masked = value.length === 0 ? "" : value.length <= 16 ? value : `${value.slice(0, 12)}\u2026 (${value.length} chars)`;
|
|
2031
|
-
return /* @__PURE__ */
|
|
2032
|
-
/* @__PURE__ */
|
|
2033
|
-
reason ? /* @__PURE__ */
|
|
2034
|
-
/* @__PURE__ */
|
|
2346
|
+
return /* @__PURE__ */ jsxs8(Box6, { flexDirection: "column", borderStyle: "round", borderColor: "yellow", paddingX: 1, children: [
|
|
2347
|
+
/* @__PURE__ */ jsx9(Text8, { bold: true, color: "yellow", children: "token needed" }),
|
|
2348
|
+
reason ? /* @__PURE__ */ jsx9(Text8, { dimColor: true, children: sanitizeForTty(reason) }) : null,
|
|
2349
|
+
/* @__PURE__ */ jsxs8(Text8, { dimColor: true, children: [
|
|
2035
2350
|
"mint one: open ",
|
|
2036
2351
|
webUrl,
|
|
2037
2352
|
" logged in, browser console:"
|
|
2038
2353
|
] }),
|
|
2039
|
-
/* @__PURE__ */
|
|
2040
|
-
/* @__PURE__ */
|
|
2041
|
-
/* @__PURE__ */
|
|
2042
|
-
/* @__PURE__ */
|
|
2043
|
-
/* @__PURE__ */
|
|
2354
|
+
/* @__PURE__ */ jsx9(Text8, { dimColor: true, children: ` await window.Clerk.session.getToken({template:'${jwtTemplate}'})` }),
|
|
2355
|
+
/* @__PURE__ */ jsxs8(Text8, { children: [
|
|
2356
|
+
/* @__PURE__ */ jsx9(Text8, { color: "yellow", children: "> " }),
|
|
2357
|
+
/* @__PURE__ */ jsx9(Text8, { children: masked }),
|
|
2358
|
+
/* @__PURE__ */ jsx9(Text8, { inverse: true, children: " " })
|
|
2044
2359
|
] }),
|
|
2045
|
-
/* @__PURE__ */
|
|
2360
|
+
/* @__PURE__ */ jsx9(Text8, { dimColor: true, children: "paste, then Enter \xB7 Ctrl+C quits" })
|
|
2046
2361
|
] });
|
|
2047
2362
|
}
|
|
2048
2363
|
|
|
2049
2364
|
// src/app.tsx
|
|
2050
|
-
import { jsx as
|
|
2365
|
+
import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
|
|
2051
2366
|
function cleanInput(input) {
|
|
2052
2367
|
return input.replace(/\r\n|[\r\n]/g, " ").replace(/[\x00-\x1f\x7f]/g, "");
|
|
2053
2368
|
}
|
|
@@ -2083,17 +2398,28 @@ function App({ store: store2 }) {
|
|
|
2083
2398
|
else if (input) store2.setDraft(s.draft + cleanInput(input));
|
|
2084
2399
|
return;
|
|
2085
2400
|
}
|
|
2401
|
+
if (s.mode === "notifications") {
|
|
2402
|
+
if (key.escape) store2.setMode("normal");
|
|
2403
|
+
else if (input === "j" || key.downArrow) store2.moveNotifSelection(1);
|
|
2404
|
+
else if (input === "k" || key.upArrow) store2.moveNotifSelection(-1);
|
|
2405
|
+
else if (input === "d" || key.return) {
|
|
2406
|
+
const n = s.notifications[s.notifSelected];
|
|
2407
|
+
if (n) store2.dismissNotification(n.id);
|
|
2408
|
+
} else if (input === "a") store2.markAllNotificationsRead();
|
|
2409
|
+
return;
|
|
2410
|
+
}
|
|
2086
2411
|
if (s.mode === "switcher") {
|
|
2087
2412
|
const filtered = filterChannels(s.channels, s.switcherFilter);
|
|
2413
|
+
const sorted = sortChannelsByPriority(filtered, { attention: s.attention, unread: s.unread });
|
|
2088
2414
|
const filterEmpty = s.switcherFilter.length === 0;
|
|
2089
2415
|
if (key.escape) store2.setMode("normal");
|
|
2090
2416
|
else if (key.return) {
|
|
2091
2417
|
if (filterEmpty) store2.setMode("normal");
|
|
2092
|
-
else if (
|
|
2418
|
+
else if (sorted[0]) store2.openChannel(sorted[0].id);
|
|
2093
2419
|
} else if (key.backspace || key.delete) {
|
|
2094
2420
|
store2.setSwitcherFilter(s.switcherFilter.slice(0, -1));
|
|
2095
2421
|
} else if (filterEmpty && /^[1-9]$/.test(input)) {
|
|
2096
|
-
const ch =
|
|
2422
|
+
const ch = sorted[Number(input) - 1];
|
|
2097
2423
|
if (ch) store2.openChannel(ch.id);
|
|
2098
2424
|
} else if (input) {
|
|
2099
2425
|
store2.setSwitcherFilter(s.switcherFilter + cleanInput(input));
|
|
@@ -2104,14 +2430,16 @@ function App({ store: store2 }) {
|
|
|
2104
2430
|
store2.quit();
|
|
2105
2431
|
exit();
|
|
2106
2432
|
} else if (input === "c") store2.setMode("switcher");
|
|
2433
|
+
else if (input === "a") store2.setMode("notifications");
|
|
2107
2434
|
else if (input === "i" || key.return) store2.setMode("insert");
|
|
2108
2435
|
else if (input === "r") store2.reconnectNow();
|
|
2436
|
+
else if (input === "n") store2.jumpNextActive();
|
|
2109
2437
|
},
|
|
2110
2438
|
{ isActive: interactive }
|
|
2111
2439
|
);
|
|
2112
2440
|
const activeChannel = state.channels.find((c) => c.id === state.activeChannelId);
|
|
2113
|
-
return /* @__PURE__ */
|
|
2114
|
-
/* @__PURE__ */
|
|
2441
|
+
return /* @__PURE__ */ jsxs9(Box7, { flexDirection: "column", children: [
|
|
2442
|
+
/* @__PURE__ */ jsx10(
|
|
2115
2443
|
Header,
|
|
2116
2444
|
{
|
|
2117
2445
|
channelName: activeChannel?.name ?? "(connecting\u2026)",
|
|
@@ -2120,16 +2448,25 @@ function App({ store: store2 }) {
|
|
|
2120
2448
|
presence: state.presence
|
|
2121
2449
|
}
|
|
2122
2450
|
),
|
|
2123
|
-
/* @__PURE__ */
|
|
2124
|
-
state.mode === "switcher" ? /* @__PURE__ */
|
|
2451
|
+
/* @__PURE__ */ jsx10(MessageList, { messages: state.messages }),
|
|
2452
|
+
state.mode === "switcher" ? /* @__PURE__ */ jsx10(
|
|
2125
2453
|
ChannelSwitcher,
|
|
2126
2454
|
{
|
|
2127
2455
|
channels: state.channels,
|
|
2128
2456
|
activeChannelId: state.activeChannelId,
|
|
2129
|
-
filter: state.switcherFilter
|
|
2457
|
+
filter: state.switcherFilter,
|
|
2458
|
+
attention: state.attention,
|
|
2459
|
+
unread: state.unread
|
|
2130
2460
|
}
|
|
2131
2461
|
) : null,
|
|
2132
|
-
state.mode === "
|
|
2462
|
+
state.mode === "notifications" ? /* @__PURE__ */ jsx10(
|
|
2463
|
+
NotificationsPanel,
|
|
2464
|
+
{
|
|
2465
|
+
notifications: state.notifications,
|
|
2466
|
+
notifSelected: state.notifSelected
|
|
2467
|
+
}
|
|
2468
|
+
) : null,
|
|
2469
|
+
state.mode === "token" ? /* @__PURE__ */ jsx10(
|
|
2133
2470
|
TokenPrompt,
|
|
2134
2471
|
{
|
|
2135
2472
|
value: state.tokenPrompt,
|
|
@@ -2138,9 +2475,9 @@ function App({ store: store2 }) {
|
|
|
2138
2475
|
jwtTemplate: store2.instance.clerkJwtTemplate
|
|
2139
2476
|
}
|
|
2140
2477
|
) : null,
|
|
2141
|
-
/* @__PURE__ */
|
|
2142
|
-
/* @__PURE__ */
|
|
2143
|
-
/* @__PURE__ */
|
|
2478
|
+
/* @__PURE__ */ jsx10(ActivityLine, { activities: state.activities }),
|
|
2479
|
+
/* @__PURE__ */ jsx10(Composer, { draft: state.draft, focused: state.mode === "insert" }),
|
|
2480
|
+
/* @__PURE__ */ jsx10(
|
|
2144
2481
|
StatusBar,
|
|
2145
2482
|
{
|
|
2146
2483
|
mode: state.mode,
|
|
@@ -2152,15 +2489,95 @@ function App({ store: store2 }) {
|
|
|
2152
2489
|
tokenObtainedAt: state.tokenObtainedAt,
|
|
2153
2490
|
tokenRefreshable: state.tokenRefreshable,
|
|
2154
2491
|
clerkRefresh: state.clerkRefresh,
|
|
2492
|
+
notifCount: state.notifications.filter((n) => !n.readAt).length,
|
|
2155
2493
|
error: state.error
|
|
2156
2494
|
}
|
|
2157
2495
|
),
|
|
2158
|
-
!interactive ? /* @__PURE__ */
|
|
2496
|
+
!interactive ? /* @__PURE__ */ jsx10(Text9, { dimColor: true, children: "(non-interactive terminal: keys disabled \u2014 tail is read-only here)" }) : null
|
|
2159
2497
|
] });
|
|
2160
2498
|
}
|
|
2161
2499
|
|
|
2500
|
+
// src/commands/profile.ts
|
|
2501
|
+
var USAGE = `usage: arp-tui profile <list|add|switch|remove> [...]
|
|
2502
|
+
|
|
2503
|
+
profile list [--json]
|
|
2504
|
+
show every saved instance profile (the active one is marked *)
|
|
2505
|
+
|
|
2506
|
+
profile add <name> --relay <url> [--web <url>] [--clerk-fapi <url>]
|
|
2507
|
+
[--clerk-template <name>] [--clerk-oauth-client-id <id>]
|
|
2508
|
+
define a new instance. --web and --clerk-fapi are required unless --relay
|
|
2509
|
+
is the built-in dev relay. Does not change the active profile.
|
|
2510
|
+
|
|
2511
|
+
profile switch <name>
|
|
2512
|
+
make <name> the active profile
|
|
2513
|
+
|
|
2514
|
+
profile remove <name>
|
|
2515
|
+
delete a profile (not the active one, not the last one)`;
|
|
2516
|
+
function requireName(name, verb) {
|
|
2517
|
+
if (!name) throw new Error(`profile ${verb} requires a profile name \u2014 see \`arp-tui profile\`.`);
|
|
2518
|
+
return name;
|
|
2519
|
+
}
|
|
2520
|
+
function runProfileCommand(args2) {
|
|
2521
|
+
try {
|
|
2522
|
+
switch (args2.sub) {
|
|
2523
|
+
case "list": {
|
|
2524
|
+
const { active, profiles } = listProfiles();
|
|
2525
|
+
if (args2.json) {
|
|
2526
|
+
console.log(JSON.stringify({ active, profiles }, null, 2));
|
|
2527
|
+
} else {
|
|
2528
|
+
const names = Object.keys(profiles);
|
|
2529
|
+
if (names.length === 0) {
|
|
2530
|
+
console.log("(no profiles)");
|
|
2531
|
+
} else {
|
|
2532
|
+
for (const name of names) {
|
|
2533
|
+
const mark = name === active ? "*" : " ";
|
|
2534
|
+
console.log(`${mark} ${name.padEnd(16)} ${profiles[name].relayUrl}`);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
process.exit(0);
|
|
2539
|
+
}
|
|
2540
|
+
case "add": {
|
|
2541
|
+
const name = requireName(args2.name, "add");
|
|
2542
|
+
if (!args2.relay) throw new Error("profile add requires --relay <url>.");
|
|
2543
|
+
const raw = { relayUrl: args2.relay };
|
|
2544
|
+
if (args2.web) raw["webUrl"] = args2.web;
|
|
2545
|
+
if (args2.clerkFapi) raw["clerkFapiUrl"] = args2.clerkFapi;
|
|
2546
|
+
if (args2.clerkTemplate) raw["clerkJwtTemplate"] = args2.clerkTemplate;
|
|
2547
|
+
if (args2.clerkOAuthClientId) raw["clerkOAuthClientId"] = args2.clerkOAuthClientId;
|
|
2548
|
+
const profile = addProfile(name, raw);
|
|
2549
|
+
console.error(`\u2713 added profile "${name}" (${profile.relayUrl})`);
|
|
2550
|
+
console.error(` run \`arp-tui profile switch ${name}\` to make it active`);
|
|
2551
|
+
process.exit(0);
|
|
2552
|
+
}
|
|
2553
|
+
case "switch": {
|
|
2554
|
+
const name = requireName(args2.name, "switch");
|
|
2555
|
+
const profile = switchProfile(name);
|
|
2556
|
+
console.error(`\u2713 active profile is now "${name}" (${profile.relayUrl})`);
|
|
2557
|
+
process.exit(0);
|
|
2558
|
+
}
|
|
2559
|
+
case "remove": {
|
|
2560
|
+
const name = requireName(args2.name, "remove");
|
|
2561
|
+
removeProfile(name);
|
|
2562
|
+
console.error(`\u2713 removed profile "${name}"`);
|
|
2563
|
+
console.error(" (stored credentials for its relay remain \u2014 `arp-tui logout` clears those)");
|
|
2564
|
+
process.exit(0);
|
|
2565
|
+
}
|
|
2566
|
+
default: {
|
|
2567
|
+
console.error(USAGE);
|
|
2568
|
+
if (args2.sub) console.error(`
|
|
2569
|
+
unknown profile command "${args2.sub}".`);
|
|
2570
|
+
process.exit(1);
|
|
2571
|
+
}
|
|
2572
|
+
}
|
|
2573
|
+
} catch (err) {
|
|
2574
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
2575
|
+
process.exit(1);
|
|
2576
|
+
}
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2162
2579
|
// src/index.tsx
|
|
2163
|
-
import { jsx as
|
|
2580
|
+
import { jsx as jsx11 } from "react/jsx-runtime";
|
|
2164
2581
|
function helpText(profileName2, base2, effective) {
|
|
2165
2582
|
const relayOverridden = effective.relayUrl !== base2.relayUrl;
|
|
2166
2583
|
const overrideNote = relayOverridden ? `
|
|
@@ -2169,7 +2586,7 @@ function helpText(profileName2, base2, effective) {
|
|
|
2169
2586
|
instance \u2014 a JWT minted there will NOT work against the override)` : "";
|
|
2170
2587
|
return `arp-tui \u2014 tail + post for Agent Relay Protocol channels (as the HUMAN)
|
|
2171
2588
|
|
|
2172
|
-
usage: arp-tui [tail|login|logout|status] [--profile <name>] [--relay <url>] [--token <jwt>]
|
|
2589
|
+
usage: arp-tui [tail|login|logout|status|profile] [--profile <name>] [--relay <url>] [--token <jwt>]
|
|
2173
2590
|
|
|
2174
2591
|
commands:
|
|
2175
2592
|
tail live channel tail + post (default)
|
|
@@ -2177,6 +2594,7 @@ commands:
|
|
|
2177
2594
|
stores refreshable tokens in ~/.arp-tui/credentials.json
|
|
2178
2595
|
logout revoke the OAuth grant and delete this instance's stored tokens
|
|
2179
2596
|
status show the stored credential state for the active instance
|
|
2597
|
+
profile manage saved instances (list|add|switch|remove; run \`arp-tui profile\`)
|
|
2180
2598
|
|
|
2181
2599
|
options:
|
|
2182
2600
|
--profile <name> instance profile from ~/.arp-tui/config.json
|
|
@@ -2206,6 +2624,11 @@ function parseCliArgs(argv) {
|
|
|
2206
2624
|
profile: { type: "string" },
|
|
2207
2625
|
relay: { type: "string" },
|
|
2208
2626
|
token: { type: "string" },
|
|
2627
|
+
web: { type: "string" },
|
|
2628
|
+
"clerk-fapi": { type: "string" },
|
|
2629
|
+
"clerk-template": { type: "string" },
|
|
2630
|
+
"clerk-oauth-client-id": { type: "string" },
|
|
2631
|
+
json: { type: "boolean" },
|
|
2209
2632
|
help: { type: "boolean", short: "h" }
|
|
2210
2633
|
},
|
|
2211
2634
|
allowPositionals: true
|
|
@@ -2216,9 +2639,16 @@ function parseCliArgs(argv) {
|
|
|
2216
2639
|
}
|
|
2217
2640
|
return {
|
|
2218
2641
|
cmd: parsed.positionals[0] ?? "tail",
|
|
2642
|
+
sub: parsed.positionals[1],
|
|
2643
|
+
name: parsed.positionals[2],
|
|
2219
2644
|
profile: parsed.values.profile,
|
|
2220
2645
|
relay: parsed.values.relay,
|
|
2221
2646
|
token: parsed.values.token,
|
|
2647
|
+
web: parsed.values.web,
|
|
2648
|
+
clerkFapi: parsed.values["clerk-fapi"],
|
|
2649
|
+
clerkTemplate: parsed.values["clerk-template"],
|
|
2650
|
+
clerkOAuthClientId: parsed.values["clerk-oauth-client-id"],
|
|
2651
|
+
json: parsed.values.json === true,
|
|
2222
2652
|
help: parsed.values.help === true
|
|
2223
2653
|
};
|
|
2224
2654
|
}
|
|
@@ -2246,6 +2676,18 @@ try {
|
|
|
2246
2676
|
console.error(err instanceof Error ? err.message : String(err));
|
|
2247
2677
|
process.exit(1);
|
|
2248
2678
|
}
|
|
2679
|
+
if (args.cmd === "profile") {
|
|
2680
|
+
runProfileCommand({
|
|
2681
|
+
sub: args.sub,
|
|
2682
|
+
name: args.name,
|
|
2683
|
+
relay: args.relay,
|
|
2684
|
+
web: args.web,
|
|
2685
|
+
clerkFapi: args.clerkFapi,
|
|
2686
|
+
clerkTemplate: args.clerkTemplate,
|
|
2687
|
+
clerkOAuthClientId: args.clerkOAuthClientId,
|
|
2688
|
+
json: args.json
|
|
2689
|
+
});
|
|
2690
|
+
}
|
|
2249
2691
|
var profileName;
|
|
2250
2692
|
var base;
|
|
2251
2693
|
var instance;
|
|
@@ -2330,4 +2772,4 @@ if (credentials?.tokenType === "dev-jwt" && credentials.token && !credentials.cl
|
|
|
2330
2772
|
);
|
|
2331
2773
|
}
|
|
2332
2774
|
var store = createStore({ instance, credentials });
|
|
2333
|
-
render(/* @__PURE__ */
|
|
2775
|
+
render(/* @__PURE__ */ jsx11(App, { store }));
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arp-tui",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "arp-tui",
|
|
9
|
-
"version": "0.0.
|
|
9
|
+
"version": "0.0.2",
|
|
10
10
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"ink": "^5.2.0",
|
package/package.json
CHANGED