@minions-tasks/cli 0.3.0 → 0.5.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/dist/index.d.ts +3 -0
- package/dist/index.js +305 -182
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -1,32 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
3
|
* @minions-tasks/cli — CLI for Minions Tasks
|
|
4
|
+
*
|
|
5
|
+
* Uses minions-sdk's JsonFileStorageAdapter for sharded, atomic file storage:
|
|
6
|
+
* <rootDir>/<id[0..1]>/<id[2..3]>/<id>.json
|
|
4
7
|
*/
|
|
5
8
|
import { Command } from 'commander';
|
|
6
9
|
import chalk from 'chalk';
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { randomUUID } from 'crypto';
|
|
10
|
+
import { createMinion, updateMinion, softDelete, TypeRegistry, } from 'minions-sdk';
|
|
11
|
+
import { JsonFileStorageAdapter } from 'minions-sdk/node';
|
|
10
12
|
import { customTypes } from '@minions-tasks/sdk';
|
|
11
13
|
const program = new Command();
|
|
12
|
-
const STORE_DIR =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
14
|
+
const STORE_DIR = process.env.MINIONS_STORE || '.minions';
|
|
15
|
+
const registry = new TypeRegistry();
|
|
16
|
+
for (const t of customTypes) {
|
|
17
|
+
registry.register(t);
|
|
17
18
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
if (!
|
|
21
|
-
|
|
19
|
+
let _storage = null;
|
|
20
|
+
async function getStorage() {
|
|
21
|
+
if (!_storage) {
|
|
22
|
+
_storage = await JsonFileStorageAdapter.create(STORE_DIR);
|
|
22
23
|
}
|
|
23
|
-
return
|
|
24
|
+
return _storage;
|
|
24
25
|
}
|
|
25
26
|
function findType(slug) {
|
|
26
|
-
const type =
|
|
27
|
+
const type = registry.getBySlug(slug);
|
|
27
28
|
if (!type) {
|
|
28
29
|
console.error(chalk.red(`Unknown type: ${slug}`));
|
|
29
|
-
console.error(chalk.dim(`Available
|
|
30
|
+
console.error(chalk.dim(`Available: ${customTypes.map(t => t.slug).join(', ')}`));
|
|
30
31
|
process.exit(1);
|
|
31
32
|
}
|
|
32
33
|
return type;
|
|
@@ -34,7 +35,7 @@ function findType(slug) {
|
|
|
34
35
|
program
|
|
35
36
|
.name('tasks')
|
|
36
37
|
.description('Task and work management across agents, humans, and workflows')
|
|
37
|
-
.version('0.
|
|
38
|
+
.version('0.5.0');
|
|
38
39
|
// ─── info ──────────────────────────────────────────────────
|
|
39
40
|
program
|
|
40
41
|
.command('info')
|
|
@@ -60,7 +61,7 @@ types
|
|
|
60
61
|
for (const type of customTypes) {
|
|
61
62
|
const fieldCount = type.schema.length;
|
|
62
63
|
console.log(` ${type.icon} ${chalk.bold(type.name)} ${chalk.dim(`(${type.slug})`)}`);
|
|
63
|
-
console.log(` ${chalk.dim(type.description)}`);
|
|
64
|
+
console.log(` ${chalk.dim(type.description || '')}`);
|
|
64
65
|
console.log(` ${chalk.dim(`${fieldCount} fields: ${type.schema.map(f => f.name).join(', ')}`)}`);
|
|
65
66
|
console.log('');
|
|
66
67
|
}
|
|
@@ -71,12 +72,13 @@ types
|
|
|
71
72
|
.action((slug) => {
|
|
72
73
|
const type = findType(slug);
|
|
73
74
|
console.log(`\n ${type.icon} ${chalk.bold(type.name)}`);
|
|
74
|
-
console.log(` ${chalk.dim(type.description)}`);
|
|
75
|
+
console.log(` ${chalk.dim(type.description || '')}`);
|
|
75
76
|
console.log(` ${chalk.dim(`ID: ${type.id} Slug: ${type.slug}`)}\n`);
|
|
76
77
|
console.log(chalk.bold(' Fields:\n'));
|
|
77
78
|
for (const field of type.schema) {
|
|
78
79
|
const typeColor = field.type === 'string' ? 'green' : field.type === 'number' ? 'yellow' : field.type === 'boolean' ? 'blue' : 'magenta';
|
|
79
|
-
|
|
80
|
+
const req = field.required ? chalk.red('*') : ' ';
|
|
81
|
+
console.log(` ${req} ${chalk.bold(field.name)} ${chalk[typeColor](field.type)}${field.description ? ` ${chalk.dim(field.description)}` : ''}`);
|
|
80
82
|
}
|
|
81
83
|
console.log('');
|
|
82
84
|
});
|
|
@@ -86,42 +88,37 @@ program
|
|
|
86
88
|
.description('Create a new Minion of the specified type')
|
|
87
89
|
.option('-d, --data <json>', 'Field data as JSON string')
|
|
88
90
|
.option('-f, --file <path>', 'Read field data from a JSON file')
|
|
89
|
-
.option('-t, --title <title>', '
|
|
90
|
-
.option('-s, --status <status>', '
|
|
91
|
-
.option('-p, --priority <priority>', '
|
|
92
|
-
.
|
|
91
|
+
.option('-t, --title <title>', 'Minion title')
|
|
92
|
+
.option('-s, --status <status>', 'Status: active, todo, in_progress, completed, cancelled')
|
|
93
|
+
.option('-p, --priority <priority>', 'Priority: low, medium, high, urgent')
|
|
94
|
+
.option('--tags <tags>', 'Comma-separated tags')
|
|
95
|
+
.action(async (typeSlug, opts) => {
|
|
93
96
|
const type = findType(typeSlug);
|
|
94
|
-
|
|
97
|
+
const storage = await getStorage();
|
|
95
98
|
let fields = {};
|
|
96
99
|
if (opts.file) {
|
|
100
|
+
const { readFileSync } = await import('fs');
|
|
97
101
|
fields = JSON.parse(readFileSync(opts.file, 'utf-8'));
|
|
98
102
|
}
|
|
99
103
|
else if (opts.data) {
|
|
100
104
|
fields = JSON.parse(opts.data);
|
|
101
105
|
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
fields.status = opts.status;
|
|
107
|
-
if (opts.priority)
|
|
108
|
-
fields.priority = opts.priority;
|
|
109
|
-
const minion = {
|
|
110
|
-
id: randomUUID(),
|
|
111
|
-
type: type.slug,
|
|
112
|
-
typeName: type.name,
|
|
106
|
+
const title = opts.title || fields.title || fields.name || type.name;
|
|
107
|
+
const tags = opts.tags ? opts.tags.split(',').map((t) => t.trim()) : undefined;
|
|
108
|
+
const { minion } = createMinion({
|
|
109
|
+
title,
|
|
113
110
|
fields,
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
111
|
+
status: opts.status || 'active',
|
|
112
|
+
priority: opts.priority,
|
|
113
|
+
tags,
|
|
114
|
+
createdBy: 'cli',
|
|
115
|
+
}, type);
|
|
116
|
+
await storage.set(minion);
|
|
117
|
+
const hex = minion.id.replace(/-/g, '');
|
|
120
118
|
console.log(chalk.green(`\n ✔ Created ${type.icon} ${type.name}`));
|
|
121
119
|
console.log(` ${chalk.dim('ID:')} ${minion.id}`);
|
|
122
|
-
console.log(` ${chalk.dim('
|
|
123
|
-
|
|
124
|
-
console.log(` ${chalk.dim('Title:')} ${fields.title}`);
|
|
120
|
+
console.log(` ${chalk.dim('Title:')} ${minion.title}`);
|
|
121
|
+
console.log(` ${chalk.dim('Path:')} ${STORE_DIR}/${hex.slice(0, 2)}/${hex.slice(2, 4)}/${minion.id}.json`);
|
|
125
122
|
console.log('');
|
|
126
123
|
});
|
|
127
124
|
// ─── list ──────────────────────────────────────────────────
|
|
@@ -129,37 +126,36 @@ program
|
|
|
129
126
|
.command('list [type]')
|
|
130
127
|
.alias('ls')
|
|
131
128
|
.description('List all Minions, optionally filtered by type')
|
|
129
|
+
.option('--status <status>', 'Filter by status')
|
|
132
130
|
.option('--json', 'Output as JSON')
|
|
133
|
-
.
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
continue;
|
|
141
|
-
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
142
|
-
for (const file of files) {
|
|
143
|
-
const minion = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
144
|
-
allMinions.push(minion);
|
|
145
|
-
}
|
|
131
|
+
.option('-n, --limit <n>', 'Max results', parseInt)
|
|
132
|
+
.action(async (typeSlug, opts) => {
|
|
133
|
+
const storage = await getStorage();
|
|
134
|
+
const filter = {};
|
|
135
|
+
if (typeSlug) {
|
|
136
|
+
const type = findType(typeSlug);
|
|
137
|
+
filter.minionTypeId = type.id;
|
|
146
138
|
}
|
|
139
|
+
if (opts.status)
|
|
140
|
+
filter.status = opts.status;
|
|
141
|
+
if (opts.limit)
|
|
142
|
+
filter.limit = opts.limit;
|
|
143
|
+
const minions = await storage.list(filter);
|
|
147
144
|
if (opts.json) {
|
|
148
|
-
console.log(JSON.stringify(
|
|
145
|
+
console.log(JSON.stringify(minions, null, 2));
|
|
149
146
|
return;
|
|
150
147
|
}
|
|
151
|
-
if (
|
|
148
|
+
if (minions.length === 0) {
|
|
152
149
|
console.log(chalk.dim('\n No Minions found.\n'));
|
|
153
150
|
return;
|
|
154
151
|
}
|
|
155
|
-
console.log(chalk.bold(`\n ${
|
|
156
|
-
for (const m of
|
|
157
|
-
const type =
|
|
152
|
+
console.log(chalk.bold(`\n ${minions.length} Minion(s):\n`));
|
|
153
|
+
for (const m of minions) {
|
|
154
|
+
const type = registry.getById(m.minionTypeId) || registry.getBySlug(m.minionTypeId);
|
|
158
155
|
const icon = type?.icon || '?';
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
console.log(`
|
|
162
|
-
console.log(` ${chalk.dim(m.id)} ${chalk.dim(m.type)}`);
|
|
156
|
+
const status = m.status ? chalk.dim(`[${m.status}]`) : '';
|
|
157
|
+
console.log(` ${icon} ${chalk.bold(m.title)} ${status}`);
|
|
158
|
+
console.log(` ${chalk.dim(m.id)} ${chalk.dim(type?.slug || m.minionTypeId)}`);
|
|
163
159
|
}
|
|
164
160
|
console.log('');
|
|
165
161
|
});
|
|
@@ -168,139 +164,270 @@ program
|
|
|
168
164
|
.command('show <id>')
|
|
169
165
|
.description('Show a Minion by ID')
|
|
170
166
|
.option('--json', 'Output as JSON')
|
|
171
|
-
.action((id, opts) => {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const minion = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
178
|
-
if (opts.json) {
|
|
179
|
-
console.log(JSON.stringify(minion, null, 2));
|
|
180
|
-
return;
|
|
181
|
-
}
|
|
182
|
-
console.log(`\n ${type.icon} ${chalk.bold(minion.fields?.title || minion.fields?.name || type.name)}`);
|
|
183
|
-
console.log(` ${chalk.dim(`Type: ${minion.type} ID: ${minion.id}`)}`);
|
|
184
|
-
console.log(` ${chalk.dim(`Created: ${minion.createdAt}`)}\n`);
|
|
185
|
-
console.log(chalk.bold(' Fields:\n'));
|
|
186
|
-
for (const [key, value] of Object.entries(minion.fields || {})) {
|
|
187
|
-
console.log(` ${chalk.dim('•')} ${chalk.bold(key)}: ${value}`);
|
|
188
|
-
}
|
|
189
|
-
console.log('');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
167
|
+
.action(async (id, opts) => {
|
|
168
|
+
const storage = await getStorage();
|
|
169
|
+
const minion = await storage.get(id);
|
|
170
|
+
if (!minion) {
|
|
171
|
+
console.error(chalk.red(`\n Minion not found: ${id}\n`));
|
|
172
|
+
process.exit(1);
|
|
192
173
|
}
|
|
193
|
-
|
|
194
|
-
|
|
174
|
+
if (opts.json) {
|
|
175
|
+
console.log(JSON.stringify(minion, null, 2));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const type = registry.getById(minion.minionTypeId) || registry.getBySlug(minion.minionTypeId);
|
|
179
|
+
console.log(`\n ${type?.icon || '?'} ${chalk.bold(minion.title)}`);
|
|
180
|
+
console.log(` ${chalk.dim(`Type: ${type?.slug || minion.minionTypeId} ID: ${minion.id}`)}`);
|
|
181
|
+
console.log(` ${chalk.dim(`Status: ${minion.status || '-'} Priority: ${minion.priority || '-'}`)}`);
|
|
182
|
+
console.log(` ${chalk.dim(`Created: ${minion.createdAt} Updated: ${minion.updatedAt}`)}`);
|
|
183
|
+
if (minion.tags?.length)
|
|
184
|
+
console.log(` ${chalk.dim(`Tags: ${minion.tags.join(', ')}`)}`);
|
|
185
|
+
console.log(chalk.bold('\n Fields:\n'));
|
|
186
|
+
for (const [key, value] of Object.entries(minion.fields || {})) {
|
|
187
|
+
console.log(` ${chalk.dim('•')} ${chalk.bold(key)}: ${value}`);
|
|
188
|
+
}
|
|
189
|
+
console.log('');
|
|
195
190
|
});
|
|
196
191
|
// ─── update ────────────────────────────────────────────────
|
|
197
192
|
program
|
|
198
193
|
.command('update <id>')
|
|
199
194
|
.description('Update fields on an existing Minion')
|
|
200
195
|
.option('-d, --data <json>', 'Fields to update as JSON')
|
|
201
|
-
.option('-s, --status <status>', '
|
|
202
|
-
.option('-p, --priority <priority>', '
|
|
203
|
-
.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
updates = JSON.parse(opts.data);
|
|
212
|
-
if (opts.status)
|
|
213
|
-
updates.status = opts.status;
|
|
214
|
-
if (opts.priority)
|
|
215
|
-
updates.priority = opts.priority;
|
|
216
|
-
minion.fields = { ...minion.fields, ...updates };
|
|
217
|
-
minion.updatedAt = new Date().toISOString();
|
|
218
|
-
writeFileSync(filePath, JSON.stringify(minion, null, 2));
|
|
219
|
-
console.log(chalk.green(`\n ✔ Updated ${type.icon} ${minion.fields?.title || type.name}`));
|
|
220
|
-
for (const [key, value] of Object.entries(updates)) {
|
|
221
|
-
console.log(` ${chalk.dim('•')} ${key} → ${value}`);
|
|
222
|
-
}
|
|
223
|
-
console.log('');
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
196
|
+
.option('-s, --status <status>', 'Update status')
|
|
197
|
+
.option('-p, --priority <priority>', 'Update priority')
|
|
198
|
+
.option('-t, --title <title>', 'Update title')
|
|
199
|
+
.option('--tags <tags>', 'Replace tags (comma-separated)')
|
|
200
|
+
.action(async (id, opts) => {
|
|
201
|
+
const storage = await getStorage();
|
|
202
|
+
const existing = await storage.get(id);
|
|
203
|
+
if (!existing) {
|
|
204
|
+
console.error(chalk.red(`\n Minion not found: ${id}\n`));
|
|
205
|
+
process.exit(1);
|
|
226
206
|
}
|
|
227
|
-
|
|
228
|
-
|
|
207
|
+
const updates = {};
|
|
208
|
+
if (opts.data)
|
|
209
|
+
updates.fields = { ...existing.fields, ...JSON.parse(opts.data) };
|
|
210
|
+
if (opts.status)
|
|
211
|
+
updates.status = opts.status;
|
|
212
|
+
if (opts.priority)
|
|
213
|
+
updates.priority = opts.priority;
|
|
214
|
+
if (opts.title)
|
|
215
|
+
updates.title = opts.title;
|
|
216
|
+
if (opts.tags)
|
|
217
|
+
updates.tags = opts.tags.split(',').map((t) => t.trim());
|
|
218
|
+
const typeDef = registry.getById(existing.minionTypeId) || registry.getBySlug(existing.minionTypeId);
|
|
219
|
+
if (!typeDef) {
|
|
220
|
+
console.error(chalk.red(`\n Minion type not found: ${existing.minionTypeId}\n`));
|
|
221
|
+
process.exit(1);
|
|
222
|
+
}
|
|
223
|
+
const { minion: updated } = updateMinion(existing, { ...updates, updatedBy: 'cli' }, typeDef);
|
|
224
|
+
await storage.set(updated);
|
|
225
|
+
const type = registry.getById(updated.minionTypeId) || registry.getBySlug(updated.minionTypeId);
|
|
226
|
+
console.log(chalk.green(`\n ✔ Updated ${type?.icon || '?'} ${updated.title}`));
|
|
229
227
|
});
|
|
230
228
|
// ─── delete ────────────────────────────────────────────────
|
|
231
229
|
program
|
|
232
230
|
.command('delete <id>')
|
|
233
|
-
.description('
|
|
234
|
-
.option('--hard', 'Permanently
|
|
235
|
-
.action((id, opts) => {
|
|
236
|
-
|
|
237
|
-
const
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
231
|
+
.description('Soft-delete a Minion')
|
|
232
|
+
.option('--hard', 'Permanently remove from disk')
|
|
233
|
+
.action(async (id, opts) => {
|
|
234
|
+
const storage = await getStorage();
|
|
235
|
+
const existing = await storage.get(id);
|
|
236
|
+
if (!existing) {
|
|
237
|
+
console.error(chalk.red(`\n Minion not found: ${id}\n`));
|
|
238
|
+
process.exit(1);
|
|
239
|
+
}
|
|
240
|
+
if (opts.hard) {
|
|
241
|
+
await storage.delete(id);
|
|
242
|
+
console.log(chalk.yellow(`\n 🗑 Permanently deleted ${id}\n`));
|
|
243
|
+
}
|
|
244
|
+
else {
|
|
245
|
+
const deleted = softDelete(existing, 'cli');
|
|
246
|
+
await storage.set(deleted);
|
|
247
|
+
console.log(chalk.yellow(`\n ✔ Soft-deleted ${existing.title}`));
|
|
248
|
+
console.log(chalk.dim(` Use --hard to permanently remove\n`));
|
|
249
|
+
}
|
|
250
|
+
});
|
|
251
|
+
// ─── search ────────────────────────────────────────────────
|
|
252
|
+
program
|
|
253
|
+
.command('search <query>')
|
|
254
|
+
.description('Full-text search across all Minions')
|
|
255
|
+
.option('--json', 'Output as JSON')
|
|
256
|
+
.action(async (query, opts) => {
|
|
257
|
+
const storage = await getStorage();
|
|
258
|
+
const results = await storage.search(query);
|
|
259
|
+
if (opts.json) {
|
|
260
|
+
console.log(JSON.stringify(results, null, 2));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (results.length === 0) {
|
|
264
|
+
console.log(chalk.dim(`\n No results for "${query}".\n`));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
console.log(chalk.bold(`\n ${results.length} result(s) for "${query}":\n`));
|
|
268
|
+
for (const m of results) {
|
|
269
|
+
const type = registry.getById(m.minionTypeId) || registry.getBySlug(m.minionTypeId);
|
|
270
|
+
const icon = type?.icon || '?';
|
|
271
|
+
const status = m.status ? chalk.dim(`[${m.status}]`) : '';
|
|
272
|
+
console.log(` ${icon} ${chalk.bold(m.title)} ${status}`);
|
|
273
|
+
console.log(` ${chalk.dim(m.id)} ${chalk.dim(type?.slug || m.minionTypeId)}`);
|
|
274
|
+
}
|
|
275
|
+
console.log('');
|
|
276
|
+
});
|
|
277
|
+
// ─── blocked ───────────────────────────────────────────────
|
|
278
|
+
program
|
|
279
|
+
.command('blocked')
|
|
280
|
+
.description('List all tasks with status "blocked" and their blockers')
|
|
281
|
+
.action(async () => {
|
|
282
|
+
const storage = await getStorage();
|
|
283
|
+
const taskType = findType('task');
|
|
284
|
+
const depType = registry.getBySlug('task-dependency');
|
|
285
|
+
const blockedTasks = (await storage.list({ minionTypeId: taskType.id }))
|
|
286
|
+
.filter(t => t.fields?.status === 'blocked' || t.status === 'in_progress');
|
|
287
|
+
// Also check status field in fields for tasks
|
|
288
|
+
const allBlocked = (await storage.list({ minionTypeId: taskType.id }))
|
|
289
|
+
.filter(t => {
|
|
290
|
+
const fieldStatus = t.fields?.status;
|
|
291
|
+
return fieldStatus === 'blocked' || t.status === 'in_progress';
|
|
292
|
+
});
|
|
293
|
+
if (allBlocked.length === 0) {
|
|
294
|
+
console.log(chalk.green('\n ✔ No blocked tasks.\n'));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
const deps = depType ? await storage.list({ minionTypeId: depType.id }) : [];
|
|
298
|
+
console.log(chalk.bold(`\n 🚫 ${allBlocked.length} blocked/in-progress task(s):\n`));
|
|
299
|
+
for (const t of allBlocked) {
|
|
300
|
+
console.log(` ✅ ${chalk.bold(t.title)}`);
|
|
301
|
+
console.log(` ${chalk.dim(t.id)}`);
|
|
302
|
+
const taskDeps = deps.filter(d => d.fields?.taskId === t.id);
|
|
303
|
+
for (const d of taskDeps) {
|
|
304
|
+
console.log(` ${chalk.red('→')} blocked by ${chalk.dim(d.fields.dependsOnTaskId)} (${d.fields.type})`);
|
|
254
305
|
}
|
|
306
|
+
console.log('');
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
// ─── complete ──────────────────────────────────────────────
|
|
310
|
+
program
|
|
311
|
+
.command('complete <id>')
|
|
312
|
+
.description('Mark a task as done and create a task-outcome')
|
|
313
|
+
.option('-r, --result <result>', 'Outcome: success, partial, failed', 'success')
|
|
314
|
+
.option('--summary <summary>', 'What was accomplished')
|
|
315
|
+
.option('--lessons <lessons>', 'Lessons for agent learning loop')
|
|
316
|
+
.action(async (id, opts) => {
|
|
317
|
+
const storage = await getStorage();
|
|
318
|
+
const task = await storage.get(id);
|
|
319
|
+
if (!task) {
|
|
320
|
+
console.error(chalk.red(`\n Task not found: ${id}\n`));
|
|
321
|
+
process.exit(1);
|
|
322
|
+
}
|
|
323
|
+
// Update task status
|
|
324
|
+
const typeDef = registry.getById(task.minionTypeId) || registry.getBySlug(task.minionTypeId);
|
|
325
|
+
if (!typeDef) {
|
|
326
|
+
console.error(chalk.red(`\n Minion type not found: ${task.minionTypeId}\n`));
|
|
327
|
+
process.exit(1);
|
|
255
328
|
}
|
|
256
|
-
|
|
257
|
-
|
|
329
|
+
const { minion: completed } = updateMinion(task, {
|
|
330
|
+
fields: { ...task.fields, status: 'done', completedAt: new Date().toISOString() },
|
|
331
|
+
status: 'completed',
|
|
332
|
+
updatedBy: 'cli',
|
|
333
|
+
}, typeDef);
|
|
334
|
+
await storage.set(completed);
|
|
335
|
+
// Create task-outcome
|
|
336
|
+
const outcomeType = findType('task-outcome');
|
|
337
|
+
const { minion: outcome } = createMinion({
|
|
338
|
+
title: `Outcome: ${task.title}`,
|
|
339
|
+
fields: {
|
|
340
|
+
taskId: id,
|
|
341
|
+
result: opts.result,
|
|
342
|
+
summary: opts.summary || '',
|
|
343
|
+
artifactIds: '',
|
|
344
|
+
lessons: opts.lessons || '',
|
|
345
|
+
},
|
|
346
|
+
createdBy: 'cli',
|
|
347
|
+
}, outcomeType);
|
|
348
|
+
await storage.set(outcome);
|
|
349
|
+
console.log(chalk.green(`\n ✔ Completed ✅ ${task.title}`));
|
|
350
|
+
console.log(` ${chalk.dim('Result:')} ${opts.result}`);
|
|
351
|
+
console.log(` ${chalk.dim('Outcome:')} ${outcome.id}\n`);
|
|
352
|
+
});
|
|
353
|
+
// ─── assign ────────────────────────────────────────────────
|
|
354
|
+
program
|
|
355
|
+
.command('assign <taskId> <assigneeId>')
|
|
356
|
+
.description('Assign a task to a person or agent')
|
|
357
|
+
.option('--type <type>', 'Assignee type: human or agent', 'agent')
|
|
358
|
+
.option('--role <role>', 'Role: owner, collaborator, reviewer, observer', 'owner')
|
|
359
|
+
.action(async (taskId, assigneeId, opts) => {
|
|
360
|
+
const storage = await getStorage();
|
|
361
|
+
const task = await storage.get(taskId);
|
|
362
|
+
if (!task) {
|
|
363
|
+
console.error(chalk.red(`\n Task not found: ${taskId}\n`));
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
const assignType = findType('task-assignment');
|
|
367
|
+
const { minion: assignment } = createMinion({
|
|
368
|
+
title: `Assignment: ${task.title} → ${assigneeId}`,
|
|
369
|
+
fields: {
|
|
370
|
+
taskId,
|
|
371
|
+
assigneeId,
|
|
372
|
+
assigneeType: opts.type,
|
|
373
|
+
assignedAt: new Date().toISOString(),
|
|
374
|
+
assignedBy: 'cli',
|
|
375
|
+
role: opts.role,
|
|
376
|
+
},
|
|
377
|
+
createdBy: 'cli',
|
|
378
|
+
}, assignType);
|
|
379
|
+
await storage.set(assignment);
|
|
380
|
+
console.log(chalk.green(`\n ✔ Assigned task to ${assigneeId}`));
|
|
381
|
+
console.log(` ${chalk.dim('Task:')} ${taskId}`);
|
|
382
|
+
console.log(` ${chalk.dim('Role:')} ${opts.role}`);
|
|
383
|
+
console.log(` ${chalk.dim('ID:')} ${assignment.id}\n`);
|
|
384
|
+
});
|
|
385
|
+
// ─── comment ───────────────────────────────────────────────
|
|
386
|
+
program
|
|
387
|
+
.command('comment <taskId> <body>')
|
|
388
|
+
.description('Add a comment to a task')
|
|
389
|
+
.option('--author <id>', 'Author ID', 'cli')
|
|
390
|
+
.option('--type <type>', 'Author type: human or agent', 'agent')
|
|
391
|
+
.action(async (taskId, body, opts) => {
|
|
392
|
+
const storage = await getStorage();
|
|
393
|
+
const commentType = findType('task-comment');
|
|
394
|
+
const { minion: comment } = createMinion({
|
|
395
|
+
title: `Comment on ${taskId}`,
|
|
396
|
+
fields: {
|
|
397
|
+
taskId,
|
|
398
|
+
authorId: opts.author,
|
|
399
|
+
authorType: opts.type,
|
|
400
|
+
body,
|
|
401
|
+
createdAt: new Date().toISOString(),
|
|
402
|
+
resolvedAt: '',
|
|
403
|
+
},
|
|
404
|
+
createdBy: opts.author,
|
|
405
|
+
}, commentType);
|
|
406
|
+
await storage.set(comment);
|
|
407
|
+
console.log(chalk.green(`\n ✔ Comment added to task ${taskId}`));
|
|
408
|
+
console.log(` ${chalk.dim('ID:')} ${comment.id}\n`);
|
|
258
409
|
});
|
|
259
410
|
// ─── validate ──────────────────────────────────────────────
|
|
260
411
|
program
|
|
261
412
|
.command('validate <file>')
|
|
262
413
|
.description('Validate a JSON file against its MinionType schema')
|
|
263
|
-
.action((file) => {
|
|
414
|
+
.action(async (file) => {
|
|
415
|
+
const { readFileSync } = await import('fs');
|
|
416
|
+
const { validateFields } = await import('minions-sdk');
|
|
264
417
|
const data = JSON.parse(readFileSync(file, 'utf-8'));
|
|
265
|
-
const type =
|
|
418
|
+
const type = registry.getById(data.minionTypeId) || registry.getBySlug(data.minionTypeId);
|
|
266
419
|
if (!type) {
|
|
267
|
-
console.error(chalk.red(`\n Unknown type: ${data.
|
|
420
|
+
console.error(chalk.red(`\n Unknown type: ${data.minionTypeId}\n`));
|
|
268
421
|
process.exit(1);
|
|
269
422
|
}
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
const dataFields = Object.keys(data.fields || {});
|
|
273
|
-
// Check for missing fields
|
|
274
|
-
for (const f of schemaFields) {
|
|
275
|
-
if (!(f in (data.fields || {}))) {
|
|
276
|
-
errors.push(`Missing field: ${f}`);
|
|
277
|
-
}
|
|
278
|
-
}
|
|
279
|
-
// Check for unknown fields
|
|
280
|
-
for (const f of dataFields) {
|
|
281
|
-
if (!schemaFields.includes(f)) {
|
|
282
|
-
errors.push(`Unknown field: ${f}`);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
// Check field types
|
|
286
|
-
for (const field of type.schema) {
|
|
287
|
-
const value = data.fields?.[field.name];
|
|
288
|
-
if (value === undefined)
|
|
289
|
-
continue;
|
|
290
|
-
if (field.type === 'number' && typeof value !== 'number') {
|
|
291
|
-
errors.push(`Field ${field.name} should be number, got ${typeof value}`);
|
|
292
|
-
}
|
|
293
|
-
if (field.type === 'boolean' && typeof value !== 'boolean') {
|
|
294
|
-
errors.push(`Field ${field.name} should be boolean, got ${typeof value}`);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
if (errors.length === 0) {
|
|
423
|
+
const result = validateFields(data.fields, type.schema);
|
|
424
|
+
if (result.valid) {
|
|
298
425
|
console.log(chalk.green(`\n ✔ Valid ${type.icon} ${type.name}\n`));
|
|
299
426
|
}
|
|
300
427
|
else {
|
|
301
|
-
console.log(chalk.red(`\n ✘ ${errors.length} validation error(s):\n`));
|
|
302
|
-
for (const err of errors) {
|
|
303
|
-
console.log(` ${chalk.red('•')} ${err}`);
|
|
428
|
+
console.log(chalk.red(`\n ✘ ${result.errors.length} validation error(s):\n`));
|
|
429
|
+
for (const err of result.errors) {
|
|
430
|
+
console.log(` ${chalk.red('•')} ${err.field}: ${err.message}`);
|
|
304
431
|
}
|
|
305
432
|
console.log('');
|
|
306
433
|
process.exit(1);
|
|
@@ -310,20 +437,16 @@ program
|
|
|
310
437
|
program
|
|
311
438
|
.command('stats')
|
|
312
439
|
.description('Show statistics about stored Minions')
|
|
313
|
-
.action(() => {
|
|
314
|
-
|
|
440
|
+
.action(async () => {
|
|
441
|
+
const storage = await getStorage();
|
|
315
442
|
console.log(chalk.bold('\n Minion Statistics:\n'));
|
|
316
443
|
let total = 0;
|
|
317
444
|
for (const type of customTypes) {
|
|
318
|
-
const
|
|
319
|
-
|
|
320
|
-
console.log(` ${type.icon} ${type.name.padEnd(22)} ${chalk.dim('0')}`);
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
const count = readdirSync(dir).filter(f => f.endsWith('.json')).length;
|
|
445
|
+
const minions = await storage.list({ minionTypeId: type.id });
|
|
446
|
+
const count = minions.length;
|
|
324
447
|
total += count;
|
|
325
448
|
const bar = chalk.cyan('█'.repeat(Math.min(count, 30)));
|
|
326
|
-
console.log(` ${type.icon} ${type.name.padEnd(22)} ${String(count).padStart(4)} ${bar}`);
|
|
449
|
+
console.log(` ${type.icon} ${(type.name || '').padEnd(22)} ${String(count).padStart(4)} ${count > 0 ? bar : chalk.dim('0')}`);
|
|
327
450
|
}
|
|
328
451
|
console.log(`\n ${chalk.bold('Total:')} ${total} Minion(s)`);
|
|
329
452
|
console.log(` ${chalk.dim(`Store: ${STORE_DIR}`)}\n`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minions-tasks/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "CLI for minions-tasks — Task and work management across agents, humans, and workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"chalk": "^5.6.2",
|
|
28
28
|
"commander": "^14.0.3",
|
|
29
29
|
"inquirer": "^13.2.5",
|
|
30
|
-
"minions-sdk": "^0.2.
|
|
31
|
-
"@minions-tasks/sdk": "0.
|
|
30
|
+
"minions-sdk": "^0.2.3",
|
|
31
|
+
"@minions-tasks/sdk": "0.5.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^25.3.0",
|