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 CHANGED
@@ -64,7 +64,7 @@ Claude: ✏️ Edit: api/login.ts
64
64
  ✅ Fixed. 3 tests passing.
65
65
  ```
66
66
 
67
- Start on your laptop, continue on the train. `/stop` to interrupt, `/undo` to rollback, `/sh ls` for raw shell access when everything else breaks.
67
+ Start on your laptop, continue on the train. `/stop` to interrupt, `/undo` to rollback, `/mac check` for macOS automation diagnostics, and `/sh ls` for raw shell access when everything else breaks.
68
68
 
69
69
  ### 3. Layered Memory That Works While You Sleep
70
70
 
@@ -124,7 +124,8 @@ projects:
124
124
  heartbeat_tasks:
125
125
  - name: "daily-draft"
126
126
  prompt: "Research top AI news and write an article"
127
- interval: "24h"
127
+ at: "09:30"
128
+ days: "weekdays"
128
129
  model: "sonnet"
129
130
  notify: true
130
131
 
@@ -132,7 +133,7 @@ heartbeat:
132
133
  tasks:
133
134
  - name: "morning-brief"
134
135
  prompt: "Summarize my git activity from yesterday"
135
- interval: "24h"
136
+ at: "09:00"
136
137
  notify: true
137
138
  ```
138
139
 
@@ -150,7 +151,7 @@ Chain skills into multi-step workflows — research → write → publish — fu
150
151
  prompt: "Publish it"
151
152
  ```
152
153
 
153
- Task options: `require_idle` (defer when you're active, retry on next heartbeat tick), `precondition` (shell guard — skip if false, zero tokens), `notify` (push result to phone), `model`, `cwd`, `allowedTools`, `timeout`.
154
+ Task options: `interval` (every N seconds/minutes/hours/days), `at` (fixed local `HH:MM`), `days` (optional day filter), `require_idle` (defer when you're active, retry on next heartbeat tick), `precondition` (shell guard — skip if false, zero tokens), `notify` (push result to phone), `model`, `cwd`, `allowedTools`, `timeout`.
154
155
 
155
156
  ### 5. Skills That Evolve Themselves
156
157
 
@@ -208,7 +209,7 @@ npm install -g metame-cli && metame
208
209
  | **Browser Automation** | Built-in Playwright MCP. Browser control out of the box for every user. |
209
210
  | **Provider Relay** | Route through any Anthropic-compatible API. Use GPT-4, DeepSeek, Gemini — zero config file mutation. |
210
211
  | **Metacognition** | Detects behavioral patterns (decision style, comfort zones, goal drift) and injects mirror observations. Zero extra API cost. |
211
- | **Emergency Tools** | `/doctor` diagnostics, `/sh` raw shell, `/fix` config restore, `/undo` git-based rollback. |
212
+ | **Emergency Tools** | `/doctor` diagnostics, `/mac` macOS control helpers, `/sh` raw shell, `/fix` config restore, `/undo` git-based rollback. |
212
213
 
213
214
  ## Defining Your Agents
214
215
 
@@ -263,7 +264,8 @@ projects:
263
264
  heartbeat_tasks:
264
265
  - name: "daily-review"
265
266
  prompt: "Review yesterday's commits and flag any issues"
266
- interval: "24h"
267
+ at: "20:30"
268
+ days: [mon, tue, wed, thu, fri]
267
269
  notify: true
268
270
 
269
271
  feishu:
@@ -294,6 +296,7 @@ All agents share your cognitive profile (`~/.claude_profile.yaml`) — they all
294
296
  | `/list` | Browse & download project files |
295
297
  | `/model` | Switch model (sonnet/opus/haiku) |
296
298
  | `/agent bind <name> [dir]` | Register group as dedicated agent |
299
+ | `/mac` | macOS control helper: permissions check/open + AppleScript/JXA execution |
297
300
  | `/sh <cmd>` | Raw shell — bypasses Claude |
298
301
  | `/memory` | Memory stats: fact count, session tags, DB size |
299
302
  | `/memory <keyword>` | Search long-term facts by keyword |
package/index.js CHANGED
@@ -30,7 +30,7 @@ if (!fs.existsSync(METAME_DIR)) {
30
30
  // Auto-deploy bundled scripts to ~/.metame/
31
31
  // IMPORTANT: daemon.yaml is USER CONFIG — never overwrite it. Only daemon-default.yaml (template) is synced.
32
32
  const scriptsDir = path.join(__dirname, 'scripts');
33
- const BUNDLED_BASE_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'qmd-client.js', 'session-summarize.js'];
33
+ const BUNDLED_BASE_SCRIPTS = ['signal-capture.js', 'distill.js', 'schema.js', 'pending-traits.js', 'migrate-v2.js', 'daemon.js', 'telegram-adapter.js', 'feishu-adapter.js', 'daemon-default.yaml', 'providers.js', 'session-analytics.js', 'resolve-yaml.js', 'utils.js', 'skill-evolution.js', 'memory.js', 'memory-extract.js', 'memory-search.js', 'qmd-client.js', 'session-summarize.js', 'check-macos-control-capabilities.sh'];
34
34
  const DAEMON_MODULE_SCRIPTS = (() => {
35
35
  try {
36
36
  return fs.readdirSync(scriptsDir).filter((f) => /^daemon-[\w-]+\.js$/.test(f));
@@ -1755,15 +1755,22 @@ if (!isKnownUser) {
1755
1755
  // RAG: inject relevant facts based on current project (desktop-side equivalent of daemon RAG)
1756
1756
  try {
1757
1757
  const memory = require(path.join(__dirname, 'scripts', 'memory.js'));
1758
- // Derive project key from git repo name or cwd basename
1759
- let projectQuery = path.basename(process.cwd());
1758
+ const { projectScopeFromCwd } = require(path.join(__dirname, 'scripts', 'utils.js'));
1759
+ // Keep cwd basename as authoritative project filter for legacy rows (scope IS NULL).
1760
+ const cwdProject = path.basename(process.cwd());
1761
+ let repoProject = cwdProject;
1760
1762
  try {
1761
1763
  const { execSync } = require('child_process');
1762
1764
  const remote = execSync('git remote get-url origin 2>/dev/null || true', { encoding: 'utf8', stdio: 'pipe' }).trim();
1763
- if (remote) projectQuery = path.basename(remote, '.git');
1765
+ if (remote) repoProject = path.basename(remote, '.git');
1764
1766
  } catch { /* not a git repo, use dirname */ }
1765
1767
 
1766
- const facts = memory.searchFacts(projectQuery, { limit: 5 });
1768
+ const factQuery = repoProject === cwdProject ? cwdProject : `${repoProject} ${cwdProject}`;
1769
+ const facts = memory.searchFacts(factQuery, {
1770
+ limit: 5,
1771
+ project: cwdProject || undefined,
1772
+ scope: projectScopeFromCwd(process.cwd()) || undefined,
1773
+ });
1767
1774
  if (facts.length > 0) {
1768
1775
  const factBlock = facts.map(f => `- [${f.relation}] ${f.value}`).join('\n');
1769
1776
  launchArgs.push(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metame-cli",
3
- "version": "1.4.17",
3
+ "version": "1.4.18",
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": {
@@ -13,7 +13,7 @@
13
13
  "scripts": {
14
14
  "test": "node --test scripts/*.test.js",
15
15
  "start": "node index.js",
16
- "sync:plugin": "cp scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-command-router.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-extract.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/skill-evolution.js plugin/scripts/ && echo '✅ Plugin scripts synced'",
16
+ "sync:plugin": "cp scripts/schema.js scripts/pending-traits.js scripts/signal-capture.js scripts/distill.js scripts/daemon.js scripts/daemon-agent-commands.js scripts/daemon-session-commands.js scripts/daemon-admin-commands.js scripts/daemon-exec-commands.js scripts/daemon-ops-commands.js scripts/daemon-session-store.js scripts/daemon-checkpoints.js scripts/daemon-bridges.js scripts/daemon-file-browser.js scripts/daemon-runtime-lifecycle.js scripts/daemon-notify.js scripts/daemon-claude-engine.js scripts/daemon-command-router.js scripts/daemon-agent-tools.js scripts/daemon-task-scheduler.js scripts/daemon-task-envelope.js scripts/task-board.js scripts/telegram-adapter.js scripts/feishu-adapter.js scripts/daemon-default.yaml scripts/providers.js scripts/utils.js scripts/usage-classifier.js scripts/resolve-yaml.js scripts/memory.js scripts/memory-extract.js scripts/qmd-client.js scripts/session-summarize.js scripts/session-analytics.js scripts/skill-evolution.js scripts/check-macos-control-capabilities.sh plugin/scripts/ && echo '✅ Plugin scripts synced'",
17
17
  "restart:daemon": "node index.js stop 2>/dev/null; sleep 1; node index.js start 2>/dev/null || echo '⚠️ Daemon not running or restart failed'",
18
18
  "precommit": "npm run sync:plugin && npm run restart:daemon"
19
19
  },
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env bash
2
+ set -u
3
+ set -o pipefail
4
+
5
+ PASS=0
6
+ FAIL=0
7
+ WARN=0
8
+
9
+ print_line() {
10
+ printf '%s\n' "$1"
11
+ }
12
+
13
+ run_check() {
14
+ local name="$1"
15
+ local mode="$2"
16
+ local cmd="$3"
17
+
18
+ local output
19
+ output="$(bash -o pipefail -lc "$cmd" 2>&1)"
20
+ local code=$?
21
+
22
+ if [ "$mode" = "pass_on_zero" ]; then
23
+ if [ $code -eq 0 ]; then
24
+ PASS=$((PASS + 1))
25
+ print_line "[PASS] $name"
26
+ [ -n "$output" ] && print_line " $output"
27
+ else
28
+ FAIL=$((FAIL + 1))
29
+ print_line "[FAIL] $name"
30
+ [ -n "$output" ] && print_line " $output"
31
+ fi
32
+ return
33
+ fi
34
+
35
+ if [ "$mode" = "warn_on_nonzero" ]; then
36
+ if [ $code -eq 0 ]; then
37
+ PASS=$((PASS + 1))
38
+ print_line "[PASS] $name"
39
+ [ -n "$output" ] && print_line " $output"
40
+ else
41
+ WARN=$((WARN + 1))
42
+ print_line "[WARN] $name"
43
+ [ -n "$output" ] && print_line " $output"
44
+ fi
45
+ return
46
+ fi
47
+
48
+ FAIL=$((FAIL + 1))
49
+ print_line "[FAIL] $name"
50
+ print_line " invalid mode: $mode"
51
+ }
52
+
53
+ print_line "MetaMe macOS control capability check"
54
+ print_line "Timestamp: $(date '+%Y-%m-%d %H:%M:%S %z')"
55
+ print_line ""
56
+
57
+ run_check "osascript binary available" "pass_on_zero" "which osascript"
58
+ run_check "AppleScript baseline" "pass_on_zero" "osascript -e 'return \"ok\"'"
59
+ run_check "Finder automation" "pass_on_zero" "osascript -e 'tell application \"Finder\" to get name of startup disk'"
60
+ run_check "System Events accessibility" "pass_on_zero" "osascript -e 'tell application \"System Events\" to get UI elements enabled'"
61
+ run_check "GUI app launch/control (Calculator)" "pass_on_zero" "open -a Calculator >/dev/null 2>&1; sleep 1; osascript -e 'tell application \"System Events\" to tell process \"Calculator\" to return {frontmost, (count of windows)}'; osascript -e 'tell application \"Calculator\" to quit' >/dev/null 2>&1"
62
+
63
+ SHOT_PATH="/tmp/metame_gui_test_$$.png"
64
+ run_check "Screenshot capability (screencapture)" "pass_on_zero" "screencapture -x '$SHOT_PATH' && ls -lh '$SHOT_PATH'"
65
+ rm -f "$SHOT_PATH" >/dev/null 2>&1
66
+
67
+ run_check "Full Disk probe: read ~/Library/Mail" "warn_on_nonzero" "ls '$HOME/Library/Mail' | head -n 3"
68
+ run_check "Full Disk probe: query Safari History.db" "warn_on_nonzero" "sqlite3 '$HOME/Library/Safari/History.db' 'select count(*) from history_items;'"
69
+
70
+ print_line ""
71
+ print_line "Summary: pass=$PASS warn=$WARN fail=$FAIL"
72
+
73
+ if [ $FAIL -gt 0 ]; then
74
+ exit 1
75
+ fi
76
+
77
+ exit 0
@@ -1,5 +1,11 @@
1
1
  'use strict';
2
2
 
3
+ const {
4
+ USAGE_CATEGORY_ORDER,
5
+ CORE_USAGE_CATEGORIES,
6
+ USAGE_CATEGORY_LABEL,
7
+ } = require('./usage-classifier');
8
+
3
9
  function createAdminCommandHandler(deps) {
4
10
  const {
5
11
  fs,
@@ -18,8 +24,70 @@ function createAdminCommandHandler(deps) {
18
24
  dispatchTask,
19
25
  log,
20
26
  skillEvolution,
27
+ taskBoard,
28
+ taskEnvelope,
21
29
  } = deps;
22
30
 
31
+ function resolveProjectKey(targetName, projects) {
32
+ if (!targetName || !projects) return null;
33
+ for (const [key, proj] of Object.entries(projects || {})) {
34
+ const nicknames = Array.isArray(proj.nicknames)
35
+ ? proj.nicknames
36
+ : (proj.nicknames ? [proj.nicknames] : []);
37
+ if (key === targetName || nicknames.some(n => n === targetName)) return key;
38
+ }
39
+ return null;
40
+ }
41
+
42
+ function resolveSenderKey(chatId, config) {
43
+ const map = {
44
+ ...(config && config.feishu ? config.feishu.chat_agent_map : {}),
45
+ ...(config && config.telegram ? config.telegram.chat_agent_map : {}),
46
+ };
47
+ return map[String(chatId)] || 'user';
48
+ }
49
+
50
+ function popFlag(input, flagName) {
51
+ const src = String(input || '');
52
+ const re = new RegExp(`(?:^|\\s)--${flagName}\\s+(\\S+)`, 'i');
53
+ const m = src.match(re);
54
+ if (!m) return { text: src.trim(), value: '' };
55
+ const value = String(m[1] || '').trim();
56
+ const text = src.replace(m[0], ' ').replace(/\s+/g, ' ').trim();
57
+ return { text, value };
58
+ }
59
+
60
+ function parseTeamTaskArgs(raw) {
61
+ const src = String(raw || '').trim();
62
+ const first = src.match(/^(\S+)\s+([\s\S]+)$/);
63
+ if (!first) return null;
64
+ const targetName = first[1];
65
+ let rest = first[2].trim();
66
+ const scopePop = popFlag(rest, 'scope');
67
+ rest = scopePop.text;
68
+ const parentPop = popFlag(rest, 'parent');
69
+ rest = parentPop.text;
70
+ return {
71
+ targetName,
72
+ goal: rest,
73
+ scopeId: scopePop.value || '',
74
+ parentTaskId: parentPop.value || '',
75
+ };
76
+ }
77
+
78
+ function formatTaskSchedule(task) {
79
+ const at = typeof task.at === 'string' ? task.at.trim() : '';
80
+ if (at) {
81
+ const rawDays = task.days !== undefined ? task.days : task.weekdays;
82
+ let daysLabel = '';
83
+ if (Array.isArray(rawDays)) daysLabel = rawDays.join(',');
84
+ else if (typeof rawDays === 'string') daysLabel = rawDays.trim();
85
+ return daysLabel ? `at ${at} ${daysLabel}` : `at ${at}`;
86
+ }
87
+ if (task.interval) return `every ${task.interval}`;
88
+ return 'unspecified';
89
+ }
90
+
23
91
  async function handleAdminCommand(ctx) {
24
92
  const { bot, chatId, text } = ctx;
25
93
  const state = ctx.state || {};
@@ -138,7 +206,7 @@ function createAdminCommandHandler(deps) {
138
206
  msg += '📋 General:\n';
139
207
  for (const t of general) {
140
208
  const ts = state.tasks[t.name] || {};
141
- msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${t.interval}) ${ts.status || 'never_run'}\n`;
209
+ msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${formatTaskSchedule(t)}) ${ts.status || 'never_run'}\n`;
142
210
  }
143
211
  }
144
212
  // Project tasks grouped by _project
@@ -152,7 +220,7 @@ function createAdminCommandHandler(deps) {
152
220
  msg += `\n${proj.icon} ${proj.name}:\n`;
153
221
  for (const t of tasks) {
154
222
  const ts = state.tasks[t.name] || {};
155
- msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${t.interval}) ${ts.status || 'never_run'}\n`;
223
+ msg += `${t.enabled !== false ? '✅' : '⏸'} ${t.name} (${formatTaskSchedule(t)}) ${ts.status || 'never_run'}\n`;
156
224
  }
157
225
  }
158
226
  if (!msg) {
@@ -163,6 +231,216 @@ function createAdminCommandHandler(deps) {
163
231
  return { handled: true, config };
164
232
  }
165
233
 
234
+ // /TeamTask — create/list/detail/resume team collaboration tasks
235
+ const teamTaskCmdMatch = text.match(/^\/teamtask(?:\s+([\s\S]+))?$/i);
236
+ if (teamTaskCmdMatch) {
237
+ const args = String(teamTaskCmdMatch[1] || '').trim();
238
+ if (/^create$/i.test(args)) {
239
+ await bot.sendMessage(chatId, '❌ 用法: /TeamTask create <agent> <目标> [--scope <scopeId>] [--parent <taskId>]');
240
+ return { handled: true, config };
241
+ }
242
+ const createMatch = args.match(/^create\s+([\s\S]+)$/i);
243
+ if (createMatch) {
244
+ if (!taskEnvelope) {
245
+ await bot.sendMessage(chatId, '❌ task protocol 不可用');
246
+ return { handled: true, config };
247
+ }
248
+ const parsed = parseTeamTaskArgs(createMatch[1]);
249
+ if (!parsed || !parsed.targetName || !parsed.goal) {
250
+ await bot.sendMessage(chatId, '❌ 用法: /TeamTask create <agent> <目标> [--scope <scopeId>] [--parent <taskId>]');
251
+ return { handled: true, config };
252
+ }
253
+ const { targetName, goal, scopeId, parentTaskId } = parsed;
254
+ const targetKey = resolveProjectKey(targetName, config.projects || {});
255
+ if (!targetKey) {
256
+ await bot.sendMessage(chatId, `未找到 agent: ${targetName}\n可用: ${Object.keys(config.projects || {}).join(', ')}`);
257
+ return { handled: true, config };
258
+ }
259
+ const senderKey = resolveSenderKey(chatId, config);
260
+ const participants = (scopeId && taskBoard && taskBoard.listScopeParticipants)
261
+ ? taskBoard.listScopeParticipants(scopeId)
262
+ : [];
263
+ participants.push(senderKey, targetKey);
264
+ const envelope = taskEnvelope.normalizeTaskEnvelope({
265
+ from_agent: senderKey,
266
+ to_agent: targetKey,
267
+ scope_id: scopeId || '',
268
+ parent_task_id: parentTaskId || null,
269
+ participants,
270
+ goal,
271
+ task_kind: 'team',
272
+ definition_of_done: [
273
+ '输出可执行结果和关键结论',
274
+ '必要时给出产物路径与下一步建议',
275
+ ],
276
+ inputs: {
277
+ source_chat_id: String(chatId),
278
+ source: 'mobile_teamtask',
279
+ },
280
+ priority: 'normal',
281
+ status: 'queued',
282
+ });
283
+ const checked = taskEnvelope.validateTaskEnvelope(envelope);
284
+ if (!checked.ok) {
285
+ await bot.sendMessage(chatId, `❌ TeamTask 无效: ${checked.error}`);
286
+ return { handled: true, config };
287
+ }
288
+ const result = dispatchTask(targetKey, {
289
+ from: senderKey,
290
+ type: 'task',
291
+ priority: envelope.priority,
292
+ payload: {
293
+ title: goal.slice(0, 60),
294
+ prompt: goal,
295
+ task_envelope: envelope,
296
+ },
297
+ callback: false,
298
+ }, config);
299
+ if (result.success) {
300
+ await bot.sendMessage(chatId, [
301
+ `✅ 已创建 TeamTask 并派发: ${envelope.task_id}`,
302
+ `Scope: ${envelope.scope_id || envelope.task_id}`,
303
+ `查看: /TeamTask ${envelope.task_id}`,
304
+ ].join('\n'));
305
+ } else {
306
+ await bot.sendMessage(chatId, `❌ 创建 TeamTask 失败: ${result.error}`);
307
+ }
308
+ return { handled: true, config };
309
+ }
310
+
311
+ if (!taskBoard) {
312
+ await bot.sendMessage(chatId, '❌ Task Board 不可用');
313
+ return { handled: true, config };
314
+ }
315
+
316
+ if (!args || /^list$/i.test(args)) {
317
+ const recent = taskBoard.listRecentTasks(10, null, 'team');
318
+ if (recent.length === 0) {
319
+ await bot.sendMessage(chatId, '暂无 TeamTask。\n使用 /TeamTask create <agent> <goal> 创建。');
320
+ return { handled: true, config };
321
+ }
322
+ let msg = '🧩 TeamTask (最近10条)\n';
323
+ for (const t of recent) {
324
+ msg += `\n- ${t.task_id} [${t.status}] scope=${t.scope_id || t.task_id}\n ${t.from_agent}→${t.to_agent} · ${t.goal.slice(0, 80)}`;
325
+ }
326
+ msg += '\n\n查看详情: /TeamTask <task_id>\n续跑: /TeamTask resume <task_id>';
327
+ await bot.sendMessage(chatId, msg);
328
+ return { handled: true, config };
329
+ }
330
+
331
+ const resumeMatch = args.match(/^resume\s+(\S+)$/i);
332
+ if (resumeMatch) {
333
+ const taskId = resumeMatch[1];
334
+ const task = taskBoard.getTask(taskId);
335
+ if (!task || task.task_kind !== 'team') {
336
+ await bot.sendMessage(chatId, `❌ 未找到 TeamTask: ${taskId}`);
337
+ return { handled: true, config };
338
+ }
339
+ const targetKey = task.to_agent;
340
+ if (!config.projects || !config.projects[targetKey]) {
341
+ await bot.sendMessage(chatId, `❌ 目标 agent 不存在: ${targetKey}`);
342
+ return { handled: true, config };
343
+ }
344
+ const envelope = taskEnvelope && taskEnvelope.normalizeTaskEnvelope
345
+ ? taskEnvelope.normalizeTaskEnvelope({
346
+ ...task,
347
+ status: 'queued',
348
+ updated_at: new Date().toISOString(),
349
+ task_kind: 'team',
350
+ participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
351
+ }, {
352
+ from_agent: task.from_agent || resolveSenderKey(chatId, config),
353
+ to_agent: targetKey,
354
+ scope_id: task.scope_id || task.task_id,
355
+ })
356
+ : {
357
+ task_id: task.task_id,
358
+ scope_id: task.scope_id || task.task_id,
359
+ from_agent: task.from_agent || resolveSenderKey(chatId, config),
360
+ to_agent: targetKey,
361
+ participants: taskBoard.listScopeParticipants(task.scope_id || task.task_id),
362
+ goal: task.goal,
363
+ definition_of_done: task.definition_of_done || [],
364
+ inputs: task.inputs || {},
365
+ artifacts: task.artifacts || [],
366
+ owned_paths: task.owned_paths || [],
367
+ priority: task.priority || 'normal',
368
+ status: 'queued',
369
+ task_kind: 'team',
370
+ created_at: task.created_at,
371
+ updated_at: new Date().toISOString(),
372
+ };
373
+
374
+ const result = dispatchTask(targetKey, {
375
+ from: envelope.from_agent || 'user',
376
+ type: 'task',
377
+ priority: envelope.priority || 'normal',
378
+ payload: {
379
+ title: envelope.goal.slice(0, 60),
380
+ prompt: envelope.goal,
381
+ task_envelope: envelope,
382
+ },
383
+ callback: false,
384
+ new_session: false,
385
+ }, config);
386
+
387
+ if (result.success) {
388
+ taskBoard.appendTaskEvent(task.task_id, 'task_resume_requested', String(chatId), { by: String(chatId) });
389
+ await bot.sendMessage(chatId, `✅ 已续跑 TeamTask: ${task.task_id}`);
390
+ } else {
391
+ await bot.sendMessage(chatId, `❌ 续跑失败: ${result.error}`);
392
+ }
393
+ return { handled: true, config };
394
+ }
395
+
396
+ if (/^resume$/i.test(args)) {
397
+ await bot.sendMessage(chatId, '❌ 用法: /TeamTask resume <task_id>');
398
+ return { handled: true, config };
399
+ }
400
+
401
+ const task = taskBoard.getTask(args);
402
+ if (!task || task.task_kind !== 'team') {
403
+ await bot.sendMessage(chatId, `❌ 未找到 TeamTask: ${args}`);
404
+ return { handled: true, config };
405
+ }
406
+ const events = taskBoard.listTaskEvents(task.task_id, 8);
407
+ const scopeId = task.scope_id || task.task_id;
408
+ const scopeTasks = taskBoard.listScopeTasks(scopeId, 12);
409
+ const scopeParticipants = taskBoard.listScopeParticipants(scopeId);
410
+ let detail = [
411
+ `🧩 TeamTask: ${task.task_id}`,
412
+ `Scope: ${scopeId}`,
413
+ `状态: ${task.status}`,
414
+ `优先级: ${task.priority}`,
415
+ `流向: ${task.from_agent} → ${task.to_agent}`,
416
+ `目标: ${task.goal}`,
417
+ ];
418
+ if (scopeParticipants.length > 0) {
419
+ detail.push(`参与者: ${scopeParticipants.join(', ')}`);
420
+ }
421
+ if (Array.isArray(task.definition_of_done) && task.definition_of_done.length > 0) {
422
+ detail.push('DoD:');
423
+ for (const d of task.definition_of_done.slice(0, 6)) detail.push(`- ${d}`);
424
+ }
425
+ if (Array.isArray(task.artifacts) && task.artifacts.length > 0) {
426
+ detail.push('产物:');
427
+ for (const a of task.artifacts.slice(0, 6)) detail.push(`- ${a}`);
428
+ }
429
+ if (task.last_error) detail.push(`错误: ${task.last_error.slice(0, 180)}`);
430
+ if (events.length > 0) {
431
+ detail.push('最近事件:');
432
+ for (const ev of events.slice(0, 5)) detail.push(`- [${ev.event_type}] ${ev.actor} @ ${ev.created_at}`);
433
+ }
434
+ if (scopeTasks.length > 1) {
435
+ detail.push('同 Scope 相关任务:');
436
+ for (const st of scopeTasks.filter(x => x.task_id !== task.task_id).slice(0, 5)) {
437
+ detail.push(`- ${st.task_id} [${st.status}] ${st.from_agent}→${st.to_agent}`);
438
+ }
439
+ }
440
+ await bot.sendMessage(chatId, detail.join('\n'));
441
+ return { handled: true, config };
442
+ }
443
+
166
444
  // /dispatch — inter-agent task dispatch
167
445
  if (text.startsWith('/dispatch')) {
168
446
  const args = text.slice('/dispatch'.length).trim();
@@ -216,21 +494,14 @@ function createAdminCommandHandler(deps) {
216
494
  const prompt = toMatch[2].trim();
217
495
 
218
496
  // Resolve target by project key or nickname
219
- let targetKey = null;
220
- for (const [key, proj] of Object.entries(config.projects || {})) {
221
- if (key === targetName || (proj.nicknames || []).some(n => n === targetName)) {
222
- targetKey = key;
223
- break;
224
- }
225
- }
497
+ const targetKey = resolveProjectKey(targetName, config.projects || {});
226
498
  if (!targetKey) {
227
499
  await bot.sendMessage(chatId, `未找到 agent: ${targetName}\n可用: ${Object.keys(config.projects || {}).join(', ')}`);
228
500
  return { handled: true, config };
229
501
  }
230
502
 
231
503
  // Determine sender from current chat's project mapping
232
- const chatAgentMap = (config.feishu && config.feishu.chat_agent_map) || {};
233
- const senderKey = chatAgentMap[chatId] || 'user';
504
+ const senderKey = resolveSenderKey(chatId, config);
234
505
 
235
506
  const projInfo = config.projects[targetKey] || {};
236
507
  // Find the target project's own Feishu chat (reverse lookup of chat_agent_map)
@@ -263,7 +534,14 @@ function createAdminCommandHandler(deps) {
263
534
  return { handled: true, config };
264
535
  }
265
536
 
266
- await bot.sendMessage(chatId, '用法:\n/dispatch status — 查看状态\n/dispatch log — 查看记录\n/dispatch to <agent> <任务内容>');
537
+ await bot.sendMessage(chatId, [
538
+ '用法:',
539
+ '/dispatch status — 查看状态',
540
+ '/dispatch log — 查看记录',
541
+ '/dispatch to <agent> <任务内容> — 直接跨 agent 派发',
542
+ '/TeamTask create <agent> <目标> [--scope <id>] [--parent <id>] — 创建/续接 TeamTask',
543
+ '/TeamTask — 查看 TeamTask 列表',
544
+ ].join('\n'));
267
545
  return { handled: true, config };
268
546
  }
269
547
 
@@ -274,6 +552,66 @@ function createAdminCommandHandler(deps) {
274
552
  return { handled: true, config };
275
553
  }
276
554
 
555
+ if (text === '/usage' || text.startsWith('/usage ')) {
556
+ const arg = text.slice('/usage'.length).trim() || 'today';
557
+ const usage = state.usage || {};
558
+ const daily = usage.daily || {};
559
+ const categories = usage.categories || {};
560
+ const limit = (config.budget && config.budget.daily_limit) || 50000;
561
+ const todayIso = new Date().toISOString().slice(0, 10);
562
+
563
+ // Resolve date range
564
+ let days = 1;
565
+ if (arg === 'week') days = 7;
566
+ else if (arg === 'month') days = 30;
567
+ else if (/^\d+d$/.test(arg)) days = Math.min(90, parseInt(arg, 10));
568
+
569
+ const dates = [];
570
+ for (let i = days - 1; i >= 0; i--) {
571
+ const d = new Date(`${todayIso}T00:00:00.000Z`);
572
+ d.setUTCDate(d.getUTCDate() - i);
573
+ dates.push(d.toISOString().slice(0, 10));
574
+ }
575
+
576
+ // Aggregate tokens by category across the date window
577
+ const totals = {};
578
+ let grandTotal = 0;
579
+ for (const date of dates) {
580
+ const bucket = daily[date] || {};
581
+ for (const [key, val] of Object.entries(bucket)) {
582
+ if (key === 'total') continue;
583
+ const n = Math.max(0, Math.floor(Number(val) || 0));
584
+ totals[key] = (totals[key] || 0) + n;
585
+ grandTotal += n;
586
+ }
587
+ }
588
+ // Fallback: if no daily breakdown yet, use categories totals for today
589
+ if (grandTotal === 0 && days === 1) {
590
+ for (const [key, meta] of Object.entries(categories)) {
591
+ const n = Math.max(0, Math.floor(Number(meta && meta.total) || 0));
592
+ if (n > 0) { totals[key] = n; grandTotal += n; }
593
+ }
594
+ }
595
+
596
+ const label = days === 1 ? `今日 (${todayIso})` : `近 ${days} 天`;
597
+ const budgetPct = limit > 0 ? ((grandTotal / limit) * 100).toFixed(1) : '—';
598
+ let lines = [`📊 Token 用量 — ${label}`, `合计: ${grandTotal.toLocaleString()} / ${limit.toLocaleString()} tokens (${budgetPct}%)`];
599
+
600
+ // Render by canonical order, then extras
601
+ const orderedKeys = [...USAGE_CATEGORY_ORDER, ...Object.keys(totals).filter(k => !USAGE_CATEGORY_ORDER.includes(k))];
602
+ for (const key of orderedKeys) {
603
+ const n = totals[key] || 0;
604
+ if (n === 0 && !CORE_USAGE_CATEGORIES.includes(key)) continue;
605
+ const pct = grandTotal > 0 ? ((n / grandTotal) * 100).toFixed(1) : '0.0';
606
+ const lbl = USAGE_CATEGORY_LABEL[key] || key;
607
+ const bar = '█'.repeat(Math.round(Number(pct) / 10)).padEnd(10, '░');
608
+ lines.push(`${lbl}: ${n.toLocaleString()} tokens (${pct}%) ${bar}`);
609
+ }
610
+
611
+ await bot.sendMessage(chatId, lines.join('\n'));
612
+ return { handled: true, config };
613
+ }
614
+
277
615
  if (text === '/quiet') {
278
616
  try {
279
617
  const doc = yaml.load(fs.readFileSync(BRAIN_FILE, 'utf8')) || {};