metame-cli 1.4.15 → 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.
@@ -0,0 +1,398 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const os = require('os');
5
+ const path = require('path');
6
+
7
+ const DEFAULT_DB_PATH = path.join(os.homedir(), '.metame', 'task_board.db');
8
+
9
+ function parseJsonSafe(raw, fallback) {
10
+ try { return JSON.parse(raw); } catch { return fallback; }
11
+ }
12
+
13
+ function toJson(value, fallback) {
14
+ try { return JSON.stringify(value); } catch { return JSON.stringify(fallback); }
15
+ }
16
+
17
+ function sanitizeText(input, maxLen = 1000) {
18
+ return String(input || '').replace(/[\x00-\x1F\x7F]/g, ' ').trim().slice(0, maxLen);
19
+ }
20
+
21
+ function sanitizeStringArray(values, maxItems = 40, maxItemLen = 500) {
22
+ if (!Array.isArray(values)) return [];
23
+ const out = [];
24
+ const seen = new Set();
25
+ for (const item of values) {
26
+ const v = sanitizeText(item, maxItemLen);
27
+ if (!v || seen.has(v)) continue;
28
+ seen.add(v);
29
+ out.push(v);
30
+ if (out.length >= maxItems) break;
31
+ }
32
+ return out;
33
+ }
34
+
35
+ function createTaskBoard(opts = {}) {
36
+ const dbPath = opts.dbPath || DEFAULT_DB_PATH;
37
+ const logger = typeof opts.logger === 'function' ? opts.logger : null;
38
+ let db = null;
39
+
40
+ function logWarn(msg) {
41
+ if (logger) logger(msg);
42
+ }
43
+
44
+ function getDb() {
45
+ if (db) return db;
46
+ const dir = path.dirname(dbPath);
47
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
48
+
49
+ const { DatabaseSync } = require('node:sqlite');
50
+ db = new DatabaseSync(dbPath);
51
+ db.exec('PRAGMA journal_mode = WAL');
52
+ db.exec('PRAGMA busy_timeout = 3000');
53
+
54
+ db.exec(`
55
+ CREATE TABLE IF NOT EXISTS tasks (
56
+ task_id TEXT PRIMARY KEY,
57
+ scope_id TEXT NOT NULL DEFAULT '',
58
+ parent_task_id TEXT,
59
+ from_agent TEXT NOT NULL,
60
+ to_agent TEXT NOT NULL,
61
+ goal TEXT NOT NULL,
62
+ task_kind TEXT NOT NULL DEFAULT 'team',
63
+ participants TEXT NOT NULL DEFAULT '[]',
64
+ definition_of_done TEXT NOT NULL DEFAULT '[]',
65
+ inputs TEXT NOT NULL DEFAULT '{}',
66
+ artifacts TEXT NOT NULL DEFAULT '[]',
67
+ owned_paths TEXT NOT NULL DEFAULT '[]',
68
+ status TEXT NOT NULL DEFAULT 'queued',
69
+ priority TEXT NOT NULL DEFAULT 'normal',
70
+ summary TEXT NOT NULL DEFAULT '',
71
+ last_error TEXT NOT NULL DEFAULT '',
72
+ created_at TEXT NOT NULL,
73
+ updated_at TEXT NOT NULL
74
+ )
75
+ `);
76
+ try { db.exec("ALTER TABLE tasks ADD COLUMN scope_id TEXT NOT NULL DEFAULT ''"); } catch {}
77
+ try { db.exec("ALTER TABLE tasks ADD COLUMN task_kind TEXT NOT NULL DEFAULT 'team'"); } catch {}
78
+ try { db.exec("ALTER TABLE tasks ADD COLUMN participants TEXT NOT NULL DEFAULT '[]'"); } catch {}
79
+
80
+ db.exec(`
81
+ CREATE TABLE IF NOT EXISTS handoffs (
82
+ handoff_id TEXT PRIMARY KEY,
83
+ task_id TEXT NOT NULL,
84
+ from_agent TEXT NOT NULL,
85
+ to_agent TEXT NOT NULL,
86
+ payload TEXT NOT NULL DEFAULT '{}',
87
+ status TEXT NOT NULL DEFAULT 'sent',
88
+ created_at TEXT NOT NULL,
89
+ updated_at TEXT NOT NULL
90
+ )
91
+ `);
92
+
93
+ db.exec(`
94
+ CREATE TABLE IF NOT EXISTS task_events (
95
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
96
+ task_id TEXT NOT NULL,
97
+ event_type TEXT NOT NULL,
98
+ actor TEXT NOT NULL,
99
+ body TEXT NOT NULL DEFAULT '{}',
100
+ created_at TEXT NOT NULL
101
+ )
102
+ `);
103
+
104
+ try { db.exec('CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)'); } catch {}
105
+ try { db.exec('CREATE INDEX IF NOT EXISTS idx_tasks_scope_id ON tasks(scope_id)'); } catch {}
106
+ try { db.exec('CREATE INDEX IF NOT EXISTS idx_tasks_updated_at ON tasks(updated_at)'); } catch {}
107
+ try { db.exec('CREATE INDEX IF NOT EXISTS idx_events_task_id ON task_events(task_id)'); } catch {}
108
+ try { db.exec('CREATE INDEX IF NOT EXISTS idx_handoffs_task_id ON handoffs(task_id)'); } catch {}
109
+ return db;
110
+ }
111
+
112
+ function upsertTask(task) {
113
+ if (!task || !task.task_id) return { ok: false, error: 'task_id_required' };
114
+ const nowIso = new Date().toISOString();
115
+ const safe = {
116
+ task_id: sanitizeText(task.task_id, 80),
117
+ scope_id: sanitizeText(task.scope_id, 120) || sanitizeText(task.task_id, 80),
118
+ parent_task_id: sanitizeText(task.parent_task_id, 80) || null,
119
+ from_agent: sanitizeText(task.from_agent, 80) || 'unknown',
120
+ to_agent: sanitizeText(task.to_agent, 80),
121
+ goal: sanitizeText(task.goal, 500),
122
+ task_kind: sanitizeText(task.task_kind, 20) || 'team',
123
+ participants: sanitizeStringArray(task.participants, 40, 80),
124
+ definition_of_done: sanitizeStringArray(task.definition_of_done, 20, 300),
125
+ inputs: task.inputs && typeof task.inputs === 'object' ? task.inputs : {},
126
+ artifacts: sanitizeStringArray(task.artifacts, 40, 500),
127
+ owned_paths: sanitizeStringArray(task.owned_paths, 40, 500),
128
+ status: sanitizeText(task.status, 20) || 'queued',
129
+ priority: sanitizeText(task.priority, 20) || 'normal',
130
+ summary: sanitizeText(task.summary, 2000),
131
+ last_error: sanitizeText(task.last_error, 2000),
132
+ created_at: sanitizeText(task.created_at, 64) || nowIso,
133
+ updated_at: sanitizeText(task.updated_at, 64) || nowIso,
134
+ };
135
+ if (!safe.task_id || !safe.to_agent || !safe.goal) {
136
+ return { ok: false, error: 'task_fields_missing' };
137
+ }
138
+
139
+ const sql = `
140
+ INSERT INTO tasks (
141
+ task_id, scope_id, parent_task_id, from_agent, to_agent, goal, task_kind, participants,
142
+ definition_of_done, inputs, artifacts, owned_paths, status, priority, summary, last_error,
143
+ created_at, updated_at
144
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
145
+ ON CONFLICT(task_id) DO UPDATE SET
146
+ scope_id = excluded.scope_id,
147
+ parent_task_id = excluded.parent_task_id,
148
+ from_agent = excluded.from_agent,
149
+ to_agent = excluded.to_agent,
150
+ goal = excluded.goal,
151
+ task_kind = excluded.task_kind,
152
+ participants = excluded.participants,
153
+ definition_of_done = excluded.definition_of_done,
154
+ inputs = excluded.inputs,
155
+ artifacts = excluded.artifacts,
156
+ owned_paths = excluded.owned_paths,
157
+ status = excluded.status,
158
+ priority = excluded.priority,
159
+ summary = excluded.summary,
160
+ last_error = excluded.last_error,
161
+ updated_at = excluded.updated_at
162
+ `;
163
+ try {
164
+ getDb().prepare(sql).run(
165
+ safe.task_id,
166
+ safe.scope_id,
167
+ safe.parent_task_id,
168
+ safe.from_agent,
169
+ safe.to_agent,
170
+ safe.goal,
171
+ safe.task_kind,
172
+ toJson(safe.participants, []),
173
+ toJson(safe.definition_of_done, []),
174
+ toJson(safe.inputs, {}),
175
+ toJson(safe.artifacts, []),
176
+ toJson(safe.owned_paths, []),
177
+ safe.status,
178
+ safe.priority,
179
+ safe.summary,
180
+ safe.last_error,
181
+ safe.created_at,
182
+ safe.updated_at
183
+ );
184
+ return { ok: true, task_id: safe.task_id };
185
+ } catch (e) {
186
+ logWarn(`TaskBoard upsertTask failed: ${e.message}`);
187
+ return { ok: false, error: e.message };
188
+ }
189
+ }
190
+
191
+ function appendTaskEvent(taskId, eventType, actor, body = {}) {
192
+ const safeTaskId = sanitizeText(taskId, 80);
193
+ if (!safeTaskId) return { ok: false, error: 'task_id_required' };
194
+ const safeEvent = sanitizeText(eventType, 60) || 'event';
195
+ const safeActor = sanitizeText(actor, 80) || 'system';
196
+ const nowIso = new Date().toISOString();
197
+ try {
198
+ getDb().prepare(`
199
+ INSERT INTO task_events (task_id, event_type, actor, body, created_at)
200
+ VALUES (?, ?, ?, ?, ?)
201
+ `).run(safeTaskId, safeEvent, safeActor, toJson(body, {}), nowIso);
202
+ return { ok: true };
203
+ } catch (e) {
204
+ logWarn(`TaskBoard appendTaskEvent failed: ${e.message}`);
205
+ return { ok: false, error: e.message };
206
+ }
207
+ }
208
+
209
+ function recordHandoff(handoff) {
210
+ const nowIso = new Date().toISOString();
211
+ const safe = {
212
+ handoff_id: sanitizeText(handoff && handoff.handoff_id, 90),
213
+ task_id: sanitizeText(handoff && handoff.task_id, 80),
214
+ from_agent: sanitizeText(handoff && handoff.from_agent, 80) || 'unknown',
215
+ to_agent: sanitizeText(handoff && handoff.to_agent, 80),
216
+ payload: handoff && typeof handoff.payload === 'object' ? handoff.payload : {},
217
+ status: sanitizeText(handoff && handoff.status, 30) || 'sent',
218
+ created_at: sanitizeText(handoff && handoff.created_at, 64) || nowIso,
219
+ updated_at: sanitizeText(handoff && handoff.updated_at, 64) || nowIso,
220
+ };
221
+ if (!safe.handoff_id || !safe.task_id || !safe.to_agent) return { ok: false, error: 'handoff_fields_missing' };
222
+ try {
223
+ getDb().prepare(`
224
+ INSERT INTO handoffs (handoff_id, task_id, from_agent, to_agent, payload, status, created_at, updated_at)
225
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)
226
+ ON CONFLICT(handoff_id) DO UPDATE SET
227
+ payload = excluded.payload,
228
+ status = excluded.status,
229
+ updated_at = excluded.updated_at
230
+ `).run(
231
+ safe.handoff_id,
232
+ safe.task_id,
233
+ safe.from_agent,
234
+ safe.to_agent,
235
+ toJson(safe.payload, {}),
236
+ safe.status,
237
+ safe.created_at,
238
+ safe.updated_at
239
+ );
240
+ return { ok: true };
241
+ } catch (e) {
242
+ logWarn(`TaskBoard recordHandoff failed: ${e.message}`);
243
+ return { ok: false, error: e.message };
244
+ }
245
+ }
246
+
247
+ function getTask(taskId) {
248
+ const safeTaskId = sanitizeText(taskId, 80);
249
+ if (!safeTaskId) return null;
250
+ try {
251
+ const row = getDb().prepare('SELECT * FROM tasks WHERE task_id = ?').get(safeTaskId);
252
+ if (!row) return null;
253
+ return {
254
+ ...row,
255
+ participants: parseJsonSafe(row.participants, []),
256
+ definition_of_done: parseJsonSafe(row.definition_of_done, []),
257
+ inputs: parseJsonSafe(row.inputs, {}),
258
+ artifacts: parseJsonSafe(row.artifacts, []),
259
+ owned_paths: parseJsonSafe(row.owned_paths, []),
260
+ };
261
+ } catch (e) {
262
+ logWarn(`TaskBoard getTask failed: ${e.message}`);
263
+ return null;
264
+ }
265
+ }
266
+
267
+ function listRecentTasks(limit = 10, status = null, taskKind = null) {
268
+ const lim = Math.max(1, Math.min(100, Number(limit) || 10));
269
+ const statusVal = sanitizeText(status, 30);
270
+ const kindVal = sanitizeText(taskKind, 20);
271
+ try {
272
+ let sql = 'SELECT * FROM tasks';
273
+ const params = [];
274
+ const where = [];
275
+ if (statusVal) { where.push('status = ?'); params.push(statusVal); }
276
+ if (kindVal) { where.push('task_kind = ?'); params.push(kindVal); }
277
+ if (where.length > 0) sql += ' WHERE ' + where.join(' AND ');
278
+ sql += ' ORDER BY updated_at DESC LIMIT ?';
279
+ params.push(lim);
280
+ const rows = getDb().prepare(sql).all(...params);
281
+ return rows.map(r => ({
282
+ ...r,
283
+ participants: parseJsonSafe(r.participants, []),
284
+ definition_of_done: parseJsonSafe(r.definition_of_done, []),
285
+ inputs: parseJsonSafe(r.inputs, {}),
286
+ artifacts: parseJsonSafe(r.artifacts, []),
287
+ owned_paths: parseJsonSafe(r.owned_paths, []),
288
+ }));
289
+ } catch (e) {
290
+ logWarn(`TaskBoard listRecentTasks failed: ${e.message}`);
291
+ return [];
292
+ }
293
+ }
294
+
295
+ function listTaskEvents(taskId, limit = 20) {
296
+ const safeTaskId = sanitizeText(taskId, 80);
297
+ if (!safeTaskId) return [];
298
+ const lim = Math.max(1, Math.min(200, Number(limit) || 20));
299
+ try {
300
+ const rows = getDb()
301
+ .prepare('SELECT * FROM task_events WHERE task_id = ? ORDER BY id DESC LIMIT ?')
302
+ .all(safeTaskId, lim);
303
+ return rows.map(r => ({ ...r, body: parseJsonSafe(r.body, {}) }));
304
+ } catch (e) {
305
+ logWarn(`TaskBoard listTaskEvents failed: ${e.message}`);
306
+ return [];
307
+ }
308
+ }
309
+
310
+ function listScopeTasks(scopeId, limit = 30) {
311
+ const safeScopeId = sanitizeText(scopeId, 120);
312
+ if (!safeScopeId) return [];
313
+ const lim = Math.max(1, Math.min(200, Number(limit) || 30));
314
+ try {
315
+ const rows = getDb()
316
+ .prepare('SELECT * FROM tasks WHERE scope_id = ? ORDER BY updated_at DESC LIMIT ?')
317
+ .all(safeScopeId, lim);
318
+ return rows.map(r => ({
319
+ ...r,
320
+ participants: parseJsonSafe(r.participants, []),
321
+ definition_of_done: parseJsonSafe(r.definition_of_done, []),
322
+ inputs: parseJsonSafe(r.inputs, {}),
323
+ artifacts: parseJsonSafe(r.artifacts, []),
324
+ owned_paths: parseJsonSafe(r.owned_paths, []),
325
+ }));
326
+ } catch (e) {
327
+ logWarn(`TaskBoard listScopeTasks failed: ${e.message}`);
328
+ return [];
329
+ }
330
+ }
331
+
332
+ function listScopeParticipants(scopeId) {
333
+ const tasks = listScopeTasks(scopeId, 200);
334
+ const set = new Set();
335
+ for (const t of tasks) {
336
+ const arr = Array.isArray(t.participants) ? t.participants : [];
337
+ for (const p of arr) {
338
+ const v = sanitizeText(p, 80);
339
+ if (v) set.add(v);
340
+ }
341
+ const from = sanitizeText(t.from_agent, 80);
342
+ const to = sanitizeText(t.to_agent, 80);
343
+ if (from) set.add(from);
344
+ if (to) set.add(to);
345
+ }
346
+ return [...set];
347
+ }
348
+
349
+ function markTaskStatus(taskId, status, opts = {}) {
350
+ const task = getTask(taskId);
351
+ if (!task) return { ok: false, error: 'task_not_found' };
352
+ const mergedArtifacts = sanitizeStringArray([...(task.artifacts || []), ...sanitizeStringArray(opts.artifacts || [])], 80, 500);
353
+ const mergedOwned = sanitizeStringArray([...(task.owned_paths || []), ...sanitizeStringArray(opts.owned_paths || [])], 80, 500);
354
+ const next = {
355
+ ...task,
356
+ status: sanitizeText(status, 20) || task.status,
357
+ summary: sanitizeText(opts.summary, 2000) || task.summary || '',
358
+ last_error: sanitizeText(opts.last_error, 2000) || '',
359
+ artifacts: mergedArtifacts,
360
+ owned_paths: mergedOwned,
361
+ updated_at: new Date().toISOString(),
362
+ };
363
+ return upsertTask(next);
364
+ }
365
+
366
+ function addArtifacts(taskId, artifacts) {
367
+ const task = getTask(taskId);
368
+ if (!task) return { ok: false, error: 'task_not_found' };
369
+ const merged = sanitizeStringArray([...(task.artifacts || []), ...sanitizeStringArray(artifacts || [])], 80, 500);
370
+ return upsertTask({ ...task, artifacts: merged, updated_at: new Date().toISOString() });
371
+ }
372
+
373
+ function close() {
374
+ if (!db) return;
375
+ try { db.close(); } catch {}
376
+ db = null;
377
+ }
378
+
379
+ return {
380
+ dbPath,
381
+ upsertTask,
382
+ appendTaskEvent,
383
+ recordHandoff,
384
+ getTask,
385
+ listRecentTasks,
386
+ listScopeTasks,
387
+ listScopeParticipants,
388
+ listTaskEvents,
389
+ markTaskStatus,
390
+ addArtifacts,
391
+ close,
392
+ };
393
+ }
394
+
395
+ module.exports = {
396
+ createTaskBoard,
397
+ DEFAULT_DB_PATH,
398
+ };
@@ -0,0 +1,83 @@
1
+ 'use strict';
2
+
3
+ const test = require('node:test');
4
+ const assert = require('node:assert/strict');
5
+ const fs = require('fs');
6
+ const os = require('os');
7
+ const path = require('path');
8
+
9
+ const { createTaskBoard } = require('./task-board');
10
+
11
+ function newTmpDbPath() {
12
+ const rand = Math.random().toString(36).slice(2, 8);
13
+ return path.join(os.tmpdir(), `metame-task-board-${Date.now()}-${rand}.db`);
14
+ }
15
+
16
+ test('task board upsert/get/list/status flow', () => {
17
+ const dbPath = newTmpDbPath();
18
+ const board = createTaskBoard({ dbPath });
19
+ const taskId = 't_test_001';
20
+
21
+ const up = board.upsertTask({
22
+ task_id: taskId,
23
+ scope_id: 'scope_alpha',
24
+ from_agent: 'assistant',
25
+ to_agent: 'coder',
26
+ goal: 'run tests',
27
+ task_kind: 'team',
28
+ participants: ['assistant', 'coder'],
29
+ definition_of_done: ['all tests pass'],
30
+ inputs: { cwd: '/tmp/project' },
31
+ priority: 'high',
32
+ status: 'queued',
33
+ created_at: '2026-02-25T00:00:00.000Z',
34
+ updated_at: '2026-02-25T00:00:00.000Z',
35
+ });
36
+ assert.equal(up.ok, true);
37
+
38
+ const got = board.getTask(taskId);
39
+ assert.ok(got);
40
+ assert.equal(got.task_kind, 'team');
41
+ assert.equal(got.scope_id, 'scope_alpha');
42
+ assert.equal(got.goal, 'run tests');
43
+ assert.deepEqual(got.definition_of_done, ['all tests pass']);
44
+ assert.deepEqual(got.participants, ['assistant', 'coder']);
45
+
46
+ const ev = board.appendTaskEvent(taskId, 'dispatch_enqueued', 'assistant', { x: 1 });
47
+ assert.equal(ev.ok, true);
48
+ const events = board.listTaskEvents(taskId, 5);
49
+ assert.equal(events.length, 1);
50
+ assert.equal(events[0].event_type, 'dispatch_enqueued');
51
+
52
+ const st = board.markTaskStatus(taskId, 'done', { summary: 'ok', artifacts: ['/tmp/log.txt'] });
53
+ assert.equal(st.ok, true);
54
+ const done = board.getTask(taskId);
55
+ assert.equal(done.status, 'done');
56
+ assert.equal(done.summary, 'ok');
57
+ assert.deepEqual(done.artifacts, ['/tmp/log.txt']);
58
+
59
+ const recent = board.listRecentTasks(5, null, 'team');
60
+ assert.ok(recent.some(t => t.task_id === taskId));
61
+
62
+ const up2 = board.upsertTask({
63
+ task_id: 't_test_002',
64
+ scope_id: 'scope_alpha',
65
+ from_agent: 'coder',
66
+ to_agent: 'reviewer',
67
+ goal: 'review test results',
68
+ task_kind: 'team',
69
+ participants: ['coder', 'reviewer'],
70
+ status: 'queued',
71
+ priority: 'normal',
72
+ created_at: '2026-02-25T00:01:00.000Z',
73
+ updated_at: '2026-02-25T00:01:00.000Z',
74
+ });
75
+ assert.equal(up2.ok, true);
76
+ const scoped = board.listScopeTasks('scope_alpha', 10);
77
+ assert.equal(scoped.length >= 2, true);
78
+ const participants = board.listScopeParticipants('scope_alpha');
79
+ assert.deepEqual(participants.sort(), ['assistant', 'coder', 'reviewer'].sort());
80
+
81
+ board.close();
82
+ try { fs.unlinkSync(dbPath); } catch {}
83
+ });
@@ -0,0 +1,139 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+
5
+ const DEFAULT_USAGE_CATEGORY = 'unknown';
6
+ const USAGE_RETENTION_DAYS_DEFAULT = 30;
7
+
8
+ const USAGE_CATEGORY_ORDER = Object.freeze([
9
+ 'memory',
10
+ 'cognition',
11
+ 'skill_evolution',
12
+ 'heartbeat',
13
+ 'team_task',
14
+ 'chat',
15
+ 'chat_project',
16
+ 'manual_task',
17
+ 'unknown',
18
+ ]);
19
+
20
+ const CORE_USAGE_CATEGORIES = Object.freeze([
21
+ 'memory',
22
+ 'cognition',
23
+ 'skill_evolution',
24
+ 'heartbeat',
25
+ 'team_task',
26
+ ]);
27
+
28
+ const USAGE_CATEGORY_LABEL = Object.freeze({
29
+ memory: '记忆',
30
+ cognition: '认知',
31
+ skill_evolution: '技能演化',
32
+ heartbeat: '心跳任务',
33
+ team_task: '团队任务',
34
+ chat: '对话',
35
+ chat_project: '项目对话',
36
+ manual_task: '手动任务',
37
+ unknown: '未分类',
38
+ });
39
+
40
+ const USAGE_CATEGORY_ALIASES = Object.freeze({
41
+ memory: 'memory',
42
+ recall: 'memory',
43
+ facts: 'memory',
44
+ cognition: 'cognition',
45
+ distill: 'cognition',
46
+ reflection: 'cognition',
47
+ skill_evolution: 'skill_evolution',
48
+ skillevolution: 'skill_evolution',
49
+ 'skill-evolution': 'skill_evolution',
50
+ skill: 'skill_evolution',
51
+ heartbeat: 'heartbeat',
52
+ scheduled_task: 'heartbeat',
53
+ team_task: 'team_task',
54
+ teamtask: 'team_task',
55
+ team: 'team_task',
56
+ chat: 'chat',
57
+ conversation: 'chat',
58
+ chat_project: 'chat_project',
59
+ project_chat: 'chat_project',
60
+ scoped_chat: 'chat_project',
61
+ manual_task: 'manual_task',
62
+ manual: 'manual_task',
63
+ unknown: 'unknown',
64
+ });
65
+
66
+ const warnedUnknownCategories = new Set();
67
+
68
+ function normalizeUsageCategory(rawCategory, opts = {}) {
69
+ const key = String(rawCategory || '').trim().toLowerCase();
70
+ if (!key) return DEFAULT_USAGE_CATEGORY;
71
+ const normalized = USAGE_CATEGORY_ALIASES[key];
72
+ if (normalized) return normalized;
73
+
74
+ if (typeof opts.logger === 'function' && !warnedUnknownCategories.has(key)) {
75
+ warnedUnknownCategories.add(key);
76
+ opts.logger(`Unknown usage category "${key}", fallback to "${DEFAULT_USAGE_CATEGORY}"`);
77
+ }
78
+ return DEFAULT_USAGE_CATEGORY;
79
+ }
80
+
81
+ function classifyTaskUsage(task, context = {}, opts = {}) {
82
+ const fallbackCategory = normalizeUsageCategory(opts.fallbackCategory || 'heartbeat');
83
+ const kind = String(task && task.task_kind ? task.task_kind : '').toLowerCase();
84
+ if (kind === 'team') return 'team_task';
85
+
86
+ const joined = [
87
+ task && task.name,
88
+ task && task.type,
89
+ task && task.prompt,
90
+ task && task._project && task._project.key,
91
+ context && context.skill,
92
+ context && context.prompt,
93
+ ].filter(Boolean).join(' ').toLowerCase();
94
+
95
+ if (!joined) return fallbackCategory;
96
+ if (/\bteam[-_\s]?task\b|团队|协作|handoff|dispatch/.test(joined)) return 'team_task';
97
+ if (/\bskill[-_\s]?(?:evo|evolution|manager|scout)\b|技能演化/.test(joined)) return 'skill_evolution';
98
+ if (/\bmemory(?:-extract)?\b|记忆|facts?|recall|retriev|rag/.test(joined)) return 'memory';
99
+ if (/\bdistill\b|\bcognition\b|认知|反思|洞察/.test(joined)) return 'cognition';
100
+ if (/\bheartbeat\b|提醒|定时|cron|every\s+\d/.test(joined)) return 'heartbeat';
101
+ return fallbackCategory;
102
+ }
103
+
104
+ function projectKeyFromCwd(cwd, homeDir = '') {
105
+ const raw = String(cwd || '').trim();
106
+ if (!raw) return '';
107
+ try {
108
+ const abs = path.resolve(raw);
109
+ const homeAbs = String(homeDir || '').trim() ? path.resolve(homeDir) : '';
110
+ if (homeAbs && abs === homeAbs) return '';
111
+ const base = path.basename(abs);
112
+ return base && base !== '.' && base !== path.sep ? base : '';
113
+ } catch {
114
+ return '';
115
+ }
116
+ }
117
+
118
+ function classifyChatUsage(chatId, opts = {}) {
119
+ const cid = String(chatId || '');
120
+ if (cid.startsWith('_scope_') || cid.startsWith('_agent_')) return 'team_task';
121
+
122
+ const projectScope = String(opts.projectScope || '').trim();
123
+ const projectKey = String(opts.projectKey || '').trim();
124
+ const derivedKey = projectKeyFromCwd(opts.cwd, opts.homeDir);
125
+ if (projectScope || projectKey || derivedKey) return 'chat_project';
126
+ return 'chat';
127
+ }
128
+
129
+ module.exports = {
130
+ DEFAULT_USAGE_CATEGORY,
131
+ USAGE_RETENTION_DAYS_DEFAULT,
132
+ USAGE_CATEGORY_ORDER,
133
+ CORE_USAGE_CATEGORIES,
134
+ USAGE_CATEGORY_LABEL,
135
+ normalizeUsageCategory,
136
+ classifyTaskUsage,
137
+ classifyChatUsage,
138
+ };
139
+