@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@myvillage/cli",
3
- "version": "2.13.1",
3
+ "version": "2.13.2",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, writeAgentConfig } from '../utils/local-agent.js';
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 { writeSyncedModel } from '../utils/agent-models.js';
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 c = res.json.data;
171
-
172
- fs.mkdirSync(agentDir, { recursive: true });
173
- if (typeof c.promptMd === 'string' && c.promptMd) {
174
- fs.writeFileSync(path.join(agentDir, 'prompt.md'), c.promptMd);
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) and a
1319
- // link to the existing VillageAgent task queue.
1320
- // What doesn't: custom prompt edits, tools.yaml customizations, and any
1321
- // API keys. Those stay machine-local and you'll need to redo them if you
1322
- // want an exact replica. The scaffold defaults are a reasonable starting
1323
- // point that will boot.
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
- // Default scaffold: just the always-on MyVillage MCP tool. Users can add
1369
- // more via `agent add-tool` once they're up.
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
- console.log('');
1408
- console.log(brand.teal(' Defaults that may need tweaking:'));
1409
- console.log(` - ${brand.gold('prompt.md')} (system prompt — generic template)`);
1410
- console.log(` - ${brand.gold('tools.yaml')} (only the MyVillage tool is enabled)`);
1411
- console.log(` - ${brand.gold('agent.config.yaml')} (anthropic + claude-sonnet by default)`);
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}`);