@minions-tasks/cli 0.4.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 +259 -325
- 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,232 +164,98 @@ 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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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);
|
|
173
|
+
}
|
|
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}`);
|
|
192
188
|
}
|
|
193
|
-
console.
|
|
194
|
-
process.exit(1);
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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);
|
|
206
|
+
}
|
|
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);
|
|
226
222
|
}
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
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`));
|
|
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`));
|
|
268
238
|
process.exit(1);
|
|
269
239
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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`));
|
|
240
|
+
if (opts.hard) {
|
|
241
|
+
await storage.delete(id);
|
|
242
|
+
console.log(chalk.yellow(`\n 🗑 Permanently deleted ${id}\n`));
|
|
299
243
|
}
|
|
300
244
|
else {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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('');
|
|
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`));
|
|
367
249
|
}
|
|
368
250
|
});
|
|
369
251
|
// ─── search ────────────────────────────────────────────────
|
|
370
252
|
program
|
|
371
253
|
.command('search <query>')
|
|
372
|
-
.description('
|
|
373
|
-
.option('-t, --type <type>', 'Filter by MinionType slug')
|
|
374
|
-
.option('--status <status>', 'Filter by status')
|
|
254
|
+
.description('Full-text search across all Minions')
|
|
375
255
|
.option('--json', 'Output as JSON')
|
|
376
|
-
.action((query, opts) => {
|
|
377
|
-
|
|
378
|
-
const
|
|
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
|
-
}
|
|
256
|
+
.action(async (query, opts) => {
|
|
257
|
+
const storage = await getStorage();
|
|
258
|
+
const results = await storage.search(query);
|
|
397
259
|
if (opts.json) {
|
|
398
260
|
console.log(JSON.stringify(results, null, 2));
|
|
399
261
|
return;
|
|
@@ -404,39 +266,76 @@ program
|
|
|
404
266
|
}
|
|
405
267
|
console.log(chalk.bold(`\n ${results.length} result(s) for "${query}":\n`));
|
|
406
268
|
for (const m of results) {
|
|
407
|
-
const type =
|
|
269
|
+
const type = registry.getById(m.minionTypeId) || registry.getBySlug(m.minionTypeId);
|
|
408
270
|
const icon = type?.icon || '?';
|
|
409
|
-
const
|
|
410
|
-
|
|
411
|
-
console.log(`
|
|
412
|
-
console.log(` ${chalk.dim(m.id)} ${chalk.dim(m.type)}`);
|
|
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)}`);
|
|
413
274
|
}
|
|
414
275
|
console.log('');
|
|
415
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})`);
|
|
305
|
+
}
|
|
306
|
+
console.log('');
|
|
307
|
+
}
|
|
308
|
+
});
|
|
416
309
|
// ─── complete ──────────────────────────────────────────────
|
|
417
310
|
program
|
|
418
311
|
.command('complete <id>')
|
|
419
312
|
.description('Mark a task as done and create a task-outcome')
|
|
420
|
-
.option('-r, --result <result>', 'Outcome
|
|
421
|
-
.option('--summary <summary>', '
|
|
422
|
-
.option('--lessons <lessons>', 'Lessons
|
|
423
|
-
.action((id, opts) => {
|
|
424
|
-
|
|
425
|
-
const
|
|
426
|
-
if (!
|
|
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) {
|
|
427
320
|
console.error(chalk.red(`\n Task not found: ${id}\n`));
|
|
428
321
|
process.exit(1);
|
|
429
322
|
}
|
|
430
|
-
|
|
431
|
-
task.
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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);
|
|
328
|
+
}
|
|
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);
|
|
435
335
|
// Create task-outcome
|
|
436
|
-
const
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
typeName: 'Task outcome',
|
|
336
|
+
const outcomeType = findType('task-outcome');
|
|
337
|
+
const { minion: outcome } = createMinion({
|
|
338
|
+
title: `Outcome: ${task.title}`,
|
|
440
339
|
fields: {
|
|
441
340
|
taskId: id,
|
|
442
341
|
result: opts.result,
|
|
@@ -444,12 +343,10 @@ program
|
|
|
444
343
|
artifactIds: '',
|
|
445
344
|
lessons: opts.lessons || '',
|
|
446
345
|
},
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
writeFileSync(join(outcomeDir, `${outcome.id}.json`), JSON.stringify(outcome, null, 2));
|
|
452
|
-
console.log(chalk.green(`\n ✔ Completed ✅ ${task.fields.title || 'Task'}`));
|
|
346
|
+
createdBy: 'cli',
|
|
347
|
+
}, outcomeType);
|
|
348
|
+
await storage.set(outcome);
|
|
349
|
+
console.log(chalk.green(`\n ✔ Completed ✅ ${task.title}`));
|
|
453
350
|
console.log(` ${chalk.dim('Result:')} ${opts.result}`);
|
|
454
351
|
console.log(` ${chalk.dim('Outcome:')} ${outcome.id}\n`);
|
|
455
352
|
});
|
|
@@ -459,17 +356,16 @@ program
|
|
|
459
356
|
.description('Assign a task to a person or agent')
|
|
460
357
|
.option('--type <type>', 'Assignee type: human or agent', 'agent')
|
|
461
358
|
.option('--role <role>', 'Role: owner, collaborator, reviewer, observer', 'owner')
|
|
462
|
-
.action((taskId, assigneeId, opts) => {
|
|
463
|
-
|
|
464
|
-
const
|
|
465
|
-
if (!
|
|
359
|
+
.action(async (taskId, assigneeId, opts) => {
|
|
360
|
+
const storage = await getStorage();
|
|
361
|
+
const task = await storage.get(taskId);
|
|
362
|
+
if (!task) {
|
|
466
363
|
console.error(chalk.red(`\n Task not found: ${taskId}\n`));
|
|
467
364
|
process.exit(1);
|
|
468
365
|
}
|
|
469
|
-
const
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
typeName: 'Task assignment',
|
|
366
|
+
const assignType = findType('task-assignment');
|
|
367
|
+
const { minion: assignment } = createMinion({
|
|
368
|
+
title: `Assignment: ${task.title} → ${assigneeId}`,
|
|
473
369
|
fields: {
|
|
474
370
|
taskId,
|
|
475
371
|
assigneeId,
|
|
@@ -478,16 +374,13 @@ program
|
|
|
478
374
|
assignedBy: 'cli',
|
|
479
375
|
role: opts.role,
|
|
480
376
|
},
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const dir = getTypeDir('task-assignment');
|
|
485
|
-
writeFileSync(join(dir, `${assignment.id}.json`), JSON.stringify(assignment, null, 2));
|
|
377
|
+
createdBy: 'cli',
|
|
378
|
+
}, assignType);
|
|
379
|
+
await storage.set(assignment);
|
|
486
380
|
console.log(chalk.green(`\n ✔ Assigned task to ${assigneeId}`));
|
|
487
|
-
console.log(` ${chalk.dim('Task:')}
|
|
488
|
-
console.log(` ${chalk.dim('
|
|
489
|
-
console.log(` ${chalk.dim('
|
|
490
|
-
console.log(` ${chalk.dim('Assignment:')} ${assignment.id}\n`);
|
|
381
|
+
console.log(` ${chalk.dim('Task:')} ${taskId}`);
|
|
382
|
+
console.log(` ${chalk.dim('Role:')} ${opts.role}`);
|
|
383
|
+
console.log(` ${chalk.dim('ID:')} ${assignment.id}\n`);
|
|
491
384
|
});
|
|
492
385
|
// ─── comment ───────────────────────────────────────────────
|
|
493
386
|
program
|
|
@@ -495,12 +388,11 @@ program
|
|
|
495
388
|
.description('Add a comment to a task')
|
|
496
389
|
.option('--author <id>', 'Author ID', 'cli')
|
|
497
390
|
.option('--type <type>', 'Author type: human or agent', 'agent')
|
|
498
|
-
.action((taskId, body, opts) => {
|
|
499
|
-
|
|
500
|
-
const
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
typeName: 'Task comment',
|
|
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}`,
|
|
504
396
|
fields: {
|
|
505
397
|
taskId,
|
|
506
398
|
authorId: opts.author,
|
|
@@ -509,12 +401,54 @@ program
|
|
|
509
401
|
createdAt: new Date().toISOString(),
|
|
510
402
|
resolvedAt: '',
|
|
511
403
|
},
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
const dir = getTypeDir('task-comment');
|
|
516
|
-
writeFileSync(join(dir, `${comment.id}.json`), JSON.stringify(comment, null, 2));
|
|
404
|
+
createdBy: opts.author,
|
|
405
|
+
}, commentType);
|
|
406
|
+
await storage.set(comment);
|
|
517
407
|
console.log(chalk.green(`\n ✔ Comment added to task ${taskId}`));
|
|
518
408
|
console.log(` ${chalk.dim('ID:')} ${comment.id}\n`);
|
|
519
409
|
});
|
|
410
|
+
// ─── validate ──────────────────────────────────────────────
|
|
411
|
+
program
|
|
412
|
+
.command('validate <file>')
|
|
413
|
+
.description('Validate a JSON file against its MinionType schema')
|
|
414
|
+
.action(async (file) => {
|
|
415
|
+
const { readFileSync } = await import('fs');
|
|
416
|
+
const { validateFields } = await import('minions-sdk');
|
|
417
|
+
const data = JSON.parse(readFileSync(file, 'utf-8'));
|
|
418
|
+
const type = registry.getById(data.minionTypeId) || registry.getBySlug(data.minionTypeId);
|
|
419
|
+
if (!type) {
|
|
420
|
+
console.error(chalk.red(`\n Unknown type: ${data.minionTypeId}\n`));
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
const result = validateFields(data.fields, type.schema);
|
|
424
|
+
if (result.valid) {
|
|
425
|
+
console.log(chalk.green(`\n ✔ Valid ${type.icon} ${type.name}\n`));
|
|
426
|
+
}
|
|
427
|
+
else {
|
|
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}`);
|
|
431
|
+
}
|
|
432
|
+
console.log('');
|
|
433
|
+
process.exit(1);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
// ─── stats ─────────────────────────────────────────────────
|
|
437
|
+
program
|
|
438
|
+
.command('stats')
|
|
439
|
+
.description('Show statistics about stored Minions')
|
|
440
|
+
.action(async () => {
|
|
441
|
+
const storage = await getStorage();
|
|
442
|
+
console.log(chalk.bold('\n Minion Statistics:\n'));
|
|
443
|
+
let total = 0;
|
|
444
|
+
for (const type of customTypes) {
|
|
445
|
+
const minions = await storage.list({ minionTypeId: type.id });
|
|
446
|
+
const count = minions.length;
|
|
447
|
+
total += count;
|
|
448
|
+
const bar = chalk.cyan('█'.repeat(Math.min(count, 30)));
|
|
449
|
+
console.log(` ${type.icon} ${(type.name || '').padEnd(22)} ${String(count).padStart(4)} ${count > 0 ? bar : chalk.dim('0')}`);
|
|
450
|
+
}
|
|
451
|
+
console.log(`\n ${chalk.bold('Total:')} ${total} Minion(s)`);
|
|
452
|
+
console.log(` ${chalk.dim(`Store: ${STORE_DIR}`)}\n`);
|
|
453
|
+
});
|
|
520
454
|
program.parse();
|
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",
|