@myvillage/cli 1.23.3 → 1.28.1
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.28.1",
|
|
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;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Core agent loop using Vercel AI SDK. Reads prompt.md,
|
|
3
3
|
// gathers context, calls LLM with tools, logs results.
|
|
4
4
|
|
|
5
|
-
import { generateText } from 'ai';
|
|
5
|
+
import { generateText, stepCountIs } from 'ai';
|
|
6
6
|
import { createAnthropic } from '@ai-sdk/anthropic';
|
|
7
7
|
import { createOpenAI } from '@ai-sdk/openai';
|
|
8
8
|
import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync } from 'fs';
|
|
@@ -176,7 +176,15 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
176
176
|
const instructionText = activeTask.instruction
|
|
177
177
|
|| (activeTask.input ? JSON.stringify(activeTask.input, null, 2) : '');
|
|
178
178
|
|
|
179
|
-
systemPrompt = `${systemPrompt}\n\n## ACTIVE TASK MODE
|
|
179
|
+
systemPrompt = `${systemPrompt}\n\n## ACTIVE TASK MODE
|
|
180
|
+
A client has assigned you the task below. Your job this iteration is to **execute it fully** using your available tools, not just acknowledge it.
|
|
181
|
+
|
|
182
|
+
Guidelines:
|
|
183
|
+
- Use as many tool calls as needed to complete the task. A single discovery call (e.g. listing directories) is usually NOT enough — follow up with the calls that actually produce the requested result.
|
|
184
|
+
- Do not stop after one tool call unless that call alone fulfills the entire instruction.
|
|
185
|
+
- Do not ask clarifying questions back to the user; make a reasonable interpretation and proceed.
|
|
186
|
+
- The feed context is provided only for situational awareness — do not let "no new feed activity" prevent you from carrying out the task.
|
|
187
|
+
- Your final text response should include the actual answer/output the user asked for (e.g. the list of files, the summary, the result), not just a description of what you did.`;
|
|
180
188
|
|
|
181
189
|
context = `## TASK (id=${activeTask.id}, type=${activeTask.taskType})\n${instructionText}\n\n---\n\n## FEED CONTEXT (for awareness only)\n${context}`;
|
|
182
190
|
|
|
@@ -193,13 +201,20 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
193
201
|
});
|
|
194
202
|
|
|
195
203
|
// Call LLM with tools
|
|
204
|
+
// AI SDK v6 removed `maxSteps` — the loop now uses `stopWhen` with
|
|
205
|
+
// a StopCondition predicate. The default is `stepCountIs(1)`, so
|
|
206
|
+
// without this the model gets exactly ONE round of tool calls and
|
|
207
|
+
// returns immediately. We give it up to 10 steps so a task like
|
|
208
|
+
// "find any README under the workspace" can do
|
|
209
|
+
// list_allowed_directories → list_directory → search_files → ...
|
|
210
|
+
// until it's actually done.
|
|
196
211
|
const result = await generateText({
|
|
197
212
|
model,
|
|
198
213
|
system: systemPrompt,
|
|
199
214
|
prompt: context,
|
|
200
215
|
tools,
|
|
201
|
-
|
|
202
|
-
maxTokens,
|
|
216
|
+
stopWhen: stepCountIs(10),
|
|
217
|
+
maxOutputTokens: maxTokens,
|
|
203
218
|
});
|
|
204
219
|
|
|
205
220
|
// Log LLM response
|
|
@@ -207,13 +222,18 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
207
222
|
type: 'llm_response',
|
|
208
223
|
text: (result.text || '').slice(0, 500),
|
|
209
224
|
tokensUsed: {
|
|
210
|
-
prompt: result.usage?.
|
|
211
|
-
completion: result.usage?.
|
|
225
|
+
prompt: result.usage?.inputTokens || 0,
|
|
226
|
+
completion: result.usage?.outputTokens || 0,
|
|
227
|
+
total: result.usage?.totalTokens || 0,
|
|
212
228
|
},
|
|
213
229
|
});
|
|
214
230
|
|
|
215
231
|
// Log tool calls and count activity. Also audit action-tool success
|
|
216
232
|
// so we don't trust the model's final text about whether a task worked.
|
|
233
|
+
// `toolCallSummary` accumulates a structured per-call record that we
|
|
234
|
+
// attach to the task output so the Activity panel can show what the
|
|
235
|
+
// agent actually did — not just a tool-call count.
|
|
236
|
+
const toolCallSummary = [];
|
|
217
237
|
if (result.steps?.length) {
|
|
218
238
|
for (const step of result.steps) {
|
|
219
239
|
if (step.toolCalls?.length) {
|
|
@@ -227,25 +247,39 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
227
247
|
if (step.toolCalls?.length && step.toolResults?.length) {
|
|
228
248
|
for (let i = 0; i < step.toolResults.length; i++) {
|
|
229
249
|
const tr = step.toolResults[i];
|
|
230
|
-
|
|
250
|
+
// AI SDK v6 renamed `args` → `input`. Try v6 first.
|
|
251
|
+
const args = step.toolCalls[i]?.input ?? step.toolCalls[i]?.args;
|
|
231
252
|
const errored = isToolResultError(tr);
|
|
253
|
+
const resultSummary = summarizeToolResult(tr);
|
|
232
254
|
auditToolCall(taskActionAudit, tr.toolName, errored, tr);
|
|
255
|
+
toolCallSummary.push({
|
|
256
|
+
tool: tr.toolName,
|
|
257
|
+
args,
|
|
258
|
+
result: resultSummary,
|
|
259
|
+
ok: !errored,
|
|
260
|
+
});
|
|
233
261
|
logActivity(agentDir, {
|
|
234
262
|
type: 'tool_call',
|
|
235
263
|
tool: tr.toolName,
|
|
236
264
|
args,
|
|
237
|
-
result:
|
|
265
|
+
result: resultSummary,
|
|
238
266
|
ok: !errored,
|
|
239
267
|
});
|
|
240
268
|
}
|
|
241
269
|
} else if (step.toolResults?.length) {
|
|
242
270
|
for (const tr of step.toolResults) {
|
|
243
271
|
const errored = isToolResultError(tr);
|
|
272
|
+
const resultSummary = summarizeToolResult(tr);
|
|
244
273
|
auditToolCall(taskActionAudit, tr.toolName, errored, tr);
|
|
274
|
+
toolCallSummary.push({
|
|
275
|
+
tool: tr.toolName,
|
|
276
|
+
result: resultSummary,
|
|
277
|
+
ok: !errored,
|
|
278
|
+
});
|
|
245
279
|
logActivity(agentDir, {
|
|
246
280
|
type: 'tool_call',
|
|
247
281
|
tool: tr.toolName,
|
|
248
|
-
result:
|
|
282
|
+
result: resultSummary,
|
|
249
283
|
ok: !errored,
|
|
250
284
|
});
|
|
251
285
|
}
|
|
@@ -258,10 +292,16 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
258
292
|
if (tc.toolName === 'comment_create') activity.commentsCreated++;
|
|
259
293
|
if (tc.toolName === 'vote_cast') activity.votesGiven++;
|
|
260
294
|
// No paired result here — assume executed, can't audit.
|
|
295
|
+
const args = tc.input ?? tc.args;
|
|
296
|
+
toolCallSummary.push({
|
|
297
|
+
tool: tc.toolName,
|
|
298
|
+
args,
|
|
299
|
+
result: 'executed',
|
|
300
|
+
});
|
|
261
301
|
logActivity(agentDir, {
|
|
262
302
|
type: 'tool_call',
|
|
263
303
|
tool: tc.toolName,
|
|
264
|
-
args
|
|
304
|
+
args,
|
|
265
305
|
result: 'executed',
|
|
266
306
|
});
|
|
267
307
|
}
|
|
@@ -280,12 +320,12 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
280
320
|
if (result.steps?.length) {
|
|
281
321
|
for (const step of result.steps) {
|
|
282
322
|
for (const tc of step.toolCalls || []) {
|
|
283
|
-
collectActions(tc.toolName, tc.args);
|
|
323
|
+
collectActions(tc.toolName, tc.input ?? tc.args);
|
|
284
324
|
}
|
|
285
325
|
}
|
|
286
326
|
} else if (result.toolCalls?.length) {
|
|
287
327
|
for (const tc of result.toolCalls) {
|
|
288
|
-
collectActions(tc.toolName, tc.args);
|
|
328
|
+
collectActions(tc.toolName, tc.input ?? tc.args);
|
|
289
329
|
}
|
|
290
330
|
}
|
|
291
331
|
// Keep only last 50 actions to bound memory
|
|
@@ -309,11 +349,12 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
309
349
|
errorMessage,
|
|
310
350
|
output: {
|
|
311
351
|
text: result.text || '',
|
|
312
|
-
|
|
352
|
+
toolCallCount: activity.toolCalls,
|
|
353
|
+
toolCalls: toolCallSummary,
|
|
313
354
|
toolErrors: taskActionAudit.toolErrors,
|
|
314
355
|
note: 'Marked FAILED because the action tools did not succeed. The model\'s text may claim success but the underlying tool calls errored.',
|
|
315
356
|
},
|
|
316
|
-
tokensUsed:
|
|
357
|
+
tokensUsed: result.usage?.totalTokens || 0,
|
|
317
358
|
durationMs: Date.now() - loopStart,
|
|
318
359
|
});
|
|
319
360
|
logActivity(agentDir, {
|
|
@@ -325,10 +366,11 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
325
366
|
await completeAgentTask(config.man.village_agent_id, activeTask.id, {
|
|
326
367
|
output: {
|
|
327
368
|
text: result.text || '',
|
|
328
|
-
|
|
369
|
+
toolCallCount: activity.toolCalls,
|
|
370
|
+
toolCalls: toolCallSummary,
|
|
329
371
|
toolErrors: taskActionAudit.toolErrors.length > 0 ? taskActionAudit.toolErrors : undefined,
|
|
330
372
|
},
|
|
331
|
-
tokensUsed:
|
|
373
|
+
tokensUsed: result.usage?.totalTokens || 0,
|
|
332
374
|
durationMs: Date.now() - loopStart,
|
|
333
375
|
});
|
|
334
376
|
logActivity(agentDir, { type: 'task_completed', taskId: activeTask.id });
|
|
@@ -347,7 +389,7 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
347
389
|
votesGiven: activity.votesGiven,
|
|
348
390
|
toolCalls: activity.toolCalls,
|
|
349
391
|
modelUsed: modelId,
|
|
350
|
-
tokensUsed:
|
|
392
|
+
tokensUsed: result.usage?.totalTokens || 0,
|
|
351
393
|
durationMs: Date.now() - loopStart,
|
|
352
394
|
activitySummary: { feedItemsRead, mentionsFound },
|
|
353
395
|
});
|
|
@@ -451,7 +493,10 @@ const ACTION_TOOLS = new Set([
|
|
|
451
493
|
|
|
452
494
|
function flattenToolResultText(tr) {
|
|
453
495
|
if (!tr) return '';
|
|
454
|
-
|
|
496
|
+
// AI SDK v6 renamed `result` → `output`. Fall back to the old name so
|
|
497
|
+
// older SDK versions still work if anyone pins them.
|
|
498
|
+
const r = tr.output ?? tr.result;
|
|
499
|
+
if (r == null) return '';
|
|
455
500
|
if (typeof r === 'string') return r;
|
|
456
501
|
if (Array.isArray(r?.content)) {
|
|
457
502
|
return r.content
|
|
@@ -33,7 +33,10 @@ 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
|
|
@@ -55,34 +58,67 @@ export async function getMCPTools(agentDir, agentConfig) {
|
|
|
55
58
|
+ `npm uninstall -g @myvillage/cli && npm install -g @myvillage/cli`
|
|
56
59
|
);
|
|
57
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
|
+
]);
|
|
58
74
|
let client;
|
|
59
75
|
|
|
60
76
|
if (server.url) {
|
|
61
77
|
// Remote MCP server via Streamable HTTP transport
|
|
62
78
|
const headers = {};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
+
}
|
|
68
90
|
}
|
|
69
|
-
|
|
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({
|
|
70
98
|
transport: {
|
|
71
|
-
type:
|
|
99
|
+
type: transport,
|
|
72
100
|
url: server.url,
|
|
73
101
|
headers,
|
|
74
102
|
},
|
|
75
|
-
});
|
|
103
|
+
}), `${name} (${transport} ${server.url})`);
|
|
76
104
|
} else {
|
|
77
|
-
// Local MCP servers via stdio transport
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
105
|
+
// Local MCP servers via stdio transport.
|
|
106
|
+
//
|
|
107
|
+
// @ai-sdk/mcp v1 doesn't accept a `{ type: 'stdio', ... }` config —
|
|
108
|
+
// its MCPTransportConfig is HTTP-only (`'sse' | 'http'`). For stdio
|
|
109
|
+
// you must instantiate the transport class explicitly from the
|
|
110
|
+
// `/mcp-stdio` subpath and pass the instance.
|
|
111
|
+
const { Experimental_StdioMCPTransport } =
|
|
112
|
+
await import('@ai-sdk/mcp/mcp-stdio');
|
|
113
|
+
const stdioTransport = new Experimental_StdioMCPTransport({
|
|
114
|
+
command: server.command,
|
|
115
|
+
args: server.args || [],
|
|
116
|
+
env: { ...process.env, ...resolveEnvVars(server.env || {}) },
|
|
85
117
|
});
|
|
118
|
+
client = await withTimeout(
|
|
119
|
+
createMCPClient({ transport: stdioTransport }),
|
|
120
|
+
`${name} (stdio ${server.command})`,
|
|
121
|
+
);
|
|
86
122
|
}
|
|
87
123
|
|
|
88
124
|
activeClients.push(client);
|
|
@@ -121,6 +157,15 @@ export async function cleanupMCPClients() {
|
|
|
121
157
|
|
|
122
158
|
// ── Helpers ─────────────────────────────────────────────
|
|
123
159
|
|
|
160
|
+
function isMyVillageHost(urlString) {
|
|
161
|
+
try {
|
|
162
|
+
const host = new URL(urlString).hostname;
|
|
163
|
+
return host === 'myvillageproject.ai' || host.endsWith('.myvillageproject.ai');
|
|
164
|
+
} catch {
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
124
169
|
function resolveEnvVars(envMap) {
|
|
125
170
|
const resolved = {};
|
|
126
171
|
for (const [key, val] of Object.entries(envMap)) {
|
|
@@ -4,6 +4,7 @@ import { villageSpinner, brand } from '../utils/brand.js';
|
|
|
4
4
|
import inquirer from 'inquirer';
|
|
5
5
|
import { existsSync, readFileSync } from 'fs';
|
|
6
6
|
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
7
8
|
import { execSync } from 'child_process';
|
|
8
9
|
import { isAuthenticated, getAccessToken } from '../utils/auth.js';
|
|
9
10
|
import { getConfig, setConfig } from '../utils/config.js';
|
|
@@ -41,7 +42,7 @@ import {
|
|
|
41
42
|
readAgentLogs,
|
|
42
43
|
getLogFilePath,
|
|
43
44
|
} from '../utils/local-agent.js';
|
|
44
|
-
import { scaffoldAgent, TOOL_CATALOG } from '../utils/agent-scaffolder.js';
|
|
45
|
+
import { scaffoldAgent, TOOL_CATALOG, resolveCatalogEntry } from '../utils/agent-scaffolder.js';
|
|
45
46
|
import { formatLocalAgentList, formatLocalAgentStatus } from '../utils/formatters.js';
|
|
46
47
|
|
|
47
48
|
// ── Create Local Agent (wizard) ─────────────────────────
|
|
@@ -117,11 +118,10 @@ export async function agentCreateLocalCommand() {
|
|
|
117
118
|
message: 'Which tools should your agent have?',
|
|
118
119
|
choices: [
|
|
119
120
|
{ name: 'MyVillageOS MCP (feed, posts, communities, wallet)', value: 'myvillage', checked: true, disabled: 'always enabled' },
|
|
120
|
-
{ name: 'Local Files (read/write
|
|
121
|
+
{ name: 'Local Files (read/write a sandboxed workspace)', value: 'filesystem', checked: true },
|
|
121
122
|
{ name: 'Gmail', value: 'gmail' },
|
|
122
123
|
{ name: 'Calendar', value: 'calendar' },
|
|
123
124
|
{ name: 'GitHub', value: 'github' },
|
|
124
|
-
{ name: 'Browser', value: 'browser' },
|
|
125
125
|
],
|
|
126
126
|
},
|
|
127
127
|
{
|
|
@@ -396,7 +396,7 @@ export async function agentStartCommand(name) {
|
|
|
396
396
|
const toolsConfig = readToolsYaml(name);
|
|
397
397
|
if (toolsConfig.servers?.['man-feed'] && !toolsConfig.servers?.['myvillage']) {
|
|
398
398
|
delete toolsConfig.servers['man-feed'];
|
|
399
|
-
toolsConfig.servers = { myvillage:
|
|
399
|
+
toolsConfig.servers = { myvillage: resolveCatalogEntry('myvillage', name), ...toolsConfig.servers };
|
|
400
400
|
writeToolsYaml(name, toolsConfig);
|
|
401
401
|
}
|
|
402
402
|
|
|
@@ -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
|
}
|
|
@@ -629,9 +648,22 @@ function printLogEntry(entry) {
|
|
|
629
648
|
console.log(` ${' '.repeat(10)} ${brand.teal(`Tokens: ${entry.tokensUsed.prompt || 0}p / ${entry.tokensUsed.completion || 0}c`)}`);
|
|
630
649
|
}
|
|
631
650
|
break;
|
|
632
|
-
case 'tool_call':
|
|
633
|
-
|
|
651
|
+
case 'tool_call': {
|
|
652
|
+
const arrow = entry.ok === false ? chalk.red('→') : brand.teal('→');
|
|
653
|
+
const statusLabel = entry.ok === false ? chalk.red('error') : '';
|
|
654
|
+
console.log(` ${time} ${chalk.yellow('TOOL')} ${entry.tool || '?'} ${arrow} ${statusLabel}`);
|
|
655
|
+
// Show args (if any) and the result content on indented sub-lines,
|
|
656
|
+
// so a developer tailing logs can actually see what the agent did.
|
|
657
|
+
if (entry.args && Object.keys(entry.args).length > 0) {
|
|
658
|
+
const argsStr = truncateLog(JSON.stringify(entry.args), 200);
|
|
659
|
+
console.log(` ${' '.repeat(10)} ${brand.teal('args:')} ${argsStr}`);
|
|
660
|
+
}
|
|
661
|
+
if (entry.result && entry.result !== 'ok' && entry.result !== 'executed') {
|
|
662
|
+
const resultStr = truncateLog(String(entry.result), 200);
|
|
663
|
+
console.log(` ${' '.repeat(10)} ${brand.teal('result:')} ${resultStr}`);
|
|
664
|
+
}
|
|
634
665
|
break;
|
|
666
|
+
}
|
|
635
667
|
case 'error':
|
|
636
668
|
console.log(` ${time} ${chalk.red('ERR')} ${entry.error || entry.message || 'Unknown error'}`);
|
|
637
669
|
break;
|
|
@@ -691,11 +723,15 @@ export async function agentAddToolCommand(name, tool) {
|
|
|
691
723
|
}
|
|
692
724
|
|
|
693
725
|
toolsConfig.servers = toolsConfig.servers || {};
|
|
694
|
-
toolsConfig.servers[tool] =
|
|
726
|
+
toolsConfig.servers[tool] = resolveCatalogEntry(tool, name);
|
|
695
727
|
writeToolsYaml(name, toolsConfig);
|
|
696
728
|
|
|
697
729
|
console.log(brand.green(` \u2713 Added "${tool}" to agent "${name}".`));
|
|
698
730
|
|
|
731
|
+
if (tool === 'filesystem') {
|
|
732
|
+
const workspace = join(homedir(), '.myvillage', 'agents', name, 'workspace');
|
|
733
|
+
console.log(brand.teal(` Workspace: ${workspace}`));
|
|
734
|
+
}
|
|
699
735
|
if (tool === 'github') {
|
|
700
736
|
console.log(brand.teal(' Note: Set GITHUB_TOKEN in your environment for GitHub access.'));
|
|
701
737
|
}
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'fs';
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync } from 'fs';
|
|
2
2
|
import { join } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
3
4
|
import { stringify as stringifyYaml } from 'yaml';
|
|
4
5
|
|
|
5
6
|
// ── MCP Tool Catalog ────────────────────────────────────
|
|
7
|
+
//
|
|
8
|
+
// Static-shape entries. The `filesystem` entry's allow-listed root is
|
|
9
|
+
// resolved per-agent at scaffold/add-tool time — see
|
|
10
|
+
// resolveCatalogEntry() — so each agent gets its own sandboxed workspace.
|
|
6
11
|
|
|
7
12
|
const TOOL_CATALOG = {
|
|
8
13
|
myvillage: {
|
|
@@ -12,8 +17,10 @@ const TOOL_CATALOG = {
|
|
|
12
17
|
},
|
|
13
18
|
filesystem: {
|
|
14
19
|
command: 'npx',
|
|
15
|
-
|
|
16
|
-
|
|
20
|
+
// Last arg is a placeholder; replaced with `<agentDir>/workspace`
|
|
21
|
+
// by resolveCatalogEntry().
|
|
22
|
+
args: ['-y', '@modelcontextprotocol/server-filesystem', '__AGENT_WORKSPACE__'],
|
|
23
|
+
description: 'Read/write files under the agent\'s sandboxed workspace',
|
|
17
24
|
},
|
|
18
25
|
gmail: {
|
|
19
26
|
command: 'npx',
|
|
@@ -31,15 +38,41 @@ const TOOL_CATALOG = {
|
|
|
31
38
|
env: { GITHUB_TOKEN: '${GITHUB_TOKEN}' },
|
|
32
39
|
description: 'GitHub repository access',
|
|
33
40
|
},
|
|
34
|
-
browser:
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
description: 'Navigate web pages, extract content',
|
|
38
|
-
},
|
|
41
|
+
// browser: removed in v1.27.0 — the previously catalog'd
|
|
42
|
+
// `@anthropic/mcp-puppeteer` package is no longer published under that
|
|
43
|
+
// name. Add a working browser MCP back when one stabilizes.
|
|
39
44
|
};
|
|
40
45
|
|
|
41
46
|
export { TOOL_CATALOG };
|
|
42
47
|
|
|
48
|
+
// ── Catalog Resolution ──────────────────────────────────
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Returns a deep-copied catalog entry with per-agent placeholders
|
|
52
|
+
* resolved (e.g. the filesystem workspace path). Also creates any
|
|
53
|
+
* directories the entry depends on (the workspace dir for filesystem).
|
|
54
|
+
*
|
|
55
|
+
* Use this anywhere you'd otherwise do `{ ...TOOL_CATALOG[toolId] }`
|
|
56
|
+
* to write into a tools.yaml.
|
|
57
|
+
*/
|
|
58
|
+
export function resolveCatalogEntry(toolId, agentName) {
|
|
59
|
+
const entry = TOOL_CATALOG[toolId];
|
|
60
|
+
if (!entry) return null;
|
|
61
|
+
const resolved = JSON.parse(JSON.stringify(entry));
|
|
62
|
+
|
|
63
|
+
if (toolId === 'filesystem') {
|
|
64
|
+
const workspace = join(homedir(), '.myvillage', 'agents', agentName, 'workspace');
|
|
65
|
+
if (!existsSync(workspace)) {
|
|
66
|
+
mkdirSync(workspace, { recursive: true });
|
|
67
|
+
}
|
|
68
|
+
resolved.args = resolved.args.map(a =>
|
|
69
|
+
a === '__AGENT_WORKSPACE__' ? workspace : a,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return resolved;
|
|
74
|
+
}
|
|
75
|
+
|
|
43
76
|
// ── Check-in Interval Map ───────────────────────────────
|
|
44
77
|
|
|
45
78
|
const INTERVAL_MAP = {
|
|
@@ -73,7 +106,7 @@ export function scaffoldAgent(agentDir, options) {
|
|
|
73
106
|
// Write tools.yaml
|
|
74
107
|
writeFileSync(
|
|
75
108
|
join(agentDir, 'tools.yaml'),
|
|
76
|
-
generateToolsYaml(tools)
|
|
109
|
+
generateToolsYaml(tools, name)
|
|
77
110
|
);
|
|
78
111
|
|
|
79
112
|
// Write logs/.gitkeep
|
|
@@ -156,17 +189,16 @@ When you receive a TASK in your context, follow these rules:
|
|
|
156
189
|
|
|
157
190
|
// ── Tools YAML Generation ───────────────────────────────
|
|
158
191
|
|
|
159
|
-
function generateToolsYaml(selectedTools) {
|
|
192
|
+
function generateToolsYaml(selectedTools, agentName) {
|
|
160
193
|
const servers = {};
|
|
161
194
|
|
|
162
195
|
// myvillage is always included
|
|
163
|
-
servers['myvillage'] =
|
|
196
|
+
servers['myvillage'] = resolveCatalogEntry('myvillage', agentName);
|
|
164
197
|
|
|
165
198
|
for (const toolId of selectedTools) {
|
|
166
199
|
if (toolId === 'myvillage') continue;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
200
|
+
const entry = resolveCatalogEntry(toolId, agentName);
|
|
201
|
+
if (entry) servers[toolId] = entry;
|
|
170
202
|
}
|
|
171
203
|
|
|
172
204
|
return stringifyYaml({ servers }, { lineWidth: 0 });
|