@gavana.ai/cli 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +54 -0
- package/LICENSE.md +7 -0
- package/README.md +237 -0
- package/bin/craftboard.mjs +5 -0
- package/bin/gavana.mjs +5 -0
- package/guides/connections.md +35 -0
- package/guides/examples-common-mistakes.md +29 -0
- package/guides/existing-canvases.md +19 -0
- package/guides/generated-assets.md +29 -0
- package/guides/getting-started.md +26 -0
- package/guides/notes-text-sections.md +44 -0
- package/guides/paid-action-safety.md +22 -0
- package/guides/prompt-lists.md +20 -0
- package/guides/sections-layout.md +43 -0
- package/guides/validation-recovery.md +33 -0
- package/package.json +44 -0
- package/src/canvas-agent-guide.mjs +133 -0
- package/src/canvas-agent-validation.mjs +554 -0
- package/src/canvas-layout.mjs +287 -0
- package/src/capabilities.mjs +61 -0
- package/src/client.mjs +1141 -0
- package/src/commands.mjs +259 -0
- package/src/config.mjs +197 -0
- package/src/guide-sources.mjs +86 -0
- package/src/runner.mjs +1968 -0
- package/src/tools/action_get.mjs +16 -0
- package/src/tools/action_list.mjs +21 -0
- package/src/tools/action_run.mjs +60 -0
- package/src/tools/agent_canvas_get.mjs +15 -0
- package/src/tools/asset_get.mjs +16 -0
- package/src/tools/asset_list.mjs +17 -0
- package/src/tools/asset_upload.mjs +24 -0
- package/src/tools/campaign_cancel.mjs +16 -0
- package/src/tools/campaign_get.mjs +16 -0
- package/src/tools/campaign_plan.mjs +31 -0
- package/src/tools/campaign_review.mjs +24 -0
- package/src/tools/campaign_start.mjs +19 -0
- package/src/tools/canvas_apply_batch.mjs +35 -0
- package/src/tools/canvas_create.mjs +15 -0
- package/src/tools/canvas_get.mjs +16 -0
- package/src/tools/canvas_list.mjs +17 -0
- package/src/tools/canvas_render.mjs +34 -0
- package/src/tools/canvas_validate.mjs +34 -0
- package/src/tools/connection_create.mjs +38 -0
- package/src/tools/connection_delete.mjs +31 -0
- package/src/tools/definitions.mjs +111 -0
- package/src/tools/guide_get.mjs +16 -0
- package/src/tools/guide_search.mjs +16 -0
- package/src/tools/helpers.mjs +66 -0
- package/src/tools/image_edit.mjs +8 -0
- package/src/tools/image_generate.mjs +8 -0
- package/src/tools/image_tool.mjs +56 -0
- package/src/tools/image_variations.mjs +8 -0
- package/src/tools/job_cancel.mjs +16 -0
- package/src/tools/job_get.mjs +17 -0
- package/src/tools/job_wait.mjs +18 -0
- package/src/tools/model_get.mjs +16 -0
- package/src/tools/model_list.mjs +23 -0
- package/src/tools/node_create.mjs +36 -0
- package/src/tools/node_delete.mjs +31 -0
- package/src/tools/node_get.mjs +16 -0
- package/src/tools/node_move.mjs +37 -0
- package/src/tools/node_resize.mjs +37 -0
- package/src/tools/node_update.mjs +36 -0
- package/src/tools/progress.mjs +101 -0
- package/src/tools/provider_list.mjs +17 -0
- package/src/tools/recipe_fork.mjs +32 -0
- package/src/tools/recipe_get.mjs +19 -0
- package/src/tools/recipe_run.mjs +61 -0
- package/src/tools/recipe_search.mjs +17 -0
- package/src/tools/registry.mjs +550 -0
- package/src/tools/run_cancel.mjs +16 -0
- package/src/tools/run_get.mjs +17 -0
- package/src/tools/run_wait.mjs +18 -0
- package/src/tools/schemas.mjs +165 -0
- package/src/tools/video_generate.mjs +37 -0
- package/src/version.mjs +12 -0
package/src/commands.mjs
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// The Gavana CLI command table.
|
|
2
|
+
//
|
|
3
|
+
// One declaration of what commands exist. Before this, the same knowledge lived
|
|
4
|
+
// twice: as nested group/action comparisons inside executeCommand, and again as a
|
|
5
|
+
// hand-maintained usage block inside helpText. The two drifted, and nothing failed
|
|
6
|
+
// when they did — a renamed command kept a stale help line, and a new command
|
|
7
|
+
// shipped undocumented.
|
|
8
|
+
//
|
|
9
|
+
// Help text and shell completions are generated from this array, and runner.mjs
|
|
10
|
+
// asks gavanaCommandIsKnown before dispatching anything. So this is a gate, not a
|
|
11
|
+
// description: a command absent from here is unreachable, whatever the dispatch
|
|
12
|
+
// branches happen to look like. The one exception is a freeformAction group, where
|
|
13
|
+
// the second positional is data and only the group name can be checked.
|
|
14
|
+
//
|
|
15
|
+
// Handler bodies deliberately stay in runner.mjs. They reach into roughly seventy
|
|
16
|
+
// helpers in that file, so moving them here would drag most of the module across a
|
|
17
|
+
// module boundary for no gain. What matters is that the catalog has one home; the
|
|
18
|
+
// drift test in scripts/gavana-cli-command-table.test.mjs holds runner's dispatch
|
|
19
|
+
// to it.
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {object} GavanaCliCommand
|
|
23
|
+
* @property {string} group Command group, e.g. "canvas".
|
|
24
|
+
* @property {string} action Sub-command, or "" for a group that takes none.
|
|
25
|
+
* @property {string[]} usage Usage lines exactly as the root help prints them.
|
|
26
|
+
* @property {string[]} [notesBefore] Prose printed above this command in root help.
|
|
27
|
+
* @property {boolean} [hidden] Dispatchable but never advertised anywhere: a
|
|
28
|
+
* retired surface or a compatibility alias. Absent from help AND completions.
|
|
29
|
+
* @property {string} [documentedBy] A real, supported command whose usage line
|
|
30
|
+
* lives on another entry (`config get` rides `config list | get | use`). It is
|
|
31
|
+
* advertised by completions, it just does not print a second usage line. The
|
|
32
|
+
* value names the group whose help block covers it, and a test asserts that
|
|
33
|
+
* block really does show the invocation.
|
|
34
|
+
* @property {boolean} [freeformAction] The second positional is data, not a
|
|
35
|
+
* sub-command: `gavana api GET /canvases` passes GET as the HTTP method. Such a
|
|
36
|
+
* group accepts any action, so the dispatch guard cannot check it.
|
|
37
|
+
*/
|
|
38
|
+
|
|
39
|
+
/** @type {GavanaCliCommand[]} */
|
|
40
|
+
export const GAVANA_CLI_COMMANDS = Object.freeze([
|
|
41
|
+
{
|
|
42
|
+
group: "auth",
|
|
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]"],
|
|
46
|
+
},
|
|
47
|
+
{ group: "auth", action: "status", usage: ["gavana auth status"], groupUsage: ["gavana auth status [--profile NAME]"] },
|
|
48
|
+
{ group: "config", action: "list", usage: ["gavana config list | get [PROFILE] | use PROFILE"], groupUsage: ["gavana config list"] },
|
|
49
|
+
{ group: "doctor", action: "", usage: ["gavana doctor [--profile NAME]"] },
|
|
50
|
+
{
|
|
51
|
+
group: "api",
|
|
52
|
+
action: "",
|
|
53
|
+
usage: ["gavana api GET /canvases --field limit=10"],
|
|
54
|
+
groupUsage: ["gavana api GET /canvases --field limit=10", `gavana api POST /canvases --json '{"title":"Concepts"}'`],
|
|
55
|
+
freeformAction: true,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
group: "mcp",
|
|
59
|
+
action: "install",
|
|
60
|
+
usage: ["gavana mcp install codex|claude [--read-only]"],
|
|
61
|
+
groupUsage: ["gavana mcp install codex [--read-only]", "gavana mcp install claude [--read-only]"],
|
|
62
|
+
},
|
|
63
|
+
{ group: "mcp", action: "config", usage: ["gavana mcp config cursor|chatgpt|local [--read-only]"] },
|
|
64
|
+
{ group: "completion", action: "zsh", usage: ["gavana completion zsh|bash|fish"], groupUsage: ["gavana completion zsh"] },
|
|
65
|
+
{ group: "version", action: "", usage: ["gavana version"] },
|
|
66
|
+
{ group: "capabilities", action: "", usage: ["gavana capabilities"] },
|
|
67
|
+
{
|
|
68
|
+
group: "recipe",
|
|
69
|
+
action: "search",
|
|
70
|
+
usage: ["gavana recipe search [query] [--limit N] [--cursor CURSOR]"],
|
|
71
|
+
notesBefore: [
|
|
72
|
+
"Recipe commands are retained API compatibility while the Recipe UI is unavailable in production.",
|
|
73
|
+
"They never start image work automatically; recipe_run is the separate explicit paid action.",
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
{ group: "recipe", action: "get", usage: ["gavana recipe get recipe:<id> [--version VERSION]"] },
|
|
77
|
+
{ group: "recipe", action: "fork", usage: ["gavana recipe fork recipe:<id> --canvas canvas:<id> --idempotency-key KEY [--x X --y Y]"] },
|
|
78
|
+
{
|
|
79
|
+
group: "recipe",
|
|
80
|
+
action: "run",
|
|
81
|
+
usage: ["gavana recipe run recipe:<id> --input key=value --destination canvas:<id>", "gavana recipe run recipe:social-creative-angles --input offer-brief=@brief.md --destination agent-canvas"],
|
|
82
|
+
groupUsage: [
|
|
83
|
+
"gavana recipe run recipe:<id> --input KEY=VALUE --destination agent-canvas|new-canvas|canvas:<id>",
|
|
84
|
+
"gavana recipe run recipe:social-creative-angles --input offer-brief=@brief.md --destination agent-canvas",
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
{ group: "canvas", action: "list", usage: ["gavana canvas list [--limit N] [--cursor CURSOR]"] },
|
|
88
|
+
{ group: "canvas", action: "agent", usage: ["gavana canvas agent"] },
|
|
89
|
+
{ group: "canvas", action: "create", usage: ["gavana canvas create --title \"New canvas\""] },
|
|
90
|
+
{ group: "canvas", action: "get", usage: ["gavana canvas get canvas:<owner>:<id>"] },
|
|
91
|
+
{ group: "canvas", action: "render", usage: ["gavana canvas render canvas:<id> --file canvas.svg"] },
|
|
92
|
+
{ group: "canvas", action: "apply", usage: ["gavana canvas apply canvas:<id> --file batch.json [--yes]"] },
|
|
93
|
+
{ group: "node", action: "get", usage: ["gavana node get canvas:<id> node:<id>"] },
|
|
94
|
+
{ group: "node", action: "create", usage: ["gavana node create canvas:<id> --type text --title \"Direction\" --prompt \"...\""] },
|
|
95
|
+
{
|
|
96
|
+
group: "node",
|
|
97
|
+
action: "update",
|
|
98
|
+
usage: ["gavana node update canvas:<id> node:<id> --title \"Approved\""],
|
|
99
|
+
groupUsage: ["gavana node update canvas:<id> node:<id> [--title VALUE --prompt VALUE]"],
|
|
100
|
+
},
|
|
101
|
+
{ group: "node", action: "move", usage: ["gavana node move canvas:<id> node:<id> --x 800 --y 120"] },
|
|
102
|
+
{ group: "node", action: "resize", usage: ["gavana node resize canvas:<id> node:<id> --width 420 --height 300"] },
|
|
103
|
+
{ group: "node", action: "delete", usage: ["gavana node delete canvas:<id> node:<id> --yes"] },
|
|
104
|
+
{ group: "connection", action: "list", usage: ["gavana connection list canvas:<id>"] },
|
|
105
|
+
{ group: "connection", action: "create", usage: ["gavana connection create canvas:<id> --from node:<id> --to node:<id>"] },
|
|
106
|
+
{ group: "connection", action: "delete", usage: ["gavana connection delete canvas:<id> connection:<id> --yes"] },
|
|
107
|
+
{ group: "asset", action: "list", usage: ["gavana asset list [--canvas canvas:<id>] [--limit N] [--cursor CURSOR]"] },
|
|
108
|
+
{ group: "asset", action: "get", usage: ["gavana asset get asset:<id>|asset:<ownerUid>:<id>"] },
|
|
109
|
+
{ group: "asset", action: "upload", usage: ["gavana asset upload path/to/image.png"] },
|
|
110
|
+
{ group: "provider", action: "list", usage: ["gavana provider list [--limit N] [--cursor CURSOR]"] },
|
|
111
|
+
{ group: "model", action: "list", usage: ["gavana model list [query] [--provider PROVIDER] [--capability image.generate]"] },
|
|
112
|
+
{ group: "model", action: "get", usage: ["gavana model get model:<id>"] },
|
|
113
|
+
{ group: "action", action: "list", usage: ["gavana action list [query] [--limit N] [--cursor CURSOR]"] },
|
|
114
|
+
{ group: "action", action: "get", usage: ["gavana action get action:<id>"] },
|
|
115
|
+
{
|
|
116
|
+
group: "action",
|
|
117
|
+
action: "run",
|
|
118
|
+
usage: ["gavana action run action:resize --input node:<id> --destination canvas:<id> --width 1080 --height 1350", "gavana action run action:side-by-side-composite --input node:<id> --input asset:<id> --destination canvas:<id>"],
|
|
119
|
+
groupUsage: [
|
|
120
|
+
"gavana action run action:<id> --input VALUE --destination VALUE [--param NAME=VALUE]",
|
|
121
|
+
"gavana action run action:resize --input node:<id> --destination canvas:<id> --width 1080 --height 1350",
|
|
122
|
+
"gavana action run action:side-by-side-composite --input node:<id> --input asset:<id> --destination canvas:<id>",
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
{ group: "image", action: "generate", usage: ["gavana image generate --destination agent-canvas --prompt \"...\""] },
|
|
126
|
+
{ group: "image", action: "edit", usage: ["gavana image edit --destination canvas:<id> --reference path/to/image.png --prompt \"...\""] },
|
|
127
|
+
{ group: "image", action: "variations", usage: ["gavana image variations --destination new-canvas --canvas-title \"Variations\" --reference path/to/source.png --prompt \"...\""] },
|
|
128
|
+
{ group: "video", action: "generate", usage: ["gavana video generate --model model:<id> --prompt \"...\" --duration 15", "gavana video generate --model model:<id> --prompt \"...\" --first-frame path/to/start.png --no-wait", "gavana video generate --model model:<id> --prompt \"...\" --download output.mp4"] },
|
|
129
|
+
{ group: "video", action: "download", usage: ["gavana video download job:<id> --file output.mp4 [--yes]"] },
|
|
130
|
+
{ group: "job", action: "get", usage: ["gavana job get job:<id>"] },
|
|
131
|
+
{ group: "job", action: "wait", usage: ["gavana job wait job:<id> [--progress] [--output markdown]"] },
|
|
132
|
+
{ group: "job", action: "cancel", usage: ["gavana job cancel job:<id> --yes"] },
|
|
133
|
+
{ group: "run", action: "get", usage: ["gavana run get run:<id>"] },
|
|
134
|
+
{ group: "run", action: "wait", usage: ["gavana run wait run:<id> [--progress] [--output markdown]"] },
|
|
135
|
+
{ group: "run", action: "cancel", usage: ["gavana run cancel run:<id> --yes"] },
|
|
136
|
+
// Retired campaign surface. Still dispatched so an installed script fails with a
|
|
137
|
+
// clear message rather than "unknown command", but never advertised.
|
|
138
|
+
{ group: "campaign", action: "plan", usage: [], hidden: true },
|
|
139
|
+
{ group: "campaign", action: "start", usage: [], hidden: true },
|
|
140
|
+
{ group: "campaign", action: "get", usage: [], hidden: true },
|
|
141
|
+
{ group: "campaign", action: "review", usage: [], hidden: true },
|
|
142
|
+
{ group: "campaign", action: "cancel", usage: [], hidden: true },
|
|
143
|
+
// Accepted as an alias of `provider list` for older scripts.
|
|
144
|
+
{ group: "ai-connection", action: "list", usage: [], hidden: true },
|
|
145
|
+
// Group-only commands the root help lists without a sub-command.
|
|
146
|
+
{ group: "auth", action: "logout", usage: [], groupUsage: ["gavana auth logout [--profile NAME]"], documentedBy: "auth" },
|
|
147
|
+
// Bare `gavana config` behaves as `config list` and `gavana completion` as
|
|
148
|
+
// `completion zsh`. Both work, neither appears in any help text, so they are
|
|
149
|
+
// hidden rather than claiming a documentation home they do not have.
|
|
150
|
+
{ group: "config", action: "", usage: [], hidden: true },
|
|
151
|
+
{ group: "config", action: "get", usage: [], groupUsage: ["gavana config get [PROFILE]"], documentedBy: "config" },
|
|
152
|
+
// Undeclared until the command table went in: `config profiles` has always been
|
|
153
|
+
// an accepted alias of `config list`, and appeared in no help text.
|
|
154
|
+
{ group: "config", action: "profiles", usage: [], hidden: true },
|
|
155
|
+
{ group: "config", action: "use", usage: [], groupUsage: ["gavana config use PROFILE"], documentedBy: "config" },
|
|
156
|
+
{ group: "completion", action: "", usage: [], hidden: true },
|
|
157
|
+
{ group: "completion", action: "bash", usage: [], groupUsage: ["gavana completion bash"], documentedBy: "completion" },
|
|
158
|
+
{ group: "completion", action: "fish", usage: [], groupUsage: ["gavana completion fish"], documentedBy: "completion" },
|
|
159
|
+
]);
|
|
160
|
+
|
|
161
|
+
/** Every command key, in `group action` form (a group-only command is just the group). */
|
|
162
|
+
export function gavanaCommandKey(command) {
|
|
163
|
+
return [command.group, command.action].filter(Boolean).join(" ");
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Lookup by group and action. Undefined means the CLI does not have this command. */
|
|
167
|
+
export function gavanaCommand(group, action = "") {
|
|
168
|
+
return GAVANA_CLI_COMMANDS.find((command) => command.group === group && command.action === action);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Distinct groups, in the order the root help introduces them.
|
|
173
|
+
*
|
|
174
|
+
* A group whose every command is hidden is itself hidden, so the retired campaign
|
|
175
|
+
* surface and the ai-connection alias fall out of completions without a second
|
|
176
|
+
* hardcoded list to keep in step with this one.
|
|
177
|
+
*/
|
|
178
|
+
export function gavanaCommandGroups({ includeHidden = false } = {}) {
|
|
179
|
+
const groups = Array.from(new Set(GAVANA_CLI_COMMANDS.map((command) => command.group)));
|
|
180
|
+
if (includeHidden) return groups;
|
|
181
|
+
return groups.filter((group) => GAVANA_CLI_COMMANDS.some((command) => command.group === group && !command.hidden));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Actions under one group. Hidden ones are excluded by default: advertising an
|
|
186
|
+
* unadvertised alias in a shell completion is the same leak as printing it in help.
|
|
187
|
+
*/
|
|
188
|
+
export function gavanaCommandActions(group, { includeHidden = false } = {}) {
|
|
189
|
+
return GAVANA_CLI_COMMANDS.filter((command) => command.group === group && command.action && (includeHidden || !command.hidden)).map((command) => command.action);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The usage block of the root help, generated rather than maintained.
|
|
194
|
+
*
|
|
195
|
+
* Hidden commands are omitted here and only here: they stay dispatchable, and the
|
|
196
|
+
* drift test still sees them, but they are not advertised.
|
|
197
|
+
*/
|
|
198
|
+
export function gavanaCommandUsageLines() {
|
|
199
|
+
return GAVANA_CLI_COMMANDS.filter((command) => !command.hidden).flatMap((command) => [...(command.notesBefore || []), ...command.usage]);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The five groups whose `--help` page has its own title and closing note.
|
|
204
|
+
*
|
|
205
|
+
* Every other group gets the generic "Gavana <group> commands" page. These five
|
|
206
|
+
* carry something a usage line cannot: where the token is stored, that a bare
|
|
207
|
+
* --profile works everywhere, that remote clients use OAuth, that API paths are
|
|
208
|
+
* restricted. The usage lines themselves still come from the table.
|
|
209
|
+
*/
|
|
210
|
+
export const GAVANA_CLI_GROUP_HELP = Object.freeze({
|
|
211
|
+
auth: {
|
|
212
|
+
title: "Gavana authentication",
|
|
213
|
+
note: "Browser login is the default. On macOS, the token is stored in Keychain; custom config paths use a mode-0600 file.",
|
|
214
|
+
},
|
|
215
|
+
config: { title: "Gavana profiles", note: "Use --profile NAME on any command or set GAVANA_PROFILE." },
|
|
216
|
+
mcp: { title: "Gavana MCP setup", note: "Remote clients use browser OAuth. The read-only endpoint exposes no mutation or generation tools." },
|
|
217
|
+
api: { title: "Raw Gavana Canvas API", note: "Paths are restricted to /api/canvas-agent/v1." },
|
|
218
|
+
completion: { title: "Generate shell completion", note: "" },
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* The usage lines for one group's help page, optionally narrowed to one action.
|
|
223
|
+
*
|
|
224
|
+
* `groupUsage` overrides `usage` where the root help deliberately compresses
|
|
225
|
+
* several commands onto one line — `gavana config list | get [PROFILE] | use
|
|
226
|
+
* PROFILE` is right for a root summary and wrong for the config page, which has
|
|
227
|
+
* room to show each form. Everything else falls back to `usage`, so a command
|
|
228
|
+
* added to the table appears on its group page without a second edit. That
|
|
229
|
+
* fallback is the point: group help used to be an independent catalog, and it had
|
|
230
|
+
* drifted behind the root list.
|
|
231
|
+
*/
|
|
232
|
+
export function gavanaCommandGroupUsageLines(group, action = "") {
|
|
233
|
+
const entries = GAVANA_CLI_COMMANDS.filter((command) => command.group === group && !command.hidden);
|
|
234
|
+
// A freeform group takes data in the action slot, so narrowing by it is
|
|
235
|
+
// meaningless: `gavana api GET /canvases --help` would look up an action named
|
|
236
|
+
// GET, find none, and answer "Unknown api action: GET" for a command that is
|
|
237
|
+
// perfectly valid. The whole group's page is the right answer there.
|
|
238
|
+
const narrow = action && !entries.some((command) => command.freeformAction);
|
|
239
|
+
const selected = narrow ? entries.filter((command) => command.action === action) : entries;
|
|
240
|
+
return selected.flatMap((command) => command.groupUsage || command.usage);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Whether the CLI should accept this command at all.
|
|
245
|
+
*
|
|
246
|
+
* runner.mjs calls this before dispatching, which is what makes the table the
|
|
247
|
+
* catalog rather than a description of one. A source scanner could only ever
|
|
248
|
+
* detect drift after the fact, and only for the code shapes it happened to
|
|
249
|
+
* recognise; a runtime lookup makes an undeclared command impossible to reach.
|
|
250
|
+
*
|
|
251
|
+
* A freeformAction group takes data in the action position, so only its group is
|
|
252
|
+
* checked.
|
|
253
|
+
*/
|
|
254
|
+
export function gavanaCommandIsKnown(group, action = "") {
|
|
255
|
+
if (!group) return false;
|
|
256
|
+
const freeform = GAVANA_CLI_COMMANDS.find((command) => command.group === group && command.freeformAction);
|
|
257
|
+
if (freeform) return true;
|
|
258
|
+
return Boolean(gavanaCommand(group, action));
|
|
259
|
+
}
|
package/src/config.mjs
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { execFile as execFileCallback } from "node:child_process";
|
|
5
|
+
import { promisify } from "node:util";
|
|
6
|
+
|
|
7
|
+
const execFile = promisify(execFileCallback);
|
|
8
|
+
const KEYCHAIN_SERVICE = "ai.gavana.cli";
|
|
9
|
+
|
|
10
|
+
export async function readAgentConfig(env = process.env) {
|
|
11
|
+
const store = await readAgentConfigStore(env);
|
|
12
|
+
const profile = selectedProfileName(store, env);
|
|
13
|
+
const selected = store.profiles?.[profile] || {};
|
|
14
|
+
let token = selected.token || "";
|
|
15
|
+
if (!token && selected.credentialStore === "macos-keychain") token = await readMacKeychainCredential(profile);
|
|
16
|
+
return { ...store, ...selected, ...(token ? { token } : {}), profile };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function readAgentConfigMetadata(env = process.env) {
|
|
20
|
+
const store = await readAgentConfigStore(env);
|
|
21
|
+
const profile = selectedProfileName(store, env);
|
|
22
|
+
const selected = store.profiles?.[profile] || {};
|
|
23
|
+
return {
|
|
24
|
+
version: 1,
|
|
25
|
+
activeProfile: store.activeProfile,
|
|
26
|
+
profile,
|
|
27
|
+
baseUrl: selected.baseUrl || "",
|
|
28
|
+
credentialStore: selected.credentialStore || "",
|
|
29
|
+
authMode: selected.authMode || "",
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function readAgentConfigStore(env = process.env) {
|
|
34
|
+
for (const filePath of agentConfigFileCandidates(env)) {
|
|
35
|
+
try {
|
|
36
|
+
const parsed = JSON.parse(await fs.readFile(filePath, "utf8"));
|
|
37
|
+
return normalizeConfigStore(parsed);
|
|
38
|
+
} catch {
|
|
39
|
+
// New Gavana config wins. Fall back to the legacy Craftboard path
|
|
40
|
+
// only when no explicit config file was requested.
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return normalizeConfigStore({});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export async function writeAgentConfig(config, env = process.env) {
|
|
47
|
+
const current = await readAgentConfigStore(env);
|
|
48
|
+
const profile = cleanProfileName(config.profile || selectedProfileName(current, env));
|
|
49
|
+
let token = config.token || "";
|
|
50
|
+
let credentialStore = config.credentialStore || "";
|
|
51
|
+
if (token && shouldUseMacKeychain(env)) {
|
|
52
|
+
await writeMacKeychainCredential(profile, token);
|
|
53
|
+
token = "";
|
|
54
|
+
credentialStore = "macos-keychain";
|
|
55
|
+
}
|
|
56
|
+
const profileConfig = {
|
|
57
|
+
...(current.profiles?.[profile] || {}),
|
|
58
|
+
...(config.baseUrl ? { baseUrl: config.baseUrl } : {}),
|
|
59
|
+
...(token ? { token } : {}),
|
|
60
|
+
...(credentialStore ? { credentialStore } : {}),
|
|
61
|
+
...(config.authMode ? { authMode: config.authMode } : {}),
|
|
62
|
+
};
|
|
63
|
+
if (credentialStore === "macos-keychain") delete profileConfig.token;
|
|
64
|
+
const next = {
|
|
65
|
+
version: 1,
|
|
66
|
+
activeProfile: profile,
|
|
67
|
+
profiles: { ...(current.profiles || {}), [profile]: profileConfig },
|
|
68
|
+
};
|
|
69
|
+
return writeAgentConfigStore(next, env);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function writeAgentConfigStore(config, env = process.env) {
|
|
73
|
+
const filePath = agentConfigFilePath(env);
|
|
74
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
75
|
+
const temporary = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
76
|
+
const active = config.profiles?.[config.activeProfile] || {};
|
|
77
|
+
const compatible = {
|
|
78
|
+
...config,
|
|
79
|
+
...(active.baseUrl ? { baseUrl: active.baseUrl } : {}),
|
|
80
|
+
...(active.token ? { token: active.token } : {}),
|
|
81
|
+
};
|
|
82
|
+
await fs.writeFile(temporary, `${JSON.stringify(compatible, null, 2)}\n`, { mode: 0o600 });
|
|
83
|
+
await fs.rename(temporary, filePath);
|
|
84
|
+
await fs.chmod(filePath, 0o600);
|
|
85
|
+
return filePath;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export async function removeAgentConfig(env = process.env) {
|
|
89
|
+
const filePaths = agentConfigFileCandidates(env);
|
|
90
|
+
await Promise.all(filePaths.map((filePath) => fs.rm(filePath, { force: true })));
|
|
91
|
+
return filePaths[0];
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export async function removeAgentProfile(profile, env = process.env) {
|
|
95
|
+
const store = await readAgentConfigStore(env);
|
|
96
|
+
const name = cleanProfileName(profile || selectedProfileName(store, env));
|
|
97
|
+
if (!store.profiles?.[name]) return { configPath: agentConfigFilePath(env), profile: name, removed: false };
|
|
98
|
+
const profiles = { ...store.profiles };
|
|
99
|
+
if (profiles[name]?.credentialStore === "macos-keychain") await removeMacKeychainCredential(name);
|
|
100
|
+
delete profiles[name];
|
|
101
|
+
if (!Object.keys(profiles).length) {
|
|
102
|
+
await removeAgentConfig(env);
|
|
103
|
+
return { configPath: agentConfigFilePath(env), profile: name, removed: true };
|
|
104
|
+
}
|
|
105
|
+
const activeProfile = store.activeProfile === name ? Object.keys(profiles).sort()[0] : store.activeProfile;
|
|
106
|
+
await writeAgentConfigStore({ version: 1, activeProfile, profiles }, env);
|
|
107
|
+
return { configPath: agentConfigFilePath(env), profile: name, removed: true, activeProfile };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function setActiveAgentProfile(profile, env = process.env) {
|
|
111
|
+
const store = await readAgentConfigStore(env);
|
|
112
|
+
const name = cleanProfileName(profile);
|
|
113
|
+
if (!store.profiles?.[name]) throw configUsageError(`Unknown Gavana profile: ${name}`);
|
|
114
|
+
await writeAgentConfigStore({ ...store, version: 1, activeProfile: name }, env);
|
|
115
|
+
return { profile: name, configPath: agentConfigFilePath(env) };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export async function listAgentProfiles(env = process.env) {
|
|
119
|
+
const store = await readAgentConfigStore(env);
|
|
120
|
+
return {
|
|
121
|
+
activeProfile: selectedProfileName(store, env),
|
|
122
|
+
profiles: Object.entries(store.profiles || {})
|
|
123
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
124
|
+
.map(([name, value]) => ({
|
|
125
|
+
name,
|
|
126
|
+
baseUrl: value?.baseUrl || "",
|
|
127
|
+
active: name === selectedProfileName(store, env),
|
|
128
|
+
configured: Boolean(value?.baseUrl && (value?.token || value?.credentialStore === "macos-keychain")),
|
|
129
|
+
})),
|
|
130
|
+
configPath: agentConfigFilePath(env),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function agentConfigFilePath(env = process.env) {
|
|
135
|
+
if (env.GAVANA_AGENT_CONFIG_FILE) return path.resolve(env.GAVANA_AGENT_CONFIG_FILE);
|
|
136
|
+
if (env.CRAFTBOARD_AGENT_CONFIG_FILE) return path.resolve(env.CRAFTBOARD_AGENT_CONFIG_FILE);
|
|
137
|
+
const configHome = env.XDG_CONFIG_HOME || path.join(env.HOME || os.homedir(), ".config");
|
|
138
|
+
return path.join(configHome, "gavana", "agent.json");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function agentConfigFileCandidates(env) {
|
|
142
|
+
const preferred = agentConfigFilePath(env);
|
|
143
|
+
if (env.GAVANA_AGENT_CONFIG_FILE || env.CRAFTBOARD_AGENT_CONFIG_FILE) return [preferred];
|
|
144
|
+
const configHome = env.XDG_CONFIG_HOME || path.join(env.HOME || os.homedir(), ".config");
|
|
145
|
+
return [preferred, path.join(configHome, "craftboard", "agent.json")];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function normalizeConfigStore(value) {
|
|
149
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return { version: 1, activeProfile: "default", profiles: {} };
|
|
150
|
+
if (value.profiles && typeof value.profiles === "object" && !Array.isArray(value.profiles)) {
|
|
151
|
+
return { version: 1, activeProfile: cleanProfileName(value.activeProfile || "default"), profiles: value.profiles };
|
|
152
|
+
}
|
|
153
|
+
const legacy = {
|
|
154
|
+
...(typeof value.baseUrl === "string" && value.baseUrl ? { baseUrl: value.baseUrl } : {}),
|
|
155
|
+
...(typeof value.token === "string" && value.token ? { token: value.token } : {}),
|
|
156
|
+
};
|
|
157
|
+
return { version: 1, activeProfile: "default", profiles: Object.keys(legacy).length ? { default: legacy } : {} };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function selectedProfileName(store, env) {
|
|
161
|
+
return cleanProfileName(env.GAVANA_PROFILE || env.CRAFTBOARD_PROFILE || store.activeProfile || "default");
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function cleanProfileName(value) {
|
|
165
|
+
const name = String(value || "default").trim();
|
|
166
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$/.test(name)) throw configUsageError("Profile names must use 1-64 letters, numbers, dots, underscores, or dashes.");
|
|
167
|
+
return name;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function configUsageError(message) {
|
|
171
|
+
return Object.assign(new Error(message), { code: "usage" });
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function shouldUseMacKeychain(env) {
|
|
175
|
+
return process.platform === "darwin" && env.GAVANA_CLI_KEYCHAIN !== "false" && !env.GAVANA_AGENT_CONFIG_FILE && !env.CRAFTBOARD_AGENT_CONFIG_FILE && !env.XDG_CONFIG_HOME;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
async function writeMacKeychainCredential(profile, token) {
|
|
179
|
+
await execFile("security", ["add-generic-password", "-a", profile, "-s", KEYCHAIN_SERVICE, "-w", token, "-U"]);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async function readMacKeychainCredential(profile) {
|
|
183
|
+
try {
|
|
184
|
+
const { stdout } = await execFile("security", ["find-generic-password", "-a", profile, "-s", KEYCHAIN_SERVICE, "-w"]);
|
|
185
|
+
return stdout.trim();
|
|
186
|
+
} catch {
|
|
187
|
+
return "";
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function removeMacKeychainCredential(profile) {
|
|
192
|
+
try {
|
|
193
|
+
await execFile("security", ["delete-generic-password", "-a", profile, "-s", KEYCHAIN_SERVICE]);
|
|
194
|
+
} catch {
|
|
195
|
+
// Missing keychain items are already logged out.
|
|
196
|
+
}
|
|
197
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// Generated by scripts/generate-canvas-guides.mjs from packages/cli/guides/*.md.
|
|
2
|
+
// Do not edit. Edit the markdown and run: node scripts/generate-canvas-guides.mjs
|
|
3
|
+
|
|
4
|
+
/** Guide prose, ordered by frontmatter `order`. Order drives the guide index and search listings. */
|
|
5
|
+
export const GAVANA_CANVAS_GUIDE_SOURCES = Object.freeze([
|
|
6
|
+
Object.freeze({
|
|
7
|
+
id: "getting-started",
|
|
8
|
+
title: "Canvas Agent Workflow",
|
|
9
|
+
description: "The required inspect, guide, validate, edit, and review sequence for every canvas task.",
|
|
10
|
+
keywords: Object.freeze(["start","workflow","inspect","read","validate","edit","review","revision","idempotency"]),
|
|
11
|
+
order: 1,
|
|
12
|
+
markdown: "\n## Required sequence\n\n1. Identify one exact canvas handle. Never guess between candidates.\n2. Call `canvas_get` before reasoning about or changing an existing canvas.\n3. Read the guide topics relevant to the requested operation.\n4. Plan the smallest graph change that satisfies the request. Preserve unrelated nodes, connections, positions, metadata, and the user's current structure.\n5. Call `canvas_validate` with the proposed operations before a large, spatial, or destructive batch.\n6. Apply related changes atomically with `canvas_apply_batch`, the current `baseRevision`, and one caller-stable idempotency key.\n7. If a write returns revision conflict `409`, read the canvas again, preserve the newer user change, and retry the same intent. Reuse the same idempotency key only for the same payload.\n8. Call `canvas_validate` after editing. Report exact changed handles and unresolved warnings.\n\n## Non-negotiable safety\n\n- Building or preparing a workflow does not mean running it.\n- Never start image, video, Action, or Recipe generation unless the user explicitly requested that paid action in the current conversation.\n- Never automatically retry a failed paid action.\n- Never delete, disconnect, overwrite, or reorganize existing work unless the user explicitly requested that scope.\n- Generated media content is server-owned. Use generation or import tools; do not place bytes or arbitrary media URLs into node metadata.\n",
|
|
13
|
+
}),
|
|
14
|
+
Object.freeze({
|
|
15
|
+
id: "notes-text-sections",
|
|
16
|
+
title: "Notes, Text, and Sections",
|
|
17
|
+
description: "Choose the correct native canvas object for observations, durable copy, headings, and spatial groups.",
|
|
18
|
+
keywords: Object.freeze(["note","notes","sticky","text","heading","header","section","frame","content","annotation"]),
|
|
19
|
+
order: 2,
|
|
20
|
+
markdown: "\n## Object grammar\n\n- Sticky note: one short observation, idea, decision, question, or human annotation. Create a `sticky` node and put the readable value in `metadata.content`.\n- Text: a paragraph, prompt, brief, instruction, caption, or durable written output. Create a `text` node and put the body in `metadata.content`.\n- Section: a labeled spatial container and navigation landmark. Create a `text` node with `metadata.isSection: true`. Put the section title in `title`; keep `metadata.content` empty unless the user asked for a section description.\n- Heading: use the title of a real Section. Do not stretch an ordinary Text or Sticky node across the canvas to imitate a section header.\n\n## Section example\n\n```json\n{\n \"type\": \"node.create\",\n \"clientId\": \"research-section\",\n \"node\": {\n \"type\": \"text\",\n \"title\": \"Customer objections\",\n \"position\": { \"x\": 1200, \"y\": 200 },\n \"width\": 1040,\n \"height\": 720,\n \"metadata\": { \"isSection\": true }\n }\n}\n```\n\nPlace each child's complete frame inside the Section bounds; a centered but\noverflowing frame is not contained. A Section may overlap its children by\ndesign; ordinary nodes should not overlap one another. A child whose frame\ncrosses its Section boundary receives a `section_content_overflow` finding.\n\n## Stored membership\n\nMembership is stored: the server keeps `metadata.sectionId` on every node\nwhose complete frame fits inside a Section, recomputing it after each batch on\ncreate, move, resize, and delete. When Sections nest or overlap, the smallest\ncontaining Section wins. Do not set `sectionId` yourself — geometry is the\nsource of truth and caller-supplied values are corrected.\n",
|
|
21
|
+
}),
|
|
22
|
+
Object.freeze({
|
|
23
|
+
id: "sections-layout",
|
|
24
|
+
title: "Sections and Layout",
|
|
25
|
+
description: "Place native objects in readable groups without hiding or rearranging existing work.",
|
|
26
|
+
keywords: Object.freeze(["section","layout","position","overlap","spacing","grid","right","below","contain","navigation"]),
|
|
27
|
+
order: 3,
|
|
28
|
+
markdown: "\n## Layout rules\n\n- Treat the current canvas as user-owned. Preserve existing coordinates unless reorganization was explicitly requested.\n- For additions to an existing canvas, compute its visible bounding box and place the new Section to the right with at least 160 canvas units of outer spacing. If right-side placement would make the canvas excessively wide, place it below with the same spacing.\n- Use 48 units of inner Section padding, 32 units between sibling nodes, and at least 80 units between major stages.\n- Keep workflow direction consistent, normally left to right. Keep inputs before transformations and outputs after them.\n- Use compact rows or columns. Avoid extremely long, thin canvases that become unreadable at Fit Canvas.\n- For 2-8 generated image outputs, reserve each requested final aspect frame before work begins and pack the complete result cluster as a grid. The usual four-image photoshoot is a 2x2 grid; do not stack it as a tall output column that can overlap when images finalize.\n- Size Sections after their contents. Do not use Section overlap as a substitute for node placement.\n- Run `canvas_validate` before and after a multi-node layout change.\n\n## One task = one Section\n\nGroup each task's output in a titled Section sized to its contents plus 48\nunits of padding. Workflow creation and multi-output image generation wrap\ntheir clusters in a Section automatically (multi-output wrapping applies to\nautomatic placement; passing explicit target coordinates opts out and leaves\nplacement fully caller-controlled); do the same for hand-built\nclusters. Agent-created nodes left outside every Section raise an info-level\n`unsectioned_node` finding. Membership is stored server-side as\n`metadata.sectionId`, recomputed from complete-frame geometry after every\nbatch. Agent-created Sections receive an owned auto-fit contract; after a\ngenerated child changes size, Gavana refits only that still-owned Section to\nthe full child bounds plus 48 units. Do not use this as permission to move or\nresize a user-controlled Section.\n\nFor a newly proposed agent cluster, ordinary-node overlap and full-frame\nSection overflow are write-blocking errors. On an existing Canvas, historical\nuser-created overlap or overflow remains advisory so an unrelated scoped edit\ncan still proceed. Agent-owned task findings still block completion review\nuntil they are corrected.\n\n## Existing canvas rule\n\nWhen the user says \"add\", do not interpret it as \"reorganize\". New work should be distinguishable, spatially contained, and reversible without moving unrelated content.\n",
|
|
29
|
+
}),
|
|
30
|
+
Object.freeze({
|
|
31
|
+
id: "connections",
|
|
32
|
+
title: "Connections",
|
|
33
|
+
description: "Create directed, persistent relationships with valid endpoint semantics.",
|
|
34
|
+
keywords: Object.freeze(["connection","connect","edge","direction","input","output","reference","list","first-frame","last-frame","prompt"]),
|
|
35
|
+
order: 4,
|
|
36
|
+
markdown: "\n## Direction\n\nA connection goes from source to target. Use source input or instruction -> transformation or generator -> output. Never connect a node to itself, and never connect a Section as if it were a workflow step.\n\n## Modes\n\n- Omit `mode` for a normal dependency or transformation flow.\n- `prompt`: a text or sticky note whose content is the generation prompt for the target image node. One prompt connection per target; image generation reads the prompt from this node when the request omits a prompt.\n- `reference`: an image reference that guides another node.\n- `list`: a Prompt List or Image List flow.\n- `first-frame`: an image used as the first frame of a video node.\n- `last-frame`: an image used as the last frame of a video node.\n\nUse exact `node:` handles or batch-local `client:` references. Connections remain attached when nodes move. Avoid duplicate edges with the same source, target, and mode.\n\n## Batch-local example\n\n```json\n{\n \"type\": \"connection.create\",\n \"clientId\": \"brief-to-generator\",\n \"from\": \"client:brief\",\n \"to\": \"client:generator\"\n}\n```\n\nDo not rely on spatial proximity to imply a relationship. If a relationship matters to execution or later understanding, connect it.\n",
|
|
37
|
+
}),
|
|
38
|
+
Object.freeze({
|
|
39
|
+
id: "prompt-lists",
|
|
40
|
+
title: "Prompt Lists and Repeated Directions",
|
|
41
|
+
description: "Represent several editable creative directions as one native Prompt List with shared and row-specific references.",
|
|
42
|
+
keywords: Object.freeze(["prompt","list","directions","rows","referenceBindings","product","style","batch","generator"]),
|
|
43
|
+
order: 5,
|
|
44
|
+
markdown: "\n## Prompt List shape\n\nUse one `text` node with:\n\n- `metadata.isList: true`\n- `metadata.listType: \"prompt\"`\n- `metadata.listExecutionMode: \"batch\"` for separate outputs\n- one checked `metadata.listItems` entry per direction\n\nConnect a shared product image to the List once. Connect the List to one empty image generator. For a row-specific style, store the exact style image in that row's `referenceBindings` as `{ \"nodeId\": \"node:<id>\", \"role\": \"style\" }`. Do not copy a filename into prompt prose as a substitute for a binding.\n\nEvery product-variant row should explicitly say to preserve the exact connected product and not substitute it. Building the List and generator prepares the workflow only; it must not start generation.\n",
|
|
45
|
+
}),
|
|
46
|
+
Object.freeze({
|
|
47
|
+
id: "generated-assets",
|
|
48
|
+
title: "Generated Images, Videos, and Durable Outputs",
|
|
49
|
+
description: "Prepare media nodes, start only explicit generation, and preserve output lineage.",
|
|
50
|
+
keywords: Object.freeze(["generated","generation","image","video","output","asset","durable","lineage","placeholder","job"]),
|
|
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- 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- 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. Poll the returned Run or Job; do not start another run while waiting. A terminal failure must be reported without automatic retry.\n\n## Completion\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. 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, durable output count, and product-fidelity review state. Do not claim Done while it says `doneClaimAllowed: false`. Product-fidelity uncertainty requires human review; never create another paid provider call automatically.\n",
|
|
53
|
+
}),
|
|
54
|
+
Object.freeze({
|
|
55
|
+
id: "existing-canvases",
|
|
56
|
+
title: "Editing Existing Canvases Safely",
|
|
57
|
+
description: "Preserve user structure, concurrent edits, and unrelated content while making scoped changes.",
|
|
58
|
+
keywords: Object.freeze(["existing","preserve","concurrent","revision","conflict","409","idempotency","unrelated","delete","scope"]),
|
|
59
|
+
order: 7,
|
|
60
|
+
markdown: "\n## Preservation contract\n\n- Call `canvas_get` and retain the exact revision before planning a write.\n- Make the smallest requested delta. Do not move, resize, retitle, reconnect, delete, or rewrite an existing node outside the requested scope.\n- Prefer one atomic batch for related nodes and connections so a malformed operation leaves no partial graph.\n- Pass the read revision as `baseRevision`.\n- On `409`, read again and rebase the intended additions around the newer graph. Preserve the concurrent user change.\n- Reuse an idempotency key only for an identical intended payload. If the intended payload changes, use a new key.\n- Pass proposed delete operations to `canvas_validate` and report their node and connection impact before applying them.\n\nAn agent-authored addition should be removable without damaging surrounding user work. Exact returned handles are the audit trail.\n",
|
|
61
|
+
}),
|
|
62
|
+
Object.freeze({
|
|
63
|
+
id: "paid-action-safety",
|
|
64
|
+
title: "Paid Action Safety",
|
|
65
|
+
description: "Separate preparation from execution and prevent accidental or repeated provider charges.",
|
|
66
|
+
keywords: Object.freeze(["paid","credits","cost","generate","run","retry","failure","prepare","setup","explicit"]),
|
|
67
|
+
order: 8,
|
|
68
|
+
markdown: "\n## Intent boundary\n\n\"Build\", \"prepare\", \"set up\", \"connect\", \"draft\", and \"make ready\" authorize graph edits only. They do not authorize Recipe, image, video, or Action execution.\n\nStart paid work only when the current user message explicitly asks to run or generate it. Do not infer authorization from an older message, a node label, an unfinished placeholder, or nearby content.\n\n## Retry boundary\n\n- Use one stable idempotency key for one intended paid operation.\n- Poll the returned handle with status tools.\n- Never automatically retry a terminal failure, timeout, disconnect, or ambiguous provider response with a new key.\n- Ask for new user intent before any new paid attempt.\n\nDeterministic Actions may be described as credit-free only when `action_get` confirms that contract. Inspect an Action before running it.\n",
|
|
69
|
+
}),
|
|
70
|
+
Object.freeze({
|
|
71
|
+
id: "validation-recovery",
|
|
72
|
+
title: "Validation and Recovery",
|
|
73
|
+
description: "Interpret structural findings, correct invalid batches, and finish with an evidence-based review.",
|
|
74
|
+
keywords: Object.freeze(["validate","lint","overlap","orphan","header","connection","destructive","recovery","error","atomic","blocked","force"]),
|
|
75
|
+
order: 9,
|
|
76
|
+
markdown: "\n## Use `canvas_validate`\n\nCall it with only `canvasId` to audit the current graph. Pass the proposed `operations` to inspect the planned post-batch graph and destructive impact before writing.\n\nWarning- and info-level findings are advisory. Error-severity findings caused by the proposed agent write have teeth: `canvas_apply_batch` rejects that batch and writes nothing. Historical findings remain visible for review but do not turn an unrelated scoped write into a forced cleanup. Validation never mutates the canvas or consumes an idempotency key.\n\n`summary.passed` means there is no structural error. `summary.reviewRequired` is true when errors, warnings, or truncated findings still require agent or human review. `completionReview` is the finalization-ready structured view: it names overlap, containment, reference lineage, output count, and product-fidelity state. Never claim Done unless `completionReview.doneClaimAllowed` is true.\n\n## Blocked writes\n\nWhen a batch is rejected with error-severity findings:\n\n- Nothing was written and the idempotency key was not consumed — the same key retries the corrected batch of the same intent.\n- The rejection lists the blocking findings. Fix the listed operations, then confirm with `canvas_validate` using the same `operations` before applying again.\n- `force: true` applies the batch despite error findings. Use it only after validating, and only when the user explicitly accepts the listed findings — never as a routine retry shortcut.\n\n## Recovery\n\n- If an operation is rejected, the atomic batch makes no partial change.\n- Read the finding's exact handles and linked guide topic.\n- Correct unsupported object types, metadata, endpoint modes, positions, or references instead of guessing repeatedly.\n- Use a new idempotency key if the corrected payload represents a changed intent.\n- Read and validate again after a successful write.\n\nCommon findings include ordinary-node overlap, Section content overflow, generated output without an incoming relationship, text imitating a Section header, broken lineage handles, unclear media connections, and proposed deletions. Product-fidelity uncertainty is a review state, not authorization to regenerate.\n",
|
|
77
|
+
}),
|
|
78
|
+
Object.freeze({
|
|
79
|
+
id: "examples-common-mistakes",
|
|
80
|
+
title: "Examples and Common Mistakes",
|
|
81
|
+
description: "Compact patterns to copy and anti-patterns agents must avoid.",
|
|
82
|
+
keywords: Object.freeze(["example","examples","mistake","mistakes","anti-pattern","section","note","text","generated","connection"]),
|
|
83
|
+
order: 10,
|
|
84
|
+
markdown: "\n## Good patterns\n\n- Research cluster: one Section, several Sticky observations, one Text synthesis, and directed observation -> synthesis connections.\n- Creative workflow: source image -> Prompt List -> empty generator -> generated outputs.\n- Video workflow: first image -[first-frame]-> video target and optional last image -[last-frame]-> video target.\n- Existing-canvas addition: new contained Section outside the current bounds, with no existing-node mutations.\n\n## Common mistakes\n\n- Using long Text or Sticky nodes as fake headers instead of Sections.\n- Creating every node at `{ \"x\": 0, \"y\": 0 }` or stacking nodes on top of one another.\n- Leaving generated outputs disconnected from their prompt, List, source, or stage.\n- Connecting a Section to workflow nodes.\n- Treating nearby objects as implicit inputs without explicit handles or connections.\n- Copying a style image name into prompt text instead of using a row-level reference binding.\n- Writing generated media content directly into node metadata.\n- Reorganizing or deleting existing work when the user asked only to add something.\n- Starting generation when the user asked only to prepare a workflow.\n- Retrying a failed paid operation automatically.\n- Reporting a temporary Job or Run handle as a durable Canvas output.\n- Passing `force: true` to push a batch past error-severity findings instead of fixing the operations. Force is for explicit, user-approved exceptions only.\n",
|
|
85
|
+
}),
|
|
86
|
+
]);
|