dsh-plugin-wiki-tools 0.2.1 → 0.4.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/index.js +64 -6
- package/lib/lint.js +19 -6
- package/lib/search.js +4 -4
- package/lib/vault.js +20 -2
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -76,9 +76,7 @@ export function createTools(vault, options = {}) {
|
|
|
76
76
|
},
|
|
77
77
|
render: (_args, value) => [{
|
|
78
78
|
type: 'text',
|
|
79
|
-
text:
|
|
80
|
-
? `wiki_query: ${value.results.length} of ${value.totalMatches} matching pages`
|
|
81
|
-
: 'wiki_query: returned hot cache and master index',
|
|
79
|
+
text: renderQueryResult(value),
|
|
82
80
|
}],
|
|
83
81
|
},
|
|
84
82
|
async execute(args) {
|
|
@@ -181,9 +179,7 @@ export function createTools(vault, options = {}) {
|
|
|
181
179
|
},
|
|
182
180
|
render: (_args, value) => [{
|
|
183
181
|
type: 'text',
|
|
184
|
-
text:
|
|
185
|
-
? `wiki_lint: ${value.summary.issues} issues across ${value.summary.pagesScanned} pages; report at ${value.reportPath ?? '(not written)'}`
|
|
186
|
-
: 'wiki_lint: failed',
|
|
182
|
+
text: renderLintResult(value),
|
|
187
183
|
}],
|
|
188
184
|
},
|
|
189
185
|
async execute() {
|
|
@@ -195,6 +191,68 @@ export function createTools(vault, options = {}) {
|
|
|
195
191
|
return [wikiQuery, wikiWrite, wikiLint]
|
|
196
192
|
}
|
|
197
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Render a wiki_query result as the model-facing text. The render output is the
|
|
196
|
+
* ONLY content the model sees, so it carries the full payload — hot cache and
|
|
197
|
+
* index verbatim in quick mode, every result with path, snippets, and link
|
|
198
|
+
* context in standard mode — not a summary line.
|
|
199
|
+
* @param {unknown} value - the execute return value.
|
|
200
|
+
* @returns {string} the model-facing result text.
|
|
201
|
+
*/
|
|
202
|
+
function renderQueryResult(value) {
|
|
203
|
+
if (typeof value !== 'object' || value === null) return 'wiki_query: no result'
|
|
204
|
+
const result = value
|
|
205
|
+
if (result.mode === 'quick') {
|
|
206
|
+
const parts = []
|
|
207
|
+
if (typeof result.hot === 'string' && result.hot.length > 0) {
|
|
208
|
+
parts.push(`--- wiki/hot.md (recent context cache) ---\n${capText(result.hot, 6000)}`)
|
|
209
|
+
}
|
|
210
|
+
if (typeof result.index === 'string' && result.index.length > 0) {
|
|
211
|
+
parts.push(`--- wiki/index.md (master catalog) ---\n${capText(result.index, 8000)}`)
|
|
212
|
+
}
|
|
213
|
+
if (parts.length === 0) return 'wiki_query (quick): the vault has no hot.md or index.md yet'
|
|
214
|
+
return `wiki_query (quick): hot cache and master index follow. Read these before any page.\n\n${parts.join('\n\n')}`
|
|
215
|
+
}
|
|
216
|
+
if (!Array.isArray(result.results)) return 'wiki_query: no result'
|
|
217
|
+
const lines = [`wiki_query: ${result.results.length} of ${result.totalMatches} matching pages. Open a page with the fs read tool for full content.`]
|
|
218
|
+
for (const hit of result.results) {
|
|
219
|
+
lines.push(`\n### ${hit.name}`)
|
|
220
|
+
lines.push(`path: ${hit.path} · score ${hit.score} · inbound ${hit.inbound.length}${hit.inbound.length > 0 ? ` (${hit.inbound.slice(0, 5).join(', ')})` : ''} · outbound ${hit.outbound}`)
|
|
221
|
+
for (const snippet of hit.snippets) lines.push(`> ${snippet}`)
|
|
222
|
+
}
|
|
223
|
+
return capText(lines.join('\n'), 12000)
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Render a wiki_lint result: every issue with page, severity, and suggestion.
|
|
228
|
+
* @param {unknown} value - the execute return value.
|
|
229
|
+
* @returns {string} the model-facing result text.
|
|
230
|
+
*/
|
|
231
|
+
function renderLintResult(value) {
|
|
232
|
+
if (typeof value !== 'object' || value === null || !('summary' in value)) return 'wiki_lint: failed'
|
|
233
|
+
const result = value
|
|
234
|
+
const lines = [
|
|
235
|
+
`wiki_lint: ${result.summary.issues} issues across ${result.summary.pagesScanned} pages.`,
|
|
236
|
+
`checks: ${Object.entries(result.summary.byCheck ?? {}).map(([check, count]) => `${check}×${count}`).join(', ') || 'none'}`,
|
|
237
|
+
`full report: ${typeof result.reportPath === 'string' ? result.reportPath : '(not written)'}`,
|
|
238
|
+
]
|
|
239
|
+
for (const issue of (result.issues ?? []).slice(0, 60)) {
|
|
240
|
+
lines.push(`- [${issue.severity}] ${issue.check} · ${issue.page}: ${issue.detail} → ${issue.suggestion}`)
|
|
241
|
+
}
|
|
242
|
+
if ((result.issues ?? []).length > 60) lines.push(`… and ${result.issues.length - 60} more (see the report file)`)
|
|
243
|
+
return lines.join('\n')
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Cap one text block, marking the cut.
|
|
248
|
+
* @param {string} text - full text.
|
|
249
|
+
* @param {number} max - maximum characters kept.
|
|
250
|
+
* @returns {string} the capped text.
|
|
251
|
+
*/
|
|
252
|
+
function capText(text, max) {
|
|
253
|
+
return text.length <= max ? text : `${text.slice(0, max)}\n… (truncated at ${max} characters)`
|
|
254
|
+
}
|
|
255
|
+
|
|
198
256
|
/**
|
|
199
257
|
* Validate the deployment config and register the three wiki tools.
|
|
200
258
|
* @param {import('@deepseek-ai/cordis').Context} ctx - registrant context carrying the tool registry.
|
package/lib/lint.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
|
-
import { collectMarkdown,
|
|
11
|
+
import { collectMarkdown, isMachineryPage, splitFrontmatter, today } from './vault.js'
|
|
12
12
|
|
|
13
13
|
/** Frontmatter fields every content page must carry. */
|
|
14
14
|
const REQUIRED_FIELDS = ['type', 'status', 'created', 'updated', 'tags']
|
|
@@ -53,10 +53,9 @@ export async function lintVault(root) {
|
|
|
53
53
|
* @param {Add} add - issue recorder.
|
|
54
54
|
*/
|
|
55
55
|
function checkDuplicateFilenames(pages, add) {
|
|
56
|
-
const isMachinery = page => META_FILENAMES.has(page.name.toLowerCase()) || page.name.startsWith('_')
|
|
57
56
|
const seen = new Map()
|
|
58
57
|
for (const page of pages) {
|
|
59
|
-
if (
|
|
58
|
+
if (isMachineryPage(page.name)) continue
|
|
60
59
|
const key = page.name.toLowerCase()
|
|
61
60
|
if (seen.has(key)) {
|
|
62
61
|
add('duplicate-filename', 'error', page.name,
|
|
@@ -103,6 +102,7 @@ function checkDeadLinks(pages, add) {
|
|
|
103
102
|
const names = new Set(pages.map(page => page.name))
|
|
104
103
|
const inbound = new Map(pages.map(page => [page.name, []]))
|
|
105
104
|
for (const page of pages) {
|
|
105
|
+
if (isMachineryPage(page.name)) continue
|
|
106
106
|
for (const target of page.links) {
|
|
107
107
|
if (names.has(target)) {
|
|
108
108
|
inbound.get(target)?.push(page.name)
|
|
@@ -126,7 +126,7 @@ function checkDeadLinks(pages, add) {
|
|
|
126
126
|
function checkOrphans(pages, inbound, indexed, add) {
|
|
127
127
|
for (const page of pages) {
|
|
128
128
|
const base = page.name.toLowerCase()
|
|
129
|
-
if (
|
|
129
|
+
if (isMachineryPage(page.name)) continue
|
|
130
130
|
const links = inbound.get(page.name) ?? []
|
|
131
131
|
const isIndexed = indexed.has(page.name)
|
|
132
132
|
if (links.length === 0 && !isIndexed) {
|
|
@@ -145,7 +145,7 @@ function checkOrphans(pages, inbound, indexed, add) {
|
|
|
145
145
|
function checkFrontmatterGaps(pages, add) {
|
|
146
146
|
for (const page of pages) {
|
|
147
147
|
const base = page.name.toLowerCase()
|
|
148
|
-
if (
|
|
148
|
+
if (isMachineryPage(page.name)) continue
|
|
149
149
|
const missing = REQUIRED_FIELDS.filter(field => page.fields?.[field] === undefined)
|
|
150
150
|
if (missing.length > 0) {
|
|
151
151
|
add('frontmatter-gap', 'warn', page.name,
|
|
@@ -162,12 +162,25 @@ function checkFrontmatterGaps(pages, add) {
|
|
|
162
162
|
*/
|
|
163
163
|
function checkEmptySections(pages, add) {
|
|
164
164
|
for (const page of pages) {
|
|
165
|
-
if (
|
|
165
|
+
if (isMachineryPage(page.name)) continue
|
|
166
|
+
// Fence-aware scan: lines inside code fences are content (a section whose
|
|
167
|
+
// body is only code is not empty) and heading-like comment lines inside
|
|
168
|
+
// fences never open sections.
|
|
166
169
|
const lines = [...page.content.split('\n'), '# __eof__ sentinel']
|
|
167
170
|
let heading
|
|
168
171
|
let headingLevel = 0
|
|
169
172
|
let hasContent = false
|
|
173
|
+
let inFence = false
|
|
170
174
|
for (const line of lines) {
|
|
175
|
+
if (line.startsWith('```')) {
|
|
176
|
+
inFence = !inFence
|
|
177
|
+
hasContent = true
|
|
178
|
+
continue
|
|
179
|
+
}
|
|
180
|
+
if (inFence) {
|
|
181
|
+
hasContent = true
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
171
184
|
const match = /^(#{1,6}) (.+)$/.exec(line)
|
|
172
185
|
if (match !== null) {
|
|
173
186
|
const nextLevel = match[1].length
|
package/lib/search.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import { readFile } from 'node:fs/promises'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
|
-
import { collectMarkdown,
|
|
11
|
+
import { collectMarkdown, isMachineryPage } from './vault.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Answer the quick mode: the hot cache and master index verbatim. The caller
|
|
@@ -40,9 +40,9 @@ export async function searchVault(root, { query, limit = 10 }) {
|
|
|
40
40
|
const pages = await collectMarkdown(join(root, 'wiki'))
|
|
41
41
|
// The link graph covers every file, but results exclude vault machinery:
|
|
42
42
|
// the index matches nearly every term by construction and is already the
|
|
43
|
-
// quick-mode payload
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
// quick-mode payload, and dated lint reports quote past wikilinks as records,
|
|
44
|
+
// not graph edges — so they neither appear as hits nor count as inbound.
|
|
45
|
+
const searchable = pages.filter(page => !isMachineryPage(page.name))
|
|
46
46
|
const inbound = new Map(pages.map(page => [page.name, []]))
|
|
47
47
|
for (const page of searchable) {
|
|
48
48
|
// One inbound record per source page, however many times it links.
|
package/lib/vault.js
CHANGED
|
@@ -390,15 +390,33 @@ export async function collectMarkdown(directory) {
|
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
392
|
* Extract wikilink targets from markdown, dropping aliases and heading anchors.
|
|
393
|
+
* Fenced code blocks and inline code spans are not links: examples written as
|
|
394
|
+
* `` `[[Target]]` `` must not join the graph.
|
|
393
395
|
* @param {string} content - markdown body.
|
|
394
396
|
* @returns {string[]} link targets in order of appearance.
|
|
395
397
|
*/
|
|
396
398
|
export function extractWikilinks(content) {
|
|
397
399
|
const links = []
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
+
const withoutCode = content
|
|
401
|
+
.replace(/```[\s\S]*?```/g, '')
|
|
402
|
+
.replace(/`[^`\n]*`/g, '')
|
|
403
|
+
for (const match of withoutCode.matchAll(/\[\[([^\]]+)\]\]/g)) {
|
|
400
404
|
const target = match[1].split('|')[0].split('#')[0].trim()
|
|
401
405
|
if (target.length > 0) links.push(target)
|
|
402
406
|
}
|
|
403
407
|
return links
|
|
404
408
|
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Whether a page basename is vault machinery rather than content: the index,
|
|
412
|
+
* log, hot cache, overview, per-folder sub-indexes, and dated lint reports.
|
|
413
|
+
* Lint reports quote past issues verbatim, so their wikilinks are records, not
|
|
414
|
+
* graph edges.
|
|
415
|
+
* @param {string} name - page basename without extension.
|
|
416
|
+
* @returns {boolean} whether the page is machinery.
|
|
417
|
+
*/
|
|
418
|
+
export function isMachineryPage(name) {
|
|
419
|
+
return META_FILENAMES.has(name.toLowerCase())
|
|
420
|
+
|| name.startsWith('_')
|
|
421
|
+
|| /^lint-report-/.test(name)
|
|
422
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wiki-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Native DeepSeek Harness tools for an Obsidian wiki vault: wiki_query, wiki_write, and wiki_lint implement the mechanical core (path routing, delta tracking, index/log bookkeeping, health checks) of the wiki skill suite.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|