@phnx-labs/agents-cli 1.20.64 → 1.20.66
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/CHANGELOG.md +51 -3
- package/README.md +156 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.d.ts +48 -0
- package/dist/commands/exec.js +159 -49
- package/dist/commands/feed.js +25 -11
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +748 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/plugins.js +28 -7
- package/dist/commands/routines.js +23 -2
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-browser.d.ts +82 -0
- package/dist/commands/sessions-browser.js +320 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +17 -0
- package/dist/commands/sessions.js +157 -10
- package/dist/commands/share.d.ts +2 -0
- package/dist/commands/share.js +150 -0
- package/dist/commands/ssh.js +54 -3
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +26 -0
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +46 -0
- package/dist/lib/agents.js +121 -3
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.js +5 -1
- package/dist/lib/crabbox/runtimes.js +11 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +62 -19
- package/dist/lib/devices/connect.d.ts +18 -1
- package/dist/lib/devices/connect.js +10 -2
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/known-hosts.d.ts +62 -0
- package/dist/lib/devices/known-hosts.js +137 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +66 -20
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +79 -4
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +205 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +45 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/picker.d.ts +53 -0
- package/dist/lib/picker.js +214 -1
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.d.ts +31 -1
- package/dist/lib/plugins.js +175 -15
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +19 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +102 -9
- package/dist/lib/secrets/agent.d.ts +83 -10
- package/dist/lib/secrets/agent.js +237 -70
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/filestore.d.ts +9 -0
- package/dist/lib/secrets/filestore.js +21 -8
- package/dist/lib/secrets/index.d.ts +7 -0
- package/dist/lib/secrets/index.js +26 -6
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +14 -0
- package/dist/lib/session/bundle.js +12 -1
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/share/capture.d.ts +29 -0
- package/dist/lib/share/capture.js +140 -0
- package/dist/lib/share/config.d.ts +35 -0
- package/dist/lib/share/config.js +100 -0
- package/dist/lib/share/og.d.ts +25 -0
- package/dist/lib/share/og.js +84 -0
- package/dist/lib/share/provision.d.ts +10 -0
- package/dist/lib/share/provision.js +91 -0
- package/dist/lib/share/publish.d.ts +57 -0
- package/dist/lib/share/publish.js +145 -0
- package/dist/lib/share/worker-template.d.ts +2 -0
- package/dist/lib/share/worker-template.js +82 -0
- package/dist/lib/shims.d.ts +13 -0
- package/dist/lib/shims.js +42 -2
- package/dist/lib/ssh-exec.d.ts +24 -0
- package/dist/lib/ssh-exec.js +15 -3
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +86 -7
- package/dist/lib/startup/command-registry.d.ts +3 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +36 -1
- package/dist/lib/usage.js +7 -5
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -16,7 +16,7 @@ import { truncate, padRight } from '../lib/format.js';
|
|
|
16
16
|
import ora from 'ora';
|
|
17
17
|
import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
18
18
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
19
|
-
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable } from '../lib/platform/index.js';
|
|
19
|
+
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, findExecutable, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
20
20
|
import { getActiveSessions } from '../lib/session/active.js';
|
|
21
21
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
22
22
|
import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
|
|
@@ -25,6 +25,7 @@ import { machineId, normalizeHost } from '../lib/session/sync/config.js';
|
|
|
25
25
|
import { gatherRemoteActive, NO_FANOUT_ENV } from '../lib/session/remote-active.js';
|
|
26
26
|
import { gatherRemoteList, runOnPeer } from '../lib/session/remote-list.js';
|
|
27
27
|
import { stringWidth, truncateToWidth, padToWidth, terminalWidth } from '../lib/session/width.js';
|
|
28
|
+
import { inferSessionState } from '../lib/session/state.js';
|
|
28
29
|
import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex, getSessionRoots } from '../lib/session/discover.js';
|
|
29
30
|
import { filterTeamSessions } from '../lib/session/team-filter.js';
|
|
30
31
|
import { parseSession } from '../lib/session/parse.js';
|
|
@@ -36,7 +37,7 @@ import { colorAgent, resolveAgentName } from '../lib/agents.js';
|
|
|
36
37
|
import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
|
|
37
38
|
import { resolveVersionAliasLoose } from '../lib/versions.js';
|
|
38
39
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
39
|
-
import { sessionPicker } from './sessions-picker.js';
|
|
40
|
+
import { sessionPicker, buildPreview } from './sessions-picker.js';
|
|
40
41
|
import { setHelpSections } from '../lib/help.js';
|
|
41
42
|
import { registerSessionsTailCommand } from './sessions-tail.js';
|
|
42
43
|
import { registerSessionsSyncCommand } from './sessions-sync.js';
|
|
@@ -789,6 +790,57 @@ async function renderActiveSessions(asJson, waitingOnly = false, opts = {}) {
|
|
|
789
790
|
function printCrossMachineTip() {
|
|
790
791
|
console.log(chalk.gray("\nTip: include sessions from your other machines — register them with 'ag devices sync', then rerun. Use --local to skip."));
|
|
791
792
|
}
|
|
793
|
+
/**
|
|
794
|
+
* True when the interactive session browser should open instead of a printed
|
|
795
|
+
* listing: a real TTY, no `--json`, and `--no-interactive` not set. The bare
|
|
796
|
+
* listing and `--active` both default to it; scripts/pipes/agents fall through
|
|
797
|
+
* to the existing printed/JSON paths.
|
|
798
|
+
*/
|
|
799
|
+
function useInteractiveBrowser(options) {
|
|
800
|
+
return options.interactive !== false && !options.json && isInteractiveTerminal();
|
|
801
|
+
}
|
|
802
|
+
/** The canonical `ag sessions …` command for a set of flags — the twin of the
|
|
803
|
+
* browser's `y` hotkey (see --print-cmd). Normalizes to the stable flag form. */
|
|
804
|
+
function canonicalSessionsCommand(query, options) {
|
|
805
|
+
const a = ['sessions'];
|
|
806
|
+
if (options.active)
|
|
807
|
+
a.push('--active');
|
|
808
|
+
if (options.teams)
|
|
809
|
+
a.push('--teams');
|
|
810
|
+
if (options.agent)
|
|
811
|
+
a.push('-a', options.agent);
|
|
812
|
+
for (const h of options.host ?? [])
|
|
813
|
+
a.push('--device', h);
|
|
814
|
+
if (options.project)
|
|
815
|
+
a.push('--project', options.project);
|
|
816
|
+
if (options.all)
|
|
817
|
+
a.push('--all');
|
|
818
|
+
if (options.since)
|
|
819
|
+
a.push('--since', options.since);
|
|
820
|
+
if (options.until)
|
|
821
|
+
a.push('--until', options.until);
|
|
822
|
+
if (options.local)
|
|
823
|
+
a.push('--local');
|
|
824
|
+
if (options.waiting)
|
|
825
|
+
a.push('--waiting');
|
|
826
|
+
const q = (query ?? '').trim();
|
|
827
|
+
if (q)
|
|
828
|
+
a.push(JSON.stringify(q));
|
|
829
|
+
return 'ag ' + a.join(' ');
|
|
830
|
+
}
|
|
831
|
+
/** Resolve a session by id/query globally and print its compact preview (no pager).
|
|
832
|
+
* Backs `--preview` — the fast path for the "peek before resume" hot loop. */
|
|
833
|
+
async function renderSessionPreview(query, scope) {
|
|
834
|
+
const discovered = await discoverSessions({ all: true, cwd: process.cwd(), limit: 5000 });
|
|
835
|
+
const pool = applyScopeFilters(discovered, scope);
|
|
836
|
+
const matches = resolveSessionById(pool, query);
|
|
837
|
+
const session = (matches.length > 0 ? matches : filterSessionsByQuery(pool, query))[0];
|
|
838
|
+
if (!session) {
|
|
839
|
+
console.log(chalk.gray(`No session matches "${query}".`));
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
console.log(buildPreview(session));
|
|
843
|
+
}
|
|
792
844
|
/** Main action handler for `agents sessions`. Routes to picker, table, or single-session render. */
|
|
793
845
|
async function sessionsAction(query, options) {
|
|
794
846
|
// Explicit --query is interchangeable with the positional; it's how you search
|
|
@@ -801,6 +853,13 @@ async function sessionsAction(query, options) {
|
|
|
801
853
|
if (options.device && options.device.length > 0) {
|
|
802
854
|
options.host = [...(options.host ?? []), ...options.device];
|
|
803
855
|
}
|
|
856
|
+
// --print-cmd: echo the canonical `ag sessions …` for the given flags and exit.
|
|
857
|
+
// The non-interactive twin of the browser's `y` hotkey — lets an agent compose
|
|
858
|
+
// (or a human copy) the exact command a view maps to.
|
|
859
|
+
if (options.printCmd) {
|
|
860
|
+
process.stdout.write(canonicalSessionsCommand(query, options) + '\n');
|
|
861
|
+
return;
|
|
862
|
+
}
|
|
804
863
|
// --roots: emit the local session-scan directories, per agent, as JSON. A pure
|
|
805
864
|
// machine-readable query (no listing/render) — external watchers (the Factory
|
|
806
865
|
// extension's fs.watch) read it to track the same dirs the CLI scans, instead
|
|
@@ -829,7 +888,39 @@ async function sessionsAction(query, options) {
|
|
|
829
888
|
}
|
|
830
889
|
return;
|
|
831
890
|
}
|
|
891
|
+
// --preview <id/query>: resolve one session and print its compact preview, then
|
|
892
|
+
// exit — checked before --active so `--active --preview <id>` peeks the id
|
|
893
|
+
// rather than being swallowed by the active listing.
|
|
894
|
+
if (options.preview) {
|
|
895
|
+
if (!query) {
|
|
896
|
+
console.error(chalk.red('--preview requires a session id or query.'));
|
|
897
|
+
process.exit(1);
|
|
898
|
+
}
|
|
899
|
+
await renderSessionPreview(query, { agent: options.agent, project: options.project });
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
832
902
|
if (options.active) {
|
|
903
|
+
// On a TTY (and not a scripting path), open the interactive browser seeded to
|
|
904
|
+
// running-only. --json / --waiting / --no-interactive / a peer fan-out keep the
|
|
905
|
+
// static dump untouched, so scripts and agents are unaffected. An explicit
|
|
906
|
+
// --since seeds the window; --until / --project (no browser field) or a
|
|
907
|
+
// multi-host scope fall through to the static dump that already honors them.
|
|
908
|
+
if (useInteractiveBrowser(options) &&
|
|
909
|
+
!options.waiting &&
|
|
910
|
+
!options.until &&
|
|
911
|
+
!options.project &&
|
|
912
|
+
!options.sort &&
|
|
913
|
+
(options.host?.length ?? 0) <= 1 &&
|
|
914
|
+
process.env.AGENTS_SESSIONS_LOCAL !== '1') {
|
|
915
|
+
const { runSessionBrowser, activeBrowserSeed } = await import('./sessions-browser.js');
|
|
916
|
+
await runSessionBrowser(activeBrowserSeed({
|
|
917
|
+
teams: options.teams,
|
|
918
|
+
agent: options.agent,
|
|
919
|
+
host: options.host,
|
|
920
|
+
since: options.since,
|
|
921
|
+
}), { local: options.local === true, hosts: options.host });
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
833
924
|
// AGENTS_SESSIONS_LOCAL is set by a parent fan-out invocation (see
|
|
834
925
|
// remote-active.ts) so a peer answers for itself without recursing.
|
|
835
926
|
const forceLocal = options.local === true || process.env.AGENTS_SESSIONS_LOCAL === '1';
|
|
@@ -843,6 +934,30 @@ async function sessionsAction(query, options) {
|
|
|
843
934
|
await runCloudSessions(query, options);
|
|
844
935
|
return;
|
|
845
936
|
}
|
|
937
|
+
// Bare interactive listing → the interactive fleet browser (humans). A query,
|
|
938
|
+
// a render/filter flag, --flat/--tree, --json, --until, --project (a named-project
|
|
939
|
+
// filter the browser can't represent), or --no-interactive keep the existing
|
|
940
|
+
// printed/render paths (agents and scripts unaffected). An explicit --since seeds
|
|
941
|
+
// the browser's window so the flag is honored, not swallowed.
|
|
942
|
+
if (useInteractiveBrowser(options) &&
|
|
943
|
+
!query &&
|
|
944
|
+
!options.flat &&
|
|
945
|
+
!options.tree &&
|
|
946
|
+
!options.markdown &&
|
|
947
|
+
!options.until &&
|
|
948
|
+
!options.project &&
|
|
949
|
+
!options.sort &&
|
|
950
|
+
!options.artifacts &&
|
|
951
|
+
options.artifact === undefined) {
|
|
952
|
+
const { runSessionBrowser, bareBrowserSeed } = await import('./sessions-browser.js');
|
|
953
|
+
await runSessionBrowser(bareBrowserSeed({
|
|
954
|
+
teams: options.teams,
|
|
955
|
+
agent: options.agent,
|
|
956
|
+
all: options.all,
|
|
957
|
+
since: options.since,
|
|
958
|
+
}), { local: options.local === true, hosts: options.host });
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
846
961
|
let filterOpts;
|
|
847
962
|
try {
|
|
848
963
|
filterOpts = buildFilterOptions(options);
|
|
@@ -1323,9 +1438,9 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
1323
1438
|
return;
|
|
1324
1439
|
}
|
|
1325
1440
|
const spinner = ora(`Parsing ${session.agent} session...`).start();
|
|
1326
|
-
|
|
1441
|
+
const parsedEvents = parseSession(session.filePath, session.agent);
|
|
1327
1442
|
spinner.stop();
|
|
1328
|
-
events = filterEvents(
|
|
1443
|
+
let events = filterEvents(parsedEvents, filters);
|
|
1329
1444
|
const agentColor = colorAgent(session.agent);
|
|
1330
1445
|
console.log('');
|
|
1331
1446
|
if (mode === 'summary') {
|
|
@@ -1367,7 +1482,11 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
1367
1482
|
// engine (plan text, PR, worktree, ticket). Pre-1.20.51 emitted a bare event
|
|
1368
1483
|
// array; consumers that JSON.parse this now read `output.events` for the
|
|
1369
1484
|
// array. See issue #743 (plan surfaced) and CHANGELOG for the shape change.
|
|
1370
|
-
|
|
1485
|
+
// `todos` (RUSH-1503) is computed from the UNFILTERED transcript so the
|
|
1486
|
+
// checklist reflects true session state regardless of any `--include` filter;
|
|
1487
|
+
// it lets the Factory panel read the CLI's checklist instead of re-parsing.
|
|
1488
|
+
const todos = inferSessionState(parsedEvents, { cwd: session.cwd }).todos;
|
|
1489
|
+
process.stdout.write(renderJson(events, todos ? { ...session, todos } : session));
|
|
1371
1490
|
}
|
|
1372
1491
|
function renderTopicCell(label, topic, query, visibleWidth, paddedWidth) {
|
|
1373
1492
|
const lbl = (label ?? '').trim();
|
|
@@ -1554,7 +1673,7 @@ function warnNoPeerTarget(machine, session) {
|
|
|
1554
1673
|
console.log(chalk.yellow(`Session ${session.shortId} lives on ${machine}, which isn't a reachable device right now.`));
|
|
1555
1674
|
console.log(chalk.gray(`Register/wake it (ag devices), or run there: agents ssh ${machine}`));
|
|
1556
1675
|
}
|
|
1557
|
-
async function handlePickedSession(picked) {
|
|
1676
|
+
export async function handlePickedSession(picked) {
|
|
1558
1677
|
// A session on another machine is read/resumed ON that machine over SSH — its
|
|
1559
1678
|
// transcript and agent binary live there. Both actions execute on the peer
|
|
1560
1679
|
// (not a local `--host` hop, which would discover locally and dead-end for a
|
|
@@ -1613,11 +1732,35 @@ export async function resumeSessionInPlace(session) {
|
|
|
1613
1732
|
}
|
|
1614
1733
|
await spawnResumeCommand(resume, cwd);
|
|
1615
1734
|
}
|
|
1735
|
+
/**
|
|
1736
|
+
* Map a resume argv to the spawn(command, args, {shell}) triple.
|
|
1737
|
+
*
|
|
1738
|
+
* On Windows the agent launcher is a `.cmd`/PATHEXT shim and needs shell:true.
|
|
1739
|
+
* With shell:true, Node concatenates args into the cmd.exe line unescaped
|
|
1740
|
+
* (DEP0190 + injection). A session.id derived from a filename can carry
|
|
1741
|
+
* metacharacters (`&|<>`); compose a fully-quoted line and pass an EMPTY args
|
|
1742
|
+
* array so cmd.exe cannot reparse them (RUSH-1753). See composeWin32CommandLine.
|
|
1743
|
+
*
|
|
1744
|
+
* `platform` is injectable so the win32 shell path is unit-testable on any host.
|
|
1745
|
+
*/
|
|
1746
|
+
export function resumeSpawnInvocation(cmd, platform = process.platform) {
|
|
1747
|
+
const shell = needsWindowsShell(cmd[0], platform);
|
|
1748
|
+
if (shell) {
|
|
1749
|
+
return {
|
|
1750
|
+
command: composeWin32CommandLine(cmd[0], cmd.slice(1)),
|
|
1751
|
+
args: [],
|
|
1752
|
+
shell: true,
|
|
1753
|
+
};
|
|
1754
|
+
}
|
|
1755
|
+
return { command: cmd[0], args: cmd.slice(1), shell: false };
|
|
1756
|
+
}
|
|
1616
1757
|
/**
|
|
1617
1758
|
* Spawn a resume command as a foreground takeover (inherited stdio), resolving
|
|
1618
1759
|
* when it exits. On Windows the agent launcher is a `.cmd`/PATHEXT shim that
|
|
1619
1760
|
* `spawn` can't exec directly — a bare-name `shell:false` spawn throws
|
|
1620
1761
|
* `EFTYPE`/`ENOENT` there — so we go through the shell via `needsWindowsShell`.
|
|
1762
|
+
* When shell:true, argv is composed via resumeSpawnInvocation (quoted line +
|
|
1763
|
+
* empty args) so an untrusted session.id cannot inject through cmd.exe.
|
|
1621
1764
|
* The spawn is guarded because such a failure can be thrown synchronously;
|
|
1622
1765
|
* without the guard it would surface under an unrelated "Failed to discover
|
|
1623
1766
|
* sessions" catch upstream instead of a truthful launch error.
|
|
@@ -1626,10 +1769,11 @@ function spawnResumeCommand(cmd, cwd) {
|
|
|
1626
1769
|
return new Promise((resolve) => {
|
|
1627
1770
|
let child;
|
|
1628
1771
|
try {
|
|
1629
|
-
|
|
1772
|
+
const { command, args, shell } = resumeSpawnInvocation(cmd);
|
|
1773
|
+
child = spawn(command, args, {
|
|
1630
1774
|
cwd,
|
|
1631
1775
|
stdio: 'inherit',
|
|
1632
|
-
shell
|
|
1776
|
+
shell,
|
|
1633
1777
|
});
|
|
1634
1778
|
}
|
|
1635
1779
|
catch (err) {
|
|
@@ -2045,7 +2189,7 @@ export function registerSessionsCommands(program) {
|
|
|
2045
2189
|
.option('--opencode', 'Shorthand for --agent opencode')
|
|
2046
2190
|
.option('--all', 'Include sessions from every directory (not just current project)')
|
|
2047
2191
|
.option('--teams', 'Include team-spawned sessions (hidden by default)')
|
|
2048
|
-
.option('--project <name>', 'Filter by project name (searches across all directories)')
|
|
2192
|
+
.option('-p, --project <name>', 'Filter by project name (searches across all directories)')
|
|
2049
2193
|
.option('--since <time>', 'Only sessions newer than this (e.g., 2h, 7d, 4w, or ISO date)')
|
|
2050
2194
|
.option('--until <time>', 'Only sessions older than this (ISO timestamp)')
|
|
2051
2195
|
.option('-n, --limit <n>', 'Maximum number of sessions to return', '50')
|
|
@@ -2069,7 +2213,10 @@ export function registerSessionsCommands(program) {
|
|
|
2069
2213
|
.option('--cloud', 'Source sessions from Rush Cloud (captured runs) instead of local disk')
|
|
2070
2214
|
.option('-H, --host <target...>', 'Run this query on remote machine(s) over SSH (host alias or user@host; repeatable)')
|
|
2071
2215
|
.option('--device <target...>', 'Alias for --host (device alias from `agents devices`; repeatable)')
|
|
2072
|
-
.option('--browser', 'List browser-profile captures (screenshots, PDFs, recordings, downloads) instead of agent transcripts — alias of `agents browser sessions`')
|
|
2216
|
+
.option('--browser', 'List browser-profile captures (screenshots, PDFs, recordings, downloads) instead of agent transcripts — alias of `agents browser sessions`')
|
|
2217
|
+
.option('--no-interactive', 'Print the listing instead of opening the interactive browser (default on a TTY for the bare listing and --active)')
|
|
2218
|
+
.option('--print-cmd', 'Print the canonical `ag sessions …` command for the given flags and exit (the twin of the browser’s `y` hotkey)')
|
|
2219
|
+
.option('--preview', 'With a session id/query: print a compact preview and exit (no pager)');
|
|
2073
2220
|
setHelpSections(sessionsCmd, {
|
|
2074
2221
|
examples: `
|
|
2075
2222
|
# Search prior sessions in this project by topic, file path, or command
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// `agents share` — publish an HTML file to your own Cloudflare R2 behind a tiny
|
|
2
|
+
// Worker, and get a shareable link (~$0). See apps/cli/docs/share.md.
|
|
3
|
+
import { existsSync } from 'node:fs';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { DEFAULT_BUCKET_NAME, DEFAULT_CF_BUNDLE, DEFAULT_WORKER_NAME, generateWriteToken, readCloudflareCreds, readShareConfig, storeWriteToken, writeShareConfig, } from '../lib/share/config.js';
|
|
6
|
+
import { addCustomDomain, createBucket, deployWorker, enableWorkersDev, findZoneId, } from '../lib/share/provision.js';
|
|
7
|
+
import { publishFile } from '../lib/share/publish.js';
|
|
8
|
+
import { renderWorkerScript } from '../lib/share/worker-template.js';
|
|
9
|
+
export function registerShareCommands(program) {
|
|
10
|
+
const shareCmd = program
|
|
11
|
+
.command('share')
|
|
12
|
+
.description('Publish an HTML file to your own Cloudflare R2 and get a shareable link (~$0).')
|
|
13
|
+
.argument('[file]', 'file to publish (HTML or any static asset)')
|
|
14
|
+
.option('--slug <slug>', 'custom URL slug (default: <project>-<feature>-<hash>)')
|
|
15
|
+
.option('--expire <spec>', 'auto-expire, e.g. 30d, 12h, or 2026-08-01')
|
|
16
|
+
.option('--no-cover', 'skip the OG preview image (HTML pages get one by default)')
|
|
17
|
+
.action(async (file, opts) => {
|
|
18
|
+
if (!file) {
|
|
19
|
+
shareCmd.help();
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (!existsSync(file)) {
|
|
23
|
+
console.error(chalk.red(`No such file: ${file}`));
|
|
24
|
+
process.exitCode = 1;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const { url, expiresAt, coverUrl } = await publishFile(file, {
|
|
29
|
+
slug: opts.slug,
|
|
30
|
+
expire: opts.expire,
|
|
31
|
+
cover: opts.cover,
|
|
32
|
+
});
|
|
33
|
+
console.log(chalk.green(url));
|
|
34
|
+
if (coverUrl)
|
|
35
|
+
console.log(chalk.dim(` cover ${coverUrl}`));
|
|
36
|
+
if (expiresAt)
|
|
37
|
+
console.log(chalk.dim(` expires ${new Date(expiresAt).toLocaleString()}`));
|
|
38
|
+
}
|
|
39
|
+
catch (e) {
|
|
40
|
+
console.error(chalk.red(e.message));
|
|
41
|
+
process.exitCode = 1;
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
shareCmd
|
|
45
|
+
.command('setup')
|
|
46
|
+
.description('One-time: provision an R2 bucket + Worker on your Cloudflare and save the config.')
|
|
47
|
+
.option('--bundle <name>', 'secrets bundle holding the Cloudflare API token', DEFAULT_CF_BUNDLE)
|
|
48
|
+
.option('--worker <name>', 'Worker name', DEFAULT_WORKER_NAME)
|
|
49
|
+
.option('--bucket <name>', 'R2 bucket name', DEFAULT_BUCKET_NAME)
|
|
50
|
+
.option('--account <id>', 'Cloudflare account id (else read from the bundle / prompt)')
|
|
51
|
+
.option('--token <t>', 'Cloudflare API token (else read from the --bundle)')
|
|
52
|
+
.option('--domain <host>', 'also map a custom domain (e.g. share.agents-cli.sh) if the token owns the zone')
|
|
53
|
+
.action(async (opts) => {
|
|
54
|
+
try {
|
|
55
|
+
await runSetup(opts);
|
|
56
|
+
}
|
|
57
|
+
catch (e) {
|
|
58
|
+
console.error(chalk.red(e.message));
|
|
59
|
+
process.exitCode = 1;
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
shareCmd
|
|
63
|
+
.command('join')
|
|
64
|
+
.description('Use an existing share endpoint (no provisioning) — pass the base URL and paste the write token.')
|
|
65
|
+
.argument('<baseUrl>', 'base URL of the endpoint, e.g. https://share.agents-cli.sh')
|
|
66
|
+
.action(async (baseUrl) => {
|
|
67
|
+
try {
|
|
68
|
+
await runJoin(baseUrl);
|
|
69
|
+
}
|
|
70
|
+
catch (e) {
|
|
71
|
+
console.error(chalk.red(e.message));
|
|
72
|
+
process.exitCode = 1;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
shareCmd
|
|
76
|
+
.command('status')
|
|
77
|
+
.description('Show the configured share endpoint.')
|
|
78
|
+
.action(() => {
|
|
79
|
+
const cfg = readShareConfig();
|
|
80
|
+
if (!cfg) {
|
|
81
|
+
console.log(chalk.dim("Not configured. Run 'agents share setup' or 'agents share join'."));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
console.log(`${chalk.bold('endpoint')} ${chalk.green(cfg.baseUrl)}`);
|
|
85
|
+
console.log(chalk.dim(`worker ${cfg.workerName} · bucket ${cfg.bucketName} · account ${cfg.accountId}`));
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
async function runSetup(opts) {
|
|
89
|
+
const { default: ora } = await import('ora');
|
|
90
|
+
const { input } = await import('@inquirer/prompts');
|
|
91
|
+
const { apiToken, accountId: acctFromBundle } = readCloudflareCreds(opts.bundle, {
|
|
92
|
+
apiToken: opts.token,
|
|
93
|
+
accountId: opts.account,
|
|
94
|
+
});
|
|
95
|
+
const accountId = opts.account || acctFromBundle || (await input({ message: 'Cloudflare account id' }));
|
|
96
|
+
if (!accountId)
|
|
97
|
+
throw new Error('A Cloudflare account id is required.');
|
|
98
|
+
const workerName = opts.worker;
|
|
99
|
+
const bucketName = opts.bucket;
|
|
100
|
+
const token = generateWriteToken();
|
|
101
|
+
const spin = ora('Provisioning on Cloudflare…').start();
|
|
102
|
+
try {
|
|
103
|
+
await createBucket(apiToken, accountId, bucketName);
|
|
104
|
+
spin.text = `R2 bucket '${bucketName}' ready`;
|
|
105
|
+
await deployWorker(apiToken, accountId, workerName, renderWorkerScript(), bucketName, token);
|
|
106
|
+
spin.text = `Worker '${workerName}' deployed`;
|
|
107
|
+
const subdomain = await enableWorkersDev(apiToken, accountId, workerName);
|
|
108
|
+
let baseUrl = `https://${workerName}.${subdomain}.workers.dev`;
|
|
109
|
+
let domain;
|
|
110
|
+
if (opts.domain) {
|
|
111
|
+
spin.text = `Mapping ${opts.domain}…`;
|
|
112
|
+
const zoneId = await findZoneId(apiToken, opts.domain);
|
|
113
|
+
if (zoneId) {
|
|
114
|
+
await addCustomDomain(apiToken, accountId, workerName, zoneId, opts.domain);
|
|
115
|
+
baseUrl = `https://${opts.domain}`;
|
|
116
|
+
domain = opts.domain;
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
spin.warn(`Zone for ${opts.domain} not visible to this token — staying on workers.dev`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
spin.succeed('Provisioned');
|
|
123
|
+
const cfg = { baseUrl, accountId, workerName, bucketName, domain };
|
|
124
|
+
writeShareConfig(cfg);
|
|
125
|
+
storeWriteToken(token);
|
|
126
|
+
console.log(chalk.green(`\nShare endpoint ready → ${chalk.bold(baseUrl)}`));
|
|
127
|
+
console.log(chalk.dim('Publish with: ') + chalk.cyan('agents share <file>'));
|
|
128
|
+
console.log(chalk.dim(`Fleet: push the token with 'agents secrets export share --host <box>' and pull config with 'agents repo pull'.`));
|
|
129
|
+
}
|
|
130
|
+
catch (e) {
|
|
131
|
+
spin.fail('Provisioning failed');
|
|
132
|
+
throw e;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
async function runJoin(baseUrl) {
|
|
136
|
+
const { password, input } = await import('@inquirer/prompts');
|
|
137
|
+
const clean = baseUrl.replace(/\/+$/, '');
|
|
138
|
+
const workerName = await input({ message: 'Worker name', default: DEFAULT_WORKER_NAME });
|
|
139
|
+
const bucketName = await input({ message: 'Bucket name', default: DEFAULT_BUCKET_NAME });
|
|
140
|
+
const accountId = await input({ message: 'Cloudflare account id' });
|
|
141
|
+
const token = await password({ message: 'Write token (from the endpoint owner)', mask: true });
|
|
142
|
+
if (!token)
|
|
143
|
+
throw new Error('A write token is required to join.');
|
|
144
|
+
const domain = clean.startsWith('https://') && !clean.includes('.workers.dev')
|
|
145
|
+
? clean.replace(/^https:\/\//, '')
|
|
146
|
+
: undefined;
|
|
147
|
+
writeShareConfig({ baseUrl: clean, accountId, workerName, bucketName, domain });
|
|
148
|
+
storeWriteToken(token);
|
|
149
|
+
console.log(chalk.green(`Joined ${chalk.bold(clean)} — publish with `) + chalk.cyan('agents share <file>'));
|
|
150
|
+
}
|
package/dist/commands/ssh.js
CHANGED
|
@@ -15,9 +15,11 @@ import * as os from 'os';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import chalk from 'chalk';
|
|
17
17
|
import ora from 'ora';
|
|
18
|
-
import { readAndResolveBundleEnv } from '../lib/secrets/bundles.js';
|
|
18
|
+
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
|
|
19
19
|
import { machineId } from '../lib/session/sync/config.js';
|
|
20
20
|
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, upsertDevice, } from '../lib/devices/registry.js';
|
|
21
|
+
import { addControlToken } from '../lib/serve/token.js';
|
|
22
|
+
import { DEFAULT_SERVE_PORT } from '../lib/serve/server.js';
|
|
21
23
|
import { nodeToDeviceInput, parseTailscaleStatus, tailscaleStatusJson, } from '../lib/devices/tailscale.js';
|
|
22
24
|
import { localLoginUser, planDeviceReconciliation, runDeviceSync, withDefaultUser } from '../lib/devices/sync.js';
|
|
23
25
|
import { resolveDeviceTarget, splitUserHost } from '../lib/devices/resolve-target.js';
|
|
@@ -25,6 +27,7 @@ import { clearPendingSentinel } from '../lib/devices/pending.js';
|
|
|
25
27
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
26
28
|
import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
27
29
|
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
30
|
+
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
28
31
|
import { planFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
29
32
|
import { fleetCapacity, fmtBytes, headroom, probeFleetStats, } from '../lib/devices/health.js';
|
|
30
33
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
@@ -400,6 +403,47 @@ Typical workflow:
|
|
|
400
403
|
process.exit(1);
|
|
401
404
|
}
|
|
402
405
|
});
|
|
406
|
+
devicesCmd
|
|
407
|
+
.command('pair-ios [name]')
|
|
408
|
+
.description('Pair an iPhone/iPad cockpit (RUSH-1733): mint a control token for `agents serve --control` and mark the device control-only. The token is shown ONCE — enter it in the app. Run this on the anchor.')
|
|
409
|
+
.option('--port <n>', 'Anchor control port to advertise to the app', String(DEFAULT_SERVE_PORT))
|
|
410
|
+
.action(async (name, opts) => {
|
|
411
|
+
const label = (name || 'iphone').trim();
|
|
412
|
+
// Mint the bearer token — hash-only on disk, raw shown once (see serve/token.ts).
|
|
413
|
+
const { id, token } = addControlToken(label);
|
|
414
|
+
// If a device with this name is already registered (e.g. discovered over
|
|
415
|
+
// Tailscale as an `unknown`-platform node), mark it control-only so the
|
|
416
|
+
// fleet stops trying to dial it for sessions/placement.
|
|
417
|
+
let marked = false;
|
|
418
|
+
let unknownName = false;
|
|
419
|
+
if (name) {
|
|
420
|
+
const existing = await getDevice(name);
|
|
421
|
+
if (existing) {
|
|
422
|
+
await upsertDevice(name, { role: 'control' });
|
|
423
|
+
marked = true;
|
|
424
|
+
}
|
|
425
|
+
else {
|
|
426
|
+
unknownName = true;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const port = parseInt(opts.port ?? '', 10) || DEFAULT_SERVE_PORT;
|
|
430
|
+
console.log(chalk.green(`Paired cockpit '${label}'`) + chalk.gray(` (token id ${id})`));
|
|
431
|
+
if (marked)
|
|
432
|
+
console.log(chalk.gray(`Marked device '${name}' role=control — the fleet won't dial it.`));
|
|
433
|
+
if (unknownName) {
|
|
434
|
+
console.log(chalk.yellow(`Note: no registered device named '${name}' — token minted, but no device was marked role=control.`) +
|
|
435
|
+
chalk.gray(` Run \`agents devices sync\` first if this phone should appear in the fleet.`));
|
|
436
|
+
}
|
|
437
|
+
console.log();
|
|
438
|
+
console.log(chalk.bold('Control token (shown once — enter it in the app):'));
|
|
439
|
+
console.log(' ' + chalk.cyan(token));
|
|
440
|
+
console.log();
|
|
441
|
+
console.log('On the anchor, expose the control server on your tailnet:');
|
|
442
|
+
console.log(chalk.gray(` agents serve --control --bind <anchor-tailnet-ip> --port ${port}`));
|
|
443
|
+
console.log('Then point the app at:');
|
|
444
|
+
console.log(chalk.gray(` http://<anchor-tailnet-ip>:${port} (Bearer: the token above)`));
|
|
445
|
+
console.log(chalk.yellow('Keep the control server on the tailnet — never public Funnel.'));
|
|
446
|
+
});
|
|
403
447
|
devicesCmd
|
|
404
448
|
.command('rm <name>')
|
|
405
449
|
.alias('remove')
|
|
@@ -515,7 +559,14 @@ secrets bundle via an askpass shim — the password never touches argv.
|
|
|
515
559
|
}
|
|
516
560
|
try {
|
|
517
561
|
const shim = writeAskpassShim();
|
|
518
|
-
|
|
562
|
+
// Pin the host key on first connect and verify strictly thereafter: the
|
|
563
|
+
// managed known_hosts store must exist before ssh writes the learned key
|
|
564
|
+
// into it, and a host already recorded there is checked with
|
|
565
|
+
// StrictHostKeyChecking=yes (RUSH-1767).
|
|
566
|
+
ensureManagedKnownHostsDir();
|
|
567
|
+
const addr = hostNameFor(device);
|
|
568
|
+
const pinned = addr ? isHostPinned(addr) : false;
|
|
569
|
+
const { args, env } = buildSshInvocation(device, cmd, shim, { pinned });
|
|
519
570
|
const res = spawnSync('ssh', args, {
|
|
520
571
|
stdio: 'inherit',
|
|
521
572
|
env: { ...process.env, ...env },
|
|
@@ -544,7 +595,7 @@ async function runAskpass() {
|
|
|
544
595
|
process.exit(1);
|
|
545
596
|
}
|
|
546
597
|
try {
|
|
547
|
-
const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh' });
|
|
598
|
+
const { env } = readAndResolveBundleEnv(bundle, { caller: 'agents ssh', agentOnly: isHeadlessSecretsContext() });
|
|
548
599
|
const value = env[key];
|
|
549
600
|
if (value === undefined) {
|
|
550
601
|
console.error(`askpass: key '${key}' not found in bundle '${bundle}'`);
|
|
@@ -4,7 +4,7 @@ import ora from 'ora';
|
|
|
4
4
|
import * as fs from 'fs';
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import { select, confirm, checkbox } from '@inquirer/prompts';
|
|
7
|
-
import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
|
|
7
|
+
import { AGENTS, ALL_AGENT_IDS, accountOrgBadge, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, isSelfUpdatingAgent, } from '../lib/agents.js';
|
|
8
8
|
import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
9
9
|
import { viewAction } from './view.js';
|
|
10
10
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
@@ -32,8 +32,12 @@ function fixSessionFilePaths(agent, version, oldVersionDir) {
|
|
|
32
32
|
}
|
|
33
33
|
function formatAccountHint(info, usage) {
|
|
34
34
|
const parts = [];
|
|
35
|
-
if (info.email)
|
|
36
|
-
|
|
35
|
+
if (info.email) {
|
|
36
|
+
// Same-email accounts can live in different orgs (personal Max vs a Team
|
|
37
|
+
// seat) — the badge is what makes the picker disambiguate them.
|
|
38
|
+
const badge = accountOrgBadge(info);
|
|
39
|
+
parts.push(badge ? `${info.email} (${badge})` : info.email);
|
|
40
|
+
}
|
|
37
41
|
const usageSummary = formatUsageSummary(info.plan, usage);
|
|
38
42
|
if (usageSummary)
|
|
39
43
|
parts.push(usageSummary);
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -7,8 +7,23 @@
|
|
|
7
7
|
* rules, hooks, and promptcuts synced to that version.
|
|
8
8
|
*/
|
|
9
9
|
import type { Command } from 'commander';
|
|
10
|
+
import type { AccountInfo } from '../lib/agents.js';
|
|
10
11
|
import type { AgentId } from '../lib/types.js';
|
|
11
12
|
import { type ProfileSummary } from '../lib/profiles.js';
|
|
13
|
+
/**
|
|
14
|
+
* Text for the account (email) column. Prefers the real email; otherwise, for a
|
|
15
|
+
* signed-in agent whose credential carries no email but does carry an opaque
|
|
16
|
+
* account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
|
|
17
|
+
* distinctly instead of a generic "signed in". Falls back to "signed in" when we
|
|
18
|
+
* have neither (Antigravity), and empty when signed out.
|
|
19
|
+
*
|
|
20
|
+
* When the account carries a Claude organizationType, the org badge is appended
|
|
21
|
+
* — "email (Turing Labs · Team)" — so two installs signed into the same email
|
|
22
|
+
* under different orgs (personal Max vs a Team seat) read distinctly. The
|
|
23
|
+
* suffix is plain text inside the label so the existing column-width pass
|
|
24
|
+
* measures and pads it for free.
|
|
25
|
+
*/
|
|
26
|
+
export declare function accountColumnLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
12
27
|
type SyncState = 'synced' | 'new' | 'modified' | 'deleted';
|
|
13
28
|
/** Per-section filter flags. When any are true, only those sections render. */
|
|
14
29
|
export interface ViewSectionFilter {
|
|
@@ -32,6 +47,8 @@ export interface ViewJsonVersion {
|
|
|
32
47
|
signedIn: boolean;
|
|
33
48
|
email: string | null;
|
|
34
49
|
accountId?: string | null;
|
|
50
|
+
organizationType?: string | null;
|
|
51
|
+
organizationName?: string | null;
|
|
35
52
|
plan: string | null;
|
|
36
53
|
usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null;
|
|
37
54
|
overageCredits?: {
|
|
@@ -86,6 +103,15 @@ export declare function parseResourceSections(options: {
|
|
|
86
103
|
resources?: string | boolean;
|
|
87
104
|
detailed?: boolean;
|
|
88
105
|
} & ViewSectionFilter, jsonMode: boolean): Set<ResourceSection>;
|
|
106
|
+
/**
|
|
107
|
+
* Identity key for duplicate-install detection. Prefers accountKey — which
|
|
108
|
+
* encodes account AND org — over the bare email: two installs can share an
|
|
109
|
+
* email yet belong to different orgs (a personal Max plan and a Team seat),
|
|
110
|
+
* and grouping those by email alone would propose pruning a live account.
|
|
111
|
+
* Falls back to the lowercased email for agents whose credentials expose no
|
|
112
|
+
* identity key. Null when there is no usable identity.
|
|
113
|
+
*/
|
|
114
|
+
export declare function pruneGroupKey(info: Pick<AccountInfo, 'accountKey' | 'email'>): string | null;
|
|
89
115
|
/**
|
|
90
116
|
* Prune older installed versions that share an email with a newer installed
|
|
91
117
|
* version. Keeps the highest semver per email, skips the global default (with
|