metame-cli 1.3.17 → 1.3.20

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.
@@ -159,6 +159,7 @@ RULES:
159
159
  5. Episodic exceptions: context.anti_patterns (max 5, cross-project lessons only), context.milestones (max 3).
160
160
  6. Strong directives (以后一律/always/never/from now on) → _confidence: high. Otherwise: normal.
161
161
  7. Add _confidence and _source blocks mapping field keys to confidence level and triggering quote.
162
+ 8. NEVER extract agent identity or role definitions. Messages like "你是贾维斯/你的角色是.../you are Jarvis" define the AGENT, not the USER. The profile is about the USER's cognition only.
162
163
 
163
164
  BIAS PREVENTION:
164
165
  - Single observation = STATE, not TRAIT. T3 cognition needs 3+ observations.
@@ -38,7 +38,7 @@ function createBot(config) {
38
38
  * Send a plain text message
39
39
  */
40
40
  async sendMessage(chatId, text) {
41
- await client.im.message.create({
41
+ const res = await client.im.message.create({
42
42
  params: { receive_id_type: 'chat_id' },
43
43
  data: {
44
44
  receive_id: chatId,
@@ -46,20 +46,155 @@ function createBot(config) {
46
46
  content: JSON.stringify({ text }),
47
47
  },
48
48
  });
49
+ // Return Telegram-compatible shape so daemon can edit it later
50
+ const msgId = res?.data?.message_id;
51
+ return msgId ? { message_id: msgId } : null;
52
+ },
53
+
54
+ _editBroken: false, // Set to true if patch API consistently fails
55
+ async editMessage(chatId, messageId, text) {
56
+ if (this._editBroken) return false;
57
+ try {
58
+ await client.im.message.patch({
59
+ path: { message_id: messageId },
60
+ data: { content: JSON.stringify({ text }) },
61
+ });
62
+ return true;
63
+ } catch (e) {
64
+ const code = e?.code || e?.response?.data?.code;
65
+ if (code === 230001 || code === 230002 || /permission|forbidden/i.test(String(e))) {
66
+ this._editBroken = true;
67
+ }
68
+ return false;
69
+ }
49
70
  },
50
71
 
51
72
  /**
52
- * Send markdown (Feishu doesn't support raw markdown sends as text)
73
+ * Send markdown as Feishu interactive card (lark_md renders bold, lists, code, links)
53
74
  */
54
75
  async sendMarkdown(chatId, markdown) {
55
- await client.im.message.create({
76
+ // Convert standard markdown → lark_md compatible format
77
+ let content = markdown
78
+ .replace(/^(#{1,3})\s+(.+)$/gm, '**$2**') // headers → bold
79
+ .replace(/^---+$/gm, '─────────────────────'); // hr → unicode line
80
+
81
+ // Split into chunks if too long (element limit ~4000 chars)
82
+ const MAX_CHUNK = 3800;
83
+ const chunks = [];
84
+ if (content.length <= MAX_CHUNK) {
85
+ chunks.push(content);
86
+ } else {
87
+ const paragraphs = content.split(/\n\n/);
88
+ let buf = '';
89
+ for (const p of paragraphs) {
90
+ if (buf.length + p.length + 2 > MAX_CHUNK && buf) {
91
+ chunks.push(buf);
92
+ buf = p;
93
+ } else {
94
+ buf = buf ? buf + '\n\n' + p : p;
95
+ }
96
+ }
97
+ if (buf) chunks.push(buf);
98
+ }
99
+
100
+ // V2 schema: markdown element with normal text size
101
+ const elements = chunks.map(c => ({
102
+ tag: 'markdown',
103
+ content: c,
104
+ text_size: 'x-large',
105
+ }));
106
+
107
+ const card = {
108
+ schema: '2.0',
109
+ body: { elements },
110
+ };
111
+
112
+ const res = await client.im.message.create({
56
113
  params: { receive_id_type: 'chat_id' },
57
114
  data: {
58
115
  receive_id: chatId,
59
- msg_type: 'text',
60
- content: JSON.stringify({ text: markdown }),
116
+ msg_type: 'interactive',
117
+ content: JSON.stringify(card),
61
118
  },
62
119
  });
120
+ const msgId = res?.data?.message_id;
121
+ return msgId ? { message_id: msgId } : null;
122
+ },
123
+
124
+ /**
125
+ * Send a colored interactive card (for project-tagged notifications)
126
+ * @param {string} chatId
127
+ * @param {string} title - card header text
128
+ * @param {string} body - card body (lark markdown)
129
+ * @param {string} color - header color: blue|orange|green|red|grey|purple|turquoise
130
+ */
131
+ async sendCard(chatId, { title, body, color = 'blue' }) {
132
+ // Use card schema V2 for better text sizing
133
+ if (!body) {
134
+ const card = {
135
+ schema: '2.0',
136
+ header: { title: { tag: 'plain_text', content: title }, template: color },
137
+ body: { elements: [] },
138
+ };
139
+ const res = await client.im.message.create({
140
+ params: { receive_id_type: 'chat_id' },
141
+ data: { receive_id: chatId, msg_type: 'interactive', content: JSON.stringify(card) },
142
+ });
143
+ const msgId = res?.data?.message_id;
144
+ return msgId ? { message_id: msgId } : null;
145
+ }
146
+
147
+ // Convert standard markdown → lark_md
148
+ let content = body
149
+ .replace(/^(#{1,3})\s+(.+)$/gm, '**$2**')
150
+ .replace(/^---+$/gm, '─────────────────────');
151
+
152
+ // Split into chunks (lark_md element limit ~4000 chars)
153
+ const MAX_CHUNK = 3800;
154
+ const chunks = [];
155
+ if (content.length <= MAX_CHUNK) {
156
+ chunks.push(content);
157
+ } else {
158
+ const paragraphs = content.split(/\n\n/);
159
+ let buf = '';
160
+ for (const p of paragraphs) {
161
+ if (buf.length + p.length + 2 > MAX_CHUNK && buf) {
162
+ chunks.push(buf);
163
+ buf = p;
164
+ } else {
165
+ buf = buf ? buf + '\n\n' + p : p;
166
+ }
167
+ }
168
+ if (buf) chunks.push(buf);
169
+ }
170
+
171
+ // V2: use markdown element with text_size for readable font
172
+ const elements = chunks.map(c => ({
173
+ tag: 'markdown',
174
+ content: c,
175
+ text_size: 'x-large',
176
+ }));
177
+
178
+ const card = {
179
+ schema: '2.0',
180
+ header: { title: { tag: 'plain_text', content: title }, template: color },
181
+ body: { elements },
182
+ };
183
+ const res = await client.im.message.create({
184
+ params: { receive_id_type: 'chat_id' },
185
+ data: { receive_id: chatId, msg_type: 'interactive', content: JSON.stringify(card) },
186
+ });
187
+ const msgId = res?.data?.message_id;
188
+ return msgId ? { message_id: msgId } : null;
189
+ },
190
+
191
+ /**
192
+ * Delete a message by ID
193
+ */
194
+ async deleteMessage(chatId, messageId) {
195
+ try {
196
+ await client.im.message.delete({ path: { message_id: messageId } });
197
+ } catch { /* non-fatal — message may already be deleted or expired */ }
63
198
  },
64
199
 
65
200
  /**
@@ -271,6 +406,7 @@ function createBot(config) {
271
406
  if (isDuplicate(msg.message_id)) return;
272
407
 
273
408
  const chatId = msg.chat_id;
409
+ const senderId = data.sender && data.sender.sender_id && data.sender.sender_id.open_id || null;
274
410
  let text = '';
275
411
  let fileInfo = null;
276
412
 
@@ -299,7 +435,7 @@ function createBot(config) {
299
435
 
300
436
  if (text || fileInfo) {
301
437
  // Fire-and-forget: don't block the event loop (SDK needs fast ack)
302
- Promise.resolve().then(() => onMessage(chatId, text, data, fileInfo)).catch(() => {});
438
+ Promise.resolve().then(() => onMessage(chatId, text, data, fileInfo, senderId)).catch(() => {});
303
439
  }
304
440
  } catch (e) {
305
441
  // Non-fatal
package/scripts/schema.js CHANGED
@@ -16,8 +16,7 @@
16
16
  */
17
17
 
18
18
  const SCHEMA = {
19
- // === T1: Identity ===
20
- 'identity.nickname': { tier: 'T1', type: 'string', locked: true },
19
+ // === T1: Identity (USER's identity, not agent's) ===
21
20
  'identity.role': { tier: 'T1', type: 'string', locked: false },
22
21
  'identity.locale': { tier: 'T1', type: 'string', locked: true },
23
22
 
@@ -13,7 +13,6 @@ const path = require('path');
13
13
  const os = require('os');
14
14
 
15
15
  const BUFFER_FILE = path.join(os.homedir(), '.metame', 'raw_signals.jsonl');
16
- const MAX_BUFFER_LINES = 50; // Safety cap to avoid unbounded growth
17
16
 
18
17
  // === CONFIDENCE PATTERNS ===
19
18
 
@@ -66,6 +65,11 @@ process.stdin.on('end', () => {
66
65
  process.exit(0);
67
66
  }
68
67
 
68
+ // Skip agent identity definitions (these belong in project CLAUDE.md, not user profile)
69
+ if (/^(你是|你叫|你的(角色|身份|职责|任务)|你负责|你现在是|from now on you are|you are now|your role is)/i.test(prompt)) {
70
+ process.exit(0);
71
+ }
72
+
69
73
  // Skip pasted error logs / stack traces
70
74
  if (/^(Error|TypeError|SyntaxError|ReferenceError|at\s+\w+|Traceback|FATAL|WARN|ERR!)/i.test(prompt)) {
71
75
  process.exit(0);
@@ -110,11 +114,6 @@ process.stdin.on('end', () => {
110
114
 
111
115
  existingLines.push(JSON.stringify(entry));
112
116
 
113
- // Keep only the most recent entries (drop oldest)
114
- if (existingLines.length > MAX_BUFFER_LINES) {
115
- existingLines = existingLines.slice(-MAX_BUFFER_LINES);
116
- }
117
-
118
117
  fs.writeFileSync(BUFFER_FILE, existingLines.join('\n') + '\n');
119
118
 
120
119
  } catch {