@bli-cockpit/memory-mcp 0.1.6 → 0.1.7

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,5 +1,6 @@
1
1
  /**
2
- * The last exchange out of a Claude Code transcript (BLI-3580).
2
+ * The last exchange out of an agent host's transcript (BLI-3580; Codex added
3
+ * by BLI-3729).
3
4
  *
4
5
  * The Stop hook is handed `transcript_path`: a JSONL file, one record per line,
5
6
  * appended to for the whole session. What the save door wants is the LAST
@@ -19,6 +20,31 @@
19
20
  * A record shape that is not recognised is skipped, never guessed at. The
20
21
  * reader returns null rather than half an exchange: half a turn saved as a
21
22
  * memory is a memory that says something the person did not.
23
+ *
24
+ * ## Two hosts, two record shapes (BLI-3729)
25
+ *
26
+ * `cockpit memory install` now registers the same three hooks with Codex, and
27
+ * Codex hands its Stop hook a `transcript_path` too — pointing at its own
28
+ * rollout JSONL, whose records are shaped differently:
29
+ *
30
+ * Claude Code { "message": { "role": "user", "content": [{ "type": "text", … }] } }
31
+ * Codex { "type": "response_item",
32
+ * "payload": { "type": "message", "role": "user",
33
+ * "content": [{ "type": "input_text", "text": … }] } }
34
+ *
35
+ * Both are read here, by the same backwards walk, because the alternative was a
36
+ * Codex Stop hook that fired on every turn and reported `transcript_no_exchange`
37
+ * every time — a hook that does nothing and says it did nothing. The Codex
38
+ * shape was captured from a real rollout file on 2026-09-05 rather than read
39
+ * off a doc, and the fixture in `hooks.test.ts` keeps the record structure
40
+ * verbatim with only the `text` values replaced (the real ones are somebody's
41
+ * conversation).
42
+ *
43
+ * `assistant` content blocks are `output_text` and `user` blocks are
44
+ * `input_text`; both are accepted alongside Claude's `text`. A `reasoning`,
45
+ * `custom_tool_call` or `event_msg` record contributes nothing, which is the
46
+ * same rule Claude's tool results already follow — a memory made of tool output
47
+ * is the transcript dump this repo refuses.
22
48
  */
23
49
  /** How much of the file's end is read. Big enough for a long final turn. */
24
50
  export declare const TAIL_BYTES: number;
@@ -1,5 +1,6 @@
1
1
  /**
2
- * The last exchange out of a Claude Code transcript (BLI-3580).
2
+ * The last exchange out of an agent host's transcript (BLI-3580; Codex added
3
+ * by BLI-3729).
3
4
  *
4
5
  * The Stop hook is handed `transcript_path`: a JSONL file, one record per line,
5
6
  * appended to for the whole session. What the save door wants is the LAST
@@ -19,6 +20,31 @@
19
20
  * A record shape that is not recognised is skipped, never guessed at. The
20
21
  * reader returns null rather than half an exchange: half a turn saved as a
21
22
  * memory is a memory that says something the person did not.
23
+ *
24
+ * ## Two hosts, two record shapes (BLI-3729)
25
+ *
26
+ * `cockpit memory install` now registers the same three hooks with Codex, and
27
+ * Codex hands its Stop hook a `transcript_path` too — pointing at its own
28
+ * rollout JSONL, whose records are shaped differently:
29
+ *
30
+ * Claude Code { "message": { "role": "user", "content": [{ "type": "text", … }] } }
31
+ * Codex { "type": "response_item",
32
+ * "payload": { "type": "message", "role": "user",
33
+ * "content": [{ "type": "input_text", "text": … }] } }
34
+ *
35
+ * Both are read here, by the same backwards walk, because the alternative was a
36
+ * Codex Stop hook that fired on every turn and reported `transcript_no_exchange`
37
+ * every time — a hook that does nothing and says it did nothing. The Codex
38
+ * shape was captured from a real rollout file on 2026-09-05 rather than read
39
+ * off a doc, and the fixture in `hooks.test.ts` keeps the record structure
40
+ * verbatim with only the `text` values replaced (the real ones are somebody's
41
+ * conversation).
42
+ *
43
+ * `assistant` content blocks are `output_text` and `user` blocks are
44
+ * `input_text`; both are accepted alongside Claude's `text`. A `reasoning`,
45
+ * `custom_tool_call` or `event_msg` record contributes nothing, which is the
46
+ * same rule Claude's tool results already follow — a memory made of tool output
47
+ * is the transcript dump this repo refuses.
22
48
  */
23
49
  import fs from "node:fs";
24
50
  import { STOP_CHUNK_CHARS } from "./contract.js";
@@ -123,8 +149,8 @@ function parseJsonlTail(raw) {
123
149
  function roleOf(record) {
124
150
  if (!record)
125
151
  return "";
126
- const message = record["message"];
127
- if (message && typeof message === "object" && !Array.isArray(message)) {
152
+ const message = messageOf(record);
153
+ if (message) {
128
154
  const role = message["role"];
129
155
  if (typeof role === "string")
130
156
  return role;
@@ -133,18 +159,32 @@ function roleOf(record) {
133
159
  return typeof type === "string" ? type : "";
134
160
  }
135
161
  /**
136
- * The text of a record, whichever of the three shapes it uses: a string
137
- * `content`, an array of blocks with `type: "text"`, or a bare `text`. A tool
138
- * result or an image block contributes nothing, on purpose a memory made of
139
- * tool output is the transcript dump this repo refuses.
162
+ * The record's message envelope, whichever host wrote it: Claude Code's
163
+ * `message`, Codex's `payload`, or the record itself when it carries `role`
164
+ * directly. Returns null for a record with no envelope at all, which is how a
165
+ * Codex `reasoning` or `event_msg` line drops out without a special case.
166
+ */
167
+ function messageOf(record) {
168
+ for (const key of ["message", "payload"]) {
169
+ const value = record[key];
170
+ if (value && typeof value === "object" && !Array.isArray(value)) {
171
+ return value;
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ /** The content block types that carry a person's or the model's own words. */
177
+ const TEXT_BLOCK_TYPES = new Set(["text", "input_text", "output_text"]);
178
+ /**
179
+ * The text of a record, whichever of the shapes it uses: a string `content`, an
180
+ * array of blocks whose `type` is one of `TEXT_BLOCK_TYPES`, or a bare `text`.
181
+ * A tool result or an image block contributes nothing, on purpose — a memory
182
+ * made of tool output is the transcript dump this repo refuses.
140
183
  */
141
184
  function textOf(record) {
142
185
  if (!record)
143
186
  return "";
144
- const message = record["message"];
145
- const source = message && typeof message === "object" && !Array.isArray(message)
146
- ? message
147
- : record;
187
+ const source = messageOf(record) ?? record;
148
188
  const content = source["content"] ?? source["text"];
149
189
  if (typeof content === "string")
150
190
  return content.trim();
@@ -159,7 +199,9 @@ function textOf(record) {
159
199
  if (!block || typeof block !== "object")
160
200
  continue;
161
201
  const record_ = block;
162
- if (record_["type"] !== "text")
202
+ if (typeof record_["type"] !== "string")
203
+ continue;
204
+ if (!TEXT_BLOCK_TYPES.has(record_["type"]))
163
205
  continue;
164
206
  const text = record_["text"];
165
207
  if (typeof text === "string")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bli-cockpit/memory-mcp",
3
- "version": "0.1.6",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "description": "BLI Memory — an MCP server for the memory layer BLI owns (save, search, update, forget).",
6
6
  "type": "module",
@@ -28,7 +28,7 @@
28
28
  "start": "node dist/index.js"
29
29
  },
30
30
  "dependencies": {
31
- "@bli-cockpit/telemetry-core": "0.1.28",
31
+ "@bli-cockpit/telemetry-core": "0.1.29",
32
32
  "@modelcontextprotocol/sdk": "^1.29.0",
33
33
  "zod": "^4.3.6"
34
34
  },