@gavana.ai/cli 0.2.0 → 0.2.1

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 (41) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +34 -0
  3. package/guides/creative-canvas.md +52 -0
  4. package/guides/generated-assets.md +6 -1
  5. package/guides/sections-layout.md +3 -3
  6. package/guides/validation-recovery.md +9 -2
  7. package/package.json +1 -1
  8. package/src/canvas-agent-guide.mjs +3 -3
  9. package/src/canvas-agent-validation.mjs +30 -15
  10. package/src/capabilities.mjs +3 -1
  11. package/src/client.mjs +252 -0
  12. package/src/commands.mjs +23 -1
  13. package/src/config.mjs +50 -17
  14. package/src/guide-sources.mjs +11 -3
  15. package/src/mcp-targets.mjs +72 -0
  16. package/src/runner.mjs +191 -39
  17. package/src/tools/campaign_plan.mjs +2 -2
  18. package/src/tools/campaign_review.mjs +2 -2
  19. package/src/tools/campaign_start.mjs +2 -2
  20. package/src/tools/definitions.mjs +34 -0
  21. package/src/tools/element_archive.mjs +12 -0
  22. package/src/tools/element_collection_create.mjs +11 -0
  23. package/src/tools/element_collection_delete.mjs +12 -0
  24. package/src/tools/element_collection_list.mjs +13 -0
  25. package/src/tools/element_collection_update.mjs +12 -0
  26. package/src/tools/element_create.mjs +11 -0
  27. package/src/tools/element_get.mjs +12 -0
  28. package/src/tools/element_history.mjs +13 -0
  29. package/src/tools/element_list.mjs +13 -0
  30. package/src/tools/element_restore.mjs +12 -0
  31. package/src/tools/element_update.mjs +21 -0
  32. package/src/tools/element_update_collections.mjs +12 -0
  33. package/src/tools/image_tool.mjs +2 -2
  34. package/src/tools/registry.mjs +202 -0
  35. package/src/tools/schemas.mjs +62 -0
  36. package/src/tools/work_continue.mjs +42 -0
  37. package/src/tools/work_execute.mjs +12 -0
  38. package/src/tools/work_get.mjs +12 -0
  39. package/src/tools/work_prepare.mjs +21 -0
  40. package/src/tools/work_refresh.mjs +12 -0
  41. package/src/version.mjs +5 -7
package/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changelog — @gavana.ai/cli
2
2
 
3
+ ## 0.2.1
4
+
5
+ ### Fixed
6
+
7
+ - macOS Keychain-backed profiles now distinguish a credential that is genuinely
8
+ absent from one that the current process cannot access. Restricted processes
9
+ report `keychain_inaccessible` with an actionable explanation instead of
10
+ sending users through login again.
11
+
3
12
  ## 0.2.0
4
13
 
5
14
  First release published to npm. Version 0.1.0 existed in this repository but was
package/README.md CHANGED
@@ -103,6 +103,9 @@ retried automatically.
103
103
  gavana canvas list
104
104
  gavana canvas list --limit 25 --jq '.canvases[].handle' -r
105
105
  gavana asset get asset:OWNER_UID:ASSET_ID
106
+ gavana element list
107
+ gavana element get element:ELEMENT_ID@v3
108
+ gavana element create --name "Soft window light" --type lighting --guidelines "Keep diffuse, cool window light."
106
109
  gavana recipe search "product visual"
107
110
  gavana recipe run recipe:product-visual-direction --input product-context="A matte black travel bottle" --destination agent-canvas
108
111
  gavana model list --capability image.generate
@@ -217,6 +220,37 @@ commands for canvases, recipes, assets, Actions, models, and providers accept `-
217
220
  `--jq` selector supports property paths, array indexes, and `[]` projections
218
221
  without requiring a separate jq installation.
219
222
 
223
+ ## Elements
224
+
225
+ Elements are reusable visual references, not copied image files. Use
226
+ `element list`, `element get element:<id>@v<n>`, and `element history` to
227
+ inspect current or immutable historical revisions. Create or update with one
228
+ or more `--source-asset asset:<id>` references, `--guidelines`, or both.
229
+ `element archive` asks for interactive confirmation; in scripts and other
230
+ non-interactive environments it requires `--yes`. An archived Element remains
231
+ restorable with `element restore`. `element collection-delete` follows the same
232
+ confirmation rule and never deletes the Elements it organized.
233
+
234
+ Apply an exact immutable revision during image work with repeatable
235
+ `--element element:<id>@v<n>` flags. JSON callers may also pass
236
+ `{ "handle": "element:<id>@v<n>", "role": "style", "influence": 0.8 }`.
237
+ An image job accepts up to eight Elements; their source images share the
238
+ existing 16-reference limit.
239
+
240
+ ## Chat-first work
241
+
242
+ Use `work_prepare` or `gavana work prepare` for a broad campaign request. It
243
+ returns exactly three directions and a recommendation without paid generation.
244
+ Use `work_continue` or `gavana work continue` to answer a factual question,
245
+ select one direction, adjust the brief, or acknowledge a changed Canvas with
246
+ `--rebase`. `work_execute` and `gavana work execute work:<id> --confirm
247
+ --idempotency-key KEY` send `confirm: true`, start only the selected direction,
248
+ and return durable progress immediately; they never wait or automatically
249
+ retry paid work. `work_get` is read-only and never polls providers or writes
250
+ state; use `gavana work refresh work:<id>` to reconcile provider progress.
251
+ The snapshot returns the latest brief, choice, outputs, and Canvas
252
+ synchronization state.
253
+
220
254
  ## JavaScript client
221
255
 
222
256
  The same dependency-free client is exported for Node.js applications:
@@ -0,0 +1,52 @@
1
+ ---
2
+ id: creative-canvas
3
+ title: Creative Canvas
4
+ description: Keep creative work on the Canvas without lifecycle states.
5
+ keywords: ["creative", "ideas", "directions", "brainstorm", "refine", "compose", "campaign"]
6
+ order: 11
7
+ ---
8
+
9
+ ## The Canvas is the memory
10
+
11
+ Every generated image, written thought, reference, and experiment remains on the Canvas until a person deletes it. Do not assign creative output a draft, rejected, accepted, approved, or final lifecycle state. A retained node has value even when it is not currently selected.
12
+
13
+ Reference handles and connections are durable provenance. They explain where a creative result came from; they do not require a user to click Keep or clear a review gate.
14
+
15
+ ## Spatial structure, not stages
16
+
17
+ Use an ordinary Section only when the person explicitly asks to organize work. Name it for their brief; it is never a required left-to-right workflow. Any node may move between Sections, appear in more than one discussion, become a reference for later work, or remain untouched. Do not move, rename, or create Sections unless the user requests structure.
18
+
19
+ ## Campaign assistance
20
+
21
+ When a person supplies only a product and a broad ask, do not jump straight to a
22
+ generic photoshoot, poster, or banner. First make a visible reference-led
23
+ concept cluster with three roles:
24
+
25
+ - **Product identity:** the exact supplied product, logo, garment, or other
26
+ identity reference when one exists.
27
+ - **Brand-world:** the lighting, material, setting, and cultural visual world.
28
+ - **Typography/layout:** the editorial hierarchy, copy placement, crop, and
29
+ composition reference.
30
+
31
+ Connect each source to the work it informs. If inspiration is missing and the
32
+ current request explicitly authorizes generation, autonomously create small,
33
+ clearly labelled concept-reference images or boards for every missing role: a
34
+ product-identity concept study, brand-world, and typography/layout. A
35
+ product-identity study is provisional when no exact product source exists; none
36
+ of these generated concept references may be claimed as product evidence,
37
+ official assets, or real campaigns. If the request authorizes preparation only,
38
+ create editable prompt and Text references without starting a paid image job.
39
+
40
+ Infer a concise brief and create at least three materially different directions.
41
+ Vary the central idea, composition, setting, copy hierarchy, typography/layout
42
+ concept, or audience — not merely the pose or crop. Give every direction a
43
+ short rationale Text node and preserve all attempts on the Canvas. Add one
44
+ **Recommended next move** Text node that explains the strongest direction; this
45
+ is editorial advice, not approval.
46
+
47
+ Do not create a default Section set. If a person asks for a structured campaign
48
+ area, use only the ordinary Sections they request. Ask one short question only
49
+ when a missing product-versus-style distinction would materially change the
50
+ work. Otherwise make a useful first pass and let the person point to, combine,
51
+ or refine any result. Exporting or publishing is an explicit action from any
52
+ selected node or Section, not a status transition.
@@ -10,11 +10,16 @@ order: 6
10
10
 
11
11
  - Read the destination canvas and relevant source nodes.
12
12
  - Use exact source `node:` or `asset:` handles.
13
+ - Before image or video generation, call `model_list` for the required capability and pass its exact `model:` handle. A bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access that connection; do not ask the user to add a key again.
13
14
  - For a standalone image request, pass every visual source in `references`.
14
15
  Use `{ "handle": "node:...", "role": "identity" }` when its
15
16
  responsibility is known; valid roles are `identity`, `construction`,
16
17
  `texture`, `fit`, and `style`. Do not flatten multi-reference work
17
18
  into prompt prose or omit a source during fallback.
19
+ - Reuse existing Canvas `node:` or `asset:` handles directly. Do not download
20
+ and re-upload a generated Canvas image merely to use it as the next
21
+ generation's reference. State whether a style reference establishes the
22
+ brand-world or typography/layout direction in the prompt.
18
23
  - Create an empty image or video target only through supported operations. Do not write media bytes, storage keys, or arbitrary output URLs into metadata.
19
24
  - Connect prompts, products, references, Lists, and frame inputs to their target with the correct direction and mode.
20
25
 
@@ -26,4 +31,4 @@ Generation is allowed only after explicit current-turn user intent. Start one ru
26
31
 
27
32
  Do not claim a generated image is durable until the result returns a target `node:`, durable `asset:`, and the final canvas read shows server-owned media fields. A video Job may return a protected download without materializing a native video node; report exactly what the server returned and do not invent durability.
28
33
 
29
- Keep generated output spatially near its input stage and connected to its source, prompt, List, or workflow. After finalization, run `canvas_validate` and read `completionReview`: it reports overlap, full-frame Section containment, reference lineage, durable output count, and product-fidelity review state. Do not claim Done while it says `doneClaimAllowed: false`. Product-fidelity uncertainty requires human review; never create another paid provider call automatically.
34
+ Keep generated output spatially near its input stage and connected to its source, prompt, List, or workflow. After finalization, run `canvas_validate` and read `completionReview`: it reports overlap, full-frame Section containment, reference lineage, and delivery state. Do not claim Done while it says `doneClaimAllowed: false`, including when delivery is pending, failed, or non-durable. Render the Canvas for visual inspection when the request includes a campaign, poster, banner, or multi-direction composition. Reference provenance is not a user workflow state and never requires a Keep action. Never create another paid provider call automatically.
@@ -10,10 +10,10 @@ order: 3
10
10
 
11
11
  - Treat the current canvas as user-owned. Preserve existing coordinates unless reorganization was explicitly requested.
12
12
  - For additions to an existing canvas, compute its visible bounding box and place the new Section to the right with at least 160 canvas units of outer spacing. If right-side placement would make the canvas excessively wide, place it below with the same spacing.
13
- - Use 48 units of inner Section padding, 32 units between sibling nodes, and at least 80 units between major stages.
14
- - Keep workflow direction consistent, normally left to right. Keep inputs before transformations and outputs after them.
13
+ - Use 48 units of inner Section padding, 32 units between sibling nodes, and at least 80 units between major clusters.
14
+ - Keep a left-to-right direction only when the user asks for a linear workflow. For creative exploration, use Sections as optional spatial places rather than a required sequence.
15
15
  - Use compact rows or columns. Avoid extremely long, thin canvases that become unreadable at Fit Canvas.
16
- - For 2-8 generated image outputs, reserve each requested final aspect frame before work begins and pack the complete result cluster as a grid. The usual four-image photoshoot is a 2x2 grid; do not stack it as a tall output column that can overlap when images finalize.
16
+ - For 2-8 generated image outputs, reserve each requested final aspect frame before work begins and pack the complete result cluster as a grid. A normal feed photoshoot uses 4:5 portrait frames in its own 2x2 Section. Put horizontal 16:9 campaign ads in a separate Section with their own targets; do not mix formats unless the user explicitly asks for a mixed-format deliverable.
17
17
  - Size Sections after their contents. Do not use Section overlap as a substitute for node placement.
18
18
  - Run `canvas_validate` before and after a multi-node layout change.
19
19
 
@@ -12,7 +12,14 @@ Call it with only `canvasId` to audit the current graph. Pass the proposed `oper
12
12
 
13
13
  Warning- and info-level findings are advisory. Error-severity findings caused by the proposed agent write have teeth: `canvas_apply_batch` rejects that batch and writes nothing. Historical findings remain visible for review but do not turn an unrelated scoped write into a forced cleanup. Validation never mutates the canvas or consumes an idempotency key.
14
14
 
15
- `summary.passed` means there is no structural error. `summary.reviewRequired` is true when errors, warnings, or truncated findings still require agent or human review. `completionReview` is the finalization-ready structured view: it names overlap, containment, reference lineage, output count, and product-fidelity state. Never claim Done unless `completionReview.doneClaimAllowed` is true.
15
+ `summary.passed` means there is no structural error. `summary.reviewRequired` is true when errors, warnings, or truncated findings still require agent or human review. `completionReview` is the finalization-ready structured view: it names overlap, containment, reference lineage, and delivery state. Never claim Done unless `completionReview.doneClaimAllowed` is true; pending, failed, or non-durable generated outputs block that claim even when the geometry is clean.
16
+
17
+ For a campaign, poster, banner, or multi-direction composition, structural
18
+ validation is necessary but not visual proof. Render the completed Canvas and
19
+ check the actual composition, distinctness of directions, visible product and
20
+ layout, and separation of 4:5 feed work from 16:9 ads. Report an unavailable
21
+ render or incomplete visual check as a limitation rather than claiming it was
22
+ verified.
16
23
 
17
24
  ## Blocked writes
18
25
 
@@ -30,4 +37,4 @@ When a batch is rejected with error-severity findings:
30
37
  - Use a new idempotency key if the corrected payload represents a changed intent.
31
38
  - Read and validate again after a successful write.
32
39
 
33
- Common findings include ordinary-node overlap, Section content overflow, generated output without an incoming relationship, text imitating a Section header, broken lineage handles, unclear media connections, and proposed deletions. Product-fidelity uncertainty is a review state, not authorization to regenerate.
40
+ Common findings include ordinary-node overlap, Section content overflow, generated output without an incoming relationship, text imitating a Section header, broken lineage handles, unclear media connections, and proposed deletions. Reference provenance is not authorization to regenerate.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gavana.ai/cli",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "JSON-first command-line client for the Gavana Canvas API",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,13 +1,13 @@
1
1
  // Guide metadata, lookup, and search. The prose itself lives in ../guides/*.md
2
2
  // and is loaded through ./guide-sources.mjs, so editing a guide is a content
3
3
  // change rather than a code change. Graph validation moved to
4
- // ./canvas-agent-validation.mjs in guide version 1.2.0.
4
+ // ./canvas-agent-validation.mjs in guide version 1.5.0.
5
5
  import { GAVANA_CANVAS_GUIDE_SOURCES } from "./guide-sources.mjs";
6
6
 
7
- export const GAVANA_CANVAS_GUIDE_VERSION = "1.2.0";
7
+ export const GAVANA_CANVAS_GUIDE_VERSION = "1.5.0";
8
8
  export const GAVANA_CANVAS_GUIDE_INDEX_URI = `gavana://guides/canvas/v${GAVANA_CANVAS_GUIDE_VERSION.split(".")[0]}/index`;
9
9
  export const GAVANA_CANVAS_GUIDE_WORKFLOW_INSTRUCTION =
10
- "For the first canvas task in a session, and before any unfamiliar canvas operation, inspect the relevant guide through MCP resources or call guide_search then guide_get. Before changing an existing canvas, call canvas_get. For spatial, multi-node, or destructive work, call canvas_validate with the proposed operations, apply one revision-safe atomic batch, then call canvas_validate again. When a standalone image fallback uses visual references, forward every exact node:/asset: handle (and each known role) in the image request; never reduce that work to a prompt-only generation. After an image or workflow finishes, read completionReview. Never claim Done while completionReview.doneClaimAllowed is false, blocking findings remain, or product-fidelity review is needed. Treat legacy warnings as review items, not permission to rewrite unrelated work.";
10
+ "For the first canvas task in a session, and before any unfamiliar canvas operation, inspect the relevant guide through MCP resources or call guide_search then guide_get. Before changing an existing canvas, call canvas_get. For spatial, multi-node, or destructive work, call canvas_validate with the proposed operations, apply one revision-safe atomic batch, then call canvas_validate again. Campaign work must read creative-canvas and use a reference-led concept before image output. Before image or video generation, call model_list for the required capability and use its exact model: handle; a bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access the connection instead of asking the user to add a key again. When a standalone image fallback uses visual references, forward every exact node:/asset: handle (and each known role) in the image request; never reduce that work to a prompt-only generation. After an image or workflow finishes, read completionReview. Never claim Done while completionReview.doneClaimAllowed is false, delivery is pending, failed, or non-durable, or blocking findings remain. Render campaign compositions for visual inspection. Treat legacy warnings as review items, not permission to rewrite unrelated work.";
11
11
  export const GAVANA_CANVAS_GUIDE_READ_ONLY_INSTRUCTION =
12
12
  "For the first canvas task in a session, and before any unfamiliar canvas review, inspect the relevant guide through MCP resources or call guide_search then guide_get. Call canvas_get before reasoning about an existing canvas, and use canvas_validate to audit its current graph. Treat validation warnings as review items and never claim to mutate the canvas.";
13
13
 
@@ -1,6 +1,6 @@
1
1
  // Deterministic canvas graph validation and destructive-impact reporting.
2
2
  //
3
- // Split out of canvas-agent-guide.mjs in guide version 1.2.0: guide prose is now
3
+ // Split out of canvas-agent-guide.mjs in guide version 1.5.0: guide prose is now
4
4
  // editable markdown under ../guides/, and validation is code. The two no longer
5
5
  // share a file. Validation still cites guide topics, so it imports the guide URI
6
6
  // helpers — the dependency runs one way only (validation -> guide).
@@ -333,8 +333,9 @@ function agentOwnsNode(node) {
333
333
  function buildCompletionReview(canvas, findings) {
334
334
  const generatedOutputs = canvas.nodes.filter(isGeneratedOutput);
335
335
  const durableOutputs = generatedOutputs.filter(hasDurableOutput);
336
- const pendingOutputs = generatedOutputs.filter((node) => node.metadata?.aiJobStatus === "queued" || node.metadata?.aiJobStatus === "running" || node.metadata?.outputValidationStatus === "checking");
336
+ const pendingOutputs = generatedOutputs.filter((node) => node.metadata?.aiJobStatus === "queued" || node.metadata?.aiJobStatus === "running");
337
337
  const failedOutputs = generatedOutputs.filter((node) => node.metadata?.status === "error" || node.metadata?.aiJobStatus === "failed" || node.metadata?.aiJobStatus === "canceled");
338
+ const nonDurableOutputs = generatedOutputs.filter((node) => !hasDurableOutput(node) && !pendingOutputs.includes(node) && !failedOutputs.includes(node));
338
339
  const overlap = reviewArea(findings, "node_overlap");
339
340
  const containment = reviewArea(findings, "section_content_overflow", "unsectioned_node");
340
341
  const referenceLineage = reviewArea(findings, "broken_generated_lineage", "unconnected_generated_output", "orphan_media_placeholder");
@@ -342,12 +343,13 @@ function buildCompletionReview(canvas, findings) {
342
343
  const blockingFindings = findings.filter((finding) => finding.blocksWrite === true || finding.blocksCompletion === true);
343
344
  const blockingFindingCodes = uniqueSorted(blockingFindings.map((finding) => finding.code));
344
345
  const advisoryFindingCodes = uniqueSorted(findings.filter((finding) => !blockingFindings.includes(finding)).map((finding) => finding.code));
345
- const doneClaimAllowed = blockingFindings.length === 0 && productFidelity.status !== "needs-review";
346
+ const delivery = deliveryReview(pendingOutputs, failedOutputs, nonDurableOutputs);
347
+ const doneClaimAllowed = blockingFindings.length === 0 && delivery.status === "clear";
346
348
 
347
349
  return {
348
- status: doneClaimAllowed ? "ready" : blockingFindings.length ? "blocked" : "needs-review",
350
+ status: doneClaimAllowed ? "ready" : "blocked",
349
351
  doneClaimAllowed,
350
- instruction: "Do not claim Done while blocking findings remain or product-fidelity review is needed.",
352
+ instruction: "Do not claim Done while blocking findings remain or generated output delivery is pending, failed, or non-durable.",
351
353
  outputs: {
352
354
  count: generatedOutputs.length,
353
355
  durableCount: durableOutputs.length,
@@ -359,11 +361,27 @@ function buildCompletionReview(canvas, findings) {
359
361
  containment,
360
362
  referenceLineage,
361
363
  productFidelity,
364
+ delivery,
362
365
  blockingFindingCodes,
363
366
  advisoryFindingCodes,
364
367
  };
365
368
  }
366
369
 
370
+ function deliveryReview(pendingOutputs, failedOutputs, nonDurableOutputs) {
371
+ const statuses = [
372
+ pendingOutputs.length ? "pending" : "",
373
+ failedOutputs.length ? "failed" : "",
374
+ nonDurableOutputs.length ? "non-durable" : "",
375
+ ].filter(Boolean);
376
+ return {
377
+ status: statuses.length ? "blocked" : "clear",
378
+ ...(statuses.length ? { reasons: statuses } : {}),
379
+ pendingOutputHandles: pendingOutputs.map((node) => nodeHandle(node.id)).sort(),
380
+ failedOutputHandles: failedOutputs.map((node) => nodeHandle(node.id)).sort(),
381
+ nonDurableOutputHandles: nonDurableOutputs.map((node) => nodeHandle(node.id)).sort(),
382
+ };
383
+ }
384
+
367
385
  function reviewArea(findings, ...codes) {
368
386
  const matched = findings.filter((finding) => codes.includes(finding.code));
369
387
  const blocking = matched.some((finding) => finding.blocksWrite === true || finding.blocksCompletion === true);
@@ -385,17 +403,14 @@ function productFidelityReview(canvas, generatedOutputs) {
385
403
  evidenceMissingOutputHandles: [],
386
404
  };
387
405
  }
388
- const needsReview = referenceTargets.filter((node) => node.metadata?.outputValidationStatus === "needs-review" || node.metadata?.outputValidationStatus === "checking");
389
- const evidenceMissing = referenceTargets.filter((node) => !node.metadata?.outputValidationStatus || node.metadata?.outputValidationStatus === "skipped");
390
406
  return {
391
- status: needsReview.length || evidenceMissing.length ? "needs-review" : "passed",
392
- reviewedOutputCount: referenceTargets.length - needsReview.length - evidenceMissing.length,
393
- needsReviewOutputHandles: needsReview.map((node) => nodeHandle(node.id)).sort(),
394
- evidenceMissingOutputHandles: evidenceMissing.map((node) => nodeHandle(node.id)).sort(),
395
- reasons: referenceTargets
396
- .filter((node) => typeof node.metadata?.outputValidationReason === "string" && node.metadata.outputValidationReason)
397
- .map((node) => ({ nodeHandle: nodeHandle(node.id), reason: node.metadata.outputValidationReason }))
398
- .sort((left, right) => left.nodeHandle.localeCompare(right.nodeHandle)),
407
+ // References preserve origin and intent. They never turn a retained
408
+ // creative output into a completion gate.
409
+ status: "passed",
410
+ reviewedOutputCount: referenceTargets.length,
411
+ needsReviewOutputHandles: [],
412
+ evidenceMissingOutputHandles: [],
413
+ reasons: [],
399
414
  };
400
415
  }
401
416
 
@@ -4,7 +4,7 @@
4
4
  import { GAVANA_CLI_VERSION } from "./version.mjs";
5
5
 
6
6
  export { GAVANA_CLI_VERSION };
7
- export const GAVANA_MCP_TOOLSETS = Object.freeze(["canvas", "recipes", "assets", "models", "actions", "images", "videos", "runs", "campaigns"]);
7
+ export const GAVANA_MCP_TOOLSETS = Object.freeze(["canvas", "recipes", "assets", "elements", "models", "actions", "images", "videos", "runs", "campaigns"]);
8
8
 
9
9
  import { GAVANA_TOOL_REGISTRY } from "./tools/registry.mjs";
10
10
 
@@ -37,6 +37,7 @@ const TOOLSET_FOR_PREFIX = Object.freeze([
37
37
  ["node_", "canvas"],
38
38
  ["connection_", "canvas"],
39
39
  ["asset_", "assets"],
40
+ ["element_", "elements"],
40
41
  ["provider_", "models"],
41
42
  ["model_", "models"],
42
43
  ["action_", "actions"],
@@ -44,6 +45,7 @@ const TOOLSET_FOR_PREFIX = Object.freeze([
44
45
  ["video_", "videos"],
45
46
  ["job_", "runs"],
46
47
  ["run_", "runs"],
48
+ ["work_", "canvas"],
47
49
  ]);
48
50
 
49
51
  export function gavanaToolsetForName(name) {
package/src/client.mjs CHANGED
@@ -519,6 +519,36 @@ export function createCanvasAgentClient(options = {}) {
519
519
  signal: requestOptions.signal,
520
520
  });
521
521
  },
522
+ prepareWork: (input, requestOptions = {}) =>
523
+ request("/works", {
524
+ method: "POST",
525
+ body: normalizeWorkPrepareInput(input),
526
+ signal: requestOptions.signal,
527
+ }),
528
+ getWork: (workReference, requestOptions = {}) => {
529
+ const work = parseRequiredStableHandle(workReference, "work");
530
+ return request(`/works/${encodeURIComponent(work.id)}`, { signal: requestOptions.signal });
531
+ },
532
+ refreshWork: (workReference, requestOptions = {}) => {
533
+ const work = parseRequiredStableHandle(workReference, "work");
534
+ return request(`/works/${encodeURIComponent(work.id)}/refresh`, { method: "POST", signal: requestOptions.signal });
535
+ },
536
+ continueWork: (workReference, input, requestOptions = {}) => {
537
+ const work = parseRequiredStableHandle(workReference, "work");
538
+ return request(`/works/${encodeURIComponent(work.id)}/continue`, {
539
+ method: "POST",
540
+ body: normalizeWorkContinueInput(input),
541
+ signal: requestOptions.signal,
542
+ });
543
+ },
544
+ executeWork: (workReference, input, requestOptions = {}) => {
545
+ const work = parseRequiredStableHandle(workReference, "work");
546
+ return request(`/works/${encodeURIComponent(work.id)}/execute`, {
547
+ method: "POST",
548
+ body: normalizeWorkExecuteInput(input),
549
+ signal: requestOptions.signal,
550
+ });
551
+ },
522
552
  cancelCampaign: (runReference, requestOptions = {}) => {
523
553
  const run = parseRequiredStableHandle(runReference, "run");
524
554
  return request(`/campaigns/${encodeURIComponent(run.id)}`, {
@@ -558,6 +588,94 @@ export function createCanvasAgentClient(options = {}) {
558
588
  signal: requestOptions.signal,
559
589
  });
560
590
  },
591
+ listElements: (filters = {}, requestOptions = {}) =>
592
+ request("/elements", {
593
+ query: {
594
+ search: cleanOptionalText(filters.query, "Element search query", 240),
595
+ state: filters.state === "archived" ? "archived" : filters.state === "active" ? "active" : undefined,
596
+ ...paginationQuery(requestOptions, 100),
597
+ },
598
+ signal: requestOptions.signal,
599
+ }).then(decorateElementListResponse),
600
+ getElement: (elementReference, requestOptions = {}) => {
601
+ const element = parseElementHandle(elementReference);
602
+ return request(`/elements/${encodeURIComponent(element.id)}`, {
603
+ query: element.version ? { version: element.version } : undefined,
604
+ signal: requestOptions.signal,
605
+ }).then((result) => decorateElementGetResponse(result, element));
606
+ },
607
+ listElementHistory: (elementReference, requestOptions = {}) => {
608
+ const element = parseMutableElementHandle(elementReference);
609
+ return request(`/elements/${encodeURIComponent(element.id)}/versions`, {
610
+ query: paginationQuery(requestOptions, 100),
611
+ signal: requestOptions.signal,
612
+ }).then(decorateElementHistoryResponse);
613
+ },
614
+ createElement: (input, requestOptions = {}) =>
615
+ request("/elements", {
616
+ method: "POST",
617
+ body: normalizeElementInput(input, { includeCollections: true }),
618
+ signal: requestOptions.signal,
619
+ }),
620
+ updateElement: (elementReference, input, requestOptions = {}) => {
621
+ const element = parseMutableElementHandle(elementReference);
622
+ return request(`/elements/${encodeURIComponent(element.id)}`, {
623
+ method: "PATCH",
624
+ body: { operation: "update", element: normalizeElementInput(input) },
625
+ signal: requestOptions.signal,
626
+ });
627
+ },
628
+ updateElementCollections: (elementReference, collectionIds, requestOptions = {}) => {
629
+ const element = parseMutableElementHandle(elementReference);
630
+ return request(`/elements/${encodeURIComponent(element.id)}`, {
631
+ method: "PATCH",
632
+ body: { operation: "collections", collectionIds: normalizeElementCollectionIds(collectionIds) },
633
+ signal: requestOptions.signal,
634
+ });
635
+ },
636
+ archiveElement: (elementReference, requestOptions = {}) => {
637
+ const element = parseMutableElementHandle(elementReference);
638
+ return request(`/elements/${encodeURIComponent(element.id)}`, {
639
+ method: "PATCH",
640
+ body: { operation: "archive", confirm: true },
641
+ signal: requestOptions.signal,
642
+ });
643
+ },
644
+ restoreElement: (elementReference, requestOptions = {}) => {
645
+ const element = parseMutableElementHandle(elementReference);
646
+ return request(`/elements/${encodeURIComponent(element.id)}`, {
647
+ method: "PATCH",
648
+ body: { operation: "restore" },
649
+ signal: requestOptions.signal,
650
+ });
651
+ },
652
+ listElementCollections: (requestOptions = {}) =>
653
+ request("/element-collections", {
654
+ query: paginationQuery(requestOptions, 100),
655
+ signal: requestOptions.signal,
656
+ }).then(decorateElementCollectionsResponse),
657
+ createElementCollection: (input, requestOptions = {}) =>
658
+ request("/element-collections", {
659
+ method: "POST",
660
+ body: normalizeElementCollectionInput(input),
661
+ signal: requestOptions.signal,
662
+ }),
663
+ updateElementCollection: (collectionReference, input, requestOptions = {}) => {
664
+ const collection = parseElementCollectionHandle(collectionReference);
665
+ return request(`/element-collections/${encodeURIComponent(collection.id)}`, {
666
+ method: "PATCH",
667
+ body: normalizeElementCollectionInput(input),
668
+ signal: requestOptions.signal,
669
+ });
670
+ },
671
+ deleteElementCollection: (collectionReference, requestOptions = {}) => {
672
+ const collection = parseElementCollectionHandle(collectionReference);
673
+ return request(`/element-collections/${encodeURIComponent(collection.id)}`, {
674
+ method: "DELETE",
675
+ body: { confirm: true },
676
+ signal: requestOptions.signal,
677
+ });
678
+ },
561
679
  uploadAsset: (input, requestOptions = {}) => {
562
680
  if (!input || !input.bytes) throw new CanvasAgentApiError("Image bytes are required.", { code: "usage" });
563
681
  const canvas = input.canvasReference ? parseStableHandle(input.canvasReference, "canvas") : undefined;
@@ -752,6 +870,92 @@ export function parseStableHandle(value, expectedKind) {
752
870
  return { kind: expectedKind, id: validateId(parts[1], capitalize(expectedKind)) };
753
871
  }
754
872
 
873
+ export function parseElementHandle(value) {
874
+ const raw = String(value || "").trim();
875
+ const match = /^element:([A-Za-z0-9_-]{1,180})(?:@v([1-9][0-9]*))?$/.exec(raw);
876
+ if (!match) throw new CanvasAgentApiError("Element reference must be element:<id> or element:<id>@v<n>.", { code: "usage" });
877
+ return { kind: "element", id: match[1], ...(match[2] ? { version: Number(match[2]) } : {}) };
878
+ }
879
+
880
+ export function parseMutableElementHandle(value) {
881
+ const element = parseElementHandle(value);
882
+ if (element.version) throw new CanvasAgentApiError("Element mutations require the current element:<id> handle, not a pinned version.", { code: "usage" });
883
+ return element;
884
+ }
885
+
886
+ export function parseElementCollectionHandle(value) {
887
+ const raw = String(value || "").trim();
888
+ const match = /^element-collection:([A-Za-z0-9_-]{1,180})$/.exec(raw);
889
+ if (!match) throw new CanvasAgentApiError("Element collection reference must be element-collection:<id>.", { code: "usage" });
890
+ return { kind: "element-collection", id: match[1] };
891
+ }
892
+
893
+ function normalizeElementInput(input, { includeCollections = false } = {}) {
894
+ const value = requireRecord(input, "Element");
895
+ const sourceAssetIds = Array.isArray(value.sourceAssetIds) ? value.sourceAssetIds.map((asset) => parseStableHandle(asset, "asset").id) : [];
896
+ if (!includeCollections && value.collectionIds !== undefined) throw new CanvasAgentApiError("Use the Element collections operation to change collection membership.", { code: "usage" });
897
+ const collectionIds = includeCollections && value.collectionIds !== undefined ? normalizeElementCollectionIds(value.collectionIds) : undefined;
898
+ return {
899
+ name: cleanRequiredText(value.name, "Element name", 160),
900
+ type: cleanRequiredText(value.type, "Element type", 80),
901
+ sourceAssetIds,
902
+ ...(cleanOptionalText(value.guidelines, "Element guidelines", 2_000) ? { guidelines: cleanOptionalText(value.guidelines, "Element guidelines", 2_000) } : {}),
903
+ ...(collectionIds !== undefined ? { collectionIds } : {}),
904
+ };
905
+ }
906
+
907
+ function cleanRequiredText(value, label, maximum) {
908
+ const cleaned = cleanOptionalText(value, label, maximum);
909
+ if (!cleaned) throw new CanvasAgentApiError(`${label} is required.`, { code: "usage" });
910
+ return cleaned;
911
+ }
912
+
913
+ function normalizeElementCollectionIds(value) {
914
+ if (!Array.isArray(value) || value.length > 24) throw new CanvasAgentApiError("Choose up to 24 element collections.", { code: "usage" });
915
+ const ids = value.map((collection) => parseElementCollectionHandle(collection).id);
916
+ if (new Set(ids).size !== ids.length) throw new CanvasAgentApiError("Element collections must be unique.", { code: "usage" });
917
+ return ids;
918
+ }
919
+
920
+ function normalizeElementCollectionInput(input) {
921
+ const value = requireRecord(input, "Element collection");
922
+ return { name: cleanRequiredText(value.name, "Element collection name", 120) };
923
+ }
924
+
925
+ function decorateElementListResponse(result) {
926
+ if (!isRecord(result) || !Array.isArray(result.elements)) return result;
927
+ return { ...result, elements: result.elements.map(decorateElement) };
928
+ }
929
+
930
+ function decorateElementGetResponse(result, reference) {
931
+ if (!isRecord(result)) return result;
932
+ if (reference.version && isRecord(result.version)) return { ...result, version: decorateElementVersion(result.version, reference.id) };
933
+ if (isRecord(result.element)) return { ...result, element: decorateElement(result.element) };
934
+ return result;
935
+ }
936
+
937
+ function decorateElementHistoryResponse(result) {
938
+ if (!isRecord(result) || !Array.isArray(result.versions)) return result;
939
+ return { ...result, versions: result.versions.map(decorateElementVersion) };
940
+ }
941
+
942
+ function decorateElementCollectionsResponse(result) {
943
+ if (!isRecord(result) || !Array.isArray(result.collections)) return result;
944
+ return { ...result, collections: result.collections.map((collection) => (isRecord(collection) && typeof collection.id === "string" ? { ...collection, handle: `element-collection:${collection.id}` } : collection)) };
945
+ }
946
+
947
+ function decorateElement(element) {
948
+ if (!isRecord(element) || typeof element.id !== "string" || !Number.isInteger(element.version) || element.version < 1) return element;
949
+ return { ...element, handle: `element:${element.id}`, versionHandle: `element:${element.id}@v${element.version}` };
950
+ }
951
+
952
+ function decorateElementVersion(version, fallbackElementId) {
953
+ if (!isRecord(version)) return version;
954
+ const elementId = typeof version.elementId === "string" ? version.elementId : fallbackElementId;
955
+ if (!elementId || !Number.isInteger(version.version) || version.version < 1) return version;
956
+ return { ...version, handle: `element:${elementId}@v${version.version}` };
957
+ }
958
+
755
959
  export function parseRequiredStableHandle(value, expectedKind) {
756
960
  const raw = String(value || "").trim();
757
961
  if (!raw.startsWith(`${expectedKind}:`)) throw invalidHandle(expectedKind);
@@ -842,6 +1046,54 @@ function normalizeCampaignReviewInput(input) {
842
1046
  };
843
1047
  }
844
1048
 
1049
+ function normalizeWorkPrepareInput(input) {
1050
+ const source = requireRecord(input, "work prepare");
1051
+ const references = source.references === undefined ? undefined : normalizeWorkReferences(source.references);
1052
+ const canvas = source.canvasId === undefined ? undefined : stableHandle(parseRequiredStableHandle(source.canvasId, "canvas"));
1053
+ return {
1054
+ request: cleanRequiredText(source.request, "Work request", 8_000),
1055
+ ...(references ? { references } : {}),
1056
+ ...(canvas ? { canvasId: canvas } : {}),
1057
+ idempotencyKey: requiredIdempotencyKey(source.idempotencyKey),
1058
+ };
1059
+ }
1060
+
1061
+ function normalizeWorkContinueInput(input) {
1062
+ const source = requireRecord(input, "work continuation");
1063
+ const idempotencyKey = requiredIdempotencyKey(source.idempotencyKey);
1064
+ const action = source.action === undefined ? "" : cleanRequiredText(source.action, "Work continuation action", 80);
1065
+ const rebase = source.rebase === true;
1066
+ if (rebase || action === "acknowledge_canvas") {
1067
+ if (action && action !== "acknowledge_canvas") throw new CanvasAgentApiError("rebase may only be used with acknowledge_canvas.", { code: "usage" });
1068
+ if (source.answer !== undefined || source.directionId !== undefined || source.adjustment !== undefined) throw new CanvasAgentApiError("Canvas acknowledgement cannot include an answer, selection, or adjustment.", { code: "usage" });
1069
+ return { ...(action ? { action } : {}), ...(rebase ? { rebase: true } : {}), idempotencyKey };
1070
+ }
1071
+ if (action === "answer") return { action, answer: cleanRequiredText(source.answer, "Work answer", 2_000), idempotencyKey };
1072
+ if (action === "select_direction") return { action, directionId: cleanRequiredText(source.directionId, "Work direction", 400), idempotencyKey };
1073
+ if (action === "adjust") return { action, adjustment: cleanRequiredText(source.adjustment, "Work adjustment", 2_000), idempotencyKey };
1074
+ throw new CanvasAgentApiError("Work continuation action must be answer, select_direction, adjust, or acknowledge_canvas.", { code: "usage" });
1075
+ }
1076
+
1077
+ function normalizeWorkExecuteInput(input) {
1078
+ const source = requireRecord(input, "work execute");
1079
+ if (source.confirm !== true) throw new CanvasAgentApiError("Work execution requires confirm: true.", { code: "usage" });
1080
+ return { confirm: true, idempotencyKey: requiredIdempotencyKey(source.idempotencyKey) };
1081
+ }
1082
+
1083
+ function normalizeWorkReferences(value) {
1084
+ if (!Array.isArray(value) || value.length > 12) throw new CanvasAgentApiError("Work references must contain at most 12 node: or asset: handles.", { code: "usage" });
1085
+ return value.map((reference) => {
1086
+ const source = isRecord(reference) ? reference : { handle: reference };
1087
+ const raw = String(source.handle || "").trim();
1088
+ const kind = raw.startsWith("node:") ? "node" : raw.startsWith("asset:") ? "asset" : "";
1089
+ if (!kind) throw new CanvasAgentApiError("Work references must be node: or asset: handles.", { code: "usage" });
1090
+ const role = source.role;
1091
+ if (role !== undefined && role !== "identity" && role !== "style" && role !== "product") throw new CanvasAgentApiError("Work reference role must be identity, style, or product.", { code: "usage" });
1092
+ const handle = stableHandle(parseRequiredStableHandle(raw, kind));
1093
+ return role ? { handle, role } : handle;
1094
+ });
1095
+ }
1096
+
845
1097
  function normalizeCampaignProductReference(value) {
846
1098
  if (value === undefined || value === null) return undefined;
847
1099
  const reference = isRecord(value) ? (value.handle ?? value.nodeId ?? value.assetId) : value;