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,328 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL plugin for opencode — thin adapter over shared hooks/lib/ modules.
|
|
3
|
+
*
|
|
4
|
+
* All business logic lives in hooks/lib/ so it stays in sync with Claude Code hooks.
|
|
5
|
+
* This plugin just wires opencode's hook API to those shared functions.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { writeFileSync } from "node:fs";
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
|
|
11
|
+
|
|
12
|
+
const PAL_DIR = process.env.PAL_DIR || resolve(import.meta.dir, "../../..");
|
|
13
|
+
|
|
14
|
+
// Dynamic imports from shared lib — resolved at runtime via PAL_DIR
|
|
15
|
+
async function lib<T>(mod: string): Promise<T> {
|
|
16
|
+
return await import(resolve(PAL_DIR, "src", "hooks", "lib", mod));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type TranscriptMessage = { role: string; content: string };
|
|
20
|
+
|
|
21
|
+
const PALPlugin: Plugin = async ({ directory, client }: PluginInput) => {
|
|
22
|
+
// Pre-load shared modules
|
|
23
|
+
const { buildGreeting, buildSystemReminder } =
|
|
24
|
+
await lib<typeof import("../../hooks/lib/context")>("context.ts");
|
|
25
|
+
const { checkBashCommand, checkFilePath } =
|
|
26
|
+
await lib<typeof import("../../hooks/lib/security")>("security.ts");
|
|
27
|
+
const { paths, ensureDir } =
|
|
28
|
+
await lib<typeof import("../../hooks/lib/paths")>("paths.ts");
|
|
29
|
+
const { emitRating } =
|
|
30
|
+
await lib<typeof import("../../hooks/lib/signals")>("signals.ts");
|
|
31
|
+
const { now } = await lib<typeof import("../../hooks/lib/time")>("time.ts");
|
|
32
|
+
const { monthPath, fileTimestamp } =
|
|
33
|
+
await lib<typeof import("../../hooks/lib/time")>("time.ts");
|
|
34
|
+
const { logDebug, logError } =
|
|
35
|
+
await lib<typeof import("../../hooks/lib/log")>("log.ts");
|
|
36
|
+
|
|
37
|
+
// Load shared stop-orchestrator handler
|
|
38
|
+
const { runStopHandlers } = await lib<typeof import("../../hooks/lib/stop")>("stop.ts");
|
|
39
|
+
const { captureSessionName } = await lib<
|
|
40
|
+
typeof import("../../hooks/handlers/session-name")
|
|
41
|
+
>("../handlers/session-name.ts");
|
|
42
|
+
|
|
43
|
+
function partsToText(parts: Array<Record<string, unknown>>): string {
|
|
44
|
+
return parts
|
|
45
|
+
.filter((p) => p?.type === "text" && !p.ignored && !p.synthetic)
|
|
46
|
+
.map((p) => (typeof p.text === "string" ? p.text : ""))
|
|
47
|
+
.join(" ")
|
|
48
|
+
.trim();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function buildSessionTranscript(sessionID: string): Promise<TranscriptMessage[]> {
|
|
52
|
+
const result = await client.session.messages({
|
|
53
|
+
path: { id: sessionID },
|
|
54
|
+
query: { directory },
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (result.error || !result.data) {
|
|
58
|
+
logDebug(
|
|
59
|
+
"opencode:session.messages",
|
|
60
|
+
`Failed to fetch messages (error=${Boolean(result.error)})`
|
|
61
|
+
);
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const rows = result.data as Array<{
|
|
66
|
+
info: { role?: string };
|
|
67
|
+
parts: Array<Record<string, unknown>>;
|
|
68
|
+
}>;
|
|
69
|
+
|
|
70
|
+
return rows
|
|
71
|
+
.map((row) => {
|
|
72
|
+
const role = row?.info?.role ?? "unknown";
|
|
73
|
+
const content = partsToText(row?.parts ?? []);
|
|
74
|
+
return { role, content };
|
|
75
|
+
})
|
|
76
|
+
.filter((m) => m.content.length > 0);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const { categorizeLearning } =
|
|
80
|
+
await lib<typeof import("../../hooks/lib/learning-category")>("learning-category.ts");
|
|
81
|
+
|
|
82
|
+
// Local helpers for rating (thin wrappers around shared signals)
|
|
83
|
+
function handleRating(
|
|
84
|
+
rating: number,
|
|
85
|
+
context: string,
|
|
86
|
+
source: string,
|
|
87
|
+
detailedContext?: string,
|
|
88
|
+
userMessage?: string
|
|
89
|
+
): void {
|
|
90
|
+
emitRating(rating, context, source);
|
|
91
|
+
|
|
92
|
+
if (rating < 5) {
|
|
93
|
+
const category = categorizeLearning(context, detailedContext ?? "");
|
|
94
|
+
const dir = ensureDir(resolve(paths.sessionLearning(), monthPath()));
|
|
95
|
+
const filename = `${fileTimestamp()}_${source}-rating-${rating}_${category}.md`;
|
|
96
|
+
writeFileSync(
|
|
97
|
+
resolve(dir, filename),
|
|
98
|
+
[
|
|
99
|
+
`# ${source === "explicit" ? "Low Rating" : "Implicit Low Rating"}: ${rating}/10`,
|
|
100
|
+
`**Title:** ${context.slice(0, 100) || "(low rating)"}`,
|
|
101
|
+
`**Date:** ${new Date().toISOString().slice(0, 10)}`,
|
|
102
|
+
`**Rating:** ${rating}/10`,
|
|
103
|
+
`**Source:** ${source}`,
|
|
104
|
+
`**Category:** ${category.toUpperCase()}`,
|
|
105
|
+
"",
|
|
106
|
+
"## Context",
|
|
107
|
+
context || "*(unavailable)*",
|
|
108
|
+
"",
|
|
109
|
+
...(detailedContext ? ["## Analysis", detailedContext, ""] : []),
|
|
110
|
+
].join("\n")
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (rating <= 3) {
|
|
115
|
+
const userPreview = userMessage?.slice(0, 400);
|
|
116
|
+
writeFileSync(
|
|
117
|
+
resolve(paths.state(), "pending-failure.json"),
|
|
118
|
+
JSON.stringify(
|
|
119
|
+
{ rating, context, source, detailedContext, userPreview, ts: now() },
|
|
120
|
+
null,
|
|
121
|
+
2
|
|
122
|
+
),
|
|
123
|
+
"utf-8"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const PRAISE_PATTERNS =
|
|
129
|
+
/^(great\s*job|nice|perfect|awesome|excellent|thanks|thank\s*you|well\s*done|good\s*job|love\s*it|amazing|brilliant|fantastic|wonderful|superb|nailed\s*it)[.!?]?$/i;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
// --- Per-message: Inject dynamic system reminder ---
|
|
133
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
134
|
+
const reminder = buildSystemReminder();
|
|
135
|
+
if (reminder) output.system.push(reminder);
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
// --- Session events: start and stop handling ---
|
|
139
|
+
event: async ({ event }) => {
|
|
140
|
+
logDebug("opencode:event", `Event: ${event.type}`);
|
|
141
|
+
|
|
142
|
+
if (event.type === "session.created" || event.type === "session.updated") {
|
|
143
|
+
const { regenerateIfNeeded } =
|
|
144
|
+
await lib<typeof import("../../hooks/lib/claude-md")>("claude-md.ts");
|
|
145
|
+
regenerateIfNeeded();
|
|
146
|
+
console.log(buildGreeting().join("\n"));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (event.type === "session.idle" || event.type === "session.diff") {
|
|
150
|
+
logDebug("opencode:event", "Running stop handlers...");
|
|
151
|
+
try {
|
|
152
|
+
const sessionID = (event as { properties?: { sessionID?: string } })?.properties
|
|
153
|
+
?.sessionID;
|
|
154
|
+
if (!sessionID) {
|
|
155
|
+
logDebug("opencode:event", "Skipping stop handlers: missing sessionID");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const messages = await buildSessionTranscript(sessionID);
|
|
160
|
+
logDebug("opencode:event", `Got ${messages.length} transcript messages`);
|
|
161
|
+
if (messages.length < 2) return;
|
|
162
|
+
|
|
163
|
+
// Name session from first user message (if not already named)
|
|
164
|
+
const firstUser = messages.find((m: TranscriptMessage) => m.role === "user");
|
|
165
|
+
if (firstUser) {
|
|
166
|
+
await captureSessionName(firstUser.content, sessionID);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
await runStopHandlers(JSON.stringify(messages), { sessionId: sessionID });
|
|
170
|
+
logDebug("opencode:event", "Stop handlers complete");
|
|
171
|
+
} catch (err) {
|
|
172
|
+
logError("opencode:session.stop", err);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// --- Capture ratings from user messages ---
|
|
178
|
+
"chat.message": async (_input, output) => {
|
|
179
|
+
const text =
|
|
180
|
+
output.parts
|
|
181
|
+
?.filter((p) => p.type === "text")
|
|
182
|
+
.map((p) => p.text || "")
|
|
183
|
+
.join(" ") ?? "";
|
|
184
|
+
|
|
185
|
+
// Explicit rating
|
|
186
|
+
const match = text.match(
|
|
187
|
+
/(?:^|rating:?\s*|score:?\s*)(\d|10)(?:\s*(?:\/10|[-.])|$|\s)/i
|
|
188
|
+
);
|
|
189
|
+
if (match) {
|
|
190
|
+
const rating = parseInt(match[1], 10);
|
|
191
|
+
if (rating >= 1 && rating <= 10) {
|
|
192
|
+
handleRating(rating, text.slice(0, 200), "explicit", undefined, text);
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Implicit sentiment: auto-enabled when ANTHROPIC_API_KEY is set
|
|
198
|
+
if (process.env.ANTHROPIC_API_KEY) {
|
|
199
|
+
const trimmed = text.trim();
|
|
200
|
+
if (PRAISE_PATTERNS.test(trimmed)) {
|
|
201
|
+
handleRating(8, trimmed, "implicit", undefined, trimmed);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Full implicit via API — only for medium-length messages
|
|
206
|
+
if (
|
|
207
|
+
trimmed.length >= 5 &&
|
|
208
|
+
trimmed.length <= 500 &&
|
|
209
|
+
!/^[/$`{]/.test(trimmed) &&
|
|
210
|
+
!trimmed.includes("\n\n")
|
|
211
|
+
) {
|
|
212
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
213
|
+
if (apiKey) {
|
|
214
|
+
try {
|
|
215
|
+
const response = await fetch("https://api.anthropic.com/v1/messages", {
|
|
216
|
+
method: "POST",
|
|
217
|
+
headers: {
|
|
218
|
+
"x-api-key": apiKey,
|
|
219
|
+
"anthropic-version": "2023-06-01",
|
|
220
|
+
"content-type": "application/json",
|
|
221
|
+
},
|
|
222
|
+
body: JSON.stringify({
|
|
223
|
+
model: (await lib<{ HAIKU_MODEL: string }>("models")).HAIKU_MODEL,
|
|
224
|
+
max_tokens: 100,
|
|
225
|
+
messages: [
|
|
226
|
+
{
|
|
227
|
+
role: "user",
|
|
228
|
+
content: `Rate the sentiment of this user message toward an AI assistant on a 1-10 scale (1=very negative, 5=neutral, 10=very positive). If the message has no clear sentiment toward the assistant, respond with just "neutral". Otherwise respond with just a JSON object: {"rating": N, "sentiment": "one-word"}\n\nMessage: "${trimmed.slice(0, 300)}"`,
|
|
229
|
+
},
|
|
230
|
+
],
|
|
231
|
+
}),
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
if (response.ok) {
|
|
235
|
+
const data = (await response.json()) as {
|
|
236
|
+
content?: Array<{ text?: string }>;
|
|
237
|
+
};
|
|
238
|
+
const rText = data?.content?.[0]?.text?.trim();
|
|
239
|
+
if (rText && rText !== "neutral") {
|
|
240
|
+
try {
|
|
241
|
+
const parsed = JSON.parse(rText) as {
|
|
242
|
+
rating?: number;
|
|
243
|
+
sentiment?: string;
|
|
244
|
+
};
|
|
245
|
+
if (
|
|
246
|
+
typeof parsed.rating === "number" &&
|
|
247
|
+
parsed.rating >= 1 &&
|
|
248
|
+
parsed.rating <= 10 &&
|
|
249
|
+
parsed.rating !== 5
|
|
250
|
+
) {
|
|
251
|
+
handleRating(
|
|
252
|
+
parsed.rating,
|
|
253
|
+
`${parsed.sentiment || "inferred"}: ${trimmed.slice(0, 150)}`,
|
|
254
|
+
"implicit",
|
|
255
|
+
undefined,
|
|
256
|
+
trimmed
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
// Ignore parse errors
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} catch {
|
|
265
|
+
// Ignore API errors
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
// --- Security: block dangerous tool executions ---
|
|
273
|
+
"tool.execute.before": async (
|
|
274
|
+
_input: { tool: string; sessionID: string; callID: string },
|
|
275
|
+
output: { args: Record<string, unknown> | string }
|
|
276
|
+
) => {
|
|
277
|
+
const toolName = _input.tool;
|
|
278
|
+
|
|
279
|
+
if (toolName === "shell" || toolName === "bash") {
|
|
280
|
+
const cmd =
|
|
281
|
+
typeof output.args === "string"
|
|
282
|
+
? output.args
|
|
283
|
+
: ((output.args?.command as string) ?? "");
|
|
284
|
+
const reason = checkBashCommand(cmd);
|
|
285
|
+
if (reason) {
|
|
286
|
+
throw new Error(`PAL Security: Blocked — ${reason}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (toolName === "write" || toolName === "edit" || toolName === "patch") {
|
|
291
|
+
const args = output.args as Record<string, string>;
|
|
292
|
+
const filePath = args?.file_path ?? args?.filePath ?? args?.path ?? "";
|
|
293
|
+
const fileReason = checkFilePath(filePath);
|
|
294
|
+
if (fileReason) {
|
|
295
|
+
throw new Error(`PAL Security: ${fileReason}`);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
|
|
300
|
+
// --- Capture work state after tool use ---
|
|
301
|
+
"tool.execute.after": async (
|
|
302
|
+
input: { tool: string; sessionID: string; callID: string; args: unknown },
|
|
303
|
+
_output: { title: string; output: string; metadata: unknown }
|
|
304
|
+
) => {
|
|
305
|
+
try {
|
|
306
|
+
writeFileSync(
|
|
307
|
+
resolve(ensureDir(paths.state()), "current-work.json"),
|
|
308
|
+
JSON.stringify({ ts: now(), tool: input.tool, cwd: directory }, null, 2)
|
|
309
|
+
);
|
|
310
|
+
} catch {
|
|
311
|
+
// Ignore write errors
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
// --- Inject PAL_DIR into shell environment ---
|
|
316
|
+
"shell.env": async (
|
|
317
|
+
_input: { cwd: string; sessionID?: string; callID?: string },
|
|
318
|
+
output: { env: Record<string, string> }
|
|
319
|
+
) => {
|
|
320
|
+
output.env.PAL_DIR = PAL_DIR;
|
|
321
|
+
if (process.env.PAL_DEBUG) {
|
|
322
|
+
output.env.PAL_DEBUG = process.env.PAL_DEBUG;
|
|
323
|
+
}
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export default PALPlugin;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL — opencode uninstaller (TypeScript)
|
|
3
|
+
* Removes plugin and AGENTS.md.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { unlinkSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
import { platform } from "../../hooks/lib/paths";
|
|
9
|
+
import { log, removeAgentsFromOpencode, removeSkills } from "../lib";
|
|
10
|
+
|
|
11
|
+
const OC_GLOBAL_DIR = platform.opencodeDir() || "";
|
|
12
|
+
|
|
13
|
+
const PAL_CLAUDE_DIR = platform.claudeDir() || "";
|
|
14
|
+
|
|
15
|
+
if (!OC_GLOBAL_DIR || !PAL_CLAUDE_DIR) {
|
|
16
|
+
log.error("PAL_OPENCODE_DIR or PAL_CLAUDE_DIR not set");
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// --- Remove plugin ---
|
|
21
|
+
const pluginPath = resolve(OC_GLOBAL_DIR, "plugins", "pal-plugin.ts");
|
|
22
|
+
try {
|
|
23
|
+
unlinkSync(pluginPath);
|
|
24
|
+
log.success("Removed PAL plugin");
|
|
25
|
+
} catch {
|
|
26
|
+
log.info("No PAL plugin found");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// --- Remove skills ---
|
|
30
|
+
const removed = removeSkills(resolve(PAL_CLAUDE_DIR, "skills"));
|
|
31
|
+
if (removed.length > 0)
|
|
32
|
+
log.success(`Removed ${removed.length} skill(s): ${removed.join(", ")}`);
|
|
33
|
+
|
|
34
|
+
// --- Remove agents ---
|
|
35
|
+
const removedAgents = removeAgentsFromOpencode(resolve(OC_GLOBAL_DIR, "agents"));
|
|
36
|
+
if (removedAgents.length > 0)
|
|
37
|
+
log.success(
|
|
38
|
+
`Removed ${removedAgents.length} opencode agent(s): ${removedAgents.join(", ")}`
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
// --- Remove AGENTS.md and CLAUDE.md symlink ---
|
|
42
|
+
const agentsMd = resolve(OC_GLOBAL_DIR, "AGENTS.md");
|
|
43
|
+
const claudeMd = resolve(PAL_CLAUDE_DIR, "CLAUDE.md");
|
|
44
|
+
try {
|
|
45
|
+
unlinkSync(claudeMd);
|
|
46
|
+
log.success("Removed ~/.claude/CLAUDE.md");
|
|
47
|
+
} catch {
|
|
48
|
+
/* gone */
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
unlinkSync(agentsMd);
|
|
52
|
+
log.success("Removed ~/.config/opencode/AGENTS.md");
|
|
53
|
+
} catch {
|
|
54
|
+
/* gone */
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
log.success("opencode uninstall complete");
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Entity Save — Deduplicate and persist extracted entities.
|
|
4
|
+
*
|
|
5
|
+
* Accepts extracted people/companies JSON via stdin or --file,
|
|
6
|
+
* deduplicates against the entity index, and saves.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* echo '{"people":[...],"companies":[...]}' | bun run ai:entity-save -- --source "https://example.com"
|
|
10
|
+
* bun run ai:entity-save -- --file /path/to/extracted.json --source "https://example.com"
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { parseArgs } from "node:util";
|
|
15
|
+
import { loadEntityIndex, processEntities } from "../hooks/lib/entities";
|
|
16
|
+
|
|
17
|
+
const { values } = parseArgs({
|
|
18
|
+
args: Bun.argv.slice(2),
|
|
19
|
+
options: {
|
|
20
|
+
source: { type: "string", short: "s", default: "manual" },
|
|
21
|
+
file: { type: "string", short: "f" },
|
|
22
|
+
},
|
|
23
|
+
strict: true,
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const sourceId = values.source ?? "manual";
|
|
27
|
+
|
|
28
|
+
let raw: string;
|
|
29
|
+
if (values.file) {
|
|
30
|
+
raw = readFileSync(values.file, "utf-8");
|
|
31
|
+
} else {
|
|
32
|
+
raw = await Bun.stdin.text();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!raw.trim()) {
|
|
36
|
+
console.error("Error: No input provided. Pipe JSON via stdin or use --file.");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let data: {
|
|
41
|
+
people: Array<Record<string, unknown>>;
|
|
42
|
+
companies: Array<Record<string, unknown>>;
|
|
43
|
+
links?: Array<Record<string, unknown>>;
|
|
44
|
+
sources?: Array<Record<string, unknown>>;
|
|
45
|
+
};
|
|
46
|
+
try {
|
|
47
|
+
data = JSON.parse(raw);
|
|
48
|
+
} catch {
|
|
49
|
+
console.error("Error: Invalid JSON input.");
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (!Array.isArray(data.people) || !Array.isArray(data.companies)) {
|
|
54
|
+
console.error('Error: JSON must have "people" and "companies" arrays.');
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
data.links ??= [];
|
|
58
|
+
data.sources ??= [];
|
|
59
|
+
|
|
60
|
+
const before = loadEntityIndex();
|
|
61
|
+
const counts = (idx: ReturnType<typeof loadEntityIndex>) => ({
|
|
62
|
+
people: Object.keys(idx.people).length,
|
|
63
|
+
companies: Object.keys(idx.companies).length,
|
|
64
|
+
links: Object.keys(idx.links).length,
|
|
65
|
+
sources: Object.keys(idx.sources).length,
|
|
66
|
+
});
|
|
67
|
+
const cb = counts(before);
|
|
68
|
+
|
|
69
|
+
const result = processEntities(
|
|
70
|
+
{
|
|
71
|
+
people: data.people as Array<{ name: string; [key: string]: unknown }>,
|
|
72
|
+
companies: data.companies as Array<{
|
|
73
|
+
name: string;
|
|
74
|
+
domain: string | null;
|
|
75
|
+
[key: string]: unknown;
|
|
76
|
+
}>,
|
|
77
|
+
links: data.links as Array<{ url: string; [key: string]: unknown }>,
|
|
78
|
+
sources: data.sources as Array<{
|
|
79
|
+
url: string | null;
|
|
80
|
+
author: string | null;
|
|
81
|
+
publication: string | null;
|
|
82
|
+
[key: string]: unknown;
|
|
83
|
+
}>,
|
|
84
|
+
},
|
|
85
|
+
sourceId
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
const ca = counts(loadEntityIndex());
|
|
89
|
+
|
|
90
|
+
console.log(
|
|
91
|
+
JSON.stringify(
|
|
92
|
+
{
|
|
93
|
+
saved: {
|
|
94
|
+
people: result.people.length,
|
|
95
|
+
companies: result.companies.length,
|
|
96
|
+
links: result.links.length,
|
|
97
|
+
sources: result.sources.length,
|
|
98
|
+
},
|
|
99
|
+
new: {
|
|
100
|
+
people: ca.people - cb.people,
|
|
101
|
+
companies: ca.companies - cb.companies,
|
|
102
|
+
links: ca.links - cb.links,
|
|
103
|
+
sources: ca.sources - cb.sources,
|
|
104
|
+
},
|
|
105
|
+
total: ca,
|
|
106
|
+
},
|
|
107
|
+
null,
|
|
108
|
+
2
|
|
109
|
+
)
|
|
110
|
+
);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PAL Export — Zips all gitignored personal files (memory, telos, state)
|
|
3
|
+
* into a portable archive for transfer between machines.
|
|
4
|
+
*
|
|
5
|
+
* Usage: bun run tool:export [output-path] [--dry-run]
|
|
6
|
+
* Default output: pal-export-YYYYMMDD-HHmmss.zip in the repo root.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { resolve } from "node:path";
|
|
10
|
+
import { collectExportFiles, exportZip, timestamp } from "../hooks/lib/export";
|
|
11
|
+
import { palHome } from "../hooks/lib/paths";
|
|
12
|
+
|
|
13
|
+
const args = process.argv.slice(2);
|
|
14
|
+
const dryRun = args.includes("--dry-run");
|
|
15
|
+
const pathArg = args.find((a) => a !== "--dry-run");
|
|
16
|
+
|
|
17
|
+
const outputPath = pathArg || resolve(palHome(), `pal-export-${timestamp()}.zip`);
|
|
18
|
+
|
|
19
|
+
if (dryRun) {
|
|
20
|
+
const files = collectExportFiles();
|
|
21
|
+
if (files.length === 0) {
|
|
22
|
+
console.log("Nothing to export — no gitignored personal files found.");
|
|
23
|
+
} else {
|
|
24
|
+
console.log(`Would export ${files.length} files → ${outputPath}\n`);
|
|
25
|
+
for (const f of files) console.log(` ${f}`);
|
|
26
|
+
}
|
|
27
|
+
} else {
|
|
28
|
+
const count = exportZip(outputPath);
|
|
29
|
+
if (count === 0) {
|
|
30
|
+
console.log("Nothing to export — no gitignored personal files found.");
|
|
31
|
+
} else {
|
|
32
|
+
console.log(`Exported ${count} files → ${outputPath}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
/**
|
|
3
|
+
* Fyzz Chat API — CLI wrapper for programmatic conversation access.
|
|
4
|
+
*
|
|
5
|
+
* Reads the API key from FYZZ_API_KEY env var (never printed to stdout).
|
|
6
|
+
* Returns JSON responses from the Fyzz Chat REST API.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* bun run ai:fyzz-api -- conversations [--limit 20] [--search "query"] [--project-id <id>] [--cursor <cursor>]
|
|
10
|
+
* bun run ai:fyzz-api -- conversations <id>
|
|
11
|
+
* bun run ai:fyzz-api -- projects
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { parseArgs } from "node:util";
|
|
15
|
+
|
|
16
|
+
function loadApiKey(): string {
|
|
17
|
+
const key = process.env.FYZZ_API_KEY;
|
|
18
|
+
if (!key) {
|
|
19
|
+
console.error("Error: FYZZ_API_KEY environment variable is not set.");
|
|
20
|
+
console.error("Set it in your shell profile or PAL settings.json env section.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
return key;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function apiFetch(path: string, params?: Record<string, string>): Promise<unknown> {
|
|
27
|
+
const apiKey = loadApiKey();
|
|
28
|
+
const baseUrl = process.env.FYZZ_BASE_URL ?? "http://localhost:3000";
|
|
29
|
+
|
|
30
|
+
const url = new URL(`/api/v1${path}`, baseUrl);
|
|
31
|
+
if (params) {
|
|
32
|
+
for (const [k, v] of Object.entries(params)) {
|
|
33
|
+
if (v !== undefined && v !== "") url.searchParams.set(k, v);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const response = await fetch(url.toString(), {
|
|
38
|
+
headers: { Authorization: `Bearer ${apiKey}` },
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (!response.ok) {
|
|
42
|
+
console.error(`Error: ${response.status} ${response.statusText}`);
|
|
43
|
+
const body = await response.text();
|
|
44
|
+
if (body) console.error(body);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return response.json();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const args = process.argv.slice(2);
|
|
52
|
+
const command = args[0];
|
|
53
|
+
|
|
54
|
+
if (!command || command === "--help" || command === "-h") {
|
|
55
|
+
console.log("Usage:");
|
|
56
|
+
console.log(" bun run ai:fyzz-api -- conversations List conversations");
|
|
57
|
+
console.log(
|
|
58
|
+
" bun run ai:fyzz-api -- conversations <id> Get conversation with messages"
|
|
59
|
+
);
|
|
60
|
+
console.log(" bun run ai:fyzz-api -- projects List projects");
|
|
61
|
+
console.log("");
|
|
62
|
+
console.log("Options for 'conversations' (list mode):");
|
|
63
|
+
console.log(" --limit <n> Max results (default 20)");
|
|
64
|
+
console.log(" --search <query> Search in titles and messages");
|
|
65
|
+
console.log(" --project-id <id> Filter by project");
|
|
66
|
+
console.log(" --cursor <cursor> Pagination cursor");
|
|
67
|
+
process.exit(0);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (command === "conversations") {
|
|
71
|
+
const secondArg = args[1];
|
|
72
|
+
|
|
73
|
+
if (secondArg && !secondArg.startsWith("--")) {
|
|
74
|
+
const result = await apiFetch(`/conversations/${secondArg}`);
|
|
75
|
+
console.log(JSON.stringify(result, null, 2));
|
|
76
|
+
} else {
|
|
77
|
+
const { values } = parseArgs({
|
|
78
|
+
args: args.slice(1),
|
|
79
|
+
options: {
|
|
80
|
+
limit: { type: "string", short: "l", default: "20" },
|
|
81
|
+
search: { type: "string", short: "s" },
|
|
82
|
+
"project-id": { type: "string", short: "p" },
|
|
83
|
+
cursor: { type: "string", short: "c" },
|
|
84
|
+
},
|
|
85
|
+
strict: true,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const params: Record<string, string> = {};
|
|
89
|
+
if (values.limit) params.limit = values.limit;
|
|
90
|
+
if (values.search) params.search = values.search;
|
|
91
|
+
if (values["project-id"]) params.projectId = values["project-id"];
|
|
92
|
+
if (values.cursor) params.cursor = values.cursor;
|
|
93
|
+
|
|
94
|
+
const result = await apiFetch("/conversations", params);
|
|
95
|
+
console.log(JSON.stringify(result, null, 2));
|
|
96
|
+
}
|
|
97
|
+
} else if (command === "projects") {
|
|
98
|
+
const result = await apiFetch("/projects");
|
|
99
|
+
console.log(JSON.stringify(result, null, 2));
|
|
100
|
+
} else {
|
|
101
|
+
console.error(`Unknown command: ${command}`);
|
|
102
|
+
console.error("Run with --help for usage.");
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|