airclaw 1.0.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.
Files changed (2) hide show
  1. package/dist/cli.js +1660 -0
  2. package/package.json +21 -0
package/dist/cli.js ADDED
@@ -0,0 +1,1660 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { readFileSync as readFileSync3 } from "fs";
5
+ import { fileURLToPath } from "url";
6
+ import { dirname, join as join2 } from "path";
7
+
8
+ // src/commands/auth.ts
9
+ import { createInterface } from "readline";
10
+
11
+ // src/config.ts
12
+ import {
13
+ existsSync,
14
+ mkdirSync,
15
+ readFileSync,
16
+ writeFileSync,
17
+ rmSync,
18
+ chmodSync,
19
+ cpSync,
20
+ readdirSync
21
+ } from "fs";
22
+ import { homedir } from "os";
23
+ import { join } from "path";
24
+ var CONFIG_DIR = join(homedir(), ".airclaw");
25
+ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
26
+ var CONNECTIONS_FILE = join(CONFIG_DIR, "connections.json");
27
+ var KEYS_DIR = join(CONFIG_DIR, "keys");
28
+ var AIRTERM_DIR = join(homedir(), ".airterm");
29
+ function ensureDir() {
30
+ if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
31
+ if (!existsSync(KEYS_DIR)) mkdirSync(KEYS_DIR, { recursive: true, mode: 448 });
32
+ try {
33
+ chmodSync(CONFIG_DIR, 448);
34
+ } catch {
35
+ }
36
+ try {
37
+ chmodSync(KEYS_DIR, 448);
38
+ } catch {
39
+ }
40
+ }
41
+ function writeSecureFile(path, data) {
42
+ writeFileSync(path, data, { mode: 384 });
43
+ chmodSync(path, 384);
44
+ }
45
+ function loadConfig() {
46
+ migrateFromAirterm();
47
+ if (!existsSync(CONFIG_FILE)) return {};
48
+ try {
49
+ return JSON.parse(readFileSync(CONFIG_FILE, "utf-8"));
50
+ } catch {
51
+ return {};
52
+ }
53
+ }
54
+ function saveConfig(config) {
55
+ ensureDir();
56
+ writeSecureFile(CONFIG_FILE, JSON.stringify(config, null, 2));
57
+ }
58
+ function getSavedConnections() {
59
+ migrateFromAirterm();
60
+ if (!existsSync(CONNECTIONS_FILE)) return [];
61
+ try {
62
+ const data = JSON.parse(readFileSync(CONNECTIONS_FILE, "utf-8"));
63
+ return data.saved || [];
64
+ } catch {
65
+ return [];
66
+ }
67
+ }
68
+ function saveConnections(connections) {
69
+ ensureDir();
70
+ writeSecureFile(CONNECTIONS_FILE, JSON.stringify({ saved: connections }, null, 2));
71
+ }
72
+ function addSavedConnection(conn) {
73
+ const connections = getSavedConnections();
74
+ const idx = connections.findIndex((c) => c.id === conn.id);
75
+ if (idx >= 0) {
76
+ connections[idx] = conn;
77
+ } else {
78
+ connections.push(conn);
79
+ }
80
+ saveConnections(connections);
81
+ }
82
+ function saveKey(machineId, keyData) {
83
+ ensureDir();
84
+ const keyPath = join(KEYS_DIR, `${machineId}.key`);
85
+ writeFileSync(keyPath, keyData, { mode: 384 });
86
+ chmodSync(keyPath, 384);
87
+ return keyPath;
88
+ }
89
+ function resetAll() {
90
+ const connections = getSavedConnections();
91
+ const count = connections.length;
92
+ if (existsSync(CONFIG_DIR)) {
93
+ if (existsSync(CONNECTIONS_FILE)) rmSync(CONNECTIONS_FILE);
94
+ if (existsSync(KEYS_DIR)) rmSync(KEYS_DIR, { recursive: true, force: true });
95
+ }
96
+ return count;
97
+ }
98
+ var migrated = false;
99
+ function migrateFromAirterm() {
100
+ if (migrated) return;
101
+ migrated = true;
102
+ if (!existsSync(AIRTERM_DIR) || existsSync(CONFIG_DIR)) return;
103
+ try {
104
+ ensureDir();
105
+ const airtermConfig = join(AIRTERM_DIR, "connections.json");
106
+ if (existsSync(airtermConfig)) {
107
+ const raw = JSON.parse(readFileSync(airtermConfig, "utf-8"));
108
+ const oldConns = raw.connections || [];
109
+ const newConns = oldConns.map((c) => ({
110
+ id: c.id,
111
+ name: c.name,
112
+ hostname: c.hostname,
113
+ port: c.port,
114
+ username: c.username,
115
+ keyPath: c.keyPath.replace(`${AIRTERM_DIR}/`, `${CONFIG_DIR}/`),
116
+ addedAt: c.addedAt
117
+ }));
118
+ saveConnections(newConns);
119
+ }
120
+ const airtermKeys = join(AIRTERM_DIR, "keys");
121
+ if (existsSync(airtermKeys)) {
122
+ cpSync(airtermKeys, KEYS_DIR, { recursive: true });
123
+ for (const file of readdirSync(KEYS_DIR)) {
124
+ chmodSync(join(KEYS_DIR, file), 384);
125
+ }
126
+ }
127
+ console.log(
128
+ "\x1B[33mMigrated saved connections from ~/.airterm/ to ~/.airclaw/\x1B[0m"
129
+ );
130
+ console.log("You can safely delete ~/.airterm/ now.\n");
131
+ } catch {
132
+ }
133
+ }
134
+
135
+ // src/api.ts
136
+ var ApiError = class extends Error {
137
+ constructor(status3, message) {
138
+ super(message);
139
+ this.status = status3;
140
+ this.name = "ApiError";
141
+ }
142
+ };
143
+ var AirClawAPI = class {
144
+ apiKey;
145
+ baseUrl;
146
+ constructor(apiKey, baseUrl = "https://app.airclaw.com") {
147
+ this.apiKey = apiKey;
148
+ this.baseUrl = baseUrl.replace(/\/$/, "");
149
+ }
150
+ async request(method, path, body, query) {
151
+ const url = new URL(`/api/v1${path}`, this.baseUrl);
152
+ if (query) {
153
+ for (const [k, v] of Object.entries(query)) {
154
+ if (v !== void 0) url.searchParams.set(k, v);
155
+ }
156
+ }
157
+ const headers = {
158
+ Authorization: `Bearer ${this.apiKey}`
159
+ };
160
+ if (body !== void 0) {
161
+ headers["Content-Type"] = "application/json";
162
+ }
163
+ const res = await fetch(url.toString(), {
164
+ method,
165
+ headers,
166
+ body: body !== void 0 ? JSON.stringify(body) : void 0,
167
+ signal: AbortSignal.timeout(3e4)
168
+ });
169
+ if (!res.ok) {
170
+ const data = await res.json().catch(() => ({}));
171
+ throw new ApiError(
172
+ res.status,
173
+ data.error || `HTTP ${res.status}`
174
+ );
175
+ }
176
+ const text = await res.text();
177
+ if (!text) return void 0;
178
+ return JSON.parse(text);
179
+ }
180
+ // ── Machines ──
181
+ async list(source) {
182
+ return this.request("GET", "/airclaw/list", void 0, source ? { source } : void 0);
183
+ }
184
+ async create() {
185
+ return this.request("POST", "/airclaw");
186
+ }
187
+ async get(id) {
188
+ return this.request("GET", `/airclaw/${encodeURIComponent(id)}`);
189
+ }
190
+ async destroy(id) {
191
+ await this.request("DELETE", `/airclaw/${encodeURIComponent(id)}`);
192
+ }
193
+ async sleep(id) {
194
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/sleep`);
195
+ }
196
+ // ── SSH ──
197
+ async sshAccess(id) {
198
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/ssh/access`);
199
+ }
200
+ // ── Drive ──
201
+ async driveUploadUrl(id, path, contentType) {
202
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/drive/upload`, {
203
+ path,
204
+ content_type: contentType
205
+ });
206
+ }
207
+ async driveDownloadUrl(id, path) {
208
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/drive/download`, {
209
+ path
210
+ });
211
+ }
212
+ async driveList(id, prefix, limit) {
213
+ const query = {};
214
+ if (prefix) query.prefix = prefix;
215
+ if (limit) query.limit = String(limit);
216
+ return this.request("GET", `/airclaw/${encodeURIComponent(id)}/drive/list`, void 0, query);
217
+ }
218
+ async driveDelete(id, key) {
219
+ await this.request("DELETE", `/airclaw/${encodeURIComponent(id)}/drive/${encodeURIComponent(key)}`);
220
+ }
221
+ async driveShare(id, path) {
222
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/drive/share`, { path });
223
+ }
224
+ // ── Mail ──
225
+ async mailList(id, limit, pageToken) {
226
+ const query = {};
227
+ if (limit) query.limit = String(limit);
228
+ if (pageToken) query.page_token = pageToken;
229
+ return this.request("GET", `/airclaw/${encodeURIComponent(id)}/mail`, void 0, query);
230
+ }
231
+ async mailRead(id, emailId) {
232
+ return this.request(
233
+ "GET",
234
+ `/airclaw/${encodeURIComponent(id)}/mail/${encodeURIComponent(emailId)}`
235
+ );
236
+ }
237
+ async mailSend(id, opts) {
238
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/mail/send`, opts);
239
+ }
240
+ async mailReply(id, messageId, opts) {
241
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/mail/reply`, {
242
+ message_id: messageId,
243
+ ...opts
244
+ });
245
+ }
246
+ async mailStatus(id) {
247
+ return this.request("GET", `/airclaw/${encodeURIComponent(id)}/mail/status`);
248
+ }
249
+ async mailClaim(id, username, displayName) {
250
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/mail/claim`, {
251
+ username,
252
+ display_name: displayName
253
+ });
254
+ }
255
+ // ── Todos ──
256
+ async todosList(id, params) {
257
+ const query = {};
258
+ if (params?.status) query.status = params.status;
259
+ if (params?.tag) query.tag = params.tag;
260
+ return this.request("GET", `/airclaw/${encodeURIComponent(id)}/todos`, void 0, query);
261
+ }
262
+ async todosCreate(id, body) {
263
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/todos`, body);
264
+ }
265
+ async todosUpdate(id, todoId, body) {
266
+ return this.request(
267
+ "PATCH",
268
+ `/airclaw/${encodeURIComponent(id)}/todos/${encodeURIComponent(todoId)}`,
269
+ body
270
+ );
271
+ }
272
+ async todosDelete(id, todoId) {
273
+ await this.request(
274
+ "DELETE",
275
+ `/airclaw/${encodeURIComponent(id)}/todos/${encodeURIComponent(todoId)}`
276
+ );
277
+ }
278
+ async todosTags(id) {
279
+ return this.request("GET", `/airclaw/${encodeURIComponent(id)}/todos/tags`);
280
+ }
281
+ async todosCreateTag(id, body) {
282
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/todos/tags`, body);
283
+ }
284
+ // ── Gateway ──
285
+ async getConfig(id) {
286
+ return this.request("GET", `/airclaw/${encodeURIComponent(id)}/gateway/config`);
287
+ }
288
+ async patchConfig(id, patch) {
289
+ return this.request("PATCH", `/airclaw/${encodeURIComponent(id)}/gateway/config`, patch);
290
+ }
291
+ async listModels(id) {
292
+ return this.request("GET", `/airclaw/${encodeURIComponent(id)}/gateway/models`);
293
+ }
294
+ async rpc(id, method, params, timeoutMs) {
295
+ return this.request("POST", `/airclaw/${encodeURIComponent(id)}/gateway/rpc`, {
296
+ method,
297
+ params: params || {},
298
+ timeout_ms: timeoutMs
299
+ });
300
+ }
301
+ async invokeTool(id, name, args2) {
302
+ return this.request(
303
+ "POST",
304
+ `/airclaw/${encodeURIComponent(id)}/gateway/tools/invoke`,
305
+ { name, arguments: args2 || {} }
306
+ );
307
+ }
308
+ async *chatStream(id, messages, extraHeaders) {
309
+ const url = new URL(
310
+ `/api/v1/airclaw/${encodeURIComponent(id)}/gateway/chat`,
311
+ this.baseUrl
312
+ );
313
+ const headers = {
314
+ Authorization: `Bearer ${this.apiKey}`,
315
+ "Content-Type": "application/json",
316
+ ...extraHeaders
317
+ };
318
+ const res = await fetch(url.toString(), {
319
+ method: "POST",
320
+ headers,
321
+ body: JSON.stringify({ messages, stream: true })
322
+ });
323
+ if (!res.ok) {
324
+ const data = await res.json().catch(() => ({}));
325
+ throw new ApiError(
326
+ res.status,
327
+ data.error || `HTTP ${res.status}`
328
+ );
329
+ }
330
+ if (!res.body) throw new ApiError(502, "No response body");
331
+ const reader = res.body.getReader();
332
+ const decoder = new TextDecoder();
333
+ let buffer = "";
334
+ try {
335
+ while (true) {
336
+ const { done, value } = await reader.read();
337
+ if (done) break;
338
+ buffer += decoder.decode(value, { stream: true });
339
+ const lines = buffer.split("\n");
340
+ buffer = lines.pop() || "";
341
+ for (const line of lines) {
342
+ if (!line.startsWith("data: ")) continue;
343
+ const data = line.slice(6).trim();
344
+ if (data === "[DONE]") return;
345
+ try {
346
+ const parsed = JSON.parse(data);
347
+ const content = parsed.choices?.[0]?.delta?.content;
348
+ if (content) yield content;
349
+ } catch {
350
+ }
351
+ }
352
+ }
353
+ } finally {
354
+ reader.releaseLock();
355
+ }
356
+ }
357
+ // ── Keys ──
358
+ async keysList() {
359
+ return this.request("GET", "/keys/list");
360
+ }
361
+ async keysCreate(name) {
362
+ return this.request("POST", "/keys", { name });
363
+ }
364
+ async keysRevoke(id) {
365
+ await this.request("DELETE", `/keys/${encodeURIComponent(id)}`);
366
+ }
367
+ // ── Static (no auth) ──
368
+ static async redeemCode(code, baseUrl = "https://app.airclaw.com") {
369
+ const url = new URL("/api/airterm/redeem", baseUrl);
370
+ const res = await fetch(url.toString(), {
371
+ method: "POST",
372
+ headers: { "Content-Type": "application/json" },
373
+ body: JSON.stringify({ code }),
374
+ signal: AbortSignal.timeout(3e4)
375
+ });
376
+ const data = await res.json();
377
+ if (!res.ok) {
378
+ throw new ApiError(res.status, data.error || `HTTP ${res.status}`);
379
+ }
380
+ return data;
381
+ }
382
+ static async downloadKey(url) {
383
+ const parsed = new URL(url);
384
+ if (parsed.protocol !== "https:") {
385
+ throw new ApiError(400, "Key download URL must use HTTPS");
386
+ }
387
+ const res = await fetch(url, { signal: AbortSignal.timeout(3e4) });
388
+ if (!res.ok) throw new ApiError(res.status, `Failed to download key: HTTP ${res.status}`);
389
+ return res.text();
390
+ }
391
+ static async checkMachines(ids, baseUrl = "https://app.airclaw.com") {
392
+ try {
393
+ const url = new URL("/api/airterm/check", baseUrl);
394
+ const res = await fetch(url.toString(), {
395
+ method: "POST",
396
+ headers: { "Content-Type": "application/json" },
397
+ body: JSON.stringify({ machineIds: ids }),
398
+ signal: AbortSignal.timeout(1e4)
399
+ });
400
+ if (!res.ok) return new Set(ids);
401
+ const data = await res.json();
402
+ return new Set(data.alive);
403
+ } catch {
404
+ return new Set(ids);
405
+ }
406
+ }
407
+ };
408
+
409
+ // src/utils.ts
410
+ var bold = (s) => `\x1B[1m${s}\x1B[0m`;
411
+ var dim = (s) => `\x1B[2m${s}\x1B[0m`;
412
+ var red = (s) => `\x1B[31m${s}\x1B[0m`;
413
+ var green = (s) => `\x1B[32m${s}\x1B[0m`;
414
+ var yellow = (s) => `\x1B[33m${s}\x1B[0m`;
415
+ var cyan = (s) => `\x1B[36m${s}\x1B[0m`;
416
+ function parseFlags(args2) {
417
+ const positional = [];
418
+ const flags = {};
419
+ for (let i = 0; i < args2.length; i++) {
420
+ const arg = args2[i];
421
+ if (arg === "--") {
422
+ positional.push(...args2.slice(i + 1));
423
+ break;
424
+ }
425
+ if (arg.startsWith("--")) {
426
+ const key = arg.slice(2);
427
+ const next = args2[i + 1];
428
+ if (next && !next.startsWith("-")) {
429
+ flags[key] = next;
430
+ i++;
431
+ } else {
432
+ flags[key] = true;
433
+ }
434
+ } else if (arg.startsWith("-") && arg.length === 2) {
435
+ const key = arg.slice(1);
436
+ const next = args2[i + 1];
437
+ if (next && !next.startsWith("-")) {
438
+ flags[key] = next;
439
+ i++;
440
+ } else {
441
+ flags[key] = true;
442
+ }
443
+ } else {
444
+ positional.push(arg);
445
+ }
446
+ }
447
+ return { positional, flags };
448
+ }
449
+ function requireAuth() {
450
+ const envKey = process.env.AIRCLAW_API_KEY;
451
+ const config = loadConfig();
452
+ const apiKey = envKey || config.apiKey;
453
+ const baseUrl = process.env.AIRCLAW_API_URL || config.baseUrl;
454
+ if (!apiKey) {
455
+ console.error(red("Not authenticated.") + " Run `airclaw auth login` or set AIRCLAW_API_KEY.");
456
+ process.exit(1);
457
+ }
458
+ return new AirClawAPI(apiKey, baseUrl);
459
+ }
460
+ async function resolveId(idArg, api) {
461
+ if (idArg) return idArg;
462
+ const config = loadConfig();
463
+ if (config.defaultMachine) return config.defaultMachine;
464
+ const { airclaws } = await api.list();
465
+ if (airclaws.length === 0) {
466
+ console.error("No AirClaws found. Create one with: airclaw create");
467
+ process.exit(1);
468
+ }
469
+ if (airclaws.length === 1) return airclaws[0].id;
470
+ console.error("Multiple AirClaws found. Specify an ID.");
471
+ console.error(dim("Run `airclaw list` to see your AirClaws."));
472
+ process.exit(1);
473
+ }
474
+ function formatTable(headers, rows, minWidths) {
475
+ const widths = headers.map((h, i) => {
476
+ const min = minWidths?.[i] ?? 0;
477
+ return Math.max(h.length, min, ...rows.map((r) => (r[i] || "").length));
478
+ });
479
+ const sep = widths.map((w) => "\u2500".repeat(w + 2)).join("\u253C");
480
+ const header = headers.map((h, i) => ` ${bold(h.padEnd(widths[i]))} `).join("\u2502");
481
+ const body = rows.map(
482
+ (row) => row.map((cell, i) => ` ${(cell || "").padEnd(widths[i])} `).join("\u2502")
483
+ ).join("\n");
484
+ return `${header}
485
+ \u2500${sep}\u2500
486
+ ${body}`;
487
+ }
488
+ function die(msg) {
489
+ console.error(red("Error: ") + msg);
490
+ process.exit(1);
491
+ }
492
+ async function confirm(prompt2) {
493
+ const { createInterface: createInterface3 } = await import("readline");
494
+ return new Promise((resolve) => {
495
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
496
+ rl.question(`${prompt2} (y/N) `, (answer) => {
497
+ rl.close();
498
+ resolve(answer.toLowerCase() === "y");
499
+ });
500
+ });
501
+ }
502
+
503
+ // src/commands/auth.ts
504
+ async function handleAuth(args2) {
505
+ const sub = args2[0];
506
+ switch (sub) {
507
+ case "login":
508
+ return login(args2.slice(1));
509
+ case "logout":
510
+ return logout();
511
+ case "status":
512
+ return status();
513
+ default:
514
+ console.log(`
515
+ ${bold("airclaw auth")} \u2014 Manage authentication
516
+
517
+ Commands:
518
+ login Save your API key
519
+ logout Remove stored credentials
520
+ status Show current auth state
521
+
522
+ Usage:
523
+ airclaw auth login
524
+ airclaw auth login --key sk-ac-...
525
+ airclaw auth logout
526
+ airclaw auth status
527
+
528
+ ${dim("Security: prefer the interactive prompt or AIRCLAW_API_KEY env var over --key.")}
529
+ ${dim("Command-line arguments are visible to other users via process listings (ps).")}
530
+ `);
531
+ }
532
+ }
533
+ async function login(args2) {
534
+ const { flags } = parseFlags(args2);
535
+ let apiKey = flags.key;
536
+ const baseUrl = flags.url;
537
+ if (!apiKey) {
538
+ apiKey = await prompt("API key: ");
539
+ if (!apiKey) {
540
+ console.error("No API key provided.");
541
+ process.exit(1);
542
+ }
543
+ }
544
+ apiKey = apiKey.trim();
545
+ if (!apiKey.startsWith("sk-ac-")) {
546
+ console.error(red("Invalid API key format.") + " Keys start with sk-ac-");
547
+ process.exit(1);
548
+ }
549
+ process.stdout.write(dim("Validating..."));
550
+ try {
551
+ const api = new AirClawAPI(apiKey, baseUrl);
552
+ await api.list();
553
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
554
+ console.log(green("Authenticated successfully."));
555
+ } catch (err) {
556
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
557
+ if (err.status === 401) {
558
+ console.error(red("Invalid API key."));
559
+ process.exit(1);
560
+ }
561
+ console.log("Could not validate key (network error). Saving anyway.");
562
+ }
563
+ const config = loadConfig();
564
+ config.apiKey = apiKey;
565
+ if (baseUrl) config.baseUrl = baseUrl;
566
+ saveConfig(config);
567
+ console.log(dim("Key saved to ~/.airclaw/config.json"));
568
+ }
569
+ function logout() {
570
+ const config = loadConfig();
571
+ if (!config.apiKey) {
572
+ console.log("Not logged in.");
573
+ return;
574
+ }
575
+ delete config.apiKey;
576
+ saveConfig(config);
577
+ console.log("Logged out. API key removed.");
578
+ }
579
+ function status() {
580
+ const config = loadConfig();
581
+ if (config.apiKey) {
582
+ const prefix = config.apiKey.slice(0, 12);
583
+ console.log(`${green("Authenticated")} \u2014 key: ${prefix}...`);
584
+ if (config.baseUrl) console.log(`API: ${config.baseUrl}`);
585
+ if (config.defaultMachine) console.log(`Default machine: ${config.defaultMachine}`);
586
+ } else {
587
+ console.log("Not authenticated. Run `airclaw auth login` to set up.");
588
+ }
589
+ }
590
+ function prompt(question) {
591
+ return new Promise((resolve) => {
592
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
593
+ rl.question(question, (answer) => {
594
+ rl.close();
595
+ resolve(answer);
596
+ });
597
+ });
598
+ }
599
+
600
+ // src/commands/machines.ts
601
+ async function handleList(args2) {
602
+ const { flags } = parseFlags(args2);
603
+ const api = requireAuth();
604
+ const source = flags.source;
605
+ const { airclaws } = await api.list(source);
606
+ if (flags.json) {
607
+ console.log(JSON.stringify(airclaws, null, 2));
608
+ return;
609
+ }
610
+ if (airclaws.length === 0) {
611
+ console.log("No AirClaws found. Create one with: airclaw create");
612
+ return;
613
+ }
614
+ const config = loadConfig();
615
+ const rows = airclaws.map((m) => [
616
+ m.id === config.defaultMachine ? `${m.id} ${dim("(default)")}` : m.id,
617
+ statusColor(m.status),
618
+ m.region || "\u2014",
619
+ m.source || "\u2014",
620
+ timeAgo(m.created_at)
621
+ ]);
622
+ console.log(formatTable(["ID", "Status", "Region", "Source", "Created"], rows));
623
+ console.log(dim(`
624
+ ${airclaws.length} AirClaw${airclaws.length !== 1 ? "s" : ""}`));
625
+ }
626
+ async function handleCreate(args2) {
627
+ const { flags } = parseFlags(args2);
628
+ const api = requireAuth();
629
+ process.stdout.write(dim("Creating AirClaw..."));
630
+ const machine = await api.create();
631
+ process.stdout.write("\r" + " ".repeat(30) + "\r");
632
+ if (flags.json) {
633
+ console.log(JSON.stringify(machine, null, 2));
634
+ return;
635
+ }
636
+ console.log(green("AirClaw created"));
637
+ console.log(` ID: ${bold(machine.id)}`);
638
+ console.log(` Status: ${statusColor(machine.status)}`);
639
+ console.log(` Region: ${machine.region || "\u2014"}`);
640
+ const { airclaws } = await api.list();
641
+ if (airclaws.length === 1) {
642
+ const config = loadConfig();
643
+ config.defaultMachine = machine.id;
644
+ saveConfig(config);
645
+ console.log(dim(" Set as default machine."));
646
+ }
647
+ }
648
+ async function handleInfo(args2) {
649
+ const { positional, flags } = parseFlags(args2);
650
+ const api = requireAuth();
651
+ const id = await resolveId(positional[0], api);
652
+ const machine = await api.get(id);
653
+ if (flags.json) {
654
+ console.log(JSON.stringify(machine, null, 2));
655
+ return;
656
+ }
657
+ console.log(`${bold("AirClaw")} ${machine.id}`);
658
+ console.log(` Status: ${statusColor(machine.status)}`);
659
+ console.log(` Region: ${machine.region || "\u2014"}`);
660
+ console.log(` Source: ${machine.source || "\u2014"}`);
661
+ console.log(` Environment: ${machine.environment || "\u2014"}`);
662
+ console.log(` Created: ${new Date(machine.created_at).toLocaleString()}`);
663
+ if (machine.last_active_at) {
664
+ console.log(` Last active: ${new Date(machine.last_active_at).toLocaleString()}`);
665
+ }
666
+ }
667
+ async function handleDestroy(args2) {
668
+ const { positional, flags } = parseFlags(args2);
669
+ const api = requireAuth();
670
+ const id = await resolveId(positional[0], api);
671
+ if (!flags.force && !flags.f) {
672
+ const yes = await confirm(`Destroy AirClaw ${bold(id)}? This cannot be undone.`);
673
+ if (!yes) {
674
+ console.log("Cancelled.");
675
+ return;
676
+ }
677
+ }
678
+ process.stdout.write(dim("Destroying..."));
679
+ await api.destroy(id);
680
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
681
+ console.log(red("Destroyed") + ` AirClaw ${id}`);
682
+ const config = loadConfig();
683
+ if (config.defaultMachine === id) {
684
+ delete config.defaultMachine;
685
+ saveConfig(config);
686
+ }
687
+ }
688
+ async function handleSleep(args2) {
689
+ const { positional } = parseFlags(args2);
690
+ const api = requireAuth();
691
+ const id = await resolveId(positional[0], api);
692
+ await api.sleep(id);
693
+ console.log(`AirClaw ${bold(id)} suspended. It will wake automatically on next use.`);
694
+ }
695
+ async function handleDefault(args2) {
696
+ const { positional } = parseFlags(args2);
697
+ const id = positional[0];
698
+ if (!id) {
699
+ const config2 = loadConfig();
700
+ if (config2.defaultMachine) {
701
+ console.log(`Default machine: ${bold(config2.defaultMachine)}`);
702
+ } else {
703
+ console.log("No default machine set. Usage: airclaw default <id>");
704
+ }
705
+ return;
706
+ }
707
+ const api = requireAuth();
708
+ await api.get(id);
709
+ const config = loadConfig();
710
+ config.defaultMachine = id;
711
+ saveConfig(config);
712
+ console.log(`Default machine set to ${bold(id)}`);
713
+ }
714
+ function statusColor(status3) {
715
+ switch (status3) {
716
+ case "started":
717
+ case "running":
718
+ return green(status3);
719
+ case "suspended":
720
+ case "stopping":
721
+ return yellow(status3);
722
+ case "stopped":
723
+ case "destroyed":
724
+ return red(status3);
725
+ default:
726
+ return status3;
727
+ }
728
+ }
729
+ function timeAgo(iso) {
730
+ const diff = Date.now() - new Date(iso).getTime();
731
+ const mins = Math.floor(diff / 6e4);
732
+ if (mins < 60) return `${mins}m ago`;
733
+ const hours = Math.floor(mins / 60);
734
+ if (hours < 24) return `${hours}h ago`;
735
+ const days = Math.floor(hours / 24);
736
+ return `${days}d ago`;
737
+ }
738
+
739
+ // src/commands/ssh-cmd.ts
740
+ import { existsSync as existsSync2 } from "fs";
741
+
742
+ // src/ssh.ts
743
+ import { spawnSync } from "child_process";
744
+ function connectSSH(target, command2) {
745
+ if (!/^[a-zA-Z0-9._-]+$/.test(target.hostname)) {
746
+ console.error("Invalid hostname.");
747
+ return 1;
748
+ }
749
+ if (!/^[a-zA-Z0-9_-]+$/.test(target.username)) {
750
+ console.error("Invalid username.");
751
+ return 1;
752
+ }
753
+ const needsTls = target.hostname.endsWith(".fly.dev");
754
+ const args2 = [
755
+ "-i",
756
+ target.keyPath,
757
+ "-p",
758
+ String(target.port),
759
+ "-o",
760
+ "StrictHostKeyChecking=accept-new",
761
+ "-o",
762
+ "UserKnownHostsFile=~/.airclaw/known_hosts",
763
+ "-o",
764
+ "LogLevel=ERROR"
765
+ ];
766
+ if (needsTls) {
767
+ args2.push(
768
+ "-o",
769
+ `ProxyCommand openssl s_client -connect %h:%p -quiet 2>/dev/null`
770
+ );
771
+ }
772
+ if (command2 && command2.length > 0 && process.stdin.isTTY) {
773
+ args2.push("-t");
774
+ }
775
+ args2.push(`${target.username}@${target.hostname}`);
776
+ if (command2 && command2.length > 0) {
777
+ args2.push("--", ...command2);
778
+ }
779
+ const result = spawnSync("ssh", args2, { stdio: "inherit" });
780
+ return result.status ?? 1;
781
+ }
782
+
783
+ // src/commands/ssh-cmd.ts
784
+ async function handleSSH(args2) {
785
+ const { positional, flags } = parseFlags(args2);
786
+ if (flags.list || flags.l) return listConnections();
787
+ if (flags.reset || flags.r) return resetConnections();
788
+ const target = positional[0];
789
+ const remoteCmd = positional.slice(1);
790
+ if (!target) {
791
+ return connectDefault(remoteCmd.length > 0 ? remoteCmd : void 0);
792
+ }
793
+ if (target.startsWith("otp_")) {
794
+ return redeemAndConnect(target, remoteCmd.length > 0 ? remoteCmd : void 0);
795
+ }
796
+ return connectById(target, remoteCmd.length > 0 ? remoteCmd : void 0);
797
+ }
798
+ async function connectById(id, command2) {
799
+ const api = requireAuth();
800
+ const saved = getSavedConnections().find((c) => c.id === id);
801
+ if (saved && existsSync2(saved.keyPath)) {
802
+ const exitCode2 = connectSSH(saved, command2);
803
+ if (exitCode2 === 0) process.exit(0);
804
+ console.log(dim("Saved credentials failed, fetching fresh ones..."));
805
+ }
806
+ process.stdout.write(dim("Getting SSH credentials..."));
807
+ const creds = await api.sshAccess(id);
808
+ process.stdout.write("\r" + " ".repeat(35) + "\r");
809
+ const keyData = await AirClawAPI.downloadKey(creds.key_url);
810
+ const keyPath = saveKey(id, keyData);
811
+ const target = {
812
+ hostname: creds.hostname,
813
+ port: creds.port,
814
+ username: creds.username,
815
+ keyPath
816
+ };
817
+ const exitCode = connectSSH(target, command2);
818
+ if (exitCode !== 0) {
819
+ console.error(
820
+ `
821
+ ${yellow("Connection failed.")} The machine may be suspended \u2014 try \`airclaw info ${id}\` to check status.`
822
+ );
823
+ }
824
+ process.exit(exitCode);
825
+ }
826
+ async function redeemAndConnect(code, command2) {
827
+ const config = loadConfig();
828
+ const baseUrl = config.baseUrl;
829
+ process.stdout.write(dim("Redeeming code..."));
830
+ const result = await AirClawAPI.redeemCode(code, baseUrl);
831
+ process.stdout.write("\r" + " ".repeat(25) + "\r");
832
+ console.log(`${green("Connected to")} ${bold(result.machineName)}`);
833
+ const keyData = await AirClawAPI.downloadKey(result.keyUrl);
834
+ const keyPath = saveKey(result.machineId, keyData);
835
+ const conn = {
836
+ id: result.machineId,
837
+ name: result.machineName,
838
+ hostname: result.hostname,
839
+ port: result.port,
840
+ username: result.username,
841
+ keyPath,
842
+ addedAt: (/* @__PURE__ */ new Date()).toISOString()
843
+ };
844
+ addSavedConnection(conn);
845
+ const exitCode = connectSSH(conn, command2);
846
+ if (exitCode !== 0) {
847
+ console.error(`
848
+ ${yellow("Connection failed.")} Run \`airclaw ssh --reset\` and request a new code.`);
849
+ }
850
+ process.exit(exitCode);
851
+ }
852
+ async function connectDefault(command2) {
853
+ const config = loadConfig();
854
+ if (config.apiKey) {
855
+ const api = new AirClawAPI(config.apiKey, config.baseUrl);
856
+ try {
857
+ const id = await resolveId(void 0, api);
858
+ return connectById(id, command2);
859
+ } catch {
860
+ }
861
+ }
862
+ const saved = getSavedConnections();
863
+ if (saved.length === 0) {
864
+ console.error("No AirClaws found.");
865
+ console.error(dim("Run `airclaw auth login` to authenticate, or `airclaw ssh <code>` to redeem a code."));
866
+ process.exit(1);
867
+ }
868
+ if (saved.length === 1) {
869
+ const exitCode2 = connectSSH(saved[0], command2);
870
+ process.exit(exitCode2);
871
+ }
872
+ console.log(bold("Saved connections:"));
873
+ saved.forEach((c, i) => {
874
+ console.log(` ${dim(`${i + 1}.`)} ${c.name} ${dim(`(${c.id})`)}`);
875
+ });
876
+ console.log();
877
+ const { createInterface: createInterface3 } = await import("readline");
878
+ const rl = createInterface3({ input: process.stdin, output: process.stdout });
879
+ const answer = await new Promise((resolve) => {
880
+ rl.question(`Select (1-${saved.length}): `, resolve);
881
+ });
882
+ rl.close();
883
+ const idx = parseInt(answer, 10) - 1;
884
+ if (isNaN(idx) || idx < 0 || idx >= saved.length) {
885
+ console.error("Invalid selection.");
886
+ process.exit(1);
887
+ }
888
+ const exitCode = connectSSH(saved[idx], command2);
889
+ process.exit(exitCode);
890
+ }
891
+ function listConnections() {
892
+ const config = loadConfig();
893
+ const saved = getSavedConnections();
894
+ const hasAuth = !!config.apiKey;
895
+ if (saved.length === 0 && !hasAuth) {
896
+ console.log("No saved connections.");
897
+ console.log(dim("Run `airclaw auth login` or `airclaw ssh <code>` to get started."));
898
+ return;
899
+ }
900
+ if (hasAuth) {
901
+ console.log(dim("Account machines are accessed via API key \u2014 run `airclaw list` to see them."));
902
+ console.log();
903
+ }
904
+ if (saved.length > 0) {
905
+ console.log(bold("Saved Connections"));
906
+ for (const conn of saved) {
907
+ const status3 = existsSync2(conn.keyPath) ? green("key ok") : red("key missing");
908
+ console.log(` ${conn.name} ${dim(`(${conn.id})`)}`);
909
+ console.log(` ${conn.hostname}:${conn.port} [${status3}]`);
910
+ }
911
+ } else {
912
+ console.log("No saved connections.");
913
+ }
914
+ }
915
+ function resetConnections() {
916
+ const count = resetAll();
917
+ if (count > 0) {
918
+ console.log(`Removed ${count} saved connection${count !== 1 ? "s" : ""} and keys.`);
919
+ } else {
920
+ console.log("No saved connections to remove.");
921
+ }
922
+ }
923
+
924
+ // src/commands/chat.ts
925
+ import { createInterface as createInterface2 } from "readline";
926
+ async function handleChat(args2) {
927
+ const { positional, flags } = parseFlags(args2);
928
+ const api = requireAuth();
929
+ const id = await resolveId(positional[0], api);
930
+ const singleMessage = flags.m || flags.message;
931
+ if (singleMessage) {
932
+ const messages = [{ role: "user", content: singleMessage }];
933
+ for await (const chunk of api.chatStream(id, messages)) {
934
+ process.stdout.write(chunk);
935
+ }
936
+ process.stdout.write("\n");
937
+ return;
938
+ }
939
+ console.log(`${bold("AirClaw Chat")} ${dim(`(${id})`)}`);
940
+ console.log(dim("Type a message and press Enter. Ctrl+C to exit.\n"));
941
+ const history = [];
942
+ const rl = createInterface2({
943
+ input: process.stdin,
944
+ output: process.stdout,
945
+ prompt: cyan("You: ")
946
+ });
947
+ rl.prompt();
948
+ for await (const line of rl) {
949
+ const input = line.trim();
950
+ if (!input) {
951
+ rl.prompt();
952
+ continue;
953
+ }
954
+ if (input.toLowerCase() === "exit" || input.toLowerCase() === "quit") {
955
+ break;
956
+ }
957
+ history.push({ role: "user", content: input });
958
+ process.stdout.write(bold("AirClaw: "));
959
+ let response = "";
960
+ try {
961
+ for await (const chunk of api.chatStream(id, history)) {
962
+ process.stdout.write(chunk);
963
+ response += chunk;
964
+ }
965
+ } catch (err) {
966
+ process.stdout.write(red(`
967
+ Error: ${err.message}`));
968
+ }
969
+ process.stdout.write("\n\n");
970
+ if (response) {
971
+ history.push({ role: "assistant", content: response });
972
+ }
973
+ rl.prompt();
974
+ }
975
+ console.log(dim("\nGoodbye."));
976
+ }
977
+
978
+ // src/commands/gateway.ts
979
+ async function handleConfig(args2) {
980
+ const { positional, flags } = parseFlags(args2);
981
+ const api = requireAuth();
982
+ const id = await resolveId(positional[0], api);
983
+ const setVal = flags.set;
984
+ if (setVal) {
985
+ let patch;
986
+ try {
987
+ patch = JSON.parse(setVal);
988
+ } catch {
989
+ die(`Invalid JSON for --set. Usage: airclaw config <id> --set '{"key": "value"}'`);
990
+ }
991
+ const result = await api.patchConfig(id, patch);
992
+ console.log(JSON.stringify(result, null, 2));
993
+ } else {
994
+ const config = await api.getConfig(id);
995
+ console.log(JSON.stringify(config, null, 2));
996
+ }
997
+ }
998
+ async function handleModels(args2) {
999
+ const { positional, flags } = parseFlags(args2);
1000
+ const api = requireAuth();
1001
+ const id = await resolveId(positional[0], api);
1002
+ const models = await api.listModels(id);
1003
+ if (flags.json) {
1004
+ console.log(JSON.stringify(models, null, 2));
1005
+ return;
1006
+ }
1007
+ if (Array.isArray(models)) {
1008
+ for (const m of models) {
1009
+ const name = typeof m === "string" ? m : m.id || m.name || JSON.stringify(m);
1010
+ console.log(` ${name}`);
1011
+ }
1012
+ } else {
1013
+ console.log(JSON.stringify(models, null, 2));
1014
+ }
1015
+ }
1016
+ async function handleRpc(args2) {
1017
+ const { positional, flags } = parseFlags(args2);
1018
+ const api = requireAuth();
1019
+ const id = await resolveId(positional[0], api);
1020
+ const method = positional[1];
1021
+ if (!method) {
1022
+ die("Usage: airclaw rpc <id> <method> [--params '{...}']");
1023
+ }
1024
+ let params;
1025
+ const paramsStr = flags.params;
1026
+ if (paramsStr) {
1027
+ try {
1028
+ params = JSON.parse(paramsStr);
1029
+ } catch {
1030
+ die("Invalid JSON for --params");
1031
+ }
1032
+ }
1033
+ const timeoutMs = flags.timeout ? parseInt(flags.timeout, 10) : void 0;
1034
+ const result = await api.rpc(id, method, params, timeoutMs);
1035
+ console.log(JSON.stringify(result, null, 2));
1036
+ }
1037
+ async function handleTools(args2) {
1038
+ const sub = args2[0];
1039
+ if (sub !== "invoke") {
1040
+ console.log(`
1041
+ ${bold("airclaw tools")} \u2014 Invoke agent tools
1042
+
1043
+ Usage:
1044
+ airclaw tools invoke <id> <tool-name> [--args '{...}']
1045
+ `);
1046
+ return;
1047
+ }
1048
+ const { positional, flags } = parseFlags(args2.slice(1));
1049
+ const api = requireAuth();
1050
+ const id = await resolveId(positional[0], api);
1051
+ const toolName = positional[1];
1052
+ if (!toolName) {
1053
+ die("Usage: airclaw tools invoke <id> <tool-name> [--args '{...}']");
1054
+ }
1055
+ let toolArgs;
1056
+ const argsStr = flags.args;
1057
+ if (argsStr) {
1058
+ try {
1059
+ toolArgs = JSON.parse(argsStr);
1060
+ } catch {
1061
+ die("Invalid JSON for --args");
1062
+ }
1063
+ }
1064
+ const result = await api.invokeTool(id, toolName, toolArgs);
1065
+ console.log(JSON.stringify(result, null, 2));
1066
+ }
1067
+
1068
+ // src/commands/drive.ts
1069
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2, statSync } from "fs";
1070
+ import { basename, extname } from "path";
1071
+ var MIME_TYPES = {
1072
+ ".txt": "text/plain",
1073
+ ".html": "text/html",
1074
+ ".css": "text/css",
1075
+ ".js": "application/javascript",
1076
+ ".ts": "application/typescript",
1077
+ ".json": "application/json",
1078
+ ".xml": "application/xml",
1079
+ ".csv": "text/csv",
1080
+ ".md": "text/markdown",
1081
+ ".png": "image/png",
1082
+ ".jpg": "image/jpeg",
1083
+ ".jpeg": "image/jpeg",
1084
+ ".gif": "image/gif",
1085
+ ".svg": "image/svg+xml",
1086
+ ".webp": "image/webp",
1087
+ ".pdf": "application/pdf",
1088
+ ".zip": "application/zip",
1089
+ ".tar": "application/x-tar",
1090
+ ".gz": "application/gzip",
1091
+ ".mp3": "audio/mpeg",
1092
+ ".mp4": "video/mp4",
1093
+ ".wav": "audio/wav"
1094
+ };
1095
+ async function handleDrive(args2) {
1096
+ const sub = args2[0];
1097
+ switch (sub) {
1098
+ case "upload":
1099
+ return upload(args2.slice(1));
1100
+ case "download":
1101
+ return download(args2.slice(1));
1102
+ case "list":
1103
+ case "ls":
1104
+ return list(args2.slice(1));
1105
+ case "delete":
1106
+ case "rm":
1107
+ return del(args2.slice(1));
1108
+ case "share":
1109
+ return share(args2.slice(1));
1110
+ default:
1111
+ console.log(`
1112
+ ${bold("airclaw drive")} \u2014 Manage AirClaw files
1113
+
1114
+ Commands:
1115
+ upload <id> <file> [remote-path] Upload a file
1116
+ download <id> <remote-path> [local] Download a file
1117
+ list <id> [prefix] List files
1118
+ delete <id> <path> Delete a file
1119
+ share <id> <path> Create a shareable link
1120
+ `);
1121
+ }
1122
+ }
1123
+ async function upload(args2) {
1124
+ const { positional } = parseFlags(args2);
1125
+ const api = requireAuth();
1126
+ const id = await resolveId(positional[0], api);
1127
+ const localPath = positional[1];
1128
+ const remotePath = positional[2] || basename(localPath || "");
1129
+ if (!localPath) die("Usage: airclaw drive upload <id> <file> [remote-path]");
1130
+ const ext = extname(localPath).toLowerCase();
1131
+ const contentType = MIME_TYPES[ext] || "application/octet-stream";
1132
+ process.stdout.write(dim("Uploading..."));
1133
+ const { url } = await api.driveUploadUrl(id, remotePath, contentType);
1134
+ const fileData = readFileSync2(localPath);
1135
+ const uploadRes = await fetch(url, {
1136
+ method: "PUT",
1137
+ headers: { "Content-Type": contentType },
1138
+ body: fileData
1139
+ });
1140
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
1141
+ if (!uploadRes.ok) {
1142
+ die(`Upload failed: HTTP ${uploadRes.status}`);
1143
+ }
1144
+ const size = statSync(localPath).size;
1145
+ console.log(`${green("Uploaded")} ${remotePath} (${formatBytes(size)})`);
1146
+ }
1147
+ async function download(args2) {
1148
+ const { positional } = parseFlags(args2);
1149
+ const api = requireAuth();
1150
+ const id = await resolveId(positional[0], api);
1151
+ const remotePath = positional[1];
1152
+ const localPath = positional[2] || basename(remotePath || "");
1153
+ if (!remotePath) die("Usage: airclaw drive download <id> <remote-path> [local-path]");
1154
+ process.stdout.write(dim("Downloading..."));
1155
+ const { url } = await api.driveDownloadUrl(id, remotePath);
1156
+ const res = await fetch(url);
1157
+ process.stdout.write("\r" + " ".repeat(20) + "\r");
1158
+ if (!res.ok) {
1159
+ die(`Download failed: HTTP ${res.status}`);
1160
+ }
1161
+ const buffer = Buffer.from(await res.arrayBuffer());
1162
+ writeFileSync2(localPath, buffer);
1163
+ console.log(`${green("Downloaded")} ${remotePath} \u2192 ${localPath} (${formatBytes(buffer.length)})`);
1164
+ }
1165
+ async function list(args2) {
1166
+ const { positional, flags } = parseFlags(args2);
1167
+ const api = requireAuth();
1168
+ const id = await resolveId(positional[0], api);
1169
+ const prefix = positional[1];
1170
+ const limit = flags.limit ? parseInt(flags.limit, 10) : void 0;
1171
+ const result = await api.driveList(id, prefix, limit);
1172
+ if (flags.json) {
1173
+ console.log(JSON.stringify(result, null, 2));
1174
+ return;
1175
+ }
1176
+ if (result.files.length === 0) {
1177
+ console.log("No files found.");
1178
+ return;
1179
+ }
1180
+ const rows = result.files.map((f) => [
1181
+ f.key,
1182
+ formatBytes(f.size),
1183
+ new Date(f.last_modified).toLocaleString()
1184
+ ]);
1185
+ console.log(formatTable(["File", "Size", "Modified"], rows));
1186
+ console.log(dim(`
1187
+ Total: ${formatBytes(result.total_size)}`));
1188
+ }
1189
+ async function del(args2) {
1190
+ const { positional } = parseFlags(args2);
1191
+ const api = requireAuth();
1192
+ const id = await resolveId(positional[0], api);
1193
+ const path = positional[1];
1194
+ if (!path) die("Usage: airclaw drive delete <id> <path>");
1195
+ await api.driveDelete(id, path);
1196
+ console.log(`Deleted ${path}`);
1197
+ }
1198
+ async function share(args2) {
1199
+ const { positional } = parseFlags(args2);
1200
+ const api = requireAuth();
1201
+ const id = await resolveId(positional[0], api);
1202
+ const path = positional[1];
1203
+ if (!path) die("Usage: airclaw drive share <id> <path>");
1204
+ const result = await api.driveShare(id, path);
1205
+ console.log(result.url);
1206
+ }
1207
+ function formatBytes(bytes) {
1208
+ if (bytes === 0) return "0 B";
1209
+ const units = ["B", "KB", "MB", "GB"];
1210
+ const i = Math.floor(Math.log(bytes) / Math.log(1024));
1211
+ const val = bytes / Math.pow(1024, i);
1212
+ return `${val.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
1213
+ }
1214
+
1215
+ // src/commands/mail.ts
1216
+ async function handleMail(args2) {
1217
+ const sub = args2[0];
1218
+ switch (sub) {
1219
+ case "list":
1220
+ case "ls":
1221
+ return list2(args2.slice(1));
1222
+ case "read":
1223
+ return read(args2.slice(1));
1224
+ case "send":
1225
+ return send(args2.slice(1));
1226
+ case "reply":
1227
+ return reply(args2.slice(1));
1228
+ case "status":
1229
+ return status2(args2.slice(1));
1230
+ case "claim":
1231
+ return claim(args2.slice(1));
1232
+ default:
1233
+ console.log(`
1234
+ ${bold("airclaw mail")} \u2014 Manage AirClaw email
1235
+
1236
+ Commands:
1237
+ list <id> List emails
1238
+ read <id> <email-id> Read an email
1239
+ send <id> --to <addr> --subject <subj> --body <text>
1240
+ reply <id> <email-id> --body <text>
1241
+ status <id> Check email configuration
1242
+ claim <id> <username> Claim an email address
1243
+ `);
1244
+ }
1245
+ }
1246
+ async function list2(args2) {
1247
+ const { positional, flags } = parseFlags(args2);
1248
+ const api = requireAuth();
1249
+ const id = await resolveId(positional[0], api);
1250
+ const limit = flags.limit ? parseInt(flags.limit, 10) : void 0;
1251
+ const pageToken = flags.page;
1252
+ const result = await api.mailList(id, limit, pageToken);
1253
+ if (flags.json) {
1254
+ console.log(JSON.stringify(result, null, 2));
1255
+ return;
1256
+ }
1257
+ if (result.emails.length === 0) {
1258
+ console.log("No emails.");
1259
+ return;
1260
+ }
1261
+ for (const email of result.emails) {
1262
+ const from = email.from?.address || email.from || "unknown";
1263
+ const subject = email.subject || "(no subject)";
1264
+ const date = email.created_at ? new Date(email.created_at).toLocaleString() : "";
1265
+ console.log(`${dim(email.id || "")} ${bold(subject)}`);
1266
+ console.log(` From: ${from} ${dim(date)}`);
1267
+ console.log();
1268
+ }
1269
+ if (result.next_page_token) {
1270
+ console.log(dim(`More emails available. Use --page ${result.next_page_token}`));
1271
+ }
1272
+ }
1273
+ async function read(args2) {
1274
+ const { positional, flags } = parseFlags(args2);
1275
+ const api = requireAuth();
1276
+ const id = await resolveId(positional[0], api);
1277
+ const emailId = positional[1];
1278
+ if (!emailId) die("Usage: airclaw mail read <id> <email-id>");
1279
+ const { email } = await api.mailRead(id, emailId);
1280
+ if (flags.json) {
1281
+ console.log(JSON.stringify(email, null, 2));
1282
+ return;
1283
+ }
1284
+ console.log(bold(`Subject: ${email.subject || "(no subject)"}`));
1285
+ console.log(`From: ${email.from?.address || email.from || "unknown"}`);
1286
+ if (email.to) console.log(`To: ${Array.isArray(email.to) ? email.to.join(", ") : email.to}`);
1287
+ if (email.created_at) console.log(`Date: ${new Date(email.created_at).toLocaleString()}`);
1288
+ console.log("\u2500".repeat(60));
1289
+ console.log(email.text || email.html || "(empty)");
1290
+ }
1291
+ async function send(args2) {
1292
+ const { positional, flags } = parseFlags(args2);
1293
+ const api = requireAuth();
1294
+ const id = await resolveId(positional[0], api);
1295
+ const to = flags.to;
1296
+ const subject = flags.subject;
1297
+ const body = flags.body;
1298
+ if (!to) die("Usage: airclaw mail send <id> --to <address> --subject <subject> --body <text>");
1299
+ if (!subject) die("--subject is required");
1300
+ if (!body) die("--body is required");
1301
+ const recipients = to.split(",").map((s) => s.trim());
1302
+ await api.mailSend(id, {
1303
+ to: recipients,
1304
+ subject,
1305
+ text: body
1306
+ });
1307
+ console.log(green("Email sent."));
1308
+ }
1309
+ async function reply(args2) {
1310
+ const { positional, flags } = parseFlags(args2);
1311
+ const api = requireAuth();
1312
+ const id = await resolveId(positional[0], api);
1313
+ const emailId = positional[1];
1314
+ if (!emailId) die("Usage: airclaw mail reply <id> <email-id> --body <text>");
1315
+ const body = flags.body;
1316
+ if (!body) die("--body is required");
1317
+ await api.mailReply(id, emailId, { text: body });
1318
+ console.log(green("Reply sent."));
1319
+ }
1320
+ async function status2(args2) {
1321
+ const { positional, flags } = parseFlags(args2);
1322
+ const api = requireAuth();
1323
+ const id = await resolveId(positional[0], api);
1324
+ const result = await api.mailStatus(id);
1325
+ if (flags.json) {
1326
+ console.log(JSON.stringify(result, null, 2));
1327
+ return;
1328
+ }
1329
+ if (result.configured) {
1330
+ console.log(`Email: ${green(result.address || "configured")}`);
1331
+ if (result.display_name) console.log(`Name: ${result.display_name}`);
1332
+ } else {
1333
+ console.log("Email not configured. Run `airclaw mail claim <id> <username>` to set up.");
1334
+ }
1335
+ }
1336
+ async function claim(args2) {
1337
+ const { positional, flags } = parseFlags(args2);
1338
+ const api = requireAuth();
1339
+ const id = await resolveId(positional[0], api);
1340
+ const username = positional[1];
1341
+ if (!username) die("Usage: airclaw mail claim <id> <username>");
1342
+ const displayName = flags.name;
1343
+ const result = await api.mailClaim(id, username, displayName);
1344
+ console.log(`${green("Email claimed:")} ${result.address}`);
1345
+ }
1346
+
1347
+ // src/commands/todos.ts
1348
+ async function handleTodos(args2) {
1349
+ const sub = args2[0];
1350
+ switch (sub) {
1351
+ case "list":
1352
+ case "ls":
1353
+ return list3(args2.slice(1));
1354
+ case "create":
1355
+ case "add":
1356
+ return create(args2.slice(1));
1357
+ case "update":
1358
+ return update(args2.slice(1));
1359
+ case "delete":
1360
+ case "rm":
1361
+ return del2(args2.slice(1));
1362
+ case "tags":
1363
+ return tags(args2.slice(1));
1364
+ default:
1365
+ console.log(`
1366
+ ${bold("airclaw todos")} \u2014 Manage AirClaw todos
1367
+
1368
+ Commands:
1369
+ list <id> [--status <status>] [--tag <tag>]
1370
+ create <id> <text>
1371
+ update <id> <todo-id> [--status <status>] [--text <text>]
1372
+ delete <id> <todo-id>
1373
+ tags <id>
1374
+ `);
1375
+ }
1376
+ }
1377
+ async function list3(args2) {
1378
+ const { positional, flags } = parseFlags(args2);
1379
+ const api = requireAuth();
1380
+ const id = await resolveId(positional[0], api);
1381
+ const params = {};
1382
+ if (flags.status) params.status = flags.status;
1383
+ if (flags.tag) params.tag = flags.tag;
1384
+ const result = await api.todosList(id, params);
1385
+ if (flags.json) {
1386
+ console.log(JSON.stringify(result, null, 2));
1387
+ return;
1388
+ }
1389
+ const todos = Array.isArray(result) ? result : result?.todos || [];
1390
+ if (todos.length === 0) {
1391
+ console.log("No todos.");
1392
+ return;
1393
+ }
1394
+ for (const todo of todos) {
1395
+ const check = todo.status === "completed" ? "\x1B[32m[x]\x1B[0m" : "[ ]";
1396
+ const tags2 = todo.tags?.length ? dim(` [${todo.tags.join(", ")}]`) : "";
1397
+ console.log(` ${check} ${todo.text || todo.title || todo.id}${tags2} ${dim(todo.id || "")}`);
1398
+ }
1399
+ }
1400
+ async function create(args2) {
1401
+ const { positional } = parseFlags(args2);
1402
+ const api = requireAuth();
1403
+ const id = await resolveId(positional[0], api);
1404
+ const text = positional.slice(1).join(" ");
1405
+ if (!text) die("Usage: airclaw todos create <id> <text>");
1406
+ const result = await api.todosCreate(id, { text });
1407
+ console.log(`${green("Created")} todo ${dim(result?.id || "")}`);
1408
+ }
1409
+ async function update(args2) {
1410
+ const { positional, flags } = parseFlags(args2);
1411
+ const api = requireAuth();
1412
+ const id = await resolveId(positional[0], api);
1413
+ const todoId = positional[1];
1414
+ if (!todoId) die("Usage: airclaw todos update <id> <todo-id> [--status <status>] [--text <text>]");
1415
+ const updates = {};
1416
+ if (flags.status) updates.status = flags.status;
1417
+ if (flags.text) updates.text = flags.text;
1418
+ if (Object.keys(updates).length === 0) {
1419
+ die("Specify at least one update: --status or --text");
1420
+ }
1421
+ await api.todosUpdate(id, todoId, updates);
1422
+ console.log(green("Updated."));
1423
+ }
1424
+ async function del2(args2) {
1425
+ const { positional } = parseFlags(args2);
1426
+ const api = requireAuth();
1427
+ const id = await resolveId(positional[0], api);
1428
+ const todoId = positional[1];
1429
+ if (!todoId) die("Usage: airclaw todos delete <id> <todo-id>");
1430
+ await api.todosDelete(id, todoId);
1431
+ console.log("Deleted.");
1432
+ }
1433
+ async function tags(args2) {
1434
+ const { positional, flags } = parseFlags(args2);
1435
+ const api = requireAuth();
1436
+ const id = await resolveId(positional[0], api);
1437
+ const result = await api.todosTags(id);
1438
+ if (flags.json) {
1439
+ console.log(JSON.stringify(result, null, 2));
1440
+ return;
1441
+ }
1442
+ const tagList = Array.isArray(result) ? result : result?.tags || [];
1443
+ if (tagList.length === 0) {
1444
+ console.log("No tags.");
1445
+ return;
1446
+ }
1447
+ for (const tag of tagList) {
1448
+ console.log(` ${tag.name || tag.id || tag}`);
1449
+ }
1450
+ }
1451
+
1452
+ // src/commands/keys.ts
1453
+ async function handleKeys(args2) {
1454
+ const sub = args2[0];
1455
+ switch (sub) {
1456
+ case "list":
1457
+ case "ls":
1458
+ return list4(args2.slice(1));
1459
+ case "create":
1460
+ return create2(args2.slice(1));
1461
+ case "revoke":
1462
+ return revoke(args2.slice(1));
1463
+ default:
1464
+ console.log(`
1465
+ ${bold("airclaw keys")} \u2014 Manage API keys
1466
+
1467
+ Commands:
1468
+ list List your API keys
1469
+ create [name] Create a new key
1470
+ revoke <key-id> Revoke a key
1471
+ `);
1472
+ }
1473
+ }
1474
+ async function list4(args2) {
1475
+ const { flags } = parseFlags(args2);
1476
+ const api = requireAuth();
1477
+ const { keys } = await api.keysList();
1478
+ if (flags.json) {
1479
+ console.log(JSON.stringify(keys, null, 2));
1480
+ return;
1481
+ }
1482
+ if (keys.length === 0) {
1483
+ console.log("No API keys.");
1484
+ return;
1485
+ }
1486
+ const rows = keys.map((k) => [
1487
+ k.id,
1488
+ k.key_prefix + "...",
1489
+ k.name || "\u2014",
1490
+ new Date(k.created_at).toLocaleDateString(),
1491
+ k.last_used_at ? new Date(k.last_used_at).toLocaleDateString() : "never"
1492
+ ]);
1493
+ console.log(formatTable(["ID", "Key", "Name", "Created", "Last Used"], rows));
1494
+ }
1495
+ async function create2(args2) {
1496
+ const { positional } = parseFlags(args2);
1497
+ const api = requireAuth();
1498
+ const name = positional.join(" ") || void 0;
1499
+ const result = await api.keysCreate(name);
1500
+ console.log(green("API key created"));
1501
+ console.log();
1502
+ console.log(` ${bold(result.key)}`);
1503
+ console.log();
1504
+ console.log(dim("Save this key \u2014 it won't be shown again."));
1505
+ }
1506
+ async function revoke(args2) {
1507
+ const { positional } = parseFlags(args2);
1508
+ const api = requireAuth();
1509
+ const keyId = positional[0];
1510
+ if (!keyId) die("Usage: airclaw keys revoke <key-id>");
1511
+ await api.keysRevoke(keyId);
1512
+ console.log(`${red("Revoked")} key ${keyId}`);
1513
+ }
1514
+
1515
+ // src/cli.ts
1516
+ function getVersion() {
1517
+ try {
1518
+ const __dirname = dirname(fileURLToPath(import.meta.url));
1519
+ const pkg = JSON.parse(readFileSync3(join2(__dirname, "..", "package.json"), "utf-8"));
1520
+ return pkg.version;
1521
+ } catch {
1522
+ return "1.0.0";
1523
+ }
1524
+ }
1525
+ function showHelp(exitCode = 0) {
1526
+ const v = getVersion();
1527
+ console.log(`
1528
+ ${bold("airclaw")} v${v} \u2014 CLI for AirClaw
1529
+
1530
+ ${bold("Usage:")}
1531
+ airclaw <command> [options]
1532
+
1533
+ ${bold("Authentication:")}
1534
+ auth login Save your API key
1535
+ auth logout Remove stored credentials
1536
+ auth status Show current auth state
1537
+
1538
+ ${bold("Machines:")}
1539
+ list List your AirClaws
1540
+ create Create a new AirClaw
1541
+ info <id> Get AirClaw details
1542
+ destroy <id> Destroy an AirClaw
1543
+ sleep <id> Suspend an AirClaw
1544
+ default <id> Set default machine
1545
+
1546
+ ${bold("Access:")}
1547
+ ssh <id|code> SSH into an AirClaw
1548
+ ssh <id> <command...> Run a remote command
1549
+ chat <id> Chat with an AirClaw
1550
+ chat <id> -m "message" Send a single message
1551
+
1552
+ ${bold("Gateway:")}
1553
+ config <id> View/update configuration
1554
+ models <id> List available models
1555
+ rpc <id> <method> Send RPC to gateway
1556
+ tools invoke <id> <n> Invoke a tool
1557
+
1558
+ ${bold("Resources:")}
1559
+ drive <subcommand> Manage files (upload, download, list, delete, share)
1560
+ mail <subcommand> Manage email (list, read, send, reply, status, claim)
1561
+ todos <subcommand> Manage todos (list, create, update, delete, tags)
1562
+
1563
+ ${bold("API Keys:")}
1564
+ keys list List your API keys
1565
+ keys create [name] Create a new key
1566
+ keys revoke <id> Revoke a key
1567
+
1568
+ ${bold("Options:")}
1569
+ -h, --help Show help
1570
+ -v, --version Show version
1571
+ --json Output as JSON (where applicable)
1572
+
1573
+ ${bold("Environment:")}
1574
+ AIRCLAW_API_KEY API key (alternative to 'auth login')
1575
+ AIRCLAW_API_URL Custom API base URL
1576
+
1577
+ ${bold("Security:")}
1578
+ Prefer ${bold("airclaw auth login")} (interactive prompt) or the ${bold("AIRCLAW_API_KEY")}
1579
+ env var over ${bold("--key")} on the command line. Command-line arguments are
1580
+ visible to other users on the system via process listings (ps).
1581
+
1582
+ ${dim("When <id> is omitted, uses default machine (or auto-selects if only one exists).")}
1583
+ ${dim("Run 'airclaw <command> --help' for command-specific help.")}
1584
+ `);
1585
+ process.exit(exitCode);
1586
+ }
1587
+ var args = process.argv.slice(2);
1588
+ var command = args[0];
1589
+ async function main() {
1590
+ switch (command) {
1591
+ case "auth":
1592
+ return handleAuth(args.slice(1));
1593
+ case "list":
1594
+ case "ls":
1595
+ return handleList(args.slice(1));
1596
+ case "create":
1597
+ return handleCreate(args.slice(1));
1598
+ case "info":
1599
+ return handleInfo(args.slice(1));
1600
+ case "destroy":
1601
+ return handleDestroy(args.slice(1));
1602
+ case "sleep":
1603
+ return handleSleep(args.slice(1));
1604
+ case "default":
1605
+ return handleDefault(args.slice(1));
1606
+ case "ssh":
1607
+ return handleSSH(args.slice(1));
1608
+ case "chat":
1609
+ return handleChat(args.slice(1));
1610
+ case "config":
1611
+ return handleConfig(args.slice(1));
1612
+ case "models":
1613
+ return handleModels(args.slice(1));
1614
+ case "rpc":
1615
+ return handleRpc(args.slice(1));
1616
+ case "tools":
1617
+ return handleTools(args.slice(1));
1618
+ case "drive":
1619
+ return handleDrive(args.slice(1));
1620
+ case "mail":
1621
+ return handleMail(args.slice(1));
1622
+ case "todos":
1623
+ return handleTodos(args.slice(1));
1624
+ case "keys":
1625
+ return handleKeys(args.slice(1));
1626
+ case "--help":
1627
+ case "-h":
1628
+ case "help":
1629
+ showHelp();
1630
+ case "--version":
1631
+ case "-v":
1632
+ console.log(getVersion());
1633
+ process.exit(0);
1634
+ case void 0:
1635
+ return handleSSH([]);
1636
+ default:
1637
+ console.error(`Unknown command: ${command}`);
1638
+ console.error(dim("Run `airclaw --help` for usage."));
1639
+ process.exit(1);
1640
+ }
1641
+ }
1642
+ main().catch((err) => {
1643
+ if (err instanceof ApiError) {
1644
+ console.error(red("Error: ") + err.message);
1645
+ if (err.status === 401) {
1646
+ console.error(dim("Run `airclaw auth login` to authenticate."));
1647
+ } else if (err.status === 404) {
1648
+ console.error(dim("The resource was not found. Check the ID and try again."));
1649
+ } else if (err.status === 429) {
1650
+ console.error(dim("Rate limit exceeded. Wait a moment and try again."));
1651
+ }
1652
+ process.exit(1);
1653
+ }
1654
+ if (err.code === "ECONNREFUSED" || err.code === "ENOTFOUND") {
1655
+ console.error(red("Connection failed.") + " Could not reach the API.");
1656
+ process.exit(1);
1657
+ }
1658
+ console.error(red("Error: ") + (err.message || err));
1659
+ process.exit(1);
1660
+ });