@tekmidian/pai 0.2.1 → 0.3.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/ARCHITECTURE.md +148 -6
- package/FEATURE.md +11 -0
- package/README.md +79 -0
- package/dist/{auto-route-D7W6RE06.mjs → auto-route-JjW3f7pV.mjs} +4 -4
- package/dist/{auto-route-D7W6RE06.mjs.map → auto-route-JjW3f7pV.mjs.map} +1 -1
- package/dist/chunker-CbnBe0s0.mjs +191 -0
- package/dist/chunker-CbnBe0s0.mjs.map +1 -0
- package/dist/cli/index.mjs +835 -40
- package/dist/cli/index.mjs.map +1 -1
- package/dist/{config-DBh1bYM2.mjs → config-DELNqq3Z.mjs} +4 -2
- package/dist/{config-DBh1bYM2.mjs.map → config-DELNqq3Z.mjs.map} +1 -1
- package/dist/daemon/index.mjs +9 -9
- package/dist/{daemon-v5O897D4.mjs → daemon-CeTX4NpF.mjs} +94 -13
- package/dist/daemon-CeTX4NpF.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +3 -3
- package/dist/db-Dp8VXIMR.mjs +212 -0
- package/dist/db-Dp8VXIMR.mjs.map +1 -0
- package/dist/{detect-BHqYcjJ1.mjs → detect-D7gPV3fQ.mjs} +1 -1
- package/dist/{detect-BHqYcjJ1.mjs.map → detect-D7gPV3fQ.mjs.map} +1 -1
- package/dist/{detector-DKA83aTZ.mjs → detector-cYYhK2Mi.mjs} +2 -2
- package/dist/{detector-DKA83aTZ.mjs.map → detector-cYYhK2Mi.mjs.map} +1 -1
- package/dist/{embeddings-mfqv-jFu.mjs → embeddings-DGRAPAYb.mjs} +2 -2
- package/dist/{embeddings-mfqv-jFu.mjs.map → embeddings-DGRAPAYb.mjs.map} +1 -1
- package/dist/{factory-BDAiKtYR.mjs → factory-DZLvRf4m.mjs} +4 -4
- package/dist/{factory-BDAiKtYR.mjs.map → factory-DZLvRf4m.mjs.map} +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +9 -7
- package/dist/{indexer-B20bPHL-.mjs → indexer-CKQcgKsz.mjs} +4 -190
- package/dist/indexer-CKQcgKsz.mjs.map +1 -0
- package/dist/{indexer-backend-BXaocO5r.mjs → indexer-backend-BHztlJJg.mjs} +4 -3
- package/dist/{indexer-backend-BXaocO5r.mjs.map → indexer-backend-BHztlJJg.mjs.map} +1 -1
- package/dist/{ipc-client-DPy7s3iu.mjs → ipc-client-CLt2fNlC.mjs} +1 -1
- package/dist/ipc-client-CLt2fNlC.mjs.map +1 -0
- package/dist/mcp/index.mjs +118 -5
- package/dist/mcp/index.mjs.map +1 -1
- package/dist/{migrate-Bwj7qPaE.mjs → migrate-jokLenje.mjs} +8 -1
- package/dist/migrate-jokLenje.mjs.map +1 -0
- package/dist/{pai-marker-DX_mFLum.mjs → pai-marker-CXQPX2P6.mjs} +1 -1
- package/dist/{pai-marker-DX_mFLum.mjs.map → pai-marker-CXQPX2P6.mjs.map} +1 -1
- package/dist/{postgres-Ccvpc6fC.mjs → postgres-CRBe30Ag.mjs} +1 -1
- package/dist/{postgres-Ccvpc6fC.mjs.map → postgres-CRBe30Ag.mjs.map} +1 -1
- package/dist/{schemas-DjdwzIQ8.mjs → schemas-BY3Pjvje.mjs} +1 -1
- package/dist/{schemas-DjdwzIQ8.mjs.map → schemas-BY3Pjvje.mjs.map} +1 -1
- package/dist/{search-PjftDxxs.mjs → search-GK0ibTJy.mjs} +2 -2
- package/dist/{search-PjftDxxs.mjs.map → search-GK0ibTJy.mjs.map} +1 -1
- package/dist/{sqlite-CHUrNtbI.mjs → sqlite-RyR8Up1v.mjs} +3 -3
- package/dist/{sqlite-CHUrNtbI.mjs.map → sqlite-RyR8Up1v.mjs.map} +1 -1
- package/dist/{tools-CLK4080-.mjs → tools-CUg0Lyg-.mjs} +175 -11
- package/dist/{tools-CLK4080-.mjs.map → tools-CUg0Lyg-.mjs.map} +1 -1
- package/dist/{utils-DEWdIFQ0.mjs → utils-QSfKagcj.mjs} +62 -2
- package/dist/utils-QSfKagcj.mjs.map +1 -0
- package/dist/vault-indexer-Bo2aPSzP.mjs +499 -0
- package/dist/vault-indexer-Bo2aPSzP.mjs.map +1 -0
- package/dist/zettelkasten-Co-w0XSZ.mjs +901 -0
- package/dist/zettelkasten-Co-w0XSZ.mjs.map +1 -0
- package/package.json +2 -1
- package/src/hooks/README.md +99 -0
- package/src/hooks/hooks.md +13 -0
- package/src/hooks/pre-compact.sh +95 -0
- package/src/hooks/session-stop.sh +93 -0
- package/statusline-command.sh +9 -4
- package/templates/README.md +7 -0
- package/templates/agent-prefs.example.md +7 -0
- package/templates/claude-md.template.md +7 -0
- package/templates/pai-project.template.md +4 -6
- package/templates/pai-skill.template.md +295 -0
- package/templates/templates.md +20 -0
- package/dist/daemon-v5O897D4.mjs.map +0 -1
- package/dist/db-BcDxXVBu.mjs +0 -110
- package/dist/db-BcDxXVBu.mjs.map +0 -1
- package/dist/indexer-B20bPHL-.mjs.map +0 -1
- package/dist/ipc-client-DPy7s3iu.mjs.map +0 -1
- package/dist/migrate-Bwj7qPaE.mjs.map +0 -1
- package/dist/utils-DEWdIFQ0.mjs.map +0 -1
package/dist/cli/index.mjs
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { n as openRegistry } from "../db-4lSqLFb8.mjs";
|
|
3
|
-
import { a as
|
|
4
|
-
import { n as
|
|
5
|
-
import { n as
|
|
6
|
-
import {
|
|
7
|
-
import "../
|
|
8
|
-
import
|
|
9
|
-
import { n as
|
|
10
|
-
import {
|
|
11
|
-
import { t as PaiClient } from "../ipc-client-
|
|
12
|
-
import { a as expandHome, n as CONFIG_FILE$2, o as loadConfig$1, t as CONFIG_DIR } from "../config-
|
|
13
|
-
import { t as createStorageBackend } from "../factory-
|
|
14
|
-
import { appendFileSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { _ as warn, a as fmtDate, c as ok, d as scaffoldProjectDirs, f as shortenPath, i as err, l as renderTable, m as slugify, n as dim, o as header, p as slugFromPath, r as encodeDir, s as now, t as bold, u as resolvePath } from "../utils-QSfKagcj.mjs";
|
|
4
|
+
import { a as slugify$1, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "../migrate-jokLenje.mjs";
|
|
5
|
+
import { n as ensurePaiMarker, t as discoverPaiMarkers } from "../pai-marker-CXQPX2P6.mjs";
|
|
6
|
+
import { n as openFederation } from "../db-Dp8VXIMR.mjs";
|
|
7
|
+
import { a as indexProject, n as embedChunks, r as indexAll } from "../indexer-CKQcgKsz.mjs";
|
|
8
|
+
import "../embeddings-DGRAPAYb.mjs";
|
|
9
|
+
import { n as populateSlugs, r as searchMemory } from "../search-GK0ibTJy.mjs";
|
|
10
|
+
import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "../detect-D7gPV3fQ.mjs";
|
|
11
|
+
import { t as PaiClient } from "../ipc-client-CLt2fNlC.mjs";
|
|
12
|
+
import { a as expandHome, n as CONFIG_FILE$2, o as loadConfig$1, t as CONFIG_DIR } from "../config-DELNqq3Z.mjs";
|
|
13
|
+
import { t as createStorageBackend } from "../factory-DZLvRf4m.mjs";
|
|
14
|
+
import { appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
15
15
|
import { homedir, tmpdir } from "node:os";
|
|
16
16
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
17
|
+
import chalk from "chalk";
|
|
17
18
|
import { Command } from "commander";
|
|
18
19
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import chalk from "chalk";
|
|
20
20
|
import { execSync, spawnSync } from "node:child_process";
|
|
21
21
|
import { createInterface } from "node:readline";
|
|
22
22
|
|
|
@@ -65,7 +65,7 @@ function cmdPromote(db, opts) {
|
|
|
65
65
|
process.exit(1);
|
|
66
66
|
}
|
|
67
67
|
const displayName = opts.name ?? deriveNameFromFilename(sessionPath);
|
|
68
|
-
const slug = slugify
|
|
68
|
+
const slug = slugify(displayName);
|
|
69
69
|
if (!slug) {
|
|
70
70
|
console.error(err(`Could not derive a valid slug from name: "${displayName}"`));
|
|
71
71
|
process.exit(1);
|
|
@@ -417,7 +417,7 @@ function suggestMovedPath(project) {
|
|
|
417
417
|
];
|
|
418
418
|
for (const c of candidates) if (existsSync(c)) return c;
|
|
419
419
|
}
|
|
420
|
-
function cmdHealth(db, opts) {
|
|
420
|
+
function cmdHealth$1(db, opts) {
|
|
421
421
|
const rows = db.prepare(`SELECT p.*,
|
|
422
422
|
(SELECT COUNT(*) FROM sessions s WHERE s.project_id = p.id) AS session_count
|
|
423
423
|
FROM projects p
|
|
@@ -729,7 +729,7 @@ function registerProjectCommands(projectCmd, getDb) {
|
|
|
729
729
|
cmdDetect(getDb(), pathArg, opts);
|
|
730
730
|
});
|
|
731
731
|
projectCmd.command("health").description("Audit all registered projects: check which paths still exist, find moved/dead projects").option("--fix", "Auto-remediate where possible (update moved paths, archive dead zero-session projects)").option("--json", "Output raw JSON report").option("--status <category>", "Filter output to: active | stale | dead").action((opts) => {
|
|
732
|
-
cmdHealth(getDb(), opts);
|
|
732
|
+
cmdHealth$1(getDb(), opts);
|
|
733
733
|
});
|
|
734
734
|
projectCmd.command("consolidate <identifier>").description("Consolidate scattered ~/.claude/projects/.../Notes/ directories for a project into its canonical Notes/ location").option("--yes", "Perform consolidation without confirmation prompt").option("--dry-run", "Preview what would be moved without making changes").action((identifier, opts) => {
|
|
735
735
|
cmdConsolidate(getDb(), identifier, opts);
|
|
@@ -1723,11 +1723,96 @@ function cmdHandover(db, projectSlug, numberOrLatest) {
|
|
|
1723
1723
|
}
|
|
1724
1724
|
process.exit(0);
|
|
1725
1725
|
}
|
|
1726
|
+
function cmdActive(db, opts) {
|
|
1727
|
+
const minutes = parseInt(opts.minutes ?? "60", 10);
|
|
1728
|
+
const cutoff = Date.now() - minutes * 60 * 1e3;
|
|
1729
|
+
const claudeProjectsDir = join(homedir(), ".claude", "projects");
|
|
1730
|
+
if (!existsSync(claudeProjectsDir)) {
|
|
1731
|
+
console.log(err("Claude projects directory not found."));
|
|
1732
|
+
return;
|
|
1733
|
+
}
|
|
1734
|
+
const active = [];
|
|
1735
|
+
const entries = readdirSync(claudeProjectsDir);
|
|
1736
|
+
for (const entry of entries) {
|
|
1737
|
+
const projectDir = join(claudeProjectsDir, entry);
|
|
1738
|
+
try {
|
|
1739
|
+
if (!statSync(projectDir).isDirectory()) continue;
|
|
1740
|
+
} catch {
|
|
1741
|
+
continue;
|
|
1742
|
+
}
|
|
1743
|
+
let latestJsonl = null;
|
|
1744
|
+
let latestMtime = 0;
|
|
1745
|
+
try {
|
|
1746
|
+
for (const file of readdirSync(projectDir)) {
|
|
1747
|
+
if (!file.endsWith(".jsonl")) continue;
|
|
1748
|
+
const filePath = join(projectDir, file);
|
|
1749
|
+
try {
|
|
1750
|
+
const mtime = statSync(filePath).mtimeMs;
|
|
1751
|
+
if (mtime > latestMtime) {
|
|
1752
|
+
latestMtime = mtime;
|
|
1753
|
+
latestJsonl = filePath;
|
|
1754
|
+
}
|
|
1755
|
+
} catch {
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
} catch {
|
|
1760
|
+
continue;
|
|
1761
|
+
}
|
|
1762
|
+
if (!latestJsonl || latestMtime < cutoff) continue;
|
|
1763
|
+
const project = db.prepare("SELECT slug, display_name, root_path FROM projects WHERE encoded_dir = ?").get(entry);
|
|
1764
|
+
active.push({
|
|
1765
|
+
slug: project?.slug ?? entry,
|
|
1766
|
+
displayName: project?.display_name ?? project?.slug ?? entry,
|
|
1767
|
+
rootPath: project?.root_path ?? "",
|
|
1768
|
+
encodedDir: entry,
|
|
1769
|
+
lastModified: new Date(latestMtime),
|
|
1770
|
+
jsonlFile: latestJsonl
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
active.sort((a, b) => b.lastModified.getTime() - a.lastModified.getTime());
|
|
1774
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1775
|
+
const deduped = active.filter((a) => {
|
|
1776
|
+
const key = a.slug.replace(/-\d+$/, "");
|
|
1777
|
+
if (seen.has(key)) return false;
|
|
1778
|
+
seen.add(key);
|
|
1779
|
+
return true;
|
|
1780
|
+
});
|
|
1781
|
+
if (opts.json) {
|
|
1782
|
+
console.log(JSON.stringify(deduped.map((a) => ({
|
|
1783
|
+
slug: a.slug,
|
|
1784
|
+
display_name: a.displayName,
|
|
1785
|
+
root_path: a.rootPath,
|
|
1786
|
+
last_modified: a.lastModified.toISOString()
|
|
1787
|
+
})), null, 2));
|
|
1788
|
+
return;
|
|
1789
|
+
}
|
|
1790
|
+
if (deduped.length === 0) {
|
|
1791
|
+
console.log(dim(`No active sessions in the last ${minutes} minutes.`));
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
console.log(header(`Currently Active Sessions`) + dim(` (modified in last ${minutes}min)`));
|
|
1795
|
+
console.log();
|
|
1796
|
+
const rows = deduped.map((a) => {
|
|
1797
|
+
const time = a.lastModified.toTimeString().slice(0, 5);
|
|
1798
|
+
const dirName = a.rootPath ? a.rootPath.replace(homedir(), "~").split("/").pop() ?? a.slug : a.slug;
|
|
1799
|
+
return [
|
|
1800
|
+
chalk.cyan(dirName),
|
|
1801
|
+
dim(a.slug),
|
|
1802
|
+
chalk.green(time)
|
|
1803
|
+
];
|
|
1804
|
+
});
|
|
1805
|
+
console.log(renderTable([
|
|
1806
|
+
"Directory",
|
|
1807
|
+
"Project",
|
|
1808
|
+
"Last Active"
|
|
1809
|
+
], rows));
|
|
1810
|
+
}
|
|
1726
1811
|
async function cmdAutoRoute(opts) {
|
|
1727
|
-
const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-
|
|
1812
|
+
const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-JjW3f7pV.mjs");
|
|
1728
1813
|
const { openRegistry } = await import("../db-4lSqLFb8.mjs").then((n) => n.t);
|
|
1729
|
-
const { createStorageBackend } = await import("../factory-
|
|
1730
|
-
const { loadConfig } = await import("../config-
|
|
1814
|
+
const { createStorageBackend } = await import("../factory-DZLvRf4m.mjs").then((n) => n.n);
|
|
1815
|
+
const { loadConfig } = await import("../config-DELNqq3Z.mjs").then((n) => n.r);
|
|
1731
1816
|
const config = loadConfig();
|
|
1732
1817
|
const registryDb = openRegistry();
|
|
1733
1818
|
const federation = await createStorageBackend(config);
|
|
@@ -1784,6 +1869,9 @@ function registerSessionCommands(sessionCmd, getDb) {
|
|
|
1784
1869
|
sessionCmd.command("checkpoint <message>").description("Append a timestamped checkpoint to the active session note.\nDesigned for hooks (PostToolUse, UserPromptSubmit) — fast and silent.\nRate-limited: skips silently if last checkpoint was < --min-gap seconds ago.").option("--min-gap <seconds>", "Minimum seconds between checkpoints (default: 300 = 5 minutes)", "300").action((message, opts) => {
|
|
1785
1870
|
cmdCheckpoint(message, opts);
|
|
1786
1871
|
});
|
|
1872
|
+
sessionCmd.command("active").description("Show currently active Claude Code sessions.\nDetects live sessions by checking which JSONL transcript files\nwere recently modified in ~/.claude/projects/.").option("--minutes <n>", "Consider sessions active if modified within N minutes (default: 60)", "60").option("--json", "Output raw JSON instead of formatted display").action((opts) => {
|
|
1873
|
+
cmdActive(getDb(), opts);
|
|
1874
|
+
});
|
|
1787
1875
|
sessionCmd.command("auto-route").description("Auto-detect which project this session belongs to.\nTries: (1) path match in registry, (2) Notes/PAI.md marker walk, (3) topic detection.\nDesigned for use in CLAUDE.md session-start hooks.").option("--cwd <path>", "Working directory to detect from (default: process.cwd())").option("--context <text>", "Conversation context for topic-based fallback routing").option("--json", "Output raw JSON instead of formatted display").action(async (opts) => {
|
|
1788
1876
|
await cmdAutoRoute(opts);
|
|
1789
1877
|
});
|
|
@@ -2252,8 +2340,8 @@ async function displayDryRun(plans) {
|
|
|
2252
2340
|
async function countVectorDbPaths(oldPaths) {
|
|
2253
2341
|
if (oldPaths.length === 0) return 0;
|
|
2254
2342
|
try {
|
|
2255
|
-
const { loadConfig } = await import("../config-
|
|
2256
|
-
const { PostgresBackend } = await import("../postgres-
|
|
2343
|
+
const { loadConfig } = await import("../config-DELNqq3Z.mjs").then((n) => n.r);
|
|
2344
|
+
const { PostgresBackend } = await import("../postgres-CRBe30Ag.mjs");
|
|
2257
2345
|
const config = loadConfig();
|
|
2258
2346
|
if (config.storageBackend !== "postgres") return 0;
|
|
2259
2347
|
const pgBackend = new PostgresBackend(config.postgres ?? {});
|
|
@@ -2279,8 +2367,8 @@ async function countVectorDbPaths(oldPaths) {
|
|
|
2279
2367
|
async function updateVectorDbPaths(moves) {
|
|
2280
2368
|
if (moves.length === 0) return 0;
|
|
2281
2369
|
try {
|
|
2282
|
-
const { loadConfig } = await import("../config-
|
|
2283
|
-
const { PostgresBackend } = await import("../postgres-
|
|
2370
|
+
const { loadConfig } = await import("../config-DELNqq3Z.mjs").then((n) => n.r);
|
|
2371
|
+
const { PostgresBackend } = await import("../postgres-CRBe30Ag.mjs");
|
|
2284
2372
|
const config = loadConfig();
|
|
2285
2373
|
if (config.storageBackend !== "postgres") return 0;
|
|
2286
2374
|
const pgBackend = new PostgresBackend(config.postgres ?? {});
|
|
@@ -2653,7 +2741,7 @@ function performScan(db) {
|
|
|
2653
2741
|
result.projectsScanned++;
|
|
2654
2742
|
continue;
|
|
2655
2743
|
}
|
|
2656
|
-
const slug = slugify(basename(rootPath) || encodedDir);
|
|
2744
|
+
const slug = slugify$1(basename(rootPath) || encodedDir);
|
|
2657
2745
|
const { id, isNew } = upsertProject(db, slug, rootPath, encodedDir);
|
|
2658
2746
|
result.projectsScanned++;
|
|
2659
2747
|
if (isNew) result.projectsNew++;
|
|
@@ -2711,7 +2799,7 @@ function performScan(db) {
|
|
|
2711
2799
|
});
|
|
2712
2800
|
for (const child of children) {
|
|
2713
2801
|
const childPath = join(scanDir, child);
|
|
2714
|
-
const childSlug = slugify(child);
|
|
2802
|
+
const childSlug = slugify$1(child);
|
|
2715
2803
|
const childEncoded = encodeDir(childPath);
|
|
2716
2804
|
const existing = db.prepare("SELECT id FROM projects WHERE root_path = ?").get(childPath);
|
|
2717
2805
|
if (existing) {
|
|
@@ -2823,7 +2911,7 @@ function cmdMigrate$1(db) {
|
|
|
2823
2911
|
}
|
|
2824
2912
|
const rootPath = entry.original_path;
|
|
2825
2913
|
const encodedDir = entry.encoded_dir;
|
|
2826
|
-
const slug = slugify(rootPath);
|
|
2914
|
+
const slug = slugify$1(rootPath);
|
|
2827
2915
|
try {
|
|
2828
2916
|
const { isNew, id } = upsertProject(db, slug, rootPath, encodedDir);
|
|
2829
2917
|
if (isNew) projectsNew++;
|
|
@@ -3058,7 +3146,7 @@ function registerMemoryCommands(memoryCmd, getDb) {
|
|
|
3058
3146
|
else if (mode === "semantic" || mode === "hybrid") {
|
|
3059
3147
|
const backend = await createStorageBackend(loadConfig$1());
|
|
3060
3148
|
try {
|
|
3061
|
-
const { generateEmbedding } = await import("../embeddings-
|
|
3149
|
+
const { generateEmbedding } = await import("../embeddings-DGRAPAYb.mjs").then((n) => n.i);
|
|
3062
3150
|
console.log(dim("Generating query embedding..."));
|
|
3063
3151
|
const queryEmbedding = await generateEmbedding(query, true);
|
|
3064
3152
|
if (mode === "semantic") results = await backend.searchSemantic(queryEmbedding, searchOpts);
|
|
@@ -3552,8 +3640,8 @@ function cmdLogs(opts) {
|
|
|
3552
3640
|
}
|
|
3553
3641
|
function registerDaemonCommands(daemonCmd) {
|
|
3554
3642
|
daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
|
|
3555
|
-
const { serve } = await import("../daemon-
|
|
3556
|
-
const { loadConfig: lc, ensureConfigDir } = await import("../config-
|
|
3643
|
+
const { serve } = await import("../daemon-CeTX4NpF.mjs").then((n) => n.t);
|
|
3644
|
+
const { loadConfig: lc, ensureConfigDir } = await import("../config-DELNqq3Z.mjs").then((n) => n.r);
|
|
3557
3645
|
ensureConfigDir();
|
|
3558
3646
|
await serve(lc());
|
|
3559
3647
|
});
|
|
@@ -3917,6 +4005,125 @@ function registerRestoreCommands(program) {
|
|
|
3917
4005
|
});
|
|
3918
4006
|
}
|
|
3919
4007
|
|
|
4008
|
+
//#endregion
|
|
4009
|
+
//#region src/cli/commands/settings-manager.ts
|
|
4010
|
+
/**
|
|
4011
|
+
* settings-manager — merge-not-overwrite utility for ~/.claude/settings.json
|
|
4012
|
+
*
|
|
4013
|
+
* Provides safe, idempotent writes to Claude Code's settings.json:
|
|
4014
|
+
* - env vars: added only if the key is absent (never overwrites)
|
|
4015
|
+
* - hooks: appended per hookType, deduplicated by command string
|
|
4016
|
+
* - statusLine: written only if the key is not already present
|
|
4017
|
+
*/
|
|
4018
|
+
const CLAUDE_DIR = join(homedir(), ".claude");
|
|
4019
|
+
const SETTINGS_FILE = join(CLAUDE_DIR, "settings.json");
|
|
4020
|
+
function readSettingsJson() {
|
|
4021
|
+
if (!existsSync(SETTINGS_FILE)) return {};
|
|
4022
|
+
try {
|
|
4023
|
+
return JSON.parse(readFileSync(SETTINGS_FILE, "utf-8"));
|
|
4024
|
+
} catch {
|
|
4025
|
+
return {};
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
function writeSettingsJson(data) {
|
|
4029
|
+
if (!existsSync(CLAUDE_DIR)) mkdirSync(CLAUDE_DIR, { recursive: true });
|
|
4030
|
+
writeFileSync(SETTINGS_FILE, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
4031
|
+
}
|
|
4032
|
+
/**
|
|
4033
|
+
* Merge env vars — add keys that are absent, never overwrite existing ones.
|
|
4034
|
+
*/
|
|
4035
|
+
function mergeEnv(settings, incoming, report) {
|
|
4036
|
+
let changed = false;
|
|
4037
|
+
const existing = typeof settings["env"] === "object" && settings["env"] !== null ? settings["env"] : {};
|
|
4038
|
+
for (const [key, value] of Object.entries(incoming)) if (Object.prototype.hasOwnProperty.call(existing, key)) report.push(chalk.dim(` Skipped: env.${key} already set`));
|
|
4039
|
+
else {
|
|
4040
|
+
existing[key] = value;
|
|
4041
|
+
report.push(chalk.green(` Added env: ${key}`));
|
|
4042
|
+
changed = true;
|
|
4043
|
+
}
|
|
4044
|
+
settings["env"] = existing;
|
|
4045
|
+
return changed;
|
|
4046
|
+
}
|
|
4047
|
+
/**
|
|
4048
|
+
* Collect every command string already registered for a given hookType.
|
|
4049
|
+
* Stores both the full command and the basename for flexible matching
|
|
4050
|
+
* (handles ${PAI_DIR}/Hooks/foo.sh vs /Users/.../Hooks/foo.sh).
|
|
4051
|
+
*/
|
|
4052
|
+
function existingCommandsForHookType(rules) {
|
|
4053
|
+
const cmds = /* @__PURE__ */ new Set();
|
|
4054
|
+
for (const rule of rules) for (const entry of rule.hooks) {
|
|
4055
|
+
cmds.add(entry.command);
|
|
4056
|
+
const base = entry.command.split("/").pop();
|
|
4057
|
+
if (base) cmds.add(base);
|
|
4058
|
+
}
|
|
4059
|
+
return cmds;
|
|
4060
|
+
}
|
|
4061
|
+
/**
|
|
4062
|
+
* Merge hooks — append entries, deduplicating by command string.
|
|
4063
|
+
*/
|
|
4064
|
+
function mergeHooks(settings, incoming, report) {
|
|
4065
|
+
let changed = false;
|
|
4066
|
+
const hooksSection = typeof settings["hooks"] === "object" && settings["hooks"] !== null ? settings["hooks"] : {};
|
|
4067
|
+
for (const entry of incoming) {
|
|
4068
|
+
const { hookType, matcher, command } = entry;
|
|
4069
|
+
const existingRules = Array.isArray(hooksSection[hookType]) ? hooksSection[hookType] : [];
|
|
4070
|
+
const existingCmds = existingCommandsForHookType(existingRules);
|
|
4071
|
+
const basename = command.split("/").pop() ?? command;
|
|
4072
|
+
if (existingCmds.has(command) || existingCmds.has(basename)) {
|
|
4073
|
+
report.push(chalk.dim(` Skipped: hook ${hookType} → ${basename} already registered`));
|
|
4074
|
+
continue;
|
|
4075
|
+
}
|
|
4076
|
+
const newRule = { hooks: [{
|
|
4077
|
+
type: "command",
|
|
4078
|
+
command
|
|
4079
|
+
}] };
|
|
4080
|
+
if (matcher !== void 0) newRule.matcher = matcher;
|
|
4081
|
+
existingRules.push(newRule);
|
|
4082
|
+
hooksSection[hookType] = existingRules;
|
|
4083
|
+
report.push(chalk.green(` Added hook: ${hookType} → ${basename}`));
|
|
4084
|
+
changed = true;
|
|
4085
|
+
}
|
|
4086
|
+
settings["hooks"] = hooksSection;
|
|
4087
|
+
return changed;
|
|
4088
|
+
}
|
|
4089
|
+
/**
|
|
4090
|
+
* Merge statusLine — write only if the key is not already present.
|
|
4091
|
+
*/
|
|
4092
|
+
function mergeStatusLine(settings, incoming, report) {
|
|
4093
|
+
if (Object.prototype.hasOwnProperty.call(settings, "statusLine")) {
|
|
4094
|
+
report.push(chalk.dim(" Skipped: statusLine already configured"));
|
|
4095
|
+
return false;
|
|
4096
|
+
}
|
|
4097
|
+
settings["statusLine"] = { ...incoming };
|
|
4098
|
+
report.push(chalk.green(" Added statusLine"));
|
|
4099
|
+
return true;
|
|
4100
|
+
}
|
|
4101
|
+
/**
|
|
4102
|
+
* Merge env vars, hooks, and/or a statusLine entry into ~/.claude/settings.json.
|
|
4103
|
+
* Never overwrites existing values — only adds what is missing.
|
|
4104
|
+
*
|
|
4105
|
+
* Returns { changed, report } where report contains human-readable lines.
|
|
4106
|
+
*/
|
|
4107
|
+
function mergeSettings(opts) {
|
|
4108
|
+
const settings = readSettingsJson();
|
|
4109
|
+
const report = [];
|
|
4110
|
+
let changed = false;
|
|
4111
|
+
if (opts.env !== void 0 && Object.keys(opts.env).length > 0) {
|
|
4112
|
+
if (mergeEnv(settings, opts.env, report)) changed = true;
|
|
4113
|
+
}
|
|
4114
|
+
if (opts.hooks !== void 0 && opts.hooks.length > 0) {
|
|
4115
|
+
if (mergeHooks(settings, opts.hooks, report)) changed = true;
|
|
4116
|
+
}
|
|
4117
|
+
if (opts.statusLine !== void 0) {
|
|
4118
|
+
if (mergeStatusLine(settings, opts.statusLine, report)) changed = true;
|
|
4119
|
+
}
|
|
4120
|
+
if (changed) writeSettingsJson(settings);
|
|
4121
|
+
return {
|
|
4122
|
+
changed,
|
|
4123
|
+
report
|
|
4124
|
+
};
|
|
4125
|
+
}
|
|
4126
|
+
|
|
3920
4127
|
//#endregion
|
|
3921
4128
|
//#region src/cli/commands/setup.ts
|
|
3922
4129
|
const c = {
|
|
@@ -4038,6 +4245,24 @@ function getTemplatesDir$1() {
|
|
|
4038
4245
|
for (const c of candidates) if (existsSync(join(c, "claude-md.template.md"))) return c;
|
|
4039
4246
|
return join(process.cwd(), "templates");
|
|
4040
4247
|
}
|
|
4248
|
+
function getHooksDir() {
|
|
4249
|
+
const candidates = [
|
|
4250
|
+
join(process.cwd(), "src", "hooks"),
|
|
4251
|
+
join(homedir(), "dev", "ai", "PAI", "src", "hooks"),
|
|
4252
|
+
join("/", "usr", "local", "lib", "node_modules", "@tekmidian", "pai", "src", "hooks")
|
|
4253
|
+
];
|
|
4254
|
+
for (const c of candidates) if (existsSync(join(c, "session-stop.sh"))) return c;
|
|
4255
|
+
return join(process.cwd(), "src", "hooks");
|
|
4256
|
+
}
|
|
4257
|
+
function getStatuslineScript() {
|
|
4258
|
+
const candidates = [
|
|
4259
|
+
join(process.cwd(), "statusline-command.sh"),
|
|
4260
|
+
join(homedir(), "dev", "ai", "PAI", "statusline-command.sh"),
|
|
4261
|
+
join("/", "usr", "local", "lib", "node_modules", "@tekmidian", "pai", "statusline-command.sh")
|
|
4262
|
+
];
|
|
4263
|
+
for (const c of candidates) if (existsSync(c)) return c;
|
|
4264
|
+
return null;
|
|
4265
|
+
}
|
|
4041
4266
|
async function startDocker(rl) {
|
|
4042
4267
|
const dockerDir = getDockerDir();
|
|
4043
4268
|
if (!existsSync(join(dockerDir, "docker-compose.yml"))) {
|
|
@@ -4069,8 +4294,8 @@ async function startDocker(rl) {
|
|
|
4069
4294
|
}
|
|
4070
4295
|
async function testPostgresConnection(connectionString) {
|
|
4071
4296
|
try {
|
|
4072
|
-
const
|
|
4073
|
-
const client = new
|
|
4297
|
+
const pgModule = await import("pg");
|
|
4298
|
+
const client = new (pgModule.default ?? pgModule).Client({ connectionString });
|
|
4074
4299
|
await client.connect();
|
|
4075
4300
|
await client.end();
|
|
4076
4301
|
return true;
|
|
@@ -4099,6 +4324,35 @@ function stepWelcome() {
|
|
|
4099
4324
|
*/
|
|
4100
4325
|
async function stepStorage(rl) {
|
|
4101
4326
|
section("Step 2: Storage Backend");
|
|
4327
|
+
const existing = readConfigRaw();
|
|
4328
|
+
if (existing.storageBackend) {
|
|
4329
|
+
const backend = String(existing.storageBackend);
|
|
4330
|
+
if (backend === "postgres") {
|
|
4331
|
+
try {
|
|
4332
|
+
const status = spawnSync("docker", [
|
|
4333
|
+
"ps",
|
|
4334
|
+
"--filter",
|
|
4335
|
+
"name=pai-pgvector",
|
|
4336
|
+
"--format",
|
|
4337
|
+
"{{.Status}}"
|
|
4338
|
+
], { stdio: "pipe" }).stdout?.toString().trim();
|
|
4339
|
+
if (status && status.includes("Up")) {
|
|
4340
|
+
console.log(c.ok(`Storage backend: PostgreSQL (container running). Skipping.`));
|
|
4341
|
+
return existing;
|
|
4342
|
+
}
|
|
4343
|
+
console.log(c.dim(" PostgreSQL container found but not running. Starting..."));
|
|
4344
|
+
await startDocker(rl);
|
|
4345
|
+
await new Promise((r) => setTimeout(r, 3e3));
|
|
4346
|
+
console.log(c.ok("PostgreSQL container started."));
|
|
4347
|
+
} catch {
|
|
4348
|
+
console.log(c.ok(`Storage backend: PostgreSQL (configured). Skipping.`));
|
|
4349
|
+
}
|
|
4350
|
+
return existing;
|
|
4351
|
+
} else {
|
|
4352
|
+
console.log(c.ok(`Storage backend: ${backend}. Skipping.`));
|
|
4353
|
+
return existing;
|
|
4354
|
+
}
|
|
4355
|
+
}
|
|
4102
4356
|
line$1();
|
|
4103
4357
|
line$1(" Choose how PAI stores your indexed knowledge:");
|
|
4104
4358
|
line$1();
|
|
@@ -4176,6 +4430,11 @@ async function stepStorage(rl) {
|
|
|
4176
4430
|
*/
|
|
4177
4431
|
async function stepEmbedding(rl) {
|
|
4178
4432
|
section("Step 3: Embedding Model");
|
|
4433
|
+
const existing = readConfigRaw();
|
|
4434
|
+
if (existing.embeddingModel) {
|
|
4435
|
+
console.log(c.ok(`Embedding model: ${existing.embeddingModel}. Skipping.`));
|
|
4436
|
+
return { embeddingModel: existing.embeddingModel };
|
|
4437
|
+
}
|
|
4179
4438
|
line$1();
|
|
4180
4439
|
line$1(" An embedding model converts your text into vectors for semantic search.");
|
|
4181
4440
|
line$1(" Models are downloaded from HuggingFace on first use.");
|
|
@@ -4278,10 +4537,192 @@ async function stepClaudeMd(rl) {
|
|
|
4278
4537
|
return true;
|
|
4279
4538
|
}
|
|
4280
4539
|
/**
|
|
4281
|
-
* Step 5:
|
|
4540
|
+
* Step 5: PAI skill installation (~/.claude/skills/PAI/SKILL.md)
|
|
4541
|
+
*/
|
|
4542
|
+
async function stepPaiSkill(rl) {
|
|
4543
|
+
section("Step 5: PAI Skill Installation");
|
|
4544
|
+
line$1();
|
|
4545
|
+
line$1(" PAI ships a SKILL.md that tells Claude Code how to invoke PAI commands.");
|
|
4546
|
+
line$1();
|
|
4547
|
+
const templatePath = join(getTemplatesDir$1(), "pai-skill.template.md");
|
|
4548
|
+
if (!existsSync(templatePath)) {
|
|
4549
|
+
console.log(c.warn("Skill template not found: " + templatePath));
|
|
4550
|
+
console.log(c.dim(" Skipping PAI skill installation."));
|
|
4551
|
+
return false;
|
|
4552
|
+
}
|
|
4553
|
+
const skillDir = join(homedir(), ".claude", "skills", "PAI");
|
|
4554
|
+
const skillFile = join(skillDir, "SKILL.md");
|
|
4555
|
+
if (existsSync(skillFile)) {
|
|
4556
|
+
const content = readFileSync(skillFile, "utf-8");
|
|
4557
|
+
const isGenerated = content.includes("Generated by PAI Setup");
|
|
4558
|
+
if (isGenerated) console.log(c.dim(" Found existing PAI-generated SKILL.md."));
|
|
4559
|
+
else {
|
|
4560
|
+
console.log(c.yellow(" Found existing SKILL.md (not PAI-generated)."));
|
|
4561
|
+
console.log(c.dim(" A backup will be created before overwriting."));
|
|
4562
|
+
}
|
|
4563
|
+
line$1();
|
|
4564
|
+
if (!await promptYesNo(rl, "Update ~/.claude/skills/PAI/SKILL.md with the latest PAI skill?", isGenerated)) {
|
|
4565
|
+
console.log(c.dim(" Keeping existing SKILL.md unchanged."));
|
|
4566
|
+
return false;
|
|
4567
|
+
}
|
|
4568
|
+
if (!isGenerated) {
|
|
4569
|
+
const backupPath = skillFile + ".backup";
|
|
4570
|
+
writeFileSync(backupPath, content, "utf-8");
|
|
4571
|
+
console.log(c.ok(`Backed up existing SKILL.md to ${backupPath}`));
|
|
4572
|
+
}
|
|
4573
|
+
} else if (!await promptYesNo(rl, "Install PAI skill to ~/.claude/skills/PAI/SKILL.md?", true)) {
|
|
4574
|
+
console.log(c.dim(" Skipping PAI skill installation."));
|
|
4575
|
+
return false;
|
|
4576
|
+
}
|
|
4577
|
+
let template = readFileSync(templatePath, "utf-8");
|
|
4578
|
+
template = template.replace(/\$\{HOME\}/g, homedir());
|
|
4579
|
+
if (!existsSync(skillDir)) mkdirSync(skillDir, { recursive: true });
|
|
4580
|
+
writeFileSync(skillFile, template, "utf-8");
|
|
4581
|
+
line$1();
|
|
4582
|
+
console.log(c.ok("Installed ~/.claude/skills/PAI/SKILL.md"));
|
|
4583
|
+
return true;
|
|
4584
|
+
}
|
|
4585
|
+
/**
|
|
4586
|
+
* Step 6: Hook scripts (pre-compact, session-stop, statusline)
|
|
4587
|
+
*/
|
|
4588
|
+
async function stepHooks(rl) {
|
|
4589
|
+
section("Step 6: Lifecycle Hooks");
|
|
4590
|
+
line$1();
|
|
4591
|
+
line$1(" PAI hooks fire on session stop and context compaction to save state,");
|
|
4592
|
+
line$1(" update notes, and display live statusline information.");
|
|
4593
|
+
line$1();
|
|
4594
|
+
if (!await promptYesNo(rl, "Install PAI lifecycle hooks (session stop, pre-compact, statusline)?", true)) {
|
|
4595
|
+
console.log(c.dim(" Skipping hook installation."));
|
|
4596
|
+
return false;
|
|
4597
|
+
}
|
|
4598
|
+
const hooksDir = getHooksDir();
|
|
4599
|
+
const statuslineSrc = getStatuslineScript();
|
|
4600
|
+
const claudeDir = join(homedir(), ".claude");
|
|
4601
|
+
const hooksTarget = join(claudeDir, "Hooks");
|
|
4602
|
+
if (!existsSync(hooksTarget)) mkdirSync(hooksTarget, { recursive: true });
|
|
4603
|
+
let anyInstalled = false;
|
|
4604
|
+
function installFile(src, dest, label) {
|
|
4605
|
+
if (!existsSync(src)) {
|
|
4606
|
+
console.log(c.warn(` Source not found: ${src}`));
|
|
4607
|
+
return;
|
|
4608
|
+
}
|
|
4609
|
+
const srcContent = readFileSync(src, "utf-8");
|
|
4610
|
+
if (existsSync(dest)) {
|
|
4611
|
+
if (srcContent === readFileSync(dest, "utf-8")) {
|
|
4612
|
+
console.log(c.dim(` Unchanged: ${label}`));
|
|
4613
|
+
return;
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
copyFileSync(src, dest);
|
|
4617
|
+
chmodSync(dest, 493);
|
|
4618
|
+
console.log(c.ok(`Installed: ${label}`));
|
|
4619
|
+
anyInstalled = true;
|
|
4620
|
+
}
|
|
4621
|
+
line$1();
|
|
4622
|
+
installFile(join(hooksDir, "pre-compact.sh"), join(hooksTarget, "pai-pre-compact.sh"), "pai-pre-compact.sh");
|
|
4623
|
+
installFile(join(hooksDir, "session-stop.sh"), join(hooksTarget, "pai-session-stop.sh"), "pai-session-stop.sh");
|
|
4624
|
+
if (statuslineSrc) installFile(statuslineSrc, join(claudeDir, "statusline-command.sh"), "statusline-command.sh");
|
|
4625
|
+
else console.log(c.warn(" statusline-command.sh not found — skipping statusline."));
|
|
4626
|
+
return anyInstalled;
|
|
4627
|
+
}
|
|
4628
|
+
/**
|
|
4629
|
+
* Step 7: Patch ~/.claude/settings.json with PAI hooks, env vars, and statusline
|
|
4630
|
+
*/
|
|
4631
|
+
async function stepSettings(rl) {
|
|
4632
|
+
section("Step 7: Settings Patch");
|
|
4633
|
+
line$1();
|
|
4634
|
+
line$1(" PAI will add env vars, hook registrations, and the statusline command");
|
|
4635
|
+
line$1(" to ~/.claude/settings.json. Existing values are never overwritten.");
|
|
4636
|
+
line$1();
|
|
4637
|
+
if (!await promptYesNo(rl, "Patch ~/.claude/settings.json with PAI hooks, env vars, and statusline?", true)) {
|
|
4638
|
+
console.log(c.dim(" Skipping settings patch."));
|
|
4639
|
+
return false;
|
|
4640
|
+
}
|
|
4641
|
+
const result = mergeSettings({
|
|
4642
|
+
env: {
|
|
4643
|
+
PAI_DIR: join(homedir(), ".claude"),
|
|
4644
|
+
CLAUDE_AUTOCOMPACT_PCT_OVERRIDE: "80"
|
|
4645
|
+
},
|
|
4646
|
+
hooks: [{
|
|
4647
|
+
hookType: "PreCompact",
|
|
4648
|
+
matcher: "",
|
|
4649
|
+
command: "${PAI_DIR}/Hooks/pai-pre-compact.sh"
|
|
4650
|
+
}, {
|
|
4651
|
+
hookType: "Stop",
|
|
4652
|
+
command: "${PAI_DIR}/Hooks/pai-session-stop.sh"
|
|
4653
|
+
}],
|
|
4654
|
+
statusLine: {
|
|
4655
|
+
type: "command",
|
|
4656
|
+
command: "bash ${PAI_DIR}/statusline-command.sh"
|
|
4657
|
+
}
|
|
4658
|
+
});
|
|
4659
|
+
line$1();
|
|
4660
|
+
for (const r of result.report) console.log(r);
|
|
4661
|
+
if (!result.changed) console.log(c.dim(" Settings already up-to-date. No changes made."));
|
|
4662
|
+
return result.changed;
|
|
4663
|
+
}
|
|
4664
|
+
/**
|
|
4665
|
+
* Step 8: Daemon install (launchd plist)
|
|
4666
|
+
*/
|
|
4667
|
+
async function stepDaemon(rl) {
|
|
4668
|
+
section("Step 8: Daemon Install");
|
|
4669
|
+
line$1();
|
|
4670
|
+
line$1(" The PAI daemon indexes your projects every 5 minutes in the background.");
|
|
4671
|
+
line$1();
|
|
4672
|
+
if (existsSync(join(homedir(), "Library", "LaunchAgents", "com.pai.pai-daemon.plist"))) {
|
|
4673
|
+
console.log(c.dim(" PAI daemon plist already installed."));
|
|
4674
|
+
line$1();
|
|
4675
|
+
if (!await promptYesNo(rl, "Reinstall the PAI daemon launchd plist?", false)) {
|
|
4676
|
+
console.log(c.dim(" Keeping existing daemon installation."));
|
|
4677
|
+
return false;
|
|
4678
|
+
}
|
|
4679
|
+
} else if (!await promptYesNo(rl, "Install the PAI daemon to run automatically at login?", true)) {
|
|
4680
|
+
console.log(c.dim(" Skipping daemon install. Run manually: pai daemon install"));
|
|
4681
|
+
return false;
|
|
4682
|
+
}
|
|
4683
|
+
line$1();
|
|
4684
|
+
if (spawnSync("pai", ["daemon", "install"], { stdio: "inherit" }).status !== 0) {
|
|
4685
|
+
console.log(c.warn(" Daemon install failed. Run manually: pai daemon install"));
|
|
4686
|
+
return false;
|
|
4687
|
+
}
|
|
4688
|
+
console.log(c.ok("Daemon installed as com.pai.pai-daemon."));
|
|
4689
|
+
return true;
|
|
4690
|
+
}
|
|
4691
|
+
/**
|
|
4692
|
+
* Step 9: MCP registration in ~/.claude.json
|
|
4693
|
+
*/
|
|
4694
|
+
async function stepMcp(rl) {
|
|
4695
|
+
section("Step 9: MCP Registration");
|
|
4696
|
+
line$1();
|
|
4697
|
+
line$1(" Registering the PAI MCP server lets Claude Code call PAI tools directly.");
|
|
4698
|
+
line$1();
|
|
4699
|
+
const claudeJsonPath = join(homedir(), ".claude.json");
|
|
4700
|
+
if (existsSync(claudeJsonPath)) try {
|
|
4701
|
+
const raw = readFileSync(claudeJsonPath, "utf-8");
|
|
4702
|
+
const mcpServers = JSON.parse(raw)["mcpServers"];
|
|
4703
|
+
if (mcpServers && Object.prototype.hasOwnProperty.call(mcpServers, "pai")) {
|
|
4704
|
+
console.log(c.ok("PAI MCP server already registered in ~/.claude.json."));
|
|
4705
|
+
console.log(c.dim(" Skipping MCP registration."));
|
|
4706
|
+
return false;
|
|
4707
|
+
}
|
|
4708
|
+
} catch {}
|
|
4709
|
+
if (!await promptYesNo(rl, "Register the PAI MCP server in ~/.claude.json?", true)) {
|
|
4710
|
+
console.log(c.dim(" Skipping MCP registration. Run manually: pai mcp install"));
|
|
4711
|
+
return false;
|
|
4712
|
+
}
|
|
4713
|
+
line$1();
|
|
4714
|
+
if (spawnSync("pai", ["mcp", "install"], { stdio: "inherit" }).status !== 0) {
|
|
4715
|
+
console.log(c.warn(" MCP registration failed. Run manually: pai mcp install"));
|
|
4716
|
+
return false;
|
|
4717
|
+
}
|
|
4718
|
+
console.log(c.ok("PAI MCP server registered in ~/.claude.json."));
|
|
4719
|
+
return true;
|
|
4720
|
+
}
|
|
4721
|
+
/**
|
|
4722
|
+
* Step 10: Directory scanning configuration
|
|
4282
4723
|
*/
|
|
4283
4724
|
async function stepDirectories(rl) {
|
|
4284
|
-
section("Step
|
|
4725
|
+
section("Step 10: Directories to Index");
|
|
4285
4726
|
line$1();
|
|
4286
4727
|
line$1(" PAI indexes files in your registered projects. You can register projects");
|
|
4287
4728
|
line$1(" individually with `pai project add <path>`, or let the registry scanner");
|
|
@@ -4308,10 +4749,10 @@ async function stepDirectories(rl) {
|
|
|
4308
4749
|
stepDirectories._runScan = runScan;
|
|
4309
4750
|
}
|
|
4310
4751
|
/**
|
|
4311
|
-
* Step
|
|
4752
|
+
* Step 11: Initial index
|
|
4312
4753
|
*/
|
|
4313
4754
|
async function stepInitialIndex(rl) {
|
|
4314
|
-
section("Step
|
|
4755
|
+
section("Step 11: Initial Index");
|
|
4315
4756
|
line$1();
|
|
4316
4757
|
line$1(" Indexing scans your registered projects and builds the search index.");
|
|
4317
4758
|
line$1(" The daemon runs indexing automatically every 5 minutes once started.");
|
|
@@ -4353,9 +4794,9 @@ async function stepInitialIndex(rl) {
|
|
|
4353
4794
|
}
|
|
4354
4795
|
}
|
|
4355
4796
|
/**
|
|
4356
|
-
* Step
|
|
4797
|
+
* Step 12: Summary and next steps
|
|
4357
4798
|
*/
|
|
4358
|
-
function stepSummary(configUpdates, claudeMdGenerated) {
|
|
4799
|
+
function stepSummary(configUpdates, claudeMdGenerated, paiSkillInstalled, hooksInstalled, settingsPatched, daemonInstalled, mcpRegistered) {
|
|
4359
4800
|
section("Setup Complete");
|
|
4360
4801
|
line$1();
|
|
4361
4802
|
console.log(c.ok("PAI Knowledge OS is configured!"));
|
|
@@ -4366,7 +4807,14 @@ function stepSummary(configUpdates, claudeMdGenerated) {
|
|
|
4366
4807
|
line$1();
|
|
4367
4808
|
console.log(chalk.dim(" Storage backend: ") + chalk.cyan(backend ?? "sqlite"));
|
|
4368
4809
|
console.log(chalk.dim(" Embedding model: ") + chalk.cyan(model && model !== "none" ? model : "(none — keyword search only)"));
|
|
4369
|
-
console.log(chalk.dim("
|
|
4810
|
+
console.log(chalk.dim(" CLAUDE.md: ") + chalk.cyan(claudeMdGenerated ? "~/.claude/CLAUDE.md (generated)" : "(unchanged)"));
|
|
4811
|
+
console.log(chalk.dim(" PAI skill: ") + chalk.cyan(paiSkillInstalled ? "~/.claude/skills/PAI/SKILL.md (installed)" : "(unchanged)"));
|
|
4812
|
+
console.log(chalk.dim(" Hooks: ") + chalk.cyan(hooksInstalled ? "pai-pre-compact.sh, pai-session-stop.sh (installed)" : "(unchanged)"));
|
|
4813
|
+
console.log(chalk.dim(" Settings: ") + chalk.cyan(settingsPatched ? "env vars, hooks, statusline (patched)" : "(unchanged)"));
|
|
4814
|
+
console.log(chalk.dim(" Daemon: ") + chalk.cyan(daemonInstalled ? "com.pai.pai-daemon (installed)" : "(unchanged)"));
|
|
4815
|
+
console.log(chalk.dim(" MCP: ") + chalk.cyan(mcpRegistered ? "registered in ~/.claude.json" : "(unchanged)"));
|
|
4816
|
+
line$1();
|
|
4817
|
+
console.log(chalk.bold.yellow(" → RESTART Claude Code to activate all changes."));
|
|
4370
4818
|
line$1();
|
|
4371
4819
|
line$1(chalk.bold(" Next steps:"));
|
|
4372
4820
|
line$1();
|
|
@@ -4414,10 +4862,14 @@ async function runSetup() {
|
|
|
4414
4862
|
stepWelcome();
|
|
4415
4863
|
line$1();
|
|
4416
4864
|
await prompt(rl, chalk.dim(" Press Enter to begin setup..."));
|
|
4417
|
-
section("Step 2: Storage Backend");
|
|
4418
4865
|
const storageConfig = await stepStorage(rl);
|
|
4419
4866
|
const embeddingConfig = await stepEmbedding(rl);
|
|
4420
4867
|
const claudeMdGenerated = await stepClaudeMd(rl);
|
|
4868
|
+
const paiSkillInstalled = await stepPaiSkill(rl);
|
|
4869
|
+
const hooksInstalled = await stepHooks(rl);
|
|
4870
|
+
const settingsPatched = await stepSettings(rl);
|
|
4871
|
+
const daemonInstalled = await stepDaemon(rl);
|
|
4872
|
+
const mcpRegistered = await stepMcp(rl);
|
|
4421
4873
|
await stepDirectories(rl);
|
|
4422
4874
|
const allUpdates = {
|
|
4423
4875
|
...storageConfig,
|
|
@@ -4427,7 +4879,7 @@ async function runSetup() {
|
|
|
4427
4879
|
line$1();
|
|
4428
4880
|
console.log(c.ok("Configuration saved."));
|
|
4429
4881
|
await stepInitialIndex(rl);
|
|
4430
|
-
stepSummary(allUpdates, claudeMdGenerated);
|
|
4882
|
+
stepSummary(allUpdates, claudeMdGenerated, paiSkillInstalled, hooksInstalled, settingsPatched, daemonInstalled, mcpRegistered);
|
|
4431
4883
|
} finally {
|
|
4432
4884
|
rl.close();
|
|
4433
4885
|
}
|
|
@@ -5296,6 +5748,348 @@ function registerObsidianCommands(obsidianCmd, getDb) {
|
|
|
5296
5748
|
});
|
|
5297
5749
|
}
|
|
5298
5750
|
|
|
5751
|
+
//#endregion
|
|
5752
|
+
//#region src/cli/commands/zettel.ts
|
|
5753
|
+
let _fedDb = null;
|
|
5754
|
+
function getFedDb() {
|
|
5755
|
+
if (!_fedDb) try {
|
|
5756
|
+
_fedDb = openFederation();
|
|
5757
|
+
} catch (e) {
|
|
5758
|
+
console.error(err(`Failed to open PAI federation DB: ${e}`));
|
|
5759
|
+
process.exit(1);
|
|
5760
|
+
}
|
|
5761
|
+
return _fedDb;
|
|
5762
|
+
}
|
|
5763
|
+
/** Shorten a vault path to just the last 2-3 components for display. */
|
|
5764
|
+
function shortPath(p, parts = 3) {
|
|
5765
|
+
return p.split("/").filter(Boolean).slice(-parts).join("/");
|
|
5766
|
+
}
|
|
5767
|
+
async function cmdExplore(note, opts) {
|
|
5768
|
+
const depth = parseInt(opts.depth ?? "3", 10);
|
|
5769
|
+
const direction = opts.direction ?? "both";
|
|
5770
|
+
const mode = opts.mode ?? "all";
|
|
5771
|
+
const { zettelExplore } = await import("../zettelkasten-Co-w0XSZ.mjs");
|
|
5772
|
+
const result = zettelExplore(getFedDb(), {
|
|
5773
|
+
startNote: note,
|
|
5774
|
+
depth,
|
|
5775
|
+
direction,
|
|
5776
|
+
mode
|
|
5777
|
+
});
|
|
5778
|
+
console.log();
|
|
5779
|
+
console.log(header(" PAI Zettel Explore"));
|
|
5780
|
+
console.log(dim(` Starting note: ${note}`));
|
|
5781
|
+
console.log(dim(` Depth: ${depth} Direction: ${direction} Mode: ${mode}`));
|
|
5782
|
+
console.log();
|
|
5783
|
+
if (result.nodes.length === 0) {
|
|
5784
|
+
console.log(warn(" No connected notes found. Check that the note path exists in the vault index."));
|
|
5785
|
+
console.log();
|
|
5786
|
+
return;
|
|
5787
|
+
}
|
|
5788
|
+
console.log(` ${chalk.cyan("●")} ${bold(shortPath(result.root))} ${dim("(root)")}`);
|
|
5789
|
+
const byDepth = /* @__PURE__ */ new Map();
|
|
5790
|
+
for (const node of result.nodes) {
|
|
5791
|
+
const list = byDepth.get(node.depth) ?? [];
|
|
5792
|
+
list.push(node);
|
|
5793
|
+
byDepth.set(node.depth, list);
|
|
5794
|
+
}
|
|
5795
|
+
for (let d = 1; d <= depth; d++) {
|
|
5796
|
+
const nodes = byDepth.get(d) ?? [];
|
|
5797
|
+
if (nodes.length === 0) continue;
|
|
5798
|
+
console.log();
|
|
5799
|
+
console.log(dim(` ${" ".repeat(d - 1)}Depth ${d}:`));
|
|
5800
|
+
for (const node of nodes) {
|
|
5801
|
+
const indent = " ".repeat(d);
|
|
5802
|
+
const isBranching = result.branchingPoints.includes(node.path);
|
|
5803
|
+
const typeColor = node.linkType === "sequential" ? chalk.blue : chalk.magenta;
|
|
5804
|
+
const branchMark = isBranching ? chalk.yellow(" ⑂ branching") : "";
|
|
5805
|
+
const title = node.title ?? shortPath(node.path);
|
|
5806
|
+
const stats = dim(`in:${node.inbound} out:${node.outbound}`);
|
|
5807
|
+
console.log(` ${indent}${typeColor("→")} ${bold(title)}${branchMark} ${stats} ${dim(typeColor(node.linkType))}`);
|
|
5808
|
+
}
|
|
5809
|
+
}
|
|
5810
|
+
console.log();
|
|
5811
|
+
const edgeSummary = `${result.edges.length} edges (${result.edges.filter((e) => e.type === "sequential").length} sequential, ${result.edges.filter((e) => e.type === "associative").length} associative)`;
|
|
5812
|
+
console.log(dim(` ${edgeSummary}`));
|
|
5813
|
+
if (result.branchingPoints.length > 0) console.log(ok(` ${result.branchingPoints.length} branching point(s) found`));
|
|
5814
|
+
if (result.maxDepthReached) console.log(warn(" Max depth reached — use --depth to explore further"));
|
|
5815
|
+
console.log();
|
|
5816
|
+
}
|
|
5817
|
+
async function cmdHealth(opts) {
|
|
5818
|
+
const scope = opts.scope ?? "full";
|
|
5819
|
+
const projectPath = opts.project;
|
|
5820
|
+
const recentDays = parseInt(opts.days ?? "30", 10);
|
|
5821
|
+
const includeTypes = opts.include ? opts.include.split(",").map((s) => s.trim()) : void 0;
|
|
5822
|
+
const { zettelHealth } = await import("../zettelkasten-Co-w0XSZ.mjs");
|
|
5823
|
+
const result = zettelHealth(getFedDb(), {
|
|
5824
|
+
scope,
|
|
5825
|
+
projectPath,
|
|
5826
|
+
recentDays,
|
|
5827
|
+
include: includeTypes
|
|
5828
|
+
});
|
|
5829
|
+
console.log();
|
|
5830
|
+
console.log(header(" PAI Zettel Health"));
|
|
5831
|
+
console.log(dim(` Scope: ${scope}${scope === "project" ? ` Path: ${projectPath ?? "(none)"}` : ""}${scope === "recent" ? ` Days: ${recentDays}` : ""}`));
|
|
5832
|
+
console.log();
|
|
5833
|
+
const score = result.healthScore;
|
|
5834
|
+
const scoreColor = score >= 80 ? chalk.green : score >= 60 ? chalk.yellow : chalk.red;
|
|
5835
|
+
const barWidth = 30;
|
|
5836
|
+
const filled = Math.round(score / 100 * barWidth);
|
|
5837
|
+
const bar = scoreColor("█".repeat(filled)) + dim("░".repeat(barWidth - filled));
|
|
5838
|
+
console.log(` Health Score: ${scoreColor(bold(String(score)))}% [${bar}]`);
|
|
5839
|
+
console.log();
|
|
5840
|
+
console.log(dim(` Files: ${result.totalFiles} Links: ${result.totalLinks}`));
|
|
5841
|
+
console.log();
|
|
5842
|
+
if (result.deadLinks.length === 0) console.log(ok(" Dead links: none"));
|
|
5843
|
+
else {
|
|
5844
|
+
console.log(warn(` Dead links: ${result.deadLinks.length}`));
|
|
5845
|
+
const preview = result.deadLinks.slice(0, 10);
|
|
5846
|
+
for (const dl of preview) console.log(` ${chalk.red("✗")} ${dim(shortPath(dl.sourcePath))} → ${bold(dl.targetRaw)} ${dim(`(line ${dl.lineNumber})`)}`);
|
|
5847
|
+
if (result.deadLinks.length > 10) console.log(dim(` ... and ${result.deadLinks.length - 10} more`));
|
|
5848
|
+
console.log();
|
|
5849
|
+
}
|
|
5850
|
+
if (result.orphans.length === 0) console.log(ok(" Orphan notes: none"));
|
|
5851
|
+
else {
|
|
5852
|
+
console.log(warn(` Orphan notes: ${result.orphans.length}`));
|
|
5853
|
+
const preview = result.orphans.slice(0, 10);
|
|
5854
|
+
for (const o of preview) console.log(` ${chalk.yellow("○")} ${dim(shortPath(o))}`);
|
|
5855
|
+
if (result.orphans.length > 10) console.log(dim(` ... and ${result.orphans.length - 10} more`));
|
|
5856
|
+
console.log();
|
|
5857
|
+
}
|
|
5858
|
+
if (result.disconnectedClusters <= 1) console.log(ok(" Disconnected clusters: 1 (fully connected)"));
|
|
5859
|
+
else console.log(warn(` Disconnected clusters: ${result.disconnectedClusters}`));
|
|
5860
|
+
if (result.lowConnectivity.length === 0) console.log(ok(" Low-connectivity: none"));
|
|
5861
|
+
else {
|
|
5862
|
+
console.log(warn(` Low-connectivity: ${result.lowConnectivity.length} note(s) with ≤1 link`));
|
|
5863
|
+
const preview = result.lowConnectivity.slice(0, 5);
|
|
5864
|
+
for (const lc of preview) console.log(` ${chalk.dim("—")} ${dim(shortPath(lc))}`);
|
|
5865
|
+
if (result.lowConnectivity.length > 5) console.log(dim(` ... and ${result.lowConnectivity.length - 5} more`));
|
|
5866
|
+
}
|
|
5867
|
+
console.log();
|
|
5868
|
+
}
|
|
5869
|
+
async function cmdSurprise(note, opts) {
|
|
5870
|
+
if (!opts.vaultProjectId) {
|
|
5871
|
+
console.error(err(" --vault-project-id is required"));
|
|
5872
|
+
process.exit(1);
|
|
5873
|
+
}
|
|
5874
|
+
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
5875
|
+
const limit = parseInt(opts.limit ?? "10", 10);
|
|
5876
|
+
const minSimilarity = parseFloat(opts.minSimilarity ?? "0.3");
|
|
5877
|
+
const minGraphDistance = parseInt(opts.minDistance ?? "3", 10);
|
|
5878
|
+
const { zettelSurprise } = await import("../zettelkasten-Co-w0XSZ.mjs");
|
|
5879
|
+
const db = getFedDb();
|
|
5880
|
+
console.log();
|
|
5881
|
+
console.log(header(" PAI Zettel Surprise"));
|
|
5882
|
+
console.log(dim(` Reference note: ${note}`));
|
|
5883
|
+
process.stdout.write(dim(" Searching for surprising connections...\n"));
|
|
5884
|
+
const results = await zettelSurprise(db, {
|
|
5885
|
+
referencePath: note,
|
|
5886
|
+
vaultProjectId,
|
|
5887
|
+
limit,
|
|
5888
|
+
minSimilarity,
|
|
5889
|
+
minGraphDistance
|
|
5890
|
+
});
|
|
5891
|
+
if (results.length === 0) {
|
|
5892
|
+
console.log(warn(" No surprising connections found. Try lowering --min-similarity or --min-distance."));
|
|
5893
|
+
console.log();
|
|
5894
|
+
return;
|
|
5895
|
+
}
|
|
5896
|
+
console.log();
|
|
5897
|
+
console.log(bold(` Found ${results.length} surprising connection(s):`));
|
|
5898
|
+
console.log();
|
|
5899
|
+
for (let i = 0; i < results.length; i++) {
|
|
5900
|
+
const r = results[i];
|
|
5901
|
+
const title = r.title ?? shortPath(r.path);
|
|
5902
|
+
const surpriseBar = Math.round(r.surpriseScore * 10);
|
|
5903
|
+
console.log(` ${chalk.cyan(String(i + 1).padStart(2, " "))}. ${bold(title)}`);
|
|
5904
|
+
console.log(` ${dim("Surprise:")} ${chalk.magenta(r.surpriseScore.toFixed(3))} ${"■".repeat(surpriseBar)}${"□".repeat(10 - surpriseBar)}`);
|
|
5905
|
+
console.log(` ${dim("Cosine:")} ${r.cosineSimilarity.toFixed(3)} ${dim("Graph distance:")} ${r.graphDistance}`);
|
|
5906
|
+
if (r.sharedSnippet) console.log(` ${dim("Context:")} ${r.sharedSnippet.slice(0, 120)}`);
|
|
5907
|
+
console.log();
|
|
5908
|
+
}
|
|
5909
|
+
}
|
|
5910
|
+
async function cmdSuggest(note, opts) {
|
|
5911
|
+
if (!opts.vaultProjectId) {
|
|
5912
|
+
console.error(err(" --vault-project-id is required"));
|
|
5913
|
+
process.exit(1);
|
|
5914
|
+
}
|
|
5915
|
+
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
5916
|
+
const limit = parseInt(opts.limit ?? "5", 10);
|
|
5917
|
+
const excludeLinked = opts.excludeLinked !== false;
|
|
5918
|
+
const { zettelSuggest } = await import("../zettelkasten-Co-w0XSZ.mjs");
|
|
5919
|
+
const db = getFedDb();
|
|
5920
|
+
console.log();
|
|
5921
|
+
console.log(header(" PAI Zettel Suggest"));
|
|
5922
|
+
console.log(dim(` Note: ${note}`));
|
|
5923
|
+
process.stdout.write(dim(" Computing suggestions...\n"));
|
|
5924
|
+
const suggestions = await zettelSuggest(db, {
|
|
5925
|
+
notePath: note,
|
|
5926
|
+
vaultProjectId,
|
|
5927
|
+
limit,
|
|
5928
|
+
excludeLinked
|
|
5929
|
+
});
|
|
5930
|
+
if (suggestions.length === 0) {
|
|
5931
|
+
console.log(warn(" No suggestions found. The note may be well-connected already."));
|
|
5932
|
+
console.log();
|
|
5933
|
+
return;
|
|
5934
|
+
}
|
|
5935
|
+
console.log();
|
|
5936
|
+
console.log(bold(` ${suggestions.length} suggested connection(s):`));
|
|
5937
|
+
console.log();
|
|
5938
|
+
for (let i = 0; i < suggestions.length; i++) {
|
|
5939
|
+
const s = suggestions[i];
|
|
5940
|
+
const title = s.title ?? shortPath(s.path);
|
|
5941
|
+
console.log(` ${chalk.green(String(i + 1).padStart(2, " "))}. ${bold(title)}`);
|
|
5942
|
+
console.log(` ${dim("Score:")} ${chalk.green(s.score.toFixed(3))} ${dim("Semantic:")} ${s.semanticScore.toFixed(2)} ${dim("Tag:")} ${s.tagScore.toFixed(2)} ${dim("Neighbor:")} ${s.neighborScore.toFixed(2)}`);
|
|
5943
|
+
console.log(` ${dim("Reason:")} ${s.reason}`);
|
|
5944
|
+
console.log(` ${dim("Wikilink:")} ${chalk.cyan(s.suggestedWikilink)}`);
|
|
5945
|
+
console.log();
|
|
5946
|
+
}
|
|
5947
|
+
}
|
|
5948
|
+
async function cmdConverse(question, opts) {
|
|
5949
|
+
if (!opts.vaultProjectId) {
|
|
5950
|
+
console.error(err(" --vault-project-id is required"));
|
|
5951
|
+
process.exit(1);
|
|
5952
|
+
}
|
|
5953
|
+
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
5954
|
+
const depth = parseInt(opts.depth ?? "2", 10);
|
|
5955
|
+
const limit = parseInt(opts.limit ?? "15", 10);
|
|
5956
|
+
const { zettelConverse } = await import("../zettelkasten-Co-w0XSZ.mjs");
|
|
5957
|
+
const db = getFedDb();
|
|
5958
|
+
console.log();
|
|
5959
|
+
console.log(header(" PAI Zettel Converse"));
|
|
5960
|
+
console.log(dim(` Question: "${question}"`));
|
|
5961
|
+
process.stdout.write(dim(" Searching vault for relevant notes...\n"));
|
|
5962
|
+
const result = await zettelConverse(db, {
|
|
5963
|
+
question,
|
|
5964
|
+
vaultProjectId,
|
|
5965
|
+
depth,
|
|
5966
|
+
limit
|
|
5967
|
+
});
|
|
5968
|
+
if (result.relevantNotes.length === 0) {
|
|
5969
|
+
console.log(warn(" No relevant notes found. Try rephrasing your question."));
|
|
5970
|
+
console.log();
|
|
5971
|
+
return;
|
|
5972
|
+
}
|
|
5973
|
+
console.log();
|
|
5974
|
+
console.log(bold(` ${result.relevantNotes.length} relevant note(s) from ${result.domains.length} domain(s):`));
|
|
5975
|
+
console.log(dim(` Domains: ${result.domains.join(", ")}`));
|
|
5976
|
+
console.log();
|
|
5977
|
+
for (const note of result.relevantNotes) {
|
|
5978
|
+
const title = note.title ?? shortPath(note.path);
|
|
5979
|
+
console.log(` ${chalk.cyan("◆")} ${bold(title)} ${dim(`[${note.domain}] score: ${note.score.toFixed(3)}`)}`);
|
|
5980
|
+
if (note.snippet) console.log(` ${dim(note.snippet.slice(0, 200))}`);
|
|
5981
|
+
console.log();
|
|
5982
|
+
}
|
|
5983
|
+
if (result.connections.length > 0) {
|
|
5984
|
+
console.log(bold(" Cross-domain connections:"));
|
|
5985
|
+
for (const conn of result.connections.slice(0, 10)) console.log(` ${chalk.magenta("⟷")} ${dim(conn.fromDomain)} ${chalk.dim("→")} ${dim(conn.toDomain)} ${dim(shortPath(conn.fromPath))} → ${dim(shortPath(conn.toPath))} ${dim(`strength: ${conn.strength}`)}`);
|
|
5986
|
+
console.log();
|
|
5987
|
+
}
|
|
5988
|
+
console.log(bold(" Synthesis prompt (paste into your AI):"));
|
|
5989
|
+
console.log();
|
|
5990
|
+
const promptLines = result.synthesisPrompt.split("\n");
|
|
5991
|
+
for (const line of promptLines) console.log(` ${dim(line)}`);
|
|
5992
|
+
console.log();
|
|
5993
|
+
}
|
|
5994
|
+
async function cmdThemes(opts) {
|
|
5995
|
+
if (!opts.vaultProjectId) {
|
|
5996
|
+
console.error(err(" --vault-project-id is required"));
|
|
5997
|
+
process.exit(1);
|
|
5998
|
+
}
|
|
5999
|
+
const vaultProjectId = parseInt(opts.vaultProjectId, 10);
|
|
6000
|
+
const lookbackDays = parseInt(opts.days ?? "30", 10);
|
|
6001
|
+
const minClusterSize = parseInt(opts.minSize ?? "3", 10);
|
|
6002
|
+
const maxThemes = parseInt(opts.maxThemes ?? "10", 10);
|
|
6003
|
+
const similarityThreshold = parseFloat(opts.threshold ?? "0.65");
|
|
6004
|
+
const { zettelThemes } = await import("../zettelkasten-Co-w0XSZ.mjs");
|
|
6005
|
+
const db = getFedDb();
|
|
6006
|
+
console.log();
|
|
6007
|
+
console.log(header(" PAI Zettel Themes"));
|
|
6008
|
+
console.log(dim(` Lookback: ${lookbackDays}d Min cluster: ${minClusterSize} Threshold: ${similarityThreshold}`));
|
|
6009
|
+
process.stdout.write(dim(" Detecting emerging themes...\n"));
|
|
6010
|
+
const result = await zettelThemes(db, {
|
|
6011
|
+
vaultProjectId,
|
|
6012
|
+
lookbackDays,
|
|
6013
|
+
minClusterSize,
|
|
6014
|
+
maxThemes,
|
|
6015
|
+
similarityThreshold
|
|
6016
|
+
});
|
|
6017
|
+
if (result.themes.length === 0) {
|
|
6018
|
+
console.log(warn(` No themes detected in the last ${lookbackDays} days. Try --days with a larger window.`));
|
|
6019
|
+
console.log();
|
|
6020
|
+
return;
|
|
6021
|
+
}
|
|
6022
|
+
const fromDate = new Date(result.timeWindow.from).toISOString().slice(0, 10);
|
|
6023
|
+
const toDate = new Date(result.timeWindow.to).toISOString().slice(0, 10);
|
|
6024
|
+
console.log();
|
|
6025
|
+
console.log(bold(` ${result.themes.length} theme(s) from ${result.totalNotesAnalyzed} notes [${fromDate} → ${toDate}]:`));
|
|
6026
|
+
console.log();
|
|
6027
|
+
for (let i = 0; i < result.themes.length; i++) {
|
|
6028
|
+
const cluster = result.themes[i];
|
|
6029
|
+
const diversityBar = Math.round(cluster.folderDiversity * 10);
|
|
6030
|
+
const indexSuggestion = cluster.suggestIndexNote ? chalk.yellow(" ⚑ suggest index note") : "";
|
|
6031
|
+
console.log(` ${chalk.cyan(String(i + 1).padStart(2, " "))}. ${bold(cluster.label)}${indexSuggestion}`);
|
|
6032
|
+
console.log(` ${dim("Notes:")} ${cluster.size} ${dim("Diversity:")} ${"█".repeat(diversityBar)}${"░".repeat(10 - diversityBar)} ${cluster.folderDiversity.toFixed(2)} ${dim("Linked:")} ${Math.round(cluster.linkedRatio * 100)}%`);
|
|
6033
|
+
const preview = cluster.notes.slice(0, 5);
|
|
6034
|
+
for (const note of preview) {
|
|
6035
|
+
const title = note.title ?? shortPath(note.path);
|
|
6036
|
+
console.log(` ${dim("•")} ${title}`);
|
|
6037
|
+
}
|
|
6038
|
+
if (cluster.notes.length > 5) console.log(dim(` ... and ${cluster.notes.length - 5} more`));
|
|
6039
|
+
console.log();
|
|
6040
|
+
}
|
|
6041
|
+
}
|
|
6042
|
+
function registerZettelCommands(parent, _getDb) {
|
|
6043
|
+
parent.command("explore <note>").description("Follow link chains from a starting note").option("--depth <n>", "Maximum traversal depth (1-10)", "3").option("--direction <d>", "Link direction: forward | backward | both", "both").option("--mode <m>", "Edge mode: sequential | associative | all", "all").action(async (note, opts) => {
|
|
6044
|
+
try {
|
|
6045
|
+
await cmdExplore(note, opts);
|
|
6046
|
+
} catch (e) {
|
|
6047
|
+
console.error(err(` Error: ${e}`));
|
|
6048
|
+
process.exit(1);
|
|
6049
|
+
}
|
|
6050
|
+
});
|
|
6051
|
+
parent.command("health").description("Vault structural health audit: dead links, orphans, connectivity").option("--scope <s>", "Scope: full | recent | project", "full").option("--project <path>", "Project path prefix (requires --scope project)").option("--days <n>", "Look-back window in days (requires --scope recent)", "30").option("--include <types>", "Comma-separated subset: dead_links,orphans,disconnected,low_connectivity").action(async (opts) => {
|
|
6052
|
+
try {
|
|
6053
|
+
await cmdHealth(opts);
|
|
6054
|
+
} catch (e) {
|
|
6055
|
+
console.error(err(` Error: ${e}`));
|
|
6056
|
+
process.exit(1);
|
|
6057
|
+
}
|
|
6058
|
+
});
|
|
6059
|
+
parent.command("surprise <note>").description("Find semantically similar but graph-distant notes (surprising connections)").requiredOption("--vault-project-id <n>", "Project ID for the vault in the federation DB").option("--limit <n>", "Maximum results", "10").option("--min-similarity <f>", "Minimum cosine similarity (0–1)", "0.3").option("--min-distance <n>", "Minimum graph distance", "3").action(async (note, opts) => {
|
|
6060
|
+
try {
|
|
6061
|
+
await cmdSurprise(note, opts);
|
|
6062
|
+
} catch (e) {
|
|
6063
|
+
console.error(err(` Error: ${e}`));
|
|
6064
|
+
process.exit(1);
|
|
6065
|
+
}
|
|
6066
|
+
});
|
|
6067
|
+
parent.command("suggest <note>").description("Suggest new wikilink connections for a note").requiredOption("--vault-project-id <n>", "Project ID for the vault in the federation DB").option("--limit <n>", "Maximum suggestions", "5").option("--no-exclude-linked", "Include notes already linked from this one").action(async (note, opts) => {
|
|
6068
|
+
try {
|
|
6069
|
+
await cmdSuggest(note, opts);
|
|
6070
|
+
} catch (e) {
|
|
6071
|
+
console.error(err(` Error: ${e}`));
|
|
6072
|
+
process.exit(1);
|
|
6073
|
+
}
|
|
6074
|
+
});
|
|
6075
|
+
parent.command("converse <question>").description("Ask the vault a question and get a synthesis prompt with relevant notes").requiredOption("--vault-project-id <n>", "Project ID for the vault in the federation DB").option("--depth <n>", "Graph expansion depth around matched notes", "2").option("--limit <n>", "Maximum relevant notes to include", "15").action(async (question, opts) => {
|
|
6076
|
+
try {
|
|
6077
|
+
await cmdConverse(question, opts);
|
|
6078
|
+
} catch (e) {
|
|
6079
|
+
console.error(err(` Error: ${e}`));
|
|
6080
|
+
process.exit(1);
|
|
6081
|
+
}
|
|
6082
|
+
});
|
|
6083
|
+
parent.command("themes").description("Detect emerging theme clusters in recently edited notes").requiredOption("--vault-project-id <n>", "Project ID for the vault in the federation DB").option("--days <n>", "Look-back window in days", "30").option("--min-size <n>", "Minimum notes per cluster", "3").option("--max-themes <n>", "Maximum themes to return", "10").option("--threshold <f>", "Similarity threshold for clustering (0–1)", "0.65").action(async (opts) => {
|
|
6084
|
+
try {
|
|
6085
|
+
await cmdThemes(opts);
|
|
6086
|
+
} catch (e) {
|
|
6087
|
+
console.error(err(` Error: ${e}`));
|
|
6088
|
+
process.exit(1);
|
|
6089
|
+
}
|
|
6090
|
+
});
|
|
6091
|
+
}
|
|
6092
|
+
|
|
5299
6093
|
//#endregion
|
|
5300
6094
|
//#region src/cli/commands/update.ts
|
|
5301
6095
|
function line(text = "") {
|
|
@@ -5906,6 +6700,7 @@ registerUpdateCommand(program);
|
|
|
5906
6700
|
registerNotifyCommands(program.command("notify").description("Notification config: status, get, set, test, send"));
|
|
5907
6701
|
registerTopicCommands(program.command("topic").description("Topic shift detection: check whether context has drifted to a different project"));
|
|
5908
6702
|
registerObsidianCommands(program.command("obsidian").description("Obsidian vault: sync project notes, view status, open in Obsidian"), getDb);
|
|
6703
|
+
registerZettelCommands(program.command("zettel").description("Zettelkasten intelligence: explore, surprise, converse, themes, health, suggest"), getDb);
|
|
5909
6704
|
program.command("go <query>").description("Jump to a project directory by slug or partial name.\nPrints the root path to stdout — use with: cd $(pai go <query>)\nExample shell function in ~/.zshrc:\n pcd() { cd \"$(pai go \"$@\")\" }").action((query) => {
|
|
5910
6705
|
cmdGo(getDb(), query);
|
|
5911
6706
|
});
|