@vincentt-xr/harness 0.3.0 → 1.0.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/dist/client/HarnessProvider.d.ts +5 -0
- package/dist/client/HarnessProvider.js +11 -0
- package/dist/client/annotate.d.ts +34 -0
- package/dist/client/annotate.js +104 -0
- package/dist/client/index.d.ts +2 -0
- package/dist/client/index.js +1 -0
- package/dist/shared/events.d.ts +50 -0
- package/package.json +8 -33
- package/README.md +0 -87
- package/dist/mcp/backend.d.ts +0 -52
- package/dist/mcp/backend.js +0 -146
- package/dist/mcp/cli.d.ts +0 -2
- package/dist/mcp/cli.js +0 -10
- package/dist/mcp/diagnostics.d.ts +0 -13
- package/dist/mcp/diagnostics.js +0 -61
- package/dist/mcp/server.d.ts +0 -16
- package/dist/mcp/server.js +0 -221
- package/dist/preview/cloudflared.d.ts +0 -13
- package/dist/preview/cloudflared.js +0 -37
- package/dist/preview/index.d.ts +0 -3
- package/dist/preview/index.js +0 -6
- package/dist/preview/net.d.ts +0 -6
- package/dist/preview/net.js +0 -56
- package/dist/preview/proxy.d.ts +0 -4
- package/dist/preview/proxy.js +0 -49
- package/dist/preview/runner.d.ts +0 -43
- package/dist/preview/runner.js +0 -110
- package/dist/preview/tunnel.d.ts +0 -14
- package/dist/preview/tunnel.js +0 -28
- package/dist/relay/cli.d.ts +0 -2
- package/dist/relay/cli.js +0 -7
- package/dist/relay/server.d.ts +0 -12
- package/dist/relay/server.js +0 -85
- package/dist/relay/store.d.ts +0 -13
- package/dist/relay/store.js +0 -68
- package/dist/scaffold/index.d.ts +0 -26
- package/dist/scaffold/index.js +0 -85
- package/dist/shared/config.d.ts +0 -33
- package/dist/shared/config.js +0 -76
|
@@ -17,5 +17,10 @@ export interface HarnessProviderProps {
|
|
|
17
17
|
captureConsole?: boolean;
|
|
18
18
|
captureNetwork?: boolean;
|
|
19
19
|
captureTrace?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Mount the floating "Send feedback" button (the reverse-channel capture
|
|
22
|
+
* overlay). Defaults to `enabled` — on in preview, gone in production.
|
|
23
|
+
*/
|
|
24
|
+
feedback?: boolean;
|
|
20
25
|
}
|
|
21
26
|
export declare function HarnessProvider(props: HarnessProviderProps): ReactNode;
|
|
@@ -52,9 +52,20 @@ export function HarnessProvider(props) {
|
|
|
52
52
|
captureTrace: props.captureTrace,
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
+
let unmountFeedback;
|
|
56
|
+
if (props.feedback !== false) {
|
|
57
|
+
// Separate dynamic import so the capture overlay (and its DOM code) also
|
|
58
|
+
// drops from a production bundle.
|
|
59
|
+
void import("./annotate.js").then(({ mountFeedbackButton }) => {
|
|
60
|
+
if (cancelled)
|
|
61
|
+
return;
|
|
62
|
+
unmountFeedback = mountFeedbackButton({ sessionId: props.sessionId });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
55
65
|
return () => {
|
|
56
66
|
cancelled = true;
|
|
57
67
|
teardown?.();
|
|
68
|
+
unmountFeedback?.();
|
|
58
69
|
};
|
|
59
70
|
// Instrumentation is installed once for the provider's lifetime; option
|
|
60
71
|
// changes mid-session are not a supported case.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { AnnotationInput, AnnotationSpec } from "../shared/events.js";
|
|
2
|
+
/**
|
|
3
|
+
* Capture the current frame as a PNG data URL. Prefers the largest <canvas> (the
|
|
4
|
+
* AR/three.js surface); returns an empty-ish 1x1 PNG if there is none.
|
|
5
|
+
*
|
|
6
|
+
* Caveat: reading a WebGL canvas requires it to have been created with
|
|
7
|
+
* `preserveDrawingBuffer: true`, or the read can come back blank. r3f/three set
|
|
8
|
+
* this via `gl={{ preserveDrawingBuffer: true }}`. DOM-only content isn't captured
|
|
9
|
+
* here (that needs html2canvas, deliberately not a dependency).
|
|
10
|
+
*/
|
|
11
|
+
export declare function captureScreenshot(): string;
|
|
12
|
+
export interface SendAnnotationOptions {
|
|
13
|
+
/** Relay HTTP base (default: `<origin>/__harness`). */
|
|
14
|
+
relayHttpUrl?: string;
|
|
15
|
+
}
|
|
16
|
+
/** POST an annotation to the relay. Resolves with the relay-assigned id + seq. */
|
|
17
|
+
export declare function sendAnnotation(input: AnnotationInput, opts?: SendAnnotationOptions): Promise<{
|
|
18
|
+
id: string;
|
|
19
|
+
seq: number;
|
|
20
|
+
}>;
|
|
21
|
+
export interface FeedbackButtonOptions extends SendAnnotationOptions {
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
/** Collect the creator's message. Default: window.prompt. */
|
|
24
|
+
promptMessage?: () => string | null | Promise<string | null>;
|
|
25
|
+
/** Override the captured spec (strokes/pins/labels). Default: empty. */
|
|
26
|
+
spec?: AnnotationSpec;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Mount a minimal floating "Send feedback" button. On click it collects a message,
|
|
30
|
+
* captures the frame, and sends the annotation. Returns an unmount function. This
|
|
31
|
+
* is the smallest useful Send action; a richer draw-on-frame overlay can replace
|
|
32
|
+
* the capture/prompt without changing the wire contract.
|
|
33
|
+
*/
|
|
34
|
+
export declare function mountFeedbackButton(opts?: FeedbackButtonOptions): () => void;
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// The in-app capture overlay's Send action — the client half of the reverse
|
|
2
|
+
// channel. It captures the current frame, packages the creator's message + spec,
|
|
3
|
+
// and POSTs it to the relay over the same tunnel the app is served on. The relay
|
|
4
|
+
// (see the CLI's relay/annotations) stamps + persists it and unblocks a waiting
|
|
5
|
+
// `vincentt feedback --wait`.
|
|
6
|
+
//
|
|
7
|
+
// Loaded via dynamic import from HarnessProvider (like the instrumentation), so a
|
|
8
|
+
// production build never ships it. DOM-only, no React.
|
|
9
|
+
/** Default relay HTTP base: the same origin the app is served on + the harness path. */
|
|
10
|
+
function defaultRelayHttpUrl() {
|
|
11
|
+
return `${window.location.origin}/__harness`;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Capture the current frame as a PNG data URL. Prefers the largest <canvas> (the
|
|
15
|
+
* AR/three.js surface); returns an empty-ish 1x1 PNG if there is none.
|
|
16
|
+
*
|
|
17
|
+
* Caveat: reading a WebGL canvas requires it to have been created with
|
|
18
|
+
* `preserveDrawingBuffer: true`, or the read can come back blank. r3f/three set
|
|
19
|
+
* this via `gl={{ preserveDrawingBuffer: true }}`. DOM-only content isn't captured
|
|
20
|
+
* here (that needs html2canvas, deliberately not a dependency).
|
|
21
|
+
*/
|
|
22
|
+
export function captureScreenshot() {
|
|
23
|
+
const canvases = Array.from(document.querySelectorAll("canvas"));
|
|
24
|
+
const biggest = canvases.sort((a, b) => b.width * b.height - a.width * a.height)[0];
|
|
25
|
+
if (biggest) {
|
|
26
|
+
try {
|
|
27
|
+
return biggest.toDataURL("image/png");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
// Tainted canvas (cross-origin texture without CORS) — fall through.
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const fallback = document.createElement("canvas");
|
|
34
|
+
fallback.width = fallback.height = 1;
|
|
35
|
+
return fallback.toDataURL("image/png");
|
|
36
|
+
}
|
|
37
|
+
/** POST an annotation to the relay. Resolves with the relay-assigned id + seq. */
|
|
38
|
+
export async function sendAnnotation(input, opts = {}) {
|
|
39
|
+
const base = opts.relayHttpUrl ?? defaultRelayHttpUrl();
|
|
40
|
+
const res = await fetch(`${base}/annotation`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { "content-type": "application/json" },
|
|
43
|
+
body: JSON.stringify(input),
|
|
44
|
+
});
|
|
45
|
+
if (!res.ok)
|
|
46
|
+
throw new Error(`annotation POST failed: ${res.status}`);
|
|
47
|
+
return (await res.json());
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mount a minimal floating "Send feedback" button. On click it collects a message,
|
|
51
|
+
* captures the frame, and sends the annotation. Returns an unmount function. This
|
|
52
|
+
* is the smallest useful Send action; a richer draw-on-frame overlay can replace
|
|
53
|
+
* the capture/prompt without changing the wire contract.
|
|
54
|
+
*/
|
|
55
|
+
export function mountFeedbackButton(opts = {}) {
|
|
56
|
+
if (typeof document === "undefined")
|
|
57
|
+
return () => undefined;
|
|
58
|
+
const btn = document.createElement("button");
|
|
59
|
+
btn.textContent = "Send feedback";
|
|
60
|
+
Object.assign(btn.style, {
|
|
61
|
+
position: "fixed",
|
|
62
|
+
right: "12px",
|
|
63
|
+
bottom: "12px",
|
|
64
|
+
zIndex: "2147483647",
|
|
65
|
+
padding: "10px 14px",
|
|
66
|
+
borderRadius: "10px",
|
|
67
|
+
border: "none",
|
|
68
|
+
background: "linear-gradient(135deg,#7c3aed,#2563eb)",
|
|
69
|
+
color: "#fff",
|
|
70
|
+
font: "600 13px/1 -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif",
|
|
71
|
+
boxShadow: "0 4px 16px rgba(0,0,0,.25)",
|
|
72
|
+
cursor: "pointer",
|
|
73
|
+
});
|
|
74
|
+
const setBusy = (busy, label) => {
|
|
75
|
+
btn.disabled = busy;
|
|
76
|
+
btn.textContent = label ?? "Send feedback";
|
|
77
|
+
btn.style.opacity = busy ? "0.6" : "1";
|
|
78
|
+
};
|
|
79
|
+
btn.addEventListener("click", async () => {
|
|
80
|
+
const getMsg = opts.promptMessage ?? (() => window.prompt("Feedback for the agent:"));
|
|
81
|
+
const message = await getMsg();
|
|
82
|
+
if (message == null || message.trim() === "")
|
|
83
|
+
return;
|
|
84
|
+
setBusy(true, "Sending…");
|
|
85
|
+
try {
|
|
86
|
+
const screenshot = captureScreenshot();
|
|
87
|
+
const input = {
|
|
88
|
+
message: message.trim(),
|
|
89
|
+
screenshot,
|
|
90
|
+
spec: opts.spec ?? {},
|
|
91
|
+
sessionId: opts.sessionId,
|
|
92
|
+
};
|
|
93
|
+
await sendAnnotation(input, { relayHttpUrl: opts.relayHttpUrl });
|
|
94
|
+
setBusy(false, "Sent ✓");
|
|
95
|
+
window.setTimeout(() => setBusy(false), 1500);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
setBusy(false, "Failed — retry");
|
|
99
|
+
console.error("[harness] annotation send failed:", err);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
document.body.appendChild(btn);
|
|
103
|
+
return () => btn.remove();
|
|
104
|
+
}
|
package/dist/client/index.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
export { HarnessProvider } from "./HarnessProvider.js";
|
|
2
2
|
export type { HarnessProviderProps } from "./HarnessProvider.js";
|
|
3
|
+
export { sendAnnotation, captureScreenshot, mountFeedbackButton, type SendAnnotationOptions, type FeedbackButtonOptions, } from "./annotate.js";
|
|
3
4
|
export type { DiagEvent, LogEvent, NetworkEvent, TraceEvent } from "../shared/events.js";
|
|
5
|
+
export type { Annotation, AnnotationInput, AnnotationSpec, AnnotationStroke, AnnotationPin, } from "../shared/events.js";
|
package/dist/client/index.js
CHANGED
package/dist/shared/events.d.ts
CHANGED
|
@@ -75,4 +75,54 @@ export interface RelayResult {
|
|
|
75
75
|
/** Sessions the relay has seen events for (so the agent can disambiguate). */
|
|
76
76
|
sessions: string[];
|
|
77
77
|
}
|
|
78
|
+
/** A freehand stroke over the frame, in normalized [0,1] frame coordinates. */
|
|
79
|
+
export interface AnnotationStroke {
|
|
80
|
+
points: {
|
|
81
|
+
x: number;
|
|
82
|
+
y: number;
|
|
83
|
+
}[];
|
|
84
|
+
color?: string;
|
|
85
|
+
}
|
|
86
|
+
/** A point marker on the frame, in normalized [0,1] frame coordinates. */
|
|
87
|
+
export interface AnnotationPin {
|
|
88
|
+
x: number;
|
|
89
|
+
y: number;
|
|
90
|
+
label?: string;
|
|
91
|
+
}
|
|
92
|
+
/** The drawn overlay accompanying an annotation. All fields optional — a bare
|
|
93
|
+
* message with a screenshot is a valid annotation. */
|
|
94
|
+
export interface AnnotationSpec {
|
|
95
|
+
strokes?: AnnotationStroke[];
|
|
96
|
+
pins?: AnnotationPin[];
|
|
97
|
+
labels?: string[];
|
|
98
|
+
note?: string;
|
|
99
|
+
}
|
|
100
|
+
/** Client → relay POST body (`POST /__harness/annotation`). The relay stamps the
|
|
101
|
+
* id, receipt time, project binding, and on-disk screenshot path. */
|
|
102
|
+
export interface AnnotationInput {
|
|
103
|
+
/** The creator's message to the agent. */
|
|
104
|
+
message: string;
|
|
105
|
+
/** Annotated frame as a data URL (image/png). */
|
|
106
|
+
screenshot: string;
|
|
107
|
+
spec?: AnnotationSpec;
|
|
108
|
+
/** Optional preview session id, to correlate with diag events. */
|
|
109
|
+
sessionId?: string;
|
|
110
|
+
}
|
|
111
|
+
/** A persisted annotation the agent consumes (via `vincentt feedback --wait`). */
|
|
112
|
+
export interface Annotation {
|
|
113
|
+
/** Short relay-assigned id (e.g. "a7f3"). */
|
|
114
|
+
id: string;
|
|
115
|
+
/** Monotonic per-project cursor; a consumer polls "after this seq". */
|
|
116
|
+
seq: number;
|
|
117
|
+
/** Receipt time, stamped by the relay (not the browser). */
|
|
118
|
+
createdAt: Timestamp;
|
|
119
|
+
/** The bound project directory this annotation belongs to. */
|
|
120
|
+
projectDir: string;
|
|
121
|
+
/** The creator's message to the agent. */
|
|
122
|
+
message: string;
|
|
123
|
+
/** Absolute path to the full-res annotated screenshot on disk. */
|
|
124
|
+
screenshotPath: string;
|
|
125
|
+
spec: AnnotationSpec;
|
|
126
|
+
sessionId?: string;
|
|
127
|
+
}
|
|
78
128
|
export {};
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vincentt-xr/harness",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Vincentt AR dev-loop harness — in-app diagnostics
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Vincentt AR dev-loop harness — in-app diagnostics provider + wire contract",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"sideEffects": false,
|
|
8
8
|
"files": [
|
|
9
|
-
"
|
|
9
|
+
"dist"
|
|
10
10
|
],
|
|
11
11
|
"main": "./dist/client/index.js",
|
|
12
12
|
"module": "./dist/client/index.js",
|
|
@@ -16,45 +16,20 @@
|
|
|
16
16
|
"types": "./dist/client/index.d.ts",
|
|
17
17
|
"import": "./dist/client/index.js"
|
|
18
18
|
},
|
|
19
|
-
"./
|
|
20
|
-
"types": "./dist/
|
|
21
|
-
"import": "./dist/
|
|
22
|
-
},
|
|
23
|
-
"./preview": {
|
|
24
|
-
"types": "./dist/preview/index.d.ts",
|
|
25
|
-
"import": "./dist/preview/index.js"
|
|
19
|
+
"./events": {
|
|
20
|
+
"types": "./dist/shared/events.d.ts",
|
|
21
|
+
"import": "./dist/shared/events.js"
|
|
26
22
|
}
|
|
27
23
|
},
|
|
28
|
-
"bin": {
|
|
29
|
-
"harness-relay": "./dist/relay/cli.js",
|
|
30
|
-
"harness-mcp": "./dist/mcp/cli.js"
|
|
31
|
-
},
|
|
32
24
|
"peerDependencies": {
|
|
33
25
|
"react": "^18.2.0"
|
|
34
26
|
},
|
|
35
|
-
"dependencies": {
|
|
36
|
-
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
37
|
-
"cloudflared": "^0.7.1",
|
|
38
|
-
"ws": "^8.18.0",
|
|
39
|
-
"zod": "^3.23.8"
|
|
40
|
-
},
|
|
41
27
|
"devDependencies": {
|
|
42
|
-
"@types/
|
|
43
|
-
"@types/react": "^18.3.12",
|
|
44
|
-
"@types/ws": "^8.5.13",
|
|
45
|
-
"prettier": "3.3.3",
|
|
46
|
-
"ts-node": "^10.9.2",
|
|
47
|
-
"typescript": "^5.6.3",
|
|
48
|
-
"vitest": "^2.1.5"
|
|
28
|
+
"@types/react": "^18.3.12"
|
|
49
29
|
},
|
|
50
30
|
"scripts": {
|
|
51
31
|
"build": "tsc -p tsconfig.build.json",
|
|
52
|
-
"relay": "node --loader ts-node/esm src/relay/cli.ts",
|
|
53
|
-
"mcp": "node --loader ts-node/esm src/mcp/cli.ts",
|
|
54
|
-
"test": "vitest run",
|
|
55
|
-
"test:watch": "vitest",
|
|
56
32
|
"typecheck": "tsc --noEmit",
|
|
57
|
-
"
|
|
58
|
-
"format": "prettier --write ."
|
|
33
|
+
"test": "vitest run"
|
|
59
34
|
}
|
|
60
35
|
}
|
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.
|
package/dist/mcp/backend.d.ts
DELETED
|
@@ -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>;
|
package/dist/mcp/backend.js
DELETED
|
@@ -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
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;
|