skillwiki 0.8.5-beta.5 → 0.8.7
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 +427 -48
- package/package.json +1 -1
- package/skills/.claude-plugin/plugin.json +1 -1
- package/skills/.codex-plugin/plugin.json +1 -1
- package/skills/hooks/session-context +136 -0
- package/skills/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "./chunk-TPS5XD2J.js";
|
|
15
15
|
|
|
16
16
|
// src/cli.ts
|
|
17
|
-
import { join as
|
|
17
|
+
import { join as join46 } from "path";
|
|
18
18
|
import { Command as Command2 } from "commander";
|
|
19
19
|
|
|
20
20
|
// ../shared/src/exit-codes.ts
|
|
@@ -166,8 +166,14 @@ var MetaSchema = z.object({
|
|
|
166
166
|
tags: z.array(z.string()),
|
|
167
167
|
confidence: z.enum(["high", "medium", "low"]).optional(),
|
|
168
168
|
provenance: z.enum(["research", "project", "mixed"]).optional(),
|
|
169
|
-
provenance_projects: z.array(wikilink).
|
|
169
|
+
provenance_projects: z.array(wikilink).optional(),
|
|
170
|
+
generated_by: z.string().min(1).optional(),
|
|
171
|
+
generated_at: z.string().datetime().optional(),
|
|
172
|
+
generated_kind: z.enum(["session-brief"]).optional()
|
|
170
173
|
}).superRefine((v, ctx) => {
|
|
174
|
+
if (v.generated_kind !== "session-brief" && (!v.provenance_projects || v.provenance_projects.length < 2)) {
|
|
175
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "meta pages must reference \u22652 projects" });
|
|
176
|
+
}
|
|
171
177
|
if (v.provenance && v.provenance !== "research" && (!v.provenance_projects || v.provenance_projects.length === 0)) {
|
|
172
178
|
ctx.addIssue({ code: z.ZodIssueCode.custom, path: ["provenance_projects"], message: "required when provenance != research" });
|
|
173
179
|
}
|
|
@@ -3067,6 +3073,7 @@ function buildCliSurface() {
|
|
|
3067
3073
|
program2.command("backup");
|
|
3068
3074
|
program2.command("seed").option("--wiki <name>");
|
|
3069
3075
|
program2.command("observe").requiredOption("--text <text>").option("--kind <kind>").option("--project <slug>").option("--wiki <name>");
|
|
3076
|
+
program2.command("session-brief").option("--project <slug>").option("--write").option("--wiki <name>");
|
|
3070
3077
|
program2.command("ingest").requiredOption("--vault <path>").requiredOption("--type <type>").requiredOption("--title <title>").option("--tags <csv>").option("--provenance <provenance>").option("--dry-run");
|
|
3071
3078
|
const graphCmd = program2.commands.find((c) => c.name() === "graph");
|
|
3072
3079
|
graphCmd.command("build").option("--out <path>").option("--wiki <name>");
|
|
@@ -7386,9 +7393,370 @@ ${input.text.trim()}
|
|
|
7386
7393
|
};
|
|
7387
7394
|
}
|
|
7388
7395
|
|
|
7396
|
+
// src/commands/session-brief.ts
|
|
7397
|
+
import { mkdir as mkdir13, readFile as readFile24, writeFile as writeFile14 } from "fs/promises";
|
|
7398
|
+
import { join as join36, relative as relative3, sep as sep3 } from "path";
|
|
7399
|
+
var MAX_WORDS = 900;
|
|
7400
|
+
async function runSessionBrief(input) {
|
|
7401
|
+
const scan = await scanVault(input.vault);
|
|
7402
|
+
if (!scan.ok) {
|
|
7403
|
+
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: scan };
|
|
7404
|
+
}
|
|
7405
|
+
const now = /* @__PURE__ */ new Date();
|
|
7406
|
+
const generatedAt = now.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
7407
|
+
const today = generatedAt.slice(0, 10);
|
|
7408
|
+
const project = await resolveProject(input);
|
|
7409
|
+
try {
|
|
7410
|
+
const transcripts = await loadTranscriptInfo(scan.data.raw);
|
|
7411
|
+
const workItems = await loadWorkItems(scan.data.workItems);
|
|
7412
|
+
const digests = await loadTrendDigests(scan.data.typedKnowledge);
|
|
7413
|
+
const healthWarnings = await loadHealthWarnings(input.vault);
|
|
7414
|
+
const latestLogs = newest(transcripts.filter((t) => t.kind === "session-log"), 3);
|
|
7415
|
+
const unclaimedCaptures = newest(transcripts.filter((t) => {
|
|
7416
|
+
if (t.kind !== "task" && t.kind !== "bug") return false;
|
|
7417
|
+
return !t.workItem && (!project || t.project === project);
|
|
7418
|
+
}), 5);
|
|
7419
|
+
const activeWork = newest(workItems.filter((w) => {
|
|
7420
|
+
if (w.status !== "planned" && w.status !== "in-progress") return false;
|
|
7421
|
+
return !project || w.project === project;
|
|
7422
|
+
}), 5);
|
|
7423
|
+
const latestDigest = newest(digests, 1);
|
|
7424
|
+
const projectLogs = project ? newest(transcripts.filter((t) => t.kind === "session-log" && t.project === project), 3) : [];
|
|
7425
|
+
const brief = capWords(renderBrief({
|
|
7426
|
+
project,
|
|
7427
|
+
generatedAt,
|
|
7428
|
+
latestLogs,
|
|
7429
|
+
unclaimedCaptures,
|
|
7430
|
+
activeWork,
|
|
7431
|
+
latestDigest,
|
|
7432
|
+
projectLogs,
|
|
7433
|
+
healthWarnings
|
|
7434
|
+
}), MAX_WORDS);
|
|
7435
|
+
let filesWritten = [];
|
|
7436
|
+
let indexUpdated = false;
|
|
7437
|
+
let logUpdated = false;
|
|
7438
|
+
if (input.write) {
|
|
7439
|
+
const writeResult = await writeBriefArtifacts(input.vault, {
|
|
7440
|
+
project,
|
|
7441
|
+
brief,
|
|
7442
|
+
generatedAt,
|
|
7443
|
+
today,
|
|
7444
|
+
wordCount: countWords(brief)
|
|
7445
|
+
});
|
|
7446
|
+
filesWritten = writeResult.filesWritten;
|
|
7447
|
+
indexUpdated = writeResult.indexUpdated;
|
|
7448
|
+
logUpdated = writeResult.logUpdated;
|
|
7449
|
+
}
|
|
7450
|
+
const humanHint = brief;
|
|
7451
|
+
return {
|
|
7452
|
+
exitCode: ExitCode.OK,
|
|
7453
|
+
result: ok({
|
|
7454
|
+
project,
|
|
7455
|
+
brief,
|
|
7456
|
+
word_count: countWords(brief),
|
|
7457
|
+
files_written: filesWritten,
|
|
7458
|
+
index_updated: indexUpdated,
|
|
7459
|
+
log_updated: logUpdated,
|
|
7460
|
+
generated_at: generatedAt,
|
|
7461
|
+
humanHint
|
|
7462
|
+
})
|
|
7463
|
+
};
|
|
7464
|
+
} catch (e) {
|
|
7465
|
+
return {
|
|
7466
|
+
exitCode: ExitCode.WRITE_FAILED,
|
|
7467
|
+
result: err("WRITE_FAILED", { message: String(e) })
|
|
7468
|
+
};
|
|
7469
|
+
}
|
|
7470
|
+
}
|
|
7471
|
+
async function resolveProject(input) {
|
|
7472
|
+
if (input.project && input.project !== "auto") return input.project;
|
|
7473
|
+
const envProject = input.env?.SKILLWIKI_PROJECT;
|
|
7474
|
+
if (envProject) return envProject;
|
|
7475
|
+
const cwd = input.cwd ?? process.cwd();
|
|
7476
|
+
const projectDotenv = await readProjectSlug(join36(cwd, ".skillwiki", ".env"));
|
|
7477
|
+
if (projectDotenv) return projectDotenv;
|
|
7478
|
+
const inferred = inferProjectFromPath(input.vault, cwd);
|
|
7479
|
+
if (inferred) return inferred;
|
|
7480
|
+
return void 0;
|
|
7481
|
+
}
|
|
7482
|
+
async function readProjectSlug(file) {
|
|
7483
|
+
let text;
|
|
7484
|
+
try {
|
|
7485
|
+
text = await readFile24(file, "utf8");
|
|
7486
|
+
} catch {
|
|
7487
|
+
return void 0;
|
|
7488
|
+
}
|
|
7489
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
7490
|
+
const line = rawLine.trim();
|
|
7491
|
+
if (line.startsWith("#")) continue;
|
|
7492
|
+
const match = line.match(/^PROJECT_SLUG=(.+)$/);
|
|
7493
|
+
if (match && match[1].trim().length > 0) return unquote(match[1].trim());
|
|
7494
|
+
}
|
|
7495
|
+
return void 0;
|
|
7496
|
+
}
|
|
7497
|
+
function inferProjectFromPath(vault, cwd) {
|
|
7498
|
+
const rel = relative3(vault, cwd).split(sep3).join("/");
|
|
7499
|
+
const match = rel.match(/^projects\/([^/]+)(?:\/|$)/);
|
|
7500
|
+
return match?.[1];
|
|
7501
|
+
}
|
|
7502
|
+
function unquote(value) {
|
|
7503
|
+
return value.replace(/^["']|["']$/g, "");
|
|
7504
|
+
}
|
|
7505
|
+
async function loadTranscriptInfo(rawPages) {
|
|
7506
|
+
const out = [];
|
|
7507
|
+
for (const page of rawPages.filter((p) => p.relPath.startsWith("raw/transcripts/"))) {
|
|
7508
|
+
const text = await readPage(page);
|
|
7509
|
+
const fm = extractFrontmatter(text);
|
|
7510
|
+
if (!fm.ok) continue;
|
|
7511
|
+
const split = splitFrontmatter(text);
|
|
7512
|
+
const body = split.ok ? split.data.body : text;
|
|
7513
|
+
out.push({
|
|
7514
|
+
path: page.relPath,
|
|
7515
|
+
title: titleFromFmOrPath(fm.data, page.relPath),
|
|
7516
|
+
summary: summarize(body),
|
|
7517
|
+
date: stringField(fm.data.ingested) || dateFromPath(page.relPath),
|
|
7518
|
+
kind: stringField(fm.data.kind),
|
|
7519
|
+
project: wikilinkSlug(fm.data.project),
|
|
7520
|
+
workItem: wikilinkSlug(fm.data.work_item)
|
|
7521
|
+
});
|
|
7522
|
+
}
|
|
7523
|
+
return out;
|
|
7524
|
+
}
|
|
7525
|
+
async function loadWorkItems(workItemPages) {
|
|
7526
|
+
const out = [];
|
|
7527
|
+
for (const page of workItemPages.filter((p) => p.relPath.endsWith("/spec.md"))) {
|
|
7528
|
+
const text = await readPage(page);
|
|
7529
|
+
const fm = extractFrontmatter(text);
|
|
7530
|
+
if (!fm.ok) continue;
|
|
7531
|
+
const split = splitFrontmatter(text);
|
|
7532
|
+
const body = split.ok ? split.data.body : text;
|
|
7533
|
+
out.push({
|
|
7534
|
+
path: page.relPath,
|
|
7535
|
+
title: titleFromFmOrPath(fm.data, page.relPath),
|
|
7536
|
+
summary: summarize(body),
|
|
7537
|
+
date: stringField(fm.data.updated) || stringField(fm.data.created) || dateFromPath(page.relPath),
|
|
7538
|
+
status: stringField(fm.data.status),
|
|
7539
|
+
project: wikilinkSlug(fm.data.project)
|
|
7540
|
+
});
|
|
7541
|
+
}
|
|
7542
|
+
return out;
|
|
7543
|
+
}
|
|
7544
|
+
async function loadTrendDigests(typedPages) {
|
|
7545
|
+
const out = [];
|
|
7546
|
+
for (const page of typedPages.filter((p) => p.relPath.startsWith("queries/") && p.relPath.includes("agent-memory-trends"))) {
|
|
7547
|
+
const text = await readPage(page);
|
|
7548
|
+
const fm = extractFrontmatter(text);
|
|
7549
|
+
if (!fm.ok) continue;
|
|
7550
|
+
const split = splitFrontmatter(text);
|
|
7551
|
+
const body = split.ok ? split.data.body : text;
|
|
7552
|
+
out.push({
|
|
7553
|
+
path: page.relPath,
|
|
7554
|
+
title: titleFromFmOrPath(fm.data, page.relPath),
|
|
7555
|
+
summary: summarize(body),
|
|
7556
|
+
date: stringField(fm.data.updated) || stringField(fm.data.created) || dateFromPath(page.relPath)
|
|
7557
|
+
});
|
|
7558
|
+
}
|
|
7559
|
+
return out;
|
|
7560
|
+
}
|
|
7561
|
+
function renderBrief(input) {
|
|
7562
|
+
const lines = [
|
|
7563
|
+
"# Session Brief",
|
|
7564
|
+
"",
|
|
7565
|
+
`Generated: ${input.generatedAt}`,
|
|
7566
|
+
`Scope: ${input.project ? `[[${input.project}]] plus global context` : "global context"}`,
|
|
7567
|
+
""
|
|
7568
|
+
];
|
|
7569
|
+
appendSection(lines, "Active Work", input.activeWork, "No active project work found.");
|
|
7570
|
+
appendSection(lines, "Unclaimed Captures", input.unclaimedCaptures, "No unclaimed task or bug captures found.");
|
|
7571
|
+
appendSection(lines, "Recent Session Logs", input.project ? input.projectLogs : input.latestLogs, "No recent session logs found.");
|
|
7572
|
+
appendSection(lines, "Latest Agent Memory Trends", input.latestDigest, "No agent memory trends digest found.");
|
|
7573
|
+
appendTextSection(lines, "Health Warnings", input.healthWarnings, "No high-level health warnings found.");
|
|
7574
|
+
lines.push(
|
|
7575
|
+
"## Suggested Commands",
|
|
7576
|
+
"",
|
|
7577
|
+
"- `skillwiki status`",
|
|
7578
|
+
input.project ? `- \`skillwiki project-index ${input.project}\`` : '- `skillwiki query "active work"`',
|
|
7579
|
+
"- `skillwiki transcripts --since <date>`",
|
|
7580
|
+
""
|
|
7581
|
+
);
|
|
7582
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
7583
|
+
}
|
|
7584
|
+
function appendSection(lines, title, items, empty) {
|
|
7585
|
+
lines.push(`## ${title}`, "");
|
|
7586
|
+
if (items.length === 0) {
|
|
7587
|
+
lines.push(`- ${empty}`, "");
|
|
7588
|
+
return;
|
|
7589
|
+
}
|
|
7590
|
+
for (const item of items) {
|
|
7591
|
+
const status = item.status ? ` [${item.status}]` : "";
|
|
7592
|
+
const date = item.date ? `${item.date} ` : "";
|
|
7593
|
+
const summary = item.summary ? ` \u2014 ${item.summary}` : "";
|
|
7594
|
+
lines.push(`- ${date}${item.title}${status} (${item.path})${summary}`);
|
|
7595
|
+
}
|
|
7596
|
+
lines.push("");
|
|
7597
|
+
}
|
|
7598
|
+
function appendTextSection(lines, title, items, empty) {
|
|
7599
|
+
lines.push(`## ${title}`, "");
|
|
7600
|
+
if (items.length === 0) {
|
|
7601
|
+
lines.push(`- ${empty}`, "");
|
|
7602
|
+
return;
|
|
7603
|
+
}
|
|
7604
|
+
for (const item of items.slice(0, 5)) {
|
|
7605
|
+
lines.push(`- ${item}`);
|
|
7606
|
+
}
|
|
7607
|
+
lines.push("");
|
|
7608
|
+
}
|
|
7609
|
+
async function loadHealthWarnings(vault) {
|
|
7610
|
+
const text = await readIfExists2(join36(vault, ".skillwiki", "health.json"));
|
|
7611
|
+
if (!text) return [];
|
|
7612
|
+
try {
|
|
7613
|
+
const parsed = JSON.parse(text);
|
|
7614
|
+
const warnings = Array.isArray(parsed.warnings) ? parsed.warnings.filter((w) => typeof w === "string") : [];
|
|
7615
|
+
const riskFlags = Array.isArray(parsed.risk_flags) ? parsed.risk_flags.filter((w) => typeof w === "string") : [];
|
|
7616
|
+
const status = typeof parsed.status === "string" && parsed.status !== "ok" ? [`health status: ${parsed.status}`] : [];
|
|
7617
|
+
return [...status, ...warnings, ...riskFlags].slice(0, 5);
|
|
7618
|
+
} catch {
|
|
7619
|
+
return [];
|
|
7620
|
+
}
|
|
7621
|
+
}
|
|
7622
|
+
async function writeBriefArtifacts(vault, input) {
|
|
7623
|
+
const metaPath = join36(vault, "meta", "latest-session-brief.md");
|
|
7624
|
+
const cacheMdPath = join36(vault, ".skillwiki", "session-brief.md");
|
|
7625
|
+
const cacheJsonPath = join36(vault, ".skillwiki", "session-brief.json");
|
|
7626
|
+
await mkdir13(join36(vault, "meta"), { recursive: true });
|
|
7627
|
+
await mkdir13(join36(vault, ".skillwiki"), { recursive: true });
|
|
7628
|
+
const committed = renderCommittedBrief(input);
|
|
7629
|
+
const previousComparable = comparableBrief(await readIfExists2(metaPath));
|
|
7630
|
+
const nextComparable = comparableBrief(committed);
|
|
7631
|
+
const materialChange = previousComparable !== nextComparable;
|
|
7632
|
+
await writeFile14(metaPath, committed, "utf8");
|
|
7633
|
+
await writeFile14(cacheMdPath, input.brief, "utf8");
|
|
7634
|
+
await writeFile14(cacheJsonPath, `${JSON.stringify({
|
|
7635
|
+
project: input.project,
|
|
7636
|
+
brief: input.brief,
|
|
7637
|
+
word_count: input.wordCount,
|
|
7638
|
+
generated_at: input.generatedAt
|
|
7639
|
+
}, null, 2)}
|
|
7640
|
+
`, "utf8");
|
|
7641
|
+
const indexUpdated = await ensureIndexEntry(vault);
|
|
7642
|
+
const logUpdated = materialChange ? await appendMaterialLog(vault, input.today) : false;
|
|
7643
|
+
if (materialChange) {
|
|
7644
|
+
appendLastOp(vault, {
|
|
7645
|
+
operation: "session-brief",
|
|
7646
|
+
summary: "refreshed latest session brief",
|
|
7647
|
+
files: [
|
|
7648
|
+
"meta/latest-session-brief.md",
|
|
7649
|
+
".skillwiki/session-brief.md",
|
|
7650
|
+
".skillwiki/session-brief.json"
|
|
7651
|
+
],
|
|
7652
|
+
timestamp: input.generatedAt
|
|
7653
|
+
});
|
|
7654
|
+
}
|
|
7655
|
+
return {
|
|
7656
|
+
filesWritten: [
|
|
7657
|
+
"meta/latest-session-brief.md",
|
|
7658
|
+
".skillwiki/session-brief.md",
|
|
7659
|
+
".skillwiki/session-brief.json"
|
|
7660
|
+
],
|
|
7661
|
+
indexUpdated,
|
|
7662
|
+
logUpdated
|
|
7663
|
+
};
|
|
7664
|
+
}
|
|
7665
|
+
function renderCommittedBrief(input) {
|
|
7666
|
+
const projectLine = input.project ? `project_hint: "[[${input.project}]]"
|
|
7667
|
+
` : "";
|
|
7668
|
+
return [
|
|
7669
|
+
"---",
|
|
7670
|
+
"title: Latest Session Brief",
|
|
7671
|
+
`created: ${input.today}`,
|
|
7672
|
+
`updated: ${input.today}`,
|
|
7673
|
+
"type: meta",
|
|
7674
|
+
"tags: [generated, session-brief]",
|
|
7675
|
+
"confidence: high",
|
|
7676
|
+
"generated_by: skillwiki session-brief",
|
|
7677
|
+
`generated_at: ${input.generatedAt}`,
|
|
7678
|
+
"generated_kind: session-brief",
|
|
7679
|
+
projectLine.trimEnd(),
|
|
7680
|
+
"---",
|
|
7681
|
+
"",
|
|
7682
|
+
input.brief.trimEnd(),
|
|
7683
|
+
""
|
|
7684
|
+
].filter((line) => line !== "").join("\n");
|
|
7685
|
+
}
|
|
7686
|
+
async function ensureIndexEntry(vault) {
|
|
7687
|
+
const indexPath = join36(vault, "index.md");
|
|
7688
|
+
let text = await readIfExists2(indexPath);
|
|
7689
|
+
if (!text) return false;
|
|
7690
|
+
if (text.includes("[[meta/latest-session-brief]]")) return false;
|
|
7691
|
+
const entry = "- [[meta/latest-session-brief]] \u2014 Latest Session Brief";
|
|
7692
|
+
const lines = text.split(/\r?\n/);
|
|
7693
|
+
const sectionIdx = lines.findIndex((line) => line.trim() === "## Meta");
|
|
7694
|
+
if (sectionIdx === -1) {
|
|
7695
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "") lines.pop();
|
|
7696
|
+
lines.push("", "## Meta", entry);
|
|
7697
|
+
} else {
|
|
7698
|
+
let insertAt = sectionIdx + 1;
|
|
7699
|
+
while (insertAt < lines.length && !lines[insertAt].startsWith("## ")) insertAt++;
|
|
7700
|
+
lines.splice(insertAt, 0, entry);
|
|
7701
|
+
}
|
|
7702
|
+
await writeFile14(indexPath, lines.join("\n"), "utf8");
|
|
7703
|
+
return true;
|
|
7704
|
+
}
|
|
7705
|
+
async function appendMaterialLog(vault, today) {
|
|
7706
|
+
const logPath = join36(vault, "log.md");
|
|
7707
|
+
const text = await readIfExists2(logPath);
|
|
7708
|
+
if (!text) return false;
|
|
7709
|
+
const entry = `
|
|
7710
|
+
## [${today}] session-brief | refreshed: meta/latest-session-brief.md`;
|
|
7711
|
+
await writeFile14(logPath, text.trimEnd() + entry + "\n", "utf8");
|
|
7712
|
+
return true;
|
|
7713
|
+
}
|
|
7714
|
+
async function readIfExists2(path) {
|
|
7715
|
+
try {
|
|
7716
|
+
return await readFile24(path, "utf8");
|
|
7717
|
+
} catch {
|
|
7718
|
+
return "";
|
|
7719
|
+
}
|
|
7720
|
+
}
|
|
7721
|
+
function comparableBrief(text) {
|
|
7722
|
+
if (!text) return "";
|
|
7723
|
+
return text.split(/\r?\n/).filter((line) => !line.startsWith("created:")).filter((line) => !line.startsWith("updated:")).filter((line) => !line.startsWith("generated_at:")).filter((line) => !line.startsWith("Generated: ")).join("\n").trim();
|
|
7724
|
+
}
|
|
7725
|
+
function newest(items, limit) {
|
|
7726
|
+
return [...items].sort((a, b) => b.date.localeCompare(a.date) || b.path.localeCompare(a.path)).slice(0, limit);
|
|
7727
|
+
}
|
|
7728
|
+
function titleFromFmOrPath(fm, path) {
|
|
7729
|
+
return stringField(fm.title) || path.split("/").pop()?.replace(/\.md$/, "") || path;
|
|
7730
|
+
}
|
|
7731
|
+
function summarize(body) {
|
|
7732
|
+
const lines = body.split(/\r?\n/).map((line) => line.replace(/^#+\s*/, "").trim()).filter((line) => line.length > 0 && !line.startsWith("```"));
|
|
7733
|
+
const summary = lines.join(" ").replace(/\s+/g, " ").trim();
|
|
7734
|
+
return summary.length > 180 ? `${summary.slice(0, 177).trimEnd()}...` : summary;
|
|
7735
|
+
}
|
|
7736
|
+
function capWords(text, maxWords) {
|
|
7737
|
+
const words = text.trim().split(/\s+/);
|
|
7738
|
+
if (words.length <= maxWords) return text;
|
|
7739
|
+
return words.slice(0, maxWords).join(" ") + "\n";
|
|
7740
|
+
}
|
|
7741
|
+
function countWords(text) {
|
|
7742
|
+
const trimmed = text.trim();
|
|
7743
|
+
return trimmed.length === 0 ? 0 : trimmed.split(/\s+/).length;
|
|
7744
|
+
}
|
|
7745
|
+
function stringField(value) {
|
|
7746
|
+
return typeof value === "string" ? value : "";
|
|
7747
|
+
}
|
|
7748
|
+
function wikilinkSlug(value) {
|
|
7749
|
+
if (typeof value !== "string") return void 0;
|
|
7750
|
+
const match = value.match(/^\[\[([^\]]+)\]\]$/);
|
|
7751
|
+
return match?.[1] ?? value;
|
|
7752
|
+
}
|
|
7753
|
+
function dateFromPath(path) {
|
|
7754
|
+
return path.match(/\d{4}-\d{2}-\d{2}/)?.[0] ?? "";
|
|
7755
|
+
}
|
|
7756
|
+
|
|
7389
7757
|
// src/commands/ingest.ts
|
|
7390
|
-
import { readFile as
|
|
7391
|
-
import { join as
|
|
7758
|
+
import { readFile as readFile25, writeFile as writeFile15, mkdir as mkdir14 } from "fs/promises";
|
|
7759
|
+
import { join as join37 } from "path";
|
|
7392
7760
|
import { createHash as createHash5 } from "crypto";
|
|
7393
7761
|
var ALLOWED_TYPES = /* @__PURE__ */ new Set(["entity", "concept", "comparison", "query"]);
|
|
7394
7762
|
var TYPE_DIR = {
|
|
@@ -7547,7 +7915,7 @@ async function runIngest(input) {
|
|
|
7547
7915
|
sourceContent = fetchResult.data.body;
|
|
7548
7916
|
} else {
|
|
7549
7917
|
try {
|
|
7550
|
-
sourceContent = await
|
|
7918
|
+
sourceContent = await readFile25(input.source, "utf8");
|
|
7551
7919
|
} catch {
|
|
7552
7920
|
return {
|
|
7553
7921
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -7562,8 +7930,8 @@ async function runIngest(input) {
|
|
|
7562
7930
|
const rawRelPath = `raw/articles/${slug}.md`;
|
|
7563
7931
|
const typedDir = TYPE_DIR[input.type] ?? `${input.type}s`;
|
|
7564
7932
|
const typedRelPath = `${typedDir}/${slug}.md`;
|
|
7565
|
-
const rawAbsPath =
|
|
7566
|
-
const typedAbsPath =
|
|
7933
|
+
const rawAbsPath = join37(input.vault, rawRelPath);
|
|
7934
|
+
const typedAbsPath = join37(input.vault, typedRelPath);
|
|
7567
7935
|
const identity = assessSourceIdentity({
|
|
7568
7936
|
rawPath: rawRelPath,
|
|
7569
7937
|
sourceUrl: sourceUrl ?? void 0,
|
|
@@ -7645,8 +8013,8 @@ async function runIngest(input) {
|
|
|
7645
8013
|
};
|
|
7646
8014
|
}
|
|
7647
8015
|
try {
|
|
7648
|
-
await
|
|
7649
|
-
await
|
|
8016
|
+
await mkdir14(join37(input.vault, "raw", "articles"), { recursive: true });
|
|
8017
|
+
await writeFile15(rawAbsPath, rawContent, "utf8");
|
|
7650
8018
|
} catch (e) {
|
|
7651
8019
|
return {
|
|
7652
8020
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -7654,8 +8022,8 @@ async function runIngest(input) {
|
|
|
7654
8022
|
};
|
|
7655
8023
|
}
|
|
7656
8024
|
try {
|
|
7657
|
-
await
|
|
7658
|
-
await
|
|
8025
|
+
await mkdir14(join37(input.vault, typedDir), { recursive: true });
|
|
8026
|
+
await writeFile15(typedAbsPath, typedContent, "utf8");
|
|
7659
8027
|
} catch (e) {
|
|
7660
8028
|
return {
|
|
7661
8029
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -7834,11 +8202,11 @@ ${body}`;
|
|
|
7834
8202
|
|
|
7835
8203
|
// src/commands/sync.ts
|
|
7836
8204
|
import { existsSync as existsSync16 } from "fs";
|
|
7837
|
-
import { join as
|
|
8205
|
+
import { join as join39 } from "path";
|
|
7838
8206
|
|
|
7839
8207
|
// src/utils/sync-lock.ts
|
|
7840
8208
|
import { existsSync as existsSync15, mkdirSync as mkdirSync5, readFileSync as readFileSync11, renameSync as renameSync2, unlinkSync as unlinkSync5, writeFileSync as writeFileSync7 } from "fs";
|
|
7841
|
-
import { join as
|
|
8209
|
+
import { join as join38 } from "path";
|
|
7842
8210
|
import { createHash as createHash6 } from "crypto";
|
|
7843
8211
|
function getSessionId() {
|
|
7844
8212
|
if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
|
|
@@ -7846,7 +8214,7 @@ function getSessionId() {
|
|
|
7846
8214
|
return process.pid.toString();
|
|
7847
8215
|
}
|
|
7848
8216
|
function lockPath(vault) {
|
|
7849
|
-
return
|
|
8217
|
+
return join38(vault, ".skillwiki", "sync.lock");
|
|
7850
8218
|
}
|
|
7851
8219
|
function readLock(vault) {
|
|
7852
8220
|
const path = lockPath(vault);
|
|
@@ -7865,7 +8233,7 @@ function isStale(lock, now) {
|
|
|
7865
8233
|
}
|
|
7866
8234
|
function acquireLock(vault, opts = {}) {
|
|
7867
8235
|
const path = lockPath(vault);
|
|
7868
|
-
const dir =
|
|
8236
|
+
const dir = join38(vault, ".skillwiki");
|
|
7869
8237
|
if (!existsSync15(dir)) {
|
|
7870
8238
|
mkdirSync5(dir, { recursive: true });
|
|
7871
8239
|
}
|
|
@@ -7940,7 +8308,7 @@ function releaseLock(vault, opts = {}) {
|
|
|
7940
8308
|
function runSyncStatus(input) {
|
|
7941
8309
|
const vault = input.vault;
|
|
7942
8310
|
const includeStashes = input.includeStashes ?? false;
|
|
7943
|
-
if (!existsSync16(
|
|
8311
|
+
if (!existsSync16(join39(vault, ".git"))) {
|
|
7944
8312
|
return {
|
|
7945
8313
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
7946
8314
|
result: ok({
|
|
@@ -8018,7 +8386,7 @@ function runSyncStatus(input) {
|
|
|
8018
8386
|
}
|
|
8019
8387
|
async function runSyncPush(input) {
|
|
8020
8388
|
const vault = input.vault;
|
|
8021
|
-
if (!existsSync16(
|
|
8389
|
+
if (!existsSync16(join39(vault, ".git"))) {
|
|
8022
8390
|
return {
|
|
8023
8391
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
8024
8392
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -8141,7 +8509,7 @@ function enableGitLongPathsOnWindows(vault) {
|
|
|
8141
8509
|
}
|
|
8142
8510
|
async function runSyncPull(input) {
|
|
8143
8511
|
const vault = input.vault;
|
|
8144
|
-
if (!existsSync16(
|
|
8512
|
+
if (!existsSync16(join39(vault, ".git"))) {
|
|
8145
8513
|
return {
|
|
8146
8514
|
exitCode: ExitCode.VAULT_PATH_INVALID,
|
|
8147
8515
|
result: err("NOT_A_GIT_REPO", { path: vault })
|
|
@@ -8382,7 +8750,7 @@ function runSyncUnlock(input) {
|
|
|
8382
8750
|
|
|
8383
8751
|
// src/commands/backup.ts
|
|
8384
8752
|
import { statSync as statSync5, readdirSync as readdirSync3, readFileSync as readFileSync12, mkdirSync as mkdirSync6, writeFileSync as writeFileSync8 } from "fs";
|
|
8385
|
-
import { join as
|
|
8753
|
+
import { join as join40, relative as relative4, dirname as dirname13 } from "path";
|
|
8386
8754
|
import { PutObjectCommand, HeadObjectCommand, ListObjectsV2Command, GetObjectCommand, DeleteObjectsCommand } from "@aws-sdk/client-s3";
|
|
8387
8755
|
|
|
8388
8756
|
// src/utils/s3-client.ts
|
|
@@ -8406,11 +8774,11 @@ var SKIP_DIRS2 = /* @__PURE__ */ new Set([".git", ".obsidian", "_archive", "node
|
|
|
8406
8774
|
function* walkMarkdown(dir, base) {
|
|
8407
8775
|
for (const entry of readdirSync3(dir, { withFileTypes: true })) {
|
|
8408
8776
|
if (SKIP_DIRS2.has(entry.name)) continue;
|
|
8409
|
-
const full =
|
|
8777
|
+
const full = join40(dir, entry.name);
|
|
8410
8778
|
if (entry.isDirectory()) {
|
|
8411
8779
|
yield* walkMarkdown(full, base);
|
|
8412
8780
|
} else if (entry.name.endsWith(".md")) {
|
|
8413
|
-
yield
|
|
8781
|
+
yield relative4(base, full).replace(/\\/g, "/");
|
|
8414
8782
|
}
|
|
8415
8783
|
}
|
|
8416
8784
|
}
|
|
@@ -8429,7 +8797,7 @@ async function runBackupSync(input) {
|
|
|
8429
8797
|
let failed = 0;
|
|
8430
8798
|
const files = [...walkMarkdown(input.vault, input.vault)];
|
|
8431
8799
|
for (const relPath of files) {
|
|
8432
|
-
const absPath =
|
|
8800
|
+
const absPath = join40(input.vault, relPath);
|
|
8433
8801
|
const localStat = statSync5(absPath);
|
|
8434
8802
|
let needsUpload = true;
|
|
8435
8803
|
try {
|
|
@@ -8505,7 +8873,7 @@ async function runBackupRestore(input) {
|
|
|
8505
8873
|
const objects = list.Contents ?? [];
|
|
8506
8874
|
for (const obj of objects) {
|
|
8507
8875
|
if (!obj.Key) continue;
|
|
8508
|
-
const localPath =
|
|
8876
|
+
const localPath = join40(target, obj.Key);
|
|
8509
8877
|
try {
|
|
8510
8878
|
const localStat = statSync5(localPath);
|
|
8511
8879
|
if (obj.LastModified && localStat.mtime > obj.LastModified) {
|
|
@@ -8552,8 +8920,8 @@ async function runBackupRestore(input) {
|
|
|
8552
8920
|
|
|
8553
8921
|
// src/commands/status.ts
|
|
8554
8922
|
import { existsSync as existsSync17, statSync as statSync6 } from "fs";
|
|
8555
|
-
import { readFile as
|
|
8556
|
-
import { join as
|
|
8923
|
+
import { readFile as readFile26 } from "fs/promises";
|
|
8924
|
+
import { join as join41 } from "path";
|
|
8557
8925
|
async function runStatus(input) {
|
|
8558
8926
|
if (!existsSync17(input.vault)) {
|
|
8559
8927
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { vault: input.vault }) };
|
|
@@ -8580,7 +8948,7 @@ async function runStatus(input) {
|
|
|
8580
8948
|
const compound = scan.data.compound.length;
|
|
8581
8949
|
let schemaVersion = "v1";
|
|
8582
8950
|
try {
|
|
8583
|
-
const schemaContent = await
|
|
8951
|
+
const schemaContent = await readFile26(join41(input.vault, "SCHEMA.md"), "utf8");
|
|
8584
8952
|
const versionMatch = schemaContent.match(/version:\s*["']?([^"'\s\n]+)/i);
|
|
8585
8953
|
if (versionMatch) schemaVersion = versionMatch[1];
|
|
8586
8954
|
} catch {
|
|
@@ -8640,8 +9008,8 @@ async function runStatus(input) {
|
|
|
8640
9008
|
}
|
|
8641
9009
|
|
|
8642
9010
|
// src/commands/seed.ts
|
|
8643
|
-
import { mkdir as
|
|
8644
|
-
import { join as
|
|
9011
|
+
import { mkdir as mkdir15, writeFile as writeFile16, stat as stat8 } from "fs/promises";
|
|
9012
|
+
import { join as join42 } from "path";
|
|
8645
9013
|
var TODAY = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
8646
9014
|
var EXAMPLE_PAGES = {
|
|
8647
9015
|
"entities/example-project.md": `---
|
|
@@ -8710,30 +9078,30 @@ Real sources are immutable after ingestion \u2014 never edit them.
|
|
|
8710
9078
|
`;
|
|
8711
9079
|
async function runSeed(input) {
|
|
8712
9080
|
try {
|
|
8713
|
-
await stat8(
|
|
9081
|
+
await stat8(join42(input.vault, "SCHEMA.md"));
|
|
8714
9082
|
} catch {
|
|
8715
9083
|
return { exitCode: ExitCode.VAULT_PATH_INVALID, result: err("VAULT_PATH_INVALID", { root: input.vault, reason: "SCHEMA.md missing \u2014 run `skillwiki init` first" }) };
|
|
8716
9084
|
}
|
|
8717
9085
|
const created = [];
|
|
8718
9086
|
const skipped = [];
|
|
8719
9087
|
for (const [relPath, content] of Object.entries(EXAMPLE_PAGES)) {
|
|
8720
|
-
const absPath =
|
|
9088
|
+
const absPath = join42(input.vault, relPath);
|
|
8721
9089
|
try {
|
|
8722
9090
|
await stat8(absPath);
|
|
8723
9091
|
skipped.push(relPath);
|
|
8724
9092
|
} catch {
|
|
8725
|
-
await
|
|
8726
|
-
await
|
|
9093
|
+
await mkdir15(join42(absPath, ".."), { recursive: true });
|
|
9094
|
+
await writeFile16(absPath, content, "utf8");
|
|
8727
9095
|
created.push(relPath);
|
|
8728
9096
|
}
|
|
8729
9097
|
}
|
|
8730
|
-
const rawPath =
|
|
9098
|
+
const rawPath = join42(input.vault, "raw", "articles", "example-source.md");
|
|
8731
9099
|
try {
|
|
8732
9100
|
await stat8(rawPath);
|
|
8733
9101
|
skipped.push("raw/articles/example-source.md");
|
|
8734
9102
|
} catch {
|
|
8735
|
-
await
|
|
8736
|
-
await
|
|
9103
|
+
await mkdir15(join42(rawPath, ".."), { recursive: true });
|
|
9104
|
+
await writeFile16(rawPath, EXAMPLE_RAW, "utf8");
|
|
8737
9105
|
created.push("raw/articles/example-source.md");
|
|
8738
9106
|
}
|
|
8739
9107
|
if (created.length > 0) {
|
|
@@ -8755,9 +9123,9 @@ async function runSeed(input) {
|
|
|
8755
9123
|
}
|
|
8756
9124
|
|
|
8757
9125
|
// src/commands/canvas.ts
|
|
8758
|
-
import { readFile as
|
|
9126
|
+
import { readFile as readFile27, writeFile as writeFile17 } from "fs/promises";
|
|
8759
9127
|
import { existsSync as existsSync18 } from "fs";
|
|
8760
|
-
import { join as
|
|
9128
|
+
import { join as join43 } from "path";
|
|
8761
9129
|
var NODE_WIDTH = 240;
|
|
8762
9130
|
var NODE_HEIGHT = 60;
|
|
8763
9131
|
var COLUMN_SPACING = 400;
|
|
@@ -8835,7 +9203,7 @@ function buildCanvasEdges(adjacency) {
|
|
|
8835
9203
|
return edges;
|
|
8836
9204
|
}
|
|
8837
9205
|
async function runCanvasGenerate(input) {
|
|
8838
|
-
const graphPath = input.graphPath ??
|
|
9206
|
+
const graphPath = input.graphPath ?? join43(input.vault, ".skillwiki", "graph.json");
|
|
8839
9207
|
if (!existsSync18(graphPath)) {
|
|
8840
9208
|
return {
|
|
8841
9209
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -8847,7 +9215,7 @@ async function runCanvasGenerate(input) {
|
|
|
8847
9215
|
}
|
|
8848
9216
|
let raw;
|
|
8849
9217
|
try {
|
|
8850
|
-
raw = await
|
|
9218
|
+
raw = await readFile27(graphPath, "utf8");
|
|
8851
9219
|
} catch (e) {
|
|
8852
9220
|
return {
|
|
8853
9221
|
exitCode: ExitCode.FILE_NOT_FOUND,
|
|
@@ -8873,9 +9241,9 @@ async function runCanvasGenerate(input) {
|
|
|
8873
9241
|
const nodes = buildCanvasNodes(paths);
|
|
8874
9242
|
const edges = buildCanvasEdges(graph.adjacency);
|
|
8875
9243
|
const canvas = { nodes, edges };
|
|
8876
|
-
const outPath =
|
|
9244
|
+
const outPath = join43(input.vault, "vault-graph.canvas");
|
|
8877
9245
|
try {
|
|
8878
|
-
await
|
|
9246
|
+
await writeFile17(outPath, JSON.stringify(canvas, null, 2));
|
|
8879
9247
|
} catch (e) {
|
|
8880
9248
|
return {
|
|
8881
9249
|
exitCode: ExitCode.WRITE_FAILED,
|
|
@@ -8895,8 +9263,8 @@ written: ${outPath}`
|
|
|
8895
9263
|
}
|
|
8896
9264
|
|
|
8897
9265
|
// src/commands/query.ts
|
|
8898
|
-
import { readFile as
|
|
8899
|
-
import { join as
|
|
9266
|
+
import { readFile as readFile28, stat as stat9 } from "fs/promises";
|
|
9267
|
+
import { join as join44 } from "path";
|
|
8900
9268
|
var W_KEYWORD = 2;
|
|
8901
9269
|
var W_SOURCE_OVERLAP = 4;
|
|
8902
9270
|
var W_WIKILINK = 3;
|
|
@@ -9017,7 +9385,7 @@ function computeKeywordScore(terms, title, tags, body) {
|
|
|
9017
9385
|
return score;
|
|
9018
9386
|
}
|
|
9019
9387
|
async function loadOrBuildGraph(vault) {
|
|
9020
|
-
const graphPath =
|
|
9388
|
+
const graphPath = join44(vault, ".skillwiki", "graph.json");
|
|
9021
9389
|
let needsBuild = false;
|
|
9022
9390
|
try {
|
|
9023
9391
|
const fileStat = await stat9(graphPath);
|
|
@@ -9031,7 +9399,7 @@ async function loadOrBuildGraph(vault) {
|
|
|
9031
9399
|
if (buildResult.exitCode !== 0) return null;
|
|
9032
9400
|
}
|
|
9033
9401
|
try {
|
|
9034
|
-
const raw = await
|
|
9402
|
+
const raw = await readFile28(graphPath, "utf8");
|
|
9035
9403
|
return JSON.parse(raw);
|
|
9036
9404
|
} catch {
|
|
9037
9405
|
return null;
|
|
@@ -9040,13 +9408,13 @@ async function loadOrBuildGraph(vault) {
|
|
|
9040
9408
|
|
|
9041
9409
|
// src/utils/auto-commit.ts
|
|
9042
9410
|
import { existsSync as existsSync19 } from "fs";
|
|
9043
|
-
import { join as
|
|
9411
|
+
import { join as join45 } from "path";
|
|
9044
9412
|
async function postCommit(vault, exitCode) {
|
|
9045
9413
|
if (exitCode !== 0) return;
|
|
9046
9414
|
const home = process.env.HOME ?? "";
|
|
9047
9415
|
const dotenv = await parseDotenvFile(configPath(home));
|
|
9048
9416
|
if (dotenv["AUTO_COMMIT"] === "false") return;
|
|
9049
|
-
if (!existsSync19(
|
|
9417
|
+
if (!existsSync19(join45(vault, ".git"))) return;
|
|
9050
9418
|
const lastOps = readLastOp(vault);
|
|
9051
9419
|
if (lastOps.length === 0) return;
|
|
9052
9420
|
const porcelain = git(vault, ["status", "--porcelain"]);
|
|
@@ -9097,7 +9465,7 @@ program.command("validate <file>").description("validate vault page frontmatter
|
|
|
9097
9465
|
emit(await runValidate({ file, apply: !!opts.apply, vault }), vault);
|
|
9098
9466
|
});
|
|
9099
9467
|
program.command("graph").description("graph subcommands").command("build <vault>").option("--out <path>", "graph output path (default: <vault>/.skillwiki/graph.json)").option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
9100
|
-
const out = opts.out ??
|
|
9468
|
+
const out = opts.out ?? join46(vault, ".skillwiki", "graph.json");
|
|
9101
9469
|
emit(await runGraphBuild({ vault, out }), vault);
|
|
9102
9470
|
});
|
|
9103
9471
|
var canvasCmd = program.command("canvas").description("manage Obsidian canvas files");
|
|
@@ -9446,6 +9814,17 @@ program.command("observe [vault]").description("create a raw transcript observat
|
|
|
9446
9814
|
project: opts.project
|
|
9447
9815
|
}), v.vault);
|
|
9448
9816
|
});
|
|
9817
|
+
program.command("session-brief [vault]").description("render or refresh the bounded startup session brief").option("--project <slug>", "project slug, or auto for deterministic detection", "auto").option("--write", "write meta/latest-session-brief.md and local cache files", false).option("--wiki <name>", "wiki profile name").action(async (vault, opts) => {
|
|
9818
|
+
const v = await resolveVaultArg(vault, opts.wiki);
|
|
9819
|
+
if (!v.ok) emit({ exitCode: v.exitCode, result: v.payload });
|
|
9820
|
+
else emit(await runSessionBrief({
|
|
9821
|
+
vault: v.vault,
|
|
9822
|
+
project: opts.project,
|
|
9823
|
+
write: !!opts.write,
|
|
9824
|
+
cwd: process.cwd(),
|
|
9825
|
+
env: { SKILLWIKI_PROJECT: process.env.SKILLWIKI_PROJECT }
|
|
9826
|
+
}), v.vault, { postCommit: !!opts.write });
|
|
9827
|
+
});
|
|
9449
9828
|
program.command("ingest <source>").description("ingest a source URL or local file into the vault").requiredOption("--vault <path>", "vault root directory").requiredOption("--type <type>", "typed-knowledge type (entity|concept|comparison|query)").requiredOption("--title <title>", "page title").option("--tags <csv>", "comma-separated tags").option("--provenance <provenance>", "provenance (research|project)").option("--dry-run", "preview without writing files", false).action(async (source, opts) => {
|
|
9450
9829
|
const tags = typeof opts.tags === "string" ? opts.tags.split(",").map((s) => s.trim()).filter((s) => s.length > 0) : [];
|
|
9451
9830
|
emit(await runIngest({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skillwiki",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.7",
|
|
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": {
|
|
@@ -53,6 +53,136 @@ Use this detected PRD mode as the source of truth. Route generated spec/plan art
|
|
|
53
53
|
EOF
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
read_dotenv_key() {
|
|
57
|
+
local file="$1"
|
|
58
|
+
local key="$2"
|
|
59
|
+
local line
|
|
60
|
+
|
|
61
|
+
[[ -f "$file" ]] || return 0
|
|
62
|
+
line=$(grep -E "^[[:space:]]*${key}=" "$file" 2>/dev/null | head -n 1 || true)
|
|
63
|
+
[[ -n "$line" ]] || return 0
|
|
64
|
+
|
|
65
|
+
printf '%s' "$line" \
|
|
66
|
+
| sed -E "s/^[[:space:]]*${key}=//; s/[[:space:]]+#.*$//; s/[[:space:]]+$//; s/^['\"]//; s/['\"]$//"
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
resolve_vault_path_for_session() {
|
|
70
|
+
if [[ -n "${WIKI_PATH:-}" ]]; then
|
|
71
|
+
printf '%s' "$WIKI_PATH"
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
local project_env="${PWD}/skillwiki/.env"
|
|
76
|
+
local project_path
|
|
77
|
+
project_path=$(read_dotenv_key "$project_env" "WIKI_PATH")
|
|
78
|
+
if [[ -n "$project_path" ]]; then
|
|
79
|
+
printf '%s' "$project_path"
|
|
80
|
+
return 0
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
local global_path
|
|
84
|
+
global_path=$(read_dotenv_key "${HOME:-}/.skillwiki/.env" "WIKI_PATH")
|
|
85
|
+
if [[ -n "$global_path" ]]; then
|
|
86
|
+
printf '%s' "$global_path"
|
|
87
|
+
return 0
|
|
88
|
+
fi
|
|
89
|
+
|
|
90
|
+
if command -v skillwiki >/dev/null 2>&1; then
|
|
91
|
+
skillwiki path 2>/dev/null | sed -n 's/^{"ok":true,"data":{"path":"\([^"]*\)".*$/\1/p' | head -n 1
|
|
92
|
+
fi
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
file_mtime_epoch() {
|
|
96
|
+
local file="$1"
|
|
97
|
+
|
|
98
|
+
stat -c %Y "$file" 2>/dev/null || stat -f %m "$file" 2>/dev/null || printf '0'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
strip_frontmatter_for_session() {
|
|
102
|
+
local file="$1"
|
|
103
|
+
|
|
104
|
+
awk '
|
|
105
|
+
NR == 1 && $0 == "---" { in_fm = 1; next }
|
|
106
|
+
in_fm && $0 == "---" { in_fm = 0; next }
|
|
107
|
+
!in_fm { print }
|
|
108
|
+
' "$file" 2>/dev/null
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
read_session_brief_candidate() {
|
|
112
|
+
local vault="$1"
|
|
113
|
+
local file="$2"
|
|
114
|
+
local label="$3"
|
|
115
|
+
local max_age_hours="$4"
|
|
116
|
+
local strip_frontmatter="${5:-false}"
|
|
117
|
+
|
|
118
|
+
[[ -f "$file" ]] || return 1
|
|
119
|
+
|
|
120
|
+
local now
|
|
121
|
+
local mtime
|
|
122
|
+
local age_seconds
|
|
123
|
+
local age_hours
|
|
124
|
+
now=$(date +%s)
|
|
125
|
+
mtime=$(file_mtime_epoch "$file")
|
|
126
|
+
[[ "$mtime" =~ ^[0-9]+$ ]] || mtime=0
|
|
127
|
+
age_seconds=$((now - mtime))
|
|
128
|
+
if (( age_seconds < 0 )); then age_seconds=0; fi
|
|
129
|
+
age_hours=$((age_seconds / 3600))
|
|
130
|
+
|
|
131
|
+
if (( age_hours > max_age_hours )); then
|
|
132
|
+
return 1
|
|
133
|
+
fi
|
|
134
|
+
|
|
135
|
+
local freshness="fresh"
|
|
136
|
+
if (( age_hours >= 24 )); then
|
|
137
|
+
freshness="stale"
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
printf '## Dynamic Session Memory\n\n'
|
|
141
|
+
printf '_Source: %s; Session brief age: %s (%sh)._ \n\n' "$label" "$freshness" "$age_hours"
|
|
142
|
+
|
|
143
|
+
if [[ "$strip_frontmatter" == "true" ]]; then
|
|
144
|
+
strip_frontmatter_for_session "$file"
|
|
145
|
+
else
|
|
146
|
+
cat "$file" 2>/dev/null
|
|
147
|
+
fi
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
compute_session_brief_readonly() {
|
|
151
|
+
local vault="$1"
|
|
152
|
+
|
|
153
|
+
command -v skillwiki >/dev/null 2>&1 || return 1
|
|
154
|
+
skillwiki session-brief "$vault" --project auto --human 2>/dev/null || return 1
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
build_dynamic_session_memory() {
|
|
158
|
+
local vault
|
|
159
|
+
vault=$(resolve_vault_path_for_session)
|
|
160
|
+
[[ -n "$vault" && -d "$vault" ]] || return 0
|
|
161
|
+
|
|
162
|
+
local cache_file="${vault}/.skillwiki/session-brief.md"
|
|
163
|
+
local committed_file="${vault}/meta/latest-session-brief.md"
|
|
164
|
+
local brief
|
|
165
|
+
|
|
166
|
+
if brief=$(read_session_brief_candidate "$vault" "$cache_file" ".skillwiki/session-brief.md" 72 false); then
|
|
167
|
+
printf '%s' "$brief"
|
|
168
|
+
return 0
|
|
169
|
+
fi
|
|
170
|
+
|
|
171
|
+
if brief=$(read_session_brief_candidate "$vault" "$committed_file" "meta/latest-session-brief.md" 72 true); then
|
|
172
|
+
printf '%s' "$brief"
|
|
173
|
+
return 0
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
if brief=$(compute_session_brief_readonly "$vault"); then
|
|
177
|
+
printf '## Dynamic Session Memory\n\n'
|
|
178
|
+
printf '_Source: read-only `skillwiki session-brief --project auto --human` fallback._\n\n'
|
|
179
|
+
printf '%s' "$brief"
|
|
180
|
+
return 0
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
return 0
|
|
184
|
+
}
|
|
185
|
+
|
|
56
186
|
escape_for_json() {
|
|
57
187
|
local s="$1"
|
|
58
188
|
s="${s//\\/\\\\}"
|
|
@@ -88,11 +218,17 @@ resolve_skill_path() {
|
|
|
88
218
|
build_skillwiki_session_context() {
|
|
89
219
|
local skill_content="$1"
|
|
90
220
|
local project_prd_context
|
|
221
|
+
local dynamic_session_memory
|
|
91
222
|
local session_context
|
|
92
223
|
|
|
93
224
|
project_prd_context=$(build_project_prd_context)
|
|
225
|
+
dynamic_session_memory=$(build_dynamic_session_memory)
|
|
94
226
|
|
|
95
227
|
session_context=$'### Skillwiki Activation\n\nSkillwiki is active for this workspace. Below are the capability guidelines for local reference:'
|
|
228
|
+
if [[ -n "$dynamic_session_memory" ]]; then
|
|
229
|
+
session_context+=$'\n\n'
|
|
230
|
+
session_context+="$dynamic_session_memory"
|
|
231
|
+
fi
|
|
96
232
|
if [[ -n "$project_prd_context" ]]; then
|
|
97
233
|
session_context+=$'\n\n'
|
|
98
234
|
session_context+="$project_prd_context"
|