@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.
Files changed (117) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +180 -2
  3. package/bin/pine.js +27 -0
  4. package/docs/SKILL.md +189 -0
  5. package/docs/references/agents.md +368 -0
  6. package/docs/references/coding.md +126 -0
  7. package/docs/references/configuration.md +229 -0
  8. package/docs/references/events.md +263 -0
  9. package/docs/references/mcp.md +257 -0
  10. package/docs/references/operations.md +280 -0
  11. package/docs/references/overview.md +186 -0
  12. package/docs/references/security.md +489 -0
  13. package/docs/references/setup.md +181 -0
  14. package/docs/references/sources.md +768 -0
  15. package/docs/references/stack.md +105 -0
  16. package/docs/references/tui.md +348 -0
  17. package/examples/home/.env.example +5 -0
  18. package/examples/home/USER.md +4 -0
  19. package/examples/home/agents/main/agent.json +4 -0
  20. package/examples/home/agents/main/workspace/AGENTS.md +14 -0
  21. package/examples/home/agents/main/workspace/MEMORY.md +4 -0
  22. package/examples/home/agents/main/workspace/SOUL.md +4 -0
  23. package/examples/home/models.json +62 -0
  24. package/examples/home/pine.json +30 -0
  25. package/examples/home/skills/README.txt +7 -0
  26. package/examples/launchd/com.pine.daemon.plist +32 -0
  27. package/mcp/browser.d.ts +34 -0
  28. package/mcp/browser.js +228 -0
  29. package/mcp/commands.d.ts +20 -0
  30. package/mcp/commands.js +88 -0
  31. package/mcp/lib.d.ts +13 -0
  32. package/mcp/lib.js +50 -0
  33. package/mcp/web.d.ts +6 -0
  34. package/mcp/web.js +140 -0
  35. package/package.json +32 -4
  36. package/src/agent-admin.d.ts +24 -0
  37. package/src/agent-admin.js +55 -0
  38. package/src/agents.d.ts +343 -0
  39. package/src/agents.js +1587 -0
  40. package/src/cli.d.ts +2 -0
  41. package/src/cli.js +526 -0
  42. package/src/config.d.ts +351 -0
  43. package/src/config.js +514 -0
  44. package/src/dev/claude-model.d.ts +28 -0
  45. package/src/dev/claude-model.js +174 -0
  46. package/src/env-file.d.ts +4 -0
  47. package/src/env-file.js +40 -0
  48. package/src/http.d.ts +46 -0
  49. package/src/http.js +407 -0
  50. package/src/limiter.d.ts +32 -0
  51. package/src/limiter.js +84 -0
  52. package/src/login.d.ts +21 -0
  53. package/src/login.js +126 -0
  54. package/src/manual.d.ts +61 -0
  55. package/src/manual.js +425 -0
  56. package/src/mcp-config.d.ts +40 -0
  57. package/src/mcp-config.js +132 -0
  58. package/src/net.d.ts +36 -0
  59. package/src/net.js +122 -0
  60. package/src/pine.d.ts +32 -0
  61. package/src/pine.js +142 -0
  62. package/src/router.d.ts +56 -0
  63. package/src/router.js +164 -0
  64. package/src/sandbox-fs.d.ts +27 -0
  65. package/src/sandbox-fs.js +172 -0
  66. package/src/security.d.ts +121 -0
  67. package/src/security.js +363 -0
  68. package/src/skills.d.ts +67 -0
  69. package/src/skills.js +163 -0
  70. package/src/source-types.d.ts +61 -0
  71. package/src/source-types.js +1 -0
  72. package/src/sources/command.d.ts +4 -0
  73. package/src/sources/command.js +77 -0
  74. package/src/sources/folder.d.ts +6 -0
  75. package/src/sources/folder.js +116 -0
  76. package/src/sources/ftp.d.ts +4 -0
  77. package/src/sources/ftp.js +307 -0
  78. package/src/sources/http.d.ts +4 -0
  79. package/src/sources/http.js +201 -0
  80. package/src/sources/mqtt.d.ts +18 -0
  81. package/src/sources/mqtt.js +97 -0
  82. package/src/sources/telegram.d.ts +22 -0
  83. package/src/sources/telegram.js +269 -0
  84. package/src/sources/webhook.d.ts +4 -0
  85. package/src/sources/webhook.js +52 -0
  86. package/src/sources.d.ts +339 -0
  87. package/src/sources.js +955 -0
  88. package/src/store.d.ts +89 -0
  89. package/src/store.js +251 -0
  90. package/src/timers.d.ts +83 -0
  91. package/src/timers.js +318 -0
  92. package/src/tools.d.ts +6 -0
  93. package/src/tools.js +777 -0
  94. package/src/tui/app.d.ts +209 -0
  95. package/src/tui/app.js +2223 -0
  96. package/src/tui/client.d.ts +178 -0
  97. package/src/tui/client.js +116 -0
  98. package/src/tui/code-pane.d.ts +51 -0
  99. package/src/tui/code-pane.js +398 -0
  100. package/src/tui/highlight.d.ts +8 -0
  101. package/src/tui/highlight.js +86 -0
  102. package/src/tui/picture.d.ts +141 -0
  103. package/src/tui/picture.js +571 -0
  104. package/src/tui/run.d.ts +8 -0
  105. package/src/tui/run.js +97 -0
  106. package/src/tui/theme.d.ts +32 -0
  107. package/src/tui/theme.js +128 -0
  108. package/src/types.d.ts +124 -0
  109. package/src/types.js +1 -0
  110. package/src/update.d.ts +25 -0
  111. package/src/update.js +110 -0
  112. package/src/util.d.ts +32 -0
  113. package/src/util.js +116 -0
  114. package/src/watching.d.ts +20 -0
  115. package/src/watching.js +68 -0
  116. package/src/workspace-view.d.ts +38 -0
  117. package/src/workspace-view.js +160 -0
package/src/config.js ADDED
@@ -0,0 +1,514 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { parseEnv } from "node:util";
4
+ import { expandHome, parseDuration, resolvePath } from "./util.js";
5
+ export const SOURCE_TYPES = ["folder", "mqtt", "webhook", "ftp", "telegram", "http", "command"];
6
+ export const MODES = ["manual", "auto", "bypass"];
7
+ export class ConfigError extends Error {
8
+ problems;
9
+ constructor(problems) {
10
+ super(`Invalid pine configuration:\n${problems.map((p) => ` - ${p}`).join("\n")}`);
11
+ this.problems = problems;
12
+ }
13
+ }
14
+ /** The pine checkout: bundled MCP servers and source types live here. */
15
+ export const PINE_ROOT = path.resolve(import.meta.dirname, "..");
16
+ /**
17
+ * How pine's own module files end: ".ts" in a checkout, where Node runs the sources as they are,
18
+ * ".js" in the published package, which holds them compiled (Node does not run TypeScript from
19
+ * inside node_modules). Used wherever pine names one of its own files at run time.
20
+ */
21
+ export const EXT = import.meta.filename.endsWith(".ts") ? ".ts" : ".js";
22
+ export const defaultHome = () => path.resolve(expandHome(process.env.PINE_HOME ?? "~/.pine"));
23
+ const readJson = (file, problems) => {
24
+ try {
25
+ return JSON.parse(fs.readFileSync(file, "utf8"));
26
+ }
27
+ catch (e) {
28
+ problems.push(`${file}: ${e.message}`);
29
+ return {};
30
+ }
31
+ };
32
+ /** Replace ${VAR} and ${VAR:-fallback} in every string of a JSON value. */
33
+ function interpolate(value, where, problems, env) {
34
+ if (typeof value === "string") {
35
+ return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_m, name, fallback) => {
36
+ const v = env[name] ?? fallback;
37
+ if (v === undefined)
38
+ problems.push(`${where}: environment variable ${name} is not set`);
39
+ return v ?? "";
40
+ });
41
+ }
42
+ if (Array.isArray(value))
43
+ return value.map((v, i) => interpolate(v, `${where}[${i}]`, problems, env));
44
+ if (value && typeof value === "object")
45
+ return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, interpolate(v, `${where}.${k}`, problems, env)]));
46
+ return value;
47
+ }
48
+ /**
49
+ * pine had plugins once: modules listed under "plugins". What they did is done by event sources
50
+ * now, which agents set up themselves. An empty list in an older file is ignored; one that still
51
+ * names something is a problem to fix, with a pointer to what took its place.
52
+ */
53
+ const noPlugins = (list, where, problems) => {
54
+ if (Array.isArray(list) && list.length)
55
+ problems.push(`${where}: plugins are gone. Telegram, MQTT and watched folders are event sources now, which an agent sets up when you ask it to ("watch telegram", "watch ~/Scans"); see docs/references/sources.md. Take the "plugins" entry out`);
56
+ };
57
+ const strings = (v, fallback) => (Array.isArray(v) ? v.map(String) : fallback);
58
+ const paths = (v, base) => strings(v, []).map((p) => resolvePath(p, base));
59
+ function loadAgent(id, dir, raw, cfg, problems) {
60
+ const where = `agents/${id}/agent.json`;
61
+ noPlugins(raw.plugins, `${where}: plugins`, problems);
62
+ const workspace = raw.workspace ? resolvePath(raw.workspace, dir) : path.join(dir, "workspace");
63
+ if (!raw.workspace)
64
+ fs.mkdirSync(workspace, { recursive: true });
65
+ else if (!fs.existsSync(workspace))
66
+ problems.push(`${where}: workspace ${workspace} does not exist`);
67
+ const sub = raw.subagents === false ? { enabled: false } : raw.subagents === true || raw.subagents === undefined ? {} : raw.subagents;
68
+ const state = raw.state ?? {};
69
+ return {
70
+ id,
71
+ dir,
72
+ description: raw.description ?? "",
73
+ workspace,
74
+ model: raw.model,
75
+ thinking: raw.thinking ?? cfg.defaults.thinking,
76
+ tools: Array.isArray(raw.tools) ? raw.tools.map(String) : undefined,
77
+ mcp: raw.mcp ?? {},
78
+ globalMcp: raw.globalMcp ?? true,
79
+ sources: loadSources(raw.sources, dir, `${where}: sources`, problems, Object.keys(cfg.sourceTypes)),
80
+ subscribe: loadSubscriptions(raw.subscribe, `${where}: subscribe`, problems),
81
+ schedules: loadSchedules(raw.schedules, problems, where, id),
82
+ extensions: paths(raw.extensions, dir),
83
+ skills: paths(raw.skills, dir),
84
+ context: paths(raw.context, dir),
85
+ delegates: strings(raw.delegates, id === cfg.main ? ["*"] : []),
86
+ subagents: { enabled: sub.enabled ?? true, maxParallel: sub.maxParallel ?? 4, tools: sub.tools, mcp: sub.mcp ?? true },
87
+ publish: strings(raw.publish, []),
88
+ state: { read: strings(state.read, ["#"]), write: strings(state.write, [`agent/${id}/#`]), inject: strings(state.inject, []) },
89
+ trustWorkspace: raw.trustWorkspace === true,
90
+ mode: raw.mode ?? cfg.defaults.mode,
91
+ sandbox: {
92
+ enabled: raw.sandbox !== false && raw.sandbox?.enabled !== false,
93
+ network: raw.sandbox?.network === true,
94
+ read: paths(raw.sandbox?.read, dir),
95
+ write: paths(raw.sandbox?.write, dir),
96
+ sharedTmp: raw.sandbox?.sharedTmp !== false,
97
+ env: raw.sandbox?.env ?? {},
98
+ },
99
+ approve: strings(raw.approve, []),
100
+ approveWhenTainted: strings(raw.approveWhenTainted, ["publish_event", "send"]),
101
+ untrusted: raw.untrusted === false
102
+ ? false
103
+ : {
104
+ tools: strings(raw.untrusted?.tools, ["read", "grep", "find", "ls"]),
105
+ mcp: strings(raw.untrusted?.mcp, []),
106
+ delegates: strings(raw.untrusted?.delegates, []),
107
+ publish: strings(raw.untrusted?.publish, []),
108
+ stateWrite: raw.untrusted?.stateWrite === true,
109
+ subagents: raw.untrusted?.subagents === true,
110
+ },
111
+ bashTimeoutSec: raw.bashTimeoutSec ?? cfg.defaults.bashTimeoutSec,
112
+ bashDeny: strings(raw.bashDeny, []),
113
+ taskTimeoutSec: raw.taskTimeoutSec ?? cfg.defaults.taskTimeoutSec,
114
+ idleMs: raw.idleMs !== undefined ? parseDuration(raw.idleMs) : cfg.defaults.idleMs,
115
+ settings: { ...cfg.defaults.settings, ...(raw.settings ?? {}) },
116
+ };
117
+ }
118
+ function loadRoutes(raw, problems) {
119
+ if (raw === undefined)
120
+ return [];
121
+ if (!Array.isArray(raw)) {
122
+ problems.push("pine.json: routes must be an array");
123
+ return [];
124
+ }
125
+ return raw.flatMap((r, i) => {
126
+ const where = `pine.json: routes[${i}]`;
127
+ if (!r || (typeof r.on !== "string" && !Array.isArray(r.on))) {
128
+ problems.push(`${where}: needs "on" (topic pattern)`);
129
+ return [];
130
+ }
131
+ if (!r.agent && !r.call && !r.publish && !r.set)
132
+ problems.push(`${where}: needs one of agent, call, publish, set`);
133
+ if (r.agent && r.call)
134
+ problems.push(`${where}: use either agent or call, not both`);
135
+ if (r.call && typeof r.call.prompt !== "string")
136
+ problems.push(`${where}: call needs "prompt"`);
137
+ if (r.publish && typeof r.publish.topic !== "string")
138
+ problems.push(`${where}: publish needs "topic"`);
139
+ return [
140
+ {
141
+ ...r,
142
+ name: r.name ?? `route${i}`,
143
+ on: Array.isArray(r.on) ? r.on : [r.on],
144
+ debounceMs: r.debounceMs !== undefined ? parseDuration(r.debounceMs) : undefined,
145
+ throttleMs: r.throttleMs !== undefined ? parseDuration(r.throttleMs) : undefined,
146
+ },
147
+ ];
148
+ });
149
+ }
150
+ /** `file`: where the list stands. `agent`: the agent it belongs to, whose schedules need a task and are published under its name. */
151
+ function loadSchedules(raw, problems, file = "pine.json", agent) {
152
+ if (raw === undefined)
153
+ return [];
154
+ if (!Array.isArray(raw)) {
155
+ problems.push(`${file}: schedules must be an array`);
156
+ return [];
157
+ }
158
+ return raw.flatMap((s, i) => {
159
+ const where = `${file}: schedules[${i}]`;
160
+ if (!s?.name) {
161
+ problems.push(`${where}: needs "name"`);
162
+ return [];
163
+ }
164
+ if (!/^[a-z][a-z0-9_-]{0,39}$/.test(s.name))
165
+ problems.push(`${where}: a schedule's name is lowercase letters, digits, - and _`);
166
+ if ([s.cron, s.every, s.at].filter((x) => x !== undefined).length !== 1)
167
+ problems.push(`${where}: needs exactly one of cron, every, at`);
168
+ const task = typeof s.task === "string" && s.task.trim() ? s.task.trim() : undefined;
169
+ if (agent && !task)
170
+ problems.push(`${where}: needs "task": what the agent is to do when it fires`);
171
+ let every;
172
+ let at;
173
+ try {
174
+ every = s.every !== undefined ? parseDuration(s.every) : undefined;
175
+ if (s.at !== undefined) {
176
+ at = typeof s.at === "number" ? s.at : Date.parse(String(s.at));
177
+ if (!Number.isFinite(at))
178
+ throw new Error(`"${s.at}" is not a time. Write it as 2026-12-24T17:00`);
179
+ }
180
+ }
181
+ catch (e) {
182
+ problems.push(`${where}: ${e.message}`);
183
+ }
184
+ const key = agent ? `${s.name}@${agent}` : s.name;
185
+ // A task that was due while pine was not running is still worth doing; an event for a route is not repeated unless asked.
186
+ return [{ name: s.name, cron: s.cron, every, at, task, session: typeof s.session === "string" && s.session ? s.session : undefined, topic: s.topic ?? `timer/${key}`, payload: s.payload, catchUp: s.catchUp === undefined ? Boolean(task) : s.catchUp === true }];
187
+ });
188
+ }
189
+ /** Read `<home>/pine.json`, `<home>/.env` and every `<home>/agents/<id>/agent.json`. Throws ConfigError. */
190
+ export function loadConfig(home = defaultHome()) {
191
+ const problems = [];
192
+ home = path.resolve(expandHome(home));
193
+ const file = path.join(home, "pine.json");
194
+ if (!fs.existsSync(file))
195
+ throw new ConfigError([`${file} not found. Run "pine init ${home}" to create a home directory.`]);
196
+ // Secrets from .env are used to fill in the configuration only. They are not exported to the
197
+ // process environment, so agent shells and MCP servers do not inherit them.
198
+ const envFile = path.join(home, ".env");
199
+ const fileEnv = fs.existsSync(envFile) ? parseEnv(fs.readFileSync(envFile, "utf8")) : {};
200
+ const env = { ...process.env, PINE_HOME: home, PINE_ROOT, ...fileEnv };
201
+ const raw = interpolate(readJson(file, problems), "pine.json", problems, env);
202
+ noPlugins(raw?.plugins, "pine.json: plugins", problems);
203
+ const d = raw.defaults ?? {};
204
+ // Source types of the owner's own: a name and the module behind it.
205
+ const sourceTypes = {};
206
+ for (const [name, file] of Object.entries(raw.sourceTypes && typeof raw.sourceTypes === "object" ? raw.sourceTypes : {})) {
207
+ if (!/^[a-z][a-z0-9_-]{0,39}$/.test(name) || SOURCE_TYPES.includes(name))
208
+ problems.push(`pine.json: sourceTypes.${name}: a type name is lowercase letters, digits, - and _, and not one of the built-in types`);
209
+ else if (typeof file !== "string" || !file)
210
+ problems.push(`pine.json: sourceTypes.${name} must be the path of a module`);
211
+ else
212
+ sourceTypes[name] = resolvePath(file, home);
213
+ }
214
+ const l = raw.limits ?? {};
215
+ const cfg = {
216
+ home,
217
+ dataDir: resolvePath(raw.dataDir ?? "data", home),
218
+ logLevel: raw.logLevel ?? "info",
219
+ main: raw.main,
220
+ defaults: {
221
+ model: d.model,
222
+ thinking: d.thinking ?? "off",
223
+ mode: d.mode ?? "auto",
224
+ bashTimeoutSec: d.bashTimeoutSec ?? 300,
225
+ taskTimeoutSec: d.taskTimeoutSec ?? 1800,
226
+ idleMs: parseDuration(d.idleMs ?? "15m"),
227
+ settings: d.settings ?? {},
228
+ },
229
+ limits: {
230
+ maxConcurrentRuns: l.maxConcurrentRuns ?? 2,
231
+ maxDelegationDepth: l.maxDelegationDepth ?? 3,
232
+ maxDownloadMb: l.maxDownloadMb ?? 200,
233
+ maxEventHops: l.maxEventHops ?? 8,
234
+ modelHeadersTimeoutMs: parseDuration(l.modelHeadersTimeoutMs ?? "10m"),
235
+ modelStreamIdleMs: parseDuration(l.modelStreamIdleMs ?? "5m"),
236
+ eventRetentionDays: l.eventRetentionDays ?? 30,
237
+ conversationDays: l.conversationDays ?? 7,
238
+ maxQueuedPerAgent: l.maxQueuedPerAgent ?? 50,
239
+ maxEventChars: l.maxEventChars ?? 20_000,
240
+ },
241
+ security: {
242
+ approvalTimeoutMs: parseDuration(raw.security?.approvalTimeout ?? "5m"),
243
+ taint: raw.security?.taint ?? "session",
244
+ taintTools: strings(raw.security?.taintTools, []),
245
+ bypassWhenTainted: raw.security?.bypassWhenTainted === true,
246
+ },
247
+ secrets: Object.values(fileEnv).filter((v) => v.length >= 8),
248
+ mcp: raw.mcp ?? {},
249
+ sources: loadSources(raw.sources, home, "pine.json: sources", problems, Object.keys(sourceTypes)),
250
+ sourceTypes,
251
+ subscribe: loadSubscriptions(raw.subscribe, "pine.json: subscribe", problems),
252
+ extensions: paths(raw.extensions, home),
253
+ skills: paths(raw.skills, home),
254
+ routes: loadRoutes(raw.routes, problems),
255
+ schedules: loadSchedules(raw.schedules, problems),
256
+ http: raw.http === false ? undefined : { host: raw.http?.host ?? "127.0.0.1", port: raw.http?.port ?? 7463, token: raw.http?.token, hooks: {} },
257
+ agents: {},
258
+ };
259
+ if (cfg.http) {
260
+ for (const [name, h] of Object.entries(raw.http?.hooks ?? {})) {
261
+ if (!/^[A-Za-z0-9_-]+$/.test(name))
262
+ problems.push(`pine.json: invalid hook name "${name}"`);
263
+ if (!h?.secret && h?.open !== true)
264
+ problems.push(`pine.json: http.hooks.${name} needs a "secret" (or "open": true to accept anyone who can reach the port)`);
265
+ cfg.http.hooks[name] = { topic: h?.topic ?? `hook/${name}`, secret: h?.secret || undefined };
266
+ }
267
+ if (cfg.http.token)
268
+ cfg.secrets.push(cfg.http.token);
269
+ }
270
+ const agentsDir = path.join(home, "agents");
271
+ for (const entry of fs.existsSync(agentsDir) ? fs.readdirSync(agentsDir, { withFileTypes: true }) : []) {
272
+ const dir = path.join(agentsDir, entry.name);
273
+ const agentFile = path.join(dir, "agent.json");
274
+ if (!entry.isDirectory() || !fs.existsSync(agentFile))
275
+ continue;
276
+ if (!/^[a-z0-9][a-z0-9_-]*$/.test(entry.name)) {
277
+ problems.push(`agents/${entry.name}: agent ids are lowercase letters, digits, "-" and "_"`);
278
+ continue;
279
+ }
280
+ const rawAgent = interpolate(readJson(agentFile, problems), `agents/${entry.name}/agent.json`, problems, env);
281
+ cfg.agents[entry.name] = loadAgent(entry.name, dir, rawAgent, cfg, problems);
282
+ }
283
+ const ids = Object.keys(cfg.agents);
284
+ if (ids.length === 0)
285
+ problems.push(`no agents found in ${agentsDir} (each agent is a folder with an agent.json)`);
286
+ if (cfg.main && !cfg.agents[cfg.main])
287
+ problems.push(`pine.json: main agent "${cfg.main}" does not exist`);
288
+ for (const a of Object.values(cfg.agents)) {
289
+ if (!MODES.includes(a.mode))
290
+ problems.push(`agents/${a.id}/agent.json: mode must be one of ${MODES.join(", ")} (got "${a.mode}")`);
291
+ if (!(a.model ?? cfg.defaults.model))
292
+ problems.push(`agents/${a.id}/agent.json: no model (set "model" or defaults.model in pine.json)`);
293
+ for (const t of a.delegates)
294
+ if (t !== "*" && !cfg.agents[t])
295
+ problems.push(`agents/${a.id}/agent.json: delegates names unknown agent "${t}"`);
296
+ if (Array.isArray(a.globalMcp))
297
+ for (const n of a.globalMcp)
298
+ if (!cfg.mcp[n])
299
+ problems.push(`agents/${a.id}/agent.json: globalMcp names unknown server "${n}"`);
300
+ for (const n of Object.keys(a.mcp))
301
+ if (!/^[A-Za-z0-9_-]+$/.test(n))
302
+ problems.push(`agents/${a.id}/agent.json: invalid MCP server name "${n}"`);
303
+ for (const p of [...a.extensions, ...a.skills, ...a.context])
304
+ if (!fs.existsSync(p))
305
+ problems.push(`agents/${a.id}/agent.json: ${p} does not exist`);
306
+ for (const sub of a.subscribe)
307
+ if (!sourceFor(cfg, a.id, sub.source))
308
+ problems.push(`agents/${a.id}/agent.json: subscribe names "${sub.source}", which is not a source this agent can use`);
309
+ }
310
+ for (const sub of cfg.subscribe)
311
+ if (!cfg.sources[sub.source])
312
+ problems.push(`pine.json: subscribe names "${sub.source}", which is not a source in pine.json (a subscription for all agents needs a source all agents can use)`);
313
+ for (const [name, src] of Object.entries(cfg.sources))
314
+ for (const a of src.agents ?? [])
315
+ if (a !== "*" && !cfg.agents[a])
316
+ problems.push(`pine.json: sources.${name}.agents names unknown agent "${a}"`);
317
+ for (const [name, s] of Object.entries(cfg.mcp)) {
318
+ if (!/^[A-Za-z0-9_-]+$/.test(name))
319
+ problems.push(`pine.json: invalid MCP server name "${name}"`);
320
+ for (const a of s.agents ?? [])
321
+ if (a !== "*" && !cfg.agents[a])
322
+ problems.push(`pine.json: mcp.${name}.agents names unknown agent "${a}"`);
323
+ }
324
+ for (const p of [...cfg.extensions, ...cfg.skills])
325
+ if (!fs.existsSync(p))
326
+ problems.push(`pine.json: ${p} does not exist`);
327
+ for (const r of cfg.routes)
328
+ if (r.agent && !cfg.agents[r.agent])
329
+ problems.push(`pine.json: route "${r.name}" names unknown agent "${r.agent}"`);
330
+ for (const [where, servers] of [["pine.json", cfg.mcp], ...Object.values(cfg.agents).map((a) => [`agents/${a.id}/agent.json`, a.mcp])]) {
331
+ for (const [name, s] of Object.entries(servers)) {
332
+ if (s.pine && !BUNDLED[s.pine])
333
+ problems.push(`${where}: mcp.${name}.pine "${s.pine}" is not a bundled server (have: ${Object.keys(BUNDLED).join(", ")})`);
334
+ if (!s.pine && !s.command && !s.url)
335
+ problems.push(`${where}: mcp.${name} needs "command", "url" or "pine"`);
336
+ }
337
+ }
338
+ if (problems.length)
339
+ throw new ConfigError(problems);
340
+ return cfg;
341
+ }
342
+ /** MCP servers an agent ends up with: its own, plus the global ones that include it. Own entries win. */
343
+ /** The main agent: the one pine.json names; without that entry the one called "main", else the first one. */
344
+ /**
345
+ * The files in an agent's workspace that are part of its prompt, in the order they enter it. They
346
+ * are the agent's own: it reads them as its instructions and keeps them up to date itself.
347
+ * AGENTS.md is how it works, SOUL.md who it is, MEMORY.md what should outlive a conversation.
348
+ */
349
+ export const WORKSPACE_FILES = ["AGENTS.md", "SOUL.md", "MEMORY.md"];
350
+ /** What all agents know about the owner: one file in pine's home, in every agent's prompt, kept by the main agent. */
351
+ export const userFile = (cfg) => path.join(cfg.home, "USER.md");
352
+ /**
353
+ * Where skills lie that were set up by tool call. The place says whom a skill is for: all agents,
354
+ * one agent, or one conversation of an agent. All of it is inside pine's home, which agents read
355
+ * but do not write.
356
+ */
357
+ export const globalSkillsDir = (cfg) => path.join(cfg.home, "skills");
358
+ export const agentSkillsDir = (agent) => path.join(agent.dir, "skills");
359
+ export const conversationsDir = (agent) => path.join(agent.dir, "conversations");
360
+ export function mainAgentId(cfg) {
361
+ return cfg.main ?? (cfg.agents.main ? "main" : Object.keys(cfg.agents)[0]);
362
+ }
363
+ /**
364
+ * The MCP servers an agent has. `session`: the conversation asked for, which also gets the servers
365
+ * that were added for it alone; without it, every server the agent has in any conversation.
366
+ */
367
+ export function effectiveMcp(cfg, agent, session) {
368
+ const out = {};
369
+ for (const [name, server] of Object.entries(cfg.mcp)) {
370
+ const scope = server.agents ?? ["*"];
371
+ const listed = scope.includes("*") || scope.includes(agent.id);
372
+ const wanted = agent.globalMcp === true ? listed : Array.isArray(agent.globalMcp) ? agent.globalMcp.includes(name) : false;
373
+ if (wanted && server.enabled !== false)
374
+ out[name] = server;
375
+ }
376
+ for (const [name, server] of Object.entries(agent.mcp))
377
+ if (server.enabled !== false && (!server.session || session === undefined || server.session === session))
378
+ out[name] = server;
379
+ return out;
380
+ }
381
+ const BUNDLED = {
382
+ web: { file: `mcp/web${EXT}`, taints: true },
383
+ browser: { file: `mcp/browser${EXT}`, taints: true },
384
+ commands: { file: `mcp/commands${EXT}`, taints: false },
385
+ };
386
+ /** Turn a pine MCP entry into what pi's registerMcpServer() accepts. */
387
+ export function toPiMcp(name, server, extraOptions = {}) {
388
+ const { agents: _a, pine, options, taints: _t, approve: _ap, session: _s, ...rest } = server;
389
+ if (!pine)
390
+ return { exposure: "direct", ...rest };
391
+ const bundled = BUNDLED[pine];
392
+ if (!bundled)
393
+ throw new Error(`mcp.${name}: unknown bundled server "${pine}" (have: ${Object.keys(BUNDLED).join(", ")})`);
394
+ return {
395
+ exposure: "direct",
396
+ ...rest,
397
+ command: process.execPath,
398
+ args: [path.join(PINE_ROOT, bundled.file)],
399
+ env: { ...(rest.env ?? {}), PINE_MCP_OPTIONS: JSON.stringify({ ...(options ?? {}), ...extraOptions }) },
400
+ };
401
+ }
402
+ const SOURCE_KEYS = ["type", "use", "description", "agents", "trust", "start", "enabled", "session"];
403
+ function loadSources(raw, base, where, problems, own = []) {
404
+ const types = [...SOURCE_TYPES, ...own];
405
+ const out = {};
406
+ if (raw === undefined || raw === null)
407
+ return out;
408
+ if (typeof raw !== "object" || Array.isArray(raw)) {
409
+ problems.push(`${where} must be an object of named sources`);
410
+ return out;
411
+ }
412
+ for (const [name, v] of Object.entries(raw)) {
413
+ if (!/^[a-z][a-z0-9_-]{0,39}$/.test(name))
414
+ problems.push(`${where}.${name}: a source name is lowercase letters, digits, - and _`);
415
+ if (!v || typeof v !== "object") {
416
+ problems.push(`${where}.${name} must be an object`);
417
+ continue;
418
+ }
419
+ if (!v.type === !v.use)
420
+ problems.push(`${where}.${name} needs either "type" (${types.join(", ")}) or "use" (a module of your own)`);
421
+ // Times are not a source any more: a schedule does what a timer source and a subscription did together.
422
+ if (v.type === "timer")
423
+ problems.push(`${where}.${name}: there is no "timer" source any more. Something that happens at a time is a schedule: ask the agent ("every morning at seven, ..."), or write { "name": "${name}", ${v.cron ? `"cron": "${v.cron}"` : `"every": "${v.every ?? "1h"}"`}, "task": "what to do" } under "schedules" and remove this source and its subscriptions`);
424
+ else if (v.type && !types.includes(v.type))
425
+ problems.push(`${where}.${name}: unknown type "${v.type}" (have: ${types.join(", ")})`);
426
+ if (v.trust !== undefined && v.trust !== "owner" && v.trust !== "untrusted")
427
+ problems.push(`${where}.${name}: trust is "owner" or "untrusted"`);
428
+ if (v.start !== undefined && v.start !== "subscribed" && v.start !== "always")
429
+ problems.push(`${where}.${name}: start is "subscribed" or "always"`);
430
+ out[name] = {
431
+ type: v.type,
432
+ use: v.use,
433
+ base,
434
+ description: typeof v.description === "string" ? v.description : undefined,
435
+ agents: Array.isArray(v.agents) ? v.agents.map(String) : undefined,
436
+ session: typeof v.session === "string" && v.session ? v.session : undefined,
437
+ // What a source reports was produced by someone or something else, unless the owner says it is theirs.
438
+ trust: v.trust ?? "untrusted",
439
+ start: v.start ?? "subscribed",
440
+ enabled: v.enabled !== false,
441
+ options: Object.fromEntries(Object.entries(v).filter(([k]) => !SOURCE_KEYS.includes(k))),
442
+ };
443
+ }
444
+ return out;
445
+ }
446
+ /**
447
+ * The forms "subscribe" may have in a file, as a plain list of entries:
448
+ * `["camera", "printer"]`, `{ "camera": { "note": ... } }`, or the form pine itself writes,
449
+ * `[{ "source": "camera", "session": "main", "note": ... }]`, which is the only one that can
450
+ * say that two conversations listen to the same source.
451
+ */
452
+ export function rawSubscriptions(raw) {
453
+ if (Array.isArray(raw))
454
+ return raw.map((item) => (typeof item === "string" ? { source: item } : item && typeof item === "object" ? { ...item } : { source: String(item) }));
455
+ if (raw && typeof raw === "object")
456
+ return Object.entries(raw).map(([name, v]) => ({ ...(v && typeof v === "object" ? v : {}), source: name }));
457
+ return [];
458
+ }
459
+ function loadSubscriptions(raw, where, problems) {
460
+ const out = [];
461
+ if (raw === undefined || raw === null)
462
+ return out;
463
+ if (typeof raw !== "object") {
464
+ problems.push(`${where} must be a list of source names, a list of entries, or an object`);
465
+ return out;
466
+ }
467
+ for (const entry of rawSubscriptions(raw)) {
468
+ try {
469
+ const sub = subscriptionFrom(entry);
470
+ if (out.some((x) => x.source === sub.source && x.session === sub.session))
471
+ problems.push(`${where}: "${sub.source}" is listed twice for the conversation "${sub.session}"`);
472
+ else
473
+ out.push(sub);
474
+ }
475
+ catch (e) {
476
+ problems.push(`${where}.${entry.source ?? "?"}: ${e.message}`);
477
+ }
478
+ }
479
+ return out;
480
+ }
481
+ export function subscriptionFrom(v) {
482
+ if (typeof v.source !== "string" || !v.source)
483
+ throw new Error(`an entry needs "source": the name of the source`);
484
+ if (v.when !== undefined && (typeof v.when !== "object" || v.when === null || Array.isArray(v.when)))
485
+ throw new Error(`"when" must be an object of conditions`);
486
+ const separate = v.separate === true;
487
+ return {
488
+ source: v.source,
489
+ session: typeof v.session === "string" && v.session ? v.session : separate ? `on-${v.source}` : "main",
490
+ note: typeof v.note === "string" && v.note.trim() ? v.note.trim().slice(0, 2000) : undefined,
491
+ when: v.when,
492
+ debounceMs: v.debounce !== undefined ? parseDuration(v.debounce) : undefined,
493
+ throttleMs: parseDuration(v.throttle ?? "10s"),
494
+ throttleDefault: v.throttle === undefined ? true : undefined,
495
+ separate,
496
+ prompt: typeof v.prompt === "string" ? v.prompt : undefined,
497
+ };
498
+ }
499
+ /** The source an agent means by a name: its own first, then a global one it is allowed to use. */
500
+ export function sourceFor(cfg, agentId, name) {
501
+ const agent = cfg.agents[agentId];
502
+ if (agent?.sources[name])
503
+ return { source: agent.sources[name], key: `${name}@${agentId}`, scope: agentId };
504
+ const global = cfg.sources[name];
505
+ if (global && (!global.agents || global.agents.includes("*") || global.agents.includes(agentId)))
506
+ return { source: global, key: name, scope: "global" };
507
+ return undefined;
508
+ }
509
+ export const mcpTaints = (server) => server.taints ?? (server.pine ? (BUNDLED[server.pine]?.taints ?? false) : false);
510
+ /** Agents `from` may delegate to. */
511
+ export function delegateTargets(cfg, from) {
512
+ const all = from.delegates.includes("*");
513
+ return Object.values(cfg.agents).filter((a) => a.id !== from.id && (all || from.delegates.includes(a.id)));
514
+ }
@@ -0,0 +1,28 @@
1
+ /** The model's reply is one JSON object; tolerate a code fence or stray text around it. */
2
+ export declare function parseReply(raw: string): {
3
+ text: string;
4
+ tool_calls: Array<{
5
+ name: string;
6
+ arguments: unknown;
7
+ }>;
8
+ };
9
+ export declare function buildPrompt(body: any): string;
10
+ export interface ClaudeModelOptions {
11
+ port?: number;
12
+ host?: string;
13
+ /** Model alias passed to `claude --model`. Default "haiku". */
14
+ model?: string;
15
+ /** How many `claude` processes may run at once. Default 3. */
16
+ concurrency?: number;
17
+ claudePath?: string;
18
+ log?: (line: string) => void;
19
+ }
20
+ export declare function startClaudeModel(opts?: ClaudeModelOptions): Promise<{
21
+ port: any;
22
+ baseUrl: string;
23
+ stats: {
24
+ requests: number;
25
+ failures: number;
26
+ };
27
+ close: () => Promise<void>;
28
+ }>;