@myvillage/cli 2.13.1 → 2.13.2
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 +1 -1
- package/src/agent-runtime/runner.js +7 -62
- package/src/commands/agent-local.js +48 -13
- package/src/utils/agent-config-sync.js +79 -0
- package/src/utils/api.js +8 -0
package/package.json
CHANGED
|
@@ -24,10 +24,10 @@ import path from 'path';
|
|
|
24
24
|
import { getConfig } from '../utils/config.js';
|
|
25
25
|
import { getAccessToken, isTokenExpired } from '../utils/auth.js';
|
|
26
26
|
import { refreshAccessToken } from '../utils/api.js';
|
|
27
|
-
import { listLocalAgents, readAgentConfig, getAgentDir
|
|
27
|
+
import { listLocalAgents, readAgentConfig, getAgentDir } from '../utils/local-agent.js';
|
|
28
28
|
import { startDaemon, stopDaemon, isDaemonRunning, getDaemonInfo } from './daemon.js';
|
|
29
29
|
import { shipLogsOnce, forgetLogCursor } from './log-shipper.js';
|
|
30
|
-
import {
|
|
30
|
+
import { writePublishedAgentConfig, readSyncedVersion } from '../utils/agent-config-sync.js';
|
|
31
31
|
|
|
32
32
|
const RUNNER_STATE_FILE = path.join(os.homedir(), '.myvillage', 'runner.json');
|
|
33
33
|
const DEFAULT_HEARTBEAT_MS = 20_000;
|
|
@@ -128,20 +128,6 @@ async function request(networkBaseUrl, pathname, method, body, headers) {
|
|
|
128
128
|
|
|
129
129
|
// ── Config sync (materialise an assigned agent from the control plane) ───────
|
|
130
130
|
|
|
131
|
-
// Records which published config version this machine last wrote, so we only
|
|
132
|
-
// re-write files when the definition actually changed.
|
|
133
|
-
function syncedVersionPath(agentDir) {
|
|
134
|
-
return path.join(agentDir, '.config-version');
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function readSyncedVersion(agentDir) {
|
|
138
|
-
try {
|
|
139
|
-
return Number(fs.readFileSync(syncedVersionPath(agentDir), 'utf8').trim()) || 0;
|
|
140
|
-
} catch {
|
|
141
|
-
return 0;
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
131
|
/**
|
|
146
132
|
* Write an assigned agent's files from its published definition, creating the
|
|
147
133
|
* agent directory if this machine has never run it. Returns true when the agent
|
|
@@ -167,53 +153,12 @@ async function syncAgentConfig(ctx, assignment) {
|
|
|
167
153
|
log(`could not fetch config for ${name} (${res.status}) — leaving local files alone`);
|
|
168
154
|
return exists;
|
|
169
155
|
}
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
}
|
|
176
|
-
if (typeof c.toolsYaml === 'string' && c.toolsYaml) {
|
|
177
|
-
fs.writeFileSync(path.join(agentDir, 'tools.yaml'), c.toolsYaml);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// The trained models this agent was given travel with its config.
|
|
181
|
-
if (c.models && typeof c.models === 'object') {
|
|
182
|
-
for (const [slug, entry] of Object.entries(c.models)) {
|
|
183
|
-
const result = writeSyncedModel(slug, entry);
|
|
184
|
-
if (result === 'written') log(`synced model ${slug} for ${name}`);
|
|
185
|
-
else if (result === 'kept-local') log(`kept this machine's own model ${slug} for ${name}`);
|
|
186
|
-
else if (result === 'invalid') log(`skipped model ${slug} for ${name}: its files didn't check out`);
|
|
187
|
-
}
|
|
156
|
+
const { models } = writePublishedAgentConfig(name, { ...res.json.data, version: configVersion });
|
|
157
|
+
for (const { slug, result } of models) {
|
|
158
|
+
if (result === 'written') log(`synced model ${slug} for ${name}`);
|
|
159
|
+
else if (result === 'kept-local') log(`kept this machine's own model ${slug} for ${name}`);
|
|
160
|
+
else if (result === 'invalid') log(`skipped model ${slug} for ${name}: its files didn't check out`);
|
|
188
161
|
}
|
|
189
|
-
|
|
190
|
-
// Compose agent.config.yaml from server-known identity + published schedule
|
|
191
|
-
// and brain, preserving anything already set locally that the control plane
|
|
192
|
-
// does not own.
|
|
193
|
-
let existingCfg = {};
|
|
194
|
-
try { existingCfg = readAgentConfig(name) || {}; } catch { /* first sync */ }
|
|
195
|
-
const cfg = {
|
|
196
|
-
...existingCfg,
|
|
197
|
-
name,
|
|
198
|
-
display_name: c.displayName || existingCfg.display_name || name,
|
|
199
|
-
description: c.description ?? existingCfg.description ?? '',
|
|
200
|
-
man: {
|
|
201
|
-
...(existingCfg.man || {}),
|
|
202
|
-
agent_id: c.agentId,
|
|
203
|
-
...(c.villageAgentId ? { village_agent_id: c.villageAgentId } : {}),
|
|
204
|
-
handle: c.handle || name,
|
|
205
|
-
},
|
|
206
|
-
...(c.schedule ? { schedule: c.schedule } : {}),
|
|
207
|
-
...(c.brain ? { brain: c.brain } : {}),
|
|
208
|
-
};
|
|
209
|
-
writeAgentConfig(name, cfg);
|
|
210
|
-
|
|
211
|
-
// Keep an empty .env so the daemon's loader and 0600 expectations hold, but
|
|
212
|
-
// never overwrite secrets a person put here.
|
|
213
|
-
const envPath = path.join(agentDir, '.env');
|
|
214
|
-
if (!fs.existsSync(envPath)) fs.writeFileSync(envPath, '', { mode: 0o600 });
|
|
215
|
-
|
|
216
|
-
fs.writeFileSync(syncedVersionPath(agentDir), String(configVersion));
|
|
217
162
|
log(`synced ${name} config v${configVersion}${exists ? '' : ' (new on this machine)'}`);
|
|
218
163
|
return true;
|
|
219
164
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
listCommunities,
|
|
16
16
|
getAgentActivity,
|
|
17
17
|
listMyAgents,
|
|
18
|
+
getAgentPublishedConfig,
|
|
18
19
|
listMyUnlinkedAgentProfiles,
|
|
19
20
|
createVillageAgent as apiCreateVillageAgent,
|
|
20
21
|
listAgentTasks,
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
readAgentLogs,
|
|
46
47
|
getLogFilePath,
|
|
47
48
|
} from '../utils/local-agent.js';
|
|
49
|
+
import { writePublishedAgentConfig } from '../utils/agent-config-sync.js';
|
|
48
50
|
import { scaffoldAgent, TOOL_CATALOG, resolveCatalogEntry } from '../utils/agent-scaffolder.js';
|
|
49
51
|
import { loadAgentRecipe, listAgentRecipes } from '../utils/agent-recipes.js';
|
|
50
52
|
import { formatLocalAgentList, formatLocalAgentStatus } from '../utils/formatters.js';
|
|
@@ -1315,12 +1317,12 @@ export async function agentRememberCommand(name, text, options = {}) {
|
|
|
1315
1317
|
// already has the agent — this command scaffolds the missing local config
|
|
1316
1318
|
// (~/.myvillage/agents/<handle>/) so `agent start` can launch the daemon.
|
|
1317
1319
|
//
|
|
1318
|
-
// What pulls down: the network identity (id, handle, displayName)
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
//
|
|
1323
|
-
//
|
|
1320
|
+
// What pulls down: the network identity (id, handle, displayName), a link to
|
|
1321
|
+
// the existing VillageAgent task queue, and — when the agent has a published
|
|
1322
|
+
// definition (Studio publishes on every save) — its prompt.md, tools.yaml,
|
|
1323
|
+
// schedule, model settings and trained models.
|
|
1324
|
+
// What doesn't: API keys and other .env secrets. Those stay machine-local.
|
|
1325
|
+
// An agent that was never published falls back to the scaffold defaults.
|
|
1324
1326
|
|
|
1325
1327
|
async function fetchMyAgentByHandle(handle) {
|
|
1326
1328
|
const result = await listMyAgents({ includeVillageAgent: true });
|
|
@@ -1365,8 +1367,17 @@ export async function agentPullCommand(handle, options = {}) {
|
|
|
1365
1367
|
return;
|
|
1366
1368
|
}
|
|
1367
1369
|
|
|
1368
|
-
//
|
|
1369
|
-
//
|
|
1370
|
+
// The published definition, if there is one. A failure here is not fatal:
|
|
1371
|
+
// the scaffold defaults still boot.
|
|
1372
|
+
let published = null;
|
|
1373
|
+
try {
|
|
1374
|
+
const c = await getAgentPublishedConfig(profile.id);
|
|
1375
|
+
if (c && c.version > 0) published = c;
|
|
1376
|
+
} catch { /* fall back to defaults */ }
|
|
1377
|
+
|
|
1378
|
+
// Scaffold first so the directory has everything `start` expects (routines/,
|
|
1379
|
+
// logs/, .env); the published definition is then written over the defaults.
|
|
1380
|
+
// Default scaffold: just the always-on MyVillage MCP tool.
|
|
1370
1381
|
const agentDir = getAgentDir(agentName);
|
|
1371
1382
|
try {
|
|
1372
1383
|
scaffoldAgent(agentDir, {
|
|
@@ -1394,6 +1405,16 @@ export async function agentPullCommand(handle, options = {}) {
|
|
|
1394
1405
|
}
|
|
1395
1406
|
writeAgentConfig(agentName, cfg);
|
|
1396
1407
|
|
|
1408
|
+
let models = [];
|
|
1409
|
+
if (published) {
|
|
1410
|
+
try {
|
|
1411
|
+
({ models } = writePublishedAgentConfig(agentName, published));
|
|
1412
|
+
} catch (err) {
|
|
1413
|
+
spinner.warn(`Pulled ${handle}, but its saved setup couldn't be written: ${err.message}`);
|
|
1414
|
+
published = null;
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1397
1418
|
spinner.succeed(`Pulled ${handle} to ~/.myvillage/agents/${agentName}/`);
|
|
1398
1419
|
|
|
1399
1420
|
console.log('');
|
|
@@ -1404,11 +1425,25 @@ export async function agentPullCommand(handle, options = {}) {
|
|
|
1404
1425
|
} else {
|
|
1405
1426
|
console.log(` - ${chalk.yellow('No VillageAgent shim found — one will be created on first start.')}`);
|
|
1406
1427
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1428
|
+
if (published) {
|
|
1429
|
+
const brain = published.brain;
|
|
1430
|
+
console.log(` - Saved setup v${published.version}: ${published.promptMd ? 'prompt.md, ' : ''}${published.toolsYaml ? 'tools.yaml, ' : ''}schedule & model settings`);
|
|
1431
|
+
if (brain?.model) console.log(` - Model: ${brain.provider ? `${brain.provider} / ` : ''}${brain.model}`);
|
|
1432
|
+
const written = models.filter(m => m.result === 'written' || m.result === 'kept-local').map(m => m.slug);
|
|
1433
|
+
if (written.length) console.log(` - Trained models: ${written.join(', ')}`);
|
|
1434
|
+
const skipped = models.filter(m => m.result === 'invalid').map(m => m.slug);
|
|
1435
|
+
if (skipped.length) console.log(` - ${chalk.yellow(`Skipped models whose files didn't check out: ${skipped.join(', ')}`)}`);
|
|
1436
|
+
console.log('');
|
|
1437
|
+
console.log(brand.teal(' Not included:'));
|
|
1438
|
+
console.log(` - ${brand.gold('.env')} secrets (API keys etc.) stay on the machine they were set on.`);
|
|
1439
|
+
} else {
|
|
1440
|
+
console.log('');
|
|
1441
|
+
console.log(brand.teal(' No saved setup on the network yet, so these are defaults:'));
|
|
1442
|
+
console.log(` - ${brand.gold('prompt.md')} (system prompt — generic template)`);
|
|
1443
|
+
console.log(` - ${brand.gold('tools.yaml')} (only the MyVillage tool is enabled)`);
|
|
1444
|
+
console.log(` - ${brand.gold('agent.config.yaml')} (anthropic + claude-sonnet by default)`);
|
|
1445
|
+
console.log(` Open the agent in Studio and save once on the original machine to publish it.`);
|
|
1446
|
+
}
|
|
1412
1447
|
console.log('');
|
|
1413
1448
|
console.log(brand.teal(' Next:'));
|
|
1414
1449
|
console.log(` ${brand.gold(`myvillage agent edit ${agentName}`)} Customize prompt / tools`);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Write an agent's published definition (GET /agents/:id/config) to its local
|
|
2
|
+
// directory. Shared by the fleet runner, which keeps assigned agents in sync,
|
|
3
|
+
// and `agent pull`, which rehydrates an agent on a new machine.
|
|
4
|
+
//
|
|
5
|
+
// `.env` is never written from the server: secrets stay per-machine until they
|
|
6
|
+
// can be envelope-encrypted (see AgentConfig in the ed-platform schema).
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { readAgentConfig, getAgentDir, writeAgentConfig } from './local-agent.js';
|
|
11
|
+
import { writeSyncedModel } from './agent-models.js';
|
|
12
|
+
|
|
13
|
+
// Records which published config version this machine last wrote, so the
|
|
14
|
+
// runner only re-writes files when the definition actually changed.
|
|
15
|
+
export function syncedVersionPath(agentDir) {
|
|
16
|
+
return path.join(agentDir, '.config-version');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function readSyncedVersion(agentDir) {
|
|
20
|
+
try {
|
|
21
|
+
return Number(fs.readFileSync(syncedVersionPath(agentDir), 'utf8').trim()) || 0;
|
|
22
|
+
} catch {
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Materialise a published config `c` into ~/.myvillage/agents/<name>/.
|
|
29
|
+
* Anything already set locally that the control plane does not own is kept.
|
|
30
|
+
* Returns { models: [{ slug, result }] } so callers can report what happened.
|
|
31
|
+
*/
|
|
32
|
+
export function writePublishedAgentConfig(name, c) {
|
|
33
|
+
const agentDir = getAgentDir(name);
|
|
34
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
35
|
+
|
|
36
|
+
if (typeof c.promptMd === 'string' && c.promptMd) {
|
|
37
|
+
fs.writeFileSync(path.join(agentDir, 'prompt.md'), c.promptMd);
|
|
38
|
+
}
|
|
39
|
+
if (typeof c.toolsYaml === 'string' && c.toolsYaml) {
|
|
40
|
+
fs.writeFileSync(path.join(agentDir, 'tools.yaml'), c.toolsYaml);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The trained models this agent was given travel with its config.
|
|
44
|
+
const models = [];
|
|
45
|
+
if (c.models && typeof c.models === 'object') {
|
|
46
|
+
for (const [slug, entry] of Object.entries(c.models)) {
|
|
47
|
+
models.push({ slug, result: writeSyncedModel(slug, entry) });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Compose agent.config.yaml from server-known identity + published schedule
|
|
52
|
+
// and brain, preserving anything already set locally that the control plane
|
|
53
|
+
// does not own.
|
|
54
|
+
let existingCfg = {};
|
|
55
|
+
try { existingCfg = readAgentConfig(name) || {}; } catch { /* first sync */ }
|
|
56
|
+
const cfg = {
|
|
57
|
+
...existingCfg,
|
|
58
|
+
name,
|
|
59
|
+
display_name: c.displayName || existingCfg.display_name || name,
|
|
60
|
+
description: c.description ?? existingCfg.description ?? '',
|
|
61
|
+
man: {
|
|
62
|
+
...(existingCfg.man || {}),
|
|
63
|
+
agent_id: c.agentId,
|
|
64
|
+
...(c.villageAgentId ? { village_agent_id: c.villageAgentId } : {}),
|
|
65
|
+
handle: c.handle || name,
|
|
66
|
+
},
|
|
67
|
+
...(c.schedule ? { schedule: c.schedule } : {}),
|
|
68
|
+
...(c.brain ? { brain: c.brain } : {}),
|
|
69
|
+
};
|
|
70
|
+
writeAgentConfig(name, cfg);
|
|
71
|
+
|
|
72
|
+
// Keep an empty .env so the daemon's loader and 0600 expectations hold, but
|
|
73
|
+
// never overwrite secrets a person put here.
|
|
74
|
+
const envPath = path.join(agentDir, '.env');
|
|
75
|
+
if (!fs.existsSync(envPath)) fs.writeFileSync(envPath, '', { mode: 0o600 });
|
|
76
|
+
|
|
77
|
+
if (c.version) fs.writeFileSync(syncedVersionPath(agentDir), String(c.version));
|
|
78
|
+
return { models };
|
|
79
|
+
}
|
package/src/utils/api.js
CHANGED
|
@@ -302,6 +302,14 @@ export async function listMyAgents(options = {}) {
|
|
|
302
302
|
return response.data;
|
|
303
303
|
}
|
|
304
304
|
|
|
305
|
+
// The agent's published definition (prompt, tools, schedule, model settings,
|
|
306
|
+
// trained models). Studio republishes it on every save.
|
|
307
|
+
export async function getAgentPublishedConfig(id) {
|
|
308
|
+
const client = getNetworkClient();
|
|
309
|
+
const response = await client.get(`/agents/${id}/config`);
|
|
310
|
+
return response.data?.data ?? response.data;
|
|
311
|
+
}
|
|
312
|
+
|
|
305
313
|
export async function getAgent(id) {
|
|
306
314
|
const client = getNetworkClient();
|
|
307
315
|
const response = await client.get(`/agents/${id}`);
|