@myvillage/cli 1.10.2 → 1.18.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 +215 -6
- package/src/commands/agent-client.js +435 -0
- package/src/commands/agent-grant.js +131 -0
- package/src/commands/agent-local.js +395 -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 +212 -0
- package/src/utils/agent-scaffolder.js +8 -0
- package/src/utils/agentic-templates.js +10 -2
- package/src/utils/api.js +179 -0
- package/src/utils/formatters.js +72 -0
- package/src/utils/wisdom.js +102 -0
package/src/utils/formatters.js
CHANGED
|
@@ -396,6 +396,78 @@ export function formatAgentList(agents) {
|
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
// ── Client Agent Formatting ────────────────────────────
|
|
400
|
+
|
|
401
|
+
function workflowTypeBadge(type) {
|
|
402
|
+
switch (type) {
|
|
403
|
+
case 'SUBMISSION_PROCESSOR': return chalk.blue('[SUBMISSIONS]');
|
|
404
|
+
case 'DIGEST_GENERATOR': return chalk.green('[DIGEST]');
|
|
405
|
+
case 'MEMBER_MATCHER': return chalk.magenta('[MATCHER]');
|
|
406
|
+
case 'STALE_DATA_DETECTOR': return chalk.yellow('[STALE_DATA]');
|
|
407
|
+
case 'CUSTOM': return chalk.cyan('[CUSTOM]');
|
|
408
|
+
default: return chalk.dim(`[${type}]`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export function formatClientAgentConfig(config) {
|
|
413
|
+
const lines = [];
|
|
414
|
+
|
|
415
|
+
lines.push('');
|
|
416
|
+
const status = config.isActive !== false ? chalk.green('active') : chalk.red('inactive');
|
|
417
|
+
const badge = workflowTypeBadge(config.workflowType);
|
|
418
|
+
lines.push(` ${chalk.bold(config.clientName)} ${chalk.dim('·')} ${badge} ${chalk.dim('·')} ${status}`);
|
|
419
|
+
lines.push(` ${chalk.dim(config.baseUrl)}`);
|
|
420
|
+
lines.push(` ${'─'.repeat(50)}`);
|
|
421
|
+
|
|
422
|
+
lines.push(` ${chalk.dim('Client ID:')} ${config.clientId}`);
|
|
423
|
+
const agentName = config.villageAgent?.name || config.villageAgentId;
|
|
424
|
+
lines.push(` ${chalk.dim('Agent:')} ${agentName}`);
|
|
425
|
+
lines.push(` ${chalk.dim('Schedule:')} ${config.schedule || 'None (manual)'}`);
|
|
426
|
+
lines.push(` ${chalk.dim('Timezone:')} ${config.timezone || 'America/Chicago'}`);
|
|
427
|
+
|
|
428
|
+
if (config.lastRunAt) {
|
|
429
|
+
const lastRunStatus = config.lastRunStatus === 'success'
|
|
430
|
+
? chalk.green(config.lastRunStatus)
|
|
431
|
+
: config.lastRunStatus === 'failed'
|
|
432
|
+
? chalk.red(config.lastRunStatus)
|
|
433
|
+
: chalk.yellow(config.lastRunStatus || 'unknown');
|
|
434
|
+
lines.push(` ${chalk.dim('Last Run:')} ${relativeTime(config.lastRunAt)} (${lastRunStatus})`);
|
|
435
|
+
} else {
|
|
436
|
+
lines.push(` ${chalk.dim('Last Run:')} ${chalk.dim('Never')}`);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
if (config.workflowConfig) {
|
|
440
|
+
lines.push(` ${chalk.dim('Config:')} ${chalk.dim(JSON.stringify(config.workflowConfig))}`);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
lines.push(` ${chalk.dim('Config ID:')} ${chalk.dim(config.id)}`);
|
|
444
|
+
lines.push('');
|
|
445
|
+
console.log(lines.join('\n'));
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
export function formatClientAgentList(configs) {
|
|
449
|
+
if (!configs || configs.length === 0) {
|
|
450
|
+
console.log(chalk.dim('\n No client agent configs found.\n'));
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
console.log('');
|
|
455
|
+
for (const config of configs) {
|
|
456
|
+
const name = chalk.blue(padRight(config.clientName || '', 24));
|
|
457
|
+
const clientId = padRight(config.clientId || '', 16);
|
|
458
|
+
const badge = workflowTypeBadge(config.workflowType);
|
|
459
|
+
const status = config.isActive !== false ? chalk.green('active') : chalk.red('inactive');
|
|
460
|
+
const schedule = config.schedule ? chalk.dim(config.schedule) : chalk.dim('(manual)');
|
|
461
|
+
|
|
462
|
+
console.log(` ${name} ${clientId} ${badge} ${status} ${schedule}`);
|
|
463
|
+
const agentName = config.villageAgent?.name || config.villageAgentId || '';
|
|
464
|
+
if (agentName) {
|
|
465
|
+
console.log(` ${chalk.dim(' ')}${chalk.dim(`Agent: ${agentName}`)}`);
|
|
466
|
+
}
|
|
467
|
+
console.log('');
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
|
|
399
471
|
// ── Local Agent Formatting ──────────────────────────────
|
|
400
472
|
|
|
401
473
|
export function formatLocalAgentList(agents) {
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// Wisdom file format helpers
|
|
2
|
+
//
|
|
3
|
+
// A `.wisdom` file is a Claude-Code-SKILLS-style document used by a local
|
|
4
|
+
// agent: YAML frontmatter (metadata the loop reads at start-up) followed by
|
|
5
|
+
// a markdown body (the skill content the model uses when the trigger applies).
|
|
6
|
+
//
|
|
7
|
+
// ---
|
|
8
|
+
// name: gentle-greeter
|
|
9
|
+
// description: How to welcome new villagers warmly
|
|
10
|
+
// trigger: when a new villager joins a community I'm in
|
|
11
|
+
// villageBookId: a3f2... # only set after `myvillage wisdom push`
|
|
12
|
+
// ---
|
|
13
|
+
// <markdown body>
|
|
14
|
+
|
|
15
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync } from 'fs';
|
|
16
|
+
import { join, basename } from 'path';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
|
|
19
|
+
// Mirrors the backend parser at app/api/village-books/import/route.ts.
|
|
20
|
+
// Kept tiny and dependency-free so the loop start-up isn't slow.
|
|
21
|
+
export function parseWisdom(text) {
|
|
22
|
+
const match = text.match(/^---\s*\n([\s\S]*?)\n---\s*\n?([\s\S]*)$/);
|
|
23
|
+
if (!match) return { frontmatter: {}, body: text };
|
|
24
|
+
const [, yamlBlock, body] = match;
|
|
25
|
+
const frontmatter = {};
|
|
26
|
+
for (const line of yamlBlock.split('\n')) {
|
|
27
|
+
const m = line.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*(.*)$/);
|
|
28
|
+
if (!m) continue;
|
|
29
|
+
const key = m[1];
|
|
30
|
+
let value = m[2].trim();
|
|
31
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
32
|
+
value = value.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\');
|
|
33
|
+
}
|
|
34
|
+
frontmatter[key] = value;
|
|
35
|
+
}
|
|
36
|
+
return { frontmatter, body: body.replace(/^\n+/, '') };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function escapeYamlString(value) {
|
|
40
|
+
if (/^[a-zA-Z0-9 _.,'\-:/]+$/.test(value) && !value.startsWith(' ') && !value.endsWith(' ')) {
|
|
41
|
+
return value;
|
|
42
|
+
}
|
|
43
|
+
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function serializeWisdom({ frontmatter = {}, body = '' }) {
|
|
47
|
+
const lines = ['---'];
|
|
48
|
+
for (const [key, value] of Object.entries(frontmatter)) {
|
|
49
|
+
if (value == null || value === '') continue;
|
|
50
|
+
lines.push(`${key}: ${escapeYamlString(String(value))}`);
|
|
51
|
+
}
|
|
52
|
+
lines.push('---');
|
|
53
|
+
return `${lines.join('\n')}\n\n${body.trim()}\n`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function slugify(name) {
|
|
57
|
+
return String(name).toLowerCase().trim()
|
|
58
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
59
|
+
.replace(/^-+|-+$/g, '')
|
|
60
|
+
.slice(0, 60) || 'wisdom';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function getAgentWisdomDir(agentName) {
|
|
64
|
+
return join(homedir(), '.myvillage', 'agents', agentName, 'wisdom');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function ensureWisdomDir(agentName) {
|
|
68
|
+
const dir = getAgentWisdomDir(agentName);
|
|
69
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
70
|
+
return dir;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Reads every .wisdom file in the agent's wisdom dir, returning parsed entries
|
|
74
|
+
// in stable filename order. The loop calls this once per iteration.
|
|
75
|
+
export function readAgentWisdom(agentName) {
|
|
76
|
+
const dir = getAgentWisdomDir(agentName);
|
|
77
|
+
if (!existsSync(dir)) return [];
|
|
78
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.wisdom')).sort();
|
|
79
|
+
const out = [];
|
|
80
|
+
for (const file of files) {
|
|
81
|
+
try {
|
|
82
|
+
const text = readFileSync(join(dir, file), 'utf-8');
|
|
83
|
+
const parsed = parseWisdom(text);
|
|
84
|
+
out.push({
|
|
85
|
+
file,
|
|
86
|
+
path: join(dir, file),
|
|
87
|
+
name: parsed.frontmatter.name || basename(file, '.wisdom'),
|
|
88
|
+
description: parsed.frontmatter.description || '',
|
|
89
|
+
trigger: parsed.frontmatter.trigger || '',
|
|
90
|
+
villageBookId: parsed.frontmatter.villageBookId || null,
|
|
91
|
+
body: parsed.body,
|
|
92
|
+
});
|
|
93
|
+
} catch {
|
|
94
|
+
// Skip unreadable files — don't crash the loop over one bad file
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return out;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function writeWisdomFile(filePath, payload) {
|
|
101
|
+
writeFileSync(filePath, serializeWisdom(payload));
|
|
102
|
+
}
|