happy-coder 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -0
- package/bin/happy +2 -0
- package/bin/happy.cmd +2 -0
- package/dist/auth/auth.d.ts +38 -0
- package/dist/auth/auth.js +76 -0
- package/dist/auth/auth.test.d.ts +7 -0
- package/dist/auth/auth.test.js +96 -0
- package/dist/auth/crypto.d.ts +25 -0
- package/dist/auth/crypto.js +36 -0
- package/dist/claude/claude.d.ts +54 -0
- package/dist/claude/claude.js +170 -0
- package/dist/claude/claude.test.d.ts +7 -0
- package/dist/claude/claude.test.js +130 -0
- package/dist/claude/types.d.ts +37 -0
- package/dist/claude/types.js +7 -0
- package/dist/commands/start.d.ts +38 -0
- package/dist/commands/start.js +161 -0
- package/dist/commands/start.test.d.ts +7 -0
- package/dist/commands/start.test.js +307 -0
- package/dist/handlers/message-handler.d.ts +65 -0
- package/dist/handlers/message-handler.js +187 -0
- package/dist/index.cjs +603 -0
- package/dist/index.d.cts +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/index.mjs +583 -0
- package/dist/session/service.d.ts +27 -0
- package/dist/session/service.js +93 -0
- package/dist/session/service.test.d.ts +7 -0
- package/dist/session/service.test.js +71 -0
- package/dist/session/types.d.ts +44 -0
- package/dist/session/types.js +4 -0
- package/dist/socket/client.d.ts +50 -0
- package/dist/socket/client.js +136 -0
- package/dist/socket/client.test.d.ts +7 -0
- package/dist/socket/client.test.js +74 -0
- package/dist/socket/types.d.ts +80 -0
- package/dist/socket/types.js +12 -0
- package/dist/utils/config.d.ts +22 -0
- package/dist/utils/config.js +23 -0
- package/dist/utils/logger.d.ts +26 -0
- package/dist/utils/logger.js +60 -0
- package/dist/utils/paths.d.ts +18 -0
- package/dist/utils/paths.js +24 -0
- package/dist/utils/qrcode.d.ts +19 -0
- package/dist/utils/qrcode.js +37 -0
- package/dist/utils/qrcode.test.d.ts +7 -0
- package/dist/utils/qrcode.test.js +14 -0
- package/package.json +60 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var axios = require('axios');
|
|
4
|
+
var fs = require('node:fs');
|
|
5
|
+
var promises = require('node:fs/promises');
|
|
6
|
+
var node_crypto = require('node:crypto');
|
|
7
|
+
var tweetnacl = require('tweetnacl');
|
|
8
|
+
var node_os = require('node:os');
|
|
9
|
+
var node_path = require('node:path');
|
|
10
|
+
var chalk = require('chalk');
|
|
11
|
+
var node_events = require('node:events');
|
|
12
|
+
var socket_ioClient = require('socket.io-client');
|
|
13
|
+
var zod = require('zod');
|
|
14
|
+
var qrcode = require('qrcode-terminal');
|
|
15
|
+
var node_child_process = require('node:child_process');
|
|
16
|
+
|
|
17
|
+
function _interopNamespaceDefault(e) {
|
|
18
|
+
var n = Object.create(null);
|
|
19
|
+
if (e) {
|
|
20
|
+
Object.keys(e).forEach(function (k) {
|
|
21
|
+
if (k !== 'default') {
|
|
22
|
+
var d = Object.getOwnPropertyDescriptor(e, k);
|
|
23
|
+
Object.defineProperty(n, k, d.get ? d : {
|
|
24
|
+
enumerable: true,
|
|
25
|
+
get: function () { return e[k]; }
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
n.default = e;
|
|
31
|
+
return Object.freeze(n);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
var fs__namespace = /*#__PURE__*/_interopNamespaceDefault(fs);
|
|
35
|
+
|
|
36
|
+
function encodeBase64(buffer) {
|
|
37
|
+
return Buffer.from(buffer).toString("base64");
|
|
38
|
+
}
|
|
39
|
+
function encodeBase64Url(buffer) {
|
|
40
|
+
return Buffer.from(buffer).toString("base64").replaceAll("+", "-").replaceAll("/", "_").replaceAll("=", "");
|
|
41
|
+
}
|
|
42
|
+
function decodeBase64(base64) {
|
|
43
|
+
return new Uint8Array(Buffer.from(base64, "base64"));
|
|
44
|
+
}
|
|
45
|
+
function getRandomBytes(size) {
|
|
46
|
+
return new Uint8Array(node_crypto.randomBytes(size));
|
|
47
|
+
}
|
|
48
|
+
function encrypt(data, secret) {
|
|
49
|
+
const nonce = getRandomBytes(tweetnacl.secretbox.nonceLength);
|
|
50
|
+
const encrypted = tweetnacl.secretbox(new TextEncoder().encode(JSON.stringify(data)), nonce, secret);
|
|
51
|
+
const result = new Uint8Array(nonce.length + encrypted.length);
|
|
52
|
+
result.set(nonce);
|
|
53
|
+
result.set(encrypted, nonce.length);
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
function decrypt(data, secret) {
|
|
57
|
+
const nonce = data.slice(0, tweetnacl.secretbox.nonceLength);
|
|
58
|
+
const encrypted = data.slice(tweetnacl.secretbox.nonceLength);
|
|
59
|
+
const decrypted = tweetnacl.secretbox.open(encrypted, nonce, secret);
|
|
60
|
+
if (!decrypted) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
return JSON.parse(new TextDecoder().decode(decrypted));
|
|
64
|
+
}
|
|
65
|
+
function authChallenge(secret) {
|
|
66
|
+
const keypair = tweetnacl.sign.keyPair.fromSeed(secret);
|
|
67
|
+
const challenge = getRandomBytes(32);
|
|
68
|
+
const signature = tweetnacl.sign.detached(challenge, keypair.secretKey);
|
|
69
|
+
return {
|
|
70
|
+
challenge,
|
|
71
|
+
publicKey: keypair.publicKey,
|
|
72
|
+
signature
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function getOrCreateSecretKey() {
|
|
77
|
+
const keyPath = node_path.join(node_os.homedir(), ".handy", "access.key");
|
|
78
|
+
if (fs.existsSync(keyPath)) {
|
|
79
|
+
const keyBase642 = fs.readFileSync(keyPath, "utf8").trim();
|
|
80
|
+
return new Uint8Array(Buffer.from(keyBase642, "base64"));
|
|
81
|
+
}
|
|
82
|
+
const secret = getRandomBytes(32);
|
|
83
|
+
const keyBase64 = encodeBase64(secret);
|
|
84
|
+
fs.mkdirSync(node_path.join(node_os.homedir(), ".handy"), { recursive: true });
|
|
85
|
+
fs.writeFileSync(keyPath, keyBase64);
|
|
86
|
+
await promises.chmod(keyPath, 384);
|
|
87
|
+
return secret;
|
|
88
|
+
}
|
|
89
|
+
async function authGetToken(secret) {
|
|
90
|
+
const { challenge, publicKey, signature } = authChallenge(secret);
|
|
91
|
+
const response = await axios.post(`https://handy-api.korshakov.org/v1/auth`, {
|
|
92
|
+
challenge: encodeBase64(challenge),
|
|
93
|
+
publicKey: encodeBase64(publicKey),
|
|
94
|
+
signature: encodeBase64(signature)
|
|
95
|
+
});
|
|
96
|
+
if (!response.data.success || !response.data.token) {
|
|
97
|
+
throw new Error("Authentication failed");
|
|
98
|
+
}
|
|
99
|
+
return response.data.token;
|
|
100
|
+
}
|
|
101
|
+
function generateAppUrl(secret) {
|
|
102
|
+
const secretBase64Url = encodeBase64Url(secret);
|
|
103
|
+
return `handy://${secretBase64Url}`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
class Logger {
|
|
107
|
+
debug(message, ...args) {
|
|
108
|
+
if (process.env.DEBUG) {
|
|
109
|
+
this.log("DEBUG" /* DEBUG */, message, ...args);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
error(message, ...args) {
|
|
113
|
+
this.log("ERROR" /* ERROR */, message, ...args);
|
|
114
|
+
}
|
|
115
|
+
info(message, ...args) {
|
|
116
|
+
this.log("INFO" /* INFO */, message, ...args);
|
|
117
|
+
}
|
|
118
|
+
warn(message, ...args) {
|
|
119
|
+
this.log("WARN" /* WARN */, message, ...args);
|
|
120
|
+
}
|
|
121
|
+
getTimestamp() {
|
|
122
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
123
|
+
}
|
|
124
|
+
log(level, message, ...args) {
|
|
125
|
+
const timestamp = this.getTimestamp();
|
|
126
|
+
const prefix = `[${timestamp}] [${level}]`;
|
|
127
|
+
switch (level) {
|
|
128
|
+
case "DEBUG" /* DEBUG */: {
|
|
129
|
+
console.log(chalk.gray(prefix), message, ...args);
|
|
130
|
+
break;
|
|
131
|
+
}
|
|
132
|
+
case "ERROR" /* ERROR */: {
|
|
133
|
+
console.error(chalk.red(prefix), message, ...args);
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
case "INFO" /* INFO */: {
|
|
137
|
+
console.log(chalk.blue(prefix), message, ...args);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
case "WARN" /* WARN */: {
|
|
141
|
+
console.log(chalk.yellow(prefix), message, ...args);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
const logger = new Logger();
|
|
148
|
+
|
|
149
|
+
const SessionMessageContentSchema = zod.z.object({
|
|
150
|
+
c: zod.z.string(),
|
|
151
|
+
// Base64 encoded encrypted content
|
|
152
|
+
t: zod.z.literal("encrypted")
|
|
153
|
+
});
|
|
154
|
+
const UpdateBodySchema = zod.z.object({
|
|
155
|
+
message: zod.z.object({
|
|
156
|
+
id: zod.z.string(),
|
|
157
|
+
seq: zod.z.number(),
|
|
158
|
+
content: SessionMessageContentSchema
|
|
159
|
+
}),
|
|
160
|
+
sid: zod.z.string(),
|
|
161
|
+
// Session ID
|
|
162
|
+
t: zod.z.literal("new-message")
|
|
163
|
+
});
|
|
164
|
+
zod.z.object({
|
|
165
|
+
id: zod.z.string(),
|
|
166
|
+
seq: zod.z.number(),
|
|
167
|
+
body: UpdateBodySchema,
|
|
168
|
+
createdAt: zod.z.number()
|
|
169
|
+
});
|
|
170
|
+
zod.z.object({
|
|
171
|
+
createdAt: zod.z.number(),
|
|
172
|
+
id: zod.z.string(),
|
|
173
|
+
seq: zod.z.number(),
|
|
174
|
+
updatedAt: zod.z.number()
|
|
175
|
+
});
|
|
176
|
+
zod.z.object({
|
|
177
|
+
content: SessionMessageContentSchema,
|
|
178
|
+
createdAt: zod.z.number(),
|
|
179
|
+
id: zod.z.string(),
|
|
180
|
+
seq: zod.z.number(),
|
|
181
|
+
updatedAt: zod.z.number()
|
|
182
|
+
});
|
|
183
|
+
zod.z.object({
|
|
184
|
+
session: zod.z.object({
|
|
185
|
+
id: zod.z.string(),
|
|
186
|
+
tag: zod.z.string(),
|
|
187
|
+
seq: zod.z.number(),
|
|
188
|
+
createdAt: zod.z.number(),
|
|
189
|
+
updatedAt: zod.z.number()
|
|
190
|
+
})
|
|
191
|
+
});
|
|
192
|
+
const UserMessageSchema = zod.z.object({
|
|
193
|
+
role: zod.z.literal("user"),
|
|
194
|
+
content: zod.z.object({
|
|
195
|
+
type: zod.z.literal("text"),
|
|
196
|
+
text: zod.z.string()
|
|
197
|
+
})
|
|
198
|
+
});
|
|
199
|
+
const AgentMessageSchema = zod.z.object({
|
|
200
|
+
role: zod.z.literal("agent"),
|
|
201
|
+
content: zod.z.any()
|
|
202
|
+
});
|
|
203
|
+
zod.z.union([UserMessageSchema, AgentMessageSchema]);
|
|
204
|
+
|
|
205
|
+
class ApiSessionClient extends node_events.EventEmitter {
|
|
206
|
+
token;
|
|
207
|
+
secret;
|
|
208
|
+
sessionId;
|
|
209
|
+
socket;
|
|
210
|
+
receivedMessages = /* @__PURE__ */ new Set();
|
|
211
|
+
pendingMessages = [];
|
|
212
|
+
pendingMessageCallback = null;
|
|
213
|
+
constructor(token, secret, sessionId) {
|
|
214
|
+
super();
|
|
215
|
+
this.token = token;
|
|
216
|
+
this.secret = secret;
|
|
217
|
+
this.sessionId = sessionId;
|
|
218
|
+
this.socket = socket_ioClient.io("https://handy-api.korshakov.org", {
|
|
219
|
+
auth: {
|
|
220
|
+
token: this.token
|
|
221
|
+
},
|
|
222
|
+
path: "/v1/updates",
|
|
223
|
+
reconnection: true,
|
|
224
|
+
reconnectionAttempts: Infinity,
|
|
225
|
+
reconnectionDelay: 1e3,
|
|
226
|
+
reconnectionDelayMax: 5e3,
|
|
227
|
+
transports: ["websocket"],
|
|
228
|
+
withCredentials: true,
|
|
229
|
+
autoConnect: false
|
|
230
|
+
});
|
|
231
|
+
this.socket.on("connect", () => {
|
|
232
|
+
logger.info("Socket connected successfully");
|
|
233
|
+
});
|
|
234
|
+
this.socket.on("disconnect", (reason) => {
|
|
235
|
+
logger.warn("Socket disconnected:", reason);
|
|
236
|
+
});
|
|
237
|
+
this.socket.on("connect_error", (error) => {
|
|
238
|
+
logger.error("Socket connection error:", error.message);
|
|
239
|
+
});
|
|
240
|
+
this.socket.on("update", (data) => {
|
|
241
|
+
if (data.body.t === "new-message" && data.body.message.content.t === "encrypted") {
|
|
242
|
+
const body = decrypt(decodeBase64(data.body.message.content.c), this.secret);
|
|
243
|
+
const result = UserMessageSchema.safeParse(body);
|
|
244
|
+
if (result.success) {
|
|
245
|
+
if (!this.receivedMessages.has(data.body.message.id)) {
|
|
246
|
+
this.receivedMessages.add(data.body.message.id);
|
|
247
|
+
if (this.pendingMessageCallback) {
|
|
248
|
+
this.pendingMessageCallback(result.data);
|
|
249
|
+
} else {
|
|
250
|
+
this.pendingMessages.push(result.data);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
this.socket.connect();
|
|
257
|
+
}
|
|
258
|
+
onUserMessage(callback) {
|
|
259
|
+
this.pendingMessageCallback = callback;
|
|
260
|
+
while (this.pendingMessages.length > 0) {
|
|
261
|
+
callback(this.pendingMessages.shift());
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Send message to session
|
|
266
|
+
* @param body - Message body
|
|
267
|
+
*/
|
|
268
|
+
sendMessage(body) {
|
|
269
|
+
let content = {
|
|
270
|
+
role: "agent",
|
|
271
|
+
content: body
|
|
272
|
+
};
|
|
273
|
+
const encrypted = encodeBase64(encrypt(content, this.secret));
|
|
274
|
+
this.socket.emit("message", {
|
|
275
|
+
sid: this.sessionId,
|
|
276
|
+
message: encrypted
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
async close() {
|
|
280
|
+
this.socket.close();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
class ApiClient {
|
|
285
|
+
token;
|
|
286
|
+
secret;
|
|
287
|
+
constructor(token, secret) {
|
|
288
|
+
this.token = token;
|
|
289
|
+
this.secret = secret;
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Create a new session or load existing one with the given tag
|
|
293
|
+
*/
|
|
294
|
+
async getOrCreateSession(tag) {
|
|
295
|
+
try {
|
|
296
|
+
const response = await axios.post(
|
|
297
|
+
`https://handy-api.korshakov.org/v1/sessions`,
|
|
298
|
+
{ tag },
|
|
299
|
+
{
|
|
300
|
+
headers: {
|
|
301
|
+
"Authorization": `Bearer ${this.token}`,
|
|
302
|
+
"Content-Type": "application/json"
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
);
|
|
306
|
+
logger.info(`Session created/loaded: ${response.data.session.id} (tag: ${tag})`);
|
|
307
|
+
return response.data;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
logger.error("Failed to get or create session:", error);
|
|
310
|
+
throw new Error(`Failed to get or create session: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Start realtime session client
|
|
315
|
+
* @param id - Session ID
|
|
316
|
+
* @returns Session client
|
|
317
|
+
*/
|
|
318
|
+
session(id) {
|
|
319
|
+
return new ApiSessionClient(this.token, this.secret, id);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function displayQRCode(url) {
|
|
324
|
+
try {
|
|
325
|
+
logger.info("=".repeat(50));
|
|
326
|
+
logger.info("\u{1F4F1} Scan this QR code with your mobile device:");
|
|
327
|
+
logger.info("=".repeat(50));
|
|
328
|
+
qrcode.generate(url, { small: true }, (qr) => {
|
|
329
|
+
for (let l of qr.split("\n")) {
|
|
330
|
+
logger.info(" " + l);
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
logger.info("=".repeat(50));
|
|
334
|
+
} catch (error) {
|
|
335
|
+
logger.error("Failed to generate QR code:", error);
|
|
336
|
+
logger.info(`\u{1F4CB} Use this URL to connect: ${url}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function claudePath() {
|
|
341
|
+
if (fs__namespace.existsSync(process.env.HOME + "/.claude/local/claude")) {
|
|
342
|
+
return process.env.HOME + "/.claude/local/claude";
|
|
343
|
+
} else {
|
|
344
|
+
return "claude";
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
async function* claude(options) {
|
|
349
|
+
const args = buildArgs(options);
|
|
350
|
+
const path = claudePath();
|
|
351
|
+
logger.info("Spawning Claude CLI with args:", args);
|
|
352
|
+
const process = node_child_process.spawn(path, args, {
|
|
353
|
+
cwd: options.workingDirectory,
|
|
354
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
355
|
+
shell: false
|
|
356
|
+
});
|
|
357
|
+
process.stdin?.end();
|
|
358
|
+
let outputBuffer = "";
|
|
359
|
+
let stderrBuffer = "";
|
|
360
|
+
let processExited = false;
|
|
361
|
+
const outputQueue = [];
|
|
362
|
+
let outputResolve = null;
|
|
363
|
+
process.stdout?.on("data", (data) => {
|
|
364
|
+
outputBuffer += data.toString();
|
|
365
|
+
const lines = outputBuffer.split("\n");
|
|
366
|
+
outputBuffer = lines.pop() || "";
|
|
367
|
+
for (const line of lines) {
|
|
368
|
+
if (line.trim()) {
|
|
369
|
+
try {
|
|
370
|
+
const json = JSON.parse(line);
|
|
371
|
+
outputQueue.push({ type: "json", data: json });
|
|
372
|
+
} catch {
|
|
373
|
+
outputQueue.push({ type: "text", data: line });
|
|
374
|
+
}
|
|
375
|
+
if (outputResolve) {
|
|
376
|
+
outputResolve();
|
|
377
|
+
outputResolve = null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
process.stderr?.on("data", (data) => {
|
|
383
|
+
stderrBuffer += data.toString();
|
|
384
|
+
const lines = stderrBuffer.split("\n");
|
|
385
|
+
stderrBuffer = lines.pop() || "";
|
|
386
|
+
for (const line of lines) {
|
|
387
|
+
if (line.trim()) {
|
|
388
|
+
outputQueue.push({ type: "error", error: line });
|
|
389
|
+
if (outputResolve) {
|
|
390
|
+
outputResolve();
|
|
391
|
+
outputResolve = null;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
process.on("exit", (code, signal) => {
|
|
397
|
+
processExited = true;
|
|
398
|
+
outputQueue.push({ type: "exit", code, signal });
|
|
399
|
+
if (outputResolve) {
|
|
400
|
+
outputResolve();
|
|
401
|
+
outputResolve = null;
|
|
402
|
+
}
|
|
403
|
+
});
|
|
404
|
+
process.on("error", (error) => {
|
|
405
|
+
outputQueue.push({ type: "error", error: error.message });
|
|
406
|
+
processExited = true;
|
|
407
|
+
if (outputResolve) {
|
|
408
|
+
outputResolve();
|
|
409
|
+
outputResolve = null;
|
|
410
|
+
}
|
|
411
|
+
});
|
|
412
|
+
while (!processExited || outputQueue.length > 0) {
|
|
413
|
+
if (outputQueue.length === 0) {
|
|
414
|
+
await new Promise((resolve) => {
|
|
415
|
+
outputResolve = resolve;
|
|
416
|
+
if (outputQueue.length > 0 || processExited) {
|
|
417
|
+
resolve();
|
|
418
|
+
outputResolve = null;
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
while (outputQueue.length > 0) {
|
|
423
|
+
const output = outputQueue.shift();
|
|
424
|
+
yield output;
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
if (outputBuffer.trim()) {
|
|
428
|
+
try {
|
|
429
|
+
const json = JSON.parse(outputBuffer);
|
|
430
|
+
yield { type: "json", data: json };
|
|
431
|
+
} catch {
|
|
432
|
+
yield { type: "text", data: outputBuffer };
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (stderrBuffer.trim()) {
|
|
436
|
+
yield { type: "error", error: stderrBuffer };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
function buildArgs(options) {
|
|
440
|
+
const args = [
|
|
441
|
+
"--print",
|
|
442
|
+
options.command,
|
|
443
|
+
"--output-format",
|
|
444
|
+
"stream-json",
|
|
445
|
+
"--verbose"
|
|
446
|
+
];
|
|
447
|
+
if (options.model) {
|
|
448
|
+
args.push("--model", options.model);
|
|
449
|
+
}
|
|
450
|
+
if (options.permissionMode) {
|
|
451
|
+
const modeMap = {
|
|
452
|
+
"auto": "acceptEdits",
|
|
453
|
+
"default": "default",
|
|
454
|
+
"plan": "bypassPermissions"
|
|
455
|
+
};
|
|
456
|
+
args.push("--permission-mode", modeMap[options.permissionMode]);
|
|
457
|
+
}
|
|
458
|
+
if (options.skipPermissions) {
|
|
459
|
+
args.push("--dangerously-skip-permissions");
|
|
460
|
+
}
|
|
461
|
+
if (options.sessionId) {
|
|
462
|
+
args.push("--resume", options.sessionId);
|
|
463
|
+
}
|
|
464
|
+
return args;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
function startClaudeLoop(opts, session) {
|
|
468
|
+
let exiting = false;
|
|
469
|
+
const messageQueue = [];
|
|
470
|
+
let messageResolve = null;
|
|
471
|
+
let promise = (async () => {
|
|
472
|
+
session.onUserMessage((message) => {
|
|
473
|
+
messageQueue.push(message);
|
|
474
|
+
if (messageResolve) {
|
|
475
|
+
messageResolve();
|
|
476
|
+
messageResolve = null;
|
|
477
|
+
}
|
|
478
|
+
});
|
|
479
|
+
while (!exiting) {
|
|
480
|
+
if (messageQueue.length > 0) {
|
|
481
|
+
const message = messageQueue.shift();
|
|
482
|
+
if (message) {
|
|
483
|
+
for await (const output of claude({
|
|
484
|
+
command: message.content.text,
|
|
485
|
+
workingDirectory: opts.path,
|
|
486
|
+
model: opts.model,
|
|
487
|
+
permissionMode: opts.permissionMode
|
|
488
|
+
})) {
|
|
489
|
+
if (output.type === "exit") {
|
|
490
|
+
if (output.code !== 0 || output.code === void 0) {
|
|
491
|
+
session.sendMessage({
|
|
492
|
+
content: {
|
|
493
|
+
type: "error",
|
|
494
|
+
error: output.error,
|
|
495
|
+
code: output.code
|
|
496
|
+
},
|
|
497
|
+
role: "assistant"
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
if (output.type === "json") {
|
|
503
|
+
session.sendMessage({
|
|
504
|
+
data: output.data,
|
|
505
|
+
type: "output"
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
await new Promise((resolve) => {
|
|
512
|
+
messageResolve = resolve;
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
})();
|
|
516
|
+
return async () => {
|
|
517
|
+
exiting = true;
|
|
518
|
+
if (messageResolve) {
|
|
519
|
+
messageResolve();
|
|
520
|
+
}
|
|
521
|
+
await promise;
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
async function start(options = {}) {
|
|
526
|
+
const workingDirectory = process.cwd();
|
|
527
|
+
const projectName = node_path.basename(workingDirectory);
|
|
528
|
+
const sessionTag = node_crypto.randomUUID();
|
|
529
|
+
logger.info(`Starting happy session for project: ${projectName}`);
|
|
530
|
+
const secret = await getOrCreateSecretKey();
|
|
531
|
+
logger.info("Secret key loaded");
|
|
532
|
+
const token = await authGetToken(secret);
|
|
533
|
+
logger.info("Authenticated with handy server");
|
|
534
|
+
const api = new ApiClient(token, secret);
|
|
535
|
+
const response = await api.getOrCreateSession(sessionTag);
|
|
536
|
+
logger.info(`Session created: ${response.session.id}`);
|
|
537
|
+
const handyUrl = generateAppUrl(secret);
|
|
538
|
+
displayQRCode(handyUrl);
|
|
539
|
+
const session = api.session(response.session.id);
|
|
540
|
+
const loopDestroy = startClaudeLoop({ path: workingDirectory }, session);
|
|
541
|
+
const shutdown = async () => {
|
|
542
|
+
logger.info("Shutting down...");
|
|
543
|
+
await loopDestroy();
|
|
544
|
+
await session.close();
|
|
545
|
+
process.exit(0);
|
|
546
|
+
};
|
|
547
|
+
process.on("SIGINT", shutdown);
|
|
548
|
+
process.on("SIGTERM", shutdown);
|
|
549
|
+
logger.info("Happy CLI is running. Press Ctrl+C to stop.");
|
|
550
|
+
await new Promise(() => {
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const args = process.argv.slice(2);
|
|
555
|
+
const options = {};
|
|
556
|
+
let showHelp = false;
|
|
557
|
+
let showVersion = false;
|
|
558
|
+
for (let i = 0; i < args.length; i++) {
|
|
559
|
+
const arg = args[i];
|
|
560
|
+
if (arg === "-h" || arg === "--help") {
|
|
561
|
+
showHelp = true;
|
|
562
|
+
} else if (arg === "-v" || arg === "--version") {
|
|
563
|
+
showVersion = true;
|
|
564
|
+
} else if (arg === "-m" || arg === "--model") {
|
|
565
|
+
options.model = args[++i];
|
|
566
|
+
} else if (arg === "-p" || arg === "--permission-mode") {
|
|
567
|
+
options.permissionMode = args[++i];
|
|
568
|
+
} else {
|
|
569
|
+
console.error(chalk.red(`Unknown argument: ${arg}`));
|
|
570
|
+
process.exit(1);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (showHelp) {
|
|
574
|
+
console.log(`
|
|
575
|
+
${chalk.bold("happy")} - Claude Code session sharing
|
|
576
|
+
|
|
577
|
+
${chalk.bold("Usage:")}
|
|
578
|
+
happy [options]
|
|
579
|
+
|
|
580
|
+
${chalk.bold("Options:")}
|
|
581
|
+
-h, --help Show this help message
|
|
582
|
+
-v, --version Show version
|
|
583
|
+
-m, --model <model> Claude model to use (default: sonnet)
|
|
584
|
+
-p, --permission-mode Permission mode: auto, default, or plan
|
|
585
|
+
|
|
586
|
+
${chalk.bold("Examples:")}
|
|
587
|
+
happy Start a session with default settings
|
|
588
|
+
happy -m opus Use Claude Opus model
|
|
589
|
+
happy -p plan Use plan permission mode
|
|
590
|
+
`);
|
|
591
|
+
process.exit(0);
|
|
592
|
+
}
|
|
593
|
+
if (showVersion) {
|
|
594
|
+
console.log("0.1.0");
|
|
595
|
+
process.exit(0);
|
|
596
|
+
}
|
|
597
|
+
start(options).catch((error) => {
|
|
598
|
+
console.error(chalk.red("Error:"), error.message);
|
|
599
|
+
if (process.env.DEBUG) {
|
|
600
|
+
console.error(error);
|
|
601
|
+
}
|
|
602
|
+
process.exit(1);
|
|
603
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { run } from '@oclif/core';
|