@mrtrinhvn/ag-kit 1.3.1 → 1.4.0

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.
@@ -0,0 +1,367 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ memory_tool.py — Ag-Kit Two-Tier Memory System (Option B)
4
+
5
+ TIER 1 (HOT) — Working Memory: Fast write/read, exact + semantic search
6
+ Save immediately: `save "content" --category decision`
7
+ Search: `search "keyword"` (uses both LIKE and Cosine Similarity if model present)
8
+ List hot nodes: `hot`
9
+
10
+ TIER 2 (COLD) — Long-term Memory: Consolidated summaries
11
+ Consolidate hot → cold: `consolidate [--category X] [--days 7]`
12
+ Search cold tier: `cold` | `search "kw" --tier cold`
13
+
14
+ EMBEDDINGS (Option B - Dual-Model Coordination):
15
+ Automatically calls local Ollama `nomic-embed-text` during save & search.
16
+ No external libraries required (pure python struct/math).
17
+ """
18
+
19
+ import argparse
20
+ import sqlite3
21
+ import json
22
+ import urllib.request
23
+ import struct
24
+ import math
25
+ from pathlib import Path
26
+ from datetime import datetime, timedelta
27
+
28
+ DEFAULT_DB = Path(__file__).parent.parent / "memory" / "graph.db"
29
+ VALID_CATEGORIES = {"user_pref", "decision", "error", "pattern", "context", "general"}
30
+
31
+
32
+ # ─── Option B: Vector Embeddings (Ollama) ─────────────────────────────────────
33
+
34
+ def get_embedding(text: str) -> bytes:
35
+ """Calls local Ollama nomic-embed-text and returns packed bytes."""
36
+ try:
37
+ req = urllib.request.Request(
38
+ "http://localhost:11434/api/embed",
39
+ json.dumps({"model": "nomic-embed-text", "input": text}).encode('utf-8')
40
+ )
41
+ with urllib.request.urlopen(req, timeout=10) as res:
42
+ data = json.loads(res.read().decode('utf-8'))
43
+ floats = data.get("embeddings", [[]])[0]
44
+ if not floats:
45
+ return None
46
+ return struct.pack(f"{len(floats)}f", *floats)
47
+ except Exception as e:
48
+ # Silently fail if Ollama is not running or model missing
49
+ return None
50
+
51
+ def cosine_similarity(v1: bytes, v2: bytes) -> float:
52
+ """Computes cosine similarity between two byte-packed float vectors."""
53
+ if not v1 or not v2: return 0.0
54
+ try:
55
+ f1 = struct.unpack(f"{len(v1)//4}f", v1)
56
+ f2 = struct.unpack(f"{len(v2)//4}f", v2)
57
+ dot = sum(a * b for a, b in zip(f1, f2))
58
+ norm1 = math.sqrt(sum(a * a for a in f1))
59
+ norm2 = math.sqrt(sum(b * b for b in f2))
60
+ if norm1 == 0 or norm2 == 0: return 0.0
61
+ return dot / (norm1 * norm2)
62
+ except Exception:
63
+ return 0.0
64
+
65
+
66
+ # ─── DB Setup ─────────────────────────────────────────────────────────────────
67
+
68
+ def get_db(db_path: Path) -> sqlite3.Connection:
69
+ db_path.parent.mkdir(parents=True, exist_ok=True)
70
+ conn = sqlite3.connect(str(db_path))
71
+ conn.row_factory = sqlite3.Row
72
+ conn.execute("PRAGMA journal_mode=WAL")
73
+ _init_schema(conn)
74
+ return conn
75
+
76
+
77
+ def _init_schema(conn: sqlite3.Connection):
78
+ conn.executescript("""
79
+ CREATE TABLE IF NOT EXISTS nodes (
80
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
81
+ content TEXT NOT NULL,
82
+ category TEXT NOT NULL DEFAULT 'general',
83
+ energy INTEGER NOT NULL DEFAULT 100,
84
+ tier TEXT NOT NULL DEFAULT 'hot',
85
+ source_ids TEXT,
86
+ embedding BLOB,
87
+ created_at TEXT NOT NULL,
88
+ updated_at TEXT NOT NULL
89
+ );
90
+ CREATE TABLE IF NOT EXISTS edges (
91
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
92
+ from_node INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
93
+ to_node INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE,
94
+ relation TEXT NOT NULL DEFAULT 'related_to',
95
+ created_at TEXT NOT NULL
96
+ );
97
+ CREATE INDEX IF NOT EXISTS idx_nodes_category ON nodes(category);
98
+ CREATE INDEX IF NOT EXISTS idx_nodes_tier ON nodes(tier);
99
+ CREATE INDEX IF NOT EXISTS idx_nodes_energy ON nodes(energy);
100
+ CREATE INDEX IF NOT EXISTS idx_edges_from ON edges(from_node);
101
+ CREATE INDEX IF NOT EXISTS idx_edges_to ON edges(to_node);
102
+ """)
103
+ # Live migration
104
+ cols = [r[1] for r in conn.execute("PRAGMA table_info(nodes)").fetchall()]
105
+ for col, typedef in [("tier", "TEXT DEFAULT 'hot'"), ("source_ids", "TEXT"), ("embedding", "BLOB")]:
106
+ if col not in cols:
107
+ conn.execute(f"ALTER TABLE nodes ADD COLUMN {col} {typedef}")
108
+ conn.commit()
109
+
110
+
111
+ # ─── HOT TIER ─────────────────────────────────────────────────────────────────
112
+
113
+ def cmd_save(args, conn):
114
+ now = datetime.now().isoformat()
115
+ category = args.category if args.category in VALID_CATEGORIES else "general"
116
+
117
+ emb = get_embedding(args.content)
118
+
119
+ cur = conn.execute(
120
+ "INSERT INTO nodes (content, category, energy, tier, embedding, created_at, updated_at) VALUES (?, ?, 100, 'hot', ?, ?, ?)",
121
+ (args.content, category, emb, now, now)
122
+ )
123
+ conn.commit()
124
+ emb_status = "✨[Vector]" if emb else "⚠️[No-Vector]"
125
+ print(f"✅ {emb_status} Saved hot node #{cur.lastrowid} [{category}]: {args.content[:80]}")
126
+
127
+
128
+ def cmd_hot(args, conn):
129
+ rows = conn.execute(
130
+ "SELECT id, category, energy, content, embedding, created_at FROM nodes WHERE tier='hot' ORDER BY energy DESC, updated_at DESC LIMIT ?",
131
+ (args.limit,)
132
+ ).fetchall()
133
+ print(f"🔥 HOT nodes ({len(rows)} shown):\n")
134
+ for r in rows:
135
+ v_mark = "✨" if r["embedding"] else " "
136
+ print(f" {v_mark}#{r['id']} [{r['category']}] ⚡{r['energy']} 📅{r['created_at'][:10]}")
137
+ print(f" {r['content'][:120]}\n")
138
+
139
+
140
+ # ─── CONSOLIDATION (Hot → Cold) ───────────────────────────────────────────────
141
+
142
+ def cmd_consolidate(args, conn):
143
+ threshold_date = (datetime.now() - timedelta(days=args.days)).isoformat()
144
+ cat_filter = f"AND category = '{args.category}'" if args.category else ""
145
+
146
+ hot_rows = conn.execute(
147
+ f"SELECT id, content, category FROM nodes WHERE tier='hot' AND updated_at < ? {cat_filter} ORDER BY category, energy DESC",
148
+ (threshold_date,)
149
+ ).fetchall()
150
+
151
+ if not hot_rows:
152
+ print(f"⚡ No hot nodes older than {args.days} days to consolidate.")
153
+ return
154
+
155
+ from collections import defaultdict
156
+ groups = defaultdict(list)
157
+ for r in hot_rows:
158
+ groups[r["category"]].append(r)
159
+
160
+ consolidated_count = 0
161
+ now = datetime.now().isoformat()
162
+
163
+ for category, nodes in groups.items():
164
+ if len(nodes) < 2 and not args.force:
165
+ continue
166
+
167
+ node_ids = [r["id"] for r in nodes]
168
+ merged = "\n".join(f"• {r['content'][:200]}" for r in nodes[:20])
169
+ summary = f"[Consolidated {len(nodes)} nodes — {category}]\n{merged}"
170
+
171
+ emb = get_embedding(summary)
172
+
173
+ cur = conn.execute(
174
+ "INSERT INTO nodes (content, category, energy, tier, source_ids, embedding, created_at, updated_at) VALUES (?, ?, 80, 'cold', ?, ?, ?, ?)",
175
+ (summary, category, json.dumps(node_ids), emb, now, now)
176
+ )
177
+ cold_id = cur.lastrowid
178
+
179
+ for nid in node_ids:
180
+ conn.execute(
181
+ "INSERT INTO edges (from_node, to_node, relation, created_at) VALUES (?, ?, 'consolidated_into', ?)",
182
+ (nid, cold_id, now)
183
+ )
184
+ conn.execute("UPDATE nodes SET energy = MAX(0, energy - 40) WHERE id = ?", (nid,))
185
+
186
+ consolidated_count += 1
187
+ print(f"❄️ [COLD] '{category}': merged {len(nodes)} hot nodes → #{cold_id}")
188
+
189
+ conn.commit()
190
+ print(f"\n✅ Consolidation complete. Run `gc` to cleanup dead hot nodes.")
191
+
192
+
193
+ # ─── COLD TIER ────────────────────────────────────────────────────────────────
194
+
195
+ def cmd_cold(args, conn):
196
+ rows = conn.execute(
197
+ "SELECT id, category, energy, content, source_ids, updated_at FROM nodes WHERE tier='cold' ORDER BY energy DESC, updated_at DESC LIMIT ?",
198
+ (args.limit,)
199
+ ).fetchall()
200
+ print(f"❄️ COLD nodes ({len(rows)} shown):\n")
201
+ for r in rows:
202
+ sources = json.loads(r["source_ids"] or "[]")
203
+ print(f" #{r['id']} [{r['category']}] ⚡{r['energy']} (from {len(sources)} hot nodes)")
204
+ print(f" {r['content'][:200]}\n")
205
+
206
+
207
+ # ─── GRAPH (Edges) ────────────────────────────────────────────────────────────
208
+
209
+ def cmd_link(args, conn):
210
+ now = datetime.now().isoformat()
211
+ a = conn.execute("SELECT id, content FROM nodes WHERE id = ?", (args.from_id,)).fetchone()
212
+ b = conn.execute("SELECT id, content FROM nodes WHERE id = ?", (args.to_id,)).fetchone()
213
+ if not a or not b:
214
+ print(f"❌ Node #{args.from_id} or #{args.to_id} not found.")
215
+ return
216
+ conn.execute(
217
+ "INSERT INTO edges (from_node, to_node, relation, created_at) VALUES (?, ?, ?, ?)",
218
+ (args.from_id, args.to_id, args.relation, now)
219
+ )
220
+ conn.commit()
221
+ print(f"🔗 Linked: #{args.from_id} --[{args.relation}]--> #{args.to_id}")
222
+
223
+
224
+ def cmd_graph(args, conn):
225
+ node = conn.execute("SELECT * FROM nodes WHERE id = ?", (args.node_id,)).fetchone()
226
+ if not node:
227
+ print(f"❌ Node #{args.node_id} not found.")
228
+ return
229
+ print(f"🕸️ Graph around Node #{args.node_id} [{node['tier'].upper()}]:\n")
230
+ print(f" 📌 #{node['id']} [{node['category']}] ⚡{node['energy']} — {node['content'][:150]}\n")
231
+
232
+ outgoing = conn.execute("SELECT e.relation, e.to_node, n.content FROM edges e JOIN nodes n ON e.to_node=n.id WHERE e.from_node=?", (args.node_id,)).fetchall()
233
+ if outgoing:
234
+ print(f" → OUT ({len(outgoing)}):")
235
+ for e in outgoing: print(f" --[{e['relation']}]--> #{e['to_node']}: {e['content'][:80]}")
236
+
237
+ incoming = conn.execute("SELECT e.relation, e.from_node, n.content FROM edges e JOIN nodes n ON e.from_node=n.id WHERE e.to_node=?", (args.node_id,)).fetchall()
238
+ if incoming:
239
+ print(f"\n ← IN ({len(incoming)}):")
240
+ for e in incoming: print(f" #{e['from_node']}: {e['content'][:80]} --[{e['relation']}]-->")
241
+
242
+
243
+ # ─── SEARCH ───────────────────────────────────────────────────────────────────
244
+
245
+ def cmd_search(args, conn):
246
+ keyword = args.keyword.strip()
247
+ tier_filter = f"AND tier = '{args.tier}'" if args.tier else ""
248
+
249
+ # 1. Exact Match Search
250
+ exact_rows = conn.execute(
251
+ f"SELECT id, tier, category, energy, content, created_at FROM nodes WHERE content LIKE ? {tier_filter} ORDER BY energy DESC LIMIT ?",
252
+ (f"%{keyword}%", args.limit)
253
+ ).fetchall()
254
+
255
+ results = []
256
+ seen = set()
257
+
258
+ for r in exact_rows:
259
+ seen.add(r["id"])
260
+ results.append({"row": r, "score": 1.0, "match": "exact"})
261
+
262
+ # 2. Semantic Search (Cosine Similarity)
263
+ query_emb = get_embedding(keyword)
264
+ if query_emb:
265
+ all_nodes = conn.execute(
266
+ f"SELECT id, tier, category, energy, content, created_at, embedding FROM nodes WHERE embedding IS NOT NULL {tier_filter}"
267
+ ).fetchall()
268
+ for r in all_nodes:
269
+ if r["id"] in seen: continue
270
+ sim = cosine_similarity(query_emb, r["embedding"])
271
+ if sim > 0.60:
272
+ results.append({"row": dict(r), "score": sim, "match": f"semantic: {sim:.2f}"})
273
+
274
+ # Sort & Limit
275
+ results.sort(key=lambda x: (x["score"], x["row"]["energy"]), reverse=True)
276
+ results = results[:args.limit]
277
+
278
+ tier_label = f" [{args.tier.upper()}]" if args.tier else ""
279
+ if not results:
280
+ print(f"🔍 No results for '{keyword}'{tier_label}")
281
+ return
282
+
283
+ print(f"🔍 Found {len(results)} result(s) for '{keyword}'{tier_label}:\n")
284
+ for res in results:
285
+ r = res["row"]
286
+ tier_icon = "🔥" if r["tier"] == "hot" else "❄️"
287
+ print(f" {tier_icon} #{r['id']} [{r['category']}] ⚡{r['energy']} ({res['match']}) — {r['content'][:120]}")
288
+
289
+ ids = [res["row"]["id"] for res in results]
290
+ if ids:
291
+ conn.execute(
292
+ f"UPDATE nodes SET energy = MIN(100, energy + 10), updated_at = ? WHERE id IN ({','.join('?'*len(ids))})",
293
+ [datetime.now().isoformat()] + ids
294
+ )
295
+ conn.commit()
296
+
297
+
298
+ # ─── GC & LIST (Truncated Logic) ──────────────────────────────────────────────
299
+
300
+ def cmd_gc(args, conn):
301
+ threshold = (datetime.now() - timedelta(days=args.days)).isoformat()
302
+ conn.execute("UPDATE nodes SET energy = energy - 15 WHERE updated_at < ? AND tier = 'hot'", (threshold,))
303
+ cur = conn.execute("DELETE FROM nodes WHERE energy <= 0")
304
+ conn.commit()
305
+ print(f"🗑️ GC: {cur.rowcount} dead nodes removed.")
306
+
307
+ def cmd_list(args, conn):
308
+ rows = conn.execute("SELECT id, tier, category, energy, content FROM nodes ORDER BY energy DESC LIMIT ?", (args.limit,)).fetchall()
309
+ hot_c = sum(1 for r in rows if r["tier"] == "hot")
310
+ print(f"📋 {hot_c} hot 🔥 | {len(rows)-hot_c} cold ❄️")
311
+ for r in rows:
312
+ print(f" #{r['id']} [{r['category']}] ⚡{r['energy']} — {r['content'][:80]}")
313
+
314
+ def cmd_export(args, conn):
315
+ tier_filter = f"WHERE tier = '{args.tier}'" if args.tier else ""
316
+ nodes = conn.execute(f"SELECT id, content, category, tier, energy, source_ids, created_at FROM nodes {tier_filter}").fetchall()
317
+ edges = conn.execute("SELECT * FROM edges").fetchall()
318
+ f = Path(args.output or ".agent/memory/export.json")
319
+ f.parent.mkdir(parents=True, exist_ok=True)
320
+ f.write_text(json.dumps({"nodes": [dict(r) for r in nodes], "edges": [dict(r) for r in edges]}, indent=2, ensure_ascii=False))
321
+ print(f"✅ Exported {len(nodes)} nodes to {f}")
322
+
323
+
324
+ # ─── Main ─────────────────────────────────────────────────────────────────────
325
+
326
+ def main():
327
+ p = argparse.ArgumentParser()
328
+ p.add_argument("--db", default=str(DEFAULT_DB))
329
+ sub = p.add_subparsers(dest="cmd")
330
+
331
+ ps = sub.add_parser("save")
332
+ ps.add_argument("content")
333
+ ps.add_argument("--category", "-c", default="general")
334
+
335
+ pf = sub.add_parser("search")
336
+ pf.add_argument("keyword")
337
+ pf.add_argument("--limit", "-n", type=int, default=10)
338
+ pf.add_argument("--tier", choices=["hot", "cold"], default=None)
339
+
340
+ sub.add_parser("hot").add_argument("--limit", "-n", type=int, default=20)
341
+ sub.add_parser("cold").add_argument("--limit", "-n", type=int, default=20)
342
+ sub.add_parser("list").add_argument("--limit", "-n", type=int, default=20)
343
+
344
+ pl = sub.add_parser("link")
345
+ pl.add_argument("from_id", type=int); pl.add_argument("to_id", type=int); pl.add_argument("--relation", "-r", default="related_to")
346
+
347
+ sub.add_parser("graph").add_argument("node_id", type=int)
348
+
349
+ pcon = sub.add_parser("consolidate")
350
+ pcon.add_argument("--days", type=int, default=7); pcon.add_argument("--category", default=None); pcon.add_argument("--force", action="store_true")
351
+
352
+ sub.add_parser("gc").add_argument("--days", type=int, default=30)
353
+
354
+ pex = sub.add_parser("export")
355
+ pex.add_argument("--output", "-o", default=None); pex.add_argument("--tier", default=None)
356
+
357
+ args = p.parse_args()
358
+ if not args.cmd: return p.print_help()
359
+
360
+ conn = get_db(Path(args.db))
361
+ try:
362
+ globals()["cmd_"+args.cmd](args, conn)
363
+ finally:
364
+ conn.close()
365
+
366
+ if __name__ == "__main__":
367
+ main()
@@ -1,7 +1,7 @@
1
1
  #!/bin/bash
2
2
 
3
3
  # --- Antigravity Unified Shutdown (Golden V6 - High Precision) ---
4
- # Dọn dẹp trạm gác CEOgravity.
4
+ # Dọn dẹp trạm gác Agentic Gateway.
5
5
 
6
6
  PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
7
7
  cd "$PROJECT_DIR"
@@ -13,7 +13,7 @@ fi
13
13
  BRIDGE_PORT=${BRIDGE_PORT:-9558}
14
14
 
15
15
  echo "════════════════════════════════════════════════════"
16
- echo " 🛑 SHUTTING DOWN CEOGRAVITY [$BRIDGE_PORT]..."
16
+ echo " 🛑 SHUTTING DOWN AG GATEWAY [$BRIDGE_PORT]..."
17
17
  echo "════════════════════════════════════════════════════"
18
18
 
19
19
  # 1. Tắt Sentinel Monitor
@@ -31,15 +31,15 @@ if [ -f "$BRIDGE_PID_FILE" ]; then
31
31
  fi
32
32
 
33
33
  # 3. Tắt Bot
34
- BOT_PID_FILE=".ceogravity_bot.pid"
34
+ BOT_PID_FILE=".ag_gateway_bot.pid"
35
35
  if [ -f "$BOT_PID_FILE" ]; then
36
36
  PID=$(cat "$BOT_PID_FILE")
37
37
  # Kill the whole process group for the bash loop
38
38
  pkill -P $PID
39
39
  kill $PID 2>/dev/null && rm "$BOT_PID_FILE"
40
- echo "✅ CEOgravity Bot stopped."
40
+ echo "✅ AG Gateway Bot stopped."
41
41
  fi
42
42
 
43
43
  echo "════════════════════════════════════════════════════"
44
- echo "✅ CEOGRAVITY IS IDLE."
44
+ echo "✅ AG GATEWAY IS IDLE."
45
45
  echo "════════════════════════════════════════════════════"
@@ -1,8 +1,8 @@
1
1
  #!/bin/bash
2
2
 
3
3
  # --- Antigravity Unified Receptionist (Golden V6 - High Precision) ---
4
- # Trạm gác hợp nhất: CEOgravity Bot + Portal Bridge (HUD)
5
- # Dự án: CEOgravity Startup Orchestrator
4
+ # Trạm gác hợp nhất: AG Gateway Bot + Portal Bridge (HUD)
5
+ # Dự án: Template Agentic Orchestrator
6
6
 
7
7
  # 1. Load Cấu hình & Xử lý Cổng Thông minh
8
8
  PROJECT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
@@ -17,7 +17,7 @@ else
17
17
  fi
18
18
 
19
19
  echo "════════════════════════════════════════════════════"
20
- echo " 🏢 CEOGRAVITY GATEWAY - PORTAL [$BRIDGE_PORT]"
20
+ echo " 🏢 AG GATEWAY - PORTAL [$BRIDGE_PORT]"
21
21
  echo "════════════════════════════════════════════════════"
22
22
 
23
23
  # 2. Xử lý Chế độ Headless & Cloudflare Tunnel
@@ -99,13 +99,16 @@ nohup node scripts/ag_portal_bridge.js > ".portal_bridge_${BRIDGE_PORT}.log" 2>&
99
99
  echo $! > "$BRIDGE_PID_FILE"
100
100
  echo " ✅ Bridge running (PID: $(cat "$BRIDGE_PID_FILE") on Port $BRIDGE_PORT)"
101
101
 
102
- # 4. Khởi động CEOgravity Bot (Local TSX)
103
- echo "[4/4] 🤖 Waking up CEOgravity Bot... "
104
- BOT_PID_FILE=".ceogravity_bot.pid"
102
+ # 4. Khởi động AG Gateway Bot (Trợ lý Telegram)
103
+ echo "[4/4] 🤖 Waking up AG Gateway Bot... "
104
+ BOT_PID_FILE=".ag_gateway_bot.pid"
105
105
 
106
106
  # Kill loop AND all tsx/node processes belonging to this token/project
107
- pkill -f "tsx src/index.ts" 2>/dev/null
108
- pkill -f "node src/index.ts" 2>/dev/null
107
+ # Chỉ kill nếu dự án có thư mục src/index.ts (tránh kill nhầm project khác chạy chung server)
108
+ if [ -f "src/index.ts" ] || [ -f "bot/index.ts" ]; then
109
+ pkill -f "tsx src/index.ts" 2>/dev/null
110
+ pkill -f "node src/index.ts" 2>/dev/null
111
+ fi
109
112
  pkill -f "interaction_final.log" 2>/dev/null
110
113
  if [ -f "$BOT_PID_FILE" ]; then
111
114
  kill -9 $(cat "$BOT_PID_FILE") 2>/dev/null
@@ -118,11 +121,11 @@ nohup bash -c "while true; do
118
121
  npx tsx src/index.ts >> interaction_final.log 2>&1
119
122
  EXIT_CODE=\$?
120
123
  if [ \$EXIT_CODE -eq 0 ]; then break; fi # Exit loop if deliberate stop
121
- echo \"\$(date): Bot crashed (Code: \$EXIT_CODE). Restarting in 5s...\" >> interaction_final.log
124
+ echo \"\$(date): AG Gateway Bot crashed (Code: \$EXIT_CODE). Restarting in 5s...\" >> interaction_final.log
122
125
  sleep 5
123
126
  done" &
124
127
  echo $! > "$BOT_PID_FILE"
125
- echo " ✅ Bot active (PID: $(cat "$BOT_PID_FILE"))"
128
+ echo " ✅ AG Gateway Bot active (PID: $(cat "$BOT_PID_FILE"))"
126
129
 
127
130
  # 5. Sentinel Monitor (Giám sát Trí não)
128
131
  echo "🛡️ Sentinel Active: Auto-shutdown if IDE connection lost."
@@ -0,0 +1,21 @@
1
+ #!/bin/bash
2
+ # refresh_brain.sh — Tầng 1 Memory: Rebuild codebase index for the current project
3
+ # Usage: bash .agent/scripts/refresh_brain.sh [root_dir]
4
+ # Output: .agent/cache/repomap.json
5
+
6
+ set -e
7
+ ROOT="${1:-.}"
8
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
9
+ OUTPUT="$ROOT/.agent/cache/repomap.json"
10
+
11
+ echo "🧠 Refreshing Codebase Brain (Tầng 1)..."
12
+ echo " Root: $(realpath $ROOT)"
13
+ echo " Output: $OUTPUT"
14
+
15
+ python3 "$SCRIPT_DIR/repomap.py" "$ROOT" --output "$OUTPUT" --max-files 200
16
+
17
+ echo ""
18
+ echo "✅ Codebase index updated at: $OUTPUT"
19
+ echo " AI agents can now read this file instead of re-scanning the entire codebase."
20
+ echo ""
21
+ echo "📌 Next: Run memory_tool.py to access Tầng 2 (Working Memory Graph)"
@@ -121,6 +121,35 @@ class RepoMapper:
121
121
  return "\n".join(repo_map)
122
122
 
123
123
  if __name__ == "__main__":
124
- root = sys.argv[1] if len(sys.argv) > 1 else "."
125
- mapper = RepoMapper(root)
126
- print(mapper.generate_map())
124
+ import argparse
125
+ import json
126
+ from datetime import datetime
127
+
128
+ parser = argparse.ArgumentParser(description="RepoMapper: Generate a structural map of a codebase.")
129
+ parser.add_argument("root", nargs="?", default=".", help="Root directory to scan (default: current dir)")
130
+ parser.add_argument("--output", "-o", help="Output file path (e.g. .agent/cache/repomap.json). If not set, prints to stdout.")
131
+ parser.add_argument("--max-files", type=int, default=100, help="Max files to scan (default: 100)")
132
+ parser.add_argument("--format", choices=["text", "json"], default="text", help="Output format (default: text)")
133
+ args = parser.parse_args()
134
+
135
+ mapper = RepoMapper(args.root)
136
+ text_map = mapper.generate_map(max_files=args.max_files)
137
+
138
+ if args.output:
139
+ output_path = Path(args.output)
140
+ output_path.parent.mkdir(parents=True, exist_ok=True)
141
+
142
+ if args.format == "json" or args.output.endswith(".json"):
143
+ data = {
144
+ "generated_at": datetime.now().isoformat(),
145
+ "root": str(Path(args.root).absolute()),
146
+ "map": text_map,
147
+ "lines": text_map.splitlines()
148
+ }
149
+ output_path.write_text(json.dumps(data, indent=2, ensure_ascii=False))
150
+ else:
151
+ output_path.write_text(text_map)
152
+
153
+ print(f"✅ RepoMap saved to: {output_path.absolute()}")
154
+ else:
155
+ print(text_map)
@@ -12,7 +12,8 @@ allowed-tools: Read, Write, Glob, Grep, Commands
12
12
 
13
13
  ## 1. 🧬 KHẲNG ĐỊNH ĐỊNH DANH (IDENTITY BOOTSTRAP)
14
14
 
15
- - **Cội nguồn:** Bạn ra đời từ khuôn mẫu **`ag-kit` (Antigravity Kit)** - một Framework giúp tự động hóa và quản trị các AI Agents khác.
15
+ - **Cội nguồn:** Bạn ra đời từ khuôn mẫu **`ag-kit` (Antigravity Kit)** - một Framework riêng tư của Owner (một cá nhân duy nhất), giúp tự động hóa và quản trị các AI Agents trên nhiều dự án.
16
+ - **Hệ sinh thái:** Bạn đang ở trong **một trong nhiều dự án** thuộc hệ sinh thái ag-kit. Đọc `.agent/knowledge/ag-kit-ecosystem.md` để biết toàn cảnh.
16
17
  - **Vai trò:** Bạn đóng vai trò "Kẻ Điều Phối" (Supervisor) giám sát và cấy ghép trí não vào "Bác sĩ Phẫu thuật" (Antigravity IDE) qua giao thức CDP.
17
18
  - **Luật Cấm Kị (Anti-Hallucination):** Tuyệt đối không được dùng tư duy lập trình phổ quát máy móc để sửa dự án này. Mọi quyết định sửa mã liên quan đến CDP, luồng Telegram, hay Ports mạng, CẦN chiếu theo các file `SKILL.md` lân cận (`telegram-agentic-gateway`, `remoat-integration`, `llm-routing-quirks`).
18
19
 
@@ -45,6 +46,76 @@ Phải cực kỳ tỉnh táo để nhận diện loại kiến thức trước
45
46
  2. **Kiến trúc Lõi (AG-KIT Framework):** Nếu logic bạn vừa sửa là kiến trúc lõi dùng chung cho mọi con Bot/Agent khác về sau (Ví dụ: Định tuyến LLM, Telegram Gateway, Cơ chế Dual-Ports, Lỗi đứt cáp CDP) $\rightarrow$ Bạn **BẮT BUỘC phải ghi vào `.agent/skills/`** và đóng gói code chuẩn mực vào thư mục `templates/` của ag-kit.
46
47
  *Bê logic riêng của dự án nhét vào ag-kit là tội đồ làm ô nhiễm Framework.*
47
48
 
49
+ ### 3.1 🛡️ LUẬT CẤM "CHẮP VÁ" (NO FRAGMENTED UPDATES)
50
+
51
+ **Đây là quy tắc tối cao để giữ Giao diện Trí tuệ của Agent luôn chuyên nghiệp và logic.**
52
+
53
+ - **Cảnh báo Số thứ tự (Index Integrity):** Cấm tuyệt đối việc cập nhật nhảy cóc số thứ tự (Ví dụ: 1, 2, 3 -> 5). Mọi danh sách phải được AI rà soát lại từ đầu đến cuối trước khi ghi file để đảm bảo tính tuần tự (1, 2, 3, 4, 5...).
54
+ - **Cảnh báo Chỗ trống (Structural Gap):** Không được để lại các đề mục rỗng hoặc các chú thích "Sẽ cập nhật sau".
55
+ - **Hành động bắt buộc:** Trước khi thực hiện `multi_replace_file_content` hay `replace_file_content`, Agent phải thực hiện một bước "Visual Audit" (Đọc lại toàn bộ nội dung file) để đảm bảo các thay đổi mới không làm gãy cấu trúc cũ hoặc gây lộn xộn nội dung. *Mọi sự cẩu thả trong trình bày tri thức sẽ bị coi là lỗi hệ thống.*
56
+
57
+ ### 3.2 🧠 BẢN ĐỒ 3 TẦNG TRÍ NHỚ (THE 3-TIER MEMORY MODEL — MANDATORY)
58
+
59
+ **Mọi Agent vận hành trên ag-kit PHẢI hiểu và lưu đúng tầng:**
60
+
61
+ ```
62
+ ┌─────────────────────────────────────────────────────────────┐
63
+ │ TẦNG 3 — KNOWLEDGE BASE (Sự Thật Vĩnh Cửu) │
64
+ │ Lưu ở: .agent/knowledge/*.md │
65
+ │ Nội dung: kiến trúc, patterns, quy tắc đã được duyệt │
66
+ │ ✅ Git-tracked ✅ Con người đọc được ✅ Cross-session │
67
+ ├─────────────────────────────────────────────────────────────┤
68
+ │ TẦNG 2 — WORKING MEMORY GRAPH (Ký Ức Động) │
69
+ │ Lưu ở: .agent/memory/graph.db (SQLite) │
70
+ │ Nội dung: error solutions, decisions, context sự kiện │
71
+ │ Tool: python3 .agent/scripts/memory_tool.py │
72
+ │ ⚠️ .gitignore ✅ Auto-GC (Energy decay) ✅ Persistent │
73
+ ├─────────────────────────────────────────────────────────────┤
74
+ │ TẦNG 1 — CODEBASE INDEX (Đôi Mắt) │
75
+ │ Lưu ở: .agent/cache/repomap.json │
76
+ │ Nội dung: sơ đồ function/class của toàn codebase │
77
+ │ Rebuild: bash .agent/scripts/refresh_brain.sh │
78
+ │ ⚠️ .gitignore ✅ Auto-rebuild khi code đổi ✅ Fast │
79
+ └─────────────────────────────────────────────────────────────┘
80
+ ```
81
+
82
+ **Quy tắc định tuyến (Routing Rules):**
83
+ | Loại thông tin | Lưu vào Tầng |
84
+ |---|---|
85
+ | Kiến trúc, quy tắc vận hành đã xác nhận | Tầng 3 (`.md`) |
86
+ | Bug đã fix, quyết định ngắn hạn, context phiên | Tầng 2 (`graph.db`) |
87
+ | Cấu trúc code, danh sách hàm | Tầng 1 (`repomap.json`) |
88
+ | Trọng số sống, dữ liệu thời gian thực | Biệt lập (`brain_v1.json` + `.gitignore`) |
89
+
90
+ **Auto-Brain (Tổng hợp tự động — KHÔNG CẦN GÕ LỆNH):**
91
+ - Mỗi project có `.agent/brain/summary.md` — bộ não tổng hợp từ cả 3 tầng trên.
92
+ - Tự động rebuild sau mỗi `git commit` (hook) và khi khởi động session.
93
+ - Tool: `python3 .agent/scripts/brain_builder.py`
94
+ - Global registry: `~/.config/ag-kit/projects.json` — danh bạ tất cả projects.
95
+
96
+ ### 3.3 🧠 GIAO THỨC MCP MEMORY (MANDATORY)
97
+
98
+ **BẮT BUỘC: AI phải gọi MCP tools, KHÔNG được dùng shell command để truy cập memory.**
99
+
100
+ Server `ag-kit-memory` được đăng ký sẵn trong `~/.gemini/antigravity/mcp_config.json`. AI gọi trực tiếp:
101
+
102
+ | Tool | Khi nào gọi |
103
+ |---|---|
104
+ | `memory_search(keyword)` | **TRƯỚC** khi bắt đầu bất kỳ task nào — kiểm tra context cũ |
105
+ | `memory_save(content, category)` | **SAU** khi fix bug, ra quyết định, học được điều mới |
106
+ | `memory_link(from, to, relation)` | Khi phát hiện 2 kiến thức có quan hệ |
107
+ | `memory_consolidate(days=7)` | Hàng tuần: gộp hot → cold summaries |
108
+ | `memory_status()` | Kiểm tra sức khỏe của bộ não |
109
+
110
+ ```
111
+ Workflow đúng:
112
+ 1. Bắt đầu task → memory_search("topic") → đọc context cũ
113
+ 2. Làm việc...
114
+ 3. Xong task → memory_save("kết quả/quyết định quan trịng")
115
+ 4. Hàng tuần → memory_consolidate(days=7) → dọn rác, tổng hợp
116
+ ```
117
+
118
+
48
119
  ---
49
120
 
50
121
  ## 4. 🔗 THE DUO-CORE (Kiến Trúc Multi-Tenant Bắt Buộc)
@@ -52,7 +123,22 @@ Hãy khắc cốt ghi tâm quy tắc: `IDE_PORT` (Nhận lệnh AI - ví dụ: 9
52
123
 
53
124
  ---
54
125
 
55
- ## 5. 🛠️ HỆ THỐNG CLI CỦA AG-KIT (BẢN ĐỒ CÔNG CỤ)
126
+ ## 5. 🌐 TẦM NHÌN XUYÊN DỰ ÁN VÀ TỔNG QUÁT HÓA (CROSS-PROJECT VISION & GENERALIZATION)
127
+
128
+ **ĐÂY LÀ QUY TẮC ĐỂ TRÁNH "NHIỄM ĐỘC NGỮ CẢNH" (CONTEXT CONTAMINATION).**
129
+
130
+ - **Quyền quan sát (Workspace as Library):** Bạn ĐƯỢC PHÉP "soi" sang các thư mục khác trong `/home/tao/Projects/` để nghiên cứu giải pháp kiến trúc.
131
+ - **Giao thức đọc bộ não dự án khác (Cross-Project Brain Reading):**
132
+ 1. Kiểm tra `~/.config/ag-kit/projects.json` để tìm tên Project cần xem.
133
+ 2. Đọc `<project_root>/.agent/brain/summary.md` (một file duy nhất, ~500 tokens).
134
+ 3. Nếu cần đi sâu hơn: dùng `vfs <project_root>/src` hoặc `memory_tool.py search`.
135
+ 4. **CẤM** scan toàn bộ thư mục project khác khi chưa đọc `summary.md` trước.
136
+ - **Cấm ô nhiễm (No Leakage):** Tuyệt đối không ghi chép các từ khóa chuyên biệt (B1, B2, Sentinel, DNSE...) vào các file Skill dùng chung.
137
+ - **Tổng quát hóa (Sanitization):** Mọi pattern học được từ project A khi đưa vào Core Framework đều phải tẩy sạch tên nghiệp vụ. Trước khi trích xuất pattern lớn, hỏi Sếp: *"Em thấy pattern X ở dự án Y rất hay, em xin phép tổng quát hóa nó không?"*
138
+
139
+ ---
140
+
141
+ ## 6. 🛠️ HỆ THỐNG CLI CỦA AG-KIT (BẢN ĐỒ CÔNG CỤ)
56
142
  Hệ sinh thái `ag-kit` được cung cấp sức mạnh cài đặt và cập nhật thông qua 2 bộ công cụ trên NPM. Bất kỳ AI nào khi làm việc cũng phải hiểu rõ điểm khác biệt và biết cách hướng dẫn User:
57
143
 
58
144
  1. **Bộ Cập Nhật & Quản Trị Tương Tác (@mrtrinhvn/ag-kit):**