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.
- package/dist/cli.js +1660 -0
- 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
|
+
});
|