@phnx-labs/agents-cli 1.20.72 → 1.20.73
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 +299 -0
- package/README.md +40 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/activity.d.ts +12 -0
- package/dist/commands/activity.js +68 -0
- package/dist/commands/apply.d.ts +13 -0
- package/dist/commands/apply.js +28 -3
- package/dist/commands/browser.js +1 -1
- package/dist/commands/cloud.js +21 -11
- package/dist/commands/doctor.js +8 -4
- package/dist/commands/events.d.ts +11 -10
- package/dist/commands/events.js +30 -21
- package/dist/commands/exec.d.ts +34 -0
- package/dist/commands/exec.js +337 -39
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +130 -29
- package/dist/commands/hq.d.ts +2 -0
- package/dist/commands/hq.js +58 -0
- package/dist/commands/lease.d.ts +19 -0
- package/dist/commands/lease.js +138 -11
- package/dist/commands/login.d.ts +2 -0
- package/dist/commands/login.js +123 -0
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mine.d.ts +27 -0
- package/dist/commands/mine.js +207 -0
- package/dist/commands/monitors.js +79 -49
- package/dist/commands/plugins.js +1 -0
- package/dist/commands/profiles.js +158 -0
- package/dist/commands/repo.js +2 -60
- package/dist/commands/resources.d.ts +5 -0
- package/dist/commands/resources.js +95 -0
- package/dist/commands/routines.d.ts +2 -0
- package/dist/commands/routines.js +139 -37
- package/dist/commands/secrets.js +234 -47
- package/dist/commands/send.d.ts +15 -0
- package/dist/commands/send.js +62 -0
- package/dist/commands/sessions-tail.d.ts +1 -0
- package/dist/commands/sessions-tail.js +20 -9
- package/dist/commands/sessions.js +18 -3
- package/dist/commands/setup-mine.d.ts +18 -0
- package/dist/commands/setup-mine.js +106 -0
- package/dist/commands/setup-share.js +2 -2
- package/dist/commands/setup.js +3 -1
- package/dist/commands/share.d.ts +8 -1
- package/dist/commands/share.js +122 -37
- package/dist/commands/ssh.d.ts +9 -0
- package/dist/commands/ssh.js +196 -26
- package/dist/commands/sync.js +1 -1
- package/dist/commands/teams.js +11 -2
- package/dist/index.js +79 -10
- package/dist/lib/activity.d.ts +124 -0
- package/dist/lib/activity.js +542 -0
- package/dist/lib/agents.js +16 -5
- package/dist/lib/artifact-actions.d.ts +1 -1
- package/dist/lib/artifact-actions.js +1 -1
- package/dist/lib/ask-classifier.d.ts +2 -1
- package/dist/lib/ask-classifier.js +3 -3
- package/dist/lib/auth-health.d.ts +32 -4
- package/dist/lib/auth-health.js +40 -1
- package/dist/lib/auto-pull.js +8 -1
- package/dist/lib/brand.d.ts +40 -0
- package/dist/lib/brand.js +122 -0
- package/dist/lib/browser/drivers/ssh.js +4 -7
- package/dist/lib/browser/service.js +6 -8
- package/dist/lib/channels/providers/index.d.ts +2 -0
- package/dist/lib/channels/providers/index.js +20 -0
- package/dist/lib/channels/providers/mailbox.d.ts +2 -0
- package/dist/lib/channels/providers/mailbox.js +26 -0
- package/dist/lib/channels/providers/openclaw-telegram.d.ts +2 -0
- package/dist/lib/channels/providers/openclaw-telegram.js +34 -0
- package/dist/lib/channels/providers/rush.d.ts +6 -0
- package/dist/lib/channels/providers/rush.js +55 -0
- package/dist/lib/channels/registry.d.ts +42 -0
- package/dist/lib/channels/registry.js +20 -0
- package/dist/lib/channels/resolve.d.ts +11 -0
- package/dist/lib/channels/resolve.js +12 -0
- package/dist/lib/cli-resources.d.ts +18 -0
- package/dist/lib/cli-resources.js +53 -4
- package/dist/lib/cloud/codex.js +6 -3
- package/dist/lib/cloud/factory.d.ts +1 -0
- package/dist/lib/cloud/factory.js +10 -4
- package/dist/lib/cloud/rush.d.ts +3 -1
- package/dist/lib/cloud/rush.js +6 -1
- package/dist/lib/cloud/types.d.ts +2 -0
- package/dist/lib/codex-home.d.ts +35 -0
- package/dist/lib/codex-home.js +136 -0
- package/dist/lib/crabbox/cli.d.ts +44 -0
- package/dist/lib/crabbox/cli.js +120 -20
- package/dist/lib/crabbox/lease.d.ts +52 -1
- package/dist/lib/crabbox/lease.js +117 -16
- package/dist/lib/crabbox/progress.d.ts +31 -0
- package/dist/lib/crabbox/progress.js +76 -0
- package/dist/lib/crabbox/runtimes.d.ts +4 -0
- package/dist/lib/crabbox/runtimes.js +33 -8
- package/dist/lib/crabbox/setup-copy.d.ts +87 -0
- package/dist/lib/crabbox/setup-copy.js +127 -0
- package/dist/lib/daemon.d.ts +5 -5
- package/dist/lib/daemon.js +9 -9
- package/dist/lib/drive-sync.js +6 -3
- package/dist/lib/event-stream.d.ts +36 -0
- package/dist/lib/event-stream.js +68 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/exec.d.ts +11 -0
- package/dist/lib/exec.js +23 -2
- package/dist/lib/feed-policy.d.ts +1 -1
- package/dist/lib/feed-policy.js +14 -9
- package/dist/lib/feed-ranking.d.ts +32 -0
- package/dist/lib/feed-ranking.js +224 -0
- package/dist/lib/feed.d.ts +35 -2
- package/dist/lib/feed.js +66 -3
- package/dist/lib/fleet/auth-sync.d.ts +53 -0
- package/dist/lib/fleet/auth-sync.js +92 -0
- package/dist/lib/fleet/remote-login.d.ts +187 -0
- package/dist/lib/fleet/remote-login.js +556 -0
- package/dist/lib/format.d.ts +30 -3
- package/dist/lib/format.js +34 -5
- package/dist/lib/fs-atomic.d.ts +7 -2
- package/dist/lib/fs-atomic.js +8 -12
- package/dist/lib/git.d.ts +16 -0
- package/dist/lib/git.js +79 -2
- package/dist/lib/heal.js +11 -3
- package/dist/lib/hosts/progress.d.ts +28 -10
- package/dist/lib/hosts/progress.js +79 -22
- package/dist/lib/hosts/remote-cmd.js +4 -0
- package/dist/lib/hq/floor.d.ts +87 -0
- package/dist/lib/hq/floor.js +226 -0
- package/dist/lib/menubar/install-menubar.js +14 -20
- package/dist/lib/notify.d.ts +1 -0
- package/dist/lib/notify.js +3 -3
- package/dist/lib/open-url.d.ts +2 -0
- package/dist/lib/open-url.js +19 -0
- package/dist/lib/openclaw-keychain.d.ts +56 -0
- package/dist/lib/openclaw-keychain.js +236 -0
- package/dist/lib/overdue.js +10 -0
- package/dist/lib/permissions.d.ts +44 -1
- package/dist/lib/permissions.js +284 -7
- package/dist/lib/project-launch.d.ts +6 -12
- package/dist/lib/project-launch.js +13 -228
- package/dist/lib/project-resources.d.ts +7 -0
- package/dist/lib/project-resources.js +291 -0
- package/dist/lib/pty-client.d.ts +27 -0
- package/dist/lib/pty-client.js +136 -10
- package/dist/lib/refresh.js +14 -10
- package/dist/lib/resource-profiles.d.ts +26 -0
- package/dist/lib/resource-profiles.js +157 -0
- package/dist/lib/resources/permissions.js +7 -1
- package/dist/lib/resources/types.d.ts +1 -1
- package/dist/lib/resources.d.ts +1 -1
- package/dist/lib/resources.js +32 -3
- package/dist/lib/routines.d.ts +11 -0
- package/dist/lib/routines.js +56 -15
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +66 -5
- package/dist/lib/secrets/bundles.d.ts +16 -3
- package/dist/lib/secrets/bundles.js +206 -37
- package/dist/lib/secrets/icloud-import.d.ts +2 -2
- package/dist/lib/secrets/icloud-import.js +9 -6
- package/dist/lib/secrets/mcp.js +1 -1
- package/dist/lib/secrets/vault-age-helper.d.ts +1 -0
- package/dist/lib/secrets/vault-age-helper.js +34 -0
- package/dist/lib/secrets/vault.d.ts +49 -0
- package/dist/lib/secrets/vault.js +397 -0
- package/dist/lib/self-heal/checks/path.js +3 -1
- package/dist/lib/self-heal/checks/shadowing.js +10 -2
- package/dist/lib/self-heal/checks/shims.js +19 -11
- package/dist/lib/session/cloud.d.ts +2 -2
- package/dist/lib/session/cloud.js +2 -2
- package/dist/lib/session/db.d.ts +4 -0
- package/dist/lib/session/db.js +44 -7
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +114 -3
- package/dist/lib/session/stream-render.d.ts +3 -0
- package/dist/lib/session/stream-render.js +130 -0
- package/dist/lib/session/types.d.ts +6 -0
- package/dist/lib/share/analytics.d.ts +13 -0
- package/dist/lib/share/analytics.js +45 -0
- package/dist/lib/share/config.d.ts +19 -5
- package/dist/lib/share/config.js +44 -8
- package/dist/lib/share/provision.d.ts +58 -6
- package/dist/lib/share/provision.js +97 -22
- package/dist/lib/share/publish.d.ts +36 -13
- package/dist/lib/share/publish.js +55 -8
- package/dist/lib/share/worker-template.js +83 -17
- package/dist/lib/shims.d.ts +9 -0
- package/dist/lib/shims.js +91 -13
- package/dist/lib/ssh-exec.d.ts +14 -0
- package/dist/lib/ssh-exec.js +57 -0
- package/dist/lib/staleness/detectors/hooks.js +25 -1
- package/dist/lib/staleness/detectors/permissions.js +66 -0
- package/dist/lib/staleness/detectors/workflows.js +20 -0
- package/dist/lib/staleness/writers/hooks.js +58 -4
- package/dist/lib/staleness/writers/permissions.js +2 -2
- package/dist/lib/staleness/writers/skills.js +1 -1
- package/dist/lib/staleness/writers/sources.d.ts +10 -1
- package/dist/lib/staleness/writers/sources.js +68 -1
- package/dist/lib/star-nudge.d.ts +45 -0
- package/dist/lib/star-nudge.js +91 -0
- package/dist/lib/startup/command-registry.d.ts +6 -1
- package/dist/lib/startup/command-registry.js +17 -3
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +3 -0
- package/dist/lib/subagents-registry.d.ts +2 -0
- package/dist/lib/subagents-registry.js +3 -0
- package/dist/lib/teams/parsers.js +214 -6
- package/dist/lib/teams/supervisor.d.ts +7 -0
- package/dist/lib/teams/supervisor.js +2 -1
- package/dist/lib/template.d.ts +1 -1
- package/dist/lib/template.js +1 -1
- package/dist/lib/triggers/webhook.js +36 -3
- package/dist/lib/types.d.ts +73 -0
- package/dist/lib/version.d.ts +40 -0
- package/dist/lib/version.js +94 -16
- package/dist/lib/versions.d.ts +11 -0
- package/dist/lib/versions.js +208 -76
- package/dist/lib/workflows.d.ts +2 -0
- package/dist/lib/workflows.js +88 -0
- package/package.json +2 -1
- package/dist/commands/daemon.d.ts +0 -10
- package/dist/commands/daemon.js +0 -121
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// Cloudflare provisioning for `agents share setup` — plain `fetch` against the CF
|
|
2
|
-
// REST API (the repo has no CF wrapper). Creates the R2 bucket,
|
|
3
|
-
// (with an R2 binding
|
|
4
|
-
// `*.workers.dev` subdomain, and — when the token owns the zone —
|
|
2
|
+
// REST API (the repo has no CF wrapper). Creates the R2 bucket, configures its
|
|
3
|
+
// lifecycle, uploads the Worker (with an R2 binding), sets the WRITE_TOKEN secret,
|
|
4
|
+
// enables the free `*.workers.dev` subdomain, and — when the token owns the zone —
|
|
5
|
+
// maps a custom domain.
|
|
5
6
|
const CF_API = 'https://api.cloudflare.com/client/v4';
|
|
7
|
+
export const SHARE_LIFECYCLE_RULE_ID = 'agents-share-expire-objects';
|
|
8
|
+
export const SHARE_LIFECYCLE_RETENTION_DAYS = 366;
|
|
9
|
+
const SECONDS_PER_DAY = 86400;
|
|
6
10
|
async function cf(apiToken, method, pathname, body, form) {
|
|
7
11
|
const headers = { authorization: `Bearer ${apiToken}` };
|
|
8
12
|
let payload;
|
|
@@ -22,66 +26,137 @@ async function cf(apiToken, method, pathname, body, form) {
|
|
|
22
26
|
}
|
|
23
27
|
return json.result;
|
|
24
28
|
}
|
|
29
|
+
const defaultCloudflareRequester = (request) => cf(request.apiToken, request.method, request.pathname, request.body, request.form);
|
|
25
30
|
/** True if the CF error looks like "the thing already exists" (idempotent create). */
|
|
26
31
|
function isAlreadyExists(e) {
|
|
27
32
|
return /already exists|duplicate|10004|10014/i.test(String(e));
|
|
28
33
|
}
|
|
29
34
|
/** Create the R2 bucket (idempotent). */
|
|
30
|
-
export async function createBucket(apiToken, accountId, name) {
|
|
35
|
+
export async function createBucket(apiToken, accountId, name, opts = {}) {
|
|
36
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
31
37
|
try {
|
|
32
|
-
await
|
|
38
|
+
await request({
|
|
39
|
+
apiToken,
|
|
40
|
+
method: 'POST',
|
|
41
|
+
pathname: `/accounts/${accountId}/r2/buckets`,
|
|
42
|
+
body: { name },
|
|
43
|
+
});
|
|
33
44
|
}
|
|
34
45
|
catch (e) {
|
|
35
46
|
if (!isAlreadyExists(e))
|
|
36
47
|
throw e;
|
|
37
48
|
}
|
|
38
49
|
}
|
|
39
|
-
|
|
40
|
-
|
|
50
|
+
export function buildShareLifecycleRule(days = SHARE_LIFECYCLE_RETENTION_DAYS) {
|
|
51
|
+
return {
|
|
52
|
+
id: SHARE_LIFECYCLE_RULE_ID,
|
|
53
|
+
enabled: true,
|
|
54
|
+
conditions: { prefix: '' },
|
|
55
|
+
deleteObjectsTransition: {
|
|
56
|
+
condition: { type: 'Age', maxAge: days * SECONDS_PER_DAY },
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function mergeShareLifecycleRule(existing = [], rule = buildShareLifecycleRule()) {
|
|
61
|
+
return [...existing.filter((r) => r.id !== SHARE_LIFECYCLE_RULE_ID), rule];
|
|
62
|
+
}
|
|
63
|
+
/** Ensure the share bucket self-cleans old objects. Exact per-link expiry is enforced by the Worker. */
|
|
64
|
+
export async function configureBucketLifecycle(apiToken, accountId, bucketName, opts = {}) {
|
|
65
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
66
|
+
const lifecycle = await request({
|
|
67
|
+
apiToken,
|
|
68
|
+
method: 'GET',
|
|
69
|
+
pathname: `/accounts/${accountId}/r2/buckets/${bucketName}/lifecycle`,
|
|
70
|
+
});
|
|
71
|
+
await request({
|
|
72
|
+
apiToken,
|
|
73
|
+
method: 'PUT',
|
|
74
|
+
pathname: `/accounts/${accountId}/r2/buckets/${bucketName}/lifecycle`,
|
|
75
|
+
body: { rules: mergeShareLifecycleRule(lifecycle.rules ?? []) },
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/** Upload the module Worker with an R2 binding (`BUCKET`). Secrets are set via the Workers Secrets API. */
|
|
79
|
+
export async function deployWorker(apiToken, accountId, workerName, script, bucketName, opts = {}) {
|
|
80
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
41
81
|
const metadata = {
|
|
42
82
|
main_module: 'worker.js',
|
|
43
83
|
compatibility_date: '2024-11-06',
|
|
44
84
|
bindings: [
|
|
45
85
|
{ type: 'r2_bucket', name: 'BUCKET', bucket_name: bucketName },
|
|
46
|
-
{ type: 'secret_text', name: 'WRITE_TOKEN', text: writeToken },
|
|
47
86
|
],
|
|
48
87
|
};
|
|
49
88
|
const form = new FormData();
|
|
50
89
|
form.set('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
51
90
|
form.set('worker.js', new Blob([script], { type: 'application/javascript+module' }), 'worker.js');
|
|
52
|
-
await
|
|
91
|
+
await request({
|
|
92
|
+
apiToken,
|
|
93
|
+
method: 'PUT',
|
|
94
|
+
pathname: `/accounts/${accountId}/workers/scripts/${workerName}`,
|
|
95
|
+
form,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** Add/update the WRITE_TOKEN binding using Cloudflare's Workers Secrets API. */
|
|
99
|
+
export async function setWorkerSecret(apiToken, accountId, workerName, writeToken, opts = {}) {
|
|
100
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
101
|
+
await request({
|
|
102
|
+
apiToken,
|
|
103
|
+
method: 'PUT',
|
|
104
|
+
pathname: `/accounts/${accountId}/workers/scripts/${workerName}/secrets`,
|
|
105
|
+
body: { name: 'WRITE_TOKEN', text: writeToken, type: 'secret_text' },
|
|
106
|
+
});
|
|
53
107
|
}
|
|
54
108
|
/** Enable the free `*.workers.dev` route for the script, and return the account subdomain. */
|
|
55
|
-
export async function enableWorkersDev(apiToken, accountId, workerName) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
109
|
+
export async function enableWorkersDev(apiToken, accountId, workerName, opts = {}) {
|
|
110
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
111
|
+
await request({
|
|
112
|
+
apiToken,
|
|
113
|
+
method: 'POST',
|
|
114
|
+
pathname: `/accounts/${accountId}/workers/scripts/${workerName}/subdomain`,
|
|
115
|
+
body: {
|
|
116
|
+
enabled: true,
|
|
117
|
+
previews_enabled: false,
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
const sub = await request({
|
|
121
|
+
apiToken,
|
|
122
|
+
method: 'GET',
|
|
123
|
+
pathname: `/accounts/${accountId}/workers/subdomain`,
|
|
59
124
|
});
|
|
60
|
-
const sub = await cf(apiToken, 'GET', `/accounts/${accountId}/workers/subdomain`);
|
|
61
125
|
if (!sub?.subdomain) {
|
|
62
126
|
throw new Error('No workers.dev subdomain on this account yet — register one at dash.cloudflare.com → Workers → Subdomain, then re-run.');
|
|
63
127
|
}
|
|
64
128
|
return sub.subdomain;
|
|
65
129
|
}
|
|
66
130
|
/** Resolve a zone id for a domain the token can see, or null if not owned/visible. */
|
|
67
|
-
export async function findZoneId(apiToken, domain) {
|
|
131
|
+
export async function findZoneId(apiToken, domain, opts = {}) {
|
|
132
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
68
133
|
// Try the exact name, then the registrable parent (share.agents-cli.sh -> agents-cli.sh).
|
|
69
134
|
const candidates = [domain, domain.split('.').slice(-2).join('.')];
|
|
70
135
|
for (const name of candidates) {
|
|
71
|
-
const zones = await
|
|
136
|
+
const zones = await request({
|
|
137
|
+
apiToken,
|
|
138
|
+
method: 'GET',
|
|
139
|
+
pathname: `/zones?name=${encodeURIComponent(name)}`,
|
|
140
|
+
}).catch(() => []);
|
|
72
141
|
if (zones?.length)
|
|
73
142
|
return zones[0].id;
|
|
74
143
|
}
|
|
75
144
|
return null;
|
|
76
145
|
}
|
|
77
146
|
/** Map a custom hostname (e.g. `share.agents-cli.sh`) to the Worker via Workers Custom Domains. */
|
|
78
|
-
export async function addCustomDomain(apiToken, accountId, workerName, zoneId, hostname) {
|
|
147
|
+
export async function addCustomDomain(apiToken, accountId, workerName, zoneId, hostname, opts = {}) {
|
|
148
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
79
149
|
try {
|
|
80
|
-
await
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
150
|
+
await request({
|
|
151
|
+
apiToken,
|
|
152
|
+
method: 'PUT',
|
|
153
|
+
pathname: `/accounts/${accountId}/workers/domains`,
|
|
154
|
+
body: {
|
|
155
|
+
zone_id: zoneId,
|
|
156
|
+
hostname,
|
|
157
|
+
service: workerName,
|
|
158
|
+
environment: 'production',
|
|
159
|
+
},
|
|
85
160
|
});
|
|
86
161
|
}
|
|
87
162
|
catch (e) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { type ShareConfig } from './config.js';
|
|
2
|
+
export type PutFn = (url: string, body: Buffer, headers: Record<string, string>) => Promise<{
|
|
2
3
|
ok: boolean;
|
|
3
4
|
status: number;
|
|
4
5
|
url?: string;
|
|
5
6
|
}>;
|
|
7
|
+
export interface PublishEndpoint {
|
|
8
|
+
baseUrl: string;
|
|
9
|
+
token: string;
|
|
10
|
+
}
|
|
6
11
|
export interface PublishOptions {
|
|
7
12
|
slug?: string;
|
|
8
13
|
/** e.g. `30d`, `12h`, or an absolute date like `2026-08-01`. */
|
|
@@ -10,15 +15,26 @@ export interface PublishOptions {
|
|
|
10
15
|
contentType?: string;
|
|
11
16
|
/** Generate + attach an OG cover for HTML pages (default true). */
|
|
12
17
|
cover?: boolean;
|
|
18
|
+
/** Inject the Cloudflare Web Analytics beacon (default true for HTML). */
|
|
19
|
+
analytics?: boolean;
|
|
20
|
+
/** Override the analytics token from share config. */
|
|
21
|
+
analyticsToken?: string;
|
|
22
|
+
/** Override the GitHub username used for the URL namespace. */
|
|
23
|
+
githubUser?: string;
|
|
24
|
+
/** DI seam for tests — override the persisted share endpoint config. */
|
|
25
|
+
config?: ShareConfig;
|
|
26
|
+
/** DI seam for tests — override the keychain-backed write token. */
|
|
27
|
+
writeToken?: string;
|
|
13
28
|
/** DI seam for tests — override the real HTTP PUT. */
|
|
14
|
-
uploader?:
|
|
15
|
-
ok: boolean;
|
|
16
|
-
status: number;
|
|
17
|
-
url?: string;
|
|
18
|
-
}>;
|
|
29
|
+
uploader?: PutFn;
|
|
19
30
|
/** DI seam for tests — override cover capture (returns a PNG buffer or null). */
|
|
20
31
|
capturer?: (htmlPath: string) => Promise<Buffer | null>;
|
|
21
32
|
}
|
|
33
|
+
export interface PublishResult {
|
|
34
|
+
url: string;
|
|
35
|
+
expiresAt?: string;
|
|
36
|
+
coverUrl?: string;
|
|
37
|
+
}
|
|
22
38
|
/** `30d` / `12h` / `2026-08-01` → an absolute ISO timestamp (or undefined). */
|
|
23
39
|
export declare function parseExpire(spec: string | undefined): string | undefined;
|
|
24
40
|
/** Derive a URL-safe slug from a filename (or pass one through). */
|
|
@@ -26,9 +42,14 @@ export declare function slugify(name: string): string;
|
|
|
26
42
|
/** The project the file belongs to — git repo name, else the cwd's basename. */
|
|
27
43
|
export declare function detectProject(dir?: string): string;
|
|
28
44
|
/**
|
|
29
|
-
* Notion-style default slug: `<project>-<feature>-<
|
|
45
|
+
* Notion-style default slug: `<project>-<feature>-<16hex>`. Project scopes the link
|
|
30
46
|
* to the repo the agent is in; the random tail keeps it unguessable + collision-free.
|
|
31
47
|
* A leading `plan-` on the filename is dropped (it's redundant under the project).
|
|
48
|
+
*
|
|
49
|
+
* The tail is 8 random bytes (64-bit, 16 hex chars). Reads are public — the URL is
|
|
50
|
+
* the only capability — so the nonce must be genuinely infeasible to brute-force,
|
|
51
|
+
* not merely unlisted; 64 bits puts a blind guess out of reach. (See docs/share.md
|
|
52
|
+
* §Security for the threat model and `--expire` for sensitive content.)
|
|
32
53
|
*/
|
|
33
54
|
export declare function defaultSlug(filePath: string, dir?: string): string;
|
|
34
55
|
/**
|
|
@@ -49,9 +70,11 @@ export declare function attachOgCover(filePath: string, body: Buffer, ctx: {
|
|
|
49
70
|
body: Buffer;
|
|
50
71
|
coverUrl?: string;
|
|
51
72
|
}>;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
export
|
|
73
|
+
/** Resolve the publisher's GitHub username, with an explicit override winning first. */
|
|
74
|
+
export declare function resolveShareUsername(opts?: {
|
|
75
|
+
githubUser?: string;
|
|
76
|
+
}): Promise<string>;
|
|
77
|
+
/** Build the R2 object key from a namespace username and a slug part. */
|
|
78
|
+
export declare function buildShareKey(username: string, slugPart: string): string;
|
|
79
|
+
export declare function publishFile(filePath: string, opts?: PublishOptions): Promise<PublishResult>;
|
|
80
|
+
export declare function publishToEndpoint(filePath: string, endpoint: PublishEndpoint, opts?: PublishOptions): Promise<PublishResult>;
|
|
@@ -10,8 +10,10 @@ import { basename } from 'node:path';
|
|
|
10
10
|
import { execFileSync } from 'node:child_process';
|
|
11
11
|
import { randomBytes } from 'node:crypto';
|
|
12
12
|
import { readShareConfig, readWriteToken } from './config.js';
|
|
13
|
+
import { resolveGitHubUsername } from '../git.js';
|
|
13
14
|
import { captureCover, OG_WIDTH, OG_HEIGHT, OG_SCALE } from './capture.js';
|
|
14
15
|
import { deriveMeta, injectOgMeta } from './og.js';
|
|
16
|
+
import { injectAnalyticsBeacon } from './analytics.js';
|
|
15
17
|
const UNIT_MS = { s: 1e3, m: 6e4, h: 36e5, d: 864e5, w: 6048e5 };
|
|
16
18
|
/** `30d` / `12h` / `2026-08-01` → an absolute ISO timestamp (or undefined). */
|
|
17
19
|
export function parseExpire(spec) {
|
|
@@ -55,13 +57,18 @@ export function detectProject(dir = process.cwd()) {
|
|
|
55
57
|
return sanitizeSlugPart(basename(dir)) || 'share';
|
|
56
58
|
}
|
|
57
59
|
/**
|
|
58
|
-
* Notion-style default slug: `<project>-<feature>-<
|
|
60
|
+
* Notion-style default slug: `<project>-<feature>-<16hex>`. Project scopes the link
|
|
59
61
|
* to the repo the agent is in; the random tail keeps it unguessable + collision-free.
|
|
60
62
|
* A leading `plan-` on the filename is dropped (it's redundant under the project).
|
|
63
|
+
*
|
|
64
|
+
* The tail is 8 random bytes (64-bit, 16 hex chars). Reads are public — the URL is
|
|
65
|
+
* the only capability — so the nonce must be genuinely infeasible to brute-force,
|
|
66
|
+
* not merely unlisted; 64 bits puts a blind guess out of reach. (See docs/share.md
|
|
67
|
+
* §Security for the threat model and `--expire` for sensitive content.)
|
|
61
68
|
*/
|
|
62
69
|
export function defaultSlug(filePath, dir) {
|
|
63
70
|
const feature = slugify(filePath).replace(/^plan-/, '') || 'page';
|
|
64
|
-
return `${detectProject(dir)}-${feature}-${randomBytes(
|
|
71
|
+
return `${detectProject(dir)}-${feature}-${randomBytes(8).toString('hex')}`;
|
|
65
72
|
}
|
|
66
73
|
function guessContentType(filePath) {
|
|
67
74
|
if (/\.html?$/i.test(filePath))
|
|
@@ -103,30 +110,70 @@ export async function attachOgCover(filePath, body, ctx) {
|
|
|
103
110
|
});
|
|
104
111
|
return { body: Buffer.from(injected, 'utf8'), coverUrl: ctx.pngUrl };
|
|
105
112
|
}
|
|
113
|
+
/** Resolve the publisher's GitHub username, with an explicit override winning first. */
|
|
114
|
+
export async function resolveShareUsername(opts = {}) {
|
|
115
|
+
if (opts.githubUser) {
|
|
116
|
+
const sanitized = sanitizeSlugPart(opts.githubUser);
|
|
117
|
+
if (sanitized)
|
|
118
|
+
return sanitized;
|
|
119
|
+
}
|
|
120
|
+
const resolved = await resolveGitHubUsername();
|
|
121
|
+
if (resolved)
|
|
122
|
+
return sanitizeSlugPart(resolved);
|
|
123
|
+
throw new Error("Could not determine your GitHub username for the share URL namespace. " +
|
|
124
|
+
"Authenticate with `gh auth login`, set `git config --global github.user <user>`, " +
|
|
125
|
+
"or pass `--github-user <user>`.");
|
|
126
|
+
}
|
|
127
|
+
/** Build the R2 object key from a namespace username and a slug part. */
|
|
128
|
+
export function buildShareKey(username, slugPart) {
|
|
129
|
+
const user = sanitizeSlugPart(username);
|
|
130
|
+
const part = sanitizeSlugPart(slugPart.replace(/\//g, '-'));
|
|
131
|
+
if (!user)
|
|
132
|
+
throw new Error('GitHub username is required for the share URL namespace.');
|
|
133
|
+
if (!part)
|
|
134
|
+
throw new Error('Share slug is empty.');
|
|
135
|
+
return `${user}/${part}`;
|
|
136
|
+
}
|
|
106
137
|
export async function publishFile(filePath, opts = {}) {
|
|
107
|
-
const cfg = readShareConfig();
|
|
138
|
+
const cfg = opts.config ?? readShareConfig();
|
|
108
139
|
if (!cfg) {
|
|
109
140
|
throw new Error("Not set up yet. Run 'agents share setup' (provision your own endpoint) or 'agents share join' (use an existing one).");
|
|
110
141
|
}
|
|
111
|
-
const token = readWriteToken();
|
|
112
|
-
const
|
|
142
|
+
const token = opts.writeToken ?? readWriteToken();
|
|
143
|
+
const username = await resolveShareUsername(opts);
|
|
144
|
+
const analyticsToken = opts.analyticsToken ?? cfg.analyticsToken;
|
|
145
|
+
return publishToEndpoint(filePath, { baseUrl: cfg.baseUrl, token }, {
|
|
146
|
+
...opts,
|
|
147
|
+
githubUser: username,
|
|
148
|
+
analyticsToken,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
152
|
+
const username = await resolveShareUsername(opts);
|
|
153
|
+
const slugPart = (opts.slug ?? defaultSlug(filePath)).replace(/^\/+/, '');
|
|
154
|
+
const key = buildShareKey(username, slugPart);
|
|
113
155
|
const expiresAt = parseExpire(opts.expire);
|
|
114
|
-
const pageUrl = `${
|
|
156
|
+
const pageUrl = `${endpoint.baseUrl.replace(/\/+$/, '')}/${key}`;
|
|
115
157
|
const put = opts.uploader ??
|
|
116
158
|
(async (u, b, h) => {
|
|
117
159
|
const res = await fetch(u, { method: 'PUT', headers: h, body: new Uint8Array(b) });
|
|
118
160
|
return { ok: res.ok, status: res.status, url: u };
|
|
119
161
|
});
|
|
120
162
|
const authHeaders = (contentType) => {
|
|
121
|
-
const h = { authorization: `Bearer ${token}`, 'content-type': contentType };
|
|
163
|
+
const h = { authorization: `Bearer ${endpoint.token}`, 'content-type': contentType };
|
|
122
164
|
if (expiresAt)
|
|
123
165
|
h['x-share-expires-at'] = expiresAt;
|
|
124
166
|
return h;
|
|
125
167
|
};
|
|
126
168
|
let body = readFileSync(filePath);
|
|
127
169
|
let coverUrl;
|
|
170
|
+
const isHtml = /\.html?$/i.test(filePath);
|
|
171
|
+
// Analytics: cookieless CF Web Analytics beacon, injected for HTML by default.
|
|
172
|
+
if (isHtml && opts.analytics !== false && opts.analyticsToken) {
|
|
173
|
+
body = Buffer.from(injectAnalyticsBeacon(body.toString('utf8'), opts.analyticsToken), 'utf8');
|
|
174
|
+
}
|
|
128
175
|
// Cover: screenshot the page's hero → upload <slug>.png → inject og:image meta.
|
|
129
|
-
if (
|
|
176
|
+
if (isHtml && opts.cover !== false) {
|
|
130
177
|
const res = await attachOgCover(filePath, body, {
|
|
131
178
|
pngUrl: `${pageUrl}.png`,
|
|
132
179
|
pageUrl,
|
|
@@ -1,26 +1,30 @@
|
|
|
1
1
|
// The Cloudflare Worker that fronts the R2 share bucket.
|
|
2
2
|
//
|
|
3
3
|
// One tiny Worker does both sides:
|
|
4
|
-
// - PUT /<slug> — bearer-gated (WRITE_TOKEN secret); writes the body
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
4
|
+
// - PUT /<username>/<slug> — bearer-gated (WRITE_TOKEN secret); writes the body
|
|
5
|
+
// to R2 via the BUCKET binding, storing an optional expires-at value in
|
|
6
|
+
// object metadata.
|
|
7
|
+
// - GET /<username>/<slug> — public; streams the object from R2, 410s (and lazily
|
|
8
|
+
// deletes) once its expiry has passed. A bucket lifecycle rule is the durable
|
|
9
|
+
// sweeper; this is the immediate gate.
|
|
10
|
+
// - GET /<username> — public gallery of that user's shares.
|
|
11
|
+
// - GET /<slug> — backward-compat flat slug (legacy shares before
|
|
12
|
+
// per-user namespaces).
|
|
9
13
|
//
|
|
10
14
|
// Emitted as a string (mirrors src/lib/serve/page.ts `renderPage()`), so it compiles
|
|
11
15
|
// into `dist/**` and ships with no package.json#files change. `provision.ts` uploads
|
|
12
16
|
// this verbatim as an ES-module Worker with a BUCKET (R2) binding + a WRITE_TOKEN secret.
|
|
13
17
|
/** Render the Worker source. Pure — the R2 binding + token are wired at deploy time. */
|
|
14
18
|
export function renderWorkerScript() {
|
|
15
|
-
return `// GENERATED by agents-cli
|
|
19
|
+
return `// GENERATED by agents-cli agents share setup — do not edit here; edit
|
|
16
20
|
// src/lib/share/worker-template.ts and re-run setup.
|
|
17
21
|
export default {
|
|
18
22
|
async fetch(request, env) {
|
|
19
23
|
const url = new URL(request.url);
|
|
20
|
-
const
|
|
24
|
+
const path = decodeURIComponent(url.pathname.replace(/^\\/+/, ''));
|
|
21
25
|
|
|
22
|
-
if (!
|
|
23
|
-
return new Response('agents share — POST is not it; PUT /<slug> to publish, GET /<slug> to view.', {
|
|
26
|
+
if (!path) {
|
|
27
|
+
return new Response('agents share — POST is not it; PUT /<username>/<slug> to publish, GET /<username>/<slug> to view.', {
|
|
24
28
|
status: 200, headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
25
29
|
});
|
|
26
30
|
}
|
|
@@ -32,19 +36,28 @@ export default {
|
|
|
32
36
|
}
|
|
33
37
|
const expiresAt = request.headers.get('x-share-expires-at') || '';
|
|
34
38
|
const contentType = request.headers.get('content-type') || 'text/html; charset=utf-8';
|
|
35
|
-
await env.BUCKET.put(
|
|
39
|
+
await env.BUCKET.put(path, request.body, {
|
|
36
40
|
httpMetadata: { contentType },
|
|
37
|
-
customMetadata: expiresAt ? { expiresAt } : {},
|
|
41
|
+
customMetadata: expiresAt ? { 'expires-at': expiresAt } : {},
|
|
38
42
|
});
|
|
39
|
-
return json({ ok: true, url: url.origin + '/' +
|
|
43
|
+
return json({ ok: true, url: url.origin + '/' + path, expiresAt: expiresAt || null }, 200);
|
|
40
44
|
}
|
|
41
45
|
|
|
42
46
|
if (request.method === 'GET' || request.method === 'HEAD') {
|
|
43
|
-
|
|
47
|
+
// Single-segment path may be a user gallery OR a legacy flat slug.
|
|
48
|
+
const segments = path.split('/').filter(Boolean);
|
|
49
|
+
if (segments.length === 1) {
|
|
50
|
+
const list = await env.BUCKET.list({ prefix: segments[0] + '/', limit: 1 });
|
|
51
|
+
if (list.objects && list.objects.length > 0) {
|
|
52
|
+
return renderGallery(env.BUCKET, url.origin, segments[0], request.method);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const obj = await env.BUCKET.get(path);
|
|
44
57
|
if (!obj) return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
|
|
45
|
-
const expiresAt = obj.customMetadata && obj.customMetadata
|
|
58
|
+
const expiresAt = obj.customMetadata && obj.customMetadata['expires-at'];
|
|
46
59
|
if (expiresAt && Date.now() > Date.parse(expiresAt)) {
|
|
47
|
-
await env.BUCKET.delete(
|
|
60
|
+
await env.BUCKET.delete(path);
|
|
48
61
|
return new Response('gone — this link has expired', { status: 410, headers: { 'content-type': 'text/plain' } });
|
|
49
62
|
}
|
|
50
63
|
const headers = new Headers();
|
|
@@ -58,14 +71,67 @@ export default {
|
|
|
58
71
|
if (request.method === 'DELETE') {
|
|
59
72
|
const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
|
|
60
73
|
if (!env.WRITE_TOKEN || !safeEqual(presented, env.WRITE_TOKEN)) return json({ error: 'unauthorized' }, 401);
|
|
61
|
-
await env.BUCKET.delete(
|
|
62
|
-
return json({ ok: true, deleted:
|
|
74
|
+
await env.BUCKET.delete(path);
|
|
75
|
+
return json({ ok: true, deleted: path }, 200);
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
return new Response('method not allowed', { status: 405 });
|
|
66
79
|
},
|
|
67
80
|
};
|
|
68
81
|
|
|
82
|
+
async function renderGallery(bucket, origin, user, method) {
|
|
83
|
+
const objects = [];
|
|
84
|
+
let cursor;
|
|
85
|
+
do {
|
|
86
|
+
const list = await bucket.list({ prefix: user + '/', limit: 1000, cursor, include: ['customMetadata'] });
|
|
87
|
+
objects.push(...(list.objects || []));
|
|
88
|
+
cursor = list.truncated ? list.cursor : undefined;
|
|
89
|
+
} while (cursor);
|
|
90
|
+
|
|
91
|
+
const activeObjects = objects.filter(o => {
|
|
92
|
+
if (o.key.endsWith('.png')) return false;
|
|
93
|
+
const expiresAt = o.customMetadata && o.customMetadata['expires-at'];
|
|
94
|
+
return !(expiresAt && Date.now() > Date.parse(expiresAt));
|
|
95
|
+
});
|
|
96
|
+
const items = activeObjects.map(o => {
|
|
97
|
+
const slug = o.key.slice(o.key.indexOf('/') + 1);
|
|
98
|
+
const url = origin + '/' + o.key;
|
|
99
|
+
return { slug, url, updated: o.uploaded };
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
if (method === 'HEAD') {
|
|
103
|
+
return new Response(null, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8' } });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const html =
|
|
107
|
+
'<!doctype html><html><head>' +
|
|
108
|
+
'<meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1">' +
|
|
109
|
+
'<title>' + escapeHtml(user) + ' — agents share</title>' +
|
|
110
|
+
'<style>' +
|
|
111
|
+
'body{font-family:system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;max-width:720px;margin:48px auto;padding:0 24px;color:#111;background:#fafafa}' +
|
|
112
|
+
'h1{font-size:28px;margin-bottom:8px}a{color:#0a0a0a;text-decoration:none;border-bottom:1px solid #999}' +
|
|
113
|
+
'a:hover{border-color:#111}ul{list-style:none;padding:0}li{margin:16px 0}' +
|
|
114
|
+
'.slug{font-weight:600}.meta{color:#666;font-size:14px}' +
|
|
115
|
+
'</style></head><body>' +
|
|
116
|
+
'<h1>@' + escapeHtml(user) + '</h1>' +
|
|
117
|
+
'<p class="meta">' + items.length + ' shared ' + (items.length === 1 ? 'page' : 'pages') + '</p>' +
|
|
118
|
+
'<ul>' +
|
|
119
|
+
items.map(i =>
|
|
120
|
+
'<li><a class="slug" href="' + escapeHtml(i.url) + '">' + escapeHtml(i.slug) + '</a>' +
|
|
121
|
+
'<br><span class="meta">' + new Date(i.updated).toISOString().slice(0, 10) + '</span></li>'
|
|
122
|
+
).join('') +
|
|
123
|
+
'</ul></body></html>';
|
|
124
|
+
return new Response(html, { status: 200, headers: { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'public, max-age=60' } });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function escapeHtml(s) {
|
|
128
|
+
return String(s)
|
|
129
|
+
.replace(/&/g, '&')
|
|
130
|
+
.replace(/</g, '<')
|
|
131
|
+
.replace(/>/g, '>')
|
|
132
|
+
.replace(/"/g, '"');
|
|
133
|
+
}
|
|
134
|
+
|
|
69
135
|
function json(body, status) {
|
|
70
136
|
return new Response(JSON.stringify(body), { status, headers: { 'content-type': 'application/json' } });
|
|
71
137
|
}
|
package/dist/lib/shims.d.ts
CHANGED
|
@@ -99,6 +99,14 @@ export declare function shimTargetsFor(platform: NodeJS.Platform): {
|
|
|
99
99
|
* Create a shim for an agent.
|
|
100
100
|
*/
|
|
101
101
|
export declare function createShim(agent: AgentId): string;
|
|
102
|
+
/** The POSIX pass-through shim for a brand. */
|
|
103
|
+
export declare function generateBrandShim(name: string): string;
|
|
104
|
+
/** Write a brand's pass-through shim(s) onto PATH; returns the shim path. */
|
|
105
|
+
export declare function createBrandShim(name: string): string;
|
|
106
|
+
/** True when the file at the given path is an agents-cli brand shim. */
|
|
107
|
+
export declare function isBrandShim(filePath: string): boolean;
|
|
108
|
+
/** Remove a brand's shim companions. Returns true if anything was removed. */
|
|
109
|
+
export declare function removeBrandShim(name: string): boolean;
|
|
102
110
|
/**
|
|
103
111
|
* Remove the shim for an agent.
|
|
104
112
|
*/
|
|
@@ -500,6 +508,7 @@ export declare function addShimsToPath(overrides?: {
|
|
|
500
508
|
shimsDir?: string;
|
|
501
509
|
}): ShimPathResult;
|
|
502
510
|
export declare function listAgentsWithInstalledVersions(): AgentId[];
|
|
511
|
+
export declare function listAgentsWithNonIsolatedInstalledVersions(): AgentId[];
|
|
503
512
|
/**
|
|
504
513
|
* Resource diff between two versions. Each field lists resources present in
|
|
505
514
|
* the current version but missing from the target.
|