dsh-plugin-wiki-tools 0.4.0 → 0.6.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 +15 -2
- package/lib/lint.js +21 -4
- package/lib/search.js +10 -4
- package/lib/vault.js +46 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -76,7 +76,7 @@ export function createTools(vault, options = {}) {
|
|
|
76
76
|
},
|
|
77
77
|
render: (_args, value) => [{
|
|
78
78
|
type: 'text',
|
|
79
|
-
text: renderQueryResult(value)
|
|
79
|
+
text: `${renderVaultRoot(vault.root)}\n\n${renderQueryResult(value)}`,
|
|
80
80
|
}],
|
|
81
81
|
},
|
|
82
82
|
async execute(args) {
|
|
@@ -95,7 +95,9 @@ export function createTools(vault, options = {}) {
|
|
|
95
95
|
description:
|
|
96
96
|
'Write or update one wiki page with full bookkeeping: routes the page to its type folder, '
|
|
97
97
|
+ 'completes YAML frontmatter (type, title, status, created, updated, tags), guards filename '
|
|
98
|
-
+ 'uniqueness, updates the master index entry, and prepends a log entry.
|
|
98
|
+
+ 'uniqueness, updates the master index entry, and prepends a log entry. Writes go to the '
|
|
99
|
+
+ 'CONFIGURED VAULT (run wiki_query to see its absolute root), not the session workspace. '
|
|
100
|
+
+ 'The content is the '
|
|
99
101
|
+ 'markdown body only — frontmatter is managed. With source_path, records the source hash in '
|
|
100
102
|
+ 'the ingest manifest and reports already_ingested for unchanged content unless force is set.',
|
|
101
103
|
parameters: {
|
|
@@ -191,6 +193,17 @@ export function createTools(vault, options = {}) {
|
|
|
191
193
|
return [wikiQuery, wikiWrite, wikiLint]
|
|
192
194
|
}
|
|
193
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Render a compact byline disclosing the vault root. Without it, a model whose
|
|
198
|
+
* session workspace differs from the vault resolves `.raw/…` against the
|
|
199
|
+
* workspace and concludes the tools point somewhere else.
|
|
200
|
+
* @param {string} root - absolute vault root.
|
|
201
|
+
* @returns {string} the byline.
|
|
202
|
+
*/
|
|
203
|
+
function renderVaultRoot(root) {
|
|
204
|
+
return `wiki vault: ${root} — every wiki tool (query, write, lint) operates on this configured vault, not the session workspace. Resolve vault-relative paths like .raw/… and wiki/… against this root.`
|
|
205
|
+
}
|
|
206
|
+
|
|
194
207
|
/**
|
|
195
208
|
* Render a wiki_query result as the model-facing text. The render output is the
|
|
196
209
|
* ONLY content the model sees, so it carries the full payload — hot cache and
|
package/lib/lint.js
CHANGED
|
@@ -8,7 +8,14 @@
|
|
|
8
8
|
|
|
9
9
|
import { mkdir, readFile, writeFile } from 'node:fs/promises'
|
|
10
10
|
import { join } from 'node:path'
|
|
11
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
buildAliasMap,
|
|
13
|
+
collectMarkdown,
|
|
14
|
+
isMachineryPage,
|
|
15
|
+
resolveLinkTarget,
|
|
16
|
+
splitFrontmatter,
|
|
17
|
+
today,
|
|
18
|
+
} from './vault.js'
|
|
12
19
|
|
|
13
20
|
/** Frontmatter fields every content page must carry. */
|
|
14
21
|
const REQUIRED_FIELDS = ['type', 'status', 'created', 'updated', 'tags']
|
|
@@ -98,19 +105,29 @@ async function checkStaleIndexEntries(root, pages, add) {
|
|
|
98
105
|
* @param {Add} add - issue recorder.
|
|
99
106
|
* @returns {Map<string, string[]>} inbound links per page title.
|
|
100
107
|
*/
|
|
108
|
+
/**
|
|
109
|
+
* Wikilinks targeting pages that do not exist. Targets resolve through page
|
|
110
|
+
* titles first, then frontmatter `aliases` (Obsidian's order); only an
|
|
111
|
+
* unresolved target is dead.
|
|
112
|
+
* @param {Pages} pages - collected pages.
|
|
113
|
+
* @param {Add} add - issue recorder.
|
|
114
|
+
* @returns {Map<string, string[]>} inbound links per resolved page title.
|
|
115
|
+
*/
|
|
101
116
|
function checkDeadLinks(pages, add) {
|
|
102
117
|
const names = new Set(pages.map(page => page.name))
|
|
118
|
+
const aliases = buildAliasMap(pages)
|
|
103
119
|
const inbound = new Map(pages.map(page => [page.name, []]))
|
|
104
120
|
for (const page of pages) {
|
|
105
121
|
if (isMachineryPage(page.name)) continue
|
|
106
122
|
for (const target of page.links) {
|
|
107
|
-
|
|
108
|
-
|
|
123
|
+
const resolved = resolveLinkTarget(target, names, aliases)
|
|
124
|
+
if (resolved !== undefined) {
|
|
125
|
+
inbound.get(resolved)?.push(page.name)
|
|
109
126
|
continue
|
|
110
127
|
}
|
|
111
128
|
add('dead-link', 'error', page.name,
|
|
112
129
|
`links to [[${target}]] which does not exist`,
|
|
113
|
-
'Create a stub page or remove the link')
|
|
130
|
+
'Create a stub page, add the target to a page\u2019s aliases, or remove the link')
|
|
114
131
|
}
|
|
115
132
|
}
|
|
116
133
|
return inbound
|
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, isMachineryPage } from './vault.js'
|
|
11
|
+
import { buildAliasMap, collectMarkdown, isMachineryPage, resolveLinkTarget } from './vault.js'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Answer the quick mode: the hot cache and master index verbatim. The caller
|
|
@@ -43,17 +43,23 @@ export async function searchVault(root, { query, limit = 10 }) {
|
|
|
43
43
|
// quick-mode payload, and dated lint reports quote past wikilinks as records,
|
|
44
44
|
// not graph edges — so they neither appear as hits nor count as inbound.
|
|
45
45
|
const searchable = pages.filter(page => !isMachineryPage(page.name))
|
|
46
|
+
const names = new Set(pages.map(page => page.name))
|
|
47
|
+
const aliases = buildAliasMap(pages)
|
|
46
48
|
const inbound = new Map(pages.map(page => [page.name, []]))
|
|
47
49
|
for (const page of searchable) {
|
|
48
|
-
// One inbound record per source page, however many times it links
|
|
50
|
+
// One inbound record per source page, however many times it links; alias
|
|
51
|
+
// targets resolve to their owning page.
|
|
49
52
|
for (const target of new Set(page.links)) {
|
|
50
|
-
|
|
53
|
+
const resolved = resolveLinkTarget(target, names, aliases)
|
|
54
|
+
if (resolved !== undefined) inbound.get(resolved)?.push(page.name)
|
|
51
55
|
}
|
|
52
56
|
}
|
|
53
57
|
const results = []
|
|
54
58
|
let totalMatches = 0
|
|
55
59
|
for (const page of searchable) {
|
|
56
|
-
const
|
|
60
|
+
const aliasText = (page.aliases ?? []).join(' ').toLowerCase()
|
|
61
|
+
const titleHits = (countOccurrences(page.name.toLowerCase(), needle)
|
|
62
|
+
+ (aliasText.includes(needle) ? 1 : 0)) * 5
|
|
57
63
|
const tags = Array.isArray(page.fields?.tags) ? page.fields.tags.join(' ').toLowerCase() : ''
|
|
58
64
|
const tagHits = tags.includes(needle) ? 4 : 0
|
|
59
65
|
const headings = page.content.split('\n').filter(line => line.startsWith('#')).join('\n').toLowerCase()
|
package/lib/vault.js
CHANGED
|
@@ -279,7 +279,7 @@ export class Vault {
|
|
|
279
279
|
*/
|
|
280
280
|
async trackSource({ sourcePath, pagesCreated = [], pagesUpdated = [] }) {
|
|
281
281
|
const absolute = isAbsolute(sourcePath) ? sourcePath : join(this.root, sourcePath)
|
|
282
|
-
const rel = relative(this.root, absolute)
|
|
282
|
+
const rel = relative(this.root, absolute).split(sep).join('/')
|
|
283
283
|
const raw = await readFile(absolute).catch(error => {
|
|
284
284
|
if (error.code === 'ENOENT') throw new Error(`wiki-tools: source ${sourcePath} not found under the vault`)
|
|
285
285
|
throw error
|
|
@@ -382,12 +382,57 @@ export async function collectMarkdown(directory) {
|
|
|
382
382
|
fields,
|
|
383
383
|
content,
|
|
384
384
|
links: extractWikilinks(content),
|
|
385
|
+
aliases: frontmatterAliases(fields),
|
|
385
386
|
})
|
|
386
387
|
}
|
|
387
388
|
}
|
|
389
|
+
pages.sort((left, right) => left.name.localeCompare(right.name))
|
|
388
390
|
return pages
|
|
389
391
|
}
|
|
390
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Read a page's declared `aliases` frontmatter field (Obsidian resolves these
|
|
395
|
+
* as link targets): a string or list of strings.
|
|
396
|
+
* @param {Record<string, unknown> | undefined} fields - parsed frontmatter.
|
|
397
|
+
* @returns {string[]} declared aliases, empty when absent or malformed.
|
|
398
|
+
*/
|
|
399
|
+
function frontmatterAliases(fields) {
|
|
400
|
+
const raw = fields?.aliases
|
|
401
|
+
if (typeof raw === 'string') return raw.trim().length > 0 ? [raw.trim()] : []
|
|
402
|
+
if (Array.isArray(raw)) return raw.filter(alias => typeof alias === 'string' && alias.trim().length > 0).map(alias => alias.trim())
|
|
403
|
+
return []
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Build the alias resolution map every link consumer shares: alias → page
|
|
408
|
+
* title. Earlier pages win a duplicate alias, deterministically, because
|
|
409
|
+
* {@link collectMarkdown} sorts by name.
|
|
410
|
+
* @param {{ name: string, aliases?: string[] }[]} pages - collected pages.
|
|
411
|
+
* @returns {Map<string, string>} alias → owning page title.
|
|
412
|
+
*/
|
|
413
|
+
export function buildAliasMap(pages) {
|
|
414
|
+
const map = new Map()
|
|
415
|
+
for (const page of pages) {
|
|
416
|
+
for (const alias of page.aliases ?? []) {
|
|
417
|
+
if (!map.has(alias)) map.set(alias, page.name)
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
return map
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Resolve one wikilink target to a page title: direct name hit first, then the
|
|
425
|
+
* alias map (Obsidian's resolution order).
|
|
426
|
+
* @param {string} target - the raw link target.
|
|
427
|
+
* @param {Set<string>} names - every page title.
|
|
428
|
+
* @param {Map<string, string>} aliases - alias → page title.
|
|
429
|
+
* @returns {string | undefined} the resolved page title, or undefined when dead.
|
|
430
|
+
*/
|
|
431
|
+
export function resolveLinkTarget(target, names, aliases) {
|
|
432
|
+
if (names.has(target)) return target
|
|
433
|
+
return aliases.get(target)
|
|
434
|
+
}
|
|
435
|
+
|
|
391
436
|
/**
|
|
392
437
|
* Extract wikilink targets from markdown, dropping aliases and heading anchors.
|
|
393
438
|
* Fenced code blocks and inline code spans are not links: examples written as
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wiki-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.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",
|