@cerulin/chell 0.2.5
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/README.md +75 -0
- package/bin/chell-mcp.mjs +33 -0
- package/bin/chell.mjs +37 -0
- package/dist/codex/chellMcpStdioBridge.cjs +80 -0
- package/dist/codex/chellMcpStdioBridge.d.cts +2 -0
- package/dist/codex/chellMcpStdioBridge.d.mts +2 -0
- package/dist/codex/chellMcpStdioBridge.mjs +78 -0
- package/dist/index-B443j7JQ.mjs +6714 -0
- package/dist/index-qS668VWY.cjs +6730 -0
- package/dist/index.cjs +42 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +39 -0
- package/dist/lib.cjs +32 -0
- package/dist/lib.d.cts +891 -0
- package/dist/lib.d.mts +891 -0
- package/dist/lib.mjs +22 -0
- package/dist/runCodex-DHtm7TWT.cjs +2020 -0
- package/dist/runCodex-DLbjgnc4.mjs +2017 -0
- package/dist/runGemini-C03RUmvr.mjs +788 -0
- package/dist/runGemini-fdb5jxAA.cjs +791 -0
- package/dist/types-DBjv5m4J.cjs +2499 -0
- package/dist/types-fM_iFuNp.mjs +2452 -0
- package/package.json +131 -0
- package/scripts/claude_local_launcher.cjs +98 -0
- package/scripts/claude_remote_launcher.cjs +13 -0
- package/scripts/codex_local_launcher.cjs +155 -0
- package/scripts/codex_preload.cjs +56 -0
- package/scripts/codex_remote_launcher.cjs +129 -0
- package/scripts/obfuscate-dist.mjs +73 -0
- package/scripts/pack-chell.cjs +32 -0
- package/scripts/publish-scoped.ps1 +58 -0
- package/scripts/ripgrep_launcher.cjs +33 -0
- package/scripts/unpack-tools.cjs +163 -0
- package/tools/archives/difftastic-LICENSE +21 -0
- package/tools/archives/difftastic-arm64-darwin.tar.gz +0 -0
- package/tools/archives/difftastic-arm64-linux.tar.gz +0 -0
- package/tools/archives/difftastic-x64-darwin.tar.gz +0 -0
- package/tools/archives/difftastic-x64-linux.tar.gz +0 -0
- package/tools/archives/difftastic-x64-win32.tar.gz +0 -0
- package/tools/archives/ripgrep-LICENSE +3 -0
- package/tools/archives/ripgrep-arm64-darwin.tar.gz +0 -0
- package/tools/archives/ripgrep-arm64-linux.tar.gz +0 -0
- package/tools/archives/ripgrep-x64-darwin.tar.gz +0 -0
- package/tools/archives/ripgrep-x64-linux.tar.gz +0 -0
- package/tools/archives/ripgrep-x64-win32.tar.gz +0 -0
- package/tools/licenses/difftastic-LICENSE +21 -0
- package/tools/licenses/ripgrep-LICENSE +3 -0
- package/tools/unpacked/difft +0 -0
- package/tools/unpacked/difft.exe +0 -0
- package/tools/unpacked/rg +0 -0
- package/tools/unpacked/rg.exe +0 -0
- package/tools/unpacked/ripgrep.node +0 -0
|
@@ -0,0 +1,2452 @@
|
|
|
1
|
+
import axios from 'axios';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { mkdirSync as mkdirSync$1, appendFileSync, writeFileSync as writeFileSync$1 } from 'fs';
|
|
4
|
+
import { existsSync, mkdirSync, constants, readFileSync, unlinkSync, writeFileSync, readdirSync, statSync } from 'node:fs';
|
|
5
|
+
import os, { homedir } from 'node:os';
|
|
6
|
+
import { join, basename, resolve as resolve$1 } from 'node:path';
|
|
7
|
+
import { readFile, open, stat, unlink, mkdir, writeFile, rename } from 'node:fs/promises';
|
|
8
|
+
import * as z from 'zod';
|
|
9
|
+
import { z as z$1 } from 'zod';
|
|
10
|
+
import { randomBytes, randomUUID } from 'node:crypto';
|
|
11
|
+
import tweetnacl from 'tweetnacl';
|
|
12
|
+
import { EventEmitter } from 'node:events';
|
|
13
|
+
import { io } from 'socket.io-client';
|
|
14
|
+
import { spawn, exec } from 'child_process';
|
|
15
|
+
import { promisify } from 'util';
|
|
16
|
+
import { readFile as readFile$1, stat as stat$1, writeFile as writeFile$1, readdir } from 'fs/promises';
|
|
17
|
+
import { createHash } from 'crypto';
|
|
18
|
+
import { dirname, resolve, join as join$1 } from 'path';
|
|
19
|
+
import { fileURLToPath } from 'url';
|
|
20
|
+
import { platform } from 'os';
|
|
21
|
+
import { spawn as spawn$1 } from 'node-pty';
|
|
22
|
+
import { Expo } from 'expo-server-sdk';
|
|
23
|
+
|
|
24
|
+
var name = "@cerulin/chell";
|
|
25
|
+
var version = "0.2.5";
|
|
26
|
+
var description = "Mobile and Web client for Claude Code and Codex";
|
|
27
|
+
var author = "Cerulin";
|
|
28
|
+
var license = "MIT";
|
|
29
|
+
var type = "module";
|
|
30
|
+
var homepage = "https://github.com/cerulin/chell-cli";
|
|
31
|
+
var bugs = "https://github.com/cerulin/chell-cli/issues";
|
|
32
|
+
var repository = "cerulin/chell-cli";
|
|
33
|
+
var bin = {
|
|
34
|
+
happy: "./bin/chell.mjs",
|
|
35
|
+
"happy-mcp": "./bin/chell-mcp.mjs",
|
|
36
|
+
chell: "./bin/chell.mjs",
|
|
37
|
+
"chell-mcp": "./bin/chell-mcp.mjs"
|
|
38
|
+
};
|
|
39
|
+
var main = "./dist/index.cjs";
|
|
40
|
+
var module = "./dist/index.mjs";
|
|
41
|
+
var types = "./dist/index.d.cts";
|
|
42
|
+
var exports = {
|
|
43
|
+
".": {
|
|
44
|
+
require: {
|
|
45
|
+
types: "./dist/index.d.cts",
|
|
46
|
+
"default": "./dist/index.cjs"
|
|
47
|
+
},
|
|
48
|
+
"import": {
|
|
49
|
+
types: "./dist/index.d.mts",
|
|
50
|
+
"default": "./dist/index.mjs"
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
"./lib": {
|
|
54
|
+
require: {
|
|
55
|
+
types: "./dist/lib.d.cts",
|
|
56
|
+
"default": "./dist/lib.cjs"
|
|
57
|
+
},
|
|
58
|
+
"import": {
|
|
59
|
+
types: "./dist/lib.d.mts",
|
|
60
|
+
"default": "./dist/lib.mjs"
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"./codex/chellMcpStdioBridge": {
|
|
64
|
+
require: {
|
|
65
|
+
types: "./dist/codex/chellMcpStdioBridge.d.cts",
|
|
66
|
+
"default": "./dist/codex/chellMcpStdioBridge.cjs"
|
|
67
|
+
},
|
|
68
|
+
"import": {
|
|
69
|
+
types: "./dist/codex/chellMcpStdioBridge.d.mts",
|
|
70
|
+
"default": "./dist/codex/chellMcpStdioBridge.mjs"
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
var files = [
|
|
75
|
+
"dist",
|
|
76
|
+
"bin",
|
|
77
|
+
"scripts",
|
|
78
|
+
"tools",
|
|
79
|
+
"package.json"
|
|
80
|
+
];
|
|
81
|
+
var scripts = {
|
|
82
|
+
"why do we need to build before running tests / dev?": "We need the binary to be built so we run daemon commands which directly run the binary - we don't want them to go out of sync or have custom spawn logic depending how we started happy",
|
|
83
|
+
typecheck: "tsc --noEmit",
|
|
84
|
+
build: "shx rm -rf dist && npx tsc --noEmit && pkgroll",
|
|
85
|
+
obfuscate: "shx rm -rf dist.obf && javascript-obfuscator dist --output dist.obf --compact true --self-defending false --control-flow-flattening true --string-array true --string-array-encoding base64 --identifier-names-generator mangled && shx cp -r dist/*.d.* dist.obf 2>nul && shx rm -rf dist && shx mv dist.obf dist",
|
|
86
|
+
"build:obf": "yarn build && yarn obfuscate",
|
|
87
|
+
"pack:chell": "node scripts/pack-chell.cjs",
|
|
88
|
+
test: "yarn build && tsx --env-file .env.integration-test node_modules/.bin/vitest run",
|
|
89
|
+
start: "yarn build && ./bin/chell.mjs",
|
|
90
|
+
dev: "yarn build && tsx --env-file .env.dev src/index.ts",
|
|
91
|
+
"dev:local-server": "yarn build && tsx --env-file .env.dev-local-server src/index.ts",
|
|
92
|
+
"dev:integration-test-env": "yarn build && tsx --env-file .env.integration-test src/index.ts",
|
|
93
|
+
prepublishOnly: "yarn build && yarn test",
|
|
94
|
+
release: "release-it",
|
|
95
|
+
postinstall: "node scripts/unpack-tools.cjs"
|
|
96
|
+
};
|
|
97
|
+
var dependencies = {
|
|
98
|
+
"@anthropic-ai/claude-code": "^2.0.14",
|
|
99
|
+
"@anthropic-ai/sdk": "^0.56.0",
|
|
100
|
+
"@modelcontextprotocol/sdk": "^1.15.1",
|
|
101
|
+
"@openai/codex": "^0.50.0",
|
|
102
|
+
"@stablelib/base64": "^2.0.1",
|
|
103
|
+
"@types/cross-spawn": "^6.0.6",
|
|
104
|
+
"@types/http-proxy": "^1.17.16",
|
|
105
|
+
"@types/ps-list": "^6.2.1",
|
|
106
|
+
"@types/qrcode-terminal": "^0.12.2",
|
|
107
|
+
"@types/react": "^19.1.9",
|
|
108
|
+
axios: "^1.10.0",
|
|
109
|
+
chalk: "^5.4.1",
|
|
110
|
+
chell: "^1.0.4",
|
|
111
|
+
"cross-spawn": "^7.0.6",
|
|
112
|
+
"expo-server-sdk": "^3.15.0",
|
|
113
|
+
fastify: "^5.5.0",
|
|
114
|
+
"fastify-type-provider-zod": "4.0.2",
|
|
115
|
+
"http-proxy": "^1.18.1",
|
|
116
|
+
"http-proxy-middleware": "^3.0.5",
|
|
117
|
+
ink: "^6.1.0",
|
|
118
|
+
"node-pty": "1.0.0",
|
|
119
|
+
open: "^10.2.0",
|
|
120
|
+
"ps-list": "^8.1.1",
|
|
121
|
+
"qrcode-terminal": "^0.12.0",
|
|
122
|
+
react: "^19.1.1",
|
|
123
|
+
"socket.io-client": "^4.8.1",
|
|
124
|
+
tar: "^7.4.3",
|
|
125
|
+
tweetnacl: "^1.0.3",
|
|
126
|
+
zod: "^3.23.8"
|
|
127
|
+
};
|
|
128
|
+
var devDependencies = {
|
|
129
|
+
"@eslint/compat": "^1",
|
|
130
|
+
"@types/node": ">=20",
|
|
131
|
+
"cross-env": "^10.0.0",
|
|
132
|
+
dotenv: "^16.6.1",
|
|
133
|
+
eslint: "^9",
|
|
134
|
+
"eslint-config-prettier": "^10",
|
|
135
|
+
"javascript-obfuscator": "4",
|
|
136
|
+
pkgroll: "^2.14.2",
|
|
137
|
+
"release-it": "^19.0.4",
|
|
138
|
+
shx: "^0.3.3",
|
|
139
|
+
"ts-node": "^10",
|
|
140
|
+
tsx: "^4.20.3",
|
|
141
|
+
typescript: "^5",
|
|
142
|
+
vitest: "^3.2.4"
|
|
143
|
+
};
|
|
144
|
+
var resolutions = {
|
|
145
|
+
"whatwg-url": "14.2.0",
|
|
146
|
+
"parse-path": "7.0.3",
|
|
147
|
+
"@types/parse-path": "7.0.3"
|
|
148
|
+
};
|
|
149
|
+
var publishConfig = {
|
|
150
|
+
registry: "https://registry.npmjs.org"
|
|
151
|
+
};
|
|
152
|
+
var packageManager = "yarn@1.22.22";
|
|
153
|
+
var packageJson = {
|
|
154
|
+
name: name,
|
|
155
|
+
version: version,
|
|
156
|
+
description: description,
|
|
157
|
+
author: author,
|
|
158
|
+
license: license,
|
|
159
|
+
type: type,
|
|
160
|
+
homepage: homepage,
|
|
161
|
+
bugs: bugs,
|
|
162
|
+
repository: repository,
|
|
163
|
+
bin: bin,
|
|
164
|
+
main: main,
|
|
165
|
+
module: module,
|
|
166
|
+
types: types,
|
|
167
|
+
exports: exports,
|
|
168
|
+
files: files,
|
|
169
|
+
scripts: scripts,
|
|
170
|
+
dependencies: dependencies,
|
|
171
|
+
devDependencies: devDependencies,
|
|
172
|
+
resolutions: resolutions,
|
|
173
|
+
publishConfig: publishConfig,
|
|
174
|
+
packageManager: packageManager
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
class Configuration {
|
|
178
|
+
serverUrl;
|
|
179
|
+
isDaemonProcess;
|
|
180
|
+
isHeadlessMode;
|
|
181
|
+
// Directories and paths (from persistence)
|
|
182
|
+
happyHomeDir;
|
|
183
|
+
logsDir;
|
|
184
|
+
settingsFile;
|
|
185
|
+
privateKeyFile;
|
|
186
|
+
daemonStateFile;
|
|
187
|
+
daemonLockFile;
|
|
188
|
+
currentCliVersion;
|
|
189
|
+
isExperimentalEnabled;
|
|
190
|
+
constructor() {
|
|
191
|
+
this.serverUrl = process.env.HAPPY_SERVER_URL || "https://auth.cerulin.com";
|
|
192
|
+
const args = process.argv.slice(2);
|
|
193
|
+
this.isDaemonProcess = args.length >= 2 && args[0] === "daemon" && args[1] === "start-sync";
|
|
194
|
+
this.isHeadlessMode = process.env.CHELL_HEADLESS === "1" || args.includes("--headless");
|
|
195
|
+
if (process.env.HAPPY_HOME_DIR) {
|
|
196
|
+
const expandedPath = process.env.HAPPY_HOME_DIR.replace(/^~/, homedir());
|
|
197
|
+
this.happyHomeDir = expandedPath;
|
|
198
|
+
} else if (this.isHeadlessMode && process.platform !== "win32") {
|
|
199
|
+
this.happyHomeDir = join("/home", "dev", ".chell");
|
|
200
|
+
} else {
|
|
201
|
+
this.happyHomeDir = join(homedir(), ".chell");
|
|
202
|
+
}
|
|
203
|
+
this.logsDir = join(this.happyHomeDir, "logs");
|
|
204
|
+
this.settingsFile = join(this.happyHomeDir, "settings.json");
|
|
205
|
+
this.privateKeyFile = join(this.happyHomeDir, "access.key");
|
|
206
|
+
this.daemonStateFile = join(this.happyHomeDir, "daemon.state.json");
|
|
207
|
+
this.daemonLockFile = join(this.happyHomeDir, "daemon.state.json.lock");
|
|
208
|
+
this.isExperimentalEnabled = ["true", "1", "yes"].includes(process.env.HAPPY_EXPERIMENTAL?.toLowerCase() || "");
|
|
209
|
+
this.currentCliVersion = packageJson.version;
|
|
210
|
+
try {
|
|
211
|
+
if (!existsSync(this.happyHomeDir)) {
|
|
212
|
+
mkdirSync(this.happyHomeDir, { recursive: true });
|
|
213
|
+
}
|
|
214
|
+
} catch (error) {
|
|
215
|
+
if (process.env.DEBUG) {
|
|
216
|
+
console.error(`[CONFIG] Failed to create ${this.happyHomeDir}, falling back to /tmp/.chell:`, error);
|
|
217
|
+
}
|
|
218
|
+
this.happyHomeDir = join("/tmp", ".chell");
|
|
219
|
+
this.logsDir = join(this.happyHomeDir, "logs");
|
|
220
|
+
this.settingsFile = join(this.happyHomeDir, "settings.json");
|
|
221
|
+
this.privateKeyFile = join(this.happyHomeDir, "access.key");
|
|
222
|
+
this.daemonStateFile = join(this.happyHomeDir, "daemon.state.json");
|
|
223
|
+
this.daemonLockFile = join(this.happyHomeDir, "daemon.state.json.lock");
|
|
224
|
+
try {
|
|
225
|
+
if (!existsSync(this.happyHomeDir)) {
|
|
226
|
+
mkdirSync(this.happyHomeDir, { recursive: true });
|
|
227
|
+
}
|
|
228
|
+
} catch (fallbackError) {
|
|
229
|
+
if (process.env.DEBUG) {
|
|
230
|
+
console.error(`[CONFIG] Failed to create fallback directory ${this.happyHomeDir}:`, fallbackError);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
const configuration = new Configuration();
|
|
237
|
+
|
|
238
|
+
function encodeBase64(buffer, variant = "base64") {
|
|
239
|
+
if (variant === "base64url") {
|
|
240
|
+
return encodeBase64Url(buffer);
|
|
241
|
+
}
|
|
242
|
+
return Buffer.from(buffer).toString("base64");
|
|
243
|
+
}
|
|
244
|
+
function encodeBase64Url(buffer) {
|
|
245
|
+
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
246
|
+
}
|
|
247
|
+
function decodeBase64(base64, variant = "base64") {
|
|
248
|
+
if (variant === "base64url") {
|
|
249
|
+
const base64Standard = base64.replaceAll("-", "+").replaceAll("_", "/") + "=".repeat((4 - base64.length % 4) % 4);
|
|
250
|
+
return new Uint8Array(Buffer.from(base64Standard, "base64"));
|
|
251
|
+
}
|
|
252
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
253
|
+
}
|
|
254
|
+
function getRandomBytes(size) {
|
|
255
|
+
return new Uint8Array(randomBytes(size));
|
|
256
|
+
}
|
|
257
|
+
function encrypt(data, secret) {
|
|
258
|
+
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
259
|
+
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
260
|
+
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
261
|
+
result.set(nonce);
|
|
262
|
+
result.set(encrypted, nonce.length);
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
function decrypt(data, secret) {
|
|
266
|
+
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
267
|
+
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
268
|
+
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
269
|
+
if (!decrypted) {
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const defaultSettings = {
|
|
276
|
+
onboardingCompleted: false,
|
|
277
|
+
terminalEnabled: true
|
|
278
|
+
};
|
|
279
|
+
async function readSettings() {
|
|
280
|
+
if (!existsSync(configuration.settingsFile)) {
|
|
281
|
+
return { ...defaultSettings };
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const content = await readFile(configuration.settingsFile, "utf8");
|
|
285
|
+
return JSON.parse(content);
|
|
286
|
+
} catch {
|
|
287
|
+
return { ...defaultSettings };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
async function updateSettings(updater) {
|
|
291
|
+
const LOCK_RETRY_INTERVAL_MS = 100;
|
|
292
|
+
const MAX_LOCK_ATTEMPTS = 50;
|
|
293
|
+
const STALE_LOCK_TIMEOUT_MS = 1e4;
|
|
294
|
+
const lockFile = configuration.settingsFile + ".lock";
|
|
295
|
+
const tmpFile = configuration.settingsFile + ".tmp";
|
|
296
|
+
let fileHandle;
|
|
297
|
+
let attempts = 0;
|
|
298
|
+
while (attempts < MAX_LOCK_ATTEMPTS) {
|
|
299
|
+
try {
|
|
300
|
+
fileHandle = await open(lockFile, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY);
|
|
301
|
+
break;
|
|
302
|
+
} catch (err) {
|
|
303
|
+
if (err.code === "EEXIST") {
|
|
304
|
+
attempts++;
|
|
305
|
+
await new Promise((resolve) => setTimeout(resolve, LOCK_RETRY_INTERVAL_MS));
|
|
306
|
+
try {
|
|
307
|
+
const stats = await stat(lockFile);
|
|
308
|
+
if (Date.now() - stats.mtimeMs > STALE_LOCK_TIMEOUT_MS) {
|
|
309
|
+
await unlink(lockFile).catch(() => {
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
} else {
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!fileHandle) {
|
|
320
|
+
throw new Error(`Failed to acquire settings lock after ${MAX_LOCK_ATTEMPTS * LOCK_RETRY_INTERVAL_MS / 1e3} seconds`);
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const current = await readSettings() || { ...defaultSettings };
|
|
324
|
+
const updated = await updater(current);
|
|
325
|
+
if (!existsSync(configuration.happyHomeDir)) {
|
|
326
|
+
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
327
|
+
}
|
|
328
|
+
await writeFile(tmpFile, JSON.stringify(updated, null, 2));
|
|
329
|
+
await rename(tmpFile, configuration.settingsFile);
|
|
330
|
+
return updated;
|
|
331
|
+
} finally {
|
|
332
|
+
await fileHandle.close();
|
|
333
|
+
await unlink(lockFile).catch(() => {
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
const credentialsSchema = z.object({
|
|
338
|
+
secret: z.string().base64(),
|
|
339
|
+
token: z.string()
|
|
340
|
+
});
|
|
341
|
+
async function readCredentials() {
|
|
342
|
+
if (!existsSync(configuration.privateKeyFile)) {
|
|
343
|
+
return null;
|
|
344
|
+
}
|
|
345
|
+
try {
|
|
346
|
+
const keyBase64 = await readFile(configuration.privateKeyFile, "utf8");
|
|
347
|
+
const credentials = credentialsSchema.parse(JSON.parse(keyBase64));
|
|
348
|
+
return {
|
|
349
|
+
secret: new Uint8Array(Buffer.from(credentials.secret, "base64")),
|
|
350
|
+
token: credentials.token
|
|
351
|
+
};
|
|
352
|
+
} catch {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
async function writeCredentials(credentials) {
|
|
357
|
+
if (!existsSync(configuration.happyHomeDir)) {
|
|
358
|
+
await mkdir(configuration.happyHomeDir, { recursive: true });
|
|
359
|
+
}
|
|
360
|
+
await writeFile(configuration.privateKeyFile, JSON.stringify({
|
|
361
|
+
secret: encodeBase64(credentials.secret),
|
|
362
|
+
token: credentials.token
|
|
363
|
+
}, null, 2));
|
|
364
|
+
}
|
|
365
|
+
async function clearCredentials() {
|
|
366
|
+
if (existsSync(configuration.privateKeyFile)) {
|
|
367
|
+
await unlink(configuration.privateKeyFile);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function clearMachineId() {
|
|
371
|
+
await updateSettings((settings) => ({
|
|
372
|
+
...settings,
|
|
373
|
+
machineId: void 0
|
|
374
|
+
}));
|
|
375
|
+
}
|
|
376
|
+
async function readDaemonState() {
|
|
377
|
+
try {
|
|
378
|
+
if (!existsSync(configuration.daemonStateFile)) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
const content = await readFile(configuration.daemonStateFile, "utf-8");
|
|
382
|
+
return JSON.parse(content);
|
|
383
|
+
} catch (error) {
|
|
384
|
+
console.error(`[PERSISTENCE] Daemon state file corrupted: ${configuration.daemonStateFile}`, error);
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
function writeDaemonState(state) {
|
|
389
|
+
writeFileSync(configuration.daemonStateFile, JSON.stringify(state, null, 2), "utf-8");
|
|
390
|
+
}
|
|
391
|
+
async function clearDaemonState() {
|
|
392
|
+
if (existsSync(configuration.daemonStateFile)) {
|
|
393
|
+
await unlink(configuration.daemonStateFile);
|
|
394
|
+
}
|
|
395
|
+
if (existsSync(configuration.daemonLockFile)) {
|
|
396
|
+
try {
|
|
397
|
+
await unlink(configuration.daemonLockFile);
|
|
398
|
+
} catch {
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
async function acquireDaemonLock(maxAttempts = 5, delayIncrementMs = 200) {
|
|
403
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
404
|
+
try {
|
|
405
|
+
const fileHandle = await open(
|
|
406
|
+
configuration.daemonLockFile,
|
|
407
|
+
constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY
|
|
408
|
+
);
|
|
409
|
+
await fileHandle.writeFile(String(process.pid));
|
|
410
|
+
return fileHandle;
|
|
411
|
+
} catch (error) {
|
|
412
|
+
if (error.code === "EEXIST") {
|
|
413
|
+
try {
|
|
414
|
+
const lockPid = readFileSync(configuration.daemonLockFile, "utf-8").trim();
|
|
415
|
+
if (lockPid && !isNaN(Number(lockPid))) {
|
|
416
|
+
try {
|
|
417
|
+
process.kill(Number(lockPid), 0);
|
|
418
|
+
} catch {
|
|
419
|
+
unlinkSync(configuration.daemonLockFile);
|
|
420
|
+
continue;
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
if (attempt === maxAttempts) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
const delayMs = attempt * delayIncrementMs;
|
|
430
|
+
await new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return null;
|
|
434
|
+
}
|
|
435
|
+
async function releaseDaemonLock(lockHandle) {
|
|
436
|
+
try {
|
|
437
|
+
await lockHandle.close();
|
|
438
|
+
} catch {
|
|
439
|
+
}
|
|
440
|
+
try {
|
|
441
|
+
if (existsSync(configuration.daemonLockFile)) {
|
|
442
|
+
unlinkSync(configuration.daemonLockFile);
|
|
443
|
+
}
|
|
444
|
+
} catch {
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function createTimestampForFilename(date = /* @__PURE__ */ new Date()) {
|
|
449
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
450
|
+
const year = date.getFullYear();
|
|
451
|
+
const month = pad(date.getMonth() + 1);
|
|
452
|
+
const day = pad(date.getDate());
|
|
453
|
+
const hour = pad(date.getHours());
|
|
454
|
+
const minute = pad(date.getMinutes());
|
|
455
|
+
const second = pad(date.getSeconds());
|
|
456
|
+
return `${year}-${month}-${day}-${hour}-${minute}-${second}-pid-${process.pid}`;
|
|
457
|
+
}
|
|
458
|
+
function createTimestampForLogEntry(date = /* @__PURE__ */ new Date()) {
|
|
459
|
+
return date.toLocaleTimeString("en-US", {
|
|
460
|
+
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
461
|
+
hour12: false,
|
|
462
|
+
hour: "2-digit",
|
|
463
|
+
minute: "2-digit",
|
|
464
|
+
second: "2-digit",
|
|
465
|
+
fractionalSecondDigits: 3
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
function getSessionLogPath() {
|
|
469
|
+
const timestamp = createTimestampForFilename();
|
|
470
|
+
const filename = configuration.isDaemonProcess ? `${timestamp}-daemon.log` : `${timestamp}.log`;
|
|
471
|
+
return join(configuration.logsDir, filename);
|
|
472
|
+
}
|
|
473
|
+
class Logger {
|
|
474
|
+
constructor(logFilePath = getSessionLogPath()) {
|
|
475
|
+
this.logFilePath = logFilePath;
|
|
476
|
+
if (process.env.DANGEROUSLY_LOG_TO_SERVER_FOR_AI_AUTO_DEBUGGING && process.env.HAPPY_SERVER_URL) {
|
|
477
|
+
this.dangerouslyUnencryptedServerLoggingUrl = process.env.HAPPY_SERVER_URL;
|
|
478
|
+
console.log(chalk.yellow("[REMOTE LOGGING] Sending logs to server for AI debugging"));
|
|
479
|
+
}
|
|
480
|
+
try {
|
|
481
|
+
mkdirSync$1(configuration.logsDir, { recursive: true });
|
|
482
|
+
appendFileSync(this.logFilePath, "");
|
|
483
|
+
this.latestLogDupPath = join(configuration.logsDir, "latest-logs.txt");
|
|
484
|
+
writeFileSync$1(this.latestLogDupPath, `# Latest session log (overwritten each start)
|
|
485
|
+
# Path: ${this.logFilePath}
|
|
486
|
+
`);
|
|
487
|
+
try {
|
|
488
|
+
const legacy = join(configuration.logsDir, "latest-logs.text");
|
|
489
|
+
if (existsSync(legacy)) {
|
|
490
|
+
unlinkSync(legacy);
|
|
491
|
+
}
|
|
492
|
+
} catch {
|
|
493
|
+
}
|
|
494
|
+
if (configuration.isHeadlessMode) {
|
|
495
|
+
try {
|
|
496
|
+
console.log(`[HEADLESS] Logs directory: ${configuration.logsDir}`);
|
|
497
|
+
console.log(`[HEADLESS] Session log: ${this.logFilePath}`);
|
|
498
|
+
console.log(`[HEADLESS] Latest log mirror: ${this.latestLogDupPath}`);
|
|
499
|
+
} catch {
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
} catch (error) {
|
|
503
|
+
this.initError = error;
|
|
504
|
+
console.error(chalk.yellow(`[LOGGER] FAILED TO CREATE LOG FILE: ${this.logFilePath}`));
|
|
505
|
+
console.error(chalk.yellow(`[LOGGER] Error:`), error);
|
|
506
|
+
if (configuration.isHeadlessMode) {
|
|
507
|
+
try {
|
|
508
|
+
console.log(`[HEADLESS] Log init failed at: ${this.logFilePath}`);
|
|
509
|
+
console.log(`[HEADLESS] Reason: ${formatError(error)}`);
|
|
510
|
+
console.log(`[HEADLESS] Logs directory attempted: ${configuration.logsDir}`);
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
dangerouslyUnencryptedServerLoggingUrl;
|
|
517
|
+
latestLogDupPath;
|
|
518
|
+
initError;
|
|
519
|
+
// Use local timezone for simplicity of locating the logs,
|
|
520
|
+
// in practice you will not need absolute timestamps
|
|
521
|
+
localTimezoneTimestamp() {
|
|
522
|
+
return createTimestampForLogEntry();
|
|
523
|
+
}
|
|
524
|
+
debug(message, ...args) {
|
|
525
|
+
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
|
|
526
|
+
}
|
|
527
|
+
debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
|
|
528
|
+
if (!process.env.DEBUG) {
|
|
529
|
+
this.debug(`In production, skipping message inspection`);
|
|
530
|
+
}
|
|
531
|
+
const truncateStrings = (obj) => {
|
|
532
|
+
if (typeof obj === "string") {
|
|
533
|
+
return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
|
|
534
|
+
}
|
|
535
|
+
if (Array.isArray(obj)) {
|
|
536
|
+
const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
|
|
537
|
+
if (obj.length > maxArrayLength) {
|
|
538
|
+
truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
|
|
539
|
+
}
|
|
540
|
+
return truncatedArray;
|
|
541
|
+
}
|
|
542
|
+
if (obj && typeof obj === "object") {
|
|
543
|
+
const result = {};
|
|
544
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
545
|
+
if (key === "usage") {
|
|
546
|
+
continue;
|
|
547
|
+
}
|
|
548
|
+
result[key] = truncateStrings(value);
|
|
549
|
+
}
|
|
550
|
+
return result;
|
|
551
|
+
}
|
|
552
|
+
return obj;
|
|
553
|
+
};
|
|
554
|
+
const truncatedObject = truncateStrings(object);
|
|
555
|
+
const json = JSON.stringify(truncatedObject, null, 2);
|
|
556
|
+
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, "\n", json);
|
|
557
|
+
}
|
|
558
|
+
info(message, ...args) {
|
|
559
|
+
this.logToConsole("info", "", message, ...args);
|
|
560
|
+
this.debug(message, args);
|
|
561
|
+
}
|
|
562
|
+
infoDeveloper(message, ...args) {
|
|
563
|
+
this.debug(message, ...args);
|
|
564
|
+
if (process.env.DEBUG) {
|
|
565
|
+
this.logToConsole("info", "[DEV]", message, ...args);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
warn(message, ...args) {
|
|
569
|
+
this.logToConsole("warn", "", message, ...args);
|
|
570
|
+
this.debug(`[WARN] ${message}`, ...args);
|
|
571
|
+
}
|
|
572
|
+
getLogPath() {
|
|
573
|
+
return this.logFilePath;
|
|
574
|
+
}
|
|
575
|
+
logToConsole(level, prefix, message, ...args) {
|
|
576
|
+
switch (level) {
|
|
577
|
+
case "debug": {
|
|
578
|
+
console.log(chalk.gray(prefix), message, ...args);
|
|
579
|
+
break;
|
|
580
|
+
}
|
|
581
|
+
case "error": {
|
|
582
|
+
console.error(chalk.red(prefix), message, ...args);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
case "info": {
|
|
586
|
+
console.log(chalk.blue(prefix), message, ...args);
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
case "warn": {
|
|
590
|
+
console.log(chalk.yellow(prefix), message, ...args);
|
|
591
|
+
break;
|
|
592
|
+
}
|
|
593
|
+
default: {
|
|
594
|
+
this.debug("Unknown log level:", level);
|
|
595
|
+
console.log(chalk.blue(prefix), message, ...args);
|
|
596
|
+
break;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
async sendToRemoteServer(level, message, ...args) {
|
|
601
|
+
if (!this.dangerouslyUnencryptedServerLoggingUrl) return;
|
|
602
|
+
try {
|
|
603
|
+
await fetch(this.dangerouslyUnencryptedServerLoggingUrl + "/logs-combined-from-cli-and-mobile-for-simple-ai-debugging", {
|
|
604
|
+
method: "POST",
|
|
605
|
+
headers: { "Content-Type": "application/json" },
|
|
606
|
+
body: JSON.stringify({
|
|
607
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
608
|
+
level,
|
|
609
|
+
message: `${message} ${args.map(
|
|
610
|
+
(a) => typeof a === "object" ? JSON.stringify(a, null, 2) : String(a)
|
|
611
|
+
).join(" ")}`,
|
|
612
|
+
source: "cli",
|
|
613
|
+
platform: process.platform
|
|
614
|
+
})
|
|
615
|
+
});
|
|
616
|
+
} catch (error) {
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
logToFile(prefix, message, ...args) {
|
|
620
|
+
const logLine = `${prefix} ${message} ${args.map(
|
|
621
|
+
(arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
|
|
622
|
+
).join(" ")}
|
|
623
|
+
`;
|
|
624
|
+
if (this.dangerouslyUnencryptedServerLoggingUrl) {
|
|
625
|
+
let level = "info";
|
|
626
|
+
if (prefix.includes(this.localTimezoneTimestamp())) {
|
|
627
|
+
level = "debug";
|
|
628
|
+
}
|
|
629
|
+
this.sendToRemoteServer(level, message, ...args).catch(() => {
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
mkdirSync$1(configuration.logsDir, { recursive: true });
|
|
634
|
+
appendFileSync(this.logFilePath, logLine);
|
|
635
|
+
try {
|
|
636
|
+
appendFileSync(this.latestLogDupPath, logLine);
|
|
637
|
+
} catch {
|
|
638
|
+
}
|
|
639
|
+
} catch (appendError) {
|
|
640
|
+
console.error(`[LOGGER] FAILED TO WRITE LOG: ${this.logFilePath}`);
|
|
641
|
+
console.error(`[LOGGER] Error:`, appendError);
|
|
642
|
+
if (configuration.isHeadlessMode) {
|
|
643
|
+
try {
|
|
644
|
+
console.log(`[HEADLESS] Log write failed at: ${this.logFilePath}`);
|
|
645
|
+
console.log(`[HEADLESS] Reason: ${formatError(appendError)}`);
|
|
646
|
+
} catch {
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
if (process.env.DEBUG) {
|
|
650
|
+
throw appendError;
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
getInitError() {
|
|
655
|
+
return this.initError;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
let logger = new Logger();
|
|
659
|
+
function formatError(err) {
|
|
660
|
+
try {
|
|
661
|
+
if (!err) return "Unknown error";
|
|
662
|
+
if (typeof err === "string") return err;
|
|
663
|
+
if (err instanceof Error) {
|
|
664
|
+
const anyErr = err;
|
|
665
|
+
const code = anyErr && anyErr.code ? ` (code: ${anyErr.code})` : "";
|
|
666
|
+
return `${err.message}${code}`;
|
|
667
|
+
}
|
|
668
|
+
if (typeof err === "object") {
|
|
669
|
+
const anyErr = err;
|
|
670
|
+
const msg = anyErr.message ? String(anyErr.message) : JSON.stringify(anyErr);
|
|
671
|
+
const code = anyErr.code ? ` (code: ${anyErr.code})` : "";
|
|
672
|
+
return `${msg}${code}`;
|
|
673
|
+
}
|
|
674
|
+
return String(err);
|
|
675
|
+
} catch {
|
|
676
|
+
return "Unprintable error";
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
async function listDaemonLogFiles(limit = 50) {
|
|
680
|
+
try {
|
|
681
|
+
const logsDir = configuration.logsDir;
|
|
682
|
+
if (!existsSync(logsDir)) {
|
|
683
|
+
return [];
|
|
684
|
+
}
|
|
685
|
+
const logs = readdirSync(logsDir).filter((file) => file.endsWith("-daemon.log")).map((file) => {
|
|
686
|
+
const fullPath = join(logsDir, file);
|
|
687
|
+
const stats = statSync(fullPath);
|
|
688
|
+
return { file, path: fullPath, modified: stats.mtime };
|
|
689
|
+
}).sort((a, b) => b.modified.getTime() - a.modified.getTime());
|
|
690
|
+
try {
|
|
691
|
+
const state = await readDaemonState();
|
|
692
|
+
if (!state) {
|
|
693
|
+
return logs;
|
|
694
|
+
}
|
|
695
|
+
if (state.daemonLogPath && existsSync(state.daemonLogPath)) {
|
|
696
|
+
const stats = statSync(state.daemonLogPath);
|
|
697
|
+
const persisted = {
|
|
698
|
+
file: basename(state.daemonLogPath),
|
|
699
|
+
path: state.daemonLogPath,
|
|
700
|
+
modified: stats.mtime
|
|
701
|
+
};
|
|
702
|
+
const idx = logs.findIndex((l) => l.path === persisted.path);
|
|
703
|
+
if (idx >= 0) {
|
|
704
|
+
const [found] = logs.splice(idx, 1);
|
|
705
|
+
logs.unshift(found);
|
|
706
|
+
} else {
|
|
707
|
+
logs.unshift(persisted);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
} catch {
|
|
711
|
+
}
|
|
712
|
+
return logs.slice(0, Math.max(0, limit));
|
|
713
|
+
} catch {
|
|
714
|
+
return [];
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
async function getLatestDaemonLog() {
|
|
718
|
+
const [latest] = await listDaemonLogFiles(1);
|
|
719
|
+
return latest || null;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
const SessionMessageContentSchema = z$1.object({
|
|
723
|
+
c: z$1.string(),
|
|
724
|
+
// Base64 encoded encrypted content
|
|
725
|
+
t: z$1.literal("encrypted")
|
|
726
|
+
});
|
|
727
|
+
const UpdateBodySchema = z$1.object({
|
|
728
|
+
message: z$1.object({
|
|
729
|
+
id: z$1.string(),
|
|
730
|
+
seq: z$1.number(),
|
|
731
|
+
content: SessionMessageContentSchema
|
|
732
|
+
}),
|
|
733
|
+
sid: z$1.string(),
|
|
734
|
+
// Session ID
|
|
735
|
+
t: z$1.literal("new-message")
|
|
736
|
+
});
|
|
737
|
+
const UpdateSessionBodySchema = z$1.object({
|
|
738
|
+
t: z$1.literal("update-session"),
|
|
739
|
+
sid: z$1.string(),
|
|
740
|
+
metadata: z$1.object({
|
|
741
|
+
version: z$1.number(),
|
|
742
|
+
value: z$1.string()
|
|
743
|
+
}).nullish(),
|
|
744
|
+
agentState: z$1.object({
|
|
745
|
+
version: z$1.number(),
|
|
746
|
+
value: z$1.string()
|
|
747
|
+
}).nullish()
|
|
748
|
+
});
|
|
749
|
+
const UpdateMachineBodySchema = z$1.object({
|
|
750
|
+
t: z$1.literal("update-machine"),
|
|
751
|
+
machineId: z$1.string(),
|
|
752
|
+
metadata: z$1.object({
|
|
753
|
+
version: z$1.number(),
|
|
754
|
+
value: z$1.string()
|
|
755
|
+
}).nullish(),
|
|
756
|
+
daemonState: z$1.object({
|
|
757
|
+
version: z$1.number(),
|
|
758
|
+
value: z$1.string()
|
|
759
|
+
}).nullish()
|
|
760
|
+
});
|
|
761
|
+
z$1.object({
|
|
762
|
+
id: z$1.string(),
|
|
763
|
+
seq: z$1.number(),
|
|
764
|
+
body: z$1.union([
|
|
765
|
+
UpdateBodySchema,
|
|
766
|
+
UpdateSessionBodySchema,
|
|
767
|
+
UpdateMachineBodySchema
|
|
768
|
+
]),
|
|
769
|
+
createdAt: z$1.number()
|
|
770
|
+
});
|
|
771
|
+
z$1.object({
|
|
772
|
+
createdAt: z$1.number(),
|
|
773
|
+
id: z$1.string(),
|
|
774
|
+
seq: z$1.number(),
|
|
775
|
+
updatedAt: z$1.number(),
|
|
776
|
+
metadata: z$1.any(),
|
|
777
|
+
metadataVersion: z$1.number(),
|
|
778
|
+
agentState: z$1.any().nullable(),
|
|
779
|
+
agentStateVersion: z$1.number(),
|
|
780
|
+
// Connectivity tracking (from server)
|
|
781
|
+
connectivityStatus: z$1.union([
|
|
782
|
+
z$1.enum(["neverConnected", "online", "offline"]),
|
|
783
|
+
z$1.string()
|
|
784
|
+
// Forward compatibility
|
|
785
|
+
]).optional(),
|
|
786
|
+
connectivityStatusSince: z$1.number().optional(),
|
|
787
|
+
connectivityStatusReason: z$1.string().optional(),
|
|
788
|
+
// State tracking (from server)
|
|
789
|
+
state: z$1.union([
|
|
790
|
+
z$1.enum(["running", "archiveRequested", "archived"]),
|
|
791
|
+
z$1.string()
|
|
792
|
+
// Forward compatibility
|
|
793
|
+
]).optional(),
|
|
794
|
+
stateSince: z$1.number().optional(),
|
|
795
|
+
stateReason: z$1.string().optional()
|
|
796
|
+
});
|
|
797
|
+
z$1.object({
|
|
798
|
+
host: z$1.string(),
|
|
799
|
+
platform: z$1.string(),
|
|
800
|
+
happyCliVersion: z$1.string(),
|
|
801
|
+
homeDir: z$1.string(),
|
|
802
|
+
happyHomeDir: z$1.string(),
|
|
803
|
+
happyLibDir: z$1.string()
|
|
804
|
+
});
|
|
805
|
+
z$1.object({
|
|
806
|
+
status: z$1.union([
|
|
807
|
+
z$1.enum(["running", "shutting-down"]),
|
|
808
|
+
z$1.string()
|
|
809
|
+
// Forward compatibility
|
|
810
|
+
]),
|
|
811
|
+
pid: z$1.number().optional(),
|
|
812
|
+
httpPort: z$1.number().optional(),
|
|
813
|
+
startedAt: z$1.number().optional(),
|
|
814
|
+
shutdownRequestedAt: z$1.number().optional(),
|
|
815
|
+
shutdownSource: z$1.union([
|
|
816
|
+
z$1.enum(["mobile-app", "cli", "os-signal", "unknown"]),
|
|
817
|
+
z$1.string()
|
|
818
|
+
// Forward compatibility
|
|
819
|
+
]).optional()
|
|
820
|
+
});
|
|
821
|
+
z$1.object({
|
|
822
|
+
id: z$1.string(),
|
|
823
|
+
metadata: z$1.any(),
|
|
824
|
+
// Decrypted MachineMetadata
|
|
825
|
+
metadataVersion: z$1.number(),
|
|
826
|
+
daemonState: z$1.any().nullable(),
|
|
827
|
+
// Decrypted DaemonState
|
|
828
|
+
daemonStateVersion: z$1.number(),
|
|
829
|
+
// We don't really care about these on the CLI for now
|
|
830
|
+
// ApiMachineClient will not sync these
|
|
831
|
+
active: z$1.boolean(),
|
|
832
|
+
activeAt: z$1.number(),
|
|
833
|
+
createdAt: z$1.number(),
|
|
834
|
+
updatedAt: z$1.number(),
|
|
835
|
+
// Connectivity tracking (from server)
|
|
836
|
+
connectivityStatus: z$1.union([
|
|
837
|
+
z$1.enum(["neverConnected", "online", "offline"]),
|
|
838
|
+
z$1.string()
|
|
839
|
+
// Forward compatibility
|
|
840
|
+
]).optional(),
|
|
841
|
+
connectivityStatusSince: z$1.number().optional(),
|
|
842
|
+
connectivityStatusReason: z$1.string().optional(),
|
|
843
|
+
// State tracking (from server)
|
|
844
|
+
state: z$1.union([
|
|
845
|
+
z$1.enum(["running", "archiveRequested", "archived"]),
|
|
846
|
+
z$1.string()
|
|
847
|
+
// Forward compatibility
|
|
848
|
+
]).optional(),
|
|
849
|
+
stateSince: z$1.number().optional(),
|
|
850
|
+
stateReason: z$1.string().optional()
|
|
851
|
+
});
|
|
852
|
+
z$1.object({
|
|
853
|
+
content: SessionMessageContentSchema,
|
|
854
|
+
createdAt: z$1.number(),
|
|
855
|
+
id: z$1.string(),
|
|
856
|
+
seq: z$1.number(),
|
|
857
|
+
updatedAt: z$1.number()
|
|
858
|
+
});
|
|
859
|
+
const MessageMetaSchema = z$1.object({
|
|
860
|
+
sentFrom: z$1.string().optional(),
|
|
861
|
+
// Source identifier
|
|
862
|
+
permissionMode: z$1.string().optional(),
|
|
863
|
+
// Permission mode for this message
|
|
864
|
+
model: z$1.string().nullable().optional(),
|
|
865
|
+
// Model name for this message (null = reset)
|
|
866
|
+
fallbackModel: z$1.string().nullable().optional(),
|
|
867
|
+
// Fallback model for this message (null = reset)
|
|
868
|
+
customSystemPrompt: z$1.string().nullable().optional(),
|
|
869
|
+
// Custom system prompt for this message (null = reset)
|
|
870
|
+
appendSystemPrompt: z$1.string().nullable().optional(),
|
|
871
|
+
// Append to system prompt for this message (null = reset)
|
|
872
|
+
allowedTools: z$1.array(z$1.string()).nullable().optional(),
|
|
873
|
+
// Allowed tools for this message (null = reset)
|
|
874
|
+
disallowedTools: z$1.array(z$1.string()).nullable().optional()
|
|
875
|
+
// Disallowed tools for this message (null = reset)
|
|
876
|
+
});
|
|
877
|
+
z$1.object({
|
|
878
|
+
session: z$1.object({
|
|
879
|
+
id: z$1.string(),
|
|
880
|
+
tag: z$1.string(),
|
|
881
|
+
seq: z$1.number(),
|
|
882
|
+
createdAt: z$1.number(),
|
|
883
|
+
updatedAt: z$1.number(),
|
|
884
|
+
metadata: z$1.string(),
|
|
885
|
+
metadataVersion: z$1.number(),
|
|
886
|
+
agentState: z$1.string().nullable(),
|
|
887
|
+
agentStateVersion: z$1.number()
|
|
888
|
+
})
|
|
889
|
+
});
|
|
890
|
+
const UserMessageSchema = z$1.object({
|
|
891
|
+
role: z$1.literal("user"),
|
|
892
|
+
content: z$1.object({
|
|
893
|
+
type: z$1.literal("text"),
|
|
894
|
+
text: z$1.string()
|
|
895
|
+
}),
|
|
896
|
+
localKey: z$1.string().optional(),
|
|
897
|
+
// Mobile messages include this
|
|
898
|
+
meta: MessageMetaSchema.optional()
|
|
899
|
+
});
|
|
900
|
+
const AgentMessageSchema = z$1.object({
|
|
901
|
+
role: z$1.literal("agent"),
|
|
902
|
+
content: z$1.object({
|
|
903
|
+
type: z$1.literal("output"),
|
|
904
|
+
data: z$1.any()
|
|
905
|
+
}),
|
|
906
|
+
meta: MessageMetaSchema.optional()
|
|
907
|
+
});
|
|
908
|
+
z$1.union([UserMessageSchema, AgentMessageSchema]);
|
|
909
|
+
|
|
910
|
+
async function delay(ms) {
|
|
911
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
912
|
+
}
|
|
913
|
+
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
|
|
914
|
+
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.min(currentFailureCount, maxFailureCount);
|
|
915
|
+
return Math.round(Math.random() * maxDelayRet);
|
|
916
|
+
}
|
|
917
|
+
function createBackoff(opts) {
|
|
918
|
+
return async (callback) => {
|
|
919
|
+
let currentFailureCount = 0;
|
|
920
|
+
const minDelay = 250;
|
|
921
|
+
const maxDelay = 1e3;
|
|
922
|
+
const maxFailureCount = 50;
|
|
923
|
+
while (true) {
|
|
924
|
+
try {
|
|
925
|
+
return await callback();
|
|
926
|
+
} catch (e) {
|
|
927
|
+
if (currentFailureCount < maxFailureCount) {
|
|
928
|
+
currentFailureCount++;
|
|
929
|
+
}
|
|
930
|
+
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
|
|
931
|
+
await delay(waitForRequest);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
};
|
|
935
|
+
}
|
|
936
|
+
let backoff = createBackoff();
|
|
937
|
+
|
|
938
|
+
class AsyncLock {
|
|
939
|
+
permits = 1;
|
|
940
|
+
promiseResolverQueue = [];
|
|
941
|
+
async inLock(func) {
|
|
942
|
+
try {
|
|
943
|
+
await this.lock();
|
|
944
|
+
return await func();
|
|
945
|
+
} finally {
|
|
946
|
+
this.unlock();
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
async lock() {
|
|
950
|
+
if (this.permits > 0) {
|
|
951
|
+
this.permits = this.permits - 1;
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
await new Promise((resolve) => this.promiseResolverQueue.push(resolve));
|
|
955
|
+
}
|
|
956
|
+
unlock() {
|
|
957
|
+
this.permits += 1;
|
|
958
|
+
if (this.permits > 1 && this.promiseResolverQueue.length > 0) {
|
|
959
|
+
throw new Error("this.permits should never be > 0 when there is someone waiting.");
|
|
960
|
+
} else if (this.permits === 1 && this.promiseResolverQueue.length > 0) {
|
|
961
|
+
this.permits -= 1;
|
|
962
|
+
const nextResolver = this.promiseResolverQueue.shift();
|
|
963
|
+
if (nextResolver) {
|
|
964
|
+
setTimeout(() => {
|
|
965
|
+
nextResolver(true);
|
|
966
|
+
}, 0);
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
class RpcHandlerManager {
|
|
973
|
+
handlers = /* @__PURE__ */ new Map();
|
|
974
|
+
scopePrefix;
|
|
975
|
+
secret;
|
|
976
|
+
logger;
|
|
977
|
+
socket = null;
|
|
978
|
+
constructor(config) {
|
|
979
|
+
this.scopePrefix = config.scopePrefix;
|
|
980
|
+
this.secret = config.secret;
|
|
981
|
+
this.logger = config.logger || ((msg, data) => logger.debug(msg, data));
|
|
982
|
+
}
|
|
983
|
+
/**
|
|
984
|
+
* Register an RPC handler for a specific method
|
|
985
|
+
* @param method - The method name (without prefix)
|
|
986
|
+
* @param handler - The handler function
|
|
987
|
+
*/
|
|
988
|
+
registerHandler(method, handler) {
|
|
989
|
+
const prefixedMethod = this.getPrefixedMethod(method);
|
|
990
|
+
this.handlers.set(prefixedMethod, handler);
|
|
991
|
+
if (this.socket) {
|
|
992
|
+
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
/**
|
|
996
|
+
* Handle an incoming RPC request
|
|
997
|
+
* @param request - The RPC request data
|
|
998
|
+
* @param callback - The response callback
|
|
999
|
+
*/
|
|
1000
|
+
async handleRequest(request) {
|
|
1001
|
+
try {
|
|
1002
|
+
const handler = this.handlers.get(request.method);
|
|
1003
|
+
if (!handler) {
|
|
1004
|
+
this.logger("[RPC] [ERROR] Method not found", { method: request.method });
|
|
1005
|
+
const errorResponse = { error: "Method not found" };
|
|
1006
|
+
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
1007
|
+
return encryptedError;
|
|
1008
|
+
}
|
|
1009
|
+
const decryptedParams = decrypt(decodeBase64(request.params), this.secret);
|
|
1010
|
+
const result = await handler(decryptedParams);
|
|
1011
|
+
const encryptedResponse = encodeBase64(encrypt(result, this.secret));
|
|
1012
|
+
return encryptedResponse;
|
|
1013
|
+
} catch (error) {
|
|
1014
|
+
this.logger("[RPC] [ERROR] Error handling request", { error });
|
|
1015
|
+
const errorResponse = {
|
|
1016
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1017
|
+
};
|
|
1018
|
+
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
1019
|
+
return encryptedError;
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
onSocketConnect(socket) {
|
|
1023
|
+
this.socket = socket;
|
|
1024
|
+
for (const [prefixedMethod] of this.handlers) {
|
|
1025
|
+
socket.emit("rpc-register", { method: prefixedMethod });
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
onSocketDisconnect() {
|
|
1029
|
+
this.socket = null;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* Get the number of registered handlers
|
|
1033
|
+
*/
|
|
1034
|
+
getHandlerCount() {
|
|
1035
|
+
return this.handlers.size;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Check if a handler is registered
|
|
1039
|
+
* @param method - The method name (without prefix)
|
|
1040
|
+
*/
|
|
1041
|
+
hasHandler(method) {
|
|
1042
|
+
const prefixedMethod = this.getPrefixedMethod(method);
|
|
1043
|
+
return this.handlers.has(prefixedMethod);
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Clear all handlers
|
|
1047
|
+
*/
|
|
1048
|
+
clearHandlers() {
|
|
1049
|
+
this.handlers.clear();
|
|
1050
|
+
this.logger("Cleared all RPC handlers");
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Get the prefixed method name
|
|
1054
|
+
* @param method - The method name
|
|
1055
|
+
*/
|
|
1056
|
+
getPrefixedMethod(method) {
|
|
1057
|
+
return `${this.scopePrefix}:${method}`;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
1062
|
+
function projectPath() {
|
|
1063
|
+
const path = resolve(__dirname, "..");
|
|
1064
|
+
return path;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
function run$1(args, options) {
|
|
1068
|
+
const RUNNER_PATH = resolve(join$1(projectPath(), "scripts", "ripgrep_launcher.cjs"));
|
|
1069
|
+
return new Promise((resolve2, reject) => {
|
|
1070
|
+
const child = spawn("node", [RUNNER_PATH, JSON.stringify(args)], {
|
|
1071
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1072
|
+
cwd: options?.cwd
|
|
1073
|
+
});
|
|
1074
|
+
let stdout = "";
|
|
1075
|
+
let stderr = "";
|
|
1076
|
+
child.stdout.on("data", (data) => {
|
|
1077
|
+
stdout += data.toString();
|
|
1078
|
+
});
|
|
1079
|
+
child.stderr.on("data", (data) => {
|
|
1080
|
+
stderr += data.toString();
|
|
1081
|
+
});
|
|
1082
|
+
child.on("close", (code) => {
|
|
1083
|
+
resolve2({
|
|
1084
|
+
exitCode: code || 0,
|
|
1085
|
+
stdout,
|
|
1086
|
+
stderr
|
|
1087
|
+
});
|
|
1088
|
+
});
|
|
1089
|
+
child.on("error", (err) => {
|
|
1090
|
+
reject(err);
|
|
1091
|
+
});
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function getBinaryPath() {
|
|
1096
|
+
const platformName = platform();
|
|
1097
|
+
const binaryName = platformName === "win32" ? "difft.exe" : "difft";
|
|
1098
|
+
return resolve(join$1(projectPath(), "tools", "unpacked", binaryName));
|
|
1099
|
+
}
|
|
1100
|
+
function run(args, options) {
|
|
1101
|
+
const binaryPath = getBinaryPath();
|
|
1102
|
+
return new Promise((resolve2, reject) => {
|
|
1103
|
+
const child = spawn(binaryPath, args, {
|
|
1104
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1105
|
+
cwd: options?.cwd,
|
|
1106
|
+
env: {
|
|
1107
|
+
...process.env,
|
|
1108
|
+
// Force color output when needed
|
|
1109
|
+
FORCE_COLOR: "1"
|
|
1110
|
+
}
|
|
1111
|
+
});
|
|
1112
|
+
let stdout = "";
|
|
1113
|
+
let stderr = "";
|
|
1114
|
+
child.stdout.on("data", (data) => {
|
|
1115
|
+
stdout += data.toString();
|
|
1116
|
+
});
|
|
1117
|
+
child.stderr.on("data", (data) => {
|
|
1118
|
+
stderr += data.toString();
|
|
1119
|
+
});
|
|
1120
|
+
child.on("close", (code) => {
|
|
1121
|
+
resolve2({
|
|
1122
|
+
exitCode: code || 0,
|
|
1123
|
+
stdout,
|
|
1124
|
+
stderr
|
|
1125
|
+
});
|
|
1126
|
+
});
|
|
1127
|
+
child.on("error", (err) => {
|
|
1128
|
+
reject(err);
|
|
1129
|
+
});
|
|
1130
|
+
});
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const execAsync = promisify(exec);
|
|
1134
|
+
function registerCommonHandlers(rpcHandlerManager) {
|
|
1135
|
+
rpcHandlerManager.registerHandler("bash", async (data) => {
|
|
1136
|
+
logger.debug("Shell command request:", data.command);
|
|
1137
|
+
try {
|
|
1138
|
+
const options = {
|
|
1139
|
+
cwd: data.cwd,
|
|
1140
|
+
timeout: data.timeout || 3e4
|
|
1141
|
+
// Default 30 seconds timeout
|
|
1142
|
+
};
|
|
1143
|
+
const { stdout, stderr } = await execAsync(data.command, options);
|
|
1144
|
+
return {
|
|
1145
|
+
success: true,
|
|
1146
|
+
stdout: stdout ? stdout.toString() : "",
|
|
1147
|
+
stderr: stderr ? stderr.toString() : "",
|
|
1148
|
+
exitCode: 0
|
|
1149
|
+
};
|
|
1150
|
+
} catch (error) {
|
|
1151
|
+
const execError = error;
|
|
1152
|
+
if (execError.code === "ETIMEDOUT" || execError.killed) {
|
|
1153
|
+
return {
|
|
1154
|
+
success: false,
|
|
1155
|
+
stdout: execError.stdout || "",
|
|
1156
|
+
stderr: execError.stderr || "",
|
|
1157
|
+
exitCode: typeof execError.code === "number" ? execError.code : -1,
|
|
1158
|
+
error: "Command timed out"
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
return {
|
|
1162
|
+
success: false,
|
|
1163
|
+
stdout: execError.stdout ? execError.stdout.toString() : "",
|
|
1164
|
+
stderr: execError.stderr ? execError.stderr.toString() : execError.message || "Command failed",
|
|
1165
|
+
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
1166
|
+
error: execError.message || "Command failed"
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
});
|
|
1170
|
+
rpcHandlerManager.registerHandler("readFile", async (data) => {
|
|
1171
|
+
logger.debug("Read file request:", data.path);
|
|
1172
|
+
try {
|
|
1173
|
+
const buffer = await readFile$1(data.path);
|
|
1174
|
+
const content = buffer.toString("base64");
|
|
1175
|
+
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
1176
|
+
return { success: true, content, hash };
|
|
1177
|
+
} catch (error) {
|
|
1178
|
+
logger.debug("Failed to read file:", error);
|
|
1179
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
1180
|
+
}
|
|
1181
|
+
});
|
|
1182
|
+
rpcHandlerManager.registerHandler("writeFile", async (data) => {
|
|
1183
|
+
logger.debug("Write file request:", data.path);
|
|
1184
|
+
try {
|
|
1185
|
+
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
1186
|
+
try {
|
|
1187
|
+
const existingBuffer = await readFile$1(data.path);
|
|
1188
|
+
const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
|
|
1189
|
+
if (existingHash !== data.expectedHash) {
|
|
1190
|
+
return {
|
|
1191
|
+
success: false,
|
|
1192
|
+
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
const nodeError = error;
|
|
1197
|
+
if (nodeError.code !== "ENOENT") {
|
|
1198
|
+
throw error;
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
success: false,
|
|
1202
|
+
error: "File does not exist but hash was provided"
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
} else {
|
|
1206
|
+
try {
|
|
1207
|
+
await stat$1(data.path);
|
|
1208
|
+
return {
|
|
1209
|
+
success: false,
|
|
1210
|
+
error: "File already exists but was expected to be new"
|
|
1211
|
+
};
|
|
1212
|
+
} catch (error) {
|
|
1213
|
+
const nodeError = error;
|
|
1214
|
+
if (nodeError.code !== "ENOENT") {
|
|
1215
|
+
throw error;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
const buffer = Buffer.from(data.content, "base64");
|
|
1220
|
+
await writeFile$1(data.path, buffer);
|
|
1221
|
+
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
1222
|
+
return { success: true, hash };
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
logger.debug("Failed to write file:", error);
|
|
1225
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
1226
|
+
}
|
|
1227
|
+
});
|
|
1228
|
+
rpcHandlerManager.registerHandler("listDirectory", async (data) => {
|
|
1229
|
+
logger.debug("List directory request:", data.path);
|
|
1230
|
+
try {
|
|
1231
|
+
const entries = await readdir(data.path, { withFileTypes: true });
|
|
1232
|
+
const directoryEntries = await Promise.all(
|
|
1233
|
+
entries.map(async (entry) => {
|
|
1234
|
+
const fullPath = join$1(data.path, entry.name);
|
|
1235
|
+
let type = "other";
|
|
1236
|
+
let size;
|
|
1237
|
+
let modified;
|
|
1238
|
+
if (entry.isDirectory()) {
|
|
1239
|
+
type = "directory";
|
|
1240
|
+
} else if (entry.isFile()) {
|
|
1241
|
+
type = "file";
|
|
1242
|
+
}
|
|
1243
|
+
try {
|
|
1244
|
+
const stats = await stat$1(fullPath);
|
|
1245
|
+
size = stats.size;
|
|
1246
|
+
modified = stats.mtime.getTime();
|
|
1247
|
+
} catch (error) {
|
|
1248
|
+
logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
1249
|
+
}
|
|
1250
|
+
return {
|
|
1251
|
+
name: entry.name,
|
|
1252
|
+
type,
|
|
1253
|
+
size,
|
|
1254
|
+
modified
|
|
1255
|
+
};
|
|
1256
|
+
})
|
|
1257
|
+
);
|
|
1258
|
+
directoryEntries.sort((a, b) => {
|
|
1259
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1260
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1261
|
+
return a.name.localeCompare(b.name);
|
|
1262
|
+
});
|
|
1263
|
+
return { success: true, entries: directoryEntries };
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
logger.debug("Failed to list directory:", error);
|
|
1266
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
1267
|
+
}
|
|
1268
|
+
});
|
|
1269
|
+
rpcHandlerManager.registerHandler("getDirectoryTree", async (data) => {
|
|
1270
|
+
logger.debug("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
1271
|
+
async function buildTree(path, name, currentDepth) {
|
|
1272
|
+
try {
|
|
1273
|
+
const stats = await stat$1(path);
|
|
1274
|
+
const node = {
|
|
1275
|
+
name,
|
|
1276
|
+
path,
|
|
1277
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
1278
|
+
size: stats.size,
|
|
1279
|
+
modified: stats.mtime.getTime()
|
|
1280
|
+
};
|
|
1281
|
+
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
1282
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
1283
|
+
const children = [];
|
|
1284
|
+
await Promise.all(
|
|
1285
|
+
entries.map(async (entry) => {
|
|
1286
|
+
if (entry.isSymbolicLink()) {
|
|
1287
|
+
logger.debug(`Skipping symlink: ${join$1(path, entry.name)}`);
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
const childPath = join$1(path, entry.name);
|
|
1291
|
+
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
1292
|
+
if (childNode) {
|
|
1293
|
+
children.push(childNode);
|
|
1294
|
+
}
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
children.sort((a, b) => {
|
|
1298
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1299
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1300
|
+
return a.name.localeCompare(b.name);
|
|
1301
|
+
});
|
|
1302
|
+
node.children = children;
|
|
1303
|
+
}
|
|
1304
|
+
return node;
|
|
1305
|
+
} catch (error) {
|
|
1306
|
+
logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
|
|
1307
|
+
return null;
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
try {
|
|
1311
|
+
if (data.maxDepth < 0) {
|
|
1312
|
+
return { success: false, error: "maxDepth must be non-negative" };
|
|
1313
|
+
}
|
|
1314
|
+
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
|
|
1315
|
+
const tree = await buildTree(data.path, baseName, 0);
|
|
1316
|
+
if (!tree) {
|
|
1317
|
+
return { success: false, error: "Failed to access the specified path" };
|
|
1318
|
+
}
|
|
1319
|
+
return { success: true, tree };
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
logger.debug("Failed to get directory tree:", error);
|
|
1322
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
rpcHandlerManager.registerHandler("ripgrep", async (data) => {
|
|
1326
|
+
logger.debug("Ripgrep request with args:", data.args, "cwd:", data.cwd);
|
|
1327
|
+
try {
|
|
1328
|
+
const result = await run$1(data.args, { cwd: data.cwd });
|
|
1329
|
+
return {
|
|
1330
|
+
success: true,
|
|
1331
|
+
exitCode: result.exitCode,
|
|
1332
|
+
stdout: result.stdout.toString(),
|
|
1333
|
+
stderr: result.stderr.toString()
|
|
1334
|
+
};
|
|
1335
|
+
} catch (error) {
|
|
1336
|
+
logger.debug("Failed to run ripgrep:", error);
|
|
1337
|
+
return {
|
|
1338
|
+
success: false,
|
|
1339
|
+
error: error instanceof Error ? error.message : "Failed to run ripgrep"
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
});
|
|
1343
|
+
rpcHandlerManager.registerHandler("difftastic", async (data) => {
|
|
1344
|
+
logger.debug("Difftastic request with args:", data.args, "cwd:", data.cwd);
|
|
1345
|
+
try {
|
|
1346
|
+
const result = await run(data.args, { cwd: data.cwd });
|
|
1347
|
+
return {
|
|
1348
|
+
success: true,
|
|
1349
|
+
exitCode: result.exitCode,
|
|
1350
|
+
stdout: result.stdout.toString(),
|
|
1351
|
+
stderr: result.stderr.toString()
|
|
1352
|
+
};
|
|
1353
|
+
} catch (error) {
|
|
1354
|
+
logger.debug("Failed to run difftastic:", error);
|
|
1355
|
+
return {
|
|
1356
|
+
success: false,
|
|
1357
|
+
error: error instanceof Error ? error.message : "Failed to run difftastic"
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
});
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
function getHeadlessJsonPath() {
|
|
1364
|
+
return resolve$1(configuration.happyHomeDir, "link", "headless.json");
|
|
1365
|
+
}
|
|
1366
|
+
function writeHeadlessJson(updates) {
|
|
1367
|
+
try {
|
|
1368
|
+
const outputDir = resolve$1(configuration.happyHomeDir, "link");
|
|
1369
|
+
const jsonPath = getHeadlessJsonPath();
|
|
1370
|
+
mkdirSync(outputDir, { recursive: true });
|
|
1371
|
+
let data = {};
|
|
1372
|
+
try {
|
|
1373
|
+
const existing = readFileSync(jsonPath, "utf8");
|
|
1374
|
+
data = JSON.parse(existing);
|
|
1375
|
+
} catch {
|
|
1376
|
+
}
|
|
1377
|
+
if (typeof data.claude_return_auth !== "string") data.claude_return_auth = "";
|
|
1378
|
+
if (typeof data.codex_return_auth !== "string") data.codex_return_auth = "";
|
|
1379
|
+
if (typeof data.claude_auth_url !== "string") data.claude_auth_url = "";
|
|
1380
|
+
if (typeof data.codex_auth_url !== "string") data.codex_auth_url = "";
|
|
1381
|
+
if (typeof data.last_message_time !== "number") data.last_message_time = 0;
|
|
1382
|
+
Object.assign(data, updates);
|
|
1383
|
+
writeFileSync(jsonPath, JSON.stringify(data, null, 2));
|
|
1384
|
+
logger.debug(`[headlessJson] Updated: ${JSON.stringify(updates)}`);
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
logger.debug(`[headlessJson] Failed to write: ${error}`);
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
function updateActivityTimestamp() {
|
|
1390
|
+
writeHeadlessJson({
|
|
1391
|
+
last_message_time: Math.floor(Date.now() / 1e3)
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
class ApiSessionClient extends EventEmitter {
|
|
1396
|
+
token;
|
|
1397
|
+
secret;
|
|
1398
|
+
sessionId;
|
|
1399
|
+
metadata;
|
|
1400
|
+
metadataVersion;
|
|
1401
|
+
agentState;
|
|
1402
|
+
agentStateVersion;
|
|
1403
|
+
socket;
|
|
1404
|
+
pendingMessages = [];
|
|
1405
|
+
pendingMessageCallback = null;
|
|
1406
|
+
rpcHandlerManager;
|
|
1407
|
+
agentStateLock = new AsyncLock();
|
|
1408
|
+
metadataLock = new AsyncLock();
|
|
1409
|
+
constructor(token, secret, session) {
|
|
1410
|
+
super();
|
|
1411
|
+
this.token = token;
|
|
1412
|
+
this.secret = secret;
|
|
1413
|
+
this.sessionId = session.id;
|
|
1414
|
+
this.metadata = session.metadata;
|
|
1415
|
+
this.metadataVersion = session.metadataVersion;
|
|
1416
|
+
this.agentState = session.agentState;
|
|
1417
|
+
this.agentStateVersion = session.agentStateVersion;
|
|
1418
|
+
this.rpcHandlerManager = new RpcHandlerManager({
|
|
1419
|
+
scopePrefix: this.sessionId,
|
|
1420
|
+
secret: this.secret,
|
|
1421
|
+
logger: (msg, data) => logger.debug(msg, data)
|
|
1422
|
+
});
|
|
1423
|
+
registerCommonHandlers(this.rpcHandlerManager);
|
|
1424
|
+
this.socket = io(configuration.serverUrl, {
|
|
1425
|
+
auth: {
|
|
1426
|
+
token: this.token,
|
|
1427
|
+
clientType: "session-scoped",
|
|
1428
|
+
sessionId: this.sessionId
|
|
1429
|
+
},
|
|
1430
|
+
path: "/v1/updates",
|
|
1431
|
+
reconnection: true,
|
|
1432
|
+
reconnectionAttempts: Infinity,
|
|
1433
|
+
reconnectionDelay: 1e3,
|
|
1434
|
+
reconnectionDelayMax: 5e3,
|
|
1435
|
+
transports: ["websocket"],
|
|
1436
|
+
withCredentials: true,
|
|
1437
|
+
autoConnect: false
|
|
1438
|
+
});
|
|
1439
|
+
this.socket.on("connect", () => {
|
|
1440
|
+
logger.debug("Socket connected successfully");
|
|
1441
|
+
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
1442
|
+
});
|
|
1443
|
+
this.socket.on("rpc-request", async (data, callback) => {
|
|
1444
|
+
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
1445
|
+
});
|
|
1446
|
+
this.socket.on("disconnect", (reason) => {
|
|
1447
|
+
logger.debug("[API] Socket disconnected:", reason);
|
|
1448
|
+
this.rpcHandlerManager.onSocketDisconnect();
|
|
1449
|
+
});
|
|
1450
|
+
this.socket.on("connect_error", (error) => {
|
|
1451
|
+
logger.debug("[API] Socket connection error:", error);
|
|
1452
|
+
this.rpcHandlerManager.onSocketDisconnect();
|
|
1453
|
+
});
|
|
1454
|
+
this.socket.on("update", (data) => {
|
|
1455
|
+
try {
|
|
1456
|
+
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", data);
|
|
1457
|
+
if (!data.body) {
|
|
1458
|
+
logger.debug("[SOCKET] [UPDATE] [ERROR] No body in update!");
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
|
|
1462
|
+
const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
|
|
1463
|
+
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
1464
|
+
const userResult = UserMessageSchema.safeParse(body);
|
|
1465
|
+
if (userResult.success) {
|
|
1466
|
+
updateActivityTimestamp();
|
|
1467
|
+
if (this.pendingMessageCallback) {
|
|
1468
|
+
this.pendingMessageCallback(userResult.data);
|
|
1469
|
+
} else {
|
|
1470
|
+
this.pendingMessages.push(userResult.data);
|
|
1471
|
+
}
|
|
1472
|
+
} else {
|
|
1473
|
+
this.emit("message", body);
|
|
1474
|
+
}
|
|
1475
|
+
} else if (data.body.t === "update-session") {
|
|
1476
|
+
if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
|
|
1477
|
+
this.metadata = decrypt(decodeBase64(data.body.metadata.value), this.secret);
|
|
1478
|
+
this.metadataVersion = data.body.metadata.version;
|
|
1479
|
+
}
|
|
1480
|
+
if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
|
|
1481
|
+
this.agentState = data.body.agentState.value ? decrypt(decodeBase64(data.body.agentState.value), this.secret) : null;
|
|
1482
|
+
this.agentStateVersion = data.body.agentState.version;
|
|
1483
|
+
}
|
|
1484
|
+
} else if (data.body.t === "update-machine") {
|
|
1485
|
+
logger.debug(`[SOCKET] WARNING: Session client received unexpected machine update - ignoring`);
|
|
1486
|
+
} else {
|
|
1487
|
+
this.emit("message", data.body);
|
|
1488
|
+
}
|
|
1489
|
+
} catch (error) {
|
|
1490
|
+
logger.debug("[SOCKET] [UPDATE] [ERROR] Error handling update", { error });
|
|
1491
|
+
}
|
|
1492
|
+
});
|
|
1493
|
+
this.socket.on("error", (error) => {
|
|
1494
|
+
logger.debug("[API] Socket error:", error);
|
|
1495
|
+
});
|
|
1496
|
+
this.socket.connect();
|
|
1497
|
+
}
|
|
1498
|
+
onUserMessage(callback) {
|
|
1499
|
+
this.pendingMessageCallback = callback;
|
|
1500
|
+
while (this.pendingMessages.length > 0) {
|
|
1501
|
+
callback(this.pendingMessages.shift());
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
/**
|
|
1505
|
+
* Send message to session
|
|
1506
|
+
* @param body - Message body (can be MessageContent or raw content for agent messages)
|
|
1507
|
+
*/
|
|
1508
|
+
sendClaudeSessionMessage(body) {
|
|
1509
|
+
let content;
|
|
1510
|
+
if (body.type === "user" && body.isSidechain !== true && body.isMeta !== true) {
|
|
1511
|
+
let userText = null;
|
|
1512
|
+
const raw = body.message?.content;
|
|
1513
|
+
if (typeof raw === "string") {
|
|
1514
|
+
userText = raw;
|
|
1515
|
+
} else if (Array.isArray(raw)) {
|
|
1516
|
+
const pieces = [];
|
|
1517
|
+
for (const c of raw) {
|
|
1518
|
+
if (c && typeof c === "object" && c.type === "text" && typeof c.text === "string") {
|
|
1519
|
+
pieces.push(c.text);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
if (pieces.length > 0) {
|
|
1523
|
+
userText = pieces.join("\n\n");
|
|
1524
|
+
}
|
|
1525
|
+
}
|
|
1526
|
+
if (typeof userText === "string") {
|
|
1527
|
+
content = {
|
|
1528
|
+
role: "user",
|
|
1529
|
+
content: { type: "text", text: userText },
|
|
1530
|
+
meta: { sentFrom: "cli" }
|
|
1531
|
+
};
|
|
1532
|
+
updateActivityTimestamp();
|
|
1533
|
+
} else {
|
|
1534
|
+
content = {
|
|
1535
|
+
role: "agent",
|
|
1536
|
+
content: { type: "output", data: body },
|
|
1537
|
+
meta: { sentFrom: "cli" }
|
|
1538
|
+
};
|
|
1539
|
+
updateActivityTimestamp();
|
|
1540
|
+
}
|
|
1541
|
+
} else {
|
|
1542
|
+
content = {
|
|
1543
|
+
role: "agent",
|
|
1544
|
+
content: { type: "output", data: body },
|
|
1545
|
+
meta: { sentFrom: "cli" }
|
|
1546
|
+
};
|
|
1547
|
+
updateActivityTimestamp();
|
|
1548
|
+
}
|
|
1549
|
+
logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
|
|
1550
|
+
const encrypted = encodeBase64(encrypt(content, this.secret));
|
|
1551
|
+
this.socket.emit("message", {
|
|
1552
|
+
sid: this.sessionId,
|
|
1553
|
+
message: encrypted
|
|
1554
|
+
});
|
|
1555
|
+
if (body.type === "assistant" && body.message.usage) {
|
|
1556
|
+
try {
|
|
1557
|
+
this.sendUsageData(body.message.usage);
|
|
1558
|
+
} catch (error) {
|
|
1559
|
+
logger.debug("[SOCKET] Failed to send usage data:", error);
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1562
|
+
if (body.type === "summary" && "summary" in body && "leafUuid" in body) {
|
|
1563
|
+
this.updateMetadata((metadata) => ({
|
|
1564
|
+
...metadata,
|
|
1565
|
+
summary: {
|
|
1566
|
+
text: body.summary,
|
|
1567
|
+
updatedAt: Date.now()
|
|
1568
|
+
}
|
|
1569
|
+
}));
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
sendCodexMessage(body) {
|
|
1573
|
+
updateActivityTimestamp();
|
|
1574
|
+
let content = {
|
|
1575
|
+
role: "agent",
|
|
1576
|
+
content: {
|
|
1577
|
+
type: "codex",
|
|
1578
|
+
data: body
|
|
1579
|
+
// This wraps the entire Claude message
|
|
1580
|
+
},
|
|
1581
|
+
meta: {
|
|
1582
|
+
sentFrom: "cli"
|
|
1583
|
+
}
|
|
1584
|
+
};
|
|
1585
|
+
logger.debugLargeJson("[SOCKET] Sending codex message through socket:", content);
|
|
1586
|
+
const encrypted = encodeBase64(encrypt(content, this.secret));
|
|
1587
|
+
this.socket.emit("message", {
|
|
1588
|
+
sid: this.sessionId,
|
|
1589
|
+
message: encrypted
|
|
1590
|
+
});
|
|
1591
|
+
}
|
|
1592
|
+
sendGeminiMessage(body) {
|
|
1593
|
+
updateActivityTimestamp();
|
|
1594
|
+
let content = {
|
|
1595
|
+
role: "agent",
|
|
1596
|
+
content: {
|
|
1597
|
+
type: "gemini",
|
|
1598
|
+
data: body
|
|
1599
|
+
},
|
|
1600
|
+
meta: {
|
|
1601
|
+
sentFrom: "cli"
|
|
1602
|
+
}
|
|
1603
|
+
};
|
|
1604
|
+
const encrypted = encodeBase64(encrypt(content, this.secret));
|
|
1605
|
+
this.socket.emit("message", {
|
|
1606
|
+
sid: this.sessionId,
|
|
1607
|
+
message: encrypted
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
1610
|
+
sendSessionEvent(event, id) {
|
|
1611
|
+
let content = {
|
|
1612
|
+
role: "agent",
|
|
1613
|
+
content: {
|
|
1614
|
+
id: id ?? randomUUID(),
|
|
1615
|
+
type: "event",
|
|
1616
|
+
data: event
|
|
1617
|
+
}
|
|
1618
|
+
};
|
|
1619
|
+
const encrypted = encodeBase64(encrypt(content, this.secret));
|
|
1620
|
+
this.socket.emit("message", {
|
|
1621
|
+
sid: this.sessionId,
|
|
1622
|
+
message: encrypted
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
/**
|
|
1626
|
+
* Send a ping message to keep the connection alive
|
|
1627
|
+
*/
|
|
1628
|
+
keepAlive(thinking, mode) {
|
|
1629
|
+
if (process.env.DEBUG) {
|
|
1630
|
+
logger.debug(`[API] Sending keep alive message: ${thinking}`);
|
|
1631
|
+
}
|
|
1632
|
+
this.socket.volatile.emit("session-alive", {
|
|
1633
|
+
sid: this.sessionId,
|
|
1634
|
+
time: Date.now(),
|
|
1635
|
+
thinking,
|
|
1636
|
+
mode
|
|
1637
|
+
});
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Send session death message
|
|
1641
|
+
*/
|
|
1642
|
+
sendSessionDeath() {
|
|
1643
|
+
this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
|
|
1644
|
+
}
|
|
1645
|
+
/**
|
|
1646
|
+
* Send usage data to the server
|
|
1647
|
+
*/
|
|
1648
|
+
sendUsageData(usage) {
|
|
1649
|
+
const totalTokens = usage.input_tokens + usage.output_tokens + (usage.cache_creation_input_tokens || 0) + (usage.cache_read_input_tokens || 0);
|
|
1650
|
+
const usageReport = {
|
|
1651
|
+
key: "claude-session",
|
|
1652
|
+
sessionId: this.sessionId,
|
|
1653
|
+
tokens: {
|
|
1654
|
+
total: totalTokens,
|
|
1655
|
+
input: usage.input_tokens,
|
|
1656
|
+
output: usage.output_tokens,
|
|
1657
|
+
cache_creation: usage.cache_creation_input_tokens || 0,
|
|
1658
|
+
cache_read: usage.cache_read_input_tokens || 0
|
|
1659
|
+
},
|
|
1660
|
+
cost: {
|
|
1661
|
+
// TODO: Calculate actual costs based on pricing
|
|
1662
|
+
// For now, using placeholder values
|
|
1663
|
+
total: 0,
|
|
1664
|
+
input: 0,
|
|
1665
|
+
output: 0
|
|
1666
|
+
}
|
|
1667
|
+
};
|
|
1668
|
+
logger.debugLargeJson("[SOCKET] Sending usage data:", usageReport);
|
|
1669
|
+
this.socket.emit("usage-report", usageReport);
|
|
1670
|
+
}
|
|
1671
|
+
/**
|
|
1672
|
+
* Update session metadata
|
|
1673
|
+
* @param handler - Handler function that returns the updated metadata
|
|
1674
|
+
*/
|
|
1675
|
+
updateMetadata(handler) {
|
|
1676
|
+
this.metadataLock.inLock(async () => {
|
|
1677
|
+
await backoff(async () => {
|
|
1678
|
+
let updated = handler(this.metadata);
|
|
1679
|
+
const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
|
|
1680
|
+
if (answer.result === "success") {
|
|
1681
|
+
this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
1682
|
+
this.metadataVersion = answer.version;
|
|
1683
|
+
} else if (answer.result === "version-mismatch") {
|
|
1684
|
+
if (answer.version > this.metadataVersion) {
|
|
1685
|
+
this.metadataVersion = answer.version;
|
|
1686
|
+
this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
1687
|
+
}
|
|
1688
|
+
throw new Error("Metadata version mismatch");
|
|
1689
|
+
} else if (answer.result === "error") ;
|
|
1690
|
+
});
|
|
1691
|
+
});
|
|
1692
|
+
}
|
|
1693
|
+
/**
|
|
1694
|
+
* Update session agent state
|
|
1695
|
+
* @param handler - Handler function that returns the updated agent state
|
|
1696
|
+
*/
|
|
1697
|
+
updateAgentState(handler) {
|
|
1698
|
+
logger.debugLargeJson("Updating agent state", this.agentState);
|
|
1699
|
+
this.agentStateLock.inLock(async () => {
|
|
1700
|
+
await backoff(async () => {
|
|
1701
|
+
let updated = handler(this.agentState || {});
|
|
1702
|
+
const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
|
|
1703
|
+
if (answer.result === "success") {
|
|
1704
|
+
this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
|
|
1705
|
+
this.agentStateVersion = answer.version;
|
|
1706
|
+
logger.debug("Agent state updated", this.agentState);
|
|
1707
|
+
} else if (answer.result === "version-mismatch") {
|
|
1708
|
+
if (answer.version > this.agentStateVersion) {
|
|
1709
|
+
this.agentStateVersion = answer.version;
|
|
1710
|
+
this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
|
|
1711
|
+
}
|
|
1712
|
+
throw new Error("Agent state version mismatch");
|
|
1713
|
+
} else if (answer.result === "error") ;
|
|
1714
|
+
});
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
/**
|
|
1718
|
+
* Wait for socket buffer to flush
|
|
1719
|
+
*/
|
|
1720
|
+
async flush() {
|
|
1721
|
+
if (!this.socket.connected) {
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
return new Promise((resolve) => {
|
|
1725
|
+
this.socket.emit("ping", () => {
|
|
1726
|
+
resolve();
|
|
1727
|
+
});
|
|
1728
|
+
setTimeout(() => {
|
|
1729
|
+
resolve();
|
|
1730
|
+
}, 1e4);
|
|
1731
|
+
});
|
|
1732
|
+
}
|
|
1733
|
+
async close() {
|
|
1734
|
+
logger.debug("[API] socket.close() called");
|
|
1735
|
+
this.socket.close();
|
|
1736
|
+
}
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
class TerminalManager {
|
|
1740
|
+
constructor(callbacks) {
|
|
1741
|
+
this.callbacks = callbacks;
|
|
1742
|
+
}
|
|
1743
|
+
sessions = /* @__PURE__ */ new Map();
|
|
1744
|
+
/**
|
|
1745
|
+
* Spawn a new PTY terminal session
|
|
1746
|
+
*/
|
|
1747
|
+
spawn(sessionId, cols = 80, rows = 24) {
|
|
1748
|
+
if (this.sessions.has(sessionId)) {
|
|
1749
|
+
logger.debug(`[TERMINAL] Session ${sessionId} already exists`);
|
|
1750
|
+
this.callbacks.onError(sessionId, "ALREADY_EXISTS", "Terminal session already exists");
|
|
1751
|
+
return;
|
|
1752
|
+
}
|
|
1753
|
+
try {
|
|
1754
|
+
const shell = process.env.SHELL || (os.platform() === "win32" ? "powershell.exe" : "/bin/bash");
|
|
1755
|
+
const cwd = process.env.HOME || process.env.USERPROFILE || process.cwd();
|
|
1756
|
+
logger.debug(`[TERMINAL] Spawning PTY for session ${sessionId}: shell=${shell}, cwd=${cwd}, size=${cols}x${rows}`);
|
|
1757
|
+
const pty = spawn$1(shell, [], {
|
|
1758
|
+
name: "xterm-256color",
|
|
1759
|
+
cols,
|
|
1760
|
+
rows,
|
|
1761
|
+
cwd,
|
|
1762
|
+
env: process.env
|
|
1763
|
+
});
|
|
1764
|
+
const session = {
|
|
1765
|
+
id: sessionId,
|
|
1766
|
+
pty,
|
|
1767
|
+
cols,
|
|
1768
|
+
rows,
|
|
1769
|
+
createdAt: Date.now()
|
|
1770
|
+
};
|
|
1771
|
+
this.sessions.set(sessionId, session);
|
|
1772
|
+
pty.onData((data) => {
|
|
1773
|
+
const base64 = Buffer.from(data, "utf-8").toString("base64");
|
|
1774
|
+
this.callbacks.onStdout(sessionId, base64);
|
|
1775
|
+
});
|
|
1776
|
+
pty.onExit(({ exitCode, signal }) => {
|
|
1777
|
+
logger.debug(`[TERMINAL] Session ${sessionId} exited: code=${exitCode}, signal=${signal}`);
|
|
1778
|
+
this.callbacks.onExit(sessionId, exitCode, signal);
|
|
1779
|
+
this.sessions.delete(sessionId);
|
|
1780
|
+
});
|
|
1781
|
+
this.callbacks.onOpen(sessionId);
|
|
1782
|
+
logger.debug(`[TERMINAL] Session ${sessionId} spawned successfully`);
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
logger.debug(`[TERMINAL] Failed to spawn session ${sessionId}:`, error);
|
|
1785
|
+
this.callbacks.onError(sessionId, "SPAWN_FAILED", error instanceof Error ? error.message : String(error));
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
/**
|
|
1789
|
+
* Write data to PTY stdin
|
|
1790
|
+
*/
|
|
1791
|
+
writeStdin(sessionId, dataBase64) {
|
|
1792
|
+
const session = this.sessions.get(sessionId);
|
|
1793
|
+
if (!session) {
|
|
1794
|
+
logger.debug(`[TERMINAL] Session ${sessionId} not found for stdin write`);
|
|
1795
|
+
return;
|
|
1796
|
+
}
|
|
1797
|
+
try {
|
|
1798
|
+
const data = Buffer.from(dataBase64, "base64").toString("utf-8");
|
|
1799
|
+
session.pty.write(data);
|
|
1800
|
+
} catch (error) {
|
|
1801
|
+
logger.debug(`[TERMINAL] Failed to write to session ${sessionId}:`, error);
|
|
1802
|
+
this.callbacks.onError(sessionId, "WRITE_FAILED", error instanceof Error ? error.message : String(error));
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
/**
|
|
1806
|
+
* Resize PTY
|
|
1807
|
+
*/
|
|
1808
|
+
resize(sessionId, cols, rows) {
|
|
1809
|
+
const session = this.sessions.get(sessionId);
|
|
1810
|
+
if (!session) {
|
|
1811
|
+
logger.debug(`[TERMINAL] Session ${sessionId} not found for resize`);
|
|
1812
|
+
return;
|
|
1813
|
+
}
|
|
1814
|
+
try {
|
|
1815
|
+
session.pty.resize(cols, rows);
|
|
1816
|
+
session.cols = cols;
|
|
1817
|
+
session.rows = rows;
|
|
1818
|
+
logger.debug(`[TERMINAL] Session ${sessionId} resized to ${cols}x${rows}`);
|
|
1819
|
+
} catch (error) {
|
|
1820
|
+
logger.debug(`[TERMINAL] Failed to resize session ${sessionId}:`, error);
|
|
1821
|
+
this.callbacks.onError(sessionId, "RESIZE_FAILED", error instanceof Error ? error.message : String(error));
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
/**
|
|
1825
|
+
* Close PTY session
|
|
1826
|
+
*/
|
|
1827
|
+
close(sessionId) {
|
|
1828
|
+
const session = this.sessions.get(sessionId);
|
|
1829
|
+
if (!session) {
|
|
1830
|
+
logger.debug(`[TERMINAL] Session ${sessionId} not found for close`);
|
|
1831
|
+
return;
|
|
1832
|
+
}
|
|
1833
|
+
try {
|
|
1834
|
+
session.pty.kill();
|
|
1835
|
+
this.sessions.delete(sessionId);
|
|
1836
|
+
logger.debug(`[TERMINAL] Session ${sessionId} closed`);
|
|
1837
|
+
} catch (error) {
|
|
1838
|
+
logger.debug(`[TERMINAL] Failed to close session ${sessionId}:`, error);
|
|
1839
|
+
this.callbacks.onError(sessionId, "CLOSE_FAILED", error instanceof Error ? error.message : String(error));
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
/**
|
|
1843
|
+
* Close all sessions (cleanup on shutdown)
|
|
1844
|
+
*/
|
|
1845
|
+
closeAll() {
|
|
1846
|
+
logger.debug(`[TERMINAL] Closing all ${this.sessions.size} sessions`);
|
|
1847
|
+
for (const sessionId of this.sessions.keys()) {
|
|
1848
|
+
this.close(sessionId);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
/**
|
|
1852
|
+
* Get session info
|
|
1853
|
+
*/
|
|
1854
|
+
getSession(sessionId) {
|
|
1855
|
+
return this.sessions.get(sessionId);
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* Get all active session IDs
|
|
1859
|
+
*/
|
|
1860
|
+
getActiveSessionIds() {
|
|
1861
|
+
return Array.from(this.sessions.keys());
|
|
1862
|
+
}
|
|
1863
|
+
}
|
|
1864
|
+
|
|
1865
|
+
class ApiMachineClient {
|
|
1866
|
+
constructor(token, secret, machine) {
|
|
1867
|
+
this.token = token;
|
|
1868
|
+
this.secret = secret;
|
|
1869
|
+
this.machine = machine;
|
|
1870
|
+
this.rpcHandlerManager = new RpcHandlerManager({
|
|
1871
|
+
scopePrefix: this.machine.id,
|
|
1872
|
+
secret: this.secret,
|
|
1873
|
+
logger: (msg, data) => logger.debug(msg, data)
|
|
1874
|
+
});
|
|
1875
|
+
registerCommonHandlers(this.rpcHandlerManager);
|
|
1876
|
+
this.terminalManager = new TerminalManager({
|
|
1877
|
+
onOpen: (sessionId) => {
|
|
1878
|
+
if (this.socket?.connected) {
|
|
1879
|
+
this.socket.emit("terminal-open", { sessionId });
|
|
1880
|
+
}
|
|
1881
|
+
},
|
|
1882
|
+
onStdout: (sessionId, dataBase64) => {
|
|
1883
|
+
if (this.socket?.connected) {
|
|
1884
|
+
this.socket.emit("terminal-stdout", { sessionId, dataBase64 });
|
|
1885
|
+
}
|
|
1886
|
+
},
|
|
1887
|
+
onExit: (sessionId, code, signal) => {
|
|
1888
|
+
if (this.socket?.connected) {
|
|
1889
|
+
const signalStr = signal !== void 0 ? String(signal) : void 0;
|
|
1890
|
+
this.socket.emit("terminal-exit", { sessionId, code, signal: signalStr });
|
|
1891
|
+
}
|
|
1892
|
+
},
|
|
1893
|
+
onError: (sessionId, code, message) => {
|
|
1894
|
+
if (this.socket?.connected) {
|
|
1895
|
+
this.socket.emit("terminal-error", { sessionId, code, message });
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
});
|
|
1899
|
+
}
|
|
1900
|
+
socket;
|
|
1901
|
+
keepAliveInterval = null;
|
|
1902
|
+
rpcHandlerManager;
|
|
1903
|
+
terminalManager;
|
|
1904
|
+
setRPCHandlers({
|
|
1905
|
+
spawnSession,
|
|
1906
|
+
stopSession,
|
|
1907
|
+
requestShutdown
|
|
1908
|
+
}) {
|
|
1909
|
+
this.rpcHandlerManager.registerHandler("spawn-happy-session", async (params) => {
|
|
1910
|
+
const { directory, sessionId, machineId, approvedNewDirectoryCreation } = params || {};
|
|
1911
|
+
if (!directory) {
|
|
1912
|
+
throw new Error("Directory is required");
|
|
1913
|
+
}
|
|
1914
|
+
const result = await spawnSession({ directory, sessionId, machineId, approvedNewDirectoryCreation });
|
|
1915
|
+
switch (result.type) {
|
|
1916
|
+
case "success":
|
|
1917
|
+
logger.debug(`[API MACHINE] Spawned session ${result.sessionId}`);
|
|
1918
|
+
return { type: "success", sessionId: result.sessionId };
|
|
1919
|
+
case "requestToApproveDirectoryCreation":
|
|
1920
|
+
logger.debug(`[API MACHINE] Requesting directory creation approval for: ${result.directory}`);
|
|
1921
|
+
return { type: "requestToApproveDirectoryCreation", directory: result.directory };
|
|
1922
|
+
case "error":
|
|
1923
|
+
throw new Error(result.errorMessage);
|
|
1924
|
+
}
|
|
1925
|
+
});
|
|
1926
|
+
this.rpcHandlerManager.registerHandler("stop-session", (params) => {
|
|
1927
|
+
const { sessionId } = params || {};
|
|
1928
|
+
if (!sessionId) {
|
|
1929
|
+
throw new Error("Session ID is required");
|
|
1930
|
+
}
|
|
1931
|
+
const success = stopSession(sessionId);
|
|
1932
|
+
if (!success) {
|
|
1933
|
+
throw new Error("Session not found or failed to stop");
|
|
1934
|
+
}
|
|
1935
|
+
logger.debug(`[API MACHINE] Stopped session ${sessionId}`);
|
|
1936
|
+
return { message: "Session stopped" };
|
|
1937
|
+
});
|
|
1938
|
+
this.rpcHandlerManager.registerHandler("stop-daemon", () => {
|
|
1939
|
+
logger.debug("[API MACHINE] Received stop-daemon RPC request");
|
|
1940
|
+
setTimeout(() => {
|
|
1941
|
+
logger.debug("[API MACHINE] Initiating daemon shutdown from RPC");
|
|
1942
|
+
requestShutdown();
|
|
1943
|
+
}, 100);
|
|
1944
|
+
return { message: "Daemon stop request acknowledged, starting shutdown sequence..." };
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Update machine metadata
|
|
1949
|
+
* Currently unused, changes from the mobile client are more likely
|
|
1950
|
+
* for example to set a custom name.
|
|
1951
|
+
*/
|
|
1952
|
+
async updateMachineMetadata(handler) {
|
|
1953
|
+
await backoff(async () => {
|
|
1954
|
+
const updated = handler(this.machine.metadata);
|
|
1955
|
+
const answer = await this.socket.emitWithAck("machine-update-metadata", {
|
|
1956
|
+
machineId: this.machine.id,
|
|
1957
|
+
metadata: encodeBase64(encrypt(updated, this.secret)),
|
|
1958
|
+
expectedVersion: this.machine.metadataVersion
|
|
1959
|
+
});
|
|
1960
|
+
if (answer.result === "success") {
|
|
1961
|
+
this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
1962
|
+
this.machine.metadataVersion = answer.version;
|
|
1963
|
+
logger.debug("[API MACHINE] Metadata updated successfully");
|
|
1964
|
+
} else if (answer.result === "version-mismatch") {
|
|
1965
|
+
if (answer.version > this.machine.metadataVersion) {
|
|
1966
|
+
this.machine.metadataVersion = answer.version;
|
|
1967
|
+
this.machine.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
1968
|
+
}
|
|
1969
|
+
throw new Error("Metadata version mismatch");
|
|
1970
|
+
}
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
/**
|
|
1974
|
+
* Update daemon state (runtime info) - similar to session updateAgentState
|
|
1975
|
+
* Simplified without lock - relies on backoff for retry
|
|
1976
|
+
*/
|
|
1977
|
+
async updateDaemonState(handler) {
|
|
1978
|
+
await backoff(async () => {
|
|
1979
|
+
const updated = handler(this.machine.daemonState);
|
|
1980
|
+
const answer = await this.socket.emitWithAck("machine-update-state", {
|
|
1981
|
+
machineId: this.machine.id,
|
|
1982
|
+
daemonState: encodeBase64(encrypt(updated, this.secret)),
|
|
1983
|
+
expectedVersion: this.machine.daemonStateVersion
|
|
1984
|
+
});
|
|
1985
|
+
if (answer.result === "success") {
|
|
1986
|
+
this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
|
|
1987
|
+
this.machine.daemonStateVersion = answer.version;
|
|
1988
|
+
logger.debug("[API MACHINE] Daemon state updated successfully");
|
|
1989
|
+
} else if (answer.result === "version-mismatch") {
|
|
1990
|
+
if (answer.version > this.machine.daemonStateVersion) {
|
|
1991
|
+
this.machine.daemonStateVersion = answer.version;
|
|
1992
|
+
this.machine.daemonState = decrypt(decodeBase64(answer.daemonState), this.secret);
|
|
1993
|
+
}
|
|
1994
|
+
throw new Error("Daemon state version mismatch");
|
|
1995
|
+
}
|
|
1996
|
+
});
|
|
1997
|
+
}
|
|
1998
|
+
connect() {
|
|
1999
|
+
const serverUrl = configuration.serverUrl.replace(/^http/, "ws");
|
|
2000
|
+
logger.debug(`[API MACHINE] Connecting to ${serverUrl}`);
|
|
2001
|
+
this.socket = io(serverUrl, {
|
|
2002
|
+
transports: ["websocket"],
|
|
2003
|
+
auth: {
|
|
2004
|
+
token: this.token,
|
|
2005
|
+
clientType: "machine-scoped",
|
|
2006
|
+
machineId: this.machine.id
|
|
2007
|
+
},
|
|
2008
|
+
path: "/v1/updates",
|
|
2009
|
+
reconnection: true,
|
|
2010
|
+
reconnectionDelay: 1e3,
|
|
2011
|
+
reconnectionDelayMax: 5e3
|
|
2012
|
+
});
|
|
2013
|
+
this.socket.on("connect", () => {
|
|
2014
|
+
logger.debug("[API MACHINE] Connected to server");
|
|
2015
|
+
this.updateDaemonState((state) => ({
|
|
2016
|
+
...state,
|
|
2017
|
+
status: "running",
|
|
2018
|
+
pid: process.pid,
|
|
2019
|
+
httpPort: this.machine.daemonState?.httpPort,
|
|
2020
|
+
startedAt: Date.now()
|
|
2021
|
+
}));
|
|
2022
|
+
this.rpcHandlerManager.onSocketConnect(this.socket);
|
|
2023
|
+
this.startKeepAlive();
|
|
2024
|
+
this.setupTerminalListeners();
|
|
2025
|
+
});
|
|
2026
|
+
this.socket.on("disconnect", () => {
|
|
2027
|
+
logger.debug("[API MACHINE] Disconnected from server");
|
|
2028
|
+
this.rpcHandlerManager.onSocketDisconnect();
|
|
2029
|
+
this.stopKeepAlive();
|
|
2030
|
+
this.terminalManager.closeAll();
|
|
2031
|
+
});
|
|
2032
|
+
this.socket.on("rpc-request", async (data, callback) => {
|
|
2033
|
+
logger.debugLargeJson(`[API MACHINE] Received RPC request:`, data);
|
|
2034
|
+
callback(await this.rpcHandlerManager.handleRequest(data));
|
|
2035
|
+
});
|
|
2036
|
+
this.socket.on("update", (data) => {
|
|
2037
|
+
if (data.body.t === "update-machine" && data.body.machineId === this.machine.id) {
|
|
2038
|
+
const update = data.body;
|
|
2039
|
+
if (update.metadata) {
|
|
2040
|
+
logger.debug("[API MACHINE] Received external metadata update");
|
|
2041
|
+
this.machine.metadata = decrypt(decodeBase64(update.metadata.value), this.secret);
|
|
2042
|
+
this.machine.metadataVersion = update.metadata.version;
|
|
2043
|
+
}
|
|
2044
|
+
if (update.daemonState) {
|
|
2045
|
+
logger.debug("[API MACHINE] Received external daemon state update");
|
|
2046
|
+
this.machine.daemonState = decrypt(decodeBase64(update.daemonState.value), this.secret);
|
|
2047
|
+
this.machine.daemonStateVersion = update.daemonState.version;
|
|
2048
|
+
}
|
|
2049
|
+
} else {
|
|
2050
|
+
logger.debug(`[API MACHINE] Received unknown update type: ${data.body.t}`);
|
|
2051
|
+
}
|
|
2052
|
+
});
|
|
2053
|
+
this.socket.on("connect_error", (error) => {
|
|
2054
|
+
logger.debug(`[API MACHINE] Connection error: ${error.message}`);
|
|
2055
|
+
});
|
|
2056
|
+
this.socket.io.on("error", (error) => {
|
|
2057
|
+
logger.debug("[API MACHINE] Socket error:", error);
|
|
2058
|
+
});
|
|
2059
|
+
}
|
|
2060
|
+
/**
|
|
2061
|
+
* Set up terminal event listeners from server
|
|
2062
|
+
*/
|
|
2063
|
+
setupTerminalListeners() {
|
|
2064
|
+
this.socket.on("terminal-spawn", async (data) => {
|
|
2065
|
+
try {
|
|
2066
|
+
const settings = await readSettings();
|
|
2067
|
+
if (settings.terminalEnabled === false) {
|
|
2068
|
+
logger.debug(`[API MACHINE] Terminal is disabled; rejecting spawn ${data.sessionId}`);
|
|
2069
|
+
this.socket.emit("terminal-error", { sessionId: data.sessionId, code: "DISABLED", message: "Terminal is disabled on this machine" });
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
} catch {
|
|
2073
|
+
}
|
|
2074
|
+
logger.debug(`[API MACHINE] Received terminal-spawn request: ${data.sessionId}`);
|
|
2075
|
+
this.terminalManager.spawn(data.sessionId, data.cols || 80, data.rows || 24);
|
|
2076
|
+
});
|
|
2077
|
+
this.socket.on("terminal-stdin", (data) => {
|
|
2078
|
+
this.terminalManager.writeStdin(data.sessionId, data.dataBase64);
|
|
2079
|
+
});
|
|
2080
|
+
this.socket.on("terminal-resize", (data) => {
|
|
2081
|
+
logger.debug(`[API MACHINE] Received terminal-resize request: ${data.sessionId} to ${data.cols}x${data.rows}`);
|
|
2082
|
+
this.terminalManager.resize(data.sessionId, data.cols, data.rows);
|
|
2083
|
+
});
|
|
2084
|
+
this.socket.on("terminal-close", (data) => {
|
|
2085
|
+
logger.debug(`[API MACHINE] Received terminal-close request: ${data.sessionId}`);
|
|
2086
|
+
this.terminalManager.close(data.sessionId);
|
|
2087
|
+
});
|
|
2088
|
+
}
|
|
2089
|
+
startKeepAlive() {
|
|
2090
|
+
this.stopKeepAlive();
|
|
2091
|
+
this.keepAliveInterval = setInterval(() => {
|
|
2092
|
+
const payload = {
|
|
2093
|
+
machineId: this.machine.id,
|
|
2094
|
+
time: Date.now()
|
|
2095
|
+
};
|
|
2096
|
+
if (process.env.DEBUG) {
|
|
2097
|
+
logger.debugLargeJson(`[API MACHINE] Emitting machine-alive`, payload);
|
|
2098
|
+
}
|
|
2099
|
+
this.socket.emit("machine-alive", payload);
|
|
2100
|
+
}, 2e4);
|
|
2101
|
+
logger.debug("[API MACHINE] Keep-alive started (20s interval)");
|
|
2102
|
+
}
|
|
2103
|
+
stopKeepAlive() {
|
|
2104
|
+
if (this.keepAliveInterval) {
|
|
2105
|
+
clearInterval(this.keepAliveInterval);
|
|
2106
|
+
this.keepAliveInterval = null;
|
|
2107
|
+
logger.debug("[API MACHINE] Keep-alive stopped");
|
|
2108
|
+
}
|
|
2109
|
+
}
|
|
2110
|
+
shutdown() {
|
|
2111
|
+
logger.debug("[API MACHINE] Shutting down");
|
|
2112
|
+
this.stopKeepAlive();
|
|
2113
|
+
this.terminalManager.closeAll();
|
|
2114
|
+
if (this.socket) {
|
|
2115
|
+
this.socket.close();
|
|
2116
|
+
logger.debug("[API MACHINE] Socket closed");
|
|
2117
|
+
}
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
class PushNotificationClient {
|
|
2122
|
+
token;
|
|
2123
|
+
baseUrl;
|
|
2124
|
+
expo;
|
|
2125
|
+
constructor(token, baseUrl = "https://api.cluster-fluster.com") {
|
|
2126
|
+
this.token = token;
|
|
2127
|
+
this.baseUrl = baseUrl;
|
|
2128
|
+
this.expo = new Expo();
|
|
2129
|
+
}
|
|
2130
|
+
/**
|
|
2131
|
+
* Fetch all push tokens for the authenticated user
|
|
2132
|
+
*/
|
|
2133
|
+
async fetchPushTokens() {
|
|
2134
|
+
try {
|
|
2135
|
+
const response = await axios.get(
|
|
2136
|
+
`${this.baseUrl}/v1/push-tokens`,
|
|
2137
|
+
{
|
|
2138
|
+
headers: {
|
|
2139
|
+
"Authorization": `Bearer ${this.token}`,
|
|
2140
|
+
"Content-Type": "application/json"
|
|
2141
|
+
}
|
|
2142
|
+
}
|
|
2143
|
+
);
|
|
2144
|
+
logger.debug(`Fetched ${response.data.tokens.length} push tokens`);
|
|
2145
|
+
response.data.tokens.forEach((token, index) => {
|
|
2146
|
+
logger.debug(`[PUSH] Token ${index + 1}: id=${token.id}, token=${token.token}, created=${new Date(token.createdAt).toISOString()}, updated=${new Date(token.updatedAt).toISOString()}`);
|
|
2147
|
+
});
|
|
2148
|
+
return response.data.tokens;
|
|
2149
|
+
} catch (error) {
|
|
2150
|
+
logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
|
|
2151
|
+
throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2152
|
+
}
|
|
2153
|
+
}
|
|
2154
|
+
/**
|
|
2155
|
+
* Send push notification via Expo Push API with retry
|
|
2156
|
+
* @param messages - Array of push messages to send
|
|
2157
|
+
*/
|
|
2158
|
+
async sendPushNotifications(messages) {
|
|
2159
|
+
logger.debug(`Sending ${messages.length} push notifications`);
|
|
2160
|
+
const validMessages = messages.filter((message) => {
|
|
2161
|
+
if (Array.isArray(message.to)) {
|
|
2162
|
+
return message.to.every((token) => Expo.isExpoPushToken(token));
|
|
2163
|
+
}
|
|
2164
|
+
return Expo.isExpoPushToken(message.to);
|
|
2165
|
+
});
|
|
2166
|
+
if (validMessages.length === 0) {
|
|
2167
|
+
logger.debug("No valid Expo push tokens found");
|
|
2168
|
+
return;
|
|
2169
|
+
}
|
|
2170
|
+
const chunks = this.expo.chunkPushNotifications(validMessages);
|
|
2171
|
+
for (const chunk of chunks) {
|
|
2172
|
+
const startTime = Date.now();
|
|
2173
|
+
const timeout = 3e5;
|
|
2174
|
+
let attempt = 0;
|
|
2175
|
+
while (true) {
|
|
2176
|
+
try {
|
|
2177
|
+
const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
|
|
2178
|
+
const errors = ticketChunk.filter((ticket) => ticket.status === "error");
|
|
2179
|
+
if (errors.length > 0) {
|
|
2180
|
+
const errorDetails = errors.map((e) => ({ message: e.message, details: e.details }));
|
|
2181
|
+
logger.debug("[PUSH] Some notifications failed:", errorDetails);
|
|
2182
|
+
}
|
|
2183
|
+
if (errors.length === ticketChunk.length) {
|
|
2184
|
+
throw new Error("All push notifications in chunk failed");
|
|
2185
|
+
}
|
|
2186
|
+
break;
|
|
2187
|
+
} catch (error) {
|
|
2188
|
+
const elapsed = Date.now() - startTime;
|
|
2189
|
+
if (elapsed >= timeout) {
|
|
2190
|
+
logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
|
|
2191
|
+
break;
|
|
2192
|
+
}
|
|
2193
|
+
attempt++;
|
|
2194
|
+
const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
2195
|
+
const remainingTime = timeout - elapsed;
|
|
2196
|
+
const waitTime = Math.min(delay, remainingTime);
|
|
2197
|
+
if (waitTime > 0) {
|
|
2198
|
+
logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
|
|
2199
|
+
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
2200
|
+
}
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
}
|
|
2204
|
+
logger.debug(`Push notifications sent successfully`);
|
|
2205
|
+
}
|
|
2206
|
+
/**
|
|
2207
|
+
* Send a push notification to all registered devices for the user
|
|
2208
|
+
* @param title - Notification title
|
|
2209
|
+
* @param body - Notification body
|
|
2210
|
+
* @param data - Additional data to send with the notification
|
|
2211
|
+
*/
|
|
2212
|
+
sendToAllDevices(title, body, data) {
|
|
2213
|
+
logger.debug(`[PUSH] sendToAllDevices called with title: "${title}", body: "${body}"`);
|
|
2214
|
+
(async () => {
|
|
2215
|
+
try {
|
|
2216
|
+
logger.debug("[PUSH] Fetching push tokens...");
|
|
2217
|
+
const tokens = await this.fetchPushTokens();
|
|
2218
|
+
logger.debug(`[PUSH] Fetched ${tokens.length} push tokens`);
|
|
2219
|
+
tokens.forEach((token, index) => {
|
|
2220
|
+
logger.debug(`[PUSH] Using token ${index + 1}: id=${token.id}, token=${token.token}`);
|
|
2221
|
+
});
|
|
2222
|
+
if (tokens.length === 0) {
|
|
2223
|
+
logger.debug("No push tokens found for user");
|
|
2224
|
+
return;
|
|
2225
|
+
}
|
|
2226
|
+
const messages = tokens.map((token, index) => {
|
|
2227
|
+
logger.debug(`[PUSH] Creating message ${index + 1} for token: ${token.token}`);
|
|
2228
|
+
return {
|
|
2229
|
+
to: token.token,
|
|
2230
|
+
title,
|
|
2231
|
+
body,
|
|
2232
|
+
data,
|
|
2233
|
+
sound: "default",
|
|
2234
|
+
priority: "high"
|
|
2235
|
+
};
|
|
2236
|
+
});
|
|
2237
|
+
logger.debug(`[PUSH] Sending ${messages.length} push notifications...`);
|
|
2238
|
+
await this.sendPushNotifications(messages);
|
|
2239
|
+
logger.debug("[PUSH] Push notifications sent successfully");
|
|
2240
|
+
} catch (error) {
|
|
2241
|
+
logger.debug("[PUSH] Error sending to all devices:", error);
|
|
2242
|
+
}
|
|
2243
|
+
})();
|
|
2244
|
+
}
|
|
2245
|
+
}
|
|
2246
|
+
|
|
2247
|
+
class ApiClient {
|
|
2248
|
+
token;
|
|
2249
|
+
secret;
|
|
2250
|
+
pushClient;
|
|
2251
|
+
constructor(token, secret) {
|
|
2252
|
+
this.token = token;
|
|
2253
|
+
this.secret = secret;
|
|
2254
|
+
this.pushClient = new PushNotificationClient(token);
|
|
2255
|
+
}
|
|
2256
|
+
/**
|
|
2257
|
+
* Create a new session or load existing one with the given tag
|
|
2258
|
+
*/
|
|
2259
|
+
async getOrCreateSession(opts) {
|
|
2260
|
+
try {
|
|
2261
|
+
const response = await axios.post(
|
|
2262
|
+
`${configuration.serverUrl}/v1/sessions`,
|
|
2263
|
+
{
|
|
2264
|
+
tag: opts.tag,
|
|
2265
|
+
metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
|
|
2266
|
+
agentState: opts.state ? encodeBase64(encrypt(opts.state, this.secret)) : null
|
|
2267
|
+
},
|
|
2268
|
+
{
|
|
2269
|
+
headers: {
|
|
2270
|
+
"Authorization": `Bearer ${this.token}`,
|
|
2271
|
+
"Content-Type": "application/json"
|
|
2272
|
+
},
|
|
2273
|
+
timeout: 5e3
|
|
2274
|
+
// 5 second timeout
|
|
2275
|
+
}
|
|
2276
|
+
);
|
|
2277
|
+
logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
|
|
2278
|
+
let raw = response.data.session;
|
|
2279
|
+
let session = {
|
|
2280
|
+
id: raw.id,
|
|
2281
|
+
createdAt: raw.createdAt,
|
|
2282
|
+
updatedAt: raw.updatedAt,
|
|
2283
|
+
seq: raw.seq,
|
|
2284
|
+
metadata: decrypt(decodeBase64(raw.metadata), this.secret),
|
|
2285
|
+
metadataVersion: raw.metadataVersion,
|
|
2286
|
+
agentState: raw.agentState ? decrypt(decodeBase64(raw.agentState), this.secret) : null,
|
|
2287
|
+
agentStateVersion: raw.agentStateVersion
|
|
2288
|
+
};
|
|
2289
|
+
return session;
|
|
2290
|
+
} catch (error) {
|
|
2291
|
+
logger.debug("[API] [ERROR] Failed to get or create session:", error);
|
|
2292
|
+
throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Get machine by ID from the server
|
|
2297
|
+
* Returns the current machine state from the server with decrypted metadata and daemonState
|
|
2298
|
+
*/
|
|
2299
|
+
async getMachine(machineId) {
|
|
2300
|
+
const response = await axios.get(`${configuration.serverUrl}/v1/machines/${machineId}`, {
|
|
2301
|
+
headers: {
|
|
2302
|
+
"Authorization": `Bearer ${this.token}`,
|
|
2303
|
+
"Content-Type": "application/json"
|
|
2304
|
+
},
|
|
2305
|
+
timeout: 2e3
|
|
2306
|
+
});
|
|
2307
|
+
const raw = response.data.machine;
|
|
2308
|
+
if (!raw) {
|
|
2309
|
+
return null;
|
|
2310
|
+
}
|
|
2311
|
+
logger.debug(`[API] Machine ${machineId} fetched from server`);
|
|
2312
|
+
const machine = {
|
|
2313
|
+
id: raw.id,
|
|
2314
|
+
metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
|
|
2315
|
+
metadataVersion: raw.metadataVersion || 0,
|
|
2316
|
+
daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
|
|
2317
|
+
daemonStateVersion: raw.daemonStateVersion || 0,
|
|
2318
|
+
active: raw.active,
|
|
2319
|
+
activeAt: raw.activeAt,
|
|
2320
|
+
createdAt: raw.createdAt,
|
|
2321
|
+
updatedAt: raw.updatedAt
|
|
2322
|
+
};
|
|
2323
|
+
return machine;
|
|
2324
|
+
}
|
|
2325
|
+
/**
|
|
2326
|
+
* Register or update machine with the server
|
|
2327
|
+
* Returns the current machine state from the server with decrypted metadata and daemonState
|
|
2328
|
+
*/
|
|
2329
|
+
async getOrCreateMachine(opts) {
|
|
2330
|
+
const response = await axios.post(
|
|
2331
|
+
`${configuration.serverUrl}/v1/machines`,
|
|
2332
|
+
{
|
|
2333
|
+
id: opts.machineId,
|
|
2334
|
+
metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
|
|
2335
|
+
daemonState: opts.daemonState ? encodeBase64(encrypt(opts.daemonState, this.secret)) : void 0
|
|
2336
|
+
},
|
|
2337
|
+
{
|
|
2338
|
+
headers: {
|
|
2339
|
+
"Authorization": `Bearer ${this.token}`,
|
|
2340
|
+
"Content-Type": "application/json"
|
|
2341
|
+
},
|
|
2342
|
+
timeout: 5e3
|
|
2343
|
+
}
|
|
2344
|
+
);
|
|
2345
|
+
if (response.status !== 200) {
|
|
2346
|
+
console.error(chalk.red(`[API] Failed to create machine: ${response.statusText}`));
|
|
2347
|
+
console.log(chalk.yellow(`[API] Failed to create machine: ${response.statusText}, most likely you have re-authenticated, but you still have a machine associated with the old account. Now we are trying to re-associate the machine with the new account. That is not allowed. Please run 'happy doctor clean' to clean up your happy state, and try your original command again. Please create an issue on github if this is causing you problems. We apologize for the inconvenience.`));
|
|
2348
|
+
process.exit(1);
|
|
2349
|
+
}
|
|
2350
|
+
const raw = response.data.machine;
|
|
2351
|
+
logger.debug(`[API] Machine ${opts.machineId} registered/updated with server`);
|
|
2352
|
+
const machine = {
|
|
2353
|
+
id: raw.id,
|
|
2354
|
+
metadata: raw.metadata ? decrypt(decodeBase64(raw.metadata), this.secret) : null,
|
|
2355
|
+
metadataVersion: raw.metadataVersion || 0,
|
|
2356
|
+
daemonState: raw.daemonState ? decrypt(decodeBase64(raw.daemonState), this.secret) : null,
|
|
2357
|
+
daemonStateVersion: raw.daemonStateVersion || 0,
|
|
2358
|
+
active: raw.active,
|
|
2359
|
+
activeAt: raw.activeAt,
|
|
2360
|
+
createdAt: raw.createdAt,
|
|
2361
|
+
updatedAt: raw.updatedAt
|
|
2362
|
+
};
|
|
2363
|
+
return machine;
|
|
2364
|
+
}
|
|
2365
|
+
sessionSyncClient(session) {
|
|
2366
|
+
return new ApiSessionClient(this.token, this.secret, session);
|
|
2367
|
+
}
|
|
2368
|
+
machineSyncClient(machine) {
|
|
2369
|
+
return new ApiMachineClient(this.token, this.secret, machine);
|
|
2370
|
+
}
|
|
2371
|
+
push() {
|
|
2372
|
+
return this.pushClient;
|
|
2373
|
+
}
|
|
2374
|
+
/**
|
|
2375
|
+
* Register a vendor API token with the server
|
|
2376
|
+
* The token is sent as a JSON string - server handles encryption
|
|
2377
|
+
*/
|
|
2378
|
+
async registerVendorToken(vendor, apiKey) {
|
|
2379
|
+
try {
|
|
2380
|
+
const response = await axios.post(
|
|
2381
|
+
`${configuration.serverUrl}/v1/connect/${vendor}/register`,
|
|
2382
|
+
{
|
|
2383
|
+
token: JSON.stringify(apiKey)
|
|
2384
|
+
},
|
|
2385
|
+
{
|
|
2386
|
+
headers: {
|
|
2387
|
+
"Authorization": `Bearer ${this.token}`,
|
|
2388
|
+
"Content-Type": "application/json"
|
|
2389
|
+
},
|
|
2390
|
+
timeout: 5e3
|
|
2391
|
+
}
|
|
2392
|
+
);
|
|
2393
|
+
if (response.status !== 200 && response.status !== 201) {
|
|
2394
|
+
throw new Error(`Server returned status ${response.status}`);
|
|
2395
|
+
}
|
|
2396
|
+
logger.debug(`[API] Vendor token for ${vendor} registered successfully`);
|
|
2397
|
+
} catch (error) {
|
|
2398
|
+
logger.debug(`[API] [ERROR] Failed to register vendor token:`, error);
|
|
2399
|
+
throw new Error(`Failed to register vendor token: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
2400
|
+
}
|
|
2401
|
+
}
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2404
|
+
const UsageSchema = z$1.object({
|
|
2405
|
+
input_tokens: z$1.number().int().nonnegative(),
|
|
2406
|
+
cache_creation_input_tokens: z$1.number().int().nonnegative().optional(),
|
|
2407
|
+
cache_read_input_tokens: z$1.number().int().nonnegative().optional(),
|
|
2408
|
+
output_tokens: z$1.number().int().nonnegative(),
|
|
2409
|
+
service_tier: z$1.string().optional()
|
|
2410
|
+
}).passthrough();
|
|
2411
|
+
const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
|
|
2412
|
+
// User message - validates uuid and message.content
|
|
2413
|
+
z$1.object({
|
|
2414
|
+
type: z$1.literal("user"),
|
|
2415
|
+
isSidechain: z$1.boolean().optional(),
|
|
2416
|
+
isMeta: z$1.boolean().optional(),
|
|
2417
|
+
uuid: z$1.string(),
|
|
2418
|
+
// Used in getMessageKey()
|
|
2419
|
+
message: z$1.object({
|
|
2420
|
+
content: z$1.union([z$1.string(), z$1.any()])
|
|
2421
|
+
// Used in sessionScanner.ts
|
|
2422
|
+
}).passthrough()
|
|
2423
|
+
}).passthrough(),
|
|
2424
|
+
// Assistant message - validates message object with usage and content
|
|
2425
|
+
z$1.object({
|
|
2426
|
+
uuid: z$1.string(),
|
|
2427
|
+
type: z$1.literal("assistant"),
|
|
2428
|
+
message: z$1.object({
|
|
2429
|
+
// Entire message used in getMessageKey()
|
|
2430
|
+
usage: UsageSchema.optional(),
|
|
2431
|
+
// Used in apiSession.ts
|
|
2432
|
+
content: z$1.any()
|
|
2433
|
+
// Used in tests
|
|
2434
|
+
}).passthrough()
|
|
2435
|
+
}).passthrough(),
|
|
2436
|
+
// Summary message - validates summary and leafUuid
|
|
2437
|
+
z$1.object({
|
|
2438
|
+
type: z$1.literal("summary"),
|
|
2439
|
+
summary: z$1.string(),
|
|
2440
|
+
// Used in apiSession.ts
|
|
2441
|
+
leafUuid: z$1.string()
|
|
2442
|
+
// Used in getMessageKey()
|
|
2443
|
+
}).passthrough(),
|
|
2444
|
+
// System message - validates uuid
|
|
2445
|
+
z$1.object({
|
|
2446
|
+
type: z$1.literal("system"),
|
|
2447
|
+
uuid: z$1.string()
|
|
2448
|
+
// Used in getMessageKey()
|
|
2449
|
+
}).passthrough()
|
|
2450
|
+
]);
|
|
2451
|
+
|
|
2452
|
+
export { ApiClient as A, RawJSONLinesSchema as R, ApiSessionClient as a, packageJson as b, configuration as c, backoff as d, delay as e, AsyncLock as f, readDaemonState as g, clearDaemonState as h, readCredentials as i, encodeBase64 as j, updateSettings as k, logger as l, encodeBase64Url as m, decodeBase64 as n, writeCredentials as o, projectPath as p, acquireDaemonLock as q, readSettings as r, writeDaemonState as s, releaseDaemonLock as t, updateActivityTimestamp as u, clearCredentials as v, writeHeadlessJson as w, clearMachineId as x, getLatestDaemonLog as y };
|