hop2 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +33 -0
- package/INTEGRATION.md +77 -85
- package/MULTI-AGENT-CHECKLIST.md +37 -0
- package/README-MCP.md +232 -0
- package/README.md +209 -148
- package/demo/30s-product-demo.md +98 -0
- package/demo/README.md +106 -0
- package/demo/director.mjs +236 -0
- package/demo/drive-hop-terminal.mjs +70 -0
- package/demo/hop-demo-lib.mjs +525 -0
- package/demo/record-hop-view.mjs +26 -0
- package/demo/static-preview-server.mjs +45 -0
- package/demo/stitch-demo-video.mjs +128 -0
- package/hay/CHANGELOG.md +6 -0
- package/hay/DEV_NOTES.md +25 -0
- package/hay/README.md +136 -198
- package/hay/apps/cli/src/index.ts +321 -67
- package/hay/apps/server/src/index.ts +4 -3
- package/hay/apps/server/src/lib.ts +5 -4
- package/hay/apps/server/src/pty.ts +20 -7
- package/hay/apps/server/src/rooms.ts +226 -12
- package/hay/apps/server/test/rooms.test.ts +82 -3
- package/hay/apps/web/src/App.tsx +487 -112
- package/hay/apps/web/src/components/MobileKeyboard.tsx +43 -2
- package/hay/apps/web/src/styles.css +259 -29
- package/hay/package-lock.json +4 -4
- package/hay/packages/shared/src/protocol.ts +8 -0
- package/hay-web/assets/index-BmWjq_Nh.css +32 -0
- package/hay-web/assets/index-D9aOC5Zt.js +63 -0
- package/hay-web/assets/{index-FDfdolCs.js.map → index-D9aOC5Zt.js.map} +1 -1
- package/hay-web/index.html +2 -2
- package/hop +6801 -1714
- package/lib/naming-themes.js +194 -0
- package/lib/terminal-access.js +17 -0
- package/mcp/hop-mcp.js +4730 -0
- package/package.json +6 -3
- package/scripts/hay-host.js +189 -0
- package/sessions.html +378 -26
- package/setup-mcp.js +443 -0
- package/tests/hop-mcp.integration.test.cjs +3014 -0
- package/tests/terminal-access.unit.test.cjs +21 -0
- package/tests/terminal-api.integration.test.cjs +298 -0
- package/.hop-tmux.conf +0 -18
- package/hay-web/assets/index-CTTzApRp.css +0 -32
- package/hay-web/assets/index-FDfdolCs.js +0 -60
- package/tunnel-shell.js +0 -254
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
buildSanitizedLaunchCommand,
|
|
5
|
+
createSession,
|
|
6
|
+
ensureTerminal,
|
|
7
|
+
execTerminal,
|
|
8
|
+
findFreePort,
|
|
9
|
+
getHopState,
|
|
10
|
+
parseArgs,
|
|
11
|
+
prepareSanitizedWorkspace,
|
|
12
|
+
recordHopView,
|
|
13
|
+
resizeTerminal,
|
|
14
|
+
sleep,
|
|
15
|
+
startHopDaemon,
|
|
16
|
+
startStaticPreviewServer,
|
|
17
|
+
stopHopDaemon,
|
|
18
|
+
waitForTerminalOutput,
|
|
19
|
+
writeTerminal
|
|
20
|
+
} from "./hop-demo-lib.mjs";
|
|
21
|
+
|
|
22
|
+
function readPrompt(args, key, fallback) {
|
|
23
|
+
const fileKey = `${key}-file`;
|
|
24
|
+
if (args[fileKey]) {
|
|
25
|
+
return fs.readFileSync(path.resolve(args[fileKey]), "utf8").trim();
|
|
26
|
+
}
|
|
27
|
+
if (args[key]) {
|
|
28
|
+
return String(args[key]).trim();
|
|
29
|
+
}
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function resolvePreset(args) {
|
|
34
|
+
const preset = args.preset || "summary";
|
|
35
|
+
if (preset === "landing-page") {
|
|
36
|
+
return {
|
|
37
|
+
prompt: "Open index.html and turn it into a more polished, visually striking product landing page for Hop. Keep it as a single static page. Make the changes directly.",
|
|
38
|
+
followup: "Now tighten the headline and hero copy so it reads like a product demo page in one quick scan."
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
prompt: "Summarize the current project in 3 short bullets and suggest one safe improvement. Do not mention any local usernames, hostnames, or file paths.",
|
|
43
|
+
followup: "Now restate the improvement as one short sentence fit for an on-screen demo caption. Do not mention local paths."
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function bestEffortOutputWait(state, terminalId, timeoutMs) {
|
|
48
|
+
try {
|
|
49
|
+
await waitForTerminalOutput(state, terminalId, { timeoutMs });
|
|
50
|
+
} catch {
|
|
51
|
+
// Demo capture should keep moving even if a terminal stream reconnect is noisy.
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function ensurePortSession(state, name, port) {
|
|
56
|
+
try {
|
|
57
|
+
await createSession(state, {
|
|
58
|
+
name,
|
|
59
|
+
type: "port",
|
|
60
|
+
port
|
|
61
|
+
});
|
|
62
|
+
} catch (error) {
|
|
63
|
+
if (!String(error.message || error).includes("already in use")) {
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const args = parseArgs(process.argv.slice(2));
|
|
70
|
+
const outRoot = path.resolve(args.out || path.join("demo-output", `run-${Date.now()}`));
|
|
71
|
+
const isolatedHopHome = path.resolve(args["hop-home"] || path.join(outRoot, "hop-home"));
|
|
72
|
+
const startDaemon = args["start-daemon"] === true || args["start-daemon"] === "true";
|
|
73
|
+
const stopDaemon = args["stop-daemon"] === true || args["stop-daemon"] === "true";
|
|
74
|
+
const sessionName = args.name || "demo-agent";
|
|
75
|
+
const previewSessionName = args["preview-name"] || "demo-preview";
|
|
76
|
+
const workspaceDir = path.resolve(args.cwd || path.join(outRoot, "workspace"));
|
|
77
|
+
const baseLaunchCommand = args.launch || "codex";
|
|
78
|
+
const commandSettleMs = Number(args["command-settle"] || 1000);
|
|
79
|
+
const desktopDurationMs = Number(args["desktop-duration"] || 8000);
|
|
80
|
+
const followupDurationMs = Number(args["followup-duration"] || 5000);
|
|
81
|
+
const previewDurationMs = Number(args["preview-duration"] || 3500);
|
|
82
|
+
const mobileDurationMs = Number(args["mobile-duration"] || 3500);
|
|
83
|
+
const sessionsDurationMs = Number(args["sessions-duration"] || 2500);
|
|
84
|
+
const desktopWidth = Number(args.width || 1920);
|
|
85
|
+
const desktopHeight = Number(args.height || 1080);
|
|
86
|
+
const skipSessions = args["skip-sessions"] === true || args["skip-sessions"] === "true";
|
|
87
|
+
const skipPreview = args["skip-preview"] === true || args["skip-preview"] === "true";
|
|
88
|
+
const skipMobile = args["skip-mobile"] === true || args["skip-mobile"] === "true";
|
|
89
|
+
|
|
90
|
+
const preset = resolvePreset(args);
|
|
91
|
+
const prompt = readPrompt(args, "prompt", preset.prompt);
|
|
92
|
+
const followup = readPrompt(args, "followup", preset.followup);
|
|
93
|
+
|
|
94
|
+
fs.mkdirSync(outRoot, { recursive: true });
|
|
95
|
+
prepareSanitizedWorkspace(workspaceDir, {
|
|
96
|
+
reset: args["reset-workspace"] !== "false"
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
const launchCommand = buildSanitizedLaunchCommand(baseLaunchCommand, {
|
|
100
|
+
cwd: workspaceDir,
|
|
101
|
+
cwdLabel: "workspace",
|
|
102
|
+
homeDir: path.join(outRoot, "shell-home")
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
let state = startDaemon ? await startHopDaemon(isolatedHopHome) : getHopState(args["hop-home"]);
|
|
106
|
+
let previewServer = null;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const terminal = await ensureTerminal(state, {
|
|
110
|
+
name: sessionName,
|
|
111
|
+
cwd: workspaceDir,
|
|
112
|
+
cols: 140,
|
|
113
|
+
rows: 40
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await resizeTerminal(state, terminal.id, 140, 40);
|
|
117
|
+
await bestEffortOutputWait(state, terminal.id, 3000);
|
|
118
|
+
|
|
119
|
+
const allowedSessions = [sessionName];
|
|
120
|
+
|
|
121
|
+
if (!skipPreview) {
|
|
122
|
+
const previewPort = await findFreePort();
|
|
123
|
+
previewServer = await startStaticPreviewServer(workspaceDir, previewPort);
|
|
124
|
+
await ensurePortSession(state, previewSessionName, previewPort);
|
|
125
|
+
allowedSessions.push(previewSessionName);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const manifest = {
|
|
129
|
+
outRoot,
|
|
130
|
+
sessionName,
|
|
131
|
+
previewSessionName: skipPreview ? null : previewSessionName,
|
|
132
|
+
localUrl: state.localUrl,
|
|
133
|
+
publicUrl: state.publicUrl,
|
|
134
|
+
clips: []
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
if (!skipSessions) {
|
|
138
|
+
const clipDir = path.join(outRoot, "01-sessions");
|
|
139
|
+
await recordHopView(state, {
|
|
140
|
+
outDir: clipDir,
|
|
141
|
+
width: desktopWidth,
|
|
142
|
+
height: desktopHeight,
|
|
143
|
+
allowedSessions,
|
|
144
|
+
pageMode: "session-picker",
|
|
145
|
+
durationMs: sessionsDurationMs,
|
|
146
|
+
settleMs: 500
|
|
147
|
+
});
|
|
148
|
+
manifest.clips.push({ name: "sessions", dir: clipDir });
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
await execTerminal(state, terminal.id, launchCommand);
|
|
152
|
+
await bestEffortOutputWait(state, terminal.id, 6000);
|
|
153
|
+
await sleep(commandSettleMs);
|
|
154
|
+
|
|
155
|
+
if (prompt) {
|
|
156
|
+
await writeTerminal(state, terminal.id, `${prompt}\n`);
|
|
157
|
+
await bestEffortOutputWait(state, terminal.id, 2000);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const liveDir = path.join(outRoot, "02-agent-live");
|
|
161
|
+
await recordHopView(state, {
|
|
162
|
+
session: sessionName,
|
|
163
|
+
outDir: liveDir,
|
|
164
|
+
width: desktopWidth,
|
|
165
|
+
height: desktopHeight,
|
|
166
|
+
allowedSessions: [sessionName],
|
|
167
|
+
durationMs: desktopDurationMs,
|
|
168
|
+
settleMs: 500
|
|
169
|
+
});
|
|
170
|
+
manifest.clips.push({ name: "agent-live", dir: liveDir });
|
|
171
|
+
|
|
172
|
+
if (followup) {
|
|
173
|
+
await writeTerminal(state, terminal.id, `${followup}\n`);
|
|
174
|
+
await bestEffortOutputWait(state, terminal.id, 1500);
|
|
175
|
+
|
|
176
|
+
const followupDir = path.join(outRoot, "03-agent-redirect");
|
|
177
|
+
await recordHopView(state, {
|
|
178
|
+
session: sessionName,
|
|
179
|
+
outDir: followupDir,
|
|
180
|
+
width: desktopWidth,
|
|
181
|
+
height: desktopHeight,
|
|
182
|
+
allowedSessions: [sessionName],
|
|
183
|
+
durationMs: followupDurationMs,
|
|
184
|
+
settleMs: 500
|
|
185
|
+
});
|
|
186
|
+
manifest.clips.push({ name: "agent-redirect", dir: followupDir });
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (!skipPreview) {
|
|
190
|
+
const previewDir = path.join(outRoot, "04-preview");
|
|
191
|
+
await recordHopView(state, {
|
|
192
|
+
session: previewSessionName,
|
|
193
|
+
outDir: previewDir,
|
|
194
|
+
width: desktopWidth,
|
|
195
|
+
height: desktopHeight,
|
|
196
|
+
allowedSessions,
|
|
197
|
+
durationMs: previewDurationMs,
|
|
198
|
+
settleMs: 500
|
|
199
|
+
});
|
|
200
|
+
manifest.clips.push({ name: "preview", dir: previewDir });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (!skipMobile) {
|
|
204
|
+
const mobileDir = path.join(outRoot, skipPreview ? "04-mobile" : "05-mobile");
|
|
205
|
+
await recordHopView(state, {
|
|
206
|
+
session: sessionName,
|
|
207
|
+
outDir: mobileDir,
|
|
208
|
+
mobile: true,
|
|
209
|
+
allowedSessions: [sessionName],
|
|
210
|
+
durationMs: mobileDurationMs,
|
|
211
|
+
settleMs: 500
|
|
212
|
+
});
|
|
213
|
+
manifest.clips.push({ name: "mobile", dir: mobileDir });
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const manifestPath = path.join(outRoot, "manifest.json");
|
|
217
|
+
fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
218
|
+
|
|
219
|
+
console.log(JSON.stringify({
|
|
220
|
+
ok: true,
|
|
221
|
+
outRoot,
|
|
222
|
+
manifest: manifestPath,
|
|
223
|
+
sessionName,
|
|
224
|
+
previewSessionName: skipPreview ? null : previewSessionName,
|
|
225
|
+
workspaceDir,
|
|
226
|
+
hopHome: isolatedHopHome,
|
|
227
|
+
clips: manifest.clips
|
|
228
|
+
}, null, 2));
|
|
229
|
+
} finally {
|
|
230
|
+
if (previewServer) {
|
|
231
|
+
previewServer.kill("SIGTERM");
|
|
232
|
+
}
|
|
233
|
+
if (stopDaemon) {
|
|
234
|
+
stopHopDaemon(isolatedHopHome);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import {
|
|
3
|
+
closeTerminal,
|
|
4
|
+
ensureTerminal,
|
|
5
|
+
execTerminal,
|
|
6
|
+
getHopState,
|
|
7
|
+
parseArgs,
|
|
8
|
+
resizeTerminal,
|
|
9
|
+
waitForTerminalOutput,
|
|
10
|
+
writeTerminal
|
|
11
|
+
} from "./hop-demo-lib.mjs";
|
|
12
|
+
|
|
13
|
+
const args = parseArgs(process.argv.slice(2));
|
|
14
|
+
const state = getHopState(args["hop-home"]);
|
|
15
|
+
const name = args.name || "demo-agent";
|
|
16
|
+
const cwd = args.cwd ? path.resolve(args.cwd) : process.cwd();
|
|
17
|
+
const cols = Number(args.cols || 140);
|
|
18
|
+
const rows = Number(args.rows || 40);
|
|
19
|
+
const kill = args.kill === true || args.kill === "true";
|
|
20
|
+
const readyTimeoutMs = Number(args["ready-timeout"] || 3000);
|
|
21
|
+
const skipReadyWait = args["skip-ready-wait"] === true || args["skip-ready-wait"] === "true";
|
|
22
|
+
|
|
23
|
+
const terminal = await ensureTerminal(state, {
|
|
24
|
+
name,
|
|
25
|
+
cwd,
|
|
26
|
+
cols,
|
|
27
|
+
rows
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
await resizeTerminal(state, terminal.id, cols, rows);
|
|
31
|
+
|
|
32
|
+
if (!skipReadyWait && (args.exec || args.write)) {
|
|
33
|
+
await waitForTerminalOutput(state, terminal.id, {
|
|
34
|
+
timeoutMs: readyTimeoutMs
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (args.exec) {
|
|
39
|
+
await execTerminal(state, terminal.id, String(args.exec));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (args.write) {
|
|
43
|
+
await writeTerminal(state, terminal.id, String(args.write));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (args["wait-pattern"] || args["wait-timeout"]) {
|
|
47
|
+
const waitResult = await waitForTerminalOutput(state, terminal.id, {
|
|
48
|
+
pattern: args["wait-pattern"],
|
|
49
|
+
timeoutMs: Number(args["wait-timeout"] || 15000)
|
|
50
|
+
});
|
|
51
|
+
console.log(JSON.stringify({
|
|
52
|
+
ok: true,
|
|
53
|
+
terminalId: terminal.id,
|
|
54
|
+
sessionName: terminal.sessionName,
|
|
55
|
+
displayName: terminal.displayName,
|
|
56
|
+
waitMatched: Boolean(waitResult.payload),
|
|
57
|
+
outputPreview: waitResult.text.slice(-500)
|
|
58
|
+
}, null, 2));
|
|
59
|
+
} else {
|
|
60
|
+
console.log(JSON.stringify({
|
|
61
|
+
ok: true,
|
|
62
|
+
terminalId: terminal.id,
|
|
63
|
+
sessionName: terminal.sessionName,
|
|
64
|
+
displayName: terminal.displayName
|
|
65
|
+
}, null, 2));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (kill) {
|
|
69
|
+
await closeTerminal(state, terminal.id, true);
|
|
70
|
+
}
|