@redaksjon/protokoll 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.cursor/rules/definition-of-done.md +1 -0
  2. package/.cursor/rules/no-emoticons.md +26 -12
  3. package/README.md +483 -69
  4. package/dist/agentic/executor.js +473 -41
  5. package/dist/agentic/executor.js.map +1 -1
  6. package/dist/agentic/index.js.map +1 -1
  7. package/dist/agentic/tools/lookup-person.js +123 -4
  8. package/dist/agentic/tools/lookup-person.js.map +1 -1
  9. package/dist/agentic/tools/lookup-project.js +139 -22
  10. package/dist/agentic/tools/lookup-project.js.map +1 -1
  11. package/dist/agentic/tools/route-note.js +5 -1
  12. package/dist/agentic/tools/route-note.js.map +1 -1
  13. package/dist/arguments.js +6 -3
  14. package/dist/arguments.js.map +1 -1
  15. package/dist/cli/action.js +704 -0
  16. package/dist/cli/action.js.map +1 -0
  17. package/dist/cli/config.js +482 -0
  18. package/dist/cli/config.js.map +1 -0
  19. package/dist/cli/context.js +466 -0
  20. package/dist/cli/context.js.map +1 -0
  21. package/dist/cli/feedback.js +858 -0
  22. package/dist/cli/feedback.js.map +1 -0
  23. package/dist/cli/index.js +103 -0
  24. package/dist/cli/index.js.map +1 -0
  25. package/dist/cli/install.js +572 -0
  26. package/dist/cli/install.js.map +1 -0
  27. package/dist/cli/transcript.js +199 -0
  28. package/dist/cli/transcript.js.map +1 -0
  29. package/dist/constants.js +12 -5
  30. package/dist/constants.js.map +1 -1
  31. package/dist/context/discovery.js +1 -1
  32. package/dist/context/discovery.js.map +1 -1
  33. package/dist/context/index.js +25 -1
  34. package/dist/context/index.js.map +1 -1
  35. package/dist/context/storage.js +57 -4
  36. package/dist/context/storage.js.map +1 -1
  37. package/dist/interactive/handler.js +310 -9
  38. package/dist/interactive/handler.js.map +1 -1
  39. package/dist/main.js +11 -1
  40. package/dist/main.js.map +1 -1
  41. package/dist/output/index.js.map +1 -1
  42. package/dist/output/manager.js +47 -2
  43. package/dist/output/manager.js.map +1 -1
  44. package/dist/phases/complete.js +38 -3
  45. package/dist/phases/complete.js.map +1 -1
  46. package/dist/phases/locate.js +1 -1
  47. package/dist/phases/locate.js.map +1 -1
  48. package/dist/pipeline/orchestrator.js +104 -31
  49. package/dist/pipeline/orchestrator.js.map +1 -1
  50. package/dist/protokoll.js +68 -2
  51. package/dist/protokoll.js.map +1 -1
  52. package/dist/reasoning/client.js +83 -0
  53. package/dist/reasoning/client.js.map +1 -1
  54. package/dist/reasoning/index.js +1 -0
  55. package/dist/reasoning/index.js.map +1 -1
  56. package/dist/routing/router.js +2 -2
  57. package/dist/routing/router.js.map +1 -1
  58. package/dist/util/media.js +1 -1
  59. package/dist/util/media.js.map +1 -1
  60. package/dist/util/metadata.js.map +1 -1
  61. package/dist/util/sound.js +116 -0
  62. package/dist/util/sound.js.map +1 -0
  63. package/dist/util/storage.js +3 -3
  64. package/dist/util/storage.js.map +1 -1
  65. package/docs/duplicate-question-prevention.md +117 -0
  66. package/docs/examples.md +152 -0
  67. package/docs/interactive-context-example.md +92 -0
  68. package/docs/package-lock.json +6 -0
  69. package/docs/package.json +3 -1
  70. package/eslint.config.mjs +1 -1
  71. package/guide/action.md +375 -0
  72. package/guide/config.md +207 -0
  73. package/guide/configuration.md +82 -67
  74. package/guide/context-commands.md +574 -0
  75. package/guide/context-system.md +20 -7
  76. package/guide/development.md +106 -4
  77. package/guide/feedback.md +335 -0
  78. package/guide/index.md +100 -4
  79. package/guide/interactive.md +15 -14
  80. package/guide/quickstart.md +21 -7
  81. package/guide/reasoning.md +18 -4
  82. package/guide/routing.md +192 -97
  83. package/package.json +2 -3
  84. package/scripts/copy-assets.mjs +47 -0
  85. package/scripts/coverage-priority.mjs +323 -0
  86. package/tsconfig.tsbuildinfo +1 -1
  87. package/vite.config.ts +6 -13
  88. package/vitest.config.ts +5 -1
@@ -0,0 +1,116 @@
1
+ import { spawn } from 'child_process';
2
+ import { getLogger } from '../logging.js';
3
+
4
+ // macOS system sounds that work well for notifications
5
+ const MACOS_NOTIFICATION_SOUNDS = [
6
+ '/System/Library/Sounds/Glass.aiff',
7
+ '/System/Library/Sounds/Ping.aiff',
8
+ '/System/Library/Sounds/Pop.aiff',
9
+ '/System/Library/Sounds/Tink.aiff'
10
+ ];
11
+ // Default sound to use (Glass is similar to Cursor's notification)
12
+ const DEFAULT_MACOS_SOUND = MACOS_NOTIFICATION_SOUNDS[0];
13
+ /**
14
+ * Play a sound file using afplay (macOS)
15
+ */ const playWithAfplay = (soundPath)=>{
16
+ return new Promise((resolve)=>{
17
+ const afplay = spawn('afplay', [
18
+ soundPath
19
+ ], {
20
+ stdio: 'ignore',
21
+ detached: true
22
+ });
23
+ afplay.on('error', ()=>{
24
+ resolve(false);
25
+ });
26
+ afplay.on('close', (code)=>{
27
+ resolve(code === 0);
28
+ });
29
+ // Don't wait for the sound to finish - just fire and forget
30
+ afplay.unref();
31
+ // Consider it successful if spawn didn't throw
32
+ setTimeout(()=>resolve(true), 50);
33
+ });
34
+ };
35
+ /**
36
+ * Play Windows system notification sound using PowerShell
37
+ * Uses SystemSounds.Asterisk which is a pleasant notification tone
38
+ */ const playWithPowerShell = ()=>{
39
+ return new Promise((resolve)=>{
40
+ // Use PowerShell to access .NET System.Media.SystemSounds
41
+ // Asterisk is a pleasant notification sound, similar to macOS Glass
42
+ const ps = spawn('powershell', [
43
+ '-NoProfile',
44
+ '-NonInteractive',
45
+ '-Command',
46
+ '[System.Media.SystemSounds]::Asterisk.Play()'
47
+ ], {
48
+ stdio: 'ignore',
49
+ detached: true,
50
+ // On Windows, use shell to ensure PowerShell is found
51
+ shell: true
52
+ });
53
+ ps.on('error', ()=>{
54
+ resolve(false);
55
+ });
56
+ ps.on('close', (code)=>{
57
+ resolve(code === 0);
58
+ });
59
+ ps.unref();
60
+ // Consider it successful if spawn didn't throw
61
+ setTimeout(()=>resolve(true), 50);
62
+ });
63
+ };
64
+ /**
65
+ * Play terminal bell as fallback for Linux and other platforms
66
+ */ const playTerminalBell = ()=>{
67
+ // Write ASCII bell character to stdout
68
+ process.stdout.write('\x07');
69
+ };
70
+ const create = (config)=>{
71
+ const logger = getLogger();
72
+ const playNotification = async ()=>{
73
+ if (config.silent) {
74
+ logger.debug('Sound notification skipped (silent mode)');
75
+ return;
76
+ }
77
+ try {
78
+ // macOS: use afplay with system sounds
79
+ if (process.platform === 'darwin') {
80
+ const success = await playWithAfplay(DEFAULT_MACOS_SOUND);
81
+ if (success) {
82
+ logger.debug('Played notification sound: %s', DEFAULT_MACOS_SOUND);
83
+ return;
84
+ }
85
+ }
86
+ // Windows: use PowerShell to play system sound
87
+ if (process.platform === 'win32') {
88
+ const success = await playWithPowerShell();
89
+ if (success) {
90
+ logger.debug('Played Windows notification sound via PowerShell');
91
+ return;
92
+ }
93
+ }
94
+ // Linux and others: fall back to terminal bell
95
+ playTerminalBell();
96
+ logger.debug('Played terminal bell notification');
97
+ } catch (error) {
98
+ // Sound failures should never interrupt the workflow
99
+ logger.debug('Failed to play notification sound: %s', error);
100
+ // Try terminal bell as last resort
101
+ try {
102
+ playTerminalBell();
103
+ } catch {
104
+ // Silently ignore - sound is not critical
105
+ }
106
+ }
107
+ };
108
+ const isEnabled = ()=>!config.silent;
109
+ return {
110
+ playNotification,
111
+ isEnabled
112
+ };
113
+ };
114
+
115
+ export { create };
116
+ //# sourceMappingURL=sound.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sound.js","sources":["../../src/util/sound.ts"],"sourcesContent":["/**\n * Sound Notification Utility\n * \n * Plays system sounds to notify the user when interactive input is needed.\n * Similar to Cursor's notification behavior.\n * \n * Platform support:\n * - macOS: Uses afplay with system sounds (Glass.aiff)\n * - Windows: Uses PowerShell to play system notification sound\n * - Linux/Other: Falls back to terminal bell\n */\n\nimport { spawn } from 'child_process';\nimport * as Logging from '../logging';\n\nexport interface SoundConfig {\n /** Whether sounds are disabled (silent mode) */\n silent: boolean;\n}\n\nexport interface SoundInstance {\n /** Play a notification sound to get user's attention */\n playNotification(): Promise<void>;\n /** Check if sounds are enabled */\n isEnabled(): boolean;\n}\n\n// macOS system sounds that work well for notifications\nconst MACOS_NOTIFICATION_SOUNDS = [\n '/System/Library/Sounds/Glass.aiff',\n '/System/Library/Sounds/Ping.aiff', \n '/System/Library/Sounds/Pop.aiff',\n '/System/Library/Sounds/Tink.aiff',\n];\n\n// Default sound to use (Glass is similar to Cursor's notification)\nconst DEFAULT_MACOS_SOUND = MACOS_NOTIFICATION_SOUNDS[0];\n\n/**\n * Play a sound file using afplay (macOS)\n */\nconst playWithAfplay = (soundPath: string): Promise<boolean> => {\n return new Promise((resolve) => {\n const afplay = spawn('afplay', [soundPath], {\n stdio: 'ignore',\n detached: true,\n });\n\n afplay.on('error', () => {\n resolve(false);\n });\n\n afplay.on('close', (code) => {\n resolve(code === 0);\n });\n\n // Don't wait for the sound to finish - just fire and forget\n afplay.unref();\n \n // Consider it successful if spawn didn't throw\n setTimeout(() => resolve(true), 50);\n });\n};\n\n/**\n * Play Windows system notification sound using PowerShell\n * Uses SystemSounds.Asterisk which is a pleasant notification tone\n */\nconst playWithPowerShell = (): Promise<boolean> => {\n return new Promise((resolve) => {\n // Use PowerShell to access .NET System.Media.SystemSounds\n // Asterisk is a pleasant notification sound, similar to macOS Glass\n const ps = spawn('powershell', [\n '-NoProfile',\n '-NonInteractive', \n '-Command',\n '[System.Media.SystemSounds]::Asterisk.Play()'\n ], {\n stdio: 'ignore',\n detached: true,\n // On Windows, use shell to ensure PowerShell is found\n shell: true,\n });\n\n ps.on('error', () => {\n resolve(false);\n });\n\n ps.on('close', (code) => {\n resolve(code === 0);\n });\n\n ps.unref();\n \n // Consider it successful if spawn didn't throw\n setTimeout(() => resolve(true), 50);\n });\n};\n\n/**\n * Play terminal bell as fallback for Linux and other platforms\n */\nconst playTerminalBell = (): void => {\n // Write ASCII bell character to stdout\n process.stdout.write('\\x07');\n};\n\nexport const create = (config: SoundConfig): SoundInstance => {\n const logger = Logging.getLogger();\n \n const playNotification = async (): Promise<void> => {\n if (config.silent) {\n logger.debug('Sound notification skipped (silent mode)');\n return;\n }\n\n try {\n // macOS: use afplay with system sounds\n if (process.platform === 'darwin') {\n const success = await playWithAfplay(DEFAULT_MACOS_SOUND);\n if (success) {\n logger.debug('Played notification sound: %s', DEFAULT_MACOS_SOUND);\n return;\n }\n }\n \n // Windows: use PowerShell to play system sound\n if (process.platform === 'win32') {\n const success = await playWithPowerShell();\n if (success) {\n logger.debug('Played Windows notification sound via PowerShell');\n return;\n }\n }\n \n // Linux and others: fall back to terminal bell\n playTerminalBell();\n logger.debug('Played terminal bell notification');\n } catch (error) {\n // Sound failures should never interrupt the workflow\n logger.debug('Failed to play notification sound: %s', error);\n // Try terminal bell as last resort\n try {\n playTerminalBell();\n } catch {\n // Silently ignore - sound is not critical\n }\n }\n };\n\n const isEnabled = (): boolean => !config.silent;\n\n return {\n playNotification,\n isEnabled,\n };\n};\n\n"],"names":["MACOS_NOTIFICATION_SOUNDS","DEFAULT_MACOS_SOUND","playWithAfplay","soundPath","Promise","resolve","afplay","spawn","stdio","detached","on","code","unref","setTimeout","playWithPowerShell","ps","shell","playTerminalBell","process","stdout","write","create","config","logger","Logging","playNotification","silent","debug","platform","success","error","isEnabled"],"mappings":"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"}
@@ -1,7 +1,7 @@
1
- import * as fs from 'fs';
1
+ import * as fs from 'node:fs';
2
2
  import { glob } from 'glob';
3
- import path__default from 'path';
4
- import crypto from 'crypto';
3
+ import path__default from 'node:path';
4
+ import crypto from 'node:crypto';
5
5
 
6
6
  // eslint-disable-next-line no-restricted-imports
7
7
  const create = (params)=>{
@@ -1 +1 @@
1
- {"version":3,"file":"storage.js","sources":["../../src/util/storage.ts"],"sourcesContent":["// eslint-disable-next-line no-restricted-imports\nimport * as fs from 'fs';\nimport { glob } from 'glob';\nimport path from 'path';\nimport crypto from 'crypto';\n/**\n * This module exists to isolate filesystem operations from the rest of the codebase.\n * This makes testing easier by avoiding direct fs mocking in jest configuration.\n * \n * Additionally, abstracting storage operations allows for future flexibility - \n * this export utility may need to work with storage systems other than the local filesystem\n * (e.g. S3, Google Cloud Storage, etc).\n */\n\nexport interface Utility {\n exists: (path: string) => Promise<boolean>;\n isDirectory: (path: string) => Promise<boolean>;\n isFile: (path: string) => Promise<boolean>;\n isReadable: (path: string) => Promise<boolean>;\n isWritable: (path: string) => Promise<boolean>;\n isFileReadable: (path: string) => Promise<boolean>;\n isDirectoryWritable: (path: string) => Promise<boolean>;\n isDirectoryReadable: (path: string) => Promise<boolean>;\n createDirectory: (path: string) => Promise<void>;\n readFile: (path: string, encoding: string) => Promise<string>;\n readStream: (path: string) => Promise<fs.ReadStream>;\n writeFile: (path: string, data: string | Buffer, encoding: string) => Promise<void>;\n forEachFileIn: (directory: string, callback: (path: string) => Promise<void>, options?: { pattern: string }) => Promise<void>;\n hashFile: (path: string, length: number) => Promise<string>;\n listFiles: (directory: string) => Promise<string[]>;\n deleteFile: (path: string) => Promise<void>;\n getFileSize: (path: string) => Promise<number>;\n}\n\nexport const create = (params: { log?: (message: string, ...args: any[]) => void }): Utility => {\n\n // eslint-disable-next-line no-console\n const log = params.log || console.log;\n\n const exists = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.stat(path);\n return true;\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n } catch (error: any) {\n return false;\n }\n }\n\n const isDirectory = async (path: string): Promise<boolean> => {\n const stats = await fs.promises.stat(path);\n if (!stats.isDirectory()) {\n log(`${path} is not a directory`);\n return false;\n }\n return true;\n }\n\n const isFile = async (path: string): Promise<boolean> => {\n const stats = await fs.promises.stat(path);\n if (!stats.isFile()) {\n log(`${path} is not a file`);\n return false;\n }\n return true;\n }\n\n const isReadable = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.access(path, fs.constants.R_OK);\n } catch (error: any) {\n log(`${path} is not readable: %s %s`, error.message, error.stack);\n return false;\n }\n return true;\n }\n\n const isWritable = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.access(path, fs.constants.W_OK);\n } catch (error: any) {\n log(`${path} is not writable: %s %s`, error.message, error.stack);\n return false;\n }\n return true;\n }\n\n const isFileReadable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isFile(path) && await isReadable(path);\n }\n\n const isDirectoryWritable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isDirectory(path) && await isWritable(path);\n }\n\n const isDirectoryReadable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isDirectory(path) && await isReadable(path);\n }\n\n const createDirectory = async (path: string): Promise<void> => {\n try {\n await fs.promises.mkdir(path, { recursive: true });\n } catch (mkdirError: any) {\n throw new Error(`Failed to create output directory ${path}: ${mkdirError.message} ${mkdirError.stack}`);\n }\n }\n\n const readFile = async (path: string, encoding: string): Promise<string> => {\n return await fs.promises.readFile(path, { encoding: encoding as BufferEncoding });\n }\n\n const writeFile = async (path: string, data: string | Buffer, encoding: string): Promise<void> => {\n await fs.promises.writeFile(path, data, { encoding: encoding as BufferEncoding });\n }\n\n const forEachFileIn = async (directory: string, callback: (file: string) => Promise<void>, options: { pattern: string | string[] } = { pattern: '*.*' }): Promise<void> => {\n try {\n const files = await glob(options.pattern, { cwd: directory, nodir: true });\n for (const file of files) {\n await callback(path.join(directory, file));\n }\n } catch (err: any) {\n throw new Error(`Failed to glob pattern ${options.pattern} in ${directory}: ${err.message}`);\n }\n }\n\n const readStream = async (path: string): Promise<fs.ReadStream> => {\n return fs.createReadStream(path);\n }\n\n const hashFile = async (path: string, length: number): Promise<string> => {\n const file = await readFile(path, 'utf8');\n return crypto.createHash('sha256').update(file).digest('hex').slice(0, length);\n }\n\n const listFiles = async (directory: string): Promise<string[]> => {\n return await fs.promises.readdir(directory);\n }\n\n const deleteFile = async (path: string): Promise<void> => {\n await fs.promises.unlink(path);\n }\n\n const getFileSize = async (path: string): Promise<number> => {\n const stats = await fs.promises.stat(path);\n return stats.size;\n }\n\n return {\n exists,\n isDirectory,\n isFile,\n isReadable,\n isWritable,\n isFileReadable,\n isDirectoryWritable,\n isDirectoryReadable,\n createDirectory,\n readFile,\n readStream,\n writeFile,\n forEachFileIn,\n hashFile,\n listFiles,\n deleteFile,\n getFileSize,\n };\n}"],"names":["create","params","log","console","exists","path","fs","promises","stat","error","isDirectory","stats","isFile","isReadable","access","constants","R_OK","message","stack","isWritable","W_OK","isFileReadable","isDirectoryWritable","isDirectoryReadable","createDirectory","mkdir","recursive","mkdirError","Error","readFile","encoding","writeFile","data","forEachFileIn","directory","callback","options","pattern","files","glob","cwd","nodir","file","join","err","readStream","createReadStream","hashFile","length","crypto","createHash","update","digest","slice","listFiles","readdir","deleteFile","unlink","getFileSize","size"],"mappings":"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"}
1
+ {"version":3,"file":"storage.js","sources":["../../src/util/storage.ts"],"sourcesContent":["// eslint-disable-next-line no-restricted-imports\nimport * as fs from 'node:fs';\nimport { glob } from 'glob';\nimport path from 'node:path';\nimport crypto from 'node:crypto';\n/**\n * This module exists to isolate filesystem operations from the rest of the codebase.\n * This makes testing easier by avoiding direct fs mocking in jest configuration.\n * \n * Additionally, abstracting storage operations allows for future flexibility - \n * this export utility may need to work with storage systems other than the local filesystem\n * (e.g. S3, Google Cloud Storage, etc).\n */\n\nexport interface Utility {\n exists: (path: string) => Promise<boolean>;\n isDirectory: (path: string) => Promise<boolean>;\n isFile: (path: string) => Promise<boolean>;\n isReadable: (path: string) => Promise<boolean>;\n isWritable: (path: string) => Promise<boolean>;\n isFileReadable: (path: string) => Promise<boolean>;\n isDirectoryWritable: (path: string) => Promise<boolean>;\n isDirectoryReadable: (path: string) => Promise<boolean>;\n createDirectory: (path: string) => Promise<void>;\n readFile: (path: string, encoding: string) => Promise<string>;\n readStream: (path: string) => Promise<fs.ReadStream>;\n writeFile: (path: string, data: string | Buffer, encoding: string) => Promise<void>;\n forEachFileIn: (directory: string, callback: (path: string) => Promise<void>, options?: { pattern: string }) => Promise<void>;\n hashFile: (path: string, length: number) => Promise<string>;\n listFiles: (directory: string) => Promise<string[]>;\n deleteFile: (path: string) => Promise<void>;\n getFileSize: (path: string) => Promise<number>;\n}\n\nexport const create = (params: { log?: (message: string, ...args: any[]) => void }): Utility => {\n\n // eslint-disable-next-line no-console\n const log = params.log || console.log;\n\n const exists = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.stat(path);\n return true;\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n } catch (error: any) {\n return false;\n }\n }\n\n const isDirectory = async (path: string): Promise<boolean> => {\n const stats = await fs.promises.stat(path);\n if (!stats.isDirectory()) {\n log(`${path} is not a directory`);\n return false;\n }\n return true;\n }\n\n const isFile = async (path: string): Promise<boolean> => {\n const stats = await fs.promises.stat(path);\n if (!stats.isFile()) {\n log(`${path} is not a file`);\n return false;\n }\n return true;\n }\n\n const isReadable = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.access(path, fs.constants.R_OK);\n } catch (error: any) {\n log(`${path} is not readable: %s %s`, error.message, error.stack);\n return false;\n }\n return true;\n }\n\n const isWritable = async (path: string): Promise<boolean> => {\n try {\n await fs.promises.access(path, fs.constants.W_OK);\n } catch (error: any) {\n log(`${path} is not writable: %s %s`, error.message, error.stack);\n return false;\n }\n return true;\n }\n\n const isFileReadable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isFile(path) && await isReadable(path);\n }\n\n const isDirectoryWritable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isDirectory(path) && await isWritable(path);\n }\n\n const isDirectoryReadable = async (path: string): Promise<boolean> => {\n return await exists(path) && await isDirectory(path) && await isReadable(path);\n }\n\n const createDirectory = async (path: string): Promise<void> => {\n try {\n await fs.promises.mkdir(path, { recursive: true });\n } catch (mkdirError: any) {\n throw new Error(`Failed to create output directory ${path}: ${mkdirError.message} ${mkdirError.stack}`);\n }\n }\n\n const readFile = async (path: string, encoding: string): Promise<string> => {\n return await fs.promises.readFile(path, { encoding: encoding as BufferEncoding });\n }\n\n const writeFile = async (path: string, data: string | Buffer, encoding: string): Promise<void> => {\n await fs.promises.writeFile(path, data, { encoding: encoding as BufferEncoding });\n }\n\n const forEachFileIn = async (directory: string, callback: (file: string) => Promise<void>, options: { pattern: string | string[] } = { pattern: '*.*' }): Promise<void> => {\n try {\n const files = await glob(options.pattern, { cwd: directory, nodir: true });\n for (const file of files) {\n await callback(path.join(directory, file));\n }\n } catch (err: any) {\n throw new Error(`Failed to glob pattern ${options.pattern} in ${directory}: ${err.message}`);\n }\n }\n\n const readStream = async (path: string): Promise<fs.ReadStream> => {\n return fs.createReadStream(path);\n }\n\n const hashFile = async (path: string, length: number): Promise<string> => {\n const file = await readFile(path, 'utf8');\n return crypto.createHash('sha256').update(file).digest('hex').slice(0, length);\n }\n\n const listFiles = async (directory: string): Promise<string[]> => {\n return await fs.promises.readdir(directory);\n }\n\n const deleteFile = async (path: string): Promise<void> => {\n await fs.promises.unlink(path);\n }\n\n const getFileSize = async (path: string): Promise<number> => {\n const stats = await fs.promises.stat(path);\n return stats.size;\n }\n\n return {\n exists,\n isDirectory,\n isFile,\n isReadable,\n isWritable,\n isFileReadable,\n isDirectoryWritable,\n isDirectoryReadable,\n createDirectory,\n readFile,\n readStream,\n writeFile,\n forEachFileIn,\n hashFile,\n listFiles,\n deleteFile,\n getFileSize,\n };\n}"],"names":["create","params","log","console","exists","path","fs","promises","stat","error","isDirectory","stats","isFile","isReadable","access","constants","R_OK","message","stack","isWritable","W_OK","isFileReadable","isDirectoryWritable","isDirectoryReadable","createDirectory","mkdir","recursive","mkdirError","Error","readFile","encoding","writeFile","data","forEachFileIn","directory","callback","options","pattern","files","glob","cwd","nodir","file","join","err","readStream","createReadStream","hashFile","length","crypto","createHash","update","digest","slice","listFiles","readdir","deleteFile","unlink","getFileSize","size"],"mappings":"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"}
@@ -0,0 +1,117 @@
1
+ # Duplicate Question Prevention
2
+
3
+ ## Problem
4
+ When processing long transcripts in interactive mode, protokoll was asking the same questions repeatedly about people and projects that had already been resolved during the same session.
5
+
6
+ ## Root Cause
7
+ The lookup tools (`lookup_person`, `lookup_project`) were only checking the context files on disk, not the in-memory state of entities that were just resolved. When a user answered a question about "John Doe" early in the transcript, and the same name appeared again later, the tool would ask about it again because:
8
+
9
+ 1. The newly created entity was saved to disk but not immediately available in subsequent searches
10
+ 2. The tools didn't check if the entity was already resolved during this processing session
11
+
12
+ ## Solution
13
+
14
+ ### 1. Resolved Entities Tracking
15
+ Added `resolvedEntities` Map to the `ToolContext`:
16
+
17
+ ```typescript
18
+ export interface ToolContext {
19
+ // ... existing fields
20
+ resolvedEntities?: Map<string, string>; // Entities resolved during this session
21
+ }
22
+ ```
23
+
24
+ This Map tracks all entity resolutions made during the current transcript processing session.
25
+
26
+ ### 2. Check Before Prompting
27
+ Updated both `lookup_person` and `lookup_project` tools to check the resolved entities first:
28
+
29
+ ```typescript
30
+ // First, check if this person was already resolved in this session
31
+ if (ctx.resolvedEntities?.has(args.name)) {
32
+ const resolvedName = ctx.resolvedEntities.get(args.name);
33
+ return {
34
+ success: true,
35
+ data: {
36
+ found: true,
37
+ suggestion: `Already resolved: use "${resolvedName}"`,
38
+ cached: true,
39
+ },
40
+ };
41
+ }
42
+ ```
43
+
44
+ ### 3. Immediate Context Reload
45
+ After saving a new entity to disk, immediately reload the context:
46
+
47
+ ```typescript
48
+ await ctx.contextInstance.saveEntity(newProject);
49
+ await ctx.contextInstance.reload(); // Reload so subsequent searches find this entity
50
+ ```
51
+
52
+ This ensures that subsequent tool calls will find the newly created entity even before checking the resolved entities cache.
53
+
54
+ ## Benefits
55
+
56
+ **No Duplicate Questions:** Users are only asked about each person/project/term once per transcript
57
+ **Better UX:** Faster processing for long transcripts with repeated mentions
58
+ **Maintains State:** The resolved entities Map is shared across all tool executions in the session
59
+ **Backwards Compatible:** Works even if `resolvedEntities` is undefined (graceful fallback)
60
+
61
+ ## Example
62
+
63
+ ### Before (Annoying!)
64
+ ```
65
+ Processing transcript with 20 mentions of "Trey Toulson"...
66
+
67
+ ────────────────────────────────────────────────────────────
68
+ [Unknown Person Detected]
69
+ Name heard: "Trey Toulson"
70
+ ...
71
+ ────────────────────────────────────────────────────────────
72
+ Is the name spelled correctly? → User enters details
73
+
74
+ [... processing continues ...]
75
+
76
+ ────────────────────────────────────────────────────────────
77
+ [Unknown Person Detected]
78
+ Name heard: "Trey Toulson" ← SAME PERSON AGAIN!
79
+ ...
80
+ ────────────────────────────────────────────────────────────
81
+ Is the name spelled correctly? → User asked AGAIN!
82
+ ```
83
+
84
+ ### After (Fixed!)
85
+ ```
86
+ Processing transcript with 20 mentions of "Trey Toulson"...
87
+
88
+ ────────────────────────────────────────────────────────────
89
+ [Unknown Person Detected]
90
+ Name heard: "Trey Toulson"
91
+ ...
92
+ ────────────────────────────────────────────────────────────
93
+ Is the name spelled correctly? → User enters details
94
+
95
+ [... processing continues ...]
96
+
97
+ [Tool automatically uses cached resolution for "Trey Toulson"]
98
+ [No prompt - continues processing smoothly]
99
+ ```
100
+
101
+ ## Implementation Details
102
+
103
+ ### Files Modified
104
+ - `src/agentic/types.ts` - Added `resolvedEntities` to `ToolContext`
105
+ - `src/agentic/executor.ts` - Wire up resolved entities and reload context after saves
106
+ - `src/agentic/tools/lookup-person.ts` - Check cache before prompting
107
+ - `src/agentic/tools/lookup-project.ts` - Check cache before prompting
108
+
109
+ ### Test Coverage
110
+ Added 8 new tests in `tests/agentic/resolved-entities.test.ts`:
111
+ - Test that first lookup prompts
112
+ - Test that second lookup returns cached result
113
+ - Test that multiple lookups of same entity don't prompt again
114
+ - Test cross-tool entity sharing
115
+ - Test graceful handling when resolvedEntities is undefined
116
+
117
+ All 580 tests passing.
package/docs/examples.md CHANGED
@@ -222,3 +222,155 @@ cd ~/work/project-a
222
222
  protokoll --input-directory ./recordings
223
223
  ```
224
224
 
225
+ ## Scenario 11: Edit Transcript Title
226
+
227
+ Rename a transcript with a more meaningful title.
228
+
229
+ ```bash
230
+ # Change title (updates document heading and filename)
231
+ protokoll action --title "Q1 Budget Review Meeting" ~/notes/2026/01/15-1412-meeting.md
232
+
233
+ # Preview changes first
234
+ protokoll action --title "Q1 Budget Review" ~/notes/file.md --dry-run --verbose
235
+ ```
236
+
237
+ Result:
238
+ - Document heading changes to `# Q1 Budget Review Meeting`
239
+ - File renames to `15-1412-q1-budget-review-meeting.md`
240
+
241
+ ## Scenario 12: Move Transcript to Different Project
242
+
243
+ Realize a transcript belongs to a different project.
244
+
245
+ ```bash
246
+ # Move to different project (updates metadata and routes to project destination)
247
+ protokoll action --project client-alpha ~/notes/2026/01/15-1412-meeting.md
248
+
249
+ # Change both title and project
250
+ protokoll action --title "Alpha Kickoff" --project client-alpha ~/notes/file.md
251
+ ```
252
+
253
+ Result:
254
+ - Metadata updated with new project name and ID
255
+ - File moved to project's configured destination
256
+ - Original file removed
257
+
258
+ ## Scenario 13: Combine Multiple Transcripts
259
+
260
+ Merge several related transcripts from a long meeting.
261
+
262
+ ```bash
263
+ # Combine with a custom title
264
+ protokoll action --title "Full Team Standup" --combine "~/notes/2026/01/15-1412-part1.md
265
+ ~/notes/2026/01/15-1421-part2.md
266
+ ~/notes/2026/01/15-1435-part3.md"
267
+
268
+ # Combine and assign to project
269
+ protokoll action --title "Sprint 42 Planning" --project sprint-42 --combine "~/notes/misc1.md
270
+ ~/notes/misc2.md"
271
+
272
+ # Preview what would happen
273
+ protokoll action --combine "~/notes/files..." --dry-run --verbose
274
+ ```
275
+
276
+ Result:
277
+ - Single combined transcript with custom title
278
+ - Sorted chronologically by timestamp
279
+ - Durations summed, tags deduplicated
280
+ - Source files automatically deleted
281
+
282
+ ## Scenario 14: Reorganize Scattered Notes
283
+
284
+ Consolidate notes that were initially routed to the default location.
285
+
286
+ ```bash
287
+ # Find notes that mention a specific topic
288
+ ls ~/notes/2026/01/*standards*.md
289
+
290
+ # Combine them into a project
291
+ protokoll action --title "Fellow Standards Discussion" --project fellow-standards --combine "~/notes/2026/01/15-1412-ne-4th-st-0.md
292
+ ~/notes/2026/01/15-1421-dimension-talk.md
293
+ ~/notes/2026/01/15-1435-standards-continued.md"
294
+ ```
295
+
296
+ Result:
297
+ - All related notes combined into one comprehensive document
298
+ - Routed to the `fellow-standards` project destination
299
+ - Original scattered files cleaned up
300
+
301
+ ## Scenario 15: Fix a Misheard Term
302
+
303
+ A technical term was transcribed incorrectly.
304
+
305
+ ```bash
306
+ # The transcript has "WCMP" but should be "WCNP"
307
+ protokoll feedback ~/notes/2026/01/15-1412-meeting.md \
308
+ -f "Everywhere it says WCMP, that should be WCNP - Walmart's Native Cloud Platform"
309
+ ```
310
+
311
+ Result:
312
+ - "WCMP" replaced with "WCNP" throughout the transcript
313
+ - "WCNP" added to your vocabulary with the full expansion
314
+ - Phonetic variants stored so it won't be misheard again
315
+
316
+ ## Scenario 16: Fix a Misheard Name
317
+
318
+ A person's name was transcribed phonetically.
319
+
320
+ ```bash
321
+ # The transcript has "San Jay Grouper" but should be "Sanjay Gupta"
322
+ protokoll feedback ~/notes/2026/01/15-1412-meeting.md \
323
+ -f "San Jay Grouper is actually Sanjay Gupta"
324
+ ```
325
+
326
+ Result:
327
+ - Name corrected throughout the transcript
328
+ - Variations like "San Jay" or "Sanjay Grouper" also fixed
329
+ - Person added to context for future recognition
330
+
331
+ ## Scenario 17: Reassign to Different Project via Feedback
332
+
333
+ A transcript was routed to the wrong project.
334
+
335
+ ```bash
336
+ # Interactive feedback
337
+ protokoll feedback ~/notes/2026/01/15-1412-meeting.md
338
+
339
+ # When prompted: "This should be in the Quantum Readiness project"
340
+ ```
341
+
342
+ Result:
343
+ - Project metadata updated in the transcript
344
+ - File moved to the project's configured destination
345
+ - Filename updated according to project rules
346
+
347
+ ## Scenario 18: Preview Feedback Changes
348
+
349
+ See what would happen without making changes.
350
+
351
+ ```bash
352
+ # Dry run with verbose output
353
+ protokoll feedback ~/notes/2026/01/15-1412-meeting.md \
354
+ -f "YB should be Wibey" \
355
+ --dry-run --verbose
356
+ ```
357
+
358
+ Output:
359
+ ```
360
+ [Dry Run] Would apply the following changes:
361
+ - Replaced "YB" with "Wibey" (3 occurrences)
362
+ - Added term "Wibey" to context
363
+ ```
364
+
365
+ ## Scenario 19: Get Help with Feedback
366
+
367
+ Not sure what feedback you can give.
368
+
369
+ ```bash
370
+ # Show feedback examples
371
+ protokoll feedback --help-me
372
+
373
+ # Or during interactive session
374
+ protokoll feedback ~/notes/meeting.md
375
+ # Enter: "What kinds of feedback can I give?"
376
+ ```
@@ -0,0 +1,92 @@
1
+ # Interactive Prompt Context Improvements
2
+
3
+ ## Overview
4
+ When protokoll encounters an unknown person or project, it now provides rich context about the file being processed, making it easier to make informed decisions.
5
+
6
+ ## Before (Old Output)
7
+ ```
8
+ ────────────────────────────────────────────────────────────
9
+ [Unknown Person Detected]
10
+ Name heard: "Trey Toulson"
11
+ Context: Name heard: "Trey Toulson"
12
+ ────────────────────────────────────────────────────────────
13
+
14
+ Is the name spelled correctly? (Enter to accept, or type correction):
15
+ ```
16
+
17
+ **Problem:** No file context, duplicate information, unclear what recording this is from.
18
+
19
+ ## After (New Output)
20
+ ```
21
+ ────────────────────────────────────────────────────────────
22
+ [Unknown Person Detected]
23
+ Name heard: "Trey Toulson"
24
+
25
+ File: meeting-notes-2026-01-15.m4a
26
+ Date: Wed, Jan 15, 2026, 07:10 AM
27
+
28
+ Unknown person mentioned: "Trey Toulson"
29
+
30
+ Context from transcript:
31
+ "I had a really productive meeting with Trey Toulson yesterday.
32
+ He's the new VP of Engineering at Acme Corp and is interested
33
+ in collaborating on the Phoenix Initiative."
34
+ ────────────────────────────────────────────────────────────
35
+
36
+ Is the name spelled correctly? (Enter to accept, or type correction):
37
+ ```
38
+
39
+ **Benefits:**
40
+ - **File context:** See which recording this person was mentioned in
41
+ - **Date/time:** Know when the recording was made
42
+ - **Transcript excerpt:** See the sentences around where the name appears
43
+ - **Clear formatting:** Better visual hierarchy and no duplication
44
+
45
+ Now you can immediately see that Trey is the VP of Engineering at Acme Corp!
46
+
47
+ ## Project/Term Detection
48
+
49
+ Similar improvements for unknown projects:
50
+
51
+ ```
52
+ ────────────────────────────────────────────────────────────
53
+ [Unknown Project/Term]
54
+ Term: "Phoenix Initiative"
55
+
56
+ File: status-update-2026-01-15.m4a
57
+ Date: Wed, Jan 15, 2026, 02:30 PM
58
+
59
+ Unknown project/term: "Phoenix Initiative"
60
+
61
+ Context from transcript:
62
+ "We're making great progress on the Phoenix Initiative. The team
63
+ has completed the initial architecture design and we're ready to
64
+ start implementation next week."
65
+ ────────────────────────────────────────────────────────────
66
+
67
+ Is this a Project or a Term? (P/T, or Enter to skip):
68
+ ```
69
+
70
+ You can immediately see this is an active project with progress updates!
71
+
72
+ ## Implementation Details
73
+
74
+ The improvements were made to:
75
+ - `src/agentic/tools/lookup-person.ts` - Added file metadata and transcript context extraction
76
+ - `src/agentic/tools/lookup-project.ts` - Added file metadata and transcript context extraction
77
+ - `src/interactive/handler.ts` - Enhanced display formatting in wizard prompts
78
+
79
+ The `ToolContext` interface already provided:
80
+ - `sourceFile: string` - Full path to the audio file
81
+ - `audioDate: Date` - Recording creation date
82
+ - `transcriptText: string` - The full transcript being processed
83
+
84
+ ### Transcript Context Extraction
85
+
86
+ The tools now include intelligent context extraction that:
87
+ 1. **Finds the mention:** Searches for the name/term in the transcript (case-insensitive)
88
+ 2. **Extracts surrounding text:** Captures approximately one sentence before and after
89
+ 3. **Limits length:** Keeps context under ~300 characters to avoid overwhelming the prompt
90
+ 4. **Handles edge cases:** Gracefully handles missing mentions or very long sentences
91
+
92
+ This gives you immediate understanding of who someone is or what a project involves, right from the prompt!
@@ -49,6 +49,7 @@
49
49
  "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
50
50
  "dev": true,
51
51
  "license": "MIT",
52
+ "peer": true,
52
53
  "dependencies": {
53
54
  "@babel/code-frame": "^7.27.1",
54
55
  "@babel/generator": "^7.28.5",
@@ -1214,6 +1215,7 @@
1214
1215
  "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==",
1215
1216
  "dev": true,
1216
1217
  "license": "MIT",
1218
+ "peer": true,
1217
1219
  "dependencies": {
1218
1220
  "@types/prop-types": "*",
1219
1221
  "csstype": "^3.2.2"
@@ -1280,6 +1282,7 @@
1280
1282
  }
1281
1283
  ],
1282
1284
  "license": "MIT",
1285
+ "peer": true,
1283
1286
  "dependencies": {
1284
1287
  "baseline-browser-mapping": "^2.9.0",
1285
1288
  "caniuse-lite": "^1.0.30001759",
@@ -1549,6 +1552,7 @@
1549
1552
  "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
1550
1553
  "dev": true,
1551
1554
  "license": "MIT",
1555
+ "peer": true,
1552
1556
  "engines": {
1553
1557
  "node": ">=12"
1554
1558
  },
@@ -1590,6 +1594,7 @@
1590
1594
  "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
1591
1595
  "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
1592
1596
  "license": "MIT",
1597
+ "peer": true,
1593
1598
  "dependencies": {
1594
1599
  "loose-envify": "^1.1.0"
1595
1600
  },
@@ -1748,6 +1753,7 @@
1748
1753
  "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
1749
1754
  "dev": true,
1750
1755
  "license": "MIT",
1756
+ "peer": true,
1751
1757
  "dependencies": {
1752
1758
  "esbuild": "^0.27.0",
1753
1759
  "fdir": "^6.5.0",
package/docs/package.json CHANGED
@@ -7,7 +7,9 @@
7
7
  "scripts": {
8
8
  "dev": "vite",
9
9
  "build": "vite build && cp dist/index.html dist/404.html",
10
- "preview": "vite preview"
10
+ "preview": "vite preview",
11
+ "lint": "true",
12
+ "test": "true"
11
13
  },
12
14
  "dependencies": {
13
15
  "react": "^18.2.0",
package/eslint.config.mjs CHANGED
@@ -71,7 +71,7 @@ export default defineConfig([
71
71
  "no-console": ["error"],
72
72
 
73
73
  "no-restricted-imports": ["error", {
74
- paths: ["dayjs", "fs", "moment-timezone"],
74
+ paths: ["dayjs", "fs", "node:fs", "moment-timezone"],
75
75
  patterns: [
76
76
  {
77
77
  group: ["src/**"],