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.
@@ -1,116 +0,0 @@
1
- /**
2
- * Stop handler: extracts relationship observations via Haiku inference.
3
- * Only runs on substantial sessions (≥10 messages).
4
- * Dedup: skips if this session already has notes in today's file.
5
- */
6
-
7
- import { inference } from "../lib/inference";
8
- import { logDebug, logError } from "../lib/log";
9
- import { appendNotes, hasSessionNotes, type RelationshipNote } from "../lib/relationship";
10
- import { logTokenUsage } from "../lib/token-usage";
11
- import { extractContent, parseMessages } from "../lib/transcript";
12
-
13
- const OBSERVATION_SCHEMA = {
14
- type: "object",
15
- properties: {
16
- observations: {
17
- type: "array",
18
- items: {
19
- type: "object",
20
- properties: {
21
- type: {
22
- type: "string",
23
- enum: ["O", "W", "B"],
24
- description:
25
- "O=opinion/preference, W=factual observation, B=biographical (what the AI did this session)",
26
- },
27
- text: { type: "string" },
28
- confidence: { type: "number" },
29
- },
30
- required: ["type", "text", "confidence"],
31
- additionalProperties: false,
32
- },
33
- },
34
- },
35
- required: ["observations"],
36
- additionalProperties: false,
37
- } as const;
38
-
39
- export async function captureRelationship(
40
- transcript: string,
41
- sessionId?: string
42
- ): Promise<void> {
43
- if (sessionId && hasSessionNotes(sessionId)) {
44
- logDebug("relationship", "Skipped: session already has notes");
45
- return;
46
- }
47
-
48
- const messages = parseMessages(transcript);
49
- logDebug("relationship", `Messages: ${messages.length}`);
50
- if (messages.length < 10) {
51
- logDebug("relationship", "Skipped: < 10 messages");
52
- return;
53
- }
54
-
55
- if (!process.env.PAL_ANTHROPIC_API_KEY) {
56
- logDebug("relationship", "Skipped: no PAL_ANTHROPIC_API_KEY");
57
- return;
58
- }
59
-
60
- // Collect user messages for analysis
61
- const userMessages = messages
62
- .filter((m) => m.role === "user")
63
- .map((m) => extractContent(m))
64
- .filter((t) => t.length > 0)
65
- .slice(-15)
66
- .map((t) => t.slice(0, 200));
67
-
68
- logDebug("relationship", `User messages: ${userMessages.length}`);
69
- if (userMessages.length < 3) {
70
- logDebug("relationship", "Skipped: < 3 user messages");
71
- return;
72
- }
73
-
74
- logDebug("relationship", "Calling inference...");
75
- const result = await inference({
76
- system:
77
- "You analyze messages from an AI assistant 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
- "Return 0-3 observations. If nothing notable, return empty observations array. Be concise.",
83
- user: `User messages from this session:\n${userMessages.map((m, i) => `${i + 1}. ${m}`).join("\n")}`,
84
- maxTokens: 300,
85
- timeout: 8000,
86
- jsonSchema: OBSERVATION_SCHEMA,
87
- });
88
-
89
- if (result.usage) logTokenUsage("relationship", result.usage);
90
-
91
- logDebug("relationship", `Inference result: success=${result.success}`);
92
- if (!result.success || !result.output) {
93
- logDebug("relationship", "Skipped: inference failed or empty output");
94
- return;
95
- }
96
-
97
- try {
98
- const parsed = JSON.parse(result.output) as {
99
- observations: Array<{ type: "O" | "W" | "B"; text: string; confidence: number }>;
100
- };
101
-
102
- logDebug("relationship", `Parsed ${parsed.observations?.length ?? 0} observations`);
103
- if (!parsed.observations || parsed.observations.length === 0) return;
104
-
105
- const notes: RelationshipNote[] = parsed.observations.map((o) => ({
106
- type: o.type,
107
- text: o.text,
108
- confidence: o.confidence,
109
- }));
110
-
111
- appendNotes(notes, sessionId);
112
- logDebug("relationship", `Captured ${notes.length} observations via inference`);
113
- } catch (err) {
114
- logError("relationship", err);
115
- }
116
- }
@@ -1,196 +0,0 @@
1
- /**
2
- * Stop handler: writes a structured learning file for significant sessions.
3
- * Dedup: only writes once per session ID (tracks in a marker file).
4
- * Threshold: >2000 chars transcript + at least 6 messages.
5
- * Output: memory/learning/session/YYYY-MM/{datetime}_work_{slug}.md
6
- */
7
-
8
- import { existsSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
9
- import { resolve } from "node:path";
10
- import { stringify } from "../lib/frontmatter";
11
- import { inference } from "../lib/inference";
12
- import { categorizeLearning } from "../lib/learning-category";
13
- import { ensureDir, paths } from "../lib/paths";
14
- import { fileTimestamp, monthPath } from "../lib/time";
15
- import { logTokenUsage } from "../lib/token-usage";
16
- import {
17
- extractContent,
18
- extractLastAssistant,
19
- extractLastUser,
20
- parseMessages,
21
- } from "../lib/transcript";
22
- import { appendProjectHistory } from "../lib/work-tracking";
23
-
24
- function slugify(text: string): string {
25
- return text
26
- .toLowerCase()
27
- .replace(/[^a-z0-9\s]/g, "")
28
- .trim()
29
- .split(/\s+/)
30
- .slice(0, 4)
31
- .join("-");
32
- }
33
-
34
- const MIN_NEW_MESSAGES = 10;
35
-
36
- interface CaptureEntry {
37
- filepath: string;
38
- messageCount: number;
39
- }
40
-
41
- /** Get the previously captured entry for a session, if any */
42
- function getPreviousCapture(sessionId: string): CaptureEntry | null {
43
- const markerPath = resolve(paths.state(), "captured-learnings.json");
44
- if (!existsSync(markerPath)) return null;
45
- try {
46
- const raw = JSON.parse(readFileSync(markerPath, "utf-8"));
47
- if (Array.isArray(raw)) return null;
48
- const entry = raw[sessionId];
49
- if (!entry) return null;
50
- // Migrate old string format
51
- if (typeof entry === "string") return { filepath: entry, messageCount: 0 };
52
- return entry as CaptureEntry;
53
- } catch {
54
- return null;
55
- }
56
- }
57
-
58
- function markCaptured(sessionId: string, filepath: string, messageCount: number): void {
59
- const markerPath = resolve(paths.state(), "captured-learnings.json");
60
- let data: Record<string, CaptureEntry> = {};
61
- try {
62
- if (existsSync(markerPath)) {
63
- const raw = JSON.parse(readFileSync(markerPath, "utf-8"));
64
- if (!Array.isArray(raw) && typeof raw === "object") {
65
- for (const [k, v] of Object.entries(raw)) {
66
- if (typeof v === "string") {
67
- data[k] = { filepath: v, messageCount: 0 };
68
- } else {
69
- data[k] = v as CaptureEntry;
70
- }
71
- }
72
- }
73
- }
74
- } catch {
75
- /* start fresh */
76
- }
77
- data[sessionId] = { filepath, messageCount };
78
- // Keep last 50
79
- const entries = Object.entries(data);
80
- if (entries.length > 50) {
81
- data = Object.fromEntries(entries.slice(-50));
82
- }
83
- writeFileSync(markerPath, JSON.stringify(data, null, 2), "utf-8");
84
- }
85
-
86
- export async function captureWorkLearning(
87
- transcript: string,
88
- sessionId?: string
89
- ): Promise<void> {
90
- if (transcript.length < 2000) return;
91
-
92
- const messages = parseMessages(transcript);
93
- if (messages.length < 6) return;
94
-
95
- // Skip if not enough new messages since last capture
96
- if (sessionId) {
97
- const prev = getPreviousCapture(sessionId);
98
- if (prev && messages.length - prev.messageCount < MIN_NEW_MESSAGES) return;
99
- }
100
-
101
- const lastUser = extractLastUser(messages);
102
- const lastAssistant = extractLastAssistant(messages);
103
-
104
- const rawTitle = extractContent(lastUser).slice(0, 80) || "session";
105
- const rawSummary = extractContent(lastAssistant).slice(0, 600);
106
-
107
- // Generate title, summary, and insights in a single inference call
108
- let title = rawTitle;
109
- let summary = rawSummary;
110
- let insights = "";
111
- try {
112
- const userMessages = messages
113
- .filter((m) => m.role === "user")
114
- .map((m) => extractContent(m).slice(0, 100))
115
- .slice(-8)
116
- .join("\n");
117
- const result = await inference({
118
- system:
119
- "You summarize sessions between a human user and an AI assistant. Sessions may involve coding, research, writing, planning, analysis, or any other task. The 'Human messages' are what the user said. The 'AI response' is what the assistant said. Produce: 1) a short title (5-10 words) describing what was accomplished, 2) a summary of what the AI assistant did for the user (2-4 sentences, write from the AI's perspective using 'we'), 3) insights — what worked well, what was surprising, or what should be done differently next time (2-3 bullet points, no markdown).",
120
- user: `Human messages:\n${userMessages}\n\nAI response:\n${rawSummary.slice(0, 400)}`,
121
- maxTokens: 300,
122
- timeout: 15000,
123
- jsonSchema: {
124
- type: "object" as const,
125
- additionalProperties: false,
126
- properties: {
127
- title: { type: "string" as const },
128
- summary: { type: "string" as const },
129
- insights: { type: "string" as const },
130
- },
131
- required: ["title", "summary", "insights"],
132
- },
133
- });
134
- if (result.usage) logTokenUsage("work-learning", result.usage);
135
- if (result.success && result.output) {
136
- const parsed = JSON.parse(result.output) as {
137
- title?: string;
138
- summary?: string;
139
- insights?: string;
140
- };
141
- if (parsed.title) title = parsed.title.slice(0, 100);
142
- if (parsed.summary) summary = parsed.summary;
143
- if (parsed.insights) insights = parsed.insights;
144
- }
145
- } catch {
146
- // Fallback to raw values
147
- }
148
- const category = categorizeLearning(title, summary);
149
-
150
- const slug = slugify(title);
151
- const dir = ensureDir(resolve(paths.sessionLearning(), monthPath()));
152
- const filename = `${fileTimestamp()}_${category}_${slug}.md`;
153
-
154
- const meta: Record<string, unknown> = {
155
- title,
156
- category,
157
- date: new Date().toISOString().slice(0, 10),
158
- cwd: process.cwd(),
159
- };
160
- if (sessionId) meta.session = sessionId;
161
-
162
- const body = [
163
- "## What Was Done",
164
- summary,
165
- "",
166
- "## Insights",
167
- insights || "*No insights captured.*",
168
- ].join("\n");
169
-
170
- const content = stringify(meta, body);
171
-
172
- // Remove previous capture for this session (overwrite on continued conversations)
173
- if (sessionId) {
174
- const prev = getPreviousCapture(sessionId);
175
- if (prev?.filepath && existsSync(prev.filepath)) {
176
- try {
177
- unlinkSync(prev.filepath);
178
- } catch {
179
- /* ignore */
180
- }
181
- }
182
- }
183
-
184
- const filepath = resolve(dir, filename);
185
- writeFileSync(filepath, content, "utf-8");
186
-
187
- // Append to per-project history (agent-agnostic recall)
188
- appendProjectHistory(process.cwd(), {
189
- date: new Date().toISOString().slice(0, 10),
190
- title,
191
- summary,
192
- insights,
193
- });
194
-
195
- if (sessionId) markCaptured(sessionId, filepath, messages.length);
196
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * CLI helper: check setup state and output results.
3
- *
4
- * Usage:
5
- * bun run hooks/setup-check.ts status → "complete" or "incomplete"
6
- * bun run hooks/setup-check.ts init → create setup.json if missing
7
- * bun run hooks/setup-check.ts prompt → output setup prompt (if incomplete)
8
- *
9
- * Used by shell scripts to avoid duplicating setup logic.
10
- */
11
-
12
- import {
13
- buildSetupPrompt,
14
- ensureSetupState,
15
- isSetupComplete,
16
- readSetupState,
17
- } from "./lib/setup";
18
-
19
- const command = process.argv[2] ?? "status";
20
-
21
- switch (command) {
22
- case "status": {
23
- const state = readSetupState();
24
- console.log(state && isSetupComplete(state) ? "complete" : "incomplete");
25
- break;
26
- }
27
- case "init": {
28
- ensureSetupState();
29
- break;
30
- }
31
- case "prompt": {
32
- const state = readSetupState();
33
- if (state) {
34
- const prompt = buildSetupPrompt(state);
35
- if (prompt) console.log(prompt);
36
- }
37
- break;
38
- }
39
- default:
40
- console.error(`Unknown command: ${command}`);
41
- process.exit(1);
42
- }