portable-agent-layer 0.41.0 → 0.42.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/.husky/install.mjs +8 -0
- package/README.md +2 -1
- package/assets/skills/analyze-youtube/SKILL.md +1 -1
- package/assets/skills/entities/SKILL.md +95 -0
- package/assets/templates/PAL/README.md +1 -1
- package/package.json +10 -12
- package/src/cli/index.ts +8 -0
- package/src/cli/knowledge.ts +620 -0
- package/src/cli/migrate.ts +188 -3
- package/src/hooks/handlers/reflect-trigger.ts +1 -0
- package/src/hooks/handlers/update-check.ts +4 -0
- package/src/hooks/lib/detached-inference.ts +1 -0
- package/src/hooks/lib/export.ts +1 -1
- package/src/hooks/lib/inference.ts +1 -0
- package/src/hooks/lib/paths.ts +2 -1
- package/src/hooks/lib/retrieval-index.ts +1 -0
- package/src/targets/lib.ts +23 -36
- package/src/tools/knowledge/graph.ts +395 -0
- package/src/tools/knowledge/ingest.ts +409 -0
- package/src/tools/knowledge/lib.ts +493 -0
- package/assets/skills/extract-entities/SKILL.md +0 -62
- package/assets/skills/extract-entities/tools/entity-save.ts +0 -110
- package/src/hooks/lib/entities.ts +0 -304
- package/src/tools/export.ts +0 -40
- package/src/tools/import.ts +0 -111
|
@@ -1,304 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Entity Collision Detection — deduplicates people, companies, links,
|
|
3
|
-
* and sources across extracted content, assigning stable UUIDs and
|
|
4
|
-
* tracking occurrences to build a knowledge graph.
|
|
5
|
-
*
|
|
6
|
-
* Ported from ~/git/Personal_AI_Infrastructure/Packs/Utilities/src/Parser/Utils/collision-detection.ts
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
10
|
-
import { resolve } from "node:path";
|
|
11
|
-
import { ensureDir, paths } from "./paths";
|
|
12
|
-
|
|
13
|
-
// --- Types ---
|
|
14
|
-
|
|
15
|
-
interface PersonEntity {
|
|
16
|
-
id: string;
|
|
17
|
-
name: string;
|
|
18
|
-
first_seen: string;
|
|
19
|
-
occurrences: number;
|
|
20
|
-
source_ids: string[];
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
interface CompanyEntity {
|
|
24
|
-
id: string;
|
|
25
|
-
name: string;
|
|
26
|
-
domain: string | null;
|
|
27
|
-
first_seen: string;
|
|
28
|
-
occurrences: number;
|
|
29
|
-
source_ids: string[];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
interface LinkEntity {
|
|
33
|
-
id: string;
|
|
34
|
-
url: string;
|
|
35
|
-
first_seen: string;
|
|
36
|
-
occurrences: number;
|
|
37
|
-
source_ids: string[];
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
interface SourceEntity {
|
|
41
|
-
id: string;
|
|
42
|
-
url: string | null;
|
|
43
|
-
author: string | null;
|
|
44
|
-
publication: string | null;
|
|
45
|
-
first_seen: string;
|
|
46
|
-
occurrences: number;
|
|
47
|
-
source_ids: string[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface EntityIndex {
|
|
51
|
-
version: string;
|
|
52
|
-
last_updated: string;
|
|
53
|
-
people: Record<string, PersonEntity>;
|
|
54
|
-
companies: Record<string, CompanyEntity>;
|
|
55
|
-
links: Record<string, LinkEntity>;
|
|
56
|
-
sources: Record<string, SourceEntity>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// --- Normalization ---
|
|
60
|
-
|
|
61
|
-
export function normalizeName(name: string): string {
|
|
62
|
-
return name.toLowerCase().trim();
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export function normalizeCompanyKey(name: string, domain: string | null): string {
|
|
66
|
-
return domain ? domain.toLowerCase().trim() : normalizeName(name);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export function normalizeUrl(url: string): string {
|
|
70
|
-
return url.toLowerCase().trim().replace(/\/$/, "");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
export function normalizeSourceKey(
|
|
74
|
-
url: string | null,
|
|
75
|
-
author: string | null,
|
|
76
|
-
publication: string | null
|
|
77
|
-
): string {
|
|
78
|
-
if (url) return normalizeUrl(url);
|
|
79
|
-
const a = author ? normalizeName(author) : "";
|
|
80
|
-
const p = publication ? normalizeName(publication) : "";
|
|
81
|
-
return `${a}|${p}`;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
// --- Index I/O ---
|
|
85
|
-
|
|
86
|
-
function defaultIndexPath(): string {
|
|
87
|
-
return resolve(ensureDir(paths.entities()), "entity-index.json");
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function emptyIndex(): EntityIndex {
|
|
91
|
-
return {
|
|
92
|
-
version: "1.1.0",
|
|
93
|
-
last_updated: new Date().toISOString(),
|
|
94
|
-
people: {},
|
|
95
|
-
companies: {},
|
|
96
|
-
links: {},
|
|
97
|
-
sources: {},
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/** Migrate older indexes that lack links/sources. */
|
|
102
|
-
function ensureShape(index: EntityIndex): EntityIndex {
|
|
103
|
-
index.links ??= {};
|
|
104
|
-
index.sources ??= {};
|
|
105
|
-
return index;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
export function loadEntityIndex(filepath?: string): EntityIndex {
|
|
109
|
-
const p = filepath ?? defaultIndexPath();
|
|
110
|
-
if (!existsSync(p)) return emptyIndex();
|
|
111
|
-
try {
|
|
112
|
-
return ensureShape(JSON.parse(readFileSync(p, "utf-8")));
|
|
113
|
-
} catch {
|
|
114
|
-
return emptyIndex();
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function saveEntityIndex(index: EntityIndex, filepath?: string): void {
|
|
119
|
-
const p = filepath ?? defaultIndexPath();
|
|
120
|
-
const tempPath = `${p}.tmp`;
|
|
121
|
-
index.last_updated = new Date().toISOString();
|
|
122
|
-
writeFileSync(tempPath, JSON.stringify(index, null, 2), "utf-8");
|
|
123
|
-
renameSync(tempPath, p);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// --- Deduplication ---
|
|
127
|
-
|
|
128
|
-
export function getOrCreatePerson(
|
|
129
|
-
person: { name: string },
|
|
130
|
-
index: EntityIndex,
|
|
131
|
-
sourceId: string
|
|
132
|
-
): string {
|
|
133
|
-
const key = normalizeName(person.name);
|
|
134
|
-
const existing = index.people[key];
|
|
135
|
-
|
|
136
|
-
if (existing) {
|
|
137
|
-
if (!existing.source_ids.includes(sourceId)) {
|
|
138
|
-
existing.occurrences++;
|
|
139
|
-
existing.source_ids.push(sourceId);
|
|
140
|
-
}
|
|
141
|
-
return existing.id;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
const id = crypto.randomUUID();
|
|
145
|
-
index.people[key] = {
|
|
146
|
-
id,
|
|
147
|
-
name: person.name,
|
|
148
|
-
first_seen: new Date().toISOString(),
|
|
149
|
-
occurrences: 1,
|
|
150
|
-
source_ids: [sourceId],
|
|
151
|
-
};
|
|
152
|
-
return id;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export function getOrCreateCompany(
|
|
156
|
-
company: { name: string; domain: string | null },
|
|
157
|
-
index: EntityIndex,
|
|
158
|
-
sourceId: string
|
|
159
|
-
): string {
|
|
160
|
-
const key = normalizeCompanyKey(company.name, company.domain);
|
|
161
|
-
const existing = index.companies[key];
|
|
162
|
-
|
|
163
|
-
if (existing) {
|
|
164
|
-
if (!existing.source_ids.includes(sourceId)) {
|
|
165
|
-
existing.occurrences++;
|
|
166
|
-
existing.source_ids.push(sourceId);
|
|
167
|
-
}
|
|
168
|
-
return existing.id;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const id = crypto.randomUUID();
|
|
172
|
-
index.companies[key] = {
|
|
173
|
-
id,
|
|
174
|
-
name: company.name,
|
|
175
|
-
domain: company.domain,
|
|
176
|
-
first_seen: new Date().toISOString(),
|
|
177
|
-
occurrences: 1,
|
|
178
|
-
source_ids: [sourceId],
|
|
179
|
-
};
|
|
180
|
-
return id;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
export function getOrCreateLink(
|
|
184
|
-
link: { url: string },
|
|
185
|
-
index: EntityIndex,
|
|
186
|
-
sourceId: string
|
|
187
|
-
): string {
|
|
188
|
-
const key = normalizeUrl(link.url);
|
|
189
|
-
const existing = index.links[key];
|
|
190
|
-
|
|
191
|
-
if (existing) {
|
|
192
|
-
if (!existing.source_ids.includes(sourceId)) {
|
|
193
|
-
existing.occurrences++;
|
|
194
|
-
existing.source_ids.push(sourceId);
|
|
195
|
-
}
|
|
196
|
-
return existing.id;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
const id = crypto.randomUUID();
|
|
200
|
-
index.links[key] = {
|
|
201
|
-
id,
|
|
202
|
-
url: link.url,
|
|
203
|
-
first_seen: new Date().toISOString(),
|
|
204
|
-
occurrences: 1,
|
|
205
|
-
source_ids: [sourceId],
|
|
206
|
-
};
|
|
207
|
-
return id;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function getOrCreateSource(
|
|
211
|
-
source: { url: string | null; author: string | null; publication: string | null },
|
|
212
|
-
index: EntityIndex,
|
|
213
|
-
sourceId: string
|
|
214
|
-
): string {
|
|
215
|
-
const key = normalizeSourceKey(source.url, source.author, source.publication);
|
|
216
|
-
const existing = index.sources[key];
|
|
217
|
-
|
|
218
|
-
if (existing) {
|
|
219
|
-
if (!existing.source_ids.includes(sourceId)) {
|
|
220
|
-
existing.occurrences++;
|
|
221
|
-
existing.source_ids.push(sourceId);
|
|
222
|
-
}
|
|
223
|
-
return existing.id;
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const id = crypto.randomUUID();
|
|
227
|
-
index.sources[key] = {
|
|
228
|
-
id,
|
|
229
|
-
url: source.url,
|
|
230
|
-
author: source.author,
|
|
231
|
-
publication: source.publication,
|
|
232
|
-
first_seen: new Date().toISOString(),
|
|
233
|
-
occurrences: 1,
|
|
234
|
-
source_ids: [sourceId],
|
|
235
|
-
};
|
|
236
|
-
return id;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Check if a URL has already been parsed (exists in the links index). */
|
|
240
|
-
export function isUrlAlreadyParsed(url: string, index: EntityIndex): boolean {
|
|
241
|
-
const key = normalizeUrl(url);
|
|
242
|
-
const link = index.links[key];
|
|
243
|
-
return !!link && link.source_ids.length > 0;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
/** Get the first source_id that referenced this URL, or null. */
|
|
247
|
-
export function getExistingContentId(url: string, index: EntityIndex): string | null {
|
|
248
|
-
const key = normalizeUrl(url);
|
|
249
|
-
const link = index.links[key];
|
|
250
|
-
return link?.source_ids[0] ?? null;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// --- Batch processing ---
|
|
254
|
-
|
|
255
|
-
export function processEntities(
|
|
256
|
-
extractedData: {
|
|
257
|
-
people: Array<{ name: string; [key: string]: unknown }>;
|
|
258
|
-
companies: Array<{
|
|
259
|
-
name: string;
|
|
260
|
-
domain: string | null;
|
|
261
|
-
[key: string]: unknown;
|
|
262
|
-
}>;
|
|
263
|
-
links?: Array<{ url: string; [key: string]: unknown }>;
|
|
264
|
-
sources?: Array<{
|
|
265
|
-
url: string | null;
|
|
266
|
-
author: string | null;
|
|
267
|
-
publication: string | null;
|
|
268
|
-
[key: string]: unknown;
|
|
269
|
-
}>;
|
|
270
|
-
},
|
|
271
|
-
sourceId: string,
|
|
272
|
-
indexPath?: string
|
|
273
|
-
): {
|
|
274
|
-
people: Array<{ id: string; [key: string]: unknown }>;
|
|
275
|
-
companies: Array<{ id: string; [key: string]: unknown }>;
|
|
276
|
-
links: Array<{ id: string; [key: string]: unknown }>;
|
|
277
|
-
sources: Array<{ id: string; [key: string]: unknown }>;
|
|
278
|
-
} {
|
|
279
|
-
const index = loadEntityIndex(indexPath);
|
|
280
|
-
|
|
281
|
-
const people = extractedData.people.map((person) => ({
|
|
282
|
-
...person,
|
|
283
|
-
id: getOrCreatePerson(person, index, sourceId),
|
|
284
|
-
}));
|
|
285
|
-
|
|
286
|
-
const companies = extractedData.companies.map((company) => ({
|
|
287
|
-
...company,
|
|
288
|
-
id: getOrCreateCompany(company, index, sourceId),
|
|
289
|
-
}));
|
|
290
|
-
|
|
291
|
-
const links = (extractedData.links ?? []).map((link) => ({
|
|
292
|
-
...link,
|
|
293
|
-
id: getOrCreateLink(link, index, sourceId),
|
|
294
|
-
}));
|
|
295
|
-
|
|
296
|
-
const sources = (extractedData.sources ?? []).map((source) => ({
|
|
297
|
-
...source,
|
|
298
|
-
id: getOrCreateSource(source, index, sourceId),
|
|
299
|
-
}));
|
|
300
|
-
|
|
301
|
-
saveEntityIndex(index, indexPath);
|
|
302
|
-
|
|
303
|
-
return { people, companies, links, sources };
|
|
304
|
-
}
|
package/src/tools/export.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
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
|
-
export { collectExportFiles, exportZip };
|
|
14
|
-
|
|
15
|
-
function run() {
|
|
16
|
-
const args = process.argv.slice(2);
|
|
17
|
-
const dryRun = args.includes("--dry-run");
|
|
18
|
-
const pathArg = args.find((a) => a !== "--dry-run");
|
|
19
|
-
|
|
20
|
-
const outputPath = pathArg || resolve(palHome(), `pal-export-${timestamp()}.zip`);
|
|
21
|
-
|
|
22
|
-
if (dryRun) {
|
|
23
|
-
const files = collectExportFiles();
|
|
24
|
-
if (files.length === 0) {
|
|
25
|
-
console.log("Nothing to export — no gitignored personal files found.");
|
|
26
|
-
} else {
|
|
27
|
-
console.log(`Would export ${files.length} files → ${outputPath}\n`);
|
|
28
|
-
for (const f of files) console.log(` ${f}`);
|
|
29
|
-
}
|
|
30
|
-
} else {
|
|
31
|
-
const count = exportZip(outputPath);
|
|
32
|
-
if (count === 0) {
|
|
33
|
-
console.log("Nothing to export — no gitignored personal files found.");
|
|
34
|
-
} else {
|
|
35
|
-
console.log(`Exported ${count} files → ${outputPath}`);
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (import.meta.main) run();
|
package/src/tools/import.ts
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
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
|
-
export function findLatestExport(root: string): string | null {
|
|
17
|
-
const candidates: string[] = [];
|
|
18
|
-
|
|
19
|
-
try {
|
|
20
|
-
candidates.push(
|
|
21
|
-
...readdirSync(root)
|
|
22
|
-
.filter((f) => f.startsWith("pal-export-") && f.endsWith(".zip"))
|
|
23
|
-
.map((f) => resolve(root, f))
|
|
24
|
-
);
|
|
25
|
-
} catch {
|
|
26
|
-
/* empty */
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
try {
|
|
30
|
-
const backupDir = resolve(root, "backups");
|
|
31
|
-
candidates.push(
|
|
32
|
-
...readdirSync(backupDir)
|
|
33
|
-
.filter(
|
|
34
|
-
(f) =>
|
|
35
|
-
(f.startsWith("pal-export-") || f.startsWith("pal-backup-")) &&
|
|
36
|
-
f.endsWith(".zip")
|
|
37
|
-
)
|
|
38
|
-
.map((f) => resolve(backupDir, f))
|
|
39
|
-
);
|
|
40
|
-
} catch {
|
|
41
|
-
/* empty */
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (candidates.length === 0) return null;
|
|
45
|
-
return candidates.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)[0];
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export function importZip(zipPath: string, targetDir: string, dryRun: boolean): number {
|
|
49
|
-
const zip = new AdmZip(zipPath);
|
|
50
|
-
const entries = zip.getEntries();
|
|
51
|
-
|
|
52
|
-
if (entries.length === 0) {
|
|
53
|
-
console.log("Archive is empty — nothing to import.");
|
|
54
|
-
return 0;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (dryRun) {
|
|
58
|
-
console.log(`Would import ${entries.length} files → ${targetDir}\n`);
|
|
59
|
-
for (const e of entries) console.log(` ${e.entryName}`);
|
|
60
|
-
return entries.length;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
zip.extractAllTo(targetDir, true);
|
|
64
|
-
console.log(`Imported ${entries.length} files → ${targetDir}`);
|
|
65
|
-
console.log("\nRun 'bun run install:all' to re-create symlinks and hooks.");
|
|
66
|
-
return entries.length;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
async function run() {
|
|
70
|
-
const repoRoot = palHome();
|
|
71
|
-
const args = process.argv.slice(2);
|
|
72
|
-
const dryRun = args.includes("--dry-run");
|
|
73
|
-
const pathArg = args.find((a) => a !== "--dry-run");
|
|
74
|
-
|
|
75
|
-
let zipPath: string;
|
|
76
|
-
|
|
77
|
-
if (pathArg) {
|
|
78
|
-
zipPath = resolve(pathArg);
|
|
79
|
-
} else {
|
|
80
|
-
const latest = findLatestExport(repoRoot);
|
|
81
|
-
if (!latest) {
|
|
82
|
-
console.error(
|
|
83
|
-
"No export or backup files found. Provide a path: bun run tool:import <path-to-zip>"
|
|
84
|
-
);
|
|
85
|
-
process.exit(1);
|
|
86
|
-
}
|
|
87
|
-
console.log(`Found: ${latest}`);
|
|
88
|
-
const zip = new AdmZip(latest);
|
|
89
|
-
const entries = zip.getEntries();
|
|
90
|
-
console.log(
|
|
91
|
-
`Contains ${entries.length} files, created ${statSync(latest).mtime.toISOString().slice(0, 16).replace("T", " ")}`
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
95
|
-
const answer = await new Promise<string>((res) => {
|
|
96
|
-
rl.question("Import this file? [y/N] ", (a) => {
|
|
97
|
-
rl.close();
|
|
98
|
-
res(a);
|
|
99
|
-
});
|
|
100
|
-
});
|
|
101
|
-
if (answer.trim().toLowerCase() !== "y") {
|
|
102
|
-
console.log("Cancelled.");
|
|
103
|
-
process.exit(0);
|
|
104
|
-
}
|
|
105
|
-
zipPath = latest;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
importZip(zipPath, repoRoot, dryRun);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (import.meta.main) await run();
|