@redaksjon/protokoll 1.0.0 → 1.0.2

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.
@@ -2,23 +2,26 @@ import * as readline from 'readline';
2
2
  import { spawn } from 'child_process';
3
3
  import * as path from 'node:path';
4
4
  import path__default from 'node:path';
5
- import * as fs$1 from 'fs/promises';
5
+ import * as fs$2 from 'fs/promises';
6
6
  import OpenAI$1, { OpenAI } from 'openai';
7
7
  import ffmpeg from 'fluent-ffmpeg';
8
8
  import * as os from 'node:os';
9
9
  import os__default from 'node:os';
10
+ import { IterationStrategyFactory, ToolRegistry, ConversationBuilder } from '@riotprompt/riotprompt';
11
+ import { z } from 'zod';
10
12
  import * as fs from 'node:fs';
13
+ import { existsSync, statSync } from 'node:fs';
11
14
  import { glob } from 'glob';
12
15
  import crypto from 'node:crypto';
13
- import * as fs$2 from 'node:fs/promises';
16
+ import * as fs$1 from 'node:fs/promises';
14
17
  import { htmlToText } from 'html-to-text';
15
18
  import { Command } from 'commander';
19
+ import { discoverOvercontext, discoverContextRoot } from '@theunwalked/overcontext';
20
+ import { redaksjonPluralNames, redaksjonSchemas } from '@redaksjon/context';
16
21
  import * as yaml from 'js-yaml';
17
- import { existsSync, statSync } from 'fs';
18
22
  import winston from 'winston';
19
- import { IterationStrategyFactory } from '@riotprompt/riotprompt';
20
23
 
21
- const VERSION = "1.0.0 (HEAD/9fe7b5f T:v1.0.0 2026-01-24 09:49:02 -0800) linux x64 v24.13.0";
24
+ const VERSION = "1.0.2 (HEAD/bcca7ab T:v1.0.2 2026-01-28 09:24:52 -0800) linux x64 v24.13.0";
22
25
  const PROGRAM_NAME = "protokoll";
23
26
  const DEFAULT_DIFF = true;
24
27
  const DEFAULT_LOG = false;
@@ -242,216 +245,168 @@ const create$w = (params) => {
242
245
  };
243
246
  };
244
247
 
245
- const isParentProject = (projectA, projectB) => {
246
- return projectB.relationships?.parent === projectA.id;
247
- };
248
- const isChildProject = (projectA, projectB) => {
249
- return projectA.relationships?.parent === projectB.id;
250
- };
251
- const areSiblingProjects = (projectA, projectB) => {
252
- const aSiblings = projectA.relationships?.siblings || [];
253
- const bSiblings = projectB.relationships?.siblings || [];
254
- return aSiblings.includes(projectB.id) || bSiblings.includes(projectA.id);
255
- };
256
- const getProjectRelationshipDistance = (projectA, projectB) => {
257
- if (projectA.id === projectB.id) return 0;
258
- if (isParentProject(projectA, projectB) || isChildProject(projectA, projectB)) return 1;
259
- if (areSiblingProjects(projectA, projectB)) return 2;
260
- if (projectA.relationships?.parent && projectB.relationships?.parent && projectA.relationships.parent === projectB.relationships.parent) {
261
- return 2;
262
- }
263
- return -1;
248
+ const protokollDiscoveryOptions = {
249
+ contextDirName: ".protokoll/context",
250
+ maxLevels: 10
264
251
  };
265
252
 
266
- const DIRECTORY_TO_TYPE = {
267
- "people": "person",
268
- "projects": "project",
269
- "companies": "company",
270
- "terms": "term",
271
- "ignored": "ignored"
272
- };
273
253
  const TYPE_TO_DIRECTORY = {
274
- "person": "people",
275
- "project": "projects",
276
- "company": "companies",
277
- "term": "terms",
278
- "ignored": "ignored"
254
+ person: "people",
255
+ project: "projects",
256
+ company: "companies",
257
+ term: "terms",
258
+ ignored: "ignored"
279
259
  };
280
260
  const create$v = () => {
281
- const entities = /* @__PURE__ */ new Map([
282
- ["person", /* @__PURE__ */ new Map()],
283
- ["project", /* @__PURE__ */ new Map()],
284
- ["company", /* @__PURE__ */ new Map()],
285
- ["term", /* @__PURE__ */ new Map()],
286
- ["ignored", /* @__PURE__ */ new Map()]
287
- ]);
288
- const load = async (contextDirs) => {
289
- for (const contextDir of contextDirs) {
290
- for (const dirName of Object.keys(DIRECTORY_TO_TYPE)) {
291
- const typeDir = path.join(contextDir, dirName);
292
- const entityType = DIRECTORY_TO_TYPE[dirName];
293
- try {
294
- const files = await fs$1.readdir(typeDir);
295
- for (const file of files) {
296
- if (!file.endsWith(".yaml") && !file.endsWith(".yml")) continue;
297
- const content = await fs$1.readFile(path.join(typeDir, file), "utf-8");
298
- const parsed = yaml.load(content);
299
- if (parsed && parsed.id) {
300
- entities.get(entityType)?.set(parsed.id, {
301
- ...parsed,
302
- type: entityType
303
- });
304
- }
305
- }
306
- } catch {
307
- }
261
+ const cache = /* @__PURE__ */ new Map();
262
+ let api;
263
+ let loadedContextDirs = [];
264
+ const initCache = () => {
265
+ cache.set("person", /* @__PURE__ */ new Map());
266
+ cache.set("project", /* @__PURE__ */ new Map());
267
+ cache.set("company", /* @__PURE__ */ new Map());
268
+ cache.set("term", /* @__PURE__ */ new Map());
269
+ cache.set("ignored", /* @__PURE__ */ new Map());
270
+ };
271
+ initCache();
272
+ return {
273
+ async load(contextDirs) {
274
+ initCache();
275
+ loadedContextDirs = contextDirs;
276
+ if (contextDirs.length === 0) {
277
+ api = void 0;
278
+ return;
308
279
  }
309
- }
310
- };
311
- const save = async (entity, targetDir) => {
312
- const dirName = TYPE_TO_DIRECTORY[entity.type];
313
- const dirPath = path.join(targetDir, "context", dirName);
314
- await fs$1.mkdir(dirPath, { recursive: true });
315
- const filePath = path.join(dirPath, `${entity.id}.yaml`);
316
- const { type: _entityType, ...entityWithoutType } = entity;
317
- const content = yaml.dump(entityWithoutType, { lineWidth: -1 });
318
- await fs$1.writeFile(filePath, content, "utf-8");
319
- entities.get(entity.type)?.set(entity.id, entity);
320
- };
321
- const deleteEntity = async (type, id, targetDir) => {
322
- const dirName = TYPE_TO_DIRECTORY[type];
323
- const possiblePaths = [
324
- path.join(targetDir, dirName, `${id}.yaml`),
325
- path.join(targetDir, dirName, `${id}.yml`),
326
- path.join(targetDir, "context", dirName, `${id}.yaml`),
327
- path.join(targetDir, "context", dirName, `${id}.yml`)
328
- ];
329
- for (const filePath of possiblePaths) {
330
280
  try {
331
- await fs$1.unlink(filePath);
332
- entities.get(type)?.delete(id);
333
- return true;
334
- } catch {
335
- }
336
- }
337
- return false;
338
- };
339
- const getEntityFilePath = (type, id, contextDirs) => {
340
- const dirName = TYPE_TO_DIRECTORY[type];
341
- for (const contextDir of [...contextDirs].reverse()) {
342
- const possiblePaths = [
343
- path.join(contextDir, dirName, `${id}.yaml`),
344
- path.join(contextDir, dirName, `${id}.yml`)
345
- ];
346
- for (const filePath of possiblePaths) {
347
- if (existsSync(filePath)) {
348
- const stat = statSync(filePath);
349
- if (stat.isFile()) {
350
- return filePath;
281
+ const lastContextDir = contextDirs[contextDirs.length - 1];
282
+ const protokollDir = lastContextDir.replace(/\/context$/, "");
283
+ const startDir = protokollDir.replace(/\/\.protokoll$/, "");
284
+ api = await discoverOvercontext({
285
+ schemas: redaksjonSchemas,
286
+ pluralNames: redaksjonPluralNames,
287
+ startDir,
288
+ ...protokollDiscoveryOptions
289
+ });
290
+ for (const type of ["person", "project", "company", "term", "ignored"]) {
291
+ const entities = await api.getAll(type);
292
+ for (const entity of entities) {
293
+ cache.get(type)?.set(entity.id, entity);
351
294
  }
352
295
  }
296
+ } catch (error) {
297
+ if (error instanceof Error && error.message.includes("No context directory found")) {
298
+ api = void 0;
299
+ } else {
300
+ throw error;
301
+ }
353
302
  }
354
- }
355
- return void 0;
356
- };
357
- const get = (type, id) => {
358
- return entities.get(type)?.get(id);
359
- };
360
- const getAll = (type) => {
361
- return Array.from(entities.get(type)?.values() ?? []);
362
- };
363
- const search = (query) => {
364
- const normalizedQuery = query.toLowerCase();
365
- const results = [];
366
- const seen = /* @__PURE__ */ new Set();
367
- for (const entityMap of entities.values()) {
368
- for (const entity of entityMap.values()) {
369
- let matched = false;
370
- if (entity.name.toLowerCase().includes(normalizedQuery)) {
371
- matched = true;
303
+ },
304
+ async save(entity, _targetDir) {
305
+ if (!api) {
306
+ const existing = cache.get(entity.type)?.get(entity.id);
307
+ if (existing) {
308
+ throw new Error(`Entity with id "${entity.id}" already exists`);
372
309
  }
373
- if (!matched) {
374
- const entityWithSoundsLike = entity;
375
- const variants = entityWithSoundsLike.sounds_like;
376
- if (variants?.some((v) => v.toLowerCase().includes(normalizedQuery))) {
377
- matched = true;
310
+ cache.get(entity.type)?.set(entity.id, entity);
311
+ return;
312
+ }
313
+ const saved = await api.upsert(entity.type, entity);
314
+ cache.get(entity.type)?.set(saved.id, saved);
315
+ },
316
+ async delete(type, id, _targetDir) {
317
+ if (!api) return false;
318
+ const deleted = await api.delete(type, id);
319
+ if (deleted) {
320
+ cache.get(type)?.delete(id);
321
+ }
322
+ return deleted;
323
+ },
324
+ get(type, id) {
325
+ return cache.get(type)?.get(id);
326
+ },
327
+ getAll(type) {
328
+ return Array.from(cache.get(type)?.values() ?? []);
329
+ },
330
+ search(query) {
331
+ const normalizedQuery = query.toLowerCase();
332
+ const results = [];
333
+ const seen = /* @__PURE__ */ new Set();
334
+ for (const entityMap of cache.values()) {
335
+ for (const entity of entityMap.values()) {
336
+ if (seen.has(entity.id)) continue;
337
+ if (entity.name.toLowerCase().includes(normalizedQuery)) {
338
+ results.push(entity);
339
+ seen.add(entity.id);
340
+ continue;
378
341
  }
379
- }
380
- if (!matched) {
381
- const entityWithSoundsLike = entity;
382
- const variants = entityWithSoundsLike.sounds_like;
383
- if (variants?.some((v) => v.toLowerCase() === normalizedQuery)) {
384
- matched = true;
342
+ const sounds = entity.sounds_like;
343
+ if (sounds?.some((s) => s.toLowerCase().includes(normalizedQuery))) {
344
+ results.push(entity);
345
+ seen.add(entity.id);
385
346
  }
386
347
  }
387
- if (matched && !seen.has(entity.id)) {
388
- results.push(entity);
389
- seen.add(entity.id);
348
+ }
349
+ return results;
350
+ },
351
+ findBySoundsLike(phonetic) {
352
+ const normalized = phonetic.toLowerCase().trim();
353
+ for (const entityMap of cache.values()) {
354
+ for (const entity of entityMap.values()) {
355
+ const sounds = entity.sounds_like;
356
+ if (sounds?.some((s) => s.toLowerCase() === normalized)) {
357
+ return entity;
358
+ }
390
359
  }
391
360
  }
392
- }
393
- return results;
394
- };
395
- const findBySoundsLike = (phonetic) => {
396
- const normalized = phonetic.toLowerCase().trim();
397
- for (const entityMap of entities.values()) {
398
- for (const entity of entityMap.values()) {
399
- const entityWithSoundsLike = entity;
400
- const variants = entityWithSoundsLike.sounds_like;
401
- if (variants?.some((v) => v.toLowerCase() === normalized)) {
402
- return entity;
361
+ return void 0;
362
+ },
363
+ clear() {
364
+ initCache();
365
+ api = void 0;
366
+ },
367
+ getEntityFilePath(type, id, contextDirs) {
368
+ const dirName = TYPE_TO_DIRECTORY[type];
369
+ const dirsToSearch = contextDirs.length > 0 ? contextDirs : loadedContextDirs;
370
+ for (const contextDir of [...dirsToSearch].reverse()) {
371
+ const possiblePaths = [
372
+ path.join(contextDir, dirName, `${id}.yaml`),
373
+ path.join(contextDir, dirName, `${id}.yml`)
374
+ ];
375
+ for (const filePath of possiblePaths) {
376
+ if (existsSync(filePath)) {
377
+ const stat = statSync(filePath);
378
+ if (stat.isFile()) {
379
+ return filePath;
380
+ }
381
+ }
403
382
  }
404
383
  }
405
- }
406
- return void 0;
407
- };
408
- const clear = () => {
409
- for (const entityMap of entities.values()) {
410
- entityMap.clear();
384
+ return void 0;
411
385
  }
412
386
  };
413
- return { load, save, delete: deleteEntity, get, getAll, search, findBySoundsLike, clear, getEntityFilePath };
414
387
  };
415
388
 
416
389
  const discoverConfigDirectories = async (options) => {
417
- const {
418
- configDirName,
419
- maxLevels = 10,
420
- startingDir = process.cwd()
421
- } = options;
422
- const discovered = [];
423
- let currentDir = path.resolve(startingDir);
424
- let level = 0;
425
- const visited = /* @__PURE__ */ new Set();
426
- while (level < maxLevels) {
427
- const realPath = path.resolve(currentDir);
428
- if (visited.has(realPath)) break;
429
- visited.add(realPath);
430
- const configDirPath = path.join(currentDir, configDirName);
431
- try {
432
- const stat = await fs$1.stat(configDirPath);
433
- if (stat.isDirectory()) {
434
- discovered.push({ path: configDirPath, level });
435
- }
436
- } catch {
437
- }
438
- const parentDir = path.dirname(currentDir);
439
- if (parentDir === currentDir) break;
440
- currentDir = parentDir;
441
- level++;
442
- }
443
- return discovered;
390
+ const contextRoot = await discoverContextRoot({
391
+ startDir: options.startingDir || process.cwd(),
392
+ contextDirName: options.configDirName,
393
+ maxLevels: options.maxLevels || 10
394
+ });
395
+ return contextRoot.directories.map((dir) => ({
396
+ path: dir.path,
397
+ level: dir.level
398
+ }));
444
399
  };
445
400
  const loadHierarchicalConfig = async (options) => {
446
- const discoveredDirs = await discoverConfigDirectories(options);
447
- if (discoveredDirs.length === 0) {
401
+ const discovered = await discoverConfigDirectories(options);
402
+ if (discovered.length === 0) {
448
403
  return {
449
404
  config: {},
450
405
  discoveredDirs: [],
451
406
  contextDirs: []
452
407
  };
453
408
  }
454
- const sortedDirs = [...discoveredDirs].sort((a, b) => b.level - a.level);
409
+ const sortedDirs = [...discovered].sort((a, b) => b.level - a.level);
455
410
  const configs = [];
456
411
  const contextDirs = [];
457
412
  for (const dir of sortedDirs) {
@@ -474,41 +429,36 @@ const loadHierarchicalConfig = async (options) => {
474
429
  }
475
430
  }
476
431
  const mergedConfig = configs.reduce(
477
- (acc, curr) => deepMerge(acc, curr),
432
+ (acc, curr) => ({ ...acc, ...curr }),
478
433
  {}
479
434
  );
480
435
  return {
481
436
  config: mergedConfig,
482
- discoveredDirs,
437
+ discoveredDirs: discovered,
483
438
  contextDirs
484
439
  };
485
440
  };
486
- function deepMerge(target, source) {
487
- if (source === null || source === void 0) return target;
488
- if (target === null || target === void 0) return source;
489
- if (typeof source !== "object" || typeof target !== "object") {
490
- return source;
491
- }
492
- if (Array.isArray(source)) {
493
- return [...source];
494
- }
495
- const result = { ...target };
496
- for (const key in source) {
497
- if (Object.prototype.hasOwnProperty.call(source, key)) {
498
- const targetVal = result[key];
499
- const sourceVal = source[key];
500
- if (typeof targetVal === "object" && typeof sourceVal === "object" && targetVal !== null && sourceVal !== null && !Array.isArray(targetVal) && !Array.isArray(sourceVal)) {
501
- result[key] = deepMerge(
502
- targetVal,
503
- sourceVal
504
- );
505
- } else {
506
- result[key] = sourceVal;
507
- }
508
- }
441
+
442
+ const isParentProject = (projectA, projectB) => {
443
+ return projectB.relationships?.parent === projectA.id;
444
+ };
445
+ const isChildProject = (projectA, projectB) => {
446
+ return projectA.relationships?.parent === projectB.id;
447
+ };
448
+ const areSiblingProjects = (projectA, projectB) => {
449
+ const aSiblings = projectA.relationships?.siblings || [];
450
+ const bSiblings = projectB.relationships?.siblings || [];
451
+ return aSiblings.includes(projectB.id) || bSiblings.includes(projectA.id);
452
+ };
453
+ const getProjectRelationshipDistance = (projectA, projectB) => {
454
+ if (projectA.id === projectB.id) return 0;
455
+ if (isParentProject(projectA, projectB) || isChildProject(projectA, projectB)) return 1;
456
+ if (areSiblingProjects(projectA, projectB)) return 2;
457
+ if (projectA.relationships?.parent && projectB.relationships?.parent && projectA.relationships.parent === projectB.relationships.parent) {
458
+ return 2;
509
459
  }
510
- return result;
511
- }
460
+ return -1;
461
+ };
512
462
 
513
463
  const getSmartAssistanceConfig = (config) => {
514
464
  const smartConfig = config.smartAssistance;
@@ -576,10 +526,6 @@ const create$u = async (options = {}) => {
576
526
  },
577
527
  search: (query) => storage.search(query),
578
528
  findBySoundsLike: (phonetic) => storage.findBySoundsLike(phonetic),
579
- /**
580
- * Context-aware search that prefers entities related to context project.
581
- * Still returns standard search results if no context provided.
582
- */
583
529
  searchWithContext: (query, contextProjectId) => {
584
530
  const results = storage.search(query);
585
531
  if (!contextProjectId) {
@@ -607,10 +553,6 @@ const create$u = async (options = {}) => {
607
553
  });
608
554
  return scoredResults.sort((a, b) => b.score - a.score).map((r) => r.entity);
609
555
  },
610
- /**
611
- * Get all projects related to a given project within maxDistance
612
- * Distance: 0 = same, 1 = parent/child, 2 = siblings/cousins
613
- */
614
556
  getRelatedProjects: (projectId, maxDistance = 2) => {
615
557
  const project = storage.get("project", projectId);
616
558
  if (!project) return [];
@@ -1170,7 +1112,7 @@ const create$p = () => {
1170
1112
  error: `Unsupported file type: ${ext}. Supported: ${supportedExtensions.join(", ")}`
1171
1113
  };
1172
1114
  }
1173
- let content = await fs$2.readFile(filePath, "utf-8");
1115
+ let content = await fs$1.readFile(filePath, "utf-8");
1174
1116
  if (content.length > MAX_CONTENT_LENGTH) {
1175
1117
  content = content.substring(0, MAX_CONTENT_LENGTH) + "\n\n[Content truncated...]";
1176
1118
  }
@@ -1192,7 +1134,7 @@ const create$p = () => {
1192
1134
  const fetchDirectory = async (dirPath) => {
1193
1135
  logger.debug("Reading directory: %s", dirPath);
1194
1136
  try {
1195
- const files = await fs$2.readdir(dirPath);
1137
+ const files = await fs$1.readdir(dirPath);
1196
1138
  const priorityFiles = [
1197
1139
  "README.md",
1198
1140
  "readme.md",
@@ -1251,7 +1193,7 @@ const create$p = () => {
1251
1193
  return await fetchUrl(source);
1252
1194
  }
1253
1195
  const resolvedPath = path.resolve(source);
1254
- const stat = await fs$2.stat(resolvedPath);
1196
+ const stat = await fs$1.stat(resolvedPath);
1255
1197
  if (stat.isDirectory()) {
1256
1198
  return await fetchDirectory(resolvedPath);
1257
1199
  } else {
@@ -2443,9 +2385,9 @@ const create$l = (config) => {
2443
2385
  };
2444
2386
  };
2445
2387
  const ensureDirectories = async (paths) => {
2446
- await fs$1.mkdir(path.dirname(paths.intermediate.transcript), { recursive: true });
2447
- await fs$1.mkdir(path.dirname(paths.final), { recursive: true });
2448
- await fs$1.mkdir(path.dirname(paths.rawTranscript), { recursive: true });
2388
+ await fs$2.mkdir(path.dirname(paths.intermediate.transcript), { recursive: true });
2389
+ await fs$2.mkdir(path.dirname(paths.final), { recursive: true });
2390
+ await fs$2.mkdir(path.dirname(paths.rawTranscript), { recursive: true });
2449
2391
  logger.debug("Ensured output directories", {
2450
2392
  intermediate: path.dirname(paths.intermediate.transcript),
2451
2393
  final: path.dirname(paths.final),
@@ -2458,7 +2400,7 @@ const create$l = (config) => {
2458
2400
  throw new Error(`Invalid intermediate type: ${type}`);
2459
2401
  }
2460
2402
  const contentStr = typeof content === "string" ? content : JSON.stringify(content, null, 2);
2461
- await fs$1.writeFile(filePath, contentStr, "utf-8");
2403
+ await fs$2.writeFile(filePath, contentStr, "utf-8");
2462
2404
  logger.debug("Wrote intermediate file", { type, path: filePath });
2463
2405
  return filePath;
2464
2406
  };
@@ -2472,7 +2414,7 @@ const create$l = (config) => {
2472
2414
  finalContent = finalContent + entitySection;
2473
2415
  }
2474
2416
  }
2475
- await fs$1.writeFile(paths.final, finalContent, "utf-8");
2417
+ await fs$2.writeFile(paths.final, finalContent, "utf-8");
2476
2418
  logger.info("Wrote final transcript", { path: paths.final });
2477
2419
  return paths.final;
2478
2420
  };
@@ -2484,7 +2426,7 @@ const create$l = (config) => {
2484
2426
  for (const [type, filePath] of Object.entries(paths.intermediate)) {
2485
2427
  if (filePath) {
2486
2428
  try {
2487
- await fs$1.unlink(filePath);
2429
+ await fs$2.unlink(filePath);
2488
2430
  logger.debug("Removed intermediate file", { type, path: filePath });
2489
2431
  } catch {
2490
2432
  }
@@ -2493,7 +2435,7 @@ const create$l = (config) => {
2493
2435
  };
2494
2436
  const writeRawTranscript = async (paths, data) => {
2495
2437
  const filePath = paths.rawTranscript;
2496
- await fs$1.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
2438
+ await fs$2.writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
2497
2439
  logger.debug("Wrote raw transcript to .transcript/", { path: filePath });
2498
2440
  return filePath;
2499
2441
  };
@@ -2502,7 +2444,7 @@ const create$l = (config) => {
2502
2444
  const finalBasename = path.basename(finalOutputPath, path.extname(finalOutputPath));
2503
2445
  const rawTranscriptPath = path.join(finalDir, ".transcript", `${finalBasename}.json`);
2504
2446
  try {
2505
- const content = await fs$1.readFile(rawTranscriptPath, "utf-8");
2447
+ const content = await fs$2.readFile(rawTranscriptPath, "utf-8");
2506
2448
  return JSON.parse(content);
2507
2449
  } catch (error) {
2508
2450
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
@@ -2928,7 +2870,7 @@ const create$i = (config) => {
2928
2870
  };
2929
2871
  const save = async (report, path) => {
2930
2872
  const content = formatMarkdown(report) ;
2931
- await fs$1.writeFile(path, content, "utf-8");
2873
+ await fs$2.writeFile(path, content, "utf-8");
2932
2874
  logger.info("Saved reflection report", { path });
2933
2875
  };
2934
2876
  return {
@@ -3913,6 +3855,17 @@ const create$7 = (_ctx) => ({
3913
3855
  }
3914
3856
  });
3915
3857
 
3858
+ const toRiotTool = (tool, category) => ({
3859
+ name: tool.name,
3860
+ description: tool.description,
3861
+ parameters: tool.parameters,
3862
+ category,
3863
+ cost: "cheap",
3864
+ execute: async (params) => {
3865
+ const result = await tool.execute(params);
3866
+ return result;
3867
+ }
3868
+ });
3916
3869
  const create$6 = (ctx) => {
3917
3870
  const tools = [
3918
3871
  create$b(ctx),
@@ -3922,13 +3875,19 @@ const create$6 = (ctx) => {
3922
3875
  create$7()
3923
3876
  ];
3924
3877
  const toolMap = new Map(tools.map((t) => [t.name, t]));
3878
+ const riotRegistry = ToolRegistry.create();
3879
+ riotRegistry.register(toRiotTool(tools[0], "lookup"));
3880
+ riotRegistry.register(toRiotTool(tools[1], "lookup"));
3881
+ riotRegistry.register(toRiotTool(tools[2], "verification"));
3882
+ riotRegistry.register(toRiotTool(tools[3], "routing"));
3883
+ riotRegistry.register(toRiotTool(tools[4], "storage"));
3925
3884
  return {
3926
3885
  getTools: () => tools,
3927
- // Return flat tool definitions - reasoning client handles OpenAI formatting
3928
- getToolDefinitions: () => tools.map((tool) => ({
3929
- name: tool.name,
3930
- description: tool.description,
3931
- parameters: tool.parameters
3886
+ // Use RiotPrompt's OpenAI format export
3887
+ getToolDefinitions: () => riotRegistry.toOpenAIFormat().map((t) => ({
3888
+ name: t.function.name,
3889
+ description: t.function.description,
3890
+ parameters: t.function.parameters
3932
3891
  })),
3933
3892
  executeTool: async (name, args) => {
3934
3893
  const tool = toolMap.get(name);
@@ -3939,20 +3898,36 @@ const create$6 = (ctx) => {
3939
3898
  };
3940
3899
  }
3941
3900
  return tool.execute(args);
3942
- }
3901
+ },
3902
+ getRiotRegistry: () => riotRegistry
3943
3903
  };
3944
3904
  };
3945
3905
 
3906
+ const toRiotToolCalls = (toolCalls) => {
3907
+ return toolCalls.map((tc) => ({
3908
+ id: tc.id,
3909
+ type: "function",
3910
+ function: {
3911
+ name: tc.name,
3912
+ arguments: JSON.stringify(tc.arguments)
3913
+ }
3914
+ }));
3915
+ };
3946
3916
  const cleanResponseContent = (content) => {
3947
3917
  let cleaned = content.replace(/^(?:Using tools?|Let me|I'll|I will|Now I'll|First,?\s*I(?:'ll| will)).*?[\r\n]+/gim, "");
3948
- cleaned = cleaned.replace(/\{"tool":\s*"[^"]+",\s*"input":\s*\{[^}]*\}\}/g, "");
3918
+ cleaned = cleaned.replace(/\{"tool":\s*"[^"]+",\s*"(?:args|input)":\s*\{[^}]*\}\}/g, "");
3949
3919
  cleaned = cleaned.replace(/\b\w+_\w+\(\{[^}]*\}\)/g, "");
3920
+ cleaned = cleaned.replace(/^.*\s+to=\w+\.\w+.*$/gm, "");
3921
+ const spamPattern = /^.*[\u4E00-\u9FFF].*(app|官网|彩票|中彩票).*$/gm;
3922
+ cleaned = cleaned.replace(spamPattern, "");
3923
+ const corruptionStartPattern = /^[\u0530-\u058F\u0E00-\u0E7F\u0A80-\u0AFF\u0C00-\u0C7F].*$/gm;
3924
+ cleaned = cleaned.replace(corruptionStartPattern, "");
3950
3925
  const lines = cleaned.split("\n");
3951
3926
  let startIndex = 0;
3952
3927
  for (let i = 0; i < lines.length; i++) {
3953
3928
  const line = lines[i].trim();
3954
3929
  if (line === "") continue;
3955
- const isCommentary = /^(checking|verifying|looking|searching|analyzing|processing|determining|using|calling|executing|I'm|I am|Let me)/i.test(line) || line.includes("tool") || line.includes('{"') || line.includes("reasoning");
3930
+ const isCommentary = /^(checking|verifying|looking|searching|analyzing|processing|determining|using|calling|executing|I'm|I am|Let me)/i.test(line) || line.includes("tool") || line.includes('{"') || line.includes("reasoning") || line.includes("undefined");
3956
3931
  if (!isCommentary) {
3957
3932
  startIndex = i;
3958
3933
  break;
@@ -3961,6 +3936,7 @@ const cleanResponseContent = (content) => {
3961
3936
  if (startIndex > 0) {
3962
3937
  cleaned = lines.slice(startIndex).join("\n");
3963
3938
  }
3939
+ cleaned = cleaned.replace(/\n{3,}/g, "\n\n");
3964
3940
  return cleaned.trim();
3965
3941
  };
3966
3942
  const create$5 = (reasoning, ctx) => {
@@ -3986,7 +3962,20 @@ const create$5 = (reasoning, ctx) => {
3986
3962
  let iterations = 0;
3987
3963
  let totalTokens = 0;
3988
3964
  const maxIterations = 15;
3989
- const conversationHistory = [];
3965
+ const conversation = ConversationBuilder.create({ model: "gpt-4o" }).withTokenBudget({
3966
+ max: 1e5,
3967
+ // 100k token context window
3968
+ reserveForResponse: 4e3,
3969
+ // Reserve 4k tokens for response
3970
+ strategy: "summarize",
3971
+ // Summarize old messages if budget exceeded
3972
+ onBudgetExceeded: "compress",
3973
+ // Automatically compress when exceeded
3974
+ preserveSystem: true,
3975
+ // Always keep system messages
3976
+ preserveRecent: 5
3977
+ // Keep last 5 messages
3978
+ });
3990
3979
  const systemPrompt = `You are an intelligent transcription assistant. Your job is to:
3991
3980
  1. Analyze the transcript for names, projects, and companies
3992
3981
  2. Use the available tools to verify spellings and gather context
@@ -4015,7 +4004,7 @@ Available tools:
4015
4004
  - verify_spelling: Ask user about unknown terms (if interactive mode)
4016
4005
  - route_note: Determine where to file this note
4017
4006
  - store_context: Remember new information for future use`;
4018
- conversationHistory.push({ role: "system", content: systemPrompt });
4007
+ conversation.addSystemMessage(systemPrompt);
4019
4008
  const initialPrompt = `Here is the raw transcript to process:
4020
4009
 
4021
4010
  --- BEGIN TRANSCRIPT ---
@@ -4030,7 +4019,7 @@ Please:
4030
4019
 
4031
4020
  CRITICAL: Your response must contain ONLY the transcript text - no commentary, no explanations, no tool information.
4032
4021
  Remember: preserve ALL content, only fix transcription errors.`;
4033
- conversationHistory.push({ role: "user", content: initialPrompt });
4022
+ conversation.addUserMessage(initialPrompt);
4034
4023
  try {
4035
4024
  logger.debug("Starting agentic transcription - analyzing for names and routing...");
4036
4025
  let response = await reasoning.complete({
@@ -4042,15 +4031,14 @@ Remember: preserve ALL content, only fix transcription errors.`;
4042
4031
  if (response.usage) {
4043
4032
  totalTokens += response.usage.totalTokens;
4044
4033
  }
4045
- conversationHistory.push({
4046
- role: "assistant",
4047
- content: response.content,
4048
- tool_calls: response.toolCalls?.map((tc) => ({
4049
- id: tc.id,
4050
- name: tc.name,
4051
- arguments: tc.arguments
4052
- }))
4053
- });
4034
+ if (response.toolCalls && response.toolCalls.length > 0) {
4035
+ conversation.addAssistantWithToolCalls(
4036
+ response.content,
4037
+ toRiotToolCalls(response.toolCalls)
4038
+ );
4039
+ } else {
4040
+ conversation.addAssistantMessage(response.content);
4041
+ }
4054
4042
  while (response.toolCalls && response.toolCalls.length > 0 && iterations < maxIterations) {
4055
4043
  iterations++;
4056
4044
  logger.debug("Iteration %d: Processing %d tool calls...", iterations, response.toolCalls.length);
@@ -4534,11 +4522,7 @@ ${termName}: ${wizardResult.termDescription}`.trim() : linkedProject.description
4534
4522
  }
4535
4523
  }
4536
4524
  for (const tr of toolResults) {
4537
- conversationHistory.push({
4538
- role: "tool",
4539
- tool_call_id: tr.id,
4540
- content: tr.result
4541
- });
4525
+ conversation.addToolResult(tr.id, tr.result, tr.name);
4542
4526
  }
4543
4527
  const continuationPrompt = `Tool results received. Here's a reminder of your task:
4544
4528
 
@@ -4554,7 +4538,7 @@ When you're done with tool calls, output the COMPLETE corrected transcript as Ma
4554
4538
  Do NOT summarize - include ALL original content with corrections applied.
4555
4539
 
4556
4540
  CRITICAL REMINDER: Your response must contain ONLY the transcript text. Do NOT include any commentary, explanations, or processing notes - those will leak into the user-facing document.`;
4557
- conversationHistory.push({ role: "user", content: continuationPrompt });
4541
+ conversation.addUserMessage(continuationPrompt);
4558
4542
  response = await reasoning.complete({
4559
4543
  systemPrompt,
4560
4544
  prompt: continuationPrompt,
@@ -4563,24 +4547,38 @@ CRITICAL REMINDER: Your response must contain ONLY the transcript text. Do NOT i
4563
4547
  if (response.usage) {
4564
4548
  totalTokens += response.usage.totalTokens;
4565
4549
  }
4566
- conversationHistory.push({
4567
- role: "assistant",
4568
- content: response.content,
4569
- tool_calls: response.toolCalls?.map((tc) => ({
4570
- id: tc.id,
4571
- name: tc.name,
4572
- arguments: tc.arguments
4573
- }))
4574
- });
4550
+ if (response.toolCalls && response.toolCalls.length > 0) {
4551
+ conversation.addAssistantWithToolCalls(
4552
+ response.content,
4553
+ toRiotToolCalls(response.toolCalls)
4554
+ );
4555
+ } else {
4556
+ conversation.addAssistantMessage(response.content);
4557
+ }
4575
4558
  }
4576
4559
  if (response.content && response.content.length > 50) {
4577
4560
  const cleanedContent = cleanResponseContent(response.content);
4578
4561
  if (cleanedContent !== response.content) {
4562
+ const removedChars = response.content.length - cleanedContent.length;
4579
4563
  logger.warn(
4580
- "Removed leaked internal processing from response (%d -> %d chars)",
4564
+ "Removed leaked internal processing from response (%d -> %d chars, removed %d chars)",
4581
4565
  response.content.length,
4582
- cleanedContent.length
4566
+ cleanedContent.length,
4567
+ removedChars
4583
4568
  );
4569
+ const corruptionRatio = removedChars / response.content.length;
4570
+ const hasSuspiciousUnicode = /[\u0530-\u058F\u0E00-\u0E7F\u4E00-\u9FFF\u0A80-\u0AFF\u0C00-\u0C7F]/.test(response.content);
4571
+ if (corruptionRatio > 0.1 || hasSuspiciousUnicode) {
4572
+ logger.error(
4573
+ "SEVERE CORRUPTION DETECTED in LLM response (%.1f%% removed, suspicious unicode: %s)",
4574
+ corruptionRatio * 100,
4575
+ hasSuspiciousUnicode
4576
+ );
4577
+ logger.error(
4578
+ "Raw response preview (first 500 chars): %s",
4579
+ response.content.substring(0, 500).replace(/\n/g, "\\n")
4580
+ );
4581
+ }
4584
4582
  }
4585
4583
  state.correctedText = cleanedContent;
4586
4584
  state.confidence = 0.9;
@@ -4607,11 +4605,26 @@ CRITICAL: Your response must contain ONLY the corrected transcript text - absolu
4607
4605
  }
4608
4606
  const cleanedFinalContent = cleanResponseContent(finalResponse.content || transcriptText);
4609
4607
  if (cleanedFinalContent !== finalResponse.content) {
4608
+ const removedChars = (finalResponse.content?.length || 0) - cleanedFinalContent.length;
4610
4609
  logger.warn(
4611
- "Removed leaked internal processing from final response (%d -> %d chars)",
4610
+ "Removed leaked internal processing from final response (%d -> %d chars, removed %d chars)",
4612
4611
  finalResponse.content?.length || 0,
4613
- cleanedFinalContent.length
4612
+ cleanedFinalContent.length,
4613
+ removedChars
4614
4614
  );
4615
+ const corruptionRatio = removedChars / (finalResponse.content?.length || 1);
4616
+ const hasSuspiciousUnicode = /[\u0530-\u058F\u0E00-\u0E7F\u4E00-\u9FFF\u0A80-\u0AFF\u0C00-\u0C7F]/.test(finalResponse.content || "");
4617
+ if (corruptionRatio > 0.1 || hasSuspiciousUnicode) {
4618
+ logger.error(
4619
+ "SEVERE CORRUPTION DETECTED in final LLM response (%.1f%% removed, suspicious unicode: %s)",
4620
+ corruptionRatio * 100,
4621
+ hasSuspiciousUnicode
4622
+ );
4623
+ logger.error(
4624
+ "Raw response preview (first 500 chars): %s",
4625
+ (finalResponse.content || "").substring(0, 500).replace(/\n/g, "\\n")
4626
+ );
4627
+ }
4615
4628
  }
4616
4629
  state.correctedText = cleanedFinalContent;
4617
4630
  state.confidence = 0.8;
@@ -4633,6 +4646,40 @@ CRITICAL: Your response must contain ONLY the corrected transcript text - absolu
4633
4646
  return { process };
4634
4647
  };
4635
4648
 
4649
+ z.object({
4650
+ original: z.string().describe("Original text from transcript"),
4651
+ corrected: z.string().describe("Corrected spelling/name"),
4652
+ type: z.enum(["person", "project", "term", "company"]).describe("Entity type"),
4653
+ confidence: z.number().min(0).max(1).describe("Confidence in correction")
4654
+ });
4655
+ z.object({
4656
+ projectId: z.string().optional().describe("Matched project ID"),
4657
+ destination: z.object({
4658
+ path: z.string().describe("File destination path"),
4659
+ structure: z.enum(["none", "year", "month", "day"]).default("month")
4660
+ }),
4661
+ confidence: z.number().min(0).max(1).describe("Confidence in routing"),
4662
+ signals: z.array(z.object({
4663
+ type: z.enum(["explicit_phrase", "associated_person", "associated_company", "topic", "context_type"]),
4664
+ value: z.string(),
4665
+ weight: z.number()
4666
+ })).optional(),
4667
+ reasoning: z.string().optional().describe("Why this destination was chosen")
4668
+ });
4669
+ z.object({
4670
+ people: z.array(z.string()).describe("IDs of people mentioned"),
4671
+ projects: z.array(z.string()).describe("IDs of projects mentioned"),
4672
+ terms: z.array(z.string()).describe("IDs of terms mentioned"),
4673
+ companies: z.array(z.string()).describe("IDs of companies mentioned")
4674
+ });
4675
+ z.object({
4676
+ success: z.boolean(),
4677
+ data: z.any().optional(),
4678
+ error: z.string().optional(),
4679
+ needsUserInput: z.boolean().optional(),
4680
+ userPrompt: z.string().optional()
4681
+ });
4682
+
4636
4683
  const create$4 = (reasoning, toolContext) => {
4637
4684
  const executor = create$5(reasoning, toolContext);
4638
4685
  return {
@@ -5301,7 +5348,7 @@ const projectAssist = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.definePrope
5301
5348
 
5302
5349
  const print$1 = (text) => process.stdout.write(text + "\n");
5303
5350
  const parseTranscript = async (filePath) => {
5304
- const rawText = await fs$1.readFile(filePath, "utf-8");
5351
+ const rawText = await fs$2.readFile(filePath, "utf-8");
5305
5352
  const lines = rawText.split("\n");
5306
5353
  const result = {
5307
5354
  filePath,
@@ -5727,7 +5774,7 @@ const executeAction = async (file, options) => {
5727
5774
  }
5728
5775
  for (const filePath of filePaths) {
5729
5776
  try {
5730
- await fs$1.access(filePath);
5777
+ await fs$2.access(filePath);
5731
5778
  } catch {
5732
5779
  print$1(`Error: File not found: ${filePath}`);
5733
5780
  process.exit(1);
@@ -5771,15 +5818,15 @@ Custom title: ${options.title}`);
5771
5818
  print$1("...");
5772
5819
  }
5773
5820
  } else {
5774
- await fs$1.mkdir(path.dirname(result.outputPath), { recursive: true });
5775
- await fs$1.writeFile(result.outputPath, result.content, "utf-8");
5821
+ await fs$2.mkdir(path.dirname(result.outputPath), { recursive: true });
5822
+ await fs$2.writeFile(result.outputPath, result.content, "utf-8");
5776
5823
  print$1(`Combined transcript created: ${result.outputPath}`);
5777
5824
  if (options.verbose) {
5778
5825
  print$1("\nDeleting source files...");
5779
5826
  }
5780
5827
  for (const fp of filePaths) {
5781
5828
  try {
5782
- await fs$1.unlink(fp);
5829
+ await fs$2.unlink(fp);
5783
5830
  if (options.verbose) {
5784
5831
  print$1(` Deleted: ${fp}`);
5785
5832
  }
@@ -5799,7 +5846,7 @@ Custom title: ${options.title}`);
5799
5846
  process.exit(1);
5800
5847
  }
5801
5848
  try {
5802
- await fs$1.access(file);
5849
+ await fs$2.access(file);
5803
5850
  } catch {
5804
5851
  print$1(`Error: File not found: ${file}`);
5805
5852
  process.exit(1);
@@ -5839,10 +5886,10 @@ Custom title: ${options.title}`);
5839
5886
  print$1("...");
5840
5887
  }
5841
5888
  } else {
5842
- await fs$1.mkdir(path.dirname(result.outputPath), { recursive: true });
5843
- await fs$1.writeFile(result.outputPath, result.content, "utf-8");
5889
+ await fs$2.mkdir(path.dirname(result.outputPath), { recursive: true });
5890
+ await fs$2.writeFile(result.outputPath, result.content, "utf-8");
5844
5891
  if (isRename) {
5845
- await fs$1.unlink(file);
5892
+ await fs$2.unlink(file);
5846
5893
  print$1(`Transcript updated and renamed:`);
5847
5894
  print$1(` From: ${file}`);
5848
5895
  print$1(` To: ${result.outputPath}`);
@@ -6417,11 +6464,11 @@ const applyChanges = async (feedbackCtx) => {
6417
6464
  moved = true;
6418
6465
  }
6419
6466
  }
6420
- await fs$1.mkdir(path.dirname(newPath), { recursive: true });
6467
+ await fs$2.mkdir(path.dirname(newPath), { recursive: true });
6421
6468
  if (!feedbackCtx.dryRun) {
6422
- await fs$1.writeFile(newPath, feedbackCtx.transcriptContent, "utf-8");
6469
+ await fs$2.writeFile(newPath, feedbackCtx.transcriptContent, "utf-8");
6423
6470
  if (newPath !== feedbackCtx.transcriptPath) {
6424
- await fs$1.unlink(feedbackCtx.transcriptPath);
6471
+ await fs$2.unlink(feedbackCtx.transcriptPath);
6425
6472
  }
6426
6473
  }
6427
6474
  logger.info("Applied %d changes to transcript", feedbackCtx.changes.length);
@@ -6429,12 +6476,12 @@ const applyChanges = async (feedbackCtx) => {
6429
6476
  };
6430
6477
  const runFeedback = async (transcriptPath, options) => {
6431
6478
  try {
6432
- await fs$1.access(transcriptPath);
6479
+ await fs$2.access(transcriptPath);
6433
6480
  } catch {
6434
6481
  print(`Error: File not found: ${transcriptPath}`);
6435
6482
  process.exit(1);
6436
6483
  }
6437
- const transcriptContent = await fs$1.readFile(transcriptPath, "utf-8");
6484
+ const transcriptContent = await fs$2.readFile(transcriptPath, "utf-8");
6438
6485
  const context = await create$u();
6439
6486
  const reasoning = create$c({ model: options.model || DEFAULT_MODEL });
6440
6487
  const feedbackCtx = {
@@ -6559,7 +6606,7 @@ const getRawTranscriptPath = (finalPath) => {
6559
6606
  const readRawTranscript = async (finalPath) => {
6560
6607
  const rawPath = getRawTranscriptPath(finalPath);
6561
6608
  try {
6562
- const content = await fs$1.readFile(rawPath, "utf-8");
6609
+ const content = await fs$2.readFile(rawPath, "utf-8");
6563
6610
  return JSON.parse(content);
6564
6611
  } catch (error) {
6565
6612
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
@@ -6570,7 +6617,7 @@ const readRawTranscript = async (finalPath) => {
6570
6617
  };
6571
6618
  const readFinalTranscript = async (finalPath) => {
6572
6619
  try {
6573
- return await fs$1.readFile(finalPath, "utf-8");
6620
+ return await fs$2.readFile(finalPath, "utf-8");
6574
6621
  } catch (error) {
6575
6622
  if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
6576
6623
  return null;
@@ -6729,7 +6776,7 @@ const listTranscripts = async (options) => {
6729
6776
  if (endDate && dateTime && dateTime.date > endDate) continue;
6730
6777
  let stats;
6731
6778
  try {
6732
- stats = await fs$1.stat(filePath);
6779
+ stats = await fs$2.stat(filePath);
6733
6780
  if (!stats.isFile()) {
6734
6781
  continue;
6735
6782
  }
@@ -6738,7 +6785,7 @@ const listTranscripts = async (options) => {
6738
6785
  }
6739
6786
  let content;
6740
6787
  try {
6741
- content = await fs$1.readFile(filePath, "utf-8");
6788
+ content = await fs$2.readFile(filePath, "utf-8");
6742
6789
  } catch {
6743
6790
  continue;
6744
6791
  }