@vincentt-xr/harness 0.4.0 → 1.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.
Files changed (54) hide show
  1. package/dist/client/HarnessProvider.d.ts +43 -1
  2. package/dist/client/HarnessProvider.js +66 -10
  3. package/dist/client/annotate.d.ts +34 -0
  4. package/dist/client/annotate.js +104 -0
  5. package/dist/client/index.d.ts +2 -0
  6. package/dist/client/index.js +1 -0
  7. package/dist/shared/events.d.ts +138 -3
  8. package/dist/shared/events.js +25 -1
  9. package/dist/tunnel/cbor.d.ts +71 -0
  10. package/dist/tunnel/cbor.js +403 -0
  11. package/dist/tunnel/client.d.ts +219 -0
  12. package/dist/tunnel/client.js +620 -0
  13. package/dist/tunnel/forwardTarget.d.ts +69 -0
  14. package/dist/tunnel/forwardTarget.js +174 -0
  15. package/dist/tunnel/frame.d.ts +126 -0
  16. package/dist/tunnel/frame.js +244 -0
  17. package/dist/tunnel/index.d.ts +11 -0
  18. package/dist/tunnel/index.js +11 -0
  19. package/package.json +14 -31
  20. package/README.md +0 -87
  21. package/dist/cli/index.d.ts +0 -2
  22. package/dist/cli/index.js +0 -55
  23. package/dist/login/login.d.ts +0 -34
  24. package/dist/login/login.js +0 -148
  25. package/dist/mcp/backend.d.ts +0 -52
  26. package/dist/mcp/backend.js +0 -146
  27. package/dist/mcp/cli.d.ts +0 -2
  28. package/dist/mcp/cli.js +0 -10
  29. package/dist/mcp/diagnostics.d.ts +0 -13
  30. package/dist/mcp/diagnostics.js +0 -61
  31. package/dist/mcp/server.d.ts +0 -16
  32. package/dist/mcp/server.js +0 -239
  33. package/dist/preview/cloudflared.d.ts +0 -13
  34. package/dist/preview/cloudflared.js +0 -46
  35. package/dist/preview/index.d.ts +0 -3
  36. package/dist/preview/index.js +0 -6
  37. package/dist/preview/net.d.ts +0 -6
  38. package/dist/preview/net.js +0 -56
  39. package/dist/preview/proxy.d.ts +0 -4
  40. package/dist/preview/proxy.js +0 -49
  41. package/dist/preview/runner.d.ts +0 -45
  42. package/dist/preview/runner.js +0 -110
  43. package/dist/preview/tunnel.d.ts +0 -14
  44. package/dist/preview/tunnel.js +0 -28
  45. package/dist/relay/cli.d.ts +0 -2
  46. package/dist/relay/cli.js +0 -7
  47. package/dist/relay/server.d.ts +0 -12
  48. package/dist/relay/server.js +0 -85
  49. package/dist/relay/store.d.ts +0 -13
  50. package/dist/relay/store.js +0 -68
  51. package/dist/scaffold/index.d.ts +0 -26
  52. package/dist/scaffold/index.js +0 -85
  53. package/dist/shared/config.d.ts +0 -39
  54. package/dist/shared/config.js +0 -90
package/README.md DELETED
@@ -1,87 +0,0 @@
1
- # @vincentt-xr/harness
2
-
3
- The Vincentt AR dev-loop harness. It is **one always-on diagnostics channel**
4
- between the AR app running on a phone and the coding agent running on a laptop,
5
- so the agent can see what the phone sees — console logs, network requests,
6
- performance — without the developer hand-ferrying DevTools traces or pasting
7
- console output.
8
-
9
- Agent-agnostic: the agent side is a plain **MCP server**, so any MCP-capable
10
- agent (Claude Code, Codex, Cursor, Cline, …) uses it the same way.
11
-
12
- ## The three parts
13
-
14
- ```
15
- PHONE (preview app) RELAY (laptop) AGENT (laptop)
16
- ┌──────────────────┐ ┌────────────────┐ ┌────────────────────┐
17
- │ HarnessProvider │ │ harness-relay │ │ harness-mcp │
18
- │ • patch console │─ws→│ ring buffer │←──→│ diag_logs │
19
- │ • wrap fetch/XHR │ │ per session │http│ diag_network │
20
- │ • sample perf │ │ │ │ diag_trace │
21
- └──────────────────┘ └────────────────┘ └────────────────────┘
22
- src/client (npm pkg) src/relay (bin) src/mcp (bin)
23
- ```
24
-
25
- - **`src/client`** — the in-app half; the published npm package. `HarnessProvider`
26
- patches `console.*`, wraps `fetch`/`XHR`, and samples `performance`, shipping
27
- events to the relay over a WebSocket. Auto-on in a dev build, no-op (and
28
- tree-shaken) in production.
29
- - **`src/relay`** — a small WebSocket + HTTP server beside Vite. Holds a bounded
30
- ring buffer of recent events per session. The phone pushes; the agent pulls.
31
- - **`src/mcp`** — the agent-agnostic MCP server. Exposes `diag_logs`,
32
- `diag_network`, `diag_trace` that pull from the relay.
33
-
34
- - **`src/shared/events.ts`** — the wire contract all three import, so they can't
35
- drift on the event shape.
36
-
37
- ## Use it
38
-
39
- **In the app (once):**
40
-
41
- ```tsx
42
- import { HarnessProvider } from "@vincentt-xr/harness";
43
-
44
- <HarnessProvider>
45
- <App />
46
- </HarnessProvider>;
47
- ```
48
-
49
- That's the whole author-facing surface. It self-activates in dev and disappears
50
- in the production build.
51
-
52
- **Run the relay** beside your dev server (the preview tunnel carries its socket
53
- to the phone):
54
-
55
- ```
56
- npx harness-relay # listens on :7331, ws path /__harness
57
- ```
58
-
59
- **Point your agent at the MCP server.** In the agent's MCP config:
60
-
61
- ```json
62
- {
63
- "mcpServers": {
64
- "vincentt-harness": { "command": "npx", "args": ["harness-mcp"] }
65
- }
66
- }
67
- ```
68
-
69
- Then, while the app is open on the phone, the agent calls `diag_logs`,
70
- `diag_network`, `diag_trace` to read what the device is doing — no DevTools, no
71
- copy-paste.
72
-
73
- ## Develop
74
-
75
- ```
76
- pnpm install
77
- pnpm test # unit tests (pure event-shaping, buffer, ring store, formatting, fps)
78
- pnpm typecheck
79
- pnpm build # emits dist/ (client = npm surface; relay/mcp = bins)
80
- ```
81
-
82
- ## Status
83
-
84
- Local-first. The diagnostics limb is built and proven end-to-end
85
- (phone → relay → agent). The platform lifecycle (per-project server-minted
86
- tunnels, `createProject`/`publish`, auth) is a separate later phase; the same
87
- client + relay also feed the production client-review service.
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/cli/index.js DELETED
@@ -1,55 +0,0 @@
1
- #!/usr/bin/env node
2
- // `harness` — top-level CLI. Today it dispatches a single subcommand, `login`,
3
- // which obtains a Personal Access Token through the editor's /oauth flow and
4
- // writes it to ~/.vincentt/config.json (the MCP verbs then read it).
5
- import os from "node:os";
6
- import { runLogin } from "../login/login.js";
7
- const DEFAULT_EDITOR_URL = "https://editor.vincentt.studio";
8
- const DEFAULT_API_URL = "https://api.vincentt.studio";
9
- function flag(name) {
10
- const i = process.argv.indexOf(name);
11
- return i !== -1 ? process.argv[i + 1] : undefined;
12
- }
13
- function usage() {
14
- console.log(`Usage: harness login [options]\n\n` +
15
- ` Sign in and save a Vincentt access token to ~/.vincentt/config.json.\n\n` +
16
- `Options:\n` +
17
- ` --editor <url> Editor base URL (default ${DEFAULT_EDITOR_URL}, or $VINCENTT_EDITOR_URL)\n` +
18
- ` --api <url> Backend API base URL (default ${DEFAULT_API_URL}, or $VINCENTT_API_URL)\n` +
19
- ` --name <label> Token label shown in the editor (default "Harness CLI (<host>)")\n`);
20
- }
21
- async function login() {
22
- const editorUrl = flag("--editor") ?? process.env.VINCENTT_EDITOR_URL ?? DEFAULT_EDITOR_URL;
23
- const apiUrl = flag("--api") ?? process.env.VINCENTT_API_URL ?? DEFAULT_API_URL;
24
- const tokenName = flag("--name") ?? `Harness CLI (${os.hostname()})`;
25
- console.log("Opening your browser to authorize this device…");
26
- const { apiUrl: saved, configPath } = await runLogin({
27
- editorUrl,
28
- apiUrl,
29
- tokenName,
30
- onUrl: (url) => console.log(`\nIf your browser didn't open, visit:\n ${url}\n`),
31
- });
32
- console.log(`\n✓ Signed in. Access token saved to ${configPath}`);
33
- console.log(` API: ${saved}`);
34
- }
35
- async function main() {
36
- const cmd = process.argv[2];
37
- switch (cmd) {
38
- case "login":
39
- await login();
40
- break;
41
- case undefined:
42
- case "-h":
43
- case "--help":
44
- usage();
45
- break;
46
- default:
47
- console.error(`Unknown command: ${cmd}\n`);
48
- usage();
49
- process.exitCode = 1;
50
- }
51
- }
52
- main().catch((err) => {
53
- console.error(`\n✗ ${err instanceof Error ? err.message : String(err)}`);
54
- process.exit(1);
55
- });
@@ -1,34 +0,0 @@
1
- export interface LoginOptions {
2
- /** Editor base URL that serves the /oauth consent page. */
3
- editorUrl: string;
4
- /** Backend API base URL the minted PAT authenticates against (paired with editorUrl). */
5
- apiUrl: string;
6
- /** Human label stored with the token (shown in the editor's token list). */
7
- tokenName: string;
8
- /** Abort if the browser round-trip doesn't complete in time. Default 5 min. */
9
- timeoutMs?: number;
10
- /** Set false to skip launching a browser (tests / headless). Default true. */
11
- openInBrowser?: boolean;
12
- /** Called with the authorize URL once the listener is up (for a printable fallback). */
13
- onUrl?: (url: string) => void;
14
- }
15
- /** Prefix a scheme when the user passed a bare host; localhost defaults to http. */
16
- export declare function normalizeBaseUrl(u: string): string;
17
- export declare function buildAuthorizeUrl(editorBase: string, redirectUri: string, state: string, tokenName: string): string;
18
- export type CallbackResult = {
19
- ok: true;
20
- token: string;
21
- } | {
22
- ok: false;
23
- message: string;
24
- };
25
- /** Interpret the editor's loopback redirect. State is verified first, always. */
26
- export declare function parseCallback(params: URLSearchParams, expectedState: string): CallbackResult;
27
- /**
28
- * Run the loopback OAuth round-trip and persist the resulting PAT. Resolves with
29
- * the apiUrl written to config; rejects on cancel, state mismatch, or timeout.
30
- */
31
- export declare function runLogin(opts: LoginOptions): Promise<{
32
- apiUrl: string;
33
- configPath: string;
34
- }>;
@@ -1,148 +0,0 @@
1
- // `harness login` — obtain a Personal Access Token via the editor's /oauth flow.
2
- //
3
- // The CLI can't mint a PAT directly (POST /access-tokens is Cognito-only, so a
4
- // machine token can never mint another). Instead it delegates to the editor:
5
- // 1. Start a loopback HTTP listener on 127.0.0.1:<ephemeral port>.
6
- // 2. Open the browser to <editor>/oauth?redirect_uri=…&state=…&name=… — the
7
- // editor reuses its own passwordless login + a consent screen and mints the
8
- // PAT on the user's behalf.
9
- // 3. The editor redirects ?token=<vpat_…>&state=… back to the loopback.
10
- // 4. Verify state, write the PAT to ~/.vincentt/config.json.
11
- // The PAT crosses only over loopback on the user's own machine.
12
- import { spawn } from "node:child_process";
13
- import { createServer } from "node:http";
14
- import { randomBytes } from "node:crypto";
15
- import { getFreePort } from "../preview/net.js";
16
- import { writeMachineConfig } from "../shared/config.js";
17
- /** Prefix a scheme when the user passed a bare host; localhost defaults to http. */
18
- export function normalizeBaseUrl(u) {
19
- if (/^https?:\/\//i.test(u))
20
- return u;
21
- const local = /^(localhost|127\.0\.0\.1)(:|\/|$)/i.test(u);
22
- return `${local ? "http" : "https"}://${u}`;
23
- }
24
- export function buildAuthorizeUrl(editorBase, redirectUri, state, tokenName) {
25
- const u = new URL("/oauth", normalizeBaseUrl(editorBase));
26
- u.searchParams.set("redirect_uri", redirectUri);
27
- u.searchParams.set("state", state);
28
- u.searchParams.set("name", tokenName);
29
- return u.toString();
30
- }
31
- /** Interpret the editor's loopback redirect. State is verified first, always. */
32
- export function parseCallback(params, expectedState) {
33
- if (params.get("state") !== expectedState) {
34
- return { ok: false, message: "State mismatch — ignoring an unexpected callback." };
35
- }
36
- const error = params.get("error");
37
- if (error) {
38
- return {
39
- ok: false,
40
- message: error === "access_denied" ? "Authorization was cancelled." : `Authorization failed: ${error}`,
41
- };
42
- }
43
- const token = params.get("token");
44
- if (!token)
45
- return { ok: false, message: "No token in the authorization response." };
46
- return { ok: true, token };
47
- }
48
- function escapeHtml(s) {
49
- return s.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
50
- }
51
- // Standalone success/failure page shown in the browser tab after the loopback
52
- // round-trip. Fully self-contained (inline CSS, no assets) since it's served off
53
- // the CLI's own listener; theme-aware to match the editor's sand/ink palette.
54
- function browserPage(ok, heading, body) {
55
- const accent = ok ? "#16a34a" : "#dc2626";
56
- const glyph = ok ? "&check;" : "&times;";
57
- return `<!doctype html>
58
- <html lang="en"><head><meta charset="utf-8">
59
- <meta name="viewport" content="width=device-width,initial-scale=1"><title>Vincentt XR</title>
60
- <style>
61
- :root { color-scheme: light dark; }
62
- * { box-sizing: border-box; }
63
- body { margin:0; min-height:100vh; display:flex; align-items:center; justify-content:center;
64
- font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
65
- background:#f4efe6; color:#1c1917; }
66
- .card { width:min(92vw,380px); background:#fff; border:1px solid #e7e0d3; border-radius:16px;
67
- padding:40px 36px; text-align:center; box-shadow:0 10px 40px rgba(0,0,0,.08); }
68
- .brand { display:flex; align-items:center; justify-content:center; gap:8px; margin-bottom:26px;
69
- font-weight:700; font-size:15px; letter-spacing:-.01em; }
70
- .dot { width:26px; height:26px; border-radius:8px; background:linear-gradient(135deg,#7c3aed,#2563eb); }
71
- .glyph { width:48px; height:48px; margin:0 auto 16px; border-radius:50%; color:#fff;
72
- display:flex; align-items:center; justify-content:center; font-size:26px; line-height:1; background:${accent}; }
73
- h1 { font-size:19px; margin:0 0 6px; }
74
- p { font-size:13px; opacity:.7; margin:0; line-height:1.5; }
75
- @media (prefers-color-scheme: dark) {
76
- body { background:#1c1917; color:#f4efe6; }
77
- .card { background:#292524; border-color:#3f3a36; }
78
- }
79
- </style></head>
80
- <body><div class="card">
81
- <div class="brand"><span class="dot"></span>Vincentt XR</div>
82
- <div class="glyph">${glyph}</div>
83
- <h1>${escapeHtml(heading)}</h1>
84
- <p>${escapeHtml(body)}</p>
85
- </div></body></html>`;
86
- }
87
- function openBrowser(url) {
88
- const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
89
- const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
90
- try {
91
- const child = spawn(cmd, args, { stdio: "ignore", detached: true });
92
- child.on("error", () => { });
93
- child.unref();
94
- }
95
- catch {
96
- // Falls back to the printed URL (onUrl) — never fatal.
97
- }
98
- }
99
- /**
100
- * Run the loopback OAuth round-trip and persist the resulting PAT. Resolves with
101
- * the apiUrl written to config; rejects on cancel, state mismatch, or timeout.
102
- */
103
- export async function runLogin(opts) {
104
- const state = randomBytes(16).toString("hex");
105
- const port = await getFreePort();
106
- const redirectUri = `http://127.0.0.1:${port}/callback`;
107
- const authorizeUrl = buildAuthorizeUrl(opts.editorUrl, redirectUri, state, opts.tokenName);
108
- const token = await new Promise((resolve, reject) => {
109
- const server = createServer((req, res) => {
110
- const url = new URL(req.url ?? "/", `http://127.0.0.1:${port}`);
111
- if (url.pathname !== "/callback") {
112
- res.writeHead(404, { "content-type": "text/plain" });
113
- res.end("Not found");
114
- return;
115
- }
116
- const result = parseCallback(url.searchParams, state);
117
- res.writeHead(result.ok ? 200 : 400, { "content-type": "text/html; charset=utf-8" });
118
- res.end(result.ok
119
- ? browserPage(true, "You're signed in", "You can close this tab and return to your terminal.")
120
- : browserPage(false, "Sign-in failed", result.message));
121
- // A state-mismatch response is answered but must NOT settle the flow — a
122
- // stray/forged request shouldn't tear down the listener the real callback
123
- // still needs.
124
- if (result.ok) {
125
- server.close();
126
- resolve(result.token);
127
- }
128
- else if (url.searchParams.get("state") === state) {
129
- server.close();
130
- reject(new Error(result.message));
131
- }
132
- });
133
- server.on("error", reject);
134
- server.listen(port, "127.0.0.1", () => {
135
- opts.onUrl?.(authorizeUrl);
136
- if (opts.openInBrowser !== false)
137
- openBrowser(authorizeUrl);
138
- });
139
- const timer = setTimeout(() => {
140
- server.close();
141
- reject(new Error("Timed out waiting for browser authorization."));
142
- }, opts.timeoutMs ?? 5 * 60_000);
143
- timer.unref();
144
- });
145
- const apiUrl = opts.apiUrl.replace(/\/$/, "");
146
- const configPath = await writeMachineConfig({ apiUrl, pat: token });
147
- return { apiUrl, configPath };
148
- }
@@ -1,52 +0,0 @@
1
- import type { ResolvedConfig } from "../shared/config.js";
2
- export interface CreatedProject {
3
- projectId: string;
4
- slug: string;
5
- name: string;
6
- }
7
- export interface PublishResult {
8
- version: number;
9
- /** Immutable versioned URL (<slug>.<apex>/v<n>/). */
10
- url: string;
11
- /** Stable live alias (<slug>.<apex>/). */
12
- liveUrl: string;
13
- }
14
- export declare class BackendError extends Error {
15
- readonly status?: number | undefined;
16
- constructor(message: string, status?: number | undefined);
17
- }
18
- /**
19
- * Create a backend project (reuses POST /projects). A `slug` pins the permanent
20
- * <slug>.vincentt.app host; omit it and the platform assigns a catchy one. A
21
- * taken slug comes back 409, surfaced so the caller can re-ask.
22
- */
23
- export declare function createProject(cfg: ResolvedConfig, name: string, slug?: string): Promise<CreatedProject>;
24
- export interface MintedTunnel {
25
- tunnelId: string;
26
- /** The public host the phone opens: <slug>-<token>.<previewApex>. */
27
- hostname: string;
28
- /** cloudflared run token — `cloudflared tunnel run --token <this>`. */
29
- runToken: string;
30
- /** DNS record id, passed back on reap. */
31
- dnsRecordId: string;
32
- }
33
- /** Mint a per-session dev tunnel for `localPort` (the backend holds CF creds). */
34
- export declare function mintTunnel(cfg: ResolvedConfig, projectId: string, localPort: number): Promise<MintedTunnel>;
35
- /** Tear down a session tunnel (DNS route + tunnel). Best-effort; never throws. */
36
- export declare function reapTunnel(cfg: ResolvedConfig, projectId: string, ref: {
37
- tunnelId: string;
38
- dnsRecordId?: string;
39
- }): Promise<void>;
40
- /** Recursively list files under `dir`, returning dir-relative POSIX paths. */
41
- export declare function walkDir(dir: string, base?: string): Promise<string[]>;
42
- /**
43
- * Upload a locally-built dist and make it live. Each file rides as a multipart
44
- * part whose filename is its dist-relative path (the backend reconstructs the
45
- * tree). Requires an index.html at the dist root.
46
- */
47
- export declare function publishUpload(cfg: ResolvedConfig, projectId: string, distDir: string, opts?: {
48
- note?: string;
49
- commitSha?: string;
50
- }): Promise<PublishResult>;
51
- /** The working tree's current commit, for publish provenance. Undefined if not a repo. */
52
- export declare function gitHeadSha(cwd: string): Promise<string | undefined>;
@@ -1,146 +0,0 @@
1
- // The lifecycle verbs' backend calls, isolated from the MCP SDK wiring (server.ts)
2
- // so they're unit-testable and the transport can change without touching them.
3
- // Every call is PAT-authed (Bearer). The publish path uploads an already-built
4
- // dist as-is — the backend never rebuilds (Principle 0).
5
- import { promises as fs } from "node:fs";
6
- import path from "node:path";
7
- import { execFile } from "node:child_process";
8
- import { promisify } from "node:util";
9
- const execFileAsync = promisify(execFile);
10
- export class BackendError extends Error {
11
- status;
12
- constructor(message, status) {
13
- super(message);
14
- this.status = status;
15
- this.name = "BackendError";
16
- }
17
- }
18
- async function authFetch(cfg, pathname, init = {}) {
19
- return fetch(`${cfg.apiUrl}${pathname}`, {
20
- ...init,
21
- headers: { ...(init.headers ?? {}), authorization: `Bearer ${cfg.pat}` },
22
- });
23
- }
24
- async function safeText(res) {
25
- try {
26
- return (await res.text()).slice(0, 300);
27
- }
28
- catch {
29
- return "";
30
- }
31
- }
32
- /**
33
- * Create a backend project (reuses POST /projects). A `slug` pins the permanent
34
- * <slug>.vincentt.app host; omit it and the platform assigns a catchy one. A
35
- * taken slug comes back 409, surfaced so the caller can re-ask.
36
- */
37
- export async function createProject(cfg, name, slug) {
38
- const res = await authFetch(cfg, "/projects", {
39
- method: "POST",
40
- headers: { "content-type": "application/json" },
41
- body: JSON.stringify({ name, ...(slug ? { slug } : {}) }),
42
- });
43
- if (res.status === 409) {
44
- throw new BackendError(`slug "${slug}" is already in use — pick another.`, 409);
45
- }
46
- if (!res.ok) {
47
- throw new BackendError(`create project failed (${res.status}): ${await safeText(res)}`, res.status);
48
- }
49
- const p = (await res.json());
50
- const projectId = p._id ?? p.id;
51
- if (!projectId || !p.slug) {
52
- throw new BackendError("create project: response missing id/slug");
53
- }
54
- return { projectId, slug: p.slug, name: p.name ?? name };
55
- }
56
- /** Mint a per-session dev tunnel for `localPort` (the backend holds CF creds). */
57
- export async function mintTunnel(cfg, projectId, localPort) {
58
- const res = await authFetch(cfg, `/projects/${projectId}/tunnel`, {
59
- method: "POST",
60
- headers: { "content-type": "application/json" },
61
- body: JSON.stringify({ port: localPort }),
62
- });
63
- if (!res.ok) {
64
- throw new BackendError(`tunnel mint failed (${res.status}): ${await safeText(res)}`, res.status);
65
- }
66
- return (await res.json());
67
- }
68
- /** Tear down a session tunnel (DNS route + tunnel). Best-effort; never throws. */
69
- export async function reapTunnel(cfg, projectId, ref) {
70
- try {
71
- await authFetch(cfg, `/projects/${projectId}/tunnel/reap`, {
72
- method: "POST",
73
- headers: { "content-type": "application/json" },
74
- body: JSON.stringify(ref),
75
- });
76
- }
77
- catch {
78
- // Reap is a cleanup courtesy — a failure must never crash the preview teardown.
79
- }
80
- }
81
- /** Recursively list files under `dir`, returning dir-relative POSIX paths. */
82
- export async function walkDir(dir, base = dir) {
83
- const out = [];
84
- let entries;
85
- try {
86
- entries = await fs.readdir(dir, { withFileTypes: true });
87
- }
88
- catch (err) {
89
- if (err.code === "ENOENT") {
90
- throw new BackendError(`dist directory not found: ${dir}`);
91
- }
92
- throw err;
93
- }
94
- for (const entry of entries) {
95
- const full = path.join(dir, entry.name);
96
- if (entry.isDirectory())
97
- out.push(...(await walkDir(full, base)));
98
- else if (entry.isFile())
99
- out.push(path.relative(base, full).split(path.sep).join("/"));
100
- }
101
- return out;
102
- }
103
- /**
104
- * Upload a locally-built dist and make it live. Each file rides as a multipart
105
- * part whose filename is its dist-relative path (the backend reconstructs the
106
- * tree). Requires an index.html at the dist root.
107
- */
108
- export async function publishUpload(cfg, projectId, distDir, opts = {}) {
109
- const files = await walkDir(distDir);
110
- if (!files.includes("index.html")) {
111
- throw new BackendError(`dist at ${distDir} has no index.html at its root — did the build run?`);
112
- }
113
- const form = new FormData();
114
- // The real dist-relative paths ride as an ordered manifest — a multipart filename
115
- // can't carry a directory (FormData strips it to a basename), so the backend maps
116
- // paths[i] → files[i] to rebuild the tree.
117
- form.append("paths", JSON.stringify(files));
118
- for (const rel of files) {
119
- const bytes = await fs.readFile(path.join(distDir, rel));
120
- // Uint8Array (Buffer) is a valid BlobPart; the filename is cosmetic (the backend
121
- // uses the manifest), but pass the basename so server logs read sensibly.
122
- form.append("files", new Blob([bytes]), path.basename(rel));
123
- }
124
- if (opts.note)
125
- form.append("note", opts.note);
126
- if (opts.commitSha)
127
- form.append("commitSha", opts.commitSha);
128
- const res = await authFetch(cfg, `/projects/${projectId}/publish/upload`, {
129
- method: "POST",
130
- body: form,
131
- });
132
- if (!res.ok) {
133
- throw new BackendError(`publish failed (${res.status}): ${await safeText(res)}`, res.status);
134
- }
135
- return (await res.json());
136
- }
137
- /** The working tree's current commit, for publish provenance. Undefined if not a repo. */
138
- export async function gitHeadSha(cwd) {
139
- try {
140
- const { stdout } = await execFileAsync("git", ["-C", cwd, "rev-parse", "HEAD"]);
141
- return stdout.trim();
142
- }
143
- catch {
144
- return undefined;
145
- }
146
- }
package/dist/mcp/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/mcp/cli.js DELETED
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env node
2
- // `harness-mcp` — the agent-side MCP server. An MCP agent spawns this over
3
- // stdio. Point it at a non-default relay with --relay-url.
4
- import { runStdio } from "./server.js";
5
- const urlArg = process.argv.indexOf("--relay-url");
6
- const relayUrl = urlArg !== -1 ? process.argv[urlArg + 1] : undefined;
7
- runStdio({ relayUrl }).catch((err) => {
8
- console.error("[harness-mcp] fatal:", err);
9
- process.exit(1);
10
- });
@@ -1,13 +0,0 @@
1
- import type { DiagEvent, LogEvent, NetworkEvent, RelayQuery, RelayResult, TraceEvent } from "../shared/events.js";
2
- export interface RelayClient {
3
- query(q: RelayQuery): Promise<RelayResult>;
4
- }
5
- /** A RelayClient backed by the relay's HTTP /query endpoint. */
6
- export declare function httpRelayClient(baseUrl: string): RelayClient;
7
- export declare function formatLog(e: LogEvent, now: number): string;
8
- export declare function formatNetwork(e: NetworkEvent, now: number): string;
9
- export declare function formatTrace(e: TraceEvent, now: number): string;
10
- /** Render a RelayResult for one kind into the text block a tool returns. */
11
- export declare function renderResult(result: RelayResult, kind: DiagEvent["kind"], now: number): string;
12
- /** Only-errors convenience filter applied on top of a log query result. */
13
- export declare function filterErrors(result: RelayResult): RelayResult;
@@ -1,61 +0,0 @@
1
- // The MCP server's logic, independent of the MCP SDK surface: fetch events from
2
- // the relay and format them into the compact text an agent reads. Kept separate
3
- // from the SDK wiring (server.ts) so the formatting is unit-testable and the SDK
4
- // version can churn without touching this.
5
- /** A RelayClient backed by the relay's HTTP /query endpoint. */
6
- export function httpRelayClient(baseUrl) {
7
- return {
8
- async query(q) {
9
- const res = await fetch(`${baseUrl}/query`, {
10
- method: "POST",
11
- headers: { "content-type": "application/json" },
12
- body: JSON.stringify(q),
13
- });
14
- if (!res.ok)
15
- throw new Error(`relay /query returned ${res.status}`);
16
- return (await res.json());
17
- },
18
- };
19
- }
20
- /** Human-time from the client timestamp, relative to now, for the agent's read. */
21
- function ago(t, now) {
22
- const s = Math.max(0, Math.round((now - t) / 1000));
23
- return s < 60 ? `${s}s ago` : `${Math.round(s / 60)}m ago`;
24
- }
25
- export function formatLog(e, now) {
26
- const level = e.level.toUpperCase().padEnd(5);
27
- const origin = e.origin ? ` (${e.origin})` : "";
28
- return `[${level}] ${e.message}${origin} · ${ago(e.t, now)} #${e.seq}`;
29
- }
30
- export function formatNetwork(e, now) {
31
- const status = e.error ? `ERR ${e.error}` : String(e.status);
32
- return `${e.method} ${e.url} → ${status} (${e.durationMs}ms) · ${ago(e.t, now)} #${e.seq}`;
33
- }
34
- export function formatTrace(e, now) {
35
- const marks = e.marks.length ? ` marks: ${e.marks.join(", ")}` : "";
36
- return (`${e.fps.toFixed(0)}fps over ${e.windowMs}ms · longest task ${e.longestTaskMs}ms · ` +
37
- `${e.longTaskCount} long task(s)${marks} · ${ago(e.t, now)} #${e.seq}`);
38
- }
39
- /** Render a RelayResult for one kind into the text block a tool returns. */
40
- export function renderResult(result, kind, now) {
41
- const lines = result.events.map((e) => {
42
- if (e.kind === "log")
43
- return formatLog(e, now);
44
- if (e.kind === "network")
45
- return formatNetwork(e, now);
46
- return formatTrace(e, now);
47
- });
48
- const header = result.sessions.length > 1 ? `sessions: ${result.sessions.join(", ")}\n` : "";
49
- const cursor = `\n\n(latestSeq ${result.latestSeq} — pass since=${result.latestSeq} to get only newer ${kind} events)`;
50
- if (lines.length === 0) {
51
- return `${header}No ${kind} events${result.latestSeq >= 0 ? " matched" : " yet — is the preview app open on the device?"}.${result.latestSeq >= 0 ? cursor : ""}`;
52
- }
53
- return `${header}${lines.join("\n")}${cursor}`;
54
- }
55
- /** Only-errors convenience filter applied on top of a log query result. */
56
- export function filterErrors(result) {
57
- return {
58
- ...result,
59
- events: result.events.filter((e) => e.kind === "log" && e.level === "error"),
60
- };
61
- }
@@ -1,16 +0,0 @@
1
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
- import { type RelayClient } from "./diagnostics.js";
3
- /** Reap the running preview, if any. Called by preview_stop and on server exit. */
4
- export declare function shutdownActivePreview(): Promise<void>;
5
- export interface McpOptions {
6
- /** Base URL of the relay's HTTP endpoint (default matches the relay CLI). */
7
- relayUrl?: string;
8
- /** Injected for tests; defaults to an HTTP client against relayUrl. */
9
- relay?: RelayClient;
10
- /** Injected clock for deterministic relative-time rendering in tests. */
11
- now?: () => number;
12
- /** Project directory for the lifecycle verbs' binding. Defaults to process.cwd(). */
13
- cwd?: string;
14
- }
15
- export declare function createHarnessMcp(opts?: McpOptions): McpServer;
16
- export declare function runStdio(opts?: McpOptions): Promise<void>;