@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
@@ -0,0 +1,178 @@
1
+ import type { PineEvent, Task, TaskResult, TranscriptEntry } from "../types.ts";
2
+ /** One conversation of an agent. `expires`: when it is removed by itself unless something more is said in it. */
3
+ export interface Conversation {
4
+ session: string;
5
+ tasks: number;
6
+ last: number;
7
+ pinned: boolean;
8
+ open: boolean;
9
+ expires?: number;
10
+ /** The folder opened for it, if any. */
11
+ folder?: string;
12
+ }
13
+ export interface AgentInfo {
14
+ id: string;
15
+ main: boolean;
16
+ description: string;
17
+ workspace: string;
18
+ model: string;
19
+ mode: "manual" | "auto" | "bypass";
20
+ thinking: string;
21
+ delegates: string[];
22
+ }
23
+ /** Talks to a pine daemon over its control API. */
24
+ export declare class Client {
25
+ base: string;
26
+ private token;
27
+ constructor(base: string, token: string);
28
+ private call;
29
+ /** A picture from the daemon, or undefined when there is none (or it is not an image). */
30
+ private picture;
31
+ toolImage: (id: string) => Promise<{
32
+ mime: string;
33
+ bytes: Buffer;
34
+ } | undefined>;
35
+ taskImage: (task: number, n: number) => Promise<{
36
+ mime: string;
37
+ bytes: Buffer;
38
+ } | undefined>;
39
+ workspaceImage: (agent: string, file: string, session?: string) => Promise<{
40
+ mime: string;
41
+ bytes: Buffer;
42
+ } | undefined>;
43
+ fetchImage: (url: string) => Promise<{
44
+ mime: string;
45
+ bytes: Buffer;
46
+ } | undefined>;
47
+ sources: () => Promise<any[]>;
48
+ subscribeTo: (agent: string, source: string, details?: Record<string, unknown>) => Promise<any>;
49
+ unsubscribeFrom: (agent: string, source: string, session?: string) => Promise<{
50
+ unsubscribed: boolean;
51
+ sessions: string[];
52
+ }>;
53
+ subscribeAll: (source: string, details?: Record<string, unknown>) => Promise<any>;
54
+ unsubscribeAll: (source: string) => Promise<{
55
+ unsubscribed: boolean;
56
+ }>;
57
+ /** Tell the daemon a conversation was closed here; returns the sources it was subscribed to on its own. */
58
+ closeSession: (agent: string, session: string) => Promise<{
59
+ unsubscribed: string[];
60
+ }>;
61
+ get: <T>(url: string) => Promise<T>;
62
+ post: <T>(url: string, body?: unknown) => Promise<T>;
63
+ agents: () => Promise<AgentInfo[]>;
64
+ status: () => Promise<any>;
65
+ models: () => Promise<string[]>;
66
+ sessions: (agent: string) => Promise<Conversation[]>;
67
+ removeSession: (agent: string, session: string) => Promise<{
68
+ removed: boolean;
69
+ unsubscribed: string[];
70
+ sources: string[];
71
+ servers: string[];
72
+ }>;
73
+ renameSession: (agent: string, session: string, to: string) => Promise<{
74
+ session: string;
75
+ }>;
76
+ pinSession: (agent: string, session: string, pinned: boolean) => Promise<{
77
+ pinned: boolean;
78
+ }>;
79
+ folder: (agent: string, session: string) => Promise<{
80
+ folder?: string;
81
+ workspace: string;
82
+ }>;
83
+ folders: (agent: string) => Promise<{
84
+ session: string;
85
+ folder: string;
86
+ }[]>;
87
+ setFolder: (agent: string, session: string, folder: string | null) => Promise<{
88
+ folder?: string;
89
+ }>;
90
+ changes: (agent: string, session: string) => Promise<{
91
+ root: string;
92
+ repo: boolean;
93
+ branch?: string;
94
+ files: Array<{
95
+ path: string;
96
+ status: string;
97
+ added?: number;
98
+ removed?: number;
99
+ }>;
100
+ more: boolean;
101
+ }>;
102
+ diff: (agent: string, session: string, file: string) => Promise<{
103
+ diff: string;
104
+ truncated: boolean;
105
+ }>;
106
+ tree: (agent: string, session: string, dir: string) => Promise<{
107
+ entries: Array<{
108
+ name: string;
109
+ dir: boolean;
110
+ size: number;
111
+ }>;
112
+ more: boolean;
113
+ }>;
114
+ file: (agent: string, session: string, file: string) => Promise<{
115
+ text?: string;
116
+ size: number;
117
+ binary: boolean;
118
+ truncated: boolean;
119
+ }>;
120
+ output: (task: number) => Promise<{
121
+ task: Task;
122
+ entries: TranscriptEntry[];
123
+ }>;
124
+ tabs: () => Promise<{
125
+ tabs: Array<{
126
+ agent: string;
127
+ session: string;
128
+ }>;
129
+ active: number;
130
+ code?: boolean;
131
+ }>;
132
+ saveTabs: (tabs: Array<{
133
+ agent: string;
134
+ session: string;
135
+ }>, active: number, code?: boolean) => Promise<{
136
+ ok: boolean;
137
+ }>;
138
+ history: (agent: string, session: string, limit?: number) => Promise<Task[]>;
139
+ prompt: (agent: string, prompt: string, session: string, attachments?: Array<{
140
+ name: string;
141
+ data: string;
142
+ }>) => Promise<{
143
+ task: number;
144
+ }>;
145
+ ask: (agent: string, prompt: string, session: string) => Promise<TaskResult>;
146
+ transcript: (task: number) => Promise<{
147
+ task: Task;
148
+ entries: TranscriptEntry[];
149
+ }>;
150
+ cancel: (task: number) => Promise<{
151
+ cancelled: boolean;
152
+ }>;
153
+ reset: (agent: string, session: string) => Promise<{
154
+ reset: boolean;
155
+ }>;
156
+ setThinking: (agent: string, level: string) => Promise<{
157
+ level: string;
158
+ }>;
159
+ setMode: (agent: string, mode: string, anyway?: string) => Promise<{
160
+ mode: string;
161
+ }>;
162
+ setModel: (agent: string, model: string) => Promise<{
163
+ model: string;
164
+ }>;
165
+ approvals: () => Promise<any[]>;
166
+ decide: (id: string, allow: boolean, always?: boolean, values?: Record<string, string>) => Promise<{
167
+ decided: boolean;
168
+ }>;
169
+ emit: (topic: string, payload: unknown) => Promise<{
170
+ id: number;
171
+ }>;
172
+ state: (pattern?: string) => Promise<{
173
+ key: string;
174
+ value: unknown;
175
+ }[]>;
176
+ /** Live events. Reconnects until the returned function is called. */
177
+ subscribe(onEvent: (e: PineEvent) => void, onState?: (connected: boolean) => void): () => void;
178
+ }
@@ -0,0 +1,116 @@
1
+ /** Talks to a pine daemon over its control API. */
2
+ export class Client {
3
+ base;
4
+ token;
5
+ constructor(base, token) {
6
+ this.base = base.replace(/\/$/, "");
7
+ this.token = token;
8
+ }
9
+ async call(method, url, body) {
10
+ const res = await fetch(this.base + url, {
11
+ method,
12
+ headers: { authorization: `Bearer ${this.token}`, ...(body !== undefined ? { "content-type": "application/json" } : {}) },
13
+ body: body !== undefined ? JSON.stringify(body) : undefined,
14
+ });
15
+ const json = await res.json().catch(() => ({}));
16
+ if (!res.ok)
17
+ throw new Error(json.error ?? `HTTP ${res.status}`);
18
+ return json;
19
+ }
20
+ /** A picture from the daemon, or undefined when there is none (or it is not an image). */
21
+ async picture(method, url, body) {
22
+ const res = await fetch(this.base + url, {
23
+ method,
24
+ headers: { authorization: `Bearer ${this.token}`, ...(body !== undefined ? { "content-type": "application/json" } : {}) },
25
+ body: body !== undefined ? JSON.stringify(body) : undefined,
26
+ });
27
+ if (res.status === 404)
28
+ return undefined;
29
+ if (!res.ok)
30
+ throw new Error((await res.json().catch(() => ({}))).error ?? `HTTP ${res.status}`);
31
+ return { mime: res.headers.get("content-type") ?? "", bytes: Buffer.from(await res.arrayBuffer()) };
32
+ }
33
+ toolImage = (id) => this.picture("GET", `/media/tool/${encodeURIComponent(id)}`);
34
+ taskImage = (task, n) => this.picture("GET", `/tasks/${task}/images/${n}`);
35
+ workspaceImage = (agent, file, session) => this.picture("GET", `/agents/${encodeURIComponent(agent)}/image?path=${encodeURIComponent(file)}${session ? `&session=${encodeURIComponent(session)}` : ""}`);
36
+ fetchImage = (url) => this.picture("POST", "/images/fetch", { url });
37
+ sources = () => this.get("/sources");
38
+ subscribeTo = (agent, source, details = {}) => this.post(`/agents/${agent}/subscribe`, { source, ...details });
39
+ unsubscribeFrom = (agent, source, session) => this.post(`/agents/${agent}/unsubscribe`, { source, session });
40
+ subscribeAll = (source, details = {}) => this.post("/subscribe", { source, ...details });
41
+ unsubscribeAll = (source) => this.post("/unsubscribe", { source });
42
+ /** Tell the daemon a conversation was closed here; returns the sources it was subscribed to on its own. */
43
+ closeSession = (agent, session) => this.post(`/agents/${agent}/close`, { session });
44
+ get = (url) => this.call("GET", url);
45
+ post = (url, body = {}) => this.call("POST", url, body);
46
+ agents = () => this.get("/agents");
47
+ status = () => this.get("/status");
48
+ models = () => this.get("/models");
49
+ sessions = (agent) => this.get(`/agents/${agent}/sessions`);
50
+ removeSession = (agent, session) => this.post(`/agents/${agent}/remove`, { session });
51
+ renameSession = (agent, session, to) => this.post(`/agents/${agent}/rename`, { session, to });
52
+ pinSession = (agent, session, pinned) => this.post(`/agents/${agent}/pin`, { session, pinned });
53
+ folder = (agent, session) => this.get(`/agents/${agent}/folder?session=${encodeURIComponent(session)}`);
54
+ folders = (agent) => this.get(`/agents/${agent}/folders`);
55
+ setFolder = (agent, session, folder) => this.post(`/agents/${agent}/folder`, { session, folder });
56
+ changes = (agent, session) => this.get(`/agents/${agent}/view/changes?session=${encodeURIComponent(session)}`);
57
+ diff = (agent, session, file) => this.get(`/agents/${agent}/view/diff?session=${encodeURIComponent(session)}&path=${encodeURIComponent(file)}`);
58
+ tree = (agent, session, dir) => this.get(`/agents/${agent}/view/tree?session=${encodeURIComponent(session)}&path=${encodeURIComponent(dir)}`);
59
+ file = (agent, session, file) => this.get(`/agents/${agent}/view/file?session=${encodeURIComponent(session)}&path=${encodeURIComponent(file)}`);
60
+ output = (task) => this.get(`/tasks/${task}/transcript?full=1`);
61
+ tabs = () => this.get("/ui/tabs");
62
+ saveTabs = (tabs, active, code = false) => this.post("/ui/tabs", { tabs, active, code });
63
+ history = (agent, session, limit = 30) => this.get(`/tasks?agent=${encodeURIComponent(agent)}&session=${encodeURIComponent(session)}&limit=${limit}`);
64
+ prompt = (agent, prompt, session, attachments) => this.post(`/agents/${agent}/prompt`, { prompt, session, wait: false, attachments });
65
+ ask = (agent, prompt, session) => this.post(`/agents/${agent}/prompt`, { prompt, session });
66
+ transcript = (task) => this.get(`/tasks/${task}/transcript`);
67
+ cancel = (task) => this.post(`/tasks/${task}/cancel`);
68
+ reset = (agent, session) => this.post(`/agents/${agent}/reset`, { session });
69
+ setThinking = (agent, level) => this.post(`/agents/${agent}/thinking`, { level });
70
+ setMode = (agent, mode, anyway) => this.post(`/agents/${agent}/mode`, { mode, session: anyway, anyway: anyway !== undefined });
71
+ setModel = (agent, model) => this.post(`/agents/${agent}/model`, { model });
72
+ approvals = () => this.get("/approvals");
73
+ decide = (id, allow, always = false, values) => this.post(`/approvals/${id}`, { allow, always, values });
74
+ emit = (topic, payload) => this.post("/events", { topic, payload });
75
+ state = (pattern = "#") => this.get(`/state?key=${encodeURIComponent(pattern)}`);
76
+ /** Live events. Reconnects until the returned function is called. */
77
+ subscribe(onEvent, onState = () => { }) {
78
+ const abort = new AbortController();
79
+ const loop = async () => {
80
+ while (!abort.signal.aborted) {
81
+ try {
82
+ const res = await fetch(`${this.base}/events/stream?topic=%23`, { headers: { authorization: `Bearer ${this.token}` }, signal: abort.signal });
83
+ if (!res.ok || !res.body)
84
+ throw new Error(`HTTP ${res.status}`);
85
+ onState(true);
86
+ let buf = "";
87
+ const decoder = new TextDecoder();
88
+ for await (const chunk of res.body) {
89
+ buf += decoder.decode(chunk, { stream: true });
90
+ for (let i = buf.indexOf("\n\n"); i >= 0; i = buf.indexOf("\n\n")) {
91
+ const block = buf.slice(0, i);
92
+ buf = buf.slice(i + 2);
93
+ if (!block.startsWith("data: "))
94
+ continue;
95
+ try {
96
+ onEvent(JSON.parse(block.slice(6)));
97
+ }
98
+ catch {
99
+ // A malformed frame is skipped; the stream goes on.
100
+ }
101
+ }
102
+ }
103
+ }
104
+ catch {
105
+ // fall through to the retry
106
+ }
107
+ if (abort.signal.aborted)
108
+ return;
109
+ onState(false);
110
+ await new Promise((r) => setTimeout(r, 1000));
111
+ }
112
+ };
113
+ void loop();
114
+ return () => abort.abort();
115
+ }
116
+ }
@@ -0,0 +1,51 @@
1
+ import { VStack } from "@earendil-works/pi-tui";
2
+ import type { Client } from "./client.ts";
3
+ export type PaneView = "changes" | "files" | "output";
4
+ export declare const PANE_VIEWS: PaneView[];
5
+ /** The conversation a pane shows. */
6
+ export interface PaneTarget {
7
+ agent: string;
8
+ session: string;
9
+ }
10
+ /** The lines of a diff as git prints it, in colours, without the lines that only name the file. */
11
+ export declare function diffLines(diff: string): string[];
12
+ export declare class CodePane {
13
+ /** What goes into the layout. */
14
+ readonly component: VStack;
15
+ private client;
16
+ private redraw;
17
+ private close;
18
+ private target;
19
+ private states;
20
+ private rows;
21
+ private scroll;
22
+ private hits;
23
+ private timer?;
24
+ private width;
25
+ /** Counts loads, so that an answer that arrives late does not overwrite a newer one. */
26
+ private turn;
27
+ constructor(opts: {
28
+ client: Client;
29
+ redraw: () => void;
30
+ close: () => void;
31
+ target: () => PaneTarget | undefined;
32
+ });
33
+ private get state();
34
+ get view(): PaneView;
35
+ /** Show one of the views, from its top level. Without a view: whatever was shown last, read again. */
36
+ show(view?: PaneView): void;
37
+ /** The conversation shown changed its folder: what was read from the old one is not shown any more. */
38
+ forget(target: PaneTarget): void;
39
+ /** Something happened that may have changed what is shown (a tool ran, a task ended): read it again in a moment. */
40
+ touch(): void;
41
+ stop(): void;
42
+ private load;
43
+ private fileRows;
44
+ private go;
45
+ private back;
46
+ private build;
47
+ private changesRows;
48
+ private filesRows;
49
+ private outputRows;
50
+ private renderHead;
51
+ }