@pmndrs/pine 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export interface Change {
|
|
2
|
+
/** Path from the top of the repository. */
|
|
3
|
+
path: string;
|
|
4
|
+
/** "M" changed, "A" added, "D" deleted, "R" renamed, "?" not tracked yet. */
|
|
5
|
+
status: string;
|
|
6
|
+
added?: number;
|
|
7
|
+
removed?: number;
|
|
8
|
+
}
|
|
9
|
+
/** What differs from the last commit in a folder: changed, added, deleted and new files, with line counts. */
|
|
10
|
+
export declare function changes(root: string): Promise<{
|
|
11
|
+
repo: boolean;
|
|
12
|
+
branch?: string;
|
|
13
|
+
top?: string;
|
|
14
|
+
files: Change[];
|
|
15
|
+
more: boolean;
|
|
16
|
+
}>;
|
|
17
|
+
/** The diff of one file against the last commit. A file git does not know yet is shown as all new. */
|
|
18
|
+
export declare function diffOf(root: string, file: string): Promise<{
|
|
19
|
+
diff: string;
|
|
20
|
+
truncated: boolean;
|
|
21
|
+
} | undefined>;
|
|
22
|
+
export interface Entry {
|
|
23
|
+
name: string;
|
|
24
|
+
dir: boolean;
|
|
25
|
+
size: number;
|
|
26
|
+
}
|
|
27
|
+
/** What is in one folder: folders first, then files, by name. Git's own folder is left out. */
|
|
28
|
+
export declare function listDir(root: string, rel: string): {
|
|
29
|
+
entries: Entry[];
|
|
30
|
+
more: boolean;
|
|
31
|
+
} | undefined;
|
|
32
|
+
/** A file's text, for reading. Binary files and very large ones are only described. */
|
|
33
|
+
export declare function readText(root: string, rel: string): {
|
|
34
|
+
text?: string;
|
|
35
|
+
size: number;
|
|
36
|
+
binary: boolean;
|
|
37
|
+
truncated: boolean;
|
|
38
|
+
} | undefined;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { isInside, realish, sandboxAvailable } from "./security.js";
|
|
5
|
+
const MAX_OUT = 2_000_000;
|
|
6
|
+
/**
|
|
7
|
+
* Run git so that it can only look. A repository's own configuration can name programs for git
|
|
8
|
+
* to run (filters, diff drivers, hooks, a file system monitor); a folder somebody cloned must not
|
|
9
|
+
* get to run anything with the daemon's rights just because it was looked at. So git runs in a
|
|
10
|
+
* sandbox that can neither write nor use the network, and with the programs it would start for
|
|
11
|
+
* a diff turned off.
|
|
12
|
+
*/
|
|
13
|
+
function git(cwd, args) {
|
|
14
|
+
if (!sandboxAvailable())
|
|
15
|
+
return Promise.resolve({ code: 127, out: "" });
|
|
16
|
+
const profile = `(version 1)(allow default)(deny network*)(deny file-write*)(allow file-write* (literal "/dev/null"))`;
|
|
17
|
+
const safe = ["--no-optional-locks", "-c", "core.fsmonitor=false", "-c", "core.hooksPath=/dev/null", "-c", "core.pager=cat", "-c", "diff.external=", "-c", "safe.directory=*", "-c", "color.ui=false", "-c", "core.quotePath=false"];
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const child = spawn("/usr/bin/sandbox-exec", ["-p", profile, "/usr/bin/git", "-C", cwd, ...safe, ...args], { stdio: ["ignore", "pipe", "ignore"], env: { PATH: "/usr/bin:/bin", HOME: process.env.HOME ?? "", GIT_TERMINAL_PROMPT: "0", LC_ALL: "C" } });
|
|
20
|
+
const chunks = [];
|
|
21
|
+
let size = 0;
|
|
22
|
+
child.stdout.on("data", (d) => {
|
|
23
|
+
size += d.length;
|
|
24
|
+
if (size <= MAX_OUT)
|
|
25
|
+
chunks.push(d);
|
|
26
|
+
else
|
|
27
|
+
child.kill("SIGKILL");
|
|
28
|
+
});
|
|
29
|
+
const timer = setTimeout(() => child.kill("SIGKILL"), 15_000);
|
|
30
|
+
child.on("error", () => (clearTimeout(timer), resolve({ code: 127, out: "" })));
|
|
31
|
+
child.on("close", (code) => (clearTimeout(timer), resolve({ code: code ?? 1, out: Buffer.concat(chunks).toString("utf8") })));
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
/** The repository a folder lies in, if any: its top folder and the branch that is checked out. */
|
|
35
|
+
async function repository(root) {
|
|
36
|
+
const top = await git(root, ["rev-parse", "--show-toplevel"]);
|
|
37
|
+
if (top.code !== 0 || !top.out.trim())
|
|
38
|
+
return undefined;
|
|
39
|
+
const branch = await git(root, ["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
40
|
+
return { top: realish(top.out.trim()), branch: branch.code === 0 ? branch.out.trim() : "" };
|
|
41
|
+
}
|
|
42
|
+
/** What differs from the last commit in a folder: changed, added, deleted and new files, with line counts. */
|
|
43
|
+
export async function changes(root) {
|
|
44
|
+
const repo = await repository(root);
|
|
45
|
+
if (!repo)
|
|
46
|
+
return { repo: false, files: [], more: false };
|
|
47
|
+
const status = await git(repo.top, ["status", "--porcelain=v1", "-z", "--", root]);
|
|
48
|
+
const files = [];
|
|
49
|
+
const parts = status.out.split("\0");
|
|
50
|
+
for (let i = 0; i < parts.length; i++) {
|
|
51
|
+
const entry = parts[i];
|
|
52
|
+
if (entry.length < 4)
|
|
53
|
+
continue;
|
|
54
|
+
const xy = entry.slice(0, 2);
|
|
55
|
+
const file = entry.slice(3);
|
|
56
|
+
// A rename is followed by the name it had before.
|
|
57
|
+
if (xy.includes("R") || xy.includes("C"))
|
|
58
|
+
i++;
|
|
59
|
+
const code = xy === "??" ? "?" : xy.includes("D") ? "D" : xy.includes("A") ? "A" : xy.includes("R") ? "R" : "M";
|
|
60
|
+
files.push({ path: file, status: code });
|
|
61
|
+
}
|
|
62
|
+
const hasHead = (await git(repo.top, ["rev-parse", "--verify", "--quiet", "HEAD"])).code === 0;
|
|
63
|
+
const counts = await git(repo.top, ["diff", "--numstat", "-z", "--no-ext-diff", "--no-textconv", ...(hasHead ? ["HEAD"] : []), "--", root]);
|
|
64
|
+
for (const line of counts.out.split("\0")) {
|
|
65
|
+
const m = /^(\d+|-)\t(\d+|-)\t(.+)$/.exec(line);
|
|
66
|
+
const file = m && files.find((f) => f.path === m[3]);
|
|
67
|
+
if (m && file && m[1] !== "-")
|
|
68
|
+
Object.assign(file, { added: Number(m[1]), removed: Number(m[2]) });
|
|
69
|
+
}
|
|
70
|
+
// A new file has no counts from git; its lines are all new.
|
|
71
|
+
for (const f of files) {
|
|
72
|
+
if (f.status !== "?" || f.path.endsWith("/"))
|
|
73
|
+
continue;
|
|
74
|
+
try {
|
|
75
|
+
const full = path.join(repo.top, f.path);
|
|
76
|
+
if (fs.statSync(full).size < 1_000_000)
|
|
77
|
+
f.added = fs.readFileSync(full, "utf8").split("\n").length - 1;
|
|
78
|
+
}
|
|
79
|
+
catch {
|
|
80
|
+
// gone since
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return { repo: true, branch: repo.branch, top: repo.top, files: files.slice(0, 300), more: files.length > 300 };
|
|
84
|
+
}
|
|
85
|
+
/** The diff of one file against the last commit. A file git does not know yet is shown as all new. */
|
|
86
|
+
export async function diffOf(root, file) {
|
|
87
|
+
const repo = await repository(root);
|
|
88
|
+
if (!repo || file.includes("\0"))
|
|
89
|
+
return undefined;
|
|
90
|
+
const target = realish(path.join(repo.top, file));
|
|
91
|
+
if (!isInside(realish(root), target) && target !== realish(root))
|
|
92
|
+
return undefined;
|
|
93
|
+
const hasHead = (await git(repo.top, ["rev-parse", "--verify", "--quiet", "HEAD"])).code === 0;
|
|
94
|
+
let r = await git(repo.top, ["diff", "--no-ext-diff", "--no-textconv", ...(hasHead ? ["HEAD"] : []), "--", file]);
|
|
95
|
+
if (!r.out.trim())
|
|
96
|
+
r = await git(repo.top, ["diff", "--no-ext-diff", "--no-textconv", "--no-index", "--", "/dev/null", file]);
|
|
97
|
+
const max = 200_000;
|
|
98
|
+
return { diff: r.out.slice(0, max), truncated: r.out.length > max };
|
|
99
|
+
}
|
|
100
|
+
/** A path inside a folder, with links resolved, or undefined when it leads out of it. */
|
|
101
|
+
function inside(root, rel) {
|
|
102
|
+
if (rel.includes("\0"))
|
|
103
|
+
return undefined;
|
|
104
|
+
const base = realish(root);
|
|
105
|
+
const target = realish(path.join(base, rel));
|
|
106
|
+
return target === base || isInside(base, target) ? target : undefined;
|
|
107
|
+
}
|
|
108
|
+
/** What is in one folder: folders first, then files, by name. Git's own folder is left out. */
|
|
109
|
+
export function listDir(root, rel) {
|
|
110
|
+
const dir = inside(root, rel);
|
|
111
|
+
if (!dir)
|
|
112
|
+
return undefined;
|
|
113
|
+
let names;
|
|
114
|
+
try {
|
|
115
|
+
names = fs.readdirSync(dir, { withFileTypes: true });
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
const entries = [];
|
|
121
|
+
for (const d of names) {
|
|
122
|
+
if (d.name === ".git" || d.name === ".DS_Store")
|
|
123
|
+
continue;
|
|
124
|
+
const full = path.join(dir, d.name);
|
|
125
|
+
try {
|
|
126
|
+
const stat = fs.statSync(full);
|
|
127
|
+
// A link that leads out of the folder is not shown as part of it.
|
|
128
|
+
if (!inside(root, path.relative(realish(root), full)))
|
|
129
|
+
continue;
|
|
130
|
+
entries.push({ name: d.name, dir: stat.isDirectory(), size: stat.isDirectory() ? 0 : stat.size });
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
// a broken link
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
entries.sort((a, b) => Number(b.dir) - Number(a.dir) || a.name.localeCompare(b.name));
|
|
137
|
+
return { entries: entries.slice(0, 500), more: entries.length > 500 };
|
|
138
|
+
}
|
|
139
|
+
/** A file's text, for reading. Binary files and very large ones are only described. */
|
|
140
|
+
export function readText(root, rel) {
|
|
141
|
+
const file = inside(root, rel);
|
|
142
|
+
if (!file)
|
|
143
|
+
return undefined;
|
|
144
|
+
try {
|
|
145
|
+
const stat = fs.statSync(file);
|
|
146
|
+
if (!stat.isFile())
|
|
147
|
+
return undefined;
|
|
148
|
+
const max = 256 * 1024;
|
|
149
|
+
const fd = fs.openSync(file, "r");
|
|
150
|
+
const buf = Buffer.alloc(Math.min(stat.size, max));
|
|
151
|
+
fs.readSync(fd, buf, 0, buf.length, 0);
|
|
152
|
+
fs.closeSync(fd);
|
|
153
|
+
if (buf.subarray(0, 8192).includes(0))
|
|
154
|
+
return { size: stat.size, binary: true, truncated: false };
|
|
155
|
+
return { text: buf.toString("utf8"), size: stat.size, binary: false, truncated: stat.size > max };
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return undefined;
|
|
159
|
+
}
|
|
160
|
+
}
|