@cydm/happy-elves 0.1.0-beta.85 → 0.1.0-beta.87
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/apps/cli/dist/commands/daemon.js +87 -3
- package/apps/cli/dist/commands/lib/args.js +1 -0
- package/apps/cli/dist/commands/lib/bootstrap-daemon.js +2 -2
- package/apps/cli/dist/commands/lib/local-daemon.d.ts +1 -0
- package/apps/cli/dist/commands/lib/local-daemon.js +14 -0
- package/apps/cli/dist/commands/lib/usage.js +1 -1
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/session/prompt.d.ts +26 -0
- package/apps/daemon/dist/session/prompt.js +88 -0
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/index.js +28 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import os from "node:os";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, idleSessionStatuses, localDaemonStatus, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readLocalAuditLog, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, showMachine, spawnDaemonStart, stopLocalDaemon, throwRelayHttpError, wantsJson, writeDaemonConfig } from "./lib/index.js";
|
|
3
|
+
import { CliError, compactText, ControllerClient, configDir, configPath, daemonPidPath, deriveOrchestration, ensureDaemonRunning, idleSessionStatuses, localDaemonStatus, ok, parsePairingClaimResponse, randomId, readConfig, readDaemonConfig, readLocalAuditLog, readRelayJson, redactedDaemonConfig, requirePositional, requireRelayUrl, requireString, restartDaemonForNewConfig, showMachine, spawnDaemonStart, stopLocalDaemon, throwRelayHttpError, waitForMachineProjection, wantsJson, writeDaemonConfig } from "./lib/index.js";
|
|
4
4
|
export async function handleDaemon({ domain, action, flags }) {
|
|
5
5
|
if (domain === "daemon" && action === "pair") {
|
|
6
6
|
const relayUrl = requireRelayUrl(flags);
|
|
@@ -25,11 +25,87 @@ export async function handleDaemon({ domain, action, flags }) {
|
|
|
25
25
|
machineToken: claimed.machineToken,
|
|
26
26
|
accountSecret,
|
|
27
27
|
};
|
|
28
|
+
const localBefore = await localDaemonStatus();
|
|
28
29
|
const daemonConfigPath = await writeDaemonConfig(daemonConfig);
|
|
29
|
-
|
|
30
|
+
let daemonStart = { skipped: true, reason: "not requested" };
|
|
31
|
+
let projection = { checked: false, reason: "not checked" };
|
|
32
|
+
let startError;
|
|
33
|
+
if (flags["no-start"] === true) {
|
|
34
|
+
daemonStart = { skipped: true, reason: "--no-start" };
|
|
35
|
+
projection = { checked: false, reason: "--no-start" };
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
try {
|
|
39
|
+
const startResult = localBefore.running
|
|
40
|
+
? await restartDaemonForNewConfig(relayUrl)
|
|
41
|
+
: await ensureDaemonRunning(relayUrl);
|
|
42
|
+
daemonStart = {
|
|
43
|
+
skipped: false,
|
|
44
|
+
restarted: localBefore.running,
|
|
45
|
+
local: startResult.local,
|
|
46
|
+
started: startResult.started,
|
|
47
|
+
...(startResult.startedPid ? { startedPid: startResult.startedPid } : {}),
|
|
48
|
+
...("stopped" in startResult ? { stop: startResult.stopped } : {}),
|
|
49
|
+
};
|
|
50
|
+
try {
|
|
51
|
+
const controllerConfig = await readConfig({ relay: relayUrl });
|
|
52
|
+
const machine = await waitForMachineProjection(new ControllerClient(controllerConfig), claimed.machineId, 10_000);
|
|
53
|
+
projection = {
|
|
54
|
+
checked: true,
|
|
55
|
+
online: machine?.online === true,
|
|
56
|
+
machineId: claimed.machineId,
|
|
57
|
+
...(machine?.lastSeen ? { lastSeen: machine.lastSeen } : {}),
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
projection = {
|
|
62
|
+
checked: false,
|
|
63
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
const code = error instanceof CliError ? error.code : "DAEMON_START_FAILED";
|
|
69
|
+
startError = { code, message: error instanceof Error ? error.message : String(error) };
|
|
70
|
+
process.exitCode = 1;
|
|
71
|
+
daemonStart = { skipped: true, reason: "start failed" };
|
|
72
|
+
projection = { checked: false, reason: "daemon did not start" };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const data = {
|
|
30
76
|
path: daemonConfigPath,
|
|
31
77
|
config: redactedDaemonConfig(daemonConfig),
|
|
32
|
-
|
|
78
|
+
daemon: daemonStart,
|
|
79
|
+
projection,
|
|
80
|
+
...(startError ? { error: startError } : {}),
|
|
81
|
+
};
|
|
82
|
+
const commandOk = !startError && !(projection.checked && !projection.online);
|
|
83
|
+
if (!commandOk)
|
|
84
|
+
process.exitCode = 1;
|
|
85
|
+
if (!wantsJson(flags)) {
|
|
86
|
+
console.log(`Paired ${machineName} with ${relayUrl}`);
|
|
87
|
+
console.log(`Config: ${daemonConfigPath}`);
|
|
88
|
+
if (startError) {
|
|
89
|
+
console.error(`Daemon start failed: ${startError.message}`);
|
|
90
|
+
}
|
|
91
|
+
else if (daemonStart.skipped) {
|
|
92
|
+
console.log(`Daemon start: skipped (${daemonStart.reason})`);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
console.log(`${daemonStart.restarted ? "Restarted" : "Started"} daemon${daemonStart.startedPid ? ` pid ${daemonStart.startedPid}` : ""}`);
|
|
96
|
+
}
|
|
97
|
+
if (projection.checked) {
|
|
98
|
+
console.log(`Relay projection: ${projection.online ? "online" : "offline"} (${projection.machineId})`);
|
|
99
|
+
if (!projection.online) {
|
|
100
|
+
console.log("If this stays offline, run happy-elves daemon logs --tail 100 and happy-elves daemon status --local.");
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
console.log(`Relay projection: not checked (${projection.reason})`);
|
|
105
|
+
}
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
ok("daemon.pair", data, { machineId: claimed.machineId }, commandOk);
|
|
33
109
|
return true;
|
|
34
110
|
}
|
|
35
111
|
if (domain === "daemon" && action === "doctor") {
|
|
@@ -118,6 +194,14 @@ export async function handleDaemon({ domain, action, flags }) {
|
|
|
118
194
|
if (domain === "daemon" && action === "start") {
|
|
119
195
|
const status = await localDaemonStatus();
|
|
120
196
|
if (status.running) {
|
|
197
|
+
const existingConfig = await readDaemonConfig();
|
|
198
|
+
if (typeof flags.relay === "string")
|
|
199
|
+
existingConfig.relayUrl = requireRelayUrl(flags);
|
|
200
|
+
const restarted = await ensureDaemonRunning(existingConfig.relayUrl);
|
|
201
|
+
if (restarted.started) {
|
|
202
|
+
ok("daemon.start", { ...restarted.local, started: true, startedPid: restarted.startedPid, restartedForChangedConfig: true });
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
121
205
|
ok("daemon.start", { ...status, started: false });
|
|
122
206
|
return true;
|
|
123
207
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { CliError } from "../../errors.js";
|
|
2
2
|
import { startDaemonTimeoutMs } from "./paths.js";
|
|
3
3
|
import { readDaemonConfig } from "./config.js";
|
|
4
|
-
import { daemonBinaryUpdatedAfterStart, localDaemonStatus, spawnDaemonStart, stopLocalDaemon } from "./local-daemon.js";
|
|
4
|
+
import { daemonBinaryUpdatedAfterStart, daemonConfigUpdatedAfterStart, localDaemonStatus, spawnDaemonStart, stopLocalDaemon } from "./local-daemon.js";
|
|
5
5
|
export async function ensureDaemonRunning(relayUrl, cwd = process.cwd()) {
|
|
6
6
|
const current = await localDaemonStatus();
|
|
7
7
|
if (current.running) {
|
|
8
|
-
if (await daemonBinaryUpdatedAfterStart(current)) {
|
|
8
|
+
if ((await daemonBinaryUpdatedAfterStart(current)) || (await daemonConfigUpdatedAfterStart(current))) {
|
|
9
9
|
return await restartDaemonForNewConfig(relayUrl, cwd);
|
|
10
10
|
}
|
|
11
11
|
return { local: current, started: false };
|
|
@@ -4,6 +4,7 @@ export declare function localDaemonStatus(): Promise<{
|
|
|
4
4
|
running: boolean;
|
|
5
5
|
}>;
|
|
6
6
|
export declare function daemonBinaryUpdatedAfterStart(status: Awaited<ReturnType<typeof localDaemonStatus>>): Promise<boolean>;
|
|
7
|
+
export declare function daemonConfigUpdatedAfterStart(status: Awaited<ReturnType<typeof localDaemonStatus>>): Promise<boolean>;
|
|
7
8
|
export declare function spawnDaemonStart(args: string[], cwd?: string): number;
|
|
8
9
|
export declare function stopLocalDaemon(timeoutMs?: number): Promise<{
|
|
9
10
|
pidPath: string;
|
|
@@ -102,6 +102,20 @@ export async function daemonBinaryUpdatedAfterStart(status) {
|
|
|
102
102
|
return false;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
export async function daemonConfigUpdatedAfterStart(status) {
|
|
106
|
+
if (!status.running || !status.pid)
|
|
107
|
+
return false;
|
|
108
|
+
const startedAt = readProcessStartedAtMs(status.pid);
|
|
109
|
+
if (!startedAt)
|
|
110
|
+
return false;
|
|
111
|
+
try {
|
|
112
|
+
const config = await fs.stat(path.join(configDir, "daemon.json"));
|
|
113
|
+
return config.mtimeMs > startedAt + 1000;
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
105
119
|
export function spawnDaemonStart(args, cwd = process.cwd()) {
|
|
106
120
|
const child = spawn(process.execPath, [daemonCliPath(), "start", ...args], {
|
|
107
121
|
cwd,
|
|
@@ -157,7 +157,7 @@ missed and are never auto-caught-up.
|
|
|
157
157
|
happy-elves loop delete <loopId> --json
|
|
158
158
|
`,
|
|
159
159
|
daemon: `Usage:
|
|
160
|
-
happy-elves daemon pair --relay <url> --code <code> --secret <account-secret> [--name <machine-name>] --json
|
|
160
|
+
happy-elves daemon pair --relay <url> --code <code> --secret <account-secret> [--name <machine-name>] [--no-start] --json
|
|
161
161
|
happy-elves daemon start [--relay <url>] [--json]
|
|
162
162
|
happy-elves daemon stop [--json]
|
|
163
163
|
happy-elves daemon restart [--relay <url>] [--json]
|
package/apps/cli/package.json
CHANGED
|
@@ -1,4 +1,30 @@
|
|
|
1
1
|
import { type MachineCommand } from "../../../../packages/shared/dist/index.js";
|
|
2
2
|
import type { DaemonConfig } from "../types.js";
|
|
3
|
+
type PromptCommand = Extract<MachineCommand, {
|
|
4
|
+
type: "machine:prompt";
|
|
5
|
+
}>;
|
|
6
|
+
type PromptExecutionPhase = "decrypt-prompt" | "decrypt-memory-options" | "prepare-memory-context" | "emit-submitted" | "emit-memory-status" | "start-turn" | "attach-turn" | "audit-turn-start" | "stream-turn" | "await-turn-result" | "write-memory" | "refresh-runtime-metadata" | "emit-terminal" | "refresh-authoritative-transcript" | "send-turn-done" | "terminal-hooks";
|
|
3
7
|
export declare function handlePrompt(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
8
|
+
export declare function promptFailureResponse(command: PromptCommand, phase: PromptExecutionPhase, error: unknown): {
|
|
9
|
+
type: "machine:error";
|
|
10
|
+
requestId: string;
|
|
11
|
+
sessionId: string;
|
|
12
|
+
code: string;
|
|
13
|
+
message: string;
|
|
14
|
+
};
|
|
15
|
+
export declare function promptFailureAuditEvent(config: Pick<DaemonConfig, "machineId">, command: PromptCommand, phase: PromptExecutionPhase, error: unknown): {
|
|
16
|
+
sessionId: string;
|
|
17
|
+
machineId: string;
|
|
18
|
+
actor: "system";
|
|
19
|
+
action: string;
|
|
20
|
+
summary: string;
|
|
21
|
+
evidence: {
|
|
22
|
+
requestId: string;
|
|
23
|
+
turnId: string;
|
|
24
|
+
code: string;
|
|
25
|
+
phase: PromptExecutionPhase;
|
|
26
|
+
message: string;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
4
29
|
export declare function handleCancel(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
30
|
+
export {};
|
|
@@ -52,19 +52,24 @@ export async function handlePrompt(ws, config, command) {
|
|
|
52
52
|
activePromptRequests.delete(command.requestId);
|
|
53
53
|
releaseSessionTurn(command.sessionId, command.requestId);
|
|
54
54
|
};
|
|
55
|
+
let phase = "decrypt-prompt";
|
|
55
56
|
try {
|
|
56
57
|
const prompt = await decryptJson(config.accountSecret, command.encryptedPrompt);
|
|
57
58
|
const rawText = prompt.type === "user_prompt" ? prompt.text : JSON.stringify(prompt);
|
|
59
|
+
phase = "decrypt-memory-options";
|
|
58
60
|
const memoryOptions = await decryptMemoryOptions(config, command);
|
|
61
|
+
phase = "prepare-memory-context";
|
|
59
62
|
const memoryContext = memoryOptions
|
|
60
63
|
? await prepareMemoryContext(memoryOptions, rawText)
|
|
61
64
|
: { text: rawText, used: [], options: undefined };
|
|
62
65
|
const text = memoryContext.text;
|
|
66
|
+
phase = "emit-submitted";
|
|
63
67
|
await emitEvent(ws, config, command.sessionId, command.turnId, {
|
|
64
68
|
type: "status",
|
|
65
69
|
text: "Prompt submitted to runtime",
|
|
66
70
|
});
|
|
67
71
|
if (memoryOptions) {
|
|
72
|
+
phase = "emit-memory-status";
|
|
68
73
|
await emitEvent(ws, config, command.sessionId, command.turnId, {
|
|
69
74
|
type: "status",
|
|
70
75
|
tag: "memory",
|
|
@@ -90,6 +95,7 @@ export async function handlePrompt(ws, config, command) {
|
|
|
90
95
|
}
|
|
91
96
|
}
|
|
92
97
|
const previousBackendSessionId = session.handle.backendSessionId;
|
|
98
|
+
phase = "start-turn";
|
|
93
99
|
const turn = getRuntime(session.cwd).startTurn({
|
|
94
100
|
handle: session.handle,
|
|
95
101
|
text,
|
|
@@ -98,9 +104,11 @@ export async function handlePrompt(ws, config, command) {
|
|
|
98
104
|
requestId: command.requestId,
|
|
99
105
|
timeoutMs: command.timeoutMs,
|
|
100
106
|
});
|
|
107
|
+
phase = "attach-turn";
|
|
101
108
|
if (!attachClaimedTurn(command.sessionId, command.requestId, command.turnId, turn)) {
|
|
102
109
|
throw new Error(`Session turn claim was lost before start: ${command.sessionId}`);
|
|
103
110
|
}
|
|
111
|
+
phase = "audit-turn-start";
|
|
104
112
|
await appendAudit({
|
|
105
113
|
sessionId: command.sessionId,
|
|
106
114
|
machineId: config.machineId,
|
|
@@ -118,6 +126,7 @@ export async function handlePrompt(ws, config, command) {
|
|
|
118
126
|
},
|
|
119
127
|
});
|
|
120
128
|
let outputPreview = "";
|
|
129
|
+
phase = "stream-turn";
|
|
121
130
|
for await (const event of turn.events) {
|
|
122
131
|
const payload = runtimeEventToPayload(event);
|
|
123
132
|
if (payload.type === "text_delta" && payload.stream !== "thought") {
|
|
@@ -125,7 +134,9 @@ export async function handlePrompt(ws, config, command) {
|
|
|
125
134
|
}
|
|
126
135
|
await emitEvent(ws, config, command.sessionId, command.turnId, payload);
|
|
127
136
|
}
|
|
137
|
+
phase = "await-turn-result";
|
|
128
138
|
const result = await turn.result;
|
|
139
|
+
phase = "write-memory";
|
|
129
140
|
const dailyMemory = await writeTurnMemory(memoryOptions, {
|
|
130
141
|
promptText: rawText,
|
|
131
142
|
outputPreview,
|
|
@@ -144,6 +155,7 @@ export async function handlePrompt(ws, config, command) {
|
|
|
144
155
|
].filter(Boolean).join(" · "),
|
|
145
156
|
});
|
|
146
157
|
}
|
|
158
|
+
phase = "refresh-runtime-metadata";
|
|
147
159
|
const refreshedMetadata = session.handle.backendSessionId
|
|
148
160
|
? await encryptedSessionMetadata(config, command.sessionId, session, {
|
|
149
161
|
currentHead: result.currentHead,
|
|
@@ -165,6 +177,7 @@ export async function handlePrompt(ws, config, command) {
|
|
|
165
177
|
},
|
|
166
178
|
});
|
|
167
179
|
}
|
|
180
|
+
phase = "emit-terminal";
|
|
168
181
|
await emitEvent(ws, config, command.sessionId, command.turnId, result.status === "failed"
|
|
169
182
|
? {
|
|
170
183
|
type: "done",
|
|
@@ -180,8 +193,10 @@ export async function handlePrompt(ws, config, command) {
|
|
|
180
193
|
currentHead: result.currentHead,
|
|
181
194
|
lastTurnId: result.lastTurnId,
|
|
182
195
|
});
|
|
196
|
+
phase = "refresh-authoritative-transcript";
|
|
183
197
|
await refreshAuthoritativeTranscriptAfterTurn(ws, config, session, command.requestId, result.currentHead, result.lastTurnId, result.status);
|
|
184
198
|
releasePromptTurn();
|
|
199
|
+
phase = "send-turn-done";
|
|
185
200
|
await sendReliable({
|
|
186
201
|
type: "machine:turnDone",
|
|
187
202
|
sessionId: command.sessionId,
|
|
@@ -192,6 +207,7 @@ export async function handlePrompt(ws, config, command) {
|
|
|
192
207
|
error: result.status === "failed" ? result.error.message : undefined,
|
|
193
208
|
encryptedMetadata: refreshedMetadata,
|
|
194
209
|
}, ws);
|
|
210
|
+
phase = "terminal-hooks";
|
|
195
211
|
await handleGatewayTurnTerminal(config, {
|
|
196
212
|
sessionId: command.sessionId,
|
|
197
213
|
requestId: command.requestId,
|
|
@@ -231,10 +247,82 @@ export async function handlePrompt(ws, config, command) {
|
|
|
231
247
|
},
|
|
232
248
|
});
|
|
233
249
|
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
const response = promptFailureResponse(command, phase, error);
|
|
252
|
+
await appendAudit(promptFailureAuditEvent(config, command, phase, error)).catch((auditError) => {
|
|
253
|
+
console.error(`Prompt failure audit failed for ${command.turnId}: ${auditError instanceof Error ? auditError.message : String(auditError)}`);
|
|
254
|
+
});
|
|
255
|
+
await sendCommandResponse(ws, response);
|
|
256
|
+
}
|
|
234
257
|
finally {
|
|
235
258
|
releasePromptTurn();
|
|
236
259
|
}
|
|
237
260
|
}
|
|
261
|
+
export function promptFailureResponse(command, phase, error) {
|
|
262
|
+
const code = promptFailureCode(phase, error);
|
|
263
|
+
return {
|
|
264
|
+
type: "machine:error",
|
|
265
|
+
requestId: command.requestId,
|
|
266
|
+
sessionId: command.sessionId,
|
|
267
|
+
code,
|
|
268
|
+
message: promptFailureMessage(code, error),
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
export function promptFailureAuditEvent(config, command, phase, error) {
|
|
272
|
+
const code = promptFailureCode(phase, error);
|
|
273
|
+
return {
|
|
274
|
+
sessionId: command.sessionId,
|
|
275
|
+
machineId: config.machineId,
|
|
276
|
+
actor: "system",
|
|
277
|
+
action: "turn.prompt.failed",
|
|
278
|
+
summary: `Failed to run prompt turn ${command.turnId}`,
|
|
279
|
+
evidence: {
|
|
280
|
+
requestId: command.requestId,
|
|
281
|
+
turnId: command.turnId,
|
|
282
|
+
code,
|
|
283
|
+
phase,
|
|
284
|
+
message: safeErrorMessage(error),
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function promptFailureCode(phase, error) {
|
|
289
|
+
if (phase === "decrypt-prompt" && isCryptoOperationFailure(error))
|
|
290
|
+
return "PROMPT_DECRYPT_FAILED";
|
|
291
|
+
if (phase === "decrypt-memory-options" && isCryptoOperationFailure(error))
|
|
292
|
+
return "MEMORY_OPTIONS_DECRYPT_FAILED";
|
|
293
|
+
if (phase === "start-turn" || phase === "attach-turn")
|
|
294
|
+
return "TURN_START_FAILED";
|
|
295
|
+
if (phase === "stream-turn" || phase === "await-turn-result")
|
|
296
|
+
return "TURN_RUNTIME_FAILED";
|
|
297
|
+
if (phase === "refresh-runtime-metadata")
|
|
298
|
+
return "SESSION_METADATA_REFRESH_FAILED";
|
|
299
|
+
if (phase === "emit-submitted" || phase === "emit-memory-status" || phase === "emit-terminal")
|
|
300
|
+
return "SESSION_EVENT_EMIT_FAILED";
|
|
301
|
+
return "PROMPT_EXECUTION_FAILED";
|
|
302
|
+
}
|
|
303
|
+
function promptFailureMessage(code, error) {
|
|
304
|
+
if (code === "PROMPT_DECRYPT_FAILED") {
|
|
305
|
+
return "Failed to decrypt the prompt for this daemon. Re-pair the daemon with the correct account secret.";
|
|
306
|
+
}
|
|
307
|
+
if (code === "MEMORY_OPTIONS_DECRYPT_FAILED") {
|
|
308
|
+
return "Failed to decrypt memory options for this turn.";
|
|
309
|
+
}
|
|
310
|
+
return safeErrorMessage(error);
|
|
311
|
+
}
|
|
312
|
+
function isCryptoOperationFailure(error) {
|
|
313
|
+
const record = error && typeof error === "object" ? error : {};
|
|
314
|
+
const name = typeof record.name === "string" ? record.name : "";
|
|
315
|
+
const code = typeof record.code === "string" ? record.code : "";
|
|
316
|
+
const message = safeErrorMessage(error);
|
|
317
|
+
return name === "OperationError" ||
|
|
318
|
+
code === "ERR_OSSL_BAD_DECRYPT" ||
|
|
319
|
+
/operation-specific reason|decrypt|authentication tag|Unsupported state or unable to authenticate data/i.test(message);
|
|
320
|
+
}
|
|
321
|
+
function safeErrorMessage(error) {
|
|
322
|
+
if (error instanceof Error && error.message)
|
|
323
|
+
return error.message;
|
|
324
|
+
return String(error);
|
|
325
|
+
}
|
|
238
326
|
async function decryptMemoryOptions(config, command) {
|
|
239
327
|
if (!command.encryptedMemoryOptions)
|
|
240
328
|
return undefined;
|
package/apps/daemon/package.json
CHANGED
package/apps/relay/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ const dbPath = process.env.HAPPY_ELVES_DB ?? path.join(process.cwd(), ".happy-el
|
|
|
18
18
|
const pairingTtlMs = 10 * 60 * 1000;
|
|
19
19
|
const controllerInviteTtlMs = 10 * 60 * 1000;
|
|
20
20
|
const eventReplayLimit = intEnv("HAPPY_ELVES_EVENT_REPLAY_LIMIT", process.env.HAPPY_ELVES_EVENT_REPLAY_LIMIT, 100);
|
|
21
|
+
const lanPackDir = process.env.HAPPY_ELVES_LAN_PACK_DIR ? path.resolve(process.env.HAPPY_ELVES_LAN_PACK_DIR) : undefined;
|
|
21
22
|
const retention = createRetentionConfig(process.env);
|
|
22
23
|
const retentionPruneIntervalMs = retentionMs("HAPPY_ELVES_RETENTION_PRUNE_INTERVAL_MS", process.env.HAPPY_ELVES_RETENTION_PRUNE_INTERVAL_MS, 60_000);
|
|
23
24
|
const originAllowed = createOriginPolicy(process.env);
|
|
@@ -77,6 +78,33 @@ app.setErrorHandler((error, _request, reply) => {
|
|
|
77
78
|
}
|
|
78
79
|
reply.code(500).send({ error: { code: "INTERNAL_ERROR", message } });
|
|
79
80
|
});
|
|
81
|
+
if (lanPackDir) {
|
|
82
|
+
app.get("/__lan/*", async (request, reply) => {
|
|
83
|
+
const params = request.params;
|
|
84
|
+
const requested = decodeURIComponent(params["*"] ?? "");
|
|
85
|
+
const filePath = path.resolve(lanPackDir, requested);
|
|
86
|
+
if (!filePath.startsWith(`${lanPackDir}${path.sep}`)) {
|
|
87
|
+
reply.code(403).send("forbidden");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
let stat;
|
|
91
|
+
try {
|
|
92
|
+
stat = fs.statSync(filePath);
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
reply.code(404).send("not found");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (!stat.isFile()) {
|
|
99
|
+
reply.code(404).send("not found");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const extension = path.extname(filePath);
|
|
103
|
+
reply.header("content-length", String(stat.size));
|
|
104
|
+
reply.type(extension === ".ps1" ? "text/plain; charset=utf-8" : extension === ".tgz" ? "application/gzip" : "application/octet-stream");
|
|
105
|
+
return reply.send(fs.createReadStream(filePath));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
80
108
|
registerHttpRoutes(app, context, controllerInviteTtlMs);
|
|
81
109
|
registerWebsocketRoute(app, context, originAllowed);
|
|
82
110
|
export async function startRelay(options = {}) {
|