personal-ai 0.2.1 → 0.2.3
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/SKILL.md +132 -267
- package/dist/entry.mjs +270 -50
- package/dist/entry.mjs.map +1 -1
- package/dist/index.mjs +267 -47
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -1
package/dist/entry.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import fs from "node:fs/promises";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import os from "node:os";
|
|
7
7
|
import http from "node:http";
|
|
8
|
-
import { URL } from "node:url";
|
|
8
|
+
import { URL as URL$1 } from "node:url";
|
|
9
9
|
import { google } from "googleapis";
|
|
10
10
|
import open from "open";
|
|
11
11
|
import chalk from "chalk";
|
|
@@ -282,7 +282,7 @@ var GoogleOAuth = class {
|
|
|
282
282
|
return new Promise((resolve, reject) => {
|
|
283
283
|
const server = http.createServer((req, res) => {
|
|
284
284
|
if (req.url?.startsWith("/callback")) {
|
|
285
|
-
const url = new URL(req.url ?? "", `http://${req.headers.host}`);
|
|
285
|
+
const url = new URL$1(req.url ?? "", `http://${req.headers.host}`);
|
|
286
286
|
const code = url.searchParams.get("code");
|
|
287
287
|
const state = url.searchParams.get("state");
|
|
288
288
|
if (code && state === expectedState) {
|
|
@@ -1822,11 +1822,123 @@ function registerInitCommand(program) {
|
|
|
1822
1822
|
//#endregion
|
|
1823
1823
|
//#region src/scraper/index.ts
|
|
1824
1824
|
/**
|
|
1825
|
-
*
|
|
1826
|
-
*
|
|
1827
|
-
*
|
|
1825
|
+
* Web scraper: three-tier strategy for content extraction.
|
|
1826
|
+
*
|
|
1827
|
+
* 1. GitHub blob URLs → convert to raw.githubusercontent.com, fetch raw markdown (zero rendering)
|
|
1828
|
+
* 2. General URLs → Playwright headless browser + DOM preprocessing + Defuddle extraction
|
|
1829
|
+
* 3. Fallback → plain fetch + Defuddle (for when Playwright is unavailable)
|
|
1830
|
+
*
|
|
1831
|
+
* Reference: linkmind-master/src/scraper.ts
|
|
1828
1832
|
*/
|
|
1829
|
-
|
|
1833
|
+
const CHROME_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";
|
|
1834
|
+
/** Minimum markdown length to consider a scrape successful */
|
|
1835
|
+
const MIN_CONTENT_LENGTH = 100;
|
|
1836
|
+
/** Check if a URL points to a file on GitHub (blob view) */
|
|
1837
|
+
function isGithubBlobUrl(url) {
|
|
1838
|
+
try {
|
|
1839
|
+
const u = new URL(url);
|
|
1840
|
+
return u.hostname === "github.com" && /\/blob\//.test(u.pathname);
|
|
1841
|
+
} catch {
|
|
1842
|
+
return false;
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
/** Convert GitHub blob URL to raw.githubusercontent.com URL */
|
|
1846
|
+
function toRawGithubUrl(url) {
|
|
1847
|
+
return url.replace("https://github.com/", "https://raw.githubusercontent.com/").replace("/blob/", "/");
|
|
1848
|
+
}
|
|
1849
|
+
/** Fast path: fetch raw content directly from GitHub (returns markdown/text as-is) */
|
|
1850
|
+
async function scrapeGithubRaw(url, timeout) {
|
|
1851
|
+
const rawUrl = toRawGithubUrl(url);
|
|
1852
|
+
const controller = new AbortController();
|
|
1853
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
1854
|
+
try {
|
|
1855
|
+
const resp = await fetch(rawUrl, {
|
|
1856
|
+
signal: controller.signal,
|
|
1857
|
+
headers: { "User-Agent": CHROME_UA }
|
|
1858
|
+
});
|
|
1859
|
+
if (!resp.ok) throw new Error(`GitHub raw fetch failed: HTTP ${resp.status}`);
|
|
1860
|
+
const markdown = await resp.text();
|
|
1861
|
+
const titleMatch = markdown.match(/^#\s+(.+)$/m);
|
|
1862
|
+
const pathParts = new URL(url).pathname.split("/");
|
|
1863
|
+
const filename = pathParts[pathParts.length - 1] ?? "Untitled";
|
|
1864
|
+
return {
|
|
1865
|
+
url,
|
|
1866
|
+
title: titleMatch?.[1]?.trim() ?? filename,
|
|
1867
|
+
markdown
|
|
1868
|
+
};
|
|
1869
|
+
} finally {
|
|
1870
|
+
clearTimeout(timer);
|
|
1871
|
+
}
|
|
1872
|
+
}
|
|
1873
|
+
/**
|
|
1874
|
+
* DOM preprocessing script executed inside the browser.
|
|
1875
|
+
* Removes navigation, ads, cookie banners, and other non-content elements
|
|
1876
|
+
* before Defuddle extraction. (Borrowed from linkmind)
|
|
1877
|
+
*/
|
|
1878
|
+
const DOM_PREPROCESS_SCRIPT = `(() => {
|
|
1879
|
+
// Remove script, style, stylesheet links
|
|
1880
|
+
document.querySelectorAll("script, style, link[rel='stylesheet']").forEach(el => el.remove());
|
|
1881
|
+
// Remove navigation elements
|
|
1882
|
+
document.querySelectorAll("nav, footer, aside").forEach(el => el.remove());
|
|
1883
|
+
// Remove headers not inside article/main
|
|
1884
|
+
document.querySelectorAll("header").forEach(el => {
|
|
1885
|
+
if (!el.closest("article") && !el.closest("main")) el.remove();
|
|
1886
|
+
});
|
|
1887
|
+
// Remove ARIA landmark roles
|
|
1888
|
+
document.querySelectorAll('[role="navigation"], [role="banner"], [role="contentinfo"], [role="complementary"], [role="search"]').forEach(el => el.remove());
|
|
1889
|
+
// Remove cookie/share/comment noise
|
|
1890
|
+
document.querySelectorAll('[class*="cookie-banner"], [id*="cookie-banner"], [class*="cookie-consent"], [class*="share-buttons"], [class*="social-share"], [class*="comment-section"], [id*="comments"]').forEach(el => el.remove());
|
|
1891
|
+
// Remove hidden elements
|
|
1892
|
+
document.querySelectorAll('[hidden], [aria-hidden="true"]').forEach(el => el.remove());
|
|
1893
|
+
|
|
1894
|
+
return {
|
|
1895
|
+
title: document.title,
|
|
1896
|
+
html: document.documentElement.outerHTML,
|
|
1897
|
+
};
|
|
1898
|
+
})()`;
|
|
1899
|
+
/** Scrape with Playwright headless browser + Defuddle */
|
|
1900
|
+
async function scrapeWithPlaywright(url, timeout) {
|
|
1901
|
+
const pw = await import("playwright");
|
|
1902
|
+
const { Defuddle } = await import("defuddle/node");
|
|
1903
|
+
const browser = await pw.chromium.launch({
|
|
1904
|
+
headless: true,
|
|
1905
|
+
args: ["--disable-blink-features=AutomationControlled"]
|
|
1906
|
+
});
|
|
1907
|
+
try {
|
|
1908
|
+
const page = await (await browser.newContext({
|
|
1909
|
+
viewport: {
|
|
1910
|
+
width: 1280,
|
|
1911
|
+
height: 900
|
|
1912
|
+
},
|
|
1913
|
+
userAgent: CHROME_UA,
|
|
1914
|
+
locale: "en-US"
|
|
1915
|
+
})).newPage();
|
|
1916
|
+
await page.goto(url, {
|
|
1917
|
+
waitUntil: "domcontentloaded",
|
|
1918
|
+
timeout
|
|
1919
|
+
});
|
|
1920
|
+
await page.waitForTimeout(2e3);
|
|
1921
|
+
const { title: pageTitle, html } = await page.evaluate(DOM_PREPROCESS_SCRIPT);
|
|
1922
|
+
await browser.close();
|
|
1923
|
+
const origLog = globalThis.console.log;
|
|
1924
|
+
globalThis.console.log = (msg, ...args) => {
|
|
1925
|
+
if (typeof msg === "string" && msg.includes("Initial parse returned very little content")) return;
|
|
1926
|
+
origLog(msg, ...args);
|
|
1927
|
+
};
|
|
1928
|
+
const result = await Defuddle(html, url);
|
|
1929
|
+
globalThis.console.log = origLog;
|
|
1930
|
+
return {
|
|
1931
|
+
url,
|
|
1932
|
+
title: result.title || pageTitle || "Untitled",
|
|
1933
|
+
markdown: result.content ? htmlToSimpleMarkdown(result.content) : ""
|
|
1934
|
+
};
|
|
1935
|
+
} catch (err) {
|
|
1936
|
+
await browser.close().catch(() => {});
|
|
1937
|
+
throw err;
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
/** Lightweight scrape: plain HTTP fetch + Defuddle (no JS rendering) */
|
|
1941
|
+
async function scrapeWithFetch(url, timeout) {
|
|
1830
1942
|
const controller = new AbortController();
|
|
1831
1943
|
const timer = setTimeout(() => controller.abort(), timeout);
|
|
1832
1944
|
let html;
|
|
@@ -1834,7 +1946,7 @@ async function scrapeUrl(url, timeout = 3e4) {
|
|
|
1834
1946
|
const response = await fetch(url, {
|
|
1835
1947
|
signal: controller.signal,
|
|
1836
1948
|
headers: {
|
|
1837
|
-
"User-Agent":
|
|
1949
|
+
"User-Agent": CHROME_UA,
|
|
1838
1950
|
Accept: "text/html,application/xhtml+xml"
|
|
1839
1951
|
}
|
|
1840
1952
|
});
|
|
@@ -1844,16 +1956,13 @@ async function scrapeUrl(url, timeout = 3e4) {
|
|
|
1844
1956
|
clearTimeout(timer);
|
|
1845
1957
|
}
|
|
1846
1958
|
try {
|
|
1847
|
-
const
|
|
1848
|
-
const
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
markdown: result.content ? htmlToSimpleMarkdown(result.content) : extractTextFromHtml(html)
|
|
1855
|
-
};
|
|
1856
|
-
}
|
|
1959
|
+
const { Defuddle } = await import("defuddle/node");
|
|
1960
|
+
const result = await Defuddle(html, url);
|
|
1961
|
+
return {
|
|
1962
|
+
url,
|
|
1963
|
+
title: result.title || extractTitleFromHtml(html),
|
|
1964
|
+
markdown: result.content ? htmlToSimpleMarkdown(result.content) : extractTextFromHtml(html)
|
|
1965
|
+
};
|
|
1857
1966
|
} catch {
|
|
1858
1967
|
warn("defuddle not available, using basic HTML extraction");
|
|
1859
1968
|
}
|
|
@@ -1863,17 +1972,77 @@ async function scrapeUrl(url, timeout = 3e4) {
|
|
|
1863
1972
|
markdown: extractTextFromHtml(html)
|
|
1864
1973
|
};
|
|
1865
1974
|
}
|
|
1975
|
+
/**
|
|
1976
|
+
* Scrape a URL and return title + markdown content.
|
|
1977
|
+
*
|
|
1978
|
+
* Strategy:
|
|
1979
|
+
* 1. GitHub blob URL → raw.githubusercontent.com (instant, perfect fidelity)
|
|
1980
|
+
* 2. Playwright + Defuddle (handles JS-rendered pages)
|
|
1981
|
+
* 3. Fetch + Defuddle fallback (static pages, or when Playwright missing)
|
|
1982
|
+
*/
|
|
1983
|
+
async function scrapeUrl(url, timeout = 3e4) {
|
|
1984
|
+
if (isGithubBlobUrl(url)) {
|
|
1985
|
+
info("GitHub blob detected — fetching raw content directly");
|
|
1986
|
+
return scrapeGithubRaw(url, timeout);
|
|
1987
|
+
}
|
|
1988
|
+
try {
|
|
1989
|
+
const result = await scrapeWithPlaywright(url, timeout);
|
|
1990
|
+
if (result.markdown.length < MIN_CONTENT_LENGTH) {
|
|
1991
|
+
warn(`Playwright extracted only ${result.markdown.length} chars — trying fetch fallback`);
|
|
1992
|
+
const fallback = await scrapeWithFetch(url, timeout);
|
|
1993
|
+
return fallback.markdown.length > result.markdown.length ? fallback : result;
|
|
1994
|
+
}
|
|
1995
|
+
return result;
|
|
1996
|
+
} catch (err) {
|
|
1997
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1998
|
+
warn(`Playwright scrape failed (${msg}) — falling back to fetch`);
|
|
1999
|
+
}
|
|
2000
|
+
return scrapeWithFetch(url, timeout);
|
|
2001
|
+
}
|
|
1866
2002
|
/** Extract <title> from HTML */
|
|
1867
2003
|
function extractTitleFromHtml(html) {
|
|
1868
2004
|
return html.match(/<title[^>]*>([^<]+)<\/title>/i)?.[1]?.trim() ?? "Untitled";
|
|
1869
2005
|
}
|
|
1870
|
-
/** Basic HTML to text extraction (fallback) */
|
|
2006
|
+
/** Basic HTML to text extraction (last-resort fallback) */
|
|
1871
2007
|
function extractTextFromHtml(html) {
|
|
1872
2008
|
return html.replace(/<script[\s\S]*?<\/script>/gi, "").replace(/<style[\s\S]*?<\/style>/gi, "").replace(/<[^>]+>/g, " ").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, "\"").replace(/'/g, "'").replace(/ /g, " ").replace(/\s+/g, " ").trim().slice(0, 1e4);
|
|
1873
2009
|
}
|
|
1874
|
-
/** Convert
|
|
2010
|
+
/** Convert HTML fragment to simple Markdown (from linkmind, extended) */
|
|
1875
2011
|
function htmlToSimpleMarkdown(html) {
|
|
1876
|
-
|
|
2012
|
+
if (!html) return "";
|
|
2013
|
+
let md = html;
|
|
2014
|
+
md = md.replace(/<h1[^>]*>(.*?)<\/h1>/gi, "# $1\n\n");
|
|
2015
|
+
md = md.replace(/<h2[^>]*>(.*?)<\/h2>/gi, "## $1\n\n");
|
|
2016
|
+
md = md.replace(/<h3[^>]*>(.*?)<\/h3>/gi, "### $1\n\n");
|
|
2017
|
+
md = md.replace(/<h4[^>]*>(.*?)<\/h4>/gi, "#### $1\n\n");
|
|
2018
|
+
md = md.replace(/<h5[^>]*>(.*?)<\/h5>/gi, "##### $1\n\n");
|
|
2019
|
+
md = md.replace(/<h6[^>]*>(.*?)<\/h6>/gi, "###### $1\n\n");
|
|
2020
|
+
md = md.replace(/<p[^>]*>/gi, "\n\n");
|
|
2021
|
+
md = md.replace(/<\/p>/gi, "");
|
|
2022
|
+
md = md.replace(/<br\s*\/?>/gi, "\n");
|
|
2023
|
+
md = md.replace(/<(strong|b)[^>]*>(.*?)<\/(strong|b)>/gi, "**$2**");
|
|
2024
|
+
md = md.replace(/<(em|i)[^>]*>(.*?)<\/(em|i)>/gi, "*$2*");
|
|
2025
|
+
md = md.replace(/<a[^>]*href="([^"]*)"[^>]*>(.*?)<\/a>/gi, "[$2]($1)");
|
|
2026
|
+
md = md.replace(/<code[^>]*>(.*?)<\/code>/gi, "`$1`");
|
|
2027
|
+
md = md.replace(/<pre[^>]*>(.*?)<\/pre>/gis, "\n```\n$1\n```\n");
|
|
2028
|
+
md = md.replace(/<li[^>]*>/gi, "- ");
|
|
2029
|
+
md = md.replace(/<\/li>/gi, "\n");
|
|
2030
|
+
md = md.replace(/<\/?[uo]l[^>]*>/gi, "\n");
|
|
2031
|
+
md = md.replace(/<blockquote[^>]*>(.*?)<\/blockquote>/gis, (_, content) => {
|
|
2032
|
+
return content.split("\n").map((line) => `> ${line}`).join("\n");
|
|
2033
|
+
});
|
|
2034
|
+
md = md.replace(/<img[^>]*src="([^"]*)"[^>]*alt="([^"]*)"[^>]*\/?>/gi, "");
|
|
2035
|
+
md = md.replace(/<img[^>]*src="([^"]*)"[^>]*\/?>/gi, "");
|
|
2036
|
+
md = md.replace(/<[^>]+>/g, "");
|
|
2037
|
+
md = md.replace(/&/g, "&");
|
|
2038
|
+
md = md.replace(/</g, "<");
|
|
2039
|
+
md = md.replace(/>/g, ">");
|
|
2040
|
+
md = md.replace(/"/g, "\"");
|
|
2041
|
+
md = md.replace(/'/g, "'");
|
|
2042
|
+
md = md.replace(/ /g, " ");
|
|
2043
|
+
md = md.replace(/\n{3,}/g, "\n\n");
|
|
2044
|
+
md = md.trim();
|
|
2045
|
+
return md;
|
|
1877
2046
|
}
|
|
1878
2047
|
|
|
1879
2048
|
//#endregion
|
|
@@ -2060,44 +2229,95 @@ const VALID_TYPES = new Set([
|
|
|
2060
2229
|
"entity",
|
|
2061
2230
|
"event"
|
|
2062
2231
|
]);
|
|
2063
|
-
/**
|
|
2232
|
+
/**
|
|
2233
|
+
* Budget for the total text sent to LLM.
|
|
2234
|
+
* ~10K chars ≈ ~2.5K tokens (English) / ~5K tokens (CJK).
|
|
2235
|
+
* PINData extraction only needs gist, not full content.
|
|
2236
|
+
*/
|
|
2237
|
+
const MAX_INPUT_CHARS = 1e4;
|
|
2238
|
+
/** Head portion gets the lion's share — title, intro, overview */
|
|
2239
|
+
const HEAD_CHARS = 4e3;
|
|
2240
|
+
/** Tail portion — conclusions, takeaways, resource lists */
|
|
2241
|
+
const TAIL_CHARS = 3e3;
|
|
2242
|
+
/** Remaining budget goes to random middle samples */
|
|
2243
|
+
const MIDDLE_BUDGET = MAX_INPUT_CHARS - HEAD_CHARS - TAIL_CHARS;
|
|
2244
|
+
/** Number of random middle samples to pick */
|
|
2245
|
+
const MIDDLE_SAMPLES = 2;
|
|
2246
|
+
/**
|
|
2247
|
+
* For content that fits the budget, return as-is.
|
|
2248
|
+
* For long content, sample: head + tail + random middle paragraphs.
|
|
2249
|
+
*
|
|
2250
|
+
* Rationale: PINData extraction asks "what does this mean to the USER",
|
|
2251
|
+
* not "summarize the entire document". The head (title/intro) and tail
|
|
2252
|
+
* (conclusions/resources) carry 80%+ of personal signal. Middle sections
|
|
2253
|
+
* of long articles (paper tables, code listings, repetitive data) are
|
|
2254
|
+
* mostly noise for personal knowledge extraction.
|
|
2255
|
+
*/
|
|
2256
|
+
function prepareContent(text) {
|
|
2257
|
+
if (text.length <= MAX_INPUT_CHARS) return text;
|
|
2258
|
+
const head = text.slice(0, HEAD_CHARS);
|
|
2259
|
+
const tail = text.slice(-TAIL_CHARS);
|
|
2260
|
+
const middleStart = HEAD_CHARS;
|
|
2261
|
+
const middleEnd = text.length - TAIL_CHARS;
|
|
2262
|
+
const paragraphs = text.slice(middleStart, middleEnd).split(/\n{2,}/).map((p) => p.trim()).filter((p) => p.length > 100);
|
|
2263
|
+
const samples = [];
|
|
2264
|
+
let sampledChars = 0;
|
|
2265
|
+
const perSampleBudget = Math.floor(MIDDLE_BUDGET / MIDDLE_SAMPLES);
|
|
2266
|
+
if (paragraphs.length > 0) {
|
|
2267
|
+
const step = Math.max(1, Math.floor(paragraphs.length / MIDDLE_SAMPLES));
|
|
2268
|
+
for (let i = 0; i < MIDDLE_SAMPLES && i * step < paragraphs.length; i++) {
|
|
2269
|
+
const truncated = paragraphs[i * step].slice(0, perSampleBudget);
|
|
2270
|
+
samples.push(truncated);
|
|
2271
|
+
sampledChars += truncated.length;
|
|
2272
|
+
}
|
|
2273
|
+
}
|
|
2274
|
+
const assembled = `${head}${samples.length > 0 ? `\n\n[... middle section sampled — ${(middleEnd - middleStart).toLocaleString()} chars total ...]\n\n${samples.join("\n\n---\n\n")}` : ""}\n\n[... end section ...]\n\n${tail}`;
|
|
2275
|
+
info(`Content ${text.length.toLocaleString()} chars → sampled to ${assembled.length.toLocaleString()} chars (head:${HEAD_CHARS} + ${samples.length} mid-samples:${sampledChars} + tail:${TAIL_CHARS})`);
|
|
2276
|
+
return assembled;
|
|
2277
|
+
}
|
|
2278
|
+
/** Parse one LLM JSON response into validated PINDataEntry[] */
|
|
2279
|
+
function parseExtractResponse(response) {
|
|
2280
|
+
const cleaned = response.replace(/```json?\n?/g, "").replace(/```/g, "").trim();
|
|
2281
|
+
const raw = JSON.parse(cleaned);
|
|
2282
|
+
let rawEntries;
|
|
2283
|
+
if (Array.isArray(raw)) rawEntries = raw;
|
|
2284
|
+
else if (raw && typeof raw === "object" && "entries" in raw && Array.isArray(raw.entries)) rawEntries = raw.entries;
|
|
2285
|
+
else return [];
|
|
2286
|
+
const entries = [];
|
|
2287
|
+
for (const item of rawEntries) {
|
|
2288
|
+
if (!item || typeof item !== "object") continue;
|
|
2289
|
+
const obj = item;
|
|
2290
|
+
const type = obj.type;
|
|
2291
|
+
const entryContent = obj.content;
|
|
2292
|
+
const topic = obj.topic;
|
|
2293
|
+
if (!type || !entryContent || !topic) continue;
|
|
2294
|
+
if (!VALID_TYPES.has(type)) continue;
|
|
2295
|
+
entries.push({
|
|
2296
|
+
type,
|
|
2297
|
+
content: entryContent.trim(),
|
|
2298
|
+
topic: topic.trim(),
|
|
2299
|
+
tags: Array.isArray(obj.tags) ? obj.tags.filter((t) => typeof t === "string") : void 0
|
|
2300
|
+
});
|
|
2301
|
+
}
|
|
2302
|
+
return entries;
|
|
2303
|
+
}
|
|
2304
|
+
/**
|
|
2305
|
+
* Extract PINData entries from raw/journal content via a single LLM call.
|
|
2306
|
+
* Long content is sampled (head + tail + middle samples) to fit the budget.
|
|
2307
|
+
*/
|
|
2064
2308
|
async function extractPinData(content, source) {
|
|
2065
2309
|
const system = extractSystemPrompt();
|
|
2066
|
-
const
|
|
2310
|
+
const prompt = extractUserPrompt(prepareContent(content), source);
|
|
2067
2311
|
try {
|
|
2068
|
-
const
|
|
2069
|
-
const raw = JSON.parse(cleaned);
|
|
2070
|
-
let rawEntries;
|
|
2071
|
-
if (Array.isArray(raw)) rawEntries = raw;
|
|
2072
|
-
else if (raw && typeof raw === "object" && "entries" in raw && Array.isArray(raw.entries)) rawEntries = raw.entries;
|
|
2073
|
-
else return {
|
|
2074
|
-
entries: [],
|
|
2075
|
-
summary: ""
|
|
2076
|
-
};
|
|
2077
|
-
const entries = [];
|
|
2078
|
-
for (const item of rawEntries) {
|
|
2079
|
-
if (!item || typeof item !== "object") continue;
|
|
2080
|
-
const obj = item;
|
|
2081
|
-
const type = obj.type;
|
|
2082
|
-
const content = obj.content;
|
|
2083
|
-
const topic = obj.topic;
|
|
2084
|
-
if (!type || !content || !topic) continue;
|
|
2085
|
-
if (!VALID_TYPES.has(type)) continue;
|
|
2086
|
-
entries.push({
|
|
2087
|
-
type,
|
|
2088
|
-
content: content.trim(),
|
|
2089
|
-
topic: topic.trim(),
|
|
2090
|
-
tags: Array.isArray(obj.tags) ? obj.tags.filter((t) => typeof t === "string") : void 0
|
|
2091
|
-
});
|
|
2092
|
-
}
|
|
2312
|
+
const entries = parseExtractResponse(await llmCall(prompt, system));
|
|
2093
2313
|
return {
|
|
2094
2314
|
entries,
|
|
2095
2315
|
summary: entries.length > 0 ? entries.slice(0, 3).map((e) => e.content).join("; ") : "no extractable signal"
|
|
2096
2316
|
};
|
|
2097
|
-
} catch {
|
|
2317
|
+
} catch (err) {
|
|
2098
2318
|
return {
|
|
2099
2319
|
entries: [],
|
|
2100
|
-
summary: `Failed to parse extract response: ${
|
|
2320
|
+
summary: `Failed to parse extract response: ${(err instanceof Error ? err.message : String(err)).slice(0, 100)}`
|
|
2101
2321
|
};
|
|
2102
2322
|
}
|
|
2103
2323
|
}
|
|
@@ -4440,7 +4660,7 @@ function registerAllCommands(program) {
|
|
|
4440
4660
|
//#endregion
|
|
4441
4661
|
//#region src/cli/build-program.ts
|
|
4442
4662
|
function buildProgram() {
|
|
4443
|
-
const program = new Command().name("pai").description("Personal AI Identity Provider — local-first AI agent identity & memory system").version("0.2.
|
|
4663
|
+
const program = new Command().name("pai").description("Personal AI Identity Provider — local-first AI agent identity & memory system").version("0.2.3");
|
|
4444
4664
|
registerAllCommands(program);
|
|
4445
4665
|
return program;
|
|
4446
4666
|
}
|