happy-coder 0.1.7 → 0.1.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +950 -938
- package/dist/index.mjs +880 -868
- package/dist/lib.cjs +32 -0
- package/dist/lib.d.cts +527 -0
- package/dist/lib.d.mts +527 -0
- package/dist/lib.mjs +14 -0
- package/dist/types-B2JzqUiU.cjs +831 -0
- package/dist/types-DnQGY77F.mjs +818 -0
- package/package.json +25 -10
- package/dist/auth/auth.d.ts +0 -38
- package/dist/auth/auth.js +0 -76
- package/dist/auth/auth.test.d.ts +0 -7
- package/dist/auth/auth.test.js +0 -96
- package/dist/auth/crypto.d.ts +0 -25
- package/dist/auth/crypto.js +0 -36
- package/dist/claude/claude.d.ts +0 -54
- package/dist/claude/claude.js +0 -170
- package/dist/claude/claude.test.d.ts +0 -7
- package/dist/claude/claude.test.js +0 -130
- package/dist/claude/types.d.ts +0 -37
- package/dist/claude/types.js +0 -7
- package/dist/commands/start.d.ts +0 -38
- package/dist/commands/start.js +0 -161
- package/dist/commands/start.test.d.ts +0 -7
- package/dist/commands/start.test.js +0 -307
- package/dist/handlers/message-handler.d.ts +0 -65
- package/dist/handlers/message-handler.js +0 -187
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/session/service.d.ts +0 -27
- package/dist/session/service.js +0 -93
- package/dist/session/service.test.d.ts +0 -7
- package/dist/session/service.test.js +0 -71
- package/dist/session/types.d.ts +0 -44
- package/dist/session/types.js +0 -4
- package/dist/socket/client.d.ts +0 -50
- package/dist/socket/client.js +0 -136
- package/dist/socket/client.test.d.ts +0 -7
- package/dist/socket/client.test.js +0 -74
- package/dist/socket/types.d.ts +0 -80
- package/dist/socket/types.js +0 -12
- package/dist/utils/config.d.ts +0 -22
- package/dist/utils/config.js +0 -23
- package/dist/utils/logger.d.ts +0 -26
- package/dist/utils/logger.js +0 -60
- package/dist/utils/paths.d.ts +0 -18
- package/dist/utils/paths.js +0 -24
- package/dist/utils/qrcode.d.ts +0 -19
- package/dist/utils/qrcode.js +0 -37
- package/dist/utils/qrcode.test.d.ts +0 -7
- package/dist/utils/qrcode.test.js +0 -14
package/dist/index.mjs
CHANGED
|
@@ -1,716 +1,34 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
import os, { homedir } from 'node:os';
|
|
5
|
-
import { join, resolve, dirname } from 'node:path';
|
|
6
|
-
import { mkdir, watch as watch$1, readFile, writeFile } from 'node:fs/promises';
|
|
7
|
-
import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
|
|
8
|
-
import { EventEmitter } from 'node:events';
|
|
9
|
-
import { io } from 'socket.io-client';
|
|
10
|
-
import * as z from 'zod';
|
|
11
|
-
import { z as z$1 } from 'zod';
|
|
12
|
-
import { randomBytes, randomUUID } from 'node:crypto';
|
|
13
|
-
import tweetnacl from 'tweetnacl';
|
|
14
|
-
import { Expo } from 'expo-server-sdk';
|
|
2
|
+
import { l as logger, d as backoff, R as RawJSONLinesSchema, A as ApiClient, c as configuration, e as encodeBase64, f as encodeBase64Url, g as decodeBase64, h as delay, j as encrypt, k as decrypt, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-DnQGY77F.mjs';
|
|
3
|
+
import { randomUUID, randomBytes } from 'node:crypto';
|
|
15
4
|
import { query, AbortError } from '@anthropic-ai/claude-code';
|
|
5
|
+
import { existsSync, readFileSync, mkdirSync, watch, rmSync } from 'node:fs';
|
|
6
|
+
import os, { homedir } from 'node:os';
|
|
7
|
+
import { resolve, join, dirname } from 'node:path';
|
|
16
8
|
import { spawn } from 'node:child_process';
|
|
17
9
|
import { createInterface } from 'node:readline';
|
|
18
|
-
import { fileURLToPath } from 'node:url';
|
|
10
|
+
import { fileURLToPath, URL as URL$1 } from 'node:url';
|
|
11
|
+
import { watch as watch$1, readFile, mkdir, writeFile as writeFile$1 } from 'node:fs/promises';
|
|
19
12
|
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
20
|
-
import { createServer } from 'node:http';
|
|
13
|
+
import { createServer, request as request$1 } from 'node:http';
|
|
21
14
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
|
|
15
|
+
import * as z from 'zod';
|
|
16
|
+
import { z as z$1 } from 'zod';
|
|
17
|
+
import { request } from 'node:https';
|
|
18
|
+
import net from 'node:net';
|
|
19
|
+
import { exec, spawn as spawn$1, execSync } from 'child_process';
|
|
20
|
+
import { promisify } from 'util';
|
|
21
|
+
import { readFile as readFile$1, stat, writeFile, readdir } from 'fs/promises';
|
|
22
|
+
import crypto, { createHash } from 'crypto';
|
|
23
|
+
import { join as join$1 } from 'path';
|
|
24
|
+
import tweetnacl from 'tweetnacl';
|
|
25
|
+
import axios from 'axios';
|
|
22
26
|
import qrcode from 'qrcode-terminal';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
logsDir;
|
|
29
|
-
settingsFile;
|
|
30
|
-
privateKeyFile;
|
|
31
|
-
constructor(location) {
|
|
32
|
-
this.serverUrl = process.env.HANDY_SERVER_URL || "https://handy-api.korshakov.org";
|
|
33
|
-
if (location === "local") {
|
|
34
|
-
this.happyDir = join(process.cwd(), ".happy");
|
|
35
|
-
} else {
|
|
36
|
-
this.happyDir = join(homedir(), ".happy");
|
|
37
|
-
}
|
|
38
|
-
this.logsDir = join(this.happyDir, "logs");
|
|
39
|
-
this.settingsFile = join(this.happyDir, "settings.json");
|
|
40
|
-
this.privateKeyFile = join(this.happyDir, "access.key");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
let configuration = void 0;
|
|
44
|
-
function initializeConfiguration(location) {
|
|
45
|
-
configuration = new Configuration(location);
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
async function getSessionLogPath() {
|
|
49
|
-
if (!existsSync(configuration.logsDir)) {
|
|
50
|
-
await mkdir(configuration.logsDir, { recursive: true });
|
|
51
|
-
}
|
|
52
|
-
const now = /* @__PURE__ */ new Date();
|
|
53
|
-
const timestamp = now.toLocaleString("sv-SE", {
|
|
54
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
55
|
-
year: "numeric",
|
|
56
|
-
month: "2-digit",
|
|
57
|
-
day: "2-digit",
|
|
58
|
-
hour: "2-digit",
|
|
59
|
-
minute: "2-digit",
|
|
60
|
-
second: "2-digit"
|
|
61
|
-
}).replace(/[: ]/g, "-").replace(/,/g, "");
|
|
62
|
-
return join(configuration.logsDir, `${timestamp}.log`);
|
|
63
|
-
}
|
|
64
|
-
class Logger {
|
|
65
|
-
constructor(logFilePathPromise = getSessionLogPath()) {
|
|
66
|
-
this.logFilePathPromise = logFilePathPromise;
|
|
67
|
-
}
|
|
68
|
-
// Use local timezone for simplicity of locating the logs,
|
|
69
|
-
// in practice you will not need absolute timestamps
|
|
70
|
-
localTimezoneTimestamp() {
|
|
71
|
-
return (/* @__PURE__ */ new Date()).toLocaleTimeString("en-US", {
|
|
72
|
-
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
73
|
-
hour12: false,
|
|
74
|
-
hour: "2-digit",
|
|
75
|
-
minute: "2-digit",
|
|
76
|
-
second: "2-digit",
|
|
77
|
-
fractionalSecondDigits: 3
|
|
78
|
-
});
|
|
79
|
-
}
|
|
80
|
-
debug(message, ...args) {
|
|
81
|
-
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, ...args);
|
|
82
|
-
}
|
|
83
|
-
debugLargeJson(message, object, maxStringLength = 100, maxArrayLength = 10) {
|
|
84
|
-
if (!process.env.DEBUG) {
|
|
85
|
-
this.debug(`In production, skipping message inspection`);
|
|
86
|
-
}
|
|
87
|
-
const truncateStrings = (obj) => {
|
|
88
|
-
if (typeof obj === "string") {
|
|
89
|
-
return obj.length > maxStringLength ? obj.substring(0, maxStringLength) + "... [truncated for logs]" : obj;
|
|
90
|
-
}
|
|
91
|
-
if (Array.isArray(obj)) {
|
|
92
|
-
const truncatedArray = obj.map((item) => truncateStrings(item)).slice(0, maxArrayLength);
|
|
93
|
-
if (obj.length > maxArrayLength) {
|
|
94
|
-
truncatedArray.push(`... [truncated array for logs up to ${maxArrayLength} items]`);
|
|
95
|
-
}
|
|
96
|
-
return truncatedArray;
|
|
97
|
-
}
|
|
98
|
-
if (obj && typeof obj === "object") {
|
|
99
|
-
const result = {};
|
|
100
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
101
|
-
if (key === "usage") {
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
104
|
-
result[key] = truncateStrings(value);
|
|
105
|
-
}
|
|
106
|
-
return result;
|
|
107
|
-
}
|
|
108
|
-
return obj;
|
|
109
|
-
};
|
|
110
|
-
const truncatedObject = truncateStrings(object);
|
|
111
|
-
const json = JSON.stringify(truncatedObject, null, 2);
|
|
112
|
-
this.logToFile(`[${this.localTimezoneTimestamp()}]`, message, "\n", json);
|
|
113
|
-
}
|
|
114
|
-
info(message, ...args) {
|
|
115
|
-
this.logToConsole("info", "", message, ...args);
|
|
116
|
-
this.debug(message, args);
|
|
117
|
-
}
|
|
118
|
-
logToConsole(level, prefix, message, ...args) {
|
|
119
|
-
switch (level) {
|
|
120
|
-
case "debug": {
|
|
121
|
-
console.log(chalk.gray(prefix), message, ...args);
|
|
122
|
-
break;
|
|
123
|
-
}
|
|
124
|
-
case "error": {
|
|
125
|
-
console.error(chalk.red(prefix), message, ...args);
|
|
126
|
-
break;
|
|
127
|
-
}
|
|
128
|
-
case "info": {
|
|
129
|
-
console.log(chalk.blue(prefix), message, ...args);
|
|
130
|
-
break;
|
|
131
|
-
}
|
|
132
|
-
case "warn": {
|
|
133
|
-
console.log(chalk.yellow(prefix), message, ...args);
|
|
134
|
-
break;
|
|
135
|
-
}
|
|
136
|
-
default: {
|
|
137
|
-
this.debug("Unknown log level:", level);
|
|
138
|
-
console.log(chalk.blue(prefix), message, ...args);
|
|
139
|
-
break;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
logToFile(prefix, message, ...args) {
|
|
144
|
-
const logLine = `${prefix} ${message} ${args.map(
|
|
145
|
-
(arg) => typeof arg === "string" ? arg : JSON.stringify(arg)
|
|
146
|
-
).join(" ")}
|
|
147
|
-
`;
|
|
148
|
-
this.logFilePathPromise.then((logFilePath) => {
|
|
149
|
-
try {
|
|
150
|
-
appendFileSync(logFilePath, logLine);
|
|
151
|
-
} catch (appendError) {
|
|
152
|
-
if (process.env.DEBUG) {
|
|
153
|
-
console.error("Failed to append to log file:", appendError);
|
|
154
|
-
throw appendError;
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}).catch((error) => {
|
|
158
|
-
if (process.env.DEBUG) {
|
|
159
|
-
console.log("This message only visible in DEBUG mode, not in production");
|
|
160
|
-
console.error("Failed to resolve log file path:", error);
|
|
161
|
-
console.log(prefix, message, ...args);
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
}
|
|
165
|
-
}
|
|
166
|
-
let logger;
|
|
167
|
-
function initLoggerWithGlobalConfiguration() {
|
|
168
|
-
logger = new Logger();
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const SessionMessageContentSchema = z$1.object({
|
|
172
|
-
c: z$1.string(),
|
|
173
|
-
// Base64 encoded encrypted content
|
|
174
|
-
t: z$1.literal("encrypted")
|
|
175
|
-
});
|
|
176
|
-
const UpdateBodySchema = z$1.object({
|
|
177
|
-
message: z$1.object({
|
|
178
|
-
id: z$1.string(),
|
|
179
|
-
seq: z$1.number(),
|
|
180
|
-
content: SessionMessageContentSchema
|
|
181
|
-
}),
|
|
182
|
-
sid: z$1.string(),
|
|
183
|
-
// Session ID
|
|
184
|
-
t: z$1.literal("new-message")
|
|
185
|
-
});
|
|
186
|
-
const UpdateSessionBodySchema = z$1.object({
|
|
187
|
-
t: z$1.literal("update-session"),
|
|
188
|
-
sid: z$1.string(),
|
|
189
|
-
metadata: z$1.object({
|
|
190
|
-
version: z$1.number(),
|
|
191
|
-
metadata: z$1.string()
|
|
192
|
-
}).nullish(),
|
|
193
|
-
agentState: z$1.object({
|
|
194
|
-
version: z$1.number(),
|
|
195
|
-
agentState: z$1.string()
|
|
196
|
-
}).nullish()
|
|
197
|
-
});
|
|
198
|
-
z$1.object({
|
|
199
|
-
id: z$1.string(),
|
|
200
|
-
seq: z$1.number(),
|
|
201
|
-
body: z$1.union([UpdateBodySchema, UpdateSessionBodySchema]),
|
|
202
|
-
createdAt: z$1.number()
|
|
203
|
-
});
|
|
204
|
-
z$1.object({
|
|
205
|
-
createdAt: z$1.number(),
|
|
206
|
-
id: z$1.string(),
|
|
207
|
-
seq: z$1.number(),
|
|
208
|
-
updatedAt: z$1.number(),
|
|
209
|
-
metadata: z$1.any(),
|
|
210
|
-
metadataVersion: z$1.number(),
|
|
211
|
-
agentState: z$1.any().nullable(),
|
|
212
|
-
agentStateVersion: z$1.number()
|
|
213
|
-
});
|
|
214
|
-
z$1.object({
|
|
215
|
-
content: SessionMessageContentSchema,
|
|
216
|
-
createdAt: z$1.number(),
|
|
217
|
-
id: z$1.string(),
|
|
218
|
-
seq: z$1.number(),
|
|
219
|
-
updatedAt: z$1.number()
|
|
220
|
-
});
|
|
221
|
-
z$1.object({
|
|
222
|
-
session: z$1.object({
|
|
223
|
-
id: z$1.string(),
|
|
224
|
-
tag: z$1.string(),
|
|
225
|
-
seq: z$1.number(),
|
|
226
|
-
createdAt: z$1.number(),
|
|
227
|
-
updatedAt: z$1.number(),
|
|
228
|
-
metadata: z$1.string(),
|
|
229
|
-
metadataVersion: z$1.number(),
|
|
230
|
-
agentState: z$1.string().nullable(),
|
|
231
|
-
agentStateVersion: z$1.number()
|
|
232
|
-
})
|
|
233
|
-
});
|
|
234
|
-
const UserMessageSchema$1 = z$1.object({
|
|
235
|
-
role: z$1.literal("user"),
|
|
236
|
-
content: z$1.object({
|
|
237
|
-
type: z$1.literal("text"),
|
|
238
|
-
text: z$1.string()
|
|
239
|
-
}),
|
|
240
|
-
localKey: z$1.string().optional(),
|
|
241
|
-
// Mobile messages include this
|
|
242
|
-
sentFrom: z$1.enum(["mobile", "cli"]).optional()
|
|
243
|
-
// Source identifier
|
|
244
|
-
});
|
|
245
|
-
const AgentMessageSchema = z$1.object({
|
|
246
|
-
role: z$1.literal("agent"),
|
|
247
|
-
content: z$1.object({
|
|
248
|
-
type: z$1.literal("output"),
|
|
249
|
-
data: z$1.any()
|
|
250
|
-
})
|
|
251
|
-
});
|
|
252
|
-
z$1.union([UserMessageSchema$1, AgentMessageSchema]);
|
|
253
|
-
|
|
254
|
-
function encodeBase64(buffer) {
|
|
255
|
-
return Buffer.from(buffer).toString("base64");
|
|
256
|
-
}
|
|
257
|
-
function encodeBase64Url(buffer) {
|
|
258
|
-
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
259
|
-
}
|
|
260
|
-
function decodeBase64(base64) {
|
|
261
|
-
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
262
|
-
}
|
|
263
|
-
function getRandomBytes(size) {
|
|
264
|
-
return new Uint8Array(randomBytes(size));
|
|
265
|
-
}
|
|
266
|
-
function encrypt(data, secret) {
|
|
267
|
-
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
268
|
-
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
269
|
-
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
270
|
-
result.set(nonce);
|
|
271
|
-
result.set(encrypted, nonce.length);
|
|
272
|
-
return result;
|
|
273
|
-
}
|
|
274
|
-
function decrypt(data, secret) {
|
|
275
|
-
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
276
|
-
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
277
|
-
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
278
|
-
if (!decrypted) {
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
async function delay(ms) {
|
|
285
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
286
|
-
}
|
|
287
|
-
function exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount) {
|
|
288
|
-
let maxDelayRet = minDelay + (maxDelay - minDelay) / maxFailureCount * Math.max(currentFailureCount, maxFailureCount);
|
|
289
|
-
return Math.round(Math.random() * maxDelayRet);
|
|
290
|
-
}
|
|
291
|
-
function createBackoff(opts) {
|
|
292
|
-
return async (callback) => {
|
|
293
|
-
let currentFailureCount = 0;
|
|
294
|
-
const minDelay = 250;
|
|
295
|
-
const maxDelay = 1e3;
|
|
296
|
-
const maxFailureCount = 50;
|
|
297
|
-
while (true) {
|
|
298
|
-
try {
|
|
299
|
-
return await callback();
|
|
300
|
-
} catch (e) {
|
|
301
|
-
if (currentFailureCount < maxFailureCount) {
|
|
302
|
-
currentFailureCount++;
|
|
303
|
-
}
|
|
304
|
-
let waitForRequest = exponentialBackoffDelay(currentFailureCount, minDelay, maxDelay, maxFailureCount);
|
|
305
|
-
await delay(waitForRequest);
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
let backoff = createBackoff();
|
|
311
|
-
|
|
312
|
-
class ApiSessionClient extends EventEmitter {
|
|
313
|
-
token;
|
|
314
|
-
secret;
|
|
315
|
-
sessionId;
|
|
316
|
-
metadata;
|
|
317
|
-
metadataVersion;
|
|
318
|
-
agentState;
|
|
319
|
-
agentStateVersion;
|
|
320
|
-
socket;
|
|
321
|
-
pendingMessages = [];
|
|
322
|
-
pendingMessageCallback = null;
|
|
323
|
-
rpcHandlers = /* @__PURE__ */ new Map();
|
|
324
|
-
constructor(token, secret, session) {
|
|
325
|
-
super();
|
|
326
|
-
this.token = token;
|
|
327
|
-
this.secret = secret;
|
|
328
|
-
this.sessionId = session.id;
|
|
329
|
-
this.metadata = session.metadata;
|
|
330
|
-
this.metadataVersion = session.metadataVersion;
|
|
331
|
-
this.agentState = session.agentState;
|
|
332
|
-
this.agentStateVersion = session.agentStateVersion;
|
|
333
|
-
this.socket = io(configuration.serverUrl, {
|
|
334
|
-
auth: {
|
|
335
|
-
token: this.token,
|
|
336
|
-
clientType: "session-scoped",
|
|
337
|
-
sessionId: this.sessionId
|
|
338
|
-
},
|
|
339
|
-
path: "/v1/updates",
|
|
340
|
-
reconnection: true,
|
|
341
|
-
reconnectionAttempts: Infinity,
|
|
342
|
-
reconnectionDelay: 1e3,
|
|
343
|
-
reconnectionDelayMax: 5e3,
|
|
344
|
-
transports: ["websocket"],
|
|
345
|
-
withCredentials: true,
|
|
346
|
-
autoConnect: false
|
|
347
|
-
});
|
|
348
|
-
this.socket.on("connect", () => {
|
|
349
|
-
logger.debug("Socket connected successfully");
|
|
350
|
-
this.reregisterHandlers();
|
|
351
|
-
});
|
|
352
|
-
this.socket.on("rpc-request", async (data, callback) => {
|
|
353
|
-
try {
|
|
354
|
-
const method = data.method;
|
|
355
|
-
const handler = this.rpcHandlers.get(method);
|
|
356
|
-
if (!handler) {
|
|
357
|
-
logger.debug("[SOCKET] [RPC] [ERROR] method not found", { method });
|
|
358
|
-
const errorResponse = { error: "Method not found" };
|
|
359
|
-
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
360
|
-
callback(encryptedError);
|
|
361
|
-
return;
|
|
362
|
-
}
|
|
363
|
-
const decryptedParams = decrypt(decodeBase64(data.params), this.secret);
|
|
364
|
-
const result = await handler(decryptedParams);
|
|
365
|
-
const encryptedResponse = encodeBase64(encrypt(result, this.secret));
|
|
366
|
-
callback(encryptedResponse);
|
|
367
|
-
} catch (error) {
|
|
368
|
-
logger.debug("[SOCKET] [RPC] [ERROR] Error handling RPC request", { error });
|
|
369
|
-
const errorResponse = { error: error instanceof Error ? error.message : "Unknown error" };
|
|
370
|
-
const encryptedError = encodeBase64(encrypt(errorResponse, this.secret));
|
|
371
|
-
callback(encryptedError);
|
|
372
|
-
}
|
|
373
|
-
});
|
|
374
|
-
this.socket.on("disconnect", (reason) => {
|
|
375
|
-
logger.debug("[API] Socket disconnected:", reason);
|
|
376
|
-
});
|
|
377
|
-
this.socket.on("connect_error", (error) => {
|
|
378
|
-
logger.debug("[API] Socket connection error:", error);
|
|
379
|
-
});
|
|
380
|
-
this.socket.on("update", (data) => {
|
|
381
|
-
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
|
|
382
|
-
const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
|
|
383
|
-
logger.debugLargeJson("[SOCKET] [UPDATE] Received update:", body);
|
|
384
|
-
const userResult = UserMessageSchema$1.safeParse(body);
|
|
385
|
-
if (userResult.success) {
|
|
386
|
-
if (this.pendingMessageCallback) {
|
|
387
|
-
this.pendingMessageCallback(userResult.data);
|
|
388
|
-
} else {
|
|
389
|
-
this.pendingMessages.push(userResult.data);
|
|
390
|
-
}
|
|
391
|
-
} else {
|
|
392
|
-
this.emit("message", body);
|
|
393
|
-
}
|
|
394
|
-
} else if (data.body.t === "update-session") {
|
|
395
|
-
if (data.body.metadata && data.body.metadata.version > this.metadataVersion) {
|
|
396
|
-
this.metadata = decrypt(decodeBase64(data.body.metadata.metadata), this.secret);
|
|
397
|
-
this.metadataVersion = data.body.metadata.version;
|
|
398
|
-
}
|
|
399
|
-
if (data.body.agentState && data.body.agentState.version > this.agentStateVersion) {
|
|
400
|
-
this.agentState = data.body.agentState.agentState ? decrypt(decodeBase64(data.body.agentState.agentState), this.secret) : null;
|
|
401
|
-
this.agentStateVersion = data.body.agentState.version;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
});
|
|
405
|
-
this.socket.on("error", (error) => {
|
|
406
|
-
logger.debug("[API] Socket error:", error);
|
|
407
|
-
});
|
|
408
|
-
this.socket.connect();
|
|
409
|
-
}
|
|
410
|
-
onUserMessage(callback) {
|
|
411
|
-
this.pendingMessageCallback = callback;
|
|
412
|
-
while (this.pendingMessages.length > 0) {
|
|
413
|
-
callback(this.pendingMessages.shift());
|
|
414
|
-
}
|
|
415
|
-
}
|
|
416
|
-
/**
|
|
417
|
-
* Send message to session
|
|
418
|
-
* @param body - Message body (can be MessageContent or raw content for agent messages)
|
|
419
|
-
*/
|
|
420
|
-
sendClaudeSessionMessage(body) {
|
|
421
|
-
let content;
|
|
422
|
-
if (body.type === "user" && typeof body.message.content === "string") {
|
|
423
|
-
content = {
|
|
424
|
-
role: "user",
|
|
425
|
-
content: {
|
|
426
|
-
type: "text",
|
|
427
|
-
text: body.message.content
|
|
428
|
-
}
|
|
429
|
-
};
|
|
430
|
-
} else {
|
|
431
|
-
content = {
|
|
432
|
-
role: "agent",
|
|
433
|
-
content: {
|
|
434
|
-
type: "output",
|
|
435
|
-
data: body
|
|
436
|
-
// This wraps the entire Claude message
|
|
437
|
-
}
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
logger.debugLargeJson("[SOCKET] Sending message through socket:", content);
|
|
441
|
-
const encrypted = encodeBase64(encrypt(content, this.secret));
|
|
442
|
-
this.socket.emit("message", {
|
|
443
|
-
sid: this.sessionId,
|
|
444
|
-
message: encrypted
|
|
445
|
-
});
|
|
446
|
-
}
|
|
447
|
-
/**
|
|
448
|
-
* Send a ping message to keep the connection alive
|
|
449
|
-
*/
|
|
450
|
-
keepAlive(thinking) {
|
|
451
|
-
this.socket.volatile.emit("session-alive", { sid: this.sessionId, time: Date.now(), thinking });
|
|
452
|
-
}
|
|
453
|
-
/**
|
|
454
|
-
* Send session death message
|
|
455
|
-
*/
|
|
456
|
-
sendSessionDeath() {
|
|
457
|
-
this.socket.emit("session-end", { sid: this.sessionId, time: Date.now() });
|
|
458
|
-
}
|
|
459
|
-
/**
|
|
460
|
-
* Update session metadata
|
|
461
|
-
* @param handler - Handler function that returns the updated metadata
|
|
462
|
-
*/
|
|
463
|
-
updateMetadata(handler) {
|
|
464
|
-
backoff(async () => {
|
|
465
|
-
let updated = handler(this.metadata);
|
|
466
|
-
const answer = await this.socket.emitWithAck("update-metadata", { sid: this.sessionId, expectedVersion: this.metadataVersion, metadata: encodeBase64(encrypt(updated, this.secret)) });
|
|
467
|
-
if (answer.result === "success") {
|
|
468
|
-
this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
469
|
-
this.metadataVersion = answer.version;
|
|
470
|
-
} else if (answer.result === "version-mismatch") {
|
|
471
|
-
if (answer.version > this.metadataVersion) {
|
|
472
|
-
this.metadataVersion = answer.version;
|
|
473
|
-
this.metadata = decrypt(decodeBase64(answer.metadata), this.secret);
|
|
474
|
-
}
|
|
475
|
-
throw new Error("Metadata version mismatch");
|
|
476
|
-
} else if (answer.result === "error") ;
|
|
477
|
-
});
|
|
478
|
-
}
|
|
479
|
-
/**
|
|
480
|
-
* Update session agent state
|
|
481
|
-
* @param handler - Handler function that returns the updated agent state
|
|
482
|
-
*/
|
|
483
|
-
updateAgentState(handler) {
|
|
484
|
-
console.log("Updating agent state", this.agentState);
|
|
485
|
-
backoff(async () => {
|
|
486
|
-
let updated = handler(this.agentState || {});
|
|
487
|
-
const answer = await this.socket.emitWithAck("update-state", { sid: this.sessionId, expectedVersion: this.agentStateVersion, agentState: updated ? encodeBase64(encrypt(updated, this.secret)) : null });
|
|
488
|
-
if (answer.result === "success") {
|
|
489
|
-
this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
|
|
490
|
-
this.agentStateVersion = answer.version;
|
|
491
|
-
console.log("Agent state updated", this.agentState);
|
|
492
|
-
} else if (answer.result === "version-mismatch") {
|
|
493
|
-
if (answer.version > this.agentStateVersion) {
|
|
494
|
-
this.agentStateVersion = answer.version;
|
|
495
|
-
this.agentState = answer.agentState ? decrypt(decodeBase64(answer.agentState), this.secret) : null;
|
|
496
|
-
}
|
|
497
|
-
throw new Error("Agent state version mismatch");
|
|
498
|
-
} else if (answer.result === "error") {
|
|
499
|
-
console.error("Agent state update error", answer);
|
|
500
|
-
}
|
|
501
|
-
});
|
|
502
|
-
}
|
|
503
|
-
/**
|
|
504
|
-
* Set a custom RPC handler for a specific method with encrypted arguments and responses
|
|
505
|
-
* @param method - The method name to handle
|
|
506
|
-
* @param handler - The handler function to call when the method is invoked
|
|
507
|
-
*/
|
|
508
|
-
setHandler(method, handler) {
|
|
509
|
-
const prefixedMethod = `${this.sessionId}:${method}`;
|
|
510
|
-
this.rpcHandlers.set(prefixedMethod, handler);
|
|
511
|
-
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
512
|
-
logger.debug("Registered RPC handler", { method, prefixedMethod });
|
|
513
|
-
}
|
|
514
|
-
/**
|
|
515
|
-
* Re-register all RPC handlers after reconnection
|
|
516
|
-
*/
|
|
517
|
-
reregisterHandlers() {
|
|
518
|
-
logger.debug("Re-registering RPC handlers after reconnection", {
|
|
519
|
-
totalMethods: this.rpcHandlers.size
|
|
520
|
-
});
|
|
521
|
-
for (const [prefixedMethod] of this.rpcHandlers) {
|
|
522
|
-
this.socket.emit("rpc-register", { method: prefixedMethod });
|
|
523
|
-
logger.debug("Re-registered method", { prefixedMethod });
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
/**
|
|
527
|
-
* Wait for socket buffer to flush
|
|
528
|
-
*/
|
|
529
|
-
async flush() {
|
|
530
|
-
if (!this.socket.connected) {
|
|
531
|
-
return;
|
|
532
|
-
}
|
|
533
|
-
return new Promise((resolve) => {
|
|
534
|
-
this.socket.emit("ping", () => {
|
|
535
|
-
resolve();
|
|
536
|
-
});
|
|
537
|
-
setTimeout(() => {
|
|
538
|
-
resolve();
|
|
539
|
-
}, 1e4);
|
|
540
|
-
});
|
|
541
|
-
}
|
|
542
|
-
async close() {
|
|
543
|
-
this.socket.close();
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
|
|
547
|
-
class PushNotificationClient {
|
|
548
|
-
token;
|
|
549
|
-
baseUrl;
|
|
550
|
-
expo;
|
|
551
|
-
constructor(token, baseUrl = "https://handy-api.korshakov.org") {
|
|
552
|
-
this.token = token;
|
|
553
|
-
this.baseUrl = baseUrl;
|
|
554
|
-
this.expo = new Expo();
|
|
555
|
-
}
|
|
556
|
-
/**
|
|
557
|
-
* Fetch all push tokens for the authenticated user
|
|
558
|
-
*/
|
|
559
|
-
async fetchPushTokens() {
|
|
560
|
-
try {
|
|
561
|
-
const response = await axios.get(
|
|
562
|
-
`${this.baseUrl}/v1/push-tokens`,
|
|
563
|
-
{
|
|
564
|
-
headers: {
|
|
565
|
-
"Authorization": `Bearer ${this.token}`,
|
|
566
|
-
"Content-Type": "application/json"
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
);
|
|
570
|
-
logger.info(`Fetched ${response.data.tokens.length} push tokens`);
|
|
571
|
-
return response.data.tokens;
|
|
572
|
-
} catch (error) {
|
|
573
|
-
logger.debug("[PUSH] [ERROR] Failed to fetch push tokens:", error);
|
|
574
|
-
throw new Error(`Failed to fetch push tokens: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
/**
|
|
578
|
-
* Send push notification via Expo Push API with retry
|
|
579
|
-
* @param messages - Array of push messages to send
|
|
580
|
-
*/
|
|
581
|
-
async sendPushNotifications(messages) {
|
|
582
|
-
logger.info(`Sending ${messages.length} push notifications`);
|
|
583
|
-
const validMessages = messages.filter((message) => {
|
|
584
|
-
if (Array.isArray(message.to)) {
|
|
585
|
-
return message.to.every((token) => Expo.isExpoPushToken(token));
|
|
586
|
-
}
|
|
587
|
-
return Expo.isExpoPushToken(message.to);
|
|
588
|
-
});
|
|
589
|
-
if (validMessages.length === 0) {
|
|
590
|
-
logger.info("No valid Expo push tokens found");
|
|
591
|
-
return;
|
|
592
|
-
}
|
|
593
|
-
const chunks = this.expo.chunkPushNotifications(validMessages);
|
|
594
|
-
for (const chunk of chunks) {
|
|
595
|
-
const startTime = Date.now();
|
|
596
|
-
const timeout = 3e5;
|
|
597
|
-
let attempt = 0;
|
|
598
|
-
while (true) {
|
|
599
|
-
try {
|
|
600
|
-
const ticketChunk = await this.expo.sendPushNotificationsAsync(chunk);
|
|
601
|
-
const errors = ticketChunk.filter((ticket) => ticket.status === "error");
|
|
602
|
-
if (errors.length > 0) {
|
|
603
|
-
logger.debug("[PUSH] Some notifications failed:", errors);
|
|
604
|
-
}
|
|
605
|
-
if (errors.length === ticketChunk.length) {
|
|
606
|
-
throw new Error("All push notifications in chunk failed");
|
|
607
|
-
}
|
|
608
|
-
break;
|
|
609
|
-
} catch (error) {
|
|
610
|
-
const elapsed = Date.now() - startTime;
|
|
611
|
-
if (elapsed >= timeout) {
|
|
612
|
-
logger.debug("[PUSH] Timeout reached after 5 minutes, giving up on chunk");
|
|
613
|
-
break;
|
|
614
|
-
}
|
|
615
|
-
attempt++;
|
|
616
|
-
const delay = Math.min(1e3 * Math.pow(2, attempt), 3e4);
|
|
617
|
-
const remainingTime = timeout - elapsed;
|
|
618
|
-
const waitTime = Math.min(delay, remainingTime);
|
|
619
|
-
if (waitTime > 0) {
|
|
620
|
-
logger.debug(`[PUSH] Retrying in ${waitTime}ms (attempt ${attempt})`);
|
|
621
|
-
await new Promise((resolve) => setTimeout(resolve, waitTime));
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
logger.info(`Push notifications sent successfully`);
|
|
627
|
-
}
|
|
628
|
-
/**
|
|
629
|
-
* Send a push notification to all registered devices for the user
|
|
630
|
-
* @param title - Notification title
|
|
631
|
-
* @param body - Notification body
|
|
632
|
-
* @param data - Additional data to send with the notification
|
|
633
|
-
*/
|
|
634
|
-
async sendToAllDevices(title, body, data) {
|
|
635
|
-
const tokens = await this.fetchPushTokens();
|
|
636
|
-
if (tokens.length === 0) {
|
|
637
|
-
logger.info("No push tokens found for user");
|
|
638
|
-
return;
|
|
639
|
-
}
|
|
640
|
-
const messages = tokens.map((token) => ({
|
|
641
|
-
to: token.token,
|
|
642
|
-
title,
|
|
643
|
-
body,
|
|
644
|
-
data,
|
|
645
|
-
sound: "default",
|
|
646
|
-
priority: "high"
|
|
647
|
-
}));
|
|
648
|
-
await this.sendPushNotifications(messages);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
class ApiClient {
|
|
653
|
-
token;
|
|
654
|
-
secret;
|
|
655
|
-
pushClient;
|
|
656
|
-
constructor(token, secret) {
|
|
657
|
-
this.token = token;
|
|
658
|
-
this.secret = secret;
|
|
659
|
-
this.pushClient = new PushNotificationClient(token);
|
|
660
|
-
}
|
|
661
|
-
/**
|
|
662
|
-
* Create a new session or load existing one with the given tag
|
|
663
|
-
*/
|
|
664
|
-
async getOrCreateSession(opts) {
|
|
665
|
-
try {
|
|
666
|
-
const response = await axios.post(
|
|
667
|
-
`${configuration.serverUrl}/v1/sessions`,
|
|
668
|
-
{
|
|
669
|
-
tag: opts.tag,
|
|
670
|
-
metadata: encodeBase64(encrypt(opts.metadata, this.secret)),
|
|
671
|
-
agentState: opts.state ? encodeBase64(encrypt(opts.state, this.secret)) : null
|
|
672
|
-
},
|
|
673
|
-
{
|
|
674
|
-
headers: {
|
|
675
|
-
"Authorization": `Bearer ${this.token}`,
|
|
676
|
-
"Content-Type": "application/json"
|
|
677
|
-
}
|
|
678
|
-
}
|
|
679
|
-
);
|
|
680
|
-
logger.debug(`Session created/loaded: ${response.data.session.id} (tag: ${opts.tag})`);
|
|
681
|
-
let raw = response.data.session;
|
|
682
|
-
let session = {
|
|
683
|
-
id: raw.id,
|
|
684
|
-
createdAt: raw.createdAt,
|
|
685
|
-
updatedAt: raw.updatedAt,
|
|
686
|
-
seq: raw.seq,
|
|
687
|
-
metadata: decrypt(decodeBase64(raw.metadata), this.secret),
|
|
688
|
-
metadataVersion: raw.metadataVersion,
|
|
689
|
-
agentState: raw.agentState ? decrypt(decodeBase64(raw.agentState), this.secret) : null,
|
|
690
|
-
agentStateVersion: raw.agentStateVersion
|
|
691
|
-
};
|
|
692
|
-
return session;
|
|
693
|
-
} catch (error) {
|
|
694
|
-
logger.debug("[API] [ERROR] Failed to get or create session:", error);
|
|
695
|
-
throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
696
|
-
}
|
|
697
|
-
}
|
|
698
|
-
/**
|
|
699
|
-
* Start realtime session client
|
|
700
|
-
* @param id - Session ID
|
|
701
|
-
* @returns Session client
|
|
702
|
-
*/
|
|
703
|
-
session(session) {
|
|
704
|
-
return new ApiSessionClient(this.token, this.secret, session);
|
|
705
|
-
}
|
|
706
|
-
/**
|
|
707
|
-
* Get push notification client
|
|
708
|
-
* @returns Push notification client
|
|
709
|
-
*/
|
|
710
|
-
push() {
|
|
711
|
-
return this.pushClient;
|
|
712
|
-
}
|
|
713
|
-
}
|
|
27
|
+
import { EventEmitter } from 'node:events';
|
|
28
|
+
import { io } from 'socket.io-client';
|
|
29
|
+
import { homedir as homedir$1, hostname } from 'os';
|
|
30
|
+
import { existsSync as existsSync$1, readFileSync as readFileSync$1, unlinkSync, mkdirSync as mkdirSync$1, writeFileSync, chmodSync } from 'fs';
|
|
31
|
+
import 'expo-server-sdk';
|
|
714
32
|
|
|
715
33
|
function formatClaudeMessage(message, onAssistantResult) {
|
|
716
34
|
logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
|
|
@@ -1199,105 +517,6 @@ class InvalidateSync {
|
|
|
1199
517
|
};
|
|
1200
518
|
}
|
|
1201
519
|
|
|
1202
|
-
const UsageSchema = z$1.object({
|
|
1203
|
-
input_tokens: z$1.number().int().nonnegative(),
|
|
1204
|
-
cache_creation_input_tokens: z$1.number().int().nonnegative().optional(),
|
|
1205
|
-
cache_read_input_tokens: z$1.number().int().nonnegative().optional(),
|
|
1206
|
-
output_tokens: z$1.number().int().nonnegative(),
|
|
1207
|
-
service_tier: z$1.string().optional()
|
|
1208
|
-
});
|
|
1209
|
-
const TextContentSchema = z$1.object({
|
|
1210
|
-
type: z$1.literal("text"),
|
|
1211
|
-
text: z$1.string()
|
|
1212
|
-
});
|
|
1213
|
-
const ThinkingContentSchema = z$1.object({
|
|
1214
|
-
type: z$1.literal("thinking"),
|
|
1215
|
-
thinking: z$1.string(),
|
|
1216
|
-
signature: z$1.string()
|
|
1217
|
-
});
|
|
1218
|
-
const ToolUseContentSchema = z$1.object({
|
|
1219
|
-
type: z$1.literal("tool_use"),
|
|
1220
|
-
id: z$1.string(),
|
|
1221
|
-
name: z$1.string(),
|
|
1222
|
-
input: z$1.unknown()
|
|
1223
|
-
// Tool-specific input parameters
|
|
1224
|
-
});
|
|
1225
|
-
const ToolResultContentSchema = z$1.object({
|
|
1226
|
-
tool_use_id: z$1.string(),
|
|
1227
|
-
type: z$1.literal("tool_result"),
|
|
1228
|
-
content: z$1.union([
|
|
1229
|
-
z$1.string(),
|
|
1230
|
-
// For simple string responses
|
|
1231
|
-
z$1.array(TextContentSchema)
|
|
1232
|
-
// For structured content blocks (typically text)
|
|
1233
|
-
]),
|
|
1234
|
-
is_error: z$1.boolean().optional()
|
|
1235
|
-
});
|
|
1236
|
-
const ContentSchema = z$1.union([
|
|
1237
|
-
TextContentSchema,
|
|
1238
|
-
ThinkingContentSchema,
|
|
1239
|
-
ToolUseContentSchema,
|
|
1240
|
-
ToolResultContentSchema
|
|
1241
|
-
]);
|
|
1242
|
-
const UserMessageSchema = z$1.object({
|
|
1243
|
-
role: z$1.literal("user"),
|
|
1244
|
-
content: z$1.union([
|
|
1245
|
-
z$1.string(),
|
|
1246
|
-
// Simple string content
|
|
1247
|
-
z$1.array(z$1.union([ToolResultContentSchema, TextContentSchema]))
|
|
1248
|
-
])
|
|
1249
|
-
});
|
|
1250
|
-
const AssistantMessageSchema = z$1.object({
|
|
1251
|
-
id: z$1.string(),
|
|
1252
|
-
type: z$1.literal("message"),
|
|
1253
|
-
role: z$1.literal("assistant"),
|
|
1254
|
-
model: z$1.string(),
|
|
1255
|
-
content: z$1.array(ContentSchema),
|
|
1256
|
-
stop_reason: z$1.string().nullable(),
|
|
1257
|
-
stop_sequence: z$1.string().nullable(),
|
|
1258
|
-
usage: UsageSchema
|
|
1259
|
-
});
|
|
1260
|
-
const BaseEntrySchema = z$1.object({
|
|
1261
|
-
cwd: z$1.string(),
|
|
1262
|
-
sessionId: z$1.string(),
|
|
1263
|
-
version: z$1.string(),
|
|
1264
|
-
uuid: z$1.string(),
|
|
1265
|
-
timestamp: z$1.string().datetime(),
|
|
1266
|
-
parent_tool_use_id: z$1.string().nullable().optional()
|
|
1267
|
-
});
|
|
1268
|
-
const SummaryEntrySchema = z$1.object({
|
|
1269
|
-
type: z$1.literal("summary"),
|
|
1270
|
-
summary: z$1.string(),
|
|
1271
|
-
leafUuid: z$1.string()
|
|
1272
|
-
});
|
|
1273
|
-
const UserEntrySchema = BaseEntrySchema.extend({
|
|
1274
|
-
type: z$1.literal("user"),
|
|
1275
|
-
message: UserMessageSchema,
|
|
1276
|
-
isMeta: z$1.boolean().optional(),
|
|
1277
|
-
toolUseResult: z$1.unknown().optional()
|
|
1278
|
-
// Present when user responds to tool use
|
|
1279
|
-
});
|
|
1280
|
-
const AssistantEntrySchema = BaseEntrySchema.extend({
|
|
1281
|
-
type: z$1.literal("assistant"),
|
|
1282
|
-
message: AssistantMessageSchema,
|
|
1283
|
-
requestId: z$1.string().optional()
|
|
1284
|
-
});
|
|
1285
|
-
const SystemEntrySchema = BaseEntrySchema.extend({
|
|
1286
|
-
type: z$1.literal("system"),
|
|
1287
|
-
content: z$1.string(),
|
|
1288
|
-
isMeta: z$1.boolean().optional(),
|
|
1289
|
-
level: z$1.string().optional(),
|
|
1290
|
-
parentUuid: z$1.string().optional(),
|
|
1291
|
-
isSidechain: z$1.boolean().optional(),
|
|
1292
|
-
userType: z$1.string().optional()
|
|
1293
|
-
});
|
|
1294
|
-
const RawJSONLinesSchema = z$1.discriminatedUnion("type", [
|
|
1295
|
-
UserEntrySchema,
|
|
1296
|
-
AssistantEntrySchema,
|
|
1297
|
-
SummaryEntrySchema,
|
|
1298
|
-
SystemEntrySchema
|
|
1299
|
-
]);
|
|
1300
|
-
|
|
1301
520
|
function createSessionScanner(opts) {
|
|
1302
521
|
const projectName = resolve(opts.workingDirectory).replace(/\//g, "-");
|
|
1303
522
|
const projectDir = join(homedir(), ".claude", "projects", projectName);
|
|
@@ -1306,6 +525,7 @@ function createSessionScanner(opts) {
|
|
|
1306
525
|
let currentSessionId = null;
|
|
1307
526
|
let currentSessionWatcherAbortController = null;
|
|
1308
527
|
let processedMessages = /* @__PURE__ */ new Set();
|
|
528
|
+
let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
|
|
1309
529
|
const sync = new InvalidateSync(async () => {
|
|
1310
530
|
let sessions = [];
|
|
1311
531
|
for (let p of pendingSessions) {
|
|
@@ -1338,7 +558,14 @@ function createSessionScanner(opts) {
|
|
|
1338
558
|
processedMessages.add(key);
|
|
1339
559
|
logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
|
|
1340
560
|
logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
|
|
1341
|
-
|
|
561
|
+
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string" && parsed.data.isSidechain !== true && parsed.data.isMeta !== true) {
|
|
562
|
+
const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
|
|
563
|
+
if (currentCounter && currentCounter > 0) {
|
|
564
|
+
seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
|
|
565
|
+
continue;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
opts.onMessage(message);
|
|
1342
569
|
} catch (e) {
|
|
1343
570
|
continue;
|
|
1344
571
|
}
|
|
@@ -1392,8 +619,12 @@ function createSessionScanner(opts) {
|
|
|
1392
619
|
if (currentSessionId) {
|
|
1393
620
|
pendingSessions.add(currentSessionId);
|
|
1394
621
|
}
|
|
622
|
+
logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
|
|
1395
623
|
currentSessionId = sessionId;
|
|
1396
624
|
sync.invalidate();
|
|
625
|
+
},
|
|
626
|
+
onRemoteUserMessageForDeduplication: (messageContent) => {
|
|
627
|
+
seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
|
|
1397
628
|
}
|
|
1398
629
|
};
|
|
1399
630
|
}
|
|
@@ -1427,32 +658,24 @@ function sortKeys(value) {
|
|
|
1427
658
|
}
|
|
1428
659
|
|
|
1429
660
|
async function loop(opts) {
|
|
1430
|
-
let mode = "interactive";
|
|
661
|
+
let mode = opts.startingMode ?? "interactive";
|
|
1431
662
|
let currentMessageQueue = new MessageQueue();
|
|
1432
663
|
let sessionId = null;
|
|
1433
664
|
let onMessage = null;
|
|
1434
|
-
let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
|
|
1435
|
-
opts.session.onUserMessage((message) => {
|
|
1436
|
-
logger.debugLargeJson("User message pushed to queue:", message);
|
|
1437
|
-
currentMessageQueue.push(message.content.text);
|
|
1438
|
-
seenRemoteUserMessageCounters.set(message.content.text, (seenRemoteUserMessageCounters.get(message.content.text) || 0) + 1);
|
|
1439
|
-
if (onMessage) {
|
|
1440
|
-
onMessage();
|
|
1441
|
-
}
|
|
1442
|
-
});
|
|
1443
665
|
const sessionScanner = createSessionScanner({
|
|
1444
666
|
workingDirectory: opts.path,
|
|
1445
667
|
onMessage: (message) => {
|
|
1446
|
-
if (message.type === "user" && typeof message.message.content === "string") {
|
|
1447
|
-
const currentCounter = seenRemoteUserMessageCounters.get(message.message.content);
|
|
1448
|
-
if (currentCounter && currentCounter > 0) {
|
|
1449
|
-
seenRemoteUserMessageCounters.set(message.message.content, currentCounter - 1);
|
|
1450
|
-
return;
|
|
1451
|
-
}
|
|
1452
|
-
}
|
|
1453
668
|
opts.session.sendClaudeSessionMessage(message);
|
|
1454
669
|
}
|
|
1455
670
|
});
|
|
671
|
+
opts.session.onUserMessage((message) => {
|
|
672
|
+
sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
|
|
673
|
+
currentMessageQueue.push(message.content.text);
|
|
674
|
+
logger.debugLargeJson("User message pushed to queue:", message);
|
|
675
|
+
if (onMessage) {
|
|
676
|
+
onMessage();
|
|
677
|
+
}
|
|
678
|
+
});
|
|
1456
679
|
let onSessionFound = (newSessionId) => {
|
|
1457
680
|
sessionId = newSessionId;
|
|
1458
681
|
sessionScanner.onNewSession(newSessionId);
|
|
@@ -1507,10 +730,14 @@ async function loop(opts) {
|
|
|
1507
730
|
mode = "interactive";
|
|
1508
731
|
remoteAbortController.abort();
|
|
1509
732
|
}
|
|
1510
|
-
process.stdin.
|
|
733
|
+
if (process.stdin.isTTY) {
|
|
734
|
+
process.stdin.setRawMode(false);
|
|
735
|
+
}
|
|
1511
736
|
};
|
|
1512
737
|
process.stdin.resume();
|
|
1513
|
-
process.stdin.
|
|
738
|
+
if (process.stdin.isTTY) {
|
|
739
|
+
process.stdin.setRawMode(true);
|
|
740
|
+
}
|
|
1514
741
|
process.stdin.setEncoding("utf8");
|
|
1515
742
|
process.stdin.on("data", abortHandler);
|
|
1516
743
|
try {
|
|
@@ -1528,7 +755,9 @@ async function loop(opts) {
|
|
|
1528
755
|
});
|
|
1529
756
|
} finally {
|
|
1530
757
|
process.stdin.off("data", abortHandler);
|
|
1531
|
-
process.stdin.
|
|
758
|
+
if (process.stdin.isTTY) {
|
|
759
|
+
process.stdin.setRawMode(false);
|
|
760
|
+
}
|
|
1532
761
|
currentMessageQueue.close();
|
|
1533
762
|
currentMessageQueue = new MessageQueue();
|
|
1534
763
|
}
|
|
@@ -1639,16 +868,410 @@ class InterruptController {
|
|
|
1639
868
|
}
|
|
1640
869
|
}
|
|
1641
870
|
|
|
871
|
+
var version = "0.1.10";
|
|
872
|
+
var packageJson = {
|
|
873
|
+
version: version};
|
|
874
|
+
|
|
875
|
+
async function startAnthropicActivityProxy(onClaudeActivity) {
|
|
876
|
+
const requestTimeouts = /* @__PURE__ */ new Map();
|
|
877
|
+
let requestCounter = 0;
|
|
878
|
+
let idleTimer = null;
|
|
879
|
+
const maxTimeBeforeIdle = 50;
|
|
880
|
+
const requestTimeout = 5 * 60 * 1e3;
|
|
881
|
+
const cleanupRequest = (requestId, reason) => {
|
|
882
|
+
const timeout = requestTimeouts.get(requestId);
|
|
883
|
+
if (timeout) {
|
|
884
|
+
clearTimeout(timeout);
|
|
885
|
+
requestTimeouts.delete(requestId);
|
|
886
|
+
logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
|
|
887
|
+
claudeDidSomeWork();
|
|
888
|
+
}
|
|
889
|
+
};
|
|
890
|
+
const claudeDidSomeWork = () => {
|
|
891
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
892
|
+
if (requestTimeouts.size === 0) {
|
|
893
|
+
idleTimer = setTimeout(() => {
|
|
894
|
+
logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
|
|
895
|
+
onClaudeActivity("idle");
|
|
896
|
+
}, maxTimeBeforeIdle);
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
const server = createServer((req, res) => {
|
|
900
|
+
const requestId = ++requestCounter;
|
|
901
|
+
const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
|
|
902
|
+
if (isAnthropicRequest) {
|
|
903
|
+
const timeout = setTimeout(() => {
|
|
904
|
+
logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
|
|
905
|
+
cleanupRequest(requestId, "timeout");
|
|
906
|
+
}, requestTimeout);
|
|
907
|
+
requestTimeouts.set(requestId, timeout);
|
|
908
|
+
onClaudeActivity("working");
|
|
909
|
+
logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
|
|
910
|
+
}
|
|
911
|
+
const chunks = [];
|
|
912
|
+
req.on("data", (chunk) => {
|
|
913
|
+
chunks.push(chunk);
|
|
914
|
+
if (isAnthropicRequest) {
|
|
915
|
+
claudeDidSomeWork();
|
|
916
|
+
}
|
|
917
|
+
});
|
|
918
|
+
req.on("end", () => {
|
|
919
|
+
const body = Buffer.concat(chunks);
|
|
920
|
+
let targetUrl;
|
|
921
|
+
if (isAnthropicRequest) {
|
|
922
|
+
targetUrl = new URL$1(req.url || "/", "https://api.anthropic.com");
|
|
923
|
+
} else {
|
|
924
|
+
const protocol = req.headers["x-forwarded-proto"] || "https";
|
|
925
|
+
const host = req.headers.host || "localhost";
|
|
926
|
+
targetUrl = new URL$1(req.url || "/", `${protocol}://${host}`);
|
|
927
|
+
}
|
|
928
|
+
const options = {
|
|
929
|
+
hostname: targetUrl.hostname,
|
|
930
|
+
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
931
|
+
path: targetUrl.pathname + targetUrl.search,
|
|
932
|
+
method: req.method,
|
|
933
|
+
headers: {
|
|
934
|
+
...req.headers,
|
|
935
|
+
host: targetUrl.hostname
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
const requestMethod = targetUrl.protocol === "https:" ? request : request$1;
|
|
939
|
+
const proxyReq = requestMethod(options, (proxyRes) => {
|
|
940
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
941
|
+
proxyRes.pipe(res);
|
|
942
|
+
proxyRes.on("end", () => {
|
|
943
|
+
if (isAnthropicRequest) {
|
|
944
|
+
cleanupRequest(requestId, "completed");
|
|
945
|
+
}
|
|
946
|
+
});
|
|
947
|
+
});
|
|
948
|
+
proxyReq.on("error", (error) => {
|
|
949
|
+
if (isAnthropicRequest) {
|
|
950
|
+
cleanupRequest(requestId, `error: ${error.message}`);
|
|
951
|
+
} else {
|
|
952
|
+
logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
|
|
953
|
+
}
|
|
954
|
+
res.writeHead(502);
|
|
955
|
+
res.end("Bad Gateway");
|
|
956
|
+
});
|
|
957
|
+
if (body.length > 0) {
|
|
958
|
+
proxyReq.write(body);
|
|
959
|
+
}
|
|
960
|
+
proxyReq.end();
|
|
961
|
+
});
|
|
962
|
+
});
|
|
963
|
+
server.on("connect", (req, clientSocket, head) => {
|
|
964
|
+
const requestId = ++requestCounter;
|
|
965
|
+
const [hostname, port] = req.url?.split(":") || ["", "443"];
|
|
966
|
+
const isAnthropicRequest = hostname === "api.anthropic.com";
|
|
967
|
+
if (isAnthropicRequest) {
|
|
968
|
+
const timeout = setTimeout(() => {
|
|
969
|
+
logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
|
|
970
|
+
cleanupRequest(requestId, "timeout");
|
|
971
|
+
}, requestTimeout);
|
|
972
|
+
requestTimeouts.set(requestId, timeout);
|
|
973
|
+
onClaudeActivity("working");
|
|
974
|
+
logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
|
|
975
|
+
}
|
|
976
|
+
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
|
|
977
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
978
|
+
serverSocket.write(head);
|
|
979
|
+
serverSocket.pipe(clientSocket);
|
|
980
|
+
clientSocket.pipe(serverSocket);
|
|
981
|
+
});
|
|
982
|
+
const cleanup = () => {
|
|
983
|
+
if (isAnthropicRequest) {
|
|
984
|
+
cleanupRequest(requestId, "CONNECT closed");
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
serverSocket.on("error", (err) => {
|
|
988
|
+
logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
|
|
989
|
+
clientSocket.end();
|
|
990
|
+
cleanup();
|
|
991
|
+
});
|
|
992
|
+
clientSocket.on("error", cleanup);
|
|
993
|
+
clientSocket.on("end", cleanup);
|
|
994
|
+
serverSocket.on("end", cleanup);
|
|
995
|
+
});
|
|
996
|
+
const url = await new Promise((resolve) => {
|
|
997
|
+
server.listen(0, "127.0.0.1", () => {
|
|
998
|
+
const addr = server.address();
|
|
999
|
+
if (addr && typeof addr === "object") {
|
|
1000
|
+
resolve(`http://127.0.0.1:${addr.port}`);
|
|
1001
|
+
}
|
|
1002
|
+
});
|
|
1003
|
+
});
|
|
1004
|
+
logger.debug(`[AnthropicProxy] Started at ${url}`);
|
|
1005
|
+
return {
|
|
1006
|
+
url,
|
|
1007
|
+
cleanup: () => {
|
|
1008
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
1009
|
+
for (const [requestId, timeout] of requestTimeouts) {
|
|
1010
|
+
clearTimeout(timeout);
|
|
1011
|
+
logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
|
|
1012
|
+
}
|
|
1013
|
+
requestTimeouts.clear();
|
|
1014
|
+
if (requestTimeouts.size > 0) {
|
|
1015
|
+
logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
|
|
1016
|
+
}
|
|
1017
|
+
server.close();
|
|
1018
|
+
}
|
|
1019
|
+
};
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
const execAsync = promisify(exec);
|
|
1023
|
+
function registerHandlers(session, interruptController, permissionCallbacks) {
|
|
1024
|
+
session.setHandler("abort", async () => {
|
|
1025
|
+
logger.info("Abort request - interrupting Claude");
|
|
1026
|
+
await interruptController.interrupt();
|
|
1027
|
+
});
|
|
1028
|
+
if (permissionCallbacks) {
|
|
1029
|
+
session.setHandler("permission", async (message) => {
|
|
1030
|
+
logger.info("Permission response" + JSON.stringify(message));
|
|
1031
|
+
const id = message.id;
|
|
1032
|
+
const resolve = permissionCallbacks.requests.get(id);
|
|
1033
|
+
if (resolve) {
|
|
1034
|
+
if (!message.approved) {
|
|
1035
|
+
logger.debug("Permission denied, interrupting Claude");
|
|
1036
|
+
await interruptController.interrupt();
|
|
1037
|
+
}
|
|
1038
|
+
resolve({ approved: message.approved, reason: message.reason });
|
|
1039
|
+
permissionCallbacks.requests.delete(id);
|
|
1040
|
+
} else {
|
|
1041
|
+
logger.info("Permission request stale, likely timed out");
|
|
1042
|
+
return;
|
|
1043
|
+
}
|
|
1044
|
+
session.updateAgentState((currentState) => {
|
|
1045
|
+
let r = { ...currentState.requests };
|
|
1046
|
+
delete r[id];
|
|
1047
|
+
return {
|
|
1048
|
+
...currentState,
|
|
1049
|
+
requests: r
|
|
1050
|
+
};
|
|
1051
|
+
});
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
session.setHandler("bash", async (data) => {
|
|
1055
|
+
logger.info("Shell command request:", data.command);
|
|
1056
|
+
try {
|
|
1057
|
+
const options = {
|
|
1058
|
+
cwd: data.cwd,
|
|
1059
|
+
timeout: data.timeout || 3e4
|
|
1060
|
+
// Default 30 seconds timeout
|
|
1061
|
+
};
|
|
1062
|
+
const { stdout, stderr } = await execAsync(data.command, options);
|
|
1063
|
+
return {
|
|
1064
|
+
success: true,
|
|
1065
|
+
stdout: stdout || "",
|
|
1066
|
+
stderr: stderr || "",
|
|
1067
|
+
exitCode: 0
|
|
1068
|
+
};
|
|
1069
|
+
} catch (error) {
|
|
1070
|
+
const execError = error;
|
|
1071
|
+
if (execError.code === "ETIMEDOUT" || execError.killed) {
|
|
1072
|
+
return {
|
|
1073
|
+
success: false,
|
|
1074
|
+
stdout: execError.stdout || "",
|
|
1075
|
+
stderr: execError.stderr || "",
|
|
1076
|
+
exitCode: typeof execError.code === "number" ? execError.code : -1,
|
|
1077
|
+
error: "Command timed out"
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
return {
|
|
1081
|
+
success: false,
|
|
1082
|
+
stdout: execError.stdout || "",
|
|
1083
|
+
stderr: execError.stderr || execError.message || "Command failed",
|
|
1084
|
+
exitCode: typeof execError.code === "number" ? execError.code : 1,
|
|
1085
|
+
error: execError.message || "Command failed"
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
});
|
|
1089
|
+
session.setHandler("readFile", async (data) => {
|
|
1090
|
+
logger.info("Read file request:", data.path);
|
|
1091
|
+
try {
|
|
1092
|
+
const buffer = await readFile$1(data.path);
|
|
1093
|
+
const content = buffer.toString("base64");
|
|
1094
|
+
return { success: true, content };
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
logger.debug("Failed to read file:", error);
|
|
1097
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to read file" };
|
|
1098
|
+
}
|
|
1099
|
+
});
|
|
1100
|
+
session.setHandler("writeFile", async (data) => {
|
|
1101
|
+
logger.info("Write file request:", data.path);
|
|
1102
|
+
try {
|
|
1103
|
+
if (data.expectedHash !== null && data.expectedHash !== void 0) {
|
|
1104
|
+
try {
|
|
1105
|
+
const existingBuffer = await readFile$1(data.path);
|
|
1106
|
+
const existingHash = createHash("sha256").update(existingBuffer).digest("hex");
|
|
1107
|
+
if (existingHash !== data.expectedHash) {
|
|
1108
|
+
return {
|
|
1109
|
+
success: false,
|
|
1110
|
+
error: `File hash mismatch. Expected: ${data.expectedHash}, Actual: ${existingHash}`
|
|
1111
|
+
};
|
|
1112
|
+
}
|
|
1113
|
+
} catch (error) {
|
|
1114
|
+
const nodeError = error;
|
|
1115
|
+
if (nodeError.code !== "ENOENT") {
|
|
1116
|
+
throw error;
|
|
1117
|
+
}
|
|
1118
|
+
return {
|
|
1119
|
+
success: false,
|
|
1120
|
+
error: "File does not exist but hash was provided"
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
} else {
|
|
1124
|
+
try {
|
|
1125
|
+
await stat(data.path);
|
|
1126
|
+
return {
|
|
1127
|
+
success: false,
|
|
1128
|
+
error: "File already exists but was expected to be new"
|
|
1129
|
+
};
|
|
1130
|
+
} catch (error) {
|
|
1131
|
+
const nodeError = error;
|
|
1132
|
+
if (nodeError.code !== "ENOENT") {
|
|
1133
|
+
throw error;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
const buffer = Buffer.from(data.content, "base64");
|
|
1138
|
+
await writeFile(data.path, buffer);
|
|
1139
|
+
const hash = createHash("sha256").update(buffer).digest("hex");
|
|
1140
|
+
return { success: true, hash };
|
|
1141
|
+
} catch (error) {
|
|
1142
|
+
logger.debug("Failed to write file:", error);
|
|
1143
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to write file" };
|
|
1144
|
+
}
|
|
1145
|
+
});
|
|
1146
|
+
session.setHandler("listDirectory", async (data) => {
|
|
1147
|
+
logger.info("List directory request:", data.path);
|
|
1148
|
+
try {
|
|
1149
|
+
const entries = await readdir(data.path, { withFileTypes: true });
|
|
1150
|
+
const directoryEntries = await Promise.all(
|
|
1151
|
+
entries.map(async (entry) => {
|
|
1152
|
+
const fullPath = join$1(data.path, entry.name);
|
|
1153
|
+
let type = "other";
|
|
1154
|
+
let size;
|
|
1155
|
+
let modified;
|
|
1156
|
+
if (entry.isDirectory()) {
|
|
1157
|
+
type = "directory";
|
|
1158
|
+
} else if (entry.isFile()) {
|
|
1159
|
+
type = "file";
|
|
1160
|
+
}
|
|
1161
|
+
try {
|
|
1162
|
+
const stats = await stat(fullPath);
|
|
1163
|
+
size = stats.size;
|
|
1164
|
+
modified = stats.mtime.getTime();
|
|
1165
|
+
} catch (error) {
|
|
1166
|
+
logger.debug(`Failed to stat ${fullPath}:`, error);
|
|
1167
|
+
}
|
|
1168
|
+
return {
|
|
1169
|
+
name: entry.name,
|
|
1170
|
+
type,
|
|
1171
|
+
size,
|
|
1172
|
+
modified
|
|
1173
|
+
};
|
|
1174
|
+
})
|
|
1175
|
+
);
|
|
1176
|
+
directoryEntries.sort((a, b) => {
|
|
1177
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1178
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1179
|
+
return a.name.localeCompare(b.name);
|
|
1180
|
+
});
|
|
1181
|
+
return { success: true, entries: directoryEntries };
|
|
1182
|
+
} catch (error) {
|
|
1183
|
+
logger.debug("Failed to list directory:", error);
|
|
1184
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to list directory" };
|
|
1185
|
+
}
|
|
1186
|
+
});
|
|
1187
|
+
session.setHandler("getDirectoryTree", async (data) => {
|
|
1188
|
+
logger.info("Get directory tree request:", data.path, "maxDepth:", data.maxDepth);
|
|
1189
|
+
async function buildTree(path, name, currentDepth) {
|
|
1190
|
+
try {
|
|
1191
|
+
const stats = await stat(path);
|
|
1192
|
+
const node = {
|
|
1193
|
+
name,
|
|
1194
|
+
path,
|
|
1195
|
+
type: stats.isDirectory() ? "directory" : "file",
|
|
1196
|
+
size: stats.size,
|
|
1197
|
+
modified: stats.mtime.getTime()
|
|
1198
|
+
};
|
|
1199
|
+
if (stats.isDirectory() && currentDepth < data.maxDepth) {
|
|
1200
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
1201
|
+
const children = [];
|
|
1202
|
+
await Promise.all(
|
|
1203
|
+
entries.map(async (entry) => {
|
|
1204
|
+
if (entry.isSymbolicLink()) {
|
|
1205
|
+
logger.debug(`Skipping symlink: ${join$1(path, entry.name)}`);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const childPath = join$1(path, entry.name);
|
|
1209
|
+
const childNode = await buildTree(childPath, entry.name, currentDepth + 1);
|
|
1210
|
+
if (childNode) {
|
|
1211
|
+
children.push(childNode);
|
|
1212
|
+
}
|
|
1213
|
+
})
|
|
1214
|
+
);
|
|
1215
|
+
children.sort((a, b) => {
|
|
1216
|
+
if (a.type === "directory" && b.type !== "directory") return -1;
|
|
1217
|
+
if (a.type !== "directory" && b.type === "directory") return 1;
|
|
1218
|
+
return a.name.localeCompare(b.name);
|
|
1219
|
+
});
|
|
1220
|
+
node.children = children;
|
|
1221
|
+
}
|
|
1222
|
+
return node;
|
|
1223
|
+
} catch (error) {
|
|
1224
|
+
logger.debug(`Failed to process ${path}:`, error instanceof Error ? error.message : String(error));
|
|
1225
|
+
return null;
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
if (data.maxDepth < 0) {
|
|
1230
|
+
return { success: false, error: "maxDepth must be non-negative" };
|
|
1231
|
+
}
|
|
1232
|
+
const baseName = data.path === "/" ? "/" : data.path.split("/").pop() || data.path;
|
|
1233
|
+
const tree = await buildTree(data.path, baseName, 0);
|
|
1234
|
+
if (!tree) {
|
|
1235
|
+
return { success: false, error: "Failed to access the specified path" };
|
|
1236
|
+
}
|
|
1237
|
+
return { success: true, tree };
|
|
1238
|
+
} catch (error) {
|
|
1239
|
+
logger.debug("Failed to get directory tree:", error);
|
|
1240
|
+
return { success: false, error: error instanceof Error ? error.message : "Failed to get directory tree" };
|
|
1241
|
+
}
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1642
1245
|
async function start(credentials, options = {}) {
|
|
1643
1246
|
const workingDirectory = process.cwd();
|
|
1644
1247
|
const sessionTag = randomUUID();
|
|
1645
1248
|
const api = new ApiClient(credentials.token, credentials.secret);
|
|
1646
1249
|
let state = {};
|
|
1647
|
-
let metadata = { path: workingDirectory, host: os.hostname() };
|
|
1250
|
+
let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version, os: os.platform() };
|
|
1648
1251
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1649
1252
|
logger.debug(`Session created: ${response.id}`);
|
|
1650
1253
|
const session = api.session(response);
|
|
1651
1254
|
const pushClient = api.push();
|
|
1255
|
+
let thinking = false;
|
|
1256
|
+
let pingInterval = setInterval(() => {
|
|
1257
|
+
session.keepAlive(thinking);
|
|
1258
|
+
}, 2e3);
|
|
1259
|
+
const antropicActivityProxy = await startAnthropicActivityProxy(
|
|
1260
|
+
(activity) => {
|
|
1261
|
+
const newThinking = activity === "working";
|
|
1262
|
+
if (newThinking !== thinking) {
|
|
1263
|
+
thinking = newThinking;
|
|
1264
|
+
logger.debug(`[PING] Thinking state changed: ${thinking}`);
|
|
1265
|
+
session.keepAlive(thinking);
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
);
|
|
1269
|
+
process.env.HTTP_PROXY = antropicActivityProxy.url;
|
|
1270
|
+
process.env.HTTPS_PROXY = antropicActivityProxy.url;
|
|
1271
|
+
logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
|
|
1272
|
+
const logPath = await logger.logFilePathPromise;
|
|
1273
|
+
logger.infoDeveloper(`Session: ${response.id}`);
|
|
1274
|
+
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
1652
1275
|
const interruptController = new InterruptController();
|
|
1653
1276
|
let requests = /* @__PURE__ */ new Map();
|
|
1654
1277
|
const permissionServer = await startPermissionServerV2(async (request) => {
|
|
@@ -1701,33 +1324,7 @@ async function start(credentials, options = {}) {
|
|
|
1701
1324
|
promise.then(() => clearTimeout(timeout)).catch(() => clearTimeout(timeout));
|
|
1702
1325
|
return promise;
|
|
1703
1326
|
});
|
|
1704
|
-
session
|
|
1705
|
-
logger.info("Permission response" + JSON.stringify(message));
|
|
1706
|
-
const id = message.id;
|
|
1707
|
-
const resolve = requests.get(id);
|
|
1708
|
-
if (resolve) {
|
|
1709
|
-
resolve({ approved: message.approved, reason: message.reason });
|
|
1710
|
-
} else {
|
|
1711
|
-
logger.info("Permission request stale, likely timed out");
|
|
1712
|
-
return;
|
|
1713
|
-
}
|
|
1714
|
-
session.updateAgentState((currentState) => {
|
|
1715
|
-
let r = { ...currentState.requests };
|
|
1716
|
-
delete r[id];
|
|
1717
|
-
return {
|
|
1718
|
-
...currentState,
|
|
1719
|
-
requests: r
|
|
1720
|
-
};
|
|
1721
|
-
});
|
|
1722
|
-
});
|
|
1723
|
-
session.setHandler("abort", async () => {
|
|
1724
|
-
logger.info("Abort request - interrupting Claude");
|
|
1725
|
-
await interruptController.interrupt();
|
|
1726
|
-
});
|
|
1727
|
-
let thinking = false;
|
|
1728
|
-
const pingInterval = setInterval(() => {
|
|
1729
|
-
session.keepAlive(thinking);
|
|
1730
|
-
}, 15e3);
|
|
1327
|
+
registerHandlers(session, interruptController, { requests });
|
|
1731
1328
|
const onAssistantResult = async (result) => {
|
|
1732
1329
|
try {
|
|
1733
1330
|
const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
|
|
@@ -1751,6 +1348,7 @@ async function start(credentials, options = {}) {
|
|
|
1751
1348
|
path: workingDirectory,
|
|
1752
1349
|
model: options.model,
|
|
1753
1350
|
permissionMode: options.permissionMode,
|
|
1351
|
+
startingMode: options.startingMode,
|
|
1754
1352
|
mcpServers: {
|
|
1755
1353
|
"permission": {
|
|
1756
1354
|
type: "http",
|
|
@@ -1758,18 +1356,38 @@ async function start(credentials, options = {}) {
|
|
|
1758
1356
|
}
|
|
1759
1357
|
},
|
|
1760
1358
|
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1761
|
-
onThinking: (t) => {
|
|
1762
|
-
thinking = t;
|
|
1763
|
-
session.keepAlive(t);
|
|
1764
|
-
},
|
|
1765
1359
|
session,
|
|
1766
1360
|
onAssistantResult,
|
|
1767
1361
|
interruptController
|
|
1768
1362
|
});
|
|
1769
1363
|
clearInterval(pingInterval);
|
|
1364
|
+
if (antropicActivityProxy) {
|
|
1365
|
+
logger.debug("[AnthropicProxy] Shutting down thinking activity monitoring proxy");
|
|
1366
|
+
antropicActivityProxy.cleanup();
|
|
1367
|
+
}
|
|
1770
1368
|
process.exit(0);
|
|
1771
1369
|
}
|
|
1772
1370
|
|
|
1371
|
+
const defaultSettings = {
|
|
1372
|
+
onboardingCompleted: false
|
|
1373
|
+
};
|
|
1374
|
+
async function readSettings() {
|
|
1375
|
+
if (!existsSync(configuration.settingsFile)) {
|
|
1376
|
+
return { ...defaultSettings };
|
|
1377
|
+
}
|
|
1378
|
+
try {
|
|
1379
|
+
const content = await readFile(configuration.settingsFile, "utf8");
|
|
1380
|
+
return JSON.parse(content);
|
|
1381
|
+
} catch {
|
|
1382
|
+
return { ...defaultSettings };
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1385
|
+
async function writeSettings(settings) {
|
|
1386
|
+
if (!existsSync(configuration.happyDir)) {
|
|
1387
|
+
await mkdir(configuration.happyDir, { recursive: true });
|
|
1388
|
+
}
|
|
1389
|
+
await writeFile$1(configuration.settingsFile, JSON.stringify(settings, null, 2));
|
|
1390
|
+
}
|
|
1773
1391
|
const credentialsSchema = z.object({
|
|
1774
1392
|
secret: z.string().base64(),
|
|
1775
1393
|
token: z.string()
|
|
@@ -1793,7 +1411,7 @@ async function writeCredentials(credentials) {
|
|
|
1793
1411
|
if (!existsSync(configuration.happyDir)) {
|
|
1794
1412
|
await mkdir(configuration.happyDir, { recursive: true });
|
|
1795
1413
|
}
|
|
1796
|
-
await writeFile(configuration.privateKeyFile, JSON.stringify({
|
|
1414
|
+
await writeFile$1(configuration.privateKeyFile, JSON.stringify({
|
|
1797
1415
|
secret: encodeBase64(credentials.secret),
|
|
1798
1416
|
token: credentials.token
|
|
1799
1417
|
}, null, 2));
|
|
@@ -1824,7 +1442,12 @@ async function doAuth() {
|
|
|
1824
1442
|
return null;
|
|
1825
1443
|
}
|
|
1826
1444
|
console.log("Please, authenticate using mobile app");
|
|
1827
|
-
|
|
1445
|
+
const authUrl = "happy://terminal?" + encodeBase64Url(keypair.publicKey);
|
|
1446
|
+
displayQRCode(authUrl);
|
|
1447
|
+
if (process.env.DEBUG === "1") {
|
|
1448
|
+
console.log("\n\u{1F4CB} For manual entry, copy this URL:");
|
|
1449
|
+
console.log(authUrl);
|
|
1450
|
+
}
|
|
1828
1451
|
let credentials = null;
|
|
1829
1452
|
while (true) {
|
|
1830
1453
|
try {
|
|
@@ -1866,6 +1489,355 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1866
1489
|
return decrypted;
|
|
1867
1490
|
}
|
|
1868
1491
|
|
|
1492
|
+
class ApiDaemonSession extends EventEmitter {
|
|
1493
|
+
socket;
|
|
1494
|
+
machineIdentity;
|
|
1495
|
+
keepAliveInterval = null;
|
|
1496
|
+
token;
|
|
1497
|
+
secret;
|
|
1498
|
+
constructor(token, secret, machineIdentity) {
|
|
1499
|
+
super();
|
|
1500
|
+
this.token = token;
|
|
1501
|
+
this.secret = secret;
|
|
1502
|
+
this.machineIdentity = machineIdentity;
|
|
1503
|
+
const socket = io(configuration.serverUrl, {
|
|
1504
|
+
auth: {
|
|
1505
|
+
token: this.token,
|
|
1506
|
+
clientType: "machine-scoped",
|
|
1507
|
+
machineId: this.machineIdentity.machineId
|
|
1508
|
+
},
|
|
1509
|
+
path: "/v1/user-machine-daemon",
|
|
1510
|
+
reconnection: true,
|
|
1511
|
+
reconnectionAttempts: Infinity,
|
|
1512
|
+
reconnectionDelay: 1e3,
|
|
1513
|
+
reconnectionDelayMax: 5e3,
|
|
1514
|
+
transports: ["websocket"],
|
|
1515
|
+
withCredentials: true,
|
|
1516
|
+
autoConnect: false
|
|
1517
|
+
});
|
|
1518
|
+
socket.on("connect", () => {
|
|
1519
|
+
logger.debug("[DAEMON] Connected to server");
|
|
1520
|
+
this.emit("connected");
|
|
1521
|
+
socket.emit("machine-connect", {
|
|
1522
|
+
token: this.token,
|
|
1523
|
+
machineIdentity: encodeBase64(encrypt(this.machineIdentity, this.secret))
|
|
1524
|
+
});
|
|
1525
|
+
this.startKeepAlive();
|
|
1526
|
+
});
|
|
1527
|
+
socket.on("disconnect", () => {
|
|
1528
|
+
logger.debug("[DAEMON] Disconnected from server");
|
|
1529
|
+
this.emit("disconnected");
|
|
1530
|
+
this.stopKeepAlive();
|
|
1531
|
+
});
|
|
1532
|
+
socket.on("spawn-session", async (encryptedData, callback) => {
|
|
1533
|
+
let requestData;
|
|
1534
|
+
try {
|
|
1535
|
+
requestData = decrypt(decodeBase64(encryptedData), this.secret);
|
|
1536
|
+
logger.debug("[DAEMON] Received spawn-session request", requestData);
|
|
1537
|
+
const args = [
|
|
1538
|
+
"--directory",
|
|
1539
|
+
requestData.directory,
|
|
1540
|
+
"--happy-starting-mode",
|
|
1541
|
+
requestData.startingMode
|
|
1542
|
+
];
|
|
1543
|
+
if (requestData.metadata) {
|
|
1544
|
+
args.push("--metadata", requestData.metadata);
|
|
1545
|
+
}
|
|
1546
|
+
if (requestData.startingMode === "interactive" && process.platform === "darwin") {
|
|
1547
|
+
const script = `
|
|
1548
|
+
tell application "Terminal"
|
|
1549
|
+
activate
|
|
1550
|
+
do script "cd ${requestData.directory} && happy ${args.join(" ")}"
|
|
1551
|
+
end tell
|
|
1552
|
+
`;
|
|
1553
|
+
spawn$1("osascript", ["-e", script], { detached: true });
|
|
1554
|
+
} else {
|
|
1555
|
+
const child = spawn$1("happy", args, {
|
|
1556
|
+
detached: true,
|
|
1557
|
+
stdio: "ignore",
|
|
1558
|
+
cwd: requestData.directory
|
|
1559
|
+
});
|
|
1560
|
+
child.unref();
|
|
1561
|
+
}
|
|
1562
|
+
const result = { success: true };
|
|
1563
|
+
socket.emit("session-spawn-result", {
|
|
1564
|
+
requestId: requestData.requestId,
|
|
1565
|
+
result: encodeBase64(encrypt(result, this.secret))
|
|
1566
|
+
});
|
|
1567
|
+
callback(encodeBase64(encrypt({ success: true }, this.secret)));
|
|
1568
|
+
} catch (error) {
|
|
1569
|
+
logger.debug("[DAEMON] Failed to spawn session", error);
|
|
1570
|
+
const errorResult = {
|
|
1571
|
+
success: false,
|
|
1572
|
+
error: error instanceof Error ? error.message : "Unknown error"
|
|
1573
|
+
};
|
|
1574
|
+
socket.emit("session-spawn-result", {
|
|
1575
|
+
requestId: requestData?.requestId || "",
|
|
1576
|
+
result: encodeBase64(encrypt(errorResult, this.secret))
|
|
1577
|
+
});
|
|
1578
|
+
callback(encodeBase64(encrypt(errorResult, this.secret)));
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
socket.on("daemon-command", (data) => {
|
|
1582
|
+
switch (data.command) {
|
|
1583
|
+
case "shutdown":
|
|
1584
|
+
this.shutdown();
|
|
1585
|
+
break;
|
|
1586
|
+
case "status":
|
|
1587
|
+
this.emit("status-request");
|
|
1588
|
+
break;
|
|
1589
|
+
}
|
|
1590
|
+
});
|
|
1591
|
+
this.socket = socket;
|
|
1592
|
+
}
|
|
1593
|
+
startKeepAlive() {
|
|
1594
|
+
this.stopKeepAlive();
|
|
1595
|
+
this.keepAliveInterval = setInterval(() => {
|
|
1596
|
+
this.socket.volatile.emit("machine-alive", {
|
|
1597
|
+
time: Date.now()
|
|
1598
|
+
});
|
|
1599
|
+
}, 2e4);
|
|
1600
|
+
}
|
|
1601
|
+
stopKeepAlive() {
|
|
1602
|
+
if (this.keepAliveInterval) {
|
|
1603
|
+
clearInterval(this.keepAliveInterval);
|
|
1604
|
+
this.keepAliveInterval = null;
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
connect() {
|
|
1608
|
+
this.socket.connect();
|
|
1609
|
+
}
|
|
1610
|
+
shutdown() {
|
|
1611
|
+
this.stopKeepAlive();
|
|
1612
|
+
this.socket.close();
|
|
1613
|
+
this.emit("shutdown");
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
const DAEMON_PID_FILE = join$1(homedir$1(), ".happy", "daemon-pid");
|
|
1618
|
+
async function startDaemon() {
|
|
1619
|
+
if (isDaemonRunning()) {
|
|
1620
|
+
console.log("Happy daemon is already running");
|
|
1621
|
+
process.exit(0);
|
|
1622
|
+
}
|
|
1623
|
+
logger.info("Happy CLI daemon started successfully");
|
|
1624
|
+
writePidFile();
|
|
1625
|
+
process.on("SIGINT", stopDaemon);
|
|
1626
|
+
process.on("SIGTERM", stopDaemon);
|
|
1627
|
+
process.on("exit", stopDaemon);
|
|
1628
|
+
try {
|
|
1629
|
+
const settings = await readSettings() || { onboardingCompleted: false };
|
|
1630
|
+
if (!settings.machineId) {
|
|
1631
|
+
settings.machineId = crypto.randomUUID();
|
|
1632
|
+
settings.machineHost = hostname();
|
|
1633
|
+
await writeSettings(settings);
|
|
1634
|
+
}
|
|
1635
|
+
const machineIdentity = {
|
|
1636
|
+
machineId: settings.machineId,
|
|
1637
|
+
machineHost: settings.machineHost || hostname(),
|
|
1638
|
+
platform: process.platform,
|
|
1639
|
+
version: process.env.npm_package_version || "unknown"
|
|
1640
|
+
};
|
|
1641
|
+
let credentials = await readCredentials();
|
|
1642
|
+
if (!credentials) {
|
|
1643
|
+
logger.debug("[DAEMON] No credentials found, running auth");
|
|
1644
|
+
await doAuth();
|
|
1645
|
+
credentials = await readCredentials();
|
|
1646
|
+
if (!credentials) {
|
|
1647
|
+
throw new Error("Failed to authenticate");
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
const { token, secret } = credentials;
|
|
1651
|
+
const daemon = new ApiDaemonSession(token, secret, machineIdentity);
|
|
1652
|
+
daemon.on("connected", () => {
|
|
1653
|
+
logger.debug("[DAEMON] Successfully connected to server");
|
|
1654
|
+
});
|
|
1655
|
+
daemon.on("disconnected", () => {
|
|
1656
|
+
logger.debug("[DAEMON] Disconnected from server");
|
|
1657
|
+
});
|
|
1658
|
+
daemon.on("shutdown", () => {
|
|
1659
|
+
logger.debug("[DAEMON] Shutdown requested");
|
|
1660
|
+
stopDaemon();
|
|
1661
|
+
process.exit(0);
|
|
1662
|
+
});
|
|
1663
|
+
daemon.connect();
|
|
1664
|
+
setInterval(() => {
|
|
1665
|
+
}, 1e3);
|
|
1666
|
+
} catch (error) {
|
|
1667
|
+
logger.debug("[DAEMON] Failed to start daemon", error);
|
|
1668
|
+
stopDaemon();
|
|
1669
|
+
process.exit(1);
|
|
1670
|
+
}
|
|
1671
|
+
process.on("SIGINT", () => process.exit(0));
|
|
1672
|
+
process.on("SIGTERM", () => process.exit(0));
|
|
1673
|
+
process.on("exit", () => process.exit(0));
|
|
1674
|
+
while (true) {
|
|
1675
|
+
await new Promise((resolve) => setTimeout(resolve, 1e3));
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
function isDaemonRunning() {
|
|
1679
|
+
try {
|
|
1680
|
+
if (!existsSync$1(DAEMON_PID_FILE)) {
|
|
1681
|
+
console.log("No PID file found");
|
|
1682
|
+
return false;
|
|
1683
|
+
}
|
|
1684
|
+
const pid = parseInt(readFileSync$1(DAEMON_PID_FILE, "utf-8"));
|
|
1685
|
+
try {
|
|
1686
|
+
process.kill(pid, 0);
|
|
1687
|
+
return true;
|
|
1688
|
+
} catch (error) {
|
|
1689
|
+
console.log("Process not running", error);
|
|
1690
|
+
unlinkSync(DAEMON_PID_FILE);
|
|
1691
|
+
return false;
|
|
1692
|
+
}
|
|
1693
|
+
} catch {
|
|
1694
|
+
return false;
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
function writePidFile() {
|
|
1698
|
+
const happyDir = join$1(homedir$1(), ".happy");
|
|
1699
|
+
if (!existsSync$1(happyDir)) {
|
|
1700
|
+
mkdirSync$1(happyDir, { recursive: true });
|
|
1701
|
+
}
|
|
1702
|
+
writeFileSync(DAEMON_PID_FILE, process.pid.toString());
|
|
1703
|
+
}
|
|
1704
|
+
function stopDaemon() {
|
|
1705
|
+
try {
|
|
1706
|
+
if (existsSync$1(DAEMON_PID_FILE)) {
|
|
1707
|
+
logger.debug("[DAEMON] Stopping daemon");
|
|
1708
|
+
process.kill(parseInt(readFileSync$1(DAEMON_PID_FILE, "utf-8")), "SIGTERM");
|
|
1709
|
+
unlinkSync(DAEMON_PID_FILE);
|
|
1710
|
+
}
|
|
1711
|
+
} catch (error) {
|
|
1712
|
+
logger.debug("[DAEMON] Error cleaning up PID file", error);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
function trimIdent(text) {
|
|
1717
|
+
const lines = text.split("\n");
|
|
1718
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
1719
|
+
lines.shift();
|
|
1720
|
+
}
|
|
1721
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") {
|
|
1722
|
+
lines.pop();
|
|
1723
|
+
}
|
|
1724
|
+
const minSpaces = lines.reduce((min, line) => {
|
|
1725
|
+
if (line.trim() === "") {
|
|
1726
|
+
return min;
|
|
1727
|
+
}
|
|
1728
|
+
const leadingSpaces = line.match(/^\s*/)[0].length;
|
|
1729
|
+
return Math.min(min, leadingSpaces);
|
|
1730
|
+
}, Infinity);
|
|
1731
|
+
const trimmedLines = lines.map((line) => line.slice(minSpaces));
|
|
1732
|
+
return trimmedLines.join("\n");
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
const PLIST_LABEL$1 = "com.happy-cli.daemon";
|
|
1736
|
+
const PLIST_FILE$1 = `/Library/LaunchDaemons/${PLIST_LABEL$1}.plist`;
|
|
1737
|
+
const USER_HOME = process.env.HOME || process.env.USERPROFILE;
|
|
1738
|
+
async function install$1() {
|
|
1739
|
+
try {
|
|
1740
|
+
if (existsSync$1(PLIST_FILE$1)) {
|
|
1741
|
+
logger.info("Daemon plist already exists. Uninstalling first...");
|
|
1742
|
+
execSync(`launchctl unload ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1743
|
+
}
|
|
1744
|
+
const happyPath = process.argv[0];
|
|
1745
|
+
const scriptPath = process.argv[1];
|
|
1746
|
+
const plistContent = trimIdent(`
|
|
1747
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
1748
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
1749
|
+
<plist version="1.0">
|
|
1750
|
+
<dict>
|
|
1751
|
+
<key>Label</key>
|
|
1752
|
+
<string>${PLIST_LABEL$1}</string>
|
|
1753
|
+
|
|
1754
|
+
<key>ProgramArguments</key>
|
|
1755
|
+
<array>
|
|
1756
|
+
<string>${happyPath}</string>
|
|
1757
|
+
<string>${scriptPath}</string>
|
|
1758
|
+
<string>happy-daemon</string>
|
|
1759
|
+
</array>
|
|
1760
|
+
|
|
1761
|
+
<key>EnvironmentVariables</key>
|
|
1762
|
+
<dict>
|
|
1763
|
+
<key>HAPPY_DAEMON_MODE</key>
|
|
1764
|
+
<string>true</string>
|
|
1765
|
+
</dict>
|
|
1766
|
+
|
|
1767
|
+
<key>RunAtLoad</key>
|
|
1768
|
+
<true/>
|
|
1769
|
+
|
|
1770
|
+
<key>KeepAlive</key>
|
|
1771
|
+
<true/>
|
|
1772
|
+
|
|
1773
|
+
<key>StandardErrorPath</key>
|
|
1774
|
+
<string>${USER_HOME}/.happy/daemon.err</string>
|
|
1775
|
+
|
|
1776
|
+
<key>StandardOutPath</key>
|
|
1777
|
+
<string>${USER_HOME}/.happy/daemon.log</string>
|
|
1778
|
+
|
|
1779
|
+
<key>WorkingDirectory</key>
|
|
1780
|
+
<string>/tmp</string>
|
|
1781
|
+
</dict>
|
|
1782
|
+
</plist>
|
|
1783
|
+
`);
|
|
1784
|
+
writeFileSync(PLIST_FILE$1, plistContent);
|
|
1785
|
+
chmodSync(PLIST_FILE$1, 420);
|
|
1786
|
+
logger.info(`Created daemon plist at ${PLIST_FILE$1}`);
|
|
1787
|
+
execSync(`launchctl load ${PLIST_FILE$1}`, { stdio: "inherit" });
|
|
1788
|
+
logger.info("Daemon installed and started successfully");
|
|
1789
|
+
logger.info("Check logs at ~/.happy/daemon.log");
|
|
1790
|
+
} catch (error) {
|
|
1791
|
+
logger.debug("Failed to install daemon:", error);
|
|
1792
|
+
throw error;
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
async function install() {
|
|
1797
|
+
if (process.platform !== "darwin") {
|
|
1798
|
+
throw new Error("Daemon installation is currently only supported on macOS");
|
|
1799
|
+
}
|
|
1800
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1801
|
+
throw new Error("Daemon installation requires sudo privileges. Please run with sudo.");
|
|
1802
|
+
}
|
|
1803
|
+
logger.info("Installing Happy CLI daemon for macOS...");
|
|
1804
|
+
await install$1();
|
|
1805
|
+
}
|
|
1806
|
+
|
|
1807
|
+
const PLIST_LABEL = "com.happy-cli.daemon";
|
|
1808
|
+
const PLIST_FILE = `/Library/LaunchDaemons/${PLIST_LABEL}.plist`;
|
|
1809
|
+
async function uninstall$1() {
|
|
1810
|
+
try {
|
|
1811
|
+
if (!existsSync$1(PLIST_FILE)) {
|
|
1812
|
+
logger.info("Daemon plist not found. Nothing to uninstall.");
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1815
|
+
try {
|
|
1816
|
+
execSync(`launchctl unload ${PLIST_FILE}`, { stdio: "inherit" });
|
|
1817
|
+
logger.info("Daemon stopped successfully");
|
|
1818
|
+
} catch (error) {
|
|
1819
|
+
logger.info("Failed to unload daemon (it might not be running)");
|
|
1820
|
+
}
|
|
1821
|
+
unlinkSync(PLIST_FILE);
|
|
1822
|
+
logger.info(`Removed daemon plist from ${PLIST_FILE}`);
|
|
1823
|
+
logger.info("Daemon uninstalled successfully");
|
|
1824
|
+
} catch (error) {
|
|
1825
|
+
logger.debug("Failed to uninstall daemon:", error);
|
|
1826
|
+
throw error;
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
|
|
1830
|
+
async function uninstall() {
|
|
1831
|
+
if (process.platform !== "darwin") {
|
|
1832
|
+
throw new Error("Daemon uninstallation is currently only supported on macOS");
|
|
1833
|
+
}
|
|
1834
|
+
if (process.getuid && process.getuid() !== 0) {
|
|
1835
|
+
throw new Error("Daemon uninstallation requires sudo privileges. Please run with sudo.");
|
|
1836
|
+
}
|
|
1837
|
+
logger.info("Uninstalling Happy CLI daemon for macOS...");
|
|
1838
|
+
await uninstall$1();
|
|
1839
|
+
}
|
|
1840
|
+
|
|
1869
1841
|
(async () => {
|
|
1870
1842
|
const args = process.argv.slice(2);
|
|
1871
1843
|
let installationLocation = args.includes("--local") || process.env.HANDY_LOCAL ? "local" : "global";
|
|
@@ -1884,25 +1856,64 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1884
1856
|
process.exit(1);
|
|
1885
1857
|
}
|
|
1886
1858
|
return;
|
|
1887
|
-
} else if (subcommand === "
|
|
1888
|
-
|
|
1859
|
+
} else if (subcommand === "daemon") {
|
|
1860
|
+
const daemonSubcommand = args[1];
|
|
1861
|
+
if (daemonSubcommand === "start") {
|
|
1862
|
+
await startDaemon();
|
|
1863
|
+
process.exit(0);
|
|
1864
|
+
} else if (daemonSubcommand === "stop") {
|
|
1865
|
+
await stopDaemon();
|
|
1866
|
+
process.exit(0);
|
|
1867
|
+
} else if (daemonSubcommand === "install") {
|
|
1868
|
+
try {
|
|
1869
|
+
await install();
|
|
1870
|
+
} catch (error) {
|
|
1871
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1872
|
+
process.exit(1);
|
|
1873
|
+
}
|
|
1874
|
+
} else if (daemonSubcommand === "uninstall") {
|
|
1875
|
+
try {
|
|
1876
|
+
await uninstall();
|
|
1877
|
+
} catch (error) {
|
|
1878
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1879
|
+
process.exit(1);
|
|
1880
|
+
}
|
|
1881
|
+
} else {
|
|
1882
|
+
console.log(`
|
|
1883
|
+
${chalk.bold("happy daemon")} - Daemon management
|
|
1884
|
+
|
|
1885
|
+
${chalk.bold("Usage:")}
|
|
1886
|
+
happy daemon start Start the daemon
|
|
1887
|
+
happy daemon stop Stop the daemon
|
|
1888
|
+
sudo happy daemon install Install the daemon (requires sudo)
|
|
1889
|
+
sudo happy daemon uninstall Uninstall the daemon (requires sudo)
|
|
1890
|
+
|
|
1891
|
+
${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
|
|
1892
|
+
Currently only supported on macOS.
|
|
1893
|
+
`);
|
|
1894
|
+
}
|
|
1889
1895
|
return;
|
|
1890
1896
|
} else {
|
|
1891
1897
|
const options = {};
|
|
1892
1898
|
let showHelp = false;
|
|
1893
1899
|
let showVersion = false;
|
|
1900
|
+
let forceAuth = false;
|
|
1894
1901
|
for (let i = 0; i < args.length; i++) {
|
|
1895
1902
|
const arg = args[i];
|
|
1896
1903
|
if (arg === "-h" || arg === "--help") {
|
|
1897
1904
|
showHelp = true;
|
|
1898
1905
|
} else if (arg === "-v" || arg === "--version") {
|
|
1899
1906
|
showVersion = true;
|
|
1907
|
+
} else if (arg === "--auth" || arg === "--login") {
|
|
1908
|
+
forceAuth = true;
|
|
1900
1909
|
} else if (arg === "-m" || arg === "--model") {
|
|
1901
1910
|
options.model = args[++i];
|
|
1902
1911
|
} else if (arg === "-p" || arg === "--permission-mode") {
|
|
1903
|
-
options.permissionMode = args[++i];
|
|
1912
|
+
options.permissionMode = z$1.enum(["auto", "default", "plan"]).parse(args[++i]);
|
|
1904
1913
|
} else if (arg === "--local") {
|
|
1905
1914
|
i++;
|
|
1915
|
+
} else if (arg === "--happy-starting-mode") {
|
|
1916
|
+
options.startingMode = z$1.enum(["interactive", "remote"]).parse(args[++i]);
|
|
1906
1917
|
} else {
|
|
1907
1918
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
1908
1919
|
process.exit(1);
|
|
@@ -1915,35 +1926,36 @@ ${chalk.bold("happy")} - Claude Code session sharing
|
|
|
1915
1926
|
${chalk.bold("Usage:")}
|
|
1916
1927
|
happy [options]
|
|
1917
1928
|
happy logout Logs out of your account and removes data directory
|
|
1918
|
-
happy login Show your secret QR code
|
|
1919
|
-
happy auth Same as login
|
|
1920
1929
|
|
|
1921
1930
|
${chalk.bold("Options:")}
|
|
1922
1931
|
-h, --help Show this help message
|
|
1923
1932
|
-v, --version Show version
|
|
1924
1933
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
1925
1934
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
1935
|
+
--auth, --login Force re-authentication
|
|
1926
1936
|
|
|
1927
1937
|
[Advanced]
|
|
1928
1938
|
--local < global | local >
|
|
1929
1939
|
Will use .happy folder in the current directory for storing your private key and debug logs.
|
|
1930
1940
|
You will require re-login each time you run this in a new directory.
|
|
1931
|
-
|
|
1941
|
+
--happy-starting-mode <interactive|remote>
|
|
1942
|
+
Set the starting mode for new sessions (default: remote)
|
|
1932
1943
|
|
|
1933
1944
|
${chalk.bold("Examples:")}
|
|
1934
1945
|
happy Start a session with default settings
|
|
1935
1946
|
happy -m opus Use Claude Opus model
|
|
1936
1947
|
happy -p plan Use plan permission mode
|
|
1948
|
+
happy --auth Force re-authentication before starting session
|
|
1937
1949
|
happy logout Logs out of your account and removes data directory
|
|
1938
1950
|
`);
|
|
1939
1951
|
process.exit(0);
|
|
1940
1952
|
}
|
|
1941
1953
|
if (showVersion) {
|
|
1942
|
-
console.log(
|
|
1954
|
+
console.log(packageJson.version);
|
|
1943
1955
|
process.exit(0);
|
|
1944
1956
|
}
|
|
1945
1957
|
let credentials = await readCredentials();
|
|
1946
|
-
if (!credentials) {
|
|
1958
|
+
if (!credentials || forceAuth) {
|
|
1947
1959
|
let res = await doAuth();
|
|
1948
1960
|
if (!res) {
|
|
1949
1961
|
process.exit(1);
|