@pmndrs/pine 0.0.0-stage → 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.
- package/LICENSE +21 -0
- package/README.md +180 -2
- package/bin/pine.js +27 -0
- package/docs/SKILL.md +189 -0
- package/docs/references/agents.md +368 -0
- package/docs/references/coding.md +126 -0
- package/docs/references/configuration.md +229 -0
- package/docs/references/events.md +263 -0
- package/docs/references/mcp.md +257 -0
- package/docs/references/operations.md +280 -0
- package/docs/references/overview.md +186 -0
- package/docs/references/security.md +489 -0
- package/docs/references/setup.md +181 -0
- package/docs/references/sources.md +768 -0
- package/docs/references/stack.md +105 -0
- package/docs/references/tui.md +348 -0
- package/examples/home/.env.example +5 -0
- package/examples/home/USER.md +4 -0
- package/examples/home/agents/main/agent.json +4 -0
- package/examples/home/agents/main/workspace/AGENTS.md +14 -0
- package/examples/home/agents/main/workspace/MEMORY.md +4 -0
- package/examples/home/agents/main/workspace/SOUL.md +4 -0
- package/examples/home/models.json +62 -0
- package/examples/home/pine.json +30 -0
- package/examples/home/skills/README.txt +7 -0
- package/examples/launchd/com.pine.daemon.plist +32 -0
- package/mcp/browser.d.ts +34 -0
- package/mcp/browser.js +228 -0
- package/mcp/commands.d.ts +20 -0
- package/mcp/commands.js +88 -0
- package/mcp/lib.d.ts +13 -0
- package/mcp/lib.js +50 -0
- package/mcp/web.d.ts +6 -0
- package/mcp/web.js +140 -0
- package/package.json +32 -4
- package/src/agent-admin.d.ts +24 -0
- package/src/agent-admin.js +55 -0
- package/src/agents.d.ts +343 -0
- package/src/agents.js +1587 -0
- package/src/cli.d.ts +2 -0
- package/src/cli.js +526 -0
- package/src/config.d.ts +351 -0
- package/src/config.js +514 -0
- package/src/dev/claude-model.d.ts +28 -0
- package/src/dev/claude-model.js +174 -0
- package/src/env-file.d.ts +4 -0
- package/src/env-file.js +40 -0
- package/src/http.d.ts +46 -0
- package/src/http.js +407 -0
- package/src/limiter.d.ts +32 -0
- package/src/limiter.js +84 -0
- package/src/login.d.ts +21 -0
- package/src/login.js +126 -0
- package/src/manual.d.ts +61 -0
- package/src/manual.js +425 -0
- package/src/mcp-config.d.ts +40 -0
- package/src/mcp-config.js +132 -0
- package/src/net.d.ts +36 -0
- package/src/net.js +122 -0
- package/src/pine.d.ts +32 -0
- package/src/pine.js +142 -0
- package/src/router.d.ts +56 -0
- package/src/router.js +164 -0
- package/src/sandbox-fs.d.ts +27 -0
- package/src/sandbox-fs.js +172 -0
- package/src/security.d.ts +121 -0
- package/src/security.js +363 -0
- package/src/skills.d.ts +67 -0
- package/src/skills.js +163 -0
- package/src/source-types.d.ts +61 -0
- package/src/source-types.js +1 -0
- package/src/sources/command.d.ts +4 -0
- package/src/sources/command.js +77 -0
- package/src/sources/folder.d.ts +6 -0
- package/src/sources/folder.js +116 -0
- package/src/sources/ftp.d.ts +4 -0
- package/src/sources/ftp.js +307 -0
- package/src/sources/http.d.ts +4 -0
- package/src/sources/http.js +201 -0
- package/src/sources/mqtt.d.ts +18 -0
- package/src/sources/mqtt.js +97 -0
- package/src/sources/telegram.d.ts +22 -0
- package/src/sources/telegram.js +269 -0
- package/src/sources/webhook.d.ts +4 -0
- package/src/sources/webhook.js +52 -0
- package/src/sources.d.ts +339 -0
- package/src/sources.js +955 -0
- package/src/store.d.ts +89 -0
- package/src/store.js +251 -0
- package/src/timers.d.ts +83 -0
- package/src/timers.js +318 -0
- package/src/tools.d.ts +6 -0
- package/src/tools.js +777 -0
- package/src/tui/app.d.ts +209 -0
- package/src/tui/app.js +2223 -0
- package/src/tui/client.d.ts +178 -0
- package/src/tui/client.js +116 -0
- package/src/tui/code-pane.d.ts +51 -0
- package/src/tui/code-pane.js +398 -0
- package/src/tui/highlight.d.ts +8 -0
- package/src/tui/highlight.js +86 -0
- package/src/tui/picture.d.ts +141 -0
- package/src/tui/picture.js +571 -0
- package/src/tui/run.d.ts +8 -0
- package/src/tui/run.js +97 -0
- package/src/tui/theme.d.ts +32 -0
- package/src/tui/theme.js +128 -0
- package/src/types.d.ts +124 -0
- package/src/types.js +1 -0
- package/src/update.d.ts +25 -0
- package/src/update.js +110 -0
- package/src/util.d.ts +32 -0
- package/src/util.js +116 -0
- package/src/watching.d.ts +20 -0
- package/src/watching.js +68 -0
- package/src/workspace-view.d.ts +38 -0
- package/src/workspace-view.js +160 -0
package/src/store.d.ts
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
import type { PineEvent, PublishOptions, Task, TaskStatus } from "./types.ts";
|
|
3
|
+
import { type Logger } from "./util.ts";
|
|
4
|
+
export declare function openDb(dataDir: string): DatabaseSync;
|
|
5
|
+
type Handler = (event: PineEvent) => void | Promise<void>;
|
|
6
|
+
/** Append-only event log with in-process subscriptions. Everything that happens goes through here. */
|
|
7
|
+
export declare class Bus {
|
|
8
|
+
private db;
|
|
9
|
+
private log;
|
|
10
|
+
private subs;
|
|
11
|
+
private pending;
|
|
12
|
+
/** Applied to every payload before it is stored or delivered. */
|
|
13
|
+
redact?: <T>(payload: T) => T;
|
|
14
|
+
constructor(db: DatabaseSync, log: Logger);
|
|
15
|
+
/**
|
|
16
|
+
* Take a secret out of what is already stored. A token the owner typed into a conversation
|
|
17
|
+
* ("here is the bot's token") was in events and tasks before anyone knew it was one.
|
|
18
|
+
*/
|
|
19
|
+
scrub(secret: string): void;
|
|
20
|
+
publish(topic: string, payload?: unknown, opts?: PublishOptions): PineEvent;
|
|
21
|
+
/** Handlers run asynchronously after publish() returns; a throwing handler is logged, never rethrown. */
|
|
22
|
+
subscribe(pattern: string | string[], handler: Handler, name?: string): () => void;
|
|
23
|
+
/** Resolve with the next event matching `pattern` (and `filter`), or reject after `timeoutMs`. */
|
|
24
|
+
waitFor(pattern: string, filter?: (e: PineEvent) => boolean, timeoutMs?: number): Promise<PineEvent>;
|
|
25
|
+
/** Wait until every handler started so far has finished. */
|
|
26
|
+
drain(): Promise<void>;
|
|
27
|
+
get(id: number): PineEvent | undefined;
|
|
28
|
+
query(q?: {
|
|
29
|
+
topic?: string;
|
|
30
|
+
after?: number;
|
|
31
|
+
limit?: number;
|
|
32
|
+
}): PineEvent[];
|
|
33
|
+
prune(olderThanMs: number): number;
|
|
34
|
+
private fromRow;
|
|
35
|
+
}
|
|
36
|
+
/** Shared world state: small JSON values under slash-separated keys. Every write is also an event on `state/<key>`. */
|
|
37
|
+
export declare class State {
|
|
38
|
+
private db;
|
|
39
|
+
private bus;
|
|
40
|
+
constructor(db: DatabaseSync, bus: Bus);
|
|
41
|
+
get<T = unknown>(key: string): T | undefined;
|
|
42
|
+
set(key: string, value: unknown, opts?: {
|
|
43
|
+
source?: string;
|
|
44
|
+
cause?: PineEvent | null;
|
|
45
|
+
}): void;
|
|
46
|
+
list(patterns?: string | string[]): Array<{
|
|
47
|
+
key: string;
|
|
48
|
+
value: unknown;
|
|
49
|
+
ts: number;
|
|
50
|
+
source: string;
|
|
51
|
+
}>;
|
|
52
|
+
}
|
|
53
|
+
/** Private key-value storage for sources and the daemon (cursors, last-run times). No events. */
|
|
54
|
+
export declare class Kv {
|
|
55
|
+
private db;
|
|
56
|
+
constructor(db: DatabaseSync);
|
|
57
|
+
get<T = unknown>(key: string): T | undefined;
|
|
58
|
+
set(key: string, value: unknown): void;
|
|
59
|
+
/** Remove every entry whose key starts with a prefix. */
|
|
60
|
+
forget(prefix: string): void;
|
|
61
|
+
/** The keys that start with a prefix. */
|
|
62
|
+
keys(prefix: string): string[];
|
|
63
|
+
}
|
|
64
|
+
export declare class TaskStore {
|
|
65
|
+
private db;
|
|
66
|
+
constructor(db: DatabaseSync);
|
|
67
|
+
insert(t: Omit<Task, "id">): Task;
|
|
68
|
+
update(id: number, patch: Partial<Pick<Task, "status" | "started" | "finished" | "result" | "error" | "session" | "meta">>): void;
|
|
69
|
+
get(id: number): Task | undefined;
|
|
70
|
+
list(q?: {
|
|
71
|
+
status?: TaskStatus;
|
|
72
|
+
agent?: string;
|
|
73
|
+
session?: string;
|
|
74
|
+
limit?: number;
|
|
75
|
+
}): Task[];
|
|
76
|
+
/** Named sessions of an agent that have had tasks, most recently used first. */
|
|
77
|
+
sessions(agent: string): Array<{
|
|
78
|
+
session: string;
|
|
79
|
+
tasks: number;
|
|
80
|
+
last: number;
|
|
81
|
+
}>;
|
|
82
|
+
/** Delete everything said in these conversations of an agent, with all the work they started. */
|
|
83
|
+
forget(agent: string, sessions: string[]): number;
|
|
84
|
+
/** Give a conversation's tasks another conversation name. */
|
|
85
|
+
rename(agent: string, from: string, to: string): void;
|
|
86
|
+
byStatus(status: TaskStatus): Task[];
|
|
87
|
+
private fromRow;
|
|
88
|
+
}
|
|
89
|
+
export {};
|
package/src/store.js
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import { errText, topicMatches } from "./util.js";
|
|
5
|
+
const SCHEMA = `
|
|
6
|
+
CREATE TABLE IF NOT EXISTS events (
|
|
7
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, topic TEXT NOT NULL, source TEXT NOT NULL,
|
|
8
|
+
payload TEXT, cause INTEGER, hops INTEGER NOT NULL DEFAULT 0, reply TEXT, trust TEXT NOT NULL DEFAULT 'untrusted'
|
|
9
|
+
);
|
|
10
|
+
CREATE INDEX IF NOT EXISTS events_topic ON events(topic, id);
|
|
11
|
+
CREATE INDEX IF NOT EXISTS events_ts ON events(ts);
|
|
12
|
+
CREATE TABLE IF NOT EXISTS state (key TEXT PRIMARY KEY, value TEXT, ts INTEGER NOT NULL, source TEXT NOT NULL);
|
|
13
|
+
CREATE TABLE IF NOT EXISTS kv (key TEXT PRIMARY KEY, value TEXT);
|
|
14
|
+
CREATE TABLE IF NOT EXISTS tasks (
|
|
15
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT, agent TEXT NOT NULL, session TEXT NOT NULL, kind TEXT NOT NULL, prompt TEXT NOT NULL,
|
|
16
|
+
status TEXT NOT NULL, priority INTEGER NOT NULL DEFAULT 0, created INTEGER NOT NULL, started INTEGER, finished INTEGER,
|
|
17
|
+
result TEXT, error TEXT, event INTEGER, hops INTEGER NOT NULL DEFAULT 0, parent INTEGER, depth INTEGER NOT NULL DEFAULT 0,
|
|
18
|
+
reply TEXT, meta TEXT
|
|
19
|
+
);
|
|
20
|
+
CREATE INDEX IF NOT EXISTS tasks_status ON tasks(status, id);
|
|
21
|
+
`;
|
|
22
|
+
export function openDb(dataDir) {
|
|
23
|
+
fs.mkdirSync(dataDir, { recursive: true });
|
|
24
|
+
const db = new DatabaseSync(path.join(dataDir, "pine.db"));
|
|
25
|
+
db.exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = NORMAL; PRAGMA busy_timeout = 5000;");
|
|
26
|
+
db.exec(SCHEMA);
|
|
27
|
+
return db;
|
|
28
|
+
}
|
|
29
|
+
const parse = (s) => (typeof s === "string" && s.length ? JSON.parse(s) : undefined);
|
|
30
|
+
/** Append-only event log with in-process subscriptions. Everything that happens goes through here. */
|
|
31
|
+
export class Bus {
|
|
32
|
+
db;
|
|
33
|
+
log;
|
|
34
|
+
subs = new Set();
|
|
35
|
+
pending = new Set();
|
|
36
|
+
/** Applied to every payload before it is stored or delivered. */
|
|
37
|
+
redact;
|
|
38
|
+
constructor(db, log) {
|
|
39
|
+
this.db = db;
|
|
40
|
+
this.log = log;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Take a secret out of what is already stored. A token the owner typed into a conversation
|
|
44
|
+
* ("here is the bot's token") was in events and tasks before anyone knew it was one.
|
|
45
|
+
*/
|
|
46
|
+
scrub(secret) {
|
|
47
|
+
if (secret.length < 8)
|
|
48
|
+
return;
|
|
49
|
+
this.db.prepare("UPDATE events SET payload = replace(payload, ?, '[redacted]') WHERE instr(payload, ?) > 0").run(secret, secret);
|
|
50
|
+
for (const col of ["prompt", "result", "error", "meta"])
|
|
51
|
+
this.db.prepare(`UPDATE tasks SET ${col} = replace(${col}, ?, '[redacted]') WHERE instr(${col}, ?) > 0`).run(secret, secret);
|
|
52
|
+
}
|
|
53
|
+
publish(topic, payload = null, opts = {}) {
|
|
54
|
+
if (!topic || /[+#]/.test(topic))
|
|
55
|
+
throw new Error(`invalid topic: ${JSON.stringify(topic)}`);
|
|
56
|
+
const cause = opts.cause ?? undefined;
|
|
57
|
+
const event = {
|
|
58
|
+
id: 0,
|
|
59
|
+
ts: Date.now(),
|
|
60
|
+
topic,
|
|
61
|
+
source: opts.source ?? "pine",
|
|
62
|
+
payload,
|
|
63
|
+
cause: cause?.id || undefined,
|
|
64
|
+
hops: (cause?.hops ?? 0) + (opts.hop ? 1 : 0),
|
|
65
|
+
reply: opts.reply,
|
|
66
|
+
trust: opts.trust ?? "untrusted",
|
|
67
|
+
};
|
|
68
|
+
if (this.redact)
|
|
69
|
+
event.payload = this.redact(event.payload);
|
|
70
|
+
if (opts.persist !== false) {
|
|
71
|
+
const r = this.db
|
|
72
|
+
.prepare("INSERT INTO events (ts, topic, source, payload, cause, hops, reply, trust) VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
|
|
73
|
+
.run(event.ts, topic, event.source, JSON.stringify(event.payload ?? null), event.cause ?? null, event.hops, event.reply ? JSON.stringify(event.reply) : null, event.trust);
|
|
74
|
+
event.id = Number(r.lastInsertRowid);
|
|
75
|
+
}
|
|
76
|
+
for (const sub of this.subs) {
|
|
77
|
+
if (!topicMatches(sub.pattern, topic))
|
|
78
|
+
continue;
|
|
79
|
+
const p = Promise.resolve()
|
|
80
|
+
.then(() => sub.handler(event))
|
|
81
|
+
.catch((e) => this.log.error(`subscriber ${sub.name} failed on ${topic}`, errText(e)))
|
|
82
|
+
.finally(() => this.pending.delete(p));
|
|
83
|
+
this.pending.add(p);
|
|
84
|
+
}
|
|
85
|
+
return event;
|
|
86
|
+
}
|
|
87
|
+
/** Handlers run asynchronously after publish() returns; a throwing handler is logged, never rethrown. */
|
|
88
|
+
subscribe(pattern, handler, name = "anonymous") {
|
|
89
|
+
const entries = (Array.isArray(pattern) ? pattern : [pattern]).map((p) => ({ pattern: p, handler, name }));
|
|
90
|
+
for (const e of entries)
|
|
91
|
+
this.subs.add(e);
|
|
92
|
+
return () => {
|
|
93
|
+
for (const e of entries)
|
|
94
|
+
this.subs.delete(e);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
/** Resolve with the next event matching `pattern` (and `filter`), or reject after `timeoutMs`. */
|
|
98
|
+
waitFor(pattern, filter = () => true, timeoutMs = 10_000) {
|
|
99
|
+
return new Promise((resolve, reject) => {
|
|
100
|
+
const timer = setTimeout(() => {
|
|
101
|
+
off();
|
|
102
|
+
reject(new Error(`no event on ${pattern} within ${timeoutMs} ms`));
|
|
103
|
+
}, timeoutMs);
|
|
104
|
+
const off = this.subscribe(pattern, (e) => {
|
|
105
|
+
if (!filter(e))
|
|
106
|
+
return;
|
|
107
|
+
clearTimeout(timer);
|
|
108
|
+
off();
|
|
109
|
+
resolve(e);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Wait until every handler started so far has finished. */
|
|
114
|
+
async drain() {
|
|
115
|
+
while (this.pending.size)
|
|
116
|
+
await Promise.allSettled([...this.pending]);
|
|
117
|
+
}
|
|
118
|
+
get(id) {
|
|
119
|
+
const row = this.db.prepare("SELECT * FROM events WHERE id = ?").get(id);
|
|
120
|
+
return row ? this.fromRow(row) : undefined;
|
|
121
|
+
}
|
|
122
|
+
query(q = {}) {
|
|
123
|
+
const limit = Math.min(q.limit ?? 100, 1000);
|
|
124
|
+
// Patterns are matched in JS; a literal prefix narrows the scan.
|
|
125
|
+
const prefix = q.topic ? q.topic.split(/[+#]/)[0] : "";
|
|
126
|
+
const rows = q.after !== undefined
|
|
127
|
+
? this.db.prepare("SELECT * FROM events WHERE id > ? AND topic LIKE ? ESCAPE '\\' ORDER BY id ASC LIMIT ?").all(q.after, `${prefix.replace(/[\\%_]/g, "\\$&")}%`, limit * 20)
|
|
128
|
+
: this.db.prepare("SELECT * FROM events WHERE topic LIKE ? ESCAPE '\\' ORDER BY id DESC LIMIT ?").all(`${prefix.replace(/[\\%_]/g, "\\$&")}%`, limit * 20);
|
|
129
|
+
const events = rows.map((r) => this.fromRow(r)).filter((e) => !q.topic || topicMatches(q.topic, e.topic)).slice(0, limit);
|
|
130
|
+
return q.after !== undefined ? events : events.reverse();
|
|
131
|
+
}
|
|
132
|
+
prune(olderThanMs) {
|
|
133
|
+
return Number(this.db.prepare("DELETE FROM events WHERE ts < ?").run(Date.now() - olderThanMs).changes);
|
|
134
|
+
}
|
|
135
|
+
fromRow(r) {
|
|
136
|
+
return { id: r.id, ts: r.ts, topic: r.topic, source: r.source, payload: parse(r.payload) ?? null, cause: r.cause ?? undefined, hops: r.hops, reply: parse(r.reply), trust: r.trust === "owner" ? "owner" : "untrusted" };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
/** Shared world state: small JSON values under slash-separated keys. Every write is also an event on `state/<key>`. */
|
|
140
|
+
export class State {
|
|
141
|
+
db;
|
|
142
|
+
bus;
|
|
143
|
+
constructor(db, bus) {
|
|
144
|
+
this.db = db;
|
|
145
|
+
this.bus = bus;
|
|
146
|
+
}
|
|
147
|
+
get(key) {
|
|
148
|
+
const row = this.db.prepare("SELECT value FROM state WHERE key = ?").get(key);
|
|
149
|
+
return row ? parse(row.value) : undefined;
|
|
150
|
+
}
|
|
151
|
+
set(key, value, opts = {}) {
|
|
152
|
+
if (!key || /[+#]/.test(key))
|
|
153
|
+
throw new Error(`invalid state key: ${JSON.stringify(key)}`);
|
|
154
|
+
const previous = this.get(key);
|
|
155
|
+
if (value === undefined || value === null)
|
|
156
|
+
this.db.prepare("DELETE FROM state WHERE key = ?").run(key);
|
|
157
|
+
else
|
|
158
|
+
this.db.prepare("INSERT INTO state (key, value, ts, source) VALUES (?, ?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, ts = excluded.ts, source = excluded.source").run(key, JSON.stringify(value), Date.now(), opts.source ?? "pine");
|
|
159
|
+
if (JSON.stringify(previous) !== JSON.stringify(value ?? undefined))
|
|
160
|
+
this.bus.publish(`state/${key}`, { value: value ?? null, previous: previous ?? null }, { source: opts.source ?? "pine", cause: opts.cause, hop: Boolean(opts.cause) });
|
|
161
|
+
}
|
|
162
|
+
list(patterns = "#") {
|
|
163
|
+
const list = Array.isArray(patterns) ? patterns : [patterns];
|
|
164
|
+
return this.db.prepare("SELECT * FROM state ORDER BY key").all()
|
|
165
|
+
.filter((r) => list.some((p) => topicMatches(p, r.key)))
|
|
166
|
+
.map((r) => ({ key: r.key, value: parse(r.value), ts: r.ts, source: r.source }));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
/** Private key-value storage for sources and the daemon (cursors, last-run times). No events. */
|
|
170
|
+
export class Kv {
|
|
171
|
+
db;
|
|
172
|
+
constructor(db) {
|
|
173
|
+
this.db = db;
|
|
174
|
+
}
|
|
175
|
+
get(key) {
|
|
176
|
+
const row = this.db.prepare("SELECT value FROM kv WHERE key = ?").get(key);
|
|
177
|
+
return row ? parse(row.value) : undefined;
|
|
178
|
+
}
|
|
179
|
+
set(key, value) {
|
|
180
|
+
this.db.prepare("INSERT INTO kv (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value").run(key, JSON.stringify(value));
|
|
181
|
+
}
|
|
182
|
+
/** Remove every entry whose key starts with a prefix. */
|
|
183
|
+
forget(prefix) {
|
|
184
|
+
this.db.prepare("DELETE FROM kv WHERE substr(key, 1, ?) = ?").run(prefix.length, prefix);
|
|
185
|
+
}
|
|
186
|
+
/** The keys that start with a prefix. */
|
|
187
|
+
keys(prefix) {
|
|
188
|
+
return this.db.prepare("SELECT key FROM kv WHERE substr(key, 1, ?) = ? ORDER BY key").all(prefix.length, prefix).map((r) => r.key);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export class TaskStore {
|
|
192
|
+
db;
|
|
193
|
+
constructor(db) {
|
|
194
|
+
this.db = db;
|
|
195
|
+
}
|
|
196
|
+
insert(t) {
|
|
197
|
+
const r = this.db
|
|
198
|
+
.prepare("INSERT INTO tasks (agent, session, kind, prompt, status, priority, created, event, hops, parent, depth, reply, meta) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
|
|
199
|
+
.run(t.agent, t.session, t.kind, t.prompt, t.status, t.priority, t.created, t.event ?? null, t.hops, t.parent ?? null, t.depth, t.reply ? JSON.stringify(t.reply) : null, JSON.stringify(t.meta ?? {}));
|
|
200
|
+
return { ...t, id: Number(r.lastInsertRowid) };
|
|
201
|
+
}
|
|
202
|
+
update(id, patch) {
|
|
203
|
+
const cols = Object.keys(patch);
|
|
204
|
+
if (!cols.length)
|
|
205
|
+
return;
|
|
206
|
+
const values = cols.map((c) => (c === "meta" ? JSON.stringify(patch.meta) : (patch[c] ?? null)));
|
|
207
|
+
this.db.prepare(`UPDATE tasks SET ${cols.map((c) => `${c} = ?`).join(", ")} WHERE id = ?`).run(...values, id);
|
|
208
|
+
}
|
|
209
|
+
get(id) {
|
|
210
|
+
const row = this.db.prepare("SELECT * FROM tasks WHERE id = ?").get(id);
|
|
211
|
+
return row ? this.fromRow(row) : undefined;
|
|
212
|
+
}
|
|
213
|
+
list(q = {}) {
|
|
214
|
+
const where = [];
|
|
215
|
+
const args = [];
|
|
216
|
+
if (q.status)
|
|
217
|
+
where.push("status = ?"), args.push(q.status);
|
|
218
|
+
if (q.agent)
|
|
219
|
+
where.push("agent = ?"), args.push(q.agent);
|
|
220
|
+
if (q.session)
|
|
221
|
+
where.push("session = ?"), args.push(q.session);
|
|
222
|
+
const sql = `SELECT * FROM tasks ${where.length ? `WHERE ${where.join(" AND ")}` : ""} ORDER BY id DESC LIMIT ?`;
|
|
223
|
+
return this.db.prepare(sql).all(...args, Math.min(q.limit ?? 50, 1000)).map((r) => this.fromRow(r));
|
|
224
|
+
}
|
|
225
|
+
/** Named sessions of an agent that have had tasks, most recently used first. */
|
|
226
|
+
sessions(agent) {
|
|
227
|
+
return this.db.prepare("SELECT session, COUNT(*) AS tasks, MAX(created) AS last FROM tasks WHERE agent = ? AND session NOT LIKE '\\_t%' ESCAPE '\\' GROUP BY session ORDER BY last DESC").all(agent).map((r) => ({ session: r.session, tasks: r.tasks, last: r.last }));
|
|
228
|
+
}
|
|
229
|
+
/** Delete everything said in these conversations of an agent, with all the work they started. */
|
|
230
|
+
forget(agent, sessions) {
|
|
231
|
+
if (!sessions.length)
|
|
232
|
+
return 0;
|
|
233
|
+
const marks = sessions.map(() => "?").join(", ");
|
|
234
|
+
const sql = `WITH RECURSIVE gone(id) AS (SELECT id FROM tasks WHERE agent = ? AND session IN (${marks}) UNION SELECT t.id FROM tasks t JOIN gone g ON t.parent = g.id) DELETE FROM tasks WHERE id IN (SELECT id FROM gone)`;
|
|
235
|
+
return Number(this.db.prepare(sql).run(agent, ...sessions).changes);
|
|
236
|
+
}
|
|
237
|
+
/** Give a conversation's tasks another conversation name. */
|
|
238
|
+
rename(agent, from, to) {
|
|
239
|
+
this.db.prepare("UPDATE tasks SET session = ? WHERE agent = ? AND session = ?").run(to, agent, from);
|
|
240
|
+
}
|
|
241
|
+
byStatus(status) {
|
|
242
|
+
return this.db.prepare("SELECT * FROM tasks WHERE status = ? ORDER BY id ASC").all(status).map((r) => this.fromRow(r));
|
|
243
|
+
}
|
|
244
|
+
fromRow(r) {
|
|
245
|
+
return {
|
|
246
|
+
id: r.id, agent: r.agent, session: r.session, kind: r.kind, prompt: r.prompt, status: r.status, priority: r.priority, created: r.created,
|
|
247
|
+
started: r.started ?? undefined, finished: r.finished ?? undefined, result: r.result ?? undefined, error: r.error ?? undefined,
|
|
248
|
+
event: r.event ?? undefined, hops: r.hops, parent: r.parent ?? undefined, depth: r.depth, reply: parse(r.reply), meta: parse(r.meta) ?? {},
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
package/src/timers.d.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { type PineConfig, type ScheduleConfig } from "./config.ts";
|
|
2
|
+
import type { Router } from "./router.ts";
|
|
3
|
+
import type { Bus, Kv } from "./store.ts";
|
|
4
|
+
import { type Logger } from "./util.ts";
|
|
5
|
+
/** Five fields: minute hour day-of-month month day-of-week (0 or 7 is Sunday). Local time. */
|
|
6
|
+
export declare function parseCron(expr: string): (d: Date) => boolean;
|
|
7
|
+
/** A moment as it is written into a file and shown to people: local time, to the minute or second. */
|
|
8
|
+
export declare const localTime: (ms: number) => string;
|
|
9
|
+
/** When a schedule fires, in words. */
|
|
10
|
+
export declare const whenText: (s: Pick<ScheduleConfig, "cron" | "every" | "at">) => string;
|
|
11
|
+
export interface ScheduleInfo {
|
|
12
|
+
name: string;
|
|
13
|
+
/** "global", or the agent it belongs to. */
|
|
14
|
+
scope: string;
|
|
15
|
+
/** The conversation it wakes, for an agent's own. */
|
|
16
|
+
session?: string;
|
|
17
|
+
when: string;
|
|
18
|
+
task?: string;
|
|
19
|
+
/** The next time it fires, if that can be said. */
|
|
20
|
+
next?: number;
|
|
21
|
+
topic: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The scheduler: everything in pine that happens because of the time.
|
|
25
|
+
*
|
|
26
|
+
* A schedule fires at set times (cron), every so often, or once. Firing publishes an event on
|
|
27
|
+
* `timer/<name>` (`timer/<name>@<agent>` for an agent's own). A schedule with a task also wakes
|
|
28
|
+
* whoever it is for with that task: one conversation of an agent, an agent's main conversation,
|
|
29
|
+
* or every agent. Schedules are made by the owner, in the files or by asking an agent, so what
|
|
30
|
+
* they start counts as the owner's wish.
|
|
31
|
+
*/
|
|
32
|
+
export declare class Scheduler {
|
|
33
|
+
private cfg;
|
|
34
|
+
private bus;
|
|
35
|
+
private kv;
|
|
36
|
+
private router?;
|
|
37
|
+
private log;
|
|
38
|
+
private handles;
|
|
39
|
+
private routes;
|
|
40
|
+
private crons;
|
|
41
|
+
constructor(cfg: PineConfig, bus: Bus, kv: Kv, log: Logger, router?: Router);
|
|
42
|
+
/** Every schedule with its key: those for all agents by name, an agent's own as name@agent. */
|
|
43
|
+
private all;
|
|
44
|
+
private match;
|
|
45
|
+
start(): void;
|
|
46
|
+
stop(): void;
|
|
47
|
+
/** A schedule with a task wakes whoever it is for: the routes that do so, rebuilt whenever a schedule or the list of agents changes. */
|
|
48
|
+
private wire;
|
|
49
|
+
/** Set the clocks: one look every quarter minute for the cron times, an interval for each "every", a timeout for each "at". */
|
|
50
|
+
private arm;
|
|
51
|
+
private fire;
|
|
52
|
+
/** The next time a schedule fires. For an interval, counted from now. */
|
|
53
|
+
private next;
|
|
54
|
+
private info;
|
|
55
|
+
list(): ScheduleInfo[];
|
|
56
|
+
/** What one agent has: its own schedules, in whichever conversation, and those for all agents that carry a task. */
|
|
57
|
+
available(agentId: string): ScheduleInfo[];
|
|
58
|
+
/** Check a schedule that is to be added, before anybody is asked: its name, and that its times can be read. Returns it as it goes into the file. */
|
|
59
|
+
check(raw: {
|
|
60
|
+
name: string;
|
|
61
|
+
task: string;
|
|
62
|
+
cron?: string;
|
|
63
|
+
every?: string;
|
|
64
|
+
at?: string | number;
|
|
65
|
+
session?: string;
|
|
66
|
+
}): Record<string, unknown>;
|
|
67
|
+
private file;
|
|
68
|
+
/**
|
|
69
|
+
* Add a schedule while pine runs: to one agent's agent.json, or to pine.json for all agents.
|
|
70
|
+
* The file is written and read back through the normal loader, so it is checked like one that
|
|
71
|
+
* was there at the start; if it does not pass, the file is put back as it was.
|
|
72
|
+
*/
|
|
73
|
+
add(agentId: string | undefined, entry: Record<string, unknown>): ScheduleInfo;
|
|
74
|
+
/** Take a schedule out of the configuration, leaving the file as it was before it was added. */
|
|
75
|
+
private drop;
|
|
76
|
+
remove(agentId: string | undefined, name: string): boolean;
|
|
77
|
+
/** A conversation was removed: what was scheduled for it alone goes with it. */
|
|
78
|
+
closeSession(agentId: string, session: string): string[];
|
|
79
|
+
/** A conversation got another name: its schedules follow it. */
|
|
80
|
+
renameSession(agentId: string, from: string, to: string): void;
|
|
81
|
+
/** Agents were added or removed: whom a schedule for all agents wakes has changed. */
|
|
82
|
+
agentsChanged(): void;
|
|
83
|
+
}
|