keystone-cli 0.5.1 โ†’ 0.6.1

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.
Files changed (48) hide show
  1. package/README.md +55 -8
  2. package/package.json +8 -17
  3. package/src/cli.ts +219 -166
  4. package/src/db/memory-db.test.ts +54 -0
  5. package/src/db/memory-db.ts +128 -0
  6. package/src/db/sqlite-setup.test.ts +47 -0
  7. package/src/db/sqlite-setup.ts +49 -0
  8. package/src/db/workflow-db.test.ts +41 -10
  9. package/src/db/workflow-db.ts +90 -28
  10. package/src/expression/evaluator.test.ts +19 -0
  11. package/src/expression/evaluator.ts +134 -39
  12. package/src/parser/schema.ts +41 -0
  13. package/src/runner/audit-verification.test.ts +23 -0
  14. package/src/runner/auto-heal.test.ts +64 -0
  15. package/src/runner/debug-repl.test.ts +308 -0
  16. package/src/runner/debug-repl.ts +225 -0
  17. package/src/runner/foreach-executor.ts +327 -0
  18. package/src/runner/llm-adapter.test.ts +37 -18
  19. package/src/runner/llm-adapter.ts +90 -112
  20. package/src/runner/llm-executor.test.ts +47 -6
  21. package/src/runner/llm-executor.ts +18 -3
  22. package/src/runner/mcp-client.audit.test.ts +69 -0
  23. package/src/runner/mcp-client.test.ts +12 -3
  24. package/src/runner/mcp-client.ts +199 -19
  25. package/src/runner/mcp-manager.ts +19 -8
  26. package/src/runner/mcp-server.test.ts +8 -5
  27. package/src/runner/mcp-server.ts +31 -17
  28. package/src/runner/optimization-runner.ts +305 -0
  29. package/src/runner/reflexion.test.ts +87 -0
  30. package/src/runner/shell-executor.test.ts +12 -0
  31. package/src/runner/shell-executor.ts +9 -6
  32. package/src/runner/step-executor.test.ts +240 -2
  33. package/src/runner/step-executor.ts +183 -68
  34. package/src/runner/stream-utils.test.ts +171 -0
  35. package/src/runner/stream-utils.ts +186 -0
  36. package/src/runner/workflow-runner.test.ts +4 -4
  37. package/src/runner/workflow-runner.ts +438 -259
  38. package/src/templates/agents/keystone-architect.md +6 -4
  39. package/src/templates/full-feature-demo.yaml +4 -4
  40. package/src/types/assets.d.ts +14 -0
  41. package/src/types/status.ts +1 -1
  42. package/src/ui/dashboard.tsx +38 -26
  43. package/src/utils/auth-manager.ts +3 -1
  44. package/src/utils/logger.test.ts +76 -0
  45. package/src/utils/logger.ts +39 -0
  46. package/src/utils/prompt.ts +75 -0
  47. package/src/utils/redactor.test.ts +86 -4
  48. package/src/utils/redactor.ts +48 -13
package/README.md CHANGED
@@ -25,6 +25,8 @@ Keystone allows you to define complex automation workflows using a simple YAML s
25
25
  - ๐Ÿ› ๏ธ **Extensible:** Support for shell, file, HTTP request, LLM, and sub-workflow steps.
26
26
  - ๐Ÿ”Œ **MCP Support:** Integrated Model Context Protocol server.
27
27
  - ๐Ÿ›ก๏ธ **Secret Redaction:** Automatically redacts environment variables and secrets from logs and outputs.
28
+ - ๐Ÿง  **Semantic Memory:** Store and retrieve step outputs using vector embeddings/RAG.
29
+ - ๐ŸŽฏ **Prompt Optimization:** Automatically optimize prompts using iterative evaluation (DSPy-style).
28
30
 
29
31
  ---
30
32
 
@@ -281,10 +283,26 @@ Keystone supports several specialized step types:
281
283
  - `script`: Run arbitrary JavaScript in a sandbox. On Bun, uses `node:vm` (since `isolated-vm` requires V8).
282
284
  - โš ๏ธ **Security Note:** The `node:vm` sandbox is not secure against malicious code. Only run scripts from trusted sources.
283
285
  - `sleep`: Pause execution for a specified duration.
286
+ - `memory`: Store or retrieve information from the semantic memory vector database.
284
287
 
285
- All steps support common features like `needs` (dependencies), `if` (conditionals), `retry`, `timeout`, `foreach` (parallel iteration), `concurrency` (max parallel items for foreach), and `transform` (post-process output using expressions).
288
+ All steps support common features like `needs` (dependencies), `if` (conditionals), `retry`, `timeout`, `foreach` (parallel iteration), `concurrency` (max parallel items for foreach), `transform` (post-process output using expressions), `learn` (auto-index for few-shot), and `reflexion` (self-correction loop).
286
289
 
287
- Workflows also support a top-level `concurrency` field to limit how many steps can run in parallel across the entire workflow.
290
+ Workflows also support a top-level `concurrency` field to limit how many steps can run in parallel across the entire workflow. This must be a positive integer.
291
+
292
+ ### Self-Healing Steps
293
+ Steps can be configured to automatically recover from failures using an LLM agent.
294
+
295
+ ```yaml
296
+ - id: build
297
+ type: shell
298
+ run: bun build
299
+ auto_heal:
300
+ agent: debugger_agent
301
+ maxAttempts: 3
302
+ model: gpt-4o # Optional override
303
+ ```
304
+
305
+ When a step fails, the specified agent is invoked with the error details. The agent proposes a fix (e.g., a corrected command), and the step is automatically retried.
288
306
 
289
307
  #### Example: Transform & Foreach Concurrency
290
308
  ```yaml
@@ -297,13 +315,14 @@ Workflows also support a top-level `concurrency` field to limit how many steps c
297
315
  - id: process_files
298
316
  type: shell
299
317
  foreach: ${{ steps.list_files.output }}
300
- concurrency: 5 # Process 5 files at a time
318
+ concurrency: 5 # Process 5 files at a time (must be a positive integer)
301
319
  run: echo "Processing ${{ item }}"
302
320
 
303
321
  #### Example: Script Step
304
322
  ```yaml
305
323
  - id: calculate
306
324
  type: script
325
+ allowInsecure: true
307
326
  run: |
308
327
  const data = context.steps.fetch_data.output;
309
328
  return data.map(i => i.value * 2).reduce((a, b) => a + b, 0);
@@ -427,7 +446,8 @@ In these examples, the agent will have access to all tools provided by the MCP s
427
446
  | Command | Description |
428
447
  | :--- | :--- |
429
448
  | `init` | Initialize a new Keystone project |
430
- | `run <workflow>` | Execute a workflow (use `-i key=val` for inputs, `--dry-run` to test) |
449
+ | `run <workflow>` | Execute a workflow (use `-i key=val` for inputs, `--dry-run` to test, `--debug` for REPL) |
450
+ | `optimize <workflow>` | Optimize a specific step in a workflow (requires --target) |
431
451
  | `resume <run_id>` | Resume a failed or paused workflow |
432
452
  | `validate [path]` | Check workflow files for errors |
433
453
  | `workflows` | List available workflows |
@@ -442,11 +462,38 @@ In these examples, the agent will have access to all tools provided by the MCP s
442
462
  | `mcp start` | Start the Keystone MCP server |
443
463
  | `mcp login <server>` | Login to a remote MCP server |
444
464
  | `completion [shell]` | Generate shell completion script (zsh, bash) |
445
- | `prune [--days N]` | Cleanup old run data from the database |
465
+ | `maintenance [--days N]` | Perform database maintenance (prune old runs and vacuum) |
446
466
 
447
467
  ---
448
-
449
- ## ๐Ÿ“‚ Project Structure
468
+
469
+ ## ๐Ÿ›ก๏ธ Security
470
+
471
+ ### Shell Execution
472
+ By default, Keystone analyzes shell commands for potentially dangerous patterns (like shell injection, `rm -rf`, piped commands). If a risk is detected:
473
+ - In interactive mode, the user is prompted for confirmation.
474
+ - In non-interactive mode, the step is suspended or failed.
475
+
476
+ You can bypass this check if you trust the command:
477
+ ```yaml
478
+ - id: deploy
479
+ type: shell
480
+ run: ./deploy.sh ${{ inputs.env }}
481
+ allowInsecure: true
482
+ ```
483
+
484
+ ### Expression Safety
485
+ Expressions `${{ }}` are evaluated using a safe AST parser (`jsep`) which:
486
+ - Prevents arbitrary code execution (no `eval` or `Function`).
487
+ - Whitelists safe global objects (`Math`, `JSON`, `Date`, etc.).
488
+ - Blocks access to sensitive properties (`constructor`, `__proto__`).
489
+ - Enforces a maximum template length to prevent ReDoS attacks.
490
+
491
+ ### Script Sandboxing
492
+ The `script` step uses Node.js `vm` module. While it provides isolation for variables, it is **not a security boundary** for malicious code. Only run scripts from trusted sources.
493
+
494
+ ---
495
+
496
+ ## ๐Ÿ“‚ Project Structure
450
497
 
451
498
  - `src/db/`: SQLite persistence layer.
452
499
  - `src/runner/`: The core execution engine, handles parallelization and retries.
@@ -460,4 +507,4 @@ In these examples, the agent will have access to all tools provided by the MCP s
460
507
 
461
508
  ## ๐Ÿ“„ License
462
509
 
463
- MIT
510
+ MIT
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keystone-cli",
3
- "version": "0.5.1",
3
+ "version": "0.6.1",
4
4
  "description": "A local-first, declarative, agentic workflow orchestrator built on Bun",
5
5
  "type": "module",
6
6
  "bin": {
@@ -13,13 +13,7 @@
13
13
  "lint:fix": "biome check --write .",
14
14
  "format": "biome format --write ."
15
15
  },
16
- "keywords": [
17
- "workflow",
18
- "orchestrator",
19
- "agentic",
20
- "automation",
21
- "bun"
22
- ],
16
+ "keywords": ["workflow", "orchestrator", "agentic", "automation", "bun"],
23
17
  "author": "Mark Hingston",
24
18
  "license": "MIT",
25
19
  "repository": {
@@ -27,16 +21,12 @@
27
21
  "url": "https://github.com/mhingston/keystone-cli.git"
28
22
  },
29
23
  "homepage": "https://github.com/mhingston/keystone-cli#readme",
30
- "files": [
31
- "src",
32
- "README.md",
33
- "LICENSE",
34
- "logo.png"
35
- ],
24
+ "files": ["src", "README.md", "LICENSE", "logo.png"],
36
25
  "dependencies": {
37
26
  "@jsep-plugin/arrow": "^1.0.6",
38
27
  "@jsep-plugin/object": "^1.2.2",
39
- "@types/react": "^19.2.7",
28
+ "@types/react": "^19.0.0",
29
+ "@xenova/transformers": "^2.17.2",
40
30
  "commander": "^12.1.0",
41
31
  "dagre": "^0.8.5",
42
32
  "ink": "^6.5.1",
@@ -44,7 +34,8 @@
44
34
  "ink-spinner": "^5.0.0",
45
35
  "js-yaml": "^4.1.0",
46
36
  "jsep": "^1.4.0",
47
- "react": "^19.2.3",
37
+ "react": "^19.0.0",
38
+ "sqlite-vec": "0.1.6",
48
39
  "zod": "^3.23.8"
49
40
  },
50
41
  "devDependencies": {
@@ -57,4 +48,4 @@
57
48
  "engines": {
58
49
  "bun": ">=1.0.0"
59
50
  }
60
- }
51
+ }
package/src/cli.ts CHANGED
@@ -9,9 +9,10 @@ import architectAgent from './templates/agents/keystone-architect.md' with { typ
9
9
  // Default templates
10
10
  import scaffoldWorkflow from './templates/scaffold-feature.yaml' with { type: 'text' };
11
11
 
12
- import { WorkflowDb } from './db/workflow-db.ts';
12
+ import { WorkflowDb, type WorkflowRun } from './db/workflow-db.ts';
13
13
  import { WorkflowParser } from './parser/workflow-parser.ts';
14
14
  import { ConfigLoader } from './utils/config-loader.ts';
15
+ import { ConsoleLogger } from './utils/logger.ts';
15
16
  import { generateMermaidGraph, renderWorkflowAsAscii } from './utils/mermaid.ts';
16
17
  import { WorkflowRegistry } from './utils/workflow-registry.ts';
17
18
 
@@ -226,6 +227,7 @@ program
226
227
  .argument('<workflow>', 'Workflow name or path to workflow file')
227
228
  .option('-i, --input <key=value...>', 'Input values')
228
229
  .option('--dry-run', 'Show what would be executed without actually running it')
230
+ .option('--debug', 'Enable interactive debug mode on failure')
229
231
  .action(async (workflowPath, options) => {
230
232
  // Parse inputs
231
233
  const inputs: Record<string, unknown> = {};
@@ -250,25 +252,15 @@ program
250
252
  const resolvedPath = WorkflowRegistry.resolvePath(workflowPath);
251
253
  const workflow = WorkflowParser.loadWorkflow(resolvedPath);
252
254
 
253
- // Auto-prune old runs
254
- try {
255
- const config = ConfigLoader.load();
256
- const db = new WorkflowDb();
257
- const deleted = await db.pruneRuns(config.storage.retention_days);
258
- if (deleted > 0) {
259
- await db.vacuum();
260
- }
261
- db.close();
262
- } catch (error) {
263
- // Non-fatal
264
- }
265
-
266
255
  // Import WorkflowRunner dynamically
267
256
  const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
257
+ const logger = new ConsoleLogger();
268
258
  const runner = new WorkflowRunner(workflow, {
269
259
  inputs,
270
260
  workflowDir: dirname(resolvedPath),
271
261
  dryRun: !!options.dryRun,
262
+ debug: !!options.debug,
263
+ logger,
272
264
  });
273
265
 
274
266
  const outputs = await runner.run();
@@ -287,6 +279,80 @@ program
287
279
  }
288
280
  });
289
281
 
282
+ // ===== keystone workflows =====
283
+ program
284
+ .command('workflows')
285
+ .description('List available workflows')
286
+ .action(() => {
287
+ const workflows = WorkflowRegistry.listWorkflows();
288
+ if (workflows.length === 0) {
289
+ console.log('No workflows found. Run "keystone init" to seed default workflows.');
290
+ return;
291
+ }
292
+
293
+ console.log('\n๐Ÿ›๏ธ Available Workflows:');
294
+ for (const w of workflows) {
295
+ console.log(`\n ${w.name}`);
296
+ if (w.description) {
297
+ console.log(` ${w.description}`);
298
+ }
299
+ }
300
+ console.log('');
301
+ });
302
+
303
+ // ===== keystone optimize =====
304
+ program
305
+ .command('optimize')
306
+ .description('Optimize a specific step in a workflow using iterative evaluation')
307
+ .argument('<workflow>', 'Workflow name or path to workflow file')
308
+ .requiredOption('-t, --target <step_id>', 'Target step ID to optimize')
309
+ .option('-n, --iterations <number>', 'Number of optimization iterations', '5')
310
+ .option('-i, --input <key=value...>', 'Input values for evaluation')
311
+ .action(async (workflowPath, options) => {
312
+ try {
313
+ const { OptimizationRunner } = await import('./runner/optimization-runner.ts');
314
+ const resolvedPath = WorkflowRegistry.resolvePath(workflowPath);
315
+ const workflow = WorkflowParser.loadWorkflow(resolvedPath);
316
+
317
+ // Parse inputs
318
+ const inputs: Record<string, unknown> = {};
319
+ if (options.input) {
320
+ for (const pair of options.input) {
321
+ const index = pair.indexOf('=');
322
+ if (index > 0) {
323
+ const key = pair.slice(0, index);
324
+ const value = pair.slice(index + 1);
325
+ try {
326
+ inputs[key] = JSON.parse(value);
327
+ } catch {
328
+ inputs[key] = value;
329
+ }
330
+ }
331
+ }
332
+ }
333
+
334
+ const runner = new OptimizationRunner(workflow, {
335
+ workflowPath: resolvedPath,
336
+ targetStepId: options.target,
337
+ iterations: Number.parseInt(options.iterations, 10),
338
+ inputs,
339
+ });
340
+
341
+ console.log('๐Ÿ›๏ธ Keystone Prompt Optimization');
342
+ const { bestPrompt, bestScore } = await runner.optimize();
343
+
344
+ console.log('\nโœจ Optimization Complete!');
345
+ console.log(`๐Ÿ† Best Score: ${bestScore}/100`);
346
+ console.log('\nBest Prompt/Command:');
347
+ console.log(''.padEnd(80, '-'));
348
+ console.log(bestPrompt);
349
+ console.log(''.padEnd(80, '-'));
350
+ } catch (error) {
351
+ console.error('โœ— Optimization failed:', error instanceof Error ? error.message : error);
352
+ process.exit(1);
353
+ }
354
+ });
355
+
290
356
  // ===== keystone resume =====
291
357
  program
292
358
  .command('resume')
@@ -295,21 +361,10 @@ program
295
361
  .option('-w, --workflow <path>', 'Path to workflow file (auto-detected if not specified)')
296
362
  .action(async (runId, options) => {
297
363
  try {
298
- const config = ConfigLoader.load();
299
364
  const db = new WorkflowDb();
300
365
 
301
- // Auto-prune old runs
302
- try {
303
- const deleted = await db.pruneRuns(config.storage.retention_days);
304
- if (deleted > 0) {
305
- await db.vacuum();
306
- }
307
- } catch (error) {
308
- // Non-fatal
309
- }
310
-
311
366
  // Load run from database to get workflow name
312
- const run = db.getRun(runId);
367
+ const run = await db.getRun(runId);
313
368
 
314
369
  if (!run) {
315
370
  console.error(`โœ— Run not found: ${runId}`);
@@ -344,9 +399,11 @@ program
344
399
 
345
400
  // Import WorkflowRunner dynamically
346
401
  const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
402
+ const logger = new ConsoleLogger();
347
403
  const runner = new WorkflowRunner(workflow, {
348
404
  resumeRunId: runId,
349
405
  workflowDir: dirname(workflowPath),
406
+ logger,
350
407
  });
351
408
 
352
409
  const outputs = await runner.run();
@@ -362,55 +419,44 @@ program
362
419
  }
363
420
  });
364
421
 
365
- // ===== keystone workflows =====
366
- program
367
- .command('workflows')
368
- .description('List available workflows')
369
- .action(() => {
370
- try {
371
- const workflows = WorkflowRegistry.listWorkflows();
372
- if (workflows.length === 0) {
373
- console.log('No workflows found.');
374
- return;
375
- }
376
-
377
- console.log('\nAvailable workflows:\n');
378
- for (const w of workflows) {
379
- const description = w.description ? ` - ${w.description}` : '';
380
- console.log(` ${w.name.padEnd(25)}${description}`);
381
- }
382
- console.log();
383
- } catch (error) {
384
- console.error('โœ— Failed to list workflows:', error instanceof Error ? error.message : error);
385
- process.exit(1);
386
- }
387
- });
388
-
389
422
  // ===== keystone history =====
390
423
  program
391
424
  .command('history')
392
- .description('List recent workflow runs')
393
- .option('-n, --limit <number>', 'Number of runs to show', '20')
394
- .action((options) => {
425
+ .description('Show recent workflow runs')
426
+ .option('-l, --limit <number>', 'Limit the number of runs to show', '50')
427
+ .action(async (options) => {
395
428
  try {
396
429
  const db = new WorkflowDb();
397
- const runs = db.listRuns(Number.parseInt(options.limit));
430
+ const limit = Number.parseInt(options.limit, 10);
431
+ const runs = await db.listRuns(limit);
432
+ db.close();
398
433
 
399
434
  if (runs.length === 0) {
400
435
  console.log('No workflow runs found.');
401
436
  return;
402
437
  }
403
438
 
404
- console.log('\nRecent workflow runs:\n');
439
+ console.log('\n๐Ÿ›๏ธ Workflow Run History:');
440
+ console.log(''.padEnd(100, '-'));
441
+ console.log(
442
+ `${'ID'.padEnd(10)} ${'Workflow'.padEnd(25)} ${'Status'.padEnd(15)} ${'Started At'}`
443
+ );
444
+ console.log(''.padEnd(100, '-'));
445
+
405
446
  for (const run of runs) {
406
- const status = run.status.toUpperCase().padEnd(10);
407
- const date = new Date(run.started_at).toLocaleString();
447
+ const id = run.id.slice(0, 8);
448
+ const status = run.status;
449
+ const color =
450
+ status === 'success' ? '\x1b[32m' : status === 'failed' ? '\x1b[31m' : '\x1b[33m';
451
+ const reset = '\x1b[0m';
452
+
408
453
  console.log(
409
- `${run.id.substring(0, 8)} ${status} ${run.workflow_name.padEnd(20)} ${date}`
454
+ `${id.padEnd(10)} ${run.workflow_name.padEnd(25)} ${color}${status.padEnd(
455
+ 15
456
+ )}${reset} ${new Date(run.started_at).toLocaleString()}`
410
457
  );
411
458
  }
412
-
413
- db.close();
459
+ console.log('');
414
460
  } catch (error) {
415
461
  console.error('โœ— Failed to list runs:', error instanceof Error ? error.message : error);
416
462
  process.exit(1);
@@ -420,86 +466,33 @@ program
420
466
  // ===== keystone logs =====
421
467
  program
422
468
  .command('logs')
423
- .description('Show logs for a workflow run')
424
- .argument('<run_id>', 'Run ID')
425
- .option('-v, --verbose', 'Show full output without truncation')
426
- .action((runId, options) => {
469
+ .description('Show logs for a specific workflow run')
470
+ .argument('<run_id>', 'Run ID to show logs for')
471
+ .option('-v, --verbose', 'Show detailed step outputs')
472
+ .action(async (runId, options) => {
427
473
  try {
428
474
  const db = new WorkflowDb();
429
- const run = db.getRun(runId);
475
+ const run = await db.getRun(runId);
430
476
 
431
477
  if (!run) {
432
- console.error(`โœ— Run not found: ${runId}`);
433
- process.exit(1);
434
- }
435
-
436
- console.log(`\n๐Ÿ“‹ Workflow: ${run.workflow_name}`);
437
- console.log(`Status: ${run.status}`);
438
- console.log(`Started: ${new Date(run.started_at).toLocaleString()}`);
439
- if (run.completed_at) {
440
- console.log(`Completed: ${new Date(run.completed_at).toLocaleString()}`);
441
- }
442
- if (run.error) {
443
- console.log(`\nโŒ Error: ${run.error}`);
444
- }
445
-
446
- const steps = db.getStepsByRun(runId);
447
- if (steps.length > 0) {
448
- console.log('\nSteps:');
449
- for (const step of steps) {
450
- const statusColors: Record<string, string> = {
451
- success: '\x1b[32m', // green
452
- failed: '\x1b[31m', // red
453
- pending: '\x1b[33m', // yellow
454
- skipped: '\x1b[90m', // gray
455
- suspended: '\x1b[35m', // magenta
456
- };
457
- const RESET = '\x1b[0m';
458
- const color = statusColors[step.status] || '';
459
- const status = `${color}${step.status.toUpperCase().padEnd(10)}${RESET}`;
460
- const iteration = step.iteration_index !== null ? ` [${step.iteration_index}]` : '';
461
- console.log(` ${(step.step_id + iteration).padEnd(25)} ${status}`);
462
-
463
- // Show error if present
464
- if (step.error) {
465
- console.log(` โŒ Error: ${step.error}`);
466
- }
467
-
468
- // Show output if present
469
- if (step.output) {
470
- try {
471
- const output = JSON.parse(step.output);
472
- let outputStr = JSON.stringify(output, null, 2);
473
- if (!options.verbose && outputStr.length > 500) {
474
- outputStr = `${outputStr.substring(0, 500)}... (use --verbose for full output)`;
475
- }
476
- // Indent output
477
- const indentedOutput = outputStr
478
- .split('\n')
479
- .map((line: string) => ` ${line}`)
480
- .join('\n');
481
- console.log(` ๐Ÿ“ค Output:\n${indentedOutput}`);
482
- } catch {
483
- console.log(` ๐Ÿ“ค Output: ${step.output.substring(0, 200)}`);
484
- }
485
- }
486
-
487
- // Show usage if present
488
- if (step.usage) {
489
- try {
490
- const usage = JSON.parse(step.usage);
491
- if (usage.total_tokens) {
492
- console.log(
493
- ` ๐Ÿ“Š Tokens: ${usage.total_tokens} (prompt: ${usage.prompt_tokens}, completion: ${usage.completion_tokens})`
494
- );
495
- }
496
- } catch {
497
- // Ignore parse errors
498
- }
478
+ // Try searching by short ID
479
+ const allRuns = await db.listRuns(200);
480
+ const matching = allRuns.find((r) => r.id.startsWith(runId));
481
+ if (matching) {
482
+ const detailedRun = await db.getRun(matching.id);
483
+ if (detailedRun) {
484
+ await showRunLogs(detailedRun, db, !!options.verbose);
485
+ db.close();
486
+ return;
499
487
  }
500
488
  }
489
+
490
+ console.error(`โœ— Run not found: ${runId}`);
491
+ db.close();
492
+ process.exit(1);
501
493
  }
502
494
 
495
+ await showRunLogs(run, db, !!options.verbose);
503
496
  db.close();
504
497
  } catch (error) {
505
498
  console.error('โœ— Failed to show logs:', error instanceof Error ? error.message : error);
@@ -507,31 +500,97 @@ program
507
500
  }
508
501
  });
509
502
 
510
- // ===== keystone prune =====
511
- program
512
- .command('prune')
513
- .description('Delete old workflow runs from the database')
514
- .option('--days <days>', 'Delete runs older than this many days', '7')
515
- .action(async (options) => {
516
- try {
517
- const days = Number.parseInt(options.days, 10);
518
- if (Number.isNaN(days) || days < 0) {
519
- console.error('โœ— Invalid days value. Must be a positive number.');
520
- process.exit(1);
521
- }
503
+ async function showRunLogs(run: WorkflowRun, db: WorkflowDb, verbose: boolean) {
504
+ console.log(`\n๐Ÿ›๏ธ Run: ${run.workflow_name} (${run.id})`);
505
+ console.log(` Status: ${run.status}`);
506
+ console.log(` Started: ${new Date(run.started_at).toLocaleString()}`);
507
+ if (run.completed_at) {
508
+ console.log(` Completed: ${new Date(run.completed_at).toLocaleString()}`);
509
+ }
522
510
 
523
- const db = new WorkflowDb();
524
- const deleted = await db.pruneRuns(days);
525
- if (deleted > 0) {
526
- await db.vacuum();
511
+ const steps = await db.getStepsByRun(run.id);
512
+ console.log(`\nSteps (${steps.length}):`);
513
+ console.log(''.padEnd(100, '-'));
514
+
515
+ for (const step of steps) {
516
+ const statusColor =
517
+ step.status === 'success' ? '\x1b[32m' : step.status === 'failed' ? '\x1b[31m' : '\x1b[33m';
518
+ const reset = '\x1b[0m';
519
+
520
+ let label = step.step_id;
521
+ if (step.iteration_index !== null) {
522
+ label += ` [${step.iteration_index}]`;
523
+ }
524
+
525
+ console.log(`${statusColor}${step.status.toUpperCase().padEnd(10)}${reset} ${label}`);
526
+
527
+ if (step.error) {
528
+ console.log(` \x1b[31mError: ${step.error}\x1b[0m`);
529
+ }
530
+
531
+ if (verbose && step.output) {
532
+ try {
533
+ const output = JSON.parse(step.output);
534
+ console.log(
535
+ ` Output: ${JSON.stringify(output, null, 2).replace(/\n/g, '\n ')}`
536
+ );
537
+ } catch {
538
+ console.log(` Output: ${step.output}`);
527
539
  }
528
- db.close();
540
+ }
541
+ }
529
542
 
530
- console.log(`โœ“ Deleted ${deleted} workflow run(s) older than ${days} days`);
531
- } catch (error) {
532
- console.error('โœ— Failed to prune runs:', error instanceof Error ? error.message : error);
533
- process.exit(1);
543
+ if (run.outputs) {
544
+ console.log('\nFinal Outputs:');
545
+ try {
546
+ const parsed = JSON.parse(run.outputs);
547
+ console.log(JSON.stringify(parsed, null, 2));
548
+ } catch {
549
+ console.log(run.outputs);
534
550
  }
551
+ }
552
+
553
+ if (run.error) {
554
+ console.log(`\n\x1b[31mWorkflow Error:\x1b[0m ${run.error}`);
555
+ }
556
+ }
557
+
558
+ // ===== keystone prune / maintenance =====
559
+ async function performMaintenance(days: number) {
560
+ try {
561
+ console.log(`๐Ÿงน Starting maintenance (pruning runs older than ${days} days)...`);
562
+ const db = new WorkflowDb();
563
+ const count = await db.pruneRuns(days);
564
+ console.log(` โœ“ Pruned ${count} old run(s)`);
565
+
566
+ console.log(' Vacuuming database (reclaiming space)...');
567
+ await db.vacuum();
568
+ console.log(' โœ“ Vacuum complete');
569
+
570
+ db.close();
571
+ console.log('\nโœจ Maintenance completed successfully!');
572
+ } catch (error) {
573
+ console.error('โœ— Maintenance failed:', error instanceof Error ? error.message : error);
574
+ process.exit(1);
575
+ }
576
+ }
577
+
578
+ program
579
+ .command('prune')
580
+ .description('Delete old workflow runs from the database (alias for maintenance)')
581
+ .option('--days <number>', 'Days to keep', '30')
582
+ .action(async (options) => {
583
+ const days = Number.parseInt(options.days, 10);
584
+ await performMaintenance(days);
585
+ });
586
+
587
+ program
588
+ .command('maintenance')
589
+ .description('Perform database maintenance (prune old runs and vacuum)')
590
+ .option('--days <days>', 'Delete runs older than this many days', '30')
591
+ .action(async (options) => {
592
+ const days = Number.parseInt(options.days, 10);
593
+ await performMaintenance(days);
535
594
  });
536
595
 
537
596
  // ===== keystone ui =====
@@ -591,14 +650,8 @@ mcp
591
650
  console.log(' You can still manually provide an OAuth token below if you have one.');
592
651
  console.log('\n2. Paste the access token below:\n');
593
652
 
594
- const prompt = 'Access Token: ';
595
- process.stdout.write(prompt);
596
-
597
- let token = '';
598
- for await (const line of console) {
599
- token = line.trim();
600
- break;
601
- }
653
+ const { promptSecret } = await import('./utils/prompt.ts');
654
+ const token = await promptSecret('Access Token: ');
602
655
 
603
656
  if (token) {
604
657
  const auth = AuthManager.load();
@@ -859,10 +912,10 @@ program.command('_list-workflows', { hidden: true }).action(() => {
859
912
  }
860
913
  });
861
914
 
862
- program.command('_list-runs', { hidden: true }).action(() => {
915
+ program.command('_list-runs', { hidden: true }).action(async () => {
863
916
  try {
864
917
  const db = new WorkflowDb();
865
- const runs = db.listRuns(50);
918
+ const runs = await db.listRuns(50);
866
919
  for (const run of runs) {
867
920
  console.log(run.id);
868
921
  }
@@ -959,11 +1012,11 @@ __keystone_runs() {
959
1012
  console.log(`_keystone_completion() {
960
1013
  local cur prev opts
961
1014
  COMPREPLY=()
962
- cur="${COMP_WORDS[COMP_CWORD]}"
963
- prev="${COMP_WORDS[COMP_CWORD - 1]}"
1015
+ cur="\${COMP_WORDS[COMP_CWORD]}"
1016
+ prev="\${COMP_WORDS[COMP_CWORD - 1]}"
964
1017
  opts="init validate graph run resume workflows history logs prune ui mcp config auth completion"
965
1018
 
966
- case "${prev}" in
1019
+ case "\${prev}" in
967
1020
  run|graph)
968
1021
  local workflows=$(keystone _list-workflows 2>/dev/null)
969
1022
  COMPREPLY=( $(compgen -W "\${workflows}" -- \${cur}) )