@gavana.ai/cli 0.3.0 → 0.3.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +58 -0
- package/bin/gavana.mjs +0 -0
- package/guides/generated-assets.md +8 -0
- package/package.json +1 -1
- package/src/canvas-agent-guide.mjs +1 -1
- package/src/canvas-agent-validation.mjs +77 -2
- package/src/commands.mjs +2 -2
- package/src/config.mjs +20 -2
- package/src/guide-sources.mjs +1 -1
- package/src/runner.mjs +58 -8
- package/src/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,63 @@
|
|
|
1
1
|
# Changelog — @gavana.ai/cli
|
|
2
2
|
|
|
3
|
+
## 0.3.3
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- **`auth login` stops handing out an instruction it already carried out.** It
|
|
8
|
+
opened the browser and then printed a line that read as a request to open the
|
|
9
|
+
URL, so a coding agent running the command opened it a second time and the
|
|
10
|
+
user was left on two tabs of one authorization request — same `client_id`,
|
|
11
|
+
same `state`, same `code_challenge`. The browser path now says the URL is
|
|
12
|
+
already open and prints it only to be shown. The one case that still asks is a
|
|
13
|
+
machine where the open actually failed.
|
|
14
|
+
- **The login wait is announced, and settable.** It blocked for an unannounced
|
|
15
|
+
180 seconds, longer than the command timeout a caller is likely to assume, so
|
|
16
|
+
an agent-run login was killed mid-sign-in and took the local callback server —
|
|
17
|
+
and the user's open tab — with it. The wait is now printed, `--timeout
|
|
18
|
+
SECONDS` sets it, and the timeout error says the open page is dead and that
|
|
19
|
+
re-running prints a new URL.
|
|
20
|
+
- **`canvas_validate` no longer excuses a run that referenced nothing.** The
|
|
21
|
+
product-fidelity review began by filtering to outputs that already carried
|
|
22
|
+
reference evidence, so a prompt-only run emptied that filter and was graded
|
|
23
|
+
`not-applicable` — the worst case was the one case it excused. An imported
|
|
24
|
+
product reference that no generated output uses now raises
|
|
25
|
+
`unused_product_reference` and reports `needs-review`, naming both the ignored
|
|
26
|
+
product and the outputs that ignored it. A canvas holding an imported product
|
|
27
|
+
and no generated output stays `not-applicable`.
|
|
28
|
+
|
|
29
|
+
## 0.3.2
|
|
30
|
+
|
|
31
|
+
### Changed
|
|
32
|
+
|
|
33
|
+
- **`mcp install` says what is left to do.** It answered `installed: true` and
|
|
34
|
+
stopped, so agents reported the tools as ready while the server sat
|
|
35
|
+
unauthenticated. The result now carries `authenticated: false` and the
|
|
36
|
+
remaining steps: restart the client, sign that server in (its login is its
|
|
37
|
+
own, separate from the CLI's), and confirm with a real read rather than a
|
|
38
|
+
status line.
|
|
39
|
+
|
|
40
|
+
### Fixed
|
|
41
|
+
|
|
42
|
+
- **`bin/gavana.mjs` ships executable.** It was published as `0644` while its
|
|
43
|
+
`craftboard` twin was `0755`, so the `gavana` command depended on npm
|
|
44
|
+
repairing the mode while linking. Where it did not, the first command a new
|
|
45
|
+
user ran answered `permission denied` (exit 126).
|
|
46
|
+
|
|
47
|
+
## 0.3.1
|
|
48
|
+
|
|
49
|
+
### Fixed
|
|
50
|
+
|
|
51
|
+
- **`auth login` no longer stacks a new delegation on every login.** The CLI
|
|
52
|
+
registered a fresh OAuth client each time — a new client id against a new
|
|
53
|
+
loopback port — so Gavana could not tell one login from the next, and the
|
|
54
|
+
delegations it minted never expire. They accumulated until the account hit
|
|
55
|
+
its token cap, at which point the exchange failed and the CLI answered with
|
|
56
|
+
another login that failed the same way. The CLI now registers under a stable
|
|
57
|
+
per-installation id (`installId` in the agent config), so Gavana returns the
|
|
58
|
+
client id this machine already had and replaces the delegation it holds.
|
|
59
|
+
Other machines are untouched: each install keeps its own id.
|
|
60
|
+
|
|
3
61
|
## 0.3.0
|
|
4
62
|
|
|
5
63
|
### Added
|
package/bin/gavana.mjs
CHANGED
|
File without changes
|
|
@@ -33,6 +33,14 @@ order: 6
|
|
|
33
33
|
proves more than one variant, pass the one the user named rather than letting
|
|
34
34
|
the import pick. If no durable references can be imported, say exact product
|
|
35
35
|
fidelity is unverified.
|
|
36
|
+
- When the user supplied the product image themselves and there is no product
|
|
37
|
+
page, call `product_photoshoot_generate` with `references` set to the durable
|
|
38
|
+
`node:` handles already on the canvas, one direction object per requested
|
|
39
|
+
output. It verifies those nodes before any paid work and stops when one is
|
|
40
|
+
not a durable image. Save an asset to the canvas first: a workflow input
|
|
41
|
+
binds to a node, not to an `asset:` handle. This is the route for several
|
|
42
|
+
distinct directions from one uploaded photo; one image call with `count > 1`
|
|
43
|
+
is not.
|
|
36
44
|
{{/hosted}}{{#local}}- This surface has no product-page importer. Ask the user for the images
|
|
37
45
|
themselves, bring them in through the supported asset flow, and pass the
|
|
38
46
|
resulting handles as references. Do not fetch the page and describe it in a
|
package/package.json
CHANGED
|
@@ -8,7 +8,7 @@ import { renderForSurface } from "./tools/surface-names.mjs";
|
|
|
8
8
|
export const GAVANA_CANVAS_GUIDE_VERSION = "1.6.1";
|
|
9
9
|
export const GAVANA_CANVAS_GUIDE_INDEX_URI = `gavana://guides/canvas/v${GAVANA_CANVAS_GUIDE_VERSION.split(".")[0]}/index`;
|
|
10
10
|
export const GAVANA_CANVAS_GUIDE_WORKFLOW_INSTRUCTION =
|
|
11
|
-
"For the first canvas task in a session, and before any unfamiliar canvas operation, inspect the relevant guide through MCP resources or call guide_search then guide_get. Before changing an existing canvas, call canvas_get. For spatial, multi-node, or destructive work, call canvas_validate with the proposed operations, apply one revision-safe atomic batch, then call canvas_validate again. Campaign work must read creative-canvas and use a reference-led concept before image output. {{#local}}Before image or video generation, call model_list for the required capability and use its exact model: handle; a bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access the connection instead of asking the user to add a key again.{{/local}}{{#hosted}}Before video generation, call video_model_find for the required capability and use its exact model: handle; a bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access the connection instead of asking the user to add a key again. Image generation has no model-discovery tool here: omit model unless the user named an exact handle.{{/hosted}} A product page URL is source input, never prompt prose. {{#hosted}}When asked to generate one or more exact-product directions from a product page, call product_photoshoot_generate once with one explicit direction object per requested output; it imports verified references before any paid generation and stops if none are available. Use product_reference_pack_import only for a standalone import with no generation. When the page proves more than one variant, pass the variantSelector the user named.{{/hosted}}{{#local}}This surface has no product-page importer: ask for the images themselves and bring them in through the supported asset flow rather than describing the page in a prompt.{{/local}} When a standalone image fallback uses visual references, forward every exact node:/asset: handle (and each known role) in the image request; never reduce that work to a prompt-only generation. Image tools return durable queued progress by default; report it immediately and {{#local}}call run_wait{{/local}}{{#hosted}}set wait true{{/hosted}} only when the current user explicitly needs the completed asset in the same interaction. Otherwise run_get is authoritative; use the exact Canvas URL for navigation, but remember Canvas activity may lag. After an image or workflow finishes, read completionReview. Never claim Done while completionReview.doneClaimAllowed is false, delivery is pending, failed, or non-durable, or blocking findings remain. Render campaign compositions for visual inspection. Treat legacy warnings as review items, not permission to rewrite unrelated work. For N separate images, set count on one image call and never ask one prompt for N panels, frames, or a collage.";
|
|
11
|
+
"For the first canvas task in a session, and before any unfamiliar canvas operation, inspect the relevant guide through MCP resources or call guide_search then guide_get. Before changing an existing canvas, call canvas_get. For spatial, multi-node, or destructive work, call canvas_validate with the proposed operations, apply one revision-safe atomic batch, then call canvas_validate again. Campaign work must read creative-canvas and use a reference-led concept before image output. {{#local}}Before image or video generation, call model_list for the required capability and use its exact model: handle; a bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access the connection instead of asking the user to add a key again.{{/local}}{{#hosted}}Before video generation, call video_model_find for the required capability and use its exact model: handle; a bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access the connection instead of asking the user to add a key again. Image generation has no model-discovery tool here: omit model unless the user named an exact handle.{{/hosted}} A product page URL is source input, never prompt prose. {{#hosted}}When asked to generate one or more exact-product directions from a product page, call product_photoshoot_generate once with one explicit direction object per requested output; it imports verified references before any paid generation and stops if none are available. Use product_reference_pack_import only for a standalone import with no generation. When the page proves more than one variant, pass the variantSelector the user named. When the product image is already on the canvas and there is no product page, call the same tool with references set to its durable node: handles and one direction object per requested output; it verifies those nodes before any paid generation. Several distinct directions are never one image call with count > 1.{{/hosted}}{{#local}}This surface has no product-page importer: ask for the images themselves and bring them in through the supported asset flow rather than describing the page in a prompt.{{/local}} When a standalone image fallback uses visual references, forward every exact node:/asset: handle (and each known role) in the image request; never reduce that work to a prompt-only generation. Image tools return durable queued progress by default; report it immediately and {{#local}}call run_wait{{/local}}{{#hosted}}set wait true{{/hosted}} only when the current user explicitly needs the completed asset in the same interaction. Otherwise run_get is authoritative; use the exact Canvas URL for navigation, but remember Canvas activity may lag. After an image or workflow finishes, read completionReview. Never claim Done while completionReview.doneClaimAllowed is false, delivery is pending, failed, or non-durable, or blocking findings remain. Render campaign compositions for visual inspection. Treat legacy warnings as review items, not permission to rewrite unrelated work. For N separate images, set count on one image call and never ask one prompt for N panels, frames, or a collage.";
|
|
12
12
|
export const GAVANA_CANVAS_GUIDE_READ_ONLY_INSTRUCTION =
|
|
13
13
|
"For the first canvas task in a session, and before any unfamiliar canvas review, inspect the relevant guide through MCP resources or call guide_search then guide_get. Call canvas_get before reasoning about an existing canvas, and use canvas_validate to audit its current graph. Treat validation warnings as review items and never claim to mutate the canvas.";
|
|
14
14
|
|
|
@@ -324,9 +324,65 @@ function graphFindings(canvas, validationOptions = {}) {
|
|
|
324
324
|
}
|
|
325
325
|
}
|
|
326
326
|
}
|
|
327
|
+
|
|
328
|
+
// Canvas-level, because the failure is an absence: no single output is
|
|
329
|
+
// malformed, they simply all ignored the product the person imported.
|
|
330
|
+
// Reported against the outputs so the standard scoping decides whether it
|
|
331
|
+
// blocks — this task's work is pinned, somebody else's older run is not.
|
|
332
|
+
const unusedProducts = unusedProductReferenceNodes(canvas);
|
|
333
|
+
if (unusedProducts.length) {
|
|
334
|
+
const outputs = canvas.nodes.filter(isGeneratedOutput);
|
|
335
|
+
if (outputs.length) {
|
|
336
|
+
findings.push(
|
|
337
|
+
finding(
|
|
338
|
+
"unused_product_reference",
|
|
339
|
+
"warning",
|
|
340
|
+
`${unusedProducts.map((node) => nodeHandle(node.id)).join(", ")} was imported as an exact product reference, but no generated output on this canvas references it. Prompt-only output is not the exact product.`,
|
|
341
|
+
{ nodeIds: [...outputs.map((node) => cleanId(node.id)), ...unusedProducts.map((node) => cleanId(node.id))], guideId: "generated-assets" },
|
|
342
|
+
),
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
327
346
|
return { findings, truncatedCodes: Array.from(truncatedCodes) };
|
|
328
347
|
}
|
|
329
348
|
|
|
349
|
+
/**
|
|
350
|
+
* Imported product references that nothing generated on this canvas used.
|
|
351
|
+
*
|
|
352
|
+
* `productFidelityReview` cannot see this on its own: it starts by filtering to
|
|
353
|
+
* outputs that already carry reference evidence, so the run that used no
|
|
354
|
+
* reference at all empties its input and grades itself not-applicable. The
|
|
355
|
+
* worst case was the one case it excused.
|
|
356
|
+
*/
|
|
357
|
+
function unusedProductReferenceNodes(canvas) {
|
|
358
|
+
const products = canvas.nodes.filter((node) => isRecord(node.metadata) && node.metadata.productReference === true);
|
|
359
|
+
if (!products.length) return [];
|
|
360
|
+
const outputs = canvas.nodes.filter(isGeneratedOutput);
|
|
361
|
+
if (!outputs.length) return [];
|
|
362
|
+
const usedNodeIds = new Set();
|
|
363
|
+
const usedStorageKeys = new Set();
|
|
364
|
+
for (const output of outputs) {
|
|
365
|
+
const metadata = isRecord(output.metadata) ? output.metadata : {};
|
|
366
|
+
for (const value of Array.isArray(metadata.referenceNodeIds) ? metadata.referenceNodeIds : []) usedNodeIds.add(cleanId(value));
|
|
367
|
+
for (const value of Array.isArray(metadata.referenceHandles) ? metadata.referenceHandles : []) {
|
|
368
|
+
const handle = String(value || "");
|
|
369
|
+
if (handle.startsWith("node:")) usedNodeIds.add(cleanId(handle.slice("node:".length)));
|
|
370
|
+
}
|
|
371
|
+
for (const value of Array.isArray(metadata.references) ? metadata.references : []) usedStorageKeys.add(String(value || ""));
|
|
372
|
+
for (const connection of canvas.connections) {
|
|
373
|
+
if (connection.mode !== "reference") continue;
|
|
374
|
+
if (cleanId(connection.toNodeId || connection.to) !== cleanId(output.id)) continue;
|
|
375
|
+
usedNodeIds.add(cleanId(connection.fromNodeId || connection.from));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
return products.filter((node) => {
|
|
379
|
+
if (usedNodeIds.has(cleanId(node.id))) return false;
|
|
380
|
+
const storageKey = isRecord(node.metadata) ? node.metadata.storageKey : undefined;
|
|
381
|
+
const content = isRecord(node.metadata) ? node.metadata.content : undefined;
|
|
382
|
+
return !(storageKey && usedStorageKeys.has(String(storageKey))) && !(content && usedStorageKeys.has(String(content)));
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
330
386
|
/**
|
|
331
387
|
* Detect the most specific candidate Section by center point so validation can
|
|
332
388
|
* diagnose a frame that visibly escapes it. Persisted section membership uses
|
|
@@ -482,11 +538,29 @@ function reviewArea(findings, ...codes) {
|
|
|
482
538
|
}
|
|
483
539
|
|
|
484
540
|
function productFidelityReview(canvas, generatedOutputs) {
|
|
541
|
+
const unusedProductHandles = unusedProductReferenceNodes(canvas)
|
|
542
|
+
.map((node) => nodeHandle(node.id))
|
|
543
|
+
.sort();
|
|
485
544
|
const referenceTargets = generatedOutputs.filter((node) => outputHasReferenceEvidence(node, canvas.connections));
|
|
486
545
|
if (!referenceTargets.length) {
|
|
546
|
+
// An unused imported product is the whole reason this review exists.
|
|
547
|
+
// Standing down here graded the one run that ignored every reference as
|
|
548
|
+
// if fidelity were never in question.
|
|
549
|
+
if (unusedProductHandles.length && generatedOutputs.length) {
|
|
550
|
+
const outputHandles = generatedOutputs.map((node) => nodeHandle(node.id)).sort();
|
|
551
|
+
return {
|
|
552
|
+
status: "needs-review",
|
|
553
|
+
reviewedOutputCount: generatedOutputs.length,
|
|
554
|
+
unusedProductReferenceHandles: unusedProductHandles,
|
|
555
|
+
needsReviewOutputHandles: outputHandles,
|
|
556
|
+
evidenceMissingOutputHandles: [],
|
|
557
|
+
reasons: outputHandles.map((handle) => ({ nodeHandle: handle, reason: `Generated without referencing the imported product ${unusedProductHandles.join(", ")}; exact product fidelity is unverified.` })),
|
|
558
|
+
};
|
|
559
|
+
}
|
|
487
560
|
return {
|
|
488
561
|
status: "not-applicable",
|
|
489
562
|
reviewedOutputCount: 0,
|
|
563
|
+
unusedProductReferenceHandles: unusedProductHandles,
|
|
490
564
|
needsReviewOutputHandles: [],
|
|
491
565
|
evidenceMissingOutputHandles: [],
|
|
492
566
|
};
|
|
@@ -501,9 +575,10 @@ function productFidelityReview(canvas, generatedOutputs) {
|
|
|
501
575
|
// References preserve origin and intent. They never turn a retained
|
|
502
576
|
// creative output into a completion gate. Exact composition is the
|
|
503
577
|
// exception: it must carry a verified finalized-image hash.
|
|
504
|
-
status: evidenceMissing.length ? "needs-review" : "passed",
|
|
578
|
+
status: evidenceMissing.length || unusedProductHandles.length ? "needs-review" : "passed",
|
|
505
579
|
reviewedOutputCount: referenceTargets.length,
|
|
506
|
-
|
|
580
|
+
unusedProductReferenceHandles: unusedProductHandles,
|
|
581
|
+
needsReviewOutputHandles: uniqueSorted([...evidenceMissingOutputHandles, ...(unusedProductHandles.length ? generatedOutputs.filter((node) => !outputHasReferenceEvidence(node, canvas.connections)).map((node) => nodeHandle(node.id)) : [])]),
|
|
507
582
|
evidenceMissingOutputHandles,
|
|
508
583
|
reasons: evidenceMissing.map((node) => ({ nodeHandle: nodeHandle(node.id), reason: "Exact composition is missing its verified output hash." })),
|
|
509
584
|
};
|
package/src/commands.mjs
CHANGED
|
@@ -41,8 +41,8 @@ export const GAVANA_CLI_COMMANDS = Object.freeze([
|
|
|
41
41
|
{
|
|
42
42
|
group: "auth",
|
|
43
43
|
action: "login",
|
|
44
|
-
usage: ["gavana auth login [--profile NAME] [--read-only]", "gavana auth login --base-url URL --token-stdin"],
|
|
45
|
-
groupUsage: ["gavana auth login [--profile NAME] [--read-only] [--no-browser]", "gavana auth login --token-stdin [--profile NAME]"],
|
|
44
|
+
usage: ["gavana auth login [--profile NAME] [--read-only] [--timeout SECONDS]", "gavana auth login --base-url URL --token-stdin"],
|
|
45
|
+
groupUsage: ["gavana auth login [--profile NAME] [--read-only] [--no-browser] [--timeout SECONDS]", "gavana auth login --token-stdin [--profile NAME]"],
|
|
46
46
|
},
|
|
47
47
|
{ group: "auth", action: "status", usage: ["gavana auth status"], groupUsage: ["gavana auth status [--profile NAME]"] },
|
|
48
48
|
{ group: "config", action: "list", usage: ["gavana config list | get [PROFILE] | use PROFILE"], groupUsage: ["gavana config list"] },
|
package/src/config.mjs
CHANGED
|
@@ -2,10 +2,12 @@ import fs from "node:fs/promises";
|
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { execFile as execFileCallback } from "node:child_process";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
5
6
|
import { promisify } from "node:util";
|
|
6
7
|
|
|
7
8
|
const execFile = promisify(execFileCallback);
|
|
8
9
|
const KEYCHAIN_SERVICE = "ai.gavana.cli";
|
|
10
|
+
const INSTALL_ID_PATTERN = /^[A-Za-z0-9_-]{16,64}$/;
|
|
9
11
|
|
|
10
12
|
export async function readAgentConfig(env = process.env, dependencies = {}) {
|
|
11
13
|
const store = await readAgentConfigStore(env);
|
|
@@ -68,6 +70,7 @@ export async function writeAgentConfig(config, env = process.env) {
|
|
|
68
70
|
if (credentialStore === "macos-keychain") delete profileConfig.token;
|
|
69
71
|
const next = {
|
|
70
72
|
version: 1,
|
|
73
|
+
...(current.installId ? { installId: current.installId } : {}),
|
|
71
74
|
activeProfile: profile,
|
|
72
75
|
profiles: { ...(current.profiles || {}), [profile]: profileConfig },
|
|
73
76
|
};
|
|
@@ -90,6 +93,20 @@ export async function writeAgentConfigStore(config, env = process.env) {
|
|
|
90
93
|
return filePath;
|
|
91
94
|
}
|
|
92
95
|
|
|
96
|
+
/**
|
|
97
|
+
* The id this installation registers its OAuth client under. It is not a
|
|
98
|
+
* credential: it exists so a repeat `auth login` is recognised as the same
|
|
99
|
+
* install and replaces the delegation it already holds, instead of leaving a
|
|
100
|
+
* new one behind on every login.
|
|
101
|
+
*/
|
|
102
|
+
export async function readOAuthInstallId(env = process.env) {
|
|
103
|
+
const store = await readAgentConfigStore(env);
|
|
104
|
+
if (store.installId) return store.installId;
|
|
105
|
+
const installId = randomBytes(24).toString("base64url");
|
|
106
|
+
await writeAgentConfigStore({ ...store, installId }, env);
|
|
107
|
+
return installId;
|
|
108
|
+
}
|
|
109
|
+
|
|
93
110
|
export async function removeAgentConfig(env = process.env) {
|
|
94
111
|
const filePaths = agentConfigFileCandidates(env);
|
|
95
112
|
await Promise.all(filePaths.map((filePath) => fs.rm(filePath, { force: true })));
|
|
@@ -164,14 +181,15 @@ function agentConfigFileCandidates(env) {
|
|
|
164
181
|
|
|
165
182
|
function normalizeConfigStore(value) {
|
|
166
183
|
if (!value || typeof value !== "object" || Array.isArray(value)) return { version: 1, activeProfile: "default", profiles: {} };
|
|
184
|
+
const install = typeof value.installId === "string" && INSTALL_ID_PATTERN.test(value.installId) ? { installId: value.installId } : {};
|
|
167
185
|
if (value.profiles && typeof value.profiles === "object" && !Array.isArray(value.profiles)) {
|
|
168
|
-
return { version: 1, activeProfile: cleanProfileName(value.activeProfile || "default"), profiles: value.profiles };
|
|
186
|
+
return { version: 1, ...install, activeProfile: cleanProfileName(value.activeProfile || "default"), profiles: value.profiles };
|
|
169
187
|
}
|
|
170
188
|
const legacy = {
|
|
171
189
|
...(typeof value.baseUrl === "string" && value.baseUrl ? { baseUrl: value.baseUrl } : {}),
|
|
172
190
|
...(typeof value.token === "string" && value.token ? { token: value.token } : {}),
|
|
173
191
|
};
|
|
174
|
-
return { version: 1, activeProfile: "default", profiles: Object.keys(legacy).length ? { default: legacy } : {} };
|
|
192
|
+
return { version: 1, ...install, activeProfile: "default", profiles: Object.keys(legacy).length ? { default: legacy } : {} };
|
|
175
193
|
}
|
|
176
194
|
|
|
177
195
|
function selectedProfileName(store, env) {
|
package/src/guide-sources.mjs
CHANGED
|
@@ -49,7 +49,7 @@ export const GAVANA_CANVAS_GUIDE_SOURCES = Object.freeze([
|
|
|
49
49
|
description: "Prepare media nodes, start only explicit generation, and preserve output lineage.",
|
|
50
50
|
keywords: Object.freeze(["generated","generation","image","video","output","asset","durable","lineage","placeholder","job"]),
|
|
51
51
|
order: 6,
|
|
52
|
-
markdown: "\n## Before generation\n\n- Read the destination canvas and relevant source nodes.\n- Use exact source `node:` or `asset:` handles.\n{{#local}}- Before image or video generation, call `model_list` for the required capability and pass its exact `model:` handle. A bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access that connection; do not ask the user to add a key again.\n{{/local}}{{#hosted}}- Before video generation, call `video_model_find` for the required capability and pass its exact `model:` handle. A bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access that connection; do not ask the user to add a key again.\n- Image generation has no model-discovery tool on this surface. Omit `model` and Gavana uses the account's configured default; pass `model` only when the user named an exact handle. Do not go looking for a tool that lists image models here.\n{{/hosted}}- For a standalone image request, pass every visual source in `references`.\n Use `{ \"handle\": \"node:...\", \"role\": \"identity\" }` when its\n responsibility is known; valid roles are `identity`, `construction`,\n `texture`, `fit`, and `style`. Do not flatten multi-reference work\n into prompt prose or omit a source during fallback.\n- Reuse existing Canvas `node:` or `asset:` handles directly. Do not download\n and re-upload a generated Canvas image merely to use it as the next\n generation's reference. State whether a style reference establishes the\n brand-world or typography/layout direction in the prompt.\n- A product page URL is source input, not prompt prose. Never paste it into a\n generation prompt, and never describe prompt-only output as the exact product.\n{{#hosted}}- When asked to generate one or more exact-product directions from a product\n page, call `product_photoshoot_generate` once. Supply one explicit direction\n object per requested output; the tool imports durable gallery references\n first and runs each direction as its own reference-guided image job. Use\n `product_reference_pack_import` only when the user wants a standalone import\n with no generation. Both take an optional `variantSelector`; when the page\n proves more than one variant, pass the one the user named rather than letting\n the import pick. If no durable references can be imported, say exact product\n fidelity is unverified.\n{{/hosted}}{{#local}}- This surface has no product-page importer. Ask the user for the images\n themselves, bring them in through the supported asset flow, and pass the\n resulting handles as references. Do not fetch the page and describe it in a\n prompt: that produces a generic product, not theirs.\n{{/local}}- Create an empty image or video target only through supported operations. Do not write media bytes, storage keys, or arbitrary output URLs into metadata.\n- Connect prompts, products, references, Lists, and frame inputs to their target with the correct direction and mode.\n\n## Paid execution\n\nGeneration is allowed only after explicit current-turn user intent. Start one run with one caller-stable idempotency key. When the user asks for N separate images, pass `count: N` (1-4) on one image call: Gavana reserves one image node per output and writes one image into each. Never encode N as a prompt describing N panels, frames, or a collage — that returns one composite image in one node. When you pass `targetNodeIds`, pass exactly one target node per output; a mismatch is rejected. For a multi-direction product photoshoot, never encode the directions as one shared prompt with `count > 1`; the product photoshoot tool owns one `count: 1` child per direction under one durable Recipe Run. Image tools return durable queued progress by default; report that progress immediately. {{#local}}Do not automatically call `run_wait`; call it only when the current user explicitly needs the completed asset in this same interaction.{{/local}}{{#hosted}}Do not block on the result: leave `wait` false and let the call return its queued run. Set `wait` true only when the current user explicitly needs the completed asset in this same interaction.{{/hosted}} Otherwise, use the returned exact Canvas URL for navigation, but treat `run_get` as the authoritative status; Canvas activity is a presentation surface and may lag. Do not start another run while one is pending. A terminal failure must be reported without automatic retry.\n\n## Sizes, formats, and limits\n\nThere is no size discovery tool. What holds today:\n\n- `size` accepts an exact `WIDTHxHEIGHT` or an aspect shorthand like `4:5`.\n gpt-image models render `1024x1024`, `1024x1536`, and `1536x1024`; anything\n else resolves to the closest of those. The run's `settings` reports both\n `requestedSize` and `effectiveSize`, and a resolved request also returns a\n `parameterAdjustments` entry — read them before telling the person what they\n got, and never assume the size you asked for is the size that was made.\n- Imports accept PNG, JPEG, WebP, and GIF up to 50 MB. A larger or undecodable\n source is refused without writing anything; that refusal will not succeed on\n retry, so shrink the image or link a smaller rendition instead.\n\n## Completion\n\n`run_get` is what you call while a run is going, and it answers about state, not pixels. Its response always links the finished image; {{#hosted}}pass `includeImage: true` {{/hosted}}{{#local}}read the linked asset {{/local}}only when a person is about to look at the picture, because inlining one costs a couple of megabytes on every read. A run that is still going also dates its own answer: `timing.observedAt` is when the record was read, `timing.elapsedMs` runs off the wall clock, and `timing.stale` with `timing.staleNote` means nothing has written to the run in a long while — the work may already be finished, so read the destination canvas before starting anything new.\n\nDo not claim a generated image is durable until the result returns a target `node:`, durable `asset:`, and the final canvas read shows server-owned media fields. `run_get` is the authoritative status when a user asks whether a run is done; a queued Canvas activity row is not evidence that the provider never started. A video Job may return a protected download without materializing a native video node; report exactly what the server returned and do not invent durability.\n\nKeep generated output spatially near its input stage and connected to its source, prompt, List, or workflow. After finalization, run `canvas_validate` and read `completionReview`: it reports overlap, full-frame Section containment, reference lineage, and delivery state. Do not claim Done while it says `doneClaimAllowed: false`, including when delivery is pending, failed, or non-durable. Render the Canvas for visual inspection when the request includes a campaign, poster, banner, or multi-direction composition. Reference provenance is not a user workflow state and never requires a Keep action. Never create another paid provider call automatically.\n",
|
|
52
|
+
markdown: "\n## Before generation\n\n- Read the destination canvas and relevant source nodes.\n- Use exact source `node:` or `asset:` handles.\n{{#local}}- Before image or video generation, call `model_list` for the required capability and pass its exact `model:` handle. A bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access that connection; do not ask the user to add a key again.\n{{/local}}{{#hosted}}- Before video generation, call `video_model_find` for the required capability and pass its exact `model:` handle. A bare model name does not select a saved connection. If no matching model is returned, report that the agent account cannot access that connection; do not ask the user to add a key again.\n- Image generation has no model-discovery tool on this surface. Omit `model` and Gavana uses the account's configured default; pass `model` only when the user named an exact handle. Do not go looking for a tool that lists image models here.\n{{/hosted}}- For a standalone image request, pass every visual source in `references`.\n Use `{ \"handle\": \"node:...\", \"role\": \"identity\" }` when its\n responsibility is known; valid roles are `identity`, `construction`,\n `texture`, `fit`, and `style`. Do not flatten multi-reference work\n into prompt prose or omit a source during fallback.\n- Reuse existing Canvas `node:` or `asset:` handles directly. Do not download\n and re-upload a generated Canvas image merely to use it as the next\n generation's reference. State whether a style reference establishes the\n brand-world or typography/layout direction in the prompt.\n- A product page URL is source input, not prompt prose. Never paste it into a\n generation prompt, and never describe prompt-only output as the exact product.\n{{#hosted}}- When asked to generate one or more exact-product directions from a product\n page, call `product_photoshoot_generate` once. Supply one explicit direction\n object per requested output; the tool imports durable gallery references\n first and runs each direction as its own reference-guided image job. Use\n `product_reference_pack_import` only when the user wants a standalone import\n with no generation. Both take an optional `variantSelector`; when the page\n proves more than one variant, pass the one the user named rather than letting\n the import pick. If no durable references can be imported, say exact product\n fidelity is unverified.\n- When the user supplied the product image themselves and there is no product\n page, call `product_photoshoot_generate` with `references` set to the durable\n `node:` handles already on the canvas, one direction object per requested\n output. It verifies those nodes before any paid work and stops when one is\n not a durable image. Save an asset to the canvas first: a workflow input\n binds to a node, not to an `asset:` handle. This is the route for several\n distinct directions from one uploaded photo; one image call with `count > 1`\n is not.\n{{/hosted}}{{#local}}- This surface has no product-page importer. Ask the user for the images\n themselves, bring them in through the supported asset flow, and pass the\n resulting handles as references. Do not fetch the page and describe it in a\n prompt: that produces a generic product, not theirs.\n{{/local}}- Create an empty image or video target only through supported operations. Do not write media bytes, storage keys, or arbitrary output URLs into metadata.\n- Connect prompts, products, references, Lists, and frame inputs to their target with the correct direction and mode.\n\n## Paid execution\n\nGeneration is allowed only after explicit current-turn user intent. Start one run with one caller-stable idempotency key. When the user asks for N separate images, pass `count: N` (1-4) on one image call: Gavana reserves one image node per output and writes one image into each. Never encode N as a prompt describing N panels, frames, or a collage — that returns one composite image in one node. When you pass `targetNodeIds`, pass exactly one target node per output; a mismatch is rejected. For a multi-direction product photoshoot, never encode the directions as one shared prompt with `count > 1`; the product photoshoot tool owns one `count: 1` child per direction under one durable Recipe Run. Image tools return durable queued progress by default; report that progress immediately. {{#local}}Do not automatically call `run_wait`; call it only when the current user explicitly needs the completed asset in this same interaction.{{/local}}{{#hosted}}Do not block on the result: leave `wait` false and let the call return its queued run. Set `wait` true only when the current user explicitly needs the completed asset in this same interaction.{{/hosted}} Otherwise, use the returned exact Canvas URL for navigation, but treat `run_get` as the authoritative status; Canvas activity is a presentation surface and may lag. Do not start another run while one is pending. A terminal failure must be reported without automatic retry.\n\n## Sizes, formats, and limits\n\nThere is no size discovery tool. What holds today:\n\n- `size` accepts an exact `WIDTHxHEIGHT` or an aspect shorthand like `4:5`.\n gpt-image models render `1024x1024`, `1024x1536`, and `1536x1024`; anything\n else resolves to the closest of those. The run's `settings` reports both\n `requestedSize` and `effectiveSize`, and a resolved request also returns a\n `parameterAdjustments` entry — read them before telling the person what they\n got, and never assume the size you asked for is the size that was made.\n- Imports accept PNG, JPEG, WebP, and GIF up to 50 MB. A larger or undecodable\n source is refused without writing anything; that refusal will not succeed on\n retry, so shrink the image or link a smaller rendition instead.\n\n## Completion\n\n`run_get` is what you call while a run is going, and it answers about state, not pixels. Its response always links the finished image; {{#hosted}}pass `includeImage: true` {{/hosted}}{{#local}}read the linked asset {{/local}}only when a person is about to look at the picture, because inlining one costs a couple of megabytes on every read. A run that is still going also dates its own answer: `timing.observedAt` is when the record was read, `timing.elapsedMs` runs off the wall clock, and `timing.stale` with `timing.staleNote` means nothing has written to the run in a long while — the work may already be finished, so read the destination canvas before starting anything new.\n\nDo not claim a generated image is durable until the result returns a target `node:`, durable `asset:`, and the final canvas read shows server-owned media fields. `run_get` is the authoritative status when a user asks whether a run is done; a queued Canvas activity row is not evidence that the provider never started. A video Job may return a protected download without materializing a native video node; report exactly what the server returned and do not invent durability.\n\nKeep generated output spatially near its input stage and connected to its source, prompt, List, or workflow. After finalization, run `canvas_validate` and read `completionReview`: it reports overlap, full-frame Section containment, reference lineage, and delivery state. Do not claim Done while it says `doneClaimAllowed: false`, including when delivery is pending, failed, or non-durable. Render the Canvas for visual inspection when the request includes a campaign, poster, banner, or multi-direction composition. Reference provenance is not a user workflow state and never requires a Keep action. Never create another paid provider call automatically.\n",
|
|
53
53
|
}),
|
|
54
54
|
Object.freeze({
|
|
55
55
|
id: "existing-canvases",
|
package/src/runner.mjs
CHANGED
|
@@ -9,7 +9,7 @@ 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
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"]);
|
|
@@ -643,7 +643,7 @@ async function runAuthCommand(action, _args, options, env, runtime) {
|
|
|
643
643
|
if (!token) {
|
|
644
644
|
const login = runtime.oauthLogin
|
|
645
645
|
? await runtime.oauthLogin({ baseUrl, readOnly: options["read-only"] === true, profile: profile || current.profile })
|
|
646
|
-
: 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, timeoutMs: secondsOption(options.timeout, OAUTH_LOGIN_DEFAULT_TIMEOUT_MS / 1000) * 1000, env, runtime });
|
|
647
647
|
token = login.accessToken;
|
|
648
648
|
loginMethod = "browser";
|
|
649
649
|
}
|
|
@@ -738,7 +738,10 @@ async function revokeBrowserOAuthToken(baseUrl, token, fetchImpl) {
|
|
|
738
738
|
}
|
|
739
739
|
}
|
|
740
740
|
|
|
741
|
-
export
|
|
741
|
+
export const OAUTH_LOGIN_DEFAULT_TIMEOUT_MS = 180_000;
|
|
742
|
+
|
|
743
|
+
export async function performBrowserOAuthLogin({ baseUrl, readOnly = false, noBrowser = false, timeoutMs, env = process.env, runtime = {} }) {
|
|
744
|
+
const waitMs = timeoutMs || runtime.oauthTimeoutMs || OAUTH_LOGIN_DEFAULT_TIMEOUT_MS;
|
|
742
745
|
const fetchImpl = runtime.fetchImpl || globalThis.fetch;
|
|
743
746
|
if (typeof fetchImpl !== "function") throw usageError("This runtime does not provide fetch for browser login.");
|
|
744
747
|
const origin = new URL(createCanvasAgentClient({ token: "configuration-validation", baseUrl, fetchImpl }).baseUrl).origin;
|
|
@@ -748,11 +751,17 @@ export async function performBrowserOAuthLogin({ baseUrl, readOnly = false, noBr
|
|
|
748
751
|
const callback = runtime.createOAuthCallbackServer ? await runtime.createOAuthCallbackServer(state) : await createOAuthCallbackServer(state);
|
|
749
752
|
try {
|
|
750
753
|
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";
|
|
754
|
+
// Registering under a stable install id is what makes this login a
|
|
755
|
+
// reconnect: Gavana returns the client id this machine already had and
|
|
756
|
+
// replaces the delegation it issued last time, instead of stacking a
|
|
757
|
+
// new one behind a client it has never seen.
|
|
758
|
+
const softwareId = runtime.installId || (await readOAuthInstallId(env));
|
|
751
759
|
const registrationResponse = await fetchImpl(`${origin}/oauth/register`, {
|
|
752
760
|
method: "POST",
|
|
753
761
|
headers: { "Content-Type": "application/json", Accept: "application/json" },
|
|
754
762
|
body: JSON.stringify({
|
|
755
763
|
client_name: "Gavana CLI",
|
|
764
|
+
software_id: softwareId,
|
|
756
765
|
redirect_uris: [callback.redirectUri],
|
|
757
766
|
token_endpoint_auth_method: "none",
|
|
758
767
|
grant_types: ["authorization_code"],
|
|
@@ -774,9 +783,37 @@ export async function performBrowserOAuthLogin({ baseUrl, readOnly = false, noBr
|
|
|
774
783
|
scope: scopes,
|
|
775
784
|
state,
|
|
776
785
|
}).toString();
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
786
|
+
// Whoever reads this line acts on it, and under a coding agent that
|
|
787
|
+
// reader is the agent. Reporting instead of ordering was not enough:
|
|
788
|
+
// "if no window opens, this is the URL" states a condition the reader
|
|
789
|
+
// cannot evaluate — it never sees the window — so it assumes the worst
|
|
790
|
+
// and opens the URL that is already open, and one login costs the user
|
|
791
|
+
// two tabs on the same authorization request. The prohibition has to be
|
|
792
|
+
// said outright, and only the branch where nothing opened may ask for
|
|
793
|
+
// the URL to be opened.
|
|
794
|
+
//
|
|
795
|
+
// The wait is announced because the caller has to survive it. An agent
|
|
796
|
+
// running this as a command sizes its own timeout, and every default
|
|
797
|
+
// worth guessing at is shorter than this one.
|
|
798
|
+
const stderrStream = runtime.stderr || process.stderr;
|
|
799
|
+
const askToOpen = () => stderrStream.write(`Open this URL to connect Gavana:\n${authorize}\n`);
|
|
800
|
+
const announceWait = () => stderrStream.write(`This waits up to ${Math.round(waitMs / 1000)} seconds for the sign-in to finish.\n`);
|
|
801
|
+
if (noBrowser) askToOpen();
|
|
802
|
+
else {
|
|
803
|
+
let opened = true;
|
|
804
|
+
try {
|
|
805
|
+
await (runtime.openBrowser ? runtime.openBrowser(authorize.toString()) : openBrowser(authorize.toString()));
|
|
806
|
+
} catch {
|
|
807
|
+
// No window came up, so a prohibition would strand the user on a
|
|
808
|
+
// page nobody opened. A machine with no browser is the one case
|
|
809
|
+
// where asking is the right instruction.
|
|
810
|
+
opened = false;
|
|
811
|
+
}
|
|
812
|
+
if (opened) stderrStream.write(`Your browser is opening the Gavana sign-in page. Do not open it again — this URL is already open, and is printed only so you can show it to me:\n${authorize}\n`);
|
|
813
|
+
else askToOpen();
|
|
814
|
+
}
|
|
815
|
+
announceWait();
|
|
816
|
+
const { code } = await callback.wait(waitMs);
|
|
780
817
|
const tokenResponse = await fetchImpl(`${origin}/oauth/token`, {
|
|
781
818
|
method: "POST",
|
|
782
819
|
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
@@ -837,7 +874,7 @@ async function createOAuthCallbackServer(expectedState) {
|
|
|
837
874
|
Promise.race([
|
|
838
875
|
result,
|
|
839
876
|
new Promise((_, rejectTimeout) => {
|
|
840
|
-
const timer = setTimeout(() => rejectTimeout(usageError("Gavana browser login timed out. Run auth login again.")), timeoutMs);
|
|
877
|
+
const timer = setTimeout(() => rejectTimeout(usageError("Gavana browser login timed out. The page that was open is now dead. Run auth login again, use the new URL it prints, and pass --timeout SECONDS if you need longer than this.")), timeoutMs);
|
|
841
878
|
timer.unref?.();
|
|
842
879
|
}),
|
|
843
880
|
]),
|
|
@@ -944,7 +981,20 @@ async function runMcpCommand(action, args, options, baseUrl, runtime) {
|
|
|
944
981
|
if (action === "config" || !definition.command) return definition;
|
|
945
982
|
const execute = runtime.execFile || execFile;
|
|
946
983
|
await execute(definition.command, definition.args);
|
|
947
|
-
|
|
984
|
+
// Registering is not signing in, and `installed: true` was the last thing
|
|
985
|
+
// this command said. Agents read that as the end of the job and told people
|
|
986
|
+
// their tools were ready while the server sat unauthenticated. The steps
|
|
987
|
+
// that remain are known here, so they ship with the result.
|
|
988
|
+
return {
|
|
989
|
+
...definition,
|
|
990
|
+
installed: true,
|
|
991
|
+
authenticated: false,
|
|
992
|
+
nextSteps: [
|
|
993
|
+
`Restart your client so it loads the ${definition.serverName} server.`,
|
|
994
|
+
`Sign this server in from inside your client — it has its own login, separate from the CLI's. In Claude Code: /mcp, choose ${definition.serverName}.`,
|
|
995
|
+
"Confirm with a read-only call, such as listing canvases. A status line saying connected is not proof.",
|
|
996
|
+
],
|
|
997
|
+
};
|
|
948
998
|
}
|
|
949
999
|
|
|
950
1000
|
function completionScript(shell) {
|
package/src/version.mjs
CHANGED