@jinn-network/core 0.1.0

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 (111) hide show
  1. package/dist/canonical-json.d.ts +1 -0
  2. package/dist/canonical-json.js +67 -0
  3. package/dist/captured-task.d.ts +140 -0
  4. package/dist/captured-task.js +122 -0
  5. package/dist/contribution-store.d.ts +97 -0
  6. package/dist/contribution-store.js +770 -0
  7. package/dist/corpus-read/acquire.d.ts +39 -0
  8. package/dist/corpus-read/acquire.js +212 -0
  9. package/dist/corpus-read/cache.d.ts +14 -0
  10. package/dist/corpus-read/cache.js +17 -0
  11. package/dist/corpus-read/capture-meta.d.ts +16 -0
  12. package/dist/corpus-read/capture-meta.js +18 -0
  13. package/dist/corpus-read/create-corpus.d.ts +2 -0
  14. package/dist/corpus-read/create-corpus.js +97 -0
  15. package/dist/corpus-read/fetch-artifact.d.ts +21 -0
  16. package/dist/corpus-read/fetch-artifact.js +32 -0
  17. package/dist/corpus-read/fetch.d.ts +12 -0
  18. package/dist/corpus-read/fetch.js +24 -0
  19. package/dist/corpus-read/http-discovery.d.ts +9 -0
  20. package/dist/corpus-read/http-discovery.js +128 -0
  21. package/dist/corpus-read/index.d.ts +10 -0
  22. package/dist/corpus-read/index.js +10 -0
  23. package/dist/corpus-read/ipfs.d.ts +5 -0
  24. package/dist/corpus-read/ipfs.js +72 -0
  25. package/dist/corpus-read/route-resolver.d.ts +16 -0
  26. package/dist/corpus-read/route-resolver.js +19 -0
  27. package/dist/corpus-read/types.d.ts +177 -0
  28. package/dist/corpus-read/types.js +42 -0
  29. package/dist/envelope.d.ts +134 -0
  30. package/dist/envelope.js +162 -0
  31. package/dist/evidence-adapter.d.ts +26 -0
  32. package/dist/evidence-adapter.js +321 -0
  33. package/dist/evidence-filesystem.d.ts +40 -0
  34. package/dist/evidence-filesystem.js +267 -0
  35. package/dist/evidence-index.d.ts +117 -0
  36. package/dist/evidence-index.js +1083 -0
  37. package/dist/evidence-store-lock.d.ts +2 -0
  38. package/dist/evidence-store-lock.js +208 -0
  39. package/dist/execution-envelope.d.ts +3142 -0
  40. package/dist/execution-envelope.js +203 -0
  41. package/dist/index.d.ts +12 -0
  42. package/dist/index.js +12 -0
  43. package/dist/manifest.d.ts +75 -0
  44. package/dist/manifest.js +184 -0
  45. package/dist/paired.d.ts +68 -0
  46. package/dist/paired.js +92 -0
  47. package/dist/scrub/build.d.ts +52 -0
  48. package/dist/scrub/build.js +84 -0
  49. package/dist/scrub/emit-scrub.d.ts +41 -0
  50. package/dist/scrub/emit-scrub.js +87 -0
  51. package/dist/scrub/index.d.ts +12 -0
  52. package/dist/scrub/index.js +12 -0
  53. package/dist/scrub/key-policy.d.ts +20 -0
  54. package/dist/scrub/key-policy.js +42 -0
  55. package/dist/scrub/layer2.d.ts +23 -0
  56. package/dist/scrub/layer2.js +32 -0
  57. package/dist/scrub/ml-pii-stage.d.ts +24 -0
  58. package/dist/scrub/ml-pii-stage.js +42 -0
  59. package/dist/scrub/openredaction-stage.d.ts +196 -0
  60. package/dist/scrub/openredaction-stage.js +270 -0
  61. package/dist/scrub/pii-build.d.ts +38 -0
  62. package/dist/scrub/pii-build.js +67 -0
  63. package/dist/scrub/pipeline.d.ts +38 -0
  64. package/dist/scrub/pipeline.js +83 -0
  65. package/dist/scrub/plain-patterns-stage.d.ts +31 -0
  66. package/dist/scrub/plain-patterns-stage.js +84 -0
  67. package/dist/scrub/secretlint-stage.d.ts +36 -0
  68. package/dist/scrub/secretlint-stage.js +249 -0
  69. package/dist/scrub/transformers-detector.d.ts +35 -0
  70. package/dist/scrub/transformers-detector.js +67 -0
  71. package/dist/scrub/types.d.ts +25 -0
  72. package/dist/scrub/types.js +1 -0
  73. package/dist/session-provenance.d.ts +75 -0
  74. package/dist/session-provenance.js +20 -0
  75. package/dist/skill-artifact.d.ts +442 -0
  76. package/dist/skill-artifact.js +136 -0
  77. package/dist/trajectory/hash-chain.d.ts +18 -0
  78. package/dist/trajectory/hash-chain.js +47 -0
  79. package/dist/trajectory/index.d.ts +4 -0
  80. package/dist/trajectory/index.js +4 -0
  81. package/dist/trajectory/schema.d.ts +606 -0
  82. package/dist/trajectory/schema.js +110 -0
  83. package/dist/trajectory/transcript-parsers/aider-history.d.ts +42 -0
  84. package/dist/trajectory/transcript-parsers/aider-history.js +181 -0
  85. package/dist/trajectory/transcript-parsers/claude-code-jsonl.d.ts +37 -0
  86. package/dist/trajectory/transcript-parsers/claude-code-jsonl.js +142 -0
  87. package/dist/trajectory/transcript-parsers/codex-session.d.ts +42 -0
  88. package/dist/trajectory/transcript-parsers/codex-session.js +235 -0
  89. package/dist/trajectory/transcript-parsers/continue-devdata.d.ts +41 -0
  90. package/dist/trajectory/transcript-parsers/continue-devdata.js +190 -0
  91. package/dist/trajectory/transcript-parsers/cursor-sqlite.d.ts +38 -0
  92. package/dist/trajectory/transcript-parsers/cursor-sqlite.js +118 -0
  93. package/dist/trajectory/transcript-parsers/gemini-session.d.ts +35 -0
  94. package/dist/trajectory/transcript-parsers/gemini-session.js +137 -0
  95. package/dist/trajectory/transcript-parsers/index.d.ts +7 -0
  96. package/dist/trajectory/transcript-parsers/index.js +7 -0
  97. package/dist/trajectory/transcript-parsers/types.d.ts +101 -0
  98. package/dist/trajectory/transcript-parsers/types.js +32 -0
  99. package/dist/trajectory/transcript-to-spans/attrs.d.ts +4 -0
  100. package/dist/trajectory/transcript-to-spans/attrs.js +30 -0
  101. package/dist/trajectory/transcript-to-spans/claude-code-stream-json.d.ts +8 -0
  102. package/dist/trajectory/transcript-to-spans/claude-code-stream-json.js +162 -0
  103. package/dist/trajectory/transcript-to-spans/codex-exec-json.d.ts +8 -0
  104. package/dist/trajectory/transcript-to-spans/codex-exec-json.js +253 -0
  105. package/dist/trajectory/transcript-to-spans/index.d.ts +4 -0
  106. package/dist/trajectory/transcript-to-spans/index.js +4 -0
  107. package/dist/trajectory/transcript-to-spans/types.d.ts +18 -0
  108. package/dist/trajectory/transcript-to-spans/types.js +1 -0
  109. package/dist/window.d.ts +12 -0
  110. package/dist/window.js +5 -0
  111. package/package.json +67 -0
@@ -0,0 +1,110 @@
1
+ /**
2
+ * jinn.trajectory.v1 — OTLP-JSON-shaped trace blob signed and uploaded once
3
+ * per run. Scope: docs/superpowers/specs/2026-04-23-jinn-execution-envelope-tee-scope.md
4
+ * §3.1 trajectory row + K6 span profile, §4.3 trajectory profile deliverable.
5
+ *
6
+ * Each span carries jinn.prevSpanHash (in-run hash chain) + jinn.span.kind
7
+ * (normative profile). Secret-scrub (§4.3 V1 minimum) produces a run-level
8
+ * redactionManifest signed alongside the spans.
9
+ */
10
+ import { z } from 'zod/v3';
11
+ const HexStringSchema = z.string().regex(/^0x[0-9a-fA-F]*$/);
12
+ export const JinnSpanKindSchema = z.enum([
13
+ 'jinn.phase',
14
+ 'jinn.llm_call',
15
+ 'jinn.mcp_call',
16
+ 'jinn.artifact.emit',
17
+ 'jinn.venue_io',
18
+ 'jinn.state_transition',
19
+ 'jinn.agent_turn',
20
+ 'jinn.tool_call',
21
+ ]);
22
+ const EventSchema = z.object({
23
+ timeUnixNano: z.string(),
24
+ name: z.string(),
25
+ attributes: z.record(z.unknown()).optional(),
26
+ });
27
+ const SpanStatusSchema = z.object({
28
+ code: z.enum(['UNSET', 'OK', 'ERROR']),
29
+ message: z.string().optional(),
30
+ });
31
+ /** An OTLP-shaped span with Jinn-required attributes. */
32
+ export const SpanSchema = z.object({
33
+ traceId: z.string().regex(/^[0-9a-f]{32}$/),
34
+ spanId: z.string().regex(/^[0-9a-f]{16}$/),
35
+ parentSpanId: z.string().regex(/^[0-9a-f]{16}$/).nullable(),
36
+ name: z.string().min(1),
37
+ kind: z.enum(['INTERNAL', 'CLIENT', 'SERVER', 'PRODUCER', 'CONSUMER']),
38
+ startTimeUnixNano: z.string(),
39
+ endTimeUnixNano: z.string(),
40
+ attributes: z
41
+ .record(z.unknown())
42
+ .refine((a) => typeof a['jinn.span.kind'] === 'string', {
43
+ message: 'jinn.span.kind attribute required',
44
+ })
45
+ .refine((a) => typeof a['jinn.prevSpanHash'] === 'string', {
46
+ message: 'jinn.prevSpanHash attribute required',
47
+ }),
48
+ events: z.array(EventSchema),
49
+ status: SpanStatusSchema,
50
+ });
51
+ export const RedactionManifestSchema = z
52
+ .object({
53
+ spans: z.array(z.object({
54
+ spanId: z.string().regex(/^[0-9a-f]{16}$/),
55
+ redactedKeys: z.array(z.string()),
56
+ })),
57
+ totalRedactions: z.number().int().nonnegative(),
58
+ })
59
+ .refine((m) => m.spans.reduce((acc, s) => acc + s.redactedKeys.length, 0) === m.totalRedactions, { message: 'totalRedactions must equal sum of per-span redactedKeys' });
60
+ const SignatureSchema = z.object({
61
+ algo: z.literal('secp256k1'),
62
+ signer: HexStringSchema,
63
+ hash: HexStringSchema,
64
+ sig: HexStringSchema,
65
+ });
66
+ export const JinnTrajectoryV1Schema = z.object({
67
+ schemaVersion: z.literal('jinn.trajectory.v1'),
68
+ runId: z.string().min(1),
69
+ parentEnvelopeCid: z.string().nullable(),
70
+ spans: z.array(SpanSchema),
71
+ redactionManifest: RedactionManifestSchema,
72
+ signature: SignatureSchema,
73
+ });
74
+ /** Unsigned form — what we hash + sign. */
75
+ export const UnsignedTrajectorySchema = JinnTrajectoryV1Schema.omit({ signature: true });
76
+ /**
77
+ * Per-run capture manifest — records the operator's authorisation and
78
+ * coarse harness-bundle metadata for telemetry capture. v0 deliberately
79
+ * keeps operator control at the bundle level (no per-file curation) per
80
+ * DR-2026-05-07-g; the audit surface is "what did the operator authorise
81
+ * the daemon to read?" not "what did the daemon ultimately read?"
82
+ *
83
+ * Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §3.3
84
+ */
85
+ export const CaptureManifestSchema = z.object({
86
+ scrubProcessors: z.array(z.object({
87
+ name: z.string(),
88
+ version: z.string(),
89
+ config: z.record(z.unknown()).optional(),
90
+ })),
91
+ reviewedBy: z.object({
92
+ safeAddress: z.string(),
93
+ reviewedAt: z.string().datetime(),
94
+ }),
95
+ trustedRepoToggle: z.boolean(),
96
+ harnessBundle: z.object({
97
+ included: z.boolean(),
98
+ sha256: z.string().regex(/^[0-9a-f]{64}$/),
99
+ allowedDirectoriesHash: z.string().regex(/^[0-9a-f]{64}$/),
100
+ capturePath: z.enum(['A', 'B', 'C', 'D']),
101
+ }),
102
+ /**
103
+ * The single `share` consent (mono#1714): may a mined task be published off
104
+ * the box (enforced at the mint publish gate). Local retention is
105
+ * unconditional, so there is no separate retention-consent field.
106
+ */
107
+ publishMinedTasksConsent: z.boolean().optional().default(false),
108
+ });
109
+ /** sha256 of an empty (no-files) bundle. Used when the operator opts out of harness-bundle capture. */
110
+ export const EMPTY_BUNDLE_SHA256 = 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Aider history parser.
3
+ *
4
+ * Aider records two transcript flavours:
5
+ *
6
+ * 1. `.aider.analytics.log.jsonl` (preferred; emitted when --analytics-log
7
+ * is enabled). One JSON record per line. Common events:
8
+ * - { event: 'message_send', role: 'user', content } → user-message
9
+ * - { event: 'model_response', role: 'assistant', content } → assistant-message
10
+ * - { event: 'edit', path, diff } → edit
11
+ *
12
+ * 2. `.aider.chat.history.md` (fallback; always emitted by Aider). A
13
+ * timestamped markdown log:
14
+ * # 2026-05-07 00:00:00 +0000
15
+ *
16
+ * > user message
17
+ *
18
+ * assistant reply text
19
+ *
20
+ * The parser dispatches on file extension: `.jsonl` → analytics path; `.md`
21
+ * (or anything else) → markdown path. Both produce canonical TranscriptEvents.
22
+ *
23
+ * For the JSONL path, `parseChunk` follows the streaming pattern (preserves
24
+ * any incomplete trailing line). For the markdown path, `parseChunk` is a
25
+ * no-op — markdown is not line-stable, so the watcher polls and replays
26
+ * `parseFull`.
27
+ *
28
+ * Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
29
+ */
30
+ import type { TranscriptEvent, TranscriptParser } from './types.js';
31
+ export declare class AiderHistoryParser implements TranscriptParser {
32
+ readonly tool: "aider";
33
+ private buffer;
34
+ parseChunk(input: {
35
+ sessionId: string;
36
+ chunk: Buffer | string;
37
+ }): TranscriptEvent[];
38
+ parseFull(input: {
39
+ sessionId: string;
40
+ path: string;
41
+ }): Promise<TranscriptEvent[]>;
42
+ }
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Aider history parser.
3
+ *
4
+ * Aider records two transcript flavours:
5
+ *
6
+ * 1. `.aider.analytics.log.jsonl` (preferred; emitted when --analytics-log
7
+ * is enabled). One JSON record per line. Common events:
8
+ * - { event: 'message_send', role: 'user', content } → user-message
9
+ * - { event: 'model_response', role: 'assistant', content } → assistant-message
10
+ * - { event: 'edit', path, diff } → edit
11
+ *
12
+ * 2. `.aider.chat.history.md` (fallback; always emitted by Aider). A
13
+ * timestamped markdown log:
14
+ * # 2026-05-07 00:00:00 +0000
15
+ *
16
+ * > user message
17
+ *
18
+ * assistant reply text
19
+ *
20
+ * The parser dispatches on file extension: `.jsonl` → analytics path; `.md`
21
+ * (or anything else) → markdown path. Both produce canonical TranscriptEvents.
22
+ *
23
+ * For the JSONL path, `parseChunk` follows the streaming pattern (preserves
24
+ * any incomplete trailing line). For the markdown path, `parseChunk` is a
25
+ * no-op — markdown is not line-stable, so the watcher polls and replays
26
+ * `parseFull`.
27
+ *
28
+ * Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
29
+ */
30
+ import { readFile } from 'node:fs/promises';
31
+ export class AiderHistoryParser {
32
+ tool = 'aider';
33
+ buffer = '';
34
+ parseChunk(input) {
35
+ // parseChunk is the streaming-friendly path; only meaningful for the
36
+ // JSONL analytics format. Callers wiring the markdown fallback through
37
+ // a watcher poll the file and re-run parseFull.
38
+ const text = typeof input.chunk === 'string' ? input.chunk : input.chunk.toString('utf-8');
39
+ this.buffer += text;
40
+ const lines = this.buffer.split('\n');
41
+ this.buffer = lines.pop() ?? '';
42
+ const events = [];
43
+ for (const line of lines) {
44
+ if (!line.trim())
45
+ continue;
46
+ let record;
47
+ try {
48
+ record = JSON.parse(line);
49
+ }
50
+ catch {
51
+ continue;
52
+ }
53
+ const event = analyticsRecordToEvent(record);
54
+ if (event)
55
+ events.push(event);
56
+ }
57
+ return events;
58
+ }
59
+ async parseFull(input) {
60
+ const lower = input.path.toLowerCase();
61
+ if (lower.endsWith('.jsonl')) {
62
+ const text = await readFile(input.path, 'utf-8');
63
+ const normalised = text.endsWith('\n') ? text : text + '\n';
64
+ const previousBuffer = this.buffer;
65
+ this.buffer = '';
66
+ try {
67
+ return this.parseChunk({ sessionId: input.sessionId, chunk: normalised });
68
+ }
69
+ finally {
70
+ this.buffer = previousBuffer;
71
+ }
72
+ }
73
+ // Markdown fallback (.md or otherwise).
74
+ const text = await readFile(input.path, 'utf-8');
75
+ return parseMarkdownHistory(text);
76
+ }
77
+ }
78
+ function analyticsRecordToEvent(record) {
79
+ if (!record || typeof record !== 'object')
80
+ return null;
81
+ const ts = record.ts ?? record.timestamp;
82
+ if (typeof ts !== 'string')
83
+ return null;
84
+ const event = record.event;
85
+ if (event === 'message_send' || record.role === 'user') {
86
+ if (typeof record.content !== 'string')
87
+ return null;
88
+ return { kind: 'user-message', timestamp: ts, content: record.content };
89
+ }
90
+ if (event === 'model_response' || record.role === 'assistant') {
91
+ if (typeof record.content !== 'string')
92
+ return null;
93
+ return { kind: 'assistant-message', timestamp: ts, content: record.content };
94
+ }
95
+ if (event === 'edit') {
96
+ if (typeof record.path !== 'string' || typeof record.diff !== 'string')
97
+ return null;
98
+ return { kind: 'edit', timestamp: ts, path: record.path, diff: record.diff };
99
+ }
100
+ return null;
101
+ }
102
+ /**
103
+ * Best-effort markdown chat-history parser. The format is human-friendly and
104
+ * not designed for round-tripping; we split on date headings (`# YYYY-MM-DD ...`)
105
+ * and within each block treat lines starting with `> ` as user messages and
106
+ * the remaining non-empty text as the assistant reply.
107
+ */
108
+ function parseMarkdownHistory(text) {
109
+ const events = [];
110
+ const lines = text.split('\n');
111
+ let currentTs = null;
112
+ let userBuf = [];
113
+ let assistantBuf = [];
114
+ let mode = 'idle';
115
+ const flush = () => {
116
+ if (!currentTs) {
117
+ userBuf = [];
118
+ assistantBuf = [];
119
+ mode = 'idle';
120
+ return;
121
+ }
122
+ if (userBuf.length > 0) {
123
+ const content = userBuf.join('\n').trim();
124
+ if (content) {
125
+ events.push({ kind: 'user-message', timestamp: currentTs, content });
126
+ }
127
+ }
128
+ if (assistantBuf.length > 0) {
129
+ const content = assistantBuf.join('\n').trim();
130
+ if (content) {
131
+ events.push({ kind: 'assistant-message', timestamp: currentTs, content });
132
+ }
133
+ }
134
+ userBuf = [];
135
+ assistantBuf = [];
136
+ mode = 'idle';
137
+ };
138
+ for (const line of lines) {
139
+ const headingMatch = /^#\s+(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})/.exec(line);
140
+ if (headingMatch) {
141
+ flush();
142
+ // Normalise to ISO 8601 UTC. The header includes a tz offset which we
143
+ // drop here for v0; downstream ingest only needs a parseable timestamp.
144
+ currentTs = `${headingMatch[1]}T${headingMatch[2]}.000Z`;
145
+ continue;
146
+ }
147
+ if (line.startsWith('> ')) {
148
+ // Switch into user-message mode. If we were previously in assistant
149
+ // mode, flush the assistant buffer as the previous turn's reply.
150
+ if (mode === 'assistant') {
151
+ if (currentTs) {
152
+ const content = assistantBuf.join('\n').trim();
153
+ if (content) {
154
+ events.push({ kind: 'assistant-message', timestamp: currentTs, content });
155
+ }
156
+ }
157
+ assistantBuf = [];
158
+ }
159
+ mode = 'user';
160
+ userBuf.push(line.slice(2));
161
+ continue;
162
+ }
163
+ if (line.startsWith('>')) {
164
+ // Continuation of a user message ("> ").
165
+ if (mode === 'user')
166
+ userBuf.push(line.slice(1));
167
+ continue;
168
+ }
169
+ if (mode === 'user' && line.trim() === '') {
170
+ // Blank line ends the user block; assistant text (if any) follows.
171
+ mode = 'assistant';
172
+ continue;
173
+ }
174
+ if (mode === 'assistant' || (mode === 'idle' && currentTs && line.trim() !== '')) {
175
+ mode = 'assistant';
176
+ assistantBuf.push(line);
177
+ }
178
+ }
179
+ flush();
180
+ return events;
181
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Claude Code JSONL transcript parser.
3
+ *
4
+ * Reads `~/.claude/projects/<project>/<session-id>.jsonl` line-by-line, mapping
5
+ * per-record content to canonical TranscriptEvent shapes (Task 3.1):
6
+ * - user record with string content → user-message
7
+ * - user record with tool_result content blocks → tool-result
8
+ * - assistant record with text content blocks → assistant-message
9
+ * - assistant record with tool_use content blocks → tool-call
10
+ *
11
+ * `parseChunk` preserves any incomplete trailing line across calls so the
12
+ * watcher (Task 3.9) can stream tail bytes without losing partial records.
13
+ * `parseFull` is a one-shot for `jinn capture import` and end-of-session
14
+ * safety-net ingest.
15
+ *
16
+ * Known v0 limitation: `tool-result.name` is filled from `tool_use_id` (the
17
+ * reference back to the originating call) rather than the actual tool name —
18
+ * resolving the name requires remembering each `tool_use` block's name across
19
+ * the stream. A follow-up will add that resolution map; for now, downstream
20
+ * consumers see a stable identifier even if it carries less display signal.
21
+ *
22
+ * Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
23
+ */
24
+ import type { TranscriptEvent, TranscriptParser } from './types.js';
25
+ export declare class ClaudeCodeJsonlParser implements TranscriptParser {
26
+ readonly tool: "claude-code";
27
+ private buffer;
28
+ parseChunk(input: {
29
+ sessionId: string;
30
+ chunk: Buffer | string;
31
+ }): TranscriptEvent[];
32
+ parseFull(input: {
33
+ sessionId: string;
34
+ path: string;
35
+ }): Promise<TranscriptEvent[]>;
36
+ private recordToEvents;
37
+ }
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Claude Code JSONL transcript parser.
3
+ *
4
+ * Reads `~/.claude/projects/<project>/<session-id>.jsonl` line-by-line, mapping
5
+ * per-record content to canonical TranscriptEvent shapes (Task 3.1):
6
+ * - user record with string content → user-message
7
+ * - user record with tool_result content blocks → tool-result
8
+ * - assistant record with text content blocks → assistant-message
9
+ * - assistant record with tool_use content blocks → tool-call
10
+ *
11
+ * `parseChunk` preserves any incomplete trailing line across calls so the
12
+ * watcher (Task 3.9) can stream tail bytes without losing partial records.
13
+ * `parseFull` is a one-shot for `jinn capture import` and end-of-session
14
+ * safety-net ingest.
15
+ *
16
+ * Known v0 limitation: `tool-result.name` is filled from `tool_use_id` (the
17
+ * reference back to the originating call) rather than the actual tool name —
18
+ * resolving the name requires remembering each `tool_use` block's name across
19
+ * the stream. A follow-up will add that resolution map; for now, downstream
20
+ * consumers see a stable identifier even if it carries less display signal.
21
+ *
22
+ * Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
23
+ */
24
+ import { readFile } from 'node:fs/promises';
25
+ export class ClaudeCodeJsonlParser {
26
+ tool = 'claude-code';
27
+ buffer = '';
28
+ parseChunk(input) {
29
+ const text = typeof input.chunk === 'string' ? input.chunk : input.chunk.toString('utf-8');
30
+ this.buffer += text;
31
+ const lines = this.buffer.split('\n');
32
+ // Last fragment (no trailing newline) is preserved for the next chunk.
33
+ this.buffer = lines.pop() ?? '';
34
+ const events = [];
35
+ for (const line of lines) {
36
+ if (!line.trim())
37
+ continue;
38
+ let record;
39
+ try {
40
+ record = JSON.parse(line);
41
+ }
42
+ catch {
43
+ // Malformed line — skip silently. The watcher cannot recover bytes
44
+ // we drop, but a single corrupt line should not poison the stream.
45
+ continue;
46
+ }
47
+ events.push(...this.recordToEvents(record));
48
+ }
49
+ return events;
50
+ }
51
+ async parseFull(input) {
52
+ const text = await readFile(input.path, 'utf-8');
53
+ // Ensure the final record has a terminating newline so parseChunk drains it.
54
+ const normalised = text.endsWith('\n') ? text : text + '\n';
55
+ // parseFull is a one-shot. Use a fresh parser-like state to avoid leaking
56
+ // the buffer if a caller reuses the same instance for streaming + full.
57
+ const previousBuffer = this.buffer;
58
+ this.buffer = '';
59
+ try {
60
+ return this.parseChunk({ sessionId: input.sessionId, chunk: normalised });
61
+ }
62
+ finally {
63
+ this.buffer = previousBuffer;
64
+ }
65
+ }
66
+ recordToEvents(record) {
67
+ if (!record || typeof record.timestamp !== 'string')
68
+ return [];
69
+ const ts = record.timestamp;
70
+ if (record.type === 'user') {
71
+ const content = record.message?.content;
72
+ // String content is the canonical user message shape.
73
+ if (typeof content === 'string') {
74
+ return [{ kind: 'user-message', timestamp: ts, content }];
75
+ }
76
+ // Array content typically carries tool_result blocks (Claude Code emits
77
+ // tool responses inside a synthetic user turn). Other block types in a
78
+ // user-array record are skipped.
79
+ if (Array.isArray(content)) {
80
+ const out = [];
81
+ for (const block of content) {
82
+ if (isToolResultBlock(block)) {
83
+ const resultText = typeof block.content === 'string'
84
+ ? block.content
85
+ : JSON.stringify(block.content);
86
+ out.push({
87
+ kind: 'tool-result',
88
+ timestamp: ts,
89
+ // v0: use the reference id as `name`. Resolution-map upgrade
90
+ // pending — see file header.
91
+ name: block.tool_use_id,
92
+ content: resultText,
93
+ isError: block.is_error ?? false,
94
+ });
95
+ }
96
+ }
97
+ return out;
98
+ }
99
+ return [];
100
+ }
101
+ if (record.type === 'assistant') {
102
+ const content = record.message?.content;
103
+ const blocks = Array.isArray(content)
104
+ ? content
105
+ : typeof content === 'string'
106
+ ? [{ type: 'text', text: content }]
107
+ : [];
108
+ const out = [];
109
+ for (const block of blocks) {
110
+ if (isTextBlock(block)) {
111
+ out.push({ kind: 'assistant-message', timestamp: ts, content: block.text });
112
+ }
113
+ else if (isToolUseBlock(block)) {
114
+ out.push({
115
+ kind: 'tool-call',
116
+ timestamp: ts,
117
+ name: block.name,
118
+ args: block.input ?? {},
119
+ });
120
+ }
121
+ // `thinking` and any other unrecognised block types are intentionally
122
+ // dropped — the canonical TranscriptEvent shape doesn't carry them and
123
+ // including them would violate downstream identity-scrub assumptions.
124
+ }
125
+ return out;
126
+ }
127
+ // Records with unknown `type` (e.g. queue-operation, summary) are dropped.
128
+ return [];
129
+ }
130
+ }
131
+ function isTextBlock(block) {
132
+ return block.type === 'text' && typeof block.text === 'string';
133
+ }
134
+ function isToolUseBlock(block) {
135
+ return (block.type === 'tool_use' &&
136
+ typeof block.name === 'string' &&
137
+ typeof block.id === 'string');
138
+ }
139
+ function isToolResultBlock(block) {
140
+ return (block.type === 'tool_result' &&
141
+ typeof block.tool_use_id === 'string');
142
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Codex CLI session transcript parser.
3
+ *
4
+ * Reads `~/.codex/sessions/<id>.jsonl` line-by-line, mapping per-record content
5
+ * to canonical TranscriptEvent shapes (Task 3.1):
6
+ * - role: 'user' with string content → user-message
7
+ * - role: 'assistant' with string content → assistant-message
8
+ * - role: 'function' with function_call → tool-call (also: assistant with
9
+ * a top-level function_call field)
10
+ * - role: 'tool' with content → tool-result
11
+ *
12
+ * Codex CLI 0.129.0+ wraps each line as `{ timestamp, type, payload }` where
13
+ * `type` is `session_meta`, `turn_context`, `response_item`, or `event_msg`.
14
+ * Tool calls and messages live inside `payload` (e.g. `function_call`,
15
+ * `function_call_output`, `message` with `input_text` / `output_text` blocks).
16
+ * Older sessions use a flat per-line shape with `role` at the top level; both
17
+ * are detected by record shape.
18
+ *
19
+ * `parseChunk` preserves any incomplete trailing line across calls so the
20
+ * watcher (Task 3.9) can stream tail bytes without losing partial records.
21
+ *
22
+ * Spec: spec/2026-05-07-telemetry-collector-and-task-generator.md §4.2 path B.
23
+ */
24
+ import type { TranscriptEvent, TranscriptParser } from './types.js';
25
+ export declare class CodexSessionParser implements TranscriptParser {
26
+ readonly tool: "codex";
27
+ private buffer;
28
+ /** Maps function_call `call_id` → tool name for function_call_output rows. */
29
+ private callNames;
30
+ parseChunk(input: {
31
+ sessionId: string;
32
+ chunk: Buffer | string;
33
+ }): TranscriptEvent[];
34
+ parseFull(input: {
35
+ sessionId: string;
36
+ path: string;
37
+ }): Promise<TranscriptEvent[]>;
38
+ private lineToEvents;
39
+ private wrappedEnvelopeToEvents;
40
+ private responseItemToEvents;
41
+ private recordToEvents;
42
+ }