@vincentt-xr/cli 0.9.0 → 0.10.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/README.md +40 -13
- package/dist/cli/args.d.ts +12 -0
- package/dist/cli/args.js +40 -0
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +296 -95
- package/dist/cli/usage-text.d.ts +1 -0
- package/dist/cli/usage-text.js +97 -0
- package/dist/cli/verb-surface.d.ts +24 -0
- package/dist/cli/verb-surface.js +49 -0
- package/dist/dx/clusters.d.ts +0 -17
- package/dist/dx/clusters.js +50 -1
- package/dist/lib/backend.d.ts +11 -0
- package/dist/lib/backend.js +23 -0
- package/dist/lib/classifyTarget.d.ts +42 -0
- package/dist/lib/classifyTarget.js +83 -0
- package/dist/lib/feedback.d.ts +46 -5
- package/dist/lib/feedback.js +76 -19
- package/dist/lib/feedbackCopy.d.ts +70 -0
- package/dist/lib/feedbackCopy.js +163 -0
- package/dist/lib/initFrom.d.ts +26 -4
- package/dist/lib/initFrom.js +65 -41
- package/dist/lib/lifecycle.d.ts +28 -3
- package/dist/lib/lifecycle.js +99 -7
- package/dist/lib/listProjects.d.ts +41 -0
- package/dist/lib/listProjects.js +77 -0
- package/dist/lib/projects.d.ts +64 -8
- package/dist/lib/projects.js +316 -24
- package/dist/lib/sourceRefCopy.d.ts +19 -0
- package/dist/lib/sourceRefCopy.js +36 -0
- package/dist/preview/command.d.ts +11 -0
- package/dist/preview/command.js +19 -1
- package/dist/preview/copy.d.ts +7 -0
- package/dist/preview/copy.js +8 -1
- package/dist/preview/runner.d.ts +6 -0
- package/dist/preview/runner.js +1 -0
- package/dist/preview/stream.d.ts +11 -0
- package/dist/preview/stream.js +13 -0
- package/dist/relay/annotations.d.ts +28 -4
- package/dist/relay/annotations.js +23 -7
- package/dist/relay/qa.f12.fixtures.d.ts +154 -0
- package/dist/relay/qa.f12.fixtures.js +358 -0
- package/dist/relay/server.d.ts +41 -3
- package/dist/relay/server.js +90 -32
- package/package.json +2 -2
- package/dist/library/qa.f5-b32.spec.d.ts +0 -1
- package/dist/library/qa.f5-b32.spec.js +0 -202
- package/dist/library/qa.f5-cli.spec.d.ts +0 -1
- package/dist/library/qa.f5-cli.spec.js +0 -433
- package/dist/library/qa.f5-contract.spec.d.ts +0 -1
- package/dist/library/qa.f5-contract.spec.js +0 -206
- package/dist/relay/injectors.d.ts +0 -29
- package/dist/relay/injectors.js +0 -72
|
@@ -1,206 +0,0 @@
|
|
|
1
|
-
// QA-F5-G6 · The cross-repo contract — the API's REAL output through the CLI's
|
|
2
|
-
// REAL parser. EXEMPT from the guardrail budget and always written.
|
|
3
|
-
//
|
|
4
|
-
// This is HALF the case. The other half is
|
|
5
|
-
// api/internal/httpapi/qa_f5_contract_test.go, which boots the real router over
|
|
6
|
-
// the real corpus and writes the API's ACTUAL response bytes into
|
|
7
|
-
// __fixtures__/api-golden/. This file feeds those exact bytes to the CLI's real
|
|
8
|
-
// parser and renderers — never to a hand-written stub.
|
|
9
|
-
//
|
|
10
|
-
// WHY. A per-repo suite green against its OWN stub hides a param-name or casing
|
|
11
|
-
// mismatch. On f1 the CLI emitted `redirect` while the console read
|
|
12
|
-
// `redirect_uri`: every suite was green and login was broken end to end, and it
|
|
13
|
-
// surfaced only at the security pass. f5 is the highest-probability casing bug
|
|
14
|
-
// in the product, because it is the ONE response in this API with a mixed
|
|
15
|
-
// convention — camelCase envelope, snake_case record — in a single body.
|
|
16
|
-
//
|
|
17
|
-
// ⚠ THE GOLDEN FILES ARE NOT HAND-EDITABLE. Editing one turns this case back
|
|
18
|
-
// into a per-repo stub test, which is precisely the failure it exists to catch.
|
|
19
|
-
// Regenerate them by running the Go half.
|
|
20
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
21
|
-
import path from "node:path";
|
|
22
|
-
import { fileURLToPath } from "node:url";
|
|
23
|
-
import { describe, expect, it } from "vitest";
|
|
24
|
-
import { searchLines, showLines, unknownBlockLines } from "./render.js";
|
|
25
|
-
import { parseBatchResponse, parseFetchResponse, parseSearchResponse, } from "./wire.js";
|
|
26
|
-
const GOLDEN = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "__fixtures__", "api-golden");
|
|
27
|
-
/**
|
|
28
|
-
* Read one captured response.
|
|
29
|
-
*
|
|
30
|
-
* A MISSING file FAILS rather than skips. A contract case that quietly tests
|
|
31
|
-
* five of seven arms is worse than no case: it reports green while the two arms
|
|
32
|
-
* that were dropped are exactly the ones nobody looked at.
|
|
33
|
-
*/
|
|
34
|
-
function golden(name) {
|
|
35
|
-
const file = path.join(GOLDEN, `${name}.json`);
|
|
36
|
-
if (!existsSync(file)) {
|
|
37
|
-
throw new Error(`the captured API response ${name}.json is MISSING from ${GOLDEN}.\n\n` +
|
|
38
|
-
`Regenerate it from the api worktree:\n` +
|
|
39
|
-
` go test -C <api> ./internal/httpapi/ -run TestQAF5G6 -count=1\n\n` +
|
|
40
|
-
`These files are the API's ACTUAL bytes. Do NOT hand-write one to make this pass — ` +
|
|
41
|
-
`a hand-written fixture turns the cross-repo contract case back into a per-repo stub ` +
|
|
42
|
-
`test, which is the exact failure it exists to catch.`);
|
|
43
|
-
}
|
|
44
|
-
return JSON.parse(readFileSync(file, "utf8"));
|
|
45
|
-
}
|
|
46
|
-
/** The raw object, for key-presence assertions a typed parse cannot make. */
|
|
47
|
-
function rawGolden(name) {
|
|
48
|
-
return golden(name);
|
|
49
|
-
}
|
|
50
|
-
describe("QA-F5-G6 · every arm of the contract table round-trips", () => {
|
|
51
|
-
it("the capture manifest is complete — a missing arm FAILS rather than skips", () => {
|
|
52
|
-
const manifest = rawGolden("manifest");
|
|
53
|
-
expect(manifest.arms, "the capture manifest names no arms").toBeDefined();
|
|
54
|
-
for (const arm of manifest.arms ?? []) {
|
|
55
|
-
expect(existsSync(path.join(GOLDEN, `${arm}.json`)), `the manifest names ${arm} but the capture is missing`).toBe(true);
|
|
56
|
-
}
|
|
57
|
-
expect(manifest.arms?.length).toBeGreaterThanOrEqual(7);
|
|
58
|
-
});
|
|
59
|
-
it("(a) search WITH results — the camelCase envelope and the snake_case record BOTH survive", () => {
|
|
60
|
-
const raw = rawGolden("search-results");
|
|
61
|
-
// THE ENVELOPE IS camelCase. Asserted on the RAW bytes, because the parser
|
|
62
|
-
// would happily read either spelling and report nothing.
|
|
63
|
-
expect(Object.keys(raw), "the API's search envelope does not carry `resultCount`").toContain("resultCount");
|
|
64
|
-
expect(Object.keys(raw), "the API's search envelope carries a snake_case `result_count`. THE ENVELOPE IS " +
|
|
65
|
-
"camelCase and the RECORD is snake_case, in ONE body. Someone normalized both layers " +
|
|
66
|
-
"to one convention — which reads like a fix and silently breaks every agent prompt in " +
|
|
67
|
-
"the wild that was told the field is `usage_hint`.").not.toContain("result_count");
|
|
68
|
-
const res = parseSearchResponse(raw);
|
|
69
|
-
expect(res.resultCount).toBeGreaterThan(0);
|
|
70
|
-
expect(res.results.length).toBe(res.resultCount);
|
|
71
|
-
expect(res.gaps, "`gaps` is absent on a non-empty result set").toEqual([]);
|
|
72
|
-
// THE RECORD IS snake_case, and each of these is a field the renderer reads.
|
|
73
|
-
const rec = raw.results[0];
|
|
74
|
-
for (const k of ["id", "url", "has_alpha", "usage_hint", "occupies_canvas"]) {
|
|
75
|
-
expect(Object.keys(rec), `the API's record does not carry ${k}`).toContain(k);
|
|
76
|
-
}
|
|
77
|
-
for (const k of ["hasAlpha", "usageHint", "occupiesCanvas", "textInImage"]) {
|
|
78
|
-
expect(Object.keys(rec), `the API's record carries the camelCase key ${k}. The record is snake_case.`).not.toContain(k);
|
|
79
|
-
}
|
|
80
|
-
// The PARSER carried the values through, not merely the keys. A parser that
|
|
81
|
-
// read a key and dropped its value passes every key assertion above.
|
|
82
|
-
const parsed = res.results[0];
|
|
83
|
-
expect(parsed.id).toBe(rec.id);
|
|
84
|
-
expect(parsed.url).toBe(rec.url);
|
|
85
|
-
expect(parsed.usage_hint).toBe(rec.usage_hint);
|
|
86
|
-
expect(parsed.has_alpha).toBe(rec.has_alpha);
|
|
87
|
-
expect(parsed.occupies_canvas).toBe(rec.occupies_canvas);
|
|
88
|
-
// And the RENDERER prints the url — the one thing `add`/`search` exist to
|
|
89
|
-
// produce.
|
|
90
|
-
const screen = searchLines(res, "pumpkin").join("\n");
|
|
91
|
-
expect(screen).toContain(parsed.url);
|
|
92
|
-
});
|
|
93
|
-
it("(b) search with ZERO results — gaps[].tag AND gaps[].message BOTH survive", () => {
|
|
94
|
-
const raw = rawGolden("search-zero-gaps");
|
|
95
|
-
const res = parseSearchResponse(raw);
|
|
96
|
-
expect(res.resultCount).toBe(0);
|
|
97
|
-
expect(res.results).toEqual([]);
|
|
98
|
-
expect(res.gaps.length, "the API's zero-result response carried NO gaps through the CLI's parser. The honest " +
|
|
99
|
-
"empty is the whole feature: without gaps, an unexplained empty is what teaches an " +
|
|
100
|
-
"agent to invent a URL.").toBeGreaterThan(0);
|
|
101
|
-
for (const g of res.gaps) {
|
|
102
|
-
expect(g.tag, "a gap survived without its `tag`. An agent BRANCHES on tag and QUOTES message; " +
|
|
103
|
-
"shipping one means it branches on prose.").toBeTruthy();
|
|
104
|
-
expect(g.message, "a gap survived without its `message`").toBeTruthy();
|
|
105
|
-
}
|
|
106
|
-
// VERBATIM: the rendered sentence is byte-identical to the corpus's, not one
|
|
107
|
-
// the CLI composed.
|
|
108
|
-
const rawGaps = raw.gaps;
|
|
109
|
-
const screen = searchLines(res, "christmas face filter").join("\n");
|
|
110
|
-
// The renderer wraps at 70 columns, so compare on the collapsed whitespace.
|
|
111
|
-
const collapsed = screen.replace(/\s+/g, " ");
|
|
112
|
-
expect(collapsed, "the rendered zero-result screen does not carry the corpus's gap message VERBATIM. The " +
|
|
113
|
-
"moment the CLI edits this string it is COMPOSING an inventory claim rather than " +
|
|
114
|
-
"REPORTING one — and closing a gap stops being a corpus change.").toContain(rawGaps[0].message.replace(/\s+/g, " "));
|
|
115
|
-
});
|
|
116
|
-
it("(c) the 503 body carries NO gaps, NO results and NO resultCount — the CLI cannot render it as an empty answer", () => {
|
|
117
|
-
const raw = rawGolden("search-503");
|
|
118
|
-
for (const forbidden of ["gaps", "results", "resultCount"]) {
|
|
119
|
-
expect(Object.keys(raw), `the API's 503 body carries ${forbidden}. A LOAD FAILURE MUST NOT WEAR AN INVENTORY ` +
|
|
120
|
-
`FACT'S CLOTHES: with a \`gaps\` key present the CLI could render "the library has ` +
|
|
121
|
-
`no wearable assets" about a ~500-record library whose corpus merely failed to load.`).not.toContain(forbidden);
|
|
122
|
-
}
|
|
123
|
-
expect(raw.code).toBe("library_unavailable");
|
|
124
|
-
});
|
|
125
|
-
it("(d) batch PARTIAL — resolved and unknown are disjoint, and unknown echoes VERBATIM", () => {
|
|
126
|
-
const raw = rawGolden("batch-partial");
|
|
127
|
-
expect(Object.keys(raw)).toContain("resolved");
|
|
128
|
-
expect(Object.keys(raw)).toContain("unknown");
|
|
129
|
-
const res = parseBatchResponse(raw);
|
|
130
|
-
expect(res.resolved.length).toBeGreaterThan(0);
|
|
131
|
-
expect(res.unknown.length).toBeGreaterThan(0);
|
|
132
|
-
const resolvedIds = new Set(res.resolved.map((r) => r.id));
|
|
133
|
-
for (const u of res.unknown) {
|
|
134
|
-
expect(resolvedIds.has(u), `${u} is in BOTH arrays`).toBe(false);
|
|
135
|
-
}
|
|
136
|
-
// The echo is the caller's own input, verbatim, so an agent can diff against
|
|
137
|
-
// what it sent without parsing our prose.
|
|
138
|
-
expect(res.unknown).toContain("nope");
|
|
139
|
-
expect(res.unknown).toContain("lib_ffffffffffffffff");
|
|
140
|
-
// The renderer does not throw on a partial and names the offending strings.
|
|
141
|
-
const block = unknownBlockLines(res.unknown, res.resolved.length, true).join("\n");
|
|
142
|
-
expect(block).toContain("nope");
|
|
143
|
-
});
|
|
144
|
-
it("(e) fetch-one RETIRED — `retired` rides fetch and survives the parser", () => {
|
|
145
|
-
const raw = rawGolden("fetch-retired");
|
|
146
|
-
expect(Object.keys(raw), "the fetch response for a RETIRED asset carries no `retired` key. Without it a caller " +
|
|
147
|
-
"cannot tell a de-listed asset from a current one, which is the one fact they need to " +
|
|
148
|
-
"decide whether to keep using it — and the URL keeps serving forever either way (B37).").toContain("retired");
|
|
149
|
-
const rec = parseFetchResponse(raw);
|
|
150
|
-
expect(rec).toBeDefined();
|
|
151
|
-
expect(rec?.retired).toBe(true);
|
|
152
|
-
expect(() => showLines(rec)).not.toThrow();
|
|
153
|
-
});
|
|
154
|
-
it("(f) a record with NO kind renders FEWER lines and does NOT throw", () => {
|
|
155
|
-
const raw = rawGolden("fetch-no-kind");
|
|
156
|
-
expect(Object.keys(raw), "the unclassifiable record carries a `kind` key. It must be ABSENT — not \"\", not " +
|
|
157
|
-
"\"unknown\". `kind: \"\"` is a PLACEMENT CONTRACT that reads as a value, and an agent " +
|
|
158
|
-
"branching on it places an unclassifiable asset as a prop.").not.toContain("kind");
|
|
159
|
-
const rec = parseFetchResponse(raw);
|
|
160
|
-
expect(rec).toBeDefined();
|
|
161
|
-
expect(rec?.kind).toBeUndefined();
|
|
162
|
-
// The renderer READS EVERY FIELD DEFENSIVELY: a missing optional field must
|
|
163
|
-
// degrade to a MISSING LINE, never to an exception. The CLI and the API are
|
|
164
|
-
// versioned separately on a creator's machine — `npx vincentt` fetches
|
|
165
|
-
// whatever the registry has and points it at whatever API the config
|
|
166
|
-
// resolves to.
|
|
167
|
-
let lines = [];
|
|
168
|
-
expect(() => {
|
|
169
|
-
lines = showLines(rec);
|
|
170
|
-
}).not.toThrow();
|
|
171
|
-
const full = parseFetchResponse(rawGolden("fetch-retired"));
|
|
172
|
-
const fullLines = showLines(full);
|
|
173
|
-
expect(lines.length, "the record with no kind rendered the SAME number of lines as a full record — the " +
|
|
174
|
-
"renderer is emitting a line for a field that is not there.").toBeLessThan(fullLines.length);
|
|
175
|
-
});
|
|
176
|
-
it("(g) stage's 202 carries every field the operator's process must transport", () => {
|
|
177
|
-
const raw = rawGolden("stage-202");
|
|
178
|
-
// The camelCase envelope of the admin surface. `confirmationDigest` is the
|
|
179
|
-
// ONE wire value both sides COMPUTE rather than copy, which is why a
|
|
180
|
-
// differing key order or float format silently makes EVERY commit fail.
|
|
181
|
-
for (const k of ["revisionId", "counts", "diff", "confirmationDigest", "ratio", "expiresAt"]) {
|
|
182
|
-
expect(Object.keys(raw), `the stage response omits ${k}`).toContain(k);
|
|
183
|
-
}
|
|
184
|
-
expect(typeof raw.confirmationDigest).toBe("string");
|
|
185
|
-
expect(raw.confirmationDigest.length).toBeGreaterThan(16);
|
|
186
|
-
// The diff's own shape, which the ratio tripwire and the digest are computed
|
|
187
|
-
// over.
|
|
188
|
-
const diff = raw.diff;
|
|
189
|
-
for (const k of ["added", "removed", "changed", "ratio"]) {
|
|
190
|
-
expect(Object.keys(diff), `the diff omits ${k}`).toContain(k);
|
|
191
|
-
}
|
|
192
|
-
// The server's ratio and the envelope's agree. Two numbers for one fact is
|
|
193
|
-
// how a client ends up trusting the wrong one.
|
|
194
|
-
expect(raw.ratio).toBe(diff.ratio);
|
|
195
|
-
// ⚠ THE CLI DOES NOT RECOMPUTE THE DIGEST IN v1.
|
|
196
|
-
//
|
|
197
|
-
// The catalog's (g) called for the CLI to recompute confirmationDigest from
|
|
198
|
-
// the diff bytes and match the server's, pinned to a golden vector. That arm
|
|
199
|
-
// was RETIRED with `vincentt library publish`
|
|
200
|
-
// (D-cli-does-not-publish-the-corpus-admin-panel-will): there is no CLI code
|
|
201
|
-
// that computes a digest, so there is nothing to compare. The SERVER-side
|
|
202
|
-
// half of the pin — that the value is present, stable and computed over the
|
|
203
|
-
// diff — is asserted here and in the Go contract test; the CLIENT-side half
|
|
204
|
-
// belongs to the admin panel's groom.
|
|
205
|
-
});
|
|
206
|
-
});
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { Annotation } from "@vincentt-xr/harness/events";
|
|
2
|
-
export interface Injector {
|
|
3
|
-
/** Stable id for config/logging: "claude-code" | "paste" | … */
|
|
4
|
-
readonly name: string;
|
|
5
|
-
/** Cheap best-effort readiness probe. The dispatcher skips unavailable injectors. */
|
|
6
|
-
isAvailable(): Promise<boolean>;
|
|
7
|
-
/**
|
|
8
|
-
* Deliver so the agent acts with zero user action. Resolves when the host
|
|
9
|
-
* accepted the injection (not when the agent finishes). Throws if it couldn't —
|
|
10
|
-
* the dispatcher falls through to the next injector (paste never throws).
|
|
11
|
-
*/
|
|
12
|
-
inject(a: Annotation): Promise<void>;
|
|
13
|
-
}
|
|
14
|
-
export interface Dispatcher {
|
|
15
|
-
dispatch(a: Annotation): Promise<string>;
|
|
16
|
-
}
|
|
17
|
-
/** Walk the injector chain; first available that accepts wins, else fall through. */
|
|
18
|
-
export declare function createDispatcher(injectors: Injector[], onLog?: (msg: string) => void): Dispatcher;
|
|
19
|
-
/**
|
|
20
|
-
* Claude Code passive injector: append the annotation to a queue a running `/loop`
|
|
21
|
-
* drains. Available only when the queue dir exists (the loop has been set up), so a
|
|
22
|
-
* project with no loop configured falls through to the fallback.
|
|
23
|
-
*/
|
|
24
|
-
export declare function claudeCodeInjector(queuePath: string): Injector;
|
|
25
|
-
/**
|
|
26
|
-
* Universal never-fail fallback. When no zero-action injector accepted the
|
|
27
|
-
* annotation, stage a ready prompt so the worst case is a single paste.
|
|
28
|
-
*/
|
|
29
|
-
export declare function pasteInjector(onReady: (prompt: string) => void): Injector;
|
package/dist/relay/injectors.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
// The one per-agent seam of the reverse channel (docs/harness-annotation-push.md).
|
|
2
|
-
// Everything upstream — capture, relay, store — is agent-agnostic; turning a
|
|
3
|
-
// stored annotation into an agent turn goes through an Injector. The dispatcher
|
|
4
|
-
// walks an ordered chain: the first available injector that accepts wins, and on
|
|
5
|
-
// failure it falls through to the next. The final injector never fails.
|
|
6
|
-
//
|
|
7
|
-
// This change ships only the Claude Code passive injector (append to the queue a
|
|
8
|
-
// running `/loop` drains) plus the never-fail paste fallback. Active injectors and
|
|
9
|
-
// other agents' injectors are follow-ups behind this same interface.
|
|
10
|
-
import { appendFile, mkdir } from "node:fs/promises";
|
|
11
|
-
import { existsSync } from "node:fs";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
/** Walk the injector chain; first available that accepts wins, else fall through. */
|
|
14
|
-
export function createDispatcher(injectors, onLog = () => undefined) {
|
|
15
|
-
return {
|
|
16
|
-
async dispatch(a) {
|
|
17
|
-
for (const inj of injectors) {
|
|
18
|
-
try {
|
|
19
|
-
if (!(await inj.isAvailable()))
|
|
20
|
-
continue;
|
|
21
|
-
await inj.inject(a);
|
|
22
|
-
onLog(`annotation ${a.id} → ${inj.name}`);
|
|
23
|
-
return inj.name;
|
|
24
|
-
}
|
|
25
|
-
catch (err) {
|
|
26
|
-
onLog(`injector ${inj.name} failed: ${err.message}; falling back`);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
// Unreachable when a never-fail fallback (pasteInjector) terminates the chain.
|
|
30
|
-
throw new Error("no injector accepted the annotation");
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Claude Code passive injector: append the annotation to a queue a running `/loop`
|
|
36
|
-
* drains. Available only when the queue dir exists (the loop has been set up), so a
|
|
37
|
-
* project with no loop configured falls through to the fallback.
|
|
38
|
-
*/
|
|
39
|
-
export function claudeCodeInjector(queuePath) {
|
|
40
|
-
return {
|
|
41
|
-
name: "claude-code",
|
|
42
|
-
async isAvailable() {
|
|
43
|
-
return existsSync(path.dirname(queuePath));
|
|
44
|
-
},
|
|
45
|
-
async inject(a) {
|
|
46
|
-
await mkdir(path.dirname(queuePath), { recursive: true });
|
|
47
|
-
const line = JSON.stringify({
|
|
48
|
-
id: a.id,
|
|
49
|
-
seq: a.seq,
|
|
50
|
-
message: a.message,
|
|
51
|
-
screenshot: a.screenshotPath,
|
|
52
|
-
spec: a.spec,
|
|
53
|
-
}) + "\n";
|
|
54
|
-
await appendFile(queuePath, line, "utf8");
|
|
55
|
-
},
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
/**
|
|
59
|
-
* Universal never-fail fallback. When no zero-action injector accepted the
|
|
60
|
-
* annotation, stage a ready prompt so the worst case is a single paste.
|
|
61
|
-
*/
|
|
62
|
-
export function pasteInjector(onReady) {
|
|
63
|
-
return {
|
|
64
|
-
name: "paste",
|
|
65
|
-
async isAvailable() {
|
|
66
|
-
return true;
|
|
67
|
-
},
|
|
68
|
-
async inject(a) {
|
|
69
|
-
onReady(`Review annotation ${a.id}: ${a.message}\nScreenshot: ${a.screenshotPath}`);
|
|
70
|
-
},
|
|
71
|
-
};
|
|
72
|
-
}
|