@gavana.ai/cli 0.2.0

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.
Files changed (77) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/LICENSE.md +7 -0
  3. package/README.md +237 -0
  4. package/bin/craftboard.mjs +5 -0
  5. package/bin/gavana.mjs +5 -0
  6. package/guides/connections.md +35 -0
  7. package/guides/examples-common-mistakes.md +29 -0
  8. package/guides/existing-canvases.md +19 -0
  9. package/guides/generated-assets.md +29 -0
  10. package/guides/getting-started.md +26 -0
  11. package/guides/notes-text-sections.md +44 -0
  12. package/guides/paid-action-safety.md +22 -0
  13. package/guides/prompt-lists.md +20 -0
  14. package/guides/sections-layout.md +43 -0
  15. package/guides/validation-recovery.md +33 -0
  16. package/package.json +44 -0
  17. package/src/canvas-agent-guide.mjs +133 -0
  18. package/src/canvas-agent-validation.mjs +554 -0
  19. package/src/canvas-layout.mjs +287 -0
  20. package/src/capabilities.mjs +61 -0
  21. package/src/client.mjs +1141 -0
  22. package/src/commands.mjs +259 -0
  23. package/src/config.mjs +197 -0
  24. package/src/guide-sources.mjs +86 -0
  25. package/src/runner.mjs +1968 -0
  26. package/src/tools/action_get.mjs +16 -0
  27. package/src/tools/action_list.mjs +21 -0
  28. package/src/tools/action_run.mjs +60 -0
  29. package/src/tools/agent_canvas_get.mjs +15 -0
  30. package/src/tools/asset_get.mjs +16 -0
  31. package/src/tools/asset_list.mjs +17 -0
  32. package/src/tools/asset_upload.mjs +24 -0
  33. package/src/tools/campaign_cancel.mjs +16 -0
  34. package/src/tools/campaign_get.mjs +16 -0
  35. package/src/tools/campaign_plan.mjs +31 -0
  36. package/src/tools/campaign_review.mjs +24 -0
  37. package/src/tools/campaign_start.mjs +19 -0
  38. package/src/tools/canvas_apply_batch.mjs +35 -0
  39. package/src/tools/canvas_create.mjs +15 -0
  40. package/src/tools/canvas_get.mjs +16 -0
  41. package/src/tools/canvas_list.mjs +17 -0
  42. package/src/tools/canvas_render.mjs +34 -0
  43. package/src/tools/canvas_validate.mjs +34 -0
  44. package/src/tools/connection_create.mjs +38 -0
  45. package/src/tools/connection_delete.mjs +31 -0
  46. package/src/tools/definitions.mjs +111 -0
  47. package/src/tools/guide_get.mjs +16 -0
  48. package/src/tools/guide_search.mjs +16 -0
  49. package/src/tools/helpers.mjs +66 -0
  50. package/src/tools/image_edit.mjs +8 -0
  51. package/src/tools/image_generate.mjs +8 -0
  52. package/src/tools/image_tool.mjs +56 -0
  53. package/src/tools/image_variations.mjs +8 -0
  54. package/src/tools/job_cancel.mjs +16 -0
  55. package/src/tools/job_get.mjs +17 -0
  56. package/src/tools/job_wait.mjs +18 -0
  57. package/src/tools/model_get.mjs +16 -0
  58. package/src/tools/model_list.mjs +23 -0
  59. package/src/tools/node_create.mjs +36 -0
  60. package/src/tools/node_delete.mjs +31 -0
  61. package/src/tools/node_get.mjs +16 -0
  62. package/src/tools/node_move.mjs +37 -0
  63. package/src/tools/node_resize.mjs +37 -0
  64. package/src/tools/node_update.mjs +36 -0
  65. package/src/tools/progress.mjs +101 -0
  66. package/src/tools/provider_list.mjs +17 -0
  67. package/src/tools/recipe_fork.mjs +32 -0
  68. package/src/tools/recipe_get.mjs +19 -0
  69. package/src/tools/recipe_run.mjs +61 -0
  70. package/src/tools/recipe_search.mjs +17 -0
  71. package/src/tools/registry.mjs +550 -0
  72. package/src/tools/run_cancel.mjs +16 -0
  73. package/src/tools/run_get.mjs +17 -0
  74. package/src/tools/run_wait.mjs +18 -0
  75. package/src/tools/schemas.mjs +165 -0
  76. package/src/tools/video_generate.mjs +37 -0
  77. package/src/version.mjs +12 -0
@@ -0,0 +1,287 @@
1
+ // Shared server-side canvas layout allocator.
2
+ //
3
+ // One implementation of the guide's placement doctrine — 160 canvas units of
4
+ // outer spacing beside the existing bounding box (or below it when the canvas
5
+ // is already wide), 80 units between major stages, 32 units between sibling
6
+ // nodes — used by canvas operations, image-target creation, workflow planning,
7
+ // and recipe-run materialization instead of five drifting ad-hoc allocators.
8
+ // Lives in the CLI package (like canvas-agent-guide.mjs) so both the packaged
9
+ // CLI and the server can share it; the typed server entry point is
10
+ // src/lib/canvas-agent/layout.ts.
11
+
12
+ export const CANVAS_LAYOUT_GRID = 8;
13
+ export const CANVAS_SIBLING_SPACING = 32;
14
+ export const CANVAS_STAGE_SPACING = 80;
15
+ export const CANVAS_OUTER_SPACING = 160;
16
+ export const CANVAS_SECTION_PADDING = 48;
17
+ /** Matches the explicit-position bound enforced by canvas operations. */
18
+ export const CANVAS_COORDINATE_LIMIT = 10_000_000;
19
+
20
+ export function snapToCanvasGrid(value, grid = CANVAS_LAYOUT_GRID) {
21
+ return Math.round(value / grid) * grid;
22
+ }
23
+
24
+ function finiteOr(value, fallback) {
25
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
26
+ }
27
+
28
+ /**
29
+ * Grid-snaps an origin coordinate and shifts it so the full extent
30
+ * [origin, origin + extent] stays inside the explicit-coordinate bound. Every
31
+ * allocator return path goes through this, so sibling and lane offsets can
32
+ * never push a node past the invariant.
33
+ */
34
+ function fitOriginToBounds(value, extent) {
35
+ const highest = Math.floor((CANVAS_COORDINATE_LIMIT - extent) / CANVAS_LAYOUT_GRID) * CANVAS_LAYOUT_GRID;
36
+ const lowest = Math.ceil(-CANVAS_COORDINATE_LIMIT / CANVAS_LAYOUT_GRID) * CANVAS_LAYOUT_GRID;
37
+ return Math.min(Math.max(snapToCanvasGrid(value), lowest), Math.max(lowest, highest));
38
+ }
39
+
40
+ function nodeRect(node) {
41
+ const x = finiteOr(node?.position?.x, 0);
42
+ const y = finiteOr(node?.position?.y, 0);
43
+ return { left: x, top: y, right: x + Math.max(finiteOr(node?.width, 1), 1), bottom: y + Math.max(finiteOr(node?.height, 1), 1) };
44
+ }
45
+
46
+ /** Bounding box of the existing graph, or null for an empty canvas. */
47
+ export function canvasContentBounds(nodes) {
48
+ const list = Array.isArray(nodes) ? nodes : [];
49
+ if (!list.length) return null;
50
+ const rects = list.map(nodeRect);
51
+ return {
52
+ left: Math.min(...rects.map((rect) => rect.left)),
53
+ top: Math.min(...rects.map((rect) => rect.top)),
54
+ right: Math.max(...rects.map((rect) => rect.right)),
55
+ bottom: Math.max(...rects.map((rect) => rect.bottom)),
56
+ };
57
+ }
58
+
59
+ function rectIntersects(a, b, spacing) {
60
+ return a.left < b.right + spacing && a.right + spacing > b.left && a.top < b.bottom + spacing && a.bottom + spacing > b.top;
61
+ }
62
+
63
+ /** True when a rect (inflated by spacing) touches no existing node. */
64
+ export function canvasRegionIsFree(nodes, region, spacing = CANVAS_SIBLING_SPACING) {
65
+ const rect = { left: region.x, top: region.y, right: region.x + region.width, bottom: region.y + region.height };
66
+ return (Array.isArray(nodes) ? nodes : []).every((node) => !rectIntersects(rect, nodeRect(node), spacing));
67
+ }
68
+
69
+ /**
70
+ * Places a width x height region on the canvas without overlapping existing
71
+ * nodes. Follows the guide's doctrine: beside the current bounding box with
72
+ * outer spacing — below it instead when the canvas is already much wider than
73
+ * tall — then the remaining sides, then bounded lane scans below and above the
74
+ * content. The final growing diagonal scan always terminates on a free spot
75
+ * because the coordinate space dwarfs any real canvas. Deterministic for
76
+ * identical inputs; results are grid-snapped and always inside the
77
+ * explicit-coordinate bound.
78
+ */
79
+ export function allocateCanvasRegion(nodes, size, options = {}) {
80
+ const width = Math.max(finiteOr(size?.width, 1), 1);
81
+ const height = Math.max(finiteOr(size?.height, 1), 1);
82
+ const spacing = Math.max(finiteOr(options.spacing, CANVAS_OUTER_SPACING), 0);
83
+ const list = Array.isArray(nodes) ? nodes : [];
84
+ const fit = (candidate) => ({ x: fitOriginToBounds(candidate.x, width), y: fitOriginToBounds(candidate.y, height) });
85
+ const freeAt = (candidate) => canvasRegionIsFree(list, { ...candidate, width, height }, Math.min(spacing, CANVAS_STAGE_SPACING));
86
+ const bounds = canvasContentBounds(list);
87
+ if (!bounds) return fit({ x: finiteOr(options.anchorX, 0), y: finiteOr(options.anchorY, 0) });
88
+
89
+ const candidates = [];
90
+ if (options.anchorX !== undefined || options.anchorY !== undefined) {
91
+ candidates.push({ x: finiteOr(options.anchorX, bounds.left), y: finiteOr(options.anchorY, bounds.top) });
92
+ }
93
+ const boundsWidth = bounds.right - bounds.left;
94
+ const boundsHeight = bounds.bottom - bounds.top;
95
+ const preferBelow = options.preferBelow === true || boundsWidth > Math.max(boundsHeight, 1) * 2.5;
96
+ const sides = [
97
+ { x: bounds.right + spacing, y: bounds.top },
98
+ { x: bounds.left, y: bounds.bottom + spacing },
99
+ { x: bounds.left - spacing - width, y: bounds.top },
100
+ { x: bounds.left, y: bounds.top - spacing - height },
101
+ ];
102
+ candidates.push(...(preferBelow ? [sides[1], sides[0], sides[3], sides[2]] : sides));
103
+ for (const candidate of candidates) {
104
+ const fitted = fit(candidate);
105
+ if (freeAt(fitted)) return fitted;
106
+ }
107
+ // Bounded lane scans below, then above, the content for dense canvases.
108
+ for (const direction of [1, -1]) {
109
+ for (let row = 0; row < 50; row += 1) {
110
+ const y = direction === 1 ? bounds.bottom + spacing + row * (height + CANVAS_STAGE_SPACING) : bounds.top - spacing - height - row * (height + CANVAS_STAGE_SPACING);
111
+ for (let column = 0; column < 50; column += 1) {
112
+ const fitted = fit({ x: bounds.left + column * (width + CANVAS_SIBLING_SPACING), y });
113
+ if (fitted.x + width > bounds.right + width + spacing) break;
114
+ if (freeAt(fitted)) return fitted;
115
+ }
116
+ }
117
+ }
118
+ // Growing diagonal escape: step size doubles until a free spot fits inside
119
+ // the coordinate bound.
120
+ for (let step = Math.max(width, height) + spacing; step <= CANVAS_COORDINATE_LIMIT * 2; step *= 2) {
121
+ const fitted = fit({ x: bounds.right + step, y: bounds.bottom + step });
122
+ if (freeAt(fitted)) return fitted;
123
+ const mirrored = fit({ x: bounds.left - step - width, y: bounds.top - step - height });
124
+ if (freeAt(mirrored)) return mirrored;
125
+ }
126
+ // Terminal center-out sweep over the whole coordinate square. Nodes parked
127
+ // at the coordinate corners stretch the content bounds across the entire
128
+ // space, making every bounds-relative candidate collide — but the interior
129
+ // stays overwhelmingly free (canvas node-count and node-size limits cover
130
+ // a vanishing fraction of the 20M x 20M square), so a coarse sweep ordered
131
+ // by distance from the center finds a genuinely free cell almost
132
+ // immediately. A never-free result is impossible within canvas limits;
133
+ // the assertion-style final return is unreachable in practice.
134
+ const cellCount = 128;
135
+ const cell = Math.max((CANVAS_COORDINATE_LIMIT * 2) / cellCount, width + spacing, height + spacing);
136
+ const cells = [];
137
+ for (let row = 0; row < cellCount; row += 1) {
138
+ for (let column = 0; column < cellCount; column += 1) cells.push({ row, column });
139
+ }
140
+ const middle = (cellCount - 1) / 2;
141
+ cells.sort((a, b) => Math.max(Math.abs(a.row - middle), Math.abs(a.column - middle)) - Math.max(Math.abs(b.row - middle), Math.abs(b.column - middle)) || a.row - b.row || a.column - b.column);
142
+ for (const { row, column } of cells) {
143
+ const fitted = fit({ x: -CANVAS_COORDINATE_LIMIT + column * cell, y: -CANVAS_COORDINATE_LIMIT + row * cell });
144
+ if (freeAt(fitted)) return fitted;
145
+ }
146
+ return fit({ x: 0, y: 0 });
147
+ }
148
+
149
+ function laneStride(height, gap) {
150
+ return Math.ceil((height + gap) / CANVAS_LAYOUT_GRID) * CANVAS_LAYOUT_GRID;
151
+ }
152
+
153
+ /**
154
+ * Places a left-to-right row of sibling regions (32-unit gaps) as one block,
155
+ * returning one position per size. Strides round up to the grid so every
156
+ * sibling stays grid-aligned; the whole block is placed (and bound-fitted) as
157
+ * one region, so no sibling can exceed the coordinate bound.
158
+ */
159
+ export function allocateCanvasRow(nodes, sizes, options = {}) {
160
+ const list = Array.isArray(sizes) ? sizes : [];
161
+ if (!list.length) return [];
162
+ const strides = list.map((size) => laneStride(Math.max(finiteOr(size?.width, 1), 1), CANVAS_SIBLING_SPACING));
163
+ const rowWidth = strides.reduce((total, stride) => total + stride, 0) - CANVAS_SIBLING_SPACING;
164
+ const rowHeight = Math.max(...list.map((size) => Math.max(finiteOr(size?.height, 1), 1)));
165
+ const origin = allocateCanvasRegion(nodes, { width: rowWidth, height: rowHeight }, options);
166
+ const positions = [];
167
+ let x = origin.x;
168
+ for (const stride of strides) {
169
+ positions.push({ x, y: origin.y });
170
+ x += stride;
171
+ }
172
+ return positions;
173
+ }
174
+
175
+ function outputGridColumns(count, requestedColumns) {
176
+ if (Number.isFinite(requestedColumns)) return Math.max(1, Math.min(count, Math.floor(requestedColumns)));
177
+ // Keep small result sets readable at Fit Canvas. In particular, the
178
+ // standard four-image photoshoot is a 2x2 grid, never a tall column.
179
+ if (count <= 2) return count;
180
+ if (count <= 4) return 2;
181
+ if (count <= 6) return 3;
182
+ return 4;
183
+ }
184
+
185
+ /**
186
+ * Deterministically packs 2-8 mixed-size media frames into a compact grid.
187
+ * Each row and column uses its largest child as the lane size, so unlike a
188
+ * fixed-cell grid no completed frame can overlap a sibling after resizing.
189
+ * Positions are relative to the grid origin; use allocateCanvasGrid when the
190
+ * cluster also needs free-region placement against existing canvas content.
191
+ */
192
+ export function canvasOutputGrid(sizes, options = {}) {
193
+ const list = Array.isArray(sizes)
194
+ ? sizes.map((size) => ({
195
+ width: Math.max(finiteOr(size?.width, 1), 1),
196
+ height: Math.max(finiteOr(size?.height, 1), 1),
197
+ }))
198
+ : [];
199
+ if (!list.length) return { columns: 0, rows: 0, width: 0, height: 0, positions: [] };
200
+
201
+ const gap = Math.max(finiteOr(options.gap, CANVAS_SIBLING_SPACING), 0);
202
+ const columns = outputGridColumns(list.length, options.columns);
203
+ const rows = Math.ceil(list.length / columns);
204
+ const columnWidths = Array.from({ length: columns }, (_, column) =>
205
+ Math.max(...list.filter((_, index) => index % columns === column).map((size) => size.width)),
206
+ );
207
+ const rowHeights = Array.from({ length: rows }, (_, row) =>
208
+ Math.max(...list.slice(row * columns, (row + 1) * columns).map((size) => size.height)),
209
+ );
210
+ const columnOffsets = [];
211
+ const rowOffsets = [];
212
+ let x = 0;
213
+ let y = 0;
214
+ for (const width of columnWidths) {
215
+ columnOffsets.push(x);
216
+ x += laneStride(width, gap);
217
+ }
218
+ for (const height of rowHeights) {
219
+ rowOffsets.push(y);
220
+ y += laneStride(height, gap);
221
+ }
222
+
223
+ return {
224
+ columns,
225
+ rows,
226
+ width: columnOffsets[columns - 1] + columnWidths[columns - 1],
227
+ height: rowOffsets[rows - 1] + rowHeights[rows - 1],
228
+ positions: list.map((_, index) => ({ x: columnOffsets[index % columns], y: rowOffsets[Math.floor(index / columns)] })),
229
+ };
230
+ }
231
+
232
+ /**
233
+ * Allocates a complete output grid as one free canvas region, then returns
234
+ * absolute positions for its children. Existing content is never moved.
235
+ */
236
+ export function allocateCanvasGrid(nodes, sizes, options = {}) {
237
+ const grid = canvasOutputGrid(sizes, options);
238
+ if (!grid.positions.length) return grid;
239
+ const origin = allocateCanvasRegion(nodes, { width: grid.width, height: grid.height }, {
240
+ spacing: Math.max(finiteOr(options.spacing, CANVAS_STAGE_SPACING), 0),
241
+ ...(options.anchorX === undefined ? {} : { anchorX: options.anchorX }),
242
+ ...(options.anchorY === undefined ? {} : { anchorY: options.anchorY }),
243
+ ...(options.preferBelow === undefined ? {} : { preferBelow: options.preferBelow }),
244
+ });
245
+ return { ...grid, origin, positions: grid.positions.map((position) => ({ x: origin.x + position.x, y: origin.y + position.y })) };
246
+ }
247
+
248
+ /**
249
+ * Stacks mixed-height lane nodes into one vertical column beside an anchor
250
+ * (for example a Recipe card's output stage), centered on the anchor's
251
+ * vertical midpoint. The column slides away from occupied space in growing
252
+ * steps in both directions; when no nearby free spot exists it falls back to
253
+ * the general free-region allocator, so the returned positions are always
254
+ * free of existing nodes. The block is bound-fitted as a whole, so stacking
255
+ * offsets can never collapse or exceed the coordinate bound.
256
+ */
257
+ export function allocateAnchoredColumn(nodes, sizes, anchor, options = {}) {
258
+ const list = Array.isArray(sizes) ? sizes : [];
259
+ if (!list.length) return [];
260
+ const gap = Math.max(finiteOr(options.gap, CANVAS_SIBLING_SPACING), 0);
261
+ const minLaneHeight = Math.max(finiteOr(options.minLaneHeight, 0), 0);
262
+ const strides = list.map((size) => laneStride(Math.max(finiteOr(size?.height, 1), minLaneHeight, 1), gap));
263
+ const width = Math.max(...list.map((size) => Math.max(finiteOr(size?.width, 1), 1)));
264
+ const totalHeight = strides.reduce((total, stride) => total + stride, 0) - gap;
265
+ const columnList = Array.isArray(nodes) ? nodes : [];
266
+ const stack = (origin) => {
267
+ const positions = [];
268
+ let y = origin.y;
269
+ for (const stride of strides) {
270
+ positions.push({ x: origin.x, y });
271
+ y += stride;
272
+ }
273
+ return positions;
274
+ };
275
+ const x = fitOriginToBounds(finiteOr(anchor?.x, 0), width);
276
+ const anchoredTop = finiteOr(anchor?.centerY, 0) - totalHeight / 2;
277
+ for (let attempt = 0, offset = 0; attempt < 40; attempt += 1) {
278
+ for (const direction of attempt === 0 ? [1] : [1, -1]) {
279
+ const top = fitOriginToBounds(anchoredTop + direction * offset, totalHeight);
280
+ if (canvasRegionIsFree(columnList, { x, y: top, width, height: totalHeight }, Math.min(gap, CANVAS_SIBLING_SPACING))) {
281
+ return stack({ x, y: top });
282
+ }
283
+ }
284
+ offset = offset ? offset * 1.6 : CANVAS_STAGE_SPACING;
285
+ }
286
+ return stack(allocateCanvasRegion(columnList, { width, height: totalHeight }, { spacing: CANVAS_STAGE_SPACING }));
287
+ }
@@ -0,0 +1,61 @@
1
+ // Imported and re-exported, not `export ... from`. The forwarding form creates no
2
+ // local binding, so gavanaCapabilitySummary below would throw ReferenceError at
3
+ // the point it reads the version — and only there, at runtime, on one command.
4
+ import { GAVANA_CLI_VERSION } from "./version.mjs";
5
+
6
+ export { GAVANA_CLI_VERSION };
7
+ export const GAVANA_MCP_TOOLSETS = Object.freeze(["canvas", "recipes", "assets", "models", "actions", "images", "videos", "runs", "campaigns"]);
8
+
9
+ import { GAVANA_TOOL_REGISTRY } from "./tools/registry.mjs";
10
+
11
+ /**
12
+ * The hosted surface, projected from the tool registry. Names, order, toolsets, and
13
+ * readOnly/paid flags are the published @gavana.ai/mcp@0.1.1 contract — hosted tools
14
+ * are advertised under their original verb-object names, which the registry carries
15
+ * as `hostedAlias`.
16
+ */
17
+ export const GAVANA_REMOTE_CAPABILITIES = Object.freeze(
18
+ GAVANA_TOOL_REGISTRY.filter((tool) => typeof tool.hostedOrder === "number")
19
+ .slice()
20
+ .sort((left, right) => left.hostedOrder - right.hostedOrder)
21
+ .map((tool) => Object.freeze({ name: tool.hostedAlias || tool.name, toolset: tool.hostedToolset, readOnly: tool.hostedReadOnly, paid: Boolean(tool.paid) })),
22
+ );
23
+
24
+ /** Canonical names exposed by the local stdio MCP server, projected from the registry. */
25
+ export const GAVANA_LOCAL_MCP_TOOL_NAMES = Object.freeze(
26
+ GAVANA_TOOL_REGISTRY.filter((tool) => tool.surfaces.includes("local"))
27
+ .map((tool) => tool.name)
28
+ .sort(),
29
+ );
30
+
31
+ const TOOLSET_FOR_PREFIX = Object.freeze([
32
+ ["guide_", "canvas"],
33
+ ["recipe_", "recipes"],
34
+ ["campaign_", "campaigns"],
35
+ ["canvas_", "canvas"],
36
+ ["agent_canvas_", "canvas"],
37
+ ["node_", "canvas"],
38
+ ["connection_", "canvas"],
39
+ ["asset_", "assets"],
40
+ ["provider_", "models"],
41
+ ["model_", "models"],
42
+ ["action_", "actions"],
43
+ ["image_", "images"],
44
+ ["video_", "videos"],
45
+ ["job_", "runs"],
46
+ ["run_", "runs"],
47
+ ]);
48
+
49
+ export function gavanaToolsetForName(name) {
50
+ return TOOLSET_FOR_PREFIX.find(([prefix]) => name.startsWith(prefix))?.[1] || "other";
51
+ }
52
+
53
+ export function gavanaCapabilitySummary() {
54
+ return {
55
+ apiVersion: "v1",
56
+ cliVersion: GAVANA_CLI_VERSION,
57
+ toolsets: [...GAVANA_MCP_TOOLSETS],
58
+ remote: GAVANA_REMOTE_CAPABILITIES.map((capability) => ({ ...capability })),
59
+ endpoints: { full: "/mcp", readOnly: "/mcp/readonly", api: "/api/canvas-agent/v1" },
60
+ };
61
+ }