metame-cli 1.5.21 → 1.5.23
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 +3 -2
- package/index.js +25 -24
- package/package.json +1 -1
- package/scripts/agent-intent-shared.js +111 -0
- package/scripts/daemon-agent-commands.js +160 -354
- package/scripts/daemon-agent-intent.js +282 -0
- package/scripts/daemon-agent-lifecycle.js +243 -0
- package/scripts/daemon-agent-workflow.js +295 -0
- package/scripts/daemon-bridges.js +18 -5
- package/scripts/daemon-claude-engine.js +74 -75
- package/scripts/daemon-command-router.js +24 -294
- package/scripts/daemon-prompt-context.js +127 -0
- package/scripts/daemon-reactive-lifecycle.js +138 -6
- package/scripts/daemon-session-commands.js +16 -2
- package/scripts/daemon-session-store.js +104 -0
- package/scripts/daemon-team-workflow.js +146 -0
- package/scripts/daemon.js +14 -3
- package/scripts/docs/hook-config.md +41 -21
- package/scripts/docs/maintenance-manual.md +2 -2
- package/scripts/docs/orphan-files-review.md +1 -1
- package/scripts/docs/pointer-map.md +2 -2
- package/scripts/hooks/intent-agent-capability.js +51 -0
- package/scripts/hooks/intent-doc-router.js +23 -11
- package/scripts/hooks/intent-memory-recall.js +1 -3
- package/scripts/hooks/intent-team-dispatch.js +1 -1
- package/scripts/intent-registry.js +78 -14
- package/scripts/ops-mission-queue.js +101 -36
- package/scripts/ops-reactive-bootstrap.js +86 -0
- package/scripts/resolve-yaml.js +3 -0
- package/scripts/runtime-bootstrap.js +77 -0
- package/scripts/hooks/intent-engine.js +0 -75
- package/scripts/hooks/team-context.js +0 -143
|
@@ -12,16 +12,18 @@
|
|
|
12
12
|
|
|
13
13
|
const { createDocRoute } = require('./doc-router');
|
|
14
14
|
|
|
15
|
+
const AGENT_DOC_PATTERNS = [
|
|
16
|
+
/(?:agent|智能体|机器人|bot).{0,12}(文档|手册|说明|guide)/i,
|
|
17
|
+
/(?:怎么|如何|手册|文档|说明).{0,12}(配置|管理|使用).{0,12}(agent|智能体|机器人|bot)/i,
|
|
18
|
+
/(?:agent|智能体|机器人|bot).{0,12}(怎么|如何).{0,12}(配置|管理|使用)/i,
|
|
19
|
+
];
|
|
20
|
+
|
|
15
21
|
const routes = [
|
|
16
22
|
createDocRoute({
|
|
17
|
-
patterns:
|
|
18
|
-
|
|
19
|
-
/(?:agent|bot|智能体).{0,8}(?:创建|新建|添加|注册|绑定|配置|管理)/i,
|
|
20
|
-
/\b(?:create|add|register|bind|manage|setup|configure)\s+(?:an?\s+)?agent\b/i,
|
|
21
|
-
],
|
|
22
|
-
title: 'Agent 管理提示',
|
|
23
|
+
patterns: AGENT_DOC_PATTERNS,
|
|
24
|
+
title: 'Agent 文档提示',
|
|
23
25
|
docPath: '~/.metame/docs/agent-guide.md',
|
|
24
|
-
summary: '
|
|
26
|
+
summary: 'Agent 配置/管理/使用说明',
|
|
25
27
|
}),
|
|
26
28
|
createDocRoute({
|
|
27
29
|
patterns: [
|
|
@@ -34,8 +36,8 @@ const routes = [
|
|
|
34
36
|
}),
|
|
35
37
|
createDocRoute({
|
|
36
38
|
patterns: [
|
|
37
|
-
/(?:hook|intent|意图).{0,10}(
|
|
38
|
-
/(
|
|
39
|
+
/(?:hook|intent|意图).{0,10}(?:配置|设置|开关|新增|添加|修改|怎么配|怎么设置|怎么改|原理)/i,
|
|
40
|
+
/(?:配置|设置|开关|新增|添加|修改|原理).{0,10}(?:hook|intent|意图)/i,
|
|
39
41
|
/intent.?engine/i,
|
|
40
42
|
/意图引擎|意图模块/,
|
|
41
43
|
],
|
|
@@ -45,10 +47,20 @@ const routes = [
|
|
|
45
47
|
}),
|
|
46
48
|
];
|
|
47
49
|
|
|
48
|
-
|
|
50
|
+
function hasExplicitDocIntent(prompt) {
|
|
51
|
+
const text = String(prompt || '').trim();
|
|
52
|
+
if (!text) return false;
|
|
53
|
+
return AGENT_DOC_PATTERNS.some((pattern) => pattern.test(text)) ||
|
|
54
|
+
/(?:文档|手册|说明|guide|readme)/i.test(text);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function detectDocRouter(prompt) {
|
|
49
58
|
const hints = routes
|
|
50
59
|
.map((detect) => detect(prompt))
|
|
51
60
|
.filter(Boolean);
|
|
52
61
|
|
|
53
62
|
return hints.length ? hints.join('\n') : null;
|
|
54
|
-
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = detectDocRouter;
|
|
66
|
+
module.exports.hasExplicitDocIntent = hasExplicitDocIntent;
|
|
@@ -17,8 +17,6 @@ const RECALL_PATTERNS = [
|
|
|
17
17
|
/之前.{0,4}(?:说过|讨论过|聊过|提到过|商量过|做过的)/,
|
|
18
18
|
// "还记得/记不记得" — asking if AI remembers (exclude "你记得" which is often imperative)
|
|
19
19
|
/(?:还记得|记不记得|记得吗)/,
|
|
20
|
-
// "之前那个/上次那个" — referencing past artifacts
|
|
21
|
-
/(?:之前|上次|前几天)那个/,
|
|
22
20
|
// English recall patterns
|
|
23
21
|
/\b(?:last time|previously|remember when|do you remember|earlier we)\b/i,
|
|
24
22
|
];
|
|
@@ -31,6 +29,6 @@ module.exports = function detectMemoryRecall(prompt) {
|
|
|
31
29
|
'- 搜索记忆: `node ~/.metame/memory-search.js "关键词1" "keyword2"`',
|
|
32
30
|
'- 一次传 3-4 个关键词(中文+英文+函数名)',
|
|
33
31
|
'- `--facts` 只搜事实,`--sessions` 只搜会话',
|
|
34
|
-
'-
|
|
32
|
+
'- 不要假设工作区里存在 `./memory` 模块;优先走 `memory-search.js` CLI 做召回',
|
|
35
33
|
].join('\n');
|
|
36
34
|
};
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Team Dispatch Intent Module
|
|
5
5
|
*
|
|
6
6
|
* Detects communication intent towards team members in the prompt.
|
|
7
|
-
*
|
|
7
|
+
* Daemon-side intent module for team dispatch hints.
|
|
8
8
|
*
|
|
9
9
|
* @param {string} prompt - sanitized user prompt
|
|
10
10
|
* @param {object} config - daemon.yaml config
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const detectDocRouter = require('./hooks/intent-doc-router');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* Shared intent registry for all MetaMe runtime adapters.
|
|
5
7
|
*
|
|
@@ -8,9 +10,9 @@
|
|
|
8
10
|
*/
|
|
9
11
|
|
|
10
12
|
const DEFAULTS = Object.freeze({
|
|
13
|
+
agent_capability: true,
|
|
11
14
|
team_dispatch: true,
|
|
12
15
|
ops_assist: true,
|
|
13
|
-
task_create: true,
|
|
14
16
|
file_transfer: true,
|
|
15
17
|
weixin_bridge: true,
|
|
16
18
|
memory_recall: true,
|
|
@@ -20,17 +22,54 @@ const DEFAULTS = Object.freeze({
|
|
|
20
22
|
});
|
|
21
23
|
|
|
22
24
|
const INTENT_MODULES = Object.freeze({
|
|
25
|
+
agent_capability: {
|
|
26
|
+
detect: require('./hooks/intent-agent-capability'),
|
|
27
|
+
priority: 100,
|
|
28
|
+
},
|
|
23
29
|
team_dispatch: require('./hooks/intent-team-dispatch'),
|
|
24
|
-
ops_assist:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
ops_assist: {
|
|
31
|
+
detect: require('./hooks/intent-ops-assist'),
|
|
32
|
+
priority: 80,
|
|
33
|
+
},
|
|
34
|
+
file_transfer: {
|
|
35
|
+
detect: require('./hooks/intent-file-transfer'),
|
|
36
|
+
priority: 95,
|
|
37
|
+
},
|
|
38
|
+
weixin_bridge: {
|
|
39
|
+
detect: require('./hooks/intent-weixin-bridge'),
|
|
40
|
+
priority: 90,
|
|
41
|
+
},
|
|
42
|
+
memory_recall: {
|
|
43
|
+
detect: require('./hooks/intent-memory-recall'),
|
|
44
|
+
priority: 70,
|
|
45
|
+
},
|
|
46
|
+
doc_router: {
|
|
47
|
+
detect: detectDocRouter,
|
|
48
|
+
priority: 10,
|
|
49
|
+
fallbackOnly: true,
|
|
50
|
+
},
|
|
51
|
+
perpetual: {
|
|
52
|
+
detect: require('./hooks/intent-perpetual'),
|
|
53
|
+
priority: 60,
|
|
54
|
+
},
|
|
55
|
+
research: {
|
|
56
|
+
detect: require('./hooks/intent-research'),
|
|
57
|
+
priority: 55,
|
|
58
|
+
},
|
|
32
59
|
});
|
|
33
60
|
|
|
61
|
+
const DEFAULT_MAX_HINTS = 2;
|
|
62
|
+
const DEFAULT_MAX_HINT_CHARS = 1200;
|
|
63
|
+
|
|
64
|
+
function normalizeIntentModule(entry) {
|
|
65
|
+
if (typeof entry === 'function') return { detect: entry, priority: 50, fallbackOnly: false };
|
|
66
|
+
return {
|
|
67
|
+
detect: entry.detect,
|
|
68
|
+
priority: Number.isFinite(entry.priority) ? entry.priority : 50,
|
|
69
|
+
fallbackOnly: !!entry.fallbackOnly,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
34
73
|
function resolveEnabledIntents(config = {}) {
|
|
35
74
|
const hooksCfg = (config.hooks && typeof config.hooks === 'object') ? config.hooks : {};
|
|
36
75
|
return { ...DEFAULTS, ...hooksCfg };
|
|
@@ -42,18 +81,43 @@ function collectIntentHints(prompt, config = {}, projectKey = '') {
|
|
|
42
81
|
|
|
43
82
|
const enabled = resolveEnabledIntents(config);
|
|
44
83
|
const hints = [];
|
|
45
|
-
for (const [key,
|
|
84
|
+
for (const [key, rawEntry] of Object.entries(INTENT_MODULES)) {
|
|
46
85
|
if (enabled[key] === false) continue;
|
|
47
|
-
const
|
|
86
|
+
const entry = normalizeIntentModule(rawEntry);
|
|
87
|
+
const hint = entry.detect(text, config, projectKey);
|
|
48
88
|
if (hint) hints.push({ key, hint });
|
|
49
89
|
}
|
|
50
90
|
return hints;
|
|
51
91
|
}
|
|
52
92
|
|
|
53
93
|
function buildIntentHintBlock(prompt, config = {}, projectKey = '') {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
94
|
+
const maxHints = Number.isInteger(config && config.intent_max_hints) && config.intent_max_hints > 0
|
|
95
|
+
? config.intent_max_hints
|
|
96
|
+
: DEFAULT_MAX_HINTS;
|
|
97
|
+
const maxChars = Number.isInteger(config && config.intent_max_hint_chars) && config.intent_max_hint_chars > 0
|
|
98
|
+
? config.intent_max_hint_chars
|
|
99
|
+
: DEFAULT_MAX_HINT_CHARS;
|
|
100
|
+
|
|
101
|
+
let hits = collectIntentHints(prompt, config, projectKey)
|
|
102
|
+
.map((item) => ({ ...item, ...normalizeIntentModule(INTENT_MODULES[item.key]) }));
|
|
103
|
+
|
|
104
|
+
if (hits.some(item => !item.fallbackOnly) && !(typeof detectDocRouter.hasExplicitDocIntent === 'function' && detectDocRouter.hasExplicitDocIntent(prompt))) {
|
|
105
|
+
hits = hits.filter(item => !item.fallbackOnly);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
hits.sort((a, b) => b.priority - a.priority);
|
|
109
|
+
|
|
110
|
+
const selected = [];
|
|
111
|
+
let usedChars = 0;
|
|
112
|
+
for (const item of hits) {
|
|
113
|
+
if (selected.length >= maxHints) break;
|
|
114
|
+
const nextChars = usedChars + item.hint.length;
|
|
115
|
+
if (selected.length > 0 && nextChars > maxChars) continue;
|
|
116
|
+
selected.push(item);
|
|
117
|
+
usedChars = nextChars;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return selected.map(item => item.hint).join('\n\n');
|
|
57
121
|
}
|
|
58
122
|
|
|
59
123
|
module.exports = {
|
|
@@ -15,7 +15,12 @@ const os = require('os');
|
|
|
15
15
|
|
|
16
16
|
const MISSIONS_FILE = 'workspace/missions.md';
|
|
17
17
|
const SECTIONS = ['pending', 'active', 'completed', 'abandoned'];
|
|
18
|
-
const
|
|
18
|
+
const RECENT_LOG_LINES = 500;
|
|
19
|
+
const ERROR_THRESHOLD = 3;
|
|
20
|
+
|
|
21
|
+
function getMetameDir() {
|
|
22
|
+
return process.env.METAME_DIR || path.join(os.homedir(), '.metame');
|
|
23
|
+
}
|
|
19
24
|
|
|
20
25
|
// ── Mission file parser (same format as topic-pool) ──────────
|
|
21
26
|
|
|
@@ -86,6 +91,90 @@ function findMission(sections, id) {
|
|
|
86
91
|
return null;
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
function normalizeLogLine(line) {
|
|
95
|
+
return String(line || '')
|
|
96
|
+
.replace(/\d{4}-\d{2}-\d{2}T[\d:.]+Z?/g, '<TS>')
|
|
97
|
+
.replace(/\d{10,}/g, '<ID>')
|
|
98
|
+
.replace(/\/tmp\/[^\s]+/g, '<TMP>')
|
|
99
|
+
.trim();
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function collectRecurringErrors() {
|
|
103
|
+
const counts = {};
|
|
104
|
+
const logPath = path.join(getMetameDir(), 'daemon.log');
|
|
105
|
+
if (!fs.existsSync(logPath)) return counts;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
const content = fs.readFileSync(logPath, 'utf8');
|
|
109
|
+
const recent = content.split('\n').slice(-RECENT_LOG_LINES);
|
|
110
|
+
for (const line of recent) {
|
|
111
|
+
if (!/\bERR\b|\bWARN\b|\bError\b|\bfailed\b/i.test(line)) continue;
|
|
112
|
+
const normalized = normalizeLogLine(line);
|
|
113
|
+
if (normalized.length < 20) continue;
|
|
114
|
+
const key = normalized.slice(0, 120);
|
|
115
|
+
counts[key] = (counts[key] || 0) + 1;
|
|
116
|
+
}
|
|
117
|
+
} catch { /* ignore unreadable log */ }
|
|
118
|
+
|
|
119
|
+
return counts;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function parseRecurringErrorTitle(title) {
|
|
123
|
+
const m = String(title || '').match(/^Fix recurring error(?:\s*\(\d+x\))?:\s*(.+)$/i);
|
|
124
|
+
return m ? m[1].trim() : '';
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function shouldKeepMission(title, cwd, recurringErrors, now) {
|
|
128
|
+
const recurringPattern = parseRecurringErrorTitle(title);
|
|
129
|
+
if (recurringPattern) {
|
|
130
|
+
return (recurringErrors[recurringPattern] || 0) >= ERROR_THRESHOLD;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const testMatch = String(title || '').match(/^Fix failing tests in (.+)$/i);
|
|
134
|
+
if (testMatch) {
|
|
135
|
+
const testName = testMatch[1].trim();
|
|
136
|
+
const testPath = path.join(cwd, 'scripts', testName);
|
|
137
|
+
if (!fs.existsSync(testPath)) return false;
|
|
138
|
+
try {
|
|
139
|
+
execSyncSafe(`node --test scripts/${testName}`, cwd, 30000);
|
|
140
|
+
return false;
|
|
141
|
+
} catch {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const staleMatch = String(title || '').match(/^Investigate stale project:\s*(.+)$/i);
|
|
147
|
+
if (staleMatch) {
|
|
148
|
+
const projectKey = staleMatch[1].trim();
|
|
149
|
+
const fp = path.join(getMetameDir(), 'events', `${projectKey}.jsonl`);
|
|
150
|
+
if (!fs.existsSync(fp)) return false;
|
|
151
|
+
try {
|
|
152
|
+
const staleHours = (now - fs.statSync(fp).mtimeMs) / (60 * 60 * 1000);
|
|
153
|
+
return staleHours > 48;
|
|
154
|
+
} catch {
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function pruneObsoleteMissions(cwd) {
|
|
163
|
+
const sections = readSections(cwd);
|
|
164
|
+
const recurringErrors = collectRecurringErrors();
|
|
165
|
+
const now = Date.now();
|
|
166
|
+
const pruned = [];
|
|
167
|
+
|
|
168
|
+
sections.pending = sections.pending.filter((mission) => {
|
|
169
|
+
const keep = shouldKeepMission(mission.title, cwd, recurringErrors, now);
|
|
170
|
+
if (!keep) pruned.push(mission);
|
|
171
|
+
return keep;
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
if (pruned.length > 0) writeSections(cwd, sections);
|
|
175
|
+
return { success: true, pruned: pruned.length, pruned_ids: pruned.map(m => m.id) };
|
|
176
|
+
}
|
|
177
|
+
|
|
89
178
|
// ── Standard queue commands ──────────────────────────────────
|
|
90
179
|
|
|
91
180
|
function nextMission(cwd) {
|
|
@@ -134,6 +223,7 @@ function listMissions(cwd) {
|
|
|
134
223
|
// ── Log scanner: generates missions from error patterns ──────
|
|
135
224
|
|
|
136
225
|
function scanLogs(cwd) {
|
|
226
|
+
const pruneResult = pruneObsoleteMissions(cwd);
|
|
137
227
|
const sections = readSections(cwd);
|
|
138
228
|
const existingTitles = new Set(
|
|
139
229
|
[...sections.pending, ...sections.active, ...sections.completed, ...sections.abandoned].map(t => t.title)
|
|
@@ -141,44 +231,18 @@ function scanLogs(cwd) {
|
|
|
141
231
|
|
|
142
232
|
const newMissions = [];
|
|
143
233
|
const now = Date.now();
|
|
144
|
-
const
|
|
234
|
+
const recurringErrors = collectRecurringErrors();
|
|
145
235
|
|
|
146
236
|
// 1. Scan daemon log for repeated errors
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
// Only look at recent lines (last 500)
|
|
153
|
-
const recent = lines.slice(-500);
|
|
154
|
-
const errorCounts = {};
|
|
155
|
-
for (const line of recent) {
|
|
156
|
-
if (!/\bERR\b|\bWARN\b|\bError\b|\bfailed\b/i.test(line)) continue;
|
|
157
|
-
// Normalize: strip timestamps and variable data
|
|
158
|
-
const normalized = line
|
|
159
|
-
.replace(/\d{4}-\d{2}-\d{2}T[\d:.]+Z?/g, '<TS>')
|
|
160
|
-
.replace(/\d{10,}/g, '<ID>')
|
|
161
|
-
.replace(/\/tmp\/[^\s]+/g, '<TMP>')
|
|
162
|
-
.trim();
|
|
163
|
-
if (normalized.length < 20) continue;
|
|
164
|
-
const key = normalized.slice(0, 120);
|
|
165
|
-
errorCounts[key] = (errorCounts[key] || 0) + 1;
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// Generate missions for errors occurring 3+ times
|
|
169
|
-
// Dedup key is the pattern alone (not the count, which changes every scan)
|
|
170
|
-
for (const [pattern, count] of Object.entries(errorCounts)) {
|
|
171
|
-
if (count < 3) continue;
|
|
172
|
-
const dedupKey = `Fix recurring error: ${pattern.slice(0, 80)}`;
|
|
173
|
-
if (existingTitles.has(dedupKey)) continue;
|
|
174
|
-
// Use stable dedup key as title (count excluded to prevent duplicates)
|
|
175
|
-
newMissions.push({ title: dedupKey, priority: Math.max(1, 10 - count) });
|
|
176
|
-
}
|
|
177
|
-
} catch { /* skip unreadable log */ }
|
|
237
|
+
for (const [pattern, count] of Object.entries(recurringErrors)) {
|
|
238
|
+
if (count < ERROR_THRESHOLD) continue;
|
|
239
|
+
const dedupKey = `Fix recurring error: ${pattern.slice(0, 80)}`;
|
|
240
|
+
if (existingTitles.has(dedupKey)) continue;
|
|
241
|
+
newMissions.push({ title: dedupKey, priority: Math.max(1, 10 - count) });
|
|
178
242
|
}
|
|
179
243
|
|
|
180
244
|
// 2. Scan event logs for stuck/stale projects
|
|
181
|
-
const eventsDir = path.join(
|
|
245
|
+
const eventsDir = path.join(getMetameDir(), 'events');
|
|
182
246
|
if (fs.existsSync(eventsDir)) {
|
|
183
247
|
try {
|
|
184
248
|
const evFiles = fs.readdirSync(eventsDir).filter(f => f.endsWith('.jsonl'));
|
|
@@ -248,11 +312,12 @@ if (require.main === module) {
|
|
|
248
312
|
case 'activate': result = args[0] ? activateMission(cwd, args[0]) : { success: false, message: 'usage: activate <id>' }; break;
|
|
249
313
|
case 'complete': result = args[0] ? completeMission(cwd, args[0]) : { success: false, message: 'usage: complete <id>' }; break;
|
|
250
314
|
case 'list': result = listMissions(cwd); break;
|
|
315
|
+
case 'prune': result = pruneObsoleteMissions(cwd); break;
|
|
251
316
|
case 'scan': result = scanLogs(cwd); break;
|
|
252
|
-
default: result = { success: false, message: `unknown: ${command}. Available: next, activate, complete, list, scan` };
|
|
317
|
+
default: result = { success: false, message: `unknown: ${command}. Available: next, activate, complete, list, prune, scan` };
|
|
253
318
|
}
|
|
254
319
|
|
|
255
320
|
process.stdout.write(JSON.stringify(result) + '\n');
|
|
256
321
|
}
|
|
257
322
|
|
|
258
|
-
module.exports = { nextMission, activateMission, completeMission, listMissions, scanLogs };
|
|
323
|
+
module.exports = { nextMission, activateMission, completeMission, listMissions, pruneObsoleteMissions, scanLogs };
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const { execFileSync } = require('child_process');
|
|
7
|
+
|
|
8
|
+
const yaml = require('./resolve-yaml');
|
|
9
|
+
const { pruneObsoleteMissions, scanLogs } = require('./ops-mission-queue');
|
|
10
|
+
const { bootstrapReactiveProject } = require('./daemon-reactive-lifecycle');
|
|
11
|
+
|
|
12
|
+
const HOME = os.homedir();
|
|
13
|
+
const METAME_DIR = path.join(HOME, '.metame');
|
|
14
|
+
const CONFIG_PATH = path.join(METAME_DIR, 'daemon.yaml');
|
|
15
|
+
const STATE_PATH = path.join(METAME_DIR, 'daemon_state.json');
|
|
16
|
+
const PROJECT_KEY = 'metame_ops';
|
|
17
|
+
|
|
18
|
+
function loadConfig() {
|
|
19
|
+
return yaml.load(fs.readFileSync(CONFIG_PATH, 'utf8')) || {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function loadState() {
|
|
23
|
+
try {
|
|
24
|
+
return JSON.parse(fs.readFileSync(STATE_PATH, 'utf8'));
|
|
25
|
+
} catch {
|
|
26
|
+
return { reactive: {}, budget: { tokens_used: 0 } };
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function saveState(state) {
|
|
31
|
+
fs.writeFileSync(STATE_PATH, JSON.stringify(state, null, 2));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function checkBudget(config, state) {
|
|
35
|
+
const budget = config?.daemon?.daily_token_budget;
|
|
36
|
+
const used = Number(state?.budget?.tokens_used || 0);
|
|
37
|
+
if (!Number.isFinite(budget) || budget <= 0) return true;
|
|
38
|
+
return used < budget;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function dispatchReactiveItem(item) {
|
|
42
|
+
const dispatchBin = path.join(METAME_DIR, 'bin', 'dispatch_to');
|
|
43
|
+
const args = [dispatchBin];
|
|
44
|
+
if (item.new_session) args.push('--new');
|
|
45
|
+
if (item.from) args.push('--from', item.from);
|
|
46
|
+
args.push(item.target, item.prompt);
|
|
47
|
+
execFileSync(args[0], args.slice(1), {
|
|
48
|
+
encoding: 'utf8',
|
|
49
|
+
timeout: 30000,
|
|
50
|
+
env: process.env,
|
|
51
|
+
});
|
|
52
|
+
return { success: true };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function main() {
|
|
56
|
+
const config = loadConfig();
|
|
57
|
+
const project = config?.projects?.[PROJECT_KEY];
|
|
58
|
+
if (!project || !project.cwd) {
|
|
59
|
+
process.stdout.write(JSON.stringify({ success: false, skipped: true, reason: 'project_missing' }) + '\n');
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cwd = project.cwd.replace(/^~/, HOME);
|
|
64
|
+
const pruned = pruneObsoleteMissions(cwd);
|
|
65
|
+
const scanned = scanLogs(cwd);
|
|
66
|
+
|
|
67
|
+
const result = bootstrapReactiveProject(PROJECT_KEY, config, {
|
|
68
|
+
metameDir: METAME_DIR,
|
|
69
|
+
loadState,
|
|
70
|
+
saveState,
|
|
71
|
+
checkBudget,
|
|
72
|
+
handleDispatchItem: dispatchReactiveItem,
|
|
73
|
+
log: () => {},
|
|
74
|
+
notifyUser: () => {},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
process.stdout.write(JSON.stringify({
|
|
78
|
+
success: true,
|
|
79
|
+
pruned: pruned.pruned || 0,
|
|
80
|
+
new_missions: scanned.new_missions || 0,
|
|
81
|
+
total_pending: scanned.total_pending || 0,
|
|
82
|
+
bootstrap: result,
|
|
83
|
+
}) + '\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (require.main === module) main();
|
package/scripts/resolve-yaml.js
CHANGED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const Module = require('module');
|
|
7
|
+
|
|
8
|
+
function readRuntimeEnvFile(baseDir) {
|
|
9
|
+
const runtimeFile = path.join(baseDir, 'runtime-env.json');
|
|
10
|
+
try {
|
|
11
|
+
if (!fs.existsSync(runtimeFile)) return null;
|
|
12
|
+
return JSON.parse(fs.readFileSync(runtimeFile, 'utf8'));
|
|
13
|
+
} catch {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function collectNodeModuleCandidates(baseDir) {
|
|
19
|
+
const candidates = [];
|
|
20
|
+
const runtimeEnv = readRuntimeEnvFile(baseDir);
|
|
21
|
+
const metameRoot = String(process.env.METAME_ROOT || runtimeEnv?.metameRoot || '').trim();
|
|
22
|
+
const runtimeNodeModules = String(runtimeEnv?.nodeModules || '').trim();
|
|
23
|
+
|
|
24
|
+
if (runtimeNodeModules) candidates.push(runtimeNodeModules);
|
|
25
|
+
if (metameRoot) candidates.push(path.join(metameRoot, 'node_modules'));
|
|
26
|
+
|
|
27
|
+
candidates.push(path.join(baseDir, 'node_modules'));
|
|
28
|
+
candidates.push(path.resolve(baseDir, '..', 'node_modules'));
|
|
29
|
+
|
|
30
|
+
const home = process.env.HOME || process.env.USERPROFILE || os.homedir();
|
|
31
|
+
if (home) candidates.push(path.join(home, '.metame', 'node_modules'));
|
|
32
|
+
|
|
33
|
+
return [...new Set(candidates.filter(Boolean))];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function bootstrapRuntimeModulePaths(baseDir = __dirname) {
|
|
37
|
+
const runtimeEnv = readRuntimeEnvFile(baseDir);
|
|
38
|
+
if (!process.env.METAME_ROOT && runtimeEnv?.metameRoot) {
|
|
39
|
+
process.env.METAME_ROOT = runtimeEnv.metameRoot;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const existingNodePath = String(process.env.NODE_PATH || '')
|
|
43
|
+
.split(path.delimiter)
|
|
44
|
+
.map(p => p.trim())
|
|
45
|
+
.filter(Boolean);
|
|
46
|
+
|
|
47
|
+
let updated = false;
|
|
48
|
+
for (const candidate of collectNodeModuleCandidates(baseDir)) {
|
|
49
|
+
try {
|
|
50
|
+
if (!fs.existsSync(candidate) || !fs.statSync(candidate).isDirectory()) continue;
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (!existingNodePath.includes(candidate)) {
|
|
55
|
+
existingNodePath.unshift(candidate);
|
|
56
|
+
updated = true;
|
|
57
|
+
}
|
|
58
|
+
if (!Module.globalPaths.includes(candidate)) {
|
|
59
|
+
Module.globalPaths.unshift(candidate);
|
|
60
|
+
updated = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (updated) {
|
|
65
|
+
process.env.NODE_PATH = existingNodePath.join(path.delimiter);
|
|
66
|
+
if (typeof Module._initPaths === 'function') Module._initPaths();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
metameRoot: process.env.METAME_ROOT || '',
|
|
71
|
+
nodePath: process.env.NODE_PATH || '',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = {
|
|
76
|
+
bootstrapRuntimeModulePaths,
|
|
77
|
+
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* MetaMe Intent Engine — Unified UserPromptSubmit Hook
|
|
5
|
-
*
|
|
6
|
-
* Config-driven intent dispatcher. Replaces the standalone team-context.js hook.
|
|
7
|
-
* Each intent module detects a specific pattern and returns a hint string or null.
|
|
8
|
-
* Only injects an additionalSystemPrompt when at least one intent fires.
|
|
9
|
-
*
|
|
10
|
-
* Enabled intents are controlled via daemon.yaml `hooks:` section:
|
|
11
|
-
*
|
|
12
|
-
* hooks:
|
|
13
|
-
* team_dispatch: true # team member communication hints (default: on)
|
|
14
|
-
* ops_assist: true # /undo /restart /logs etc. hints (default: on)
|
|
15
|
-
* task_create: true # task scheduling hints (default: on)
|
|
16
|
-
*
|
|
17
|
-
* Set any key to false to disable that intent module.
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
'use strict';
|
|
21
|
-
|
|
22
|
-
// Global safety net: hooks must NEVER crash or exit non-zero
|
|
23
|
-
process.on('uncaughtException', () => process.exit(0));
|
|
24
|
-
process.on('unhandledRejection', () => process.exit(0));
|
|
25
|
-
|
|
26
|
-
const fs = require('fs');
|
|
27
|
-
const path = require('path');
|
|
28
|
-
const os = require('os');
|
|
29
|
-
|
|
30
|
-
const METAME_DIR = path.join(os.homedir(), '.metame');
|
|
31
|
-
const { sanitizePrompt, isInternalPrompt } = require('./hook-utils');
|
|
32
|
-
const { buildIntentHintBlock } = require('../intent-registry');
|
|
33
|
-
|
|
34
|
-
function exit() { process.exit(0); }
|
|
35
|
-
|
|
36
|
-
let raw = '';
|
|
37
|
-
process.stdin.setEncoding('utf8');
|
|
38
|
-
process.stdin.on('data', c => { raw += c; });
|
|
39
|
-
process.stdin.on('end', () => {
|
|
40
|
-
try { run(JSON.parse(raw)); } catch { exit(); }
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
function run(data) {
|
|
44
|
-
// Internal daemon subprocesses set this env flag — never inject hints into them
|
|
45
|
-
if (process.env.METAME_INTERNAL_PROMPT === '1') return exit();
|
|
46
|
-
|
|
47
|
-
const projectKey = process.env.METAME_PROJECT || '';
|
|
48
|
-
const rawPrompt = (data.prompt || data.user_prompt || '').trim();
|
|
49
|
-
if (!rawPrompt) return exit();
|
|
50
|
-
|
|
51
|
-
// Strip daemon-injected blocks, then bail if this is a system prompt
|
|
52
|
-
if (isInternalPrompt(rawPrompt)) return exit();
|
|
53
|
-
const prompt = sanitizePrompt(rawPrompt);
|
|
54
|
-
if (!prompt) return exit();
|
|
55
|
-
|
|
56
|
-
// Load daemon.yaml config (graceful: intents that don't need config still run)
|
|
57
|
-
let config = {};
|
|
58
|
-
try {
|
|
59
|
-
const yaml = require('../resolve-yaml');
|
|
60
|
-
config = yaml.load(fs.readFileSync(path.join(METAME_DIR, 'daemon.yaml'), 'utf8')) || {};
|
|
61
|
-
} catch { /* proceed with defaults */ }
|
|
62
|
-
|
|
63
|
-
let intentBlock = '';
|
|
64
|
-
try {
|
|
65
|
-
intentBlock = buildIntentHintBlock(prompt, config, projectKey);
|
|
66
|
-
} catch {
|
|
67
|
-
return exit();
|
|
68
|
-
}
|
|
69
|
-
if (!intentBlock) return exit();
|
|
70
|
-
|
|
71
|
-
process.stdout.write(JSON.stringify({
|
|
72
|
-
hookSpecificOutput: { additionalContext: intentBlock },
|
|
73
|
-
}));
|
|
74
|
-
exit();
|
|
75
|
-
}
|