@gavana.ai/cli 0.2.2 → 0.3.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.
@@ -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.1";