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 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
- constructor() {
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
- this.register(new BrowserCapability());
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 execSync4 } from "node:child_process";
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 = execSync4(
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 execSync4("git rev-parse HEAD", { cwd: this.cwd, timeout: 1e3, encoding: "utf8" }).trim();
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 Hono11 } from "hono";
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 Hono11();
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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.27",
3
+ "version": "1.0.29",
4
4
  "description": "A persistent, learning AI agent that runs on your machine. An agent that learns.",
5
5
  "private": false,
6
6
  "license": "Apache-2.0",