sakuraai 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -0
- package/dist/index.js +1996 -0
- package/package.json +59 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1996 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
|
|
5
|
+
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
|
|
6
|
+
}) : x)(function(x) {
|
|
7
|
+
if (typeof require !== "undefined") return require.apply(this, arguments);
|
|
8
|
+
throw Error('Dynamic require of "' + x + '" is not supported');
|
|
9
|
+
});
|
|
10
|
+
var __esm = (fn, res) => function __init() {
|
|
11
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
12
|
+
};
|
|
13
|
+
var __export = (target, all) => {
|
|
14
|
+
for (var name in all)
|
|
15
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/config.ts
|
|
19
|
+
import os from "os";
|
|
20
|
+
import path from "path";
|
|
21
|
+
import fs from "fs";
|
|
22
|
+
import { randomUUID } from "crypto";
|
|
23
|
+
function ensureDir(dir) {
|
|
24
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
function readJson(file, fallback) {
|
|
27
|
+
try {
|
|
28
|
+
return { ...fallback, ...JSON.parse(fs.readFileSync(file, "utf8")) };
|
|
29
|
+
} catch {
|
|
30
|
+
return fallback;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function writeJson(file, value) {
|
|
34
|
+
ensureDir(path.dirname(file));
|
|
35
|
+
fs.writeFileSync(file, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
36
|
+
}
|
|
37
|
+
function defaultConfig() {
|
|
38
|
+
const hostname2 = os.hostname();
|
|
39
|
+
return {
|
|
40
|
+
machineId: randomUUID(),
|
|
41
|
+
machineName: hostname2,
|
|
42
|
+
defaultWorkspace: "local",
|
|
43
|
+
workspaces: [{ id: "local", name: "Local", slug: "local" }],
|
|
44
|
+
projects: [],
|
|
45
|
+
agentConfigs: [],
|
|
46
|
+
daemon: { host: DEFAULT_HOST, port: DEFAULT_PORT }
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function loadConfig() {
|
|
50
|
+
if (_config) return _config;
|
|
51
|
+
const cfg = readJson(CONFIG_PATH, defaultConfig());
|
|
52
|
+
if (!fs.existsSync(CONFIG_PATH)) writeJson(CONFIG_PATH, cfg);
|
|
53
|
+
_config = cfg;
|
|
54
|
+
return cfg;
|
|
55
|
+
}
|
|
56
|
+
function saveConfig(cfg) {
|
|
57
|
+
_config = cfg;
|
|
58
|
+
writeJson(CONFIG_PATH, cfg);
|
|
59
|
+
}
|
|
60
|
+
function updateConfig(mut) {
|
|
61
|
+
const cfg = loadConfig();
|
|
62
|
+
mut(cfg);
|
|
63
|
+
saveConfig(cfg);
|
|
64
|
+
return cfg;
|
|
65
|
+
}
|
|
66
|
+
function loadAuth() {
|
|
67
|
+
return readJson(AUTH_PATH, {});
|
|
68
|
+
}
|
|
69
|
+
function saveAuth(auth) {
|
|
70
|
+
writeJson(AUTH_PATH, auth);
|
|
71
|
+
}
|
|
72
|
+
function clearAuth() {
|
|
73
|
+
try {
|
|
74
|
+
fs.rmSync(AUTH_PATH, { force: true });
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function ensureSakuraDirs() {
|
|
79
|
+
ensureDir(SAKURA_DIR);
|
|
80
|
+
ensureDir(LOG_DIR);
|
|
81
|
+
}
|
|
82
|
+
var SAKURA_DIR, CONFIG_PATH, AUTH_PATH, DAEMON_PATH, PID_PATH, LOG_DIR, LOG_PATH, DEFAULT_PORT, DEFAULT_HOST, _config;
|
|
83
|
+
var init_config = __esm({
|
|
84
|
+
"src/config.ts"() {
|
|
85
|
+
"use strict";
|
|
86
|
+
SAKURA_DIR = process.env.SAKURA_HOME ?? path.join(os.homedir(), ".sakura");
|
|
87
|
+
CONFIG_PATH = path.join(SAKURA_DIR, "config.json");
|
|
88
|
+
AUTH_PATH = path.join(SAKURA_DIR, "auth.json");
|
|
89
|
+
DAEMON_PATH = path.join(SAKURA_DIR, "daemon.json");
|
|
90
|
+
PID_PATH = path.join(SAKURA_DIR, "daemon.pid");
|
|
91
|
+
LOG_DIR = path.join(SAKURA_DIR, "logs");
|
|
92
|
+
LOG_PATH = path.join(LOG_DIR, "daemon.log");
|
|
93
|
+
DEFAULT_PORT = Number(process.env.SAKURA_PORT ?? 4787);
|
|
94
|
+
DEFAULT_HOST = process.env.SAKURA_HOST ?? "127.0.0.1";
|
|
95
|
+
_config = null;
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
// src/auth.ts
|
|
100
|
+
var auth_exports = {};
|
|
101
|
+
__export(auth_exports, {
|
|
102
|
+
getToken: () => getToken,
|
|
103
|
+
isLoggedIn: () => isLoggedIn,
|
|
104
|
+
login: () => login,
|
|
105
|
+
logout: () => logout,
|
|
106
|
+
requireToken: () => requireToken
|
|
107
|
+
});
|
|
108
|
+
import { randomBytes } from "crypto";
|
|
109
|
+
import os2 from "os";
|
|
110
|
+
function getToken() {
|
|
111
|
+
return process.env.SAKURA_AUTH || loadAuth().token;
|
|
112
|
+
}
|
|
113
|
+
function isLoggedIn() {
|
|
114
|
+
return !!getToken();
|
|
115
|
+
}
|
|
116
|
+
function generateToken() {
|
|
117
|
+
return "sk_" + randomBytes(24).toString("hex");
|
|
118
|
+
}
|
|
119
|
+
function login(opts = {}) {
|
|
120
|
+
const token = opts.auth?.trim() || generateToken();
|
|
121
|
+
const machineName = opts.machineName?.trim() || os2.hostname();
|
|
122
|
+
const state = {
|
|
123
|
+
token,
|
|
124
|
+
serverUrl: opts.serverUrl,
|
|
125
|
+
machineName,
|
|
126
|
+
login: process.env.USER || machineName,
|
|
127
|
+
loggedInAt: Date.now()
|
|
128
|
+
};
|
|
129
|
+
saveAuth(state);
|
|
130
|
+
updateConfig((cfg) => {
|
|
131
|
+
cfg.machineName = machineName;
|
|
132
|
+
});
|
|
133
|
+
return state;
|
|
134
|
+
}
|
|
135
|
+
function logout() {
|
|
136
|
+
clearAuth();
|
|
137
|
+
}
|
|
138
|
+
function requireToken() {
|
|
139
|
+
const t = getToken();
|
|
140
|
+
if (!t) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
"Not signed in. Run `sakuraai login` (or `sakuraai login --auth <token>`) first."
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
return t;
|
|
146
|
+
}
|
|
147
|
+
var init_auth = __esm({
|
|
148
|
+
"src/auth.ts"() {
|
|
149
|
+
"use strict";
|
|
150
|
+
init_config();
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// src/util/output.ts
|
|
155
|
+
import chalk from "chalk";
|
|
156
|
+
import Table from "cli-table3";
|
|
157
|
+
function printJson(value) {
|
|
158
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
159
|
+
}
|
|
160
|
+
function printJsonl(rows) {
|
|
161
|
+
for (const r of rows) process.stdout.write(JSON.stringify(r) + "\n");
|
|
162
|
+
}
|
|
163
|
+
function printTable(headers, rows) {
|
|
164
|
+
const table = new Table({
|
|
165
|
+
head: headers.map((h) => chalk.bold(h)),
|
|
166
|
+
style: { head: [], border: [] }
|
|
167
|
+
});
|
|
168
|
+
for (const r of rows) table.push(r.map((c) => String(c)));
|
|
169
|
+
process.stdout.write(table.toString() + "\n");
|
|
170
|
+
}
|
|
171
|
+
function success(msg) {
|
|
172
|
+
process.stdout.write(chalk.green("\u2713 ") + msg + "\n");
|
|
173
|
+
}
|
|
174
|
+
function info(msg) {
|
|
175
|
+
process.stdout.write(msg + "\n");
|
|
176
|
+
}
|
|
177
|
+
function warn(msg) {
|
|
178
|
+
process.stdout.write(chalk.yellow("! ") + msg + "\n");
|
|
179
|
+
}
|
|
180
|
+
function die(msg, code = 1) {
|
|
181
|
+
process.stderr.write(chalk.red("\u2717 ") + msg + "\n");
|
|
182
|
+
process.exit(code);
|
|
183
|
+
}
|
|
184
|
+
var init_output = __esm({
|
|
185
|
+
"src/util/output.ts"() {
|
|
186
|
+
"use strict";
|
|
187
|
+
}
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// src/logger.ts
|
|
191
|
+
import fs2 from "fs";
|
|
192
|
+
function enableFileLogging() {
|
|
193
|
+
ensureSakuraDirs();
|
|
194
|
+
fileLogging = true;
|
|
195
|
+
}
|
|
196
|
+
function write(level, args) {
|
|
197
|
+
if (LEVELS[level] < threshold) return;
|
|
198
|
+
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
199
|
+
const line = `${ts} ${level.toUpperCase()} ${args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}`;
|
|
200
|
+
process.stderr.write(line + "\n");
|
|
201
|
+
if (fileLogging) {
|
|
202
|
+
try {
|
|
203
|
+
fs2.appendFileSync(LOG_PATH, line + "\n");
|
|
204
|
+
} catch {
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
var LEVELS, threshold, fileLogging, log;
|
|
209
|
+
var init_logger = __esm({
|
|
210
|
+
"src/logger.ts"() {
|
|
211
|
+
"use strict";
|
|
212
|
+
init_config();
|
|
213
|
+
LEVELS = { debug: 10, info: 20, warn: 30, error: 40 };
|
|
214
|
+
threshold = LEVELS[process.env.SAKURA_LOG_LEVEL ?? "info"] ?? 20;
|
|
215
|
+
fileLogging = false;
|
|
216
|
+
log = {
|
|
217
|
+
debug: (...a) => write("debug", a),
|
|
218
|
+
info: (...a) => write("info", a),
|
|
219
|
+
warn: (...a) => write("warn", a),
|
|
220
|
+
error: (...a) => write("error", a)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
// src/runtime/util.ts
|
|
226
|
+
import os3 from "os";
|
|
227
|
+
function nowSecs() {
|
|
228
|
+
return Math.floor(Date.now() / 1e3);
|
|
229
|
+
}
|
|
230
|
+
function formatAge(ageSecs) {
|
|
231
|
+
if (ageSecs < 3600) return `${Math.floor(Math.max(ageSecs, 60) / 60)}m`;
|
|
232
|
+
if (ageSecs < 86400) return `${Math.floor(ageSecs / 3600)}h`;
|
|
233
|
+
return `${Math.floor(ageSecs / 86400)}d`;
|
|
234
|
+
}
|
|
235
|
+
function statusFromAge(ageSecs) {
|
|
236
|
+
if (ageSecs < 3600) return "active";
|
|
237
|
+
if (ageSecs < 86400) return "idle";
|
|
238
|
+
return "done";
|
|
239
|
+
}
|
|
240
|
+
function hostname() {
|
|
241
|
+
return os3.hostname();
|
|
242
|
+
}
|
|
243
|
+
function stripLeadingTags(input) {
|
|
244
|
+
let rest = input.trimStart();
|
|
245
|
+
while (rest.startsWith("<")) {
|
|
246
|
+
const close = rest.indexOf(">");
|
|
247
|
+
if (close === -1) break;
|
|
248
|
+
const tag = rest.slice(1, close).split(/\s+/)[0]?.replace(/\/$/, "");
|
|
249
|
+
if (!tag) break;
|
|
250
|
+
const endTag = `</${tag}>`;
|
|
251
|
+
const end = rest.indexOf(endTag);
|
|
252
|
+
if (end === -1) break;
|
|
253
|
+
rest = rest.slice(end + endTag.length).trimStart();
|
|
254
|
+
}
|
|
255
|
+
return rest;
|
|
256
|
+
}
|
|
257
|
+
function extractTextFromContent(content) {
|
|
258
|
+
let raw = null;
|
|
259
|
+
if (Array.isArray(content)) {
|
|
260
|
+
const part = content.find(
|
|
261
|
+
(p) => p && typeof p === "object" && p.type === "text"
|
|
262
|
+
);
|
|
263
|
+
raw = part?.text ?? null;
|
|
264
|
+
} else if (typeof content === "string") {
|
|
265
|
+
raw = content;
|
|
266
|
+
}
|
|
267
|
+
if (raw == null) return null;
|
|
268
|
+
const stripped = stripLeadingTags(raw);
|
|
269
|
+
const title = stripped.replace(/\n/g, " ").slice(0, 80).trim();
|
|
270
|
+
return title.length ? title : null;
|
|
271
|
+
}
|
|
272
|
+
function extractAssistantText(content) {
|
|
273
|
+
const parts = [];
|
|
274
|
+
let hasTools = false;
|
|
275
|
+
if (Array.isArray(content)) {
|
|
276
|
+
for (const p of content) {
|
|
277
|
+
const t = p?.type;
|
|
278
|
+
if (t === "text" && typeof p.text === "string") parts.push(p.text);
|
|
279
|
+
else if (t === "tool_use" || t === "tool_result") hasTools = true;
|
|
280
|
+
}
|
|
281
|
+
} else if (typeof content === "string") {
|
|
282
|
+
parts.push(content);
|
|
283
|
+
}
|
|
284
|
+
return { text: parts.join(""), hasTools };
|
|
285
|
+
}
|
|
286
|
+
function parseIsoUnix(s) {
|
|
287
|
+
const t = Date.parse(s);
|
|
288
|
+
return Number.isNaN(t) ? null : Math.floor(t / 1e3);
|
|
289
|
+
}
|
|
290
|
+
function chronoIso(secs) {
|
|
291
|
+
return new Date(secs * 1e3).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
292
|
+
}
|
|
293
|
+
var HOME;
|
|
294
|
+
var init_util = __esm({
|
|
295
|
+
"src/runtime/util.ts"() {
|
|
296
|
+
"use strict";
|
|
297
|
+
HOME = os3.homedir();
|
|
298
|
+
}
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
// src/runtime/claude.ts
|
|
302
|
+
import fs3 from "fs";
|
|
303
|
+
import path2 from "path";
|
|
304
|
+
import { spawn } from "child_process";
|
|
305
|
+
function* jsonlEntries(file) {
|
|
306
|
+
let content;
|
|
307
|
+
try {
|
|
308
|
+
content = fs3.readFileSync(file, "utf8");
|
|
309
|
+
} catch {
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
for (const line of content.split("\n")) {
|
|
313
|
+
if (!line.trim()) continue;
|
|
314
|
+
try {
|
|
315
|
+
yield JSON.parse(line);
|
|
316
|
+
} catch {
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
function sessionTitle(file) {
|
|
321
|
+
for (let pass = 0; pass < 2; pass++) {
|
|
322
|
+
for (const entry of jsonlEntries(file)) {
|
|
323
|
+
if (entry?.type !== "user") continue;
|
|
324
|
+
if (pass === 0 && entry?.userType !== "external") continue;
|
|
325
|
+
const title = extractTextFromContent(entry?.message?.content);
|
|
326
|
+
if (title) return title;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return "Untitled";
|
|
330
|
+
}
|
|
331
|
+
function listSessions() {
|
|
332
|
+
const host = hostname();
|
|
333
|
+
const now = nowSecs();
|
|
334
|
+
const out = [];
|
|
335
|
+
let dirs;
|
|
336
|
+
try {
|
|
337
|
+
dirs = fs3.readdirSync(PROJECTS_DIR, { withFileTypes: true });
|
|
338
|
+
} catch {
|
|
339
|
+
return out;
|
|
340
|
+
}
|
|
341
|
+
for (const d of dirs) {
|
|
342
|
+
if (!d.isDirectory()) continue;
|
|
343
|
+
const dir = path2.join(PROJECTS_DIR, d.name);
|
|
344
|
+
let files;
|
|
345
|
+
try {
|
|
346
|
+
files = fs3.readdirSync(dir);
|
|
347
|
+
} catch {
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
for (const f of files) {
|
|
351
|
+
if (!f.endsWith(".jsonl")) continue;
|
|
352
|
+
const p = path2.join(dir, f);
|
|
353
|
+
let mtime;
|
|
354
|
+
try {
|
|
355
|
+
mtime = Math.floor(fs3.statSync(p).mtimeMs / 1e3);
|
|
356
|
+
} catch {
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const age = Math.max(0, now - mtime);
|
|
360
|
+
out.push({
|
|
361
|
+
id: f.replace(/\.jsonl$/, ""),
|
|
362
|
+
title: sessionTitle(p),
|
|
363
|
+
agent: "claude",
|
|
364
|
+
status: statusFromAge(age),
|
|
365
|
+
updatedAt: formatAge(age),
|
|
366
|
+
machine: host,
|
|
367
|
+
mtime
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
return out;
|
|
372
|
+
}
|
|
373
|
+
function findFile(sessionId) {
|
|
374
|
+
let dirs;
|
|
375
|
+
try {
|
|
376
|
+
dirs = fs3.readdirSync(PROJECTS_DIR);
|
|
377
|
+
} catch {
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
for (const d of dirs) {
|
|
381
|
+
const p = path2.join(PROJECTS_DIR, d, `${sessionId}.jsonl`);
|
|
382
|
+
if (fs3.existsSync(p)) return p;
|
|
383
|
+
}
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
function readMessages(file) {
|
|
387
|
+
const msgs = [];
|
|
388
|
+
let startSecs = null;
|
|
389
|
+
for (const entry of jsonlEntries(file)) {
|
|
390
|
+
if (entry?.isSidechain === true) continue;
|
|
391
|
+
const tsStr = entry?.timestamp ?? "";
|
|
392
|
+
const tsSecs = parseIsoUnix(tsStr);
|
|
393
|
+
if (startSecs == null) startSecs = tsSecs;
|
|
394
|
+
if (entry?.type === "user") {
|
|
395
|
+
if (entry?.userType !== "external") continue;
|
|
396
|
+
const text = extractTextFromContent(entry?.message?.content);
|
|
397
|
+
if (text) {
|
|
398
|
+
msgs.push({
|
|
399
|
+
id: entry?.uuid ?? "",
|
|
400
|
+
role: "user",
|
|
401
|
+
content: text,
|
|
402
|
+
timestamp: tsStr,
|
|
403
|
+
hasToolUse: false
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
} else if (entry?.type === "assistant") {
|
|
407
|
+
const { text, hasTools } = extractAssistantText(entry?.message?.content);
|
|
408
|
+
if (!text) continue;
|
|
409
|
+
const inputTokens = entry?.message?.usage?.input_tokens;
|
|
410
|
+
const elapsed = startSecs != null && tsSecs != null ? Math.max(0, tsSecs - startSecs) : void 0;
|
|
411
|
+
msgs.push({
|
|
412
|
+
id: entry?.uuid ?? "",
|
|
413
|
+
role: "assistant",
|
|
414
|
+
content: text,
|
|
415
|
+
timestamp: tsStr,
|
|
416
|
+
hasToolUse: hasTools,
|
|
417
|
+
inputTokens: typeof inputTokens === "number" ? inputTokens : void 0,
|
|
418
|
+
elapsedSecs: elapsed
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
return msgs;
|
|
423
|
+
}
|
|
424
|
+
function messages(sessionId) {
|
|
425
|
+
const file = findFile(sessionId);
|
|
426
|
+
return file ? readMessages(file) : [];
|
|
427
|
+
}
|
|
428
|
+
function send(sessionId, message) {
|
|
429
|
+
return new Promise((resolve) => {
|
|
430
|
+
const bin = process.env.CLAUDE_BIN || "claude";
|
|
431
|
+
const child = spawn(bin, ["--resume", sessionId, "-p", message], {
|
|
432
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
433
|
+
});
|
|
434
|
+
let out = "";
|
|
435
|
+
let err = "";
|
|
436
|
+
child.stdout.on("data", (d) => out += d.toString());
|
|
437
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
438
|
+
child.on(
|
|
439
|
+
"error",
|
|
440
|
+
(e) => resolve({ ok: false, output: "", error: e.message })
|
|
441
|
+
);
|
|
442
|
+
child.on(
|
|
443
|
+
"close",
|
|
444
|
+
(code) => resolve({
|
|
445
|
+
ok: code === 0,
|
|
446
|
+
output: out.trim(),
|
|
447
|
+
error: code === 0 ? void 0 : err.trim() || `exited ${code}`
|
|
448
|
+
})
|
|
449
|
+
);
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
var PROJECTS_DIR;
|
|
453
|
+
var init_claude = __esm({
|
|
454
|
+
"src/runtime/claude.ts"() {
|
|
455
|
+
"use strict";
|
|
456
|
+
init_util();
|
|
457
|
+
PROJECTS_DIR = path2.join(HOME, ".claude", "projects");
|
|
458
|
+
}
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
// src/runtime/codex.ts
|
|
462
|
+
import fs4 from "fs";
|
|
463
|
+
import path3 from "path";
|
|
464
|
+
import { spawn as spawn2 } from "child_process";
|
|
465
|
+
function codexBin() {
|
|
466
|
+
if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
|
|
467
|
+
if (fs4.existsSync(MAC_APP_BIN)) return MAC_APP_BIN;
|
|
468
|
+
return "codex";
|
|
469
|
+
}
|
|
470
|
+
function listSessions2() {
|
|
471
|
+
const host = hostname();
|
|
472
|
+
const now = nowSecs();
|
|
473
|
+
let content;
|
|
474
|
+
try {
|
|
475
|
+
content = fs4.readFileSync(INDEX_PATH, "utf8");
|
|
476
|
+
} catch {
|
|
477
|
+
return [];
|
|
478
|
+
}
|
|
479
|
+
const out = [];
|
|
480
|
+
for (const line of content.split("\n")) {
|
|
481
|
+
if (!line.trim()) continue;
|
|
482
|
+
let entry;
|
|
483
|
+
try {
|
|
484
|
+
entry = JSON.parse(line);
|
|
485
|
+
} catch {
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
const id = entry?.id;
|
|
489
|
+
const title = entry?.thread_name;
|
|
490
|
+
const updatedAt = entry?.updated_at;
|
|
491
|
+
if (!id || !title || !updatedAt) continue;
|
|
492
|
+
const mtime = parseIsoUnix(updatedAt);
|
|
493
|
+
if (mtime == null) continue;
|
|
494
|
+
const age = Math.max(0, now - mtime);
|
|
495
|
+
out.push({
|
|
496
|
+
id,
|
|
497
|
+
title,
|
|
498
|
+
agent: "codex",
|
|
499
|
+
status: statusFromAge(age),
|
|
500
|
+
updatedAt: formatAge(age),
|
|
501
|
+
machine: host,
|
|
502
|
+
mtime
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return out;
|
|
506
|
+
}
|
|
507
|
+
function findFile2(sessionId) {
|
|
508
|
+
const walk = (dir) => {
|
|
509
|
+
let entries;
|
|
510
|
+
try {
|
|
511
|
+
entries = fs4.readdirSync(dir, { withFileTypes: true });
|
|
512
|
+
} catch {
|
|
513
|
+
return null;
|
|
514
|
+
}
|
|
515
|
+
for (const e of entries) {
|
|
516
|
+
const p = path3.join(dir, e.name);
|
|
517
|
+
if (e.isDirectory()) {
|
|
518
|
+
const found = walk(p);
|
|
519
|
+
if (found) return found;
|
|
520
|
+
} else if (e.name.endsWith(".jsonl") && e.name.includes(sessionId)) {
|
|
521
|
+
return p;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return null;
|
|
525
|
+
};
|
|
526
|
+
return walk(SESSIONS_DIR);
|
|
527
|
+
}
|
|
528
|
+
function readMessages2(file) {
|
|
529
|
+
let content;
|
|
530
|
+
try {
|
|
531
|
+
content = fs4.readFileSync(file, "utf8");
|
|
532
|
+
} catch {
|
|
533
|
+
return [];
|
|
534
|
+
}
|
|
535
|
+
const msgs = [];
|
|
536
|
+
for (const line of content.split("\n")) {
|
|
537
|
+
if (!line.trim()) continue;
|
|
538
|
+
let entry;
|
|
539
|
+
try {
|
|
540
|
+
entry = JSON.parse(line);
|
|
541
|
+
} catch {
|
|
542
|
+
continue;
|
|
543
|
+
}
|
|
544
|
+
if (entry?.type !== "response_item") continue;
|
|
545
|
+
const payload = entry?.payload;
|
|
546
|
+
if (payload?.type !== "message") continue;
|
|
547
|
+
const role = payload?.role;
|
|
548
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
549
|
+
const part = Array.isArray(payload?.content) ? payload.content.find(
|
|
550
|
+
(p) => ["input_text", "output_text", "text"].includes(p?.type)
|
|
551
|
+
) : null;
|
|
552
|
+
const text = part?.text ?? "";
|
|
553
|
+
if (!text) continue;
|
|
554
|
+
const ts = entry?.timestamp ?? "";
|
|
555
|
+
msgs.push({ id: ts, role, content: text, timestamp: ts, hasToolUse: false });
|
|
556
|
+
}
|
|
557
|
+
return msgs;
|
|
558
|
+
}
|
|
559
|
+
function messages2(sessionId) {
|
|
560
|
+
const file = findFile2(sessionId);
|
|
561
|
+
return file ? readMessages2(file) : [];
|
|
562
|
+
}
|
|
563
|
+
function send2(sessionId, message) {
|
|
564
|
+
return new Promise((resolve) => {
|
|
565
|
+
const child = spawn2(codexBin(), ["exec", "resume", sessionId, message], {
|
|
566
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
567
|
+
env: process.env
|
|
568
|
+
});
|
|
569
|
+
let out = "";
|
|
570
|
+
let err = "";
|
|
571
|
+
child.stdout.on("data", (d) => out += d.toString());
|
|
572
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
573
|
+
child.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
574
|
+
child.on(
|
|
575
|
+
"close",
|
|
576
|
+
(code) => resolve({
|
|
577
|
+
ok: code === 0,
|
|
578
|
+
output: out.trim(),
|
|
579
|
+
error: code === 0 ? void 0 : err.trim() || `exited ${code}`
|
|
580
|
+
})
|
|
581
|
+
);
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
var CODEX_DIR, INDEX_PATH, SESSIONS_DIR, MAC_APP_BIN;
|
|
585
|
+
var init_codex = __esm({
|
|
586
|
+
"src/runtime/codex.ts"() {
|
|
587
|
+
"use strict";
|
|
588
|
+
init_util();
|
|
589
|
+
CODEX_DIR = path3.join(HOME, ".codex");
|
|
590
|
+
INDEX_PATH = path3.join(CODEX_DIR, "session_index.jsonl");
|
|
591
|
+
SESSIONS_DIR = path3.join(CODEX_DIR, "sessions");
|
|
592
|
+
MAC_APP_BIN = "/Applications/Codex.app/Contents/Resources/codex";
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// src/runtime/opencode.ts
|
|
597
|
+
import path4 from "path";
|
|
598
|
+
import fs5 from "fs";
|
|
599
|
+
import { spawn as spawn3 } from "child_process";
|
|
600
|
+
function opencodeBin() {
|
|
601
|
+
if (process.env.OPENCODE_BIN) return process.env.OPENCODE_BIN;
|
|
602
|
+
if (fs5.existsSync(MAC_APP_BIN2)) return MAC_APP_BIN2;
|
|
603
|
+
return "opencode";
|
|
604
|
+
}
|
|
605
|
+
async function loadDriver() {
|
|
606
|
+
if (_ctor !== void 0) return _ctor;
|
|
607
|
+
try {
|
|
608
|
+
const mod = await import("better-sqlite3");
|
|
609
|
+
_ctor = mod.default ?? mod;
|
|
610
|
+
} catch {
|
|
611
|
+
_ctor = null;
|
|
612
|
+
}
|
|
613
|
+
return _ctor ?? null;
|
|
614
|
+
}
|
|
615
|
+
function available() {
|
|
616
|
+
return fs5.existsSync(DB_PATH);
|
|
617
|
+
}
|
|
618
|
+
async function open() {
|
|
619
|
+
if (!available()) return null;
|
|
620
|
+
const Ctor = await loadDriver();
|
|
621
|
+
if (!Ctor) return null;
|
|
622
|
+
try {
|
|
623
|
+
return new Ctor(DB_PATH, { readonly: true, fileMustExist: true });
|
|
624
|
+
} catch {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
async function listSessions3() {
|
|
629
|
+
const db = await open();
|
|
630
|
+
if (!db) return [];
|
|
631
|
+
const host = hostname();
|
|
632
|
+
const now = nowSecs();
|
|
633
|
+
try {
|
|
634
|
+
const rows = db.prepare(
|
|
635
|
+
"SELECT id, title, time_updated FROM session WHERE time_archived IS NULL ORDER BY time_updated DESC"
|
|
636
|
+
).all();
|
|
637
|
+
return rows.map((r) => {
|
|
638
|
+
const mtime = Math.floor(r.time_updated / 1e3);
|
|
639
|
+
const age = Math.max(0, now - mtime);
|
|
640
|
+
return {
|
|
641
|
+
id: r.id,
|
|
642
|
+
title: r.title,
|
|
643
|
+
agent: "opencode",
|
|
644
|
+
status: statusFromAge(age),
|
|
645
|
+
updatedAt: formatAge(age),
|
|
646
|
+
machine: host,
|
|
647
|
+
mtime
|
|
648
|
+
};
|
|
649
|
+
});
|
|
650
|
+
} finally {
|
|
651
|
+
db.close();
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
async function messages3(sessionId) {
|
|
655
|
+
const db = await open();
|
|
656
|
+
if (!db) return [];
|
|
657
|
+
try {
|
|
658
|
+
const msgRows = db.prepare(
|
|
659
|
+
"SELECT id, time_created, data FROM message WHERE session_id = ? ORDER BY time_created ASC"
|
|
660
|
+
).all(sessionId);
|
|
661
|
+
const partRows = db.prepare(
|
|
662
|
+
"SELECT message_id, data FROM part WHERE session_id = ? ORDER BY time_created ASC"
|
|
663
|
+
).all(sessionId);
|
|
664
|
+
const partsMap = /* @__PURE__ */ new Map();
|
|
665
|
+
for (const p of partRows) {
|
|
666
|
+
try {
|
|
667
|
+
const v = JSON.parse(p.data);
|
|
668
|
+
if (v?.type === "text" && typeof v.text === "string") {
|
|
669
|
+
const arr = partsMap.get(p.message_id) ?? [];
|
|
670
|
+
arr.push(v.text);
|
|
671
|
+
partsMap.set(p.message_id, arr);
|
|
672
|
+
}
|
|
673
|
+
} catch {
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
const startMs = msgRows[0]?.time_created ?? 0;
|
|
677
|
+
const out = [];
|
|
678
|
+
for (const m of msgRows) {
|
|
679
|
+
let v;
|
|
680
|
+
try {
|
|
681
|
+
v = JSON.parse(m.data);
|
|
682
|
+
} catch {
|
|
683
|
+
continue;
|
|
684
|
+
}
|
|
685
|
+
const role = v?.role;
|
|
686
|
+
if (role !== "user" && role !== "assistant") continue;
|
|
687
|
+
const text = (partsMap.get(m.id) ?? []).join("");
|
|
688
|
+
if (!text) continue;
|
|
689
|
+
const mtime = Math.floor(m.time_created / 1e3);
|
|
690
|
+
out.push({
|
|
691
|
+
id: m.id,
|
|
692
|
+
role,
|
|
693
|
+
content: text,
|
|
694
|
+
timestamp: chronoIso(mtime),
|
|
695
|
+
hasToolUse: false,
|
|
696
|
+
elapsedSecs: role === "assistant" ? Math.floor((m.time_created - startMs) / 1e3) : void 0
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
return out;
|
|
700
|
+
} finally {
|
|
701
|
+
db.close();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
function send3(sessionId, message) {
|
|
705
|
+
return new Promise((resolve) => {
|
|
706
|
+
const child = spawn3(opencodeBin(), ["run", "-s", sessionId, message], {
|
|
707
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
708
|
+
env: process.env
|
|
709
|
+
});
|
|
710
|
+
let out = "";
|
|
711
|
+
let err = "";
|
|
712
|
+
child.stdout.on("data", (d) => out += d.toString());
|
|
713
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
714
|
+
child.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
715
|
+
child.on(
|
|
716
|
+
"close",
|
|
717
|
+
(code) => resolve({
|
|
718
|
+
ok: code === 0,
|
|
719
|
+
output: out.trim(),
|
|
720
|
+
error: code === 0 ? void 0 : err.trim() || `exited ${code}`
|
|
721
|
+
})
|
|
722
|
+
);
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
var DB_PATH, MAC_APP_BIN2, _ctor;
|
|
726
|
+
var init_opencode = __esm({
|
|
727
|
+
"src/runtime/opencode.ts"() {
|
|
728
|
+
"use strict";
|
|
729
|
+
init_util();
|
|
730
|
+
DB_PATH = path4.join(HOME, ".local", "share", "opencode", "opencode.db");
|
|
731
|
+
MAC_APP_BIN2 = "/Applications/OpenCode.app/Contents/MacOS/opencode-cli";
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
|
|
735
|
+
// src/runtime/sessions.ts
|
|
736
|
+
import { spawn as spawn4 } from "child_process";
|
|
737
|
+
async function list(opts = {}) {
|
|
738
|
+
let sessions = [];
|
|
739
|
+
if (!opts.agent || opts.agent === "claude") sessions.push(...listSessions());
|
|
740
|
+
if (!opts.agent || opts.agent === "codex") sessions.push(...listSessions2());
|
|
741
|
+
if (!opts.agent || opts.agent === "opencode")
|
|
742
|
+
sessions.push(...await listSessions3());
|
|
743
|
+
sessions.sort((a, b) => b.mtime - a.mtime);
|
|
744
|
+
if (opts.limit && opts.limit > 0) sessions = sessions.slice(0, opts.limit);
|
|
745
|
+
return sessions;
|
|
746
|
+
}
|
|
747
|
+
async function show(sessionId) {
|
|
748
|
+
const all = await list();
|
|
749
|
+
return all.find((s) => s.id === sessionId) ?? null;
|
|
750
|
+
}
|
|
751
|
+
async function detectAgent(sessionId) {
|
|
752
|
+
if (findFile(sessionId)) return "claude";
|
|
753
|
+
if (findFile2(sessionId)) return "codex";
|
|
754
|
+
const oc = await messages3(sessionId);
|
|
755
|
+
if (oc.length) return "opencode";
|
|
756
|
+
return null;
|
|
757
|
+
}
|
|
758
|
+
async function messages4(sessionId, agent) {
|
|
759
|
+
const a = agent ?? await detectAgent(sessionId);
|
|
760
|
+
switch (a) {
|
|
761
|
+
case "claude":
|
|
762
|
+
return messages(sessionId);
|
|
763
|
+
case "codex":
|
|
764
|
+
return messages2(sessionId);
|
|
765
|
+
case "opencode":
|
|
766
|
+
return messages3(sessionId);
|
|
767
|
+
default:
|
|
768
|
+
return [];
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
async function history(sessionId, opts = {}, agent) {
|
|
772
|
+
let msgs = await messages4(sessionId, agent);
|
|
773
|
+
if (!opts.all && opts.limit && opts.limit > 0) {
|
|
774
|
+
msgs = msgs.slice(-opts.limit);
|
|
775
|
+
}
|
|
776
|
+
if (opts.reverse) msgs = [...msgs].reverse();
|
|
777
|
+
return msgs;
|
|
778
|
+
}
|
|
779
|
+
async function chat(sessionId, prompt, agent) {
|
|
780
|
+
const a = agent ?? await detectAgent(sessionId);
|
|
781
|
+
if (a === "claude") return send(sessionId, prompt);
|
|
782
|
+
if (a === "codex") return send2(sessionId, prompt);
|
|
783
|
+
if (a === "opencode") return send3(sessionId, prompt);
|
|
784
|
+
return {
|
|
785
|
+
ok: false,
|
|
786
|
+
output: "",
|
|
787
|
+
error: `Sending is not supported for this session (got ${a ?? "unknown"}).`
|
|
788
|
+
};
|
|
789
|
+
}
|
|
790
|
+
function create(prompt, opts = {}) {
|
|
791
|
+
const agent = opts.agent ?? "claude";
|
|
792
|
+
if (agent !== "claude") {
|
|
793
|
+
return Promise.resolve({
|
|
794
|
+
ok: false,
|
|
795
|
+
output: "",
|
|
796
|
+
error: `Creating sessions from the CLI is only supported for Claude (got ${agent}).`
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
return new Promise((resolve) => {
|
|
800
|
+
const bin = process.env.CLAUDE_BIN || "claude";
|
|
801
|
+
const child = spawn4(bin, ["-p", prompt], {
|
|
802
|
+
cwd: opts.cwd || process.cwd(),
|
|
803
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
804
|
+
});
|
|
805
|
+
let out = "";
|
|
806
|
+
let err = "";
|
|
807
|
+
child.stdout.on("data", (d) => out += d.toString());
|
|
808
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
809
|
+
child.on("error", (e) => resolve({ ok: false, output: "", error: e.message }));
|
|
810
|
+
child.on(
|
|
811
|
+
"close",
|
|
812
|
+
(code) => resolve({
|
|
813
|
+
ok: code === 0,
|
|
814
|
+
output: out.trim(),
|
|
815
|
+
error: code === 0 ? void 0 : err.trim() || `exited ${code}`
|
|
816
|
+
})
|
|
817
|
+
);
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
var init_sessions = __esm({
|
|
821
|
+
"src/runtime/sessions.ts"() {
|
|
822
|
+
"use strict";
|
|
823
|
+
init_claude();
|
|
824
|
+
init_codex();
|
|
825
|
+
init_opencode();
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// src/runtime/registry.ts
|
|
830
|
+
var registry_exports = {};
|
|
831
|
+
__export(registry_exports, {
|
|
832
|
+
addProject: () => addProject,
|
|
833
|
+
createAgentConfig: () => createAgentConfig,
|
|
834
|
+
deleteAgentConfig: () => deleteAgentConfig,
|
|
835
|
+
deleteProject: () => deleteProject,
|
|
836
|
+
listAgentConfigs: () => listAgentConfigs,
|
|
837
|
+
listMachines: () => listMachines,
|
|
838
|
+
listProjects: () => listProjects,
|
|
839
|
+
listWorkspaces: () => listWorkspaces,
|
|
840
|
+
refreshCapabilities: () => refreshCapabilities,
|
|
841
|
+
resolveAgentConfig: () => resolveAgentConfig,
|
|
842
|
+
resolveProject: () => resolveProject,
|
|
843
|
+
thisMachine: () => thisMachine,
|
|
844
|
+
updateAgentConfig: () => updateAgentConfig
|
|
845
|
+
});
|
|
846
|
+
import os4 from "os";
|
|
847
|
+
import fs6 from "fs";
|
|
848
|
+
import path5 from "path";
|
|
849
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
850
|
+
function listWorkspaces() {
|
|
851
|
+
return loadConfig().workspaces;
|
|
852
|
+
}
|
|
853
|
+
function detectCli() {
|
|
854
|
+
const found = [];
|
|
855
|
+
const candidates = {
|
|
856
|
+
claude: process.env.CLAUDE_BIN || "claude",
|
|
857
|
+
codex: process.env.CODEX_BIN || "codex",
|
|
858
|
+
opencode: process.env.OPENCODE_BIN || "opencode"
|
|
859
|
+
};
|
|
860
|
+
const pathDirs = (process.env.PATH || "").split(path5.delimiter);
|
|
861
|
+
for (const [name, bin] of Object.entries(candidates)) {
|
|
862
|
+
const onPath = pathDirs.some((dir) => {
|
|
863
|
+
try {
|
|
864
|
+
return fs6.existsSync(path5.join(dir, bin));
|
|
865
|
+
} catch {
|
|
866
|
+
return false;
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
const home = os4.homedir();
|
|
870
|
+
const dataDirs = {
|
|
871
|
+
claude: path5.join(home, ".claude"),
|
|
872
|
+
codex: path5.join(home, ".codex"),
|
|
873
|
+
opencode: path5.join(home, ".local", "share", "opencode")
|
|
874
|
+
};
|
|
875
|
+
if (onPath || fs6.existsSync(dataDirs[name] ?? "")) found.push(name);
|
|
876
|
+
}
|
|
877
|
+
return found;
|
|
878
|
+
}
|
|
879
|
+
function thisMachine() {
|
|
880
|
+
const cfg = loadConfig();
|
|
881
|
+
return {
|
|
882
|
+
id: cfg.machineId,
|
|
883
|
+
name: cfg.machineName,
|
|
884
|
+
online: true,
|
|
885
|
+
os: `${os4.type()} ${os4.release()}`,
|
|
886
|
+
hostname: os4.hostname(),
|
|
887
|
+
cli: detectCli()
|
|
888
|
+
};
|
|
889
|
+
}
|
|
890
|
+
function listMachines(onlineOnly = false) {
|
|
891
|
+
const m = thisMachine();
|
|
892
|
+
if (onlineOnly && !m.online) return [];
|
|
893
|
+
return [m];
|
|
894
|
+
}
|
|
895
|
+
function listProjects() {
|
|
896
|
+
return loadConfig().projects;
|
|
897
|
+
}
|
|
898
|
+
function addProject(dir, name) {
|
|
899
|
+
const abs = path5.resolve(dir);
|
|
900
|
+
if (!fs6.existsSync(abs) || !fs6.statSync(abs).isDirectory()) {
|
|
901
|
+
throw new Error(`Not a directory: ${abs}`);
|
|
902
|
+
}
|
|
903
|
+
const existing = loadConfig().projects.find((p) => p.path === abs);
|
|
904
|
+
if (existing) return existing;
|
|
905
|
+
const project = {
|
|
906
|
+
id: randomUUID2(),
|
|
907
|
+
name: name || path5.basename(abs),
|
|
908
|
+
path: abs,
|
|
909
|
+
addedAt: Date.now()
|
|
910
|
+
};
|
|
911
|
+
updateConfig((cfg) => {
|
|
912
|
+
cfg.projects.push(project);
|
|
913
|
+
});
|
|
914
|
+
return project;
|
|
915
|
+
}
|
|
916
|
+
function deleteProject(idOrNameOrPath) {
|
|
917
|
+
let removed = false;
|
|
918
|
+
updateConfig((cfg) => {
|
|
919
|
+
const before = cfg.projects.length;
|
|
920
|
+
cfg.projects = cfg.projects.filter(
|
|
921
|
+
(p) => p.id !== idOrNameOrPath && p.name !== idOrNameOrPath && p.path !== path5.resolve(idOrNameOrPath)
|
|
922
|
+
);
|
|
923
|
+
removed = cfg.projects.length !== before;
|
|
924
|
+
});
|
|
925
|
+
return removed;
|
|
926
|
+
}
|
|
927
|
+
function resolveProject(idOrNameOrPath) {
|
|
928
|
+
const cfg = loadConfig();
|
|
929
|
+
const abs = path5.resolve(idOrNameOrPath);
|
|
930
|
+
return cfg.projects.find(
|
|
931
|
+
(p) => p.id === idOrNameOrPath || p.name === idOrNameOrPath || p.path === abs
|
|
932
|
+
) ?? null;
|
|
933
|
+
}
|
|
934
|
+
function listAgentConfigs() {
|
|
935
|
+
return loadConfig().agentConfigs;
|
|
936
|
+
}
|
|
937
|
+
function resolveAgentConfig(idOrName) {
|
|
938
|
+
const cfg = loadConfig();
|
|
939
|
+
const matches = cfg.agentConfigs.filter(
|
|
940
|
+
(a) => a.id === idOrName || a.name === idOrName
|
|
941
|
+
);
|
|
942
|
+
if (matches.length > 1 && !cfg.agentConfigs.some((a) => a.id === idOrName)) {
|
|
943
|
+
throw new Error(
|
|
944
|
+
`Multiple agent-configs named "${idOrName}". Use the explicit id instead.`
|
|
945
|
+
);
|
|
946
|
+
}
|
|
947
|
+
return matches[0] ?? null;
|
|
948
|
+
}
|
|
949
|
+
function createAgentConfig(input) {
|
|
950
|
+
const now = Date.now();
|
|
951
|
+
const config = {
|
|
952
|
+
id: randomUUID2(),
|
|
953
|
+
name: input.name,
|
|
954
|
+
agentType: input.agentType,
|
|
955
|
+
description: input.description,
|
|
956
|
+
env: input.env ?? {},
|
|
957
|
+
prompt: input.prompt,
|
|
958
|
+
createdAt: now,
|
|
959
|
+
updatedAt: now
|
|
960
|
+
};
|
|
961
|
+
updateConfig((cfg) => {
|
|
962
|
+
cfg.agentConfigs.push(config);
|
|
963
|
+
});
|
|
964
|
+
return config;
|
|
965
|
+
}
|
|
966
|
+
function updateAgentConfig(idOrName, input) {
|
|
967
|
+
const target = resolveAgentConfig(idOrName);
|
|
968
|
+
if (!target) throw new Error(`No agent-config matching "${idOrName}".`);
|
|
969
|
+
updateConfig((cfg) => {
|
|
970
|
+
const a = cfg.agentConfigs.find((c) => c.id === target.id);
|
|
971
|
+
if (input.name !== void 0) a.name = input.name;
|
|
972
|
+
if (input.description !== void 0)
|
|
973
|
+
a.description = input.description || void 0;
|
|
974
|
+
if (input.env) a.env = { ...a.env, ...input.env };
|
|
975
|
+
if (input.unsetEnv) for (const k of input.unsetEnv) delete a.env[k];
|
|
976
|
+
if (input.prompt !== void 0) a.prompt = input.prompt || void 0;
|
|
977
|
+
a.updatedAt = Date.now();
|
|
978
|
+
});
|
|
979
|
+
return resolveAgentConfig(target.id);
|
|
980
|
+
}
|
|
981
|
+
function deleteAgentConfig(idOrName) {
|
|
982
|
+
const target = resolveAgentConfig(idOrName);
|
|
983
|
+
if (!target) return false;
|
|
984
|
+
updateConfig((cfg) => {
|
|
985
|
+
cfg.agentConfigs = cfg.agentConfigs.filter((c) => c.id !== target.id);
|
|
986
|
+
});
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
function refreshCapabilities(idOrName) {
|
|
990
|
+
const target = resolveAgentConfig(idOrName);
|
|
991
|
+
if (!target) throw new Error(`No agent-config matching "${idOrName}".`);
|
|
992
|
+
const cli = detectCli();
|
|
993
|
+
const cliAvailable = cli.includes(target.agentType);
|
|
994
|
+
const MODELS = {
|
|
995
|
+
claude: ["default", "opus", "haiku", "opus-1m"],
|
|
996
|
+
codex: ["default", "gpt-5-codex"],
|
|
997
|
+
opencode: ["default"]
|
|
998
|
+
};
|
|
999
|
+
const MODES = {
|
|
1000
|
+
claude: ["default", "accept_edits", "plan", "dont_ask", "full_access"],
|
|
1001
|
+
codex: ["default"],
|
|
1002
|
+
opencode: ["default"]
|
|
1003
|
+
};
|
|
1004
|
+
return {
|
|
1005
|
+
agentType: target.agentType,
|
|
1006
|
+
cliAvailable,
|
|
1007
|
+
models: MODELS[target.agentType] ?? ["default"],
|
|
1008
|
+
modes: MODES[target.agentType] ?? ["default"]
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
var init_registry = __esm({
|
|
1012
|
+
"src/runtime/registry.ts"() {
|
|
1013
|
+
"use strict";
|
|
1014
|
+
init_config();
|
|
1015
|
+
}
|
|
1016
|
+
});
|
|
1017
|
+
|
|
1018
|
+
// src/tailscale.ts
|
|
1019
|
+
import fs7 from "fs";
|
|
1020
|
+
import { spawn as spawn5 } from "child_process";
|
|
1021
|
+
function tsBin() {
|
|
1022
|
+
const macPath = "/Applications/Tailscale.app/Contents/MacOS/Tailscale";
|
|
1023
|
+
if (fs7.existsSync(macPath)) return macPath;
|
|
1024
|
+
return process.env.TAILSCALE_BIN || "tailscale";
|
|
1025
|
+
}
|
|
1026
|
+
function runTs(args, timeoutMs = 6e4) {
|
|
1027
|
+
return new Promise((resolve) => {
|
|
1028
|
+
const child = spawn5(tsBin(), args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
1029
|
+
let out = "";
|
|
1030
|
+
let err = "";
|
|
1031
|
+
const timer = setTimeout(() => {
|
|
1032
|
+
child.kill("SIGKILL");
|
|
1033
|
+
resolve({
|
|
1034
|
+
ok: false,
|
|
1035
|
+
data: null,
|
|
1036
|
+
stdout: out.trim(),
|
|
1037
|
+
stderr: "tailscale command timed out",
|
|
1038
|
+
code: "timeout"
|
|
1039
|
+
});
|
|
1040
|
+
}, timeoutMs);
|
|
1041
|
+
child.stdout.on("data", (d) => out += d.toString());
|
|
1042
|
+
child.stderr.on("data", (d) => err += d.toString());
|
|
1043
|
+
child.on("error", (e) => {
|
|
1044
|
+
clearTimeout(timer);
|
|
1045
|
+
if (e.code === "ENOENT") {
|
|
1046
|
+
resolve({
|
|
1047
|
+
ok: false,
|
|
1048
|
+
data: null,
|
|
1049
|
+
stdout: "",
|
|
1050
|
+
stderr: "Tailscale is not installed. Install it from https://tailscale.com/download, run `tailscale up`, and connect your phone to the same tailnet.",
|
|
1051
|
+
code: "not_installed"
|
|
1052
|
+
});
|
|
1053
|
+
} else {
|
|
1054
|
+
resolve({
|
|
1055
|
+
ok: false,
|
|
1056
|
+
data: null,
|
|
1057
|
+
stdout: "",
|
|
1058
|
+
stderr: `Failed to launch tailscale: ${e.message}`,
|
|
1059
|
+
code: "spawn_error"
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
child.on("close", (code) => {
|
|
1064
|
+
clearTimeout(timer);
|
|
1065
|
+
const stdout = out.trim();
|
|
1066
|
+
const stderr = err.trim();
|
|
1067
|
+
let data = null;
|
|
1068
|
+
try {
|
|
1069
|
+
data = JSON.parse(stdout);
|
|
1070
|
+
} catch {
|
|
1071
|
+
}
|
|
1072
|
+
if (code === 0) {
|
|
1073
|
+
resolve({ ok: true, data, stdout, stderr, code: "ok" });
|
|
1074
|
+
} else {
|
|
1075
|
+
resolve({
|
|
1076
|
+
ok: false,
|
|
1077
|
+
data,
|
|
1078
|
+
stdout,
|
|
1079
|
+
stderr: stderr || "tailscale exited with a non-zero status",
|
|
1080
|
+
code: "nonzero"
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
});
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
async function status(port2) {
|
|
1087
|
+
const res = await runTs(["status", "--json"]);
|
|
1088
|
+
if (!res.ok) return res;
|
|
1089
|
+
const self = res.data?.Self ?? {};
|
|
1090
|
+
const ips = Array.isArray(self.TailscaleIPs) ? self.TailscaleIPs : [];
|
|
1091
|
+
const ipv4 = ips.find((ip) => !ip.includes(":")) ?? null;
|
|
1092
|
+
const dnsName = typeof self.DNSName === "string" ? self.DNSName.replace(/\.$/, "") : null;
|
|
1093
|
+
const online = self.Online === true;
|
|
1094
|
+
const backendState = res.data?.BackendState ?? "Unknown";
|
|
1095
|
+
const baseUrl = ipv4 ? `http://${ipv4}:${port2}` : null;
|
|
1096
|
+
const sakura = {
|
|
1097
|
+
online,
|
|
1098
|
+
backendState,
|
|
1099
|
+
tailscaleIp: ipv4,
|
|
1100
|
+
dnsName,
|
|
1101
|
+
port: port2,
|
|
1102
|
+
baseUrl
|
|
1103
|
+
};
|
|
1104
|
+
if (res.data && typeof res.data === "object") res.data.sakura = sakura;
|
|
1105
|
+
return res;
|
|
1106
|
+
}
|
|
1107
|
+
function up() {
|
|
1108
|
+
return runTs(["up"]);
|
|
1109
|
+
}
|
|
1110
|
+
function down() {
|
|
1111
|
+
return runTs(["down"]);
|
|
1112
|
+
}
|
|
1113
|
+
function serve(port2) {
|
|
1114
|
+
return runTs(["serve", "--bg", `http://127.0.0.1:${port2}`]);
|
|
1115
|
+
}
|
|
1116
|
+
function serveOff() {
|
|
1117
|
+
return runTs(["serve", "--https=443", "off"]);
|
|
1118
|
+
}
|
|
1119
|
+
async function installed() {
|
|
1120
|
+
const r = await runTs(["version"]);
|
|
1121
|
+
return { installed: r.code !== "not_installed", version: r.stdout, code: r.code };
|
|
1122
|
+
}
|
|
1123
|
+
async function discoverBaseUrl(port2) {
|
|
1124
|
+
try {
|
|
1125
|
+
const r = await status(port2);
|
|
1126
|
+
return r.ok ? r.data?.sakura?.baseUrl ?? null : null;
|
|
1127
|
+
} catch {
|
|
1128
|
+
return null;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
var init_tailscale = __esm({
|
|
1132
|
+
"src/tailscale.ts"() {
|
|
1133
|
+
"use strict";
|
|
1134
|
+
}
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1137
|
+
// src/daemon/server.ts
|
|
1138
|
+
import http from "http";
|
|
1139
|
+
import fs8 from "fs";
|
|
1140
|
+
import { URL } from "url";
|
|
1141
|
+
function add(method, path6, handler) {
|
|
1142
|
+
const keys = [];
|
|
1143
|
+
const pattern = new RegExp(
|
|
1144
|
+
"^" + path6.replace(/:[^/]+/g, (m) => {
|
|
1145
|
+
keys.push(m.slice(1));
|
|
1146
|
+
return "([^/]+)";
|
|
1147
|
+
}) + "/?$"
|
|
1148
|
+
);
|
|
1149
|
+
routes.push({ method, pattern, keys, handler });
|
|
1150
|
+
}
|
|
1151
|
+
function ok(data = null) {
|
|
1152
|
+
return { ok: true, data, stdout: "", stderr: "", code: "ok" };
|
|
1153
|
+
}
|
|
1154
|
+
function fail(code, stderr, data = null) {
|
|
1155
|
+
return { ok: false, data, stdout: "", stderr, code };
|
|
1156
|
+
}
|
|
1157
|
+
function kvToObj(env) {
|
|
1158
|
+
if (!Array.isArray(env)) return void 0;
|
|
1159
|
+
const out = {};
|
|
1160
|
+
for (const kv of env) {
|
|
1161
|
+
const s = String(kv);
|
|
1162
|
+
const i = s.indexOf("=");
|
|
1163
|
+
if (i > 0) out[s.slice(0, i)] = s.slice(i + 1);
|
|
1164
|
+
}
|
|
1165
|
+
return out;
|
|
1166
|
+
}
|
|
1167
|
+
function readBody(req) {
|
|
1168
|
+
return new Promise((resolve) => {
|
|
1169
|
+
let data = "";
|
|
1170
|
+
req.on("data", (c) => data += c);
|
|
1171
|
+
req.on("end", () => {
|
|
1172
|
+
if (!data) return resolve(null);
|
|
1173
|
+
try {
|
|
1174
|
+
resolve(JSON.parse(data));
|
|
1175
|
+
} catch {
|
|
1176
|
+
resolve(null);
|
|
1177
|
+
}
|
|
1178
|
+
});
|
|
1179
|
+
req.on("error", () => resolve(null));
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
function send4(res, status2, payload) {
|
|
1183
|
+
const body = JSON.stringify(payload);
|
|
1184
|
+
res.writeHead(status2, {
|
|
1185
|
+
"Content-Type": "application/json",
|
|
1186
|
+
"Access-Control-Allow-Origin": "*",
|
|
1187
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
1188
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS"
|
|
1189
|
+
});
|
|
1190
|
+
res.end(body);
|
|
1191
|
+
}
|
|
1192
|
+
function createServer(port2) {
|
|
1193
|
+
return http.createServer(async (req, res) => {
|
|
1194
|
+
const method = req.method ?? "GET";
|
|
1195
|
+
const url = new URL(req.url ?? "/", `http://localhost:${port2}`);
|
|
1196
|
+
if (method === "OPTIONS") return send4(res, 204, {});
|
|
1197
|
+
if (method === "GET" && url.pathname === "/ping") {
|
|
1198
|
+
return send4(res, 200, { pong: true, pid: process.pid });
|
|
1199
|
+
}
|
|
1200
|
+
const expected = getToken();
|
|
1201
|
+
const auth = req.headers["authorization"];
|
|
1202
|
+
const presented = typeof auth === "string" && auth.startsWith("Bearer ") ? auth.slice(7) : void 0;
|
|
1203
|
+
if (!expected || presented !== expected) {
|
|
1204
|
+
return send4(res, 401, { error: "Unauthorized" });
|
|
1205
|
+
}
|
|
1206
|
+
const route = routes.find(
|
|
1207
|
+
(r) => r.method === method && r.pattern.test(url.pathname)
|
|
1208
|
+
);
|
|
1209
|
+
if (!route) return send4(res, 404, { error: "Not found" });
|
|
1210
|
+
const match = route.pattern.exec(url.pathname);
|
|
1211
|
+
const params = {};
|
|
1212
|
+
route.keys.forEach((k, i) => params[k] = decodeURIComponent(match[i + 1]));
|
|
1213
|
+
const body = method === "GET" ? null : await readBody(req);
|
|
1214
|
+
try {
|
|
1215
|
+
const result = await route.handler({ req, res, url, params, body, port: port2 });
|
|
1216
|
+
send4(res, 200, result);
|
|
1217
|
+
} catch (e) {
|
|
1218
|
+
log.error("handler error", e.message);
|
|
1219
|
+
send4(res, 500, fail("nonzero", e.message));
|
|
1220
|
+
}
|
|
1221
|
+
});
|
|
1222
|
+
}
|
|
1223
|
+
function setVersion(v) {
|
|
1224
|
+
VERSION2 = v;
|
|
1225
|
+
}
|
|
1226
|
+
var routes, VERSION2;
|
|
1227
|
+
var init_server = __esm({
|
|
1228
|
+
"src/daemon/server.ts"() {
|
|
1229
|
+
"use strict";
|
|
1230
|
+
init_auth();
|
|
1231
|
+
init_config();
|
|
1232
|
+
init_logger();
|
|
1233
|
+
init_sessions();
|
|
1234
|
+
init_registry();
|
|
1235
|
+
init_tailscale();
|
|
1236
|
+
init_registry();
|
|
1237
|
+
routes = [];
|
|
1238
|
+
add("GET", "/sessions", async () => {
|
|
1239
|
+
const list2 = await list();
|
|
1240
|
+
return { sessions: list2 };
|
|
1241
|
+
});
|
|
1242
|
+
add("GET", "/sessions/:id/messages", async ({ params, url }) => {
|
|
1243
|
+
const agent = url.searchParams.get("agent") || void 0;
|
|
1244
|
+
const messages5 = await messages4(params.id, agent);
|
|
1245
|
+
return { messages: messages5 };
|
|
1246
|
+
});
|
|
1247
|
+
add("POST", "/sessions/:id/send", async ({ params, body }) => {
|
|
1248
|
+
const agent = body?.agent || void 0;
|
|
1249
|
+
const r = await chat(params.id, String(body?.message ?? ""), agent);
|
|
1250
|
+
return { ok: r.ok, output: r.output, error: r.error };
|
|
1251
|
+
});
|
|
1252
|
+
add(
|
|
1253
|
+
"GET",
|
|
1254
|
+
"/sakura/health",
|
|
1255
|
+
() => ok({ installed: true, daemon: true, version: VERSION2 })
|
|
1256
|
+
);
|
|
1257
|
+
add("GET", "/sakura/status", async ({ port: port2 }) => {
|
|
1258
|
+
const machine = thisMachine();
|
|
1259
|
+
const ts = await discoverBaseUrl(port2).catch(() => null);
|
|
1260
|
+
return ok({
|
|
1261
|
+
runtime: "running",
|
|
1262
|
+
machine,
|
|
1263
|
+
workspaces: listWorkspaces(),
|
|
1264
|
+
tailnetBaseUrl: ts
|
|
1265
|
+
});
|
|
1266
|
+
});
|
|
1267
|
+
add("POST", "/sakura/daemon/:action", ({ params }) => {
|
|
1268
|
+
switch (params.action) {
|
|
1269
|
+
case "status":
|
|
1270
|
+
return ok({ running: true, pid: process.pid, machine: thisMachine() });
|
|
1271
|
+
case "logs": {
|
|
1272
|
+
let logs2 = "";
|
|
1273
|
+
try {
|
|
1274
|
+
const lines = fs8.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
1275
|
+
logs2 = lines.slice(-200).join("\n");
|
|
1276
|
+
} catch {
|
|
1277
|
+
}
|
|
1278
|
+
return ok({ logs: logs2 });
|
|
1279
|
+
}
|
|
1280
|
+
// start/stop/restart are process-level and handled by the CLI, not here.
|
|
1281
|
+
case "start":
|
|
1282
|
+
case "stop":
|
|
1283
|
+
case "restart":
|
|
1284
|
+
return fail(
|
|
1285
|
+
"nonzero",
|
|
1286
|
+
`daemon ${params.action} must be run from the CLI (\`sakuraai daemon ${params.action}\`).`
|
|
1287
|
+
);
|
|
1288
|
+
default:
|
|
1289
|
+
return fail("nonzero", `unknown daemon action: ${params.action}`);
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
1292
|
+
add("GET", "/sakura/workspaces", () => ok(listWorkspaces()));
|
|
1293
|
+
add(
|
|
1294
|
+
"GET",
|
|
1295
|
+
"/sakura/github",
|
|
1296
|
+
() => (
|
|
1297
|
+
// No cloud backend: GitHub linkage lives in the Sakura server/Convex, not here.
|
|
1298
|
+
ok([])
|
|
1299
|
+
)
|
|
1300
|
+
);
|
|
1301
|
+
add(
|
|
1302
|
+
"GET",
|
|
1303
|
+
"/sakura/machines",
|
|
1304
|
+
({ url }) => ok(listMachines(url.searchParams.get("onlineOnly") === "true"))
|
|
1305
|
+
);
|
|
1306
|
+
add("GET", "/sakura/projects", () => ok(listProjects()));
|
|
1307
|
+
add("GET", "/sakura/sessions", async ({ url }) => {
|
|
1308
|
+
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
1309
|
+
const agent = url.searchParams.get("agent") || void 0;
|
|
1310
|
+
return ok(await list({ limit, agent }));
|
|
1311
|
+
});
|
|
1312
|
+
add("POST", "/sakura/sessions", async ({ body }) => {
|
|
1313
|
+
const r = await create(String(body?.prompt ?? ""), {
|
|
1314
|
+
agent: body?.agent,
|
|
1315
|
+
cwd: body?.cwd
|
|
1316
|
+
});
|
|
1317
|
+
return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "create failed");
|
|
1318
|
+
});
|
|
1319
|
+
add("GET", "/sakura/sessions/:id", async ({ params }) => {
|
|
1320
|
+
const s = await show(params.id);
|
|
1321
|
+
return s ? ok(s) : fail("nonzero", "session not found");
|
|
1322
|
+
});
|
|
1323
|
+
add("GET", "/sakura/sessions/:id/history", async ({ params, url }) => {
|
|
1324
|
+
const all = url.searchParams.get("all") === "true";
|
|
1325
|
+
const limit = Number(url.searchParams.get("limit")) || void 0;
|
|
1326
|
+
const reverse = url.searchParams.get("reverse") === "true";
|
|
1327
|
+
return ok(await history(params.id, { all, limit, reverse }));
|
|
1328
|
+
});
|
|
1329
|
+
add("POST", "/sakura/sessions/:id/chat", async ({ params, body }) => {
|
|
1330
|
+
const r = await chat(params.id, String(body?.prompt ?? ""), body?.agent);
|
|
1331
|
+
return r.ok ? ok({ output: r.output }) : fail("nonzero", r.error ?? "chat failed");
|
|
1332
|
+
});
|
|
1333
|
+
add(
|
|
1334
|
+
"POST",
|
|
1335
|
+
"/sakura/sessions/:id/cancel",
|
|
1336
|
+
() => fail("nonzero", "cancel is not supported for local file-backed sessions")
|
|
1337
|
+
);
|
|
1338
|
+
add(
|
|
1339
|
+
"POST",
|
|
1340
|
+
"/sakura/sessions/:id/rename",
|
|
1341
|
+
() => fail("nonzero", "rename is not supported for local file-backed sessions")
|
|
1342
|
+
);
|
|
1343
|
+
add(
|
|
1344
|
+
"POST",
|
|
1345
|
+
"/sakura/sessions/:id/:action",
|
|
1346
|
+
({ params }) => (
|
|
1347
|
+
// archive/restore/delete would destroy local agent history — refuse.
|
|
1348
|
+
fail(
|
|
1349
|
+
"nonzero",
|
|
1350
|
+
`${params.action} is disabled for local sessions to avoid deleting agent history`
|
|
1351
|
+
)
|
|
1352
|
+
)
|
|
1353
|
+
);
|
|
1354
|
+
add("GET", "/sakura/agent-configs", () => ok(listAgentConfigs()));
|
|
1355
|
+
add("POST", "/sakura/agent-configs", ({ body }) => {
|
|
1356
|
+
try {
|
|
1357
|
+
return ok(
|
|
1358
|
+
createAgentConfig({
|
|
1359
|
+
name: body.name,
|
|
1360
|
+
agentType: body.agent_type ?? body.agentType,
|
|
1361
|
+
description: body.description,
|
|
1362
|
+
env: kvToObj(body.env),
|
|
1363
|
+
prompt: body.prompt
|
|
1364
|
+
})
|
|
1365
|
+
);
|
|
1366
|
+
} catch (e) {
|
|
1367
|
+
return fail("nonzero", e.message);
|
|
1368
|
+
}
|
|
1369
|
+
});
|
|
1370
|
+
add("PUT", "/sakura/agent-configs", ({ body }) => {
|
|
1371
|
+
try {
|
|
1372
|
+
return ok(
|
|
1373
|
+
updateAgentConfig(body.id_or_name ?? body.idOrName, {
|
|
1374
|
+
name: body.name,
|
|
1375
|
+
description: body.description,
|
|
1376
|
+
env: kvToObj(body.env),
|
|
1377
|
+
unsetEnv: body.unset_env ?? body.unsetEnv,
|
|
1378
|
+
prompt: body.prompt
|
|
1379
|
+
})
|
|
1380
|
+
);
|
|
1381
|
+
} catch (e) {
|
|
1382
|
+
return fail("nonzero", e.message);
|
|
1383
|
+
}
|
|
1384
|
+
});
|
|
1385
|
+
add("GET", "/sakura/agent-configs/:id", ({ params }) => {
|
|
1386
|
+
const a = resolveAgentConfig(params.id);
|
|
1387
|
+
return a ? ok(a) : fail("nonzero", "agent-config not found");
|
|
1388
|
+
});
|
|
1389
|
+
add(
|
|
1390
|
+
"DELETE",
|
|
1391
|
+
"/sakura/agent-configs/:id",
|
|
1392
|
+
({ params }) => deleteAgentConfig(params.id) ? ok({ deleted: true }) : fail("nonzero", "agent-config not found")
|
|
1393
|
+
);
|
|
1394
|
+
add("POST", "/sakura/agent-configs/:id/refresh", ({ params }) => {
|
|
1395
|
+
try {
|
|
1396
|
+
return ok(refreshCapabilities(params.id));
|
|
1397
|
+
} catch (e) {
|
|
1398
|
+
return fail("nonzero", e.message);
|
|
1399
|
+
}
|
|
1400
|
+
});
|
|
1401
|
+
add("GET", "/tailscale/health", () => installed());
|
|
1402
|
+
add("GET", "/tailscale/status", ({ port: port2 }) => status(port2));
|
|
1403
|
+
add("POST", "/tailscale/up", () => up());
|
|
1404
|
+
add("POST", "/tailscale/down", () => down());
|
|
1405
|
+
add("POST", "/tailscale/serve", ({ port: port2 }) => serve(port2));
|
|
1406
|
+
add("POST", "/tailscale/serve-off", () => serveOff());
|
|
1407
|
+
VERSION2 = "0.0.0";
|
|
1408
|
+
}
|
|
1409
|
+
});
|
|
1410
|
+
|
|
1411
|
+
// src/daemon/manager.ts
|
|
1412
|
+
import fs9 from "fs";
|
|
1413
|
+
import { spawn as spawn6 } from "child_process";
|
|
1414
|
+
function readDaemonInfo() {
|
|
1415
|
+
try {
|
|
1416
|
+
return JSON.parse(fs9.readFileSync(DAEMON_PATH, "utf8"));
|
|
1417
|
+
} catch {
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
function pidAlive(pid) {
|
|
1422
|
+
try {
|
|
1423
|
+
process.kill(pid, 0);
|
|
1424
|
+
return true;
|
|
1425
|
+
} catch {
|
|
1426
|
+
return false;
|
|
1427
|
+
}
|
|
1428
|
+
}
|
|
1429
|
+
function running() {
|
|
1430
|
+
const info2 = readDaemonInfo();
|
|
1431
|
+
if (info2 && pidAlive(info2.pid)) return info2;
|
|
1432
|
+
if (info2) {
|
|
1433
|
+
fs9.rmSync(DAEMON_PATH, { force: true });
|
|
1434
|
+
fs9.rmSync(PID_PATH, { force: true });
|
|
1435
|
+
}
|
|
1436
|
+
return null;
|
|
1437
|
+
}
|
|
1438
|
+
async function runServer(version) {
|
|
1439
|
+
ensureSakuraDirs();
|
|
1440
|
+
enableFileLogging();
|
|
1441
|
+
setVersion(version);
|
|
1442
|
+
const cfg = loadConfig();
|
|
1443
|
+
const host = process.env.SAKURA_HOST || cfg.daemon.host;
|
|
1444
|
+
const port2 = Number(process.env.SAKURA_PORT || cfg.daemon.port);
|
|
1445
|
+
const server = createServer(port2);
|
|
1446
|
+
await new Promise((resolve, reject) => {
|
|
1447
|
+
server.once("error", reject);
|
|
1448
|
+
server.listen(port2, host === "127.0.0.1" ? "0.0.0.0" : host, resolve);
|
|
1449
|
+
});
|
|
1450
|
+
const info2 = {
|
|
1451
|
+
pid: process.pid,
|
|
1452
|
+
host,
|
|
1453
|
+
port: port2,
|
|
1454
|
+
url: `http://${host}:${port2}`,
|
|
1455
|
+
startedAt: Date.now()
|
|
1456
|
+
};
|
|
1457
|
+
fs9.writeFileSync(DAEMON_PATH, JSON.stringify(info2, null, 2));
|
|
1458
|
+
fs9.writeFileSync(PID_PATH, String(process.pid));
|
|
1459
|
+
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
1460
|
+
log.info(`sakuraai runtime listening on http://0.0.0.0:${port2}`);
|
|
1461
|
+
if (tailnet) log.info(`reachable over Tailscale at ${tailnet}`);
|
|
1462
|
+
log.info("point the Sakura app at the tailnet URL above (Settings \u2192 Connectivity)");
|
|
1463
|
+
const shutdown = () => {
|
|
1464
|
+
log.info("shutting down");
|
|
1465
|
+
server.close();
|
|
1466
|
+
fs9.rmSync(DAEMON_PATH, { force: true });
|
|
1467
|
+
fs9.rmSync(PID_PATH, { force: true });
|
|
1468
|
+
process.exit(0);
|
|
1469
|
+
};
|
|
1470
|
+
process.on("SIGINT", shutdown);
|
|
1471
|
+
process.on("SIGTERM", shutdown);
|
|
1472
|
+
}
|
|
1473
|
+
function start() {
|
|
1474
|
+
const existing = running();
|
|
1475
|
+
if (existing) return existing;
|
|
1476
|
+
ensureSakuraDirs();
|
|
1477
|
+
const entry = process.argv[1] ?? "";
|
|
1478
|
+
const out = fs9.openSync(LOG_PATH, "a");
|
|
1479
|
+
const err = fs9.openSync(LOG_PATH, "a");
|
|
1480
|
+
const child = spawn6(process.execPath, [entry, "__run-daemon"], {
|
|
1481
|
+
detached: true,
|
|
1482
|
+
stdio: ["ignore", out, err],
|
|
1483
|
+
env: process.env
|
|
1484
|
+
});
|
|
1485
|
+
child.unref();
|
|
1486
|
+
const deadline = Date.now() + 5e3;
|
|
1487
|
+
while (Date.now() < deadline) {
|
|
1488
|
+
const info2 = readDaemonInfo();
|
|
1489
|
+
if (info2 && pidAlive(info2.pid)) return info2;
|
|
1490
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 100);
|
|
1491
|
+
}
|
|
1492
|
+
throw new Error("daemon did not come up within 5s; check `sakuraai daemon logs`");
|
|
1493
|
+
}
|
|
1494
|
+
function stop() {
|
|
1495
|
+
const info2 = running();
|
|
1496
|
+
if (!info2) return false;
|
|
1497
|
+
try {
|
|
1498
|
+
process.kill(info2.pid, "SIGTERM");
|
|
1499
|
+
} catch {
|
|
1500
|
+
}
|
|
1501
|
+
fs9.rmSync(DAEMON_PATH, { force: true });
|
|
1502
|
+
fs9.rmSync(PID_PATH, { force: true });
|
|
1503
|
+
return true;
|
|
1504
|
+
}
|
|
1505
|
+
function restart() {
|
|
1506
|
+
stop();
|
|
1507
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 300);
|
|
1508
|
+
return start();
|
|
1509
|
+
}
|
|
1510
|
+
function logs(lines = 50) {
|
|
1511
|
+
try {
|
|
1512
|
+
const all = fs9.readFileSync(LOG_PATH, "utf8").split("\n");
|
|
1513
|
+
return all.slice(-lines).join("\n");
|
|
1514
|
+
} catch {
|
|
1515
|
+
return "";
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
var init_manager = __esm({
|
|
1519
|
+
"src/daemon/manager.ts"() {
|
|
1520
|
+
"use strict";
|
|
1521
|
+
init_config();
|
|
1522
|
+
init_logger();
|
|
1523
|
+
init_server();
|
|
1524
|
+
init_tailscale();
|
|
1525
|
+
}
|
|
1526
|
+
});
|
|
1527
|
+
|
|
1528
|
+
// src/pairing.ts
|
|
1529
|
+
import qrcode from "qrcode-terminal";
|
|
1530
|
+
async function buildPairing() {
|
|
1531
|
+
const token = requireToken();
|
|
1532
|
+
const cfg = loadConfig();
|
|
1533
|
+
const port2 = Number(process.env.SAKURA_PORT || cfg.daemon.port);
|
|
1534
|
+
const tailnet = await discoverBaseUrl(port2).catch(() => null);
|
|
1535
|
+
const url = tailnet ?? `http://${localIp()}:${port2}`;
|
|
1536
|
+
const deepLink = `sakura://pair?url=${encodeURIComponent(url)}&token=${encodeURIComponent(token)}`;
|
|
1537
|
+
return { url, token, deepLink };
|
|
1538
|
+
}
|
|
1539
|
+
function renderQr(deepLink) {
|
|
1540
|
+
return new Promise((resolve) => {
|
|
1541
|
+
qrcode.generate(deepLink, { small: true }, (qr) => resolve(qr));
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
function localIp() {
|
|
1545
|
+
try {
|
|
1546
|
+
const os5 = __require("os");
|
|
1547
|
+
const nets = os5.networkInterfaces();
|
|
1548
|
+
for (const name of Object.keys(nets)) {
|
|
1549
|
+
for (const net of nets[name] ?? []) {
|
|
1550
|
+
if (net.family === "IPv4" && !net.internal) return net.address;
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
} catch {
|
|
1554
|
+
}
|
|
1555
|
+
return "127.0.0.1";
|
|
1556
|
+
}
|
|
1557
|
+
var init_pairing = __esm({
|
|
1558
|
+
"src/pairing.ts"() {
|
|
1559
|
+
"use strict";
|
|
1560
|
+
init_config();
|
|
1561
|
+
init_auth();
|
|
1562
|
+
init_tailscale();
|
|
1563
|
+
}
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
// src/commands/pair.ts
|
|
1567
|
+
var pair_exports = {};
|
|
1568
|
+
__export(pair_exports, {
|
|
1569
|
+
registerPair: () => registerPair,
|
|
1570
|
+
showPairing: () => showPairing
|
|
1571
|
+
});
|
|
1572
|
+
async function showPairing(opts = {}) {
|
|
1573
|
+
const pairing = await buildPairing();
|
|
1574
|
+
if (opts.json) return printJson(pairing);
|
|
1575
|
+
const qr = await renderQr(pairing.deepLink);
|
|
1576
|
+
info("");
|
|
1577
|
+
info("Scan this with your phone to pair the Sakura app:");
|
|
1578
|
+
info("");
|
|
1579
|
+
info(qr);
|
|
1580
|
+
info(` URL: ${pairing.url}`);
|
|
1581
|
+
info(` Token: ${pairing.token}`);
|
|
1582
|
+
info("");
|
|
1583
|
+
info("Or paste the token into the app (Settings \u2192 Connect).");
|
|
1584
|
+
}
|
|
1585
|
+
function registerPair(program) {
|
|
1586
|
+
program.command("pair").description("Show a QR code + token to pair the Sakura mobile app").option("--json", "output the pairing payload as JSON").action(async (opts) => {
|
|
1587
|
+
if (!isLoggedIn()) die("Not signed in. Run `sakuraai login` first.");
|
|
1588
|
+
if (opts.json) {
|
|
1589
|
+
return showPairing(opts);
|
|
1590
|
+
}
|
|
1591
|
+
if (!running()) {
|
|
1592
|
+
warn("The daemon is not running \u2014 start it with `sakuraai daemon start` so the app can connect.");
|
|
1593
|
+
}
|
|
1594
|
+
await showPairing(opts);
|
|
1595
|
+
success("Waiting for the app to connect\u2026");
|
|
1596
|
+
});
|
|
1597
|
+
}
|
|
1598
|
+
var init_pair = __esm({
|
|
1599
|
+
"src/commands/pair.ts"() {
|
|
1600
|
+
"use strict";
|
|
1601
|
+
init_pairing();
|
|
1602
|
+
init_auth();
|
|
1603
|
+
init_manager();
|
|
1604
|
+
init_output();
|
|
1605
|
+
}
|
|
1606
|
+
});
|
|
1607
|
+
|
|
1608
|
+
// src/index.ts
|
|
1609
|
+
import { Command } from "commander";
|
|
1610
|
+
|
|
1611
|
+
// src/version.ts
|
|
1612
|
+
var VERSION = "0.0.1";
|
|
1613
|
+
|
|
1614
|
+
// src/index.ts
|
|
1615
|
+
init_config();
|
|
1616
|
+
|
|
1617
|
+
// src/commands/auth.ts
|
|
1618
|
+
init_auth();
|
|
1619
|
+
init_config();
|
|
1620
|
+
init_output();
|
|
1621
|
+
function registerAuth(program) {
|
|
1622
|
+
program.command("login").description("Sign in and register this machine (generates a CLI token to pair the app)").option("--auth <token>", "use a pre-created CLI token instead of generating one").option("--server-url <url>", "Sakura server / Convex URL to associate").option("--machine-name <name>", "override the machine name (defaults to hostname)").option("--json", "output as JSON").action((opts) => {
|
|
1623
|
+
const state = login({
|
|
1624
|
+
auth: opts.auth,
|
|
1625
|
+
serverUrl: opts.serverUrl,
|
|
1626
|
+
machineName: opts.machineName
|
|
1627
|
+
});
|
|
1628
|
+
if (opts.json) return printJson(state);
|
|
1629
|
+
success(`Signed in as ${state.login} on "${state.machineName}".`);
|
|
1630
|
+
info("");
|
|
1631
|
+
info("Pair the Sakura mobile app with this token (Settings \u2192 Connectivity):");
|
|
1632
|
+
info("");
|
|
1633
|
+
info(` ${state.token}`);
|
|
1634
|
+
info("");
|
|
1635
|
+
info("Then start the runtime so the app can reach this machine:");
|
|
1636
|
+
info(" sakuraai daemon start");
|
|
1637
|
+
});
|
|
1638
|
+
program.command("logout").description("Clear local credentials").action(() => {
|
|
1639
|
+
logout();
|
|
1640
|
+
success("Logged out. The mobile app can no longer reach this machine until you log in again.");
|
|
1641
|
+
});
|
|
1642
|
+
program.command("whoami").description("Show the current login + token status").option("--json", "output as JSON").action((opts) => {
|
|
1643
|
+
const auth = loadAuth();
|
|
1644
|
+
const has = !!getToken();
|
|
1645
|
+
if (opts.json) return printJson({ loggedIn: has, login: auth.login, machineName: auth.machineName });
|
|
1646
|
+
if (!has) return warn("Not signed in. Run `sakuraai login`.");
|
|
1647
|
+
info(`Signed in as ${auth.login ?? "unknown"} on "${auth.machineName ?? "unknown"}".`);
|
|
1648
|
+
});
|
|
1649
|
+
}
|
|
1650
|
+
|
|
1651
|
+
// src/commands/runtime.ts
|
|
1652
|
+
init_manager();
|
|
1653
|
+
init_tailscale();
|
|
1654
|
+
init_auth();
|
|
1655
|
+
init_output();
|
|
1656
|
+
function registerRuntime(program, version) {
|
|
1657
|
+
program.command("start").description("Run the Sakura runtime in this terminal (foreground)").option("--auth <token>", "sign in with a CLI token before starting").action(async (opts) => {
|
|
1658
|
+
if (opts.auth) {
|
|
1659
|
+
const { login: login2 } = await Promise.resolve().then(() => (init_auth(), auth_exports));
|
|
1660
|
+
login2({ auth: opts.auth });
|
|
1661
|
+
}
|
|
1662
|
+
if (!isLoggedIn()) {
|
|
1663
|
+
die("Not signed in. Run `sakuraai login` (or pass --auth <token>) first.");
|
|
1664
|
+
}
|
|
1665
|
+
await runServer(version);
|
|
1666
|
+
});
|
|
1667
|
+
const d = program.command("daemon").description("Manage the background runtime daemon");
|
|
1668
|
+
d.command("start").description("Start the background daemon").action(async () => {
|
|
1669
|
+
if (!isLoggedIn()) die("Not signed in. Run `sakuraai login` first.");
|
|
1670
|
+
const info0 = start();
|
|
1671
|
+
success(`Daemon running (pid ${info0.pid}) on ${info0.url}`);
|
|
1672
|
+
const tailnet = await discoverBaseUrl(info0.port).catch(() => null);
|
|
1673
|
+
if (!tailnet) {
|
|
1674
|
+
warn("Tailscale not detected. Run `sakuraai serve` after `tailscale up` for a private URL.");
|
|
1675
|
+
}
|
|
1676
|
+
const { showPairing: showPairing2 } = await Promise.resolve().then(() => (init_pair(), pair_exports));
|
|
1677
|
+
await showPairing2();
|
|
1678
|
+
});
|
|
1679
|
+
d.command("stop").description("Stop the background daemon").action(() => {
|
|
1680
|
+
stop() ? success("Daemon stopped.") : warn("Daemon was not running.");
|
|
1681
|
+
});
|
|
1682
|
+
d.command("restart").description("Restart the background daemon").action(() => {
|
|
1683
|
+
const i = restart();
|
|
1684
|
+
success(`Daemon restarted (pid ${i.pid}) on ${i.url}`);
|
|
1685
|
+
});
|
|
1686
|
+
d.command("status").description("Show runtime + connection status").option("--json", "output as JSON").action(async (opts) => {
|
|
1687
|
+
const i = running();
|
|
1688
|
+
const tailnet = i ? await discoverBaseUrl(i.port).catch(() => null) : null;
|
|
1689
|
+
if (opts.json) return printJson({ running: !!i, daemon: i, tailnetBaseUrl: tailnet });
|
|
1690
|
+
if (!i) return warn("Daemon is not running. Start it with `sakuraai daemon start`.");
|
|
1691
|
+
success(`Daemon running (pid ${i.pid}) on ${i.url}`);
|
|
1692
|
+
if (tailnet) info(`Tailscale URL: ${tailnet}`);
|
|
1693
|
+
});
|
|
1694
|
+
d.command("logs").description("Print recent daemon logs").option("--lines <n>", "number of lines", "50").action((opts) => {
|
|
1695
|
+
const out = logs(Number(opts.lines) || 50);
|
|
1696
|
+
info(out || "(no logs yet)");
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
// src/commands/session.ts
|
|
1701
|
+
init_sessions();
|
|
1702
|
+
init_output();
|
|
1703
|
+
import fs10 from "fs";
|
|
1704
|
+
function resolveSessionId(arg) {
|
|
1705
|
+
const id = arg || process.env.SAKURA_SESSION_ID;
|
|
1706
|
+
if (!id) die("Provide a sessionId (positional or via SAKURA_SESSION_ID).");
|
|
1707
|
+
return id;
|
|
1708
|
+
}
|
|
1709
|
+
async function resolvePrompt(positional, opts) {
|
|
1710
|
+
if (positional) return positional;
|
|
1711
|
+
if (opts.prompt) return opts.prompt;
|
|
1712
|
+
if (opts.promptFile) return fs10.readFileSync(opts.promptFile, "utf8");
|
|
1713
|
+
if (!process.stdin.isTTY) {
|
|
1714
|
+
const chunks = [];
|
|
1715
|
+
for await (const c of process.stdin) chunks.push(c);
|
|
1716
|
+
const s = Buffer.concat(chunks).toString("utf8").trim();
|
|
1717
|
+
if (s) return s;
|
|
1718
|
+
}
|
|
1719
|
+
die("No prompt provided. Pass text, --prompt, --prompt-file, or pipe via stdin.");
|
|
1720
|
+
}
|
|
1721
|
+
function registerSession(program) {
|
|
1722
|
+
const s = program.command("session").description("Create, chat with, and inspect coding sessions");
|
|
1723
|
+
s.command("list").description("List sessions in a workspace").option("--workspace <idOrSlug>", "workspace (informational; sessions are local)").option("--agent <agent>", "filter by agent (claude|codex|opencode)").option("--all", "include all sessions").option("--archived", "archived only (n/a for local; returns empty)").option("--limit <n>", "max results").option("--json", "output as JSON").action(async (opts) => {
|
|
1724
|
+
if (opts.archived) {
|
|
1725
|
+
if (opts.json) return printJson([]);
|
|
1726
|
+
return info("No archived sessions (archival is not tracked for local sessions).");
|
|
1727
|
+
}
|
|
1728
|
+
const list2 = await list({
|
|
1729
|
+
agent: opts.agent,
|
|
1730
|
+
limit: opts.limit ? Number(opts.limit) : void 0
|
|
1731
|
+
});
|
|
1732
|
+
if (opts.json) return printJson(list2);
|
|
1733
|
+
if (!list2.length) return info("No sessions found.");
|
|
1734
|
+
printTable(
|
|
1735
|
+
["ID", "Agent", "Status", "Updated", "Title"],
|
|
1736
|
+
list2.map((x) => [x.id.slice(0, 12), x.agent, x.status, x.updatedAt, x.title])
|
|
1737
|
+
);
|
|
1738
|
+
});
|
|
1739
|
+
s.command("show [sessionId]").description("Show session metadata").option("--json", "output as JSON").action(async (sessionId, opts) => {
|
|
1740
|
+
const id = resolveSessionId(sessionId);
|
|
1741
|
+
const meta = await show(id);
|
|
1742
|
+
if (!meta) die(`Session not found: ${id}`);
|
|
1743
|
+
const msgs = await messages4(id, meta.agent);
|
|
1744
|
+
const enriched = { ...meta, historyCount: msgs.length };
|
|
1745
|
+
if (opts.json) return printJson(enriched);
|
|
1746
|
+
info(`ID: ${meta.id}`);
|
|
1747
|
+
info(`Title: ${meta.title}`);
|
|
1748
|
+
info(`Agent: ${meta.agent}`);
|
|
1749
|
+
info(`Status: ${meta.status}`);
|
|
1750
|
+
info(`Updated: ${meta.updatedAt}`);
|
|
1751
|
+
info(`Machine: ${meta.machine}`);
|
|
1752
|
+
info(`Messages: ${msgs.length}`);
|
|
1753
|
+
});
|
|
1754
|
+
s.command("history [sessionId]").description("Read the visible transcript for a session").option("--limit <n>", "newest N turns").option("--all", "full transcript").option("--reverse", "newest first").option("--json", "output as JSON").option("--jsonl", "output as JSON lines").action(async (sessionId, opts) => {
|
|
1755
|
+
const id = resolveSessionId(sessionId);
|
|
1756
|
+
const msgs = await history(id, {
|
|
1757
|
+
all: opts.all,
|
|
1758
|
+
limit: opts.limit ? Number(opts.limit) : void 0,
|
|
1759
|
+
reverse: opts.reverse
|
|
1760
|
+
});
|
|
1761
|
+
if (opts.jsonl) return printJsonl(msgs);
|
|
1762
|
+
if (opts.json) return printJson(msgs);
|
|
1763
|
+
if (!msgs.length) return info("(no visible turns)");
|
|
1764
|
+
for (const m of msgs) {
|
|
1765
|
+
const who = m.role === "user" ? "\u203A" : "\u2039";
|
|
1766
|
+
info(`${who} [${m.role}] ${m.content}`);
|
|
1767
|
+
info("");
|
|
1768
|
+
}
|
|
1769
|
+
});
|
|
1770
|
+
s.command("create [prompt]").description("Create a new session on this machine and send the first prompt").option("--workspace <idOrSlug>", "workspace").option("--agent-config <idOrName>", "agent config").option("--agent <agent>", "agent runtime (claude|codex|opencode)", "claude").option("--repo <owner/repo>", "attach a GitHub repo (informational)").option("--local-project <id|name|path>", "attach a registered local project").option("--branch <name>", "starting branch").option("--mode <modeId>", "ACP mode override").option("--model <modelId>", "model override").option("--env <KEY=VALUE>", "env var (repeatable)", collect, []).option("--prompt <text>", "prompt text").option("--prompt-file <path>", "read prompt from a file").option("--json", "output as JSON").action(async (prompt, opts) => {
|
|
1771
|
+
const text = await resolvePrompt(prompt, opts);
|
|
1772
|
+
let cwd;
|
|
1773
|
+
if (opts.localProject) {
|
|
1774
|
+
const { resolveProject: resolveProject2 } = await Promise.resolve().then(() => (init_registry(), registry_exports));
|
|
1775
|
+
cwd = resolveProject2(opts.localProject)?.path;
|
|
1776
|
+
}
|
|
1777
|
+
const r = await create(text, { agent: opts.agent, cwd });
|
|
1778
|
+
if (opts.json) return printJson(r);
|
|
1779
|
+
if (!r.ok) die(r.error ?? "create failed");
|
|
1780
|
+
success("Session created.");
|
|
1781
|
+
if (r.output) info(r.output);
|
|
1782
|
+
});
|
|
1783
|
+
s.command("chat [sessionId] [prompt]").description("Send a new turn to an existing session").option("--prompt <text>", "prompt text").option("--prompt-file <path>", "read prompt from a file").option("--mode <modeId>", "ACP mode override").option("--model <modelId>", "model override").option("--json", "output as JSON").action(async (sessionId, prompt, opts) => {
|
|
1784
|
+
const id = resolveSessionId(sessionId);
|
|
1785
|
+
const text = await resolvePrompt(prompt, opts);
|
|
1786
|
+
const r = await chat(id, text);
|
|
1787
|
+
if (opts.json) return printJson(r);
|
|
1788
|
+
if (!r.ok) die(r.error ?? "chat failed");
|
|
1789
|
+
success("Turn sent.");
|
|
1790
|
+
if (r.output) info(r.output);
|
|
1791
|
+
});
|
|
1792
|
+
s.command("cancel [sessionId]").description("Cancel the running turn on a local session").action((sessionId) => {
|
|
1793
|
+
resolveSessionId(sessionId);
|
|
1794
|
+
die("Cancel is not supported for local file-backed sessions.", 2);
|
|
1795
|
+
});
|
|
1796
|
+
s.command("rename [sessionId] [title]").description("Rename a session").option("--title <title>", "new title").action((sessionId) => {
|
|
1797
|
+
resolveSessionId(sessionId);
|
|
1798
|
+
die("Rename is not supported for local file-backed sessions.", 2);
|
|
1799
|
+
});
|
|
1800
|
+
for (const action of ["archive", "restore", "delete"]) {
|
|
1801
|
+
s.command(`${action} [sessionId]`).description(`${action[0].toUpperCase()}${action.slice(1)} a session (disabled for local sessions)`).action((sessionId) => {
|
|
1802
|
+
resolveSessionId(sessionId);
|
|
1803
|
+
die(`${action} is disabled for local sessions to avoid deleting agent history.`, 2);
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
}
|
|
1807
|
+
function collect(value, prev) {
|
|
1808
|
+
return prev.concat([value]);
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1811
|
+
// src/commands/registry.ts
|
|
1812
|
+
init_registry();
|
|
1813
|
+
init_output();
|
|
1814
|
+
function collect2(value, prev) {
|
|
1815
|
+
return prev.concat([value]);
|
|
1816
|
+
}
|
|
1817
|
+
function envToObj(pairs) {
|
|
1818
|
+
const out = {};
|
|
1819
|
+
for (const kv of pairs) {
|
|
1820
|
+
const i = kv.indexOf("=");
|
|
1821
|
+
if (i > 0) out[kv.slice(0, i)] = kv.slice(i + 1);
|
|
1822
|
+
}
|
|
1823
|
+
return out;
|
|
1824
|
+
}
|
|
1825
|
+
function registerRegistry(program) {
|
|
1826
|
+
const ws = program.command("workspace").description("Discover accessible workspaces");
|
|
1827
|
+
ws.command("list").description("List workspaces you can access").option("--json", "output as JSON").action((opts) => {
|
|
1828
|
+
const list2 = listWorkspaces();
|
|
1829
|
+
if (opts.json) return printJson(list2);
|
|
1830
|
+
printTable(["ID", "Name", "Slug"], list2.map((w) => [w.id, w.name, w.slug]));
|
|
1831
|
+
});
|
|
1832
|
+
const gh = program.command("github").description("GitHub repositories linked to a workspace");
|
|
1833
|
+
gh.command("list").description("List GitHub repositories linked to a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
|
|
1834
|
+
if (opts.json) return printJson([]);
|
|
1835
|
+
warn("GitHub repositories are managed by the Sakura server, not the local runtime.");
|
|
1836
|
+
info("Link repos in the Sakura app (Settings \u2192 GitHub).");
|
|
1837
|
+
});
|
|
1838
|
+
const proj = program.command("project").description("Manage local project registrations");
|
|
1839
|
+
proj.command("add [path]").description("Register a local project directory").option("--name <name>", "display name (defaults to folder name)").option("--json", "output as JSON").action((p, opts) => {
|
|
1840
|
+
try {
|
|
1841
|
+
const project = addProject(p ?? process.cwd(), opts.name);
|
|
1842
|
+
if (opts.json) return printJson(project);
|
|
1843
|
+
success(`Registered project "${project.name}" \u2192 ${project.path}`);
|
|
1844
|
+
} catch (e) {
|
|
1845
|
+
die(e.message);
|
|
1846
|
+
}
|
|
1847
|
+
});
|
|
1848
|
+
proj.command("list").description("List registered local projects").option("--json", "output as JSON").action((opts) => {
|
|
1849
|
+
const list2 = listProjects();
|
|
1850
|
+
if (opts.json) return printJson(list2);
|
|
1851
|
+
if (!list2.length) return info("No projects registered. Add one with `sakuraai project add`.");
|
|
1852
|
+
printTable(["ID", "Name", "Path"], list2.map((p) => [p.id.slice(0, 8), p.name, p.path]));
|
|
1853
|
+
});
|
|
1854
|
+
proj.command("delete <idOrNameOrPath>").description("Delete a registered local project").action((sel) => {
|
|
1855
|
+
deleteProject(sel) ? success("Project removed.") : warn("No matching project.");
|
|
1856
|
+
});
|
|
1857
|
+
const m = program.command("machine").description("Inspect machines in a workspace");
|
|
1858
|
+
m.command("list").description("List machines in a workspace").option("--workspace <idOrSlug>", "workspace").option("--online-only", "only machines with a recent heartbeat").option("--include-acp-capabilities", "include capability cache in JSON").option("--json", "output as JSON").action((opts) => {
|
|
1859
|
+
const list2 = listMachines(!!opts.onlineOnly);
|
|
1860
|
+
if (opts.json) return printJson(list2);
|
|
1861
|
+
printTable(
|
|
1862
|
+
["ID", "Name", "Online", "CLI"],
|
|
1863
|
+
list2.map((x) => [x.id.slice(0, 8), x.name, x.online ? "yes" : "no", x.cli.join(", ") || "\u2014"])
|
|
1864
|
+
);
|
|
1865
|
+
});
|
|
1866
|
+
const ac = program.command("agent-config").description("Manage agent configs");
|
|
1867
|
+
ac.command("list").description("List configs in a workspace").option("--workspace <idOrSlug>", "workspace").option("--json", "output as JSON").action((opts) => {
|
|
1868
|
+
const list2 = listAgentConfigs();
|
|
1869
|
+
if (opts.json) return printJson(list2);
|
|
1870
|
+
if (!list2.length) return info("No agent configs. Create one with `sakuraai agent-config create`.");
|
|
1871
|
+
printTable(
|
|
1872
|
+
["ID", "Name", "Type", "Description"],
|
|
1873
|
+
list2.map((a) => [a.id.slice(0, 8), a.name, a.agentType, a.description ?? ""])
|
|
1874
|
+
);
|
|
1875
|
+
});
|
|
1876
|
+
ac.command("show <idOrName>").description("Show one config").option("--json", "output as JSON").action((sel, opts) => {
|
|
1877
|
+
const a = resolveAgentConfig(sel);
|
|
1878
|
+
if (!a) die(`No agent-config matching "${sel}".`);
|
|
1879
|
+
if (opts.json) return printJson(a);
|
|
1880
|
+
info(JSON.stringify(a, null, 2));
|
|
1881
|
+
});
|
|
1882
|
+
ac.command("create").description("Create a new config").requiredOption("--agent-type <type>", "agent runtime (claude|codex|opencode|<registry>)").requiredOption("--name <name>", "display name").option("--workspace <idOrSlug>", "workspace").option("--description <text>", "description").option("--env <KEY=VALUE>", "env var (repeatable)", collect2, []).option("--prompt <text>", "default prompt prefix").option("--json", "output as JSON").action((opts) => {
|
|
1883
|
+
const a = createAgentConfig({
|
|
1884
|
+
name: opts.name,
|
|
1885
|
+
agentType: opts.agentType,
|
|
1886
|
+
description: opts.description,
|
|
1887
|
+
env: envToObj(opts.env),
|
|
1888
|
+
prompt: opts.prompt
|
|
1889
|
+
});
|
|
1890
|
+
if (opts.json) return printJson(a);
|
|
1891
|
+
success(`Created agent-config "${a.name}" (${a.id}).`);
|
|
1892
|
+
});
|
|
1893
|
+
ac.command("update <idOrName>").description("Update an existing config").option("--name <name>", "new name").option("--description <text>", 'description ("" clears)').option("--env <KEY=VALUE>", "merge env var (repeatable)", collect2, []).option("--unset-env <KEY>", "remove env var (repeatable)", collect2, []).option("--prompt <text>", 'default prompt prefix ("" clears)').option("--json", "output as JSON").action((sel, opts) => {
|
|
1894
|
+
try {
|
|
1895
|
+
const a = updateAgentConfig(sel, {
|
|
1896
|
+
name: opts.name,
|
|
1897
|
+
description: opts.description,
|
|
1898
|
+
env: opts.env.length ? envToObj(opts.env) : void 0,
|
|
1899
|
+
unsetEnv: opts.unsetEnv,
|
|
1900
|
+
prompt: opts.prompt
|
|
1901
|
+
});
|
|
1902
|
+
if (opts.json) return printJson(a);
|
|
1903
|
+
success(`Updated agent-config "${a.name}".`);
|
|
1904
|
+
} catch (e) {
|
|
1905
|
+
die(e.message);
|
|
1906
|
+
}
|
|
1907
|
+
});
|
|
1908
|
+
ac.command("delete <idOrName>").description("Delete a config").action((sel) => {
|
|
1909
|
+
deleteAgentConfig(sel) ? success("Deleted.") : die(`No agent-config matching "${sel}".`);
|
|
1910
|
+
});
|
|
1911
|
+
ac.command("refresh-capabilities <idOrName>").description("Refresh supported modes/models on the current machine").option("--workspace <idOrSlug>", "workspace").option("--machine <idOrName>", "machine (defaults to current)").option("--json", "output as JSON").action((sel, opts) => {
|
|
1912
|
+
try {
|
|
1913
|
+
const caps = refreshCapabilities(sel);
|
|
1914
|
+
if (opts.json) return printJson(caps);
|
|
1915
|
+
success(`Refreshed capabilities for "${sel}".`);
|
|
1916
|
+
info(` CLI available: ${caps.cliAvailable ? "yes" : "no"}`);
|
|
1917
|
+
info(` Models: ${caps.models.join(", ")}`);
|
|
1918
|
+
info(` Modes: ${caps.modes.join(", ")}`);
|
|
1919
|
+
} catch (e) {
|
|
1920
|
+
die(e.message);
|
|
1921
|
+
}
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
// src/commands/connectivity.ts
|
|
1926
|
+
init_tailscale();
|
|
1927
|
+
init_config();
|
|
1928
|
+
init_output();
|
|
1929
|
+
function port() {
|
|
1930
|
+
return Number(process.env.SAKURA_PORT || loadConfig().daemon.port);
|
|
1931
|
+
}
|
|
1932
|
+
function registerConnectivity(program) {
|
|
1933
|
+
const ts = program.command("tailscale").alias("ts").description("Tailscale connectivity for private PC \u2194 mobile access");
|
|
1934
|
+
ts.command("status").description("Show tailnet status + the base URL the app should use").option("--json", "output as JSON").action(async (opts) => {
|
|
1935
|
+
const r = await status(port());
|
|
1936
|
+
if (opts.json) return printJson(r);
|
|
1937
|
+
if (!r.ok) return die(r.stderr || "tailscale status failed");
|
|
1938
|
+
const s = r.data?.sakura;
|
|
1939
|
+
success(`Tailscale: ${s?.backendState ?? "unknown"} (${s?.online ? "online" : "offline"})`);
|
|
1940
|
+
if (s?.tailscaleIp) info(`Tailnet IP: ${s.tailscaleIp}`);
|
|
1941
|
+
if (s?.dnsName) info(`DNS name: ${s.dnsName}`);
|
|
1942
|
+
if (s?.baseUrl) {
|
|
1943
|
+
info(`App base URL: ${s.baseUrl}`);
|
|
1944
|
+
info("Set this as EXPO_PUBLIC_API_URL (or paste it in Settings \u2192 Connectivity).");
|
|
1945
|
+
}
|
|
1946
|
+
});
|
|
1947
|
+
ts.command("up").description("Bring this node onto the tailnet").action(async () => {
|
|
1948
|
+
const r = await up();
|
|
1949
|
+
r.ok ? success("Tailscale up.") : die(r.stderr);
|
|
1950
|
+
});
|
|
1951
|
+
ts.command("down").description("Disconnect from the tailnet").action(async () => {
|
|
1952
|
+
const r = await down();
|
|
1953
|
+
r.ok ? success("Tailscale down.") : die(r.stderr);
|
|
1954
|
+
});
|
|
1955
|
+
program.command("serve").description("Expose the runtime port over the tailnet via Tailscale (HTTPS)").action(async () => {
|
|
1956
|
+
const r = await serve(port());
|
|
1957
|
+
if (!r.ok) return die(r.stderr);
|
|
1958
|
+
success("Serving the runtime over the tailnet.");
|
|
1959
|
+
const status2 = await status(port());
|
|
1960
|
+
const url = status2.data?.sakura?.baseUrl;
|
|
1961
|
+
if (url) info(`App base URL: ${url}`);
|
|
1962
|
+
else warn("Run `sakuraai tailscale status` to see the URL.");
|
|
1963
|
+
});
|
|
1964
|
+
program.command("serve-off").description("Stop serving the runtime over the tailnet").action(async () => {
|
|
1965
|
+
const r = await serveOff();
|
|
1966
|
+
r.ok ? success("Stopped serving.") : die(r.stderr);
|
|
1967
|
+
});
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/index.ts
|
|
1971
|
+
init_pair();
|
|
1972
|
+
init_manager();
|
|
1973
|
+
async function main() {
|
|
1974
|
+
ensureSakuraDirs();
|
|
1975
|
+
if (process.argv[2] === "__run-daemon") {
|
|
1976
|
+
await runServer(VERSION);
|
|
1977
|
+
return;
|
|
1978
|
+
}
|
|
1979
|
+
const program = new Command();
|
|
1980
|
+
program.name("sakuraai").description(
|
|
1981
|
+
"Sakura Agent CLI \u2014 manage local AI coding sessions and reach them from the Sakura app over Tailscale"
|
|
1982
|
+
).version(VERSION, "-v, --version", "output the version number");
|
|
1983
|
+
registerAuth(program);
|
|
1984
|
+
registerRuntime(program, VERSION);
|
|
1985
|
+
registerSession(program);
|
|
1986
|
+
registerRegistry(program);
|
|
1987
|
+
registerConnectivity(program);
|
|
1988
|
+
registerPair(program);
|
|
1989
|
+
program.showHelpAfterError("(add --help for usage)");
|
|
1990
|
+
await program.parseAsync(process.argv);
|
|
1991
|
+
}
|
|
1992
|
+
main().catch((e) => {
|
|
1993
|
+
process.stderr.write(`\u2717 ${e.message}
|
|
1994
|
+
`);
|
|
1995
|
+
process.exit(1);
|
|
1996
|
+
});
|