@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/src/runner.mjs CHANGED
@@ -12,7 +12,7 @@ import { GAVANA_CLI_GROUP_HELP, gavanaCommandActions, gavanaCommandGroupUsageLin
12
12
  import { agentConfigFilePath, listAgentProfiles, readAgentConfig, readAgentConfigMetadata, removeAgentProfile, setActiveAgentProfile, writeAgentConfig } from "./config.mjs";
13
13
  import { gavanaMcpClientDefinition } from "./mcp-targets.mjs";
14
14
 
15
- const BOOLEAN_OPTIONS = new Set(["help", "version", "pretty", "raw", "yes", "confirm", "rebase", "wait", "no-wait", "progress", "token-stdin", "no-verify", "no-browser", "audio", "no-audio", "read-only"]);
15
+ const BOOLEAN_OPTIONS = new Set(["help", "version", "pretty", "raw", "yes", "confirm", "rebase", "wait", "no-wait", "progress", "preflight", "token-stdin", "no-verify", "no-browser", "audio", "no-audio", "read-only"]);
16
16
  const REPEATABLE_OPTIONS = new Set(["reference", "reference-role", "element", "input", "param", "target", "ratio", "approved", "field", "source-asset", "collection"]);
17
17
  const MAX_REFERENCE_IMAGES = 16;
18
18
  const MAX_VIDEO_REFERENCE_IMAGES = 9;
@@ -95,7 +95,7 @@ export async function runGavanaCli(argv = process.argv.slice(2), runtime = {}) {
95
95
  }
96
96
  if (!token && config.credentialState === "inaccessible") throw keychainInaccessibleError();
97
97
  const client = createCanvasAgentClient({ token, baseUrl, surface: "cli", fetchImpl: runtime.fetchImpl });
98
- return await runTrackedRemoteCommand(client, `${group}.${String(action || "default").toLowerCase()}`, runtime.analyticsEnabled ?? !runtime.fetchImpl, async () => {
98
+ return await runTrackedRemoteCommand(client, analyticsCommandName(group, action), runtime.analyticsEnabled ?? !runtime.fetchImpl, async () => {
99
99
  if (group === "api") {
100
100
  writeResult(stdout, await runApiCommand(client, action, positionals, parsed.options), parsed.options);
101
101
  return 0;
@@ -156,6 +156,11 @@ async function runTrackedRemoteCommand(client, name, enabled, handler) {
156
156
  });
157
157
  }
158
158
 
159
+ function analyticsCommandName(group, action) {
160
+ if (group === "api") return `api.${/^(get|post|put|patch|delete)$/i.test(action || "") ? action.toLowerCase() : "get"}`;
161
+ return `${group}.${String(action || "default").toLowerCase()}`;
162
+ }
163
+
159
164
  export const runCraftboardAgentCli = runGavanaCli;
160
165
 
161
166
  function exitCodeForResult(group, action, result) {
@@ -504,11 +509,12 @@ async function executeCommand(client, group, action, args, options, runtime) {
504
509
  }
505
510
 
506
511
  if (group === "image" && (action === "generate" || action === "edit" || action === "variations")) {
512
+ if (options.wait === true && options["no-wait"] === true) throw usageError("--wait and --no-wait cannot be combined.");
507
513
  const prepared = await imageInput(client, action, args, options, runtime);
508
- const { __destination, ...input } = prepared;
509
- const queued = await client.startImage(action, input);
510
- const shouldWait = options["no-wait"] !== true;
511
- if (!shouldWait) return { ...queued, destination: __destination };
514
+ const { __destination, __preflight, ...input } = prepared;
515
+ const queued = __preflight ? await client.preflightImage(action, input) : await client.startImage(action, input);
516
+ if (__preflight) return { ...queued, destination: __destination };
517
+ if (options.wait !== true) return { ...queued, destination: __destination };
512
518
  const result = await client.waitForRun(queued.run || queued.id, {
513
519
  timeoutMs: secondsOption(options.timeout, 15 * 60) * 1000,
514
520
  intervalMs: secondsOption(options.interval, 1.5, true) * 1000,
@@ -1196,6 +1202,7 @@ async function imageInput(client, operation, args, options, runtime) {
1196
1202
  const targetNodeIds = supplied.targetNodeIds || repeatableStrings(options.target);
1197
1203
  const singleTarget = supplied.targetNodeId || firstString(options["target-node"]);
1198
1204
  const count = supplied.count !== undefined || options.count !== undefined ? (supplied.count ?? requiredNumber(options.count, "--count must be a number.")) : undefined;
1205
+ const preflight = supplied.preflight === true || options.preflight === true;
1199
1206
  if (count !== undefined && (!Number.isInteger(Number(count)) || Number(count) < 1 || Number(count) > 4)) throw usageError("--count must be an integer between 1 and 4.");
1200
1207
  const referenceInputs = imageReferenceInputs(supplied.references, options.reference, supplied.referenceRoles, options["reference-role"]);
1201
1208
  const optionElements = repeatableStrings(options.element);
@@ -1204,14 +1211,58 @@ async function imageInput(client, operation, args, options, runtime) {
1204
1211
  if (elements.length > 8) throw usageError("An image job can use up to 8 Elements.");
1205
1212
  for (const entry of elements) {
1206
1213
  const typed = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
1207
- if (typed && Object.keys(typed).some((key) => key !== "handle" && key !== "role" && key !== "influence")) throw usageError('An Element reference may contain only "handle", "role", and "influence".');
1214
+ if (typed && Object.keys(typed).some((key) => !["handle", "role", "influence", "applicationMode", "currentTurnUserModeOverride", "placements"].includes(key))) {
1215
+ throw usageError('An Element reference may contain only "handle", "role", "influence", "applicationMode", "currentTurnUserModeOverride", and "placements".');
1216
+ }
1208
1217
  const handle = typeof entry === "string" ? entry.trim() : typeof typed?.handle === "string" ? typed.handle.trim() : "";
1209
1218
  if (!/^element:[A-Za-z0-9_-]{1,160}@v[1-9][0-9]*$/.test(handle)) throw usageError("Each --element must be a version-pinned element:<id>@v<n> handle.");
1210
1219
  if (typed?.role !== undefined && !["identity", "construction", "texture", "fit", "style"].includes(typed.role)) throw usageError("Element roles must be identity, construction, texture, fit, or style.");
1211
1220
  if (typed?.influence !== undefined && (typeof typed.influence !== "number" || !Number.isFinite(typed.influence) || typed.influence < 0 || typed.influence > 1)) throw usageError("Element influence must be a number from 0 through 1.");
1221
+ if (typed?.applicationMode !== undefined && !["creative", "identity", "exact"].includes(typed.applicationMode)) throw usageError("Element applicationMode must be creative, identity, or exact.");
1222
+ if (typed?.currentTurnUserModeOverride !== undefined && typed.currentTurnUserModeOverride !== true) throw usageError("currentTurnUserModeOverride must be true when supplied.");
1223
+ if (typed?.placements !== undefined) {
1224
+ if (!Array.isArray(typed.placements) || !typed.placements.length || typed.placements.length > 16) throw usageError("Element placements must contain 1 through 16 normalized rectangles.");
1225
+ for (const placement of typed.placements) {
1226
+ if (!placement || typeof placement !== "object" || Array.isArray(placement) || Object.keys(placement).some((key) => !["x", "y", "width", "height"].includes(key))) throw usageError('Each Element placement must contain only "x", "y", "width", and "height".');
1227
+ const { x, y, width, height } = placement;
1228
+ if (![x, y, width, height].every((value) => typeof value === "number" && Number.isFinite(value)) || x < 0 || y < 0 || width <= 0 || height <= 0 || x + width > 1 || y + height > 1) {
1229
+ throw usageError("Element placements must stay within normalized output bounds.");
1230
+ }
1231
+ }
1232
+ }
1212
1233
  }
1213
1234
  const elementHandles = elements.map((entry) => (typeof entry === "string" ? entry.trim() : entry.handle.trim()));
1214
1235
  if (new Set(elementHandles).size !== elementHandles.length) throw usageError("Element handles must be unique.");
1236
+ const source = supplied.source || firstString(options.source);
1237
+ const prompt = supplied.prompt || firstString(options.prompt);
1238
+ if (preflight) {
1239
+ if (destination === "agent-canvas" || destination === "new-canvas") throw usageError("--preflight requires an existing --destination canvas:<id>; it never creates a Canvas or target node.");
1240
+ const preflightTargets = [...targetNodeIds, ...(singleTarget ? [singleTarget] : [])];
1241
+ if (!preflightTargets.length) throw usageError("--preflight requires one or more explicit --target node:<id> handles.");
1242
+ if (referenceInputs.some((reference) => !/^(?:node:|asset:)/.test(reference.value)) || (source && !/^(?:node:|asset:)/.test(source))) {
1243
+ throw usageError("--preflight accepts only existing node: or asset: references; upload local images first.");
1244
+ }
1245
+ const canvasResult = await client.getCanvas(destination);
1246
+ const references = referenceInputs.map((reference) => ({ handle: reference.value, ...(reference.role ? { role: reference.role } : {}) }));
1247
+ return {
1248
+ canvasId: canvasResult.canvas.handle,
1249
+ baseRevision: supplied.baseRevision || firstString(options["base-revision"]) || canvasResult.canvas.revision,
1250
+ idempotencyKey,
1251
+ targetNodeIds: preflightTargets,
1252
+ ...(prompt ? { prompt } : {}),
1253
+ ...(supplied.promptNodeId || firstString(options["prompt-node"]) ? { promptNodeId: supplied.promptNodeId || firstString(options["prompt-node"]) } : {}),
1254
+ ...(references.length ? { references: references.some((reference) => reference.role) ? references : references.map((reference) => reference.handle) } : {}),
1255
+ ...(elements.length ? { elements } : {}),
1256
+ ...(source ? { source } : {}),
1257
+ ...(supplied.connectionId || firstString(options.connection) ? { connectionId: supplied.connectionId || firstString(options.connection) } : {}),
1258
+ ...(supplied.model || firstString(options.model) ? { model: supplied.model || firstString(options.model) } : {}),
1259
+ ...(supplied.size || firstString(options.size) ? { size: supplied.size || firstString(options.size) } : {}),
1260
+ ...(supplied.quality || firstString(options.quality) ? { quality: supplied.quality || firstString(options.quality) } : {}),
1261
+ count: count ?? preflightTargets.length,
1262
+ __preflight: true,
1263
+ __destination: { requested: String(destination), canvasId: canvasResult.canvas.handle, targetNodeIds: preflightTargets },
1264
+ };
1265
+ }
1215
1266
  const preparedReferences = await prepareReferenceInputs(referenceInputs.map((reference) => reference.value), runtime);
1216
1267
  const canvasResult = await client.resolveCanvasDestination(destination, {
1217
1268
  idempotencyKey,
@@ -1220,8 +1271,6 @@ async function imageInput(client, operation, args, options, runtime) {
1220
1271
  const canvasId = canvasResult.canvas.handle;
1221
1272
  const referenceHandles = await uploadLocalReferenceInputs(client, preparedReferences, canvasId);
1222
1273
  const references = referenceHandles.map((handle, index) => ({ handle, ...(referenceInputs[index]?.role ? { role: referenceInputs[index].role } : {}) }));
1223
- const source = supplied.source || firstString(options.source);
1224
- const prompt = supplied.prompt || firstString(options.prompt);
1225
1274
  const preparedDestination = await client.prepareImageDestination({
1226
1275
  canvasResult,
1227
1276
  destination,
@@ -1580,23 +1629,17 @@ function ensureUniqueVideoReferences(references) {
1580
1629
  }
1581
1630
 
1582
1631
  async function uploadLocalReferenceInputs(client, references, canvasReference) {
1583
- const handles = [];
1584
1632
  const uploads = new Map();
1585
- for (const reference of references) {
1586
- if (reference.kind === "handle") {
1587
- handles.push(reference.handle);
1588
- continue;
1589
- }
1590
- if (reference.kind === "url") {
1591
- handles.push(reference.url);
1592
- continue;
1593
- }
1594
- const fingerprint = crypto.createHash("sha256").update(reference.bytes).digest("base64url");
1595
- if (!uploads.has(fingerprint)) uploads.set(fingerprint, client.uploadAsset({ ...reference, canvasReference }));
1596
- const uploaded = await uploads.get(fingerprint);
1597
- handles.push(uploaded.asset.handle);
1598
- }
1599
- return handles;
1633
+ return Promise.all(
1634
+ references.map(async (reference) => {
1635
+ if (reference.kind === "handle") return reference.handle;
1636
+ if (reference.kind === "url") return reference.url;
1637
+ const fingerprint = crypto.createHash("sha256").update(reference.bytes).digest("base64url");
1638
+ if (!uploads.has(fingerprint)) uploads.set(fingerprint, client.uploadAsset({ ...reference, canvasReference }));
1639
+ const uploaded = await uploads.get(fingerprint);
1640
+ return uploaded.asset.handle;
1641
+ }),
1642
+ );
1600
1643
  }
1601
1644
 
1602
1645
  async function readMacClipboardImage() {
@@ -1938,6 +1981,7 @@ function elementInputFromOptions(options) {
1938
1981
  type: firstString(options.type),
1939
1982
  sourceAssetIds: repeatableStrings(options["source-asset"]),
1940
1983
  ...(firstString(options.guidelines) ? { guidelines: firstString(options.guidelines) } : {}),
1984
+ ...(firstString(options["application-mode"]) ? { applicationMode: firstString(options["application-mode"]) } : {}),
1941
1985
  ...(repeatableStrings(options.collection).length ? { collectionIds: repeatableStrings(options.collection) } : {}),
1942
1986
  };
1943
1987
  }
@@ -2047,12 +2091,14 @@ Global options:
2047
2091
  --connection VALUE Disambiguate a provider model id by connection
2048
2092
  --reference VALUE Image reference; repeat for multiple visuals
2049
2093
  --reference-role ROLE Optional role for the matching --reference: identity, construction, texture, fit, or style
2094
+ --preflight Compile an image reference plan without starting paid generation
2050
2095
  --input VALUE Recipe KEY=VALUE or Action input; repeat as needed
2051
2096
  --param NAME=VALUE Action parameter; repeat for multiple parameters
2052
2097
  --webhook-url URL Send one signed callback when a Run finishes
2053
2098
  --webhook-secret-env VAR Read the signing secret from VAR (default: GAVANA_WEBHOOK_SECRET)
2054
2099
  --canvas-title VALUE Title used when --destination is new-canvas
2055
- --no-wait Return immediately after queueing Recipe, image, video, or Action work
2100
+ --wait Wait for an image Run to finish instead of returning after queueing
2101
+ --no-wait Return immediately after queueing Recipe, video, or Action work (legacy image alias)
2056
2102
  --progress Write Run or video Job state transitions to stderr
2057
2103
 
2058
2104
  Video options:
@@ -2074,6 +2120,11 @@ ports treat @path as text; upload a visual first and pass its asset: handle when
2074
2120
  that written port explicitly accepts an image reference. For image generation,
2075
2121
  repeat --reference-role in the same order as --reference to preserve each
2076
2122
  visual's identity, construction, texture, fit, or style role.
2123
+ Elements are version-pinned. creative permits visual reinterpretation; identity
2124
+ guides preservation but is not pixel-exact. exact Elements use JSON input with
2125
+ normalized placements and deterministic original-pixel composition. Use
2126
+ --preflight with an existing canvas and explicit --target handles to inspect the
2127
+ compiled reference plan before paid generation; it never creates a Canvas or target.
2077
2128
  Image and Action commands create target nodes automatically when --target is omitted.
2078
2129
 
2079
2130
  For history-safe token entry in zsh (macOS):
@@ -26,6 +26,7 @@ import { defineCampaignReview } from "./campaign_review.mjs";
26
26
  import { defineCampaignStart } from "./campaign_start.mjs";
27
27
  import { defineCanvasApplyBatch } from "./canvas_apply_batch.mjs";
28
28
  import { defineCanvasCreate } from "./canvas_create.mjs";
29
+ import { defineRunList } from "./run_list.mjs";
29
30
  import { defineCanvasGet } from "./canvas_get.mjs";
30
31
  import { defineCanvasList } from "./canvas_list.mjs";
31
32
  import { defineCanvasValidate } from "./canvas_validate.mjs";
@@ -91,6 +92,7 @@ export const GAVANA_TOOL_DEFINITIONS = Object.freeze({
91
92
  campaign_start: defineCampaignStart,
92
93
  canvas_apply_batch: defineCanvasApplyBatch,
93
94
  canvas_create: defineCanvasCreate,
95
+ run_list: defineRunList,
94
96
  canvas_get: defineCanvasGet,
95
97
  canvas_list: defineCanvasList,
96
98
  canvas_validate: defineCanvasValidate,
@@ -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
@@ -48,7 +54,7 @@ export function imageToolDefinition(operation, client) {
48
54
  idempotencyKey: input.idempotencyKey,
49
55
  });
50
56
  const destinationResult = { requested: destination, canvasId: prepared.canvasId, targetNodeIds: prepared.targetNodeIds };
51
- if (input.wait === false) return { ...queued, destination: destinationResult };
57
+ if (input.wait !== true) return { ...queued, destination: destinationResult };
52
58
  const result = await client.waitForRun(queued.run || queued.id, withProgress({ timeoutMs: (input.timeoutSeconds || 900) * 1000 }, extra));
53
59
  return { ...result, destination: destinationResult };
54
60
  },
@@ -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
+ }