@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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rewake",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Continues this session when your usage limit resets, and sends messages you schedule into it.",
5
5
  "author": {
6
6
  "name": "Codize Labs",
@@ -144,7 +144,7 @@ const startOfDay = (ms) => {
144
144
  };
145
145
 
146
146
  /**
147
- * "3:05 PM today", "9:00 AM tomorrow (Thursday)", "Friday at 9:00 AM", "Monday 12 October at
147
+ * "3:05 PM today", "9:00 AM tomorrow, Thursday", "Friday at 9:00 AM", "Monday 12 October at
148
148
  * 9:00 AM": Rewake's own wording for times (src/core/time.ts formatWhen), in the host's time zone.
149
149
  */
150
150
  export function when(ms, now, clock = "12h") {
@@ -152,7 +152,7 @@ export function when(ms, now, clock = "12h") {
152
152
  const dayDiff = Math.round((startOfDay(ms) - startOfDay(now)) / 86_400_000);
153
153
  const weekday = new Intl.DateTimeFormat("en-US", { weekday: "long" }).format(ms);
154
154
  if (dayDiff === 0) return `${time} today`;
155
- if (dayDiff === 1) return `${time} tomorrow (${weekday})`;
155
+ if (dayDiff === 1) return `${time} tomorrow, ${weekday}`;
156
156
  if (dayDiff > 1 && dayDiff < 7) return `${weekday} at ${time}`;
157
157
  const date = new Intl.DateTimeFormat("en-US", { day: "numeric", month: "long" }).format(ms);
158
158
  return `${weekday} ${date} at ${time}`;
@@ -56,6 +56,13 @@ const QUESTION =
56
56
 
57
57
  let isInteractive = false;
58
58
  let surface = null;
59
+ /**
60
+ * Claude Code's own "Continue automatically at usage limit" as this session started with it: the
61
+ * running Claude Code keeps that value, so a change in the settings file mid-session doesn't count.
62
+ */
63
+ let nativeSetting;
64
+ /** How long after the reset Claude Code's own continue may take before Rewake steps in. */
65
+ const NATIVE_GRACE_MS = 2 * 60_000;
59
66
  /** How Claude Code was started (`CLAUDE_CODE_ENTRYPOINT`): "cli", "claude-vscode", "sdk-ts"… */
60
67
  let entrypoint;
61
68
  /** Sessions whose limit is being handled right now (a burst of StopFailures is one limit). */
@@ -67,6 +74,10 @@ let clock = "12h";
67
74
  let armed;
68
75
  /** `<stateDir>/hosts/claude-code/sessions`, when the installer said where the state folder is. */
69
76
  let mirrorDir;
77
+ /** Rewake's shared "automatic resume" (settings.json newThreads): "on" counts as "always" here. */
78
+ let sharedAuto;
79
+ /** The session's folder, for where Rewake's CLI names it ("in the shop folder"). */
80
+ let sessionCwd;
70
81
  /** This process, in a send's claim: two Claude Code processes can have one session open. */
71
82
  const ME = Math.random().toString(36).slice(2);
72
83
  /** A claim older than this was left by a process that stopped mid-send. */
@@ -95,6 +106,7 @@ async function loadConfig($) {
95
106
  try {
96
107
  const settings = JSON.parse(await $.fs.read(`${cfg.stateDir}/settings.json`));
97
108
  if (settings.clock === "24h" || settings.clock === "12h") clock = settings.clock;
109
+ if (["on", "off", "ask"].includes(settings.newThreads)) sharedAuto = settings.newThreads;
98
110
  if (["plugged-in", "always", "never"].includes(settings.keepAwake))
99
111
  keepAwake = settings.keepAwake;
100
112
  } catch {
@@ -115,6 +127,7 @@ async function mirror($, id, ep) {
115
127
  schemaVersion: 1,
116
128
  host: "claude-code",
117
129
  sessionId: id,
130
+ ...(typeof sessionCwd === "string" && { cwd: sessionCwd }),
118
131
  state,
119
132
  kind,
120
133
  resetAt,
@@ -221,7 +234,10 @@ async function refreshStatus($, id, now) {
221
234
 
222
235
  /** A limit whose reset time is known: leave it to Claude Code, or ask. */
223
236
  async function classify($, id, ep, now) {
224
- const { autoContinueAtUsageLimit } = await $.settings.read();
237
+ const autoContinueAtUsageLimit =
238
+ nativeSetting === undefined
239
+ ? (await $.settings.read()).autoContinueAtUsageLimit
240
+ : nativeSetting.value;
225
241
  if (
226
242
  nativeLikely({
227
243
  isInteractive,
@@ -238,11 +254,26 @@ async function classify($, id, ep, now) {
238
254
  await ask($, id, ep, now);
239
255
  }
240
256
 
257
+ /**
258
+ * The person's choice for this plugin: what they said here ("always" or "ask"), else Rewake's
259
+ * shared setting (turned on with `agent-rewake continue --always` or in Zed's Settings).
260
+ */
261
+ function autoChoice(prefs) {
262
+ return prefs.autoContinue ?? (sharedAuto === "on" ? "always" : undefined);
263
+ }
264
+
265
+ /** Why Rewake continues without asking here: the person's choice in Claude Code, or the shared one. */
266
+ function autoText(prefs) {
267
+ return prefs.autoContinue === "always"
268
+ ? "After a usage limit, Rewake continues without asking when the reset is within a day. To be asked each time: /rewake auto off"
269
+ : "Rewake's setting for every agent is on (agent-rewake continue --always, or Zed's Settings), so after a usage limit it continues without asking when the reset is within a day. To be asked in Claude Code: /rewake auto off";
270
+ }
271
+
241
272
  async function ask($, id, ep, now) {
242
273
  const prefs = (await $.store.get("prefs")) ?? {};
243
274
  let state = "armed";
244
275
  // A reset more than a day away is always asked about, even with "always".
245
- if (mustAsk({ autoContinue: prefs.autoContinue, fireAt: ep.fireAt, now, surface })) {
276
+ if (mustAsk({ autoContinue: autoChoice(prefs), fireAt: ep.fireAt, now, surface })) {
246
277
  const yes = `Continue at ${at(ep.fireAt, now)}`;
247
278
  const always = `${yes}, and from now on in every session when the reset is within a day`;
248
279
  try {
@@ -438,6 +469,10 @@ async function tick($) {
438
469
  await refreshStatus($, id, now);
439
470
  }
440
471
  if (ep?.state === "armed" && ep.fireAt !== undefined && now >= ep.fireAt) return fire($, id);
472
+ // Claude Code was expected to continue by itself, but hasn't said so since the reset: Rewake
473
+ // asks (or, with "always", continues), so a session never waits with nobody to continue it.
474
+ if (ep?.state === "native" && ep.resetAt !== undefined && now >= ep.resetAt + NATIVE_GRACE_MS)
475
+ return ask($, id, { ...ep, fireAt: now }, now);
441
476
  await updateWake($, ep, now);
442
477
  // Scheduled messages wait while a limit is pending; one per tick.
443
478
  if (busy || (ep && ep.state !== "sent")) return;
@@ -492,6 +527,12 @@ async function addMessage($, id, when, text) {
492
527
  * `/rewake` in a session that isn't waiting at a limit: one question per step, when (presets with
493
528
  * their times, or Custom…), then what. Dismissing the first question shows what's scheduled.
494
529
  */
530
+ /** Messages to pick in `/rewake`'s second question; "Other" types any message. */
531
+ const MESSAGE_PRESETS = [
532
+ "Continue from where you left off.",
533
+ "Check where things stand and report back.",
534
+ ];
535
+
495
536
  async function scheduleFlow($, id, now) {
496
537
  const presets = [
497
538
  ["In 30 minutes", 30 * MINUTE],
@@ -506,11 +547,27 @@ async function scheduleFlow($, id, now) {
506
547
  header: "Rewake",
507
548
  });
508
549
  when = presets.find((p) => p.label === answer)?.at;
550
+ // A time typed in the question's own free-text choice ("Other", "Type something").
551
+ if (when === undefined && answer !== custom && String(answer).trim() !== "") {
552
+ const p = parseWhen(String(answer).replace(/^\s*at\s+/i, ""), now);
553
+ if (!p.ok) return `Rewake: ${p.error}`;
554
+ when = p.at;
555
+ }
509
556
  if (when === undefined && answer === custom) {
510
- const typed = await $.ui.ask(
511
- `When? For example "in 45m", "${clock === "24h" ? "18:00" : "6pm"}" or "tomorrow 9:00".`,
557
+ // Real choices, not a bare question: Claude Code's VS Code and Cursor panels answer a
558
+ // question without options with Yes/No. "Other" (the panels' own) types any time.
559
+ const examples = ["in 45m", clock === "24h" ? "18:00" : "6pm", "tomorrow 9:00"]
560
+ .map((t) => ({ t, p: parseWhen(t, now) }))
561
+ .filter((e) => e.p.ok)
562
+ .map((e) => ({ label: `${e.t} (${at(e.p.at, now)})`, t: e.t }));
563
+ const typed = String(
564
+ await $.ui.ask("When? Pick one, or type a time.", {
565
+ options: examples.map((e) => e.label),
566
+ header: "Rewake",
567
+ }),
512
568
  );
513
- const p = parseWhen(String(typed).replace(/^\s*at\s+/i, ""), now);
569
+ const picked = examples.find((e) => e.label === typed)?.t ?? typed;
570
+ const p = parseWhen(picked.replace(/^\s*at\s+/i, ""), now);
514
571
  if (!p.ok) return `Rewake: ${p.error}`;
515
572
  when = p.at;
516
573
  }
@@ -521,7 +578,10 @@ async function scheduleFlow($, id, now) {
521
578
  let text;
522
579
  try {
523
580
  text = String(
524
- await $.ui.ask(`What should Rewake send into this session ${atWhen(when, now)}?`),
581
+ await $.ui.ask(
582
+ `What should Rewake send into this session ${atWhen(when, now)}? Pick one, or type your own.`,
583
+ { options: MESSAGE_PRESETS, header: "Rewake" },
584
+ ),
525
585
  ).trim();
526
586
  } catch {
527
587
  return "Nothing was scheduled.";
@@ -542,10 +602,7 @@ async function listText($, id, now) {
542
602
  for (const [i, s] of sched.entries()) lines.push(`${i + 1}. ${at(s.at, now)}: ${s.text}`);
543
603
  if (lines.length === 0) lines.push("Nothing scheduled in this session.");
544
604
  const prefs = (await $.store.get("prefs")) ?? {};
545
- if (prefs.autoContinue === "always")
546
- lines.push(
547
- "After a usage limit, Rewake continues without asking when the reset is within a day. To be asked each time: /rewake auto off",
548
- );
605
+ if (autoChoice(prefs) === "always") lines.push(autoText(prefs));
549
606
  return lines.join("\n");
550
607
  }
551
608
 
@@ -650,12 +707,13 @@ async function runCommand($, args) {
650
707
  text =
651
708
  "From now on, after a usage limit Rewake continues every session without asking when the reset is within a day. To be asked each time: /rewake auto off";
652
709
  } else if (c.on === false) {
653
- await $.store.set("prefs", rest);
710
+ // "ask" stays even when Rewake's shared setting is on: the person asked for it here.
711
+ await $.store.set("prefs", { ...rest, autoContinue: "ask" });
654
712
  text = "Rewake will ask before continuing after a usage limit.";
655
713
  } else {
656
714
  text =
657
- prefs.autoContinue === "always"
658
- ? "After a usage limit, Rewake continues without asking when the reset is within a day. To be asked each time: /rewake auto off"
715
+ autoChoice(prefs) === "always"
716
+ ? autoText(prefs)
659
717
  : "After a usage limit, Rewake asks before continuing. To continue without asking: /rewake auto on";
660
718
  }
661
719
  } else {
@@ -671,8 +729,14 @@ const passOn = (_$, e, next) => (next.called ? undefined : next(e));
671
729
  export function register(on) {
672
730
  on("session.start", async ($, e, next) => {
673
731
  isInteractive = e.isInteractive;
732
+ sessionCwd = e.cwd;
674
733
  entrypoint = await $.env.get("CLAUDE_CODE_ENTRYPOINT").catch(() => undefined);
675
734
  surface = surfaceOf({ surface: e.surface, entrypoint });
735
+ try {
736
+ nativeSetting = { value: (await $.settings.read()).autoContinueAtUsageLimit };
737
+ } catch {
738
+ nativeSetting = undefined;
739
+ }
676
740
  await loadConfig($);
677
741
  $.clock.every(TICK_MS, () => void tick($));
678
742
  void reopen($);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codizelabs/agent-rewake",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Auto-resume Claude Code, Codex, Gemini CLI, GitHub Copilot, Grok or any Zed agent when its usage limit resets, in the same session, and schedule messages into your agent threads.",
5
5
  "keywords": [
6
6
  "zed",