@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.
- package/.cursor/rules/definition-of-done.md +1 -0
- package/.cursor/rules/no-emoticons.md +26 -12
- package/README.md +483 -69
- package/dist/agentic/executor.js +473 -41
- package/dist/agentic/executor.js.map +1 -1
- package/dist/agentic/index.js.map +1 -1
- package/dist/agentic/tools/lookup-person.js +123 -4
- package/dist/agentic/tools/lookup-person.js.map +1 -1
- package/dist/agentic/tools/lookup-project.js +139 -22
- package/dist/agentic/tools/lookup-project.js.map +1 -1
- package/dist/agentic/tools/route-note.js +5 -1
- package/dist/agentic/tools/route-note.js.map +1 -1
- package/dist/arguments.js +6 -3
- package/dist/arguments.js.map +1 -1
- package/dist/cli/action.js +704 -0
- package/dist/cli/action.js.map +1 -0
- package/dist/cli/config.js +482 -0
- package/dist/cli/config.js.map +1 -0
- package/dist/cli/context.js +466 -0
- package/dist/cli/context.js.map +1 -0
- package/dist/cli/feedback.js +858 -0
- package/dist/cli/feedback.js.map +1 -0
- package/dist/cli/index.js +103 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/install.js +572 -0
- package/dist/cli/install.js.map +1 -0
- package/dist/cli/transcript.js +199 -0
- package/dist/cli/transcript.js.map +1 -0
- package/dist/constants.js +11 -4
- package/dist/constants.js.map +1 -1
- package/dist/context/index.js +25 -1
- package/dist/context/index.js.map +1 -1
- package/dist/context/storage.js +56 -3
- package/dist/context/storage.js.map +1 -1
- package/dist/interactive/handler.js +310 -9
- package/dist/interactive/handler.js.map +1 -1
- package/dist/main.js +11 -1
- package/dist/main.js.map +1 -1
- package/dist/output/index.js.map +1 -1
- package/dist/output/manager.js +46 -1
- package/dist/output/manager.js.map +1 -1
- package/dist/phases/complete.js +37 -2
- package/dist/phases/complete.js.map +1 -1
- package/dist/pipeline/orchestrator.js +104 -31
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/protokoll.js +68 -2
- package/dist/protokoll.js.map +1 -1
- package/dist/reasoning/client.js +83 -0
- package/dist/reasoning/client.js.map +1 -1
- package/dist/reasoning/index.js +1 -0
- package/dist/reasoning/index.js.map +1 -1
- package/dist/util/metadata.js.map +1 -1
- package/dist/util/sound.js +116 -0
- package/dist/util/sound.js.map +1 -0
- package/docs/duplicate-question-prevention.md +117 -0
- package/docs/examples.md +152 -0
- package/docs/interactive-context-example.md +92 -0
- package/docs/package-lock.json +6 -0
- package/docs/package.json +3 -1
- package/guide/action.md +375 -0
- package/guide/config.md +207 -0
- package/guide/configuration.md +82 -67
- package/guide/context-commands.md +574 -0
- package/guide/context-system.md +20 -7
- package/guide/development.md +106 -4
- package/guide/feedback.md +335 -0
- package/guide/index.md +100 -4
- package/guide/interactive.md +15 -14
- package/guide/quickstart.md +21 -7
- package/guide/reasoning.md +18 -4
- package/guide/routing.md +192 -97
- package/package.json +1 -1
- package/scripts/coverage-priority.mjs +323 -0
- package/tsconfig.tsbuildinfo +1 -1
- 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
|