portable-agent-layer 0.11.0 → 0.13.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/assets/skills/opinion/SKILL.md +84 -0
- package/{src → assets/skills/opinion}/tools/opinion.ts +11 -27
- package/assets/skills/telos/SKILL.md +42 -8
- package/assets/skills/telos/tools/update-telos.ts +0 -1
- package/assets/templates/AGENTS.md.template +56 -2
- package/assets/templates/PAL/ALGORITHM.md +120 -0
- package/assets/templates/PAL/CONTEXT_ROUTING.md +18 -0
- package/assets/templates/PAL/OPINION_TRACKING.md +1 -1
- package/assets/templates/PAL/README.md +127 -0
- package/assets/templates/PAL/STEERING_RULES.md +25 -1
- package/assets/templates/PAL/SYSTEM_ARCHITECTURE.md +471 -0
- package/assets/templates/pal-settings.json +32 -0
- package/assets/templates/settings.claude.json +14 -14
- package/package.json +4 -4
- package/src/cli/index.ts +7 -0
- package/src/cli/setup-identity.ts +119 -0
- package/src/hooks/handlers/relationship.ts +6 -6
- package/src/hooks/handlers/session-name.ts +19 -20
- package/src/hooks/lib/claude-md.ts +55 -4
- package/src/hooks/lib/context.ts +59 -8
- package/src/hooks/lib/relationship.ts +2 -2
- package/src/hooks/lib/security.ts +1 -0
- package/src/hooks/lib/setup.ts +4 -16
- package/src/targets/claude/install.ts +4 -0
- package/src/targets/lib.ts +17 -0
- package/src/tools/relationship-reflect.ts +5 -5
- package/assets/templates/telos/IDENTITY.md +0 -4
- package/src/cli/install.ts +0 -86
- package/src/cli/uninstall.ts +0 -45
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interactive identity setup — prompts for missing fields in pal-settings.json.
|
|
3
|
+
* Called during `pal install`. Skips fields that already have values.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import * as clack from "@clack/prompts";
|
|
9
|
+
import { palHome } from "../hooks/lib/paths";
|
|
10
|
+
|
|
11
|
+
interface PalSettings {
|
|
12
|
+
identity?: {
|
|
13
|
+
ai?: { name?: string; fullName?: string; displayName?: string; catchphrase?: string };
|
|
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
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Prompt for missing identity fields. Skips any field that already has a value. */
|
|
38
|
+
export async function promptIdentity(): Promise<void> {
|
|
39
|
+
// Skip interactive prompts in non-TTY environments (tests, CI)
|
|
40
|
+
if (!process.stdin.isTTY) return;
|
|
41
|
+
|
|
42
|
+
const settings = readSettings();
|
|
43
|
+
if (!settings.identity) settings.identity = {};
|
|
44
|
+
if (!settings.identity.ai) settings.identity.ai = {};
|
|
45
|
+
if (!settings.identity.principal) settings.identity.principal = {};
|
|
46
|
+
|
|
47
|
+
const ai = settings.identity.ai;
|
|
48
|
+
const principal = settings.identity.principal;
|
|
49
|
+
|
|
50
|
+
// Check if anything is missing
|
|
51
|
+
const needsPrincipal = !principal.name;
|
|
52
|
+
const needsAi = !ai.name;
|
|
53
|
+
const needsCatchphrase = !ai.catchphrase;
|
|
54
|
+
const needsTimezone = !principal.timezone;
|
|
55
|
+
|
|
56
|
+
if (!needsPrincipal && !needsAi && !needsCatchphrase && !needsTimezone) {
|
|
57
|
+
clack.log.info("Identity already configured");
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
clack.intro("Identity Setup");
|
|
62
|
+
|
|
63
|
+
if (needsPrincipal) {
|
|
64
|
+
const name = await clack.text({
|
|
65
|
+
message: "What's your name?",
|
|
66
|
+
placeholder: "e.g. John",
|
|
67
|
+
validate: (v) => (!v || v.length === 0 ? "Name is required" : undefined),
|
|
68
|
+
});
|
|
69
|
+
if (clack.isCancel(name)) {
|
|
70
|
+
clack.cancel("Setup cancelled");
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
principal.name = name;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (needsAi) {
|
|
77
|
+
const name = await clack.text({
|
|
78
|
+
message: "Name your AI",
|
|
79
|
+
defaultValue: "Assistant",
|
|
80
|
+
placeholder: "e.g. Jarvis, Friday, Atlas",
|
|
81
|
+
});
|
|
82
|
+
if (clack.isCancel(name)) {
|
|
83
|
+
clack.cancel("Setup cancelled");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
ai.name = name;
|
|
87
|
+
ai.fullName = `${name} — Personal AI`;
|
|
88
|
+
ai.displayName = name.toUpperCase();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (needsCatchphrase) {
|
|
92
|
+
const catchphrase = await clack.text({
|
|
93
|
+
message: "Startup catchphrase ({name} gets replaced with AI name)",
|
|
94
|
+
defaultValue: "{name} here, ready when you are.",
|
|
95
|
+
placeholder: "{name} online. What's the mission?",
|
|
96
|
+
});
|
|
97
|
+
if (clack.isCancel(catchphrase)) {
|
|
98
|
+
clack.cancel("Setup cancelled");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
ai.catchphrase = catchphrase;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (needsTimezone) {
|
|
105
|
+
const guess = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
106
|
+
const tz = await clack.text({
|
|
107
|
+
message: "Your timezone",
|
|
108
|
+
defaultValue: guess,
|
|
109
|
+
});
|
|
110
|
+
if (clack.isCancel(tz)) {
|
|
111
|
+
clack.cancel("Setup cancelled");
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
principal.timezone = tz;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
writeSettings(settings);
|
|
118
|
+
clack.outro("Identity saved ✓");
|
|
119
|
+
}
|
|
@@ -22,7 +22,7 @@ const OBSERVATION_SCHEMA = {
|
|
|
22
22
|
type: "string",
|
|
23
23
|
enum: ["O", "W", "B"],
|
|
24
24
|
description:
|
|
25
|
-
"O=opinion/preference, W=factual observation, B=
|
|
25
|
+
"O=opinion/preference, W=factual observation, B=biographical (what the AI did this session)",
|
|
26
26
|
},
|
|
27
27
|
text: { type: "string" },
|
|
28
28
|
confidence: { type: "number" },
|
|
@@ -74,11 +74,11 @@ export async function captureRelationship(
|
|
|
74
74
|
logDebug("relationship", "Calling inference...");
|
|
75
75
|
const result = await inference({
|
|
76
76
|
system:
|
|
77
|
-
"You analyze
|
|
78
|
-
"Types: O=opinions/preferences (how
|
|
79
|
-
"B=
|
|
80
|
-
"W=world facts (
|
|
81
|
-
"Focus on: preferences, corrections, frustrations, positive reactions, communication style,
|
|
77
|
+
"You analyze messages from an AI coding session to extract relationship observations. " +
|
|
78
|
+
"Types: O=opinions/preferences (how the user likes to work, what they want), " +
|
|
79
|
+
"B=biographical (what the AI accomplished this session, written in first-person), " +
|
|
80
|
+
"W=world facts (user's situation, projects, tools they use). " +
|
|
81
|
+
"Focus on: preferences, corrections, frustrations, positive reactions, communication style, and session accomplishments. " +
|
|
82
82
|
"Return 0-3 observations. If nothing notable, return empty observations array. Be concise.",
|
|
83
83
|
user: `User messages from this session:\n${userMessages.map((m, i) => `${i + 1}. ${m}`).join("\n")}`,
|
|
84
84
|
maxTokens: 300,
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* This avoids the 1-5s inference latency that previously blocked every first prompt.
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
import { spawn } from "node:child_process";
|
|
13
13
|
import { inference } from "../lib/inference";
|
|
14
14
|
import { logDebug, logError } from "../lib/log";
|
|
15
15
|
import {
|
|
@@ -41,25 +41,24 @@ export async function captureSessionName(
|
|
|
41
41
|
writeSessionName(sessionId, name);
|
|
42
42
|
logDebug("session-name", `Named from prompt: "${name}"`);
|
|
43
43
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
// }
|
|
44
|
+
// Spawn detached background process to upgrade with Haiku inference
|
|
45
|
+
if (!process.env.ANTHROPIC_API_KEY) return;
|
|
46
|
+
try {
|
|
47
|
+
const promptB64 = Buffer.from(message.slice(0, 800)).toString("base64");
|
|
48
|
+
const child = spawn(
|
|
49
|
+
"bun",
|
|
50
|
+
[import.meta.filename, "--upgrade", sessionId, promptB64, name],
|
|
51
|
+
{
|
|
52
|
+
detached: true,
|
|
53
|
+
stdio: "ignore",
|
|
54
|
+
env: { ...process.env, CLAUDECODE: undefined },
|
|
55
|
+
}
|
|
56
|
+
);
|
|
57
|
+
child.unref();
|
|
58
|
+
logDebug("session-name", "Spawned background Haiku upgrade");
|
|
59
|
+
} catch {
|
|
60
|
+
// Non-critical — deterministic name is already stored
|
|
61
|
+
}
|
|
63
62
|
}
|
|
64
63
|
|
|
65
64
|
/**
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
writeFileSync,
|
|
18
18
|
} from "node:fs";
|
|
19
19
|
import { dirname, relative, resolve } from "node:path";
|
|
20
|
-
import { assets, ensureDir, paths, platform } from "./paths";
|
|
20
|
+
import { assets, ensureDir, palHome, paths, platform } from "./paths";
|
|
21
21
|
import { buildSetupPrompt, readSetupState } from "./setup";
|
|
22
22
|
|
|
23
23
|
const TEMPLATE_PATH = assets.agentsMdTemplate();
|
|
@@ -70,8 +70,12 @@ export function needsRebuild(): boolean {
|
|
|
70
70
|
|
|
71
71
|
const outputMtime = statSync(outputPath).mtimeMs;
|
|
72
72
|
|
|
73
|
-
// Collect source files: template + setup.json + PAL docs
|
|
74
|
-
const sources: string[] = [
|
|
73
|
+
// Collect source files: template + setup.json + identity + PAL docs
|
|
74
|
+
const sources: string[] = [
|
|
75
|
+
TEMPLATE_PATH,
|
|
76
|
+
resolve(paths.state(), "setup.json"),
|
|
77
|
+
palSettingsPath(),
|
|
78
|
+
];
|
|
75
79
|
|
|
76
80
|
// Track PAL doc sources for rebuild detection
|
|
77
81
|
const palDocsDir = assets.palDocs();
|
|
@@ -84,6 +88,47 @@ export function needsRebuild(): boolean {
|
|
|
84
88
|
return latestMtime(...sources) > outputMtime;
|
|
85
89
|
}
|
|
86
90
|
|
|
91
|
+
interface Identity {
|
|
92
|
+
ai: { name: string; displayName: string; catchphrase: string };
|
|
93
|
+
principal: { name: string };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const IDENTITY_DEFAULTS: Identity = {
|
|
97
|
+
ai: { name: "Assistant", displayName: "ASSISTANT", catchphrase: "" },
|
|
98
|
+
principal: { name: "" },
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
function palSettingsPath(): string {
|
|
102
|
+
return resolve(palHome(), "memory", "pal-settings.json");
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Load identity from pal-settings.json */
|
|
106
|
+
export function loadIdentity(): Identity {
|
|
107
|
+
const p = palSettingsPath();
|
|
108
|
+
if (!existsSync(p)) return IDENTITY_DEFAULTS;
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const data = JSON.parse(readFileSync(p, "utf-8"));
|
|
112
|
+
const ai = data.identity?.ai ?? {};
|
|
113
|
+
const principal = data.identity?.principal ?? {};
|
|
114
|
+
const name = ai.name || IDENTITY_DEFAULTS.ai.name;
|
|
115
|
+
const catchphrase = (ai.catchphrase || "").replace("{name}", name);
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
ai: {
|
|
119
|
+
name,
|
|
120
|
+
displayName: ai.displayName || IDENTITY_DEFAULTS.ai.displayName,
|
|
121
|
+
catchphrase,
|
|
122
|
+
},
|
|
123
|
+
principal: {
|
|
124
|
+
name: principal.name || IDENTITY_DEFAULTS.principal.name,
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
} catch {
|
|
128
|
+
return IDENTITY_DEFAULTS;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
87
132
|
/** Render AGENTS.md from the template using current state */
|
|
88
133
|
export function buildClaudeMd(): string {
|
|
89
134
|
const template = existsSync(TEMPLATE_PATH)
|
|
@@ -92,8 +137,14 @@ export function buildClaudeMd(): string {
|
|
|
92
137
|
|
|
93
138
|
const state = readSetupState();
|
|
94
139
|
const setupPrompt = state ? buildSetupPrompt(state) : null;
|
|
140
|
+
const identity = loadIdentity();
|
|
95
141
|
|
|
96
|
-
return template
|
|
142
|
+
return template
|
|
143
|
+
.replace("{{SETUP_PROMPT}}", setupPrompt ? `${setupPrompt}\n` : "")
|
|
144
|
+
.replaceAll("{{IDENTITY_NAME}}", identity.ai.name)
|
|
145
|
+
.replaceAll("{{IDENTITY_DISPLAY}}", identity.ai.displayName)
|
|
146
|
+
.replaceAll("{{IDENTITY_CATCHPHRASE}}", identity.ai.catchphrase)
|
|
147
|
+
.replaceAll("{{PRINCIPAL_NAME}}", identity.principal.name);
|
|
97
148
|
}
|
|
98
149
|
|
|
99
150
|
/** 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
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
7
|
+
import { homedir } from "node:os";
|
|
7
8
|
import { resolve } from "node:path";
|
|
8
9
|
import { parse } from "./frontmatter";
|
|
9
10
|
import { readFailures, readLearnings } from "./learning-store";
|
|
@@ -21,6 +22,49 @@ import {
|
|
|
21
22
|
staleProjects,
|
|
22
23
|
} from "./work-tracking";
|
|
23
24
|
|
|
25
|
+
interface PalSettings {
|
|
26
|
+
loadAtStartup?: { files?: string[] };
|
|
27
|
+
dynamicContext?: Record<string, boolean>;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Load pal-settings.json from memory/ */
|
|
31
|
+
function loadPalSettings(): PalSettings {
|
|
32
|
+
const p = resolve(paths.memory(), "pal-settings.json");
|
|
33
|
+
if (!existsSync(p)) return {};
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(readFileSync(p, "utf-8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return {};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Check if a dynamic context section is enabled (defaults to true) */
|
|
42
|
+
function isEnabled(settings: PalSettings, key: string): boolean {
|
|
43
|
+
return settings.dynamicContext?.[key] !== false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Load and concatenate loadAtStartup files */
|
|
47
|
+
function loadStartupFiles(settings: PalSettings): string {
|
|
48
|
+
const files = settings.loadAtStartup?.files;
|
|
49
|
+
if (!files || files.length === 0) return "";
|
|
50
|
+
|
|
51
|
+
const home = homedir();
|
|
52
|
+
const sections: string[] = [];
|
|
53
|
+
|
|
54
|
+
for (const file of files) {
|
|
55
|
+
const resolved = file.replace("~", home);
|
|
56
|
+
if (!existsSync(resolved)) continue;
|
|
57
|
+
try {
|
|
58
|
+
const content = readFileSync(resolved, "utf-8").trim();
|
|
59
|
+
if (content) sections.push(content);
|
|
60
|
+
} catch {
|
|
61
|
+
/* skip unreadable files */
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return sections.join("\n\n---\n\n");
|
|
66
|
+
}
|
|
67
|
+
|
|
24
68
|
/** Count lines in a signals JSONL file */
|
|
25
69
|
export function countSignals(filename: string): number {
|
|
26
70
|
const filepath = resolve(paths.signals(), filename);
|
|
@@ -321,15 +365,22 @@ export function loadRelationshipContext(): string {
|
|
|
321
365
|
* things that change per-session and can't live in a static file.
|
|
322
366
|
*/
|
|
323
367
|
export function buildSystemReminder(): string {
|
|
324
|
-
const
|
|
325
|
-
const
|
|
326
|
-
const
|
|
327
|
-
const
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
const
|
|
368
|
+
const settings = loadPalSettings();
|
|
369
|
+
const startup = loadStartupFiles(settings);
|
|
370
|
+
const work = isEnabled(settings, "activeWork") ? loadActiveWork() : null;
|
|
371
|
+
const wisdom = isEnabled(settings, "wisdom") ? loadWisdomContext() : "";
|
|
372
|
+
const relationship = isEnabled(settings, "relationship")
|
|
373
|
+
? loadRelationshipContext()
|
|
374
|
+
: "";
|
|
375
|
+
const digest = isEnabled(settings, "learningDigest") ? loadLearningDigest() : "";
|
|
376
|
+
const trends = isEnabled(settings, "signalTrends") ? loadSignalTrends() : "";
|
|
377
|
+
const failures = isEnabled(settings, "failurePatterns") ? loadFailurePatterns() : "";
|
|
378
|
+
const synthesis = isEnabled(settings, "synthesis")
|
|
379
|
+
? loadSynthesisRecommendations()
|
|
380
|
+
: "";
|
|
381
|
+
const opinions = isEnabled(settings, "opinions") ? loadOpinionContext() : "";
|
|
332
382
|
const parts: string[] = [];
|
|
383
|
+
if (startup) parts.push(startup);
|
|
333
384
|
if (wisdom) parts.push(wisdom);
|
|
334
385
|
if (opinions) parts.push(opinions);
|
|
335
386
|
if (relationship) parts.push(relationship);
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Notes live at memory/relationship/YYYY-MM/YYYY-MM-DD.md
|
|
6
6
|
* W = world (facts about user's situation)
|
|
7
7
|
* O = opinion (preference with confidence)
|
|
8
|
-
* B =
|
|
8
|
+
* B = biographical (what the AI did this session, first-person)
|
|
9
9
|
*
|
|
10
10
|
* Extraction is handled by the relationship handler via Haiku inference.
|
|
11
11
|
* This lib provides storage and reading utilities only.
|
|
@@ -80,7 +80,7 @@ export function appendNotes(notes: RelationshipNote[], sessionId?: string): void
|
|
|
80
80
|
if (sessionId) lines.push(`<!-- session:${sessionId} -->`);
|
|
81
81
|
|
|
82
82
|
for (const note of fresh) {
|
|
83
|
-
if (
|
|
83
|
+
if (note.type === "O" && note.confidence !== undefined) {
|
|
84
84
|
lines.push(`- ${note.type}(c=${note.confidence}): ${note.text}`);
|
|
85
85
|
} else {
|
|
86
86
|
lines.push(`- ${note.type}: ${note.text}`);
|
package/src/hooks/lib/setup.ts
CHANGED
|
@@ -26,20 +26,8 @@ export interface SetupState {
|
|
|
26
26
|
const SETUP_STEPS: Record<string, Omit<SetupStep, "done">> = {
|
|
27
27
|
mission: {
|
|
28
28
|
file: "telos/MISSION.md",
|
|
29
|
-
question: "What's your
|
|
30
|
-
hint: "Write their
|
|
31
|
-
},
|
|
32
|
-
ai_name: {
|
|
33
|
-
file: "telos/IDENTITY.md",
|
|
34
|
-
question:
|
|
35
|
-
"What would you like to call your AI? (Pick a name — this is how I'll identify myself.)",
|
|
36
|
-
hint: "Write the chosen AI name and identity to telos/IDENTITY.md with fields: name, fullName (name — Personal AI), displayName (UPPERCASED)",
|
|
37
|
-
},
|
|
38
|
-
catchphrase: {
|
|
39
|
-
file: "telos/IDENTITY.md",
|
|
40
|
-
question:
|
|
41
|
-
'What should your AI\'s startup catchphrase be? (e.g. "{name} here, ready to go" — {name} gets replaced with the AI name.)',
|
|
42
|
-
hint: "Append the catchphrase to telos/IDENTITY.md under a ## Catchphrase heading. Support {name} as a placeholder.",
|
|
29
|
+
question: "What do you do? What's your role and core purpose?",
|
|
30
|
+
hint: "Write their role and core purpose to telos/MISSION.md",
|
|
43
31
|
},
|
|
44
32
|
goals: {
|
|
45
33
|
file: "telos/GOALS.md",
|
|
@@ -152,8 +140,8 @@ export function buildSetupPrompt(state: SetupState): string | null {
|
|
|
152
140
|
const lines: string[] = [
|
|
153
141
|
"## IMPORTANT: PAL First-Run Setup Required",
|
|
154
142
|
"",
|
|
155
|
-
"TELOS files are empty —
|
|
156
|
-
"You MUST start the setup process immediately
|
|
143
|
+
"TELOS files are empty — the user's identity is already configured (via the installer),",
|
|
144
|
+
"but personal context is still needed. You MUST start the setup process immediately.",
|
|
157
145
|
"Greet them, explain that PAL needs to learn about them to personalize future sessions,",
|
|
158
146
|
"and ask the first remaining question below. Do NOT wait for the user to ask about setup.",
|
|
159
147
|
"",
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
log,
|
|
20
20
|
mergeSettings,
|
|
21
21
|
readJson,
|
|
22
|
+
scaffoldPalSettings,
|
|
22
23
|
writeJson,
|
|
23
24
|
} from "../lib";
|
|
24
25
|
|
|
@@ -57,6 +58,9 @@ copyAgents();
|
|
|
57
58
|
const palDocsCount = copyPalDocs();
|
|
58
59
|
log.success(`Installed ${palDocsCount} PAL docs to ~/.agents/PAL/`);
|
|
59
60
|
|
|
61
|
+
// --- Scaffold PAL settings ---
|
|
62
|
+
scaffoldPalSettings();
|
|
63
|
+
|
|
60
64
|
// --- Generate ~/.claude/AGENTS.md and symlink ~/.claude/CLAUDE.md → AGENTS.md ---
|
|
61
65
|
regenerateIfNeeded();
|
|
62
66
|
log.success("Generated ~/.config/opencode/AGENTS.md (→ ~/.claude/CLAUDE.md symlink)");
|
package/src/targets/lib.ts
CHANGED
|
@@ -155,6 +155,23 @@ export function scaffoldTelos(): void {
|
|
|
155
155
|
}
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
// --- PAL settings scaffolding ---
|
|
159
|
+
|
|
160
|
+
/** Copy pal-settings.json template to memory/ without overwriting */
|
|
161
|
+
export function scaffoldPalSettings(): void {
|
|
162
|
+
const src = resolve(assets.skills(), "..", "templates", "pal-settings.json");
|
|
163
|
+
if (!existsSync(src)) return;
|
|
164
|
+
|
|
165
|
+
const memDir = resolve(palHome(), "memory");
|
|
166
|
+
mkdirSync(memDir, { recursive: true });
|
|
167
|
+
|
|
168
|
+
const dst = resolve(memDir, "pal-settings.json");
|
|
169
|
+
if (!existsSync(dst)) {
|
|
170
|
+
copyFileSync(src, dst);
|
|
171
|
+
log.info("Created pal-settings.json from template");
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
158
175
|
// --- PAL docs (modular context routing files) ---
|
|
159
176
|
|
|
160
177
|
const PAL_DOCS_DIR = resolve(platform.agentsDir(), "PAL");
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* RelationshipReflect — Periodic reflection on relationship patterns.
|
|
4
4
|
*
|
|
5
5
|
* Reads recent relationship notes and ratings to:
|
|
6
|
-
* - Promote recurring O
|
|
6
|
+
* - Promote recurring O notes into tracked opinions with confidence
|
|
7
7
|
* - Update confidence on existing opinions via supporting evidence
|
|
8
8
|
* - Generate a summary report
|
|
9
9
|
*
|
|
@@ -159,9 +159,9 @@ function promoteToOpinions(notes: ParsedNote[], dryRun: boolean): OpinionChange[
|
|
|
159
159
|
const opinions = readOpinions();
|
|
160
160
|
const lastReflect = getLastReflectDate();
|
|
161
161
|
|
|
162
|
-
// Only O
|
|
162
|
+
// Only O notes become opinions (B=biographical, about the AI, not user preferences)
|
|
163
163
|
const opinionNotes = notes.filter(
|
|
164
|
-
(n) =>
|
|
164
|
+
(n) => n.type === "O" && (!lastReflect || n.date > lastReflect)
|
|
165
165
|
);
|
|
166
166
|
|
|
167
167
|
// Group similar notes together
|
|
@@ -225,7 +225,7 @@ interface OpinionSummary {
|
|
|
225
225
|
}
|
|
226
226
|
|
|
227
227
|
function groupNoteOccurrences(notes: ParsedNote[]): OpinionSummary[] {
|
|
228
|
-
const opNotes = notes.filter((n) => n.type === "O"
|
|
228
|
+
const opNotes = notes.filter((n) => n.type === "O");
|
|
229
229
|
const groups = new Map<
|
|
230
230
|
string,
|
|
231
231
|
{ confidences: number[]; dates: string[]; text: string }
|
|
@@ -397,7 +397,7 @@ if (values.help) {
|
|
|
397
397
|
RelationshipReflect — Periodic reflection + opinion promotion
|
|
398
398
|
|
|
399
399
|
Reads recent relationship notes and ratings. Promotes recurring
|
|
400
|
-
observations (O
|
|
400
|
+
observations (O type) into tracked opinions with confidence scoring.
|
|
401
401
|
|
|
402
402
|
Usage:
|
|
403
403
|
bun run tool:reflect Reflect on last 7 days (default)
|
package/src/cli/install.ts
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PAL — main installer entry point (TypeScript)
|
|
3
|
-
* Usage: bun run install.ts [--claude] [--opencode] [--all]
|
|
4
|
-
* Default: installs for both targets.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { ensureSetupState, isSetupComplete } from "../hooks/lib/setup";
|
|
8
|
-
import { log, scaffoldTelos } from "../targets/lib";
|
|
9
|
-
|
|
10
|
-
// --- Parse args ---
|
|
11
|
-
const args = process.argv.slice(2);
|
|
12
|
-
let installClaude = false;
|
|
13
|
-
let installOpencode = false;
|
|
14
|
-
|
|
15
|
-
if (args.length === 0) {
|
|
16
|
-
installClaude = true;
|
|
17
|
-
installOpencode = true;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
for (const arg of args) {
|
|
21
|
-
if (arg === "--claude") installClaude = true;
|
|
22
|
-
else if (arg === "--opencode") installOpencode = true;
|
|
23
|
-
else if (arg === "--all") {
|
|
24
|
-
installClaude = true;
|
|
25
|
-
installOpencode = true;
|
|
26
|
-
} else if (arg === "--help" || arg === "-h") {
|
|
27
|
-
console.log("Usage: bun run install.ts [--claude] [--opencode] [--all]");
|
|
28
|
-
console.log("");
|
|
29
|
-
console.log(" --claude Install hooks/skills for Claude Code");
|
|
30
|
-
console.log(" --opencode Install context/skills for opencode");
|
|
31
|
-
console.log(" --all Install for both (default)");
|
|
32
|
-
process.exit(0);
|
|
33
|
-
} else {
|
|
34
|
-
log.error(`Unknown option: ${arg}`);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// --- Check bun ---
|
|
40
|
-
if (installClaude) {
|
|
41
|
-
try {
|
|
42
|
-
Bun.version; // always available in bun
|
|
43
|
-
} catch {
|
|
44
|
-
log.error("bun is required: curl -fsSL https://bun.sh/install | bash");
|
|
45
|
-
process.exit(1);
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
console.log("");
|
|
50
|
-
console.log(" ╔═══════════════════════════════════╗");
|
|
51
|
-
console.log(" ║ PAL — Portable Agent Layer ║");
|
|
52
|
-
console.log(" ║ Non-destructive · Modular ║");
|
|
53
|
-
console.log(" ╚═══════════════════════════════════╝");
|
|
54
|
-
console.log("");
|
|
55
|
-
|
|
56
|
-
// --- Scaffold TELOS + seed setup state ---
|
|
57
|
-
scaffoldTelos();
|
|
58
|
-
ensureSetupState();
|
|
59
|
-
|
|
60
|
-
// --- Run target installers ---
|
|
61
|
-
if (installClaude) {
|
|
62
|
-
console.log("━━━ Claude Code ━━━");
|
|
63
|
-
await import("../targets/claude/install");
|
|
64
|
-
console.log("");
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
if (installOpencode) {
|
|
68
|
-
console.log("━━━ opencode ━━━");
|
|
69
|
-
await import("../targets/opencode/install");
|
|
70
|
-
console.log("");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
log.success("Done. Existing config was preserved — only new entries were added.");
|
|
74
|
-
console.log("");
|
|
75
|
-
log.info("Next steps:");
|
|
76
|
-
|
|
77
|
-
const state = ensureSetupState();
|
|
78
|
-
if (!isSetupComplete(state)) {
|
|
79
|
-
log.info(" 1. Start a session — PAL will guide you through first-run setup");
|
|
80
|
-
log.info(" 2. Or fill in telos/*.md manually, then re-run install.ts");
|
|
81
|
-
} else {
|
|
82
|
-
log.info(" 1. Fill in telos/*.md with your info (if not already done)");
|
|
83
|
-
log.info(" 2. Re-run install.ts to regenerate context files");
|
|
84
|
-
}
|
|
85
|
-
log.info(" 3. Add skills by dropping .md files into skills/");
|
|
86
|
-
log.info(" 4. Uninstall: bun run uninstall.ts [--claude] [--opencode]");
|
package/src/cli/uninstall.ts
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PAL — main uninstaller entry point (TypeScript)
|
|
3
|
-
* Usage: bun run uninstall.ts [--claude] [--opencode] [--all]
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { palHome } from "../hooks/lib/paths";
|
|
7
|
-
import { log } from "../targets/lib";
|
|
8
|
-
|
|
9
|
-
const args = process.argv.slice(2);
|
|
10
|
-
let removeClaude = false;
|
|
11
|
-
let removeOpencode = false;
|
|
12
|
-
|
|
13
|
-
if (args.length === 0) {
|
|
14
|
-
removeClaude = true;
|
|
15
|
-
removeOpencode = true;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
for (const arg of args) {
|
|
19
|
-
if (arg === "--claude") removeClaude = true;
|
|
20
|
-
else if (arg === "--opencode") removeOpencode = true;
|
|
21
|
-
else if (arg === "--all") {
|
|
22
|
-
removeClaude = true;
|
|
23
|
-
removeOpencode = true;
|
|
24
|
-
} else if (arg === "--help" || arg === "-h") {
|
|
25
|
-
console.log("Usage: bun run uninstall.ts [--claude] [--opencode] [--all]");
|
|
26
|
-
process.exit(0);
|
|
27
|
-
} else {
|
|
28
|
-
log.error(`Unknown option: ${arg}`);
|
|
29
|
-
process.exit(1);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (removeClaude) {
|
|
34
|
-
console.log("━━━ Claude Code ━━━");
|
|
35
|
-
await import("../targets/claude/uninstall");
|
|
36
|
-
console.log("");
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
if (removeOpencode) {
|
|
40
|
-
console.log("━━━ opencode ━━━");
|
|
41
|
-
await import("../targets/opencode/uninstall");
|
|
42
|
-
console.log("");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
log.success(`PAL uninstalled. Your TELOS, skills, and memory are still in ${palHome()}.`);
|