@edeng23/pines 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 +202 -0
- package/README.md +37 -0
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -0
- package/dist/client/ansi.js +37 -0
- package/dist/client/ansi.js.map +1 -0
- package/dist/client/app.js +1868 -0
- package/dist/client/app.js.map +1 -0
- package/dist/client/boot.js +221 -0
- package/dist/client/boot.js.map +1 -0
- package/dist/client/commands.js +133 -0
- package/dist/client/commands.js.map +1 -0
- package/dist/client/daemon-client.js +99 -0
- package/dist/client/daemon-client.js.map +1 -0
- package/dist/client/forest/camera.js +88 -0
- package/dist/client/forest/camera.js.map +1 -0
- package/dist/client/forest/canvas.js +175 -0
- package/dist/client/forest/canvas.js.map +1 -0
- package/dist/client/forest/sidebar.js +182 -0
- package/dist/client/forest/sidebar.js.map +1 -0
- package/dist/client/forest/status.js +39 -0
- package/dist/client/forest/status.js.map +1 -0
- package/dist/client/forest/style.js +70 -0
- package/dist/client/forest/style.js.map +1 -0
- package/dist/client/forest/styles.js +429 -0
- package/dist/client/forest/styles.js.map +1 -0
- package/dist/client/forest/view.js +181 -0
- package/dist/client/forest/view.js.map +1 -0
- package/dist/client/forest/wordmark.js +26 -0
- package/dist/client/forest/wordmark.js.map +1 -0
- package/dist/client/input.js +89 -0
- package/dist/client/input.js.map +1 -0
- package/dist/client/theme.js +27 -0
- package/dist/client/theme.js.map +1 -0
- package/dist/client/tree/asciitree.js +277 -0
- package/dist/client/tree/asciitree.js.map +1 -0
- package/dist/client/tree/convmap.js +260 -0
- package/dist/client/tree/convmap.js.map +1 -0
- package/dist/client/tree/lanegraph.js +260 -0
- package/dist/client/tree/lanegraph.js.map +1 -0
- package/dist/client/tree/merge.js +161 -0
- package/dist/client/tree/merge.js.map +1 -0
- package/dist/client/tree/sessionview.js +163 -0
- package/dist/client/tree/sessionview.js.map +1 -0
- package/dist/client/tree/view.js +238 -0
- package/dist/client/tree/view.js.map +1 -0
- package/dist/client/uistate.js +45 -0
- package/dist/client/uistate.js.map +1 -0
- package/dist/daemon/agents.js +102 -0
- package/dist/daemon/agents.js.map +1 -0
- package/dist/daemon/branch.js +149 -0
- package/dist/daemon/branch.js.map +1 -0
- package/dist/daemon/embed-worker.js +74 -0
- package/dist/daemon/embed-worker.js.map +1 -0
- package/dist/daemon/essence.js +194 -0
- package/dist/daemon/essence.js.map +1 -0
- package/dist/daemon/extbridge.js +138 -0
- package/dist/daemon/extbridge.js.map +1 -0
- package/dist/daemon/lifecycle.js +134 -0
- package/dist/daemon/lifecycle.js.map +1 -0
- package/dist/daemon/pty-compat.js +53 -0
- package/dist/daemon/pty-compat.js.map +1 -0
- package/dist/daemon/semantic.js +337 -0
- package/dist/daemon/semantic.js.map +1 -0
- package/dist/daemon/server.js +808 -0
- package/dist/daemon/server.js.map +1 -0
- package/dist/daemon/supervisor.js +311 -0
- package/dist/daemon/supervisor.js.map +1 -0
- package/dist/extension/pines-extension.js +191 -0
- package/dist/extension/pines-extension.js.map +1 -0
- package/dist/layout/lexical.js +104 -0
- package/dist/layout/lexical.js.map +1 -0
- package/dist/layout/pca.js +91 -0
- package/dist/layout/pca.js.map +1 -0
- package/dist/layout/relax.js +47 -0
- package/dist/layout/relax.js.map +1 -0
- package/dist/session/ingest.js +95 -0
- package/dist/session/ingest.js.map +1 -0
- package/dist/session/parser.js +254 -0
- package/dist/session/parser.js.map +1 -0
- package/dist/shared/config.js +30 -0
- package/dist/shared/config.js.map +1 -0
- package/dist/shared/paths.js +47 -0
- package/dist/shared/paths.js.map +1 -0
- package/dist/shared/protocol.js +9 -0
- package/dist/shared/protocol.js.map +1 -0
- package/dist/shared/types.js +3 -0
- package/dist/shared/types.js.map +1 -0
- package/dist/shared/wire.js +51 -0
- package/dist/shared/wire.js.map +1 -0
- package/dist/store/db.js +203 -0
- package/dist/store/db.js.map +1 -0
- package/dist/store/watcher.js +69 -0
- package/dist/store/watcher.js.map +1 -0
- package/package.json +80 -0
- package/scripts/fix-node-pty.mjs +34 -0
- package/src/extension/pines-extension.ts +192 -0
- package/src/store/schema.sql +60 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pines extension — loaded INSIDE each pines-spawned pi via `-e <this file>`.
|
|
3
|
+
*
|
|
4
|
+
* Reports precise agent/session state to the pines daemon over PINES_SOCK and
|
|
5
|
+
* executes daemon commands (labels, session name, in-place tree navigation).
|
|
6
|
+
* No-ops instantly when PINES_SOCK is unset, so it is harmless if a user
|
|
7
|
+
* copies it into ~/.pi/agent/extensions/.
|
|
8
|
+
*
|
|
9
|
+
* SELF-CONTAINED by design: runtime imports are node builtins only; pi types
|
|
10
|
+
* are `import type` (erased). jiti loads this from wherever pines is installed,
|
|
11
|
+
* where pines' own modules may not be resolvable.
|
|
12
|
+
*
|
|
13
|
+
* Wire protocol: NDJSON over a Unix socket — keep in sync with
|
|
14
|
+
* src/shared/protocol.ts (PROTOCOL_VERSION below must match).
|
|
15
|
+
*/
|
|
16
|
+
import * as net from "node:net";
|
|
17
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
18
|
+
|
|
19
|
+
const PROTOCOL_VERSION = 2;
|
|
20
|
+
const NAV_COMMAND = "pines-nav";
|
|
21
|
+
|
|
22
|
+
export default function pinesExtension(pi: ExtensionAPI): void {
|
|
23
|
+
const sockPath = process.env.PINES_SOCK;
|
|
24
|
+
const treeId = process.env.PINES_TREE_ID ?? "";
|
|
25
|
+
if (!sockPath) return;
|
|
26
|
+
|
|
27
|
+
let socket: net.Socket | null = null;
|
|
28
|
+
let connected = false;
|
|
29
|
+
let backoff = 250;
|
|
30
|
+
let lastCtx: ExtensionContext | null = null;
|
|
31
|
+
let stamped = false;
|
|
32
|
+
/** Editor draft saved around an injected /pines-nav submission. */
|
|
33
|
+
let savedDraft: string | null = null;
|
|
34
|
+
|
|
35
|
+
function send(obj: Record<string, unknown>): void {
|
|
36
|
+
if (connected && socket) socket.write(JSON.stringify(obj) + "\n");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hello(ctx: ExtensionContext): void {
|
|
40
|
+
send({
|
|
41
|
+
t: "hello",
|
|
42
|
+
role: "extension",
|
|
43
|
+
protocolVersion: PROTOCOL_VERSION,
|
|
44
|
+
pid: process.pid,
|
|
45
|
+
treeId,
|
|
46
|
+
sessionPath: ctx.sessionManager.getSessionFile() ?? null,
|
|
47
|
+
sessionId: ctx.sessionManager.getSessionId(),
|
|
48
|
+
leafId: ctx.sessionManager.getLeafId(),
|
|
49
|
+
idle: ctx.isIdle(),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function handleCmd(msg: Record<string, unknown>): void {
|
|
54
|
+
const id = String(msg.id ?? "");
|
|
55
|
+
const reply = (ok: boolean, err?: string) => send({ t: "cmd_result", re: id, ok, err });
|
|
56
|
+
const ctx = lastCtx;
|
|
57
|
+
if (!ctx) return reply(false, "no session context yet");
|
|
58
|
+
|
|
59
|
+
try {
|
|
60
|
+
switch (msg.op) {
|
|
61
|
+
case "set_label":
|
|
62
|
+
pi.setLabel(String(msg.entryId), (msg.label as string | null) ?? undefined);
|
|
63
|
+
return reply(true);
|
|
64
|
+
case "set_session_name":
|
|
65
|
+
pi.setSessionName(String(msg.name ?? ""));
|
|
66
|
+
return reply(true);
|
|
67
|
+
case "stamp":
|
|
68
|
+
pi.appendEntry("pines", msg.data ?? { treeId, v: 1 });
|
|
69
|
+
return reply(true);
|
|
70
|
+
case "navigate_tree": {
|
|
71
|
+
// navigateTree needs a user-initiated command context. We stage the
|
|
72
|
+
// registered /pines-nav command in the editor; the daemon submits it
|
|
73
|
+
// by writing CR to the PTY once we ack.
|
|
74
|
+
if (ctx.mode !== "tui") return reply(false, "navigate requires tui mode");
|
|
75
|
+
if (!ctx.isIdle()) return reply(false, "agent is busy");
|
|
76
|
+
savedDraft = ctx.ui.getEditorText();
|
|
77
|
+
ctx.ui.setEditorText(`/${NAV_COMMAND} ${String(msg.targetId)}`);
|
|
78
|
+
return reply(true);
|
|
79
|
+
}
|
|
80
|
+
default:
|
|
81
|
+
return reply(false, `unknown op ${String(msg.op)}`);
|
|
82
|
+
}
|
|
83
|
+
} catch (err) {
|
|
84
|
+
return reply(false, err instanceof Error ? err.message : String(err));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function connect(): void {
|
|
89
|
+
if (socket) return;
|
|
90
|
+
const sock = net.connect(sockPath!);
|
|
91
|
+
socket = sock;
|
|
92
|
+
let buf = "";
|
|
93
|
+
sock.on("connect", () => {
|
|
94
|
+
connected = true;
|
|
95
|
+
backoff = 250;
|
|
96
|
+
if (lastCtx) hello(lastCtx);
|
|
97
|
+
});
|
|
98
|
+
sock.on("data", (chunk) => {
|
|
99
|
+
buf += chunk.toString("utf8");
|
|
100
|
+
let idx: number;
|
|
101
|
+
while ((idx = buf.indexOf("\n")) >= 0) {
|
|
102
|
+
const line = buf.slice(0, idx);
|
|
103
|
+
buf = buf.slice(idx + 1);
|
|
104
|
+
if (!line) continue;
|
|
105
|
+
try {
|
|
106
|
+
const msg = JSON.parse(line) as Record<string, unknown>;
|
|
107
|
+
if (msg.t === "cmd") handleCmd(msg);
|
|
108
|
+
} catch {
|
|
109
|
+
// ignore malformed daemon lines
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
const onGone = () => {
|
|
114
|
+
connected = false;
|
|
115
|
+
socket = null;
|
|
116
|
+
setTimeout(connect, backoff);
|
|
117
|
+
backoff = Math.min(backoff * 2, 5000);
|
|
118
|
+
};
|
|
119
|
+
sock.on("error", onGone);
|
|
120
|
+
sock.on("close", () => {
|
|
121
|
+
if (socket === sock) onGone();
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/* The in-place branch target of the editor-injection flow. */
|
|
126
|
+
pi.registerCommand(NAV_COMMAND, {
|
|
127
|
+
description: "pines: navigate the session tree to an entry (internal)",
|
|
128
|
+
handler: async (args, ctx) => {
|
|
129
|
+
const targetId = args.trim();
|
|
130
|
+
// Restore whatever the user had typed before the injection.
|
|
131
|
+
if (savedDraft !== null && ctx.mode === "tui") {
|
|
132
|
+
ctx.ui.setEditorText(savedDraft);
|
|
133
|
+
savedDraft = null;
|
|
134
|
+
}
|
|
135
|
+
if (!targetId) return;
|
|
136
|
+
await ctx.navigateTree(targetId, { summarize: false });
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
pi.on("session_start", (_event, ctx) => {
|
|
141
|
+
// Fires on startup AND after new/resume/fork (fresh runtime → re-hello).
|
|
142
|
+
lastCtx = ctx;
|
|
143
|
+
stamped = false;
|
|
144
|
+
connect();
|
|
145
|
+
if (connected) hello(ctx);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
pi.on("agent_start", (_event, ctx) => {
|
|
149
|
+
lastCtx = ctx;
|
|
150
|
+
if (!stamped) {
|
|
151
|
+
stamped = true;
|
|
152
|
+
const hasStamp = ctx.sessionManager
|
|
153
|
+
.getEntries()
|
|
154
|
+
.some((e) => e.type === "custom" && (e as { customType?: string }).customType === "pines");
|
|
155
|
+
if (!hasStamp && treeId) pi.appendEntry("pines", { treeId, v: 1 });
|
|
156
|
+
}
|
|
157
|
+
send({ t: "ev", type: "agent_start" });
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
pi.on("agent_end", (_event, ctx) => {
|
|
161
|
+
lastCtx = ctx;
|
|
162
|
+
send({ t: "ev", type: "agent_end" });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
pi.on("agent_settled", (_event, ctx) => {
|
|
166
|
+
lastCtx = ctx;
|
|
167
|
+
send({
|
|
168
|
+
t: "ev",
|
|
169
|
+
type: "agent_settled",
|
|
170
|
+
newLeafId: ctx.sessionManager.getLeafId(),
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
pi.on("session_tree", (event, ctx) => {
|
|
175
|
+
lastCtx = ctx;
|
|
176
|
+
send({
|
|
177
|
+
t: "ev",
|
|
178
|
+
type: "session_tree",
|
|
179
|
+
newLeafId: event.newLeafId,
|
|
180
|
+
oldLeafId: event.oldLeafId,
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
pi.on("session_info_changed", (event, ctx) => {
|
|
185
|
+
lastCtx = ctx;
|
|
186
|
+
send({ t: "ev", type: "session_name", name: event.name ?? "" });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
pi.on("session_shutdown", (event, _ctx) => {
|
|
190
|
+
send({ t: "ev", type: "session_shutdown", sessionPath: event.targetSessionFile });
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
-- pines state store. Owned exclusively by the daemon. WAL mode.
|
|
2
|
+
|
|
3
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
4
|
+
key TEXT PRIMARY KEY,
|
|
5
|
+
value TEXT
|
|
6
|
+
);
|
|
7
|
+
|
|
8
|
+
CREATE TABLE IF NOT EXISTS trees (
|
|
9
|
+
tree_id TEXT PRIMARY KEY,
|
|
10
|
+
session_path TEXT UNIQUE NOT NULL,
|
|
11
|
+
session_id TEXT,
|
|
12
|
+
name TEXT,
|
|
13
|
+
cwd TEXT,
|
|
14
|
+
parent_session_path TEXT,
|
|
15
|
+
status TEXT NOT NULL DEFAULT 'dormant',
|
|
16
|
+
seen INTEGER NOT NULL DEFAULT 1,
|
|
17
|
+
leaf_id TEXT,
|
|
18
|
+
node_count INTEGER NOT NULL DEFAULT 0,
|
|
19
|
+
mtime INTEGER NOT NULL DEFAULT 0,
|
|
20
|
+
ingest_offset INTEGER NOT NULL DEFAULT 0,
|
|
21
|
+
embedding BLOB,
|
|
22
|
+
embedding_version INTEGER NOT NULL DEFAULT 0,
|
|
23
|
+
x REAL NOT NULL DEFAULT 0,
|
|
24
|
+
y REAL NOT NULL DEFAULT 0,
|
|
25
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
26
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
last_screen BLOB,
|
|
28
|
+
created_at INTEGER NOT NULL,
|
|
29
|
+
updated_at INTEGER NOT NULL
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
-- One row per parent in v1 (plain lineage); n rows per graft in v2.
|
|
33
|
+
CREATE TABLE IF NOT EXISTS graft_parents (
|
|
34
|
+
tree_id TEXT NOT NULL REFERENCES trees(tree_id) ON DELETE CASCADE,
|
|
35
|
+
parent_session_path TEXT NOT NULL,
|
|
36
|
+
parent_entry_id TEXT,
|
|
37
|
+
ordinal INTEGER NOT NULL,
|
|
38
|
+
PRIMARY KEY (tree_id, ordinal)
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
-- Per-chunk embedding cache: one row per embedded piece of a session (user
|
|
42
|
+
-- message, summary, or the name+labels/tools digests). chunk_key is the entry
|
|
43
|
+
-- id for message-anchored chunks, or a content-hashed 'meta:'/'tools:' key for
|
|
44
|
+
-- digest chunks, so incremental ingest embeds only what actually changed.
|
|
45
|
+
CREATE TABLE IF NOT EXISTS tree_chunks (
|
|
46
|
+
tree_id TEXT NOT NULL REFERENCES trees(tree_id) ON DELETE CASCADE,
|
|
47
|
+
chunk_key TEXT NOT NULL,
|
|
48
|
+
kind TEXT NOT NULL, -- 'meta' | 'tools' | 'user' | 'summary'
|
|
49
|
+
pos INTEGER NOT NULL, -- file-order index (recency weighting); -1 for digests
|
|
50
|
+
embedding BLOB NOT NULL, -- float32 vector, unit norm
|
|
51
|
+
PRIMARY KEY (tree_id, chunk_key)
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS search_fts USING fts5(
|
|
55
|
+
text,
|
|
56
|
+
tree_id UNINDEXED,
|
|
57
|
+
entry_id UNINDEXED,
|
|
58
|
+
kind UNINDEXED,
|
|
59
|
+
tokenize = 'porter unicode61'
|
|
60
|
+
);
|