@gmickel/gno 1.38.0 → 1.39.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/assets/skill/README.md +2 -0
- package/browser-extension/artifacts/{gno-browser-clipper-v1.38.0.zip → gno-browser-clipper-v1.39.0.zip} +0 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.39.0.zip.sha256 +1 -0
- package/browser-extension/dist/manifest.json +1 -1
- package/package.json +1 -1
- package/spec/cli.md +208 -11
- package/spec/output-schemas/agents-mutation.schema.json +108 -0
- package/spec/output-schemas/agents-verify.schema.json +89 -0
- package/src/cli/commands/agents/block.ts +164 -0
- package/src/cli/commands/agents/commands.ts +413 -0
- package/src/cli/commands/agents/engine.ts +417 -0
- package/src/cli/commands/agents/harnesses.ts +298 -0
- package/src/cli/commands/agents/index.ts +35 -0
- package/src/cli/commands/completion/scripts.ts +5 -0
- package/src/cli/program.ts +104 -0
- package/browser-extension/artifacts/gno-browser-clipper-v1.38.0.zip.sha256 +0 -1
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gno agents` command runners: install, update, verify, uninstall.
|
|
3
|
+
*
|
|
4
|
+
* @module src/cli/commands/agents/commands
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { CliError } from "../../errors.js";
|
|
8
|
+
import { getGlobals } from "../../program.js";
|
|
9
|
+
import {
|
|
10
|
+
BLOCK_VERSION,
|
|
11
|
+
extractBlock,
|
|
12
|
+
renderBlock,
|
|
13
|
+
stampAuthenticates,
|
|
14
|
+
} from "./block.js";
|
|
15
|
+
import {
|
|
16
|
+
applyPlan,
|
|
17
|
+
decodeInstructionFile,
|
|
18
|
+
type PlanMode,
|
|
19
|
+
planTargets,
|
|
20
|
+
planWrites,
|
|
21
|
+
type TargetPlan,
|
|
22
|
+
unifiedDiff,
|
|
23
|
+
} from "./engine.js";
|
|
24
|
+
import {
|
|
25
|
+
type HarnessId,
|
|
26
|
+
HARNESS_IDS,
|
|
27
|
+
type ResolvedTarget,
|
|
28
|
+
resolveTargets,
|
|
29
|
+
} from "./harnesses.js";
|
|
30
|
+
|
|
31
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
32
|
+
// Shared
|
|
33
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export interface AgentsOptions {
|
|
36
|
+
target?: HarnessId | "all";
|
|
37
|
+
extraDirs?: string[];
|
|
38
|
+
dryRun?: boolean;
|
|
39
|
+
json?: boolean;
|
|
40
|
+
quiet?: boolean;
|
|
41
|
+
/** Override for testing / sandboxed live verification. */
|
|
42
|
+
homeDir?: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function safeGetGlobals(): { json: boolean; quiet: boolean } {
|
|
46
|
+
try {
|
|
47
|
+
return getGlobals();
|
|
48
|
+
} catch {
|
|
49
|
+
return { json: false, quiet: false };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function parseTargetOption(raw: string): HarnessId | "all" {
|
|
54
|
+
if (raw === "all" || (HARNESS_IDS as string[]).includes(raw)) {
|
|
55
|
+
return raw as HarnessId | "all";
|
|
56
|
+
}
|
|
57
|
+
throw new CliError(
|
|
58
|
+
"VALIDATION",
|
|
59
|
+
`Invalid target: ${raw}. Must be one of ${HARNESS_IDS.join(", ")}, or 'all'.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function outputSettings(opts: AgentsOptions): {
|
|
64
|
+
json: boolean;
|
|
65
|
+
quiet: boolean;
|
|
66
|
+
} {
|
|
67
|
+
const globals = safeGetGlobals();
|
|
68
|
+
return {
|
|
69
|
+
json: opts.json ?? globals.json,
|
|
70
|
+
quiet: opts.quiet ?? globals.quiet,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
interface TargetReport {
|
|
75
|
+
target: string;
|
|
76
|
+
label: string;
|
|
77
|
+
path: string;
|
|
78
|
+
action: string;
|
|
79
|
+
detected: boolean;
|
|
80
|
+
via?: string;
|
|
81
|
+
detail?: string;
|
|
82
|
+
backup?: string | null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function reportFor(
|
|
86
|
+
plan: TargetPlan,
|
|
87
|
+
backup: string | null | undefined
|
|
88
|
+
): TargetReport {
|
|
89
|
+
return {
|
|
90
|
+
target: plan.target.id,
|
|
91
|
+
label: plan.target.label,
|
|
92
|
+
path: plan.target.file,
|
|
93
|
+
action: plan.action,
|
|
94
|
+
detected: plan.target.detected,
|
|
95
|
+
...(plan.via && { via: plan.via }),
|
|
96
|
+
...(plan.detail && { detail: plan.detail }),
|
|
97
|
+
...(backup !== undefined && { backup }),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function printHumanReports(reports: TargetReport[]): void {
|
|
102
|
+
for (const r of reports) {
|
|
103
|
+
const via = r.via ? ` (${r.detail ?? `via ${r.via}`})` : "";
|
|
104
|
+
const detail = !r.via && r.detail ? `: ${r.detail}` : "";
|
|
105
|
+
process.stdout.write(
|
|
106
|
+
`${r.action.padEnd(12)} ${r.target.padEnd(10)} ${r.path}${via}${detail}\n`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
112
|
+
// Install / Update / Uninstall (shared mutation runner)
|
|
113
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
114
|
+
|
|
115
|
+
async function runMutation(
|
|
116
|
+
verb: "install" | "update" | "uninstall",
|
|
117
|
+
mode: PlanMode,
|
|
118
|
+
opts: AgentsOptions
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
const { json, quiet } = outputSettings(opts);
|
|
121
|
+
const dryRun = opts.dryRun ?? false;
|
|
122
|
+
|
|
123
|
+
const targets = resolveTargets(opts.target ?? "all", {
|
|
124
|
+
homeDir: opts.homeDir,
|
|
125
|
+
extraDirs: opts.extraDirs,
|
|
126
|
+
});
|
|
127
|
+
const plans = await planTargets(targets, mode);
|
|
128
|
+
|
|
129
|
+
const reports: TargetReport[] = [];
|
|
130
|
+
const diffs: string[] = [];
|
|
131
|
+
const failedPaths: string[] = [];
|
|
132
|
+
let validationErrors = 0;
|
|
133
|
+
|
|
134
|
+
for (const plan of plans) {
|
|
135
|
+
if (plan.action === "error") {
|
|
136
|
+
failedPaths.push(plan.target.file);
|
|
137
|
+
if (plan.errorCode !== "RUNTIME") {
|
|
138
|
+
validationErrors += 1;
|
|
139
|
+
}
|
|
140
|
+
reports.push(reportFor(plan, undefined));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (!planWrites(plan)) {
|
|
144
|
+
reports.push(reportFor(plan, undefined));
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (dryRun) {
|
|
148
|
+
diffs.push(
|
|
149
|
+
unifiedDiff(plan.oldContent, plan.newContent, plan.target.file)
|
|
150
|
+
);
|
|
151
|
+
reports.push(reportFor(plan, null));
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
// A failing write must not abort the run before the receipt is emitted:
|
|
155
|
+
// earlier targets may already have been written, and the operator needs
|
|
156
|
+
// to see exactly which. Record an `error` row and keep going.
|
|
157
|
+
try {
|
|
158
|
+
reports.push(reportFor(plan, await applyPlan(plan)));
|
|
159
|
+
} catch (err) {
|
|
160
|
+
failedPaths.push(plan.target.file);
|
|
161
|
+
reports.push({
|
|
162
|
+
...reportFor(plan, undefined),
|
|
163
|
+
action: "error",
|
|
164
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Fallback: when the installer could not apply the block somewhere, hand
|
|
170
|
+
// the operator the exact block to paste (install/update only — uninstall
|
|
171
|
+
// guidance is in the per-target detail).
|
|
172
|
+
const manualBlock =
|
|
173
|
+
failedPaths.length > 0 && mode === "install" ? renderBlock() : undefined;
|
|
174
|
+
|
|
175
|
+
if (json) {
|
|
176
|
+
process.stdout.write(
|
|
177
|
+
`${JSON.stringify(
|
|
178
|
+
{
|
|
179
|
+
command: verb,
|
|
180
|
+
blockVersion: BLOCK_VERSION,
|
|
181
|
+
dryRun,
|
|
182
|
+
results: reports,
|
|
183
|
+
...(dryRun && { diffs }),
|
|
184
|
+
...(manualBlock !== undefined && { manualBlock }),
|
|
185
|
+
},
|
|
186
|
+
null,
|
|
187
|
+
2
|
|
188
|
+
)}\n`
|
|
189
|
+
);
|
|
190
|
+
} else if (!quiet) {
|
|
191
|
+
printHumanReports(reports);
|
|
192
|
+
if (dryRun) {
|
|
193
|
+
for (const diff of diffs) {
|
|
194
|
+
if (diff) {
|
|
195
|
+
process.stdout.write(`\n${diff}\n`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
process.stdout.write("\nDry run — nothing was written.\n");
|
|
199
|
+
}
|
|
200
|
+
if (manualBlock !== undefined) {
|
|
201
|
+
process.stdout.write(
|
|
202
|
+
`\nCould not apply the block to: ${failedPaths.join(", ")}\n` +
|
|
203
|
+
"Append this block to the file yourself (replacing any existing gno:agents block):\n\n" +
|
|
204
|
+
`${manualBlock}\n`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (failedPaths.length > 0) {
|
|
210
|
+
// Exit-code contract (spec/cli.md): 1 = validation (malformed markers,
|
|
211
|
+
// non-UTF-8), 2 = runtime (I/O). Any validation failure makes it 1.
|
|
212
|
+
throw new CliError(
|
|
213
|
+
validationErrors > 0 ? "VALIDATION" : "RUNTIME",
|
|
214
|
+
`${verb} failed for ${failedPaths.length} target(s); see per-target detail above. Nothing was written to the failing file(s).`
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Install or refresh the protocol block. Both verbs converge the block to
|
|
221
|
+
* the current release: install appends when absent, update replaces an
|
|
222
|
+
* older/stale block in place; a current block is a no-op for either.
|
|
223
|
+
*/
|
|
224
|
+
export function installAgents(
|
|
225
|
+
opts: AgentsOptions = {},
|
|
226
|
+
verb: "install" | "update" = "install"
|
|
227
|
+
): Promise<void> {
|
|
228
|
+
return runMutation(verb, "install", opts);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Remove the protocol block and its markers, leaving the rest untouched. */
|
|
232
|
+
export function uninstallAgents(opts: AgentsOptions = {}): Promise<void> {
|
|
233
|
+
return runMutation("uninstall", "uninstall", opts);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
237
|
+
// Verify
|
|
238
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
239
|
+
|
|
240
|
+
type VerifyStatus =
|
|
241
|
+
| "ok"
|
|
242
|
+
| "outdated"
|
|
243
|
+
| "missing"
|
|
244
|
+
| "malformed"
|
|
245
|
+
| "error"
|
|
246
|
+
| "covered"
|
|
247
|
+
| "not-detected";
|
|
248
|
+
|
|
249
|
+
interface VerifyReport {
|
|
250
|
+
target: string;
|
|
251
|
+
label: string;
|
|
252
|
+
path: string;
|
|
253
|
+
status: VerifyStatus;
|
|
254
|
+
detected: boolean;
|
|
255
|
+
via?: string;
|
|
256
|
+
detail?: string;
|
|
257
|
+
blockVersion?: number;
|
|
258
|
+
hashOk?: boolean;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Inner text (stamp + body, no markers) of the current release's block. */
|
|
262
|
+
function expectedInner(): string {
|
|
263
|
+
return renderBlock().split("\n").slice(1, -1).join("\n");
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
async function verifyTarget(
|
|
267
|
+
target: ResolvedTarget,
|
|
268
|
+
runContext: {
|
|
269
|
+
resolvedIds: Set<string>;
|
|
270
|
+
requiredCovering: Set<string>;
|
|
271
|
+
/** Real-file identity → id of the target that owns verification. */
|
|
272
|
+
owners: Map<string, string>;
|
|
273
|
+
}
|
|
274
|
+
): Promise<VerifyReport> {
|
|
275
|
+
const base = {
|
|
276
|
+
target: target.id,
|
|
277
|
+
label: target.label,
|
|
278
|
+
path: target.file,
|
|
279
|
+
detected: target.detected,
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
if (!(target.detected || runContext.requiredCovering.has(target.id))) {
|
|
283
|
+
return { ...base, status: "not-detected" };
|
|
284
|
+
}
|
|
285
|
+
if (target.coveredBy) {
|
|
286
|
+
if (!runContext.resolvedIds.has(target.coveredBy)) {
|
|
287
|
+
return {
|
|
288
|
+
...base,
|
|
289
|
+
status: "missing",
|
|
290
|
+
detail: `covered via ${target.coveredBy}, but ${target.coveredBy} was not resolved in this run`,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return {
|
|
294
|
+
...base,
|
|
295
|
+
status: "covered",
|
|
296
|
+
via: target.coveredBy,
|
|
297
|
+
detail: `covered via ${target.coveredBy}`,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const owner = runContext.owners.get(target.realFile);
|
|
301
|
+
if (owner) {
|
|
302
|
+
return {
|
|
303
|
+
...base,
|
|
304
|
+
status: "covered",
|
|
305
|
+
via: owner,
|
|
306
|
+
detail: `covered via ${owner} (same file)`,
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
runContext.owners.set(target.realFile, target.id);
|
|
310
|
+
|
|
311
|
+
const file = Bun.file(target.file);
|
|
312
|
+
if (!(await file.exists())) {
|
|
313
|
+
return { ...base, status: "missing", detail: "instruction file not found" };
|
|
314
|
+
}
|
|
315
|
+
let bytes: Uint8Array;
|
|
316
|
+
try {
|
|
317
|
+
bytes = await file.bytes();
|
|
318
|
+
} catch (err) {
|
|
319
|
+
return {
|
|
320
|
+
...base,
|
|
321
|
+
status: "error",
|
|
322
|
+
detail: `could not read instruction file: ${err instanceof Error ? err.message : String(err)}`,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
let extraction: ReturnType<typeof extractBlock>;
|
|
326
|
+
try {
|
|
327
|
+
const { content } = decodeInstructionFile(bytes, target.file);
|
|
328
|
+
extraction = extractBlock(content, target.file);
|
|
329
|
+
} catch (err) {
|
|
330
|
+
return {
|
|
331
|
+
...base,
|
|
332
|
+
status: "malformed",
|
|
333
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (!extraction.found) {
|
|
337
|
+
return { ...base, status: "missing", detail: "no GNO agents block" };
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const { block } = extraction;
|
|
341
|
+
const hashOk = stampAuthenticates(block);
|
|
342
|
+
const versionOk = block.stamp?.version === BLOCK_VERSION;
|
|
343
|
+
const isCurrent = versionOk && hashOk && block.inner === expectedInner();
|
|
344
|
+
const versioned = { blockVersion: block.stamp?.version, hashOk };
|
|
345
|
+
if (isCurrent) {
|
|
346
|
+
return { ...base, status: "ok", ...versioned };
|
|
347
|
+
}
|
|
348
|
+
const detail = !block.stamp
|
|
349
|
+
? "block has no valid stamp line (missing or unparseable) — run `gno agents update`"
|
|
350
|
+
: !hashOk
|
|
351
|
+
? "block content does not match its stamp hash (edited inside markers?) — run `gno agents update`"
|
|
352
|
+
: !versionOk
|
|
353
|
+
? `block v${block.stamp?.version ?? "?"} does not match installed release v${BLOCK_VERSION} — run \`gno agents update\``
|
|
354
|
+
: "block content differs from the installed release — run `gno agents update`";
|
|
355
|
+
return { ...base, status: "outdated", ...versioned, detail };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
export async function verifyAgents(opts: AgentsOptions = {}): Promise<void> {
|
|
359
|
+
const { json, quiet } = outputSettings(opts);
|
|
360
|
+
const targets = resolveTargets(opts.target ?? "all", {
|
|
361
|
+
homeDir: opts.homeDir,
|
|
362
|
+
extraDirs: opts.extraDirs,
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
const resolvedIds = new Set(targets.map((t) => t.id as string));
|
|
366
|
+
const requiredCovering = new Set<string>();
|
|
367
|
+
for (const t of targets) {
|
|
368
|
+
if (t.detected && t.coveredBy) {
|
|
369
|
+
requiredCovering.add(t.coveredBy);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
const owners = new Map<string, string>();
|
|
373
|
+
const results: VerifyReport[] = [];
|
|
374
|
+
for (const target of targets) {
|
|
375
|
+
results.push(
|
|
376
|
+
await verifyTarget(target, { resolvedIds, requiredCovering, owners })
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const failing = results.filter(
|
|
381
|
+
(r) => !["ok", "covered", "not-detected"].includes(r.status)
|
|
382
|
+
);
|
|
383
|
+
const ok = failing.length === 0;
|
|
384
|
+
|
|
385
|
+
if (json) {
|
|
386
|
+
process.stdout.write(
|
|
387
|
+
`${JSON.stringify(
|
|
388
|
+
{ command: "verify", blockVersion: BLOCK_VERSION, ok, results },
|
|
389
|
+
null,
|
|
390
|
+
2
|
|
391
|
+
)}\n`
|
|
392
|
+
);
|
|
393
|
+
} else if (!quiet) {
|
|
394
|
+
for (const r of results) {
|
|
395
|
+
const detail = r.detail ? ` — ${r.detail}` : "";
|
|
396
|
+
process.stdout.write(
|
|
397
|
+
`${r.status.padEnd(13)} ${r.target.padEnd(10)} ${r.path}${detail}\n`
|
|
398
|
+
);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (!ok) {
|
|
403
|
+
// 2 when the only failures were unreadable files (could not check),
|
|
404
|
+
// 1 for any content verdict (outdated / missing / malformed).
|
|
405
|
+
const allRuntime = failing.every((r) => r.status === "error");
|
|
406
|
+
throw new CliError(
|
|
407
|
+
allRuntime ? "RUNTIME" : "VALIDATION",
|
|
408
|
+
`verification failed for ${failing.length} target(s): ${failing
|
|
409
|
+
.map((r) => `${r.target} (${r.status})`)
|
|
410
|
+
.join(", ")}`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
}
|