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.
Files changed (104) hide show
  1. package/README.md +486 -54
  2. package/package.json +8 -2
  3. package/src/__fixtures__/index.ts +100 -0
  4. package/src/cli.ts +841 -91
  5. package/src/db/memory-db.ts +35 -1
  6. package/src/db/workflow-db.test.ts +24 -0
  7. package/src/db/workflow-db.ts +484 -14
  8. package/src/expression/evaluator.ts +68 -4
  9. package/src/parser/agent-parser.ts +6 -3
  10. package/src/parser/config-schema.ts +38 -2
  11. package/src/parser/schema.ts +192 -7
  12. package/src/parser/test-schema.ts +29 -0
  13. package/src/parser/workflow-parser.test.ts +54 -0
  14. package/src/parser/workflow-parser.ts +153 -7
  15. package/src/runner/aggregate-error.test.ts +57 -0
  16. package/src/runner/aggregate-error.ts +46 -0
  17. package/src/runner/audit-verification.test.ts +2 -2
  18. package/src/runner/auto-heal.test.ts +1 -1
  19. package/src/runner/blueprint-executor.test.ts +63 -0
  20. package/src/runner/blueprint-executor.ts +157 -0
  21. package/src/runner/concurrency-limit.test.ts +82 -0
  22. package/src/runner/debug-repl.ts +18 -3
  23. package/src/runner/durable-timers.test.ts +200 -0
  24. package/src/runner/engine-executor.test.ts +464 -0
  25. package/src/runner/engine-executor.ts +491 -0
  26. package/src/runner/foreach-executor.ts +30 -12
  27. package/src/runner/llm-adapter.test.ts +282 -5
  28. package/src/runner/llm-adapter.ts +581 -8
  29. package/src/runner/llm-clarification.test.ts +79 -21
  30. package/src/runner/llm-errors.ts +83 -0
  31. package/src/runner/llm-executor.test.ts +258 -219
  32. package/src/runner/llm-executor.ts +226 -29
  33. package/src/runner/mcp-client.ts +70 -3
  34. package/src/runner/mcp-manager.test.ts +52 -52
  35. package/src/runner/mcp-manager.ts +12 -5
  36. package/src/runner/mcp-server.test.ts +117 -78
  37. package/src/runner/mcp-server.ts +13 -4
  38. package/src/runner/optimization-runner.ts +48 -31
  39. package/src/runner/reflexion.test.ts +1 -1
  40. package/src/runner/resource-pool.test.ts +113 -0
  41. package/src/runner/resource-pool.ts +164 -0
  42. package/src/runner/shell-executor.ts +130 -32
  43. package/src/runner/standard-tools-execution.test.ts +39 -0
  44. package/src/runner/standard-tools-integration.test.ts +36 -36
  45. package/src/runner/standard-tools.test.ts +18 -0
  46. package/src/runner/standard-tools.ts +174 -93
  47. package/src/runner/step-executor.test.ts +176 -16
  48. package/src/runner/step-executor.ts +534 -83
  49. package/src/runner/stream-utils.test.ts +14 -0
  50. package/src/runner/subflow-outputs.test.ts +103 -0
  51. package/src/runner/test-harness.ts +161 -0
  52. package/src/runner/tool-integration.test.ts +73 -79
  53. package/src/runner/workflow-runner.test.ts +549 -15
  54. package/src/runner/workflow-runner.ts +1448 -79
  55. package/src/runner/workflow-subflows.test.ts +255 -0
  56. package/src/templates/agents/keystone-architect.md +17 -12
  57. package/src/templates/agents/tester.md +21 -0
  58. package/src/templates/child-rollback.yaml +11 -0
  59. package/src/templates/decompose-implement.yaml +53 -0
  60. package/src/templates/decompose-problem.yaml +159 -0
  61. package/src/templates/decompose-research.yaml +52 -0
  62. package/src/templates/decompose-review.yaml +51 -0
  63. package/src/templates/dev.yaml +134 -0
  64. package/src/templates/engine-example.yaml +33 -0
  65. package/src/templates/fan-out-fan-in.yaml +61 -0
  66. package/src/templates/memory-service.yaml +1 -1
  67. package/src/templates/parent-rollback.yaml +16 -0
  68. package/src/templates/robust-automation.yaml +1 -1
  69. package/src/templates/scaffold-feature.yaml +29 -27
  70. package/src/templates/scaffold-generate.yaml +41 -0
  71. package/src/templates/scaffold-plan.yaml +53 -0
  72. package/src/types/status.ts +3 -0
  73. package/src/ui/dashboard.tsx +4 -3
  74. package/src/utils/assets.macro.ts +36 -0
  75. package/src/utils/auth-manager.ts +585 -8
  76. package/src/utils/blueprint-utils.test.ts +49 -0
  77. package/src/utils/blueprint-utils.ts +80 -0
  78. package/src/utils/circuit-breaker.test.ts +177 -0
  79. package/src/utils/circuit-breaker.ts +160 -0
  80. package/src/utils/config-loader.test.ts +100 -13
  81. package/src/utils/config-loader.ts +44 -17
  82. package/src/utils/constants.ts +62 -0
  83. package/src/utils/error-renderer.test.ts +267 -0
  84. package/src/utils/error-renderer.ts +320 -0
  85. package/src/utils/json-parser.test.ts +4 -0
  86. package/src/utils/json-parser.ts +18 -1
  87. package/src/utils/mermaid.ts +4 -0
  88. package/src/utils/paths.test.ts +46 -0
  89. package/src/utils/paths.ts +70 -0
  90. package/src/utils/process-sandbox.test.ts +128 -0
  91. package/src/utils/process-sandbox.ts +293 -0
  92. package/src/utils/rate-limiter.test.ts +143 -0
  93. package/src/utils/rate-limiter.ts +221 -0
  94. package/src/utils/redactor.test.ts +23 -15
  95. package/src/utils/redactor.ts +65 -25
  96. package/src/utils/resource-loader.test.ts +54 -0
  97. package/src/utils/resource-loader.ts +158 -0
  98. package/src/utils/sandbox.test.ts +69 -4
  99. package/src/utils/sandbox.ts +69 -6
  100. package/src/utils/schema-validator.ts +65 -0
  101. package/src/utils/workflow-registry.test.ts +57 -0
  102. package/src/utils/workflow-registry.ts +45 -25
  103. /package/src/expression/{evaluator.audit.test.ts → evaluator-audit.test.ts} +0 -0
  104. /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
- .action(async (pathArg) => {
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
- console.error(
183
- ` ✗ ${file.padEnd(40)} ${error instanceof Error ? error.message : String(error)}`
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
- .action(async (workflowPath, options) => {
232
- // Parse inputs
233
- const inputs: Record<string, unknown> = {};
234
- if (options.input) {
235
- for (const pair of options.input) {
236
- const index = pair.indexOf('=');
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
- const resolvedPath = WorkflowRegistry.resolvePath(workflowPath);
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
- console.error(
275
- '✗ Failed to execute workflow:',
276
- error instanceof Error ? error.message : error
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
- // 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
- }
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', '30')
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', '30')
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
- .command('config')
711
- .description('Show current configuration')
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
- .action(async (providerArg, options) => {
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 provider = (options.provider || providerArg).toLowerCase();
1352
+ const providerName = provider.toLowerCase();
738
1353
 
739
- if (provider === 'github') {
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 (provider === 'openai' || provider === 'anthropic') {
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 ${provider.toUpperCase()}`);
799
- console.log(` Please provide your ${provider.toUpperCase()} API key.\n`);
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 (provider === 'openai') {
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 ${provider.toUpperCase()} API key.`);
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: ${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
- .option('-p, --provider <provider>', 'Authentication provider')
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 provider = (options.provider || providerArg)?.toLowerCase();
1522
+ const providerName = provider?.toLowerCase();
834
1523
 
835
1524
  console.log('\n🏛️ Authentication Status:');
836
1525
 
837
- if (!provider || provider === 'github' || provider === 'copilot') {
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 (provider) {
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 (!provider || provider === 'openai') {
1540
+ if (!providerName || providerName === 'openai' || providerName === 'openai-chatgpt') {
852
1541
  if (auth.openai_api_key) {
853
1542
  console.log(' ✓ OpenAI API key configured');
854
- } else if (provider) {
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 API key not configured. Run "keystone auth login openai" to authenticate.`
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 (!provider || provider === 'anthropic') {
1559
+ if (!providerName || providerName === 'anthropic' || providerName === 'anthropic-claude') {
862
1560
  if (auth.anthropic_api_key) {
863
1561
  console.log(' ✓ Anthropic API key configured');
864
- } else if (provider) {
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
- ` ⊘ Anthropic API key not configured. Run "keystone auth login anthropic" to authenticate.`
1590
+ ` ⊘ Google Gemini authentication not configured. Run "keystone auth login gemini" to authenticate.`
867
1591
  );
868
1592
  }
869
1593
  }
870
1594
 
871
- if (!auth.github_token && !auth.openai_api_key && !auth.anthropic_api_key && !provider) {
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
- .option(
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 provider = (options.provider || providerArg)?.toLowerCase();
1614
+ const providerName = provider?.toLowerCase();
887
1615
 
888
- if (!provider || provider === 'github' || provider === 'copilot') {
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 (provider === 'openai') {
896
- AuthManager.save({ openai_api_key: undefined });
897
- console.log(' Successfully cleared OpenAI API key.');
898
- } else if (provider === 'anthropic') {
899
- AuthManager.save({ anthropic_api_key: undefined });
900
- console.log('✓ Successfully cleared Anthropic API key.');
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: ${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|logs)
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)