@biaoo/tiangong-wiki 0.2.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/LICENSE +21 -0
- package/README.md +167 -0
- package/README.zh-CN.md +167 -0
- package/SKILL.md +116 -0
- package/agents/openai.yaml +4 -0
- package/assets/config.example.env +18 -0
- package/assets/templates/achievement.md +32 -0
- package/assets/templates/bridge.md +33 -0
- package/assets/templates/concept.md +47 -0
- package/assets/templates/faq.md +31 -0
- package/assets/templates/lesson.md +31 -0
- package/assets/templates/method.md +31 -0
- package/assets/templates/misconception.md +35 -0
- package/assets/templates/person.md +31 -0
- package/assets/templates/research-note.md +34 -0
- package/assets/templates/resume.md +34 -0
- package/assets/templates/source-summary.md +35 -0
- package/assets/vllm/qwen3_5_openai_developer.jinja +182 -0
- package/assets/wiki.config.default.json +193 -0
- package/dist/commands/check-config.js +77 -0
- package/dist/commands/create.js +32 -0
- package/dist/commands/daemon.js +186 -0
- package/dist/commands/dashboard.js +112 -0
- package/dist/commands/doctor.js +22 -0
- package/dist/commands/export-graph.js +28 -0
- package/dist/commands/export-index.js +31 -0
- package/dist/commands/find.js +36 -0
- package/dist/commands/fts.js +32 -0
- package/dist/commands/graph.js +35 -0
- package/dist/commands/init.js +48 -0
- package/dist/commands/lint.js +35 -0
- package/dist/commands/list.js +28 -0
- package/dist/commands/page-info.js +24 -0
- package/dist/commands/search.js +32 -0
- package/dist/commands/setup.js +15 -0
- package/dist/commands/stat.js +20 -0
- package/dist/commands/sync.js +38 -0
- package/dist/commands/template.js +71 -0
- package/dist/commands/type.js +88 -0
- package/dist/commands/vault.js +64 -0
- package/dist/core/agent.js +201 -0
- package/dist/core/cli-env.js +129 -0
- package/dist/core/codex-workflow.js +233 -0
- package/dist/core/config.js +126 -0
- package/dist/core/db.js +292 -0
- package/dist/core/embedding.js +104 -0
- package/dist/core/frontmatter.js +287 -0
- package/dist/core/indexer.js +241 -0
- package/dist/core/onboarding.js +967 -0
- package/dist/core/page-files.js +91 -0
- package/dist/core/paths.js +161 -0
- package/dist/core/presenters.js +23 -0
- package/dist/core/query.js +58 -0
- package/dist/core/runtime.js +20 -0
- package/dist/core/sync.js +235 -0
- package/dist/core/synology.js +412 -0
- package/dist/core/template-evolution.js +38 -0
- package/dist/core/vault-processing.js +742 -0
- package/dist/core/vault.js +594 -0
- package/dist/core/workflow-context.js +188 -0
- package/dist/core/workflow-result.js +162 -0
- package/dist/core/workspace-bootstrap.js +30 -0
- package/dist/core/workspace-skills.js +220 -0
- package/dist/daemon/client.js +147 -0
- package/dist/daemon/server.js +807 -0
- package/dist/daemon/state.js +53 -0
- package/dist/dashboard/assets/index-1FgAUZ28.css +1 -0
- package/dist/dashboard/assets/index-6A0PWT4X.js +154 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-BWZEU5yA.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-500-normal-CJOVTJB7.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-BYuf6tUa.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-700-normal-D3wTyLJW.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist/dashboard/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/dashboard/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-400-normal-BnQMeOim.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-400-normal-CJ-V5oYT.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-500-normal-CNSSEhBt.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-500-normal-lFbtlQH6.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-700-normal-CwsQ-cCU.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-700-normal-RjhwGPKo.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-CfP_5XZW.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-400-normal-DRPE3kg4.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-3dgZTiw9.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-500-normal-DUe3BAxM.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-BQnZhY3m.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-latin-ext-700-normal-HVCqSBdx.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-B7xT_GF5.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-400-normal-BIWiOVfw.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BTqKIpxg.woff +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-500-normal-BmEvtly_.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-DMty7AZE.woff2 +0 -0
- package/dist/dashboard/assets/space-grotesk-vietnamese-700-normal-Duxec5Rn.woff +0 -0
- package/dist/dashboard/index.html +18 -0
- package/dist/index.js +86 -0
- package/dist/operations/dashboard.js +1231 -0
- package/dist/operations/export.js +110 -0
- package/dist/operations/query.js +649 -0
- package/dist/operations/type-template.js +210 -0
- package/dist/operations/write.js +143 -0
- package/dist/types/config.js +1 -0
- package/dist/types/page.js +1 -0
- package/dist/utils/case.js +22 -0
- package/dist/utils/errors.js +26 -0
- package/dist/utils/fs.js +77 -0
- package/dist/utils/output.js +33 -0
- package/dist/utils/process.js +60 -0
- package/dist/utils/segmenter.js +24 -0
- package/dist/utils/slug.js +10 -0
- package/dist/utils/time.js +24 -0
- package/package.json +64 -0
- package/references/cli-interface.md +312 -0
- package/references/env.md +122 -0
- package/references/template-design-guide.md +271 -0
- package/references/vault-to-wiki-instruction.md +110 -0
- package/references/wiki-maintenance-instruction.md +190 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { chmodSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { ensureDirSync, writeTextFileSync } from "../utils/fs.js";
|
|
4
|
+
import { sha256Text } from "../utils/fs.js";
|
|
5
|
+
function shellSingleQuote(value) {
|
|
6
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
7
|
+
}
|
|
8
|
+
function resolveNodeExecutable() {
|
|
9
|
+
const currentExec = path.basename(process.execPath).toLowerCase();
|
|
10
|
+
if (currentExec === "node" || currentExec.startsWith("node")) {
|
|
11
|
+
return process.execPath;
|
|
12
|
+
}
|
|
13
|
+
const npmNodeExecPath = process.env.npm_node_execpath?.trim();
|
|
14
|
+
if (npmNodeExecPath) {
|
|
15
|
+
return npmNodeExecPath;
|
|
16
|
+
}
|
|
17
|
+
return "node";
|
|
18
|
+
}
|
|
19
|
+
function readableArtifactPrefix(queueItemId) {
|
|
20
|
+
const normalized = queueItemId
|
|
21
|
+
.replace(/[\\/]+/g, "__")
|
|
22
|
+
.replace(/[^A-Za-z0-9._-]+/g, "_")
|
|
23
|
+
.replace(/^_+|_+$/g, "");
|
|
24
|
+
if (!normalized) {
|
|
25
|
+
return "queue-item";
|
|
26
|
+
}
|
|
27
|
+
return normalized.slice(0, 80);
|
|
28
|
+
}
|
|
29
|
+
export function toWorkflowArtifactId(queueItemId) {
|
|
30
|
+
return `${readableArtifactPrefix(queueItemId)}--${sha256Text(queueItemId).slice(0, 12)}`;
|
|
31
|
+
}
|
|
32
|
+
export function getWorkflowArtifactSet(paths, queueItemId) {
|
|
33
|
+
const artifactId = toWorkflowArtifactId(queueItemId);
|
|
34
|
+
const rootDir = path.join(paths.queueArtifactsPath, artifactId);
|
|
35
|
+
return {
|
|
36
|
+
queueItemId,
|
|
37
|
+
artifactId,
|
|
38
|
+
rootDir,
|
|
39
|
+
queueItemPath: path.join(rootDir, "queue-item.json"),
|
|
40
|
+
promptPath: path.join(rootDir, "prompt.md"),
|
|
41
|
+
resultPath: path.join(rootDir, "result.json"),
|
|
42
|
+
skillArtifactsPath: path.join(rootDir, "skill-artifacts"),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
export function buildVaultWorkflowPrompt(input) {
|
|
46
|
+
return [
|
|
47
|
+
"Process one vault queue item.",
|
|
48
|
+
"",
|
|
49
|
+
`WORKSPACE_ROOT=${input.workspaceRoot}`,
|
|
50
|
+
`VAULT_FILE_PATH=${input.vaultFilePath}`,
|
|
51
|
+
`RESULT_JSON_PATH=${input.resultJsonPath}`,
|
|
52
|
+
`ALLOW_TEMPLATE_EVOLUTION=${input.allowTemplateEvolution ? "true" : "false"}`,
|
|
53
|
+
"",
|
|
54
|
+
"## Environment",
|
|
55
|
+
"",
|
|
56
|
+
"Workspace-local skills are available from WORKSPACE_ROOT through normal Codex skill discovery.",
|
|
57
|
+
"A local tiangong-wiki CLI launcher is already available on PATH for this run.",
|
|
58
|
+
"",
|
|
59
|
+
"The tiangong-wiki CLI provides these discovery and search capabilities:",
|
|
60
|
+
"- `tiangong-wiki type list` / `tiangong-wiki type show <type>` — discover registered page types and their purpose",
|
|
61
|
+
"- `tiangong-wiki template show <type>` — see the exact frontmatter fields and body structure for a type",
|
|
62
|
+
"- `tiangong-wiki find [options]` — find pages by structured metadata filters",
|
|
63
|
+
"- `tiangong-wiki search <query>` — semantic search over page summary embeddings",
|
|
64
|
+
"- `tiangong-wiki fts <query>` — full-text search over title, tags, and summary",
|
|
65
|
+
"- `tiangong-wiki list [options]` — list existing wiki pages",
|
|
66
|
+
"- `tiangong-wiki page-info <pageId>` — show full metadata and edges for one page",
|
|
67
|
+
"- `tiangong-wiki graph <root>` — traverse the wiki knowledge graph",
|
|
68
|
+
"- `tiangong-wiki stat` — show aggregate wiki index statistics",
|
|
69
|
+
"",
|
|
70
|
+
"Use whichever combination you judge necessary. These are tools at your disposal, not a mandatory checklist.",
|
|
71
|
+
"",
|
|
72
|
+
"## Step 1 — Read and Discover",
|
|
73
|
+
"",
|
|
74
|
+
"1. Read queue-item.json next to RESULT_JSON_PATH.",
|
|
75
|
+
"2. Read the target vault file at VAULT_FILE_PATH.",
|
|
76
|
+
"3. Discover the current page type ontology through the tiangong-wiki CLI. Do not assume any type, template, or default target type.",
|
|
77
|
+
"4. Understand the existing wiki knowledge landscape before deciding what to create or update:",
|
|
78
|
+
" - What pages already exist? Are any of them covering the same or overlapping topics?",
|
|
79
|
+
" - What is the current knowledge graph structure? Are there clusters of related pages that this new source naturally connects to?",
|
|
80
|
+
" - Does this source introduce genuinely new knowledge, or does it reinforce, extend, or contradict something already captured?",
|
|
81
|
+
"",
|
|
82
|
+
"These questions must be answered before proceeding to Step 2.",
|
|
83
|
+
"",
|
|
84
|
+
"Keep the run narrowly focused on the target vault file, the current ontology, and the best candidate pages.",
|
|
85
|
+
"Do not inspect the whole workspace, list broad file trees, or read large reference files unless a concrete command failure blocks you.",
|
|
86
|
+
"Do not call tiangong-wiki --help or perform broad discovery unless a specific command failure forces it.",
|
|
87
|
+
"",
|
|
88
|
+
"## Step 2 — Decide",
|
|
89
|
+
"",
|
|
90
|
+
"### Type Selection",
|
|
91
|
+
"",
|
|
92
|
+
"Choose the page type that best matches the nature of the knowledge in the source. Query the registered types and understand their intended use before deciding. Do not default to any single type.",
|
|
93
|
+
"",
|
|
94
|
+
"### Update vs Create",
|
|
95
|
+
"",
|
|
96
|
+
"If an existing page already covers the same topic, prefer updating it over creating a duplicate. Only create a new page when the source introduces a genuinely new topic not yet represented in the wiki.",
|
|
97
|
+
"",
|
|
98
|
+
"### Splitting",
|
|
99
|
+
"",
|
|
100
|
+
"A single vault source MAY produce multiple pages of different types when the source contains independently reusable knowledge points.",
|
|
101
|
+
"- At most 5 pages per vault source to avoid over-fragmentation.",
|
|
102
|
+
"- Link all pages created from the same source via relatedPages.",
|
|
103
|
+
"",
|
|
104
|
+
"### Building Relations",
|
|
105
|
+
"",
|
|
106
|
+
"Every page you create or update should be connected to the existing knowledge graph where meaningful relations exist. Orphan pages with no relations are acceptable only when the source introduces a topic with no overlap to existing wiki content.",
|
|
107
|
+
"",
|
|
108
|
+
"## Step 3 — Create or Update Pages",
|
|
109
|
+
"",
|
|
110
|
+
"### Field Conventions",
|
|
111
|
+
"",
|
|
112
|
+
"- **vaultPath**: MUST be relative to the vault root. Never use absolute paths. Derive it by stripping the vault root prefix from VAULT_FILE_PATH.",
|
|
113
|
+
"- **sourceRefs**: Prefer existing wiki page IDs when the new page directly builds on already-indexed wiki pages. Do not put absolute vault paths into sourceRefs. If no existing wiki page is directly referenced, it is acceptable to leave sourceRefs empty.",
|
|
114
|
+
"- **relatedPages**: Populate with page IDs of related pages discovered in Step 1. Every page should have relations when related content exists in the wiki.",
|
|
115
|
+
"- **createdAt / updatedAt**: Leave placeholders unchanged or omit them. The system will normalize them to YYYY-MM-DD during indexing and refresh updatedAt on modified pages.",
|
|
116
|
+
"- **nodeId**: Use a lowercase kebab-case slug derived from the title.",
|
|
117
|
+
"",
|
|
118
|
+
"Consult the template for your chosen type before writing a page.",
|
|
119
|
+
"If ALLOW_TEMPLATE_EVOLUTION=false, do not create templates or new page types.",
|
|
120
|
+
"",
|
|
121
|
+
"### Quality Gate",
|
|
122
|
+
"",
|
|
123
|
+
"For every changed page, run:",
|
|
124
|
+
"1. `tiangong-wiki sync --path <page>`",
|
|
125
|
+
"2. `tiangong-wiki lint --path <page> --format json`",
|
|
126
|
+
"",
|
|
127
|
+
"Fix any errors before proceeding. Warnings are acceptable.",
|
|
128
|
+
"",
|
|
129
|
+
"## Step 4 — Write Result Manifest",
|
|
130
|
+
"",
|
|
131
|
+
"The authoritative threadId is queue-item.json.threadId. Read it from there and copy it unchanged into result.json.threadId. If it is empty on first read, read queue-item.json again immediately before writing the manifest.",
|
|
132
|
+
"",
|
|
133
|
+
"Write RESULT_JSON_PATH as one JSON object with: status, decision, reason, threadId, skillsUsed, createdPageIds, updatedPageIds, appliedTypeNames, proposedTypes, actions, lint.",
|
|
134
|
+
"",
|
|
135
|
+
"### Allowed Values",
|
|
136
|
+
"",
|
|
137
|
+
"- **status**: done | skipped | error. Use done for successful apply or propose_only runs, skipped for skip, and error only when the workflow itself failed. Never use success, completed, failed, or other aliases.",
|
|
138
|
+
"- **decision**: apply | skip | propose_only. Never use update_existing, create_new, update, create, or other aliases.",
|
|
139
|
+
"- **actions**: Array of objects, never strings. Allowed action kinds: create_page, update_page, create_template. Every action object must include kind and summary. create_page requires pageType and title. update_page requires pageId. create_template requires pageType and title.",
|
|
140
|
+
"- **proposedTypes**: Objects with name, reason, suggestedTemplateSections.",
|
|
141
|
+
"- **lint**: Objects with pageId, errors, warnings.",
|
|
142
|
+
"",
|
|
143
|
+
"### Example",
|
|
144
|
+
"",
|
|
145
|
+
'{"status":"done","decision":"apply","reason":"Updated the existing method.","threadId":"<copy queue-item.json.threadId>","skillsUsed":["tiangong-wiki-skill"],"createdPageIds":[],"updatedPageIds":["methods/example.md"],"appliedTypeNames":["method"],"proposedTypes":[],"actions":[{"kind":"update_page","pageId":"methods/example.md","pageType":"method","summary":"Updated the page with durable knowledge."}],"lint":[{"pageId":"methods/example.md","errors":0,"warnings":0}]}',
|
|
146
|
+
"",
|
|
147
|
+
"If no page change is justified, still write RESULT_JSON_PATH with decision=skip or decision=propose_only and then stop.",
|
|
148
|
+
"Use RESULT_JSON_PATH only for the final structured manifest. Write raw JSON only, with no Markdown fences and no prose before or after the JSON object.",
|
|
149
|
+
"The queue item metadata is stored next to RESULT_JSON_PATH as queue-item.json.",
|
|
150
|
+
"Stop immediately after RESULT_JSON_PATH is fully written.",
|
|
151
|
+
].join("\n");
|
|
152
|
+
}
|
|
153
|
+
export function ensureWorkflowArtifactSet(paths, input) {
|
|
154
|
+
const artifacts = getWorkflowArtifactSet(paths, input.queueItemId);
|
|
155
|
+
ensureDirSync(paths.queueArtifactsPath);
|
|
156
|
+
ensureDirSync(artifacts.rootDir);
|
|
157
|
+
ensureDirSync(artifacts.skillArtifactsPath);
|
|
158
|
+
const wikiCliWrapperPath = path.join(artifacts.skillArtifactsPath, "tiangong-wiki");
|
|
159
|
+
const nodeExecutable = resolveNodeExecutable();
|
|
160
|
+
const cliEntrypoint = path.join(paths.packageRoot, "dist", "index.js");
|
|
161
|
+
writeTextFileSync(artifacts.queueItemPath, `${JSON.stringify(input.queueItem, null, 2)}\n`);
|
|
162
|
+
writeTextFileSync(wikiCliWrapperPath, [
|
|
163
|
+
"#!/bin/sh",
|
|
164
|
+
'node_bin=${WIKI_CLI_NODE:-}',
|
|
165
|
+
'if [ -z "$node_bin" ]; then',
|
|
166
|
+
` node_bin=${shellSingleQuote(nodeExecutable)}`,
|
|
167
|
+
"fi",
|
|
168
|
+
'cli_entry=${WIKI_CLI_ENTRYPOINT:-}',
|
|
169
|
+
'if [ -z "$cli_entry" ]; then',
|
|
170
|
+
` cli_entry=${shellSingleQuote(cliEntrypoint)}`,
|
|
171
|
+
"fi",
|
|
172
|
+
'if [ ! -f "$cli_entry" ]; then',
|
|
173
|
+
' echo "tiangong-wiki CLI entrypoint not found: ${cli_entry}" >&2',
|
|
174
|
+
" exit 127",
|
|
175
|
+
"fi",
|
|
176
|
+
'exec "$node_bin" "$cli_entry" "$@"',
|
|
177
|
+
"",
|
|
178
|
+
].join("\n"));
|
|
179
|
+
chmodSync(wikiCliWrapperPath, 0o755);
|
|
180
|
+
writeTextFileSync(artifacts.promptPath, input.promptMarkdown ??
|
|
181
|
+
[
|
|
182
|
+
"# Vault To Wiki Workflow",
|
|
183
|
+
"",
|
|
184
|
+
"This prompt is intentionally minimal and will be populated by the workflow runner.",
|
|
185
|
+
].join("\n"));
|
|
186
|
+
writeTextFileSync(artifacts.resultPath, "");
|
|
187
|
+
return artifacts;
|
|
188
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { AppError } from "../utils/errors.js";
|
|
2
|
+
import { pathExistsSync, readTextFileSync } from "../utils/fs.js";
|
|
3
|
+
function fail(message, details) {
|
|
4
|
+
throw new AppError(message, "runtime", details);
|
|
5
|
+
}
|
|
6
|
+
function ensureRecord(value, label) {
|
|
7
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
8
|
+
fail(`${label} must be an object`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function ensureString(value, label) {
|
|
13
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
14
|
+
fail(`${label} must be a non-empty string`);
|
|
15
|
+
}
|
|
16
|
+
return value.trim();
|
|
17
|
+
}
|
|
18
|
+
function ensureStringArray(value, label) {
|
|
19
|
+
if (!Array.isArray(value)) {
|
|
20
|
+
fail(`${label} must be an array`);
|
|
21
|
+
}
|
|
22
|
+
return value.map((entry, index) => ensureString(entry, `${label}[${index}]`));
|
|
23
|
+
}
|
|
24
|
+
function ensureNumber(value, label) {
|
|
25
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
26
|
+
fail(`${label} must be a finite number`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
function ensureStatus(value) {
|
|
31
|
+
const status = ensureString(value, "result.status");
|
|
32
|
+
if (status === "done" || status === "skipped" || status === "error") {
|
|
33
|
+
return status;
|
|
34
|
+
}
|
|
35
|
+
fail(`result.status must be one of done, skipped, error`);
|
|
36
|
+
}
|
|
37
|
+
function ensureDecision(value) {
|
|
38
|
+
const decision = ensureString(value, "result.decision");
|
|
39
|
+
if (decision === "skip" || decision === "apply" || decision === "propose_only") {
|
|
40
|
+
return decision;
|
|
41
|
+
}
|
|
42
|
+
fail(`result.decision must be one of skip, apply, propose_only`);
|
|
43
|
+
}
|
|
44
|
+
function parseSourceFile(value) {
|
|
45
|
+
if (value === undefined) {
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
const sourceFile = ensureRecord(value, "result.sourceFile");
|
|
49
|
+
const path = ensureString(sourceFile.path, "result.sourceFile.path");
|
|
50
|
+
const sha256 = sourceFile.sha256 === undefined ? undefined : ensureString(sourceFile.sha256, "result.sourceFile.sha256");
|
|
51
|
+
return { path, ...(sha256 ? { sha256 } : {}) };
|
|
52
|
+
}
|
|
53
|
+
function parseProposedTypes(value) {
|
|
54
|
+
if (!Array.isArray(value)) {
|
|
55
|
+
fail("result.proposedTypes must be an array");
|
|
56
|
+
}
|
|
57
|
+
return value.map((entry, index) => {
|
|
58
|
+
const proposed = ensureRecord(entry, `result.proposedTypes[${index}]`);
|
|
59
|
+
return {
|
|
60
|
+
name: ensureString(proposed.name, `result.proposedTypes[${index}].name`),
|
|
61
|
+
reason: ensureString(proposed.reason, `result.proposedTypes[${index}].reason`),
|
|
62
|
+
suggestedTemplateSections: ensureStringArray(proposed.suggestedTemplateSections, `result.proposedTypes[${index}].suggestedTemplateSections`),
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
function parseActions(value) {
|
|
67
|
+
if (!Array.isArray(value)) {
|
|
68
|
+
fail("result.actions must be an array");
|
|
69
|
+
}
|
|
70
|
+
return value.map((entry, index) => {
|
|
71
|
+
const action = ensureRecord(entry, `result.actions[${index}]`);
|
|
72
|
+
const kind = ensureString(action.kind, `result.actions[${index}].kind`);
|
|
73
|
+
if (kind !== "create_page" && kind !== "update_page" && kind !== "create_template") {
|
|
74
|
+
fail(`result.actions[${index}].kind must be create_page, update_page, or create_template`);
|
|
75
|
+
}
|
|
76
|
+
const parsed = {
|
|
77
|
+
kind,
|
|
78
|
+
summary: ensureString(action.summary, `result.actions[${index}].summary`),
|
|
79
|
+
};
|
|
80
|
+
if (action.pageType !== undefined) {
|
|
81
|
+
parsed.pageType = ensureString(action.pageType, `result.actions[${index}].pageType`);
|
|
82
|
+
}
|
|
83
|
+
if (action.pageId !== undefined) {
|
|
84
|
+
parsed.pageId = ensureString(action.pageId, `result.actions[${index}].pageId`);
|
|
85
|
+
}
|
|
86
|
+
if (action.title !== undefined) {
|
|
87
|
+
parsed.title = ensureString(action.title, `result.actions[${index}].title`);
|
|
88
|
+
}
|
|
89
|
+
if ((kind === "create_page" || kind === "create_template") && !parsed.pageType) {
|
|
90
|
+
fail(`result.actions[${index}].pageType must be provided for ${kind}`);
|
|
91
|
+
}
|
|
92
|
+
if (kind === "create_page" && !parsed.title) {
|
|
93
|
+
fail(`result.actions[${index}].title must be provided for create_page`);
|
|
94
|
+
}
|
|
95
|
+
if (kind === "update_page" && !parsed.pageId) {
|
|
96
|
+
fail(`result.actions[${index}].pageId must be provided for update_page`);
|
|
97
|
+
}
|
|
98
|
+
if (kind === "create_template" && !parsed.title) {
|
|
99
|
+
fail(`result.actions[${index}].title must be provided for create_template`);
|
|
100
|
+
}
|
|
101
|
+
return parsed;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function parseLint(value) {
|
|
105
|
+
if (!Array.isArray(value)) {
|
|
106
|
+
fail("result.lint must be an array");
|
|
107
|
+
}
|
|
108
|
+
return value.map((entry, index) => {
|
|
109
|
+
const lint = ensureRecord(entry, `result.lint[${index}]`);
|
|
110
|
+
return {
|
|
111
|
+
pageId: ensureString(lint.pageId, `result.lint[${index}].pageId`),
|
|
112
|
+
errors: ensureNumber(lint.errors, `result.lint[${index}].errors`),
|
|
113
|
+
warnings: ensureNumber(lint.warnings, `result.lint[${index}].warnings`),
|
|
114
|
+
};
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
export function parseWorkflowResult(raw) {
|
|
118
|
+
try {
|
|
119
|
+
const result = ensureRecord(raw, "result");
|
|
120
|
+
const manifest = {
|
|
121
|
+
status: ensureStatus(result.status),
|
|
122
|
+
decision: ensureDecision(result.decision),
|
|
123
|
+
reason: ensureString(result.reason, "result.reason"),
|
|
124
|
+
threadId: ensureString(result.threadId, "result.threadId"),
|
|
125
|
+
skillsUsed: ensureStringArray(result.skillsUsed, "result.skillsUsed"),
|
|
126
|
+
createdPageIds: ensureStringArray(result.createdPageIds, "result.createdPageIds"),
|
|
127
|
+
updatedPageIds: ensureStringArray(result.updatedPageIds, "result.updatedPageIds"),
|
|
128
|
+
appliedTypeNames: ensureStringArray(result.appliedTypeNames, "result.appliedTypeNames"),
|
|
129
|
+
proposedTypes: parseProposedTypes(result.proposedTypes),
|
|
130
|
+
actions: parseActions(result.actions),
|
|
131
|
+
lint: parseLint(result.lint),
|
|
132
|
+
sourceFile: parseSourceFile(result.sourceFile),
|
|
133
|
+
};
|
|
134
|
+
if (manifest.decision === "apply" && manifest.actions.length === 0) {
|
|
135
|
+
fail("result.actions must contain at least one action when decision=apply");
|
|
136
|
+
}
|
|
137
|
+
return manifest;
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (error instanceof AppError) {
|
|
141
|
+
throw error;
|
|
142
|
+
}
|
|
143
|
+
fail("Failed to parse workflow result", { cause: error instanceof Error ? error.message : String(error) });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
export function readWorkflowResult(resultPath) {
|
|
147
|
+
if (!pathExistsSync(resultPath)) {
|
|
148
|
+
fail(`Workflow result not found: ${resultPath}`);
|
|
149
|
+
}
|
|
150
|
+
const rawText = readTextFileSync(resultPath);
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(rawText);
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
fail("Workflow result is not valid JSON", {
|
|
157
|
+
resultPath,
|
|
158
|
+
cause: error instanceof Error ? error.message : String(error),
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return parseWorkflowResult(parsed);
|
|
162
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { copyDirectoryContentsSync, copyFileIfMissingSync, ensureDirSync, isDirectoryEmptySync, pathExistsSync, } from "../utils/fs.js";
|
|
3
|
+
function ensureDirectory(dirPath, createdDirectories) {
|
|
4
|
+
if (!pathExistsSync(dirPath)) {
|
|
5
|
+
createdDirectories.push(dirPath);
|
|
6
|
+
}
|
|
7
|
+
ensureDirSync(dirPath);
|
|
8
|
+
}
|
|
9
|
+
export function scaffoldWorkspaceAssets(paths) {
|
|
10
|
+
const createdDirectories = [];
|
|
11
|
+
ensureDirectory(paths.wikiRoot, createdDirectories);
|
|
12
|
+
ensureDirectory(paths.wikiPath, createdDirectories);
|
|
13
|
+
ensureDirectory(paths.templatesPath, createdDirectories);
|
|
14
|
+
if (paths.vaultPath) {
|
|
15
|
+
ensureDirectory(paths.vaultPath, createdDirectories);
|
|
16
|
+
}
|
|
17
|
+
const defaultConfigPath = path.join(paths.packageRoot, "assets", "wiki.config.default.json");
|
|
18
|
+
const defaultTemplatesPath = path.join(paths.packageRoot, "assets", "templates");
|
|
19
|
+
const copiedConfig = copyFileIfMissingSync(defaultConfigPath, paths.configPath);
|
|
20
|
+
let copiedTemplates = 0;
|
|
21
|
+
if (isDirectoryEmptySync(paths.templatesPath) && pathExistsSync(defaultTemplatesPath)) {
|
|
22
|
+
copyDirectoryContentsSync(defaultTemplatesPath, paths.templatesPath);
|
|
23
|
+
copiedTemplates = 1;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
copiedConfig,
|
|
27
|
+
copiedTemplates,
|
|
28
|
+
createdDirectories,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { accessSync, constants, lstatSync, realpathSync, rmSync, symlinkSync, unlinkSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { AppError } from "../utils/errors.js";
|
|
5
|
+
import { ensureDirSync, pathExistsSync } from "../utils/fs.js";
|
|
6
|
+
export const PARSER_SKILL_SOURCE = "https://github.com/anthropics/skills";
|
|
7
|
+
export const OPTIONAL_PARSER_SKILLS = [
|
|
8
|
+
{ name: "pdf", summary: "Process PDF files" },
|
|
9
|
+
{ name: "docx", summary: "Process DOCX files" },
|
|
10
|
+
{ name: "pptx", summary: "Process PPTX files" },
|
|
11
|
+
{ name: "xlsx", summary: "Process XLSX/CSV files" },
|
|
12
|
+
];
|
|
13
|
+
const OPTIONAL_PARSER_SKILL_NAMES = new Set(OPTIONAL_PARSER_SKILLS.map((skill) => skill.name));
|
|
14
|
+
function canRead(filePath) {
|
|
15
|
+
accessSync(filePath, constants.R_OK);
|
|
16
|
+
return true;
|
|
17
|
+
}
|
|
18
|
+
function getNpxCommand() {
|
|
19
|
+
return process.platform === "win32" ? "npx.cmd" : "npx";
|
|
20
|
+
}
|
|
21
|
+
export function resolveWorkspaceRootFromWikiPath(wikiPath) {
|
|
22
|
+
return path.resolve(wikiPath, "..", "..");
|
|
23
|
+
}
|
|
24
|
+
export function resolveWorkspaceSkillPaths(wikiPath) {
|
|
25
|
+
const workspaceRoot = resolveWorkspaceRootFromWikiPath(wikiPath);
|
|
26
|
+
const skillsRoot = path.join(workspaceRoot, ".agents", "skills");
|
|
27
|
+
return {
|
|
28
|
+
workspaceRoot,
|
|
29
|
+
skillsRoot,
|
|
30
|
+
wikiSkillPath: path.join(skillsRoot, "tiangong-wiki-skill"),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export function resolveWorkspaceSkillPath(workspaceRoot, skillName) {
|
|
34
|
+
return path.join(workspaceRoot, ".agents", "skills", skillName);
|
|
35
|
+
}
|
|
36
|
+
export function parseParserSkillSelection(rawValue) {
|
|
37
|
+
const value = rawValue?.trim();
|
|
38
|
+
if (!value) {
|
|
39
|
+
return {
|
|
40
|
+
skills: [],
|
|
41
|
+
invalid: [],
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
const skills = [];
|
|
45
|
+
const seen = new Set();
|
|
46
|
+
const invalid = [];
|
|
47
|
+
for (const entry of value.split(",")) {
|
|
48
|
+
const candidate = entry.trim().toLowerCase();
|
|
49
|
+
if (!candidate) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (!OPTIONAL_PARSER_SKILL_NAMES.has(candidate)) {
|
|
53
|
+
invalid.push(candidate);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
const skill = candidate;
|
|
57
|
+
if (!seen.has(skill)) {
|
|
58
|
+
seen.add(skill);
|
|
59
|
+
skills.push(skill);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
skills,
|
|
64
|
+
invalid,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
export function parseParserSkills(rawValue, options = {}) {
|
|
68
|
+
const { skills, invalid } = parseParserSkillSelection(rawValue);
|
|
69
|
+
if (invalid.length > 0 && options.strict !== false) {
|
|
70
|
+
throw new AppError(`WIKI_PARSER_SKILLS contains unsupported skills: ${invalid.join(", ")}`, "config");
|
|
71
|
+
}
|
|
72
|
+
return skills;
|
|
73
|
+
}
|
|
74
|
+
export function formatParserSkills(skills) {
|
|
75
|
+
return skills.join(",");
|
|
76
|
+
}
|
|
77
|
+
export function inspectSkillInstall(skillPath, name = path.basename(skillPath)) {
|
|
78
|
+
const skillMdPath = path.join(skillPath, "SKILL.md");
|
|
79
|
+
if (!pathExistsSync(skillPath)) {
|
|
80
|
+
return {
|
|
81
|
+
name,
|
|
82
|
+
skillPath,
|
|
83
|
+
skillMdPath,
|
|
84
|
+
exists: false,
|
|
85
|
+
readable: false,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
canRead(skillMdPath);
|
|
90
|
+
return {
|
|
91
|
+
name,
|
|
92
|
+
skillPath,
|
|
93
|
+
skillMdPath,
|
|
94
|
+
exists: true,
|
|
95
|
+
readable: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
catch {
|
|
99
|
+
return {
|
|
100
|
+
name,
|
|
101
|
+
skillPath,
|
|
102
|
+
skillMdPath,
|
|
103
|
+
exists: true,
|
|
104
|
+
readable: false,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
export function ensureWikiSkillInstall(wikiPath, packageRoot) {
|
|
109
|
+
const paths = resolveWorkspaceSkillPaths(wikiPath);
|
|
110
|
+
const packageRealPath = realpathSync(packageRoot);
|
|
111
|
+
const existing = inspectSkillInstall(paths.wikiSkillPath, "tiangong-wiki-skill");
|
|
112
|
+
ensureDirSync(paths.skillsRoot);
|
|
113
|
+
if (existing.exists) {
|
|
114
|
+
const stats = lstatSync(paths.wikiSkillPath);
|
|
115
|
+
if (stats.isSymbolicLink()) {
|
|
116
|
+
const currentRealPath = realpathSync(paths.wikiSkillPath);
|
|
117
|
+
if (currentRealPath === packageRealPath) {
|
|
118
|
+
return {
|
|
119
|
+
sourcePath: packageRoot,
|
|
120
|
+
skillPath: paths.wikiSkillPath,
|
|
121
|
+
status: "linked",
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
unlinkSync(paths.wikiSkillPath);
|
|
125
|
+
symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
|
|
126
|
+
return {
|
|
127
|
+
sourcePath: packageRoot,
|
|
128
|
+
skillPath: paths.wikiSkillPath,
|
|
129
|
+
status: "updated",
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
if (existing.readable) {
|
|
133
|
+
rmSync(paths.wikiSkillPath, { recursive: true, force: true });
|
|
134
|
+
symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
|
|
135
|
+
return {
|
|
136
|
+
sourcePath: packageRoot,
|
|
137
|
+
skillPath: paths.wikiSkillPath,
|
|
138
|
+
status: "updated",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
throw new AppError(`workspace skill path is occupied and cannot be reused: ${paths.wikiSkillPath}`, "config", {
|
|
142
|
+
skillName: "tiangong-wiki-skill",
|
|
143
|
+
skillPath: paths.wikiSkillPath,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
symlinkSync(packageRoot, paths.wikiSkillPath, "dir");
|
|
147
|
+
return {
|
|
148
|
+
sourcePath: packageRoot,
|
|
149
|
+
skillPath: paths.wikiSkillPath,
|
|
150
|
+
status: "linked",
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
function renderCommand(command, args) {
|
|
154
|
+
return [command, ...args]
|
|
155
|
+
.map((part) => (/[A-Za-z0-9_./:@+-]+/.test(part) ? part : JSON.stringify(part)))
|
|
156
|
+
.join(" ");
|
|
157
|
+
}
|
|
158
|
+
export function buildParserSkillInstallInvocation(skillName) {
|
|
159
|
+
const command = getNpxCommand();
|
|
160
|
+
const args = ["-y", "skills", "add", PARSER_SKILL_SOURCE, "--skill", skillName, "-a", "codex", "-y"];
|
|
161
|
+
return {
|
|
162
|
+
command,
|
|
163
|
+
args,
|
|
164
|
+
rendered: renderCommand(command, args),
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export function installParserSkill(skillName, workspaceRoot, options = {}) {
|
|
168
|
+
const skillPath = resolveWorkspaceSkillPath(workspaceRoot, skillName);
|
|
169
|
+
const current = inspectSkillInstall(skillPath, skillName);
|
|
170
|
+
const invocation = buildParserSkillInstallInvocation(skillName);
|
|
171
|
+
if (current.readable) {
|
|
172
|
+
return {
|
|
173
|
+
name: skillName,
|
|
174
|
+
skillPath,
|
|
175
|
+
skillMdPath: current.skillMdPath,
|
|
176
|
+
status: "existing",
|
|
177
|
+
command: invocation.rendered,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
options.output?.write(`Installing parser skill ${skillName}...\n`);
|
|
181
|
+
const result = spawnSync(invocation.command, invocation.args, {
|
|
182
|
+
cwd: workspaceRoot,
|
|
183
|
+
env: options.env ?? process.env,
|
|
184
|
+
encoding: "utf8",
|
|
185
|
+
});
|
|
186
|
+
if (result.error) {
|
|
187
|
+
throw new AppError(`failed to install parser skill ${skillName}: ${result.error.message}`, "runtime", {
|
|
188
|
+
skillName,
|
|
189
|
+
command: invocation.rendered,
|
|
190
|
+
cwd: workspaceRoot,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
if (result.status !== 0) {
|
|
194
|
+
throw new AppError(`failed to install parser skill ${skillName}`, "runtime", {
|
|
195
|
+
skillName,
|
|
196
|
+
command: invocation.rendered,
|
|
197
|
+
cwd: workspaceRoot,
|
|
198
|
+
exitCode: result.status,
|
|
199
|
+
stdout: result.stdout.trim(),
|
|
200
|
+
stderr: result.stderr.trim(),
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
const installed = inspectSkillInstall(skillPath, skillName);
|
|
204
|
+
if (!installed.readable) {
|
|
205
|
+
throw new AppError(`parser skill ${skillName} was installed but SKILL.md is missing or unreadable`, "runtime", {
|
|
206
|
+
skillName,
|
|
207
|
+
command: invocation.rendered,
|
|
208
|
+
cwd: workspaceRoot,
|
|
209
|
+
skillPath,
|
|
210
|
+
skillMdPath: installed.skillMdPath,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
name: skillName,
|
|
215
|
+
skillPath,
|
|
216
|
+
skillMdPath: installed.skillMdPath,
|
|
217
|
+
status: "installed",
|
|
218
|
+
command: invocation.rendered,
|
|
219
|
+
};
|
|
220
|
+
}
|