keystone-cli 0.8.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) 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 +809 -90
  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 +469 -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 +489 -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-integration.test.ts +36 -36
  44. package/src/runner/standard-tools.test.ts +18 -0
  45. package/src/runner/standard-tools.ts +110 -37
  46. package/src/runner/step-executor.test.ts +176 -16
  47. package/src/runner/step-executor.ts +530 -86
  48. package/src/runner/stream-utils.test.ts +14 -0
  49. package/src/runner/subflow-outputs.test.ts +103 -0
  50. package/src/runner/test-harness.ts +161 -0
  51. package/src/runner/tool-integration.test.ts +73 -79
  52. package/src/runner/workflow-runner.test.ts +492 -15
  53. package/src/runner/workflow-runner.ts +1438 -79
  54. package/src/runner/workflow-subflows.test.ts +255 -0
  55. package/src/templates/agents/keystone-architect.md +19 -14
  56. package/src/templates/agents/tester.md +21 -0
  57. package/src/templates/batch-processor.yaml +1 -1
  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/loop-parallel.yaml +1 -1
  67. package/src/templates/memory-service.yaml +1 -1
  68. package/src/templates/parent-rollback.yaml +16 -0
  69. package/src/templates/robust-automation.yaml +1 -1
  70. package/src/templates/scaffold-feature.yaml +29 -27
  71. package/src/templates/scaffold-generate.yaml +41 -0
  72. package/src/templates/scaffold-plan.yaml +53 -0
  73. package/src/types/status.ts +3 -0
  74. package/src/ui/dashboard.tsx +4 -3
  75. package/src/utils/assets.macro.ts +36 -0
  76. package/src/utils/auth-manager.ts +585 -8
  77. package/src/utils/blueprint-utils.test.ts +49 -0
  78. package/src/utils/blueprint-utils.ts +80 -0
  79. package/src/utils/circuit-breaker.test.ts +177 -0
  80. package/src/utils/circuit-breaker.ts +160 -0
  81. package/src/utils/config-loader.test.ts +100 -13
  82. package/src/utils/config-loader.ts +44 -17
  83. package/src/utils/constants.ts +62 -0
  84. package/src/utils/error-renderer.test.ts +267 -0
  85. package/src/utils/error-renderer.ts +320 -0
  86. package/src/utils/json-parser.test.ts +4 -0
  87. package/src/utils/json-parser.ts +18 -1
  88. package/src/utils/mermaid.ts +4 -0
  89. package/src/utils/paths.test.ts +46 -0
  90. package/src/utils/paths.ts +70 -0
  91. package/src/utils/process-sandbox.test.ts +128 -0
  92. package/src/utils/process-sandbox.ts +293 -0
  93. package/src/utils/rate-limiter.test.ts +143 -0
  94. package/src/utils/rate-limiter.ts +221 -0
  95. package/src/utils/redactor.test.ts +23 -15
  96. package/src/utils/redactor.ts +65 -25
  97. package/src/utils/resource-loader.test.ts +54 -0
  98. package/src/utils/resource-loader.ts +158 -0
  99. package/src/utils/sandbox.test.ts +69 -4
  100. package/src/utils/sandbox.ts +69 -6
  101. package/src/utils/schema-validator.ts +65 -0
  102. package/src/utils/workflow-registry.test.ts +57 -0
  103. package/src/utils/workflow-registry.ts +45 -25
  104. /package/src/expression/{evaluator.audit.test.ts → evaluator-audit.test.ts} +0 -0
  105. /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,29 +387,16 @@ 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')
390
+ .option('--no-dedup', 'Disable idempotency/deduplication')
231
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')
232
393
  .action(async (workflowPathArg, options) => {
233
- // Parse inputs
234
- const inputs: Record<string, unknown> = {};
235
- if (options.input) {
236
- for (const pair of options.input) {
237
- const index = pair.indexOf('=');
238
- if (index > 0) {
239
- const key = pair.slice(0, index);
240
- const value = pair.slice(index + 1);
241
- // Try to parse as JSON, otherwise use as string
242
- try {
243
- inputs[key] = JSON.parse(value);
244
- } catch {
245
- inputs[key] = value;
246
- }
247
- }
248
- }
249
- }
394
+ const inputs = parseInputs(options.input);
395
+ let resolvedPath: string | undefined;
250
396
 
251
397
  // Load and validate workflow
252
398
  try {
253
- const resolvedPath = WorkflowRegistry.resolvePath(workflowPathArg);
399
+ resolvedPath = WorkflowRegistry.resolvePath(workflowPathArg);
254
400
  const workflow = WorkflowParser.loadWorkflow(resolvedPath);
255
401
 
256
402
  // Import WorkflowRunner dynamically
@@ -286,10 +432,12 @@ program
286
432
  }
287
433
 
288
434
  const runner = new WorkflowRunner(workflow, {
289
- inputs,
435
+ inputs: resumeRunId ? undefined : inputs,
436
+ resumeInputs: resumeRunId ? inputs : undefined,
290
437
  workflowDir: dirname(resolvedPath),
291
438
  dryRun: !!options.dryRun,
292
439
  debug: !!options.debug,
440
+ dedup: options.dedup,
293
441
  resumeRunId,
294
442
  logger,
295
443
  });
@@ -302,10 +450,108 @@ program
302
450
  }
303
451
  process.exit(0);
304
452
  } catch (error) {
305
- console.error(
306
- '✗ Failed to execute workflow:',
307
- error instanceof Error ? error.message : error
308
- );
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);
309
555
  process.exit(1);
310
556
  }
311
557
  });
@@ -345,22 +591,7 @@ program
345
591
  const resolvedPath = WorkflowRegistry.resolvePath(workflowPath);
346
592
  const workflow = WorkflowParser.loadWorkflow(resolvedPath);
347
593
 
348
- // Parse inputs
349
- const inputs: Record<string, unknown> = {};
350
- if (options.input) {
351
- for (const pair of options.input) {
352
- const index = pair.indexOf('=');
353
- if (index > 0) {
354
- const key = pair.slice(0, index);
355
- const value = pair.slice(index + 1);
356
- try {
357
- inputs[key] = JSON.parse(value);
358
- } catch {
359
- inputs[key] = value;
360
- }
361
- }
362
- }
363
- }
594
+ const inputs = parseInputs(options.input);
364
595
 
365
596
  const runner = new OptimizationRunner(workflow, {
366
597
  workflowPath: resolvedPath,
@@ -390,6 +621,7 @@ program
390
621
  .description('Resume a paused or failed workflow run')
391
622
  .argument('<run_id>', 'Run ID to resume')
392
623
  .option('-w, --workflow <path>', 'Path to workflow file (auto-detected if not specified)')
624
+ .option('-i, --input <key=value...>', 'Input values for resume')
393
625
  .action(async (runId, options) => {
394
626
  try {
395
627
  const db = new WorkflowDb();
@@ -431,8 +663,10 @@ program
431
663
  // Import WorkflowRunner dynamically
432
664
  const { WorkflowRunner } = await import('./runner/workflow-runner.ts');
433
665
  const logger = new ConsoleLogger();
666
+ const inputs = parseInputs(options.input);
434
667
  const runner = new WorkflowRunner(workflow, {
435
668
  resumeRunId: runId,
669
+ resumeInputs: inputs,
436
670
  workflowDir: dirname(workflowPath),
437
671
  logger,
438
672
  });
@@ -531,6 +765,124 @@ program
531
765
  }
532
766
  });
533
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
+
534
886
  async function showRunLogs(run: WorkflowRun, db: WorkflowDb, verbose: boolean) {
535
887
  console.log(`\n🏛️ Run: ${run.workflow_name} (${run.id})`);
536
888
  console.log(` Status: ${run.status}`);
@@ -609,7 +961,7 @@ async function performMaintenance(days: number) {
609
961
  program
610
962
  .command('prune')
611
963
  .description('Delete old workflow runs from the database (alias for maintenance)')
612
- .option('--days <number>', 'Days to keep', '30')
964
+ .option('--days <number>', 'Days to keep', String(defaultRetentionDays))
613
965
  .action(async (options) => {
614
966
  const days = Number.parseInt(options.days, 10);
615
967
  await performMaintenance(days);
@@ -618,12 +970,236 @@ program
618
970
  program
619
971
  .command('maintenance')
620
972
  .description('Perform database maintenance (prune old runs and vacuum)')
621
- .option('--days <days>', 'Delete runs older than this many days', '30')
973
+ .option('--days <days>', 'Delete runs older than this many days', String(defaultRetentionDays))
622
974
  .action(async (options) => {
623
975
  const days = Number.parseInt(options.days, 10);
624
976
  await performMaintenance(days);
625
977
  });
626
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
+
627
1203
  // ===== keystone ui =====
628
1204
  program
629
1205
  .command('ui')
@@ -737,15 +1313,26 @@ mcp
737
1313
  });
738
1314
 
739
1315
  // ===== keystone config =====
740
- program
741
- .command('config')
742
- .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')
743
1322
  .action(async () => {
744
1323
  const { ConfigLoader } = await import('./utils/config-loader.ts');
1324
+ const { PathResolver } = await import('./utils/paths.ts');
745
1325
  try {
746
1326
  const config = ConfigLoader.load();
747
1327
  console.log('\n🏛️ Keystone Configuration:');
748
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
+ }
749
1336
  } catch (error) {
750
1337
  console.error('✗ Failed to load config:', error instanceof Error ? error.message : error);
751
1338
  }
@@ -758,16 +1345,13 @@ auth
758
1345
  .command('login')
759
1346
  .description('Login to an authentication provider')
760
1347
  .argument('[provider]', 'Authentication provider', 'github')
761
- .option(
762
- '-p, --provider <provider>',
763
- 'Authentication provider (deprecated, use positional argument)'
764
- )
765
1348
  .option('-t, --token <token>', 'Personal Access Token (if not using interactive mode)')
766
- .action(async (providerArg, options) => {
1349
+ .option('--project <project_id>', 'Google Cloud project ID (Gemini OAuth)')
1350
+ .action(async (provider, options) => {
767
1351
  const { AuthManager } = await import('./utils/auth-manager.ts');
768
- const provider = (options.provider || providerArg).toLowerCase();
1352
+ const providerName = provider.toLowerCase();
769
1353
 
770
- if (provider === 'github') {
1354
+ if (providerName === 'github') {
771
1355
  let token = options.token;
772
1356
 
773
1357
  if (!token) {
@@ -822,12 +1406,87 @@ auth
822
1406
  console.error('✗ No token provided.');
823
1407
  process.exit(1);
824
1408
  }
825
- } 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') {
826
1485
  let key = options.token; // Use --token if provided as the API key
827
1486
 
828
1487
  if (!key) {
829
- console.log(`\n🔑 Login to ${provider.toUpperCase()}`);
830
- 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`);
831
1490
  const prompt = 'API Key: ';
832
1491
  process.stdout.write(prompt);
833
1492
  for await (const line of console) {
@@ -837,18 +1496,18 @@ auth
837
1496
  }
838
1497
 
839
1498
  if (key) {
840
- if (provider === 'openai') {
1499
+ if (providerName === 'openai') {
841
1500
  AuthManager.save({ openai_api_key: key });
842
1501
  } else {
843
1502
  AuthManager.save({ anthropic_api_key: key });
844
1503
  }
845
- console.log(`\n✓ Successfully saved ${provider.toUpperCase()} API key.`);
1504
+ console.log(`\n✓ Successfully saved ${providerName.toUpperCase()} API key.`);
846
1505
  } else {
847
1506
  console.error('✗ No API key provided.');
848
1507
  process.exit(1);
849
1508
  }
850
1509
  } else {
851
- console.error(`✗ Unsupported provider: ${provider}`);
1510
+ console.error(`✗ Unsupported provider: ${providerName}`);
852
1511
  process.exit(1);
853
1512
  }
854
1513
  });
@@ -857,49 +1516,91 @@ auth
857
1516
  .command('status')
858
1517
  .description('Show authentication status')
859
1518
  .argument('[provider]', 'Authentication provider')
860
- .option('-p, --provider <provider>', 'Authentication provider')
861
- .action(async (providerArg, options) => {
1519
+ .action(async (provider) => {
862
1520
  const { AuthManager } = await import('./utils/auth-manager.ts');
863
1521
  const auth = AuthManager.load();
864
- const provider = (options.provider || providerArg)?.toLowerCase();
1522
+ const providerName = provider?.toLowerCase();
865
1523
 
866
1524
  console.log('\n🏛️ Authentication Status:');
867
1525
 
868
- if (!provider || provider === 'github' || provider === 'copilot') {
1526
+ if (!providerName || providerName === 'github' || providerName === 'copilot') {
869
1527
  if (auth.github_token) {
870
1528
  console.log(' ✓ Logged into GitHub');
871
1529
  if (auth.copilot_expires_at) {
872
1530
  const expires = new Date(auth.copilot_expires_at * 1000);
873
1531
  console.log(` ✓ Copilot session expires: ${expires.toLocaleString()}`);
874
1532
  }
875
- } else if (provider) {
1533
+ } else if (providerName) {
876
1534
  console.log(
877
1535
  ` ⊘ Not logged into GitHub. Run "keystone auth login github" to authenticate.`
878
1536
  );
879
1537
  }
880
1538
  }
881
1539
 
882
- if (!provider || provider === 'openai') {
1540
+ if (!providerName || providerName === 'openai' || providerName === 'openai-chatgpt') {
883
1541
  if (auth.openai_api_key) {
884
1542
  console.log(' ✓ OpenAI API key configured');
885
- } 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) {
886
1553
  console.log(
887
- ` ⊘ 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.`
888
1555
  );
889
1556
  }
890
1557
  }
891
1558
 
892
- if (!provider || provider === 'anthropic') {
1559
+ if (!providerName || providerName === 'anthropic' || providerName === 'anthropic-claude') {
893
1560
  if (auth.anthropic_api_key) {
894
1561
  console.log(' ✓ Anthropic API key configured');
895
- } 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) {
896
1572
  console.log(
897
- ` ⊘ Anthropic API key not configured. Run "keystone auth login anthropic" to authenticate.`
1573
+ ` ⊘ Anthropic authentication not configured. Run "keystone auth login anthropic" or "keystone auth login anthropic-claude" to authenticate.`
898
1574
  );
899
1575
  }
900
1576
  }
901
1577
 
902
- if (!auth.github_token && !auth.openai_api_key && !auth.anthropic_api_key && !provider) {
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) {
1589
+ console.log(
1590
+ ` ⊘ Google Gemini authentication not configured. Run "keystone auth login gemini" to authenticate.`
1591
+ );
1592
+ }
1593
+ }
1594
+
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
+ ) {
903
1604
  console.log(' ⊘ No providers configured. Run "keystone auth login" to authenticate.');
904
1605
  }
905
1606
  });
@@ -908,29 +1609,42 @@ auth
908
1609
  .command('logout')
909
1610
  .description('Logout and clear authentication tokens')
910
1611
  .argument('[provider]', 'Authentication provider')
911
- .option(
912
- '-p, --provider <provider>',
913
- 'Authentication provider (deprecated, use positional argument)'
914
- )
915
- .action(async (providerArg, options) => {
1612
+ .action(async (provider) => {
916
1613
  const { AuthManager } = await import('./utils/auth-manager.ts');
917
- const provider = (options.provider || providerArg)?.toLowerCase();
1614
+ const providerName = provider?.toLowerCase();
918
1615
 
919
- if (!provider || provider === 'github' || provider === 'copilot') {
1616
+ const auth = AuthManager.load();
1617
+
1618
+ if (!providerName || providerName === 'github' || providerName === 'copilot') {
920
1619
  AuthManager.save({
921
1620
  github_token: undefined,
922
1621
  copilot_token: undefined,
923
1622
  copilot_expires_at: undefined,
924
1623
  });
925
1624
  console.log('✓ Successfully logged out of GitHub.');
926
- } else if (provider === 'openai') {
927
- AuthManager.save({ openai_api_key: undefined });
928
- console.log(' Successfully cleared OpenAI API key.');
929
- } else if (provider === 'anthropic') {
930
- AuthManager.save({ anthropic_api_key: undefined });
931
- 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.');
932
1646
  } else {
933
- console.error(`✗ Unknown provider: ${provider}`);
1647
+ console.error(`✗ Unknown provider: ${providerName}`);
934
1648
  process.exit(1);
935
1649
  }
936
1650
  });
@@ -1010,7 +1724,12 @@ _keystone() {
1010
1724
  validate)
1011
1725
  _arguments ':path:_files'
1012
1726
  ;;
1013
- resume|logs)
1727
+ resume)
1728
+ _arguments \\
1729
+ '(-i --input)'{-i,--input}'[Input values]:key=value' \\
1730
+ ':run_id:__keystone_runs'
1731
+ ;;
1732
+ logs)
1014
1733
  _arguments ':run_id:__keystone_runs'
1015
1734
  ;;
1016
1735
  auth)