@mediamonks/canopy-cli 0.7.31 → 0.7.32

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.
@@ -2,6 +2,7 @@ import { a as projectDirArgs, c as resolveProject, l as stagingUrl, n as siteCon
2
2
  import { i as apiRequest, n as apiDataRequest, o as withContext, r as apiDownload } from "./api-C4rMhE7i.mjs";
3
3
  import { a as unwrap, r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
4
4
  import { n as migrateProject, r as readMigrationFiles, t as describeMigrateResult } from "./migrations-Czlcddyz.mjs";
5
+ import { builtinModules } from "node:module";
5
6
  import { defineCommand } from "citty";
6
7
  import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
7
8
  import { tmpdir } from "node:os";
@@ -27,7 +28,7 @@ async function bundleWorker(projectDir, entry) {
27
28
  format: "esm",
28
29
  target: "es2022",
29
30
  packages: "bundle",
30
- external: ["node:*"],
31
+ plugins: [nodeBuiltins],
31
32
  logLevel: "silent"
32
33
  });
33
34
  return new Uint8Array(readFileSync(outfile));
@@ -38,6 +39,115 @@ async function bundleWorker(projectDir, entry) {
38
39
  });
39
40
  }
40
41
  }
42
+ /** Bare names Node resolves to a built-in (`fs`, `fs/promises`, …). */
43
+ const BARE_BUILTINS = new Set(builtinModules.filter((name) => !name.startsWith("_")));
44
+ /**
45
+ * Built-ins both hosts provide, each with a default export: Node, and workerd
46
+ * at the dispatch scripts' compatibility date (2026-05-01, nodejs_compat —
47
+ * probed with a worker importing every one; several are stubs that throw only
48
+ * when called). Missing on workerd: `sea`, `test/reporters`. Left out on
49
+ * purpose: `sqlite` (experimental on Node) and `test`.
50
+ */
51
+ const HOSTED_BUILTINS = /* @__PURE__ */ new Set([
52
+ "assert",
53
+ "assert/strict",
54
+ "async_hooks",
55
+ "buffer",
56
+ "child_process",
57
+ "cluster",
58
+ "console",
59
+ "constants",
60
+ "crypto",
61
+ "dgram",
62
+ "diagnostics_channel",
63
+ "dns",
64
+ "dns/promises",
65
+ "domain",
66
+ "events",
67
+ "fs",
68
+ "fs/promises",
69
+ "http",
70
+ "http2",
71
+ "https",
72
+ "inspector",
73
+ "inspector/promises",
74
+ "module",
75
+ "net",
76
+ "os",
77
+ "path",
78
+ "path/posix",
79
+ "path/win32",
80
+ "perf_hooks",
81
+ "process",
82
+ "punycode",
83
+ "querystring",
84
+ "readline",
85
+ "readline/promises",
86
+ "repl",
87
+ "stream",
88
+ "stream/consumers",
89
+ "stream/promises",
90
+ "stream/web",
91
+ "string_decoder",
92
+ "sys",
93
+ "timers",
94
+ "timers/promises",
95
+ "tls",
96
+ "trace_events",
97
+ "tty",
98
+ "url",
99
+ "util",
100
+ "util/types",
101
+ "v8",
102
+ "vm",
103
+ "wasi",
104
+ "worker_threads",
105
+ "zlib"
106
+ ]);
107
+ const REQUIRE_NAMESPACE = "canopy-node-require";
108
+ /**
109
+ * Node built-ins are the host's, not the bundle's: the GCP runtime is Node and
110
+ * Cloudflare dispatch scripts run with nodejs_compat. A `neutral` build cannot
111
+ * resolve them at all, so every import of one — `node:fs` or bare `fs`, as Node
112
+ * itself treats them — stays external under its `node:` name.
113
+ *
114
+ * A CommonJS `require()` of one cannot simply stay external: in an ESM bundle
115
+ * esbuild turns it into a `__require` shim that throws "Dynamic require of …
116
+ * is not supported" when it runs. Neither host offers a `require` to an ES
117
+ * module, so the call is routed to a tiny ES module that re-exports the
118
+ * built-in through a static `import`. The AI SDK needs this: `ai` reaches
119
+ * `@vercel/oidc`, whose CommonJS build requires `fs`, `os` and `path` at load.
120
+ *
121
+ * That turns a lazy require — often an optional one in a try/catch — into an
122
+ * import made at load, and Cloudflare refuses a script importing a module it
123
+ * lacks. So only HOSTED_BUILTINS are rewritten; a require of any other
124
+ * built-in keeps the throwing shim, which fails only if that line runs.
125
+ */
126
+ const nodeBuiltins = {
127
+ name: "canopy-node-builtins",
128
+ setup(build) {
129
+ build.onResolve({ filter: /^(node:)?[a-z_][a-z0-9_/]*$/ }, (args) => {
130
+ const prefixed = args.path.startsWith("node:");
131
+ const name = prefixed ? args.path.slice(5) : args.path;
132
+ if (!prefixed && !BARE_BUILTINS.has(name)) return void 0;
133
+ if (args.kind === "require-call" && HOSTED_BUILTINS.has(name)) return {
134
+ path: name,
135
+ namespace: REQUIRE_NAMESPACE
136
+ };
137
+ return {
138
+ path: `node:${name}`,
139
+ external: true
140
+ };
141
+ });
142
+ build.onLoad({
143
+ filter: /.*/,
144
+ namespace: REQUIRE_NAMESPACE
145
+ }, (args) => ({
146
+ contents: `export * from "node:${args.path}";\nexport { default } from "node:${args.path}";\n`,
147
+ loader: "js"
148
+ }));
149
+ }
150
+ };
41
151
  //#endregion
42
152
  //#region src/deploy.ts
43
153
  /**
@@ -101,7 +101,7 @@ wrote for another MCP server carry over as they are.
101
101
  A tool set you already use with the AI SDK works too. Put it in the same list:
102
102
 
103
103
  ```ts
104
- import { tool } from "@ai-sdk/provider-utils";
104
+ import { tool } from "ai";
105
105
 
106
106
  const aiTools = {
107
107
  weather: tool({
@@ -114,9 +114,7 @@ const aiTools = {
114
114
  mountMcpTools(app, [...myTools, aiTools]);
115
115
  ```
116
116
 
117
- Import `tool` from `@ai-sdk/provider-utils` in a backend: the full `ai`
118
- package doesn't bundle for Canopy backends yet. Tools without `execute` (ones
119
- the AI provider runs itself) are left out.
117
+ Tools without `execute` (ones the AI provider runs itself) are left out.
120
118
 
121
119
  ## Write good tools
122
120
 
@@ -210,4 +208,5 @@ an agent (`agent`), in any route. Two limits apply to your own server:
210
208
  60 seconds.
211
209
  - Agent calls show up in your backend's logs like any other request.
212
210
 
213
- See [canopy mcp](https://docs.canopy.website/cli/mcp) for every flag.
211
+ See the [SDK reference](https://docs.canopy.website/sdk/mcp) for every option and [canopy mcp](https://docs.canopy.website/cli/mcp)
212
+ for every flag.
@@ -363,6 +363,11 @@
363
363
  "title": "Realtime",
364
364
  "summary": "Live updates between the people using your app — subscribe to a topic, publish to it, no backend needed."
365
365
  },
366
+ {
367
+ "slug": "sdk-mcp",
368
+ "title": "Agent tools (MCP)",
369
+ "summary": "\"@mediamonks/canopy/mcp — serve your backend's tools to AI agents: mountMcpTools, createMcpToolsHandler, MCP_PATH and mcpCaller.\""
370
+ },
366
371
  {
367
372
  "slug": "sdk-google",
368
373
  "title": "Google",
@@ -0,0 +1,222 @@
1
+ ---
2
+ title: Agent tools (MCP)
3
+ description: "@mediamonks/canopy/mcp — serve your backend's tools to AI agents: mountMcpTools, createMcpToolsHandler, MCP_PATH and mcpCaller."
4
+ ---
5
+
6
+ `@mediamonks/canopy/mcp` turns your backend into a set of MCP tools that AI
7
+ agents call through `canopy mcp serve`, always as the signed-in Monk running
8
+ them. It is a separate entry of the SDK, for backend code only: browser
9
+ bundles never include it.
10
+
11
+ ```ts
12
+ import { createMcpToolsHandler, MCP_PATH, mcpCaller, mountMcpTools } from "@mediamonks/canopy/mcp";
13
+ ```
14
+
15
+ For the why and the how-to, read the [Agent tools guide](https://docs.canopy.website/guides/agent-tools).
16
+ This page lists the exports and what they accept.
17
+
18
+ ## mountMcpTools(app, tools, options?)
19
+
20
+ Serves `tools` at `MCP_PATH` on a Hono app. Route tools run this same app's
21
+ routes.
22
+
23
+ ```ts
24
+ import { mountMcpTools } from "@mediamonks/canopy/mcp";
25
+ import { z } from "zod";
26
+
27
+ mountMcpTools(app, [
28
+ {
29
+ name: "list_notes",
30
+ description: "List the current user's notes, newest first",
31
+ route: "GET /api/notes",
32
+ annotations: { readOnlyHint: true },
33
+ },
34
+ {
35
+ name: "summarise",
36
+ description: "Summarise a note",
37
+ inputSchema: z.object({ id: z.string() }),
38
+ handler: async ({ id }, ctx) => summarise(ctx.env, id),
39
+ },
40
+ ]);
41
+ ```
42
+
43
+ - `app` — your Hono app, or anything with `all(path, handler)` and
44
+ `fetch(request, env, ctx)`.
45
+ - `tools` — a list of [tool definitions](#tool-definitions) and
46
+ [AI SDK tool sets](#vercel-ai-sdk-tool-sets), mixed.
47
+ - `options` — optional `{ name?, version? }`: the server name and version MCP
48
+ clients see. The name defaults to your project's slug.
49
+
50
+ ## createMcpToolsHandler(tools, options?)
51
+
52
+ The same thing without Hono: returns a handler you route `MCP_PATH` to
53
+ yourself.
54
+
55
+ ```ts
56
+ import { createMcpToolsHandler, MCP_PATH } from "@mediamonks/canopy/mcp";
57
+
58
+ const tools = createMcpToolsHandler(myTools, { app });
59
+
60
+ export default {
61
+ fetch(request, env, ctx) {
62
+ if (new URL(request.url).pathname === MCP_PATH) return tools(request, env, ctx);
63
+ return app.fetch(request, env, ctx);
64
+ },
65
+ };
66
+ ```
67
+
68
+ **Returns** `(request, env?, executionCtx?) => Promise<Response>`. Pass the
69
+ backend's `env` so handlers and route tools see `env.USER`, `env.CANOPY` and
70
+ your secrets.
71
+
72
+ **Options:** `{ app?, name?, version? }`. `app` is required when the list has
73
+ route tools: they run its routes.
74
+
75
+ Each request gets a fresh, stateless MCP server built on the official
76
+ `@modelcontextprotocol/server`. It speaks both the current MCP protocol and
77
+ the 2025 revision, so older clients work too.
78
+
79
+ ## Tool definitions
80
+
81
+ A tool is an object in the MCP specification's shape, plus either a `handler`
82
+ or a `route`:
83
+
84
+ | Field | Type | |
85
+ | -------------- | ------------------------------ | ------------------------------------------------------------------------------------- |
86
+ | `name` | `string` | Required. Unique; letters, digits, `_`, `-` and `.`, at most 128. |
87
+ | `description` | `string` | What it does and when to use it. The agent reads this. |
88
+ | `title` | `string` | A display name. |
89
+ | `inputSchema` | JSON Schema or Standard Schema | The arguments. Defaults to "no arguments". |
90
+ | `outputSchema` | JSON Schema or Standard Schema | Shape of the structured result, if you want clients to rely on it. |
91
+ | `annotations` | `McpToolAnnotations` | Hints: `readOnlyHint`, `destructiveHint`, `idempotentHint`, `openWorldHint`, `title`. |
92
+ | `handler` | `(args, ctx) => unknown` | Runs the tool. |
93
+ | `route` | `"<METHOD> /path"` | Runs one of your routes instead. |
94
+
95
+ **Schemas** can be plain JSON Schema or any schema that implements Standard
96
+ Schema with JSON Schema output: Zod 4, Valibot (with `toStandardJsonSchema`),
97
+ ArkType. Arguments are validated against `inputSchema` before your handler or
98
+ route runs. Invalid arguments come back to the agent as an error naming the
99
+ field.
100
+
101
+ **Annotations** matter to agents. Mark tools that change nothing with
102
+ `readOnlyHint: true`. Unmarked tools are treated as possibly destructive, and
103
+ clients usually ask the user before calling them.
104
+
105
+ ### Handler tools
106
+
107
+ ```ts
108
+ {
109
+ name: "add_comment",
110
+ inputSchema: z.object({ noteId: z.string(), text: z.string().max(2000) }),
111
+ handler: async ({ noteId, text }, ctx) => {
112
+ const env = ctx.env as Env; // your backend's Env type
113
+ const canopy = createCanopy({ fetch: env.CANOPY.fetch.bind(env.CANOPY) });
114
+ return canopy.db.collection("comments").insert({ noteId, text, by: ctx.user?.email });
115
+ },
116
+ }
117
+ ```
118
+
119
+ What the handler returns becomes the result:
120
+
121
+ - an object — sent as structured content, with a JSON text copy;
122
+ - a string — sent as text;
123
+ - an MCP `CallToolResult` (`{ content, structuredContent?, isError? }`) —
124
+ sent as is;
125
+ - nothing — an empty result.
126
+
127
+ Throw to report a failure. The agent gets a tool error with your message.
128
+
129
+ **`ctx`** (`McpToolContext`):
130
+
131
+ - `user` — the signed-in Monk (`{ sub, email, name }`), or `null`.
132
+ - `agent` — `true` when an AI agent made the call through `canopy mcp`.
133
+ - `toolCallId` — this call's id, for logs.
134
+ - `signal` — an `AbortSignal`, aborted when the agent cancels.
135
+ - `request` — the HTTP request the call arrived on, without platform
136
+ credentials.
137
+ - `env` — your backend's env, as routes get it. Typed `unknown`: cast it to
138
+ your `Env`.
139
+
140
+ `canopy.*` calls from a handler act as the user, as they would for that
141
+ person's browser.
142
+
143
+ ### Route tools
144
+
145
+ ```ts
146
+ { name: "get_note", route: "GET /api/notes/:id", inputSchema: z.object({ id: z.string() }) }
147
+ ```
148
+
149
+ The route runs in-process, with the same `env` and identity:
150
+
151
+ - `:name` segments of the path are filled from the argument of that name. A
152
+ missing one is a tool error.
153
+ - `GET` and `DELETE`: the other arguments become the query string (arrays
154
+ repeat the key, objects are sent as JSON).
155
+ - `POST`, `PUT`, `PATCH`: the other arguments become a JSON body.
156
+ - A 2xx JSON response becomes the result (structured when it's an object).
157
+ Other 2xx bodies become text.
158
+ - Any other status becomes a tool error: `HTTP 403: {"error":"not_owner"}`.
159
+
160
+ ### Vercel AI SDK tool sets
161
+
162
+ Put a tool set in the list as it is. Each key becomes a tool name:
163
+
164
+ ```ts
165
+ import { tool } from "ai";
166
+
167
+ const aiTools = {
168
+ weather: tool({
169
+ description: "Weather in a city",
170
+ inputSchema: z.object({ city: z.string() }),
171
+ execute: async ({ city }) => ({ city, tempC: 21 }),
172
+ }),
173
+ };
174
+
175
+ mountMcpTools(app, [...myTools, aiTools]);
176
+ ```
177
+
178
+ `execute` gets the arguments and `{ toolCallId, abortSignal, messages: [] }`.
179
+ Tools without `execute` are skipped. If the result is a stream, its last value
180
+ is the answer.
181
+
182
+ ## MCP_PATH
183
+
184
+ `"/__canopy/mcp"` — the one path on your backend that agents reach. Mount your
185
+ own MCP server there if you don't use `mountMcpTools`. It must answer on that
186
+ single URL with MCP's streamable HTTP transport.
187
+
188
+ ## mcpCaller(request, env?)
189
+
190
+ Who is calling, in any route or in your own MCP server:
191
+
192
+ ```ts
193
+ app.get("/api/notes", (c) => {
194
+ const { user, agent } = mcpCaller(c.req.raw, c.env);
195
+ // agent is true for a tools call that ran this route
196
+ });
197
+ ```
198
+
199
+ **Returns** `McpCaller`: `{ user: McpUser | null, agent: boolean }`.
200
+ `user` comes from `env.USER`. Without `env`, it falls back to the identity the
201
+ platform attached to the request.
202
+
203
+ ## When the tool list is wrong
204
+
205
+ The list is checked on the first request. A duplicate or invalid name, a tool
206
+ with neither or both of `handler` and `route`, a route tool without `app`, or
207
+ a schema that can't describe itself as JSON Schema makes the tools endpoint
208
+ answer `500 invalid_tools`, naming the tool. `canopy mcp add` shows that
209
+ message. The rest of your backend keeps working.
210
+
211
+ ## Limits
212
+
213
+ `canopy mcp serve` holds back tools whose description is over 2,048
214
+ characters or whose input schema is over 16 KB, shows at most 64 tools, turns
215
+ results over 512 KB into an error, and stops a call after 60 seconds.
216
+
217
+ ## Types
218
+
219
+ All exported from `@mediamonks/canopy/mcp`: `McpTool`, `McpHandlerTool`,
220
+ `McpRouteTool`, `McpToolAnnotations`, `McpToolContext`, `McpToolsInput`,
221
+ `McpToolsOptions`, `McpCaller`, `McpUser`, `AiSdkToolSet`, `ToolSchema`,
222
+ `JsonSchema`, `StandardSchemaLike`, `FetchApp`, `HttpMethod`.
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CiKTWo0_.mjs";
1
+ import { t as version } from "./package-YGjjin4p.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
4
  import { r as reportErrors } from "./ui-C_jSc1Kz.mjs";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CiKTWo0_.mjs";
1
+ import { t as version } from "./package-YGjjin4p.mjs";
2
2
  import { a as projectDirArgs, c as resolveProject, d as tryResolveProject, n as siteConfig, u as tryReadCanopyJson } from "./site-DM2J10CB.mjs";
3
3
  import { a as apiUpload, i as apiRequest, r as apiDownload } from "./api-C4rMhE7i.mjs";
4
4
  import { r as reportErrors, t as formatDateTime } from "./ui-C_jSc1Kz.mjs";
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as version } from "./package-CiKTWo0_.mjs";
2
+ import { t as version } from "./package-YGjjin4p.mjs";
3
3
  import { r as siteHeaderLine, t as setInvocationProfile } from "./site-DM2J10CB.mjs";
4
4
  import { a as notifyOfUpdate, o as runRefresh } from "./version-check-CcsA1QjI.mjs";
5
5
  import { defineCommand, runMain } from "citty";
@@ -70,25 +70,25 @@ const main = defineCommand({
70
70
  projects: () => import("./projects-CN2YhCZn.mjs").then((m) => m.projectsCommand),
71
71
  delete: () => import("./projects-CN2YhCZn.mjs").then((m) => m.deleteCommand),
72
72
  dev: () => import("./dev-MoHHjQh1.mjs").then((m) => m.devCommand),
73
- deploy: () => import("./deploy-B-znPE-v.mjs").then((m) => m.deployCommand),
74
- promote: () => import("./deploy-B-znPE-v.mjs").then((m) => m.promoteCommand),
73
+ deploy: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.deployCommand),
74
+ promote: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.promoteCommand),
75
75
  staging: () => import("./staging-BZVty57b.mjs").then((m) => m.stagingCommand),
76
- rollback: () => import("./deploy-B-znPE-v.mjs").then((m) => m.rollbackCommand),
77
- download: () => import("./deploy-B-znPE-v.mjs").then((m) => m.downloadCommand),
76
+ rollback: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.rollbackCommand),
77
+ download: () => import("./deploy-Ka2lHkyy.mjs").then((m) => m.downloadCommand),
78
78
  token: () => import("./token-HHlUR0CF.mjs").then((m) => m.tokenCommand),
79
79
  egress: () => import("./egress-If_Gsx_M.mjs").then((m) => m.egressCommand),
80
80
  secrets: () => import("./secrets-BSEjZnxg.mjs").then((m) => m.secretsCommand),
81
81
  schedule: () => import("./schedule-C5lMf3ny.mjs").then((m) => m.scheduleCommand),
82
- feedback: () => import("./feedback-Cd7Rvg9a.mjs").then((m) => m.feedbackCommand),
82
+ feedback: () => import("./feedback-_L3vqktY.mjs").then((m) => m.feedbackCommand),
83
83
  db: () => import("./db-BB6O6085.mjs").then((m) => m.dbCommand),
84
84
  usage: () => import("./usage-CUYrWxv0.mjs").then((m) => m.usageCommand),
85
85
  ai: () => import("./ai-DHLnyqfq.mjs").then((m) => m.aiCommand),
86
86
  open: () => import("./open-DcaQKi1v.mjs").then((m) => m.openCommand),
87
- docs: () => import("./docs-YD4z70gq.mjs").then((m) => m.docsCommand),
88
- skill: () => import("./skill-BWmXiJAz.mjs").then((m) => m.skillCommand),
89
- upgrade: () => import("./upgrade-Cexs9KYX.mjs").then((m) => m.upgradeCommand),
87
+ docs: () => import("./docs-CtNH5kbN.mjs").then((m) => m.docsCommand),
88
+ skill: () => import("./skill-CM7R-4Bq.mjs").then((m) => m.skillCommand),
89
+ upgrade: () => import("./upgrade-ChcSZW8i.mjs").then((m) => m.upgradeCommand),
90
90
  logs: () => import("./logs-DQQOawOG.mjs").then((m) => m.logsCommand),
91
- mcp: () => import("./mcp-GKwctfHN.mjs").then((m) => m.mcpCommand)
91
+ mcp: () => import("./mcp-RcN17Z2v.mjs").then((m) => m.mcpCommand)
92
92
  }
93
93
  });
94
94
  const quiet = process.argv[2] === "mcp" && process.argv[3] === "serve";
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CiKTWo0_.mjs";
1
+ import { t as version } from "./package-YGjjin4p.mjs";
2
2
  import { d as tryResolveProject, l as stagingUrl, n as siteConfig, s as projectUrl } from "./site-DM2J10CB.mjs";
3
3
  import { i as apiRequest, t as ApiError } from "./api-C4rMhE7i.mjs";
4
4
  import { t as liveDevServer } from "./dev-server-file-Dk9pk4AR.mjs";
@@ -1,4 +1,4 @@
1
1
  //#region package.json
2
- var version = "0.7.31";
2
+ var version = "0.7.32";
3
3
  //#endregion
4
4
  export { version as t };
@@ -1,4 +1,4 @@
1
- import { t as version } from "./package-CiKTWo0_.mjs";
1
+ import { t as version } from "./package-YGjjin4p.mjs";
2
2
  import { r as reportErrors } from "./ui-C_jSc1Kz.mjs";
3
3
  import { defineCommand } from "citty";
4
4
  import { cpSync, existsSync, mkdirSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
@@ -112,7 +112,7 @@ const upgradeCommand = defineCommand({
112
112
  const spec = `${PACKAGE_NAME}@${args.version ?? "latest"}`;
113
113
  const command = pmCommand(pm);
114
114
  const cmd = installArgs(pm, spec);
115
- const { skillInstalled } = await import("./skill-BWmXiJAz.mjs");
115
+ const { skillInstalled } = await import("./skill-CM7R-4Bq.mjs");
116
116
  const { homedir } = await import("node:os");
117
117
  const refreshGlobal = skillInstalled(homedir());
118
118
  const refreshRepo = process.cwd() !== homedir() && skillInstalled(process.cwd());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mediamonks/canopy-cli",
3
- "version": "0.7.31",
3
+ "version": "0.7.32",
4
4
  "description": "Canopy CLI — create, develop and deploy Canopy projects.",
5
5
  "bin": {
6
6
  "canopy": "./dist/index.mjs"
@@ -27,6 +27,7 @@
27
27
  },
28
28
  "devDependencies": {
29
29
  "@types/node": "^26.1.1",
30
+ "ai": "^7.0.82",
30
31
  "typescript": "^7.0.2",
31
32
  "vite-plus": "0.2.5",
32
33
  "@mediamonks/canopy-pages": "0.0.0"