@mediamonks/canopy-cli 0.7.21 → 0.7.22

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.
@@ -77,11 +77,6 @@
77
77
  "title": "CLI command reference",
78
78
  "summary": "Every canopy command, its flags, and what it does."
79
79
  },
80
- {
81
- "slug": "notifications",
82
- "title": "Slack notifications",
83
- "summary": "Canopy DMs you about your reports and projects — replies, new reports, blocked fetches, budget hits, failing schedules — and answers questions when you ask."
84
- },
85
80
  {
86
81
  "slug": "static-serving",
87
82
  "title": "How static files are served",
@@ -129,6 +124,11 @@
129
124
  "title": "Google",
130
125
  "summary": "Let your project work with Google data — Sheets, Docs, Calendar, Drive, Gmail — without handling any OAuth yourself."
131
126
  },
127
+ {
128
+ "slug": "platform-slack",
129
+ "title": "Slack",
130
+ "summary": "Give your project its own Slack bot — Canopy creates and manages the app, your code handles the events."
131
+ },
132
132
  {
133
133
  "slug": "platform-members-and-access",
134
134
  "title": "Members & access",
@@ -139,6 +139,16 @@
139
139
  "title": "Feedback & bug reports",
140
140
  "summary": "A built-in feedback widget on every project, a ticket-lite inbox behind it, and a CLI that lets agents work the backlog."
141
141
  },
142
+ {
143
+ "slug": "platform-notifications",
144
+ "title": "Notifications",
145
+ "summary": "Canopy DMs you on Slack about your reports and projects — replies, new reports, blocked fetches, budget hits, failing schedules."
146
+ },
147
+ {
148
+ "slug": "platform-assistant",
149
+ "title": "The Canopy assistant",
150
+ "summary": "Ask questions about your projects and the platform in plain language — in Slack, or from the button in the corner of the docs and the dashboard."
151
+ },
142
152
  {
143
153
  "slug": "platform-client-access",
144
154
  "title": "Client access",
@@ -323,6 +333,11 @@
323
333
  "title": "Google",
324
334
  "summary": "Call Google APIs as the project's connected account or as the visitor's own — the platform holds the OAuth."
325
335
  },
336
+ {
337
+ "slug": "sdk-slack",
338
+ "title": "Slack",
339
+ "summary": "Handle your bot's events and slash commands in your worker, and post to Slack as the bot — no tokens in project code."
340
+ },
326
341
  {
327
342
  "slug": "sdk-services",
328
343
  "title": "Services",
@@ -0,0 +1,50 @@
1
+ ---
2
+ title: The Canopy assistant
3
+ description: Ask questions about your projects and the platform in plain language — in Slack, or from the button in the corner of the docs and the dashboard.
4
+ ---
5
+
6
+ Canopy has an assistant that answers questions about **your projects** and
7
+ **how the platform works**. It reads the same things you can see — your
8
+ projects, their deploys, reports, AI spend, egress log and schedules — plus
9
+ these docs, and answers with a link to the guide it answered from.
10
+
11
+ It can only read. It never deploys, rolls back, changes settings or touches
12
+ your data; asked to, it points you at the dashboard or the `canopy` CLI.
13
+
14
+ ## Where to ask
15
+
16
+ **In the docs and the dashboard.** The floating button in the bottom-right
17
+ corner opens a choice: send feedback, or ask the assistant. Pick the second
18
+ and type your question. The page you're on sets the context, so on a project's
19
+ dashboard page "how many deploys today?" already knows which project you mean.
20
+
21
+ Your conversation survives a reload — come back to the tab an hour later and
22
+ it's still there. **New chat** clears it and starts over. Only one
23
+ conversation is kept, and it lives in your own browser.
24
+
25
+ **In Slack.** DM **Canopy**, or mention **@Canopy** in a channel. Same
26
+ assistant, same answers. In a channel linked to a project it knows which
27
+ project you mean; replying in a report's notification thread, it knows which
28
+ report. See [Notifications](https://docs.canopy.website/platform/notifications).
29
+
30
+ ## What it's good at
31
+
32
+ - **"Why is my fetch failing?"** — it checks the egress log and tells you
33
+ which host was blocked, and where to allow it.
34
+ - **"What happened on my project today?"** — deploys, new reports, status
35
+ changes.
36
+ - **"How do I promote staging to live?"** — it looks it up in these docs
37
+ rather than guessing.
38
+ - **"What's the context on this report?"** — the body, the trail, and the
39
+ triage notes you're allowed to see.
40
+
41
+ ## What it can't do
42
+
43
+ - **Change anything.** Read-only, by design.
44
+ - **See projects you can't.** Every lookup runs as you: a project you're not
45
+ on simply doesn't resolve.
46
+ - **Answer questions about your app's own content.** It knows Canopy and your
47
+ projects on it — not what's inside them.
48
+
49
+ It runs on a small daily budget shared by everyone. On the rare day that runs
50
+ out, it says so and comes back the next.
@@ -18,6 +18,9 @@ boards.
18
18
 
19
19
  - Appears only for **signed-in Monks**. Client-password visitors never see it —
20
20
  the platform doesn't even send them the script.
21
+ - On Canopy's own pages (these docs and the dashboard) the same button also
22
+ opens [the assistant](https://docs.canopy.website/platform/assistant); on your project it is a feedback
23
+ widget and nothing else.
21
24
  - On by default for every project. Turn it off per project in
22
25
  **Settings → Feedback widget**; the change applies on the next page load.
23
26
  - Submissions are always identified (your company sign-in), so there's no
@@ -1,6 +1,6 @@
1
1
  ---
2
- title: Slack notifications
3
- description: Canopy DMs you about your reports and projects — replies, new reports, blocked fetches, budget hits, failing schedules — and answers questions when you ask.
2
+ title: Notifications
3
+ description: Canopy DMs you on Slack about your reports and projects — replies, new reports, blocked fetches, budget hits, failing schedules.
4
4
  ---
5
5
 
6
6
  Canopy sends you Slack DMs when something on the platform needs your
@@ -51,6 +51,10 @@ for that it points you at the dashboard or the CLI. In a channel that's
51
51
  linked to a project, it already knows which project you mean; replying in a
52
52
  report's notification thread, it knows which report.
53
53
 
54
+ The same assistant answers from the button in the corner of these docs and
55
+ the dashboard, if you'd rather not switch to Slack — see
56
+ [The Canopy assistant](https://docs.canopy.website/platform/assistant).
57
+
54
58
  ## If DMs don't arrive
55
59
 
56
60
  The bot finds you by your work email. If your Slack account uses a different
@@ -0,0 +1,85 @@
1
+ ---
2
+ title: Slack
3
+ description: Give your project its own Slack bot — Canopy creates and manages the app, your code handles the events.
4
+ ---
5
+
6
+ Your project can have its own Slack app: its own name and bot, in whatever
7
+ workspace you choose. Canopy creates the app, keeps its settings in sync,
8
+ holds every credential, and routes Slack's traffic to your backend. Your
9
+ code never sees a bot token, a signing secret, or a webhook — it exports a
10
+ handler and calls `canopy.slack`, both covered in
11
+ [the SDK reference](https://docs.canopy.website/sdk/slack).
12
+
13
+ Everything on this page happens on the project's **Services** tab.
14
+
15
+ ## One-time: your configuration token
16
+
17
+ Creating Slack apps requires a Slack _app configuration token_, which is
18
+ personal — it belongs to you, in one workspace. You set it up once and every
19
+ project you manage shares it:
20
+
21
+ 1. Go to [api.slack.com/apps](https://api.slack.com/apps) and pick the
22
+ workspace your bots should live in — **that choice is baked into the
23
+ token**, and it's where every app you create will land.
24
+ 2. Under "Your App Configuration Tokens", generate a token and copy the
25
+ **refresh token** (the one starting `xoxe-1-`). The access token isn't
26
+ needed.
27
+ 3. Paste it into the token section of any project's Services tab and save.
28
+
29
+ Canopy validates it against Slack, stores it encrypted, and keeps it fresh
30
+ from then on — this is genuinely one-time. If it ever stops working (for
31
+ example, it was revoked), existing bots keep running; only app-settings
32
+ changes pause until you paste a fresh one.
33
+
34
+ To create bots in a different workspace — a client's, say — generate a token
35
+ for that workspace and save it. One workspace at a time per person.
36
+
37
+ ## Creating the app
38
+
39
+ Name the bot, pick its features, create. Every app comes with a bot that can
40
+ be invited to channels, post messages, and send people direct messages — the
41
+ notification baseline, no options needed. On top of that:
42
+
43
+ - **AI assistant** — people can chat with the bot: by @-mentioning it in
44
+ channels, in direct messages, and in Slack's assistant pane. Without it
45
+ the bot only sends; nobody can talk back. One caveat from Slack's side:
46
+ once on, it can't be turned off again.
47
+ - **Slash commands** — each becomes a workspace command handled by your
48
+ backend. Command names aren't reserved per app, so pick ones another app
49
+ in the workspace isn't already using.
50
+
51
+ ## Installing to the workspace
52
+
53
+ Creating the app doesn't put it in the workspace — installing does, and
54
+ that's a human step by design: you approve the bot's permissions on Slack's
55
+ own screen. Click **Install to workspace** and follow it through.
56
+
57
+ If your workspace requires admin approval for new apps, the first attempt
58
+ becomes an approval request instead. Once the admin approves (Slack DMs you
59
+ about it), come back and click install again.
60
+
61
+ The same button reappears whenever you change features in a way that needs
62
+ new permissions — the card shows a clear warning when saved changes are
63
+ waiting on a reinstall. Feature changes that don't widen permissions apply
64
+ immediately.
65
+
66
+ ## Day to day
67
+
68
+ - **The status line** shows where the app is installed and when Slack last
69
+ delivered something — the quickest "is it wired up" check.
70
+ - **Enabled** is the kill switch: off, Slack traffic is accepted and
71
+ dropped, nothing reaches your code, and outbound sends are refused. The
72
+ app stays installed; flip it back anytime.
73
+ - **Remove Slack app** deletes the app from the workspace too — bot,
74
+ commands, history access, all of it.
75
+
76
+ Every message your project sends through the bot is counted per day, so a
77
+ runaway loop is visible.
78
+
79
+ ## Local development
80
+
81
+ Slack needs a public URL to deliver to, so `canopy dev` can't receive
82
+ events or commands. Test inbound handling on a deployed project — a scratch
83
+ project with a scratch bot works well — and unit-test your handler directly.
84
+ Outbound (`canopy.slack.api`) works from `canopy dev` like any other SDK
85
+ call.
@@ -0,0 +1,132 @@
1
+ ---
2
+ title: Slack
3
+ description: Handle your bot's events and slash commands in your worker, and post to Slack as the bot — no tokens in project code.
4
+ ---
5
+
6
+ Once your project has a Slack app ([set up on the Services
7
+ tab](https://docs.canopy.website/platform/slack)), the integration is two halves: inbound events arrive
8
+ at a `slack()` export in your worker, and outbound calls go through
9
+ `canopy.slack`. The bot token and signing secret stay with the platform —
10
+ Slack's signatures are verified before anything reaches your code.
11
+
12
+ ## The `slack()` handler
13
+
14
+ Export it next to `fetch` and `scheduled`:
15
+
16
+ ```ts
17
+ export default {
18
+ fetch(request, env) {
19
+ /* … */
20
+ },
21
+
22
+ async slack(event, env) {
23
+ if (event.kind === "command") {
24
+ return { response_type: "in_channel", text: `Running ${event.payload.command}…` };
25
+ }
26
+ // kind === "event": an @-mention, a DM, …
27
+ },
28
+ };
29
+ ```
30
+
31
+ **The event object:**
32
+
33
+ - `kind` — `"event"` (Events API: `app_mention`, `message`, …) or
34
+ `"command"` (a slash command).
35
+ - `payload` — Slack's payload, verbatim. For events that's the full
36
+ callback (the interesting part is `payload.event`); for commands it's the
37
+ command form fields (`command`, `text`, `channel_id`, `user_id`, …).
38
+ - `responseUrl` — commands only: where a late reply can still be sent (see
39
+ below).
40
+ - `user` — who triggered this, as Slack tells it: `{ id, name? }`. Present
41
+ whenever Slack names a person.
42
+ - `teamId`, `appId` — which workspace and app this came from.
43
+ - `retry` — set when Slack is redelivering. Delivery is at-least-once, so
44
+ handlers that create things should tolerate seeing an event twice.
45
+
46
+ ## Who is talking to the bot
47
+
48
+ Two identities, at two trust levels:
49
+
50
+ - **`env.USER`** is set when the bot lives in the Monks workspace and the
51
+ sender has a Canopy account: the same `{ sub, email, name }` your fetch
52
+ routes see for a signed-in visitor, so user-specific logic works the same
53
+ way. The mapping rides the corporate directory — Slack accounts and Canopy
54
+ sign-ins are the same people there.
55
+ - In any other workspace — a client's, say — `env.USER` is `null` and
56
+ `event.user` is all you get: Slack's own word for who typed, nothing
57
+ Canopy has verified. Fine for personalising a reply; don't hang
58
+ authorization on it.
59
+
60
+ Either way, payload text is written by whoever typed it in Slack — treat it
61
+ as untrusted input. And SDK calls from the handler still run as the project
62
+ itself (`canopy.auth.user()` reports kind `worker`), regardless of
63
+ `env.USER`.
64
+
65
+ ## Answering a slash command
66
+
67
+ Slack gives a command three seconds. Return quickly and Canopy relays your
68
+ answer as the command's response:
69
+
70
+ ```ts
71
+ async slack(event, env) {
72
+ if (event.kind === "command") {
73
+ return { response_type: "ephemeral", text: "Done." };
74
+ }
75
+ }
76
+ ```
77
+
78
+ Return a Slack message object, or a plain string (sent as ephemeral text).
79
+ Return nothing and the command completes silently.
80
+
81
+ If your handler needs longer — it queries something slow, calls a model —
82
+ the command is acknowledged for you, and you answer afterwards through the
83
+ `responseUrl`, which stays valid for 30 minutes:
84
+
85
+ ```ts
86
+ if (event.kind === "command") {
87
+ const answer = await somethingSlow();
88
+ await canopy.slack.respond(event.responseUrl!, {
89
+ response_type: "in_channel",
90
+ text: answer,
91
+ });
92
+ }
93
+ ```
94
+
95
+ Events (`kind: "event"`) have no response channel — react by calling out,
96
+ usually `chat.postMessage` back into the conversation the event names.
97
+
98
+ ## slack.api(method, args)
99
+
100
+ Any [Slack Web API method](https://docs.slack.dev/reference/methods), called
101
+ as your project's bot:
102
+
103
+ ```ts
104
+ const result = await canopy.slack.api("chat.postMessage", {
105
+ channel: "C0123456789",
106
+ text: "Deploy finished.",
107
+ });
108
+ if (!result.ok) console.warn("slack said no:", result.error);
109
+ ```
110
+
111
+ The response is Slack's own, `ok: false` included — check it for
112
+ method-level outcomes like `channel_not_found` or `not_in_channel` (the bot
113
+ can only post to channels it's been invited to, and to people's DMs).
114
+ Workspace-management method families (`admin.*`, `tooling.*`, `apps.*`,
115
+ `oauth.*`) are refused before Slack is ever contacted; everything else is
116
+ bounded by the permissions the app was installed with.
117
+
118
+ Backend and `canopy dev` only — pages can't call it, so a visitor can never
119
+ act as your bot.
120
+
121
+ ## slack.respond(responseUrl, message)
122
+
123
+ The late reply to a slash command, described above. `message` is a Slack
124
+ message object or a plain string.
125
+
126
+ ## Errors
127
+
128
+ Refusals arrive as thrown errors with stable codes: `slack_not_configured`
129
+ (no app on this project), `slack_not_installed` (created but not installed
130
+ to a workspace), `slack_disabled` (switched off on the Services tab), and
131
+ `slack_method_not_allowed`. A network-level failure reaching Slack is
132
+ `slack_api_error`.
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-C_DLlB96.mjs";
1
+ import { t as version } from "./package-BuwV_mje.mjs";
2
2
  import { n as siteConfig } from "./site-BFaWfSVu.mjs";
3
3
  import { i as latestKnownVersion, r as compareSemver } from "./version-check-CcsA1QjI.mjs";
4
4
  import { r as reportErrors } from "./ui--atbZ-TM.mjs";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-C_DLlB96.mjs";
1
+ import { t as version } from "./package-BuwV_mje.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-BFaWfSVu.mjs";
3
3
  import { c as apiDownload, l as apiRequest, r as reportErrors, t as formatDateTime, u as apiUpload } from "./ui--atbZ-TM.mjs";
4
4
  import { defineCommand } from "citty";
@@ -89,6 +89,10 @@ function firstLine(body) {
89
89
  const line = body.split("\n")[0] ?? "";
90
90
  return line.length > 72 ? `${line.slice(0, 71)}…` : line;
91
91
  }
92
+ /** List label: the AI-written title when there is one, else the first line. */
93
+ function itemLabel(item) {
94
+ return firstLine(item.title || item.body);
95
+ }
92
96
  function printItems(items) {
93
97
  if (items.length === 0) {
94
98
  console.log("No matching feedback items.");
@@ -97,7 +101,7 @@ function printItems(items) {
97
101
  for (const item of items) {
98
102
  const comments = item.comment_count ? ` 💬${item.comment_count}` : "";
99
103
  const shots = item.attachment_count ? ` 📎${item.attachment_count}` : "";
100
- console.log(`${item.id} ${item.status} ${item.kind} ${firstLine(item.body)}${comments}${shots}`);
104
+ console.log(`${item.id} ${item.status} ${item.kind} ${itemLabel(item)}${comments}${shots}`);
101
105
  }
102
106
  }
103
107
  const listCommand = defineCommand({
@@ -153,6 +157,7 @@ const showCommand = defineCommand({
153
157
  return;
154
158
  }
155
159
  const { item, comments } = detail;
160
+ if (item.title) console.log(item.title);
156
161
  console.log(`${item.kind} · ${item.status} · via ${item.source} · ${formatDateTime(item.created_at)}`);
157
162
  console.log(`reported by ${item.reporter_name || item.reporter_email || "unknown"}` + (item.deployment_id ? ` on ${item.deployment_id}` : "") + (item.channel === "staging" ? " (staging)" : ""));
158
163
  console.log(`\n${item.body}\n`);
@@ -276,7 +281,7 @@ const mineCommand = defineCommand({
276
281
  console.log("You haven't filed any reports.");
277
282
  return;
278
283
  }
279
- for (const item of items) console.log(`${item.id} ${item.status} ${item.kind} [${item.project_slug}] ${firstLine(item.body)}`);
284
+ for (const item of items) console.log(`${item.id} ${item.status} ${item.kind} [${item.project_slug}] ${itemLabel(item)}`);
280
285
  })
281
286
  });
282
287
  const reportCommand = defineCommand({
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-C_DLlB96.mjs";
2
+ import { t as version } from "./package-BuwV_mje.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-BFaWfSVu.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -60,9 +60,9 @@ const main = defineCommand({
60
60
  description: "canopy — create and deploy vibecoded projects"
61
61
  },
62
62
  subCommands: {
63
- login: () => import("./login-DRYaJcfT.mjs").then((m) => m.loginCommand),
64
- whoami: () => import("./login-DRYaJcfT.mjs").then((m) => m.whoamiCommand),
65
- logout: () => import("./login-DRYaJcfT.mjs").then((m) => m.logoutCommand),
63
+ login: () => import("./login-BrEDKrb3.mjs").then((m) => m.loginCommand),
64
+ whoami: () => import("./login-BrEDKrb3.mjs").then((m) => m.whoamiCommand),
65
+ logout: () => import("./login-BrEDKrb3.mjs").then((m) => m.logoutCommand),
66
66
  create: () => import("./projects-1r5387ui.mjs").then((m) => m.createCommand),
67
67
  init: () => import("./projects-1r5387ui.mjs").then((m) => m.initCommand),
68
68
  projects: () => import("./projects-1r5387ui.mjs").then((m) => m.projectsCommand),
@@ -76,13 +76,13 @@ const main = defineCommand({
76
76
  token: () => import("./token-C8Bmzwfl.mjs").then((m) => m.tokenCommand),
77
77
  egress: () => import("./egress-BhbQfjjV.mjs").then((m) => m.egressCommand),
78
78
  schedule: () => import("./schedule-DK_Cv6Xf.mjs").then((m) => m.scheduleCommand),
79
- feedback: () => import("./feedback-CAwUHwWa.mjs").then((m) => m.feedbackCommand),
79
+ feedback: () => import("./feedback-QhD8k6Ph.mjs").then((m) => m.feedbackCommand),
80
80
  db: () => import("./db-TaSLpQZ5.mjs").then((m) => m.dbCommand),
81
- usage: () => import("./usage-CRpVLLi6.mjs").then((m) => m.usageCommand),
81
+ usage: () => import("./usage-C1-cvS2-.mjs").then((m) => m.usageCommand),
82
82
  open: () => import("./open-D7f1wNn3.mjs").then((m) => m.openCommand),
83
- docs: () => import("./docs-fiC6q9Ce.mjs").then((m) => m.docsCommand),
84
- skill: () => import("./skill-CmgMU2Iq.mjs").then((m) => m.skillCommand),
85
- upgrade: () => import("./upgrade-D1j0WV4l.mjs").then((m) => m.upgradeCommand),
83
+ docs: () => import("./docs-c0CdeBxh.mjs").then((m) => m.docsCommand),
84
+ skill: () => import("./skill-BvoYCIKk.mjs").then((m) => m.skillCommand),
85
+ upgrade: () => import("./upgrade-0lmdjvom.mjs").then((m) => m.upgradeCommand),
86
86
  logs: () => import("./logs-Beqz--Df.mjs").then((m) => m.logsCommand)
87
87
  }
88
88
  });