@lebronj/pi-suite 0.1.12 → 0.1.14
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 +11 -2
- package/extensions/pi-suite-repair.ts +143 -0
- package/package.json +4 -15
- package/scripts/bootstrap.sh +11 -0
- package/skills/pi-skill/SKILL.md +4 -2
- package/vendor/pi-memory/src/service-controller.ts +57 -8
package/README.md
CHANGED
|
@@ -7,12 +7,15 @@ JHP's Pi extension suite for team coding workflows.
|
|
|
7
7
|
```bash
|
|
8
8
|
npm install -g --ignore-scripts @earendil-works/pi-coding-agent
|
|
9
9
|
pi install npm:@lebronj/pi-suite
|
|
10
|
+
pi install npm:pi-mcp-adapter
|
|
11
|
+
pi install npm:pi-subagents
|
|
12
|
+
pi install npm:pi-web-access
|
|
10
13
|
```
|
|
11
14
|
|
|
12
15
|
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
16
|
|
|
14
17
|
```bash
|
|
15
|
-
curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.
|
|
18
|
+
curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.14.tgz | tar -xzO package/scripts/bootstrap.sh | bash
|
|
16
19
|
```
|
|
17
20
|
|
|
18
21
|
## What Is Included
|
|
@@ -22,7 +25,11 @@ curl -fsSL https://registry.npmjs.org/@lebronj/pi-suite/-/pi-suite-0.1.12.tgz |
|
|
|
22
25
|
- Skills: provider checklist, Pi capability reference, image-to-editable-PPT workflow.
|
|
23
26
|
- Vendored package: `@jhp/pi-memory`, including qmd search, external curator service, and memory/skill-draft versioning.
|
|
24
27
|
|
|
25
|
-
|
|
28
|
+
Install the companion packages above with the suite so MCP, subagent, and web tools register from their own package manifests. The bootstrap script installs the same companion packages automatically.
|
|
29
|
+
|
|
30
|
+
Do not add those companion packages inside the `@lebronj/pi-suite` manifest at the same time; loading them both from the suite manifest and as standalone Pi packages creates duplicate tool/flag registration conflicts.
|
|
31
|
+
|
|
32
|
+
Existing users can run `pi update --extensions`; if Pi reports missing suite companion packages on startup, run `/pi-suite-repair` and it will install or refresh the currently required companion package set, then reload resources.
|
|
26
33
|
|
|
27
34
|
Figma is not installed or loaded by default. Enable it only when needed:
|
|
28
35
|
|
|
@@ -98,6 +105,8 @@ qmd embed
|
|
|
98
105
|
|
|
99
106
|
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
107
|
|
|
108
|
+
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.
|
|
109
|
+
|
|
101
110
|
Useful commands:
|
|
102
111
|
|
|
103
112
|
```bash
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { getAgentDir, type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
|
|
5
|
+
interface PackageEntryObject {
|
|
6
|
+
source?: unknown;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
interface SettingsFile {
|
|
10
|
+
packages?: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface RepairRequirement {
|
|
14
|
+
source: string;
|
|
15
|
+
description: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const REQUIRED_PACKAGES: RepairRequirement[] = [
|
|
19
|
+
{ source: "npm:pi-mcp-adapter", description: "MCP tools" },
|
|
20
|
+
{ source: "npm:pi-subagents", description: "subagent delegation" },
|
|
21
|
+
{ source: "npm:pi-web-access", description: "web search/content tools" },
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
function readSettings(path: string): SettingsFile | undefined {
|
|
25
|
+
if (!existsSync(path)) return undefined;
|
|
26
|
+
try {
|
|
27
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as unknown;
|
|
28
|
+
return parsed && typeof parsed === "object" ? (parsed as SettingsFile) : undefined;
|
|
29
|
+
} catch {
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function packageSource(entry: unknown): string | undefined {
|
|
35
|
+
if (typeof entry === "string") return entry;
|
|
36
|
+
if (!entry || typeof entry !== "object") return undefined;
|
|
37
|
+
const source = (entry as PackageEntryObject).source;
|
|
38
|
+
return typeof source === "string" ? source : undefined;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function packageSourcesFromSettings(settings: SettingsFile | undefined): string[] {
|
|
42
|
+
if (!settings || !Array.isArray(settings.packages)) return [];
|
|
43
|
+
return settings.packages.flatMap((entry) => {
|
|
44
|
+
const source = packageSource(entry);
|
|
45
|
+
return source ? [source] : [];
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function npmPackageName(source: string): string | undefined {
|
|
50
|
+
if (!source.startsWith("npm:")) return undefined;
|
|
51
|
+
const spec = source.slice("npm:".length);
|
|
52
|
+
if (spec.startsWith("@")) {
|
|
53
|
+
const slash = spec.indexOf("/");
|
|
54
|
+
if (slash === -1) return undefined;
|
|
55
|
+
const versionStart = spec.indexOf("@", slash + 1);
|
|
56
|
+
return versionStart === -1 ? spec : spec.slice(0, versionStart);
|
|
57
|
+
}
|
|
58
|
+
const versionStart = spec.indexOf("@");
|
|
59
|
+
return versionStart === -1 ? spec : spec.slice(0, versionStart);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function sourceIdentity(source: string): string {
|
|
63
|
+
return npmPackageName(source) ?? source;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isVersionPinnedNpmSource(source: string): boolean {
|
|
67
|
+
if (!source.startsWith("npm:")) return false;
|
|
68
|
+
const name = npmPackageName(source);
|
|
69
|
+
return Boolean(name && source !== `npm:${name}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function requirementSatisfied(requirement: RepairRequirement, installedSources: string[]): boolean {
|
|
73
|
+
if (isVersionPinnedNpmSource(requirement.source)) {
|
|
74
|
+
return installedSources.includes(requirement.source);
|
|
75
|
+
}
|
|
76
|
+
const requiredIdentity = sourceIdentity(requirement.source);
|
|
77
|
+
return installedSources.some((source) => sourceIdentity(source) === requiredIdentity);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function readInstalledPackageSources(ctx: ExtensionContext): string[] {
|
|
81
|
+
const globalSettings = readSettings(join(getAgentDir(), "settings.json"));
|
|
82
|
+
const projectSettings = readSettings(join(ctx.cwd, ".pi", "settings.json"));
|
|
83
|
+
return [...packageSourcesFromSettings(globalSettings), ...packageSourcesFromSettings(projectSettings)];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pendingRequirements(ctx: ExtensionContext): RepairRequirement[] {
|
|
87
|
+
const installedSources = readInstalledPackageSources(ctx);
|
|
88
|
+
return REQUIRED_PACKAGES.filter((requirement) => !requirementSatisfied(requirement, installedSources));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function formatRequirement(requirement: RepairRequirement): string {
|
|
92
|
+
return `${requirement.source} (${requirement.description})`;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export default function piSuiteRepairExtension(pi: ExtensionAPI): void {
|
|
96
|
+
let reminderShown = false;
|
|
97
|
+
|
|
98
|
+
pi.on("session_start", (_event, ctx) => {
|
|
99
|
+
if (!ctx.hasUI || reminderShown) return;
|
|
100
|
+
const pending = pendingRequirements(ctx);
|
|
101
|
+
if (pending.length === 0) return;
|
|
102
|
+
reminderShown = true;
|
|
103
|
+
ctx.ui.notify(
|
|
104
|
+
`pi-suite needs companion package repair: ${pending.map((item) => item.source).join(", ")}. Run /pi-suite-repair, then reload if prompted.`,
|
|
105
|
+
"warning",
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
pi.registerCommand("pi-suite-repair", {
|
|
110
|
+
description: "Install missing required companion Pi packages, then reload resources.",
|
|
111
|
+
handler: async (_args, ctx) => {
|
|
112
|
+
if (!ctx.hasUI) return;
|
|
113
|
+
|
|
114
|
+
const pending = pendingRequirements(ctx);
|
|
115
|
+
if (pending.length === 0) {
|
|
116
|
+
ctx.ui.notify("pi-suite companion packages are already registered. No repair needed.", "info");
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
ctx.ui.notify(
|
|
121
|
+
`Repairing missing pi-suite companion packages: ${pending.map((item) => item.source).join(", ")}`,
|
|
122
|
+
"info",
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const failures: string[] = [];
|
|
126
|
+
for (const requirement of pending) {
|
|
127
|
+
const result = await pi.exec("pi", ["install", requirement.source], { signal: ctx.signal, timeout: 120_000 });
|
|
128
|
+
if (result.code !== 0) {
|
|
129
|
+
const details = (result.stderr || result.stdout || "unknown error").trim();
|
|
130
|
+
failures.push(`${formatRequirement(requirement)}: ${details}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (failures.length > 0) {
|
|
135
|
+
ctx.ui.notify(`pi-suite repair failed: ${failures.join("; ")}`, "error");
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
ctx.ui.notify("pi-suite companion packages are installed. Reloading Pi resources...", "info");
|
|
140
|
+
await ctx.reload();
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lebronj/pi-suite",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"description": "JHP's Pi extension suite for team coding workflows",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": [
|
|
@@ -22,11 +22,6 @@
|
|
|
22
22
|
"README.md",
|
|
23
23
|
"LICENSE"
|
|
24
24
|
],
|
|
25
|
-
"dependencies": {
|
|
26
|
-
"pi-mcp-adapter": "2.9.0",
|
|
27
|
-
"pi-subagents": "0.28.0",
|
|
28
|
-
"pi-web-access": "0.10.7"
|
|
29
|
-
},
|
|
30
25
|
"peerDependencies": {
|
|
31
26
|
"@earendil-works/pi-agent-core": "*",
|
|
32
27
|
"@earendil-works/pi-ai": "*",
|
|
@@ -54,19 +49,13 @@
|
|
|
54
49
|
"pi": {
|
|
55
50
|
"extensions": [
|
|
56
51
|
"./extensions",
|
|
57
|
-
"./vendor/pi-memory/index.ts"
|
|
58
|
-
"node_modules/pi-mcp-adapter/index.ts",
|
|
59
|
-
"node_modules/pi-subagents/src/extension/index.ts",
|
|
60
|
-
"node_modules/pi-web-access/index.ts"
|
|
52
|
+
"./vendor/pi-memory/index.ts"
|
|
61
53
|
],
|
|
62
54
|
"prompts": [
|
|
63
|
-
"./prompts"
|
|
64
|
-
"node_modules/pi-subagents/prompts"
|
|
55
|
+
"./prompts"
|
|
65
56
|
],
|
|
66
57
|
"skills": [
|
|
67
|
-
"./skills"
|
|
68
|
-
"node_modules/pi-subagents/skills",
|
|
69
|
-
"node_modules/pi-web-access/skills"
|
|
58
|
+
"./skills"
|
|
70
59
|
]
|
|
71
60
|
},
|
|
72
61
|
"publishConfig": {
|
package/scripts/bootstrap.sh
CHANGED
|
@@ -83,6 +83,17 @@ NODE
|
|
|
83
83
|
echo "Installing Pi extension suite: $PI_SUITE"
|
|
84
84
|
pi install "$PI_SUITE"
|
|
85
85
|
|
|
86
|
+
COMPANION_PACKAGES=(
|
|
87
|
+
"npm:pi-mcp-adapter"
|
|
88
|
+
"npm:pi-subagents"
|
|
89
|
+
"npm:pi-web-access"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
echo "Installing Pi companion packages..."
|
|
93
|
+
for companion_package in "${COMPANION_PACKAGES[@]}"; do
|
|
94
|
+
pi install "$companion_package"
|
|
95
|
+
done
|
|
96
|
+
|
|
86
97
|
link_if_safe() {
|
|
87
98
|
local source_path="$1"
|
|
88
99
|
local link_path="$2"
|
package/skills/pi-skill/SKILL.md
CHANGED
|
@@ -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`.
|
|
@@ -208,6 +209,7 @@ The pet extension provides a small terminal companion and durable profile.
|
|
|
208
209
|
- `prompt-url-widget.ts`: detects PR/issue prompt templates, fetches GitHub metadata with `gh`, shows a widget, and names the session when possible.
|
|
209
210
|
- `snake.ts`: `/snake` opens a TUI snake game; `Esc` pauses/saves, `q` quits, arrows/WASD move.
|
|
210
211
|
- `tps.ts`: after each assistant run, shows tokens-per-second and token usage details.
|
|
212
|
+
- `pi-suite-repair.ts`: registers `/pi-suite-repair` and startup reminders for missing companion Pi packages declared by the suite.
|
|
211
213
|
- `memory-curator.ts`: deprecated compatibility notice only; external curation is managed by pi-memory service tools.
|
|
212
214
|
|
|
213
215
|
## Skills
|
|
@@ -243,7 +245,7 @@ Global user package configuration is in `~/.pi/agent/settings.json`; project pac
|
|
|
243
245
|
|
|
244
246
|
Current suite package:
|
|
245
247
|
|
|
246
|
-
- `@lebronj/pi-suite`: bundles local extensions, prompts, suite skills, vendored `@jhp/pi-memory
|
|
248
|
+
- `@lebronj/pi-suite`: bundles local extensions, prompts, suite skills, and vendored `@jhp/pi-memory`. The bootstrap script installs `pi-mcp-adapter`, `pi-subagents`, and `pi-web-access` as standalone companion Pi packages. It does not install or load `pi-mono-figma` by default.
|
|
247
249
|
|
|
248
250
|
Common project packages:
|
|
249
251
|
|
|
@@ -259,7 +261,7 @@ Bootstrap behavior:
|
|
|
259
261
|
- Installs global `@earendil-works/pi-coding-agent`.
|
|
260
262
|
- Writes the team OpenAI-compatible provider to `~/.pi/agent/models.json`.
|
|
261
263
|
- Sets default provider/model in `~/.pi/agent/settings.json`.
|
|
262
|
-
- Runs `pi install npm:@lebronj/pi-suite` by default
|
|
264
|
+
- Runs `pi install npm:@lebronj/pi-suite` by default, then installs companion packages with `pi install npm:pi-mcp-adapter`, `pi install npm:pi-subagents`, and `pi install npm:pi-web-access`; this does not install or load `pi-mono-figma`.
|
|
263
265
|
- Prints follow-up instructions for enabling Figma later with `pi install npm:pi-mono-figma` and disabling it with `pi remove npm:pi-mono-figma`.
|
|
264
266
|
- Creates `~/.pi/agent/memory` and links it into the workspace `.pi/memory` when safe.
|
|
265
267
|
- Optionally initializes the local `~/.pi/agent/evolution` repo for memory/skill-draft snapshots; it never writes tokens or enables auto-push.
|
|
@@ -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
|
|
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
|
|
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:
|
|
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
|
|
245
|
-
|
|
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
|
}
|