skillwiki 0.8.5 → 0.8.8

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 CHANGED
@@ -14,7 +14,7 @@ import {
14
14
  } from "./chunk-TPS5XD2J.js";
15
15
 
16
16
  // src/cli.ts
17
- import { join as join45 } from "path";
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).min(2, "meta pages must reference \u22652 projects")
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 readFile24, writeFile as writeFile14, mkdir as mkdir13 } from "fs/promises";
7391
- import { join as join36 } from "path";
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 readFile24(input.source, "utf8");
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 = join36(input.vault, rawRelPath);
7566
- const typedAbsPath = join36(input.vault, typedRelPath);
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 mkdir13(join36(input.vault, "raw", "articles"), { recursive: true });
7649
- await writeFile14(rawAbsPath, rawContent, "utf8");
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 mkdir13(join36(input.vault, typedDir), { recursive: true });
7658
- await writeFile14(typedAbsPath, typedContent, "utf8");
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 join38 } from "path";
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 join37 } from "path";
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 join37(vault, ".skillwiki", "sync.lock");
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 = join37(vault, ".skillwiki");
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(join38(vault, ".git"))) {
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(join38(vault, ".git"))) {
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(join38(vault, ".git"))) {
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 join39, relative as relative3, dirname as dirname13 } from "path";
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 = join39(dir, entry.name);
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 relative3(base, full).replace(/\\/g, "/");
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 = join39(input.vault, relPath);
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 = join39(target, obj.Key);
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 readFile25 } from "fs/promises";
8556
- import { join as join40 } from "path";
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 readFile25(join40(input.vault, "SCHEMA.md"), "utf8");
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 mkdir14, writeFile as writeFile15, stat as stat8 } from "fs/promises";
8644
- import { join as join41 } from "path";
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(join41(input.vault, "SCHEMA.md"));
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 = join41(input.vault, relPath);
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 mkdir14(join41(absPath, ".."), { recursive: true });
8726
- await writeFile15(absPath, content, "utf8");
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 = join41(input.vault, "raw", "articles", "example-source.md");
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 mkdir14(join41(rawPath, ".."), { recursive: true });
8736
- await writeFile15(rawPath, EXAMPLE_RAW, "utf8");
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 readFile26, writeFile as writeFile16 } from "fs/promises";
9126
+ import { readFile as readFile27, writeFile as writeFile17 } from "fs/promises";
8759
9127
  import { existsSync as existsSync18 } from "fs";
8760
- import { join as join42 } from "path";
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 ?? join42(input.vault, ".skillwiki", "graph.json");
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 readFile26(graphPath, "utf8");
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 = join42(input.vault, "vault-graph.canvas");
9244
+ const outPath = join43(input.vault, "vault-graph.canvas");
8877
9245
  try {
8878
- await writeFile16(outPath, JSON.stringify(canvas, null, 2));
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 readFile27, stat as stat9 } from "fs/promises";
8899
- import { join as join43 } from "path";
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 = join43(vault, ".skillwiki", "graph.json");
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 readFile27(graphPath, "utf8");
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 join44 } from "path";
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(join44(vault, ".git"))) return;
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 ?? join45(vault, ".skillwiki", "graph.json");
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.5",
3
+ "version": "0.8.8",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "skillwiki": "dist/cli.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.8.5",
3
+ "version": "0.8.8",
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": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skillwiki",
3
- "version": "0.8.5",
3
+ "version": "0.8.8",
4
4
  "description": "Project-aware Karpathy-style knowledge base for Codex with 18 prompt-only skills backed by the deterministic skillwiki CLI.",
5
5
  "author": {
6
6
  "name": "karlorz",
@@ -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"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillwiki/skills",
3
- "version": "0.8.5",
3
+ "version": "0.8.8",
4
4
  "private": true,
5
5
  "files": [
6
6
  "wiki-*",