@redaksjon/protokoll 1.0.2 → 1.0.8
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/BUG_FIX_CONTEXT_DIRECTORY.md +147 -0
- package/README.md +4 -0
- package/dist/main.js +4 -4
- package/dist/main.js.map +1 -1
- package/dist/mcp/server.js +622 -187
- package/dist/mcp/server.js.map +1 -1
- package/dist/term-assist.js +2 -2
- package/dist/term-context.js +2 -2
- package/dist/transcript.js +138 -45
- package/dist/transcript.js.map +1 -1
- package/docs/MCP_OVERVIEW.md +475 -0
- package/docs/MCP_RESOURCES.md +301 -0
- package/docs/MCP_RESOURCES_IMPLEMENTATION.md +278 -0
- package/docs/MCP_RESOURCES_QUICK_START.md +280 -0
- package/docs/MCP_RESOURCES_REFACTOR.md +200 -0
- package/docs/MCP_SMART_TRANSCRIPT_LOOKUP.md +562 -0
- package/docs/MCP_WORKSPACE_CONFIG.md +355 -0
- package/guide/architecture.md +3 -3
- package/guide/development.md +3 -3
- package/package.json +7 -7
- package/vite.config.ts +3 -3
package/dist/mcp/server.js
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
|
|
4
4
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
5
|
-
import { ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
5
|
+
import { ListRootsRequestSchema, ListToolsRequestSchema, CallToolRequestSchema, ListResourcesRequestSchema, ReadResourceRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from '@modelcontextprotocol/sdk/types.js';
|
|
6
6
|
import { fileURLToPath } from 'node:url';
|
|
7
|
-
import { resolve, dirname } from 'node:path';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { resolve, extname, join, dirname, basename } from 'node:path';
|
|
8
|
+
import { q as create, B as listTranscripts, m as DEFAULT_AUDIO_EXTENSIONS, d as create$1, c as create$2, g as getLogger, l as DEFAULT_OUTPUT_STRUCTURE, k as DEFAULT_OUTPUT_FILENAME_OPTIONS, p as create$3, w as DEFAULT_TRANSCRIPTION_MODEL, v as DEFAULT_MODEL, C as DEFAULT_TEMP_DIRECTORY, b as DEFAULT_MAX_AUDIO_SIZE, e as DEFAULT_INTERMEDIATE_DIRECTORY, D as DEFAULT_REASONING_LEVEL, r as create$4, E as create$6, F as create$8, G as processFeedback, H as applyChanges, I as combineTranscripts, J as editTranscript, K as parseTranscript, P as PROGRAM_NAME, V as VERSION } from '../transcript.js';
|
|
9
|
+
import { readFile, readdir, stat, mkdir, writeFile, unlink } from 'node:fs/promises';
|
|
10
10
|
import * as yaml from 'js-yaml';
|
|
11
11
|
import { readFileSync } from 'node:fs';
|
|
12
12
|
import { glob } from 'glob';
|
|
@@ -18,12 +18,12 @@ import 'fs/promises';
|
|
|
18
18
|
import 'openai';
|
|
19
19
|
import 'fluent-ffmpeg';
|
|
20
20
|
import 'node:os';
|
|
21
|
-
import '@
|
|
21
|
+
import '@kjerneverk/riotprompt';
|
|
22
22
|
import 'zod';
|
|
23
23
|
import 'node:crypto';
|
|
24
24
|
import 'html-to-text';
|
|
25
25
|
import 'commander';
|
|
26
|
-
import '@
|
|
26
|
+
import '@utilarium/overcontext';
|
|
27
27
|
import '@redaksjon/context';
|
|
28
28
|
import 'winston';
|
|
29
29
|
|
|
@@ -53,6 +53,8 @@ function parseUri(uri) {
|
|
|
53
53
|
case "entities":
|
|
54
54
|
case "entities-list":
|
|
55
55
|
return parseEntitiesListUri(uri, segments, params);
|
|
56
|
+
case "audio":
|
|
57
|
+
return parseAudioUri(uri, segments, params);
|
|
56
58
|
default:
|
|
57
59
|
throw new Error(`Unknown resource type: ${firstSegment}`);
|
|
58
60
|
}
|
|
@@ -135,6 +137,27 @@ function parseEntitiesListUri(uri, segments, params) {
|
|
|
135
137
|
entityType
|
|
136
138
|
};
|
|
137
139
|
}
|
|
140
|
+
function parseAudioUri(uri, segments, params) {
|
|
141
|
+
const audioType = segments[1];
|
|
142
|
+
if (audioType === "inbound") {
|
|
143
|
+
return {
|
|
144
|
+
scheme: SCHEME,
|
|
145
|
+
resourceType: "audio-inbound",
|
|
146
|
+
path: "audio/inbound",
|
|
147
|
+
params,
|
|
148
|
+
directory: params.directory
|
|
149
|
+
};
|
|
150
|
+
} else if (audioType === "processed") {
|
|
151
|
+
return {
|
|
152
|
+
scheme: SCHEME,
|
|
153
|
+
resourceType: "audio-processed",
|
|
154
|
+
path: "audio/processed",
|
|
155
|
+
params,
|
|
156
|
+
directory: params.directory
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
throw new Error(`Invalid audio URI: ${uri}. Expected protokoll://audio/inbound or protokoll://audio/processed`);
|
|
160
|
+
}
|
|
138
161
|
function buildTranscriptUri(transcriptPath) {
|
|
139
162
|
const encoded = encodeURIComponent(transcriptPath).replace(/%2F/g, "/");
|
|
140
163
|
return `${SCHEME}://transcript/${encoded}`;
|
|
@@ -160,6 +183,18 @@ function buildTranscriptsListUri(options) {
|
|
|
160
183
|
function buildEntitiesListUri(entityType) {
|
|
161
184
|
return `${SCHEME}://entities/${entityType}`;
|
|
162
185
|
}
|
|
186
|
+
function buildAudioInboundUri(directory) {
|
|
187
|
+
if (directory) {
|
|
188
|
+
return `${SCHEME}://audio/inbound?directory=${encodeURIComponent(directory)}`;
|
|
189
|
+
}
|
|
190
|
+
return `${SCHEME}://audio/inbound`;
|
|
191
|
+
}
|
|
192
|
+
function buildAudioProcessedUri(directory) {
|
|
193
|
+
if (directory) {
|
|
194
|
+
return `${SCHEME}://audio/processed?directory=${encodeURIComponent(directory)}`;
|
|
195
|
+
}
|
|
196
|
+
return `${SCHEME}://audio/processed`;
|
|
197
|
+
}
|
|
163
198
|
|
|
164
199
|
const directResources = [
|
|
165
200
|
// Will be populated dynamically based on context
|
|
@@ -194,15 +229,21 @@ const resourceTemplates = [
|
|
|
194
229
|
name: "Entities List",
|
|
195
230
|
description: "List of entities of a given type",
|
|
196
231
|
mimeType: "application/json"
|
|
232
|
+
},
|
|
233
|
+
{
|
|
234
|
+
uriTemplate: "protokoll://audio/inbound?directory={directory}",
|
|
235
|
+
name: "Inbound Audio Files",
|
|
236
|
+
description: "List of audio files waiting to be processed",
|
|
237
|
+
mimeType: "application/json"
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
uriTemplate: "protokoll://audio/processed?directory={directory}",
|
|
241
|
+
name: "Processed Audio Files",
|
|
242
|
+
description: "List of audio files that have been processed",
|
|
243
|
+
mimeType: "application/json"
|
|
197
244
|
}
|
|
198
245
|
];
|
|
199
|
-
|
|
200
|
-
const dynamicResources = await getDynamicResources(contextDirectory);
|
|
201
|
-
return {
|
|
202
|
-
resources: [...directResources, ...dynamicResources],
|
|
203
|
-
resourceTemplates
|
|
204
|
-
};
|
|
205
|
-
}
|
|
246
|
+
|
|
206
247
|
async function getDynamicResources(contextDirectory) {
|
|
207
248
|
const resources = [];
|
|
208
249
|
try {
|
|
@@ -212,6 +253,7 @@ async function getDynamicResources(contextDirectory) {
|
|
|
212
253
|
if (!context.hasContext()) {
|
|
213
254
|
return resources;
|
|
214
255
|
}
|
|
256
|
+
const config = context.getConfig();
|
|
215
257
|
const dirs = context.getDiscoveredDirs();
|
|
216
258
|
const configPath = dirs[0]?.path;
|
|
217
259
|
if (configPath) {
|
|
@@ -222,37 +264,72 @@ async function getDynamicResources(contextDirectory) {
|
|
|
222
264
|
mimeType: "application/json"
|
|
223
265
|
});
|
|
224
266
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
267
|
+
const inputDirectory = config.inputDirectory || "./recordings";
|
|
268
|
+
resources.push({
|
|
269
|
+
uri: buildAudioInboundUri(inputDirectory),
|
|
270
|
+
name: "Inbound Audio Files",
|
|
271
|
+
description: `Audio files waiting to be processed in ${inputDirectory}`,
|
|
272
|
+
mimeType: "application/json"
|
|
273
|
+
});
|
|
274
|
+
const processedDirectory = config.processedDirectory;
|
|
275
|
+
if (processedDirectory) {
|
|
276
|
+
resources.push({
|
|
277
|
+
uri: buildAudioProcessedUri(processedDirectory),
|
|
278
|
+
name: "Processed Audio Files",
|
|
279
|
+
description: `Audio files that have been processed in ${processedDirectory}`,
|
|
280
|
+
mimeType: "application/json"
|
|
281
|
+
});
|
|
237
282
|
}
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
283
|
+
const entityCounts = {
|
|
284
|
+
projects: context.getAllProjects().length,
|
|
285
|
+
people: context.getAllPeople().length,
|
|
286
|
+
terms: context.getAllTerms().length,
|
|
287
|
+
companies: context.getAllCompanies().length
|
|
288
|
+
};
|
|
289
|
+
if (entityCounts.projects > 0) {
|
|
290
|
+
resources.push({
|
|
291
|
+
uri: buildEntitiesListUri("project"),
|
|
292
|
+
name: "All Projects",
|
|
293
|
+
description: `${entityCounts.projects} project(s) in context`,
|
|
294
|
+
mimeType: "application/json"
|
|
248
295
|
});
|
|
249
296
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
297
|
+
if (entityCounts.people > 0) {
|
|
298
|
+
resources.push({
|
|
299
|
+
uri: buildEntitiesListUri("person"),
|
|
300
|
+
name: "All People",
|
|
301
|
+
description: `${entityCounts.people} person/people in context`,
|
|
302
|
+
mimeType: "application/json"
|
|
303
|
+
});
|
|
304
|
+
}
|
|
305
|
+
if (entityCounts.terms > 0) {
|
|
306
|
+
resources.push({
|
|
307
|
+
uri: buildEntitiesListUri("term"),
|
|
308
|
+
name: "All Terms",
|
|
309
|
+
description: `${entityCounts.terms} term(s) in context`,
|
|
310
|
+
mimeType: "application/json"
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
if (entityCounts.companies > 0) {
|
|
314
|
+
resources.push({
|
|
315
|
+
uri: buildEntitiesListUri("company"),
|
|
316
|
+
name: "All Companies",
|
|
317
|
+
description: `${entityCounts.companies} company/companies in context`,
|
|
318
|
+
mimeType: "application/json"
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
const outputDirectory = config.outputDirectory || "~/notes";
|
|
322
|
+
resources.push({
|
|
323
|
+
uri: buildTranscriptsListUri({ directory: outputDirectory, limit: 10 }),
|
|
324
|
+
name: "Recent Transcripts",
|
|
325
|
+
description: `10 most recent transcripts in ${outputDirectory}`,
|
|
326
|
+
mimeType: "application/json"
|
|
327
|
+
});
|
|
328
|
+
} catch {
|
|
254
329
|
}
|
|
330
|
+
return resources;
|
|
255
331
|
}
|
|
332
|
+
|
|
256
333
|
async function readTranscriptResource(transcriptPath) {
|
|
257
334
|
const fullPath = transcriptPath.startsWith("/") ? transcriptPath : resolve(process.cwd(), transcriptPath);
|
|
258
335
|
try {
|
|
@@ -269,84 +346,6 @@ async function readTranscriptResource(transcriptPath) {
|
|
|
269
346
|
throw error;
|
|
270
347
|
}
|
|
271
348
|
}
|
|
272
|
-
async function readEntityResource(entityType, entityId, contextDirectory) {
|
|
273
|
-
const context = await create({
|
|
274
|
-
startingDir: process.cwd()
|
|
275
|
-
});
|
|
276
|
-
let entity;
|
|
277
|
-
switch (entityType) {
|
|
278
|
-
case "person":
|
|
279
|
-
entity = context.getPerson(entityId);
|
|
280
|
-
break;
|
|
281
|
-
case "project":
|
|
282
|
-
entity = context.getProject(entityId);
|
|
283
|
-
break;
|
|
284
|
-
case "term":
|
|
285
|
-
entity = context.getTerm(entityId);
|
|
286
|
-
break;
|
|
287
|
-
case "company":
|
|
288
|
-
entity = context.getCompany(entityId);
|
|
289
|
-
break;
|
|
290
|
-
case "ignored":
|
|
291
|
-
entity = context.getIgnored(entityId);
|
|
292
|
-
break;
|
|
293
|
-
default:
|
|
294
|
-
throw new Error(`Unknown entity type: ${entityType}`);
|
|
295
|
-
}
|
|
296
|
-
if (!entity) {
|
|
297
|
-
throw new Error(`${entityType} "${entityId}" not found`);
|
|
298
|
-
}
|
|
299
|
-
const yamlContent = yaml.dump(entity);
|
|
300
|
-
return {
|
|
301
|
-
uri: buildEntityUri(entityType, entityId),
|
|
302
|
-
mimeType: "application/yaml",
|
|
303
|
-
text: yamlContent
|
|
304
|
-
};
|
|
305
|
-
}
|
|
306
|
-
async function readConfigResource(configPath) {
|
|
307
|
-
const startDir = configPath || process.cwd();
|
|
308
|
-
const context = await create({
|
|
309
|
-
startingDir: startDir
|
|
310
|
-
});
|
|
311
|
-
if (!context.hasContext()) {
|
|
312
|
-
throw new Error(`No Protokoll context found at or above: ${startDir}`);
|
|
313
|
-
}
|
|
314
|
-
const dirs = context.getDiscoveredDirs();
|
|
315
|
-
const config = context.getConfig();
|
|
316
|
-
const configData = {
|
|
317
|
-
hasContext: true,
|
|
318
|
-
discoveredDirectories: dirs.map((d) => ({
|
|
319
|
-
path: d.path,
|
|
320
|
-
level: d.level,
|
|
321
|
-
isPrimary: d.level === 0
|
|
322
|
-
})),
|
|
323
|
-
entityCounts: {
|
|
324
|
-
projects: context.getAllProjects().length,
|
|
325
|
-
people: context.getAllPeople().length,
|
|
326
|
-
terms: context.getAllTerms().length,
|
|
327
|
-
companies: context.getAllCompanies().length,
|
|
328
|
-
ignored: context.getAllIgnored().length
|
|
329
|
-
},
|
|
330
|
-
config: {
|
|
331
|
-
outputDirectory: config.outputDirectory,
|
|
332
|
-
outputStructure: config.outputStructure,
|
|
333
|
-
model: config.model,
|
|
334
|
-
smartAssistance: context.getSmartAssistanceConfig()
|
|
335
|
-
},
|
|
336
|
-
// Include URIs for easy navigation
|
|
337
|
-
resourceUris: {
|
|
338
|
-
projects: buildEntitiesListUri("project"),
|
|
339
|
-
people: buildEntitiesListUri("person"),
|
|
340
|
-
terms: buildEntitiesListUri("term"),
|
|
341
|
-
companies: buildEntitiesListUri("company")
|
|
342
|
-
}
|
|
343
|
-
};
|
|
344
|
-
return {
|
|
345
|
-
uri: buildConfigUri(configPath),
|
|
346
|
-
mimeType: "application/json",
|
|
347
|
-
text: JSON.stringify(configData, null, 2)
|
|
348
|
-
};
|
|
349
|
-
}
|
|
350
349
|
async function readTranscriptsListResource(options) {
|
|
351
350
|
const { directory, startDate, endDate, limit = 50, offset = 0 } = options;
|
|
352
351
|
if (!directory) {
|
|
@@ -388,6 +387,41 @@ async function readTranscriptsListResource(options) {
|
|
|
388
387
|
text: JSON.stringify(responseData, null, 2)
|
|
389
388
|
};
|
|
390
389
|
}
|
|
390
|
+
|
|
391
|
+
async function readEntityResource(entityType, entityId, contextDirectory) {
|
|
392
|
+
const context = await create({
|
|
393
|
+
startingDir: process.cwd()
|
|
394
|
+
});
|
|
395
|
+
let entity;
|
|
396
|
+
switch (entityType) {
|
|
397
|
+
case "person":
|
|
398
|
+
entity = context.getPerson(entityId);
|
|
399
|
+
break;
|
|
400
|
+
case "project":
|
|
401
|
+
entity = context.getProject(entityId);
|
|
402
|
+
break;
|
|
403
|
+
case "term":
|
|
404
|
+
entity = context.getTerm(entityId);
|
|
405
|
+
break;
|
|
406
|
+
case "company":
|
|
407
|
+
entity = context.getCompany(entityId);
|
|
408
|
+
break;
|
|
409
|
+
case "ignored":
|
|
410
|
+
entity = context.getIgnored(entityId);
|
|
411
|
+
break;
|
|
412
|
+
default:
|
|
413
|
+
throw new Error(`Unknown entity type: ${entityType}`);
|
|
414
|
+
}
|
|
415
|
+
if (!entity) {
|
|
416
|
+
throw new Error(`${entityType} "${entityId}" not found`);
|
|
417
|
+
}
|
|
418
|
+
const yamlContent = yaml.dump(entity);
|
|
419
|
+
return {
|
|
420
|
+
uri: buildEntityUri(entityType, entityId),
|
|
421
|
+
mimeType: "application/yaml",
|
|
422
|
+
text: yamlContent
|
|
423
|
+
};
|
|
424
|
+
}
|
|
391
425
|
async function readEntitiesListResource(entityType, contextDirectory) {
|
|
392
426
|
const context = await create({
|
|
393
427
|
startingDir: process.cwd()
|
|
@@ -456,6 +490,190 @@ async function readEntitiesListResource(entityType, contextDirectory) {
|
|
|
456
490
|
};
|
|
457
491
|
}
|
|
458
492
|
|
|
493
|
+
async function listAudioFiles(directory) {
|
|
494
|
+
const dirPath = resolve(directory);
|
|
495
|
+
try {
|
|
496
|
+
const entries = await readdir(dirPath, { withFileTypes: true });
|
|
497
|
+
const audioFiles = [];
|
|
498
|
+
for (const entry of entries) {
|
|
499
|
+
if (entry.isFile()) {
|
|
500
|
+
const ext = extname(entry.name).toLowerCase().substring(1);
|
|
501
|
+
if (DEFAULT_AUDIO_EXTENSIONS.includes(ext)) {
|
|
502
|
+
const filePath = join(dirPath, entry.name);
|
|
503
|
+
const stats = await stat(filePath);
|
|
504
|
+
audioFiles.push({
|
|
505
|
+
filename: entry.name,
|
|
506
|
+
path: filePath,
|
|
507
|
+
size: stats.size,
|
|
508
|
+
modified: stats.mtime.toISOString(),
|
|
509
|
+
extension: ext
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
audioFiles.sort(
|
|
515
|
+
(a, b) => new Date(b.modified).getTime() - new Date(a.modified).getTime()
|
|
516
|
+
);
|
|
517
|
+
return audioFiles;
|
|
518
|
+
} catch (error) {
|
|
519
|
+
if (error.code === "ENOENT") {
|
|
520
|
+
return [];
|
|
521
|
+
}
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
function formatBytes(bytes) {
|
|
526
|
+
if (bytes === 0) return "0 B";
|
|
527
|
+
const k = 1024;
|
|
528
|
+
const sizes = ["B", "KB", "MB", "GB"];
|
|
529
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
530
|
+
return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`;
|
|
531
|
+
}
|
|
532
|
+
async function readAudioInboundResource(directory) {
|
|
533
|
+
const context = await create({
|
|
534
|
+
startingDir: directory || process.cwd()
|
|
535
|
+
});
|
|
536
|
+
if (!context.hasContext()) {
|
|
537
|
+
throw new Error("No Protokoll context found");
|
|
538
|
+
}
|
|
539
|
+
const config = context.getConfig();
|
|
540
|
+
const inputDirectory = directory || config.inputDirectory || "./recordings";
|
|
541
|
+
const audioFiles = await listAudioFiles(inputDirectory);
|
|
542
|
+
const responseData = {
|
|
543
|
+
directory: resolve(inputDirectory),
|
|
544
|
+
count: audioFiles.length,
|
|
545
|
+
totalSize: audioFiles.reduce((sum, f) => sum + f.size, 0),
|
|
546
|
+
files: audioFiles.map((f) => ({
|
|
547
|
+
filename: f.filename,
|
|
548
|
+
path: f.path,
|
|
549
|
+
size: f.size,
|
|
550
|
+
sizeHuman: formatBytes(f.size),
|
|
551
|
+
modified: f.modified,
|
|
552
|
+
extension: f.extension
|
|
553
|
+
})),
|
|
554
|
+
supportedExtensions: DEFAULT_AUDIO_EXTENSIONS
|
|
555
|
+
};
|
|
556
|
+
return {
|
|
557
|
+
uri: buildAudioInboundUri(inputDirectory),
|
|
558
|
+
mimeType: "application/json",
|
|
559
|
+
text: JSON.stringify(responseData, null, 2)
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
async function readAudioProcessedResource(directory) {
|
|
563
|
+
const context = await create({
|
|
564
|
+
startingDir: directory || process.cwd()
|
|
565
|
+
});
|
|
566
|
+
if (!context.hasContext()) {
|
|
567
|
+
throw new Error("No Protokoll context found");
|
|
568
|
+
}
|
|
569
|
+
const config = context.getConfig();
|
|
570
|
+
const processedDirectory = directory || config.processedDirectory || "./processed";
|
|
571
|
+
const audioFiles = await listAudioFiles(processedDirectory);
|
|
572
|
+
const responseData = {
|
|
573
|
+
directory: resolve(processedDirectory),
|
|
574
|
+
count: audioFiles.length,
|
|
575
|
+
totalSize: audioFiles.reduce((sum, f) => sum + f.size, 0),
|
|
576
|
+
files: audioFiles.map((f) => ({
|
|
577
|
+
filename: f.filename,
|
|
578
|
+
path: f.path,
|
|
579
|
+
size: f.size,
|
|
580
|
+
sizeHuman: formatBytes(f.size),
|
|
581
|
+
modified: f.modified,
|
|
582
|
+
extension: f.extension
|
|
583
|
+
})),
|
|
584
|
+
supportedExtensions: DEFAULT_AUDIO_EXTENSIONS
|
|
585
|
+
};
|
|
586
|
+
return {
|
|
587
|
+
uri: buildAudioProcessedUri(processedDirectory),
|
|
588
|
+
mimeType: "application/json",
|
|
589
|
+
text: JSON.stringify(responseData, null, 2)
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async function readConfigResource(configPath) {
|
|
594
|
+
const startDir = configPath || process.cwd();
|
|
595
|
+
const context = await create({
|
|
596
|
+
startingDir: startDir
|
|
597
|
+
});
|
|
598
|
+
if (!context.hasContext()) {
|
|
599
|
+
throw new Error(`No Protokoll context found at or above: ${startDir}`);
|
|
600
|
+
}
|
|
601
|
+
const dirs = context.getDiscoveredDirs();
|
|
602
|
+
const config = context.getConfig();
|
|
603
|
+
const configData = {
|
|
604
|
+
hasContext: true,
|
|
605
|
+
discoveredDirectories: dirs.map((d) => ({
|
|
606
|
+
path: d.path,
|
|
607
|
+
level: d.level,
|
|
608
|
+
isPrimary: d.level === 0
|
|
609
|
+
})),
|
|
610
|
+
entityCounts: {
|
|
611
|
+
projects: context.getAllProjects().length,
|
|
612
|
+
people: context.getAllPeople().length,
|
|
613
|
+
terms: context.getAllTerms().length,
|
|
614
|
+
companies: context.getAllCompanies().length,
|
|
615
|
+
ignored: context.getAllIgnored().length
|
|
616
|
+
},
|
|
617
|
+
config: {
|
|
618
|
+
outputDirectory: config.outputDirectory,
|
|
619
|
+
outputStructure: config.outputStructure,
|
|
620
|
+
model: config.model,
|
|
621
|
+
smartAssistance: context.getSmartAssistanceConfig()
|
|
622
|
+
},
|
|
623
|
+
// Include URIs for easy navigation
|
|
624
|
+
resourceUris: {
|
|
625
|
+
projects: buildEntitiesListUri("project"),
|
|
626
|
+
people: buildEntitiesListUri("person"),
|
|
627
|
+
terms: buildEntitiesListUri("term"),
|
|
628
|
+
companies: buildEntitiesListUri("company")
|
|
629
|
+
}
|
|
630
|
+
};
|
|
631
|
+
return {
|
|
632
|
+
uri: buildConfigUri(configPath),
|
|
633
|
+
mimeType: "application/json",
|
|
634
|
+
text: JSON.stringify(configData, null, 2)
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
async function handleListResources(contextDirectory) {
|
|
639
|
+
const dynamicResources = await getDynamicResources(contextDirectory);
|
|
640
|
+
return {
|
|
641
|
+
resources: [...directResources, ...dynamicResources],
|
|
642
|
+
resourceTemplates
|
|
643
|
+
};
|
|
644
|
+
}
|
|
645
|
+
async function handleReadResource(uri) {
|
|
646
|
+
const parsed = parseUri(uri);
|
|
647
|
+
switch (parsed.resourceType) {
|
|
648
|
+
case "transcript":
|
|
649
|
+
return readTranscriptResource(parsed.transcriptPath);
|
|
650
|
+
case "entity": {
|
|
651
|
+
const entityUri = parsed;
|
|
652
|
+
return readEntityResource(entityUri.entityType, entityUri.entityId);
|
|
653
|
+
}
|
|
654
|
+
case "config":
|
|
655
|
+
return readConfigResource(parsed.configPath);
|
|
656
|
+
case "transcripts-list": {
|
|
657
|
+
const listUri = parsed;
|
|
658
|
+
return readTranscriptsListResource({
|
|
659
|
+
directory: listUri.directory,
|
|
660
|
+
startDate: listUri.startDate,
|
|
661
|
+
endDate: listUri.endDate,
|
|
662
|
+
limit: listUri.limit,
|
|
663
|
+
offset: listUri.offset
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
case "entities-list":
|
|
667
|
+
return readEntitiesListResource(parsed.entityType);
|
|
668
|
+
case "audio-inbound":
|
|
669
|
+
return readAudioInboundResource(parsed.directory);
|
|
670
|
+
case "audio-processed":
|
|
671
|
+
return readAudioProcessedResource(parsed.directory);
|
|
672
|
+
default:
|
|
673
|
+
throw new Error(`Unknown resource type: ${parsed.resourceType}`);
|
|
674
|
+
}
|
|
675
|
+
}
|
|
676
|
+
|
|
459
677
|
const __filename$1 = fileURLToPath(import.meta.url);
|
|
460
678
|
const __dirname$1 = dirname(__filename$1);
|
|
461
679
|
function getPromptsDir() {
|
|
@@ -898,6 +1116,17 @@ async function fileExists(path) {
|
|
|
898
1116
|
return false;
|
|
899
1117
|
}
|
|
900
1118
|
}
|
|
1119
|
+
async function getConfiguredDirectory(key, _contextDirectory) {
|
|
1120
|
+
const ServerConfig = await Promise.resolve().then(() => serverConfig$1);
|
|
1121
|
+
switch (key) {
|
|
1122
|
+
case "inputDirectory":
|
|
1123
|
+
return ServerConfig.getInputDirectory();
|
|
1124
|
+
case "outputDirectory":
|
|
1125
|
+
return ServerConfig.getOutputDirectory();
|
|
1126
|
+
case "processedDirectory":
|
|
1127
|
+
return ServerConfig.getProcessedDirectory() || resolve(process.cwd(), "./processed");
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
901
1130
|
function slugify(text) {
|
|
902
1131
|
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/--+/g, "-").replace(/^-|-$/g, "");
|
|
903
1132
|
}
|
|
@@ -1151,19 +1380,46 @@ async function handleSuggestProject(args) {
|
|
|
1151
1380
|
};
|
|
1152
1381
|
}
|
|
1153
1382
|
|
|
1383
|
+
async function findAudioFile(filenameOrPath) {
|
|
1384
|
+
if (filenameOrPath.startsWith("/") && await fileExists(filenameOrPath)) {
|
|
1385
|
+
return filenameOrPath;
|
|
1386
|
+
}
|
|
1387
|
+
const inputDirectory = await getConfiguredDirectory("inputDirectory");
|
|
1388
|
+
const entries = await readdir(inputDirectory, { withFileTypes: true });
|
|
1389
|
+
const matches = [];
|
|
1390
|
+
for (const entry of entries) {
|
|
1391
|
+
if (entry.isFile()) {
|
|
1392
|
+
const filename = entry.name;
|
|
1393
|
+
const ext = filename.split(".").pop()?.toLowerCase();
|
|
1394
|
+
if (ext && DEFAULT_AUDIO_EXTENSIONS.includes(ext)) {
|
|
1395
|
+
if (filename === filenameOrPath || filename.includes(filenameOrPath) || basename(filename, `.${ext}`) === filenameOrPath) {
|
|
1396
|
+
matches.push(join(inputDirectory, filename));
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
if (matches.length === 0) {
|
|
1402
|
+
throw new Error(
|
|
1403
|
+
`No audio file found matching "${filenameOrPath}" in ${inputDirectory}. Try using the protokoll://audio/inbound resource to see available audio files.`
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
if (matches.length === 1) {
|
|
1407
|
+
return matches[0];
|
|
1408
|
+
}
|
|
1409
|
+
const matchNames = matches.map((m) => basename(m)).join(", ");
|
|
1410
|
+
throw new Error(
|
|
1411
|
+
`Multiple audio files match "${filenameOrPath}": ${matchNames}. Please be more specific.`
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1154
1414
|
const processAudioTool = {
|
|
1155
1415
|
name: "protokoll_process_audio",
|
|
1156
|
-
description: "Process an audio file through Protokoll's intelligent transcription pipeline.
|
|
1416
|
+
description: "Process an audio file through Protokoll's intelligent transcription pipeline. You can provide either an absolute path OR just a filename/partial filename. If you provide a filename, it will search in the workspace's configured input directory. This tool uses workspace-level configuration automatically - no need to specify directories. Transcribes audio using Whisper, then enhances it with context-aware processing that corrects names, terms, and routes the output to the appropriate project folder. Returns the enhanced transcript text and output file path.",
|
|
1157
1417
|
inputSchema: {
|
|
1158
1418
|
type: "object",
|
|
1159
1419
|
properties: {
|
|
1160
1420
|
audioFile: {
|
|
1161
1421
|
type: "string",
|
|
1162
|
-
description:
|
|
1163
|
-
},
|
|
1164
|
-
contextDirectory: {
|
|
1165
|
-
type: "string",
|
|
1166
|
-
description: "Path to the .protokoll context directory. If not specified, walks up from the audio file location to find one."
|
|
1422
|
+
description: 'Filename, partial filename, or absolute path to the audio file. Examples: "recording.m4a", "2026-01-29", "/full/path/to/audio.m4a"'
|
|
1167
1423
|
},
|
|
1168
1424
|
projectId: {
|
|
1169
1425
|
type: "string",
|
|
@@ -1171,7 +1427,7 @@ const processAudioTool = {
|
|
|
1171
1427
|
},
|
|
1172
1428
|
outputDirectory: {
|
|
1173
1429
|
type: "string",
|
|
1174
|
-
description: "Override the
|
|
1430
|
+
description: "Override the workspace output directory"
|
|
1175
1431
|
},
|
|
1176
1432
|
model: {
|
|
1177
1433
|
type: "string",
|
|
@@ -1187,44 +1443,40 @@ const processAudioTool = {
|
|
|
1187
1443
|
};
|
|
1188
1444
|
const batchProcessTool = {
|
|
1189
1445
|
name: "protokoll_batch_process",
|
|
1190
|
-
description: "Process multiple audio files in a directory. Finds all audio files matching the configured extensions and processes them sequentially. Returns a summary of all processed files with their output paths.",
|
|
1446
|
+
description: "Process multiple audio files in a directory. If no directory is specified, uses the workspace's configured input directory. This tool uses workspace-level configuration automatically - no need to specify directories. Finds all audio files matching the configured extensions and processes them sequentially. Returns a summary of all processed files with their output paths.",
|
|
1191
1447
|
inputSchema: {
|
|
1192
1448
|
type: "object",
|
|
1193
1449
|
properties: {
|
|
1194
1450
|
inputDirectory: {
|
|
1195
1451
|
type: "string",
|
|
1196
|
-
description: "
|
|
1452
|
+
description: "Optional: Directory containing audio files. If not specified, uses the workspace input directory."
|
|
1197
1453
|
},
|
|
1198
1454
|
extensions: {
|
|
1199
1455
|
type: "array",
|
|
1200
1456
|
items: { type: "string" },
|
|
1201
1457
|
description: 'Audio file extensions to process (default: [".m4a", ".mp3", ".wav", ".webm"])'
|
|
1202
1458
|
},
|
|
1203
|
-
contextDirectory: {
|
|
1204
|
-
type: "string",
|
|
1205
|
-
description: "Path to the .protokoll context directory"
|
|
1206
|
-
},
|
|
1207
1459
|
outputDirectory: {
|
|
1208
1460
|
type: "string",
|
|
1209
|
-
description: "Override the
|
|
1461
|
+
description: "Override the workspace output directory"
|
|
1210
1462
|
}
|
|
1211
1463
|
},
|
|
1212
|
-
required: [
|
|
1464
|
+
required: []
|
|
1213
1465
|
}
|
|
1214
1466
|
};
|
|
1215
1467
|
async function handleProcessAudio(args) {
|
|
1216
|
-
const
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
}
|
|
1223
|
-
const
|
|
1224
|
-
const outputDirectory = args.outputDirectory || config.outputDirectory
|
|
1225
|
-
const outputStructure =
|
|
1226
|
-
const outputFilenameOptions =
|
|
1227
|
-
const processedDirectory = config.processedDirectory
|
|
1468
|
+
const ServerConfig = await Promise.resolve().then(() => serverConfig$1);
|
|
1469
|
+
const audioFile = await findAudioFile(args.audioFile);
|
|
1470
|
+
const config = ServerConfig.getServerConfig();
|
|
1471
|
+
const context = config.context;
|
|
1472
|
+
if (!context) {
|
|
1473
|
+
throw new Error("Protokoll context not available. Ensure .protokoll directory exists in workspace.");
|
|
1474
|
+
}
|
|
1475
|
+
const contextConfig = context.getConfig();
|
|
1476
|
+
const outputDirectory = args.outputDirectory || config.outputDirectory;
|
|
1477
|
+
const outputStructure = contextConfig.outputStructure || DEFAULT_OUTPUT_STRUCTURE;
|
|
1478
|
+
const outputFilenameOptions = contextConfig.outputFilenameOptions || DEFAULT_OUTPUT_FILENAME_OPTIONS;
|
|
1479
|
+
const processedDirectory = config.processedDirectory ?? void 0;
|
|
1228
1480
|
const { creationTime, hash } = await getAudioMetadata(audioFile);
|
|
1229
1481
|
const pipeline = await create$3({
|
|
1230
1482
|
model: args.model || DEFAULT_MODEL,
|
|
@@ -1236,13 +1488,13 @@ async function handleProcessAudio(args) {
|
|
|
1236
1488
|
silent: true,
|
|
1237
1489
|
debug: false,
|
|
1238
1490
|
dryRun: false,
|
|
1239
|
-
contextDirectory:
|
|
1491
|
+
contextDirectory: config.workspaceRoot || void 0,
|
|
1240
1492
|
intermediateDir: DEFAULT_INTERMEDIATE_DIRECTORY,
|
|
1241
1493
|
keepIntermediates: false,
|
|
1242
1494
|
outputDirectory,
|
|
1243
1495
|
outputStructure,
|
|
1244
1496
|
outputFilenameOptions,
|
|
1245
|
-
processedDirectory,
|
|
1497
|
+
processedDirectory: processedDirectory || void 0,
|
|
1246
1498
|
maxAudioSize: DEFAULT_MAX_AUDIO_SIZE,
|
|
1247
1499
|
tempDirectory: DEFAULT_TEMP_DIRECTORY
|
|
1248
1500
|
});
|
|
@@ -1263,7 +1515,7 @@ async function handleProcessAudio(args) {
|
|
|
1263
1515
|
};
|
|
1264
1516
|
}
|
|
1265
1517
|
async function handleBatchProcess(args) {
|
|
1266
|
-
const inputDir = resolve(args.inputDirectory);
|
|
1518
|
+
const inputDir = args.inputDirectory ? resolve(args.inputDirectory) : await getConfiguredDirectory("inputDirectory");
|
|
1267
1519
|
const extensions = args.extensions || [".m4a", ".mp3", ".wav", ".webm"];
|
|
1268
1520
|
if (!await fileExists(inputDir)) {
|
|
1269
1521
|
throw new Error(`Input directory not found: ${inputDir}`);
|
|
@@ -1279,7 +1531,6 @@ async function handleBatchProcess(args) {
|
|
|
1279
1531
|
try {
|
|
1280
1532
|
const result = await handleProcessAudio({
|
|
1281
1533
|
audioFile: file,
|
|
1282
|
-
contextDirectory: args.contextDirectory,
|
|
1283
1534
|
outputDirectory: args.outputDirectory
|
|
1284
1535
|
});
|
|
1285
1536
|
processed.push(result);
|
|
@@ -2184,7 +2435,7 @@ async function handleEditPerson(args) {
|
|
|
2184
2435
|
} else if (existingPerson.sounds_like && (args.sounds_like !== void 0 || args.remove_sounds_like)) {
|
|
2185
2436
|
delete updatedPerson.sounds_like;
|
|
2186
2437
|
}
|
|
2187
|
-
await context.saveEntity(updatedPerson);
|
|
2438
|
+
await context.saveEntity(updatedPerson, true);
|
|
2188
2439
|
const changes = [];
|
|
2189
2440
|
if (args.name !== void 0) changes.push(`name: "${args.name}"`);
|
|
2190
2441
|
if (args.firstName !== void 0) changes.push(`firstName: "${args.firstName}"`);
|
|
@@ -2398,7 +2649,7 @@ async function handleEditProject(args) {
|
|
|
2398
2649
|
updatedProject.relationships.relatedTerms = updatedRelatedTerms;
|
|
2399
2650
|
}
|
|
2400
2651
|
}
|
|
2401
|
-
await context.saveEntity(updatedProject);
|
|
2652
|
+
await context.saveEntity(updatedProject, true);
|
|
2402
2653
|
const changes = [];
|
|
2403
2654
|
if (args.name !== void 0) changes.push(`name: "${args.name}"`);
|
|
2404
2655
|
if (args.description !== void 0) changes.push(`description updated`);
|
|
@@ -2465,7 +2716,7 @@ async function handleUpdateProject(args) {
|
|
|
2465
2716
|
},
|
|
2466
2717
|
updatedAt: /* @__PURE__ */ new Date()
|
|
2467
2718
|
};
|
|
2468
|
-
await context.saveEntity(updatedProject);
|
|
2719
|
+
await context.saveEntity(updatedProject, true);
|
|
2469
2720
|
return {
|
|
2470
2721
|
success: true,
|
|
2471
2722
|
message: `Updated project "${existingProject.name}" from source`,
|
|
@@ -2558,7 +2809,7 @@ async function handleEditTerm(args) {
|
|
|
2558
2809
|
} else if (existingTerm.projects && (args.projects !== void 0 || args.remove_projects)) {
|
|
2559
2810
|
delete updatedTerm.projects;
|
|
2560
2811
|
}
|
|
2561
|
-
await context.saveEntity(updatedTerm);
|
|
2812
|
+
await context.saveEntity(updatedTerm, true);
|
|
2562
2813
|
const changes = [];
|
|
2563
2814
|
if (args.expansion !== void 0) changes.push(`expansion: "${args.expansion}"`);
|
|
2564
2815
|
if (args.domain !== void 0) changes.push(`domain: "${args.domain}"`);
|
|
@@ -2629,7 +2880,7 @@ async function handleUpdateTerm(args) {
|
|
|
2629
2880
|
const projects = termContextHelper.findProjectsByTopic(suggestions.topics);
|
|
2630
2881
|
suggestedProjects = projects.map((p) => p.id);
|
|
2631
2882
|
}
|
|
2632
|
-
await context.saveEntity(updatedTerm);
|
|
2883
|
+
await context.saveEntity(updatedTerm, true);
|
|
2633
2884
|
return {
|
|
2634
2885
|
success: true,
|
|
2635
2886
|
message: `Updated term "${existingTerm.name}" from source`,
|
|
@@ -2876,15 +3127,48 @@ async function handleSuggestTermMetadata(args) {
|
|
|
2876
3127
|
};
|
|
2877
3128
|
}
|
|
2878
3129
|
|
|
3130
|
+
async function findTranscript(filenameOrPath, contextDirectory) {
|
|
3131
|
+
if (filenameOrPath.startsWith("/") && await fileExists(filenameOrPath)) {
|
|
3132
|
+
return filenameOrPath;
|
|
3133
|
+
}
|
|
3134
|
+
const outputDirectory = await getConfiguredDirectory("outputDirectory");
|
|
3135
|
+
const result = await listTranscripts({
|
|
3136
|
+
directory: outputDirectory,
|
|
3137
|
+
search: filenameOrPath,
|
|
3138
|
+
limit: 10
|
|
3139
|
+
});
|
|
3140
|
+
if (result.transcripts.length === 0) {
|
|
3141
|
+
throw new Error(
|
|
3142
|
+
`No transcript found matching "${filenameOrPath}" in ${outputDirectory}. Try using protokoll_list_transcripts to see available transcripts.`
|
|
3143
|
+
);
|
|
3144
|
+
}
|
|
3145
|
+
if (result.transcripts.length === 1) {
|
|
3146
|
+
return result.transcripts[0].path;
|
|
3147
|
+
}
|
|
3148
|
+
const exactMatch = result.transcripts.find(
|
|
3149
|
+
(t) => t.filename === filenameOrPath || t.filename.includes(filenameOrPath)
|
|
3150
|
+
);
|
|
3151
|
+
if (exactMatch) {
|
|
3152
|
+
return exactMatch.path;
|
|
3153
|
+
}
|
|
3154
|
+
const matches = result.transcripts.map((t) => t.filename).join(", ");
|
|
3155
|
+
throw new Error(
|
|
3156
|
+
`Multiple transcripts match "${filenameOrPath}": ${matches}. Please be more specific.`
|
|
3157
|
+
);
|
|
3158
|
+
}
|
|
2879
3159
|
const readTranscriptTool = {
|
|
2880
3160
|
name: "protokoll_read_transcript",
|
|
2881
|
-
description: "Read a transcript file and parse its metadata and content. Returns structured data including title, metadata, routing info, and content.",
|
|
3161
|
+
description: "Read a transcript file and parse its metadata and content. You can provide either an absolute path OR just a filename/partial filename. If you provide a filename, it will search in the configured output directory. Returns structured data including title, metadata, routing info, and content.",
|
|
2882
3162
|
inputSchema: {
|
|
2883
3163
|
type: "object",
|
|
2884
3164
|
properties: {
|
|
2885
3165
|
transcriptPath: {
|
|
2886
3166
|
type: "string",
|
|
2887
|
-
description:
|
|
3167
|
+
description: 'Filename, partial filename, or absolute path to the transcript. Examples: "meeting-notes.md", "2026-01-29", "/full/path/to/transcript.md"'
|
|
3168
|
+
},
|
|
3169
|
+
contextDirectory: {
|
|
3170
|
+
type: "string",
|
|
3171
|
+
description: "Optional: Path to the .protokoll context directory"
|
|
2888
3172
|
}
|
|
2889
3173
|
},
|
|
2890
3174
|
required: ["transcriptPath"]
|
|
@@ -2892,13 +3176,13 @@ const readTranscriptTool = {
|
|
|
2892
3176
|
};
|
|
2893
3177
|
const listTranscriptsTool = {
|
|
2894
3178
|
name: "protokoll_list_transcripts",
|
|
2895
|
-
description: "List transcripts
|
|
3179
|
+
description: "List transcripts with pagination, filtering, and search. If no directory is specified, uses the configured output directory. Returns transcript metadata including date, time, title, and file path. Supports sorting by date (default), filename, or title. Can filter by date range and search within transcript content.",
|
|
2896
3180
|
inputSchema: {
|
|
2897
3181
|
type: "object",
|
|
2898
3182
|
properties: {
|
|
2899
3183
|
directory: {
|
|
2900
3184
|
type: "string",
|
|
2901
|
-
description: "Directory to search for transcripts (searches recursively)"
|
|
3185
|
+
description: "Optional: Directory to search for transcripts (searches recursively). If not specified, uses the configured output directory."
|
|
2902
3186
|
},
|
|
2903
3187
|
limit: {
|
|
2904
3188
|
type: "number",
|
|
@@ -2927,20 +3211,24 @@ const listTranscriptsTool = {
|
|
|
2927
3211
|
search: {
|
|
2928
3212
|
type: "string",
|
|
2929
3213
|
description: "Search for transcripts containing this text (searches filename and content)"
|
|
3214
|
+
},
|
|
3215
|
+
contextDirectory: {
|
|
3216
|
+
type: "string",
|
|
3217
|
+
description: "Optional: Path to the .protokoll context directory"
|
|
2930
3218
|
}
|
|
2931
3219
|
},
|
|
2932
|
-
required: [
|
|
3220
|
+
required: []
|
|
2933
3221
|
}
|
|
2934
3222
|
};
|
|
2935
3223
|
const editTranscriptTool = {
|
|
2936
3224
|
name: "protokoll_edit_transcript",
|
|
2937
|
-
description: "Edit an existing transcript's title and/or project assignment. IMPORTANT: When you change the title, this tool RENAMES THE FILE to match the new title (slugified). Always use this tool instead of directly editing transcript files when changing titles. Changing the project will update metadata and may move the file to a new location based on the project's routing configuration.",
|
|
3225
|
+
description: "Edit an existing transcript's title and/or project assignment. You can provide either an absolute path OR just a filename/partial filename. IMPORTANT: When you change the title, this tool RENAMES THE FILE to match the new title (slugified). Always use this tool instead of directly editing transcript files when changing titles. Changing the project will update metadata and may move the file to a new location based on the project's routing configuration.",
|
|
2938
3226
|
inputSchema: {
|
|
2939
3227
|
type: "object",
|
|
2940
3228
|
properties: {
|
|
2941
3229
|
transcriptPath: {
|
|
2942
3230
|
type: "string",
|
|
2943
|
-
description:
|
|
3231
|
+
description: 'Filename, partial filename, or absolute path to the transcript. Examples: "meeting-notes.md", "2026-01-29", "/full/path/to/transcript.md"'
|
|
2944
3232
|
},
|
|
2945
3233
|
title: {
|
|
2946
3234
|
type: "string",
|
|
@@ -2952,7 +3240,7 @@ const editTranscriptTool = {
|
|
|
2952
3240
|
},
|
|
2953
3241
|
contextDirectory: {
|
|
2954
3242
|
type: "string",
|
|
2955
|
-
description: "Path to the .protokoll context directory"
|
|
3243
|
+
description: "Optional: Path to the .protokoll context directory"
|
|
2956
3244
|
}
|
|
2957
3245
|
},
|
|
2958
3246
|
required: ["transcriptPath"]
|
|
@@ -2960,14 +3248,14 @@ const editTranscriptTool = {
|
|
|
2960
3248
|
};
|
|
2961
3249
|
const combineTranscriptsTool = {
|
|
2962
3250
|
name: "protokoll_combine_transcripts",
|
|
2963
|
-
description: "Combine multiple transcripts into a single document. Source files are automatically deleted after combining. Metadata from the first transcript is preserved, and content is organized into sections.",
|
|
3251
|
+
description: "Combine multiple transcripts into a single document. You can provide absolute paths OR filenames/partial filenames. Source files are automatically deleted after combining. Metadata from the first transcript is preserved, and content is organized into sections.",
|
|
2964
3252
|
inputSchema: {
|
|
2965
3253
|
type: "object",
|
|
2966
3254
|
properties: {
|
|
2967
3255
|
transcriptPaths: {
|
|
2968
3256
|
type: "array",
|
|
2969
3257
|
items: { type: "string" },
|
|
2970
|
-
description:
|
|
3258
|
+
description: 'Array of filenames, partial filenames, or absolute paths. Examples: ["meeting-1.md", "meeting-2.md"] or ["2026-01-29", "2026-01-30"]'
|
|
2971
3259
|
},
|
|
2972
3260
|
title: {
|
|
2973
3261
|
type: "string",
|
|
@@ -2979,7 +3267,7 @@ const combineTranscriptsTool = {
|
|
|
2979
3267
|
},
|
|
2980
3268
|
contextDirectory: {
|
|
2981
3269
|
type: "string",
|
|
2982
|
-
description: "Path to the .protokoll context directory"
|
|
3270
|
+
description: "Optional: Path to the .protokoll context directory"
|
|
2983
3271
|
}
|
|
2984
3272
|
},
|
|
2985
3273
|
required: ["transcriptPaths"]
|
|
@@ -2987,13 +3275,13 @@ const combineTranscriptsTool = {
|
|
|
2987
3275
|
};
|
|
2988
3276
|
const provideFeedbackTool = {
|
|
2989
3277
|
name: "protokoll_provide_feedback",
|
|
2990
|
-
description: 'Provide natural language feedback to correct a transcript. The feedback is processed by an agentic model that can: - Fix spelling and term errors - Add new terms, people, or companies to context - Change project assignment - Update the title Example: "YB should be Wibey" or "San Jay Grouper is actually Sanjay Gupta"',
|
|
3278
|
+
description: 'Provide natural language feedback to correct a transcript. You can provide either an absolute path OR just a filename/partial filename. The feedback is processed by an agentic model that can: - Fix spelling and term errors - Add new terms, people, or companies to context - Change project assignment - Update the title Example: "YB should be Wibey" or "San Jay Grouper is actually Sanjay Gupta"',
|
|
2991
3279
|
inputSchema: {
|
|
2992
3280
|
type: "object",
|
|
2993
3281
|
properties: {
|
|
2994
3282
|
transcriptPath: {
|
|
2995
3283
|
type: "string",
|
|
2996
|
-
description:
|
|
3284
|
+
description: 'Filename, partial filename, or absolute path to the transcript. Examples: "meeting-notes.md", "2026-01-29", "/full/path/to/transcript.md"'
|
|
2997
3285
|
},
|
|
2998
3286
|
feedback: {
|
|
2999
3287
|
type: "string",
|
|
@@ -3005,17 +3293,14 @@ const provideFeedbackTool = {
|
|
|
3005
3293
|
},
|
|
3006
3294
|
contextDirectory: {
|
|
3007
3295
|
type: "string",
|
|
3008
|
-
description: "Path to the .protokoll context directory"
|
|
3296
|
+
description: "Optional: Path to the .protokoll context directory"
|
|
3009
3297
|
}
|
|
3010
3298
|
},
|
|
3011
3299
|
required: ["transcriptPath", "feedback"]
|
|
3012
3300
|
}
|
|
3013
3301
|
};
|
|
3014
3302
|
async function handleReadTranscript(args) {
|
|
3015
|
-
const transcriptPath =
|
|
3016
|
-
if (!await fileExists(transcriptPath)) {
|
|
3017
|
-
throw new Error(`Transcript not found: ${transcriptPath}`);
|
|
3018
|
-
}
|
|
3303
|
+
const transcriptPath = await findTranscript(args.transcriptPath, args.contextDirectory);
|
|
3019
3304
|
const parsed = await parseTranscript(transcriptPath);
|
|
3020
3305
|
return {
|
|
3021
3306
|
filePath: transcriptPath,
|
|
@@ -3026,7 +3311,7 @@ async function handleReadTranscript(args) {
|
|
|
3026
3311
|
};
|
|
3027
3312
|
}
|
|
3028
3313
|
async function handleListTranscripts(args) {
|
|
3029
|
-
const directory = resolve(args.directory);
|
|
3314
|
+
const directory = args.directory ? resolve(args.directory) : await getConfiguredDirectory("outputDirectory", args.contextDirectory);
|
|
3030
3315
|
if (!await fileExists(directory)) {
|
|
3031
3316
|
throw new Error(`Directory not found: ${directory}`);
|
|
3032
3317
|
}
|
|
@@ -3065,16 +3350,14 @@ async function handleListTranscripts(args) {
|
|
|
3065
3350
|
};
|
|
3066
3351
|
}
|
|
3067
3352
|
async function handleEditTranscript(args) {
|
|
3068
|
-
const transcriptPath =
|
|
3069
|
-
if (!await fileExists(transcriptPath)) {
|
|
3070
|
-
throw new Error(`Transcript not found: ${transcriptPath}`);
|
|
3071
|
-
}
|
|
3353
|
+
const transcriptPath = await findTranscript(args.transcriptPath, args.contextDirectory);
|
|
3072
3354
|
if (!args.title && !args.projectId) {
|
|
3073
3355
|
throw new Error("Must specify title and/or projectId");
|
|
3074
3356
|
}
|
|
3075
3357
|
const result = await editTranscript(transcriptPath, {
|
|
3076
3358
|
title: args.title,
|
|
3077
|
-
projectId: args.projectId
|
|
3359
|
+
projectId: args.projectId,
|
|
3360
|
+
contextDirectory: args.contextDirectory
|
|
3078
3361
|
});
|
|
3079
3362
|
await mkdir(dirname(result.outputPath), { recursive: true });
|
|
3080
3363
|
await writeFile(result.outputPath, result.content, "utf-8");
|
|
@@ -3093,16 +3376,15 @@ async function handleCombineTranscripts(args) {
|
|
|
3093
3376
|
if (args.transcriptPaths.length < 2) {
|
|
3094
3377
|
throw new Error("At least 2 transcript files are required");
|
|
3095
3378
|
}
|
|
3379
|
+
const resolvedPaths = [];
|
|
3096
3380
|
for (const path of args.transcriptPaths) {
|
|
3097
|
-
const resolved =
|
|
3098
|
-
|
|
3099
|
-
throw new Error(`Transcript not found: ${resolved}`);
|
|
3100
|
-
}
|
|
3381
|
+
const resolved = await findTranscript(path, args.contextDirectory);
|
|
3382
|
+
resolvedPaths.push(resolved);
|
|
3101
3383
|
}
|
|
3102
|
-
const resolvedPaths = args.transcriptPaths.map((p) => resolve(p));
|
|
3103
3384
|
const result = await combineTranscripts(resolvedPaths, {
|
|
3104
3385
|
title: args.title,
|
|
3105
|
-
projectId: args.projectId
|
|
3386
|
+
projectId: args.projectId,
|
|
3387
|
+
contextDirectory: args.contextDirectory
|
|
3106
3388
|
});
|
|
3107
3389
|
await mkdir(dirname(result.outputPath), { recursive: true });
|
|
3108
3390
|
await writeFile(result.outputPath, result.content, "utf-8");
|
|
@@ -3123,10 +3405,7 @@ async function handleCombineTranscripts(args) {
|
|
|
3123
3405
|
};
|
|
3124
3406
|
}
|
|
3125
3407
|
async function handleProvideFeedback(args) {
|
|
3126
|
-
const transcriptPath =
|
|
3127
|
-
if (!await fileExists(transcriptPath)) {
|
|
3128
|
-
throw new Error(`Transcript not found: ${transcriptPath}`);
|
|
3129
|
-
}
|
|
3408
|
+
const transcriptPath = await findTranscript(args.transcriptPath, args.contextDirectory);
|
|
3130
3409
|
const transcriptContent = await readFile(transcriptPath, "utf-8");
|
|
3131
3410
|
const context = await create({
|
|
3132
3411
|
startingDir: args.contextDirectory || dirname(transcriptPath)
|
|
@@ -3288,6 +3567,151 @@ async function handleToolCall(name, args) {
|
|
|
3288
3567
|
}
|
|
3289
3568
|
}
|
|
3290
3569
|
|
|
3570
|
+
let cachedRoots = null;
|
|
3571
|
+
function setRoots(roots) {
|
|
3572
|
+
cachedRoots = roots;
|
|
3573
|
+
}
|
|
3574
|
+
function getCachedRoots() {
|
|
3575
|
+
return cachedRoots;
|
|
3576
|
+
}
|
|
3577
|
+
function fileUriToPath(uri) {
|
|
3578
|
+
if (!uri.startsWith("file://")) {
|
|
3579
|
+
return null;
|
|
3580
|
+
}
|
|
3581
|
+
try {
|
|
3582
|
+
const url = new URL(uri);
|
|
3583
|
+
return decodeURIComponent(url.pathname);
|
|
3584
|
+
} catch {
|
|
3585
|
+
return null;
|
|
3586
|
+
}
|
|
3587
|
+
}
|
|
3588
|
+
|
|
3589
|
+
let serverConfig = {
|
|
3590
|
+
context: null,
|
|
3591
|
+
workspaceRoot: null,
|
|
3592
|
+
inputDirectory: null,
|
|
3593
|
+
outputDirectory: null,
|
|
3594
|
+
processedDirectory: null,
|
|
3595
|
+
initialized: false
|
|
3596
|
+
};
|
|
3597
|
+
async function initializeServerConfig(roots) {
|
|
3598
|
+
const workspaceRoot = roots.length > 0 ? fileUriToPath(roots[0].uri) : null;
|
|
3599
|
+
if (!workspaceRoot) {
|
|
3600
|
+
serverConfig = {
|
|
3601
|
+
context: null,
|
|
3602
|
+
workspaceRoot: process.cwd(),
|
|
3603
|
+
inputDirectory: resolve(process.cwd(), "./recordings"),
|
|
3604
|
+
outputDirectory: resolve(process.cwd(), "./notes"),
|
|
3605
|
+
processedDirectory: resolve(process.cwd(), "./processed"),
|
|
3606
|
+
initialized: true
|
|
3607
|
+
};
|
|
3608
|
+
return;
|
|
3609
|
+
}
|
|
3610
|
+
try {
|
|
3611
|
+
const context = await create({
|
|
3612
|
+
startingDir: workspaceRoot
|
|
3613
|
+
});
|
|
3614
|
+
const config = context.getConfig();
|
|
3615
|
+
serverConfig = {
|
|
3616
|
+
context,
|
|
3617
|
+
workspaceRoot,
|
|
3618
|
+
inputDirectory: resolveDirectory(config.inputDirectory, workspaceRoot, "./recordings"),
|
|
3619
|
+
outputDirectory: resolveDirectory(config.outputDirectory, workspaceRoot, "./notes"),
|
|
3620
|
+
processedDirectory: resolveDirectory(config.processedDirectory, workspaceRoot, "./processed"),
|
|
3621
|
+
initialized: true
|
|
3622
|
+
};
|
|
3623
|
+
} catch {
|
|
3624
|
+
serverConfig = {
|
|
3625
|
+
context: null,
|
|
3626
|
+
workspaceRoot,
|
|
3627
|
+
inputDirectory: resolve(workspaceRoot, "./recordings"),
|
|
3628
|
+
outputDirectory: resolve(workspaceRoot, "./notes"),
|
|
3629
|
+
processedDirectory: resolve(workspaceRoot, "./processed"),
|
|
3630
|
+
initialized: true
|
|
3631
|
+
};
|
|
3632
|
+
}
|
|
3633
|
+
}
|
|
3634
|
+
async function reloadServerConfig(roots) {
|
|
3635
|
+
await initializeServerConfig(roots);
|
|
3636
|
+
}
|
|
3637
|
+
function clearServerConfig() {
|
|
3638
|
+
serverConfig = {
|
|
3639
|
+
context: null,
|
|
3640
|
+
workspaceRoot: null,
|
|
3641
|
+
inputDirectory: null,
|
|
3642
|
+
outputDirectory: null,
|
|
3643
|
+
processedDirectory: null,
|
|
3644
|
+
initialized: false
|
|
3645
|
+
};
|
|
3646
|
+
}
|
|
3647
|
+
function getServerConfig() {
|
|
3648
|
+
if (!serverConfig.initialized) {
|
|
3649
|
+
throw new Error("Server configuration not initialized. Call initializeServerConfig() first.");
|
|
3650
|
+
}
|
|
3651
|
+
return {
|
|
3652
|
+
context: serverConfig.context,
|
|
3653
|
+
workspaceRoot: serverConfig.workspaceRoot,
|
|
3654
|
+
inputDirectory: serverConfig.inputDirectory,
|
|
3655
|
+
outputDirectory: serverConfig.outputDirectory,
|
|
3656
|
+
processedDirectory: serverConfig.processedDirectory,
|
|
3657
|
+
initialized: serverConfig.initialized
|
|
3658
|
+
};
|
|
3659
|
+
}
|
|
3660
|
+
function getContext() {
|
|
3661
|
+
return serverConfig.context;
|
|
3662
|
+
}
|
|
3663
|
+
function getWorkspaceRoot() {
|
|
3664
|
+
return serverConfig.workspaceRoot;
|
|
3665
|
+
}
|
|
3666
|
+
function getInputDirectory() {
|
|
3667
|
+
if (!serverConfig.initialized || !serverConfig.inputDirectory) {
|
|
3668
|
+
return resolve(process.cwd(), "./recordings");
|
|
3669
|
+
}
|
|
3670
|
+
return serverConfig.inputDirectory;
|
|
3671
|
+
}
|
|
3672
|
+
function getOutputDirectory() {
|
|
3673
|
+
if (!serverConfig.initialized || !serverConfig.outputDirectory) {
|
|
3674
|
+
return resolve(process.cwd(), "./notes");
|
|
3675
|
+
}
|
|
3676
|
+
return serverConfig.outputDirectory;
|
|
3677
|
+
}
|
|
3678
|
+
function getProcessedDirectory() {
|
|
3679
|
+
if (!serverConfig.initialized) {
|
|
3680
|
+
return null;
|
|
3681
|
+
}
|
|
3682
|
+
return serverConfig.processedDirectory;
|
|
3683
|
+
}
|
|
3684
|
+
function isInitialized() {
|
|
3685
|
+
return serverConfig.initialized;
|
|
3686
|
+
}
|
|
3687
|
+
function resolveDirectory(configValue, workspaceRoot, defaultRelative) {
|
|
3688
|
+
if (configValue) {
|
|
3689
|
+
if (configValue.startsWith("~")) {
|
|
3690
|
+
const homeDir = process.env.HOME || process.env.USERPROFILE || "";
|
|
3691
|
+
return resolve(homeDir, configValue.substring(1));
|
|
3692
|
+
}
|
|
3693
|
+
if (configValue.startsWith("/")) {
|
|
3694
|
+
return configValue;
|
|
3695
|
+
}
|
|
3696
|
+
return resolve(workspaceRoot, configValue);
|
|
3697
|
+
}
|
|
3698
|
+
return resolve(workspaceRoot, defaultRelative);
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
const serverConfig$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
|
|
3702
|
+
__proto__: null,
|
|
3703
|
+
clearServerConfig,
|
|
3704
|
+
getContext,
|
|
3705
|
+
getInputDirectory,
|
|
3706
|
+
getOutputDirectory,
|
|
3707
|
+
getProcessedDirectory,
|
|
3708
|
+
getServerConfig,
|
|
3709
|
+
getWorkspaceRoot,
|
|
3710
|
+
initializeServerConfig,
|
|
3711
|
+
isInitialized,
|
|
3712
|
+
reloadServerConfig
|
|
3713
|
+
}, Symbol.toStringTag, { value: 'Module' }));
|
|
3714
|
+
|
|
3291
3715
|
async function main() {
|
|
3292
3716
|
const server = new Server(
|
|
3293
3717
|
{
|
|
@@ -3308,6 +3732,10 @@ async function main() {
|
|
|
3308
3732
|
}
|
|
3309
3733
|
}
|
|
3310
3734
|
);
|
|
3735
|
+
server.setRequestHandler(ListRootsRequestSchema, async () => {
|
|
3736
|
+
const roots = getCachedRoots() || [];
|
|
3737
|
+
return { roots };
|
|
3738
|
+
});
|
|
3311
3739
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
3312
3740
|
tools
|
|
3313
3741
|
}));
|
|
@@ -3354,6 +3782,13 @@ async function main() {
|
|
|
3354
3782
|
});
|
|
3355
3783
|
const transport = new StdioServerTransport();
|
|
3356
3784
|
await server.connect(transport);
|
|
3785
|
+
const workspaceRoot = process.env.WORKSPACE_ROOT || process.cwd();
|
|
3786
|
+
const initialRoots = [{
|
|
3787
|
+
uri: `file://${workspaceRoot}`,
|
|
3788
|
+
name: "Workspace"
|
|
3789
|
+
}];
|
|
3790
|
+
setRoots(initialRoots);
|
|
3791
|
+
await initializeServerConfig(initialRoots);
|
|
3357
3792
|
await new Promise(() => {
|
|
3358
3793
|
});
|
|
3359
3794
|
}
|