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.
Files changed (90) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +80 -0
  3. package/assets/agents/claude-researcher.md +43 -0
  4. package/assets/agents/investigative-researcher.md +44 -0
  5. package/assets/agents/multi-perspective-researcher.md +43 -0
  6. package/assets/skills/analyze-pdf.md +40 -0
  7. package/assets/skills/analyze-youtube.md +35 -0
  8. package/assets/skills/council.md +43 -0
  9. package/assets/skills/create-skill.md +31 -0
  10. package/assets/skills/extract-entities.md +63 -0
  11. package/assets/skills/extract-wisdom.md +18 -0
  12. package/assets/skills/first-principles.md +17 -0
  13. package/assets/skills/fyzz-chat-api.md +43 -0
  14. package/assets/skills/reflect.md +87 -0
  15. package/assets/skills/research.md +68 -0
  16. package/assets/skills/review.md +19 -0
  17. package/assets/skills/summarize.md +15 -0
  18. package/assets/templates/AGENTS.md.template +45 -0
  19. package/assets/templates/telos/BELIEFS.md +4 -0
  20. package/assets/templates/telos/CHALLENGES.md +4 -0
  21. package/assets/templates/telos/GOALS.md +12 -0
  22. package/assets/templates/telos/IDEAS.md +4 -0
  23. package/assets/templates/telos/IDENTITY.md +4 -0
  24. package/assets/templates/telos/LEARNED.md +4 -0
  25. package/assets/templates/telos/MISSION.md +4 -0
  26. package/assets/templates/telos/MODELS.md +4 -0
  27. package/assets/templates/telos/NARRATIVES.md +4 -0
  28. package/assets/templates/telos/PROJECTS.md +7 -0
  29. package/assets/templates/telos/STRATEGIES.md +4 -0
  30. package/bin/pal +24 -0
  31. package/bin/pal.bat +8 -0
  32. package/bin/pal.ps1 +30 -0
  33. package/package.json +82 -0
  34. package/src/cli/index.ts +344 -0
  35. package/src/cli/install.ts +86 -0
  36. package/src/cli/uninstall.ts +45 -0
  37. package/src/hooks/LoadContext.ts +41 -0
  38. package/src/hooks/SecurityValidator.ts +52 -0
  39. package/src/hooks/SkillGuard.ts +41 -0
  40. package/src/hooks/StopOrchestrator.ts +35 -0
  41. package/src/hooks/UserPromptOrchestrator.ts +35 -0
  42. package/src/hooks/handlers/backup.ts +41 -0
  43. package/src/hooks/handlers/failure.ts +136 -0
  44. package/src/hooks/handlers/rating.ts +409 -0
  45. package/src/hooks/handlers/relationship.ts +113 -0
  46. package/src/hooks/handlers/session-name.ts +121 -0
  47. package/src/hooks/handlers/synthesis.ts +109 -0
  48. package/src/hooks/handlers/tab.ts +8 -0
  49. package/src/hooks/handlers/update-counts.ts +151 -0
  50. package/src/hooks/handlers/work-learning.ts +183 -0
  51. package/src/hooks/handlers/work-session.ts +58 -0
  52. package/src/hooks/lib/claude-md.ts +121 -0
  53. package/src/hooks/lib/context.ts +433 -0
  54. package/src/hooks/lib/entities.ts +304 -0
  55. package/src/hooks/lib/export.ts +76 -0
  56. package/src/hooks/lib/inference.ts +91 -0
  57. package/src/hooks/lib/learning-category.ts +14 -0
  58. package/src/hooks/lib/log.ts +53 -0
  59. package/src/hooks/lib/models.ts +16 -0
  60. package/src/hooks/lib/paths.ts +80 -0
  61. package/src/hooks/lib/relationship.ts +135 -0
  62. package/src/hooks/lib/security.ts +122 -0
  63. package/src/hooks/lib/session-names.ts +247 -0
  64. package/src/hooks/lib/setup.ts +189 -0
  65. package/src/hooks/lib/signal-trends.ts +117 -0
  66. package/src/hooks/lib/signals.ts +37 -0
  67. package/src/hooks/lib/stdin.ts +18 -0
  68. package/src/hooks/lib/stop.ts +155 -0
  69. package/src/hooks/lib/time.ts +19 -0
  70. package/src/hooks/lib/token-usage.ts +42 -0
  71. package/src/hooks/lib/transcript.ts +76 -0
  72. package/src/hooks/lib/wisdom.ts +48 -0
  73. package/src/hooks/lib/work-tracking.ts +193 -0
  74. package/src/hooks/setup-check.ts +42 -0
  75. package/src/targets/claude/install.ts +145 -0
  76. package/src/targets/claude/uninstall.ts +101 -0
  77. package/src/targets/lib.ts +337 -0
  78. package/src/targets/opencode/install.ts +59 -0
  79. package/src/targets/opencode/plugin.ts +328 -0
  80. package/src/targets/opencode/uninstall.ts +57 -0
  81. package/src/tools/entity-save.ts +110 -0
  82. package/src/tools/export.ts +34 -0
  83. package/src/tools/fyzz-api.ts +104 -0
  84. package/src/tools/import.ts +123 -0
  85. package/src/tools/pattern-synthesis.ts +435 -0
  86. package/src/tools/pdf-download.ts +102 -0
  87. package/src/tools/relationship-reflect.ts +362 -0
  88. package/src/tools/session-summary.ts +206 -0
  89. package/src/tools/token-cost.ts +301 -0
  90. package/src/tools/youtube-analyze.ts +105 -0
@@ -0,0 +1,301 @@
1
+ /**
2
+ * CLI tool: summarize token usage and estimated cost.
3
+ *
4
+ * Reads from two sources:
5
+ * 1. Claude Code session transcripts (~/.claude/projects/)
6
+ * 2. PAL Haiku inference logs (memory/signals/token-usage.jsonl)
7
+ *
8
+ * Usage: bun run tool:tokens [--today|--week|--month|--all] [--project <name>]
9
+ */
10
+
11
+ import { existsSync, readdirSync, readFileSync } from "node:fs";
12
+ import { resolve } from "node:path";
13
+ import { parseArgs } from "node:util";
14
+ import { MODEL_PRICING } from "../hooks/lib/models";
15
+ import { palHome } from "../hooks/lib/paths";
16
+
17
+ // ── Args ──
18
+
19
+ const { values: args } = parseArgs({
20
+ options: {
21
+ today: { type: "boolean", default: false },
22
+ week: { type: "boolean", default: false },
23
+ month: { type: "boolean", default: false },
24
+ all: { type: "boolean", default: false },
25
+ project: { type: "string" },
26
+ },
27
+ strict: false,
28
+ });
29
+
30
+ const now = new Date();
31
+ const todayPrefix = now.toISOString().slice(0, 10);
32
+ const weekAgo = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString();
33
+ const monthAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString();
34
+
35
+ // ── Types ──
36
+
37
+ interface Bucket {
38
+ input: number;
39
+ output: number;
40
+ cacheWrite: number;
41
+ cacheRead: number;
42
+ cost: number;
43
+ calls: number;
44
+ }
45
+
46
+ function emptyBucket(): Bucket {
47
+ return { input: 0, output: 0, cacheWrite: 0, cacheRead: 0, cost: 0, calls: 0 };
48
+ }
49
+
50
+ function costForUsage(
51
+ model: string,
52
+ input: number,
53
+ output: number,
54
+ cacheWrite: number,
55
+ cacheRead: number
56
+ ): number {
57
+ const p = MODEL_PRICING[model];
58
+ if (!p) return 0;
59
+ return (
60
+ (input * p.input +
61
+ output * p.output +
62
+ cacheWrite * p.cacheWrite +
63
+ cacheRead * p.cacheRead) /
64
+ 1_000_000
65
+ );
66
+ }
67
+
68
+ function addToBucket(
69
+ bucket: Bucket,
70
+ model: string,
71
+ input: number,
72
+ output: number,
73
+ cacheWrite: number,
74
+ cacheRead: number
75
+ ): void {
76
+ bucket.input += input;
77
+ bucket.output += output;
78
+ bucket.cacheWrite += cacheWrite;
79
+ bucket.cacheRead += cacheRead;
80
+ bucket.cost += costForUsage(model, input, output, cacheWrite, cacheRead);
81
+ bucket.calls++;
82
+ }
83
+
84
+ // ── Formatting ──
85
+
86
+ function fmt(n: number): string {
87
+ if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
88
+ if (n >= 1_000) return `${(n / 1_000).toFixed(1)}k`;
89
+ return n.toLocaleString("en-US");
90
+ }
91
+
92
+ function fmtCost(n: number): string {
93
+ if (n >= 1) return `$${n.toFixed(2)}`;
94
+ return `$${n.toFixed(4)}`;
95
+ }
96
+
97
+ function printRow(label: string, b: Bucket): void {
98
+ const tokens = b.input + b.output + b.cacheWrite + b.cacheRead;
99
+ console.log(
100
+ ` ${label.padEnd(14)} ${fmt(tokens).padStart(8)} tok ${fmt(b.calls).padStart(5)} calls ${fmtCost(b.cost).padStart(8)}`
101
+ );
102
+ }
103
+
104
+ function printDetailed(label: string, b: Bucket): void {
105
+ console.log(
106
+ ` ${label.padEnd(14)} ${fmt(b.input).padStart(8)} in ${fmt(b.output).padStart(8)} out ${fmt(b.cacheWrite).padStart(8)} cw ${fmt(b.cacheRead).padStart(8)} cr ${fmtCost(b.cost).padStart(8)}`
107
+ );
108
+ }
109
+
110
+ // ── Claude Code transcripts ──
111
+
112
+ interface TimeBuckets {
113
+ today: Bucket;
114
+ week: Bucket;
115
+ month: Bucket;
116
+ total: Bucket;
117
+ }
118
+
119
+ function emptyTimeBuckets(): TimeBuckets {
120
+ return {
121
+ today: emptyBucket(),
122
+ week: emptyBucket(),
123
+ month: emptyBucket(),
124
+ total: emptyBucket(),
125
+ };
126
+ }
127
+
128
+ function addToTimeBuckets(
129
+ tb: TimeBuckets,
130
+ ts: string,
131
+ model: string,
132
+ input: number,
133
+ output: number,
134
+ cacheWrite: number,
135
+ cacheRead: number
136
+ ): void {
137
+ addToBucket(tb.total, model, input, output, cacheWrite, cacheRead);
138
+ if (ts >= monthAgo) addToBucket(tb.month, model, input, output, cacheWrite, cacheRead);
139
+ if (ts >= weekAgo) addToBucket(tb.week, model, input, output, cacheWrite, cacheRead);
140
+ if (ts.startsWith(todayPrefix))
141
+ addToBucket(tb.today, model, input, output, cacheWrite, cacheRead);
142
+ }
143
+
144
+ function readClaudeCode(): {
145
+ buckets: TimeBuckets;
146
+ byModel: Record<string, Bucket>;
147
+ byProject: Record<string, TimeBuckets>;
148
+ } {
149
+ const buckets = emptyTimeBuckets();
150
+ const byModel: Record<string, Bucket> = {};
151
+ const byProject: Record<string, TimeBuckets> = {};
152
+
153
+ const claudeDir = resolve(process.env.HOME ?? "~", ".claude", "projects");
154
+ if (!existsSync(claudeDir)) return { buckets, byModel, byProject };
155
+
156
+ const projectDirs = readdirSync(claudeDir, { withFileTypes: true })
157
+ .filter((d) => d.isDirectory())
158
+ .map((d) => d.name);
159
+
160
+ for (const projDir of projectDirs) {
161
+ const projPath = resolve(claudeDir, projDir);
162
+ const projName = projDir.split("-").pop() ?? projDir;
163
+
164
+ if (typeof args.project === "string" && !projName.includes(args.project)) continue;
165
+
166
+ const jsonlFiles = readdirSync(projPath).filter((f) => f.endsWith(".jsonl"));
167
+
168
+ for (const file of jsonlFiles) {
169
+ const filepath = resolve(projPath, file);
170
+ let content: string;
171
+ try {
172
+ content = readFileSync(filepath, "utf-8");
173
+ } catch {
174
+ continue;
175
+ }
176
+
177
+ for (const line of content.split("\n")) {
178
+ if (!line.includes('"usage"')) continue;
179
+ try {
180
+ const d = JSON.parse(line) as {
181
+ type?: string;
182
+ timestamp?: string;
183
+ message?: {
184
+ model?: string;
185
+ usage?: {
186
+ input_tokens?: number;
187
+ output_tokens?: number;
188
+ cache_creation_input_tokens?: number;
189
+ cache_read_input_tokens?: number;
190
+ };
191
+ };
192
+ };
193
+ if (d.type !== "assistant") continue;
194
+ const usage = d.message?.usage;
195
+ const model = d.message?.model;
196
+ const ts = d.timestamp;
197
+ if (!usage || !model || !ts) continue;
198
+
199
+ const input = usage.input_tokens ?? 0;
200
+ const output = usage.output_tokens ?? 0;
201
+ const cw = usage.cache_creation_input_tokens ?? 0;
202
+ const cr = usage.cache_read_input_tokens ?? 0;
203
+
204
+ addToTimeBuckets(buckets, ts, model, input, output, cw, cr);
205
+
206
+ if (!byModel[model]) byModel[model] = emptyBucket();
207
+ addToBucket(byModel[model], model, input, output, cw, cr);
208
+
209
+ if (!byProject[projName]) byProject[projName] = emptyTimeBuckets();
210
+ addToTimeBuckets(byProject[projName], ts, model, input, output, cw, cr);
211
+ } catch {
212
+ /* skip */
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ return { buckets, byModel, byProject };
219
+ }
220
+
221
+ // ── PAL Haiku inference ──
222
+
223
+ function readPalInference(): { buckets: TimeBuckets; byCaller: Record<string, Bucket> } {
224
+ const buckets = emptyTimeBuckets();
225
+ const byCaller: Record<string, Bucket> = {};
226
+
227
+ const filepath = resolve(palHome(), "memory", "signals", "token-usage.jsonl");
228
+ if (!existsSync(filepath)) return { buckets, byCaller };
229
+
230
+ const content = readFileSync(filepath, "utf-8").trim();
231
+ if (!content) return { buckets, byCaller };
232
+
233
+ for (const line of content.split("\n")) {
234
+ try {
235
+ const e = JSON.parse(line) as {
236
+ ts: string;
237
+ caller: string;
238
+ model: string;
239
+ inputTokens: number;
240
+ outputTokens: number;
241
+ };
242
+ addToTimeBuckets(buckets, e.ts, e.model, e.inputTokens, e.outputTokens, 0, 0);
243
+ if (!byCaller[e.caller]) byCaller[e.caller] = emptyBucket();
244
+ addToBucket(byCaller[e.caller], e.model, e.inputTokens, e.outputTokens, 0, 0);
245
+ } catch {
246
+ /* skip */
247
+ }
248
+ }
249
+
250
+ return { buckets, byCaller };
251
+ }
252
+
253
+ // ── Main ──
254
+
255
+ const cc = readClaudeCode();
256
+ const pal = readPalInference();
257
+
258
+ console.log("\n Claude Code Usage\n");
259
+ printRow("Today", cc.buckets.today);
260
+ printRow("7d", cc.buckets.week);
261
+ printRow("30d", cc.buckets.month);
262
+ printRow("Total", cc.buckets.total);
263
+
264
+ if (Object.keys(cc.byModel).length > 0) {
265
+ console.log("\n By Model (all time)\n");
266
+ const sorted = Object.entries(cc.byModel).sort((a, b) => b[1].cost - a[1].cost);
267
+ for (const [model, bucket] of sorted) {
268
+ printDetailed(model.replace("claude-", ""), bucket);
269
+ }
270
+ }
271
+
272
+ if (Object.keys(cc.byProject).length > 1) {
273
+ console.log("\n By Project (all time)\n");
274
+ const sorted = Object.entries(cc.byProject).sort(
275
+ (a, b) => b[1].total.cost - a[1].total.cost
276
+ );
277
+ for (const [proj, tb] of sorted) {
278
+ printRow(proj, tb.total);
279
+ }
280
+ }
281
+
282
+ if (pal.buckets.total.calls > 0) {
283
+ console.log("\n PAL Inference (Haiku)\n");
284
+ printRow("Today", pal.buckets.today);
285
+ printRow("7d", pal.buckets.week);
286
+ printRow("30d", pal.buckets.month);
287
+ printRow("Total", pal.buckets.total);
288
+ }
289
+
290
+ // Grand total
291
+ const grand = emptyBucket();
292
+ for (const b of [cc.buckets.total, pal.buckets.total]) {
293
+ grand.input += b.input;
294
+ grand.output += b.output;
295
+ grand.cacheWrite += b.cacheWrite;
296
+ grand.cacheRead += b.cacheRead;
297
+ grand.cost += b.cost;
298
+ grand.calls += b.calls;
299
+ }
300
+
301
+ console.log(`\n Grand Total: ${fmtCost(grand.cost)}\n`);
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env bun
2
+
3
+ /**
4
+ * YouTube Analyze — Sends a YouTube URL + prompt to Gemini for video analysis.
5
+ *
6
+ * Gemini can natively process YouTube videos (visual + audio).
7
+ * Requires GEMINI_API_KEY environment variable.
8
+ *
9
+ * Usage:
10
+ * bun run ai:youtube-analyze -- <youtube-url> [--prompt "your question"]
11
+ *
12
+ * Default prompt extracts a structured summary with key insights.
13
+ */
14
+
15
+ import { parseArgs } from "node:util";
16
+
17
+ const DEFAULT_PROMPT = `Analyze this video and provide:
18
+ - **Title & Channel**
19
+ - **Summary** (3-5 sentences)
20
+ - **Key Insights** (bullet points)
21
+ - **Topics** covered
22
+ - **People & Companies** mentioned (with context)
23
+ - **Notable Quotes** (verbatim if possible)`;
24
+
25
+ const MODEL = "gemini-3.1-flash-lite-preview";
26
+
27
+ function loadApiKey(): string {
28
+ const key = process.env.GEMINI_API_KEY;
29
+ if (!key) {
30
+ console.error("Error: GEMINI_API_KEY environment variable is not set.");
31
+ console.error("Get a free key at https://aistudio.google.com/apikey");
32
+ process.exit(1);
33
+ }
34
+ return key;
35
+ }
36
+
37
+ async function main() {
38
+ const { positionals, values } = parseArgs({
39
+ allowPositionals: true,
40
+ options: {
41
+ prompt: { type: "string", short: "p" },
42
+ },
43
+ });
44
+
45
+ const url = positionals[0];
46
+ if (!url) {
47
+ console.error(
48
+ 'Usage: bun run ai:youtube-analyze -- <youtube-url> [--prompt "your question"]'
49
+ );
50
+ process.exit(1);
51
+ }
52
+
53
+ if (!url.includes("youtube.com/") && !url.includes("youtu.be/")) {
54
+ console.error("Error: URL does not look like a YouTube link.");
55
+ process.exit(1);
56
+ }
57
+
58
+ const apiKey = loadApiKey();
59
+ const prompt = values.prompt ?? DEFAULT_PROMPT;
60
+
61
+ const response = await fetch(
62
+ `https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent?key=${apiKey}`,
63
+ {
64
+ method: "POST",
65
+ headers: { "content-type": "application/json" },
66
+ body: JSON.stringify({
67
+ contents: [
68
+ {
69
+ parts: [
70
+ {
71
+ fileData: {
72
+ mimeType: "video/*",
73
+ fileUri: url,
74
+ },
75
+ },
76
+ { text: prompt },
77
+ ],
78
+ },
79
+ ],
80
+ }),
81
+ }
82
+ );
83
+
84
+ if (!response.ok) {
85
+ const err = await response.text().catch(() => "");
86
+ console.error(`Error: HTTP ${response.status} — ${err.slice(0, 500)}`);
87
+ process.exit(1);
88
+ }
89
+
90
+ const data = (await response.json()) as {
91
+ candidates?: Array<{
92
+ content?: { parts?: Array<{ text?: string }> };
93
+ }>;
94
+ };
95
+
96
+ const text = data.candidates?.[0]?.content?.parts?.[0]?.text;
97
+ if (!text) {
98
+ console.error("Error: No response content from Gemini.");
99
+ process.exit(1);
100
+ }
101
+
102
+ console.log(text);
103
+ }
104
+
105
+ main();