tiger-agent 0.2.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,302 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+ const readline = require('readline');
6
+ const { execFileSync } = require('child_process');
7
+ const { encryptString } = require('./cryptoEnv');
8
+
9
+ function exists(p) {
10
+ try {
11
+ fs.accessSync(p, fs.constants.F_OK);
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ function writeFile600(filePath, content) {
19
+ fs.writeFileSync(filePath, content, { mode: 0o600 });
20
+ }
21
+
22
+ function normalizeYesNo(s, defBool) {
23
+ const t = String(s || '').trim().toLowerCase();
24
+ if (!t) return defBool;
25
+ if (['y', 'yes', 'true', '1', 'on'].includes(t)) return true;
26
+ if (['n', 'no', 'false', '0', 'off'].includes(t)) return false;
27
+ return defBool;
28
+ }
29
+
30
+ function createRl() {
31
+ return readline.createInterface({ input: process.stdin, output: process.stdout });
32
+ }
33
+
34
+ function question(rl, prompt) {
35
+ return new Promise((resolve) => rl.question(prompt, (ans) => resolve(ans)));
36
+ }
37
+
38
+ async function questionHidden(prompt) {
39
+ // Minimal hidden input prompt that works in a normal TTY.
40
+ // Prints '*' for each char.
41
+ return await new Promise((resolve, reject) => {
42
+ const stdin = process.stdin;
43
+ const stdout = process.stdout;
44
+
45
+ if (!stdin.isTTY) {
46
+ reject(new Error('Hidden prompt requires a TTY'));
47
+ return;
48
+ }
49
+
50
+ stdout.write(prompt);
51
+ stdin.setRawMode(true);
52
+ stdin.resume();
53
+
54
+ let buf = '';
55
+ function onData(chunk) {
56
+ const s = chunk.toString('utf8');
57
+ for (const ch of s) {
58
+ if (ch === '\r' || ch === '\n') {
59
+ stdout.write('\n');
60
+ cleanup();
61
+ resolve(buf);
62
+ return;
63
+ }
64
+ if (ch === '\u0003') {
65
+ // Ctrl-C
66
+ cleanup();
67
+ reject(new Error('Aborted'));
68
+ return;
69
+ }
70
+ if (ch === '\u007f') {
71
+ // backspace
72
+ if (buf.length) {
73
+ buf = buf.slice(0, -1);
74
+ stdout.write('\b \b');
75
+ }
76
+ continue;
77
+ }
78
+ buf += ch;
79
+ stdout.write('*');
80
+ }
81
+ }
82
+
83
+ function cleanup() {
84
+ stdin.off('data', onData);
85
+ stdin.setRawMode(false);
86
+ stdin.pause();
87
+ }
88
+
89
+ stdin.on('data', onData);
90
+ });
91
+ }
92
+
93
+ function envLine(k, v) {
94
+ if (v == null) v = '';
95
+ const s = String(v);
96
+ // Quote if contains spaces or # to avoid comment truncation.
97
+ const needsQuotes = /\s|#|"|'/g.test(s);
98
+ if (!s) return `${k}=`;
99
+ if (!needsQuotes) return `${k}=${s}`;
100
+ // Use double quotes, escape backslashes and quotes.
101
+ const escaped = s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
102
+ return `${k}="${escaped}"`;
103
+ }
104
+
105
+ (async function main() {
106
+ const cwd = process.cwd();
107
+ const envPath = path.join(cwd, '.env');
108
+ const secretsPath = path.join(cwd, '.env.secrets');
109
+ const secretsEncPath = path.join(cwd, '.env.secrets.enc');
110
+
111
+ const rl = createRl();
112
+ try {
113
+ console.log('Tiger setup (creates .env and .env.secrets; optionally .env.secrets.enc)');
114
+
115
+ if (exists(envPath) || exists(secretsPath) || exists(secretsEncPath)) {
116
+ const ans = await question(
117
+ rl,
118
+ 'Existing config found (.env/.env.secrets/.env.secrets.enc). Overwrite? (y/N): '
119
+ );
120
+ const ok = normalizeYesNo(ans, false);
121
+ if (!ok) {
122
+ console.log('Canceled. No files changed.');
123
+ process.exit(0);
124
+ }
125
+ }
126
+
127
+ const providerAns = (await question(rl, 'Choose provider [moonshot/code] (moonshot): ')).trim();
128
+ const provider = (providerAns || 'moonshot').toLowerCase();
129
+ if (!['moonshot', 'code'].includes(provider)) {
130
+ throw new Error('Invalid provider. Use moonshot or code.');
131
+ }
132
+
133
+ const useTelegram = normalizeYesNo(await question(rl, 'Enable Telegram bot? (y/N): '), false);
134
+
135
+ // Non-secret config
136
+ const baseUrlDefault = provider === 'code' ? 'https://api.kimi.com/coding/v1' : 'https://api.moonshot.cn/v1';
137
+ const chatModelDefault = provider === 'code' ? 'k2p5' : 'kimi-k1';
138
+ const embedModelDefault = provider === 'code' ? '' : 'kimi-embedding-v1';
139
+ const userAgentDefault = provider === 'code' ? 'KimiCLI/0.77' : '';
140
+
141
+ const baseUrl = (await question(rl, `KIMI_BASE_URL (${baseUrlDefault}): `)).trim() || baseUrlDefault;
142
+ const chatModel = (await question(rl, `KIMI_CHAT_MODEL (${chatModelDefault}): `)).trim() || chatModelDefault;
143
+ const enableEmbeddings = normalizeYesNo(
144
+ await question(rl, `Enable embeddings? (${provider === 'code' ? 'N' : 'Y'}/n): `),
145
+ provider !== 'code'
146
+ );
147
+ const embedModel = enableEmbeddings
148
+ ? (await question(rl, `KIMI_EMBED_MODEL (${embedModelDefault || 'empty'}): `)).trim() || embedModelDefault
149
+ : '';
150
+ const userAgent = (await question(rl, `KIMI_USER_AGENT (${userAgentDefault || 'empty'}): `)).trim() || userAgentDefault;
151
+
152
+ const timeoutMsRaw = (await question(rl, 'KIMI_TIMEOUT_MS (30000): ')).trim();
153
+ const timeoutMs = timeoutMsRaw ? String(Number(timeoutMsRaw) || 30000) : '30000';
154
+
155
+ const ownHoursRaw = (await question(rl, 'OWN_SKILL_UPDATE_HOURS (24): ')).trim();
156
+ const soulHoursRaw = (await question(rl, 'SOUL_UPDATE_HOURS (24): ')).trim();
157
+ const reflectionHoursRaw = (await question(rl, 'REFLECTION_UPDATE_HOURS (12): ')).trim();
158
+ const ingestTurnsRaw = (await question(rl, 'MEMORY_INGEST_EVERY_TURNS (2): ')).trim();
159
+ const ingestMinCharsRaw = (await question(rl, 'MEMORY_INGEST_MIN_CHARS (140): ')).trim();
160
+ const ownHours = String(Math.max(1, Number(ownHoursRaw || 24)));
161
+ const soulHours = String(Math.max(1, Number(soulHoursRaw || 24)));
162
+ const reflectionHours = String(Math.max(1, Number(reflectionHoursRaw || 12)));
163
+ const ingestTurns = String(Math.max(1, Number(ingestTurnsRaw || 2)));
164
+ const ingestMinChars = String(Math.max(20, Number(ingestMinCharsRaw || 140)));
165
+
166
+ const allowShell = normalizeYesNo(await question(rl, 'ALLOW_SHELL (false): '), false);
167
+ const allowSkillInstall = normalizeYesNo(await question(rl, 'ALLOW_SKILL_INSTALL (false): '), false);
168
+
169
+ const dataDir = (await question(rl, 'DATA_DIR (./data): ')).trim() || './data';
170
+ const dbPath = (await question(rl, 'DB_PATH (./db/agent.json): ')).trim() || './db/agent.json';
171
+ const usePersistentVectorDb = normalizeYesNo(
172
+ await question(rl, 'Use persistent SQLite vector DB path? (Y/n): '),
173
+ true
174
+ );
175
+ const defaultVectorDbPath = usePersistentVectorDb ? './db/memory.sqlite' : '/tmp/tiger_memory.db';
176
+ const vectorDbPath =
177
+ (await question(rl, `VECTOR_DB_PATH (${defaultVectorDbPath}): `)).trim() || defaultVectorDbPath;
178
+ const sqliteVecExtension = (await question(rl, 'SQLITE_VEC_EXTENSION (optional path): ')).trim();
179
+ const runSqliteVecSetup = normalizeYesNo(
180
+ await question(rl, 'Run sqlite-vec auto-setup now? (pip install + detect path) (y/N): '),
181
+ false
182
+ );
183
+
184
+ const maxMessagesRaw = (await question(rl, 'MAX_MESSAGES (200): ')).trim();
185
+ const recentMessagesRaw = (await question(rl, 'RECENT_MESSAGES (40): ')).trim();
186
+ const maxMessages = String(Number(maxMessagesRaw || 200) || 200);
187
+ const recentMessages = String(Number(recentMessagesRaw || 40) || 40);
188
+
189
+ // Secrets
190
+ let moonshotKey = '';
191
+ let kimiCodeKey = '';
192
+ let kimiAliasKey = '';
193
+ if (provider === 'moonshot') {
194
+ moonshotKey = (await questionHidden('MOONSHOT_API_KEY (hidden): ')).trim();
195
+ } else {
196
+ kimiCodeKey = (await questionHidden('KIMI_CODE_API_KEY (hidden): ')).trim();
197
+ }
198
+ const useAlias = normalizeYesNo(await question(rl, 'Also set KIMI_API_KEY alias? (y/N): '), false);
199
+ if (useAlias) {
200
+ kimiAliasKey = (await questionHidden('KIMI_API_KEY (hidden): ')).trim();
201
+ }
202
+
203
+ let telegramToken = '';
204
+ if (useTelegram) {
205
+ telegramToken = (await questionHidden('TELEGRAM_BOT_TOKEN (hidden): ')).trim();
206
+ }
207
+
208
+ const wantEncrypt = normalizeYesNo(
209
+ await question(rl, 'Encrypt .env.secrets to .env.secrets.enc and delete plaintext? (y/N): '),
210
+ false
211
+ );
212
+
213
+ let secretsPass = '';
214
+ if (wantEncrypt) {
215
+ secretsPass = await questionHidden('SECRETS_PASSPHRASE (hidden): ');
216
+ const secretsPass2 = await questionHidden('Confirm SECRETS_PASSPHRASE (hidden): ');
217
+ if (secretsPass !== secretsPass2) throw new Error('Passphrases do not match');
218
+ secretsPass = secretsPass.trim();
219
+ if (!secretsPass) throw new Error('Empty passphrase not allowed');
220
+ }
221
+
222
+ // Write files
223
+ const envLines = [
224
+ '# Non-secret config',
225
+ envLine('KIMI_PROVIDER', provider),
226
+ envLine('KIMI_BASE_URL', baseUrl),
227
+ envLine('KIMI_CHAT_MODEL', chatModel),
228
+ envLine('KIMI_EMBED_MODEL', embedModel),
229
+ envLine('KIMI_USER_AGENT', userAgent),
230
+ envLine('KIMI_ENABLE_EMBEDDINGS', enableEmbeddings ? 'true' : 'false'),
231
+ envLine('KIMI_TIMEOUT_MS', timeoutMs),
232
+ envLine('OWN_SKILL_UPDATE_HOURS', ownHours),
233
+ envLine('SOUL_UPDATE_HOURS', soulHours),
234
+ envLine('REFLECTION_UPDATE_HOURS', reflectionHours),
235
+ envLine('MEMORY_INGEST_EVERY_TURNS', ingestTurns),
236
+ envLine('MEMORY_INGEST_MIN_CHARS', ingestMinChars),
237
+ '',
238
+ '# Encrypted secrets support (optional)',
239
+ envLine('SECRETS_FILE', '.env.secrets.enc'),
240
+ // If encrypting, user must export SECRETS_PASSPHRASE when running the bot.
241
+ envLine('SECRETS_PASSPHRASE', ''),
242
+ '',
243
+ envLine('ALLOW_SHELL', allowShell ? 'true' : 'false'),
244
+ envLine('ALLOW_SKILL_INSTALL', allowSkillInstall ? 'true' : 'false'),
245
+ envLine('DATA_DIR', dataDir),
246
+ envLine('DB_PATH', dbPath),
247
+ envLine('VECTOR_DB_PATH', vectorDbPath),
248
+ envLine('SQLITE_VEC_EXTENSION', sqliteVecExtension),
249
+ envLine('MAX_MESSAGES', maxMessages),
250
+ envLine('RECENT_MESSAGES', recentMessages),
251
+ ''
252
+ ];
253
+
254
+ const secretLines = [
255
+ '# Secrets only (do not commit)',
256
+ envLine('MOONSHOT_API_KEY', moonshotKey),
257
+ envLine('KIMI_CODE_API_KEY', kimiCodeKey),
258
+ envLine('KIMI_API_KEY', kimiAliasKey),
259
+ envLine('TELEGRAM_BOT_TOKEN', telegramToken),
260
+ ''
261
+ ];
262
+
263
+ writeFile600(envPath, envLines.join('\n') + '\n');
264
+ writeFile600(secretsPath, secretLines.join('\n') + '\n');
265
+
266
+ if (wantEncrypt) {
267
+ const plaintext = fs.readFileSync(secretsPath, 'utf8');
268
+ const payload = encryptString(plaintext, secretsPass);
269
+ fs.writeFileSync(secretsEncPath, JSON.stringify(payload, null, 2) + '\n', { mode: 0o600 });
270
+ fs.unlinkSync(secretsPath);
271
+ console.log('Wrote .env.secrets.enc and removed plaintext .env.secrets');
272
+ console.log('When running the bot, you must export SECRETS_PASSPHRASE in your shell or service env.');
273
+ } else {
274
+ console.log('Wrote .env and .env.secrets');
275
+ }
276
+
277
+ if (runSqliteVecSetup) {
278
+ try {
279
+ const out = execFileSync(
280
+ 'python3',
281
+ ['scripts/sqlite_vec_setup.py', '--install', '--write-env'],
282
+ { cwd, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }
283
+ );
284
+ console.log(`sqlite-vec setup result: ${String(out || '').trim()}`);
285
+ } catch (err) {
286
+ const msg =
287
+ (err && err.stderr && String(err.stderr).trim()) ||
288
+ (err && err.stdout && String(err.stdout).trim()) ||
289
+ (err && err.message) ||
290
+ 'unknown sqlite-vec setup error';
291
+ console.log(`sqlite-vec setup skipped/failed: ${msg}`);
292
+ }
293
+ }
294
+
295
+ console.log('Setup complete. Restart the bot to load the new config.');
296
+ } finally {
297
+ rl.close();
298
+ }
299
+ })().catch((err) => {
300
+ console.error(`Setup failed: ${err.message}`);
301
+ process.exit(1);
302
+ });
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env python3
2
+ import argparse
3
+ import json
4
+ import math
5
+ import os
6
+ import sqlite3
7
+ import sys
8
+ from typing import Any, Dict, List
9
+
10
+
11
+ def connect(db_path: str) -> sqlite3.Connection:
12
+ os.makedirs(os.path.dirname(db_path), exist_ok=True)
13
+ conn = sqlite3.connect(db_path)
14
+ conn.row_factory = sqlite3.Row
15
+ return conn
16
+
17
+
18
+ def set_meta(conn: sqlite3.Connection, key: str, value: str) -> None:
19
+ conn.execute(
20
+ "INSERT INTO meta(key, value) VALUES(?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value",
21
+ (key, value),
22
+ )
23
+
24
+
25
+ def init_db(db_path: str, vec_ext_path: str) -> Dict[str, Any]:
26
+ conn = connect(db_path)
27
+ try:
28
+ conn.execute(
29
+ """
30
+ CREATE TABLE IF NOT EXISTS conversations (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ conversation_id TEXT NOT NULL UNIQUE,
33
+ platform TEXT DEFAULT '',
34
+ user_id TEXT DEFAULT '',
35
+ created_at INTEGER NOT NULL,
36
+ updated_at INTEGER NOT NULL
37
+ )
38
+ """
39
+ )
40
+ conn.execute(
41
+ """
42
+ CREATE TABLE IF NOT EXISTS messages (
43
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
44
+ conversation_id TEXT NOT NULL,
45
+ role TEXT NOT NULL,
46
+ content TEXT NOT NULL,
47
+ created_at INTEGER NOT NULL
48
+ )
49
+ """
50
+ )
51
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_messages_conv_time ON messages(conversation_id, created_at)")
52
+ conn.execute(
53
+ """
54
+ CREATE TABLE IF NOT EXISTS skills (
55
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
56
+ name TEXT NOT NULL UNIQUE,
57
+ provider TEXT DEFAULT '',
58
+ enabled INTEGER DEFAULT 1,
59
+ updated_at INTEGER NOT NULL
60
+ )
61
+ """
62
+ )
63
+ conn.execute(
64
+ """
65
+ CREATE TABLE IF NOT EXISTS memories (
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
67
+ conversation_id TEXT NOT NULL,
68
+ source TEXT NOT NULL,
69
+ content TEXT NOT NULL,
70
+ embedding_json TEXT NOT NULL,
71
+ created_at INTEGER NOT NULL
72
+ )
73
+ """
74
+ )
75
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_memories_conv_time ON memories(conversation_id, created_at)")
76
+ conn.execute("CREATE INDEX IF NOT EXISTS idx_memories_source_time ON memories(source, created_at)")
77
+ conn.execute(
78
+ """
79
+ CREATE TABLE IF NOT EXISTS meta (
80
+ key TEXT PRIMARY KEY,
81
+ value TEXT NOT NULL
82
+ )
83
+ """
84
+ )
85
+
86
+ vec_loaded = False
87
+ vec_error = ""
88
+ if vec_ext_path:
89
+ try:
90
+ conn.enable_load_extension(True)
91
+ conn.load_extension(vec_ext_path)
92
+ vec_loaded = True
93
+ except Exception as err: # pragma: no cover
94
+ vec_loaded = False
95
+ vec_error = str(err)
96
+ finally:
97
+ try:
98
+ conn.enable_load_extension(False)
99
+ except Exception:
100
+ pass
101
+
102
+ set_meta(conn, "sqlite_vec_extension_path", vec_ext_path or "")
103
+ set_meta(conn, "sqlite_vec_loaded", "1" if vec_loaded else "0")
104
+ set_meta(conn, "sqlite_vec_error", vec_error)
105
+ conn.commit()
106
+ return {"ok": True, "vec_loaded": vec_loaded, "vec_error": vec_error}
107
+ finally:
108
+ conn.close()
109
+
110
+
111
+ def stats(db_path: str) -> Dict[str, Any]:
112
+ conn = connect(db_path)
113
+ try:
114
+ out = {}
115
+ for table in ["memories", "conversations", "messages", "skills"]:
116
+ row = conn.execute(f"SELECT COUNT(*) AS c FROM {table}").fetchone()
117
+ out[table] = int(row["c"] if row else 0)
118
+ return {"ok": True, "counts": out}
119
+ finally:
120
+ conn.close()
121
+
122
+
123
+ def upsert_skill(db_path: str, name: str, provider: str, enabled: int, updated_at: int) -> Dict[str, Any]:
124
+ conn = connect(db_path)
125
+ try:
126
+ conn.execute(
127
+ """
128
+ INSERT INTO skills(name, provider, enabled, updated_at)
129
+ VALUES(?, ?, ?, ?)
130
+ ON CONFLICT(name) DO UPDATE SET
131
+ provider=excluded.provider,
132
+ enabled=excluded.enabled,
133
+ updated_at=excluded.updated_at
134
+ """,
135
+ (name, provider, int(enabled), int(updated_at)),
136
+ )
137
+ conn.commit()
138
+ return {"ok": True}
139
+ finally:
140
+ conn.close()
141
+
142
+
143
+ def parse_embedding(text: str) -> List[float]:
144
+ try:
145
+ data = json.loads(text or "[]")
146
+ if not isinstance(data, list):
147
+ return []
148
+ out = []
149
+ for v in data:
150
+ try:
151
+ out.append(float(v))
152
+ except Exception:
153
+ pass
154
+ return out
155
+ except Exception:
156
+ return []
157
+
158
+
159
+ def cosine_similarity(a: List[float], b: List[float]) -> float:
160
+ if not a or not b or len(a) != len(b):
161
+ return -1.0
162
+ dot = 0.0
163
+ aa = 0.0
164
+ bb = 0.0
165
+ for i in range(len(a)):
166
+ av = float(a[i])
167
+ bv = float(b[i])
168
+ dot += av * bv
169
+ aa += av * av
170
+ bb += bv * bv
171
+ if aa <= 0.0 or bb <= 0.0:
172
+ return -1.0
173
+ return dot / (math.sqrt(aa) * math.sqrt(bb))
174
+
175
+
176
+ def add_memory(
177
+ db_path: str, conversation_id: str, source: str, content: str, embedding_json: str, created_at: int
178
+ ) -> Dict[str, Any]:
179
+ conn = connect(db_path)
180
+ try:
181
+ conn.execute(
182
+ "INSERT INTO memories(conversation_id, source, content, embedding_json, created_at) VALUES(?, ?, ?, ?, ?)",
183
+ (conversation_id, source, content, embedding_json or "[]", int(created_at)),
184
+ )
185
+ row_id = conn.execute("SELECT last_insert_rowid() AS id").fetchone()["id"]
186
+ conn.commit()
187
+ return {"ok": True, "id": row_id}
188
+ finally:
189
+ conn.close()
190
+
191
+
192
+ def search_memories(
193
+ db_path: str,
194
+ conversation_id: str,
195
+ query_embedding_json: str,
196
+ limit: int,
197
+ min_score: float,
198
+ window: int,
199
+ ) -> Dict[str, Any]:
200
+ conn = connect(db_path)
201
+ try:
202
+ q_emb = parse_embedding(query_embedding_json)
203
+ if not q_emb:
204
+ return {"ok": True, "rows": []}
205
+
206
+ rows = conn.execute(
207
+ """
208
+ SELECT id, conversation_id, source, content, embedding_json, created_at
209
+ FROM memories
210
+ WHERE (conversation_id = ? OR conversation_id = 'global' OR source = 'self_reflection')
211
+ ORDER BY created_at DESC
212
+ LIMIT ?
213
+ """,
214
+ (conversation_id, int(window)),
215
+ ).fetchall()
216
+
217
+ ranked = []
218
+ for row in rows:
219
+ emb = parse_embedding(row["embedding_json"])
220
+ score = cosine_similarity(q_emb, emb)
221
+ if score > float(min_score):
222
+ ranked.append(
223
+ {
224
+ "id": row["id"],
225
+ "source": row["source"],
226
+ "content": row["content"],
227
+ "created_at": row["created_at"],
228
+ "score": score,
229
+ }
230
+ )
231
+
232
+ ranked.sort(key=lambda r: r["score"], reverse=True)
233
+ return {"ok": True, "rows": ranked[: int(limit)]}
234
+ finally:
235
+ conn.close()
236
+
237
+
238
+ def main() -> int:
239
+ parser = argparse.ArgumentParser()
240
+ parser.add_argument("command", choices=["init", "add", "search", "stats", "upsert-skill"])
241
+ parser.add_argument("--db", required=True)
242
+ parser.add_argument("--vec-ext", default="")
243
+ parser.add_argument("--conversation-id", default="")
244
+ parser.add_argument("--source", default="")
245
+ parser.add_argument("--content", default="")
246
+ parser.add_argument("--embedding-json", default="[]")
247
+ parser.add_argument("--query-embedding-json", default="[]")
248
+ parser.add_argument("--created-at", default="0")
249
+ parser.add_argument("--limit", default="6")
250
+ parser.add_argument("--min-score", default="0.1")
251
+ parser.add_argument("--window", default="600")
252
+ parser.add_argument("--name", default="")
253
+ parser.add_argument("--provider", default="")
254
+ parser.add_argument("--enabled", default="1")
255
+ parser.add_argument("--updated-at", default="0")
256
+ args = parser.parse_args()
257
+
258
+ try:
259
+ if args.command == "init":
260
+ result = init_db(args.db, args.vec_ext)
261
+ elif args.command == "add":
262
+ result = add_memory(
263
+ args.db,
264
+ args.conversation_id,
265
+ args.source,
266
+ args.content,
267
+ args.embedding_json,
268
+ int(args.created_at or "0"),
269
+ )
270
+ elif args.command == "stats":
271
+ result = stats(args.db)
272
+ elif args.command == "upsert-skill":
273
+ result = upsert_skill(
274
+ args.db,
275
+ args.name,
276
+ args.provider or "tool",
277
+ int(args.enabled or "1"),
278
+ int(args.updated_at or "0"),
279
+ )
280
+ else:
281
+ result = search_memories(
282
+ args.db,
283
+ args.conversation_id,
284
+ args.query_embedding_json,
285
+ int(args.limit or "6"),
286
+ float(args.min_score or "0.1"),
287
+ int(args.window or "600"),
288
+ )
289
+ sys.stdout.write(json.dumps(result))
290
+ return 0
291
+ except Exception as err:
292
+ sys.stdout.write(json.dumps({"ok": False, "error": str(err)}))
293
+ return 1
294
+
295
+
296
+ if __name__ == "__main__":
297
+ raise SystemExit(main())