@ccmsg/cli 0.2.13 → 0.3.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.
@@ -133,6 +133,11 @@ export class TranscriptFold {
133
133
  return false;
134
134
  }
135
135
  if (!isRecord(row)) return false;
136
+ // A Codex rollout line settles one of these facts and none of the others,
137
+ // so it is folded on its own rather than run past readers of records it
138
+ // does not have (§3.8).
139
+ const rollout = rolloutRecord(row, str(row["type"]));
140
+ if (rollout !== undefined) return this.#foldRollout(rollout);
136
141
  // Every value this fold derives, derived from the one parse (M5).
137
142
  let changed = this.#foldApiError(row);
138
143
  if (this.#foldAnswered(row)) changed = true;
@@ -223,6 +228,20 @@ export class TranscriptFold {
223
228
  *
224
229
  * A sidechain user row is a subagent being prompted by its parent, which is
225
230
  * a session speaking to itself rather than a person speaking to it. */
231
+ /** What a Codex rollout says: when a person last spoke.
232
+ *
233
+ * The rest of what this fold holds — the api error, the model of the latest
234
+ * turn, todos, teammates, background work — are records Claude Code writes
235
+ * and a rollout does not, so they stay as they are for a Codex session
236
+ * rather than being guessed at from something that resembles them. */
237
+ #foldRollout(record: TranscriptRecord): boolean {
238
+ if (record.said_by !== "user" || record.said_at === undefined) return false;
239
+ if (record.text === undefined || !isHuman(record.text)) return false;
240
+ if ((this.#lastUserInputAt ?? 0) >= record.said_at) return false;
241
+ this.#lastUserInputAt = record.said_at;
242
+ return true;
243
+ }
244
+
226
245
  #foldUserInput(row: Record<string, unknown>): boolean {
227
246
  if (row["type"] !== "user" || row["isSidechain"] === true) return false;
228
247
  if (row["isMeta"] === true || row["promptSource"] === "system") return false;
@@ -744,6 +763,8 @@ export function readRecord(line: string): TranscriptRecord | undefined {
744
763
  }
745
764
  if (!isRecord(row)) return undefined;
746
765
  const type = str(row["type"]);
766
+ const rollout = rolloutRecord(row, type);
767
+ if (rollout !== undefined) return rollout;
747
768
  const message = isRecord(row["message"]) ? row["message"] : undefined;
748
769
  const model = message === undefined ? undefined : str(message["model"]);
749
770
  return {
@@ -760,6 +781,52 @@ export function readRecord(line: string): TranscriptRecord | undefined {
760
781
  };
761
782
  }
762
783
 
784
+ /** One line of a Codex rollout, or nothing where the line is not one.
785
+ *
786
+ * A rollout says what kind of line it is in its own `type`, and the words it
787
+ * uses appear in no Claude Code transcript — so the two formats are told apart
788
+ * by the line rather than by anything the reader was told beforehand.
789
+ *
790
+ * What is read is what §5 asks a transcript for and a rollout answers: when a
791
+ * person last spoke, and where the session runs. The rest of the fold's facts —
792
+ * a session's todos, its teammates, the files it named — are Claude Code's own
793
+ * records, and a Codex session simply declares none of them.
794
+ *
795
+ * `developer` is not a person: Codex writes the instructions a turn runs under
796
+ * as messages of that role, so only `user` and `assistant` are read as somebody
797
+ * speaking. One of the `user` rows is not a person either — Codex opens a
798
+ * thread by stating the environment as `<environment_context>` — and it is the
799
+ * fold that turns that away, by the same rule that turns away every injected
800
+ * opening a Claude Code transcript carries: a row that opens with a tag is the
801
+ * harness talking. So the role decides who is read here, and what is read
802
+ * decides whether a person said it. */
803
+ function rolloutRecord(
804
+ row: Record<string, unknown>,
805
+ type: string | undefined,
806
+ ): TranscriptRecord | undefined {
807
+ if (type === "session_meta") {
808
+ const payload = isRecord(row["payload"]) ? row["payload"] : undefined;
809
+ return {
810
+ sidechain: false,
811
+ ...optional("said_at", instant(row["timestamp"])),
812
+ ...optional("cwd", payload === undefined ? undefined : str(payload["cwd"])),
813
+ };
814
+ }
815
+ if (type !== "response_item") return undefined;
816
+ const payload = isRecord(row["payload"]) ? row["payload"] : undefined;
817
+ if (payload === undefined || str(payload["type"]) !== "message") return { sidechain: false };
818
+ const role = str(payload["role"]);
819
+ return {
820
+ sidechain: false,
821
+ ...optional("said_at", instant(row["timestamp"])),
822
+ ...optional<"said_by", "user" | "agent">(
823
+ "said_by",
824
+ role === "user" ? "user" : role === "assistant" ? "agent" : undefined,
825
+ ),
826
+ ...optional("text", blockText(payload["content"])),
827
+ };
828
+ }
829
+
763
830
  /** The harness's record types, in the contract's two words. Its `assistant` is
764
831
  * the contract's `agent`; every other type is a record neither side spoke. */
765
832
  function saidBy(type: string | undefined): "user" | "agent" | undefined {
@@ -771,6 +838,8 @@ function optional<K extends string, V>(key: K, value: V | undefined): Record<K,
771
838
  return value === undefined ? {} : { [key]: value };
772
839
  }
773
840
 
841
+ const TEXT_BLOCK = new Set(["text", "input_text", "output_text"]);
842
+
774
843
  /** The text a message states, whoever wrote it. A plain prompt is a string; a
775
844
  * prompt with an attachment, and every row the harness writes, is a block
776
845
  * array whose text blocks carry the words. An array holding only tool results
@@ -781,7 +850,9 @@ function blockText(content: unknown): string | undefined {
781
850
  if (!Array.isArray(content)) return undefined;
782
851
  const parts: string[] = [];
783
852
  for (const block of content) {
784
- if (!isRecord(block) || block["type"] !== "text") continue;
853
+ // `text` is what Claude Code writes; `input_text` and `output_text` are
854
+ // what a Codex rollout writes for the same thing, one per direction.
855
+ if (!isRecord(block) || !TEXT_BLOCK.has(block["type"] as string)) continue;
785
856
  const text = str(block["text"]);
786
857
  if (text !== undefined) parts.push(text);
787
858
  }