@gavana.ai/cli 0.2.2 → 0.3.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.
@@ -3,7 +3,7 @@ import { elementInput } from "./schemas.mjs";
3
3
  export function defineElementCreate(client) {
4
4
  return {
5
5
  title: "Create a Gavana Element",
6
- description: "Create a reusable visual Element from 1-8 existing image assets, guidelines, or both.",
6
+ description: "Create a reusable visual Element. creative Elements inspire, identity Elements guide recognizable subjects, and exact Elements require one PNG source for deterministic composition.",
7
7
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
8
8
  inputSchema: elementInput,
9
9
  handler: (input) => client.createElement(input),
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import { elementType, mutableElementReference, assetReference } from "./schemas.mjs";
2
+ import { elementApplicationMode, elementType, mutableElementReference, assetReference } from "./schemas.mjs";
3
3
 
4
4
  export function defineElementUpdate(client) {
5
5
  return {
@@ -13,6 +13,7 @@ export function defineElementUpdate(client) {
13
13
  type: elementType,
14
14
  sourceAssetIds: z.array(assetReference).max(8).default([]),
15
15
  guidelines: z.string().max(2_000).optional(),
16
+ applicationMode: elementApplicationMode.optional(),
16
17
  })
17
18
  .strict()
18
19
  .refine((value) => value.sourceAssetIds.length > 0 || Boolean(value.guidelines?.trim()), "Add at least one source asset or guideline."),
@@ -11,6 +11,6 @@ export function defineGuideGet(client) {
11
11
  description: `Read one canonical Gavana Canvas Agent Guide topic. Start at ${GAVANA_CANVAS_GUIDE_INDEX_URI} or use guide_search to find an exact topic ID.`,
12
12
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
13
13
  inputSchema: z.object({ guideId: z.string().min(1).max(600).describe("Guide topic ID or exact gavana:// guide URI returned by guide_search or resources/list.") }),
14
- handler: ({ guideId }) => getCanvasGuide(guideId),
14
+ handler: ({ guideId }) => getCanvasGuide(guideId, "local"),
15
15
  };
16
16
  }
@@ -11,6 +11,6 @@ export function defineGuideSearch(client) {
11
11
  description: `Search the canonical Gavana Canvas Agent Guide v${GAVANA_CANVAS_GUIDE_VERSION}. Use this before the first canvas mutation in a session or whenever an operation is unfamiliar.`,
12
12
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
13
13
  inputSchema: z.object({ query: z.string().max(240).default(""), limit: z.number().int().min(1).max(10).default(10) }),
14
- handler: ({ query, limit }) => searchCanvasGuides(query, limit),
14
+ handler: ({ query, limit }) => searchCanvasGuides(query, limit, "local"),
15
15
  };
16
16
  }
@@ -11,16 +11,21 @@ export function imageToolDefinition(operation, client) {
11
11
  title: `${operation === "generate" ? "Generate" : operation === "edit" ? "Edit" : "Vary"} canvas images`,
12
12
  description:
13
13
  operation === "generate"
14
- ? "Queue image generation into existing target image nodes. Campaign work needs a visible reference-led concept before output; reuse existing Canvas node:/asset: handles for product identity, brand-world, and typography/layout references. For an exact product, logo, face, garment, or other visual identity, do not rely on prompt-only generation: use a user-provided local image (or an existing node:/asset: handle), then call image_edit with references."
14
+ ? "Queue image generation into existing target image nodes. Campaign work needs a visible reference-led concept before output; reuse existing Canvas node:/asset: handles for product identity, brand-world, and typography/layout references. identity Elements guide preservation but do not guarantee identical pixels. exact Elements require normalized placements and are deterministically composited from original source pixels. Set preflight true to inspect the compiled reference plan before paid generation. For several images set count (1-4): Gavana writes one image node per output. Never describe multiple panels, frames, or a collage in one prompt, and when you pass targetNodeIds pass exactly one target node per output."
15
15
  : operation === "edit"
16
- ? "Queue an image edit using stable node: or asset: references. Reuse a Canvas image by its existing handle rather than re-uploading it. Use this for exact product or visual identity work; call asset_upload first only when the user supplied a local image."
17
- : "Queue variations using an existing node: or asset: source.",
16
+ ? "Queue an image edit using stable node: or asset: references. Reuse a Canvas image by its existing handle rather than re-uploading it. For a local image the user explicitly supplied, call asset_upload first. identity guidance is not pixel-exact; use an exact Element with placements for deterministic logo or wordmark composition. Set preflight true to inspect the compiled reference plan before paid generation. For several images set count (1-4): Gavana writes one image node per output. Never describe multiple panels, frames, or a collage in one prompt, and when you pass targetNodeIds pass exactly one target node per output."
17
+ : "Queue variations using an existing node: or asset: source. For several images set count (1-4): Gavana writes one image node per output. Never describe multiple panels, frames, or a collage in one prompt, and when you pass targetNodeIds pass exactly one target node per output.",
18
18
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
19
19
  inputSchema: imageSchema(),
20
20
  markdown: true,
21
21
  handler: async (input, extra) => {
22
22
  validateImageInput(operation, input);
23
23
  const destination = input.destination || input.canvasId;
24
+ if (input.preflight === true) {
25
+ const { destination: _destination, canvasId: _canvasId, canvasTitle: _canvasTitle, preflight: _preflight, wait: _wait, timeoutSeconds: _timeoutSeconds, ...preflightInput } = input;
26
+ const plan = await client.preflightImage(operation, { ...preflightInput, canvasId: destination });
27
+ return { ...plan, destination: { requested: destination, canvasId: destination, targetNodeIds: input.targetNodeIds } };
28
+ }
24
29
  const prepared = await client.prepareImageDestination({
25
30
  ...input,
26
31
  operation,
@@ -36,6 +41,7 @@ export function imageToolDefinition(operation, client) {
36
41
  targetY: _targetY,
37
42
  targetWidth: _targetWidth,
38
43
  targetHeight: _targetHeight,
44
+ preflight: _preflight,
39
45
  wait: _wait,
40
46
  timeoutSeconds: _timeoutSeconds,
41
47
  ...jobInput
@@ -144,7 +144,11 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
144
144
  {
145
145
  name: "canvas_create",
146
146
  toolset: "canvas",
147
- surfaces: ["local"],
147
+ surfaces: ["hosted","local"],
148
+ scopes: ["canvas:read","canvas:write"],
149
+ hostedOrder: 6,
150
+ hostedReadOnly: false,
151
+ hostedToolset: "canvas",
148
152
  localOrder: 15,
149
153
  annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":false,"openWorldHint":false},
150
154
  },
@@ -260,6 +264,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
260
264
  scopes: ["canvas:read","job:manage"],
261
265
  hostedOrder: 12,
262
266
  hostedReadOnly: false,
267
+ hostedCategory: "read",
263
268
  hostedToolset: "runs",
264
269
  embedImage: true,
265
270
  annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},
@@ -284,7 +289,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
284
289
  toolset: "elements",
285
290
  surfaces: ["hosted","local"],
286
291
  scopes: ["element:read","element:write"],
287
- hostedOrder: 17,
292
+ hostedOrder: 18,
288
293
  hostedReadOnly: false,
289
294
  hostedToolset: "elements",
290
295
  localOrder: 48,
@@ -296,7 +301,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
296
301
  toolset: "elements",
297
302
  surfaces: ["hosted","local"],
298
303
  scopes: ["element:read","element:write"],
299
- hostedOrder: 18,
304
+ hostedOrder: 19,
300
305
  hostedReadOnly: false,
301
306
  hostedToolset: "elements",
302
307
  localOrder: 50,
@@ -307,7 +312,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
307
312
  toolset: "elements",
308
313
  surfaces: ["hosted","local"],
309
314
  scopes: ["element:read","element:write"],
310
- hostedOrder: 19,
315
+ hostedOrder: 20,
311
316
  hostedReadOnly: false,
312
317
  hostedToolset: "elements",
313
318
  localOrder: 53,
@@ -320,7 +325,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
320
325
  surfaces: ["hosted","local"],
321
326
  scopes: ["element:read"],
322
327
  readOnlySurface: true,
323
- hostedOrder: 20,
328
+ hostedOrder: 21,
324
329
  hostedReadOnly: true,
325
330
  hostedToolset: "elements",
326
331
  localOrder: 49,
@@ -331,7 +336,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
331
336
  toolset: "elements",
332
337
  surfaces: ["hosted","local"],
333
338
  scopes: ["element:read","element:write"],
334
- hostedOrder: 21,
339
+ hostedOrder: 22,
335
340
  hostedReadOnly: false,
336
341
  hostedToolset: "elements",
337
342
  localOrder: 52,
@@ -342,7 +347,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
342
347
  toolset: "elements",
343
348
  surfaces: ["hosted","local"],
344
349
  scopes: ["element:read","element:write"],
345
- hostedOrder: 22,
350
+ hostedOrder: 23,
346
351
  hostedReadOnly: false,
347
352
  hostedToolset: "elements",
348
353
  localOrder: 45,
@@ -354,7 +359,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
354
359
  surfaces: ["hosted","local"],
355
360
  scopes: ["element:read"],
356
361
  readOnlySurface: true,
357
- hostedOrder: 23,
362
+ hostedOrder: 24,
358
363
  hostedReadOnly: true,
359
364
  hostedToolset: "elements",
360
365
  localOrder: 43,
@@ -366,7 +371,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
366
371
  surfaces: ["hosted","local"],
367
372
  scopes: ["element:read"],
368
373
  readOnlySurface: true,
369
- hostedOrder: 24,
374
+ hostedOrder: 25,
370
375
  hostedReadOnly: true,
371
376
  hostedToolset: "elements",
372
377
  localOrder: 44,
@@ -378,7 +383,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
378
383
  surfaces: ["hosted","local"],
379
384
  scopes: ["element:read"],
380
385
  readOnlySurface: true,
381
- hostedOrder: 25,
386
+ hostedOrder: 26,
382
387
  hostedReadOnly: true,
383
388
  hostedToolset: "elements",
384
389
  localOrder: 42,
@@ -389,7 +394,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
389
394
  toolset: "elements",
390
395
  surfaces: ["hosted","local"],
391
396
  scopes: ["element:read","element:write"],
392
- hostedOrder: 26,
397
+ hostedOrder: 27,
393
398
  hostedReadOnly: false,
394
399
  hostedToolset: "elements",
395
400
  localOrder: 47,
@@ -400,7 +405,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
400
405
  toolset: "elements",
401
406
  surfaces: ["hosted","local"],
402
407
  scopes: ["element:read","element:write"],
403
- hostedOrder: 27,
408
+ hostedOrder: 28,
404
409
  hostedReadOnly: false,
405
410
  hostedToolset: "elements",
406
411
  localOrder: 46,
@@ -411,7 +416,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
411
416
  toolset: "elements",
412
417
  surfaces: ["hosted","local"],
413
418
  scopes: ["element:read","element:write"],
414
- hostedOrder: 28,
419
+ hostedOrder: 29,
415
420
  hostedReadOnly: false,
416
421
  hostedToolset: "elements",
417
422
  localOrder: 51,
@@ -455,7 +460,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
455
460
  hostedAlias: "generate_image_in_canvas",
456
461
  scopes: ["canvas:read","canvas:write","asset:read","image:generate","job:manage"],
457
462
  conditionalScopes: { elements: ["element:read"] },
458
- hostedOrder: 13,
463
+ hostedOrder: 14,
459
464
  paid: true,
460
465
  hostedReadOnly: false,
461
466
  hostedToolset: "images",
@@ -551,6 +556,18 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
551
556
  localOrder: 20,
552
557
  annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},
553
558
  },
559
+ {
560
+ name: "product_photoshoot_generate",
561
+ toolset: "images",
562
+ surfaces: ["hosted"],
563
+ hostedAlias: "generate_product_photoshoot_in_canvas",
564
+ scopes: ["canvas:read","canvas:write","asset:read","image:generate","job:manage"],
565
+ hostedOrder: 13,
566
+ paid: true,
567
+ hostedReadOnly: false,
568
+ hostedToolset: "images",
569
+ annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":true},
570
+ },
554
571
  {
555
572
  name: "product_reference_pack_import",
556
573
  toolset: "assets",
@@ -611,10 +628,30 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
611
628
  {
612
629
  name: "run_get",
613
630
  toolset: "runs",
614
- surfaces: ["local"],
631
+ surfaces: ["hosted","local"],
632
+ hostedAlias: "get_image_run",
633
+ scopes: ["job:manage"],
634
+ embedImage: true,
635
+ hostedOrder: 35,
636
+ hostedReadOnly: false,
637
+ hostedCategory: "read",
638
+ hostedToolset: "runs",
615
639
  localOrder: 42,
616
640
  annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},
617
641
  },
642
+ {
643
+ name: "run_list",
644
+ toolset: "jobs",
645
+ surfaces: ["hosted","local"],
646
+ scopes: ["job:manage"],
647
+ hostedAlias: "list_image_runs",
648
+ hostedOrder: 36,
649
+ hostedReadOnly: false,
650
+ hostedToolset: "jobs",
651
+ localOrder: 41,
652
+ readOnlySurface: true,
653
+ annotations: {"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},
654
+ },
618
655
  {
619
656
  name: "run_wait",
620
657
  toolset: "runs",
@@ -627,8 +664,8 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
627
664
  toolset: "videos",
628
665
  surfaces: ["hosted","local"],
629
666
  hostedAlias: "generate_video",
630
- scopes: ["canvas:read","asset:read","video:generate","job:manage"],
631
- hostedOrder: 15,
667
+ scopes: ["canvas:read","canvas:write","asset:read","video:generate","job:manage"],
668
+ hostedOrder: 16,
632
669
  paid: true,
633
670
  hostedReadOnly: false,
634
671
  hostedToolset: "videos",
@@ -641,8 +678,9 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
641
678
  surfaces: ["hosted"],
642
679
  hostedAlias: "get_video_job",
643
680
  scopes: ["job:manage"],
644
- hostedOrder: 16,
681
+ hostedOrder: 17,
645
682
  hostedReadOnly: false,
683
+ hostedCategory: "read",
646
684
  hostedToolset: "runs",
647
685
  annotations: {"readOnlyHint":false,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},
648
686
  },
@@ -652,8 +690,8 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
652
690
  surfaces: ["hosted"],
653
691
  hostedAlias: "find_video_models",
654
692
  scopes: ["video:generate"],
655
- hostedOrder: 14,
656
- hostedReadOnly: true,
693
+ hostedOrder: 15,
694
+ hostedReadOnly: false,
657
695
  hostedToolset: "models",
658
696
  annotations: {"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true,"openWorldHint":false},
659
697
  },
@@ -662,7 +700,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
662
700
  toolset: "canvas",
663
701
  surfaces: ["hosted","local"],
664
702
  scopes: ["canvas:read","canvas:write"],
665
- hostedOrder: 31,
703
+ hostedOrder: 32,
666
704
  hostedReadOnly: false,
667
705
  hostedToolset: "canvas",
668
706
  localOrder: 56,
@@ -674,7 +712,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
674
712
  toolset: "canvas",
675
713
  surfaces: ["hosted","local"],
676
714
  scopes: ["canvas:read","canvas:write","asset:read","image:generate"],
677
- hostedOrder: 32,
715
+ hostedOrder: 33,
678
716
  paid: true,
679
717
  hostedReadOnly: false,
680
718
  hostedToolset: "canvas",
@@ -688,7 +726,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
688
726
  surfaces: ["hosted","local"],
689
727
  scopes: ["canvas:read"],
690
728
  readOnlySurface: true,
691
- hostedOrder: 30,
729
+ hostedOrder: 31,
692
730
  hostedReadOnly: true,
693
731
  hostedToolset: "canvas",
694
732
  localOrder: 55,
@@ -700,7 +738,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
700
738
  toolset: "canvas",
701
739
  surfaces: ["hosted","local"],
702
740
  scopes: ["canvas:read","canvas:write","asset:read"],
703
- hostedOrder: 29,
741
+ hostedOrder: 30,
704
742
  hostedReadOnly: false,
705
743
  hostedToolset: "canvas",
706
744
  localOrder: 54,
@@ -712,7 +750,7 @@ export const GAVANA_TOOL_REGISTRY = Object.freeze([
712
750
  toolset: "canvas",
713
751
  surfaces: ["hosted","local"],
714
752
  scopes: ["canvas:read","canvas:write","image:generate"],
715
- hostedOrder: 33,
753
+ hostedOrder: 34,
716
754
  hostedReadOnly: false,
717
755
  hostedToolset: "canvas",
718
756
  localOrder: 58,
@@ -744,8 +782,16 @@ export function gavanaToolsForSurface(surface, { includeLegacyCampaign = false,
744
782
  // each surface keeps the order it shipped with rather than falling out of the
745
783
  // registry's alphabetical layout.
746
784
  const key = surface === "hosted" ? "hostedOrder" : "localOrder";
785
+ // The two read-only flags are not the same question, and `run_list` is the
786
+ // first tool where they part. Local read-only serves whatever declares
787
+ // `readOnlyHint`, so a tool that only reads belongs there. Hosted read-only
788
+ // additionally has to grant the tool's OAuth scopes out of a read-only set
789
+ // that does not contain `job:manage`, so the same tool cannot be served
790
+ // there at all. Filtering both surfaces through the local flag advertised a
791
+ // hosted tool the hosted server would never serve.
792
+ const readOnlyKey = surface === "hosted" ? "hostedReadOnly" : "readOnlySurface";
747
793
  return GAVANA_TOOL_REGISTRY.filter(
748
- (tool) => tool.surfaces.includes(surface) && (includeLegacyCampaign || !tool.legacyCampaign) && (!readOnly || tool.readOnlySurface),
794
+ (tool) => tool.surfaces.includes(surface) && (includeLegacyCampaign || !tool.legacyCampaign) && (!readOnly || tool[readOnlyKey]),
749
795
  )
750
796
  .slice()
751
797
  .sort((left, right) => (left[key] ?? 0) - (right[key] ?? 0));
@@ -0,0 +1,20 @@
1
+ // Tool definition: run_list
2
+ //
3
+ // Registered through ./definitions.mjs. ./registry.mjs is the catalog that says
4
+ // this tool exists and on which surfaces; this file is what it does.
5
+ import { z } from "zod";
6
+
7
+ export function defineRunList(client) {
8
+ return {
9
+ title: "List Gavana image runs",
10
+ description:
11
+ "List image and Action runs this account has started, newest first, with their status and canvas. Use this to answer questions about work already in flight, to recover a run handle you no longer have, and to see which runs are holding the concurrent-job limit.",
12
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
13
+ inputSchema: z.object({
14
+ status: z.enum(["active", "all"]).default("active").describe("active returns only runs still queued or running; all includes finished and failed runs."),
15
+ canvasId: z.string().min(1).max(240).optional().describe("Restrict to runs targeting one canvas."),
16
+ limit: z.number().int().min(1).max(50).default(20),
17
+ }),
18
+ handler: (input) => client.listRuns(input),
19
+ };
20
+ }
@@ -26,13 +26,17 @@ const elementCollectionReference = z
26
26
  .string()
27
27
  .regex(/^element-collection:[A-Za-z0-9_-]{1,180}$/)
28
28
  .describe("Stable Element collection handle element-collection:<id>.");
29
- const elementType = z.enum(["character", "product/object", "environment", "style", "material/texture", "lighting"]);
29
+ const elementType = z.enum(["character", "product/object", "environment", "style", "material/texture", "lighting", "brand/mark"]);
30
+ const elementApplicationMode = z
31
+ .enum(["creative", "identity", "exact"])
32
+ .describe("creative permits visual reinterpretation; identity guides preservation but is not pixel-exact; exact composites original Element pixels after generation.");
30
33
  const elementInput = z
31
34
  .object({
32
35
  name: z.string().min(1).max(160),
33
36
  type: elementType,
34
37
  sourceAssetIds: z.array(assetReference).max(8).default([]),
35
38
  guidelines: z.string().max(2_000).optional(),
39
+ applicationMode: elementApplicationMode.optional().describe("creative uses an Element as inspiration; identity guides recognizable subjects; exact preserves an original PNG through deterministic composition."),
36
40
  collectionIds: z.array(elementCollectionReference).max(24).optional(),
37
41
  })
38
42
  .strict()
@@ -119,6 +123,15 @@ const approvedOutputReferences = z
119
123
  const campaignIdempotencyKey = z.string().min(8).max(200).describe("Required caller-stable retry key. Reuse it only for the exact same campaign action.");
120
124
  const recipeForkIdempotencyKey = z.string().min(8).max(200).describe("Required caller-stable retry key. Reuse it only for the exact same Recipe fork.");
121
125
  const campaignAspectRatio = z.enum(["1:1", "4:5", "3:4", "16:9", "9:16"]);
126
+ const exactElementPlacement = z
127
+ .object({
128
+ x: z.number().min(0).max(1).describe("Normalized left coordinate in the output, from 0 through 1."),
129
+ y: z.number().min(0).max(1).describe("Normalized top coordinate in the output, from 0 through 1."),
130
+ width: z.number().gt(0).max(1).describe("Normalized width in the output, greater than 0 through 1."),
131
+ height: z.number().gt(0).max(1).describe("Normalized height in the output, greater than 0 through 1."),
132
+ })
133
+ .strict()
134
+ .refine((placement) => placement.x + placement.width <= 1 && placement.y + placement.height <= 1, "Exact Element placement must remain inside the normalized output bounds.");
122
135
  const elementGenerationInput = z
123
136
  .union([
124
137
  pinnedElementReference,
@@ -127,10 +140,16 @@ const elementGenerationInput = z
127
140
  handle: pinnedElementReference,
128
141
  role: z.enum(["identity", "construction", "texture", "fit", "style"]).optional(),
129
142
  influence: z.number().min(0).max(1).optional(),
143
+ applicationMode: elementApplicationMode.optional(),
144
+ currentTurnUserModeOverride: z
145
+ .literal(true)
146
+ .optional()
147
+ .describe("Required only when the current user explicitly asks to override this Element version's stored application mode."),
148
+ placements: z.array(exactElementPlacement).min(1).max(16).optional().describe("Required for exact Elements. Repeat placements to composite unchanged source pixels more than once."),
130
149
  })
131
150
  .strict(),
132
151
  ])
133
- .describe("A version-pinned Element, optionally with the visual role and influence to apply.");
152
+ .describe("A version-pinned Element. identity is guided preservation, not pixel-exact. exact requires placements and returns exact-composed only when original Element pixels are deterministically composited.");
134
153
  const MAX_LOCAL_REFERENCE_IMAGE_BYTES = 50 * 1024 * 1024;
135
154
  const revisionFields = {
136
155
  baseRevision: z.string().min(1).optional().describe("Revision returned by canvas_get. Omit to read the latest revision immediately before the write."),
@@ -165,7 +184,16 @@ function imageSchema() {
165
184
  model: z.union([modelReference, rawModelId]).optional(),
166
185
  size: z.string().max(80).optional(),
167
186
  quality: z.string().max(80).optional(),
168
- count: z.number().int().min(1).max(4).optional(),
187
+ count: z
188
+ .number()
189
+ .int()
190
+ .min(1)
191
+ .max(4)
192
+ .optional()
193
+ .describe(
194
+ "Number of separate images to generate, 1-4. Gavana creates one image node per output, and this is the only way to produce more than one image. Never ask for several panels, frames, or a collage inside one prompt. When you pass targetNodeIds, count must equal the number of targetNodeIds; when you omit count, Gavana uses the number of targets.",
195
+ ),
196
+ preflight: z.boolean().optional().describe("Compile and validate the final reference plan without creating targets, starting a provider request, or consuming generation credits. Requires an existing canvas and explicit targetNodeIds."),
169
197
  wait: z.boolean().default(false).describe("Return after queueing by default. Set true to wait for the completed Run."),
170
198
  timeoutSeconds: z.number().min(1).max(3_600).default(900),
171
199
  });
@@ -182,6 +210,11 @@ function validateImageInput(operation, value) {
182
210
  // the job, and the blank node would be left behind as an orphan.
183
211
  if (operation !== "variations" && !value.prompt && !value.promptNodeId && !value.targetNodeIds?.length) issues.push("Provide prompt or promptNodeId, or pass targetNodeIds whose nodes have a prompt connection.");
184
212
  if (operation !== "generate" && !value.source && !value.references?.length) issues.push(`${operation} requires source or references.`);
213
+ if (value.preflight) {
214
+ const destination = value.destination || value.canvasId;
215
+ if (destination === "agent-canvas" || destination === "new-canvas") issues.push("preflight requires an existing canvas handle, not agent-canvas or new-canvas.");
216
+ if (!value.targetNodeIds?.length) issues.push("preflight requires explicit targetNodeIds; it never creates target nodes.");
217
+ }
185
218
  if (!issues.length) return;
186
219
  const error = new Error(`Input validation error: ${issues.join(" ")}`);
187
220
  error.code = "invalid_image_input";
@@ -195,6 +228,7 @@ export {
195
228
  assetReference,
196
229
  elementCollectionReference,
197
230
  elementGenerationInput,
231
+ elementApplicationMode,
198
232
  elementInput,
199
233
  elementType,
200
234
  campaignAspectRatio,
@@ -0,0 +1,120 @@
1
+ // Tool vocabulary per surface.
2
+ //
3
+ // The hosted and local servers advertise different catalogs under different
4
+ // naming conventions (see ./registry.mjs). The Canvas Agent Guide is shared
5
+ // prose, so a sentence written in one surface's vocabulary reaches agents on
6
+ // the other surface naming a tool they cannot call. That is not a hypothetical:
7
+ // the guide told every agent to "call `model_list` before image or video
8
+ // generation", and `model_list` is local-only, so hosted agents either guessed
9
+ // a substitute or gave up on a mandatory pre-flight step.
10
+ //
11
+ // This module is the one place that answers "what is this tool called here, and
12
+ // does it exist here at all". scripts/canvas-agent-guide.test.mjs asserts that
13
+ // no served guide text names a tool the reading surface cannot call, so the
14
+ // drift fails the build instead of reaching an agent.
15
+
16
+ import { GAVANA_TOOL_REGISTRY } from "./registry.mjs";
17
+
18
+ export const GAVANA_TOOL_SURFACES = Object.freeze(["hosted", "local"]);
19
+
20
+ /** Name a surface advertises for one registry entry, or undefined when it does not advertise it. */
21
+ function advertisedNameFor(tool, surface) {
22
+ if (!tool.surfaces.includes(surface)) return undefined;
23
+ return surface === "hosted" ? (tool.hostedAlias ?? tool.name) : tool.name;
24
+ }
25
+
26
+ /**
27
+ * Every spelling of every tool, mapped to what `surface` calls it.
28
+ *
29
+ * `rename` covers both directions: a canonical name written in the guide maps to
30
+ * the hosted alias when hosted advertises it, and a hosted alias maps back to the
31
+ * canonical name for the local surface. `absent` holds tools no spelling of which
32
+ * the surface can call.
33
+ */
34
+ export function surfaceToolVocabulary(surface) {
35
+ if (!GAVANA_TOOL_SURFACES.includes(surface)) {
36
+ throw new Error(`Unknown tool surface: ${surface}. Expected one of ${GAVANA_TOOL_SURFACES.join(", ")}.`);
37
+ }
38
+ const advertised = new Set();
39
+ const rename = new Map();
40
+ const absent = new Set();
41
+
42
+ for (const tool of GAVANA_TOOL_REGISTRY) {
43
+ const spellings = [tool.name, tool.hostedAlias].filter(Boolean);
44
+ const here = advertisedNameFor(tool, surface);
45
+ if (!here) {
46
+ for (const spelling of spellings) absent.add(spelling);
47
+ continue;
48
+ }
49
+ advertised.add(here);
50
+ for (const spelling of spellings) {
51
+ if (spelling !== here) rename.set(spelling, here);
52
+ }
53
+ }
54
+
55
+ // A spelling this surface advertises is never "absent", even when another
56
+ // registry entry shares the word. Advertised always wins.
57
+ for (const name of advertised) absent.delete(name);
58
+ for (const spelling of rename.keys()) absent.delete(spelling);
59
+
60
+ return { surface, advertised, rename, absent };
61
+ }
62
+
63
+ const VOCABULARIES = new Map(GAVANA_TOOL_SURFACES.map((surface) => [surface, surfaceToolVocabulary(surface)]));
64
+
65
+ /** Cached vocabulary. Callers must not mutate the returned sets. */
66
+ export function toolVocabulary(surface) {
67
+ const vocabulary = VOCABULARIES.get(surface);
68
+ if (!vocabulary) throw new Error(`Unknown tool surface: ${surface}. Expected one of ${GAVANA_TOOL_SURFACES.join(", ")}.`);
69
+ return vocabulary;
70
+ }
71
+
72
+ // Word boundary that keeps `run_get` from matching inside `canvas_workflow_run_get`.
73
+ const identifier = (name) => new RegExp(`(?<![A-Za-z0-9_])${name}(?![A-Za-z0-9_])`, "g");
74
+
75
+ const CONDITIONAL = /\{\{#(hosted|local)\}\}([\s\S]*?)\{\{\/\1\}\}/g;
76
+
77
+ /**
78
+ * Resolve `{{#hosted}}…{{/hosted}}` / `{{#local}}…{{/local}}` spans for one surface.
79
+ *
80
+ * Guide prose uses these where the two surfaces genuinely differ in capability
81
+ * rather than only in spelling — an instruction that cannot be satisfied at all
82
+ * on one surface must not merely be renamed there.
83
+ */
84
+ export function resolveSurfaceConditionals(text, surface) {
85
+ return String(text ?? "").replace(CONDITIONAL, (_match, branch, body) => (branch === surface ? body : ""));
86
+ }
87
+
88
+ /** Rewrite every tool name in `text` to the spelling `surface` advertises. */
89
+ export function renameToolsForSurface(text, surface) {
90
+ const { rename } = toolVocabulary(surface);
91
+ let output = String(text ?? "");
92
+ for (const [from, to] of rename) output = output.replace(identifier(from), to);
93
+ return output;
94
+ }
95
+
96
+ /**
97
+ * Guide prose as one surface should read it: conditionals resolved, then names
98
+ * translated. Order matters — a conditional branch may name a tool that only
99
+ * exists on its own surface, so the other branch must be dropped before renaming.
100
+ */
101
+ export function renderForSurface(text, surface) {
102
+ return renameToolsForSurface(resolveSurfaceConditionals(text, surface), surface);
103
+ }
104
+
105
+ /**
106
+ * Tool names `text` mentions that `surface` cannot call. Empty is the contract.
107
+ *
108
+ * Only registry-known spellings count, so ordinary prose is never flagged. Call
109
+ * this on already-rendered text: an unresolved conditional would report its
110
+ * other branch as a violation.
111
+ */
112
+ export function unavailableToolMentions(text, surface) {
113
+ const { absent } = toolVocabulary(surface);
114
+ const value = String(text ?? "");
115
+ const found = new Set();
116
+ for (const name of absent) {
117
+ if (identifier(name).test(value)) found.add(name);
118
+ }
119
+ return Array.from(found).sort();
120
+ }
package/src/version.mjs CHANGED
@@ -7,4 +7,4 @@
7
7
  //
8
8
  // scripts/gavana-mcp-tool-contract.test.mjs asserts this equals
9
9
  // packages/cli/package.json, so the two cannot drift.
10
- export const GAVANA_CLI_VERSION = "0.2.2";
10
+ export const GAVANA_CLI_VERSION = "0.3.0";