@mediamonks/canopy-cli 0.7.21 → 0.7.23
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/dist/dev-BFPlhkzp.mjs +2 -0
- package/dist/{dev-CNqx_53I.mjs → dev-XULukupE.mjs} +3 -3
- package/dist/{dev-worker-Bij9OUMk.mjs → dev-worker-BDMjExps.mjs} +35 -1
- package/dist/docs/cli-login.md +2 -0
- package/dist/docs/cli-logout.md +9 -6
- package/dist/docs/index.json +27 -7
- package/dist/docs/platform-ai-usage.md +36 -10
- package/dist/docs/platform-assistant.md +50 -0
- package/dist/docs/platform-client-access.md +5 -2
- package/dist/docs/platform-feedback.md +3 -0
- package/dist/docs/platform-members-and-access.md +29 -6
- package/dist/docs/{notifications.md → platform-notifications.md} +6 -2
- package/dist/docs/platform-security-headers.md +53 -0
- package/dist/docs/platform-slack.md +87 -0
- package/dist/docs/sdk-ai.md +102 -11
- package/dist/docs/sdk-files.md +6 -0
- package/dist/docs/sdk-slack.md +132 -0
- package/dist/{docs-fiC6q9Ce.mjs → docs-BDPL0TI3.mjs} +1 -1
- package/dist/{feedback-CAwUHwWa.mjs → feedback-B3pnUPLf.mjs} +8 -3
- package/dist/index.mjs +10 -10
- package/dist/{local-CsSP4Vpt.mjs → local-BjsuI1IB.mjs} +37 -8
- package/dist/login-By4y5O5-.mjs +336 -0
- package/dist/{package-C_DLlB96.mjs → package-C5rZwlw8.mjs} +1 -1
- package/dist/{skill-CmgMU2Iq.mjs → skill-CgToW_Ac.mjs} +1 -1
- package/dist/{src-BKKAEkbH.mjs → src-BPKfY9IN.mjs} +53 -1
- package/dist/{upgrade-D1j0WV4l.mjs → upgrade-Bl_NJ_m5.mjs} +1 -1
- package/dist/{usage-CRpVLLi6.mjs → usage-C1-cvS2-.mjs} +13 -1
- package/package.json +3 -3
- package/dist/dev-D7v7WK9O.mjs +0 -2
- package/dist/login-DRYaJcfT.mjs +0 -285
package/dist/docs/sdk-ai.md
CHANGED
|
@@ -4,8 +4,8 @@ description: Metered LLM access through the project's budget-capped gateway key
|
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
`canopy.ai` gives your project LLM access without any API keys in your code.
|
|
7
|
-
Every request authenticates as the project and is metered against
|
|
8
|
-
per-project
|
|
7
|
+
Every request authenticates as the project and is metered against hard
|
|
8
|
+
per-project budgets — spend, limits, and how prompts are handled by the
|
|
9
9
|
model providers are covered in [AI access & usage](https://docs.canopy.website/platform/ai-usage).
|
|
10
10
|
|
|
11
11
|
## ai.chat(options)
|
|
@@ -32,16 +32,65 @@ res.usage; // { inputTokens: number, outputTokens: number }
|
|
|
32
32
|
**`ChatOptions`:**
|
|
33
33
|
|
|
34
34
|
- `messages` — required, non-empty array of `{ role, content }`. `role` is
|
|
35
|
-
`"system" | "user" | "assistant"`; `content` is a non-empty string
|
|
35
|
+
`"system" | "user" | "assistant"`; `content` is a non-empty string, or an
|
|
36
|
+
array of content parts to attach documents and images (below).
|
|
36
37
|
- `model` — optional `"{provider}/{model}"` id. Omit it and the platform's
|
|
37
38
|
default model answers. Ask for a model whose provider isn't enabled and the
|
|
38
39
|
call fails with a message listing what _is_ available; `ai.endpoint()`
|
|
39
40
|
returns the full curated list.
|
|
40
|
-
- `temperature` — optional number between 0 and 2.
|
|
41
|
-
-
|
|
41
|
+
- `temperature` — optional number between 0 and 2. Reasoning models (the
|
|
42
|
+
GPT-5 family) only accept their default temperature, so the platform drops
|
|
43
|
+
this setting for them rather than failing the call — it has effect on
|
|
44
|
+
Claude models.
|
|
45
|
+
- `maxTokens` — optional integer, at least 1. On reasoning models the budget
|
|
46
|
+
also covers the model's internal reasoning, so a very small value can
|
|
47
|
+
return an empty answer — leave headroom.
|
|
48
|
+
- `effort` — optional `"low" | "medium" | "high"`: how hard a reasoning model
|
|
49
|
+
should think. Lowered onto each provider's own dial; models without an
|
|
50
|
+
effort dial (Claude Haiku, for one) simply ignore it. Lower effort is
|
|
51
|
+
faster and cheaper, higher is more thorough.
|
|
52
|
+
|
|
53
|
+
**`ChatResponse`:** `{ message, model, usage: { inputTokens, outputTokens } }`.
|
|
54
|
+
`message.role` is `"assistant"` and `message.content` is always a plain
|
|
55
|
+
string, whatever went in.
|
|
56
|
+
|
|
57
|
+
## Attaching documents and images
|
|
58
|
+
|
|
59
|
+
A message's `content` can be an array of parts instead of a string. The
|
|
60
|
+
easiest way to send a file is to upload it to [blob storage](https://docs.canopy.website/sdk/blob) once
|
|
61
|
+
and reference it by key — the platform fetches it server-side and picks the
|
|
62
|
+
best way to hand it to the model:
|
|
42
63
|
|
|
43
|
-
|
|
44
|
-
|
|
64
|
+
```ts
|
|
65
|
+
await canopy.blob.put("briefs/q3.pdf", file); // once
|
|
66
|
+
|
|
67
|
+
const res = await canopy.ai.chat({
|
|
68
|
+
messages: [
|
|
69
|
+
{
|
|
70
|
+
role: "user",
|
|
71
|
+
content: [
|
|
72
|
+
{ type: "text", text: "Summarize this briefing." },
|
|
73
|
+
{ type: "blob", key: "briefs/q3.pdf" },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
The part types:
|
|
81
|
+
|
|
82
|
+
- `{ type: "text", text }` — plain text.
|
|
83
|
+
- `{ type: "blob", key, mediaType? }` — a file already in the project's blob
|
|
84
|
+
storage, by key. `mediaType` overrides the stored content type if needed.
|
|
85
|
+
**Prefer this** for anything you'd otherwise base64 by hand.
|
|
86
|
+
- `{ type: "file", data, mediaType, filename? }` — an inline file;
|
|
87
|
+
`data` is base64 (no `data:` prefix), `mediaType` e.g. `"application/pdf"`.
|
|
88
|
+
- `{ type: "image", data, mediaType }` — an inline image, same encoding.
|
|
89
|
+
|
|
90
|
+
Pick a model that can read what you attach — document and image understanding
|
|
91
|
+
varies per model. The whole request (inline base64 and referenced blobs
|
|
92
|
+
together) must stay under the 8 MB request limit — see
|
|
93
|
+
[AI access & usage](https://docs.canopy.website/platform/ai-usage).
|
|
45
94
|
|
|
46
95
|
## ai.chatStream(options)
|
|
47
96
|
|
|
@@ -70,9 +119,10 @@ let an ecosystem client handle streaming for you.
|
|
|
70
119
|
Returns the project's OpenAI-compatible endpoint info:
|
|
71
120
|
|
|
72
121
|
```ts
|
|
73
|
-
const { baseUrl, models } = await canopy.ai.endpoint();
|
|
122
|
+
const { baseUrl, models, native } = await canopy.ai.endpoint();
|
|
74
123
|
// baseUrl — an OpenAI-compatible base URL
|
|
75
124
|
// models — the curated model list, "{provider}/{model}" ids; models[0] is the default
|
|
125
|
+
// native — provider-native base URLs: { anthropic, openai }
|
|
76
126
|
```
|
|
77
127
|
|
|
78
128
|
Point any OpenAI-compatible client — the Vercel AI SDK, an OpenAI SDK, a
|
|
@@ -87,15 +137,56 @@ const openai = createOpenAI({ baseURL: baseUrl, apiKey: "unused" });
|
|
|
87
137
|
const model = openai(models[0]);
|
|
88
138
|
```
|
|
89
139
|
|
|
140
|
+
## Provider-native endpoints
|
|
141
|
+
|
|
142
|
+
The compat endpoint speaks OpenAI's chat-completions dialect to every model —
|
|
143
|
+
portable, but the lowest common denominator. When you want a provider's own
|
|
144
|
+
protocol — Anthropic prompt caching, PDFs with citations, or OpenAI's
|
|
145
|
+
Responses API — use the native base URLs from `endpoint()`. Requests pass
|
|
146
|
+
through to the provider unchanged; auth, the model allowlist, the daily
|
|
147
|
+
budget, and metering all still apply.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
151
|
+
|
|
152
|
+
const { native } = await canopy.ai.endpoint();
|
|
153
|
+
const anthropic = new Anthropic({ baseURL: native.anthropic, apiKey: "unused" });
|
|
154
|
+
const msg = await anthropic.messages.create({
|
|
155
|
+
model: "claude-haiku-4-5", // bare or "anthropic/…" — must be on the curated list
|
|
156
|
+
max_tokens: 1024,
|
|
157
|
+
messages: [{ role: "user", content: "Hello" }],
|
|
158
|
+
});
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
A base URL is just a prefix — the client library appends the endpoint path
|
|
162
|
+
itself, based on which method you call. Both values point at the platform,
|
|
163
|
+
which forwards each path to the provider:
|
|
164
|
+
|
|
165
|
+
- `native.anthropic` — hand it to `@anthropic-ai/sdk` or `@ai-sdk/anthropic`.
|
|
166
|
+
Calling `anthropic.messages.create(...)` makes the SDK request
|
|
167
|
+
`POST {native.anthropic}/v1/messages` — the Messages API.
|
|
168
|
+
- `native.openai` — hand it to the `openai` package or `@ai-sdk/openai`. It
|
|
169
|
+
already ends in `/v1`, matching OpenAI's own base URL convention; the
|
|
170
|
+
method you call picks the endpoint: `client.responses.create(...)` requests
|
|
171
|
+
`POST {native.openai}/responses` (the Responses API) and
|
|
172
|
+
`client.chat.completions.create(...)` requests
|
|
173
|
+
`POST {native.openai}/chat/completions`. Both are supported.
|
|
174
|
+
|
|
175
|
+
Pick the surface by what you need: `ai.chat()` for plain text (and simple
|
|
176
|
+
attachments), the compat endpoint for portable multi-model apps and ecosystem
|
|
177
|
+
tooling, a native endpoint for provider-specific capabilities. Moving between
|
|
178
|
+
them never changes auth, budgets, or what the Usage tab shows.
|
|
179
|
+
|
|
90
180
|
## Errors
|
|
91
181
|
|
|
92
182
|
Failed calls throw `CanopyError` with the HTTP `status` and the response body
|
|
93
183
|
as the `message`. Two worth handling:
|
|
94
184
|
|
|
95
185
|
- **Budget exhausted** — status `429`, body contains
|
|
96
|
-
`"ai_budget_exceeded"
|
|
97
|
-
the next day
|
|
98
|
-
your app keeps
|
|
186
|
+
`"ai_budget_exceeded"` and names which budget tripped: the daily limit
|
|
187
|
+
(calls resume the next day) or the monthly cap (calls resume next month —
|
|
188
|
+
or after a maintainer raises it). Everything else in your app keeps
|
|
189
|
+
working.
|
|
99
190
|
- **Invalid request** — status `400`, body contains `"invalid_chat"` and a
|
|
100
191
|
detail explaining which option was rejected.
|
|
101
192
|
|
package/dist/docs/sdk-files.md
CHANGED
|
@@ -62,6 +62,12 @@ lifetime that isn't a whole number of seconds above zero is rejected. Keep the
|
|
|
62
62
|
window as short as the job needs — anyone holding the URL can download the
|
|
63
63
|
file until it expires.
|
|
64
64
|
|
|
65
|
+
What a URL does in the browser depends on the file's content type. Images,
|
|
66
|
+
video, audio, fonts, plain text, CSV, JSON and PDF display; SVG displays in
|
|
67
|
+
an `<img>` but is sandboxed when opened directly; anything a browser would
|
|
68
|
+
treat as a page — HTML, XML, scripts, unknown types — downloads instead of
|
|
69
|
+
rendering. See [Browser security](https://docs.canopy.website/platform/security-headers).
|
|
70
|
+
|
|
65
71
|
## Listing and deleting
|
|
66
72
|
|
|
67
73
|
```ts
|
|
@@ -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-
|
|
1
|
+
import { t as version } from "./package-C5rZwlw8.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-
|
|
1
|
+
import { t as version } from "./package-C5rZwlw8.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} ${
|
|
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}] ${
|
|
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-
|
|
2
|
+
import { t as version } from "./package-C5rZwlw8.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,14 +60,14 @@ const main = defineCommand({
|
|
|
60
60
|
description: "canopy — create and deploy vibecoded projects"
|
|
61
61
|
},
|
|
62
62
|
subCommands: {
|
|
63
|
-
login: () => import("./login-
|
|
64
|
-
whoami: () => import("./login-
|
|
65
|
-
logout: () => import("./login-
|
|
63
|
+
login: () => import("./login-By4y5O5-.mjs").then((m) => m.loginCommand),
|
|
64
|
+
whoami: () => import("./login-By4y5O5-.mjs").then((m) => m.whoamiCommand),
|
|
65
|
+
logout: () => import("./login-By4y5O5-.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),
|
|
69
69
|
delete: () => import("./projects-1r5387ui.mjs").then((m) => m.deleteCommand),
|
|
70
|
-
dev: () => import("./dev-
|
|
70
|
+
dev: () => import("./dev-BFPlhkzp.mjs").then((m) => m.devCommand),
|
|
71
71
|
deploy: () => import("./deploy-DCggsLD7.mjs").then((m) => m.deployCommand),
|
|
72
72
|
promote: () => import("./deploy-DCggsLD7.mjs").then((m) => m.promoteCommand),
|
|
73
73
|
staging: () => import("./staging-CdpV2YHB.mjs").then((m) => m.stagingCommand),
|
|
@@ -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-
|
|
79
|
+
feedback: () => import("./feedback-B3pnUPLf.mjs").then((m) => m.feedbackCommand),
|
|
80
80
|
db: () => import("./db-TaSLpQZ5.mjs").then((m) => m.dbCommand),
|
|
81
|
-
usage: () => import("./usage-
|
|
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-
|
|
84
|
-
skill: () => import("./skill-
|
|
85
|
-
upgrade: () => import("./upgrade-
|
|
83
|
+
docs: () => import("./docs-BDPL0TI3.mjs").then((m) => m.docsCommand),
|
|
84
|
+
skill: () => import("./skill-CgToW_Ac.mjs").then((m) => m.skillCommand),
|
|
85
|
+
upgrade: () => import("./upgrade-Bl_NJ_m5.mjs").then((m) => m.upgradeCommand),
|
|
86
86
|
logs: () => import("./logs-Beqz--Df.mjs").then((m) => m.logsCommand)
|
|
87
87
|
}
|
|
88
88
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { s as apiDataRequest } from "./ui--atbZ-TM.mjs";
|
|
2
|
-
import { n as META_COLUMNS, r as blobRouter, t as dbRouter } from "./src-
|
|
3
|
-
import { n as rewriteApiPath } from "./dev-
|
|
2
|
+
import { n as META_COLUMNS, r as blobRouter, t as dbRouter } from "./src-BPKfY9IN.mjs";
|
|
3
|
+
import { n as rewriteApiPath } from "./dev-XULukupE.mjs";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { dirname, join, relative, sep } from "node:path";
|
|
@@ -15,19 +15,48 @@ import { Hono } from "hono";
|
|
|
15
15
|
* - `fts: false` — search() is an ILIKE substring fallback: different
|
|
16
16
|
* matches, no ranking (results order by created_at). Surfaced to clients
|
|
17
17
|
* as `capabilities.fts` rather than papered over.
|
|
18
|
-
* - patch()
|
|
19
|
-
*
|
|
20
|
-
*
|
|
18
|
+
* - patch() has the SAME RFC 7386 semantics as SQLite's json_patch (nested
|
|
19
|
+
* objects merge, null removes a key), but Postgres has no built-in for it,
|
|
20
|
+
* so it goes through `public._canopy_merge_patch(jsonb, jsonb)` — a small
|
|
21
|
+
* recursive plpgsql function declared in the DDL below. It lives in
|
|
22
|
+
* `public` (not the per-project schema) so one CREATE OR REPLACE covers
|
|
23
|
+
* every project schema in the database, existing ones included.
|
|
21
24
|
*/
|
|
22
25
|
const DOC_COLUMNS$1 = "id, data, created_at, updated_at";
|
|
23
|
-
const DOC_STORE_DDL_PG = [
|
|
26
|
+
const DOC_STORE_DDL_PG = [
|
|
27
|
+
`CREATE TABLE IF NOT EXISTS _canopy_docs (
|
|
24
28
|
collection TEXT NOT NULL,
|
|
25
29
|
id TEXT NOT NULL,
|
|
26
30
|
data TEXT NOT NULL,
|
|
27
31
|
created_at TEXT NOT NULL,
|
|
28
32
|
updated_at TEXT NOT NULL,
|
|
29
33
|
PRIMARY KEY (collection, id)
|
|
30
|
-
)`,
|
|
34
|
+
)`,
|
|
35
|
+
`CREATE INDEX IF NOT EXISTS _canopy_docs_created ON _canopy_docs (collection, created_at)`,
|
|
36
|
+
`CREATE OR REPLACE FUNCTION public._canopy_merge_patch(target jsonb, patch jsonb)
|
|
37
|
+
RETURNS jsonb LANGUAGE plpgsql IMMUTABLE AS $fn$
|
|
38
|
+
BEGIN
|
|
39
|
+
IF jsonb_typeof(patch) IS DISTINCT FROM 'object' THEN
|
|
40
|
+
RETURN patch;
|
|
41
|
+
END IF;
|
|
42
|
+
IF jsonb_typeof(target) IS DISTINCT FROM 'object' THEN
|
|
43
|
+
target := '{}'::jsonb;
|
|
44
|
+
END IF;
|
|
45
|
+
RETURN COALESCE((
|
|
46
|
+
SELECT jsonb_object_agg(m.key, m.value) FROM (
|
|
47
|
+
SELECT t.key, t.value FROM jsonb_each(target) t WHERE NOT (patch ? t.key)
|
|
48
|
+
UNION ALL
|
|
49
|
+
SELECT p.key,
|
|
50
|
+
CASE WHEN jsonb_typeof(p.value) = 'object'
|
|
51
|
+
THEN public._canopy_merge_patch(target -> p.key, p.value)
|
|
52
|
+
ELSE p.value END
|
|
53
|
+
FROM jsonb_each(patch) p
|
|
54
|
+
WHERE jsonb_typeof(p.value) <> 'null'
|
|
55
|
+
) m
|
|
56
|
+
), '{}'::jsonb);
|
|
57
|
+
END
|
|
58
|
+
$fn$`
|
|
59
|
+
];
|
|
31
60
|
/** `data`'s JSON value at a `$.a.b` path param, as jsonb. */
|
|
32
61
|
const jsonAt = (column, pathParam) => `(${column}::jsonb #> string_to_array(trim(leading '$.' from ${pathParam}::text), '.'))`;
|
|
33
62
|
/**
|
|
@@ -124,7 +153,7 @@ const postgresDialect = {
|
|
|
124
153
|
},
|
|
125
154
|
patch(collection, id, patchJson, now) {
|
|
126
155
|
return {
|
|
127
|
-
sql: `UPDATE _canopy_docs SET data = (data::jsonb
|
|
156
|
+
sql: `UPDATE _canopy_docs SET data = public._canopy_merge_patch(data::jsonb, $1::jsonb)::text, updated_at = $2
|
|
128
157
|
WHERE collection = $3 AND id = $4
|
|
129
158
|
RETURNING ${DOC_COLUMNS$1}`,
|
|
130
159
|
params: [
|