@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 CHANGED
@@ -1,5 +1,8 @@
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
  export {};
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 { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync } from 'fs';
8
- import { join, resolve } from 'path';
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 = resolve(process.env.MINIONS_STORE || '.minions');
13
- function ensureStore() {
14
- if (!existsSync(STORE_DIR)) {
15
- mkdirSync(STORE_DIR, { recursive: true });
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
- function getTypeDir(slug) {
19
- const dir = join(STORE_DIR, slug);
20
- if (!existsSync(dir)) {
21
- mkdirSync(dir, { recursive: true });
19
+ let _storage = null;
20
+ async function getStorage() {
21
+ if (!_storage) {
22
+ _storage = await JsonFileStorageAdapter.create(STORE_DIR);
22
23
  }
23
- return dir;
24
+ return _storage;
24
25
  }
25
26
  function findType(slug) {
26
- const type = customTypes.find(t => t.slug === slug);
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 types: ${customTypes.map(t => t.slug).join(', ')}`));
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.4.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
- console.log(` ${chalk.dim('')} ${chalk.bold(field.name)} ${chalk[typeColor](field.type)}`);
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>', '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) => {
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
- ensureStore();
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
- // 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,
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
- 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));
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('File:')} ${filePath}`);
123
- if (fields.title)
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
- .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
- }
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(allMinions, null, 2));
145
+ console.log(JSON.stringify(minions, null, 2));
149
146
  return;
150
147
  }
151
- if (allMinions.length === 0) {
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 ${allMinions.length} Minion(s):\n`));
156
- for (const m of allMinions) {
157
- const type = customTypes.find(t => t.slug === m.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 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)}`);
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
- 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
- }
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.error(chalk.red(`\n Minion not found: ${id}\n`));
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>', '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
- }
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
- console.error(chalk.red(`\n Minion not found: ${id}\n`));
228
- process.exit(1);
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('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`));
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
- 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`));
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
- 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('');
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('Search Minions by title, name, or label')
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
- 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
- }
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 = customTypes.find(t => t.slug === m.type);
269
+ const type = registry.getById(m.minionTypeId) || registry.getBySlug(m.minionTypeId);
408
270
  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)}`);
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 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)) {
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
- 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));
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 outcome = {
437
- id: randomUUID(),
438
- type: 'task-outcome',
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
- 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'}`));
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
- ensureStore();
464
- const taskPath = join(STORE_DIR, 'task', `${taskId}.json`);
465
- if (!existsSync(taskPath)) {
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 assignment = {
470
- id: randomUUID(),
471
- type: 'task-assignment',
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
- 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));
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:')} ${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`);
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
- ensureStore();
500
- const comment = {
501
- id: randomUUID(),
502
- type: 'task-comment',
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
- 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));
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.4.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.2",
31
- "@minions-tasks/sdk": "0.4.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",