@cogit/cli 0.30.0 → 0.31.1

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "cogit",
3
3
  "displayName": "cogit",
4
- "version": "0.30.0",
4
+ "version": "0.31.1",
5
5
  "description": "Git remembers what changed. cogit remembers why.",
6
6
  "author": {
7
7
  "name": "Eren Sertkaya"
package/README.md CHANGED
@@ -8,8 +8,47 @@ Git remembers what changed. cogit remembers why.
8
8
 
9
9
  cogit records every message you send to an AI coding agent as a *thought commit*: what you
10
10
  asked, what came of it, and the message it answered. Your code commits point back to the
11
- thought behind them, so `git log` tells you what changed and cogit tells you why. It works with
12
- Claude Code today.
11
+ thought behind them, so `cogit why` takes any line of code back to the reason it exists. It works
12
+ with Claude Code today.
13
+
14
+ Agent memory helps your agent remember. cogit helps you understand.
15
+
16
+ It runs as a Claude Code plugin and works through Claude Code hooks, so the record is written
17
+ while you work, not reconstructed from the diff afterwards.
18
+
19
+ **[Click through a saved map](https://erensrtky.github.io/cogit/demo/)**: no install, no server.
20
+ It's the page `cogit open` writes, built from sample data and read-only.
21
+
22
+ ## Why
23
+
24
+ I started by copying code out of ChatGPT, then moved to browser tools, editor extensions and now
25
+ terminal agents. Each step moved the code further from me, and I kept feeling something was
26
+ slipping away. When you write code yourself, it sinks in bit by bit: you know why that table
27
+ exists and why you wrote that class. When an agent writes it, that feeling goes away.
28
+
29
+ The information isn't really gone. It sits in your messages, the agent's replies and your commits,
30
+ just in separate places. cogit ties them together, so you can always walk back and see why
31
+ something was done. It helps most when you juggle projects: I run 5-10 agents at once across 2-3
32
+ of them, and a few minutes later I can lose track of what I did where.
33
+
34
+ <img src="docs/images/why-abstraction.png" alt="A conceptual chart from machine code to software factories: abstraction keeps rising, while how much of the why you can still trace falls sharply once AI starts writing code. cogit keeps what falls away.">
35
+
36
+ It's bigger than one person's memory: a reviewer sees what changed, not why.
37
+ [More, with research and sources](docs/guide/why.md).
38
+
39
+ cogit isn't:
40
+
41
+ - **Agent memory** (OpenViking, Mem0): that helps your agent recall what you talked about.
42
+ - **A token saver** (RTK, Headroom): that sends the model less.
43
+ - **A code map** (graphify): that shows how a codebase fits together.
44
+ - **An orchestrator** (Herdr, Conductor): that runs many agents at once.
45
+ - **A planning framework** (BMAD, Spec Kit): that decides what to build next.
46
+ - **An AI review bot** (CodeRabbit, Greptile): that comments on whether a diff looks right.
47
+
48
+ It answers the one question they leave open: why is this code the way it is? I use several of
49
+ them alongside it.
50
+
51
+ ## See it
13
52
 
14
53
  <img src="docs/images/map.png" alt="The map: sessions as a git graph, each with its messages, branches, commit chips and a left off here marker">
15
54
 
@@ -23,8 +62,8 @@ what the change does, and the diff.*
23
62
 
24
63
  <img src="docs/images/tasks.png" alt="The Tasks panel: suggested items with Accept and Dismiss, open items with Done, and a Done section">
25
64
 
26
- *Tasks — what your sessions left undone, drafted when a session closes. You accept, dismiss or
27
- close each one.*
65
+ *Tasks — work that came up in a session and didn't get finished, caught when the session closes.
66
+ You accept, dismiss or close each one.*
28
67
 
29
68
  <img src="docs/images/files.png" alt="The Files view: a changed file opened, listing the messages that touched it with each message's reason and the option it rejected">
30
69
 
@@ -43,25 +82,21 @@ including the option that was rejected.*
43
82
  - **Intent, Shape, Code**: a message that landed commits shows why it happened, a small
44
83
  picture of what the change does (a diagram, a before/after or interface table, an example
45
84
  or plain steps), and the diff, in one place.
46
- - **Tasks**: when a session ends, a small model drafts a few user-level items from what it
47
- left undone; you accept, dismiss or close them (`cogit captures`, and on the map).
85
+ - **Tasks**: when a session ends, a small model catches the work that came up and didn't get
86
+ finished as a few user-level items; you accept, dismiss or close them (`cogit captures`, and
87
+ on the map).
48
88
  - **Pull request descriptions with the decisions**: `cogit pr` writes the goal, the
49
89
  decisions and the rejected options, never the raw chat.
50
90
  - **Teams share the record through git**: after a pull, your teammates' sessions are on the
51
91
  map too, with who ran them and what changed most.
52
- - **The agent remembers**: a new session starts where the last one left off, and before
53
- editing a file the agent hears what was already tried and rejected there.
54
92
  - **Cheap by default**: a small side model records in the background; in our sessions it
55
93
  cost about 1–2% of a session's tokens.
56
- - **Private by default**: no server, no account. The raw conversation stays on your machine
57
- and keys are redacted.
58
-
59
- ## Why
60
-
61
- AI coding agents made writing code faster and moved the bottleneck to review: in a study of
62
- 718 firms, they opened 23% more pull requests while review time rose 49% and finished work did
63
- not measurably grow. A reviewer sees what changed, not why; the why stays in a chat nobody else
64
- reads. cogit keeps it next to the code. [More, with sources](docs/guide/why.md).
94
+ - **No server, no account**: it runs on your machine. Your chats are not committed. The record
95
+ (titles, decisions and reasons) is, so it travels with your branch; `cogit init` lets you keep
96
+ it local instead. Key and token patterns are redacted.
97
+ - **The agent gets the context too**: a new session starts where the last one left off, and
98
+ before editing a file the agent hears what was already tried and rejected there. It works
99
+ alongside memory tools; it doesn't replace them.
65
100
 
66
101
  ## Install
67
102
 
@@ -21206,7 +21206,7 @@ async function migrateProject(root) {
21206
21206
  const next = renameGitignoreLines(text);
21207
21207
  if (next !== text) await atomicWrite(gitignore, next).catch(() => void 0);
21208
21208
  }
21209
- const { installGitHooks: installGitHooks2 } = await import("./git-hooks-6ROMVVPI.js");
21209
+ const { installGitHooks: installGitHooks2 } = await import("./git-hooks-KQ3BN2VC.js");
21210
21210
  await installGitHooks2(dir).catch(() => void 0);
21211
21211
  return true;
21212
21212
  }
@@ -23793,6 +23793,9 @@ function isStoredTurn(value) {
23793
23793
  const turn = value;
23794
23794
  return typeof turn.uuid === "string" && turn.uuid !== "" && typeof turn.at === "string" && typeof turn.text === "string" && Array.isArray(turn.replies) && turn.replies.every((reply) => typeof reply === "string") && turn.tools !== null && typeof turn.tools === "object";
23795
23795
  }
23796
+ function answerSize(turn) {
23797
+ return turn.replies.reduce((sum, reply) => sum + reply.length, 0) + JSON.stringify(turn.tools).length;
23798
+ }
23796
23799
  async function readConversationStore(root, sessionId) {
23797
23800
  let text;
23798
23801
  try {
@@ -23819,16 +23822,28 @@ async function readConversationStore(root, sessionId) {
23819
23822
  order.push(value.uuid);
23820
23823
  continue;
23821
23824
  }
23822
- if (prev.hash === void 0 && value.hash !== void 0) byUuid.set(value.uuid, value);
23825
+ const fuller = answerSize(value) > answerSize(prev) ? value : prev;
23826
+ const hash2 = prev.hash ?? value.hash;
23827
+ byUuid.set(value.uuid, {
23828
+ ...prev,
23829
+ ...hash2 === void 0 ? {} : { hash: hash2 },
23830
+ replies: fuller.replies,
23831
+ tools: fuller.tools
23832
+ });
23823
23833
  }
23824
23834
  return order.map((uuid3) => byUuid.get(uuid3));
23825
23835
  }
23826
23836
  async function appendConversationTurns(root, sessionId, turns) {
23827
- const known = new Set((await readConversationStore(root, sessionId)).map((turn) => turn.uuid));
23837
+ const stored = new Map(
23838
+ (await readConversationStore(root, sessionId)).map((turn) => [turn.uuid, turn])
23839
+ );
23840
+ const seen = /* @__PURE__ */ new Set();
23828
23841
  const fresh = [];
23829
23842
  for (const turn of turns) {
23830
- if (turn.uuid === void 0 || known.has(turn.uuid)) continue;
23831
- known.add(turn.uuid);
23843
+ if (turn.uuid === void 0 || seen.has(turn.uuid)) continue;
23844
+ seen.add(turn.uuid);
23845
+ const held = stored.get(turn.uuid);
23846
+ if (held && answerSize(turn) <= answerSize(held)) continue;
23832
23847
  fresh.push({
23833
23848
  uuid: turn.uuid,
23834
23849
  ...turn.hash === void 0 ? {} : { hash: turn.hash },
@@ -23839,13 +23854,17 @@ async function appendConversationTurns(root, sessionId, turns) {
23839
23854
  });
23840
23855
  }
23841
23856
  if (fresh.length === 0) return 0;
23842
- const { value } = redactDeep(fresh);
23857
+ const value = redactDeep(fresh).value.filter((turn) => {
23858
+ const held = stored.get(turn.uuid);
23859
+ return !held || answerSize(turn) > answerSize(held);
23860
+ });
23861
+ if (value.length === 0) return 0;
23843
23862
  const chunk = `${value.map((turn) => JSON.stringify(turn)).join("\n")}
23844
23863
  `;
23845
23864
  const file2 = projectPaths(root).conversation(sessionId);
23846
23865
  await mkdir4(dirname6(file2), { recursive: true });
23847
23866
  await appendFile(file2, gzipSync(Buffer.from(chunk, "utf8")));
23848
- return fresh.length;
23867
+ return value.length;
23849
23868
  }
23850
23869
  async function appendConversationHashes(root, sessionId, hashes) {
23851
23870
  if (hashes.size === 0) return 0;
@@ -24044,6 +24063,9 @@ var exec4 = promisify3(execFile3);
24044
24063
  var THOUGHT_TRAILER = "Thought";
24045
24064
  var THOUGHT_VALUE = /^h[0-9a-f]{12}$/;
24046
24065
  var FULL_HASH = /^[0-9a-f]{40}$/;
24066
+ var THOUGHT_LINE = new RegExp(`^[ \\t]*${THOUGHT_TRAILER}:[ \\t]*(h[0-9a-f]{12})[ \\t]*$`, "gim");
24067
+ var COMMIT_MARK = "";
24068
+ var LOG_MAX_BUFFER = 256 * 1024 * 1024;
24047
24069
  async function readable(path2) {
24048
24070
  return access2(path2).then(
24049
24071
  () => true,
@@ -24070,10 +24092,23 @@ async function writeTrailer(messageFile, value) {
24070
24092
  messagePath(messageFile)
24071
24093
  ]);
24072
24094
  }
24095
+ async function replayingCommits(dir, env) {
24096
+ let gitDir;
24097
+ try {
24098
+ const { stdout } = await exec4("git", ["-C", dir, "rev-parse", "--absolute-git-dir"], { env });
24099
+ gitDir = String(stdout).trim();
24100
+ } catch {
24101
+ return false;
24102
+ }
24103
+ const states = ["rebase-merge", "rebase-apply", "CHERRY_PICK_HEAD"];
24104
+ const found = await Promise.all(states.map((state) => readable(resolve3(gitDir, state))));
24105
+ return found.includes(true);
24106
+ }
24073
24107
  async function addThoughtTrailer(opts) {
24074
24108
  const env = opts.env ?? process.env;
24075
24109
  if (captureDisabled(env)) return false;
24076
24110
  if (opts.source === "merge" || opts.source === "squash") return false;
24111
+ if (await replayingCommits(opts.dir, env)) return false;
24077
24112
  const agent = defaultAgent();
24078
24113
  const named = (env[agent.commitSessionEnv] ?? "").trim();
24079
24114
  if (named !== "") {
@@ -24101,17 +24136,14 @@ async function addThoughtTrailer(opts) {
24101
24136
  }
24102
24137
  function parseThoughtTrailers(stdout) {
24103
24138
  const out = /* @__PURE__ */ new Map();
24104
- for (const line of stdout.split("\n")) {
24105
- const separator = line.indexOf("\0");
24139
+ for (const record2 of stdout.split(COMMIT_MARK)) {
24140
+ const separator = record2.indexOf("\0");
24106
24141
  if (separator === -1) continue;
24107
- const commit = line.slice(0, separator);
24142
+ const commit = record2.slice(0, separator);
24108
24143
  if (!FULL_HASH.test(commit)) continue;
24109
- for (const value of line.slice(separator + 1).split(/[,\s]+/)) {
24110
- if (THOUGHT_VALUE.test(value)) {
24111
- out.set(commit, value);
24112
- break;
24113
- }
24114
- }
24144
+ const body = record2.slice(separator + 1);
24145
+ const thoughts = [...body.matchAll(THOUGHT_LINE)].map((match) => match[1]);
24146
+ if (thoughts.length > 0) out.set(commit, [...new Set(thoughts)]);
24115
24147
  }
24116
24148
  return out;
24117
24149
  }
@@ -24122,15 +24154,17 @@ async function readThoughtTrailers(root, opts = {}) {
24122
24154
  "-C",
24123
24155
  gitRoot,
24124
24156
  "log",
24125
- "--format=%H%x00%(trailers:key=Thought,valueonly)",
24157
+ "--format=%x01%H%x00%B",
24158
+ "--extended-regexp",
24159
+ "--regexp-ignore-case",
24160
+ `--grep=^[[:space:]]*${THOUGHT_TRAILER}:`,
24126
24161
  ...opts.since === void 0 ? [] : [`--since=${opts.since}`]
24127
24162
  ];
24128
24163
  try {
24129
- const { stdout } = await exec4(
24130
- "git",
24131
- args,
24132
- opts.env === void 0 ? void 0 : { env: opts.env }
24133
- );
24164
+ const { stdout } = await exec4("git", args, {
24165
+ maxBuffer: LOG_MAX_BUFFER,
24166
+ ...opts.env === void 0 ? {} : { env: opts.env }
24167
+ });
24134
24168
  return parseThoughtTrailers(String(stdout));
24135
24169
  } catch {
24136
24170
  return /* @__PURE__ */ new Map();
@@ -24158,19 +24192,21 @@ function earliestSessionStart(sessions) {
24158
24192
  function sameCommit(recorded, commit) {
24159
24193
  return commit.startsWith(recorded) || recorded.startsWith(commit);
24160
24194
  }
24161
- function thoughtOf(recorded, trailers) {
24162
- for (const [commit, thought] of trailers) {
24163
- if (sameCommit(recorded, commit)) return thought;
24195
+ function thoughtsOf(recorded, trailers) {
24196
+ for (const [commit, thoughts] of trailers) {
24197
+ if (sameCommit(recorded, commit)) return thoughts;
24164
24198
  }
24165
24199
  return void 0;
24166
24200
  }
24167
24201
  function attachThoughtTrailers(sessions, trailers) {
24168
24202
  if (trailers.size === 0) return [...sessions];
24169
24203
  const byThought = /* @__PURE__ */ new Map();
24170
- for (const [commit, thought] of trailers) {
24171
- const list = byThought.get(thought);
24172
- if (list === void 0) byThought.set(thought, [commit]);
24173
- else if (!list.some((hash2) => sameCommit(hash2, commit))) list.push(commit);
24204
+ for (const [commit, thoughts] of trailers) {
24205
+ for (const thought of thoughts) {
24206
+ const list = byThought.get(thought);
24207
+ if (list === void 0) byThought.set(thought, [commit]);
24208
+ else if (!list.some((hash2) => sameCommit(hash2, commit))) list.push(commit);
24209
+ }
24174
24210
  }
24175
24211
  return sessions.map((session) => {
24176
24212
  let changed = false;
@@ -24178,8 +24214,8 @@ function attachThoughtTrailers(sessions, trailers) {
24178
24214
  if (node2.kind !== "message") return node2;
24179
24215
  const current = node2.commits ?? [];
24180
24216
  const kept = current.filter((hash2) => {
24181
- const thought = thoughtOf(hash2, trailers);
24182
- return thought === void 0 || thought === node2.id;
24217
+ const thoughts = thoughtsOf(hash2, trailers);
24218
+ return thoughts === void 0 || thoughts.includes(node2.id);
24183
24219
  });
24184
24220
  const claimed = (byThought.get(node2.id) ?? []).filter(
24185
24221
  (commit) => !kept.some((hash2) => sameCommit(hash2, commit))
@@ -24500,9 +24536,8 @@ function embedViewModel(template, model) {
24500
24536
  if (!template.includes(EMPTY_DATA_ELEMENT)) {
24501
24537
  throw new Error(`the template has no empty <script id="${DATA_ELEMENT_ID}"> element`);
24502
24538
  }
24503
- const data = `<script id="${DATA_ELEMENT_ID}" type="application/json">${escapeForScript(
24504
- JSON.stringify(model)
24505
- )}</script>`;
24539
+ const json2 = JSON.stringify(model, null, 1).replace(/\n +/g, "\n");
24540
+ const data = `<script id="${DATA_ELEMENT_ID}" type="application/json">${escapeForScript(json2)}</script>`;
24506
24541
  return template.replace(EMPTY_DATA_ELEMENT, () => data);
24507
24542
  }
24508
24543
 
@@ -25090,7 +25125,9 @@ async function show(opts) {
25090
25125
  ` ${candidate.sessionId} \xB7 ${candidate.label}${candidate.id === candidate.label ? "" : ` (${candidate.id})`} ${candidate.title}`
25091
25126
  );
25092
25127
  }
25093
- throw new CliError(`"${opts.ref}" is ambiguous; use a longer prefix`);
25128
+ throw new CliError(
25129
+ COMMIT_REF.test(opts.ref) ? `commit ${opts.ref} comes from several thoughts; open one by its message hash` : `"${opts.ref}" is ambiguous; use a longer prefix`
25130
+ );
25094
25131
  }
25095
25132
  const { session, messages, message } = result.hit;
25096
25133
  const sessionId = session.sessionId;
@@ -25559,7 +25596,7 @@ async function onSessionEnd(ctx) {
25559
25596
  // package.json
25560
25597
  var package_default = {
25561
25598
  name: "@cogit/cli",
25562
- version: "0.30.0",
25599
+ version: "0.31.1",
25563
25600
  description: "Git remembers what changed. cogit remembers why.",
25564
25601
  license: "MIT",
25565
25602
  keywords: [