portable-agent-layer 0.1.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/LICENSE +21 -0
- package/README.md +80 -0
- package/assets/agents/claude-researcher.md +43 -0
- package/assets/agents/investigative-researcher.md +44 -0
- package/assets/agents/multi-perspective-researcher.md +43 -0
- package/assets/skills/analyze-pdf.md +40 -0
- package/assets/skills/analyze-youtube.md +35 -0
- package/assets/skills/council.md +43 -0
- package/assets/skills/create-skill.md +31 -0
- package/assets/skills/extract-entities.md +63 -0
- package/assets/skills/extract-wisdom.md +18 -0
- package/assets/skills/first-principles.md +17 -0
- package/assets/skills/fyzz-chat-api.md +43 -0
- package/assets/skills/reflect.md +87 -0
- package/assets/skills/research.md +68 -0
- package/assets/skills/review.md +19 -0
- package/assets/skills/summarize.md +15 -0
- package/assets/templates/AGENTS.md.template +45 -0
- package/assets/templates/telos/BELIEFS.md +4 -0
- package/assets/templates/telos/CHALLENGES.md +4 -0
- package/assets/templates/telos/GOALS.md +12 -0
- package/assets/templates/telos/IDEAS.md +4 -0
- package/assets/templates/telos/IDENTITY.md +4 -0
- package/assets/templates/telos/LEARNED.md +4 -0
- package/assets/templates/telos/MISSION.md +4 -0
- package/assets/templates/telos/MODELS.md +4 -0
- package/assets/templates/telos/NARRATIVES.md +4 -0
- package/assets/templates/telos/PROJECTS.md +7 -0
- package/assets/templates/telos/STRATEGIES.md +4 -0
- package/bin/pal +24 -0
- package/bin/pal.bat +8 -0
- package/bin/pal.ps1 +30 -0
- package/package.json +82 -0
- package/src/cli/index.ts +344 -0
- package/src/cli/install.ts +86 -0
- package/src/cli/uninstall.ts +45 -0
- package/src/hooks/LoadContext.ts +41 -0
- package/src/hooks/SecurityValidator.ts +52 -0
- package/src/hooks/SkillGuard.ts +41 -0
- package/src/hooks/StopOrchestrator.ts +35 -0
- package/src/hooks/UserPromptOrchestrator.ts +35 -0
- package/src/hooks/handlers/backup.ts +41 -0
- package/src/hooks/handlers/failure.ts +136 -0
- package/src/hooks/handlers/rating.ts +409 -0
- package/src/hooks/handlers/relationship.ts +113 -0
- package/src/hooks/handlers/session-name.ts +121 -0
- package/src/hooks/handlers/synthesis.ts +109 -0
- package/src/hooks/handlers/tab.ts +8 -0
- package/src/hooks/handlers/update-counts.ts +151 -0
- package/src/hooks/handlers/work-learning.ts +183 -0
- package/src/hooks/handlers/work-session.ts +58 -0
- package/src/hooks/lib/claude-md.ts +121 -0
- package/src/hooks/lib/context.ts +433 -0
- package/src/hooks/lib/entities.ts +304 -0
- package/src/hooks/lib/export.ts +76 -0
- package/src/hooks/lib/inference.ts +91 -0
- package/src/hooks/lib/learning-category.ts +14 -0
- package/src/hooks/lib/log.ts +53 -0
- package/src/hooks/lib/models.ts +16 -0
- package/src/hooks/lib/paths.ts +80 -0
- package/src/hooks/lib/relationship.ts +135 -0
- package/src/hooks/lib/security.ts +122 -0
- package/src/hooks/lib/session-names.ts +247 -0
- package/src/hooks/lib/setup.ts +189 -0
- package/src/hooks/lib/signal-trends.ts +117 -0
- package/src/hooks/lib/signals.ts +37 -0
- package/src/hooks/lib/stdin.ts +18 -0
- package/src/hooks/lib/stop.ts +155 -0
- package/src/hooks/lib/time.ts +19 -0
- package/src/hooks/lib/token-usage.ts +42 -0
- package/src/hooks/lib/transcript.ts +76 -0
- package/src/hooks/lib/wisdom.ts +48 -0
- package/src/hooks/lib/work-tracking.ts +193 -0
- package/src/hooks/setup-check.ts +42 -0
- package/src/targets/claude/install.ts +145 -0
- package/src/targets/claude/uninstall.ts +101 -0
- package/src/targets/lib.ts +337 -0
- package/src/targets/opencode/install.ts +59 -0
- package/src/targets/opencode/plugin.ts +328 -0
- package/src/targets/opencode/uninstall.ts +57 -0
- package/src/tools/entity-save.ts +110 -0
- package/src/tools/export.ts +34 -0
- package/src/tools/fyzz-api.ts +104 -0
- package/src/tools/import.ts +123 -0
- package/src/tools/pattern-synthesis.ts +435 -0
- package/src/tools/pdf-download.ts +102 -0
- package/src/tools/relationship-reflect.ts +362 -0
- package/src/tools/session-summary.ts +206 -0
- package/src/tools/token-cost.ts +301 -0
- package/src/tools/youtube-analyze.ts +105 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL Import — Extracts a PAL export archive into the repo,
|
|
3
|
+
* restoring personal files (memory, telos, state).
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun run tool:import [path-to-zip] [--dry-run]
|
|
6
|
+
* If no path is given, finds the latest pal-export-*.zip and asks for confirmation.
|
|
7
|
+
* Then run: bun run install:all to re-create symlinks and hooks.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readdirSync, statSync } from "node:fs";
|
|
11
|
+
import { resolve } from "node:path";
|
|
12
|
+
import { createInterface } from "node:readline";
|
|
13
|
+
import AdmZip from "adm-zip";
|
|
14
|
+
import { palHome } from "../hooks/lib/paths";
|
|
15
|
+
|
|
16
|
+
const repoRoot = palHome();
|
|
17
|
+
const args = process.argv.slice(2);
|
|
18
|
+
const dryRun = args.includes("--dry-run");
|
|
19
|
+
const pathArg = args.find((a) => a !== "--dry-run");
|
|
20
|
+
|
|
21
|
+
async function confirm(message: string): Promise<boolean> {
|
|
22
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
23
|
+
return new Promise((res) => {
|
|
24
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
25
|
+
rl.close();
|
|
26
|
+
res(answer.trim().toLowerCase() === "y");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findLatestExport(): string | null {
|
|
32
|
+
const files = readdirSync(repoRoot)
|
|
33
|
+
.filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
|
|
34
|
+
.sort()
|
|
35
|
+
.reverse();
|
|
36
|
+
|
|
37
|
+
// Also check backups/
|
|
38
|
+
try {
|
|
39
|
+
const backupDir = resolve(repoRoot, "backups");
|
|
40
|
+
const backups = readdirSync(backupDir)
|
|
41
|
+
.filter(
|
|
42
|
+
(f) =>
|
|
43
|
+
(f.startsWith("pal-export-") || f.startsWith("pal-backup-")) &&
|
|
44
|
+
f.endsWith(".zip")
|
|
45
|
+
)
|
|
46
|
+
.map((f) => ({ name: f, path: resolve(backupDir, f) }))
|
|
47
|
+
.sort((a, b) => b.name.localeCompare(a.name));
|
|
48
|
+
if (backups.length > 0) files.push(backups[0].name);
|
|
49
|
+
} catch {
|
|
50
|
+
// No backups dir
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (files.length === 0) return null;
|
|
54
|
+
|
|
55
|
+
// Find the most recent by mtime across both locations
|
|
56
|
+
const candidates = [
|
|
57
|
+
...readdirSync(repoRoot)
|
|
58
|
+
.filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
|
|
59
|
+
.map((f) => resolve(repoRoot, f)),
|
|
60
|
+
];
|
|
61
|
+
try {
|
|
62
|
+
const backupDir = resolve(repoRoot, "backups");
|
|
63
|
+
candidates.push(
|
|
64
|
+
...readdirSync(backupDir)
|
|
65
|
+
.filter(
|
|
66
|
+
(f) =>
|
|
67
|
+
(f.startsWith("pal-export-") || f.startsWith("pal-backup-")) &&
|
|
68
|
+
f.endsWith(".zip")
|
|
69
|
+
)
|
|
70
|
+
.map((f) => resolve(backupDir, f))
|
|
71
|
+
);
|
|
72
|
+
} catch {
|
|
73
|
+
// No backups dir
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (candidates.length === 0) return null;
|
|
77
|
+
return candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Resolve zip path
|
|
81
|
+
let zipPath: string;
|
|
82
|
+
|
|
83
|
+
if (pathArg) {
|
|
84
|
+
zipPath = resolve(pathArg);
|
|
85
|
+
} else {
|
|
86
|
+
const latest = findLatestExport();
|
|
87
|
+
if (!latest) {
|
|
88
|
+
console.error(
|
|
89
|
+
"No export or backup files found. Provide a path: bun run tool:import <path-to-zip>"
|
|
90
|
+
);
|
|
91
|
+
process.exit(1);
|
|
92
|
+
}
|
|
93
|
+
console.log(`Found: ${latest}`);
|
|
94
|
+
const zip = new AdmZip(latest);
|
|
95
|
+
const entries = zip.getEntries();
|
|
96
|
+
console.log(
|
|
97
|
+
`Contains ${entries.length} files, created ${statSync(latest).mtime.toISOString().slice(0, 16).replace("T", " ")}`
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (!(await confirm("Import this file?"))) {
|
|
101
|
+
console.log("Cancelled.");
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}
|
|
104
|
+
zipPath = latest;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Import
|
|
108
|
+
const zip = new AdmZip(zipPath);
|
|
109
|
+
const entries = zip.getEntries();
|
|
110
|
+
|
|
111
|
+
if (entries.length === 0) {
|
|
112
|
+
console.log("Archive is empty — nothing to import.");
|
|
113
|
+
process.exit(0);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (dryRun) {
|
|
117
|
+
console.log(`Would import ${entries.length} files → ${repoRoot}\n`);
|
|
118
|
+
for (const e of entries) console.log(` ${e.entryName}`);
|
|
119
|
+
} else {
|
|
120
|
+
zip.extractAllTo(repoRoot, true);
|
|
121
|
+
console.log(`Imported ${entries.length} files → ${repoRoot}`);
|
|
122
|
+
console.log("\nRun 'bun run install:all' to re-create symlinks and hooks.");
|
|
123
|
+
}
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* LearningPatternSynthesis — Aggregate ratings into actionable patterns.
|
|
4
|
+
*
|
|
5
|
+
* Analyzes memory/signals/ratings.jsonl to find recurring frustration/success
|
|
6
|
+
* patterns and generates synthesis reports.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun run tool:patterns # Analyze last 7 days (default)
|
|
10
|
+
* bun run tool:patterns -- --month # Analyze last 30 days
|
|
11
|
+
* bun run tool:patterns -- --all # Analyze all ratings
|
|
12
|
+
* bun run tool:patterns -- --dry-run # Preview without writing
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { resolve } from "node:path";
|
|
17
|
+
import { parseArgs } from "node:util";
|
|
18
|
+
import { HAIKU_MODEL } from "../hooks/lib/models";
|
|
19
|
+
import { palHome } from "../hooks/lib/paths";
|
|
20
|
+
|
|
21
|
+
// ── Paths ──
|
|
22
|
+
|
|
23
|
+
const RATINGS_FILE = resolve(palHome(), "memory", "signals", "ratings.jsonl");
|
|
24
|
+
const SYNTHESIS_DIR = resolve(palHome(), "memory", "learning", "synthesis");
|
|
25
|
+
|
|
26
|
+
// ── Types ──
|
|
27
|
+
|
|
28
|
+
interface Rating {
|
|
29
|
+
ts: string;
|
|
30
|
+
rating: number;
|
|
31
|
+
context: string;
|
|
32
|
+
source: "explicit" | "implicit";
|
|
33
|
+
response_preview?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface PatternGroup {
|
|
37
|
+
pattern: string;
|
|
38
|
+
count: number;
|
|
39
|
+
avgRating: number;
|
|
40
|
+
examples: string[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface SynthesisResult {
|
|
44
|
+
period: string;
|
|
45
|
+
totalRatings: number;
|
|
46
|
+
avgRating: number;
|
|
47
|
+
frustrations: PatternGroup[];
|
|
48
|
+
successes: PatternGroup[];
|
|
49
|
+
topIssues: string[];
|
|
50
|
+
recommendations: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── Pattern Detection ──
|
|
54
|
+
|
|
55
|
+
const FRUSTRATION_PATTERNS: Record<string, RegExp> = {
|
|
56
|
+
"Time/Performance Issues": /time|slow|delay|hang|wait|long|minutes|hours/i,
|
|
57
|
+
"Incomplete Work": /incomplete|missing|partial|didn't finish|not done/i,
|
|
58
|
+
"Wrong Approach": /wrong|incorrect|not what|misunderstand|mistake/i,
|
|
59
|
+
"Over-engineering": /over-?engineer|too complex|unnecessary|bloat/i,
|
|
60
|
+
"Tool/System Failures": /fail|error|broken|crash|bug|issue/i,
|
|
61
|
+
"Communication Problems": /unclear|confus|didn't ask|should have asked/i,
|
|
62
|
+
"Repetitive Issues": /again|repeat|still|same problem/i,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const SUCCESS_PATTERNS: Record<string, RegExp> = {
|
|
66
|
+
"Quick Resolution": /quick|fast|efficient|smooth/i,
|
|
67
|
+
"Good Understanding": /understood|clear|exactly|perfect/i,
|
|
68
|
+
"Proactive Help": /proactive|anticipat|helpful|above and beyond/i,
|
|
69
|
+
"Clean Implementation": /clean|simple|elegant|well done/i,
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
function detectPatterns(
|
|
73
|
+
summaries: string[],
|
|
74
|
+
patterns: Record<string, RegExp>
|
|
75
|
+
): Map<string, string[]> {
|
|
76
|
+
const results = new Map<string, string[]>();
|
|
77
|
+
for (const summary of summaries) {
|
|
78
|
+
for (const [name, pattern] of Object.entries(patterns)) {
|
|
79
|
+
if (pattern.test(summary)) {
|
|
80
|
+
const arr = results.get(name) ?? [];
|
|
81
|
+
arr.push(summary);
|
|
82
|
+
results.set(name, arr);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function toPatternGroups(
|
|
90
|
+
grouped: Map<string, string[]>,
|
|
91
|
+
ratings: Rating[]
|
|
92
|
+
): PatternGroup[] {
|
|
93
|
+
const groups: PatternGroup[] = [];
|
|
94
|
+
|
|
95
|
+
for (const [pattern, examples] of grouped.entries()) {
|
|
96
|
+
const matching = ratings.filter((r) => examples.some((e) => e === r.context));
|
|
97
|
+
const avgRating =
|
|
98
|
+
matching.length > 0
|
|
99
|
+
? matching.reduce((sum, r) => sum + r.rating, 0) / matching.length
|
|
100
|
+
: 5;
|
|
101
|
+
|
|
102
|
+
groups.push({
|
|
103
|
+
pattern,
|
|
104
|
+
count: examples.length,
|
|
105
|
+
avgRating,
|
|
106
|
+
examples: examples.slice(0, 3),
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return groups.sort((a, b) => b.count - a.count);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// ── Analysis ──
|
|
114
|
+
|
|
115
|
+
async function generateRecommendations(
|
|
116
|
+
frustrations: PatternGroup[],
|
|
117
|
+
successes: PatternGroup[],
|
|
118
|
+
avgRating: number
|
|
119
|
+
): Promise<string[]> {
|
|
120
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
121
|
+
if (!apiKey || frustrations.length === 0) {
|
|
122
|
+
// Fallback: generic recommendations
|
|
123
|
+
if (frustrations.length === 0)
|
|
124
|
+
return ["Continue current patterns - no major issues detected"];
|
|
125
|
+
return frustrations
|
|
126
|
+
.slice(0, 3)
|
|
127
|
+
.map(
|
|
128
|
+
(f) =>
|
|
129
|
+
`Address "${f.pattern}" (${f.count} occurrences, avg ${f.avgRating.toFixed(1)}/10)`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const context = [
|
|
135
|
+
`Average rating: ${avgRating.toFixed(1)}/10`,
|
|
136
|
+
"",
|
|
137
|
+
"Top frustration patterns:",
|
|
138
|
+
...frustrations
|
|
139
|
+
.slice(0, 5)
|
|
140
|
+
.map(
|
|
141
|
+
(f) =>
|
|
142
|
+
`- ${f.pattern} (${f.count}x, avg ${f.avgRating.toFixed(1)}): ${f.examples.slice(0, 2).join("; ")}`
|
|
143
|
+
),
|
|
144
|
+
"",
|
|
145
|
+
successes.length > 0 ? "Success patterns:" : "",
|
|
146
|
+
...successes
|
|
147
|
+
.slice(0, 3)
|
|
148
|
+
.map((s) => `- ${s.pattern} (${s.count}x, avg ${s.avgRating.toFixed(1)})`),
|
|
149
|
+
]
|
|
150
|
+
.filter(Boolean)
|
|
151
|
+
.join("\n");
|
|
152
|
+
|
|
153
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
154
|
+
method: "POST",
|
|
155
|
+
headers: {
|
|
156
|
+
"x-api-key": apiKey,
|
|
157
|
+
"anthropic-version": "2023-06-01",
|
|
158
|
+
"content-type": "application/json",
|
|
159
|
+
},
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
model: HAIKU_MODEL,
|
|
162
|
+
max_tokens: 300,
|
|
163
|
+
messages: [{ role: "user", content: context }],
|
|
164
|
+
system:
|
|
165
|
+
"You analyze AI assistant interaction patterns. Given frustration and success patterns from user ratings, generate 3-5 recommendations. Each MUST reference a specific example from the data — no generic advice like 'ask clarifying questions' or 'communicate better'. Every recommendation should name the concrete situation and the concrete fix. One sentence each. Return a JSON array of strings.",
|
|
166
|
+
output_config: {
|
|
167
|
+
format: {
|
|
168
|
+
type: "json_schema",
|
|
169
|
+
schema: {
|
|
170
|
+
type: "object",
|
|
171
|
+
additionalProperties: false,
|
|
172
|
+
properties: {
|
|
173
|
+
recommendations: {
|
|
174
|
+
type: "array",
|
|
175
|
+
items: { type: "string" },
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
required: ["recommendations"],
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
}),
|
|
183
|
+
signal: AbortSignal.timeout(15000),
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
if (response.ok) {
|
|
187
|
+
const data = (await response.json()) as { content?: Array<{ text?: string }> };
|
|
188
|
+
const text = data?.content?.[0]?.text?.trim();
|
|
189
|
+
if (text) {
|
|
190
|
+
const parsed = JSON.parse(text) as { recommendations: string[] };
|
|
191
|
+
if (parsed.recommendations?.length > 0) return parsed.recommendations.slice(0, 5);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
} catch {
|
|
195
|
+
// Fallback silently
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return frustrations
|
|
199
|
+
.slice(0, 3)
|
|
200
|
+
.map((f) => `Address "${f.pattern}" (${f.count} occurrences)`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
async function analyzeRatings(
|
|
204
|
+
ratings: Rating[],
|
|
205
|
+
period: string
|
|
206
|
+
): Promise<SynthesisResult> {
|
|
207
|
+
if (ratings.length === 0) {
|
|
208
|
+
return {
|
|
209
|
+
period,
|
|
210
|
+
totalRatings: 0,
|
|
211
|
+
avgRating: 0,
|
|
212
|
+
frustrations: [],
|
|
213
|
+
successes: [],
|
|
214
|
+
topIssues: [],
|
|
215
|
+
recommendations: [],
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const avgRating = ratings.reduce((sum, r) => sum + r.rating, 0) / ratings.length;
|
|
220
|
+
|
|
221
|
+
const frustrationRatings = ratings.filter((r) => r.rating <= 4);
|
|
222
|
+
const successRatings = ratings.filter((r) => r.rating >= 7);
|
|
223
|
+
|
|
224
|
+
const frustrationGroups = detectPatterns(
|
|
225
|
+
frustrationRatings.map((r) => r.context),
|
|
226
|
+
FRUSTRATION_PATTERNS
|
|
227
|
+
);
|
|
228
|
+
const successGroups = detectPatterns(
|
|
229
|
+
successRatings.map((r) => r.context),
|
|
230
|
+
SUCCESS_PATTERNS
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
const frustrations = toPatternGroups(frustrationGroups, frustrationRatings);
|
|
234
|
+
const successes = toPatternGroups(successGroups, successRatings);
|
|
235
|
+
|
|
236
|
+
const topIssues = frustrations
|
|
237
|
+
.slice(0, 3)
|
|
238
|
+
.map(
|
|
239
|
+
(f) => `${f.pattern} (${f.count} occurrences, avg rating ${f.avgRating.toFixed(1)})`
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
const recommendations = await generateRecommendations(
|
|
243
|
+
frustrations,
|
|
244
|
+
successes,
|
|
245
|
+
avgRating
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
period,
|
|
250
|
+
totalRatings: ratings.length,
|
|
251
|
+
avgRating,
|
|
252
|
+
frustrations,
|
|
253
|
+
successes,
|
|
254
|
+
topIssues,
|
|
255
|
+
recommendations,
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ── Report ──
|
|
260
|
+
|
|
261
|
+
function formatReport(result: SynthesisResult): string {
|
|
262
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
263
|
+
const lines: string[] = [
|
|
264
|
+
"# Learning Pattern Synthesis",
|
|
265
|
+
"",
|
|
266
|
+
`**Period:** ${result.period}`,
|
|
267
|
+
`**Generated:** ${date}`,
|
|
268
|
+
`**Total Ratings:** ${result.totalRatings}`,
|
|
269
|
+
`**Average Rating:** ${result.avgRating.toFixed(1)}/10`,
|
|
270
|
+
"",
|
|
271
|
+
"---",
|
|
272
|
+
"",
|
|
273
|
+
"## Top Issues",
|
|
274
|
+
"",
|
|
275
|
+
];
|
|
276
|
+
|
|
277
|
+
if (result.topIssues.length > 0) {
|
|
278
|
+
for (let i = 0; i < result.topIssues.length; i++) {
|
|
279
|
+
lines.push(`${i + 1}. ${result.topIssues[i]}`);
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
lines.push("No significant issues detected");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
lines.push("", "## Frustration Patterns", "");
|
|
286
|
+
if (result.frustrations.length === 0) {
|
|
287
|
+
lines.push("*No frustration patterns detected*");
|
|
288
|
+
} else {
|
|
289
|
+
for (const f of result.frustrations) {
|
|
290
|
+
lines.push(
|
|
291
|
+
`### ${f.pattern}`,
|
|
292
|
+
"",
|
|
293
|
+
`- **Occurrences:** ${f.count}`,
|
|
294
|
+
`- **Avg Rating:** ${f.avgRating.toFixed(1)}`,
|
|
295
|
+
`- **Examples:**`,
|
|
296
|
+
...f.examples.map((e) => ` - "${e}"`),
|
|
297
|
+
""
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
lines.push("", "## Success Patterns", "");
|
|
303
|
+
if (result.successes.length === 0) {
|
|
304
|
+
lines.push("*No success patterns detected*");
|
|
305
|
+
} else {
|
|
306
|
+
for (const s of result.successes) {
|
|
307
|
+
lines.push(
|
|
308
|
+
`### ${s.pattern}`,
|
|
309
|
+
"",
|
|
310
|
+
`- **Occurrences:** ${s.count}`,
|
|
311
|
+
`- **Avg Rating:** ${s.avgRating.toFixed(1)}`,
|
|
312
|
+
`- **Examples:**`,
|
|
313
|
+
...s.examples.map((e) => ` - "${e}"`),
|
|
314
|
+
""
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
lines.push(
|
|
320
|
+
"",
|
|
321
|
+
"## Recommendations",
|
|
322
|
+
"",
|
|
323
|
+
...result.recommendations.map((r, i) => `${i + 1}. ${r}`),
|
|
324
|
+
""
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
return lines.join("\n");
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function writeReport(result: SynthesisResult, period: string): string {
|
|
331
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
332
|
+
const monthDir = resolve(SYNTHESIS_DIR, date.slice(0, 7));
|
|
333
|
+
if (!existsSync(monthDir)) mkdirSync(monthDir, { recursive: true });
|
|
334
|
+
|
|
335
|
+
const slug = period.toLowerCase().replace(/\s+/g, "-");
|
|
336
|
+
const filename = `${date}_${slug}-patterns.md`;
|
|
337
|
+
const filepath = resolve(monthDir, filename);
|
|
338
|
+
|
|
339
|
+
writeFileSync(filepath, formatReport(result), "utf-8");
|
|
340
|
+
return filepath;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// ── CLI ──
|
|
344
|
+
|
|
345
|
+
const { values } = parseArgs({
|
|
346
|
+
args: Bun.argv.slice(2),
|
|
347
|
+
options: {
|
|
348
|
+
week: { type: "boolean" },
|
|
349
|
+
month: { type: "boolean" },
|
|
350
|
+
all: { type: "boolean" },
|
|
351
|
+
"dry-run": { type: "boolean" },
|
|
352
|
+
help: { type: "boolean", short: "h" },
|
|
353
|
+
},
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
if (values.help) {
|
|
357
|
+
console.log(`
|
|
358
|
+
LearningPatternSynthesis — Aggregate ratings into actionable patterns
|
|
359
|
+
|
|
360
|
+
Usage:
|
|
361
|
+
bun run tool:patterns Analyze last 7 days (default)
|
|
362
|
+
bun run tool:patterns -- --month Analyze last 30 days
|
|
363
|
+
bun run tool:patterns -- --all Analyze all ratings
|
|
364
|
+
bun run tool:patterns -- --dry-run Preview without writing
|
|
365
|
+
|
|
366
|
+
Output: Creates synthesis report in memory/learning/synthesis/YYYY-MM/
|
|
367
|
+
`);
|
|
368
|
+
process.exit(0);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (!existsSync(RATINGS_FILE)) {
|
|
372
|
+
console.log("No ratings file found at:", RATINGS_FILE);
|
|
373
|
+
process.exit(0);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// Read ratings
|
|
377
|
+
const allRatings: Rating[] = readFileSync(RATINGS_FILE, "utf-8")
|
|
378
|
+
.split("\n")
|
|
379
|
+
.filter((l) => l.trim())
|
|
380
|
+
.map((l) => {
|
|
381
|
+
try {
|
|
382
|
+
return JSON.parse(l);
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
.filter((r): r is Rating => r !== null);
|
|
388
|
+
|
|
389
|
+
console.log(`Loaded ${allRatings.length} total ratings`);
|
|
390
|
+
|
|
391
|
+
// Determine period
|
|
392
|
+
let period = "Weekly";
|
|
393
|
+
const cutoff = new Date();
|
|
394
|
+
|
|
395
|
+
if (values.month) {
|
|
396
|
+
period = "Monthly";
|
|
397
|
+
cutoff.setDate(cutoff.getDate() - 30);
|
|
398
|
+
} else if (values.all) {
|
|
399
|
+
period = "All Time";
|
|
400
|
+
cutoff.setTime(0);
|
|
401
|
+
} else {
|
|
402
|
+
cutoff.setDate(cutoff.getDate() - 7);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const filtered = allRatings.filter((r) => new Date(r.ts).getTime() >= cutoff.getTime());
|
|
406
|
+
console.log(`Analyzing ${filtered.length} ratings for ${period.toLowerCase()} period`);
|
|
407
|
+
|
|
408
|
+
if (filtered.length === 0) {
|
|
409
|
+
console.log("No ratings in this period");
|
|
410
|
+
process.exit(0);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const result = await analyzeRatings(filtered, period);
|
|
414
|
+
|
|
415
|
+
console.log(`\nAverage Rating: ${result.avgRating.toFixed(1)}/10`);
|
|
416
|
+
console.log(`Frustration Patterns: ${result.frustrations.length}`);
|
|
417
|
+
console.log(`Success Patterns: ${result.successes.length}`);
|
|
418
|
+
|
|
419
|
+
if (result.topIssues.length > 0) {
|
|
420
|
+
console.log("\nTop Issues:");
|
|
421
|
+
for (const issue of result.topIssues) {
|
|
422
|
+
console.log(` - ${issue}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (values["dry-run"]) {
|
|
427
|
+
console.log("\n[DRY RUN] Would write synthesis report");
|
|
428
|
+
console.log("\nRecommendations:");
|
|
429
|
+
for (const rec of result.recommendations) {
|
|
430
|
+
console.log(` - ${rec}`);
|
|
431
|
+
}
|
|
432
|
+
} else {
|
|
433
|
+
const filepath = writeReport(result, period);
|
|
434
|
+
console.log(`\nCreated synthesis report: ${filepath}`);
|
|
435
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PDF Download — Downloads a PDF from a URL and saves it to an organized local archive.
|
|
5
|
+
*
|
|
6
|
+
* Saves to: {PAL_ROOT}/memory/downloads/{YYYY}/{MM}/{DD}/{filename}.pdf
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun run ai:pdf-download -- <url> [--filename <name.pdf>]
|
|
10
|
+
*
|
|
11
|
+
* Returns JSON with the saved file path for downstream reading.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { mkdir } from "node:fs/promises";
|
|
15
|
+
import { basename, join } from "node:path";
|
|
16
|
+
import { parseArgs } from "node:util";
|
|
17
|
+
import { palHome } from "../hooks/lib/paths";
|
|
18
|
+
|
|
19
|
+
const DOWNLOADS_DIR = join(palHome(), "memory", "downloads");
|
|
20
|
+
|
|
21
|
+
function buildDatePath(): string {
|
|
22
|
+
const now = new Date();
|
|
23
|
+
const yyyy = now.getFullYear().toString();
|
|
24
|
+
const mm = (now.getMonth() + 1).toString().padStart(2, "0");
|
|
25
|
+
const dd = now.getDate().toString().padStart(2, "0");
|
|
26
|
+
return join(yyyy, mm, dd);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function extractFilename(url: string, override?: string): string {
|
|
30
|
+
if (override) {
|
|
31
|
+
return override.endsWith(".pdf") ? override : `${override}.pdf`;
|
|
32
|
+
}
|
|
33
|
+
const urlPath = new URL(url).pathname;
|
|
34
|
+
const name = basename(urlPath);
|
|
35
|
+
return name.endsWith(".pdf") ? name : `${name}.pdf`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function main() {
|
|
39
|
+
const { positionals, values } = parseArgs({
|
|
40
|
+
allowPositionals: true,
|
|
41
|
+
options: {
|
|
42
|
+
filename: { type: "string", short: "f" },
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const url = positionals[0];
|
|
47
|
+
if (!url) {
|
|
48
|
+
console.error("Usage: bun run ai:pdf-download -- <url> [--filename <name.pdf>]");
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Validate URL
|
|
53
|
+
let parsed: URL;
|
|
54
|
+
try {
|
|
55
|
+
parsed = new URL(url);
|
|
56
|
+
} catch {
|
|
57
|
+
console.error(`Error: Invalid URL: ${url}`);
|
|
58
|
+
process.exit(1);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!["http:", "https:"].includes(parsed.protocol)) {
|
|
62
|
+
console.error(`Error: Only HTTP/HTTPS URLs are supported.`);
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Download
|
|
67
|
+
const response = await fetch(url);
|
|
68
|
+
if (!response.ok) {
|
|
69
|
+
console.error(`Error: HTTP ${response.status} ${response.statusText}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
74
|
+
if (!contentType.includes("pdf") && !url.endsWith(".pdf")) {
|
|
75
|
+
console.error(`Warning: Content-Type is "${contentType}", may not be a PDF.`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const buffer = await response.arrayBuffer();
|
|
79
|
+
|
|
80
|
+
// Build destination path
|
|
81
|
+
const datePath = buildDatePath();
|
|
82
|
+
const dir = join(DOWNLOADS_DIR, datePath);
|
|
83
|
+
await mkdir(dir, { recursive: true });
|
|
84
|
+
|
|
85
|
+
const filename = extractFilename(url, values.filename);
|
|
86
|
+
const filePath = join(dir, filename);
|
|
87
|
+
|
|
88
|
+
// Write file
|
|
89
|
+
await Bun.write(filePath, buffer);
|
|
90
|
+
|
|
91
|
+
const result = {
|
|
92
|
+
path: filePath,
|
|
93
|
+
filename,
|
|
94
|
+
size: buffer.byteLength,
|
|
95
|
+
url,
|
|
96
|
+
downloadedAt: new Date().toISOString(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
console.log(JSON.stringify(result, null, 2));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
main();
|