metame-cli 1.5.4 → 1.5.5

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.
Files changed (40) hide show
  1. package/README.md +6 -1
  2. package/index.js +277 -55
  3. package/package.json +2 -2
  4. package/scripts/agent-layer.js +4 -2
  5. package/scripts/bin/dispatch_to +17 -5
  6. package/scripts/daemon-admin-commands.js +264 -62
  7. package/scripts/daemon-agent-commands.js +188 -66
  8. package/scripts/daemon-bridges.js +447 -48
  9. package/scripts/daemon-claude-engine.js +650 -103
  10. package/scripts/daemon-command-router.js +134 -27
  11. package/scripts/daemon-command-session-route.js +118 -0
  12. package/scripts/daemon-default.yaml +2 -0
  13. package/scripts/daemon-engine-runtime.js +96 -20
  14. package/scripts/daemon-exec-commands.js +106 -50
  15. package/scripts/daemon-file-browser.js +63 -7
  16. package/scripts/daemon-notify.js +18 -4
  17. package/scripts/daemon-ops-commands.js +16 -2
  18. package/scripts/daemon-remote-dispatch.js +34 -2
  19. package/scripts/daemon-session-commands.js +102 -45
  20. package/scripts/daemon-session-store.js +497 -66
  21. package/scripts/daemon-siri-bridge.js +234 -0
  22. package/scripts/daemon-siri-imessage.js +209 -0
  23. package/scripts/daemon-task-scheduler.js +10 -2
  24. package/scripts/daemon.js +610 -181
  25. package/scripts/docs/hook-config.md +7 -4
  26. package/scripts/docs/maintenance-manual.md +8 -1
  27. package/scripts/feishu-adapter.js +7 -15
  28. package/scripts/hooks/doc-router.js +29 -0
  29. package/scripts/hooks/intent-doc-router.js +54 -0
  30. package/scripts/hooks/intent-engine.js +9 -40
  31. package/scripts/intent-registry.js +59 -0
  32. package/scripts/memory-extract.js +59 -0
  33. package/scripts/mentor-engine.js +6 -0
  34. package/scripts/schema.js +1 -0
  35. package/scripts/self-reflect.js +110 -12
  36. package/scripts/session-analytics.js +160 -0
  37. package/scripts/signal-capture.js +1 -1
  38. package/scripts/team-dispatch.js +150 -11
  39. package/scripts/hooks/intent-agent-manage.js +0 -50
  40. package/scripts/hooks/intent-hook-config.js +0 -28
@@ -0,0 +1,234 @@
1
+ 'use strict';
2
+ /**
3
+ * daemon-siri-bridge.js — HTTP bridge for Siri Shortcuts integration
4
+ *
5
+ * Exposes GET/POST /ask endpoint and returns plain text.
6
+ * Processes through the same Claude pipeline as Telegram/Feishu/iMessage.
7
+ * Designed for iOS Shortcuts: Dictate → HTTP GET/POST → Speak Text.
8
+ */
9
+
10
+ const http = require('http');
11
+ const querystring = require('querystring');
12
+
13
+ function createSiriBridge(deps) {
14
+ const { log, loadConfig, handleCommand } = deps;
15
+
16
+ function writeText(res, statusCode, text, extraHeaders = {}) {
17
+ const body = String(text || '');
18
+ res.writeHead(statusCode, {
19
+ 'Content-Type': 'text/plain; charset=utf-8',
20
+ 'Content-Length': Buffer.byteLength(body),
21
+ 'Cache-Control': 'no-store, no-transform',
22
+ 'X-Content-Type-Options': 'nosniff',
23
+ ...extraHeaders,
24
+ });
25
+ res.end(body);
26
+ }
27
+
28
+ function writeJson(res, statusCode, payload, extraHeaders = {}) {
29
+ const body = JSON.stringify(payload);
30
+ res.writeHead(statusCode, {
31
+ 'Content-Type': 'application/json; charset=utf-8',
32
+ 'Content-Length': Buffer.byteLength(body),
33
+ 'Cache-Control': 'no-store, no-transform',
34
+ 'X-Content-Type-Options': 'nosniff',
35
+ ...extraHeaders,
36
+ });
37
+ res.end(body);
38
+ }
39
+
40
+ function normalizePlainText(text) {
41
+ return String(text || '').replace(/\r\n/g, '\n').trim();
42
+ }
43
+
44
+ function getAuthToken(req, urlObj) {
45
+ const auth = String(req.headers.authorization || '').replace(/^Bearer\s+/i, '').trim();
46
+ if (auth) return auth;
47
+ return String(urlObj.searchParams.get('token') || req.headers['x-api-key'] || '').trim();
48
+ }
49
+
50
+ async function readRequestBody(req) {
51
+ const chunks = [];
52
+ for await (const chunk of req) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
53
+ return Buffer.concat(chunks).toString('utf8');
54
+ }
55
+
56
+ function extractAskText(req, urlObj, rawBody) {
57
+ const queryText = normalizePlainText(urlObj.searchParams.get('q') || urlObj.searchParams.get('text') || '');
58
+ if (queryText) return queryText;
59
+
60
+ const body = String(rawBody || '');
61
+ if (!body.trim()) return '';
62
+
63
+ const contentType = String(req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
64
+ if (contentType === 'application/json') {
65
+ try {
66
+ const parsed = JSON.parse(body);
67
+ return normalizePlainText(
68
+ parsed && typeof parsed === 'object'
69
+ ? (parsed.text || parsed.q || parsed.prompt || '')
70
+ : ''
71
+ );
72
+ } catch {
73
+ return '';
74
+ }
75
+ }
76
+
77
+ if (contentType === 'application/x-www-form-urlencoded') {
78
+ const parsed = querystring.parse(body);
79
+ return normalizePlainText(parsed.text || parsed.q || parsed.prompt || '');
80
+ }
81
+
82
+ return normalizePlainText(body);
83
+ }
84
+
85
+ /**
86
+ * Collector bot — captures Claude's response instead of sending to a chat.
87
+ * handleCommand calls bot.sendMessage / sendMarkdown / editMessage as it
88
+ * streams; we collect everything and return the final text.
89
+ */
90
+ function createCollectorBot() {
91
+ const messages = new Map();
92
+ let nextId = 1;
93
+
94
+ const bot = {
95
+ suppressAck: true,
96
+ sendMessage: async (_chatId, text) => {
97
+ const id = nextId++;
98
+ messages.set(id, String(text || ''));
99
+ return { message_id: id };
100
+ },
101
+ sendMarkdown: async (_chatId, text) => {
102
+ const id = nextId++;
103
+ messages.set(id, String(text || '').replace(/[*_`~#>]/g, '').trim());
104
+ return { message_id: id };
105
+ },
106
+ editMessage: async (_chatId, msgId, text) => {
107
+ const plain = String(text || '').replace(/[*_`~#>]/g, '').trim();
108
+ messages.set(msgId, plain);
109
+ return true;
110
+ },
111
+ deleteMessage: async () => false,
112
+ sendTyping: async () => {},
113
+ getResult: () => {
114
+ let last = '';
115
+ for (const [, text] of messages) {
116
+ if (text && text.trim()) last = text.trim();
117
+ }
118
+ return last;
119
+ },
120
+ };
121
+ return bot;
122
+ }
123
+
124
+ function startSiriBridge(config, executeTaskByName) {
125
+ const cfg = config.siri_bridge || {};
126
+ if (!cfg.enabled) return null;
127
+
128
+ const port = cfg.port || 8200;
129
+ const token = cfg.token || '';
130
+ const chatId = cfg.chat_id || '_siri_';
131
+ const timeoutMs = cfg.timeout_ms || 120000;
132
+ const maxReplyLen = cfg.max_reply_length || 0; // 0 = unlimited
133
+
134
+ if (!token) {
135
+ log('WARN', '[SIRI] siri_bridge.token not configured — bridge disabled');
136
+ return null;
137
+ }
138
+
139
+ const server = http.createServer(async (req, res) => {
140
+ log('DEBUG', `[SIRI] ${req.method} ${req.url} from ${req.socket.remoteAddress}`);
141
+ const urlObj = new URL(req.url, `http://${req.headers.host || 'localhost'}`);
142
+
143
+ // CORS preflight
144
+ if (req.method === 'OPTIONS') {
145
+ res.writeHead(204, {
146
+ 'Access-Control-Allow-Origin': '*',
147
+ 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
148
+ 'Access-Control-Allow-Headers': 'Content-Type, Authorization, X-API-Key',
149
+ 'Access-Control-Max-Age': '86400',
150
+ });
151
+ res.end();
152
+ return;
153
+ }
154
+
155
+ // Health check
156
+ if (req.method === 'GET' && urlObj.pathname === '/health') {
157
+ writeJson(res, 200, { status: 'ok' });
158
+ return;
159
+ }
160
+
161
+ // Quick echo test — GET /echo?t=xxx returns plain text immediately
162
+ if (req.method === 'GET' && urlObj.pathname === '/echo') {
163
+ const t = normalizePlainText(urlObj.searchParams.get('t') || 'hello from jarvis');
164
+ writeText(res, 200, t);
165
+ return;
166
+ }
167
+
168
+ // /ask supports GET for iOS Shortcuts and POST for compatibility.
169
+ if (!['GET', 'POST'].includes(req.method) || urlObj.pathname !== '/ask') {
170
+ writeText(res, 404, 'Not Found');
171
+ return;
172
+ }
173
+
174
+ // Auth
175
+ const auth = getAuthToken(req, urlObj);
176
+ if (auth !== token) {
177
+ writeText(res, 401, 'Unauthorized');
178
+ return;
179
+ }
180
+
181
+ const rawBody = req.method === 'POST' ? await readRequestBody(req) : '';
182
+ const text = extractAskText(req, urlObj, rawBody);
183
+ if (!text) {
184
+ writeText(res, 400, 'Missing q/text');
185
+ return;
186
+ }
187
+
188
+ log('INFO', `[SIRI] Received: "${text.slice(0, 80)}"`);
189
+
190
+ // Timeout guard
191
+ let timedOut = false;
192
+ const timer = setTimeout(() => {
193
+ timedOut = true;
194
+ writeText(res, 504, 'timeout');
195
+ }, timeoutMs);
196
+
197
+ try {
198
+ const bot = createCollectorBot();
199
+ const liveCfg = loadConfig();
200
+ await handleCommand(bot, chatId, text, liveCfg, executeTaskByName, null, true);
201
+
202
+ if (timedOut) return;
203
+ clearTimeout(timer);
204
+
205
+ let reply = bot.getResult() || '(no response)';
206
+ if (maxReplyLen && reply.length > maxReplyLen) {
207
+ reply = reply.slice(0, maxReplyLen) + '...';
208
+ }
209
+
210
+ log('INFO', `[SIRI] Reply: "${reply.slice(0, 80)}"`);
211
+ writeText(res, 200, reply);
212
+ } catch (err) {
213
+ if (timedOut) return;
214
+ clearTimeout(timer);
215
+ log('ERROR', `[SIRI] Error: ${err.message}`);
216
+ writeText(res, 500, err.message);
217
+ }
218
+ });
219
+
220
+ server.listen(port, '0.0.0.0', () => {
221
+ log('INFO', `[SIRI] HTTP bridge listening on 0.0.0.0:${port}`);
222
+ });
223
+
224
+ server.on('error', (err) => {
225
+ log('ERROR', `[SIRI] Server error: ${err.message}`);
226
+ });
227
+
228
+ return { stop: () => server.close() };
229
+ }
230
+
231
+ return { startSiriBridge };
232
+ }
233
+
234
+ module.exports = { createSiriBridge };
@@ -0,0 +1,209 @@
1
+ 'use strict';
2
+ /**
3
+ * daemon-siri-imessage.js — iMessage I/O layer
4
+ *
5
+ * 底层函数:轮询 chat.db + AppleScript 发送。
6
+ * 上层 Bridge 逻辑在 daemon-bridges.js 的 startImessageBridge()。
7
+ */
8
+
9
+ const fs = require('fs');
10
+ const os = require('os');
11
+ const path = require('path');
12
+ const { execFile, execFileSync } = require('child_process');
13
+
14
+ const CHAT_DB = path.join(os.homedir(), 'Library', 'Messages', 'chat.db');
15
+
16
+ // macOS Ventura+: text may be stored in attributedBody blob, not text field.
17
+ // Returns TSV rows:
18
+ // rowid\ttext\tsender\tchat_guid\tchat_identifier\tchat_name
19
+ //
20
+ // Key filters:
21
+ // 1. is_from_me=0 in SQL WHERE clause is the primary echo guard.
22
+ // 2. local_guids filter REMOVED: in self-chat (note-to-self), all incoming messages
23
+ // share the Mac's account_guid, so the old filter blocked everything.
24
+ // Echo prevention is now handled by content fingerprint in createImessageBot.
25
+ // 3. Drop reactions/system/service messages so tapbacks never become prompts.
26
+ const PYTHON_QUERY = `
27
+ import sqlite3,os,re,sys
28
+ db=os.path.expanduser('~/Library/Messages/chat.db')
29
+ con=sqlite3.connect(db)
30
+ rows=con.execute("""
31
+ SELECT
32
+ m.rowid,
33
+ m.text,
34
+ m.attributedBody,
35
+ h.id,
36
+ c.guid,
37
+ c.chat_identifier,
38
+ coalesce(c.display_name, c.room_name, c.chat_identifier, c.guid, ''),
39
+ coalesce(m.account_guid, ''),
40
+ coalesce(m.associated_message_guid, ''),
41
+ coalesce(m.associated_message_type, 0),
42
+ coalesce(m.associated_message_emoji, ''),
43
+ coalesce(m.is_system_message, 0),
44
+ coalesce(m.is_service_message, 0),
45
+ coalesce(m.item_type, 0)
46
+ FROM message m
47
+ LEFT JOIN handle h ON m.handle_id=h.rowid
48
+ LEFT JOIN chat_message_join cmj ON cmj.message_id=m.rowid
49
+ LEFT JOIN chat c ON c.rowid=cmj.chat_id
50
+ WHERE m.rowid > ? AND m.is_from_me = 0
51
+ ORDER BY m.rowid ASC LIMIT 20
52
+ """,(int(sys.argv[1]),)).fetchall()
53
+ seen=set()
54
+ for rowid,text,body,sender,chat_guid,chat_identifier,chat_name,account_guid,assoc_guid,assoc_type,assoc_emoji,is_system,is_service,item_type in rows:
55
+ if assoc_guid or assoc_type or assoc_emoji or is_system or is_service or item_type:
56
+ continue
57
+ if not text and body:
58
+ decoded=body.decode('utf-8','ignore')
59
+ m=re.search(r'[\\x20-\\x7e\\u4e00-\\u9fff\\u3000-\\u303f\\uff00-\\uffef]{2,}',decoded)
60
+ if m: text=m.group(0).strip()
61
+ if text and text.strip():
62
+ key=(rowid, chat_guid or chat_identifier or sender or '')
63
+ if key in seen:
64
+ continue
65
+ seen.add(key)
66
+ print(
67
+ str(rowid)+'\\t'
68
+ +str(text).replace('\\n',' ')+'\\t'
69
+ +(sender or '')+'\\t'
70
+ +(chat_guid or '')+'\\t'
71
+ +(chat_identifier or '')+'\\t'
72
+ +str(chat_name or '').replace('\\n',' ')
73
+ )
74
+ `.trim();
75
+
76
+ function isAvailable() {
77
+ return fs.existsSync(CHAT_DB);
78
+ }
79
+
80
+ function getMaxRowId() {
81
+ try {
82
+ return parseInt(
83
+ execFileSync('sqlite3', [CHAT_DB, 'SELECT MAX(rowid) FROM message;'],
84
+ { encoding: 'utf8', timeout: 5000 }).trim(), 10
85
+ ) || 0;
86
+ } catch { return 0; }
87
+ }
88
+
89
+ function queryNewMessages(lastRowId) {
90
+ try {
91
+ return execFileSync('python3', ['-c', PYTHON_QUERY, String(lastRowId)],
92
+ { encoding: 'utf8', timeout: 8000 }).trim();
93
+ } catch { return ''; }
94
+ }
95
+
96
+ function escapeAppleScriptString(input) {
97
+ return String(input || '')
98
+ .replace(/\\/g, '\\\\')
99
+ .replace(/"/g, '\\"')
100
+ .replace(/\n/g, '\\n');
101
+ }
102
+
103
+ // Send iMessage via AppleScript; targetChatId should be chat.id / chat.guid.
104
+ function sendImessage(targetChatId, text) {
105
+ return new Promise((resolve) => {
106
+ const safe = escapeAppleScriptString(text);
107
+ const safeChatId = escapeAppleScriptString(targetChatId);
108
+ const script = [
109
+ 'tell application "Messages"',
110
+ ` send "${safe}" to chat id "${safeChatId}"`,
111
+ 'end tell',
112
+ ].join('\n');
113
+ execFile('osascript', ['-e', script], { timeout: 15000 }, (err) => resolve(!err));
114
+ });
115
+ }
116
+
117
+ // Create a bot object compatible with handleCommand / daemon-claude-engine.
118
+ //
119
+ // Design goals for iMessage:
120
+ // 1. No intermediate status spam — buffer all sends, flush only the final text.
121
+ // 2. Prevent echo loop — track recently-sent content fingerprints.
122
+ // 3. editMessage returns true (pretend success) so engine doesn't double-send.
123
+ function createImessageBot(targetChatId, log) {
124
+ let flushTimer = null;
125
+ let pendingText = '';
126
+ // onAfterSend: bridge injects this to advance lastRowId after we send
127
+ let _onAfterSend = null;
128
+
129
+ // Echo fingerprint ring — stores { text, ts } of recently sent messages.
130
+ // Used by bridge to detect and skip echoed incoming messages.
131
+ const sentFingerprints = [];
132
+ const FINGERPRINT_TTL = 30000; // 30s window
133
+
134
+ const addFingerprint = (text) => {
135
+ const now = Date.now();
136
+ sentFingerprints.push({ text: text.trim().toLowerCase().replace(/\s+/g, ' '), ts: now });
137
+ // Prune expired entries
138
+ while (sentFingerprints.length && sentFingerprints[0].ts < now - FINGERPRINT_TTL) {
139
+ sentFingerprints.shift();
140
+ }
141
+ };
142
+
143
+ const isEcho = (text) => {
144
+ const now = Date.now();
145
+ const needle = text.trim().toLowerCase().replace(/\s+/g, ' ');
146
+ for (let i = sentFingerprints.length - 1; i >= 0; i--) {
147
+ const fp = sentFingerprints[i];
148
+ if (fp.ts < now - FINGERPRINT_TTL) break;
149
+ // Prefix match: echo may have trailing whitespace or truncation differences
150
+ if (fp.text === needle || needle.startsWith(fp.text.slice(0, 20)) || fp.text.startsWith(needle.slice(0, 20))) return true;
151
+ }
152
+ return false;
153
+ };
154
+
155
+ const scheduleFlush = (text) => {
156
+ if (flushTimer) clearTimeout(flushTimer);
157
+ pendingText = text;
158
+ flushTimer = setTimeout(async () => {
159
+ flushTimer = null;
160
+ const final = pendingText;
161
+ pendingText = '';
162
+ if (!final) return;
163
+ const tagged = `🤖 ${final}`;
164
+ addFingerprint(tagged);
165
+ const ok = await sendImessage(targetChatId, tagged);
166
+ if (!ok && log) log('WARN', `[IMESSAGE] send failed to ${targetChatId}`);
167
+ // After sending, advance lastRowId so the echo is skipped
168
+ if (_onAfterSend) _onAfterSend();
169
+ }, 3000); // wait 3s for streaming to settle before sending
170
+ };
171
+
172
+ // Record fingerprint IMMEDIATELY on send/edit calls — not in flush timer.
173
+ // This ensures the echo is recognized even if poll runs before flush fires.
174
+ const sendAndFingerprint = (text) => {
175
+ if (text) {
176
+ addFingerprint(text);
177
+ scheduleFlush(text);
178
+ }
179
+ };
180
+
181
+ const bot = {
182
+ suppressAck: true,
183
+ isEcho,
184
+ sendMessage: (_chatId, text) => {
185
+ const plain = String(text || '').replace(/[*_`~]/g, '').trim();
186
+ sendAndFingerprint(plain);
187
+ return Promise.resolve({ message_id: Date.now() });
188
+ },
189
+ sendMarkdown: (_chatId, text) => {
190
+ const plain = String(text || '').replace(/[*_`~#>]/g, '').trim();
191
+ sendAndFingerprint(plain);
192
+ return Promise.resolve({ message_id: Date.now() });
193
+ },
194
+ // editMessage: update buffered text and extend flush timer — return true so
195
+ // engine doesn't fall through to a redundant sendMessage call.
196
+ editMessage: (_chatId, _msgId, text) => {
197
+ const plain = String(text || '').replace(/[*_`~]/g, '').trim();
198
+ sendAndFingerprint(plain);
199
+ return Promise.resolve(true);
200
+ },
201
+ deleteMessage: async () => false,
202
+ sendTyping: async () => {},
203
+ // Bridge injects this after bot is created
204
+ setOnAfterSend: (fn) => { _onAfterSend = fn; },
205
+ };
206
+ return bot;
207
+ }
208
+
209
+ module.exports = { isAvailable, getMaxRowId, queryNewMessages, createImessageBot };
@@ -2,7 +2,6 @@
2
2
 
3
3
  const crypto = require('crypto');
4
4
  const { classifyTaskUsage } = require('./usage-classifier');
5
- const { IS_WIN } = require('./platform');
6
5
 
7
6
  const WEEKDAY_INDEX = Object.freeze({
8
7
  sun: 0,
@@ -196,6 +195,7 @@ function createTaskScheduler(deps) {
196
195
  isInSleepMode,
197
196
  setSleepMode,
198
197
  spawnSessionSummaries,
198
+ getWakeRecoveryHook,
199
199
  skillEvolution,
200
200
  } = deps;
201
201
 
@@ -746,7 +746,15 @@ function createTaskScheduler(deps) {
746
746
  st.wake_restart = new Date().toISOString();
747
747
  st.wake_sleep_seconds = Math.round(tickElapsed / 1000);
748
748
  saveState(st);
749
- // Don't exit Feishu and Telegram have built-in auto-reconnect
749
+ const onWakeDetected = typeof getWakeRecoveryHook === 'function' ? getWakeRecoveryHook() : null;
750
+ if (typeof onWakeDetected === 'function') {
751
+ Promise.resolve(onWakeDetected({
752
+ sleepSeconds: Math.round(tickElapsed / 1000),
753
+ resumedAt: new Date(tickNow).toISOString(),
754
+ })).catch((e) => {
755
+ log('WARN', `[WAKE-DETECT] bridge recovery failed: ${e.message}`);
756
+ });
757
+ }
750
758
  }
751
759
 
752
760
  // ① Physiological heartbeat (zero token, pure awareness)