@myvillage/cli 1.17.0 → 1.21.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/agent-runtime/loop.js +141 -10
- package/src/commands/agent-local.js +41 -0
- package/src/commands/api-key.js +172 -0
- package/src/index.js +48 -0
- package/src/utils/agent-scaffolder.js +8 -0
- package/src/utils/api.js +48 -0
package/package.json
CHANGED
|
@@ -110,6 +110,14 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
110
110
|
let mentionsFound = 0;
|
|
111
111
|
// Hoisted so the catch block can mark in-flight tasks FAILED.
|
|
112
112
|
let activeTask = null;
|
|
113
|
+
// Tracks whether the task's tool calls actually succeeded. The LLM
|
|
114
|
+
// sometimes "summarizes" a tool error into a falsely-confident final
|
|
115
|
+
// response — we don't want to trust the model's word about success.
|
|
116
|
+
const taskActionAudit = {
|
|
117
|
+
actionToolsCalled: 0,
|
|
118
|
+
actionToolsSucceeded: 0,
|
|
119
|
+
toolErrors: [], // { tool, message }
|
|
120
|
+
};
|
|
113
121
|
|
|
114
122
|
try {
|
|
115
123
|
// Read prompt.md fresh each iteration (villager may have edited it)
|
|
@@ -178,7 +186,8 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
178
186
|
},
|
|
179
187
|
});
|
|
180
188
|
|
|
181
|
-
// Log tool calls and count activity
|
|
189
|
+
// Log tool calls and count activity. Also audit action-tool success
|
|
190
|
+
// so we don't trust the model's final text about whether a task worked.
|
|
182
191
|
if (result.steps?.length) {
|
|
183
192
|
for (const step of result.steps) {
|
|
184
193
|
if (step.toolCalls?.length) {
|
|
@@ -193,19 +202,25 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
193
202
|
for (let i = 0; i < step.toolResults.length; i++) {
|
|
194
203
|
const tr = step.toolResults[i];
|
|
195
204
|
const args = step.toolCalls[i]?.args;
|
|
205
|
+
const errored = isToolResultError(tr);
|
|
206
|
+
auditToolCall(taskActionAudit, tr.toolName, errored, tr);
|
|
196
207
|
logActivity(agentDir, {
|
|
197
208
|
type: 'tool_call',
|
|
198
209
|
tool: tr.toolName,
|
|
199
210
|
args,
|
|
200
|
-
result:
|
|
211
|
+
result: summarizeToolResult(tr),
|
|
212
|
+
ok: !errored,
|
|
201
213
|
});
|
|
202
214
|
}
|
|
203
215
|
} else if (step.toolResults?.length) {
|
|
204
216
|
for (const tr of step.toolResults) {
|
|
217
|
+
const errored = isToolResultError(tr);
|
|
218
|
+
auditToolCall(taskActionAudit, tr.toolName, errored, tr);
|
|
205
219
|
logActivity(agentDir, {
|
|
206
220
|
type: 'tool_call',
|
|
207
221
|
tool: tr.toolName,
|
|
208
|
-
result:
|
|
222
|
+
result: summarizeToolResult(tr),
|
|
223
|
+
ok: !errored,
|
|
209
224
|
});
|
|
210
225
|
}
|
|
211
226
|
}
|
|
@@ -216,6 +231,7 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
216
231
|
if (tc.toolName === 'post_create') activity.postsCreated++;
|
|
217
232
|
if (tc.toolName === 'comment_create') activity.commentsCreated++;
|
|
218
233
|
if (tc.toolName === 'vote_cast') activity.votesGiven++;
|
|
234
|
+
// No paired result here — assume executed, can't audit.
|
|
219
235
|
logActivity(agentDir, {
|
|
220
236
|
type: 'tool_call',
|
|
221
237
|
tool: tc.toolName,
|
|
@@ -249,15 +265,48 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
249
265
|
// Keep only last 50 actions to bound memory
|
|
250
266
|
if (recentActions.length > 50) recentActions.splice(0, recentActions.length - 50);
|
|
251
267
|
|
|
252
|
-
// If a task was being processed,
|
|
268
|
+
// If a task was being processed, decide success vs. failure based on
|
|
269
|
+
// whether the action tools actually succeeded — not on the model's
|
|
270
|
+
// self-report. The LLM sometimes claims "I posted!" after a tool error.
|
|
253
271
|
if (activeTask && config.man?.village_agent_id) {
|
|
272
|
+
const shouldFail =
|
|
273
|
+
taskActionAudit.actionToolsCalled > 0 &&
|
|
274
|
+
taskActionAudit.actionToolsSucceeded === 0;
|
|
275
|
+
|
|
254
276
|
try {
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
277
|
+
if (shouldFail) {
|
|
278
|
+
const firstError = taskActionAudit.toolErrors[0];
|
|
279
|
+
const errorMessage = firstError
|
|
280
|
+
? `${firstError.tool} failed: ${firstError.message}`
|
|
281
|
+
: 'Action tools called but all failed';
|
|
282
|
+
await completeAgentTask(config.man.village_agent_id, activeTask.id, {
|
|
283
|
+
errorMessage,
|
|
284
|
+
output: {
|
|
285
|
+
text: result.text || '',
|
|
286
|
+
toolCalls: activity.toolCalls,
|
|
287
|
+
toolErrors: taskActionAudit.toolErrors,
|
|
288
|
+
note: 'Marked FAILED because the action tools did not succeed. The model\'s text may claim success but the underlying tool calls errored.',
|
|
289
|
+
},
|
|
290
|
+
tokensUsed: (result.usage?.promptTokens || 0) + (result.usage?.completionTokens || 0),
|
|
291
|
+
durationMs: Date.now() - loopStart,
|
|
292
|
+
});
|
|
293
|
+
logActivity(agentDir, {
|
|
294
|
+
type: 'task_failed',
|
|
295
|
+
taskId: activeTask.id,
|
|
296
|
+
reason: errorMessage,
|
|
297
|
+
});
|
|
298
|
+
} else {
|
|
299
|
+
await completeAgentTask(config.man.village_agent_id, activeTask.id, {
|
|
300
|
+
output: {
|
|
301
|
+
text: result.text || '',
|
|
302
|
+
toolCalls: activity.toolCalls,
|
|
303
|
+
toolErrors: taskActionAudit.toolErrors.length > 0 ? taskActionAudit.toolErrors : undefined,
|
|
304
|
+
},
|
|
305
|
+
tokensUsed: (result.usage?.promptTokens || 0) + (result.usage?.completionTokens || 0),
|
|
306
|
+
durationMs: Date.now() - loopStart,
|
|
307
|
+
});
|
|
308
|
+
logActivity(agentDir, { type: 'task_completed', taskId: activeTask.id });
|
|
309
|
+
}
|
|
261
310
|
} catch (taskErr) {
|
|
262
311
|
logActivity(agentDir, { type: 'error', error: `Failed to mark task complete: ${taskErr.message}` });
|
|
263
312
|
}
|
|
@@ -343,6 +392,88 @@ function updateHeartbeat(agentDir) {
|
|
|
343
392
|
}
|
|
344
393
|
}
|
|
345
394
|
|
|
395
|
+
// ── Tool result auditing ───────────────────────────────────────────
|
|
396
|
+
// The Vercel AI SDK returns tool results in a few different shapes
|
|
397
|
+
// depending on the underlying transport. These helpers normalise them
|
|
398
|
+
// so we can detect errors regardless of which path is in play.
|
|
399
|
+
|
|
400
|
+
// Tools that take a real, externally-visible action on the platform.
|
|
401
|
+
// We use this set to decide whether a task that ran but didn't actually
|
|
402
|
+
// succeed (e.g. a 404 from post_create) should be marked FAILED.
|
|
403
|
+
const ACTION_TOOLS = new Set([
|
|
404
|
+
'post_create',
|
|
405
|
+
'comment_create',
|
|
406
|
+
'vote_cast',
|
|
407
|
+
'knowledge_submit',
|
|
408
|
+
'community_join',
|
|
409
|
+
'community_leave',
|
|
410
|
+
'community_event_create',
|
|
411
|
+
'community_event_register',
|
|
412
|
+
'community_event_unregister',
|
|
413
|
+
'community_event_cancel',
|
|
414
|
+
'moment_create',
|
|
415
|
+
'pulse_create',
|
|
416
|
+
'agent_join_community',
|
|
417
|
+
'agent_leave_community',
|
|
418
|
+
'wallet_send',
|
|
419
|
+
'wallet_tip',
|
|
420
|
+
'wisdom_import',
|
|
421
|
+
'task_assign',
|
|
422
|
+
'task_complete',
|
|
423
|
+
'task_retry',
|
|
424
|
+
]);
|
|
425
|
+
|
|
426
|
+
function flattenToolResultText(tr) {
|
|
427
|
+
if (!tr) return '';
|
|
428
|
+
const r = tr.result;
|
|
429
|
+
if (typeof r === 'string') return r;
|
|
430
|
+
if (Array.isArray(r?.content)) {
|
|
431
|
+
return r.content
|
|
432
|
+
.map(c => (typeof c === 'string' ? c : c?.text || ''))
|
|
433
|
+
.filter(Boolean)
|
|
434
|
+
.join(' ');
|
|
435
|
+
}
|
|
436
|
+
try { return JSON.stringify(r); } catch { return ''; }
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function isToolResultError(tr) {
|
|
440
|
+
if (!tr) return false;
|
|
441
|
+
// Explicit MCP / Vercel AI SDK error flags
|
|
442
|
+
if (tr.isError === true) return true;
|
|
443
|
+
if (tr.result?.isError === true) return true;
|
|
444
|
+
if (Array.isArray(tr.result?.content) && tr.result.content.some(c => c?.isError === true)) {
|
|
445
|
+
return true;
|
|
446
|
+
}
|
|
447
|
+
// Heuristic fallback: look for HTTP-error and well-known failure phrases
|
|
448
|
+
// in the result text. Conservative; doesn't false-positive on prose like
|
|
449
|
+
// "the user was unauthorized to do X" because we anchor on word boundaries.
|
|
450
|
+
const text = flattenToolResultText(tr);
|
|
451
|
+
if (!text) return false;
|
|
452
|
+
return /\b(40[0-9]|50[0-9])\b/.test(text) ||
|
|
453
|
+
/\b(not found|unauthorized|forbidden|invalid|insufficient_quota|authentication failed)\b/i.test(text);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function auditToolCall(audit, toolName, errored, tr) {
|
|
457
|
+
if (ACTION_TOOLS.has(toolName)) {
|
|
458
|
+
audit.actionToolsCalled++;
|
|
459
|
+
if (!errored) {
|
|
460
|
+
audit.actionToolsSucceeded++;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (errored) {
|
|
464
|
+
audit.toolErrors.push({
|
|
465
|
+
tool: toolName,
|
|
466
|
+
message: flattenToolResultText(tr).slice(0, 300) || 'unknown error',
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function summarizeToolResult(tr) {
|
|
472
|
+
const text = flattenToolResultText(tr);
|
|
473
|
+
if (!text) return 'ok';
|
|
474
|
+
return text.slice(0, 200);
|
|
475
|
+
}
|
|
476
|
+
|
|
346
477
|
// Pull up to 5 pending tasks and claim the first one we can win the race for.
|
|
347
478
|
// Returns the claimed task or null. Errors are swallowed and logged — the loop
|
|
348
479
|
// should keep running on transient backend issues.
|
|
@@ -24,6 +24,8 @@ import {
|
|
|
24
24
|
deleteAgentMemoryEntry,
|
|
25
25
|
listKnowledgeFiltered,
|
|
26
26
|
shareKnowledgeAsAgent,
|
|
27
|
+
retryAgentTask,
|
|
28
|
+
retryFailedAgentTasks,
|
|
27
29
|
} from '../utils/api.js';
|
|
28
30
|
import {
|
|
29
31
|
getAgentDir,
|
|
@@ -880,6 +882,45 @@ export async function reportTaskOutcome(villageAgentId, taskId, outcome) {
|
|
|
880
882
|
return completeAgentTask(villageAgentId, taskId, outcome);
|
|
881
883
|
}
|
|
882
884
|
|
|
885
|
+
export async function agentTaskRetryCommand(name, taskId) {
|
|
886
|
+
if (!isAuthenticated()) {
|
|
887
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
const villageAgentId = resolveVillageAgentId(name);
|
|
891
|
+
if (!villageAgentId) return;
|
|
892
|
+
if (!taskId) {
|
|
893
|
+
console.log(chalk.red(' ✗ Usage: myvillage agent task-retry <name> <taskId>\n'));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
try {
|
|
898
|
+
await retryAgentTask(villageAgentId, taskId);
|
|
899
|
+
console.log(brand.green(` ✓ Task ${taskId} reset to PENDING. Agent will re-claim on next poll.\n`));
|
|
900
|
+
} catch (err) {
|
|
901
|
+
const msg = err.response?.data?.error || err.message;
|
|
902
|
+
console.log(chalk.red(` ✗ Retry failed: ${msg}\n`));
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
export async function agentTaskRetryFailedCommand(name, options = {}) {
|
|
907
|
+
if (!isAuthenticated()) {
|
|
908
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
const villageAgentId = resolveVillageAgentId(name);
|
|
912
|
+
if (!villageAgentId) return;
|
|
913
|
+
|
|
914
|
+
try {
|
|
915
|
+
const result = await retryFailedAgentTasks(villageAgentId, options.filter);
|
|
916
|
+
const filterNote = options.filter ? chalk.dim(` (filter: ${options.filter})`) : '';
|
|
917
|
+
console.log(brand.green(` ✓ ${result.retried} task(s) reset to PENDING${filterNote}.\n`));
|
|
918
|
+
} catch (err) {
|
|
919
|
+
const msg = err.response?.data?.error || err.message;
|
|
920
|
+
console.log(chalk.red(` ✗ Bulk retry failed: ${msg}\n`));
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
|
|
883
924
|
// ── Memory (short-term KV) and Recall (long-term searchable) ────
|
|
884
925
|
|
|
885
926
|
function resolveAgentProfileId(name) {
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
4
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
5
|
+
import {
|
|
6
|
+
createApiKey,
|
|
7
|
+
listApiKeys,
|
|
8
|
+
revokeApiKey,
|
|
9
|
+
listAllowedScopes,
|
|
10
|
+
} from '../utils/api.js';
|
|
11
|
+
|
|
12
|
+
// ── create ──────────────────────────────────────────────
|
|
13
|
+
|
|
14
|
+
export async function apiKeyCreateCommand(options = {}) {
|
|
15
|
+
if (!isAuthenticated()) {
|
|
16
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!options.name) {
|
|
21
|
+
console.log(chalk.red(' ✗ --name is required (e.g. --name "claude-desktop")\n'));
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
let scopes;
|
|
26
|
+
if (options.scopes) {
|
|
27
|
+
scopes = options.scopes.split(',').map(s => s.trim()).filter(Boolean);
|
|
28
|
+
} else {
|
|
29
|
+
// Interactive picker — fetch what the caller is allowed to mint
|
|
30
|
+
const spinner = villageSpinner('Loading available scopes...').start();
|
|
31
|
+
let allowed;
|
|
32
|
+
try {
|
|
33
|
+
const result = await listAllowedScopes();
|
|
34
|
+
allowed = result.allowedScopes || {};
|
|
35
|
+
spinner.stop();
|
|
36
|
+
} catch (err) {
|
|
37
|
+
spinner.fail(`Failed to load scopes: ${err.response?.data?.error || err.message}`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const scopeKeys = Object.keys(allowed);
|
|
41
|
+
if (scopeKeys.length === 0) {
|
|
42
|
+
console.log(chalk.yellow(' ⚠ Your account has no API scopes available.\n'));
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const { picked } = await inquirer.prompt([{
|
|
46
|
+
type: 'checkbox',
|
|
47
|
+
name: 'picked',
|
|
48
|
+
message: 'Pick the scopes this key should carry (Space to toggle, Enter to confirm):',
|
|
49
|
+
pageSize: Math.min(scopeKeys.length, 15),
|
|
50
|
+
choices: scopeKeys.map(s => ({
|
|
51
|
+
name: `${s} ${chalk.dim('— ' + allowed[s])}`,
|
|
52
|
+
value: s,
|
|
53
|
+
checked: s === 'agents:tasks:read' || s === 'agents:tasks:write',
|
|
54
|
+
})),
|
|
55
|
+
validate: (input) => input.length > 0 || 'Pick at least one scope.',
|
|
56
|
+
}]);
|
|
57
|
+
scopes = picked;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const body = { name: options.name, scopes };
|
|
61
|
+
if (options.expiresInDays) {
|
|
62
|
+
body.expiresInDays = Number(options.expiresInDays);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const spinner = villageSpinner('Creating API key...').start();
|
|
66
|
+
try {
|
|
67
|
+
const result = await createApiKey(body);
|
|
68
|
+
spinner.succeed('API key created.');
|
|
69
|
+
|
|
70
|
+
console.log(brand.teal('\n Key (shown ONCE — copy it now):\n'));
|
|
71
|
+
console.log(' ' + chalk.bold(result.key));
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(brand.teal(` Id: ${result.apiKey.id}`));
|
|
74
|
+
console.log(brand.teal(` Prefix: ${result.apiKey.prefix}`));
|
|
75
|
+
console.log(brand.teal(` Scopes: ${result.apiKey.scopes.join(', ')}`));
|
|
76
|
+
if (result.apiKey.expiresAt) {
|
|
77
|
+
console.log(brand.teal(` Expires: ${result.apiKey.expiresAt}`));
|
|
78
|
+
} else {
|
|
79
|
+
console.log(brand.teal(' Expires: never (revoke manually when no longer needed)'));
|
|
80
|
+
}
|
|
81
|
+
console.log(chalk.yellow('\n ⚠ Treat this key like a password. Anyone with it can act as you'));
|
|
82
|
+
console.log(chalk.yellow(' within the scopes you selected. Store it in an env var, not in git.\n'));
|
|
83
|
+
console.log(brand.teal(' Use in REST calls:'));
|
|
84
|
+
console.log(' ' + chalk.dim(`curl -H "Authorization: Bearer ${result.key.slice(0, 12)}..." https://portal.myvillageproject.ai/api/...\n`));
|
|
85
|
+
console.log(brand.teal(' Use in Claude Desktop / Cursor MCP config:'));
|
|
86
|
+
console.log(' ' + chalk.dim(`"headers": { "Authorization": "Bearer ${result.key.slice(0, 12)}..." }\n`));
|
|
87
|
+
} catch (err) {
|
|
88
|
+
const msg = err.response?.data?.error || err.message;
|
|
89
|
+
spinner.fail(`Failed to create API key: ${msg}`);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ── list ────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
export async function apiKeyListCommand() {
|
|
96
|
+
if (!isAuthenticated()) {
|
|
97
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const result = await listApiKeys();
|
|
103
|
+
const keys = result.apiKeys || [];
|
|
104
|
+
if (keys.length === 0) {
|
|
105
|
+
console.log(brand.teal(' No API keys yet. Create one with: myvillage api-key create --name "..."\n'));
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
console.log(brand.teal(`\n ${keys.length} API key(s):\n`));
|
|
109
|
+
for (const k of keys) {
|
|
110
|
+
const status = k.isActive ? brand.green('active') : chalk.dim('revoked');
|
|
111
|
+
console.log(` ${chalk.bold(k.name)} ${status}`);
|
|
112
|
+
console.log(` id: ${k.id}`);
|
|
113
|
+
console.log(` prefix: ${k.prefix}`);
|
|
114
|
+
console.log(` scopes: ${(k.scopes || []).join(', ')}`);
|
|
115
|
+
if (k.lastUsedAt) console.log(` used: ${k.lastUsedAt}`);
|
|
116
|
+
if (k.expiresAt) console.log(` expires: ${k.expiresAt}`);
|
|
117
|
+
console.log('');
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
const msg = err.response?.data?.error || err.message;
|
|
121
|
+
console.log(chalk.red(` ✗ Failed to list API keys: ${msg}\n`));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// ── revoke ──────────────────────────────────────────────
|
|
126
|
+
|
|
127
|
+
export async function apiKeyRevokeCommand(id) {
|
|
128
|
+
if (!isAuthenticated()) {
|
|
129
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
if (!id) {
|
|
133
|
+
console.log(chalk.red(' ✗ Usage: myvillage api-key revoke <id>\n'));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
await revokeApiKey(id);
|
|
139
|
+
console.log(brand.green(` ✓ API key ${id} revoked. Any further requests using it will fail.\n`));
|
|
140
|
+
} catch (err) {
|
|
141
|
+
const msg = err.response?.data?.error || err.message;
|
|
142
|
+
console.log(chalk.red(` ✗ Failed to revoke API key: ${msg}\n`));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── show-scopes ─────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
export async function apiKeyShowScopesCommand() {
|
|
149
|
+
if (!isAuthenticated()) {
|
|
150
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const result = await listAllowedScopes();
|
|
156
|
+
const allowed = result.allowedScopes || {};
|
|
157
|
+
const keys = Object.keys(allowed);
|
|
158
|
+
if (keys.length === 0) {
|
|
159
|
+
console.log(brand.teal(' No API scopes available for your account.\n'));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
console.log(brand.teal(`\n ${keys.length} scope(s) available to you:\n`));
|
|
163
|
+
for (const k of keys) {
|
|
164
|
+
console.log(` ${chalk.bold(k)}`);
|
|
165
|
+
console.log(` ${chalk.dim(allowed[k])}`);
|
|
166
|
+
}
|
|
167
|
+
console.log('');
|
|
168
|
+
} catch (err) {
|
|
169
|
+
const msg = err.response?.data?.error || err.message;
|
|
170
|
+
console.log(chalk.red(` ✗ Failed to load allowed scopes: ${msg}\n`));
|
|
171
|
+
}
|
|
172
|
+
}
|
package/src/index.js
CHANGED
|
@@ -9,6 +9,12 @@ import {
|
|
|
9
9
|
mediaDraftStatusCommand,
|
|
10
10
|
} from './commands/media.js';
|
|
11
11
|
import { logoutCommand } from './commands/logout.js';
|
|
12
|
+
import {
|
|
13
|
+
apiKeyCreateCommand,
|
|
14
|
+
apiKeyListCommand,
|
|
15
|
+
apiKeyRevokeCommand,
|
|
16
|
+
apiKeyShowScopesCommand,
|
|
17
|
+
} from './commands/api-key.js';
|
|
12
18
|
import { createGameCommand } from './commands/create-game.js';
|
|
13
19
|
import { createCommand } from './commands/create-app.js';
|
|
14
20
|
import { deployCommand } from './commands/deploy.js';
|
|
@@ -53,6 +59,8 @@ import {
|
|
|
53
59
|
agentRemoveToolCommand,
|
|
54
60
|
agentTaskListCommand,
|
|
55
61
|
agentTaskAssignCommand,
|
|
62
|
+
agentTaskRetryCommand,
|
|
63
|
+
agentTaskRetryFailedCommand,
|
|
56
64
|
agentMemoryCommand,
|
|
57
65
|
agentRecallCommand,
|
|
58
66
|
agentRememberCommand,
|
|
@@ -139,6 +147,35 @@ export function run() {
|
|
|
139
147
|
.description('Clear stored credentials')
|
|
140
148
|
.action(logoutCommand);
|
|
141
149
|
|
|
150
|
+
// ── API Keys (developer self-service) ──────────────────
|
|
151
|
+
|
|
152
|
+
const apiKeyCmd = program
|
|
153
|
+
.command('api-key')
|
|
154
|
+
.description('Manage your API keys (used for REST and MCP authentication)');
|
|
155
|
+
|
|
156
|
+
apiKeyCmd
|
|
157
|
+
.command('create')
|
|
158
|
+
.description('Create a new API key. Shown ONCE — copy it immediately.')
|
|
159
|
+
.requiredOption('--name <name>', 'Friendly name for this key (e.g. "claude-desktop")')
|
|
160
|
+
.option('--scopes <list>', 'Comma-separated scopes (omit for interactive picker)')
|
|
161
|
+
.option('--expires-in-days <n>', 'Number of days until expiry (default: never)')
|
|
162
|
+
.action(apiKeyCreateCommand);
|
|
163
|
+
|
|
164
|
+
apiKeyCmd
|
|
165
|
+
.command('list')
|
|
166
|
+
.description('List your API keys (names, prefixes, scopes — never the secret)')
|
|
167
|
+
.action(apiKeyListCommand);
|
|
168
|
+
|
|
169
|
+
apiKeyCmd
|
|
170
|
+
.command('revoke <id>')
|
|
171
|
+
.description('Revoke an API key by id so further requests using it are rejected')
|
|
172
|
+
.action(apiKeyRevokeCommand);
|
|
173
|
+
|
|
174
|
+
apiKeyCmd
|
|
175
|
+
.command('show-scopes')
|
|
176
|
+
.description('Print the API scopes your account is allowed to mint into a key')
|
|
177
|
+
.action(apiKeyShowScopesCommand);
|
|
178
|
+
|
|
142
179
|
program
|
|
143
180
|
.command('create-game')
|
|
144
181
|
.description('Create a new game project with interactive wizard')
|
|
@@ -455,6 +492,17 @@ export function run() {
|
|
|
455
492
|
.option('--priority <n>', 'Priority 1-10 (lower runs first)', '5')
|
|
456
493
|
.action(agentTaskAssignCommand);
|
|
457
494
|
|
|
495
|
+
agentCmd
|
|
496
|
+
.command('task-retry <name> <taskId>')
|
|
497
|
+
.description('Reset a FAILED or CANCELLED task back to PENDING so the agent retries it')
|
|
498
|
+
.action(agentTaskRetryCommand);
|
|
499
|
+
|
|
500
|
+
agentCmd
|
|
501
|
+
.command('task-retry-failed <name>')
|
|
502
|
+
.description('Bulk-reset every FAILED task for an agent back to PENDING')
|
|
503
|
+
.option('--filter <text>', 'Only retry tasks whose errorMessage contains this substring')
|
|
504
|
+
.action(agentTaskRetryFailedCommand);
|
|
505
|
+
|
|
458
506
|
// Agent memory (short-term KV state) and recall (long-term searchable memory)
|
|
459
507
|
agentCmd
|
|
460
508
|
.command('memory <name> <action> [args...]')
|
|
@@ -138,6 +138,14 @@ ${description}
|
|
|
138
138
|
- Speaks casually but clearly
|
|
139
139
|
- Concise in responses
|
|
140
140
|
|
|
141
|
+
## Handling tasks
|
|
142
|
+
When you receive a TASK in your context, follow these rules:
|
|
143
|
+
|
|
144
|
+
- **Use the values from the task input verbatim.** If the task input is JSON like \`{"communitySlug":"general"}\`, call the tool with \`communitySlug: "general"\` exactly. Do not substitute, translate, or invent slugs, IDs, or names.
|
|
145
|
+
- **If a required value is missing, do NOT guess.** Reply in your final text that the task is missing required information (e.g. "Task is missing a communitySlug — cannot proceed"). Don't pick a community at random.
|
|
146
|
+
- **If a tool call fails, do NOT claim success.** Report what failed and why in your final text. The platform decides whether the task is FAILED based on whether the tools actually succeeded — making up a success message hides the real error.
|
|
147
|
+
- **Use the platform's communities you already belong to.** If you don't know a community exists, use \`community_view\` to check before posting.
|
|
148
|
+
|
|
141
149
|
## Boundaries
|
|
142
150
|
- Never share personal files or private data to the feed
|
|
143
151
|
- Ask before posting anything longer than 2 sentences
|
package/src/utils/api.js
CHANGED
|
@@ -679,6 +679,54 @@ export async function completeAgentTask(villageAgentId, taskId, data = {}) {
|
|
|
679
679
|
return response.data;
|
|
680
680
|
}
|
|
681
681
|
|
|
682
|
+
// Retry a single FAILED or CANCELLED task: resets it to PENDING so the
|
|
683
|
+
// agent daemon re-claims it on the next polling iteration.
|
|
684
|
+
export async function retryAgentTask(villageAgentId, taskId) {
|
|
685
|
+
const client = getPlatformClient();
|
|
686
|
+
const response = await client.post(
|
|
687
|
+
`/village-agents/${encodeURIComponent(villageAgentId)}/tasks/${encodeURIComponent(taskId)}/retry`,
|
|
688
|
+
);
|
|
689
|
+
return response.data;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// Bulk-retry every FAILED task for an agent. Optional `errorPattern`
|
|
693
|
+
// filters by errorMessage substring (case-insensitive).
|
|
694
|
+
export async function retryFailedAgentTasks(villageAgentId, errorPattern) {
|
|
695
|
+
const client = getPlatformClient();
|
|
696
|
+
const body = errorPattern ? { errorPattern } : {};
|
|
697
|
+
const response = await client.post(
|
|
698
|
+
`/village-agents/${encodeURIComponent(villageAgentId)}/tasks/retry-failed`,
|
|
699
|
+
body,
|
|
700
|
+
);
|
|
701
|
+
return response.data;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// ── API Key management ─────────────────────────────────
|
|
705
|
+
|
|
706
|
+
export async function createApiKey(data) {
|
|
707
|
+
const client = getPlatformClient();
|
|
708
|
+
const response = await client.post('/api-keys', data);
|
|
709
|
+
return response.data;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
export async function listApiKeys() {
|
|
713
|
+
const client = getPlatformClient();
|
|
714
|
+
const response = await client.get('/api-keys');
|
|
715
|
+
return response.data;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
export async function revokeApiKey(id) {
|
|
719
|
+
const client = getPlatformClient();
|
|
720
|
+
const response = await client.delete(`/api-keys/${encodeURIComponent(id)}`);
|
|
721
|
+
return response.data;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
export async function listAllowedScopes() {
|
|
725
|
+
const client = getPlatformClient();
|
|
726
|
+
const response = await client.get('/api-keys/allowed-scopes');
|
|
727
|
+
return response.data;
|
|
728
|
+
}
|
|
729
|
+
|
|
682
730
|
// ── Wisdom (VillageBooks repurposed as agent skill packs) ──────────
|
|
683
731
|
|
|
684
732
|
export async function listVillageBooks(params = {}) {
|