tuna-agent 0.1.75 → 0.1.77

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.
@@ -930,8 +930,45 @@ export class ClaudeCodeAdapter {
930
930
  }
931
931
  }
932
932
  }
933
- // Filter: skip patterns similar to ANY existing rule
934
- const newPatterns = patterns.filter(p => {
933
+ // Filter 1: Quality gate reject garbage rules
934
+ const MAX_LEARNED_RULES = 20;
935
+ const MIN_CONFIDENCE = 3;
936
+ const qualityPatterns = patterns.filter(p => {
937
+ const r = p.rule.trim();
938
+ // Confidence gate — only persist rules seen 3+ times
939
+ if (p.confidence < MIN_CONFIDENCE) {
940
+ console.log(`[Self-Improve] Skipped (low confidence ${p.confidence}): "${r.substring(0, 80)}"`);
941
+ return false;
942
+ }
943
+ // Reject generic platitudes
944
+ if (/^always (prioritize|verify|ensure|keep an eye|strive|validate|check|monitor)/i.test(r)) {
945
+ console.log(`[Self-Improve] Skipped (generic): "${r.substring(0, 80)}"`);
946
+ return false;
947
+ }
948
+ if (/^(prior to|when making decisions|before engaging|before using)/i.test(r)) {
949
+ console.log(`[Self-Improve] Skipped (generic): "${r.substring(0, 80)}"`);
950
+ return false;
951
+ }
952
+ // Reject rules that look like app descriptions (hallucination from research)
953
+ if (/\b(block all apps|learned to read|language required|learnlock|user education)\b/i.test(r)) {
954
+ console.log(`[Self-Improve] Skipped (hallucinated from app idea): "${r.substring(0, 80)}"`);
955
+ return false;
956
+ }
957
+ // Reject too short or too vague rules
958
+ if (r.length < 30) {
959
+ console.log(`[Self-Improve] Skipped (too short): "${r}"`);
960
+ return false;
961
+ }
962
+ return true;
963
+ });
964
+ // Cap total rules — if already at max, skip adding more
965
+ if (existingRules.length >= MAX_LEARNED_RULES) {
966
+ console.log(`[Self-Improve] Already at max ${MAX_LEARNED_RULES} rules — skipping`);
967
+ return;
968
+ }
969
+ const slotsAvailable = MAX_LEARNED_RULES - existingRules.length;
970
+ // Filter 2: skip patterns similar to ANY existing rule
971
+ const newPatterns = qualityPatterns.filter(p => {
935
972
  return !existingRules.some(existing => ClaudeCodeAdapter.isSimilarRule(p.rule, existing));
936
973
  });
937
974
  // Also dedup among new patterns themselves
@@ -945,8 +982,8 @@ export class ClaudeCodeAdapter {
945
982
  console.log(`[Self-Improve] ${patterns.length} patterns found but all similar to existing rules`);
946
983
  return;
947
984
  }
948
- // Cap at 3 new rules per run to avoid bloating
949
- const toAdd = dedupedPatterns.slice(0, 3);
985
+ // Cap at 3 new rules per run, and respect total max
986
+ const toAdd = dedupedPatterns.slice(0, Math.min(3, slotsAvailable));
950
987
  // Append new rules at the END of the Learned Rules section
951
988
  const rulesBlock = toAdd.map(p => `- ${p.rule} (confidence: ${p.confidence})`).join('\n');
952
989
  if (existingContent.includes(SECTION_HEADER)) {
@@ -61,3 +61,8 @@ export interface CharProfile {
61
61
  export declare function handleListCharacters(ws: AgentWebSocketClient, code: string, taskId: string): void;
62
62
  export declare function handleCreateCharacter(ws: AgentWebSocketClient, code: string, taskId: string, name: string, displayName: string, prompt: string, outputCount?: number, orientation?: string): Promise<void>;
63
63
  export declare function handleSaveCharacterSelection(ws: AgentWebSocketClient, code: string, taskId: string, characterId: string, selectedImages: string[]): void;
64
+ /**
65
+ * Sync apps from the agent's knowledge base.
66
+ * Reads knowledge items, identifies app-related documents, extracts app info.
67
+ */
68
+ export declare function handleSyncApps(ws: AgentWebSocketClient, code: string, taskId: string, apiUrl: string, agentToken: string, agentId: string): Promise<void>;
@@ -890,6 +890,120 @@ export function handleSaveCharacterSelection(ws, code, taskId, characterId, sele
890
890
  ws.sendExtensionDone(code, taskId, { error });
891
891
  }
892
892
  }
893
+ const APP_COLORS = [
894
+ '#22D3EE', '#A78BFA', '#F472B6', '#34D399', '#FBBF24',
895
+ '#FB923C', '#60A5FA', '#E879F9', '#2DD4BF', '#F87171',
896
+ ];
897
+ /**
898
+ * Sync apps from the agent's knowledge base.
899
+ * Reads knowledge items, identifies app-related documents, extracts app info.
900
+ */
901
+ export async function handleSyncApps(ws, code, taskId, apiUrl, agentToken, agentId) {
902
+ try {
903
+ console.log('[sync_apps] Starting knowledge sync...');
904
+ const knowledgeApi = async (method, apiPath) => {
905
+ const url = `${apiUrl}${apiPath}`;
906
+ const res = await fetch(url, {
907
+ method,
908
+ headers: {
909
+ 'Authorization': `Bearer ${agentToken}`,
910
+ 'Content-Type': 'application/json',
911
+ },
912
+ });
913
+ const json = await res.json();
914
+ if (!res.ok || (json.code && json.code >= 400)) {
915
+ throw new Error(json.message || `API error: ${res.status}`);
916
+ }
917
+ return json.data;
918
+ };
919
+ // 1. List all root-level knowledge items
920
+ const rootData = await knowledgeApi('GET', `/agent-knowledge?agent_id=${agentId}`);
921
+ const items = rootData.items || [];
922
+ // 2. Find app-related documents
923
+ const apps = [];
924
+ let colorIdx = 0;
925
+ for (const item of items) {
926
+ const nameLower = item.name.toLowerCase();
927
+ const descLower = (item.description || '').toLowerCase();
928
+ const isApp = nameLower.includes('app') ||
929
+ descLower.includes('app') ||
930
+ descLower.includes('application') ||
931
+ descLower.includes('mobile app') ||
932
+ descLower.includes('product');
933
+ if (!isApp)
934
+ continue;
935
+ if (item.kind === 'folder') {
936
+ const folderData = await knowledgeApi('GET', `/agent-knowledge?agent_id=${agentId}&parent_id=${item._id}`);
937
+ const children = folderData.items || [];
938
+ const features = [];
939
+ let appDesc = item.description || '';
940
+ for (const child of children) {
941
+ if (child.kind === 'document') {
942
+ const doc = await knowledgeApi('GET', `/agent-knowledge/${child._id}`);
943
+ features.push(..._extractFeatures(doc.content || ''));
944
+ if (!appDesc && doc.description)
945
+ appDesc = doc.description;
946
+ }
947
+ }
948
+ apps.push({
949
+ id: item._id,
950
+ name: item.name,
951
+ description: appDesc,
952
+ features: [...new Set(features)].slice(0, 20),
953
+ color: APP_COLORS[colorIdx++ % APP_COLORS.length],
954
+ });
955
+ }
956
+ else if (item.kind === 'document') {
957
+ const doc = await knowledgeApi('GET', `/agent-knowledge/${item._id}`);
958
+ apps.push({
959
+ id: item._id,
960
+ name: item.name,
961
+ description: item.description || doc.description || '',
962
+ features: [...new Set(_extractFeatures(doc.content || ''))].slice(0, 20),
963
+ color: APP_COLORS[colorIdx++ % APP_COLORS.length],
964
+ });
965
+ }
966
+ }
967
+ console.log(`[sync_apps] Found ${apps.length} app(s)`);
968
+ ws.sendExtensionEvent(code, {
969
+ type: 'sync_apps_result',
970
+ apps,
971
+ });
972
+ ws.sendExtensionDone(code, taskId, { ok: true, count: apps.length });
973
+ }
974
+ catch (err) {
975
+ const error = err instanceof Error ? err.message : String(err);
976
+ console.error('[sync_apps] Error:', error);
977
+ ws.sendExtensionEvent(code, {
978
+ type: 'sync_apps_result',
979
+ apps: null,
980
+ error,
981
+ });
982
+ ws.sendExtensionDone(code, taskId, { error });
983
+ }
984
+ }
985
+ /** Extract feature names from markdown bullet lists or headings. */
986
+ function _extractFeatures(content) {
987
+ const features = [];
988
+ const bulletRegex = /^[\s]*[-*]\s+\*{0,2}([^*\n]+)\*{0,2}/gm;
989
+ let match;
990
+ while ((match = bulletRegex.exec(content)) !== null) {
991
+ const feat = match[1].trim().replace(/[:–—].+$/, '').trim();
992
+ if (feat.length > 2 && feat.length < 60) {
993
+ features.push(feat);
994
+ }
995
+ }
996
+ if (features.length === 0) {
997
+ const headingRegex = /^#{2,4}\s+(.+)$/gm;
998
+ while ((match = headingRegex.exec(content)) !== null) {
999
+ const feat = match[1].trim();
1000
+ if (feat.length > 2 && feat.length < 60) {
1001
+ features.push(feat);
1002
+ }
1003
+ }
1004
+ }
1005
+ return features;
1006
+ }
893
1007
  function _sleep(ms) {
894
1008
  return new Promise(resolve => setTimeout(resolve, ms));
895
1009
  }
@@ -9,7 +9,7 @@ import { loadPMState, savePMState, clearPMState } from './pm-state.js';
9
9
  import { chatWithPM } from '../pm/planner.js';
10
10
  import { executePlanAndReport, simplifyMarkdown, waitForInput } from '../utils/execution-helpers.js';
11
11
  import { runClaude } from '../utils/claude-cli.js';
12
- import { handleGetHistory, handleRetryVideo, handleGenerateIdeas, handleGenerateScript, handleGenerateScene, handleGenerateScenes, handleRenderVideo, handleListCharacters, handleCreateCharacter, handleSaveCharacterSelection, } from './extension-handlers.js';
12
+ import { handleGetHistory, handleRetryVideo, handleGenerateIdeas, handleGenerateScript, handleGenerateScene, handleGenerateScenes, handleRenderVideo, handleListCharacters, handleCreateCharacter, handleSaveCharacterSelection, handleSyncApps, } from './extension-handlers.js';
13
13
  import { downloadAttachments, cleanupAttachments } from '../utils/image-download.js';
14
14
  import { scanSkills } from '../utils/skill-scanner.js';
15
15
  import { setupMcpConfig } from '../mcp/setup.js';
@@ -565,6 +565,12 @@ ${skillContent.slice(0, 15000)}`;
565
565
  handleSaveCharacterSelection(ws, extCode, extTaskId, msg.characterId, msg.selectedImages || []);
566
566
  break;
567
567
  }
568
+ if (extTask === 'sync_apps') {
569
+ (async () => {
570
+ await handleSyncApps(ws, extCode, extTaskId, config.apiUrl, config.agentToken, config.agentId);
571
+ })();
572
+ break;
573
+ }
568
574
  // ── Claude-powered tasks (ideas, script, etc.) ────────────────────────
569
575
  (async () => {
570
576
  try {
@@ -5,11 +5,6 @@ import type { AgentConfig } from '../types/index.js';
5
5
  * Returns 0 if MEM0_HTTP_BASE is not set or request fails.
6
6
  */
7
7
  export declare function fetchMem0Count(agentName: string): Promise<number>;
8
- /**
9
- * Add memory via OpenMemory HTTP API (POST /api/v1/memories/).
10
- * Uses MEM0_HTTP_BASE if available, falls back to SSH+mem0-add.
11
- * OpenMemory API stores in both SQLite (metadata) + Qdrant (vectors) — correct path.
12
- */
13
8
  export declare function callMem0AddMemory(text: string, agentName: string): Promise<void>;
14
9
  /**
15
10
  * Search Mem0 for relevant memories before task execution.
package/dist/mcp/setup.js CHANGED
@@ -65,9 +65,46 @@ export async function fetchMem0Count(agentName) {
65
65
  * Uses MEM0_HTTP_BASE if available, falls back to SSH+mem0-add.
66
66
  * OpenMemory API stores in both SQLite (metadata) + Qdrant (vectors) — correct path.
67
67
  */
68
+ /**
69
+ * Validate memory text before storing. Returns null if valid, or rejection reason.
70
+ */
71
+ function validateMemory(text) {
72
+ const t = text.trim();
73
+ // Too short — likely a fragment like "Upvoted 10 posts"
74
+ if (t.length < 60)
75
+ return `too short (${t.length} chars, min 60)`;
76
+ // Generic platitudes that waste memory
77
+ const genericPatterns = [
78
+ /^always (prioritize|verify|ensure|keep an eye|strive|validate|check)/i,
79
+ /^prior to (any|engaging|making|using)/i,
80
+ /^when making decisions/i,
81
+ /^(believe|believes) reddit is/i,
82
+ /^follow (accounts|verified)/i,
83
+ /^before (engaging|using|running)/i,
84
+ ];
85
+ if (genericPatterns.some(p => p.test(t)))
86
+ return `generic platitude`;
87
+ // Individual action fragments
88
+ const fragmentPatterns = [
89
+ /^(followed|upvoted|liked|joined|saved|has \d+|observed \d+)\b/i,
90
+ /^top product hunt product:/i,
91
+ /^plan(s)? to /i,
92
+ /^\w+ promoted to watching/i,
93
+ /^\w+ is top watching/i,
94
+ ];
95
+ if (fragmentPatterns.some(p => p.test(t)))
96
+ return `action fragment`;
97
+ return null;
98
+ }
68
99
  export async function callMem0AddMemory(text, agentName) {
69
100
  if (!MEM0_SSH_HOST && !MEM0_HTTP_BASE)
70
101
  throw new Error('Mem0 not configured');
102
+ // Validate memory quality before storing
103
+ const rejection = validateMemory(text);
104
+ if (rejection) {
105
+ console.log(`[Mem0 Filter] Rejected memory (${rejection}): "${text.substring(0, 80)}..."`);
106
+ return;
107
+ }
71
108
  const safeAgentName = agentName.toLowerCase().replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '') || 'agent';
72
109
  // Try HTTP API first (OpenMemory) — stores in SQLite+Qdrant, shows up in counts
73
110
  // Falls through to SSH+curl if HTTP is not reachable (e.g. port not exposed externally)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tuna-agent",
3
- "version": "0.1.75",
3
+ "version": "0.1.77",
4
4
  "description": "Tuna Agent - Run AI coding tasks on your machine",
5
5
  "bin": {
6
6
  "tuna-agent": "dist/cli/index.js"