@myvillage/cli 1.88.3 → 1.89.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.88.3",
3
+ "version": "1.89.1",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -23,7 +23,7 @@ import fs from 'fs';
23
23
  import path from 'path';
24
24
  import { getConfig } from '../utils/config.js';
25
25
  import { getAccessToken } from '../utils/auth.js';
26
- import { listLocalAgents, readAgentConfig } from '../utils/local-agent.js';
26
+ import { listLocalAgents, readAgentConfig, getAgentDir, writeAgentConfig } from '../utils/local-agent.js';
27
27
  import { startDaemon, stopDaemon, isDaemonRunning, getDaemonInfo } from './daemon.js';
28
28
 
29
29
  const RUNNER_STATE_FILE = path.join(os.homedir(), '.myvillage', 'runner.json');
@@ -101,6 +101,88 @@ async function request(networkBaseUrl, pathname, method, body, headers) {
101
101
  return { status: res.status, ok: res.ok, json };
102
102
  }
103
103
 
104
+ // ── Config sync (materialise an assigned agent from the control plane) ───────
105
+
106
+ // Records which published config version this machine last wrote, so we only
107
+ // re-write files when the definition actually changed.
108
+ function syncedVersionPath(agentDir) {
109
+ return path.join(agentDir, '.config-version');
110
+ }
111
+
112
+ function readSyncedVersion(agentDir) {
113
+ try {
114
+ return Number(fs.readFileSync(syncedVersionPath(agentDir), 'utf8').trim()) || 0;
115
+ } catch {
116
+ return 0;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Write an assigned agent's files from its published definition, creating the
122
+ * agent directory if this machine has never run it. Returns true when the agent
123
+ * is ready to start.
124
+ *
125
+ * `.env` is never written here: secrets stay per-machine until they can be
126
+ * envelope-encrypted (see AgentConfig in the schema). An existing local `.env`
127
+ * is left untouched.
128
+ */
129
+ async function syncAgentConfig(ctx, assignment) {
130
+ const { agentProfileId, handle, configVersion } = assignment;
131
+ const name = handle || agentProfileId;
132
+ const agentDir = getAgentDir(name);
133
+ const exists = fs.existsSync(path.join(agentDir, 'agent.config.yaml'));
134
+
135
+ // Nothing published: fall back to whatever is already on this machine.
136
+ if (!configVersion) return exists;
137
+ // Already current.
138
+ if (exists && readSyncedVersion(agentDir) === configVersion) return true;
139
+
140
+ const res = await ctx.cp(`/agents/${agentProfileId}/config`);
141
+ if (!res.ok) {
142
+ log(`could not fetch config for ${name} (${res.status}) — leaving local files alone`);
143
+ return exists;
144
+ }
145
+ const c = res.json.data;
146
+
147
+ fs.mkdirSync(agentDir, { recursive: true });
148
+ if (typeof c.promptMd === 'string' && c.promptMd) {
149
+ fs.writeFileSync(path.join(agentDir, 'prompt.md'), c.promptMd);
150
+ }
151
+ if (typeof c.toolsYaml === 'string' && c.toolsYaml) {
152
+ fs.writeFileSync(path.join(agentDir, 'tools.yaml'), c.toolsYaml);
153
+ }
154
+
155
+ // Compose agent.config.yaml from server-known identity + published schedule
156
+ // and brain, preserving anything already set locally that the control plane
157
+ // does not own.
158
+ let existingCfg = {};
159
+ try { existingCfg = readAgentConfig(name) || {}; } catch { /* first sync */ }
160
+ const cfg = {
161
+ ...existingCfg,
162
+ name,
163
+ display_name: c.displayName || existingCfg.display_name || name,
164
+ description: c.description ?? existingCfg.description ?? '',
165
+ man: {
166
+ ...(existingCfg.man || {}),
167
+ agent_id: c.agentId,
168
+ ...(c.villageAgentId ? { village_agent_id: c.villageAgentId } : {}),
169
+ handle: c.handle || name,
170
+ },
171
+ ...(c.schedule ? { schedule: c.schedule } : {}),
172
+ ...(c.brain ? { brain: c.brain } : {}),
173
+ };
174
+ writeAgentConfig(name, cfg);
175
+
176
+ // Keep an empty .env so the daemon's loader and 0600 expectations hold, but
177
+ // never overwrite secrets a person put here.
178
+ const envPath = path.join(agentDir, '.env');
179
+ if (!fs.existsSync(envPath)) fs.writeFileSync(envPath, '', { mode: 0o600 });
180
+
181
+ fs.writeFileSync(syncedVersionPath(agentDir), String(configVersion));
182
+ log(`synced ${name} config v${configVersion}${exists ? '' : ' (new on this machine)'}`);
183
+ return true;
184
+ }
185
+
104
186
  // ── Local agent indexing ─────────────────────────────────────────────────────
105
187
 
106
188
  // Map control-plane agentProfileId -> local agent { name, config } via the
@@ -238,9 +320,28 @@ async function reconcileOnce(ctx) {
238
320
 
239
321
  // Start desired-running agents we don't yet own+run here.
240
322
  for (const a of desiredRunning) {
241
- const entry = local.get(a.agentProfileId);
323
+ // Materialise (or refresh) the agent from its published definition, so a
324
+ // machine that has never seen this agent can still run it.
325
+ let entry = local.get(a.agentProfileId);
326
+ try {
327
+ const ready = await syncAgentConfig(ctx, a);
328
+ if (ready && !entry) {
329
+ const name = a.handle || a.agentProfileId;
330
+ entry = { name, config: readAgentConfig(name) };
331
+ local.set(a.agentProfileId, entry);
332
+ } else if (ready && entry) {
333
+ // Pick up an edited prompt/tools on the next start.
334
+ entry.config = readAgentConfig(entry.name);
335
+ }
336
+ } catch (err) {
337
+ log(`config sync failed for ${a.handle || a.agentProfileId}`, err?.message || err);
338
+ }
339
+
242
340
  if (!entry) {
243
- log(`assigned agent ${a.handle || a.agentProfileId} has no local config on this machine — skipping (provision it first)`);
341
+ log(
342
+ `assigned agent ${a.handle || a.agentProfileId} has no config on this machine and none published — ` +
343
+ `open it in Studio to publish its setup`,
344
+ );
244
345
  continue;
245
346
  }
246
347
  if (ctx.leases.has(a.agentProfileId) && isDaemonRunning(entry.name)) continue;