portable-agent-layer 0.23.1 → 0.24.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/assets/templates/pal-settings.json +2 -1
- package/package.json +1 -1
- package/src/cli/index.ts +6 -0
- package/src/cli/setup-identity.ts +6 -30
- package/src/hooks/handlers/self-model-trigger.ts +27 -0
- package/src/hooks/lib/claude-md.ts +8 -47
- package/src/hooks/lib/context.ts +31 -39
- package/src/hooks/lib/models.ts +1 -0
- package/src/hooks/lib/settings.ts +112 -0
- package/src/hooks/lib/stop.ts +2 -0
- package/src/targets/lib.ts +0 -18
- package/src/tools/self-model.ts +668 -0
- package/src/hooks/handlers/relationship.ts +0 -116
- package/src/hooks/handlers/work-learning.ts +0 -196
- package/src/hooks/setup-check.ts +0 -42
package/package.json
CHANGED
package/src/cli/index.ts
CHANGED
|
@@ -382,6 +382,12 @@ function doctor(silent = false): DoctorResult {
|
|
|
382
382
|
process.env.PAL_GEMINI_API_KEY
|
|
383
383
|
? ok("PAL_GEMINI_API_KEY is set")
|
|
384
384
|
: warn("PAL_GEMINI_API_KEY — not set (optional, for YouTube analysis)");
|
|
385
|
+
process.env.PAL_XAI_API_KEY
|
|
386
|
+
? ok("PAL_XAI_API_KEY is set")
|
|
387
|
+
: warn("PAL_XAI_API_KEY — not set (optional, for Grok researcher)");
|
|
388
|
+
process.env.PAL_PERPLEXITY_API_KEY
|
|
389
|
+
? ok("PAL_PERPLEXITY_API_KEY is set")
|
|
390
|
+
: warn("PAL_PERPLEXITY_API_KEY — not set (optional, for Perplexity researcher)");
|
|
385
391
|
|
|
386
392
|
// Hook health from debug.log
|
|
387
393
|
const hookHealth = checkHookHealth(home);
|
|
@@ -3,43 +3,19 @@
|
|
|
3
3
|
* Called during `pal install`. Skips fields that already have values.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { resolve } from "node:path";
|
|
8
6
|
import * as clack from "@clack/prompts";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
principal?: { name?: string; timezone?: string };
|
|
15
|
-
};
|
|
16
|
-
[key: string]: unknown;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function settingsPath(): string {
|
|
20
|
-
return resolve(palHome(), "memory", "pal-settings.json");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function readSettings(): PalSettings {
|
|
24
|
-
const p = settingsPath();
|
|
25
|
-
if (!existsSync(p)) return {};
|
|
26
|
-
try {
|
|
27
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
28
|
-
} catch {
|
|
29
|
-
return {};
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
function writeSettings(settings: PalSettings): void {
|
|
34
|
-
writeFileSync(settingsPath(), `${JSON.stringify(settings, null, 2)}\n`, "utf-8");
|
|
35
|
-
}
|
|
7
|
+
import {
|
|
8
|
+
type PalSettingsData,
|
|
9
|
+
raw as readSettings,
|
|
10
|
+
write as writeSettings,
|
|
11
|
+
} from "../hooks/lib/settings";
|
|
36
12
|
|
|
37
13
|
/** Prompt for missing identity fields. Skips any field that already has a value. */
|
|
38
14
|
export async function promptIdentity(): Promise<void> {
|
|
39
15
|
// Skip interactive prompts in non-TTY environments (tests, CI)
|
|
40
16
|
if (!process.stdin.isTTY) return;
|
|
41
17
|
|
|
42
|
-
const settings = readSettings();
|
|
18
|
+
const settings: PalSettingsData = { ...readSettings() };
|
|
43
19
|
if (!settings.identity) settings.identity = {};
|
|
44
20
|
if (!settings.identity.ai) settings.identity.ai = {};
|
|
45
21
|
if (!settings.identity.principal) settings.identity.principal = {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-trigger for self-model synthesis — runs daily.
|
|
3
|
+
* writeSelfModel has a 24h TTL guard, so this is safe to call every session.
|
|
4
|
+
* Respects dynamicContext.selfModel — if disabled, skips generation entirely.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { writeSelfModel } from "../../tools/self-model";
|
|
8
|
+
import { logDebug } from "../lib/log";
|
|
9
|
+
import { isEnabled } from "../lib/settings";
|
|
10
|
+
|
|
11
|
+
export async function checkSelfModelTrigger(): Promise<void> {
|
|
12
|
+
if (!isEnabled("selfModel")) {
|
|
13
|
+
logDebug("self-model-trigger", "Disabled in pal-settings.json");
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const result = await writeSelfModel(30);
|
|
19
|
+
if (result.skipped) {
|
|
20
|
+
logDebug("self-model-trigger", "Skipped — last synthesis < 24h ago");
|
|
21
|
+
} else {
|
|
22
|
+
logDebug("self-model-trigger", `Self-model written: ${result.path}`);
|
|
23
|
+
}
|
|
24
|
+
} catch {
|
|
25
|
+
// Non-critical — self-model is best-effort
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -18,7 +18,7 @@ import {
|
|
|
18
18
|
writeFileSync,
|
|
19
19
|
} from "node:fs";
|
|
20
20
|
import { dirname, relative, resolve } from "node:path";
|
|
21
|
-
import { assets, ensureDir,
|
|
21
|
+
import { assets, ensureDir, paths, platform } from "./paths";
|
|
22
22
|
import { buildSetupPrompt, readSetupState } from "./setup";
|
|
23
23
|
|
|
24
24
|
const TEMPLATE_PATH = assets.agentsMdTemplate();
|
|
@@ -89,7 +89,7 @@ export function needsRebuild(): boolean {
|
|
|
89
89
|
const sources: string[] = [
|
|
90
90
|
TEMPLATE_PATH,
|
|
91
91
|
resolve(paths.state(), "setup.json"),
|
|
92
|
-
|
|
92
|
+
resolve(paths.memory(), "pal-settings.json"),
|
|
93
93
|
];
|
|
94
94
|
|
|
95
95
|
// Track PAL doc sources for rebuild detection
|
|
@@ -103,46 +103,7 @@ export function needsRebuild(): boolean {
|
|
|
103
103
|
return latestMtime(...sources) > outputMtime;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
ai: { name: string; displayName: string; catchphrase: string };
|
|
108
|
-
principal: { name: string };
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const IDENTITY_DEFAULTS: Identity = {
|
|
112
|
-
ai: { name: "Assistant", displayName: "ASSISTANT", catchphrase: "" },
|
|
113
|
-
principal: { name: "" },
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
function palSettingsPath(): string {
|
|
117
|
-
return resolve(palHome(), "memory", "pal-settings.json");
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
/** Load identity from pal-settings.json */
|
|
121
|
-
export function loadIdentity(): Identity {
|
|
122
|
-
const p = palSettingsPath();
|
|
123
|
-
if (!existsSync(p)) return IDENTITY_DEFAULTS;
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const data = JSON.parse(readFileSync(p, "utf-8"));
|
|
127
|
-
const ai = data.identity?.ai ?? {};
|
|
128
|
-
const principal = data.identity?.principal ?? {};
|
|
129
|
-
const name = ai.name || IDENTITY_DEFAULTS.ai.name;
|
|
130
|
-
const catchphrase = (ai.catchphrase || "").replace("{name}", name);
|
|
131
|
-
|
|
132
|
-
return {
|
|
133
|
-
ai: {
|
|
134
|
-
name,
|
|
135
|
-
displayName: ai.displayName || IDENTITY_DEFAULTS.ai.displayName,
|
|
136
|
-
catchphrase,
|
|
137
|
-
},
|
|
138
|
-
principal: {
|
|
139
|
-
name: principal.name || IDENTITY_DEFAULTS.principal.name,
|
|
140
|
-
},
|
|
141
|
-
};
|
|
142
|
-
} catch {
|
|
143
|
-
return IDENTITY_DEFAULTS;
|
|
144
|
-
}
|
|
145
|
-
}
|
|
106
|
+
import { identity } from "./settings";
|
|
146
107
|
|
|
147
108
|
/** Render AGENTS.md from the template using current state */
|
|
148
109
|
export function buildClaudeMd(): string {
|
|
@@ -152,14 +113,14 @@ export function buildClaudeMd(): string {
|
|
|
152
113
|
|
|
153
114
|
const state = readSetupState();
|
|
154
115
|
const setupPrompt = state ? buildSetupPrompt(state) : null;
|
|
155
|
-
const
|
|
116
|
+
const id = identity();
|
|
156
117
|
|
|
157
118
|
return template
|
|
158
119
|
.replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "")
|
|
159
|
-
.replaceAll("{{IDENTITY_NAME}}",
|
|
160
|
-
.replaceAll("{{IDENTITY_DISPLAY}}",
|
|
161
|
-
.replaceAll("{{IDENTITY_CATCHPHRASE}}",
|
|
162
|
-
.replaceAll("{{PRINCIPAL_NAME}}",
|
|
120
|
+
.replaceAll("{{IDENTITY_NAME}}", id.ai.name)
|
|
121
|
+
.replaceAll("{{IDENTITY_DISPLAY}}", id.ai.displayName)
|
|
122
|
+
.replaceAll("{{IDENTITY_CATCHPHRASE}}", id.ai.catchphrase)
|
|
123
|
+
.replaceAll("{{PRINCIPAL_NAME}}", id.principal.name);
|
|
163
124
|
}
|
|
164
125
|
|
|
165
126
|
/** Regenerate AGENTS.md if any source file is newer, and ensure CLAUDE.md symlink exists. Returns true if rebuilt. */
|
package/src/hooks/lib/context.ts
CHANGED
|
@@ -12,36 +12,16 @@ import { loadOpinionContext } from "./opinions";
|
|
|
12
12
|
import { paths } from "./paths";
|
|
13
13
|
import { loadRecentNotes } from "./relationship";
|
|
14
14
|
import { readSessionNames } from "./session-names";
|
|
15
|
+
import * as settings from "./settings";
|
|
15
16
|
import { buildSetupPrompt, readSetupState, remainingSteps, STEP_ORDER } from "./setup";
|
|
16
17
|
import { computeSignalTrends, formatTrends } from "./signal-trends";
|
|
17
18
|
import { readFramePrinciples } from "./wisdom";
|
|
18
19
|
import { readProjectHistory, readSessions, recentSessions } from "./work-tracking";
|
|
19
20
|
|
|
20
|
-
interface PalSettings {
|
|
21
|
-
loadAtStartup?: { files?: string[] };
|
|
22
|
-
dynamicContext?: Record<string, boolean>;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
/** Load pal-settings.json from memory/ */
|
|
26
|
-
function loadPalSettings(): PalSettings {
|
|
27
|
-
const p = resolve(paths.memory(), "pal-settings.json");
|
|
28
|
-
if (!existsSync(p)) return {};
|
|
29
|
-
try {
|
|
30
|
-
return JSON.parse(readFileSync(p, "utf-8"));
|
|
31
|
-
} catch {
|
|
32
|
-
return {};
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Check if a dynamic context section is enabled (defaults to true) */
|
|
37
|
-
function isEnabled(settings: PalSettings, key: string): boolean {
|
|
38
|
-
return settings.dynamicContext?.[key] !== false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
21
|
/** Load and concatenate loadAtStartup files */
|
|
42
|
-
function loadStartupFiles(
|
|
43
|
-
const files = settings.
|
|
44
|
-
if (
|
|
22
|
+
function loadStartupFiles(): string {
|
|
23
|
+
const files = settings.startupFiles();
|
|
24
|
+
if (files.length === 0) return "";
|
|
45
25
|
|
|
46
26
|
const home = homedir();
|
|
47
27
|
const sections: string[] = [];
|
|
@@ -223,6 +203,19 @@ export function loadLearningDigest(): string {
|
|
|
223
203
|
}
|
|
224
204
|
}
|
|
225
205
|
|
|
206
|
+
/** Load self-model for session context injection */
|
|
207
|
+
export function loadSelfModel(): string {
|
|
208
|
+
try {
|
|
209
|
+
const p = resolve(paths.memory(), "self-model", "current.md");
|
|
210
|
+
if (!existsSync(p)) return "";
|
|
211
|
+
const content = readFileSync(p, "utf-8").trim();
|
|
212
|
+
if (!content) return "";
|
|
213
|
+
return content;
|
|
214
|
+
} catch {
|
|
215
|
+
return "";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
226
219
|
/** Load 5 most recent failure contexts as an "avoid" list */
|
|
227
220
|
export function loadFailurePatterns(): string {
|
|
228
221
|
try {
|
|
@@ -452,30 +445,29 @@ export function loadHandoff(): string {
|
|
|
452
445
|
* things that change per-session and can't live in a static file.
|
|
453
446
|
*/
|
|
454
447
|
export function buildSystemReminder(): string {
|
|
455
|
-
const
|
|
456
|
-
const
|
|
457
|
-
const
|
|
458
|
-
const
|
|
459
|
-
const relationship = isEnabled(settings, "relationship")
|
|
448
|
+
const startup = loadStartupFiles();
|
|
449
|
+
const work = settings.isEnabled("activeWork") ? loadActiveWork() : null;
|
|
450
|
+
const wisdom = settings.isEnabled("wisdom") ? loadWisdomContext() : "";
|
|
451
|
+
const relationship = settings.isEnabled("relationship")
|
|
460
452
|
? loadRelationshipContext()
|
|
461
453
|
: "";
|
|
462
|
-
const digest = isEnabled(
|
|
463
|
-
const projectHistory = isEnabled(
|
|
454
|
+
const digest = settings.isEnabled("learningDigest") ? loadLearningDigest() : "";
|
|
455
|
+
const projectHistory = settings.isEnabled("projectHistory")
|
|
464
456
|
? loadProjectHistoryContext()
|
|
465
457
|
: "";
|
|
466
|
-
const trends = isEnabled(
|
|
467
|
-
const failures = isEnabled(
|
|
468
|
-
const synthesis = isEnabled(
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
const
|
|
472
|
-
const intelligence = isEnabled(settings, "sessionIntelligence")
|
|
458
|
+
const trends = settings.isEnabled("signalTrends") ? loadSignalTrends() : "";
|
|
459
|
+
const failures = settings.isEnabled("failurePatterns") ? loadFailurePatterns() : "";
|
|
460
|
+
const synthesis = settings.isEnabled("synthesis") ? loadSynthesisRecommendations() : "";
|
|
461
|
+
const opinions = settings.isEnabled("opinions") ? loadOpinionContext() : "";
|
|
462
|
+
const selfModel = settings.isEnabled("selfModel") ? loadSelfModel() : "";
|
|
463
|
+
const intelligence = settings.isEnabled("sessionIntelligence")
|
|
473
464
|
? loadSessionIntelligence()
|
|
474
465
|
: "";
|
|
475
|
-
const handoff = isEnabled(
|
|
466
|
+
const handoff = settings.isEnabled("handoff") ? loadHandoff() : "";
|
|
476
467
|
const parts: string[] = [];
|
|
477
468
|
if (startup) parts.push(startup);
|
|
478
469
|
if (handoff) parts.push(handoff);
|
|
470
|
+
if (selfModel) parts.push(selfModel);
|
|
479
471
|
if (wisdom) parts.push(wisdom);
|
|
480
472
|
if (opinions) parts.push(opinions);
|
|
481
473
|
if (intelligence) parts.push(intelligence);
|
package/src/hooks/lib/models.ts
CHANGED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PalSettings — single source of truth for pal-settings.json.
|
|
3
|
+
*
|
|
4
|
+
* Reads once, caches in memory for the process lifetime.
|
|
5
|
+
* All consumers import from here instead of reading the file directly.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { paths } from "./paths";
|
|
11
|
+
|
|
12
|
+
// ── Types ──
|
|
13
|
+
|
|
14
|
+
export interface Identity {
|
|
15
|
+
ai: { name: string; fullName: string; displayName: string; catchphrase: string };
|
|
16
|
+
principal: { name: string; timezone: string };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PalSettingsData {
|
|
20
|
+
identity?: {
|
|
21
|
+
ai?: { name?: string; fullName?: string; displayName?: string; catchphrase?: string };
|
|
22
|
+
principal?: { name?: string; timezone?: string };
|
|
23
|
+
};
|
|
24
|
+
loadAtStartup?: { files?: string[] };
|
|
25
|
+
dynamicContext?: Record<string, boolean>;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const IDENTITY_DEFAULTS: Identity = {
|
|
30
|
+
ai: {
|
|
31
|
+
name: "Assistant",
|
|
32
|
+
fullName: "AI Assistant",
|
|
33
|
+
displayName: "ASSISTANT",
|
|
34
|
+
catchphrase: "",
|
|
35
|
+
},
|
|
36
|
+
principal: { name: "User", timezone: "" },
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
// ── Singleton ──
|
|
40
|
+
|
|
41
|
+
let cached: PalSettingsData | null = null;
|
|
42
|
+
|
|
43
|
+
function settingsPath(): string {
|
|
44
|
+
return resolve(paths.memory(), "pal-settings.json");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function load(): PalSettingsData {
|
|
48
|
+
if (cached) return cached;
|
|
49
|
+
const p = settingsPath();
|
|
50
|
+
if (!existsSync(p)) {
|
|
51
|
+
cached = {};
|
|
52
|
+
return cached;
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
cached = JSON.parse(readFileSync(p, "utf-8")) as PalSettingsData;
|
|
56
|
+
return cached;
|
|
57
|
+
} catch {
|
|
58
|
+
cached = {};
|
|
59
|
+
return cached;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Force re-read from disk (useful after writes) */
|
|
64
|
+
export function reload(): PalSettingsData {
|
|
65
|
+
cached = null;
|
|
66
|
+
return load();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Public API ──
|
|
70
|
+
|
|
71
|
+
/** Get the raw settings data */
|
|
72
|
+
export function raw(): PalSettingsData {
|
|
73
|
+
return load();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Get resolved identity with defaults */
|
|
77
|
+
export function identity(): Identity {
|
|
78
|
+
const data = load();
|
|
79
|
+
const ai = data.identity?.ai ?? {};
|
|
80
|
+
const principal = data.identity?.principal ?? {};
|
|
81
|
+
const name = ai.name || IDENTITY_DEFAULTS.ai.name;
|
|
82
|
+
const catchphrase = (ai.catchphrase || "").replace("{name}", name);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
ai: {
|
|
86
|
+
name,
|
|
87
|
+
fullName: ai.fullName || IDENTITY_DEFAULTS.ai.fullName,
|
|
88
|
+
displayName: ai.displayName || IDENTITY_DEFAULTS.ai.displayName,
|
|
89
|
+
catchphrase,
|
|
90
|
+
},
|
|
91
|
+
principal: {
|
|
92
|
+
name: principal.name || IDENTITY_DEFAULTS.principal.name,
|
|
93
|
+
timezone: principal.timezone || IDENTITY_DEFAULTS.principal.timezone,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/** Check if a dynamic context section is enabled (defaults to true) */
|
|
99
|
+
export function isEnabled(key: string): boolean {
|
|
100
|
+
return load().dynamicContext?.[key] !== false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Get the loadAtStartup file list */
|
|
104
|
+
export function startupFiles(): string[] {
|
|
105
|
+
return load().loadAtStartup?.files ?? [];
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Write settings back to disk and bust cache */
|
|
109
|
+
export function write(data: PalSettingsData): void {
|
|
110
|
+
writeFileSync(settingsPath(), `${JSON.stringify(data, null, 2)}\n`, "utf-8");
|
|
111
|
+
cached = null;
|
|
112
|
+
}
|
package/src/hooks/lib/stop.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { resolve } from "node:path";
|
|
|
8
8
|
import { autoBackup } from "../handlers/backup";
|
|
9
9
|
import { captureFailure } from "../handlers/failure";
|
|
10
10
|
import { checkReflectTrigger } from "../handlers/reflect-trigger";
|
|
11
|
+
import { checkSelfModelTrigger } from "../handlers/self-model-trigger";
|
|
11
12
|
import { captureSessionIntelligence } from "../handlers/session-intelligence";
|
|
12
13
|
import { runSynthesis } from "../handlers/synthesis";
|
|
13
14
|
import { resetTab } from "../handlers/tab";
|
|
@@ -45,6 +46,7 @@ export async function runStopHandlers(
|
|
|
45
46
|
updateCounts(),
|
|
46
47
|
autoBackup(),
|
|
47
48
|
checkReflectTrigger(),
|
|
49
|
+
checkSelfModelTrigger(),
|
|
48
50
|
runSynthesis(),
|
|
49
51
|
]);
|
|
50
52
|
|
package/src/targets/lib.ts
CHANGED
|
@@ -660,21 +660,3 @@ export function countMd(dir: string): number {
|
|
|
660
660
|
return 0;
|
|
661
661
|
}
|
|
662
662
|
}
|
|
663
|
-
|
|
664
|
-
/** Read skill frontmatter field */
|
|
665
|
-
export function readSkillField(skillPath: string, field: string): string {
|
|
666
|
-
try {
|
|
667
|
-
const content = readFileSync(skillPath, "utf-8");
|
|
668
|
-
const match = content.match(new RegExp(`^${field}:\\s*(.+)$`, "m"));
|
|
669
|
-
return match?.[1]?.trim() ?? "";
|
|
670
|
-
} catch {
|
|
671
|
-
return "";
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/** Strip frontmatter from a skill file (content after second ---) */
|
|
676
|
-
export function skillBody(skillPath: string): string {
|
|
677
|
-
const content = readFileSync(skillPath, "utf-8");
|
|
678
|
-
const parts = content.split(/^---\s*$/m);
|
|
679
|
-
return parts.length >= 3 ? parts.slice(2).join("---").trim() : content.trim();
|
|
680
|
-
}
|