@agentmessier/restwalker 1.0.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,22 +9,22 @@ Runs as a LaunchAgent on port **47290** with a SQLite database, a dashboard UI,
9
9
  ## How it works
10
10
 
11
11
  1. You add tasks to the queue (via dashboard, REST API, or MCP tools in Claude Code)
12
- 2. The gate checks live Claude usage from `api.anthropic.com` — same source as `ccstatusline`
13
- 3. When usage is low and you're outside your coding window, tasks run automatically
12
+ 2. The gate checks live Claude usage from `api.anthropic.com` — Claude tracks your token spend in rolling 5-hour and weekly windows
13
+ 3. **All gates must be open simultaneously** for a task to run — coding window, 5h budget, and weekly budget
14
14
  4. Sessions are recorded; results, token counts, and transcripts are stored per task
15
15
 
16
16
  ### Budget gates (all configurable)
17
17
 
18
18
  | Gate | Default | Behaviour |
19
19
  |---|---|---|
20
- | Coding window | 4 PM 2 AM | Always paused during active hours |
21
- | 5h usage | ≥ 75% | Pause to protect interactive budget |
20
+ | Coding window | disabled | Optional enable in Settings to pause during a set time range each day |
21
+ | 5h rolling usage | ≥ 75% | Pause to protect your interactive budget |
22
22
  | Weekly ceiling | ≥ 65% | Pause background jobs |
23
23
  | Weekly hard stop | ≥ 90% | Hard stop regardless |
24
24
 
25
25
  ## Install
26
26
 
27
- **Requirements:** macOS, Node.js 20+, Claude Code CLI (must be logged in)
27
+ **Requirements:** macOS, Node.js 20+, Claude Code CLI (`claude login` must have been run — restwalker reads your credentials from the macOS Keychain)
28
28
 
29
29
  ```bash
30
30
  git clone https://github.com/agentmessier-ai/restwalker.git
@@ -57,14 +57,18 @@ Stops the service, removes the LaunchAgent, and optionally deletes `~/.restwalke
57
57
 
58
58
  `http://localhost:47290`
59
59
 
60
+ ![Dashboard](docs/screenshot-dashboard.png)
61
+
60
62
  - Live gate status, 5h and weekly usage, next window
61
63
  - 48h trend chart with threshold overlays and coding-window shading
62
64
  - Task queue: add, paginate, expand rows to view session transcripts and reasoning blocks
63
- - Agent providers: configure which CLI runs tasks and with what arguments
65
+ - Agent providers: configure which CLI runs tasks (a provider is a command template like `claude --print {{task}}`)
64
66
  - Settings: all thresholds configurable without restart
65
67
 
66
68
  ## Task queue
67
69
 
70
+ ![Queue](docs/screenshot-queue.png)
71
+
68
72
  Tasks have a description (the prompt), an optional working directory, model, provider, and schedule:
69
73
 
70
74
  | Schedule | Behaviour |
@@ -88,10 +92,11 @@ The MCP server (`node/mcp.ts`) exposes 17 tools for Claude Code via stdio transp
88
92
  | Discovery | `list_models`, `list_projects` |
89
93
  | Settings | `get_settings`, `update_settings` |
90
94
 
91
- Register manually if you skipped it during install:
95
+ Register manually if you skipped it during install (replace `~/dev/restwalker` with your clone path):
92
96
 
93
97
  ```bash
94
- claude mcp add --scope user restwalker -- node /path/to/node_modules/.bin/tsx /path/to/node/mcp.ts
98
+ claude mcp add --scope user restwalker -- \
99
+ node ~/dev/restwalker/node/node_modules/.bin/tsx ~/dev/restwalker/node/mcp.ts
95
100
  ```
96
101
 
97
102
  ## API
package/index.html CHANGED
@@ -214,7 +214,7 @@
214
214
  <div class="sub" id="card-weekly-sub"></div>
215
215
  </div>
216
216
  <div class="card">
217
- <div class="label">Next Milestone</div>
217
+ <div class="label">Next Reset</div>
218
218
  <div class="value" id="card-next" style="font-size:18px;padding-top:4px">—</div>
219
219
  <div class="sub" id="card-next-sub"></div>
220
220
  </div>
@@ -298,7 +298,7 @@
298
298
  <div class="setting-row">
299
299
  <div>
300
300
  <div class="setting-label">Timezone</div>
301
- <div class="setting-hint">Coding window hours are interpreted in this timezone</div>
301
+ <div class="setting-hint">Used for coding window hours</div>
302
302
  </div>
303
303
  <div class="setting-input-wrap">
304
304
  <select class="setting-input" id="s-TIMEZONE" style="width:14rem"></select>
@@ -306,20 +306,34 @@
306
306
  </div>
307
307
  <div class="setting-row">
308
308
  <div>
309
- <div class="setting-label">Coding window start</div>
310
- <div class="setting-hint">Jobs pause after this time</div>
309
+ <div class="setting-label">Coding window</div>
310
+ <div class="setting-hint">Pause jobs during a set time range each day</div>
311
311
  </div>
312
- <div class="setting-input-wrap">
313
- <select class="setting-input" id="s-CODING_START_H" style="width:9rem"></select>
312
+ <div class="setting-input-wrap" style="display:flex;align-items:center;gap:8px">
313
+ <label style="display:flex;align-items:center;gap:6px;cursor:pointer;font-size:13px;color:var(--text)">
314
+ <input type="checkbox" id="s-CODING_WINDOW_ENABLED" onchange="toggleCodingWindow(this.checked)" style="width:15px;height:15px;cursor:pointer;accent-color:var(--blue)">
315
+ Enable
316
+ </label>
314
317
  </div>
315
318
  </div>
316
- <div class="setting-row">
317
- <div>
318
- <div class="setting-label">Coding window end</div>
319
- <div class="setting-hint">Jobs resume after this time</div>
319
+ <div id="coding-window-times" style="display:none">
320
+ <div class="setting-row" style="padding-left:16px">
321
+ <div>
322
+ <div class="setting-label">Start</div>
323
+ <div class="setting-hint">Jobs pause from this hour</div>
324
+ </div>
325
+ <div class="setting-input-wrap">
326
+ <select class="setting-input" id="s-CODING_START_H" style="width:9rem"></select>
327
+ </div>
320
328
  </div>
321
- <div class="setting-input-wrap">
322
- <select class="setting-input" id="s-CODING_END_H" style="width:9rem"></select>
329
+ <div class="setting-row" style="padding-left:16px">
330
+ <div>
331
+ <div class="setting-label">End</div>
332
+ <div class="setting-hint">Jobs resume at this hour</div>
333
+ </div>
334
+ <div class="setting-input-wrap">
335
+ <select class="setting-input" id="s-CODING_END_H" style="width:9rem"></select>
336
+ </div>
323
337
  </div>
324
338
  </div>
325
339
  </div>
@@ -444,10 +458,10 @@ function barColor(pct, warnAt, stopAt) {
444
458
  function isCodingHour(date, startH, endH) {
445
459
  try {
446
460
  const h = parseInt(new Intl.DateTimeFormat('en-US',{timeZone:tz,hour:'numeric',hour12:false}).format(date));
447
- return h >= startH || h < endH;
461
+ return startH < endH ? h >= startH && h < endH : h >= startH || h < endH;
448
462
  } catch {
449
463
  const pstH = ((date.getUTCHours() - 8 + 24) % 24);
450
- return pstH >= startH || pstH < endH;
464
+ return startH < endH ? pstH >= startH && pstH < endH : pstH >= startH || pstH < endH;
451
465
  }
452
466
  }
453
467
  function slope(points) {
@@ -480,9 +494,9 @@ function buildCodingBands(startMs, endMs, startH, endH) {
480
494
  function renderChart(fiveH, weekly, startMs, endMs, thr) {
481
495
  const ctx = document.getElementById('chart').getContext('2d');
482
496
  if (chart) chart.destroy();
483
- const startH = thr.coding_start_h ?? 16;
484
- const endH = thr.coding_end_h ?? 2;
485
- const coding = buildCodingBands(startMs, endMs, startH, endH);
497
+ const startH = thr.coding_start_h ?? 9;
498
+ const endH = thr.coding_end_h ?? 18;
499
+ const coding = thr.coding_window_enabled ? buildCodingBands(startMs, endMs, startH, endH) : [];
486
500
 
487
501
  chart = new Chart(ctx, {
488
502
  type: 'line',
@@ -690,7 +704,11 @@ function buildHourSelects() {
690
704
  });
691
705
  }
692
706
 
693
- const SETTING_KEYS = ['CODING_START_H','CODING_END_H','TIMEZONE','FIVE_HOUR_PAUSE_PCT','WEEKLY_RESERVE_PCT','WEEKLY_HARD_STOP_PCT','POLL_INTERVAL_MIN','CACHE_STALE_MIN'];
707
+ const SETTING_KEYS = ['CODING_WINDOW_ENABLED','CODING_START_H','CODING_END_H','TIMEZONE','FIVE_HOUR_PAUSE_PCT','WEEKLY_RESERVE_PCT','WEEKLY_HARD_STOP_PCT','POLL_INTERVAL_MIN','CACHE_STALE_MIN'];
708
+
709
+ function toggleCodingWindow(enabled) {
710
+ document.getElementById('coding-window-times').style.display = enabled ? 'block' : 'none';
711
+ }
694
712
 
695
713
  async function openSettings() {
696
714
  buildHourSelects();
@@ -702,6 +720,7 @@ async function openSettings() {
702
720
  for (const k of SETTING_KEYS) {
703
721
  const el = document.getElementById('s-'+k);
704
722
  if (!el) continue;
723
+ if (el.type === 'checkbox') { el.checked = cfg[k] === '1'; toggleCodingWindow(el.checked); continue; }
705
724
  el.value = cfg[k] ?? '';
706
725
  const vEl = document.getElementById('v-'+k);
707
726
  if (vEl && SLIDER_KEYS[k]) vEl.textContent = (cfg[k] ?? '') + SLIDER_KEYS[k];
@@ -723,7 +742,8 @@ async function saveSettings() {
723
742
  const body = {};
724
743
  for (const k of SETTING_KEYS) {
725
744
  const el = document.getElementById('s-'+k);
726
- if (el) body[k] = el.value;
745
+ if (!el) continue;
746
+ body[k] = el.type === 'checkbox' ? (el.checked ? '1' : '0') : el.value;
727
747
  }
728
748
  const res = await fetch('/settings', { method:'POST', headers:{'Content-Type':'application/json'}, body:JSON.stringify(body) });
729
749
  if (res.ok) {
@@ -857,7 +877,8 @@ async function loadProjects() {
857
877
  sel.innerHTML =
858
878
  `<option value="">— home dir (blank) —</option>` +
859
879
  projects.map(p => {
860
- const label = p.cwd.replace('/Users/missa/', '~/').replace('/Users/missa', '~')
880
+ const home = data?.home ?? ''
881
+ const label = home ? p.cwd.replace(home + '/', '~/').replace(home, '~') : p.cwd
861
882
  return `<option value="${p.cwd}">${label}</option>`
862
883
  }).join('') +
863
884
  `<option value="__custom__">Other…</option>`
@@ -906,7 +927,7 @@ function scheduleChip(t) {
906
927
  if (!t.schedule || t.schedule === 'once') return ''
907
928
  const label = SCHEDULE_LABELS[t.schedule] || t.schedule
908
929
  if (t.status === 'scheduled' && t.next_run_at) {
909
- return `<span style="font-size:10px;background:rgba(167,139,250,.15);color:#a78bfa;padding:1px 6px;border-radius:3px;margin-left:4px">${label} · runs ${qTimeSince(t.next_run_at)}</span>`
930
+ return `<span style="font-size:10px;background:rgba(167,139,250,.15);color:#a78bfa;padding:1px 6px;border-radius:3px;margin-left:4px">${label} · next run ${qTimeSince(t.next_run_at)}</span>`
910
931
  }
911
932
  return `<span style="font-size:10px;background:rgba(167,139,250,.15);color:#a78bfa;padding:1px 6px;border-radius:3px;margin-left:4px">${label}</span>`
912
933
  }
@@ -914,9 +935,16 @@ function scheduleChip(t) {
914
935
  function qTimeSince(iso) {
915
936
  if (!iso) return ''
916
937
  const d = (Date.now() - new Date(iso).getTime()) / 1000
938
+ if (d < 0) {
939
+ const f = -d
940
+ if (f < 3600) return `in ${Math.round(f/60)}m`
941
+ if (f < 86400) return `in ${Math.round(f/3600)}h`
942
+ return `in ${Math.round(f/86400)}d`
943
+ }
917
944
  if (d < 60) return `${Math.round(d)}s ago`
918
945
  if (d < 3600) return `${Math.round(d/60)}m ago`
919
- return `${Math.round(d/3600)}h ago`
946
+ if (d < 86400) return `${Math.round(d/3600)}h ago`
947
+ return `${Math.round(d/86400)}d ago`
920
948
  }
921
949
 
922
950
  function qBadge(status) {
package/node/app.ts CHANGED
@@ -219,6 +219,7 @@ app.get('/status', {
219
219
  thresholds: {
220
220
  type: 'object',
221
221
  properties: {
222
+ coding_window_enabled: { type: 'boolean' },
222
223
  coding_start_h: { type: 'number' },
223
224
  coding_end_h: { type: 'number' },
224
225
  timezone: { type: 'string' },
@@ -246,7 +247,7 @@ app.get('/status', {
246
247
  const tz = cfg.TIMEZONE
247
248
 
248
249
  return {
249
- window: scheduler.isCodingWindow(now, startH, endH, tz) ? 'coding' : 'idle',
250
+ window: cfg.CODING_WINDOW_ENABLED === '1' && scheduler.isCodingWindow(now, startH, endH, tz) ? 'coding' : 'idle',
250
251
  next_idle_in_s: scheduler.nextIdleInS(now, startH, endH, tz),
251
252
  ok: decision.ok,
252
253
  provider: decision.provider,
@@ -261,6 +262,7 @@ app.get('/status', {
261
262
  },
262
263
  last_db_snapshot: snap,
263
264
  thresholds: {
265
+ coding_window_enabled: cfg.CODING_WINDOW_ENABLED === '1',
264
266
  coding_start_h: startH,
265
267
  coding_end_h: endH,
266
268
  timezone: tz,
@@ -471,6 +473,7 @@ app.get('/projects', {
471
473
  } catch { return { projects: [] } }
472
474
 
473
475
  return {
476
+ home: homedir(),
474
477
  projects: [...seen.entries()]
475
478
  .sort((a, b) => b[1] - a[1])
476
479
  .map(([cwd, ts]) => ({ cwd, last_active: new Date(ts).toISOString() }))
package/node/db.ts CHANGED
@@ -24,6 +24,7 @@ const db = drizzle(client, { schema })
24
24
  // ── Types ──────────────────────────────────────────────────────────────────────
25
25
 
26
26
  export interface Settings {
27
+ CODING_WINDOW_ENABLED: string // '0' or '1'
27
28
  CODING_START_H: string
28
29
  CODING_END_H: string
29
30
  TIMEZONE: string
@@ -56,8 +57,9 @@ export interface Snapshot {
56
57
  }
57
58
 
58
59
  export const SETTING_DEFAULTS: Settings = {
59
- CODING_START_H: process.env.CODING_START_H ?? '16',
60
- CODING_END_H: process.env.CODING_END_H ?? '2',
60
+ CODING_WINDOW_ENABLED: process.env.CODING_WINDOW_ENABLED ?? '0',
61
+ CODING_START_H: process.env.CODING_START_H ?? '9',
62
+ CODING_END_H: process.env.CODING_END_H ?? '18',
61
63
  TIMEZONE: process.env.TIMEZONE ?? 'America/Los_Angeles',
62
64
  FIVE_HOUR_PAUSE_PCT: process.env.FIVE_HOUR_PAUSE_PCT ?? '75',
63
65
  WEEKLY_RESERVE_PCT: process.env.WEEKLY_RESERVE_PCT ?? '35',
package/node/mcp.ts CHANGED
@@ -196,6 +196,7 @@ server.tool(
196
196
  'update_settings',
197
197
  'Update one or more daemon settings',
198
198
  {
199
+ CODING_WINDOW_ENABLED: z.enum(['0','1']).optional().describe('Enable coding window time gate (1=on, 0=off)'),
199
200
  CODING_START_H: z.string().optional().describe('Hour (0-23) coding window starts'),
200
201
  CODING_END_H: z.string().optional().describe('Hour (0-23) coding window ends'),
201
202
  TIMEZONE: z.string().optional().describe('IANA timezone, e.g. America/Los_Angeles'),
package/node/scheduler.ts CHANGED
@@ -162,12 +162,14 @@ function localHour(date: Date, tz: string): number {
162
162
  }
163
163
  }
164
164
 
165
- export function isCodingWindow(now = new Date(), startH = 16, endH = 2, tz = 'America/Los_Angeles'): boolean {
165
+ export function isCodingWindow(now = new Date(), startH = 0, endH = 0, tz = 'America/Los_Angeles'): boolean {
166
+ if (startH === endH) return false // 0,0 = disabled
166
167
  const h = localHour(now, tz)
167
- return h >= startH || h < endH
168
+ if (startH < endH) return h >= startH && h < endH
169
+ return h >= startH || h < endH // wraps midnight e.g. 22–6
168
170
  }
169
171
 
170
- export function nextIdleInS(now = new Date(), startH = 16, endH = 2, tz = 'America/Los_Angeles'): number {
172
+ export function nextIdleInS(now = new Date(), startH = 0, endH = 0, tz = 'America/Los_Angeles'): number {
171
173
  if (!isCodingWindow(now, startH, endH, tz)) return 0
172
174
  const h = localHour(now, tz)
173
175
  const m = now.getMinutes()
@@ -191,7 +193,7 @@ export async function canRun(usage: UsageData | null, cfg: Settings): Promise<Ca
191
193
 
192
194
  const now = new Date()
193
195
 
194
- if (isCodingWindow(now, startH, endH, tz)) {
196
+ if (cfg.CODING_WINDOW_ENABLED === '1' && isCodingWindow(now, startH, endH, tz)) {
195
197
  const idleIn = nextIdleInS(now, startH, endH, tz)
196
198
  return {
197
199
  ok: false, provider: null, next_idle_in_s: idleIn,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentmessier/restwalker",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "While you rest, it walks — idle-time Claude task runner for Mac",
5
5
  "type": "module",
6
6
  "bin": {