keystone-cli 0.7.2 → 1.0.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/README.md +486 -54
- package/package.json +8 -2
- package/src/__fixtures__/index.ts +100 -0
- package/src/cli.ts +841 -91
- package/src/db/memory-db.ts +35 -1
- package/src/db/workflow-db.test.ts +24 -0
- package/src/db/workflow-db.ts +484 -14
- package/src/expression/evaluator.ts +68 -4
- package/src/parser/agent-parser.ts +6 -3
- package/src/parser/config-schema.ts +38 -2
- package/src/parser/schema.ts +192 -7
- package/src/parser/test-schema.ts +29 -0
- package/src/parser/workflow-parser.test.ts +54 -0
- package/src/parser/workflow-parser.ts +153 -7
- package/src/runner/aggregate-error.test.ts +57 -0
- package/src/runner/aggregate-error.ts +46 -0
- package/src/runner/audit-verification.test.ts +2 -2
- package/src/runner/auto-heal.test.ts +1 -1
- package/src/runner/blueprint-executor.test.ts +63 -0
- package/src/runner/blueprint-executor.ts +157 -0
- package/src/runner/concurrency-limit.test.ts +82 -0
- package/src/runner/debug-repl.ts +18 -3
- package/src/runner/durable-timers.test.ts +200 -0
- package/src/runner/engine-executor.test.ts +464 -0
- package/src/runner/engine-executor.ts +491 -0
- package/src/runner/foreach-executor.ts +30 -12
- package/src/runner/llm-adapter.test.ts +282 -5
- package/src/runner/llm-adapter.ts +581 -8
- package/src/runner/llm-clarification.test.ts +79 -21
- package/src/runner/llm-errors.ts +83 -0
- package/src/runner/llm-executor.test.ts +258 -219
- package/src/runner/llm-executor.ts +226 -29
- package/src/runner/mcp-client.ts +70 -3
- package/src/runner/mcp-manager.test.ts +52 -52
- package/src/runner/mcp-manager.ts +12 -5
- package/src/runner/mcp-server.test.ts +117 -78
- package/src/runner/mcp-server.ts +13 -4
- package/src/runner/optimization-runner.ts +48 -31
- package/src/runner/reflexion.test.ts +1 -1
- package/src/runner/resource-pool.test.ts +113 -0
- package/src/runner/resource-pool.ts +164 -0
- package/src/runner/shell-executor.ts +130 -32
- package/src/runner/standard-tools-execution.test.ts +39 -0
- package/src/runner/standard-tools-integration.test.ts +36 -36
- package/src/runner/standard-tools.test.ts +18 -0
- package/src/runner/standard-tools.ts +174 -93
- package/src/runner/step-executor.test.ts +176 -16
- package/src/runner/step-executor.ts +534 -83
- package/src/runner/stream-utils.test.ts +14 -0
- package/src/runner/subflow-outputs.test.ts +103 -0
- package/src/runner/test-harness.ts +161 -0
- package/src/runner/tool-integration.test.ts +73 -79
- package/src/runner/workflow-runner.test.ts +549 -15
- package/src/runner/workflow-runner.ts +1448 -79
- package/src/runner/workflow-subflows.test.ts +255 -0
- package/src/templates/agents/keystone-architect.md +17 -12
- package/src/templates/agents/tester.md +21 -0
- package/src/templates/child-rollback.yaml +11 -0
- package/src/templates/decompose-implement.yaml +53 -0
- package/src/templates/decompose-problem.yaml +159 -0
- package/src/templates/decompose-research.yaml +52 -0
- package/src/templates/decompose-review.yaml +51 -0
- package/src/templates/dev.yaml +134 -0
- package/src/templates/engine-example.yaml +33 -0
- package/src/templates/fan-out-fan-in.yaml +61 -0
- package/src/templates/memory-service.yaml +1 -1
- package/src/templates/parent-rollback.yaml +16 -0
- package/src/templates/robust-automation.yaml +1 -1
- package/src/templates/scaffold-feature.yaml +29 -27
- package/src/templates/scaffold-generate.yaml +41 -0
- package/src/templates/scaffold-plan.yaml +53 -0
- package/src/types/status.ts +3 -0
- package/src/ui/dashboard.tsx +4 -3
- package/src/utils/assets.macro.ts +36 -0
- package/src/utils/auth-manager.ts +585 -8
- package/src/utils/blueprint-utils.test.ts +49 -0
- package/src/utils/blueprint-utils.ts +80 -0
- package/src/utils/circuit-breaker.test.ts +177 -0
- package/src/utils/circuit-breaker.ts +160 -0
- package/src/utils/config-loader.test.ts +100 -13
- package/src/utils/config-loader.ts +44 -17
- package/src/utils/constants.ts +62 -0
- package/src/utils/error-renderer.test.ts +267 -0
- package/src/utils/error-renderer.ts +320 -0
- package/src/utils/json-parser.test.ts +4 -0
- package/src/utils/json-parser.ts +18 -1
- package/src/utils/mermaid.ts +4 -0
- package/src/utils/paths.test.ts +46 -0
- package/src/utils/paths.ts +70 -0
- package/src/utils/process-sandbox.test.ts +128 -0
- package/src/utils/process-sandbox.ts +293 -0
- package/src/utils/rate-limiter.test.ts +143 -0
- package/src/utils/rate-limiter.ts +221 -0
- package/src/utils/redactor.test.ts +23 -15
- package/src/utils/redactor.ts +65 -25
- package/src/utils/resource-loader.test.ts +54 -0
- package/src/utils/resource-loader.ts +158 -0
- package/src/utils/sandbox.test.ts +69 -4
- package/src/utils/sandbox.ts +69 -6
- package/src/utils/schema-validator.ts +65 -0
- package/src/utils/workflow-registry.test.ts +57 -0
- package/src/utils/workflow-registry.ts +45 -25
- /package/src/expression/{evaluator.audit.test.ts → evaluator-audit.test.ts} +0 -0
- /package/src/runner/{mcp-client.audit.test.ts → mcp-client-audit.test.ts} +0 -0
package/src/cli.ts
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
import { existsSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
|
|
6
6
|
import exploreAgent from './templates/agents/explore.md' with { type: 'text' };
|
|
7
7
|
import generalAgent from './templates/agents/general.md' with { type: 'text' };
|
|
8
8
|
import architectAgent from './templates/agents/keystone-architect.md' with { type: 'text' };
|
|
9
|
+
import softwareEngineerAgent from './templates/agents/software-engineer.md' with { type: 'text' };
|
|
10
|
+
import summarizerAgent from './templates/agents/summarizer.md' with { type: 'text' };
|
|
11
|
+
import testerAgent from './templates/agents/tester.md' with { type: 'text' };
|
|
12
|
+
import decomposeImplementWorkflow from './templates/decompose-implement.yaml' with { type: 'text' };
|
|
13
|
+
import decomposeWorkflow from './templates/decompose-problem.yaml' with { type: 'text' };
|
|
14
|
+
import decomposeResearchWorkflow from './templates/decompose-research.yaml' with { type: 'text' };
|
|
15
|
+
import decomposeReviewWorkflow from './templates/decompose-review.yaml' with { type: 'text' };
|
|
16
|
+
import devWorkflow from './templates/dev.yaml' with { type: 'text' };
|
|
9
17
|
// Default templates
|
|
10
18
|
import scaffoldWorkflow from './templates/scaffold-feature.yaml' with { type: 'text' };
|
|
19
|
+
import scaffoldGenerateWorkflow from './templates/scaffold-generate.yaml' with { type: 'text' };
|
|
20
|
+
import scaffoldPlanWorkflow from './templates/scaffold-plan.yaml' with { type: 'text' };
|
|
11
21
|
|
|
22
|
+
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
12
23
|
import { WorkflowDb, type WorkflowRun } from './db/workflow-db.ts';
|
|
24
|
+
import type { TestDefinition } from './parser/test-schema.ts';
|
|
13
25
|
import { WorkflowParser } from './parser/workflow-parser.ts';
|
|
26
|
+
import { WorkflowSuspendedError, WorkflowWaitingError } from './runner/step-executor.ts';
|
|
27
|
+
import { TestHarness } from './runner/test-harness.ts';
|
|
14
28
|
import { ConfigLoader } from './utils/config-loader.ts';
|
|
29
|
+
import { LIMITS } from './utils/constants.ts';
|
|
15
30
|
import { ConsoleLogger } from './utils/logger.ts';
|
|
16
31
|
import { generateMermaidGraph, renderWorkflowAsAscii } from './utils/mermaid.ts';
|
|
17
32
|
import { WorkflowRegistry } from './utils/workflow-registry.ts';
|
|
@@ -19,12 +34,85 @@ import { WorkflowRegistry } from './utils/workflow-registry.ts';
|
|
|
19
34
|
import pkg from '../package.json' with { type: 'json' };
|
|
20
35
|
|
|
21
36
|
const program = new Command();
|
|
37
|
+
const defaultRetentionDays = ConfigLoader.load().storage?.retention_days ?? 30;
|
|
38
|
+
const MAX_INPUT_STRING_LENGTH = LIMITS.MAX_INPUT_STRING_LENGTH;
|
|
22
39
|
|
|
23
40
|
program
|
|
24
41
|
.name('keystone')
|
|
25
42
|
.description('A local-first, declarative, agentic workflow orchestrator')
|
|
26
43
|
.version(pkg.version);
|
|
27
44
|
|
|
45
|
+
/**
|
|
46
|
+
* Parse CLI input pairs (key=value) into a record.
|
|
47
|
+
* Attempts JSON parsing for complex types, falls back to string for simple values.
|
|
48
|
+
*
|
|
49
|
+
* @param pairs Array of key=value strings
|
|
50
|
+
* @returns Record of parsed inputs
|
|
51
|
+
*/
|
|
52
|
+
const parseInputs = (pairs?: string[]): Record<string, unknown> => {
|
|
53
|
+
const inputs: Record<string, unknown> = Object.create(null);
|
|
54
|
+
const blockedKeys = new Set(['__proto__', 'prototype', 'constructor']);
|
|
55
|
+
if (!pairs) return inputs;
|
|
56
|
+
for (const pair of pairs) {
|
|
57
|
+
const index = pair.indexOf('=');
|
|
58
|
+
if (index <= 0) {
|
|
59
|
+
console.warn(`⚠️ Invalid input format: "${pair}" (expected key=value)`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
const key = pair.slice(0, index);
|
|
63
|
+
const value = pair.slice(index + 1);
|
|
64
|
+
|
|
65
|
+
// Validate key format (no special characters that could cause issues)
|
|
66
|
+
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(key)) {
|
|
67
|
+
console.warn(`⚠️ Invalid input key: "${key}" (use alphanumeric and underscores only)`);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
if (blockedKeys.has(key)) {
|
|
71
|
+
console.warn(`⚠️ Invalid input key: "${key}" (reserved keyword)`);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
// Attempt JSON parse for objects, arrays, booleans, numbers
|
|
77
|
+
const parsed = JSON.parse(value);
|
|
78
|
+
if (typeof parsed === 'string') {
|
|
79
|
+
if (parsed.length > MAX_INPUT_STRING_LENGTH) {
|
|
80
|
+
console.warn(
|
|
81
|
+
`⚠️ Input "${key}" exceeds maximum length of ${MAX_INPUT_STRING_LENGTH} characters`
|
|
82
|
+
);
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (parsed.includes('\u0000')) {
|
|
86
|
+
console.warn(`⚠️ Input "${key}" contains invalid null characters`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
inputs[key] = parsed;
|
|
91
|
+
} catch {
|
|
92
|
+
if (value.length > MAX_INPUT_STRING_LENGTH) {
|
|
93
|
+
console.warn(
|
|
94
|
+
`⚠️ Input "${key}" exceeds maximum length of ${MAX_INPUT_STRING_LENGTH} characters`
|
|
95
|
+
);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (value.includes('\u0000')) {
|
|
99
|
+
console.warn(`⚠️ Input "${key}" contains invalid null characters`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
// Check if it looks like malformed JSON (starts with { or [)
|
|
103
|
+
if ((value.startsWith('{') || value.startsWith('[')) && value.length > 1) {
|
|
104
|
+
console.warn(
|
|
105
|
+
`⚠️ Input "${key}" looks like JSON but failed to parse. Check for syntax errors.`
|
|
106
|
+
);
|
|
107
|
+
console.warn(` Value: ${value.slice(0, 50)}${value.length > 50 ? '...' : ''}`);
|
|
108
|
+
}
|
|
109
|
+
// Fall back to string value
|
|
110
|
+
inputs[key] = value;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return inputs;
|
|
114
|
+
};
|
|
115
|
+
|
|
28
116
|
// ===== keystone init =====
|
|
29
117
|
program
|
|
30
118
|
.command('init')
|
|
@@ -77,9 +165,15 @@ model_mappings:
|
|
|
77
165
|
# command: npx
|
|
78
166
|
# args: ["-y", "@modelcontextprotocol/server-filesystem", "."]
|
|
79
167
|
|
|
168
|
+
# engines:
|
|
169
|
+
# allowlist:
|
|
170
|
+
# codex:
|
|
171
|
+
# command: codex
|
|
172
|
+
# version: "1.2.3"
|
|
173
|
+
# versionArgs: ["--version"]
|
|
174
|
+
|
|
80
175
|
storage:
|
|
81
176
|
retention_days: 30
|
|
82
|
-
workflows_directory: workflows
|
|
83
177
|
`;
|
|
84
178
|
writeFileSync(configPath, defaultConfig);
|
|
85
179
|
console.log(`✓ Created ${configPath}`);
|
|
@@ -106,6 +200,30 @@ workflows_directory: workflows
|
|
|
106
200
|
path: '.keystone/workflows/scaffold-feature.yaml',
|
|
107
201
|
content: scaffoldWorkflow,
|
|
108
202
|
},
|
|
203
|
+
{
|
|
204
|
+
path: '.keystone/workflows/scaffold-plan.yaml',
|
|
205
|
+
content: scaffoldPlanWorkflow,
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
path: '.keystone/workflows/scaffold-generate.yaml',
|
|
209
|
+
content: scaffoldGenerateWorkflow,
|
|
210
|
+
},
|
|
211
|
+
{
|
|
212
|
+
path: '.keystone/workflows/decompose-problem.yaml',
|
|
213
|
+
content: decomposeWorkflow,
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
path: '.keystone/workflows/decompose-research.yaml',
|
|
217
|
+
content: decomposeResearchWorkflow,
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
path: '.keystone/workflows/decompose-implement.yaml',
|
|
221
|
+
content: decomposeImplementWorkflow,
|
|
222
|
+
},
|
|
223
|
+
{
|
|
224
|
+
path: '.keystone/workflows/decompose-review.yaml',
|
|
225
|
+
content: decomposeReviewWorkflow,
|
|
226
|
+
},
|
|
109
227
|
{
|
|
110
228
|
path: '.keystone/workflows/agents/keystone-architect.md',
|
|
111
229
|
content: architectAgent,
|
|
@@ -118,6 +236,22 @@ workflows_directory: workflows
|
|
|
118
236
|
path: '.keystone/workflows/agents/explore.md',
|
|
119
237
|
content: exploreAgent,
|
|
120
238
|
},
|
|
239
|
+
{
|
|
240
|
+
path: '.keystone/workflows/agents/software-engineer.md',
|
|
241
|
+
content: softwareEngineerAgent,
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
path: '.keystone/workflows/agents/summarizer.md',
|
|
245
|
+
content: summarizerAgent,
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
path: '.keystone/workflows/dev.yaml',
|
|
249
|
+
content: devWorkflow,
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
path: '.keystone/workflows/agents/tester.md',
|
|
253
|
+
content: testerAgent,
|
|
254
|
+
},
|
|
121
255
|
];
|
|
122
256
|
|
|
123
257
|
for (const seed of seeds) {
|
|
@@ -141,7 +275,9 @@ program
|
|
|
141
275
|
.command('validate')
|
|
142
276
|
.description('Validate workflow files')
|
|
143
277
|
.argument('[path]', 'Workflow file or directory to validate (default: .keystone/workflows/)')
|
|
144
|
-
.
|
|
278
|
+
.option('--strict', 'Enable strict validation (schemas, enums)')
|
|
279
|
+
.option('--explain', 'Show detailed error context with suggestions')
|
|
280
|
+
.action(async (pathArg, options) => {
|
|
145
281
|
const path = pathArg || '.keystone/workflows/';
|
|
146
282
|
|
|
147
283
|
try {
|
|
@@ -176,12 +312,35 @@ program
|
|
|
176
312
|
for (const file of files) {
|
|
177
313
|
try {
|
|
178
314
|
const workflow = WorkflowParser.loadWorkflow(file);
|
|
315
|
+
if (options.strict) {
|
|
316
|
+
const source = readFileSync(file, 'utf-8');
|
|
317
|
+
WorkflowParser.validateStrict(workflow, source);
|
|
318
|
+
}
|
|
179
319
|
console.log(` ✓ ${file.padEnd(40)} ${workflow.name} (${workflow.steps.length} steps)`);
|
|
180
320
|
successCount++;
|
|
181
321
|
} catch (error) {
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
322
|
+
if (options.explain) {
|
|
323
|
+
const { readFileSync } = await import('node:fs');
|
|
324
|
+
const { formatYamlError, renderError, formatError } = await import(
|
|
325
|
+
'./utils/error-renderer.ts'
|
|
326
|
+
);
|
|
327
|
+
try {
|
|
328
|
+
const source = readFileSync(file, 'utf-8');
|
|
329
|
+
const formatted = formatYamlError(error as Error, source, file);
|
|
330
|
+
console.error(renderError({ message: formatted.summary, source, filePath: file }));
|
|
331
|
+
} catch {
|
|
332
|
+
console.error(
|
|
333
|
+
renderError({
|
|
334
|
+
message: error instanceof Error ? error.message : String(error),
|
|
335
|
+
filePath: file,
|
|
336
|
+
})
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
} else {
|
|
340
|
+
console.error(
|
|
341
|
+
` ✗ ${file.padEnd(40)} ${error instanceof Error ? error.message : String(error)}`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
185
344
|
failCount++;
|
|
186
345
|
}
|
|
187
346
|
}
|
|
@@ -228,38 +387,58 @@ program
|
|
|
228
387
|
.option('-i, --input <key=value...>', 'Input values')
|
|
229
388
|
.option('--dry-run', 'Show what would be executed without actually running it')
|
|
230
389
|
.option('--debug', 'Enable interactive debug mode on failure')
|
|
231
|
-
.
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
if (index > 0) {
|
|
238
|
-
const key = pair.slice(0, index);
|
|
239
|
-
const value = pair.slice(index + 1);
|
|
240
|
-
// Try to parse as JSON, otherwise use as string
|
|
241
|
-
try {
|
|
242
|
-
inputs[key] = JSON.parse(value);
|
|
243
|
-
} catch {
|
|
244
|
-
inputs[key] = value;
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
}
|
|
390
|
+
.option('--no-dedup', 'Disable idempotency/deduplication')
|
|
391
|
+
.option('--resume', 'Resume the last run of this workflow if it failed or was paused')
|
|
392
|
+
.option('--explain', 'Show detailed error context with suggestions on failure')
|
|
393
|
+
.action(async (workflowPathArg, options) => {
|
|
394
|
+
const inputs = parseInputs(options.input);
|
|
395
|
+
let resolvedPath: string | undefined;
|
|
249
396
|
|
|
250
397
|
// Load and validate workflow
|
|
251
398
|
try {
|
|
252
|
-
|
|
399
|
+
resolvedPath = WorkflowRegistry.resolvePath(workflowPathArg);
|
|
253
400
|
const workflow = WorkflowParser.loadWorkflow(resolvedPath);
|
|
254
401
|
|
|
255
402
|
// Import WorkflowRunner dynamically
|
|
256
403
|
const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
|
|
257
404
|
const logger = new ConsoleLogger();
|
|
405
|
+
|
|
406
|
+
let resumeRunId: string | undefined;
|
|
407
|
+
|
|
408
|
+
// Handle auto-resume
|
|
409
|
+
if (options.resume) {
|
|
410
|
+
const db = new WorkflowDb();
|
|
411
|
+
const lastRun = await db.getLastRun(workflow.name);
|
|
412
|
+
db.close();
|
|
413
|
+
|
|
414
|
+
if (lastRun) {
|
|
415
|
+
if (
|
|
416
|
+
lastRun.status === 'failed' ||
|
|
417
|
+
lastRun.status === 'paused' ||
|
|
418
|
+
lastRun.status === 'running'
|
|
419
|
+
) {
|
|
420
|
+
resumeRunId = lastRun.id;
|
|
421
|
+
console.log(
|
|
422
|
+
`Resuming run ${lastRun.id} (status: ${lastRun.status}) from ${new Date(
|
|
423
|
+
lastRun.started_at
|
|
424
|
+
).toLocaleString()}`
|
|
425
|
+
);
|
|
426
|
+
} else {
|
|
427
|
+
console.log(`Last run ${lastRun.id} completed successfully. Starting new run.`);
|
|
428
|
+
}
|
|
429
|
+
} else {
|
|
430
|
+
console.log('No previous run found. Starting new run.');
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
258
434
|
const runner = new WorkflowRunner(workflow, {
|
|
259
|
-
inputs,
|
|
435
|
+
inputs: resumeRunId ? undefined : inputs,
|
|
436
|
+
resumeInputs: resumeRunId ? inputs : undefined,
|
|
260
437
|
workflowDir: dirname(resolvedPath),
|
|
261
438
|
dryRun: !!options.dryRun,
|
|
262
439
|
debug: !!options.debug,
|
|
440
|
+
dedup: options.dedup,
|
|
441
|
+
resumeRunId,
|
|
263
442
|
logger,
|
|
264
443
|
});
|
|
265
444
|
|
|
@@ -271,10 +450,108 @@ program
|
|
|
271
450
|
}
|
|
272
451
|
process.exit(0);
|
|
273
452
|
} catch (error) {
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
453
|
+
if (options.explain) {
|
|
454
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
455
|
+
try {
|
|
456
|
+
const { readFileSync } = await import('node:fs');
|
|
457
|
+
const { renderError } = await import('./utils/error-renderer.ts');
|
|
458
|
+
const source = resolvedPath ? readFileSync(resolvedPath, 'utf-8') : undefined;
|
|
459
|
+
console.error(
|
|
460
|
+
renderError({
|
|
461
|
+
message,
|
|
462
|
+
source,
|
|
463
|
+
filePath: resolvedPath,
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
} catch {
|
|
467
|
+
console.error('✗ Failed to execute workflow:', message);
|
|
468
|
+
}
|
|
469
|
+
} else {
|
|
470
|
+
console.error(
|
|
471
|
+
'✗ Failed to execute workflow:',
|
|
472
|
+
error instanceof Error ? error.message : error
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// ===== keystone test =====
|
|
480
|
+
program
|
|
481
|
+
.command('test')
|
|
482
|
+
.description('Run workflow tests with fixtures and snapshots')
|
|
483
|
+
.argument('[path]', 'Test file or directory to run (default: .keystone/tests/)')
|
|
484
|
+
.option('-u, --update', 'Update snapshots on mismatch or failure')
|
|
485
|
+
.action(async (pathArg, options) => {
|
|
486
|
+
const testPath = pathArg || '.keystone/tests/';
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
let files: string[] = [];
|
|
490
|
+
if (existsSync(testPath) && (testPath.endsWith('.yaml') || testPath.endsWith('.yml'))) {
|
|
491
|
+
files = [testPath];
|
|
492
|
+
} else if (existsSync(testPath)) {
|
|
493
|
+
const glob = new Bun.Glob('**/*.test.{yaml,yml}');
|
|
494
|
+
for await (const file of glob.scan(testPath)) {
|
|
495
|
+
files.push(join(testPath, file));
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (files.length === 0) {
|
|
500
|
+
console.log('⊘ No test files found.');
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
console.log(`🧪 Running ${files.length} test(s)...\n`);
|
|
505
|
+
|
|
506
|
+
let totalPassed = 0;
|
|
507
|
+
let totalFailed = 0;
|
|
508
|
+
|
|
509
|
+
for (const file of files) {
|
|
510
|
+
try {
|
|
511
|
+
const content = readFileSync(file, 'utf-8');
|
|
512
|
+
const testDef = parseYaml(content) as TestDefinition;
|
|
513
|
+
|
|
514
|
+
console.log(` ▶ ${testDef.name} (${file})`);
|
|
515
|
+
|
|
516
|
+
const workflowPath = WorkflowRegistry.resolvePath(testDef.workflow);
|
|
517
|
+
const workflow = WorkflowParser.loadWorkflow(workflowPath);
|
|
518
|
+
|
|
519
|
+
const harness = new TestHarness(workflow, testDef.fixture);
|
|
520
|
+
const result = await harness.run();
|
|
521
|
+
|
|
522
|
+
if (!testDef.snapshot || options.update) {
|
|
523
|
+
testDef.snapshot = result;
|
|
524
|
+
writeFileSync(file, stringifyYaml(testDef));
|
|
525
|
+
console.log(` ✓ Snapshot ${options.update ? 'updated' : 'initialized'}`);
|
|
526
|
+
totalPassed++;
|
|
527
|
+
continue;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Compare snapshot (simple JSON stringify for now)
|
|
531
|
+
const expected = JSON.stringify(testDef.snapshot);
|
|
532
|
+
const actual = JSON.stringify(result);
|
|
533
|
+
|
|
534
|
+
if (expected !== actual) {
|
|
535
|
+
console.error(` ✗ Snapshot mismatch in ${file}`);
|
|
536
|
+
totalFailed++;
|
|
537
|
+
} else {
|
|
538
|
+
console.log(' ✓ Passed');
|
|
539
|
+
totalPassed++;
|
|
540
|
+
}
|
|
541
|
+
} catch (error) {
|
|
542
|
+
console.error(
|
|
543
|
+
` ✗ Test failed: ${error instanceof Error ? error.message : String(error)}`
|
|
544
|
+
);
|
|
545
|
+
totalFailed++;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
console.log(`\nSummary: ${totalPassed} passed, ${totalFailed} failed.`);
|
|
550
|
+
if (totalFailed > 0) {
|
|
551
|
+
process.exit(1);
|
|
552
|
+
}
|
|
553
|
+
} catch (error) {
|
|
554
|
+
console.error('✗ Test execution failed:', error instanceof Error ? error.message : error);
|
|
278
555
|
process.exit(1);
|
|
279
556
|
}
|
|
280
557
|
});
|
|
@@ -314,22 +591,7 @@ program
|
|
|
314
591
|
const resolvedPath = WorkflowRegistry.resolvePath(workflowPath);
|
|
315
592
|
const workflow = WorkflowParser.loadWorkflow(resolvedPath);
|
|
316
593
|
|
|
317
|
-
|
|
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
|
-
}
|
|
594
|
+
const inputs = parseInputs(options.input);
|
|
333
595
|
|
|
334
596
|
const runner = new OptimizationRunner(workflow, {
|
|
335
597
|
workflowPath: resolvedPath,
|
|
@@ -359,6 +621,7 @@ program
|
|
|
359
621
|
.description('Resume a paused or failed workflow run')
|
|
360
622
|
.argument('<run_id>', 'Run ID to resume')
|
|
361
623
|
.option('-w, --workflow <path>', 'Path to workflow file (auto-detected if not specified)')
|
|
624
|
+
.option('-i, --input <key=value...>', 'Input values for resume')
|
|
362
625
|
.action(async (runId, options) => {
|
|
363
626
|
try {
|
|
364
627
|
const db = new WorkflowDb();
|
|
@@ -400,8 +663,10 @@ program
|
|
|
400
663
|
// Import WorkflowRunner dynamically
|
|
401
664
|
const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
|
|
402
665
|
const logger = new ConsoleLogger();
|
|
666
|
+
const inputs = parseInputs(options.input);
|
|
403
667
|
const runner = new WorkflowRunner(workflow, {
|
|
404
668
|
resumeRunId: runId,
|
|
669
|
+
resumeInputs: inputs,
|
|
405
670
|
workflowDir: dirname(workflowPath),
|
|
406
671
|
logger,
|
|
407
672
|
});
|
|
@@ -500,6 +765,124 @@ program
|
|
|
500
765
|
}
|
|
501
766
|
});
|
|
502
767
|
|
|
768
|
+
// ===== keystone compile =====
|
|
769
|
+
program
|
|
770
|
+
.command('compile')
|
|
771
|
+
.description('Compile a project into a single executable with embedded assets')
|
|
772
|
+
.option('-o, --outfile <path>', 'Output executable path', 'keystone-app')
|
|
773
|
+
.option('--project <path>', 'Project directory (default: .)', '.')
|
|
774
|
+
.action(async (options) => {
|
|
775
|
+
const { spawnSync } = await import('node:child_process');
|
|
776
|
+
const { resolve, join } = await import('node:path');
|
|
777
|
+
const { existsSync } = await import('node:fs');
|
|
778
|
+
|
|
779
|
+
const projectDir = resolve(options.project);
|
|
780
|
+
const keystoneDir = join(projectDir, '.keystone');
|
|
781
|
+
|
|
782
|
+
if (!existsSync(keystoneDir)) {
|
|
783
|
+
console.error(`✗ No .keystone directory found at ${projectDir}`);
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
console.log(`🏗️ Compiling project at ${projectDir}...`);
|
|
788
|
+
console.log(`📂 Embedding assets from ${keystoneDir}`);
|
|
789
|
+
|
|
790
|
+
// Find the CLI source path
|
|
791
|
+
const cliSource = resolve(import.meta.dir, 'cli.ts');
|
|
792
|
+
|
|
793
|
+
const buildArgs = ['build', cliSource, '--compile', '--outfile', options.outfile];
|
|
794
|
+
|
|
795
|
+
console.log(`🚀 Running: ASSETS_DIR=${keystoneDir} bun ${buildArgs.join(' ')}`);
|
|
796
|
+
|
|
797
|
+
const result = spawnSync('bun', buildArgs, {
|
|
798
|
+
env: {
|
|
799
|
+
...process.env,
|
|
800
|
+
ASSETS_DIR: keystoneDir,
|
|
801
|
+
},
|
|
802
|
+
stdio: 'inherit',
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
if (result.status === 0) {
|
|
806
|
+
console.log(`\n✨ Successfully compiled to ${options.outfile}`);
|
|
807
|
+
console.log(` You can now run ./${options.outfile} anywhere!`);
|
|
808
|
+
} else {
|
|
809
|
+
console.error(`\n✗ Compilation failed with exit code ${result.status}`);
|
|
810
|
+
process.exit(1);
|
|
811
|
+
}
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
// ===== keystone dev =====
|
|
815
|
+
program
|
|
816
|
+
.command('dev')
|
|
817
|
+
.description('Run the self-bootstrapping DevMode workflow')
|
|
818
|
+
.argument('<task>', 'The development task to perform')
|
|
819
|
+
.option('--auto-approve', 'Skip the plan approval step', false)
|
|
820
|
+
.action(async (task, options) => {
|
|
821
|
+
try {
|
|
822
|
+
// Find the dev workflow path
|
|
823
|
+
// Priority:
|
|
824
|
+
// 1. Local .keystone/workflows/dev.yaml
|
|
825
|
+
// 2. Embedded resource
|
|
826
|
+
let devPath: string;
|
|
827
|
+
try {
|
|
828
|
+
devPath = WorkflowRegistry.resolvePath('dev');
|
|
829
|
+
} catch {
|
|
830
|
+
// Fallback to searching in templates if not indexed yet
|
|
831
|
+
devPath = join(process.cwd(), '.keystone/workflows/dev.yaml');
|
|
832
|
+
if (!existsSync(devPath)) {
|
|
833
|
+
console.error('✗ Dev workflow not found. Run "keystone init" to seed it.');
|
|
834
|
+
process.exit(1);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
console.log(`🏗️ Starting DevMode for task: ${task}\n`);
|
|
839
|
+
|
|
840
|
+
// Import WorkflowRunner dynamically
|
|
841
|
+
const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
|
|
842
|
+
const { WorkflowParser } = await import('./parser/workflow-parser.ts');
|
|
843
|
+
const logger = new ConsoleLogger();
|
|
844
|
+
|
|
845
|
+
const workflow = WorkflowParser.loadWorkflow(devPath);
|
|
846
|
+
const runner = new WorkflowRunner(workflow, {
|
|
847
|
+
inputs: { task, auto_approve: options.auto_approve },
|
|
848
|
+
workflowDir: dirname(devPath),
|
|
849
|
+
logger,
|
|
850
|
+
allowInsecure: true, // Trusted internal workflow
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
const outputs = await runner.run();
|
|
854
|
+
if (Object.keys(outputs).length > 0) {
|
|
855
|
+
console.log('\nDevMode Summary:');
|
|
856
|
+
console.log(JSON.stringify(runner.redact(outputs), null, 2));
|
|
857
|
+
}
|
|
858
|
+
process.exit(0);
|
|
859
|
+
} catch (error) {
|
|
860
|
+
console.error('\n✗ DevMode failed:', error instanceof Error ? error.message : error);
|
|
861
|
+
process.exit(1);
|
|
862
|
+
}
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// ===== keystone manifest =====
|
|
866
|
+
program
|
|
867
|
+
.command('manifest')
|
|
868
|
+
.description('Show embedded assets manifest')
|
|
869
|
+
.action(async () => {
|
|
870
|
+
const { ResourceLoader } = await import('./utils/resource-loader.ts');
|
|
871
|
+
const assets = ResourceLoader.getEmbeddedAssets();
|
|
872
|
+
const keys = Object.keys(assets);
|
|
873
|
+
|
|
874
|
+
if (keys.length === 0) {
|
|
875
|
+
console.log('No embedded assets found.');
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
console.log(`\n📦 Embedded Assets (${keys.length}):`);
|
|
880
|
+
for (const key of keys.sort()) {
|
|
881
|
+
console.log(` - ${key} (${assets[key].length} bytes)`);
|
|
882
|
+
}
|
|
883
|
+
console.log('');
|
|
884
|
+
});
|
|
885
|
+
|
|
503
886
|
async function showRunLogs(run: WorkflowRun, db: WorkflowDb, verbose: boolean) {
|
|
504
887
|
console.log(`\n🏛️ Run: ${run.workflow_name} (${run.id})`);
|
|
505
888
|
console.log(` Status: ${run.status}`);
|
|
@@ -578,7 +961,7 @@ async function performMaintenance(days: number) {
|
|
|
578
961
|
program
|
|
579
962
|
.command('prune')
|
|
580
963
|
.description('Delete old workflow runs from the database (alias for maintenance)')
|
|
581
|
-
.option('--days <number>', 'Days to keep',
|
|
964
|
+
.option('--days <number>', 'Days to keep', String(defaultRetentionDays))
|
|
582
965
|
.action(async (options) => {
|
|
583
966
|
const days = Number.parseInt(options.days, 10);
|
|
584
967
|
await performMaintenance(days);
|
|
@@ -587,12 +970,236 @@ program
|
|
|
587
970
|
program
|
|
588
971
|
.command('maintenance')
|
|
589
972
|
.description('Perform database maintenance (prune old runs and vacuum)')
|
|
590
|
-
.option('--days <days>', 'Delete runs older than this many days',
|
|
973
|
+
.option('--days <days>', 'Delete runs older than this many days', String(defaultRetentionDays))
|
|
591
974
|
.action(async (options) => {
|
|
592
975
|
const days = Number.parseInt(options.days, 10);
|
|
593
976
|
await performMaintenance(days);
|
|
594
977
|
});
|
|
595
978
|
|
|
979
|
+
// ===== keystone dedup =====
|
|
980
|
+
const dedup = program.command('dedup').description('Manage idempotency/deduplication records');
|
|
981
|
+
|
|
982
|
+
dedup
|
|
983
|
+
.command('list')
|
|
984
|
+
.description('List idempotency records')
|
|
985
|
+
.argument('[run_id]', 'Filter by run ID (optional)')
|
|
986
|
+
.action(async (runId) => {
|
|
987
|
+
try {
|
|
988
|
+
const db = new WorkflowDb();
|
|
989
|
+
const records = await db.listIdempotencyRecords(runId);
|
|
990
|
+
db.close();
|
|
991
|
+
|
|
992
|
+
if (records.length === 0) {
|
|
993
|
+
console.log('No idempotency records found.');
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
console.log('\n🔑 Idempotency Records:');
|
|
998
|
+
console.log(''.padEnd(100, '-'));
|
|
999
|
+
console.log(
|
|
1000
|
+
`${'Key'.padEnd(30)} ${'Step'.padEnd(15)} ${'Status'.padEnd(10)} ${'Created At'}`
|
|
1001
|
+
);
|
|
1002
|
+
console.log(''.padEnd(100, '-'));
|
|
1003
|
+
|
|
1004
|
+
for (const record of records) {
|
|
1005
|
+
const key = record.idempotency_key.slice(0, 28);
|
|
1006
|
+
console.log(
|
|
1007
|
+
`${key.padEnd(30)} ${record.step_id.padEnd(15)} ${record.status.padEnd(10)} ${record.created_at}`
|
|
1008
|
+
);
|
|
1009
|
+
}
|
|
1010
|
+
console.log(`\nTotal: ${records.length} record(s)`);
|
|
1011
|
+
} catch (error) {
|
|
1012
|
+
console.error('✗ Failed to list records:', error instanceof Error ? error.message : error);
|
|
1013
|
+
process.exit(1);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
|
|
1017
|
+
dedup
|
|
1018
|
+
.command('clear')
|
|
1019
|
+
.description('Clear idempotency records')
|
|
1020
|
+
.argument('<target>', 'Run ID to clear, or "--all" to clear all records')
|
|
1021
|
+
.action(async (target) => {
|
|
1022
|
+
try {
|
|
1023
|
+
const db = new WorkflowDb();
|
|
1024
|
+
let count: number;
|
|
1025
|
+
|
|
1026
|
+
if (target === '--all') {
|
|
1027
|
+
count = await db.clearAllIdempotencyRecords();
|
|
1028
|
+
console.log(`✓ Cleared ${count} idempotency record(s)`);
|
|
1029
|
+
} else {
|
|
1030
|
+
count = await db.clearIdempotencyRecords(target);
|
|
1031
|
+
console.log(`✓ Cleared ${count} idempotency record(s) for run ${target.slice(0, 8)}`);
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
db.close();
|
|
1035
|
+
} catch (error) {
|
|
1036
|
+
console.error('✗ Failed to clear records:', error instanceof Error ? error.message : error);
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
|
|
1041
|
+
dedup
|
|
1042
|
+
.command('prune')
|
|
1043
|
+
.description('Remove expired idempotency records')
|
|
1044
|
+
.action(async () => {
|
|
1045
|
+
try {
|
|
1046
|
+
const db = new WorkflowDb();
|
|
1047
|
+
const count = await db.pruneIdempotencyRecords();
|
|
1048
|
+
db.close();
|
|
1049
|
+
console.log(`✓ Pruned ${count} expired idempotency record(s)`);
|
|
1050
|
+
} catch (error) {
|
|
1051
|
+
console.error('✗ Failed to prune records:', error instanceof Error ? error.message : error);
|
|
1052
|
+
process.exit(1);
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
// ===== keystone scheduler =====
|
|
1057
|
+
program
|
|
1058
|
+
.command('scheduler')
|
|
1059
|
+
.description('Run the durable timer scheduler (polls for ready timers)')
|
|
1060
|
+
.option('-i, --interval <seconds>', 'Poll interval in seconds', '30')
|
|
1061
|
+
.option('--once', "Run once and exit (don't poll)")
|
|
1062
|
+
.action(async (options) => {
|
|
1063
|
+
const interval = Number.parseInt(options.interval, 10) * 1000;
|
|
1064
|
+
const db = new WorkflowDb();
|
|
1065
|
+
|
|
1066
|
+
console.log('🏛️ Keystone Durable Timer Scheduler');
|
|
1067
|
+
console.log(`📡 Polling every ${options.interval}s for ready timers...`);
|
|
1068
|
+
|
|
1069
|
+
const poll = async () => {
|
|
1070
|
+
try {
|
|
1071
|
+
const pending = await db.getPendingTimers(undefined, 'sleep');
|
|
1072
|
+
if (pending.length > 0) {
|
|
1073
|
+
console.log(`\n⏰ Found ${pending.length} ready timer(s)`);
|
|
1074
|
+
|
|
1075
|
+
for (const timer of pending) {
|
|
1076
|
+
console.log(` - Resuming run ${timer.run_id.slice(0, 8)} (step: ${timer.step_id})`);
|
|
1077
|
+
|
|
1078
|
+
// Load run to get workflow name
|
|
1079
|
+
const run = await db.getRun(timer.run_id);
|
|
1080
|
+
if (!run) {
|
|
1081
|
+
console.warn(` ⚠️ Run ${timer.run_id} not found in DB`);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
try {
|
|
1086
|
+
const workflowPath = WorkflowRegistry.resolvePath(run.workflow_name);
|
|
1087
|
+
const workflow = WorkflowParser.loadWorkflow(workflowPath);
|
|
1088
|
+
|
|
1089
|
+
const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
|
|
1090
|
+
const runner = new WorkflowRunner(workflow, {
|
|
1091
|
+
resumeRunId: timer.run_id,
|
|
1092
|
+
workflowDir: dirname(workflowPath),
|
|
1093
|
+
logger: new ConsoleLogger(),
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
// Running this in current process iteration
|
|
1097
|
+
// The runner will handle checking the timer status in restoreState
|
|
1098
|
+
await runner.run();
|
|
1099
|
+
console.log(` ✓ Run ${timer.run_id.slice(0, 8)} resumed and finished/paused`);
|
|
1100
|
+
} catch (err) {
|
|
1101
|
+
if (err instanceof WorkflowWaitingError || err instanceof WorkflowSuspendedError) {
|
|
1102
|
+
// This is expected if it hits another wait/human step
|
|
1103
|
+
console.log(` ⏸ Run ${timer.run_id.slice(0, 8)} paused/waiting again`);
|
|
1104
|
+
} else {
|
|
1105
|
+
console.error(
|
|
1106
|
+
` ✗ Failed to resume run ${timer.run_id.slice(0, 8)}:`,
|
|
1107
|
+
err instanceof Error ? err.message : String(err)
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
console.error('✗ Scheduler error:', err instanceof Error ? err.message : String(err));
|
|
1115
|
+
}
|
|
1116
|
+
};
|
|
1117
|
+
|
|
1118
|
+
if (options.once) {
|
|
1119
|
+
await poll();
|
|
1120
|
+
db.close();
|
|
1121
|
+
process.exit(0);
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Polling loop
|
|
1125
|
+
await poll();
|
|
1126
|
+
setInterval(poll, interval);
|
|
1127
|
+
|
|
1128
|
+
// Keep process alive
|
|
1129
|
+
process.on('SIGINT', () => {
|
|
1130
|
+
console.log('\n👋 Scheduler stopping...');
|
|
1131
|
+
db.close();
|
|
1132
|
+
process.exit(0);
|
|
1133
|
+
});
|
|
1134
|
+
});
|
|
1135
|
+
|
|
1136
|
+
// ===== keystone timers =====
|
|
1137
|
+
const timersCmd = program.command('timers').description('Manage durable timers');
|
|
1138
|
+
|
|
1139
|
+
timersCmd
|
|
1140
|
+
.command('list')
|
|
1141
|
+
.description('List pending timers')
|
|
1142
|
+
.option('-r, --run <run_id>', 'Filter by run ID')
|
|
1143
|
+
.action(async (options) => {
|
|
1144
|
+
try {
|
|
1145
|
+
const db = new WorkflowDb();
|
|
1146
|
+
const timers = await db.listTimers(options.run);
|
|
1147
|
+
db.close();
|
|
1148
|
+
|
|
1149
|
+
if (timers.length === 0) {
|
|
1150
|
+
console.log('No durable timers found.');
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
console.log('\n⏰ Durable Timers:');
|
|
1155
|
+
console.log(''.padEnd(100, '-'));
|
|
1156
|
+
console.log(
|
|
1157
|
+
`${'ID'.padEnd(10)} ${'Run'.padEnd(15)} ${'Step'.padEnd(20)} ${'Type'.padEnd(10)} ${'Wake At'}`
|
|
1158
|
+
);
|
|
1159
|
+
console.log(''.padEnd(100, '-'));
|
|
1160
|
+
|
|
1161
|
+
for (const timer of timers) {
|
|
1162
|
+
const id = timer.id.slice(0, 8);
|
|
1163
|
+
const run = timer.run_id.slice(0, 8);
|
|
1164
|
+
const wakeAt = timer.wake_at ? new Date(timer.wake_at).toLocaleString() : 'N/A';
|
|
1165
|
+
const statusStr = timer.completed_at
|
|
1166
|
+
? ` (DONE at ${new Date(timer.completed_at).toLocaleTimeString()})`
|
|
1167
|
+
: '';
|
|
1168
|
+
|
|
1169
|
+
console.log(
|
|
1170
|
+
`${id.padEnd(10)} ${run.padEnd(15)} ${timer.step_id.padEnd(20)} ${timer.timer_type.padEnd(
|
|
1171
|
+
10
|
|
1172
|
+
)} ${wakeAt}${statusStr}`
|
|
1173
|
+
);
|
|
1174
|
+
}
|
|
1175
|
+
console.log(`\nTotal: ${timers.length} timer(s)`);
|
|
1176
|
+
} catch (error) {
|
|
1177
|
+
console.error('✗ Failed to list timers:', error instanceof Error ? error.message : error);
|
|
1178
|
+
process.exit(1);
|
|
1179
|
+
}
|
|
1180
|
+
});
|
|
1181
|
+
|
|
1182
|
+
timersCmd
|
|
1183
|
+
.command('clear')
|
|
1184
|
+
.description('Clear pending timers')
|
|
1185
|
+
.option('-r, --run <run_id>', 'Clear timers for a specific run')
|
|
1186
|
+
.option('--all', 'Clear all timers')
|
|
1187
|
+
.action(async (options) => {
|
|
1188
|
+
try {
|
|
1189
|
+
if (!options.all && !options.run) {
|
|
1190
|
+
console.error('✗ Please specify --run <id> or --all');
|
|
1191
|
+
process.exit(1);
|
|
1192
|
+
}
|
|
1193
|
+
const db = new WorkflowDb();
|
|
1194
|
+
const count = await db.clearTimers(options.run);
|
|
1195
|
+
db.close();
|
|
1196
|
+
console.log(`✓ Cleared ${count} timer(s)`);
|
|
1197
|
+
} catch (error) {
|
|
1198
|
+
console.error('✗ Failed to clear timers:', error instanceof Error ? error.message : error);
|
|
1199
|
+
process.exit(1);
|
|
1200
|
+
}
|
|
1201
|
+
});
|
|
1202
|
+
|
|
596
1203
|
// ===== keystone ui =====
|
|
597
1204
|
program
|
|
598
1205
|
.command('ui')
|
|
@@ -706,15 +1313,26 @@ mcp
|
|
|
706
1313
|
});
|
|
707
1314
|
|
|
708
1315
|
// ===== keystone config =====
|
|
709
|
-
program
|
|
710
|
-
|
|
711
|
-
|
|
1316
|
+
const configCmd = program.command('config').description('Configuration management');
|
|
1317
|
+
|
|
1318
|
+
configCmd
|
|
1319
|
+
.command('show')
|
|
1320
|
+
.alias('list')
|
|
1321
|
+
.description('Show current configuration and discovery paths')
|
|
712
1322
|
.action(async () => {
|
|
713
1323
|
const { ConfigLoader } = await import('./utils/config-loader.ts');
|
|
1324
|
+
const { PathResolver } = await import('./utils/paths.ts');
|
|
714
1325
|
try {
|
|
715
1326
|
const config = ConfigLoader.load();
|
|
716
1327
|
console.log('\n🏛️ Keystone Configuration:');
|
|
717
1328
|
console.log(JSON.stringify(config, null, 2));
|
|
1329
|
+
|
|
1330
|
+
console.log('\n🔍 Configuration Search Paths (in precedence order):');
|
|
1331
|
+
const paths = PathResolver.getConfigPaths();
|
|
1332
|
+
for (const [i, p] of paths.entries()) {
|
|
1333
|
+
const exists = existsSync(p) ? '✓' : '⊘';
|
|
1334
|
+
console.log(` ${i + 1}. ${exists} ${p}`);
|
|
1335
|
+
}
|
|
718
1336
|
} catch (error) {
|
|
719
1337
|
console.error('✗ Failed to load config:', error instanceof Error ? error.message : error);
|
|
720
1338
|
}
|
|
@@ -727,16 +1345,13 @@ auth
|
|
|
727
1345
|
.command('login')
|
|
728
1346
|
.description('Login to an authentication provider')
|
|
729
1347
|
.argument('[provider]', 'Authentication provider', 'github')
|
|
730
|
-
.option(
|
|
731
|
-
'-p, --provider <provider>',
|
|
732
|
-
'Authentication provider (deprecated, use positional argument)'
|
|
733
|
-
)
|
|
734
1348
|
.option('-t, --token <token>', 'Personal Access Token (if not using interactive mode)')
|
|
735
|
-
.
|
|
1349
|
+
.option('--project <project_id>', 'Google Cloud project ID (Gemini OAuth)')
|
|
1350
|
+
.action(async (provider, options) => {
|
|
736
1351
|
const { AuthManager } = await import('./utils/auth-manager.ts');
|
|
737
|
-
const
|
|
1352
|
+
const providerName = provider.toLowerCase();
|
|
738
1353
|
|
|
739
|
-
if (
|
|
1354
|
+
if (providerName === 'github') {
|
|
740
1355
|
let token = options.token;
|
|
741
1356
|
|
|
742
1357
|
if (!token) {
|
|
@@ -791,12 +1406,87 @@ auth
|
|
|
791
1406
|
console.error('✗ No token provided.');
|
|
792
1407
|
process.exit(1);
|
|
793
1408
|
}
|
|
794
|
-
} else if (
|
|
1409
|
+
} else if (providerName === 'openai-chatgpt') {
|
|
1410
|
+
try {
|
|
1411
|
+
await AuthManager.loginOpenAIChatGPT();
|
|
1412
|
+
console.log('\n✓ Successfully logged in to OpenAI ChatGPT.');
|
|
1413
|
+
return;
|
|
1414
|
+
} catch (error) {
|
|
1415
|
+
console.error(
|
|
1416
|
+
'\n✗ Failed to login with OpenAI ChatGPT:',
|
|
1417
|
+
error instanceof Error ? error.message : error
|
|
1418
|
+
);
|
|
1419
|
+
process.exit(1);
|
|
1420
|
+
}
|
|
1421
|
+
} else if (providerName === 'anthropic-claude') {
|
|
1422
|
+
try {
|
|
1423
|
+
const { url, verifier } = AuthManager.createAnthropicClaudeAuth();
|
|
1424
|
+
|
|
1425
|
+
console.log('\nTo login with Anthropic Claude (Pro/Max):');
|
|
1426
|
+
console.log('1. Visit the following URL in your browser:');
|
|
1427
|
+
console.log(` ${url}\n`);
|
|
1428
|
+
console.log('2. Copy the authorization code and paste it below:\n');
|
|
1429
|
+
|
|
1430
|
+
try {
|
|
1431
|
+
const { platform } = process;
|
|
1432
|
+
const command =
|
|
1433
|
+
platform === 'win32' ? 'start' : platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1434
|
+
const { spawn } = require('node:child_process');
|
|
1435
|
+
spawn(command, [url]);
|
|
1436
|
+
} catch (e) {
|
|
1437
|
+
// Ignore if we can't open the browser automatically
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
let code = options.token;
|
|
1441
|
+
if (!code) {
|
|
1442
|
+
const prompt = 'Authorization Code: ';
|
|
1443
|
+
process.stdout.write(prompt);
|
|
1444
|
+
for await (const line of console) {
|
|
1445
|
+
code = line.trim();
|
|
1446
|
+
break;
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
if (!code) {
|
|
1451
|
+
console.error('✗ No authorization code provided.');
|
|
1452
|
+
process.exit(1);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
const data = await AuthManager.exchangeAnthropicClaudeCode(code, verifier);
|
|
1456
|
+
AuthManager.save({
|
|
1457
|
+
anthropic_claude: {
|
|
1458
|
+
access_token: data.access_token,
|
|
1459
|
+
refresh_token: data.refresh_token,
|
|
1460
|
+
expires_at: Math.floor(Date.now() / 1000) + data.expires_in,
|
|
1461
|
+
},
|
|
1462
|
+
});
|
|
1463
|
+
console.log('\n✓ Successfully logged in to Anthropic Claude.');
|
|
1464
|
+
return;
|
|
1465
|
+
} catch (error) {
|
|
1466
|
+
console.error(
|
|
1467
|
+
'\n✗ Failed to login with Anthropic Claude:',
|
|
1468
|
+
error instanceof Error ? error.message : error
|
|
1469
|
+
);
|
|
1470
|
+
process.exit(1);
|
|
1471
|
+
}
|
|
1472
|
+
} else if (providerName === 'gemini' || providerName === 'google-gemini') {
|
|
1473
|
+
try {
|
|
1474
|
+
await AuthManager.loginGoogleGemini(options.project);
|
|
1475
|
+
console.log('\n✓ Successfully logged in to Google Gemini.');
|
|
1476
|
+
return;
|
|
1477
|
+
} catch (error) {
|
|
1478
|
+
console.error(
|
|
1479
|
+
'\n✗ Failed to login with Google Gemini:',
|
|
1480
|
+
error instanceof Error ? error.message : error
|
|
1481
|
+
);
|
|
1482
|
+
process.exit(1);
|
|
1483
|
+
}
|
|
1484
|
+
} else if (providerName === 'openai' || providerName === 'anthropic') {
|
|
795
1485
|
let key = options.token; // Use --token if provided as the API key
|
|
796
1486
|
|
|
797
1487
|
if (!key) {
|
|
798
|
-
console.log(`\n🔑 Login to ${
|
|
799
|
-
console.log(` Please provide your ${
|
|
1488
|
+
console.log(`\n🔑 Login to ${providerName.toUpperCase()}`);
|
|
1489
|
+
console.log(` Please provide your ${providerName.toUpperCase()} API key.\n`);
|
|
800
1490
|
const prompt = 'API Key: ';
|
|
801
1491
|
process.stdout.write(prompt);
|
|
802
1492
|
for await (const line of console) {
|
|
@@ -806,18 +1496,18 @@ auth
|
|
|
806
1496
|
}
|
|
807
1497
|
|
|
808
1498
|
if (key) {
|
|
809
|
-
if (
|
|
1499
|
+
if (providerName === 'openai') {
|
|
810
1500
|
AuthManager.save({ openai_api_key: key });
|
|
811
1501
|
} else {
|
|
812
1502
|
AuthManager.save({ anthropic_api_key: key });
|
|
813
1503
|
}
|
|
814
|
-
console.log(`\n✓ Successfully saved ${
|
|
1504
|
+
console.log(`\n✓ Successfully saved ${providerName.toUpperCase()} API key.`);
|
|
815
1505
|
} else {
|
|
816
1506
|
console.error('✗ No API key provided.');
|
|
817
1507
|
process.exit(1);
|
|
818
1508
|
}
|
|
819
1509
|
} else {
|
|
820
|
-
console.error(`✗ Unsupported provider: ${
|
|
1510
|
+
console.error(`✗ Unsupported provider: ${providerName}`);
|
|
821
1511
|
process.exit(1);
|
|
822
1512
|
}
|
|
823
1513
|
});
|
|
@@ -826,49 +1516,91 @@ auth
|
|
|
826
1516
|
.command('status')
|
|
827
1517
|
.description('Show authentication status')
|
|
828
1518
|
.argument('[provider]', 'Authentication provider')
|
|
829
|
-
.
|
|
830
|
-
.action(async (providerArg, options) => {
|
|
1519
|
+
.action(async (provider) => {
|
|
831
1520
|
const { AuthManager } = await import('./utils/auth-manager.ts');
|
|
832
1521
|
const auth = AuthManager.load();
|
|
833
|
-
const
|
|
1522
|
+
const providerName = provider?.toLowerCase();
|
|
834
1523
|
|
|
835
1524
|
console.log('\n🏛️ Authentication Status:');
|
|
836
1525
|
|
|
837
|
-
if (!
|
|
1526
|
+
if (!providerName || providerName === 'github' || providerName === 'copilot') {
|
|
838
1527
|
if (auth.github_token) {
|
|
839
1528
|
console.log(' ✓ Logged into GitHub');
|
|
840
1529
|
if (auth.copilot_expires_at) {
|
|
841
1530
|
const expires = new Date(auth.copilot_expires_at * 1000);
|
|
842
1531
|
console.log(` ✓ Copilot session expires: ${expires.toLocaleString()}`);
|
|
843
1532
|
}
|
|
844
|
-
} else if (
|
|
1533
|
+
} else if (providerName) {
|
|
845
1534
|
console.log(
|
|
846
1535
|
` ⊘ Not logged into GitHub. Run "keystone auth login github" to authenticate.`
|
|
847
1536
|
);
|
|
848
1537
|
}
|
|
849
1538
|
}
|
|
850
1539
|
|
|
851
|
-
if (!
|
|
1540
|
+
if (!providerName || providerName === 'openai' || providerName === 'openai-chatgpt') {
|
|
852
1541
|
if (auth.openai_api_key) {
|
|
853
1542
|
console.log(' ✓ OpenAI API key configured');
|
|
854
|
-
}
|
|
1543
|
+
}
|
|
1544
|
+
if (auth.openai_chatgpt) {
|
|
1545
|
+
console.log(' ✓ OpenAI ChatGPT subscription (OAuth) authenticated');
|
|
1546
|
+
if (auth.openai_chatgpt.expires_at) {
|
|
1547
|
+
const expires = new Date(auth.openai_chatgpt.expires_at * 1000);
|
|
1548
|
+
console.log(` Session expires: ${expires.toLocaleString()}`);
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
if (providerName && !auth.openai_api_key && !auth.openai_chatgpt) {
|
|
855
1553
|
console.log(
|
|
856
|
-
` ⊘ OpenAI
|
|
1554
|
+
` ⊘ OpenAI authentication not configured. Run "keystone auth login openai" or "keystone auth login openai-chatgpt" to authenticate.`
|
|
857
1555
|
);
|
|
858
1556
|
}
|
|
859
1557
|
}
|
|
860
1558
|
|
|
861
|
-
if (!
|
|
1559
|
+
if (!providerName || providerName === 'anthropic' || providerName === 'anthropic-claude') {
|
|
862
1560
|
if (auth.anthropic_api_key) {
|
|
863
1561
|
console.log(' ✓ Anthropic API key configured');
|
|
864
|
-
}
|
|
1562
|
+
}
|
|
1563
|
+
if (auth.anthropic_claude) {
|
|
1564
|
+
console.log(' ✓ Anthropic Claude subscription (OAuth) authenticated');
|
|
1565
|
+
if (auth.anthropic_claude.expires_at) {
|
|
1566
|
+
const expires = new Date(auth.anthropic_claude.expires_at * 1000);
|
|
1567
|
+
console.log(` Session expires: ${expires.toLocaleString()}`);
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (providerName && !auth.anthropic_api_key && !auth.anthropic_claude) {
|
|
1572
|
+
console.log(
|
|
1573
|
+
` ⊘ Anthropic authentication not configured. Run "keystone auth login anthropic" or "keystone auth login anthropic-claude" to authenticate.`
|
|
1574
|
+
);
|
|
1575
|
+
}
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
if (!providerName || providerName === 'gemini' || providerName === 'google-gemini') {
|
|
1579
|
+
if (auth.google_gemini) {
|
|
1580
|
+
console.log(' ✓ Google Gemini subscription (OAuth) authenticated');
|
|
1581
|
+
if (auth.google_gemini.email) {
|
|
1582
|
+
console.log(` Account: ${auth.google_gemini.email}`);
|
|
1583
|
+
}
|
|
1584
|
+
if (auth.google_gemini.expires_at) {
|
|
1585
|
+
const expires = new Date(auth.google_gemini.expires_at * 1000);
|
|
1586
|
+
console.log(` Session expires: ${expires.toLocaleString()}`);
|
|
1587
|
+
}
|
|
1588
|
+
} else if (providerName) {
|
|
865
1589
|
console.log(
|
|
866
|
-
` ⊘
|
|
1590
|
+
` ⊘ Google Gemini authentication not configured. Run "keystone auth login gemini" to authenticate.`
|
|
867
1591
|
);
|
|
868
1592
|
}
|
|
869
1593
|
}
|
|
870
1594
|
|
|
871
|
-
if (
|
|
1595
|
+
if (
|
|
1596
|
+
!auth.github_token &&
|
|
1597
|
+
!auth.openai_api_key &&
|
|
1598
|
+
!auth.openai_chatgpt &&
|
|
1599
|
+
!auth.anthropic_api_key &&
|
|
1600
|
+
!auth.anthropic_claude &&
|
|
1601
|
+
!auth.google_gemini &&
|
|
1602
|
+
!providerName
|
|
1603
|
+
) {
|
|
872
1604
|
console.log(' ⊘ No providers configured. Run "keystone auth login" to authenticate.');
|
|
873
1605
|
}
|
|
874
1606
|
});
|
|
@@ -877,29 +1609,42 @@ auth
|
|
|
877
1609
|
.command('logout')
|
|
878
1610
|
.description('Logout and clear authentication tokens')
|
|
879
1611
|
.argument('[provider]', 'Authentication provider')
|
|
880
|
-
.
|
|
881
|
-
'-p, --provider <provider>',
|
|
882
|
-
'Authentication provider (deprecated, use positional argument)'
|
|
883
|
-
)
|
|
884
|
-
.action(async (providerArg, options) => {
|
|
1612
|
+
.action(async (provider) => {
|
|
885
1613
|
const { AuthManager } = await import('./utils/auth-manager.ts');
|
|
886
|
-
const
|
|
1614
|
+
const providerName = provider?.toLowerCase();
|
|
887
1615
|
|
|
888
|
-
|
|
1616
|
+
const auth = AuthManager.load();
|
|
1617
|
+
|
|
1618
|
+
if (!providerName || providerName === 'github' || providerName === 'copilot') {
|
|
889
1619
|
AuthManager.save({
|
|
890
1620
|
github_token: undefined,
|
|
891
1621
|
copilot_token: undefined,
|
|
892
1622
|
copilot_expires_at: undefined,
|
|
893
1623
|
});
|
|
894
1624
|
console.log('✓ Successfully logged out of GitHub.');
|
|
895
|
-
} else if (
|
|
896
|
-
AuthManager.save({
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
console.log(
|
|
1625
|
+
} else if (providerName === 'openai' || providerName === 'openai-chatgpt') {
|
|
1626
|
+
AuthManager.save({
|
|
1627
|
+
openai_api_key: providerName === 'openai' ? undefined : auth.openai_api_key,
|
|
1628
|
+
openai_chatgpt: undefined,
|
|
1629
|
+
});
|
|
1630
|
+
console.log(
|
|
1631
|
+
`✓ Successfully cleared ${providerName === 'openai' ? 'OpenAI API key and ' : ''}ChatGPT session.`
|
|
1632
|
+
);
|
|
1633
|
+
} else if (providerName === 'anthropic' || providerName === 'anthropic-claude') {
|
|
1634
|
+
AuthManager.save({
|
|
1635
|
+
anthropic_api_key: providerName === 'anthropic' ? undefined : auth.anthropic_api_key,
|
|
1636
|
+
anthropic_claude: undefined,
|
|
1637
|
+
});
|
|
1638
|
+
console.log(
|
|
1639
|
+
`✓ Successfully cleared ${providerName === 'anthropic' ? 'Anthropic API key and ' : ''}Claude session.`
|
|
1640
|
+
);
|
|
1641
|
+
} else if (providerName === 'gemini' || providerName === 'google-gemini') {
|
|
1642
|
+
AuthManager.save({
|
|
1643
|
+
google_gemini: undefined,
|
|
1644
|
+
});
|
|
1645
|
+
console.log('✓ Successfully cleared Google Gemini session.');
|
|
901
1646
|
} else {
|
|
902
|
-
console.error(`✗ Unknown provider: ${
|
|
1647
|
+
console.error(`✗ Unknown provider: ${providerName}`);
|
|
903
1648
|
process.exit(1);
|
|
904
1649
|
}
|
|
905
1650
|
});
|
|
@@ -979,7 +1724,12 @@ _keystone() {
|
|
|
979
1724
|
validate)
|
|
980
1725
|
_arguments ':path:_files'
|
|
981
1726
|
;;
|
|
982
|
-
resume
|
|
1727
|
+
resume)
|
|
1728
|
+
_arguments \\
|
|
1729
|
+
'(-i --input)'{-i,--input}'[Input values]:key=value' \\
|
|
1730
|
+
':run_id:__keystone_runs'
|
|
1731
|
+
;;
|
|
1732
|
+
logs)
|
|
983
1733
|
_arguments ':run_id:__keystone_runs'
|
|
984
1734
|
;;
|
|
985
1735
|
auth)
|