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
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1759
|
-
|
|
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)
|
|
1765
|
+
if (remote) repoProject = path.basename(remote, '.git');
|
|
1764
1766
|
} catch { /* not a git repo, use dirname */ }
|
|
1765
1767
|
|
|
1766
|
-
const
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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,
|
|
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')) || {};
|