@redaksjon/protokoll 0.2.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -18,7 +18,7 @@ Protokoll solves this.
18
18
  Protokoll is an intelligent audio transcription system that uses advanced reasoning models to create highly accurate, context-enhanced transcripts. Unlike basic transcription tools, Protokoll:
19
19
 
20
20
  - **Learns Your World**: Maintains a knowledge base of people, projects, and organizations you mention. When Whisper mishears someone, Protokoll recognizes and corrects it using phonetic variants and context awareness
21
- - **Routes Intelligently**: Multi-signal classification sends notes to the right destination—work notes stay in your work folder, client calls go to client projects, personal thoughts go to personal notes
21
+ - **Routes Intelligently**: Multi-signal classification sends notes to the right destination—work notes stay in your work folder, client calls go to client projects, personal thoughts go to personal notes. Tags are automatically deduplicated when multiple classification signals identify the same topic
22
22
  - **Preserves Everything**: This is NOT a summarizer. Protokoll preserves the full content of what you said while cleaning up filler words, false starts, and obvious transcription errors
23
23
  - **Improves Over Time**: The more you use it, the smarter it gets. Build context incrementally and watch transcription quality improve session after session
24
24
  - **Zero Configuration Start**: Works out of the box with sensible defaults. No API wrestling, no complex setup—just transcribe
@@ -583,6 +583,10 @@ protokoll term add --term "Kubernetes" --domain "devops" \
583
583
  --topics "containers,orchestration,cloud-native" \
584
584
  --projects "infrastructure"
585
585
 
586
+ # Edit existing entities (incremental changes)
587
+ protokoll project edit <id> --add-topic <topic> --add-phrase <phrase>
588
+ protokoll term edit <id> --add-sound <variant> --add-project <id>
589
+
586
590
  # Update existing entity with new content (regenerates metadata)
587
591
  protokoll project update redaksjon https://github.com/user/redaksjon/README.md
588
592
  protokoll term update kubernetes https://kubernetes.io/docs/concepts/overview/
@@ -730,6 +734,35 @@ Override per-command with `--smart` or `--no-smart` flags.
730
734
  - OpenAI API key set in environment (`OPENAI_API_KEY`)
731
735
  - Network access for URL fetching and API calls
732
736
 
737
+ ### Editing Project Classification
738
+
739
+ Modify how projects are matched to transcripts:
740
+
741
+ ```bash
742
+ # Add trigger phrases (high-confidence matching)
743
+ protokoll project edit work \
744
+ --add-phrase "work meeting" \
745
+ --add-phrase "office discussion"
746
+
747
+ # Add topics (theme-based matching)
748
+ protokoll project edit work \
749
+ --add-topic standup \
750
+ --add-topic sprint
751
+
752
+ # Associate people (routes when mentioned)
753
+ protokoll project edit client-alpha \
754
+ --add-person priya-sharma \
755
+ --add-person john-smith
756
+
757
+ # Associate companies
758
+ protokoll project edit client-work \
759
+ --add-company acme-corp
760
+ ```
761
+
762
+ Use `protokoll project show <id>` to see all classification fields.
763
+
764
+ **See:** [Context Commands Guide](./guide/context-commands.md#understanding-classification) for detailed explanation of each field.
765
+
733
766
  ### Example: Adding a Project
734
767
 
735
768
  The interactive prompt guides you through each field with explanations:
@@ -1229,6 +1262,29 @@ projects: # Associated project IDs where this term is relevant
1229
1262
  - myapp
1230
1263
  ```
1231
1264
 
1265
+ ### Project Relationships (Optional)
1266
+
1267
+ If you have a parent project with subprojects, you can model the hierarchy. Protokoll will automatically suggest relationships when you create projects:
1268
+
1269
+ ```bash
1270
+ # When creating a project interactively, Protokoll suggests:
1271
+ protokoll project add --name "Kronologi"
1272
+
1273
+ # [Suggested parent project: Redaksjon]
1274
+ # Reason: topic "redaksjon-subproject" indicates subproject
1275
+ # Confidence: high
1276
+ # Set "Redaksjon" as parent? (Y/n): y
1277
+ ```
1278
+
1279
+ Or edit relationships manually:
1280
+
1281
+ ```bash
1282
+ protokoll project edit kronologi --parent redaksjon --add-sibling protokoll
1283
+ ```
1284
+
1285
+ This helps routing and disambiguation. Most users won't need this.
1286
+
1287
+
1232
1288
  ## Routing System
1233
1289
 
1234
1290
  ### Multi-Signal Classification
package/dist/main.js CHANGED
@@ -418,6 +418,156 @@ async function main() {
418
418
  }
419
419
  }
420
420
 
421
+ const suggestRelationships = (context, options) => {
422
+ const allProjects = context.getAllProjects();
423
+ const allTerms = context.getAllTerms();
424
+ const suggestions = {};
425
+ const parentCandidates = findParentCandidates(allProjects, options);
426
+ if (parentCandidates.length > 0) {
427
+ suggestions.parent = parentCandidates[0];
428
+ }
429
+ const siblingCandidates = findSiblingCandidates(allProjects, options, suggestions.parent?.id);
430
+ if (siblingCandidates.length > 0) {
431
+ suggestions.siblings = siblingCandidates.slice(0, 5);
432
+ }
433
+ const termCandidates = findRelatedTerms(allTerms, options);
434
+ if (termCandidates.length > 0) {
435
+ suggestions.relatedTerms = termCandidates.slice(0, 5);
436
+ }
437
+ return suggestions;
438
+ };
439
+ function findParentCandidates(projects, options) {
440
+ const candidates = [];
441
+ for (const project of projects) {
442
+ if (project.id === options.projectId) continue;
443
+ const reasons = [];
444
+ let score = 0;
445
+ const projectNameLower = options.projectName.toLowerCase();
446
+ const candidateNameLower = project.name.toLowerCase();
447
+ if (projectNameLower.includes(candidateNameLower)) {
448
+ score += 50;
449
+ reasons.push(`name contains "${project.name}"`);
450
+ }
451
+ if (options.topics) {
452
+ const subprojectTopic = options.topics.find(
453
+ (t) => t.toLowerCase().includes(`${project.id}-subproject`) || t.toLowerCase().includes(`${candidateNameLower}-subproject`)
454
+ );
455
+ if (subprojectTopic) {
456
+ score += 100;
457
+ reasons.push(`topic "${subprojectTopic}" indicates subproject`);
458
+ }
459
+ const projectTopics = project.classification?.topics || [];
460
+ const sharedTopics = options.topics.filter(
461
+ (t) => projectTopics.some((pt) => pt.toLowerCase() === t.toLowerCase())
462
+ );
463
+ if (sharedTopics.length > 0) {
464
+ score += sharedTopics.length * 10;
465
+ reasons.push(`${sharedTopics.length} shared topics`);
466
+ }
467
+ }
468
+ if (options.destination && project.routing?.destination) {
469
+ if (options.destination.startsWith(project.routing.destination + "/")) {
470
+ score += 75;
471
+ reasons.push("destination is subdirectory");
472
+ }
473
+ }
474
+ if (options.description && options.description.toLowerCase().includes(candidateNameLower)) {
475
+ score += 25;
476
+ reasons.push("mentioned in description");
477
+ }
478
+ if (score > 0) {
479
+ candidates.push({ project, score, reasons });
480
+ }
481
+ }
482
+ const sorted = candidates.sort((a, b) => b.score - a.score);
483
+ return sorted.map((c) => ({
484
+ id: c.project.id,
485
+ name: c.project.name,
486
+ reason: c.reasons.join(", "),
487
+ confidence: c.score >= 75 ? "high" : c.score >= 40 ? "medium" : "low"
488
+ }));
489
+ }
490
+ function findSiblingCandidates(projects, options, suggestedParentId) {
491
+ const candidates = [];
492
+ for (const project of projects) {
493
+ if (project.id === options.projectId) continue;
494
+ const reasons = [];
495
+ let score = 0;
496
+ if (suggestedParentId && project.relationships?.parent === suggestedParentId) {
497
+ score += 100;
498
+ reasons.push(`shares parent "${suggestedParentId}"`);
499
+ }
500
+ if (options.topics) {
501
+ const projectTopics = project.classification?.topics || [];
502
+ const sharedTopics = options.topics.filter(
503
+ (t) => projectTopics.some((pt) => pt.toLowerCase() === t.toLowerCase())
504
+ );
505
+ if (sharedTopics.length >= 2) {
506
+ score += sharedTopics.length * 15;
507
+ reasons.push(`${sharedTopics.length} shared topics`);
508
+ }
509
+ }
510
+ if (options.destination && project.routing?.destination) {
511
+ const newDir = options.destination.split("/").slice(0, -1).join("/");
512
+ const projDir = project.routing.destination.split("/").slice(0, -1).join("/");
513
+ if (newDir === projDir) {
514
+ score += 30;
515
+ reasons.push("same destination directory");
516
+ }
517
+ }
518
+ if (score > 0) {
519
+ candidates.push({ project, score, reasons });
520
+ }
521
+ }
522
+ const sorted = candidates.sort((a, b) => b.score - a.score);
523
+ return sorted.map((c) => ({
524
+ id: c.project.id,
525
+ name: c.project.name,
526
+ reason: c.reasons.join(", ")
527
+ }));
528
+ }
529
+ function findRelatedTerms(terms, options) {
530
+ const candidates = [];
531
+ for (const term of terms) {
532
+ const reasons = [];
533
+ let score = 0;
534
+ const projectNameLower = options.projectName.toLowerCase();
535
+ const termNameLower = term.name.toLowerCase();
536
+ if (projectNameLower.includes(termNameLower)) {
537
+ score += 50;
538
+ reasons.push("appears in project name");
539
+ }
540
+ if (options.description && options.description.toLowerCase().includes(termNameLower)) {
541
+ score += 40;
542
+ reasons.push("mentioned in description");
543
+ }
544
+ if (options.topics && term.topics) {
545
+ const sharedTopics = options.topics.filter(
546
+ (t) => term.topics.some((tt) => tt.toLowerCase() === t.toLowerCase())
547
+ );
548
+ if (sharedTopics.length > 0) {
549
+ score += sharedTopics.length * 20;
550
+ reasons.push(`${sharedTopics.length} shared topics`);
551
+ }
552
+ }
553
+ if (options.topics) {
554
+ if (options.topics.some((t) => t.toLowerCase() === termNameLower)) {
555
+ score += 30;
556
+ reasons.push("matches project topic");
557
+ }
558
+ }
559
+ if (score > 0) {
560
+ candidates.push({ term, score, reasons });
561
+ }
562
+ }
563
+ const sorted = candidates.sort((a, b) => b.score - a.score);
564
+ return sorted.map((c) => ({
565
+ id: c.term.id,
566
+ name: c.term.name,
567
+ reason: c.reasons.join(", ")
568
+ }));
569
+ }
570
+
421
571
  const print$2 = (text) => process.stdout.write(text + "\n");
422
572
  const calculateId = (name) => {
423
573
  return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -628,6 +778,12 @@ const displayEntityDetails = (entity, filePath) => {
628
778
  if (project.classification.topics && project.classification.topics.length > 0) {
629
779
  rows.push(["Topics", formatValue$1(project.classification.topics)]);
630
780
  }
781
+ if (project.classification.associated_people && project.classification.associated_people.length > 0) {
782
+ rows.push(["Associated People", formatValue$1(project.classification.associated_people)]);
783
+ }
784
+ if (project.classification.associated_companies && project.classification.associated_companies.length > 0) {
785
+ rows.push(["Associated Companies", formatValue$1(project.classification.associated_companies)]);
786
+ }
631
787
  }
632
788
  if (project.routing) {
633
789
  if (project.routing.destination) {
@@ -641,15 +797,38 @@ const displayEntityDetails = (entity, filePath) => {
641
797
  if (project.sounds_like && project.sounds_like.length > 0) {
642
798
  rows.push(["Sounds Like", formatValue$1(project.sounds_like)]);
643
799
  }
800
+ if (project.relationships) {
801
+ const rel = project.relationships;
802
+ const relParts = [];
803
+ if (rel.parent) {
804
+ relParts.push(`Parent: ${rel.parent}`);
805
+ }
806
+ if (rel.children && rel.children.length > 0) {
807
+ relParts.push(`Children: ${rel.children.join(", ")}`);
808
+ }
809
+ if (rel.siblings && rel.siblings.length > 0) {
810
+ relParts.push(`Siblings: ${rel.siblings.join(", ")}`);
811
+ }
812
+ if (rel.relatedTerms && rel.relatedTerms.length > 0) {
813
+ relParts.push(`Related Terms: ${rel.relatedTerms.join(", ")}`);
814
+ }
815
+ if (relParts.length > 0) {
816
+ rows.push(["Relationships", relParts.join("\n ")]);
817
+ }
818
+ }
644
819
  rows.push(["Active", project.active !== false ? "true" : "false"]);
645
820
  if (project.notes) rows.push(["Notes", project.notes]);
646
821
  } else if (entity.type === "term") {
647
822
  const term = entity;
648
823
  if (term.expansion) rows.push(["Expansion", term.expansion]);
649
824
  if (term.domain) rows.push(["Domain", term.domain]);
825
+ if (term.description) rows.push(["Description", term.description]);
650
826
  if (term.sounds_like && term.sounds_like.length > 0) {
651
827
  rows.push(["Sounds Like", formatValue$1(term.sounds_like)]);
652
828
  }
829
+ if (term.topics && term.topics.length > 0) {
830
+ rows.push(["Topics", formatValue$1(term.topics)]);
831
+ }
653
832
  if (term.projects && term.projects.length > 0) {
654
833
  rows.push(["Projects", formatValue$1(term.projects)]);
655
834
  }
@@ -984,6 +1163,60 @@ Description (Enter for suggested, or edit):
984
1163
  }
985
1164
  }
986
1165
  }
1166
+ let relationships;
1167
+ if (!options.yes && (topics.length > 0 || description)) {
1168
+ const relationshipSuggestions = suggestRelationships(context, {
1169
+ projectName: name,
1170
+ projectId: id,
1171
+ topics,
1172
+ destination,
1173
+ description
1174
+ });
1175
+ if (relationshipSuggestions.parent && relationshipSuggestions.parent.confidence !== "low") {
1176
+ print$2(`
1177
+ [Suggested parent project: ${relationshipSuggestions.parent.name}]`);
1178
+ print$2(` Reason: ${relationshipSuggestions.parent.reason}`);
1179
+ print$2(` Confidence: ${relationshipSuggestions.parent.confidence}`);
1180
+ const parentAnswer = await askQuestion$1(rl, `Set "${relationshipSuggestions.parent.name}" as parent? (Y/n): `);
1181
+ if (parentAnswer.toLowerCase() !== "n" && parentAnswer.toLowerCase() !== "no") {
1182
+ relationships = relationships || {};
1183
+ relationships.parent = relationshipSuggestions.parent.id;
1184
+ print$2(` ✓ Parent set to "${relationshipSuggestions.parent.name}"`);
1185
+ }
1186
+ }
1187
+ if (relationshipSuggestions.siblings && relationshipSuggestions.siblings.length > 0) {
1188
+ print$2(`
1189
+ [Suggested sibling projects:]`);
1190
+ relationshipSuggestions.siblings.forEach((s, i) => {
1191
+ print$2(` ${i + 1}. ${s.name} (${s.reason})`);
1192
+ });
1193
+ const siblingsAnswer = await askQuestion$1(rl, "Add siblings? (Enter numbers comma-separated, or Enter to skip): ");
1194
+ if (siblingsAnswer.trim()) {
1195
+ const indices = siblingsAnswer.split(",").map((n) => parseInt(n.trim()) - 1).filter((i) => i >= 0 && i < relationshipSuggestions.siblings.length);
1196
+ if (indices.length > 0) {
1197
+ relationships = relationships || {};
1198
+ relationships.siblings = indices.map((i) => relationshipSuggestions.siblings[i].id);
1199
+ print$2(` ✓ Added ${indices.length} siblings`);
1200
+ }
1201
+ }
1202
+ }
1203
+ if (relationshipSuggestions.relatedTerms && relationshipSuggestions.relatedTerms.length > 0) {
1204
+ print$2(`
1205
+ [Suggested related terms:]`);
1206
+ relationshipSuggestions.relatedTerms.forEach((t, i) => {
1207
+ print$2(` ${i + 1}. ${t.name} (${t.reason})`);
1208
+ });
1209
+ const termsAnswer = await askQuestion$1(rl, "Add related terms? (Enter numbers comma-separated, or Enter to skip): ");
1210
+ if (termsAnswer.trim()) {
1211
+ const indices = termsAnswer.split(",").map((n) => parseInt(n.trim()) - 1).filter((i) => i >= 0 && i < relationshipSuggestions.relatedTerms.length);
1212
+ if (indices.length > 0) {
1213
+ relationships = relationships || {};
1214
+ relationships.relatedTerms = indices.map((i) => relationshipSuggestions.relatedTerms[i].id);
1215
+ print$2(` ✓ Added ${indices.length} related terms`);
1216
+ }
1217
+ }
1218
+ }
1219
+ }
987
1220
  const project = {
988
1221
  id,
989
1222
  name,
@@ -1000,6 +1233,7 @@ Description (Enter for suggested, or edit):
1000
1233
  },
1001
1234
  ...soundsLike.length && { sounds_like: soundsLike },
1002
1235
  ...description && { description },
1236
+ ...relationships && { relationships },
1003
1237
  active: true
1004
1238
  };
1005
1239
  await context.saveEntity(project);
@@ -1063,6 +1297,117 @@ Term "${name}" saved successfully.`);
1063
1297
  rl.close();
1064
1298
  }
1065
1299
  };
1300
+ const editProject = async (context, id, options) => {
1301
+ const project = context.getProject(id);
1302
+ if (!project) {
1303
+ print$2(`Error: Project "${id}" not found`);
1304
+ process.exit(1);
1305
+ }
1306
+ const mergeArray = (existing, add, remove) => {
1307
+ let result = existing ? [...existing] : [];
1308
+ if (add && add.length > 0) {
1309
+ result = [...result, ...add.filter((v) => !result.includes(v))];
1310
+ }
1311
+ if (remove && remove.length > 0) {
1312
+ result = result.filter((v) => !remove.includes(v));
1313
+ }
1314
+ return result.length > 0 ? result : void 0;
1315
+ };
1316
+ const changes = [];
1317
+ const updated = { ...project };
1318
+ if (options.name) {
1319
+ updated.name = options.name;
1320
+ changes.push(`name: "${options.name}"`);
1321
+ }
1322
+ if (options.description) {
1323
+ updated.description = options.description;
1324
+ changes.push(`description updated`);
1325
+ }
1326
+ if (options.active) {
1327
+ updated.active = options.active === "true";
1328
+ changes.push(`active: ${options.active}`);
1329
+ }
1330
+ const classification = { ...project.classification };
1331
+ if (options.contextType) {
1332
+ classification.context_type = options.contextType;
1333
+ changes.push(`context_type: ${options.contextType}`);
1334
+ }
1335
+ const updatedTopics = mergeArray(classification.topics, options.addTopic, options.removeTopic);
1336
+ if (updatedTopics !== void 0 && updatedTopics !== classification.topics) {
1337
+ classification.topics = updatedTopics;
1338
+ if (options.addTopic?.length) changes.push(`added ${options.addTopic.length} topics`);
1339
+ if (options.removeTopic?.length) changes.push(`removed ${options.removeTopic.length} topics`);
1340
+ }
1341
+ const updatedPhrases = mergeArray(classification.explicit_phrases, options.addPhrase, options.removePhrase);
1342
+ if (updatedPhrases !== void 0 && updatedPhrases !== classification.explicit_phrases) {
1343
+ classification.explicit_phrases = updatedPhrases;
1344
+ if (options.addPhrase?.length) changes.push(`added ${options.addPhrase.length} trigger phrases`);
1345
+ if (options.removePhrase?.length) changes.push(`removed ${options.removePhrase.length} trigger phrases`);
1346
+ }
1347
+ const updatedPeople = mergeArray(classification.associated_people, options.addPerson, options.removePerson);
1348
+ if (updatedPeople !== void 0 && updatedPeople !== classification.associated_people) {
1349
+ classification.associated_people = updatedPeople;
1350
+ if (options.addPerson?.length) changes.push(`added ${options.addPerson.length} associated people`);
1351
+ if (options.removePerson?.length) changes.push(`removed ${options.removePerson.length} associated people`);
1352
+ }
1353
+ const updatedCompanies = mergeArray(classification.associated_companies, options.addCompany, options.removeCompany);
1354
+ if (updatedCompanies !== void 0 && updatedCompanies !== classification.associated_companies) {
1355
+ classification.associated_companies = updatedCompanies;
1356
+ if (options.addCompany?.length) changes.push(`added ${options.addCompany.length} associated companies`);
1357
+ if (options.removeCompany?.length) changes.push(`removed ${options.removeCompany.length} associated companies`);
1358
+ }
1359
+ updated.classification = classification;
1360
+ const routing = { ...project.routing };
1361
+ if (options.destination) {
1362
+ routing.destination = options.destination;
1363
+ changes.push(`destination: ${options.destination}`);
1364
+ }
1365
+ if (options.structure) {
1366
+ routing.structure = options.structure;
1367
+ changes.push(`structure: ${options.structure}`);
1368
+ }
1369
+ updated.routing = routing;
1370
+ if (options.parent || options.addChild || options.removeChild || options.addSibling || options.removeSibling || options.addTerm || options.removeTerm) {
1371
+ const existingRel = project.relationships || {};
1372
+ const relationships = { ...existingRel };
1373
+ if (options.parent) {
1374
+ relationships.parent = options.parent;
1375
+ changes.push(`parent: ${options.parent}`);
1376
+ }
1377
+ const updatedChildren = mergeArray(existingRel.children, options.addChild, options.removeChild);
1378
+ if (updatedChildren) {
1379
+ relationships.children = updatedChildren;
1380
+ if (options.addChild?.length) changes.push(`added ${options.addChild.length} children`);
1381
+ if (options.removeChild?.length) changes.push(`removed ${options.removeChild.length} children`);
1382
+ }
1383
+ const updatedSiblings = mergeArray(existingRel.siblings, options.addSibling, options.removeSibling);
1384
+ if (updatedSiblings) {
1385
+ relationships.siblings = updatedSiblings;
1386
+ if (options.addSibling?.length) changes.push(`added ${options.addSibling.length} siblings`);
1387
+ if (options.removeSibling?.length) changes.push(`removed ${options.removeSibling.length} siblings`);
1388
+ }
1389
+ const updatedRelatedTerms = mergeArray(existingRel.relatedTerms, options.addTerm, options.removeTerm);
1390
+ if (updatedRelatedTerms) {
1391
+ relationships.relatedTerms = updatedRelatedTerms;
1392
+ if (options.addTerm?.length) changes.push(`added ${options.addTerm.length} related terms`);
1393
+ if (options.removeTerm?.length) changes.push(`removed ${options.removeTerm.length} related terms`);
1394
+ }
1395
+ updated.relationships = relationships;
1396
+ }
1397
+ updated.updatedAt = /* @__PURE__ */ new Date();
1398
+ await context.saveEntity(updated);
1399
+ if (changes.length > 0) {
1400
+ print$2(`
1401
+ ✓ Updated project "${project.name}":`);
1402
+ changes.forEach((c) => print$2(` • ${c}`));
1403
+ print$2("");
1404
+ } else {
1405
+ print$2(`
1406
+ No changes made to project "${project.name}"
1407
+ `);
1408
+ }
1409
+ displayEntityDetails(updated);
1410
+ };
1066
1411
  const updateProject = async (context, id, source, options) => {
1067
1412
  const existingProject = context.getProject(id);
1068
1413
  if (!existingProject) {
@@ -1112,6 +1457,68 @@ Updating project: ${existingProject.name} (${existingProject.id})`);
1112
1457
  print$2(`
1113
1458
  ✓ Project "${existingProject.name}" updated successfully.`);
1114
1459
  };
1460
+ const editTerm = async (context, id, options) => {
1461
+ const term = context.getTerm(id);
1462
+ if (!term) {
1463
+ print$2(`Error: Term "${id}" not found`);
1464
+ process.exit(1);
1465
+ }
1466
+ const mergeArray = (existing, add, remove) => {
1467
+ let result = existing ? [...existing] : [];
1468
+ if (add && add.length > 0) {
1469
+ result = [...result, ...add.filter((v) => !result.includes(v))];
1470
+ }
1471
+ if (remove && remove.length > 0) {
1472
+ result = result.filter((v) => !remove.includes(v));
1473
+ }
1474
+ return result.length > 0 ? result : void 0;
1475
+ };
1476
+ const changes = [];
1477
+ const updated = { ...term };
1478
+ if (options.description) {
1479
+ updated.description = options.description;
1480
+ changes.push("description updated");
1481
+ }
1482
+ if (options.domain) {
1483
+ updated.domain = options.domain;
1484
+ changes.push(`domain: ${options.domain}`);
1485
+ }
1486
+ if (options.expansion) {
1487
+ updated.expansion = options.expansion;
1488
+ changes.push(`expansion: ${options.expansion}`);
1489
+ }
1490
+ const updatedSounds = mergeArray(term.sounds_like, options.addSound, options.removeSound);
1491
+ if (updatedSounds !== term.sounds_like) {
1492
+ updated.sounds_like = updatedSounds;
1493
+ if (options.addSound?.length) changes.push(`added ${options.addSound.length} sounds_like variants`);
1494
+ if (options.removeSound?.length) changes.push(`removed ${options.removeSound.length} sounds_like variants`);
1495
+ }
1496
+ const updatedTopics = mergeArray(term.topics, options.addTopic, options.removeTopic);
1497
+ if (updatedTopics !== term.topics) {
1498
+ updated.topics = updatedTopics;
1499
+ if (options.addTopic?.length) changes.push(`added ${options.addTopic.length} topics`);
1500
+ if (options.removeTopic?.length) changes.push(`removed ${options.removeTopic.length} topics`);
1501
+ }
1502
+ const updatedProjects = mergeArray(term.projects, options.addProject, options.removeProject);
1503
+ if (updatedProjects !== term.projects) {
1504
+ updated.projects = updatedProjects;
1505
+ if (options.addProject?.length) changes.push(`added ${options.addProject.length} project associations`);
1506
+ if (options.removeProject?.length) changes.push(`removed ${options.removeProject.length} project associations`);
1507
+ }
1508
+ updated.updatedAt = /* @__PURE__ */ new Date();
1509
+ await context.saveEntity(updated);
1510
+ if (changes.length > 0) {
1511
+ print$2(`
1512
+ ✓ Updated term "${term.name}":`);
1513
+ changes.forEach((c) => print$2(` • ${c}`));
1514
+ print$2("");
1515
+ } else {
1516
+ print$2(`
1517
+ No changes made to term "${term.name}"
1518
+ `);
1519
+ }
1520
+ displayEntityDetails(updated);
1521
+ };
1115
1522
  const updateTerm = async (context, id, source, options) => {
1116
1523
  const existingTerm = context.getTerm(id);
1117
1524
  if (!existingTerm) {
@@ -1396,6 +1803,14 @@ const createProjectCommand = () => {
1396
1803
  }
1397
1804
  await addProject(context, { source, ...cmdOptions });
1398
1805
  });
1806
+ cmd.command("edit <id>").description("Edit project fields (classification, routing, relationships)").option("--name <name>", "Update project name").option("--description <text>", "Update description").option("--destination <path>", "Update routing destination").option("--structure <type>", "Update directory structure: none, year, month, day").option("--context-type <type>", "Update context type: work, personal, mixed").option("--add-topic <topic>", "Add a classification topic (repeatable)", collect, []).option("--remove-topic <topic>", "Remove a classification topic (repeatable)", collect, []).option("--add-phrase <phrase>", "Add trigger phrase (repeatable)", collect, []).option("--remove-phrase <phrase>", "Remove trigger phrase (repeatable)", collect, []).option("--add-person <id>", "Associate person ID (repeatable)", collect, []).option("--remove-person <id>", "Remove associated person (repeatable)", collect, []).option("--add-company <id>", "Associate company ID (repeatable)", collect, []).option("--remove-company <id>", "Remove associated company (repeatable)", collect, []).option("--parent <id>", "Set parent project ID").option("--add-child <id>", "Add child project (repeatable)", collect, []).option("--remove-child <id>", "Remove child project (repeatable)", collect, []).option("--add-sibling <id>", "Add sibling project (repeatable)", collect, []).option("--remove-sibling <id>", "Remove sibling project (repeatable)", collect, []).option("--add-term <id>", "Add related term (repeatable)", collect, []).option("--remove-term <id>", "Remove related term (repeatable)", collect, []).option("--active <bool>", "Set active status (true/false)").action(async (id, options) => {
1807
+ const context = await create$5();
1808
+ if (!context.hasContext()) {
1809
+ print$2("Error: No .protokoll directory found.");
1810
+ process.exit(1);
1811
+ }
1812
+ await editProject(context, id, options);
1813
+ });
1399
1814
  cmd.command("delete <id>").description("Delete a project").option("-f, --force", "Skip confirmation").action(async (id, options) => {
1400
1815
  const context = await create$5();
1401
1816
  await deleteEntity(context, "project", id, options);
@@ -1410,6 +1825,9 @@ const createProjectCommand = () => {
1410
1825
  });
1411
1826
  return cmd;
1412
1827
  };
1828
+ function collect(value, previous) {
1829
+ return previous.concat([value]);
1830
+ }
1413
1831
  const createTermCommand = () => {
1414
1832
  const cmd = new Command("term").description("Manage terms");
1415
1833
  cmd.command("list").description("List all terms").option("-v, --verbose", "Show full details").action(async (options) => {
@@ -1428,6 +1846,14 @@ const createTermCommand = () => {
1428
1846
  }
1429
1847
  await addTermEnhanced(context, { source, ...cmdOptions });
1430
1848
  });
1849
+ cmd.command("edit <id>").description("Edit term fields (sounds_like, projects, metadata)").option("--description <text>", "Update description").option("--domain <domain>", "Update domain").option("--expansion <text>", "Update expansion").option("--add-sound <variant>", "Add sounds_like variant (repeatable)", collect, []).option("--remove-sound <variant>", "Remove sounds_like variant (repeatable)", collect, []).option("--add-topic <topic>", "Add topic (repeatable)", collect, []).option("--remove-topic <topic>", "Remove topic (repeatable)", collect, []).option("--add-project <id>", "Associate with project (repeatable)", collect, []).option("--remove-project <id>", "Remove project association (repeatable)", collect, []).action(async (id, options) => {
1850
+ const context = await create$5();
1851
+ if (!context.hasContext()) {
1852
+ print$2("Error: No .protokoll directory found.");
1853
+ process.exit(1);
1854
+ }
1855
+ await editTerm(context, id, options);
1856
+ });
1431
1857
  cmd.command("delete <id>").description("Delete a term").option("-f, --force", "Skip confirmation").action(async (id, options) => {
1432
1858
  const context = await create$5();
1433
1859
  await deleteEntity(context, "term", id, options);