@gavana.ai/cli 0.2.0 → 0.2.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/README.md +34 -0
  3. package/guides/creative-canvas.md +52 -0
  4. package/guides/generated-assets.md +6 -1
  5. package/guides/sections-layout.md +3 -3
  6. package/guides/validation-recovery.md +9 -2
  7. package/package.json +1 -1
  8. package/src/canvas-agent-guide.mjs +3 -3
  9. package/src/canvas-agent-validation.mjs +30 -15
  10. package/src/capabilities.mjs +3 -1
  11. package/src/client.mjs +252 -0
  12. package/src/commands.mjs +23 -1
  13. package/src/config.mjs +50 -17
  14. package/src/guide-sources.mjs +11 -3
  15. package/src/mcp-targets.mjs +72 -0
  16. package/src/runner.mjs +191 -39
  17. package/src/tools/campaign_plan.mjs +2 -2
  18. package/src/tools/campaign_review.mjs +2 -2
  19. package/src/tools/campaign_start.mjs +2 -2
  20. package/src/tools/definitions.mjs +34 -0
  21. package/src/tools/element_archive.mjs +12 -0
  22. package/src/tools/element_collection_create.mjs +11 -0
  23. package/src/tools/element_collection_delete.mjs +12 -0
  24. package/src/tools/element_collection_list.mjs +13 -0
  25. package/src/tools/element_collection_update.mjs +12 -0
  26. package/src/tools/element_create.mjs +11 -0
  27. package/src/tools/element_get.mjs +12 -0
  28. package/src/tools/element_history.mjs +13 -0
  29. package/src/tools/element_list.mjs +13 -0
  30. package/src/tools/element_restore.mjs +12 -0
  31. package/src/tools/element_update.mjs +21 -0
  32. package/src/tools/element_update_collections.mjs +12 -0
  33. package/src/tools/image_tool.mjs +2 -2
  34. package/src/tools/registry.mjs +202 -0
  35. package/src/tools/schemas.mjs +62 -0
  36. package/src/tools/work_continue.mjs +42 -0
  37. package/src/tools/work_execute.mjs +12 -0
  38. package/src/tools/work_get.mjs +12 -0
  39. package/src/tools/work_prepare.mjs +21 -0
  40. package/src/tools/work_refresh.mjs +12 -0
  41. package/src/version.mjs +5 -7
package/src/runner.mjs CHANGED
@@ -3,15 +3,17 @@ import { execFile as execFileCallback } from "node:child_process";
3
3
  import fs from "node:fs/promises";
4
4
  import http from "node:http";
5
5
  import path from "node:path";
6
+ import { createInterface } from "node:readline/promises";
6
7
  import { promisify } from "node:util";
7
8
 
8
- import { CanvasAgentApiError, createCanvasAgentClient, markdownForResult } from "./client.mjs";
9
+ import { CanvasAgentApiError, createCanvasAgentClient, markdownForResult, parseElementHandle } from "./client.mjs";
9
10
  import { GAVANA_CLI_VERSION, gavanaCapabilitySummary } from "./capabilities.mjs";
10
11
  import { GAVANA_CLI_GROUP_HELP, gavanaCommandActions, gavanaCommandGroupUsageLines, gavanaCommandGroups, gavanaCommandIsKnown, gavanaCommandUsageLines } from "./commands.mjs";
11
12
  import { agentConfigFilePath, listAgentProfiles, readAgentConfig, readAgentConfigMetadata, removeAgentProfile, setActiveAgentProfile, writeAgentConfig } from "./config.mjs";
13
+ import { gavanaMcpClientDefinition } from "./mcp-targets.mjs";
12
14
 
13
- const BOOLEAN_OPTIONS = new Set(["help", "version", "pretty", "raw", "yes", "wait", "no-wait", "progress", "token-stdin", "no-verify", "no-browser", "audio", "no-audio", "read-only"]);
14
- const REPEATABLE_OPTIONS = new Set(["reference", "reference-role", "input", "param", "target", "ratio", "approved", "field"]);
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"]);
16
+ const REPEATABLE_OPTIONS = new Set(["reference", "reference-role", "element", "input", "param", "target", "ratio", "approved", "field", "source-asset", "collection"]);
15
17
  const MAX_REFERENCE_IMAGES = 16;
16
18
  const MAX_VIDEO_REFERENCE_IMAGES = 9;
17
19
  const MAX_REFERENCE_IMAGE_BYTES = 50 * 1024 * 1024;
@@ -66,7 +68,7 @@ export async function runGavanaCli(argv = process.argv.slice(2), runtime = {}) {
66
68
  return 0;
67
69
  }
68
70
  if (group === "config") {
69
- const result = await runConfigCommand(action, positionals, parsed.options, env);
71
+ const result = await runConfigCommand(action, positionals, parsed.options, env, runtime);
70
72
  writeResult(stdout, result, parsed.options);
71
73
  return 0;
72
74
  }
@@ -78,7 +80,7 @@ export async function runGavanaCli(argv = process.argv.slice(2), runtime = {}) {
78
80
 
79
81
  const selectedProfile = firstString(parsed.options.profile) || env.GAVANA_PROFILE || env.CRAFTBOARD_PROFILE || "";
80
82
  const profileEnv = selectedProfile ? { ...env, GAVANA_PROFILE: selectedProfile } : env;
81
- const config = group === "mcp" ? await readAgentConfigMetadata(profileEnv) : await readAgentConfig(profileEnv);
83
+ const config = group === "mcp" ? await readAgentConfigMetadata(profileEnv) : await readAgentConfig(profileEnv, keychainDependencies(runtime));
82
84
  const token = firstString(parsed.options.token) || env.GAVANA_AGENT_TOKEN || env.CRAFTBOARD_AGENT_TOKEN || config.token;
83
85
  const baseUrl = firstString(parsed.options["base-url"]) || env.GAVANA_BASE_URL || env.CRAFTBOARD_BASE_URL || config.baseUrl || "https://app.gavana.ai";
84
86
  if (group === "mcp") {
@@ -91,6 +93,7 @@ export async function runGavanaCli(argv = process.argv.slice(2), runtime = {}) {
91
93
  writeResult(stdout, result, parsed.options);
92
94
  return result.ok ? 0 : 1;
93
95
  }
96
+ if (!token && config.credentialState === "inaccessible") throw keychainInaccessibleError();
94
97
  const client = createCanvasAgentClient({ token, baseUrl, surface: "cli", fetchImpl: runtime.fetchImpl });
95
98
  return await runTrackedRemoteCommand(client, `${group}.${String(action || "default").toLowerCase()}`, runtime.analyticsEnabled ?? !runtime.fetchImpl, async () => {
96
99
  if (group === "api") {
@@ -101,6 +104,9 @@ export async function runGavanaCli(argv = process.argv.slice(2), runtime = {}) {
101
104
  env,
102
105
  stderr,
103
106
  readClipboardImage: runtime.readClipboardImage,
107
+ confirm: runtime.confirm,
108
+ stdin: runtime.stdin || process.stdin,
109
+ isInteractive: runtime.isInteractive ?? Boolean((runtime.stdin || process.stdin).isTTY && stderr.isTTY),
104
110
  legacyCampaignCommandsEnabled:
105
111
  runtime.legacyCampaignCommandsEnabled ??
106
112
  (env.GAVANA_ENABLE_LEGACY_CAMPAIGN_COMMANDS === "true" ||
@@ -159,7 +165,8 @@ function exitCodeForResult(group, action, result) {
159
165
  const waitedJob = group === "job" && action === "wait";
160
166
  const waitedRun = group === "run" && action === "wait";
161
167
  const waitedRecipe = group === "recipe" && action === "run";
162
- if ((waitedImage || waitedVideo || waitedAction || waitedJob || waitedRun || waitedRecipe) && (result?.status === "failed" || result?.status === "canceled" || result?.status === "expired")) return 9;
168
+ const terminalWorkFailure = group === "work" && action === "execute" && ["FAILED", "PARTIALLY_FAILED", "CANCELED"].includes(String(result?.status || "").toUpperCase());
169
+ if (((waitedImage || waitedVideo || waitedAction || waitedJob || waitedRun || waitedRecipe) && (result?.status === "failed" || result?.status === "canceled" || result?.status === "expired")) || terminalWorkFailure) return 9;
163
170
  return 0;
164
171
  }
165
172
 
@@ -206,6 +213,35 @@ async function executeCommand(client, group, action, args, options, runtime) {
206
213
  }
207
214
  }
208
215
 
216
+ if (group === "work") {
217
+ if (action === "prepare") {
218
+ const supplied = await optionalJsonInput(options);
219
+ return client.prepareWork(
220
+ supplied || {
221
+ request: firstString(options.request) || args.join(" ") || requiredArg(args, 0, "work prepare requires --request."),
222
+ ...(repeatableStrings(options.reference).length ? { references: repeatableStrings(options.reference) } : {}),
223
+ ...(firstString(options.canvas) ? { canvasId: firstString(options.canvas) } : {}),
224
+ idempotencyKey: firstString(options["idempotency-key"]),
225
+ },
226
+ );
227
+ }
228
+ if (action === "get") return client.getWork(firstString(options.work) || requiredArg(args, 0, "work get requires work:<id>."));
229
+ if (action === "refresh") return client.refreshWork(firstString(options.work) || requiredArg(args, 0, "work refresh requires work:<id>."));
230
+ if (action === "continue") {
231
+ const supplied = await optionalJsonInput(options);
232
+ const workId = firstString(options.work) || supplied?.workId || requiredArg(args, 0, "work continue requires work:<id>.");
233
+ const input = supplied ? omitKeys(supplied, ["workId"]) : workContinuationInput(options);
234
+ return client.continueWork(workId, input);
235
+ }
236
+ if (action === "execute") {
237
+ requireWorkConfirmation(options);
238
+ const supplied = await optionalJsonInput(options);
239
+ const workId = firstString(options.work) || supplied?.workId || requiredArg(args, 0, "work execute requires work:<id>.");
240
+ const input = supplied ? omitKeys(supplied, ["workId"]) : { confirm: true, idempotencyKey: firstString(options["idempotency-key"]) };
241
+ return client.executeWork(workId, input);
242
+ }
243
+ }
244
+
209
245
  if (group === "campaign") {
210
246
  if (action === "plan") {
211
247
  const supplied = await optionalJsonInput(options);
@@ -379,6 +415,45 @@ async function executeCommand(client, group, action, args, options, runtime) {
379
415
  }
380
416
  }
381
417
 
418
+ if (group === "element") {
419
+ if (action === "list") {
420
+ const state = firstString(options.state);
421
+ if (state && state !== "active" && state !== "archived") throw usageError("--state must be active or archived.");
422
+ return client.listElements({ query: firstString(options.query) || args.join(" "), ...(state ? { state } : {}) }, paginationOptions(options, 100));
423
+ }
424
+ if (action === "get") {
425
+ const elementId = requiredArg(args, 0, "element get requires element:<id>@v<n>.");
426
+ if (!parseElementHandle(elementId).version) throw usageError("element get requires an exact element:<id>@v<n> revision handle.");
427
+ return client.getElement(elementId);
428
+ }
429
+ if (action === "history") return client.listElementHistory(requiredArg(args, 0, "element history requires element:<id>."), paginationOptions(options, 100));
430
+ if (action === "create" || action === "update") {
431
+ const supplied = await optionalJsonInput(options);
432
+ const input = supplied || elementInputFromOptions(options);
433
+ if (action === "create") return client.createElement(input);
434
+ return client.updateElement(requiredArg(args, 0, "element update requires element:<id>."), input);
435
+ }
436
+ if (action === "collections") {
437
+ const supplied = await optionalJsonInput(options);
438
+ const collectionIds = supplied?.collectionIds === undefined ? repeatableStrings(options.collection) : supplied.collectionIds;
439
+ return client.updateElementCollections(requiredArg(args, 0, "element collections requires element:<id>."), collectionIds);
440
+ }
441
+ if (action === "archive") {
442
+ const elementId = requiredArg(args, 0, "element archive requires element:<id>.");
443
+ await requireDestructiveConfirmation(options, runtime, `Archive ${elementId}? It will remain restorable.`, "element archive is destructive. Re-run with --yes in non-interactive mode.");
444
+ return client.archiveElement(elementId);
445
+ }
446
+ if (action === "restore") return client.restoreElement(requiredArg(args, 0, "element restore requires element:<id>."));
447
+ if (action === "collection-list") return client.listElementCollections(paginationOptions(options, 100));
448
+ if (action === "collection-create") return client.createElementCollection({ name: firstString(options.name) || args.join(" ") });
449
+ if (action === "collection-rename") return client.updateElementCollection(requiredArg(args, 0, "element collection-rename requires element-collection:<id>."), { name: firstString(options.name) || requiredArg(args, 1, "element collection-rename requires --name.") });
450
+ if (action === "collection-delete") {
451
+ const collectionId = requiredArg(args, 0, "element collection-delete requires element-collection:<id>.");
452
+ await requireDestructiveConfirmation(options, runtime, `Delete ${collectionId}? This removes the collection, not its Elements.`, "element collection-delete is destructive. Re-run with --yes in non-interactive mode.");
453
+ return client.deleteElementCollection(collectionId);
454
+ }
455
+ }
456
+
382
457
  if (group === "ai-connection" || group === "provider") {
383
458
  if (action === "list") return client.listConnections(paginationOptions(options, 100));
384
459
  }
@@ -551,7 +626,7 @@ async function runAuthCommand(action, _args, options, env, runtime) {
551
626
  if (action === "login") {
552
627
  const profile = firstString(options.profile) || env.GAVANA_PROFILE || env.CRAFTBOARD_PROFILE || "";
553
628
  const profileEnv = profile ? { ...env, GAVANA_PROFILE: profile } : env;
554
- const current = await readAgentConfig(profileEnv);
629
+ const current = await readAgentConfig(profileEnv, keychainDependencies(runtime));
555
630
  let token = firstString(options.token) || env.GAVANA_AGENT_TOKEN || env.CRAFTBOARD_AGENT_TOKEN || (options["token-stdin"] === true ? (await readAllStdin()).trim() : "");
556
631
  const baseUrl = createCanvasAgentClient({
557
632
  token: "configuration-validation",
@@ -593,10 +668,21 @@ async function runAuthCommand(action, _args, options, env, runtime) {
593
668
  if (action === "status") {
594
669
  const profile = firstString(options.profile) || env.GAVANA_PROFILE || env.CRAFTBOARD_PROFILE || "";
595
670
  const profileEnv = profile ? { ...env, GAVANA_PROFILE: profile } : env;
596
- const config = await readAgentConfig(profileEnv);
671
+ const config = await readAgentConfig(profileEnv, keychainDependencies(runtime));
597
672
  const token = firstString(options.token) || env.GAVANA_AGENT_TOKEN || env.CRAFTBOARD_AGENT_TOKEN || config.token;
598
673
  const baseUrl = firstString(options["base-url"]) || env.GAVANA_BASE_URL || env.CRAFTBOARD_BASE_URL || config.baseUrl;
599
- if (!token || !baseUrl) return { authenticated: false, configured: false, configPath: agentConfigFilePath(env) };
674
+ if (!token || !baseUrl) {
675
+ return {
676
+ authenticated: false,
677
+ configured: false,
678
+ profile: config.profile,
679
+ baseUrl: baseUrl || "",
680
+ credentialStore: config.credentialStore || "",
681
+ credentialState: config.credentialState,
682
+ ...(config.credentialState === "inaccessible" ? { detail: keychainInaccessibleError().message } : {}),
683
+ configPath: agentConfigFilePath(env),
684
+ };
685
+ }
600
686
  const client = createCanvasAgentClient({ token, baseUrl, surface: "cli", fetchImpl });
601
687
  const verification = await verifyAuthentication(client);
602
688
  let canvasCount = verification.canvasCount;
@@ -612,9 +698,11 @@ async function runAuthCommand(action, _args, options, env, runtime) {
612
698
  return {
613
699
  ...verification,
614
700
  profile: config.profile,
615
- configured: Boolean(config.token && config.baseUrl),
701
+ configured: Boolean(config.credentialState === "available" && config.baseUrl),
616
702
  baseUrl: client.baseUrl,
617
703
  token: maskToken(token),
704
+ credentialStore: config.credentialStore || "",
705
+ credentialState: config.credentialState,
618
706
  ...(canvasCount === undefined ? {} : { canvasCount }),
619
707
  configPath: agentConfigFilePath(env),
620
708
  };
@@ -622,7 +710,7 @@ async function runAuthCommand(action, _args, options, env, runtime) {
622
710
  if (action === "logout") {
623
711
  const profile = firstString(options.profile) || env.GAVANA_PROFILE || env.CRAFTBOARD_PROFILE || "";
624
712
  const profileEnv = profile ? { ...env, GAVANA_PROFILE: profile } : env;
625
- const config = await readAgentConfig(profileEnv);
713
+ const config = await readAgentConfig(profileEnv, keychainDependencies(runtime));
626
714
  if (config.authMode === "oauth" && config.token && config.baseUrl) {
627
715
  await revokeBrowserOAuthToken(config.baseUrl, config.token, fetchImpl || globalThis.fetch);
628
716
  }
@@ -653,7 +741,7 @@ export async function performBrowserOAuthLogin({ baseUrl, readOnly = false, noBr
653
741
  const state = crypto.randomBytes(24).toString("base64url");
654
742
  const callback = runtime.createOAuthCallbackServer ? await runtime.createOAuthCallbackServer(state) : await createOAuthCallbackServer(state);
655
743
  try {
656
- const scopes = readOnly ? "canvas:read asset:read" : "canvas:read canvas:write asset:read image:generate video:generate job:manage";
744
+ 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";
657
745
  const registrationResponse = await fetchImpl(`${origin}/oauth/register`, {
658
746
  method: "POST",
659
747
  headers: { "Content-Type": "application/json", Accept: "application/json" },
@@ -757,20 +845,31 @@ async function openBrowser(url) {
757
845
  return execFile("xdg-open", [url]);
758
846
  }
759
847
 
760
- async function runConfigCommand(action, args, options, env) {
761
- if (action === "list" || action === "profiles" || !action) return listAgentProfiles(env);
848
+ async function runConfigCommand(action, args, options, env, runtime = {}) {
849
+ if (action === "list" || action === "profiles" || !action) {
850
+ const result = await listAgentProfiles(env, keychainDependencies(runtime));
851
+ return {
852
+ ...result,
853
+ profiles: result.profiles.map((profile) =>
854
+ profile.credentialState === "inaccessible" ? { ...profile, detail: keychainInaccessibleError().message } : profile,
855
+ ),
856
+ };
857
+ }
762
858
  if (action === "use") {
763
859
  const profile = firstString(options.profile) || requiredArg(args, 0, "config use requires a profile name.");
764
860
  return { active: true, ...(await setActiveAgentProfile(profile, env)) };
765
861
  }
766
862
  if (action === "get") {
767
863
  const profile = firstString(options.profile) || args[0] || "";
768
- const config = await readAgentConfig(profile ? { ...env, GAVANA_PROFILE: profile } : env);
864
+ const config = await readAgentConfig(profile ? { ...env, GAVANA_PROFILE: profile } : env, keychainDependencies(runtime));
769
865
  return {
770
866
  profile: config.profile,
771
867
  baseUrl: config.baseUrl || "",
772
- configured: Boolean(config.token && config.baseUrl),
868
+ configured: Boolean(config.credentialState === "available" && config.baseUrl),
773
869
  token: config.token ? maskToken(config.token) : "",
870
+ credentialStore: config.credentialStore || "",
871
+ credentialState: config.credentialState,
872
+ ...(config.credentialState === "inaccessible" ? { detail: keychainInaccessibleError().message } : {}),
774
873
  configPath: agentConfigFilePath(env),
775
874
  };
776
875
  }
@@ -778,17 +877,18 @@ async function runConfigCommand(action, args, options, env) {
778
877
  }
779
878
 
780
879
  async function runDoctor(client, config, env, baseUrl, credentialSource = "") {
880
+ const credentialInaccessible = config.credentialState === "inaccessible" && !credentialSource;
781
881
  const checks = [
782
882
  { name: "node", ok: Number(process.versions.node.split(".")[0]) >= 20, detail: process.version },
783
883
  {
784
884
  name: "credentials",
785
885
  ok: Boolean(client),
786
- detail: credentialSource || (config.token && config.baseUrl ? agentConfigFilePath(env) : client ? "Environment or command-line override" : "Run gavana auth login or set GAVANA_AGENT_TOKEN."),
886
+ detail: credentialInaccessible ? keychainInaccessibleError().message : credentialSource || (config.token && config.baseUrl ? agentConfigFilePath(env) : client ? "Environment or command-line override" : "Run gavana auth login or set GAVANA_AGENT_TOKEN."),
787
887
  },
788
888
  { name: "base_url", ok: /^https:\/\//.test(baseUrl) || /^http:\/\/(?:127\.0\.0\.1|localhost|\[::1\])(?::\d+)?(?:\/|$)/.test(baseUrl), detail: baseUrl },
789
889
  ];
790
890
  if (!client) {
791
- checks.push({ name: "authentication", ok: false, detail: "No token is configured. Run gavana auth login." });
891
+ checks.push({ name: "authentication", ok: false, detail: credentialInaccessible ? keychainInaccessibleError().message : "No token is configured. Run gavana auth login." });
792
892
  return { ok: false, profile: config.profile, checks };
793
893
  }
794
894
  try {
@@ -801,6 +901,14 @@ async function runDoctor(client, config, env, baseUrl, credentialSource = "") {
801
901
  return { ok: checks.every((check) => check.ok), profile: config.profile, checks };
802
902
  }
803
903
 
904
+ function keychainDependencies(runtime) {
905
+ return runtime.readKeychainCredential ? { readKeychainCredential: runtime.readKeychainCredential } : {};
906
+ }
907
+
908
+ function keychainInaccessibleError() {
909
+ return new CanvasAgentApiError("The configured macOS Keychain credential is inaccessible to this process. Run this command from a Keychain-authorized terminal or provide GAVANA_AGENT_TOKEN for this invocation; do not log in again unless the credential is actually missing.", { code: "keychain_inaccessible" });
910
+ }
911
+
804
912
  async function runApiCommand(client, methodOrPath, args, options) {
805
913
  const knownMethod = /^(get|post|put|patch|delete)$/i.test(methodOrPath || "");
806
914
  const method = knownMethod ? methodOrPath.toUpperCase() : "GET";
@@ -823,34 +931,16 @@ async function runMcpCommand(action, args, options, baseUrl, runtime) {
823
931
  if (action !== "install" && action !== "config") throw usageError("mcp supports install and config.");
824
932
  const clientName = (firstString(options.client) || args[0] || "codex").toLowerCase();
825
933
  if (action === "install" && clientName !== "codex" && clientName !== "claude") {
826
- throw usageError("mcp install supports codex and claude. Use mcp config for cursor, chatgpt, or local clients.");
934
+ throw usageError("mcp install supports codex and claude. Use mcp config for cursor, vscode, chatgpt, or local clients.");
827
935
  }
828
- const endpoint = `${baseUrl}${options["read-only"] === true ? "/mcp/readonly" : "/mcp"}`;
829
- const definition = mcpClientDefinition(clientName, endpoint, options["read-only"] === true);
936
+ const definition = gavanaMcpClientDefinition(clientName, baseUrl, options["read-only"] === true);
937
+ if (!definition) throw usageError("mcp client must be codex, claude, cursor, vscode, chatgpt, or local.");
830
938
  if (action === "config" || !definition.command) return definition;
831
939
  const execute = runtime.execFile || execFile;
832
940
  await execute(definition.command, definition.args);
833
941
  return { ...definition, installed: true };
834
942
  }
835
943
 
836
- function mcpClientDefinition(clientName, endpoint, readOnly = false) {
837
- if (clientName === "codex") return { client: "codex", transport: "streamable-http", endpoint, command: "codex", args: ["mcp", "add", "gavana", "--url", endpoint] };
838
- if (clientName === "claude") return { client: "claude", transport: "streamable-http", endpoint, command: "claude", args: ["mcp", "add", "--transport", "http", "gavana", endpoint] };
839
- if (clientName === "cursor") return { client: "cursor", transport: "streamable-http", endpoint, config: { mcpServers: { gavana: { url: endpoint } } } };
840
- if (clientName === "chatgpt") return { client: "chatgpt", transport: "streamable-http", endpoint, instructions: "Add the endpoint as a custom MCP connector, then complete Gavana OAuth in the browser." };
841
- if (clientName === "stdio" || clientName === "local") {
842
- return {
843
- client: "local",
844
- transport: "stdio",
845
- command: "npx",
846
- args: ["-y", "@gavana.ai/mcp@0.2.0"],
847
- env: readOnly ? { GAVANA_MCP_READ_ONLY: "true" } : {},
848
- credentialSource: "Reads the active Gavana CLI profile or inherited GAVANA_BASE_URL and GAVANA_AGENT_TOKEN environment variables.",
849
- };
850
- }
851
- throw usageError("mcp client must be codex, claude, cursor, chatgpt, or local.");
852
- }
853
-
854
944
  function completionScript(shell) {
855
945
  // Generated from the command table, so a new command is completable the moment
856
946
  // it is declared. This list used to be a third hand-maintained copy of the
@@ -1108,6 +1198,20 @@ async function imageInput(client, operation, args, options, runtime) {
1108
1198
  const count = supplied.count !== undefined || options.count !== undefined ? (supplied.count ?? requiredNumber(options.count, "--count must be a number.")) : undefined;
1109
1199
  if (count !== undefined && (!Number.isInteger(Number(count)) || Number(count) < 1 || Number(count) > 4)) throw usageError("--count must be an integer between 1 and 4.");
1110
1200
  const referenceInputs = imageReferenceInputs(supplied.references, options.reference, supplied.referenceRoles, options["reference-role"]);
1201
+ const optionElements = repeatableStrings(options.element);
1202
+ if (supplied.elements !== undefined && optionElements.length) throw usageError("Set Elements in JSON or with --element, not both.");
1203
+ const elements = supplied.elements === undefined ? optionElements : Array.isArray(supplied.elements) ? supplied.elements : (() => { throw usageError("elements must be an array."); })();
1204
+ if (elements.length > 8) throw usageError("An image job can use up to 8 Elements.");
1205
+ for (const entry of elements) {
1206
+ 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".');
1208
+ const handle = typeof entry === "string" ? entry.trim() : typeof typed?.handle === "string" ? typed.handle.trim() : "";
1209
+ 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
+ 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
+ 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.");
1212
+ }
1213
+ const elementHandles = elements.map((entry) => (typeof entry === "string" ? entry.trim() : entry.handle.trim()));
1214
+ if (new Set(elementHandles).size !== elementHandles.length) throw usageError("Element handles must be unique.");
1111
1215
  const preparedReferences = await prepareReferenceInputs(referenceInputs.map((reference) => reference.value), runtime);
1112
1216
  const canvasResult = await client.resolveCanvasDestination(destination, {
1113
1217
  idempotencyKey,
@@ -1145,6 +1249,7 @@ async function imageInput(client, operation, args, options, runtime) {
1145
1249
  targetY: _targetY,
1146
1250
  targetWidth: _targetWidth,
1147
1251
  targetHeight: _targetHeight,
1252
+ elements: _elements,
1148
1253
  ...jobSupplied
1149
1254
  } = supplied;
1150
1255
  return {
@@ -1156,6 +1261,7 @@ async function imageInput(client, operation, args, options, runtime) {
1156
1261
  ...(prompt ? { prompt } : {}),
1157
1262
  ...(supplied.promptNodeId || firstString(options["prompt-node"]) ? { promptNodeId: supplied.promptNodeId || firstString(options["prompt-node"]) } : {}),
1158
1263
  ...(references.length ? { references: references.some((reference) => reference.role) ? references : referenceHandles } : {}),
1264
+ ...(elements.length ? { elements } : {}),
1159
1265
  ...(source ? { source } : {}),
1160
1266
  ...(supplied.connectionId || firstString(options.connection) ? { connectionId: supplied.connectionId || firstString(options.connection) } : {}),
1161
1267
  ...(supplied.model || firstString(options.model) ? { model: supplied.model || firstString(options.model) } : {}),
@@ -1763,6 +1869,7 @@ function exitCodeForError(error) {
1763
1869
  if (
1764
1870
  error.code === "usage" ||
1765
1871
  error.code === "configuration" ||
1872
+ error.code === "keychain_inaccessible" ||
1766
1873
  error.code === "validation" ||
1767
1874
  error.code === "input_validation_error" ||
1768
1875
  error.code === "invalid_json" ||
@@ -1786,10 +1893,55 @@ function requiredArg(args, index, message) {
1786
1893
  return value;
1787
1894
  }
1788
1895
 
1896
+ function workContinuationInput(options) {
1897
+ const action = firstString(options.action);
1898
+ const idempotencyKey = firstString(options["idempotency-key"]);
1899
+ if (action === "answer") return { action, answer: firstString(options.answer), idempotencyKey };
1900
+ if (action === "select_direction") return { action, directionId: firstString(options["direction-id"]), idempotencyKey };
1901
+ if (action === "adjust") return { action, adjustment: firstString(options.adjustment), idempotencyKey };
1902
+ if (action === "acknowledge_canvas") return { action, idempotencyKey };
1903
+ if (options.rebase === true) return { rebase: true, idempotencyKey };
1904
+ throw usageError("work continue requires --action answer, select_direction, adjust, acknowledge_canvas, or --rebase.");
1905
+ }
1906
+
1907
+ function omitKeys(value, keys) {
1908
+ return Object.fromEntries(Object.entries(value).filter(([key]) => !keys.includes(key)));
1909
+ }
1910
+
1789
1911
  function requireConfirmation(options, message) {
1790
1912
  if (options.yes !== true) throw usageError(message);
1791
1913
  }
1792
1914
 
1915
+ function requireWorkConfirmation(options) {
1916
+ if (options.confirm !== true) throw usageError("work execute starts paid work. Re-run with --confirm.");
1917
+ }
1918
+
1919
+ async function requireDestructiveConfirmation(options, runtime, prompt, nonInteractiveMessage) {
1920
+ if (options.yes === true) return;
1921
+ if (!runtime.isInteractive) throw usageError(nonInteractiveMessage);
1922
+ const confirmed = typeof runtime.confirm === "function" ? await runtime.confirm(prompt) : await promptForConfirmation(runtime.stdin, runtime.stderr, prompt);
1923
+ if (!confirmed) throw usageError("Destructive operation cancelled.");
1924
+ }
1925
+
1926
+ async function promptForConfirmation(input, output, prompt) {
1927
+ const terminal = createInterface({ input, output, terminal: true });
1928
+ try {
1929
+ return /^(y|yes)$/i.test((await terminal.question(`${prompt} [y/N] `)).trim());
1930
+ } finally {
1931
+ terminal.close();
1932
+ }
1933
+ }
1934
+
1935
+ function elementInputFromOptions(options) {
1936
+ return {
1937
+ name: firstString(options.name),
1938
+ type: firstString(options.type),
1939
+ sourceAssetIds: repeatableStrings(options["source-asset"]),
1940
+ ...(firstString(options.guidelines) ? { guidelines: firstString(options.guidelines) } : {}),
1941
+ ...(repeatableStrings(options.collection).length ? { collectionIds: repeatableStrings(options.collection) } : {}),
1942
+ };
1943
+ }
1944
+
1793
1945
  function parseJsonOption(value, message) {
1794
1946
  const text = firstString(value);
1795
1947
  return text ? parseJson(text, message) : null;
@@ -7,8 +7,8 @@ import { campaignAspectRatio, campaignIdempotencyKey, campaignProductReference,
7
7
 
8
8
  export function defineCampaignPlan(client) {
9
9
  return {
10
- title: "Legacy campaign plan",
11
- description: "Compatibility operation for an existing campaign integration. Do not use it for new work; use editable canvas blocks and explicit image jobs instead.",
10
+ title: "Retired campaign plan (migration only)",
11
+ description: "Compatibility operation for an existing historical campaign integration. The server rejects mutations unless its explicit migration flag is enabled. Do not use it for new creative work; use editable Canvas Sections, reference nodes, and explicit image jobs instead.",
12
12
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
13
13
  inputSchema: z.object({
14
14
  canvasId: exactCanvasReference,
@@ -7,8 +7,8 @@ import { approvedOutputReferences, campaignIdempotencyKey, runReference } from "
7
7
 
8
8
  export function defineCampaignReview(client) {
9
9
  return {
10
- title: "Legacy campaign review",
11
- description: "Compatibility operation for an existing campaign record. New work should remain editable on the canvas.",
10
+ title: "Retired campaign review (migration only)",
11
+ description: "Compatibility operation for an existing historical campaign record. This old approval gate is not for new creative work; keep new directions editable and spatially organized on the Canvas.",
12
12
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
13
13
  inputSchema: z.object({
14
14
  runId: runReference,
@@ -7,8 +7,8 @@ import { campaignIdempotencyKey, campaignReference } from "./schemas.mjs";
7
7
 
8
8
  export function defineCampaignStart(client) {
9
9
  return {
10
- title: "Legacy campaign start",
11
- description: "Compatibility operation for an existing campaign record. Do not use it as a new-work workflow.",
10
+ title: "Retired campaign start (migration only)",
11
+ description: "Starts an existing historical campaign record only when the server's explicit compatibility migration flag is enabled. Its old review gate is not a creative workflow; use editable Canvas Sections, reference nodes, and explicit image jobs for new work.",
12
12
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: true },
13
13
  inputSchema: z.object({
14
14
  campaignId: campaignReference,
@@ -31,6 +31,18 @@ import { defineCanvasList } from "./canvas_list.mjs";
31
31
  import { defineCanvasValidate } from "./canvas_validate.mjs";
32
32
  import { defineConnectionCreate } from "./connection_create.mjs";
33
33
  import { defineConnectionDelete } from "./connection_delete.mjs";
34
+ import { defineElementArchive } from "./element_archive.mjs";
35
+ import { defineElementCollectionCreate } from "./element_collection_create.mjs";
36
+ import { defineElementCollectionDelete } from "./element_collection_delete.mjs";
37
+ import { defineElementCollectionList } from "./element_collection_list.mjs";
38
+ import { defineElementCollectionUpdate } from "./element_collection_update.mjs";
39
+ import { defineElementCreate } from "./element_create.mjs";
40
+ import { defineElementGet } from "./element_get.mjs";
41
+ import { defineElementHistory } from "./element_history.mjs";
42
+ import { defineElementList } from "./element_list.mjs";
43
+ import { defineElementRestore } from "./element_restore.mjs";
44
+ import { defineElementUpdate } from "./element_update.mjs";
45
+ import { defineElementUpdateCollections } from "./element_update_collections.mjs";
34
46
  import { defineGuideGet } from "./guide_get.mjs";
35
47
  import { defineGuideSearch } from "./guide_search.mjs";
36
48
  import { defineJobCancel } from "./job_cancel.mjs";
@@ -53,6 +65,11 @@ import { defineRunCancel } from "./run_cancel.mjs";
53
65
  import { defineRunGet } from "./run_get.mjs";
54
66
  import { defineRunWait } from "./run_wait.mjs";
55
67
  import { defineVideoGenerate } from "./video_generate.mjs";
68
+ import { defineWorkContinue } from "./work_continue.mjs";
69
+ import { defineWorkExecute } from "./work_execute.mjs";
70
+ import { defineWorkGet } from "./work_get.mjs";
71
+ import { defineWorkPrepare } from "./work_prepare.mjs";
72
+ import { defineWorkRefresh } from "./work_refresh.mjs";
56
73
 
57
74
  /** @type {Record<string, (client: unknown) => object>} */
58
75
  export const GAVANA_TOOL_DEFINITIONS = Object.freeze({
@@ -79,6 +96,18 @@ export const GAVANA_TOOL_DEFINITIONS = Object.freeze({
79
96
  canvas_validate: defineCanvasValidate,
80
97
  connection_create: defineConnectionCreate,
81
98
  connection_delete: defineConnectionDelete,
99
+ element_archive: defineElementArchive,
100
+ element_collection_create: defineElementCollectionCreate,
101
+ element_collection_delete: defineElementCollectionDelete,
102
+ element_collection_list: defineElementCollectionList,
103
+ element_collection_update: defineElementCollectionUpdate,
104
+ element_create: defineElementCreate,
105
+ element_get: defineElementGet,
106
+ element_history: defineElementHistory,
107
+ element_list: defineElementList,
108
+ element_restore: defineElementRestore,
109
+ element_update: defineElementUpdate,
110
+ element_update_collections: defineElementUpdateCollections,
82
111
  guide_get: defineGuideGet,
83
112
  guide_search: defineGuideSearch,
84
113
  job_cancel: defineJobCancel,
@@ -101,6 +130,11 @@ export const GAVANA_TOOL_DEFINITIONS = Object.freeze({
101
130
  run_get: defineRunGet,
102
131
  run_wait: defineRunWait,
103
132
  video_generate: defineVideoGenerate,
133
+ work_continue: defineWorkContinue,
134
+ work_execute: defineWorkExecute,
135
+ work_get: defineWorkGet,
136
+ work_prepare: defineWorkPrepare,
137
+ work_refresh: defineWorkRefresh,
104
138
  });
105
139
 
106
140
  /** Build every local tool config for a client, in registry order. */
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import { mutableElementReference } from "./schemas.mjs";
3
+
4
+ export function defineElementArchive(client) {
5
+ return {
6
+ title: "Archive a Gavana Element",
7
+ description: "Archive an Element. First read and show the affected Element to the user; it stays recoverable and confirm must be true only after explicit approval.",
8
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
9
+ inputSchema: z.object({ elementId: mutableElementReference, confirm: z.literal(true).describe("Must be true after the user confirms archival.") }),
10
+ handler: ({ elementId }) => client.archiveElement(elementId),
11
+ };
12
+ }
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+
3
+ export function defineElementCollectionCreate(client) {
4
+ return {
5
+ title: "Create an Element collection",
6
+ description: "Create a user-owned collection for organizing Elements.",
7
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
8
+ inputSchema: z.object({ name: z.string().min(1).max(120) }),
9
+ handler: (input) => client.createElementCollection(input),
10
+ };
11
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import { elementCollectionReference } from "./schemas.mjs";
3
+
4
+ export function defineElementCollectionDelete(client) {
5
+ return {
6
+ title: "Delete an Element collection",
7
+ description: "Delete a collection without deleting its Elements. First show the affected collection to the user; confirm must be true only after explicit approval.",
8
+ annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
9
+ inputSchema: z.object({ collectionId: elementCollectionReference, confirm: z.literal(true).describe("Must be true after the user confirms deletion.") }),
10
+ handler: ({ collectionId }) => client.deleteElementCollection(collectionId),
11
+ };
12
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { listOptions } from "./helpers.mjs";
3
+ import { listCursor, listLimit } from "./schemas.mjs";
4
+
5
+ export function defineElementCollectionList(client) {
6
+ return {
7
+ title: "List Element collections",
8
+ description: "List the user's Element collections.",
9
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10
+ inputSchema: z.object({ limit: listLimit(100), cursor: listCursor }),
11
+ handler: ({ limit, cursor }) => client.listElementCollections(listOptions(limit, cursor)),
12
+ };
13
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import { elementCollectionReference } from "./schemas.mjs";
3
+
4
+ export function defineElementCollectionUpdate(client) {
5
+ return {
6
+ title: "Rename an Element collection",
7
+ description: "Rename a user-owned Element collection.",
8
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false },
9
+ inputSchema: z.object({ collectionId: elementCollectionReference, name: z.string().min(1).max(120) }),
10
+ handler: ({ collectionId, name }) => client.updateElementCollection(collectionId, { name }),
11
+ };
12
+ }
@@ -0,0 +1,11 @@
1
+ import { elementInput } from "./schemas.mjs";
2
+
3
+ export function defineElementCreate(client) {
4
+ return {
5
+ title: "Create a Gavana Element",
6
+ description: "Create a reusable visual Element from 1-8 existing image assets, guidelines, or both.",
7
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
8
+ inputSchema: elementInput,
9
+ handler: (input) => client.createElement(input),
10
+ };
11
+ }
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ import { pinnedElementReference } from "./schemas.mjs";
3
+
4
+ export function defineElementGet(client) {
5
+ return {
6
+ title: "Read a Gavana Element",
7
+ description: "Read one exact immutable Element revision. The user must provide an element:<id>@v<n> handle returned by list or history.",
8
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
9
+ inputSchema: z.object({ elementId: pinnedElementReference }),
10
+ handler: ({ elementId }) => client.getElement(elementId),
11
+ };
12
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { listOptions } from "./helpers.mjs";
3
+ import { listCursor, listLimit, mutableElementReference } from "./schemas.mjs";
4
+
5
+ export function defineElementHistory(client) {
6
+ return {
7
+ title: "Read Element version history",
8
+ description: "List immutable revisions for one Element, newest first.",
9
+ annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
10
+ inputSchema: z.object({ elementId: mutableElementReference, limit: listLimit(100), cursor: listCursor }),
11
+ handler: ({ elementId, limit, cursor }) => client.listElementHistory(elementId, listOptions(limit, cursor)),
12
+ };
13
+ }