@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 +13 -8
- package/index.html +50 -22
- package/node/app.ts +4 -1
- package/node/db.ts +4 -2
- package/node/mcp.ts +1 -0
- package/node/scheduler.ts +6 -4
- package/package.json +1 -1
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` —
|
|
13
|
-
3.
|
|
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 |
|
|
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
|
|
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
|
+

|
|
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
|
|
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
|
+

|
|
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 --
|
|
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
|
|
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">
|
|
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
|
|
310
|
-
<div class="setting-hint">
|
|
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
|
-
<
|
|
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
|
|
317
|
-
<div>
|
|
318
|
-
<div
|
|
319
|
-
|
|
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-
|
|
322
|
-
<
|
|
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 ??
|
|
484
|
-
const endH = thr.coding_end_h ??
|
|
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)
|
|
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
|
|
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} ·
|
|
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
|
-
|
|
60
|
-
|
|
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 =
|
|
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
|
|
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 =
|
|
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,
|