@nimrobo/wand-web 0.1.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.
@@ -0,0 +1,191 @@
1
+ export const styles = `
2
+ .wand-launcher,
3
+ .wand-panel,
4
+ .wand-frame,
5
+ .wand-toast {
6
+ box-sizing: border-box;
7
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
8
+ }
9
+ .wand-launcher {
10
+ position: fixed;
11
+ right: 18px;
12
+ bottom: 18px;
13
+ z-index: 2147483647;
14
+ width: 42px;
15
+ height: 42px;
16
+ border: 1px solid rgba(17, 24, 39, 0.15);
17
+ border-radius: 999px;
18
+ background: rgba(17, 24, 39, 0.94);
19
+ color: white;
20
+ display: inline-flex;
21
+ align-items: center;
22
+ justify-content: center;
23
+ cursor: pointer;
24
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
25
+ }
26
+ .wand-launcher svg { width: 18px; height: 18px; }
27
+ .wand-frame {
28
+ position: fixed;
29
+ z-index: 2147483645;
30
+ pointer-events: none;
31
+ border: 2px solid #3c7ce6;
32
+ background: rgba(60, 124, 230, 0.08);
33
+ box-shadow: 0 0 0 1px rgba(60, 124, 230, 0.28);
34
+ transition: left 120ms ease-out, top 120ms ease-out, width 120ms ease-out, height 120ms ease-out;
35
+ }
36
+ .wand-panel {
37
+ position: fixed;
38
+ right: 18px;
39
+ bottom: 72px;
40
+ z-index: 2147483647;
41
+ width: min(360px, calc(100vw - 36px));
42
+ border: 1px solid rgba(17, 24, 39, 0.14);
43
+ border-radius: 8px;
44
+ background: rgba(255, 255, 255, 0.98);
45
+ color: #111827;
46
+ box-shadow: 0 18px 42px rgba(0, 0, 0, 0.18);
47
+ overflow: hidden;
48
+ }
49
+ .wand-panel-header,
50
+ .wand-panel-section {
51
+ padding: 12px;
52
+ }
53
+ .wand-panel-header {
54
+ display: flex;
55
+ align-items: center;
56
+ justify-content: space-between;
57
+ border-bottom: 1px solid rgba(17, 24, 39, 0.08);
58
+ font-size: 13px;
59
+ font-weight: 600;
60
+ }
61
+ .wand-panel textarea {
62
+ box-sizing: border-box;
63
+ width: 100%;
64
+ max-width: 100%;
65
+ min-height: 92px;
66
+ resize: vertical;
67
+ border: 1px solid rgba(17, 24, 39, 0.16);
68
+ border-radius: 6px;
69
+ padding: 9px 10px;
70
+ color: #111827;
71
+ background: white;
72
+ font: inherit;
73
+ }
74
+ .wand-actions {
75
+ display: flex;
76
+ gap: 8px;
77
+ margin-top: 10px;
78
+ }
79
+ .wand-agent-picker {
80
+ display: grid;
81
+ gap: 5px;
82
+ margin-top: 10px;
83
+ }
84
+ .wand-agent-picker select {
85
+ box-sizing: border-box;
86
+ width: 100%;
87
+ border: 1px solid rgba(17, 24, 39, 0.16);
88
+ border-radius: 6px;
89
+ padding: 8px 10px;
90
+ color: #111827;
91
+ background: white;
92
+ font: inherit;
93
+ }
94
+ .wand-agent-picker select:disabled {
95
+ color: rgba(17, 24, 39, 0.42);
96
+ background: #f8fafc;
97
+ }
98
+ .wand-actions button,
99
+ .wand-queue-item button {
100
+ border: 0;
101
+ border-radius: 6px;
102
+ padding: 8px 10px;
103
+ font: inherit;
104
+ cursor: pointer;
105
+ }
106
+ .wand-actions button:disabled {
107
+ cursor: wait;
108
+ opacity: 0.72;
109
+ }
110
+ .wand-actions button:first-child {
111
+ background: #111827;
112
+ color: white;
113
+ }
114
+ .wand-actions button:last-child,
115
+ .wand-queue-item button {
116
+ background: #eef2f7;
117
+ color: #111827;
118
+ }
119
+ .wand-queue {
120
+ border-top: 1px solid rgba(17, 24, 39, 0.08);
121
+ }
122
+ .wand-queue-item {
123
+ display: grid;
124
+ gap: 8px;
125
+ padding: 10px 12px;
126
+ border-top: 1px solid rgba(17, 24, 39, 0.06);
127
+ font-size: 12px;
128
+ }
129
+ .wand-queue-item:first-child { border-top: 0; }
130
+ .wand-queue-controls { display: flex; gap: 8px; }
131
+ .wand-agent-run {
132
+ background: rgba(60, 124, 230, 0.08);
133
+ }
134
+ .wand-run-header {
135
+ display: flex;
136
+ align-items: center;
137
+ gap: 8px;
138
+ }
139
+ .wand-run-title {
140
+ min-width: 0;
141
+ flex: 1;
142
+ overflow-wrap: anywhere;
143
+ }
144
+ .wand-run-header button {
145
+ flex: none;
146
+ }
147
+ .wand-run-message {
148
+ overflow: hidden;
149
+ margin: 0;
150
+ color: rgba(17, 24, 39, 0.76);
151
+ font: 11px/1.4 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
152
+ white-space: nowrap;
153
+ text-overflow: ellipsis;
154
+ }
155
+ .wand-sending {
156
+ display: inline-flex;
157
+ align-items: center;
158
+ gap: 7px;
159
+ }
160
+ .wand-spinner {
161
+ width: 12px;
162
+ height: 12px;
163
+ border: 2px solid rgba(255, 255, 255, 0.38);
164
+ border-top-color: currentColor;
165
+ border-radius: 999px;
166
+ animation: wand-spin 700ms linear infinite;
167
+ }
168
+ @keyframes wand-spin {
169
+ to { transform: rotate(360deg); }
170
+ }
171
+ @media (prefers-reduced-motion: reduce) {
172
+ .wand-spinner { animation: none; }
173
+ }
174
+ .wand-muted {
175
+ color: rgba(17, 24, 39, 0.58);
176
+ font-size: 12px;
177
+ }
178
+ .wand-toast {
179
+ position: fixed;
180
+ right: 18px;
181
+ bottom: 72px;
182
+ z-index: 2147483647;
183
+ max-width: min(320px, calc(100vw - 36px));
184
+ border-radius: 6px;
185
+ background: rgba(17, 24, 39, 0.94);
186
+ color: white;
187
+ padding: 9px 11px;
188
+ font-size: 12px;
189
+ box-shadow: 0 12px 28px rgba(0, 0, 0, 0.22);
190
+ }
191
+ `;
@@ -0,0 +1,7 @@
1
+ import { type AdapterKind } from "@nimrobo/superconnector";
2
+ export type AgentAdapterConfig = {
3
+ availableAdapters: AdapterKind[];
4
+ selectedAdapter: AdapterKind | null;
5
+ };
6
+ export declare function getAgentAdapterConfig(projectRoot: string): AgentAdapterConfig;
7
+ export declare function savePreferredAdapter(projectRoot: string, adapter: AdapterKind): AgentAdapterConfig;
@@ -0,0 +1,63 @@
1
+ import { accessSync, constants, statSync } from "node:fs";
2
+ import { delimiter, isAbsolute, join } from "node:path";
3
+ import { ADAPTER_KINDS, detectAdapter, } from "@nimrobo/superconnector";
4
+ import { localConfigPath, readConfig, resolveConfig, writeConfig, } from "@nimrobo/superconnector/config";
5
+ const adapterCommands = {
6
+ "claude-code": { env: "CLAUDE_BIN", fallback: "claude" },
7
+ opencode: { env: "OPENCODE_BIN", fallback: "opencode" },
8
+ codex: { env: "CODEX_BIN", fallback: "codex" },
9
+ };
10
+ export function getAgentAdapterConfig(projectRoot) {
11
+ const availableAdapters = ADAPTER_KINDS.filter((adapter) => adapterBinaryAvailable(adapter));
12
+ const preferredAdapter = resolveConfig(projectRoot).merged.preferredAdapter;
13
+ const resolvedAdapter = preferredAdapter ?? detectAdapter(projectRoot);
14
+ return {
15
+ availableAdapters: [...availableAdapters],
16
+ selectedAdapter: resolvedAdapter && availableAdapters.includes(resolvedAdapter) ? resolvedAdapter : null,
17
+ };
18
+ }
19
+ export function savePreferredAdapter(projectRoot, adapter) {
20
+ if (!ADAPTER_KINDS.includes(adapter)) {
21
+ throw new Error(`Unsupported adapter: ${adapter}`);
22
+ }
23
+ if (!adapterBinaryAvailable(adapter)) {
24
+ throw new Error(`Adapter binary is not available: ${adapter}`);
25
+ }
26
+ const path = localConfigPath(projectRoot);
27
+ const current = readConfig(path) ?? {};
28
+ writeConfig(path, { ...current, preferredAdapter: adapter });
29
+ return getAgentAdapterConfig(projectRoot);
30
+ }
31
+ function adapterBinaryAvailable(adapter) {
32
+ const spec = adapterCommands[adapter];
33
+ const command = process.env[spec.env] ?? spec.fallback;
34
+ return isExecutableAvailable(command);
35
+ }
36
+ function isExecutableAvailable(command) {
37
+ if (!command)
38
+ return false;
39
+ if (isPathLike(command))
40
+ return canExecute(command);
41
+ const path = process.env.PATH ?? "";
42
+ for (const dir of path.split(delimiter)) {
43
+ if (!dir)
44
+ continue;
45
+ if (canExecute(join(dir, command)))
46
+ return true;
47
+ }
48
+ return false;
49
+ }
50
+ function isPathLike(command) {
51
+ return isAbsolute(command) || command.includes("/") || command.includes("\\");
52
+ }
53
+ function canExecute(filePath) {
54
+ try {
55
+ if (!statSync(filePath).isFile())
56
+ return false;
57
+ accessSync(filePath, constants.X_OK);
58
+ return true;
59
+ }
60
+ catch {
61
+ return false;
62
+ }
63
+ }
@@ -0,0 +1,12 @@
1
+ import { type AdapterKind, type AgentMessage } from "@nimrobo/superconnector";
2
+ import type { PersistedSelectionCapture } from "./types.js";
3
+ export type AgentSendResult = {
4
+ sessionId: string | null;
5
+ };
6
+ export type AgentSendOptions = {
7
+ adapter?: AdapterKind;
8
+ signal?: AbortSignal;
9
+ onMessage?(message: AgentMessage): void;
10
+ };
11
+ export declare function sendToAgent(projectRoot: string, prompt: string, context: PersistedSelectionCapture, options?: AgentSendOptions): Promise<AgentSendResult>;
12
+ export declare function buildAgentPrompt(prompt: string, context: PersistedSelectionCapture): string;
@@ -0,0 +1,35 @@
1
+ import { createSuperconnector, } from "@nimrobo/superconnector";
2
+ export async function sendToAgent(projectRoot, prompt, context, options = {}) {
3
+ const connector = createSuperconnector(options.adapter !== undefined ? { adapter: options.adapter } : {});
4
+ let sessionId = null;
5
+ const generatedPrompt = buildAgentPrompt(prompt, context);
6
+ for await (const message of connector.spawn({
7
+ prompt: generatedPrompt,
8
+ appId: "wand-web",
9
+ permissionMode: "acceptEdits",
10
+ signal: options.signal,
11
+ })) {
12
+ if (message.sessionId)
13
+ sessionId = message.sessionId;
14
+ options.onMessage?.(message);
15
+ }
16
+ return { sessionId };
17
+ }
18
+ export function buildAgentPrompt(prompt, context) {
19
+ return [
20
+ "The user selected a UI element in their running web app and requested a code change.",
21
+ "",
22
+ `User request: ${prompt}`,
23
+ "",
24
+ "Read these local files before editing:",
25
+ `- Selection context JSON: ${context.contextPath}`,
26
+ `- Selected element HTML: ${context.elementHtmlPath}`,
27
+ `- Viewport screenshot: ${context.viewportScreenshotPath}`,
28
+ `- Selected element crop: ${context.elementScreenshotPath}`,
29
+ "",
30
+ `Current page: ${context.url}`,
31
+ `Selected element: <${context.tagName.toLowerCase()}> ${context.selector}`,
32
+ "",
33
+ "Use the selection context and screenshots to identify the relevant source and make the requested edit.",
34
+ ].join("\n");
35
+ }
@@ -0,0 +1,9 @@
1
+ import { type AgentRunner } from "./runs.js";
2
+ export type WandServer = {
3
+ url: string;
4
+ close(): Promise<void>;
5
+ };
6
+ export type StartWandServerOptions = {
7
+ agentRunner?: AgentRunner;
8
+ };
9
+ export declare function startWandServer(projectRoot: string, port?: number, options?: StartWandServerOptions): Promise<WandServer>;
@@ -0,0 +1,166 @@
1
+ import { createServer } from "node:http";
2
+ import { access, readFile } from "node:fs/promises";
3
+ import { isAdapterKind } from "@nimrobo/superconnector";
4
+ import { getAgentAdapterConfig, savePreferredAdapter } from "./agent-config.js";
5
+ import { sendToAgent } from "./agent.js";
6
+ import { ActiveRunManager } from "./runs.js";
7
+ import { WandStorage } from "./storage.js";
8
+ export async function startWandServer(projectRoot, port = 0, options = {}) {
9
+ const storage = new WandStorage(projectRoot);
10
+ const runs = new ActiveRunManager(projectRoot, options.agentRunner ?? sendToAgent);
11
+ const closeRunStreams = new Set();
12
+ await storage.ensure();
13
+ const server = createServer(async (req, res) => {
14
+ try {
15
+ setCors(res);
16
+ if (req.method === "OPTIONS") {
17
+ res.writeHead(204);
18
+ res.end();
19
+ return;
20
+ }
21
+ const url = new URL(req.url ?? "/", "http://localhost");
22
+ if (req.method === "GET" && url.pathname === "/health") {
23
+ json(res, 200, { ok: true });
24
+ return;
25
+ }
26
+ if (req.method === "GET" && url.pathname === "/client.js") {
27
+ javascript(res, 200, await readClientBundle());
28
+ return;
29
+ }
30
+ if (req.method === "GET" && url.pathname === "/api/queue") {
31
+ json(res, 200, await storage.listQueue());
32
+ return;
33
+ }
34
+ if (req.method === "GET" && url.pathname === "/api/agent-config") {
35
+ json(res, 200, getAgentAdapterConfig(projectRoot));
36
+ return;
37
+ }
38
+ if (req.method === "GET" && url.pathname === "/api/runs") {
39
+ json(res, 200, runs.list());
40
+ return;
41
+ }
42
+ if (req.method === "GET" && url.pathname === "/api/runs/events") {
43
+ res.writeHead(200, {
44
+ "content-type": "text/event-stream",
45
+ "cache-control": "no-cache",
46
+ connection: "keep-alive",
47
+ });
48
+ res.write("retry: 1000\n\n");
49
+ let closed = false;
50
+ const unsubscribe = runs.subscribe((activeRuns) => writeRunEvent(res, activeRuns));
51
+ const close = () => {
52
+ if (closed)
53
+ return;
54
+ closed = true;
55
+ unsubscribe();
56
+ closeRunStreams.delete(close);
57
+ res.end();
58
+ };
59
+ closeRunStreams.add(close);
60
+ req.on("close", close);
61
+ return;
62
+ }
63
+ if (req.method === "POST" && url.pathname === "/api/queue") {
64
+ const body = await readJson(req);
65
+ const context = await storage.persistCapture(body.capture);
66
+ json(res, 201, await storage.queue(body.prompt, context));
67
+ return;
68
+ }
69
+ if (req.method === "PUT" && url.pathname === "/api/agent-config") {
70
+ const body = await readJson(req);
71
+ json(res, 200, savePreferredAdapter(projectRoot, body.adapter));
72
+ return;
73
+ }
74
+ if (req.method === "DELETE" && url.pathname.startsWith("/api/queue/")) {
75
+ const id = decodeURIComponent(url.pathname.split("/").pop() ?? "");
76
+ await storage.removeQueued(id);
77
+ json(res, 200, { ok: true });
78
+ return;
79
+ }
80
+ if (req.method === "DELETE" && url.pathname.startsWith("/api/runs/")) {
81
+ const id = decodeURIComponent(url.pathname.split("/").pop() ?? "");
82
+ json(res, 200, { ok: await runs.stop(id) });
83
+ return;
84
+ }
85
+ if (req.method === "POST" && url.pathname === "/api/send") {
86
+ const body = await readJson(req);
87
+ const context = body.context ?? (await storage.persistCapture(required(body.capture)));
88
+ json(res, 202, await runs.start(body.prompt, context, optionalAdapter(body.adapter)));
89
+ return;
90
+ }
91
+ json(res, 404, { error: "not_found" });
92
+ }
93
+ catch (error) {
94
+ json(res, 500, {
95
+ error: error instanceof Error ? error.message : String(error),
96
+ });
97
+ }
98
+ });
99
+ await new Promise((resolve) => server.listen(port, "127.0.0.1", resolve));
100
+ const address = server.address();
101
+ return {
102
+ url: `http://127.0.0.1:${address.port}`,
103
+ close: async () => {
104
+ for (const close of closeRunStreams)
105
+ close();
106
+ await runs.stopAll();
107
+ await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
108
+ },
109
+ };
110
+ }
111
+ function required(value) {
112
+ if (value === undefined)
113
+ throw new Error("Missing required capture payload.");
114
+ return value;
115
+ }
116
+ function optionalAdapter(value) {
117
+ if (value === undefined)
118
+ return undefined;
119
+ if (typeof value !== "string" || !isAdapterKind(value)) {
120
+ throw new Error(`Unsupported adapter: ${String(value)}`);
121
+ }
122
+ return value;
123
+ }
124
+ async function readJson(req) {
125
+ const chunks = [];
126
+ for await (const chunk of req)
127
+ chunks.push(Buffer.from(chunk));
128
+ return JSON.parse(Buffer.concat(chunks).toString("utf8"));
129
+ }
130
+ function setCors(res) {
131
+ res.setHeader("Access-Control-Allow-Origin", "*");
132
+ res.setHeader("Access-Control-Allow-Headers", "content-type");
133
+ res.setHeader("Access-Control-Allow-Methods", "GET,POST,PUT,DELETE,OPTIONS");
134
+ }
135
+ function json(res, status, payload) {
136
+ res.writeHead(status, { "content-type": "application/json" });
137
+ res.end(`${JSON.stringify(payload)}\n`);
138
+ }
139
+ function javascript(res, status, source) {
140
+ res.writeHead(status, {
141
+ "content-type": "text/javascript; charset=utf-8",
142
+ "cache-control": "no-store",
143
+ });
144
+ res.end(source);
145
+ }
146
+ function writeRunEvent(res, runs) {
147
+ res.write(`event: runs\ndata: ${JSON.stringify(runs)}\n\n`);
148
+ }
149
+ async function readClientBundle() {
150
+ for (const url of clientBundleCandidates()) {
151
+ try {
152
+ await access(url);
153
+ return await readFile(url, "utf8");
154
+ }
155
+ catch {
156
+ // Try the next runtime layout.
157
+ }
158
+ }
159
+ throw new Error('Missing Wand client bundle. Run "npm run build" before "wand run dev".');
160
+ }
161
+ function clientBundleCandidates() {
162
+ return [
163
+ new URL("../client.js", import.meta.url),
164
+ new URL("../../dist/client.js", import.meta.url),
165
+ ];
166
+ }
@@ -0,0 +1,21 @@
1
+ import { type AgentSendOptions, type AgentSendResult } from "./agent.js";
2
+ import type { AdapterKind } from "@nimrobo/superconnector";
3
+ import type { ActiveAgentRun, PersistedSelectionCapture } from "./types.js";
4
+ export type AgentRunner = (projectRoot: string, prompt: string, context: PersistedSelectionCapture, options?: AgentSendOptions) => Promise<AgentSendResult>;
5
+ type ActiveRunsListener = (runs: ActiveAgentRun[]) => void;
6
+ export declare class ActiveRunManager {
7
+ private readonly projectRoot;
8
+ private readonly runner;
9
+ private readonly entries;
10
+ private readonly listeners;
11
+ constructor(projectRoot: string, runner?: AgentRunner);
12
+ start(prompt: string, context: PersistedSelectionCapture, adapter?: AdapterKind): Promise<ActiveAgentRun>;
13
+ list(): ActiveAgentRun[];
14
+ subscribe(listener: ActiveRunsListener): () => void;
15
+ stop(id: string): Promise<boolean>;
16
+ stopAll(): Promise<void>;
17
+ private execute;
18
+ private markStarted;
19
+ private notify;
20
+ }
21
+ export {};
@@ -0,0 +1,119 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { sendToAgent } from "./agent.js";
3
+ export class ActiveRunManager {
4
+ projectRoot;
5
+ runner;
6
+ entries = new Map();
7
+ listeners = new Set();
8
+ constructor(projectRoot, runner = sendToAgent) {
9
+ this.projectRoot = projectRoot;
10
+ this.runner = runner;
11
+ }
12
+ async start(prompt, context, adapter) {
13
+ const run = {
14
+ id: randomUUID(),
15
+ createdAt: new Date().toISOString(),
16
+ prompt,
17
+ context,
18
+ ...(adapter !== undefined ? { adapter } : {}),
19
+ sessionId: null,
20
+ latestMessage: null,
21
+ };
22
+ const controller = new AbortController();
23
+ const startup = deferred();
24
+ const entry = {
25
+ run,
26
+ controller,
27
+ done: Promise.resolve(),
28
+ started: false,
29
+ startup: startup.promise,
30
+ resolveStartup: startup.resolve,
31
+ rejectStartup: startup.reject,
32
+ };
33
+ this.entries.set(run.id, entry);
34
+ entry.done = this.execute(entry);
35
+ await entry.startup;
36
+ return snapshot(run);
37
+ }
38
+ list() {
39
+ return Array.from(this.entries.values())
40
+ .filter(({ started }) => started)
41
+ .map(({ run }) => snapshot(run))
42
+ .sort((a, b) => a.createdAt.localeCompare(b.createdAt));
43
+ }
44
+ subscribe(listener) {
45
+ this.listeners.add(listener);
46
+ listener(this.list());
47
+ return () => this.listeners.delete(listener);
48
+ }
49
+ async stop(id) {
50
+ const entry = this.entries.get(id);
51
+ if (!entry)
52
+ return false;
53
+ entry.controller.abort();
54
+ await entry.done;
55
+ return true;
56
+ }
57
+ async stopAll() {
58
+ await Promise.all(Array.from(this.entries.keys(), (id) => this.stop(id)));
59
+ }
60
+ async execute(entry) {
61
+ try {
62
+ const result = await this.runner(this.projectRoot, entry.run.prompt, entry.run.context, {
63
+ ...(entry.run.adapter !== undefined ? { adapter: entry.run.adapter } : {}),
64
+ signal: entry.controller.signal,
65
+ onMessage: (message) => {
66
+ if (message.sessionId)
67
+ entry.run.sessionId = message.sessionId;
68
+ entry.run.latestMessage = message.content;
69
+ const alreadyStarted = entry.started;
70
+ this.markStarted(entry);
71
+ if (alreadyStarted)
72
+ this.notify();
73
+ },
74
+ });
75
+ entry.run.sessionId = result.sessionId ?? entry.run.sessionId;
76
+ this.markStarted(entry);
77
+ }
78
+ catch (error) {
79
+ if (!entry.started)
80
+ entry.rejectStartup(error);
81
+ // The live row is removed for both failures and user-initiated stops.
82
+ }
83
+ finally {
84
+ this.entries.delete(entry.run.id);
85
+ if (entry.started)
86
+ this.notify();
87
+ }
88
+ }
89
+ markStarted(entry) {
90
+ if (entry.started)
91
+ return;
92
+ entry.started = true;
93
+ entry.resolveStartup();
94
+ this.notify();
95
+ }
96
+ notify() {
97
+ const runs = this.list();
98
+ for (const listener of this.listeners) {
99
+ try {
100
+ listener(runs);
101
+ }
102
+ catch {
103
+ // Stream clients are isolated from run execution.
104
+ }
105
+ }
106
+ }
107
+ }
108
+ function snapshot(run) {
109
+ return { ...run };
110
+ }
111
+ function deferred() {
112
+ let resolve;
113
+ let reject;
114
+ const promise = new Promise((resolvePromise, rejectPromise) => {
115
+ resolve = resolvePromise;
116
+ reject = rejectPromise;
117
+ });
118
+ return { promise, resolve, reject };
119
+ }
@@ -0,0 +1,13 @@
1
+ import type { PersistedSelectionCapture, QueuedRequest, SelectionCapture } from "./types.js";
2
+ export declare class WandStorage {
3
+ readonly root: string;
4
+ readonly contextDir: string;
5
+ readonly queueDir: string;
6
+ readonly tmpDir: string;
7
+ constructor(projectRoot: string);
8
+ ensure(): Promise<void>;
9
+ persistCapture(capture: SelectionCapture): Promise<PersistedSelectionCapture>;
10
+ queue(prompt: string, context: PersistedSelectionCapture): Promise<QueuedRequest>;
11
+ listQueue(): Promise<QueuedRequest[]>;
12
+ removeQueued(id: string): Promise<void>;
13
+ }