@zosmaai/pi-llm-wiki 0.1.6 → 0.2.1
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/.github/workflows/ci.yml +7 -6
- package/.github/workflows/release.yml +3 -3
- package/AGENTS.md +57 -0
- package/CHANGELOG.md +30 -0
- package/CONTRIBUTING.md +43 -0
- package/LICENSE +21 -0
- package/README.md +42 -342
- package/biome.json +3 -0
- package/docs/api.md +105 -0
- package/docs/architecture.md +65 -0
- package/docs/commands.md +51 -0
- package/docs/configuration.md +38 -0
- package/docs/obsidian.md +21 -0
- package/extensions/llm-wiki/index.ts +53 -0
- package/extensions/llm-wiki/lib/guardrails.ts +69 -0
- package/extensions/llm-wiki/lib/metadata.ts +218 -0
- package/extensions/llm-wiki/lib/source-packet.ts +339 -0
- package/extensions/llm-wiki/lib/tools.ts +932 -0
- package/extensions/llm-wiki/lib/utils.ts +222 -0
- package/package.json +7 -2
- package/prompts/wiki-digest.md +5 -1
- package/prompts/wiki-discover.md +5 -1
- package/prompts/wiki-ingest.md +5 -1
- package/prompts/wiki-init.md +5 -1
- package/prompts/wiki-lint.md +5 -1
- package/prompts/wiki-query.md +5 -1
- package/prompts/wiki-run.md +5 -1
- package/prompts/wiki-status.md +1 -1
- package/scripts/release.js +72 -0
- package/skills/llm-wiki/SKILL.md +95 -369
- package/skills/llm-wiki/templates/pages/analysis.md +35 -0
- package/test/llm-wiki.test.ts +51 -9
- package/extensions/llm-wiki-tools.ts +0 -705
package/docs/api.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# API Reference
|
|
2
|
+
|
|
3
|
+
## Extension Tools
|
|
4
|
+
|
|
5
|
+
### wiki_bootstrap
|
|
6
|
+
|
|
7
|
+
Initialize a new LLM Wiki vault.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
wiki_bootstrap(topic: string, mode?: "personal" | "company", root?: string)
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### wiki_capture_source
|
|
14
|
+
|
|
15
|
+
Capture a URL, file, or text into an immutable source packet.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
wiki_capture_source(
|
|
19
|
+
inputType: "url" | "file" | "text",
|
|
20
|
+
value: string,
|
|
21
|
+
title?: string,
|
|
22
|
+
kind?: string,
|
|
23
|
+
tags?: string[],
|
|
24
|
+
createSourcePage?: boolean
|
|
25
|
+
)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### wiki_ingest
|
|
29
|
+
|
|
30
|
+
Get a batch of sources needing synthesis.
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
wiki_ingest(batch_size?: number)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### wiki_ensure_page
|
|
37
|
+
|
|
38
|
+
Create or resolve a canonical page.
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
wiki_ensure_page(
|
|
42
|
+
type: "concept" | "entity" | "synthesis" | "analysis",
|
|
43
|
+
title: string,
|
|
44
|
+
aliases?: string[],
|
|
45
|
+
tags?: string[],
|
|
46
|
+
summary?: string,
|
|
47
|
+
createIfMissing?: boolean
|
|
48
|
+
)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### wiki_search
|
|
52
|
+
|
|
53
|
+
Search the wiki registry.
|
|
54
|
+
|
|
55
|
+
```
|
|
56
|
+
wiki_search(query: string, type?: string, limit?: number)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
### wiki_lint
|
|
60
|
+
|
|
61
|
+
Health check the wiki.
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
wiki_lint(mode?: string, writeReport?: boolean, limit?: number)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### wiki_status
|
|
68
|
+
|
|
69
|
+
Show wiki statistics.
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
wiki_status()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### wiki_rebuild_meta
|
|
76
|
+
|
|
77
|
+
Force metadata rebuild.
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
wiki_rebuild_meta()
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### wiki_log_event
|
|
84
|
+
|
|
85
|
+
Record a structured event.
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
wiki_log_event(
|
|
89
|
+
kind: string,
|
|
90
|
+
title: string,
|
|
91
|
+
summary?: string,
|
|
92
|
+
sourceIds?: string[],
|
|
93
|
+
pagePaths?: string[],
|
|
94
|
+
notes?: string[],
|
|
95
|
+
actor?: "agent" | "user" | "extension"
|
|
96
|
+
)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### wiki_watch
|
|
100
|
+
|
|
101
|
+
Schedule auto-updates.
|
|
102
|
+
|
|
103
|
+
```
|
|
104
|
+
wiki_watch(schedule: string, command: string)
|
|
105
|
+
```
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Architecture
|
|
2
|
+
|
|
3
|
+
## Four Layers
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
WIKI_ROOT/
|
|
7
|
+
├── raw/sources/SRC-*/ # Immutable source packets (extension-owned)
|
|
8
|
+
│ ├── manifest.json # Capture metadata
|
|
9
|
+
│ ├── original/ # Original artifact
|
|
10
|
+
│ ├── extracted.md # Normalized markdown
|
|
11
|
+
│ └── attachments/ # Downloaded images, PDFs
|
|
12
|
+
├── wiki/ # Editable knowledge pages (you + LLM)
|
|
13
|
+
│ ├── sources/ # One summary per source
|
|
14
|
+
│ ├── entities/ # People, orgs, tools, products
|
|
15
|
+
│ ├── concepts/ # Ideas, patterns, frameworks
|
|
16
|
+
│ ├── syntheses/ # Cross-cutting analyses
|
|
17
|
+
│ └── analyses/ # Durable query answers
|
|
18
|
+
├── meta/ # Auto-generated (extension-owned)
|
|
19
|
+
│ ├── registry.json # Master page catalog
|
|
20
|
+
│ ├── backlinks.json # Inbound link map
|
|
21
|
+
│ ├── index.md # Human-readable catalog
|
|
22
|
+
│ ├── log.md # Activity log
|
|
23
|
+
│ └── events.jsonl # Structured event stream
|
|
24
|
+
└── .wiki/ # Config and templates
|
|
25
|
+
├── config.json
|
|
26
|
+
└── templates/
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Ownership Rules
|
|
30
|
+
|
|
31
|
+
| Path | Owner | Rule |
|
|
32
|
+
| --------- | ------------------------ | ------------------------ |
|
|
33
|
+
| `raw/**` | Extension | Immutable after capture |
|
|
34
|
+
| `wiki/**` | Model + user | Editable knowledge pages |
|
|
35
|
+
| `meta/*` | Extension | Auto-generated |
|
|
36
|
+
| `.wiki/*` | Human + explicit request | Operating rules |
|
|
37
|
+
|
|
38
|
+
## Source Packet Format
|
|
39
|
+
|
|
40
|
+
Each captured source becomes a packet:
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
raw/sources/SRC-YYYY-MM-DD-NNN/
|
|
44
|
+
manifest.json
|
|
45
|
+
original/
|
|
46
|
+
extracted.md
|
|
47
|
+
attachments/
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Page Types
|
|
51
|
+
|
|
52
|
+
- **source** — what this specific source says
|
|
53
|
+
- **entity** — people, orgs, tools, products
|
|
54
|
+
- **concept** — ideas, patterns, frameworks
|
|
55
|
+
- **synthesis** — cross-source theses and tensions
|
|
56
|
+
- **analysis** — durable filed answers from queries
|
|
57
|
+
|
|
58
|
+
## Linking Style
|
|
59
|
+
|
|
60
|
+
- Internal: `[[folder/page-name]]`
|
|
61
|
+
- Citation: `[[sources/SRC-YYYY-MM-DD-NNN]]`
|
|
62
|
+
|
|
63
|
+
## Guardrails
|
|
64
|
+
|
|
65
|
+
The extension blocks direct edits to `raw/**` and `meta/**`. Metadata rebuilds automatically after `wiki/**` edits.
|
package/docs/commands.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# Commands
|
|
2
|
+
|
|
3
|
+
## Slash Commands
|
|
4
|
+
|
|
5
|
+
| Command | Description |
|
|
6
|
+
| ---------------- | ------------------------------------- |
|
|
7
|
+
| `/wiki-init` | Create a new wiki vault |
|
|
8
|
+
| `/wiki-ingest` | Process new sources |
|
|
9
|
+
| `/wiki-query` | Ask questions against the wiki |
|
|
10
|
+
| `/wiki-lint` | Health check |
|
|
11
|
+
| `/wiki-discover` | Auto-discover sources |
|
|
12
|
+
| `/wiki-run` | Full cycle (discover → ingest → lint) |
|
|
13
|
+
| `/wiki-status` | Show wiki health |
|
|
14
|
+
| `/wiki-digest` | Daily/weekly summary |
|
|
15
|
+
|
|
16
|
+
## Extension Tools
|
|
17
|
+
|
|
18
|
+
The extension registers 10 tools the LLM can call directly:
|
|
19
|
+
|
|
20
|
+
| Tool | Purpose |
|
|
21
|
+
| --------------------- | ------------------------------------------- |
|
|
22
|
+
| `wiki_bootstrap` | Initialize a new vault |
|
|
23
|
+
| `wiki_capture_source` | Capture URL/file/text into immutable packet |
|
|
24
|
+
| `wiki_ingest` | Get batch of uningested sources |
|
|
25
|
+
| `wiki_ensure_page` | Create canonical page from template |
|
|
26
|
+
| `wiki_search` | Search the wiki registry |
|
|
27
|
+
| `wiki_lint` | Health check with auto-fix |
|
|
28
|
+
| `wiki_status` | Instant stats |
|
|
29
|
+
| `wiki_rebuild_meta` | Force metadata rebuild |
|
|
30
|
+
| `wiki_log_event` | Record custom event |
|
|
31
|
+
| `wiki_watch` | Schedule auto-updates |
|
|
32
|
+
|
|
33
|
+
## Workflows
|
|
34
|
+
|
|
35
|
+
### Capture → Ingest → Synthesize
|
|
36
|
+
|
|
37
|
+
1. `wiki_capture_source(url="...")` — creates packet + skeleton
|
|
38
|
+
2. `wiki_ingest()` — get batch of sources needing synthesis
|
|
39
|
+
3. Read `raw/sources/SRC-*/extracted.md`
|
|
40
|
+
4. Update skeleton source page with summary, entities, concepts
|
|
41
|
+
5. `wiki_ensure_page(type="entity", title="...")` for each entity
|
|
42
|
+
6. Add `[[wikilinks]]` between related pages
|
|
43
|
+
7. Extension auto-rebuilds metadata
|
|
44
|
+
|
|
45
|
+
### Query → Answer → File
|
|
46
|
+
|
|
47
|
+
1. `wiki_search(query="...")` to find relevant pages
|
|
48
|
+
2. Read those pages
|
|
49
|
+
3. Synthesize answer with `[[wikilink]]` citations
|
|
50
|
+
4. If novel: create analysis page via `wiki_ensure_page(type="analysis")`
|
|
51
|
+
5. Extension auto-updates metadata
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Configuration
|
|
2
|
+
|
|
3
|
+
Wiki configuration lives in `.wiki/config.json`.
|
|
4
|
+
|
|
5
|
+
## Modes
|
|
6
|
+
|
|
7
|
+
### Personal
|
|
8
|
+
|
|
9
|
+
- Extra folders: `wiki/journal/`, `wiki/goals/`
|
|
10
|
+
- Track: learning, books, health, reflections
|
|
11
|
+
|
|
12
|
+
### Company
|
|
13
|
+
|
|
14
|
+
- Extra folders: `wiki/changes/`, `wiki/decisions/`
|
|
15
|
+
- Track: competitors, market, strategy
|
|
16
|
+
- Frontmatter: `confidence: high | medium | low`
|
|
17
|
+
|
|
18
|
+
## Settings
|
|
19
|
+
|
|
20
|
+
| Setting | Default | Description |
|
|
21
|
+
| -------------------------- | ------- | ---------------------------------- |
|
|
22
|
+
| `max_sources_per_discover` | 8 | Sources fetched per discovery run |
|
|
23
|
+
| `auto_fix_lint` | false | Auto-fix lint issues |
|
|
24
|
+
| `batch_ingest_size` | 3 | Sources processed per ingest batch |
|
|
25
|
+
|
|
26
|
+
## Page Frontmatter
|
|
27
|
+
|
|
28
|
+
```yaml
|
|
29
|
+
---
|
|
30
|
+
type: entity | concept | source | synthesis | analysis
|
|
31
|
+
created: YYYY-MM-DD
|
|
32
|
+
updated: YYYY-MM-DD
|
|
33
|
+
sources: [sources/SRC-YYYY-MM-DD-NNN]
|
|
34
|
+
---
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Entity: add `category: person | organization | tool | project | product`
|
|
38
|
+
Concept: add `domain: ai | engineering | business | product | design | personal`
|
package/docs/obsidian.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Obsidian Integration
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
1. Open `wiki/` as an Obsidian vault
|
|
6
|
+
2. The extension generates `meta/index.md` as a browsable catalog
|
|
7
|
+
3. `meta/backlinks.json` is available for graph plugins
|
|
8
|
+
|
|
9
|
+
## Recommended Plugins
|
|
10
|
+
|
|
11
|
+
- [Dataview](https://github.com/blacksmithgu/obsidian-dataview) — Query pages by frontmatter
|
|
12
|
+
- [Graph View](https://obsidian.md) (built-in) — Visualize `[[wikilink]]` connections
|
|
13
|
+
- [Backlinks](https://obsidian.md) (built-in) — See inbound links
|
|
14
|
+
|
|
15
|
+
## Web Clipper
|
|
16
|
+
|
|
17
|
+
Use [Obsidian Web Clipper](https://obsidian.md/clipper) to save articles directly into `raw/articles/`.
|
|
18
|
+
|
|
19
|
+
## Dataview Dashboard
|
|
20
|
+
|
|
21
|
+
The extension creates `meta/index.md` with page listings. For custom dashboards, use Dataview queries against frontmatter fields like `type`, `domain`, `category`, `sources`.
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { installGuardrails } from "./lib/guardrails.js";
|
|
3
|
+
import {
|
|
4
|
+
registerWikiBootstrap,
|
|
5
|
+
registerWikiCaptureSource,
|
|
6
|
+
registerWikiEnsurePage,
|
|
7
|
+
registerWikiIngest,
|
|
8
|
+
registerWikiLint,
|
|
9
|
+
registerWikiLogEvent,
|
|
10
|
+
registerWikiRebuildMeta,
|
|
11
|
+
registerWikiSearch,
|
|
12
|
+
registerWikiStatus,
|
|
13
|
+
registerWikiWatch,
|
|
14
|
+
} from "./lib/tools.js";
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @zosmaai/pi-llm-wiki — LLM Wiki extension for Pi
|
|
18
|
+
*
|
|
19
|
+
* Registers 10 custom tools and installs guardrails:
|
|
20
|
+
* - wiki_bootstrap Initialize a new vault
|
|
21
|
+
* - wiki_capture_source Capture URL/file/text into source packet
|
|
22
|
+
* - wiki_ingest Get batch of sources needing synthesis
|
|
23
|
+
* - wiki_ensure_page Create canonical page from template
|
|
24
|
+
* - wiki_search Search generated registry
|
|
25
|
+
* - wiki_lint Health check with auto-fix
|
|
26
|
+
* - wiki_status Instant stats from registry
|
|
27
|
+
* - wiki_rebuild_meta Force metadata rebuild
|
|
28
|
+
* - wiki_log_event Append event and regenerate log
|
|
29
|
+
* - wiki_watch Schedule auto-updates
|
|
30
|
+
*
|
|
31
|
+
* Guardrails:
|
|
32
|
+
* - Blocks direct edits to raw/** and meta/**
|
|
33
|
+
* - Auto-rebuilds metadata after wiki/** edits
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export default function (pi: ExtensionAPI) {
|
|
37
|
+
registerWikiBootstrap(pi);
|
|
38
|
+
registerWikiCaptureSource(pi);
|
|
39
|
+
registerWikiIngest(pi);
|
|
40
|
+
registerWikiEnsurePage(pi);
|
|
41
|
+
registerWikiSearch(pi);
|
|
42
|
+
registerWikiLint(pi);
|
|
43
|
+
registerWikiStatus(pi);
|
|
44
|
+
registerWikiRebuildMeta(pi);
|
|
45
|
+
registerWikiLogEvent(pi);
|
|
46
|
+
registerWikiWatch(pi);
|
|
47
|
+
|
|
48
|
+
installGuardrails(pi);
|
|
49
|
+
|
|
50
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
51
|
+
ctx.ui.setStatus("llm-wiki", "🧠 LLM Wiki (10 tools, guardrails active)");
|
|
52
|
+
});
|
|
53
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { isToolCallEventType } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
|
|
3
|
+
import { rebuildMetadataLight } from "./metadata.js";
|
|
4
|
+
import { isProtectedPath, resolveVaultRoot } from "./utils.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Guardrails and auto-rebuild hooks for the LLM Wiki extension.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
let pendingRebuild = false;
|
|
11
|
+
|
|
12
|
+
/** Install guardrails on the extension API. */
|
|
13
|
+
export function installGuardrails(pi: ExtensionAPI): void {
|
|
14
|
+
// Block direct edits to raw/ and meta/
|
|
15
|
+
pi.on("tool_call", async (event) => {
|
|
16
|
+
if (isToolCallEventType("write", event)) {
|
|
17
|
+
const path = event.input.path as string;
|
|
18
|
+
const root = resolveVaultRoot(process.cwd());
|
|
19
|
+
const check = isProtectedPath(path, root);
|
|
20
|
+
if (check.protected) {
|
|
21
|
+
return { block: true, reason: check.reason };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (isToolCallEventType("edit", event)) {
|
|
26
|
+
const path = event.input.path as string;
|
|
27
|
+
const root = resolveVaultRoot(process.cwd());
|
|
28
|
+
const check = isProtectedPath(path, root);
|
|
29
|
+
if (check.protected) {
|
|
30
|
+
return { block: true, reason: check.reason };
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Track wiki edits for auto-rebuild
|
|
36
|
+
pi.on("tool_result", async (event) => {
|
|
37
|
+
if (event.toolName === "write" || event.toolName === "edit") {
|
|
38
|
+
const path = event.input.path as string;
|
|
39
|
+
const root = resolveVaultRoot(process.cwd());
|
|
40
|
+
const wikiPath = `${root}/wiki/`;
|
|
41
|
+
if (path?.startsWith(wikiPath)) {
|
|
42
|
+
pendingRebuild = true;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Rebuild metadata at end of turn if wiki was modified
|
|
48
|
+
pi.on("turn_end", async (_event) => {
|
|
49
|
+
if (pendingRebuild) {
|
|
50
|
+
pendingRebuild = false;
|
|
51
|
+
try {
|
|
52
|
+
const root = resolveVaultRoot(process.cwd());
|
|
53
|
+
const paths = {
|
|
54
|
+
root,
|
|
55
|
+
raw: `${root}/raw`,
|
|
56
|
+
rawSources: `${root}/raw/sources`,
|
|
57
|
+
wiki: `${root}/wiki`,
|
|
58
|
+
meta: `${root}/meta`,
|
|
59
|
+
dotWiki: `${root}/.wiki`,
|
|
60
|
+
outputs: `${root}/outputs`,
|
|
61
|
+
discoveries: `${root}/.discoveries`,
|
|
62
|
+
};
|
|
63
|
+
rebuildMetadataLight(paths);
|
|
64
|
+
} catch {
|
|
65
|
+
// Silently fail — metadata rebuild is best-effort
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
type VaultPaths,
|
|
5
|
+
extractWikilinks,
|
|
6
|
+
findWikiPages,
|
|
7
|
+
fmtDate,
|
|
8
|
+
parseFrontmatter,
|
|
9
|
+
readJson,
|
|
10
|
+
readText,
|
|
11
|
+
writeJson,
|
|
12
|
+
} from "./utils.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Metadata generation for the LLM Wiki.
|
|
16
|
+
*
|
|
17
|
+
* Rebuilds registry.json, backlinks.json, index.md, log.md, and lint-report.md
|
|
18
|
+
* deterministically from the current state of raw/ and wiki/.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
export interface RegistryEntry {
|
|
22
|
+
type: "source" | "entity" | "concept" | "synthesis" | "analysis";
|
|
23
|
+
title: string;
|
|
24
|
+
created: string;
|
|
25
|
+
updated: string;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface Registry {
|
|
30
|
+
version: string;
|
|
31
|
+
last_updated: string;
|
|
32
|
+
pages: Record<string, RegistryEntry>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Backlinks {
|
|
36
|
+
[pageId: string]: string[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface WikiEvent {
|
|
40
|
+
timestamp: string;
|
|
41
|
+
kind: string;
|
|
42
|
+
[key: string]: unknown;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Rebuild the complete metadata layer. */
|
|
46
|
+
export function rebuildMetadata(paths: VaultPaths): void {
|
|
47
|
+
mkdirSync(paths.meta, { recursive: true });
|
|
48
|
+
|
|
49
|
+
const registry = buildRegistry(paths);
|
|
50
|
+
const backlinks = buildBacklinks(paths, registry);
|
|
51
|
+
|
|
52
|
+
writeJson(join(paths.meta, "registry.json"), registry);
|
|
53
|
+
writeJson(join(paths.meta, "backlinks.json"), backlinks);
|
|
54
|
+
writeFileSync(join(paths.meta, "index.md"), buildIndexMarkdown(registry), "utf-8");
|
|
55
|
+
|
|
56
|
+
const log = buildLogMarkdown(paths);
|
|
57
|
+
writeFileSync(join(paths.meta, "log.md"), log, "utf-8");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Build registry from wiki/ and raw/ state. */
|
|
61
|
+
export function buildRegistry(paths: VaultPaths): Registry {
|
|
62
|
+
const pages: Record<string, RegistryEntry> = {};
|
|
63
|
+
|
|
64
|
+
// Scan wiki pages
|
|
65
|
+
for (const page of findWikiPages(paths.wiki)) {
|
|
66
|
+
const { frontmatter } = parseFrontmatter(page.content);
|
|
67
|
+
const type = String(frontmatter.type || "page") as RegistryEntry["type"];
|
|
68
|
+
const title = String(frontmatter.title || page.relative.split("/").pop() || "Untitled");
|
|
69
|
+
|
|
70
|
+
pages[page.relative] = {
|
|
71
|
+
type,
|
|
72
|
+
title,
|
|
73
|
+
created: String(frontmatter.created || fmtDate()),
|
|
74
|
+
updated: String(frontmatter.updated || frontmatter.created || fmtDate()),
|
|
75
|
+
...frontmatter,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Scan raw source packets
|
|
80
|
+
if (existsSync(paths.rawSources)) {
|
|
81
|
+
for (const entry of readdirSync(paths.rawSources)) {
|
|
82
|
+
const manifestPath = join(paths.rawSources, entry, "manifest.json");
|
|
83
|
+
if (!existsSync(manifestPath)) continue;
|
|
84
|
+
|
|
85
|
+
const manifest = readJson<Record<string, unknown>>(manifestPath, {});
|
|
86
|
+
const id = String(manifest.id || entry);
|
|
87
|
+
const sourcePage = `sources/${id}`;
|
|
88
|
+
|
|
89
|
+
if (!pages[sourcePage]) {
|
|
90
|
+
pages[sourcePage] = {
|
|
91
|
+
type: "source",
|
|
92
|
+
title: String(manifest.title || id),
|
|
93
|
+
created: String(manifest.captured || fmtDate()),
|
|
94
|
+
updated: String(manifest.captured || fmtDate()),
|
|
95
|
+
...manifest,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
version: "1.0",
|
|
103
|
+
last_updated: new Date().toISOString(),
|
|
104
|
+
pages,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Build backlinks map from all wiki pages. */
|
|
109
|
+
export function buildBacklinks(paths: VaultPaths, registry: Registry): Backlinks {
|
|
110
|
+
const inbound: Backlinks = {};
|
|
111
|
+
|
|
112
|
+
// Initialize all pages with empty arrays
|
|
113
|
+
for (const id of Object.keys(registry.pages)) {
|
|
114
|
+
inbound[id] = [];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Count inbound links
|
|
118
|
+
for (const page of findWikiPages(paths.wiki)) {
|
|
119
|
+
const links = extractWikilinks(page.content);
|
|
120
|
+
for (const link of links) {
|
|
121
|
+
if (inbound[link] && !inbound[link].includes(page.relative)) {
|
|
122
|
+
inbound[link].push(page.relative);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return inbound;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Build index markdown from registry. */
|
|
131
|
+
export function buildIndexMarkdown(registry: Registry): string {
|
|
132
|
+
const byType: Record<string, Array<{ id: string; entry: RegistryEntry }>> = {};
|
|
133
|
+
|
|
134
|
+
for (const [id, entry] of Object.entries(registry.pages)) {
|
|
135
|
+
const t = entry.type;
|
|
136
|
+
if (!byType[t]) byType[t] = [];
|
|
137
|
+
byType[t].push({ id, entry });
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const sections: string[] = [];
|
|
141
|
+
sections.push(
|
|
142
|
+
"# Wiki Index\n\n> Auto-generated from meta/registry.json. Do not edit manually.\n",
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
for (const [type, items] of Object.entries(byType).sort()) {
|
|
146
|
+
const label = `${type.charAt(0).toUpperCase() + type.slice(1)}s`;
|
|
147
|
+
sections.push(`## ${label}\n`);
|
|
148
|
+
for (const { id, entry } of items.sort((a, b) => a.id.localeCompare(b.id))) {
|
|
149
|
+
sections.push(`- [[${id}]] — ${entry.title} *(created: ${entry.created})*`);
|
|
150
|
+
}
|
|
151
|
+
sections.push("");
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
sections.push(
|
|
155
|
+
`---\n*Last updated: ${registry.last_updated}* | *Total pages: ${Object.keys(registry.pages).length}*`,
|
|
156
|
+
);
|
|
157
|
+
return `${sections.join("\n")}\n`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Build log markdown from events.jsonl. */
|
|
161
|
+
export function buildLogMarkdown(paths: VaultPaths): string {
|
|
162
|
+
const eventsPath = join(paths.meta, "events.jsonl");
|
|
163
|
+
const events: WikiEvent[] = [];
|
|
164
|
+
|
|
165
|
+
if (existsSync(eventsPath)) {
|
|
166
|
+
const raw = readFileSync(eventsPath, "utf-8").trim();
|
|
167
|
+
for (const line of raw.split("\n")) {
|
|
168
|
+
if (!line.trim()) continue;
|
|
169
|
+
try {
|
|
170
|
+
events.push(JSON.parse(line) as WikiEvent);
|
|
171
|
+
} catch {
|
|
172
|
+
// skip malformed
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const lines: string[] = [];
|
|
178
|
+
lines.push("# Activity Log\n\n> Auto-generated from meta/events.jsonl. Do not edit manually.\n");
|
|
179
|
+
|
|
180
|
+
for (const ev of events) {
|
|
181
|
+
const ts = ev.timestamp || "unknown";
|
|
182
|
+
const kind = ev.kind || "event";
|
|
183
|
+
const details = Object.entries(ev)
|
|
184
|
+
.filter(([k]) => k !== "timestamp" && k !== "kind")
|
|
185
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`)
|
|
186
|
+
.join(", ");
|
|
187
|
+
|
|
188
|
+
lines.push(`## [${ts}] ${kind}`);
|
|
189
|
+
if (details) lines.push(`- ${details}`);
|
|
190
|
+
lines.push("");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (events.length === 0) {
|
|
194
|
+
lines.push("_No events recorded yet._\n");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return `${lines.join("\n")}\n`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** Append an event to events.jsonl. */
|
|
201
|
+
export function appendEvent(paths: VaultPaths, event: Omit<WikiEvent, "timestamp">): void {
|
|
202
|
+
mkdirSync(paths.meta, { recursive: true });
|
|
203
|
+
const eventsPath = join(paths.meta, "events.jsonl");
|
|
204
|
+
const line = JSON.stringify({ timestamp: new Date().toISOString(), ...event });
|
|
205
|
+
writeFileSync(eventsPath, `${line}\n`, { flag: "a", encoding: "utf-8" });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Quick lightweight metadata rebuild (backlinks + index + log only). */
|
|
209
|
+
export function rebuildMetadataLight(paths: VaultPaths): void {
|
|
210
|
+
const registry = buildRegistry(paths);
|
|
211
|
+
const backlinks = buildBacklinks(paths, registry);
|
|
212
|
+
writeJson(join(paths.meta, "registry.json"), registry);
|
|
213
|
+
writeJson(join(paths.meta, "backlinks.json"), backlinks);
|
|
214
|
+
writeFileSync(join(paths.meta, "index.md"), buildIndexMarkdown(registry), "utf-8");
|
|
215
|
+
|
|
216
|
+
const log = buildLogMarkdown(paths);
|
|
217
|
+
writeFileSync(join(paths.meta, "log.md"), log, "utf-8");
|
|
218
|
+
}
|