@redaksjon/protokoll 0.0.12 → 0.0.13

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 (75) hide show
  1. package/.cursor/rules/definition-of-done.md +1 -0
  2. package/.cursor/rules/no-emoticons.md +26 -12
  3. package/README.md +483 -69
  4. package/dist/agentic/executor.js +473 -41
  5. package/dist/agentic/executor.js.map +1 -1
  6. package/dist/agentic/index.js.map +1 -1
  7. package/dist/agentic/tools/lookup-person.js +123 -4
  8. package/dist/agentic/tools/lookup-person.js.map +1 -1
  9. package/dist/agentic/tools/lookup-project.js +139 -22
  10. package/dist/agentic/tools/lookup-project.js.map +1 -1
  11. package/dist/agentic/tools/route-note.js +5 -1
  12. package/dist/agentic/tools/route-note.js.map +1 -1
  13. package/dist/arguments.js +6 -3
  14. package/dist/arguments.js.map +1 -1
  15. package/dist/cli/action.js +704 -0
  16. package/dist/cli/action.js.map +1 -0
  17. package/dist/cli/config.js +482 -0
  18. package/dist/cli/config.js.map +1 -0
  19. package/dist/cli/context.js +466 -0
  20. package/dist/cli/context.js.map +1 -0
  21. package/dist/cli/feedback.js +858 -0
  22. package/dist/cli/feedback.js.map +1 -0
  23. package/dist/cli/index.js +103 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/cli/install.js +572 -0
  26. package/dist/cli/install.js.map +1 -0
  27. package/dist/cli/transcript.js +199 -0
  28. package/dist/cli/transcript.js.map +1 -0
  29. package/dist/constants.js +11 -4
  30. package/dist/constants.js.map +1 -1
  31. package/dist/context/index.js +25 -1
  32. package/dist/context/index.js.map +1 -1
  33. package/dist/context/storage.js +56 -3
  34. package/dist/context/storage.js.map +1 -1
  35. package/dist/interactive/handler.js +310 -9
  36. package/dist/interactive/handler.js.map +1 -1
  37. package/dist/main.js +11 -1
  38. package/dist/main.js.map +1 -1
  39. package/dist/output/index.js.map +1 -1
  40. package/dist/output/manager.js +46 -1
  41. package/dist/output/manager.js.map +1 -1
  42. package/dist/phases/complete.js +37 -2
  43. package/dist/phases/complete.js.map +1 -1
  44. package/dist/pipeline/orchestrator.js +104 -31
  45. package/dist/pipeline/orchestrator.js.map +1 -1
  46. package/dist/protokoll.js +68 -2
  47. package/dist/protokoll.js.map +1 -1
  48. package/dist/reasoning/client.js +83 -0
  49. package/dist/reasoning/client.js.map +1 -1
  50. package/dist/reasoning/index.js +1 -0
  51. package/dist/reasoning/index.js.map +1 -1
  52. package/dist/util/metadata.js.map +1 -1
  53. package/dist/util/sound.js +116 -0
  54. package/dist/util/sound.js.map +1 -0
  55. package/docs/duplicate-question-prevention.md +117 -0
  56. package/docs/examples.md +152 -0
  57. package/docs/interactive-context-example.md +92 -0
  58. package/docs/package-lock.json +6 -0
  59. package/docs/package.json +3 -1
  60. package/guide/action.md +375 -0
  61. package/guide/config.md +207 -0
  62. package/guide/configuration.md +82 -67
  63. package/guide/context-commands.md +574 -0
  64. package/guide/context-system.md +20 -7
  65. package/guide/development.md +106 -4
  66. package/guide/feedback.md +335 -0
  67. package/guide/index.md +100 -4
  68. package/guide/interactive.md +15 -14
  69. package/guide/quickstart.md +21 -7
  70. package/guide/reasoning.md +18 -4
  71. package/guide/routing.md +192 -97
  72. package/package.json +1 -1
  73. package/scripts/coverage-priority.mjs +323 -0
  74. package/tsconfig.tsbuildinfo +1 -1
  75. package/vitest.config.ts +5 -1
@@ -0,0 +1,704 @@
1
+ import { Command } from 'commander';
2
+ import * as fs from 'fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { create } from '../context/index.js';
6
+ import { create as create$1 } from '../routing/index.js';
7
+
8
+ // Helper to print to stdout
9
+ const print = (text)=>process.stdout.write(text + '\n');
10
+ /**
11
+ * Parse a transcript file into its components
12
+ */ const parseTranscript = async (filePath)=>{
13
+ const rawText = await fs.readFile(filePath, 'utf-8');
14
+ const lines = rawText.split('\n');
15
+ const result = {
16
+ filePath,
17
+ metadata: {},
18
+ content: '',
19
+ rawText
20
+ };
21
+ let inMetadata = false;
22
+ let inRouting = false;
23
+ let contentStartIndex = 0;
24
+ for(let i = 0; i < lines.length; i++){
25
+ const line = lines[i];
26
+ const trimmed = line.trim();
27
+ // Title detection (first # heading)
28
+ if (!result.title && trimmed.startsWith('# ') && !trimmed.startsWith('## ')) {
29
+ result.title = trimmed.slice(2).trim();
30
+ continue;
31
+ }
32
+ // Metadata section start
33
+ if (trimmed === '## Metadata') {
34
+ inMetadata = true;
35
+ continue;
36
+ }
37
+ // Routing subsection
38
+ if (trimmed === '### Routing') {
39
+ inRouting = true;
40
+ continue;
41
+ }
42
+ // End of metadata section (horizontal rule)
43
+ if (trimmed === '---' && inMetadata) {
44
+ contentStartIndex = i + 1;
45
+ inMetadata = false;
46
+ inRouting = false;
47
+ continue;
48
+ }
49
+ // Parse metadata fields
50
+ if (inMetadata && trimmed.startsWith('**')) {
51
+ const match = trimmed.match(/^\*\*([^*]+)\*\*:\s*(.*)$/);
52
+ if (match) {
53
+ const [, key, value] = match;
54
+ const normalizedKey = key.toLowerCase().replace(/\s+/g, '');
55
+ switch(normalizedKey){
56
+ case 'date':
57
+ result.metadata.date = value;
58
+ break;
59
+ case 'time':
60
+ result.metadata.time = value;
61
+ break;
62
+ case 'project':
63
+ result.metadata.project = value;
64
+ break;
65
+ case 'projectid':
66
+ // Remove backticks from project ID
67
+ result.metadata.projectId = value.replace(/`/g, '');
68
+ break;
69
+ case 'destination':
70
+ result.metadata.destination = value;
71
+ break;
72
+ case 'confidence':
73
+ result.metadata.confidence = value;
74
+ break;
75
+ case 'reasoning':
76
+ result.metadata.reasoning = value;
77
+ break;
78
+ case 'tags':
79
+ var _value_match;
80
+ // Parse tags from backtick-wrapped format
81
+ result.metadata.tags = ((_value_match = value.match(/`([^`]+)`/g)) === null || _value_match === void 0 ? void 0 : _value_match.map((t)=>t.replace(/`/g, ''))) || [];
82
+ break;
83
+ case 'duration':
84
+ result.metadata.duration = value;
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ // Parse classification signals (list items under routing)
90
+ if (inRouting && trimmed.startsWith('- ') && !trimmed.startsWith('**')) {
91
+ if (!result.metadata.signals) {
92
+ result.metadata.signals = [];
93
+ }
94
+ result.metadata.signals.push(trimmed.slice(2));
95
+ }
96
+ }
97
+ // Extract content after metadata
98
+ if (contentStartIndex > 0) {
99
+ // Skip empty lines after ---
100
+ while(contentStartIndex < lines.length && lines[contentStartIndex].trim() === ''){
101
+ contentStartIndex++;
102
+ }
103
+ result.content = lines.slice(contentStartIndex).join('\n').trim();
104
+ } else {
105
+ // No metadata section found, entire file is content
106
+ result.content = rawText.trim();
107
+ }
108
+ return result;
109
+ };
110
+ /**
111
+ * Extract the timestamp from a transcript filename
112
+ * Expected format: DD-HHMM-subject.md (e.g., 15-1412-ne-4th-st-0.md)
113
+ */ const extractTimestampFromFilename = (filePath)=>{
114
+ const basename = path.basename(filePath, '.md');
115
+ const match = basename.match(/^(\d{1,2})-(\d{2})(\d{2})/);
116
+ if (match) {
117
+ return {
118
+ day: parseInt(match[1], 10),
119
+ hour: parseInt(match[2], 10),
120
+ minute: parseInt(match[3], 10)
121
+ };
122
+ }
123
+ return null;
124
+ };
125
+ /**
126
+ * Format metadata as Markdown heading section (matching util/metadata.ts format)
127
+ */ const formatMetadataMarkdown = (title, metadata, project)=>{
128
+ const lines = [];
129
+ // Title section
130
+ lines.push(`# ${title}`);
131
+ lines.push('');
132
+ // Metadata frontmatter as readable markdown
133
+ lines.push('## Metadata');
134
+ lines.push('');
135
+ // Date and Time
136
+ if (metadata.date) {
137
+ lines.push(`**Date**: ${metadata.date}`);
138
+ }
139
+ if (metadata.time) {
140
+ lines.push(`**Time**: ${metadata.time}`);
141
+ }
142
+ lines.push('');
143
+ // Project
144
+ if (project) {
145
+ lines.push(`**Project**: ${project.name}`);
146
+ lines.push(`**Project ID**: \`${project.id}\``);
147
+ lines.push('');
148
+ } else if (metadata.project) {
149
+ lines.push(`**Project**: ${metadata.project}`);
150
+ if (metadata.projectId) {
151
+ lines.push(`**Project ID**: \`${metadata.projectId}\``);
152
+ }
153
+ lines.push('');
154
+ }
155
+ // Routing Information
156
+ if (metadata.destination) {
157
+ lines.push('### Routing');
158
+ lines.push('');
159
+ lines.push(`**Destination**: ${metadata.destination}`);
160
+ if (metadata.confidence) {
161
+ lines.push(`**Confidence**: ${metadata.confidence}`);
162
+ }
163
+ lines.push('');
164
+ if (metadata.signals && metadata.signals.length > 0) {
165
+ lines.push('**Classification Signals**:');
166
+ for (const signal of metadata.signals){
167
+ lines.push(`- ${signal}`);
168
+ }
169
+ lines.push('');
170
+ }
171
+ if (metadata.reasoning) {
172
+ lines.push(`**Reasoning**: ${metadata.reasoning}`);
173
+ lines.push('');
174
+ }
175
+ }
176
+ // Tags
177
+ if (metadata.tags && metadata.tags.length > 0) {
178
+ lines.push('**Tags**: ' + metadata.tags.map((tag)=>`\`${tag}\``).join(', '));
179
+ lines.push('');
180
+ }
181
+ // Duration
182
+ if (metadata.duration) {
183
+ lines.push(`**Duration**: ${metadata.duration}`);
184
+ lines.push('');
185
+ }
186
+ // Separator
187
+ lines.push('---');
188
+ lines.push('');
189
+ return lines.join('\n');
190
+ };
191
+ /**
192
+ * Slugify a title for use in filenames
193
+ */ const slugifyTitle = (title)=>{
194
+ return title.toLowerCase().replace(/[^a-z0-9]+/g, '-') // Replace non-alphanumeric with dash
195
+ .replace(/--+/g, '-') // Collapse multiple dashes
196
+ .replace(/^-|-$/g, '') // Remove leading/trailing dashes
197
+ .slice(0, 50); // Limit length
198
+ };
199
+ /**
200
+ * Combine multiple transcripts into a single document
201
+ */ const combineTranscripts = async (filePaths, options = {})=>{
202
+ var _targetProject_routing;
203
+ if (filePaths.length === 0) {
204
+ throw new Error('No transcript files provided');
205
+ }
206
+ // Parse all transcripts
207
+ const transcripts = [];
208
+ for (const filePath of filePaths){
209
+ try {
210
+ const parsed = await parseTranscript(filePath);
211
+ transcripts.push(parsed);
212
+ } catch (error) {
213
+ throw new Error(`Failed to parse transcript: ${filePath} - ${error}`);
214
+ }
215
+ }
216
+ // Sort by filename (which should be chronological due to timestamp prefix)
217
+ transcripts.sort((a, b)=>{
218
+ const aName = path.basename(a.filePath);
219
+ const bName = path.basename(b.filePath);
220
+ return aName.localeCompare(bName);
221
+ });
222
+ // Use the first transcript's metadata as the base
223
+ const firstTranscript = transcripts[0];
224
+ const baseMetadata = {
225
+ ...firstTranscript.metadata
226
+ };
227
+ // Load context to get project information if needed
228
+ const context = await create();
229
+ let targetProject;
230
+ if (options.projectId) {
231
+ var _targetProject_routing1;
232
+ targetProject = context.getProject(options.projectId);
233
+ if (!targetProject) {
234
+ throw new Error(`Project not found: ${options.projectId}`);
235
+ }
236
+ // Update metadata with new project
237
+ baseMetadata.project = targetProject.name;
238
+ baseMetadata.projectId = targetProject.id;
239
+ // Update destination if project has routing configured
240
+ if ((_targetProject_routing1 = targetProject.routing) === null || _targetProject_routing1 === void 0 ? void 0 : _targetProject_routing1.destination) {
241
+ baseMetadata.destination = expandPath(targetProject.routing.destination);
242
+ }
243
+ }
244
+ // Calculate combined duration if available
245
+ let totalSeconds = 0;
246
+ let hasDuration = false;
247
+ for (const t of transcripts){
248
+ if (t.metadata.duration) {
249
+ hasDuration = true;
250
+ totalSeconds += parseDuration(t.metadata.duration);
251
+ }
252
+ }
253
+ if (hasDuration && totalSeconds > 0) {
254
+ baseMetadata.duration = formatDuration(totalSeconds);
255
+ }
256
+ // Combine tags from all transcripts (deduplicated)
257
+ const allTags = new Set();
258
+ for (const t of transcripts){
259
+ if (t.metadata.tags) {
260
+ for (const tag of t.metadata.tags){
261
+ allTags.add(tag);
262
+ }
263
+ }
264
+ }
265
+ if (allTags.size > 0) {
266
+ baseMetadata.tags = Array.from(allTags).sort();
267
+ }
268
+ // Build combined title - use custom title if provided
269
+ const combinedTitle = options.title ? options.title : firstTranscript.title ? `${firstTranscript.title} (Combined)` : 'Combined Transcript';
270
+ // Build combined content with section markers
271
+ const contentParts = [];
272
+ for(let i = 0; i < transcripts.length; i++){
273
+ const t = transcripts[i];
274
+ const sectionTitle = t.title || `Part ${i + 1}`;
275
+ const sourceFile = path.basename(t.filePath);
276
+ contentParts.push(`## ${sectionTitle}`);
277
+ contentParts.push(`*Source: ${sourceFile}*`);
278
+ contentParts.push('');
279
+ contentParts.push(t.content);
280
+ contentParts.push('');
281
+ }
282
+ // Build final document
283
+ const metadataSection = formatMetadataMarkdown(combinedTitle, baseMetadata, targetProject);
284
+ const finalContent = metadataSection + contentParts.join('\n');
285
+ // Determine output path
286
+ let outputPath;
287
+ if (targetProject === null || targetProject === void 0 ? void 0 : (_targetProject_routing = targetProject.routing) === null || _targetProject_routing === void 0 ? void 0 : _targetProject_routing.destination) {
288
+ // Build path using project routing configuration
289
+ const routingConfig = buildRoutingConfig(context);
290
+ const routing = create$1(routingConfig, context);
291
+ // Extract date from first transcript for routing
292
+ const audioDate = extractDateFromMetadata(baseMetadata, firstTranscript.filePath);
293
+ const routingContext = {
294
+ transcriptText: finalContent,
295
+ audioDate,
296
+ sourceFile: firstTranscript.filePath
297
+ };
298
+ const decision = routing.route(routingContext);
299
+ outputPath = routing.buildOutputPath(decision, routingContext);
300
+ } else {
301
+ // Use the directory of the first transcript with a new filename
302
+ const firstDir = path.dirname(firstTranscript.filePath);
303
+ const timestamp = extractTimestampFromFilename(firstTranscript.filePath);
304
+ // Use slugified custom title if provided, otherwise "combined"
305
+ const filenameSuffix = options.title ? slugifyTitle(options.title) : 'combined';
306
+ if (timestamp) {
307
+ const day = timestamp.day.toString().padStart(2, '0');
308
+ const hour = timestamp.hour.toString().padStart(2, '0');
309
+ const minute = timestamp.minute.toString().padStart(2, '0');
310
+ outputPath = path.join(firstDir, `${day}-${hour}${minute}-${filenameSuffix}.md`);
311
+ } else {
312
+ outputPath = path.join(firstDir, `${filenameSuffix}.md`);
313
+ }
314
+ }
315
+ return {
316
+ outputPath,
317
+ content: finalContent
318
+ };
319
+ };
320
+ /**
321
+ * Build a routing config from context and project
322
+ */ const buildRoutingConfig = (context, _targetProject)=>{
323
+ const config = context.getConfig();
324
+ const defaultPath = expandPath(config.outputDirectory || '~/notes');
325
+ // Build project routes from all projects
326
+ const projects = context.getAllProjects().filter((p)=>p.active !== false).map((p)=>{
327
+ var _p_routing, _p_routing1, _p_routing2, _p_routing3;
328
+ return {
329
+ projectId: p.id,
330
+ destination: {
331
+ path: expandPath(((_p_routing = p.routing) === null || _p_routing === void 0 ? void 0 : _p_routing.destination) || defaultPath),
332
+ structure: ((_p_routing1 = p.routing) === null || _p_routing1 === void 0 ? void 0 : _p_routing1.structure) || 'month',
333
+ filename_options: ((_p_routing2 = p.routing) === null || _p_routing2 === void 0 ? void 0 : _p_routing2.filename_options) || [
334
+ 'date',
335
+ 'time',
336
+ 'subject'
337
+ ]
338
+ },
339
+ classification: p.classification,
340
+ auto_tags: (_p_routing3 = p.routing) === null || _p_routing3 === void 0 ? void 0 : _p_routing3.auto_tags
341
+ };
342
+ });
343
+ return {
344
+ default: {
345
+ path: defaultPath,
346
+ structure: config.outputStructure || 'month',
347
+ filename_options: config.outputFilenameOptions || [
348
+ 'date',
349
+ 'time',
350
+ 'subject'
351
+ ]
352
+ },
353
+ projects,
354
+ conflict_resolution: 'primary'
355
+ };
356
+ };
357
+ /**
358
+ * Extract date from metadata or filename
359
+ */ const extractDateFromMetadata = (metadata, filePath)=>{
360
+ // Try to parse from metadata date string
361
+ if (metadata.date) {
362
+ const parsed = new Date(metadata.date);
363
+ if (!isNaN(parsed.getTime())) {
364
+ // Add time if available
365
+ if (metadata.time) {
366
+ const timeMatch = metadata.time.match(/(\d{1,2}):(\d{2})\s*(AM|PM)?/i);
367
+ if (timeMatch) {
368
+ var _timeMatch_;
369
+ let hours = parseInt(timeMatch[1], 10);
370
+ const minutes = parseInt(timeMatch[2], 10);
371
+ const ampm = (_timeMatch_ = timeMatch[3]) === null || _timeMatch_ === void 0 ? void 0 : _timeMatch_.toUpperCase();
372
+ if (ampm === 'PM' && hours < 12) hours += 12;
373
+ if (ampm === 'AM' && hours === 12) hours = 0;
374
+ parsed.setHours(hours, minutes);
375
+ }
376
+ }
377
+ return parsed;
378
+ }
379
+ }
380
+ // Fall back to extracting from directory structure and filename
381
+ // Expected path: .../2026/1/15-1412-subject.md
382
+ const parts = filePath.split(path.sep);
383
+ // Look for year/month in path
384
+ let year = new Date().getFullYear();
385
+ let month = new Date().getMonth();
386
+ for(let i = 0; i < parts.length - 1; i++){
387
+ const part = parts[i];
388
+ const num = parseInt(part, 10);
389
+ if (num >= 2000 && num <= 2100) {
390
+ year = num;
391
+ // Next part might be month
392
+ if (i + 1 < parts.length - 1) {
393
+ const nextNum = parseInt(parts[i + 1], 10);
394
+ if (nextNum >= 1 && nextNum <= 12) {
395
+ month = nextNum - 1; // 0-indexed
396
+ }
397
+ }
398
+ }
399
+ }
400
+ // Extract day and time from filename
401
+ const timestamp = extractTimestampFromFilename(filePath);
402
+ const day = (timestamp === null || timestamp === void 0 ? void 0 : timestamp.day) || 1;
403
+ const hour = (timestamp === null || timestamp === void 0 ? void 0 : timestamp.hour) || 0;
404
+ const minute = (timestamp === null || timestamp === void 0 ? void 0 : timestamp.minute) || 0;
405
+ return new Date(year, month, day, hour, minute);
406
+ };
407
+ /**
408
+ * Parse duration string to seconds
409
+ */ const parseDuration = (duration)=>{
410
+ let seconds = 0;
411
+ const minuteMatch = duration.match(/(\d+)m/);
412
+ const secondMatch = duration.match(/(\d+)s/);
413
+ if (minuteMatch) {
414
+ seconds += parseInt(minuteMatch[1], 10) * 60;
415
+ }
416
+ if (secondMatch) {
417
+ seconds += parseInt(secondMatch[1], 10);
418
+ }
419
+ return seconds;
420
+ };
421
+ /**
422
+ * Format seconds as duration string
423
+ */ const formatDuration = (seconds)=>{
424
+ const minutes = Math.floor(seconds / 60);
425
+ const secs = Math.round(seconds % 60);
426
+ if (minutes === 0) {
427
+ return `${secs}s`;
428
+ }
429
+ if (secs === 0) {
430
+ return `${minutes}m`;
431
+ }
432
+ return `${minutes}m ${secs}s`;
433
+ };
434
+ /**
435
+ * Expand ~ to home directory
436
+ */ const expandPath = (p)=>{
437
+ if (p.startsWith('~')) {
438
+ return path.join(os.homedir(), p.slice(1));
439
+ }
440
+ return p;
441
+ };
442
+ /**
443
+ * Parse file paths from the combine argument
444
+ * Supports newline-separated paths (from command line) or array
445
+ */ const parseFilePaths = (input)=>{
446
+ // Split by newlines and filter empty lines
447
+ return input.split('\n').map((line)=>line.trim()).filter((line)=>line.length > 0);
448
+ };
449
+ /**
450
+ * Edit a single transcript - update title and/or project
451
+ */ const editTranscript = async (filePath, options)=>{
452
+ var _targetProject_routing;
453
+ // Parse the existing transcript
454
+ const transcript = await parseTranscript(filePath);
455
+ // Load context if we need project info
456
+ const context = await create();
457
+ let targetProject;
458
+ if (options.projectId) {
459
+ targetProject = context.getProject(options.projectId);
460
+ if (!targetProject) {
461
+ throw new Error(`Project not found: ${options.projectId}`);
462
+ }
463
+ }
464
+ // Use new title if provided, otherwise keep existing
465
+ const newTitle = options.title || transcript.title || 'Untitled';
466
+ // Update metadata
467
+ const updatedMetadata = {
468
+ ...transcript.metadata
469
+ };
470
+ if (targetProject) {
471
+ var _targetProject_routing1;
472
+ updatedMetadata.project = targetProject.name;
473
+ updatedMetadata.projectId = targetProject.id;
474
+ if ((_targetProject_routing1 = targetProject.routing) === null || _targetProject_routing1 === void 0 ? void 0 : _targetProject_routing1.destination) {
475
+ updatedMetadata.destination = expandPath(targetProject.routing.destination);
476
+ }
477
+ }
478
+ // Build the updated document
479
+ const metadataSection = formatMetadataMarkdown(newTitle, updatedMetadata, targetProject);
480
+ const finalContent = metadataSection + transcript.content;
481
+ // Determine output path
482
+ let outputPath;
483
+ if (targetProject === null || targetProject === void 0 ? void 0 : (_targetProject_routing = targetProject.routing) === null || _targetProject_routing === void 0 ? void 0 : _targetProject_routing.destination) {
484
+ // Build path using project routing configuration
485
+ const routingConfig = buildRoutingConfig(context);
486
+ const routing = create$1(routingConfig, context);
487
+ const audioDate = extractDateFromMetadata(updatedMetadata, filePath);
488
+ const routingContext = {
489
+ transcriptText: finalContent,
490
+ audioDate,
491
+ sourceFile: filePath
492
+ };
493
+ const decision = routing.route(routingContext);
494
+ // If we have a custom title, override the filename with slugified title
495
+ if (options.title) {
496
+ const basePath = path.dirname(routing.buildOutputPath(decision, routingContext));
497
+ const timestamp = extractTimestampFromFilename(filePath);
498
+ const sluggedTitle = slugifyTitle(options.title);
499
+ if (timestamp) {
500
+ const day = timestamp.day.toString().padStart(2, '0');
501
+ const hour = timestamp.hour.toString().padStart(2, '0');
502
+ const minute = timestamp.minute.toString().padStart(2, '0');
503
+ outputPath = path.join(basePath, `${day}-${hour}${minute}-${sluggedTitle}.md`);
504
+ } else {
505
+ outputPath = path.join(basePath, `${sluggedTitle}.md`);
506
+ }
507
+ } else {
508
+ outputPath = routing.buildOutputPath(decision, routingContext);
509
+ }
510
+ } else {
511
+ // Keep in same directory, potentially with new filename
512
+ const dir = path.dirname(filePath);
513
+ const timestamp = extractTimestampFromFilename(filePath);
514
+ if (options.title) {
515
+ const sluggedTitle = slugifyTitle(options.title);
516
+ if (timestamp) {
517
+ const day = timestamp.day.toString().padStart(2, '0');
518
+ const hour = timestamp.hour.toString().padStart(2, '0');
519
+ const minute = timestamp.minute.toString().padStart(2, '0');
520
+ outputPath = path.join(dir, `${day}-${hour}${minute}-${sluggedTitle}.md`);
521
+ } else {
522
+ outputPath = path.join(dir, `${sluggedTitle}.md`);
523
+ }
524
+ } else {
525
+ // Keep original filename
526
+ outputPath = filePath;
527
+ }
528
+ }
529
+ return {
530
+ outputPath,
531
+ content: finalContent
532
+ };
533
+ };
534
+ /**
535
+ * Execute the action command
536
+ */ const executeAction = async (file, options)=>{
537
+ // Determine mode: combine or edit
538
+ if (options.combine) {
539
+ // Combine mode
540
+ const filePaths = parseFilePaths(options.combine);
541
+ if (filePaths.length === 0) {
542
+ print('Error: No transcript files provided for --combine.');
543
+ process.exit(1);
544
+ }
545
+ if (filePaths.length === 1) {
546
+ print('Error: At least 2 transcript files are required for --combine.');
547
+ process.exit(1);
548
+ }
549
+ // Validate all files exist
550
+ for (const filePath of filePaths){
551
+ try {
552
+ await fs.access(filePath);
553
+ } catch {
554
+ print(`Error: File not found: ${filePath}`);
555
+ process.exit(1);
556
+ }
557
+ }
558
+ if (options.verbose) {
559
+ print(`\n[Combining ${filePaths.length} transcripts]`);
560
+ for (const fp of filePaths){
561
+ print(` - ${fp}`);
562
+ }
563
+ if (options.project) {
564
+ print(`\nTarget project: ${options.project}`);
565
+ }
566
+ if (options.title) {
567
+ print(`\nCustom title: ${options.title}`);
568
+ }
569
+ print('');
570
+ }
571
+ try {
572
+ const result = await combineTranscripts(filePaths, {
573
+ projectId: options.project,
574
+ title: options.title,
575
+ dryRun: options.dryRun,
576
+ verbose: options.verbose
577
+ });
578
+ if (options.dryRun) {
579
+ print('[Dry Run] Would create combined transcript:');
580
+ print(` Output: ${result.outputPath}`);
581
+ print(` Size: ${result.content.length} characters`);
582
+ print('');
583
+ print('[Dry Run] Would delete source files:');
584
+ for (const fp of filePaths){
585
+ print(` - ${fp}`);
586
+ }
587
+ if (options.verbose) {
588
+ print('\n--- Preview (first 500 chars) ---');
589
+ print(result.content.slice(0, 500));
590
+ print('...');
591
+ }
592
+ } else {
593
+ // Ensure output directory exists
594
+ await fs.mkdir(path.dirname(result.outputPath), {
595
+ recursive: true
596
+ });
597
+ // Write the combined transcript
598
+ await fs.writeFile(result.outputPath, result.content, 'utf-8');
599
+ print(`Combined transcript created: ${result.outputPath}`);
600
+ // Automatically delete source files when combining
601
+ if (options.verbose) {
602
+ print('\nDeleting source files...');
603
+ }
604
+ for (const fp of filePaths){
605
+ try {
606
+ await fs.unlink(fp);
607
+ if (options.verbose) {
608
+ print(` Deleted: ${fp}`);
609
+ }
610
+ } catch (error) {
611
+ print(` Warning: Could not delete ${fp}: ${error}`);
612
+ }
613
+ }
614
+ print(`Deleted ${filePaths.length} source files.`);
615
+ }
616
+ } catch (error) {
617
+ print(`Error: ${error instanceof Error ? error.message : error}`);
618
+ process.exit(1);
619
+ }
620
+ } else if (file) {
621
+ // Edit mode - single file
622
+ if (!options.title && !options.project) {
623
+ print('Error: Must specify --title and/or --project when editing a single file.');
624
+ process.exit(1);
625
+ }
626
+ // Validate file exists
627
+ try {
628
+ await fs.access(file);
629
+ } catch {
630
+ print(`Error: File not found: ${file}`);
631
+ process.exit(1);
632
+ }
633
+ if (options.verbose) {
634
+ print(`\n[Editing transcript]`);
635
+ print(` File: ${file}`);
636
+ if (options.title) {
637
+ print(` New title: ${options.title}`);
638
+ }
639
+ if (options.project) {
640
+ print(` New project: ${options.project}`);
641
+ }
642
+ print('');
643
+ }
644
+ try {
645
+ const result = await editTranscript(file, {
646
+ title: options.title,
647
+ projectId: options.project,
648
+ dryRun: options.dryRun,
649
+ verbose: options.verbose
650
+ });
651
+ const isRename = result.outputPath !== file;
652
+ if (options.dryRun) {
653
+ print('[Dry Run] Would update transcript:');
654
+ if (isRename) {
655
+ print(` From: ${file}`);
656
+ print(` To: ${result.outputPath}`);
657
+ } else {
658
+ print(` File: ${result.outputPath}`);
659
+ }
660
+ print(` Size: ${result.content.length} characters`);
661
+ if (options.verbose) {
662
+ print('\n--- Preview (first 500 chars) ---');
663
+ print(result.content.slice(0, 500));
664
+ print('...');
665
+ }
666
+ } else {
667
+ // Ensure output directory exists
668
+ await fs.mkdir(path.dirname(result.outputPath), {
669
+ recursive: true
670
+ });
671
+ // Write the updated transcript
672
+ await fs.writeFile(result.outputPath, result.content, 'utf-8');
673
+ // Delete original if renamed
674
+ if (isRename) {
675
+ await fs.unlink(file);
676
+ print(`Transcript updated and renamed:`);
677
+ print(` From: ${file}`);
678
+ print(` To: ${result.outputPath}`);
679
+ } else {
680
+ print(`Transcript updated: ${result.outputPath}`);
681
+ }
682
+ }
683
+ } catch (error) {
684
+ print(`Error: ${error instanceof Error ? error.message : error}`);
685
+ process.exit(1);
686
+ }
687
+ } else {
688
+ print('Error: Must specify either a file to edit or --combine with files to combine.');
689
+ print('');
690
+ print('Usage:');
691
+ print(' protokoll action --title "New Title" /path/to/file.md');
692
+ print(' protokoll action --combine "/path/to/file1.md\\n/path/to/file2.md"');
693
+ process.exit(1);
694
+ }
695
+ };
696
+ /**
697
+ * Register the action command
698
+ */ const registerActionCommands = (program)=>{
699
+ const actionCmd = new Command('action').description('Edit a single transcript or combine multiple transcripts').argument('[file]', 'Transcript file to edit (when not using --combine)').option('-t, --title <title>', 'Set a custom title (also affects filename)').option('-p, --project <projectId>', 'Change to a different project (updates metadata and routing)').option('-c, --combine <files>', 'Combine multiple files (newline-separated list)').option('--dry-run', 'Show what would happen without making changes').option('-v, --verbose', 'Show detailed output').action(executeAction);
700
+ program.addCommand(actionCmd);
701
+ };
702
+
703
+ export { combineTranscripts, editTranscript, extractTimestampFromFilename, formatMetadataMarkdown, parseFilePaths, parseTranscript, registerActionCommands, slugifyTitle };
704
+ //# sourceMappingURL=action.js.map