@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.
Files changed (98) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +37 -0
  3. package/dist/cli.js +83 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/client/ansi.js +37 -0
  6. package/dist/client/ansi.js.map +1 -0
  7. package/dist/client/app.js +1868 -0
  8. package/dist/client/app.js.map +1 -0
  9. package/dist/client/boot.js +221 -0
  10. package/dist/client/boot.js.map +1 -0
  11. package/dist/client/commands.js +133 -0
  12. package/dist/client/commands.js.map +1 -0
  13. package/dist/client/daemon-client.js +99 -0
  14. package/dist/client/daemon-client.js.map +1 -0
  15. package/dist/client/forest/camera.js +88 -0
  16. package/dist/client/forest/camera.js.map +1 -0
  17. package/dist/client/forest/canvas.js +175 -0
  18. package/dist/client/forest/canvas.js.map +1 -0
  19. package/dist/client/forest/sidebar.js +182 -0
  20. package/dist/client/forest/sidebar.js.map +1 -0
  21. package/dist/client/forest/status.js +39 -0
  22. package/dist/client/forest/status.js.map +1 -0
  23. package/dist/client/forest/style.js +70 -0
  24. package/dist/client/forest/style.js.map +1 -0
  25. package/dist/client/forest/styles.js +429 -0
  26. package/dist/client/forest/styles.js.map +1 -0
  27. package/dist/client/forest/view.js +181 -0
  28. package/dist/client/forest/view.js.map +1 -0
  29. package/dist/client/forest/wordmark.js +26 -0
  30. package/dist/client/forest/wordmark.js.map +1 -0
  31. package/dist/client/input.js +89 -0
  32. package/dist/client/input.js.map +1 -0
  33. package/dist/client/theme.js +27 -0
  34. package/dist/client/theme.js.map +1 -0
  35. package/dist/client/tree/asciitree.js +277 -0
  36. package/dist/client/tree/asciitree.js.map +1 -0
  37. package/dist/client/tree/convmap.js +260 -0
  38. package/dist/client/tree/convmap.js.map +1 -0
  39. package/dist/client/tree/lanegraph.js +260 -0
  40. package/dist/client/tree/lanegraph.js.map +1 -0
  41. package/dist/client/tree/merge.js +161 -0
  42. package/dist/client/tree/merge.js.map +1 -0
  43. package/dist/client/tree/sessionview.js +163 -0
  44. package/dist/client/tree/sessionview.js.map +1 -0
  45. package/dist/client/tree/view.js +238 -0
  46. package/dist/client/tree/view.js.map +1 -0
  47. package/dist/client/uistate.js +45 -0
  48. package/dist/client/uistate.js.map +1 -0
  49. package/dist/daemon/agents.js +102 -0
  50. package/dist/daemon/agents.js.map +1 -0
  51. package/dist/daemon/branch.js +149 -0
  52. package/dist/daemon/branch.js.map +1 -0
  53. package/dist/daemon/embed-worker.js +74 -0
  54. package/dist/daemon/embed-worker.js.map +1 -0
  55. package/dist/daemon/essence.js +194 -0
  56. package/dist/daemon/essence.js.map +1 -0
  57. package/dist/daemon/extbridge.js +138 -0
  58. package/dist/daemon/extbridge.js.map +1 -0
  59. package/dist/daemon/lifecycle.js +134 -0
  60. package/dist/daemon/lifecycle.js.map +1 -0
  61. package/dist/daemon/pty-compat.js +53 -0
  62. package/dist/daemon/pty-compat.js.map +1 -0
  63. package/dist/daemon/semantic.js +337 -0
  64. package/dist/daemon/semantic.js.map +1 -0
  65. package/dist/daemon/server.js +808 -0
  66. package/dist/daemon/server.js.map +1 -0
  67. package/dist/daemon/supervisor.js +311 -0
  68. package/dist/daemon/supervisor.js.map +1 -0
  69. package/dist/extension/pines-extension.js +191 -0
  70. package/dist/extension/pines-extension.js.map +1 -0
  71. package/dist/layout/lexical.js +104 -0
  72. package/dist/layout/lexical.js.map +1 -0
  73. package/dist/layout/pca.js +91 -0
  74. package/dist/layout/pca.js.map +1 -0
  75. package/dist/layout/relax.js +47 -0
  76. package/dist/layout/relax.js.map +1 -0
  77. package/dist/session/ingest.js +95 -0
  78. package/dist/session/ingest.js.map +1 -0
  79. package/dist/session/parser.js +254 -0
  80. package/dist/session/parser.js.map +1 -0
  81. package/dist/shared/config.js +30 -0
  82. package/dist/shared/config.js.map +1 -0
  83. package/dist/shared/paths.js +47 -0
  84. package/dist/shared/paths.js.map +1 -0
  85. package/dist/shared/protocol.js +9 -0
  86. package/dist/shared/protocol.js.map +1 -0
  87. package/dist/shared/types.js +3 -0
  88. package/dist/shared/types.js.map +1 -0
  89. package/dist/shared/wire.js +51 -0
  90. package/dist/shared/wire.js.map +1 -0
  91. package/dist/store/db.js +203 -0
  92. package/dist/store/db.js.map +1 -0
  93. package/dist/store/watcher.js +69 -0
  94. package/dist/store/watcher.js.map +1 -0
  95. package/package.json +80 -0
  96. package/scripts/fix-node-pty.mjs +34 -0
  97. package/src/extension/pines-extension.ts +192 -0
  98. package/src/store/schema.sql +60 -0
@@ -0,0 +1,30 @@
1
+ /** User configuration: ~/.pines/config.json (all fields optional). */
2
+ import { readFileSync } from "node:fs";
3
+ import { configPath } from "./paths.js";
4
+ /** Default local embedding model (384-d, ~23 MB, offline after first download). */
5
+ export const DEFAULT_EMBED_MODEL = "Xenova/all-MiniLM-L6-v2";
6
+ let cached;
7
+ export function loadConfig() {
8
+ if (cached)
9
+ return cached;
10
+ try {
11
+ cached = JSON.parse(readFileSync(configPath(), "utf8"));
12
+ }
13
+ catch {
14
+ cached = {};
15
+ }
16
+ return cached;
17
+ }
18
+ /** "ctrl+t" → 0x14. Returns undefined for unsupported specs. */
19
+ export function prefixByte(spec) {
20
+ if (!spec)
21
+ return undefined;
22
+ const m = /^ctrl\+([a-z\]\\^_])$/i.exec(spec.trim());
23
+ if (!m)
24
+ return undefined;
25
+ const ch = m[1].toLowerCase();
26
+ if (ch >= "a" && ch <= "z")
27
+ return ch.charCodeAt(0) - 96;
28
+ return { "]": 0x1d, "\\": 0x1c, "^": 0x1e, _: 0x1f }[ch];
29
+ }
30
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/shared/config.ts"],"names":[],"mappings":"AAAA,sEAAsE;AACtE,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,mFAAmF;AACnF,MAAM,CAAC,MAAM,mBAAmB,GAAG,yBAAyB,CAAC;AAmB7D,IAAI,MAA+B,CAAC;AAEpC,MAAM,UAAU,UAAU;IACxB,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAC1B,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,UAAU,EAAE,EAAE,MAAM,CAAC,CAAgB,CAAC;IACzE,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,GAAG,EAAE,CAAC;IACd,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,gEAAgE;AAChE,MAAM,UAAU,UAAU,CAAC,IAAwB;IACjD,IAAI,CAAC,IAAI;QAAE,OAAO,SAAS,CAAC;IAC5B,MAAM,CAAC,GAAG,wBAAwB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;IACrD,IAAI,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC;IACzB,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,CAAE,CAAC,WAAW,EAAE,CAAC;IAC/B,IAAI,EAAE,IAAI,GAAG,IAAI,EAAE,IAAI,GAAG;QAAE,OAAO,EAAE,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,EAAE,CAAC;IACzD,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC;AAC3D,CAAC"}
@@ -0,0 +1,47 @@
1
+ import { homedir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { mkdirSync } from "node:fs";
4
+ /**
5
+ * All pines state lives under ~/.pines (overridable with PINES_HOME, which the
6
+ * test suite uses to sandbox daemons).
7
+ */
8
+ export function pinesHome() {
9
+ return process.env.PINES_HOME ?? join(homedir(), ".pines");
10
+ }
11
+ export function ensurePinesHome() {
12
+ const home = pinesHome();
13
+ mkdirSync(home, { recursive: true });
14
+ return home;
15
+ }
16
+ export function socketPath() {
17
+ return process.env.PINES_SOCK ?? join(pinesHome(), "pines.sock");
18
+ }
19
+ export function dbPath() {
20
+ return join(pinesHome(), "pines.db");
21
+ }
22
+ export function modelsDir() {
23
+ return join(pinesHome(), "models");
24
+ }
25
+ export function configPath() {
26
+ return join(pinesHome(), "config.json");
27
+ }
28
+ export function daemonLogPath() {
29
+ return join(pinesHome(), "daemon.log");
30
+ }
31
+ /**
32
+ * Pid of the daemon that owns the socket. The handshake carries `daemonPid`
33
+ * too, but a daemon running stale code rejects the handshake before we ever
34
+ * learn its pid — that is exactly when we most need to signal it.
35
+ */
36
+ export function daemonPidPath() {
37
+ return join(pinesHome(), "daemon.pid");
38
+ }
39
+ /** Client UI state (sidebar width/visibility, …) — separate from config.json. */
40
+ export function uiStatePath() {
41
+ return join(pinesHome(), "ui.json");
42
+ }
43
+ /** pi's default session root (all cwd-encoded subdirectories live under it). */
44
+ export function piSessionsRoot() {
45
+ return process.env.PINES_PI_SESSIONS ?? join(homedir(), ".pi", "agent", "sessions");
46
+ }
47
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","sourceRoot":"","sources":["../../src/shared/paths.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,MAAM,SAAS,CAAC;AAEpC;;;GAGG;AACH,MAAM,UAAU,SAAS;IACvB,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,CAAC,CAAC;AAC7D,CAAC;AAED,MAAM,UAAU,eAAe;IAC7B,MAAM,IAAI,GAAG,SAAS,EAAE,CAAC;IACzB,SAAS,CAAC,IAAI,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IACrC,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,OAAO,CAAC,GAAG,CAAC,UAAU,IAAI,IAAI,CAAC,SAAS,EAAE,EAAE,YAAY,CAAC,CAAC;AACnE,CAAC;AAED,MAAM,UAAU,MAAM;IACpB,OAAO,IAAI,CAAC,SAAS,EAAE,EAAE,UAAU,CAAC,CAAC;AACvC,CAAC;AAED,MAAM,UAAU,SAAS;IACvB,OAAO,IAAI,CAAC,SAAS,EAAE,EAAE,QAAQ,CAAC,CAAC;AACrC,CAAC;AAED,MAAM,UAAU,UAAU;IACxB,OAAO,IAAI,CAAC,SAAS,EAAE,EAAE,aAAa,CAAC,CAAC;AAC1C,CAAC;AAED,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,SAAS,EAAE,EAAE,YAAY,CAAC,CAAC;AACzC,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,aAAa;IAC3B,OAAO,IAAI,CAAC,SAAS,EAAE,EAAE,YAAY,CAAC,CAAC;AACzC,CAAC;AAED,iFAAiF;AACjF,MAAM,UAAU,WAAW;IACzB,OAAO,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,CAAC,CAAC;AACtC,CAAC;AAED,gFAAgF;AAChF,MAAM,UAAU,cAAc;IAC5B,OAAO,OAAO,CAAC,GAAG,CAAC,iBAAiB,IAAI,IAAI,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC;AACtF,CAAC"}
@@ -0,0 +1,9 @@
1
+ export const PROTOCOL_VERSION = 2;
2
+ export function isHello(msg) {
3
+ return (typeof msg === "object" &&
4
+ msg !== null &&
5
+ msg.t === "hello" &&
6
+ (msg.role === "client" ||
7
+ msg.role === "extension"));
8
+ }
9
+ //# sourceMappingURL=protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"protocol.js","sourceRoot":"","sources":["../../src/shared/protocol.ts"],"names":[],"mappings":"AAWA,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,CAAC;AAgSlC,MAAM,UAAU,OAAO,CACrB,GAAY;IAEZ,OAAO,CACL,OAAO,GAAG,KAAK,QAAQ;QACvB,GAAG,KAAK,IAAI;QACX,GAAuB,CAAC,CAAC,KAAK,OAAO;QACtC,CAAE,GAA0B,CAAC,IAAI,KAAK,QAAQ;YAC3C,GAA0B,CAAC,IAAI,KAAK,WAAW,CAAC,CACpD,CAAC;AACJ,CAAC"}
@@ -0,0 +1,3 @@
1
+ /** Core domain types shared by daemon, client, and tests. */
2
+ export {};
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/shared/types.ts"],"names":[],"mappings":"AAAA,6DAA6D"}
@@ -0,0 +1,51 @@
1
+ import { EventEmitter } from "node:events";
2
+ export class Wire extends EventEmitter {
3
+ socket;
4
+ buf = "";
5
+ closed = false;
6
+ constructor(socket) {
7
+ super();
8
+ this.socket = socket;
9
+ socket.setEncoding("utf8");
10
+ socket.on("data", (chunk) => this.onData(chunk));
11
+ socket.on("close", () => {
12
+ this.closed = true;
13
+ this.emit("close");
14
+ });
15
+ socket.on("error", (err) => this.emit("error", err));
16
+ }
17
+ onData(chunk) {
18
+ this.buf += chunk;
19
+ let idx;
20
+ while ((idx = this.buf.indexOf("\n")) >= 0) {
21
+ let line = this.buf.slice(0, idx);
22
+ this.buf = this.buf.slice(idx + 1);
23
+ if (line.endsWith("\r"))
24
+ line = line.slice(0, -1);
25
+ if (line.length === 0)
26
+ continue;
27
+ let parsed;
28
+ try {
29
+ parsed = JSON.parse(line);
30
+ }
31
+ catch {
32
+ // A malformed line is a protocol violation from the peer; surface and move on.
33
+ this.emit("error", new Error(`wire: malformed JSON line (${line.length} bytes)`));
34
+ continue;
35
+ }
36
+ this.emit("msg", parsed);
37
+ }
38
+ }
39
+ send(msg) {
40
+ if (this.closed || this.socket.destroyed)
41
+ return false;
42
+ return this.socket.write(JSON.stringify(msg) + "\n");
43
+ }
44
+ end() {
45
+ this.socket.end();
46
+ }
47
+ destroy() {
48
+ this.socket.destroy();
49
+ }
50
+ }
51
+ //# sourceMappingURL=wire.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"wire.js","sourceRoot":"","sources":["../../src/shared/wire.ts"],"names":[],"mappings":"AAOA,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAQ3C,MAAM,OAAO,IAAK,SAAQ,YAAwB;IAI3B;IAHb,GAAG,GAAG,EAAE,CAAC;IACT,MAAM,GAAG,KAAK,CAAC;IAEvB,YAAqB,MAAc;QACjC,KAAK,EAAE,CAAC;QADW,WAAM,GAAN,MAAM,CAAQ;QAEjC,MAAM,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QAC3B,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,KAAa,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACzD,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;YACtB,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC;YACnB,IAAI,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACrB,CAAC,CAAC,CAAC;QACH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC;IACvD,CAAC;IAEO,MAAM,CAAC,KAAa;QAC1B,IAAI,CAAC,GAAG,IAAI,KAAK,CAAC;QAClB,IAAI,GAAW,CAAC;QAChB,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3C,IAAI,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;YAClC,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC,CAAC;YACnC,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;gBAAE,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;YAClD,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC;gBAAE,SAAS;YAChC,IAAI,MAAe,CAAC;YACpB,IAAI,CAAC;gBACH,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;YAC5B,CAAC;YAAC,MAAM,CAAC;gBACP,+EAA+E;gBAC/E,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,IAAI,KAAK,CAAC,8BAA8B,IAAI,CAAC,MAAM,SAAS,CAAC,CAAC,CAAC;gBAClF,SAAS;YACX,CAAC;YACD,IAAI,CAAC,IAAI,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC;QAC3B,CAAC;IACH,CAAC;IAED,IAAI,CAAC,GAAY;QACf,IAAI,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS;YAAE,OAAO,KAAK,CAAC;QACvD,OAAO,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACvD,CAAC;IAED,GAAG;QACD,IAAI,CAAC,MAAM,CAAC,GAAG,EAAE,CAAC;IACpB,CAAC;IAED,OAAO;QACL,IAAI,CAAC,MAAM,CAAC,OAAO,EAAE,CAAC;IACxB,CAAC;CACF"}
@@ -0,0 +1,203 @@
1
+ /** better-sqlite3 open/migrate for the daemon's state store. */
2
+ import Database from "better-sqlite3";
3
+ import { readFileSync } from "node:fs";
4
+ import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { dbPath, ensurePinesHome } from "../shared/paths.js";
7
+ const SCHEMA_VERSION = 3;
8
+ function schemaSql() {
9
+ // dist/store/db.js → ../../src/store/schema.sql (schema ships as source).
10
+ const here = dirname(fileURLToPath(import.meta.url));
11
+ return readFileSync(join(here, "..", "..", "src", "store", "schema.sql"), "utf8");
12
+ }
13
+ export function openDb(path = dbPath()) {
14
+ ensurePinesHome();
15
+ const db = new Database(path);
16
+ db.pragma("journal_mode = WAL");
17
+ db.pragma("synchronous = NORMAL");
18
+ db.pragma("foreign_keys = ON");
19
+ db.exec(schemaSql());
20
+ const row = db.prepare("SELECT value FROM meta WHERE key = 'schema_version'").get();
21
+ if (!row) {
22
+ db.prepare("INSERT INTO meta (key, value) VALUES ('schema_version', ?)").run(String(SCHEMA_VERSION));
23
+ }
24
+ else {
25
+ migrate(db, Number(row.value));
26
+ }
27
+ return db;
28
+ }
29
+ /**
30
+ * Migration ladder for DBs created before the current schema. schema.sql is
31
+ * IF NOT EXISTS throughout, so new tables already exist by the time we get
32
+ * here — only in-place ALTERs live in the ladder.
33
+ */
34
+ function migrate(db, from) {
35
+ if (from >= SCHEMA_VERSION)
36
+ return;
37
+ // CREATE TABLE IF NOT EXISTS leaves existing tables untouched, so new
38
+ // columns must be added explicitly — tolerating databases that already
39
+ // have them (schema.sql creates the full shape on fresh installs).
40
+ const addColumn = (sql) => {
41
+ try {
42
+ db.exec(sql);
43
+ }
44
+ catch {
45
+ // Column already present — fine.
46
+ }
47
+ };
48
+ if (from < 2) {
49
+ addColumn("ALTER TABLE trees ADD COLUMN archived INTEGER NOT NULL DEFAULT 0");
50
+ }
51
+ if (from < 3) {
52
+ addColumn("ALTER TABLE trees ADD COLUMN embedding_version INTEGER NOT NULL DEFAULT 0");
53
+ }
54
+ db.prepare("UPDATE meta SET value = ? WHERE key = 'schema_version'").run(String(SCHEMA_VERSION));
55
+ }
56
+ export function allTrees(db) {
57
+ return db.prepare("SELECT * FROM trees").all();
58
+ }
59
+ export function upsertTree(db, row) {
60
+ const now = Date.now();
61
+ const existing = db
62
+ .prepare("SELECT tree_id FROM trees WHERE session_path = ?")
63
+ .get(row.session_path);
64
+ // A live pi can move its record to a new session file (/new inside pi):
65
+ // no row has the new path, but the tree's identity row exists — rebind it
66
+ // rather than INSERTing a tree_id duplicate. The offset belongs to the old
67
+ // file, so it resets; the old file re-ingests as its own tree via watcher.
68
+ const rebind = existing
69
+ ? undefined
70
+ : db.prepare("SELECT tree_id FROM trees WHERE tree_id = ?").get(row.tree_id);
71
+ if (existing || rebind) {
72
+ const sets = [];
73
+ const vals = [];
74
+ for (const [k, v] of Object.entries(row)) {
75
+ if (k === "tree_id" || k === "session_path" || v === undefined)
76
+ continue;
77
+ sets.push(`${k} = ?`);
78
+ vals.push(v);
79
+ }
80
+ if (rebind) {
81
+ sets.push("session_path = ?");
82
+ vals.push(row.session_path);
83
+ if (row.ingest_offset === undefined) {
84
+ sets.push("ingest_offset = 0");
85
+ }
86
+ }
87
+ sets.push("updated_at = ?");
88
+ vals.push(now, (existing ?? rebind).tree_id);
89
+ db.prepare(`UPDATE trees SET ${sets.join(", ")} WHERE tree_id = ?`).run(...vals);
90
+ }
91
+ else {
92
+ db.prepare(`INSERT INTO trees (tree_id, session_path, session_id, name, cwd, parent_session_path,
93
+ status, seen, leaf_id, node_count, mtime, ingest_offset, x, y, archived, created_at, updated_at)
94
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(row.tree_id, row.session_path, row.session_id ?? null, row.name ?? null, row.cwd ?? null, row.parent_session_path ?? null, row.status ?? "dormant", row.seen ?? 1, row.leaf_id ?? null, row.node_count ?? 0, row.mtime ?? now, row.ingest_offset ?? 0, row.x ?? 0, row.y ?? 0, row.archived ?? 0, now, now);
95
+ }
96
+ }
97
+ export function treeBySessionPath(db, sessionPath) {
98
+ return db.prepare("SELECT * FROM trees WHERE session_path = ?").get(sessionPath);
99
+ }
100
+ /** Remove a tree and its contentless FTS rows. FK cascades its lineage rows. */
101
+ export function deleteTree(db, treeId) {
102
+ db.transaction(() => {
103
+ deleteFtsForTree(db, treeId);
104
+ db.prepare("DELETE FROM trees WHERE tree_id = ?").run(treeId);
105
+ })();
106
+ }
107
+ export function deleteFtsForTree(db, treeId) {
108
+ db.prepare("DELETE FROM search_fts WHERE tree_id = ?").run(treeId);
109
+ }
110
+ export function insertFts(db, rows) {
111
+ const stmt = db.prepare("INSERT INTO search_fts (text, tree_id, entry_id, kind) VALUES (?, ?, ?, ?)");
112
+ const insertAll = db.transaction(() => {
113
+ for (const r of rows)
114
+ stmt.run(r.text, r.treeId, r.entryId, r.kind);
115
+ });
116
+ insertAll();
117
+ }
118
+ /** Zero-copy Float32Array view over an embedding BLOB. */
119
+ export function vectorOf(blob) {
120
+ return new Float32Array(blob.buffer, blob.byteOffset, blob.byteLength / 4);
121
+ }
122
+ /**
123
+ * Top-k nearest trees to `treeId` by cosine over the pooled embeddings.
124
+ * Vectors are unit-norm, so dot product == cosine. Brute force: a personal
125
+ * forest is at most a few hundred 384-d vectors — microseconds, no index.
126
+ */
127
+ export function similarTrees(db, treeId, k = 8, minScore = 0.25) {
128
+ const target = db
129
+ .prepare("SELECT embedding FROM trees WHERE tree_id = ? AND embedding IS NOT NULL")
130
+ .get(treeId);
131
+ if (!target)
132
+ return [];
133
+ const t = vectorOf(target.embedding);
134
+ const rows = db
135
+ .prepare("SELECT tree_id, embedding FROM trees WHERE embedding IS NOT NULL AND tree_id != ?")
136
+ .all(treeId);
137
+ const scored = [];
138
+ for (const r of rows) {
139
+ const v = vectorOf(r.embedding);
140
+ if (v.length !== t.length)
141
+ continue; // stale vector from another model
142
+ let dot = 0;
143
+ for (let i = 0; i < t.length; i++)
144
+ dot += t[i] * v[i];
145
+ if (dot >= minScore)
146
+ scored.push({ tree_id: r.tree_id, score: dot });
147
+ }
148
+ scored.sort((a, b) => b.score - a.score);
149
+ return scored.slice(0, k);
150
+ }
151
+ export function chunksForTree(db, treeId) {
152
+ return db
153
+ .prepare("SELECT chunk_key, kind, pos, embedding FROM tree_chunks WHERE tree_id = ? ORDER BY pos")
154
+ .all(treeId);
155
+ }
156
+ export function upsertChunks(db, treeId, rows) {
157
+ const stmt = db.prepare(`INSERT INTO tree_chunks (tree_id, chunk_key, kind, pos, embedding) VALUES (?, ?, ?, ?, ?)
158
+ ON CONFLICT(tree_id, chunk_key) DO UPDATE SET
159
+ kind = excluded.kind, pos = excluded.pos, embedding = excluded.embedding`);
160
+ db.transaction(() => {
161
+ for (const r of rows)
162
+ stmt.run(treeId, r.chunkKey, r.kind, r.pos, r.embedding);
163
+ })();
164
+ }
165
+ export function deleteChunks(db, treeId, chunkKeys) {
166
+ const stmt = db.prepare("DELETE FROM tree_chunks WHERE tree_id = ? AND chunk_key = ?");
167
+ db.transaction(() => {
168
+ for (const key of chunkKeys)
169
+ stmt.run(treeId, key);
170
+ })();
171
+ }
172
+ /** Keep the per-tree chunk cache bounded: drop the oldest user rows past `keep`. */
173
+ export function pruneUserChunks(db, treeId, keep) {
174
+ db.prepare(`DELETE FROM tree_chunks WHERE tree_id = ? AND kind = 'user' AND chunk_key IN (
175
+ SELECT chunk_key FROM tree_chunks WHERE tree_id = ? AND kind = 'user'
176
+ ORDER BY pos DESC LIMIT -1 OFFSET ?)`).run(treeId, treeId, keep);
177
+ }
178
+ export function searchFts(db, query, limit = 40, treeId) {
179
+ // Escape user input into a quoted phrase-ish prefix query per token.
180
+ const tokens = query
181
+ .split(/\s+/)
182
+ .filter(Boolean)
183
+ .map((tok) => `"${tok.replaceAll('"', '""')}"*`);
184
+ if (tokens.length === 0)
185
+ return [];
186
+ const match = tokens.join(" ");
187
+ const where = treeId ? "AND tree_id = ?" : "";
188
+ const args = treeId ? [match, treeId, limit] : [match, limit];
189
+ try {
190
+ return db
191
+ .prepare(`SELECT tree_id, entry_id, kind,
192
+ snippet(search_fts, 0, '', '', '…', 12) AS snippet,
193
+ rank
194
+ FROM search_fts
195
+ WHERE search_fts MATCH ? ${where}
196
+ ORDER BY rank LIMIT ?`)
197
+ .all(...args);
198
+ }
199
+ catch {
200
+ return []; // malformed MATCH input should never crash the daemon
201
+ }
202
+ }
203
+ //# sourceMappingURL=db.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"db.js","sourceRoot":"","sources":["../../src/store/db.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Watch pi's session directories so sessions created outside pines (a user
3
+ * running plain `pi`) appear in the forest automatically, and live sessions'
4
+ * appends get re-ingested.
5
+ */
6
+ import chokidar from "chokidar";
7
+ import { existsSync } from "node:fs";
8
+ import { piSessionsRoot } from "../shared/paths.js";
9
+ const DEBOUNCE_MS = 250;
10
+ export function watchSessions(onFile, options = {}) {
11
+ const root = options.root ?? piSessionsRoot();
12
+ if (!existsSync(root)) {
13
+ // pi never ran on this machine — nothing to watch yet. A daemon restart
14
+ // after the first pi run picks it up; cheap poll covers the gap.
15
+ const poll = setInterval(() => {
16
+ if (existsSync(root)) {
17
+ clearInterval(poll);
18
+ start();
19
+ }
20
+ }, 5000);
21
+ poll.unref();
22
+ let watcher;
23
+ const start = () => {
24
+ watcher = makeWatcher(onFile, options.onUnlink, root);
25
+ };
26
+ return {
27
+ close: async () => {
28
+ clearInterval(poll);
29
+ await watcher?.close();
30
+ },
31
+ };
32
+ }
33
+ return makeWatcher(onFile, options.onUnlink, root);
34
+ }
35
+ function makeWatcher(onFile, onUnlink, root) {
36
+ const pending = new Map();
37
+ const watcher = chokidar.watch(root, {
38
+ ignoreInitial: false,
39
+ depth: 2,
40
+ awaitWriteFinish: { stabilityThreshold: 200, pollInterval: 50 },
41
+ });
42
+ const schedule = (path, kind) => {
43
+ if (!path.endsWith(".jsonl"))
44
+ return;
45
+ const prior = pending.get(path);
46
+ if (prior)
47
+ clearTimeout(prior.timer);
48
+ const timer = setTimeout(() => {
49
+ pending.delete(path);
50
+ if (kind === "unlink")
51
+ onUnlink?.(path);
52
+ else
53
+ onFile(path);
54
+ }, DEBOUNCE_MS);
55
+ pending.set(path, { timer, kind });
56
+ };
57
+ watcher.on("add", (path) => schedule(path, "ingest"));
58
+ watcher.on("change", (path) => schedule(path, "ingest"));
59
+ watcher.on("unlink", (path) => schedule(path, "unlink"));
60
+ return {
61
+ close: async () => {
62
+ for (const { timer } of pending.values())
63
+ clearTimeout(timer);
64
+ pending.clear();
65
+ await watcher.close();
66
+ },
67
+ };
68
+ }
69
+ //# sourceMappingURL=watcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.js","sourceRoot":"","sources":["../../src/store/watcher.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "@edeng23/pines",
3
+ "version": "0.1.0",
4
+ "description": "Tree-first orchestration TUI for pi coding agents. pi + trees = pines.",
5
+ "license": "Apache-2.0",
6
+ "author": "Eden Gaon",
7
+ "homepage": "https://github.com/edeng23/pines#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/edeng23/pines.git"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/edeng23/pines/issues"
14
+ },
15
+ "keywords": [
16
+ "pi",
17
+ "pi-agent",
18
+ "coding-agent",
19
+ "agent-orchestrator",
20
+ "orchestrator",
21
+ "tui",
22
+ "terminal",
23
+ "forest"
24
+ ],
25
+ "type": "module",
26
+ "bin": {
27
+ "pines": "./dist/cli.js"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "files": [
33
+ "dist/",
34
+ "scripts/fix-node-pty.mjs",
35
+ "src/extension/",
36
+ "src/store/schema.sql"
37
+ ],
38
+ "engines": {
39
+ "node": ">=22.19.0 <25"
40
+ },
41
+ "scripts": {
42
+ "build": "tsc -p tsconfig.json",
43
+ "dev": "tsx src/cli.ts",
44
+ "predemo": "pnpm build",
45
+ "demo": "node scripts/demo.mjs",
46
+ "postinstall": "node scripts/fix-node-pty.mjs",
47
+ "prepublishOnly": "pnpm typecheck && pnpm test",
48
+ "pretest": "pnpm build",
49
+ "test": "vitest run",
50
+ "test:watch": "vitest",
51
+ "typecheck": "tsc -p tsconfig.json --noEmit"
52
+ },
53
+ "dependencies": {
54
+ "@earendil-works/pi-coding-agent": "0.82.0",
55
+ "@earendil-works/pi-tui": "0.82.0",
56
+ "@xterm/addon-serialize": "^0.14.0",
57
+ "@xterm/headless": "^6.0.0",
58
+ "better-sqlite3": "^13.0.1",
59
+ "chokidar": "^5.0.0",
60
+ "node-pty": "^1.1.0"
61
+ },
62
+ "optionalDependencies": {
63
+ "@huggingface/transformers": "^4.2.0"
64
+ },
65
+ "pnpm": {
66
+ "onlyBuiltDependencies": [
67
+ "better-sqlite3",
68
+ "node-pty",
69
+ "esbuild",
70
+ "onnxruntime-node"
71
+ ]
72
+ },
73
+ "devDependencies": {
74
+ "@types/better-sqlite3": "^7.6.11",
75
+ "@types/node": "^22.10.0",
76
+ "tsx": "^4.19.0",
77
+ "typescript": "~5.6.3",
78
+ "vitest": "^3.0.0"
79
+ }
80
+ }
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Install-time counterpart to src/daemon/pty-compat.ts. Runtime performs the
4
+ * same repair because package managers may disable lifecycle scripts.
5
+ */
6
+ import { chmodSync, existsSync, statSync } from "node:fs";
7
+ import { createRequire } from "node:module";
8
+ import { dirname, join } from "node:path";
9
+
10
+ // Never fail the install: this is an optimisation, not a requirement. node-pty
11
+ // may not be resolvable yet (install ordering), the tree may be read-only, or
12
+ // the platform may not ship a helper at all. pty-compat.ts repairs it at
13
+ // runtime regardless, so a failure here costs nothing.
14
+ if (process.platform !== "win32") {
15
+ try {
16
+ const require = createRequire(import.meta.url);
17
+ const entry = require.resolve("node-pty");
18
+ const root = join(dirname(entry), "..");
19
+ const candidates = [
20
+ join(root, "prebuilds", `${process.platform}-${process.arch}`, "spawn-helper"),
21
+ join(root, "build", "Release", "spawn-helper"),
22
+ ];
23
+ const helper = candidates.find(existsSync);
24
+ if (helper) {
25
+ const mode = statSync(helper).mode;
26
+ if ((mode & 0o111) === 0) {
27
+ chmodSync(helper, mode | 0o111);
28
+ process.stdout.write(`pines: repaired node-pty helper permissions (${helper})\n`);
29
+ }
30
+ }
31
+ } catch {
32
+ // Deliberately silent — runtime handles it.
33
+ }
34
+ }