happy-coder 0.1.7 → 0.1.9
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 +354 -917
- package/dist/index.mjs +280 -843
- package/dist/install-B2r_gX72.cjs +109 -0
- package/dist/install-HKe7dyS4.mjs +107 -0
- package/dist/lib.cjs +32 -0
- package/dist/lib.d.cts +727 -0
- package/dist/lib.d.mts +727 -0
- package/dist/lib.mjs +14 -0
- package/dist/run-FBXkmmN7.mjs +32 -0
- package/dist/run-q2To6b-c.cjs +34 -0
- package/dist/types-fXgEaaqP.mjs +861 -0
- package/dist/types-mykDX2xe.cjs +872 -0
- package/dist/uninstall-C42CoSCI.cjs +53 -0
- package/dist/uninstall-CLkTtlMv.mjs +51 -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,28 @@
|
|
|
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, b as initializeConfiguration, i as initLoggerWithGlobalConfiguration } from './types-fXgEaaqP.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 } 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 tweetnacl from 'tweetnacl';
|
|
20
|
+
import axios from 'axios';
|
|
22
21
|
import qrcode from 'qrcode-terminal';
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
happyDir;
|
|
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
|
-
}
|
|
22
|
+
import 'fs';
|
|
23
|
+
import 'node:events';
|
|
24
|
+
import 'socket.io-client';
|
|
25
|
+
import 'expo-server-sdk';
|
|
714
26
|
|
|
715
27
|
function formatClaudeMessage(message, onAssistantResult) {
|
|
716
28
|
logger.debugLargeJson("[CLAUDE] Message from non interactive & remote mode:", message);
|
|
@@ -1199,105 +511,6 @@ class InvalidateSync {
|
|
|
1199
511
|
};
|
|
1200
512
|
}
|
|
1201
513
|
|
|
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
514
|
function createSessionScanner(opts) {
|
|
1302
515
|
const projectName = resolve(opts.workingDirectory).replace(/\//g, "-");
|
|
1303
516
|
const projectDir = join(homedir(), ".claude", "projects", projectName);
|
|
@@ -1306,6 +519,7 @@ function createSessionScanner(opts) {
|
|
|
1306
519
|
let currentSessionId = null;
|
|
1307
520
|
let currentSessionWatcherAbortController = null;
|
|
1308
521
|
let processedMessages = /* @__PURE__ */ new Set();
|
|
522
|
+
let seenRemoteUserMessageCounters = /* @__PURE__ */ new Map();
|
|
1309
523
|
const sync = new InvalidateSync(async () => {
|
|
1310
524
|
let sessions = [];
|
|
1311
525
|
for (let p of pendingSessions) {
|
|
@@ -1338,6 +552,13 @@ function createSessionScanner(opts) {
|
|
|
1338
552
|
processedMessages.add(key);
|
|
1339
553
|
logger.debugLargeJson(`[SESSION_SCANNER] Processing message`, parsed.data);
|
|
1340
554
|
logger.debug(`[SESSION_SCANNER] Message key (new): ${key}`);
|
|
555
|
+
if (parsed.data.type === "user" && typeof parsed.data.message.content === "string") {
|
|
556
|
+
const currentCounter = seenRemoteUserMessageCounters.get(parsed.data.message.content);
|
|
557
|
+
if (currentCounter && currentCounter > 0) {
|
|
558
|
+
seenRemoteUserMessageCounters.set(parsed.data.message.content, currentCounter - 1);
|
|
559
|
+
continue;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
1341
562
|
opts.onMessage(parsed.data);
|
|
1342
563
|
} catch (e) {
|
|
1343
564
|
continue;
|
|
@@ -1392,8 +613,12 @@ function createSessionScanner(opts) {
|
|
|
1392
613
|
if (currentSessionId) {
|
|
1393
614
|
pendingSessions.add(currentSessionId);
|
|
1394
615
|
}
|
|
616
|
+
logger.debug(`[SESSION_SCANNER] New session: ${sessionId}`);
|
|
1395
617
|
currentSessionId = sessionId;
|
|
1396
618
|
sync.invalidate();
|
|
619
|
+
},
|
|
620
|
+
onRemoteUserMessageForDeduplication: (messageContent) => {
|
|
621
|
+
seenRemoteUserMessageCounters.set(messageContent, (seenRemoteUserMessageCounters.get(messageContent) || 0) + 1);
|
|
1397
622
|
}
|
|
1398
623
|
};
|
|
1399
624
|
}
|
|
@@ -1427,32 +652,24 @@ function sortKeys(value) {
|
|
|
1427
652
|
}
|
|
1428
653
|
|
|
1429
654
|
async function loop(opts) {
|
|
1430
|
-
let mode = "interactive";
|
|
655
|
+
let mode = opts.startingMode ?? "interactive";
|
|
1431
656
|
let currentMessageQueue = new MessageQueue();
|
|
1432
657
|
let sessionId = null;
|
|
1433
658
|
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
659
|
const sessionScanner = createSessionScanner({
|
|
1444
660
|
workingDirectory: opts.path,
|
|
1445
661
|
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
662
|
opts.session.sendClaudeSessionMessage(message);
|
|
1454
663
|
}
|
|
1455
664
|
});
|
|
665
|
+
opts.session.onUserMessage((message) => {
|
|
666
|
+
sessionScanner.onRemoteUserMessageForDeduplication(message.content.text);
|
|
667
|
+
currentMessageQueue.push(message.content.text);
|
|
668
|
+
logger.debugLargeJson("User message pushed to queue:", message);
|
|
669
|
+
if (onMessage) {
|
|
670
|
+
onMessage();
|
|
671
|
+
}
|
|
672
|
+
});
|
|
1456
673
|
let onSessionFound = (newSessionId) => {
|
|
1457
674
|
sessionId = newSessionId;
|
|
1458
675
|
sessionScanner.onNewSession(newSessionId);
|
|
@@ -1507,10 +724,14 @@ async function loop(opts) {
|
|
|
1507
724
|
mode = "interactive";
|
|
1508
725
|
remoteAbortController.abort();
|
|
1509
726
|
}
|
|
1510
|
-
process.stdin.
|
|
727
|
+
if (process.stdin.isTTY) {
|
|
728
|
+
process.stdin.setRawMode(false);
|
|
729
|
+
}
|
|
1511
730
|
};
|
|
1512
731
|
process.stdin.resume();
|
|
1513
|
-
process.stdin.
|
|
732
|
+
if (process.stdin.isTTY) {
|
|
733
|
+
process.stdin.setRawMode(true);
|
|
734
|
+
}
|
|
1514
735
|
process.stdin.setEncoding("utf8");
|
|
1515
736
|
process.stdin.on("data", abortHandler);
|
|
1516
737
|
try {
|
|
@@ -1528,7 +749,9 @@ async function loop(opts) {
|
|
|
1528
749
|
});
|
|
1529
750
|
} finally {
|
|
1530
751
|
process.stdin.off("data", abortHandler);
|
|
1531
|
-
process.stdin.
|
|
752
|
+
if (process.stdin.isTTY) {
|
|
753
|
+
process.stdin.setRawMode(false);
|
|
754
|
+
}
|
|
1532
755
|
currentMessageQueue.close();
|
|
1533
756
|
currentMessageQueue = new MessageQueue();
|
|
1534
757
|
}
|
|
@@ -1639,16 +862,187 @@ class InterruptController {
|
|
|
1639
862
|
}
|
|
1640
863
|
}
|
|
1641
864
|
|
|
865
|
+
var version = "0.1.9";
|
|
866
|
+
var packageJson = {
|
|
867
|
+
version: version};
|
|
868
|
+
|
|
869
|
+
async function startAnthropicActivityProxy(onClaudeActivity) {
|
|
870
|
+
const requestTimeouts = /* @__PURE__ */ new Map();
|
|
871
|
+
let requestCounter = 0;
|
|
872
|
+
let idleTimer = null;
|
|
873
|
+
const maxTimeBeforeIdle = 50;
|
|
874
|
+
const requestTimeout = 5 * 60 * 1e3;
|
|
875
|
+
const cleanupRequest = (requestId, reason) => {
|
|
876
|
+
const timeout = requestTimeouts.get(requestId);
|
|
877
|
+
if (timeout) {
|
|
878
|
+
clearTimeout(timeout);
|
|
879
|
+
requestTimeouts.delete(requestId);
|
|
880
|
+
logger.debug(`[AnthropicProxy #${requestId}] Cleaned up (${reason}), active requests: ${requestTimeouts.size}`);
|
|
881
|
+
claudeDidSomeWork();
|
|
882
|
+
}
|
|
883
|
+
};
|
|
884
|
+
const claudeDidSomeWork = () => {
|
|
885
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
886
|
+
if (requestTimeouts.size === 0) {
|
|
887
|
+
idleTimer = setTimeout(() => {
|
|
888
|
+
logger.debug(`[AnthropicProxy] Idle for ${maxTimeBeforeIdle}ms, active requests: ${requestTimeouts.size}`);
|
|
889
|
+
onClaudeActivity("idle");
|
|
890
|
+
}, maxTimeBeforeIdle);
|
|
891
|
+
}
|
|
892
|
+
};
|
|
893
|
+
const server = createServer((req, res) => {
|
|
894
|
+
const requestId = ++requestCounter;
|
|
895
|
+
const isAnthropicRequest = req.headers.host === "api.anthropic.com" || req.url?.includes("anthropic.com");
|
|
896
|
+
if (isAnthropicRequest) {
|
|
897
|
+
const timeout = setTimeout(() => {
|
|
898
|
+
logger.debug(`[AnthropicProxy #${requestId}] Request timeout after ${requestTimeout}ms`);
|
|
899
|
+
cleanupRequest(requestId, "timeout");
|
|
900
|
+
}, requestTimeout);
|
|
901
|
+
requestTimeouts.set(requestId, timeout);
|
|
902
|
+
onClaudeActivity("working");
|
|
903
|
+
logger.debug(`[AnthropicProxy #${requestId}] Anthropic request: ${req.method} ${req.url}, active requests: ${requestTimeouts.size}`);
|
|
904
|
+
}
|
|
905
|
+
const chunks = [];
|
|
906
|
+
req.on("data", (chunk) => {
|
|
907
|
+
chunks.push(chunk);
|
|
908
|
+
if (isAnthropicRequest) {
|
|
909
|
+
claudeDidSomeWork();
|
|
910
|
+
}
|
|
911
|
+
});
|
|
912
|
+
req.on("end", () => {
|
|
913
|
+
const body = Buffer.concat(chunks);
|
|
914
|
+
let targetUrl;
|
|
915
|
+
if (isAnthropicRequest) {
|
|
916
|
+
targetUrl = new URL$1(req.url || "/", "https://api.anthropic.com");
|
|
917
|
+
} else {
|
|
918
|
+
const protocol = req.headers["x-forwarded-proto"] || "https";
|
|
919
|
+
const host = req.headers.host || "localhost";
|
|
920
|
+
targetUrl = new URL$1(req.url || "/", `${protocol}://${host}`);
|
|
921
|
+
}
|
|
922
|
+
const options = {
|
|
923
|
+
hostname: targetUrl.hostname,
|
|
924
|
+
port: targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
|
|
925
|
+
path: targetUrl.pathname + targetUrl.search,
|
|
926
|
+
method: req.method,
|
|
927
|
+
headers: {
|
|
928
|
+
...req.headers,
|
|
929
|
+
host: targetUrl.hostname
|
|
930
|
+
}
|
|
931
|
+
};
|
|
932
|
+
const requestMethod = targetUrl.protocol === "https:" ? request : request$1;
|
|
933
|
+
const proxyReq = requestMethod(options, (proxyRes) => {
|
|
934
|
+
res.writeHead(proxyRes.statusCode || 200, proxyRes.headers);
|
|
935
|
+
proxyRes.pipe(res);
|
|
936
|
+
proxyRes.on("end", () => {
|
|
937
|
+
if (isAnthropicRequest) {
|
|
938
|
+
cleanupRequest(requestId, "completed");
|
|
939
|
+
}
|
|
940
|
+
});
|
|
941
|
+
});
|
|
942
|
+
proxyReq.on("error", (error) => {
|
|
943
|
+
if (isAnthropicRequest) {
|
|
944
|
+
cleanupRequest(requestId, `error: ${error.message}`);
|
|
945
|
+
} else {
|
|
946
|
+
logger.debug(`[AnthropicProxy #${requestId}] Error:`, error.message);
|
|
947
|
+
}
|
|
948
|
+
res.writeHead(502);
|
|
949
|
+
res.end("Bad Gateway");
|
|
950
|
+
});
|
|
951
|
+
if (body.length > 0) {
|
|
952
|
+
proxyReq.write(body);
|
|
953
|
+
}
|
|
954
|
+
proxyReq.end();
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
server.on("connect", (req, clientSocket, head) => {
|
|
958
|
+
const requestId = ++requestCounter;
|
|
959
|
+
const [hostname, port] = req.url?.split(":") || ["", "443"];
|
|
960
|
+
const isAnthropicRequest = hostname === "api.anthropic.com";
|
|
961
|
+
if (isAnthropicRequest) {
|
|
962
|
+
const timeout = setTimeout(() => {
|
|
963
|
+
logger.debug(`[AnthropicProxy #${requestId}] CONNECT timeout after ${requestTimeout}ms`);
|
|
964
|
+
cleanupRequest(requestId, "timeout");
|
|
965
|
+
}, requestTimeout);
|
|
966
|
+
requestTimeouts.set(requestId, timeout);
|
|
967
|
+
onClaudeActivity("working");
|
|
968
|
+
logger.debug(`[AnthropicProxy #${requestId}] CONNECT to api.anthropic.com, active requests: ${requestTimeouts.size}`);
|
|
969
|
+
}
|
|
970
|
+
const serverSocket = net.connect(parseInt(port) || 443, hostname, () => {
|
|
971
|
+
clientSocket.write("HTTP/1.1 200 Connection Established\r\n\r\n");
|
|
972
|
+
serverSocket.write(head);
|
|
973
|
+
serverSocket.pipe(clientSocket);
|
|
974
|
+
clientSocket.pipe(serverSocket);
|
|
975
|
+
});
|
|
976
|
+
const cleanup = () => {
|
|
977
|
+
if (isAnthropicRequest) {
|
|
978
|
+
cleanupRequest(requestId, "CONNECT closed");
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
serverSocket.on("error", (err) => {
|
|
982
|
+
logger.debug(`[AnthropicProxy #${requestId}] CONNECT error:`, err.message);
|
|
983
|
+
clientSocket.end();
|
|
984
|
+
cleanup();
|
|
985
|
+
});
|
|
986
|
+
clientSocket.on("error", cleanup);
|
|
987
|
+
clientSocket.on("end", cleanup);
|
|
988
|
+
serverSocket.on("end", cleanup);
|
|
989
|
+
});
|
|
990
|
+
const url = await new Promise((resolve) => {
|
|
991
|
+
server.listen(0, "127.0.0.1", () => {
|
|
992
|
+
const addr = server.address();
|
|
993
|
+
if (addr && typeof addr === "object") {
|
|
994
|
+
resolve(`http://127.0.0.1:${addr.port}`);
|
|
995
|
+
}
|
|
996
|
+
});
|
|
997
|
+
});
|
|
998
|
+
logger.debug(`[AnthropicProxy] Started at ${url}`);
|
|
999
|
+
return {
|
|
1000
|
+
url,
|
|
1001
|
+
cleanup: () => {
|
|
1002
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
1003
|
+
for (const [requestId, timeout] of requestTimeouts) {
|
|
1004
|
+
clearTimeout(timeout);
|
|
1005
|
+
logger.debug(`[AnthropicProxy] Cleaning up timeout for request #${requestId}`);
|
|
1006
|
+
}
|
|
1007
|
+
requestTimeouts.clear();
|
|
1008
|
+
if (requestTimeouts.size > 0) {
|
|
1009
|
+
logger.debug(`[AnthropicProxy] Warning: ${requestTimeouts.size} active requests still pending at cleanup:`, Array.from(requestTimeouts.keys()));
|
|
1010
|
+
}
|
|
1011
|
+
server.close();
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1642
1016
|
async function start(credentials, options = {}) {
|
|
1643
1017
|
const workingDirectory = process.cwd();
|
|
1644
1018
|
const sessionTag = randomUUID();
|
|
1645
1019
|
const api = new ApiClient(credentials.token, credentials.secret);
|
|
1646
1020
|
let state = {};
|
|
1647
|
-
let metadata = { path: workingDirectory, host: os.hostname() };
|
|
1021
|
+
let metadata = { path: workingDirectory, host: os.hostname(), version: packageJson.version };
|
|
1648
1022
|
const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1649
1023
|
logger.debug(`Session created: ${response.id}`);
|
|
1650
1024
|
const session = api.session(response);
|
|
1651
1025
|
const pushClient = api.push();
|
|
1026
|
+
let thinking = false;
|
|
1027
|
+
let pingInterval = setInterval(() => {
|
|
1028
|
+
session.keepAlive(thinking);
|
|
1029
|
+
}, 2e3);
|
|
1030
|
+
const antropicActivityProxy = await startAnthropicActivityProxy(
|
|
1031
|
+
(activity) => {
|
|
1032
|
+
const newThinking = activity === "working";
|
|
1033
|
+
if (newThinking !== thinking) {
|
|
1034
|
+
thinking = newThinking;
|
|
1035
|
+
logger.debug(`[PING] Thinking state changed: ${thinking}`);
|
|
1036
|
+
session.keepAlive(thinking);
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
);
|
|
1040
|
+
process.env.HTTP_PROXY = antropicActivityProxy.url;
|
|
1041
|
+
process.env.HTTPS_PROXY = antropicActivityProxy.url;
|
|
1042
|
+
logger.debug(`[AnthropicProxy] Set HTTP_PROXY and HTTPS_PROXY to ${antropicActivityProxy.url}`);
|
|
1043
|
+
const logPath = await logger.logFilePathPromise;
|
|
1044
|
+
logger.info(`Session: ${response.id}`);
|
|
1045
|
+
logger.infoDeveloper(`Logs: ${logPath}`);
|
|
1652
1046
|
const interruptController = new InterruptController();
|
|
1653
1047
|
let requests = /* @__PURE__ */ new Map();
|
|
1654
1048
|
const permissionServer = await startPermissionServerV2(async (request) => {
|
|
@@ -1724,10 +1118,6 @@ async function start(credentials, options = {}) {
|
|
|
1724
1118
|
logger.info("Abort request - interrupting Claude");
|
|
1725
1119
|
await interruptController.interrupt();
|
|
1726
1120
|
});
|
|
1727
|
-
let thinking = false;
|
|
1728
|
-
const pingInterval = setInterval(() => {
|
|
1729
|
-
session.keepAlive(thinking);
|
|
1730
|
-
}, 15e3);
|
|
1731
1121
|
const onAssistantResult = async (result) => {
|
|
1732
1122
|
try {
|
|
1733
1123
|
const summary = "result" in result && result.result ? result.result.substring(0, 100) + (result.result.length > 100 ? "..." : "") : "";
|
|
@@ -1751,6 +1141,7 @@ async function start(credentials, options = {}) {
|
|
|
1751
1141
|
path: workingDirectory,
|
|
1752
1142
|
model: options.model,
|
|
1753
1143
|
permissionMode: options.permissionMode,
|
|
1144
|
+
startingMode: options.startingMode,
|
|
1754
1145
|
mcpServers: {
|
|
1755
1146
|
"permission": {
|
|
1756
1147
|
type: "http",
|
|
@@ -1758,15 +1149,15 @@ async function start(credentials, options = {}) {
|
|
|
1758
1149
|
}
|
|
1759
1150
|
},
|
|
1760
1151
|
permissionPromptToolName: "mcp__permission__" + permissionServer.toolName,
|
|
1761
|
-
onThinking: (t) => {
|
|
1762
|
-
thinking = t;
|
|
1763
|
-
session.keepAlive(t);
|
|
1764
|
-
},
|
|
1765
1152
|
session,
|
|
1766
1153
|
onAssistantResult,
|
|
1767
1154
|
interruptController
|
|
1768
1155
|
});
|
|
1769
1156
|
clearInterval(pingInterval);
|
|
1157
|
+
if (antropicActivityProxy) {
|
|
1158
|
+
logger.info("[AnthropicProxy] Shutting down activity monitoring proxy");
|
|
1159
|
+
antropicActivityProxy.cleanup();
|
|
1160
|
+
}
|
|
1770
1161
|
process.exit(0);
|
|
1771
1162
|
}
|
|
1772
1163
|
|
|
@@ -1824,7 +1215,12 @@ async function doAuth() {
|
|
|
1824
1215
|
return null;
|
|
1825
1216
|
}
|
|
1826
1217
|
console.log("Please, authenticate using mobile app");
|
|
1827
|
-
|
|
1218
|
+
const authUrl = "happy://terminal?" + encodeBase64Url(keypair.publicKey);
|
|
1219
|
+
displayQRCode(authUrl);
|
|
1220
|
+
if (process.env.DEBUG === "1") {
|
|
1221
|
+
console.log("\n\u{1F4CB} For manual entry, copy this URL:");
|
|
1222
|
+
console.log(authUrl);
|
|
1223
|
+
}
|
|
1828
1224
|
let credentials = null;
|
|
1829
1225
|
while (true) {
|
|
1830
1226
|
try {
|
|
@@ -1884,25 +1280,63 @@ function decryptWithEphemeralKey(encryptedBundle, recipientSecretKey) {
|
|
|
1884
1280
|
process.exit(1);
|
|
1885
1281
|
}
|
|
1886
1282
|
return;
|
|
1887
|
-
} else if (subcommand === "
|
|
1888
|
-
|
|
1283
|
+
} else if (subcommand === "daemon") {
|
|
1284
|
+
if (process.env.HAPPY_DAEMON_MODE) {
|
|
1285
|
+
const { run } = await import('./run-FBXkmmN7.mjs');
|
|
1286
|
+
await run();
|
|
1287
|
+
} else {
|
|
1288
|
+
const daemonSubcommand = args[1];
|
|
1289
|
+
if (daemonSubcommand === "install") {
|
|
1290
|
+
const { install } = await import('./install-HKe7dyS4.mjs');
|
|
1291
|
+
try {
|
|
1292
|
+
await install();
|
|
1293
|
+
} catch (error) {
|
|
1294
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1295
|
+
process.exit(1);
|
|
1296
|
+
}
|
|
1297
|
+
} else if (daemonSubcommand === "uninstall") {
|
|
1298
|
+
const { uninstall } = await import('./uninstall-CLkTtlMv.mjs');
|
|
1299
|
+
try {
|
|
1300
|
+
await uninstall();
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
console.error(chalk.red("Error:"), error instanceof Error ? error.message : "Unknown error");
|
|
1303
|
+
process.exit(1);
|
|
1304
|
+
}
|
|
1305
|
+
} else {
|
|
1306
|
+
console.log(`
|
|
1307
|
+
${chalk.bold("happy daemon")} - Daemon management
|
|
1308
|
+
|
|
1309
|
+
${chalk.bold("Usage:")}
|
|
1310
|
+
sudo happy daemon install Install the daemon (requires sudo)
|
|
1311
|
+
sudo happy daemon uninstall Uninstall the daemon (requires sudo)
|
|
1312
|
+
|
|
1313
|
+
${chalk.bold("Note:")} The daemon runs in the background and provides persistent services.
|
|
1314
|
+
Currently only supported on macOS.
|
|
1315
|
+
`);
|
|
1316
|
+
}
|
|
1317
|
+
}
|
|
1889
1318
|
return;
|
|
1890
1319
|
} else {
|
|
1891
1320
|
const options = {};
|
|
1892
1321
|
let showHelp = false;
|
|
1893
1322
|
let showVersion = false;
|
|
1323
|
+
let forceAuth = false;
|
|
1894
1324
|
for (let i = 0; i < args.length; i++) {
|
|
1895
1325
|
const arg = args[i];
|
|
1896
1326
|
if (arg === "-h" || arg === "--help") {
|
|
1897
1327
|
showHelp = true;
|
|
1898
1328
|
} else if (arg === "-v" || arg === "--version") {
|
|
1899
1329
|
showVersion = true;
|
|
1330
|
+
} else if (arg === "--auth" || arg === "--login") {
|
|
1331
|
+
forceAuth = true;
|
|
1900
1332
|
} else if (arg === "-m" || arg === "--model") {
|
|
1901
1333
|
options.model = args[++i];
|
|
1902
1334
|
} else if (arg === "-p" || arg === "--permission-mode") {
|
|
1903
|
-
options.permissionMode = args[++i];
|
|
1335
|
+
options.permissionMode = z$1.enum(["auto", "default", "plan"]).parse(args[++i]);
|
|
1904
1336
|
} else if (arg === "--local") {
|
|
1905
1337
|
i++;
|
|
1338
|
+
} else if (arg === "--happy-starting-mode") {
|
|
1339
|
+
options.startingMode = z$1.enum(["interactive", "remote"]).parse(args[++i]);
|
|
1906
1340
|
} else {
|
|
1907
1341
|
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
1908
1342
|
process.exit(1);
|
|
@@ -1915,35 +1349,38 @@ ${chalk.bold("happy")} - Claude Code session sharing
|
|
|
1915
1349
|
${chalk.bold("Usage:")}
|
|
1916
1350
|
happy [options]
|
|
1917
1351
|
happy logout Logs out of your account and removes data directory
|
|
1918
|
-
happy
|
|
1919
|
-
happy auth Same as login
|
|
1352
|
+
happy daemon Manage the background daemon (macOS only)
|
|
1920
1353
|
|
|
1921
1354
|
${chalk.bold("Options:")}
|
|
1922
1355
|
-h, --help Show this help message
|
|
1923
1356
|
-v, --version Show version
|
|
1924
1357
|
-m, --model <model> Claude model to use (default: sonnet)
|
|
1925
1358
|
-p, --permission-mode Permission mode: auto, default, or plan
|
|
1359
|
+
--auth, --login Force re-authentication
|
|
1926
1360
|
|
|
1927
1361
|
[Advanced]
|
|
1928
1362
|
--local < global | local >
|
|
1929
1363
|
Will use .happy folder in the current directory for storing your private key and debug logs.
|
|
1930
1364
|
You will require re-login each time you run this in a new directory.
|
|
1931
|
-
|
|
1365
|
+
|
|
1366
|
+
--happy-starting-mode <mode> Start in specified mode (interactive or remote)
|
|
1367
|
+
Default: interactive
|
|
1932
1368
|
|
|
1933
1369
|
${chalk.bold("Examples:")}
|
|
1934
1370
|
happy Start a session with default settings
|
|
1935
1371
|
happy -m opus Use Claude Opus model
|
|
1936
1372
|
happy -p plan Use plan permission mode
|
|
1373
|
+
happy --auth Force re-authentication before starting session
|
|
1937
1374
|
happy logout Logs out of your account and removes data directory
|
|
1938
1375
|
`);
|
|
1939
1376
|
process.exit(0);
|
|
1940
1377
|
}
|
|
1941
1378
|
if (showVersion) {
|
|
1942
|
-
console.log(
|
|
1379
|
+
console.log(packageJson.version);
|
|
1943
1380
|
process.exit(0);
|
|
1944
1381
|
}
|
|
1945
1382
|
let credentials = await readCredentials();
|
|
1946
|
-
if (!credentials) {
|
|
1383
|
+
if (!credentials || forceAuth) {
|
|
1947
1384
|
let res = await doAuth();
|
|
1948
1385
|
if (!res) {
|
|
1949
1386
|
process.exit(1);
|