@hansenexus/hud 0.3.0 → 0.4.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.
Files changed (54) hide show
  1. package/README.md +186 -4
  2. package/dist/access-CNvJOwKM.d.ts +54 -0
  3. package/dist/capabilities-CjulXFQX.js +189 -0
  4. package/dist/{define-hud-BHQR8Lbv.d.ts → define-hud-BBzsSl8b.d.ts} +6 -1
  5. package/dist/env-DfxpPfs9.js +28 -0
  6. package/dist/env-vEjyJfPW.d.ts +17 -0
  7. package/dist/force-state-BaqWsmrf.js +153 -0
  8. package/dist/force-state-CCqoTfVB.d.ts +23 -0
  9. package/dist/handler-WmE4Lu7l.d.ts +39 -0
  10. package/dist/index-BYsdS-7d.d.ts +55 -0
  11. package/dist/index.d.ts +6 -36
  12. package/dist/index.js +119 -202
  13. package/dist/marker-C66LxOhJ.d.ts +9 -0
  14. package/dist/marker-CMrvxPmS.d.ts +12 -0
  15. package/dist/marker-DjeGvL-F.js +9 -0
  16. package/dist/marker-uZYPOJ4M.js +12 -0
  17. package/dist/markers.d.ts +3 -0
  18. package/dist/markers.js +3 -0
  19. package/dist/next.d.ts +3 -18
  20. package/dist/next.js +3 -28
  21. package/dist/payload-CLKDVZWl.d.ts +68 -0
  22. package/dist/plugins/access/server.d.ts +122 -0
  23. package/dist/plugins/access/server.js +339 -0
  24. package/dist/plugins/access.d.ts +41 -0
  25. package/dist/plugins/access.js +250 -0
  26. package/dist/plugins/agent/server.d.ts +129 -0
  27. package/dist/plugins/agent/server.js +429 -0
  28. package/dist/plugins/agent.d.ts +37 -0
  29. package/dist/plugins/agent.js +411 -0
  30. package/dist/plugins/design.d.ts +125 -0
  31. package/dist/plugins/design.js +619 -0
  32. package/dist/plugins/observe.d.ts +64 -0
  33. package/dist/plugins/observe.js +617 -0
  34. package/dist/plugins/ops/server.d.ts +82 -0
  35. package/dist/plugins/ops/server.js +380 -0
  36. package/dist/plugins/ops.d.ts +26 -0
  37. package/dist/plugins/ops.js +362 -0
  38. package/dist/plugins/vitals.js +3 -86
  39. package/dist/{plugins-1QmTmZyL.d.ts → plugins-CQ_0w1ot.d.ts} +39 -1
  40. package/dist/{plugins-CzUiTn_r.js → plugins-DVy_vaYr.js} +3 -1
  41. package/dist/review.d.ts +181 -0
  42. package/dist/review.js +1017 -0
  43. package/dist/selector-NSYSe7AZ.js +28 -0
  44. package/dist/server.d.ts +3 -39
  45. package/dist/server.js +1 -1
  46. package/dist/shortcut-U6ftVY06.js +222 -0
  47. package/dist/state-boundary.d.ts +26 -0
  48. package/dist/state-boundary.js +61 -0
  49. package/dist/styles--slk86ZQ.js +332 -0
  50. package/dist/types-DADBRiGk.d.ts +50 -0
  51. package/dist/types-QJUajwWq.js +35 -0
  52. package/dist/types-f3jlNxzw.d.ts +92 -0
  53. package/dist/vitals-panel-Dvc5dnR-.js +87 -0
  54. package/package.json +23 -3
@@ -0,0 +1,429 @@
1
+ import { i as definePlugin } from "../../plugins-DVy_vaYr.js";
2
+ //#region src/plugins/agent/prompt.ts
3
+ /** The task text every provider receives: grab context first, then the instruction. */
4
+ function dispatchPrompt({ note, payload, hints = [] }) {
5
+ const instruction = note.trim() || "Improve the grabbed element (see context).";
6
+ if (!payload) return [
7
+ `Instruction: ${instruction}`,
8
+ "",
9
+ "Task: apply the instruction and run the project's checks."
10
+ ].join("\n");
11
+ const { component, anchor } = payload;
12
+ const lines = [
13
+ `HUD dispatch from ${payload.app}, page ${payload.url || payload.route}.`,
14
+ `Grabbed element: ${anchor.label}`,
15
+ `Selector (from <body>): ${anchor.selector}`
16
+ ];
17
+ if (component) {
18
+ const owners = component.owners.length > 0 ? ` inside ${component.owners.join(" < ")}` : "";
19
+ lines.push(`React component: <${component.name}>${owners}`);
20
+ if (component.source) lines.push(`Source hint (from React; line approximate): ${component.source}`);
21
+ }
22
+ if (hints.length > 0) lines.push(`Component manifest matches (best effort, verify): ${hints.map((h) => h.path).join(", ")}`);
23
+ if (!component?.source && hints.length === 0) lines.push("No source hint: locate the component from the route and selector.");
24
+ lines.push(`Viewport: ${payload.viewport.width}×${payload.viewport.height}`, `Instruction: ${instruction}`, "", "Task: locate the element's source, apply the instruction, and run the project's checks.");
25
+ return lines.join("\n");
26
+ }
27
+ //#endregion
28
+ //#region src/plugins/agent/providers/claude-code.ts
29
+ /** A transcript touched within this window counts as a running session. */
30
+ const ACTIVE_MS = 12e4;
31
+ /** Enough of a transcript to find its summary or first prompt without reading megabytes. */
32
+ const HEAD_BYTES = 65536;
33
+ const defaultArgs = (prompt) => [
34
+ "-p",
35
+ prompt,
36
+ "--permission-mode",
37
+ "acceptEdits"
38
+ ];
39
+ /** Claude Code keeps transcripts in `~/.claude/projects/<cwd with every non-alphanumeric as ->`. */
40
+ function projectDirName(cwd) {
41
+ return cwd.replace(/[^a-zA-Z0-9]/g, "-");
42
+ }
43
+ /** Summary line, else first plain user prompt, from the head of a transcript. */
44
+ function transcriptTitle(head) {
45
+ let prompt;
46
+ for (const line of head.split("\n")) {
47
+ let entry;
48
+ try {
49
+ entry = JSON.parse(line);
50
+ } catch {
51
+ continue;
52
+ }
53
+ if (entry.type === "summary" && typeof entry.summary === "string") return entry.summary;
54
+ if (prompt || entry.type !== "user") continue;
55
+ const content = entry.message?.content;
56
+ const text = typeof content === "string" ? content : Array.isArray(content) ? content.find((c) => c?.type === "text")?.text : void 0;
57
+ if (typeof text === "string" && text.trim() && !text.trimStart().startsWith("<")) prompt = text.trim().split("\n")[0];
58
+ }
59
+ return prompt?.slice(0, 120);
60
+ }
61
+ /**
62
+ * Node built-ins, loaded on first use. A static import would make the whole
63
+ * server entry unloadable wherever `node:` modules are missing (an edge
64
+ * route, the playground's MSW worker), even for apps that only use `hermes()`.
65
+ */
66
+ async function loadNode() {
67
+ const [childProcess, fs, fsp, os, path, proc] = await Promise.all([
68
+ import("node:child_process"),
69
+ import("node:fs"),
70
+ import("node:fs/promises"),
71
+ import("node:os"),
72
+ import("node:path"),
73
+ import("node:process")
74
+ ]);
75
+ return {
76
+ childProcess,
77
+ fs,
78
+ fsp,
79
+ os,
80
+ path,
81
+ env: proc.env
82
+ };
83
+ }
84
+ async function readHead({ fsp }, path) {
85
+ const file = await fsp.open(path, "r");
86
+ try {
87
+ const buffer = new Uint8Array(HEAD_BYTES);
88
+ const { bytesRead } = await file.read(buffer, 0, HEAD_BYTES, 0);
89
+ return new TextDecoder().decode(buffer.subarray(0, bytesRead));
90
+ } finally {
91
+ await file.close();
92
+ }
93
+ }
94
+ /**
95
+ * Dispatches to a local Claude Code: each run is `claude -p <prompt>` spawned
96
+ * (argv, no shell) in `cwd`, detached so it outlives a dev-server reload, with
97
+ * its output in a log file. Sessions are the runs started here plus the most
98
+ * recent Claude Code transcripts of `cwd`.
99
+ */
100
+ function claudeCode({ cwd: cwdOption, command = "claude", args = defaultArgs, label = "Claude Code (local)", logDir: logDirOption, transcripts = 8, home: homeOption, spawn: spawnOption } = {}) {
101
+ const runs = /* @__PURE__ */ new Map();
102
+ let node;
103
+ const nodeApi = () => {
104
+ node ??= loadNode();
105
+ return node;
106
+ };
107
+ const cwdOf = ({ path }) => cwdOption ?? path.resolve();
108
+ async function listTranscripts() {
109
+ const api = await nodeApi();
110
+ const { fsp, path } = api;
111
+ const home = homeOption ?? api.os.homedir();
112
+ const dir = path.join(home, ".claude", "projects", projectDirName(cwdOf(api)));
113
+ let names;
114
+ try {
115
+ names = (await fsp.readdir(dir)).filter((name) => name.endsWith(".jsonl"));
116
+ } catch {
117
+ return [];
118
+ }
119
+ const recent = (await Promise.all(names.map(async (name) => {
120
+ const file = path.join(dir, name);
121
+ return {
122
+ file,
123
+ mtime: (await fsp.stat(file)).mtimeMs
124
+ };
125
+ }))).sort((a, b) => b.mtime - a.mtime).slice(0, transcripts);
126
+ return Promise.all(recent.map(async ({ file, mtime }) => {
127
+ const id = path.basename(file, ".jsonl");
128
+ return {
129
+ provider: "claude-code",
130
+ id,
131
+ title: transcriptTitle(await readHead(api, file).catch(() => "")) ?? id.slice(0, 8),
132
+ status: Date.now() - mtime < ACTIVE_MS ? "running" : "idle",
133
+ updatedAt: new Date(mtime).toISOString()
134
+ };
135
+ }));
136
+ }
137
+ return {
138
+ id: "claude-code",
139
+ label,
140
+ description: cwdOption ? `claude -p in ${cwdOption}` : "claude -p in the dev server's directory",
141
+ async dispatch(order) {
142
+ const api = await nodeApi();
143
+ const { fs, path } = api;
144
+ const logDir = logDirOption ?? path.join(api.os.tmpdir(), "hud-agent");
145
+ const spawn = spawnOption ?? api.childProcess.spawn;
146
+ await api.fsp.mkdir(logDir, { recursive: true });
147
+ const id = `run-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
148
+ const log = path.join(logDir, `${id}.log`);
149
+ const fd = fs.openSync(log, "a", 384);
150
+ let child;
151
+ try {
152
+ child = spawn(command, args(order.prompt), {
153
+ cwd: cwdOf(api),
154
+ detached: true,
155
+ stdio: [
156
+ "ignore",
157
+ fd,
158
+ fd
159
+ ],
160
+ env: api.env
161
+ });
162
+ await new Promise((resolve, reject) => {
163
+ child.once("spawn", resolve);
164
+ child.once("error", reject);
165
+ });
166
+ } finally {
167
+ fs.closeSync(fd);
168
+ }
169
+ const run = {
170
+ id,
171
+ title: order.note.trim().split("\n")[0] || order.payload?.anchor.label || "dispatch",
172
+ status: "running",
173
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
174
+ log
175
+ };
176
+ runs.set(id, run);
177
+ child.once("exit", (code) => {
178
+ run.status = code === 0 ? "done" : "failed";
179
+ run.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
180
+ });
181
+ child.unref();
182
+ return {
183
+ runId: id,
184
+ detail: log
185
+ };
186
+ },
187
+ async sessions() {
188
+ return [...[...runs.values()].map((run) => ({
189
+ provider: "claude-code",
190
+ id: run.id,
191
+ title: run.title,
192
+ status: run.status,
193
+ startedAt: run.startedAt,
194
+ ...run.updatedAt ? { updatedAt: run.updatedAt } : {},
195
+ detail: run.log
196
+ })).reverse(), ...await listTranscripts()];
197
+ }
198
+ };
199
+ }
200
+ //#endregion
201
+ //#region src/plugins/agent/providers/hermes.ts
202
+ const STATUS = {
203
+ queued: "queued",
204
+ running: "running",
205
+ succeeded: "done",
206
+ failed: "failed"
207
+ };
208
+ /**
209
+ * Dispatches to a Hermes Agent API server: `POST /v1/runs` starts a
210
+ * background run (202 `{ run_id }`), `GET /v1/jobs` lists runs for the
211
+ * Sessions panel. The token never leaves the server.
212
+ */
213
+ function hermes({ baseUrl, token, instructions, label = "Hermes", timeoutMs = 1e4, fetch: fetchImpl = globalThis.fetch }) {
214
+ const base = baseUrl.replace(/\/+$/, "");
215
+ const bearer = () => (typeof token === "function" ? token() : token)?.trim() || void 0;
216
+ async function call(path, init) {
217
+ const auth = bearer();
218
+ if (!auth) throw new Error("no Hermes token configured");
219
+ const res = await fetchImpl(`${base}${path}`, {
220
+ ...init,
221
+ headers: {
222
+ Accept: "application/json",
223
+ Authorization: `Bearer ${auth}`,
224
+ ...init?.body ? { "Content-Type": "application/json" } : {}
225
+ },
226
+ signal: AbortSignal.timeout(timeoutMs)
227
+ });
228
+ if (!res.ok) throw new Error(`Hermes answered ${res.status}`);
229
+ return res.json();
230
+ }
231
+ return {
232
+ id: "hermes",
233
+ label,
234
+ description: "Remote Hermes Agent run",
235
+ status: () => bearer() ? { available: true } : {
236
+ available: false,
237
+ reason: "no Hermes token configured"
238
+ },
239
+ async dispatch(order) {
240
+ const body = await call("/v1/runs", {
241
+ method: "POST",
242
+ body: JSON.stringify({
243
+ input: order.prompt,
244
+ ...instructions ? { instructions } : {}
245
+ })
246
+ });
247
+ if (typeof body?.run_id !== "string" || !body.run_id) throw new Error("Hermes accepted the run but returned no run_id");
248
+ return { runId: body.run_id };
249
+ },
250
+ async sessions() {
251
+ const jobs = await call("/v1/jobs");
252
+ if (!Array.isArray(jobs)) throw new Error("Hermes /v1/jobs: expected an array");
253
+ return jobs.map((job) => {
254
+ const session = {
255
+ provider: "hermes",
256
+ id: job.id,
257
+ title: job.task?.split("\n")[0]?.slice(0, 120) || job.id,
258
+ status: STATUS[job.status] ?? "idle"
259
+ };
260
+ if (typeof job.createdAt === "number") {
261
+ session.startedAt = new Date(job.createdAt).toISOString();
262
+ if (typeof job.durationMs === "number") session.updatedAt = new Date(job.createdAt + job.durationMs).toISOString();
263
+ }
264
+ return session;
265
+ });
266
+ }
267
+ };
268
+ }
269
+ //#endregion
270
+ //#region src/plugins/agent/server.ts
271
+ function message(error) {
272
+ return error instanceof Error ? error.message : String(error);
273
+ }
274
+ /**
275
+ * A dispatch spawns an agent, so a page on another origin must not be able to
276
+ * trigger one through the developer's browser. A JSON content type forces a
277
+ * CORS preflight that this route never answers, and `Sec-Fetch-Site` rejects
278
+ * cross-site requests outright where the browser sends it.
279
+ */
280
+ function refuseCrossSite(request) {
281
+ const site = request.headers.get("sec-fetch-site");
282
+ if (site && site !== "same-origin" && site !== "none") return Response.json({
283
+ ok: false,
284
+ error: "cross-site request refused"
285
+ }, { status: 403 });
286
+ if (!request.headers.get("content-type")?.toLowerCase().startsWith("application/json")) return Response.json({
287
+ ok: false,
288
+ error: "expected application/json"
289
+ }, { status: 415 });
290
+ return null;
291
+ }
292
+ function isRecord(value) {
293
+ return typeof value === "object" && value !== null && !Array.isArray(value);
294
+ }
295
+ /** Shape check, not a schema: enough that the prompt builder cannot throw on it. */
296
+ function parseBody(value) {
297
+ if (!isRecord(value) || typeof value.target !== "string") return null;
298
+ const note = typeof value.note === "string" ? value.note : "";
299
+ const payload = value.payload;
300
+ const validPayload = isRecord(payload) && typeof payload.app === "string" && typeof payload.route === "string" && isRecord(payload.anchor) && typeof payload.anchor.label === "string" && typeof payload.anchor.selector === "string" && isRecord(payload.viewport) && typeof payload.viewport.width === "number" && typeof payload.viewport.height === "number" && (payload.component == null || isRecord(payload.component) && typeof payload.component.name === "string" && Array.isArray(payload.component.owners));
301
+ if (payload != null && !validPayload) return null;
302
+ const hints = Array.isArray(value.hints) ? value.hints.filter((h) => isRecord(h) && typeof h.name === "string" && typeof h.path === "string") : [];
303
+ return {
304
+ target: value.target,
305
+ note,
306
+ payload: validPayload ? payload : null,
307
+ hints
308
+ };
309
+ }
310
+ function checkUnique(providers) {
311
+ const ids = /* @__PURE__ */ new Set();
312
+ for (const provider of providers) {
313
+ if (ids.has(provider.id)) throw new Error(`hud agent: duplicate provider id "${provider.id}"`);
314
+ ids.add(provider.id);
315
+ }
316
+ }
317
+ /**
318
+ * Server half of `agentDispatch`: `GET /targets` lists the providers,
319
+ * `POST /dispatch` hands a grab plus instruction to one of them.
320
+ */
321
+ function agentDispatch({ providers }) {
322
+ checkUnique(providers);
323
+ const byId = new Map(providers.map((provider) => [provider.id, provider]));
324
+ return definePlugin({
325
+ id: "agent-dispatch",
326
+ routes: {
327
+ "/targets": { GET: async () => {
328
+ const targets = await Promise.all(providers.map(async (provider) => {
329
+ const info = {
330
+ id: provider.id,
331
+ label: provider.label,
332
+ available: true
333
+ };
334
+ if (provider.description) info.description = provider.description;
335
+ try {
336
+ const status = await provider.status?.() ?? { available: true };
337
+ if (!status.available) return {
338
+ ...info,
339
+ available: false,
340
+ reason: status.reason
341
+ };
342
+ } catch (error) {
343
+ return {
344
+ ...info,
345
+ available: false,
346
+ reason: message(error)
347
+ };
348
+ }
349
+ return info;
350
+ }));
351
+ return Response.json(targets);
352
+ } },
353
+ "/dispatch": { POST: async (request) => {
354
+ const refusal = refuseCrossSite(request);
355
+ if (refusal) return refusal;
356
+ const body = parseBody(await request.json().catch(() => null));
357
+ if (!body) return Response.json({
358
+ ok: false,
359
+ error: "invalid dispatch"
360
+ }, { status: 400 });
361
+ const fail = (status, error) => Response.json({
362
+ ok: false,
363
+ target: body.target,
364
+ error
365
+ }, { status });
366
+ if (!body.note.trim() && !body.payload) return fail(400, "nothing to dispatch");
367
+ const provider = byId.get(body.target);
368
+ if (!provider) return fail(404, `unknown target "${body.target}"`);
369
+ try {
370
+ const status = await provider.status?.() ?? { available: true };
371
+ if (!status.available) return fail(409, status.reason);
372
+ const hints = body.hints ?? [];
373
+ const prompt = dispatchPrompt({
374
+ note: body.note,
375
+ payload: body.payload,
376
+ hints
377
+ });
378
+ const run = await provider.dispatch({
379
+ note: body.note,
380
+ payload: body.payload,
381
+ hints,
382
+ prompt
383
+ });
384
+ const receipt = {
385
+ ok: true,
386
+ target: provider.id,
387
+ runId: run.runId
388
+ };
389
+ if (run.detail) receipt.detail = run.detail;
390
+ return Response.json(receipt);
391
+ } catch (error) {
392
+ return fail(502, message(error));
393
+ }
394
+ } }
395
+ }
396
+ });
397
+ }
398
+ /** Server half of `agentSessions`: `GET /sessions` merges every provider's sessions. */
399
+ function agentSessions({ providers }) {
400
+ checkUnique(providers);
401
+ const listing = providers.filter((provider) => provider.sessions);
402
+ return definePlugin({
403
+ id: "agent-sessions",
404
+ routes: { "/sessions": { GET: async () => {
405
+ const results = await Promise.allSettled(listing.map(async (provider) => {
406
+ return (await provider.status?.() ?? { available: true }).available ? await provider.sessions?.() ?? [] : [];
407
+ }));
408
+ const response = {
409
+ sessions: [],
410
+ errors: []
411
+ };
412
+ for (const [i, result] of results.entries()) {
413
+ const provider = listing[i]?.id ?? "unknown";
414
+ if (result.status === "fulfilled") response.sessions.push(...result.value);
415
+ else response.errors.push({
416
+ provider,
417
+ error: message(result.reason)
418
+ });
419
+ }
420
+ response.sessions.sort((a, b) => sortKey(b).localeCompare(sortKey(a)));
421
+ return Response.json(response);
422
+ } } }
423
+ });
424
+ }
425
+ function sortKey(session) {
426
+ return session.updatedAt ?? session.startedAt ?? "";
427
+ }
428
+ //#endregion
429
+ export { agentDispatch, agentSessions, claudeCode, dispatchPrompt, hermes };
@@ -0,0 +1,37 @@
1
+ import { r as GrabPayload, t as ComponentHint } from "../payload-CLKDVZWl.js";
2
+ import { c as HudPluginProps, s as HudPluginContext } from "../plugins-CQ_0w1ot.js";
3
+ import "../index-BYsdS-7d.js";
4
+ import { a as DispatchTargetInfo, i as DispatchReceipt, n as AgentSessionStatus, o as SessionsResponse, r as DispatchBody, t as AgentSession } from "../types-DADBRiGk.js";
5
+ //#region src/plugins/agent/index.d.ts
6
+ export interface AgentDispatchOptions {
7
+ /**
8
+ * The app's component manifest. A grab is matched against it by route and
9
+ * label and the matches travel with the dispatch as extra source hints.
10
+ */
11
+ componentHints?: readonly ComponentHint[];
12
+ /** Open the Dispatch panel when a grab completes. Default true. */
13
+ openOnGrab?: boolean;
14
+ }
15
+ /**
16
+ * Dispatch panel: the last grab plus an instruction, sent to one of the
17
+ * server's providers (`hermes()`, `claudeCode()`). Mount the server half from
18
+ * `@hansenexus/hud/plugins/agent/server` in the catch-all route.
19
+ */
20
+ export declare function agentDispatch({ componentHints, openOnGrab }?: AgentDispatchOptions): {
21
+ readonly id: "agent-dispatch";
22
+ readonly title: "Dispatch";
23
+ readonly panel: ({ ctx }: HudPluginProps) => import("react").JSX.Element;
24
+ readonly onGrab?: (_payload: GrabPayload, ctx: HudPluginContext) => void;
25
+ };
26
+ export interface AgentSessionsOptions {
27
+ /** Refresh interval while the panel is open and the tab visible. Default 10 s. */
28
+ intervalMs?: number;
29
+ }
30
+ /** Sessions panel: runs and sessions of every provider that lists them. */
31
+ export declare function agentSessions({ intervalMs }?: AgentSessionsOptions): {
32
+ readonly id: "agent-sessions";
33
+ readonly title: "Sessions";
34
+ readonly panel: ({ ctx }: HudPluginProps) => import("react").JSX.Element;
35
+ };
36
+ //#endregion
37
+ export type { AgentSession, AgentSessionStatus, DispatchBody, DispatchReceipt, DispatchTargetInfo, SessionsResponse };