@pi-agents/loop 0.1.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.
@@ -0,0 +1,51 @@
1
+ /**
2
+ * /loop argument parser.
3
+ *
4
+ * Priority:
5
+ * 1. Leading interval token: "5m check the deploy"
6
+ * 2. Trailing "every" clause: "check the deploy every 20m"
7
+ * 3. Default 10m
8
+ */
9
+ const INTERVAL_RE = /^\d+[smhd]$/;
10
+ const TRAILING_EVERY_RE = /\s+every\s+(\d+)\s*([smhd]|seconds?|minutes?|hours?|days?)\s*$/i;
11
+ const UNIT_MAP = {
12
+ s: "s", second: "s", seconds: "s",
13
+ m: "m", minute: "m", minutes: "m",
14
+ h: "h", hour: "h", hours: "h",
15
+ d: "d", day: "d", days: "d",
16
+ };
17
+ export function parseLoopArgs(args) {
18
+ const trimmed = args.trim();
19
+ if (!trimmed)
20
+ return null;
21
+ const tokens = trimmed.split(/\s+/);
22
+ // Rule 1: Leading interval token
23
+ if (tokens.length >= 2 && INTERVAL_RE.test(tokens[0])) {
24
+ return {
25
+ interval: tokens[0],
26
+ prompt: tokens.slice(1).join(" "),
27
+ };
28
+ }
29
+ // Rule 2: Trailing "every" clause
30
+ const everyMatch = trimmed.match(TRAILING_EVERY_RE);
31
+ if (everyMatch) {
32
+ const n = everyMatch[1];
33
+ const unitRaw = everyMatch[2].toLowerCase();
34
+ const unit = UNIT_MAP[unitRaw];
35
+ if (unit) {
36
+ const prompt = trimmed.slice(0, everyMatch.index).trim();
37
+ if (prompt) {
38
+ return {
39
+ interval: `${n}${unit}`,
40
+ prompt,
41
+ };
42
+ }
43
+ }
44
+ }
45
+ // Rule 3: Default 10m
46
+ return {
47
+ interval: "10m",
48
+ prompt: trimmed,
49
+ };
50
+ }
51
+ //# sourceMappingURL=parse-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-args.js","sourceRoot":"","sources":["../src/parse-args.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAOH,MAAM,WAAW,GAAG,aAAa,CAAC;AAClC,MAAM,iBAAiB,GAAG,iEAAiE,CAAC;AAE5F,MAAM,QAAQ,GAA2B;IACvC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG;IACjC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,GAAG;IACjC,CAAC,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG,EAAE,KAAK,EAAE,GAAG;IAC7B,CAAC,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,GAAG;CAC5B,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,IAAY;IACxC,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;IAC5B,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAEpC,iCAAiC;IACjC,IAAI,MAAM,CAAC,MAAM,IAAI,CAAC,IAAI,WAAW,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACtD,OAAO;YACL,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;YACnB,MAAM,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC;SAClC,CAAC;IACJ,CAAC;IAED,kCAAkC;IAClC,MAAM,UAAU,GAAG,OAAO,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC;IACpD,IAAI,UAAU,EAAE,CAAC;QACf,MAAM,CAAC,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,OAAO,GAAG,UAAU,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC;QAC5C,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC;QAC/B,IAAI,IAAI,EAAE,CAAC;YACT,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,CAAC;YACzD,IAAI,MAAM,EAAE,CAAC;gBACX,OAAO;oBACL,QAAQ,EAAE,GAAG,CAAC,GAAG,IAAI,EAAE;oBACvB,MAAM;iBACP,CAAC;YACJ,CAAC;QACH,CAAC;IACH,CAAC;IAED,sBAAsB;IACtB,OAAO;QACL,QAAQ,EAAE,KAAK;QACf,MAAM,EAAE,OAAO;KAChB,CAAC;AACJ,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Core scheduler engine.
3
+ *
4
+ * 1-second tick loop that checks all tasks, gates on agent idle state,
5
+ * fires prompts via pi.sendUserMessage(), and handles auto-expiry.
6
+ */
7
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
8
+ import type { LoopConfig } from "./types.js";
9
+ export declare class LoopScheduler {
10
+ private interval;
11
+ private isAgentBusy;
12
+ private pendingFires;
13
+ private pi;
14
+ private config;
15
+ private cwd;
16
+ private ctx;
17
+ constructor(pi: ExtensionAPI, config: LoopConfig, cwd: string);
18
+ setContext(ctx: ExtensionContext): void;
19
+ start(): void;
20
+ stop(): void;
21
+ setBusy(): void;
22
+ setIdle(): void;
23
+ private check;
24
+ private shouldFire;
25
+ private fire;
26
+ private drainPendingFires;
27
+ private isAgedOut;
28
+ private updateStatus;
29
+ /** Refresh status bar (called externally after task changes) */
30
+ refreshStatus(): void;
31
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Core scheduler engine.
3
+ *
4
+ * 1-second tick loop that checks all tasks, gates on agent idle state,
5
+ * fires prompts via pi.sendUserMessage(), and handles auto-expiry.
6
+ */
7
+ import { nextCronRunMs, cronGapMs } from "./cron.js";
8
+ import { recurringJitterMs, oneShotJitterMs } from "./jitter.js";
9
+ import { getAllTasks, getTask, updateTask, removeTask, writeDurableTasks, } from "./store.js";
10
+ export class LoopScheduler {
11
+ interval = null;
12
+ isAgentBusy = false;
13
+ pendingFires = [];
14
+ pi;
15
+ config;
16
+ cwd;
17
+ ctx = null;
18
+ constructor(pi, config, cwd) {
19
+ this.pi = pi;
20
+ this.config = config;
21
+ this.cwd = cwd;
22
+ }
23
+ setContext(ctx) {
24
+ this.ctx = ctx;
25
+ }
26
+ start() {
27
+ if (this.interval)
28
+ return;
29
+ this.interval = setInterval(() => this.check(), this.config.checkIntervalMs);
30
+ }
31
+ stop() {
32
+ if (this.interval) {
33
+ clearInterval(this.interval);
34
+ this.interval = null;
35
+ }
36
+ }
37
+ setBusy() {
38
+ this.isAgentBusy = true;
39
+ }
40
+ setIdle() {
41
+ this.isAgentBusy = false;
42
+ this.drainPendingFires();
43
+ }
44
+ check() {
45
+ const now = Date.now();
46
+ for (const task of getAllTasks()) {
47
+ if (this.shouldFire(task, now)) {
48
+ if (this.isAgentBusy) {
49
+ // Queue for later, but only if not already queued
50
+ if (!this.pendingFires.includes(task.id)) {
51
+ this.pendingFires.push(task.id);
52
+ }
53
+ }
54
+ else {
55
+ this.fire(task);
56
+ }
57
+ }
58
+ }
59
+ }
60
+ shouldFire(task, now) {
61
+ // Compute the base next fire time
62
+ const anchor = task.lastFiredAt ?? task.createdAt;
63
+ const baseNext = nextCronRunMs(task.cron, anchor);
64
+ if (baseNext === null)
65
+ return false;
66
+ let fireTime;
67
+ if (task.recurring) {
68
+ // Forward jitter for recurring tasks
69
+ const gap = cronGapMs(task.cron, anchor);
70
+ const jitter = gap ? recurringJitterMs(task, gap, this.config) : 0;
71
+ fireTime = baseNext + jitter;
72
+ }
73
+ else {
74
+ // Backward jitter for one-shots
75
+ const jitter = oneShotJitterMs(task, baseNext, this.config);
76
+ fireTime = baseNext - jitter;
77
+ }
78
+ return now >= fireTime;
79
+ }
80
+ fire(task) {
81
+ // Notify UI
82
+ if (this.ctx?.hasUI) {
83
+ const label = task.label || task.prompt.slice(0, 40);
84
+ this.ctx.ui.notify(`Loop firing: ${label}`, "info");
85
+ }
86
+ // Inject prompt as user message
87
+ this.pi.sendUserMessage(task.prompt);
88
+ // Update fire time
89
+ task.lastFiredAt = Date.now();
90
+ if (task.recurring) {
91
+ if (this.isAgedOut(task)) {
92
+ // Final fire — remove the task
93
+ removeTask(task.id);
94
+ if (this.ctx?.hasUI) {
95
+ this.ctx.ui.notify(`Loop ${task.id} expired after 7 days`, "warning");
96
+ }
97
+ }
98
+ else {
99
+ updateTask(task);
100
+ }
101
+ }
102
+ else {
103
+ // One-shot: remove after firing
104
+ removeTask(task.id);
105
+ }
106
+ // Persist durable tasks
107
+ if (task.durable) {
108
+ writeDurableTasks(this.cwd, this.config).catch(() => { });
109
+ }
110
+ this.updateStatus();
111
+ }
112
+ drainPendingFires() {
113
+ const ids = this.pendingFires.splice(0);
114
+ for (const id of ids) {
115
+ const task = getTask(id);
116
+ if (task) {
117
+ this.fire(task);
118
+ }
119
+ }
120
+ }
121
+ isAgedOut(task) {
122
+ if (this.config.recurringMaxAgeMs <= 0)
123
+ return false;
124
+ return Date.now() - task.createdAt >= this.config.recurringMaxAgeMs;
125
+ }
126
+ updateStatus() {
127
+ if (!this.ctx?.hasUI)
128
+ return;
129
+ const count = getAllTasks().length;
130
+ if (count > 0) {
131
+ this.ctx.ui.setStatus("pi-loop", `${count} loop${count === 1 ? "" : "s"} active`);
132
+ }
133
+ else {
134
+ this.ctx.ui.setStatus("pi-loop", undefined);
135
+ }
136
+ }
137
+ /** Refresh status bar (called externally after task changes) */
138
+ refreshStatus() {
139
+ this.updateStatus();
140
+ }
141
+ }
142
+ //# sourceMappingURL=scheduler.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../src/scheduler.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * LoopTask CRUD — in-memory Map + durable .pi-loop.json persistence.
3
+ * Includes file-based O_EXCL locking for multi-instance safety.
4
+ */
5
+ import type { LoopConfig, LoopTask } from "./types.js";
6
+ export declare function generateTaskId(): string;
7
+ export declare function addTask(task: LoopTask): void;
8
+ export declare function removeTask(id: string): boolean;
9
+ export declare function getTask(id: string): LoopTask | undefined;
10
+ export declare function getAllTasks(): LoopTask[];
11
+ export declare function getTaskCount(): number;
12
+ export declare function updateTask(task: LoopTask): void;
13
+ export declare function clearAllTasks(): void;
14
+ export declare function loadDurableTasks(cwd: string, config: LoopConfig): Promise<LoopTask[]>;
15
+ export declare function writeDurableTasks(cwd: string, config: LoopConfig): Promise<void>;
16
+ export declare function acquireLock(cwd: string, config: LoopConfig): Promise<boolean>;
17
+ export declare function releaseLock(cwd: string, config: LoopConfig): Promise<void>;
package/dist/store.js ADDED
@@ -0,0 +1,104 @@
1
+ /**
2
+ * LoopTask CRUD — in-memory Map + durable .pi-loop.json persistence.
3
+ * Includes file-based O_EXCL locking for multi-instance safety.
4
+ */
5
+ import { readFile, writeFile, open, unlink } from "node:fs/promises";
6
+ import { join } from "node:path";
7
+ import { randomUUID } from "node:crypto";
8
+ // --- In-memory store ---
9
+ const tasks = new Map();
10
+ export function generateTaskId() {
11
+ return randomUUID().replace(/-/g, "").slice(0, 8);
12
+ }
13
+ export function addTask(task) {
14
+ tasks.set(task.id, task);
15
+ }
16
+ export function removeTask(id) {
17
+ return tasks.delete(id);
18
+ }
19
+ export function getTask(id) {
20
+ return tasks.get(id);
21
+ }
22
+ export function getAllTasks() {
23
+ return Array.from(tasks.values());
24
+ }
25
+ export function getTaskCount() {
26
+ return tasks.size;
27
+ }
28
+ export function updateTask(task) {
29
+ tasks.set(task.id, task);
30
+ }
31
+ export function clearAllTasks() {
32
+ tasks.clear();
33
+ }
34
+ // --- Durable file persistence ---
35
+ function durablePath(cwd, config) {
36
+ return join(cwd, config.durableFilePath);
37
+ }
38
+ function lockPath(cwd, config) {
39
+ return durablePath(cwd, config) + ".lock";
40
+ }
41
+ export async function loadDurableTasks(cwd, config) {
42
+ try {
43
+ const raw = await readFile(durablePath(cwd, config), "utf-8");
44
+ const data = JSON.parse(raw);
45
+ if (!Array.isArray(data.tasks))
46
+ return [];
47
+ return data.tasks;
48
+ }
49
+ catch {
50
+ return [];
51
+ }
52
+ }
53
+ export async function writeDurableTasks(cwd, config) {
54
+ const durableTasks = getAllTasks().filter((t) => t.durable);
55
+ const data = { tasks: durableTasks };
56
+ await writeFile(durablePath(cwd, config), JSON.stringify(data, null, 2) + "\n", "utf-8");
57
+ }
58
+ const STALE_LOCK_MS = 30_000;
59
+ export async function acquireLock(cwd, config) {
60
+ const path = lockPath(cwd, config);
61
+ const content = {
62
+ pid: process.pid,
63
+ acquiredAt: Date.now(),
64
+ };
65
+ try {
66
+ const fd = await open(path, "wx");
67
+ await fd.writeFile(JSON.stringify(content));
68
+ await fd.close();
69
+ return true;
70
+ }
71
+ catch (err) {
72
+ if (err.code !== "EEXIST")
73
+ return false;
74
+ // Check for stale lock
75
+ try {
76
+ const raw = await readFile(path, "utf-8");
77
+ const lock = JSON.parse(raw);
78
+ if (Date.now() - lock.acquiredAt > STALE_LOCK_MS) {
79
+ // Stale lock — remove and retry
80
+ await unlink(path);
81
+ return acquireLock(cwd, config);
82
+ }
83
+ }
84
+ catch {
85
+ // Can't read lock file — someone else has it
86
+ }
87
+ return false;
88
+ }
89
+ }
90
+ export async function releaseLock(cwd, config) {
91
+ try {
92
+ const path = lockPath(cwd, config);
93
+ const raw = await readFile(path, "utf-8");
94
+ const lock = JSON.parse(raw);
95
+ // Only release if we own it
96
+ if (lock.pid === process.pid) {
97
+ await unlink(path);
98
+ }
99
+ }
100
+ catch {
101
+ // Lock file gone or can't read — fine
102
+ }
103
+ }
104
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.js","sourceRoot":"","sources":["../src/store.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,7 @@
1
+ /**
2
+ * LLM-callable tools: cron_create, cron_delete, cron_list.
3
+ */
4
+ import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
5
+ import type { LoopConfig } from "../types.js";
6
+ import type { LoopScheduler } from "../scheduler.js";
7
+ export declare function registerCronTools(pi: ExtensionAPI, scheduler: LoopScheduler, config: LoopConfig, getCwd: () => string): void;
@@ -0,0 +1,129 @@
1
+ /**
2
+ * LLM-callable tools: cron_create, cron_delete, cron_list.
3
+ */
4
+ import { Type } from "@sinclair/typebox";
5
+ import { parseCronExpression, nextCronRunMs, cronToHuman } from "../cron.js";
6
+ import { addTask, removeTask, getAllTasks, getTaskCount, generateTaskId, writeDurableTasks, } from "../store.js";
7
+ function result(text) {
8
+ return { content: [{ type: "text", text }], details: undefined };
9
+ }
10
+ export function registerCronTools(pi, scheduler, config, getCwd) {
11
+ // --- CronCreate ---
12
+ pi.registerTool({
13
+ name: "cron_create",
14
+ label: "Create Scheduled Task",
15
+ description: "Schedule a prompt to run at a future time, either recurring on a cron schedule or once at a specific time. " +
16
+ "The cron field uses standard 5-field format (minute hour day-of-month month day-of-week) in local time. " +
17
+ "Examples: '*/5 * * * *' = every 5 minutes, '0 */2 * * *' = every 2 hours, '30 14 * * *' = daily at 2:30 PM.",
18
+ parameters: Type.Object({
19
+ cron: Type.String({
20
+ description: "5-field cron expression in local time (minute hour dom month dow)",
21
+ }),
22
+ prompt: Type.String({
23
+ description: "Prompt to enqueue at fire time",
24
+ }),
25
+ recurring: Type.Boolean({
26
+ default: true,
27
+ description: "true = fire on every match; false = one-shot then delete",
28
+ }),
29
+ durable: Type.Boolean({
30
+ default: false,
31
+ description: "true = persist to .pi-loop.json across sessions; false = session-only",
32
+ }),
33
+ }),
34
+ promptSnippet: "cron_create — schedule a prompt to run on a cron schedule (recurring or one-shot)",
35
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
36
+ const parsed = parseCronExpression(params.cron);
37
+ if (!parsed) {
38
+ return result(`Invalid cron expression: "${params.cron}". Use 5-field format: minute hour day-of-month month day-of-week`);
39
+ }
40
+ const nextRun = nextCronRunMs(params.cron, Date.now());
41
+ if (nextRun === null) {
42
+ return result(`Cron expression "${params.cron}" does not match any date in the next year.`);
43
+ }
44
+ if (getTaskCount() >= config.maxJobs) {
45
+ return result(`Maximum of ${config.maxJobs} scheduled tasks reached. Delete some tasks first.`);
46
+ }
47
+ const task = {
48
+ id: generateTaskId(),
49
+ cron: params.cron,
50
+ prompt: params.prompt,
51
+ createdAt: Date.now(),
52
+ recurring: params.recurring,
53
+ durable: params.durable,
54
+ };
55
+ addTask(task);
56
+ if (task.durable) {
57
+ await writeDurableTasks(getCwd(), config);
58
+ }
59
+ scheduler.refreshStatus();
60
+ const human = cronToHuman(params.cron);
61
+ const nextDate = new Date(nextRun).toLocaleString();
62
+ const expiryNote = params.recurring
63
+ ? ` Auto-expires after ${Math.round(config.recurringMaxAgeMs / (24 * 60 * 60 * 1000))} days.`
64
+ : " One-shot: will be deleted after firing.";
65
+ return result([
66
+ `Scheduled task created.`,
67
+ ` ID: ${task.id}`,
68
+ ` Schedule: ${human} (${params.cron})`,
69
+ ` Next fire: ${nextDate}`,
70
+ ` Prompt: ${task.prompt}`,
71
+ ` Recurring: ${params.recurring}`,
72
+ ` Durable: ${params.durable}`,
73
+ expiryNote,
74
+ `Cancel with: cron_delete { id: "${task.id}" } or /loop-kill ${task.id}`,
75
+ ].join("\n"));
76
+ },
77
+ });
78
+ // --- CronDelete ---
79
+ pi.registerTool({
80
+ name: "cron_delete",
81
+ label: "Cancel Scheduled Task",
82
+ description: "Cancel a scheduled cron task by ID. Use cron_list to see active tasks.",
83
+ parameters: Type.Object({
84
+ id: Type.String({ description: "Task ID to cancel" }),
85
+ }),
86
+ promptSnippet: "cron_delete — cancel a scheduled task by ID",
87
+ async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
88
+ const removed = removeTask(params.id);
89
+ if (!removed) {
90
+ return result(`No task found with ID "${params.id}".`);
91
+ }
92
+ await writeDurableTasks(getCwd(), config).catch(() => { });
93
+ scheduler.refreshStatus();
94
+ return result(`Task ${params.id} cancelled.`);
95
+ },
96
+ });
97
+ // --- CronList ---
98
+ pi.registerTool({
99
+ name: "cron_list",
100
+ label: "List Scheduled Tasks",
101
+ description: "List all active scheduled cron tasks with their IDs, schedules, and next fire times.",
102
+ parameters: Type.Object({}),
103
+ promptSnippet: "cron_list — list all active scheduled tasks",
104
+ async execute(_toolCallId, _params, _signal, _onUpdate, _ctx) {
105
+ const tasks = getAllTasks();
106
+ if (tasks.length === 0) {
107
+ return result("No scheduled tasks.");
108
+ }
109
+ const now = Date.now();
110
+ const lines = tasks.map((t) => {
111
+ const human = cronToHuman(t.cron);
112
+ const next = nextCronRunMs(t.cron, t.lastFiredAt ?? t.createdAt);
113
+ const nextStr = next ? new Date(next).toLocaleString() : "unknown";
114
+ const age = Math.round((now - t.createdAt) / (60 * 1000));
115
+ const flags = [
116
+ t.recurring ? "recurring" : "one-shot",
117
+ t.durable ? "durable" : "session-only",
118
+ ].join(", ");
119
+ return [
120
+ `[${t.id}] ${human} (${t.cron})`,
121
+ ` Prompt: ${t.prompt}`,
122
+ ` Next: ${nextStr} | Created: ${age}m ago | ${flags}`,
123
+ ].join("\n");
124
+ });
125
+ return result(`${tasks.length} scheduled task${tasks.length === 1 ? "" : "s"}:\n\n${lines.join("\n\n")}`);
126
+ },
127
+ });
128
+ }
129
+ //# sourceMappingURL=cron-tools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cron-tools.js","sourceRoot":"","sources":["../../src/tools/cron-tools.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,27 @@
1
+ import { type Static } from "@sinclair/typebox";
2
+ export declare const LoopTaskSchema: import("@sinclair/typebox").TObject<{
3
+ id: import("@sinclair/typebox").TString;
4
+ cron: import("@sinclair/typebox").TString;
5
+ prompt: import("@sinclair/typebox").TString;
6
+ createdAt: import("@sinclair/typebox").TNumber;
7
+ lastFiredAt: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TNumber>;
8
+ recurring: import("@sinclair/typebox").TBoolean;
9
+ durable: import("@sinclair/typebox").TBoolean;
10
+ label: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
11
+ }>;
12
+ export type LoopTask = Static<typeof LoopTaskSchema>;
13
+ export interface LoopConfig {
14
+ maxJobs: number;
15
+ recurringMaxAgeMs: number;
16
+ recurringJitterFrac: number;
17
+ recurringJitterCapMs: number;
18
+ oneShotJitterMaxMs: number;
19
+ oneShotJitterFloorMs: number;
20
+ oneShotJitterMinuteMod: number;
21
+ checkIntervalMs: number;
22
+ durableFilePath: string;
23
+ }
24
+ export declare const DEFAULT_CONFIG: LoopConfig;
25
+ export interface DurableFile {
26
+ tasks: LoopTask[];
27
+ }
package/dist/types.js ADDED
@@ -0,0 +1,24 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ // --- LoopTask schema (TypeBox for tool parameters) ---
3
+ export const LoopTaskSchema = Type.Object({
4
+ id: Type.String(),
5
+ cron: Type.String(),
6
+ prompt: Type.String(),
7
+ createdAt: Type.Number(),
8
+ lastFiredAt: Type.Optional(Type.Number()),
9
+ recurring: Type.Boolean(),
10
+ durable: Type.Boolean(),
11
+ label: Type.Optional(Type.String()),
12
+ });
13
+ export const DEFAULT_CONFIG = {
14
+ maxJobs: 50,
15
+ recurringMaxAgeMs: 7 * 24 * 60 * 60 * 1000,
16
+ recurringJitterFrac: 0.1,
17
+ recurringJitterCapMs: 15 * 60 * 1000,
18
+ oneShotJitterMaxMs: 90 * 1000,
19
+ oneShotJitterFloorMs: 0,
20
+ oneShotJitterMinuteMod: 30,
21
+ checkIntervalMs: 1000,
22
+ durableFilePath: ".pi-loop.json",
23
+ };
24
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAe,MAAM,mBAAmB,CAAC;AAEtD,wDAAwD;AAExD,MAAM,CAAC,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,CAAC;IACxC,EAAE,EAAE,IAAI,CAAC,MAAM,EAAE;IACjB,IAAI,EAAE,IAAI,CAAC,MAAM,EAAE;IACnB,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE;IACrB,SAAS,EAAE,IAAI,CAAC,MAAM,EAAE;IACxB,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;IACzC,SAAS,EAAE,IAAI,CAAC,OAAO,EAAE;IACzB,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE;IACvB,KAAK,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;CACpC,CAAC,CAAC;AAkBH,MAAM,CAAC,MAAM,cAAc,GAAe;IACxC,OAAO,EAAE,EAAE;IACX,iBAAiB,EAAE,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI;IAC1C,mBAAmB,EAAE,GAAG;IACxB,oBAAoB,EAAE,EAAE,GAAG,EAAE,GAAG,IAAI;IACpC,kBAAkB,EAAE,EAAE,GAAG,IAAI;IAC7B,oBAAoB,EAAE,CAAC;IACvB,sBAAsB,EAAE,EAAE;IAC1B,eAAe,EAAE,IAAI;IACrB,eAAe,EAAE,eAAe;CACjC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@pi-agents/loop",
3
+ "version": "0.1.0",
4
+ "description": "Recurring prompt execution and cron scheduling for pi-agent — /loop, cron_create, cron_delete, cron_list",
5
+ "author": "ArtemisAI",
6
+ "license": "MIT",
7
+ "type": "module",
8
+ "main": "dist/index.js",
9
+ "types": "dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist/**",
18
+ "skills",
19
+ "config/default.json",
20
+ "README.md",
21
+ "LICENSE"
22
+ ],
23
+ "pi": {
24
+ "extensions": [
25
+ "./dist/index.js"
26
+ ],
27
+ "skills": [
28
+ "./skills"
29
+ ]
30
+ },
31
+ "peerDependencies": {
32
+ "@mariozechner/pi-agent-core": "*",
33
+ "@mariozechner/pi-ai": "*",
34
+ "@mariozechner/pi-coding-agent": "*",
35
+ "@sinclair/typebox": "*"
36
+ },
37
+ "devDependencies": {
38
+ "@mariozechner/pi-agent-core": "*",
39
+ "@mariozechner/pi-ai": "*",
40
+ "@mariozechner/pi-coding-agent": "*",
41
+ "@sinclair/typebox": "^0.34.0",
42
+ "@types/node": "^22.0.0",
43
+ "typescript": "^5.7.0"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public"
47
+ },
48
+ "engines": {
49
+ "node": ">=20"
50
+ },
51
+ "keywords": [
52
+ "pi",
53
+ "pi-package",
54
+ "pi-extension",
55
+ "pi-coding-agent",
56
+ "pi-agent",
57
+ "loop",
58
+ "cron",
59
+ "scheduler",
60
+ "recurring",
61
+ "automation"
62
+ ],
63
+ "repository": {
64
+ "type": "git",
65
+ "url": "git+https://github.com/ArtemisAI/pi-loop.git"
66
+ },
67
+ "bugs": {
68
+ "url": "https://github.com/ArtemisAI/pi-loop/issues"
69
+ },
70
+ "homepage": "https://github.com/ArtemisAI/pi-loop#readme",
71
+ "scripts": {
72
+ "prebuild": "rm -rf dist",
73
+ "build": "tsc",
74
+ "lint": "tsc --noEmit",
75
+ "dev": "pi -e ./src/index.ts -s ./skills",
76
+ "prepublishOnly": "npm run build"
77
+ }
78
+ }