@myvillage/cli 1.23.2 → 1.26.0
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@myvillage/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "MyVillageOS CLI for community developers",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"author": "MyVillage Project",
|
|
27
27
|
"license": "MIT",
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@ai-sdk/anthropic": "^
|
|
29
|
+
"@ai-sdk/anthropic": "^3.0.78",
|
|
30
30
|
"@ai-sdk/mcp": "^1.0.42",
|
|
31
31
|
"@ai-sdk/openai": "^3.0.33",
|
|
32
32
|
"ai": "^6.0.0",
|
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
// It receives the agent name as a command-line argument,
|
|
5
5
|
// writes a PID file, runs the agent loop, and cleans up.
|
|
6
6
|
|
|
7
|
+
// Surface silent killers (uncaught exceptions, unhandled rejections, and
|
|
8
|
+
// event-loop-emptied exits) to the captured stderr file so a failed start
|
|
9
|
+
// always leaves a diagnostic trail.
|
|
10
|
+
process.on('uncaughtException', (err) => {
|
|
11
|
+
process.stderr.write(`[daemon-entry] UNCAUGHT EXCEPTION: ${err?.stack || err}\n`);
|
|
12
|
+
});
|
|
13
|
+
process.on('unhandledRejection', (reason) => {
|
|
14
|
+
process.stderr.write(`[daemon-entry] UNHANDLED REJECTION: ${reason?.stack || reason}\n`);
|
|
15
|
+
});
|
|
16
|
+
process.on('beforeExit', (code) => {
|
|
17
|
+
process.stderr.write(`[daemon-entry] BEFORE EXIT code=${code} — event loop emptied (likely a hung await inside agent setup)\n`);
|
|
18
|
+
});
|
|
19
|
+
|
|
7
20
|
import { writeFileSync, unlinkSync, existsSync, mkdirSync, appendFileSync } from 'fs';
|
|
8
21
|
import { join } from 'path';
|
|
9
22
|
import { homedir } from 'os';
|
|
@@ -5,16 +5,35 @@
|
|
|
5
5
|
import { fork } from 'child_process';
|
|
6
6
|
import { join, dirname } from 'path';
|
|
7
7
|
import { fileURLToPath } from 'url';
|
|
8
|
-
import { existsSync, readFileSync, unlinkSync } from 'fs';
|
|
8
|
+
import { existsSync, readFileSync, unlinkSync, openSync, mkdirSync } from 'fs';
|
|
9
9
|
import { homedir } from 'os';
|
|
10
10
|
|
|
11
11
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
12
12
|
|
|
13
|
+
export function getDaemonStderrPath(agentName) {
|
|
14
|
+
return join(homedir(), '.myvillage', 'agents', agentName, 'logs', 'daemon-stderr.log');
|
|
15
|
+
}
|
|
16
|
+
|
|
13
17
|
export function startDaemon(agentName) {
|
|
14
18
|
const entryScript = join(__dirname, 'daemon-entry.js');
|
|
19
|
+
|
|
20
|
+
// Capture daemon stderr to a file so crashes that happen BEFORE the loop
|
|
21
|
+
// starts logging (e.g. ESM module-resolution errors at top-level import)
|
|
22
|
+
// are visible. Without this, `stdio: 'ignore'` silently discarded the
|
|
23
|
+
// only signal of what went wrong, leaving the developer with a blank
|
|
24
|
+
// log file and no error.
|
|
25
|
+
const logsDir = join(homedir(), '.myvillage', 'agents', agentName, 'logs');
|
|
26
|
+
if (!existsSync(logsDir)) {
|
|
27
|
+
mkdirSync(logsDir, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
const stderrPath = getDaemonStderrPath(agentName);
|
|
30
|
+
// Open with 'w' to truncate so stale crashes from previous starts don't
|
|
31
|
+
// mislead the diagnostic.
|
|
32
|
+
const stderrFd = openSync(stderrPath, 'w');
|
|
33
|
+
|
|
15
34
|
const child = fork(entryScript, [agentName], {
|
|
16
35
|
detached: true,
|
|
17
|
-
stdio: 'ignore',
|
|
36
|
+
stdio: ['ignore', 'ignore', stderrFd, 'ipc'],
|
|
18
37
|
});
|
|
19
38
|
child.unref();
|
|
20
39
|
return child.pid;
|
|
@@ -207,8 +207,9 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
207
207
|
type: 'llm_response',
|
|
208
208
|
text: (result.text || '').slice(0, 500),
|
|
209
209
|
tokensUsed: {
|
|
210
|
-
prompt: result.usage?.
|
|
211
|
-
completion: result.usage?.
|
|
210
|
+
prompt: result.usage?.inputTokens || 0,
|
|
211
|
+
completion: result.usage?.outputTokens || 0,
|
|
212
|
+
total: result.usage?.totalTokens || 0,
|
|
212
213
|
},
|
|
213
214
|
});
|
|
214
215
|
|
|
@@ -313,7 +314,7 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
313
314
|
toolErrors: taskActionAudit.toolErrors,
|
|
314
315
|
note: 'Marked FAILED because the action tools did not succeed. The model\'s text may claim success but the underlying tool calls errored.',
|
|
315
316
|
},
|
|
316
|
-
tokensUsed:
|
|
317
|
+
tokensUsed: result.usage?.totalTokens || 0,
|
|
317
318
|
durationMs: Date.now() - loopStart,
|
|
318
319
|
});
|
|
319
320
|
logActivity(agentDir, {
|
|
@@ -328,7 +329,7 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
328
329
|
toolCalls: activity.toolCalls,
|
|
329
330
|
toolErrors: taskActionAudit.toolErrors.length > 0 ? taskActionAudit.toolErrors : undefined,
|
|
330
331
|
},
|
|
331
|
-
tokensUsed:
|
|
332
|
+
tokensUsed: result.usage?.totalTokens || 0,
|
|
332
333
|
durationMs: Date.now() - loopStart,
|
|
333
334
|
});
|
|
334
335
|
logActivity(agentDir, { type: 'task_completed', taskId: activeTask.id });
|
|
@@ -347,7 +348,7 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
347
348
|
votesGiven: activity.votesGiven,
|
|
348
349
|
toolCalls: activity.toolCalls,
|
|
349
350
|
modelUsed: modelId,
|
|
350
|
-
tokensUsed:
|
|
351
|
+
tokensUsed: result.usage?.totalTokens || 0,
|
|
351
352
|
durationMs: Date.now() - loopStart,
|
|
352
353
|
activitySummary: { feedItemsRead, mentionsFound },
|
|
353
354
|
});
|
|
@@ -33,41 +33,84 @@ export async function getMCPTools(agentDir, agentConfig) {
|
|
|
33
33
|
continue;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
// URL-based servers default to Streamable HTTP (the current MCP
|
|
37
|
+
// standard), but allow `transport: 'sse'` in tools.yaml for legacy
|
|
38
|
+
// SSE-only servers.
|
|
39
|
+
const transport = server.url ? (server.transport || 'http') : 'stdio';
|
|
37
40
|
|
|
38
41
|
try {
|
|
39
42
|
// AI SDK v6 moved the MCP client into a separate package
|
|
40
|
-
// (`@ai-sdk/mcp`).
|
|
41
|
-
//
|
|
42
|
-
|
|
43
|
+
// (`@ai-sdk/mcp`). Walk through every plausible export shape so we
|
|
44
|
+
// survive ESM-vs-CJS interop quirks and future renames — and if
|
|
45
|
+
// none match, throw a *useful* error listing what the module
|
|
46
|
+
// actually exposes instead of the cryptic "X is not a function".
|
|
47
|
+
const mcpModule = await import('@ai-sdk/mcp');
|
|
48
|
+
const createMCPClient =
|
|
49
|
+
mcpModule.experimental_createMCPClient
|
|
50
|
+
?? mcpModule.createMCPClient
|
|
51
|
+
?? mcpModule.default?.experimental_createMCPClient
|
|
52
|
+
?? mcpModule.default?.createMCPClient;
|
|
53
|
+
if (typeof createMCPClient !== 'function') {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`@ai-sdk/mcp loaded but no createMCPClient export found. `
|
|
56
|
+
+ `Module exports: [${Object.keys(mcpModule).sort().join(', ') || '(empty)'}]. `
|
|
57
|
+
+ `If you upgraded the CLI via 'npm update', try a clean reinstall: `
|
|
58
|
+
+ `npm uninstall -g @myvillage/cli && npm install -g @myvillage/cli`
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
// Wrap createMCPClient with a hard timeout. Without this, @ai-sdk/mcp's
|
|
62
|
+
// SSE transport can return a never-settling promise when the endpoint
|
|
63
|
+
// doesn't respond as expected — and because nothing else keeps the
|
|
64
|
+
// event loop alive during agent startup, Node will exit cleanly with
|
|
65
|
+
// code 0, bypassing every try/catch and .finally we've set up.
|
|
66
|
+
const MCP_CONNECT_TIMEOUT_MS = 30000;
|
|
67
|
+
const withTimeout = (promise, label) => Promise.race([
|
|
68
|
+
promise,
|
|
69
|
+
new Promise((_, reject) => setTimeout(
|
|
70
|
+
() => reject(new Error(`MCP connect timed out after ${MCP_CONNECT_TIMEOUT_MS}ms (${label})`)),
|
|
71
|
+
MCP_CONNECT_TIMEOUT_MS,
|
|
72
|
+
)),
|
|
73
|
+
]);
|
|
43
74
|
let client;
|
|
44
75
|
|
|
45
76
|
if (server.url) {
|
|
46
77
|
// Remote MCP server via Streamable HTTP transport
|
|
47
78
|
const headers = {};
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
79
|
+
|
|
80
|
+
// Auto-attach the MyVillage bearer ONLY for MyVillage hostnames.
|
|
81
|
+
// Sending an in-session OAuth token to an arbitrary third-party
|
|
82
|
+
// MCP would be a credential leak.
|
|
83
|
+
if (isMyVillageHost(server.url)) {
|
|
84
|
+
if (process.env.MYVILLAGE_ACCESS_TOKEN) {
|
|
85
|
+
headers['Authorization'] = `Bearer ${process.env.MYVILLAGE_ACCESS_TOKEN}`;
|
|
86
|
+
}
|
|
87
|
+
if (process.env.MYVILLAGE_AGENT_ID) {
|
|
88
|
+
headers['X-Agent-Id'] = process.env.MYVILLAGE_AGENT_ID;
|
|
89
|
+
}
|
|
53
90
|
}
|
|
54
|
-
|
|
91
|
+
|
|
92
|
+
// Per-server headers from tools.yaml (with `${VAR}` env expansion).
|
|
93
|
+
// These win over the auto-attached MyVillage headers so a developer
|
|
94
|
+
// can override on the same host if they need to.
|
|
95
|
+
Object.assign(headers, resolveEnvVars(server.headers || {}));
|
|
96
|
+
|
|
97
|
+
client = await withTimeout(createMCPClient({
|
|
55
98
|
transport: {
|
|
56
|
-
type:
|
|
99
|
+
type: transport,
|
|
57
100
|
url: server.url,
|
|
58
101
|
headers,
|
|
59
102
|
},
|
|
60
|
-
});
|
|
103
|
+
}), `${name} (${transport} ${server.url})`);
|
|
61
104
|
} else {
|
|
62
105
|
// Local MCP servers via stdio transport
|
|
63
|
-
client = await createMCPClient({
|
|
106
|
+
client = await withTimeout(createMCPClient({
|
|
64
107
|
transport: {
|
|
65
108
|
type: 'stdio',
|
|
66
109
|
command: server.command,
|
|
67
110
|
args: server.args || [],
|
|
68
111
|
env: { ...process.env, ...resolveEnvVars(server.env || {}) },
|
|
69
112
|
},
|
|
70
|
-
});
|
|
113
|
+
}), `${name} (stdio ${server.command})`);
|
|
71
114
|
}
|
|
72
115
|
|
|
73
116
|
activeClients.push(client);
|
|
@@ -106,6 +149,15 @@ export async function cleanupMCPClients() {
|
|
|
106
149
|
|
|
107
150
|
// ── Helpers ─────────────────────────────────────────────
|
|
108
151
|
|
|
152
|
+
function isMyVillageHost(urlString) {
|
|
153
|
+
try {
|
|
154
|
+
const host = new URL(urlString).hostname;
|
|
155
|
+
return host === 'myvillageproject.ai' || host.endsWith('.myvillageproject.ai');
|
|
156
|
+
} catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
109
161
|
function resolveEnvVars(envMap) {
|
|
110
162
|
const resolved = {};
|
|
111
163
|
for (const [key, val] of Object.entries(envMap)) {
|
|
@@ -416,7 +416,7 @@ export async function agentStartCommand(name) {
|
|
|
416
416
|
const spinner = villageSpinner(`Starting agent "${name}"...`).start();
|
|
417
417
|
|
|
418
418
|
try {
|
|
419
|
-
const { startDaemon } = await import('../agent-runtime/daemon.js');
|
|
419
|
+
const { startDaemon, getDaemonStderrPath } = await import('../agent-runtime/daemon.js');
|
|
420
420
|
startDaemon(name);
|
|
421
421
|
|
|
422
422
|
// Wait briefly for PID file to appear
|
|
@@ -437,6 +437,25 @@ export async function agentStartCommand(name) {
|
|
|
437
437
|
const lastError = [...recent].reverse().find(e => e.type === 'error');
|
|
438
438
|
if (lastError?.error) {
|
|
439
439
|
console.log(chalk.red(` ${lastError.error}`));
|
|
440
|
+
} else {
|
|
441
|
+
// Daemon died before it could write a single log entry. Fall back
|
|
442
|
+
// to the stderr capture file — catches top-level ESM resolution
|
|
443
|
+
// errors, missing modules, syntax errors, etc.
|
|
444
|
+
const stderrPath = getDaemonStderrPath(name);
|
|
445
|
+
if (existsSync(stderrPath)) {
|
|
446
|
+
try {
|
|
447
|
+
const stderrText = readFileSync(stderrPath, 'utf-8').trim();
|
|
448
|
+
if (stderrText) {
|
|
449
|
+
console.log(chalk.red(' Daemon stderr:'));
|
|
450
|
+
// Indent each line for readability and cap output length so a
|
|
451
|
+
// multi-MB stack trace doesn't drown the terminal.
|
|
452
|
+
const trimmed = stderrText.length > 2000
|
|
453
|
+
? stderrText.slice(0, 2000) + '\n... (truncated; see ' + stderrPath + ')'
|
|
454
|
+
: stderrText;
|
|
455
|
+
console.log(trimmed.split('\n').map(l => ' ' + l).join('\n'));
|
|
456
|
+
}
|
|
457
|
+
} catch { /* ignore */ }
|
|
458
|
+
}
|
|
440
459
|
}
|
|
441
460
|
console.log(brand.teal(` Full logs: myvillage agent logs ${name}\n`));
|
|
442
461
|
}
|