@myvillage/cli 1.10.2 → 1.17.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 +81 -3
- package/src/commands/agent-client.js +435 -0
- package/src/commands/agent-grant.js +131 -0
- package/src/commands/agent-local.js +354 -1
- package/src/commands/create-app.js +61 -1
- package/src/commands/media.js +185 -187
- package/src/commands/wisdom.js +185 -0
- package/src/index.js +199 -0
- package/src/utils/agentic-templates.js +10 -2
- package/src/utils/api.js +157 -0
- package/src/utils/formatters.js +72 -0
- package/src/utils/wisdom.js +102 -0
package/package.json
CHANGED
|
@@ -12,7 +12,8 @@ import { getMCPTools, cleanupMCPClients } from './mcp-client.js';
|
|
|
12
12
|
import { gatherContext } from './context.js';
|
|
13
13
|
import { isWithinActiveHours, getNextCheckInMs } from './scheduler.js';
|
|
14
14
|
import { parse as parseYaml } from 'yaml';
|
|
15
|
-
import { postAgentHeartbeat } from '../utils/api.js';
|
|
15
|
+
import { postAgentHeartbeat, listAgentTasks, claimAgentTask, completeAgentTask } from '../utils/api.js';
|
|
16
|
+
import { readAgentWisdom } from '../utils/wisdom.js';
|
|
16
17
|
|
|
17
18
|
export async function agentLoop(agentName, { signal }) {
|
|
18
19
|
const agentDir = join(homedir(), '.myvillage', 'agents', agentName);
|
|
@@ -107,19 +108,47 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
107
108
|
};
|
|
108
109
|
let feedItemsRead = 0;
|
|
109
110
|
let mentionsFound = 0;
|
|
111
|
+
// Hoisted so the catch block can mark in-flight tasks FAILED.
|
|
112
|
+
let activeTask = null;
|
|
110
113
|
|
|
111
114
|
try {
|
|
112
115
|
// Read prompt.md fresh each iteration (villager may have edited it)
|
|
113
116
|
const promptPath = join(agentDir, 'prompt.md');
|
|
114
|
-
const
|
|
117
|
+
const basePrompt = existsSync(promptPath)
|
|
115
118
|
? readFileSync(promptPath, 'utf-8')
|
|
116
119
|
: `You are an agent named ${config.display_name || agentName}. Be helpful and concise.`;
|
|
117
120
|
|
|
121
|
+
// Append wisdom skills to the system prompt. We inline the full bodies
|
|
122
|
+
// for v1 — agent skill packs are small and this keeps the loop simple.
|
|
123
|
+
// (If they grow large, switch to lazy-load via a `wisdom_load` tool.)
|
|
124
|
+
const wisdom = readAgentWisdom(agentName);
|
|
125
|
+
let systemPrompt = basePrompt;
|
|
126
|
+
if (wisdom.length > 0) {
|
|
127
|
+
const skills = wisdom.map(w => {
|
|
128
|
+
const header = `### Skill: ${w.name}${w.description ? ` — ${w.description}` : ''}${w.trigger ? `\nWhen: ${w.trigger}` : ''}`;
|
|
129
|
+
return `${header}\n\n${w.body.trim()}`;
|
|
130
|
+
}).join('\n\n---\n\n');
|
|
131
|
+
systemPrompt = `${basePrompt}\n\n## Available Skills\n\nThese are skill packs you can apply when the trigger matches the current situation.\n\n${skills}`;
|
|
132
|
+
logActivity(agentDir, { type: 'wisdom_loaded', count: wisdom.length, names: wisdom.map(w => w.name) });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Try to pull an assigned task first. Tasks take priority over ambient
|
|
136
|
+
// feed-monitoring. If nothing is queued, fall through to the default.
|
|
137
|
+
if (config.man?.village_agent_id) {
|
|
138
|
+
activeTask = await pollAndClaim(config.man.village_agent_id, agentDir);
|
|
139
|
+
}
|
|
140
|
+
|
|
118
141
|
// Gather context (returns { text, mentionsCount })
|
|
119
142
|
const contextResult = await gatherContext(config, lastCheckIn, recentActions);
|
|
120
|
-
|
|
143
|
+
let context = contextResult.text;
|
|
121
144
|
mentionsFound = contextResult.mentionsCount;
|
|
122
145
|
|
|
146
|
+
if (activeTask) {
|
|
147
|
+
const taskLine = `TASK ${activeTask.id} (${activeTask.taskType}): ${activeTask.instruction || JSON.stringify(activeTask.input || {})}`;
|
|
148
|
+
context = `${taskLine}\n\n${context}`;
|
|
149
|
+
logActivity(agentDir, { type: 'task_claimed', taskId: activeTask.id, taskType: activeTask.taskType });
|
|
150
|
+
}
|
|
151
|
+
|
|
123
152
|
// Count feed items from context
|
|
124
153
|
feedItemsRead = (context.match(/^- @/gm) || []).length;
|
|
125
154
|
|
|
@@ -220,6 +249,20 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
220
249
|
// Keep only last 50 actions to bound memory
|
|
221
250
|
if (recentActions.length > 50) recentActions.splice(0, recentActions.length - 50);
|
|
222
251
|
|
|
252
|
+
// If a task was being processed, mark it complete with the model's output.
|
|
253
|
+
if (activeTask && config.man?.village_agent_id) {
|
|
254
|
+
try {
|
|
255
|
+
await completeAgentTask(config.man.village_agent_id, activeTask.id, {
|
|
256
|
+
output: { text: result.text || '', toolCalls: activity.toolCalls },
|
|
257
|
+
tokensUsed: (result.usage?.promptTokens || 0) + (result.usage?.completionTokens || 0),
|
|
258
|
+
durationMs: Date.now() - loopStart,
|
|
259
|
+
});
|
|
260
|
+
logActivity(agentDir, { type: 'task_completed', taskId: activeTask.id });
|
|
261
|
+
} catch (taskErr) {
|
|
262
|
+
logActivity(agentDir, { type: 'error', error: `Failed to mark task complete: ${taskErr.message}` });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
223
266
|
// Send server-side heartbeat
|
|
224
267
|
if (config.man?.agent_id) {
|
|
225
268
|
try {
|
|
@@ -242,6 +285,18 @@ export async function agentLoop(agentName, { signal }) {
|
|
|
242
285
|
type: 'error',
|
|
243
286
|
error: err.message,
|
|
244
287
|
});
|
|
288
|
+
// If a task was in flight when we crashed, mark it FAILED so it isn't lost
|
|
289
|
+
if (activeTask && config.man?.village_agent_id) {
|
|
290
|
+
try {
|
|
291
|
+
await completeAgentTask(config.man.village_agent_id, activeTask.id, {
|
|
292
|
+
errorMessage: err.message,
|
|
293
|
+
durationMs: Date.now() - loopStart,
|
|
294
|
+
});
|
|
295
|
+
logActivity(agentDir, { type: 'task_failed', taskId: activeTask.id });
|
|
296
|
+
} catch {
|
|
297
|
+
// best-effort
|
|
298
|
+
}
|
|
299
|
+
}
|
|
245
300
|
}
|
|
246
301
|
|
|
247
302
|
lastCheckIn = new Date().toISOString();
|
|
@@ -288,6 +343,29 @@ function updateHeartbeat(agentDir) {
|
|
|
288
343
|
}
|
|
289
344
|
}
|
|
290
345
|
|
|
346
|
+
// Pull up to 5 pending tasks and claim the first one we can win the race for.
|
|
347
|
+
// Returns the claimed task or null. Errors are swallowed and logged — the loop
|
|
348
|
+
// should keep running on transient backend issues.
|
|
349
|
+
async function pollAndClaim(villageAgentId, agentDir) {
|
|
350
|
+
try {
|
|
351
|
+
const result = await listAgentTasks(villageAgentId, { status: 'PENDING', limit: 5 });
|
|
352
|
+
const pending = result.tasks || [];
|
|
353
|
+
if (pending.length === 0) return null;
|
|
354
|
+
for (const task of pending) {
|
|
355
|
+
try {
|
|
356
|
+
const claim = await claimAgentTask(villageAgentId, task.id);
|
|
357
|
+
return claim.data || task;
|
|
358
|
+
} catch {
|
|
359
|
+
// Race lost (409) or transient — try the next task
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return null;
|
|
363
|
+
} catch (err) {
|
|
364
|
+
logActivity(agentDir, { type: 'error', error: `Task poll failed: ${err.message}` });
|
|
365
|
+
return null;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
291
369
|
function sleep(ms, signal) {
|
|
292
370
|
return new Promise((resolve) => {
|
|
293
371
|
if (signal?.aborted) { resolve(); return; }
|
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync, appendFileSync } from 'fs';
|
|
4
|
+
import { villageSpinner, brand } from '../utils/brand.js';
|
|
5
|
+
import { isAuthenticated } from '../utils/auth.js';
|
|
6
|
+
import {
|
|
7
|
+
listMyAgents,
|
|
8
|
+
registerClientAgent as apiRegisterClientAgent,
|
|
9
|
+
listClientAgentConfigs as apiListClientAgentConfigs,
|
|
10
|
+
getClientAgentConfig as apiGetClientAgentConfig,
|
|
11
|
+
updateClientAgentConfig as apiUpdateClientAgentConfig,
|
|
12
|
+
deactivateClientAgent as apiDeactivateClientAgent,
|
|
13
|
+
rotateClientAgentKey as apiRotateClientAgentKey,
|
|
14
|
+
} from '../utils/api.js';
|
|
15
|
+
import { formatClientAgentConfig, formatClientAgentList } from '../utils/formatters.js';
|
|
16
|
+
|
|
17
|
+
// ── Schedule Presets ────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const SCHEDULE_PRESETS = [
|
|
20
|
+
{ name: 'Daily at 9am', value: '0 9 * * *' },
|
|
21
|
+
{ name: 'Weekly Monday 9am', value: '0 9 * * 1' },
|
|
22
|
+
{ name: 'Monthly 1st at 9am', value: '0 9 1 * *' },
|
|
23
|
+
{ name: 'Custom cron expression', value: 'CUSTOM' },
|
|
24
|
+
{ name: 'None (manual only)', value: '' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
const WORKFLOW_TYPES = [
|
|
28
|
+
{ name: 'Submission Processor — classify submissions, draft outreach', value: 'SUBMISSION_PROCESSOR' },
|
|
29
|
+
{ name: 'Digest Generator — weekly/daily activity summaries', value: 'DIGEST_GENERATOR' },
|
|
30
|
+
{ name: 'Member Matcher — match new signups with existing members', value: 'MEMBER_MATCHER' },
|
|
31
|
+
{ name: 'Stale Data Detector — flag inactive members', value: 'STALE_DATA_DETECTOR' },
|
|
32
|
+
{ name: 'Custom', value: 'CUSTOM' },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// ── Helpers ─────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
function displayApiKey(apiKey, clientId) {
|
|
38
|
+
console.log('');
|
|
39
|
+
console.log(chalk.yellow.bold(' ⚠ IMPORTANT: Save your API key now — it will not be shown again!'));
|
|
40
|
+
console.log('');
|
|
41
|
+
console.log(` ${chalk.dim('Client ID:')} ${brand.teal(clientId)}`);
|
|
42
|
+
console.log(` ${chalk.dim('API Key:')} ${brand.gold(apiKey)}`);
|
|
43
|
+
console.log('');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function promptSaveToEnvFile(apiKey, clientId) {
|
|
47
|
+
const { saveToFile } = await inquirer.prompt([{
|
|
48
|
+
type: 'confirm',
|
|
49
|
+
name: 'saveToFile',
|
|
50
|
+
message: 'Save API key to a .env file?',
|
|
51
|
+
default: false,
|
|
52
|
+
}]);
|
|
53
|
+
|
|
54
|
+
if (saveToFile) {
|
|
55
|
+
const { filePath } = await inquirer.prompt([{
|
|
56
|
+
type: 'input',
|
|
57
|
+
name: 'filePath',
|
|
58
|
+
message: 'File path:',
|
|
59
|
+
default: '.env.local',
|
|
60
|
+
}]);
|
|
61
|
+
|
|
62
|
+
const lines = `\n# MyVillageOS Client Agent\nMYVILLAGE_AGENT_API_KEY=${apiKey}\nMYVILLAGE_AGENT_CLIENT_ID=${clientId}\n`;
|
|
63
|
+
|
|
64
|
+
if (existsSync(filePath)) {
|
|
65
|
+
appendFileSync(filePath, lines);
|
|
66
|
+
} else {
|
|
67
|
+
writeFileSync(filePath, lines.trimStart());
|
|
68
|
+
}
|
|
69
|
+
console.log(brand.green(` ✓ API key saved to ${filePath}\n`));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function resolveAgentId(handle) {
|
|
74
|
+
const result = await listMyAgents();
|
|
75
|
+
const agents = result.data || result;
|
|
76
|
+
if (!Array.isArray(agents) || agents.length === 0) return null;
|
|
77
|
+
|
|
78
|
+
const match = agents.find(a =>
|
|
79
|
+
a.handle === handle || a.handle === handle.replace(/^@/, '')
|
|
80
|
+
);
|
|
81
|
+
return match || null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Register Client ─────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
export async function agentRegisterClientCommand(options) {
|
|
87
|
+
if (!isAuthenticated()) {
|
|
88
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const isNonInteractive = options.agent && options.clientId && options.name && options.url;
|
|
94
|
+
|
|
95
|
+
let data;
|
|
96
|
+
|
|
97
|
+
if (isNonInteractive) {
|
|
98
|
+
// Resolve agent handle to ID
|
|
99
|
+
const spinner = villageSpinner('Resolving agent...').start();
|
|
100
|
+
const agent = await resolveAgentId(options.agent);
|
|
101
|
+
if (!agent) {
|
|
102
|
+
spinner.fail(`Agent "${options.agent}" not found.`);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
spinner.succeed(`Found agent @${agent.handle}`);
|
|
106
|
+
|
|
107
|
+
data = {
|
|
108
|
+
villageAgentId: agent.villageAgent?.id || agent.villageAgentId || agent.id,
|
|
109
|
+
clientId: options.clientId.toLowerCase(),
|
|
110
|
+
clientName: options.name,
|
|
111
|
+
baseUrl: options.url,
|
|
112
|
+
workflowType: (options.workflow || 'SUBMISSION_PROCESSOR').toUpperCase(),
|
|
113
|
+
schedule: options.schedule || null,
|
|
114
|
+
timezone: options.timezone || 'America/Chicago',
|
|
115
|
+
};
|
|
116
|
+
} else {
|
|
117
|
+
// Interactive mode
|
|
118
|
+
const agentSpinner = villageSpinner('Loading your agents...').start();
|
|
119
|
+
const agentResult = await listMyAgents();
|
|
120
|
+
const agents = agentResult.data || agentResult;
|
|
121
|
+
agentSpinner.stop();
|
|
122
|
+
|
|
123
|
+
if (!Array.isArray(agents) || agents.length === 0) {
|
|
124
|
+
console.log(brand.teal('\n No agents found. Create one first with: myvillage agent create\n'));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const answers = await inquirer.prompt([
|
|
129
|
+
{
|
|
130
|
+
type: 'list',
|
|
131
|
+
name: 'selectedAgent',
|
|
132
|
+
message: 'Which agent should manage this client?',
|
|
133
|
+
choices: agents.map(a => ({
|
|
134
|
+
name: `@${a.handle} — ${a.displayName || a.handle}`,
|
|
135
|
+
value: a,
|
|
136
|
+
})),
|
|
137
|
+
},
|
|
138
|
+
{
|
|
139
|
+
type: 'input',
|
|
140
|
+
name: 'clientId',
|
|
141
|
+
message: 'Client identifier (lowercase, no spaces):',
|
|
142
|
+
validate: input => /^[a-z0-9_-]{3,40}$/.test(input) || 'Must be 3-40 chars: lowercase letters, numbers, hyphens, underscores.',
|
|
143
|
+
filter: input => input.toLowerCase().replace(/[^a-z0-9_-]/g, ''),
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
type: 'input',
|
|
147
|
+
name: 'clientName',
|
|
148
|
+
message: 'Client display name:',
|
|
149
|
+
validate: input => input.trim().length > 0 || 'Name is required.',
|
|
150
|
+
},
|
|
151
|
+
{
|
|
152
|
+
type: 'input',
|
|
153
|
+
name: 'baseUrl',
|
|
154
|
+
message: 'Client base URL:',
|
|
155
|
+
validate: input => {
|
|
156
|
+
try {
|
|
157
|
+
new URL(input);
|
|
158
|
+
return true;
|
|
159
|
+
} catch {
|
|
160
|
+
return 'Must be a valid URL (e.g., https://maywood-chatkit.vercel.app)';
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
type: 'list',
|
|
166
|
+
name: 'workflowType',
|
|
167
|
+
message: 'Workflow type:',
|
|
168
|
+
choices: WORKFLOW_TYPES,
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
type: 'list',
|
|
172
|
+
name: 'schedulePreset',
|
|
173
|
+
message: 'Schedule:',
|
|
174
|
+
choices: SCHEDULE_PRESETS,
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
177
|
+
|
|
178
|
+
let schedule = answers.schedulePreset;
|
|
179
|
+
if (schedule === 'CUSTOM') {
|
|
180
|
+
const { customCron } = await inquirer.prompt([{
|
|
181
|
+
type: 'input',
|
|
182
|
+
name: 'customCron',
|
|
183
|
+
message: 'Cron expression (e.g., 0 9 * * 1):',
|
|
184
|
+
validate: input => input.trim().split(/\s+/).length === 5 || 'Must be a 5-field cron expression.',
|
|
185
|
+
}]);
|
|
186
|
+
schedule = customCron;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const { confirm } = await inquirer.prompt([{
|
|
190
|
+
type: 'confirm',
|
|
191
|
+
name: 'confirm',
|
|
192
|
+
message: `Register "${answers.clientName}" (${answers.clientId}) with @${answers.selectedAgent.handle}?`,
|
|
193
|
+
default: true,
|
|
194
|
+
}]);
|
|
195
|
+
|
|
196
|
+
if (!confirm) {
|
|
197
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const selectedAgent = answers.selectedAgent;
|
|
202
|
+
data = {
|
|
203
|
+
villageAgentId: selectedAgent.villageAgent?.id || selectedAgent.villageAgentId || selectedAgent.id,
|
|
204
|
+
clientId: answers.clientId,
|
|
205
|
+
clientName: answers.clientName,
|
|
206
|
+
baseUrl: answers.baseUrl,
|
|
207
|
+
workflowType: answers.workflowType,
|
|
208
|
+
schedule: schedule || null,
|
|
209
|
+
timezone: options.timezone || 'America/Chicago',
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const regSpinner = villageSpinner('Registering client agent...').start();
|
|
214
|
+
const result = await apiRegisterClientAgent(data);
|
|
215
|
+
regSpinner.succeed('Client agent registered!');
|
|
216
|
+
|
|
217
|
+
displayApiKey(result.apiKey, data.clientId);
|
|
218
|
+
|
|
219
|
+
if (!isNonInteractive) {
|
|
220
|
+
await promptSaveToEnvFile(result.apiKey, data.clientId);
|
|
221
|
+
} else if (options.envFile) {
|
|
222
|
+
const lines = `\n# MyVillageOS Client Agent\nMYVILLAGE_AGENT_API_KEY=${result.apiKey}\nMYVILLAGE_AGENT_CLIENT_ID=${data.clientId}\n`;
|
|
223
|
+
if (existsSync(options.envFile)) {
|
|
224
|
+
appendFileSync(options.envFile, lines);
|
|
225
|
+
} else {
|
|
226
|
+
writeFileSync(options.envFile, lines.trimStart());
|
|
227
|
+
}
|
|
228
|
+
console.log(brand.green(` ✓ API key saved to ${options.envFile}\n`));
|
|
229
|
+
}
|
|
230
|
+
} catch (err) {
|
|
231
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
232
|
+
console.log(chalk.red(`\n ✗ Failed to register client agent: ${message}\n`));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ── List Clients ────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
export async function agentListClientsCommand() {
|
|
239
|
+
if (!isAuthenticated()) {
|
|
240
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
try {
|
|
245
|
+
const spinner = villageSpinner('Loading client agent configs...').start();
|
|
246
|
+
const result = await apiListClientAgentConfigs();
|
|
247
|
+
spinner.stop();
|
|
248
|
+
|
|
249
|
+
const configs = result.data || result;
|
|
250
|
+
formatClientAgentList(Array.isArray(configs) ? configs : []);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
253
|
+
console.log(chalk.red(`\n ✗ Failed to list client agents: ${message}\n`));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── View Client ─────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
export async function agentViewClientCommand(configId) {
|
|
260
|
+
if (!isAuthenticated()) {
|
|
261
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
try {
|
|
266
|
+
const spinner = villageSpinner('Loading client agent config...').start();
|
|
267
|
+
const result = await apiGetClientAgentConfig(configId);
|
|
268
|
+
spinner.stop();
|
|
269
|
+
|
|
270
|
+
const config = result.data || result;
|
|
271
|
+
formatClientAgentConfig(config);
|
|
272
|
+
} catch (err) {
|
|
273
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
274
|
+
console.log(chalk.red(`\n ✗ Failed to load client agent config: ${message}\n`));
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Edit Client ─────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
export async function agentEditClientCommand(configId) {
|
|
281
|
+
if (!isAuthenticated()) {
|
|
282
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
try {
|
|
287
|
+
const spinner = villageSpinner('Loading client agent config...').start();
|
|
288
|
+
const result = await apiGetClientAgentConfig(configId);
|
|
289
|
+
spinner.stop();
|
|
290
|
+
|
|
291
|
+
const config = result.data || result;
|
|
292
|
+
|
|
293
|
+
console.log(chalk.dim(`\n Editing: ${config.clientName} (${config.clientId})\n`));
|
|
294
|
+
|
|
295
|
+
// Match current schedule to a preset
|
|
296
|
+
const currentPresetMatch = SCHEDULE_PRESETS.find(p => p.value === config.schedule);
|
|
297
|
+
const currentPreset = currentPresetMatch ? currentPresetMatch.value : (config.schedule ? 'CUSTOM' : '');
|
|
298
|
+
|
|
299
|
+
const answers = await inquirer.prompt([
|
|
300
|
+
{
|
|
301
|
+
type: 'input',
|
|
302
|
+
name: 'clientName',
|
|
303
|
+
message: 'Client name:',
|
|
304
|
+
default: config.clientName,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
type: 'input',
|
|
308
|
+
name: 'baseUrl',
|
|
309
|
+
message: 'Base URL:',
|
|
310
|
+
default: config.baseUrl,
|
|
311
|
+
validate: input => {
|
|
312
|
+
try { new URL(input); return true; } catch { return 'Must be a valid URL.'; }
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
{
|
|
316
|
+
type: 'list',
|
|
317
|
+
name: 'workflowType',
|
|
318
|
+
message: 'Workflow type:',
|
|
319
|
+
choices: WORKFLOW_TYPES,
|
|
320
|
+
default: config.workflowType,
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
type: 'list',
|
|
324
|
+
name: 'schedulePreset',
|
|
325
|
+
message: 'Schedule:',
|
|
326
|
+
choices: SCHEDULE_PRESETS,
|
|
327
|
+
default: currentPreset,
|
|
328
|
+
},
|
|
329
|
+
{
|
|
330
|
+
type: 'input',
|
|
331
|
+
name: 'timezone',
|
|
332
|
+
message: 'Timezone:',
|
|
333
|
+
default: config.timezone || 'America/Chicago',
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
type: 'confirm',
|
|
337
|
+
name: 'isActive',
|
|
338
|
+
message: 'Active?',
|
|
339
|
+
default: config.isActive,
|
|
340
|
+
},
|
|
341
|
+
]);
|
|
342
|
+
|
|
343
|
+
let schedule = answers.schedulePreset;
|
|
344
|
+
if (schedule === 'CUSTOM') {
|
|
345
|
+
const { customCron } = await inquirer.prompt([{
|
|
346
|
+
type: 'input',
|
|
347
|
+
name: 'customCron',
|
|
348
|
+
message: 'Cron expression:',
|
|
349
|
+
default: config.schedule || '',
|
|
350
|
+
validate: input => input.trim().split(/\s+/).length === 5 || 'Must be a 5-field cron expression.',
|
|
351
|
+
}]);
|
|
352
|
+
schedule = customCron;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const updateData = {
|
|
356
|
+
clientName: answers.clientName,
|
|
357
|
+
baseUrl: answers.baseUrl,
|
|
358
|
+
workflowType: answers.workflowType,
|
|
359
|
+
schedule: schedule || null,
|
|
360
|
+
timezone: answers.timezone,
|
|
361
|
+
isActive: answers.isActive,
|
|
362
|
+
};
|
|
363
|
+
|
|
364
|
+
const saveSpinner = villageSpinner('Saving changes...').start();
|
|
365
|
+
await apiUpdateClientAgentConfig(configId, updateData);
|
|
366
|
+
saveSpinner.succeed('Client agent config updated!');
|
|
367
|
+
console.log('');
|
|
368
|
+
} catch (err) {
|
|
369
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
370
|
+
console.log(chalk.red(`\n ✗ Failed to update client agent config: ${message}\n`));
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
// ── Deactivate Client ───────────────────────────────────
|
|
375
|
+
|
|
376
|
+
export async function agentDeactivateClientCommand(configId) {
|
|
377
|
+
if (!isAuthenticated()) {
|
|
378
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
try {
|
|
383
|
+
const { confirm } = await inquirer.prompt([{
|
|
384
|
+
type: 'confirm',
|
|
385
|
+
name: 'confirm',
|
|
386
|
+
message: `Deactivate client agent config ${configId}? This will stop all automated workflows.`,
|
|
387
|
+
default: false,
|
|
388
|
+
}]);
|
|
389
|
+
|
|
390
|
+
if (!confirm) {
|
|
391
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const spinner = villageSpinner('Deactivating...').start();
|
|
396
|
+
await apiDeactivateClientAgent(configId);
|
|
397
|
+
spinner.succeed('Client agent deactivated.');
|
|
398
|
+
console.log('');
|
|
399
|
+
} catch (err) {
|
|
400
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
401
|
+
console.log(chalk.red(`\n ✗ Failed to deactivate: ${message}\n`));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// ── Rotate Client Key ───────────────────────────────────
|
|
406
|
+
|
|
407
|
+
export async function agentRotateClientKeyCommand(configId) {
|
|
408
|
+
if (!isAuthenticated()) {
|
|
409
|
+
console.log(chalk.red(' ✗ Authentication required. Run \'myvillage login\' first.\n'));
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
try {
|
|
414
|
+
const { confirm } = await inquirer.prompt([{
|
|
415
|
+
type: 'confirm',
|
|
416
|
+
name: 'confirm',
|
|
417
|
+
message: `Rotate API key for ${configId}? The current key will be invalidated immediately.`,
|
|
418
|
+
default: false,
|
|
419
|
+
}]);
|
|
420
|
+
|
|
421
|
+
if (!confirm) {
|
|
422
|
+
console.log(chalk.dim('\n Cancelled.\n'));
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
const spinner = villageSpinner('Rotating API key...').start();
|
|
427
|
+
const result = await apiRotateClientAgentKey(configId);
|
|
428
|
+
spinner.succeed('API key rotated!');
|
|
429
|
+
|
|
430
|
+
displayApiKey(result.apiKey, configId);
|
|
431
|
+
} catch (err) {
|
|
432
|
+
const message = err.response?.data?.error || err.response?.data?.message || err.message;
|
|
433
|
+
console.log(chalk.red(`\n ✗ Failed to rotate API key: ${message}\n`));
|
|
434
|
+
}
|
|
435
|
+
}
|