@mediamonks/canopy-cli 0.7.38 → 0.7.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/{ai-CjvrcBIz.mjs → ai-EyrXJC5z.mjs} +2 -2
- package/dist/{api-BPEYm9ts.mjs → api-ArfD6-t5.mjs} +40 -5
- package/dist/{browse-CBelCgnW.mjs → browse-DEhnJAPT.mjs} +1 -1
- package/dist/{db-CbqgjfYz.mjs → db-DGrCgj-R.mjs} +3 -3
- package/dist/{deploy-CqCyry4o.mjs → deploy-B7KItUnh.mjs} +3 -3
- package/dist/{dev-BtnEuAnS.mjs → dev-03LA_Etx.mjs} +7 -7
- package/dist/dev-dJ88L73Y.mjs +2 -0
- package/dist/{dev-worker-CpSB_Okh.mjs → dev-worker-D78leKBJ.mjs} +30 -2
- package/dist/docs/cli-dev.md +1 -1
- package/dist/docs/cli-feedback.md +32 -4
- package/dist/docs/developers-architecture.md +17 -5
- package/dist/docs/platform-client-access.md +8 -0
- package/dist/docs/platform-schedules.md +21 -0
- package/dist/docs/porting-nextjs.md +13 -8
- package/dist/docs/sdk-auth.md +32 -0
- package/dist/docs/sdk-mcp.md +5 -4
- package/dist/{docs-T1nuu00A.mjs → docs-B0-IEPjd.mjs} +7 -3
- package/dist/{egress-ButFsQ2N.mjs → egress-CzxRD6Tp.mjs} +2 -2
- package/dist/{feedback-C5NKCOXQ.mjs → feedback-BsJ_6ke2.mjs} +361 -17
- package/dist/index.mjs +31 -31
- package/dist/{local-BfqTheMP.mjs → local-CkS1XhRu.mjs} +3 -3
- package/dist/{login-CtDWXICZ.mjs → login-B2FJGIrD.mjs} +24 -7
- package/dist/{logs-BltUi4Gp.mjs → logs-wSKh_FmL.mjs} +3 -3
- package/dist/{mcp-BwLRcfqz.mjs → mcp-DtRIDtaL.mjs} +2 -2
- package/dist/{migrations-CF1iNZ7F.mjs → migrations-CG_O5Ghw.mjs} +1 -1
- package/dist/{open-DXo60Ikf.mjs → open-COFMburt.mjs} +1 -1
- package/dist/{package-CdDH35SE.mjs → package-C1xCnyOB.mjs} +1 -1
- package/dist/project-template-DlzMx8Oj.mjs +2 -0
- package/dist/{project-template-BEr_tgIY.mjs → project-template-DtTfXyLI.mjs} +2 -2
- package/dist/{projects-Bm4oTodB.mjs → projects-B3Wi01lw.mjs} +4 -4
- package/dist/{repo-CE22HpqZ.mjs → repo-4x-CJwCp.mjs} +6 -3
- package/dist/repo-CmbakFVi.mjs +2 -0
- package/dist/{schedule-DdMq5eXC.mjs → schedule-CbaDIJXr.mjs} +2 -2
- package/dist/{secrets-BOKuWBYV.mjs → secrets-CmY4dUHz.mjs} +2 -2
- package/dist/skill/canopy/SKILL.md +7 -4
- package/dist/{skill-3wOwhzDx.mjs → skill-AqZYYsE6.mjs} +2 -2
- package/dist/{src-CxzGOEhP.mjs → src-RUf3Gfuh.mjs} +6 -2
- package/dist/{staging-CU4QrzFX.mjs → staging-ypR79MlD.mjs} +2 -2
- package/dist/templates/spa/package.json +1 -1
- package/dist/templates/worker/package.json +1 -1
- package/dist/{token-BWnLsWLg.mjs → token-CDrHQ5Xb.mjs} +2 -2
- package/dist/{ui-CHfKV6lG.mjs → ui-CaHgvvkJ.mjs} +1 -1
- package/dist/{upgrade-BJ2dWjCG.mjs → upgrade-DSjmifH_.mjs} +2 -2
- package/dist/{usage-Cqqvnzrf.mjs → usage-CP8rBVZi.mjs} +2 -2
- package/package.json +2 -2
- package/dist/dev-D_qzeLng.mjs +0 -2
- package/dist/project-template-DdT0dqpn.mjs +0 -2
- package/dist/repo-DlYX_1tN.mjs +0 -2
package/README.md
CHANGED
|
@@ -11,12 +11,15 @@ canopy deploy [--no-build] # authorize → signed upload → complete
|
|
|
11
11
|
canopy rollback / deploys / download
|
|
12
12
|
canopy db pull | snapshot | snapshots | restore
|
|
13
13
|
canopy token create | list | revoke
|
|
14
|
-
canopy
|
|
14
|
+
canopy feedback list | show | comment | note | status | report | watch
|
|
15
|
+
canopy usage / docs / whoami [--json] / logout
|
|
15
16
|
```
|
|
16
17
|
|
|
17
18
|
Control-plane calls go through `src/api.ts` (`/control/v1` on the active
|
|
18
19
|
site's `api.` host, bearer token — `CANOPY_DEPLOY_TOKEN` in CI wins over the
|
|
19
|
-
login token
|
|
20
|
+
login token; every request says which AI agent drives the CLI, if any, as
|
|
21
|
+
`x-canopy-agent`: `CANOPY_AGENT=<name>`, `none` to opt out, else `claude-code`
|
|
22
|
+
under `CLAUDECODE=1`); database safety runs on the data plane (`apiDataRequest` with
|
|
20
23
|
`X-Canopy-Project`). `canopy deploy` streams bundle bytes straight to
|
|
21
24
|
`/data/v1/uploads/:grant` under a short-lived signed grant — they never
|
|
22
25
|
transit the control plane (docs/09-plane-split.md D-d). Multi-site profiles:
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { r as reportErrors } from "./ui-
|
|
1
|
+
import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
|
|
2
|
+
import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/ai.ts
|
|
5
5
|
/**
|
|
@@ -213,6 +213,41 @@ function randomHex(bytes) {
|
|
|
213
213
|
function traceHeaders() {
|
|
214
214
|
return { traceparent: `00-${TRACE_ID}-${randomHex(8)}-01` };
|
|
215
215
|
}
|
|
216
|
+
/**
|
|
217
|
+
* Which AI agent drives this process, if any (report-agent). The platform
|
|
218
|
+
* records it next to what the caller writes — "Arjan's agent (claude-code)" —
|
|
219
|
+
* without changing who the author is or what they may do. `CANOPY_AGENT`
|
|
220
|
+
* names it (`none` opts out, e.g. a person running the CLI from a shell that
|
|
221
|
+
* inherited the variable); otherwise Claude Code is recognised by the
|
|
222
|
+
* `CLAUDECODE=1` it sets in every shell it spawns.
|
|
223
|
+
*/
|
|
224
|
+
const AGENT_RE = /^[a-z0-9-]{1,40}$/;
|
|
225
|
+
const warnedAgents = /* @__PURE__ */ new Set();
|
|
226
|
+
function agentName(env = process.env) {
|
|
227
|
+
const named = env["CANOPY_AGENT"];
|
|
228
|
+
if (named !== void 0 && named !== "") {
|
|
229
|
+
if (named === "none") return void 0;
|
|
230
|
+
if (AGENT_RE.test(named)) return named;
|
|
231
|
+
if (!warnedAgents.has(named)) {
|
|
232
|
+
warnedAgents.add(named);
|
|
233
|
+
console.error(`warning: ignoring CANOPY_AGENT=${JSON.stringify(named)} — use 1–40 characters from a-z, 0-9 and -, or \`none\` to send no agent name.`);
|
|
234
|
+
}
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
return env["CLAUDECODE"] === "1" ? "claude-code" : void 0;
|
|
238
|
+
}
|
|
239
|
+
/** The `x-canopy-agent` header for every platform request (empty when no agent drives us). */
|
|
240
|
+
function agentHeaders(env = process.env) {
|
|
241
|
+
const agent = agentName(env);
|
|
242
|
+
return agent ? { "x-canopy-agent": agent } : {};
|
|
243
|
+
}
|
|
244
|
+
/** Headers every control/data-plane request carries besides auth: trace + agent. */
|
|
245
|
+
function commonHeaders() {
|
|
246
|
+
return {
|
|
247
|
+
...traceHeaders(),
|
|
248
|
+
...agentHeaders()
|
|
249
|
+
};
|
|
250
|
+
}
|
|
216
251
|
var ApiError = class extends Error {
|
|
217
252
|
status;
|
|
218
253
|
code;
|
|
@@ -297,7 +332,7 @@ async function apiRequest(method, path, body, rawContentType, headers) {
|
|
|
297
332
|
const response = await fetch(`${apiUrl}/control/v1${path}`, {
|
|
298
333
|
method,
|
|
299
334
|
headers: {
|
|
300
|
-
...
|
|
335
|
+
...commonHeaders(),
|
|
301
336
|
authorization: `Bearer ${token}`,
|
|
302
337
|
...body === void 0 || isForm ? {} : { "content-type": rawContentType ?? "application/json" },
|
|
303
338
|
...headers
|
|
@@ -317,7 +352,7 @@ async function apiUpload(path, bytes, contentType) {
|
|
|
317
352
|
const response = await fetch(`${apiUrl}/control/v1${path}`, {
|
|
318
353
|
method: "POST",
|
|
319
354
|
headers: {
|
|
320
|
-
...
|
|
355
|
+
...commonHeaders(),
|
|
321
356
|
authorization: `Bearer ${token}`,
|
|
322
357
|
"content-type": contentType
|
|
323
358
|
},
|
|
@@ -330,7 +365,7 @@ async function apiUpload(path, bytes, contentType) {
|
|
|
330
365
|
async function apiDownload(path) {
|
|
331
366
|
const { apiUrl, token, isDeployToken } = credentials();
|
|
332
367
|
const response = await fetch(`${apiUrl}/control/v1${path}`, { headers: {
|
|
333
|
-
...
|
|
368
|
+
...commonHeaders(),
|
|
334
369
|
authorization: `Bearer ${token}`
|
|
335
370
|
} });
|
|
336
371
|
if (!response.ok) return fail(response, isDeployToken, "GET", path);
|
|
@@ -346,7 +381,7 @@ async function apiDataRequest(method, path, projectSlug, body, rawContentType, c
|
|
|
346
381
|
const response = await fetch(`${apiUrl}/data/v1${path}`, {
|
|
347
382
|
method,
|
|
348
383
|
headers: {
|
|
349
|
-
...
|
|
384
|
+
...commonHeaders(),
|
|
350
385
|
authorization: `Bearer ${token}`,
|
|
351
386
|
"x-canopy-project": projectSlug,
|
|
352
387
|
...channel === "staging" ? { "x-canopy-channel": "staging" } : {},
|
|
@@ -359,4 +394,4 @@ async function apiDataRequest(method, path, projectSlug, body, rawContentType, c
|
|
|
359
394
|
return await response.json();
|
|
360
395
|
}
|
|
361
396
|
//#endregion
|
|
362
|
-
export {
|
|
397
|
+
export { apiRequest as a, credentials as c, apiDownload as i, withContext as l, agentHeaders as n, apiUpload as o, apiDataRequest as r, commonHeaders as s, ApiError as t };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { d as tryResolveProject, n as siteConfig } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { a as apiRequest, t as ApiError } from "./api-ArfD6-t5.mjs";
|
|
3
3
|
import { defineCommand } from "citty";
|
|
4
4
|
//#region src/browse.ts
|
|
5
5
|
/**
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { c as resolveProject, o as projectFlagArgs } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-
|
|
4
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
2
|
+
import { a as apiRequest, l as withContext, r as apiDataRequest } from "./api-ArfD6-t5.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors } from "./ui-CaHgvvkJ.mjs";
|
|
4
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-CG_O5Ghw.mjs";
|
|
5
5
|
import { defineCommand } from "citty";
|
|
6
6
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
7
7
|
import * as p from "@clack/prompts";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, l as stagingUrl, n as siteConfig, o as projectFlagArgs, s as projectUrl, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-
|
|
4
|
-
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-
|
|
2
|
+
import { a as apiRequest, i as apiDownload, l as withContext, r as apiDataRequest } from "./api-ArfD6-t5.mjs";
|
|
3
|
+
import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
|
|
4
|
+
import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-CG_O5Ghw.mjs";
|
|
5
5
|
import { builtinModules } from "node:module";
|
|
6
6
|
import { defineCommand } from "citty";
|
|
7
7
|
import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject, i as projectDialect, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { r as reportErrors, t as formatDateTime } from "./ui-
|
|
4
|
-
import { r as readMigrationFiles } from "./migrations-
|
|
5
|
-
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-
|
|
2
|
+
import { a as apiRequest, l as withContext, t as ApiError } from "./api-ArfD6-t5.mjs";
|
|
3
|
+
import { r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
|
|
4
|
+
import { r as readMigrationFiles } from "./migrations-CG_O5Ghw.mjs";
|
|
5
|
+
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER } from "./src-RUf3Gfuh.mjs";
|
|
6
6
|
import { n as writeDevServerRecord } from "./dev-server-file-Dk9pk4AR.mjs";
|
|
7
7
|
import { createRequire } from "node:module";
|
|
8
8
|
import { defineCommand } from "citty";
|
|
@@ -41,7 +41,7 @@ async function workerPlugin(flags, slug, workerAttest, remoteUser) {
|
|
|
41
41
|
console.log(`note: canopy.json points "worker" at ${entry}, which does not exist — skipping.`);
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
|
-
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-
|
|
44
|
+
const { missingSecretNames, readDevVars, workerDevPlugin } = await import("./dev-worker-D78leKBJ.mjs");
|
|
45
45
|
const secrets = readDevVars(flags.dir);
|
|
46
46
|
const email = siteConfig().email;
|
|
47
47
|
const user = await remoteUser?.() ?? (email ? {
|
|
@@ -251,7 +251,7 @@ async function devRemote(flags, slug) {
|
|
|
251
251
|
async function devLocal(flags, slug) {
|
|
252
252
|
let dump;
|
|
253
253
|
if (flags.pull) {
|
|
254
|
-
const { fetchProjectDump } = await import("./local-
|
|
254
|
+
const { fetchProjectDump } = await import("./local-CkS1XhRu.mjs");
|
|
255
255
|
console.log(`Pulling a fresh export of ${slug}${flags.staging ? " [staging]" : ""}…`);
|
|
256
256
|
dump = await fetchProjectDump(slug, flags.staging ? "staging" : "live");
|
|
257
257
|
}
|
|
@@ -267,7 +267,7 @@ async function devLocal(flags, slug) {
|
|
|
267
267
|
console.log(`Note: ai/services will be unavailable — could not reach the platform (${reason}).`);
|
|
268
268
|
}
|
|
269
269
|
const workerAttest = randomBytes(24).toString("base64url");
|
|
270
|
-
const { createSandbox } = await import("./local-
|
|
270
|
+
const { createSandbox } = await import("./local-CkS1XhRu.mjs");
|
|
271
271
|
const sandbox = await createSandbox({
|
|
272
272
|
slug,
|
|
273
273
|
persistDir: join(flags.dir, ".canopy", "local"),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER, r as redactCanopyCredentials, t as CALLER_HEADER } from "./src-
|
|
1
|
+
import { i as stampWorkerHeaders, n as WORKER_ATTEST_HEADER, r as redactCanopyCredentials, t as CALLER_HEADER } from "./src-RUf3Gfuh.mjs";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { relative, resolve, sep } from "node:path";
|
|
4
4
|
//#region src/dev-worker.ts
|
|
@@ -200,6 +200,31 @@ function userFromHeaders(headers, fallback) {
|
|
|
200
200
|
};
|
|
201
201
|
}
|
|
202
202
|
/**
|
|
203
|
+
* The caller in dev: stamped caller headers when a request poses as one
|
|
204
|
+
* (a client-password visitor, say), else the same monk env.USER resolves to —
|
|
205
|
+
* the developer, since no company login fronts a local server.
|
|
206
|
+
*/
|
|
207
|
+
function callerFromHeaders(headers, fallback) {
|
|
208
|
+
const kind = (headers.get("x-canopy-identity-kind") ?? "").trim();
|
|
209
|
+
const id = (headers.get("x-canopy-identity-id") ?? "").trim();
|
|
210
|
+
const agent = headers.get("x-canopy-caller") === "agent";
|
|
211
|
+
if ((kind === "monk" || kind === "external") && id) return {
|
|
212
|
+
kind,
|
|
213
|
+
id,
|
|
214
|
+
email: (headers.get("x-canopy-identity-email") ?? "").trim(),
|
|
215
|
+
name: (headers.get("x-canopy-identity-name") ?? "").trim(),
|
|
216
|
+
agent
|
|
217
|
+
};
|
|
218
|
+
const user = userFromHeaders(headers, fallback);
|
|
219
|
+
return user ? {
|
|
220
|
+
kind: "monk",
|
|
221
|
+
id: user.sub,
|
|
222
|
+
email: user.email,
|
|
223
|
+
name: user.name,
|
|
224
|
+
agent
|
|
225
|
+
} : null;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
203
228
|
* The env object the worker sees, in the shim's own key order: secrets first,
|
|
204
229
|
* platform keys after, so a stray `.dev.vars` entry can never shadow
|
|
205
230
|
* `CANOPY`/`DB`/`USER` (the dashboard rejects those names outright —
|
|
@@ -207,12 +232,14 @@ function userFromHeaders(headers, fallback) {
|
|
|
207
232
|
*/
|
|
208
233
|
function buildWorkerEnv(options, request, parts) {
|
|
209
234
|
const blobPrefix = `projects/${options.slug}/`;
|
|
235
|
+
const caller = callerFromHeaders(request.headers, options.user);
|
|
210
236
|
return {
|
|
211
237
|
...options.secrets,
|
|
212
238
|
CANOPY: { fetch: parts.canopyFetch },
|
|
213
239
|
DB: parts.db,
|
|
214
240
|
BLOB: parts.blob,
|
|
215
|
-
USER: userFromHeaders(request.headers, options.user),
|
|
241
|
+
USER: caller?.kind === "external" ? null : userFromHeaders(request.headers, options.user),
|
|
242
|
+
CALLER: caller,
|
|
216
243
|
PROJECT_SLUG: options.slug,
|
|
217
244
|
BLOB_PREFIX: blobPrefix,
|
|
218
245
|
CANOPY_CHANNEL: options.channel
|
|
@@ -316,6 +343,7 @@ function workerDevPlugin(options) {
|
|
|
316
343
|
return;
|
|
317
344
|
}
|
|
318
345
|
env.USER = null;
|
|
346
|
+
env.CALLER = null;
|
|
319
347
|
await worker.scheduled({
|
|
320
348
|
name,
|
|
321
349
|
cron: typeof body.cron === "string" ? body.cron : "",
|
package/dist/docs/cli-dev.md
CHANGED
|
@@ -39,7 +39,7 @@ With the lock in place, `--local` runs on an embedded SQLite engine instead —
|
|
|
39
39
|
|
|
40
40
|
**Your backend worker.** If `canopy.json` names a `worker` entry (the worker scaffold does), `canopy dev` serves it too: requests to `/api/*` go to your worker, everything else to the SPA — the same split the platform makes once you deploy. There's no build step; save the file and the next request runs the new code.
|
|
41
41
|
|
|
42
|
-
Your worker gets the same `env` it gets in production: `env.CANOPY.fetch` for the SDK, `env.DB` and `env.BLOB` for raw database and file access, `env.USER` for
|
|
42
|
+
Your worker gets the same `env` it gets in production: `env.CANOPY.fetch` for the SDK, `env.DB` and `env.BLOB` for raw database and file access, `env.CALLER` and `env.USER` for whoever is calling, `env.PROJECT_SLUG`. They all follow the mode you're in — live project data by default, your sandbox under `--local`. Two differences worth knowing: `env.CALLER` and `env.USER` are you (the account you logged in with), since there's no company login in front of a local server, and a write through `env.DB` reports no row count locally.
|
|
43
43
|
|
|
44
44
|
To tick a schedule by hand, post its name to the same path the platform uses:
|
|
45
45
|
|
|
@@ -13,10 +13,13 @@ ones.
|
|
|
13
13
|
canopy feedback list --status investigate
|
|
14
14
|
canopy feedback show <id>
|
|
15
15
|
canopy feedback comment <id> "root cause: the upload grant expires too early"
|
|
16
|
+
canopy feedback note <id> "stack trace and table names for the team"
|
|
16
17
|
canopy feedback status <id> investigated --comment "root cause: the grant expires too early"
|
|
17
18
|
canopy feedback list --status accepted
|
|
18
19
|
canopy feedback status <id> resolved --comment "fixed in v42"
|
|
20
|
+
canopy feedback status <id> declined --comment "works as intended: drafts are private"
|
|
19
21
|
canopy feedback mine
|
|
22
|
+
canopy feedback watch --project my-project
|
|
20
23
|
canopy feedback report "canopy deploy exits 1 after upload" --command "canopy deploy"
|
|
21
24
|
canopy feedback report "the header overlaps the logo" --attach ./shot.png
|
|
22
25
|
canopy feedback attach <id> ./after-fix.png --comment "fixed — see the shot"
|
|
@@ -46,6 +49,9 @@ machine-readable output.
|
|
|
46
49
|
updates). `--internal` (members only) keeps the note off the reporter's
|
|
47
50
|
view — for stack traces, table names, and other working detail that would
|
|
48
51
|
only confuse them.
|
|
52
|
+
- `note <id> <text>` — post an internal note: the same as
|
|
53
|
+
`comment <id> <text> --internal`, with no way to make it public. Handy
|
|
54
|
+
when you let an agent post notes but not replies the reporter sees.
|
|
49
55
|
- `attach <id> <file> [--attach <more>] [--caption <text>] [--comment <text>] [--internal]` —
|
|
50
56
|
add screenshots to an existing report. With `--comment`, the comment is
|
|
51
57
|
posted first and the screenshots hang off that trail entry — how you show
|
|
@@ -56,6 +62,12 @@ machine-readable output.
|
|
|
56
62
|
findings: pass `--comment`, or comment first.
|
|
57
63
|
- `mine [--project <slug>] [--json]` — your own reports across all projects,
|
|
58
64
|
with current statuses.
|
|
65
|
+
- `watch [--project <slug>]... [--since <time>] [--after <cursor>] [--json]` —
|
|
66
|
+
follow new reports, comments and status changes on projects you're a member
|
|
67
|
+
of, live, until you stop it. Repeat `--project` to follow several. With
|
|
68
|
+
`--json` each event is one line of JSON carrying an `id`; pass the last one
|
|
69
|
+
back as `--after` to pick up where you left off. Events say what happened
|
|
70
|
+
and who did it, not the text — use `show` for that.
|
|
59
71
|
- `report <text> [--kind bug|feedback] [--project <slug>] [--command <cmd>] [--error <output>] [--attach <file>] [--caption <text>]` —
|
|
60
72
|
file a new report. **Without `--project` it goes to the platform inbox**
|
|
61
73
|
(Canopy itself) — the right target when a `canopy` command misbehaves. The
|
|
@@ -65,6 +77,21 @@ machine-readable output.
|
|
|
65
77
|
itself is the write-up of what you found, `accepted` when the person you are
|
|
66
78
|
working with asked for it.
|
|
67
79
|
|
|
80
|
+
## Agents and people
|
|
81
|
+
|
|
82
|
+
Every report and comment remembers how it was made: from the dashboard or
|
|
83
|
+
widget, by a person at the CLI, or by an AI agent driving the CLI. Agent
|
|
84
|
+
entries read as "Ana's agent (claude-code)" in the dashboard, in Slack and
|
|
85
|
+
in `show`, so you can tell what you wrote from what your agent wrote.
|
|
86
|
+
|
|
87
|
+
The CLI notices on its own when Claude Code runs it. For another agent, set
|
|
88
|
+
`CANOPY_AGENT=<name>` (letters, digits and dashes) in its environment. To
|
|
89
|
+
mark a command as yours even inside an agent's shell, set
|
|
90
|
+
`CANOPY_AGENT=none`.
|
|
91
|
+
|
|
92
|
+
You get a Slack message when your own agent does something on a report you
|
|
93
|
+
follow, even though you never get one for your own actions.
|
|
94
|
+
|
|
68
95
|
## For agents
|
|
69
96
|
|
|
70
97
|
Two loops.
|
|
@@ -77,13 +104,14 @@ Stop there — say what it would take to fix and let the person decide.
|
|
|
77
104
|
Split your findings by audience: the status comment is what the **reporter**
|
|
78
105
|
reads, so keep it plain ("the upload fails for files over 5 MB — looking into
|
|
79
106
|
it"). The tech-loaded version — stack traces, table names, reproduction
|
|
80
|
-
queries — goes in `
|
|
107
|
+
queries — goes in `note <id> "<detail>"`, which only project
|
|
81
108
|
members see. An internal note counts as findings for `investigated`.
|
|
82
109
|
|
|
83
110
|
**Build:** `list --status accepted` is what a person has asked for. Do the
|
|
84
|
-
work, then `
|
|
85
|
-
|
|
86
|
-
you
|
|
111
|
+
work, then `status <id> resolved --comment "<what changed>"`, or
|
|
112
|
+
`status <id> declined --comment "<why>"` if it turns out to be a bad idea.
|
|
113
|
+
Set `accepted` yourself only when the person you are working with told you
|
|
114
|
+
to.
|
|
87
115
|
|
|
88
116
|
When a `canopy` command itself fails: rule out user error, check
|
|
89
117
|
`canopy feedback mine` for a duplicate, then `report` with the exact command
|
|
@@ -67,17 +67,29 @@ else is still static serving. A few things to know:
|
|
|
67
67
|
|
|
68
68
|
- **Requests arrive authenticated.** The worker sits behind the same gateway
|
|
69
69
|
wall as the frontend.
|
|
70
|
-
- **The caller is already identified.** `c.env.
|
|
71
|
-
|
|
70
|
+
- **The caller is already identified.** `c.env.CALLER` is whoever the
|
|
71
|
+
platform let in, with no round-trip needed: a signed-in Monk
|
|
72
|
+
(`kind: "monk"`) or a visitor who used the project's
|
|
73
|
+
[client password](https://docs.canopy.website/platform/client-access) (`kind: "external"`). It is
|
|
74
|
+
`null` only when nobody called, such as during a scheduled run:
|
|
72
75
|
|
|
73
76
|
```ts
|
|
74
77
|
app.get("/api/hello", (c) => {
|
|
75
|
-
const
|
|
76
|
-
if (!
|
|
77
|
-
return c.json({ message:
|
|
78
|
+
const caller = c.env.CALLER;
|
|
79
|
+
if (!caller) return c.json({ error: "no caller" }, 400); // a scheduled run
|
|
80
|
+
if (caller.kind === "external") return c.json({ message: "Hello!" });
|
|
81
|
+
return c.json({ message: `Hello, ${caller.name}!` });
|
|
78
82
|
});
|
|
79
83
|
```
|
|
80
84
|
|
|
85
|
+
`c.env.USER` is the shorter Monk-only form: the signed-in Monk, or `null`
|
|
86
|
+
for client-password visitors and scheduled runs. Use it when a route is for
|
|
87
|
+
Monks only, and don't read a `null` `USER` as "nobody is here".
|
|
88
|
+
|
|
89
|
+
A backend gets `CALLER` from its next deploy. One deployed before then can
|
|
90
|
+
ask the platform instead: `await canopy.auth.user()` through the `CANOPY`
|
|
91
|
+
binding (below) answers for every visitor.
|
|
92
|
+
|
|
81
93
|
- **Platform access goes through the same SDK**, via the `CANOPY` service
|
|
82
94
|
binding — same budgets, same data isolation as the browser:
|
|
83
95
|
|
|
@@ -46,6 +46,14 @@ immediately, unlocked browsers included.
|
|
|
46
46
|
> wrong for anything sensitive. Anyone who has it can view the app and share it further, so rotate
|
|
47
47
|
> it when a review round ends.
|
|
48
48
|
|
|
49
|
+
## Clients and your backend
|
|
50
|
+
|
|
51
|
+
A client who unlocks the project with the password can use its backend
|
|
52
|
+
routes too, as an anonymous visitor. The worker sees them on `env.CALLER`
|
|
53
|
+
with `kind: "external"`, while `env.USER` (Monks only) is `null` for them. So a
|
|
54
|
+
route that should serve clients checks `env.CALLER`, not `env.USER`. See
|
|
55
|
+
[Auth](https://docs.canopy.website/sdk/auth#in-a-backend-worker).
|
|
56
|
+
|
|
49
57
|
## Playing well with other access controls
|
|
50
58
|
|
|
51
59
|
Client access is independent of
|
|
@@ -91,6 +91,27 @@ of truth for _when_ a job runs, so you can change `daily-digest` from 6:00 to
|
|
|
91
91
|
7:30 — or pause it — without touching code or redeploying. The name is the
|
|
92
92
|
stable contract between the dashboard and your handler.
|
|
93
93
|
|
|
94
|
+
## How long a run may take
|
|
95
|
+
|
|
96
|
+
Plan for **at most 5 minutes** per run. A run that takes longer can be cut
|
|
97
|
+
off before it finishes, and is then recorded as failed. If a job grows past
|
|
98
|
+
that, split it: handle a batch per run and remember where you stopped (in
|
|
99
|
+
the database), or run the schedule more often.
|
|
100
|
+
|
|
101
|
+
What happens around a run:
|
|
102
|
+
|
|
103
|
+
- **No retries.** A failed run is recorded on the schedule row, members are
|
|
104
|
+
notified the first time it fails, and the next attempt is simply the next
|
|
105
|
+
tick.
|
|
106
|
+
- **No overlap protection.** Every tick starts a fresh run, and **Run now**
|
|
107
|
+
can run alongside a scheduled one. Make the handler safe to run twice: write
|
|
108
|
+
idempotently, or take a lock row in the database and skip when it's held.
|
|
109
|
+
- **Deploys don't stop a run.** A run in progress finishes on the version it
|
|
110
|
+
started with, and keeps its data access while it does. New code takes over
|
|
111
|
+
from the next run. Database migrations in a deploy apply right away, so a
|
|
112
|
+
run on the old version may meet the new schema. Additive migrations keep
|
|
113
|
+
that safe.
|
|
114
|
+
|
|
94
115
|
## Things to know
|
|
95
116
|
|
|
96
117
|
- **Cron runs as the project, not as a person.** Data access works (see
|
|
@@ -69,7 +69,7 @@ three buckets:
|
|
|
69
69
|
| Route handlers (`app/api/*/route.ts`) | Worker endpoints under `/api/*` |
|
|
70
70
|
| Server actions | Explicit worker endpoints, called with `fetch` from the client |
|
|
71
71
|
| Middleware | Usually deleted (auth is platform-provided); anything else moves into the worker |
|
|
72
|
-
| NextAuth / auth middleware | Deleted — use `canopy.auth.user()` (client) or `env.
|
|
72
|
+
| NextAuth / auth middleware | Deleted — use `canopy.auth.user()` (client) or `env.CALLER` (worker) |
|
|
73
73
|
| Database (Prisma, Postgres, …) | `canopy.db` — per-project database, document API + SQL escape hatch |
|
|
74
74
|
| File uploads (S3, …) | `canopy.blob` — per-project file storage with signed downloads |
|
|
75
75
|
| AI SDK / OpenAI client | `canopy.ai.chat()`, or point the Vercel AI SDK at `canopy.ai.endpoint()` |
|
|
@@ -117,12 +117,14 @@ Route handlers and server actions become endpoints in one fetch handler.
|
|
|
117
117
|
|
|
118
118
|
```ts
|
|
119
119
|
// worker/index.ts
|
|
120
|
-
import { createCanopy } from "@mediamonks/canopy";
|
|
120
|
+
import { type CanopyCaller, createCanopy } from "@mediamonks/canopy";
|
|
121
121
|
import { Hono } from "hono";
|
|
122
122
|
|
|
123
123
|
type Env = {
|
|
124
124
|
CANOPY: { fetch: typeof fetch };
|
|
125
|
-
/**
|
|
125
|
+
/** Whoever the platform let in, client-password visitors included; null for cron. */
|
|
126
|
+
CALLER: CanopyCaller | null;
|
|
127
|
+
/** The signed-in monk only; null for cron and client-password visitors. */
|
|
126
128
|
USER: { sub: string; email: string; name: string } | null;
|
|
127
129
|
/** Project secrets from dashboard Settings appear as plain keys. */
|
|
128
130
|
MY_API_KEY?: string;
|
|
@@ -132,8 +134,10 @@ const app = new Hono<{ Bindings: Env }>();
|
|
|
132
134
|
|
|
133
135
|
// was: app/api/report/route.ts
|
|
134
136
|
app.post("/api/report", async (c) => {
|
|
137
|
+
// Reports belong to a Monk's email, so this route is Monks-only. A
|
|
138
|
+
// client-password visitor is still let in, as env.CALLER with no email.
|
|
135
139
|
const user = c.env.USER;
|
|
136
|
-
if (!user) return c.json({ error: "
|
|
140
|
+
if (!user) return c.json({ error: "monks_only" }, 403);
|
|
137
141
|
|
|
138
142
|
// The SDK works inside the worker through the CANOPY binding —
|
|
139
143
|
// same database and files the frontend sees, caller's identity attached.
|
|
@@ -184,8 +188,8 @@ egress allowlist, and even an allowed hop would arrive without your session.
|
|
|
184
188
|
The platform recognizes this shape and short-circuits it: a fetch to the
|
|
185
189
|
project's **own hostname** (or to `localhost` on any port — the common
|
|
186
190
|
`BASE_URL` fallback) with a path under `/api/` re-enters your worker directly,
|
|
187
|
-
in-process. No network hop, no allowlist entry, and `env.
|
|
188
|
-
caller who made the original request. Two things to know:
|
|
191
|
+
in-process. No network hop, no allowlist entry, and `env.CALLER` and
|
|
192
|
+
`env.USER` are the same caller who made the original request. Two things to know:
|
|
189
193
|
|
|
190
194
|
- The loopback covers `/api/*` only. A self-fetch to any other path fails
|
|
191
195
|
like any other non-allowlisted host.
|
|
@@ -222,7 +226,8 @@ runs, so the ported app removes sign-in pages, session handling, and auth
|
|
|
222
226
|
middleware entirely:
|
|
223
227
|
|
|
224
228
|
- **Client:** `canopy.auth.user()` returns the signed-in monk.
|
|
225
|
-
- **Worker:** `env.
|
|
229
|
+
- **Worker:** `env.CALLER` carries the verified caller (no round-trip),
|
|
230
|
+
client-password visitors included; `env.USER` is the Monk-only form.
|
|
226
231
|
- Client-facing access without OneLogin is a per-project
|
|
227
232
|
[password](https://docs.canopy.website/platform/client-access), configured in Settings — not code.
|
|
228
233
|
|
|
@@ -252,7 +257,7 @@ past greenhouse size (see [Use cases](https://docs.canopy.website/overview/use-c
|
|
|
252
257
|
- [ ] Frontend: `output: 'export'` or Vite SPA; fix dynamic segments and `next/image`
|
|
253
258
|
- [ ] Worker: route handlers + server actions → `/api/*` endpoints in `worker/index.ts`
|
|
254
259
|
- [ ] Data: ORM → `canopy.db`; uploads → `canopy.blob`; AI clients → `canopy.ai`
|
|
255
|
-
- [ ] Delete NextAuth/middleware; read identity from `canopy.auth.user()` / `env.
|
|
260
|
+
- [ ] Delete NextAuth/middleware; read identity from `canopy.auth.user()` / `env.CALLER`
|
|
256
261
|
- [ ] Secrets via `canopy secrets set` (or dashboard Settings); `NEXT_PUBLIC_*` into your CI build
|
|
257
262
|
- [ ] Crons into Settings → Schedules, handled in `scheduled()`
|
|
258
263
|
- [ ] `canopy.json` with `build`, `dist`, `worker` — then `canopy deploy`
|
package/dist/docs/sdk-auth.md
CHANGED
|
@@ -43,6 +43,38 @@ if (user.kind === "external") {
|
|
|
43
43
|
> **Warning:** Branching on `kind` in the browser is presentation, not security — anything your page can fetch, a
|
|
44
44
|
> visitor can fetch. The platform's access rules are what actually protect data.
|
|
45
45
|
|
|
46
|
+
### In a backend worker
|
|
47
|
+
|
|
48
|
+
A worker route already knows who is calling, without asking: `env.CALLER`
|
|
49
|
+
holds the same `id`, `email`, `name` and `kind` that `auth.user()` returns,
|
|
50
|
+
plus `agent`, which is `true` when an AI agent is acting for a Monk.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import type { CanopyCaller } from "@mediamonks/canopy";
|
|
54
|
+
|
|
55
|
+
app.get("/api/me", (c) => {
|
|
56
|
+
const caller: CanopyCaller | null = c.env.CALLER;
|
|
57
|
+
if (!caller) return c.json({ error: "no caller" }, 400); // a scheduled run
|
|
58
|
+
return c.json({ kind: caller.kind, name: caller.name });
|
|
59
|
+
});
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
`env.CALLER` is `null` only when nobody called, such as during a scheduled
|
|
63
|
+
run. `env.USER` is the Monk-only form: the signed-in Monk, or `null` for a
|
|
64
|
+
client-password visitor.
|
|
65
|
+
|
|
66
|
+
A client-password visitor has no email address and is named `"Client"`. Its
|
|
67
|
+
`id` changes with every password unlock, so don't key data on it that has to
|
|
68
|
+
last.
|
|
69
|
+
|
|
70
|
+
A backend gets `env.CALLER` from its next deploy. Until it is redeployed,
|
|
71
|
+
`auth.user()` through the `CANOPY` binding gives the same answer:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const canopy = createCanopy({ fetch: c.env.CANOPY.fetch.bind(c.env.CANOPY) });
|
|
75
|
+
const user = await canopy.auth.user();
|
|
76
|
+
```
|
|
77
|
+
|
|
46
78
|
## auth.logout()
|
|
47
79
|
|
|
48
80
|
Ends the platform session for the current visitor. Their next request hits the
|
package/dist/docs/sdk-mcp.md
CHANGED
|
@@ -66,8 +66,8 @@ export default {
|
|
|
66
66
|
```
|
|
67
67
|
|
|
68
68
|
**Returns** `(request, env?, executionCtx?) => Promise<Response>`. Pass the
|
|
69
|
-
backend's `env` so handlers and route tools see `env.USER`, `env.
|
|
70
|
-
your secrets.
|
|
69
|
+
backend's `env` so handlers and route tools see `env.USER`, `env.CALLER`,
|
|
70
|
+
`env.CANOPY` and your secrets.
|
|
71
71
|
|
|
72
72
|
**Options:** `{ app?, name?, version? }`. `app` is required when the list has
|
|
73
73
|
route tools: they run its routes.
|
|
@@ -197,8 +197,9 @@ app.get("/api/notes", (c) => {
|
|
|
197
197
|
```
|
|
198
198
|
|
|
199
199
|
**Returns** `McpCaller`: `{ user: McpUser | null, agent: boolean }`.
|
|
200
|
-
`user` comes from `env.USER
|
|
201
|
-
platform attached to the
|
|
200
|
+
`user` comes from `env.USER`, and `agent` from `env.CALLER` when the backend
|
|
201
|
+
has it. Without `env`, both fall back to what the platform attached to the
|
|
202
|
+
request.
|
|
202
203
|
|
|
203
204
|
## When the tool list is wrong
|
|
204
205
|
|
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
import { t as version } from "./package-
|
|
1
|
+
import { t as version } from "./package-C1xCnyOB.mjs";
|
|
2
2
|
import { n as siteConfig } from "./site-DM2J10CB.mjs";
|
|
3
3
|
import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
|
|
4
|
-
import {
|
|
4
|
+
import { n as agentHeaders } from "./api-ArfD6-t5.mjs";
|
|
5
|
+
import { r as reportErrors } from "./ui-CaHgvvkJ.mjs";
|
|
5
6
|
import { defineCommand } from "citty";
|
|
6
7
|
import { existsSync, mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
7
8
|
import { homedir } from "node:os";
|
|
@@ -62,7 +63,10 @@ function parseFrontmatter(raw) {
|
|
|
62
63
|
/** Bearer header from the deploy token (CI) or the CLI login, if either exists. */
|
|
63
64
|
function authHeaders() {
|
|
64
65
|
const token = process.env["CANOPY_DEPLOY_TOKEN"] || siteConfig().token;
|
|
65
|
-
return token ? {
|
|
66
|
+
return token ? {
|
|
67
|
+
authorization: `Bearer ${token}`,
|
|
68
|
+
...agentHeaders()
|
|
69
|
+
} : {};
|
|
66
70
|
}
|
|
67
71
|
async function fetchRes(url) {
|
|
68
72
|
try {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as projectDirArgs, c as resolveProject } from "./site-DM2J10CB.mjs";
|
|
2
|
-
import {
|
|
3
|
-
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-
|
|
2
|
+
import { a as apiRequest, l as withContext } from "./api-ArfD6-t5.mjs";
|
|
3
|
+
import { a as unwrap, i as requireTty, r as reportErrors, t as formatDateTime } from "./ui-CaHgvvkJ.mjs";
|
|
4
4
|
import { defineCommand } from "citty";
|
|
5
5
|
import * as p from "@clack/prompts";
|
|
6
6
|
//#region src/egress.ts
|