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 +6 -6
- package/package.json +2 -2
- package/scripts/daemon.js +114 -10
- package/scripts/feishu-adapter.js +63 -2
- package/scripts/telegram-adapter.js +40 -0
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"/"
|
|
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"/"
|
|
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** โ
|
|
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:
|
|
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.
|
|
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 โ
|
|
835
|
+
// /cd last โ sync to computer: switch to most recent session AND its directory
|
|
806
836
|
if (newCwd === 'last') {
|
|
807
|
-
const
|
|
808
|
-
|
|
809
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1668
|
-
|
|
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
|