pi-orchestrator-config 4.7.0 → 4.7.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 +1 -1
- package/extensions/orchestrator/cron-status-format.ts +29 -0
- package/extensions/orchestrator/cron-status-ui.ts +5 -18
- package/extensions/orchestrator/cron-store.ts +5 -0
- package/extensions/orchestrator/cron.ts +22 -6
- package/extensions/pidiff/pidiff-ui/package.json +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -77,7 +77,7 @@ Single extension that provides:
|
|
|
77
77
|
| `/dream` | Run memory consolidation — extract, deduplicate, maintain topic-based memory |
|
|
78
78
|
| `/remember <what>` | Save a memory for future sessions |
|
|
79
79
|
| `/dream-auto` | Toggle automatic memory dreaming (every 3h + session end) |
|
|
80
|
-
| `/cron add\|list\|list-all\|remove [--persist]` | Schedule recurring tasks. Session jobs end with the session; `--persist` keeps a job across Pi sessions in this project. Add/remove mutations still work when leader election fails closed on an unsupported platform. For example: `/cron add every 2h check for new issues --persist` or `/cron remove persist:<uuid>`. `/cron list` opens an overlay for the selected jobs (or both when omitted). Persistent delivery is local at-least-once: a crash or leader recovery can repeat a run. Project cron writes use a stable OS `flock` (included in Docker via `util-linux`) across the entire transaction; native installations need `flock` and a shared filesystem that supports advisory locks. A busy or unavailable lock fails closed after two seconds. **Upgrade gate (enforced):** stop and drain every old Pi process writing crons on the shared mount before starting a version with this kernel lock; old directory-lock writers do not honor `flock` and must not resume. If a legacy `crons.json.mutation.lock` directory exists, new writes fail closed even if its PID is invisible or its heartbeat expired. Only after verifying every old writer cannot resume may an operator remove that stale directory (not `crons.json` or cron tasks). Never remove it merely because a write timed out. |
|
|
80
|
+
| `/cron add\|list\|list-all\|remove [--persist]` | Schedule recurring tasks. Session jobs end with the session; `--persist` keeps a job across Pi sessions in this project. Add/remove mutations still work when leader election fails closed on an unsupported platform. For example: `/cron add every 2h check for new issues --persist` or `/cron remove persist:<uuid>`. Pin one job's model by naming it in the request: `/cron add every 2h check for new issues using openrouter/anthropic/claude-opus-5`. A bare `model-id` (e.g. `claude-opus-5`) keeps the normal provider resolution. An unpinned job uses the worker's normal resolution chain (`agent_overrides[worker]`, worker frontmatter, then `agent_provider`/`agent_model`) — cron does not pass the session model down, so a worker-level setting wins over the session model. An `acpx-*` provider cannot be pinned here: async children cannot register those providers, so the job runs on `internal_operations_provider`/`internal_operations_model` when both are set to a non-`acpx-*` pair, and is skipped with a logged error when they are not. `/cron list` opens an overlay for the selected jobs (or both when omitted). Persistent delivery is local at-least-once: a crash or leader recovery can repeat a run. Project cron writes use a stable OS `flock` (included in Docker via `util-linux`) across the entire transaction; native installations need `flock` and a shared filesystem that supports advisory locks. A busy or unavailable lock fails closed after two seconds. **Upgrade gate (enforced):** stop and drain every old Pi process writing crons on the shared mount before starting a version with this kernel lock; old directory-lock writers do not honor `flock` and must not resume. If a legacy `crons.json.mutation.lock` directory exists, new writes fail closed even if its PID is invisible or its heartbeat expired. Only after verifying every old writer cannot resume may an operator remove that stale directory (not `crons.json` or cron tasks). Never remove it merely because a write timed out. |
|
|
81
81
|
| `/async-kill [name\|id\|all]` | Kill async agents (overlay picker or by name/id) |
|
|
82
82
|
| `/pi-config-settings [project\|global]` | Interactive TUI settings editor — browse all pi-config settings with scope indicators, smart pickers for providers/models, and immediate save |
|
|
83
83
|
| `/status` | Unified session snapshot — async agents, cron tasks, git branch, context usage |
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure cron status display helpers — no TUI deps (unit-testable).
|
|
3
3
|
*/
|
|
4
|
+
import { createLogger } from "../shared/logger.js";
|
|
5
|
+
|
|
6
|
+
const log = createLogger("cron_status");
|
|
4
7
|
|
|
5
8
|
export interface CronStatusTaskView {
|
|
6
9
|
/** Unique scope-qualified overlay row id (e.g. "session:<uuid>"). */
|
|
@@ -12,6 +15,8 @@ export interface CronStatusTaskView {
|
|
|
12
15
|
intervalMs?: number;
|
|
13
16
|
atHour?: number;
|
|
14
17
|
atMinute?: number;
|
|
18
|
+
/** "provider/model-id" or bare "model-id"; absent means the session default. */
|
|
19
|
+
model?: string;
|
|
15
20
|
createdAt: number;
|
|
16
21
|
lastRun?: number;
|
|
17
22
|
nextRun?: number;
|
|
@@ -88,6 +93,28 @@ export function formatNextRunLabel(
|
|
|
88
93
|
return `in ${formatRelativeMs(delta)}`;
|
|
89
94
|
}
|
|
90
95
|
|
|
96
|
+
/** Detail rows rendered by the cron task overlay, uncolored (label, value). */
|
|
97
|
+
export function cronDetailRows(
|
|
98
|
+
task: CronStatusTaskView,
|
|
99
|
+
now = Date.now(),
|
|
100
|
+
): Array<[string, string]> {
|
|
101
|
+
const rows: Array<[string, string]> = [
|
|
102
|
+
["Schedule", formatCronSchedule(task)],
|
|
103
|
+
[
|
|
104
|
+
"Last run",
|
|
105
|
+
`${formatLastRunLabel(task, now)}${
|
|
106
|
+
task.lastRun ? ` · ${new Date(task.lastRun).toLocaleString()}` : ""
|
|
107
|
+
}`,
|
|
108
|
+
],
|
|
109
|
+
["Next run", formatNextRunLabel(task, now)],
|
|
110
|
+
["Created", new Date(task.createdAt).toLocaleString()],
|
|
111
|
+
];
|
|
112
|
+
if (task.sessionLabel) rows.push(["Session", task.sessionLabel]);
|
|
113
|
+
if (task.model) rows.push(["Model", task.model]);
|
|
114
|
+
log.debug("cron_detail_rows", { id: task.id, rows: rows.length, model: task.model || "inherit" });
|
|
115
|
+
return rows;
|
|
116
|
+
}
|
|
117
|
+
|
|
91
118
|
/** Map a CronTask-like object into an overlay row view. */
|
|
92
119
|
export function toCronStatusTaskView(
|
|
93
120
|
task: {
|
|
@@ -97,6 +124,7 @@ export function toCronStatusTaskView(
|
|
|
97
124
|
intervalMs?: number;
|
|
98
125
|
atHour?: number;
|
|
99
126
|
atMinute?: number;
|
|
127
|
+
model?: string;
|
|
100
128
|
createdAt: number;
|
|
101
129
|
lastRun?: number;
|
|
102
130
|
nextRun?: number;
|
|
@@ -116,6 +144,7 @@ export function toCronStatusTaskView(
|
|
|
116
144
|
intervalMs: task.intervalMs,
|
|
117
145
|
atHour: task.atHour,
|
|
118
146
|
atMinute: task.atMinute,
|
|
147
|
+
model: task.model,
|
|
119
148
|
createdAt: task.createdAt,
|
|
120
149
|
lastRun: task.lastRun,
|
|
121
150
|
nextRun: task.nextRun,
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import {
|
|
7
|
+
cronDetailRows,
|
|
7
8
|
formatCronSchedule,
|
|
8
9
|
formatLastRunLabel,
|
|
9
10
|
formatNextRunLabel,
|
|
@@ -16,6 +17,7 @@ import {
|
|
|
16
17
|
|
|
17
18
|
export type { CronStatusTaskView } from "./cron-status-format.js";
|
|
18
19
|
export {
|
|
20
|
+
cronDetailRows,
|
|
19
21
|
formatCronSchedule,
|
|
20
22
|
formatLastRunLabel,
|
|
21
23
|
formatNextRunLabel,
|
|
@@ -103,24 +105,9 @@ export async function openCronStatusOverlay(
|
|
|
103
105
|
if (!current) {
|
|
104
106
|
return [t.fg("error", "Task removed."), t.fg("dim", "Esc back")];
|
|
105
107
|
}
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
`${t.fg("muted", "Last run")} ${formatLastRunLabel(current, now)}` +
|
|
110
|
-
(current.lastRun
|
|
111
|
-
? t.fg(
|
|
112
|
-
"dim",
|
|
113
|
-
` · ${new Date(current.lastRun).toLocaleString()}`,
|
|
114
|
-
)
|
|
115
|
-
: ""),
|
|
116
|
-
`${t.fg("muted", "Next run")} ${formatNextRunLabel(current, now)}`,
|
|
117
|
-
`${t.fg("muted", "Created")} ${new Date(current.createdAt).toLocaleString()}`,
|
|
118
|
-
];
|
|
119
|
-
if (current.sessionLabel) {
|
|
120
|
-
lines.push(
|
|
121
|
-
`${t.fg("muted", "Session")} ${current.sessionLabel}`,
|
|
122
|
-
);
|
|
123
|
-
}
|
|
108
|
+
const lines = cronDetailRows(current).map(
|
|
109
|
+
([label, value]) => `${t.fg("muted", label.padEnd(9))} ${value}`,
|
|
110
|
+
);
|
|
124
111
|
lines.push("", t.fg("muted", "Task"), current.task);
|
|
125
112
|
return lines;
|
|
126
113
|
},
|
|
@@ -4,6 +4,7 @@ import * as path from "node:path";
|
|
|
4
4
|
import { execFileSync, spawnSync } from "node:child_process";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { parseProcStartTime } from "./utils.js";
|
|
7
|
+
import { parseModelOverride } from "./parse-model-override.js";
|
|
7
8
|
import { createLogger } from "../shared/logger.js";
|
|
8
9
|
|
|
9
10
|
export type CronScope = "session" | "project";
|
|
@@ -16,6 +17,8 @@ export interface DurableCronTask {
|
|
|
16
17
|
intervalMs?: number;
|
|
17
18
|
atHour?: number;
|
|
18
19
|
atMinute?: number;
|
|
20
|
+
/** "provider/model-id" or bare "model-id"; absent means inherit. */
|
|
21
|
+
model?: string;
|
|
19
22
|
createdAt: number;
|
|
20
23
|
lastRun?: number;
|
|
21
24
|
nextRun?: number;
|
|
@@ -182,6 +185,8 @@ export function validateDurableCronTask(task: unknown): asserts task is DurableC
|
|
|
182
185
|
if (value.atHour !== undefined && (!Number.isInteger(value.atHour) || value.atHour < 0 || value.atHour > 23)) throw new Error("Invalid durable cron hour");
|
|
183
186
|
if (value.atMinute !== undefined && (!Number.isInteger(value.atMinute) || value.atMinute < 0 || value.atMinute > 59 || value.atHour === undefined)) throw new Error("Invalid durable cron minute");
|
|
184
187
|
if (value.intervalMs === undefined && value.atHour === undefined) throw new Error("Invalid durable cron schedule");
|
|
188
|
+
// Disk data is untrusted: guard the type before parseModelOverride, which assumes a string.
|
|
189
|
+
if (value.model !== undefined && (typeof value.model !== "string" || !parseModelOverride(value.model))) throw new Error("Invalid durable cron model");
|
|
185
190
|
if ((value.lastRun !== undefined && !Number.isFinite(value.lastRun)) || (value.nextRun !== undefined && !Number.isFinite(value.nextRun))) throw new Error("Invalid durable cron timestamp");
|
|
186
191
|
}
|
|
187
192
|
function validate(tasks: DurableCronTask[]) { for (const task of tasks) validateDurableCronTask(task); }
|
|
@@ -8,12 +8,13 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
8
8
|
import { createLogger } from "../shared/logger.js";
|
|
9
9
|
import { decideAsyncLlmDispatch } from "./async-capability.js";
|
|
10
10
|
import { formatCronSchedule, toCronStatusTaskView } from "./cron-status-format.js";
|
|
11
|
+
import { parseModelOverride } from "./parse-model-override.js";
|
|
11
12
|
import { openCronStatusOverlay } from "./cron-status-ui.js";
|
|
12
13
|
import { setSlot } from "./status-bar.js";
|
|
13
14
|
import { acquireLeaderLock, durableCronSupported, isLeaderLockCurrent, mutateDurableCronStore, readDurableCronStore, refreshLeaderLock, releaseLeaderLock, validateDurableCronTask, type CronLockOwner, type CronScope, type DurableCronTask } from "./cron-store.js";
|
|
14
15
|
|
|
15
16
|
const log = createLogger("cron");
|
|
16
|
-
export interface CronTask { id: string; scope: CronScope; cwd: string; description: string; task: string; intervalMs?: number; atHour?: number; atMinute?: number; createdAt: number; lastRun?: number; nextRun?: number; leader?: boolean; }
|
|
17
|
+
export interface CronTask { id: string; scope: CronScope; cwd: string; description: string; task: string; intervalMs?: number; atHour?: number; atMinute?: number; /** "provider/model-id" or bare "model-id"; undefined inherits the normal resolution chain. */ model?: string; createdAt: number; lastRun?: number; nextRun?: number; leader?: boolean; }
|
|
17
18
|
type Timer = ReturnType<typeof setTimeout> | ReturnType<typeof setInterval>;
|
|
18
19
|
const durable = (scope: CronScope) => scope === "project";
|
|
19
20
|
let sessionStore = "";
|
|
@@ -30,6 +31,7 @@ function validateSessionCronTask(task: unknown): asserts task is CronTask {
|
|
|
30
31
|
const value = task as CronTask;
|
|
31
32
|
if (!value || typeof value !== "object" || typeof value.id !== "string" || !value.id || typeof value.description !== "string" || typeof value.task !== "string" || !value.task.trim() || !Number.isFinite(value.createdAt)) throw new Error("Invalid session cron task");
|
|
32
33
|
validateSchedule({ interval_seconds: value.intervalMs === undefined ? undefined : value.intervalMs / 1000, at_hour: value.atHour, at_minute: value.atMinute });
|
|
34
|
+
if (value.model !== undefined && (typeof value.model !== "string" || !parseModelOverride(value.model))) throw new Error("Invalid session cron model");
|
|
33
35
|
if ((value.lastRun !== undefined && !Number.isFinite(value.lastRun)) || (value.nextRun !== undefined && !Number.isFinite(value.nextRun))) throw new Error("Invalid session cron timestamp");
|
|
34
36
|
}
|
|
35
37
|
function writeSessionTasks(file: string, tasks: Iterable<CronTask>) {
|
|
@@ -67,6 +69,14 @@ function validateSchedule(details: any) {
|
|
|
67
69
|
if (minute !== undefined && (!Number.isInteger(minute) || minute < 0 || minute > 59)) throw new Error("at_minute must be an integer from 0 to 59");
|
|
68
70
|
if (minute !== undefined && hour === undefined) throw new Error("at_minute requires at_hour");
|
|
69
71
|
}
|
|
72
|
+
/** Normalize a "provider/model-id" or "model-id" override; undefined means inherit. */
|
|
73
|
+
function normalizeModel(spec: unknown): string | undefined {
|
|
74
|
+
if (typeof spec !== "string" || !spec.trim()) { log.debug("cron_model_absent", { reason: typeof spec === "string" ? "blank" : "missing" }); return undefined; }
|
|
75
|
+
const parsed = parseModelOverride(spec);
|
|
76
|
+
if (!parsed) { log.warn("cron_model_rejected", { spec: spec.slice(0, 120) }); throw new Error('model must be "provider/model-id" or "model-id"'); }
|
|
77
|
+
log.debug("cron_model_normalized", { spec: spec.trim().slice(0, 120), provider: parsed.provider, model: parsed.model });
|
|
78
|
+
return parsed.provider ? `${parsed.provider}/${parsed.model}` : parsed.model!;
|
|
79
|
+
}
|
|
70
80
|
function nextDelay(task: CronTask) { if (task.intervalMs) return task.intervalMs; const target = new Date(); target.setHours(task.atHour!, task.atMinute!, 0, 0); if (+target <= Date.now()) target.setDate(target.getDate() + 1); return +target - Date.now(); }
|
|
71
81
|
|
|
72
82
|
export function registerCron(pi: ExtensionAPI, spawnAsyncAgent: any): { getCronTasks: () => CronTask[] } {
|
|
@@ -115,7 +125,9 @@ export function registerCron(pi: ExtensionAPI, spawnAsyncAgent: any): { getCronT
|
|
|
115
125
|
}
|
|
116
126
|
pi.sendUserMessage(cmd, { deliverAs: "followUp" }); return;
|
|
117
127
|
}
|
|
118
|
-
const
|
|
128
|
+
const explicit = parseModelOverride(task.model);
|
|
129
|
+
const dispatch = decideAsyncLlmDispatch({ parentProvider: explicit?.provider ?? ctx?.model?.provider, cwd: task.cwd, mustAsync: true });
|
|
130
|
+
log.debug("cron_dispatch", { id: qualifyCronId(task), model: task.model || "inherit", action: dispatch.action });
|
|
119
131
|
if (dispatch.action === "skip") { log.error("cron_error", qualifyCronId(task), dispatch.note); return; }
|
|
120
132
|
const { discoverAgents } = await import("./agents.js");
|
|
121
133
|
const { agents } = discoverAgents(task.cwd, "user");
|
|
@@ -123,7 +135,11 @@ export function registerCron(pi: ExtensionAPI, spawnAsyncAgent: any): { getCronT
|
|
|
123
135
|
log.warn("cron_execution_fenced", { id: qualifyCronId(task), reason: "leader_lease_lost_before_dispatch" });
|
|
124
136
|
election(); return;
|
|
125
137
|
}
|
|
126
|
-
|
|
138
|
+
// The sidecar must be authoritative: passed as parent fields it could lose to worker
|
|
139
|
+
// overrides/frontmatter, which async children cannot always register. An unusable
|
|
140
|
+
// acpx-* override is discarded rather than passed at all.
|
|
141
|
+
const override = dispatch.action === "sidecar-async" ? dispatch.sidecar : explicit;
|
|
142
|
+
spawnAsyncAgent("worker", cmd, task.cwd, agents, { name: `Cron: ${task.description.slice(0, 40)}`, ...(override ? { explicit: override } : {}) });
|
|
127
143
|
} catch (error: any) { log.error("cron_execute_failed", qualifyCronId(task), error?.message || error); }
|
|
128
144
|
}
|
|
129
145
|
function stop(id: string) { const timer = timers.get(id); if (timer) { clearTimeout(timer as any); clearInterval(timer as any); timers.delete(id); } }
|
|
@@ -162,7 +178,7 @@ export function registerCron(pi: ExtensionAPI, spawnAsyncAgent: any): { getCronT
|
|
|
162
178
|
if (scope !== "session" && scope !== "project") throw new Error("scope must be session or project");
|
|
163
179
|
if (durable(scope) && !durableCronSupported()) throw new Error("Persistent cron scheduling is unavailable on this platform");
|
|
164
180
|
validateSchedule(details);
|
|
165
|
-
const task: CronTask = { id: randomUUID(), scope, cwd: ctx?.cwd || process.cwd(), description: details.description || details.task.slice(0, 60), task: details.task, intervalMs: details.interval_seconds !== undefined ? details.interval_seconds * 1000 : undefined, atHour: details.at_hour, atMinute: details.at_minute ?? (details.at_hour !== undefined ? 0 : undefined), createdAt: Date.now(), leader: scope === "session" };
|
|
181
|
+
const task: CronTask = { id: randomUUID(), scope, cwd: ctx?.cwd || process.cwd(), description: details.description || details.task.slice(0, 60), task: details.task, intervalMs: details.interval_seconds !== undefined ? details.interval_seconds * 1000 : undefined, atHour: details.at_hour, atMinute: details.at_minute ?? (details.at_hour !== undefined ? 0 : undefined), model: normalizeModel(details.model), createdAt: Date.now(), leader: scope === "session" };
|
|
166
182
|
if (!task.task || (!task.intervalMs && task.atHour === undefined)) throw new Error("task and a schedule are required");
|
|
167
183
|
if (durable(scope)) mutateDurableCronStore(projectStore(task.cwd), old => [...old, task as DurableCronTask]);
|
|
168
184
|
tasks.set(task.id, task);
|
|
@@ -177,9 +193,9 @@ export function registerCron(pi: ExtensionAPI, spawnAsyncAgent: any): { getCronT
|
|
|
177
193
|
const task = tasks.get(id); if (scope === "session") { if (!task) return false; stop(id); tasks.delete(id); if (sessionStore) writeSessionTasks(sessionStore, [...tasks.values()].filter(t => t.scope === "session")); } else { const file = projectStore(ctx.cwd); let found = false; mutateDurableCronStore(file, old => { found = old.some(t => t.id === id); return old.filter(t => t.id !== id); }); if (!found) return false; if (task) { stop(id); tasks.delete(id); } }
|
|
178
194
|
updateStatus(); return true;
|
|
179
195
|
}
|
|
180
|
-
pi.registerTool({ name: "cron_manage", description: "Manage scheduled tasks. Set persist=true to keep a task across Pi sessions in this project.", parameters: Type.Object({ action: Type.Union([Type.Literal("add"), Type.Literal("list"), Type.Literal("remove")]), persist: Type.Optional(Type.Boolean()), description: Type.Optional(Type.String()), task: Type.Optional(Type.String()), interval_seconds: Type.Optional(Type.Number()), at_hour: Type.Optional(Type.Number()), at_minute: Type.Optional(Type.Number()), id: Type.Optional(Type.String()) }), async execute(_id, params: any) {
|
|
196
|
+
pi.registerTool({ name: "cron_manage", description: "Manage scheduled tasks. Set persist=true to keep a task across Pi sessions in this project. Set model to \"provider/model-id\" (or a bare \"model-id\") to pin that cron's model.", parameters: Type.Object({ action: Type.Union([Type.Literal("add"), Type.Literal("list"), Type.Literal("remove")]), persist: Type.Optional(Type.Boolean()), description: Type.Optional(Type.String()), task: Type.Optional(Type.String()), model: Type.Optional(Type.String()), interval_seconds: Type.Optional(Type.Number()), at_hour: Type.Optional(Type.Number()), at_minute: Type.Optional(Type.Number()), id: Type.Optional(Type.String()) }), async execute(_id, params: any) {
|
|
181
197
|
const scope: CronScope = params.persist ? "project" : "session";
|
|
182
|
-
try { if (params.action === "add" && (ctx?.mode === "print" || ctx?.mode === "json")) return { content: [{ type: "text", text: "Error: Cron scheduling is unavailable in one-shot mode." }] }; if (params.action === "add") { const task = add(scope, params); return { content: [{ type: "text", text: `Cron ${qualifyCronId(task)} created: ${formatCronSchedule(task)} → ${task.description}` }] }; } if (params.action === "remove") return { content: [{ type: "text", text: remove(params.id || "") ? "Cron removed." : "Task not found." }] }; const selected = [...tasks.values()].filter(t => !params.persist || t.scope === "project"); return { content: [{ type: "text", text: selected.length ? selected.map(t => `${qualifyCronId(t)} | ${formatCronSchedule(t)} | ${t.description} | ${t.leader === false ? "waiting for leader" : "active"}`).join("\n") : "No scheduled tasks." }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; }
|
|
198
|
+
try { if (params.action === "add" && (ctx?.mode === "print" || ctx?.mode === "json")) return { content: [{ type: "text", text: "Error: Cron scheduling is unavailable in one-shot mode." }] }; if (params.action === "add") { const task = add(scope, params); return { content: [{ type: "text", text: `Cron ${qualifyCronId(task)} created: ${formatCronSchedule(task)} → ${task.description}${task.model ? ` [${task.model}]` : ""}` }] }; } if (params.action === "remove") return { content: [{ type: "text", text: remove(params.id || "") ? "Cron removed." : "Task not found." }] }; const selected = [...tasks.values()].filter(t => !params.persist || t.scope === "project"); return { content: [{ type: "text", text: selected.length ? selected.map(t => `${qualifyCronId(t)} | ${formatCronSchedule(t)} | ${t.description} | ${t.model || "inherited model"} | ${t.leader === false ? "waiting for leader" : "active"}`).join("\n") : "No scheduled tasks." }] }; } catch (error: any) { return { content: [{ type: "text", text: `Error: ${error.message}` }] }; }
|
|
183
199
|
}, } as any);
|
|
184
200
|
pi.registerCommand("cron", { description: "Schedule tasks: add, list, list-all, remove; use --persist to keep a cron", handler: async (args, commandCtx) => { ctx = commandCtx; const parsed = parseCronScope((args || "").trim().split(/\s+/).filter(Boolean)); if (parsed.error) { ctx.ui?.notify(parsed.error, "warning"); return; } const [sub, ...rest] = parsed.rest; if (sub === "list" || sub === "list-all") { const showAll = sub === "list-all"; if (ctx.hasUI) await openCronStatusOverlay(ctx, { title: showAll ? "All cron tasks" : "Cron tasks", listTasks: () => [...tasks.values()].filter(t => showAll || !args.includes("--persist") || t.scope === "project").map(t => toCronStatusTaskView(t, { overlayId: qualifyCronId(t), isLocal: t.scope === "session", sessionLabel: t.leader === false ? `${t.scope} (waiting for leader)` : t.scope })), removeTask: remove }); return; } if (["remove", "rm", "delete"].includes(sub) && rest[0]) { ctx.ui?.notify(remove(rest[0]) ? "Cron removed." : "Task not found.", "info"); return; } pi.sendUserMessage(`Schedule this cron request using cron_manage. Set persist to ${parsed.scope === "project"}. Use persistence only when the user requests it. User request: "${parsed.rest.join(" ")}"`, { deliverAs: "followUp" }); }});
|
|
185
201
|
pi.events.on("pidash:cron-kill", (target: unknown) => {
|