remote-pi 0.4.3 → 0.5.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.
- package/README.md +33 -0
- package/dist/daemon/client.js +5 -2
- package/dist/daemon/client.js.map +1 -1
- package/dist/daemon/control_protocol.d.ts +68 -0
- package/dist/daemon/control_protocol.js.map +1 -1
- package/dist/daemon/cron_log.d.ts +45 -0
- package/dist/daemon/cron_log.js +72 -0
- package/dist/daemon/cron_log.js.map +1 -0
- package/dist/daemon/cron_registry.d.ts +80 -0
- package/dist/daemon/cron_registry.js +194 -0
- package/dist/daemon/cron_registry.js.map +1 -0
- package/dist/daemon/id.d.ts +6 -0
- package/dist/daemon/id.js +6 -0
- package/dist/daemon/id.js.map +1 -1
- package/dist/daemon/install.d.ts +9 -2
- package/dist/daemon/install.js +54 -10
- package/dist/daemon/install.js.map +1 -1
- package/dist/daemon/registry.d.ts +17 -1
- package/dist/daemon/registry.js +34 -4
- package/dist/daemon/registry.js.map +1 -1
- package/dist/daemon/rpc_child.d.ts +60 -0
- package/dist/daemon/rpc_child.js +150 -5
- package/dist/daemon/rpc_child.js.map +1 -1
- package/dist/daemon/supervisor.d.ts +44 -0
- package/dist/daemon/supervisor.js +256 -21
- package/dist/daemon/supervisor.js.map +1 -1
- package/dist/index.d.ts +68 -11
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/mesh_server.js +18 -9
- package/dist/mcp/mesh_server.js.map +1 -1
- package/dist/pairing/qr.d.ts +8 -1
- package/dist/pairing/qr.js +13 -3
- package/dist/pairing/qr.js.map +1 -1
- package/dist/protocol/codec.js +1 -0
- package/dist/protocol/codec.js.map +1 -1
- package/dist/protocol/types.d.ts +11 -0
- package/dist/rooms.d.ts +20 -0
- package/dist/rooms.js +35 -0
- package/dist/rooms.js.map +1 -1
- package/dist/session/broker.d.ts +79 -3
- package/dist/session/broker.js +155 -28
- package/dist/session/broker.js.map +1 -1
- package/dist/session/broker_remote.d.ts +30 -10
- package/dist/session/broker_remote.js +77 -39
- package/dist/session/broker_remote.js.map +1 -1
- package/dist/session/cwd_lock.d.ts +7 -2
- package/dist/session/cwd_lock.js +39 -9
- package/dist/session/cwd_lock.js.map +1 -1
- package/dist/session/global_config.d.ts +12 -2
- package/dist/session/global_config.js +16 -3
- package/dist/session/global_config.js.map +1 -1
- package/dist/session/ipc.d.ts +27 -0
- package/dist/session/ipc.js +22 -0
- package/dist/session/ipc.js.map +1 -0
- package/dist/session/leader_election.js +8 -2
- package/dist/session/leader_election.js.map +1 -1
- package/dist/session/local_config.d.ts +36 -6
- package/dist/session/local_config.js +103 -28
- package/dist/session/local_config.js.map +1 -1
- package/dist/session/mesh_node.d.ts +15 -1
- package/dist/session/mesh_node.js +26 -5
- package/dist/session/mesh_node.js.map +1 -1
- package/dist/session/peer.d.ts +19 -2
- package/dist/session/peer.js +45 -9
- package/dist/session/peer.js.map +1 -1
- package/dist/session/tools.js +14 -12
- package/dist/session/tools.js.map +1 -1
- package/package.json +2 -1
- package/service-templates/task-scheduler.xml.template +38 -0
- package/skills/agent-network/SKILL.md +63 -26
package/README.md
CHANGED
|
@@ -384,6 +384,12 @@ real name to the peer.
|
|
|
384
384
|
| `/remote-pi daemon restart` | Stop + start all daemons |
|
|
385
385
|
| `/remote-pi daemon status` | Detailed runtime status (pid, uptime, restart count) |
|
|
386
386
|
| `/remote-pi daemon send <id> "<text>"` | Send a prompt to a specific daemon |
|
|
387
|
+
| `/remote-pi cron add <id> "<expr>" "<prompt>"` | Schedule a recurring prompt (`--tz`, `--wake`, `--no-skip-busy`, `--catchup`) |
|
|
388
|
+
| `/remote-pi cron list` | List scheduled jobs (schedule, enabled, next run, last status) |
|
|
389
|
+
| `/remote-pi cron run <jobId>` | Fire a job now (ignores its schedule) |
|
|
390
|
+
| `/remote-pi cron enable\|disable <jobId>` | Toggle a job on/off |
|
|
391
|
+
| `/remote-pi cron remove <jobId>` | Delete a job |
|
|
392
|
+
| `/remote-pi cron log [<jobId>] [--tail N]` | Read the fire/skip audit log |
|
|
387
393
|
| `/remote-pi install` | Install `pi-supervisord` as a system service |
|
|
388
394
|
| `/remote-pi uninstall` | Remove the system service (registry preserved) |
|
|
389
395
|
|
|
@@ -391,6 +397,33 @@ All commands above work both as Pi slash commands (interactive) and as
|
|
|
391
397
|
shell-level `remote-pi <subcommand>` when the package is installed
|
|
392
398
|
globally (`npm install -g remote-pi`).
|
|
393
399
|
|
|
400
|
+
### Scheduled prompts (`cron`)
|
|
401
|
+
|
|
402
|
+
`remote-pi cron` schedules **recurring prompts** to daemons through the
|
|
403
|
+
supervisor — e.g. a daily "summarise new PRs". Output flows fire-and-forget to
|
|
404
|
+
the mesh/app like any prompt; the cron layer only audits the dispatch.
|
|
405
|
+
|
|
406
|
+
- **Schedule** is a cron expression (croner syntax; an optional 6th *seconds*
|
|
407
|
+
field is supported), with an optional IANA timezone via `--tz`:
|
|
408
|
+
|
|
409
|
+
```sh
|
|
410
|
+
remote-pi cron add a1b2c3d4 "0 9 * * *" "Summarise new PRs" --tz America/Sao_Paulo
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
- **Minimum interval is 60s** — more frequent schedules are rejected (guards
|
|
414
|
+
token cost + pileup). A fire is **skipped when the daemon is mid-turn**
|
|
415
|
+
(`--no-skip-busy` to override); `--wake` starts a stopped daemon first;
|
|
416
|
+
`--catchup` runs once on supervisor start if the previous run was missed.
|
|
417
|
+
- **Prerequisite**: the supervisor must run as a service (`remote-pi install`).
|
|
418
|
+
Without it there is no scheduler, and `cron` commands say so instead of
|
|
419
|
+
silently pretending to schedule.
|
|
420
|
+
- **Audit**: every fire **and** every skip appends one line to
|
|
421
|
+
`~/.pi/remote/cron.jsonl` with a `result` of `delivered`,
|
|
422
|
+
`woke_and_delivered`, `deliver_failed`, `skipped_busy`, `skipped_down`, or
|
|
423
|
+
`skipped_disabled` — read it with `remote-pi cron log`.
|
|
424
|
+
|
|
425
|
+
Step-by-step walkthrough: the [daemon tutorial](https://remote-pi.jacobmoura.work/tutorials/daemon).
|
|
426
|
+
|
|
394
427
|
### Footer + title
|
|
395
428
|
|
|
396
429
|
- `📡 local (N)` — current agent session and peer count (local mesh)
|
package/dist/daemon/client.js
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { createConnection } from "node:net";
|
|
3
3
|
import { encodeRequest, parseReply, } from "./control_protocol.js";
|
|
4
4
|
import { getSupervisorSockPath } from "./supervisor.js";
|
|
5
|
+
import { usesNamedPipe } from "../session/ipc.js";
|
|
5
6
|
/**
|
|
6
7
|
* Tiny client for the `remote-pi` CLI to call the supervisor over the
|
|
7
8
|
* `~/.pi/remote/supervisor.sock` UDS.
|
|
@@ -35,7 +36,9 @@ export class SupervisorOfflineError extends Error {
|
|
|
35
36
|
*/
|
|
36
37
|
export async function callSupervisor(req) {
|
|
37
38
|
const sockPath = getSupervisorSockPath();
|
|
38
|
-
|
|
39
|
+
// POSIX fast-fail on a missing socket file. Windows pipes have no file —
|
|
40
|
+
// skip the check and let `_connect` fail fast if the pipe isn't there.
|
|
41
|
+
if (!usesNamedPipe() && !existsSync(sockPath))
|
|
39
42
|
throw new SupervisorOfflineError(sockPath);
|
|
40
43
|
const sock = await _connect(sockPath);
|
|
41
44
|
try {
|
|
@@ -55,7 +58,7 @@ export async function callSupervisor(req) {
|
|
|
55
58
|
* registry-only listing. */
|
|
56
59
|
export async function supervisorOnline() {
|
|
57
60
|
const sockPath = getSupervisorSockPath();
|
|
58
|
-
if (!existsSync(sockPath))
|
|
61
|
+
if (!usesNamedPipe() && !existsSync(sockPath))
|
|
59
62
|
return false;
|
|
60
63
|
try {
|
|
61
64
|
const sock = await _connect(sockPath);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/daemon/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AACrC,OAAO,EAAE,gBAAgB,EAAe,MAAM,UAAU,CAAC;AACzD,OAAO,EAIL,aAAa,EACb,UAAU,GACX,MAAM,uBAAuB,CAAC;AAC/B,OAAO,EAAE,qBAAqB,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"client.js","sourceRoot":"","sources":["../../src/daemon/client.ts"],"names":[],"mappings":"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"}
|
|
@@ -11,6 +11,8 @@
|
|
|
11
11
|
* `node_modules/@earendil-works/pi-coding-agent/dist/modes/rpc/rpc-types.d.ts`.
|
|
12
12
|
* This file is strictly the supervisor's own control plane.
|
|
13
13
|
*/
|
|
14
|
+
import type { CronJob } from "./cron_registry.js";
|
|
15
|
+
import type { CronLogEntry } from "./cron_log.js";
|
|
14
16
|
/** Per-daemon runtime state observable through the supervisor. */
|
|
15
17
|
export type DaemonState = "running" | "stopped" | "starting" | "crashed";
|
|
16
18
|
export interface DaemonInfo {
|
|
@@ -34,8 +36,14 @@ export type ControlRequest = {
|
|
|
34
36
|
id: string;
|
|
35
37
|
} | {
|
|
36
38
|
op: "stop_all";
|
|
39
|
+
} | {
|
|
40
|
+
op: "stop";
|
|
41
|
+
id: string;
|
|
37
42
|
} | {
|
|
38
43
|
op: "restart_all";
|
|
44
|
+
} | {
|
|
45
|
+
op: "restart";
|
|
46
|
+
id: string;
|
|
39
47
|
} | {
|
|
40
48
|
op: "send";
|
|
41
49
|
id: string;
|
|
@@ -46,6 +54,31 @@ export type ControlRequest = {
|
|
|
46
54
|
} | {
|
|
47
55
|
op: "unregister";
|
|
48
56
|
id: string;
|
|
57
|
+
} | {
|
|
58
|
+
op: "cron_add";
|
|
59
|
+
daemon_id: string;
|
|
60
|
+
schedule: string;
|
|
61
|
+
prompt: string;
|
|
62
|
+
tz?: string;
|
|
63
|
+
skip_if_busy?: boolean;
|
|
64
|
+
wake?: boolean;
|
|
65
|
+
catchup?: boolean;
|
|
66
|
+
} | {
|
|
67
|
+
op: "cron_list";
|
|
68
|
+
} | {
|
|
69
|
+
op: "cron_remove";
|
|
70
|
+
job_id: string;
|
|
71
|
+
} | {
|
|
72
|
+
op: "cron_enable";
|
|
73
|
+
job_id: string;
|
|
74
|
+
enabled: boolean;
|
|
75
|
+
} | {
|
|
76
|
+
op: "cron_run";
|
|
77
|
+
job_id: string;
|
|
78
|
+
} | {
|
|
79
|
+
op: "cron_log";
|
|
80
|
+
job_id?: string;
|
|
81
|
+
tail?: number;
|
|
49
82
|
};
|
|
50
83
|
/** Replies sent supervisor → CLI. Tagged by `ok` boolean. */
|
|
51
84
|
export type ControlReply<T = unknown> = {
|
|
@@ -79,9 +112,19 @@ export interface ControlReplyShapes {
|
|
|
79
112
|
stopped: string[];
|
|
80
113
|
already_stopped: string[];
|
|
81
114
|
};
|
|
115
|
+
stop: {
|
|
116
|
+
id: string;
|
|
117
|
+
state: DaemonState;
|
|
118
|
+
stopped: boolean;
|
|
119
|
+
};
|
|
82
120
|
restart_all: {
|
|
83
121
|
restarted: string[];
|
|
84
122
|
};
|
|
123
|
+
restart: {
|
|
124
|
+
id: string;
|
|
125
|
+
state: DaemonState;
|
|
126
|
+
restarted: boolean;
|
|
127
|
+
};
|
|
85
128
|
send: {
|
|
86
129
|
id: string;
|
|
87
130
|
delivered: boolean;
|
|
@@ -94,7 +137,32 @@ export interface ControlReplyShapes {
|
|
|
94
137
|
removed: boolean;
|
|
95
138
|
cwd?: string;
|
|
96
139
|
};
|
|
140
|
+
cron_add: {
|
|
141
|
+
job: CronJobView;
|
|
142
|
+
};
|
|
143
|
+
cron_list: {
|
|
144
|
+
jobs: CronJobView[];
|
|
145
|
+
};
|
|
146
|
+
cron_remove: {
|
|
147
|
+
removed: boolean;
|
|
148
|
+
};
|
|
149
|
+
cron_enable: {
|
|
150
|
+
job_id: string;
|
|
151
|
+
enabled: boolean;
|
|
152
|
+
updated: boolean;
|
|
153
|
+
};
|
|
154
|
+
cron_run: {
|
|
155
|
+
job_id: string;
|
|
156
|
+
result: string;
|
|
157
|
+
};
|
|
158
|
+
cron_log: {
|
|
159
|
+
entries: CronLogEntry[];
|
|
160
|
+
};
|
|
97
161
|
}
|
|
162
|
+
/** A cron job plus its computed `next_run` (ISO), for `cron list`. */
|
|
163
|
+
export type CronJobView = CronJob & {
|
|
164
|
+
next_run?: string | null;
|
|
165
|
+
};
|
|
98
166
|
/** Convenience for typed `Client.request<...>("op")` calls. */
|
|
99
167
|
export type ControlReplyFor<Op extends ControlRequest["op"]> = Op extends keyof ControlReplyShapes ? ControlReplyShapes[Op] : never;
|
|
100
168
|
export declare function encodeRequest(req: ControlRequest): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control_protocol.js","sourceRoot":"","sources":["../../src/daemon/control_protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;
|
|
1
|
+
{"version":3,"file":"control_protocol.js","sourceRoot":"","sources":["../../src/daemon/control_protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA4EH,gFAAgF;AAEhF,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAE9B,MAAM,UAAU,aAAa,CAAC,GAAmB;IAC/C,OAAO,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,gBAAgB,CAAC;AAChD,CAAC;AAED,MAAM,UAAU,WAAW,CAAI,KAAsB;IACnD,OAAO,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,GAAG,gBAAgB,CAAC;AAClD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC;QAAC,MAAM,IAAI,KAAK,CAAC,8BAA+B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IAAC,CAAC;IACpF,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;IACD,MAAM,EAAE,GAAI,GAAwB,CAAC,EAAE,CAAC;IACxC,IAAI,OAAO,EAAE,KAAK,QAAQ,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,2CAA2C,CAAC,CAAC;IAC/D,CAAC;IACD,uEAAuE;IACvE,8DAA8D;IAC9D,OAAO,GAAqB,CAAC;AAC/B,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAY;IACrC,IAAI,GAAY,CAAC;IACjB,IAAI,CAAC;QAAC,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,CAAC;QAAC,MAAM,IAAI,KAAK,CAAC,4BAA6B,CAAW,CAAC,OAAO,EAAE,CAAC,CAAC;IAAC,CAAC;IAClF,IAAI,CAAC,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,qCAAqC,CAAC,CAAC;IACzD,CAAC;IACD,MAAM,EAAE,GAAI,GAAwB,CAAC,EAAE,CAAC;IACxC,IAAI,OAAO,EAAE,KAAK,SAAS,EAAE,CAAC;QAC5B,MAAM,IAAI,KAAK,CAAC,0CAA0C,CAAC,CAAC;IAC9D,CAAC;IACD,OAAO,GAA4B,CAAC;AACtC,CAAC"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Append-only audit trail for cron fires at `~/.pi/remote/cron.jsonl`.
|
|
3
|
+
*
|
|
4
|
+
* One JSON line per scheduler decision — **every fire AND every skip** — so an
|
|
5
|
+
* operator can see exactly what ran and what didn't (the agent's output goes
|
|
6
|
+
* fire-and-forget to the relay/mesh, so the dispatch itself needs its own
|
|
7
|
+
* trail). Plan/39 decision E.
|
|
8
|
+
*/
|
|
9
|
+
/** Outcome of a single `fireJob` decision. */
|
|
10
|
+
export type CronResult = "delivered" | "deliver_failed" | "woke_and_delivered" | "skipped_busy" | "skipped_down" | "skipped_disabled";
|
|
11
|
+
export interface CronLogEntry {
|
|
12
|
+
/** epoch ms */
|
|
13
|
+
ts: number;
|
|
14
|
+
job_id: string;
|
|
15
|
+
daemon_id: string;
|
|
16
|
+
schedule: string;
|
|
17
|
+
/** true when a prompt was actually sent (delivered / woke_and_delivered). */
|
|
18
|
+
fired: boolean;
|
|
19
|
+
result: CronResult;
|
|
20
|
+
/** First chars of the prompt, for at-a-glance log reading. */
|
|
21
|
+
prompt_preview: string;
|
|
22
|
+
}
|
|
23
|
+
/** Test/diag-only: the on-disk path. */
|
|
24
|
+
export declare function cronLogPath(): string;
|
|
25
|
+
/** Maps a result to whether a prompt was actually delivered. */
|
|
26
|
+
export declare function firedFor(result: CronResult): boolean;
|
|
27
|
+
/**
|
|
28
|
+
* Appends one entry. Best-effort: creates the parent dir + file when absent;
|
|
29
|
+
* never throws into the scheduler (a logging failure must not abort a fire).
|
|
30
|
+
*/
|
|
31
|
+
export declare function appendCronLog(entry: {
|
|
32
|
+
job_id: string;
|
|
33
|
+
daemon_id: string;
|
|
34
|
+
schedule: string;
|
|
35
|
+
result: CronResult;
|
|
36
|
+
prompt: string;
|
|
37
|
+
}): void;
|
|
38
|
+
/**
|
|
39
|
+
* Reads the log, newest-last. Optional `jobId` filter and `tail` (last N).
|
|
40
|
+
* Missing file → []. Malformed lines are skipped.
|
|
41
|
+
*/
|
|
42
|
+
export declare function readCronLog(opts?: {
|
|
43
|
+
jobId?: string;
|
|
44
|
+
tail?: number;
|
|
45
|
+
}): CronLogEntry[];
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
|
+
const PREVIEW_LEN = 80;
|
|
5
|
+
function logPath() {
|
|
6
|
+
const root = process.env["REMOTE_PI_HOME"] || homedir();
|
|
7
|
+
return join(root, ".pi", "remote", "cron.jsonl");
|
|
8
|
+
}
|
|
9
|
+
/** Test/diag-only: the on-disk path. */
|
|
10
|
+
export function cronLogPath() {
|
|
11
|
+
return logPath();
|
|
12
|
+
}
|
|
13
|
+
/** Maps a result to whether a prompt was actually delivered. */
|
|
14
|
+
export function firedFor(result) {
|
|
15
|
+
return result === "delivered" || result === "woke_and_delivered";
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Appends one entry. Best-effort: creates the parent dir + file when absent;
|
|
19
|
+
* never throws into the scheduler (a logging failure must not abort a fire).
|
|
20
|
+
*/
|
|
21
|
+
export function appendCronLog(entry) {
|
|
22
|
+
const line = JSON.stringify({
|
|
23
|
+
ts: Date.now(),
|
|
24
|
+
job_id: entry.job_id,
|
|
25
|
+
daemon_id: entry.daemon_id,
|
|
26
|
+
schedule: entry.schedule,
|
|
27
|
+
fired: firedFor(entry.result),
|
|
28
|
+
result: entry.result,
|
|
29
|
+
prompt_preview: entry.prompt.slice(0, PREVIEW_LEN),
|
|
30
|
+
}) + "\n";
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(dirname(logPath()), { recursive: true });
|
|
33
|
+
appendFileSync(logPath(), line, "utf8");
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
/* audit is best-effort — don't break the scheduler on a write error */
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Reads the log, newest-last. Optional `jobId` filter and `tail` (last N).
|
|
41
|
+
* Missing file → []. Malformed lines are skipped.
|
|
42
|
+
*/
|
|
43
|
+
export function readCronLog(opts = {}) {
|
|
44
|
+
if (!existsSync(logPath()))
|
|
45
|
+
return [];
|
|
46
|
+
let raw;
|
|
47
|
+
try {
|
|
48
|
+
raw = readFileSync(logPath(), "utf8");
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
const entries = [];
|
|
54
|
+
for (const line of raw.split("\n")) {
|
|
55
|
+
if (!line.trim())
|
|
56
|
+
continue;
|
|
57
|
+
try {
|
|
58
|
+
const e = JSON.parse(line);
|
|
59
|
+
if (opts.jobId && e.job_id !== opts.jobId)
|
|
60
|
+
continue;
|
|
61
|
+
entries.push(e);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
/* skip malformed */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (opts.tail !== undefined && opts.tail >= 0 && entries.length > opts.tail) {
|
|
68
|
+
return entries.slice(entries.length - opts.tail);
|
|
69
|
+
}
|
|
70
|
+
return entries;
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=cron_log.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron_log.js","sourceRoot":"","sources":["../../src/daemon/cron_log.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cron registry: scheduled prompts for daemons, persisted at
|
|
3
|
+
* `~/.pi/remote/cron.json`. Mirrors `registry.ts` (`daemons.json`) — tolerant
|
|
4
|
+
* load (missing/corrupt → empty), atomic-ish full-file save.
|
|
5
|
+
*
|
|
6
|
+
* Each job targets a daemon by its `daemon_id` (the 8-hex id from
|
|
7
|
+
* `daemonIdForCwd`) and fires `prompt` on `schedule` (a cron expression, run
|
|
8
|
+
* by croner in the supervisor). The scheduler engine + `fireJob` live in
|
|
9
|
+
* `supervisor.ts`; this module only owns persistence + schedule validation.
|
|
10
|
+
*
|
|
11
|
+
* Plan/39. Decisions B (croner), C (min-interval 60s), E (audit via cron_log).
|
|
12
|
+
*/
|
|
13
|
+
/** Minimum allowed interval between two consecutive runs of a schedule. */
|
|
14
|
+
export declare const MIN_INTERVAL_MS = 60000;
|
|
15
|
+
/** A scheduled prompt. */
|
|
16
|
+
export interface CronJob {
|
|
17
|
+
/** `j_<hex>` — random, stable across restarts. */
|
|
18
|
+
id: string;
|
|
19
|
+
/** Target daemon id (`daemonIdForCwd`). */
|
|
20
|
+
daemon_id: string;
|
|
21
|
+
/** Cron expression (croner syntax; optional 6th seconds field supported). */
|
|
22
|
+
schedule: string;
|
|
23
|
+
/** IANA timezone for the schedule (e.g. "America/Sao_Paulo"). */
|
|
24
|
+
tz?: string;
|
|
25
|
+
/** Prompt text injected into the daemon when the job fires. */
|
|
26
|
+
prompt: string;
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
/** Skip the fire when the daemon is mid-turn (default true). */
|
|
29
|
+
skip_if_busy: boolean;
|
|
30
|
+
/** Start the daemon if it's down, then fire (default false). */
|
|
31
|
+
wake: boolean;
|
|
32
|
+
/** On supervisor start, run once if the previous scheduled run was missed
|
|
33
|
+
* while it was down (default false; at most 1×). */
|
|
34
|
+
catchup: boolean;
|
|
35
|
+
created_at: string;
|
|
36
|
+
last_run?: string;
|
|
37
|
+
/** Last fire result — see cron_log `result` values. Shortcut for `cron list`. */
|
|
38
|
+
last_status?: string;
|
|
39
|
+
}
|
|
40
|
+
export interface CronRegistry {
|
|
41
|
+
jobs: CronJob[];
|
|
42
|
+
}
|
|
43
|
+
/** Test/diag-only: the on-disk path. */
|
|
44
|
+
export declare function cronRegistryPath(): string;
|
|
45
|
+
/** Reads the registry; returns empty on missing/corrupt file. */
|
|
46
|
+
export declare function loadCronRegistry(): CronRegistry;
|
|
47
|
+
export declare function saveCronRegistry(reg: CronRegistry): void;
|
|
48
|
+
export declare function listJobs(): CronJob[];
|
|
49
|
+
export declare function getJob(id: string): CronJob | undefined;
|
|
50
|
+
/** Fields a caller supplies when creating a job. Flags default sensibly. */
|
|
51
|
+
export interface NewJobInput {
|
|
52
|
+
daemon_id: string;
|
|
53
|
+
schedule: string;
|
|
54
|
+
prompt: string;
|
|
55
|
+
tz?: string;
|
|
56
|
+
skip_if_busy?: boolean;
|
|
57
|
+
wake?: boolean;
|
|
58
|
+
catchup?: boolean;
|
|
59
|
+
}
|
|
60
|
+
/** Adds a job with a fresh `j_<hex>` id. Pure persistence — call
|
|
61
|
+
* `validateSchedule` first at the op/CLI boundary. */
|
|
62
|
+
export declare function addJob(input: NewJobInput): CronJob;
|
|
63
|
+
export declare function removeJob(id: string): boolean;
|
|
64
|
+
export declare function setJobEnabled(id: string, enabled: boolean): boolean;
|
|
65
|
+
/** Records the outcome of a fire on the job (the `cron list` shortcut). */
|
|
66
|
+
export declare function recordRun(id: string, at: string, status: string): void;
|
|
67
|
+
export interface ScheduleValidation {
|
|
68
|
+
ok: boolean;
|
|
69
|
+
error?: string;
|
|
70
|
+
/** Interval (ms) between the next two runs, when computable. */
|
|
71
|
+
intervalMs?: number;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Validates a cron expression via croner and enforces the ≥60s min-interval
|
|
75
|
+
* (decision C — guards against pileup + token burn). Returns `ok:false` with a
|
|
76
|
+
* user-facing message on a bad expression or a too-frequent schedule.
|
|
77
|
+
*/
|
|
78
|
+
export declare function validateSchedule(schedule: string, tz?: string): ScheduleValidation;
|
|
79
|
+
/** Returns the next scheduled run for a job, or null. Used by `cron list`. */
|
|
80
|
+
export declare function nextRunFor(job: CronJob): Date | null;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { Cron } from "croner";
|
|
6
|
+
/**
|
|
7
|
+
* Cron registry: scheduled prompts for daemons, persisted at
|
|
8
|
+
* `~/.pi/remote/cron.json`. Mirrors `registry.ts` (`daemons.json`) — tolerant
|
|
9
|
+
* load (missing/corrupt → empty), atomic-ish full-file save.
|
|
10
|
+
*
|
|
11
|
+
* Each job targets a daemon by its `daemon_id` (the 8-hex id from
|
|
12
|
+
* `daemonIdForCwd`) and fires `prompt` on `schedule` (a cron expression, run
|
|
13
|
+
* by croner in the supervisor). The scheduler engine + `fireJob` live in
|
|
14
|
+
* `supervisor.ts`; this module only owns persistence + schedule validation.
|
|
15
|
+
*
|
|
16
|
+
* Plan/39. Decisions B (croner), C (min-interval 60s), E (audit via cron_log).
|
|
17
|
+
*/
|
|
18
|
+
/** Minimum allowed interval between two consecutive runs of a schedule. */
|
|
19
|
+
export const MIN_INTERVAL_MS = 60_000;
|
|
20
|
+
function cronPath() {
|
|
21
|
+
const root = process.env["REMOTE_PI_HOME"] || homedir();
|
|
22
|
+
return join(root, ".pi", "remote", "cron.json");
|
|
23
|
+
}
|
|
24
|
+
/** Test/diag-only: the on-disk path. */
|
|
25
|
+
export function cronRegistryPath() {
|
|
26
|
+
return cronPath();
|
|
27
|
+
}
|
|
28
|
+
/** Reads the registry; returns empty on missing/corrupt file. */
|
|
29
|
+
export function loadCronRegistry() {
|
|
30
|
+
if (!existsSync(cronPath()))
|
|
31
|
+
return { jobs: [] };
|
|
32
|
+
try {
|
|
33
|
+
const parsed = JSON.parse(readFileSync(cronPath(), "utf8"));
|
|
34
|
+
if (!parsed || typeof parsed !== "object")
|
|
35
|
+
return { jobs: [] };
|
|
36
|
+
const arr = parsed.jobs;
|
|
37
|
+
if (!Array.isArray(arr))
|
|
38
|
+
return { jobs: [] };
|
|
39
|
+
const jobs = [];
|
|
40
|
+
for (const item of arr) {
|
|
41
|
+
const job = _coerceJob(item);
|
|
42
|
+
if (job)
|
|
43
|
+
jobs.push(job);
|
|
44
|
+
}
|
|
45
|
+
return { jobs };
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return { jobs: [] };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function saveCronRegistry(reg) {
|
|
52
|
+
mkdirSync(dirname(cronPath()), { recursive: true });
|
|
53
|
+
writeFileSync(cronPath(), JSON.stringify(reg, null, 2) + "\n");
|
|
54
|
+
}
|
|
55
|
+
export function listJobs() {
|
|
56
|
+
return loadCronRegistry().jobs;
|
|
57
|
+
}
|
|
58
|
+
export function getJob(id) {
|
|
59
|
+
return loadCronRegistry().jobs.find((j) => j.id === id);
|
|
60
|
+
}
|
|
61
|
+
/** Adds a job with a fresh `j_<hex>` id. Pure persistence — call
|
|
62
|
+
* `validateSchedule` first at the op/CLI boundary. */
|
|
63
|
+
export function addJob(input) {
|
|
64
|
+
const reg = loadCronRegistry();
|
|
65
|
+
const job = {
|
|
66
|
+
id: _freshId(reg.jobs),
|
|
67
|
+
daemon_id: input.daemon_id,
|
|
68
|
+
schedule: input.schedule,
|
|
69
|
+
prompt: input.prompt,
|
|
70
|
+
enabled: true,
|
|
71
|
+
skip_if_busy: input.skip_if_busy ?? true,
|
|
72
|
+
wake: input.wake ?? false,
|
|
73
|
+
catchup: input.catchup ?? false,
|
|
74
|
+
created_at: new Date().toISOString(),
|
|
75
|
+
};
|
|
76
|
+
if (input.tz)
|
|
77
|
+
job.tz = input.tz;
|
|
78
|
+
reg.jobs.push(job);
|
|
79
|
+
saveCronRegistry(reg);
|
|
80
|
+
return job;
|
|
81
|
+
}
|
|
82
|
+
export function removeJob(id) {
|
|
83
|
+
const reg = loadCronRegistry();
|
|
84
|
+
const idx = reg.jobs.findIndex((j) => j.id === id);
|
|
85
|
+
if (idx === -1)
|
|
86
|
+
return false;
|
|
87
|
+
reg.jobs.splice(idx, 1);
|
|
88
|
+
saveCronRegistry(reg);
|
|
89
|
+
return true;
|
|
90
|
+
}
|
|
91
|
+
export function setJobEnabled(id, enabled) {
|
|
92
|
+
const reg = loadCronRegistry();
|
|
93
|
+
const job = reg.jobs.find((j) => j.id === id);
|
|
94
|
+
if (!job)
|
|
95
|
+
return false;
|
|
96
|
+
job.enabled = enabled;
|
|
97
|
+
saveCronRegistry(reg);
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
/** Records the outcome of a fire on the job (the `cron list` shortcut). */
|
|
101
|
+
export function recordRun(id, at, status) {
|
|
102
|
+
const reg = loadCronRegistry();
|
|
103
|
+
const job = reg.jobs.find((j) => j.id === id);
|
|
104
|
+
if (!job)
|
|
105
|
+
return;
|
|
106
|
+
job.last_run = at;
|
|
107
|
+
job.last_status = status;
|
|
108
|
+
saveCronRegistry(reg);
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Validates a cron expression via croner and enforces the ≥60s min-interval
|
|
112
|
+
* (decision C — guards against pileup + token burn). Returns `ok:false` with a
|
|
113
|
+
* user-facing message on a bad expression or a too-frequent schedule.
|
|
114
|
+
*/
|
|
115
|
+
export function validateSchedule(schedule, tz) {
|
|
116
|
+
let cron;
|
|
117
|
+
try {
|
|
118
|
+
cron = new Cron(schedule, tz ? { timezone: tz } : {});
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
return { ok: false, error: `invalid cron expression: ${e.message}` };
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
const n1 = cron.nextRun();
|
|
125
|
+
const n2 = n1 ? cron.nextRun(n1) : null;
|
|
126
|
+
if (!n1 || !n2)
|
|
127
|
+
return { ok: false, error: "schedule has no upcoming runs" };
|
|
128
|
+
const intervalMs = n2.getTime() - n1.getTime();
|
|
129
|
+
if (intervalMs < MIN_INTERVAL_MS) {
|
|
130
|
+
return {
|
|
131
|
+
ok: false,
|
|
132
|
+
intervalMs,
|
|
133
|
+
error: `schedule too frequent: ~${Math.round(intervalMs / 1000)}s between runs (minimum is 60s)`,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
return { ok: true, intervalMs };
|
|
137
|
+
}
|
|
138
|
+
finally {
|
|
139
|
+
cron.stop();
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Returns the next scheduled run for a job, or null. Used by `cron list`. */
|
|
143
|
+
export function nextRunFor(job) {
|
|
144
|
+
try {
|
|
145
|
+
const cron = new Cron(job.schedule, job.tz ? { timezone: job.tz } : {});
|
|
146
|
+
const n = cron.nextRun();
|
|
147
|
+
cron.stop();
|
|
148
|
+
return n;
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
// ── internals ───────────────────────────────────────────────────────────────
|
|
155
|
+
function _freshId(existing) {
|
|
156
|
+
for (let i = 0; i < 1000; i++) {
|
|
157
|
+
const id = `j_${randomBytes(4).toString("hex")}`;
|
|
158
|
+
if (!existing.some((j) => j.id === id))
|
|
159
|
+
return id;
|
|
160
|
+
}
|
|
161
|
+
throw new Error("cron id space exhausted");
|
|
162
|
+
}
|
|
163
|
+
function _coerceJob(item) {
|
|
164
|
+
if (!item || typeof item !== "object")
|
|
165
|
+
return null;
|
|
166
|
+
const o = item;
|
|
167
|
+
if (typeof o["id"] !== "string")
|
|
168
|
+
return null;
|
|
169
|
+
if (typeof o["daemon_id"] !== "string")
|
|
170
|
+
return null;
|
|
171
|
+
if (typeof o["schedule"] !== "string")
|
|
172
|
+
return null;
|
|
173
|
+
if (typeof o["prompt"] !== "string")
|
|
174
|
+
return null;
|
|
175
|
+
const job = {
|
|
176
|
+
id: o["id"],
|
|
177
|
+
daemon_id: o["daemon_id"],
|
|
178
|
+
schedule: o["schedule"],
|
|
179
|
+
prompt: o["prompt"],
|
|
180
|
+
enabled: o["enabled"] !== false,
|
|
181
|
+
skip_if_busy: o["skip_if_busy"] !== false,
|
|
182
|
+
wake: o["wake"] === true,
|
|
183
|
+
catchup: o["catchup"] === true,
|
|
184
|
+
created_at: typeof o["created_at"] === "string" ? o["created_at"] : new Date(0).toISOString(),
|
|
185
|
+
};
|
|
186
|
+
if (typeof o["tz"] === "string")
|
|
187
|
+
job.tz = o["tz"];
|
|
188
|
+
if (typeof o["last_run"] === "string")
|
|
189
|
+
job.last_run = o["last_run"];
|
|
190
|
+
if (typeof o["last_status"] === "string")
|
|
191
|
+
job.last_status = o["last_status"];
|
|
192
|
+
return job;
|
|
193
|
+
}
|
|
194
|
+
//# sourceMappingURL=cron_registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cron_registry.js","sourceRoot":"","sources":["../../src/daemon/cron_registry.ts"],"names":[],"mappings":"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"}
|
package/dist/daemon/id.d.ts
CHANGED
|
@@ -14,5 +14,11 @@
|
|
|
14
14
|
* `/Users/x/Movies` and `/Users/x/link-to-Movies` map to the same daemon.
|
|
15
15
|
* Falls back to the raw path when realpath fails (cwd doesn't exist —
|
|
16
16
|
* shouldn't happen in production but covers test sandboxes).
|
|
17
|
+
*
|
|
18
|
+
* plan/41 audit (2026-06-08): this stays **per-cwd** (no name axis). The
|
|
19
|
+
* multiagent-per-folder model (plan/38/41) applies to the App↔Pi room and the
|
|
20
|
+
* mesh address; a *supervisor daemon* is **one per cwd** by design — the
|
|
21
|
+
* registry keys exactly one entry per cwd (`registry.ts`), so a daemon id never
|
|
22
|
+
* needs the name to disambiguate. Decision: leave `daemonIdForCwd` cwd-only.
|
|
17
23
|
*/
|
|
18
24
|
export declare function daemonIdForCwd(cwd: string): string;
|
package/dist/daemon/id.js
CHANGED
|
@@ -16,6 +16,12 @@ import { realpathSync } from "node:fs";
|
|
|
16
16
|
* `/Users/x/Movies` and `/Users/x/link-to-Movies` map to the same daemon.
|
|
17
17
|
* Falls back to the raw path when realpath fails (cwd doesn't exist —
|
|
18
18
|
* shouldn't happen in production but covers test sandboxes).
|
|
19
|
+
*
|
|
20
|
+
* plan/41 audit (2026-06-08): this stays **per-cwd** (no name axis). The
|
|
21
|
+
* multiagent-per-folder model (plan/38/41) applies to the App↔Pi room and the
|
|
22
|
+
* mesh address; a *supervisor daemon* is **one per cwd** by design — the
|
|
23
|
+
* registry keys exactly one entry per cwd (`registry.ts`), so a daemon id never
|
|
24
|
+
* needs the name to disambiguate. Decision: leave `daemonIdForCwd` cwd-only.
|
|
19
25
|
*/
|
|
20
26
|
export function daemonIdForCwd(cwd) {
|
|
21
27
|
let target;
|
package/dist/daemon/id.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/daemon/id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC
|
|
1
|
+
{"version":3,"file":"id.js","sourceRoot":"","sources":["../../src/daemon/id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,cAAc,CAAC,GAAW;IACxC,IAAI,MAAc,CAAC;IACnB,IAAI,CAAC;QACH,MAAM,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC;IAC7B,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,GAAG,GAAG,CAAC;IACf,CAAC;IACD,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACvE,CAAC"}
|