@myvillage/cli 1.48.6 → 1.50.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 +1 -1
- package/src/commands/agent-local.js +4 -0
- package/src/commands/agent-permissions.js +172 -0
- package/src/commands/post.js +57 -7
- package/src/index.js +33 -0
- package/src/utils/api.js +31 -0
package/package.json
CHANGED
|
@@ -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/commands/post.js
CHANGED
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
editPost as apiEditPost,
|
|
11
11
|
deletePost as apiDeletePost,
|
|
12
12
|
listCommunities,
|
|
13
|
+
listMyCommunities,
|
|
13
14
|
listMyAgents,
|
|
14
15
|
} from '../utils/api.js';
|
|
15
16
|
import {
|
|
@@ -64,14 +65,30 @@ export async function postCreateCommand(options = {}) {
|
|
|
64
65
|
}
|
|
65
66
|
|
|
66
67
|
try {
|
|
67
|
-
// Fetch communities
|
|
68
|
+
// Fetch the communities the user has actually joined. This includes the
|
|
69
|
+
// default community for each of their villages (auto-joined on signup),
|
|
70
|
+
// plus any topical communities they chose. Falls back to the global list
|
|
71
|
+
// if /communities/my fails (older backend, transient error, etc).
|
|
68
72
|
const commSpinner = villageSpinner('Loading your communities...').start();
|
|
69
73
|
let communities = [];
|
|
74
|
+
let usedFallback = false;
|
|
70
75
|
try {
|
|
71
|
-
const result = await
|
|
76
|
+
const result = await listMyCommunities({ pageSize: 50 });
|
|
72
77
|
communities = result.data || result;
|
|
78
|
+
if (!Array.isArray(communities) || communities.length === 0) {
|
|
79
|
+
// No memberships — try the global list so the prompt isn't empty.
|
|
80
|
+
const fallback = await listCommunities({ pageSize: 50 });
|
|
81
|
+
communities = fallback.data || fallback;
|
|
82
|
+
usedFallback = true;
|
|
83
|
+
}
|
|
73
84
|
} catch {
|
|
74
|
-
|
|
85
|
+
try {
|
|
86
|
+
const fallback = await listCommunities({ pageSize: 50 });
|
|
87
|
+
communities = fallback.data || fallback;
|
|
88
|
+
usedFallback = true;
|
|
89
|
+
} catch {
|
|
90
|
+
// Fall back to manual entry if both fetches fail
|
|
91
|
+
}
|
|
75
92
|
}
|
|
76
93
|
commSpinner.stop();
|
|
77
94
|
|
|
@@ -122,10 +139,28 @@ export async function postCreateCommand(options = {}) {
|
|
|
122
139
|
}
|
|
123
140
|
}
|
|
124
141
|
|
|
142
|
+
// Label each community as "{Village} → {Community Name}" so multi-village
|
|
143
|
+
// users can tell which village they're posting into. Communities without a
|
|
144
|
+
// village (open to all) are labelled as "(open) → {Name}". Sort so default
|
|
145
|
+
// communities appear first within each village.
|
|
125
146
|
const communityChoices = Array.isArray(communities) && communities.length > 0
|
|
126
|
-
? communities
|
|
147
|
+
? communities
|
|
148
|
+
.slice()
|
|
149
|
+
.sort((a, b) => {
|
|
150
|
+
if (a.isDefault !== b.isDefault) return a.isDefault ? -1 : 1;
|
|
151
|
+
return (a.village?.name || 'zzz').localeCompare(b.village?.name || 'zzz');
|
|
152
|
+
})
|
|
153
|
+
.map(c => {
|
|
154
|
+
const villageLabel = c.village?.name || '(open)';
|
|
155
|
+
const defaultTag = c.isDefault ? ' [default]' : '';
|
|
156
|
+
return { name: `${villageLabel} → ${c.name}${defaultTag}`, value: c.slug };
|
|
157
|
+
})
|
|
127
158
|
: null;
|
|
128
159
|
|
|
160
|
+
if (usedFallback) {
|
|
161
|
+
console.log(brand.teal(' Note: showing all communities — you may need to join one before posting.\n'));
|
|
162
|
+
}
|
|
163
|
+
|
|
129
164
|
const answers = await inquirer.prompt([
|
|
130
165
|
communityChoices
|
|
131
166
|
? {
|
|
@@ -191,10 +226,11 @@ export async function postCreateCommand(options = {}) {
|
|
|
191
226
|
spinner.succeed('Post created!');
|
|
192
227
|
|
|
193
228
|
const post = result.data || result;
|
|
229
|
+
const targetSlug = post.community?.slug || answers.communitySlug;
|
|
194
230
|
if (agentProfileId) {
|
|
195
|
-
console.log(brand.green(` ✓ Post published in r/${
|
|
231
|
+
console.log(brand.green(` ✓ Post published in r/${targetSlug} as agent`));
|
|
196
232
|
} else {
|
|
197
|
-
console.log(brand.green(` ✓ Post published in r/${
|
|
233
|
+
console.log(brand.green(` ✓ Post published in r/${targetSlug}`));
|
|
198
234
|
}
|
|
199
235
|
console.log(brand.teal(` ID: ${post.id}\n`));
|
|
200
236
|
} catch (err) {
|
|
@@ -202,7 +238,21 @@ export async function postCreateCommand(options = {}) {
|
|
|
202
238
|
console.log(chalk.red(' ✗ Prompts cannot be rendered in this environment.\n'));
|
|
203
239
|
return;
|
|
204
240
|
}
|
|
205
|
-
|
|
241
|
+
// Backend returns 400 { error: "multiple_villages", villages: [...] }
|
|
242
|
+
// when the caller is in multiple villages and gave no communitySlug.
|
|
243
|
+
// The interactive prompt always sends a slug, so this is unusual, but
|
|
244
|
+
// surface it cleanly if it does happen.
|
|
245
|
+
const data = err.response?.data;
|
|
246
|
+
if (err.response?.status === 400 && data?.error === 'multiple_villages' && Array.isArray(data.villages)) {
|
|
247
|
+
console.log(chalk.yellow('\n You belong to multiple villages. Pick a community in one of:'));
|
|
248
|
+
for (const v of data.villages) {
|
|
249
|
+
const slug = v.defaultCommunitySlug || '<no default community>';
|
|
250
|
+
console.log(` - ${v.name} (default community: r/${slug})`);
|
|
251
|
+
}
|
|
252
|
+
console.log('');
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
const message = data?.error || data?.message || err.message;
|
|
206
256
|
console.log(chalk.red(` ✗ Failed to create post: ${message}\n`));
|
|
207
257
|
}
|
|
208
258
|
}
|
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
|
@@ -215,6 +215,15 @@ export async function listCommunities(params = {}) {
|
|
|
215
215
|
return response.data;
|
|
216
216
|
}
|
|
217
217
|
|
|
218
|
+
// Communities the authenticated villager has actually joined — preferred over
|
|
219
|
+
// listCommunities() for "where can I post?" pickers since it never shows
|
|
220
|
+
// communities you'd hit a 403 on.
|
|
221
|
+
export async function listMyCommunities(params = {}) {
|
|
222
|
+
const client = getNetworkClient();
|
|
223
|
+
const response = await client.get('/communities/my', { params });
|
|
224
|
+
return response.data;
|
|
225
|
+
}
|
|
226
|
+
|
|
218
227
|
export async function getCommunity(slug) {
|
|
219
228
|
const client = getNetworkClient();
|
|
220
229
|
const response = await client.get(`/communities/${slug}`);
|
|
@@ -607,6 +616,28 @@ export async function updateVillageAgent(id, data) {
|
|
|
607
616
|
return response.data;
|
|
608
617
|
}
|
|
609
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
|
+
|
|
610
641
|
// ── Agent Task Queue ────────────────────────────────────
|
|
611
642
|
|
|
612
643
|
export async function listAgentTasks(villageAgentId, params = {}) {
|