@lebronj/pi-suite 0.1.12 → 0.1.13

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/README.md CHANGED
@@ -12,7 +12,7 @@ pi install npm:@lebronj/pi-suite
12
12
  Or use the bootstrap script to install Pi, configure the team OpenAI-compatible endpoint, install this suite, and set up Bun + qmd for memory search:
13
13
 
14
14
  ```bash
15
- curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.12.tgz | tar -xzO package/scripts/bootstrap.sh | bash
15
+ curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.13.tgz | tar -xzO package/scripts/bootstrap.sh | bash
16
16
  ```
17
17
 
18
18
  ## What Is Included
@@ -98,6 +98,8 @@ qmd embed
98
98
 
99
99
  Memory versioning is enabled by default. It snapshots `~/.pi/agent/memory` and `~/.pi/agent/skill-drafts` into `~/.pi/agent/evolution`, commits local changes automatically, and leaves push manual by default. `memory_curate` also scans yesterday's daily log into `REVIEW.md` when learning is enabled and the daily file changed since the last scan.
100
100
 
101
+ The external memory curator service uses a systemd user timer when available, with cron fallback. When the service points at a vendored TypeScript CLI under `node_modules`, the launcher uses Bun or tsx instead of plain Node so Node 22 can run it reliably.
102
+
101
103
  Useful commands:
102
104
 
103
105
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lebronj/pi-suite",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
4
4
  "description": "JHP's Pi extension suite for team coding workflows",
5
5
  "type": "module",
6
6
  "keywords": [
@@ -95,6 +95,7 @@ External curator service:
95
95
 
96
96
  - This is the main self-evolution maintenance loop: it can run daily outside the pi process, even when pi is closed.
97
97
  - It uses a systemd user timer when available, with cron fallback.
98
+ - For vendored TypeScript CLI paths under `node_modules`, the service launcher uses Bun or tsx instead of plain Node so Node 22 does not fail on TypeScript type stripping.
98
99
  - On `session_start` and after `/reload`, pi-memory checks service status. If the service is disabled and UI is available, it shows a startup hint with enable/status/disable commands.
99
100
  - Enable with `/memory-curator-enable 03:00` or ask the agent to call `memory_curator_enable`.
100
101
  - Inspect with `/memory-curator-status` or `memory_curator_status`.
@@ -1,5 +1,5 @@
1
1
  import { execFileSync, spawnSync } from "node:child_process";
2
- import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { accessSync, constants, existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
3
3
  import { homedir } from "node:os";
4
4
  import { dirname, join } from "node:path";
5
5
 
@@ -79,8 +79,30 @@ function writeState(state: CuratorServiceState): void {
79
79
  writeFileSync(statePath(state.memoryDir), `${JSON.stringify(state, null, 2)}\n`, "utf-8");
80
80
  }
81
81
 
82
+ function commandPath(command: string): string | undefined {
83
+ if (command.includes("/")) {
84
+ try {
85
+ accessSync(command, constants.X_OK);
86
+ return command;
87
+ } catch {
88
+ return undefined;
89
+ }
90
+ }
91
+ for (const dir of (process.env.PATH || "").split(":")) {
92
+ if (!dir) continue;
93
+ const candidate = join(dir, command);
94
+ try {
95
+ accessSync(candidate, constants.X_OK);
96
+ return candidate;
97
+ } catch {
98
+ // keep searching PATH
99
+ }
100
+ }
101
+ return undefined;
102
+ }
103
+
82
104
  function hasCommand(command: string): boolean {
83
- return spawnSync(command, ["--version"], { stdio: "ignore" }).status === 0;
105
+ return commandPath(command) !== undefined || spawnSync(command, ["--version"], { stdio: "ignore" }).status === 0;
84
106
  }
85
107
 
86
108
  function canUseSystemdUser(): boolean {
@@ -93,6 +115,29 @@ function shellQuote(value: string): string {
93
115
  return `'${value.replace(/'/g, `'"'"'`)}'`;
94
116
  }
95
117
 
118
+ function systemdQuote(value: string): string {
119
+ if (/^[A-Za-z0-9_@%+=:,./-]+$/.test(value)) return value;
120
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\$/g, "\\$")}"`;
121
+ }
122
+
123
+ type CliInvocation = {
124
+ command: string;
125
+ args: string[];
126
+ };
127
+
128
+ function resolveCliInvocation(cliPath: string): CliInvocation {
129
+ const normalized = cliPath.replace(/\\/g, "/");
130
+ const isNodeModulesTypeScript = normalized.endsWith(".ts") && normalized.includes("/node_modules/");
131
+ if (!isNodeModulesTypeScript) return { command: process.execPath, args: [cliPath] };
132
+
133
+ const bunPath = commandPath("bun");
134
+ if (bunPath) return { command: bunPath, args: [cliPath] };
135
+ const tsxPath = commandPath("tsx");
136
+ if (tsxPath) return { command: tsxPath, args: [cliPath] };
137
+
138
+ throw new Error(`Cannot install memory curator service: Node cannot run TypeScript files under node_modules (${cliPath}). Install Bun or tsx, or publish a compiled JavaScript curator CLI.`);
139
+ }
140
+
96
141
  function parseSchedule(schedule: string): { hour: string; minute: string } {
97
142
  const match = /^(\d{1,2}):(\d{2})$/.exec(schedule);
98
143
  if (!match) throw new Error(`Invalid schedule '${schedule}'. Expected HH:MM.`);
@@ -109,7 +154,8 @@ function systemdCalendar(schedule: string): string {
109
154
 
110
155
  function writeSystemdUnits(memoryDir: string, cliPath: string, schedule: string): void {
111
156
  mkdirSync(systemdUserDir(), { recursive: true });
112
- const execStart = `${process.execPath} ${cliPath} run-once --memory-dir ${memoryDir} --reason systemd-timer`;
157
+ const invocation = resolveCliInvocation(cliPath);
158
+ const execStart = [invocation.command, ...invocation.args, "run-once", "--memory-dir", memoryDir, "--reason", "systemd-timer"].map(systemdQuote).join(" ");
113
159
  writeFileSync(
114
160
  servicePath(),
115
161
  [
@@ -187,9 +233,10 @@ function enableCron(memoryDir: string, cliPath: string, schedule: string): void
187
233
  if (!hasCommand("crontab")) throw new Error("Neither systemd user timers nor crontab are available.");
188
234
  const { hour, minute } = parseSchedule(schedule);
189
235
  removeCronLine();
190
- const command = `${shellQuote(process.execPath)} ${shellQuote(cliPath)} run-once --memory-dir ${shellQuote(memoryDir)} --reason cron ${CRON_MARKER}`;
236
+ const invocation = resolveCliInvocation(cliPath);
237
+ const command = [invocation.command, ...invocation.args, "run-once", "--memory-dir", memoryDir, "--reason", "cron"].map(shellQuote).join(" ");
191
238
  const existing = currentCrontab().trim();
192
- const next = `${existing ? `${existing}\n` : ""}${minute} ${hour} * * * ${command}\n`;
239
+ const next = `${existing ? `${existing}\n` : ""}${minute} ${hour} * * * ${command} ${CRON_MARKER}\n`;
193
240
  installCrontab(next);
194
241
  }
195
242
 
@@ -202,7 +249,7 @@ export function enableCuratorService(options: { memoryDir?: string; cliPath: str
202
249
  enableSystemd(memoryDir, options.cliPath, schedule);
203
250
  const state: CuratorServiceState = { ...baseState, enabled: true, backend: "systemd-user", installedAt: new Date().toISOString() };
204
251
  writeState(state);
205
- return { ok: true, backend: "systemd-user", message: "Enabled systemd user timer for daily 03:00 memory curation.", state };
252
+ return { ok: true, backend: "systemd-user", message: `Enabled systemd user timer for daily ${schedule} memory curation.`, state };
206
253
  }
207
254
  enableCron(memoryDir, options.cliPath, schedule);
208
255
  const state: CuratorServiceState = { ...baseState, enabled: true, backend: "cron", installedAt: new Date().toISOString() };
@@ -241,8 +288,10 @@ export function getCuratorServiceStatus(options: { memoryDir?: string; cliPath:
241
288
  ];
242
289
  if (state.lastError) parts.push(`Last error: ${state.lastError}`);
243
290
  if (state.backend === "systemd-user" && hasCommand("systemctl")) {
244
- const active = spawnSync("systemctl", ["--user", "is-active", `${SERVICE_NAME}.timer`], { encoding: "utf-8" });
245
- parts.push(`systemd timer active: ${active.stdout.trim() || "unknown"}`);
291
+ const timerActive = spawnSync("systemctl", ["--user", "is-active", `${SERVICE_NAME}.timer`], { encoding: "utf-8" });
292
+ const serviceActive = spawnSync("systemctl", ["--user", "is-active", `${SERVICE_NAME}.service`], { encoding: "utf-8" });
293
+ parts.push(`systemd timer active: ${timerActive.stdout.trim() || "unknown"}`);
294
+ parts.push(`systemd service active: ${serviceActive.stdout.trim() || "unknown"}`);
246
295
  }
247
296
  return { ok: true, backend: state.backend, message: parts.join("\n"), state };
248
297
  }