@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 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.3.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,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
- 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);
192
173
  }
193
- console.error(chalk.red(`\n Minion not found: ${id}\n`));
194
- process.exit(1);
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>', '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);
226
206
  }
227
- console.error(chalk.red(`\n Minion not found: ${id}\n`));
228
- process.exit(1);
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('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;
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
- console.error(chalk.red(`\n Minion not found: ${id}\n`));
257
- process.exit(1);
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 = customTypes.find(t => t.slug === data.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.type}\n`));
420
+ console.error(chalk.red(`\n Unknown type: ${data.minionTypeId}\n`));
268
421
  process.exit(1);
269
422
  }
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) {
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
- ensureStore();
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 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;
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.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.3.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",