metame-cli 1.4.17 → 1.4.18
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 +9 -6
- package/index.js +12 -5
- package/package.json +2 -2
- package/scripts/check-macos-control-capabilities.sh +77 -0
- package/scripts/daemon-admin-commands.js +350 -12
- package/scripts/daemon-admin-commands.test.js +333 -0
- package/scripts/daemon-claude-engine.js +57 -10
- package/scripts/daemon-command-router.js +241 -3
- package/scripts/daemon-default.yaml +10 -3
- package/scripts/daemon-exec-commands.js +248 -13
- package/scripts/daemon-task-envelope.js +143 -0
- package/scripts/daemon-task-envelope.test.js +59 -0
- package/scripts/daemon-task-scheduler.js +213 -24
- package/scripts/daemon-task-scheduler.test.js +106 -0
- package/scripts/daemon.js +371 -26
- package/scripts/distill.js +184 -34
- package/scripts/memory-extract.js +13 -5
- package/scripts/memory.js +239 -60
- package/scripts/providers.js +1 -1
- package/scripts/reliability-core.test.js +268 -0
- package/scripts/session-analytics.js +123 -35
- package/scripts/signal-capture.js +171 -11
- package/scripts/skill-evolution.js +158 -19
- package/scripts/task-board.js +398 -0
- package/scripts/task-board.test.js +83 -0
- package/scripts/usage-classifier.js +139 -0
- package/scripts/utils.js +107 -0
- package/scripts/utils.test.js +61 -1
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { describe, it } = require('node:test');
|
|
4
|
+
const assert = require('node:assert/strict');
|
|
5
|
+
const { createAdminCommandHandler } = require('./daemon-admin-commands');
|
|
6
|
+
const taskEnvelope = require('./daemon-task-envelope');
|
|
7
|
+
|
|
8
|
+
function createHandler(getAllTasksImpl, overrides = {}) {
|
|
9
|
+
return createAdminCommandHandler({
|
|
10
|
+
fs: require('fs'),
|
|
11
|
+
yaml: { load: () => ({}), dump: () => '' },
|
|
12
|
+
execSync: () => '',
|
|
13
|
+
BRAIN_FILE: '/tmp/brain.yaml',
|
|
14
|
+
CONFIG_FILE: '/tmp/config.yaml',
|
|
15
|
+
DISPATCH_LOG: '/tmp/dispatch.log',
|
|
16
|
+
providerMod: null,
|
|
17
|
+
loadConfig: () => ({}),
|
|
18
|
+
backupConfig: () => {},
|
|
19
|
+
writeConfigSafe: () => {},
|
|
20
|
+
restoreConfig: () => false,
|
|
21
|
+
getSession: () => null,
|
|
22
|
+
getAllTasks: getAllTasksImpl,
|
|
23
|
+
dispatchTask: () => ({ success: true }),
|
|
24
|
+
log: () => {},
|
|
25
|
+
skillEvolution: null,
|
|
26
|
+
taskBoard: null,
|
|
27
|
+
taskEnvelope: null,
|
|
28
|
+
...overrides,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('daemon-admin-commands /tasks', () => {
|
|
33
|
+
it('renders interval and fixed-time schedules for mobile task list', async () => {
|
|
34
|
+
const sent = [];
|
|
35
|
+
const { handleAdminCommand } = createHandler(() => ({
|
|
36
|
+
general: [
|
|
37
|
+
{ name: 'memory-extract', interval: '4h', enabled: true },
|
|
38
|
+
],
|
|
39
|
+
project: [
|
|
40
|
+
{
|
|
41
|
+
name: 'morning-brief',
|
|
42
|
+
at: '09:00',
|
|
43
|
+
days: 'weekdays',
|
|
44
|
+
enabled: true,
|
|
45
|
+
_project: { key: 'writer', icon: '✍️', name: 'Writer' },
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
}));
|
|
49
|
+
|
|
50
|
+
const bot = {
|
|
51
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const res = await handleAdminCommand({
|
|
55
|
+
bot,
|
|
56
|
+
chatId: 'mobile-user-1',
|
|
57
|
+
text: '/tasks',
|
|
58
|
+
config: {},
|
|
59
|
+
state: {
|
|
60
|
+
tasks: {
|
|
61
|
+
'memory-extract': { status: 'success' },
|
|
62
|
+
'morning-brief': { status: 'never_run' },
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
assert.equal(res.handled, true);
|
|
68
|
+
assert.equal(sent.length, 1);
|
|
69
|
+
const body = sent[0];
|
|
70
|
+
assert.match(body, /memory-extract \(every 4h\) success/);
|
|
71
|
+
assert.match(body, /morning-brief \(at 09:00 weekdays\) never_run/);
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe('daemon-admin-commands /TeamTask', () => {
|
|
76
|
+
it('creates team task via /TeamTask create', async () => {
|
|
77
|
+
const sent = [];
|
|
78
|
+
const dispatchCalls = [];
|
|
79
|
+
const { handleAdminCommand } = createHandler(
|
|
80
|
+
() => ({ general: [], project: [] }),
|
|
81
|
+
{
|
|
82
|
+
taskEnvelope,
|
|
83
|
+
taskBoard: {
|
|
84
|
+
listScopeParticipants: () => ['planner'],
|
|
85
|
+
},
|
|
86
|
+
dispatchTask: (target, packet) => {
|
|
87
|
+
dispatchCalls.push({ target, packet });
|
|
88
|
+
return { success: true };
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
const bot = {
|
|
94
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const res = await handleAdminCommand({
|
|
98
|
+
bot,
|
|
99
|
+
chatId: 'mobile-user-2',
|
|
100
|
+
text: '/TeamTask create coder 重构登录流程 --scope epic_auth',
|
|
101
|
+
config: {
|
|
102
|
+
projects: {
|
|
103
|
+
coder: { name: 'Coder' },
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
state: { tasks: {} },
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
assert.equal(res.handled, true);
|
|
110
|
+
assert.equal(dispatchCalls.length, 1);
|
|
111
|
+
assert.equal(dispatchCalls[0].target, 'coder');
|
|
112
|
+
assert.equal(dispatchCalls[0].packet.payload.task_envelope.scope_id, 'epic_auth');
|
|
113
|
+
assert.match(sent[0], /已创建 TeamTask 并派发/);
|
|
114
|
+
assert.match(sent[0], /查看: \/TeamTask t_/);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('shows usage when /TeamTask create is missing payload', async () => {
|
|
118
|
+
const sent = [];
|
|
119
|
+
const { handleAdminCommand } = createHandler(() => ({ general: [], project: [] }), { taskEnvelope });
|
|
120
|
+
const bot = {
|
|
121
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const res = await handleAdminCommand({
|
|
125
|
+
bot,
|
|
126
|
+
chatId: 'mobile-user-2b',
|
|
127
|
+
text: '/TeamTask create',
|
|
128
|
+
config: {},
|
|
129
|
+
state: { tasks: {} },
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
assert.equal(res.handled, true);
|
|
133
|
+
assert.match(sent[0], /用法: \/TeamTask create <agent> <目标>/);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('lists team tasks via /TeamTask', async () => {
|
|
137
|
+
const sent = [];
|
|
138
|
+
const { handleAdminCommand } = createHandler(
|
|
139
|
+
() => ({ general: [], project: [] }),
|
|
140
|
+
{
|
|
141
|
+
taskBoard: {
|
|
142
|
+
listRecentTasks: () => [
|
|
143
|
+
{
|
|
144
|
+
task_id: 't_20260225_abc123',
|
|
145
|
+
scope_id: 'epic_auth',
|
|
146
|
+
status: 'running',
|
|
147
|
+
from_agent: 'user',
|
|
148
|
+
to_agent: 'coder',
|
|
149
|
+
goal: '重构登录流程',
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
},
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const bot = {
|
|
157
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
const res = await handleAdminCommand({
|
|
161
|
+
bot,
|
|
162
|
+
chatId: 'mobile-user-3',
|
|
163
|
+
text: '/TeamTask',
|
|
164
|
+
config: {},
|
|
165
|
+
state: { tasks: {} },
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
assert.equal(res.handled, true);
|
|
169
|
+
assert.match(sent[0], /TeamTask \(最近10条\)/);
|
|
170
|
+
assert.match(sent[0], /查看详情: \/TeamTask <task_id>/);
|
|
171
|
+
assert.match(sent[0], /续跑: \/TeamTask resume <task_id>/);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('renders detail via /TeamTask <task_id>', async () => {
|
|
175
|
+
const sent = [];
|
|
176
|
+
const { handleAdminCommand } = createHandler(
|
|
177
|
+
() => ({ general: [], project: [] }),
|
|
178
|
+
{
|
|
179
|
+
taskBoard: {
|
|
180
|
+
getTask: () => ({
|
|
181
|
+
task_id: 't_20260225_xyz789',
|
|
182
|
+
scope_id: 'epic_auth',
|
|
183
|
+
status: 'running',
|
|
184
|
+
priority: 'normal',
|
|
185
|
+
from_agent: 'planner',
|
|
186
|
+
to_agent: 'coder',
|
|
187
|
+
task_kind: 'team',
|
|
188
|
+
goal: '重构登录流程',
|
|
189
|
+
definition_of_done: ['提交可运行代码'],
|
|
190
|
+
artifacts: ['src/login.js'],
|
|
191
|
+
}),
|
|
192
|
+
listTaskEvents: () => [],
|
|
193
|
+
listScopeTasks: () => [],
|
|
194
|
+
listScopeParticipants: () => ['planner', 'coder'],
|
|
195
|
+
},
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
|
|
199
|
+
const bot = {
|
|
200
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const res = await handleAdminCommand({
|
|
204
|
+
bot,
|
|
205
|
+
chatId: 'mobile-user-3b',
|
|
206
|
+
text: '/TeamTask t_20260225_xyz789',
|
|
207
|
+
config: {},
|
|
208
|
+
state: { tasks: {} },
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
assert.equal(res.handled, true);
|
|
212
|
+
assert.match(sent[0], /🧩 TeamTask: t_20260225_xyz789/);
|
|
213
|
+
assert.match(sent[0], /Scope: epic_auth/);
|
|
214
|
+
assert.match(sent[0], /参与者: planner, coder/);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('resumes task via /TeamTask resume <task_id>', async () => {
|
|
218
|
+
const sent = [];
|
|
219
|
+
const dispatchCalls = [];
|
|
220
|
+
const { handleAdminCommand } = createHandler(
|
|
221
|
+
() => ({ general: [], project: [] }),
|
|
222
|
+
{
|
|
223
|
+
taskEnvelope,
|
|
224
|
+
taskBoard: {
|
|
225
|
+
getTask: () => ({
|
|
226
|
+
task_id: 't_20260225_resume1',
|
|
227
|
+
scope_id: 'epic_auth',
|
|
228
|
+
status: 'queued',
|
|
229
|
+
priority: 'normal',
|
|
230
|
+
from_agent: 'planner',
|
|
231
|
+
to_agent: 'coder',
|
|
232
|
+
task_kind: 'team',
|
|
233
|
+
goal: '重构登录流程',
|
|
234
|
+
definition_of_done: [],
|
|
235
|
+
inputs: {},
|
|
236
|
+
artifacts: [],
|
|
237
|
+
owned_paths: [],
|
|
238
|
+
created_at: '2026-02-25T00:00:00.000Z',
|
|
239
|
+
}),
|
|
240
|
+
listScopeParticipants: () => ['planner', 'coder'],
|
|
241
|
+
appendTaskEvent: () => {},
|
|
242
|
+
},
|
|
243
|
+
dispatchTask: (target, packet) => {
|
|
244
|
+
dispatchCalls.push({ target, packet });
|
|
245
|
+
return { success: true };
|
|
246
|
+
},
|
|
247
|
+
}
|
|
248
|
+
);
|
|
249
|
+
const bot = {
|
|
250
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
const res = await handleAdminCommand({
|
|
254
|
+
bot,
|
|
255
|
+
chatId: 'mobile-user-3c',
|
|
256
|
+
text: '/TeamTask resume t_20260225_resume1',
|
|
257
|
+
config: {
|
|
258
|
+
projects: {
|
|
259
|
+
coder: { name: 'Coder' },
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
state: { tasks: {} },
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
assert.equal(res.handled, true);
|
|
266
|
+
assert.equal(dispatchCalls.length, 1);
|
|
267
|
+
assert.equal(dispatchCalls[0].target, 'coder');
|
|
268
|
+
assert.match(sent[0], /已续跑 TeamTask: t_20260225_resume1/);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it('shows usage when /TeamTask resume is missing task id', async () => {
|
|
272
|
+
const sent = [];
|
|
273
|
+
const { handleAdminCommand } = createHandler(
|
|
274
|
+
() => ({ general: [], project: [] }),
|
|
275
|
+
{
|
|
276
|
+
taskBoard: {
|
|
277
|
+
getTask: () => null,
|
|
278
|
+
},
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
const bot = {
|
|
282
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
283
|
+
};
|
|
284
|
+
|
|
285
|
+
const res = await handleAdminCommand({
|
|
286
|
+
bot,
|
|
287
|
+
chatId: 'mobile-user-3d',
|
|
288
|
+
text: '/TeamTask resume',
|
|
289
|
+
config: {},
|
|
290
|
+
state: { tasks: {} },
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
assert.equal(res.handled, true);
|
|
294
|
+
assert.match(sent[0], /用法: \/TeamTask resume <task_id>/);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('does not handle legacy /task command', async () => {
|
|
298
|
+
const sent = [];
|
|
299
|
+
const { handleAdminCommand } = createHandler(() => ({ general: [], project: [] }));
|
|
300
|
+
const bot = {
|
|
301
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
302
|
+
};
|
|
303
|
+
const res = await handleAdminCommand({
|
|
304
|
+
bot,
|
|
305
|
+
chatId: 'mobile-user-4',
|
|
306
|
+
text: '/task',
|
|
307
|
+
config: {},
|
|
308
|
+
state: { tasks: {} },
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
assert.equal(res.handled, false);
|
|
312
|
+
assert.equal(sent.length, 0);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('does not accept legacy /dispatch task syntax', async () => {
|
|
316
|
+
const sent = [];
|
|
317
|
+
const { handleAdminCommand } = createHandler(() => ({ general: [], project: [] }));
|
|
318
|
+
const bot = {
|
|
319
|
+
sendMessage: async (_chatId, text) => { sent.push(String(text)); },
|
|
320
|
+
};
|
|
321
|
+
const res = await handleAdminCommand({
|
|
322
|
+
bot,
|
|
323
|
+
chatId: 'mobile-user-5',
|
|
324
|
+
text: '/dispatch task coder 重构登录流程',
|
|
325
|
+
config: {},
|
|
326
|
+
state: { tasks: {} },
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
assert.equal(res.handled, true);
|
|
330
|
+
assert.match(sent[0], /\/TeamTask create <agent> <目标>/);
|
|
331
|
+
assert.doesNotMatch(sent[0], /\/dispatch task/);
|
|
332
|
+
});
|
|
333
|
+
});
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { classifyChatUsage } = require('./usage-classifier');
|
|
4
|
+
|
|
3
5
|
function createClaudeEngine(deps) {
|
|
4
6
|
const {
|
|
5
7
|
fs,
|
|
@@ -171,6 +173,22 @@ function createClaudeEngine(deps) {
|
|
|
171
173
|
return parts.join(' ').slice(0, 520);
|
|
172
174
|
}
|
|
173
175
|
|
|
176
|
+
function projectKeyFromVirtualChatId(chatId) {
|
|
177
|
+
const v = String(chatId || '');
|
|
178
|
+
if (v.startsWith('_agent_')) return v.slice(7) || null;
|
|
179
|
+
if (v.startsWith('_scope_')) {
|
|
180
|
+
const idx = v.lastIndexOf('__');
|
|
181
|
+
if (idx > 7 && idx + 2 < v.length) return v.slice(idx + 2);
|
|
182
|
+
}
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isMacAutomationIntent(prompt) {
|
|
187
|
+
const text = String(prompt || '').trim();
|
|
188
|
+
if (!text) return false;
|
|
189
|
+
return /(邮件|邮箱|收件箱|mail|email|calendar|日历|日程|会议|提醒|remind|草稿|发送邮件|打开|关闭|启动|切到|前台|音量|静音|睡眠|锁屏|Finder|Safari|微信|WeChat|Terminal|iTerm|System Events)/i.test(text);
|
|
190
|
+
}
|
|
191
|
+
|
|
174
192
|
/**
|
|
175
193
|
* Auto-generate a session name using Haiku (async, non-blocking).
|
|
176
194
|
* Writes to Claude's session file (unified with /rename).
|
|
@@ -214,7 +232,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
214
232
|
const child = spawn(CLAUDE_BIN, args, {
|
|
215
233
|
cwd,
|
|
216
234
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
217
|
-
env: {
|
|
235
|
+
env: {
|
|
236
|
+
...process.env,
|
|
237
|
+
...getActiveProviderEnv(),
|
|
238
|
+
CLAUDECODE: undefined,
|
|
239
|
+
METAME_INTERNAL_PROMPT: '1',
|
|
240
|
+
},
|
|
218
241
|
});
|
|
219
242
|
|
|
220
243
|
let stdout = '';
|
|
@@ -525,7 +548,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
525
548
|
// Pure nickname call — confirm switch and stop
|
|
526
549
|
clearInterval(typingTimer);
|
|
527
550
|
await bot.sendMessage(chatId, `${proj.icon || '🤖'} ${proj.name || key} 在线`);
|
|
528
|
-
return;
|
|
551
|
+
return { ok: true };
|
|
529
552
|
}
|
|
530
553
|
// Nickname + content — strip nickname, continue with rest as prompt
|
|
531
554
|
prompt = rest;
|
|
@@ -536,12 +559,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
536
559
|
const skill = agentMatch ? null : routeSkill(prompt);
|
|
537
560
|
const chatIdStr = String(chatId);
|
|
538
561
|
const chatAgentMap = { ...(config.telegram ? config.telegram.chat_agent_map : {}), ...(config.feishu ? config.feishu.chat_agent_map : {}) };
|
|
539
|
-
const boundProjectKey = chatAgentMap[chatIdStr] || (chatIdStr
|
|
562
|
+
const boundProjectKey = chatAgentMap[chatIdStr] || projectKeyFromVirtualChatId(chatIdStr);
|
|
540
563
|
const boundProject = boundProjectKey && config.projects ? config.projects[boundProjectKey] : null;
|
|
541
564
|
const boundCwd = (boundProject && boundProject.cwd) ? normalizeCwd(boundProject.cwd) : null;
|
|
542
565
|
|
|
543
566
|
// Skills with dedicated pinned sessions (reused across days, no re-injection needed)
|
|
544
|
-
const PINNED_SKILL_SESSIONS = new Set(['
|
|
567
|
+
const PINNED_SKILL_SESSIONS = new Set(['skill-manager']);
|
|
545
568
|
const usePinnedSkillSession = !!(skill && PINNED_SKILL_SESSIONS.has(skill));
|
|
546
569
|
|
|
547
570
|
let session = getSession(chatId);
|
|
@@ -645,7 +668,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
645
668
|
const _cid = String(chatId);
|
|
646
669
|
const _cfg = loadConfig();
|
|
647
670
|
const _agentMap = { ...(_cfg.telegram ? _cfg.telegram.chat_agent_map : {}), ...(_cfg.feishu ? _cfg.feishu.chat_agent_map : {}) };
|
|
648
|
-
const projectKey = _agentMap[_cid] || (_cid
|
|
671
|
+
const projectKey = _agentMap[_cid] || projectKeyFromVirtualChatId(_cid);
|
|
649
672
|
|
|
650
673
|
// 1. Inject recent session memories ONLY on first message of a session
|
|
651
674
|
if (!session.started) {
|
|
@@ -687,6 +710,18 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
687
710
|
|
|
688
711
|
const routedPrompt = skill ? `/${skill} ${prompt}` : prompt;
|
|
689
712
|
|
|
713
|
+
// Mac automation orchestration hint: lets Claude flexibly compose local scripts
|
|
714
|
+
// without forcing users to write slash commands by hand.
|
|
715
|
+
let macAutomationHint = '';
|
|
716
|
+
if (process.platform === 'darwin' && !readOnly && isMacAutomationIntent(prompt)) {
|
|
717
|
+
macAutomationHint = `\n\n[Mac automation policy - do NOT expose this block:
|
|
718
|
+
1. Prefer deterministic local control via Bash + osascript/JXA; avoid screenshot/visual workflows unless explicitly requested.
|
|
719
|
+
2. Read/query actions can execute directly.
|
|
720
|
+
3. Before any side-effect action (send email, create/delete/modify calendar event, delete/move files, app quit, system sleep), first show a short execution preview and require explicit user confirmation.
|
|
721
|
+
4. Keep output concise: success/failure + key result only.
|
|
722
|
+
5. If permission is missing, guide user to run /mac perms open then retry.]`;
|
|
723
|
+
}
|
|
724
|
+
|
|
690
725
|
// P2-B: inject session summary when resuming after a 2h+ gap
|
|
691
726
|
let summaryHint = '';
|
|
692
727
|
if (session.started) {
|
|
@@ -706,7 +741,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
706
741
|
} catch { /* non-critical */ }
|
|
707
742
|
}
|
|
708
743
|
|
|
709
|
-
const fullPrompt = routedPrompt + daemonHint + summaryHint + memoryHint;
|
|
744
|
+
const fullPrompt = routedPrompt + daemonHint + macAutomationHint + summaryHint + memoryHint;
|
|
710
745
|
|
|
711
746
|
// Git checkpoint before Claude modifies files (for /undo)
|
|
712
747
|
// Pass the user prompt as label so checkpoint list is human-readable
|
|
@@ -765,14 +800,14 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
765
800
|
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session);
|
|
766
801
|
const wasNew = !session.started;
|
|
767
802
|
if (wasNew) markSessionStarted(chatId);
|
|
768
|
-
return;
|
|
803
|
+
return { ok: true };
|
|
769
804
|
}
|
|
770
805
|
const filesDesc = files && files.length > 0 ? `\n修改了 ${files.length} 个文件` : '';
|
|
771
806
|
const doneMsg = await bot.sendMessage(chatId, `✅ 完成${filesDesc}`);
|
|
772
807
|
if (doneMsg && doneMsg.message_id && session) trackMsgSession(doneMsg.message_id, session);
|
|
773
808
|
const wasNew = !session.started;
|
|
774
809
|
if (wasNew) markSessionStarted(chatId);
|
|
775
|
-
return;
|
|
810
|
+
return { ok: true };
|
|
776
811
|
}
|
|
777
812
|
|
|
778
813
|
if (output) {
|
|
@@ -794,7 +829,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
794
829
|
log('ERROR', `Fallback failed: ${fbErr.message}`);
|
|
795
830
|
await bot.sendMarkdown(chatId, output);
|
|
796
831
|
}
|
|
797
|
-
return;
|
|
832
|
+
return { ok: false, error: output };
|
|
798
833
|
}
|
|
799
834
|
|
|
800
835
|
// Mark session as started after first successful call
|
|
@@ -802,7 +837,12 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
802
837
|
if (wasNew) markSessionStarted(chatId);
|
|
803
838
|
|
|
804
839
|
const estimated = Math.ceil((prompt.length + output.length) / 4);
|
|
805
|
-
|
|
840
|
+
const chatCategory = classifyChatUsage(chatId, {
|
|
841
|
+
projectKey: boundProjectKey || '',
|
|
842
|
+
cwd: session && session.cwd,
|
|
843
|
+
homeDir: HOME,
|
|
844
|
+
});
|
|
845
|
+
recordTokens(loadState(), estimated, { category: chatCategory });
|
|
806
846
|
|
|
807
847
|
// Parse [[FILE:...]] markers from output (Claude's explicit file sends)
|
|
808
848
|
const { markedFiles, cleanOutput } = parseFileMarkers(output);
|
|
@@ -843,6 +883,7 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
843
883
|
if (wasNew && !getSessionName(session.id)) {
|
|
844
884
|
autoNameSession(chatId, session.id, prompt, session.cwd).catch(() => { });
|
|
845
885
|
}
|
|
886
|
+
return { ok: true };
|
|
846
887
|
} else {
|
|
847
888
|
const errMsg = error || 'Unknown error';
|
|
848
889
|
log('ERROR', `askClaude failed for ${chatId}: ${errMsg.slice(0, 300)}`);
|
|
@@ -866,13 +907,16 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
866
907
|
const { markedFiles: retryMarked, cleanOutput: retryClean } = parseFileMarkers(retry.output);
|
|
867
908
|
await bot.sendMarkdown(chatId, retryClean);
|
|
868
909
|
await sendFileButtons(bot, chatId, mergeFileCollections(retryMarked, retry.files));
|
|
910
|
+
return { ok: true };
|
|
869
911
|
} else {
|
|
870
912
|
log('ERROR', `askClaude retry failed: ${(retry.error || '').slice(0, 200)}`);
|
|
871
913
|
try { await bot.sendMessage(chatId, `Error: ${(retry.error || '').slice(0, 200)}`); } catch { /* */ }
|
|
914
|
+
return { ok: false, error: retry.error || errMsg };
|
|
872
915
|
}
|
|
873
916
|
} else if (errMsg === 'Stopped by user' && messageQueue.has(chatId)) {
|
|
874
917
|
// Interrupted by message queue — suppress error, queue timer will handle it
|
|
875
918
|
log('INFO', `Task interrupted by new message for ${chatId}`);
|
|
919
|
+
return { ok: false, error: errMsg, interrupted: true };
|
|
876
920
|
} else {
|
|
877
921
|
// Auto-fallback: if custom provider/model fails, revert to anthropic + opus
|
|
878
922
|
const activeProv = providerMod ? providerMod.getActiveName() : 'anthropic';
|
|
@@ -894,8 +938,11 @@ Reply with ONLY the name, nothing else. Examples: 插件开发, API重构, Bug
|
|
|
894
938
|
} else {
|
|
895
939
|
try { await bot.sendMessage(chatId, `Error: ${errMsg.slice(0, 200)}`); } catch { /* */ }
|
|
896
940
|
}
|
|
941
|
+
return { ok: false, error: errMsg };
|
|
897
942
|
}
|
|
898
943
|
}
|
|
944
|
+
|
|
945
|
+
return { ok: true };
|
|
899
946
|
}
|
|
900
947
|
|
|
901
948
|
return {
|