dsh-plugin-wiki-tools 0.11.0 → 0.12.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 +7 -14
- package/lib/vault.js +72 -4
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -164,23 +164,16 @@ export function createTools(vault, options = {}) {
|
|
|
164
164
|
},
|
|
165
165
|
render: (args, value) => [{
|
|
166
166
|
type: 'text',
|
|
167
|
-
text: typeof value === 'object' && value !== null &&
|
|
168
|
-
? `wiki_write: ${
|
|
169
|
-
:
|
|
167
|
+
text: typeof value === 'object' && value !== null && value.alreadyIngested === true
|
|
168
|
+
? `wiki_write: skipped ${args.title} — source hash unchanged (pass force: true to re-ingest)`
|
|
169
|
+
: typeof value === 'object' && value !== null && 'path' in value
|
|
170
|
+
? `wiki_write: ${value.created ? 'created' : 'updated'} ${value.path}`
|
|
171
|
+
: `wiki_write: skipped ${args.title} (source unchanged)`,
|
|
170
172
|
}],
|
|
171
173
|
},
|
|
172
174
|
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 })
|
|
175
|
+
const { extra_frontmatter: extraFrontmatter, source_path: sourcePath, ...rest } = args
|
|
176
|
+
return await vault.writePage({ ...rest, extraFrontmatter, sourcePath })
|
|
184
177
|
},
|
|
185
178
|
presentCall: args => ({ card: 'generic', title: `Write wiki page: ${args.title}`, kind: 'other', rawInput: { title: args.title, type: args.type } }),
|
|
186
179
|
})
|
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,6 +339,13 @@ export class Vault {
|
|
|
310
339
|
`- ${existing === undefined ? 'Created' : 'Updated'}: [[${cleanTitle}]]`,
|
|
311
340
|
])
|
|
312
341
|
this.gitCommit(`wiki: ${existing === undefined ? 'create' : 'update'} ${cleanTitle}`)
|
|
342
|
+
if (sourcePath !== undefined) {
|
|
343
|
+
await this.trackSource({
|
|
344
|
+
sourcePath,
|
|
345
|
+
pagesCreated: existing === undefined ? [cleanTitle] : [],
|
|
346
|
+
pagesUpdated: existing === undefined ? [] : [cleanTitle],
|
|
347
|
+
})
|
|
348
|
+
}
|
|
313
349
|
return { path, created: existing === undefined, title: cleanTitle }
|
|
314
350
|
}))
|
|
315
351
|
}
|
|
@@ -443,7 +479,9 @@ export class Vault {
|
|
|
443
479
|
await rm(absolute)
|
|
444
480
|
const manifestPath = join(this.root, '.raw', '.manifest.json')
|
|
445
481
|
const manifest = await readJson(manifestPath, { sources: {} })
|
|
446
|
-
|
|
482
|
+
for (const key of Object.keys(manifest.sources)) {
|
|
483
|
+
if (sameSourceKey(key, rel)) delete manifest.sources[key]
|
|
484
|
+
}
|
|
447
485
|
await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
|
|
448
486
|
await this.prependLog(`## [${today()}] archive | ${rel}`, [
|
|
449
487
|
`- Archived: \`${rel}\` → \`.archive/${rel.slice('.raw/'.length)}\``,
|
|
@@ -559,8 +597,13 @@ export class Vault {
|
|
|
559
597
|
const hash = createHash('sha256').update(raw).digest('hex')
|
|
560
598
|
const manifestPath = join(this.root, '.raw', '.manifest.json')
|
|
561
599
|
const manifest = await readJson(manifestPath, { sources: {} })
|
|
562
|
-
|
|
600
|
+
let previous
|
|
601
|
+
let twinKey
|
|
602
|
+
for (const [key, entry] of Object.entries(manifest.sources)) {
|
|
603
|
+
if (sameSourceKey(key, rel)) { previous = entry; twinKey = key; break }
|
|
604
|
+
}
|
|
563
605
|
const alreadyIngested = previous !== undefined && previous.hash === hash
|
|
606
|
+
if (twinKey !== undefined && twinKey !== rel) delete manifest.sources[twinKey]
|
|
564
607
|
manifest.sources[rel] = {
|
|
565
608
|
hash,
|
|
566
609
|
ingested_at: today(),
|
|
@@ -572,6 +615,31 @@ export class Vault {
|
|
|
572
615
|
return { hash, alreadyIngested }
|
|
573
616
|
})
|
|
574
617
|
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Hash one raw source and report whether the manifest already carries this
|
|
621
|
+
* exact hash — the wiki-ingest delta check's mechanical half.
|
|
622
|
+
* @param {string} sourcePath - vault-relative (or absolute) `.raw/` path.
|
|
623
|
+
* @returns {Promise<{ rel: string, hash: string, alreadyIngested: boolean }>}
|
|
624
|
+
*/
|
|
625
|
+
async inspectSource(sourcePath) {
|
|
626
|
+
const absolute = isAbsolute(sourcePath) ? sourcePath : join(this.root, sourcePath)
|
|
627
|
+
const rel = relative(this.root, absolute).split(sep).join('/')
|
|
628
|
+
if (!rel.startsWith('.raw/')) {
|
|
629
|
+
throw new Error('wiki-tools: source_path must be a vault-relative path under .raw/')
|
|
630
|
+
}
|
|
631
|
+
const raw = await readFile(absolute).catch(error => {
|
|
632
|
+
if (error.code === 'ENOENT') throw new Error(`wiki-tools: source ${sourcePath} not found under the vault`)
|
|
633
|
+
throw error
|
|
634
|
+
})
|
|
635
|
+
const hash = createHash('sha256').update(raw).digest('hex')
|
|
636
|
+
const manifest = await readJson(join(this.root, '.raw', '.manifest.json'), { sources: {} })
|
|
637
|
+
let previous
|
|
638
|
+
for (const [key, entry] of Object.entries(manifest.sources)) {
|
|
639
|
+
if (sameSourceKey(key, rel)) { previous = entry; break }
|
|
640
|
+
}
|
|
641
|
+
return { rel, hash, alreadyIngested: previous !== undefined && previous.hash === hash }
|
|
642
|
+
}
|
|
575
643
|
}
|
|
576
644
|
|
|
577
645
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-wiki-tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.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",
|