@myvillage/cli 1.49.0 → 1.51.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.49.0",
3
+ "version": "1.51.0",
4
4
  "description": "MyVillageOS CLI for community developers",
5
5
  "type": "module",
6
6
  "bin": {
@@ -245,16 +245,38 @@ Guidelines:
245
245
  // "find any README under the workspace" can do
246
246
  // list_allowed_directories → list_directory → search_files → ...
247
247
  // until it's actually done.
248
+ //
249
+ // Anthropic prompt caching: we mark the system block as ephemeral so
250
+ // everything before it (tool schemas + system prompt) is cached.
251
+ // For Anthropic, cached reads are ~10x cheaper than fresh input —
252
+ // critical because MCP tool schemas alone can be 20–40k tokens that
253
+ // would otherwise be re-billed at every step of every loop iteration.
254
+ // The breakpoint is namespaced under `anthropic`, so OpenAI calls
255
+ // silently ignore it. Cache TTL is ~5 min; agents that loop more
256
+ // often than that stay warm and pay full price only on the first hit.
248
257
  const result = await generateText({
249
258
  model,
250
- system: systemPrompt,
251
- prompt: context,
259
+ messages: [
260
+ {
261
+ role: 'system',
262
+ content: systemPrompt,
263
+ providerOptions: {
264
+ anthropic: { cacheControl: { type: 'ephemeral' } },
265
+ },
266
+ },
267
+ { role: 'user', content: context },
268
+ ],
252
269
  tools,
253
270
  stopWhen: stepCountIs(10),
254
271
  maxOutputTokens: maxTokens,
255
272
  });
256
273
 
257
- // Log LLM response
274
+ // Log LLM response. Cache metrics come from the Anthropic provider
275
+ // metadata — surfacing them lets `agent logs` show whether caching is
276
+ // landing. `cacheReadInputTokens` should grow on the 2nd+ iteration in
277
+ // a 5-min window; if it stays 0, the prefix isn't matching (usually
278
+ // because something upstream of the breakpoint is varying per call).
279
+ const anthropicMeta = result.providerMetadata?.anthropic || {};
258
280
  logActivity(agentDir, {
259
281
  type: 'llm_response',
260
282
  text: (result.text || '').slice(0, 500),
@@ -262,6 +284,8 @@ Guidelines:
262
284
  prompt: result.usage?.inputTokens || 0,
263
285
  completion: result.usage?.outputTokens || 0,
264
286
  total: result.usage?.totalTokens || 0,
287
+ cacheCreate: anthropicMeta.cacheCreationInputTokens || 0,
288
+ cacheRead: anthropicMeta.cacheReadInputTokens || 0,
265
289
  },
266
290
  });
267
291
 
@@ -919,6 +919,10 @@ export async function agentTaskAssignCommand(name, options = {}) {
919
919
  instruction: options.instruction,
920
920
  input: inputObj,
921
921
  priority: options.priority ? Number(options.priority) : 5,
922
+ // Identifies the agent making the request so the target's policy can
923
+ // match against `allowedAgents`. Server verifies the caller owns this
924
+ // handle; coarse villager/village checks still apply if absent.
925
+ ...(options.as && { callingAgentHandle: options.as }),
922
926
  });
923
927
  const task = result.task || result;
924
928
  console.log(brand.green(` \u2713 Task ${task.id} assigned (${task.taskType}, status=${task.status}).\n`));
@@ -0,0 +1,172 @@
1
+ import chalk from 'chalk';
2
+ import { villageSpinner, brand } from '../utils/brand.js';
3
+ import { isAuthenticated } from '../utils/auth.js';
4
+ import {
5
+ listMyAgents,
6
+ getVillageAgentPolicy,
7
+ setVillageAgentPolicy,
8
+ } from '../utils/api.js';
9
+
10
+ // Resolve a CLI-friendly handle to the underlying VillageAgent id.
11
+ async function resolveVillageAgentId(handle) {
12
+ const result = await listMyAgents();
13
+ const agents = result.data || result;
14
+ if (!Array.isArray(agents)) return null;
15
+ const match = agents.find(
16
+ (a) => a.handle === handle || a.villageAgent?.handle === handle,
17
+ );
18
+ if (!match) return null;
19
+ return match.villageAgent?.id || match.villageAgentId || match.id;
20
+ }
21
+
22
+ function printPolicy(policy) {
23
+ const { allowedAgents = [], allowedVillagers = [], allowedVillages = [] } = policy || {};
24
+ if (
25
+ allowedAgents.length === 0 &&
26
+ allowedVillagers.length === 0 &&
27
+ allowedVillages.length === 0
28
+ ) {
29
+ console.log(brand.teal('\n Policy is empty — only the agent owner can assign tasks.\n'));
30
+ return;
31
+ }
32
+ console.log('');
33
+ console.log(chalk.bold(' Task-Assignment Allowlist'));
34
+ console.log('');
35
+ if (allowedAgents.length) {
36
+ console.log(` ${chalk.dim('Agents ')} ${allowedAgents.join(', ')}`);
37
+ }
38
+ if (allowedVillagers.length) {
39
+ console.log(` ${chalk.dim('Villagers ')} ${allowedVillagers.join(', ')}`);
40
+ }
41
+ if (allowedVillages.length) {
42
+ console.log(` ${chalk.dim('Villages ')} ${allowedVillages.join(', ')}`);
43
+ }
44
+ console.log('');
45
+ }
46
+
47
+ function fail(message) {
48
+ console.log(chalk.red(` ✗ ${message}`));
49
+ process.exitCode = 1;
50
+ }
51
+
52
+ async function loadCurrentPolicy(agentId) {
53
+ const result = await getVillageAgentPolicy(agentId);
54
+ return {
55
+ allowedAgents: [],
56
+ allowedVillagers: [],
57
+ allowedVillages: [],
58
+ ...(result.policy || {}),
59
+ };
60
+ }
61
+
62
+ export async function agentPermissionsShowCommand(handle) {
63
+ if (!isAuthenticated()) return fail('Authentication required. Run `myvillage login` first.');
64
+
65
+ const spinner = villageSpinner(`Loading policy for ${handle}...`).start();
66
+ try {
67
+ const agentId = await resolveVillageAgentId(handle);
68
+ if (!agentId) {
69
+ spinner.fail(`No agent found with handle: ${handle}`);
70
+ return;
71
+ }
72
+ const policy = await loadCurrentPolicy(agentId);
73
+ spinner.stop();
74
+ printPolicy(policy);
75
+ } catch (err) {
76
+ const message = err.response?.data?.error || err.message;
77
+ spinner.fail(`Failed to load policy: ${message}`);
78
+ }
79
+ }
80
+
81
+ export async function agentPermissionsAllowCommand(handle, options) {
82
+ if (!isAuthenticated()) return fail('Authentication required. Run `myvillage login` first.');
83
+
84
+ const newAgents = toArray(options.agent);
85
+ const newVillagers = toArray(options.villager);
86
+ const newVillages = toArray(options.village);
87
+
88
+ if (newAgents.length + newVillagers.length + newVillages.length === 0) {
89
+ return fail('Specify at least one of --agent, --villager, or --village');
90
+ }
91
+
92
+ const spinner = villageSpinner(`Updating policy for ${handle}...`).start();
93
+ try {
94
+ const agentId = await resolveVillageAgentId(handle);
95
+ if (!agentId) {
96
+ spinner.fail(`No agent found with handle: ${handle}`);
97
+ return;
98
+ }
99
+ const current = await loadCurrentPolicy(agentId);
100
+ const next = {
101
+ allowedAgents: union(current.allowedAgents, newAgents),
102
+ allowedVillagers: union(current.allowedVillagers, newVillagers),
103
+ allowedVillages: union(current.allowedVillages, newVillages),
104
+ };
105
+ const result = await setVillageAgentPolicy(agentId, next);
106
+ spinner.succeed(`Allowlist updated for ${handle}`);
107
+ printPolicy(result.policy);
108
+ } catch (err) {
109
+ const data = err.response?.data;
110
+ if (data?.unknown?.length) {
111
+ spinner.fail(`Unknown entries: ${data.unknown.join(', ')}`);
112
+ return;
113
+ }
114
+ const message = data?.error || err.message;
115
+ spinner.fail(`Failed to update policy: ${message}`);
116
+ }
117
+ }
118
+
119
+ export async function agentPermissionsRevokeCommand(handle, options) {
120
+ if (!isAuthenticated()) return fail('Authentication required. Run `myvillage login` first.');
121
+
122
+ const dropAgents = toArray(options.agent);
123
+ const dropVillagers = toArray(options.villager);
124
+ const dropVillages = toArray(options.village);
125
+ const clearAll = options.all === true;
126
+
127
+ if (
128
+ !clearAll &&
129
+ dropAgents.length + dropVillagers.length + dropVillages.length === 0
130
+ ) {
131
+ return fail('Specify --all to clear, or at least one of --agent/--villager/--village');
132
+ }
133
+
134
+ const spinner = villageSpinner(`Updating policy for ${handle}...`).start();
135
+ try {
136
+ const agentId = await resolveVillageAgentId(handle);
137
+ if (!agentId) {
138
+ spinner.fail(`No agent found with handle: ${handle}`);
139
+ return;
140
+ }
141
+ const next = clearAll
142
+ ? { allowedAgents: [], allowedVillagers: [], allowedVillages: [] }
143
+ : await (async () => {
144
+ const current = await loadCurrentPolicy(agentId);
145
+ return {
146
+ allowedAgents: difference(current.allowedAgents, dropAgents),
147
+ allowedVillagers: difference(current.allowedVillagers, dropVillagers),
148
+ allowedVillages: difference(current.allowedVillages, dropVillages),
149
+ };
150
+ })();
151
+ const result = await setVillageAgentPolicy(agentId, next);
152
+ spinner.succeed(`Allowlist updated for ${handle}`);
153
+ printPolicy(result.policy);
154
+ } catch (err) {
155
+ const message = err.response?.data?.error || err.message;
156
+ spinner.fail(`Failed to update policy: ${message}`);
157
+ }
158
+ }
159
+
160
+ function toArray(v) {
161
+ if (!v) return [];
162
+ return Array.isArray(v) ? v : [v];
163
+ }
164
+
165
+ function union(a, b) {
166
+ return Array.from(new Set([...(a || []), ...b]));
167
+ }
168
+
169
+ function difference(a, b) {
170
+ const drop = new Set(b);
171
+ return (a || []).filter((x) => !drop.has(x));
172
+ }
package/src/index.js CHANGED
@@ -50,6 +50,11 @@ import {
50
50
  agentLeaveCommand,
51
51
  agentRunCommand,
52
52
  } from './commands/agent.js';
53
+ import {
54
+ agentPermissionsShowCommand,
55
+ agentPermissionsAllowCommand,
56
+ agentPermissionsRevokeCommand,
57
+ } from './commands/agent-permissions.js';
53
58
  import {
54
59
  agentStartCommand,
55
60
  agentStopCommand,
@@ -419,6 +424,33 @@ export function run() {
419
424
  .description('Deactivate an agent')
420
425
  .action(agentDeleteCommand);
421
426
 
427
+ // ── Agent permissions (task-assignment allowlist) ───
428
+ const agentPermissionsCmd = agentCmd
429
+ .command('permissions')
430
+ .description('Manage who else can assign tasks to this agent');
431
+
432
+ agentPermissionsCmd
433
+ .command('show <handle>')
434
+ .description('Show the task-assignment allowlist for an agent')
435
+ .action(agentPermissionsShowCommand);
436
+
437
+ agentPermissionsCmd
438
+ .command('allow <handle>')
439
+ .description('Add entries to the allowlist (repeat flags to add multiple)')
440
+ .option('-a, --agent <handle...>', 'AgentProfile handle, e.g. teacher_mvp')
441
+ .option('-v, --villager <id...>', 'Villager id, e.g. MVP-99057')
442
+ .option('-V, --village <id...>', 'Village id, e.g. VLG-JAX-001')
443
+ .action(agentPermissionsAllowCommand);
444
+
445
+ agentPermissionsCmd
446
+ .command('revoke <handle>')
447
+ .description('Remove entries from the allowlist, or pass --all to clear it')
448
+ .option('-a, --agent <handle...>', 'AgentProfile handle to remove')
449
+ .option('-v, --villager <id...>', 'Villager id to remove')
450
+ .option('-V, --village <id...>', 'Village id to remove')
451
+ .option('--all', 'Clear the entire allowlist (back to owner-only)')
452
+ .action(agentPermissionsRevokeCommand);
453
+
422
454
  agentCmd
423
455
  .command('join <handle> <community-slug>')
424
456
  .description('Join a community as an agent')
@@ -509,6 +541,7 @@ export function run() {
509
541
  .option('--instruction <text>', 'Free-text instruction (required for CLIENT_TASK)')
510
542
  .option('--input <json>', 'JSON-encoded structured input payload')
511
543
  .option('--priority <n>', 'Priority 1-10 (lower runs first)', '5')
544
+ .option('--as <handle>', "Handle of the agent making this call (matches target's allowedAgents policy)")
512
545
  .action(agentTaskAssignCommand);
513
546
 
514
547
  agentCmd
package/src/utils/api.js CHANGED
@@ -616,6 +616,28 @@ export async function updateVillageAgent(id, data) {
616
616
  return response.data;
617
617
  }
618
618
 
619
+ // ── Task-assignment policy ──────────────────────────────
620
+ // Controls which non-owner callers (other villagers / their agents) may POST
621
+ // tasks to this agent's queue. Server stores readable identifiers:
622
+ // { allowedAgents: ["handle"], allowedVillagers: ["MVP-..."], allowedVillages: ["VLG-..."] }
623
+
624
+ export async function getVillageAgentPolicy(villageAgentId) {
625
+ const client = getPlatformClient();
626
+ const response = await client.get(
627
+ `/village-agents/${encodeURIComponent(villageAgentId)}/policy`,
628
+ );
629
+ return response.data;
630
+ }
631
+
632
+ export async function setVillageAgentPolicy(villageAgentId, policy) {
633
+ const client = getPlatformClient();
634
+ const response = await client.put(
635
+ `/village-agents/${encodeURIComponent(villageAgentId)}/policy`,
636
+ policy,
637
+ );
638
+ return response.data;
639
+ }
640
+
619
641
  // ── Agent Task Queue ────────────────────────────────────
620
642
 
621
643
  export async function listAgentTasks(villageAgentId, params = {}) {