dsh-plugin-wiki-tools 0.11.0 → 0.13.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 +13 -15
- package/lib/vault.js +94 -7
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -106,7 +106,9 @@ export function createTools(vault, options = {}) {
|
|
|
106
106
|
+ 'CONFIGURED VAULT (run wiki_query to see its absolute root), not the session workspace. '
|
|
107
107
|
+ 'The content is the '
|
|
108
108
|
+ 'markdown body only — frontmatter is managed. With source_path, records the source hash in '
|
|
109
|
-
+ 'the ingest manifest and reports already_ingested for unchanged content unless force is set.'
|
|
109
|
+
+ 'the ingest manifest and reports already_ingested for unchanged content unless force is set. '
|
|
110
|
+
+ 'The result lists unresolved wikilinks in the written page (targets that match no page or alias) '
|
|
111
|
+
+ 'so dead links are fixed at write time, not at the next lint.',
|
|
110
112
|
parameters: {
|
|
111
113
|
title: {
|
|
112
114
|
type: 'string',
|
|
@@ -164,23 +166,19 @@ export function createTools(vault, options = {}) {
|
|
|
164
166
|
},
|
|
165
167
|
render: (args, value) => [{
|
|
166
168
|
type: 'text',
|
|
167
|
-
text: typeof value === 'object' && value !== null &&
|
|
168
|
-
? `wiki_write: ${
|
|
169
|
-
:
|
|
169
|
+
text: typeof value === 'object' && value !== null && value.alreadyIngested === true
|
|
170
|
+
? `wiki_write: skipped ${args.title} — source hash unchanged (pass force: true to re-ingest)`
|
|
171
|
+
: typeof value === 'object' && value !== null && 'path' in value
|
|
172
|
+
? `wiki_write: ${value.created ? 'created' : 'updated'} ${value.path}`
|
|
173
|
+
+ (Array.isArray(value.unresolvedLinks) && value.unresolvedLinks.length > 0
|
|
174
|
+
? `\nwiki_write: note — ${value.unresolvedLinks.length} unresolved wikilink(s) in this page: ${value.unresolvedLinks.slice(0, 8).join(', ')}${value.unresolvedLinks.length > 8 ? ', …' : ''}. Create those pages or fix the targets; until then lint reports them as dead links.`
|
|
175
|
+
: '')
|
|
176
|
+
: `wiki_write: skipped ${args.title} (source unchanged)`,
|
|
170
177
|
}],
|
|
171
178
|
},
|
|
172
179
|
async execute(args) {
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
sourcePath: args.source_path,
|
|
176
|
-
pagesCreated: [args.title],
|
|
177
|
-
})
|
|
178
|
-
if (tracked.alreadyIngested && args.force !== true) {
|
|
179
|
-
return { alreadyIngested: true, hash: tracked.hash, title: args.title }
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
const { extra_frontmatter: extraFrontmatter, ...rest } = args
|
|
183
|
-
return await vault.writePage({ ...rest, extraFrontmatter })
|
|
180
|
+
const { extra_frontmatter: extraFrontmatter, source_path: sourcePath, ...rest } = args
|
|
181
|
+
return await vault.writePage({ ...rest, extraFrontmatter, sourcePath })
|
|
184
182
|
},
|
|
185
183
|
presentCall: args => ({ card: 'generic', title: `Write wiki page: ${args.title}`, kind: 'other', rawInput: { title: args.title, type: args.type } }),
|
|
186
184
|
})
|
package/lib/vault.js
CHANGED
|
@@ -76,6 +76,18 @@ export function today() {
|
|
|
76
76
|
return new Date().toISOString().slice(0, 10)
|
|
77
77
|
}
|
|
78
78
|
|
|
79
|
+
/**
|
|
80
|
+
* Compare a stored manifest key against a normalized vault-relative path.
|
|
81
|
+
* Hand-written entries (the wiki-ingest skill era) may carry the platform's
|
|
82
|
+
* backslash separators; matching must not depend on them.
|
|
83
|
+
* @param {string} storedKey - key as written in `.raw/.manifest.json`.
|
|
84
|
+
* @param {string} rel - normalized `.raw/…` path.
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
function sameSourceKey(storedKey, rel) {
|
|
88
|
+
return storedKey === rel || storedKey.replace(/\\/g, '/') === rel
|
|
89
|
+
}
|
|
90
|
+
|
|
79
91
|
/**
|
|
80
92
|
* One vault root. All bookkeeping mutations go through {@link Vault.writePage},
|
|
81
93
|
* which completes frontmatter, updates the master index, and prepends a log
|
|
@@ -277,9 +289,13 @@ export class Vault {
|
|
|
277
289
|
* @param {Record<string, unknown>} [input.extraFrontmatter] - flat schema fields to merge
|
|
278
290
|
* (related, sources, question, answer_quality, entity_type, aliases, …); must stay flat and
|
|
279
291
|
* cannot override the managed fields.
|
|
280
|
-
* @
|
|
292
|
+
* @param {string} [input.sourcePath] - vault-relative `.raw/` source this page derives from;
|
|
293
|
+
* registers/refreshes the manifest entry (sha256 delta tracking) and returns `alreadyIngested`
|
|
294
|
+
* without writing when the hash is unchanged.
|
|
295
|
+
* @param {boolean} [input.force] - with sourcePath, write even when the source hash is unchanged.
|
|
296
|
+
* @returns {Promise<{ path: string, created: boolean, title: string } | { alreadyIngested: true, title: string, sourceHash: string, detail: string }>}
|
|
281
297
|
*/
|
|
282
|
-
async writePage({ type, title, content, tags, status, summary, extraFrontmatter }) {
|
|
298
|
+
async writePage({ type, title, content, tags, status, summary, extraFrontmatter, sourcePath, force }) {
|
|
283
299
|
const path = this.pagePath(type, title)
|
|
284
300
|
return await this.enqueue(path, async () => await this.withFileLock(relative(this.root, path).split(sep).join('/'), async () => {
|
|
285
301
|
await this.assertRoot()
|
|
@@ -290,6 +306,19 @@ export class Vault {
|
|
|
290
306
|
if (status !== undefined && !PAGE_STATUSES.includes(status)) {
|
|
291
307
|
throw new Error(`wiki-tools: status must be one of ${PAGE_STATUSES.join('/')} (got ${JSON.stringify(status)})`)
|
|
292
308
|
}
|
|
309
|
+
// Source registration is mechanical bookkeeping, not model work: when the
|
|
310
|
+
// page summarizes a `.raw/` source, the manifest entry (sha256 delta
|
|
311
|
+
// tracking) is written by this tool, and an unchanged hash is rejected
|
|
312
|
+
// unless the caller forces a re-ingest.
|
|
313
|
+
if (sourcePath !== undefined) {
|
|
314
|
+
const { hash, alreadyIngested } = await this.inspectSource(sourcePath)
|
|
315
|
+
if (alreadyIngested && force !== true) {
|
|
316
|
+
return {
|
|
317
|
+
alreadyIngested: true, title: cleanTitle, sourceHash: hash,
|
|
318
|
+
detail: 'source hash unchanged; pass force: true to re-ingest, or omit source_path for a plain page write',
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
293
322
|
const date = today()
|
|
294
323
|
const fields = {
|
|
295
324
|
...(existing?.fields ?? {}),
|
|
@@ -310,7 +339,25 @@ export class Vault {
|
|
|
310
339
|
`- ${existing === undefined ? 'Created' : 'Updated'}: [[${cleanTitle}]]`,
|
|
311
340
|
])
|
|
312
341
|
this.gitCommit(`wiki: ${existing === undefined ? 'create' : 'update'} ${cleanTitle}`)
|
|
313
|
-
|
|
342
|
+
if (sourcePath !== undefined) {
|
|
343
|
+
await this.trackSource({
|
|
344
|
+
sourcePath,
|
|
345
|
+
pagesCreated: existing === undefined ? [cleanTitle] : [],
|
|
346
|
+
pagesUpdated: existing === undefined ? [] : [cleanTitle],
|
|
347
|
+
})
|
|
348
|
+
}
|
|
349
|
+
// Surface dead-on-arrival wikilinks now, not at the next lint: forward
|
|
350
|
+
// links to pages about to be created are legitimate, so this is a note,
|
|
351
|
+
// never a rejection.
|
|
352
|
+
const pagesNow = await collectMarkdown(join(this.root, 'wiki'))
|
|
353
|
+
const names = new Set(pagesNow.map(page => page.name))
|
|
354
|
+
const aliases = buildAliasMap(pagesNow)
|
|
355
|
+
const unresolvedLinks = [...new Set(extractWikilinks(content))]
|
|
356
|
+
.filter(target => resolveLinkTarget(target, names, aliases) === undefined)
|
|
357
|
+
return {
|
|
358
|
+
path, created: existing === undefined, title: cleanTitle,
|
|
359
|
+
...(unresolvedLinks.length > 0 ? { unresolvedLinks } : {}),
|
|
360
|
+
}
|
|
314
361
|
}))
|
|
315
362
|
}
|
|
316
363
|
|
|
@@ -370,6 +417,9 @@ export class Vault {
|
|
|
370
417
|
const pages = await collectMarkdown(join(this.root, 'wiki'))
|
|
371
418
|
const target = pages.find(page => page.name === title)
|
|
372
419
|
if (target === undefined) throw new Error(`wiki-tools: no page named "${title}" exists in the vault`)
|
|
420
|
+
if (isMachineryPage(title)) {
|
|
421
|
+
throw new Error(`wiki-tools: "${title}" is vault machinery (index, log, hot cache, overview, sub-index, or lint report); renames would break conventions — fix the pages linking to it instead`)
|
|
422
|
+
}
|
|
373
423
|
if (pages.some(page => page.name !== title && page.name.toLowerCase() === cleanNew.toLowerCase())) {
|
|
374
424
|
throw new Error(`wiki-tools: filename "${cleanNew}.md" already exists; wikilinks need unique filenames`)
|
|
375
425
|
}
|
|
@@ -443,7 +493,9 @@ export class Vault {
|
|
|
443
493
|
await rm(absolute)
|
|
444
494
|
const manifestPath = join(this.root, '.raw', '.manifest.json')
|
|
445
495
|
const manifest = await readJson(manifestPath, { sources: {} })
|
|
446
|
-
|
|
496
|
+
for (const key of Object.keys(manifest.sources)) {
|
|
497
|
+
if (sameSourceKey(key, rel)) delete manifest.sources[key]
|
|
498
|
+
}
|
|
447
499
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
448
500
|
await this.prependLog(`## [${today()}] archive | ${rel}`, [
|
|
449
501
|
`- Archived: \`${rel}\` → \`.archive/${rel.slice('.raw/'.length)}\``,
|
|
@@ -559,8 +611,13 @@ export class Vault {
|
|
|
559
611
|
const hash = createHash('sha256').update(raw).digest('hex')
|
|
560
612
|
const manifestPath = join(this.root, '.raw', '.manifest.json')
|
|
561
613
|
const manifest = await readJson(manifestPath, { sources: {} })
|
|
562
|
-
|
|
614
|
+
let previous
|
|
615
|
+
let twinKey
|
|
616
|
+
for (const [key, entry] of Object.entries(manifest.sources)) {
|
|
617
|
+
if (sameSourceKey(key, rel)) { previous = entry; twinKey = key; break }
|
|
618
|
+
}
|
|
563
619
|
const alreadyIngested = previous !== undefined && previous.hash === hash
|
|
620
|
+
if (twinKey !== undefined && twinKey !== rel) delete manifest.sources[twinKey]
|
|
564
621
|
manifest.sources[rel] = {
|
|
565
622
|
hash,
|
|
566
623
|
ingested_at: today(),
|
|
@@ -572,6 +629,31 @@ export class Vault {
|
|
|
572
629
|
return { hash, alreadyIngested }
|
|
573
630
|
})
|
|
574
631
|
}
|
|
632
|
+
|
|
633
|
+
/**
|
|
634
|
+
* Hash one raw source and report whether the manifest already carries this
|
|
635
|
+
* exact hash — the wiki-ingest delta check's mechanical half.
|
|
636
|
+
* @param {string} sourcePath - vault-relative (or absolute) `.raw/` path.
|
|
637
|
+
* @returns {Promise<{ rel: string, hash: string, alreadyIngested: boolean }>}
|
|
638
|
+
*/
|
|
639
|
+
async inspectSource(sourcePath) {
|
|
640
|
+
const absolute = isAbsolute(sourcePath) ? sourcePath : join(this.root, sourcePath)
|
|
641
|
+
const rel = relative(this.root, absolute).split(sep).join('/')
|
|
642
|
+
if (!rel.startsWith('.raw/')) {
|
|
643
|
+
throw new Error('wiki-tools: source_path must be a vault-relative path under .raw/')
|
|
644
|
+
}
|
|
645
|
+
const raw = await readFile(absolute).catch(error => {
|
|
646
|
+
if (error.code === 'ENOENT') throw new Error(`wiki-tools: source ${sourcePath} not found under the vault`)
|
|
647
|
+
throw error
|
|
648
|
+
})
|
|
649
|
+
const hash = createHash('sha256').update(raw).digest('hex')
|
|
650
|
+
const manifest = await readJson(join(this.root, '.raw', '.manifest.json'), { sources: {} })
|
|
651
|
+
let previous
|
|
652
|
+
for (const [key, entry] of Object.entries(manifest.sources)) {
|
|
653
|
+
if (sameSourceKey(key, rel)) { previous = entry; break }
|
|
654
|
+
}
|
|
655
|
+
return { rel, hash, alreadyIngested: previous !== undefined && previous.hash === hash }
|
|
656
|
+
}
|
|
575
657
|
}
|
|
576
658
|
|
|
577
659
|
/**
|
|
@@ -761,7 +843,12 @@ export function extractWikilinks(content) {
|
|
|
761
843
|
* @returns {boolean} whether the page is machinery.
|
|
762
844
|
*/
|
|
763
845
|
export function isMachineryPage(name) {
|
|
764
|
-
|
|
846
|
+
const lowered = name.toLowerCase()
|
|
847
|
+
return META_FILENAMES.has(lowered)
|
|
765
848
|
|| name.startsWith('_')
|
|
766
|
-
||
|
|
849
|
+
|| lowered.startsWith('_')
|
|
850
|
+
// Case-insensitive with a real boundary: reports have been hand-retitled
|
|
851
|
+
// to "Lint Report …" in real sessions, and those are still machinery —
|
|
852
|
+
// but "Lint Reporter Profile" is content.
|
|
853
|
+
|| /^lint[- _]?report([- _].+)?$/i.test(name)
|
|
767
854
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wiki-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.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",
|