@yurtsever/capsa 0.1.0-alpha.3 → 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 +284 -28
  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
  }
@@ -10227,6 +10246,10 @@ var Store = class {
10227
10246
  }
10228
10247
  }));
10229
10248
  }
10249
+ /** All chunks of one item, in document order. */
10250
+ chunksForItem(itemId) {
10251
+ return this.db.prepare("SELECT id, item_id, idx, heading_path, text FROM chunks WHERE item_id = ? ORDER BY idx").all(itemId);
10252
+ }
10230
10253
  listItems() {
10231
10254
  return this.db.prepare("SELECT * FROM items ORDER BY kind, rel_path").all();
10232
10255
  }
@@ -10246,9 +10269,164 @@ var Store = class {
10246
10269
  import { createHash as createHash2 } from "crypto";
10247
10270
  import { relative as relative5 } from "path";
10248
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
+
10249
10427
  // ../adapter-runbooks/dist/index.js
10250
10428
  import { readFile as readFile5, stat as stat3 } from "fs/promises";
10251
- 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";
10252
10430
  var DOC_DIRS = /* @__PURE__ */ new Set(["docs", "doc", "runbooks", "runbook", "wiki", "adr", "decisions"]);
10253
10431
  var EXCLUDED_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog", "plans", "plan", "roadmap", "milestones", "milestone", "node_modules"]);
10254
10432
  function segments(relPath) {
@@ -10279,7 +10457,7 @@ function runbooksAdapter() {
10279
10457
  const relPathInProject = relative3(projectRoot, entry.absPath);
10280
10458
  const fm = parseFrontmatter(raw2);
10281
10459
  const data = fm.data;
10282
- const title = data["title"] || firstHeading(raw2) || basename4(entry.name, ".md");
10460
+ const title = data["title"] || firstHeading(raw2) || basename5(entry.name, ".md");
10283
10461
  const kindFromPath = segments(relPathInProject).some((p) => ["adr", "decisions"].includes(p.toLowerCase())) ? "decision" : "runbook";
10284
10462
  const tags = parseTags(data["tags"]);
10285
10463
  return [
@@ -10287,7 +10465,7 @@ function runbooksAdapter() {
10287
10465
  id: stableId(entry.absPath),
10288
10466
  format: "runbooks",
10289
10467
  formatLabel: "Runbook",
10290
- name: `${basename4(projectRoot)}${sep4}${relPathInProject}`,
10468
+ name: `${basename5(projectRoot)}${sep4}${relPathInProject}`,
10291
10469
  description: title,
10292
10470
  path: entry.absPath,
10293
10471
  scope: "project",
@@ -10321,7 +10499,7 @@ function firstHeading(raw2) {
10321
10499
 
10322
10500
  // ../adapter-tickets/dist/index.js
10323
10501
  import { readFile as readFile6, stat as stat4 } from "fs/promises";
10324
- 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";
10325
10503
  var TICKET_DIRS = /* @__PURE__ */ new Set(["tickets", "issues", "todo", "backlog"]);
10326
10504
  var PLAN_DIRS = /* @__PURE__ */ new Set(["plans", "plan", "roadmap", "milestones", "milestone"]);
10327
10505
  var CHECKBOX = /^\s*[-*+]\s+\[( |x|X)\]\s+/gm;
@@ -10382,14 +10560,14 @@ function ticketsAdapter() {
10382
10560
  const data = fm.data;
10383
10561
  const derived2 = deriveStatus(raw2);
10384
10562
  const status = normaliseStatus(data["status"]) ?? derived2.status;
10385
- const title = data["title"] || firstHeading2(raw2) || basename5(entry.name, ".md");
10563
+ const title = data["title"] || firstHeading2(raw2) || basename6(entry.name, ".md");
10386
10564
  const tags = parseTags2(data["tags"]);
10387
10565
  return [
10388
10566
  {
10389
10567
  id: stableId(entry.absPath),
10390
10568
  format: "tickets",
10391
10569
  formatLabel: isPlan ? "Plan" : "Ticket",
10392
- name: `${basename5(projectRoot)}${sep5}${relPathInProject}`,
10570
+ name: `${basename6(projectRoot)}${sep5}${relPathInProject}`,
10393
10571
  description: title,
10394
10572
  path: entry.absPath,
10395
10573
  scope: "project",
@@ -10433,7 +10611,8 @@ function knowledgeAdapters() {
10433
10611
  claudeSkillsAdapter(),
10434
10612
  cursorRulesAdapter(),
10435
10613
  runbooksAdapter(),
10436
- ticketsAdapter()
10614
+ ticketsAdapter(),
10615
+ gitLogAdapter()
10437
10616
  ];
10438
10617
  }
10439
10618
  function openStore(projectRoot, embedder) {
@@ -10566,6 +10745,40 @@ function rrf(lists, k = DEFAULT_WEIGHTS.rrfK) {
10566
10745
  return scores;
10567
10746
  }
10568
10747
 
10748
+ // ../index/dist/pin.js
10749
+ var LEADING_ID = /^([A-Za-z]{1,8}-?\d{1,5}(?:-\d{1,5})?|\d{2,6})(?=[-_.\s]|$)/;
10750
+ function leadingId(relPath) {
10751
+ const base = relPath.split(/[\\/]/).pop() ?? "";
10752
+ return LEADING_ID.exec(base)?.[1];
10753
+ }
10754
+ function pinnedItems(query, items) {
10755
+ const q = query.toLowerCase();
10756
+ const idCount = /* @__PURE__ */ new Map();
10757
+ for (const item of items) {
10758
+ const id = leadingId(item.rel_path)?.toLowerCase();
10759
+ if (id)
10760
+ idCount.set(id, (idCount.get(id) ?? 0) + 1);
10761
+ }
10762
+ const out = [];
10763
+ for (const item of items) {
10764
+ const rel = item.rel_path.toLowerCase();
10765
+ if (q.includes(rel)) {
10766
+ out.push(item);
10767
+ continue;
10768
+ }
10769
+ const id = leadingId(item.rel_path);
10770
+ if (!id || (idCount.get(id.toLowerCase()) ?? 0) > 1)
10771
+ continue;
10772
+ const re = new RegExp(`(^|[^\\w-])${escape2(id)}(?=$|[^\\w-])`, "i");
10773
+ if (re.test(query))
10774
+ out.push(item);
10775
+ }
10776
+ return out;
10777
+ }
10778
+ function escape2(s) {
10779
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
10780
+ }
10781
+
10569
10782
  // ../index/dist/retrieve.js
10570
10783
  async function retrieve(store, embedder, query, options = {}) {
10571
10784
  const started = Date.now();
@@ -10573,31 +10786,73 @@ async function retrieve(store, embedder, query, options = {}) {
10573
10786
  const candidates = options.candidates ?? 30;
10574
10787
  const weights = options.weights ?? DEFAULT_WEIGHTS;
10575
10788
  const minRelative = options.minRelativeScore ?? 0.5;
10789
+ const perItem = /* @__PURE__ */ new Map();
10790
+ const chunks = [];
10791
+ const debug = [];
10792
+ let tokens = 0;
10793
+ const pinned = pinnedItems(query, store.listItems());
10794
+ for (const item of pinned) {
10795
+ for (const c of store.chunksForItem(item.id)) {
10796
+ const t = estimateTokens(c.text);
10797
+ const fits = tokens + t <= maxTokens || chunks.length === 0;
10798
+ if (options.debug) {
10799
+ debug.push({
10800
+ chunkId: c.id,
10801
+ relPath: item.rel_path,
10802
+ headingPath: JSON.parse(c.heading_path),
10803
+ kind: item.kind,
10804
+ status: item.status,
10805
+ vecRank: null,
10806
+ ftsRank: null,
10807
+ fused: 0,
10808
+ factor: 1,
10809
+ score: Number.POSITIVE_INFINITY,
10810
+ kept: fits,
10811
+ why: fits ? "pinned" : "budget"
10812
+ });
10813
+ }
10814
+ if (!fits)
10815
+ continue;
10816
+ perItem.set(item.id, (perItem.get(item.id) ?? 0) + 1);
10817
+ tokens += t;
10818
+ chunks.push({
10819
+ chunkId: c.id,
10820
+ score: Number.POSITIVE_INFINITY,
10821
+ kind: item.kind,
10822
+ status: item.status,
10823
+ updatedAt: item.updated_at,
10824
+ relPath: item.rel_path,
10825
+ headingPath: JSON.parse(c.heading_path),
10826
+ text: c.text,
10827
+ tokens: t
10828
+ });
10829
+ }
10830
+ }
10831
+ const pinnedIds = new Set(pinned.map((i) => i.id));
10832
+ const neighbourCutoff = pinned.length ? Math.max(minRelative, 0.75) : minRelative;
10833
+ const neighbourBudget = pinned.length ? Math.min(maxTokens, tokens + Math.floor(maxTokens / 3)) : maxTokens;
10576
10834
  const [qv] = await embedder.embed([query]);
10577
10835
  const vec = qv ? store.vectorSearch(qv, candidates).map((r) => r.chunkId) : [];
10578
10836
  const fts = store.textSearch(query, candidates).map((r) => r.chunkId);
10579
10837
  const fused = rrf([vec, fts], weights.rrfK);
10580
- const rows = store.chunksWithItems([...fused.keys()]);
10838
+ const rows = store.chunksWithItems([...fused.keys()]).filter((r) => !pinnedIds.has(r.item_id));
10581
10839
  const now = Date.now();
10582
10840
  const scored = rows.map((r) => {
10583
10841
  const fusedScore = fused.get(r.id) ?? 0;
10584
10842
  const factor = signalFactor({ kind: r.item.kind, status: r.item.status, updatedAt: r.item.updated_at }, weights, now);
10585
10843
  return { row: r, fusedScore, factor, score: fusedScore * factor };
10586
10844
  }).sort((a, b) => b.score - a.score);
10587
- const perItem = /* @__PURE__ */ new Map();
10588
- const chunks = [];
10589
- const debug = [];
10590
- let tokens = 0;
10591
10845
  const best = scored[0]?.score ?? 0;
10592
- let cut = false;
10846
+ let cut = tokens >= maxTokens;
10847
+ let kept = 0;
10593
10848
  for (const { row, fusedScore, factor, score } of scored) {
10594
10849
  let why = "kept";
10595
- if (cut || chunks.length > 0 && score < best * minRelative) {
10850
+ if (cut || kept > 0 && score < best * neighbourCutoff) {
10596
10851
  cut = true;
10597
10852
  why = "cutoff";
10598
10853
  } else if ((perItem.get(row.item_id) ?? 0) >= 2) {
10599
10854
  why = "per-item cap";
10600
- } else if (tokens + estimateTokens(row.text) > maxTokens && chunks.length > 0) {
10855
+ } else if (tokens + estimateTokens(row.text) > neighbourBudget && chunks.length > 0) {
10601
10856
  why = "budget";
10602
10857
  }
10603
10858
  if (options.debug) {
@@ -10621,6 +10876,7 @@ async function retrieve(store, embedder, query, options = {}) {
10621
10876
  const t = estimateTokens(row.text);
10622
10877
  perItem.set(row.item_id, (perItem.get(row.item_id) ?? 0) + 1);
10623
10878
  tokens += t;
10879
+ kept++;
10624
10880
  chunks.push({
10625
10881
  chunkId: row.id,
10626
10882
  score,
@@ -10632,7 +10888,7 @@ async function retrieve(store, embedder, query, options = {}) {
10632
10888
  text: row.text,
10633
10889
  tokens: t
10634
10890
  });
10635
- if (tokens >= maxTokens)
10891
+ if (tokens >= neighbourBudget)
10636
10892
  cut = true;
10637
10893
  }
10638
10894
  const durationMs = Date.now() - started;
@@ -10652,15 +10908,15 @@ function rankIn(list, id) {
10652
10908
  function formatDebug(result) {
10653
10909
  if (!result.debug)
10654
10910
  return "";
10655
- const lines = [" vec fts factor score keep where"];
10911
+ const lines = [" vec fts factor score keep where"];
10656
10912
  for (const d of result.debug) {
10657
10913
  const where = [d.relPath, ...d.headingPath].join(" \u203A ").slice(0, 70);
10658
10914
  lines.push([
10659
10915
  String(d.vecRank ?? "-").padStart(5),
10660
10916
  String(d.ftsRank ?? "-").padStart(4),
10661
10917
  d.factor.toFixed(2).padStart(7),
10662
- d.score.toFixed(4).padStart(8),
10663
- (d.kept ? "yes" : d.why).padEnd(12),
10918
+ (Number.isFinite(d.score) ? d.score.toFixed(4) : "pin").padStart(8),
10919
+ (d.kept ? d.why === "pinned" ? "pinned" : "yes" : d.why).padEnd(12),
10664
10920
  where
10665
10921
  ].join(" "));
10666
10922
  }
@@ -10688,7 +10944,7 @@ function projectState(store) {
10688
10944
  const live = (i) => (i.kind === "ticket" || i.kind === "plan") && (i.status === "open" || i.status === "in-progress");
10689
10945
  const rank = (i) => i.status === "in-progress" ? 0 : 1;
10690
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 }));
10691
- 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 }));
10692
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 }));
10693
10949
  return { counts, open: open2, recentlyChanged, decisions };
10694
10950
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yurtsever/capsa",
3
- "version": "0.1.0-alpha.3",
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",