@redaksjon/protokoll 1.0.21 → 1.0.22-dev.20260227214704.08e3d54

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.
@@ -1,18 +1,21 @@
1
- import { resolve, isAbsolute, relative, extname, join, dirname, basename } from 'node:path';
1
+ import { isAbsolute, resolve, relative, dirname, join, basename, extname } from 'node:path';
2
+ import os, { tmpdir } from 'node:os';
3
+ import * as fs from 'node:fs/promises';
4
+ import { readFile, mkdir, writeFile, rm, stat, readdir, access, mkdtemp, unlink } from 'node:fs/promises';
5
+ import Logging from '@fjell/logging';
2
6
  import * as Metadata from '@redaksjon/protokoll-engine';
3
7
  import { Transcript, Media, Util, Pipeline, findIgnoredResilient, findCompanyResilient, findTermResilient, findPersonResilient, findProjectResilient, Routing, Phases, Reasoning, Agentic, VALID_STATUSES, isValidStatus } from '@redaksjon/protokoll-engine';
8
+ import { PklTranscript, readTranscript, listTranscripts as listTranscripts$1 } from '@redaksjon/protokoll-format';
4
9
  import * as yaml from 'js-yaml';
5
- import { stat, readdir, mkdir, mkdtemp, rm, unlink } from 'node:fs/promises';
6
10
  import { readFileSync } from 'node:fs';
7
11
  import { fileURLToPath } from 'node:url';
8
12
  import { create as create$1, parseEntityUri as parseEntityUri$1, createRelationship, createDocumentContent, createCodeContent, createMarkdownContent, createTextContent, createUrlContent } from '@redaksjon/context';
9
- import os, { tmpdir } from 'node:os';
10
13
  import winston from 'winston';
11
14
  import { glob } from 'glob';
12
15
  import { randomUUID } from 'crypto';
13
16
  import { randomUUID as randomUUID$1 } from 'node:crypto';
14
- import { PklTranscript, readTranscript, listTranscripts as listTranscripts$1 } from '@redaksjon/protokoll-format';
15
17
  import * as Cardigantime from '@utilarium/cardigantime';
18
+ import { Storage } from '@google-cloud/storage';
16
19
 
17
20
  const SCHEME = "protokoll";
18
21
  function parseUri(uri) {
@@ -205,7 +208,7 @@ function isProtokolUri(uri) {
205
208
  return uri.startsWith(`${SCHEME}://`);
206
209
  }
207
210
 
208
- const VERSION = "1.0.21 (HEAD/dd3f7e4 T:v1.0.21 2026-02-25 11:08:34 -0800) linux arm64 v24.14.0";
211
+ const VERSION = "1.0.22-dev.20260227214704.08e3d54 (working/08e3d54 2026-02-27 13:46:39 -0800) linux arm64 v24.14.0";
209
212
  const PROGRAM_NAME = "protokoll";
210
213
  const DATE_FORMAT_YEAR_MONTH_DAY_HOURS_MINUTES_SECONDS = "YYYY-M-D-HHmmss";
211
214
  const DEFAULT_AUDIO_EXTENSIONS = ["mp3", "mp4", "mpeg", "mpga", "m4a", "wav", "webm", "qta"];
@@ -254,8 +257,42 @@ const getSmartAssistanceConfig = (config) => {
254
257
  };
255
258
  const create = async (options = {}) => {
256
259
  const baseInstance = await create$1(options);
260
+ const resolveEntityByIdentifier = (identifier, directLookup, listAll) => {
261
+ const normalized = identifier.trim();
262
+ if (!normalized) {
263
+ return void 0;
264
+ }
265
+ const direct = directLookup(normalized);
266
+ if (direct) {
267
+ return direct;
268
+ }
269
+ const normalizedLower = normalized.toLowerCase();
270
+ const uuidPrefix = normalizedLower.match(/^([a-f0-9]{8})/)?.[1];
271
+ for (const entity of listAll()) {
272
+ const entityId = entity.id.toLowerCase();
273
+ const entitySlug = entity.slug?.toLowerCase();
274
+ if (entityId === normalizedLower) {
275
+ return entity;
276
+ }
277
+ if (entitySlug && entitySlug === normalizedLower) {
278
+ return entity;
279
+ }
280
+ if (entityId.startsWith(normalizedLower) || normalizedLower.startsWith(entityId)) {
281
+ return entity;
282
+ }
283
+ if (uuidPrefix && entityId.startsWith(uuidPrefix)) {
284
+ return entity;
285
+ }
286
+ }
287
+ return void 0;
288
+ };
257
289
  return {
258
290
  ...baseInstance,
291
+ getPerson: (id) => resolveEntityByIdentifier(id, baseInstance.getPerson, baseInstance.getAllPeople),
292
+ getProject: (id) => resolveEntityByIdentifier(id, baseInstance.getProject, baseInstance.getAllProjects),
293
+ getCompany: (id) => resolveEntityByIdentifier(id, baseInstance.getCompany, baseInstance.getAllCompanies),
294
+ getTerm: (id) => resolveEntityByIdentifier(id, baseInstance.getTerm, baseInstance.getAllTerms),
295
+ getIgnored: (id) => resolveEntityByIdentifier(id, baseInstance.getIgnored, baseInstance.getAllIgnored),
259
296
  getSmartAssistanceConfig: () => getSmartAssistanceConfig(baseInstance.getConfig())
260
297
  };
261
298
  };
@@ -279,7 +316,270 @@ function fileUriToPath(uri) {
279
316
  }
280
317
  }
281
318
 
319
+ const DEFAULT_STORAGE_CONFIG = {
320
+ backend: "filesystem"
321
+ };
322
+
323
+ function normalizePrefix(value) {
324
+ return value.replace(/^\/+/, "").replace(/\/+/g, "/").replace(/\/+$/, "");
325
+ }
326
+ function parseGcsUri(uri) {
327
+ if (typeof uri !== "string" || uri.trim().length === 0) {
328
+ throw new Error("GCS URI must be a non-empty string.");
329
+ }
330
+ const trimmed = uri.trim();
331
+ if (!trimmed.startsWith("gs://")) {
332
+ throw new Error(`Invalid GCS URI "${uri}": must start with "gs://".`);
333
+ }
334
+ const withoutScheme = trimmed.slice("gs://".length);
335
+ if (withoutScheme.length === 0) {
336
+ throw new Error(`Invalid GCS URI "${uri}": missing bucket name.`);
337
+ }
338
+ const slashIndex = withoutScheme.indexOf("/");
339
+ const bucket = (slashIndex === -1 ? withoutScheme : withoutScheme.slice(0, slashIndex)).trim();
340
+ if (bucket.length === 0) {
341
+ throw new Error(`Invalid GCS URI "${uri}": missing bucket name.`);
342
+ }
343
+ const rawPrefix = slashIndex === -1 ? "" : withoutScheme.slice(slashIndex + 1);
344
+ const prefix = normalizePrefix(rawPrefix);
345
+ return { bucket, prefix };
346
+ }
347
+
348
+ const logger$4 = Logging.getLogger("@redaksjon/protokoll-mcp").get("gcs-storage");
349
+ function normalizePath(value) {
350
+ return value.replace(/^\/+/, "").replace(/\\/g, "/");
351
+ }
352
+ function joinObjectPath(basePrefix, objectPath) {
353
+ const normalizedBase = basePrefix.replace(/^\/+|\/+$/g, "");
354
+ const normalizedObjectPath = normalizePath(objectPath);
355
+ if (!normalizedBase) {
356
+ return normalizedObjectPath;
357
+ }
358
+ if (!normalizedObjectPath) {
359
+ return normalizedBase;
360
+ }
361
+ return `${normalizedBase}/${normalizedObjectPath}`;
362
+ }
363
+ class GcsStorageProvider {
364
+ constructor(storage, bucketName, basePrefix) {
365
+ this.storage = storage;
366
+ this.bucketName = bucketName;
367
+ this.basePrefix = basePrefix;
368
+ }
369
+ name = "gcs";
370
+ objectPath(pathValue) {
371
+ return joinObjectPath(this.basePrefix, pathValue);
372
+ }
373
+ async readFile(pathValue) {
374
+ const startedAt = Date.now();
375
+ const objectPath = this.objectPath(pathValue);
376
+ logger$4.debug("gcs.read.start", {
377
+ bucket: this.bucketName,
378
+ basePrefix: this.basePrefix,
379
+ path: pathValue,
380
+ objectPath
381
+ });
382
+ const [contents] = await this.storage.bucket(this.bucketName).file(objectPath).download();
383
+ logger$4.info("gcs.read.complete", {
384
+ bucket: this.bucketName,
385
+ objectPath,
386
+ bytes: contents.length,
387
+ elapsedMs: Date.now() - startedAt
388
+ });
389
+ return contents;
390
+ }
391
+ async writeFile(pathValue, data) {
392
+ const startedAt = Date.now();
393
+ const objectPath = this.objectPath(pathValue);
394
+ const bytes = typeof data === "string" ? Buffer.byteLength(data) : data.length;
395
+ logger$4.debug("gcs.write.start", {
396
+ bucket: this.bucketName,
397
+ basePrefix: this.basePrefix,
398
+ path: pathValue,
399
+ objectPath,
400
+ bytes
401
+ });
402
+ await this.storage.bucket(this.bucketName).file(objectPath).save(data);
403
+ logger$4.info("gcs.write.complete", {
404
+ bucket: this.bucketName,
405
+ objectPath,
406
+ bytes,
407
+ elapsedMs: Date.now() - startedAt
408
+ });
409
+ }
410
+ async listFiles(prefix, pattern) {
411
+ const startedAt = Date.now();
412
+ const fullPrefix = this.objectPath(prefix);
413
+ logger$4.debug("gcs.list.start", {
414
+ bucket: this.bucketName,
415
+ basePrefix: this.basePrefix,
416
+ prefix,
417
+ fullPrefix,
418
+ pattern: pattern ?? null
419
+ });
420
+ const [files] = await this.storage.bucket(this.bucketName).getFiles({ prefix: fullPrefix });
421
+ const normalizedBase = this.basePrefix.replace(/^\/+|\/+$/g, "");
422
+ const relativePaths = files.map((fileRef) => fileRef.name).filter((fileName) => !fileName.endsWith("/")).map((fileName) => {
423
+ if (!normalizedBase) {
424
+ return fileName;
425
+ }
426
+ const prefixWithSlash = `${normalizedBase}/`;
427
+ if (fileName.startsWith(prefixWithSlash)) {
428
+ return fileName.slice(prefixWithSlash.length);
429
+ }
430
+ return fileName;
431
+ });
432
+ if (!pattern) {
433
+ logger$4.info("gcs.list.complete", {
434
+ bucket: this.bucketName,
435
+ fullPrefix,
436
+ matchedCount: relativePaths.length,
437
+ elapsedMs: Date.now() - startedAt
438
+ });
439
+ return relativePaths;
440
+ }
441
+ const filtered = relativePaths.filter((pathValue) => pathValue.includes(pattern));
442
+ logger$4.info("gcs.list.complete", {
443
+ bucket: this.bucketName,
444
+ fullPrefix,
445
+ pattern,
446
+ matchedCount: filtered.length,
447
+ elapsedMs: Date.now() - startedAt
448
+ });
449
+ return filtered;
450
+ }
451
+ async deleteFile(pathValue) {
452
+ const startedAt = Date.now();
453
+ const objectPath = this.objectPath(pathValue);
454
+ logger$4.debug("gcs.delete.start", {
455
+ bucket: this.bucketName,
456
+ basePrefix: this.basePrefix,
457
+ path: pathValue,
458
+ objectPath
459
+ });
460
+ await this.storage.bucket(this.bucketName).file(objectPath).delete({ ignoreNotFound: true });
461
+ logger$4.info("gcs.delete.complete", {
462
+ bucket: this.bucketName,
463
+ objectPath,
464
+ elapsedMs: Date.now() - startedAt
465
+ });
466
+ }
467
+ async exists(pathValue) {
468
+ const startedAt = Date.now();
469
+ const objectPath = this.objectPath(pathValue);
470
+ logger$4.debug("gcs.exists.start", {
471
+ bucket: this.bucketName,
472
+ basePrefix: this.basePrefix,
473
+ path: pathValue,
474
+ objectPath
475
+ });
476
+ const [exists] = await this.storage.bucket(this.bucketName).file(objectPath).exists();
477
+ logger$4.info("gcs.exists.complete", {
478
+ bucket: this.bucketName,
479
+ objectPath,
480
+ exists,
481
+ elapsedMs: Date.now() - startedAt
482
+ });
483
+ return exists;
484
+ }
485
+ async mkdir(_pathValue) {
486
+ }
487
+ async verifyBucketAccess() {
488
+ const startedAt = Date.now();
489
+ logger$4.debug("gcs.verify_bucket_access.start", {
490
+ bucket: this.bucketName,
491
+ basePrefix: this.basePrefix
492
+ });
493
+ await this.storage.bucket(this.bucketName).getMetadata();
494
+ logger$4.info("gcs.verify_bucket_access.complete", {
495
+ bucket: this.bucketName,
496
+ elapsedMs: Date.now() - startedAt
497
+ });
498
+ }
499
+ }
500
+ function createGcsStorageProvider(uri, credentialsFile, projectId) {
501
+ const startedAt = Date.now();
502
+ const { bucket, prefix } = parseGcsUri(uri);
503
+ logger$4.debug("gcs.provider.create.start", {
504
+ uri,
505
+ bucket,
506
+ prefix,
507
+ hasCredentialsFile: Boolean(credentialsFile),
508
+ hasProjectId: Boolean(projectId)
509
+ });
510
+ const storage = credentialsFile ? new Storage({ keyFilename: credentialsFile, projectId }) : new Storage({ projectId });
511
+ logger$4.info("gcs.provider.create.complete", {
512
+ bucket,
513
+ prefix,
514
+ hasCredentialsFile: Boolean(credentialsFile),
515
+ hasProjectId: Boolean(projectId),
516
+ elapsedMs: Date.now() - startedAt
517
+ });
518
+ return new GcsStorageProvider(storage, bucket, prefix);
519
+ }
520
+
521
+ async function walkFilesRecursive(directory) {
522
+ const entries = await readdir(directory, { withFileTypes: true });
523
+ const files = [];
524
+ for (const entry of entries) {
525
+ const absolutePath = join(directory, entry.name);
526
+ if (entry.isDirectory()) {
527
+ files.push(...await walkFilesRecursive(absolutePath));
528
+ } else if (entry.isFile()) {
529
+ files.push(absolutePath);
530
+ }
531
+ }
532
+ return files;
533
+ }
534
+ class FilesystemStorageProvider {
535
+ constructor(baseDirectory) {
536
+ this.baseDirectory = baseDirectory;
537
+ }
538
+ name = "filesystem";
539
+ resolvePath(pathValue) {
540
+ if (isAbsolute(pathValue)) {
541
+ return pathValue;
542
+ }
543
+ return resolve(this.baseDirectory, pathValue);
544
+ }
545
+ toRelativePath(pathValue) {
546
+ return relative(this.baseDirectory, pathValue);
547
+ }
548
+ async readFile(pathValue) {
549
+ return readFile(this.resolvePath(pathValue));
550
+ }
551
+ async writeFile(pathValue, data) {
552
+ const resolvedPath = this.resolvePath(pathValue);
553
+ await mkdir(dirname(resolvedPath), { recursive: true });
554
+ await writeFile(resolvedPath, data);
555
+ }
556
+ async listFiles(prefix, pattern) {
557
+ const prefixPath = this.resolvePath(prefix || ".");
558
+ const allFiles = await walkFilesRecursive(prefixPath);
559
+ const relativeFiles = allFiles.map((filePath) => this.toRelativePath(filePath));
560
+ if (!pattern) {
561
+ return relativeFiles;
562
+ }
563
+ return relativeFiles.filter((filePath) => filePath.includes(pattern));
564
+ }
565
+ async deleteFile(pathValue) {
566
+ await rm(this.resolvePath(pathValue), { force: true });
567
+ }
568
+ async exists(pathValue) {
569
+ try {
570
+ await stat(this.resolvePath(pathValue));
571
+ return true;
572
+ } catch {
573
+ return false;
574
+ }
575
+ }
576
+ async mkdir(pathValue) {
577
+ await mkdir(this.resolvePath(pathValue), { recursive: true });
578
+ }
579
+ }
580
+
282
581
  const DEFAULT_CONFIG_FILE$1 = "protokoll-config.yaml";
582
+ const logger$3 = Logging.getLogger("@redaksjon/protokoll-mcp").get("server-config");
283
583
  const cardigantime = Cardigantime.create({
284
584
  defaults: {
285
585
  configDirectory: ".",
@@ -290,7 +590,7 @@ const cardigantime = Cardigantime.create({
290
590
  // pathFields determines which fields are processed, resolvePathArray determines
291
591
  // if array elements should be resolved individually
292
592
  pathResolution: {
293
- pathFields: ["inputDirectory", "outputDirectory", "processedDirectory", "contextDirectories"],
593
+ pathFields: ["inputDirectory", "outputDirectory", "processedDirectory", "contextDirectories", "storage.gcs.credentialsFile"],
294
594
  resolvePathArray: ["contextDirectories"]
295
595
  }
296
596
  },
@@ -301,11 +601,79 @@ async function readConfigFromDirectory(directory) {
301
601
  const previousCwd = process.cwd();
302
602
  try {
303
603
  process.chdir(directory);
304
- return await cardigantime.read({});
604
+ const discoveredConfig = await cardigantime.read({});
605
+ return mergeConfigWithEnv(discoveredConfig);
305
606
  } finally {
306
607
  process.chdir(previousCwd);
307
608
  }
308
609
  }
610
+ function parseBooleanEnv(value) {
611
+ if (!value) return void 0;
612
+ const normalized = value.trim().toLowerCase();
613
+ if (normalized === "true") return true;
614
+ if (normalized === "false") return false;
615
+ return void 0;
616
+ }
617
+ function parseCsvEnv(value) {
618
+ if (!value) return void 0;
619
+ const parsed = value.split(",").map((part) => part.trim()).filter(Boolean);
620
+ return parsed.length > 0 ? parsed : void 0;
621
+ }
622
+ function readEnvString(name) {
623
+ return readString(process.env[name]);
624
+ }
625
+ function mergeNestedObjects(base, overrides) {
626
+ const merged = { ...base };
627
+ for (const [key, value] of Object.entries(overrides)) {
628
+ if (value === void 0) continue;
629
+ const existing = merged[key];
630
+ if (isObjectRecord(existing) && isObjectRecord(value)) {
631
+ merged[key] = mergeNestedObjects(existing, value);
632
+ continue;
633
+ }
634
+ merged[key] = value;
635
+ }
636
+ return merged;
637
+ }
638
+ function buildEnvStorageConfig() {
639
+ const configuredBackend = readEnvString("PROTOKOLL_STORAGE_BACKEND");
640
+ const gcs = {
641
+ projectId: readEnvString("PROTOKOLL_STORAGE_GCS_PROJECT_ID"),
642
+ inputUri: readEnvString("PROTOKOLL_STORAGE_GCS_INPUT_URI"),
643
+ outputUri: readEnvString("PROTOKOLL_STORAGE_GCS_OUTPUT_URI"),
644
+ contextUri: readEnvString("PROTOKOLL_STORAGE_GCS_CONTEXT_URI"),
645
+ inputBucket: readEnvString("PROTOKOLL_STORAGE_GCS_INPUT_BUCKET"),
646
+ inputPrefix: readEnvString("PROTOKOLL_STORAGE_GCS_INPUT_PREFIX"),
647
+ outputBucket: readEnvString("PROTOKOLL_STORAGE_GCS_OUTPUT_BUCKET"),
648
+ outputPrefix: readEnvString("PROTOKOLL_STORAGE_GCS_OUTPUT_PREFIX"),
649
+ contextBucket: readEnvString("PROTOKOLL_STORAGE_GCS_CONTEXT_BUCKET"),
650
+ contextPrefix: readEnvString("PROTOKOLL_STORAGE_GCS_CONTEXT_PREFIX"),
651
+ credentialsFile: readEnvString("PROTOKOLL_STORAGE_GCS_CREDENTIALS_FILE")
652
+ };
653
+ const hasGcsValue = Object.values(gcs).some((value) => value !== void 0);
654
+ const backend = configuredBackend ?? (hasGcsValue ? "gcs" : void 0);
655
+ if (!backend) return void 0;
656
+ const storage = {};
657
+ if (backend) storage.backend = backend;
658
+ if (hasGcsValue) storage.gcs = gcs;
659
+ return storage;
660
+ }
661
+ function mergeConfigWithEnv(config) {
662
+ const envOverrides = {
663
+ inputDirectory: readEnvString("PROTOKOLL_INPUT_DIRECTORY"),
664
+ outputDirectory: readEnvString("PROTOKOLL_OUTPUT_DIRECTORY"),
665
+ processedDirectory: readEnvString("PROTOKOLL_PROCESSED_DIRECTORY"),
666
+ contextDirectories: parseCsvEnv(process.env.PROTOKOLL_CONTEXT_DIRECTORIES),
667
+ model: readEnvString("PROTOKOLL_MODEL"),
668
+ classifyModel: readEnvString("PROTOKOLL_CLASSIFY_MODEL"),
669
+ composeModel: readEnvString("PROTOKOLL_COMPOSE_MODEL"),
670
+ transcriptionModel: readEnvString("PROTOKOLL_TRANSCRIPTION_MODEL"),
671
+ debug: parseBooleanEnv(process.env.PROTOKOLL_DEBUG),
672
+ verbose: parseBooleanEnv(process.env.PROTOKOLL_VERBOSE),
673
+ storage: buildEnvStorageConfig()
674
+ };
675
+ return mergeNestedObjects(config, envOverrides);
676
+ }
309
677
  let serverConfig = {
310
678
  mode: "local",
311
679
  context: null,
@@ -313,10 +681,163 @@ let serverConfig = {
313
681
  inputDirectory: null,
314
682
  outputDirectory: null,
315
683
  processedDirectory: null,
684
+ inputStorage: null,
685
+ outputStorage: null,
686
+ storageConfig: DEFAULT_STORAGE_CONFIG,
316
687
  configFilePath: null,
317
688
  configFile: null,
318
689
  initialized: false
319
690
  };
691
+ function isObjectRecord(value) {
692
+ return typeof value === "object" && value !== null;
693
+ }
694
+ function readString(value) {
695
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : void 0;
696
+ }
697
+ function buildGcsUri(bucket, prefix) {
698
+ const trimmedBucket = bucket.trim();
699
+ const normalizedPrefix = (prefix ?? "").replace(/^\/+/, "").replace(/\/+/g, "/").replace(/\/+$/, "");
700
+ if (!normalizedPrefix) {
701
+ return `gs://${trimmedBucket}`;
702
+ }
703
+ return `gs://${trimmedBucket}/${normalizedPrefix}`;
704
+ }
705
+ function resolveGcsUri(rawUri, bucket, prefix) {
706
+ if (bucket) {
707
+ return buildGcsUri(bucket, prefix);
708
+ }
709
+ return rawUri ?? "";
710
+ }
711
+ function resolveGcsUris(gcs) {
712
+ return {
713
+ inputUri: resolveGcsUri(gcs.inputUri, gcs.inputBucket, gcs.inputPrefix),
714
+ outputUri: resolveGcsUri(gcs.outputUri, gcs.outputBucket, gcs.outputPrefix),
715
+ contextUri: resolveGcsUri(gcs.contextUri, gcs.contextBucket, gcs.contextPrefix)
716
+ };
717
+ }
718
+ function parseStorageConfig(configFile) {
719
+ if (!configFile) {
720
+ return DEFAULT_STORAGE_CONFIG;
721
+ }
722
+ const rawStorage = configFile.storage;
723
+ if (!isObjectRecord(rawStorage)) {
724
+ return DEFAULT_STORAGE_CONFIG;
725
+ }
726
+ const backend = rawStorage.backend === "gcs" ? "gcs" : "filesystem";
727
+ if (backend !== "gcs") {
728
+ return DEFAULT_STORAGE_CONFIG;
729
+ }
730
+ const rawGcs = isObjectRecord(rawStorage.gcs) ? rawStorage.gcs : {};
731
+ const projectId = readString(rawGcs.projectId);
732
+ const inputUri = readString(rawGcs.inputUri);
733
+ const outputUri = readString(rawGcs.outputUri);
734
+ const contextUri = readString(rawGcs.contextUri);
735
+ const inputBucket = readString(rawGcs.inputBucket);
736
+ const inputPrefix = readString(rawGcs.inputPrefix);
737
+ const outputBucket = readString(rawGcs.outputBucket);
738
+ const outputPrefix = readString(rawGcs.outputPrefix);
739
+ const contextBucket = readString(rawGcs.contextBucket);
740
+ const contextPrefix = readString(rawGcs.contextPrefix);
741
+ const credentialsFile = readString(rawGcs.credentialsFile);
742
+ return {
743
+ backend: "gcs",
744
+ gcs: {
745
+ projectId,
746
+ inputUri,
747
+ outputUri,
748
+ contextUri,
749
+ inputBucket,
750
+ inputPrefix,
751
+ outputBucket,
752
+ outputPrefix,
753
+ contextBucket,
754
+ contextPrefix,
755
+ credentialsFile
756
+ }
757
+ };
758
+ }
759
+ function resolveCredentialsFilePath(credentialsFile, configDir) {
760
+ if (!credentialsFile || credentialsFile.trim().length === 0) {
761
+ return void 0;
762
+ }
763
+ return resolve(configDir, credentialsFile);
764
+ }
765
+ async function validateGcsStorageConfig(storageConfig, configDir) {
766
+ if (storageConfig.backend !== "gcs") {
767
+ return void 0;
768
+ }
769
+ if (!storageConfig.gcs) {
770
+ throw new Error("storage.backend is set to gcs, but storage.gcs is missing.");
771
+ }
772
+ const startedAt = Date.now();
773
+ const { inputUri, outputUri, contextUri } = resolveGcsUris(storageConfig.gcs);
774
+ logger$3.debug("storage.gcs.validate.start", {
775
+ configDir,
776
+ inputUri,
777
+ outputUri,
778
+ contextUri,
779
+ projectId: storageConfig.gcs.projectId ?? null,
780
+ hasCredentialsFile: Boolean(storageConfig.gcs.credentialsFile)
781
+ });
782
+ if (!inputUri || !outputUri || !contextUri) {
783
+ throw new Error(
784
+ "GCS config is incomplete. Provide either storage.gcs.{inputUri,outputUri,contextUri} or storage.gcs.{inputBucket,inputPrefix,outputBucket,outputPrefix,contextBucket,contextPrefix}."
785
+ );
786
+ }
787
+ parseGcsUri(inputUri);
788
+ parseGcsUri(outputUri);
789
+ parseGcsUri(contextUri);
790
+ const credentialsFile = resolveCredentialsFilePath(storageConfig.gcs.credentialsFile, configDir);
791
+ if (credentialsFile) {
792
+ try {
793
+ await access(credentialsFile);
794
+ } catch {
795
+ throw new Error(`GCS credentials file is not readable: ${credentialsFile}`);
796
+ }
797
+ }
798
+ logger$3.info("storage.gcs.validate.complete", {
799
+ inputUri,
800
+ outputUri,
801
+ contextUri,
802
+ projectId: storageConfig.gcs.projectId ?? null,
803
+ credentialsFile: credentialsFile ?? null,
804
+ elapsedMs: Date.now() - startedAt
805
+ });
806
+ return credentialsFile;
807
+ }
808
+ async function createStorageProviders(storageConfig, inputDirectory, outputDirectory, configDir) {
809
+ if (storageConfig.backend === "filesystem") {
810
+ return {
811
+ inputStorage: new FilesystemStorageProvider(inputDirectory),
812
+ outputStorage: new FilesystemStorageProvider(outputDirectory)
813
+ };
814
+ }
815
+ if (!storageConfig.gcs) {
816
+ throw new Error("storage.backend is set to gcs, but storage.gcs is missing.");
817
+ }
818
+ const startedAt = Date.now();
819
+ const { inputUri, outputUri } = resolveGcsUris(storageConfig.gcs);
820
+ logger$3.debug("storage.providers.gcs.start", {
821
+ inputUri,
822
+ outputUri,
823
+ projectId: storageConfig.gcs.projectId ?? null
824
+ });
825
+ const credentialsFile = await validateGcsStorageConfig(storageConfig, configDir);
826
+ const inputStorage = createGcsStorageProvider(inputUri, credentialsFile, storageConfig.gcs.projectId);
827
+ const outputStorage = createGcsStorageProvider(outputUri, credentialsFile, storageConfig.gcs.projectId);
828
+ await inputStorage.verifyBucketAccess();
829
+ await outputStorage.verifyBucketAccess();
830
+ logger$3.info("storage.providers.gcs.complete", {
831
+ inputUri,
832
+ outputUri,
833
+ projectId: storageConfig.gcs.projectId ?? null,
834
+ elapsedMs: Date.now() - startedAt
835
+ });
836
+ return {
837
+ inputStorage,
838
+ outputStorage
839
+ };
840
+ }
320
841
  async function initializeServerConfig(roots, mode = "local") {
321
842
  const workspaceRoot = roots.length > 0 ? fileUriToPath(roots[0].uri) : null;
322
843
  if (!workspaceRoot) {
@@ -327,6 +848,9 @@ async function initializeServerConfig(roots, mode = "local") {
327
848
  inputDirectory: resolve(process.cwd(), "./recordings"),
328
849
  outputDirectory: resolve(process.cwd(), "./notes"),
329
850
  processedDirectory: resolve(process.cwd(), "./processed"),
851
+ inputStorage: new FilesystemStorageProvider(resolve(process.cwd(), "./recordings")),
852
+ outputStorage: new FilesystemStorageProvider(resolve(process.cwd(), "./notes")),
853
+ storageConfig: DEFAULT_STORAGE_CONFIG,
330
854
  configFilePath: null,
331
855
  configFile: null,
332
856
  initialized: true
@@ -339,23 +863,45 @@ async function initializeServerConfig(roots, mode = "local") {
339
863
  const configFilePath = Array.isArray(resolvedConfigDirs) && resolvedConfigDirs.length > 0 ? resolve(resolvedConfigDirs[0], DEFAULT_CONFIG_FILE$1) : null;
340
864
  const configDir = Array.isArray(resolvedConfigDirs) && resolvedConfigDirs.length > 0 ? resolvedConfigDirs[0] : workspaceRoot;
341
865
  const resolvedContextDirs = configFile.contextDirectories;
342
- const context = await create({
866
+ const contextCreateOptions = {
343
867
  startingDir: workspaceRoot,
344
868
  contextDirectories: resolvedContextDirs
345
- });
869
+ };
870
+ const storageConfig = parseStorageConfig(configFile);
871
+ const credentialsFile = await validateGcsStorageConfig(storageConfig, configDir);
872
+ if (storageConfig.backend === "gcs" && storageConfig.gcs) {
873
+ if (storageConfig.gcs.projectId) {
874
+ process.env.GOOGLE_CLOUD_PROJECT = storageConfig.gcs.projectId;
875
+ }
876
+ const { contextUri } = resolveGcsUris(storageConfig.gcs);
877
+ const parsedContextUri = parseGcsUri(contextUri);
878
+ contextCreateOptions.gcs = {
879
+ bucketName: parsedContextUri.bucket,
880
+ basePath: parsedContextUri.prefix,
881
+ credentialsFile
882
+ };
883
+ delete contextCreateOptions.contextDirectories;
884
+ }
885
+ const context = await create(contextCreateOptions);
346
886
  const contextConfig = context.getConfig();
347
887
  const mergedConfig = {
348
888
  ...contextConfig,
349
889
  ...configFile
350
890
  };
891
+ const inputDirectory = mergedConfig.inputDirectory || resolve(configDir, "./recordings");
892
+ const outputDirectory = mergedConfig.outputDirectory || resolve(configDir, "./notes");
893
+ const { inputStorage, outputStorage } = await createStorageProviders(storageConfig, inputDirectory, outputDirectory, configDir);
351
894
  serverConfig = {
352
895
  mode,
353
896
  context,
354
897
  workspaceRoot,
355
898
  // CardiganTime already resolved these paths; just provide defaults if not set
356
- inputDirectory: mergedConfig.inputDirectory || resolve(configDir, "./recordings"),
357
- outputDirectory: mergedConfig.outputDirectory || resolve(configDir, "./notes"),
899
+ inputDirectory,
900
+ outputDirectory,
358
901
  processedDirectory: mergedConfig.processedDirectory || resolve(configDir, "./processed"),
902
+ inputStorage,
903
+ outputStorage,
904
+ storageConfig,
359
905
  configFilePath,
360
906
  configFile: configFile ?? null,
361
907
  initialized: true
@@ -366,14 +912,21 @@ async function initializeServerConfig(roots, mode = "local") {
366
912
  const configFilePath = Array.isArray(resolvedConfigDirs) && resolvedConfigDirs.length > 0 ? resolve(resolvedConfigDirs[0], DEFAULT_CONFIG_FILE$1) : null;
367
913
  const configDir = Array.isArray(resolvedConfigDirs) && resolvedConfigDirs.length > 0 ? resolvedConfigDirs[0] : workspaceRoot;
368
914
  const mergedConfig = configFile ?? {};
915
+ const storageConfig = parseStorageConfig(configFile);
916
+ const inputDirectory = mergedConfig.inputDirectory || resolve(configDir, "./recordings");
917
+ const outputDirectory = mergedConfig.outputDirectory || resolve(configDir, "./notes");
918
+ const { inputStorage, outputStorage } = await createStorageProviders(storageConfig, inputDirectory, outputDirectory, configDir);
369
919
  serverConfig = {
370
920
  mode,
371
921
  context: null,
372
922
  workspaceRoot,
373
923
  // CardiganTime already resolved these paths; just provide defaults if not set
374
- inputDirectory: mergedConfig.inputDirectory || resolve(configDir, "./recordings"),
375
- outputDirectory: mergedConfig.outputDirectory || resolve(configDir, "./notes"),
924
+ inputDirectory,
925
+ outputDirectory,
376
926
  processedDirectory: mergedConfig.processedDirectory || resolve(configDir, "./processed"),
927
+ inputStorage,
928
+ outputStorage,
929
+ storageConfig,
377
930
  configFilePath,
378
931
  configFile: configFile ?? null,
379
932
  initialized: true
@@ -391,6 +944,9 @@ function clearServerConfig() {
391
944
  inputDirectory: null,
392
945
  outputDirectory: null,
393
946
  processedDirectory: null,
947
+ inputStorage: null,
948
+ outputStorage: null,
949
+ storageConfig: DEFAULT_STORAGE_CONFIG,
394
950
  configFilePath: null,
395
951
  configFile: null,
396
952
  initialized: false
@@ -407,6 +963,7 @@ function getServerConfig() {
407
963
  inputDirectory: serverConfig.inputDirectory,
408
964
  outputDirectory: serverConfig.outputDirectory,
409
965
  processedDirectory: serverConfig.processedDirectory,
966
+ storageConfig: serverConfig.storageConfig,
410
967
  configFilePath: serverConfig.configFilePath,
411
968
  configFile: serverConfig.configFile,
412
969
  initialized: serverConfig.initialized
@@ -436,6 +993,21 @@ function getProcessedDirectory() {
436
993
  }
437
994
  return serverConfig.processedDirectory;
438
995
  }
996
+ function getStorageConfig() {
997
+ return serverConfig.storageConfig;
998
+ }
999
+ function getInputStorage() {
1000
+ if (!serverConfig.initialized || !serverConfig.inputStorage) {
1001
+ return new FilesystemStorageProvider(resolve(process.cwd(), "./recordings"));
1002
+ }
1003
+ return serverConfig.inputStorage;
1004
+ }
1005
+ function getOutputStorage() {
1006
+ if (!serverConfig.initialized || !serverConfig.outputStorage) {
1007
+ return new FilesystemStorageProvider(resolve(process.cwd(), "./notes"));
1008
+ }
1009
+ return serverConfig.outputStorage;
1010
+ }
439
1011
  function isInitialized() {
440
1012
  return serverConfig.initialized;
441
1013
  }
@@ -451,10 +1023,13 @@ const serverConfig$1 = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProp
451
1023
  clearServerConfig,
452
1024
  getContext,
453
1025
  getInputDirectory,
1026
+ getInputStorage,
454
1027
  getOutputDirectory,
1028
+ getOutputStorage,
455
1029
  getProcessedDirectory,
456
1030
  getServerConfig,
457
1031
  getServerMode,
1032
+ getStorageConfig,
458
1033
  getWorkspaceRoot: getWorkspaceRoot$2,
459
1034
  initializeServerConfig,
460
1035
  isInitialized,
@@ -684,8 +1259,8 @@ const createLogger = (level = "info") => {
684
1259
  transports
685
1260
  });
686
1261
  };
687
- let logger$1 = createLogger();
688
- const getLogger = () => logger$1;
1262
+ let logger$2 = createLogger();
1263
+ const getLogger = () => logger$2;
689
1264
 
690
1265
  const transcriptExists$1 = Transcript.transcriptExists;
691
1266
  const ensurePklExtension$1 = Transcript.ensurePklExtension;
@@ -695,9 +1270,9 @@ function isUuidInput(input) {
695
1270
  async function findTranscriptByUuid$1(uuid, searchDirectories) {
696
1271
  return Transcript.findTranscriptByUuid(uuid, searchDirectories);
697
1272
  }
698
- const logger = getLogger();
699
- const media = Media.create(logger);
700
- const storage = Util.create({ log: logger.debug.bind(logger) });
1273
+ const logger$1 = getLogger();
1274
+ const media = Media.create(logger$1);
1275
+ const storage = Util.create({ log: logger$1.debug.bind(logger$1) });
701
1276
  async function fileExists(path) {
702
1277
  try {
703
1278
  await stat(path);
@@ -724,10 +1299,30 @@ async function getContextDirectories() {
724
1299
  }
725
1300
  async function createToolContext(contextDirectory) {
726
1301
  const ServerConfig = await Promise.resolve().then(() => serverConfig$1);
1302
+ const serverContext = ServerConfig.getContext();
1303
+ if (serverContext?.hasContext()) {
1304
+ return serverContext;
1305
+ }
727
1306
  const configFile = ServerConfig.isInitialized() ? ServerConfig.getServerConfig().configFile : null;
728
1307
  const rawDirs = configFile?.contextDirectories;
729
1308
  const effectiveDir = contextDirectory || (ServerConfig.isInitialized() ? ServerConfig.getWorkspaceRoot() : null) || process.cwd();
730
1309
  const contextDirs = rawDirs && rawDirs.length > 0 ? rawDirs.map((d) => isAbsolute(d) ? d : resolve(effectiveDir, d)) : void 0;
1310
+ const storageConfig = ServerConfig.getStorageConfig();
1311
+ if (storageConfig.backend === "gcs" && storageConfig.gcs) {
1312
+ const contextUri = storageConfig.gcs.contextUri || (storageConfig.gcs.contextBucket ? `gs://${storageConfig.gcs.contextBucket}/${(storageConfig.gcs.contextPrefix || "").replace(/^\/+|\/+$/g, "")}` : void 0);
1313
+ if (!contextUri) {
1314
+ throw new Error("GCS storage is enabled but context URI/bucket configuration is missing.");
1315
+ }
1316
+ const parsedContextUri = parseGcsUri(contextUri);
1317
+ return create({
1318
+ startingDir: effectiveDir,
1319
+ gcs: {
1320
+ bucketName: parsedContextUri.bucket,
1321
+ basePath: parsedContextUri.prefix,
1322
+ credentialsFile: storageConfig.gcs.credentialsFile
1323
+ }
1324
+ });
1325
+ }
731
1326
  return create({
732
1327
  startingDir: effectiveDir,
733
1328
  contextDirectories: contextDirs
@@ -901,7 +1496,7 @@ const shared = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
901
1496
  getAudioMetadata,
902
1497
  getConfiguredDirectory,
903
1498
  getContextDirectories,
904
- logger,
1499
+ logger: logger$1,
905
1500
  media,
906
1501
  mergeArray,
907
1502
  resolveTranscriptPath: resolveTranscriptPath$1,
@@ -914,6 +1509,7 @@ const shared = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
914
1509
  }, Symbol.toStringTag, { value: 'Module' }));
915
1510
 
916
1511
  const { listTranscripts, resolveTranscriptPath, readTranscriptContent, stripTranscriptExtension } = Transcript;
1512
+ const logger = Logging.getLogger("@redaksjon/protokoll-mcp").get("transcript-resources");
917
1513
  function parseStoredSummaries$1(raw) {
918
1514
  try {
919
1515
  const parsed = JSON.parse(raw);
@@ -950,6 +1546,13 @@ async function readTranscriptResource(transcriptPath) {
950
1546
  throw new Error(`Invalid transcript path: ${transcriptPath}`);
951
1547
  }
952
1548
  const outputDirectory = getOutputDirectory();
1549
+ const outputStorage = getOutputStorage();
1550
+ if (outputStorage.name === "gcs") {
1551
+ const gcsResult = await readTranscriptResourceFromStorage(transcriptPath, outputStorage, outputDirectory);
1552
+ if (gcsResult) {
1553
+ return gcsResult;
1554
+ }
1555
+ }
953
1556
  const basePath = transcriptPath.startsWith("/") ? transcriptPath : resolve(outputDirectory, transcriptPath);
954
1557
  const resolved = await resolveTranscriptPath(basePath);
955
1558
  if (!resolved.exists || !resolved.path) {
@@ -957,7 +1560,6 @@ async function readTranscriptResource(transcriptPath) {
957
1560
  }
958
1561
  try {
959
1562
  const { content, metadata, title } = await readTranscriptContent(resolved.path);
960
- const { PklTranscript } = await import('@redaksjon/protokoll-format');
961
1563
  const pklTranscript = PklTranscript.open(resolved.path, { readOnly: true });
962
1564
  let rawTranscript = void 0;
963
1565
  let summaries = [];
@@ -1016,9 +1618,245 @@ async function readTranscriptResource(transcriptPath) {
1016
1618
  throw error;
1017
1619
  }
1018
1620
  }
1621
+ async function withTempPklFile(contents, action) {
1622
+ const tempPath = `${tmpdir()}/protokoll-mcp-${Date.now()}-${Math.random().toString(36).slice(2)}.pkl`;
1623
+ await fs.writeFile(tempPath, contents);
1624
+ try {
1625
+ return await action(tempPath);
1626
+ } finally {
1627
+ await fs.rm(tempPath, { force: true });
1628
+ }
1629
+ }
1630
+ async function resolveStorageTranscriptPath(transcriptPath, outputStorage) {
1631
+ const candidates = /* @__PURE__ */ new Set();
1632
+ const normalizedInput = transcriptPath.replace(/^\/+/, "").replace(/\\/g, "/");
1633
+ if (normalizedInput.length > 0) {
1634
+ candidates.add(normalizedInput);
1635
+ }
1636
+ if (!normalizedInput.toLowerCase().endsWith(".pkl")) {
1637
+ candidates.add(`${normalizedInput}.pkl`);
1638
+ }
1639
+ for (const candidate of candidates) {
1640
+ if (await outputStorage.exists(candidate)) {
1641
+ return candidate;
1642
+ }
1643
+ }
1644
+ return null;
1645
+ }
1646
+ async function readTranscriptResourceFromStorage(transcriptPath, outputStorage, outputDirectory) {
1647
+ const storagePath = await resolveStorageTranscriptPath(transcriptPath, outputStorage);
1648
+ if (!storagePath) {
1649
+ return null;
1650
+ }
1651
+ const contents = await outputStorage.readFile(storagePath);
1652
+ return withTempPklFile(contents, async (tempPath) => {
1653
+ const { content, metadata, title } = await readTranscriptContent(tempPath);
1654
+ const pklTranscript = PklTranscript.open(tempPath, { readOnly: true });
1655
+ let rawTranscript = void 0;
1656
+ let summaries = [];
1657
+ try {
1658
+ if (pklTranscript.hasRawTranscript) {
1659
+ const rawData = pklTranscript.rawTranscript;
1660
+ if (rawData) {
1661
+ rawTranscript = {
1662
+ text: rawData.text,
1663
+ model: rawData.model,
1664
+ duration: rawData.duration,
1665
+ transcribedAt: rawData.transcribedAt
1666
+ };
1667
+ }
1668
+ }
1669
+ const historyArtifact = pklTranscript.getArtifact("summary_history");
1670
+ summaries = parseStoredSummaries$1(historyArtifact?.data?.toString("utf8") || "[]");
1671
+ } finally {
1672
+ pklTranscript.close();
1673
+ }
1674
+ const safeRelativePath = await sanitizePath(storagePath, outputDirectory);
1675
+ const identifierPath = stripTranscriptExtension(safeRelativePath);
1676
+ const structuredResponse = {
1677
+ uri: buildTranscriptUri(identifierPath),
1678
+ path: identifierPath,
1679
+ title: title || basename(identifierPath) || "Untitled",
1680
+ metadata: {
1681
+ date: metadata.date,
1682
+ time: metadata.time,
1683
+ project: metadata.project,
1684
+ projectId: metadata.projectId,
1685
+ status: metadata.status,
1686
+ tags: metadata.tags || [],
1687
+ duration: metadata.duration,
1688
+ entities: metadata.entities || {},
1689
+ tasks: metadata.tasks || [],
1690
+ history: metadata.history || [],
1691
+ routing: metadata.destination ? {
1692
+ destination: metadata.destination,
1693
+ confidence: metadata.confidence
1694
+ } : void 0
1695
+ },
1696
+ content,
1697
+ rawTranscript,
1698
+ summaries
1699
+ };
1700
+ return {
1701
+ uri: buildTranscriptUri(identifierPath),
1702
+ mimeType: "application/json",
1703
+ text: JSON.stringify(structuredResponse)
1704
+ };
1705
+ });
1706
+ }
1707
+ function normalizeDateOnly(value) {
1708
+ if (!value) {
1709
+ return void 0;
1710
+ }
1711
+ const trimmed = value.trim();
1712
+ if (!trimmed) {
1713
+ return void 0;
1714
+ }
1715
+ return trimmed.includes("T") ? trimmed.slice(0, 10) : trimmed;
1716
+ }
1717
+ function asOptionalString(value) {
1718
+ return typeof value === "string" ? value : void 0;
1719
+ }
1720
+ function passesDateFilter(date, startDate, endDate) {
1721
+ const normalized = normalizeDateOnly(date);
1722
+ if (!normalized) {
1723
+ return !startDate && !endDate;
1724
+ }
1725
+ if (startDate && normalized < startDate) {
1726
+ return false;
1727
+ }
1728
+ if (endDate && normalized > endDate) {
1729
+ return false;
1730
+ }
1731
+ return true;
1732
+ }
1733
+ async function listTranscriptsFromStorage(options) {
1734
+ const startedAt = Date.now();
1735
+ const {
1736
+ outputStorage,
1737
+ outputDirectory,
1738
+ startDate,
1739
+ endDate,
1740
+ projectId,
1741
+ projectName,
1742
+ limit,
1743
+ offset
1744
+ } = options;
1745
+ logger.info("transcripts.gcs.list.start", {
1746
+ directory: outputDirectory,
1747
+ limit,
1748
+ offset,
1749
+ startDate: startDate ?? null,
1750
+ endDate: endDate ?? null,
1751
+ projectId: projectId ?? null,
1752
+ projectName: projectName ?? null
1753
+ });
1754
+ const listStartedAt = Date.now();
1755
+ const allFiles = await outputStorage.listFiles("");
1756
+ logger.info("transcripts.gcs.list.objects_loaded", {
1757
+ totalObjects: allFiles.length,
1758
+ elapsedMs: Date.now() - listStartedAt
1759
+ });
1760
+ const transcriptCandidates = allFiles.filter((pathValue) => {
1761
+ const normalized = pathValue.replace(/\\/g, "/").toLowerCase();
1762
+ return normalized.endsWith(".pkl") && !normalized.startsWith("uploads/") && !normalized.startsWith(".intermediate/") && !normalized.includes("/uploads/") && !normalized.includes("/.intermediate/");
1763
+ });
1764
+ logger.info("transcripts.gcs.list.transcript_candidates", {
1765
+ candidates: transcriptCandidates.length,
1766
+ ignoredObjects: allFiles.length - transcriptCandidates.length
1767
+ });
1768
+ let processedCandidates = 0;
1769
+ const hydrateStartedAt = Date.now();
1770
+ const hydrated = await Promise.all(
1771
+ transcriptCandidates.map(async (pathValue) => {
1772
+ try {
1773
+ const buffer = await outputStorage.readFile(pathValue);
1774
+ const hydratedEntry = await withTempPklFile(buffer, async (tempPath) => {
1775
+ const { content, metadata, title } = await readTranscriptContent(tempPath);
1776
+ const pklTranscript = PklTranscript.open(tempPath, { readOnly: true });
1777
+ let hasRawTranscript = false;
1778
+ try {
1779
+ hasRawTranscript = Boolean(pklTranscript.hasRawTranscript);
1780
+ } finally {
1781
+ pklTranscript.close();
1782
+ }
1783
+ const metadataProjectId = asOptionalString(metadata.projectId);
1784
+ const metadataProject = asOptionalString(metadata.project);
1785
+ if (projectId) {
1786
+ const projectMatches = metadataProjectId === projectId || (projectName ? metadataProject === projectName : metadataProject === projectId);
1787
+ if (!projectMatches) {
1788
+ return null;
1789
+ }
1790
+ }
1791
+ const transcriptDate = normalizeDateOnly(asOptionalString(metadata.date));
1792
+ if (!passesDateFilter(transcriptDate, startDate, endDate)) {
1793
+ return null;
1794
+ }
1795
+ const safePath = await sanitizePath(pathValue, outputDirectory);
1796
+ return {
1797
+ path: safePath,
1798
+ filename: basename(pathValue),
1799
+ date: transcriptDate || "1970-01-01",
1800
+ time: asOptionalString(metadata.time),
1801
+ title: title || stripTranscriptExtension(basename(pathValue)),
1802
+ status: asOptionalString(metadata.status),
1803
+ openTasksCount: Array.isArray(metadata.tasks) ? metadata.tasks.filter((task) => {
1804
+ if (!task || typeof task !== "object") {
1805
+ return true;
1806
+ }
1807
+ return task.status !== "completed";
1808
+ }).length : 0,
1809
+ contentSize: content.length,
1810
+ entities: metadata.entities,
1811
+ hasRawTranscript
1812
+ };
1813
+ });
1814
+ return hydratedEntry;
1815
+ } catch {
1816
+ return null;
1817
+ } finally {
1818
+ processedCandidates++;
1819
+ if (processedCandidates % 25 === 0 || processedCandidates === transcriptCandidates.length) {
1820
+ logger.info("transcripts.gcs.list.progress", {
1821
+ processed: processedCandidates,
1822
+ total: transcriptCandidates.length,
1823
+ elapsedMs: Date.now() - hydrateStartedAt
1824
+ });
1825
+ }
1826
+ }
1827
+ })
1828
+ );
1829
+ const filtered = hydrated.filter((item) => item !== null).sort((a, b) => {
1830
+ const dateCompare = b.date.localeCompare(a.date);
1831
+ if (dateCompare !== 0) {
1832
+ return dateCompare;
1833
+ }
1834
+ const timeCompare = (b.time || "").localeCompare(a.time || "");
1835
+ if (timeCompare !== 0) {
1836
+ return timeCompare;
1837
+ }
1838
+ return b.filename.localeCompare(a.filename);
1839
+ });
1840
+ const page = filtered.slice(offset, offset + limit);
1841
+ logger.info("transcripts.gcs.list.complete", {
1842
+ totalCandidates: transcriptCandidates.length,
1843
+ totalAfterFilters: filtered.length,
1844
+ returned: page.length,
1845
+ hasMore: offset + limit < filtered.length,
1846
+ elapsedMs: Date.now() - startedAt
1847
+ });
1848
+ return {
1849
+ transcripts: page,
1850
+ total: filtered.length,
1851
+ hasMore: offset + limit < filtered.length,
1852
+ limit,
1853
+ offset
1854
+ };
1855
+ }
1019
1856
  async function readTranscriptsListResource(options) {
1020
1857
  const { startDate, endDate, limit = 50, offset = 0, projectId } = options;
1021
1858
  const outputDirectory = getOutputDirectory();
1859
+ const outputStorage = getOutputStorage();
1022
1860
  const directory = options.directory || outputDirectory;
1023
1861
  let projectName;
1024
1862
  if (projectId && isInitialized()) {
@@ -1041,7 +1879,16 @@ async function readTranscriptsListResource(options) {
1041
1879
  console.log(` Date range: ${startDate || "any"} to ${endDate || "any"}`);
1042
1880
  }
1043
1881
  console.log(` Limit: ${limit}, Offset: ${offset}`);
1044
- const result = await listTranscripts({
1882
+ const result = outputStorage.name === "gcs" ? await listTranscriptsFromStorage({
1883
+ outputStorage,
1884
+ outputDirectory,
1885
+ startDate,
1886
+ endDate,
1887
+ projectId,
1888
+ projectName,
1889
+ limit,
1890
+ offset
1891
+ }) : await listTranscripts({
1045
1892
  directory,
1046
1893
  limit,
1047
1894
  offset,
@@ -1105,16 +1952,90 @@ async function readTranscriptsListResource(options) {
1105
1952
  };
1106
1953
  }
1107
1954
 
1955
+ const ENTITY_DIRECTORY = {
1956
+ person: "people",
1957
+ project: "projects",
1958
+ term: "terms",
1959
+ company: "companies",
1960
+ ignored: "ignored"
1961
+ };
1962
+ function buildContextGcsUri() {
1963
+ const storageConfig = getStorageConfig();
1964
+ if (storageConfig.backend !== "gcs" || !storageConfig.gcs) {
1965
+ return null;
1966
+ }
1967
+ const gcs = storageConfig.gcs;
1968
+ const contextUri = gcs.contextUri || (gcs.contextBucket ? `gs://${gcs.contextBucket}/${(gcs.contextPrefix || "").replace(/^\/+|\/+$/g, "")}` : void 0);
1969
+ if (!contextUri) {
1970
+ return null;
1971
+ }
1972
+ return {
1973
+ uri: contextUri,
1974
+ projectId: gcs.projectId,
1975
+ credentialsFile: gcs.credentialsFile
1976
+ };
1977
+ }
1978
+ async function loadEntitiesFromGcs(entityType) {
1979
+ const contextGcs = buildContextGcsUri();
1980
+ if (!contextGcs) {
1981
+ return [];
1982
+ }
1983
+ const provider = createGcsStorageProvider(contextGcs.uri, contextGcs.credentialsFile, contextGcs.projectId);
1984
+ const directory = ENTITY_DIRECTORY[entityType];
1985
+ const files = await provider.listFiles(`${directory}/`);
1986
+ const yamlFiles = files.filter((pathValue) => pathValue.endsWith(".yaml") || pathValue.endsWith(".yml"));
1987
+ const entities = [];
1988
+ for (const filePath of yamlFiles) {
1989
+ try {
1990
+ const raw = await provider.readFile(filePath);
1991
+ const parsed = yaml.load(raw.toString("utf8"));
1992
+ if (parsed && typeof parsed === "object") {
1993
+ entities.push(parsed);
1994
+ }
1995
+ } catch {
1996
+ }
1997
+ }
1998
+ return entities;
1999
+ }
2000
+ async function findEntityInGcs(entityType, entityId) {
2001
+ const entities = await loadEntitiesFromGcs(entityType);
2002
+ const normalized = entityId.trim().toLowerCase();
2003
+ const prefix = normalized.match(/^([a-f0-9]{8})/)?.[1];
2004
+ for (const entity of entities) {
2005
+ const id = typeof entity.id === "string" ? entity.id : "";
2006
+ const slug = typeof entity.slug === "string" ? entity.slug : "";
2007
+ const idLower = id.toLowerCase();
2008
+ const slugLower = slug.toLowerCase();
2009
+ if (idLower === normalized || slugLower === normalized) {
2010
+ return entity;
2011
+ }
2012
+ if (normalized && (idLower.startsWith(normalized) || normalized.startsWith(idLower))) {
2013
+ return entity;
2014
+ }
2015
+ if (prefix && idLower.startsWith(prefix)) {
2016
+ return entity;
2017
+ }
2018
+ }
2019
+ return null;
2020
+ }
1108
2021
  async function readEntityResource(entityType, entityId, contextDirectory) {
1109
- const configFile = isInitialized() ? getServerConfig().configFile : null;
1110
- const rawDirs = configFile?.contextDirectories;
2022
+ if (entityType in ENTITY_DIRECTORY && isInitialized()) {
2023
+ const storageConfig = getStorageConfig();
2024
+ if (storageConfig.backend === "gcs") {
2025
+ const gcsEntity = await findEntityInGcs(entityType, entityId);
2026
+ if (gcsEntity) {
2027
+ const yamlContent2 = yaml.dump(gcsEntity);
2028
+ return {
2029
+ uri: buildEntityUri(entityType, entityId),
2030
+ mimeType: "application/yaml",
2031
+ text: yamlContent2
2032
+ };
2033
+ }
2034
+ }
2035
+ }
1111
2036
  const effectiveDir = contextDirectory || getWorkspaceRoot$2() || process.cwd();
1112
- const contextDirs = rawDirs && rawDirs.length > 0 ? rawDirs.map((d) => isAbsolute(d) ? d : resolve(effectiveDir, d)) : void 0;
1113
- console.log(` [entity] Looking up ${entityType}/${entityId} (fresh context from ${effectiveDir})`);
1114
- const context = await create({
1115
- startingDir: effectiveDir,
1116
- contextDirectories: contextDirs
1117
- });
2037
+ console.log(` [entity] Looking up ${entityType}/${entityId} (context from ${effectiveDir})`);
2038
+ const context = await createToolContext(contextDirectory);
1118
2039
  if (!context.hasContext()) {
1119
2040
  const searchDir = contextDirectory || process.cwd();
1120
2041
  console.log(` [entity] ❌ No Protokoll context found in ${searchDir}`);
@@ -1154,6 +2075,32 @@ async function readEntityResource(entityType, entityId, contextDirectory) {
1154
2075
  };
1155
2076
  }
1156
2077
  async function readEntitiesListResource(entityType, contextDirectory) {
2078
+ if (entityType in ENTITY_DIRECTORY && isInitialized()) {
2079
+ const storageConfig = getStorageConfig();
2080
+ if (storageConfig.backend === "gcs") {
2081
+ const entitiesFromGcs = await loadEntitiesFromGcs(entityType);
2082
+ const entities2 = entitiesFromGcs.map((entity) => {
2083
+ const id = String(entity.id || "");
2084
+ const name = String(entity.name || "");
2085
+ return {
2086
+ uri: buildEntityUri(entityType, id),
2087
+ id,
2088
+ name,
2089
+ ...entity
2090
+ };
2091
+ });
2092
+ const responseData2 = {
2093
+ entityType,
2094
+ count: entities2.length,
2095
+ entities: entities2
2096
+ };
2097
+ return {
2098
+ uri: buildEntitiesListUri(entityType),
2099
+ mimeType: "application/json",
2100
+ text: JSON.stringify(responseData2, null, 2)
2101
+ };
2102
+ }
2103
+ }
1157
2104
  let context;
1158
2105
  if (isInitialized()) {
1159
2106
  const serverContext = getContext();
@@ -2334,17 +3281,17 @@ async function findAudioFile(filenameOrPath) {
2334
3281
  if (filenameOrPath.startsWith("/") && await fileExists(filenameOrPath)) {
2335
3282
  return filenameOrPath;
2336
3283
  }
3284
+ const ServerConfig = await Promise.resolve().then(() => serverConfig$1);
3285
+ const inputStorage = ServerConfig.getInputStorage();
2337
3286
  const inputDirectory = await getConfiguredDirectory("inputDirectory");
2338
- const entries = await readdir(inputDirectory, { withFileTypes: true });
3287
+ const entries = await inputStorage.listFiles(".");
2339
3288
  const matches = [];
2340
3289
  for (const entry of entries) {
2341
- if (entry.isFile()) {
2342
- const filename = entry.name;
2343
- const ext = filename.split(".").pop()?.toLowerCase();
2344
- if (ext && DEFAULT_AUDIO_EXTENSIONS.includes(ext)) {
2345
- if (filename === filenameOrPath || filename.includes(filenameOrPath) || basename(filename, `.${ext}`) === filenameOrPath) {
2346
- matches.push(join(inputDirectory, filename));
2347
- }
3290
+ const filename = basename(entry);
3291
+ const ext = filename.split(".").pop()?.toLowerCase();
3292
+ if (ext && DEFAULT_AUDIO_EXTENSIONS.includes(ext)) {
3293
+ if (filename === filenameOrPath || filename.includes(filenameOrPath) || basename(filename, `.${ext}`) === filenameOrPath) {
3294
+ matches.push(entry);
2348
3295
  }
2349
3296
  }
2350
3297
  }
@@ -2354,7 +3301,19 @@ async function findAudioFile(filenameOrPath) {
2354
3301
  );
2355
3302
  }
2356
3303
  if (matches.length === 1) {
2357
- return matches[0];
3304
+ const matchedPath = matches[0];
3305
+ if (inputStorage.name === "filesystem") {
3306
+ return join(inputDirectory, matchedPath);
3307
+ }
3308
+ const contents = await inputStorage.readFile(matchedPath);
3309
+ const tempRoot = join(tmpdir(), "protokoll-gcs-input");
3310
+ await mkdir(tempRoot, { recursive: true });
3311
+ const tempPath = join(
3312
+ tempRoot,
3313
+ `${Date.now()}-${Math.random().toString(36).slice(2)}-${basename(matchedPath)}`
3314
+ );
3315
+ await writeFile(tempPath, contents);
3316
+ return tempPath;
2358
3317
  }
2359
3318
  const matchNames = matches.map((m) => basename(m)).join(", ");
2360
3319
  throw new Error(
@@ -2510,7 +3469,7 @@ async function getContextInstance(contextDirectory) {
2510
3469
  );
2511
3470
  }
2512
3471
  const serverContext = ServerConfig.getContext();
2513
- if (serverContext) {
3472
+ if (serverContext?.hasContext()) {
2514
3473
  return serverContext;
2515
3474
  }
2516
3475
  return createToolContext(contextDirectory);
@@ -7472,5 +8431,5 @@ async function initializeWorkingDirectoryFromArgsAndConfig() {
7472
8431
  }
7473
8432
  }
7474
8433
 
7475
- export { DEFAULT_CONFIG_FILE as D, handleListResources as a, handleReadResource as b, getPrompt as c, initializeServerConfig as d, getCachedRoots as e, getOutputDirectory as f, getPrompts as g, handleToolCall as h, initializeWorkingDirectoryFromArgsAndConfig as i, getServerConfig as j, getContext as k, createQuietLogger as l, setWorkerInstance as m, setRoots as s, tools as t };
8434
+ export { DEFAULT_CONFIG_FILE as D, handleListResources as a, handleReadResource as b, getPrompt as c, initializeServerConfig as d, getCachedRoots as e, getOutputDirectory as f, getPrompts as g, handleToolCall as h, initializeWorkingDirectoryFromArgsAndConfig as i, getOutputStorage as j, getServerConfig as k, getStorageConfig as l, getContext as m, createQuietLogger as n, setWorkerInstance as o, setRoots as s, tools as t };
7476
8435
  //# sourceMappingURL=configDiscovery.js.map