@minions-tasks/cli 0.2.2 → 0.4.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.js +498 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -4,11 +4,38 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { Command } from 'commander';
|
|
6
6
|
import chalk from 'chalk';
|
|
7
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs';
|
|
8
|
+
import { join, resolve } from 'path';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
|
+
import { customTypes } from '@minions-tasks/sdk';
|
|
7
11
|
const program = new Command();
|
|
12
|
+
const STORE_DIR = resolve(process.env.MINIONS_STORE || '.minions');
|
|
13
|
+
function ensureStore() {
|
|
14
|
+
if (!existsSync(STORE_DIR)) {
|
|
15
|
+
mkdirSync(STORE_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function getTypeDir(slug) {
|
|
19
|
+
const dir = join(STORE_DIR, slug);
|
|
20
|
+
if (!existsSync(dir)) {
|
|
21
|
+
mkdirSync(dir, { recursive: true });
|
|
22
|
+
}
|
|
23
|
+
return dir;
|
|
24
|
+
}
|
|
25
|
+
function findType(slug) {
|
|
26
|
+
const type = customTypes.find(t => t.slug === slug);
|
|
27
|
+
if (!type) {
|
|
28
|
+
console.error(chalk.red(`Unknown type: ${slug}`));
|
|
29
|
+
console.error(chalk.dim(`Available types: ${customTypes.map(t => t.slug).join(', ')}`));
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
return type;
|
|
33
|
+
}
|
|
8
34
|
program
|
|
9
35
|
.name('tasks')
|
|
10
36
|
.description('Task and work management across agents, humans, and workflows')
|
|
11
|
-
.version('0.
|
|
37
|
+
.version('0.4.0');
|
|
38
|
+
// ─── info ──────────────────────────────────────────────────
|
|
12
39
|
program
|
|
13
40
|
.command('info')
|
|
14
41
|
.description('Show project info')
|
|
@@ -19,5 +46,475 @@ program
|
|
|
19
46
|
console.log(` SDK: ${chalk.cyan('@minions-tasks/sdk')}`);
|
|
20
47
|
console.log(` CLI: ${chalk.cyan('@minions-tasks/cli')}`);
|
|
21
48
|
console.log(` Python: ${chalk.cyan('minions-tasks')}`);
|
|
49
|
+
console.log(` Store: ${chalk.cyan(STORE_DIR)}`);
|
|
50
|
+
console.log(` Types: ${chalk.cyan(String(customTypes.length))}`);
|
|
51
|
+
});
|
|
52
|
+
// ─── types ─────────────────────────────────────────────────
|
|
53
|
+
const types = program.command('types').description('Manage MinionType schemas');
|
|
54
|
+
types
|
|
55
|
+
.command('list')
|
|
56
|
+
.alias('ls')
|
|
57
|
+
.description('List all available MinionTypes')
|
|
58
|
+
.action(() => {
|
|
59
|
+
console.log(chalk.bold(`\n ${customTypes.length} MinionTypes available:\n`));
|
|
60
|
+
for (const type of customTypes) {
|
|
61
|
+
const fieldCount = type.schema.length;
|
|
62
|
+
console.log(` ${type.icon} ${chalk.bold(type.name)} ${chalk.dim(`(${type.slug})`)}`);
|
|
63
|
+
console.log(` ${chalk.dim(type.description)}`);
|
|
64
|
+
console.log(` ${chalk.dim(`${fieldCount} fields: ${type.schema.map(f => f.name).join(', ')}`)}`);
|
|
65
|
+
console.log('');
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
types
|
|
69
|
+
.command('show <slug>')
|
|
70
|
+
.description('Show detailed schema for a MinionType')
|
|
71
|
+
.action((slug) => {
|
|
72
|
+
const type = findType(slug);
|
|
73
|
+
console.log(`\n ${type.icon} ${chalk.bold(type.name)}`);
|
|
74
|
+
console.log(` ${chalk.dim(type.description)}`);
|
|
75
|
+
console.log(` ${chalk.dim(`ID: ${type.id} Slug: ${type.slug}`)}\n`);
|
|
76
|
+
console.log(chalk.bold(' Fields:\n'));
|
|
77
|
+
for (const field of type.schema) {
|
|
78
|
+
const typeColor = field.type === 'string' ? 'green' : field.type === 'number' ? 'yellow' : field.type === 'boolean' ? 'blue' : 'magenta';
|
|
79
|
+
console.log(` ${chalk.dim('•')} ${chalk.bold(field.name)} ${chalk[typeColor](field.type)}`);
|
|
80
|
+
}
|
|
81
|
+
console.log('');
|
|
82
|
+
});
|
|
83
|
+
// ─── create ────────────────────────────────────────────────
|
|
84
|
+
program
|
|
85
|
+
.command('create <type>')
|
|
86
|
+
.description('Create a new Minion of the specified type')
|
|
87
|
+
.option('-d, --data <json>', 'Field data as JSON string')
|
|
88
|
+
.option('-f, --file <path>', 'Read field data from a JSON file')
|
|
89
|
+
.option('-t, --title <title>', 'Shortcut: set the title field')
|
|
90
|
+
.option('-s, --status <status>', 'Shortcut: set the status field')
|
|
91
|
+
.option('-p, --priority <priority>', 'Shortcut: set the priority field')
|
|
92
|
+
.action((typeSlug, opts) => {
|
|
93
|
+
const type = findType(typeSlug);
|
|
94
|
+
ensureStore();
|
|
95
|
+
let fields = {};
|
|
96
|
+
if (opts.file) {
|
|
97
|
+
fields = JSON.parse(readFileSync(opts.file, 'utf-8'));
|
|
98
|
+
}
|
|
99
|
+
else if (opts.data) {
|
|
100
|
+
fields = JSON.parse(opts.data);
|
|
101
|
+
}
|
|
102
|
+
// Apply shortcut flags
|
|
103
|
+
if (opts.title)
|
|
104
|
+
fields.title = opts.title;
|
|
105
|
+
if (opts.status)
|
|
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,
|
|
113
|
+
fields,
|
|
114
|
+
createdAt: new Date().toISOString(),
|
|
115
|
+
updatedAt: new Date().toISOString(),
|
|
116
|
+
};
|
|
117
|
+
const dir = getTypeDir(type.slug);
|
|
118
|
+
const filePath = join(dir, `${minion.id}.json`);
|
|
119
|
+
writeFileSync(filePath, JSON.stringify(minion, null, 2));
|
|
120
|
+
console.log(chalk.green(`\n ✔ Created ${type.icon} ${type.name}`));
|
|
121
|
+
console.log(` ${chalk.dim('ID:')} ${minion.id}`);
|
|
122
|
+
console.log(` ${chalk.dim('File:')} ${filePath}`);
|
|
123
|
+
if (fields.title)
|
|
124
|
+
console.log(` ${chalk.dim('Title:')} ${fields.title}`);
|
|
125
|
+
console.log('');
|
|
126
|
+
});
|
|
127
|
+
// ─── list ──────────────────────────────────────────────────
|
|
128
|
+
program
|
|
129
|
+
.command('list [type]')
|
|
130
|
+
.alias('ls')
|
|
131
|
+
.description('List all Minions, optionally filtered by type')
|
|
132
|
+
.option('--json', 'Output as JSON')
|
|
133
|
+
.action((typeSlug, opts) => {
|
|
134
|
+
ensureStore();
|
|
135
|
+
const slugs = typeSlug ? [typeSlug] : customTypes.map(t => t.slug);
|
|
136
|
+
const allMinions = [];
|
|
137
|
+
for (const slug of slugs) {
|
|
138
|
+
const dir = join(STORE_DIR, slug);
|
|
139
|
+
if (!existsSync(dir))
|
|
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
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if (opts.json) {
|
|
148
|
+
console.log(JSON.stringify(allMinions, null, 2));
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (allMinions.length === 0) {
|
|
152
|
+
console.log(chalk.dim('\n No Minions found.\n'));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
console.log(chalk.bold(`\n ${allMinions.length} Minion(s):\n`));
|
|
156
|
+
for (const m of allMinions) {
|
|
157
|
+
const type = customTypes.find(t => t.slug === m.type);
|
|
158
|
+
const icon = type?.icon || '?';
|
|
159
|
+
const title = m.fields?.title || m.fields?.name || m.fields?.label || chalk.dim('(untitled)');
|
|
160
|
+
const status = m.fields?.status ? chalk.dim(`[${m.fields.status}]`) : '';
|
|
161
|
+
console.log(` ${icon} ${chalk.bold(title)} ${status}`);
|
|
162
|
+
console.log(` ${chalk.dim(m.id)} ${chalk.dim(m.type)}`);
|
|
163
|
+
}
|
|
164
|
+
console.log('');
|
|
165
|
+
});
|
|
166
|
+
// ─── show ──────────────────────────────────────────────────
|
|
167
|
+
program
|
|
168
|
+
.command('show <id>')
|
|
169
|
+
.description('Show a Minion by ID')
|
|
170
|
+
.option('--json', 'Output as JSON')
|
|
171
|
+
.action((id, opts) => {
|
|
172
|
+
ensureStore();
|
|
173
|
+
// Search across all type directories
|
|
174
|
+
for (const type of customTypes) {
|
|
175
|
+
const filePath = join(STORE_DIR, type.slug, `${id}.json`);
|
|
176
|
+
if (existsSync(filePath)) {
|
|
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
|
+
}
|
|
192
|
+
}
|
|
193
|
+
console.error(chalk.red(`\n Minion not found: ${id}\n`));
|
|
194
|
+
process.exit(1);
|
|
195
|
+
});
|
|
196
|
+
// ─── update ────────────────────────────────────────────────
|
|
197
|
+
program
|
|
198
|
+
.command('update <id>')
|
|
199
|
+
.description('Update fields on an existing Minion')
|
|
200
|
+
.option('-d, --data <json>', 'Fields to update as JSON')
|
|
201
|
+
.option('-s, --status <status>', 'Shortcut: update status')
|
|
202
|
+
.option('-p, --priority <priority>', 'Shortcut: update priority')
|
|
203
|
+
.action((id, opts) => {
|
|
204
|
+
ensureStore();
|
|
205
|
+
for (const type of customTypes) {
|
|
206
|
+
const filePath = join(STORE_DIR, type.slug, `${id}.json`);
|
|
207
|
+
if (existsSync(filePath)) {
|
|
208
|
+
const minion = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
209
|
+
let updates = {};
|
|
210
|
+
if (opts.data)
|
|
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
|
+
}
|
|
226
|
+
}
|
|
227
|
+
console.error(chalk.red(`\n Minion not found: ${id}\n`));
|
|
228
|
+
process.exit(1);
|
|
229
|
+
});
|
|
230
|
+
// ─── delete ────────────────────────────────────────────────
|
|
231
|
+
program
|
|
232
|
+
.command('delete <id>')
|
|
233
|
+
.description('Delete a Minion by ID (sets status to cancelled if possible)')
|
|
234
|
+
.option('--hard', 'Permanently delete the file')
|
|
235
|
+
.action((id, opts) => {
|
|
236
|
+
ensureStore();
|
|
237
|
+
const { unlinkSync } = require('fs');
|
|
238
|
+
for (const type of customTypes) {
|
|
239
|
+
const filePath = join(STORE_DIR, type.slug, `${id}.json`);
|
|
240
|
+
if (existsSync(filePath)) {
|
|
241
|
+
if (opts.hard) {
|
|
242
|
+
unlinkSync(filePath);
|
|
243
|
+
console.log(chalk.yellow(`\n 🗑 Permanently deleted ${id}\n`));
|
|
244
|
+
}
|
|
245
|
+
else {
|
|
246
|
+
const minion = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
247
|
+
minion.fields.status = 'cancelled';
|
|
248
|
+
minion.updatedAt = new Date().toISOString();
|
|
249
|
+
writeFileSync(filePath, JSON.stringify(minion, null, 2));
|
|
250
|
+
console.log(chalk.yellow(`\n ✔ Cancelled ${type.icon} ${minion.fields?.title || type.name}`));
|
|
251
|
+
console.log(chalk.dim(` Use --hard to permanently delete\n`));
|
|
252
|
+
}
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
console.error(chalk.red(`\n Minion not found: ${id}\n`));
|
|
257
|
+
process.exit(1);
|
|
258
|
+
});
|
|
259
|
+
// ─── validate ──────────────────────────────────────────────
|
|
260
|
+
program
|
|
261
|
+
.command('validate <file>')
|
|
262
|
+
.description('Validate a JSON file against its MinionType schema')
|
|
263
|
+
.action((file) => {
|
|
264
|
+
const data = JSON.parse(readFileSync(file, 'utf-8'));
|
|
265
|
+
const type = customTypes.find(t => t.slug === data.type);
|
|
266
|
+
if (!type) {
|
|
267
|
+
console.error(chalk.red(`\n Unknown type: ${data.type}\n`));
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
const errors = [];
|
|
271
|
+
const schemaFields = type.schema.map(f => f.name);
|
|
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) {
|
|
298
|
+
console.log(chalk.green(`\n ✔ Valid ${type.icon} ${type.name}\n`));
|
|
299
|
+
}
|
|
300
|
+
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}`);
|
|
304
|
+
}
|
|
305
|
+
console.log('');
|
|
306
|
+
process.exit(1);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
// ─── stats ─────────────────────────────────────────────────
|
|
310
|
+
program
|
|
311
|
+
.command('stats')
|
|
312
|
+
.description('Show statistics about stored Minions')
|
|
313
|
+
.action(() => {
|
|
314
|
+
ensureStore();
|
|
315
|
+
console.log(chalk.bold('\n Minion Statistics:\n'));
|
|
316
|
+
let total = 0;
|
|
317
|
+
for (const type of customTypes) {
|
|
318
|
+
const dir = join(STORE_DIR, type.slug);
|
|
319
|
+
if (!existsSync(dir)) {
|
|
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;
|
|
324
|
+
total += count;
|
|
325
|
+
const bar = chalk.cyan('█'.repeat(Math.min(count, 30)));
|
|
326
|
+
console.log(` ${type.icon} ${type.name.padEnd(22)} ${String(count).padStart(4)} ${bar}`);
|
|
327
|
+
}
|
|
328
|
+
console.log(`\n ${chalk.bold('Total:')} ${total} Minion(s)`);
|
|
329
|
+
console.log(` ${chalk.dim(`Store: ${STORE_DIR}`)}\n`);
|
|
330
|
+
});
|
|
331
|
+
// ─── blocked ───────────────────────────────────────────────
|
|
332
|
+
program
|
|
333
|
+
.command('blocked')
|
|
334
|
+
.description('List all tasks with status "blocked" and their dependencies')
|
|
335
|
+
.action(() => {
|
|
336
|
+
ensureStore();
|
|
337
|
+
const taskDir = join(STORE_DIR, 'task');
|
|
338
|
+
const depDir = join(STORE_DIR, 'task-dependency');
|
|
339
|
+
if (!existsSync(taskDir)) {
|
|
340
|
+
console.log(chalk.dim('\n No tasks found.\n'));
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
const tasks = readdirSync(taskDir).filter(f => f.endsWith('.json'))
|
|
344
|
+
.map(f => JSON.parse(readFileSync(join(taskDir, f), 'utf-8')))
|
|
345
|
+
.filter(t => t.fields?.status === 'blocked');
|
|
346
|
+
if (tasks.length === 0) {
|
|
347
|
+
console.log(chalk.green('\n ✔ No blocked tasks.\n'));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const deps = existsSync(depDir)
|
|
351
|
+
? readdirSync(depDir).filter(f => f.endsWith('.json')).map(f => JSON.parse(readFileSync(join(depDir, f), 'utf-8')))
|
|
352
|
+
: [];
|
|
353
|
+
console.log(chalk.bold(`\n 🚫 ${tasks.length} blocked task(s):\n`));
|
|
354
|
+
for (const t of tasks) {
|
|
355
|
+
console.log(` ✅ ${chalk.bold(t.fields.title || '(untitled)')}`);
|
|
356
|
+
console.log(` ${chalk.dim(t.id)}`);
|
|
357
|
+
const taskDeps = deps.filter(d => d.fields?.taskId === t.id);
|
|
358
|
+
if (taskDeps.length > 0) {
|
|
359
|
+
for (const d of taskDeps) {
|
|
360
|
+
console.log(` ${chalk.red('→')} blocked by ${chalk.dim(d.fields.dependsOnTaskId)} (${d.fields.type})`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
console.log(` ${chalk.dim('No dependency records found')}`);
|
|
365
|
+
}
|
|
366
|
+
console.log('');
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
// ─── search ────────────────────────────────────────────────
|
|
370
|
+
program
|
|
371
|
+
.command('search <query>')
|
|
372
|
+
.description('Search Minions by title, name, or label')
|
|
373
|
+
.option('-t, --type <type>', 'Filter by MinionType slug')
|
|
374
|
+
.option('--status <status>', 'Filter by status')
|
|
375
|
+
.option('--json', 'Output as JSON')
|
|
376
|
+
.action((query, opts) => {
|
|
377
|
+
ensureStore();
|
|
378
|
+
const slugs = opts.type ? [opts.type] : customTypes.map(t => t.slug);
|
|
379
|
+
const results = [];
|
|
380
|
+
const q = query.toLowerCase();
|
|
381
|
+
for (const slug of slugs) {
|
|
382
|
+
const dir = join(STORE_DIR, slug);
|
|
383
|
+
if (!existsSync(dir))
|
|
384
|
+
continue;
|
|
385
|
+
const files = readdirSync(dir).filter(f => f.endsWith('.json'));
|
|
386
|
+
for (const file of files) {
|
|
387
|
+
const minion = JSON.parse(readFileSync(join(dir, file), 'utf-8'));
|
|
388
|
+
const title = (minion.fields?.title || minion.fields?.name || minion.fields?.label || '').toLowerCase();
|
|
389
|
+
const desc = (minion.fields?.description || minion.fields?.body || '').toLowerCase();
|
|
390
|
+
if (title.includes(q) || desc.includes(q)) {
|
|
391
|
+
if (opts.status && minion.fields?.status !== opts.status)
|
|
392
|
+
continue;
|
|
393
|
+
results.push(minion);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (opts.json) {
|
|
398
|
+
console.log(JSON.stringify(results, null, 2));
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
if (results.length === 0) {
|
|
402
|
+
console.log(chalk.dim(`\n No results for "${query}".\n`));
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
console.log(chalk.bold(`\n ${results.length} result(s) for "${query}":\n`));
|
|
406
|
+
for (const m of results) {
|
|
407
|
+
const type = customTypes.find(t => t.slug === m.type);
|
|
408
|
+
const icon = type?.icon || '?';
|
|
409
|
+
const title = m.fields?.title || m.fields?.name || m.fields?.label || chalk.dim('(untitled)');
|
|
410
|
+
const status = m.fields?.status ? chalk.dim(`[${m.fields.status}]`) : '';
|
|
411
|
+
console.log(` ${icon} ${chalk.bold(title)} ${status}`);
|
|
412
|
+
console.log(` ${chalk.dim(m.id)} ${chalk.dim(m.type)}`);
|
|
413
|
+
}
|
|
414
|
+
console.log('');
|
|
415
|
+
});
|
|
416
|
+
// ─── complete ──────────────────────────────────────────────
|
|
417
|
+
program
|
|
418
|
+
.command('complete <id>')
|
|
419
|
+
.description('Mark a task as done and create a task-outcome')
|
|
420
|
+
.option('-r, --result <result>', 'Outcome result: success, partial, or failed', 'success')
|
|
421
|
+
.option('--summary <summary>', 'Summary of what was accomplished')
|
|
422
|
+
.option('--lessons <lessons>', 'Lessons learned for the agent learning loop')
|
|
423
|
+
.action((id, opts) => {
|
|
424
|
+
ensureStore();
|
|
425
|
+
const filePath = join(STORE_DIR, 'task', `${id}.json`);
|
|
426
|
+
if (!existsSync(filePath)) {
|
|
427
|
+
console.error(chalk.red(`\n Task not found: ${id}\n`));
|
|
428
|
+
process.exit(1);
|
|
429
|
+
}
|
|
430
|
+
const task = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
431
|
+
task.fields.status = 'done';
|
|
432
|
+
task.fields.completedAt = new Date().toISOString();
|
|
433
|
+
task.updatedAt = new Date().toISOString();
|
|
434
|
+
writeFileSync(filePath, JSON.stringify(task, null, 2));
|
|
435
|
+
// Create task-outcome
|
|
436
|
+
const outcome = {
|
|
437
|
+
id: randomUUID(),
|
|
438
|
+
type: 'task-outcome',
|
|
439
|
+
typeName: 'Task outcome',
|
|
440
|
+
fields: {
|
|
441
|
+
taskId: id,
|
|
442
|
+
result: opts.result,
|
|
443
|
+
summary: opts.summary || '',
|
|
444
|
+
artifactIds: '',
|
|
445
|
+
lessons: opts.lessons || '',
|
|
446
|
+
},
|
|
447
|
+
createdAt: new Date().toISOString(),
|
|
448
|
+
updatedAt: new Date().toISOString(),
|
|
449
|
+
};
|
|
450
|
+
const outcomeDir = getTypeDir('task-outcome');
|
|
451
|
+
writeFileSync(join(outcomeDir, `${outcome.id}.json`), JSON.stringify(outcome, null, 2));
|
|
452
|
+
console.log(chalk.green(`\n ✔ Completed ✅ ${task.fields.title || 'Task'}`));
|
|
453
|
+
console.log(` ${chalk.dim('Result:')} ${opts.result}`);
|
|
454
|
+
console.log(` ${chalk.dim('Outcome:')} ${outcome.id}\n`);
|
|
455
|
+
});
|
|
456
|
+
// ─── assign ────────────────────────────────────────────────
|
|
457
|
+
program
|
|
458
|
+
.command('assign <taskId> <assigneeId>')
|
|
459
|
+
.description('Assign a task to a person or agent')
|
|
460
|
+
.option('--type <type>', 'Assignee type: human or agent', 'agent')
|
|
461
|
+
.option('--role <role>', 'Role: owner, collaborator, reviewer, observer', 'owner')
|
|
462
|
+
.action((taskId, assigneeId, opts) => {
|
|
463
|
+
ensureStore();
|
|
464
|
+
const taskPath = join(STORE_DIR, 'task', `${taskId}.json`);
|
|
465
|
+
if (!existsSync(taskPath)) {
|
|
466
|
+
console.error(chalk.red(`\n Task not found: ${taskId}\n`));
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
const assignment = {
|
|
470
|
+
id: randomUUID(),
|
|
471
|
+
type: 'task-assignment',
|
|
472
|
+
typeName: 'Task assignment',
|
|
473
|
+
fields: {
|
|
474
|
+
taskId,
|
|
475
|
+
assigneeId,
|
|
476
|
+
assigneeType: opts.type,
|
|
477
|
+
assignedAt: new Date().toISOString(),
|
|
478
|
+
assignedBy: 'cli',
|
|
479
|
+
role: opts.role,
|
|
480
|
+
},
|
|
481
|
+
createdAt: new Date().toISOString(),
|
|
482
|
+
updatedAt: new Date().toISOString(),
|
|
483
|
+
};
|
|
484
|
+
const dir = getTypeDir('task-assignment');
|
|
485
|
+
writeFileSync(join(dir, `${assignment.id}.json`), JSON.stringify(assignment, null, 2));
|
|
486
|
+
console.log(chalk.green(`\n ✔ Assigned task to ${assigneeId}`));
|
|
487
|
+
console.log(` ${chalk.dim('Task:')} ${taskId}`);
|
|
488
|
+
console.log(` ${chalk.dim('Type:')} ${opts.type}`);
|
|
489
|
+
console.log(` ${chalk.dim('Role:')} ${opts.role}`);
|
|
490
|
+
console.log(` ${chalk.dim('Assignment:')} ${assignment.id}\n`);
|
|
491
|
+
});
|
|
492
|
+
// ─── comment ───────────────────────────────────────────────
|
|
493
|
+
program
|
|
494
|
+
.command('comment <taskId> <body>')
|
|
495
|
+
.description('Add a comment to a task')
|
|
496
|
+
.option('--author <id>', 'Author ID', 'cli')
|
|
497
|
+
.option('--type <type>', 'Author type: human or agent', 'agent')
|
|
498
|
+
.action((taskId, body, opts) => {
|
|
499
|
+
ensureStore();
|
|
500
|
+
const comment = {
|
|
501
|
+
id: randomUUID(),
|
|
502
|
+
type: 'task-comment',
|
|
503
|
+
typeName: 'Task comment',
|
|
504
|
+
fields: {
|
|
505
|
+
taskId,
|
|
506
|
+
authorId: opts.author,
|
|
507
|
+
authorType: opts.type,
|
|
508
|
+
body,
|
|
509
|
+
createdAt: new Date().toISOString(),
|
|
510
|
+
resolvedAt: '',
|
|
511
|
+
},
|
|
512
|
+
createdAt: new Date().toISOString(),
|
|
513
|
+
updatedAt: new Date().toISOString(),
|
|
514
|
+
};
|
|
515
|
+
const dir = getTypeDir('task-comment');
|
|
516
|
+
writeFileSync(join(dir, `${comment.id}.json`), JSON.stringify(comment, null, 2));
|
|
517
|
+
console.log(chalk.green(`\n ✔ Comment added to task ${taskId}`));
|
|
518
|
+
console.log(` ${chalk.dim('ID:')} ${comment.id}\n`);
|
|
22
519
|
});
|
|
23
520
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minions-tasks/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "CLI for minions-tasks — Task and work management across agents, humans, and workflows",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"commander": "^14.0.3",
|
|
29
29
|
"inquirer": "^13.2.5",
|
|
30
30
|
"minions-sdk": "^0.2.2",
|
|
31
|
-
"@minions-tasks/sdk": "0.
|
|
31
|
+
"@minions-tasks/sdk": "0.4.0"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
34
|
"@types/node": "^25.3.0",
|