@redaksjon/protokoll 1.0.20 → 1.0.21-dev.20260225190148.00bb9be

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,16 +1,17 @@
1
1
  import { resolve, isAbsolute, relative, extname, join, dirname, basename } from 'node:path';
2
2
  import * as Metadata from '@redaksjon/protokoll-engine';
3
- import { Transcript, Media, Util, Pipeline, findIgnoredResilient, findCompanyResilient, findTermResilient, findPersonResilient, findProjectResilient, Reasoning, VALID_STATUSES, isValidStatus } from '@redaksjon/protokoll-engine';
3
+ import { Transcript, Media, Util, Pipeline, findIgnoredResilient, findCompanyResilient, findTermResilient, findPersonResilient, findProjectResilient, Routing, Phases, Reasoning, Agentic, VALID_STATUSES, isValidStatus } from '@redaksjon/protokoll-engine';
4
4
  import * as yaml from 'js-yaml';
5
- import { stat, readdir, mkdir, unlink } from 'node:fs/promises';
5
+ import { stat, readdir, mkdir, mkdtemp, rm, unlink } from 'node:fs/promises';
6
6
  import { readFileSync } from 'node:fs';
7
7
  import { fileURLToPath } from 'node:url';
8
8
  import { create as create$1, parseEntityUri as parseEntityUri$1, createRelationship, createDocumentContent, createCodeContent, createMarkdownContent, createTextContent, createUrlContent } from '@redaksjon/context';
9
- import os from 'node:os';
9
+ import os, { tmpdir } from 'node:os';
10
10
  import winston from 'winston';
11
11
  import { glob } from 'glob';
12
12
  import { randomUUID } from 'crypto';
13
- import { PklTranscript, listTranscripts as listTranscripts$1, readTranscript } from '@redaksjon/protokoll-format';
13
+ import { randomUUID as randomUUID$1 } from 'node:crypto';
14
+ import { PklTranscript, readTranscript, listTranscripts as listTranscripts$1 } from '@redaksjon/protokoll-format';
14
15
  import * as Cardigantime from '@utilarium/cardigantime';
15
16
 
16
17
  const SCHEME = "protokoll";
@@ -204,7 +205,7 @@ function isProtokolUri(uri) {
204
205
  return uri.startsWith(`${SCHEME}://`);
205
206
  }
206
207
 
207
- const VERSION = "1.0.20 (HEAD/435bd4a T:v1.0.20 2026-02-24 07:49:25 -0800) linux arm64 v24.13.1";
208
+ const VERSION = "1.0.21-dev.20260225190148.00bb9be (working/00bb9be 2026-02-25 11:00:08 -0800) linux arm64 v24.14.0";
208
209
  const PROGRAM_NAME = "protokoll";
209
210
  const DATE_FORMAT_YEAR_MONTH_DAY_HOURS_MINUTES_SECONDS = "YYYY-M-D-HHmmss";
210
211
  const DEFAULT_AUDIO_EXTENSIONS = ["mp3", "mp4", "mpeg", "mpga", "m4a", "wav", "webm", "qta"];
@@ -224,6 +225,7 @@ const DEFAULT_TERM_SOUNDS_LIKE_ON_ADD = true;
224
225
  const DEFAULT_TERM_DESCRIPTION_ON_ADD = true;
225
226
  const DEFAULT_TERM_TOPICS_ON_ADD = true;
226
227
  const DEFAULT_TERM_PROJECT_SUGGESTIONS = true;
228
+ const MAX_CONTENT_LENGTH = 15e3;
227
229
  const ASSIST_TIMEOUT_MS = 3e4;
228
230
  const DEFAULT_MAX_AUDIO_SIZE = 26214400;
229
231
  const DEFAULT_TEMP_DIRECTORY = os.tmpdir();
@@ -912,6 +914,37 @@ const shared = /*#__PURE__*/Object.freeze(/*#__PURE__*/Object.defineProperty({
912
914
  }, Symbol.toStringTag, { value: 'Module' }));
913
915
 
914
916
  const { listTranscripts, resolveTranscriptPath, readTranscriptContent, stripTranscriptExtension } = Transcript;
917
+ function parseStoredSummaries$1(raw) {
918
+ try {
919
+ const parsed = JSON.parse(raw);
920
+ if (!Array.isArray(parsed)) {
921
+ return [];
922
+ }
923
+ return parsed.map((item) => {
924
+ if (!item || typeof item !== "object") {
925
+ return null;
926
+ }
927
+ const record = item;
928
+ const id = String(record.id || "").trim();
929
+ const content = String(record.content || "").trim();
930
+ if (!id || !content) {
931
+ return null;
932
+ }
933
+ return {
934
+ id,
935
+ title: String(record.title || "").trim(),
936
+ audience: String(record.audience || "").trim(),
937
+ guidance: String(record.guidance || "").trim(),
938
+ stylePreset: String(record.stylePreset || "detailed").trim() || "detailed",
939
+ styleLabel: String(record.styleLabel || "Detailed summary").trim() || "Detailed summary",
940
+ content,
941
+ generatedAt: String(record.generatedAt || "").trim() || (/* @__PURE__ */ new Date()).toISOString()
942
+ };
943
+ }).filter((summary) => summary !== null).sort((a, b) => b.generatedAt.localeCompare(a.generatedAt));
944
+ } catch {
945
+ return [];
946
+ }
947
+ }
915
948
  async function readTranscriptResource(transcriptPath) {
916
949
  if (!transcriptPath || typeof transcriptPath !== "string") {
917
950
  throw new Error(`Invalid transcript path: ${transcriptPath}`);
@@ -927,6 +960,7 @@ async function readTranscriptResource(transcriptPath) {
927
960
  const { PklTranscript } = await import('@redaksjon/protokoll-format');
928
961
  const pklTranscript = PklTranscript.open(resolved.path, { readOnly: true });
929
962
  let rawTranscript = void 0;
963
+ let summaries = [];
930
964
  try {
931
965
  if (pklTranscript.hasRawTranscript) {
932
966
  const rawData = pklTranscript.rawTranscript;
@@ -939,6 +973,8 @@ async function readTranscriptResource(transcriptPath) {
939
973
  };
940
974
  }
941
975
  }
976
+ const historyArtifact = pklTranscript.getArtifact("summary_history");
977
+ summaries = parseStoredSummaries$1(historyArtifact?.data?.toString("utf8") || "[]");
942
978
  } finally {
943
979
  pklTranscript.close();
944
980
  }
@@ -965,7 +1001,8 @@ async function readTranscriptResource(transcriptPath) {
965
1001
  } : void 0
966
1002
  },
967
1003
  content,
968
- rawTranscript
1004
+ rawTranscript,
1005
+ summaries
969
1006
  };
970
1007
  return {
971
1008
  uri: buildTranscriptUri(identifierPath),
@@ -1625,6 +1662,58 @@ function getPrompts() {
1625
1662
  }
1626
1663
  ]
1627
1664
  },
1665
+ {
1666
+ name: "identify_tasks_from_transcript",
1667
+ description: "Identify task candidates from a transcript with a review-first flow. Instructs the assistant to identify first, present candidates with none preselected, and only create tasks after explicit user approval.",
1668
+ arguments: [
1669
+ {
1670
+ name: "transcriptPath",
1671
+ description: "Path or URI to the transcript to analyze",
1672
+ required: true
1673
+ },
1674
+ {
1675
+ name: "maxCandidates",
1676
+ description: "Optional maximum number of task candidates to identify (default: 25)",
1677
+ required: false
1678
+ },
1679
+ {
1680
+ name: "includeTagSuggestions",
1681
+ description: "Optional flag to include suggested tags (default: true)",
1682
+ required: false
1683
+ }
1684
+ ]
1685
+ },
1686
+ {
1687
+ name: "summarize_transcript",
1688
+ description: "Create an audience-aware transcript summary with privacy guardrails and style presets.",
1689
+ arguments: [
1690
+ {
1691
+ name: "transcriptPath",
1692
+ description: "Path to the transcript to summarize",
1693
+ required: true
1694
+ },
1695
+ {
1696
+ name: "audience",
1697
+ description: "Target audience for the summary (e.g. internal team, external attendee)",
1698
+ required: true
1699
+ },
1700
+ {
1701
+ name: "stylePreset",
1702
+ description: "Summary style: quick_bullets, detailed, attendee_facing (default: detailed)",
1703
+ required: false
1704
+ },
1705
+ {
1706
+ name: "guidance",
1707
+ description: "Additional instructions (especially privacy/sensitivity constraints)",
1708
+ required: false
1709
+ },
1710
+ {
1711
+ name: "summaryTitle",
1712
+ description: "Optional title to use for the summary output",
1713
+ required: false
1714
+ }
1715
+ ]
1716
+ },
1628
1717
  {
1629
1718
  name: "enrich_entity",
1630
1719
  description: "Add or update an entity with smart assistance for generating metadata.",
@@ -1731,6 +1820,10 @@ async function getPrompt(name, args) {
1731
1820
  return generateSetupProjectPrompt(args);
1732
1821
  case "review_transcript":
1733
1822
  return generateReviewTranscriptPrompt(args);
1823
+ case "identify_tasks_from_transcript":
1824
+ return generateIdentifyTasksFromTranscriptPrompt(args);
1825
+ case "summarize_transcript":
1826
+ return generateSummarizeTranscriptPrompt(args);
1734
1827
  case "enrich_entity":
1735
1828
  return generateEnrichEntityPrompt(args);
1736
1829
  case "batch_transcription":
@@ -1852,6 +1945,75 @@ async function generateReviewTranscriptPrompt(args) {
1852
1945
  }
1853
1946
  ];
1854
1947
  }
1948
+ async function generateIdentifyTasksFromTranscriptPrompt(args) {
1949
+ const transcriptPath = args.transcriptPath;
1950
+ if (!transcriptPath) {
1951
+ throw new Error("transcriptPath is required");
1952
+ }
1953
+ const maxCandidates = args.maxCandidates?.trim() || "25";
1954
+ const includeTagSuggestions = args.includeTagSuggestions?.trim() || "true";
1955
+ const template = loadTemplate("identify_tasks_from_transcript");
1956
+ const content = fillTemplate(template, {
1957
+ transcriptPath,
1958
+ maxCandidates,
1959
+ includeTagSuggestions
1960
+ });
1961
+ return [
1962
+ {
1963
+ role: "user",
1964
+ content: {
1965
+ type: "text",
1966
+ text: content
1967
+ }
1968
+ }
1969
+ ];
1970
+ }
1971
+ async function generateSummarizeTranscriptPrompt(args) {
1972
+ const transcriptPath = args.transcriptPath;
1973
+ const audience = args.audience;
1974
+ if (!transcriptPath) {
1975
+ throw new Error("transcriptPath is required");
1976
+ }
1977
+ if (!audience) {
1978
+ throw new Error("audience is required");
1979
+ }
1980
+ const presetMap = {
1981
+ quick_bullets: {
1982
+ label: "Quick paragraph + bullet points",
1983
+ instructions: "Write one concise paragraph followed by 4-8 bullets covering decisions, actions, and risks."
1984
+ },
1985
+ detailed: {
1986
+ label: "Detailed summary",
1987
+ instructions: "Write a structured summary with context, key discussion points, decisions, open questions, and next steps."
1988
+ },
1989
+ attendee_facing: {
1990
+ label: "Attendee-facing summary",
1991
+ instructions: "Write a professional external-facing summary suitable for attendees; avoid private/internal reflections unless explicitly approved."
1992
+ }
1993
+ };
1994
+ const presetKey = (args.stylePreset || "detailed").trim();
1995
+ const selectedPreset = presetMap[presetKey] ?? presetMap.detailed;
1996
+ const summaryTitleLine = args.summaryTitle?.trim() ? `- Target title: "${args.summaryTitle.trim()}"` : "- Target title: (auto-generate from transcript title and date)";
1997
+ const guidanceBlock = args.guidance?.trim() ? args.guidance.trim() : "No extra guidance provided.";
1998
+ const template = loadTemplate("summarize_transcript");
1999
+ const content = fillTemplate(template, {
2000
+ transcriptPath,
2001
+ audience: audience.trim(),
2002
+ styleLabel: selectedPreset.label,
2003
+ styleInstructions: selectedPreset.instructions,
2004
+ summaryTitleLine,
2005
+ guidanceBlock
2006
+ });
2007
+ return [
2008
+ {
2009
+ role: "user",
2010
+ content: {
2011
+ type: "text",
2012
+ text: content
2013
+ }
2014
+ }
2015
+ ];
2016
+ }
1855
2017
  async function generateEnrichEntityPrompt(args) {
1856
2018
  const template = loadTemplate("enrich_entity");
1857
2019
  const content = fillTemplate(template, args);
@@ -4026,6 +4188,125 @@ async function handleSuggestTermMetadata(args) {
4026
4188
  }
4027
4189
 
4028
4190
  const { ensurePklExtension, transcriptExists } = Transcript;
4191
+ function parseStoredSummaries(raw) {
4192
+ try {
4193
+ const parsed = JSON.parse(raw);
4194
+ if (!Array.isArray(parsed)) {
4195
+ return [];
4196
+ }
4197
+ return parsed.map((item) => {
4198
+ if (!item || typeof item !== "object") {
4199
+ return null;
4200
+ }
4201
+ const record = item;
4202
+ const id = String(record.id || "").trim();
4203
+ const content = String(record.content || "").trim();
4204
+ if (!id || !content) {
4205
+ return null;
4206
+ }
4207
+ return {
4208
+ id,
4209
+ title: String(record.title || "").trim(),
4210
+ audience: String(record.audience || "").trim(),
4211
+ guidance: String(record.guidance || "").trim(),
4212
+ stylePreset: String(record.stylePreset || "detailed").trim() || "detailed",
4213
+ styleLabel: String(record.styleLabel || "Detailed summary").trim() || "Detailed summary",
4214
+ content,
4215
+ generatedAt: String(record.generatedAt || "").trim() || (/* @__PURE__ */ new Date()).toISOString()
4216
+ };
4217
+ }).filter((summary) => summary !== null).sort((a, b) => b.generatedAt.localeCompare(a.generatedAt));
4218
+ } catch {
4219
+ return [];
4220
+ }
4221
+ }
4222
+ const SUMMARY_STYLE_PRESETS = {
4223
+ quick_bullets: {
4224
+ label: "Quick paragraph + bullet points",
4225
+ instructions: "Write one concise paragraph followed by 4-8 bullets covering decisions, actions, and risks."
4226
+ },
4227
+ detailed: {
4228
+ label: "Detailed summary",
4229
+ instructions: "Write a structured summary with context, key discussion points, decisions, open questions, and next steps."
4230
+ },
4231
+ attendee_facing: {
4232
+ label: "Attendee-facing summary",
4233
+ instructions: "Write a professional external-facing summary suitable for attendees; avoid private/internal reflections unless explicitly approved."
4234
+ }
4235
+ };
4236
+ function splitIntoCandidateSentences(content) {
4237
+ return content.split(/\n+|(?<=[.!?])\s+/g).map((sentence) => sentence.trim()).filter((sentence) => sentence.length >= 8);
4238
+ }
4239
+ function normalizeTaskText(sentence) {
4240
+ const normalized = sentence.replace(/^(?:i need to|we need to|i should|we should|let'?s|remember to|todo:|action item:)\s+/i, "").replace(/\s+/g, " ").trim();
4241
+ return normalized.charAt(0).toUpperCase() + normalized.slice(1);
4242
+ }
4243
+ function inferDueDate(sentence) {
4244
+ const lower = sentence.toLowerCase();
4245
+ if (lower.includes("today")) {
4246
+ return "today";
4247
+ }
4248
+ if (lower.includes("tomorrow")) {
4249
+ return "tomorrow";
4250
+ }
4251
+ if (lower.includes("next week")) {
4252
+ return "next week";
4253
+ }
4254
+ if (lower.includes("this week")) {
4255
+ return "this week";
4256
+ }
4257
+ if (lower.includes("by friday")) {
4258
+ return "by friday";
4259
+ }
4260
+ return null;
4261
+ }
4262
+ function extractHashtagTags(sentence) {
4263
+ const matches = sentence.match(/#[a-z0-9_-]+/gi) || [];
4264
+ return Array.from(new Set(matches.map((tag) => tag.slice(1).toLowerCase())));
4265
+ }
4266
+ function toConfidenceBucket(score) {
4267
+ if (score >= 0.75) {
4268
+ return "high";
4269
+ }
4270
+ if (score >= 0.5) {
4271
+ return "medium";
4272
+ }
4273
+ return "low";
4274
+ }
4275
+ function scoreTaskCandidate(sentence) {
4276
+ const explicitActionPattern = /\b(i need to|we need to|i should|we should|let'?s|remember to|todo|action item|i will|i'll|must)\b/i;
4277
+ const inferredIntentPattern = /\b(follow up|check|review|investigate|confirm|decide|plan|schedule|reach out|send|draft|prepare|update|fix|create|write|call|email|look into|figure out)\b/i;
4278
+ let score = 0;
4279
+ const rationaleParts = [];
4280
+ if (explicitActionPattern.test(sentence)) {
4281
+ score += 0.55;
4282
+ rationaleParts.push("explicit action language");
4283
+ }
4284
+ if (inferredIntentPattern.test(sentence)) {
4285
+ score += 0.35;
4286
+ rationaleParts.push("inferred follow-up intent");
4287
+ }
4288
+ if (inferDueDate(sentence)) {
4289
+ score += 0.1;
4290
+ rationaleParts.push("time cue detected");
4291
+ }
4292
+ if (score < 0.3) {
4293
+ return null;
4294
+ }
4295
+ return {
4296
+ score: Math.min(1, Number(score.toFixed(2))),
4297
+ rationale: rationaleParts.join("; ")
4298
+ };
4299
+ }
4300
+ function getSuggestedEntities(entities) {
4301
+ if (!entities) {
4302
+ return [];
4303
+ }
4304
+ const people = (entities.people || []).map((entity) => ({ ...entity, type: "person" }));
4305
+ const projects = (entities.projects || []).map((entity) => ({ ...entity, type: "project" }));
4306
+ const terms = (entities.terms || []).map((entity) => ({ ...entity, type: "term" }));
4307
+ const companies = (entities.companies || []).map((entity) => ({ ...entity, type: "company" }));
4308
+ return [...people, ...projects, ...terms, ...companies];
4309
+ }
4029
4310
  const readTranscriptTool = {
4030
4311
  name: "protokoll_read_transcript",
4031
4312
  description: "Read a transcript file and parse its metadata and content. Path is relative to the configured output directory. Returns structured data including title, metadata, routing info, and content.",
@@ -4099,6 +4380,34 @@ const listTranscriptsTool = {
4099
4380
  required: []
4100
4381
  }
4101
4382
  };
4383
+ const identifyTasksFromTranscriptTool = {
4384
+ name: "protokoll_identify_tasks_from_transcript",
4385
+ description: "Identify task candidates from transcript or note content without creating tasks. Returns structured candidates with confidence buckets, rationale, and metadata suggestions so users can review and choose which tasks to create.",
4386
+ inputSchema: {
4387
+ type: "object",
4388
+ properties: {
4389
+ transcriptPath: {
4390
+ type: "string",
4391
+ description: 'Transcript URI (preferred) or relative path from output directory. URI format: "protokoll://transcript/2026/2/12-1606-meeting" (no file extension). Path format: "2026/2/12-1606-meeting" or "2026/2/12-1606-meeting.pkl"'
4392
+ },
4393
+ maxCandidates: {
4394
+ type: "number",
4395
+ description: "Maximum number of candidates to return (default: 25, max: 50)",
4396
+ default: 25
4397
+ },
4398
+ includeTagSuggestions: {
4399
+ type: "boolean",
4400
+ description: "Whether to include suggested tags based on transcript metadata and hashtags (default: true)",
4401
+ default: true
4402
+ },
4403
+ contextDirectory: {
4404
+ type: "string",
4405
+ description: "Optional: Path to the .protokoll context directory"
4406
+ }
4407
+ },
4408
+ required: ["transcriptPath"]
4409
+ }
4410
+ };
4102
4411
  const editTranscriptTool = {
4103
4412
  name: "protokoll_edit_transcript",
4104
4413
  description: "Edit an existing transcript's title, project assignment, tags, and/or status. Path is relative to the configured output directory. IMPORTANT: When you change the title, this tool RENAMES THE FILE to match the new title (slugified). Always use this tool instead of directly editing transcript files when changing titles. Changing the project will update metadata and may move the file to a new location based on the project's routing configuration.",
@@ -4140,6 +4449,68 @@ const editTranscriptTool = {
4140
4449
  required: ["transcriptPath"]
4141
4450
  }
4142
4451
  };
4452
+ const summarizeTranscriptTool = {
4453
+ name: "protokoll_summarize_transcript",
4454
+ description: "Generate an audience-aware summary for a transcript using privacy/sensitivity guardrails. Returns markdown summary text and does not modify transcript content.",
4455
+ inputSchema: {
4456
+ type: "object",
4457
+ properties: {
4458
+ transcriptPath: {
4459
+ type: "string",
4460
+ description: 'Transcript URI (preferred) or relative path from output directory. URI format: "protokoll://transcript/2026/2/12-1606-meeting" (no file extension). Path format: "2026/2/12-1606-meeting" or "2026/2/12-1606-meeting.pkl"'
4461
+ },
4462
+ audience: {
4463
+ type: "string",
4464
+ description: "Optional audience label (e.g. internal team, project attendees, external partner)"
4465
+ },
4466
+ stylePreset: {
4467
+ type: "string",
4468
+ enum: ["quick_bullets", "detailed", "attendee_facing"],
4469
+ description: "Summary style preset (default: detailed)",
4470
+ default: "detailed"
4471
+ },
4472
+ guidance: {
4473
+ type: "string",
4474
+ description: "Optional extra instructions, especially for privacy/sensitivity constraints"
4475
+ },
4476
+ summaryTitle: {
4477
+ type: "string",
4478
+ description: "Optional title to use in the generated summary"
4479
+ },
4480
+ model: {
4481
+ type: "string",
4482
+ description: `LLM model for summary generation (default: ${DEFAULT_MODEL})`
4483
+ },
4484
+ contextDirectory: {
4485
+ type: "string",
4486
+ description: "Optional: Path to the .protokoll context directory"
4487
+ }
4488
+ },
4489
+ required: ["transcriptPath"]
4490
+ }
4491
+ };
4492
+ const deleteTranscriptSummaryTool = {
4493
+ name: "protokoll_delete_transcript_summary",
4494
+ description: "Delete a previously generated summary from transcript artifact storage by summary ID. Path is relative to the configured output directory.",
4495
+ inputSchema: {
4496
+ type: "object",
4497
+ properties: {
4498
+ transcriptPath: {
4499
+ type: "string",
4500
+ description: 'Transcript URI (preferred) or relative path from output directory. URI format: "protokoll://transcript/2026/2/12-1606-meeting" (no file extension). Path format: "2026/2/12-1606-meeting" or "2026/2/12-1606-meeting.pkl"'
4501
+ },
4502
+ summaryId: {
4503
+ type: "string",
4504
+ description: 'Summary ID to remove (for example: "summary-174..." )'
4505
+ },
4506
+ contextDirectory: {
4507
+ type: "string",
4508
+ description: "Optional: Path to the .protokoll context directory"
4509
+ }
4510
+ },
4511
+ required: ["transcriptPath", "summaryId"]
4512
+ }
4513
+ };
4143
4514
  const changeTranscriptDateTool = {
4144
4515
  name: "protokoll_change_transcript_date",
4145
4516
  description: "Change the date of an existing transcript. This will move the transcript file to a new location based on the new date and the project's routing configuration. The file will be moved to the appropriate YYYY/MM/ directory structure. Path is relative to the configured output directory. WARNING: This may remove the transcript from the current view if it moves to a different date folder.",
@@ -4215,6 +4586,32 @@ const provideFeedbackTool = {
4215
4586
  required: ["transcriptPath", "feedback"]
4216
4587
  }
4217
4588
  };
4589
+ const enhanceTranscriptTool = {
4590
+ name: "protokoll_enhance_transcript",
4591
+ description: "Enhance an existing transcript using the same post-transcription pipeline flow (simple-replace + agentic tool-based enhancement) used after Whisper completes. Reads from originalText when provided, otherwise uses raw transcript text if available, falling back to current transcript content. Writes enhanced content and updates metadata/status.",
4592
+ inputSchema: {
4593
+ type: "object",
4594
+ properties: {
4595
+ transcriptPath: {
4596
+ type: "string",
4597
+ description: 'Transcript URI (preferred) or relative path from output directory. URI format: "protokoll://transcript/2026/2/12-1606-meeting" (no file extension). Path format: "2026/2/12-1606-meeting" or "2026/2/12-1606-meeting.pkl"'
4598
+ },
4599
+ originalText: {
4600
+ type: "string",
4601
+ description: "Optional explicit source text to enhance (usually the Original tab text). If omitted, tool uses raw transcript text when present, else current content."
4602
+ },
4603
+ model: {
4604
+ type: "string",
4605
+ description: `LLM model for enhancement (default: ${DEFAULT_MODEL})`
4606
+ },
4607
+ contextDirectory: {
4608
+ type: "string",
4609
+ description: "Optional: Path to the .protokoll context directory"
4610
+ }
4611
+ },
4612
+ required: ["transcriptPath"]
4613
+ }
4614
+ };
4218
4615
  const updateTranscriptContentTool = {
4219
4616
  name: "protokoll_update_transcript_content",
4220
4617
  description: "Update the content section of a transcript file while preserving all metadata. Path is relative to the configured output directory. This tool replaces only the content between the --- delimiters, keeping all metadata intact. IMPORTANT: The content parameter should contain ONLY the transcript body text (the text after the --- delimiter), NOT the full transcript file with headers and metadata. If the full transcript is provided, the tool will automatically extract only the content section to prevent duplication.",
@@ -4419,11 +4816,42 @@ const correctToEntityTool = {
4419
4816
  required: ["transcriptPath", "selectedText", "entityType"]
4420
4817
  }
4421
4818
  };
4819
+ const rejectCorrectionTool = {
4820
+ name: "protokoll_reject_correction",
4821
+ description: "Reject a previously applied enhancement correction and undo its text replacement in the transcript. Also logs the rejection in enhancement_log for auditability.",
4822
+ inputSchema: {
4823
+ type: "object",
4824
+ properties: {
4825
+ transcriptPath: {
4826
+ type: "string",
4827
+ description: 'Transcript URI (preferred) or relative path from output directory. URI format: "protokoll://transcript/2026/2/12-1606-meeting" (no file extension). Path format: "2026/2/12-1606-meeting" or "2026/2/12-1606-meeting.pkl"'
4828
+ },
4829
+ correctionEntryId: {
4830
+ type: "number",
4831
+ description: "Enhancement log entry id for the correction_applied event to reject"
4832
+ },
4833
+ contextDirectory: {
4834
+ type: "string",
4835
+ description: "Optional: Path to the .protokoll context directory"
4836
+ }
4837
+ },
4838
+ required: ["transcriptPath", "correctionEntryId"]
4839
+ }
4840
+ };
4422
4841
  async function handleReadTranscript(args) {
4423
4842
  const absolutePath = await resolveTranscriptPath$1(args.transcriptPath, args.contextDirectory);
4424
4843
  const transcriptData = await readTranscript(absolutePath);
4425
4844
  const outputDirectory = await getConfiguredDirectory("outputDirectory", args.contextDirectory);
4426
4845
  const relativePath = await sanitizePath(absolutePath, outputDirectory);
4846
+ const transcriptHandle = PklTranscript.open(absolutePath, { readOnly: true });
4847
+ let summaries = [];
4848
+ try {
4849
+ const historyArtifact = transcriptHandle.getArtifact("summary_history");
4850
+ const rawHistory = historyArtifact?.data?.toString("utf8") || "[]";
4851
+ summaries = parseStoredSummaries(rawHistory);
4852
+ } finally {
4853
+ transcriptHandle.close();
4854
+ }
4427
4855
  return {
4428
4856
  filePath: relativePath,
4429
4857
  title: transcriptData.metadata.title || "",
@@ -4443,7 +4871,8 @@ async function handleReadTranscript(args) {
4443
4871
  },
4444
4872
  content: transcriptData.content,
4445
4873
  hasRawTranscript: transcriptData.hasRawTranscript,
4446
- contentLength: transcriptData.content.length
4874
+ contentLength: transcriptData.content.length,
4875
+ summaries
4447
4876
  };
4448
4877
  }
4449
4878
  async function handleListTranscripts(args) {
@@ -4496,6 +4925,236 @@ async function handleListTranscripts(args) {
4496
4925
  }
4497
4926
  };
4498
4927
  }
4928
+ async function handleIdentifyTasksFromTranscript(args) {
4929
+ const absolutePath = await resolveTranscriptPath$1(args.transcriptPath, args.contextDirectory);
4930
+ const transcriptData = await readTranscript(absolutePath);
4931
+ const content = transcriptData.content?.trim() || "";
4932
+ if (!content) {
4933
+ return {
4934
+ transcriptPath: args.transcriptPath,
4935
+ candidates: [],
4936
+ totalCandidates: 0,
4937
+ message: "Transcript content is empty; no task candidates identified."
4938
+ };
4939
+ }
4940
+ const limit = Math.max(1, Math.min(50, args.maxCandidates ?? 25));
4941
+ const includeTagSuggestions = args.includeTagSuggestions !== false;
4942
+ const existingTags = transcriptData.metadata.tags || [];
4943
+ const suggestedEntities = getSuggestedEntities(transcriptData.metadata.entities);
4944
+ const suggestedProject = {
4945
+ id: transcriptData.metadata.projectId || null,
4946
+ name: transcriptData.metadata.project || null
4947
+ };
4948
+ const candidates = splitIntoCandidateSentences(content).map((sentence, index) => {
4949
+ const scored = scoreTaskCandidate(sentence);
4950
+ if (!scored) {
4951
+ return null;
4952
+ }
4953
+ const sentenceTags = includeTagSuggestions ? extractHashtagTags(sentence) : [];
4954
+ const mergedTags = includeTagSuggestions ? Array.from(/* @__PURE__ */ new Set([...existingTags.map((tag) => tag.toLowerCase()), ...sentenceTags])) : [];
4955
+ const candidate = {
4956
+ id: `candidate-${index + 1}`,
4957
+ taskText: normalizeTaskText(sentence),
4958
+ confidence: scored.score,
4959
+ confidenceBucket: toConfidenceBucket(scored.score),
4960
+ rationale: scored.rationale,
4961
+ sourceExcerpt: sentence,
4962
+ suggestedDueDate: inferDueDate(sentence),
4963
+ suggestedProject,
4964
+ suggestedEntities,
4965
+ suggestedTags: mergedTags
4966
+ };
4967
+ return candidate;
4968
+ }).filter((candidate) => candidate !== null).sort((a, b) => b.confidence - a.confidence).slice(0, limit);
4969
+ return {
4970
+ transcriptPath: args.transcriptPath,
4971
+ candidates,
4972
+ totalCandidates: candidates.length,
4973
+ message: candidates.length > 0 ? `Identified ${candidates.length} task candidate(s).` : "No likely task candidates found in transcript content."
4974
+ };
4975
+ }
4976
+ async function handleSummarizeTranscript(args) {
4977
+ const startedAt = Date.now();
4978
+ const logSummary = (message, data) => {
4979
+ process.stdout.write(`Protokoll: [SUMMARY] ${message} ${JSON.stringify(data)}
4980
+ `);
4981
+ };
4982
+ logSummary("Tool call received", {
4983
+ transcriptPath: args.transcriptPath,
4984
+ audience: args.audience,
4985
+ stylePreset: args.stylePreset || "detailed",
4986
+ hasGuidance: !!args.guidance?.trim(),
4987
+ hasSummaryTitle: !!args.summaryTitle?.trim(),
4988
+ model: args.model || DEFAULT_MODEL
4989
+ });
4990
+ const absolutePath = await resolveTranscriptPath$1(args.transcriptPath, args.contextDirectory);
4991
+ logSummary("Resolved transcript path", {
4992
+ transcriptPath: args.transcriptPath,
4993
+ absolutePath
4994
+ });
4995
+ const transcriptData = await readTranscript(absolutePath);
4996
+ const transcriptContent = (transcriptData.content || "").trim();
4997
+ if (!transcriptContent) {
4998
+ logSummary("Transcript content empty, cannot summarize", {
4999
+ transcriptPath: args.transcriptPath,
5000
+ absolutePath
5001
+ });
5002
+ throw new Error("Transcript content is empty; cannot generate summary.");
5003
+ }
5004
+ const audience = (args.audience || "").trim() || "General audience";
5005
+ const stylePreset = (args.stylePreset || "detailed").trim();
5006
+ const selectedStyle = SUMMARY_STYLE_PRESETS[stylePreset] || SUMMARY_STYLE_PRESETS.detailed;
5007
+ const guidance = (args.guidance || "").trim();
5008
+ const model = args.model || DEFAULT_MODEL;
5009
+ const transcriptTitle = transcriptData.metadata.title || "Untitled transcript";
5010
+ const transcriptDate = transcriptData.metadata.date instanceof Date ? transcriptData.metadata.date.toISOString().slice(0, 10) : "unknown date";
5011
+ const preferredTitle = (args.summaryTitle || "").trim();
5012
+ const boundedContent = transcriptContent.length > MAX_CONTENT_LENGTH ? `${transcriptContent.slice(0, MAX_CONTENT_LENGTH)}
5013
+
5014
+ [...transcript truncated for summarization input length...]` : transcriptContent;
5015
+ const truncated = transcriptContent.length > MAX_CONTENT_LENGTH;
5016
+ logSummary("Prepared summary input", {
5017
+ transcriptTitle,
5018
+ transcriptDate,
5019
+ transcriptLength: transcriptContent.length,
5020
+ boundedLength: boundedContent.length,
5021
+ truncated,
5022
+ stylePreset
5023
+ });
5024
+ const reasoning = Reasoning.create({
5025
+ model,
5026
+ reasoningLevel: "medium"
5027
+ });
5028
+ const prompt = [
5029
+ "Create an audience-aware summary for the transcript below.",
5030
+ "",
5031
+ `Transcript title: ${transcriptTitle}`,
5032
+ `Transcript date: ${transcriptDate}`,
5033
+ `Audience: ${audience}`,
5034
+ `Style preset: ${selectedStyle.label}`,
5035
+ preferredTitle ? `Preferred summary title: ${preferredTitle}` : "Preferred summary title: (generate one)",
5036
+ "",
5037
+ "Style instructions:",
5038
+ selectedStyle.instructions,
5039
+ "",
5040
+ "Privacy and sensitivity guardrails:",
5041
+ "- Treat transcript content as potentially sensitive by default.",
5042
+ "- Exclude private internal reflections, personal judgments, or sensitive notes not appropriate for the audience.",
5043
+ "- If unsure whether a detail is audience-appropriate, exclude it or generalize safely.",
5044
+ "- Prefer factual and neutral language over speculative interpretation.",
5045
+ "",
5046
+ "Additional guidance:",
5047
+ guidance || "No extra guidance provided.",
5048
+ "",
5049
+ "Required output shape:",
5050
+ "1) Title",
5051
+ "2) Summary body matching the selected style preset",
5052
+ '3) Optional "Redactions / Exclusions" section listing what was intentionally omitted for audience safety',
5053
+ "",
5054
+ "Transcript:",
5055
+ boundedContent
5056
+ ].join("\n");
5057
+ const result = await reasoning.complete({
5058
+ prompt,
5059
+ systemPrompt: "You are an expert meeting summarizer. Return markdown only."
5060
+ });
5061
+ logSummary("Reasoning call completed", {
5062
+ transcriptPath: args.transcriptPath,
5063
+ model: result.model || model,
5064
+ durationMs: result.duration ?? null,
5065
+ finishReason: result.finishReason ?? null
5066
+ });
5067
+ const summary = (result.content || "").trim();
5068
+ if (!summary) {
5069
+ logSummary("Empty summary response", {
5070
+ transcriptPath: args.transcriptPath,
5071
+ model: result.model || model
5072
+ });
5073
+ throw new Error("No summary text generated.");
5074
+ }
5075
+ logSummary("Summary generated successfully", {
5076
+ transcriptPath: args.transcriptPath,
5077
+ summaryLength: summary.length,
5078
+ elapsedMs: Date.now() - startedAt
5079
+ });
5080
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
5081
+ const summaryId = `summary-${Date.now()}-${randomUUID$1().slice(0, 8)}`;
5082
+ const stylePresetKey = SUMMARY_STYLE_PRESETS[stylePreset] ? stylePreset : "detailed";
5083
+ const storedSummary = {
5084
+ id: summaryId,
5085
+ title: preferredTitle || `${transcriptTitle} Summary`,
5086
+ audience,
5087
+ guidance,
5088
+ stylePreset: stylePresetKey,
5089
+ styleLabel: selectedStyle.label,
5090
+ content: summary,
5091
+ generatedAt
5092
+ };
5093
+ const transcriptHandle = PklTranscript.open(absolutePath, { readOnly: false });
5094
+ try {
5095
+ const existingHistory = transcriptHandle.getArtifact("summary_history");
5096
+ const existingSummaries = parseStoredSummaries(existingHistory?.data?.toString("utf8") || "[]");
5097
+ const nextSummaries = [storedSummary, ...existingSummaries.filter((entry) => entry.id !== summaryId)];
5098
+ transcriptHandle.addArtifact(
5099
+ "summary_history",
5100
+ Buffer.from(JSON.stringify(nextSummaries), "utf8"),
5101
+ {
5102
+ version: 1,
5103
+ count: nextSummaries.length,
5104
+ updatedAt: generatedAt,
5105
+ model: result.model || model
5106
+ }
5107
+ );
5108
+ } finally {
5109
+ transcriptHandle.close();
5110
+ }
5111
+ logSummary("Summary persisted to transcript artifact storage", {
5112
+ transcriptPath: args.transcriptPath,
5113
+ summaryId,
5114
+ generatedAt
5115
+ });
5116
+ return {
5117
+ summary,
5118
+ audience,
5119
+ stylePreset: stylePresetKey,
5120
+ model: result.model || model,
5121
+ summaryId,
5122
+ generatedAt
5123
+ };
5124
+ }
5125
+ async function handleDeleteTranscriptSummary(args) {
5126
+ const summaryId = (args.summaryId || "").trim();
5127
+ if (!summaryId) {
5128
+ throw new Error("summaryId is required");
5129
+ }
5130
+ const absolutePath = await resolveTranscriptPath$1(args.transcriptPath, args.contextDirectory);
5131
+ const transcriptHandle = PklTranscript.open(absolutePath, { readOnly: false });
5132
+ try {
5133
+ const historyArtifact = transcriptHandle.getArtifact("summary_history");
5134
+ const existingSummaries = parseStoredSummaries(historyArtifact?.data?.toString("utf8") || "[]");
5135
+ const remainingSummaries = existingSummaries.filter((entry) => entry.id !== summaryId);
5136
+ if (remainingSummaries.length === existingSummaries.length) {
5137
+ throw new Error(`Summary not found: ${summaryId}`);
5138
+ }
5139
+ transcriptHandle.addArtifact(
5140
+ "summary_history",
5141
+ Buffer.from(JSON.stringify(remainingSummaries), "utf8"),
5142
+ {
5143
+ version: 1,
5144
+ count: remainingSummaries.length,
5145
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
5146
+ deletedSummaryId: summaryId
5147
+ }
5148
+ );
5149
+ return {
5150
+ success: true,
5151
+ summaryId,
5152
+ remaining: remainingSummaries.length
5153
+ };
5154
+ } finally {
5155
+ transcriptHandle.close();
5156
+ }
5157
+ }
4499
5158
  async function handleEditTranscript(args) {
4500
5159
  await validateNotRemoteMode(args.contextDirectory);
4501
5160
  const outputDirectory = await getConfiguredDirectory("outputDirectory", args.contextDirectory);
@@ -4821,6 +5480,220 @@ async function handleProvideFeedback(args) {
4821
5480
  transcript.close();
4822
5481
  }
4823
5482
  }
5483
+ async function handleEnhanceTranscript(args) {
5484
+ const absolutePath = await resolveTranscriptPath$1(args.transcriptPath, args.contextDirectory);
5485
+ const pklPath = ensurePklExtension(absolutePath);
5486
+ const transcript = PklTranscript.open(pklPath, { readOnly: false });
5487
+ let tempDir = null;
5488
+ try {
5489
+ const explicitOriginal = (args.originalText || "").trim();
5490
+ const rawOriginal = (transcript.rawTranscript?.text || "").trim();
5491
+ const currentContent = (transcript.content || "").trim();
5492
+ const sourceText = explicitOriginal || rawOriginal || currentContent;
5493
+ if (!sourceText) {
5494
+ throw new Error("No source text available to enhance. Save or provide Original content first.");
5495
+ }
5496
+ const model = args.model || DEFAULT_MODEL;
5497
+ const startedAt = Date.now();
5498
+ let toolCallCount = 0;
5499
+ const contextDirectories = await getContextDirectories();
5500
+ const context = await create({
5501
+ startingDir: args.contextDirectory || dirname(pklPath),
5502
+ contextDirectories
5503
+ });
5504
+ const outputDirectory = await getConfiguredDirectory("outputDirectory", args.contextDirectory);
5505
+ const defaultStructure = "month";
5506
+ const defaultFilenameOptions = ["date", "time", "subject"];
5507
+ const routingProjects = context.getAllProjects().filter((project) => project.active !== false).map((project) => ({
5508
+ projectId: project.id,
5509
+ destination: {
5510
+ path: project.routing?.destination || outputDirectory,
5511
+ structure: project.routing?.structure || defaultStructure,
5512
+ filename_options: project.routing?.filename_options || [...defaultFilenameOptions],
5513
+ createDirectories: true
5514
+ },
5515
+ classification: project.classification,
5516
+ active: project.active,
5517
+ auto_tags: project.routing?.auto_tags
5518
+ }));
5519
+ const routing = Routing.create({
5520
+ default: {
5521
+ path: outputDirectory,
5522
+ structure: defaultStructure,
5523
+ filename_options: [...defaultFilenameOptions],
5524
+ createDirectories: true
5525
+ },
5526
+ projects: routingProjects,
5527
+ conflict_resolution: "primary"
5528
+ }, context);
5529
+ const fallbackDate = transcript.metadata.date instanceof Date ? transcript.metadata.date : /* @__PURE__ */ new Date();
5530
+ const fallbackHash = transcript.metadata.audioHash || transcript.metadata.id || "";
5531
+ const routingContext = {
5532
+ transcriptText: sourceText,
5533
+ audioDate: fallbackDate,
5534
+ sourceFile: pklPath,
5535
+ hash: fallbackHash
5536
+ };
5537
+ const routeResult = routing.route(routingContext);
5538
+ const projectForReplace = routeResult.projectId || transcript.metadata.projectId || transcript.metadata.project;
5539
+ transcript.enhancementLog.logStep(/* @__PURE__ */ new Date(), "enhance", "enhancement_start", {
5540
+ model,
5541
+ transcriptPath: args.transcriptPath,
5542
+ hasExplicitOriginal: explicitOriginal.length > 0,
5543
+ source: explicitOriginal ? "explicit_original_text" : rawOriginal ? "raw_transcript" : "enhanced_content_fallback",
5544
+ routedProject: routeResult.projectId || null,
5545
+ routedConfidence: routeResult.confidence
5546
+ });
5547
+ tempDir = await mkdtemp(resolve(tmpdir(), "protokoll-enhance-"));
5548
+ const simpleReplace = Phases.createSimpleReplacePhase({ debug: false }, context);
5549
+ const simpleReplaceResult = await simpleReplace.replace(
5550
+ sourceText,
5551
+ {
5552
+ project: projectForReplace || void 0,
5553
+ confidence: routeResult.confidence
5554
+ },
5555
+ tempDir,
5556
+ transcript.metadata.id || "manual-enhancement"
5557
+ );
5558
+ if (simpleReplaceResult.stats.totalReplacements > 0) {
5559
+ transcript.enhancementLog.logStep(/* @__PURE__ */ new Date(), "simple-replace", "phase_complete", {
5560
+ totalReplacements: simpleReplaceResult.stats.totalReplacements,
5561
+ tier1Replacements: simpleReplaceResult.stats.tier1Replacements,
5562
+ tier2Replacements: simpleReplaceResult.stats.tier2Replacements,
5563
+ projectContext: simpleReplaceResult.stats.projectContext,
5564
+ processingTimeMs: simpleReplaceResult.stats.processingTimeMs
5565
+ });
5566
+ for (const mapping of simpleReplaceResult.stats.appliedMappings) {
5567
+ transcript.enhancementLog.logStep(/* @__PURE__ */ new Date(), "simple-replace", "correction_applied", {
5568
+ original: mapping.soundsLike,
5569
+ replacement: mapping.correctText,
5570
+ tier: mapping.tier,
5571
+ occurrences: mapping.occurrences,
5572
+ entityId: mapping.entityId,
5573
+ entityType: mapping.entityType
5574
+ });
5575
+ }
5576
+ }
5577
+ const preIdentifiedEntities = {
5578
+ people: /* @__PURE__ */ new Set(),
5579
+ projects: /* @__PURE__ */ new Set(),
5580
+ terms: /* @__PURE__ */ new Set(),
5581
+ companies: /* @__PURE__ */ new Set()
5582
+ };
5583
+ for (const mapping of simpleReplaceResult.stats.appliedMappings) {
5584
+ if (!mapping.entityId || !mapping.entityType) {
5585
+ continue;
5586
+ }
5587
+ if (mapping.entityType === "person") {
5588
+ preIdentifiedEntities.people.add(mapping.entityId);
5589
+ } else if (mapping.entityType === "project") {
5590
+ preIdentifiedEntities.projects.add(mapping.entityId);
5591
+ } else if (mapping.entityType === "term") {
5592
+ preIdentifiedEntities.terms.add(mapping.entityId);
5593
+ }
5594
+ }
5595
+ const reasoning = Reasoning.create({ model, reasoningLevel: "medium" });
5596
+ const executor = Agentic.create(reasoning, {
5597
+ transcriptText: simpleReplaceResult.text,
5598
+ audioDate: fallbackDate,
5599
+ sourceFile: pklPath,
5600
+ contextInstance: context,
5601
+ routingInstance: routing,
5602
+ interactiveMode: false,
5603
+ preIdentifiedEntities,
5604
+ onToolCallStart: (tool, input) => {
5605
+ toolCallCount++;
5606
+ transcript.enhancementLog.logStep(/* @__PURE__ */ new Date(), "enhance", "tool_start", {
5607
+ callIndex: toolCallCount,
5608
+ tool,
5609
+ input
5610
+ });
5611
+ },
5612
+ onToolCallComplete: (entry) => {
5613
+ transcript.enhancementLog.logStep(entry.timestamp, "enhance", "tool_complete", {
5614
+ tool: entry.tool,
5615
+ input: entry.input,
5616
+ output: entry.output,
5617
+ durationMs: entry.durationMs,
5618
+ success: entry.success
5619
+ });
5620
+ }
5621
+ });
5622
+ const agenticResult = await executor.process(simpleReplaceResult.text);
5623
+ const enhancedText = (agenticResult.enhancedText || "").trim() || sourceText;
5624
+ const enhancementSucceeded = enhancedText.length > 50 && enhancedText !== sourceText;
5625
+ const finalStatus = enhancementSucceeded ? "enhanced" : transcript.metadata.status || "initial";
5626
+ const referenced = agenticResult.state.referencedEntities;
5627
+ const entities = {
5628
+ people: [],
5629
+ projects: [],
5630
+ terms: [],
5631
+ companies: []
5632
+ };
5633
+ for (const personId of referenced.people) {
5634
+ const person = context.getPerson(personId);
5635
+ if (person) {
5636
+ entities.people.push({ id: person.id, name: person.name, type: "person" });
5637
+ }
5638
+ }
5639
+ for (const projectId of referenced.projects) {
5640
+ const project = context.getProject(projectId);
5641
+ if (project) {
5642
+ entities.projects.push({ id: project.id, name: project.name, type: "project" });
5643
+ }
5644
+ }
5645
+ for (const termId of referenced.terms) {
5646
+ const term = context.getTerm(termId);
5647
+ if (term) {
5648
+ entities.terms.push({ id: term.id, name: term.name, type: "term" });
5649
+ }
5650
+ }
5651
+ for (const companyId of referenced.companies) {
5652
+ const company = context.getCompany(companyId);
5653
+ if (company) {
5654
+ entities.companies.push({ id: company.id, name: company.name, type: "company" });
5655
+ }
5656
+ }
5657
+ const hasEntities = entities.people.length > 0 || entities.projects.length > 0 || entities.terms.length > 0 || entities.companies.length > 0;
5658
+ const decidedProjectId = agenticResult.state.routeDecision?.projectId || routeResult.projectId || void 0;
5659
+ const decidedProject = decidedProjectId ? context.getProject(decidedProjectId) : void 0;
5660
+ const decidedConfidence = agenticResult.state.routeDecision?.confidence ?? routeResult.confidence;
5661
+ transcript.updateContent(enhancedText);
5662
+ transcript.updateMetadata({
5663
+ status: finalStatus,
5664
+ projectId: decidedProjectId || transcript.metadata.projectId,
5665
+ project: decidedProject?.name || transcript.metadata.project,
5666
+ confidence: typeof decidedConfidence === "number" ? decidedConfidence : transcript.metadata.confidence,
5667
+ entities: hasEntities ? entities : transcript.metadata.entities
5668
+ });
5669
+ transcript.enhancementLog.logStep(/* @__PURE__ */ new Date(), "enhance", "enhancement_complete", {
5670
+ status: finalStatus,
5671
+ toolsUsed: agenticResult.toolsUsed,
5672
+ totalToolCalls: toolCallCount,
5673
+ iterations: agenticResult.iterations,
5674
+ processingTimeMs: Date.now() - startedAt
5675
+ });
5676
+ return {
5677
+ success: true,
5678
+ transcriptPath: args.transcriptPath,
5679
+ status: finalStatus,
5680
+ projectId: decidedProjectId || null,
5681
+ projectName: decidedProject?.name || null,
5682
+ toolsUsed: agenticResult.toolsUsed,
5683
+ totalToolCalls: toolCallCount,
5684
+ iterations: agenticResult.iterations,
5685
+ processingTimeMs: Date.now() - startedAt,
5686
+ sourceLength: sourceText.length,
5687
+ enhancedLength: enhancedText.length,
5688
+ changed: enhancedText !== sourceText
5689
+ };
5690
+ } finally {
5691
+ if (tempDir) {
5692
+ await rm(tempDir, { recursive: true, force: true });
5693
+ }
5694
+ transcript.close();
5695
+ }
5696
+ }
4824
5697
  async function handleCreateNote(args) {
4825
5698
  const outputDirectory = await getConfiguredDirectory("outputDirectory", args.contextDirectory);
4826
5699
  const noteDate = args.date ? new Date(args.date) : /* @__PURE__ */ new Date();
@@ -4927,8 +5800,89 @@ function applyCorrections(transcriptText, corrections) {
4927
5800
  }
4928
5801
  return correctedText;
4929
5802
  }
5803
+ function countOccurrencesCaseInsensitive(text, target) {
5804
+ if (!target.trim()) {
5805
+ return 0;
5806
+ }
5807
+ const regex = new RegExp(target.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "gi");
5808
+ const matches = text.match(regex);
5809
+ return matches ? matches.length : 0;
5810
+ }
5811
+ async function handleRejectCorrection(args) {
5812
+ if (!Number.isInteger(args.correctionEntryId) || args.correctionEntryId < 1) {
5813
+ throw new Error("correctionEntryId must be a positive integer");
5814
+ }
5815
+ const absolutePath = await resolveTranscriptPath$1(args.transcriptPath, args.contextDirectory);
5816
+ const transcript = PklTranscript.open(absolutePath, { readOnly: false });
5817
+ try {
5818
+ const allEntries = transcript.getEnhancementLog();
5819
+ const correctionEntry = allEntries.find((entry) => entry.id === args.correctionEntryId);
5820
+ if (!correctionEntry) {
5821
+ throw new Error(`Correction entry not found: ${args.correctionEntryId}`);
5822
+ }
5823
+ if (correctionEntry.action !== "correction_applied") {
5824
+ throw new Error(`Entry ${args.correctionEntryId} is not a correction_applied action`);
5825
+ }
5826
+ const details = correctionEntry.details || {};
5827
+ const original = String(details.original || "").trim();
5828
+ const replacement = String(details.replacement || "").trim();
5829
+ if (!original || !replacement) {
5830
+ throw new Error(`Correction entry ${args.correctionEntryId} is missing original/replacement details`);
5831
+ }
5832
+ const alreadyRejected = allEntries.some((entry) => {
5833
+ if (entry.action !== "correction_rejected") {
5834
+ return false;
5835
+ }
5836
+ const rejectDetails = entry.details || {};
5837
+ return Number(rejectDetails.correctionEntryId) === args.correctionEntryId;
5838
+ });
5839
+ if (alreadyRejected) {
5840
+ return {
5841
+ success: true,
5842
+ alreadyRejected: true,
5843
+ correctionEntryId: args.correctionEntryId,
5844
+ original,
5845
+ replacement,
5846
+ revertedOccurrences: 0,
5847
+ message: `Correction #${args.correctionEntryId} is already rejected`
5848
+ };
5849
+ }
5850
+ const originalContent = transcript.content || "";
5851
+ const beforeCount = countOccurrencesCaseInsensitive(originalContent, replacement);
5852
+ const revertedContent = applyCorrections(
5853
+ originalContent,
5854
+ /* @__PURE__ */ new Map([[replacement, original]])
5855
+ );
5856
+ const afterCount = countOccurrencesCaseInsensitive(revertedContent, replacement);
5857
+ const revertedOccurrences = Math.max(0, beforeCount - afterCount);
5858
+ const rejectionPhase = correctionEntry.phase === "transcribe" || correctionEntry.phase === "enhance" || correctionEntry.phase === "simple-replace" ? correctionEntry.phase : "simple-replace";
5859
+ transcript.updateContent(revertedContent);
5860
+ transcript.enhancementLog.logStep(
5861
+ /* @__PURE__ */ new Date(),
5862
+ rejectionPhase,
5863
+ "correction_rejected",
5864
+ {
5865
+ correctionEntryId: args.correctionEntryId,
5866
+ original,
5867
+ replacement,
5868
+ revertedOccurrences,
5869
+ sourceTimestamp: correctionEntry.timestamp.toISOString()
5870
+ }
5871
+ );
5872
+ return {
5873
+ success: true,
5874
+ correctionEntryId: args.correctionEntryId,
5875
+ original,
5876
+ replacement,
5877
+ revertedOccurrences,
5878
+ message: `Rejected correction #${args.correctionEntryId} and restored "${replacement}" to "${original}"`
5879
+ };
5880
+ } finally {
5881
+ transcript.close();
5882
+ }
5883
+ }
4930
5884
  async function handleCorrectToEntity(args) {
4931
- const { randomUUID } = await import('node:crypto');
5885
+ const { randomUUID: randomUUID2 } = await import('node:crypto');
4932
5886
  const { slugify } = await Promise.resolve().then(() => shared);
4933
5887
  const ServerConfig = await Promise.resolve().then(() => serverConfig$1);
4934
5888
  const context = ServerConfig.getContext();
@@ -4941,7 +5895,7 @@ async function handleCorrectToEntity(args) {
4941
5895
  let finalEntityName;
4942
5896
  let isNewEntity = false;
4943
5897
  if (args.entityName) {
4944
- const id = randomUUID();
5898
+ const id = randomUUID2();
4945
5899
  const slug = slugify(args.entityName);
4946
5900
  const entityBase = {
4947
5901
  id,
@@ -6259,15 +7213,20 @@ const tools = [
6259
7213
  // Transcript Operations
6260
7214
  readTranscriptTool,
6261
7215
  listTranscriptsTool,
7216
+ identifyTasksFromTranscriptTool,
7217
+ summarizeTranscriptTool,
7218
+ deleteTranscriptSummaryTool,
6262
7219
  editTranscriptTool,
6263
7220
  changeTranscriptDateTool,
6264
7221
  combineTranscriptsTool,
6265
7222
  provideFeedbackTool,
7223
+ enhanceTranscriptTool,
6266
7224
  updateTranscriptContentTool,
6267
7225
  updateTranscriptEntityReferencesTool,
6268
7226
  createNoteTool,
6269
7227
  getEnhancementLogTool,
6270
7228
  correctToEntityTool,
7229
+ rejectCorrectionTool,
6271
7230
  // Lifecycle Status & Tasks
6272
7231
  setStatusTool,
6273
7232
  createTaskTool,
@@ -6368,6 +7327,12 @@ async function handleToolCall(name, args) {
6368
7327
  return handleReadTranscript(args);
6369
7328
  case "protokoll_list_transcripts":
6370
7329
  return handleListTranscripts(args);
7330
+ case "protokoll_identify_tasks_from_transcript":
7331
+ return handleIdentifyTasksFromTranscript(args);
7332
+ case "protokoll_summarize_transcript":
7333
+ return handleSummarizeTranscript(args);
7334
+ case "protokoll_delete_transcript_summary":
7335
+ return handleDeleteTranscriptSummary(args);
6371
7336
  case "protokoll_edit_transcript":
6372
7337
  return handleEditTranscript(args);
6373
7338
  case "protokoll_change_transcript_date":
@@ -6376,6 +7341,8 @@ async function handleToolCall(name, args) {
6376
7341
  return handleCombineTranscripts(args);
6377
7342
  case "protokoll_provide_feedback":
6378
7343
  return handleProvideFeedback(args);
7344
+ case "protokoll_enhance_transcript":
7345
+ return handleEnhanceTranscript(args);
6379
7346
  case "protokoll_update_transcript_content":
6380
7347
  return handleUpdateTranscriptContent(args);
6381
7348
  case "protokoll_update_transcript_entity_references":
@@ -6386,6 +7353,8 @@ async function handleToolCall(name, args) {
6386
7353
  return handleGetEnhancementLog(args);
6387
7354
  case "protokoll_correct_to_entity":
6388
7355
  return handleCorrectToEntity(args);
7356
+ case "protokoll_reject_correction":
7357
+ return handleRejectCorrection(args);
6389
7358
  // Lifecycle Status & Tasks
6390
7359
  case "protokoll_set_status":
6391
7360
  return handleSetStatus(args);