@tekmidian/pai 0.9.9 → 0.9.11
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 +73 -1
- package/README.md +54 -2
- package/dist/{auto-route-CruBrTf-.mjs → auto-route-DL4ViDKC.mjs} +2 -2
- package/dist/{auto-route-CruBrTf-.mjs.map → auto-route-DL4ViDKC.mjs.map} +1 -1
- package/dist/cli/index.mjs +250 -16
- package/dist/cli/index.mjs.map +1 -1
- package/dist/daemon/index.mjs +7 -6
- package/dist/daemon/index.mjs.map +1 -1
- package/dist/{daemon-WQY1kwOF.mjs → daemon-dYymOnwd.mjs} +39 -19
- package/dist/daemon-dYymOnwd.mjs.map +1 -0
- package/dist/daemon-mcp/index.mjs +46 -0
- package/dist/daemon-mcp/index.mjs.map +1 -1
- package/dist/{db-DdUperSl.mjs → db-CYmBWcjh.mjs} +35 -13
- package/dist/db-CYmBWcjh.mjs.map +1 -0
- package/dist/{detector-CNU3zCwP.mjs → detector-AzVtGLtL.mjs} +2 -2
- package/dist/{detector-CNU3zCwP.mjs.map → detector-AzVtGLtL.mjs.map} +1 -1
- package/dist/{factory-DKDPRhAN.mjs → factory-BDBIfTDC.mjs} +3 -3
- package/dist/{factory-DKDPRhAN.mjs.map → factory-BDBIfTDC.mjs.map} +1 -1
- package/dist/hooks/session-commands.mjs +17 -0
- package/dist/hooks/session-commands.mjs.map +7 -0
- package/dist/hooks/whisper-rules.mjs +12 -11
- package/dist/hooks/whisper-rules.mjs.map +2 -2
- package/dist/index.d.mts +3 -1
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +2 -2
- package/dist/kg-entity-D5v7RCDi.mjs +176 -0
- package/dist/kg-entity-D5v7RCDi.mjs.map +1 -0
- package/dist/{kg-extraction-BlGM40q7.mjs → kg-extraction-Uvr9fTlz.mjs} +56 -21
- package/dist/kg-extraction-Uvr9fTlz.mjs.map +1 -0
- package/dist/{search-DC1qhkKn.mjs → search-i2nlQ-JM.mjs} +48 -19
- package/dist/search-i2nlQ-JM.mjs.map +1 -0
- package/dist/{sqlite-BJrME_vg.mjs → sqlite-xUe94oUZ.mjs} +9 -2
- package/dist/sqlite-xUe94oUZ.mjs.map +1 -0
- package/dist/{tools-gMHdjmHC.mjs → tools-BNBvJNph.mjs} +285 -7
- package/dist/tools-BNBvJNph.mjs.map +1 -0
- package/package.json +1 -1
- package/src/hooks/ts/session-start/session-commands.ts +26 -0
- package/src/hooks/ts/user-prompt/whisper-rules.ts +12 -11
- package/dist/daemon-WQY1kwOF.mjs.map +0 -1
- package/dist/db-DdUperSl.mjs.map +0 -1
- package/dist/kg-B5ysyRLC.mjs +0 -94
- package/dist/kg-B5ysyRLC.mjs.map +0 -1
- package/dist/kg-extraction-BlGM40q7.mjs.map +0 -1
- package/dist/search-DC1qhkKn.mjs.map +0 -1
- package/dist/sqlite-BJrME_vg.mjs.map +0 -1
- package/dist/tools-gMHdjmHC.mjs.map +0 -1
package/ARCHITECTURE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PAI Knowledge OS — Architecture (v0.
|
|
1
|
+
# PAI Knowledge OS — Architecture (v0.9.10)
|
|
2
2
|
|
|
3
3
|
Technical reference for PAI's architecture, database schema, CLI commands, and development setup.
|
|
4
4
|
|
|
@@ -1047,6 +1047,78 @@ These tables are populated by the PostToolUse hook classifier and queried by the
|
|
|
1047
1047
|
|
|
1048
1048
|
**Indexes:** B-tree on project_id, session_id, type, created_at DESC, content_hash.
|
|
1049
1049
|
|
|
1050
|
+
### Knowledge Graph Entity Tables (PostgreSQL)
|
|
1051
|
+
|
|
1052
|
+
Introduced in v0.9.10 to support entity deduplication and graph-completion search.
|
|
1053
|
+
|
|
1054
|
+
**`kg_entities`** — Deduplicated named entities with content-address hashing:
|
|
1055
|
+
|
|
1056
|
+
| Column | Type | Description |
|
|
1057
|
+
|--------|------|-------------|
|
|
1058
|
+
| `id` | SERIAL | Primary key |
|
|
1059
|
+
| `project_id` | INTEGER | Owning project (FK → registry projects) |
|
|
1060
|
+
| `name` | TEXT | Canonical entity name (normalized) |
|
|
1061
|
+
| `type` | TEXT | Entity type: `person`, `project`, `library`, `concept`, `place`, `other` |
|
|
1062
|
+
| `content_hash` | TEXT | SHA-256 of `lower(trim(name))` — identity key for deduplication |
|
|
1063
|
+
| `last_accessed_at` | TIMESTAMPTZ | Timestamp of last `memory_get` that touched this entity |
|
|
1064
|
+
| `created_at` | TIMESTAMPTZ | First observation timestamp |
|
|
1065
|
+
|
|
1066
|
+
The `content_hash` column carries a unique constraint per `project_id`. When two indexing passes encounter the same entity name, they resolve to the same row — no duplicates accumulate.
|
|
1067
|
+
|
|
1068
|
+
**`kg_entity_chunks`** — Join table linking entities to the chunks they appear in:
|
|
1069
|
+
|
|
1070
|
+
| Column | Type | Description |
|
|
1071
|
+
|--------|------|-------------|
|
|
1072
|
+
| `entity_id` | INTEGER | FK → kg_entities.id |
|
|
1073
|
+
| `chunk_id` | TEXT | FK → pai_chunks.id (SHA-256) |
|
|
1074
|
+
|
|
1075
|
+
**Indexes:** Unique on `(project_id, content_hash)`, B-tree on entity_id, chunk_id.
|
|
1076
|
+
|
|
1077
|
+
---
|
|
1078
|
+
|
|
1079
|
+
### Graph-Completion Search Pipeline
|
|
1080
|
+
|
|
1081
|
+
`graphCompletionSearch` in `src/memory/graph-search.ts` implements the four-stage pipeline:
|
|
1082
|
+
|
|
1083
|
+
1. **Vector seeds** — calls the standard `memory_search` with `mode: "semantic"` to retrieve top-K chunks by embedding similarity.
|
|
1084
|
+
2. **Entity resolution** — for each seed chunk, looks up associated entities via `kg_entity_chunks`.
|
|
1085
|
+
3. **Graph traversal** — fetches one-hop neighbors from `kg_triples` for each resolved entity; collects their associated chunk IDs via `kg_entity_chunks`.
|
|
1086
|
+
4. **Re-rank** — merges seed chunks with neighbor-expanded candidates, deduplicates, and scores the full set with the cross-encoder. Returns results sorted by final cross-encoder score.
|
|
1087
|
+
|
|
1088
|
+
The pipeline is invoked when the `memory_search` MCP tool receives `mode: "graph"`. It falls back to hybrid search in SQLite mode (no graph available).
|
|
1089
|
+
|
|
1090
|
+
---
|
|
1091
|
+
|
|
1092
|
+
### Feedback Weight EMA System
|
|
1093
|
+
|
|
1094
|
+
`src/memory/feedback.ts` maintains a `search_feedback` table:
|
|
1095
|
+
|
|
1096
|
+
| Column | Type | Description |
|
|
1097
|
+
|--------|------|-------------|
|
|
1098
|
+
| `chunk_id` | TEXT | FK → pai_chunks.id |
|
|
1099
|
+
| `project_id` | INTEGER | Owning project |
|
|
1100
|
+
| `weight` | REAL | Current EMA weight (default 1.0) |
|
|
1101
|
+
| `signal_count` | INTEGER | Total positive signals received |
|
|
1102
|
+
| `updated_at` | TIMESTAMPTZ | Last signal timestamp |
|
|
1103
|
+
|
|
1104
|
+
When `memory_get` is called for a chunk, a positive signal is recorded and the weight updated:
|
|
1105
|
+
|
|
1106
|
+
```
|
|
1107
|
+
new_weight = alpha * 1.0 + (1 - alpha) * old_weight (alpha = 0.1 by default)
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
The `memory_search` result scorer multiplies each chunk's base score by its feedback weight before final ranking. A chunk accessed 10 times reaches approximately 1.65x its baseline score; one accessed 50 times approaches 2.0x.
|
|
1111
|
+
|
|
1112
|
+
Access timestamps are written to both `kg_entities.last_accessed_at` (for entity rows) and `pai_chunks.last_accessed_at` (for chunk rows) on every `memory_get` call. This supports the recency boost calculation and enables future LRU eviction for very large knowledge bases.
|
|
1113
|
+
|
|
1114
|
+
---
|
|
1115
|
+
|
|
1116
|
+
### Multi-Tenant Isolation
|
|
1117
|
+
|
|
1118
|
+
Every memory table (`pai_chunks`, `pai_files`, `kg_entities`, `kg_entity_chunks`, `kg_triples`, `pai_observations`, `pai_session_summaries`, `search_feedback`) carries a `project_id` column. All queries in the storage backend append `WHERE project_id = $1` when `all_projects` is false (the default).
|
|
1119
|
+
|
|
1120
|
+
Cross-project visibility is opt-in: pass `all_projects: true` to `memory_search`, or use `--all` with the CLI. The `memory_tunnels` MCP tool explicitly queries across projects to surface cross-project concept bridges — this is its intended purpose, not a leak.
|
|
1121
|
+
|
|
1050
1122
|
**Content Tiers:**
|
|
1051
1123
|
|
|
1052
1124
|
| Tier | Description | Example |
|
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# PAI Knowledge OS — v0.9.
|
|
1
|
+
# PAI Knowledge OS — v0.9.10
|
|
2
2
|
|
|
3
3
|
Claude Code has a memory problem. Every new session starts cold — no idea what you built yesterday, what decisions you made, or where you left off. PAI fixes this.
|
|
4
4
|
|
|
@@ -393,6 +393,53 @@ This reveals unexpected intellectual bridges: the same concurrency pattern used
|
|
|
393
393
|
|
|
394
394
|
---
|
|
395
395
|
|
|
396
|
+
## Memory Architecture
|
|
397
|
+
|
|
398
|
+
PAI's memory system uses a three-tier hybrid store inspired by Cognee's approach to knowledge graphs and retrieval. Each tier has a distinct role, and they work together to answer queries that no single store could handle alone.
|
|
399
|
+
|
|
400
|
+
### Three-Tier Hybrid Store
|
|
401
|
+
|
|
402
|
+
| Tier | Backend | What it stores |
|
|
403
|
+
|------|---------|----------------|
|
|
404
|
+
| **Chunks + entities** | SQLite (simple mode) or PostgreSQL (full mode) | Text chunks with embeddings; named entity records with content-address hashes |
|
|
405
|
+
| **Knowledge graph** | PostgreSQL (`kg_triples`) | Subject-predicate-object triples with `valid_from`/`valid_to` timestamps |
|
|
406
|
+
| **Vector embeddings** | pgvector (full mode) | 768-dimensional Snowflake Arctic embeddings on chunks and vault notes |
|
|
407
|
+
|
|
408
|
+
### Entity Deduplication via Content-Address Hashing
|
|
409
|
+
|
|
410
|
+
Named entities (people, projects, libraries, concepts) extracted during indexing are stored in a `kg_entities` table and deduplicated using a content-address hash derived from the entity's canonical name. Two mentions of "PostgreSQL" in different session notes resolve to a single entity row — the hash acts as a stable identity, so the graph stays normalized even as new content is indexed.
|
|
411
|
+
|
|
412
|
+
### Graph-Completion Search Pipeline
|
|
413
|
+
|
|
414
|
+
Standard vector search finds semantically similar chunks. Graph-completion search goes further:
|
|
415
|
+
|
|
416
|
+
1. **Vector seeds** — a semantic search returns the top-K most relevant chunks.
|
|
417
|
+
2. **Graph traversal** — the entities mentioned in those chunks are looked up in `kg_triples`; their immediate neighbors are fetched (one hop).
|
|
418
|
+
3. **Candidate expansion** — the neighbor entities' associated chunks are added to the result set.
|
|
419
|
+
4. **Re-rank** — the expanded candidate set is re-scored by the cross-encoder, which reads each (query, result) pair together. Results are sorted by this final relevance score.
|
|
420
|
+
|
|
421
|
+
This means a query about "the PAI daemon" can surface a session note that mentions the daemon only indirectly — because a connected entity (the Unix socket, the launchd service) appears in both the graph and the note.
|
|
422
|
+
|
|
423
|
+
### Feedback Loop with Relevance Scoring
|
|
424
|
+
|
|
425
|
+
Every search result that is subsequently retrieved via `memory_get` (i.e., actually read by the model) generates a positive feedback signal. These signals are stored and used to adjust future search weights using an exponential moving average (EMA):
|
|
426
|
+
|
|
427
|
+
```
|
|
428
|
+
new_weight = alpha * signal + (1 - alpha) * old_weight
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
The default alpha is 0.1, so recent positive signals gradually raise a chunk's effective score without overriding the semantic baseline. This creates a personalization loop: content you actually use rises in future rankings; content you skip does not.
|
|
432
|
+
|
|
433
|
+
### Access Timestamp Tracking
|
|
434
|
+
|
|
435
|
+
Every chunk row carries a `last_accessed_at` timestamp updated on each `memory_get` call. This supports recency boost (content accessed recently scores higher) and enables future eviction policies for very large knowledge bases.
|
|
436
|
+
|
|
437
|
+
### Multi-Tenant Support
|
|
438
|
+
|
|
439
|
+
PAI isolates memory by project. Every chunk, entity, and observation row carries a `project_id` foreign key. Searches default to the current project; the `all_projects: true` flag (or `--all` CLI option) lifts the filter. Knowledge-graph triples carry a `project_id` as well, so cross-project tunnels (`memory_tunnels`) are detected explicitly rather than accidentally.
|
|
440
|
+
|
|
441
|
+
---
|
|
442
|
+
|
|
396
443
|
## Automatic Observation Capture
|
|
397
444
|
|
|
398
445
|
PAI automatically classifies and stores every significant tool call during your sessions. When you edit a file, run a command, or make a decision, PAI captures it as a structured observation — building a searchable timeline of everything you've done across all projects.
|
|
@@ -754,7 +801,7 @@ External URLs (`https://`, `mailto:`, etc.) are excluded — only relative paths
|
|
|
754
801
|
|
|
755
802
|
## Release History
|
|
756
803
|
|
|
757
|
-
|
|
804
|
+
21 releases shipped from v0.7.2 to v0.9.10 (March 19 – April 13, 2026):
|
|
758
805
|
|
|
759
806
|
| Version | Feature |
|
|
760
807
|
|---------|---------|
|
|
@@ -779,6 +826,9 @@ External URLs (`https://`, `mailto:`, etc.) are excluded — only relative paths
|
|
|
779
826
|
| v0.9.5 | Budget-aware advisor mode |
|
|
780
827
|
| v0.9.6 | Statusline auto-writes budget to advisor |
|
|
781
828
|
| v0.9.7 | Advisor mode label in statusline, natural language mode switching |
|
|
829
|
+
| v0.9.8 | Privacy tags, compact search format, npx install |
|
|
830
|
+
| v0.9.9 | Fix advisor mode to delegate to haiku instead of hoarding in opus |
|
|
831
|
+
| v0.9.10 | Cognee-inspired three-tier memory: entity deduplication, graph-completion search, feedback EMA |
|
|
782
832
|
|
|
783
833
|
---
|
|
784
834
|
|
|
@@ -800,6 +850,8 @@ PAI Knowledge OS is inspired by [Daniel Miessler](https://github.com/danielmiess
|
|
|
800
850
|
|
|
801
851
|
The automatic observation capture system — classifying tool calls into structured observations with progressive context injection — is inspired by [claude-mem](https://github.com/thedotmack/claude-mem) by [thedotmack](https://github.com/thedotmack). claude-mem demonstrated that automatic memory capture during Claude Code sessions dramatically improves continuity. PAI adapts this concept with a rule-based classifier, PostgreSQL storage, and three-layer progressive disclosure.
|
|
802
852
|
|
|
853
|
+
The three-store hybrid memory architecture — combining SQLite/PostgreSQL chunks with a knowledge graph and vector embeddings, graph-completion search (vector seeds → graph traversal → re-rank), and the feedback EMA relevance loop — is inspired by [Cognee](https://github.com/topoteretes/cognee) by [topoteretes](https://github.com/topoteretes). Cognee showed that unifying structured knowledge graphs with unstructured vector retrieval produces dramatically better recall. PAI adapts this pattern to the personal knowledge OS context with project-scoped multi-tenancy and content-address entity deduplication.
|
|
854
|
+
|
|
803
855
|
---
|
|
804
856
|
|
|
805
857
|
## License
|
|
@@ -26,7 +26,7 @@ async function autoRoute(registryDb, federation, cwd, context) {
|
|
|
26
26
|
const markerResult = findMarkerUpward(registryDb, target);
|
|
27
27
|
if (markerResult) return markerResult;
|
|
28
28
|
if (context && context.trim().length > 0) {
|
|
29
|
-
const { detectTopicShift } = await import("./detector-
|
|
29
|
+
const { detectTopicShift } = await import("./detector-AzVtGLtL.mjs").then((n) => n.n);
|
|
30
30
|
const topicResult = await detectTopicShift(registryDb, federation, {
|
|
31
31
|
context,
|
|
32
32
|
threshold: .5
|
|
@@ -83,4 +83,4 @@ function formatAutoRouteJson(result) {
|
|
|
83
83
|
|
|
84
84
|
//#endregion
|
|
85
85
|
export { autoRoute, formatAutoRouteJson };
|
|
86
|
-
//# sourceMappingURL=auto-route-
|
|
86
|
+
//# sourceMappingURL=auto-route-DL4ViDKC.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto-route-
|
|
1
|
+
{"version":3,"file":"auto-route-DL4ViDKC.mjs","names":[],"sources":["../src/session/auto-route.ts"],"sourcesContent":["/**\n * Auto-route: automatic project routing suggestion on session start.\n *\n * Given a working directory (and optional conversation context), determine\n * which registered project the session belongs to.\n *\n * Strategy (in priority order):\n * 1. Path match — exact or parent-directory match in the project registry\n * 2. Marker walk — walk up from cwd looking for Notes/PAI.md, resolve slug\n * 3. Topic match — BM25 keyword search against memory (requires context text)\n *\n * The function is stateless and works with direct DB access (no daemon\n * required), making it fast and safe to call during session startup.\n */\n\nimport type { Database } from \"better-sqlite3\";\nimport type { StorageBackend } from \"../storage/interface.js\";\nimport { resolve, dirname } from \"node:path\";\nimport { existsSync } from \"node:fs\";\nimport { readPaiMarker } from \"../registry/pai-marker.js\";\nimport { detectProject } from \"../cli/commands/detect.js\";\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\nexport type AutoRouteMethod = \"path\" | \"marker\" | \"topic\";\n\nexport interface AutoRouteResult {\n /** Project slug */\n slug: string;\n /** Human-readable project name */\n display_name: string;\n /** Absolute path to the project root */\n root_path: string;\n /** How the project was detected */\n method: AutoRouteMethod;\n /** Confidence [0,1]: 1.0 for path/marker matches, BM25 fraction for topic */\n confidence: number;\n}\n\n// ---------------------------------------------------------------------------\n// Core function\n// ---------------------------------------------------------------------------\n\n/**\n * Determine which project a session should be routed to.\n *\n * @param registryDb Open PAI registry database\n * @param federation Memory storage backend (needed only for topic fallback)\n * @param cwd Working directory to detect from (defaults to process.cwd())\n * @param context Optional conversation text for topic-based fallback\n * @returns Best project match, or null if nothing matched\n */\nexport async function autoRoute(\n registryDb: Database,\n federation: Database | StorageBackend,\n cwd?: string,\n context?: string\n): Promise<AutoRouteResult | null> {\n const target = resolve(cwd ?? process.cwd());\n\n // -------------------------------------------------------------------------\n // Strategy 1: Path match via registry\n // -------------------------------------------------------------------------\n\n const pathMatch = detectProject(registryDb, target);\n\n if (pathMatch) {\n return {\n slug: pathMatch.slug,\n display_name: pathMatch.display_name,\n root_path: pathMatch.root_path,\n method: \"path\",\n confidence: 1.0,\n };\n }\n\n // -------------------------------------------------------------------------\n // Strategy 2: PAI.md marker file walk\n //\n // Walk up from cwd, checking <dir>/Notes/PAI.md at each level.\n // Once found, resolve the slug against the registry to get full project info.\n // -------------------------------------------------------------------------\n\n const markerResult = findMarkerUpward(registryDb, target);\n if (markerResult) {\n return markerResult;\n }\n\n // -------------------------------------------------------------------------\n // Strategy 3: Topic detection (requires context text)\n // -------------------------------------------------------------------------\n\n if (context && context.trim().length > 0) {\n // Lazy import to avoid bundler pulling in daemon/index.mjs at module load time\n const { detectTopicShift } = await import(\"../topics/detector.js\");\n const topicResult = await detectTopicShift(registryDb, federation, {\n context,\n threshold: 0.5, // Lower threshold for initial routing (vs shift detection)\n });\n\n if (topicResult.suggestedProject && topicResult.confidence > 0) {\n // Look up the full project info from the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(topicResult.suggestedProject) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"topic\",\n confidence: topicResult.confidence,\n };\n }\n }\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Marker walk helper\n// ---------------------------------------------------------------------------\n\n/**\n * Walk up the directory tree from `startDir`, checking each level for a\n * `Notes/PAI.md` file. If found, read the slug and look up the project.\n *\n * Stops at the filesystem root or after 20 levels (safety guard).\n */\nfunction findMarkerUpward(\n registryDb: Database,\n startDir: string\n): AutoRouteResult | null {\n let current = startDir;\n let depth = 0;\n\n while (depth < 20) {\n const markerPath = `${current}/Notes/PAI.md`;\n\n if (existsSync(markerPath)) {\n const marker = readPaiMarker(current);\n\n if (marker && marker.status !== \"archived\") {\n // Resolve slug to full project info in the registry\n const projectRow = registryDb\n .prepare(\n \"SELECT slug, display_name, root_path FROM projects WHERE slug = ? AND status != 'archived'\"\n )\n .get(marker.slug) as\n | { slug: string; display_name: string; root_path: string }\n | undefined;\n\n if (projectRow) {\n return {\n slug: projectRow.slug,\n display_name: projectRow.display_name,\n root_path: projectRow.root_path,\n method: \"marker\",\n confidence: 1.0,\n };\n }\n }\n }\n\n const parent = dirname(current);\n if (parent === current) break; // Reached filesystem root\n current = parent;\n depth++;\n }\n\n return null;\n}\n\n// ---------------------------------------------------------------------------\n// Format helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Format an AutoRouteResult as a human-readable string for CLI output.\n */\nexport function formatAutoRoute(result: AutoRouteResult): string {\n const lines: string[] = [\n `slug: ${result.slug}`,\n `display_name: ${result.display_name}`,\n `root_path: ${result.root_path}`,\n `method: ${result.method}`,\n `confidence: ${(result.confidence * 100).toFixed(0)}%`,\n ];\n return lines.join(\"\\n\");\n}\n\n/**\n * Format an AutoRouteResult as JSON for machine consumption.\n */\nexport function formatAutoRouteJson(result: AutoRouteResult): string {\n return JSON.stringify(result, null, 2);\n}\n"],"mappings":";;;;;;;;;;;;;;;AAsDA,eAAsB,UACpB,YACA,YACA,KACA,SACiC;CACjC,MAAM,SAAS,QAAQ,OAAO,QAAQ,KAAK,CAAC;CAM5C,MAAM,YAAY,cAAc,YAAY,OAAO;AAEnD,KAAI,UACF,QAAO;EACL,MAAM,UAAU;EAChB,cAAc,UAAU;EACxB,WAAW,UAAU;EACrB,QAAQ;EACR,YAAY;EACb;CAUH,MAAM,eAAe,iBAAiB,YAAY,OAAO;AACzD,KAAI,aACF,QAAO;AAOT,KAAI,WAAW,QAAQ,MAAM,CAAC,SAAS,GAAG;EAExC,MAAM,EAAE,qBAAqB,MAAM,OAAO;EAC1C,MAAM,cAAc,MAAM,iBAAiB,YAAY,YAAY;GACjE;GACA,WAAW;GACZ,CAAC;AAEF,MAAI,YAAY,oBAAoB,YAAY,aAAa,GAAG;GAE9D,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,YAAY,iBAAiB;AAIpC,OAAI,WACF,QAAO;IACL,MAAM,WAAW;IACjB,cAAc,WAAW;IACzB,WAAW,WAAW;IACtB,QAAQ;IACR,YAAY,YAAY;IACzB;;;AAKP,QAAO;;;;;;;;AAaT,SAAS,iBACP,YACA,UACwB;CACxB,IAAI,UAAU;CACd,IAAI,QAAQ;AAEZ,QAAO,QAAQ,IAAI;AAGjB,MAAI,WAFe,GAAG,QAAQ,eAEJ,EAAE;GAC1B,MAAM,SAAS,cAAc,QAAQ;AAErC,OAAI,UAAU,OAAO,WAAW,YAAY;IAE1C,MAAM,aAAa,WAChB,QACC,6FACD,CACA,IAAI,OAAO,KAAK;AAInB,QAAI,WACF,QAAO;KACL,MAAM,WAAW;KACjB,cAAc,WAAW;KACzB,WAAW,WAAW;KACtB,QAAQ;KACR,YAAY;KACb;;;EAKP,MAAM,SAAS,QAAQ,QAAQ;AAC/B,MAAI,WAAW,QAAS;AACxB,YAAU;AACV;;AAGF,QAAO;;;;;AAwBT,SAAgB,oBAAoB,QAAiC;AACnE,QAAO,KAAK,UAAU,QAAQ,MAAM,EAAE"}
|
package/dist/cli/index.mjs
CHANGED
|
@@ -3,18 +3,18 @@ import { n as openRegistry } from "../db-BtuN768f.mjs";
|
|
|
3
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
4
|
import { a as slugify$1, i as parseSessionFilename, n as decodeEncodedDir, t as buildEncodedDirMap } from "../migrate-jokLenje.mjs";
|
|
5
5
|
import { n as ensurePaiMarker, t as discoverPaiMarkers } from "../pai-marker-CXQPX2P6.mjs";
|
|
6
|
-
import { n as openFederation } from "../db-
|
|
6
|
+
import { n as openFederation } from "../db-CYmBWcjh.mjs";
|
|
7
7
|
import "../helpers-OCVFgprQ.mjs";
|
|
8
8
|
import { i as indexProject, n as indexAll, t as embedChunks } from "../sync-CdHSL9Kc.mjs";
|
|
9
9
|
import "../embeddings-DGRAPAYb.mjs";
|
|
10
10
|
import { t as STOP_WORDS } from "../stop-words-BaMEGVeY.mjs";
|
|
11
|
-
import { n as populateSlugs, r as searchMemory } from "../search-
|
|
11
|
+
import { n as populateSlugs, r as searchMemory } from "../search-i2nlQ-JM.mjs";
|
|
12
12
|
import { n as formatDetection, r as formatDetectionJson, t as detectProject } from "../detect-CdaA48EI.mjs";
|
|
13
|
-
import { t as extractAndStoreTriples } from "../kg-extraction-
|
|
13
|
+
import { t as extractAndStoreTriples } from "../kg-extraction-Uvr9fTlz.mjs";
|
|
14
14
|
import { t as PaiClient } from "../ipc-client-CoyUHPod.mjs";
|
|
15
15
|
import { a as expandHome, i as ensureConfigDir, n as CONFIG_FILE$2, o as loadConfig, t as CONFIG_DIR } from "../config-BuhHWyOK.mjs";
|
|
16
|
-
import { t as createStorageBackend } from "../factory-
|
|
17
|
-
import {
|
|
16
|
+
import { t as createStorageBackend } from "../factory-BDBIfTDC.mjs";
|
|
17
|
+
import { s as kgQuery } from "../kg-entity-D5v7RCDi.mjs";
|
|
18
18
|
import { appendFileSync, chmodSync, copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, renameSync, statSync, symlinkSync, unlinkSync, writeFileSync } from "node:fs";
|
|
19
19
|
import { homedir, platform, tmpdir } from "node:os";
|
|
20
20
|
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
@@ -1682,9 +1682,9 @@ function cmdActive(db, opts) {
|
|
|
1682
1682
|
], rows));
|
|
1683
1683
|
}
|
|
1684
1684
|
async function cmdAutoRoute(opts) {
|
|
1685
|
-
const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-
|
|
1685
|
+
const { autoRoute, formatAutoRoute, formatAutoRouteJson } = await import("../auto-route-DL4ViDKC.mjs");
|
|
1686
1686
|
const { openRegistry } = await import("../db-BtuN768f.mjs").then((n) => n.t);
|
|
1687
|
-
const { createStorageBackend } = await import("../factory-
|
|
1687
|
+
const { createStorageBackend } = await import("../factory-BDBIfTDC.mjs").then((n) => n.n);
|
|
1688
1688
|
const { loadConfig } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
|
|
1689
1689
|
const config = loadConfig();
|
|
1690
1690
|
const registryDb = openRegistry();
|
|
@@ -3245,7 +3245,7 @@ function registerSearchCommand(memoryCmd, getDb) {
|
|
|
3245
3245
|
}
|
|
3246
3246
|
const recencyDays = parseInt(opts.recency ?? String(searchConfig.recencyBoostDays), 10);
|
|
3247
3247
|
if (recencyDays > 0) {
|
|
3248
|
-
const { applyRecencyBoost } = await import("../search-
|
|
3248
|
+
const { applyRecencyBoost } = await import("../search-i2nlQ-JM.mjs").then((n) => n.o);
|
|
3249
3249
|
console.log(dim(`Applying recency boost (half-life: ${recencyDays} days)...`));
|
|
3250
3250
|
results = applyRecencyBoost(results, recencyDays);
|
|
3251
3251
|
}
|
|
@@ -3794,7 +3794,7 @@ function cmdLogs(opts) {
|
|
|
3794
3794
|
}
|
|
3795
3795
|
function registerDaemonCommands(daemonCmd) {
|
|
3796
3796
|
daemonCmd.command("serve").description("Start the PAI daemon in the foreground").action(async () => {
|
|
3797
|
-
const { serve } = await import("../daemon-
|
|
3797
|
+
const { serve } = await import("../daemon-dYymOnwd.mjs").then((n) => n.t);
|
|
3798
3798
|
const { loadConfig: lc, ensureConfigDir } = await import("../config-BuhHWyOK.mjs").then((n) => n.r);
|
|
3799
3799
|
ensureConfigDir();
|
|
3800
3800
|
await serve(lc());
|
|
@@ -5151,6 +5151,10 @@ async function stepSettings(rl, daName) {
|
|
|
5151
5151
|
DA: daName
|
|
5152
5152
|
},
|
|
5153
5153
|
hooks: [
|
|
5154
|
+
{
|
|
5155
|
+
hookType: "SessionStart",
|
|
5156
|
+
command: "${PAI_DIR}/Hooks/session-commands.mjs"
|
|
5157
|
+
},
|
|
5154
5158
|
{
|
|
5155
5159
|
hookType: "SessionStart",
|
|
5156
5160
|
command: "${PAI_DIR}/Hooks/load-core-context.mjs"
|
|
@@ -7659,6 +7663,7 @@ async function backfillKgFromNotes(options = {}) {
|
|
|
7659
7663
|
const backend = await createStorageBackend(config);
|
|
7660
7664
|
if (backend.backendType !== "postgres") throw new Error("Postgres backend unavailable — fell back to SQLite. Cannot backfill KG.");
|
|
7661
7665
|
const pool = backend.getPool();
|
|
7666
|
+
const federationDb = openFederation();
|
|
7662
7667
|
const registry = openRegistry();
|
|
7663
7668
|
let projects;
|
|
7664
7669
|
try {
|
|
@@ -7705,7 +7710,8 @@ async function backfillKgFromNotes(options = {}) {
|
|
|
7705
7710
|
projectId: project.id,
|
|
7706
7711
|
sessionId: `backfill:${notePath}`,
|
|
7707
7712
|
gitLog: "",
|
|
7708
|
-
model: "sonnet"
|
|
7713
|
+
model: "sonnet",
|
|
7714
|
+
federationDb
|
|
7709
7715
|
});
|
|
7710
7716
|
result.notes_processed++;
|
|
7711
7717
|
result.triples_extracted += stats.extracted;
|
|
@@ -7719,13 +7725,14 @@ async function backfillKgFromNotes(options = {}) {
|
|
|
7719
7725
|
}
|
|
7720
7726
|
}
|
|
7721
7727
|
if (!options.dryRun) saveState(state);
|
|
7728
|
+
federationDb.close();
|
|
7722
7729
|
await backend.close();
|
|
7723
7730
|
return result;
|
|
7724
7731
|
}
|
|
7725
7732
|
|
|
7726
7733
|
//#endregion
|
|
7727
7734
|
//#region src/cli/commands/kg.ts
|
|
7728
|
-
async function getPool() {
|
|
7735
|
+
async function getPool$1() {
|
|
7729
7736
|
const config = loadConfig();
|
|
7730
7737
|
if (config.storageBackend !== "postgres") {
|
|
7731
7738
|
console.error(err(" KG commands require Postgres backend."));
|
|
@@ -7782,8 +7789,8 @@ async function cmdBackfill(opts) {
|
|
|
7782
7789
|
process.exit(1);
|
|
7783
7790
|
}
|
|
7784
7791
|
}
|
|
7785
|
-
async function cmdQuery(opts) {
|
|
7786
|
-
const { pool, close } = await getPool();
|
|
7792
|
+
async function cmdQuery$1(opts) {
|
|
7793
|
+
const { pool, close } = await getPool$1();
|
|
7787
7794
|
try {
|
|
7788
7795
|
let projectId;
|
|
7789
7796
|
if (opts.project) {
|
|
@@ -7821,7 +7828,7 @@ async function cmdQuery(opts) {
|
|
|
7821
7828
|
}
|
|
7822
7829
|
async function cmdList(opts) {
|
|
7823
7830
|
const limit = opts.limit ? parseInt(opts.limit, 10) : 50;
|
|
7824
|
-
const { pool, close } = await getPool();
|
|
7831
|
+
const { pool, close } = await getPool$1();
|
|
7825
7832
|
try {
|
|
7826
7833
|
let projectId;
|
|
7827
7834
|
if (opts.project) {
|
|
@@ -7844,7 +7851,7 @@ async function cmdList(opts) {
|
|
|
7844
7851
|
}
|
|
7845
7852
|
}
|
|
7846
7853
|
async function cmdStats() {
|
|
7847
|
-
const { pool, close } = await getPool();
|
|
7854
|
+
const { pool, close } = await getPool$1();
|
|
7848
7855
|
try {
|
|
7849
7856
|
const totals = await pool.query(`SELECT
|
|
7850
7857
|
COUNT(*)::text AS total,
|
|
@@ -7880,7 +7887,7 @@ function registerKgCommands(kgCmd) {
|
|
|
7880
7887
|
await cmdBackfill(opts);
|
|
7881
7888
|
});
|
|
7882
7889
|
kgCmd.command("query").description("Query KG triples by subject, predicate, object, time, or project").option("--subject <s>", "Filter by subject").option("--predicate <p>", "Filter by predicate").option("--object <o>", "Filter by object").option("--as-of <date>", "Point-in-time query (YYYY-MM-DD or ISO 8601)").option("--project <slug>", "Restrict to a project slug").option("--json", "Output raw JSON").action(async (opts) => {
|
|
7883
|
-
await cmdQuery(opts);
|
|
7890
|
+
await cmdQuery$1(opts);
|
|
7884
7891
|
});
|
|
7885
7892
|
kgCmd.command("list").description("List currently-valid triples").option("--project <slug>", "Restrict to a project slug").option("--limit <n>", "Maximum triples to print", "50").action(async (opts) => {
|
|
7886
7893
|
await cmdList(opts);
|
|
@@ -7890,6 +7897,232 @@ function registerKgCommands(kgCmd) {
|
|
|
7890
7897
|
});
|
|
7891
7898
|
}
|
|
7892
7899
|
|
|
7900
|
+
//#endregion
|
|
7901
|
+
//#region src/cli/commands/db.ts
|
|
7902
|
+
function getSqliteDb() {
|
|
7903
|
+
return openFederation();
|
|
7904
|
+
}
|
|
7905
|
+
function sqliteQuery(sql) {
|
|
7906
|
+
const db = getSqliteDb();
|
|
7907
|
+
try {
|
|
7908
|
+
const raw = db.prepare(sql).all();
|
|
7909
|
+
if (raw.length === 0) return {
|
|
7910
|
+
columns: [],
|
|
7911
|
+
rows: []
|
|
7912
|
+
};
|
|
7913
|
+
const columns = Object.keys(raw[0]);
|
|
7914
|
+
return {
|
|
7915
|
+
columns,
|
|
7916
|
+
rows: raw.map((r) => columns.map((c) => r[c]))
|
|
7917
|
+
};
|
|
7918
|
+
} finally {
|
|
7919
|
+
db.close();
|
|
7920
|
+
}
|
|
7921
|
+
}
|
|
7922
|
+
function sqliteTables() {
|
|
7923
|
+
const db = getSqliteDb();
|
|
7924
|
+
try {
|
|
7925
|
+
return db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all().map((r) => r.name);
|
|
7926
|
+
} finally {
|
|
7927
|
+
db.close();
|
|
7928
|
+
}
|
|
7929
|
+
}
|
|
7930
|
+
function sqliteSchema(table) {
|
|
7931
|
+
const db = getSqliteDb();
|
|
7932
|
+
try {
|
|
7933
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(table)) throw new Error(`Invalid table name: ${table}`);
|
|
7934
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
7935
|
+
if (rows.length === 0) throw new Error(`Table not found: ${table}`);
|
|
7936
|
+
return {
|
|
7937
|
+
columns: [
|
|
7938
|
+
"cid",
|
|
7939
|
+
"name",
|
|
7940
|
+
"type",
|
|
7941
|
+
"notnull",
|
|
7942
|
+
"default",
|
|
7943
|
+
"pk"
|
|
7944
|
+
],
|
|
7945
|
+
data: rows.map((r) => [
|
|
7946
|
+
r.cid,
|
|
7947
|
+
r.name,
|
|
7948
|
+
r.type,
|
|
7949
|
+
r.notnull ? "NOT NULL" : "",
|
|
7950
|
+
r.dflt_value ?? "",
|
|
7951
|
+
r.pk ? "PK" : ""
|
|
7952
|
+
])
|
|
7953
|
+
};
|
|
7954
|
+
} finally {
|
|
7955
|
+
db.close();
|
|
7956
|
+
}
|
|
7957
|
+
}
|
|
7958
|
+
async function getPool() {
|
|
7959
|
+
const pgConfig = loadConfig().postgres ?? {};
|
|
7960
|
+
const { Pool } = await import("pg");
|
|
7961
|
+
const pool = new Pool(pgConfig.connectionString ? { connectionString: pgConfig.connectionString } : {
|
|
7962
|
+
host: pgConfig.host ?? "localhost",
|
|
7963
|
+
port: pgConfig.port ?? 5432,
|
|
7964
|
+
database: pgConfig.database ?? "pai",
|
|
7965
|
+
user: pgConfig.user ?? "pai",
|
|
7966
|
+
password: pgConfig.password ?? "pai",
|
|
7967
|
+
connectionTimeoutMillis: pgConfig.connectionTimeoutMs ?? 5e3
|
|
7968
|
+
});
|
|
7969
|
+
try {
|
|
7970
|
+
(await pool.connect()).release();
|
|
7971
|
+
} catch (e) {
|
|
7972
|
+
await pool.end();
|
|
7973
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
7974
|
+
throw new Error(`Cannot connect to Postgres: ${msg}`);
|
|
7975
|
+
}
|
|
7976
|
+
return pool;
|
|
7977
|
+
}
|
|
7978
|
+
async function postgresQuery(sql) {
|
|
7979
|
+
const pool = await getPool();
|
|
7980
|
+
try {
|
|
7981
|
+
const result = await pool.query(sql);
|
|
7982
|
+
const columns = result.fields.map((f) => f.name);
|
|
7983
|
+
return {
|
|
7984
|
+
columns,
|
|
7985
|
+
rows: result.rows.map((r) => columns.map((c) => r[c]))
|
|
7986
|
+
};
|
|
7987
|
+
} finally {
|
|
7988
|
+
await pool.end();
|
|
7989
|
+
}
|
|
7990
|
+
}
|
|
7991
|
+
async function postgresTables() {
|
|
7992
|
+
const pool = await getPool();
|
|
7993
|
+
try {
|
|
7994
|
+
return (await pool.query("SELECT tablename FROM pg_tables WHERE schemaname = 'public' ORDER BY tablename")).rows.map((r) => r.tablename);
|
|
7995
|
+
} finally {
|
|
7996
|
+
await pool.end();
|
|
7997
|
+
}
|
|
7998
|
+
}
|
|
7999
|
+
async function postgresSchema(table) {
|
|
8000
|
+
const pool = await getPool();
|
|
8001
|
+
try {
|
|
8002
|
+
const result = await pool.query(`SELECT column_name, data_type, is_nullable, column_default
|
|
8003
|
+
FROM information_schema.columns
|
|
8004
|
+
WHERE table_schema = 'public' AND table_name = $1
|
|
8005
|
+
ORDER BY ordinal_position`, [table]);
|
|
8006
|
+
if (result.rows.length === 0) throw new Error(`Table not found in public schema: ${table}`);
|
|
8007
|
+
return {
|
|
8008
|
+
columns: [
|
|
8009
|
+
"column",
|
|
8010
|
+
"type",
|
|
8011
|
+
"nullable",
|
|
8012
|
+
"default"
|
|
8013
|
+
],
|
|
8014
|
+
rows: result.rows.map((r) => [
|
|
8015
|
+
r.column_name,
|
|
8016
|
+
r.data_type,
|
|
8017
|
+
r.is_nullable === "YES" ? "YES" : "NO",
|
|
8018
|
+
r.column_default ?? ""
|
|
8019
|
+
])
|
|
8020
|
+
};
|
|
8021
|
+
} finally {
|
|
8022
|
+
await pool.end();
|
|
8023
|
+
}
|
|
8024
|
+
}
|
|
8025
|
+
function printResult(result, json) {
|
|
8026
|
+
if (json) {
|
|
8027
|
+
const objects = result.rows.map((row) => {
|
|
8028
|
+
const obj = {};
|
|
8029
|
+
result.columns.forEach((col, i) => {
|
|
8030
|
+
obj[col] = row[i];
|
|
8031
|
+
});
|
|
8032
|
+
return obj;
|
|
8033
|
+
});
|
|
8034
|
+
console.log(JSON.stringify(objects, null, 2));
|
|
8035
|
+
return;
|
|
8036
|
+
}
|
|
8037
|
+
if (result.rows.length === 0) {
|
|
8038
|
+
console.log();
|
|
8039
|
+
console.log(dim(" (no rows)"));
|
|
8040
|
+
console.log();
|
|
8041
|
+
return;
|
|
8042
|
+
}
|
|
8043
|
+
const stringRows = result.rows.map((row) => row.map((cell) => {
|
|
8044
|
+
if (cell === null || cell === void 0) return dim("NULL");
|
|
8045
|
+
if (typeof cell === "object") return JSON.stringify(cell);
|
|
8046
|
+
return String(cell);
|
|
8047
|
+
}));
|
|
8048
|
+
console.log();
|
|
8049
|
+
console.log(renderTable(result.columns, stringRows));
|
|
8050
|
+
console.log();
|
|
8051
|
+
console.log(dim(` ${result.rows.length} row(s)`));
|
|
8052
|
+
console.log();
|
|
8053
|
+
}
|
|
8054
|
+
async function cmdQuery(sql, opts) {
|
|
8055
|
+
const target = opts.db ?? "sqlite";
|
|
8056
|
+
console.log();
|
|
8057
|
+
console.log(header(` Query [${target}]`) + dim(` ${sql.slice(0, 80)}${sql.length > 80 ? "…" : ""}`));
|
|
8058
|
+
try {
|
|
8059
|
+
let result;
|
|
8060
|
+
if (target === "postgres") result = await postgresQuery(sql);
|
|
8061
|
+
else result = sqliteQuery(sql);
|
|
8062
|
+
printResult(result, opts.json ?? false);
|
|
8063
|
+
} catch (e) {
|
|
8064
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
8065
|
+
console.error(err(` ${msg}`));
|
|
8066
|
+
process.exit(1);
|
|
8067
|
+
}
|
|
8068
|
+
}
|
|
8069
|
+
async function cmdTables(opts) {
|
|
8070
|
+
const target = opts.db ?? "sqlite";
|
|
8071
|
+
console.log();
|
|
8072
|
+
console.log(header(` Tables [${target}]`));
|
|
8073
|
+
console.log();
|
|
8074
|
+
try {
|
|
8075
|
+
let tables;
|
|
8076
|
+
if (target === "postgres") tables = await postgresTables();
|
|
8077
|
+
else tables = sqliteTables();
|
|
8078
|
+
if (opts.json) {
|
|
8079
|
+
console.log(JSON.stringify(tables, null, 2));
|
|
8080
|
+
return;
|
|
8081
|
+
}
|
|
8082
|
+
if (tables.length === 0) console.log(dim(" (no tables found)"));
|
|
8083
|
+
else for (const t of tables) console.log(` ${bold(t)}`);
|
|
8084
|
+
console.log();
|
|
8085
|
+
console.log(dim(` ${tables.length} table(s)`));
|
|
8086
|
+
console.log();
|
|
8087
|
+
} catch (e) {
|
|
8088
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
8089
|
+
console.error(err(` ${msg}`));
|
|
8090
|
+
process.exit(1);
|
|
8091
|
+
}
|
|
8092
|
+
}
|
|
8093
|
+
async function cmdSchema(table, opts) {
|
|
8094
|
+
const target = opts.db ?? "sqlite";
|
|
8095
|
+
console.log();
|
|
8096
|
+
console.log(header(` Schema: ${table} [${target}]`));
|
|
8097
|
+
try {
|
|
8098
|
+
let result;
|
|
8099
|
+
if (target === "postgres") result = await postgresSchema(table);
|
|
8100
|
+
else {
|
|
8101
|
+
const raw = sqliteSchema(table);
|
|
8102
|
+
result = {
|
|
8103
|
+
columns: raw.columns,
|
|
8104
|
+
rows: raw.data
|
|
8105
|
+
};
|
|
8106
|
+
}
|
|
8107
|
+
printResult(result, opts.json ?? false);
|
|
8108
|
+
} catch (e) {
|
|
8109
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
8110
|
+
console.error(err(` ${msg}`));
|
|
8111
|
+
process.exit(1);
|
|
8112
|
+
}
|
|
8113
|
+
}
|
|
8114
|
+
function registerDbCommands(dbCmd) {
|
|
8115
|
+
dbCmd.command("query <sql>").description("Run a SQL query against the selected database").option("--db <target>", "Database target: sqlite (default) or postgres", "sqlite").option("--json", "Output results as JSON array").action(async (sql, opts) => {
|
|
8116
|
+
await cmdQuery(sql, opts);
|
|
8117
|
+
});
|
|
8118
|
+
dbCmd.command("tables").description("List all tables in the selected database").option("--db <target>", "Database target: sqlite (default) or postgres", "sqlite").option("--json", "Output as JSON array").action(async (opts) => {
|
|
8119
|
+
await cmdTables(opts);
|
|
8120
|
+
});
|
|
8121
|
+
dbCmd.command("schema <table>").description("Show column schema for a table").option("--db <target>", "Database target: sqlite (default) or postgres", "sqlite").option("--json", "Output as JSON array").action(async (table, opts) => {
|
|
8122
|
+
await cmdSchema(table, opts);
|
|
8123
|
+
});
|
|
8124
|
+
}
|
|
8125
|
+
|
|
7893
8126
|
//#endregion
|
|
7894
8127
|
//#region src/cli/index.ts
|
|
7895
8128
|
/**
|
|
@@ -7939,6 +8172,7 @@ registerUpdateCommand(program);
|
|
|
7939
8172
|
registerNotifyCommands(program.command("notify").description("Notification config: status, get, set, test, send"));
|
|
7940
8173
|
registerTopicCommands(program.command("topic").description("Topic shift detection: check whether context has drifted to a different project"));
|
|
7941
8174
|
registerKgCommands(program.command("kg").description("Temporal knowledge graph: backfill, query, list, stats"));
|
|
8175
|
+
registerDbCommands(program.command("db").description("Database inspection: query, tables, schema (sqlite or postgres)"));
|
|
7942
8176
|
registerObsidianCommands(program.command("obsidian").description("Obsidian vault: sync project notes, view status, open in Obsidian"), getDb);
|
|
7943
8177
|
registerZettelCommands(program.command("zettel").description("Zettelkasten intelligence: explore, surprise, converse, themes, health, suggest"), getDb);
|
|
7944
8178
|
registerObservationCommands(program.command("observation").description("Observation capture: list, search, and stats"));
|