@phnx-labs/agents-cli 1.22.53 → 1.22.55
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 +208 -0
- package/README.md +59 -9
- package/dist/bootstrap.js +55 -154
- package/dist/cli/command-registry.d.ts +5 -0
- package/dist/cli/command-registry.js +8 -1
- package/dist/commands/accounts.js +219 -173
- package/dist/commands/apply.js +6 -3
- package/dist/commands/auth-mint.d.ts +8 -0
- package/dist/commands/auth-mint.js +96 -0
- package/dist/commands/auth.js +5 -1
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cost.js +8 -2
- package/dist/commands/daemon.js +2 -2
- package/dist/commands/doctor.js +6 -1
- package/dist/commands/exec.js +10 -8
- package/dist/commands/focus.d.ts +1 -0
- package/dist/commands/focus.js +2 -2
- package/dist/commands/go.d.ts +5 -4
- package/dist/commands/go.js +7 -7
- package/dist/commands/insights.js +9 -0
- package/dist/commands/monitors.js +85 -30
- package/dist/commands/output.js +8 -2
- package/dist/commands/repo.js +18 -0
- package/dist/commands/routines.js +31 -2
- package/dist/commands/secrets.js +33 -14
- package/dist/commands/sessions.d.ts +20 -12
- package/dist/commands/sessions.js +64 -20
- package/dist/commands/setup-accounts.d.ts +8 -0
- package/dist/commands/setup-accounts.js +47 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +11 -2
- package/dist/commands/share.d.ts +79 -3
- package/dist/commands/share.js +347 -18
- package/dist/commands/ssh.d.ts +7 -0
- package/dist/commands/ssh.js +18 -2
- package/dist/commands/status.js +14 -0
- package/dist/commands/view.d.ts +11 -1
- package/dist/commands/view.js +35 -7
- package/dist/lib/account-registry.js +15 -3
- package/dist/lib/accounting/rotate.d.ts +20 -6
- package/dist/lib/accounting/rotate.js +38 -7
- package/dist/lib/accounting/usage.d.ts +68 -1
- package/dist/lib/accounting/usage.js +116 -10
- package/dist/lib/agent-spec/agents.d.ts +5 -2
- package/dist/lib/agent-spec/agents.js +25 -7
- package/dist/lib/analytics/mix-commands.js +12 -6
- package/dist/lib/auth-mint.d.ts +150 -0
- package/dist/lib/auth-mint.js +434 -0
- package/dist/lib/browser/cdp.d.ts +1 -1
- package/dist/lib/browser/cdp.js +1 -1
- package/dist/lib/browser/ffmpeg.d.ts +12 -0
- package/dist/lib/browser/ffmpeg.js +184 -0
- package/dist/lib/browser/remote-control.d.ts +9 -7
- package/dist/lib/browser/remote-control.js +9 -7
- package/dist/lib/browser/service.js +119 -25
- package/dist/lib/claude-account-token.d.ts +10 -0
- package/dist/lib/claude-account-token.js +14 -4
- package/dist/lib/config-drift.d.ts +37 -0
- package/dist/lib/config-drift.js +72 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
- package/dist/lib/daemon/auth-sync-service.js +34 -0
- package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
- package/dist/lib/daemon/browser-task-reap-service.js +26 -0
- package/dist/lib/daemon/daemon.js +87 -176
- package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
- package/dist/lib/daemon/heartbeat-service.js +26 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
- package/dist/lib/daemon/monitor-engine-service.js +15 -7
- package/dist/lib/daemon/runner.d.ts +15 -0
- package/dist/lib/daemon/runner.js +23 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
- package/dist/lib/daemon/secrets-broker-service.js +17 -32
- package/dist/lib/daemon/service.d.ts +2 -2
- package/dist/lib/daemon/service.js +1 -1
- package/dist/lib/daemon/session-state-service.d.ts +21 -0
- package/dist/lib/daemon/session-state-service.js +34 -0
- package/dist/lib/daemon/supervisor.d.ts +17 -7
- package/dist/lib/daemon/supervisor.js +87 -14
- package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
- package/dist/lib/daemon/tmux-reap-service.js +28 -0
- package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
- package/dist/lib/daemon/webhook-receiver-service.js +17 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +25 -0
- package/dist/lib/device-config.d.ts +3 -3
- package/dist/lib/device-config.js +5 -5
- package/dist/lib/devices/connect.d.ts +26 -0
- package/dist/lib/devices/connect.js +48 -1
- package/dist/lib/devices/doctor-findings.d.ts +5 -1
- package/dist/lib/devices/doctor-findings.js +19 -1
- package/dist/lib/devices/harness-inventory.js +5 -2
- package/dist/lib/exec.d.ts +28 -0
- package/dist/lib/exec.js +73 -7
- package/dist/lib/feed/feed.d.ts +1 -1
- package/dist/lib/feed/feed.js +23 -1
- package/dist/lib/feed-broadcast.js +1 -1
- package/dist/lib/fleet/apply.d.ts +11 -0
- package/dist/lib/fleet/apply.js +23 -3
- package/dist/lib/fleet/auth-sync.js +5 -3
- package/dist/lib/help.d.ts +9 -0
- package/dist/lib/help.js +29 -1
- package/dist/lib/hosts/passthrough.d.ts +1 -10
- package/dist/lib/hosts/passthrough.js +1 -13
- package/dist/lib/installations/versions.js +9 -1
- package/dist/lib/linux-userns.d.ts +58 -0
- package/dist/lib/linux-userns.js +116 -0
- package/dist/lib/memory.d.ts +26 -0
- package/dist/lib/memory.js +80 -1
- package/dist/lib/monitors/config.d.ts +11 -0
- package/dist/lib/monitors/config.js +8 -0
- package/dist/lib/monitors/engine.d.ts +5 -1
- package/dist/lib/monitors/engine.js +13 -4
- package/dist/lib/monitors/state.d.ts +37 -1
- package/dist/lib/monitors/state.js +79 -4
- package/dist/lib/permissions-registry.d.ts +2 -0
- package/dist/lib/permissions-registry.js +116 -14
- package/dist/lib/permissions.d.ts +5 -3
- package/dist/lib/permissions.js +25 -27
- package/dist/lib/profiles.d.ts +8 -7
- package/dist/lib/profiles.js +12 -0
- package/dist/lib/project-key.d.ts +9 -0
- package/dist/lib/project-key.js +11 -0
- package/dist/lib/scheduling/routines.d.ts +47 -0
- package/dist/lib/scheduling/routines.js +70 -1
- package/dist/lib/secrets/bundles.d.ts +35 -0
- package/dist/lib/secrets/bundles.js +78 -1
- package/dist/lib/secrets/push.d.ts +3 -8
- package/dist/lib/secrets/push.js +18 -14
- package/dist/lib/secrets/remote.d.ts +9 -18
- package/dist/lib/secrets/remote.js +11 -26
- package/dist/lib/secrets/reserved-sync.d.ts +65 -0
- package/dist/lib/secrets/reserved-sync.js +129 -0
- package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
- package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
- package/dist/lib/self-heal/registry.js +4 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/active.js +1 -4
- package/dist/lib/session/db.d.ts +41 -5
- package/dist/lib/session/db.js +132 -30
- package/dist/lib/session/discover.d.ts +32 -4
- package/dist/lib/session/discover.js +119 -25
- package/dist/lib/session/insights.d.ts +14 -0
- package/dist/lib/session/insights.js +25 -2
- package/dist/lib/session/linear.js +1 -1
- package/dist/lib/session/shell-programs.d.ts +17 -0
- package/dist/lib/session/shell-programs.js +21 -0
- package/dist/lib/session/state.js +2 -1
- package/dist/lib/session/stream-render.js +2 -1
- package/dist/lib/session/tool-calls.js +2 -5
- package/dist/lib/session/trajectory-html.js +2 -1
- package/dist/lib/session/trajectory.js +3 -12
- package/dist/lib/session/types.d.ts +8 -0
- package/dist/lib/share/capture.js +11 -2
- package/dist/lib/share/publish.d.ts +56 -5
- package/dist/lib/share/publish.js +126 -18
- package/dist/lib/share/worker-template.d.ts +3 -12
- package/dist/lib/share/worker-template.js +860 -59
- package/dist/lib/startup/root-command.js +2 -1
- package/dist/lib/state.d.ts +16 -0
- package/dist/lib/state.js +178 -46
- package/dist/lib/sync-status.d.ts +4 -0
- package/dist/lib/sync-status.js +3 -0
- package/dist/lib/traces/classify.js +24 -19
- package/dist/lib/usage-refresh.js +2 -1
- package/dist/lib/view-types.d.ts +7 -0
- package/package.json +9 -2
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import * as path from 'path';
|
|
18
18
|
import { isCompletedTodoStatus, SNAPSHOT_TODO_TOOLS, summarizeToolUse } from './parse.js';
|
|
19
|
+
import { isShellExecTool } from './shell-programs.js';
|
|
19
20
|
/**
|
|
20
21
|
* Detect per-session rate-limit / usage-limit signals in assistant or error
|
|
21
22
|
* text (RUSH-1523). Matches the same shapes the ext's prewarm detectBlockingPrompt
|
|
@@ -174,7 +175,7 @@ export function extractRecentDirectoriesTouched(events, cwd) {
|
|
|
174
175
|
if (['Edit', 'Write', 'edit_file', 'write_file', 'create_file', 'edit', 'write'].includes(tool)) {
|
|
175
176
|
add(args.file_path ?? args.filePath ?? args.path ?? event.path, true);
|
|
176
177
|
}
|
|
177
|
-
else if (
|
|
178
|
+
else if (isShellExecTool(tool)) {
|
|
178
179
|
add(args.cwd ?? args.Cwd ?? args.workdir ?? args.working_directory ?? cwd);
|
|
179
180
|
}
|
|
180
181
|
}
|
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { truncate } from '../format.js';
|
|
3
3
|
import { parseClaudeContent, parseCodexContent, sanitizeEvents, summarizeToolUse } from './parse.js';
|
|
4
4
|
import { linkPath, relativeToCwd } from './render.js';
|
|
5
|
+
import { isShellExecTool } from './shell-programs.js';
|
|
5
6
|
const LINE_MAX = 120;
|
|
6
7
|
function timeOf(event) {
|
|
7
8
|
const d = new Date(event.timestamp);
|
|
@@ -11,7 +12,7 @@ function timeOf(event) {
|
|
|
11
12
|
}
|
|
12
13
|
function paintTool(tool) {
|
|
13
14
|
const label = tool.padEnd(10);
|
|
14
|
-
if (tool
|
|
15
|
+
if (isShellExecTool(tool))
|
|
15
16
|
return chalk.yellow(label);
|
|
16
17
|
if (tool === 'Edit' || tool === 'Write' || tool === 'Read')
|
|
17
18
|
return chalk.cyan(label);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from 'crypto';
|
|
2
2
|
import { parse } from 'acorn';
|
|
3
3
|
import { knownSecretValuesFromEnv, redactSecrets, sanitizeForTerminal } from '../redact.js';
|
|
4
|
-
import { extractShellPrograms } from './shell-programs.js';
|
|
4
|
+
import { extractShellPrograms, isShellExecTool } from './shell-programs.js';
|
|
5
5
|
export const TOOL_INPUT_MAX_BYTES = 16 * 1024;
|
|
6
6
|
export const TOOL_SUCCESS_OUTPUT_MAX_BYTES = 1024;
|
|
7
7
|
export const TOOL_ERROR_OUTPUT_MAX_BYTES = 4 * 1024;
|
|
@@ -13,9 +13,6 @@ export const TOOL_INDEX_LIMIT_ORDINAL = Number.MAX_SAFE_INTEGER;
|
|
|
13
13
|
export const TOOL_TEXT_PROCESSING_MAX_BYTES = 64 * 1024;
|
|
14
14
|
export const TOOL_SHELL_PARSE_MAX_BYTES = 64 * 1024;
|
|
15
15
|
export const TOOL_INDEX_VERSION = 7;
|
|
16
|
-
const SHELL_TOOLS = new Set([
|
|
17
|
-
'bash', 'exec', 'execute', 'exec_command', 'run_command', 'run_shell_command', 'shell',
|
|
18
|
-
]);
|
|
19
16
|
const BASE64_BLOCK = /(?:[A-Za-z0-9+/]{256,}={0,2})/g;
|
|
20
17
|
const SECRET_FIELD = /(?:token|secret|password|authorization|cookie|api[_-]?key|private[_-]?key)$/i;
|
|
21
18
|
const KNOWN_SECRET_VALUES = knownSecretValuesFromEnv();
|
|
@@ -225,7 +222,7 @@ export function commandsFromCodexExec(source) {
|
|
|
225
222
|
return commands;
|
|
226
223
|
}
|
|
227
224
|
function commandFor(tool, args, command) {
|
|
228
|
-
if (!
|
|
225
|
+
if (!isShellExecTool(tool))
|
|
229
226
|
return undefined;
|
|
230
227
|
const direct = command
|
|
231
228
|
?? (typeof args?.command === 'string' ? args.command : undefined)
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { formatDuration, formatTokenCount } from './render.js';
|
|
16
16
|
import { escapeHtml } from './share-html.js';
|
|
17
|
+
import { isShellExecTool } from './shell-programs.js';
|
|
17
18
|
/**
|
|
18
19
|
* The honest footer prefix for a rendered trace: the redacted label only when
|
|
19
20
|
* something was actually redacted, else the "local only" disclaimer under
|
|
@@ -30,7 +31,7 @@ function toolColor(step) {
|
|
|
30
31
|
if (step.kind === 'thinking')
|
|
31
32
|
return '#3a3a55';
|
|
32
33
|
const tool = (step.tool ?? '').toLowerCase();
|
|
33
|
-
if (
|
|
34
|
+
if (isShellExecTool(tool))
|
|
34
35
|
return '#e0b341';
|
|
35
36
|
if (tool === 'read' || tool === 'grep' || tool === 'glob' || tool === 'search' || tool === 'codebase_search')
|
|
36
37
|
return '#4a9eff';
|
|
@@ -22,22 +22,13 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { redactSecrets } from '../redact.js';
|
|
24
24
|
import { computeSummaryStats } from './render.js';
|
|
25
|
-
import { extractShellPrograms } from './shell-programs.js';
|
|
25
|
+
import { extractShellPrograms, isShellExecTool } from './shell-programs.js';
|
|
26
26
|
const DEFAULT_IDLE_THRESHOLD_MS = 120_000;
|
|
27
27
|
const DEFAULT_MAX_STEPS = 5000;
|
|
28
28
|
const LABEL_MAX = 140;
|
|
29
29
|
const DETAIL_MAX = 400;
|
|
30
30
|
/** Tools that spawn an inline sub-agent inside THIS transcript (a `tool_use` row). */
|
|
31
31
|
const INLINE_TASK_TOOLS = new Set(['Task', 'Agent']);
|
|
32
|
-
/**
|
|
33
|
-
* Shell tools whose command is worth resolving to an effective program — every
|
|
34
|
-
* harness's shell-exec tool, kept in lockstep with the canonical set in
|
|
35
|
-
* `state.ts` (`['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute']`)
|
|
36
|
-
* so Codex's `exec_command`/`exec` and the rest are covered, not just Claude's `Bash`.
|
|
37
|
-
* `exec` is newer Codex (gpt-5.6-sol) whose real command the parser unwraps from a
|
|
38
|
-
* JS cell into the event's `command` before this runs.
|
|
39
|
-
*/
|
|
40
|
-
const SHELL_TOOLS = new Set(['Bash', 'exec_command', 'exec', 'run_shell_command', 'shell', 'Execute']);
|
|
41
32
|
/**
|
|
42
33
|
* Shell builtins/assignments that are rarely the POINT of a command — the action
|
|
43
34
|
* is whatever runs after them (`export X=Y; git push` → `git`, `cd dir && bun test`
|
|
@@ -141,7 +132,7 @@ function stripLeadingShellNoise(command) {
|
|
|
141
132
|
function toolLabel(tool, args, command, redact, knownSecrets) {
|
|
142
133
|
let raw;
|
|
143
134
|
const lower = tool.toLowerCase();
|
|
144
|
-
if (
|
|
135
|
+
if (isShellExecTool(tool)) {
|
|
145
136
|
const cmd = command ?? stringArg(args, 'command', 'cmd', 'script');
|
|
146
137
|
raw = cmd ? stripLeadingShellNoise(cmd) : cmd;
|
|
147
138
|
}
|
|
@@ -247,7 +238,7 @@ export function buildTrajectory(events, meta, options = {}) {
|
|
|
247
238
|
label: toolLabel(tool, e.args, e.command, redact, knownSecrets),
|
|
248
239
|
delegation: INLINE_TASK_TOOLS.has(tool) ? 'inline-task' : undefined,
|
|
249
240
|
callId: e.callId,
|
|
250
|
-
program:
|
|
241
|
+
program: isShellExecTool(tool) ? effectiveProgram(e.command ?? (typeof e.args?.command === 'string' ? e.args.command : undefined)) : undefined,
|
|
251
242
|
exitCode: typeof e.exitCode === 'number' ? e.exitCode : undefined,
|
|
252
243
|
},
|
|
253
244
|
eventIndex: i,
|
|
@@ -384,6 +384,14 @@ export interface SessionMeta {
|
|
|
384
384
|
_matchedTerms?: string[];
|
|
385
385
|
/** BM25 relevance score from the most recent content-index search */
|
|
386
386
|
_bm25Score?: number;
|
|
387
|
+
/**
|
|
388
|
+
* A short highlighted excerpt (`**term**`) around the best-matching FTS5
|
|
389
|
+
* column (label/topic/project/user content/assistant answer) for the most
|
|
390
|
+
* recent content-index search. Unlike `_matchedTerms`/`_bm25Score` this is
|
|
391
|
+
* NOT stripped from `--json` output — it's the searchable-context payload a
|
|
392
|
+
* content-search result carries, not internal ranking bookkeeping.
|
|
393
|
+
*/
|
|
394
|
+
snippet?: string;
|
|
387
395
|
}
|
|
388
396
|
/** Output format for rendering a session's content. */
|
|
389
397
|
export type ViewMode = 'summary' | 'markdown' | 'json';
|
|
@@ -36,8 +36,17 @@ export function candidateBrowsers() {
|
|
|
36
36
|
if (p && fs.existsSync(p) && !out.includes(p))
|
|
37
37
|
out.push(p);
|
|
38
38
|
};
|
|
39
|
-
// 1)
|
|
40
|
-
|
|
39
|
+
// 1) Explicit overrides are contracts, not hints. A typo must fail at the
|
|
40
|
+
// boundary instead of silently falling through to an unrelated browser.
|
|
41
|
+
for (const name of ['PUPPETEER_EXECUTABLE_PATH', 'AGENTS_SHARE_BROWSER']) {
|
|
42
|
+
const value = process.env[name]?.trim();
|
|
43
|
+
if (!value)
|
|
44
|
+
continue;
|
|
45
|
+
if (!fs.existsSync(value)) {
|
|
46
|
+
throw new Error(`${name} points to a browser that does not exist: ${value}`);
|
|
47
|
+
}
|
|
48
|
+
push(value);
|
|
49
|
+
}
|
|
41
50
|
// 2) Managed Chromium in the Playwright / Puppeteer caches — purpose-built for
|
|
42
51
|
// headless, so it's the most reliable capture host when present.
|
|
43
52
|
for (const bin of scanCaches())
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ShareConfig } from './config.js';
|
|
2
|
+
import { type ShareBackendKind } from './backend.js';
|
|
2
3
|
export type PutFn = (url: string, body: Buffer, headers: Record<string, string>) => Promise<{
|
|
3
4
|
ok: boolean;
|
|
4
5
|
status: number;
|
|
@@ -9,6 +10,8 @@ export interface PublishEndpoint {
|
|
|
9
10
|
token: string;
|
|
10
11
|
}
|
|
11
12
|
export interface PublishOptions {
|
|
13
|
+
/** Internal resolved backend; publishFile sets this after authentication. */
|
|
14
|
+
backendKind?: ShareBackendKind;
|
|
12
15
|
slug?: string;
|
|
13
16
|
/**
|
|
14
17
|
* Auto-expire window. Relative (`30d`, `12h`), absolute (`2026-08-01`), or
|
|
@@ -53,6 +56,12 @@ export interface PublishOptions {
|
|
|
53
56
|
writeToken?: string;
|
|
54
57
|
/** DI seam for tests — override `readSession()`. `null` means signed out. */
|
|
55
58
|
session?: import('../identity/client.js').PhoenixSession | null;
|
|
59
|
+
/**
|
|
60
|
+
* Override the sharer's avatar URL stamped on the object (test seam). When
|
|
61
|
+
* omitted it is derived from the signed-in email via {@link resolveShareAvatar};
|
|
62
|
+
* an empty string suppresses the stamp (initials-only bar).
|
|
63
|
+
*/
|
|
64
|
+
avatar?: string;
|
|
56
65
|
/** Force the BYO Cloudflare path even when signed in. */
|
|
57
66
|
byo?: boolean;
|
|
58
67
|
/** DI seam for tests — override the real HTTP PUT. */
|
|
@@ -83,6 +92,9 @@ export interface PublishOptions {
|
|
|
83
92
|
provenance?: ShareProvenance;
|
|
84
93
|
}
|
|
85
94
|
export type ShareVisibility = 'public' | 'unlisted' | 'me' | 'org';
|
|
95
|
+
/** The visibility levels a share may carry — the Worker's own set, used to
|
|
96
|
+
* validate `--visibility` and `share visibility <target> <level>`. */
|
|
97
|
+
export declare const SHARE_VISIBILITY_LEVELS: readonly ShareVisibility[];
|
|
86
98
|
export interface PublishResult {
|
|
87
99
|
url: string;
|
|
88
100
|
/** URL-safe object name, explicit (`--slug`) or deterministically derived. */
|
|
@@ -124,7 +136,7 @@ export interface ShareProvenance {
|
|
|
124
136
|
* provenance the CLI sets automatically, plus `expires-at` / `visibility` /
|
|
125
137
|
* `owner` which the Worker stamps itself.
|
|
126
138
|
*/
|
|
127
|
-
export declare const RESERVED_META_KEYS: readonly ["expires-at", "visibility", "owner", "agent", "session", "host", "repo", "date", "label", "label-source"];
|
|
139
|
+
export declare const RESERVED_META_KEYS: readonly ["expires-at", "published-at", "visibility", "owner", "org_domain", "agent", "session", "host", "repo", "date", "avatar", "label", "label-source", "og-title", "og-description", "og-generated", "og-source-etag"];
|
|
128
140
|
/**
|
|
129
141
|
* Auto-capture publish provenance from the exec env, git, and the local clock.
|
|
130
142
|
* Every field is present only when the environment genuinely carries it — a
|
|
@@ -139,6 +151,21 @@ export declare function resolveShareProvenance(opts?: {
|
|
|
139
151
|
dir?: string;
|
|
140
152
|
now?: Date;
|
|
141
153
|
}): ShareProvenance;
|
|
154
|
+
/**
|
|
155
|
+
* The sharer's avatar URL, stamped so the share bar can show a real profile
|
|
156
|
+
* picture instead of only the initials circle. We key a Gravatar on the SHA-256
|
|
157
|
+
* of the signed-in user's lowercased email (Gravatar resolves either MD5 or
|
|
158
|
+
* SHA-256), with `d=404` so Gravatar returns 404 for a user who has none — the
|
|
159
|
+
* bar's `<img>` onerror then falls back to the initials circle. Only the hash
|
|
160
|
+
* lands in public metadata, never the raw email. Returns '' when signed out
|
|
161
|
+
* (BYO without a Phoenix session), leaving the bar on the initials circle.
|
|
162
|
+
*
|
|
163
|
+
* `opts.session === null` means "explicitly signed out" (a test seam / BYO) and
|
|
164
|
+
* yields ''; `undefined` reads the real persisted session.
|
|
165
|
+
*/
|
|
166
|
+
export declare function resolveShareAvatar(opts?: {
|
|
167
|
+
session?: import('../identity/client.js').PhoenixSession | null;
|
|
168
|
+
}): string;
|
|
142
169
|
/**
|
|
143
170
|
* Parse repeated `--meta key=value` CLI args into a validated metadata record.
|
|
144
171
|
* Keys are lowercase `[a-z0-9-]`, up to 64 characters, and may not collide with
|
|
@@ -172,12 +199,36 @@ export declare function sanitizeLabel(text: string): string;
|
|
|
172
199
|
* The transliterations above cover what actually shows up; anything else outside
|
|
173
200
|
* latin1 is dropped, and a value that transliterates to nothing at all (a title
|
|
174
201
|
* written entirely in a non-latin script) degrades to a marker rather than an
|
|
175
|
-
* empty header.
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
202
|
+
* empty header. This value is the **latin1-safe floor** every Worker can read: a
|
|
203
|
+
* pre-Unicode Worker only ever sees `x-share-<field>`, so it MUST stay folded.
|
|
204
|
+
* Full Unicode rides ALONGSIDE it in a percent-encoded companion header
|
|
205
|
+
* (`toPercentHeaderValue` / {@link needsUnicodeCompanion}), which a new Worker
|
|
206
|
+
* opts into via `x-share-encoding: percent` and an old one ignores — so a
|
|
207
|
+
* Japanese/emoji title renders in full on an updated Worker and still folds
|
|
208
|
+
* gracefully everywhere else (PHNX-2786).
|
|
179
209
|
*/
|
|
180
210
|
export declare function toHeaderValue(text: string): string;
|
|
211
|
+
/**
|
|
212
|
+
* Whether text carries a code point above latin1 (U+00FF) — the meaningful
|
|
213
|
+
* display content `fetch`'s ByteString cannot hold, which {@link toHeaderValue}
|
|
214
|
+
* therefore transliterates or drops. This is the range worth carrying in the
|
|
215
|
+
* percent-encoded companion: an em dash, a curly quote, an emoji, or any
|
|
216
|
+
* CJK/Arabic/Hindi text. A plain accented latin1 name (`José`, é = U+00E9) is
|
|
217
|
+
* NOT lossy and needs no companion.
|
|
218
|
+
*
|
|
219
|
+
* `toHeaderValue` ALSO strips C0/C1 control characters (below U+0020, and
|
|
220
|
+
* U+007F–U+009F), which this deliberately does not flag: a raw ANSI/control
|
|
221
|
+
* sequence is not display text and must not be reconstructed into a page's
|
|
222
|
+
* rendered metadata, so it stays dropped on both the old and new Worker paths.
|
|
223
|
+
*/
|
|
224
|
+
export declare function needsUnicodeCompanion(text: string): boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Percent-encode a single-line free-text value for the `x-share-<field>-u`
|
|
227
|
+
* companion header. Whitespace is collapsed first (matching the folded value's
|
|
228
|
+
* single-line shape), then `encodeURIComponent` makes it pure-ASCII and
|
|
229
|
+
* header-safe. The Worker recovers the original with `decodeURIComponent`.
|
|
230
|
+
*/
|
|
231
|
+
export declare function toPercentHeaderValue(text: string): string;
|
|
181
232
|
/**
|
|
182
233
|
* Best-effort human title when `--label` is omitted: the HTML `<title>`, else a
|
|
183
234
|
* Markdown frontmatter `title:`, else the filename. Always returns something —
|
|
@@ -9,6 +9,8 @@ import { readFileSync } from 'node:fs';
|
|
|
9
9
|
import { basename } from 'node:path';
|
|
10
10
|
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import { hostname as osHostname } from 'node:os';
|
|
12
|
+
import { createHash } from 'node:crypto';
|
|
13
|
+
import { readSession } from '../identity/client.js';
|
|
12
14
|
import { readShareConfig } from './config.js';
|
|
13
15
|
import { resolveGitHubUsername } from '../git.js';
|
|
14
16
|
import { resolveShareBackend, sanitizeShareNamespace } from './backend.js';
|
|
@@ -16,6 +18,9 @@ import { captureCover, OG_WIDTH, OG_HEIGHT, OG_SCALE } from './capture.js';
|
|
|
16
18
|
import { deriveMeta, injectOgMeta } from './og.js';
|
|
17
19
|
import { injectAnalyticsBeacon } from './analytics.js';
|
|
18
20
|
import { prepareShareHtml } from './html.js';
|
|
21
|
+
/** The visibility levels a share may carry — the Worker's own set, used to
|
|
22
|
+
* validate `--visibility` and `share visibility <target> <level>`. */
|
|
23
|
+
export const SHARE_VISIBILITY_LEVELS = ['public', 'unlisted', 'me', 'org'];
|
|
19
24
|
/**
|
|
20
25
|
* `--unlisted` / `{ unlisted: true }` map to `unlisted`; `--visibility me|org`
|
|
21
26
|
* passes through; otherwise `visibility` (default public).
|
|
@@ -35,15 +40,22 @@ export function resolveShareVisibility(opts = {}) {
|
|
|
35
40
|
*/
|
|
36
41
|
export const RESERVED_META_KEYS = [
|
|
37
42
|
'expires-at',
|
|
43
|
+
'published-at',
|
|
38
44
|
'visibility',
|
|
39
45
|
'owner',
|
|
46
|
+
'org_domain',
|
|
40
47
|
'agent',
|
|
41
48
|
'session',
|
|
42
49
|
'host',
|
|
43
50
|
'repo',
|
|
44
51
|
'date',
|
|
52
|
+
'avatar',
|
|
45
53
|
'label',
|
|
46
54
|
'label-source',
|
|
55
|
+
'og-title',
|
|
56
|
+
'og-description',
|
|
57
|
+
'og-generated',
|
|
58
|
+
'og-source-etag',
|
|
47
59
|
];
|
|
48
60
|
const META_KEY_RE = /^[a-z0-9-]{1,64}$/;
|
|
49
61
|
/**
|
|
@@ -64,6 +76,26 @@ export function resolveShareProvenance(opts = {}) {
|
|
|
64
76
|
date: (opts.now ?? new Date()).toISOString().slice(0, 10),
|
|
65
77
|
};
|
|
66
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* The sharer's avatar URL, stamped so the share bar can show a real profile
|
|
81
|
+
* picture instead of only the initials circle. We key a Gravatar on the SHA-256
|
|
82
|
+
* of the signed-in user's lowercased email (Gravatar resolves either MD5 or
|
|
83
|
+
* SHA-256), with `d=404` so Gravatar returns 404 for a user who has none — the
|
|
84
|
+
* bar's `<img>` onerror then falls back to the initials circle. Only the hash
|
|
85
|
+
* lands in public metadata, never the raw email. Returns '' when signed out
|
|
86
|
+
* (BYO without a Phoenix session), leaving the bar on the initials circle.
|
|
87
|
+
*
|
|
88
|
+
* `opts.session === null` means "explicitly signed out" (a test seam / BYO) and
|
|
89
|
+
* yields ''; `undefined` reads the real persisted session.
|
|
90
|
+
*/
|
|
91
|
+
export function resolveShareAvatar(opts = {}) {
|
|
92
|
+
const session = opts.session !== undefined ? opts.session : readSession();
|
|
93
|
+
const email = session?.email?.trim().toLowerCase();
|
|
94
|
+
if (!email)
|
|
95
|
+
return '';
|
|
96
|
+
const hash = createHash('sha256').update(email).digest('hex');
|
|
97
|
+
return `https://www.gravatar.com/avatar/${hash}?d=404&s=52`;
|
|
98
|
+
}
|
|
67
99
|
/**
|
|
68
100
|
* Parse repeated `--meta key=value` CLI args into a validated metadata record.
|
|
69
101
|
* Keys are lowercase `[a-z0-9-]`, up to 64 characters, and may not collide with
|
|
@@ -80,12 +112,12 @@ export function parseMetaEntries(pairs) {
|
|
|
80
112
|
}
|
|
81
113
|
const key = pair.slice(0, eq).trim();
|
|
82
114
|
const value = pair.slice(eq + 1);
|
|
83
|
-
if (!META_KEY_RE.test(key)) {
|
|
84
|
-
throw new Error(`Bad --meta key '${key}'. Keys are lowercase letters, digits, and hyphens, up to 64 characters.`);
|
|
85
|
-
}
|
|
86
115
|
if (RESERVED_META_KEYS.includes(key)) {
|
|
87
116
|
throw new Error(`--meta ${key}=… is reserved (Worker-stamped, or set automatically from your session/git) — pass a different key.`);
|
|
88
117
|
}
|
|
118
|
+
if (!META_KEY_RE.test(key)) {
|
|
119
|
+
throw new Error(`Bad --meta key '${key}'. Keys are lowercase letters, digits, and hyphens, up to 64 characters.`);
|
|
120
|
+
}
|
|
89
121
|
meta[key] = value;
|
|
90
122
|
}
|
|
91
123
|
return meta;
|
|
@@ -142,10 +174,13 @@ const HEADER_TRANSLITERATIONS = [
|
|
|
142
174
|
* The transliterations above cover what actually shows up; anything else outside
|
|
143
175
|
* latin1 is dropped, and a value that transliterates to nothing at all (a title
|
|
144
176
|
* written entirely in a non-latin script) degrades to a marker rather than an
|
|
145
|
-
* empty header.
|
|
146
|
-
*
|
|
147
|
-
*
|
|
148
|
-
*
|
|
177
|
+
* empty header. This value is the **latin1-safe floor** every Worker can read: a
|
|
178
|
+
* pre-Unicode Worker only ever sees `x-share-<field>`, so it MUST stay folded.
|
|
179
|
+
* Full Unicode rides ALONGSIDE it in a percent-encoded companion header
|
|
180
|
+
* (`toPercentHeaderValue` / {@link needsUnicodeCompanion}), which a new Worker
|
|
181
|
+
* opts into via `x-share-encoding: percent` and an old one ignores — so a
|
|
182
|
+
* Japanese/emoji title renders in full on an updated Worker and still folds
|
|
183
|
+
* gracefully everywhere else (PHNX-2786).
|
|
149
184
|
*/
|
|
150
185
|
export function toHeaderValue(text) {
|
|
151
186
|
let safe = text;
|
|
@@ -159,6 +194,31 @@ export function toHeaderValue(text) {
|
|
|
159
194
|
return safe;
|
|
160
195
|
return text.trim() ? '(unnamed)' : '';
|
|
161
196
|
}
|
|
197
|
+
/**
|
|
198
|
+
* Whether text carries a code point above latin1 (U+00FF) — the meaningful
|
|
199
|
+
* display content `fetch`'s ByteString cannot hold, which {@link toHeaderValue}
|
|
200
|
+
* therefore transliterates or drops. This is the range worth carrying in the
|
|
201
|
+
* percent-encoded companion: an em dash, a curly quote, an emoji, or any
|
|
202
|
+
* CJK/Arabic/Hindi text. A plain accented latin1 name (`José`, é = U+00E9) is
|
|
203
|
+
* NOT lossy and needs no companion.
|
|
204
|
+
*
|
|
205
|
+
* `toHeaderValue` ALSO strips C0/C1 control characters (below U+0020, and
|
|
206
|
+
* U+007F–U+009F), which this deliberately does not flag: a raw ANSI/control
|
|
207
|
+
* sequence is not display text and must not be reconstructed into a page's
|
|
208
|
+
* rendered metadata, so it stays dropped on both the old and new Worker paths.
|
|
209
|
+
*/
|
|
210
|
+
export function needsUnicodeCompanion(text) {
|
|
211
|
+
return /[^\u0000-\u00ff]/.test(text);
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Percent-encode a single-line free-text value for the `x-share-<field>-u`
|
|
215
|
+
* companion header. Whitespace is collapsed first (matching the folded value's
|
|
216
|
+
* single-line shape), then `encodeURIComponent` makes it pure-ASCII and
|
|
217
|
+
* header-safe. The Worker recovers the original with `decodeURIComponent`.
|
|
218
|
+
*/
|
|
219
|
+
export function toPercentHeaderValue(text) {
|
|
220
|
+
return encodeURIComponent(text.replace(/\s+/g, ' ').trim());
|
|
221
|
+
}
|
|
162
222
|
/**
|
|
163
223
|
* Best-effort human title when `--label` is omitted: the HTML `<title>`, else a
|
|
164
224
|
* Markdown frontmatter `title:`, else the filename. Always returns something —
|
|
@@ -450,6 +510,7 @@ export async function publishFile(filePath, opts = {}) {
|
|
|
450
510
|
...opts,
|
|
451
511
|
githubUser: username,
|
|
452
512
|
analyticsToken,
|
|
513
|
+
backendKind: backend.kind,
|
|
453
514
|
});
|
|
454
515
|
}
|
|
455
516
|
export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
@@ -462,6 +523,7 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
462
523
|
const unlisted = visibility === 'unlisted';
|
|
463
524
|
const pageUrl = `${endpoint.baseUrl.replace(/\/+$/, '')}/${key}`;
|
|
464
525
|
const provenance = opts.provenance ?? resolveShareProvenance();
|
|
526
|
+
const avatarUrl = opts.avatar ?? resolveShareAvatar({ session: opts.session });
|
|
465
527
|
const meta = opts.meta ?? {};
|
|
466
528
|
const put = opts.uploader ??
|
|
467
529
|
(async (u, b, h) => {
|
|
@@ -479,6 +541,19 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
479
541
|
// is only reached when --label is omitted.
|
|
480
542
|
const label = explicitLabel ? sanitizeLabel(explicitLabel) : deriveLabel(filePath, body);
|
|
481
543
|
const labelSource = explicitLabel ? 'explicit' : 'derived';
|
|
544
|
+
const ogMeta = isHtml ? deriveMeta(body.toString('utf8')) : undefined;
|
|
545
|
+
// The managed Worker owns deterministic OG generation. Point crawlers at the
|
|
546
|
+
// lazy sibling route without invoking a browser on the publishing machine.
|
|
547
|
+
if (isHtml && opts.cover !== false && opts.backendKind === 'managed' && ogMeta) {
|
|
548
|
+
coverUrl = `${pageUrl}.png`;
|
|
549
|
+
body = Buffer.from(injectOgMeta(body.toString('utf8'), {
|
|
550
|
+
...ogMeta,
|
|
551
|
+
imageUrl: coverUrl,
|
|
552
|
+
pageUrl,
|
|
553
|
+
imageWidth: OG_WIDTH,
|
|
554
|
+
imageHeight: OG_HEIGHT,
|
|
555
|
+
}), 'utf8');
|
|
556
|
+
}
|
|
482
557
|
// Pre-publish scan (RUSH-2443/RUSH-2683): refuse emails / credential-shaped
|
|
483
558
|
// strings unless --force. Runs on the raw file body AND on every piece of
|
|
484
559
|
// free-text metadata that lands in public customMetadata — --label (explicit
|
|
@@ -501,6 +576,10 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
501
576
|
// Validate the FULL customMetadata payload before any network call — fail
|
|
502
577
|
// fast, not mid-upload.
|
|
503
578
|
const metadataPreview = { ...meta, label, 'label-source': labelSource };
|
|
579
|
+
if (ogMeta && opts.backendKind === 'managed') {
|
|
580
|
+
metadataPreview['og-title'] = ogMeta.title;
|
|
581
|
+
metadataPreview['og-description'] = ogMeta.description;
|
|
582
|
+
}
|
|
504
583
|
if (provenance.agent)
|
|
505
584
|
metadataPreview.agent = provenance.agent;
|
|
506
585
|
if (provenance.session)
|
|
@@ -511,35 +590,64 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
511
590
|
metadataPreview.repo = provenance.repo;
|
|
512
591
|
if (provenance.date)
|
|
513
592
|
metadataPreview.date = provenance.date;
|
|
593
|
+
if (avatarUrl)
|
|
594
|
+
metadataPreview.avatar = avatarUrl;
|
|
514
595
|
assertMetadataSize(metadataPreview);
|
|
515
596
|
const authHeaders = (contentType) => {
|
|
516
597
|
const h = { authorization: `Bearer ${endpoint.token}`, 'content-type': contentType };
|
|
517
598
|
if (expiresAt)
|
|
518
599
|
h['x-share-expires-at'] = expiresAt;
|
|
519
600
|
h['x-share-visibility'] = visibility;
|
|
520
|
-
//
|
|
521
|
-
//
|
|
522
|
-
//
|
|
601
|
+
// Two headers per free-text field, backward-compatible by construction
|
|
602
|
+
// (PHNX-2786): `x-share-<field>` always carries the latin1-safe folded value
|
|
603
|
+
// an already-deployed Worker reads verbatim, and — only when the fold is lossy
|
|
604
|
+
// (a curly quote, em dash, emoji, CJK/Arabic/Hindi) — a percent-encoded
|
|
605
|
+
// `x-share-<field>-u` companion carries the full Unicode. A new Worker opts
|
|
606
|
+
// into the companions via `x-share-encoding: percent`; an old one ignores the
|
|
607
|
+
// unknown headers and keeps folding gracefully. The floor also keeps the
|
|
608
|
+
// ByteString crash fixed: a non-latin1 code point never reaches a raw header.
|
|
609
|
+
let unicodeCompanion = false;
|
|
610
|
+
const setText = (name, value) => {
|
|
611
|
+
h[name] = toHeaderValue(value);
|
|
612
|
+
if (needsUnicodeCompanion(value)) {
|
|
613
|
+
h[`${name}-u`] = toPercentHeaderValue(value);
|
|
614
|
+
unicodeCompanion = true;
|
|
615
|
+
}
|
|
616
|
+
};
|
|
523
617
|
if (provenance.agent)
|
|
524
|
-
|
|
618
|
+
setText('x-share-agent', provenance.agent);
|
|
525
619
|
if (provenance.session)
|
|
526
|
-
|
|
620
|
+
setText('x-share-session', provenance.session);
|
|
527
621
|
if (provenance.host)
|
|
528
|
-
|
|
622
|
+
setText('x-share-host', provenance.host);
|
|
529
623
|
if (provenance.repo)
|
|
530
|
-
|
|
624
|
+
setText('x-share-repo', provenance.repo);
|
|
531
625
|
if (provenance.date)
|
|
532
|
-
|
|
533
|
-
|
|
626
|
+
setText('x-share-date', provenance.date);
|
|
627
|
+
if (avatarUrl)
|
|
628
|
+
setText('x-share-avatar', avatarUrl);
|
|
629
|
+
setText('x-share-label', label);
|
|
534
630
|
h['x-share-label-source'] = labelSource;
|
|
631
|
+
if (ogMeta && opts.backendKind === 'managed') {
|
|
632
|
+
setText('x-share-og-title', ogMeta.title);
|
|
633
|
+
setText('x-share-og-description', ogMeta.description);
|
|
634
|
+
}
|
|
535
635
|
// Per VALUE, before JSON.stringify — folding the serialized form would rewrite
|
|
536
636
|
// a curly quote inside a value into a bare `"`, which is structural in JSON and
|
|
537
637
|
// makes the Worker's JSON.parse throw. It swallows that error, so every --meta
|
|
538
|
-
// key would silently vanish on a 200.
|
|
638
|
+
// key would silently vanish on a 200. The companion carries the whole raw meta
|
|
639
|
+
// object percent-encoded once, so a new Worker recovers full-Unicode keys AND
|
|
640
|
+
// values in one JSON.parse rather than per-field.
|
|
539
641
|
if (Object.keys(meta).length > 0) {
|
|
540
642
|
const headerMeta = Object.fromEntries(Object.entries(meta).map(([k, v]) => [toHeaderValue(k), toHeaderValue(v)]));
|
|
541
643
|
h['x-share-meta'] = JSON.stringify(headerMeta);
|
|
644
|
+
if (Object.entries(meta).some(([k, v]) => needsUnicodeCompanion(k) || needsUnicodeCompanion(v))) {
|
|
645
|
+
h['x-share-meta-u'] = encodeURIComponent(JSON.stringify(meta));
|
|
646
|
+
unicodeCompanion = true;
|
|
647
|
+
}
|
|
542
648
|
}
|
|
649
|
+
if (unicodeCompanion)
|
|
650
|
+
h['x-share-encoding'] = 'percent';
|
|
543
651
|
if (opts.noRevision)
|
|
544
652
|
h['x-share-no-revision'] = '1';
|
|
545
653
|
return h;
|
|
@@ -551,7 +659,7 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
551
659
|
// Cover: screenshot the page's hero → upload <slug>.png → inject og:image meta.
|
|
552
660
|
// Unlisted pages still get a cover (the direct URL is the capability), but the
|
|
553
661
|
// cover inherits visibility=unlisted so it is also omitted from the gallery.
|
|
554
|
-
if (isHtml && opts.cover !== false) {
|
|
662
|
+
if (isHtml && opts.cover !== false && opts.backendKind !== 'managed') {
|
|
555
663
|
const res = await attachOgCover(filePath, body, {
|
|
556
664
|
pngUrl: `${pageUrl}.png`,
|
|
557
665
|
pageUrl,
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Render the Worker source. Pure — the R2 binding + token are wired at deploy time.
|
|
3
|
-
*
|
|
4
|
-
* The literal below still spells the CLI `agents share` in its provenance comment,
|
|
5
|
-
* its root response, and its gallery title, even though the command is now
|
|
6
|
-
* `agents artifacts share` (RUSH-2580). That is deliberate: `hashWorkerScript` of
|
|
7
|
-
* this exact text is what `shareTemplateStatus` compares a provisioned endpoint's
|
|
8
|
-
* recorded `templateHash` against, so editing ANY byte here marks every already-
|
|
9
|
-
* deployed endpoint `outdated` — which makes `agents artifacts share list` refuse
|
|
10
|
-
* until its owner re-runs `agents artifacts share update`. Cosmetic renames are not
|
|
11
|
-
* worth that; change this text only alongside a real Worker behavior change.
|
|
12
|
-
*/
|
|
1
|
+
/** Bundle the Worker and its renderer into one uploadable ES module. */
|
|
13
2
|
export declare function renderWorkerScript(): string;
|
|
3
|
+
/** Unbundled Worker source. Kept separate so esbuild can resolve npm modules. */
|
|
4
|
+
export declare function renderWorkerSource(): string;
|