@codizelabs/agent-rewake 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,9 +2,9 @@
2
2
 
3
3
  // src/cli.ts
4
4
  import { spawnSync as spawnSync10 } from "node:child_process";
5
- import { existsSync as existsSync21, readdirSync as readdirSync12, readFileSync as readFileSync27 } from "node:fs";
6
- import { homedir as homedir17 } from "node:os";
7
- import { join as join32 } from "node:path";
5
+ import { existsSync as existsSync23, readdirSync as readdirSync14, readFileSync as readFileSync32 } from "node:fs";
6
+ import { homedir as homedir19 } from "node:os";
7
+ import { join as join36 } from "node:path";
8
8
  import { createInterface as createInterface6 } from "node:readline/promises";
9
9
 
10
10
  // src/adapters/claude/spawn.ts
@@ -32,7 +32,7 @@ function claudeAdapterCommand(extraArgs, env) {
32
32
  // src/addon.ts
33
33
  import { randomUUID as randomUUID3 } from "node:crypto";
34
34
  import { watch, writeFileSync as writeFileSync3 } from "node:fs";
35
- import { join as join26 } from "node:path";
35
+ import { join as join27 } from "node:path";
36
36
  import { pathToFileURL } from "node:url";
37
37
 
38
38
  // src/acp/ndjson.ts
@@ -1394,7 +1394,7 @@ function formatWhen(at, now, locale) {
1394
1394
  const dayDiff = Math.round((startOfDay(at) - startOfDay(now)) / 864e5);
1395
1395
  const weekday = new Intl.DateTimeFormat(locale ?? TEXT_LOCALE, { weekday: "long" }).format(at);
1396
1396
  if (dayDiff === 0) return `${time} today`;
1397
- if (dayDiff === 1) return `${time} tomorrow (${weekday})`;
1397
+ if (dayDiff === 1) return `${time} tomorrow, ${weekday}`;
1398
1398
  if (dayDiff > 1 && dayDiff < 7) return `${weekday} at ${time}`;
1399
1399
  const date = new Intl.DateTimeFormat(locale ?? TEXT_LOCALE, {
1400
1400
  day: "numeric",
@@ -2566,16 +2566,16 @@ import {
2566
2566
  chmodSync as chmodSync2,
2567
2567
  closeSync as closeSync7,
2568
2568
  copyFileSync,
2569
- existsSync as existsSync16,
2569
+ existsSync as existsSync17,
2570
2570
  fsyncSync as fsyncSync3,
2571
2571
  openSync as openSync8,
2572
- readFileSync as readFileSync21,
2572
+ readFileSync as readFileSync22,
2573
2573
  realpathSync as realpathSync5,
2574
2574
  statSync as statSync4,
2575
2575
  writeSync as writeSync3
2576
2576
  } from "node:fs";
2577
- import { homedir as homedir12, platform as platform3 } from "node:os";
2578
- import { basename as basename4, delimiter as delimiter2, dirname as dirname5, join as join24, posix as posix3, win32 as win325 } from "node:path";
2577
+ import { homedir as homedir14, platform as platform3 } from "node:os";
2578
+ import { basename as basename4, delimiter as delimiter2, dirname as dirname5, join as join25, posix as posix3, win32 as win325 } from "node:path";
2579
2579
  import { createInterface as createInterface3 } from "node:readline/promises";
2580
2580
 
2581
2581
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
@@ -3988,11 +3988,11 @@ function applyEdits(text2, edits) {
3988
3988
  }
3989
3989
 
3990
3990
  // src/hosts/previews.ts
3991
- import { existsSync as existsSync15 } from "node:fs";
3991
+ import { existsSync as existsSync16 } from "node:fs";
3992
3992
 
3993
3993
  // src/hosts/antigravity/install.ts
3994
3994
  import { existsSync as existsSync5, rmSync as rmSync5 } from "node:fs";
3995
- import { homedir as homedir4 } from "node:os";
3995
+ import { homedir as homedir5 } from "node:os";
3996
3996
  import { join as join13 } from "node:path";
3997
3997
 
3998
3998
  // src/install/detect.ts
@@ -4409,7 +4409,7 @@ var pkg = (() => {
4409
4409
  return {};
4410
4410
  }
4411
4411
  })();
4412
- var VERSION2 = pkg.version ?? (true ? "0.2.0" : "0.0.0");
4412
+ var VERSION2 = pkg.version ?? (true ? "0.3.0" : "0.0.0");
4413
4413
  var PACKAGE_NAME = pkg.name ?? "@codizelabs/agent-rewake";
4414
4414
 
4415
4415
  // src/timers/launcher.ts
@@ -4463,7 +4463,7 @@ function refreshLauncher(stateDir2, bundle, version) {
4463
4463
  // src/hosts/antigravity/host.ts
4464
4464
  import { spawn as spawn2, spawnSync as spawnSync3 } from "node:child_process";
4465
4465
  import { readFileSync as readFileSync13, writeFileSync } from "node:fs";
4466
- import { homedir as homedir3 } from "node:os";
4466
+ import { homedir as homedir4 } from "node:os";
4467
4467
  import { join as join12, resolve, sep } from "node:path";
4468
4468
 
4469
4469
  // src/core/limits/agents.ts
@@ -4579,6 +4579,15 @@ function classifyGrokFailure(input, billing) {
4579
4579
  return { kind: "weekly", billing: false };
4580
4580
  return { kind: "billing", billing: true };
4581
4581
  }
4582
+ function classifyCursorError(text2) {
4583
+ if (typeof text2 !== "string" || text2 === "") return void 0;
4584
+ const t = text2.slice(0, 4096);
4585
+ if (/add a payment method|spend(ing)? limit|out of credits|on-demand usage|get cursor pro/i.test(t))
4586
+ return { kind: "billing", billing: true };
4587
+ if (/hit your (usage|rate) limit|usage limit|rate limit|upgrade your plan to continue/i.test(t))
4588
+ return { kind: "other", billing: false };
4589
+ return void 0;
4590
+ }
4582
4591
 
4583
4592
  // src/core/limits/recognise.ts
4584
4593
  function recognise(signal, now) {
@@ -4606,6 +4615,8 @@ function hostLimit(s, now) {
4606
4615
  return classifyGeminiError(text2, now);
4607
4616
  case "antigravity":
4608
4617
  return classifyAntigravityStop({ terminationReason: s.code, error: text2 }, now);
4618
+ case "cursor":
4619
+ return classifyCursorError(text2);
4609
4620
  case "grok":
4610
4621
  return classifyGrokFailure(
4611
4622
  { error: s.code, errorDetails: text2 },
@@ -4748,6 +4759,20 @@ var SESSION_GONE = /\bsession\b(?:\s+\S+)?\s+not found|unknown session|no such s
4748
4759
 
4749
4760
  // src/hosts/closed.ts
4750
4761
  var FIRE_ENV = "AGENT_REWAKE_FIRE";
4762
+ var COMMON_VARS = ["XDG_CONFIG_HOME", "XDG_DATA_HOME", "XDG_STATE_HOME", "XDG_CACHE_HOME"];
4763
+ var SECRET_NAME = /KEY|TOKEN|SECRET|PASSWORD|CREDENTIAL/i;
4764
+ function sessionEnv(host, env) {
4765
+ const vars = {};
4766
+ for (const name of [...COMMON_VARS, ...host.settingsVars ?? []]) {
4767
+ const v = env[name];
4768
+ if (v !== void 0 && v !== "" && !SECRET_NAME.test(name)) vars[name] = v;
4769
+ }
4770
+ const keys = (host.keyVars ?? []).filter((k) => env[k] !== void 0 && env[k] !== "");
4771
+ return {
4772
+ ...Object.keys(vars).length > 0 && { env: vars },
4773
+ ...keys.length > 0 && { keysSet: keys }
4774
+ };
4775
+ }
4751
4776
  function agentsOf(r) {
4752
4777
  if (r.agents) return r.agents;
4753
4778
  return r.agentPid !== void 0 && r.agentName ? [{ pid: r.agentPid, name: r.agentName }] : [];
@@ -4770,13 +4795,19 @@ function unanswered(stateDir2, r) {
4770
4795
  if (!l || l.billing) return void 0;
4771
4796
  if ((r.lastPromptAt ?? 0) > l.seenAt) return void 0;
4772
4797
  if (pendingFor(stateDir2, r.host, r.sessionId).length > 0) return void 0;
4798
+ if (resumedAfter(stateDir2, r, l.seenAt)) return void 0;
4773
4799
  return l;
4774
4800
  }
4801
+ function resumedAfter(stateDir2, r, since) {
4802
+ return new ScheduleStore(stateDir2).listForSession(r.sessionId, r.host).some((s) => s.status === "sent" && s.kind !== "user" && s.updatedAt >= since);
4803
+ }
4775
4804
  function autoFor(settings) {
4776
4805
  return settings.newThreads === "on" ? "always" : settings.newThreads === "off" ? "never" : "ask";
4777
4806
  }
4778
4807
  function armedText(host, cwd, at, now) {
4779
- return `Rewake will continue ${placeOf(host, cwd)} ${formatAt(at, now)}. Keep this computer on and awake until then. Reopening that session and typing in it before then cancels this resume. To cancel all planned resumes: agent-rewake continue --cancel`;
4808
+ const keep = host.keepOpen ? ` ${host.keepOpen}` : "";
4809
+ const typing = host.keepOpen ? `Typing in the ${host.noun ?? "session"} before then cancels this continue.` : `Reopening that ${host.noun ?? "session"} and typing in it before then cancels this resume.`;
4810
+ return `Rewake will continue ${placeOf(host, cwd)} ${formatAt(at, now)}. Keep this computer on and awake until then.${keep} ${typing} To cancel all planned resumes: agent-rewake continue --cancel`;
4780
4811
  }
4781
4812
  function armClosed(host, r, at, d) {
4782
4813
  const store = new ScheduleStore(d.stateDir);
@@ -4796,7 +4827,7 @@ function armClosed(host, r, at, d) {
4796
4827
  sessionRef: { sessionId: r.sessionId, cwd: r.cwd }
4797
4828
  };
4798
4829
  store.put(resume);
4799
- d.arm(resume.scheduleId, at);
4830
+ d.arm(resume.scheduleId, at + (host.fallbackDelayMs ?? 0));
4800
4831
  return resume;
4801
4832
  }
4802
4833
  function onSessionStart(host, sessionId, cwd, d, program) {
@@ -4818,10 +4849,18 @@ function onSessionStart(host, sessionId, cwd, d, program) {
4818
4849
  }
4819
4850
  function ensureProgram(host, sessionId, cwd, d, program) {
4820
4851
  const records = new SessionRecords(d.stateDir, host.id);
4821
- if (records.get(sessionId)?.program) return;
4822
- const found = program();
4823
- if (found)
4824
- records.update(sessionId, cwd, d.now, (r) => r.program ? r : { ...r, program: found });
4852
+ const had = records.get(sessionId);
4853
+ const seen = d.env[FIRE_ENV] ? {} : sessionEnv(host, d.env);
4854
+ const same2 = JSON.stringify(had?.env ?? null) === JSON.stringify(seen.env ?? null) && JSON.stringify(had?.keysSet ?? null) === JSON.stringify(seen.keysSet ?? null);
4855
+ if (had?.program && (same2 || d.env[FIRE_ENV])) return;
4856
+ const found = had?.program ? void 0 : program();
4857
+ records.update(sessionId, cwd, d.now, (r) => {
4858
+ const { env: _e, keysSet: _k, ...rest } = r;
4859
+ return {
4860
+ ...d.env[FIRE_ENV] ? r : { ...rest, ...seen },
4861
+ ...!r.program && found && { program: found }
4862
+ };
4863
+ });
4825
4864
  }
4826
4865
  function onPrompt(host, sessionId, cwd, d) {
4827
4866
  if (d.env[FIRE_ENV]) return;
@@ -4896,7 +4935,7 @@ function closedAdapter(host, stateDir2, env) {
4896
4935
  return {
4897
4936
  id: host.id,
4898
4937
  name: host.name,
4899
- noun: "session",
4938
+ noun: host.noun ?? "session",
4900
4939
  reopen: host.reopen ?? `open the session in ${host.name}`,
4901
4940
  async check(s) {
4902
4941
  const r = records.get(s.sessionRef?.sessionId ?? s.sessionId);
@@ -4909,7 +4948,9 @@ function closedAdapter(host, stateDir2, env) {
4909
4948
  async send(s) {
4910
4949
  const r = records.get(s.sessionRef?.sessionId ?? s.sessionId);
4911
4950
  if (!r) return { ok: false, reason: "closed", detail: "deleted" };
4912
- return host.resume(r, s.text, { ...env, [FIRE_ENV]: s.scheduleId });
4951
+ const missing = (r.keysSet ?? []).filter((k) => !env[k]);
4952
+ if (missing.length > 0) return { ok: false, reason: "failed", detail: "missing-key" };
4953
+ return host.resume(r, s.text, { ...env, ...r.env, [FIRE_ENV]: s.scheduleId });
4913
4954
  }
4914
4955
  };
4915
4956
  }
@@ -5106,7 +5147,23 @@ function listHooks(codex2, env, cwd, timeoutMs = 2e4) {
5106
5147
  });
5107
5148
  }
5108
5149
 
5150
+ // src/util/error-line.ts
5151
+ import { homedir as homedir3 } from "node:os";
5152
+ var ANSI = new RegExp(`${String.fromCharCode(27)}\\[[0-9;?]*[A-Za-z]`, "g");
5153
+ function errorLine(output, home = homedir3()) {
5154
+ const lines = output.replace(ANSI, "").split(/\r?\n/).map((l) => l.trim()).filter((l) => l !== "" && !/^at\s|^Node\.js v\d|^\^+$/.test(l));
5155
+ let line = lines.at(-1);
5156
+ if (line === void 0) return void 0;
5157
+ if (home) line = line.split(home).join("~");
5158
+ line = line.replace(/\b(sk|ghp|gho|ghs|xai|AIza)[-_A-Za-z0-9]{8,}/g, "\u2026").replace(/\b(Bearer|token|key)[=: ]+\S+/gi, "$1 \u2026").replace(/[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}/g, "\u2026").replace(/[A-Za-z0-9_-]{32,}/g, "\u2026");
5159
+ return line.length > 200 ? `${line.slice(0, 199)}\u2026` : line;
5160
+ }
5161
+
5109
5162
  // src/hosts/host.ts
5163
+ function withMessage(output) {
5164
+ const message = errorLine(output);
5165
+ return message === void 0 ? {} : { message };
5166
+ }
5110
5167
  var RESUME_TIMEOUT_MS = 30 * 6e4;
5111
5168
  function resumeDeadline(child, ms = RESUME_TIMEOUT_MS) {
5112
5169
  let stopped = false;
@@ -5121,7 +5178,7 @@ function resumeDeadline(child, ms = RESUME_TIMEOUT_MS) {
5121
5178
 
5122
5179
  // src/hosts/antigravity/host.ts
5123
5180
  var ANTIGRAVITY_ID = "antigravity";
5124
- function geminiHome(env, home = homedir3()) {
5181
+ function geminiHome(env, home = homedir4()) {
5125
5182
  return env.GEMINI_HOME || join12(home, ".gemini");
5126
5183
  }
5127
5184
  function surfaceOf(transcriptPath, env, home) {
@@ -5279,7 +5336,7 @@ function antigravityHooksJson(node, launcher) {
5279
5336
  `;
5280
5337
  }
5281
5338
  async function runAntigravityInstall(o) {
5282
- const home = o.env.HOME || o.env.USERPROFILE || homedir4();
5339
+ const home = o.env.HOME || o.env.USERPROFILE || homedir5();
5283
5340
  const dir = pluginDir(o.env, home);
5284
5341
  const installed = existsSync5(dir);
5285
5342
  if (o.uninstall) {
@@ -5365,7 +5422,7 @@ import {
5365
5422
  realpathSync as realpathSync4,
5366
5423
  rmSync as rmSync6
5367
5424
  } from "node:fs";
5368
- import { homedir as homedir5, tmpdir } from "node:os";
5425
+ import { homedir as homedir6, tmpdir } from "node:os";
5369
5426
  import { dirname as dirname4, join as join14 } from "node:path";
5370
5427
 
5371
5428
  // src/install/probe.ts
@@ -5551,7 +5608,7 @@ function pickClaude(programs2) {
5551
5608
  return [...programs2].sort((a, b) => compareVersions(b.version ?? "0", a.version ?? "0"))[0];
5552
5609
  }
5553
5610
  async function runClaudeInstall(o) {
5554
- const home = o.env.HOME || o.env.USERPROFILE || homedir5();
5611
+ const home = o.env.HOME || o.env.USERPROFILE || homedir6();
5555
5612
  const host = { env: o.env, home, platform: process.platform };
5556
5613
  const programs2 = o.programs ?? [...claudePrograms(host), ...claudeDesktopPrograms(host)];
5557
5614
  const probe = o.probe ?? (o.programs ? () => void 0 : versionProbe(o.env, o.node));
@@ -5799,7 +5856,7 @@ async function uninstallPlugin(codex2, stateDir2, env) {
5799
5856
 
5800
5857
  // src/hosts/copilot/install.ts
5801
5858
  import { existsSync as existsSync10, rmSync as rmSync9 } from "node:fs";
5802
- import { homedir as homedir8 } from "node:os";
5859
+ import { homedir as homedir9 } from "node:os";
5803
5860
  import { join as join19 } from "node:path";
5804
5861
 
5805
5862
  // src/hosts/policy.ts
@@ -5809,7 +5866,7 @@ import { join as join18 } from "node:path";
5809
5866
  // src/hosts/gemini/install.ts
5810
5867
  import { spawnSync as spawnSync5 } from "node:child_process";
5811
5868
  import { existsSync as existsSync8, readFileSync as readFileSync16, rmSync as rmSync8 } from "node:fs";
5812
- import { homedir as homedir6 } from "node:os";
5869
+ import { homedir as homedir7 } from "node:os";
5813
5870
  import { join as join16 } from "node:path";
5814
5871
 
5815
5872
  // src/hosts/gemini/host.ts
@@ -6007,14 +6064,26 @@ function resumeGemini(r, text2, env, node = process.execPath) {
6007
6064
  return resolve2({ ok: false, reason: "limited", ...resetsAt && { resetsAt } });
6008
6065
  }
6009
6066
  if (code === 0) return resolve2({ ok: true });
6010
- resolve2({ ok: false, reason: "failed", detail: `exit ${code ?? "signal"}` });
6067
+ resolve2({
6068
+ ok: false,
6069
+ reason: "failed",
6070
+ detail: `exit ${code ?? "signal"}`,
6071
+ ...withMessage(err || out)
6072
+ });
6011
6073
  });
6012
6074
  });
6013
6075
  }
6014
6076
  var geminiHost = {
6015
6077
  id: GEMINI_ID,
6016
6078
  name: "Gemini CLI",
6017
- resume: (r, text2, env) => resumeGemini(r, text2, env)
6079
+ resume: (r, text2, env) => resumeGemini(r, text2, env),
6080
+ settingsVars: [
6081
+ "GEMINI_CLI_HOME",
6082
+ "GOOGLE_GEMINI_BASE_URL",
6083
+ "GOOGLE_GENAI_USE_VERTEXAI",
6084
+ "GOOGLE_GENAI_USE_GCA"
6085
+ ],
6086
+ keyVars: ["GEMINI_API_KEY"]
6018
6087
  };
6019
6088
  var isGeminiTranscript = (p) => typeof p === "string" && /[\\/]\.gemini[\\/]tmp[\\/]/.test(p);
6020
6089
  function sessionOf(input) {
@@ -6137,6 +6206,12 @@ function geminiApiKeyAuth(env, home) {
6137
6206
  return (s?.security?.auth?.selectedType ?? s?.selectedAuthType) === "gemini-api-key";
6138
6207
  }
6139
6208
  function geminiHooksJson(node, launcher) {
6209
+ const NAMES = {
6210
+ SessionStart: "Agent Rewake (session start)",
6211
+ SessionEnd: "Agent Rewake (session end)",
6212
+ BeforeAgent: "Agent Rewake (/rewake)",
6213
+ AfterAgent: "Agent Rewake"
6214
+ };
6140
6215
  const hooks = Object.fromEntries(
6141
6216
  GEMINI_EVENTS.map((event) => [
6142
6217
  event,
@@ -6145,7 +6220,7 @@ function geminiHooksJson(node, launcher) {
6145
6220
  hooks: [
6146
6221
  {
6147
6222
  type: "command",
6148
- name: `agent-rewake-${event.replace(/[A-Z]/g, (c, i) => (i ? "-" : "") + c.toLowerCase())}`,
6223
+ name: NAMES[event] ?? "Agent Rewake",
6149
6224
  command: `"${node}" "${launcher}" hook gemini-cli ${event}`,
6150
6225
  timeout: 5e3
6151
6226
  }
@@ -6180,7 +6255,7 @@ prompt = "${REWAKE_MARKER} {{args}}"
6180
6255
  return dir;
6181
6256
  }
6182
6257
  async function runGeminiInstall(o) {
6183
- const home = o.env.HOME || o.env.USERPROFILE || homedir6();
6258
+ const home = o.env.HOME || o.env.USERPROFILE || homedir7();
6184
6259
  const programs2 = o.programs ?? geminiPrograms({ env: o.env, home, platform: process.platform });
6185
6260
  const probe = o.probe ?? (o.programs ? () => void 0 : versionProbe(o.env, o.node));
6186
6261
  const picked = [...programs2].sort(
@@ -6290,10 +6365,10 @@ async function runGeminiInstall(o) {
6290
6365
  // src/hosts/grok/host.ts
6291
6366
  import { spawn as spawn4 } from "node:child_process";
6292
6367
  import { existsSync as existsSync9, readFileSync as readFileSync17 } from "node:fs";
6293
- import { homedir as homedir7 } from "node:os";
6368
+ import { homedir as homedir8 } from "node:os";
6294
6369
  import { join as join17 } from "node:path";
6295
6370
  var GROK_ID = "grok";
6296
- function grokHome(env, home = homedir7()) {
6371
+ function grokHome(env, home = homedir8()) {
6297
6372
  return env.GROK_HOME || join17(home, ".grok");
6298
6373
  }
6299
6374
  function isGrok(input, env) {
@@ -6417,7 +6492,12 @@ ${err}`)) {
6417
6492
  if (SESSION_GONE.test(`${out}
6418
6493
  ${err}`))
6419
6494
  return resolve2({ ok: false, reason: "closed", detail: "deleted" });
6420
- resolve2({ ok: false, reason: "failed", detail: `exit ${code ?? "signal"}` });
6495
+ resolve2({
6496
+ ok: false,
6497
+ reason: "failed",
6498
+ detail: `exit ${code ?? "signal"}`,
6499
+ ...withMessage(err || out)
6500
+ });
6421
6501
  });
6422
6502
  });
6423
6503
  }
@@ -6426,7 +6506,14 @@ function grokHost(env) {
6426
6506
  id: GROK_ID,
6427
6507
  name: "Grok Build",
6428
6508
  resume: (r, text2, e) => resumeGrok(r, text2, e),
6429
- isOpen: (r) => grokSessionOpen(grokHome(env), r.sessionId)
6509
+ isOpen: (r) => grokSessionOpen(grokHome({ ...env, ...r.env }), r.sessionId),
6510
+ settingsVars: [
6511
+ "GROK_HOME",
6512
+ "GROK_CLI_CHAT_PROXY_BASE_URL",
6513
+ "GROK_XAI_API_BASE_URL",
6514
+ "GROK_MODELS_BASE_URL"
6515
+ ],
6516
+ keyVars: ["XAI_API_KEY"]
6430
6517
  };
6431
6518
  }
6432
6519
  function grokHooks(deps) {
@@ -6531,7 +6618,7 @@ function copilotHooksJson(node, launcher) {
6531
6618
  `;
6532
6619
  }
6533
6620
  async function runCopilotInstall(o) {
6534
- const home = o.env.HOME || o.env.USERPROFILE || homedir8();
6621
+ const home = o.env.HOME || o.env.USERPROFILE || homedir9();
6535
6622
  const programs2 = o.programs ?? copilotPrograms({ env: o.env, home, platform: process.platform });
6536
6623
  const probe = o.probe ?? (o.programs ? () => void 0 : versionProbe(o.env, o.node));
6537
6624
  const picked = [...programs2].sort(
@@ -6619,17 +6706,143 @@ async function runCopilotInstall(o) {
6619
6706
  return 0;
6620
6707
  }
6621
6708
 
6709
+ // src/hosts/cursor/install.ts
6710
+ import { existsSync as existsSync11, readFileSync as readFileSync19 } from "node:fs";
6711
+ import { homedir as homedir10 } from "node:os";
6712
+ import { join as join20 } from "node:path";
6713
+ var WAIT_SECONDS = 4 * 60 * 60;
6714
+ var CURSOR_EVENTS = ["beforeSubmitPrompt", "stop"];
6715
+ function cursorHooksFile(home) {
6716
+ return join20(home, ".cursor", "hooks.json");
6717
+ }
6718
+ function cursorFound(env, home, platform4) {
6719
+ const apps2 = platform4 === "darwin" ? ["/Applications/Cursor.app", join20(home, "Applications", "Cursor.app")] : platform4 === "win32" ? env.LOCALAPPDATA ? [join20(env.LOCALAPPDATA, "Programs", "cursor", "Cursor.exe")] : [] : ["/usr/share/cursor", "/opt/Cursor", "/opt/cursor"];
6720
+ if (apps2.some((a) => existsSync11(a))) return true;
6721
+ const sep2 = platform4 === "win32" ? ";" : ":";
6722
+ const names = platform4 === "win32" ? ["cursor.cmd", "cursor.exe"] : ["cursor"];
6723
+ return (env.PATH ?? "").split(sep2).filter(Boolean).some((d) => names.some((n) => existsSync11(join20(d, n))));
6724
+ }
6725
+ var OURS = /\bhook cursor\b/;
6726
+ function readHooks(file) {
6727
+ try {
6728
+ const v = JSON.parse(readFileSync19(file, "utf8"));
6729
+ return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
6730
+ } catch {
6731
+ return void 0;
6732
+ }
6733
+ }
6734
+ function cursorInstalled(home) {
6735
+ const hooks = readHooks(cursorHooksFile(home))?.hooks;
6736
+ if (typeof hooks !== "object" || hooks === null) return false;
6737
+ return Object.values(hooks).some(
6738
+ (list) => Array.isArray(list) && list.some((e) => OURS.test(String(e?.command ?? "")))
6739
+ );
6740
+ }
6741
+ var quote = (s) => `"${s}"`;
6742
+ function planCursor(file, node, launcher, uninstall) {
6743
+ if (`${node}${launcher}`.includes('"'))
6744
+ return {
6745
+ error: `Rewake couldn't set up Cursor: the path to Node.js or to Rewake contains a double quote (${node}, ${launcher}). Rewake didn't change anything.`
6746
+ };
6747
+ const existed = existsSync11(file);
6748
+ const before = existed ? readFileSync19(file, "utf8") : "";
6749
+ const parsed = existed ? readHooks(file) : { version: 1, hooks: {} };
6750
+ if (!parsed)
6751
+ return {
6752
+ error: `Rewake couldn't set up Cursor: its hooks file (${file}) isn't valid JSON. Rewake didn't change it. Fix or remove the file, then run this again.`
6753
+ };
6754
+ const hooks = typeof parsed.hooks === "object" && parsed.hooks !== null ? { ...parsed.hooks } : {};
6755
+ for (const [event, list] of Object.entries(hooks)) {
6756
+ if (!Array.isArray(list)) continue;
6757
+ const kept = list.filter((e) => !OURS.test(String(e?.command ?? "")));
6758
+ if (kept.length > 0) hooks[event] = kept;
6759
+ else delete hooks[event];
6760
+ }
6761
+ if (!uninstall)
6762
+ for (const event of CURSOR_EVENTS) {
6763
+ const mine = {
6764
+ command: `${quote(node)} ${quote(launcher)} hook cursor ${event}`,
6765
+ timeout: event === "stop" ? WAIT_SECONDS : 10
6766
+ };
6767
+ const list = Array.isArray(hooks[event]) ? hooks[event] : [];
6768
+ hooks[event] = [...list, mine];
6769
+ }
6770
+ const after = `${JSON.stringify({ ...parsed, version: parsed.version ?? 1, hooks }, null, 2)}
6771
+ `;
6772
+ if (after === before) return { changes: [], notes: [] };
6773
+ const summary = [
6774
+ uninstall ? "Remove Rewake's hooks from Cursor's agent" : "Add Rewake's hooks to Cursor's agent"
6775
+ ];
6776
+ const change = { file, existed, before, after, summary };
6777
+ return { changes: [change], notes: [] };
6778
+ }
6779
+ async function runCursorInstall(o) {
6780
+ const home = o.home ?? (o.env.HOME || o.env.USERPROFILE || homedir10());
6781
+ const file = cursorHooksFile(home);
6782
+ if (!o.uninstall && !(o.found ?? cursorFound(o.env, home, process.platform))) {
6783
+ o.out("Cursor wasn't found on this computer, so there's nothing to set up for it.\n");
6784
+ return 1;
6785
+ }
6786
+ const launcher = o.dryRun ? launcherPath(o.stateDir) : ensureLauncher(o.stateDir, o.bundle) ?? launcherPath(o.stateDir);
6787
+ const plan = planCursor(file, o.node, launcher, o.uninstall);
6788
+ if ("error" in plan) {
6789
+ o.out(`${plan.error}
6790
+ `);
6791
+ return 1;
6792
+ }
6793
+ if (plan.changes.length === 0) {
6794
+ o.out(
6795
+ o.uninstall ? "Rewake isn't set up in Cursor: nothing to remove.\n" : "Rewake is already set up in Cursor: nothing to change.\n"
6796
+ );
6797
+ return 0;
6798
+ }
6799
+ o.out(
6800
+ o.uninstall ? `Agent Rewake will remove its hooks from Cursor's agent:
6801
+ ${file}
6802
+ Your own hooks stay. Rewake keeps a backup of the file.
6803
+ ` : [
6804
+ "Agent Rewake (preview) will add its hooks to Cursor's own agent:",
6805
+ ` ${file}`,
6806
+ "",
6807
+ "When a Cursor chat stops at its usage limit, run agent-rewake continue and pick a time within 4 hours. Rewake then continues that chat, as long as its window stays open. Typing in the chat cancels it. A limit that waiting won't lift (one that needs a paid plan or a new month) gets a notification instead. Your own hooks stay; Rewake keeps a backup of the file.",
6808
+ ""
6809
+ ].join("\n")
6810
+ );
6811
+ if (o.dryRun) {
6812
+ o.out("Dry run: nothing was changed.\n");
6813
+ return 0;
6814
+ }
6815
+ if (!o.yes) {
6816
+ if (!o.interactive) {
6817
+ o.out("Not a terminal, so nothing was changed. Run again with --yes to apply.\n");
6818
+ return 1;
6819
+ }
6820
+ if (!await o.ask("\nApply these changes? [y/N] ")) {
6821
+ o.out("Nothing was changed.\n");
6822
+ return 1;
6823
+ }
6824
+ }
6825
+ applyPlan(plan);
6826
+ o.out(
6827
+ o.uninstall ? "Done. Rewake is out of Cursor.\n" : `
6828
+ Done.
6829
+ Next, reload Cursor's window so its agent loads the hooks: in Cursor, run "Developer: Reload Window" from the Command Palette (or restart Cursor).
6830
+ `
6831
+ );
6832
+ return 0;
6833
+ }
6834
+
6622
6835
  // src/hosts/devin/install.ts
6623
- import { existsSync as existsSync12, readFileSync as readFileSync19 } from "node:fs";
6624
- import { homedir as homedir9 } from "node:os";
6625
- import { join as join21 } from "node:path";
6836
+ import { existsSync as existsSync13, readFileSync as readFileSync20 } from "node:fs";
6837
+ import { homedir as homedir11 } from "node:os";
6838
+ import { join as join22 } from "node:path";
6626
6839
 
6627
6840
  // src/wrap.ts
6628
6841
  import { spawnSync as spawnSync6 } from "node:child_process";
6629
6842
  import { createHash as createHash3 } from "node:crypto";
6630
6843
  import {
6631
6844
  chmodSync,
6632
- existsSync as existsSync11,
6845
+ existsSync as existsSync12,
6633
6846
  mkdirSync as mkdirSync2,
6634
6847
  openSync as openSync7,
6635
6848
  readdirSync as readdirSync8,
@@ -6639,7 +6852,7 @@ import {
6639
6852
  writeFileSync as writeFileSync2
6640
6853
  } from "node:fs";
6641
6854
  import { platform as platform2 } from "node:os";
6642
- import { basename as basename3, join as join20, win32 as win324 } from "node:path";
6855
+ import { basename as basename3, join as join21, win32 as win324 } from "node:path";
6643
6856
  var WRAP_REGISTRY = "--wrap-registry";
6644
6857
  var WRAP_COMMAND = "--wrap-command";
6645
6858
  var CLAUDE_REGISTRY_ID = "claude-acp";
@@ -6680,7 +6893,7 @@ function platformKey(env = process.env) {
6680
6893
  }
6681
6894
  var strings = (v) => isRecord(v) ? Object.fromEntries(Object.entries(v).map(([k, x]) => [k, String(x)])) : {};
6682
6895
  function registryAgent(id, env = process.env) {
6683
- const file = join20(zedDataDir(env), "external_agents", "registry", "registry.json");
6896
+ const file = join21(zedDataDir(env), "external_agents", "registry", "registry.json");
6684
6897
  let agents;
6685
6898
  try {
6686
6899
  agents = readJsonFile(file).agents;
@@ -6736,26 +6949,26 @@ function splitPackageSpec(spec) {
6736
6949
  }
6737
6950
  function installedVersion(dir, pkg2) {
6738
6951
  try {
6739
- const json = readJsonFile(join20(dir, "node_modules", pkg2, "package.json"));
6952
+ const json = readJsonFile(join21(dir, "node_modules", pkg2, "package.json"));
6740
6953
  return typeof json.version === "string" ? json.version : void 0;
6741
6954
  } catch {
6742
6955
  return void 0;
6743
6956
  }
6744
6957
  }
6745
6958
  function packageExecutable(dir, pkg2) {
6746
- const root = join20(dir, "node_modules", pkg2);
6959
+ const root = join21(dir, "node_modules", pkg2);
6747
6960
  let json;
6748
6961
  try {
6749
- json = readJsonFile(join20(root, "package.json"));
6962
+ json = readJsonFile(join21(root, "package.json"));
6750
6963
  } catch {
6751
6964
  return void 0;
6752
6965
  }
6753
6966
  const bin = json.bin;
6754
- if (typeof bin === "string") return join20(root, bin);
6967
+ if (typeof bin === "string") return join21(root, bin);
6755
6968
  if (!isRecord(bin)) return void 0;
6756
6969
  const short = pkg2.replace(/^@[^/]+\//, "");
6757
6970
  const chosen = bin[short] ?? Object.values(bin)[0];
6758
- return typeof chosen === "string" ? join20(root, chosen) : void 0;
6971
+ return typeof chosen === "string" ? join21(root, chosen) : void 0;
6759
6972
  }
6760
6973
  function npmCli(args, env) {
6761
6974
  const script = npmScript("npm-cli.js");
@@ -6763,8 +6976,8 @@ function npmCli(args, env) {
6763
6976
  }
6764
6977
  function ensureNpxAgent(id, npx, stateDir2, env, logFile) {
6765
6978
  const [pkg2, version] = splitPackageSpec(npx.package);
6766
- const zedDir = join20(zedDataDir(env), "external_agents", "registry", "npx", safeName(id));
6767
- const ownDir = join20(stateDir2, "agents", "npx", safeName(id));
6979
+ const zedDir = join21(zedDataDir(env), "external_agents", "registry", "npx", safeName(id));
6980
+ const ownDir = join21(stateDir2, "agents", "npx", safeName(id));
6768
6981
  for (const dir of [zedDir, ownDir]) if (installedVersion(dir, pkg2)) return dir;
6769
6982
  ensurePrivateDir(ownDir);
6770
6983
  const spec = version ? `${pkg2}@0.0.0 - ${version}` : pkg2;
@@ -6785,7 +6998,7 @@ function ensureNpxAgent(id, npx, stateDir2, env, logFile) {
6785
6998
  function zedBinaryDir(id, version, target, env) {
6786
6999
  const sha = (s) => createHash3("sha256").update(s).digest("hex").slice(0, 16);
6787
7000
  const archiveKey = target.sha256 ? `${target.archive}\0sha256:${target.sha256.toLowerCase()}` : target.archive;
6788
- return join20(
7001
+ return join21(
6789
7002
  zedDataDir(env),
6790
7003
  "external_agents",
6791
7004
  "registry",
@@ -6795,18 +7008,18 @@ function zedBinaryDir(id, version, target, env) {
6795
7008
  }
6796
7009
  function commandIn(dir, cmd) {
6797
7010
  if (!/^\.[\\/]/.test(cmd) || cmd.includes("..")) return void 0;
6798
- const path = join20(dir, cmd.slice(2));
6799
- return existsSync11(path) ? path : void 0;
7011
+ const path = join21(dir, cmd.slice(2));
7012
+ return existsSync12(path) ? path : void 0;
6800
7013
  }
6801
7014
  function findBinaryAgent(id, version, target, stateDir2, env) {
6802
7015
  const exact = commandIn(zedBinaryDir(id, version, target, env), target.cmd);
6803
7016
  if (exact) return exact;
6804
- const zedAgentDir = join20(zedDataDir(env), "external_agents", "registry", zedSafeName(id));
6805
- const own = join20(stateDir2, "agents", "bin", safeName(id));
7017
+ const zedAgentDir = join21(zedDataDir(env), "external_agents", "registry", zedSafeName(id));
7018
+ const own = join21(stateDir2, "agents", "bin", safeName(id));
6806
7019
  for (const base of [zedAgentDir, own]) {
6807
7020
  let dirs = [];
6808
7021
  try {
6809
- dirs = readdirSync8(base).filter((d) => d.startsWith("v_")).map((d) => join20(base, d)).sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs);
7022
+ dirs = readdirSync8(base).filter((d) => d.startsWith("v_")).map((d) => join21(base, d)).sort((a, b) => statSync3(b).mtimeMs - statSync3(a).mtimeMs);
6810
7023
  } catch {
6811
7024
  continue;
6812
7025
  }
@@ -6818,8 +7031,8 @@ function findBinaryAgent(id, version, target, stateDir2, env) {
6818
7031
  return void 0;
6819
7032
  }
6820
7033
  async function downloadBinaryAgent(id, version, target, stateDir2, fetchImpl = fetch) {
6821
- const base = ensurePrivateDir(join20(stateDir2, "agents", "bin", safeName(id)));
6822
- const dest = join20(base, `v_${safeName(version)}`);
7034
+ const base = ensurePrivateDir(join21(stateDir2, "agents", "bin", safeName(id)));
7035
+ const dest = join21(base, `v_${safeName(version)}`);
6823
7036
  const tmp = `${dest}.partial-${process.pid}`;
6824
7037
  rmSync10(tmp, { recursive: true, force: true });
6825
7038
  mkdirSync2(tmp, { recursive: true });
@@ -6833,7 +7046,7 @@ async function downloadBinaryAgent(id, version, target, stateDir2, fetchImpl = f
6833
7046
  throw new Error("the download doesn't match the registry's checksum");
6834
7047
  }
6835
7048
  const name = basename3(new URL(target.archive).pathname) || "agent";
6836
- const file = join20(tmp, name);
7049
+ const file = join21(tmp, name);
6837
7050
  writeFileSync2(file, bytes);
6838
7051
  const lower = name.toLowerCase();
6839
7052
  if (lower.endsWith(".zip")) {
@@ -6846,14 +7059,14 @@ async function downloadBinaryAgent(id, version, target, stateDir2, fetchImpl = f
6846
7059
  unpack(unpackers("bz2", file, tmp));
6847
7060
  rmSync10(file, { force: true });
6848
7061
  } else if (target.cmd.replace(/^\.[\\/]/, "") !== name) {
6849
- renameSync2(file, join20(tmp, target.cmd.slice(2)));
7062
+ renameSync2(file, join21(tmp, target.cmd.slice(2)));
6850
7063
  }
6851
7064
  const cmd = commandIn(tmp, target.cmd);
6852
7065
  if (!cmd) throw new Error(`the download has no ${target.cmd}`);
6853
7066
  chmodSync(cmd, 493);
6854
7067
  rmSync10(dest, { recursive: true, force: true });
6855
7068
  renameWithRetry(tmp, dest);
6856
- return join20(dest, target.cmd.slice(2));
7069
+ return join21(dest, target.cmd.slice(2));
6857
7070
  } catch (err) {
6858
7071
  rmSync10(tmp, { recursive: true, force: true });
6859
7072
  throw new Error(
@@ -6932,7 +7145,7 @@ var DEVIN_AGENTS = {
6932
7145
  };
6933
7146
  function devinFile(env, home, platform4) {
6934
7147
  if (platform4 === "win32")
6935
- return join21(
7148
+ return join22(
6936
7149
  env.USERPROFILE || home,
6937
7150
  "AppData",
6938
7151
  "Roaming",
@@ -6941,12 +7154,12 @@ function devinFile(env, home, platform4) {
6941
7154
  "acp",
6942
7155
  "registry.json"
6943
7156
  );
6944
- const next = join21(home, ".windsurf-next");
6945
- const dir = existsSync12(join21(home, ".windsurf")) || !existsSync12(next) ? ".windsurf" : ".windsurf-next";
6946
- return join21(home, dir, "acp", "registry.json");
7157
+ const next = join22(home, ".windsurf-next");
7158
+ const dir = existsSync13(join22(home, ".windsurf")) || !existsSync13(next) ? ".windsurf" : ".windsurf-next";
7159
+ return join22(home, dir, "acp", "registry.json");
6947
7160
  }
6948
7161
  function devinFound(home) {
6949
- return existsSync12(join21(home, ".windsurf")) || existsSync12(join21(home, ".windsurf-next"));
7162
+ return existsSync13(join22(home, ".windsurf")) || existsSync13(join22(home, ".windsurf-next"));
6950
7163
  }
6951
7164
  function platformKey2(platform4, arch) {
6952
7165
  const os = platform4 === "win32" ? "windows" : platform4 === "darwin" ? "darwin" : "linux";
@@ -6954,7 +7167,7 @@ function platformKey2(platform4, arch) {
6954
7167
  }
6955
7168
  function readJson2(file) {
6956
7169
  try {
6957
- const v = JSON.parse(readFileSync19(file, "utf8"));
7170
+ const v = JSON.parse(readFileSync20(file, "utf8"));
6958
7171
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
6959
7172
  } catch {
6960
7173
  return void 0;
@@ -6968,8 +7181,8 @@ function devinInstalled(file) {
6968
7181
  });
6969
7182
  }
6970
7183
  function planDevin(file, launch, uninstall, platform4 = process.platform, arch = process.arch) {
6971
- const existed = existsSync12(file);
6972
- const before = existed ? readFileSync19(file, "utf8") : "";
7184
+ const existed = existsSync13(file);
7185
+ const before = existed ? readFileSync20(file, "utf8") : "";
6973
7186
  const parsed = existed ? readJson2(file) : { version: "1.0.0", agents: [], extensions: [] };
6974
7187
  if (!parsed)
6975
7188
  return {
@@ -7014,7 +7227,7 @@ function planDevin(file, launch, uninstall, platform4 = process.platform, arch =
7014
7227
  return { changes: [change], notes: [] };
7015
7228
  }
7016
7229
  async function runDevinInstall(o) {
7017
- const home = o.home ?? (o.env.HOME || o.env.USERPROFILE || homedir9());
7230
+ const home = o.home ?? (o.env.HOME || o.env.USERPROFILE || homedir11());
7018
7231
  const file = devinFile(o.env, home, process.platform);
7019
7232
  if (!o.uninstall && !(o.found ?? devinFound(home))) {
7020
7233
  o.out(
@@ -7064,9 +7277,9 @@ async function runDevinInstall(o) {
7064
7277
  }
7065
7278
 
7066
7279
  // src/hosts/grok/install.ts
7067
- import { existsSync as existsSync13, rmSync as rmSync11 } from "node:fs";
7068
- import { homedir as homedir10 } from "node:os";
7069
- import { join as join22 } from "node:path";
7280
+ import { existsSync as existsSync14, rmSync as rmSync11 } from "node:fs";
7281
+ import { homedir as homedir12 } from "node:os";
7282
+ import { join as join23 } from "node:path";
7070
7283
  var MIN_GROK = versionOf("grok").min;
7071
7284
  var GROK_EVENTS = [
7072
7285
  "SessionStart",
@@ -7075,7 +7288,7 @@ var GROK_EVENTS = [
7075
7288
  "SessionEnd"
7076
7289
  ];
7077
7290
  function grokHooksFile(env, home) {
7078
- return join22(grokHome(env, home), "hooks", "agent-rewake.json");
7291
+ return join23(grokHome(env, home), "hooks", "agent-rewake.json");
7079
7292
  }
7080
7293
  function grokHooksJson(node, launcher) {
7081
7294
  const cmd = (event) => `"${node}" "${launcher}" hook grok ${event}`;
@@ -7096,7 +7309,7 @@ function grokHooksJson(node, launcher) {
7096
7309
  `;
7097
7310
  }
7098
7311
  async function runGrokInstall(o) {
7099
- const home = o.env.HOME || o.env.USERPROFILE || homedir10();
7312
+ const home = o.env.HOME || o.env.USERPROFILE || homedir12();
7100
7313
  const programs2 = o.programs ?? grokPrograms({ env: o.env, home, platform: process.platform });
7101
7314
  const probe = o.probe ?? (o.programs ? () => void 0 : versionProbe(o.env, o.node));
7102
7315
  const picked = [...programs2].sort(
@@ -7104,7 +7317,7 @@ async function runGrokInstall(o) {
7104
7317
  )[0];
7105
7318
  const grok = picked && !o.uninstall ? withVersion(picked, probe) : picked;
7106
7319
  const file = grokHooksFile(o.env, home);
7107
- const installed = existsSync13(file);
7320
+ const installed = existsSync14(file);
7108
7321
  if (o.uninstall) {
7109
7322
  if (!installed) {
7110
7323
  o.out("Rewake isn't set up in Grok Build: nothing to remove.\n");
@@ -7161,7 +7374,7 @@ async function runGrokInstall(o) {
7161
7374
  }
7162
7375
  const launcher = ensureLauncher(o.stateDir, o.bundle) ?? launcherPath(o.stateDir);
7163
7376
  writeFileAtomic(
7164
- ensurePrivateDir(join22(grokHome(o.env, home), "hooks")),
7377
+ ensurePrivateDir(join23(grokHome(o.env, home), "hooks")),
7165
7378
  "agent-rewake.json",
7166
7379
  grokHooksJson(o.node, launcher)
7167
7380
  );
@@ -7177,23 +7390,23 @@ async function runGrokInstall(o) {
7177
7390
  }
7178
7391
 
7179
7392
  // src/hosts/jetbrains/install.ts
7180
- import { existsSync as existsSync14, readFileSync as readFileSync20 } from "node:fs";
7181
- import { homedir as homedir11 } from "node:os";
7182
- import { join as join23 } from "node:path";
7393
+ import { existsSync as existsSync15, readFileSync as readFileSync21 } from "node:fs";
7394
+ import { homedir as homedir13 } from "node:os";
7395
+ import { join as join24 } from "node:path";
7183
7396
  var JETBRAINS_AGENTS = {
7184
7397
  "Claude Agent (with Rewake)": "claude-acp",
7185
7398
  "Codex (with Rewake)": "codex-acp"
7186
7399
  };
7187
- function jetbrainsFile(env, home = homedir11()) {
7188
- return join23(
7400
+ function jetbrainsFile(env, home = homedir13()) {
7401
+ return join24(
7189
7402
  env.USERPROFILE && process.platform === "win32" ? env.USERPROFILE : home,
7190
7403
  ".jetbrains",
7191
7404
  "acp.json"
7192
7405
  );
7193
7406
  }
7194
7407
  function jetbrainsFound(env, home, platform4) {
7195
- const dirs = platform4 === "darwin" ? [join23(home, "Library", "Application Support", "JetBrains")] : platform4 === "win32" ? env.APPDATA ? [join23(env.APPDATA, "JetBrains")] : [] : [join23(env.XDG_CONFIG_HOME || join23(home, ".config"), "JetBrains")];
7196
- return dirs.some((d) => existsSync14(d)) || existsSync14(join23(home, ".jetbrains"));
7408
+ const dirs = platform4 === "darwin" ? [join24(home, "Library", "Application Support", "JetBrains")] : platform4 === "win32" ? env.APPDATA ? [join24(env.APPDATA, "JetBrains")] : [] : [join24(env.XDG_CONFIG_HOME || join24(home, ".config"), "JetBrains")];
7409
+ return dirs.some((d) => existsSync15(d)) || existsSync15(join24(home, ".jetbrains"));
7197
7410
  }
7198
7411
  function jetbrainsInstalled(env, home) {
7199
7412
  const v = readJson3(jetbrainsFile(env, home));
@@ -7202,15 +7415,15 @@ function jetbrainsInstalled(env, home) {
7202
7415
  }
7203
7416
  function readJson3(file) {
7204
7417
  try {
7205
- const v = JSON.parse(readFileSync20(file, "utf8"));
7418
+ const v = JSON.parse(readFileSync21(file, "utf8"));
7206
7419
  return typeof v === "object" && v !== null && !Array.isArray(v) ? v : void 0;
7207
7420
  } catch {
7208
7421
  return void 0;
7209
7422
  }
7210
7423
  }
7211
7424
  function planJetbrains(file, launch, uninstall) {
7212
- const existed = existsSync14(file);
7213
- const before = existed ? readFileSync20(file, "utf8") : "";
7425
+ const existed = existsSync15(file);
7426
+ const before = existed ? readFileSync21(file, "utf8") : "";
7214
7427
  const parsed = existed ? readJson3(file) : {};
7215
7428
  if (!parsed)
7216
7429
  return {
@@ -7239,7 +7452,7 @@ function planJetbrains(file, launch, uninstall) {
7239
7452
  return { changes: [change], notes: [] };
7240
7453
  }
7241
7454
  async function runJetbrainsInstall(o) {
7242
- const home = o.home ?? (o.env.HOME || o.env.USERPROFILE || homedir11());
7455
+ const home = o.home ?? (o.env.HOME || o.env.USERPROFILE || homedir13());
7243
7456
  const file = jetbrainsFile(o.env, home);
7244
7457
  if (!o.uninstall && !(o.found ?? jetbrainsFound(o.env, home, process.platform))) {
7245
7458
  o.out(
@@ -7297,18 +7510,20 @@ var PREVIEW_NAMES = {
7297
7510
  "gemini-cli": "Gemini CLI",
7298
7511
  antigravity: "Antigravity CLI",
7299
7512
  jetbrains: "JetBrains IDEs (AI Assistant)",
7300
- "devin-desktop": "Devin Desktop (formerly Windsurf)"
7513
+ "devin-desktop": "Devin Desktop (formerly Windsurf)",
7514
+ cursor: "Cursor (its own agent)"
7301
7515
  };
7302
7516
  function installedPreviews(env, home, stateDir2) {
7303
7517
  const checks = [
7304
7518
  ["claude-code", () => modInstalled(env, home)],
7305
7519
  ["codex", () => pluginInstalled(env, home)],
7306
- ["copilot-cli", () => existsSync15(hooksFile(env, home))],
7307
- ["grok", () => existsSync15(grokHooksFile(env, home))],
7308
- ["gemini-cli", () => existsSync15(extensionDir(stateDir2))],
7309
- ["antigravity", () => existsSync15(pluginDir(env, home))],
7520
+ ["copilot-cli", () => existsSync16(hooksFile(env, home))],
7521
+ ["grok", () => existsSync16(grokHooksFile(env, home))],
7522
+ ["gemini-cli", () => existsSync16(extensionDir(stateDir2))],
7523
+ ["antigravity", () => existsSync16(pluginDir(env, home))],
7310
7524
  ["jetbrains", () => jetbrainsInstalled(env, home)],
7311
- ["devin-desktop", () => devinInstalled(devinFile(env, home, process.platform))]
7525
+ ["devin-desktop", () => devinInstalled(devinFile(env, home, process.platform))],
7526
+ ["cursor", () => cursorInstalled(home)]
7312
7527
  ];
7313
7528
  return checks.filter(([, on]) => on()).map(([id]) => id);
7314
7529
  }
@@ -7318,12 +7533,12 @@ var AGENT_NAME = "Agent Rewake";
7318
7533
  var TASK_LABEL = "Agent Rewake: schedules";
7319
7534
  var FORMAT = { insertSpaces: true, tabSize: 2, eol: "\n" };
7320
7535
  function launchCommand(execPath = stableNode(), script = process.argv[1] ?? "", npxCli = npmScript("npx-cli.js", execPath)) {
7321
- const real = existsSync16(script) ? realpathSync5(script) : script;
7536
+ const real = existsSync17(script) ? realpathSync5(script) : script;
7322
7537
  if (/[\\/]_npx[\\/]/.test(real)) {
7323
7538
  const pinned = ["--yes", `${PACKAGE_NAME}@${VERSION2}`];
7324
7539
  if (npxCli) return { command: execPath, args: [npxCli, ...pinned] };
7325
- const npx = join24(dirname5(execPath), platform3() === "win32" ? "npx.cmd" : "npx");
7326
- return { command: existsSync16(npx) ? npx : "npx", args: pinned };
7540
+ const npx = join25(dirname5(execPath), platform3() === "win32" ? "npx.cmd" : "npx");
7541
+ return { command: existsSync17(npx) ? npx : "npx", args: pinned };
7327
7542
  }
7328
7543
  return { command: execPath, args: [real] };
7329
7544
  }
@@ -7336,12 +7551,12 @@ function stableNode(execPath = process.execPath, env = process.env, p = platform
7336
7551
  }
7337
7552
  };
7338
7553
  const self = real(execPath);
7339
- const candidates = p === "win32" ? [findOnWindows("node", env, existsSync16)] : (env.PATH ?? "").split(delimiter2).filter(Boolean).map((d) => join24(d, "node"));
7554
+ const candidates = p === "win32" ? [findOnWindows("node", env, existsSync17)] : (env.PATH ?? "").split(delimiter2).filter(Boolean).map((d) => join25(d, "node"));
7340
7555
  for (const c of candidates) if (c && c !== execPath && self && real(c) === self) return c;
7341
7556
  return execPath;
7342
7557
  }
7343
7558
  function selfCommand(execPath = process.execPath, script = process.argv[1] ?? "") {
7344
- return { command: execPath, args: [existsSync16(script) ? realpathSync5(script) : script] };
7559
+ return { command: execPath, args: [existsSync17(script) ? realpathSync5(script) : script] };
7345
7560
  }
7346
7561
  function wrappedEntry(original, target, launch) {
7347
7562
  const { type: _type, command: _command, args: _args, ...rest } = original;
@@ -7390,11 +7605,11 @@ function agentLabel(id, env) {
7390
7605
  return name && name !== id ? `"${id}" (${name})` : `"${id}"`;
7391
7606
  }
7392
7607
  function taskEntry(launch, p = platform3()) {
7393
- const quote = p === "win32" ? quoteForCmdTask : quoteForPosix;
7608
+ const quote2 = p === "win32" ? quoteForCmdTask : quoteForPosix;
7394
7609
  return {
7395
7610
  label: TASK_LABEL,
7396
- command: quote(launch.command),
7397
- args: [...launch.args, "ui"].map(quote),
7611
+ command: quote2(launch.command),
7612
+ args: [...launch.args, "ui"].map(quote2),
7398
7613
  ...p === "win32" && { shell: { program: "cmd" } },
7399
7614
  reveal: "always",
7400
7615
  use_new_terminal: false
@@ -7407,7 +7622,7 @@ function keyChord() {
7407
7622
  }
7408
7623
  var KEY_ACTION = ["task::Spawn", { task_name: TASK_LABEL }];
7409
7624
  function read(file) {
7410
- return existsSync16(file) ? { text: readText(file), existed: true } : { text: "", existed: false };
7625
+ return existsSync17(file) ? { text: readText(file), existed: true } : { text: "", existed: false };
7411
7626
  }
7412
7627
  function parseJsonc(text2) {
7413
7628
  const errors = [];
@@ -7416,8 +7631,8 @@ function parseJsonc(text2) {
7416
7631
  return first ? { value, error: `${printParseErrorCode(first.error)} at offset ${first.offset}` } : { value };
7417
7632
  }
7418
7633
  function zedAgentSetting(agentId, key2, env = process.env) {
7419
- const file = join24(zedConfigDir(env), "settings.json");
7420
- if (!existsSync16(file)) return void 0;
7634
+ const file = join25(zedConfigDir(env), "settings.json");
7635
+ if (!existsSync17(file)) return void 0;
7421
7636
  const value = parseJsonc(readText(file)).value;
7422
7637
  const servers = value && typeof value === "object" ? value.agent_servers : void 0;
7423
7638
  const entry = servers && typeof servers === "object" ? servers[agentId] : void 0;
@@ -7455,7 +7670,7 @@ function planInstall(opts) {
7455
7670
  const changes = [];
7456
7671
  const notes = [];
7457
7672
  const env = opts.env ?? process.env;
7458
- const settingsFile = join24(opts.dir, "settings.json");
7673
+ const settingsFile = join25(opts.dir, "settings.json");
7459
7674
  const settings = read(settingsFile);
7460
7675
  const base = settings.text.trim() === "" ? "{}\n" : settings.text;
7461
7676
  const parsed = parseJsonc(base);
@@ -7543,7 +7758,7 @@ function planInstall(opts) {
7543
7758
  ]
7544
7759
  });
7545
7760
  }
7546
- const tasksFile = join24(opts.dir, "tasks.json");
7761
+ const tasksFile = join25(opts.dir, "tasks.json");
7547
7762
  const tasks = read(tasksFile);
7548
7763
  const tasksBase = tasks.text.trim() === "" ? "[]\n" : tasks.text;
7549
7764
  const tasksParsed = parseJsonc(tasksBase);
@@ -7569,7 +7784,7 @@ function planInstall(opts) {
7569
7784
  });
7570
7785
  }
7571
7786
  if (opts.keybinding) {
7572
- const keymapFile = join24(opts.dir, "keymap.json");
7787
+ const keymapFile = join25(opts.dir, "keymap.json");
7573
7788
  const keymap = read(keymapFile);
7574
7789
  const keymapBase = keymap.text.trim() === "" ? "[]\n" : keymap.text;
7575
7790
  const keymapParsed = parseJsonc(keymapBase);
@@ -7599,8 +7814,8 @@ function planInstall(opts) {
7599
7814
  }
7600
7815
  return { changes, notes };
7601
7816
  }
7602
- function missingLaunchFiles(dir, exists = existsSync16) {
7603
- const parsed = parseJsonc(read(join24(dir, "settings.json")).text || "{}");
7817
+ function missingLaunchFiles(dir, exists = existsSync17) {
7818
+ const parsed = parseJsonc(read(join25(dir, "settings.json")).text || "{}");
7604
7819
  const servers = isRecord2(parsed.value) ? parsed.value.agent_servers : void 0;
7605
7820
  if (parsed.error || !isRecord2(servers)) return [];
7606
7821
  const missing = [];
@@ -7616,7 +7831,7 @@ function missingLaunchFiles(dir, exists = existsSync16) {
7616
7831
  function planUninstall(dir) {
7617
7832
  const changes = [];
7618
7833
  const notes = [];
7619
- const settingsFile = join24(dir, "settings.json");
7834
+ const settingsFile = join25(dir, "settings.json");
7620
7835
  const settings = read(settingsFile);
7621
7836
  const parsed = parseJsonc(settings.text || "{}");
7622
7837
  const servers = isRecord2(parsed.value) ? parsed.value.agent_servers : void 0;
@@ -7643,7 +7858,7 @@ function planUninstall(dir) {
7643
7858
  summary
7644
7859
  });
7645
7860
  }
7646
- const tasksFile = join24(dir, "tasks.json");
7861
+ const tasksFile = join25(dir, "tasks.json");
7647
7862
  const tasks = read(tasksFile);
7648
7863
  const taskList = parseJsonc(tasks.text || "[]");
7649
7864
  if (!taskList.error && Array.isArray(taskList.value)) {
@@ -7659,7 +7874,7 @@ function planUninstall(dir) {
7659
7874
  summary: [`Remove the task "${TASK_LABEL}"`]
7660
7875
  });
7661
7876
  }
7662
- const keymapFile = join24(dir, "keymap.json");
7877
+ const keymapFile = join25(dir, "keymap.json");
7663
7878
  const keymap = read(keymapFile);
7664
7879
  const blocks = parseJsonc(keymap.text || "[]");
7665
7880
  if (!blocks.error && Array.isArray(blocks.value)) {
@@ -7701,12 +7916,12 @@ function applyPlan(plan, now = /* @__PURE__ */ new Date()) {
7701
7916
  let bom = "";
7702
7917
  if (change.existed) {
7703
7918
  mode = statSync4(target).mode & 511;
7704
- if (readFileSync21(target, "utf8").charCodeAt(0) === 65279) bom = "\uFEFF";
7919
+ if (readFileSync22(target, "utf8").charCodeAt(0) === 65279) bom = "\uFEFF";
7705
7920
  const backup = `${target}.agent-rewake-backup-${stamp(now)}`;
7706
7921
  copyFileSync(target, backup);
7707
7922
  backups.push(backup);
7708
7923
  }
7709
- const tmp = join24(dir, `.${basename4(target)}.agent-rewake.${process.pid}.tmp`);
7924
+ const tmp = join25(dir, `.${basename4(target)}.agent-rewake.${process.pid}.tmp`);
7710
7925
  const fd = openSync8(tmp, "w", mode);
7711
7926
  try {
7712
7927
  writeSync3(fd, bom + change.after);
@@ -7759,7 +7974,7 @@ async function runInstall(opts) {
7759
7974
  out(describe(plan, opts.uninstall ? "remove these entries from Zed" : "set up Zed"));
7760
7975
  let note;
7761
7976
  if (!opts.uninstall) {
7762
- const home = opts.env.HOME || opts.env.USERPROFILE || homedir12();
7977
+ const home = opts.env.HOME || opts.env.USERPROFILE || homedir14();
7763
7978
  const ids = opts.previews ? [] : installedPreviews(opts.env, home, stateDir(opts.env));
7764
7979
  const previews = opts.previews ?? ids.map((id) => PREVIEW_NAMES[id] ?? id);
7765
7980
  note = otherAgentsNote(
@@ -8047,7 +8262,7 @@ function overviewMarkdown(groups, now, locale) {
8047
8262
 
8048
8263
  // src/util/keep-awake.ts
8049
8264
  import { spawn as spawn6 } from "node:child_process";
8050
- import { existsSync as existsSync17 } from "node:fs";
8265
+ import { existsSync as existsSync18 } from "node:fs";
8051
8266
  import { platform as osPlatform3 } from "node:os";
8052
8267
  var CAFFEINATE = "/usr/bin/caffeinate";
8053
8268
  var Wakefulness = class {
@@ -8062,7 +8277,7 @@ var Wakefulness = class {
8062
8277
  this.pid = opts.pid ?? process.pid;
8063
8278
  }
8064
8279
  get supported() {
8065
- return this.platform === "darwin" && existsSync17(this.program);
8280
+ return this.platform === "darwin" && existsSync18(this.program);
8066
8281
  }
8067
8282
  get holding() {
8068
8283
  return this.child !== void 0 && this.child.exitCode === null && this.child.signalCode === null;
@@ -8101,9 +8316,9 @@ var Wakefulness = class {
8101
8316
 
8102
8317
  // src/util/sleep-settings.ts
8103
8318
  import { spawnSync as spawnSync7 } from "node:child_process";
8104
- import { readFileSync as readFileSync22 } from "node:fs";
8105
- import { homedir as homedir13, platform as osPlatform4 } from "node:os";
8106
- import { join as join25 } from "node:path";
8319
+ import { readFileSync as readFileSync23 } from "node:fs";
8320
+ import { homedir as homedir15, platform as osPlatform4 } from "node:os";
8321
+ import { join as join26 } from "node:path";
8107
8322
  var SLEEP_DOCS_URL = "https://codizelabs.github.io/agent-rewake/docs/#keep-your-computer-awake";
8108
8323
  var run2 = (command, args) => {
8109
8324
  const r = spawnSync7(command, args, { encoding: "utf8", timeout: 3e3, windowsHide: true });
@@ -8117,7 +8332,7 @@ function readSleepSettings(o = {}) {
8117
8332
  const exec = o.run ?? run2;
8118
8333
  const read2 = o.readFile ?? ((path) => {
8119
8334
  try {
8120
- return readFileSync22(path, "utf8");
8335
+ return readFileSync23(path, "utf8");
8121
8336
  } catch {
8122
8337
  return void 0;
8123
8338
  }
@@ -8133,7 +8348,7 @@ function readSleepSettings(o = {}) {
8133
8348
  }
8134
8349
  if (p === "win32") return { os: "windows", ...windowsSleep(exec) };
8135
8350
  if (p === "linux")
8136
- return { os: "linux", ...linuxSleep(exec, o.env ?? process.env, o.home ?? homedir13(), read2) };
8351
+ return { os: "linux", ...linuxSleep(exec, o.env ?? process.env, o.home ?? homedir15(), read2) };
8137
8352
  return { os: "other" };
8138
8353
  }
8139
8354
  function parsePmset(text2) {
@@ -8209,7 +8424,7 @@ function linuxSleep(exec, env, home, read2) {
8209
8424
  out.pluggedInSleepMin = kind === "nothing" || kind === "blank" ? 0 : Math.ceil(seconds / 60);
8210
8425
  }
8211
8426
  } else if (desktops.includes("kde")) {
8212
- const rc = read2(join25(env.XDG_CONFIG_HOME || join25(home, ".config"), "powerdevilrc"));
8427
+ const rc = read2(join26(env.XDG_CONFIG_HOME || join26(home, ".config"), "powerdevilrc"));
8213
8428
  out.desktop = "kde";
8214
8429
  const group = /\[AC\]\[SuspendAndShutdown\]([\s\S]*?)(?:\n\[|$)/.exec(rc ?? "")?.[1] ?? "";
8215
8430
  const key2 = (k) => new RegExp(`^${k}=(\\S+)`, "m").exec(group)?.[1];
@@ -10277,7 +10492,7 @@ ${this.listText(session)}`;
10277
10492
  return command.on ? `Rewake: Automatic resume is on for this thread. After a usage limit, Rewake sends your resume message when the limit resets, follows the new reset time if ${this.agentName} is still limited, and never approves permission requests. Turn it off with /${COMMAND_NAME} auto off.` : "Rewake: Automatic resume is off for this thread. After a usage limit, Rewake will ask first.";
10278
10493
  }
10279
10494
  case "page": {
10280
- const file = join26(ensurePrivateDir(this.opts.stateDir), "Schedules.md");
10495
+ const file = join27(ensurePrivateDir(this.opts.stateDir), "Schedules.md");
10281
10496
  writeFileSync3(file, overviewMarkdown(overview(this.opts.stateDir), now, this.opts.locale), {
10282
10497
  mode: 384
10283
10498
  });
@@ -10510,11 +10725,11 @@ ${lines.join("\n")}`;
10510
10725
  const now = this.now();
10511
10726
  const owned = [...this.sessions.values()].filter((s) => this.lock.holds(s.sessionId));
10512
10727
  const ids = new Set(owned.map((s) => s.sessionId));
10513
- const due = ids.size === 0 ? [] : this.store.list().filter(
10728
+ const due2 = ids.size === 0 ? [] : this.store.list().filter(
10514
10729
  (s) => ids.has(s.sessionId) && (s.status === "scheduled" || s.status === "queued") && s.dueAt - now <= WAKE_HORIZON_MS
10515
10730
  );
10516
10731
  const delivering = owned.find((s) => s.delivering !== void 0);
10517
- const want = due.length > 0 || delivering !== void 0;
10732
+ const want = due2.length > 0 || delivering !== void 0;
10518
10733
  const held = this.wake.set(want, settings.keepAwake);
10519
10734
  if (!want || settings.keepAwake === "never") return;
10520
10735
  if (!this.wakeUnsupportedSaid) {
@@ -10522,8 +10737,8 @@ ${lines.join("\n")}`;
10522
10737
  const sleep = this.sleepSettingsNow(now);
10523
10738
  const known = sleep.pluggedInSleepMin !== void 0 || sleep.onBattery !== void 0;
10524
10739
  const risks = sleepRisks(sleep, hold);
10525
- const where2 = this.sessions.get(due[0]?.sessionId ?? "") ?? delivering;
10526
- const first = due[0];
10740
+ const where2 = this.sessions.get(due2[0]?.sessionId ?? "") ?? delivering;
10741
+ const first = due2[0];
10527
10742
  if (where2 && (risks.length > 0 || !known && !held)) {
10528
10743
  this.wakeUnsupportedSaid = true;
10529
10744
  const what = first ? ` before ${first.kind === "user" ? "the scheduled message" : "the resume"} at ${formatWhen(first.dueAt, now, this.opts.locale)}` : "";
@@ -10534,7 +10749,7 @@ ${lines.join("\n")}`;
10534
10749
  }
10535
10750
  }
10536
10751
  if (!held) return;
10537
- for (const s of due) {
10752
+ for (const s of due2) {
10538
10753
  const key2 = s.scheduleId;
10539
10754
  const session = this.sessions.get(s.sessionId);
10540
10755
  if (this.wakeAnnounced.has(key2) || !session) continue;
@@ -10696,7 +10911,7 @@ ${lines.join("\n")}`;
10696
10911
  }
10697
10912
  const content = await this.form(
10698
10913
  session,
10699
- `${capitalize2(this.agentName)} wants to change the message scheduled for ${at(s.dueAt)} ("${preview(s.text)}"): ${changes.join(", ")}.${why}${editing ? " You can edit the new message." : ""} Submit changes it; Decline keeps it as it is.`,
10914
+ `${capitalize2(this.agentName)} wants to change the message scheduled for ${at(s.dueAt)} ("${preview(s.text)}"): ${changes.join("; ")}.${why}${editing ? " You can edit the new message." : ""} Submit changes it; Decline keeps it as it is.`,
10700
10915
  editing ? { message: { type: "string", title: "Message", minLength: 1, default: r.message } } : {},
10701
10916
  editing ? ["message"] : []
10702
10917
  );
@@ -10798,12 +11013,12 @@ ${lines.join("\n")}`;
10798
11013
  * message moves on to its next run.
10799
11014
  */
10800
11015
  recoverInterrupted(session, s, now) {
10801
- const due = formatWhen(s.dueAt, now, this.opts.locale);
11016
+ const due2 = formatWhen(s.dueAt, now, this.opts.locale);
10802
11017
  if (s.repeat) {
10803
11018
  this.store.update(s.scheduleId, (x) => ({ ...x, status: "failed" }), now);
10804
11019
  this.status(
10805
11020
  session,
10806
- `Rewake: The run due at ${due} was interrupted, because Zed closed during its reply.`
11021
+ `Rewake: The run due at ${due2} was interrupted, because Zed closed during its reply.`
10807
11022
  );
10808
11023
  this.repeatNext(session, s.scheduleId, now);
10809
11024
  } else {
@@ -10812,13 +11027,13 @@ ${lines.join("\n")}`;
10812
11027
  (x) => ({ ...x, status: "needs_attention", failureReason: "interrupted" }),
10813
11028
  now
10814
11029
  );
10815
- const question = `Rewake: The message scheduled for ${due} was interrupted, because Zed closed during its reply: "${preview(s.text)}". Send it again?`;
11030
+ const question = `Rewake: The message scheduled for ${due2} was interrupted, because Zed closed during its reply: "${preview(s.text)}". Send it again?`;
10816
11031
  if (this.clientSupportsForms && !session.formOpen)
10817
11032
  void this.offerRetry(session, s.scheduleId, question);
10818
11033
  else
10819
11034
  this.status(
10820
11035
  session,
10821
- `Rewake: The message scheduled for ${due} was interrupted, because Zed closed during its reply. Type /rewake list, then /rewake now N to send it again.`
11036
+ `Rewake: The message scheduled for ${due2} was interrupted, because Zed closed during its reply. Type /rewake list, then /rewake now N to send it again.`
10822
11037
  );
10823
11038
  }
10824
11039
  this.refreshMarker(session);
@@ -11225,11 +11440,15 @@ async function runContinue(o) {
11225
11440
  o.out("Nothing to continue: no closed session is stopped at a usage limit.\n");
11226
11441
  return 0;
11227
11442
  }
11228
- const line = (c) => `${placeOf(c.host, c.record.cwd)}: stopped ${formatAt(c.record.limit?.seenAt ?? now, now)}${c.resetsAt && c.resetsAt + RESET_MARGIN_MS > now ? `; Rewake can continue it ${formatAt(c.resetsAt + RESET_MARGIN_MS, now)}` : ""}`;
11443
+ const failedTry = (c) => new ScheduleStore(o.deps.stateDir).listForSession(c.record.sessionId, c.host.id).filter((s) => s.status === "failed" && s.updatedAt >= (c.record.limit?.seenAt ?? 0)).sort((a, b) => b.updatedAt - a.updatedAt)[0];
11444
+ const line = (c) => {
11445
+ const f = failedTry(c);
11446
+ return `${placeOf(c.host, c.record.cwd)}: stopped ${formatAt(c.record.limit?.seenAt ?? now, now)}${c.resetsAt && c.resetsAt + RESET_MARGIN_MS > now ? `; Rewake can continue it ${formatAt(c.resetsAt + RESET_MARGIN_MS, now)}` : ""}${f ? `; Rewake tried ${formatAt(f.lastRun?.at ?? f.updatedAt, now)}${f.failureMessage ? ` and ${c.host.name} ended with: "${f.failureMessage}"` : ", without success"}` : ""}`;
11447
+ };
11229
11448
  if (!o.interactive) {
11230
11449
  o.out(
11231
11450
  `${list.map((c) => ` ${line(c)}`).join("\n")}
11232
- Run "agent-rewake continue" in a terminal to choose.
11451
+ Run "agent-rewake continue" in a terminal to choose.${list.some((c) => failedTry(c)) ? " If a try failed, open that session in its agent to continue it yourself." : ""}
11233
11452
  `
11234
11453
  );
11235
11454
  return 1;
@@ -11252,13 +11471,23 @@ ${list.map((c, i) => ` ${i + 1}. ${line(c)}`).join("\n")}
11252
11471
  chosen = pick;
11253
11472
  }
11254
11473
  const place = placeOf(chosen.host, chosen.record.cwd);
11474
+ const latest = chosen.host.maxWaitAfterLimitMs === void 0 ? void 0 : (chosen.record.limit?.seenAt ?? now) + chosen.host.maxWaitAfterLimitMs;
11475
+ const tooLate = () => `Rewake can continue ${place} only until ${formatWhen(latest ?? now, now)}, 4 hours after its usage limit. Choose an earlier time, or open the ${chosen.host.noun ?? "session"} later and continue it yourself.
11476
+ `;
11477
+ if (latest !== void 0 && latest - now < 5 * 6e4) {
11478
+ o.out(
11479
+ `Rewake can continue ${place} only within 4 hours of its usage limit, and that time has passed. Open the ${chosen.host.noun ?? "session"} and continue it yourself.
11480
+ `
11481
+ );
11482
+ return 1;
11483
+ }
11255
11484
  let at;
11256
11485
  if (chosen.resetsAt && chosen.resetsAt + RESET_MARGIN_MS > now) {
11257
11486
  at = chosen.resetsAt + RESET_MARGIN_MS;
11258
11487
  } else {
11259
- const presets = [1, 3, 5].map((h) => ({ h, at: now + h * 36e5 }));
11488
+ const presets = [1, 3, 5].map((h) => ({ h, at: now + h * 36e5 })).filter((p) => latest === void 0 || p.at <= latest);
11260
11489
  o.out(
11261
- `When should Rewake continue ${place}?
11490
+ `When should Rewake continue ${place}?${latest === void 0 ? "" : ` (It can wait until ${formatWhen(latest, now)}, 4 hours after the limit.)`}
11262
11491
  ${presets.map(
11263
11492
  (p, i) => ` ${i + 1}. In ${p.h} hour${p.h === 1 ? "" : "s"} (${formatWhen(p.at, now)})`
11264
11493
  ).join("\n")}
@@ -11287,6 +11516,10 @@ ${presets.map(
11287
11516
  return 1;
11288
11517
  }
11289
11518
  }
11519
+ if (latest !== void 0 && at > latest) {
11520
+ o.out(tooLate());
11521
+ return 1;
11522
+ }
11290
11523
  armClosed(chosen.host, chosen.record, at, o.deps);
11291
11524
  o.out(`${armedText(chosen.host, chosen.record.cwd, at, now)}
11292
11525
  `);
@@ -11296,7 +11529,7 @@ ${presets.map(
11296
11529
  `This computer may sleep before then: ${risks.join("; ")}. To keep it awake: ${SLEEP_DOCS_URL}
11297
11530
  `
11298
11531
  );
11299
- if (settings.newThreads !== "on")
11532
+ if (settings.newThreads !== "on" && latest === void 0)
11300
11533
  o.out(
11301
11534
  "To let Rewake do this by itself next time (when the reset is within a day): agent-rewake continue --always\n"
11302
11535
  );
@@ -11304,16 +11537,16 @@ ${presets.map(
11304
11537
  }
11305
11538
 
11306
11539
  // src/doctor.ts
11307
- import { existsSync as existsSync19, readdirSync as readdirSync10, readFileSync as readFileSync24, rmSync as rmSync12, writeFileSync as writeFileSync4 } from "node:fs";
11308
- import { homedir as homedir14 } from "node:os";
11309
- import { join as join28 } from "node:path";
11540
+ import { existsSync as existsSync20, readdirSync as readdirSync10, readFileSync as readFileSync25, rmSync as rmSync12, writeFileSync as writeFileSync4 } from "node:fs";
11541
+ import { homedir as homedir16 } from "node:os";
11542
+ import { join as join29 } from "node:path";
11310
11543
 
11311
11544
  // src/setup.ts
11312
- import { existsSync as existsSync18, readdirSync as readdirSync9, readFileSync as readFileSync23 } from "node:fs";
11313
- import { join as join27 } from "node:path";
11545
+ import { existsSync as existsSync19, readdirSync as readdirSync9, readFileSync as readFileSync24 } from "node:fs";
11546
+ import { join as join28 } from "node:path";
11314
11547
  var isRecord3 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
11315
11548
  function detectSetup(zedConfigDir2, stateDir2) {
11316
- const file = join27(zedConfigDir2, "settings.json");
11549
+ const file = join28(zedConfigDir2, "settings.json");
11317
11550
  const setup = {
11318
11551
  settings: "missing",
11319
11552
  aiOff: false,
@@ -11324,7 +11557,7 @@ function detectSetup(zedConfigDir2, stateDir2) {
11324
11557
  };
11325
11558
  const started = lastStart(stateDir2);
11326
11559
  if (started) setup.lastStart = started;
11327
- if (!existsSync18(file)) return setup;
11560
+ if (!existsSync19(file)) return setup;
11328
11561
  const errors = [];
11329
11562
  const value = parse2(readText(file) || "{}", errors, { allowTrailingComma: true });
11330
11563
  if (errors.length > 0 || !isRecord3(value)) return { ...setup, settings: "invalid" };
@@ -11347,7 +11580,7 @@ function agentName(id, env = process.env) {
11347
11580
  return id === CLAUDE_REGISTRY_ID ? "Claude Agent" : id;
11348
11581
  }
11349
11582
  function lastStart(stateDir2) {
11350
- const dir = join27(stateDir2, "logs");
11583
+ const dir = join28(stateDir2, "logs");
11351
11584
  let files;
11352
11585
  try {
11353
11586
  files = readdirSync9(dir).filter((f) => /^rewake-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f)).sort().reverse();
@@ -11355,7 +11588,7 @@ function lastStart(stateDir2) {
11355
11588
  return void 0;
11356
11589
  }
11357
11590
  for (const f of files) {
11358
- const lines = readFileSync23(join27(dir, f), "utf8").trim().split("\n").reverse();
11591
+ const lines = readFileSync24(join28(dir, f), "utf8").trim().split("\n").reverse();
11359
11592
  for (const line of lines) {
11360
11593
  try {
11361
11594
  const r = JSON.parse(line);
@@ -11379,14 +11612,14 @@ var MIN_ZED = "1.22.0";
11379
11612
  var MIN_NODE = 22;
11380
11613
  var DAY = 24 * 60 * 60 * 1e3;
11381
11614
  var isRecord4 = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
11382
- function findZedApps(platform4 = process.platform, home = homedir14(), env = process.env) {
11615
+ function findZedApps(platform4 = process.platform, home = homedir16(), env = process.env) {
11383
11616
  const apps2 = [];
11384
11617
  const names = ["Zed", "Zed Preview", "Zed Nightly", "Zed Dev"];
11385
11618
  if (platform4 === "darwin") {
11386
- for (const dir of ["/Applications", join28(home, "Applications")]) {
11619
+ for (const dir of ["/Applications", join29(home, "Applications")]) {
11387
11620
  for (const name of names) {
11388
- const plist = join28(dir, `${name}.app`, "Contents", "Info.plist");
11389
- if (!existsSync19(plist) || apps2.some((a) => a.name === name)) continue;
11621
+ const plist = join29(dir, `${name}.app`, "Contents", "Info.plist");
11622
+ if (!existsSync20(plist) || apps2.some((a) => a.name === name)) continue;
11390
11623
  const m = /<key>CFBundleShortVersionString<\/key>\s*<string>([^<]+)<\/string>/.exec(
11391
11624
  readTextSafe(plist)
11392
11625
  );
@@ -11399,25 +11632,25 @@ function findZedApps(platform4 = process.platform, home = homedir14(), env = pro
11399
11632
  ["Zed Preview", "zed-preview.app"],
11400
11633
  ["Zed Nightly", "zed-nightly.app"]
11401
11634
  ];
11402
- for (const [name, dir] of local) if (existsSync19(join28(home, ".local", dir))) apps2.push({ name });
11403
- if (existsSync19(join28(home, ".var", "app", "dev.zed.Zed"))) apps2.push({ name: "Zed (Flatpak)" });
11635
+ for (const [name, dir] of local) if (existsSync20(join29(home, ".local", dir))) apps2.push({ name });
11636
+ if (existsSync20(join29(home, ".var", "app", "dev.zed.Zed"))) apps2.push({ name: "Zed (Flatpak)" });
11404
11637
  } else if (platform4 === "win32") {
11405
- const programs2 = env.LOCALAPPDATA ? join28(env.LOCALAPPDATA, "Programs") : void 0;
11638
+ const programs2 = env.LOCALAPPDATA ? join29(env.LOCALAPPDATA, "Programs") : void 0;
11406
11639
  if (programs2) {
11407
- for (const name of names) if (existsSync19(join28(programs2, name))) apps2.push({ name });
11640
+ for (const name of names) if (existsSync20(join29(programs2, name))) apps2.push({ name });
11408
11641
  }
11409
11642
  }
11410
11643
  return apps2;
11411
11644
  }
11412
11645
  function readTextSafe(file) {
11413
11646
  try {
11414
- return readFileSync24(file, "utf8");
11647
+ return readFileSync25(file, "utf8");
11415
11648
  } catch {
11416
11649
  return "";
11417
11650
  }
11418
11651
  }
11419
11652
  function recentLogs(state, now, days = 14) {
11420
- const dir = join28(state, "logs");
11653
+ const dir = join29(state, "logs");
11421
11654
  let files;
11422
11655
  try {
11423
11656
  files = readdirSync10(dir).filter((f) => /^rewake-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f));
@@ -11429,7 +11662,7 @@ function recentLogs(state, now, days = 14) {
11429
11662
  const out = [];
11430
11663
  for (const f of files.sort()) {
11431
11664
  if (f.slice(7, 17) < cutoff) continue;
11432
- for (const line of readTextSafe(join28(dir, f)).split("\n")) {
11665
+ for (const line of readTextSafe(join29(dir, f)).split("\n")) {
11433
11666
  if (!line.startsWith("{")) continue;
11434
11667
  try {
11435
11668
  const r = JSON.parse(line);
@@ -11443,7 +11676,7 @@ function recentLogs(state, now, days = 14) {
11443
11676
  return out.sort((a, b) => a.t - b.t);
11444
11677
  }
11445
11678
  function readSettings(file) {
11446
- if (!existsSync19(file)) return { state: "missing", value: {} };
11679
+ if (!existsSync20(file)) return { state: "missing", value: {} };
11447
11680
  const errors = [];
11448
11681
  const value = parse2(readText(file) || "{}", errors, { allowTrailingComma: true });
11449
11682
  if (errors.length > 0 || !isRecord4(value)) return { state: "invalid", value: {} };
@@ -11472,7 +11705,7 @@ function diagnose(ctx) {
11472
11705
  const reach = otherAgentsNote(ctx.agents?.() ?? [], ctx.previews?.() ?? []);
11473
11706
  let reachShown = false;
11474
11707
  const apps2 = ctx.zedApps();
11475
- const settingsFile = join28(zedDir, "settings.json");
11708
+ const settingsFile = join29(zedDir, "settings.json");
11476
11709
  const settings = readSettings(settingsFile);
11477
11710
  if (apps2.length > 0) {
11478
11711
  const old = apps2.filter((a) => a.version && compareVersions(a.version, MIN_ZED) < 0);
@@ -11590,8 +11823,8 @@ function diagnose(ctx) {
11590
11823
  text: `Not on yet for: ${names(addable)}.`,
11591
11824
  fix: `Run: ${install}`
11592
11825
  });
11593
- const noRegistry = !existsSync19(
11594
- join28(zedDataDir(env), "external_agents", "registry", "registry.json")
11826
+ const noRegistry = !existsSync20(
11827
+ join29(zedDataDir(env), "external_agents", "registry", "registry.json")
11595
11828
  );
11596
11829
  for (const [id, why] of skipped) {
11597
11830
  if (noRegistry && /copy of the ACP Registry/.test(why)) continue;
@@ -11646,7 +11879,7 @@ function diagnose(ctx) {
11646
11879
  const registryIds = Object.entries(servers).filter(([id, e]) => isRecord4(e) && id !== CLAUDE_REGISTRY_ID && id !== AGENT_NAME).filter(
11647
11880
  ([, e]) => isRecord4(e) && (e.type === "registry" || (unwrappedEntry(e)?.type ?? "") === "registry")
11648
11881
  ).map(([id]) => id);
11649
- if (registryIds.length > 0 && !existsSync19(join28(zedDataDir(env), "external_agents", "registry", "registry.json")))
11882
+ if (registryIds.length > 0 && !existsSync20(join29(zedDataDir(env), "external_agents", "registry", "registry.json")))
11650
11883
  add({
11651
11884
  area: "Zed",
11652
11885
  level: "todo",
@@ -11873,9 +12106,9 @@ function diagnose(ctx) {
11873
12106
  return findings;
11874
12107
  }
11875
12108
  function canWrite(dir) {
11876
- if (!existsSync19(dir)) return true;
12109
+ if (!existsSync20(dir)) return true;
11877
12110
  try {
11878
- const probe = join28(dir, `.doctor-${process.pid}.tmp`);
12111
+ const probe = join29(dir, `.doctor-${process.pid}.tmp`);
11879
12112
  writeFileSync4(probe, "");
11880
12113
  rmSync12(probe, { force: true });
11881
12114
  return true;
@@ -12001,7 +12234,7 @@ function detailLines(ctx) {
12001
12234
  }
12002
12235
 
12003
12236
  // src/hosts/codex/install.ts
12004
- import { homedir as homedir15 } from "node:os";
12237
+ import { homedir as homedir17 } from "node:os";
12005
12238
  var MIN_CODEX = versionOf("codex").min;
12006
12239
  function pickCodex(programs2) {
12007
12240
  const byVersion = (a, b) => compareVersions(b.version ?? "0", a.version ?? "0");
@@ -12009,7 +12242,7 @@ function pickCodex(programs2) {
12009
12242
  return cli[0] ?? [...programs2].sort(byVersion)[0];
12010
12243
  }
12011
12244
  async function runCodexInstall(o) {
12012
- const home = o.env.HOME || o.env.USERPROFILE || homedir15();
12245
+ const home = o.env.HOME || o.env.USERPROFILE || homedir17();
12013
12246
  const programs2 = o.programs ?? codexPrograms({ env: o.env, home, platform: process.platform });
12014
12247
  const probe = o.probe ?? (o.programs ? () => void 0 : versionProbe(o.env, o.node));
12015
12248
  const picked = pickCodex(programs2);
@@ -12113,1275 +12346,1544 @@ async function runCodexInstall(o) {
12113
12346
  return 0;
12114
12347
  }
12115
12348
 
12116
- // src/hosts/doctor.ts
12117
- var DAY2 = 24 * 60 * 60 * 1e3;
12118
- function diagnoseOutside(f) {
12119
- const out = [];
12120
- const add = (x) => out.push({ area: "Outside Zed", ...x });
12121
- const setUp = new Set(f.previews.map((p) => p.id));
12122
- for (const a of f.agents ?? []) {
12123
- const v = AGENT_VERSIONS[a.id];
12124
- if (!v || !a.version) continue;
12125
- if (tooOld(a.id, a.version)) {
12126
- const set = setUp.has(a.id);
12127
- add({
12128
- level: set ? "problem" : "info",
12129
- text: `${v.name} ${a.version} is too old for Rewake (it needs ${v.min} or newer), so ${set ? "Rewake may miss its usage limits" : "Rewake isn't set up for it"}.`,
12130
- fix: `Update it with: ${v.update}${set ? "" : `, then run: agent-rewake install --only ${a.id}`}`
12131
- });
12132
- } else if (setUp.has(a.id) && newerThanTested(a.id, a.version))
12133
- add({ level: "info", text: untestedText(a.id, a.version) });
12134
- }
12135
- if (f.previews.length === 0) return out;
12136
- for (const p of f.previews) {
12137
- const off = hooksTurnedOff(p.id, f.env, f.home);
12138
- if (off)
12139
- add({
12140
- level: "problem",
12141
- text: `${p.name} has its hooks turned off, so Rewake can't see its usage limits.`,
12142
- fix: `Turn hooks back on in ${p.name}'s settings, or take Rewake out of it: agent-rewake uninstall --only ${p.id}`
12349
+ // src/timers/waiter.ts
12350
+ import { readdirSync as readdirSync12, readFileSync as readFileSync27, rmSync as rmSync14, writeFileSync as writeFileSync6 } from "node:fs";
12351
+ import { join as join31 } from "node:path";
12352
+
12353
+ // src/timers/timers.ts
12354
+ import { spawn as spawn7, spawnSync as spawnSync8 } from "node:child_process";
12355
+ import { existsSync as existsSync21, readdirSync as readdirSync11, readFileSync as readFileSync26, rmSync as rmSync13, writeFileSync as writeFileSync5 } from "node:fs";
12356
+ import { join as join30 } from "node:path";
12357
+ var ID = /^[a-z0-9-]{1,64}$/;
12358
+ function defaultTimerHost(stateDir2, node, cli) {
12359
+ return {
12360
+ platform: process.platform,
12361
+ stateDir: stateDir2,
12362
+ node,
12363
+ cli,
12364
+ run: (command, args, input) => {
12365
+ const r = spawnSync8(command, args, {
12366
+ encoding: "utf8",
12367
+ timeout: 5e3,
12368
+ windowsHide: true,
12369
+ ...input !== void 0 && { input }
12143
12370
  });
12144
- }
12145
- if (!f.hasTimer)
12146
- add({
12147
- level: "problem",
12148
- text: "Rewake can't continue sessions outside Zed by itself on this computer: it has no scheduler Rewake can use.",
12149
- fix: f.platform === "linux" ? "Sign in to a normal desktop session, or install and start the at service." : "Run agent-rewake doctor again after a restart; if it stays, report it with agent-rewake doctor --details."
12150
- });
12151
- const resumes = new ScheduleStore(f.stateDir).list().filter((s) => s.host !== void 0);
12152
- const name = (host) => host && f.hosts.get(host)?.name || host || "";
12153
- const upcoming = resumes.filter((s) => s.status === "scheduled" || s.status === "sending").sort((a, b) => a.dueAt - b.dueAt);
12154
- const needsYou = resumes.filter((s) => s.status === "needs_attention");
12155
- const recent = (s) => s.dueAt > f.now - 7 * DAY2;
12156
- const missed = resumes.filter((s) => s.status === "missed" && recent(s));
12157
- const failed = resumes.filter((s) => s.status === "failed" && recent(s));
12158
- const n = (k, one) => `${k} ${one}${k === 1 ? "" : "s"}`;
12159
- const next = upcoming[0];
12160
- if (next)
12161
- add({
12162
- level: "ok",
12163
- text: `${n(upcoming.length, "planned resume")}; the next one continues ${name(next.host)} ${f.when(next.dueAt, f.now)}. Keep this computer on and awake then.`
12164
- });
12165
- if (needsYou.length > 0)
12166
- add({
12167
- level: "todo",
12168
- text: `${n(needsYou.length, "resume")} need${needsYou.length === 1 ? "s" : ""} you.`,
12169
- fix: `Review ${needsYou.length === 1 ? "it" : "them"} with: agent-rewake ui`
12170
- });
12171
- if (missed.length > 0)
12172
- add({
12173
- level: "info",
12174
- text: `${n(missed.length, "resume")} ${missed.length === 1 ? "was" : "were"} missed in the last 7 days (the computer was off or asleep at the time).`
12175
- });
12176
- if (failed.length > 0)
12177
- add({
12178
- level: "info",
12179
- text: `${n(failed.length, "resume")} failed in the last 7 days.`,
12180
- fix: "See why with: agent-rewake ui"
12181
- });
12182
- if (out.length === 0)
12183
- add({
12184
- level: "ok",
12185
- text: `Set up in ${f.previews.map((p) => p.name).join(", ")}. Nothing planned right now.`
12186
- });
12187
- return out;
12371
+ return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
12372
+ },
12373
+ detached: (command, args) => {
12374
+ try {
12375
+ spawn7(command, args, { detached: true, stdio: "ignore", windowsHide: true }).unref();
12376
+ } catch {
12377
+ }
12378
+ },
12379
+ exists: existsSync21,
12380
+ uid: () => process.getuid?.(),
12381
+ waiterRunning: (pid) => isWaiter(pid, cli)
12382
+ };
12188
12383
  }
12189
-
12190
- // src/hosts/index.ts
12191
- import { homedir as homedir16 } from "node:os";
12192
-
12193
- // src/hosts/codex/adapter.ts
12194
- function programOf(s, node) {
12195
- const path = s.sessionRef?.codex;
12196
- return path ? codexProgram(path, node) : void 0;
12384
+ var label = (id) => `codizelabs.agent-rewake.${id}`;
12385
+ function timerName(id, gen = 0) {
12386
+ return gen > 0 ? `${id}-r${gen}` : id;
12197
12387
  }
12198
- function envOf(s, base) {
12199
- const home = s.sessionRef?.codexHome;
12200
- return home ? { ...base, CODEX_HOME: home } : base;
12388
+ function parseTimerName(name) {
12389
+ const m = /^(.+)-r(\d{1,3})$/.exec(name);
12390
+ return m?.[1] ? { id: m[1], gen: Number(m[2]) } : { id: name, gen: 0 };
12201
12391
  }
12202
- function codexAdapter(deps) {
12203
- const usage2 = deps.readUsage ?? ((codex2, env) => readUsage(codex2, env));
12204
- const queue = deps.queue ?? queueMessage;
12205
- const signedOut = /* @__PURE__ */ new Set();
12206
- const unanswered2 = /* @__PURE__ */ new Set();
12207
- return {
12208
- id: "codex",
12209
- name: "Codex",
12210
- noun: "thread",
12211
- reopen: "resume the thread in Codex",
12212
- again: (at) => at ? `Resume the thread in Codex and type "rewake", and Rewake continues it ${at}.` : 'Resume the thread in Codex and type "rewake" with a time, for example "rewake 3:30pm".',
12213
- async check(s, now) {
12214
- const facts = {};
12215
- const transcript = s.sessionRef?.transcript;
12216
- let limit;
12217
- if (transcript) {
12218
- try {
12219
- limit = findCodexLimit(readTail2(transcript), now);
12220
- facts.userTypedSince = !limit.limited;
12221
- } catch {
12222
- }
12223
- }
12224
- const codex2 = programOf(s, deps.node);
12225
- let noAnswer = false;
12226
- if (codex2) {
12227
- const u = await usage2(codex2, envOf(s, deps.env));
12228
- if (u.ok) {
12229
- if (u.allowed !== void 0) facts.usageAllowed = u.allowed;
12230
- if (u.allowed === false && u.resetsAt && u.resetsAt > now) facts.newResetsAt = u.resetsAt;
12231
- } else if (u.reason === "signed-out") signedOut.add(s.scheduleId);
12232
- else if (u.reason === "timeout" || u.reason === "error") noAnswer = true;
12233
- }
12234
- if (facts.usageAllowed === void 0 && limit?.limited && limit.resetsAt !== void 0 && limit.resetsAt > now) {
12235
- facts.usageAllowed = false;
12236
- facts.newResetsAt = limit.resetsAt;
12237
- }
12238
- unanswered2.delete(s.scheduleId);
12239
- if (facts.usageAllowed === void 0 && noAnswer) {
12240
- if ((s.rearms ?? 0) < MAX_REARMS) facts.usageAllowed = false;
12241
- else unanswered2.add(s.scheduleId);
12242
- }
12243
- return facts;
12244
- },
12245
- delivered(s) {
12246
- const transcript = s.sessionRef?.transcript;
12247
- if (!transcript) return void 0;
12248
- try {
12249
- return userMessages(readTail2(transcript, 512 * 1024)).includes(s.text) || void 0;
12250
- } catch {
12251
- return void 0;
12252
- }
12253
- },
12254
- async send(s) {
12255
- if (signedOut.has(s.scheduleId)) return { ok: false, reason: "failed", detail: "signed-out" };
12256
- if (unanswered2.has(s.scheduleId)) return { ok: false, reason: "failed", detail: "no-usage" };
12257
- const codex2 = programOf(s, deps.node);
12258
- const thread = s.sessionRef?.threadId;
12259
- if (!codex2 || !thread) return { ok: false, reason: "unsupported", detail: "no Codex found" };
12260
- const env = envOf(s, deps.env);
12261
- let r = await queue(codex2, thread, s.text, env);
12262
- if (!r.ok && r.reason === "daemon") r = await queue(codex2, thread, s.text, env, "unix://");
12263
- if (r.ok) return { ok: true };
12264
- return {
12265
- ok: false,
12266
- reason: r.reason === "archived" || r.reason === "deleted" ? "closed" : "failed",
12267
- detail: r.reason
12268
- };
12269
- }
12270
- };
12271
- }
12272
-
12273
- // src/hosts/codex/hooks.ts
12274
- import { basename as basename6 } from "node:path";
12275
- var CODEX_PLACE = { name: "Codex", typed: "rewake", features: /* @__PURE__ */ new Set() };
12276
- var REWAKE = /^\s*\/?rewake(?:\s+([\s\S]*?))?\s*$/i;
12277
- function threadOf(input) {
12278
- const path = input.transcript_path;
12279
- if (!isCodexRollout(path)) return void 0;
12280
- const id = threadIdOf(path);
12281
- return id ? { id, path } : void 0;
12282
- }
12283
- function pendingFor2(store, threadId) {
12284
- return store.listForSession(threadId, "codex").filter(
12285
- (s) => !TERMINAL_STATUSES.has(s.status) && !["missed", "needs_attention"].includes(s.status)
12286
- );
12287
- }
12288
- function armedText2(at, now) {
12289
- return `Rewake will continue this thread ${formatAt(at, now)}. Keep this computer on and awake until then. If Codex is closed at that time, the thread continues when you next open it. To cancel, send any other message in the thread.`;
12290
- }
12291
- function noTimerText(at, now) {
12292
- return `Rewake couldn't set a timer on this computer, so it can't continue this thread by itself. After the limit resets ${formatAt(at, now)}, send a message in the thread to continue.`;
12293
- }
12294
- var block = (reason) => JSON.stringify({ decision: "block", reason });
12295
- function codexHooks(deps) {
12296
- const arm = (ctx, thread, at) => {
12297
- const store = new ScheduleStore(ctx.stateDir);
12298
- const settings = loadSettings(ctx.stateDir);
12299
- const cwd = typeof ctx.input.cwd === "string" ? ctx.input.cwd : "";
12300
- const codex2 = deps.codexPath();
12301
- const s = store.create({
12302
- sessionId: thread.id,
12303
- cwd,
12304
- text: settings.resumePrompt ?? DEFAULT_RESUME_PROMPT,
12305
- dueAt: at,
12306
- kind: "limit_resume",
12307
- createdBy: "agent",
12308
- now: ctx.now
12309
- });
12310
- const home = ctx.env.CODEX_HOME;
12311
- const resume = {
12312
- ...s,
12313
- host: "codex",
12314
- sessionRef: {
12315
- threadId: thread.id,
12316
- transcript: thread.path,
12317
- ...codex2 && { codex: codex2 },
12318
- ...home && { codexHome: home }
12319
- }
12320
- };
12321
- store.put(resume);
12322
- if (deps.arm(resume.scheduleId, at) !== false) return true;
12323
- store.update(resume.scheduleId, (x) => ({ ...x, status: "cancelled" }), ctx.now);
12324
- return false;
12325
- };
12326
- const cancelPending = (ctx, threadId) => {
12327
- const store = new ScheduleStore(ctx.stateDir);
12328
- for (const s of pendingFor2(store, threadId)) {
12329
- store.update(s.scheduleId, (x) => ({ ...x, status: "cancelled" }), ctx.now);
12330
- deps.disarm(s.scheduleId);
12331
- }
12332
- };
12333
- return {
12334
- isMine: (input, env) => !env.GROK_HOOK_EVENT && isCodexRollout(input.transcript_path),
12335
- sessionId: (input) => threadOf(input)?.id,
12336
- async handle(ctx) {
12337
- const thread = threadOf(ctx.input);
12338
- if (!thread) return void 0;
12339
- loadSettings(ctx.stateDir);
12340
- if (ctx.event === "UserPromptSubmit") {
12341
- const prompt2 = typeof ctx.input.prompt === "string" ? ctx.input.prompt : "";
12342
- const m = REWAKE.exec(prompt2);
12343
- if (!m) {
12344
- cancelPending(ctx, thread.id);
12345
- return void 0;
12346
- }
12347
- const c = continueOnly(CODEX_PLACE, m[1] ?? "", ctx.now);
12348
- if (c.kind === "reply") return block(c.text);
12349
- if (c.kind === "list") {
12350
- const next = pendingFor2(new ScheduleStore(ctx.stateDir), thread.id)[0];
12351
- return block(
12352
- next ? `Rewake will continue this thread ${formatAt(next.dueAt, ctx.now)}. To cancel: rewake cancel` : 'Rewake: Nothing is set to continue this thread. At a usage limit, type "rewake" to continue after the reset.'
12353
- );
12354
- }
12355
- if (c.kind === "cancel") {
12356
- const had = pendingFor2(new ScheduleStore(ctx.stateDir), thread.id).length > 0;
12357
- cancelPending(ctx, thread.id);
12358
- return block(
12359
- had ? "Rewake: Cancelled. This thread won't be continued on its own." : "Rewake: Nothing is set to continue this thread."
12360
- );
12361
- }
12362
- let limit;
12363
- try {
12364
- limit = findCodexLimit(readTail2(thread.path), ctx.now);
12365
- } catch {
12366
- limit = { limited: false };
12367
- }
12368
- if (!limit.limited)
12369
- return block(
12370
- "Rewake: this thread isn't at a usage limit, so there's nothing to continue."
12371
- );
12372
- if (limit.billing)
12373
- return block(
12374
- "Rewake can't continue after this limit: this limit is about credits or spending, which waiting doesn't fix."
12375
- );
12376
- let at = c.at;
12377
- if (at === void 0) {
12378
- const later = new ScheduleStore(ctx.stateDir).listForSession(thread.id, "codex").filter((x) => x.failureReason === "far-reset" || x.failureReason === "expired").map((x) => x.dueAt).filter((t) => t > ctx.now);
12379
- const times = [
12380
- ...limit.resetsAt ? [limit.resetsAt + RESET_MARGIN_MS] : [],
12381
- ...later
12382
- ].filter((t) => t > ctx.now);
12383
- if (times.length > 0) at = Math.max(...times);
12384
- }
12385
- if (at === void 0 && limit.resetPassed) at = ctx.now + RESET_MARGIN_MS;
12386
- if (at === void 0)
12387
- return block(
12388
- `Rewake doesn't know when this limit resets. Type "rewake" with a time, for example "rewake 3:30pm".`
12389
- );
12390
- cancelPending(ctx, thread.id);
12391
- if (!arm(ctx, thread, at)) return block(noTimerText(at, ctx.now));
12392
- return block(armedText2(at, ctx.now));
12393
- }
12394
- if (ctx.event === "SessionEnd") {
12395
- if (pendingFor2(new ScheduleStore(ctx.stateDir), thread.id).length > 0) return void 0;
12396
- let limit;
12397
- try {
12398
- limit = findCodexLimit(readTail2(thread.path), ctx.now);
12399
- } catch {
12400
- return void 0;
12401
- }
12402
- if (!limit.limited) return void 0;
12403
- const settings = loadSettings(ctx.stateDir);
12404
- const decision = decideArm({
12405
- now: ctx.now,
12406
- ...limit.resetsAt !== void 0 && { resetsAt: limit.resetsAt },
12407
- isBilling: limit.billing === true,
12408
- auto: autoFor(settings)
12409
- });
12410
- const cwd = typeof ctx.input.cwd === "string" ? basename6(ctx.input.cwd) : "";
12411
- const where2 = cwd ? `Codex in the "${cwd}" folder` : "Codex";
12412
- if (decision.action === "arm") {
12413
- const ok = arm(ctx, thread, decision.fireAt);
12414
- deps.notify(
12415
- "Agent Rewake",
12416
- `${where2}: ${ok ? armedText2(decision.fireAt, ctx.now) : noTimerText(decision.fireAt, ctx.now)}`
12417
- );
12418
- return void 0;
12419
- }
12420
- if (decision.action === "offer") {
12421
- const how = limit.resetsAt ? `type "rewake", and Rewake continues it ${formatAt(limit.resetsAt + RESET_MARGIN_MS, ctx.now)}, after the limit resets` : 'type "rewake" with a time, for example "rewake 3:30pm"';
12422
- deps.notify(
12423
- "Agent Rewake",
12424
- `${where2} hit its usage limit. Resume the thread in Codex and ${how}.`
12425
- );
12426
- }
12427
- return void 0;
12428
- }
12429
- return void 0;
12430
- }
12431
- };
12392
+ var unit = (id) => `codizelabs-agent-rewake-${id}`;
12393
+ var taskName = (id) => `\\AgentRewake\\${id}`;
12394
+ function timersDir(h) {
12395
+ return ensurePrivateDir(join30(h.stateDir, "timers"));
12432
12396
  }
12433
-
12434
- // src/hosts/copilot/host.ts
12435
- import { spawn as spawn7 } from "node:child_process";
12436
- import { createInterface as createInterface4 } from "node:readline";
12437
- var COPILOT_ID = "copilot-cli";
12438
- function isCopilot(input, env) {
12439
- return !env.GROK_HOOK_EVENT && !("hookEventName" in input) && safeSessionId(input.sessionId) && typeof input.timestamp === "number";
12397
+ function checkId(id) {
12398
+ if (!ID.test(id)) throw new Error(`invalid timer id: ${id}`);
12440
12399
  }
12441
- function resumeCopilot(r, text2, env, node = process.execPath) {
12442
- if (!r.program)
12443
- return Promise.resolve({ ok: false, reason: "unsupported", detail: "no Copilot found" });
12444
- const program = codexProgram(r.program, node);
12445
- return new Promise((resolve2) => {
12446
- let limited = false;
12447
- let resetsAt;
12448
- const child = spawn7(
12449
- program.command,
12450
- [
12451
- ...program.args,
12452
- `--resume=${r.sessionId}`,
12453
- "-p",
12454
- text2,
12455
- "--no-ask-user",
12456
- "--output-format",
12457
- "json",
12458
- "--no-auto-update"
12459
- ],
12460
- {
12461
- cwd: r.cwd || void 0,
12462
- env: { ...env, COPILOT_AUTO_UPDATE: "false" },
12463
- stdio: ["ignore", "pipe", "pipe"],
12464
- windowsHide: true
12465
- }
12466
- );
12467
- let err = "";
12468
- child.stderr.on("data", (d) => {
12469
- if (err.length < 1 << 16) err += d.toString("utf8");
12470
- });
12471
- createInterface4({ input: child.stdout }).on("line", (line) => {
12472
- try {
12473
- const e = JSON.parse(line);
12474
- if (e.type === "session.error" && e.data?.errorType === "rate_limit") {
12475
- limited = true;
12476
- resetsAt = classifyCopilotError(e.data.message, Date.now())?.resetsAt;
12477
- }
12478
- } catch {
12479
- }
12480
- });
12481
- const timedOut = resumeDeadline(child);
12482
- child.on("error", () => resolve2({ ok: false, reason: "failed", detail: "spawn" }));
12483
- child.on("exit", (code) => {
12484
- if (timedOut()) return resolve2({ ok: false, reason: "failed", detail: "timeout" });
12485
- if (limited) resolve2({ ok: false, reason: "limited", ...resetsAt && { resetsAt } });
12486
- else if (code === 0) resolve2({ ok: true });
12487
- else if (SESSION_GONE.test(err)) resolve2({ ok: false, reason: "closed", detail: "deleted" });
12488
- else resolve2({ ok: false, reason: "failed", detail: `exit ${code ?? "signal"}` });
12489
- });
12490
- });
12400
+ var MARK = { systemd: ".systemd", schtasks: ".task" };
12401
+ function mark(name, kind, h) {
12402
+ const ext = MARK[kind];
12403
+ if (ext) writeFileSync5(join30(timersDir(h), `${name}${ext}`), "", { mode: 384 });
12491
12404
  }
12492
- var copilotHost = {
12493
- id: COPILOT_ID,
12494
- name: "GitHub Copilot CLI",
12495
- reopen: 'resume the session with "copilot --resume"',
12496
- resume: (r, text2, env) => resumeCopilot(r, text2, env)
12497
- };
12498
- function copilotHooks(deps) {
12499
- return {
12500
- isMine: isCopilot,
12501
- sessionId: (input) => safeSessionId(input.sessionId) ? input.sessionId : void 0,
12502
- async handle(ctx) {
12503
- const id = ctx.input.sessionId;
12504
- const cwd = typeof ctx.input.cwd === "string" ? ctx.input.cwd : "";
12505
- const d = deps.closed(ctx);
12506
- if (!ctx.env[FIRE_ENV]) ensureProgram(copilotHost, id, cwd, d, () => deps.program(ctx.env));
12507
- switch (ctx.event) {
12508
- case "sessionStart":
12509
- onSessionStart(copilotHost, id, cwd, d, deps.program(ctx.env));
12510
- break;
12511
- case "userPromptSubmitted":
12512
- onPrompt(copilotHost, id, cwd, d);
12513
- break;
12514
- case "errorOccurred": {
12515
- const e = ctx.input.error;
12516
- const text2 = typeof e === "string" ? e : e?.message;
12517
- const recoverable = ctx.input.recoverable === true || typeof e === "object" && e?.recoverable === true;
12518
- const limit = recogniseForHost(
12519
- {
12520
- agent: "copilot",
12521
- source: "hook",
12522
- ...typeof text2 === "string" && { text: text2 },
12523
- recovered: recoverable
12524
- },
12525
- ctx.now
12526
- );
12527
- if (limit) onLimit(copilotHost, id, cwd, limit, d);
12528
- break;
12529
- }
12530
- case "sessionEnd":
12531
- onSessionEnd(copilotHost, id, cwd, d);
12532
- break;
12533
- }
12534
- return void 0;
12535
- }
12536
- };
12405
+ function timerNames(id, h) {
12406
+ const names = /* @__PURE__ */ new Set([id]);
12407
+ let files = [];
12408
+ try {
12409
+ files = readdirSync11(join30(h.stateDir, "timers"));
12410
+ } catch {
12411
+ }
12412
+ for (const f of files) {
12413
+ const name = f.replace(/^codizelabs\.agent-rewake\./, "").replace(/\.(plist|systemd|task|at|at-time|wait)$/, "");
12414
+ if (name !== f && ID.test(name) && parseTimerName(name).id === id) names.add(name);
12415
+ }
12416
+ return [...names];
12537
12417
  }
12538
-
12539
- // src/hosts/index.ts
12540
- registerHost("codex");
12541
- registerHost(COPILOT_ID);
12542
- registerHost(GROK_ID);
12543
- registerHost(GEMINI_ID);
12544
- registerHost(ANTIGRAVITY_ID);
12545
- var CLOSED_HOSTS = [
12546
- copilotHost,
12547
- grokHost(process.env),
12548
- geminiHost,
12549
- antigravityHost()
12550
- ];
12551
- function hostAdapters(env, node, stateDir2) {
12552
- return new Map([
12553
- ["codex", codexAdapter({ env, node })],
12554
- ...CLOSED_HOSTS.map((h) => [h.id, closedAdapter(h, stateDir2, env)])
12418
+ var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
12419
+ function launchdPlist(id, at, node, cli, stateDir2) {
12420
+ const d = new Date(Math.ceil(at / 6e4) * 6e4);
12421
+ const int = (k, v) => `<key>${k}</key><integer>${v}</integer>`;
12422
+ return `<?xml version="1.0" encoding="UTF-8"?>
12423
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
12424
+ <plist version="1.0">
12425
+ <dict>
12426
+ <key>Label</key><string>${label(id)}</string>
12427
+ <key>ProgramArguments</key>
12428
+ <array><string>${xml(node)}</string><string>${xml(cli)}</string><string>fire</string><string>${id}</string>${stateDir2 ? `<string>--state-dir</string><string>${xml(stateDir2)}</string>` : ""}</array>
12429
+ <key>StartCalendarInterval</key>
12430
+ <dict>${int("Month", d.getMonth() + 1)}${int("Day", d.getDate())}${int("Hour", d.getHours())}${int("Minute", d.getMinutes())}</dict>
12431
+ <key>RunAtLoad</key><true/>
12432
+ <key>ProcessType</key><string>Background</string>
12433
+ <key>StandardOutPath</key><string>/dev/null</string>
12434
+ <key>StandardErrorPath</key><string>/dev/null</string>
12435
+ </dict>
12436
+ </plist>
12437
+ `;
12438
+ }
12439
+ function launchdTarget(h) {
12440
+ const uid = h.uid();
12441
+ return uid === void 0 ? void 0 : `gui/${uid}`;
12442
+ }
12443
+ function launchdArm(id, at, h) {
12444
+ const domain = launchdTarget(h);
12445
+ if (!domain) return { ok: false, reason: "no-scheduler" };
12446
+ const plist = join30(timersDir(h), `${label(id)}.plist`);
12447
+ writeFileSync5(plist, launchdPlist(id, at, h.node, h.cli, h.stateDir), { mode: 384 });
12448
+ const r = h.run("launchctl", ["bootstrap", domain, plist]);
12449
+ if (r.status === 0) return { ok: true, via: "launchd" };
12450
+ rmSync13(plist, { force: true });
12451
+ return { ok: false, reason: "failed", detail: r.stderr.trim() };
12452
+ }
12453
+ function systemdAvailable(h) {
12454
+ if (!h.exists("/run/systemd/system")) return false;
12455
+ const r = h.run("systemctl", ["--user", "is-system-running"]);
12456
+ return /^(running|degraded)\b/.test(r.stdout.trim());
12457
+ }
12458
+ function utcCalendar(at) {
12459
+ return `${new Date(Math.ceil(at / 1e3) * 1e3).toISOString().slice(0, 19).replace("T", " ")} UTC`;
12460
+ }
12461
+ function systemdArm(id, at, h) {
12462
+ h.run("systemctl", ["--user", "stop", `${unit(id)}.timer`]);
12463
+ h.run("systemctl", ["--user", "reset-failed", `${unit(id)}.timer`, `${unit(id)}.service`]);
12464
+ const r = h.run("systemd-run", [
12465
+ "--user",
12466
+ `--unit=${unit(id)}`,
12467
+ `--on-calendar=${utcCalendar(at)}`,
12468
+ "--timer-property=AccuracySec=1s",
12469
+ "--timer-property=Persistent=true",
12470
+ `--description=Agent Rewake: ${id}`,
12471
+ h.node,
12472
+ h.cli,
12473
+ "fire",
12474
+ id,
12475
+ "--state-dir",
12476
+ h.stateDir
12555
12477
  ]);
12478
+ if (r.status !== 0) return { ok: false, reason: "failed", detail: r.stderr.trim() };
12479
+ mark(id, "systemd", h);
12480
+ return { ok: true, via: "systemd" };
12556
12481
  }
12557
- function hookHandler(host, deps) {
12558
- if (host === "codex") return codexHooks(deps);
12559
- if (host === COPILOT_ID)
12560
- return copilotHooks({ closed: deps.closed, program: (env) => deps.program(host, env) });
12561
- if (host === GROK_ID)
12562
- return grokHooks({ closed: deps.closed, program: (env) => deps.program(host, env) });
12563
- if (host === GEMINI_ID)
12564
- return geminiHooks({
12565
- closed: deps.closed,
12566
- program: (env) => deps.program(host, env),
12567
- apiKey: (env) => geminiApiKeyAuth(env, env.HOME || env.USERPROFILE || homedir16())
12568
- });
12569
- if (host === ANTIGRAVITY_ID)
12570
- return antigravityHooks({ closed: deps.closed, program: (env) => deps.program(host, env) });
12571
- return void 0;
12482
+ function atTime(at) {
12483
+ const d = new Date(Math.ceil(at / 6e4) * 6e4);
12484
+ const p = (n) => String(n).padStart(2, "0");
12485
+ return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}`;
12572
12486
  }
12573
- var OWNER_ENV = "AGENT_REWAKE_OWNER";
12574
- function ownedByZed(env) {
12575
- return env[OWNER_ENV] === "acp";
12487
+ var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
12488
+ function atAvailable(h) {
12489
+ if (h.run("atq", []).status !== 0) return false;
12490
+ const pg = h.run("pgrep", ["-x", "atd"]);
12491
+ return pg.status === 0 || pg.status === null || pg.status === 127;
12576
12492
  }
12577
-
12578
- // src/hosts/hook.ts
12579
- var MAX_INPUT = 4 * 1024 * 1024;
12580
- async function readStdin(stream = process.stdin) {
12581
- const chunks = [];
12582
- let size = 0;
12583
- for await (const chunk of stream) {
12584
- const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
12585
- size += b.length;
12586
- if (size > MAX_INPUT) return "";
12587
- chunks.push(b);
12493
+ function atArm(id, at, h) {
12494
+ atCancel(id, h);
12495
+ const job = `${shQuote(h.node)} ${shQuote(h.cli)} fire ${id} --state-dir ${shQuote(h.stateDir)}
12496
+ `;
12497
+ const r = h.run("at", ["-t", atTime(at)], job);
12498
+ const n = /job (\d+)/.exec(`${r.stderr}
12499
+ ${r.stdout}`)?.[1];
12500
+ if (r.status !== 0 || !n) return { ok: false, reason: "failed", detail: r.stderr.trim() };
12501
+ writeFileSync5(join30(timersDir(h), `${id}.at`), n, { mode: 384 });
12502
+ writeFileSync5(join30(timersDir(h), `${id}.at-time`), `${atTime(at)} ${h.node} ${h.cli}`, {
12503
+ mode: 384
12504
+ });
12505
+ return { ok: true, via: "at" };
12506
+ }
12507
+ function atJob(id, h) {
12508
+ try {
12509
+ const n = readFileSync26(join30(h.stateDir, "timers", `${id}.at`), "utf8").trim();
12510
+ return /^\d+$/.test(n) ? n : void 0;
12511
+ } catch {
12512
+ return void 0;
12588
12513
  }
12589
- return Buffer.concat(chunks).toString("utf8");
12590
12514
  }
12591
- async function runHook(handler, event, stdin, env, stateDir2, now) {
12592
- if (!handler || ownedByZed(env)) return void 0;
12593
- let input;
12515
+ function atCancel(id, h) {
12516
+ const n = atJob(id, h);
12517
+ if (n) h.run("atrm", [n]);
12518
+ rmSync13(join30(h.stateDir, "timers", `${id}.at`), { force: true });
12519
+ rmSync13(join30(h.stateDir, "timers", `${id}.at-time`), { force: true });
12520
+ }
12521
+ var waiterPid = (h) => join30(h.stateDir, "timers", "waiter.pid");
12522
+ function waiterAlive(h) {
12594
12523
  try {
12595
- input = JSON.parse(stdin || "{}");
12524
+ const pid = Number(readFileSync26(waiterPid(h), "utf8").trim());
12525
+ return Number.isInteger(pid) && pid > 0 && (h.waiterRunning?.(pid) ?? false) ? pid : void 0;
12596
12526
  } catch {
12597
12527
  return void 0;
12598
12528
  }
12599
- if (typeof input !== "object" || input === null || Array.isArray(input)) return void 0;
12600
- const record = input;
12601
- if (!handler.isMine(record, env)) return void 0;
12602
- const id = handler.sessionId(record);
12603
- if (id && new ThreadStore(stateDir2).get(id)) return void 0;
12604
- return handler.handle({ event, input: record, env, stateDir: stateDir2, now });
12605
12529
  }
12606
-
12607
- // src/install/select.ts
12608
- var WHAT = {
12609
- zed: "Resumes and schedules messages in Zed's agent threads.",
12610
- jetbrains: "Adds Claude Agent and Codex with Rewake to AI Assistant in your JetBrains IDEs.",
12611
- "devin-desktop": "Adds Claude Agent and Codex with Rewake to Devin Desktop's agent selector.",
12612
- "claude-code": "Asks at a usage limit, then continues the same session when the limit resets.",
12613
- codex: 'Type "rewake" in a thread at a usage limit; it continues when the limit resets.',
12614
- "copilot-cli": "Continues a closed session when its usage limit resets.",
12615
- grok: "Continues a closed session when its usage limit resets.",
12616
- "gemini-cli": "Continues a closed session when its usage limit resets.",
12617
- antigravity: "Continues a closed CLI conversation when its limit resets; in the app and IDE, says when it resets."
12618
- };
12619
- var UNREACHABLE = "Rewake can't reach claude.ai, the Claude desktop app's chat, or Zed's own agent.";
12620
- function defaultChoice(places) {
12621
- return new Set(places.filter((p) => p.state === "ready").map((p) => p.id));
12530
+ function isWaiter(pid, cli) {
12531
+ try {
12532
+ const args = readFileSync26(`/proc/${pid}/cmdline`, "utf8").split("\0");
12533
+ return args.includes(cli) && args.includes("wait");
12534
+ } catch {
12535
+ return false;
12536
+ }
12537
+ }
12538
+ function waiterArm(id, at, h) {
12539
+ writeFileSync5(join30(timersDir(h), `${id}.wait`), String(at), { mode: 384 });
12540
+ if (waiterAlive(h) === void 0) h.detached(h.node, [h.cli, "wait", "--state-dir", h.stateDir]);
12541
+ return { ok: true, via: "waiter" };
12542
+ }
12543
+ function waiterHas(name, h) {
12544
+ return existsSync21(join30(h.stateDir, "timers", `${name}.wait`)) && waiterAlive(h) !== void 0;
12545
+ }
12546
+ function localIso(at) {
12547
+ const d = new Date(Math.ceil(at / 1e3) * 1e3);
12548
+ const p = (n) => String(Math.abs(n)).padStart(2, "0");
12549
+ const off = -d.getTimezoneOffset();
12550
+ return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}${off >= 0 ? "+" : "-"}${p(Math.trunc(off / 60))}:${p(off % 60)}`;
12551
+ }
12552
+ function taskXml(id, at, node, cli, stateDir2) {
12553
+ return `<?xml version="1.0" encoding="UTF-16"?>
12554
+ <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
12555
+ <RegistrationInfo><Description>Agent Rewake: ${id}</Description></RegistrationInfo>
12556
+ <Triggers>
12557
+ <TimeTrigger>
12558
+ <StartBoundary>${localIso(at)}</StartBoundary>
12559
+ <EndBoundary>${localIso(at + 7 * 24 * 36e5)}</EndBoundary>
12560
+ <Enabled>true</Enabled>
12561
+ </TimeTrigger>
12562
+ </Triggers>
12563
+ <Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
12564
+ <Settings>
12565
+ <StartWhenAvailable>true</StartWhenAvailable>
12566
+ <DeleteExpiredTaskAfter>PT1H</DeleteExpiredTaskAfter>
12567
+ <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
12568
+ <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
12569
+ <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
12570
+ <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>
12571
+ </Settings>
12572
+ <Actions Context="Author">
12573
+ <Exec><Command>conhost.exe</Command><Arguments>--headless "${xml(node)}" "${xml(cli)}" fire ${id}${stateDir2 ? ` --state-dir "${xml(stateDir2)}"` : ""}</Arguments></Exec>
12574
+ </Actions>
12575
+ </Task>
12576
+ `;
12577
+ }
12578
+ function schtasksArm(id, at, h) {
12579
+ h.run("schtasks", ["/Delete", "/TN", taskName(id), "/F"]);
12580
+ const file = join30(timersDir(h), `${id}.xml`);
12581
+ writeFileSync5(file, Buffer.from(`\uFEFF${taskXml(id, at, h.node, h.cli, h.stateDir)}`, "utf16le"));
12582
+ const r = h.run("schtasks", ["/Create", "/TN", taskName(id), "/XML", file]);
12583
+ rmSync13(file, { force: true });
12584
+ if (r.status !== 0) return { ok: false, reason: "failed", detail: r.stderr.trim() };
12585
+ mark(id, "schtasks", h);
12586
+ return { ok: true, via: "schtasks" };
12622
12587
  }
12623
- var PREVIEW_NOTE = "Everything except Zed is a preview: new, and may change.";
12624
- function renderChoice(places, chosen, cursor, missing) {
12625
- const lines = [];
12626
- places.forEach((p, i) => {
12627
- const pointer = i === cursor ? ">" : " ";
12628
- const label2 = `${p.name}${p.version ? ` ${p.version}` : ""}${p.surface ? ` (${p.surface})` : ""}`;
12629
- if (p.state === "too-old") {
12630
- lines.push(
12631
- `${pointer} - ${label2}: too old for Rewake (it needs ${p.needs} or newer). Update it: ${p.update ?? "see its own docs"}`
12632
- );
12633
- return;
12634
- }
12635
- const box = chosen.has(p.id) ? "[x]" : "[ ]";
12636
- const note = p.state === "installed" ? ": Rewake is set up here (tick to update)" : "";
12637
- lines.push(`${pointer} ${box} ${label2}${note}`);
12638
- lines.push(` ${p.what}`);
12639
- });
12640
- for (const name of missing) lines.push(` - ${name}: not found.`);
12641
- lines.push("", PREVIEW_NOTE, UNREACHABLE);
12642
- return lines;
12588
+ function timerKind(h) {
12589
+ if (h.platform === "darwin") return launchdTarget(h) ? "launchd" : void 0;
12590
+ if (h.platform === "win32") return "schtasks";
12591
+ if (h.platform === "linux") {
12592
+ if (systemdAvailable(h)) return "systemd";
12593
+ if (atAvailable(h)) return "at";
12594
+ return "waiter";
12595
+ }
12596
+ return void 0;
12643
12597
  }
12644
- function keysHelp(ticked) {
12645
- return `Set up Rewake in the ${ticked === 1 ? "1 ticked place" : `${ticked} ticked places`}? Up and Down move, Space ticks or unticks, a ticks all, Enter continues, q quits.`;
12598
+ function armTimer(id, at, h, gen = 0) {
12599
+ checkId(id);
12600
+ const name = timerName(id, gen);
12601
+ checkId(name);
12602
+ const kind = timerKind(h);
12603
+ if (!kind) return { ok: false, reason: "no-scheduler" };
12604
+ if (gen === 0) cancelTimer(id, h);
12605
+ if (kind === "launchd") return launchdArm(name, at, h);
12606
+ if (kind === "systemd") return systemdArm(name, at, h);
12607
+ if (kind === "at") return atArm(name, at, h);
12608
+ if (kind === "waiter") return waiterArm(name, at, h);
12609
+ return schtasksArm(name, at, h);
12646
12610
  }
12647
- function keyOf(data) {
12648
- if (data === "\x1B[A" || data === "k") return "up";
12649
- if (data === "\x1B[B" || data === "j") return "down";
12650
- if (data === " ") return "toggle";
12651
- if (data === "a") return "all";
12652
- if (data === "\r" || data === "\n") return "enter";
12653
- if (data === "q" || data === "" || data === "\x1B") return "quit";
12654
- return "other";
12611
+ function nextGen(id, current, h) {
12612
+ const used = timerNames(id, h).map((n) => parseTimerName(n).gen);
12613
+ return Math.max(current, ...used) + 1;
12655
12614
  }
12656
- function press(key2, places, chosen, state) {
12657
- const pickable = (i) => places[i]?.state !== "too-old";
12658
- if (key2 === "up") state.cursor = Math.max(0, state.cursor - 1);
12659
- if (key2 === "down") state.cursor = Math.min(places.length - 1, state.cursor + 1);
12660
- if (key2 === "toggle" && pickable(state.cursor)) {
12661
- const id = places[state.cursor]?.id;
12662
- if (id) chosen.has(id) ? chosen.delete(id) : chosen.add(id);
12615
+ function nameArmed(name, kind, h) {
12616
+ if (kind === "launchd") {
12617
+ const domain = launchdTarget(h);
12618
+ return domain !== void 0 && h.run("launchctl", ["print", `${domain}/${label(name)}`]).status === 0;
12663
12619
  }
12664
- if (key2 === "all") {
12665
- for (const p of places) if (p.state !== "too-old") chosen.add(p.id);
12620
+ if (kind === "systemd")
12621
+ return h.run("systemctl", ["--user", "is-active", `${unit(name)}.timer`]).stdout.trim() === "active";
12622
+ if (kind === "at") {
12623
+ const n = atJob(name, h);
12624
+ return n !== void 0 && new RegExp(`^${n}\\s`, "m").test(h.run("atq", []).stdout);
12666
12625
  }
12667
- if (key2 === "enter") return "done";
12668
- if (key2 === "quit") return "quit";
12669
- return void 0;
12626
+ if (kind === "waiter") return waiterHas(name, h);
12627
+ return h.run("schtasks", ["/Query", "/TN", taskName(name)]).status === 0;
12670
12628
  }
12671
- function placesFrom(found, zed, installed, minimum, older, update = () => void 0) {
12672
- const out = [];
12673
- if (zed.found)
12674
- out.push({
12675
- id: "zed",
12676
- name: "Zed",
12677
- ...zed.version && { version: zed.version },
12678
- what: WHAT.zed,
12679
- state: "ready"
12680
- });
12681
- for (const f of found) {
12682
- const min = minimum(f.id);
12683
- const tooOld2 = Boolean(min && f.version && older(f.version, min));
12684
- out.push({
12685
- id: f.id,
12686
- name: f.name,
12687
- ...f.version && { version: f.version },
12688
- what: WHAT[f.id],
12689
- state: tooOld2 ? "too-old" : installed.has(f.id) ? "installed" : "ready",
12690
- ...tooOld2 && min && { needs: min },
12691
- ...tooOld2 && update(f.id) && { update: update(f.id) },
12692
- ...f.surfaces.length > 0 && !f.surfaces.includes("terminal") && { surface: f.surfaces.join(", ") }
12693
- });
12629
+ function timerStale(id, at, h) {
12630
+ checkId(id);
12631
+ const kind = timerKind(h);
12632
+ const dir = join30(h.stateDir, "timers");
12633
+ if (kind === "launchd") {
12634
+ const names = timerNames(id, h).filter((n) => existsSync21(join30(dir, `${label(n)}.plist`)));
12635
+ if (names.length === 0) return false;
12636
+ return !names.some(
12637
+ (n) => readFileSync26(join30(dir, `${label(n)}.plist`), "utf8") === launchdPlist(n, at, h.node, h.cli, h.stateDir)
12638
+ );
12694
12639
  }
12695
- return out;
12696
- }
12697
- async function choosePlaces(places, missing, io) {
12698
- const chosen = defaultChoice(places);
12699
- const state = { cursor: 0 };
12700
- let drawn = 0;
12701
- const draw = () => {
12702
- const lines = [
12703
- ...renderChoice(places, chosen, state.cursor, missing),
12704
- "",
12705
- keysHelp(chosen.size)
12706
- ];
12707
- if (drawn > 0) io.write(`\x1B[${drawn}A`);
12708
- io.write(`${lines.map((l) => `\x1B[2K${l}`).join("\n")}
12709
- `);
12710
- drawn = lines.length;
12711
- };
12712
- draw();
12713
- for await (const data of io.keys) {
12714
- const result = press(keyOf(data), places, chosen, state);
12715
- if (result === "quit") return void 0;
12716
- if (result === "done") return places.filter((p) => chosen.has(p.id)).map((p) => p.id);
12717
- draw();
12640
+ if (kind === "at") {
12641
+ const marks = timerNames(id, h).filter((n) => existsSync21(join30(dir, `${n}.at-time`)));
12642
+ if (marks.length === 0) return false;
12643
+ const want = `${atTime(at)} ${h.node} ${h.cli}`;
12644
+ return !marks.some((n) => readFileSync26(join30(dir, `${n}.at-time`), "utf8") === want);
12718
12645
  }
12719
- return void 0;
12646
+ return false;
12720
12647
  }
12721
-
12722
- // src/mcp.ts
12723
- import { createInterface as createInterface5 } from "node:readline";
12724
- var APPROVAL_TIMEOUT_MS = 10 * 6e4;
12725
- var DELIVERY_NOTES = "Times are the user's local time; a clock time that has passed today means tomorrow, and one-off messages can be at most 30 days ahead. A message is sent only while Zed is running with this project, and after a Zed restart only once the thread has been opened again; the user can work in other threads meanwhile. If a reply is in progress at that time, it's sent right after the reply. If Zed or the computer wasn't running, a one-off message more than 15 minutes late isn't sent: it's marked missed and the user is asked whether to send it; a repeating message skips that run. For anything else, call about_rewake.";
12726
- var ENDS = {
12727
- ends_after_runs: {
12728
- type: "integer",
12729
- minimum: 1,
12730
- description: "Repeating only: stop after this many runs."
12731
- },
12732
- ends_at: {
12733
- type: "string",
12734
- description: 'Repeating only: no runs after this time, e.g. "2026-10-31 18:00" (local time).'
12735
- }
12736
- };
12737
- var TOOLS = [
12738
- {
12739
- name: "schedule_message",
12740
- description: `Schedule a message to be sent into this conversation later, as if the user typed it. Use it when the user asks you to schedule something, or to schedule a follow-up for yourself (for example to check on a long build). The user must approve it in the thread, and may edit the message first; this call waits for that answer and returns what was saved: number, exact time, repeat, message. Give either \`when\` (once) or \`cron\` (repeating); a repeat runs until cancelled unless you give an end. ${DELIVERY_NOTES}`,
12741
- inputSchema: {
12742
- type: "object",
12743
- properties: {
12744
- message: { type: "string", description: "The message to send at that time." },
12745
- when: {
12746
- type: "string",
12747
- description: 'For a one-off: "in 30m", "in 2h", "09:00", "tomorrow 09:00" or "2026-10-06 09:00" (local time).'
12748
- },
12749
- cron: {
12750
- type: "string",
12751
- description: 'For a repeating message: a 5-field cron expression in local time, e.g. "0 9 * * 1-5" (weekdays at 09:00), or @hourly, @daily, @weekly.'
12752
- },
12753
- ...ENDS,
12754
- reason: { type: "string", description: "One sentence the user sees: why you want this." }
12755
- },
12756
- required: ["message"]
12757
- }
12758
- },
12759
- {
12760
- name: "about_rewake",
12761
- description: "How Agent Rewake works, in detail: what it does, the Rewake menu and commands, settings, when a scheduled message is and isn't sent (Zed closed, thread closed or not open, another thread in use, computer asleep, late, busy, several windows), usage limits and automatic resume, approvals, statuses and data. Call it before answering any question the user asks about Rewake, and answer from it.",
12762
- inputSchema: { type: "object", properties: {} }
12763
- },
12764
- {
12765
- name: "list_scheduled_messages",
12766
- description: "List the messages scheduled in this conversation, numbered, with the current time, each one's exact next run, status, repeat and end, how its last run went, and messages finished in the last 24 hours.",
12767
- inputSchema: { type: "object", properties: {} }
12768
- },
12769
- {
12770
- name: "update_scheduled_message",
12771
- description: `Change a message scheduled in this conversation, by its number from list_scheduled_messages: its message, its time, its repeat or end, or pause or resume it. Give only what changes. The user must approve it in the thread, and may edit the message; this call waits and returns the message as saved. ${DELIVERY_NOTES}`,
12772
- inputSchema: {
12773
- type: "object",
12774
- properties: {
12775
- number: { type: "integer", minimum: 1 },
12776
- message: { type: "string", description: "The new message." },
12777
- when: {
12778
- type: "string",
12779
- description: "A new time for the next run (as in schedule_message)."
12780
- },
12781
- cron: { type: "string", description: "A new repeat (replaces the old one)." },
12782
- stop_repeating: {
12783
- type: "boolean",
12784
- description: "true: send it once more at its next run, then stop."
12785
- },
12786
- ...ENDS,
12787
- paused: { type: "boolean", description: "true pauses it; false resumes it." },
12788
- reason: { type: "string", description: "One sentence the user sees: why." }
12789
- },
12790
- required: ["number"]
12791
- }
12792
- },
12793
- {
12794
- name: "cancel_scheduled_message",
12795
- description: "Delete a message scheduled in this conversation, by its number from list_scheduled_messages. The user must approve it in the thread; this call waits for that answer.",
12796
- inputSchema: {
12797
- type: "object",
12798
- properties: {
12799
- number: { type: "integer", minimum: 1 },
12800
- reason: { type: "string", description: "One sentence the user sees: why." }
12801
- },
12802
- required: ["number"]
12648
+ function timerArmed(id, h) {
12649
+ checkId(id);
12650
+ const kind = timerKind(h);
12651
+ if (!kind) return false;
12652
+ return timerNames(id, h).some((name) => nameArmed(name, kind, h));
12653
+ }
12654
+ function cancelTimer(id, h, fromInsideTimer = false, keep) {
12655
+ checkId(id);
12656
+ const kind = timerKind(h);
12657
+ for (const name of timerNames(id, h)) {
12658
+ if (name === keep) continue;
12659
+ const dir = join30(h.stateDir, "timers");
12660
+ if (kind === "launchd") {
12661
+ const plist = join30(dir, `${label(name)}.plist`);
12662
+ const had = existsSync21(plist);
12663
+ rmSync13(plist, { force: true });
12664
+ const domain = launchdTarget(h);
12665
+ if (!domain || !had && name !== id) continue;
12666
+ if (fromInsideTimer)
12667
+ h.detached("/bin/sh", ["-c", `sleep 2; launchctl bootout ${domain}/${label(name)}`]);
12668
+ else h.run("launchctl", ["bootout", `${domain}/${label(name)}`]);
12669
+ } else if (kind === "systemd") {
12670
+ h.run("systemctl", ["--user", "stop", `${unit(name)}.timer`]);
12671
+ rmSync13(join30(dir, `${name}.systemd`), { force: true });
12672
+ } else if (kind === "at") atCancel(name, h);
12673
+ else if (kind === "waiter") rmSync13(join30(dir, `${name}.wait`), { force: true });
12674
+ else if (kind === "schtasks") {
12675
+ h.run("schtasks", ["/Delete", "/TN", taskName(name), "/F"]);
12676
+ rmSync13(join30(dir, `${name}.task`), { force: true });
12803
12677
  }
12804
12678
  }
12805
- ];
12806
- function runMcp(opts, input = process.stdin, output = process.stdout) {
12807
- const send = (m) => output.write(`${JSON.stringify({ jsonrpc: "2.0", ...m })}
12808
- `);
12809
- const rl = createInterface5({ input });
12810
- rl.on("line", (line) => {
12811
- let m;
12679
+ }
12680
+
12681
+ // src/timers/waiter.ts
12682
+ var WAITER_POLL_MS = 3e4;
12683
+ var WAIT = /^([a-z0-9-]{1,64})\.wait$/;
12684
+ function due(dir) {
12685
+ let files = [];
12686
+ try {
12687
+ files = readdirSync12(dir);
12688
+ } catch {
12689
+ return [];
12690
+ }
12691
+ return files.flatMap((f) => {
12692
+ const name = WAIT.exec(f)?.[1];
12693
+ if (!name) return [];
12812
12694
  try {
12813
- m = JSON.parse(line);
12695
+ const at = Number(readFileSync27(join31(dir, f), "utf8").trim());
12696
+ return Number.isFinite(at) ? [{ name, at }] : [];
12814
12697
  } catch {
12815
- return;
12816
- }
12817
- if (m.id === void 0 || m.id === null) return;
12818
- const reply = (result) => send({ id: m.id, result });
12819
- switch (m.method) {
12820
- case "initialize":
12821
- reply({
12822
- protocolVersion: typeof m.params?.protocolVersion === "string" ? m.params.protocolVersion : "2025-06-18",
12823
- capabilities: { tools: {} },
12824
- serverInfo: { name: "agent-rewake", version: VERSION2 },
12825
- instructions: "Agent Rewake resumes this conversation after usage limits and schedules messages into it. The user approves every schedule, change and cancellation in the thread. When the user asks anything about Rewake (how it works, what happens if Zed or the thread is closed, settings, limits), call about_rewake first and answer from it."
12826
- });
12827
- return;
12828
- case "ping":
12829
- reply({});
12830
- return;
12831
- case "tools/list":
12832
- reply({ tools: TOOLS });
12833
- return;
12834
- case "tools/call":
12835
- void callTool(
12836
- opts,
12837
- String(m.params?.name ?? ""),
12838
- m.params?.arguments ?? {}
12839
- ).then(reply).catch((err) => reply(text(`Agent Rewake: ${err.message}`, true)));
12840
- return;
12841
- default:
12842
- send({ id: m.id, error: { code: -32601, message: `Unknown method ${m.method}` } });
12698
+ return [];
12843
12699
  }
12844
12700
  });
12845
- return new Promise((resolve2) => rl.on("close", () => resolve2(0)));
12846
12701
  }
12847
- function aboutRewake(opts, sessionId, now) {
12848
- const settings = loadSettings(opts.stateDir);
12849
- const auto = {
12850
- off: "Off",
12851
- ask: "Ask when a new thread opens",
12852
- on: settings.autoWhenPromptsSkipped ? "On, even when permissions are bypassed" : "On, except when permissions are bypassed"
12853
- }[settings.newThreads];
12854
- const lines = [
12855
- "## Right now",
12856
- "",
12857
- `- Current time: ${formatExact(now, opts.locale)}.`,
12858
- `- Agent Rewake version: ${VERSION2}.`,
12859
- `- Settings: automatic resume for new threads: ${auto}${settings.newThreads !== "on" ? `; threads that bypass permissions ${settings.autoWhenPromptsSkipped ? "included" : "excluded"}` : ""}. Time format: ${settings.clock === "24h" ? "24-hour" : "12-hour"}.`
12860
- ];
12861
- if (sessionId) {
12862
- const thread = new ThreadStore(opts.stateDir).get(sessionId);
12863
- const pending = new ScheduleStore(opts.stateDir).listForSession(sessionId).filter((s) => !TERMINAL_STATUSES.has(s.status));
12864
- lines.push(
12865
- `- This thread: automatic resume after usage limits is ${thread?.autoResume ? "on" : "off"}; ${pending.length === 0 ? "nothing is scheduled" : `${pending.length} scheduled ${pending.length === 1 ? "message" : "messages"} (list_scheduled_messages shows them)`}.`
12866
- );
12702
+ async function runWaiter(d) {
12703
+ const dir = ensurePrivateDir(join31(d.timers.stateDir, "timers"));
12704
+ const pidFile = join31(dir, "waiter.pid");
12705
+ const other = waiterAlive(d.timers);
12706
+ if (other !== void 0 && other !== d.pid) return 0;
12707
+ writeFileSync6(pidFile, String(d.pid), { mode: 384 });
12708
+ const mine = () => {
12709
+ try {
12710
+ return readFileSync27(pidFile, "utf8").trim() === String(d.pid);
12711
+ } catch {
12712
+ return false;
12713
+ }
12714
+ };
12715
+ for (; ; ) {
12716
+ if (!mine()) return 0;
12717
+ const waits = due(dir);
12718
+ const now = d.now();
12719
+ for (const w of waits.filter((x) => x.at <= now)) {
12720
+ rmSync14(join31(dir, `${w.name}.wait`), { force: true });
12721
+ d.fire(w.name);
12722
+ }
12723
+ const left = waits.filter((x) => x.at > now);
12724
+ if (left.length === 0) {
12725
+ rmSync14(pidFile, { force: true });
12726
+ if (due(dir).length === 0) return 0;
12727
+ writeFileSync6(pidFile, String(d.pid), { mode: 384 });
12728
+ continue;
12729
+ }
12730
+ const next = Math.min(...left.map((x) => x.at));
12731
+ await d.sleep(Math.max(1e3, Math.min(WAITER_POLL_MS, next - now)));
12867
12732
  }
12868
- return `${AGENT_GUIDE}
12869
- ${lines.join("\n")}
12870
- `;
12871
12733
  }
12872
- function text(t, isError = false) {
12873
- return { content: [{ type: "text", text: t }], ...isError && { isError: true } };
12734
+ function isWsl(version = readProcVersion()) {
12735
+ return /microsoft/i.test(version);
12874
12736
  }
12875
- async function callTool(opts, name, args) {
12876
- const now = opts.now ?? Date.now;
12877
- applySettings(opts.stateDir);
12878
- const link = opts.link ? new LinkStore(opts.stateDir).get(opts.link) : void 0;
12879
- if (name === "about_rewake") return text(aboutRewake(opts, link?.sessionId, now()));
12880
- if (!link)
12881
- return text(
12882
- "Agent Rewake isn't linked to this conversation yet. Ask the user to reopen the thread.",
12883
- true
12884
- );
12885
- const schedules = new ScheduleStore(opts.stateDir);
12886
- const requests = new RequestStore(opts.stateDir);
12887
- const pending = () => schedules.listForSession(link.sessionId).filter((s) => !TERMINAL_STATUSES.has(s.status));
12888
- const exact = (t) => formatExact(t, opts.locale);
12889
- const clockLine = () => `Current time: ${exact(now())}.`;
12890
- const reason = () => typeof args.reason === "string" && args.reason.trim() ? { reason: args.reason.trim() } : {};
12891
- const byNumber = () => {
12892
- const n = Number(args.number);
12893
- return Number.isInteger(n) && n >= 1 ? pending()[n - 1] : void 0;
12894
- };
12895
- const ends = () => {
12896
- const out = {};
12897
- if (args.ends_after_runs !== void 0) {
12898
- const n = Number(args.ends_after_runs);
12899
- if (!Number.isInteger(n) || n < 1)
12900
- return "`ends_after_runs` must be a whole number, 1 or more.";
12901
- out.times = n;
12902
- }
12903
- if (typeof args.ends_at === "string" && args.ends_at.trim()) {
12904
- const t = parseWhen(args.ends_at, now());
12905
- if (!t.ok) return `ends_at: ${t.error}`;
12906
- out.until = t.at;
12907
- }
12908
- return out;
12909
- };
12910
- const describe2 = (sid) => {
12911
- const list = pending();
12912
- const i = list.findIndex((x) => x.scheduleId === sid);
12913
- const s = list[i] ?? schedules.get(sid);
12914
- if (!s) return "";
12915
- const p = s.repeat ? parseCron(s.repeat.cron) : void 0;
12916
- const end = [
12917
- s.repeat?.remaining !== void 0 ? `${s.repeat.remaining} run${s.repeat.remaining === 1 ? "" : "s"} left` : "",
12918
- s.repeat?.until !== void 0 ? `no runs after ${exact(s.repeat.until)}` : ""
12919
- ].filter(Boolean);
12920
- const repeat2 = p?.ok ? `Repeats: ${describeCron(p.cron)}${end.length ? ` (${end.join(", ")})` : " (until cancelled)"}` : "Once";
12921
- const last = s.lastRun ? ` \xB7 Last run: ${s.lastRun.outcome}, due ${exact(s.lastRun.at)}` : "";
12922
- const by = s.createdBy === "agent" ? "you asked, the user approved" : s.kind === "user" ? "the user" : "the user (resume after a usage limit)";
12923
- return [
12924
- `${i >= 0 ? `${i + 1}. ` : ""}Next: ${exact(s.dueAt)} \xB7 ${formatWhen(s.dueAt, now(), opts.locale)}`,
12925
- ` Status: ${s.status} \xB7 ${repeat2}${last} \xB7 Added by: ${by}`,
12926
- ` Message: ${s.text}`
12927
- ].join("\n");
12928
- };
12929
- if (name === "list_scheduled_messages") {
12930
- const list = pending();
12931
- const recent = schedules.listForSession(link.sessionId).filter((s) => TERMINAL_STATUSES.has(s.status) && now() - s.updatedAt < 24 * 36e5);
12932
- const out = [clockLine()];
12933
- out.push(
12934
- list.length === 0 ? "Nothing is scheduled in this conversation." : list.map((s) => describe2(s.scheduleId)).join("\n")
12935
- );
12936
- if (recent.length > 0)
12937
- out.push(
12938
- "Finished in the last 24 hours:",
12939
- ...recent.map((s) => `- ${s.status}: due ${exact(s.dueAt)} \xB7 ${s.text}`)
12940
- );
12941
- return text(out.join("\n"));
12737
+ function readProcVersion() {
12738
+ try {
12739
+ return readFileSync27("/proc/version", "utf8");
12740
+ } catch {
12741
+ return "";
12942
12742
  }
12943
- const settle = async (requestId) => {
12944
- const r = await wait(opts, requests, requestId);
12945
- if (r.status !== "approved") return text(`${r.text}
12946
- ${clockLine()}`, r.isError);
12947
- const saved = r.scheduleId ? describe2(r.scheduleId) : "";
12948
- return text([r.text, saved && `As saved:
12949
- ${saved}`, clockLine()].filter(Boolean).join("\n"));
12743
+ }
12744
+ function waiterNote(wsl) {
12745
+ return {
12746
+ text: "Planned resumes run at their times while this computer stays on. With no systemd or at service here, Rewake runs them with its own background process, which stops when you log out or restart and starts again the next time you use an agent. A resume missed by more than half an hour isn't sent.",
12747
+ fix: wsl ? "So resumes don't wait for you after a restart, turn on systemd in WSL: add the lines [boot] and systemd=true to /etc/wsl.conf, then run wsl.exe --shutdown in Windows and open WSL again." : "So resumes don't wait for you after a restart, install and start the at service (Debian, Ubuntu: sudo apt install at, then sudo service atd start)."
12950
12748
  };
12951
- if (name === "schedule_message") {
12952
- const message = typeof args.message === "string" ? args.message.trim() : "";
12953
- if (!message) return text("Give the message to send.", true);
12954
- const when2 = typeof args.when === "string" ? args.when.trim() : "";
12955
- const cronText = typeof args.cron === "string" ? args.cron.trim() : "";
12956
- if (!when2 === !cronText)
12957
- return text("Give either `when` (once) or `cron` (repeating), not both.", true);
12958
- const end = ends();
12959
- if (typeof end === "string") return text(end, true);
12960
- if (!cronText && (end.until !== void 0 || end.times !== void 0))
12961
- return text(
12962
- "`ends_after_runs` and `ends_at` are only for repeating messages (`cron`).",
12963
- true
12964
- );
12965
- let dueAt;
12966
- let cron;
12967
- if (cronText) {
12968
- const parsed = parseCron(cronText);
12969
- if (!parsed.ok) return text(parsed.error, true);
12970
- const first = nextRun(parsed.cron, now());
12971
- if (first === void 0) return text(`"${cronText}" never runs.`, true);
12972
- if (end.until !== void 0 && first > end.until)
12973
- return text(`It would never run: the first run, ${exact(first)}, is after ends_at.`, true);
12974
- dueAt = first;
12975
- cron = parsed.cron.source;
12976
- } else {
12977
- const parsed = parseWhen(when2, now());
12978
- if (!parsed.ok) return text(`${parsed.error} ${clockLine()}`, true);
12979
- dueAt = parsed.at;
12980
- }
12981
- const same2 = (t) => t.trim().toLowerCase();
12982
- const twin = pending().find((s) => same2(s.text) === same2(message));
12983
- const r = requests.create({
12984
- sessionId: link.sessionId,
12985
- kind: "schedule",
12986
- message,
12987
- dueAt,
12988
- ...cron && { cron },
12989
- ...end,
12990
- ...twin && { duplicateOf: twin.scheduleId },
12991
- ...reason(),
12992
- createdAt: now()
12993
- });
12994
- return settle(r.requestId);
12749
+ }
12750
+
12751
+ // src/hosts/claude-code/records.ts
12752
+ import { readdirSync as readdirSync13, readFileSync as readFileSync28 } from "node:fs";
12753
+ import { join as join32 } from "node:path";
12754
+ var STALE_MS = 2 * 24 * 60 * 60 * 1e3;
12755
+ function claudeCodeRecords(stateDir2, now) {
12756
+ const dir = join32(stateDir2, "hosts", "claude-code", "sessions");
12757
+ let names;
12758
+ try {
12759
+ names = readdirSync13(dir).filter((n) => n.endsWith(".json"));
12760
+ } catch {
12761
+ return [];
12995
12762
  }
12996
- if (name === "update_scheduled_message") {
12997
- const target = byNumber();
12998
- if (!target)
12999
- return text(
13000
- `There's no scheduled message number ${args.number}. Use list_scheduled_messages.`,
13001
- true
13002
- );
13003
- const end = ends();
13004
- if (typeof end === "string") return text(end, true);
13005
- const change = { ...end };
13006
- if (typeof args.message === "string" && args.message.trim())
13007
- change.message = args.message.trim();
13008
- if (typeof args.when === "string" && args.when.trim()) {
13009
- const t = parseWhen(args.when, now());
13010
- if (!t.ok) return text(`${t.error} ${clockLine()}`, true);
13011
- change.dueAt = t.at;
13012
- }
13013
- if (typeof args.cron === "string" && args.cron.trim()) {
13014
- const parsed = parseCron(args.cron);
13015
- if (!parsed.ok) return text(parsed.error, true);
13016
- if (change.dueAt !== void 0)
13017
- return text(
13018
- "Give either `when` or `cron`, not both: a new repeat sets its own next run.",
13019
- true
13020
- );
13021
- change.cron = parsed.cron.source;
12763
+ const out = [];
12764
+ for (const n of names) {
12765
+ try {
12766
+ const r = JSON.parse(readFileSync28(join32(dir, n), "utf8"));
12767
+ if (r.host !== "claude-code" || typeof r.sessionId !== "string") continue;
12768
+ if (typeof r.state !== "string" || typeof r.updatedAt !== "number") continue;
12769
+ if (now - r.updatedAt > STALE_MS) continue;
12770
+ out.push({
12771
+ sessionId: r.sessionId,
12772
+ state: r.state,
12773
+ updatedAt: r.updatedAt,
12774
+ ...typeof r.cwd === "string" && { cwd: r.cwd },
12775
+ ...typeof r.resetAt === "number" && { resetAt: r.resetAt },
12776
+ ...typeof r.fireAt === "number" && { fireAt: r.fireAt }
12777
+ });
12778
+ } catch {
13022
12779
  }
13023
- if (args.stop_repeating === true) change.stopRepeating = true;
13024
- if (typeof args.paused === "boolean") change.paused = args.paused;
13025
- if ((change.until !== void 0 || change.times !== void 0) && !target.repeat && change.cron === void 0)
13026
- return text("That message doesn't repeat, so it has no end to set.", true);
13027
- if (Object.keys(change).length === 0) return text("Give at least one thing to change.", true);
13028
- const r = requests.create({
13029
- sessionId: link.sessionId,
13030
- kind: "update",
13031
- scheduleId: target.scheduleId,
13032
- ...change,
13033
- ...reason(),
13034
- createdAt: now()
13035
- });
13036
- return settle(r.requestId);
13037
12780
  }
13038
- if (name === "cancel_scheduled_message") {
13039
- const target = byNumber();
13040
- if (!target)
13041
- return text(
13042
- `There's no scheduled message number ${args.number}. Use list_scheduled_messages.`,
13043
- true
13044
- );
13045
- const r = requests.create({
13046
- sessionId: link.sessionId,
13047
- kind: "cancel",
13048
- scheduleId: target.scheduleId,
13049
- ...reason(),
13050
- createdAt: now()
12781
+ return out;
12782
+ }
12783
+
12784
+ // src/hosts/doctor.ts
12785
+ var DAY2 = 24 * 60 * 60 * 1e3;
12786
+ function diagnoseOutside(f) {
12787
+ const out = [];
12788
+ const add = (x) => out.push({ area: "Outside Zed", ...x });
12789
+ const setUp = new Set(f.previews.map((p) => p.id));
12790
+ for (const a of f.agents ?? []) {
12791
+ const v = AGENT_VERSIONS[a.id];
12792
+ if (!v || !a.version) continue;
12793
+ if (tooOld(a.id, a.version)) {
12794
+ const set = setUp.has(a.id);
12795
+ add({
12796
+ level: set ? "problem" : "info",
12797
+ text: `${v.name} ${a.version} is too old for Rewake (it needs ${v.min} or newer), so ${set ? "Rewake may miss its usage limits" : "Rewake isn't set up for it"}.`,
12798
+ fix: `Update it with: ${v.update}${set ? "" : `, then run: agent-rewake install --only ${a.id}`}`
12799
+ });
12800
+ } else if (setUp.has(a.id) && newerThanTested(a.id, a.version))
12801
+ add({ level: "info", text: untestedText(a.id, a.version) });
12802
+ }
12803
+ if (f.previews.length === 0) return out;
12804
+ for (const p of f.previews) {
12805
+ const off = hooksTurnedOff(p.id, f.env, f.home);
12806
+ if (off)
12807
+ add({
12808
+ level: "problem",
12809
+ text: `${p.name} has its hooks turned off, so Rewake can't see its usage limits.`,
12810
+ fix: `Turn hooks back on in ${p.name}'s settings, or take Rewake out of it: agent-rewake uninstall --only ${p.id}`
12811
+ });
12812
+ }
12813
+ if (!f.hasTimer)
12814
+ add({
12815
+ level: "problem",
12816
+ text: "Rewake can't continue sessions outside Zed by itself on this computer: it has no scheduler Rewake can use.",
12817
+ fix: f.platform === "linux" ? "Sign in to a normal desktop session, or install and start the at service." : "Run agent-rewake doctor again after a restart; if it stays, report it with agent-rewake doctor --details."
12818
+ });
12819
+ if (f.timerKind === "waiter") add({ level: "info", ...waiterNote(f.wsl ?? false) });
12820
+ const resumes = new ScheduleStore(f.stateDir).list().filter((s) => s.host !== void 0);
12821
+ const name = (host) => host && f.hosts.get(host)?.name || (host === "claude-code" ? "Claude Code" : host) || "";
12822
+ const cc = setUp.has("claude-code") ? claudeCodeRecords(f.stateDir, f.now) : [];
12823
+ const upcoming = [
12824
+ ...resumes.filter((s) => s.status === "scheduled" || s.status === "sending"),
12825
+ ...cc.filter((r) => r.state === "armed" && r.fireAt !== void 0 && r.fireAt > f.now - 5 * 6e4).map((r) => ({ host: "claude-code", dueAt: r.fireAt }))
12826
+ ].sort((a, b) => a.dueAt - b.dueAt);
12827
+ const asking = cc.filter((r) => r.state === "offered" || r.state === "waiting");
12828
+ const needsYou = resumes.filter((s) => s.status === "needs_attention");
12829
+ const recent = (s) => s.dueAt > f.now - 7 * DAY2;
12830
+ const missed = resumes.filter((s) => s.status === "missed" && recent(s));
12831
+ const failed = resumes.filter((s) => s.status === "failed" && recent(s));
12832
+ const n = (k, one) => `${k} ${one}${k === 1 ? "" : "s"}`;
12833
+ const next = upcoming[0];
12834
+ if (next)
12835
+ add({
12836
+ level: "ok",
12837
+ text: `${n(upcoming.length, "planned resume")}; the next one continues ${name(next.host)} ${next.dueAt <= f.now ? "now" : f.when(next.dueAt, f.now)}. Keep this computer on and awake${next.host === "claude-code" ? ", and Claude Code open," : ""} then.`
12838
+ });
12839
+ if (asking.length > 0) {
12840
+ const where2 = (r) => r.cwd ? ` (in the "${r.cwd.split(/[\\/]/).filter(Boolean).at(-1) ?? r.cwd}" folder)` : "";
12841
+ add({
12842
+ level: "todo",
12843
+ text: asking.length === 1 ? `1 Claude Code session${where2(asking[0])} is waiting for your answer: continue it after the reset?` : `${asking.length} Claude Code sessions are waiting for your answer: continue them after the reset?`,
12844
+ fix: "Answer Rewake's question there, or type /rewake."
13051
12845
  });
13052
- return settle(r.requestId);
13053
12846
  }
13054
- return text(`Unknown tool ${name}.`, true);
12847
+ if (needsYou.length > 0)
12848
+ add({
12849
+ level: "todo",
12850
+ text: `${n(needsYou.length, "resume")} need${needsYou.length === 1 ? "s" : ""} you.`,
12851
+ fix: `Review ${needsYou.length === 1 ? "it" : "them"} with: agent-rewake ui`
12852
+ });
12853
+ if (missed.length > 0)
12854
+ add({
12855
+ level: "info",
12856
+ text: `${n(missed.length, "resume")} ${missed.length === 1 ? "was" : "were"} missed in the last 7 days (the computer was off or asleep at the time).`
12857
+ });
12858
+ const lastFailed = [...failed].sort((a, b) => b.updatedAt - a.updatedAt)[0];
12859
+ if (failed.length > 0)
12860
+ add({
12861
+ level: "info",
12862
+ text: `${n(failed.length, "resume")} failed in the last 7 days.${lastFailed?.failureMessage ? ` The last one, in ${name(lastFailed.host)}, ended with: "${lastFailed.failureMessage}"` : ""}`,
12863
+ fix: lastFailed?.failureMessage ? "See all of them with: agent-rewake ui" : "See why with: agent-rewake ui"
12864
+ });
12865
+ if (out.length === 0)
12866
+ add({
12867
+ level: "ok",
12868
+ text: `Set up in ${f.previews.map((p) => p.name).join(", ")}. Nothing planned right now.`
12869
+ });
12870
+ return out;
13055
12871
  }
13056
- async function wait(opts, requests, requestId) {
13057
- const deadline = Date.now() + (opts.timeoutMs ?? APPROVAL_TIMEOUT_MS);
13058
- while (Date.now() < deadline) {
13059
- const r = requests.get(requestId);
13060
- if (!r)
13061
- return {
13062
- status: "declined",
13063
- text: "The request disappeared; nothing was changed.",
13064
- isError: true
13065
- };
13066
- if (r.status !== "pending") {
13067
- requests.remove(requestId);
12872
+
12873
+ // src/hosts/index.ts
12874
+ import { homedir as homedir18 } from "node:os";
12875
+
12876
+ // src/hosts/codex/adapter.ts
12877
+ function programOf(s, node) {
12878
+ const path = s.sessionRef?.codex;
12879
+ return path ? codexProgram(path, node) : void 0;
12880
+ }
12881
+ function envOf(s, base) {
12882
+ const home = s.sessionRef?.codexHome;
12883
+ return home ? { ...base, CODEX_HOME: home } : base;
12884
+ }
12885
+ function codexAdapter(deps) {
12886
+ const usage2 = deps.readUsage ?? ((codex2, env) => readUsage(codex2, env));
12887
+ const queue = deps.queue ?? queueMessage;
12888
+ const signedOut = /* @__PURE__ */ new Set();
12889
+ const unanswered2 = /* @__PURE__ */ new Set();
12890
+ return {
12891
+ id: "codex",
12892
+ name: "Codex",
12893
+ noun: "thread",
12894
+ reopen: "resume the thread in Codex",
12895
+ again: (at) => at ? `Resume the thread in Codex and type "rewake", and Rewake continues it ${at}.` : 'Resume the thread in Codex and type "rewake" with a time, for example "rewake 3:30pm".',
12896
+ async check(s, now) {
12897
+ const facts = {};
12898
+ const transcript = s.sessionRef?.transcript;
12899
+ let limit;
12900
+ if (transcript) {
12901
+ try {
12902
+ limit = findCodexLimit(readTail2(transcript), now);
12903
+ facts.userTypedSince = !limit.limited;
12904
+ } catch {
12905
+ }
12906
+ }
12907
+ const codex2 = programOf(s, deps.node);
12908
+ let noAnswer = false;
12909
+ if (codex2) {
12910
+ const u = await usage2(codex2, envOf(s, deps.env));
12911
+ if (u.ok) {
12912
+ if (u.allowed !== void 0) facts.usageAllowed = u.allowed;
12913
+ if (u.allowed === false && u.resetsAt && u.resetsAt > now) facts.newResetsAt = u.resetsAt;
12914
+ } else if (u.reason === "signed-out") signedOut.add(s.scheduleId);
12915
+ else if (u.reason === "timeout" || u.reason === "error") noAnswer = true;
12916
+ }
12917
+ if (facts.usageAllowed === void 0 && limit?.limited && limit.resetsAt !== void 0 && limit.resetsAt > now) {
12918
+ facts.usageAllowed = false;
12919
+ facts.newResetsAt = limit.resetsAt;
12920
+ }
12921
+ unanswered2.delete(s.scheduleId);
12922
+ if (facts.usageAllowed === void 0 && noAnswer) {
12923
+ if ((s.rearms ?? 0) < MAX_REARMS) facts.usageAllowed = false;
12924
+ else unanswered2.add(s.scheduleId);
12925
+ }
12926
+ return facts;
12927
+ },
12928
+ delivered(s) {
12929
+ const transcript = s.sessionRef?.transcript;
12930
+ if (!transcript) return void 0;
12931
+ try {
12932
+ return userMessages(readTail2(transcript, 512 * 1024)).includes(s.text) || void 0;
12933
+ } catch {
12934
+ return void 0;
12935
+ }
12936
+ },
12937
+ async send(s) {
12938
+ if (signedOut.has(s.scheduleId)) return { ok: false, reason: "failed", detail: "signed-out" };
12939
+ if (unanswered2.has(s.scheduleId)) return { ok: false, reason: "failed", detail: "no-usage" };
12940
+ const codex2 = programOf(s, deps.node);
12941
+ const thread = s.sessionRef?.threadId;
12942
+ if (!codex2 || !thread) return { ok: false, reason: "unsupported", detail: "no Codex found" };
12943
+ const env = envOf(s, deps.env);
12944
+ let r = await queue(codex2, thread, s.text, env);
12945
+ if (!r.ok && r.reason === "daemon") r = await queue(codex2, thread, s.text, env, "unix://");
12946
+ if (r.ok) return { ok: true };
13068
12947
  return {
13069
- status: r.status,
13070
- text: r.answer ?? (r.status === "approved" ? "The user approved it." : "The user declined."),
13071
- ...r.scheduleId && r.kind !== "cancel" && { scheduleId: r.scheduleId }
12948
+ ok: false,
12949
+ reason: r.reason === "archived" || r.reason === "deleted" ? "closed" : "failed",
12950
+ detail: r.reason
13072
12951
  };
13073
12952
  }
13074
- await new Promise((res) => setTimeout(res, opts.pollMs ?? 500));
13075
- }
13076
- requests.remove(requestId);
13077
- return {
13078
- status: "declined",
13079
- text: "The user didn't answer within 10 minutes, so nothing was changed."
13080
12953
  };
13081
12954
  }
13082
12955
 
13083
- // src/timers/fire.ts
13084
- import { basename as basename7 } from "node:path";
13085
-
13086
- // src/timers/timers.ts
13087
- import { spawn as spawn8, spawnSync as spawnSync8 } from "node:child_process";
13088
- import { existsSync as existsSync20, readdirSync as readdirSync11, readFileSync as readFileSync25, rmSync as rmSync13, writeFileSync as writeFileSync5 } from "node:fs";
13089
- import { join as join29 } from "node:path";
13090
- var ID = /^[a-z0-9-]{1,64}$/;
13091
- function defaultTimerHost(stateDir2, node, cli) {
13092
- return {
13093
- platform: process.platform,
13094
- stateDir: stateDir2,
13095
- node,
13096
- cli,
13097
- run: (command, args, input) => {
13098
- const r = spawnSync8(command, args, {
13099
- encoding: "utf8",
13100
- timeout: 5e3,
13101
- windowsHide: true,
13102
- ...input !== void 0 && { input }
13103
- });
13104
- return { status: r.status, stdout: r.stdout ?? "", stderr: r.stderr ?? "" };
13105
- },
13106
- detached: (command, args) => {
12956
+ // src/hosts/codex/hooks.ts
12957
+ import { basename as basename6 } from "node:path";
12958
+ var CODEX_PLACE = { name: "Codex", typed: "rewake", features: /* @__PURE__ */ new Set() };
12959
+ var REWAKE = /^\s*\/?rewake(?:\s+([\s\S]*?))?\s*$/i;
12960
+ function threadOf(input) {
12961
+ const path = input.transcript_path;
12962
+ if (!isCodexRollout(path)) return void 0;
12963
+ const id = threadIdOf(path);
12964
+ return id ? { id, path } : void 0;
12965
+ }
12966
+ function pendingFor2(store, threadId) {
12967
+ return store.listForSession(threadId, "codex").filter(
12968
+ (s) => !TERMINAL_STATUSES.has(s.status) && !["missed", "needs_attention"].includes(s.status)
12969
+ );
12970
+ }
12971
+ function armedText2(at, now) {
12972
+ return `Rewake will continue this thread ${formatAt(at, now)}. Keep this computer on and awake until then. If Codex is closed at that time, the thread continues when you next open it. To cancel, send any other message in the thread.`;
12973
+ }
12974
+ function noTimerText(at, now) {
12975
+ return `Rewake couldn't set a timer on this computer, so it can't continue this thread by itself. After the limit resets ${formatAt(at, now)}, send a message in the thread to continue.`;
12976
+ }
12977
+ var block = (reason) => JSON.stringify({ decision: "block", reason });
12978
+ function codexHooks(deps) {
12979
+ const arm = (ctx, thread, at) => {
12980
+ const store = new ScheduleStore(ctx.stateDir);
12981
+ const settings = loadSettings(ctx.stateDir);
12982
+ const cwd = typeof ctx.input.cwd === "string" ? ctx.input.cwd : "";
12983
+ const codex2 = deps.codexPath();
12984
+ const s = store.create({
12985
+ sessionId: thread.id,
12986
+ cwd,
12987
+ text: settings.resumePrompt ?? DEFAULT_RESUME_PROMPT,
12988
+ dueAt: at,
12989
+ kind: "limit_resume",
12990
+ createdBy: "agent",
12991
+ now: ctx.now
12992
+ });
12993
+ const home = ctx.env.CODEX_HOME;
12994
+ const resume = {
12995
+ ...s,
12996
+ host: "codex",
12997
+ sessionRef: {
12998
+ threadId: thread.id,
12999
+ transcript: thread.path,
13000
+ ...codex2 && { codex: codex2 },
13001
+ ...home && { codexHome: home }
13002
+ }
13003
+ };
13004
+ store.put(resume);
13005
+ if (deps.arm(resume.scheduleId, at) !== false) return true;
13006
+ store.update(resume.scheduleId, (x) => ({ ...x, status: "cancelled" }), ctx.now);
13007
+ return false;
13008
+ };
13009
+ const cancelPending = (ctx, threadId) => {
13010
+ const store = new ScheduleStore(ctx.stateDir);
13011
+ for (const s of pendingFor2(store, threadId)) {
13012
+ store.update(s.scheduleId, (x) => ({ ...x, status: "cancelled" }), ctx.now);
13013
+ deps.disarm(s.scheduleId);
13014
+ }
13015
+ };
13016
+ return {
13017
+ isMine: (input, env) => !env.GROK_HOOK_EVENT && isCodexRollout(input.transcript_path),
13018
+ sessionId: (input) => threadOf(input)?.id,
13019
+ async handle(ctx) {
13020
+ const thread = threadOf(ctx.input);
13021
+ if (!thread) return void 0;
13022
+ loadSettings(ctx.stateDir);
13023
+ if (ctx.event === "UserPromptSubmit") {
13024
+ const prompt2 = typeof ctx.input.prompt === "string" ? ctx.input.prompt : "";
13025
+ const m = REWAKE.exec(prompt2);
13026
+ if (!m) {
13027
+ cancelPending(ctx, thread.id);
13028
+ return void 0;
13029
+ }
13030
+ const c = continueOnly(CODEX_PLACE, m[1] ?? "", ctx.now);
13031
+ if (c.kind === "reply") return block(c.text);
13032
+ if (c.kind === "list") {
13033
+ const next = pendingFor2(new ScheduleStore(ctx.stateDir), thread.id)[0];
13034
+ return block(
13035
+ next ? `Rewake will continue this thread ${formatAt(next.dueAt, ctx.now)}. To cancel: rewake cancel` : 'Rewake: Nothing is set to continue this thread. At a usage limit, type "rewake" to continue after the reset.'
13036
+ );
13037
+ }
13038
+ if (c.kind === "cancel") {
13039
+ const had = pendingFor2(new ScheduleStore(ctx.stateDir), thread.id).length > 0;
13040
+ cancelPending(ctx, thread.id);
13041
+ return block(
13042
+ had ? "Rewake: Cancelled. This thread won't be continued on its own." : "Rewake: Nothing is set to continue this thread."
13043
+ );
13044
+ }
13045
+ let limit;
13046
+ try {
13047
+ limit = findCodexLimit(readTail2(thread.path), ctx.now);
13048
+ } catch {
13049
+ limit = { limited: false };
13050
+ }
13051
+ if (!limit.limited)
13052
+ return block(
13053
+ "Rewake: this thread isn't at a usage limit, so there's nothing to continue."
13054
+ );
13055
+ if (limit.billing)
13056
+ return block(
13057
+ "Rewake can't continue after this limit: this limit is about credits or spending, which waiting doesn't fix."
13058
+ );
13059
+ let at = c.at;
13060
+ if (at === void 0) {
13061
+ const later = new ScheduleStore(ctx.stateDir).listForSession(thread.id, "codex").filter((x) => x.failureReason === "far-reset" || x.failureReason === "expired").map((x) => x.dueAt).filter((t) => t > ctx.now);
13062
+ const times = [
13063
+ ...limit.resetsAt ? [limit.resetsAt + RESET_MARGIN_MS] : [],
13064
+ ...later
13065
+ ].filter((t) => t > ctx.now);
13066
+ if (times.length > 0) at = Math.max(...times);
13067
+ }
13068
+ if (at === void 0 && limit.resetPassed) at = ctx.now + RESET_MARGIN_MS;
13069
+ if (at === void 0)
13070
+ return block(
13071
+ `Rewake doesn't know when this limit resets. Type "rewake" with a time, for example "rewake 3:30pm".`
13072
+ );
13073
+ cancelPending(ctx, thread.id);
13074
+ if (!arm(ctx, thread, at)) return block(noTimerText(at, ctx.now));
13075
+ return block(armedText2(at, ctx.now));
13076
+ }
13077
+ if (ctx.event === "SessionEnd") {
13078
+ if (pendingFor2(new ScheduleStore(ctx.stateDir), thread.id).length > 0) return void 0;
13079
+ let limit;
13080
+ try {
13081
+ limit = findCodexLimit(readTail2(thread.path), ctx.now);
13082
+ } catch {
13083
+ return void 0;
13084
+ }
13085
+ if (!limit.limited) return void 0;
13086
+ const settings = loadSettings(ctx.stateDir);
13087
+ const decision = decideArm({
13088
+ now: ctx.now,
13089
+ ...limit.resetsAt !== void 0 && { resetsAt: limit.resetsAt },
13090
+ isBilling: limit.billing === true,
13091
+ auto: autoFor(settings)
13092
+ });
13093
+ const cwd = typeof ctx.input.cwd === "string" ? basename6(ctx.input.cwd) : "";
13094
+ const where2 = cwd ? `Codex in the "${cwd}" folder` : "Codex";
13095
+ if (decision.action === "arm") {
13096
+ const ok = arm(ctx, thread, decision.fireAt);
13097
+ deps.notify(
13098
+ "Agent Rewake",
13099
+ `${where2}: ${ok ? armedText2(decision.fireAt, ctx.now) : noTimerText(decision.fireAt, ctx.now)}`
13100
+ );
13101
+ return void 0;
13102
+ }
13103
+ if (decision.action === "offer") {
13104
+ const how = limit.resetsAt ? `type "rewake", and Rewake continues it ${formatAt(limit.resetsAt + RESET_MARGIN_MS, ctx.now)}, after the limit resets` : 'type "rewake" with a time, for example "rewake 3:30pm"';
13105
+ deps.notify(
13106
+ "Agent Rewake",
13107
+ `${where2} hit its usage limit. Resume the thread in Codex and ${how}.`
13108
+ );
13109
+ }
13110
+ return void 0;
13111
+ }
13112
+ return void 0;
13113
+ }
13114
+ };
13115
+ }
13116
+
13117
+ // src/hosts/copilot/host.ts
13118
+ import { spawn as spawn8 } from "node:child_process";
13119
+ import { createInterface as createInterface4 } from "node:readline";
13120
+ var COPILOT_ID = "copilot-cli";
13121
+ function isCopilot(input, env) {
13122
+ return !env.GROK_HOOK_EVENT && !("hookEventName" in input) && safeSessionId(input.sessionId) && typeof input.timestamp === "number";
13123
+ }
13124
+ function resumeCopilot(r, text2, env, node = process.execPath) {
13125
+ if (!r.program)
13126
+ return Promise.resolve({ ok: false, reason: "unsupported", detail: "no Copilot found" });
13127
+ const program = codexProgram(r.program, node);
13128
+ return new Promise((resolve2) => {
13129
+ let limited = false;
13130
+ let resetsAt;
13131
+ const child = spawn8(
13132
+ program.command,
13133
+ [
13134
+ ...program.args,
13135
+ `--resume=${r.sessionId}`,
13136
+ "-p",
13137
+ text2,
13138
+ "--no-ask-user",
13139
+ "--output-format",
13140
+ "json",
13141
+ "--no-auto-update"
13142
+ ],
13143
+ {
13144
+ cwd: r.cwd || void 0,
13145
+ env: { ...env, COPILOT_AUTO_UPDATE: "false" },
13146
+ stdio: ["ignore", "pipe", "pipe"],
13147
+ windowsHide: true
13148
+ }
13149
+ );
13150
+ let err = "";
13151
+ child.stderr.on("data", (d) => {
13152
+ if (err.length < 1 << 16) err += d.toString("utf8");
13153
+ });
13154
+ createInterface4({ input: child.stdout }).on("line", (line) => {
13107
13155
  try {
13108
- spawn8(command, args, { detached: true, stdio: "ignore", windowsHide: true }).unref();
13156
+ const e = JSON.parse(line);
13157
+ if (e.type === "session.error" && e.data?.errorType === "rate_limit") {
13158
+ limited = true;
13159
+ resetsAt = classifyCopilotError(e.data.message, Date.now())?.resetsAt;
13160
+ }
13109
13161
  } catch {
13110
13162
  }
13111
- },
13112
- exists: existsSync20,
13113
- uid: () => process.getuid?.()
13114
- };
13115
- }
13116
- var label = (id) => `codizelabs.agent-rewake.${id}`;
13117
- function timerName(id, gen = 0) {
13118
- return gen > 0 ? `${id}-r${gen}` : id;
13119
- }
13120
- function parseTimerName(name) {
13121
- const m = /^(.+)-r(\d{1,3})$/.exec(name);
13122
- return m?.[1] ? { id: m[1], gen: Number(m[2]) } : { id: name, gen: 0 };
13123
- }
13124
- var unit = (id) => `codizelabs-agent-rewake-${id}`;
13125
- var taskName = (id) => `\\AgentRewake\\${id}`;
13126
- function timersDir(h) {
13127
- return ensurePrivateDir(join29(h.stateDir, "timers"));
13128
- }
13129
- function checkId(id) {
13130
- if (!ID.test(id)) throw new Error(`invalid timer id: ${id}`);
13131
- }
13132
- var MARK = { systemd: ".systemd", schtasks: ".task" };
13133
- function mark(name, kind, h) {
13134
- const ext = MARK[kind];
13135
- if (ext) writeFileSync5(join29(timersDir(h), `${name}${ext}`), "", { mode: 384 });
13136
- }
13137
- function timerNames(id, h) {
13138
- const names = /* @__PURE__ */ new Set([id]);
13139
- let files = [];
13140
- try {
13141
- files = readdirSync11(join29(h.stateDir, "timers"));
13142
- } catch {
13143
- }
13144
- for (const f of files) {
13145
- const name = f.replace(/^codizelabs\.agent-rewake\./, "").replace(/\.(plist|systemd|task|at|at-time)$/, "");
13146
- if (name !== f && ID.test(name) && parseTimerName(name).id === id) names.add(name);
13147
- }
13148
- return [...names];
13149
- }
13150
- var xml = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
13151
- function launchdPlist(id, at, node, cli, stateDir2) {
13152
- const d = new Date(Math.ceil(at / 6e4) * 6e4);
13153
- const int = (k, v) => `<key>${k}</key><integer>${v}</integer>`;
13154
- return `<?xml version="1.0" encoding="UTF-8"?>
13155
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
13156
- <plist version="1.0">
13157
- <dict>
13158
- <key>Label</key><string>${label(id)}</string>
13159
- <key>ProgramArguments</key>
13160
- <array><string>${xml(node)}</string><string>${xml(cli)}</string><string>fire</string><string>${id}</string>${stateDir2 ? `<string>--state-dir</string><string>${xml(stateDir2)}</string>` : ""}</array>
13161
- <key>StartCalendarInterval</key>
13162
- <dict>${int("Month", d.getMonth() + 1)}${int("Day", d.getDate())}${int("Hour", d.getHours())}${int("Minute", d.getMinutes())}</dict>
13163
- <key>RunAtLoad</key><true/>
13164
- <key>ProcessType</key><string>Background</string>
13165
- <key>StandardOutPath</key><string>/dev/null</string>
13166
- <key>StandardErrorPath</key><string>/dev/null</string>
13167
- </dict>
13168
- </plist>
13169
- `;
13163
+ });
13164
+ const timedOut = resumeDeadline(child);
13165
+ child.on("error", () => resolve2({ ok: false, reason: "failed", detail: "spawn" }));
13166
+ child.on("exit", (code) => {
13167
+ if (timedOut()) return resolve2({ ok: false, reason: "failed", detail: "timeout" });
13168
+ if (limited) resolve2({ ok: false, reason: "limited", ...resetsAt && { resetsAt } });
13169
+ else if (code === 0) resolve2({ ok: true });
13170
+ else if (SESSION_GONE.test(err)) resolve2({ ok: false, reason: "closed", detail: "deleted" });
13171
+ else
13172
+ resolve2({
13173
+ ok: false,
13174
+ reason: "failed",
13175
+ detail: `exit ${code ?? "signal"}`,
13176
+ ...withMessage(err)
13177
+ });
13178
+ });
13179
+ });
13170
13180
  }
13171
- function launchdTarget(h) {
13172
- const uid = h.uid();
13173
- return uid === void 0 ? void 0 : `gui/${uid}`;
13181
+ var copilotHost = {
13182
+ id: COPILOT_ID,
13183
+ name: "GitHub Copilot CLI",
13184
+ reopen: 'resume the session with "copilot --resume"',
13185
+ resume: (r, text2, env) => resumeCopilot(r, text2, env),
13186
+ settingsVars: [
13187
+ "COPILOT_HOME",
13188
+ "COPILOT_CACHE_HOME",
13189
+ "COPILOT_OFFLINE",
13190
+ "COPILOT_PROVIDER_BASE_URL",
13191
+ "COPILOT_PROVIDER_TYPE",
13192
+ "COPILOT_PROVIDER_WIRE_API",
13193
+ "COPILOT_MODEL"
13194
+ ],
13195
+ keyVars: ["COPILOT_PROVIDER_API_KEY", "GH_TOKEN", "GITHUB_TOKEN"]
13196
+ };
13197
+ function copilotHooks(deps) {
13198
+ return {
13199
+ isMine: isCopilot,
13200
+ sessionId: (input) => safeSessionId(input.sessionId) ? input.sessionId : void 0,
13201
+ async handle(ctx) {
13202
+ const id = ctx.input.sessionId;
13203
+ const cwd = typeof ctx.input.cwd === "string" ? ctx.input.cwd : "";
13204
+ const d = deps.closed(ctx);
13205
+ if (!ctx.env[FIRE_ENV]) ensureProgram(copilotHost, id, cwd, d, () => deps.program(ctx.env));
13206
+ switch (ctx.event) {
13207
+ case "sessionStart":
13208
+ onSessionStart(copilotHost, id, cwd, d, deps.program(ctx.env));
13209
+ break;
13210
+ case "userPromptSubmitted":
13211
+ onPrompt(copilotHost, id, cwd, d);
13212
+ break;
13213
+ case "errorOccurred": {
13214
+ const e = ctx.input.error;
13215
+ const text2 = typeof e === "string" ? e : e?.message;
13216
+ const recoverable = ctx.input.recoverable === true || typeof e === "object" && e?.recoverable === true;
13217
+ const limit = recogniseForHost(
13218
+ {
13219
+ agent: "copilot",
13220
+ source: "hook",
13221
+ ...typeof text2 === "string" && { text: text2 },
13222
+ recovered: recoverable
13223
+ },
13224
+ ctx.now
13225
+ );
13226
+ if (limit) onLimit(copilotHost, id, cwd, limit, d);
13227
+ break;
13228
+ }
13229
+ case "sessionEnd":
13230
+ onSessionEnd(copilotHost, id, cwd, d);
13231
+ break;
13232
+ }
13233
+ return void 0;
13234
+ }
13235
+ };
13174
13236
  }
13175
- function launchdArm(id, at, h) {
13176
- const domain = launchdTarget(h);
13177
- if (!domain) return { ok: false, reason: "no-scheduler" };
13178
- const plist = join29(timersDir(h), `${label(id)}.plist`);
13179
- writeFileSync5(plist, launchdPlist(id, at, h.node, h.cli, h.stateDir), { mode: 384 });
13180
- const r = h.run("launchctl", ["bootstrap", domain, plist]);
13181
- if (r.status === 0) return { ok: true, via: "launchd" };
13182
- rmSync13(plist, { force: true });
13183
- return { ok: false, reason: "failed", detail: r.stderr.trim() };
13237
+
13238
+ // src/hosts/cursor/host.ts
13239
+ import { readFileSync as readFileSync29 } from "node:fs";
13240
+ var CURSOR_ID = "cursor";
13241
+ var FALLBACK_MS = 3 * 6e4;
13242
+ var POLL_MS = 5e3;
13243
+ var cursorHost = {
13244
+ id: CURSOR_ID,
13245
+ name: "Cursor",
13246
+ noun: "chat",
13247
+ reopen: "open the chat in Cursor",
13248
+ fallbackDelayMs: FALLBACK_MS,
13249
+ maxWaitAfterLimitMs: WAIT_SECONDS * 1e3 - 6e4,
13250
+ keepOpen: "Keep that Cursor window open until then: reloading or quitting Cursor cancels it.",
13251
+ // Reached only when the waiting hook didn't deliver (the window was closed): nothing to run.
13252
+ resume: async () => ({ ok: false, reason: "failed", detail: "window-closed" })
13253
+ };
13254
+ function transcriptError(path) {
13255
+ if (typeof path !== "string" || path === "") return void 0;
13256
+ try {
13257
+ const lines = readFileSync29(path, "utf8").trimEnd().split("\n");
13258
+ const last = JSON.parse(lines.at(-1) ?? "{}");
13259
+ if (last.type === "turn_ended" && last.status === "error" && typeof last.error === "string")
13260
+ return last.error;
13261
+ } catch {
13262
+ }
13263
+ return void 0;
13184
13264
  }
13185
- function systemdAvailable(h) {
13186
- if (!h.exists("/run/systemd/system")) return false;
13187
- const r = h.run("systemctl", ["--user", "is-system-running"]);
13188
- return /^(running|degraded)\b/.test(r.stdout.trim());
13265
+ var isCursor = (input) => typeof input.cursor_version === "string" && typeof input.hook_event_name === "string";
13266
+ function cursorHooks(deps) {
13267
+ const now = deps.now ?? Date.now;
13268
+ const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
13269
+ return {
13270
+ isMine: isCursor,
13271
+ sessionId: (input) => safeSessionId(input.conversation_id) ? input.conversation_id : void 0,
13272
+ async handle(ctx) {
13273
+ const id = ctx.input.conversation_id;
13274
+ if (!safeSessionId(id)) return void 0;
13275
+ const roots = ctx.input.workspace_roots;
13276
+ const cwd = Array.isArray(roots) && typeof roots[0] === "string" ? roots[0] : "";
13277
+ const d = deps.closed(ctx);
13278
+ if (ctx.event === "beforeSubmitPrompt") {
13279
+ onPrompt(cursorHost, id, cwd, d);
13280
+ return void 0;
13281
+ }
13282
+ if (ctx.event !== "stop") return void 0;
13283
+ if (ctx.input.status !== "error") return "{}";
13284
+ const limit = recogniseForHost(
13285
+ { agent: "cursor", source: "hook", text: transcriptError(ctx.input.transcript_path) ?? "" },
13286
+ ctx.now
13287
+ );
13288
+ if (!limit) return "{}";
13289
+ onLimit(cursorHost, id, cwd, limit, d);
13290
+ onSessionEnd(cursorHost, id, cwd, d);
13291
+ if (limit.billing) {
13292
+ d.notify(
13293
+ "Agent Rewake",
13294
+ `${placeOf(cursorHost, cwd)} hit a usage limit that waiting won't lift (it needs a paid plan, more usage or a new month), so Rewake can't continue it. Continue the chat in Cursor when you can.`
13295
+ );
13296
+ return "{}";
13297
+ }
13298
+ return waitAndDeliver(id, ctx, d, now, sleep, deps.waitMs ?? WAIT_SECONDS * 1e3 - 6e4);
13299
+ }
13300
+ };
13189
13301
  }
13190
- function utcCalendar(at) {
13191
- return `${new Date(Math.ceil(at / 1e3) * 1e3).toISOString().slice(0, 19).replace("T", " ")} UTC`;
13302
+ async function waitAndDeliver(id, ctx, d, now, sleep, waitMs) {
13303
+ const records = new SessionRecords(ctx.stateDir, CURSOR_ID);
13304
+ const store = new ScheduleStore(ctx.stateDir);
13305
+ const seenAt = records.get(id)?.limit?.seenAt ?? ctx.now;
13306
+ const deadline = ctx.now + waitMs;
13307
+ while (now() < deadline) {
13308
+ if ((records.get(id)?.lastPromptAt ?? 0) > seenAt) return "{}";
13309
+ const s = pendingFor(ctx.stateDir, CURSOR_ID, id).sort((a, b) => a.dueAt - b.dueAt)[0];
13310
+ if (s && s.dueAt > deadline) {
13311
+ store.update(
13312
+ s.scheduleId,
13313
+ (x) => ({ ...x, status: "failed", failureReason: "too-late" }),
13314
+ now()
13315
+ );
13316
+ d.disarm(s.scheduleId);
13317
+ d.notify(
13318
+ "Agent Rewake",
13319
+ `${placeOf(cursorHost, records.get(id)?.cwd ?? "")}: Rewake can continue a Cursor chat only within 4 hours of its usage limit, so it won't continue this one ${formatAt(s.dueAt, now())}. Open the chat in Cursor then to continue it.`
13320
+ );
13321
+ return "{}";
13322
+ }
13323
+ if (s && s.dueAt <= now()) {
13324
+ store.update(
13325
+ s.scheduleId,
13326
+ (x) => ({ ...x, status: "sent", lastRun: { at: now(), outcome: "sent" } }),
13327
+ now()
13328
+ );
13329
+ d.disarm(s.scheduleId);
13330
+ return JSON.stringify({ followup_message: s.text });
13331
+ }
13332
+ await sleep(Math.min(POLL_MS, Math.max(0, deadline - now())));
13333
+ }
13334
+ return "{}";
13192
13335
  }
13193
- function systemdArm(id, at, h) {
13194
- h.run("systemctl", ["--user", "stop", `${unit(id)}.timer`]);
13195
- h.run("systemctl", ["--user", "reset-failed", `${unit(id)}.timer`, `${unit(id)}.service`]);
13196
- const r = h.run("systemd-run", [
13197
- "--user",
13198
- `--unit=${unit(id)}`,
13199
- `--on-calendar=${utcCalendar(at)}`,
13200
- "--timer-property=AccuracySec=1s",
13201
- "--timer-property=Persistent=true",
13202
- `--description=Agent Rewake: ${id}`,
13203
- h.node,
13204
- h.cli,
13205
- "fire",
13206
- id,
13207
- "--state-dir",
13208
- h.stateDir
13336
+
13337
+ // src/hosts/index.ts
13338
+ registerHost("codex");
13339
+ registerHost(COPILOT_ID);
13340
+ registerHost(GROK_ID);
13341
+ registerHost(GEMINI_ID);
13342
+ registerHost(ANTIGRAVITY_ID);
13343
+ registerHost(CURSOR_ID);
13344
+ var CLOSED_HOSTS = [
13345
+ copilotHost,
13346
+ grokHost(process.env),
13347
+ geminiHost,
13348
+ antigravityHost(),
13349
+ cursorHost
13350
+ ];
13351
+ function hostAdapters(env, node, stateDir2) {
13352
+ return new Map([
13353
+ ["codex", codexAdapter({ env, node })],
13354
+ ...CLOSED_HOSTS.map((h) => [h.id, closedAdapter(h, stateDir2, env)])
13209
13355
  ]);
13210
- if (r.status !== 0) return { ok: false, reason: "failed", detail: r.stderr.trim() };
13211
- mark(id, "systemd", h);
13212
- return { ok: true, via: "systemd" };
13213
13356
  }
13214
- function atTime(at) {
13215
- const d = new Date(Math.ceil(at / 6e4) * 6e4);
13216
- const p = (n) => String(n).padStart(2, "0");
13217
- return `${d.getFullYear()}${p(d.getMonth() + 1)}${p(d.getDate())}${p(d.getHours())}${p(d.getMinutes())}`;
13357
+ function hookHandler(host, deps) {
13358
+ if (host === "codex") return codexHooks(deps);
13359
+ if (host === COPILOT_ID)
13360
+ return copilotHooks({ closed: deps.closed, program: (env) => deps.program(host, env) });
13361
+ if (host === GROK_ID)
13362
+ return grokHooks({ closed: deps.closed, program: (env) => deps.program(host, env) });
13363
+ if (host === GEMINI_ID)
13364
+ return geminiHooks({
13365
+ closed: deps.closed,
13366
+ program: (env) => deps.program(host, env),
13367
+ apiKey: (env) => geminiApiKeyAuth(env, env.HOME || env.USERPROFILE || homedir18())
13368
+ });
13369
+ if (host === CURSOR_ID) return cursorHooks({ closed: deps.closed });
13370
+ if (host === ANTIGRAVITY_ID)
13371
+ return antigravityHooks({ closed: deps.closed, program: (env) => deps.program(host, env) });
13372
+ return void 0;
13218
13373
  }
13219
- var shQuote = (s) => `'${s.replace(/'/g, `'\\''`)}'`;
13220
- function atAvailable(h) {
13221
- if (h.run("atq", []).status !== 0) return false;
13222
- const pg = h.run("pgrep", ["-x", "atd"]);
13223
- return pg.status === 0 || pg.status === null || pg.status === 127;
13374
+ var OWNER_ENV = "AGENT_REWAKE_OWNER";
13375
+ function ownedByZed(env) {
13376
+ return env[OWNER_ENV] === "acp";
13224
13377
  }
13225
- function atArm(id, at, h) {
13226
- atCancel(id, h);
13227
- const job = `${shQuote(h.node)} ${shQuote(h.cli)} fire ${id} --state-dir ${shQuote(h.stateDir)}
13228
- `;
13229
- const r = h.run("at", ["-t", atTime(at)], job);
13230
- const n = /job (\d+)/.exec(`${r.stderr}
13231
- ${r.stdout}`)?.[1];
13232
- if (r.status !== 0 || !n) return { ok: false, reason: "failed", detail: r.stderr.trim() };
13233
- writeFileSync5(join29(timersDir(h), `${id}.at`), n, { mode: 384 });
13234
- writeFileSync5(join29(timersDir(h), `${id}.at-time`), `${atTime(at)} ${h.node} ${h.cli}`, {
13235
- mode: 384
13236
- });
13237
- return { ok: true, via: "at" };
13378
+
13379
+ // src/hosts/hook.ts
13380
+ var MAX_INPUT = 4 * 1024 * 1024;
13381
+ async function readStdin(stream = process.stdin) {
13382
+ const chunks = [];
13383
+ let size = 0;
13384
+ for await (const chunk of stream) {
13385
+ const b = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
13386
+ size += b.length;
13387
+ if (size > MAX_INPUT) return "";
13388
+ chunks.push(b);
13389
+ }
13390
+ return Buffer.concat(chunks).toString("utf8");
13238
13391
  }
13239
- function atJob(id, h) {
13392
+ async function runHook(handler, event, stdin, env, stateDir2, now) {
13393
+ if (!handler || ownedByZed(env)) return void 0;
13394
+ let input;
13240
13395
  try {
13241
- const n = readFileSync25(join29(h.stateDir, "timers", `${id}.at`), "utf8").trim();
13242
- return /^\d+$/.test(n) ? n : void 0;
13396
+ input = JSON.parse(stdin || "{}");
13243
13397
  } catch {
13244
13398
  return void 0;
13245
13399
  }
13400
+ if (typeof input !== "object" || input === null || Array.isArray(input)) return void 0;
13401
+ const record = input;
13402
+ if (!handler.isMine(record, env)) return void 0;
13403
+ const id = handler.sessionId(record);
13404
+ if (id && new ThreadStore(stateDir2).get(id)) return void 0;
13405
+ return handler.handle({ event, input: record, env, stateDir: stateDir2, now });
13246
13406
  }
13247
- function atCancel(id, h) {
13248
- const n = atJob(id, h);
13249
- if (n) h.run("atrm", [n]);
13250
- rmSync13(join29(h.stateDir, "timers", `${id}.at`), { force: true });
13251
- rmSync13(join29(h.stateDir, "timers", `${id}.at-time`), { force: true });
13407
+
13408
+ // src/install/select.ts
13409
+ var WHAT = {
13410
+ zed: "Resumes and schedules messages in Zed's agent threads.",
13411
+ jetbrains: "Adds Claude Agent and Codex with Rewake to AI Assistant in your JetBrains IDEs.",
13412
+ "devin-desktop": "Adds Claude Agent and Codex with Rewake to Devin Desktop's agent selector.",
13413
+ cursor: "At a usage limit in Cursor's own agent, continues the chat at the time you choose (within 4 hours), while its window stays open.",
13414
+ "claude-code": "Asks at a usage limit, then continues the same session when the limit resets.",
13415
+ codex: 'Type "rewake" in a thread at a usage limit; it continues when the limit resets.',
13416
+ "copilot-cli": "Continues a closed session when its usage limit resets.",
13417
+ grok: "Continues a closed session when its usage limit resets.",
13418
+ "gemini-cli": "Continues a closed session when its usage limit resets.",
13419
+ antigravity: "Continues a closed CLI conversation when its limit resets; in the app and IDE, says when it resets."
13420
+ };
13421
+ var UNREACHABLE = "Rewake can't reach claude.ai, the Claude desktop app's chat, or Zed's own agent.";
13422
+ function defaultChoice(places) {
13423
+ return new Set(places.filter((p) => p.state === "ready").map((p) => p.id));
13252
13424
  }
13253
- function localIso(at) {
13254
- const d = new Date(Math.ceil(at / 1e3) * 1e3);
13255
- const p = (n) => String(Math.abs(n)).padStart(2, "0");
13256
- const off = -d.getTimezoneOffset();
13257
- return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())}T${p(d.getHours())}:${p(d.getMinutes())}:${p(d.getSeconds())}${off >= 0 ? "+" : "-"}${p(Math.trunc(off / 60))}:${p(off % 60)}`;
13425
+ var PREVIEW_NOTE = "Everything except Zed is a preview: new, and may change.";
13426
+ function renderChoice(places, chosen, cursor, missing) {
13427
+ const lines = [];
13428
+ places.forEach((p, i) => {
13429
+ const pointer = i === cursor ? ">" : " ";
13430
+ const label2 = `${p.name}${p.version ? ` ${p.version}` : ""}${p.surface ? ` (${p.surface})` : ""}`;
13431
+ if (p.state === "too-old") {
13432
+ lines.push(
13433
+ `${pointer} - ${label2}: too old for Rewake (it needs ${p.needs} or newer). Update it: ${p.update ?? "see its own docs"}`
13434
+ );
13435
+ return;
13436
+ }
13437
+ const box = chosen.has(p.id) ? "[x]" : "[ ]";
13438
+ const note = p.state === "installed" ? ": Rewake is set up here (tick to update)" : "";
13439
+ lines.push(`${pointer} ${box} ${label2}${note}`);
13440
+ lines.push(` ${p.what}`);
13441
+ });
13442
+ for (const name of missing) lines.push(` - ${name}: not found.`);
13443
+ lines.push("", PREVIEW_NOTE, UNREACHABLE);
13444
+ return lines;
13258
13445
  }
13259
- function taskXml(id, at, node, cli, stateDir2) {
13260
- return `<?xml version="1.0" encoding="UTF-16"?>
13261
- <Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
13262
- <RegistrationInfo><Description>Agent Rewake: ${id}</Description></RegistrationInfo>
13263
- <Triggers>
13264
- <TimeTrigger>
13265
- <StartBoundary>${localIso(at)}</StartBoundary>
13266
- <EndBoundary>${localIso(at + 7 * 24 * 36e5)}</EndBoundary>
13267
- <Enabled>true</Enabled>
13268
- </TimeTrigger>
13269
- </Triggers>
13270
- <Principals><Principal id="Author"><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
13271
- <Settings>
13272
- <StartWhenAvailable>true</StartWhenAvailable>
13273
- <DeleteExpiredTaskAfter>PT1H</DeleteExpiredTaskAfter>
13274
- <DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
13275
- <StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
13276
- <MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
13277
- <ExecutionTimeLimit>PT1H</ExecutionTimeLimit>
13278
- </Settings>
13279
- <Actions Context="Author">
13280
- <Exec><Command>conhost.exe</Command><Arguments>--headless "${xml(node)}" "${xml(cli)}" fire ${id}${stateDir2 ? ` --state-dir "${xml(stateDir2)}"` : ""}</Arguments></Exec>
13281
- </Actions>
13282
- </Task>
13283
- `;
13446
+ function keysHelp(ticked) {
13447
+ return `Set up Rewake in the ${ticked === 1 ? "1 ticked place" : `${ticked} ticked places`}? Up and Down move, Space ticks or unticks, a ticks all, Enter continues, q quits.`;
13284
13448
  }
13285
- function schtasksArm(id, at, h) {
13286
- h.run("schtasks", ["/Delete", "/TN", taskName(id), "/F"]);
13287
- const file = join29(timersDir(h), `${id}.xml`);
13288
- writeFileSync5(file, Buffer.from(`\uFEFF${taskXml(id, at, h.node, h.cli, h.stateDir)}`, "utf16le"));
13289
- const r = h.run("schtasks", ["/Create", "/TN", taskName(id), "/XML", file]);
13290
- rmSync13(file, { force: true });
13291
- if (r.status !== 0) return { ok: false, reason: "failed", detail: r.stderr.trim() };
13292
- mark(id, "schtasks", h);
13293
- return { ok: true, via: "schtasks" };
13449
+ function keyOf(data) {
13450
+ if (data === "\x1B[A" || data === "k") return "up";
13451
+ if (data === "\x1B[B" || data === "j") return "down";
13452
+ if (data === " ") return "toggle";
13453
+ if (data === "a") return "all";
13454
+ if (data === "\r" || data === "\n") return "enter";
13455
+ if (data === "q" || data === "" || data === "\x1B") return "quit";
13456
+ return "other";
13457
+ }
13458
+ function press(key2, places, chosen, state) {
13459
+ const pickable = (i) => places[i]?.state !== "too-old";
13460
+ if (key2 === "up") state.cursor = Math.max(0, state.cursor - 1);
13461
+ if (key2 === "down") state.cursor = Math.min(places.length - 1, state.cursor + 1);
13462
+ if (key2 === "toggle" && pickable(state.cursor)) {
13463
+ const id = places[state.cursor]?.id;
13464
+ if (id) chosen.has(id) ? chosen.delete(id) : chosen.add(id);
13465
+ }
13466
+ if (key2 === "all") {
13467
+ for (const p of places) if (p.state !== "too-old") chosen.add(p.id);
13468
+ }
13469
+ if (key2 === "enter") return "done";
13470
+ if (key2 === "quit") return "quit";
13471
+ return void 0;
13472
+ }
13473
+ function placesFrom(found, zed, installed, minimum, older, update = () => void 0) {
13474
+ const out = [];
13475
+ if (zed.found)
13476
+ out.push({
13477
+ id: "zed",
13478
+ name: "Zed",
13479
+ ...zed.version && { version: zed.version },
13480
+ what: WHAT.zed,
13481
+ state: "ready"
13482
+ });
13483
+ for (const f of found) {
13484
+ const min = minimum(f.id);
13485
+ const tooOld2 = Boolean(min && f.version && older(f.version, min));
13486
+ out.push({
13487
+ id: f.id,
13488
+ name: f.name,
13489
+ ...f.version && { version: f.version },
13490
+ what: WHAT[f.id],
13491
+ state: tooOld2 ? "too-old" : installed.has(f.id) ? "installed" : "ready",
13492
+ ...tooOld2 && min && { needs: min },
13493
+ ...tooOld2 && update(f.id) && { update: update(f.id) },
13494
+ ...f.surfaces.length > 0 && !f.surfaces.includes("terminal") && { surface: f.surfaces.join(", ") }
13495
+ });
13496
+ }
13497
+ return out;
13294
13498
  }
13295
- function timerKind(h) {
13296
- if (h.platform === "darwin") return launchdTarget(h) ? "launchd" : void 0;
13297
- if (h.platform === "win32") return "schtasks";
13298
- if (h.platform === "linux") {
13299
- if (systemdAvailable(h)) return "systemd";
13300
- if (atAvailable(h)) return "at";
13499
+ async function choosePlaces(places, missing, io) {
13500
+ const chosen = defaultChoice(places);
13501
+ const state = { cursor: 0 };
13502
+ let drawn = 0;
13503
+ const draw = () => {
13504
+ const lines = [
13505
+ ...renderChoice(places, chosen, state.cursor, missing),
13506
+ "",
13507
+ keysHelp(chosen.size)
13508
+ ];
13509
+ if (drawn > 0) io.write(`\x1B[${drawn}A`);
13510
+ io.write(`${lines.map((l) => `\x1B[2K${l}`).join("\n")}
13511
+ `);
13512
+ drawn = lines.length;
13513
+ };
13514
+ draw();
13515
+ for await (const data of io.keys) {
13516
+ const result = press(keyOf(data), places, chosen, state);
13517
+ if (result === "quit") return void 0;
13518
+ if (result === "done") return places.filter((p) => chosen.has(p.id)).map((p) => p.id);
13519
+ draw();
13301
13520
  }
13302
13521
  return void 0;
13303
13522
  }
13304
- function armTimer(id, at, h, gen = 0) {
13305
- checkId(id);
13306
- const name = timerName(id, gen);
13307
- checkId(name);
13308
- const kind = timerKind(h);
13309
- if (!kind) return { ok: false, reason: "no-scheduler" };
13310
- if (gen === 0) cancelTimer(id, h);
13311
- if (kind === "launchd") return launchdArm(name, at, h);
13312
- if (kind === "systemd") return systemdArm(name, at, h);
13313
- if (kind === "at") return atArm(name, at, h);
13314
- return schtasksArm(name, at, h);
13523
+
13524
+ // src/mcp.ts
13525
+ import { createInterface as createInterface5 } from "node:readline";
13526
+ var APPROVAL_TIMEOUT_MS = 10 * 6e4;
13527
+ var DELIVERY_NOTES = "Times are the user's local time; a clock time that has passed today means tomorrow, and one-off messages can be at most 30 days ahead. A message is sent only while Zed is running with this project, and after a Zed restart only once the thread has been opened again; the user can work in other threads meanwhile. If a reply is in progress at that time, it's sent right after the reply. If Zed or the computer wasn't running, a one-off message more than 15 minutes late isn't sent: it's marked missed and the user is asked whether to send it; a repeating message skips that run. For anything else, call about_rewake.";
13528
+ var ENDS = {
13529
+ ends_after_runs: {
13530
+ type: "integer",
13531
+ minimum: 1,
13532
+ description: "Repeating only: stop after this many runs."
13533
+ },
13534
+ ends_at: {
13535
+ type: "string",
13536
+ description: 'Repeating only: no runs after this time, e.g. "2026-10-31 18:00" (local time).'
13537
+ }
13538
+ };
13539
+ var TOOLS = [
13540
+ {
13541
+ name: "schedule_message",
13542
+ description: `Schedule a message to be sent into this conversation later, as if the user typed it. Use it when the user asks you to schedule something, or to schedule a follow-up for yourself (for example to check on a long build). The user must approve it in the thread, and may edit the message first; this call waits for that answer and returns what was saved: number, exact time, repeat, message. Give either \`when\` (once) or \`cron\` (repeating); a repeat runs until cancelled unless you give an end. ${DELIVERY_NOTES}`,
13543
+ inputSchema: {
13544
+ type: "object",
13545
+ properties: {
13546
+ message: { type: "string", description: "The message to send at that time." },
13547
+ when: {
13548
+ type: "string",
13549
+ description: 'For a one-off: "in 30m", "in 2h", "09:00", "tomorrow 09:00" or "2026-10-06 09:00" (local time).'
13550
+ },
13551
+ cron: {
13552
+ type: "string",
13553
+ description: 'For a repeating message: a 5-field cron expression in local time, e.g. "0 9 * * 1-5" (weekdays at 09:00), or @hourly, @daily, @weekly.'
13554
+ },
13555
+ ...ENDS,
13556
+ reason: { type: "string", description: "One sentence the user sees: why you want this." }
13557
+ },
13558
+ required: ["message"]
13559
+ }
13560
+ },
13561
+ {
13562
+ name: "about_rewake",
13563
+ description: "How Agent Rewake works, in detail: what it does, the Rewake menu and commands, settings, when a scheduled message is and isn't sent (Zed closed, thread closed or not open, another thread in use, computer asleep, late, busy, several windows), usage limits and automatic resume, approvals, statuses and data. Call it before answering any question the user asks about Rewake, and answer from it.",
13564
+ inputSchema: { type: "object", properties: {} }
13565
+ },
13566
+ {
13567
+ name: "list_scheduled_messages",
13568
+ description: "List the messages scheduled in this conversation, numbered, with the current time, each one's exact next run, status, repeat and end, how its last run went, and messages finished in the last 24 hours.",
13569
+ inputSchema: { type: "object", properties: {} }
13570
+ },
13571
+ {
13572
+ name: "update_scheduled_message",
13573
+ description: `Change a message scheduled in this conversation, by its number from list_scheduled_messages: its message, its time, its repeat or end, or pause or resume it. Give only what changes. The user must approve it in the thread, and may edit the message; this call waits and returns the message as saved. ${DELIVERY_NOTES}`,
13574
+ inputSchema: {
13575
+ type: "object",
13576
+ properties: {
13577
+ number: { type: "integer", minimum: 1 },
13578
+ message: { type: "string", description: "The new message." },
13579
+ when: {
13580
+ type: "string",
13581
+ description: "A new time for the next run (as in schedule_message)."
13582
+ },
13583
+ cron: { type: "string", description: "A new repeat (replaces the old one)." },
13584
+ stop_repeating: {
13585
+ type: "boolean",
13586
+ description: "true: send it once more at its next run, then stop."
13587
+ },
13588
+ ...ENDS,
13589
+ paused: { type: "boolean", description: "true pauses it; false resumes it." },
13590
+ reason: { type: "string", description: "One sentence the user sees: why." }
13591
+ },
13592
+ required: ["number"]
13593
+ }
13594
+ },
13595
+ {
13596
+ name: "cancel_scheduled_message",
13597
+ description: "Delete a message scheduled in this conversation, by its number from list_scheduled_messages. The user must approve it in the thread; this call waits for that answer.",
13598
+ inputSchema: {
13599
+ type: "object",
13600
+ properties: {
13601
+ number: { type: "integer", minimum: 1 },
13602
+ reason: { type: "string", description: "One sentence the user sees: why." }
13603
+ },
13604
+ required: ["number"]
13605
+ }
13606
+ }
13607
+ ];
13608
+ function runMcp(opts, input = process.stdin, output = process.stdout) {
13609
+ const send = (m) => output.write(`${JSON.stringify({ jsonrpc: "2.0", ...m })}
13610
+ `);
13611
+ const rl = createInterface5({ input });
13612
+ rl.on("line", (line) => {
13613
+ let m;
13614
+ try {
13615
+ m = JSON.parse(line);
13616
+ } catch {
13617
+ return;
13618
+ }
13619
+ if (m.id === void 0 || m.id === null) return;
13620
+ const reply = (result) => send({ id: m.id, result });
13621
+ switch (m.method) {
13622
+ case "initialize":
13623
+ reply({
13624
+ protocolVersion: typeof m.params?.protocolVersion === "string" ? m.params.protocolVersion : "2025-06-18",
13625
+ capabilities: { tools: {} },
13626
+ serverInfo: { name: "agent-rewake", version: VERSION2 },
13627
+ instructions: "Agent Rewake resumes this conversation after usage limits and schedules messages into it. The user approves every schedule, change and cancellation in the thread. When the user asks anything about Rewake (how it works, what happens if Zed or the thread is closed, settings, limits), call about_rewake first and answer from it."
13628
+ });
13629
+ return;
13630
+ case "ping":
13631
+ reply({});
13632
+ return;
13633
+ case "tools/list":
13634
+ reply({ tools: TOOLS });
13635
+ return;
13636
+ case "tools/call":
13637
+ void callTool(
13638
+ opts,
13639
+ String(m.params?.name ?? ""),
13640
+ m.params?.arguments ?? {}
13641
+ ).then(reply).catch((err) => reply(text(`Agent Rewake: ${err.message}`, true)));
13642
+ return;
13643
+ default:
13644
+ send({ id: m.id, error: { code: -32601, message: `Unknown method ${m.method}` } });
13645
+ }
13646
+ });
13647
+ return new Promise((resolve2) => rl.on("close", () => resolve2(0)));
13315
13648
  }
13316
- function nextGen(id, current, h) {
13317
- const used = timerNames(id, h).map((n) => parseTimerName(n).gen);
13318
- return Math.max(current, ...used) + 1;
13649
+ function aboutRewake(opts, sessionId, now) {
13650
+ const settings = loadSettings(opts.stateDir);
13651
+ const auto = {
13652
+ off: "Off",
13653
+ ask: "Ask when a new thread opens",
13654
+ on: settings.autoWhenPromptsSkipped ? "On, even when permissions are bypassed" : "On, except when permissions are bypassed"
13655
+ }[settings.newThreads];
13656
+ const lines = [
13657
+ "## Right now",
13658
+ "",
13659
+ `- Current time: ${formatExact(now, opts.locale)}.`,
13660
+ `- Agent Rewake version: ${VERSION2}.`,
13661
+ `- Settings: automatic resume for new threads: ${auto}${settings.newThreads !== "on" ? `; threads that bypass permissions ${settings.autoWhenPromptsSkipped ? "included" : "excluded"}` : ""}. Time format: ${settings.clock === "24h" ? "24-hour" : "12-hour"}.`
13662
+ ];
13663
+ if (sessionId) {
13664
+ const thread = new ThreadStore(opts.stateDir).get(sessionId);
13665
+ const pending = new ScheduleStore(opts.stateDir).listForSession(sessionId).filter((s) => !TERMINAL_STATUSES.has(s.status));
13666
+ lines.push(
13667
+ `- This thread: automatic resume after usage limits is ${thread?.autoResume ? "on" : "off"}; ${pending.length === 0 ? "nothing is scheduled" : `${pending.length} scheduled ${pending.length === 1 ? "message" : "messages"} (list_scheduled_messages shows them)`}.`
13668
+ );
13669
+ }
13670
+ return `${AGENT_GUIDE}
13671
+ ${lines.join("\n")}
13672
+ `;
13319
13673
  }
13320
- function nameArmed(name, kind, h) {
13321
- if (kind === "launchd") {
13322
- const domain = launchdTarget(h);
13323
- return domain !== void 0 && h.run("launchctl", ["print", `${domain}/${label(name)}`]).status === 0;
13674
+ function text(t, isError = false) {
13675
+ return { content: [{ type: "text", text: t }], ...isError && { isError: true } };
13676
+ }
13677
+ async function callTool(opts, name, args) {
13678
+ const now = opts.now ?? Date.now;
13679
+ applySettings(opts.stateDir);
13680
+ const link = opts.link ? new LinkStore(opts.stateDir).get(opts.link) : void 0;
13681
+ if (name === "about_rewake") return text(aboutRewake(opts, link?.sessionId, now()));
13682
+ if (!link)
13683
+ return text(
13684
+ "Agent Rewake isn't linked to this conversation yet. Ask the user to reopen the thread.",
13685
+ true
13686
+ );
13687
+ const schedules = new ScheduleStore(opts.stateDir);
13688
+ const requests = new RequestStore(opts.stateDir);
13689
+ const pending = () => schedules.listForSession(link.sessionId).filter((s) => !TERMINAL_STATUSES.has(s.status));
13690
+ const exact = (t) => formatExact(t, opts.locale);
13691
+ const clockLine = () => `Current time: ${exact(now())}.`;
13692
+ const reason = () => typeof args.reason === "string" && args.reason.trim() ? { reason: args.reason.trim() } : {};
13693
+ const byNumber = () => {
13694
+ const n = Number(args.number);
13695
+ return Number.isInteger(n) && n >= 1 ? pending()[n - 1] : void 0;
13696
+ };
13697
+ const ends = () => {
13698
+ const out = {};
13699
+ if (args.ends_after_runs !== void 0) {
13700
+ const n = Number(args.ends_after_runs);
13701
+ if (!Number.isInteger(n) || n < 1)
13702
+ return "`ends_after_runs` must be a whole number, 1 or more.";
13703
+ out.times = n;
13704
+ }
13705
+ if (typeof args.ends_at === "string" && args.ends_at.trim()) {
13706
+ const t = parseWhen(args.ends_at, now());
13707
+ if (!t.ok) return `ends_at: ${t.error}`;
13708
+ out.until = t.at;
13709
+ }
13710
+ return out;
13711
+ };
13712
+ const describe2 = (sid) => {
13713
+ const list = pending();
13714
+ const i = list.findIndex((x) => x.scheduleId === sid);
13715
+ const s = list[i] ?? schedules.get(sid);
13716
+ if (!s) return "";
13717
+ const p = s.repeat ? parseCron(s.repeat.cron) : void 0;
13718
+ const end = [
13719
+ s.repeat?.remaining !== void 0 ? `${s.repeat.remaining} run${s.repeat.remaining === 1 ? "" : "s"} left` : "",
13720
+ s.repeat?.until !== void 0 ? `no runs after ${exact(s.repeat.until)}` : ""
13721
+ ].filter(Boolean);
13722
+ const repeat2 = p?.ok ? `Repeats: ${describeCron(p.cron)}${end.length ? ` (${end.join(", ")})` : " (until cancelled)"}` : "Once";
13723
+ const last = s.lastRun ? ` \xB7 Last run: ${s.lastRun.outcome}, due ${exact(s.lastRun.at)}` : "";
13724
+ const by = s.createdBy === "agent" ? "you asked, the user approved" : s.kind === "user" ? "the user" : "the user (resume after a usage limit)";
13725
+ return [
13726
+ `${i >= 0 ? `${i + 1}. ` : ""}Next: ${exact(s.dueAt)} \xB7 ${formatWhen(s.dueAt, now(), opts.locale)}`,
13727
+ ` Status: ${s.status} \xB7 ${repeat2}${last} \xB7 Added by: ${by}`,
13728
+ ` Message: ${s.text}`
13729
+ ].join("\n");
13730
+ };
13731
+ if (name === "list_scheduled_messages") {
13732
+ const list = pending();
13733
+ const recent = schedules.listForSession(link.sessionId).filter((s) => TERMINAL_STATUSES.has(s.status) && now() - s.updatedAt < 24 * 36e5);
13734
+ const out = [clockLine()];
13735
+ out.push(
13736
+ list.length === 0 ? "Nothing is scheduled in this conversation." : list.map((s) => describe2(s.scheduleId)).join("\n")
13737
+ );
13738
+ if (recent.length > 0)
13739
+ out.push(
13740
+ "Finished in the last 24 hours:",
13741
+ ...recent.map((s) => `- ${s.status}: due ${exact(s.dueAt)} \xB7 ${s.text}`)
13742
+ );
13743
+ return text(out.join("\n"));
13324
13744
  }
13325
- if (kind === "systemd")
13326
- return h.run("systemctl", ["--user", "is-active", `${unit(name)}.timer`]).stdout.trim() === "active";
13327
- if (kind === "at") {
13328
- const n = atJob(name, h);
13329
- return n !== void 0 && new RegExp(`^${n}\\s`, "m").test(h.run("atq", []).stdout);
13745
+ const settle = async (requestId) => {
13746
+ const r = await wait(opts, requests, requestId);
13747
+ if (r.status !== "approved") return text(`${r.text}
13748
+ ${clockLine()}`, r.isError);
13749
+ const saved = r.scheduleId ? describe2(r.scheduleId) : "";
13750
+ return text([r.text, saved && `As saved:
13751
+ ${saved}`, clockLine()].filter(Boolean).join("\n"));
13752
+ };
13753
+ if (name === "schedule_message") {
13754
+ const message = typeof args.message === "string" ? args.message.trim() : "";
13755
+ if (!message) return text("Give the message to send.", true);
13756
+ const when2 = typeof args.when === "string" ? args.when.trim() : "";
13757
+ const cronText = typeof args.cron === "string" ? args.cron.trim() : "";
13758
+ if (!when2 === !cronText)
13759
+ return text("Give either `when` (once) or `cron` (repeating), not both.", true);
13760
+ const end = ends();
13761
+ if (typeof end === "string") return text(end, true);
13762
+ if (!cronText && (end.until !== void 0 || end.times !== void 0))
13763
+ return text(
13764
+ "`ends_after_runs` and `ends_at` are only for repeating messages (`cron`).",
13765
+ true
13766
+ );
13767
+ let dueAt;
13768
+ let cron;
13769
+ if (cronText) {
13770
+ const parsed = parseCron(cronText);
13771
+ if (!parsed.ok) return text(parsed.error, true);
13772
+ const first = nextRun(parsed.cron, now());
13773
+ if (first === void 0) return text(`"${cronText}" never runs.`, true);
13774
+ if (end.until !== void 0 && first > end.until)
13775
+ return text(`It would never run: the first run, ${exact(first)}, is after ends_at.`, true);
13776
+ dueAt = first;
13777
+ cron = parsed.cron.source;
13778
+ } else {
13779
+ const parsed = parseWhen(when2, now());
13780
+ if (!parsed.ok) return text(`${parsed.error} ${clockLine()}`, true);
13781
+ dueAt = parsed.at;
13782
+ }
13783
+ const same2 = (t) => t.trim().toLowerCase();
13784
+ const twin = pending().find((s) => same2(s.text) === same2(message));
13785
+ const r = requests.create({
13786
+ sessionId: link.sessionId,
13787
+ kind: "schedule",
13788
+ message,
13789
+ dueAt,
13790
+ ...cron && { cron },
13791
+ ...end,
13792
+ ...twin && { duplicateOf: twin.scheduleId },
13793
+ ...reason(),
13794
+ createdAt: now()
13795
+ });
13796
+ return settle(r.requestId);
13330
13797
  }
13331
- return h.run("schtasks", ["/Query", "/TN", taskName(name)]).status === 0;
13332
- }
13333
- function timerStale(id, at, h) {
13334
- checkId(id);
13335
- const kind = timerKind(h);
13336
- const dir = join29(h.stateDir, "timers");
13337
- if (kind === "launchd") {
13338
- const names = timerNames(id, h).filter((n) => existsSync20(join29(dir, `${label(n)}.plist`)));
13339
- if (names.length === 0) return false;
13340
- return !names.some(
13341
- (n) => readFileSync25(join29(dir, `${label(n)}.plist`), "utf8") === launchdPlist(n, at, h.node, h.cli, h.stateDir)
13342
- );
13798
+ if (name === "update_scheduled_message") {
13799
+ const target = byNumber();
13800
+ if (!target)
13801
+ return text(
13802
+ `There's no scheduled message number ${args.number}. Use list_scheduled_messages.`,
13803
+ true
13804
+ );
13805
+ const end = ends();
13806
+ if (typeof end === "string") return text(end, true);
13807
+ const change = { ...end };
13808
+ if (typeof args.message === "string" && args.message.trim())
13809
+ change.message = args.message.trim();
13810
+ if (typeof args.when === "string" && args.when.trim()) {
13811
+ const t = parseWhen(args.when, now());
13812
+ if (!t.ok) return text(`${t.error} ${clockLine()}`, true);
13813
+ change.dueAt = t.at;
13814
+ }
13815
+ if (typeof args.cron === "string" && args.cron.trim()) {
13816
+ const parsed = parseCron(args.cron);
13817
+ if (!parsed.ok) return text(parsed.error, true);
13818
+ if (change.dueAt !== void 0)
13819
+ return text(
13820
+ "Give either `when` or `cron`, not both: a new repeat sets its own next run.",
13821
+ true
13822
+ );
13823
+ change.cron = parsed.cron.source;
13824
+ }
13825
+ if (args.stop_repeating === true) change.stopRepeating = true;
13826
+ if (typeof args.paused === "boolean") change.paused = args.paused;
13827
+ if ((change.until !== void 0 || change.times !== void 0) && !target.repeat && change.cron === void 0)
13828
+ return text("That message doesn't repeat, so it has no end to set.", true);
13829
+ if (Object.keys(change).length === 0) return text("Give at least one thing to change.", true);
13830
+ const r = requests.create({
13831
+ sessionId: link.sessionId,
13832
+ kind: "update",
13833
+ scheduleId: target.scheduleId,
13834
+ ...change,
13835
+ ...reason(),
13836
+ createdAt: now()
13837
+ });
13838
+ return settle(r.requestId);
13343
13839
  }
13344
- if (kind === "at") {
13345
- const marks = timerNames(id, h).filter((n) => existsSync20(join29(dir, `${n}.at-time`)));
13346
- if (marks.length === 0) return false;
13347
- const want = `${atTime(at)} ${h.node} ${h.cli}`;
13348
- return !marks.some((n) => readFileSync25(join29(dir, `${n}.at-time`), "utf8") === want);
13840
+ if (name === "cancel_scheduled_message") {
13841
+ const target = byNumber();
13842
+ if (!target)
13843
+ return text(
13844
+ `There's no scheduled message number ${args.number}. Use list_scheduled_messages.`,
13845
+ true
13846
+ );
13847
+ const r = requests.create({
13848
+ sessionId: link.sessionId,
13849
+ kind: "cancel",
13850
+ scheduleId: target.scheduleId,
13851
+ ...reason(),
13852
+ createdAt: now()
13853
+ });
13854
+ return settle(r.requestId);
13349
13855
  }
13350
- return false;
13351
- }
13352
- function timerArmed(id, h) {
13353
- checkId(id);
13354
- const kind = timerKind(h);
13355
- if (!kind) return false;
13356
- return timerNames(id, h).some((name) => nameArmed(name, kind, h));
13856
+ return text(`Unknown tool ${name}.`, true);
13357
13857
  }
13358
- function cancelTimer(id, h, fromInsideTimer = false, keep) {
13359
- checkId(id);
13360
- const kind = timerKind(h);
13361
- for (const name of timerNames(id, h)) {
13362
- if (name === keep) continue;
13363
- const dir = join29(h.stateDir, "timers");
13364
- if (kind === "launchd") {
13365
- const plist = join29(dir, `${label(name)}.plist`);
13366
- const had = existsSync20(plist);
13367
- rmSync13(plist, { force: true });
13368
- const domain = launchdTarget(h);
13369
- if (!domain || !had && name !== id) continue;
13370
- if (fromInsideTimer)
13371
- h.detached("/bin/sh", ["-c", `sleep 2; launchctl bootout ${domain}/${label(name)}`]);
13372
- else h.run("launchctl", ["bootout", `${domain}/${label(name)}`]);
13373
- } else if (kind === "systemd") {
13374
- h.run("systemctl", ["--user", "stop", `${unit(name)}.timer`]);
13375
- rmSync13(join29(dir, `${name}.systemd`), { force: true });
13376
- } else if (kind === "at") atCancel(name, h);
13377
- else if (kind === "schtasks") {
13378
- h.run("schtasks", ["/Delete", "/TN", taskName(name), "/F"]);
13379
- rmSync13(join29(dir, `${name}.task`), { force: true });
13858
+ async function wait(opts, requests, requestId) {
13859
+ const deadline = Date.now() + (opts.timeoutMs ?? APPROVAL_TIMEOUT_MS);
13860
+ while (Date.now() < deadline) {
13861
+ const r = requests.get(requestId);
13862
+ if (!r)
13863
+ return {
13864
+ status: "declined",
13865
+ text: "The request disappeared; nothing was changed.",
13866
+ isError: true
13867
+ };
13868
+ if (r.status !== "pending") {
13869
+ requests.remove(requestId);
13870
+ return {
13871
+ status: r.status,
13872
+ text: r.answer ?? (r.status === "approved" ? "The user approved it." : "The user declined."),
13873
+ ...r.scheduleId && r.kind !== "cancel" && { scheduleId: r.scheduleId }
13874
+ };
13380
13875
  }
13876
+ await new Promise((res) => setTimeout(res, opts.pollMs ?? 500));
13381
13877
  }
13878
+ requests.remove(requestId);
13879
+ return {
13880
+ status: "declined",
13881
+ text: "The user didn't answer within 10 minutes, so nothing was changed."
13882
+ };
13382
13883
  }
13383
13884
 
13384
13885
  // src/timers/fire.ts
13886
+ import { basename as basename7 } from "node:path";
13385
13887
  var EARLY_MS = 3e4;
13386
13888
  var MIN_ARM_MS = 6e4;
13387
13889
  var SENDING_STALE_MS = 40 * 6e4;
@@ -13413,6 +13915,10 @@ function notice(why, agent, now, f) {
13413
13915
  return `${agent}: Rewake couldn't continue the ${n} because you're signed out of ${f.agentName}. Sign in, then ${reopen} to continue.`;
13414
13916
  if (f.cause === "archived")
13415
13917
  return `${agent}: Rewake couldn't continue the ${n} because it's archived. Unarchive it, then ${reopen} to continue.`;
13918
+ if (f.cause === "missing-key")
13919
+ return `${agent}: Rewake couldn't continue the ${n} because it used a key or token from your shell, and Rewake never stores those. ${cap(reopen)} to continue. Next time, sign in to ${f.agentName} and remove the key from your shell profile.`;
13920
+ if (f.cause === "window-closed")
13921
+ return `${agent}: the time you chose has come, but Rewake couldn't continue the ${n} (was its window closed or reloaded?). ${cap(reopen)} to continue.`;
13416
13922
  if (f.cause === "deleted")
13417
13923
  return `${agent}: Rewake couldn't continue the ${n} because it no longer exists.`;
13418
13924
  return `${agent}: Rewake couldn't continue the ${n}. ${cap(reopen)} to continue.`;
@@ -13471,12 +13977,13 @@ async function fire(id, deps) {
13471
13977
  alreadySent: s.attempts.some((a) => a.idempotencyKey === key2)
13472
13978
  });
13473
13979
  const at = where(host, s);
13474
- const settle = (status, failureReason) => store.update(
13980
+ const settle = (status, failureReason, failureMessage) => store.update(
13475
13981
  id,
13476
13982
  (x) => ({
13477
13983
  ...x,
13478
13984
  status,
13479
13985
  ...failureReason !== void 0 && { failureReason },
13986
+ ...failureMessage !== void 0 && { failureMessage },
13480
13987
  lastRun: { at: now, outcome: status }
13481
13988
  }),
13482
13989
  now
@@ -13497,8 +14004,8 @@ async function fire(id, deps) {
13497
14004
  log("fire.wait", { why, armed: armed?.ok ?? false });
13498
14005
  return "waiting";
13499
14006
  };
13500
- const tell = (why, status, cause) => {
13501
- settle(status, cause ?? why);
14007
+ const tell = (why, status, cause, message) => {
14008
+ settle(status, cause ?? why, message);
13502
14009
  if (facts.newResetsAt !== void 0 && (why === "far-reset" || why === "expired"))
13503
14010
  store.update(id, (x) => ({ ...x, dueAt: (facts.newResetsAt ?? 0) + RESET_MARGIN_MS }), now);
13504
14011
  deps.notify(
@@ -13570,8 +14077,15 @@ async function fire(id, deps) {
13570
14077
  }
13571
14078
  return wait2(now + backoffMs(rearms), result.reason);
13572
14079
  }
13573
- const cause = ["signed-out", "archived", "deleted", "timeout"].includes(result.detail ?? "") ? result.detail : void 0;
13574
- return tell("failed", "failed", cause);
14080
+ const cause = [
14081
+ "signed-out",
14082
+ "archived",
14083
+ "deleted",
14084
+ "timeout",
14085
+ "missing-key",
14086
+ "window-closed"
14087
+ ].includes(result.detail ?? "") ? result.detail : void 0;
14088
+ return tell("failed", "failed", cause, result.message);
13575
14089
  }
13576
14090
  case "wait":
13577
14091
  return wait2(decision.until, decision.why);
@@ -13632,6 +14146,98 @@ function settleStale(id, s, host, now, deps, removeTimer) {
13632
14146
  }
13633
14147
  }
13634
14148
 
14149
+ // src/timers/login.ts
14150
+ import { existsSync as existsSync22, mkdirSync as mkdirSync3, readFileSync as readFileSync30, rmSync as rmSync15, writeFileSync as writeFileSync7 } from "node:fs";
14151
+ import { dirname as dirname6, join as join33 } from "node:path";
14152
+ var LOGIN_LABEL = "codizelabs.agent-rewake.sweep";
14153
+ var UNIT = "agent-rewake-sweep.service";
14154
+ var xml2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
14155
+ var dq = (s) => `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
14156
+ function loginItem(h) {
14157
+ const args = [h.node, h.cli, "sweep", "--state-dir", h.stateDir];
14158
+ if (h.platform === "darwin")
14159
+ return {
14160
+ kind: "launchd",
14161
+ path: join33(h.home, "Library", "LaunchAgents", `${LOGIN_LABEL}.plist`),
14162
+ text: `<?xml version="1.0" encoding="UTF-8"?>
14163
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
14164
+ <plist version="1.0">
14165
+ <dict>
14166
+ <key>Label</key><string>${LOGIN_LABEL}</string>
14167
+ <key>ProgramArguments</key>
14168
+ <array>${args.map((a) => `<string>${xml2(a)}</string>`).join("")}</array>
14169
+ <key>RunAtLoad</key><true/>
14170
+ <key>ProcessType</key><string>Background</string>
14171
+ <key>StandardOutPath</key><string>/dev/null</string>
14172
+ <key>StandardErrorPath</key><string>/dev/null</string>
14173
+ </dict>
14174
+ </plist>
14175
+ `
14176
+ };
14177
+ if (h.platform === "linux") {
14178
+ if (h.exists("/run/systemd/system"))
14179
+ return {
14180
+ kind: "systemd",
14181
+ path: join33(h.home, ".config", "systemd", "user", UNIT),
14182
+ text: `[Unit]
14183
+ Description=Agent Rewake: set planned resumes again after a restart
14184
+
14185
+ [Service]
14186
+ Type=oneshot
14187
+ ExecStart=${args.map(dq).join(" ")}
14188
+
14189
+ [Install]
14190
+ WantedBy=default.target
14191
+ `
14192
+ };
14193
+ return {
14194
+ kind: "autostart",
14195
+ path: join33(h.home, ".config", "autostart", "agent-rewake-sweep.desktop"),
14196
+ text: `[Desktop Entry]
14197
+ Type=Application
14198
+ Name=Agent Rewake
14199
+ Comment=Set planned resumes again after a restart
14200
+ Exec=${args.map(dq).join(" ")}
14201
+ NoDisplay=true
14202
+ X-GNOME-Autostart-enabled=true
14203
+ `
14204
+ };
14205
+ }
14206
+ return void 0;
14207
+ }
14208
+ function syncLoginItem(h, wanted) {
14209
+ const item = loginItem(h);
14210
+ if (!item) return "unchanged";
14211
+ const had = existsSync22(item.path);
14212
+ if (wanted) {
14213
+ const same2 = had && readFileSync30(item.path, "utf8") === item.text;
14214
+ if (same2) return "unchanged";
14215
+ mkdirSync3(dirname6(item.path), { recursive: true });
14216
+ writeFileSync7(item.path, item.text, { mode: 420 });
14217
+ if (item.kind === "systemd") {
14218
+ h.run("systemctl", ["--user", "daemon-reload"]);
14219
+ h.run("systemctl", ["--user", "enable", UNIT]);
14220
+ }
14221
+ return had ? "unchanged" : "added";
14222
+ }
14223
+ if (!had) return "unchanged";
14224
+ if (item.kind === "systemd") h.run("systemctl", ["--user", "disable", UNIT]);
14225
+ rmSync15(item.path, { force: true });
14226
+ if (item.kind === "systemd") h.run("systemctl", ["--user", "daemon-reload"]);
14227
+ return "removed";
14228
+ }
14229
+ var WHO = "Codex, Copilot CLI, Gemini CLI, Grok Build and Antigravity CLI";
14230
+ var NODE_NOTE = ` macOS may show "Background Items Added"; it's listed as "node" (it runs Agent Rewake) under Login Items, Allow in the Background.`;
14231
+ function loginItemPlanText(platform4) {
14232
+ return `Rewake will also add a login item, so planned resumes are set again after a restart.${platform4 === "darwin" ? NODE_NOTE : ""}
14233
+ `;
14234
+ }
14235
+ function loginItemText(change, platform4) {
14236
+ if (change === "removed") return "Removed Rewake's login item: no remaining preview needs it.\n";
14237
+ return `Added a login item, so planned resumes are set again after a restart.${platform4 === "darwin" ? NODE_NOTE : ""} It's removed when you uninstall the last of ${WHO}.
14238
+ `;
14239
+ }
14240
+
13635
14241
  // src/timers/notify.ts
13636
14242
  import { spawnSync as spawnSync9 } from "node:child_process";
13637
14243
  function appleString(s) {
@@ -13811,8 +14417,8 @@ function textAfterLineBreak(data) {
13811
14417
  }
13812
14418
 
13813
14419
  // src/ui/page.ts
13814
- import { readFileSync as readFileSync26, writeFileSync as writeFileSync6 } from "node:fs";
13815
- import { join as join30 } from "node:path";
14420
+ import { readFileSync as readFileSync31, writeFileSync as writeFileSync8 } from "node:fs";
14421
+ import { join as join34 } from "node:path";
13816
14422
  var TIPS = [
13817
14423
  "In any thread, the Rewake menu under the message box does everything this page does.",
13818
14424
  "Type /rewake in a thread to open the scheduling form, or /rewake 09:00 Run the tests to skip it.",
@@ -13952,11 +14558,11 @@ var SchedulesPage = class {
13952
14558
  return this.rows[this.selected];
13953
14559
  }
13954
14560
  prefsFile() {
13955
- return join30(this.opts.stateDir, "ui.json");
14561
+ return join34(this.opts.stateDir, "ui.json");
13956
14562
  }
13957
14563
  prefs() {
13958
14564
  try {
13959
- return JSON.parse(readFileSync26(this.prefsFile(), "utf8"));
14565
+ return JSON.parse(readFileSync31(this.prefsFile(), "utf8"));
13960
14566
  } catch {
13961
14567
  return {};
13962
14568
  }
@@ -13964,7 +14570,7 @@ var SchedulesPage = class {
13964
14570
  savePrefs(p) {
13965
14571
  try {
13966
14572
  ensurePrivateDir(this.opts.stateDir);
13967
- writeFileSync6(this.prefsFile(), `${JSON.stringify(p)}
14573
+ writeFileSync8(this.prefsFile(), `${JSON.stringify(p)}
13968
14574
  `, { mode: 384 });
13969
14575
  } catch {
13970
14576
  }
@@ -14970,7 +15576,7 @@ async function runTui(stateDir2, opts = {}) {
14970
15576
 
14971
15577
  // src/util/log.ts
14972
15578
  import { appendFileSync } from "node:fs";
14973
- import { join as join31 } from "node:path";
15579
+ import { join as join35 } from "node:path";
14974
15580
  var Logger = class {
14975
15581
  constructor(env = process.env) {
14976
15582
  this.env = env;
@@ -14992,8 +15598,8 @@ var Logger = class {
14992
15598
  try {
14993
15599
  const day = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
14994
15600
  if (!this.file?.endsWith(`rewake-${day}.jsonl`)) {
14995
- const dir = ensurePrivateDir(join31(stateDir(this.env), "logs"));
14996
- this.file = join31(dir, `rewake-${day}.jsonl`);
15601
+ const dir = ensurePrivateDir(join35(stateDir(this.env), "logs"));
15602
+ this.file = join35(dir, `rewake-${day}.jsonl`);
14997
15603
  }
14998
15604
  const record = { t: (/* @__PURE__ */ new Date()).toISOString(), level, event, pid: process.pid, ...fields };
14999
15605
  appendFileSync(this.file, `${JSON.stringify(record)}
@@ -15021,7 +15627,8 @@ var INSTALL_PLACES = /* @__PURE__ */ new Set([
15021
15627
  "gemini-cli",
15022
15628
  "antigravity",
15023
15629
  "jetbrains",
15024
- "devin-desktop"
15630
+ "devin-desktop",
15631
+ "cursor"
15025
15632
  ]);
15026
15633
  var USAGE = `agent-rewake ${VERSION2}
15027
15634
 
@@ -15052,6 +15659,7 @@ Usage:
15052
15659
  --ask: go back to asking each time.
15053
15660
  --cancel: cancel every planned resume.
15054
15661
  agent-rewake fire <id> Run by Rewake's timers at a resume's time (safe to run any time)
15662
+ agent-rewake sweep Run at login by Rewake's login item: sets planned resumes again
15055
15663
  agent-rewake hook <agent> <event>
15056
15664
  Run by an agent's hooks outside Zed, not by you
15057
15665
  agent-rewake --version
@@ -15217,6 +15825,7 @@ async function main(argv, env = process.env) {
15217
15825
  out: o.out,
15218
15826
  ask
15219
15827
  });
15828
+ if (id === "cursor") return runCursorInstall(common);
15220
15829
  if (id === "jetbrains")
15221
15830
  return runJetbrainsInstall({
15222
15831
  uninstall,
@@ -15247,6 +15856,9 @@ ${placeName(id)}
15247
15856
  out: (t) => print(t.replace(/^Dry run: nothing was (changed|written)\.\n/gm, ""))
15248
15857
  });
15249
15858
  }
15859
+ if (!uninstall && loginWouldAdd(env, order))
15860
+ print(`
15861
+ ${loginItemPlanText(process.platform)}`);
15250
15862
  if (!await ask(`
15251
15863
  Apply these changes to ${order.length} places? [y/N] `)) {
15252
15864
  print("Nothing was changed.\n");
@@ -15266,11 +15878,20 @@ Apply these changes to ${order.length} places? [y/N] `)) {
15266
15878
  code2 = Math.max(code2, c);
15267
15879
  }
15268
15880
  print(summaryText(results));
15269
- } else
15881
+ } else {
15882
+ if (!uninstall && loginWouldAdd(env, order)) print(loginItemPlanText(process.platform));
15270
15883
  for (const id of order)
15271
15884
  code2 = Math.max(code2, await runPlace(id, { yes, dryRun, out: print }));
15885
+ }
15272
15886
  if (uninstall && !dryRun) cancelResumesOf(chosen, env);
15273
- if (!dryRun) sweepQuietly(env);
15887
+ if (!dryRun) {
15888
+ const change = syncLogin(env);
15889
+ if (change !== "unchanged") print(loginItemText(change, process.platform));
15890
+ if (!uninstall && order.some((p) => TIMER_PLACES.has(p)) && usesWaiter(env))
15891
+ print(((n) => `${n.text} ${n.fix}
15892
+ `)(waiterNote(isWsl())));
15893
+ sweepQuietly(env);
15894
+ }
15274
15895
  return code2;
15275
15896
  }
15276
15897
  if (first === "continue") {
@@ -15284,6 +15905,24 @@ Apply these changes to ${order.length} places? [y/N] `)) {
15284
15905
  }
15285
15906
  return runContinueCommand(env, mode);
15286
15907
  }
15908
+ if (first === "sweep") {
15909
+ const at = argv.indexOf("--state-dir");
15910
+ const dir = at !== -1 ? argv[at + 1] : void 0;
15911
+ sweepQuietly(dir ? { ...env, AGENT_REWAKE_STATE_DIR: dir } : env);
15912
+ return 0;
15913
+ }
15914
+ if (first === "wait") {
15915
+ const at = argv.indexOf("--state-dir");
15916
+ const dir = at !== -1 ? argv[at + 1] : void 0;
15917
+ const { state, node, timers } = timerDeps(dir ? { ...env, AGENT_REWAKE_STATE_DIR: dir } : env);
15918
+ return runWaiter({
15919
+ timers,
15920
+ pid: process.pid,
15921
+ now: Date.now,
15922
+ sleep: (ms) => new Promise((r) => setTimeout(r, ms)),
15923
+ fire: (name) => timers.detached(node, [timers.cli, "fire", name, "--state-dir", state])
15924
+ });
15925
+ }
15287
15926
  if (first === "fire") {
15288
15927
  const at = argv.indexOf("--state-dir");
15289
15928
  const dir = at !== -1 ? argv[at + 1] : void 0;
@@ -15303,7 +15942,7 @@ Apply these changes to ${order.length} places? [y/N] `)) {
15303
15942
  const sep2 = argv.indexOf("--");
15304
15943
  if (wrap) {
15305
15944
  try {
15306
- const npmLog = join32(ensurePrivateDir(join32(stateDir(env), "logs")), "npm-install.log");
15945
+ const npmLog = join36(ensurePrivateDir(join36(stateDir(env), "logs")), "npm-install.log");
15307
15946
  agent = await wrappedAgentCommand(wrap.target, wrap.extra, env, stateDir(env), npmLog);
15308
15947
  } catch (err) {
15309
15948
  log.error("agent.resolve_failed", {
@@ -15367,11 +16006,11 @@ function doctor(env, details) {
15367
16006
  env,
15368
16007
  now: Date.now(),
15369
16008
  platform: process.platform,
15370
- home: homedir17(),
15371
- zedApps: () => findZedApps(process.platform, homedir17(), env),
16009
+ home: homedir19(),
16010
+ zedApps: () => findZedApps(process.platform, homedir19(), env),
15372
16011
  agents: () => {
15373
- const ids = new Set(installedPreviews(env, homedir17(), stateDir(env)));
15374
- return detectAgents({ env, home: homedir17(), platform: process.platform }).filter(
16012
+ const ids = new Set(installedPreviews(env, homedir19(), stateDir(env)));
16013
+ return detectAgents({ env, home: homedir19(), platform: process.platform }).filter(
15375
16014
  (f) => !ids.has(f.id)
15376
16015
  );
15377
16016
  },
@@ -15393,16 +16032,19 @@ function doctor(env, details) {
15393
16032
  ...diagnoseOutside({
15394
16033
  stateDir: state,
15395
16034
  env,
15396
- home: homedir17(),
16035
+ home: homedir19(),
15397
16036
  now: ctx.now,
15398
16037
  platform: process.platform,
15399
- previews: installedPreviews(env, homedir17(), state).map((id) => ({
16038
+ previews: installedPreviews(env, homedir19(), state).map((id) => ({
15400
16039
  id,
15401
16040
  name: PREVIEW_NAMES[id] ?? id
15402
16041
  })),
15403
16042
  hosts: hostAdapters(env, node, state),
15404
- hasTimer: timerKind(timers) !== void 0,
15405
- agents: terminalAgents({ env, home: homedir17(), platform: process.platform }),
16043
+ ...((kind) => ({ hasTimer: kind !== void 0, ...kind && { timerKind: kind } }))(
16044
+ timerKind(timers)
16045
+ ),
16046
+ wsl: process.platform === "linux" && isWsl(),
16047
+ agents: terminalAgents({ env, home: homedir19(), platform: process.platform }),
15406
16048
  when
15407
16049
  })
15408
16050
  );
@@ -15426,16 +16068,16 @@ function doctor(env, details) {
15426
16068
  return findings.some((f) => f.level === "problem") ? 1 : 0;
15427
16069
  }
15428
16070
  function toolsRefused(state) {
15429
- const dir = join32(state, "logs");
16071
+ const dir = join36(state, "logs");
15430
16072
  const agents = /* @__PURE__ */ new Set();
15431
16073
  let files = [];
15432
16074
  try {
15433
- files = readdirSync12(dir).filter((f) => /^rewake-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f));
16075
+ files = readdirSync14(dir).filter((f) => /^rewake-\d{4}-\d{2}-\d{2}\.jsonl$/.test(f));
15434
16076
  } catch {
15435
16077
  return [];
15436
16078
  }
15437
16079
  for (const f of files) {
15438
- for (const line of readFileSync27(join32(dir, f), "utf8").split("\n")) {
16080
+ for (const line of readFileSync32(join36(dir, f), "utf8").split("\n")) {
15439
16081
  if (!line.includes("agent_tools.refused")) continue;
15440
16082
  try {
15441
16083
  const r = JSON.parse(line);
@@ -15456,7 +16098,7 @@ function setupZedText() {
15456
16098
  const task = [taskEntry(launch)];
15457
16099
  const keymap = [{ bindings: { [keyChord()]: ["task::Spawn", { task_name: TASK_LABEL }] } }];
15458
16100
  const dir = zedConfigDir();
15459
- const file = (name) => join32(dir, name);
16101
+ const file = (name) => join36(dir, name);
15460
16102
  return [
15461
16103
  "Easiest: run `agent-rewake install`, which adds these for you after asking.",
15462
16104
  "To add them by hand instead:",
@@ -15500,14 +16142,15 @@ function testTiming(env) {
15500
16142
  return out;
15501
16143
  }
15502
16144
  function placesHere(env) {
15503
- const host = { env, home: homedir17(), platform: process.platform };
16145
+ const host = { env, home: homedir19(), platform: process.platform };
15504
16146
  const found = detectAgents(host);
15505
- const zedApps = findZedApps(process.platform, homedir17(), env);
15506
- const installed = new Set(installedPreviews(env, homedir17(), stateDir(env)));
16147
+ const zedApps = findZedApps(process.platform, homedir19(), env);
16148
+ const installed = new Set(installedPreviews(env, homedir19(), stateDir(env)));
15507
16149
  const withIde = [
15508
16150
  ...found,
15509
- ...jetbrainsFound(env, homedir17(), process.platform) ? [{ id: "jetbrains", name: "JetBrains IDEs", surfaces: ["app"] }] : [],
15510
- ...devinFound(homedir17()) ? [{ id: "devin-desktop", name: "Devin Desktop", surfaces: ["app"] }] : []
16151
+ ...jetbrainsFound(env, homedir19(), process.platform) ? [{ id: "jetbrains", name: "JetBrains IDEs", surfaces: ["app"] }] : [],
16152
+ ...devinFound(homedir19()) ? [{ id: "devin-desktop", name: "Devin Desktop", surfaces: ["app"] }] : [],
16153
+ ...cursorFound(env, homedir19(), process.platform) ? [{ id: "cursor", name: "Cursor", surfaces: ["app"] }] : []
15511
16154
  ];
15512
16155
  const places = placesFrom(
15513
16156
  withIde,
@@ -15602,13 +16245,13 @@ async function pickPlaces(env) {
15602
16245
  }
15603
16246
  function rewakeCli(state) {
15604
16247
  const stable = launcherPath(state);
15605
- if (existsSync21(stable)) return stable;
16248
+ if (existsSync23(stable)) return stable;
15606
16249
  const bundle = process.argv[1] ?? "";
15607
16250
  return bundle && ensureLauncher(state, bundle) || bundle || stable;
15608
16251
  }
15609
16252
  function cancelResumesOf(places, env) {
15610
16253
  const { state, timers } = timerDeps(env);
15611
- const still = new Set(installedPreviews(env, homedir17(), state));
16254
+ const still = new Set(installedPreviews(env, homedir19(), state));
15612
16255
  const gone = places.filter((p) => p !== "zed" && !still.has(p));
15613
16256
  if (gone.length === 0) return;
15614
16257
  const store = new ScheduleStore(state);
@@ -15629,6 +16272,56 @@ function cancelResumesOf(places, env) {
15629
16272
  );
15630
16273
  }
15631
16274
  }
16275
+ var TIMER_PLACES = /* @__PURE__ */ new Set([
16276
+ "codex",
16277
+ "copilot-cli",
16278
+ "gemini-cli",
16279
+ "grok",
16280
+ "antigravity",
16281
+ "cursor"
16282
+ ]);
16283
+ function usesWaiter(env) {
16284
+ try {
16285
+ return timerKind(timerDeps(env).timers) === "waiter";
16286
+ } catch {
16287
+ return false;
16288
+ }
16289
+ }
16290
+ function loginWouldAdd(env, places) {
16291
+ if (!places.some((p) => TIMER_PLACES.has(p))) return false;
16292
+ const { state, node } = timerDeps(env);
16293
+ const item = loginItem({
16294
+ platform: process.platform,
16295
+ home: env.HOME || env.USERPROFILE || homedir19(),
16296
+ node,
16297
+ cli: rewakeCli(state),
16298
+ stateDir: state,
16299
+ run: () => ({ status: 0 }),
16300
+ exists: existsSync23
16301
+ });
16302
+ return item !== void 0 && !existsSync23(item.path);
16303
+ }
16304
+ function syncLogin(env) {
16305
+ try {
16306
+ const { state, node, timers } = timerDeps(env);
16307
+ const home = env.HOME || env.USERPROFILE || homedir19();
16308
+ const wanted = installedPreviews(env, home, state).some((p) => TIMER_PLACES.has(p));
16309
+ return syncLoginItem(
16310
+ {
16311
+ platform: process.platform,
16312
+ home,
16313
+ node,
16314
+ cli: rewakeCli(state),
16315
+ stateDir: state,
16316
+ run: (cmd, args) => timers.run(cmd, args),
16317
+ exists: existsSync23
16318
+ },
16319
+ wanted
16320
+ );
16321
+ } catch {
16322
+ return "unchanged";
16323
+ }
16324
+ }
15632
16325
  function sweepQuietly(env) {
15633
16326
  try {
15634
16327
  const { sweepDeps } = timerDeps(env);
@@ -15722,7 +16415,7 @@ async function runHookCommand(host, event, env) {
15722
16415
  agent: () => agentProcess()
15723
16416
  }),
15724
16417
  program: (h, e) => {
15725
- const host2 = { env: e, home: homedir17(), platform: process.platform };
16418
+ const host2 = { env: e, home: homedir19(), platform: process.platform };
15726
16419
  if (h === "copilot-cli") return copilotPrograms(host2)[0]?.path;
15727
16420
  if (h === "grok") return grokPrograms(host2)[0]?.path;
15728
16421
  if (h === "gemini-cli") return geminiPrograms(host2)[0]?.path;
@@ -15730,7 +16423,7 @@ async function runHookCommand(host, event, env) {
15730
16423
  return void 0;
15731
16424
  },
15732
16425
  codexPath: () => {
15733
- const programs2 = codexPrograms({ env, home: homedir17(), platform: process.platform });
16426
+ const programs2 = codexPrograms({ env, home: homedir19(), platform: process.platform });
15734
16427
  const cli = programs2.filter((p) => p.surface === "terminal");
15735
16428
  const pool = cli.length > 0 ? cli : programs2;
15736
16429
  return pool.sort((a, b) => compareVersions(b.version ?? "0", a.version ?? "0"))[0]?.path;
@@ -15783,7 +16476,7 @@ async function runContinueCommand(env, mode) {
15783
16476
  });
15784
16477
  }
15785
16478
  function previewNames(env) {
15786
- return installedPreviews(env, homedir17(), stateDir(env)).map((id) => PREVIEW_NAMES[id] ?? id);
16479
+ return installedPreviews(env, homedir19(), stateDir(env)).map((id) => PREVIEW_NAMES[id] ?? id);
15787
16480
  }
15788
16481
 
15789
16482
  // src/main.ts