@okrapdf/cli 0.3.2 → 0.3.4

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 (48) hide show
  1. package/dist/cli.test.js +178 -11
  2. package/dist/cli.test.js.map +1 -1
  3. package/dist/commands/chat-schema-output.test.d.ts +2 -0
  4. package/dist/commands/chat-schema-output.test.d.ts.map +1 -0
  5. package/dist/commands/chat-schema-output.test.js +151 -0
  6. package/dist/commands/chat-schema-output.test.js.map +1 -0
  7. package/dist/commands/chat.d.ts.map +1 -1
  8. package/dist/commands/chat.js +268 -12
  9. package/dist/commands/chat.js.map +1 -1
  10. package/dist/commands/chat.test.js +178 -3
  11. package/dist/commands/chat.test.js.map +1 -1
  12. package/dist/commands/jobs.d.ts.map +1 -1
  13. package/dist/commands/jobs.js +4 -3
  14. package/dist/commands/jobs.js.map +1 -1
  15. package/dist/index.js +0 -0
  16. package/dist/lib/agent-renderer.d.ts +12 -0
  17. package/dist/lib/agent-renderer.d.ts.map +1 -1
  18. package/dist/lib/agent-renderer.js +28 -0
  19. package/dist/lib/agent-renderer.js.map +1 -1
  20. package/dist/lib/client.d.ts +1 -0
  21. package/dist/lib/client.d.ts.map +1 -1
  22. package/dist/lib/client.js +1 -0
  23. package/dist/lib/client.js.map +1 -1
  24. package/dist/lib/config.d.ts +8 -0
  25. package/dist/lib/config.d.ts.map +1 -1
  26. package/dist/lib/config.js +13 -0
  27. package/dist/lib/config.js.map +1 -1
  28. package/dist/lib/progress.d.ts +6 -2
  29. package/dist/lib/progress.d.ts.map +1 -1
  30. package/dist/lib/progress.js +5 -2
  31. package/dist/lib/progress.js.map +1 -1
  32. package/dist/lib/runtime.d.ts.map +1 -1
  33. package/dist/lib/runtime.js +5 -0
  34. package/dist/lib/runtime.js.map +1 -1
  35. package/dist/lib/structured-output.d.ts +32 -0
  36. package/dist/lib/structured-output.d.ts.map +1 -0
  37. package/dist/lib/structured-output.js +156 -0
  38. package/dist/lib/structured-output.js.map +1 -0
  39. package/dist/lib/structured-output.test.d.ts +2 -0
  40. package/dist/lib/structured-output.test.d.ts.map +1 -0
  41. package/dist/lib/structured-output.test.js +148 -0
  42. package/dist/lib/structured-output.test.js.map +1 -0
  43. package/dist/lib/system-prompt.d.ts +4 -0
  44. package/dist/lib/system-prompt.d.ts.map +1 -1
  45. package/dist/lib/system-prompt.js +8 -0
  46. package/dist/lib/system-prompt.js.map +1 -1
  47. package/dist/templates/replay-viewer.html +230 -0
  48. package/package.json +4 -3
@@ -0,0 +1,156 @@
1
+ /**
2
+ * Structured output: resolve schemas from built-in presets, inline JSON, or files.
3
+ * The resolved JSON Schema is sent to the Claude Agent SDK via output_format
4
+ * for constrained decoding — no prompt engineering needed.
5
+ */
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { z } from 'zod';
8
+ // ── Built-in schema presets ───────────────────────────────────────────
9
+ export const BUILTIN_SCHEMAS = {
10
+ summary: {
11
+ description: 'Document summary with title, description, and key points',
12
+ schema: z.object({
13
+ title: z.string(),
14
+ description: z.string(),
15
+ key_points: z.array(z.string()),
16
+ page_count: z.number(),
17
+ document_type: z.string().nullable().optional(),
18
+ }),
19
+ },
20
+ entities: {
21
+ description: 'People, organizations, dates, and monetary amounts found in the document',
22
+ schema: z.object({
23
+ people: z.array(z.string()),
24
+ organizations: z.array(z.string()),
25
+ dates: z.array(z.object({ date: z.string(), context: z.string() })),
26
+ amounts: z.array(z.object({
27
+ value: z.string(),
28
+ currency: z.string().nullable().optional(),
29
+ context: z.string(),
30
+ })),
31
+ }),
32
+ },
33
+ metadata: {
34
+ description: 'Document metadata including type, language, author, and structural info',
35
+ schema: z.object({
36
+ document_type: z.string(),
37
+ date: z.string().nullable().optional(),
38
+ language: z.string().nullable().optional(),
39
+ author: z.string().nullable().optional(),
40
+ page_count: z.number(),
41
+ has_tables: z.boolean(),
42
+ }),
43
+ },
44
+ tables: {
45
+ description: 'Table inventory with counts, page locations, and structure info',
46
+ schema: z.object({
47
+ table_count: z.number(),
48
+ tables: z.array(z.object({
49
+ page: z.number(),
50
+ description: z.string(),
51
+ row_count: z.number().nullable().optional(),
52
+ column_count: z.number().nullable().optional(),
53
+ key_headers: z.array(z.string()).nullable().optional(),
54
+ })),
55
+ }),
56
+ },
57
+ };
58
+ // ── Schema resolution ─────────────────────────────────────────────────
59
+ /**
60
+ * Resolve a --schema value to a JSON Schema.
61
+ *
62
+ * Accepts:
63
+ * - Built-in name: "summary", "entities", "metadata", "tables"
64
+ * - Inline JSON: '{"type":"object","properties":{"name":{"type":"string"}}}'
65
+ * - File path: "./my-schema.json" or "/abs/path/schema.json"
66
+ *
67
+ * Returns null + error message if invalid.
68
+ */
69
+ export function resolveSchema(input) {
70
+ // 1. Built-in preset
71
+ if (input in BUILTIN_SCHEMAS) {
72
+ const builtin = BUILTIN_SCHEMAS[input];
73
+ return {
74
+ schema: {
75
+ jsonSchema: z.toJSONSchema(builtin.schema, { target: 'draft-2020-12' }),
76
+ zodSchema: builtin.schema,
77
+ source: `built-in:${input}`,
78
+ },
79
+ };
80
+ }
81
+ // 2. Inline JSON (starts with '{')
82
+ if (input.startsWith('{')) {
83
+ try {
84
+ const parsed = JSON.parse(input);
85
+ if (!parsed.type) {
86
+ return { error: 'Inline JSON schema must have a "type" field' };
87
+ }
88
+ return {
89
+ schema: {
90
+ jsonSchema: parsed,
91
+ source: 'inline',
92
+ },
93
+ };
94
+ }
95
+ catch (e) {
96
+ return { error: `Invalid inline JSON: ${e instanceof Error ? e.message : e}` };
97
+ }
98
+ }
99
+ // 3. File path
100
+ if (existsSync(input)) {
101
+ try {
102
+ const raw = readFileSync(input, 'utf8');
103
+ const parsed = JSON.parse(raw);
104
+ if (!parsed.type) {
105
+ return { error: `Schema file ${input} must have a "type" field` };
106
+ }
107
+ return {
108
+ schema: {
109
+ jsonSchema: parsed,
110
+ source: `file:${input}`,
111
+ },
112
+ };
113
+ }
114
+ catch (e) {
115
+ return { error: `Failed to read schema file ${input}: ${e instanceof Error ? e.message : e}` };
116
+ }
117
+ }
118
+ // 4. Unknown
119
+ const presets = Object.keys(BUILTIN_SCHEMAS).join(', ');
120
+ return {
121
+ error: `Unknown schema "${input}". Use a built-in (${presets}), inline JSON, or a file path.`,
122
+ };
123
+ }
124
+ // ── JSON extraction (fallback for when SDK doesn't return structured_output) ──
125
+ export function extractJson(text) {
126
+ const trimmed = text.trim();
127
+ try {
128
+ return JSON.parse(trimmed);
129
+ }
130
+ catch {
131
+ // fall through
132
+ }
133
+ const match = trimmed.match(/\{[\s\S]*\}/);
134
+ if (match) {
135
+ try {
136
+ return JSON.parse(match[0]);
137
+ }
138
+ catch {
139
+ // fall through
140
+ }
141
+ }
142
+ return null;
143
+ }
144
+ // ── Prompt template (kept for system prompt hint) ─────────────────────
145
+ export function structuredOutputInstructions(schema) {
146
+ return `\n<structured-output>
147
+ RESPOND WITH ONLY A SINGLE JSON OBJECT matching the provided output_format schema.
148
+ No markdown fences, no explanations, no preamble.
149
+ Use null for unknown optional fields. Do NOT hallucinate values.
150
+ </structured-output>`;
151
+ }
152
+ // ── Helpers ───────────────────────────────────────────────────────────
153
+ export function listBuiltinNames() {
154
+ return Object.keys(BUILTIN_SCHEMAS);
155
+ }
156
+ //# sourceMappingURL=structured-output.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-output.js","sourceRoot":"","sources":["../../src/lib/structured-output.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=structured-output.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-output.test.d.ts","sourceRoot":"","sources":["../../src/lib/structured-output.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,148 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractJson, resolveSchema, listBuiltinNames, structuredOutputInstructions, } from './structured-output.js';
3
+ import { writeFileSync, unlinkSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ describe('extractJson', () => {
7
+ it('parses clean JSON', () => {
8
+ const result = extractJson('{"title":"Test","page_count":5}');
9
+ expect(result).toEqual({ title: 'Test', page_count: 5 });
10
+ });
11
+ it('extracts JSON from markdown fences', () => {
12
+ const input = 'Here is the result:\n```json\n{"title":"Test"}\n```\n';
13
+ expect(extractJson(input)).toEqual({ title: 'Test' });
14
+ });
15
+ it('extracts JSON with surrounding text', () => {
16
+ const input = 'Sure! Here is the summary:\n{"title":"Annual Report","page_count":10}\nHope that helps!';
17
+ expect(extractJson(input)).toEqual({ title: 'Annual Report', page_count: 10 });
18
+ });
19
+ it('returns null for non-JSON text', () => {
20
+ expect(extractJson('This is just plain text')).toBeNull();
21
+ });
22
+ it('returns null for empty string', () => {
23
+ expect(extractJson('')).toBeNull();
24
+ });
25
+ it('handles whitespace-padded JSON', () => {
26
+ expect(extractJson(' \n {"a":1} \n ')).toEqual({ a: 1 });
27
+ });
28
+ });
29
+ describe('resolveSchema', () => {
30
+ it('resolves built-in schema by name', () => {
31
+ const result = resolveSchema('summary');
32
+ expect('schema' in result).toBe(true);
33
+ if ('schema' in result) {
34
+ expect(result.schema.source).toBe('built-in:summary');
35
+ expect(result.schema.jsonSchema).toBeDefined();
36
+ expect(result.schema.zodSchema).toBeDefined();
37
+ }
38
+ });
39
+ it('resolves all built-in schemas', () => {
40
+ for (const name of listBuiltinNames()) {
41
+ const result = resolveSchema(name);
42
+ expect('schema' in result).toBe(true);
43
+ }
44
+ });
45
+ it('resolves inline JSON schema', () => {
46
+ const input = '{"type":"object","properties":{"name":{"type":"string"}}}';
47
+ const result = resolveSchema(input);
48
+ expect('schema' in result).toBe(true);
49
+ if ('schema' in result) {
50
+ expect(result.schema.source).toBe('inline');
51
+ expect(result.schema.jsonSchema).toEqual(JSON.parse(input));
52
+ expect(result.schema.zodSchema).toBeUndefined();
53
+ }
54
+ });
55
+ it('rejects inline JSON without type field', () => {
56
+ const result = resolveSchema('{"properties":{"name":{"type":"string"}}}');
57
+ expect('error' in result).toBe(true);
58
+ if ('error' in result) {
59
+ expect(result.error).toContain('type');
60
+ }
61
+ });
62
+ it('rejects invalid inline JSON', () => {
63
+ const result = resolveSchema('{not valid json}');
64
+ expect('error' in result).toBe(true);
65
+ });
66
+ it('resolves schema from file', () => {
67
+ const tmpFile = join(tmpdir(), `test-schema-${Date.now()}.json`);
68
+ const schema = { type: 'object', properties: { age: { type: 'number' } } };
69
+ writeFileSync(tmpFile, JSON.stringify(schema));
70
+ try {
71
+ const result = resolveSchema(tmpFile);
72
+ expect('schema' in result).toBe(true);
73
+ if ('schema' in result) {
74
+ expect(result.schema.source).toBe(`file:${tmpFile}`);
75
+ expect(result.schema.jsonSchema).toEqual(schema);
76
+ }
77
+ }
78
+ finally {
79
+ unlinkSync(tmpFile);
80
+ }
81
+ });
82
+ it('returns error for unknown name', () => {
83
+ const result = resolveSchema('nonexistent');
84
+ expect('error' in result).toBe(true);
85
+ if ('error' in result) {
86
+ expect(result.error).toContain('Unknown schema');
87
+ expect(result.error).toContain('summary');
88
+ }
89
+ });
90
+ });
91
+ describe('listBuiltinNames', () => {
92
+ it('returns all schema names', () => {
93
+ const names = listBuiltinNames();
94
+ expect(names).toContain('summary');
95
+ expect(names).toContain('entities');
96
+ expect(names).toContain('metadata');
97
+ expect(names).toContain('tables');
98
+ expect(names).toHaveLength(4);
99
+ });
100
+ });
101
+ describe('structuredOutputInstructions', () => {
102
+ it('includes structured output block', () => {
103
+ const result = resolveSchema('summary');
104
+ expect('schema' in result).toBe(true);
105
+ if ('schema' in result) {
106
+ const instructions = structuredOutputInstructions(result.schema);
107
+ expect(instructions).toContain('<structured-output>');
108
+ expect(instructions).toContain('JSON');
109
+ }
110
+ });
111
+ });
112
+ describe('built-in schema validation', () => {
113
+ it('summary schema validates correct data', () => {
114
+ const result = resolveSchema('summary');
115
+ if ('schema' in result && result.schema.zodSchema) {
116
+ const data = {
117
+ title: 'Annual Report',
118
+ description: 'Financial summary',
119
+ key_points: ['Revenue grew'],
120
+ page_count: 42,
121
+ document_type: 'report',
122
+ };
123
+ const parsed = result.schema.zodSchema.safeParse(data);
124
+ expect(parsed.success).toBe(true);
125
+ }
126
+ });
127
+ it('summary schema rejects missing required fields', () => {
128
+ const result = resolveSchema('summary');
129
+ if ('schema' in result && result.schema.zodSchema) {
130
+ const parsed = result.schema.zodSchema.safeParse({ title: 'Test' });
131
+ expect(parsed.success).toBe(false);
132
+ }
133
+ });
134
+ it('entities schema validates correct data', () => {
135
+ const result = resolveSchema('entities');
136
+ if ('schema' in result && result.schema.zodSchema) {
137
+ const data = {
138
+ people: ['John'],
139
+ organizations: ['Acme'],
140
+ dates: [{ date: '2024-01-15', context: 'filed' }],
141
+ amounts: [{ value: '$1M', currency: 'USD', context: 'revenue' }],
142
+ };
143
+ const parsed = result.schema.zodSchema.safeParse(data);
144
+ expect(parsed.success).toBe(true);
145
+ }
146
+ });
147
+ });
148
+ //# sourceMappingURL=structured-output.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"structured-output.test.js","sourceRoot":"","sources":["../../src/lib/structured-output.test.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,9 @@
1
1
  /**
2
2
  * Minimal system prompt for CLI agent chat (no UI/mention context)
3
3
  */
4
+ import { type ResolvedSchema } from './structured-output.js';
5
+ export declare function buildSystemPrompt(options?: {
6
+ schema?: ResolvedSchema;
7
+ }): string;
4
8
  export declare const CLI_SYSTEM_PROMPT = "You are a helpful assistant that can help with document analysis and extraction. Your goal is to understand the one pdf given to you with tools like parse and whole suite of python libraries.\n\n<goal>\n - To start, list, or grep for keywords inside derived folder directly. Most likely, you will find markdown files that are page-to-page extracted from the pdf.\n - Try to cite from the derived folder when you try to answer a question or create a document.\n - If there is no relevant information returned from searching the derived/ directory, offer to parse more pages.\n</goal>\n\n<environment>\n OS: Linux (x86_64) in an ephemeral sandbox environment\n Python: 3.12.3, fully functional\n</environment>\n\n<files>\n - There is only one pdf that is in /mnt/data/*.pdf. When user talks about \"the document\", refer to this pdf.\n - All the files you need are in /mnt/data/*\n - Everything you create/extract from the source pdf, move them in the app/derived directory so we can save them for future use in this ephemeral sandbox\n</files>\n\n<derived-folder-structure>\n The derived/ folder contains two parallel OCR outputs that match page-to-page:\n\n <ocr-folder path=\"derived/ocr/\">\n - Raw character extraction from Google DocAI without layout interpretation\n - Use as GROUND TRUTH to detect hallucinations in pages/ output\n - File format: page_001.md, page_002.md, ... + index.json\n </ocr-folder>\n\n <pages-folder path=\"derived/pages/\">\n - AI-interpreted OCR with layout intelligence (VLM/OpenRouter)\n - Structured output: markdown tables, proper column alignment, organized sections\n - Use for QA, data extraction, and answering user questions\n - File format: page_001.md, page_002.md, ... + index.json\n </pages-folder>\n\n <validation-workflow>\n When answering questions or extracting data:\n 1. Use pages/ for structured extraction and user-facing answers\n 2. Cross-reference ocr/ to verify numbers/text are not hallucinated\n 3. If pages/ content differs significantly from ocr/, flag as potential hallucination\n </validation-workflow>\n</derived-folder-structure>\n\n<parse-cli-reference>\n The parse CLI extracts content from PDFs. Output goes to /mnt/data/parse/ directory.\n parse document.pdf # Parse entire PDF\n parse -v document.pdf # Verbose (show progress)\n For specific page ranges: use pypdf to extract pages first, then parse the extracted PDF.\n</parse-cli-reference>\n\n<constraints>\n - NEVER use the Read tool on PDF or image files directly - they will exceed buffer limits and crash.\n - When you mention a file path, always use the full path, ie /mnt/data/derived/revenue.xlsx\n</constraints>";
5
9
  //# sourceMappingURL=system-prompt.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/lib/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,eAAO,MAAM,iBAAiB,oqFAqDf,CAAC"}
1
+ {"version":3,"file":"system-prompt.d.ts","sourceRoot":"","sources":["../../src/lib/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAE,KAAK,cAAc,EAAgC,MAAM,wBAAwB,CAAC;AAE3F,wBAAgB,iBAAiB,CAAC,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,cAAc,CAAA;CAAE,GAAG,MAAM,CAM/E;AAED,eAAO,MAAM,iBAAiB,oqFAqDf,CAAC"}
@@ -1,6 +1,14 @@
1
1
  /**
2
2
  * Minimal system prompt for CLI agent chat (no UI/mention context)
3
3
  */
4
+ import { structuredOutputInstructions } from './structured-output.js';
5
+ export function buildSystemPrompt(options) {
6
+ let prompt = CLI_SYSTEM_PROMPT;
7
+ if (options?.schema) {
8
+ prompt += structuredOutputInstructions(options.schema);
9
+ }
10
+ return prompt;
11
+ }
4
12
  export const CLI_SYSTEM_PROMPT = `You are a helpful assistant that can help with document analysis and extraction. Your goal is to understand the one pdf given to you with tools like parse and whole suite of python libraries.
5
13
 
6
14
  <goal>
@@ -1 +1 @@
1
- {"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/lib/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAqDlB,CAAC"}
1
+ {"version":3,"file":"system-prompt.js","sourceRoot":"","sources":["../../src/lib/system-prompt.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,EAAuB,4BAA4B,EAAE,MAAM,wBAAwB,CAAC;AAE3F,MAAM,UAAU,iBAAiB,CAAC,OAAqC;IACrE,IAAI,MAAM,GAAG,iBAAiB,CAAC;IAC/B,IAAI,OAAO,EAAE,MAAM,EAAE,CAAC;QACpB,MAAM,IAAI,4BAA4B,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,CAAC;IACD,OAAO,MAAM,CAAC;AAChB,CAAC;AAED,MAAM,CAAC,MAAM,iBAAiB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;eAqDlB,CAAC"}
@@ -0,0 +1,230 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>OkraPDF Chat Replay — {{SESSION_ID}}</title>
7
+ <style>
8
+ * { box-sizing: border-box; margin: 0; padding: 0; }
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
11
+ background: #0a0a0a; color: #e5e5e5;
12
+ line-height: 1.6; padding: 0;
13
+ }
14
+ .header {
15
+ position: sticky; top: 0; z-index: 10;
16
+ background: #141414; border-bottom: 1px solid #262626;
17
+ padding: 12px 24px; display: flex; align-items: center; gap: 16px;
18
+ }
19
+ .header h1 { font-size: 14px; font-weight: 600; color: #a3a3a3; }
20
+ .header .meta { font-size: 12px; color: #525252; }
21
+ .header .meta span { margin-right: 12px; }
22
+ .container { max-width: 860px; margin: 0 auto; padding: 24px; }
23
+ .event {
24
+ margin-bottom: 16px; padding: 12px 16px;
25
+ border-radius: 8px; position: relative;
26
+ border: 1px solid transparent;
27
+ }
28
+ .event.active { border-color: #555; }
29
+ .event-user {
30
+ background: #1a1a1a; border-left: 3px solid #888;
31
+ }
32
+ .event-assistant { background: #171717; }
33
+ .event-result {
34
+ background: #0c0c0c; border-left: 3px solid #22c55e;
35
+ font-size: 13px; color: #737373; padding: 8px 16px;
36
+ }
37
+ .event-error {
38
+ background: #1a0a0a; border-left: 3px solid #ef4444;
39
+ }
40
+ .event-system {
41
+ background: transparent; color: #525252;
42
+ font-size: 12px; padding: 4px 16px; text-align: center;
43
+ }
44
+ .role {
45
+ font-size: 11px; font-weight: 600; text-transform: uppercase;
46
+ letter-spacing: 0.05em; margin-bottom: 6px;
47
+ }
48
+ .role-user { color: #ccc; }
49
+ .role-assistant { color: #a3a3a3; }
50
+ .role-error { color: #f87171; }
51
+ .timestamp {
52
+ position: absolute; top: 12px; right: 16px;
53
+ font-size: 11px; color: #404040;
54
+ }
55
+ .content { white-space: pre-wrap; word-break: break-word; }
56
+ .content code {
57
+ background: #262626; padding: 1px 5px; border-radius: 3px;
58
+ font-family: 'SF Mono', 'Fira Code', monospace; font-size: 13px;
59
+ }
60
+ .tool-use {
61
+ margin: 8px 0; padding: 8px 12px;
62
+ background: #1a1a1a; border-radius: 6px;
63
+ border: 1px solid #333; cursor: pointer;
64
+ }
65
+ .tool-use summary {
66
+ font-size: 13px; color: #999; font-weight: 500;
67
+ list-style: none; display: flex; align-items: center; gap: 6px;
68
+ }
69
+ .tool-use summary::before { content: '▶'; font-size: 10px; transition: transform 0.15s; }
70
+ .tool-use[open] summary::before { transform: rotate(90deg); }
71
+ .tool-input {
72
+ margin-top: 8px; padding: 8px;
73
+ background: #111; border-radius: 4px;
74
+ font-family: 'SF Mono', monospace; font-size: 12px;
75
+ color: #a3a3a3; overflow-x: auto; white-space: pre;
76
+ }
77
+ .tool-result {
78
+ margin: 4px 0; padding: 6px 12px;
79
+ background: #111; border-radius: 4px;
80
+ font-size: 12px; color: #737373;
81
+ max-height: 200px; overflow-y: auto;
82
+ font-family: 'SF Mono', monospace; white-space: pre-wrap;
83
+ }
84
+ .nav-hint {
85
+ position: fixed; bottom: 16px; right: 16px;
86
+ background: #262626; padding: 8px 12px; border-radius: 6px;
87
+ font-size: 11px; color: #525252;
88
+ }
89
+ .nav-hint kbd {
90
+ background: #404040; padding: 2px 6px; border-radius: 3px;
91
+ font-family: monospace; color: #a3a3a3;
92
+ }
93
+ .cost { color: #22c55e; }
94
+ .duration { color: #a3a3a3; }
95
+ </style>
96
+ </head>
97
+ <body>
98
+ <div class="header">
99
+ <h1>OkraPDF Chat Replay</h1>
100
+ <div class="meta">
101
+ <span id="session-id"></span>
102
+ <span id="event-count"></span>
103
+ <span id="time-range"></span>
104
+ </div>
105
+ </div>
106
+ <div class="container" id="events"></div>
107
+ <div class="nav-hint">
108
+ <kbd>↑</kbd><kbd>↓</kbd> navigate &nbsp;
109
+ <kbd>Home</kbd><kbd>End</kbd> jump
110
+ </div>
111
+
112
+ <script>
113
+ const DATA = {{REPLAY_DATA}};
114
+ const container = document.getElementById('events');
115
+ let activeIndex = -1;
116
+ const eventEls = [];
117
+
118
+ // Metadata
119
+ document.getElementById('session-id').textContent = '{{SESSION_ID}}';
120
+ document.getElementById('event-count').textContent = DATA.length + ' events';
121
+ if (DATA.length > 0) {
122
+ const first = DATA[0].timestamp;
123
+ const last = DATA[DATA.length - 1].timestamp;
124
+ if (first && last) {
125
+ document.getElementById('time-range').textContent =
126
+ new Date(first).toLocaleString() + ' → ' + new Date(last).toLocaleString();
127
+ }
128
+ }
129
+
130
+ function formatTs(ts) {
131
+ if (!ts) return '';
132
+ const d = new Date(ts);
133
+ return d.toLocaleTimeString();
134
+ }
135
+
136
+ function escapeHtml(s) {
137
+ if (typeof s !== 'string') return '';
138
+ return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
139
+ }
140
+
141
+ function renderContent(blocks) {
142
+ if (!Array.isArray(blocks)) return escapeHtml(String(blocks || ''));
143
+ let html = '';
144
+ for (const b of blocks) {
145
+ if (b.type === 'text') {
146
+ html += '<div class="content">' + escapeHtml(b.text) + '</div>';
147
+ } else if (b.type === 'tool_use') {
148
+ html += '<details class="tool-use"><summary>' + escapeHtml(b.name) + '</summary>';
149
+ html += '<div class="tool-input">' + escapeHtml(JSON.stringify(b.input, null, 2)) + '</div>';
150
+ html += '</details>';
151
+ } else if (b.type === 'tool_result') {
152
+ const text = typeof b.content === 'string' ? b.content : JSON.stringify(b.content);
153
+ const truncated = text && text.length > 500 ? text.slice(0, 500) + '...' : text;
154
+ html += '<div class="tool-result">' + escapeHtml(truncated || '') + '</div>';
155
+ }
156
+ }
157
+ return html;
158
+ }
159
+
160
+ for (let i = 0; i < DATA.length; i++) {
161
+ const evt = DATA[i];
162
+ const div = document.createElement('div');
163
+
164
+ switch (evt.type) {
165
+ case 'user':
166
+ div.className = 'event event-user';
167
+ div.innerHTML = '<div class="role role-user">User' +
168
+ (evt.author ? ' (' + escapeHtml(evt.author.name) + ')' : '') +
169
+ '</div>' +
170
+ '<span class="timestamp">' + formatTs(evt.timestamp) + '</span>' +
171
+ '<div class="content">' + escapeHtml(evt.message?.content || '') + '</div>';
172
+ break;
173
+ case 'assistant':
174
+ div.className = 'event event-assistant';
175
+ div.innerHTML = '<div class="role role-assistant">Assistant</div>' +
176
+ '<span class="timestamp">' + formatTs(evt.timestamp) + '</span>' +
177
+ renderContent(evt.message?.content || []);
178
+ break;
179
+ case 'result':
180
+ div.className = 'event event-result';
181
+ const parts = [];
182
+ if (evt.duration_ms != null) parts.push('<span class="duration">' + (evt.duration_ms / 1000).toFixed(1) + 's</span>');
183
+ if (evt.total_cost_usd != null) parts.push('<span class="cost">$' + evt.total_cost_usd.toFixed(4) + '</span>');
184
+ if (evt.exit_code != null) parts.push('exit ' + evt.exit_code);
185
+ div.innerHTML = parts.join(' · ') || 'done';
186
+ break;
187
+ case 'error':
188
+ div.className = 'event event-error';
189
+ div.innerHTML = '<div class="role role-error">Error</div>' +
190
+ '<div class="content">' + escapeHtml(evt.error) + '</div>';
191
+ break;
192
+ case 'system':
193
+ div.className = 'event event-system';
194
+ div.textContent = evt.message || '';
195
+ break;
196
+ default:
197
+ continue;
198
+ }
199
+
200
+ div.dataset.index = i;
201
+ container.appendChild(div);
202
+ eventEls.push(div);
203
+ }
204
+
205
+ function setActive(idx) {
206
+ if (idx < 0 || idx >= eventEls.length) return;
207
+ if (activeIndex >= 0) eventEls[activeIndex].classList.remove('active');
208
+ activeIndex = idx;
209
+ eventEls[activeIndex].classList.add('active');
210
+ eventEls[activeIndex].scrollIntoView({ block: 'nearest', behavior: 'smooth' });
211
+ }
212
+
213
+ document.addEventListener('keydown', (e) => {
214
+ if (e.key === 'ArrowDown' || e.key === 'j') {
215
+ e.preventDefault();
216
+ setActive(Math.min(activeIndex + 1, eventEls.length - 1));
217
+ } else if (e.key === 'ArrowUp' || e.key === 'k') {
218
+ e.preventDefault();
219
+ setActive(Math.max(activeIndex - 1, 0));
220
+ } else if (e.key === 'Home') {
221
+ e.preventDefault();
222
+ setActive(0);
223
+ } else if (e.key === 'End') {
224
+ e.preventDefault();
225
+ setActive(eventEls.length - 1);
226
+ }
227
+ });
228
+ </script>
229
+ </body>
230
+ </html>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@okrapdf/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "OkraPDF command-line interface for PDF extraction and document chat",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "access": "public"
13
13
  },
14
14
  "scripts": {
15
- "build": "tsc",
15
+ "build": "tsc && cp -r src/templates dist/",
16
16
  "dev": "tsc --watch",
17
17
  "start": "node dist/index.js",
18
18
  "test": "vitest run",
@@ -57,7 +57,8 @@
57
57
  "got": "^14.4.2",
58
58
  "ora": "^8.0.1",
59
59
  "pdfquery": "^0.1.2",
60
- "ws": "^8.18.0"
60
+ "ws": "^8.18.0",
61
+ "zod": "^4.3.6"
61
62
  },
62
63
  "optionalDependencies": {
63
64
  "mupdf": "^0.3.0",