@yurtsever/capsa 0.1.0-alpha.4 → 0.1.0-alpha.5

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.
Files changed (2) hide show
  1. package/dist/index.js +191 -16
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7228,6 +7228,9 @@ var DEFAULT_EXCLUDES = [
7228
7228
  ".Trash"
7229
7229
  ];
7230
7230
  var DEFAULT_MAX_DEPTH = 12;
7231
+ function isSourceAdapter(adapter) {
7232
+ return typeof adapter.collect === "function";
7233
+ }
7231
7234
  function stableId(absPath) {
7232
7235
  return createHash("sha1").update(absPath).digest("hex").slice(0, 12);
7233
7236
  }
@@ -7246,16 +7249,24 @@ async function scan(adapters, options = {}) {
7246
7249
  const items = [];
7247
7250
  const seenItemPaths = /* @__PURE__ */ new Set();
7248
7251
  const scannedRoots = [];
7252
+ const formatAdapters = adapters.filter((a) => !isSourceAdapter(a));
7253
+ const sourceAdapters = adapters.filter(isSourceAdapter);
7249
7254
  const rootSet = /* @__PURE__ */ new Set();
7250
7255
  for (const r of options.roots ?? [env.homeDir])
7251
7256
  rootSet.add(resolve(r));
7252
- for (const adapter of adapters) {
7257
+ for (const adapter of formatAdapters) {
7253
7258
  for (const r of adapter.globalRoots?.(env) ?? [])
7254
7259
  rootSet.add(resolve(r));
7255
7260
  }
7256
7261
  const roots = [...rootSet].filter((r) => ![...rootSet].some((other) => other !== r && r.startsWith(other + "/")));
7262
+ const addItem = (item) => {
7263
+ if (seenItemPaths.has(item.path))
7264
+ return;
7265
+ seenItemPaths.add(item.path);
7266
+ items.push(item);
7267
+ };
7257
7268
  const handleEntry = async (entry) => {
7258
- for (const adapter of adapters) {
7269
+ for (const adapter of formatAdapters) {
7259
7270
  let matched = false;
7260
7271
  try {
7261
7272
  matched = adapter.matches(entry);
@@ -7270,12 +7281,8 @@ async function scan(adapters, options = {}) {
7270
7281
  if (!matched)
7271
7282
  continue;
7272
7283
  try {
7273
- for (const item of await adapter.parse(entry, env)) {
7274
- if (seenItemPaths.has(item.path))
7275
- continue;
7276
- seenItemPaths.add(item.path);
7277
- items.push(item);
7278
- }
7284
+ for (const item of await adapter.parse(entry, env))
7285
+ addItem(item);
7279
7286
  } catch (err) {
7280
7287
  issues.push({
7281
7288
  path: entry.absPath,
@@ -7346,6 +7353,18 @@ async function scan(adapters, options = {}) {
7346
7353
  }
7347
7354
  scannedRoots.push(root);
7348
7355
  await walk(root, root, 0);
7356
+ for (const adapter of sourceAdapters) {
7357
+ try {
7358
+ for (const item of await adapter.collect(root, env))
7359
+ addItem(item);
7360
+ } catch (err) {
7361
+ issues.push({
7362
+ path: root,
7363
+ adapter: adapter.id,
7364
+ message: `collect() threw: ${String(err)}`
7365
+ });
7366
+ }
7367
+ }
7349
7368
  }
7350
7369
  return { items, issues, scannedRoots, durationMs: Date.now() - started };
7351
7370
  }
@@ -10250,9 +10269,164 @@ var Store = class {
10250
10269
  import { createHash as createHash2 } from "crypto";
10251
10270
  import { relative as relative5 } from "path";
10252
10271
 
10272
+ // ../adapter-git-log/dist/index.js
10273
+ import { execFile } from "child_process";
10274
+ import { existsSync as existsSync2 } from "fs";
10275
+ import { basename as basename4, join as join7 } from "path";
10276
+ import { promisify } from "util";
10277
+ var execFileAsync = promisify(execFile);
10278
+ var DEFAULTS = {
10279
+ limit: 200,
10280
+ maxFiles: 50,
10281
+ maxBodyChars: 4e3,
10282
+ includeMerges: false
10283
+ };
10284
+ var RECORD = "";
10285
+ var UNIT = "";
10286
+ var FORMAT = `${RECORD}%H${UNIT}%h${UNIT}%an${UNIT}%cI${UNIT}%s${UNIT}%b${UNIT}`;
10287
+ var ITEM_DIR = join7(".git", "commits");
10288
+ function gitLogAdapter(options = {}) {
10289
+ const opts = { ...DEFAULTS, ...options };
10290
+ return {
10291
+ id: "git-log",
10292
+ displayName: "Commit",
10293
+ async collect(root, _env) {
10294
+ if (!existsSync2(join7(root, ".git")))
10295
+ return [];
10296
+ const stdout = await runGitLog(root, opts);
10297
+ const items = [];
10298
+ for (const record2 of stdout.split(RECORD)) {
10299
+ const commit = parseRecord(record2);
10300
+ if (commit)
10301
+ items.push(toItem(root, commit, opts));
10302
+ }
10303
+ return items;
10304
+ }
10305
+ };
10306
+ }
10307
+ async function runGitLog(root, opts) {
10308
+ const args = [
10309
+ "--no-pager",
10310
+ "log",
10311
+ `--max-count=${opts.limit}`,
10312
+ "--name-only",
10313
+ "--no-color",
10314
+ `--format=${FORMAT}`
10315
+ ];
10316
+ if (!opts.includeMerges)
10317
+ args.push("--no-merges");
10318
+ try {
10319
+ const { stdout } = await execFileAsync("git", args, {
10320
+ cwd: root,
10321
+ timeout: 3e4,
10322
+ maxBuffer: 32 * 1024 * 1024,
10323
+ windowsHide: true
10324
+ });
10325
+ return stdout;
10326
+ } catch (err) {
10327
+ const e = err;
10328
+ if (/does not have any commits yet|bad default revision/i.test(String(e.stderr ?? ""))) {
10329
+ return "";
10330
+ }
10331
+ if (e.code === "ENOENT") {
10332
+ throw new Error(`${root} has a .git but git is not on PATH; no commits indexed`);
10333
+ }
10334
+ throw new Error(`git log failed in ${root}: ${firstLine2(e.stderr) || String(err)}`);
10335
+ }
10336
+ }
10337
+ function parseRecord(record2) {
10338
+ if (!record2.trim())
10339
+ return void 0;
10340
+ const parts = record2.split(UNIT);
10341
+ const [sha, shortSha, author, date5, subject, body] = parts;
10342
+ if (!sha || !shortSha)
10343
+ return void 0;
10344
+ return {
10345
+ sha,
10346
+ shortSha,
10347
+ author: author ?? "",
10348
+ date: date5 ?? "",
10349
+ subject: subject ?? "",
10350
+ body: (body ?? "").trim(),
10351
+ // Everything past the last field is the file list, one path per line.
10352
+ files: parts.slice(6).join(UNIT).split("\n").map((l) => l.trim()).filter(Boolean)
10353
+ };
10354
+ }
10355
+ function toItem(root, c, opts) {
10356
+ const path = join7(root, ITEM_DIR, `${c.shortSha}.md`);
10357
+ const files = c.files.slice(0, opts.maxFiles);
10358
+ const hidden = c.files.length - files.length;
10359
+ const subject = c.subject || "(no subject)";
10360
+ return {
10361
+ id: stableId(path),
10362
+ format: "git-log",
10363
+ formatLabel: "Commit",
10364
+ name: `${c.shortSha} ${subject}`,
10365
+ // The description becomes the provenance line of every embedded chunk,
10366
+ // so it carries the subject, not the sha.
10367
+ description: subject,
10368
+ path,
10369
+ scope: "project",
10370
+ projectRoot: root,
10371
+ content: renderCommit(c, subject, files, hidden, opts.maxBodyChars),
10372
+ metadata: {
10373
+ sha: c.sha,
10374
+ shortSha: c.shortSha,
10375
+ author: c.author,
10376
+ subject,
10377
+ files: c.files,
10378
+ repo: basename4(root)
10379
+ },
10380
+ knowledge: {
10381
+ kind: "commit",
10382
+ updatedAt: isoDate(c.date),
10383
+ relatedPaths: c.files.length ? c.files : void 0,
10384
+ tags: conventionalTags(subject)
10385
+ }
10386
+ };
10387
+ }
10388
+ function renderCommit(c, subject, files, hidden, maxBodyChars) {
10389
+ const body = c.body.length > maxBodyChars ? `${c.body.slice(0, maxBodyChars)}
10390
+
10391
+ \u2026` : c.body;
10392
+ const lines = [
10393
+ `# ${subject}`,
10394
+ "",
10395
+ `commit \`${c.shortSha}\` \xB7 ${c.author} \xB7 ${c.date.slice(0, 10)}`
10396
+ ];
10397
+ if (body)
10398
+ lines.push("", body);
10399
+ if (files.length) {
10400
+ lines.push("", "## Files", "");
10401
+ for (const f of files)
10402
+ lines.push(`- ${f}`);
10403
+ if (hidden > 0)
10404
+ lines.push(`- \u2026 and ${hidden} more`);
10405
+ }
10406
+ return `${lines.join("\n")}
10407
+ `;
10408
+ }
10409
+ function isoDate(raw2) {
10410
+ if (!raw2)
10411
+ return void 0;
10412
+ const t = Date.parse(raw2);
10413
+ return Number.isNaN(t) ? void 0 : new Date(t).toISOString();
10414
+ }
10415
+ var CONVENTIONAL = /^([a-z]{3,10})(?:\(([^)]{1,40})\))?!?:\s/;
10416
+ function conventionalTags(subject) {
10417
+ const m = CONVENTIONAL.exec(subject.toLowerCase());
10418
+ if (!m)
10419
+ return void 0;
10420
+ const tags = [m[1], ...m[2] ? [m[2]] : []];
10421
+ return tags;
10422
+ }
10423
+ function firstLine2(text) {
10424
+ return (text ?? "").split("\n").find((l) => l.trim()) ?? "";
10425
+ }
10426
+
10253
10427
  // ../adapter-runbooks/dist/index.js
10254
10428
  import { readFile as readFile5, stat as stat3 } from "fs/promises";
10255
- import { basename as basename4, dirname as dirname7, relative as relative3, sep as sep4 } from "path";
10429
+ import { basename as basename5, dirname as dirname7, relative as relative3, sep as sep4 } from "path";
10256
10430
  var DOC_DIRS = /* @__PURE__ */ new Set(["docs", "doc", "runbooks", "runbook", "wiki", "adr", "decisions"]);
10257
10431
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog", "plans", "plan", "roadmap", "milestones", "milestone", "node_modules"]);
10258
10432
  function segments(relPath) {
@@ -10283,7 +10457,7 @@ function runbooksAdapter() {
10283
10457
  const relPathInProject = relative3(projectRoot, entry.absPath);
10284
10458
  const fm = parseFrontmatter(raw2);
10285
10459
  const data = fm.data;
10286
- const title = data["title"] || firstHeading(raw2) || basename4(entry.name, ".md");
10460
+ const title = data["title"] || firstHeading(raw2) || basename5(entry.name, ".md");
10287
10461
  const kindFromPath = segments(relPathInProject).some((p) => ["adr", "decisions"].includes(p.toLowerCase())) ? "decision" : "runbook";
10288
10462
  const tags = parseTags(data["tags"]);
10289
10463
  return [
@@ -10291,7 +10465,7 @@ function runbooksAdapter() {
10291
10465
  id: stableId(entry.absPath),
10292
10466
  format: "runbooks",
10293
10467
  formatLabel: "Runbook",
10294
- name: `${basename4(projectRoot)}${sep4}${relPathInProject}`,
10468
+ name: `${basename5(projectRoot)}${sep4}${relPathInProject}`,
10295
10469
  description: title,
10296
10470
  path: entry.absPath,
10297
10471
  scope: "project",
@@ -10325,7 +10499,7 @@ function firstHeading(raw2) {
10325
10499
 
10326
10500
  // ../adapter-tickets/dist/index.js
10327
10501
  import { readFile as readFile6, stat as stat4 } from "fs/promises";
10328
- import { basename as basename5, dirname as dirname8, relative as relative4, sep as sep5 } from "path";
10502
+ import { basename as basename6, dirname as dirname8, relative as relative4, sep as sep5 } from "path";
10329
10503
  var TICKET_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog"]);
10330
10504
  var PLAN_DIRS = /* @__PURE__ */ new Set(["plans", "plan", "roadmap", "milestones", "milestone"]);
10331
10505
  var CHECKBOX = /^\s*[-*+]\s+\[( |x|X)\]\s+/gm;
@@ -10386,14 +10560,14 @@ function ticketsAdapter() {
10386
10560
  const data = fm.data;
10387
10561
  const derived2 = deriveStatus(raw2);
10388
10562
  const status = normaliseStatus(data["status"]) ?? derived2.status;
10389
- const title = data["title"] || firstHeading2(raw2) || basename5(entry.name, ".md");
10563
+ const title = data["title"] || firstHeading2(raw2) || basename6(entry.name, ".md");
10390
10564
  const tags = parseTags2(data["tags"]);
10391
10565
  return [
10392
10566
  {
10393
10567
  id: stableId(entry.absPath),
10394
10568
  format: "tickets",
10395
10569
  formatLabel: isPlan ? "Plan" : "Ticket",
10396
- name: `${basename5(projectRoot)}${sep5}${relPathInProject}`,
10570
+ name: `${basename6(projectRoot)}${sep5}${relPathInProject}`,
10397
10571
  description: title,
10398
10572
  path: entry.absPath,
10399
10573
  scope: "project",
@@ -10437,7 +10611,8 @@ function knowledgeAdapters() {
10437
10611
  claudeSkillsAdapter(),
10438
10612
  cursorRulesAdapter(),
10439
10613
  runbooksAdapter(),
10440
- ticketsAdapter()
10614
+ ticketsAdapter(),
10615
+ gitLogAdapter()
10441
10616
  ];
10442
10617
  }
10443
10618
  function openStore(projectRoot, embedder) {
@@ -10769,7 +10944,7 @@ function projectState(store) {
10769
10944
  const live = (i) => (i.kind === "ticket" || i.kind === "plan") && (i.status === "open" || i.status === "in-progress");
10770
10945
  const rank = (i) => i.status === "in-progress" ? 0 : 1;
10771
10946
  const open2 = items.filter(live).sort((a, b) => rank(a) - rank(b) || a.rel_path.localeCompare(b.rel_path)).map((i) => ({ relPath: i.rel_path, name: i.name, status: i.status ?? "unknown", updatedAt: i.updated_at }));
10772
- const recentlyChanged = [...items].sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")).slice(0, 10).map((i) => ({ relPath: i.rel_path, kind: i.kind, updatedAt: i.updated_at }));
10947
+ const recentlyChanged = items.filter((i) => i.kind !== "commit").sort((a, b) => (b.updated_at ?? "").localeCompare(a.updated_at ?? "")).slice(0, 10).map((i) => ({ relPath: i.rel_path, kind: i.kind, updatedAt: i.updated_at }));
10773
10948
  const decisions = store.db.prepare("SELECT id, created_at, title FROM decisions ORDER BY id DESC LIMIT 10").all().map((d) => ({ id: d.id, createdAt: d.created_at, title: d.title }));
10774
10949
  return { counts, open: open2, recentlyChanged, decisions };
10775
10950
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yurtsever/capsa",
3
- "version": "0.1.0-alpha.4",
3
+ "version": "0.1.0-alpha.5",
4
4
  "description": "Your project's knowledge in one capsule. Local-first project memory and cockpit for AI coding agents: indexes runbooks, tickets, plans and instruction files, serves the smallest relevant context over MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",