@zonease/happy 0.6.38 → 0.6.40
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/bin/happy.mjs +10 -0
- package/dist/{types-DvTsFaqR.cjs → api-C7FyJfZ1.cjs} +400 -1964
- package/dist/{types-BWpRU1VN.mjs → api-D6WL40-U.mjs} +220 -1729
- package/dist/auth-C7ZScWEw.cjs +274 -0
- package/dist/auth-DCsIoZLT.cjs +183 -0
- package/dist/auth-DO9GexAR.mjs +181 -0
- package/dist/auth-DxJ3VvrX.mjs +270 -0
- package/dist/browser-AoVGRX7y.mjs +20 -0
- package/dist/browser-DzgO4oaD.cjs +22 -0
- package/dist/claudeLocal-C6BdzhDx.cjs +352 -0
- package/dist/claudeLocal-CWNGpJ0-.mjs +344 -0
- package/dist/codex/happyMcpStdioBridge.cjs +4 -1
- package/dist/codex/happyMcpStdioBridge.mjs +4 -1
- package/dist/common-BdNgghUJ.mjs +75 -0
- package/dist/common-CZvSRU4p.cjs +83 -0
- package/dist/{config-0Xnc8ARM.cjs → config-CclB3QW9.cjs} +21 -34
- package/dist/{config-_GW4YBCP.mjs → config-DZk-mJ0K.mjs} +7 -20
- package/dist/connect-BXLUKJ5P.mjs +679 -0
- package/dist/connect-k9CqDJ-T.cjs +681 -0
- package/dist/{constants-B4x1K7by.cjs → constants-E_UWYTo3.cjs} +2 -2
- package/dist/{constants-BO9OAFOT.mjs → constants-pmXZGibX.mjs} +1 -1
- package/dist/controlClient-B23M-WPp.cjs +177 -0
- package/dist/controlClient-DB5tDU7V.mjs +167 -0
- package/dist/deviceKeys-BTdo34Uz.mjs +175 -0
- package/dist/deviceKeys-C540DcXl.cjs +182 -0
- package/dist/diagnose-B3D1a_v0.cjs +60 -0
- package/dist/diagnose-CuRKXxt1.mjs +58 -0
- package/dist/doctor-BQGLhrcI.cjs +214 -0
- package/dist/doctor-C08m5Z9f.cjs +78 -0
- package/dist/doctor-CU4JLGQ1.mjs +211 -0
- package/dist/doctor-ON7pN5G5.mjs +74 -0
- package/dist/downloadImage-DZXI3mXL.mjs +887 -0
- package/dist/downloadImage-cQ1Z7hkC.cjs +897 -0
- package/dist/enroll-B55BQR56.mjs +113 -0
- package/dist/enroll-Bo-CNio1.cjs +115 -0
- package/dist/evidence-DQp8QWcv.cjs +129 -0
- package/dist/evidence-DTYRAaOZ.mjs +127 -0
- package/dist/formatImageMessage-CixDYY6G.cjs +46 -0
- package/dist/formatImageMessage-DbKSSjMx.mjs +43 -0
- package/dist/github-DKcyF1zp.mjs +57 -0
- package/dist/github-h9GhJJ_C.cjs +59 -0
- package/dist/index-CDTNgfUf.cjs +578 -0
- package/dist/index-CPKdvi7J.mjs +554 -0
- package/dist/index.cjs +812 -40
- package/dist/index.mjs +813 -40
- package/dist/install-B1e6rdy7.mjs +94 -0
- package/dist/install-CgUPGIoF.mjs +95 -0
- package/dist/install-D7Ir7ciZ.cjs +96 -0
- package/dist/install-DSDufaPd.cjs +98 -0
- package/dist/lib.cjs +18 -12
- package/dist/lib.d.cts +77 -0
- package/dist/lib.d.mts +77 -0
- package/dist/lib.mjs +13 -7
- package/dist/{loadableToolOutput-va3RLplh.mjs → loadableToolOutput-Bfei9wPq.mjs} +4 -2
- package/dist/{loadableToolOutput-5vdQB98M.cjs → loadableToolOutput-CbLGtvST.cjs} +63 -61
- package/dist/mcpToolSchemas-CjeTfrAs.mjs +62 -0
- package/dist/mcpToolSchemas-DK42yoZ_.cjs +64 -0
- package/dist/persistence-CBTtC_dm.cjs +996 -0
- package/dist/persistence-DKfF81pd.mjs +944 -0
- package/dist/projectPath-BTBIMB57.mjs +10 -0
- package/dist/projectPath-ChLhdOyt.cjs +13 -0
- package/dist/prompt-BTWDZZO0.cjs +25 -0
- package/dist/prompt-BsAZsPbo.mjs +22 -0
- package/dist/release-BMEzNONE.mjs +168 -0
- package/dist/release-EhOJeAZe.cjs +171 -0
- package/dist/run-BVZsMNwN.mjs +2164 -0
- package/dist/run-CURx7LhK.cjs +2188 -0
- package/dist/runClaude-6Gi0sYQy.mjs +3763 -0
- package/dist/runClaude-CL3Kt5ex.cjs +3766 -0
- package/dist/{runCodex-DiKzBf9q.mjs → runCodex-ClwEgJ7M.mjs} +36 -20
- package/dist/{runCodex-skq0j05d.cjs → runCodex-YKyVBkJ3.cjs} +169 -153
- package/dist/{runGemini-BeqrVq8R.mjs → runGemini-BPWxVr60.mjs} +34 -17
- package/dist/{runGemini-CVp-CEIh.cjs → runGemini-Xz2W1spn.cjs} +212 -195
- package/dist/{runOneShot-ltCpHbBB.mjs → runOneShot-CdCWiiLr.mjs} +14 -38
- package/dist/{runOneShot-BQNkNW61.cjs → runOneShot-DjZyCvDI.cjs} +31 -55
- package/dist/{serve-BNdoEDVx.mjs → serve-B48JttQ3.mjs} +12 -35
- package/dist/{serve-BECdtivY.cjs → serve-B8vaWQA0.cjs} +35 -58
- package/dist/session-BXboDn2V.mjs +108 -0
- package/dist/session-BZO25zxp.cjs +110 -0
- package/dist/sessionKeyCache-B6cGadYL.mjs +33 -0
- package/dist/sessionKeyCache-CDmexJ9p.cjs +36 -0
- package/dist/sessionList-CgAYZpNj.mjs +51 -0
- package/dist/sessionList-lQ-nqEhc.cjs +54 -0
- package/dist/sessionSummary-CcORiQSq.cjs +393 -0
- package/dist/sessionSummary-DCWQoaDP.mjs +387 -0
- package/dist/spawnHappyCLI-KiazE3kL.cjs +70 -0
- package/dist/spawnHappyCLI-yBIP_Oqi.mjs +66 -0
- package/dist/ssh-q5uZhqBf.mjs +359 -0
- package/dist/ssh-yMllSHu5.cjs +365 -0
- package/dist/systemdScope-BeOtAkhk.mjs +25 -0
- package/dist/systemdScope-DW3-UoKB.cjs +29 -0
- package/dist/task-_nr7J19j.cjs +167 -0
- package/dist/task-wNX1N2i4.mjs +165 -0
- package/dist/time-Bj2HRkKH.cjs +33 -0
- package/dist/time-DgsBLJA_.mjs +29 -0
- package/dist/trimIdent-4gvk9Wk6.cjs +22 -0
- package/dist/trimIdent-BAjmv_TG.mjs +20 -0
- package/dist/types-BoJh4Qfv.mjs +57 -0
- package/dist/types-DWeB7aqh.cjs +59 -0
- package/dist/uninstall-Buaz7d-X.cjs +77 -0
- package/dist/uninstall-Dxf6_4mQ.mjs +75 -0
- package/dist/update-CGlIfa8e.cjs +111 -0
- package/dist/update-CnnoX2cV.mjs +109 -0
- package/dist/workflowCommon-B0d01Y_a.mjs +56 -0
- package/dist/workflowCommon-Bl568-rt.cjs +62 -0
- package/dist/workflowReports-BEWuQesA.cjs +113 -0
- package/dist/workflowReports-Dd8-4-8f.mjs +108 -0
- package/package.json +1 -1
- package/dist/index-BbCDDvoB.cjs +0 -11376
- package/dist/index-vvZXdswy.mjs +0 -11336
- package/dist/mcpToolSchemas-CdPXJQmb.mjs +0 -175
- package/dist/mcpToolSchemas-f-kttjFw.cjs +0 -186
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var socket_ioClient = require('socket.io-client');
|
|
4
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
5
|
+
var deviceKeys = require('./deviceKeys-C540DcXl.cjs');
|
|
6
|
+
var controlClient = require('./controlClient-B23M-WPp.cjs');
|
|
7
|
+
require('node:fs/promises');
|
|
8
|
+
require('node:fs');
|
|
9
|
+
require('node:os');
|
|
10
|
+
require('node:path');
|
|
11
|
+
require('zod');
|
|
12
|
+
require('node:crypto');
|
|
13
|
+
require('tweetnacl');
|
|
14
|
+
require('chalk');
|
|
15
|
+
require('fs');
|
|
16
|
+
require('axios');
|
|
17
|
+
require('./auth-C7ZScWEw.cjs');
|
|
18
|
+
require('qrcode-terminal');
|
|
19
|
+
require('./time-Bj2HRkKH.cjs');
|
|
20
|
+
require('./browser-DzgO4oaD.cjs');
|
|
21
|
+
require('open');
|
|
22
|
+
require('react');
|
|
23
|
+
require('ink');
|
|
24
|
+
require('./projectPath-ChLhdOyt.cjs');
|
|
25
|
+
require('path');
|
|
26
|
+
require('url');
|
|
27
|
+
|
|
28
|
+
function matchDevice(devices, query) {
|
|
29
|
+
const lowered = query.trim().toLowerCase();
|
|
30
|
+
return devices.find((device) => device.id === query) ?? devices.find((device) => device.name.toLowerCase() === lowered) ?? devices.find((device) => device.name.toLowerCase().startsWith(lowered)) ?? devices.find((device) => device.name.toLowerCase().includes(lowered)) ?? null;
|
|
31
|
+
}
|
|
32
|
+
const DEFAULT_COMMAND_TIMEOUT_MS = 3e5;
|
|
33
|
+
function parseSshArgs(args) {
|
|
34
|
+
const separator = args.indexOf("--");
|
|
35
|
+
const head = separator >= 0 ? args.slice(0, separator) : args;
|
|
36
|
+
const command = separator >= 0 ? args.slice(separator + 1).join(" ") : null;
|
|
37
|
+
let target = null;
|
|
38
|
+
let timeoutMs = DEFAULT_COMMAND_TIMEOUT_MS;
|
|
39
|
+
let stream = false;
|
|
40
|
+
for (let i = 0; i < head.length; i++) {
|
|
41
|
+
if (head[i] === "--stream" || head[i] === "-s") {
|
|
42
|
+
stream = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (head[i] === "--timeout" || head[i] === "-t") {
|
|
46
|
+
const seconds = Number(head[++i]);
|
|
47
|
+
if (Number.isFinite(seconds) && seconds > 0) timeoutMs = seconds * 1e3;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (!target && !head[i].startsWith("-")) target = head[i];
|
|
51
|
+
}
|
|
52
|
+
return { target, command: command && command.trim() ? command : null, timeoutMs, stream };
|
|
53
|
+
}
|
|
54
|
+
async function resolveFromCache(query) {
|
|
55
|
+
const names = await deviceKeys.readCachedDeviceNames();
|
|
56
|
+
const id = pickCachedDeviceId(names, query);
|
|
57
|
+
if (!id) return null;
|
|
58
|
+
const material = await deviceKeys.readCachedDeviceKey(id);
|
|
59
|
+
return material ? { device: { id, name: names[id] }, ...material } : null;
|
|
60
|
+
}
|
|
61
|
+
function pickCachedDeviceId(names, query) {
|
|
62
|
+
if (names[query]) return query;
|
|
63
|
+
const lowered = query.trim().toLowerCase();
|
|
64
|
+
for (const test of [
|
|
65
|
+
(name) => name === lowered,
|
|
66
|
+
(name) => name.startsWith(lowered),
|
|
67
|
+
(name) => name.includes(lowered)
|
|
68
|
+
]) {
|
|
69
|
+
const matches = Object.keys(names).filter((id) => test(names[id].toLowerCase()));
|
|
70
|
+
if (matches.length === 1) return matches[0];
|
|
71
|
+
if (matches.length > 1) return null;
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
async function resolveFromServer(credentials, query) {
|
|
76
|
+
const { devices } = await listDevicesWithNames(credentials);
|
|
77
|
+
const device = matchDevice(devices, query);
|
|
78
|
+
if (!device) {
|
|
79
|
+
console.error(`No enrolled device matches "${query}". Run "happy ssh" with no arguments to list devices.`);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
if (!device.active) {
|
|
83
|
+
console.error(`Device "${device.name}" is offline.`);
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
const { key, variant } = await deviceKeys.ensureDeviceKey(credentials, device.id, () => {
|
|
87
|
+
console.error("Waiting for approval in the Happy app (Devices \u2192 pending request)...");
|
|
88
|
+
});
|
|
89
|
+
await deviceKeys.pruneDeviceAliases(device.id, device.name);
|
|
90
|
+
return { device, key, variant };
|
|
91
|
+
}
|
|
92
|
+
async function runOnDevice(credentials, query, command, options = {}) {
|
|
93
|
+
const cached = await resolveFromCache(query);
|
|
94
|
+
const resolved = cached ?? await resolveFromServer(credentials, query);
|
|
95
|
+
if (!resolved) return 1;
|
|
96
|
+
if (options.stream) {
|
|
97
|
+
return await streamOnResolved(credentials, resolved, command, options);
|
|
98
|
+
}
|
|
99
|
+
const viaDaemon = await controlClient.execOnDeviceViaDaemon(
|
|
100
|
+
resolved.device.id,
|
|
101
|
+
command,
|
|
102
|
+
options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS
|
|
103
|
+
).catch(() => null);
|
|
104
|
+
if (viaDaemon) {
|
|
105
|
+
return reportResult(viaDaemon);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
return await execOnResolved(credentials, resolved, command, options);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
if (!cached) throw error;
|
|
111
|
+
const fresh = await resolveFromServer(credentials, query);
|
|
112
|
+
if (!fresh) return 1;
|
|
113
|
+
return await execOnResolved(credentials, fresh, command, options);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function reportResult(result) {
|
|
117
|
+
if (result.stdout) process.stdout.write(result.stdout);
|
|
118
|
+
if (result.stderr) process.stderr.write(result.stderr);
|
|
119
|
+
if (result.error && !result.error.startsWith("Command failed:")) {
|
|
120
|
+
console.error(result.error);
|
|
121
|
+
}
|
|
122
|
+
return result.exitCode ?? (result.success ? 0 : 1);
|
|
123
|
+
}
|
|
124
|
+
async function streamOnResolved(credentials, { device, key, variant }, command, options) {
|
|
125
|
+
const socket = socket_ioClient.io(logger.configuration.serverUrl, {
|
|
126
|
+
auth: { token: credentials.token, clientType: "user-scoped" },
|
|
127
|
+
path: "/v1/updates",
|
|
128
|
+
transports: ["websocket"]
|
|
129
|
+
});
|
|
130
|
+
await new Promise((resolve, reject) => {
|
|
131
|
+
socket.once("connect", () => resolve());
|
|
132
|
+
socket.once("connect_error", reject);
|
|
133
|
+
});
|
|
134
|
+
const timeoutMs = options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
|
|
135
|
+
try {
|
|
136
|
+
const answer = await socket.timeout(3e4).emitWithAck("rpc-call", {
|
|
137
|
+
method: `${device.id}:pty-start`,
|
|
138
|
+
params: logger.encodeBase64(logger.encrypt(key, variant, {
|
|
139
|
+
cols: process.stdout.columns || 80,
|
|
140
|
+
rows: process.stdout.rows || 24,
|
|
141
|
+
exec: { command, timeoutMs }
|
|
142
|
+
}))
|
|
143
|
+
});
|
|
144
|
+
if (!answer?.ok) throw new Error(answer?.error || "pty-start failed");
|
|
145
|
+
const started = logger.decrypt(key, variant, logger.decodeBase64(answer.result));
|
|
146
|
+
if (!started?.ok || !started.ptyId) {
|
|
147
|
+
throw new Error(started?.error ?? "Device refused to start the command");
|
|
148
|
+
}
|
|
149
|
+
return await new Promise((resolve) => {
|
|
150
|
+
const execId = started.ptyId;
|
|
151
|
+
const guard = setTimeout(() => resolve(1), timeoutMs + 3e4);
|
|
152
|
+
socket.on("pty-output", (envelope) => {
|
|
153
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
|
|
154
|
+
try {
|
|
155
|
+
const frame = logger.decrypt(key, variant, logger.decodeBase64(envelope.data));
|
|
156
|
+
if (typeof frame?.chunk !== "string") return;
|
|
157
|
+
const target = frame.stream === "stderr" ? process.stderr : process.stdout;
|
|
158
|
+
target.write(Buffer.from(frame.chunk, "base64"));
|
|
159
|
+
} catch {
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
socket.on("pty-exit", (envelope) => {
|
|
163
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== execId) return;
|
|
164
|
+
clearTimeout(guard);
|
|
165
|
+
resolve(typeof envelope.exitCode === "number" ? envelope.exitCode : 1);
|
|
166
|
+
});
|
|
167
|
+
socket.on("disconnect", () => {
|
|
168
|
+
clearTimeout(guard);
|
|
169
|
+
resolve(1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
} finally {
|
|
173
|
+
socket.close();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
async function execOnResolved(credentials, { device, key, variant }, command, options) {
|
|
177
|
+
const socket = socket_ioClient.io(logger.configuration.serverUrl, {
|
|
178
|
+
auth: { token: credentials.token, clientType: "user-scoped" },
|
|
179
|
+
path: "/v1/updates",
|
|
180
|
+
transports: ["websocket"]
|
|
181
|
+
});
|
|
182
|
+
await new Promise((resolve, reject) => {
|
|
183
|
+
socket.once("connect", () => resolve());
|
|
184
|
+
socket.once("connect_error", reject);
|
|
185
|
+
});
|
|
186
|
+
try {
|
|
187
|
+
const result = await deviceKeys.deviceExec(socket, credentials, device.id, command, {
|
|
188
|
+
deviceKeyBase64: logger.encodeBase64(key),
|
|
189
|
+
deviceKeyVariant: variant,
|
|
190
|
+
timeout: options.timeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS,
|
|
191
|
+
// matchDevice already read the listing and rejected offline devices.
|
|
192
|
+
skipMachineLookup: true
|
|
193
|
+
});
|
|
194
|
+
return reportResult(result);
|
|
195
|
+
} finally {
|
|
196
|
+
socket.close();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
async function sshDevice(credentials, query) {
|
|
200
|
+
const { devices } = await listDevicesWithNames(credentials);
|
|
201
|
+
const device = matchDevice(devices, query);
|
|
202
|
+
if (!device) {
|
|
203
|
+
console.error(`No enrolled device matches "${query}". Run "happy ssh" with no arguments to list devices.`);
|
|
204
|
+
return 1;
|
|
205
|
+
}
|
|
206
|
+
if (!device.active) {
|
|
207
|
+
console.error(`Device "${device.name}" is offline.`);
|
|
208
|
+
return 1;
|
|
209
|
+
}
|
|
210
|
+
const { key, variant } = await deviceKeys.ensureDeviceKey(credentials, device.id, () => {
|
|
211
|
+
console.log("Waiting for approval in the Happy app (Devices \u2192 pending request)...");
|
|
212
|
+
});
|
|
213
|
+
const socket = socket_ioClient.io(logger.configuration.serverUrl, {
|
|
214
|
+
auth: { token: credentials.token, clientType: "user-scoped" },
|
|
215
|
+
path: "/v1/updates",
|
|
216
|
+
transports: ["websocket"]
|
|
217
|
+
});
|
|
218
|
+
await new Promise((resolve, reject) => {
|
|
219
|
+
socket.once("connect", () => resolve());
|
|
220
|
+
socket.once("connect_error", reject);
|
|
221
|
+
});
|
|
222
|
+
const rpc = async (method, params) => {
|
|
223
|
+
const answer = await socket.timeout(3e4).emitWithAck("rpc-call", {
|
|
224
|
+
method: `${device.id}:${method}`,
|
|
225
|
+
params: logger.encodeBase64(logger.encrypt(key, variant, params))
|
|
226
|
+
});
|
|
227
|
+
if (!answer?.ok) throw new Error(answer?.error || `${method} failed`);
|
|
228
|
+
return logger.decrypt(key, variant, logger.decodeBase64(answer.result));
|
|
229
|
+
};
|
|
230
|
+
const cols = process.stdout.columns || 80;
|
|
231
|
+
const rows = process.stdout.rows || 24;
|
|
232
|
+
const started = await rpc("pty-start", { cols, rows });
|
|
233
|
+
if (!started?.ok || !started.ptyId) {
|
|
234
|
+
socket.close();
|
|
235
|
+
console.error(`Could not open a shell on "${device.name}": ${started?.error ?? "unknown error"}`);
|
|
236
|
+
return 1;
|
|
237
|
+
}
|
|
238
|
+
const ptyId = started.ptyId;
|
|
239
|
+
let exitCode = 0;
|
|
240
|
+
const cleanup = () => {
|
|
241
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
242
|
+
process.stdin.pause();
|
|
243
|
+
socket.close();
|
|
244
|
+
};
|
|
245
|
+
await new Promise((resolve) => {
|
|
246
|
+
socket.on("pty-output", (envelope) => {
|
|
247
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== ptyId) return;
|
|
248
|
+
try {
|
|
249
|
+
const data = logger.decrypt(key, variant, logger.decodeBase64(envelope.data));
|
|
250
|
+
if (typeof data === "string") process.stdout.write(Buffer.from(data, "base64"));
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
socket.on("pty-exit", (envelope) => {
|
|
255
|
+
if (envelope?.sessionId !== device.id || envelope?.ptyId !== ptyId) return;
|
|
256
|
+
exitCode = typeof envelope.exitCode === "number" ? envelope.exitCode : 0;
|
|
257
|
+
resolve();
|
|
258
|
+
});
|
|
259
|
+
socket.on("disconnect", () => resolve());
|
|
260
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
261
|
+
process.stdin.resume();
|
|
262
|
+
process.stdin.on("data", (chunk) => {
|
|
263
|
+
socket.volatile.emit("pty-input", {
|
|
264
|
+
sessionId: device.id,
|
|
265
|
+
ptyId,
|
|
266
|
+
data: logger.encodeBase64(logger.encrypt(key, variant, chunk.toString("utf8")))
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
process.stdout.on("resize", () => {
|
|
270
|
+
void rpc("pty-resize", {
|
|
271
|
+
ptyId,
|
|
272
|
+
cols: process.stdout.columns || 80,
|
|
273
|
+
rows: process.stdout.rows || 24
|
|
274
|
+
}).catch(() => {
|
|
275
|
+
});
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
await rpc("pty-close", { ptyId }).catch(() => {
|
|
279
|
+
});
|
|
280
|
+
cleanup();
|
|
281
|
+
return exitCode;
|
|
282
|
+
}
|
|
283
|
+
function pickDevice(devices) {
|
|
284
|
+
const selectable = devices.filter((device) => device.active);
|
|
285
|
+
if (selectable.length === 0) {
|
|
286
|
+
console.log("No devices are online right now.");
|
|
287
|
+
return Promise.resolve(null);
|
|
288
|
+
}
|
|
289
|
+
if (selectable.length === 1) {
|
|
290
|
+
return Promise.resolve(selectable[0]);
|
|
291
|
+
}
|
|
292
|
+
return new Promise((resolve) => {
|
|
293
|
+
let index = 0;
|
|
294
|
+
let rendered = 0;
|
|
295
|
+
const render = () => {
|
|
296
|
+
if (rendered > 0) process.stdout.write(`\x1B[${rendered}A`);
|
|
297
|
+
const lines = selectable.map((device, position) => {
|
|
298
|
+
const marker = position === index ? "\x1B[36m>\x1B[0m" : " ";
|
|
299
|
+
const name = position === index ? `\x1B[1m${device.name}\x1B[0m` : device.name;
|
|
300
|
+
return `\x1B[2K ${marker} ${name}${device.platform ? ` \x1B[2m${device.platform}\x1B[0m` : ""}`;
|
|
301
|
+
});
|
|
302
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
303
|
+
rendered = lines.length;
|
|
304
|
+
};
|
|
305
|
+
const finish = (device) => {
|
|
306
|
+
process.stdin.off("data", onData);
|
|
307
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
308
|
+
process.stdin.pause();
|
|
309
|
+
resolve(device);
|
|
310
|
+
};
|
|
311
|
+
const onData = (chunk) => {
|
|
312
|
+
const key = chunk.toString();
|
|
313
|
+
if (key === "\x1B[A" || key === "k") {
|
|
314
|
+
index = (index - 1 + selectable.length) % selectable.length;
|
|
315
|
+
render();
|
|
316
|
+
} else if (key === "\x1B[B" || key === "j") {
|
|
317
|
+
index = (index + 1) % selectable.length;
|
|
318
|
+
render();
|
|
319
|
+
} else if (key === "\r" || key === "\n") {
|
|
320
|
+
finish(selectable[index]);
|
|
321
|
+
} else if (key === "" || key === "\x1B" || key === "q") {
|
|
322
|
+
console.log("Cancelled.");
|
|
323
|
+
finish(null);
|
|
324
|
+
}
|
|
325
|
+
};
|
|
326
|
+
console.log("Select a device (\u2191/\u2193, Enter to connect, q to cancel):");
|
|
327
|
+
render();
|
|
328
|
+
if (process.stdin.isTTY) process.stdin.setRawMode(true);
|
|
329
|
+
process.stdin.resume();
|
|
330
|
+
process.stdin.on("data", onData);
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
async function listDevicesWithNames(credentials) {
|
|
334
|
+
const [devices, names] = await Promise.all([deviceKeys.listDevices(credentials), deviceKeys.readCachedDeviceNames()]);
|
|
335
|
+
let named = false;
|
|
336
|
+
const withNames = devices.map((device) => {
|
|
337
|
+
const cached = names[device.id];
|
|
338
|
+
if (cached) named = true;
|
|
339
|
+
return cached ? { ...device, name: cached } : device;
|
|
340
|
+
});
|
|
341
|
+
return { devices: withNames, named };
|
|
342
|
+
}
|
|
343
|
+
async function sshPickAndConnect(credentials) {
|
|
344
|
+
const { devices } = await listDevicesWithNames(credentials);
|
|
345
|
+
if (devices.length === 0) {
|
|
346
|
+
console.log("No devices enrolled yet. Add one from Happy \u2192 Devices.");
|
|
347
|
+
return 0;
|
|
348
|
+
}
|
|
349
|
+
if (!process.stdin.isTTY) {
|
|
350
|
+
for (const device of devices) {
|
|
351
|
+
console.log(`${device.active ? "online " : "offline"} ${device.name}${device.platform ? ` (${device.platform})` : ""}`);
|
|
352
|
+
}
|
|
353
|
+
console.log("\nUsage: happy ssh <device>");
|
|
354
|
+
return 0;
|
|
355
|
+
}
|
|
356
|
+
const chosen = await pickDevice(devices);
|
|
357
|
+
if (!chosen) return 0;
|
|
358
|
+
return await sshDevice(credentials, chosen.id);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
exports.parseSshArgs = parseSshArgs;
|
|
362
|
+
exports.pickCachedDeviceId = pickCachedDeviceId;
|
|
363
|
+
exports.runOnDevice = runOnDevice;
|
|
364
|
+
exports.sshDevice = sshDevice;
|
|
365
|
+
exports.sshPickAndConnect = sshPickAndConnect;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
const SERVICE_NAME = "happy-daemon.service";
|
|
5
|
+
function systemdScopeFor(isRoot) {
|
|
6
|
+
if (isRoot) {
|
|
7
|
+
return {
|
|
8
|
+
system: true,
|
|
9
|
+
unitPath: join("/etc/systemd/system", SERVICE_NAME),
|
|
10
|
+
systemctlFlag: "",
|
|
11
|
+
wantedBy: "multi-user.target"
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
system: false,
|
|
16
|
+
unitPath: join(os.homedir(), ".config", "systemd", "user", SERVICE_NAME),
|
|
17
|
+
systemctlFlag: "--user",
|
|
18
|
+
wantedBy: "default.target"
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function isRootUser() {
|
|
22
|
+
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { SERVICE_NAME as S, isRootUser as i, systemdScopeFor as s };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var path = require('node:path');
|
|
4
|
+
var os = require('node:os');
|
|
5
|
+
|
|
6
|
+
const SERVICE_NAME = "happy-daemon.service";
|
|
7
|
+
function systemdScopeFor(isRoot) {
|
|
8
|
+
if (isRoot) {
|
|
9
|
+
return {
|
|
10
|
+
system: true,
|
|
11
|
+
unitPath: path.join("/etc/systemd/system", SERVICE_NAME),
|
|
12
|
+
systemctlFlag: "",
|
|
13
|
+
wantedBy: "multi-user.target"
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return {
|
|
17
|
+
system: false,
|
|
18
|
+
unitPath: path.join(os.homedir(), ".config", "systemd", "user", SERVICE_NAME),
|
|
19
|
+
systemctlFlag: "--user",
|
|
20
|
+
wantedBy: "default.target"
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
function isRootUser() {
|
|
24
|
+
return typeof process.getuid === "function" && process.getuid() === 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
exports.SERVICE_NAME = SERVICE_NAME;
|
|
28
|
+
exports.isRootUser = isRootUser;
|
|
29
|
+
exports.systemdScopeFor = systemdScopeFor;
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var chalk = require('chalk');
|
|
4
|
+
var path = require('node:path');
|
|
5
|
+
var logger = require('./persistence-CBTtC_dm.cjs');
|
|
6
|
+
var sessionList = require('./sessionList-lQ-nqEhc.cjs');
|
|
7
|
+
var sessionSummary = require('./sessionSummary-CcORiQSq.cjs');
|
|
8
|
+
var workflowCommon = require('./workflowCommon-Bl568-rt.cjs');
|
|
9
|
+
require('node:fs/promises');
|
|
10
|
+
require('node:fs');
|
|
11
|
+
require('node:os');
|
|
12
|
+
require('zod');
|
|
13
|
+
require('node:crypto');
|
|
14
|
+
require('tweetnacl');
|
|
15
|
+
require('fs');
|
|
16
|
+
require('axios');
|
|
17
|
+
require('./auth-C7ZScWEw.cjs');
|
|
18
|
+
require('qrcode-terminal');
|
|
19
|
+
require('./time-Bj2HRkKH.cjs');
|
|
20
|
+
require('./browser-DzgO4oaD.cjs');
|
|
21
|
+
require('open');
|
|
22
|
+
require('react');
|
|
23
|
+
require('ink');
|
|
24
|
+
require('./sessionKeyCache-CDmexJ9p.cjs');
|
|
25
|
+
|
|
26
|
+
function printTaskHelp() {
|
|
27
|
+
console.log(`
|
|
28
|
+
${chalk.bold("happy task")} - Agent handoff helpers
|
|
29
|
+
|
|
30
|
+
${chalk.bold("Usage:")}
|
|
31
|
+
happy task brief --session <sessionId> [options]
|
|
32
|
+
happy task brief <sessionId> [options]
|
|
33
|
+
happy task brief --recent [--project <path>] [options]
|
|
34
|
+
|
|
35
|
+
${chalk.bold("Options:")}
|
|
36
|
+
--session <id> Happy session ID to summarize
|
|
37
|
+
--recent Pick the most recent session, optionally filtered by --project
|
|
38
|
+
--project <path> Project path filter for --recent
|
|
39
|
+
--limit <n> Raw messages to compact (default: 200)
|
|
40
|
+
--max-turns <n> Compact turns to include (default: 80)
|
|
41
|
+
--text-limit <n> Max chars per text block (default: 800)
|
|
42
|
+
--out <path> Write the brief to a markdown file
|
|
43
|
+
--json Print machine-readable JSON
|
|
44
|
+
-h, --help Show this help
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
function collectSessionText(summary) {
|
|
48
|
+
const chunks = [];
|
|
49
|
+
if (summary.storedSummary) chunks.push(summary.storedSummary);
|
|
50
|
+
for (const turn of summary.turns) {
|
|
51
|
+
if (turn.user?.text) chunks.push(turn.user.text);
|
|
52
|
+
for (const text of turn.assistantTexts) chunks.push(text.text);
|
|
53
|
+
}
|
|
54
|
+
return chunks.join("\n");
|
|
55
|
+
}
|
|
56
|
+
function extractMatchingLines(text, patterns, limit) {
|
|
57
|
+
const seen = /* @__PURE__ */ new Set();
|
|
58
|
+
const lines = [];
|
|
59
|
+
for (const line of text.split("\n")) {
|
|
60
|
+
const trimmed = line.replace(/^[-*]\s*/, "").trim();
|
|
61
|
+
if (!trimmed || seen.has(trimmed)) continue;
|
|
62
|
+
if (patterns.some((pattern) => pattern.test(trimmed))) {
|
|
63
|
+
seen.add(trimmed);
|
|
64
|
+
lines.push(trimmed);
|
|
65
|
+
}
|
|
66
|
+
if (lines.length >= limit) break;
|
|
67
|
+
}
|
|
68
|
+
return lines;
|
|
69
|
+
}
|
|
70
|
+
function bulletList(lines, fallback) {
|
|
71
|
+
if (lines.length === 0) return `- ${fallback}`;
|
|
72
|
+
return lines.map((line) => `- ${line}`).join("\n");
|
|
73
|
+
}
|
|
74
|
+
function buildBriefMarkdown(summary, source) {
|
|
75
|
+
const text = collectSessionText(summary);
|
|
76
|
+
const done = extractMatchingLines(text, [/完成|已实现|通过|passed|fixed|implemented|verified/i], 6);
|
|
77
|
+
const blockers = extractMatchingLines(text, [/失败|阻塞|blocked|failed|error|E401|unrelated/i], 6);
|
|
78
|
+
const next = extractMatchingLines(text, [/继续|下一步|TODO|todo|next|publish|发布|验证|verify/i], 6);
|
|
79
|
+
return `# Happy Task Brief
|
|
80
|
+
|
|
81
|
+
- Source: ${source}
|
|
82
|
+
- Session: \`${summary.sessionId}\`
|
|
83
|
+
- Active: ${summary.active ? "yes" : "no"}
|
|
84
|
+
- Agent: ${summary.agent.name ?? "(unknown)"}${summary.agent.model ? ` / ${summary.agent.model}` : ""}
|
|
85
|
+
- Machine: ${summary.machine.name ?? summary.machine.id ?? "(unknown)"}
|
|
86
|
+
- Path: ${summary.path ?? "(unknown)"}
|
|
87
|
+
- Updated: ${new Date(summary.updatedAt).toISOString()}
|
|
88
|
+
|
|
89
|
+
## Stored Summary
|
|
90
|
+
|
|
91
|
+
${summary.storedSummary ?? "(none)"}
|
|
92
|
+
|
|
93
|
+
## Recent Conversation
|
|
94
|
+
|
|
95
|
+
${summary.summaryText}
|
|
96
|
+
|
|
97
|
+
## Done / Evidence
|
|
98
|
+
|
|
99
|
+
${bulletList(done, "No explicit completion evidence detected in the compact window.")}
|
|
100
|
+
|
|
101
|
+
## Blockers / Risks
|
|
102
|
+
|
|
103
|
+
${bulletList(blockers, "No explicit blocker detected in the compact window.")}
|
|
104
|
+
|
|
105
|
+
## Suggested Next Actions
|
|
106
|
+
|
|
107
|
+
${bulletList(next, "Continue from the latest user request and verify before reporting completion.")}
|
|
108
|
+
|
|
109
|
+
## Continuation Prompt
|
|
110
|
+
|
|
111
|
+
Continue session \`${summary.sessionId}\` from this brief. Preserve existing user changes, inspect the repository rules before editing, implement only the requested scope, run focused verification, and summarize changed files plus test results in Chinese.
|
|
112
|
+
`;
|
|
113
|
+
}
|
|
114
|
+
function findRecentSession(sessions, projectPath) {
|
|
115
|
+
if (!projectPath) return sessions[0];
|
|
116
|
+
const resolved = path.resolve(projectPath);
|
|
117
|
+
return sessions.find((session) => {
|
|
118
|
+
if (!session.path) return false;
|
|
119
|
+
const path$1 = path.resolve(session.path);
|
|
120
|
+
return path$1 === resolved || path$1.startsWith(`${resolved}/`) || resolved.startsWith(`${path$1}/`);
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
async function handleTaskCommand(args) {
|
|
124
|
+
const action = args[0];
|
|
125
|
+
if (!action || action === "-h" || action === "--help") {
|
|
126
|
+
printTaskHelp();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
if (action !== "brief") {
|
|
130
|
+
throw new Error(`Unknown task command: ${action}. Use "happy task brief".`);
|
|
131
|
+
}
|
|
132
|
+
const parsed = workflowCommon.parseWorkflowArgs(args.slice(1), /* @__PURE__ */ new Set(["--session", "--project", "--limit", "--max-turns", "--text-limit", "--out"]));
|
|
133
|
+
const credentials = await logger.readCredentials();
|
|
134
|
+
if (!credentials) {
|
|
135
|
+
throw new Error('No happy credentials found. Run "happy auth login" first.');
|
|
136
|
+
}
|
|
137
|
+
let sessionId = workflowCommon.stringFlag(parsed, "--session") ?? parsed.positionals[0];
|
|
138
|
+
let source = "session";
|
|
139
|
+
if (!sessionId && workflowCommon.booleanFlag(parsed, "--recent")) {
|
|
140
|
+
const result = await sessionList.runSessionList(credentials, { status: "all", limit: 150 });
|
|
141
|
+
const recent = findRecentSession(result.sessions, workflowCommon.stringFlag(parsed, "--project"));
|
|
142
|
+
if (!recent) {
|
|
143
|
+
throw new Error("No matching recent session found.");
|
|
144
|
+
}
|
|
145
|
+
sessionId = recent.sessionId;
|
|
146
|
+
source = "recent";
|
|
147
|
+
}
|
|
148
|
+
if (!sessionId) {
|
|
149
|
+
throw new Error('Session ID required. Use "happy task brief --session <id>" or "--recent".');
|
|
150
|
+
}
|
|
151
|
+
const input = {
|
|
152
|
+
sessionId,
|
|
153
|
+
limit: workflowCommon.numberFlag(parsed, "--limit"),
|
|
154
|
+
maxTurns: workflowCommon.numberFlag(parsed, "--max-turns"),
|
|
155
|
+
textLimit: workflowCommon.numberFlag(parsed, "--text-limit"),
|
|
156
|
+
includeTools: true
|
|
157
|
+
};
|
|
158
|
+
const summary = await sessionSummary.runSessionSummary(credentials, input);
|
|
159
|
+
const brief = buildBriefMarkdown(summary, source);
|
|
160
|
+
if (workflowCommon.booleanFlag(parsed, "--json")) {
|
|
161
|
+
console.log(JSON.stringify({ source, summary, brief }, null, 2));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
await workflowCommon.printOrWriteOutput(brief, workflowCommon.stringFlag(parsed, "--out"));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
exports.handleTaskCommand = handleTaskCommand;
|