@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.
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 `
|
|
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 —
|
|
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
|
|
47
|
-
|
|
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
|
-
- **
|
|
57
|
-
and
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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 ||
|
|
23831
|
-
|
|
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
|
|
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
|
|
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
|
|
24105
|
-
const separator =
|
|
24139
|
+
for (const record2 of stdout.split(COMMIT_MARK)) {
|
|
24140
|
+
const separator = record2.indexOf("\0");
|
|
24106
24141
|
if (separator === -1) continue;
|
|
24107
|
-
const commit =
|
|
24142
|
+
const commit = record2.slice(0, separator);
|
|
24108
24143
|
if (!FULL_HASH.test(commit)) continue;
|
|
24109
|
-
|
|
24110
|
-
|
|
24111
|
-
|
|
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%
|
|
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
|
-
|
|
24131
|
-
|
|
24132
|
-
|
|
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
|
|
24162
|
-
for (const [commit,
|
|
24163
|
-
if (sameCommit(recorded, commit)) return
|
|
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,
|
|
24171
|
-
const
|
|
24172
|
-
|
|
24173
|
-
|
|
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
|
|
24182
|
-
return
|
|
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
|
|
24504
|
-
|
|
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(
|
|
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.
|
|
25599
|
+
version: "0.31.1",
|
|
25563
25600
|
description: "Git remembers what changed. cogit remembers why.",
|
|
25564
25601
|
license: "MIT",
|
|
25565
25602
|
keywords: [
|