metame-cli 1.3.7 โ†’ 1.3.8

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.
package/README.md CHANGED
@@ -19,7 +19,7 @@ It is not a memory system; it is a **Cognitive Mirror** .
19
19
  ## โœจ Key Features
20
20
 
21
21
  * **๐Ÿง  Global Brain (`~/.claude_profile.yaml`):** A single, portable source of truth โ€” your identity, cognitive traits, and preferences travel with you across every project.
22
- * **๐Ÿงฌ Cognitive Evolution Engine:** MetaMe learns how you think through three channels: (1) **Passive** โ€” silently captures your messages and distills cognitive traits via Haiku on next launch; (2) **Manual** โ€” `!metame evolve` for explicit teaching; (3) **Confidence gates** โ€” strong directives ("always"/"ไปฅๅŽไธ€ๅพ‹") write immediately, normal observations need 3+ consistent sightings before promotion. Schema-enforced (41 fields, 5 tiers, 800 token budget) to prevent bloat.
22
+ * **๐Ÿงฌ Cognitive Evolution Engine:** MetaMe learns how you think through three channels: (1) **Passive** โ€” silently captures your messages and distills cognitive traits via Haiku on next launch; (2) **Manual** โ€” `!metame evolve` for explicit teaching; (3) **Confidence gates** โ€” strong directives ("always"/"from now on") write immediately, normal observations need 3+ consistent sightings before promotion. Schema-enforced (41 fields, 5 tiers, 800 token budget) to prevent bloat.
23
23
  * **๐Ÿ›ก๏ธ Auto-Lock:** Mark any value with `# [LOCKED]` โ€” treated as a constitution, never auto-modified.
24
24
  * **๐Ÿชž Metacognition Layer (v1.3):** MetaMe now observes *how* you think, not just *what* you say. Behavioral pattern detection runs inside the existing Haiku distill call (zero extra cost). It tracks decision patterns, cognitive load, comfort zones, and avoidance topics across sessions. When persistent patterns emerge, MetaMe injects a one-line mirror observation โ€” e.g., *"You tend to avoid testing until forced"* โ€” with a 14-day cooldown per pattern. Conditional reflection prompts appear only when triggered (every 7th distill or 3x consecutive comfort zone). All injection logic runs in Node.js; Claude receives only pre-decided directives, never rules to self-evaluate.
25
25
  * **๐Ÿ“ฑ Remote Claude Code (v1.3):** Full Claude Code from your phone via Telegram or Feishu (Lark). Stateful sessions with `--resume` โ€” same conversation history, tool use, and file editing as your terminal. Interactive buttons for project/session picking, directory browser, and macOS launchd auto-start.
@@ -93,7 +93,7 @@ metame interview
93
93
 
94
94
  MetaMe learns who you are through two paths:
95
95
 
96
- **Automatic (zero effort):** A global hook captures your messages. On next launch, Haiku distills cognitive traits in the background. Strong directives ("always"/"ไปฅๅŽไธ€ๅพ‹") write immediately; normal observations need 3+ consistent sightings. All writes are schema-validated (41 fields, 800 token budget). You'll see:
96
+ **Automatic (zero effort):** A global hook captures your messages. On next launch, Haiku distills cognitive traits in the background. Strong directives ("always"/"from now on") write immediately; normal observations need 3+ consistent sightings. All writes are schema-validated (41 fields, 800 token budget). You'll see:
97
97
 
98
98
  ```
99
99
  ๐Ÿง  MetaMe: Distilling 7 moments in background...
@@ -161,9 +161,9 @@ metame daemon install-launchd # macOS auto-start (RunAtLoad + KeepAlive)
161
161
 
162
162
  | Command | Description |
163
163
  |---------|-------------|
164
- | `/last` | **Quick resume** โ€” ไผ˜ๅ…ˆๅฝ“ๅ‰็›ฎๅฝ•ๆœ€่ฟ‘ session๏ผŒๅฆๅˆ™ๅ…จๅฑ€ๆœ€่ฟ‘ |
164
+ | `/last` | **Quick resume** โ€” prefers current directory's recent session, falls back to global recent |
165
165
  | `/new` | Start new session โ€” pick project directory from button list |
166
- | `/new <name>` | Start new session with a name (e.g., `/new API้‡ๆž„`) |
166
+ | `/new <name>` | Start new session with a name (e.g., `/new API Refactor`) |
167
167
  | `/resume` | Resume a session โ€” clickable list, shows session names + real-time timestamps |
168
168
  | `/resume <name>` | Resume by name (supports partial match, cross-project) |
169
169
  | `/name <name>` | Name the current session (syncs with computer's `/rename`) |
@@ -193,8 +193,8 @@ Each chat gets a persistent session via `claude -p --resume <session-id>`. This
193
193
  **File sending (v1.3.7):** Ask Claude to send any file to your phone:
194
194
 
195
195
  ```
196
- You: ๆŠŠ report.md ๅ‘่ฟ‡ๆฅ
197
- Claude: ่ฏทๆŸฅๆ”ถ~!
196
+ You: Send me report.md
197
+ Claude: Here you go!
198
198
  [๐Ÿ“Ž report.md] โ† tap to download
199
199
  ```
200
200
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.3.7",
3
+ "version": "1.3.8",
4
4
  "description": "The Cognitive Profile Layer for Claude Code. Knows how you think, not just what you said.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  ],
13
13
  "scripts": {
14
14
  "start": "node index.js",
15
- "sync:plugin": "cp scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/daemon.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml plugin/scripts/ && echo 'โœ… Plugin scripts synced'",
15
+ "sync:plugin": "cp scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml plugin/scripts/ && echo 'โœ… Plugin scripts synced'",
16
16
  "precommit": "npm run sync:plugin"
17
17
  },
18
18
  "keywords": [
package/scripts/daemon.js CHANGED
@@ -492,6 +492,36 @@ async function startTelegramBridge(config, executeTaskByName) {
492
492
  continue;
493
493
  }
494
494
 
495
+ // File/document message โ†’ download and pass to Claude
496
+ if (msg.document || msg.photo) {
497
+ const fileId = msg.document ? msg.document.file_id : msg.photo[msg.photo.length - 1].file_id;
498
+ const fileName = msg.document ? msg.document.file_name : `photo_${Date.now()}.jpg`;
499
+ const caption = msg.caption || '';
500
+
501
+ // Save to project's upload/ folder
502
+ const session = getSession(chatId);
503
+ const cwd = session?.cwd || HOME;
504
+ const uploadDir = path.join(cwd, 'upload');
505
+ if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
506
+ const destPath = path.join(uploadDir, fileName);
507
+
508
+ try {
509
+ await bot.downloadFile(fileId, destPath);
510
+ await bot.sendMessage(chatId, `๐Ÿ“ฅ Saved: ${fileName}`);
511
+
512
+ // Build prompt - don't ask Claude to read large files automatically
513
+ const prompt = caption
514
+ ? `User uploaded a file to the project: ${destPath}\nUser says: "${caption}"`
515
+ : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
516
+
517
+ await handleCommand(bot, chatId, prompt, config, executeTaskByName);
518
+ } catch (err) {
519
+ log('ERROR', `File download failed: ${err.message}`);
520
+ await bot.sendMessage(chatId, `โŒ Download failed: ${err.message}`);
521
+ }
522
+ continue;
523
+ }
524
+
495
525
  // Text message (commands or natural language)
496
526
  if (msg.text) {
497
527
  await handleCommand(bot, chatId, msg.text.trim(), config, executeTaskByName);
@@ -802,11 +832,26 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
802
832
  await sendDirPicker(bot, chatId, 'cd', 'Switch workdir:');
803
833
  return;
804
834
  }
805
- // /cd last โ€” jump to the most recent session's directory globally
835
+ // /cd last โ€” sync to computer: switch to most recent session AND its directory
806
836
  if (newCwd === 'last') {
807
- const recent = listRecentSessions(1);
808
- if (recent.length > 0 && recent[0].projectPath) {
809
- newCwd = recent[0].projectPath;
837
+ const currentSession = getSession(chatId);
838
+ const excludeId = currentSession?.id;
839
+ const recent = listRecentSessions(10);
840
+ const filtered = excludeId ? recent.filter(s => s.sessionId !== excludeId) : recent;
841
+ if (filtered.length > 0 && filtered[0].projectPath) {
842
+ const target = filtered[0];
843
+ // Switch to that session (like /resume) AND its directory
844
+ const state2 = loadState();
845
+ state2.sessions[chatId] = {
846
+ id: target.sessionId,
847
+ cwd: target.projectPath,
848
+ started: true,
849
+ };
850
+ saveState(state2);
851
+ const name = target.customTitle || target.summary || '';
852
+ const label = name ? name.slice(0, 40) : target.sessionId.slice(0, 8);
853
+ await bot.sendMessage(chatId, `๐Ÿ”„ Synced to: ${label}\n๐Ÿ“ ${path.basename(target.projectPath)}`);
854
+ return;
810
855
  } else {
811
856
  await bot.sendMessage(chatId, 'No recent session found.');
812
857
  return;
@@ -817,13 +862,27 @@ async function handleCommand(bot, chatId, text, config, executeTaskByName) {
817
862
  return;
818
863
  }
819
864
  const state2 = loadState();
820
- if (!state2.sessions[chatId]) {
865
+ // Try to find existing session in this directory
866
+ const recentInDir = listRecentSessions(1, newCwd);
867
+ if (recentInDir.length > 0 && recentInDir[0].sessionId) {
868
+ // Attach to existing session in this directory
869
+ const target = recentInDir[0];
870
+ state2.sessions[chatId] = {
871
+ id: target.sessionId,
872
+ cwd: newCwd,
873
+ started: true,
874
+ };
875
+ saveState(state2);
876
+ const label = target.customTitle || target.summary?.slice(0, 30) || target.sessionId.slice(0, 8);
877
+ await bot.sendMessage(chatId, `๐Ÿ“ ${path.basename(newCwd)}\n๐Ÿ”„ Attached: ${label}`);
878
+ } else if (!state2.sessions[chatId]) {
821
879
  createSession(chatId, newCwd);
880
+ await bot.sendMessage(chatId, `๐Ÿ“ ${path.basename(newCwd)} (new session)`);
822
881
  } else {
823
882
  state2.sessions[chatId].cwd = newCwd;
824
883
  saveState(state2);
884
+ await bot.sendMessage(chatId, `๐Ÿ“ ${path.basename(newCwd)}`);
825
885
  }
826
- await bot.sendMessage(chatId, `Workdir: ${newCwd}`);
827
886
  return;
828
887
  }
829
888
 
@@ -1518,7 +1577,22 @@ async function askClaude(bot, chatId, prompt) {
1518
1577
 
1519
1578
  let session = getSession(chatId);
1520
1579
  if (!session) {
1521
- session = createSession(chatId);
1580
+ // Auto-attach to most recent Claude session (unified session management)
1581
+ const recent = listRecentSessions(1);
1582
+ if (recent.length > 0 && recent[0].sessionId && recent[0].projectPath) {
1583
+ const target = recent[0];
1584
+ const state = loadState();
1585
+ state.sessions[chatId] = {
1586
+ id: target.sessionId,
1587
+ cwd: target.projectPath,
1588
+ started: true, // Already has history
1589
+ };
1590
+ saveState(state);
1591
+ session = state.sessions[chatId];
1592
+ log('INFO', `Auto-attached ${chatId} to recent session: ${target.sessionId.slice(0, 8)} (${path.basename(target.projectPath)})`);
1593
+ } else {
1594
+ session = createSession(chatId);
1595
+ }
1522
1596
  }
1523
1597
 
1524
1598
  // Build claude command
@@ -1657,15 +1731,45 @@ async function startFeishuBridge(config, executeTaskByName) {
1657
1731
  const allowedIds = config.feishu.allowed_chat_ids || [];
1658
1732
 
1659
1733
  try {
1660
- const receiver = await bot.startReceiving((chatId, text, event) => {
1734
+ const receiver = await bot.startReceiving(async (chatId, text, event, fileInfo) => {
1661
1735
  // Security: check whitelist (empty = allow all)
1662
1736
  if (allowedIds.length > 0 && !allowedIds.includes(chatId)) {
1663
1737
  log('WARN', `Feishu: rejected message from ${chatId}`);
1664
1738
  return;
1665
1739
  }
1666
1740
 
1667
- log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
1668
- handleCommand(bot, chatId, text, config, executeTaskByName);
1741
+ // Handle file message
1742
+ if (fileInfo && fileInfo.fileKey) {
1743
+ log('INFO', `Feishu file from ${chatId}: ${fileInfo.fileName} (key: ${fileInfo.fileKey}, msgId: ${fileInfo.messageId}, type: ${fileInfo.msgType})`);
1744
+ // Save to project's upload/ folder
1745
+ const session = getSession(chatId);
1746
+ const cwd = session?.cwd || HOME;
1747
+ const uploadDir = path.join(cwd, 'upload');
1748
+ if (!fs.existsSync(uploadDir)) fs.mkdirSync(uploadDir, { recursive: true });
1749
+ const destPath = path.join(uploadDir, fileInfo.fileName);
1750
+
1751
+ try {
1752
+ await bot.downloadFile(fileInfo.messageId, fileInfo.fileKey, destPath, fileInfo.msgType);
1753
+ await bot.sendMessage(chatId, `๐Ÿ“ฅ Saved: ${fileInfo.fileName}`);
1754
+
1755
+ // Build prompt - don't ask Claude to read large files automatically
1756
+ const prompt = text
1757
+ ? `User uploaded a file to the project: ${destPath}\nUser says: "${text}"`
1758
+ : `User uploaded a file to the project: ${destPath}\nAcknowledge receipt. Only read the file if the user asks you to.`;
1759
+
1760
+ handleCommand(bot, chatId, prompt, config, executeTaskByName);
1761
+ } catch (err) {
1762
+ log('ERROR', `Feishu file download failed: ${err.message}`);
1763
+ await bot.sendMessage(chatId, `โŒ Download failed: ${err.message}`);
1764
+ }
1765
+ return;
1766
+ }
1767
+
1768
+ // Handle text message
1769
+ if (text) {
1770
+ log('INFO', `Feishu message from ${chatId}: ${text.slice(0, 50)}`);
1771
+ handleCommand(bot, chatId, text, config, executeTaskByName);
1772
+ }
1669
1773
  });
1670
1774
 
1671
1775
  log('INFO', 'Feishu bot connected (WebSocket long connection)');
@@ -111,6 +111,55 @@ function createBot(config) {
111
111
  return { app_id, app_name: 'MetaMe' };
112
112
  },
113
113
 
114
+ /**
115
+ * Download a file from Feishu to local disk
116
+ * @param {string} messageId - Message ID containing the file
117
+ * @param {string} fileKey - File key from message content
118
+ * @param {string} destPath - Local destination path
119
+ * @returns {Promise<string>} The destination path
120
+ */
121
+ async downloadFile(messageId, fileKey, destPath, msgType = 'file') {
122
+ try {
123
+ let res;
124
+ if (msgType === 'image') {
125
+ // Images use im.image.get API
126
+ res = await client.im.image.get({
127
+ path: { image_key: fileKey },
128
+ });
129
+ } else {
130
+ // Files and media use im.messageResource.get API
131
+ res = await client.im.messageResource.get({
132
+ path: { message_id: messageId, file_key: fileKey },
133
+ params: { type: 'file' },
134
+ });
135
+ }
136
+
137
+ // SDK returns writeFile method or getReadableStream
138
+ if (res && res.writeFile) {
139
+ await res.writeFile(destPath);
140
+ return destPath;
141
+ } else if (res && res.getReadableStream) {
142
+ const stream = res.getReadableStream();
143
+ const fileStream = fs.createWriteStream(destPath);
144
+ return new Promise((resolve, reject) => {
145
+ stream.pipe(fileStream);
146
+ fileStream.on('finish', () => {
147
+ fileStream.close();
148
+ resolve(destPath);
149
+ });
150
+ fileStream.on('error', (err) => {
151
+ fs.unlink(destPath, () => {});
152
+ reject(err);
153
+ });
154
+ });
155
+ }
156
+ throw new Error('No writeFile or stream in response');
157
+ } catch (err) {
158
+ const detail = err.message || String(err);
159
+ throw new Error(detail);
160
+ }
161
+ },
162
+
114
163
  /**
115
164
  * Send a file/document
116
165
  * @param {string} chatId
@@ -228,6 +277,7 @@ function createBot(config) {
228
277
 
229
278
  const chatId = msg.chat_id;
230
279
  let text = '';
280
+ let fileInfo = null;
231
281
 
232
282
  if (msg.message_type === 'text') {
233
283
  try {
@@ -236,14 +286,25 @@ function createBot(config) {
236
286
  } catch {
237
287
  text = msg.content || '';
238
288
  }
289
+ } else if (msg.message_type === 'file' || msg.message_type === 'image' || msg.message_type === 'media') {
290
+ // File, image or media (video) message
291
+ try {
292
+ const content = JSON.parse(msg.content);
293
+ fileInfo = {
294
+ messageId: msg.message_id,
295
+ fileKey: content.file_key || content.image_key,
296
+ fileName: content.file_name || content.image_key || `file_${Date.now()}`,
297
+ msgType: msg.message_type, // 'file', 'image', or 'media'
298
+ };
299
+ } catch {}
239
300
  }
240
301
 
241
302
  // Strip @mention prefix if present
242
303
  text = text.replace(/@_user_\d+\s*/g, '').trim();
243
304
 
244
- if (text) {
305
+ if (text || fileInfo) {
245
306
  // Fire-and-forget: don't block the event loop (SDK needs fast ack)
246
- Promise.resolve().then(() => onMessage(chatId, text, data)).catch(() => {});
307
+ Promise.resolve().then(() => onMessage(chatId, text, data, fileInfo)).catch(() => {});
247
308
  }
248
309
  } catch (e) {
249
310
  // Non-fatal
@@ -171,6 +171,46 @@ function createBot(token) {
171
171
  });
172
172
  },
173
173
 
174
+ /**
175
+ * Download a file from Telegram to local disk
176
+ * @param {string} fileId - Telegram file_id
177
+ * @param {string} destPath - Local destination path
178
+ * @returns {Promise<string>} The destination path
179
+ */
180
+ async downloadFile(fileId, destPath) {
181
+ // 1. Get file path from Telegram
182
+ const fileInfo = await apiRequest(token, 'getFile', { file_id: fileId });
183
+ if (!fileInfo.file_path) {
184
+ throw new Error('Failed to get file path from Telegram');
185
+ }
186
+
187
+ // 2. Download file using stream (zero memory overhead)
188
+ const fileUrl = `${API_BASE}/file/bot${token}/${fileInfo.file_path}`;
189
+ return new Promise((resolve, reject) => {
190
+ const urlObj = new URL(fileUrl);
191
+ https.get({
192
+ hostname: urlObj.hostname,
193
+ path: urlObj.pathname,
194
+ timeout: 60000,
195
+ }, (res) => {
196
+ if (res.statusCode !== 200) {
197
+ reject(new Error(`Download failed: ${res.statusCode}`));
198
+ return;
199
+ }
200
+ const fileStream = fs.createWriteStream(destPath);
201
+ res.pipe(fileStream);
202
+ fileStream.on('finish', () => {
203
+ fileStream.close();
204
+ resolve(destPath);
205
+ });
206
+ fileStream.on('error', (err) => {
207
+ fs.unlink(destPath, () => {});
208
+ reject(err);
209
+ });
210
+ }).on('error', reject);
211
+ });
212
+ },
213
+
174
214
  /**
175
215
  * Send a file/document
176
216
  * @param {number|string} chatId - Target chat ID