tuna-agent 0.1.59 → 0.1.61

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.
@@ -69,6 +69,10 @@ export declare class ClaudeCodeAdapter implements AgentAdapter {
69
69
  * Self-Improvement: detect patterns from Mem0 and update CLAUDE.md.
70
70
  * Runs every N tasks to evolve the agent's permanent knowledge.
71
71
  */
72
+ /** Extract key phrases (3+ word sequences) from a rule for similarity matching. */
73
+ private static extractKeyPhrases;
74
+ /** Check if two rules are semantically similar (>40% key phrase overlap). */
75
+ private static isSimilarRule;
72
76
  runSelfImprovement(cwd: string): Promise<void>;
73
77
  /**
74
78
  * Parse "## Learned Rules" section from CLAUDE.md and store in learnedRulesMap.
@@ -846,6 +846,24 @@ export class ClaudeCodeAdapter {
846
846
  * Self-Improvement: detect patterns from Mem0 and update CLAUDE.md.
847
847
  * Runs every N tasks to evolve the agent's permanent knowledge.
848
848
  */
849
+ /** Extract key phrases (3+ word sequences) from a rule for similarity matching. */
850
+ static extractKeyPhrases(text) {
851
+ const words = text.toLowerCase().replace(/[^a-z0-9\s]/g, '').split(/\s+/).filter(w => w.length > 2);
852
+ const phrases = [];
853
+ for (let i = 0; i <= words.length - 3; i++) {
854
+ phrases.push(words.slice(i, i + 3).join(' '));
855
+ }
856
+ return phrases;
857
+ }
858
+ /** Check if two rules are semantically similar (>40% key phrase overlap). */
859
+ static isSimilarRule(ruleA, ruleB) {
860
+ const phrasesA = ClaudeCodeAdapter.extractKeyPhrases(ruleA);
861
+ const phrasesB = new Set(ClaudeCodeAdapter.extractKeyPhrases(ruleB));
862
+ if (phrasesA.length === 0)
863
+ return false;
864
+ const overlap = phrasesA.filter(p => phrasesB.has(p)).length;
865
+ return overlap / phrasesA.length > 0.4;
866
+ }
849
867
  async runSelfImprovement(cwd) {
850
868
  if (!process.env.MEM0_SSH_HOST)
851
869
  return;
@@ -860,41 +878,76 @@ export class ClaudeCodeAdapter {
860
878
  console.log(`[Self-Improve] No patterns detected yet`);
861
879
  return;
862
880
  }
863
- // Read current CLAUDE.md to check for existing rules
881
+ // Read current CLAUDE.md and extract existing rules
864
882
  const claudeMdPath = path.join(cwd, 'CLAUDE.md');
865
883
  let existingContent = '';
866
884
  if (fs.existsSync(claudeMdPath)) {
867
885
  existingContent = fs.readFileSync(claudeMdPath, 'utf-8');
868
886
  }
869
- // Filter out patterns that are already in CLAUDE.md
887
+ // Parse existing rules from "## Learned Rules" section
888
+ const SECTION_HEADER = '## Learned Rules';
889
+ const existingRules = [];
890
+ const sectionIdx = existingContent.indexOf(SECTION_HEADER);
891
+ if (sectionIdx !== -1) {
892
+ const sectionContent = existingContent.substring(sectionIdx + SECTION_HEADER.length);
893
+ const nextSection = sectionContent.indexOf('\n## ');
894
+ const rulesText = nextSection !== -1 ? sectionContent.substring(0, nextSection) : sectionContent;
895
+ for (const line of rulesText.split('\n')) {
896
+ const trimmed = line.trim();
897
+ if (trimmed.startsWith('- ')) {
898
+ // Strip confidence suffix
899
+ const ruleText = trimmed.replace(/\s*\(confidence:\s*[\d.]+\)\s*$/, '').substring(2);
900
+ if (ruleText)
901
+ existingRules.push(ruleText);
902
+ }
903
+ }
904
+ }
905
+ // Filter: skip patterns similar to ANY existing rule
870
906
  const newPatterns = patterns.filter(p => {
871
- // Simple dedup: check if rule text (or close match) already exists
872
- const ruleNormalized = p.rule.toLowerCase().replace(/[^a-z0-9\s]/g, '');
873
- return !existingContent.toLowerCase().includes(ruleNormalized.substring(0, 50));
907
+ return !existingRules.some(existing => ClaudeCodeAdapter.isSimilarRule(p.rule, existing));
874
908
  });
875
- if (newPatterns.length === 0) {
876
- console.log(`[Self-Improve] ${patterns.length} patterns found but all already in CLAUDE.md`);
909
+ // Also dedup among new patterns themselves
910
+ const dedupedPatterns = [];
911
+ for (const p of newPatterns) {
912
+ const isDup = dedupedPatterns.some(existing => ClaudeCodeAdapter.isSimilarRule(p.rule, existing.rule));
913
+ if (!isDup)
914
+ dedupedPatterns.push(p);
915
+ }
916
+ if (dedupedPatterns.length === 0) {
917
+ console.log(`[Self-Improve] ${patterns.length} patterns found but all similar to existing rules`);
877
918
  return;
878
919
  }
879
- // Append new rules to CLAUDE.md under "## Learned Rules"
880
- const SECTION_HEADER = '## Learned Rules';
881
- const rulesBlock = newPatterns.map(p => `- ${p.rule} (confidence: ${p.confidence})`).join('\n');
920
+ // Cap at 3 new rules per run to avoid bloating
921
+ const toAdd = dedupedPatterns.slice(0, 3);
922
+ // Append new rules at the END of the Learned Rules section
923
+ const rulesBlock = toAdd.map(p => `- ${p.rule} (confidence: ${p.confidence})`).join('\n');
882
924
  if (existingContent.includes(SECTION_HEADER)) {
883
- // Append to existing section
884
- const updatedContent = existingContent.replace(SECTION_HEADER, `${SECTION_HEADER}\n${rulesBlock}`);
885
- fs.writeFileSync(claudeMdPath, updatedContent);
925
+ // Find end of Learned Rules section (next ## or end of file)
926
+ const headerIdx = existingContent.indexOf(SECTION_HEADER);
927
+ const afterHeader = existingContent.substring(headerIdx + SECTION_HEADER.length);
928
+ const nextSectionIdx = afterHeader.indexOf('\n## ');
929
+ if (nextSectionIdx !== -1) {
930
+ // Insert before next section
931
+ const insertPos = headerIdx + SECTION_HEADER.length + nextSectionIdx;
932
+ const updatedContent = existingContent.substring(0, insertPos) + '\n' + rulesBlock + existingContent.substring(insertPos);
933
+ fs.writeFileSync(claudeMdPath, updatedContent);
934
+ }
935
+ else {
936
+ // Append at end of file
937
+ const ending = existingContent.endsWith('\n') ? '' : '\n';
938
+ fs.writeFileSync(claudeMdPath, existingContent + ending + rulesBlock + '\n');
939
+ }
886
940
  }
887
941
  else {
888
- // Add new section at the end
889
942
  const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
890
943
  fs.writeFileSync(claudeMdPath, existingContent + separator + `${SECTION_HEADER}\n${rulesBlock}\n`);
891
944
  }
892
- this.metrics.patternsLearnedCount += newPatterns.length;
893
- this.metrics.rulesCount += newPatterns.length;
945
+ this.metrics.patternsLearnedCount += toAdd.length;
946
+ this.metrics.rulesCount += toAdd.length;
894
947
  this.metrics.lastPatternAt = new Date().toISOString();
895
- this.metrics.latestLearnedRule = newPatterns[newPatterns.length - 1].rule.substring(0, 500);
896
- console.log(`[Self-Improve] Added ${newPatterns.length} new rules to CLAUDE.md:`);
897
- newPatterns.forEach(p => console.log(`[Self-Improve] - ${p.rule}`));
948
+ this.metrics.latestLearnedRule = toAdd[toAdd.length - 1].rule.substring(0, 500);
949
+ console.log(`[Self-Improve] Added ${toAdd.length} new rules to CLAUDE.md:`);
950
+ toAdd.forEach(p => console.log(`[Self-Improve] - ${p.rule}`));
898
951
  // Sync learned rules to heartbeat metrics
899
952
  this.parseLearnedRules(claudeMdPath);
900
953
  }
@@ -920,7 +973,7 @@ export class ClaudeCodeAdapter {
920
973
  const nextSection = sectionContent.indexOf('\n## ');
921
974
  const rulesText = nextSection !== -1 ? sectionContent.substring(0, nextSection) : sectionContent;
922
975
  const rules = [];
923
- const ruleRegex = /^- (.+?)(?:\s*\(confidence:\s*([\d.]+)\))?$/;
976
+ const ruleRegex = /^-{1,2}\s+(.+?)(?:\s*\(confidence:\s*([\d.]+)\))?$/;
924
977
  // First pass: collect raw confidence values
925
978
  const rawEntries = [];
926
979
  for (const line of rulesText.split('\n')) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"