@gavana.ai/cli 0.3.1 → 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 +44 -0
- package/bin/gavana.mjs +0 -0
- package/package.json +1 -1
- package/src/canvas-agent-validation.mjs +77 -2
- package/src/commands.mjs +2 -2
- package/src/runner.mjs +51 -7
- package/src/version.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
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
|
+
|
|
3
47
|
## 0.3.1
|
|
4
48
|
|
|
5
49
|
### Fixed
|
package/bin/gavana.mjs
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -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/runner.mjs
CHANGED
|
@@ -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, env, 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;
|
|
@@ -780,9 +783,37 @@ export async function performBrowserOAuthLogin({ baseUrl, readOnly = false, noBr
|
|
|
780
783
|
scope: scopes,
|
|
781
784
|
state,
|
|
782
785
|
}).toString();
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
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);
|
|
786
817
|
const tokenResponse = await fetchImpl(`${origin}/oauth/token`, {
|
|
787
818
|
method: "POST",
|
|
788
819
|
headers: { "Content-Type": "application/x-www-form-urlencoded", Accept: "application/json" },
|
|
@@ -843,7 +874,7 @@ async function createOAuthCallbackServer(expectedState) {
|
|
|
843
874
|
Promise.race([
|
|
844
875
|
result,
|
|
845
876
|
new Promise((_, rejectTimeout) => {
|
|
846
|
-
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);
|
|
847
878
|
timer.unref?.();
|
|
848
879
|
}),
|
|
849
880
|
]),
|
|
@@ -950,7 +981,20 @@ async function runMcpCommand(action, args, options, baseUrl, runtime) {
|
|
|
950
981
|
if (action === "config" || !definition.command) return definition;
|
|
951
982
|
const execute = runtime.execFile || execFile;
|
|
952
983
|
await execute(definition.command, definition.args);
|
|
953
|
-
|
|
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
|
+
};
|
|
954
998
|
}
|
|
955
999
|
|
|
956
1000
|
function completionScript(shell) {
|
package/src/version.mjs
CHANGED