@gavana.ai/cli 0.2.1 → 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.
- package/CHANGELOG.md +38 -0
- package/README.md +35 -5
- package/guides/creative-canvas.md +16 -3
- package/guides/generated-assets.md +38 -5
- package/guides/getting-started.md +16 -1
- package/guides/paid-action-safety.md +2 -2
- package/guides/product-imports.md +45 -0
- package/guides/untrusted-content.md +51 -0
- package/guides/video-generation.md +52 -0
- package/guides/workflows.md +43 -0
- package/package.json +1 -1
- package/src/canvas-agent-guide.mjs +65 -13
- package/src/canvas-agent-validation.mjs +113 -17
- package/src/capabilities.mjs +1 -1
- package/src/client.mjs +16 -0
- package/src/commands.mjs +5 -5
- package/src/guide-sources.mjs +36 -4
- package/src/mcp-targets.mjs +1 -1
- package/src/runner.mjs +77 -26
- package/src/tools/definitions.mjs +2 -0
- package/src/tools/element_create.mjs +1 -1
- package/src/tools/element_update.mjs +2 -1
- package/src/tools/guide_get.mjs +1 -1
- package/src/tools/guide_search.mjs +1 -1
- package/src/tools/image_tool.mjs +10 -4
- package/src/tools/registry.mjs +72 -26
- package/src/tools/run_list.mjs +20 -0
- package/src/tools/schemas.mjs +38 -4
- package/src/tools/surface-names.mjs +120 -0
- package/src/version.mjs +1 -1
package/src/tools/schemas.mjs
CHANGED
|
@@ -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,
|
|
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,8 +184,17 @@ 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
|
|
169
|
-
|
|
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."),
|
|
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
|
});
|
|
172
200
|
}
|
|
@@ -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