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.
- package/CHANGELOG.md +214 -0
- package/README.md +305 -0
- package/SETUP.md +171 -0
- package/dist/capture.d.ts +34 -0
- package/dist/capture.d.ts.map +1 -0
- package/dist/capture.js +461 -0
- package/dist/capture.js.map +1 -0
- package/dist/cli.d.ts +15 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +405 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands.d.ts +15 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +786 -0
- package/dist/commands.js.map +1 -0
- package/dist/context.d.ts +61 -0
- package/dist/context.d.ts.map +1 -0
- package/dist/context.js +289 -0
- package/dist/context.js.map +1 -0
- package/dist/daemon.d.ts +49 -0
- package/dist/daemon.d.ts.map +1 -0
- package/dist/daemon.js +226 -0
- package/dist/daemon.js.map +1 -0
- package/dist/frontmatter.d.ts +59 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +170 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/gepa/dataset.d.ts +36 -0
- package/dist/gepa/dataset.d.ts.map +1 -0
- package/dist/gepa/dataset.js +148 -0
- package/dist/gepa/dataset.js.map +1 -0
- package/dist/gepa/evaluator.d.ts +51 -0
- package/dist/gepa/evaluator.d.ts.map +1 -0
- package/dist/gepa/evaluator.js +317 -0
- package/dist/gepa/evaluator.js.map +1 -0
- package/dist/gepa/index.d.ts +66 -0
- package/dist/gepa/index.d.ts.map +1 -0
- package/dist/gepa/index.js +240 -0
- package/dist/gepa/index.js.map +1 -0
- package/dist/gepa/llm.d.ts +35 -0
- package/dist/gepa/llm.d.ts.map +1 -0
- package/dist/gepa/llm.js +268 -0
- package/dist/gepa/llm.js.map +1 -0
- package/dist/gepa/optimized/capture-prompt.txt +65 -0
- package/dist/gepa/optimizer.d.ts +29 -0
- package/dist/gepa/optimizer.d.ts.map +1 -0
- package/dist/gepa/optimizer.js +249 -0
- package/dist/gepa/optimizer.js.map +1 -0
- package/dist/gepa/targets.d.ts +39 -0
- package/dist/gepa/targets.d.ts.map +1 -0
- package/dist/gepa/targets.js +326 -0
- package/dist/gepa/targets.js.map +1 -0
- package/dist/gepa/types.d.ts +168 -0
- package/dist/gepa/types.d.ts.map +1 -0
- package/dist/gepa/types.js +124 -0
- package/dist/gepa/types.js.map +1 -0
- package/dist/index.d.ts +21 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +294 -0
- package/dist/index.js.map +1 -0
- package/dist/ingest.d.ts +88 -0
- package/dist/ingest.d.ts.map +1 -0
- package/dist/ingest.js +105 -0
- package/dist/ingest.js.map +1 -0
- package/dist/link-discovery.d.ts +40 -0
- package/dist/link-discovery.d.ts.map +1 -0
- package/dist/link-discovery.js +122 -0
- package/dist/link-discovery.js.map +1 -0
- package/dist/link-utils.d.ts +30 -0
- package/dist/link-utils.d.ts.map +1 -0
- package/dist/link-utils.js +145 -0
- package/dist/link-utils.js.map +1 -0
- package/dist/lint.d.ts +39 -0
- package/dist/lint.d.ts.map +1 -0
- package/dist/lint.js +827 -0
- package/dist/lint.js.map +1 -0
- package/dist/maintainer.d.ts +24 -0
- package/dist/maintainer.d.ts.map +1 -0
- package/dist/maintainer.js +358 -0
- package/dist/maintainer.js.map +1 -0
- package/dist/processor.d.ts +14 -0
- package/dist/processor.d.ts.map +1 -0
- package/dist/processor.js +361 -0
- package/dist/processor.js.map +1 -0
- package/dist/query.d.ts +69 -0
- package/dist/query.d.ts.map +1 -0
- package/dist/query.js +107 -0
- package/dist/query.js.map +1 -0
- package/dist/raw.d.ts +57 -0
- package/dist/raw.d.ts.map +1 -0
- package/dist/raw.js +219 -0
- package/dist/raw.js.map +1 -0
- package/dist/redact.d.ts +21 -0
- package/dist/redact.d.ts.map +1 -0
- package/dist/redact.js +143 -0
- package/dist/redact.js.map +1 -0
- package/dist/scope.d.ts +54 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +161 -0
- package/dist/scope.js.map +1 -0
- package/dist/scripts/bulk-link-fix.d.ts +9 -0
- package/dist/scripts/bulk-link-fix.d.ts.map +1 -0
- package/dist/scripts/bulk-link-fix.js +106 -0
- package/dist/scripts/bulk-link-fix.js.map +1 -0
- package/dist/session-tools.d.ts +33 -0
- package/dist/session-tools.d.ts.map +1 -0
- package/dist/session-tools.js +211 -0
- package/dist/session-tools.js.map +1 -0
- package/dist/state.d.ts +54 -0
- package/dist/state.d.ts.map +1 -0
- package/dist/state.js +176 -0
- package/dist/state.js.map +1 -0
- package/dist/store.d.ts +56 -0
- package/dist/store.d.ts.map +1 -0
- package/dist/store.js +227 -0
- package/dist/store.js.map +1 -0
- package/dist/summarize.d.ts +56 -0
- package/dist/summarize.d.ts.map +1 -0
- package/dist/summarize.js +231 -0
- package/dist/summarize.js.map +1 -0
- package/dist/tag-registry.d.ts +56 -0
- package/dist/tag-registry.d.ts.map +1 -0
- package/dist/tag-registry.js +146 -0
- package/dist/tag-registry.js.map +1 -0
- package/dist/templates/prompts.d.ts +63 -0
- package/dist/templates/prompts.d.ts.map +1 -0
- package/dist/templates/prompts.js +260 -0
- package/dist/templates/prompts.js.map +1 -0
- package/dist/tools.d.ts +26 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +1033 -0
- package/dist/tools.js.map +1 -0
- package/dist/watcher.d.ts +25 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +84 -0
- package/dist/watcher.js.map +1 -0
- package/dist/webui/client/dist/index.html +1885 -0
- package/dist/webui/server.d.ts +31 -0
- package/dist/webui/server.d.ts.map +1 -0
- package/dist/webui/server.js +566 -0
- package/dist/webui/server.js.map +1 -0
- package/dist/wiki.d.ts +103 -0
- package/dist/wiki.d.ts.map +1 -0
- package/dist/wiki.js +428 -0
- package/dist/wiki.js.map +1 -0
- package/package.json +142 -0
- package/scripts/gepa/__pycache__/dataset.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/lm_providers.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/metric.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/program.cpython-313.pyc +0 -0
- package/scripts/gepa/__pycache__/wiki_reader.cpython-313.pyc +0 -0
- package/scripts/gepa/dataset.py +192 -0
- package/scripts/gepa/lm_providers.py +472 -0
- package/scripts/gepa/metric.py +279 -0
- package/scripts/gepa/optimize.py +367 -0
- package/scripts/gepa/program.py +99 -0
- package/scripts/gepa/pyproject.toml +19 -0
- package/scripts/gepa/run-all.sh +53 -0
- package/scripts/gepa/uv.lock +2241 -0
- package/scripts/gepa/wiki_reader.py +113 -0
- package/skills/para/SKILL.md +77 -0
- package/skills/setup/SKILL.md +20 -0
- package/src/capture.ts +563 -0
- package/src/cli.ts +416 -0
- package/src/commands.ts +878 -0
- package/src/context.ts +390 -0
- package/src/daemon.ts +288 -0
- package/src/frontmatter.ts +219 -0
- package/src/gepa/index.ts +287 -0
- package/src/gepa/optimized/capture-prompt.txt +65 -0
- package/src/index.ts +384 -0
- package/src/ingest.ts +177 -0
- package/src/link-discovery.ts +170 -0
- package/src/link-utils.ts +169 -0
- package/src/lint.ts +1018 -0
- package/src/maintainer.ts +404 -0
- package/src/processor.ts +415 -0
- package/src/python/__pycache__/minimax_lm.cpython-312.pyc +0 -0
- package/src/python/minimax_lm.py +198 -0
- package/src/python/rlm_capture.py +215 -0
- package/src/query.ts +148 -0
- package/src/raw.ts +276 -0
- package/src/redact.ts +159 -0
- package/src/scope.ts +214 -0
- package/src/scripts/bulk-link-fix.ts +126 -0
- package/src/session-tools.ts +260 -0
- package/src/state.ts +271 -0
- package/src/store.ts +288 -0
- package/src/summarize.ts +273 -0
- package/src/tag-registry.ts +157 -0
- package/src/templates/index.md +17 -0
- package/src/templates/prompts.ts +274 -0
- package/src/templates/schema.md +116 -0
- package/src/tools.ts +1293 -0
- package/src/watcher.ts +101 -0
- package/src/webui/client/dist/index.html +1885 -0
- package/src/webui/server.ts +765 -0
- 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
|
+
});
|