@phnx-labs/agents-cli 1.22.68 → 1.22.70
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 +46 -0
- package/README.md +17 -14
- package/dist/bootstrap.js +3 -0
- package/dist/commands/accounts.js +23 -7
- package/dist/commands/exec.js +33 -23
- package/dist/commands/feed.js +20 -7
- package/dist/commands/import.js +2 -2
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +3 -0
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/rules.js +1 -1
- package/dist/commands/send.js +35 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/share.js +43 -14
- package/dist/commands/ssh.js +205 -2
- package/dist/commands/traces.js +1 -1
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-registry.d.ts +3 -3
- package/dist/lib/account-registry.js +25 -7
- package/dist/lib/accounting/usage.d.ts +7 -2
- package/dist/lib/accounting/usage.js +142 -10
- package/dist/lib/acp/client.d.ts +1 -1
- package/dist/lib/acp/client.js +12 -1
- package/dist/lib/acp/harnesses.js +1 -1
- package/dist/lib/add-dir.js +0 -2
- package/dist/lib/agent-cli-commands.js +0 -2
- package/dist/lib/agent-spec/agents.d.ts +1 -1
- package/dist/lib/agent-spec/agents.js +2 -83
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/ipc.js +9 -1
- package/dist/lib/browser/remote-control.d.ts +6 -3
- package/dist/lib/browser/remote-control.js +6 -3
- package/dist/lib/browser/service.d.ts +4 -1
- package/dist/lib/browser/service.js +7 -1
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/channels/registry.d.ts +6 -0
- package/dist/lib/channels/send.d.ts +11 -2
- package/dist/lib/channels/send.js +11 -2
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/cloud/rush.d.ts +10 -2
- package/dist/lib/cloud/rush.js +15 -9
- package/dist/lib/daemon/daemon.js +28 -8
- package/dist/lib/daemon/runner.js +6 -2
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.d.ts +2 -2
- package/dist/lib/exec.js +7 -34
- package/dist/lib/feed-broadcast.d.ts +47 -5
- package/dist/lib/feed-broadcast.js +127 -24
- package/dist/lib/hooks/install.js +0 -87
- package/dist/lib/installations/strategies.js +1 -1
- package/dist/lib/mcp-registry.js +0 -13
- package/dist/lib/mcp.js +2 -2
- package/dist/lib/model-tiers.js +1 -1
- package/dist/lib/models.js +0 -63
- package/dist/lib/monitors/engine.js +18 -0
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/notify.d.ts +11 -0
- package/dist/lib/notify.js +17 -4
- package/dist/lib/owner-message.d.ts +55 -0
- package/dist/lib/owner-message.js +64 -0
- package/dist/lib/permissions-registry.d.ts +0 -2
- package/dist/lib/permissions-registry.js +3 -50
- package/dist/lib/permissions.d.ts +3 -17
- package/dist/lib/permissions.js +4 -73
- package/dist/lib/run-trace-sync.d.ts +15 -0
- package/dist/lib/run-trace-sync.js +43 -21
- package/dist/lib/rush-session.d.ts +19 -0
- package/dist/lib/rush-session.js +24 -0
- package/dist/lib/secrets/drivers/rush.js +2 -1
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/session/active.d.ts +10 -0
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +12 -1
- package/dist/lib/session/db.js +20 -1
- package/dist/lib/session/discover.js +81 -1
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +7 -1
- package/dist/lib/session/prompt.js +12 -2
- package/dist/lib/session/recovery.d.ts +21 -12
- package/dist/lib/session/recovery.js +29 -11
- package/dist/lib/session/remote/watch.js +5 -2
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +13 -3
- package/dist/lib/share/publish.js +19 -15
- package/dist/lib/share/worker-template.js +5 -1
- package/dist/lib/sink-format.d.ts +34 -0
- package/dist/lib/sink-format.js +17 -0
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/staleness/detectors/permissions.js +0 -20
- package/dist/lib/staleness/writers/commands.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +2 -2
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/subagents-registry.js +2 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -37
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
|
@@ -35,16 +35,33 @@ export const PROJECTS_NO_FANOUT_ENV = 'AGENTS_PROJECTS_LOCAL';
|
|
|
35
35
|
/** Max peers named in the skipped note before the rest collapse to `+N`. */
|
|
36
36
|
const SKIPPED_NAME_LIMIT = 4;
|
|
37
37
|
/**
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* should be printed.
|
|
38
|
+
* A path-shaped `agents projects view` argument — `.`, `..`, a `~`-prefixed
|
|
39
|
+
* value, or anything containing a path separator — means "auto-detect the
|
|
40
|
+
* project for this DIRECTORY", not "a project named literally this". A bare
|
|
41
|
+
* token (no separators) stays a project name, exactly as before.
|
|
43
42
|
*/
|
|
44
|
-
export function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
export function looksLikePath(token) {
|
|
44
|
+
return token === '.' || token === '..' || token.startsWith('~') || token.includes('/');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Best-effort cwd->project detection for `agents projects view <path>`. Delegates
|
|
48
|
+
* to the shared {@link projectNameForCwd} (def-root containment, longest wins),
|
|
49
|
+
* then reads the matched definition's Linear binding and root so ONE call yields
|
|
50
|
+
* the name AND the Linear projectId. Fail-open: an all-null shape when nothing
|
|
51
|
+
* contains the path — never throws, so a scripted caller stays unscoped rather
|
|
52
|
+
* than crashing. This subsumes the removed `agents projects for-cwd`.
|
|
53
|
+
*/
|
|
54
|
+
export function detectProjectForPath(cwd, defs) {
|
|
55
|
+
const name = projectNameForCwd(cwd, defs);
|
|
56
|
+
const def = name ? defs.find((d) => d.name === name) : undefined;
|
|
57
|
+
return {
|
|
58
|
+
name: name ?? null,
|
|
59
|
+
linear: {
|
|
60
|
+
name: def?.linear?.name ?? null,
|
|
61
|
+
projectId: def?.linear?.projectId ?? null,
|
|
62
|
+
},
|
|
63
|
+
root: def?.root ?? null,
|
|
64
|
+
};
|
|
48
65
|
}
|
|
49
66
|
/**
|
|
50
67
|
* One compact trailing note for peers that didn't answer the `--fleet`
|
|
@@ -570,17 +587,6 @@ export function registerProjectsCommands(program) {
|
|
|
570
587
|
console.log(` ${chalk.bold(row.name.padEnd(w.name))} ${chalk.dim(row.path.padEnd(w.path))} ${chalk.cyan(row.repo.padEnd(w.repo))}${agentsSuffix}`);
|
|
571
588
|
}
|
|
572
589
|
});
|
|
573
|
-
// ---- for-cwd ----
|
|
574
|
-
projects
|
|
575
|
-
.command('for-cwd [cwd]')
|
|
576
|
-
.description('Resolve a directory to its defined project name (root or a repos[].path/subpath match). Defaults to the current directory.')
|
|
577
|
-
.option('--json', 'Machine-readable output: {"name": string | null}')
|
|
578
|
-
.action((cwdArg, opts) => {
|
|
579
|
-
const name = projectNameForCwd(cwdArg ?? process.cwd(), listProjectDefs());
|
|
580
|
-
const out = formatForCwdOutput(name, !!opts.json);
|
|
581
|
-
if (out)
|
|
582
|
-
console.log(out);
|
|
583
|
-
});
|
|
584
590
|
// ---- add ----
|
|
585
591
|
projects
|
|
586
592
|
.command('add <name>')
|
|
@@ -763,15 +769,42 @@ export function registerProjectsCommands(program) {
|
|
|
763
769
|
process.stdout.write(formatFleetSkippedNote(fleetSkipped));
|
|
764
770
|
}
|
|
765
771
|
projects
|
|
766
|
-
.command('status [
|
|
772
|
+
.command('status [nameOrPath]')
|
|
767
773
|
.alias('view')
|
|
768
|
-
.description('Progress card for every project across the whole fleet, or one named project (alias: view). Named form also prints every milestone and the stored definition.')
|
|
774
|
+
.description('Progress card for every project across the whole fleet, or one named project (alias: view). Named form also prints every milestone and the stored definition. A path argument (., .., a ~-prefixed value, a /-containing value, or --path) auto-detects the project that CONTAINS that directory.')
|
|
769
775
|
.option('--json', 'Machine-readable output')
|
|
776
|
+
.option('--path [dir]', 'Treat the argument as a DIRECTORY and auto-detect the project that contains it (bare --path uses the cwd). With --json prints {name, linear:{name,projectId}, root}, all-null when nothing matches (fail-open, exit 0).')
|
|
770
777
|
.option('--window <days>', 'Window for merged PRs, artifacts, and focus areas', '7')
|
|
771
778
|
.option('--no-remote', 'Skip the GitHub and Linear lookups; faster, offline')
|
|
772
779
|
.option('--device <name...>', 'Scope fleet status to one or more devices (repeatable)')
|
|
773
780
|
.option('--devices <names>', 'Scope fleet status to a comma-separated list of devices')
|
|
774
781
|
.action(async (name, rawOpts) => {
|
|
782
|
+
// Path mode: an explicit --path, or a path-shaped positional (., .., a
|
|
783
|
+
// ~-prefixed value, or a /-containing value) means "auto-detect the
|
|
784
|
+
// project for this DIRECTORY" rather than "a project named literally
|
|
785
|
+
// this". projectNameForCwd resolves the dir (expandLocalHome +
|
|
786
|
+
// path.resolve), so `.`, `~/…`, and relative paths all normalize. This
|
|
787
|
+
// subsumes the removed `agents projects for-cwd`.
|
|
788
|
+
let detectDir;
|
|
789
|
+
if (typeof rawOpts.path === 'string')
|
|
790
|
+
detectDir = rawOpts.path;
|
|
791
|
+
else if (rawOpts.path === true)
|
|
792
|
+
detectDir = process.cwd();
|
|
793
|
+
else if (name !== undefined && looksLikePath(name))
|
|
794
|
+
detectDir = name;
|
|
795
|
+
if (detectDir !== undefined) {
|
|
796
|
+
const detection = detectProjectForPath(detectDir, listProjectDefs());
|
|
797
|
+
if (rawOpts.json) {
|
|
798
|
+
console.log(JSON.stringify(detection));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
if (!detection.name) {
|
|
802
|
+
console.log(chalk.gray(`No defined project contains ${detectDir}`));
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
// Non-JSON: fall through and render the full card for the detected project.
|
|
806
|
+
name = detection.name;
|
|
807
|
+
}
|
|
775
808
|
// Named invocation = `view` depth (all milestones + definition). Unnamed
|
|
776
809
|
// stays the scannable multi-project rollup. `view` is a commander alias
|
|
777
810
|
// of this same command, so there is only one implementation.
|
package/dist/commands/rules.js
CHANGED
|
@@ -52,7 +52,7 @@ Project rules & @-imports:
|
|
|
52
52
|
@path imports inside AGENTS.md/CLAUDE.md are resolved at session start by the agent
|
|
53
53
|
itself, not by agents-cli. Support is per-agent:
|
|
54
54
|
Inlined natively: claude
|
|
55
|
-
Literal text: codex, cursor, opencode, copilot, amp,
|
|
55
|
+
Literal text: codex, cursor, opencode, copilot, amp, goose
|
|
56
56
|
|
|
57
57
|
For rules that need to work across all agents, inline the content rather than using
|
|
58
58
|
@-imports — the second group will load '@path/to/file.md' as a literal string.
|
package/dist/commands/send.js
CHANGED
|
@@ -2,7 +2,9 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { die } from '../lib/format.js';
|
|
3
3
|
import { setHelpSections } from '../lib/help.js';
|
|
4
4
|
import { readMeta } from '../lib/state.js';
|
|
5
|
-
import { sendMessage } from '../lib/channels/send.js';
|
|
5
|
+
import { sendMessage, isOwnerAlias } from '../lib/channels/send.js';
|
|
6
|
+
import { ownerMessageComposer } from '../lib/owner-message.js';
|
|
7
|
+
import { fireTraceSyncInBackground } from '../lib/run-trace-sync.js';
|
|
6
8
|
function mergeAttachments(opts) {
|
|
7
9
|
const list = [...(opts.attach ?? []), ...(opts.attachment ?? [])];
|
|
8
10
|
return list.length ? list : undefined;
|
|
@@ -23,7 +25,34 @@ function toInput(positionalText, opts, ownerMode) {
|
|
|
23
25
|
}
|
|
24
26
|
async function runSend(positionalText, opts, ownerMode) {
|
|
25
27
|
const meta = readMeta();
|
|
26
|
-
|
|
28
|
+
let input = toInput(positionalText, opts, ownerMode);
|
|
29
|
+
// An owner-bound ping (`agents notify`, `agents send --to owner`) goes through
|
|
30
|
+
// the SAME composer as an important `feed post` (PHNX-3698): short-shaped body,
|
|
31
|
+
// TEAM-N keys linkified, session crumb as a tappable console URL — instead of a
|
|
32
|
+
// raw dump. A non-owner send (explicit --channel/--to) is delivered verbatim.
|
|
33
|
+
let ownerCompose;
|
|
34
|
+
if (ownerMode || isOwnerAlias(opts.to)) {
|
|
35
|
+
const flagged = opts.text?.trim() ?? '';
|
|
36
|
+
const positional = (positionalText ?? '').trim();
|
|
37
|
+
const raw = flagged || positional;
|
|
38
|
+
// When both forms are given and disagree, leave it to sendMessage to fail
|
|
39
|
+
// loud with the "pass the message once" error rather than composing a guess.
|
|
40
|
+
const bothDiffer = flagged !== '' && positional !== '' && flagged !== positional;
|
|
41
|
+
if (raw && !bothDiffer) {
|
|
42
|
+
// Resolve the context once, then let each owner destination render its own
|
|
43
|
+
// format: the envelope carries the plain default (for --json and dry-run
|
|
44
|
+
// display), while ownerCompose hands sendToOwner the mrkdwn variant for a
|
|
45
|
+
// Slack owner destination and plain for iMessage (PHNX-3698).
|
|
46
|
+
ownerCompose = ownerMessageComposer(raw);
|
|
47
|
+
input = { ...input, text: ownerCompose('plain'), positionalText: undefined };
|
|
48
|
+
// The console URL in the composed body only resolves once this session's
|
|
49
|
+
// trace shard is uploaded; fire that now so the tapped link isn't a 404.
|
|
50
|
+
// A --dry-run resolves + composes but MUST NOT act (its documented contract),
|
|
51
|
+
// so it never spawns the sync — it just shows what would be sent.
|
|
52
|
+
fireTraceSyncInBackground({ disabled: Boolean(opts.dryRun) });
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const out = await sendMessage(input, meta, ownerCompose);
|
|
27
56
|
if ('error' in out) {
|
|
28
57
|
die(out.error);
|
|
29
58
|
}
|
|
@@ -127,6 +156,10 @@ export function registerSendCommand(program) {
|
|
|
127
156
|
Set owner.channels + owner.policy.normal in humans.yaml once per fleet.
|
|
128
157
|
Every channel listed in the normal policy receives an owner-addressed send.
|
|
129
158
|
|
|
159
|
+
Owner sends go through the same composer as "feed post": the body is
|
|
160
|
+
short-shaped, any TEAM-N key becomes a Linear URL, and the session crumb
|
|
161
|
+
becomes a tappable https://prix.dev/console/sessions/<id> link.
|
|
162
|
+
|
|
130
163
|
${SHARED_NOTES}
|
|
131
164
|
`,
|
|
132
165
|
});
|
|
@@ -13,5 +13,63 @@
|
|
|
13
13
|
* `--pane`/`--pty` target a backend directly when the handle is already known.
|
|
14
14
|
*/
|
|
15
15
|
import type { Command } from 'commander';
|
|
16
|
+
import { type ActiveSession } from '../lib/session/active.js';
|
|
17
|
+
interface InjectOptions {
|
|
18
|
+
pane?: string;
|
|
19
|
+
socket?: string;
|
|
20
|
+
pty?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The remote device. A single `--device box` arrives here as `['box']` because
|
|
23
|
+
* the parent `sessions` command's variadic `-D, --device <target...>` shadows
|
|
24
|
+
* this subcommand's scalar option under `optsWithGlobals()` — normalize it with
|
|
25
|
+
* {@link normalizeInjectDevice} before use (PHNX-3688).
|
|
26
|
+
*/
|
|
27
|
+
device?: string | string[];
|
|
28
|
+
enter?: boolean;
|
|
29
|
+
combined?: boolean;
|
|
30
|
+
json?: boolean;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Whether an active session is the one `sessions inject <token>` means. Matches
|
|
34
|
+
* a resolvable session id (exact or unique prefix) AND — for a tmux-hosted row
|
|
35
|
+
* whose full id never resolved (`sessionId` absent) — the `ag-<agent>-<shortid>`
|
|
36
|
+
* tmux name's `shortid` suffix (exact or prefix), the full tmux name, and the
|
|
37
|
+
* pane id. Those are the only selectors an id-less remote tmux row exposes, so
|
|
38
|
+
* without this an operator has no tool-native way to nudge it (PHNX-3688).
|
|
39
|
+
*/
|
|
40
|
+
export declare function matchInjectSelector(session: ActiveSession, token: string): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* The `--device` selector, normalized to a single host string. `optsWithGlobals()`
|
|
43
|
+
* merges the parent `sessions` command's variadic `-D, --device <target...>` over
|
|
44
|
+
* this subcommand's scalar `--device`, so a single `--device box` arrives as
|
|
45
|
+
* `['box']` — which flowed straight into `sshExec` and crashed on
|
|
46
|
+
* `host.startsWith` (PHNX-3688). Coerce the array to its one element; fail loud on
|
|
47
|
+
* several, since inject delivers to exactly one terminal (a fan-out spelling is a
|
|
48
|
+
* user error, not a first-of-list guess).
|
|
49
|
+
*/
|
|
50
|
+
export declare function normalizeInjectDevice(value: string | string[] | undefined): string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* The `agents sessions inject` argv to re-run ON a device (its tmux panes live
|
|
53
|
+
* there, so resolution must happen there). Every flag rides along EXCEPT
|
|
54
|
+
* `--device`: the command runs on the device, resolving locally. Pure so the
|
|
55
|
+
* forwarded invocation is asserted without an SSH hop (PHNX-3688).
|
|
56
|
+
*/
|
|
57
|
+
export declare function buildRemoteInjectArgv(sessionId: string, text: string, options: InjectOptions): string[];
|
|
58
|
+
/**
|
|
59
|
+
* Resolve `device` (registry alias or `user@host`) to an ssh target and re-run
|
|
60
|
+
* `agents sessions inject` there, so a bare session id + `--device` resolves on
|
|
61
|
+
* the box that actually holds the session's tmux panes. The tool-native form of
|
|
62
|
+
* the `agents ssh <device> "agents sessions inject <id> …"` workaround (PHNX-3688).
|
|
63
|
+
*/
|
|
64
|
+
/**
|
|
65
|
+
* Resolve `--device` to an ssh target. A registered device becomes its
|
|
66
|
+
* `user@dnsName`; a bare unknown name (an ad-hoc `user@host` or ssh_config alias)
|
|
67
|
+
* is handed to ssh verbatim (`resolveHost` returns null for it). A registered
|
|
68
|
+
* device we CANNOT dial — password-auth, addressless — throws its typed error and
|
|
69
|
+
* is NOT degraded to the raw name, which could ssh a coincidentally-matching but
|
|
70
|
+
* unrelated `~/.ssh/config` Host (PHNX-3688 review).
|
|
71
|
+
*/
|
|
72
|
+
export declare function resolveInjectSshTarget(device: string): Promise<string>;
|
|
16
73
|
/** Attach the `inject` subcommand to an existing `sessions` command. */
|
|
17
74
|
export declare function registerSessionsInjectCommand(sessionsCmd: Command): void;
|
|
75
|
+
export {};
|
|
@@ -13,14 +13,121 @@
|
|
|
13
13
|
* `--pane`/`--pty` target a backend directly when the handle is already known.
|
|
14
14
|
*/
|
|
15
15
|
import chalk from 'chalk';
|
|
16
|
-
import { getActiveSessions } from '../lib/session/active.js';
|
|
16
|
+
import { getActiveSessions, shortIdFromName } from '../lib/session/active.js';
|
|
17
17
|
import { injectIntoTerminal, resolveInjectTargetForSession } from '../lib/terminal/index.js';
|
|
18
|
+
import { sshExec, shellQuote } from '../lib/ssh-exec.js';
|
|
19
|
+
import { resolveHost } from '../lib/hosts/registry.js';
|
|
20
|
+
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
18
21
|
import { setHelpSections } from '../lib/help.js';
|
|
22
|
+
/**
|
|
23
|
+
* Whether an active session is the one `sessions inject <token>` means. Matches
|
|
24
|
+
* a resolvable session id (exact or unique prefix) AND — for a tmux-hosted row
|
|
25
|
+
* whose full id never resolved (`sessionId` absent) — the `ag-<agent>-<shortid>`
|
|
26
|
+
* tmux name's `shortid` suffix (exact or prefix), the full tmux name, and the
|
|
27
|
+
* pane id. Those are the only selectors an id-less remote tmux row exposes, so
|
|
28
|
+
* without this an operator has no tool-native way to nudge it (PHNX-3688).
|
|
29
|
+
*/
|
|
30
|
+
export function matchInjectSelector(session, token) {
|
|
31
|
+
if (!token)
|
|
32
|
+
return false;
|
|
33
|
+
const sid = session.sessionId;
|
|
34
|
+
if (sid && (sid === token || sid.startsWith(token)))
|
|
35
|
+
return true;
|
|
36
|
+
const short = session.tmuxName ? shortIdFromName(session.tmuxName) : undefined;
|
|
37
|
+
if (short && (short === token || short.startsWith(token)))
|
|
38
|
+
return true;
|
|
39
|
+
if (session.tmuxName && session.tmuxName === token)
|
|
40
|
+
return true;
|
|
41
|
+
if (session.paneId && session.paneId === token)
|
|
42
|
+
return true;
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* The `--device` selector, normalized to a single host string. `optsWithGlobals()`
|
|
47
|
+
* merges the parent `sessions` command's variadic `-D, --device <target...>` over
|
|
48
|
+
* this subcommand's scalar `--device`, so a single `--device box` arrives as
|
|
49
|
+
* `['box']` — which flowed straight into `sshExec` and crashed on
|
|
50
|
+
* `host.startsWith` (PHNX-3688). Coerce the array to its one element; fail loud on
|
|
51
|
+
* several, since inject delivers to exactly one terminal (a fan-out spelling is a
|
|
52
|
+
* user error, not a first-of-list guess).
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeInjectDevice(value) {
|
|
55
|
+
const list = value == null ? [] : Array.isArray(value) ? value : [value];
|
|
56
|
+
const hosts = list.map((v) => String(v).trim()).filter((v) => v.length > 0);
|
|
57
|
+
if (hosts.length === 0)
|
|
58
|
+
return undefined;
|
|
59
|
+
if (hosts.length > 1) {
|
|
60
|
+
throw new Error(`sessions inject targets a single device, but --device named ${hosts.length}: ${hosts.join(', ')}.`);
|
|
61
|
+
}
|
|
62
|
+
return hosts[0];
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The `agents sessions inject` argv to re-run ON a device (its tmux panes live
|
|
66
|
+
* there, so resolution must happen there). Every flag rides along EXCEPT
|
|
67
|
+
* `--device`: the command runs on the device, resolving locally. Pure so the
|
|
68
|
+
* forwarded invocation is asserted without an SSH hop (PHNX-3688).
|
|
69
|
+
*/
|
|
70
|
+
export function buildRemoteInjectArgv(sessionId, text, options) {
|
|
71
|
+
const argv = ['agents', 'sessions', 'inject', sessionId, text];
|
|
72
|
+
if (options.enter === false)
|
|
73
|
+
argv.push('--no-enter');
|
|
74
|
+
if (options.combined)
|
|
75
|
+
argv.push('--combined');
|
|
76
|
+
if (options.socket)
|
|
77
|
+
argv.push('--socket', options.socket);
|
|
78
|
+
if (options.pty)
|
|
79
|
+
argv.push('--pty', options.pty);
|
|
80
|
+
if (options.pane)
|
|
81
|
+
argv.push('--pane', options.pane);
|
|
82
|
+
if (options.json)
|
|
83
|
+
argv.push('--json');
|
|
84
|
+
return argv;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Resolve `device` (registry alias or `user@host`) to an ssh target and re-run
|
|
88
|
+
* `agents sessions inject` there, so a bare session id + `--device` resolves on
|
|
89
|
+
* the box that actually holds the session's tmux panes. The tool-native form of
|
|
90
|
+
* the `agents ssh <device> "agents sessions inject <id> …"` workaround (PHNX-3688).
|
|
91
|
+
*/
|
|
92
|
+
/**
|
|
93
|
+
* Resolve `--device` to an ssh target. A registered device becomes its
|
|
94
|
+
* `user@dnsName`; a bare unknown name (an ad-hoc `user@host` or ssh_config alias)
|
|
95
|
+
* is handed to ssh verbatim (`resolveHost` returns null for it). A registered
|
|
96
|
+
* device we CANNOT dial — password-auth, addressless — throws its typed error and
|
|
97
|
+
* is NOT degraded to the raw name, which could ssh a coincidentally-matching but
|
|
98
|
+
* unrelated `~/.ssh/config` Host (PHNX-3688 review).
|
|
99
|
+
*/
|
|
100
|
+
export async function resolveInjectSshTarget(device) {
|
|
101
|
+
const host = await resolveHost(device);
|
|
102
|
+
return host ? sshTargetFor(host) : device;
|
|
103
|
+
}
|
|
104
|
+
async function injectOnDevice(sessionId, text, options, device) {
|
|
105
|
+
let target;
|
|
106
|
+
try {
|
|
107
|
+
target = await resolveInjectSshTarget(device);
|
|
108
|
+
}
|
|
109
|
+
catch (err) {
|
|
110
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
111
|
+
if (options.json)
|
|
112
|
+
console.log(JSON.stringify({ ok: false, error: message }));
|
|
113
|
+
else
|
|
114
|
+
console.error(chalk.red(message));
|
|
115
|
+
process.exit(1);
|
|
116
|
+
}
|
|
117
|
+
const remoteCmd = buildRemoteInjectArgv(sessionId, text, options).map(shellQuote).join(' ');
|
|
118
|
+
const res = sshExec(target, remoteCmd, { multiplex: true });
|
|
119
|
+
if (res.stdout)
|
|
120
|
+
process.stdout.write(res.stdout);
|
|
121
|
+
if (res.stderr)
|
|
122
|
+
process.stderr.write(res.stderr);
|
|
123
|
+
if (res.code !== 0)
|
|
124
|
+
process.exit(res.code ?? 1);
|
|
125
|
+
}
|
|
19
126
|
/** Resolve a session id (short or full) to an addressable terminal target, via the
|
|
20
127
|
* same resolver the watchdog uses so both agree on what is reachable. */
|
|
21
128
|
async function resolveTarget(sessionId) {
|
|
22
129
|
const sessions = await getActiveSessions();
|
|
23
|
-
const match = sessions.find((s) =>
|
|
130
|
+
const match = sessions.find((s) => matchInjectSelector(s, sessionId));
|
|
24
131
|
if (!match)
|
|
25
132
|
return { target: null, reason: `No active session matches "${sessionId}".` };
|
|
26
133
|
const resolution = resolveInjectTargetForSession(match);
|
|
@@ -35,8 +142,21 @@ async function resolveTarget(sessionId) {
|
|
|
35
142
|
return { target: resolution.target };
|
|
36
143
|
}
|
|
37
144
|
async function runInject(sessionId, text, options) {
|
|
145
|
+
let device;
|
|
146
|
+
try {
|
|
147
|
+
device = normalizeInjectDevice(options.device);
|
|
148
|
+
}
|
|
149
|
+
catch (err) {
|
|
150
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
151
|
+
if (options.json)
|
|
152
|
+
console.log(JSON.stringify({ ok: false, error: message }));
|
|
153
|
+
else
|
|
154
|
+
console.error(chalk.red(message));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
38
157
|
// Direct-target shortcuts skip the session lookup — the watchdog often already
|
|
39
|
-
// holds the pane id or pty session it wants to type into.
|
|
158
|
+
// holds the pane id or pty session it wants to type into. The pane's exact
|
|
159
|
+
// address is known, so it composes with --device (tmux send-keys over SSH).
|
|
40
160
|
let target = null;
|
|
41
161
|
if (options.pty) {
|
|
42
162
|
target = { backend: 'pty', id: options.pty };
|
|
@@ -44,6 +164,12 @@ async function runInject(sessionId, text, options) {
|
|
|
44
164
|
else if (options.pane) {
|
|
45
165
|
target = { backend: 'tmux', pane: options.pane, socket: options.socket };
|
|
46
166
|
}
|
|
167
|
+
else if (device) {
|
|
168
|
+
// A bare session id + a device: the session's tmux panes live ON that device,
|
|
169
|
+
// so getActiveSessions here can't see them. Resolve + deliver THERE by re-running
|
|
170
|
+
// inject over SSH (the same command, minus --device) — PHNX-3688.
|
|
171
|
+
return injectOnDevice(sessionId, text, options, device);
|
|
172
|
+
}
|
|
47
173
|
else {
|
|
48
174
|
const resolved = await resolveTarget(sessionId);
|
|
49
175
|
if (!resolved.target) {
|
|
@@ -60,7 +186,7 @@ async function runInject(sessionId, text, options) {
|
|
|
60
186
|
enter: options.enter !== false,
|
|
61
187
|
combined: options.combined,
|
|
62
188
|
socket: options.socket,
|
|
63
|
-
host:
|
|
189
|
+
host: device,
|
|
64
190
|
});
|
|
65
191
|
if (options.json) {
|
|
66
192
|
console.log(JSON.stringify(res));
|
|
@@ -82,7 +208,7 @@ export function registerSessionsInjectCommand(sessionsCmd) {
|
|
|
82
208
|
.option('--pane <id>', 'Target a tmux pane id directly (e.g. %3), skipping session lookup')
|
|
83
209
|
.option('--pty <id>', 'Target an agents-pty session id directly, skipping session lookup')
|
|
84
210
|
.option('--socket <path>', 'tmux socket path (defaults to the session/shared socket)')
|
|
85
|
-
.option('--device <target>', 'Deliver on a remote device over SSH
|
|
211
|
+
.option('--device <target>', 'Deliver on a remote device over SSH. With a bare session id, the session is resolved ON that device; with --pane, the pane is addressed there directly.')
|
|
86
212
|
.option('--no-enter', 'Send only the text, without a trailing Enter')
|
|
87
213
|
.option('--combined', 'Fuse text + Enter into ONE write (default: two writes, Ink-TUI safe)')
|
|
88
214
|
.option('--json', 'Output the InjectResult as JSON');
|
|
@@ -96,6 +222,12 @@ export function registerSessionsInjectCommand(sessionsCmd) {
|
|
|
96
222
|
|
|
97
223
|
# Type into an agents-pty session without submitting
|
|
98
224
|
agents sessions inject _ "ls" --pty $SID --no-enter
|
|
225
|
+
|
|
226
|
+
# Nudge a live session on another box (resolved on the device)
|
|
227
|
+
agents sessions inject 214edaae "continue" --device yosemite-s0
|
|
228
|
+
|
|
229
|
+
# Address a known remote pane directly (skips lookup, sends over SSH)
|
|
230
|
+
agents sessions inject _ "continue" --pane %122 --socket $SOCK --device yosemite-s0
|
|
99
231
|
`,
|
|
100
232
|
notes: `
|
|
101
233
|
- Ink-TUI Enter semantics: by default the text and Enter are two separate
|
|
@@ -103,8 +235,12 @@ export function registerSessionsInjectCommand(sessionsCmd) {
|
|
|
103
235
|
- A session is addressable by id when it resolves to a precise split —
|
|
104
236
|
tmux, iTerm, a VSCodium/Cursor/VS Code integrated terminal, or a pty
|
|
105
237
|
(resolveInjectTargetForSession). Use --pane/--pty for direct targeting.
|
|
106
|
-
-
|
|
107
|
-
|
|
238
|
+
- The id may be the session id (short or full) OR the '<shortid>' suffix of
|
|
239
|
+
a tmux target (ag-<agent>-<shortid>) — the only selector a live tmux
|
|
240
|
+
session whose id column shows '-' exposes.
|
|
241
|
+
- Built on the Terminal Engine (src/lib/terminal): with --pane, --device
|
|
242
|
+
runs the tmux send-keys spec over SSH; with a bare id, --device re-runs
|
|
243
|
+
the lookup on that box (its tmux panes live there, not here).
|
|
108
244
|
`,
|
|
109
245
|
});
|
|
110
246
|
// The parent `sessions` command also defines --json, so it binds there;
|
package/dist/commands/share.js
CHANGED
|
@@ -11,11 +11,13 @@ import { Argument, Option } from 'commander';
|
|
|
11
11
|
import chalk from 'chalk';
|
|
12
12
|
import { DEFAULT_BUCKET_NAME, DEFAULT_CF_BUNDLE, DEFAULT_SHARE_DOMAIN, DEFAULT_WORKER_NAME, generateWriteToken, readCloudflareCreds, readShareConfig, readWriteToken, readWriteTokenEnv, readWriteTokenFromBundle, storeWriteToken, writeShareConfig, } from '../lib/share/config.js';
|
|
13
13
|
import { addCustomDomain, configureBucketLifecycle, createBucket, deployWorker, enableWorkersDev, findZoneId, hashWorkerScript, putWorkerSecret, updateWorker, WORKER_PHOENIX_ID_BASE_SECRET, setWorkerSecret, } from '../lib/share/provision.js';
|
|
14
|
-
import { publishFile, resolveShareUsername, parseMetaEntries, sanitizeLabel,
|
|
14
|
+
import { publishFile, resolveShareUsername, parseMetaEntries, sanitizeLabel, scanShareContent, formatSensitiveContentError, unlistedNotPrivateWarning, SHARE_VISIBILITY_LEVELS, PUBLISH_VISIBILITY_LEVELS, } from '../lib/share/publish.js';
|
|
15
15
|
import { deleteShare, resolveDeleteTarget } from '../lib/share/delete.js';
|
|
16
16
|
import { renderWorkerBundle } from '../lib/share/worker-template.js';
|
|
17
17
|
import { analyticsEnabled } from '../lib/share/analytics.js';
|
|
18
|
-
import {
|
|
18
|
+
import { extractShareHttpError, formatShareHttpErrorDetail } from '../lib/share/http-error.js';
|
|
19
|
+
import { phoenixIdBaseForDeploy, resolveShareBackend, shouldUseManaged, } from '../lib/share/backend.js';
|
|
20
|
+
import { publishVisibility } from '../lib/storage/visibility.js';
|
|
19
21
|
import { resolveGitHubUsername } from '../lib/git.js';
|
|
20
22
|
import { setHelpSections } from '../lib/help.js';
|
|
21
23
|
import { showUrl } from '../lib/open-url.js';
|
|
@@ -245,7 +247,8 @@ export async function runShareList(opts = {}) {
|
|
|
245
247
|
throw new Error(OUTDATED_TEMPLATE_HINT);
|
|
246
248
|
}
|
|
247
249
|
if (res.status !== 200) {
|
|
248
|
-
|
|
250
|
+
const detail = formatShareHttpErrorDetail(extractShareHttpError({ status: res.status, body: res.body }));
|
|
251
|
+
throw new Error(`Listing failed (${res.status}) for ${listUrl}${detail}. Check the endpoint is reachable, or that 'agents artifacts setup' / 'agents auth login' completed.`);
|
|
249
252
|
}
|
|
250
253
|
if (!/application\/json/i.test(res.contentType)) {
|
|
251
254
|
// A 200 that isn't JSON means the old Worker ignored ?format=json and served
|
|
@@ -389,7 +392,8 @@ export async function runShareRevisions(target, opts = {}) {
|
|
|
389
392
|
const fetchListing = opts.fetchListing ?? defaultListingFetch;
|
|
390
393
|
const res = await fetchListing(revUrl);
|
|
391
394
|
if (res.status !== 200) {
|
|
392
|
-
|
|
395
|
+
const detail = formatShareHttpErrorDetail(extractShareHttpError({ status: res.status, body: res.body }));
|
|
396
|
+
throw new Error(`Revisions lookup failed (${res.status}) for ${revUrl}${detail}. Check the endpoint is reachable, or that 'agents artifacts setup' / 'agents auth login' completed.`);
|
|
393
397
|
}
|
|
394
398
|
if (!/application\/json/i.test(res.contentType)) {
|
|
395
399
|
if (backend.kind === 'managed') {
|
|
@@ -624,9 +628,8 @@ export function registerShareCommands(artifactsCmd) {
|
|
|
624
628
|
.option('--slug <slug>', 'URL slug override (default: stable slug of the artifact title, then filename)')
|
|
625
629
|
.option('--github-user <user>', 'GitHub username for the share namespace (default: resolved from gh/git config; ignored on the managed endpoint)')
|
|
626
630
|
.option('--expire <spec>', "auto-expire (default 30d). e.g. 12h, 30d, 2026-08-01, or 'never'")
|
|
627
|
-
.addOption(new Option('--visibility <level>', 'public | unlisted | private | me | org (
|
|
628
|
-
.choices([...PUBLISH_VISIBILITY_LEVELS])
|
|
629
|
-
.default('public'))
|
|
631
|
+
.addOption(new Option('--visibility <level>', 'public | unlisted | private | me | org. Default when signed in: me (owner-only, Phoenix-gated); use org for your whole email domain, public to opt into the gallery. unlisted is an UNAUTHENTICATED capability URL; private is token-gated (404 without the key). All but public are hidden from the gallery')
|
|
632
|
+
.choices([...PUBLISH_VISIBILITY_LEVELS]))
|
|
630
633
|
.addOption(new Option('--protected', 'token-gated link (= --visibility private): the URL carries a secret key and returns 404 without it — the authenticated alternative to --unlisted'))
|
|
631
634
|
.addOption(new Option('--unlisted', 'hidden alias of --visibility unlisted (obscurity, NOT authentication)').hideHelp())
|
|
632
635
|
.addOption(new Option('--private', 'hidden alias of --visibility unlisted (obscurity, NOT authentication — for real read-auth use --protected)').hideHelp())
|
|
@@ -650,11 +653,16 @@ export function registerShareCommands(artifactsCmd) {
|
|
|
650
653
|
}
|
|
651
654
|
try {
|
|
652
655
|
const meta = parseMetaEntries(opts.meta);
|
|
653
|
-
|
|
656
|
+
// Private by default: an explicit flag wins; otherwise the backend-aware
|
|
657
|
+
// product default — `me` (owner-only) when signed in to Phoenix, `public`
|
|
658
|
+
// for a BYO bucket (the Worker refuses `me`/`org` without a Phoenix owner).
|
|
659
|
+
// `shouldUseManaged` reads the same session/BYO signals `publishFile` will
|
|
660
|
+
// resolve, so the default matches the backend the publish actually uses.
|
|
661
|
+
const visibility = publishVisibility({
|
|
654
662
|
visibility: opts.visibility,
|
|
655
663
|
unlisted: opts.unlisted === true || opts.private === true,
|
|
656
664
|
protected: opts.protected === true,
|
|
657
|
-
});
|
|
665
|
+
}, shouldUseManaged({}) ? 'managed' : 'byo');
|
|
658
666
|
// unlisted (incl. its --private alias) is obscurity, not read-auth — warn
|
|
659
667
|
// loudly before the publish so a "private" link is never mistaken for a
|
|
660
668
|
// gated one (PHNX-3654). Skipped for --json so machine output stays clean.
|
|
@@ -684,15 +692,25 @@ export function registerShareCommands(artifactsCmd) {
|
|
|
684
692
|
});
|
|
685
693
|
setHelpSections(shareCmd, {
|
|
686
694
|
examples: `
|
|
687
|
-
# Signed in? Just publish — no Cloudflare setup (managed endpoint)
|
|
695
|
+
# Signed in? Just publish — no Cloudflare setup (managed endpoint).
|
|
696
|
+
# Default is private: an owner-only 'me' link, Phoenix-gated.
|
|
688
697
|
agents auth login
|
|
689
698
|
agents artifacts share ./out/plan.html
|
|
690
699
|
|
|
700
|
+
# Share with everyone at your email domain (no org setup — the domain is it)
|
|
701
|
+
agents artifacts share ./out/plan.html --visibility org
|
|
702
|
+
|
|
703
|
+
# Opt into the public gallery with an OG preview card
|
|
704
|
+
agents artifacts share ./out/landing.html --visibility public
|
|
705
|
+
|
|
691
706
|
# Hide from the public gallery (direct URL still works, noindex) and expire sooner
|
|
692
707
|
agents artifacts share ./out/report.html --visibility unlisted --expire 12h
|
|
693
708
|
|
|
709
|
+
# Token-gated link — GET returns 404 without the secret ?k= key
|
|
710
|
+
agents artifacts share ./out/secret.html --protected
|
|
711
|
+
|
|
694
712
|
# Permanent public page (the slug is derived from its title)
|
|
695
|
-
agents artifacts share ./out/landing.html --expire never
|
|
713
|
+
agents artifacts share ./out/landing.html --visibility public --expire never
|
|
696
714
|
|
|
697
715
|
# Optional slug override
|
|
698
716
|
agents artifacts share ./out/report.html --slug q3-report --expire 7d
|
|
@@ -720,11 +738,21 @@ ${SHARE_DELETE_EXAMPLES}
|
|
|
720
738
|
applies (setup / join). HTML is stored as one object: local images are inlined
|
|
721
739
|
and Chrome-saved file:// TOC links become in-page hashes.
|
|
722
740
|
|
|
741
|
+
Private by default: signed in, a publish with no --visibility is 'me' —
|
|
742
|
+
owner-only, Phoenix-gated, not in the gallery. Pass --visibility org to share
|
|
743
|
+
with everyone at your email domain (no org to create — the domain is the whole
|
|
744
|
+
mechanism), or --visibility public to opt into the gallery with an OG card. A
|
|
745
|
+
BYO publish (no Phoenix session) still defaults to public, since me/org need a
|
|
746
|
+
Phoenix owner to gate on.
|
|
747
|
+
|
|
723
748
|
Default expiry is 30d so an accidental publish decays. Pass --expire never for
|
|
724
749
|
a permanent link. --visibility unlisted (hidden aliases: --unlisted / --private)
|
|
725
750
|
hides the page from the public gallery and agents artifacts share list; the
|
|
726
751
|
direct URL is still world-readable (unlisted, not secret) and GET sends
|
|
727
|
-
X-Robots-Tag: noindex. --
|
|
752
|
+
X-Robots-Tag: noindex. --protected (= --visibility private) is token-gated
|
|
753
|
+
read-auth: the published URL carries a secret ?k= key (Authorization: Bearer
|
|
754
|
+
is accepted too) and GET returns 404 without it — treat the whole link as a
|
|
755
|
+
secret. --visibility me requires a Phoenix session and is only
|
|
728
756
|
visible to the signed-in owner; --visibility org requires the same and is
|
|
729
757
|
visible to members of the same Phoenix organization. A pre-publish scan
|
|
730
758
|
refuses emails and credential-shaped strings unless --force is passed.
|
|
@@ -956,7 +984,7 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
956
984
|
.addOption(new Option('--scope <level>', 'visibility filter: public (default), unlisted, private, me, org, or all')
|
|
957
985
|
.choices(['public', 'unlisted', 'private', 'me', 'org', 'all'])
|
|
958
986
|
.default('public'))
|
|
959
|
-
.option('--all', "list every page including hidden unlisted/me/org shares (alias for --scope all)")
|
|
987
|
+
.option('--all', "list every page including hidden unlisted/private/me/org shares (alias for --scope all)")
|
|
960
988
|
.option('--agent <name>', 'filter to shares published by this agent/harness (case-insensitive)')
|
|
961
989
|
.option('--session <id>', 'filter to shares published from this session id')
|
|
962
990
|
// Named --label-contains, not --label: `share <file>` (the parent) already owns
|
|
@@ -988,10 +1016,11 @@ Prefer to change visibility without a browser? Use 'agents artifacts share visib
|
|
|
988
1016
|
# Everything you've published, newest first
|
|
989
1017
|
agents artifacts share list
|
|
990
1018
|
|
|
991
|
-
# Include your hidden pages (unlisted / me / org) so you can see everything
|
|
1019
|
+
# Include your hidden pages (unlisted / private / me / org) so you can see everything
|
|
992
1020
|
agents artifacts share list --all
|
|
993
1021
|
agents artifacts share list --scope me
|
|
994
1022
|
agents artifacts share list --scope unlisted
|
|
1023
|
+
agents artifacts share list --scope private
|
|
995
1024
|
|
|
996
1025
|
# Machine-readable — e.g. pull every still-public URL with jq
|
|
997
1026
|
agents artifacts share list --list-json | jq -r '.objects[].url'
|