codesail 0.1.0
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/bin/codesail.js +2 -0
- package/dist/chunk-4OXDCPSF.js +44 -0
- package/dist/chunk-4OXDCPSF.js.map +1 -0
- package/dist/chunk-HG54UP2Y.js +25 -0
- package/dist/chunk-HG54UP2Y.js.map +1 -0
- package/dist/chunk-LWJDKIJF.js +180 -0
- package/dist/chunk-LWJDKIJF.js.map +1 -0
- package/dist/chunk-SFXOQ3OG.js +236 -0
- package/dist/chunk-SFXOQ3OG.js.map +1 -0
- package/dist/daemon/index.js +464 -0
- package/dist/daemon/index.js.map +1 -0
- package/dist/encryption-IBHFWEPA.js +20 -0
- package/dist/encryption-IBHFWEPA.js.map +1 -0
- package/dist/index.js +658 -0
- package/dist/index.js.map +1 -0
- package/dist/state-J732R3HC.js +14 -0
- package/dist/state-J732R3HC.js.map +1 -0
- package/package.json +40 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
import {
|
|
2
|
+
authenticate,
|
|
3
|
+
createAuthRequest,
|
|
4
|
+
createSocketClient,
|
|
5
|
+
fetchSessions,
|
|
6
|
+
loadConfig,
|
|
7
|
+
loadCredentials,
|
|
8
|
+
saveCredentials,
|
|
9
|
+
setConfigValue
|
|
10
|
+
} from "./chunk-SFXOQ3OG.js";
|
|
11
|
+
import {
|
|
12
|
+
isDaemonRunning,
|
|
13
|
+
readDaemonState
|
|
14
|
+
} from "./chunk-4OXDCPSF.js";
|
|
15
|
+
import {
|
|
16
|
+
base64urlEncode,
|
|
17
|
+
boxOpen,
|
|
18
|
+
decodeBase64,
|
|
19
|
+
decryptValue,
|
|
20
|
+
encodeBase64,
|
|
21
|
+
encryptToBase64,
|
|
22
|
+
generateBoxKeyPair,
|
|
23
|
+
resolveSessionEncryption
|
|
24
|
+
} from "./chunk-LWJDKIJF.js";
|
|
25
|
+
import {
|
|
26
|
+
DEFAULT_SERVER_URL,
|
|
27
|
+
QR_PREFIX
|
|
28
|
+
} from "./chunk-HG54UP2Y.js";
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
import { Command } from "commander";
|
|
32
|
+
|
|
33
|
+
// src/commands/login.ts
|
|
34
|
+
import nacl from "tweetnacl";
|
|
35
|
+
import chalk from "chalk";
|
|
36
|
+
import ora from "ora";
|
|
37
|
+
import qrcode from "qrcode-terminal";
|
|
38
|
+
|
|
39
|
+
// src/core/crypto/backup.ts
|
|
40
|
+
var BASE32_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
41
|
+
var OCR_CORRECTIONS = {
|
|
42
|
+
"0": "O",
|
|
43
|
+
"1": "I",
|
|
44
|
+
"8": "B",
|
|
45
|
+
"l": "I",
|
|
46
|
+
"o": "O"
|
|
47
|
+
};
|
|
48
|
+
function base32Encode(data) {
|
|
49
|
+
let bits = 0;
|
|
50
|
+
let value = 0;
|
|
51
|
+
let result = "";
|
|
52
|
+
for (const byte of data) {
|
|
53
|
+
value = value << 8 | byte;
|
|
54
|
+
bits += 8;
|
|
55
|
+
while (bits >= 5) {
|
|
56
|
+
bits -= 5;
|
|
57
|
+
result += BASE32_ALPHABET[value >>> bits & 31];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
if (bits > 0) {
|
|
61
|
+
result += BASE32_ALPHABET[value << 5 - bits & 31];
|
|
62
|
+
}
|
|
63
|
+
while (result.length % 8 !== 0) {
|
|
64
|
+
result += "=";
|
|
65
|
+
}
|
|
66
|
+
return result;
|
|
67
|
+
}
|
|
68
|
+
function base32Decode(input) {
|
|
69
|
+
const cleaned = input.replace(/[\s-]/g, "").replace(/=+$/, "").split("").map((c) => OCR_CORRECTIONS[c] ?? c.toUpperCase()).join("");
|
|
70
|
+
const bytes = [];
|
|
71
|
+
let bits = 0;
|
|
72
|
+
let value = 0;
|
|
73
|
+
for (const char of cleaned) {
|
|
74
|
+
const idx = BASE32_ALPHABET.indexOf(char);
|
|
75
|
+
if (idx === -1) throw new Error(`Invalid Base32 character: ${char}`);
|
|
76
|
+
value = value << 5 | idx;
|
|
77
|
+
bits += 5;
|
|
78
|
+
if (bits >= 8) {
|
|
79
|
+
bits -= 8;
|
|
80
|
+
bytes.push(value >>> bits & 255);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return new Uint8Array(bytes);
|
|
84
|
+
}
|
|
85
|
+
function formatBase32(encoded) {
|
|
86
|
+
const clean = encoded.replace(/=+$/, "");
|
|
87
|
+
return clean.match(/.{1,4}/g)?.join("-") ?? clean;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/commands/login.ts
|
|
91
|
+
async function loginCommand(opts) {
|
|
92
|
+
const serverURL = opts.server ?? DEFAULT_SERVER_URL;
|
|
93
|
+
const existing = await loadCredentials();
|
|
94
|
+
if (existing) {
|
|
95
|
+
console.log(chalk.yellow("Already logged in. Showing QR for mobile pairing..."));
|
|
96
|
+
await showPairingQR(serverURL);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
console.log(chalk.bold("Creating new CodeSail account...\n"));
|
|
100
|
+
const masterSecret = nacl.randomBytes(32);
|
|
101
|
+
const spinner = ora("Authenticating with server...").start();
|
|
102
|
+
let token;
|
|
103
|
+
try {
|
|
104
|
+
token = await authenticate(masterSecret, serverURL);
|
|
105
|
+
spinner.succeed("Authenticated successfully");
|
|
106
|
+
} catch (err) {
|
|
107
|
+
spinner.fail("Authentication failed");
|
|
108
|
+
console.error(chalk.red(err.message));
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
await saveCredentials({
|
|
112
|
+
token,
|
|
113
|
+
masterSecret: encodeBase64(masterSecret),
|
|
114
|
+
serverURL
|
|
115
|
+
});
|
|
116
|
+
console.log(chalk.green("Credentials saved to ~/.codesail/credentials.json"));
|
|
117
|
+
const backupKey = formatBase32(base32Encode(masterSecret));
|
|
118
|
+
console.log("\n" + chalk.bold("Backup Key (save this somewhere safe!):"));
|
|
119
|
+
console.log(chalk.cyan(backupKey));
|
|
120
|
+
console.log(chalk.dim("You can restore your account on any device with this key.\n"));
|
|
121
|
+
await showPairingQR(serverURL);
|
|
122
|
+
}
|
|
123
|
+
async function showPairingQR(serverURL) {
|
|
124
|
+
const resolvedURL = serverURL ?? DEFAULT_SERVER_URL;
|
|
125
|
+
const ephemeral = generateBoxKeyPair();
|
|
126
|
+
const publicKeyBase64 = encodeBase64(ephemeral.publicKey);
|
|
127
|
+
const publicKeyBase64url = base64urlEncode(ephemeral.publicKey);
|
|
128
|
+
const spinner = ora("Creating pairing request...").start();
|
|
129
|
+
try {
|
|
130
|
+
await createAuthRequest(publicKeyBase64, true, resolvedURL);
|
|
131
|
+
spinner.succeed("Pairing request created");
|
|
132
|
+
} catch (err) {
|
|
133
|
+
spinner.fail("Failed to create pairing request");
|
|
134
|
+
console.error(chalk.red(err.message));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const qrContent = QR_PREFIX + publicKeyBase64url;
|
|
138
|
+
console.log("\n" + chalk.bold("Scan this QR code with the CodeSail iOS app:"));
|
|
139
|
+
qrcode.generate(qrContent, { small: true });
|
|
140
|
+
console.log(chalk.dim("\nWaiting for mobile app to scan and approve..."));
|
|
141
|
+
const pollSpinner = ora("Polling...").start();
|
|
142
|
+
for (let i = 0; i < 120; i++) {
|
|
143
|
+
await sleep(1e3);
|
|
144
|
+
try {
|
|
145
|
+
const result = await createAuthRequest(publicKeyBase64, true, resolvedURL);
|
|
146
|
+
if (result.response) {
|
|
147
|
+
pollSpinner.succeed("Mobile app approved the pairing!");
|
|
148
|
+
const responseData = Buffer.from(result.response, "base64");
|
|
149
|
+
const decrypted = boxOpen(new Uint8Array(responseData), ephemeral.secretKey);
|
|
150
|
+
if (decrypted[0] === 0 && decrypted.length === 33) {
|
|
151
|
+
console.log(chalk.green("V2 pairing complete \u2014 content public key received"));
|
|
152
|
+
} else if (decrypted.length === 32) {
|
|
153
|
+
console.log(chalk.green("V1 pairing complete \u2014 master secret received"));
|
|
154
|
+
const token = await authenticate(decrypted, resolvedURL);
|
|
155
|
+
await saveCredentials({
|
|
156
|
+
token,
|
|
157
|
+
masterSecret: encodeBase64(decrypted),
|
|
158
|
+
serverURL: resolvedURL
|
|
159
|
+
});
|
|
160
|
+
console.log(chalk.green("Credentials updated"));
|
|
161
|
+
}
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (result.status === "not_found") {
|
|
165
|
+
pollSpinner.text = "Request expired, creating new one...";
|
|
166
|
+
await createAuthRequest(publicKeyBase64, true, resolvedURL);
|
|
167
|
+
}
|
|
168
|
+
} catch {
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
pollSpinner.fail("Pairing timed out. Run `codesail auth login` to try again.");
|
|
172
|
+
}
|
|
173
|
+
function sleep(ms) {
|
|
174
|
+
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// src/commands/daemon.ts
|
|
178
|
+
import { fork } from "child_process";
|
|
179
|
+
import { fileURLToPath } from "url";
|
|
180
|
+
import { dirname, join } from "path";
|
|
181
|
+
import chalk2 from "chalk";
|
|
182
|
+
import ora2 from "ora";
|
|
183
|
+
function findDaemonEntry() {
|
|
184
|
+
const thisFile = fileURLToPath(import.meta.url);
|
|
185
|
+
const distDir = dirname(thisFile);
|
|
186
|
+
const candidate = join(distDir, "daemon", "index.js");
|
|
187
|
+
return candidate;
|
|
188
|
+
}
|
|
189
|
+
async function daemonStartCommand() {
|
|
190
|
+
const creds = await loadCredentials();
|
|
191
|
+
if (!creds) {
|
|
192
|
+
console.error(chalk2.red("Not logged in. Run `codesail login` first."));
|
|
193
|
+
process.exit(1);
|
|
194
|
+
}
|
|
195
|
+
if (await isDaemonRunning()) {
|
|
196
|
+
const state2 = await readDaemonState();
|
|
197
|
+
console.log(chalk2.yellow(`Daemon already running (PID: ${state2?.pid}, port: ${state2?.httpPort})`));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
const spinner = ora2("Starting daemon...").start();
|
|
201
|
+
const daemonEntry = findDaemonEntry();
|
|
202
|
+
const child = fork(daemonEntry, [], {
|
|
203
|
+
detached: true,
|
|
204
|
+
stdio: "ignore"
|
|
205
|
+
});
|
|
206
|
+
child.unref();
|
|
207
|
+
await new Promise((resolve2) => setTimeout(resolve2, 2e3));
|
|
208
|
+
const state = await readDaemonState();
|
|
209
|
+
if (state) {
|
|
210
|
+
spinner.succeed(`Daemon started (PID: ${state.pid}, port: ${state.httpPort})`);
|
|
211
|
+
} else {
|
|
212
|
+
spinner.fail("Daemon may have failed to start. Check logs.");
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async function daemonStopCommand() {
|
|
216
|
+
const state = await readDaemonState();
|
|
217
|
+
if (!state) {
|
|
218
|
+
console.log(chalk2.yellow("Daemon is not running."));
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const spinner = ora2("Stopping daemon...").start();
|
|
222
|
+
try {
|
|
223
|
+
process.kill(state.pid, "SIGTERM");
|
|
224
|
+
await new Promise((resolve2) => setTimeout(resolve2, 1e3));
|
|
225
|
+
spinner.succeed(`Daemon stopped (was PID: ${state.pid})`);
|
|
226
|
+
} catch {
|
|
227
|
+
spinner.warn("Daemon process not found \u2014 cleaning up state file.");
|
|
228
|
+
const { removeDaemonState } = await import("./state-J732R3HC.js");
|
|
229
|
+
await removeDaemonState();
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
async function daemonStatusCommand() {
|
|
233
|
+
const running = await isDaemonRunning();
|
|
234
|
+
const state = await readDaemonState();
|
|
235
|
+
if (running && state) {
|
|
236
|
+
console.log(chalk2.green("Daemon is running"));
|
|
237
|
+
console.log(` PID: ${state.pid}`);
|
|
238
|
+
console.log(` Port: ${state.httpPort}`);
|
|
239
|
+
console.log(` URL: http://127.0.0.1:${state.httpPort}`);
|
|
240
|
+
} else {
|
|
241
|
+
console.log(chalk2.yellow("Daemon is not running"));
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// src/commands/sessions.ts
|
|
246
|
+
import chalk3 from "chalk";
|
|
247
|
+
import ora3 from "ora";
|
|
248
|
+
async function sessionsCommand() {
|
|
249
|
+
if (await isDaemonRunning()) {
|
|
250
|
+
const state = await readDaemonState();
|
|
251
|
+
if (state) {
|
|
252
|
+
try {
|
|
253
|
+
const resp = await fetch(`http://127.0.0.1:${state.httpPort}/sessions`);
|
|
254
|
+
const data = await resp.json();
|
|
255
|
+
displayDaemonSessions(data.sessions);
|
|
256
|
+
return;
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const creds = await loadCredentials();
|
|
262
|
+
if (!creds) {
|
|
263
|
+
console.error(chalk3.red("Not logged in. Run `codesail login` first."));
|
|
264
|
+
process.exit(1);
|
|
265
|
+
}
|
|
266
|
+
const spinner = ora3("Fetching sessions...").start();
|
|
267
|
+
try {
|
|
268
|
+
const masterSecret = decodeBase64(creds.masterSecret);
|
|
269
|
+
const serverURL = creds.serverURL ?? DEFAULT_SERVER_URL;
|
|
270
|
+
const rawSessions = await fetchSessions(creds.token, serverURL);
|
|
271
|
+
spinner.succeed(`Found ${rawSessions.length} sessions`);
|
|
272
|
+
displaySessions(rawSessions, masterSecret);
|
|
273
|
+
} catch (err) {
|
|
274
|
+
spinner.fail("Failed to fetch sessions");
|
|
275
|
+
console.error(chalk3.red(err.message));
|
|
276
|
+
process.exit(1);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
function displaySessions(rawSessions, masterSecret) {
|
|
280
|
+
if (rawSessions.length === 0) {
|
|
281
|
+
console.log(chalk3.dim("No sessions found."));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
console.log("");
|
|
285
|
+
for (const raw of rawSessions) {
|
|
286
|
+
const encryption = resolveSessionEncryption(raw.dataEncryptionKey, masterSecret);
|
|
287
|
+
let metadata = null;
|
|
288
|
+
let agentState = null;
|
|
289
|
+
try {
|
|
290
|
+
const metaData = decodeBase64(raw.metadata);
|
|
291
|
+
metadata = decryptValue(encryption, metaData);
|
|
292
|
+
} catch {
|
|
293
|
+
}
|
|
294
|
+
if (raw.agentState) {
|
|
295
|
+
try {
|
|
296
|
+
const stateData = decodeBase64(raw.agentState);
|
|
297
|
+
agentState = decryptValue(encryption, stateData);
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
const name = metadata?.name ?? metadata?.path?.split("/").pop() ?? raw.id.slice(0, 8);
|
|
302
|
+
const path = metadata?.path ?? "";
|
|
303
|
+
const host = metadata?.host ?? "";
|
|
304
|
+
const hasPending = agentState?.requests && Object.keys(agentState.requests).length > 0;
|
|
305
|
+
const lifecycle = metadata?.lifecycleState;
|
|
306
|
+
const updatedAt = new Date(raw.updatedAt).toLocaleString();
|
|
307
|
+
let status = chalk3.dim("idle");
|
|
308
|
+
if (hasPending) {
|
|
309
|
+
const tools = Object.values(agentState.requests).map((r) => r.tool);
|
|
310
|
+
status = chalk3.yellow(`\u26A0 permission: ${tools[0]}`);
|
|
311
|
+
} else if (lifecycle === "archived") {
|
|
312
|
+
status = chalk3.dim("archived");
|
|
313
|
+
}
|
|
314
|
+
console.log(chalk3.bold(name) + chalk3.dim(` [${raw.id.slice(0, 8)}]`));
|
|
315
|
+
console.log(` ${chalk3.dim("Path:")} ${path}`);
|
|
316
|
+
if (host) console.log(` ${chalk3.dim("Host:")} ${host}`);
|
|
317
|
+
console.log(` ${chalk3.dim("Status:")} ${status}`);
|
|
318
|
+
console.log(` ${chalk3.dim("Updated:")} ${updatedAt}`);
|
|
319
|
+
if (metadata?.summary?.text) {
|
|
320
|
+
console.log(` ${chalk3.dim("Summary:")} ${metadata.summary.text.slice(0, 100)}`);
|
|
321
|
+
}
|
|
322
|
+
console.log("");
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
function displayDaemonSessions(sessions) {
|
|
326
|
+
if (sessions.length === 0) {
|
|
327
|
+
console.log(chalk3.dim("No sessions found."));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
console.log("");
|
|
331
|
+
for (const s of sessions) {
|
|
332
|
+
const name = s.name || s.id.slice(0, 8);
|
|
333
|
+
const path = s.path || "";
|
|
334
|
+
const host = s.host || "";
|
|
335
|
+
const hasPending = s.hasPendingPermission === true;
|
|
336
|
+
const updatedAt = new Date(s.updatedAt).toLocaleString();
|
|
337
|
+
const lifecycle = s.lifecycleState;
|
|
338
|
+
let status = chalk3.dim("idle");
|
|
339
|
+
if (hasPending) {
|
|
340
|
+
const reqs = s.pendingRequests;
|
|
341
|
+
const tools = Object.values(reqs).map((r) => r.tool);
|
|
342
|
+
status = chalk3.yellow(`\u26A0 permission: ${tools[0]}`);
|
|
343
|
+
} else if (lifecycle === "archived") {
|
|
344
|
+
status = chalk3.dim("archived");
|
|
345
|
+
} else {
|
|
346
|
+
const activity = s.activity;
|
|
347
|
+
if (activity?.isThinking) status = chalk3.blue("thinking...");
|
|
348
|
+
else if (activity?.isActive) status = chalk3.green("active");
|
|
349
|
+
}
|
|
350
|
+
console.log(chalk3.bold(name) + chalk3.dim(` [${s.id.slice(0, 8)}]`));
|
|
351
|
+
console.log(` ${chalk3.dim("Path:")} ${path}`);
|
|
352
|
+
if (host) console.log(` ${chalk3.dim("Host:")} ${host}`);
|
|
353
|
+
console.log(` ${chalk3.dim("Status:")} ${status}`);
|
|
354
|
+
console.log(` ${chalk3.dim("Updated:")} ${updatedAt}`);
|
|
355
|
+
if (s.summary) {
|
|
356
|
+
console.log(` ${chalk3.dim("Summary:")} ${s.summary.slice(0, 100)}`);
|
|
357
|
+
}
|
|
358
|
+
console.log("");
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/commands/send.ts
|
|
363
|
+
import chalk4 from "chalk";
|
|
364
|
+
import ora4 from "ora";
|
|
365
|
+
async function sendCommand(sessionQuery, message) {
|
|
366
|
+
if (await isDaemonRunning()) {
|
|
367
|
+
const state = await readDaemonState();
|
|
368
|
+
if (state) {
|
|
369
|
+
try {
|
|
370
|
+
const sessionId = await resolveSessionId(sessionQuery, state.httpPort);
|
|
371
|
+
const resp = await fetch(`http://127.0.0.1:${state.httpPort}/send-message`, {
|
|
372
|
+
method: "POST",
|
|
373
|
+
headers: { "Content-Type": "application/json" },
|
|
374
|
+
body: JSON.stringify({ sessionId, text: message })
|
|
375
|
+
});
|
|
376
|
+
const data = await resp.json();
|
|
377
|
+
if (data.success) {
|
|
378
|
+
console.log(chalk4.green("Message sent."));
|
|
379
|
+
} else {
|
|
380
|
+
console.error(chalk4.red(`Failed: ${data.error}`));
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
} catch {
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
const creds = await loadCredentials();
|
|
388
|
+
if (!creds) {
|
|
389
|
+
console.error(chalk4.red("Not logged in. Run `codesail login` first."));
|
|
390
|
+
process.exit(1);
|
|
391
|
+
}
|
|
392
|
+
const spinner = ora4("Sending message...").start();
|
|
393
|
+
const masterSecret = decodeBase64(creds.masterSecret);
|
|
394
|
+
const serverURL = creds.serverURL ?? DEFAULT_SERVER_URL;
|
|
395
|
+
const rawSessions = await fetchSessions(creds.token, serverURL);
|
|
396
|
+
const session = rawSessions.find(
|
|
397
|
+
(s) => s.id === sessionQuery || s.id.startsWith(sessionQuery)
|
|
398
|
+
);
|
|
399
|
+
if (!session) {
|
|
400
|
+
spinner.fail(`Session not found: ${sessionQuery}`);
|
|
401
|
+
process.exit(1);
|
|
402
|
+
}
|
|
403
|
+
const encryption = resolveSessionEncryption(session.dataEncryptionKey, masterSecret);
|
|
404
|
+
const content = {
|
|
405
|
+
role: "user",
|
|
406
|
+
content: { type: "text", text: message },
|
|
407
|
+
meta: { sentFrom: "cli" }
|
|
408
|
+
};
|
|
409
|
+
const encrypted = encryptToBase64(encryption, content);
|
|
410
|
+
const socket = createSocketClient(serverURL, creds.token);
|
|
411
|
+
await new Promise((resolve2, reject) => {
|
|
412
|
+
const timeout = setTimeout(() => {
|
|
413
|
+
socket.disconnect();
|
|
414
|
+
reject(new Error("Connection timeout"));
|
|
415
|
+
}, 1e4);
|
|
416
|
+
socket.onConnect(() => {
|
|
417
|
+
clearTimeout(timeout);
|
|
418
|
+
socket.sendMessage(session.id, encrypted);
|
|
419
|
+
setTimeout(() => {
|
|
420
|
+
socket.disconnect();
|
|
421
|
+
resolve2();
|
|
422
|
+
}, 500);
|
|
423
|
+
});
|
|
424
|
+
socket.connect();
|
|
425
|
+
});
|
|
426
|
+
spinner.succeed("Message sent.");
|
|
427
|
+
}
|
|
428
|
+
async function resolveSessionId(query, daemonPort) {
|
|
429
|
+
const resp = await fetch(`http://127.0.0.1:${daemonPort}/sessions`);
|
|
430
|
+
const data = await resp.json();
|
|
431
|
+
const exact = data.sessions.find((s) => s.id === query);
|
|
432
|
+
if (exact) return exact.id;
|
|
433
|
+
const prefix = data.sessions.find((s) => s.id.startsWith(query));
|
|
434
|
+
if (prefix) return prefix.id;
|
|
435
|
+
const name = data.sessions.find(
|
|
436
|
+
(s) => s.name.toLowerCase().includes(query.toLowerCase()) || s.path.toLowerCase().includes(query.toLowerCase())
|
|
437
|
+
);
|
|
438
|
+
if (name) return name.id;
|
|
439
|
+
throw new Error(`Session not found: ${query}`);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// src/commands/approve.ts
|
|
443
|
+
import chalk5 from "chalk";
|
|
444
|
+
import ora5 from "ora";
|
|
445
|
+
async function approveCommand(sessionQuery, opts) {
|
|
446
|
+
const decision = opts.decision ?? "approved";
|
|
447
|
+
await handlePermission(sessionQuery, true, decision);
|
|
448
|
+
}
|
|
449
|
+
async function denyCommand(sessionQuery) {
|
|
450
|
+
await handlePermission(sessionQuery, false, "denied");
|
|
451
|
+
}
|
|
452
|
+
async function handlePermission(sessionQuery, approved, decision) {
|
|
453
|
+
const action = approved ? "Approving" : "Denying";
|
|
454
|
+
const pastAction = approved ? "approved" : "denied";
|
|
455
|
+
if (await isDaemonRunning()) {
|
|
456
|
+
const state = await readDaemonState();
|
|
457
|
+
if (state) {
|
|
458
|
+
try {
|
|
459
|
+
const { sessionId, requestId: requestId2 } = await findPendingVia(sessionQuery, state.httpPort);
|
|
460
|
+
const endpoint = approved ? "approve-permission" : "deny-permission";
|
|
461
|
+
const body = { sessionId, requestId: requestId2 };
|
|
462
|
+
if (approved) body.decision = decision;
|
|
463
|
+
const resp = await fetch(`http://127.0.0.1:${state.httpPort}/${endpoint}`, {
|
|
464
|
+
method: "POST",
|
|
465
|
+
headers: { "Content-Type": "application/json" },
|
|
466
|
+
body: JSON.stringify(body)
|
|
467
|
+
});
|
|
468
|
+
const data = await resp.json();
|
|
469
|
+
if (data.success) {
|
|
470
|
+
console.log(chalk5.green(`Permission ${pastAction}.`));
|
|
471
|
+
} else {
|
|
472
|
+
console.error(chalk5.red(`Failed: ${data.error}`));
|
|
473
|
+
}
|
|
474
|
+
return;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
console.error(chalk5.dim(`Daemon error: ${err.message}, falling back to direct API`));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const creds = await loadCredentials();
|
|
481
|
+
if (!creds) {
|
|
482
|
+
console.error(chalk5.red("Not logged in. Run `codesail login` first."));
|
|
483
|
+
process.exit(1);
|
|
484
|
+
}
|
|
485
|
+
const spinner = ora5(`${action} permission...`).start();
|
|
486
|
+
const masterSecret = decodeBase64(creds.masterSecret);
|
|
487
|
+
const serverURL = creds.serverURL ?? DEFAULT_SERVER_URL;
|
|
488
|
+
const rawSessions = await fetchSessions(creds.token, serverURL);
|
|
489
|
+
const session = rawSessions.find(
|
|
490
|
+
(s) => s.id === sessionQuery || s.id.startsWith(sessionQuery)
|
|
491
|
+
);
|
|
492
|
+
if (!session) {
|
|
493
|
+
spinner.fail(`Session not found: ${sessionQuery}`);
|
|
494
|
+
process.exit(1);
|
|
495
|
+
}
|
|
496
|
+
const encryption = resolveSessionEncryption(session.dataEncryptionKey, masterSecret);
|
|
497
|
+
let agentState = null;
|
|
498
|
+
if (session.agentState) {
|
|
499
|
+
try {
|
|
500
|
+
const stateData = decodeBase64(session.agentState);
|
|
501
|
+
const { decryptValue: decryptValue2 } = await import("./encryption-IBHFWEPA.js");
|
|
502
|
+
agentState = decryptValue2(encryption, stateData);
|
|
503
|
+
} catch {
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (!agentState?.requests || Object.keys(agentState.requests).length === 0) {
|
|
507
|
+
spinner.fail("No pending permission requests for this session.");
|
|
508
|
+
process.exit(1);
|
|
509
|
+
}
|
|
510
|
+
const [requestId, request] = Object.entries(agentState.requests)[0];
|
|
511
|
+
spinner.text = `${action} "${request.tool}" request...`;
|
|
512
|
+
const params = {
|
|
513
|
+
id: requestId,
|
|
514
|
+
approved,
|
|
515
|
+
decision
|
|
516
|
+
};
|
|
517
|
+
const encrypted = encryptToBase64(encryption, params);
|
|
518
|
+
const socket = createSocketClient(serverURL, creds.token);
|
|
519
|
+
const result = await new Promise((resolve2, reject) => {
|
|
520
|
+
const timeout = setTimeout(() => {
|
|
521
|
+
socket.disconnect();
|
|
522
|
+
reject(new Error("Connection timeout"));
|
|
523
|
+
}, 15e3);
|
|
524
|
+
socket.onConnect(async () => {
|
|
525
|
+
clearTimeout(timeout);
|
|
526
|
+
try {
|
|
527
|
+
const res = await socket.rpcCall(`${session.id}:permission`, encrypted);
|
|
528
|
+
socket.disconnect();
|
|
529
|
+
resolve2(res);
|
|
530
|
+
} catch (err) {
|
|
531
|
+
socket.disconnect();
|
|
532
|
+
reject(err);
|
|
533
|
+
}
|
|
534
|
+
});
|
|
535
|
+
socket.connect();
|
|
536
|
+
});
|
|
537
|
+
spinner.succeed(`Permission ${pastAction} for "${request.tool}".`);
|
|
538
|
+
}
|
|
539
|
+
async function findPendingVia(sessionQuery, daemonPort) {
|
|
540
|
+
const resp = await fetch(`http://127.0.0.1:${daemonPort}/sessions`);
|
|
541
|
+
const data = await resp.json();
|
|
542
|
+
const session = data.sessions.find(
|
|
543
|
+
(s) => s.id === sessionQuery || s.id.startsWith(sessionQuery) || s.name.toLowerCase().includes(sessionQuery.toLowerCase()) || s.path.toLowerCase().includes(sessionQuery.toLowerCase())
|
|
544
|
+
);
|
|
545
|
+
if (!session) throw new Error(`Session not found: ${sessionQuery}`);
|
|
546
|
+
if (!session.hasPendingPermission) throw new Error("No pending permission requests");
|
|
547
|
+
const requestId = Object.keys(session.pendingRequests)[0];
|
|
548
|
+
return { sessionId: session.id, requestId };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// src/commands/backup.ts
|
|
552
|
+
import chalk6 from "chalk";
|
|
553
|
+
import ora6 from "ora";
|
|
554
|
+
import { createInterface } from "readline/promises";
|
|
555
|
+
import { stdin, stdout } from "process";
|
|
556
|
+
async function backupShowCommand() {
|
|
557
|
+
const creds = await loadCredentials();
|
|
558
|
+
if (!creds) {
|
|
559
|
+
console.error(chalk6.red("Not logged in. Run `codesail login` first."));
|
|
560
|
+
process.exit(1);
|
|
561
|
+
}
|
|
562
|
+
const masterSecret = decodeBase64(creds.masterSecret);
|
|
563
|
+
const encoded = formatBase32(base32Encode(masterSecret));
|
|
564
|
+
console.log(chalk6.bold("\nBackup Key:"));
|
|
565
|
+
console.log(chalk6.cyan(encoded));
|
|
566
|
+
console.log(chalk6.dim("\nStore this key safely. It can restore your account on any device."));
|
|
567
|
+
}
|
|
568
|
+
async function backupRestoreCommand(opts) {
|
|
569
|
+
const serverURL = opts.server ?? DEFAULT_SERVER_URL;
|
|
570
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
571
|
+
const input = await rl.question(chalk6.bold("Enter your backup key: "));
|
|
572
|
+
rl.close();
|
|
573
|
+
if (!input.trim()) {
|
|
574
|
+
console.error(chalk6.red("No key entered."));
|
|
575
|
+
process.exit(1);
|
|
576
|
+
}
|
|
577
|
+
const spinner = ora6("Decoding backup key...").start();
|
|
578
|
+
let masterSecret;
|
|
579
|
+
try {
|
|
580
|
+
masterSecret = base32Decode(input.trim());
|
|
581
|
+
if (masterSecret.length !== 32) {
|
|
582
|
+
throw new Error(`Expected 32 bytes, got ${masterSecret.length}`);
|
|
583
|
+
}
|
|
584
|
+
spinner.succeed("Backup key decoded");
|
|
585
|
+
} catch (err) {
|
|
586
|
+
spinner.fail("Invalid backup key");
|
|
587
|
+
console.error(chalk6.red(err.message));
|
|
588
|
+
process.exit(1);
|
|
589
|
+
}
|
|
590
|
+
spinner.start("Authenticating...");
|
|
591
|
+
try {
|
|
592
|
+
const token = await authenticate(masterSecret, serverURL);
|
|
593
|
+
await saveCredentials({
|
|
594
|
+
token,
|
|
595
|
+
masterSecret: encodeBase64(masterSecret),
|
|
596
|
+
serverURL
|
|
597
|
+
});
|
|
598
|
+
spinner.succeed("Account restored successfully");
|
|
599
|
+
console.log(chalk6.green("Credentials saved to ~/.codesail/credentials.json"));
|
|
600
|
+
} catch (err) {
|
|
601
|
+
spinner.fail("Authentication failed");
|
|
602
|
+
console.error(chalk6.red(err.message));
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// src/commands/config.ts
|
|
608
|
+
import chalk7 from "chalk";
|
|
609
|
+
async function configSetCommand(key, value) {
|
|
610
|
+
await setConfigValue(key, value);
|
|
611
|
+
console.log(chalk7.green(`Set ${key} = ${value}`));
|
|
612
|
+
}
|
|
613
|
+
async function configGetCommand(key) {
|
|
614
|
+
const config2 = await loadConfig();
|
|
615
|
+
const value = config2[key];
|
|
616
|
+
if (value === void 0) {
|
|
617
|
+
console.log(chalk7.dim(`${key} is not set`));
|
|
618
|
+
} else {
|
|
619
|
+
console.log(`${key} = ${value}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
async function configListCommand() {
|
|
623
|
+
const config2 = await loadConfig();
|
|
624
|
+
const entries = Object.entries(config2);
|
|
625
|
+
if (entries.length === 0) {
|
|
626
|
+
console.log(chalk7.dim("No config values set."));
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
for (const [key, value] of entries) {
|
|
630
|
+
console.log(`${key} = ${value}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
// src/index.ts
|
|
635
|
+
var program = new Command();
|
|
636
|
+
program.name("codesail").description("CLI for CodeSail \u2014 control Claude Code sessions from your terminal").version("0.1.0");
|
|
637
|
+
program.command("login").description("Create new account or show QR for mobile pairing").option("-s, --server <url>", "Server URL").action(loginCommand);
|
|
638
|
+
var auth = program.command("auth").description("Authentication commands");
|
|
639
|
+
auth.command("login").description("Display QR code for mobile app to scan & pair").option("-s, --server <url>", "Server URL").action(async (opts) => {
|
|
640
|
+
await showPairingQR(opts.server);
|
|
641
|
+
});
|
|
642
|
+
var daemon = program.command("daemon").description("Background daemon management");
|
|
643
|
+
daemon.command("start").description("Start the background daemon").action(daemonStartCommand);
|
|
644
|
+
daemon.command("stop").description("Stop the background daemon").action(daemonStopCommand);
|
|
645
|
+
daemon.command("status").description("Show daemon status").action(daemonStatusCommand);
|
|
646
|
+
program.command("sessions").description("List active sessions").action(sessionsCommand);
|
|
647
|
+
program.command("send <session> <message>").description("Send a message to a session").action(sendCommand);
|
|
648
|
+
program.command("approve <session>").description("Approve pending permission request").option("-d, --decision <decision>", "Decision type (approved, approved_for_session)", "approved").action(approveCommand);
|
|
649
|
+
program.command("deny <session>").description("Deny pending permission request").action(denyCommand);
|
|
650
|
+
var backup = program.command("backup").description("Backup key management");
|
|
651
|
+
backup.command("show").description("Display your backup key").action(backupShowCommand);
|
|
652
|
+
backup.command("restore").description("Restore account from backup key").option("-s, --server <url>", "Server URL").action(backupRestoreCommand);
|
|
653
|
+
var config = program.command("config").description("Configuration management");
|
|
654
|
+
config.command("set <key> <value>").description("Set a config value").action(configSetCommand);
|
|
655
|
+
config.command("get <key>").description("Get a config value").action(configGetCommand);
|
|
656
|
+
config.command("list").description("List all config values").action(configListCommand);
|
|
657
|
+
program.parse();
|
|
658
|
+
//# sourceMappingURL=index.js.map
|