skillwiki 0.9.22 → 0.9.23
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/dist/cli.js +231 -39
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3525,6 +3525,7 @@ function buildCliSurface() {
|
|
|
3525
3525
|
memoryCmd2.command("topics").option("--project <slug>").option("--limit <n>").option("--wiki <name>");
|
|
3526
3526
|
memoryCmd2.command("index").requiredOption("--project <slug>").option("--check").option("--if-stale").option("--wiki <name>");
|
|
3527
3527
|
memoryCmd2.command("recall").requiredOption("--project <slug>").requiredOption("--topic <slug>").option("--scope <scope>").option("--limit <n>").option("--wiki <name>");
|
|
3528
|
+
memoryCmd2.command("review").requiredOption("--project <slug>").option("--dry-run").option("--wiki <name>");
|
|
3528
3529
|
memoryCmd2.command("import").requiredOption("--from <path>").requiredOption("--project <slug>").option("--dry-run").option("--apply").option("--max-bytes <n>").option("--wiki <name>");
|
|
3529
3530
|
const fleetCmd2 = program2.commands.find((c) => c.name() === "fleet");
|
|
3530
3531
|
fleetCmd2.command("validate");
|
|
@@ -9138,6 +9139,41 @@ async function runMemoryRecall(input) {
|
|
|
9138
9139
|
})
|
|
9139
9140
|
};
|
|
9140
9141
|
}
|
|
9142
|
+
async function runMemoryReview(input) {
|
|
9143
|
+
const scan = await scanVault(input.vault);
|
|
9144
|
+
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
9145
|
+
const state = await buildMemoryIndexState(scan.data.allMarkdown, input.project);
|
|
9146
|
+
const checked = await checkMemoryIndex(input.vault, input.project, state);
|
|
9147
|
+
if (!checked.ok) {
|
|
9148
|
+
return {
|
|
9149
|
+
exitCode: checked.error.exitCode,
|
|
9150
|
+
result: checked.error.result
|
|
9151
|
+
};
|
|
9152
|
+
}
|
|
9153
|
+
const findings = buildMemoryReviewFindings({
|
|
9154
|
+
project: input.project,
|
|
9155
|
+
status: checked.data,
|
|
9156
|
+
indexState: state,
|
|
9157
|
+
reviewPages: state.reviewPages
|
|
9158
|
+
});
|
|
9159
|
+
const generatedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
9160
|
+
return {
|
|
9161
|
+
exitCode: ExitCode.OK,
|
|
9162
|
+
result: ok({
|
|
9163
|
+
project: input.project,
|
|
9164
|
+
generated_at: generatedAt,
|
|
9165
|
+
cache: {
|
|
9166
|
+
present: checked.data.cache_present,
|
|
9167
|
+
...checked.data.generated_at ? { generated_at: checked.data.generated_at } : {},
|
|
9168
|
+
stale: checked.data.stale,
|
|
9169
|
+
source_hash_mismatches: checked.data.drift.changed_sources
|
|
9170
|
+
},
|
|
9171
|
+
findings,
|
|
9172
|
+
files_written: [],
|
|
9173
|
+
humanHint: renderMemoryReviewHint(input.project, checked.data, findings)
|
|
9174
|
+
})
|
|
9175
|
+
};
|
|
9176
|
+
}
|
|
9141
9177
|
async function runMemoryImport(input) {
|
|
9142
9178
|
const scan = await scanVault(input.vault);
|
|
9143
9179
|
if (!scan.ok) return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
@@ -9197,14 +9233,18 @@ async function buildMemoryIndexState(pages, project) {
|
|
|
9197
9233
|
const warnings = [];
|
|
9198
9234
|
const sourcePages = dedupePages(pages);
|
|
9199
9235
|
const sources = [];
|
|
9236
|
+
const reviewPages = [];
|
|
9200
9237
|
for (const page of sourcePages) {
|
|
9201
|
-
const
|
|
9202
|
-
if (
|
|
9238
|
+
const memoryPage = await readMemoryPage(page, project, warnings);
|
|
9239
|
+
if (!memoryPage) continue;
|
|
9240
|
+
sources.push(memoryPage.source);
|
|
9241
|
+
if (memoryPage.reviewPage) reviewPages.push(memoryPage.reviewPage);
|
|
9203
9242
|
}
|
|
9204
9243
|
return {
|
|
9205
9244
|
sources,
|
|
9206
9245
|
topics: buildTopics(project, sources),
|
|
9207
|
-
warnings
|
|
9246
|
+
warnings,
|
|
9247
|
+
reviewPages
|
|
9208
9248
|
};
|
|
9209
9249
|
}
|
|
9210
9250
|
async function checkMemoryIndex(vault, project, current) {
|
|
@@ -9290,6 +9330,175 @@ function emptyMemoryIndexDrift() {
|
|
|
9290
9330
|
changed_sources: []
|
|
9291
9331
|
};
|
|
9292
9332
|
}
|
|
9333
|
+
async function readMemoryPage(page, project, warnings) {
|
|
9334
|
+
const text = await readPage(page);
|
|
9335
|
+
const fm = extractFrontmatter(text);
|
|
9336
|
+
if (!fm.ok) return null;
|
|
9337
|
+
const rawTopics = typeof fm.data.memory_topics === "string" ? [fm.data.memory_topics] : stringArray(fm.data.memory_topics);
|
|
9338
|
+
if (rawTopics.length === 0) return null;
|
|
9339
|
+
const topics = normalizeMemoryTopics(fm.data.memory_topics, page.relPath, warnings);
|
|
9340
|
+
if (topics.length === 0) return null;
|
|
9341
|
+
const scope = stringField2(fm.data.memory_scope);
|
|
9342
|
+
const projects = memoryProjects(page.relPath, fm.data);
|
|
9343
|
+
if (!projects.includes(project) && scope !== "global" && scope !== "cross-agent") return null;
|
|
9344
|
+
const privacy = stringField2(fm.data.memory_privacy) || "local";
|
|
9345
|
+
if (privacy === "secret-blocked") {
|
|
9346
|
+
warnings.push(`${page.relPath}: skipped secret-blocked memory`);
|
|
9347
|
+
return null;
|
|
9348
|
+
}
|
|
9349
|
+
const status = stringField2(fm.data.memory_status) || "active";
|
|
9350
|
+
if (status === "archived" || status === "rejected") return null;
|
|
9351
|
+
const metadataGaps = [];
|
|
9352
|
+
if (!scope) metadataGaps.push("memory_scope");
|
|
9353
|
+
if (!stringField2(fm.data.memory_policy)) metadataGaps.push("memory_policy");
|
|
9354
|
+
if (!stringField2(fm.data.memory_privacy)) metadataGaps.push("memory_privacy");
|
|
9355
|
+
if (!stringField2(fm.data.memory_status)) metadataGaps.push("memory_status");
|
|
9356
|
+
if (!stringField2(fm.data.last_seen) && !stringField2(fm.data.updated) && !stringField2(fm.data.ingested)) {
|
|
9357
|
+
metadataGaps.push("freshness");
|
|
9358
|
+
}
|
|
9359
|
+
const split = splitFrontmatter(text);
|
|
9360
|
+
const body = split.ok ? split.data.body : text;
|
|
9361
|
+
const updated = stringField2(fm.data.last_seen) || stringField2(fm.data.updated) || stringField2(fm.data.ingested) || dateFromPath2(page.relPath);
|
|
9362
|
+
const title = stringField2(fm.data.title) || page.relPath.split("/").pop()?.replace(/\.md$/, "") || page.relPath;
|
|
9363
|
+
return {
|
|
9364
|
+
source: {
|
|
9365
|
+
path: page.relPath,
|
|
9366
|
+
title,
|
|
9367
|
+
summary: summarize2(body),
|
|
9368
|
+
updated,
|
|
9369
|
+
hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
|
|
9370
|
+
topics,
|
|
9371
|
+
project,
|
|
9372
|
+
...stringField2(fm.data.memory_kind) ? { memory_kind: stringField2(fm.data.memory_kind) } : {},
|
|
9373
|
+
...scope ? { memory_scope: scope } : {},
|
|
9374
|
+
...stringField2(fm.data.memory_policy) ? { memory_policy: stringField2(fm.data.memory_policy) } : {},
|
|
9375
|
+
memory_privacy: privacy,
|
|
9376
|
+
memory_status: status
|
|
9377
|
+
},
|
|
9378
|
+
...metadataGaps.length > 0 ? {
|
|
9379
|
+
reviewPage: {
|
|
9380
|
+
path: page.relPath,
|
|
9381
|
+
metadataGaps
|
|
9382
|
+
}
|
|
9383
|
+
} : {}
|
|
9384
|
+
};
|
|
9385
|
+
}
|
|
9386
|
+
function buildMemoryReviewFindings(input) {
|
|
9387
|
+
const findings = [];
|
|
9388
|
+
if (!input.status.cache_present) {
|
|
9389
|
+
findings.push({
|
|
9390
|
+
id: `cache_missing:${input.project}`,
|
|
9391
|
+
severity: "warn",
|
|
9392
|
+
kind: "cache_missing",
|
|
9393
|
+
message: `memory cache for ${input.project} is missing`,
|
|
9394
|
+
suggested_action: `Run skillwiki memory index --project ${input.project} to build the local cache.`
|
|
9395
|
+
});
|
|
9396
|
+
} else if (input.status.stale) {
|
|
9397
|
+
findings.push({
|
|
9398
|
+
id: `cache_stale:${input.project}`,
|
|
9399
|
+
severity: "warn",
|
|
9400
|
+
kind: "cache_stale",
|
|
9401
|
+
message: `memory cache for ${input.project} is stale: ${input.status.drift.missing_sources.length} missing, ${input.status.drift.removed_sources.length} removed, ${input.status.drift.changed_sources.length} changed source(s)`,
|
|
9402
|
+
suggested_action: `Run skillwiki memory index --project ${input.project} or --if-stale to refresh the local cache.`
|
|
9403
|
+
});
|
|
9404
|
+
}
|
|
9405
|
+
for (const path of input.status.drift.changed_sources) {
|
|
9406
|
+
findings.push({
|
|
9407
|
+
id: `source_hash_drift:${path}`,
|
|
9408
|
+
severity: "warn",
|
|
9409
|
+
kind: "source_hash_drift",
|
|
9410
|
+
path,
|
|
9411
|
+
message: `${path} changed since the last cache build`,
|
|
9412
|
+
suggested_action: `Refresh the cache for ${input.project} and review whether the source summary still matches.`
|
|
9413
|
+
});
|
|
9414
|
+
}
|
|
9415
|
+
for (const warning of input.indexState.warnings) {
|
|
9416
|
+
const parsed = parseInvalidTopicWarning(warning);
|
|
9417
|
+
if (!parsed) continue;
|
|
9418
|
+
findings.push({
|
|
9419
|
+
id: `invalid_topic_slug:${parsed.path}:${parsed.topic}`,
|
|
9420
|
+
severity: "warn",
|
|
9421
|
+
kind: "invalid_topic_slug",
|
|
9422
|
+
path: parsed.path,
|
|
9423
|
+
topic: parsed.topic,
|
|
9424
|
+
message: `${parsed.path} declares invalid memory topic slug ${parsed.topic}`,
|
|
9425
|
+
suggested_action: "Normalize the topic slug to lowercase kebab-case so memory index and review can include it."
|
|
9426
|
+
});
|
|
9427
|
+
}
|
|
9428
|
+
for (const page of input.reviewPages) {
|
|
9429
|
+
if (page.metadataGaps.length === 0) continue;
|
|
9430
|
+
findings.push({
|
|
9431
|
+
id: `metadata_gap:${page.path}`,
|
|
9432
|
+
severity: "warn",
|
|
9433
|
+
kind: "metadata_gap",
|
|
9434
|
+
path: page.path,
|
|
9435
|
+
message: `${page.path} is missing explicit memory metadata: ${page.metadataGaps.join(", ")}`,
|
|
9436
|
+
suggested_action: "Add the missing frontmatter fields before relying on this source for cross-device memory review."
|
|
9437
|
+
});
|
|
9438
|
+
}
|
|
9439
|
+
const byTopic = groupSourcesByTopic(input.indexState.sources);
|
|
9440
|
+
for (const [topic, sources] of byTopic.entries()) {
|
|
9441
|
+
if (sources.length === 1) {
|
|
9442
|
+
findings.push({
|
|
9443
|
+
id: `sparse_topic:${topic}`,
|
|
9444
|
+
severity: "info",
|
|
9445
|
+
kind: "sparse_topic",
|
|
9446
|
+
topic,
|
|
9447
|
+
...sources[0]?.path ? { path: sources[0].path } : {},
|
|
9448
|
+
message: `topic ${topic} has only one active memory source`,
|
|
9449
|
+
suggested_action: "Decide whether the topic needs more coverage or should stay intentionally narrow."
|
|
9450
|
+
});
|
|
9451
|
+
}
|
|
9452
|
+
const hasProjectSource = sources.some((source) => isProjectMemorySource(source, input.project));
|
|
9453
|
+
const hasNonProjectScope = sources.some((source) => source.memory_scope === "global" || source.memory_scope === "cross-agent");
|
|
9454
|
+
if (hasProjectSource && hasNonProjectScope) {
|
|
9455
|
+
findings.push({
|
|
9456
|
+
id: `scope_gap:${topic}`,
|
|
9457
|
+
severity: "info",
|
|
9458
|
+
kind: "scope_gap",
|
|
9459
|
+
topic,
|
|
9460
|
+
message: `topic ${topic} mixes project and non-project memory scopes`,
|
|
9461
|
+
suggested_action: `Use skillwiki memory recall --project ${input.project} --topic ${topic} --scope <project|global|cross-agent|all> when reviewing this topic.`
|
|
9462
|
+
});
|
|
9463
|
+
}
|
|
9464
|
+
if (sources.length >= 3) {
|
|
9465
|
+
findings.push({
|
|
9466
|
+
id: `consolidation_candidate:${topic}`,
|
|
9467
|
+
severity: "info",
|
|
9468
|
+
kind: "consolidation_candidate",
|
|
9469
|
+
topic,
|
|
9470
|
+
message: `topic ${topic} has ${sources.length} active memory sources and may be ready for consolidation`,
|
|
9471
|
+
suggested_action: "Review this topic in office-hours or proj-distill before promoting or merging related memory pages."
|
|
9472
|
+
});
|
|
9473
|
+
}
|
|
9474
|
+
}
|
|
9475
|
+
return findings.sort(compareMemoryReviewFindings);
|
|
9476
|
+
}
|
|
9477
|
+
function parseInvalidTopicWarning(warning) {
|
|
9478
|
+
const match = warning.match(/^(.*): invalid memory topic slug (.+)$/);
|
|
9479
|
+
if (!match) return null;
|
|
9480
|
+
return {
|
|
9481
|
+
path: match[1] ?? "",
|
|
9482
|
+
topic: match[2] ?? ""
|
|
9483
|
+
};
|
|
9484
|
+
}
|
|
9485
|
+
function groupSourcesByTopic(sources) {
|
|
9486
|
+
const byTopic = /* @__PURE__ */ new Map();
|
|
9487
|
+
for (const source of sources) {
|
|
9488
|
+
for (const topic of source.topics) {
|
|
9489
|
+
const list = byTopic.get(topic) ?? [];
|
|
9490
|
+
list.push(source);
|
|
9491
|
+
byTopic.set(topic, list);
|
|
9492
|
+
}
|
|
9493
|
+
}
|
|
9494
|
+
return byTopic;
|
|
9495
|
+
}
|
|
9496
|
+
function compareMemoryReviewFindings(a, b) {
|
|
9497
|
+
return severityRank(b.severity) - severityRank(a.severity) || a.kind.localeCompare(b.kind) || (a.topic ?? "").localeCompare(b.topic ?? "") || (a.path ?? "").localeCompare(b.path ?? "") || a.id.localeCompare(b.id);
|
|
9498
|
+
}
|
|
9499
|
+
function severityRank(severity) {
|
|
9500
|
+
return severity === "warn" ? 2 : 1;
|
|
9501
|
+
}
|
|
9293
9502
|
function renderMemoryIndexStatusHint(status) {
|
|
9294
9503
|
if (!status.cache_present) {
|
|
9295
9504
|
return `memory index cache missing for ${status.project}; rebuild with memory index --project ${status.project}`;
|
|
@@ -9535,6 +9744,9 @@ function normalizeRecallScope(value) {
|
|
|
9535
9744
|
if (!value) return void 0;
|
|
9536
9745
|
return isRecallScope(value) ? value : void 0;
|
|
9537
9746
|
}
|
|
9747
|
+
function isValidMemoryTopicSlug(value) {
|
|
9748
|
+
return /^[a-z0-9][a-z0-9-]*$/.test(value);
|
|
9749
|
+
}
|
|
9538
9750
|
function isRecallScope(value) {
|
|
9539
9751
|
return value === "project" || value === "global" || value === "cross-agent" || value === "all";
|
|
9540
9752
|
}
|
|
@@ -9562,6 +9774,12 @@ function renderRecallHint(topic, sources) {
|
|
|
9562
9774
|
if (sources.length === 0) return `no memory sources found for topic ${topic}`;
|
|
9563
9775
|
return sources.map((source) => `${source.updated} ${source.title} (${source.path}) \u2014 ${source.summary}`).join("\n");
|
|
9564
9776
|
}
|
|
9777
|
+
function renderMemoryReviewHint(project, status, findings) {
|
|
9778
|
+
const warns = findings.filter((finding) => finding.severity === "warn").length;
|
|
9779
|
+
const infos = findings.length - warns;
|
|
9780
|
+
const cacheStatus = !status.cache_present ? "cache missing" : status.stale ? "cache stale" : "cache current";
|
|
9781
|
+
return `memory review for ${project}: ${findings.length} finding(s) (${warns} warn, ${infos} info); ${cacheStatus}`;
|
|
9782
|
+
}
|
|
9565
9783
|
function memoryCacheRelPath(project) {
|
|
9566
9784
|
return `.skillwiki/memory/${project}/topics.json`;
|
|
9567
9785
|
}
|
|
@@ -9582,46 +9800,11 @@ function dedupePages(pages) {
|
|
|
9582
9800
|
}
|
|
9583
9801
|
return out;
|
|
9584
9802
|
}
|
|
9585
|
-
async function readMemorySource(page, project, warnings) {
|
|
9586
|
-
const text = await readPage(page);
|
|
9587
|
-
const fm = extractFrontmatter(text);
|
|
9588
|
-
if (!fm.ok) return null;
|
|
9589
|
-
const topics = normalizeMemoryTopics(fm.data.memory_topics, page.relPath, warnings);
|
|
9590
|
-
if (topics.length === 0) return null;
|
|
9591
|
-
const projects = memoryProjects(page.relPath, fm.data);
|
|
9592
|
-
const scope = stringField2(fm.data.memory_scope);
|
|
9593
|
-
if (!projects.includes(project) && scope !== "global" && scope !== "cross-agent") return null;
|
|
9594
|
-
const privacy = stringField2(fm.data.memory_privacy) || "local";
|
|
9595
|
-
if (privacy === "secret-blocked") {
|
|
9596
|
-
warnings.push(`${page.relPath}: skipped secret-blocked memory`);
|
|
9597
|
-
return null;
|
|
9598
|
-
}
|
|
9599
|
-
const status = stringField2(fm.data.memory_status) || "active";
|
|
9600
|
-
if (status === "archived" || status === "rejected") return null;
|
|
9601
|
-
const split = splitFrontmatter(text);
|
|
9602
|
-
const body = split.ok ? split.data.body : text;
|
|
9603
|
-
const updated = stringField2(fm.data.last_seen) || stringField2(fm.data.updated) || stringField2(fm.data.ingested) || dateFromPath2(page.relPath);
|
|
9604
|
-
const title = stringField2(fm.data.title) || page.relPath.split("/").pop()?.replace(/\.md$/, "") || page.relPath;
|
|
9605
|
-
return {
|
|
9606
|
-
path: page.relPath,
|
|
9607
|
-
title,
|
|
9608
|
-
summary: summarize2(body),
|
|
9609
|
-
updated,
|
|
9610
|
-
hash: createHash8("sha256").update(Buffer.from(body, "utf8")).digest("hex"),
|
|
9611
|
-
topics,
|
|
9612
|
-
project,
|
|
9613
|
-
...stringField2(fm.data.memory_kind) ? { memory_kind: stringField2(fm.data.memory_kind) } : {},
|
|
9614
|
-
...scope ? { memory_scope: scope } : {},
|
|
9615
|
-
...stringField2(fm.data.memory_policy) ? { memory_policy: stringField2(fm.data.memory_policy) } : {},
|
|
9616
|
-
memory_privacy: privacy,
|
|
9617
|
-
memory_status: status
|
|
9618
|
-
};
|
|
9619
|
-
}
|
|
9620
9803
|
function normalizeMemoryTopics(value, path, warnings) {
|
|
9621
9804
|
const rawTopics = typeof value === "string" ? [value] : stringArray(value);
|
|
9622
9805
|
const topics = [];
|
|
9623
9806
|
for (const topic of rawTopics) {
|
|
9624
|
-
if (
|
|
9807
|
+
if (!isValidMemoryTopicSlug(topic)) {
|
|
9625
9808
|
warnings.push(`${path}: invalid memory topic slug ${topic}`);
|
|
9626
9809
|
continue;
|
|
9627
9810
|
}
|
|
@@ -11836,6 +12019,15 @@ memoryCmd.command("recall [vault]").description("recall bounded source summaries
|
|
|
11836
12019
|
limit: opts.limit
|
|
11837
12020
|
}), v.vault, { postCommit: false });
|
|
11838
12021
|
});
|
|
12022
|
+
memoryCmd.command("review [vault]").description("review deterministic memory gaps and cache drift without writing").requiredOption("--project <slug>", "project slug").option("--dry-run", "preview only; writes are not supported on this surface", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
12023
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
12024
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
12025
|
+
else emit(await runMemoryReview({
|
|
12026
|
+
vault: v.vault,
|
|
12027
|
+
project: opts.project,
|
|
12028
|
+
dryRun: !!opts.dryRun
|
|
12029
|
+
}), v.vault, { postCommit: false });
|
|
12030
|
+
});
|
|
11839
12031
|
memoryCmd.command("import [vault]").description("preview or apply local memory imports into raw captures").requiredOption("--from <path>", "source file or directory to scan").requiredOption("--project <slug>", "project slug").option("--dry-run", "preview only; do not write captures", false).option("--apply", "write validated raw captures for ready entries", false).option("--max-bytes <n>", "reject source files above this size", (s) => parseInt(s, 10), 2e5).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
11840
12032
|
const v = await resolveVaultArg(vault, opts.wiki);
|
|
11841
12033
|
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.23",
|
|
4
4
|
"skills": "./",
|
|
5
5
|
"description": "Project-aware Karpathy-style knowledge base for Claude Code: 18 prompt-only skills (wiki-*, proj-*, using-skillwiki) backed by the deterministic `skillwiki` CLI.",
|
|
6
6
|
"author": {
|