@creativeaitools/agent-wiki 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENT-WIKI-SPEC-v2.md +2584 -0
- package/AGENTS.md +314 -0
- package/INBOX.md +19 -0
- package/LICENSE +21 -0
- package/ONBOARD.md +373 -0
- package/README.md +429 -0
- package/WIKI.md +706 -0
- package/_system/config.example.json +105 -0
- package/dist/src/catalog.js +66 -0
- package/dist/src/cli.js +330 -0
- package/dist/src/compile.js +104 -0
- package/dist/src/config.js +84 -0
- package/dist/src/lifecycle.js +171 -0
- package/dist/src/migrate.js +26 -0
- package/dist/src/onboard.js +159 -0
- package/dist/src/page.js +188 -0
- package/dist/src/registry.js +74 -0
- package/dist/src/schedule-prompts.js +74 -0
- package/dist/src/upgrade.js +215 -0
- package/dist/src/wiki-utils.js +112 -0
- package/dist/src/workspace.js +198 -0
- package/package.json +54 -0
- package/skills/compile-wiki/SKILL.md +140 -0
- package/skills/extract-knowledge-primitives/SKILL.md +350 -0
- package/skills/import-link/SKILL.md +101 -0
- package/skills/import-link/config.json +12 -0
- package/skills/process-inbox/SKILL.md +255 -0
- package/skills/process-workspace-sources/SKILL.md +127 -0
- package/skills/update-overview/SKILL.md +140 -0
- package/skills/write-synthesis/SKILL.md +154 -0
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
export const CONFIG_PATH = "_system/config.json";
|
|
4
|
+
export const CONFIG_EXAMPLE_PATH = "_system/config.example.json";
|
|
5
|
+
export const DEFAULT_WORKSPACE_WIKI_DIR = "wiki";
|
|
6
|
+
export const VALID_WIKI_TYPES = new Set(["vault", "workspace"]);
|
|
7
|
+
const DEFAULT_SCAN = {
|
|
8
|
+
includeExtensions: [".md", ".markdown", ".txt", ".pdf", ".docx", ".csv", ".json", ".yaml", ".yml"],
|
|
9
|
+
excludeDirs: [
|
|
10
|
+
".git",
|
|
11
|
+
".hg",
|
|
12
|
+
".svn",
|
|
13
|
+
".obsidian",
|
|
14
|
+
".venv",
|
|
15
|
+
"venv",
|
|
16
|
+
"env",
|
|
17
|
+
"__pycache__",
|
|
18
|
+
".pytest_cache",
|
|
19
|
+
".mypy_cache",
|
|
20
|
+
".ruff_cache",
|
|
21
|
+
"node_modules",
|
|
22
|
+
"dist",
|
|
23
|
+
"build",
|
|
24
|
+
".next",
|
|
25
|
+
".turbo",
|
|
26
|
+
".cache",
|
|
27
|
+
"_system",
|
|
28
|
+
"reports",
|
|
29
|
+
"target",
|
|
30
|
+
"vendor"
|
|
31
|
+
],
|
|
32
|
+
excludeFileGlobs: ["*.lock", "package-lock.json", "pnpm-lock.yaml", "yarn.lock", "uv.lock", "poetry.lock"]
|
|
33
|
+
};
|
|
34
|
+
export function loadConfig(root = ".") {
|
|
35
|
+
const rootPath = resolve(root);
|
|
36
|
+
const data = readJsonObject(joinPath(rootPath, CONFIG_PATH)) ?? readJsonObject(joinPath(rootPath, CONFIG_EXAMPLE_PATH)) ?? {};
|
|
37
|
+
const rawType = String(data.wikiType ?? data.wiki_type ?? "vault");
|
|
38
|
+
const wikiType = VALID_WIKI_TYPES.has(rawType) ? rawType : "vault";
|
|
39
|
+
const workspace = isObject(data.workspace) ? data.workspace : {};
|
|
40
|
+
const workspaceRootRaw = workspace.root ?? data.workspaceRoot;
|
|
41
|
+
let workspaceRoot = null;
|
|
42
|
+
if (typeof workspaceRootRaw === "string" && workspaceRootRaw.length > 0) {
|
|
43
|
+
workspaceRoot = resolve(rootPath, workspaceRootRaw);
|
|
44
|
+
}
|
|
45
|
+
const wikiDir = cleanWikiDir(String(workspace.wikiDir ?? data.wikiDir ?? DEFAULT_WORKSPACE_WIKI_DIR));
|
|
46
|
+
const scan = isObject(workspace.scan) ? workspace.scan : {};
|
|
47
|
+
return {
|
|
48
|
+
wikiType,
|
|
49
|
+
root: rootPath,
|
|
50
|
+
workspaceRoot,
|
|
51
|
+
wikiDir,
|
|
52
|
+
workspaceScan: {
|
|
53
|
+
includeExtensions: tupleFromConfig(scan, "includeExtensions", DEFAULT_SCAN.includeExtensions).map((ext) => ext.startsWith(".") ? ext : `.${ext}`),
|
|
54
|
+
excludeDirs: tupleFromConfig(scan, "excludeDirs", DEFAULT_SCAN.excludeDirs),
|
|
55
|
+
excludeFileGlobs: tupleFromConfig(scan, "excludeFileGlobs", DEFAULT_SCAN.excludeFileGlobs)
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
export function cleanWikiDir(value) {
|
|
60
|
+
return value.trim().replace(/^\/+|\/+$/g, "") || DEFAULT_WORKSPACE_WIKI_DIR;
|
|
61
|
+
}
|
|
62
|
+
export function readJsonObject(path) {
|
|
63
|
+
try {
|
|
64
|
+
const data = JSON.parse(readFileSync(path, "utf8"));
|
|
65
|
+
return isObject(data) ? data : null;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
export function isObject(value) {
|
|
72
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
73
|
+
}
|
|
74
|
+
function tupleFromConfig(data, key, defaults) {
|
|
75
|
+
const value = data[key];
|
|
76
|
+
if (!Array.isArray(value)) {
|
|
77
|
+
return defaults;
|
|
78
|
+
}
|
|
79
|
+
const items = value.filter((item) => typeof item === "string" && item.length > 0);
|
|
80
|
+
return items.length > 0 ? items : defaults;
|
|
81
|
+
}
|
|
82
|
+
function joinPath(root, relative) {
|
|
83
|
+
return resolve(root, relative);
|
|
84
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { CONFIG_PATH, DEFAULT_WORKSPACE_WIKI_DIR, VALID_WIKI_TYPES, cleanWikiDir, readJsonObject } from "./config.js";
|
|
5
|
+
export const CONTENT_FOLDERS = ["sources", "sources/parts", "entities", "concepts", "claims", "syntheses", "questions", "_attachments", "_archive"];
|
|
6
|
+
export const VAULT_RUNTIME_FOLDERS = ["_inbox", "_inbox/trash", "raw"];
|
|
7
|
+
export const GENERATED_FOLDERS = ["reports", "_system/cache", "_system/indexes", "_system/logs", "_system/state"];
|
|
8
|
+
export const SYSTEM_FOLDERS = ["skills"];
|
|
9
|
+
export const REQUIRED_TEMPLATE_FILES = [
|
|
10
|
+
"AGENTS.md",
|
|
11
|
+
"WIKI.md",
|
|
12
|
+
"README.md",
|
|
13
|
+
"package.json",
|
|
14
|
+
"skills/compile-wiki/SKILL.md",
|
|
15
|
+
"skills/extract-knowledge-primitives/SKILL.md"
|
|
16
|
+
];
|
|
17
|
+
export const TEMPLATE_ROOT_FILES = ["AGENTS.md", "WIKI.md", "README.md", "ONBOARD.md", "INBOX.md", "AGENT-WIKI-SPEC-v2.md", "package.json"];
|
|
18
|
+
export const TEMPLATE_DIRECTORIES = ["skills"];
|
|
19
|
+
export const TEMPLATE_OPTIONAL_FILES = ["_system/config.example.json"];
|
|
20
|
+
export function resolveInitPaths(options) {
|
|
21
|
+
if (!VALID_WIKI_TYPES.has(options.wikiType)) {
|
|
22
|
+
throw new Error(`wiki_type must be one of: ${Array.from(VALID_WIKI_TYPES).sort().join(", ")}`);
|
|
23
|
+
}
|
|
24
|
+
const wikiDir = cleanWikiDir(options.wikiDir ?? DEFAULT_WORKSPACE_WIKI_DIR);
|
|
25
|
+
if (options.wikiType === "workspace") {
|
|
26
|
+
const workspace = resolve(options.workspaceRoot ?? options.root ?? ".");
|
|
27
|
+
return { workspaceRoot: workspace, wikiRoot: resolve(workspace, wikiDir) };
|
|
28
|
+
}
|
|
29
|
+
return { workspaceRoot: null, wikiRoot: resolve(options.root ?? ".") };
|
|
30
|
+
}
|
|
31
|
+
export function initWiki(options) {
|
|
32
|
+
const { workspaceRoot, wikiRoot } = resolveInitPaths(options);
|
|
33
|
+
const created = createRequiredFolders(wikiRoot);
|
|
34
|
+
let configWritten = false;
|
|
35
|
+
if (options.writeConfig) {
|
|
36
|
+
writeLocalConfig(wikiRoot, options.wikiType, workspaceRoot, options.wikiDir ?? DEFAULT_WORKSPACE_WIKI_DIR);
|
|
37
|
+
configWritten = true;
|
|
38
|
+
}
|
|
39
|
+
const templateCopied = options.withTemplate
|
|
40
|
+
? copyTemplateFiles(options.templateRoot ?? defaultTemplateRoot(), wikiRoot)
|
|
41
|
+
: [];
|
|
42
|
+
return { wikiType: options.wikiType, workspaceRoot, wikiRoot, created, configWritten, templateCopied };
|
|
43
|
+
}
|
|
44
|
+
export function createRequiredFolders(wikiRoot) {
|
|
45
|
+
const folders = [...CONTENT_FOLDERS, ...VAULT_RUNTIME_FOLDERS, ...GENERATED_FOLDERS, ...SYSTEM_FOLDERS];
|
|
46
|
+
const created = [];
|
|
47
|
+
for (const folder of folders) {
|
|
48
|
+
const path = join(wikiRoot, folder);
|
|
49
|
+
if (!existsSync(path)) {
|
|
50
|
+
mkdirSync(path, { recursive: true });
|
|
51
|
+
created.push(path);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return created;
|
|
55
|
+
}
|
|
56
|
+
export function writeLocalConfig(wikiRoot, wikiType, workspaceRoot, wikiDir) {
|
|
57
|
+
const configPath = join(wikiRoot, CONFIG_PATH);
|
|
58
|
+
mkdirSync(dirname(configPath), { recursive: true });
|
|
59
|
+
const existing = readJsonObject(configPath) ?? {};
|
|
60
|
+
existing.schemaVersion = 1;
|
|
61
|
+
existing.wikiType = wikiType;
|
|
62
|
+
const workspace = typeof existing.workspace === "object" && existing.workspace !== null && !Array.isArray(existing.workspace)
|
|
63
|
+
? existing.workspace
|
|
64
|
+
: {};
|
|
65
|
+
workspace.root = workspaceRoot;
|
|
66
|
+
workspace.wikiDir = cleanWikiDir(wikiDir);
|
|
67
|
+
existing.workspace = workspace;
|
|
68
|
+
writeFileSync(configPath, `${JSON.stringify(existing, null, 2)}\n`, "utf8");
|
|
69
|
+
}
|
|
70
|
+
export function defaultTemplateRoot() {
|
|
71
|
+
return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
72
|
+
}
|
|
73
|
+
export function copyTemplateFiles(sourceRoot, wikiRoot) {
|
|
74
|
+
const copied = [];
|
|
75
|
+
for (const name of [...TEMPLATE_ROOT_FILES, ...TEMPLATE_OPTIONAL_FILES]) {
|
|
76
|
+
const source = join(sourceRoot, name);
|
|
77
|
+
if (existsSync(source) && statSync(source).isFile() && copyFileIfMissing(source, join(wikiRoot, name))) {
|
|
78
|
+
copied.push(join(wikiRoot, name));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const name of TEMPLATE_DIRECTORIES) {
|
|
82
|
+
const source = join(sourceRoot, name);
|
|
83
|
+
if (existsSync(source) && statSync(source).isDirectory()) {
|
|
84
|
+
copied.push(...copyTreeIfMissing(source, join(wikiRoot, name)));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return copied;
|
|
88
|
+
}
|
|
89
|
+
export function doctorWiki(wikiRoot, wikiType) {
|
|
90
|
+
const root = resolve(wikiRoot);
|
|
91
|
+
if (!existsSync(root)) {
|
|
92
|
+
return [{ level: "error", code: "wiki_root_missing", message: "Wiki root does not exist.", path: root }];
|
|
93
|
+
}
|
|
94
|
+
if (!statSync(root).isDirectory()) {
|
|
95
|
+
return [{ level: "error", code: "wiki_root_not_directory", message: "Wiki root is not a directory.", path: root }];
|
|
96
|
+
}
|
|
97
|
+
const config = readJsonObject(join(root, CONFIG_PATH));
|
|
98
|
+
const detectedType = wikiType ?? detectWikiType(config);
|
|
99
|
+
const issues = [];
|
|
100
|
+
if (!VALID_WIKI_TYPES.has(detectedType)) {
|
|
101
|
+
issues.push({ level: "error", code: "invalid_wiki_type", message: `Invalid wiki type: ${detectedType}` });
|
|
102
|
+
}
|
|
103
|
+
for (const folder of requiredFoldersForDoctor(detectedType)) {
|
|
104
|
+
const path = join(root, folder);
|
|
105
|
+
if (!existsSync(path) || !statSync(path).isDirectory()) {
|
|
106
|
+
issues.push({ level: "error", code: "missing_folder", message: `Required folder is missing: ${folder}`, path });
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
for (const fileName of REQUIRED_TEMPLATE_FILES) {
|
|
110
|
+
const path = join(root, fileName);
|
|
111
|
+
if (!existsSync(path) || !statSync(path).isFile()) {
|
|
112
|
+
issues.push({ level: "warning", code: "missing_template_file", message: `Template file is missing: ${fileName}`, path });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (config === null) {
|
|
116
|
+
issues.push({
|
|
117
|
+
level: "info",
|
|
118
|
+
code: "local_config_missing",
|
|
119
|
+
message: "_system/config.json is not present; defaults or _system/config.example.json will be used.",
|
|
120
|
+
path: join(root, CONFIG_PATH)
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
else if (!VALID_WIKI_TYPES.has(String(config.wikiType))) {
|
|
124
|
+
issues.push({ level: "error", code: "config_invalid_wiki_type", message: "_system/config.json has missing or invalid wikiType.", path: join(root, CONFIG_PATH) });
|
|
125
|
+
}
|
|
126
|
+
return issues;
|
|
127
|
+
}
|
|
128
|
+
export function requiredFoldersForDoctor(wikiType) {
|
|
129
|
+
return [...CONTENT_FOLDERS, ...VAULT_RUNTIME_FOLDERS, ...GENERATED_FOLDERS, ...SYSTEM_FOLDERS];
|
|
130
|
+
}
|
|
131
|
+
export function detectWikiType(config) {
|
|
132
|
+
return config && VALID_WIKI_TYPES.has(String(config.wikiType)) ? String(config.wikiType) : "vault";
|
|
133
|
+
}
|
|
134
|
+
export function issuesToJson(issues) {
|
|
135
|
+
return JSON.stringify(issues, null, 2);
|
|
136
|
+
}
|
|
137
|
+
export function issuesToText(issues) {
|
|
138
|
+
if (issues.length === 0) {
|
|
139
|
+
return "Doctor passed: no issues found.";
|
|
140
|
+
}
|
|
141
|
+
return issues.map(formatIssue).join("\n");
|
|
142
|
+
}
|
|
143
|
+
function formatIssue(issue) {
|
|
144
|
+
const suffix = issue.path ? ` (${issue.path})` : "";
|
|
145
|
+
return `${issue.level.toUpperCase().padEnd(7)} ${issue.code}: ${issue.message}${suffix}`;
|
|
146
|
+
}
|
|
147
|
+
function copyTreeIfMissing(source, destination) {
|
|
148
|
+
const copied = [];
|
|
149
|
+
for (const entry of readdirSync(source, { withFileTypes: true })) {
|
|
150
|
+
const sourcePath = join(source, entry.name);
|
|
151
|
+
const destinationPath = join(destination, entry.name);
|
|
152
|
+
if (entry.isDirectory()) {
|
|
153
|
+
copied.push(...copyTreeIfMissing(sourcePath, destinationPath));
|
|
154
|
+
}
|
|
155
|
+
else if (!shouldSkipTemplateFile(relative(source, sourcePath)) && copyFileIfMissing(sourcePath, destinationPath)) {
|
|
156
|
+
copied.push(destinationPath);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return copied;
|
|
160
|
+
}
|
|
161
|
+
function shouldSkipTemplateFile(path) {
|
|
162
|
+
return path.split(/[\\/]/).includes("__pycache__") || path.endsWith(".pyc");
|
|
163
|
+
}
|
|
164
|
+
function copyFileIfMissing(source, destination) {
|
|
165
|
+
if (existsSync(destination)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
mkdirSync(dirname(destination), { recursive: true });
|
|
169
|
+
copyFileSync(source, destination);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { pathToWikilink, refToWikilink, walkMarkdownPages } from "./wiki-utils.js";
|
|
4
|
+
const ID_FIELDS = ["sourcePages", "derivedClaims", "relatedPages", "relatedClaims", "extractedEntities", "extractedConcepts", "extractedClaims", "extractedQuestions"];
|
|
5
|
+
export function migrateRefs(args) {
|
|
6
|
+
const root = String(args.root || process.cwd());
|
|
7
|
+
const write = Boolean(args.write);
|
|
8
|
+
let changed = 0;
|
|
9
|
+
for (const page of walkMarkdownPages(root)) {
|
|
10
|
+
const path = join(root, page.path);
|
|
11
|
+
let text = readFileSync(path, "utf8");
|
|
12
|
+
const original = text;
|
|
13
|
+
for (const field of ID_FIELDS) {
|
|
14
|
+
text = text.replace(new RegExp(`(^${field}:\\n(?:\\s+-\\s+)([^\\n]+)(?:\\n\\s+-\\s+[^\\n]+)*)`, "gm"), (block) => block.replace(/^(\s+-\s+)(.+)$/gm, (_m, prefix, value) => `${prefix}${refToWikilink(value.trim())}`));
|
|
15
|
+
}
|
|
16
|
+
text = text.replace(/^(originPath:\s*)(.+)$/gm, (_m, prefix, value) => `${prefix}${pathToWikilink(String(value).trim())}`);
|
|
17
|
+
if (text !== original) {
|
|
18
|
+
changed += 1;
|
|
19
|
+
if (write)
|
|
20
|
+
writeFileSync(path, text, "utf8");
|
|
21
|
+
console.log(`${write ? "updated" : "would update"} ${page.path}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
console.log(`${write ? "Updated" : "Would update"} ${changed} files.`);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { loadConfig, readJsonObject } from "./config.js";
|
|
5
|
+
import { doctorWiki, writeLocalConfig } from "./lifecycle.js";
|
|
6
|
+
import { writeJson } from "./wiki-utils.js";
|
|
7
|
+
const CONVERTER_COMMANDS = ["markitdown", "pymupdf4llm", "marker_single"];
|
|
8
|
+
export function onboard(args) {
|
|
9
|
+
const root = resolve(String(args["wiki-root"] || args.root || process.cwd()));
|
|
10
|
+
if (args["write-config"])
|
|
11
|
+
return writeConfig(root, args);
|
|
12
|
+
if (!args.check)
|
|
13
|
+
throw new Error("onboard requires --check or --write-config");
|
|
14
|
+
const report = buildOnboardingReport(root);
|
|
15
|
+
if (args.questions) {
|
|
16
|
+
console.log(renderQuestions(report));
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
console.log(JSON.stringify(report, null, args.compact ? undefined : 2));
|
|
20
|
+
}
|
|
21
|
+
return report.summary.ready ? 0 : 1;
|
|
22
|
+
}
|
|
23
|
+
function writeConfig(root, args) {
|
|
24
|
+
const wikiType = String(args.type || loadConfig(root).wikiType);
|
|
25
|
+
if (wikiType !== "vault" && wikiType !== "workspace")
|
|
26
|
+
throw new Error("--type must be vault or workspace");
|
|
27
|
+
const wikiDir = String(args["wiki-dir"] || "wiki");
|
|
28
|
+
const workspaceRoot = wikiType === "workspace" ? String(args["workspace-root"] || "") || null : null;
|
|
29
|
+
writeLocalConfig(root, wikiType, workspaceRoot, wikiDir);
|
|
30
|
+
const conversionPolicy = String(args.conversion || "disabled");
|
|
31
|
+
const conversionEnabled = conversionPolicy !== "disabled";
|
|
32
|
+
writeJson(join(root, "_system/config.json"), {
|
|
33
|
+
schemaVersion: 1,
|
|
34
|
+
wikiType,
|
|
35
|
+
workspace: { root: workspaceRoot, wikiDir },
|
|
36
|
+
pythonCommand: args["python-command"] || null,
|
|
37
|
+
conversion: { enabled: conversionEnabled, policy: conversionPolicy }
|
|
38
|
+
});
|
|
39
|
+
console.log("Wrote _system/config.json");
|
|
40
|
+
return 0;
|
|
41
|
+
}
|
|
42
|
+
export function buildOnboardingReport(root) {
|
|
43
|
+
const config = loadConfig(root);
|
|
44
|
+
const doctorIssues = doctorWiki(root, config.wikiType);
|
|
45
|
+
const configPath = join(root, "_system/config.json");
|
|
46
|
+
const importLinkPath = join(root, "skills/import-link/config.json");
|
|
47
|
+
const importLinkConfig = readJsonObject(importLinkPath);
|
|
48
|
+
const requiredDocs = ["AGENTS.md", "WIKI.md", "ONBOARD.md", "AGENT-WIKI-SPEC-v2.md"];
|
|
49
|
+
const requiredSkills = [
|
|
50
|
+
"skills/process-inbox/SKILL.md",
|
|
51
|
+
"skills/extract-knowledge-primitives/SKILL.md",
|
|
52
|
+
"skills/compile-wiki/SKILL.md",
|
|
53
|
+
"skills/write-synthesis/SKILL.md",
|
|
54
|
+
"skills/update-overview/SKILL.md"
|
|
55
|
+
];
|
|
56
|
+
const missingDocs = missingFiles(root, requiredDocs);
|
|
57
|
+
const missingSkills = missingFiles(root, requiredSkills);
|
|
58
|
+
const configExists = existsSync(configPath);
|
|
59
|
+
const importLinkConfigured = Boolean(importLinkConfig?.configured);
|
|
60
|
+
const errors = doctorIssues.filter((issue) => issue.level === "error");
|
|
61
|
+
return {
|
|
62
|
+
schemaVersion: 1,
|
|
63
|
+
command: "agent-wiki onboard --check",
|
|
64
|
+
root,
|
|
65
|
+
platform: {
|
|
66
|
+
os: process.platform,
|
|
67
|
+
arch: process.arch,
|
|
68
|
+
node: process.version
|
|
69
|
+
},
|
|
70
|
+
wiki: {
|
|
71
|
+
type: config.wikiType,
|
|
72
|
+
configExists,
|
|
73
|
+
configPath,
|
|
74
|
+
obsidianVault: existsDir(join(root, ".obsidian")),
|
|
75
|
+
missingDocs,
|
|
76
|
+
missingSkills
|
|
77
|
+
},
|
|
78
|
+
doctor: {
|
|
79
|
+
passed: errors.length === 0,
|
|
80
|
+
issues: doctorIssues
|
|
81
|
+
},
|
|
82
|
+
tools: {
|
|
83
|
+
agentWiki: { available: true, command: "agent-wiki" },
|
|
84
|
+
python: [probeCommand("python3", ["--version"]), probeCommand("python", ["--version"])],
|
|
85
|
+
converters: CONVERTER_COMMANDS.map((command) => probeCommand(command, ["--version"]))
|
|
86
|
+
},
|
|
87
|
+
importLink: {
|
|
88
|
+
configPath: importLinkPath,
|
|
89
|
+
configExists: existsSync(importLinkPath),
|
|
90
|
+
configured: importLinkConfigured
|
|
91
|
+
},
|
|
92
|
+
nextSteps: nextSteps({ configExists, importLinkConfigured, missingDocs, missingSkills, doctorIssues }),
|
|
93
|
+
summary: {
|
|
94
|
+
ready: errors.length === 0 && missingDocs.length === 0 && missingSkills.length === 0,
|
|
95
|
+
needsConfig: !configExists,
|
|
96
|
+
needsImportLinkConfig: !importLinkConfigured,
|
|
97
|
+
errorCount: errors.length,
|
|
98
|
+
warningCount: doctorIssues.filter((issue) => issue.level === "warning").length
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function renderQuestions(report) {
|
|
103
|
+
const lines = [
|
|
104
|
+
"Agent Wiki onboarding questions",
|
|
105
|
+
"",
|
|
106
|
+
`1. Wiki type: ${report.wiki.type}`,
|
|
107
|
+
" A. Keep detected wiki type",
|
|
108
|
+
" B. Re-run init with --type vault",
|
|
109
|
+
" C. Re-run init with --type workspace",
|
|
110
|
+
"",
|
|
111
|
+
`2. Local config: ${report.wiki.configExists ? "present" : "missing"}`,
|
|
112
|
+
" A. Leave local config as-is",
|
|
113
|
+
" B. Write local config with agent-wiki onboard --write-config",
|
|
114
|
+
"",
|
|
115
|
+
`3. Import-link config: ${report.importLink.configured ? "configured" : "not configured"}`,
|
|
116
|
+
" A. Leave import-link disabled for now",
|
|
117
|
+
" B. Configure skills/import-link/config.json before importing links",
|
|
118
|
+
"",
|
|
119
|
+
"4. Optional conversion policy",
|
|
120
|
+
" A. Keep conversion disabled",
|
|
121
|
+
" B. Enable already-installed local converters only"
|
|
122
|
+
];
|
|
123
|
+
if (!report.doctor.passed) {
|
|
124
|
+
lines.push("", "Doctor errors must be fixed before editing wiki content.");
|
|
125
|
+
}
|
|
126
|
+
return `${lines.join("\n")}\n`;
|
|
127
|
+
}
|
|
128
|
+
function nextSteps(input) {
|
|
129
|
+
const steps = [];
|
|
130
|
+
if (input.doctorIssues.some((issue) => issue.level === "error"))
|
|
131
|
+
steps.push("Fix doctor errors, then rerun agent-wiki onboard --check.");
|
|
132
|
+
if (input.missingDocs.length || input.missingSkills.length)
|
|
133
|
+
steps.push("Run agent-wiki init or migrate to restore missing docs/skills.");
|
|
134
|
+
if (!input.configExists)
|
|
135
|
+
steps.push("Persist local setup with agent-wiki onboard --write-config after choosing wiki type and policy.");
|
|
136
|
+
if (!input.importLinkConfigured)
|
|
137
|
+
steps.push("Configure skills/import-link/config.json before importing external links.");
|
|
138
|
+
steps.push("Run agent-wiki compile and agent-wiki index --check before handing the wiki to another agent.");
|
|
139
|
+
return steps;
|
|
140
|
+
}
|
|
141
|
+
function missingFiles(root, paths) {
|
|
142
|
+
return paths.filter((path) => !existsSync(join(root, path)) || !statSync(join(root, path)).isFile());
|
|
143
|
+
}
|
|
144
|
+
function existsDir(path) {
|
|
145
|
+
return existsSync(path) && statSync(path).isDirectory();
|
|
146
|
+
}
|
|
147
|
+
function probeCommand(command, args) {
|
|
148
|
+
const result = spawnSync(command, args, {
|
|
149
|
+
encoding: "utf8",
|
|
150
|
+
shell: process.platform === "win32",
|
|
151
|
+
timeout: 3000
|
|
152
|
+
});
|
|
153
|
+
const output = `${result.stdout ?? ""}${result.stderr ?? ""}`.trim().split(/\r?\n/)[0] ?? "";
|
|
154
|
+
return {
|
|
155
|
+
command,
|
|
156
|
+
available: result.status === 0 || Boolean(output),
|
|
157
|
+
version: output || null
|
|
158
|
+
};
|
|
159
|
+
}
|
package/dist/src/page.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { idToFilename, pathToWikilink, readText, refToWikilink, renderMarkdown, today, walkMarkdownPages, writeOperationalLog, writeText } from "./wiki-utils.js";
|
|
4
|
+
const PAGE_FOLDERS = {
|
|
5
|
+
source: "sources",
|
|
6
|
+
entity: "entities",
|
|
7
|
+
concept: "concepts",
|
|
8
|
+
claim: "claims",
|
|
9
|
+
question: "questions",
|
|
10
|
+
synthesis: "syntheses"
|
|
11
|
+
};
|
|
12
|
+
const SOURCE_TYPES = new Set(["webpage", "article", "document", "pdf", "transcript", "email", "meeting-notes", "dataset", "screenshot", "bridge", "import", "other"]);
|
|
13
|
+
const ENTITY_TYPES = new Set(["person", "organization", "project", "product", "system", "place", "event", "artifact", "document", "other"]);
|
|
14
|
+
const CONCEPT_TYPES = new Set(["definition", "principle", "framework", "method", "policy", "standard", "pattern", "workflow", "runbook", "checklist", "playbook", "theory", "taxonomy", "other"]);
|
|
15
|
+
const SYNTHESIS_TYPES = new Set(["summary", "overview", "analysis", "timeline", "brief", "comparison"]);
|
|
16
|
+
const CLAIM_TYPES = new Set(["descriptive", "historical", "causal", "interpretive", "normative", "forecast"]);
|
|
17
|
+
export function createPage(args) {
|
|
18
|
+
const wikiRoot = process.cwd();
|
|
19
|
+
try {
|
|
20
|
+
const pageType = required(args, "type");
|
|
21
|
+
const subtype = required(args, "subtype");
|
|
22
|
+
const slug = required(args, "slug");
|
|
23
|
+
const title = required(args, "title");
|
|
24
|
+
if (!(pageType in PAGE_FOLDERS))
|
|
25
|
+
throw new Error("--type must be source, entity, concept, claim, question, or synthesis");
|
|
26
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(slug))
|
|
27
|
+
throw new Error("--slug must use kebab-case lowercase letters, numbers, and hyphens");
|
|
28
|
+
validateSubtype(pageType, subtype);
|
|
29
|
+
const body = readBody(args, wikiRoot);
|
|
30
|
+
if (pageType !== "source" && wordCount(body) < 5)
|
|
31
|
+
throw new Error("authored knowledge pages require substantive body prose");
|
|
32
|
+
const createdAt = stringOpt(args.date, today());
|
|
33
|
+
const sourceRole = stringOpt(args["source-role"], "whole");
|
|
34
|
+
const sourceDate = stringOpt(args["source-date"], stringOpt(args["retrieved-at"], createdAt));
|
|
35
|
+
const pageId = buildPageId(pageType, subtype, slug, pageType === "source" ? sourceDate : createdAt, sourceRole, numberOpt(args["part-index"]));
|
|
36
|
+
const folder = pageType === "source" && sourceRole === "part" ? "sources/parts" : PAGE_FOLDERS[pageType];
|
|
37
|
+
const relPath = join(folder, idToFilename(pageId)).split("\\").join("/");
|
|
38
|
+
const absPath = join(wikiRoot, relPath);
|
|
39
|
+
const duplicate = walkMarkdownPages(wikiRoot).find((page) => page.id === pageId);
|
|
40
|
+
if (duplicate)
|
|
41
|
+
throw new Error(`page ID already exists in ${duplicate.path}`);
|
|
42
|
+
if (existsSync(absPath))
|
|
43
|
+
throw new Error(`target file already exists: ${relPath}`);
|
|
44
|
+
const frontmatter = buildFrontmatter(args, pageType, subtype, title, pageId, createdAt, sourceRole);
|
|
45
|
+
const result = {
|
|
46
|
+
schemaVersion: 1,
|
|
47
|
+
mode: args["dry-run"] ? "dry-run" : "write",
|
|
48
|
+
mutating: !args["dry-run"],
|
|
49
|
+
id: pageId,
|
|
50
|
+
path: relPath,
|
|
51
|
+
pageType,
|
|
52
|
+
status: frontmatter.status,
|
|
53
|
+
written: false
|
|
54
|
+
};
|
|
55
|
+
if (args["dry-run"]) {
|
|
56
|
+
result.frontmatter = frontmatter;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
writeText(absPath, renderMarkdown(frontmatter, body));
|
|
60
|
+
result.written = true;
|
|
61
|
+
if (!args["no-log"])
|
|
62
|
+
writeOperationalLog(wikiRoot, `create-page: created ${pageType} page ${pageId} at ${relPath}`);
|
|
63
|
+
}
|
|
64
|
+
console.log(JSON.stringify(result, null, args.compact ? undefined : 2));
|
|
65
|
+
return 0;
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.log(JSON.stringify({ schemaVersion: 1, mode: args["dry-run"] ? "dry-run" : "write", mutating: false, written: false, error: error instanceof Error ? error.message : String(error) }, null, args.compact ? undefined : 2));
|
|
69
|
+
return 1;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function buildFrontmatter(args, pageType, subtype, title, id, createdAt, sourceRole) {
|
|
73
|
+
const fm = { id, pageType, title, status: defaultStatus(args, pageType, sourceRole) };
|
|
74
|
+
const subtypeField = { source: "sourceType", entity: "entityType", concept: "conceptType", claim: "claimType", synthesis: "synthesisType" };
|
|
75
|
+
if (subtypeField[pageType])
|
|
76
|
+
fm[subtypeField[pageType]] = subtype;
|
|
77
|
+
if (pageType === "source") {
|
|
78
|
+
if (!args["source-url"] && !args["origin-path"])
|
|
79
|
+
throw new Error("source pages require --source-url or --origin-path");
|
|
80
|
+
fm.sourceRole = sourceRole;
|
|
81
|
+
fm.sourceParts = list(args["source-part"]);
|
|
82
|
+
if (sourceRole === "part") {
|
|
83
|
+
fm.parentSourceId = required(args, "parent-source-id");
|
|
84
|
+
fm.partIndex = numberRequired(args, "part-index");
|
|
85
|
+
fm.partCount = numberRequired(args, "part-count");
|
|
86
|
+
fm.locator = required(args, "locator");
|
|
87
|
+
}
|
|
88
|
+
else if (sourceRole === "parent") {
|
|
89
|
+
if (list(args["source-part"]).length === 0)
|
|
90
|
+
throw new Error("parent source pages require at least one --source-part path");
|
|
91
|
+
fm.partCount = numberOpt(args["part-count"]);
|
|
92
|
+
}
|
|
93
|
+
if (args["source-url"])
|
|
94
|
+
fm.originUrl = args["source-url"];
|
|
95
|
+
if (args["origin-path"])
|
|
96
|
+
fm.originPath = pathToWikilink(String(args["origin-path"]));
|
|
97
|
+
fm.retrievedAt = stringOpt(args["retrieved-at"], createdAt);
|
|
98
|
+
for (const [flag, key] of [["published-at", "publishedAt"], ["converted-at", "convertedAt"], ["conversion-tool", "conversionTool"], ["conversion-tool-version", "conversionToolVersion"], ["conversion-backend", "conversionBackend"]]) {
|
|
99
|
+
if (args[flag])
|
|
100
|
+
fm[key] = args[flag];
|
|
101
|
+
}
|
|
102
|
+
if (list(args["conversion-warning"]).length)
|
|
103
|
+
fm.conversionWarnings = list(args["conversion-warning"]);
|
|
104
|
+
fm.attachments = list(args.attachment);
|
|
105
|
+
}
|
|
106
|
+
else if (pageType === "entity") {
|
|
107
|
+
fm.canonicalName = stringOpt(args["canonical-name"], title);
|
|
108
|
+
}
|
|
109
|
+
else if (pageType === "claim") {
|
|
110
|
+
fm.confidence = Number(stringOpt(args.confidence, "0.6"));
|
|
111
|
+
fm.text = stringOpt(args["claim-text"], title);
|
|
112
|
+
fm.subjectPageId = stringOpt(args["subject-page-id"], "");
|
|
113
|
+
fm.sourceIds = list(args["source-id"]);
|
|
114
|
+
fm.evidence = list(args.evidence).map(parseEvidence);
|
|
115
|
+
}
|
|
116
|
+
else if (pageType === "question") {
|
|
117
|
+
fm.priority = stringOpt(args.priority, "medium");
|
|
118
|
+
fm.relatedClaims = list(args["related-claim"]).map(refToWikilink);
|
|
119
|
+
fm.relatedPages = list(args["related-page"]).map(refToWikilink);
|
|
120
|
+
fm.openedAt = stringOpt(args["opened-at"], createdAt);
|
|
121
|
+
}
|
|
122
|
+
else if (pageType === "synthesis") {
|
|
123
|
+
if (!args.scope)
|
|
124
|
+
throw new Error("synthesis pages require --scope");
|
|
125
|
+
fm.scope = args.scope;
|
|
126
|
+
fm.sourcePages = list(args["source-page"]).map(refToWikilink);
|
|
127
|
+
fm.derivedClaims = list(args["derived-claim"]).map(refToWikilink);
|
|
128
|
+
}
|
|
129
|
+
if (!["claim", "question", "synthesis"].includes(pageType) && list(args["source-page"]).length)
|
|
130
|
+
fm.sourcePages = list(args["source-page"]).map(refToWikilink);
|
|
131
|
+
if (pageType !== "question" && list(args["related-page"]).length)
|
|
132
|
+
fm.relatedPages = list(args["related-page"]).map(refToWikilink);
|
|
133
|
+
fm.createdAt = createdAt;
|
|
134
|
+
fm.updatedAt = stringOpt(args["updated-at"], createdAt);
|
|
135
|
+
fm.aliases = list(args.alias);
|
|
136
|
+
fm.tags = list(args.tag);
|
|
137
|
+
return fm;
|
|
138
|
+
}
|
|
139
|
+
function buildPageId(pageType, subtype, slug, createdAt, sourceRole, partIndex) {
|
|
140
|
+
if (pageType === "source")
|
|
141
|
+
return `source.${createdAt}.${subtype}.${slug}${sourceRole === "part" ? `.part${String(partIndex).padStart(3, "0")}` : ""}`;
|
|
142
|
+
if (pageType === "question")
|
|
143
|
+
return `question.${subtype}.${slug}`;
|
|
144
|
+
return `${pageType}.${subtype}.${slug}`;
|
|
145
|
+
}
|
|
146
|
+
function defaultStatus(args, pageType, sourceRole) {
|
|
147
|
+
if (typeof args.status === "string")
|
|
148
|
+
return args.status;
|
|
149
|
+
if (pageType === "source")
|
|
150
|
+
return sourceRole === "parent" ? "partitioned" : "unprocessed";
|
|
151
|
+
if (pageType === "claim")
|
|
152
|
+
return "unverified";
|
|
153
|
+
if (pageType === "question")
|
|
154
|
+
return "open";
|
|
155
|
+
return "active";
|
|
156
|
+
}
|
|
157
|
+
function validateSubtype(pageType, subtype) {
|
|
158
|
+
const sets = { source: SOURCE_TYPES, entity: ENTITY_TYPES, concept: CONCEPT_TYPES, claim: CLAIM_TYPES, synthesis: SYNTHESIS_TYPES };
|
|
159
|
+
if (sets[pageType] && !sets[pageType].has(subtype))
|
|
160
|
+
throw new Error(`--subtype ${subtype} is not valid for page type ${pageType}`);
|
|
161
|
+
}
|
|
162
|
+
function parseEvidence(value) {
|
|
163
|
+
const raw = value.trim().startsWith("{") ? JSON.parse(value) : Object.fromEntries(value.split(";").filter(Boolean).map((part) => part.split("=", 2).map((x) => x.trim())));
|
|
164
|
+
for (const key of ["id", "sourceId", "path", "kind", "relation", "weight", "updatedAt"])
|
|
165
|
+
if (!raw[key])
|
|
166
|
+
throw new Error("--evidence is missing required fields");
|
|
167
|
+
raw.weight = Number(raw.weight);
|
|
168
|
+
return raw;
|
|
169
|
+
}
|
|
170
|
+
function readBody(args, wikiRoot) {
|
|
171
|
+
const bodyFile = stringOpt(args["body-file"]);
|
|
172
|
+
const body = bodyFile ? readText(resolve(wikiRoot, bodyFile)) : stringOpt(args.body, "");
|
|
173
|
+
if (!body.trim())
|
|
174
|
+
throw new Error("body prose/content is required");
|
|
175
|
+
return body;
|
|
176
|
+
}
|
|
177
|
+
function required(args, key) {
|
|
178
|
+
const value = stringOpt(args[key]);
|
|
179
|
+
if (!value)
|
|
180
|
+
throw new Error(`missing required --${key}`);
|
|
181
|
+
return value;
|
|
182
|
+
}
|
|
183
|
+
function stringOpt(value, fallback) { return typeof value === "string" ? value : fallback ?? ""; }
|
|
184
|
+
function list(value) { return Array.isArray(value) ? value.map(String) : typeof value === "string" ? [value] : []; }
|
|
185
|
+
function numberOpt(value) { return typeof value === "string" ? Number(value) : undefined; }
|
|
186
|
+
function numberRequired(args, key) { const value = numberOpt(args[key]); if (!Number.isFinite(value))
|
|
187
|
+
throw new Error(`missing required --${key}`); return value; }
|
|
188
|
+
function wordCount(value) { return (value.match(/\b\w+\b/g) ?? []).length; }
|