@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.
package/src/runner.mjs CHANGED
@@ -9,10 +9,10 @@ import { promisify } from "node:util";
9
9
  import { CanvasAgentApiError, createCanvasAgentClient, markdownForResult, parseElementHandle } from "./client.mjs";
10
10
  import { GAVANA_CLI_VERSION, gavanaCapabilitySummary } from "./capabilities.mjs";
11
11
  import { GAVANA_CLI_GROUP_HELP, gavanaCommandActions, gavanaCommandGroupUsageLines, gavanaCommandGroups, gavanaCommandIsKnown, gavanaCommandUsageLines } from "./commands.mjs";
12
- import { agentConfigFilePath, listAgentProfiles, readAgentConfig, readAgentConfigMetadata, removeAgentProfile, setActiveAgentProfile, writeAgentConfig } from "./config.mjs";
12
+ import { agentConfigFilePath, listAgentProfiles, readAgentConfig, readAgentConfigMetadata, readOAuthInstallId, 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) {
@@ -506,8 +511,9 @@ async function executeCommand(client, group, action, args, options, runtime) {
506
511
  if (group === "image" && (action === "generate" || action === "edit" || action === "variations")) {
507
512
  if (options.wait === true && options["no-wait"] === true) throw usageError("--wait and --no-wait cannot be combined.");
508
513
  const prepared = await imageInput(client, action, args, options, runtime);
509
- const { __destination, ...input } = prepared;
510
- const queued = await client.startImage(action, input);
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 };
511
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,
@@ -637,7 +643,7 @@ async function runAuthCommand(action, _args, options, env, runtime) {
637
643
  if (!token) {
638
644
  const login = runtime.oauthLogin
639
645
  ? await runtime.oauthLogin({ baseUrl, readOnly: options["read-only"] === true, profile: profile || current.profile })
640
- : await performBrowserOAuthLogin({ baseUrl, readOnly: options["read-only"] === true, noBrowser: options["no-browser"] === true, runtime });
646
+ : await performBrowserOAuthLogin({ baseUrl, readOnly: options["read-only"] === true, noBrowser: options["no-browser"] === true, env, runtime });
641
647
  token = login.accessToken;
642
648
  loginMethod = "browser";
643
649
  }
@@ -732,7 +738,7 @@ async function revokeBrowserOAuthToken(baseUrl, token, fetchImpl) {
732
738
  }
733
739
  }
734
740
 
735
- export async function performBrowserOAuthLogin({ baseUrl, readOnly = false, noBrowser = false, runtime = {} }) {
741
+ export async function performBrowserOAuthLogin({ baseUrl, readOnly = false, noBrowser = false, env = process.env, runtime = {} }) {
736
742
  const fetchImpl = runtime.fetchImpl || globalThis.fetch;
737
743
  if (typeof fetchImpl !== "function") throw usageError("This runtime does not provide fetch for browser login.");
738
744
  const origin = new URL(createCanvasAgentClient({ token: "configuration-validation", baseUrl, fetchImpl }).baseUrl).origin;
@@ -742,11 +748,17 @@ export async function performBrowserOAuthLogin({ baseUrl, readOnly = false, noBr
742
748
  const callback = runtime.createOAuthCallbackServer ? await runtime.createOAuthCallbackServer(state) : await createOAuthCallbackServer(state);
743
749
  try {
744
750
  const scopes = readOnly ? "canvas:read asset:read element:read" : "canvas:read canvas:write asset:read element:read element:write image:generate video:generate job:manage";
751
+ // Registering under a stable install id is what makes this login a
752
+ // reconnect: Gavana returns the client id this machine already had and
753
+ // replaces the delegation it issued last time, instead of stacking a
754
+ // new one behind a client it has never seen.
755
+ const softwareId = runtime.installId || (await readOAuthInstallId(env));
745
756
  const registrationResponse = await fetchImpl(`${origin}/oauth/register`, {
746
757
  method: "POST",
747
758
  headers: { "Content-Type": "application/json", Accept: "application/json" },
748
759
  body: JSON.stringify({
749
760
  client_name: "Gavana CLI",
761
+ software_id: softwareId,
750
762
  redirect_uris: [callback.redirectUri],
751
763
  token_endpoint_auth_method: "none",
752
764
  grant_types: ["authorization_code"],
@@ -1196,6 +1208,7 @@ async function imageInput(client, operation, args, options, runtime) {
1196
1208
  const targetNodeIds = supplied.targetNodeIds || repeatableStrings(options.target);
1197
1209
  const singleTarget = supplied.targetNodeId || firstString(options["target-node"]);
1198
1210
  const count = supplied.count !== undefined || options.count !== undefined ? (supplied.count ?? requiredNumber(options.count, "--count must be a number.")) : undefined;
1211
+ const preflight = supplied.preflight === true || options.preflight === true;
1199
1212
  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
1213
  const referenceInputs = imageReferenceInputs(supplied.references, options.reference, supplied.referenceRoles, options["reference-role"]);
1201
1214
  const optionElements = repeatableStrings(options.element);
@@ -1204,14 +1217,58 @@ async function imageInput(client, operation, args, options, runtime) {
1204
1217
  if (elements.length > 8) throw usageError("An image job can use up to 8 Elements.");
1205
1218
  for (const entry of elements) {
1206
1219
  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".');
1220
+ if (typed && Object.keys(typed).some((key) => !["handle", "role", "influence", "applicationMode", "currentTurnUserModeOverride", "placements"].includes(key))) {
1221
+ throw usageError('An Element reference may contain only "handle", "role", "influence", "applicationMode", "currentTurnUserModeOverride", and "placements".');
1222
+ }
1208
1223
  const handle = typeof entry === "string" ? entry.trim() : typeof typed?.handle === "string" ? typed.handle.trim() : "";
1209
1224
  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
1225
  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
1226
  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.");
1227
+ if (typed?.applicationMode !== undefined && !["creative", "identity", "exact"].includes(typed.applicationMode)) throw usageError("Element applicationMode must be creative, identity, or exact.");
1228
+ if (typed?.currentTurnUserModeOverride !== undefined && typed.currentTurnUserModeOverride !== true) throw usageError("currentTurnUserModeOverride must be true when supplied.");
1229
+ if (typed?.placements !== undefined) {
1230
+ if (!Array.isArray(typed.placements) || !typed.placements.length || typed.placements.length > 16) throw usageError("Element placements must contain 1 through 16 normalized rectangles.");
1231
+ for (const placement of typed.placements) {
1232
+ 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".');
1233
+ const { x, y, width, height } = placement;
1234
+ 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) {
1235
+ throw usageError("Element placements must stay within normalized output bounds.");
1236
+ }
1237
+ }
1238
+ }
1212
1239
  }
1213
1240
  const elementHandles = elements.map((entry) => (typeof entry === "string" ? entry.trim() : entry.handle.trim()));
1214
1241
  if (new Set(elementHandles).size !== elementHandles.length) throw usageError("Element handles must be unique.");
1242
+ const source = supplied.source || firstString(options.source);
1243
+ const prompt = supplied.prompt || firstString(options.prompt);
1244
+ if (preflight) {
1245
+ if (destination === "agent-canvas" || destination === "new-canvas") throw usageError("--preflight requires an existing --destination canvas:<id>; it never creates a Canvas or target node.");
1246
+ const preflightTargets = [...targetNodeIds, ...(singleTarget ? [singleTarget] : [])];
1247
+ if (!preflightTargets.length) throw usageError("--preflight requires one or more explicit --target node:<id> handles.");
1248
+ if (referenceInputs.some((reference) => !/^(?:node:|asset:)/.test(reference.value)) || (source && !/^(?:node:|asset:)/.test(source))) {
1249
+ throw usageError("--preflight accepts only existing node: or asset: references; upload local images first.");
1250
+ }
1251
+ const canvasResult = await client.getCanvas(destination);
1252
+ const references = referenceInputs.map((reference) => ({ handle: reference.value, ...(reference.role ? { role: reference.role } : {}) }));
1253
+ return {
1254
+ canvasId: canvasResult.canvas.handle,
1255
+ baseRevision: supplied.baseRevision || firstString(options["base-revision"]) || canvasResult.canvas.revision,
1256
+ idempotencyKey,
1257
+ targetNodeIds: preflightTargets,
1258
+ ...(prompt ? { prompt } : {}),
1259
+ ...(supplied.promptNodeId || firstString(options["prompt-node"]) ? { promptNodeId: supplied.promptNodeId || firstString(options["prompt-node"]) } : {}),
1260
+ ...(references.length ? { references: references.some((reference) => reference.role) ? references : references.map((reference) => reference.handle) } : {}),
1261
+ ...(elements.length ? { elements } : {}),
1262
+ ...(source ? { source } : {}),
1263
+ ...(supplied.connectionId || firstString(options.connection) ? { connectionId: supplied.connectionId || firstString(options.connection) } : {}),
1264
+ ...(supplied.model || firstString(options.model) ? { model: supplied.model || firstString(options.model) } : {}),
1265
+ ...(supplied.size || firstString(options.size) ? { size: supplied.size || firstString(options.size) } : {}),
1266
+ ...(supplied.quality || firstString(options.quality) ? { quality: supplied.quality || firstString(options.quality) } : {}),
1267
+ count: count ?? preflightTargets.length,
1268
+ __preflight: true,
1269
+ __destination: { requested: String(destination), canvasId: canvasResult.canvas.handle, targetNodeIds: preflightTargets },
1270
+ };
1271
+ }
1215
1272
  const preparedReferences = await prepareReferenceInputs(referenceInputs.map((reference) => reference.value), runtime);
1216
1273
  const canvasResult = await client.resolveCanvasDestination(destination, {
1217
1274
  idempotencyKey,
@@ -1220,8 +1277,6 @@ async function imageInput(client, operation, args, options, runtime) {
1220
1277
  const canvasId = canvasResult.canvas.handle;
1221
1278
  const referenceHandles = await uploadLocalReferenceInputs(client, preparedReferences, canvasId);
1222
1279
  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
1280
  const preparedDestination = await client.prepareImageDestination({
1226
1281
  canvasResult,
1227
1282
  destination,
@@ -1932,6 +1987,7 @@ function elementInputFromOptions(options) {
1932
1987
  type: firstString(options.type),
1933
1988
  sourceAssetIds: repeatableStrings(options["source-asset"]),
1934
1989
  ...(firstString(options.guidelines) ? { guidelines: firstString(options.guidelines) } : {}),
1990
+ ...(firstString(options["application-mode"]) ? { applicationMode: firstString(options["application-mode"]) } : {}),
1935
1991
  ...(repeatableStrings(options.collection).length ? { collectionIds: repeatableStrings(options.collection) } : {}),
1936
1992
  };
1937
1993
  }
@@ -2041,6 +2097,7 @@ Global options:
2041
2097
  --connection VALUE Disambiguate a provider model id by connection
2042
2098
  --reference VALUE Image reference; repeat for multiple visuals
2043
2099
  --reference-role ROLE Optional role for the matching --reference: identity, construction, texture, fit, or style
2100
+ --preflight Compile an image reference plan without starting paid generation
2044
2101
  --input VALUE Recipe KEY=VALUE or Action input; repeat as needed
2045
2102
  --param NAME=VALUE Action parameter; repeat for multiple parameters
2046
2103
  --webhook-url URL Send one signed callback when a Run finishes
@@ -2069,6 +2126,11 @@ ports treat @path as text; upload a visual first and pass its asset: handle when
2069
2126
  that written port explicitly accepts an image reference. For image generation,
2070
2127
  repeat --reference-role in the same order as --reference to preserve each
2071
2128
  visual's identity, construction, texture, fit, or style role.
2129
+ Elements are version-pinned. creative permits visual reinterpretation; identity
2130
+ guides preservation but is not pixel-exact. exact Elements use JSON input with
2131
+ normalized placements and deterministic original-pixel composition. Use
2132
+ --preflight with an existing canvas and explicit --target handles to inspect the
2133
+ compiled reference plan before paid generation; it never creates a Canvas or target.
2072
2134
  Image and Action commands create target nodes automatically when --target is omitted.
2073
2135
 
2074
2136
  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
@@ -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
+ }