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.
- package/README.md +55 -8
- package/package.json +8 -17
- package/src/cli.ts +219 -166
- package/src/db/memory-db.test.ts +54 -0
- package/src/db/memory-db.ts +128 -0
- package/src/db/sqlite-setup.test.ts +47 -0
- package/src/db/sqlite-setup.ts +49 -0
- package/src/db/workflow-db.test.ts +41 -10
- package/src/db/workflow-db.ts +90 -28
- package/src/expression/evaluator.test.ts +19 -0
- package/src/expression/evaluator.ts +134 -39
- package/src/parser/schema.ts +41 -0
- package/src/runner/audit-verification.test.ts +23 -0
- package/src/runner/auto-heal.test.ts +64 -0
- package/src/runner/debug-repl.test.ts +308 -0
- package/src/runner/debug-repl.ts +225 -0
- package/src/runner/foreach-executor.ts +327 -0
- package/src/runner/llm-adapter.test.ts +37 -18
- package/src/runner/llm-adapter.ts +90 -112
- package/src/runner/llm-executor.test.ts +47 -6
- package/src/runner/llm-executor.ts +18 -3
- package/src/runner/mcp-client.audit.test.ts +69 -0
- package/src/runner/mcp-client.test.ts +12 -3
- package/src/runner/mcp-client.ts +199 -19
- package/src/runner/mcp-manager.ts +19 -8
- package/src/runner/mcp-server.test.ts +8 -5
- package/src/runner/mcp-server.ts +31 -17
- package/src/runner/optimization-runner.ts +305 -0
- package/src/runner/reflexion.test.ts +87 -0
- package/src/runner/shell-executor.test.ts +12 -0
- package/src/runner/shell-executor.ts +9 -6
- package/src/runner/step-executor.test.ts +240 -2
- package/src/runner/step-executor.ts +183 -68
- package/src/runner/stream-utils.test.ts +171 -0
- package/src/runner/stream-utils.ts +186 -0
- package/src/runner/workflow-runner.test.ts +4 -4
- package/src/runner/workflow-runner.ts +438 -259
- package/src/templates/agents/keystone-architect.md +6 -4
- package/src/templates/full-feature-demo.yaml +4 -4
- package/src/types/assets.d.ts +14 -0
- package/src/types/status.ts +1 -1
- package/src/ui/dashboard.tsx +38 -26
- package/src/utils/auth-manager.ts +3 -1
- package/src/utils/logger.test.ts +76 -0
- package/src/utils/logger.ts +39 -0
- package/src/utils/prompt.ts +75 -0
- package/src/utils/redactor.test.ts +86 -4
- 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),
|
|
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
|
-
| `
|
|
465
|
+
| `maintenance [--days N]` | Perform database maintenance (prune old runs and vacuum) |
|
|
446
466
|
|
|
447
467
|
---
|
|
448
|
-
|
|
449
|
-
##
|
|
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.
|
|
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.
|
|
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.
|
|
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('
|
|
393
|
-
.option('-
|
|
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
|
|
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('\
|
|
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
|
|
407
|
-
const
|
|
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
|
-
`${
|
|
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
|
|
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
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
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
|
-
|
|
511
|
-
|
|
512
|
-
.
|
|
513
|
-
.
|
|
514
|
-
.
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
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
|
-
|
|
540
|
+
}
|
|
541
|
+
}
|
|
529
542
|
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
|
595
|
-
|
|
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="
|
|
963
|
-
prev="
|
|
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 "
|
|
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}) )
|