pi-para 0.5.2

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.
Files changed (198) hide show
  1. package/CHANGELOG.md +214 -0
  2. package/README.md +305 -0
  3. package/SETUP.md +171 -0
  4. package/dist/capture.d.ts +34 -0
  5. package/dist/capture.d.ts.map +1 -0
  6. package/dist/capture.js +461 -0
  7. package/dist/capture.js.map +1 -0
  8. package/dist/cli.d.ts +15 -0
  9. package/dist/cli.d.ts.map +1 -0
  10. package/dist/cli.js +405 -0
  11. package/dist/cli.js.map +1 -0
  12. package/dist/commands.d.ts +15 -0
  13. package/dist/commands.d.ts.map +1 -0
  14. package/dist/commands.js +786 -0
  15. package/dist/commands.js.map +1 -0
  16. package/dist/context.d.ts +61 -0
  17. package/dist/context.d.ts.map +1 -0
  18. package/dist/context.js +289 -0
  19. package/dist/context.js.map +1 -0
  20. package/dist/daemon.d.ts +49 -0
  21. package/dist/daemon.d.ts.map +1 -0
  22. package/dist/daemon.js +226 -0
  23. package/dist/daemon.js.map +1 -0
  24. package/dist/frontmatter.d.ts +59 -0
  25. package/dist/frontmatter.d.ts.map +1 -0
  26. package/dist/frontmatter.js +170 -0
  27. package/dist/frontmatter.js.map +1 -0
  28. package/dist/gepa/dataset.d.ts +36 -0
  29. package/dist/gepa/dataset.d.ts.map +1 -0
  30. package/dist/gepa/dataset.js +148 -0
  31. package/dist/gepa/dataset.js.map +1 -0
  32. package/dist/gepa/evaluator.d.ts +51 -0
  33. package/dist/gepa/evaluator.d.ts.map +1 -0
  34. package/dist/gepa/evaluator.js +317 -0
  35. package/dist/gepa/evaluator.js.map +1 -0
  36. package/dist/gepa/index.d.ts +66 -0
  37. package/dist/gepa/index.d.ts.map +1 -0
  38. package/dist/gepa/index.js +240 -0
  39. package/dist/gepa/index.js.map +1 -0
  40. package/dist/gepa/llm.d.ts +35 -0
  41. package/dist/gepa/llm.d.ts.map +1 -0
  42. package/dist/gepa/llm.js +268 -0
  43. package/dist/gepa/llm.js.map +1 -0
  44. package/dist/gepa/optimized/capture-prompt.txt +65 -0
  45. package/dist/gepa/optimizer.d.ts +29 -0
  46. package/dist/gepa/optimizer.d.ts.map +1 -0
  47. package/dist/gepa/optimizer.js +249 -0
  48. package/dist/gepa/optimizer.js.map +1 -0
  49. package/dist/gepa/targets.d.ts +39 -0
  50. package/dist/gepa/targets.d.ts.map +1 -0
  51. package/dist/gepa/targets.js +326 -0
  52. package/dist/gepa/targets.js.map +1 -0
  53. package/dist/gepa/types.d.ts +168 -0
  54. package/dist/gepa/types.d.ts.map +1 -0
  55. package/dist/gepa/types.js +124 -0
  56. package/dist/gepa/types.js.map +1 -0
  57. package/dist/index.d.ts +21 -0
  58. package/dist/index.d.ts.map +1 -0
  59. package/dist/index.js +294 -0
  60. package/dist/index.js.map +1 -0
  61. package/dist/ingest.d.ts +88 -0
  62. package/dist/ingest.d.ts.map +1 -0
  63. package/dist/ingest.js +105 -0
  64. package/dist/ingest.js.map +1 -0
  65. package/dist/link-discovery.d.ts +40 -0
  66. package/dist/link-discovery.d.ts.map +1 -0
  67. package/dist/link-discovery.js +122 -0
  68. package/dist/link-discovery.js.map +1 -0
  69. package/dist/link-utils.d.ts +30 -0
  70. package/dist/link-utils.d.ts.map +1 -0
  71. package/dist/link-utils.js +145 -0
  72. package/dist/link-utils.js.map +1 -0
  73. package/dist/lint.d.ts +39 -0
  74. package/dist/lint.d.ts.map +1 -0
  75. package/dist/lint.js +827 -0
  76. package/dist/lint.js.map +1 -0
  77. package/dist/maintainer.d.ts +24 -0
  78. package/dist/maintainer.d.ts.map +1 -0
  79. package/dist/maintainer.js +358 -0
  80. package/dist/maintainer.js.map +1 -0
  81. package/dist/processor.d.ts +14 -0
  82. package/dist/processor.d.ts.map +1 -0
  83. package/dist/processor.js +361 -0
  84. package/dist/processor.js.map +1 -0
  85. package/dist/query.d.ts +69 -0
  86. package/dist/query.d.ts.map +1 -0
  87. package/dist/query.js +107 -0
  88. package/dist/query.js.map +1 -0
  89. package/dist/raw.d.ts +57 -0
  90. package/dist/raw.d.ts.map +1 -0
  91. package/dist/raw.js +219 -0
  92. package/dist/raw.js.map +1 -0
  93. package/dist/redact.d.ts +21 -0
  94. package/dist/redact.d.ts.map +1 -0
  95. package/dist/redact.js +143 -0
  96. package/dist/redact.js.map +1 -0
  97. package/dist/scope.d.ts +54 -0
  98. package/dist/scope.d.ts.map +1 -0
  99. package/dist/scope.js +161 -0
  100. package/dist/scope.js.map +1 -0
  101. package/dist/scripts/bulk-link-fix.d.ts +9 -0
  102. package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
  103. package/dist/scripts/bulk-link-fix.js +106 -0
  104. package/dist/scripts/bulk-link-fix.js.map +1 -0
  105. package/dist/session-tools.d.ts +33 -0
  106. package/dist/session-tools.d.ts.map +1 -0
  107. package/dist/session-tools.js +211 -0
  108. package/dist/session-tools.js.map +1 -0
  109. package/dist/state.d.ts +54 -0
  110. package/dist/state.d.ts.map +1 -0
  111. package/dist/state.js +176 -0
  112. package/dist/state.js.map +1 -0
  113. package/dist/store.d.ts +56 -0
  114. package/dist/store.d.ts.map +1 -0
  115. package/dist/store.js +227 -0
  116. package/dist/store.js.map +1 -0
  117. package/dist/summarize.d.ts +56 -0
  118. package/dist/summarize.d.ts.map +1 -0
  119. package/dist/summarize.js +231 -0
  120. package/dist/summarize.js.map +1 -0
  121. package/dist/tag-registry.d.ts +56 -0
  122. package/dist/tag-registry.d.ts.map +1 -0
  123. package/dist/tag-registry.js +146 -0
  124. package/dist/tag-registry.js.map +1 -0
  125. package/dist/templates/prompts.d.ts +63 -0
  126. package/dist/templates/prompts.d.ts.map +1 -0
  127. package/dist/templates/prompts.js +260 -0
  128. package/dist/templates/prompts.js.map +1 -0
  129. package/dist/tools.d.ts +26 -0
  130. package/dist/tools.d.ts.map +1 -0
  131. package/dist/tools.js +1033 -0
  132. package/dist/tools.js.map +1 -0
  133. package/dist/watcher.d.ts +25 -0
  134. package/dist/watcher.d.ts.map +1 -0
  135. package/dist/watcher.js +84 -0
  136. package/dist/watcher.js.map +1 -0
  137. package/dist/webui/client/dist/index.html +1885 -0
  138. package/dist/webui/server.d.ts +31 -0
  139. package/dist/webui/server.d.ts.map +1 -0
  140. package/dist/webui/server.js +566 -0
  141. package/dist/webui/server.js.map +1 -0
  142. package/dist/wiki.d.ts +103 -0
  143. package/dist/wiki.d.ts.map +1 -0
  144. package/dist/wiki.js +428 -0
  145. package/dist/wiki.js.map +1 -0
  146. package/package.json +142 -0
  147. package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
  148. package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
  149. package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
  150. package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
  151. package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
  152. package/scripts/gepa/dataset.py +192 -0
  153. package/scripts/gepa/lm_providers.py +472 -0
  154. package/scripts/gepa/metric.py +279 -0
  155. package/scripts/gepa/optimize.py +367 -0
  156. package/scripts/gepa/program.py +99 -0
  157. package/scripts/gepa/pyproject.toml +19 -0
  158. package/scripts/gepa/run-all.sh +53 -0
  159. package/scripts/gepa/uv.lock +2241 -0
  160. package/scripts/gepa/wiki_reader.py +113 -0
  161. package/skills/para/SKILL.md +77 -0
  162. package/skills/setup/SKILL.md +20 -0
  163. package/src/capture.ts +563 -0
  164. package/src/cli.ts +416 -0
  165. package/src/commands.ts +878 -0
  166. package/src/context.ts +390 -0
  167. package/src/daemon.ts +288 -0
  168. package/src/frontmatter.ts +219 -0
  169. package/src/gepa/index.ts +287 -0
  170. package/src/gepa/optimized/capture-prompt.txt +65 -0
  171. package/src/index.ts +384 -0
  172. package/src/ingest.ts +177 -0
  173. package/src/link-discovery.ts +170 -0
  174. package/src/link-utils.ts +169 -0
  175. package/src/lint.ts +1018 -0
  176. package/src/maintainer.ts +404 -0
  177. package/src/processor.ts +415 -0
  178. package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
  179. package/src/python/minimax_lm.py +198 -0
  180. package/src/python/rlm_capture.py +215 -0
  181. package/src/query.ts +148 -0
  182. package/src/raw.ts +276 -0
  183. package/src/redact.ts +159 -0
  184. package/src/scope.ts +214 -0
  185. package/src/scripts/bulk-link-fix.ts +126 -0
  186. package/src/session-tools.ts +260 -0
  187. package/src/state.ts +271 -0
  188. package/src/store.ts +288 -0
  189. package/src/summarize.ts +273 -0
  190. package/src/tag-registry.ts +157 -0
  191. package/src/templates/index.md +17 -0
  192. package/src/templates/prompts.ts +274 -0
  193. package/src/templates/schema.md +116 -0
  194. package/src/tools.ts +1293 -0
  195. package/src/watcher.ts +101 -0
  196. package/src/webui/client/dist/index.html +1885 -0
  197. package/src/webui/server.ts +765 -0
  198. package/src/wiki.ts +550 -0
package/src/redact.ts ADDED
@@ -0,0 +1,159 @@
1
+ /**
2
+ * Secret redaction — strips API keys, tokens, passwords, and other
3
+ * sensitive values from wiki page content before writing.
4
+ *
5
+ * Runs automatically on every wiki_write to prevent secrets from
6
+ * being persisted in the knowledge base.
7
+ */
8
+
9
+ // -- Patterns ----------------------------------------------------------------
10
+
11
+ interface SecretPattern {
12
+ name: string;
13
+ /** Regex to match the full secret value. */
14
+ pattern: RegExp;
15
+ /** Replacement text. */
16
+ replacement: string;
17
+ }
18
+
19
+ /**
20
+ * Secret detection patterns — ordered from most specific to most general.
21
+ *
22
+ * Each pattern matches a specific type of secret. The replacement preserves
23
+ * the key type for documentation purposes while removing the actual value.
24
+ */
25
+ const SECRET_PATTERNS: SecretPattern[] = [
26
+ // --- Provider-specific API key formats ---
27
+ {
28
+ name: "OpenRouter API key",
29
+ pattern: /sk-or-v1-[a-f0-9]{64}/g,
30
+ replacement: "<REDACTED:openrouter-key>",
31
+ },
32
+ {
33
+ name: "OpenAI API key",
34
+ pattern: /sk-[a-zA-Z0-9]{48,}/g,
35
+ replacement: "<REDACTED:openai-key>",
36
+ },
37
+ {
38
+ name: "Anthropic API key",
39
+ pattern: /sk-ant-[a-zA-Z0-9_-]{40,}/g,
40
+ replacement: "<REDACTED:anthropic-key>",
41
+ },
42
+ {
43
+ name: "MiniMax API key",
44
+ pattern: /sk-cp-[a-zA-Z0-9_-]{40,}/g,
45
+ replacement: "<REDACTED:minimax-key>",
46
+ },
47
+ {
48
+ name: "GitHub PAT (fine-grained)",
49
+ pattern: /github_pat_[a-zA-Z0-9_]{60,}/g,
50
+ replacement: "<REDACTED:github-pat>",
51
+ },
52
+ {
53
+ name: "GitHub PAT (classic)",
54
+ pattern: /ghp_[a-zA-Z0-9]{36,}/g,
55
+ replacement: "<REDACTED:github-pat>",
56
+ },
57
+ {
58
+ name: "GitHub OAuth token",
59
+ pattern: /gho_[a-zA-Z0-9]{36,}/g,
60
+ replacement: "<REDACTED:github-oauth>",
61
+ },
62
+ {
63
+ name: "AWS Access Key ID",
64
+ pattern: /AKIA[0-9A-Z]{16}/g,
65
+ replacement: "<REDACTED:aws-key-id>",
66
+ },
67
+ {
68
+ name: "AWS Secret Access Key",
69
+ pattern: /(?<=aws_secret_access_key\s*[=:]\s*)[A-Za-z0-9/+=]{40}/g,
70
+ replacement: "<REDACTED:aws-secret>",
71
+ },
72
+ {
73
+ name: "Jina API key",
74
+ pattern: /jina_[a-zA-Z0-9_-]{30,}/g,
75
+ replacement: "<REDACTED:jina-key>",
76
+ },
77
+ {
78
+ name: "Bearer token",
79
+ pattern: /Bearer [a-zA-Z0-9._-]{30,}/g,
80
+ replacement: "Bearer <REDACTED>",
81
+ },
82
+
83
+ // --- Generic key/value patterns in YAML/config context ---
84
+ // Match: key: <long-alphanumeric-value> (in YAML blocks)
85
+ {
86
+ name: "YAML key value",
87
+ pattern: /(?<=^\s*key:\s*)[a-zA-Z0-9_-]{32,}/gm,
88
+ replacement: "<REDACTED>",
89
+ },
90
+ // Match: token: <long-alphanumeric-value>
91
+ {
92
+ name: "YAML token value",
93
+ pattern: /(?<=^\s*token:\s*)[a-zA-Z0-9_-]{32,}/gm,
94
+ replacement: "<REDACTED>",
95
+ },
96
+ // Match: secret: <long-alphanumeric-value>
97
+ {
98
+ name: "YAML secret value",
99
+ pattern: /(?<=^\s*secret:\s*)[a-zA-Z0-9_-]{32,}/gm,
100
+ replacement: "<REDACTED>",
101
+ },
102
+ // Match: password: <value> (any length, in YAML)
103
+ {
104
+ name: "YAML password value",
105
+ pattern: /(?<=^\s*password:\s*)\S+/gm,
106
+ replacement: "<REDACTED>",
107
+ },
108
+ // Match: api_key: / apiKey: <long-alphanumeric-value>
109
+ {
110
+ name: "YAML api_key value",
111
+ pattern: /(?<=^\s*(?:api_key|apiKey|api-key):\s*)[a-zA-Z0-9_-]{20,}/gm,
112
+ replacement: "<REDACTED>",
113
+ },
114
+
115
+ // --- Environment variable patterns ---
116
+ // Match: export SECRET_NAME=value or SECRET_NAME="value"
117
+ {
118
+ name: "Env var secret",
119
+ pattern: /(?<=(?:export\s+)?(?:API_KEY|SECRET|TOKEN|PASSWORD|PRIVATE_KEY|ACCESS_KEY)[A-Z_]*\s*=\s*["']?)[^\s"']{16,}/g,
120
+ replacement: "<REDACTED>",
121
+ },
122
+ ];
123
+
124
+ // -- Public API --------------------------------------------------------------
125
+
126
+ /**
127
+ * Redact secrets from text content.
128
+ * Returns the redacted text and count of redactions made.
129
+ */
130
+ export function redactSecrets(text: string): { text: string; redactions: number } {
131
+ let result = text;
132
+ let redactions = 0;
133
+
134
+ for (const { pattern, replacement } of SECRET_PATTERNS) {
135
+ // Reset regex state for global patterns
136
+ const re = new RegExp(pattern.source, pattern.flags);
137
+ const before = result;
138
+ result = result.replace(re, replacement);
139
+ if (result !== before) {
140
+ // Count how many replacements were made
141
+ const matches = before.match(new RegExp(pattern.source, pattern.flags));
142
+ redactions += matches?.length ?? 1;
143
+ }
144
+ }
145
+
146
+ return { text: result, redactions };
147
+ }
148
+
149
+ /**
150
+ * Check if text contains any detectable secrets.
151
+ * Faster than redactSecrets() — stops at first match.
152
+ */
153
+ export function containsSecrets(text: string): boolean {
154
+ for (const { pattern } of SECRET_PATTERNS) {
155
+ const re = new RegExp(pattern.source, pattern.flags);
156
+ if (re.test(text)) return true;
157
+ }
158
+ return false;
159
+ }
package/src/scope.ts ADDED
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Project scope detection and filtering.
3
+ *
4
+ * Determines which wiki pages are relevant to the current working context
5
+ * by auto-detecting project name from cwd (git remote, git root, dirname)
6
+ * or reading explicit config from .pi/wiki-scope.json.
7
+ */
8
+
9
+ import { execFile } from "node:child_process";
10
+ import { readFile } from "node:fs/promises";
11
+ import { basename, join } from "node:path";
12
+
13
+ // -- Types ------------------------------------------------------------------
14
+
15
+ export interface ProjectScope {
16
+ name: string;
17
+ include: string[]; // scope tags to include (always includes name)
18
+ exclude: string[]; // scope tags to exclude
19
+ source: "config" | "git-remote" | "git-root" | "dirname";
20
+ }
21
+
22
+ /** .pi/wiki-scope.json schema */
23
+ export interface ScopeConfig {
24
+ name: string;
25
+ include?: string[];
26
+ exclude?: string[];
27
+ }
28
+
29
+ // -- Helpers ----------------------------------------------------------------
30
+
31
+ /** Run a command and return trimmed stdout, or null on failure. */
32
+ function exec(
33
+ cmd: string,
34
+ args: string[],
35
+ cwd: string,
36
+ ): Promise<string | null> {
37
+ return new Promise((resolve) => {
38
+ execFile(cmd, args, { cwd }, (err, stdout) => {
39
+ if (err) {
40
+ resolve(null);
41
+ return;
42
+ }
43
+ const trimmed = (stdout ?? "").trim();
44
+ resolve(trimmed || null);
45
+ });
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Extract a repo name from a git remote URL.
51
+ *
52
+ * Handles:
53
+ * git@github.com:user/repo.git -> repo
54
+ * https://github.com/user/repo.git -> repo
55
+ * https://github.com/user/repo -> repo
56
+ * ssh://git@host/user/repo.git -> repo
57
+ */
58
+ export function extractRepoName(remoteUrl: string): string | null {
59
+ // Strip trailing slashes
60
+ let url = remoteUrl.trim().replace(/\/+$/, "");
61
+ // Strip .git suffix
62
+ url = url.replace(/\.git$/, "");
63
+ // Get the last path segment
64
+ const lastSlash = url.lastIndexOf("/");
65
+ const lastColon = url.lastIndexOf(":");
66
+ const sep = Math.max(lastSlash, lastColon);
67
+ if (sep < 0 || sep === url.length - 1) return null;
68
+ const name = url.slice(sep + 1);
69
+ return name || null;
70
+ }
71
+
72
+ // -- Public API -------------------------------------------------------------
73
+
74
+ /**
75
+ * Load .pi/wiki-scope.json from the given directory.
76
+ * Returns null if the file doesn't exist or is invalid JSON.
77
+ */
78
+ export async function loadScopeConfig(
79
+ cwd: string,
80
+ ): Promise<ScopeConfig | null> {
81
+ const configPath = join(cwd, ".pi", "wiki-scope.json");
82
+ let raw: string;
83
+ try {
84
+ raw = await readFile(configPath, "utf-8");
85
+ } catch {
86
+ return null;
87
+ }
88
+
89
+ let parsed: unknown;
90
+ try {
91
+ parsed = JSON.parse(raw);
92
+ } catch {
93
+ return null;
94
+ }
95
+
96
+ if (
97
+ typeof parsed !== "object" ||
98
+ parsed === null ||
99
+ typeof (parsed as Record<string, unknown>).name !== "string" ||
100
+ !(parsed as Record<string, unknown>).name
101
+ ) {
102
+ return null;
103
+ }
104
+
105
+ const obj = parsed as Record<string, unknown>;
106
+ const config: ScopeConfig = { name: obj.name as string };
107
+
108
+ if (Array.isArray(obj.include)) {
109
+ config.include = obj.include.filter(
110
+ (v: unknown) => typeof v === "string",
111
+ );
112
+ }
113
+ if (Array.isArray(obj.exclude)) {
114
+ config.exclude = obj.exclude.filter(
115
+ (v: unknown) => typeof v === "string",
116
+ );
117
+ }
118
+
119
+ return config;
120
+ }
121
+
122
+ /**
123
+ * Auto-detect project scope from the working directory.
124
+ *
125
+ * Detection priority:
126
+ * 1. .pi/wiki-scope.json (explicit config)
127
+ * 2. Git remote origin URL -> repo name
128
+ * 3. Git repository root directory name
129
+ * 4. cwd basename (fallback)
130
+ */
131
+ export async function detectScope(cwd: string): Promise<ProjectScope> {
132
+ // 1. Explicit config
133
+ const config = await loadScopeConfig(cwd);
134
+ if (config) {
135
+ const include = Array.from(
136
+ new Set([config.name, ...(config.include ?? [])]),
137
+ );
138
+ return {
139
+ name: config.name,
140
+ include,
141
+ exclude: config.exclude ?? [],
142
+ source: "config",
143
+ };
144
+ }
145
+
146
+ // 2. Git remote origin
147
+ const remoteUrl = await exec("git", ["remote", "get-url", "origin"], cwd);
148
+ if (remoteUrl) {
149
+ const repoName = extractRepoName(remoteUrl);
150
+ if (repoName) {
151
+ return {
152
+ name: repoName,
153
+ include: [repoName],
154
+ exclude: [],
155
+ source: "git-remote",
156
+ };
157
+ }
158
+ }
159
+
160
+ // 3. Git root directory name
161
+ const gitRoot = await exec(
162
+ "git",
163
+ ["rev-parse", "--show-toplevel"],
164
+ cwd,
165
+ );
166
+ if (gitRoot) {
167
+ const name = basename(gitRoot);
168
+ if (name) {
169
+ return {
170
+ name,
171
+ include: [name],
172
+ exclude: [],
173
+ source: "git-root",
174
+ };
175
+ }
176
+ }
177
+
178
+ // 4. Fallback to cwd basename
179
+ const name = basename(cwd);
180
+ return {
181
+ name,
182
+ include: [name],
183
+ exclude: [],
184
+ source: "dirname",
185
+ };
186
+ }
187
+
188
+ /**
189
+ * Check whether a page's scope tags match the project scope.
190
+ *
191
+ * Rules:
192
+ * - A page matches if its scope contains "global" OR any tag in projectScope.include
193
+ * - A page is excluded if its scope contains any tag in projectScope.exclude
194
+ * - Exclude takes priority over include
195
+ */
196
+ export function matchesScope(
197
+ pageScope: string[],
198
+ projectScope: ProjectScope,
199
+ ): boolean {
200
+ // Exclude takes priority
201
+ if (
202
+ projectScope.exclude.length > 0 &&
203
+ pageScope.some((tag) => projectScope.exclude.includes(tag))
204
+ ) {
205
+ return false;
206
+ }
207
+
208
+ // Match: "global" or any include tag
209
+ if (pageScope.includes("global")) {
210
+ return true;
211
+ }
212
+
213
+ return pageScope.some((tag) => projectScope.include.includes(tag));
214
+ }
@@ -0,0 +1,126 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * One-time bulk fix: auto-link slugs, normalize tags, sync frontmatter.links
4
+ * across all wiki pages.
5
+ *
6
+ * Usage: npx tsx src/scripts/bulk-link-fix.ts [--dry-run]
7
+ */
8
+
9
+ import { join } from "node:path";
10
+ import { homedir } from "node:os";
11
+ import {
12
+ listPages,
13
+ readPage,
14
+ writePage,
15
+ } from "../wiki.js";
16
+ import type { WikiPage } from "../wiki.js";
17
+ import { autoLinkSlugs, syncFrontmatterLinks } from "../link-utils.js";
18
+ import { normalizeTags, normalizeScopes } from "../tag-registry.js";
19
+
20
+ const WIKI_DIR = join(homedir(), ".pi", "wiki");
21
+ const dryRun = process.argv.includes("--dry-run");
22
+
23
+ async function main() {
24
+ console.log(`\nšŸ“š Bulk Wiki Fix — ${dryRun ? "DRY RUN" : "LIVE"}`);
25
+ console.log(` Wiki: ${WIKI_DIR}\n`);
26
+
27
+ // Load all pages
28
+ const refs = await listPages(WIKI_DIR);
29
+ const pages: WikiPage[] = [];
30
+ for (const ref of refs) {
31
+ const page = await readPage(WIKI_DIR, ref.category, ref.slug);
32
+ if (page) pages.push(page);
33
+ }
34
+
35
+ const allSlugs = new Set(pages.map(p => p.slug));
36
+ console.log(` Found ${pages.length} pages, ${allSlugs.size} unique slugs\n`);
37
+
38
+ let linksAdded = 0;
39
+ let tagsNormalized = 0;
40
+ let frontmatterSynced = 0;
41
+ let pagesChanged = 0;
42
+
43
+ for (const page of pages) {
44
+ const originalBody = page.body;
45
+ const originalTags = [...page.frontmatter.tags];
46
+ const originalLinks = [...page.frontmatter.links];
47
+
48
+ const originalScope = [...page.frontmatter.scope];
49
+
50
+ // 1. Normalize scopes first (tags depend on scope)
51
+ const newScope = normalizeScopes(page.frontmatter.scope);
52
+
53
+ // 2. Auto-link slug mentions in body
54
+ let newBody = autoLinkSlugs(originalBody, allSlugs, page.slug);
55
+
56
+ // 3. Sync frontmatter.links from body [[wikilinks]]
57
+ const newLinks = syncFrontmatterLinks(newBody);
58
+
59
+ // 4. Normalize tags (using normalized scope for dedup)
60
+ const newTags = normalizeTags(page.frontmatter.tags, newScope);
61
+
62
+ // Check what changed
63
+ const bodyChanged = newBody !== originalBody;
64
+ const linksChanged = JSON.stringify(newLinks.sort()) !== JSON.stringify(originalLinks.sort());
65
+ const tagsChanged = JSON.stringify(newTags) !== JSON.stringify(originalTags.sort());
66
+ const scopeChanged = JSON.stringify(newScope) !== JSON.stringify(originalScope.sort());
67
+
68
+ if (!bodyChanged && !linksChanged && !tagsChanged && !scopeChanged) continue;
69
+
70
+ pagesChanged++;
71
+
72
+ const addedLinksCount = newLinks.filter(l => !originalLinks.includes(l)).length;
73
+ linksAdded += addedLinksCount;
74
+ if (tagsChanged) tagsNormalized++;
75
+ if (linksChanged) frontmatterSynced++;
76
+
77
+ // Report
78
+ const changes: string[] = [];
79
+ if (scopeChanged) {
80
+ changes.push(`scope: [${originalScope.join(", ")}] → [${newScope.join(", ")}]`);
81
+ }
82
+ if (bodyChanged) {
83
+ const addedLinks = newLinks.filter(l => !originalLinks.includes(l));
84
+ changes.push(`+${addedLinks.length} wikilinks: ${addedLinks.join(", ")}`);
85
+ }
86
+ if (tagsChanged) {
87
+ changes.push(`tags: [${originalTags.join(", ")}] → [${newTags.join(", ")}]`);
88
+ }
89
+ if (linksChanged && !bodyChanged) {
90
+ changes.push(`frontmatter.links synced`);
91
+ }
92
+
93
+ console.log(` ${dryRun ? "WOULD FIX" : "FIXED"} ${page.category}/${page.slug}`);
94
+ for (const c of changes) {
95
+ console.log(` → ${c}`);
96
+ }
97
+
98
+ if (!dryRun) {
99
+ const updatedPage: WikiPage = {
100
+ ...page,
101
+ body: newBody,
102
+ frontmatter: {
103
+ ...page.frontmatter,
104
+ scope: newScope,
105
+ tags: newTags,
106
+ links: newLinks,
107
+ updated: new Date().toISOString(),
108
+ },
109
+ };
110
+ await writePage(WIKI_DIR, updatedPage);
111
+ }
112
+ }
113
+
114
+ console.log(`\n━━━ Summary ━━━`);
115
+ console.log(` Pages changed: ${pagesChanged} / ${pages.length}`);
116
+ console.log(` Wikilinks added: ${linksAdded}`);
117
+ console.log(` Tags normalized: ${tagsNormalized}`);
118
+ console.log(` Frontmatter synced: ${frontmatterSynced}`);
119
+ if (dryRun) console.log(`\n (dry run — no files modified, re-run without --dry-run to apply)`);
120
+ console.log();
121
+ }
122
+
123
+ main().catch(err => {
124
+ console.error("Error:", err);
125
+ process.exit(1);
126
+ });