@zhigang1992/happy-cli 0.12.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.
Files changed (47) hide show
  1. package/README.md +60 -0
  2. package/bin/happy-mcp.mjs +32 -0
  3. package/bin/happy.mjs +35 -0
  4. package/dist/codex/happyMcpStdioBridge.cjs +80 -0
  5. package/dist/codex/happyMcpStdioBridge.d.cts +2 -0
  6. package/dist/codex/happyMcpStdioBridge.d.mts +2 -0
  7. package/dist/codex/happyMcpStdioBridge.mjs +78 -0
  8. package/dist/index-BOBrKhX5.cjs +6655 -0
  9. package/dist/index-DsHtmQqP.mjs +6624 -0
  10. package/dist/index.cjs +42 -0
  11. package/dist/index.d.cts +1 -0
  12. package/dist/index.d.mts +1 -0
  13. package/dist/index.mjs +39 -0
  14. package/dist/lib.cjs +31 -0
  15. package/dist/lib.d.cts +817 -0
  16. package/dist/lib.d.mts +817 -0
  17. package/dist/lib.mjs +21 -0
  18. package/dist/list-BW6QBLa1.cjs +328 -0
  19. package/dist/list-hET5tyMc.mjs +326 -0
  20. package/dist/prompt-DXkgjktW.cjs +203 -0
  21. package/dist/prompt-Dz7G8yGx.mjs +201 -0
  22. package/dist/runCodex-CLGYMNs2.mjs +1335 -0
  23. package/dist/runCodex-CylcX5Ug.cjs +1338 -0
  24. package/dist/types-BsjUgWOx.cjs +2264 -0
  25. package/dist/types-CGco5Y-r.mjs +2213 -0
  26. package/package.json +126 -0
  27. package/scripts/claude_local_launcher.cjs +98 -0
  28. package/scripts/claude_remote_launcher.cjs +13 -0
  29. package/scripts/ripgrep_launcher.cjs +33 -0
  30. package/scripts/unpack-tools.cjs +163 -0
  31. package/tools/archives/difftastic-LICENSE +21 -0
  32. package/tools/archives/difftastic-arm64-darwin.tar.gz +0 -0
  33. package/tools/archives/difftastic-arm64-linux.tar.gz +0 -0
  34. package/tools/archives/difftastic-x64-darwin.tar.gz +0 -0
  35. package/tools/archives/difftastic-x64-linux.tar.gz +0 -0
  36. package/tools/archives/difftastic-x64-win32.tar.gz +0 -0
  37. package/tools/archives/ripgrep-LICENSE +3 -0
  38. package/tools/archives/ripgrep-arm64-darwin.tar.gz +0 -0
  39. package/tools/archives/ripgrep-arm64-linux.tar.gz +0 -0
  40. package/tools/archives/ripgrep-x64-darwin.tar.gz +0 -0
  41. package/tools/archives/ripgrep-x64-linux.tar.gz +0 -0
  42. package/tools/archives/ripgrep-x64-win32.tar.gz +0 -0
  43. package/tools/licenses/difftastic-LICENSE +21 -0
  44. package/tools/licenses/ripgrep-LICENSE +3 -0
  45. package/tools/unpacked/difft +0 -0
  46. package/tools/unpacked/rg +0 -0
  47. package/tools/unpacked/ripgrep.node +0 -0
package/dist/lib.mjs ADDED
@@ -0,0 +1,21 @@
1
+ export { A as ApiClient, a as ApiSessionClient, R as RawJSONLinesSchema, c as configuration, l as logger } from './types-CGco5Y-r.mjs';
2
+ import 'axios';
3
+ import 'chalk';
4
+ import 'fs';
5
+ import 'node:fs';
6
+ import 'node:os';
7
+ import 'node:path';
8
+ import 'node:fs/promises';
9
+ import 'zod';
10
+ import 'node:crypto';
11
+ import 'tweetnacl';
12
+ import 'node:events';
13
+ import 'socket.io-client';
14
+ import 'child_process';
15
+ import 'util';
16
+ import 'fs/promises';
17
+ import 'crypto';
18
+ import 'path';
19
+ import 'url';
20
+ import 'os';
21
+ import 'expo-server-sdk';
@@ -0,0 +1,328 @@
1
+ 'use strict';
2
+
3
+ var types = require('./types-BsjUgWOx.cjs');
4
+ var axios = require('axios');
5
+ var fs = require('fs');
6
+ var path = require('path');
7
+ var os = require('os');
8
+ var crypto = require('crypto');
9
+ var tweetnacl = require('tweetnacl');
10
+ require('chalk');
11
+ require('node:fs');
12
+ require('node:os');
13
+ require('node:path');
14
+ require('node:fs/promises');
15
+ require('zod');
16
+ require('node:crypto');
17
+ require('node:events');
18
+ require('socket.io-client');
19
+ require('child_process');
20
+ require('util');
21
+ require('fs/promises');
22
+ require('url');
23
+ require('expo-server-sdk');
24
+
25
+ function decryptDataEncryptionKey(encryptedKey, credentials) {
26
+ try {
27
+ types.logger.debug(`Attempting to decrypt dataEncryptionKey...`);
28
+ types.logger.debug(`Credentials type: ${credentials.encryption.type}`);
29
+ if (credentials.encryption.type !== "dataKey") {
30
+ types.logger.debug(`Credentials type is not 'dataKey', returning null`);
31
+ return null;
32
+ }
33
+ const encryptedBundle = types.decodeBase64(encryptedKey);
34
+ types.logger.debug(`Encrypted bundle length: ${encryptedBundle.length}`);
35
+ types.logger.debug(`Version byte: ${encryptedBundle[0]}`);
36
+ if (encryptedBundle.length === 0 || encryptedBundle[0] !== 0) {
37
+ types.logger.debug(`Invalid version or empty bundle`);
38
+ return null;
39
+ }
40
+ const bundleWithoutVersion = encryptedBundle.slice(1);
41
+ types.logger.debug(`Bundle without version length: ${bundleWithoutVersion.length}`);
42
+ const seedBytes = credentials.encryption.dataKeySeed;
43
+ types.logger.debug(`Seed bytes from dataKeySeed: ${seedBytes.length} bytes`);
44
+ const hash = crypto.createHash("sha512").update(Buffer.from(seedBytes)).digest();
45
+ const secretKey = new Uint8Array(hash.slice(0, 32));
46
+ const keypair = tweetnacl.box.keyPair.fromSecretKey(secretKey);
47
+ types.logger.debug(`Derived keypair from dataKeySeed`);
48
+ const decrypted = types.libsodiumDecryptFromPublicKey(bundleWithoutVersion, keypair.secretKey);
49
+ types.logger.debug(`Decryption result: ${decrypted ? `success (${decrypted.length} bytes)` : "null"}`);
50
+ return decrypted;
51
+ } catch (error) {
52
+ types.logger.debug(`Failed to decrypt dataEncryptionKey:`, error);
53
+ return null;
54
+ }
55
+ }
56
+ function getCurrentWorkingDirectory(claudeSessionId, spawnDirectory) {
57
+ try {
58
+ const homeDir = os.homedir();
59
+ const claudeProjectsDir = path.join(homeDir, ".claude", "projects");
60
+ if (!fs.existsSync(claudeProjectsDir)) {
61
+ return spawnDirectory;
62
+ }
63
+ const projectDirs = fs.readdirSync(claudeProjectsDir).map((name) => path.join(claudeProjectsDir, name)).filter((path) => fs.statSync(path).isDirectory());
64
+ for (const projectDir of projectDirs) {
65
+ const sessionFile = path.join(projectDir, `${claudeSessionId}.jsonl`);
66
+ if (fs.existsSync(sessionFile)) {
67
+ const content = fs.readFileSync(sessionFile, "utf8");
68
+ const lines = content.trim().split("\n");
69
+ const lastLine = lines[lines.length - 1];
70
+ try {
71
+ const lastMessage = JSON.parse(lastLine);
72
+ if (lastMessage.cwd) {
73
+ return lastMessage.cwd;
74
+ }
75
+ } catch (e) {
76
+ types.logger.debug(`Failed to parse last line of session file ${sessionFile}:`, e);
77
+ }
78
+ }
79
+ }
80
+ } catch (error) {
81
+ types.logger.debug(`Failed to get current working directory for session ${claudeSessionId}:`, error);
82
+ }
83
+ return spawnDirectory;
84
+ }
85
+ async function fetchSessionMessages(sessionId, credentials, encryptionKey, encryptionVariant, limit) {
86
+ const serverUrl = types.configuration.serverUrl;
87
+ try {
88
+ const response = await axios.get(
89
+ `${serverUrl}/v1/sessions/${sessionId}/messages`,
90
+ {
91
+ headers: {
92
+ "Authorization": `Bearer ${credentials.token}`
93
+ }
94
+ }
95
+ );
96
+ const messages = response.data.messages;
97
+ const decryptedMessages = [];
98
+ const messagesToProcess = messages.slice(0, limit);
99
+ for (const msg of messagesToProcess) {
100
+ try {
101
+ const encryptedContent = msg.content.c;
102
+ const decrypted = types.decrypt(
103
+ encryptionKey,
104
+ encryptionVariant,
105
+ types.decodeBase64(encryptedContent)
106
+ );
107
+ if (decrypted) {
108
+ decryptedMessages.push(decrypted);
109
+ }
110
+ } catch (error) {
111
+ types.logger.debug(`Failed to decrypt message ${msg.id}:`, error);
112
+ }
113
+ }
114
+ return decryptedMessages.reverse();
115
+ } catch (error) {
116
+ types.logger.debug(`Failed to fetch messages for session ${sessionId}:`, error);
117
+ return [];
118
+ }
119
+ }
120
+ function extractMessageText(msg) {
121
+ const msgContent = msg.content;
122
+ if (typeof msgContent === "string") {
123
+ return msgContent.trim() || null;
124
+ }
125
+ if (msgContent?.type === "text" && msgContent?.text) {
126
+ return String(msgContent.text).trim() || null;
127
+ }
128
+ if (msgContent?.type === "output" && msgContent?.data?.message?.content) {
129
+ const messageContent = msgContent.data.message.content;
130
+ if (Array.isArray(messageContent)) {
131
+ const text = messageContent.filter((c) => c.type === "text").map((c) => c.text).join("\n").trim();
132
+ return text || null;
133
+ } else if (typeof messageContent === "string") {
134
+ return messageContent.trim() || null;
135
+ }
136
+ }
137
+ if (Array.isArray(msgContent)) {
138
+ const text = msgContent.filter((c) => c.type === "text").map((c) => c.text).join("\n").trim();
139
+ return text || null;
140
+ }
141
+ if (msgContent?.type === "event") {
142
+ return null;
143
+ }
144
+ return null;
145
+ }
146
+ function shouldDisplayMessage(msg) {
147
+ return extractMessageText(msg) !== null;
148
+ }
149
+ function formatMessage(msg, indent = "", maxLen = 200) {
150
+ const role = msg.role;
151
+ let prefix;
152
+ if (role === "user") {
153
+ prefix = "\u{1F464} User";
154
+ } else if (role === "agent" || role === "assistant") {
155
+ prefix = "\u{1F916} Assistant";
156
+ } else {
157
+ prefix = `[${role || "unknown"}]`;
158
+ }
159
+ let content = extractMessageText(msg) || "";
160
+ if (maxLen >= 0 && content.length > maxLen) {
161
+ content = content.substring(0, maxLen) + "...";
162
+ }
163
+ content = content.replace(/\n/g, `
164
+ ${indent} `);
165
+ return `${indent}${prefix}: ${content}`;
166
+ }
167
+ async function listSessions(credentials, options = {}) {
168
+ try {
169
+ const serverUrl = types.configuration.serverUrl;
170
+ const response = await axios.get(`${serverUrl}/v2/sessions/active`, {
171
+ headers: {
172
+ "Authorization": `Bearer ${credentials.token}`
173
+ }
174
+ });
175
+ const sessions = response.data.sessions;
176
+ if (sessions.length === 0) {
177
+ console.log("No active sessions found.");
178
+ return;
179
+ }
180
+ const processedSessions = [];
181
+ for (const session of sessions) {
182
+ types.logger.debug(`
183
+ === Processing session ${session.id} ===`);
184
+ types.logger.debug(`Has dataEncryptionKey: ${!!session.dataEncryptionKey}`);
185
+ types.logger.debug(`Has metadata: ${!!session.metadata}`);
186
+ let metadata = {};
187
+ let agentState = {};
188
+ let encryptionKey = null;
189
+ let encryptionVariant = "dataKey";
190
+ if (session.dataEncryptionKey) {
191
+ types.logger.debug(`Session has dataEncryptionKey, attempting to decrypt...`);
192
+ encryptionKey = decryptDataEncryptionKey(session.dataEncryptionKey, credentials);
193
+ if (!encryptionKey) {
194
+ types.logger.debug(`Failed to decrypt dataEncryptionKey for session ${session.id}`);
195
+ } else {
196
+ types.logger.debug(`Successfully decrypted dataEncryptionKey`);
197
+ }
198
+ } else if (credentials.encryption.type === "dataKey") {
199
+ types.logger.debug(`Using machineKey directly for CLI session`);
200
+ encryptionKey = credentials.encryption.machineKey;
201
+ } else {
202
+ types.logger.debug(`Using legacy secret key`);
203
+ encryptionKey = credentials.encryption.secret;
204
+ encryptionVariant = "legacy";
205
+ }
206
+ if (session.metadata && encryptionKey) {
207
+ types.logger.debug(`Attempting to decrypt metadata...`);
208
+ try {
209
+ const metadataBytes = types.decodeBase64(session.metadata);
210
+ const decrypted = types.decrypt(encryptionKey, encryptionVariant, metadataBytes);
211
+ if (decrypted) {
212
+ types.logger.debug(`Successfully decrypted metadata:`, JSON.stringify(decrypted));
213
+ metadata = decrypted;
214
+ }
215
+ } catch (error) {
216
+ types.logger.debug(`Failed to decrypt metadata for session ${session.id}:`, error);
217
+ }
218
+ }
219
+ if (session.agentState && encryptionKey) {
220
+ try {
221
+ const decrypted = types.decrypt(encryptionKey, encryptionVariant, types.decodeBase64(session.agentState));
222
+ if (decrypted) {
223
+ agentState = decrypted;
224
+ }
225
+ } catch (error) {
226
+ types.logger.debug(`Failed to decrypt agent state for session ${session.id}:`, error);
227
+ }
228
+ }
229
+ let title = "(Untitled)";
230
+ if (metadata.summary?.text) {
231
+ title = metadata.summary.text;
232
+ } else if (metadata.path) {
233
+ const segments = metadata.path.split("/").filter(Boolean);
234
+ if (segments.length > 0) {
235
+ title = segments[segments.length - 1];
236
+ }
237
+ }
238
+ let workingDir = metadata.path || "(Unknown)";
239
+ if (metadata.claudeSessionId && metadata.path) {
240
+ workingDir = getCurrentWorkingDirectory(metadata.claudeSessionId, metadata.path);
241
+ }
242
+ processedSessions.push({
243
+ session,
244
+ metadata,
245
+ agentState,
246
+ encryptionKey,
247
+ encryptionVariant,
248
+ title,
249
+ host: metadata.host || "(Unknown)",
250
+ workingDir,
251
+ thinking: !!agentState.thinking
252
+ });
253
+ }
254
+ let filteredSessions = processedSessions;
255
+ if (options.sessionId) {
256
+ const idFilter = options.sessionId.toLowerCase();
257
+ filteredSessions = filteredSessions.filter(
258
+ (s) => s.session.id.toLowerCase().startsWith(idFilter)
259
+ );
260
+ }
261
+ if (options.titleFilter) {
262
+ const titleFilter = options.titleFilter.toLowerCase();
263
+ filteredSessions = filteredSessions.filter(
264
+ (s) => s.title.toLowerCase().includes(titleFilter)
265
+ );
266
+ }
267
+ if (filteredSessions.length === 0) {
268
+ if (options.sessionId || options.titleFilter) {
269
+ console.log("No sessions match the specified filters.");
270
+ } else {
271
+ console.log("No active sessions found.");
272
+ }
273
+ return;
274
+ }
275
+ const showMessages = options.recentMsgs && options.recentMsgs > 0;
276
+ const needsSeparators = showMessages && filteredSessions.length > 1;
277
+ const indent = needsSeparators ? " " : "";
278
+ console.log(`
279
+ Active Sessions (${filteredSessions.length}):
280
+ `);
281
+ for (let i = 0; i < filteredSessions.length; i++) {
282
+ const { session, encryptionKey, encryptionVariant, title, host, workingDir, thinking } = filteredSessions[i];
283
+ if (needsSeparators && i > 0) {
284
+ console.log("\n" + "\u2550".repeat(70) + "\n");
285
+ }
286
+ const thinkingStatus = thinking ? "\u{1F914} Thinking" : "\u{1F4A4} Idle";
287
+ console.log(`${indent}ID: ${session.id}`);
288
+ console.log(`${indent}Title: ${title}`);
289
+ console.log(`${indent}Working Directory: ${workingDir}`);
290
+ console.log(`${indent}Host: ${host}`);
291
+ console.log(`${indent}Status: ${thinkingStatus}`);
292
+ console.log(`${indent}Last Active: ${types.formatTimeAgo(session.activeAt)}`);
293
+ if (showMessages && encryptionKey) {
294
+ console.log(`${indent}Recent Messages:`);
295
+ const messages = await fetchSessionMessages(
296
+ session.id,
297
+ credentials,
298
+ encryptionKey,
299
+ encryptionVariant,
300
+ options.recentMsgs
301
+ );
302
+ const displayableMessages = messages.filter(shouldDisplayMessage);
303
+ const msgLen = options.msgLen ?? 200;
304
+ if (displayableMessages.length === 0) {
305
+ console.log(`${indent} (no messages)`);
306
+ } else {
307
+ for (const msg of displayableMessages) {
308
+ console.log(formatMessage(msg, indent + " ", msgLen));
309
+ }
310
+ }
311
+ }
312
+ console.log("");
313
+ }
314
+ } catch (error) {
315
+ if (axios.isAxiosError(error)) {
316
+ if (error.response?.status === 401) {
317
+ console.error("Authentication failed. Please run `happy auth` to authenticate.");
318
+ } else {
319
+ console.error(`Failed to fetch sessions: ${error.response?.data?.message || error.message}`);
320
+ }
321
+ } else {
322
+ console.error(`Failed to fetch sessions: ${error}`);
323
+ }
324
+ process.exit(1);
325
+ }
326
+ }
327
+
328
+ exports.listSessions = listSessions;
@@ -0,0 +1,326 @@
1
+ import { c as configuration, l as logger, d as decodeBase64, b as decrypt, f as formatTimeAgo, e as libsodiumDecryptFromPublicKey } from './types-CGco5Y-r.mjs';
2
+ import axios from 'axios';
3
+ import { existsSync, readdirSync, statSync, readFileSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { homedir } from 'os';
6
+ import { createHash } from 'crypto';
7
+ import tweetnacl from 'tweetnacl';
8
+ import 'chalk';
9
+ import 'node:fs';
10
+ import 'node:os';
11
+ import 'node:path';
12
+ import 'node:fs/promises';
13
+ import 'zod';
14
+ import 'node:crypto';
15
+ import 'node:events';
16
+ import 'socket.io-client';
17
+ import 'child_process';
18
+ import 'util';
19
+ import 'fs/promises';
20
+ import 'url';
21
+ import 'expo-server-sdk';
22
+
23
+ function decryptDataEncryptionKey(encryptedKey, credentials) {
24
+ try {
25
+ logger.debug(`Attempting to decrypt dataEncryptionKey...`);
26
+ logger.debug(`Credentials type: ${credentials.encryption.type}`);
27
+ if (credentials.encryption.type !== "dataKey") {
28
+ logger.debug(`Credentials type is not 'dataKey', returning null`);
29
+ return null;
30
+ }
31
+ const encryptedBundle = decodeBase64(encryptedKey);
32
+ logger.debug(`Encrypted bundle length: ${encryptedBundle.length}`);
33
+ logger.debug(`Version byte: ${encryptedBundle[0]}`);
34
+ if (encryptedBundle.length === 0 || encryptedBundle[0] !== 0) {
35
+ logger.debug(`Invalid version or empty bundle`);
36
+ return null;
37
+ }
38
+ const bundleWithoutVersion = encryptedBundle.slice(1);
39
+ logger.debug(`Bundle without version length: ${bundleWithoutVersion.length}`);
40
+ const seedBytes = credentials.encryption.dataKeySeed;
41
+ logger.debug(`Seed bytes from dataKeySeed: ${seedBytes.length} bytes`);
42
+ const hash = createHash("sha512").update(Buffer.from(seedBytes)).digest();
43
+ const secretKey = new Uint8Array(hash.slice(0, 32));
44
+ const keypair = tweetnacl.box.keyPair.fromSecretKey(secretKey);
45
+ logger.debug(`Derived keypair from dataKeySeed`);
46
+ const decrypted = libsodiumDecryptFromPublicKey(bundleWithoutVersion, keypair.secretKey);
47
+ logger.debug(`Decryption result: ${decrypted ? `success (${decrypted.length} bytes)` : "null"}`);
48
+ return decrypted;
49
+ } catch (error) {
50
+ logger.debug(`Failed to decrypt dataEncryptionKey:`, error);
51
+ return null;
52
+ }
53
+ }
54
+ function getCurrentWorkingDirectory(claudeSessionId, spawnDirectory) {
55
+ try {
56
+ const homeDir = homedir();
57
+ const claudeProjectsDir = join(homeDir, ".claude", "projects");
58
+ if (!existsSync(claudeProjectsDir)) {
59
+ return spawnDirectory;
60
+ }
61
+ const projectDirs = readdirSync(claudeProjectsDir).map((name) => join(claudeProjectsDir, name)).filter((path) => statSync(path).isDirectory());
62
+ for (const projectDir of projectDirs) {
63
+ const sessionFile = join(projectDir, `${claudeSessionId}.jsonl`);
64
+ if (existsSync(sessionFile)) {
65
+ const content = readFileSync(sessionFile, "utf8");
66
+ const lines = content.trim().split("\n");
67
+ const lastLine = lines[lines.length - 1];
68
+ try {
69
+ const lastMessage = JSON.parse(lastLine);
70
+ if (lastMessage.cwd) {
71
+ return lastMessage.cwd;
72
+ }
73
+ } catch (e) {
74
+ logger.debug(`Failed to parse last line of session file ${sessionFile}:`, e);
75
+ }
76
+ }
77
+ }
78
+ } catch (error) {
79
+ logger.debug(`Failed to get current working directory for session ${claudeSessionId}:`, error);
80
+ }
81
+ return spawnDirectory;
82
+ }
83
+ async function fetchSessionMessages(sessionId, credentials, encryptionKey, encryptionVariant, limit) {
84
+ const serverUrl = configuration.serverUrl;
85
+ try {
86
+ const response = await axios.get(
87
+ `${serverUrl}/v1/sessions/${sessionId}/messages`,
88
+ {
89
+ headers: {
90
+ "Authorization": `Bearer ${credentials.token}`
91
+ }
92
+ }
93
+ );
94
+ const messages = response.data.messages;
95
+ const decryptedMessages = [];
96
+ const messagesToProcess = messages.slice(0, limit);
97
+ for (const msg of messagesToProcess) {
98
+ try {
99
+ const encryptedContent = msg.content.c;
100
+ const decrypted = decrypt(
101
+ encryptionKey,
102
+ encryptionVariant,
103
+ decodeBase64(encryptedContent)
104
+ );
105
+ if (decrypted) {
106
+ decryptedMessages.push(decrypted);
107
+ }
108
+ } catch (error) {
109
+ logger.debug(`Failed to decrypt message ${msg.id}:`, error);
110
+ }
111
+ }
112
+ return decryptedMessages.reverse();
113
+ } catch (error) {
114
+ logger.debug(`Failed to fetch messages for session ${sessionId}:`, error);
115
+ return [];
116
+ }
117
+ }
118
+ function extractMessageText(msg) {
119
+ const msgContent = msg.content;
120
+ if (typeof msgContent === "string") {
121
+ return msgContent.trim() || null;
122
+ }
123
+ if (msgContent?.type === "text" && msgContent?.text) {
124
+ return String(msgContent.text).trim() || null;
125
+ }
126
+ if (msgContent?.type === "output" && msgContent?.data?.message?.content) {
127
+ const messageContent = msgContent.data.message.content;
128
+ if (Array.isArray(messageContent)) {
129
+ const text = messageContent.filter((c) => c.type === "text").map((c) => c.text).join("\n").trim();
130
+ return text || null;
131
+ } else if (typeof messageContent === "string") {
132
+ return messageContent.trim() || null;
133
+ }
134
+ }
135
+ if (Array.isArray(msgContent)) {
136
+ const text = msgContent.filter((c) => c.type === "text").map((c) => c.text).join("\n").trim();
137
+ return text || null;
138
+ }
139
+ if (msgContent?.type === "event") {
140
+ return null;
141
+ }
142
+ return null;
143
+ }
144
+ function shouldDisplayMessage(msg) {
145
+ return extractMessageText(msg) !== null;
146
+ }
147
+ function formatMessage(msg, indent = "", maxLen = 200) {
148
+ const role = msg.role;
149
+ let prefix;
150
+ if (role === "user") {
151
+ prefix = "\u{1F464} User";
152
+ } else if (role === "agent" || role === "assistant") {
153
+ prefix = "\u{1F916} Assistant";
154
+ } else {
155
+ prefix = `[${role || "unknown"}]`;
156
+ }
157
+ let content = extractMessageText(msg) || "";
158
+ if (maxLen >= 0 && content.length > maxLen) {
159
+ content = content.substring(0, maxLen) + "...";
160
+ }
161
+ content = content.replace(/\n/g, `
162
+ ${indent} `);
163
+ return `${indent}${prefix}: ${content}`;
164
+ }
165
+ async function listSessions(credentials, options = {}) {
166
+ try {
167
+ const serverUrl = configuration.serverUrl;
168
+ const response = await axios.get(`${serverUrl}/v2/sessions/active`, {
169
+ headers: {
170
+ "Authorization": `Bearer ${credentials.token}`
171
+ }
172
+ });
173
+ const sessions = response.data.sessions;
174
+ if (sessions.length === 0) {
175
+ console.log("No active sessions found.");
176
+ return;
177
+ }
178
+ const processedSessions = [];
179
+ for (const session of sessions) {
180
+ logger.debug(`
181
+ === Processing session ${session.id} ===`);
182
+ logger.debug(`Has dataEncryptionKey: ${!!session.dataEncryptionKey}`);
183
+ logger.debug(`Has metadata: ${!!session.metadata}`);
184
+ let metadata = {};
185
+ let agentState = {};
186
+ let encryptionKey = null;
187
+ let encryptionVariant = "dataKey";
188
+ if (session.dataEncryptionKey) {
189
+ logger.debug(`Session has dataEncryptionKey, attempting to decrypt...`);
190
+ encryptionKey = decryptDataEncryptionKey(session.dataEncryptionKey, credentials);
191
+ if (!encryptionKey) {
192
+ logger.debug(`Failed to decrypt dataEncryptionKey for session ${session.id}`);
193
+ } else {
194
+ logger.debug(`Successfully decrypted dataEncryptionKey`);
195
+ }
196
+ } else if (credentials.encryption.type === "dataKey") {
197
+ logger.debug(`Using machineKey directly for CLI session`);
198
+ encryptionKey = credentials.encryption.machineKey;
199
+ } else {
200
+ logger.debug(`Using legacy secret key`);
201
+ encryptionKey = credentials.encryption.secret;
202
+ encryptionVariant = "legacy";
203
+ }
204
+ if (session.metadata && encryptionKey) {
205
+ logger.debug(`Attempting to decrypt metadata...`);
206
+ try {
207
+ const metadataBytes = decodeBase64(session.metadata);
208
+ const decrypted = decrypt(encryptionKey, encryptionVariant, metadataBytes);
209
+ if (decrypted) {
210
+ logger.debug(`Successfully decrypted metadata:`, JSON.stringify(decrypted));
211
+ metadata = decrypted;
212
+ }
213
+ } catch (error) {
214
+ logger.debug(`Failed to decrypt metadata for session ${session.id}:`, error);
215
+ }
216
+ }
217
+ if (session.agentState && encryptionKey) {
218
+ try {
219
+ const decrypted = decrypt(encryptionKey, encryptionVariant, decodeBase64(session.agentState));
220
+ if (decrypted) {
221
+ agentState = decrypted;
222
+ }
223
+ } catch (error) {
224
+ logger.debug(`Failed to decrypt agent state for session ${session.id}:`, error);
225
+ }
226
+ }
227
+ let title = "(Untitled)";
228
+ if (metadata.summary?.text) {
229
+ title = metadata.summary.text;
230
+ } else if (metadata.path) {
231
+ const segments = metadata.path.split("/").filter(Boolean);
232
+ if (segments.length > 0) {
233
+ title = segments[segments.length - 1];
234
+ }
235
+ }
236
+ let workingDir = metadata.path || "(Unknown)";
237
+ if (metadata.claudeSessionId && metadata.path) {
238
+ workingDir = getCurrentWorkingDirectory(metadata.claudeSessionId, metadata.path);
239
+ }
240
+ processedSessions.push({
241
+ session,
242
+ metadata,
243
+ agentState,
244
+ encryptionKey,
245
+ encryptionVariant,
246
+ title,
247
+ host: metadata.host || "(Unknown)",
248
+ workingDir,
249
+ thinking: !!agentState.thinking
250
+ });
251
+ }
252
+ let filteredSessions = processedSessions;
253
+ if (options.sessionId) {
254
+ const idFilter = options.sessionId.toLowerCase();
255
+ filteredSessions = filteredSessions.filter(
256
+ (s) => s.session.id.toLowerCase().startsWith(idFilter)
257
+ );
258
+ }
259
+ if (options.titleFilter) {
260
+ const titleFilter = options.titleFilter.toLowerCase();
261
+ filteredSessions = filteredSessions.filter(
262
+ (s) => s.title.toLowerCase().includes(titleFilter)
263
+ );
264
+ }
265
+ if (filteredSessions.length === 0) {
266
+ if (options.sessionId || options.titleFilter) {
267
+ console.log("No sessions match the specified filters.");
268
+ } else {
269
+ console.log("No active sessions found.");
270
+ }
271
+ return;
272
+ }
273
+ const showMessages = options.recentMsgs && options.recentMsgs > 0;
274
+ const needsSeparators = showMessages && filteredSessions.length > 1;
275
+ const indent = needsSeparators ? " " : "";
276
+ console.log(`
277
+ Active Sessions (${filteredSessions.length}):
278
+ `);
279
+ for (let i = 0; i < filteredSessions.length; i++) {
280
+ const { session, encryptionKey, encryptionVariant, title, host, workingDir, thinking } = filteredSessions[i];
281
+ if (needsSeparators && i > 0) {
282
+ console.log("\n" + "\u2550".repeat(70) + "\n");
283
+ }
284
+ const thinkingStatus = thinking ? "\u{1F914} Thinking" : "\u{1F4A4} Idle";
285
+ console.log(`${indent}ID: ${session.id}`);
286
+ console.log(`${indent}Title: ${title}`);
287
+ console.log(`${indent}Working Directory: ${workingDir}`);
288
+ console.log(`${indent}Host: ${host}`);
289
+ console.log(`${indent}Status: ${thinkingStatus}`);
290
+ console.log(`${indent}Last Active: ${formatTimeAgo(session.activeAt)}`);
291
+ if (showMessages && encryptionKey) {
292
+ console.log(`${indent}Recent Messages:`);
293
+ const messages = await fetchSessionMessages(
294
+ session.id,
295
+ credentials,
296
+ encryptionKey,
297
+ encryptionVariant,
298
+ options.recentMsgs
299
+ );
300
+ const displayableMessages = messages.filter(shouldDisplayMessage);
301
+ const msgLen = options.msgLen ?? 200;
302
+ if (displayableMessages.length === 0) {
303
+ console.log(`${indent} (no messages)`);
304
+ } else {
305
+ for (const msg of displayableMessages) {
306
+ console.log(formatMessage(msg, indent + " ", msgLen));
307
+ }
308
+ }
309
+ }
310
+ console.log("");
311
+ }
312
+ } catch (error) {
313
+ if (axios.isAxiosError(error)) {
314
+ if (error.response?.status === 401) {
315
+ console.error("Authentication failed. Please run `happy auth` to authenticate.");
316
+ } else {
317
+ console.error(`Failed to fetch sessions: ${error.response?.data?.message || error.message}`);
318
+ }
319
+ } else {
320
+ console.error(`Failed to fetch sessions: ${error}`);
321
+ }
322
+ process.exit(1);
323
+ }
324
+ }
325
+
326
+ export { listSessions };