0agent 1.0.27 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/0agent.js +56 -0
- package/bin/chat.js +81 -1
- package/dist/daemon.mjs +793 -8
- package/package.json +1 -1
package/bin/0agent.js
CHANGED
|
@@ -126,6 +126,10 @@ switch (cmd) {
|
|
|
126
126
|
await runMemoryCommand(args.slice(1));
|
|
127
127
|
break;
|
|
128
128
|
|
|
129
|
+
case 'codespace':
|
|
130
|
+
await runCodespaceCommand(args.slice(1));
|
|
131
|
+
break;
|
|
132
|
+
|
|
129
133
|
default:
|
|
130
134
|
showHelp();
|
|
131
135
|
break;
|
|
@@ -1095,6 +1099,58 @@ async function daemonMemorySync(direction) {
|
|
|
1095
1099
|
} catch { return null; }
|
|
1096
1100
|
}
|
|
1097
1101
|
|
|
1102
|
+
// ─── Codespace commands ───────────────────────────────────────────────────────
|
|
1103
|
+
|
|
1104
|
+
async function runCodespaceCommand(csArgs) {
|
|
1105
|
+
const sub = csArgs[0] ?? 'status';
|
|
1106
|
+
|
|
1107
|
+
switch (sub) {
|
|
1108
|
+
case 'setup': {
|
|
1109
|
+
console.log('\n Setting up GitHub Codespace browser backend...\n');
|
|
1110
|
+
const result = await fetch(`${BASE_URL}/api/codespace/setup`, { method: 'POST' }).catch(() => null);
|
|
1111
|
+
const data = result?.ok ? await result.json().catch(() => null) : null;
|
|
1112
|
+
if (data?.started) {
|
|
1113
|
+
console.log(` \x1b[32m✓\x1b[0m Codespace provisioning started`);
|
|
1114
|
+
console.log(` \x1b[2mFirst time: ~2-3 min. Check with: 0agent codespace status\x1b[0m\n`);
|
|
1115
|
+
} else {
|
|
1116
|
+
console.log(` \x1b[33m⚠\x1b[0m ${data?.error ?? 'Configure GitHub memory first: 0agent memory connect github'}\n`);
|
|
1117
|
+
}
|
|
1118
|
+
break;
|
|
1119
|
+
}
|
|
1120
|
+
case 'status': {
|
|
1121
|
+
const result = await fetch(`${BASE_URL}/api/codespace/status`).catch(() => null);
|
|
1122
|
+
const data = result?.ok ? await result.json().catch(() => null) : null;
|
|
1123
|
+
if (data) {
|
|
1124
|
+
const state = data.state ?? 'unknown';
|
|
1125
|
+
const icon = state === 'Available' ? '\x1b[32m✓\x1b[0m' : state === 'Shutdown' ? '\x1b[33m●\x1b[0m' : '\x1b[2m○\x1b[0m';
|
|
1126
|
+
console.log(`\n Browser backend: ${icon} ${state}`);
|
|
1127
|
+
if (data.ready) console.log(` Tunnel: \x1b[32m✓ open\x1b[0m → http://localhost:3001`);
|
|
1128
|
+
if (data.name) console.log(` Codespace: ${data.name}`);
|
|
1129
|
+
console.log(` Cost: ~60 hours/month free on GitHub personal\n`);
|
|
1130
|
+
} else {
|
|
1131
|
+
console.log('\n Codespace not configured. Run: 0agent codespace setup\n');
|
|
1132
|
+
}
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
case 'start': {
|
|
1136
|
+
process.stdout.write(' Starting codespace...');
|
|
1137
|
+
const result = await fetch(`${BASE_URL}/api/codespace/start`, { method: 'POST' }).catch(() => null);
|
|
1138
|
+
const data = result?.ok ? await result.json().catch(() => null) : null;
|
|
1139
|
+
console.log(data?.ok ? ' \x1b[32m✓\x1b[0m' : ` \x1b[31m✗\x1b[0m ${data?.error ?? 'failed'}`);
|
|
1140
|
+
break;
|
|
1141
|
+
}
|
|
1142
|
+
case 'stop': {
|
|
1143
|
+
process.stdout.write(' Stopping codespace (preserves state)...');
|
|
1144
|
+
const result = await fetch(`${BASE_URL}/api/codespace/stop`, { method: 'POST' }).catch(() => null);
|
|
1145
|
+
const data = result?.ok ? await result.json().catch(() => null) : null;
|
|
1146
|
+
console.log(data?.ok ? ' \x1b[32m✓\x1b[0m Stopped. Hours saved.' : ` \x1b[31m✗\x1b[0m`);
|
|
1147
|
+
break;
|
|
1148
|
+
}
|
|
1149
|
+
default:
|
|
1150
|
+
console.log(' Usage: 0agent codespace setup | status | start | stop');
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1098
1154
|
// ─── Result preview — confirms the agent's work actually ran ────────────────
|
|
1099
1155
|
|
|
1100
1156
|
async function showResultPreview(result) {
|
package/bin/chat.js
CHANGED
|
@@ -189,6 +189,13 @@ function handleWsEvent(event) {
|
|
|
189
189
|
lineBuffer += event.token;
|
|
190
190
|
break;
|
|
191
191
|
}
|
|
192
|
+
case 'schedule.fired': {
|
|
193
|
+
// Show when a scheduled job fires — even if user is idle
|
|
194
|
+
const ts = new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit' });
|
|
195
|
+
process.stdout.write(`\n ${fmt(C.magenta, '⏰')} [${ts}] Scheduled: ${fmt(C.bold, event.job_name)} — ${event.task}\n`);
|
|
196
|
+
if (!streaming) rl.prompt(true);
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
192
199
|
case 'session.completed': {
|
|
193
200
|
spinner.stop();
|
|
194
201
|
if (streaming) { process.stdout.write('\n'); streaming = false; }
|
|
@@ -380,6 +387,78 @@ async function handleCommand(input) {
|
|
|
380
387
|
}
|
|
381
388
|
|
|
382
389
|
// /skills
|
|
390
|
+
// /schedule — cron-like job scheduler
|
|
391
|
+
case '/schedule': {
|
|
392
|
+
const schedArgs = parts.slice(1);
|
|
393
|
+
const subCmd = schedArgs[0]?.toLowerCase() ?? 'list';
|
|
394
|
+
|
|
395
|
+
if (subCmd === 'list' || schedArgs.length === 0) {
|
|
396
|
+
const res = await fetch(`${BASE_URL}/api/schedule`).catch(() => null);
|
|
397
|
+
const jobs = res?.ok ? await res.json().catch(() => []) : [];
|
|
398
|
+
if (!Array.isArray(jobs) || jobs.length === 0) {
|
|
399
|
+
console.log('\n No scheduled jobs. Add one:\n ' +
|
|
400
|
+
fmt(C.dim, '/schedule add "run /retro" every Friday at 5pm') + '\n');
|
|
401
|
+
} else {
|
|
402
|
+
console.log('\n Scheduled jobs:\n');
|
|
403
|
+
for (const j of jobs) {
|
|
404
|
+
const status = j.enabled ? fmt(C.green, '●') : fmt(C.dim, '○');
|
|
405
|
+
const next = j.next_run_human ?? 'unknown';
|
|
406
|
+
console.log(` ${status} ${fmt(C.bold, j.id)} ${j.name}`);
|
|
407
|
+
console.log(` ${fmt(C.dim, j.schedule_human + ' · next: ' + next)}`);
|
|
408
|
+
}
|
|
409
|
+
console.log();
|
|
410
|
+
}
|
|
411
|
+
} else if (subCmd === 'add') {
|
|
412
|
+
// /schedule add "<task>" <schedule...>
|
|
413
|
+
// Parse: extract quoted task, rest is schedule
|
|
414
|
+
const rest = parts.slice(2).join(' ');
|
|
415
|
+
const quoted = rest.match(/^"([^"]+)"\s+(.+)$/) || rest.match(/^'([^']+)'\s+(.+)$/);
|
|
416
|
+
if (!quoted) {
|
|
417
|
+
console.log(` ${fmt(C.dim, 'Usage: /schedule add "<task>" <schedule>')}`);
|
|
418
|
+
console.log(` ${fmt(C.dim, 'Examples:')}`);
|
|
419
|
+
console.log(` ${fmt(C.cyan, ' /schedule add "run /retro" every Friday at 5pm')}`);
|
|
420
|
+
console.log(` ${fmt(C.cyan, ' /schedule add "run /review" every day at 9am')}`);
|
|
421
|
+
console.log(` ${fmt(C.cyan, ' /schedule add "check the build" in 2 hours')}\n`);
|
|
422
|
+
} else {
|
|
423
|
+
const task = quoted[1];
|
|
424
|
+
const schedule = quoted[2];
|
|
425
|
+
const res = await fetch(`${BASE_URL}/api/schedule`, {
|
|
426
|
+
method: 'POST',
|
|
427
|
+
headers: { 'Content-Type': 'application/json' },
|
|
428
|
+
body: JSON.stringify({ task, schedule }),
|
|
429
|
+
}).catch(() => null);
|
|
430
|
+
const data = res?.ok ? await res.json().catch(() => null) : null;
|
|
431
|
+
if (data?.id) {
|
|
432
|
+
console.log(` ${fmt(C.green, '✓')} Scheduled: ${fmt(C.bold, data.name)}`);
|
|
433
|
+
console.log(` ${fmt(C.dim, data.schedule_human + ' · next: ' + data.next_run_human)}\n`);
|
|
434
|
+
} else {
|
|
435
|
+
console.log(` ${fmt(C.red, '✗')} ${data?.error ?? 'Failed to create schedule'}\n`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
} else if (subCmd === 'delete' || subCmd === 'remove') {
|
|
439
|
+
const id = schedArgs[1];
|
|
440
|
+
if (!id) { console.log(' Usage: /schedule delete <id>\n'); break; }
|
|
441
|
+
const res = await fetch(`${BASE_URL}/api/schedule/${id}`, { method: 'DELETE' }).catch(() => null);
|
|
442
|
+
const data = res?.ok ? await res.json().catch(() => null) : null;
|
|
443
|
+
console.log(data?.ok
|
|
444
|
+
? ` ${fmt(C.green, '✓')} Deleted ${id}\n`
|
|
445
|
+
: ` ${fmt(C.red, '✗')} ${data?.error ?? 'Not found'}\n`);
|
|
446
|
+
} else if (subCmd === 'pause') {
|
|
447
|
+
const id = schedArgs[1];
|
|
448
|
+
if (!id) { console.log(' Usage: /schedule pause <id>\n'); break; }
|
|
449
|
+
await fetch(`${BASE_URL}/api/schedule/${id}/pause`, { method: 'POST' });
|
|
450
|
+
console.log(` ${fmt(C.green, '✓')} Paused ${id}\n`);
|
|
451
|
+
} else if (subCmd === 'resume') {
|
|
452
|
+
const id = schedArgs[1];
|
|
453
|
+
if (!id) { console.log(' Usage: /schedule resume <id>\n'); break; }
|
|
454
|
+
await fetch(`${BASE_URL}/api/schedule/${id}/resume`, { method: 'POST' });
|
|
455
|
+
console.log(` ${fmt(C.green, '✓')} Resumed ${id}\n`);
|
|
456
|
+
} else {
|
|
457
|
+
console.log(' Usage: /schedule list | add "<task>" <schedule> | delete <id> | pause <id> | resume <id>\n');
|
|
458
|
+
}
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
|
|
383
462
|
case '/skills': {
|
|
384
463
|
try {
|
|
385
464
|
const skills = await fetch(`${BASE_URL}/api/skills`).then(r => r.json());
|
|
@@ -442,6 +521,7 @@ const rl = createInterface({
|
|
|
442
521
|
historySize: 100,
|
|
443
522
|
completer: (line) => {
|
|
444
523
|
const commands = ['/model', '/key', '/status', '/skills', '/graph', '/clear', '/help',
|
|
524
|
+
'/schedule', '/schedule list', '/schedule add',
|
|
445
525
|
'/review', '/build', '/debug', '/qa', '/research', '/refactor', '/test-writer', '/retro'];
|
|
446
526
|
const hits = commands.filter(c => c.startsWith(line));
|
|
447
527
|
return [hits.length ? hits : commands, line];
|
|
@@ -569,7 +649,7 @@ rl.on('line', async (input) => {
|
|
|
569
649
|
const line = input.trim();
|
|
570
650
|
if (!line) { rl.prompt(); return; }
|
|
571
651
|
|
|
572
|
-
if (line.startsWith('/') || ['/model','/key','/status','/skills','/graph','/clear','/help'].some(c => line.startsWith(c))) {
|
|
652
|
+
if (line.startsWith('/') || ['/model','/key','/status','/skills','/graph','/clear','/help','/schedule'].some(c => line.startsWith(c))) {
|
|
573
653
|
await handleCommand(line);
|
|
574
654
|
rl.prompt();
|
|
575
655
|
} else {
|
package/dist/daemon.mjs
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
2
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
4
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
3
5
|
var __esm = (fn, res) => function __init() {
|
|
4
6
|
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
7
|
};
|
|
@@ -7,6 +9,15 @@ var __export = (target, all) => {
|
|
|
7
9
|
for (var name in all)
|
|
8
10
|
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
11
|
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
10
21
|
|
|
11
22
|
// packages/core/src/graph/GraphNode.ts
|
|
12
23
|
function createNode(params) {
|
|
@@ -2418,6 +2429,80 @@ var init_FileCapability = __esm({
|
|
|
2418
2429
|
}
|
|
2419
2430
|
});
|
|
2420
2431
|
|
|
2432
|
+
// packages/daemon/src/capabilities/CodespaceBrowserCapability.ts
|
|
2433
|
+
var CodespaceBrowserCapability_exports = {};
|
|
2434
|
+
__export(CodespaceBrowserCapability_exports, {
|
|
2435
|
+
CodespaceBrowserCapability: () => CodespaceBrowserCapability
|
|
2436
|
+
});
|
|
2437
|
+
var CodespaceBrowserCapability;
|
|
2438
|
+
var init_CodespaceBrowserCapability = __esm({
|
|
2439
|
+
"packages/daemon/src/capabilities/CodespaceBrowserCapability.ts"() {
|
|
2440
|
+
"use strict";
|
|
2441
|
+
init_BrowserCapability();
|
|
2442
|
+
CodespaceBrowserCapability = class {
|
|
2443
|
+
constructor(manager) {
|
|
2444
|
+
this.manager = manager;
|
|
2445
|
+
}
|
|
2446
|
+
name = "browser_open";
|
|
2447
|
+
description = "Open a URL in a cloud Linux browser (GitHub Codespace). Full JS, screenshots, clicks. Falls back to local Chrome.";
|
|
2448
|
+
fallback = new BrowserCapability();
|
|
2449
|
+
provisioningPromise = null;
|
|
2450
|
+
toolDefinition = {
|
|
2451
|
+
name: "browser_open",
|
|
2452
|
+
description: "Open URL in cloud Linux browser (GitHub Codespace). Handles JS-heavy SPAs, auth flows, screenshots. Fallback: local Chrome.",
|
|
2453
|
+
input_schema: {
|
|
2454
|
+
type: "object",
|
|
2455
|
+
properties: {
|
|
2456
|
+
url: { type: "string", description: "URL to open" },
|
|
2457
|
+
action: { type: "string", description: '"read" (default) | "screenshot" | "links" | "click" | "fill" | "snapshot"' },
|
|
2458
|
+
selector: { type: "string", description: "CSS selector for element to extract or interact with" },
|
|
2459
|
+
wait_for: { type: "string", description: "CSS selector to wait for before extracting" },
|
|
2460
|
+
wait_ms: { type: "number", description: "Additional wait after page load (for JS-heavy pages)" },
|
|
2461
|
+
value: { type: "string", description: "Value to fill (for action: fill)" }
|
|
2462
|
+
},
|
|
2463
|
+
required: ["url"]
|
|
2464
|
+
}
|
|
2465
|
+
};
|
|
2466
|
+
async execute(input, cwd) {
|
|
2467
|
+
const start = Date.now();
|
|
2468
|
+
try {
|
|
2469
|
+
if (!this.manager.isReady()) {
|
|
2470
|
+
if (!this.provisioningPromise) {
|
|
2471
|
+
this.provisioningPromise = this.manager.getReadyUrl().finally(() => {
|
|
2472
|
+
this.provisioningPromise = null;
|
|
2473
|
+
});
|
|
2474
|
+
}
|
|
2475
|
+
await this.provisioningPromise;
|
|
2476
|
+
}
|
|
2477
|
+
const res = await fetch(`${this.manager.localUrl}/browse`, {
|
|
2478
|
+
method: "POST",
|
|
2479
|
+
headers: { "Content-Type": "application/json" },
|
|
2480
|
+
body: JSON.stringify(input),
|
|
2481
|
+
signal: AbortSignal.timeout(6e4)
|
|
2482
|
+
});
|
|
2483
|
+
const data = await res.json();
|
|
2484
|
+
if (!data.ok) throw new Error(String(data.error ?? "Browse failed"));
|
|
2485
|
+
return {
|
|
2486
|
+
success: true,
|
|
2487
|
+
output: String(data.data ?? ""),
|
|
2488
|
+
structured: data,
|
|
2489
|
+
duration_ms: Date.now() - start
|
|
2490
|
+
};
|
|
2491
|
+
} catch (err) {
|
|
2492
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2493
|
+
console.warn("[CodespaceBrowser] Falling back to local Chrome:", errMsg);
|
|
2494
|
+
const result = await this.fallback.execute(input, cwd);
|
|
2495
|
+
return {
|
|
2496
|
+
...result,
|
|
2497
|
+
fallback_used: "local-chrome",
|
|
2498
|
+
duration_ms: Date.now() - start
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
};
|
|
2503
|
+
}
|
|
2504
|
+
});
|
|
2505
|
+
|
|
2421
2506
|
// packages/daemon/src/capabilities/CapabilityRegistry.ts
|
|
2422
2507
|
var CapabilityRegistry;
|
|
2423
2508
|
var init_CapabilityRegistry = __esm({
|
|
@@ -2430,9 +2515,28 @@ var init_CapabilityRegistry = __esm({
|
|
|
2430
2515
|
init_FileCapability();
|
|
2431
2516
|
CapabilityRegistry = class {
|
|
2432
2517
|
capabilities = /* @__PURE__ */ new Map();
|
|
2433
|
-
|
|
2518
|
+
/**
|
|
2519
|
+
* Constructor optionally accepts a CodespaceManager.
|
|
2520
|
+
* If provided and the gh CLI is available, uses CodespaceBrowserCapability
|
|
2521
|
+
* for browser_open — cloud Linux browser via SSH tunnel.
|
|
2522
|
+
*
|
|
2523
|
+
* SECURITY: The registry is only instantiated inside AgentExecutor,
|
|
2524
|
+
* which is only created for AUTHORISED subagents (trust_level: 1,
|
|
2525
|
+
* task_type: browser_task). The main agent does NOT have direct access
|
|
2526
|
+
* to browser_open without going through a subagent spawn.
|
|
2527
|
+
*/
|
|
2528
|
+
constructor(codespaceManager) {
|
|
2434
2529
|
this.register(new WebSearchCapability());
|
|
2435
|
-
|
|
2530
|
+
if (codespaceManager) {
|
|
2531
|
+
try {
|
|
2532
|
+
const { CodespaceBrowserCapability: CodespaceBrowserCapability2 } = (init_CodespaceBrowserCapability(), __toCommonJS(CodespaceBrowserCapability_exports));
|
|
2533
|
+
this.register(new CodespaceBrowserCapability2(codespaceManager));
|
|
2534
|
+
} catch {
|
|
2535
|
+
this.register(new BrowserCapability());
|
|
2536
|
+
}
|
|
2537
|
+
} else {
|
|
2538
|
+
this.register(new BrowserCapability());
|
|
2539
|
+
}
|
|
2436
2540
|
this.register(new ScraperCapability());
|
|
2437
2541
|
this.register(new ShellCapability());
|
|
2438
2542
|
this.register(new FileCapability());
|
|
@@ -2932,7 +3036,7 @@ var ProactiveSurface_exports = {};
|
|
|
2932
3036
|
__export(ProactiveSurface_exports, {
|
|
2933
3037
|
ProactiveSurface: () => ProactiveSurface
|
|
2934
3038
|
});
|
|
2935
|
-
import { execSync as
|
|
3039
|
+
import { execSync as execSync5 } from "node:child_process";
|
|
2936
3040
|
import { existsSync as existsSync12, readFileSync as readFileSync12, statSync, readdirSync as readdirSync5 } from "node:fs";
|
|
2937
3041
|
import { resolve as resolve11, join as join3 } from "node:path";
|
|
2938
3042
|
function readdirSafe(dir) {
|
|
@@ -3001,7 +3105,7 @@ var init_ProactiveSurface = __esm({
|
|
|
3001
3105
|
try {
|
|
3002
3106
|
const currentHead = this.getGitHead();
|
|
3003
3107
|
if (!currentHead || currentHead === this.lastKnownHead) return null;
|
|
3004
|
-
const log =
|
|
3108
|
+
const log = execSync5(
|
|
3005
3109
|
`git log ${this.lastKnownHead}..${currentHead} --oneline --stat`,
|
|
3006
3110
|
{ cwd: this.cwd, timeout: 3e3, encoding: "utf8" }
|
|
3007
3111
|
).trim();
|
|
@@ -3077,7 +3181,7 @@ var init_ProactiveSurface = __esm({
|
|
|
3077
3181
|
}
|
|
3078
3182
|
getGitHead() {
|
|
3079
3183
|
try {
|
|
3080
|
-
return
|
|
3184
|
+
return execSync5("git rev-parse HEAD", { cwd: this.cwd, timeout: 1e3, encoding: "utf8" }).trim();
|
|
3081
3185
|
} catch {
|
|
3082
3186
|
return "";
|
|
3083
3187
|
}
|
|
@@ -4644,7 +4748,7 @@ var SkillRegistry = class {
|
|
|
4644
4748
|
};
|
|
4645
4749
|
|
|
4646
4750
|
// packages/daemon/src/HTTPServer.ts
|
|
4647
|
-
import { Hono as
|
|
4751
|
+
import { Hono as Hono13 } from "hono";
|
|
4648
4752
|
import { serve } from "@hono/node-server";
|
|
4649
4753
|
import { readFileSync as readFileSync8 } from "node:fs";
|
|
4650
4754
|
import { resolve as resolve7, dirname as dirname3 } from "node:path";
|
|
@@ -4989,6 +5093,442 @@ function llmRoutes() {
|
|
|
4989
5093
|
return app;
|
|
4990
5094
|
}
|
|
4991
5095
|
|
|
5096
|
+
// packages/daemon/src/routes/codespace.ts
|
|
5097
|
+
import { Hono as Hono11 } from "hono";
|
|
5098
|
+
function codespaceRoutes(deps) {
|
|
5099
|
+
const app = new Hono11();
|
|
5100
|
+
app.get("/status", async (c) => {
|
|
5101
|
+
const mgr = deps.getManager();
|
|
5102
|
+
if (!mgr) return c.json({ configured: false, state: "not_configured" });
|
|
5103
|
+
const info = mgr.findExisting();
|
|
5104
|
+
const ping = mgr.isReady() ? await mgr.ping().catch(() => null) : null;
|
|
5105
|
+
return c.json({
|
|
5106
|
+
configured: true,
|
|
5107
|
+
state: info?.state ?? "not_found",
|
|
5108
|
+
name: info?.name ?? null,
|
|
5109
|
+
ready: mgr.isReady(),
|
|
5110
|
+
browser_ok: ping?.ok ?? false
|
|
5111
|
+
});
|
|
5112
|
+
});
|
|
5113
|
+
app.post("/setup", async (c) => {
|
|
5114
|
+
const result = await deps.setup();
|
|
5115
|
+
return c.json(result);
|
|
5116
|
+
});
|
|
5117
|
+
app.post("/start", async (c) => {
|
|
5118
|
+
const mgr = deps.getManager();
|
|
5119
|
+
if (!mgr) return c.json({ ok: false, error: "Not configured" }, 404);
|
|
5120
|
+
try {
|
|
5121
|
+
const url = await mgr.getReadyUrl();
|
|
5122
|
+
return c.json({ ok: true, url });
|
|
5123
|
+
} catch (err) {
|
|
5124
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
5125
|
+
}
|
|
5126
|
+
});
|
|
5127
|
+
app.post("/stop", async (c) => {
|
|
5128
|
+
const mgr = deps.getManager();
|
|
5129
|
+
if (!mgr) return c.json({ ok: false, error: "Not configured" }, 404);
|
|
5130
|
+
try {
|
|
5131
|
+
await mgr.stop();
|
|
5132
|
+
return c.json({ ok: true });
|
|
5133
|
+
} catch (err) {
|
|
5134
|
+
return c.json({ ok: false, error: err instanceof Error ? err.message : String(err) });
|
|
5135
|
+
}
|
|
5136
|
+
});
|
|
5137
|
+
return app;
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
// packages/daemon/src/routes/schedule.ts
|
|
5141
|
+
import { Hono as Hono12 } from "hono";
|
|
5142
|
+
|
|
5143
|
+
// packages/daemon/src/SchedulerManager.ts
|
|
5144
|
+
var DAYS = {
|
|
5145
|
+
sunday: 0,
|
|
5146
|
+
sun: 0,
|
|
5147
|
+
monday: 1,
|
|
5148
|
+
mon: 1,
|
|
5149
|
+
tuesday: 2,
|
|
5150
|
+
tue: 2,
|
|
5151
|
+
tues: 2,
|
|
5152
|
+
wednesday: 3,
|
|
5153
|
+
wed: 3,
|
|
5154
|
+
thursday: 4,
|
|
5155
|
+
thu: 4,
|
|
5156
|
+
thur: 4,
|
|
5157
|
+
thurs: 4,
|
|
5158
|
+
friday: 5,
|
|
5159
|
+
fri: 5,
|
|
5160
|
+
saturday: 6,
|
|
5161
|
+
sat: 6
|
|
5162
|
+
};
|
|
5163
|
+
function parseTime(s) {
|
|
5164
|
+
if (!s) return { hour: 9, minute: 0 };
|
|
5165
|
+
const m = s.trim().match(/^(\d{1,2})(?::(\d{2}))?\s*(am|pm)?$/i);
|
|
5166
|
+
if (!m) throw new Error(`Cannot parse time: "${s}". Use format like "9am", "5:30pm", "14:00"`);
|
|
5167
|
+
let hour = parseInt(m[1], 10);
|
|
5168
|
+
const minute = parseInt(m[2] ?? "0", 10);
|
|
5169
|
+
const ampm = m[3]?.toLowerCase();
|
|
5170
|
+
if (ampm === "pm" && hour !== 12) hour += 12;
|
|
5171
|
+
if (ampm === "am" && hour === 12) hour = 0;
|
|
5172
|
+
return { hour, minute };
|
|
5173
|
+
}
|
|
5174
|
+
function parseSchedule(text) {
|
|
5175
|
+
const t = text.trim().toLowerCase();
|
|
5176
|
+
const inMatch = t.match(/^in\s+(\d+)\s+(hour|hours|hr|hrs|minute|minutes|min|mins)$/);
|
|
5177
|
+
if (inMatch) {
|
|
5178
|
+
const n = parseInt(inMatch[1], 10);
|
|
5179
|
+
const isHours = inMatch[2].startsWith("h");
|
|
5180
|
+
const at = Date.now() + n * (isHours ? 36e5 : 6e4);
|
|
5181
|
+
return { spec: { type: "once", at }, human: text.trim() };
|
|
5182
|
+
}
|
|
5183
|
+
const tomorrowMatch = t.match(/^(?:tomorrow|tom)\s+at\s+(.+)$/);
|
|
5184
|
+
if (tomorrowMatch) {
|
|
5185
|
+
const { hour, minute } = parseTime(tomorrowMatch[1]);
|
|
5186
|
+
const d = /* @__PURE__ */ new Date();
|
|
5187
|
+
d.setDate(d.getDate() + 1);
|
|
5188
|
+
d.setHours(hour, minute, 0, 0);
|
|
5189
|
+
return { spec: { type: "once", at: d.getTime() }, human: text.trim() };
|
|
5190
|
+
}
|
|
5191
|
+
if (t === "every hour") return { spec: { type: "hourly", minute: 0 }, human: "every hour" };
|
|
5192
|
+
const everyMinsMatch = t.match(/^every\s+(\d+)\s+minutes?$/);
|
|
5193
|
+
if (everyMinsMatch) {
|
|
5194
|
+
const interval = parseInt(everyMinsMatch[1], 10);
|
|
5195
|
+
return { spec: { type: "interval_minutes", interval }, human: `every ${interval} minutes` };
|
|
5196
|
+
}
|
|
5197
|
+
const DEFAULT_TIMES = {
|
|
5198
|
+
morning: "9am",
|
|
5199
|
+
evening: "6pm",
|
|
5200
|
+
night: "10pm",
|
|
5201
|
+
noon: "12pm",
|
|
5202
|
+
midnight: "12am"
|
|
5203
|
+
};
|
|
5204
|
+
const dailyMatch = t.match(/^every\s+(day|daily|morning|evening|night|noon|midnight)\s*(?:at\s+(.+))?$/);
|
|
5205
|
+
if (dailyMatch) {
|
|
5206
|
+
const period = dailyMatch[1];
|
|
5207
|
+
const timeStr = dailyMatch[2] ?? DEFAULT_TIMES[period] ?? "9am";
|
|
5208
|
+
const { hour, minute } = parseTime(timeStr);
|
|
5209
|
+
const human = `every ${period}${dailyMatch[2] ? " at " + dailyMatch[2] : ""}`;
|
|
5210
|
+
return { spec: { type: "daily", hour, minute }, human };
|
|
5211
|
+
}
|
|
5212
|
+
const weeklyMatch = t.match(/^every\s+(\w+)\s*(?:at\s+(.+))?$/);
|
|
5213
|
+
if (weeklyMatch && DAYS[weeklyMatch[1]] !== void 0) {
|
|
5214
|
+
const day = DAYS[weeklyMatch[1]];
|
|
5215
|
+
const { hour, minute } = parseTime(weeklyMatch[2] ?? "9am");
|
|
5216
|
+
const human = `every ${weeklyMatch[1]}${weeklyMatch[2] ? " at " + weeklyMatch[2] : ""}`;
|
|
5217
|
+
return { spec: { type: "weekly", day, hour, minute }, human };
|
|
5218
|
+
}
|
|
5219
|
+
throw new Error(
|
|
5220
|
+
`Could not understand schedule: "${text}"
|
|
5221
|
+
Try: "every Friday at 5pm" \xB7 "every day at 9am" \xB7 "every morning" \xB7 "in 2 hours" \xB7 "every 30 minutes"`
|
|
5222
|
+
);
|
|
5223
|
+
}
|
|
5224
|
+
function scheduleToHuman(spec) {
|
|
5225
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
5226
|
+
const hhmm = (h, m) => {
|
|
5227
|
+
const ampm = h >= 12 ? "pm" : "am";
|
|
5228
|
+
const h12 = h === 0 ? 12 : h > 12 ? h - 12 : h;
|
|
5229
|
+
return m === 0 ? `${h12}${ampm}` : `${h12}:${pad(m)}${ampm}`;
|
|
5230
|
+
};
|
|
5231
|
+
const DAYS_REV = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
|
5232
|
+
switch (spec.type) {
|
|
5233
|
+
case "once":
|
|
5234
|
+
return `once at ${new Date(spec.at).toLocaleString()}`;
|
|
5235
|
+
case "hourly":
|
|
5236
|
+
return `every hour`;
|
|
5237
|
+
case "interval_minutes":
|
|
5238
|
+
return `every ${spec.interval} minutes`;
|
|
5239
|
+
case "daily":
|
|
5240
|
+
return `every day at ${hhmm(spec.hour, spec.minute)}`;
|
|
5241
|
+
case "weekly":
|
|
5242
|
+
return `every ${DAYS_REV[spec.day]} at ${hhmm(spec.hour, spec.minute)}`;
|
|
5243
|
+
case "monthly":
|
|
5244
|
+
return `monthly on the ${spec.date}th at ${hhmm(spec.hour, spec.minute)}`;
|
|
5245
|
+
default:
|
|
5246
|
+
return "unknown schedule";
|
|
5247
|
+
}
|
|
5248
|
+
}
|
|
5249
|
+
function nextRunAt(spec, now = Date.now()) {
|
|
5250
|
+
const d = new Date(now);
|
|
5251
|
+
const next = new Date(now);
|
|
5252
|
+
switch (spec.type) {
|
|
5253
|
+
case "once":
|
|
5254
|
+
return spec.at;
|
|
5255
|
+
case "hourly": {
|
|
5256
|
+
next.setMinutes(spec.minute, 0, 0);
|
|
5257
|
+
if (next.getTime() <= now) next.setTime(next.getTime() + 36e5);
|
|
5258
|
+
return next.getTime();
|
|
5259
|
+
}
|
|
5260
|
+
case "interval_minutes": {
|
|
5261
|
+
const minsUntil = spec.interval - d.getMinutes() % spec.interval;
|
|
5262
|
+
return now + minsUntil * 6e4;
|
|
5263
|
+
}
|
|
5264
|
+
case "daily": {
|
|
5265
|
+
next.setHours(spec.hour, spec.minute, 0, 0);
|
|
5266
|
+
if (next.getTime() <= now) next.setDate(next.getDate() + 1);
|
|
5267
|
+
return next.getTime();
|
|
5268
|
+
}
|
|
5269
|
+
case "weekly": {
|
|
5270
|
+
const daysUntil = (spec.day - d.getDay() + 7) % 7 || 7;
|
|
5271
|
+
next.setDate(d.getDate() + daysUntil);
|
|
5272
|
+
next.setHours(spec.hour, spec.minute, 0, 0);
|
|
5273
|
+
if (next.getTime() <= now) next.setDate(next.getDate() + 7);
|
|
5274
|
+
return next.getTime();
|
|
5275
|
+
}
|
|
5276
|
+
case "monthly": {
|
|
5277
|
+
next.setDate(spec.date);
|
|
5278
|
+
next.setHours(spec.hour, spec.minute, 0, 0);
|
|
5279
|
+
if (next.getTime() <= now) next.setMonth(next.getMonth() + 1);
|
|
5280
|
+
return next.getTime();
|
|
5281
|
+
}
|
|
5282
|
+
}
|
|
5283
|
+
}
|
|
5284
|
+
var DDL = `
|
|
5285
|
+
CREATE TABLE IF NOT EXISTS scheduled_jobs (
|
|
5286
|
+
id TEXT PRIMARY KEY,
|
|
5287
|
+
name TEXT NOT NULL,
|
|
5288
|
+
task TEXT NOT NULL,
|
|
5289
|
+
skill TEXT,
|
|
5290
|
+
schedule_json TEXT NOT NULL,
|
|
5291
|
+
schedule_human TEXT NOT NULL,
|
|
5292
|
+
enabled INTEGER NOT NULL DEFAULT 1,
|
|
5293
|
+
last_run_at INTEGER,
|
|
5294
|
+
next_run_at INTEGER NOT NULL,
|
|
5295
|
+
run_count INTEGER NOT NULL DEFAULT 0,
|
|
5296
|
+
created_at INTEGER NOT NULL
|
|
5297
|
+
);
|
|
5298
|
+
CREATE INDEX IF NOT EXISTS idx_jobs_next ON scheduled_jobs(next_run_at, enabled);
|
|
5299
|
+
`;
|
|
5300
|
+
var SchedulerStore = class {
|
|
5301
|
+
constructor(adapter) {
|
|
5302
|
+
this.adapter = adapter;
|
|
5303
|
+
}
|
|
5304
|
+
initialised = false;
|
|
5305
|
+
init() {
|
|
5306
|
+
if (this.initialised) return;
|
|
5307
|
+
const db = this.adapter.db;
|
|
5308
|
+
db.exec(DDL);
|
|
5309
|
+
this.initialised = true;
|
|
5310
|
+
}
|
|
5311
|
+
save(job) {
|
|
5312
|
+
this.init();
|
|
5313
|
+
const db = this.adapter.db;
|
|
5314
|
+
db.prepare(`
|
|
5315
|
+
INSERT OR REPLACE INTO scheduled_jobs
|
|
5316
|
+
(id, name, task, skill, schedule_json, schedule_human, enabled, last_run_at, next_run_at, run_count, created_at)
|
|
5317
|
+
VALUES (?,?,?,?,?,?,?,?,?,?,?)
|
|
5318
|
+
`).run(
|
|
5319
|
+
job.id,
|
|
5320
|
+
job.name,
|
|
5321
|
+
job.task,
|
|
5322
|
+
job.skill ?? null,
|
|
5323
|
+
JSON.stringify(job.schedule),
|
|
5324
|
+
job.schedule_human,
|
|
5325
|
+
job.enabled ? 1 : 0,
|
|
5326
|
+
job.last_run_at ?? null,
|
|
5327
|
+
job.next_run_at,
|
|
5328
|
+
job.run_count,
|
|
5329
|
+
job.created_at
|
|
5330
|
+
);
|
|
5331
|
+
}
|
|
5332
|
+
delete(id) {
|
|
5333
|
+
this.init();
|
|
5334
|
+
const db = this.adapter.db;
|
|
5335
|
+
db.prepare("DELETE FROM scheduled_jobs WHERE id = ?").run(id);
|
|
5336
|
+
}
|
|
5337
|
+
list() {
|
|
5338
|
+
this.init();
|
|
5339
|
+
const db = this.adapter.db;
|
|
5340
|
+
const rows = db.prepare("SELECT * FROM scheduled_jobs ORDER BY next_run_at ASC").all();
|
|
5341
|
+
return rows.map(this.rowToJob);
|
|
5342
|
+
}
|
|
5343
|
+
getDue(now) {
|
|
5344
|
+
this.init();
|
|
5345
|
+
const db = this.adapter.db;
|
|
5346
|
+
const rows = db.prepare(
|
|
5347
|
+
"SELECT * FROM scheduled_jobs WHERE enabled = 1 AND next_run_at <= ?"
|
|
5348
|
+
).all(now);
|
|
5349
|
+
return rows.map(this.rowToJob);
|
|
5350
|
+
}
|
|
5351
|
+
rowToJob(row) {
|
|
5352
|
+
return {
|
|
5353
|
+
id: row.id,
|
|
5354
|
+
name: row.name,
|
|
5355
|
+
task: row.task,
|
|
5356
|
+
skill: row.skill,
|
|
5357
|
+
schedule: JSON.parse(row.schedule_json),
|
|
5358
|
+
schedule_human: row.schedule_human,
|
|
5359
|
+
enabled: row.enabled === 1,
|
|
5360
|
+
last_run_at: row.last_run_at,
|
|
5361
|
+
next_run_at: row.next_run_at,
|
|
5362
|
+
run_count: row.run_count,
|
|
5363
|
+
created_at: row.created_at
|
|
5364
|
+
};
|
|
5365
|
+
}
|
|
5366
|
+
};
|
|
5367
|
+
var SchedulerManager = class {
|
|
5368
|
+
constructor(adapter, sessions, eventBus) {
|
|
5369
|
+
this.sessions = sessions;
|
|
5370
|
+
this.eventBus = eventBus;
|
|
5371
|
+
this.store = new SchedulerStore(adapter);
|
|
5372
|
+
this.store.init();
|
|
5373
|
+
}
|
|
5374
|
+
store;
|
|
5375
|
+
timer = null;
|
|
5376
|
+
start() {
|
|
5377
|
+
if (this.timer) return;
|
|
5378
|
+
this.timer = setInterval(() => this.tick(), 3e4);
|
|
5379
|
+
if (this.timer && typeof this.timer === "object" && "unref" in this.timer) {
|
|
5380
|
+
this.timer.unref();
|
|
5381
|
+
}
|
|
5382
|
+
const init = setTimeout(() => this.tick(), 5e3);
|
|
5383
|
+
if (typeof init === "object" && "unref" in init) init.unref();
|
|
5384
|
+
}
|
|
5385
|
+
stop() {
|
|
5386
|
+
if (this.timer) {
|
|
5387
|
+
clearInterval(this.timer);
|
|
5388
|
+
this.timer = null;
|
|
5389
|
+
}
|
|
5390
|
+
}
|
|
5391
|
+
/** Add a new scheduled job. */
|
|
5392
|
+
add(params) {
|
|
5393
|
+
const { spec, human } = parseSchedule(params.schedule);
|
|
5394
|
+
const now = Date.now();
|
|
5395
|
+
const job = {
|
|
5396
|
+
id: crypto.randomUUID().slice(0, 8),
|
|
5397
|
+
// short ID for easy reference
|
|
5398
|
+
name: params.name ?? params.task.slice(0, 40),
|
|
5399
|
+
task: params.task,
|
|
5400
|
+
skill: params.skill,
|
|
5401
|
+
schedule: spec,
|
|
5402
|
+
schedule_human: human,
|
|
5403
|
+
enabled: true,
|
|
5404
|
+
next_run_at: nextRunAt(spec, now),
|
|
5405
|
+
run_count: 0,
|
|
5406
|
+
created_at: now
|
|
5407
|
+
};
|
|
5408
|
+
this.store.save(job);
|
|
5409
|
+
return job;
|
|
5410
|
+
}
|
|
5411
|
+
/** Pause/resume a job. */
|
|
5412
|
+
setPaused(id, paused) {
|
|
5413
|
+
const jobs = this.store.list();
|
|
5414
|
+
const job = jobs.find((j) => j.id === id);
|
|
5415
|
+
if (!job) return false;
|
|
5416
|
+
job.enabled = !paused;
|
|
5417
|
+
this.store.save(job);
|
|
5418
|
+
return true;
|
|
5419
|
+
}
|
|
5420
|
+
/** Delete a job. */
|
|
5421
|
+
remove(id) {
|
|
5422
|
+
const jobs = this.store.list();
|
|
5423
|
+
const exists = jobs.some((j) => j.id === id);
|
|
5424
|
+
if (!exists) return false;
|
|
5425
|
+
this.store.delete(id);
|
|
5426
|
+
return true;
|
|
5427
|
+
}
|
|
5428
|
+
/** List all jobs. */
|
|
5429
|
+
list() {
|
|
5430
|
+
return this.store.list();
|
|
5431
|
+
}
|
|
5432
|
+
// ─── Tick ─────────────────────────────────────────────────────────────────
|
|
5433
|
+
async tick() {
|
|
5434
|
+
const now = Date.now();
|
|
5435
|
+
const due = this.store.getDue(now);
|
|
5436
|
+
for (const job of due) {
|
|
5437
|
+
if (job.last_run_at && now - job.last_run_at < 5e4) continue;
|
|
5438
|
+
await this.fire(job);
|
|
5439
|
+
}
|
|
5440
|
+
}
|
|
5441
|
+
async fire(job) {
|
|
5442
|
+
job.last_run_at = Date.now();
|
|
5443
|
+
job.run_count++;
|
|
5444
|
+
if (job.schedule.type === "once") {
|
|
5445
|
+
job.enabled = false;
|
|
5446
|
+
} else {
|
|
5447
|
+
job.next_run_at = nextRunAt(job.schedule, Date.now() + 6e4);
|
|
5448
|
+
}
|
|
5449
|
+
this.store.save(job);
|
|
5450
|
+
this.eventBus.emit({
|
|
5451
|
+
type: "schedule.fired",
|
|
5452
|
+
job_id: job.id,
|
|
5453
|
+
job_name: job.name,
|
|
5454
|
+
task: job.task,
|
|
5455
|
+
run_count: job.run_count
|
|
5456
|
+
});
|
|
5457
|
+
try {
|
|
5458
|
+
const session = this.sessions.createSession({ task: job.task, skill: job.skill });
|
|
5459
|
+
this.sessions.runExistingSession(session.id, { task: job.task, skill: job.skill }).then(() => {
|
|
5460
|
+
this.eventBus.emit({ type: "schedule.completed", job_id: job.id, session_id: session.id });
|
|
5461
|
+
}).catch((err) => {
|
|
5462
|
+
this.eventBus.emit({ type: "schedule.error", job_id: job.id, error: String(err) });
|
|
5463
|
+
});
|
|
5464
|
+
} catch (err) {
|
|
5465
|
+
this.eventBus.emit({ type: "schedule.error", job_id: job.id, error: String(err) });
|
|
5466
|
+
}
|
|
5467
|
+
}
|
|
5468
|
+
};
|
|
5469
|
+
|
|
5470
|
+
// packages/daemon/src/routes/schedule.ts
|
|
5471
|
+
function scheduleRoutes(deps) {
|
|
5472
|
+
const app = new Hono12();
|
|
5473
|
+
const getScheduler = (c) => {
|
|
5474
|
+
if (!deps.scheduler) {
|
|
5475
|
+
return { error: c.json({ error: "Scheduler not available" }, 503) };
|
|
5476
|
+
}
|
|
5477
|
+
return { scheduler: deps.scheduler };
|
|
5478
|
+
};
|
|
5479
|
+
app.get("/", (c) => {
|
|
5480
|
+
const { scheduler, error } = getScheduler(c);
|
|
5481
|
+
if (error) return error;
|
|
5482
|
+
const jobs = scheduler.list().map((j) => ({
|
|
5483
|
+
...j,
|
|
5484
|
+
schedule_human: j.schedule_human || scheduleToHuman(j.schedule),
|
|
5485
|
+
next_run_human: new Date(j.next_run_at).toLocaleString()
|
|
5486
|
+
}));
|
|
5487
|
+
return c.json(jobs);
|
|
5488
|
+
});
|
|
5489
|
+
app.post("/", async (c) => {
|
|
5490
|
+
const { scheduler, error } = getScheduler(c);
|
|
5491
|
+
if (error) return error;
|
|
5492
|
+
const body = await c.req.json();
|
|
5493
|
+
if (!body.task || !body.schedule) {
|
|
5494
|
+
return c.json({ error: "task and schedule are required" }, 400);
|
|
5495
|
+
}
|
|
5496
|
+
try {
|
|
5497
|
+
const job = scheduler.add({
|
|
5498
|
+
task: body.task,
|
|
5499
|
+
schedule: body.schedule,
|
|
5500
|
+
name: body.name,
|
|
5501
|
+
skill: body.skill
|
|
5502
|
+
});
|
|
5503
|
+
return c.json({
|
|
5504
|
+
...job,
|
|
5505
|
+
next_run_human: new Date(job.next_run_at).toLocaleString()
|
|
5506
|
+
}, 201);
|
|
5507
|
+
} catch (err) {
|
|
5508
|
+
return c.json({ error: err instanceof Error ? err.message : String(err) }, 400);
|
|
5509
|
+
}
|
|
5510
|
+
});
|
|
5511
|
+
app.delete("/:id", (c) => {
|
|
5512
|
+
const { scheduler, error } = getScheduler(c);
|
|
5513
|
+
if (error) return error;
|
|
5514
|
+
const ok = scheduler.remove(c.req.param("id"));
|
|
5515
|
+
return ok ? c.json({ ok: true }) : c.json({ error: "Job not found" }, 404);
|
|
5516
|
+
});
|
|
5517
|
+
app.post("/:id/pause", (c) => {
|
|
5518
|
+
const { scheduler, error } = getScheduler(c);
|
|
5519
|
+
if (error) return error;
|
|
5520
|
+
const ok = scheduler.setPaused(c.req.param("id"), true);
|
|
5521
|
+
return ok ? c.json({ ok: true }) : c.json({ error: "Job not found" }, 404);
|
|
5522
|
+
});
|
|
5523
|
+
app.post("/:id/resume", (c) => {
|
|
5524
|
+
const { scheduler, error } = getScheduler(c);
|
|
5525
|
+
if (error) return error;
|
|
5526
|
+
const ok = scheduler.setPaused(c.req.param("id"), false);
|
|
5527
|
+
return ok ? c.json({ ok: true }) : c.json({ error: "Job not found" }, 404);
|
|
5528
|
+
});
|
|
5529
|
+
return app;
|
|
5530
|
+
}
|
|
5531
|
+
|
|
4992
5532
|
// packages/daemon/src/HTTPServer.ts
|
|
4993
5533
|
function findGraphHtml() {
|
|
4994
5534
|
const candidates = [
|
|
@@ -5014,7 +5554,7 @@ var HTTPServer = class {
|
|
|
5014
5554
|
deps;
|
|
5015
5555
|
constructor(deps) {
|
|
5016
5556
|
this.deps = deps;
|
|
5017
|
-
this.app = new
|
|
5557
|
+
this.app = new Hono13();
|
|
5018
5558
|
this.app.route("/api/health", healthRoutes({ getStatus: deps.getStatus }));
|
|
5019
5559
|
this.app.route("/api/sessions", sessionRoutes({ sessions: deps.sessions }));
|
|
5020
5560
|
this.app.route("/api/graph", graphRoutes({ graph: deps.graph }));
|
|
@@ -5025,6 +5565,11 @@ var HTTPServer = class {
|
|
|
5025
5565
|
this.app.route("/api/insights", insightsRoutes({ proactiveSurface: deps.proactiveSurface ?? null }));
|
|
5026
5566
|
this.app.route("/api/memory", memoryRoutes({ getSync: deps.getMemorySync ?? (() => null) }));
|
|
5027
5567
|
this.app.route("/api/llm", llmRoutes());
|
|
5568
|
+
this.app.route("/api/schedule", scheduleRoutes({ scheduler: deps.scheduler ?? null }));
|
|
5569
|
+
this.app.route("/api/codespace", codespaceRoutes({
|
|
5570
|
+
getManager: deps.getCodespaceManager ?? (() => null),
|
|
5571
|
+
setup: deps.setupCodespace ?? (async () => ({ started: false, error: "Not configured" }))
|
|
5572
|
+
}));
|
|
5028
5573
|
const serveGraph = (c) => {
|
|
5029
5574
|
try {
|
|
5030
5575
|
const html = readFileSync8(GRAPH_HTML_PATH, "utf8");
|
|
@@ -5428,6 +5973,7 @@ var GitHubMemorySync = class {
|
|
|
5428
5973
|
}
|
|
5429
5974
|
const readme = this.generateReadme(nodes.length, edges.length);
|
|
5430
5975
|
pushes.push(putFile(token, owner, repo, "README.md", readme, commitMsg));
|
|
5976
|
+
await this.ensureCodespaceFiles(token, owner, repo, commitMsg);
|
|
5431
5977
|
await Promise.all(pushes);
|
|
5432
5978
|
this.lastPushAt = now;
|
|
5433
5979
|
this.pendingChanges = false;
|
|
@@ -5623,6 +6169,36 @@ var GitHubMemorySync = class {
|
|
|
5623
6169
|
} catch {
|
|
5624
6170
|
}
|
|
5625
6171
|
}
|
|
6172
|
+
/**
|
|
6173
|
+
* Push the codespace browser server files to the memory repo.
|
|
6174
|
+
* This makes the memory repo a valid Codespace template — one repo for everything.
|
|
6175
|
+
* Only pushes if the files don't exist yet (idempotent).
|
|
6176
|
+
*/
|
|
6177
|
+
async ensureCodespaceFiles(token, owner, repo, msg) {
|
|
6178
|
+
const existing = await getFileSha(token, owner, repo, ".devcontainer/devcontainer.json");
|
|
6179
|
+
if (existing) return;
|
|
6180
|
+
const devcontainer = JSON.stringify({
|
|
6181
|
+
name: "0agent Browser Sandbox",
|
|
6182
|
+
image: "mcr.microsoft.com/devcontainers/javascript-node:22",
|
|
6183
|
+
postCreateCommand: "cd /workspaces && npm install && npx playwright install chromium --with-deps && pip3 install scrapling --quiet 2>/dev/null || true",
|
|
6184
|
+
postStartCommand: 'cd /workspaces && pkill -f "node server.js" 2>/dev/null; nohup node server.js > /tmp/browser-server.log 2>&1 &',
|
|
6185
|
+
forwardPorts: [3e3],
|
|
6186
|
+
portsAttributes: {
|
|
6187
|
+
"3000": { label: "0agent Browser", onAutoForward: "silent", visibility: "private" }
|
|
6188
|
+
}
|
|
6189
|
+
}, null, 2);
|
|
6190
|
+
const packageJson = JSON.stringify({
|
|
6191
|
+
name: "0agent-browser-server",
|
|
6192
|
+
version: "1.0.0",
|
|
6193
|
+
dependencies: { playwright: "^1.42.0" }
|
|
6194
|
+
}, null, 2);
|
|
6195
|
+
await Promise.all([
|
|
6196
|
+
putFile(token, owner, repo, ".devcontainer/devcontainer.json", devcontainer, msg),
|
|
6197
|
+
putFile(token, owner, repo, "package.json", packageJson, msg),
|
|
6198
|
+
// Note: server.js is too large to inline here — users pull it from the npm package at codespace start
|
|
6199
|
+
putFile(token, owner, repo, ".gitignore", "node_modules/\n*.log\n", msg)
|
|
6200
|
+
]);
|
|
6201
|
+
}
|
|
5626
6202
|
generateReadme(nodeCount, edgeCount) {
|
|
5627
6203
|
return `# 0agent Memory
|
|
5628
6204
|
|
|
@@ -5663,6 +6239,189 @@ git checkout <commit> graph/ # restore graph files
|
|
|
5663
6239
|
}
|
|
5664
6240
|
};
|
|
5665
6241
|
|
|
6242
|
+
// packages/daemon/src/CodespaceManager.ts
|
|
6243
|
+
import { execSync as execSync4, spawn as spawn3 } from "node:child_process";
|
|
6244
|
+
var BROWSER_PORT_REMOTE = 3e3;
|
|
6245
|
+
var BROWSER_PORT_LOCAL = 3001;
|
|
6246
|
+
var DISPLAY_NAME = "0agent-browser";
|
|
6247
|
+
var FORWARD_TIMEOUT_S = 60;
|
|
6248
|
+
var CodespaceManager = class {
|
|
6249
|
+
forwardProcess = null;
|
|
6250
|
+
_ready = false;
|
|
6251
|
+
_localUrl = `http://localhost:${BROWSER_PORT_LOCAL}`;
|
|
6252
|
+
memoryRepo;
|
|
6253
|
+
// e.g. "cadetmaze/0agent-memory"
|
|
6254
|
+
constructor(memoryRepo) {
|
|
6255
|
+
this.memoryRepo = memoryRepo;
|
|
6256
|
+
}
|
|
6257
|
+
/** Is the tunnel open and browser server responding? */
|
|
6258
|
+
isReady() {
|
|
6259
|
+
return this._ready;
|
|
6260
|
+
}
|
|
6261
|
+
/** URL to call the browser server (via SSH tunnel). */
|
|
6262
|
+
get localUrl() {
|
|
6263
|
+
return this._localUrl;
|
|
6264
|
+
}
|
|
6265
|
+
// ─── Main entry point ──────────────────────────────────────────────────────
|
|
6266
|
+
/**
|
|
6267
|
+
* Ensure the codespace is running, browser server is started, and tunnel is open.
|
|
6268
|
+
* Returns the local URL to call (http://localhost:3001).
|
|
6269
|
+
* First call: 2-3 minutes (cold provision). Subsequent: 30s or instant.
|
|
6270
|
+
*/
|
|
6271
|
+
async getReadyUrl() {
|
|
6272
|
+
const name = await this.ensureRunning();
|
|
6273
|
+
await this.startBrowserServer(name);
|
|
6274
|
+
await this.openTunnel(name);
|
|
6275
|
+
return this._localUrl;
|
|
6276
|
+
}
|
|
6277
|
+
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
6278
|
+
/** Find existing 0agent-browser codespace, or create one from the memory repo. */
|
|
6279
|
+
async getOrCreate() {
|
|
6280
|
+
const existing = this.findExisting();
|
|
6281
|
+
if (existing) return existing.name;
|
|
6282
|
+
console.log(`[Codespace] Creating browser codespace from ${this.memoryRepo}...`);
|
|
6283
|
+
console.log("[Codespace] First time: ~2-3 minutes. Subsequent starts: ~30 seconds.");
|
|
6284
|
+
try {
|
|
6285
|
+
const result = execSync4(
|
|
6286
|
+
`gh codespace create --repo "${this.memoryRepo}" --machine basicLinux32gb --display-name "${DISPLAY_NAME}" --json name`,
|
|
6287
|
+
{ encoding: "utf8", timeout: 3e5 }
|
|
6288
|
+
);
|
|
6289
|
+
const parsed = JSON.parse(result.trim());
|
|
6290
|
+
console.log(`[Codespace] Created: ${parsed.name}`);
|
|
6291
|
+
return parsed.name;
|
|
6292
|
+
} catch (err) {
|
|
6293
|
+
throw new Error(`Failed to create codespace: ${err instanceof Error ? err.message : String(err)}`);
|
|
6294
|
+
}
|
|
6295
|
+
}
|
|
6296
|
+
/** Find the 0agent-browser codespace by display name. */
|
|
6297
|
+
findExisting() {
|
|
6298
|
+
try {
|
|
6299
|
+
const out = execSync4("gh codespace list --json name,state,displayName,repository", {
|
|
6300
|
+
encoding: "utf8",
|
|
6301
|
+
timeout: 1e4
|
|
6302
|
+
});
|
|
6303
|
+
const list = JSON.parse(out.trim());
|
|
6304
|
+
return list.find((c) => c.displayName === DISPLAY_NAME) ?? null;
|
|
6305
|
+
} catch {
|
|
6306
|
+
return null;
|
|
6307
|
+
}
|
|
6308
|
+
}
|
|
6309
|
+
/** Ensure the codespace is in Available state. */
|
|
6310
|
+
async ensureRunning() {
|
|
6311
|
+
const name = await this.getOrCreate();
|
|
6312
|
+
const info = this.findExisting();
|
|
6313
|
+
if (info?.state === "Shutdown") {
|
|
6314
|
+
console.log("[Codespace] Starting stopped codespace (~30s)...");
|
|
6315
|
+
execSync4(`gh codespace start --codespace "${name}"`, { timeout: 12e4 });
|
|
6316
|
+
await this.waitForState(name, "Available", 60);
|
|
6317
|
+
console.log("[Codespace] Codespace is running");
|
|
6318
|
+
} else if (info?.state === "Starting") {
|
|
6319
|
+
console.log("[Codespace] Codespace is starting...");
|
|
6320
|
+
await this.waitForState(name, "Available", 120);
|
|
6321
|
+
}
|
|
6322
|
+
return name;
|
|
6323
|
+
}
|
|
6324
|
+
/** Start the browser server inside the codespace (idempotent). */
|
|
6325
|
+
async startBrowserServer(name) {
|
|
6326
|
+
try {
|
|
6327
|
+
execSync4(
|
|
6328
|
+
`gh codespace exec --codespace "${name}" -- bash -c "pgrep -f 'node server.js' > /dev/null 2>&1 || (cd /workspaces && nohup node server.js > /tmp/browser-server.log 2>&1 &)"`,
|
|
6329
|
+
{ timeout: 3e4 }
|
|
6330
|
+
);
|
|
6331
|
+
} catch {
|
|
6332
|
+
}
|
|
6333
|
+
}
|
|
6334
|
+
/** Open an SSH tunnel via gh CLI: codespace:3000 → localhost:3001. */
|
|
6335
|
+
async openTunnel(name) {
|
|
6336
|
+
this.closeTunnel();
|
|
6337
|
+
console.log(`[Codespace] Opening tunnel port ${BROWSER_PORT_REMOTE} \u2192 localhost:${BROWSER_PORT_LOCAL}...`);
|
|
6338
|
+
this.forwardProcess = spawn3(
|
|
6339
|
+
"gh",
|
|
6340
|
+
["codespace", "ports", "forward", `${BROWSER_PORT_REMOTE}:${BROWSER_PORT_LOCAL}`, "--codespace", name],
|
|
6341
|
+
{ stdio: ["ignore", "ignore", "ignore"] }
|
|
6342
|
+
);
|
|
6343
|
+
this.forwardProcess.unref();
|
|
6344
|
+
this.forwardProcess.on("close", (code) => {
|
|
6345
|
+
if (this._ready) {
|
|
6346
|
+
console.log("[Codespace] Tunnel closed \u2014 reconnecting...");
|
|
6347
|
+
this._ready = false;
|
|
6348
|
+
this.openTunnel(name).catch(() => {
|
|
6349
|
+
});
|
|
6350
|
+
}
|
|
6351
|
+
});
|
|
6352
|
+
const deadline = Date.now() + FORWARD_TIMEOUT_S * 1e3;
|
|
6353
|
+
while (Date.now() < deadline) {
|
|
6354
|
+
await new Promise((r) => setTimeout(r, 1e3));
|
|
6355
|
+
try {
|
|
6356
|
+
const res = await fetch(`${this._localUrl}/health`, { signal: AbortSignal.timeout(2e3) });
|
|
6357
|
+
if (res.ok) {
|
|
6358
|
+
this._ready = true;
|
|
6359
|
+
console.log("[Codespace] Browser server ready at " + this._localUrl);
|
|
6360
|
+
return;
|
|
6361
|
+
}
|
|
6362
|
+
} catch {
|
|
6363
|
+
}
|
|
6364
|
+
}
|
|
6365
|
+
throw new Error(`Browser server did not respond within ${FORWARD_TIMEOUT_S}s`);
|
|
6366
|
+
}
|
|
6367
|
+
closeTunnel() {
|
|
6368
|
+
if (this.forwardProcess) {
|
|
6369
|
+
try {
|
|
6370
|
+
this.forwardProcess.kill("SIGTERM");
|
|
6371
|
+
} catch {
|
|
6372
|
+
}
|
|
6373
|
+
this.forwardProcess = null;
|
|
6374
|
+
}
|
|
6375
|
+
this._ready = false;
|
|
6376
|
+
}
|
|
6377
|
+
/** Stop the codespace to save free-tier hours. State is preserved. */
|
|
6378
|
+
async stop() {
|
|
6379
|
+
this.closeTunnel();
|
|
6380
|
+
const info = this.findExisting();
|
|
6381
|
+
if (info?.state === "Available") {
|
|
6382
|
+
execSync4(`gh codespace stop --codespace "${info.name}"`, { timeout: 3e4 });
|
|
6383
|
+
console.log("[Codespace] Stopped (state preserved, restarts in 30s when needed)");
|
|
6384
|
+
}
|
|
6385
|
+
}
|
|
6386
|
+
/** Delete the codespace entirely. */
|
|
6387
|
+
async delete() {
|
|
6388
|
+
this.closeTunnel();
|
|
6389
|
+
const info = this.findExisting();
|
|
6390
|
+
if (info) {
|
|
6391
|
+
execSync4(`gh codespace delete --codespace "${info.name}" --force`, { timeout: 3e4 });
|
|
6392
|
+
console.log("[Codespace] Deleted");
|
|
6393
|
+
}
|
|
6394
|
+
}
|
|
6395
|
+
// ─── Health checking ───────────────────────────────────────────────────────
|
|
6396
|
+
/** Ping the browser server. Returns null if not reachable. */
|
|
6397
|
+
async ping() {
|
|
6398
|
+
try {
|
|
6399
|
+
const res = await fetch(`${this._localUrl}/health`, { signal: AbortSignal.timeout(3e3) });
|
|
6400
|
+
return res.ok ? await res.json() : null;
|
|
6401
|
+
} catch {
|
|
6402
|
+
return null;
|
|
6403
|
+
}
|
|
6404
|
+
}
|
|
6405
|
+
// ─── Helpers ───────────────────────────────────────────────────────────────
|
|
6406
|
+
async waitForState(name, target, maxSeconds) {
|
|
6407
|
+
for (let i = 0; i < maxSeconds / 2; i++) {
|
|
6408
|
+
await new Promise((r) => setTimeout(r, 2e3));
|
|
6409
|
+
const info = this.findExisting();
|
|
6410
|
+
if (info?.state === target) return;
|
|
6411
|
+
}
|
|
6412
|
+
throw new Error(`Codespace did not reach ${target} state within ${maxSeconds}s`);
|
|
6413
|
+
}
|
|
6414
|
+
/** Check if gh CLI is installed and authenticated. */
|
|
6415
|
+
static isAvailable() {
|
|
6416
|
+
try {
|
|
6417
|
+
execSync4("gh auth status", { stdio: "ignore", timeout: 5e3 });
|
|
6418
|
+
return true;
|
|
6419
|
+
} catch {
|
|
6420
|
+
return false;
|
|
6421
|
+
}
|
|
6422
|
+
}
|
|
6423
|
+
};
|
|
6424
|
+
|
|
5666
6425
|
// packages/daemon/src/ZeroAgentDaemon.ts
|
|
5667
6426
|
var ZeroAgentDaemon = class {
|
|
5668
6427
|
config = null;
|
|
@@ -5678,6 +6437,8 @@ var ZeroAgentDaemon = class {
|
|
|
5678
6437
|
githubMemorySync = null;
|
|
5679
6438
|
memorySyncTimer = null;
|
|
5680
6439
|
proactiveSurfaceInstance = null;
|
|
6440
|
+
codespaceManager = null;
|
|
6441
|
+
schedulerManager = null;
|
|
5681
6442
|
startedAt = 0;
|
|
5682
6443
|
pidFilePath;
|
|
5683
6444
|
constructor() {
|
|
@@ -5718,6 +6479,13 @@ var ZeroAgentDaemon = class {
|
|
|
5718
6479
|
this.graph
|
|
5719
6480
|
);
|
|
5720
6481
|
console.log(`[0agent] Memory sync: github.com/${ghMemCfg.owner}/${ghMemCfg.repo}`);
|
|
6482
|
+
if (CodespaceManager.isAvailable()) {
|
|
6483
|
+
const memRepo = `${ghMemCfg.owner}/${ghMemCfg.repo}`;
|
|
6484
|
+
this.codespaceManager = new CodespaceManager(memRepo);
|
|
6485
|
+
this.codespaceManager.getReadyUrl().catch(() => {
|
|
6486
|
+
});
|
|
6487
|
+
console.log(`[0agent] Browser backend: github.com codespace (from ${memRepo})`);
|
|
6488
|
+
}
|
|
5721
6489
|
this.githubMemorySync.pull().then((r) => {
|
|
5722
6490
|
if (r.pulled) console.log(`[0agent] Memory pulled: +${r.nodes_synced} nodes, +${r.edges_synced} edges`);
|
|
5723
6491
|
}).catch(() => {
|
|
@@ -5770,6 +6538,8 @@ var ZeroAgentDaemon = class {
|
|
|
5770
6538
|
proactiveSurface = new ProactiveSurface2(this.graph, this.eventBus, cwd);
|
|
5771
6539
|
} catch {
|
|
5772
6540
|
}
|
|
6541
|
+
this.schedulerManager = new SchedulerManager(this.adapter, this.sessionManager, this.eventBus);
|
|
6542
|
+
this.schedulerManager.start();
|
|
5773
6543
|
this.backgroundWorkers = new BackgroundWorkers({
|
|
5774
6544
|
graph: this.graph,
|
|
5775
6545
|
traceStore: this.traceStore,
|
|
@@ -5792,7 +6562,18 @@ var ZeroAgentDaemon = class {
|
|
|
5792
6562
|
skillRegistry: this.skillRegistry,
|
|
5793
6563
|
getStatus: () => this.getStatus(),
|
|
5794
6564
|
getMemorySync: () => memSyncRef,
|
|
5795
|
-
proactiveSurface
|
|
6565
|
+
proactiveSurface,
|
|
6566
|
+
getCodespaceManager: () => this.codespaceManager,
|
|
6567
|
+
scheduler: this.schedulerManager,
|
|
6568
|
+
setupCodespace: async () => {
|
|
6569
|
+
if (!this.codespaceManager) return { started: false, error: "GitHub memory not configured. Run: 0agent memory connect github" };
|
|
6570
|
+
try {
|
|
6571
|
+
this.codespaceManager.getReadyUrl().catch(console.error);
|
|
6572
|
+
return { started: true };
|
|
6573
|
+
} catch (err) {
|
|
6574
|
+
return { started: false, error: err instanceof Error ? err.message : String(err) };
|
|
6575
|
+
}
|
|
6576
|
+
}
|
|
5796
6577
|
});
|
|
5797
6578
|
await this.httpServer.start();
|
|
5798
6579
|
writeFileSync7(this.pidFilePath, String(process.pid), "utf8");
|
|
@@ -5829,6 +6610,10 @@ var ZeroAgentDaemon = class {
|
|
|
5829
6610
|
this.memorySyncTimer = null;
|
|
5830
6611
|
}
|
|
5831
6612
|
this.githubMemorySync = null;
|
|
6613
|
+
this.schedulerManager?.stop();
|
|
6614
|
+
this.schedulerManager = null;
|
|
6615
|
+
this.codespaceManager?.closeTunnel();
|
|
6616
|
+
this.codespaceManager = null;
|
|
5832
6617
|
this.sessionManager = null;
|
|
5833
6618
|
this.skillRegistry = null;
|
|
5834
6619
|
this.inferenceEngine = null;
|