@bilalimamoglu/sift 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/cli.ts
3
+ // src/cli-app.ts
4
4
  import { createRequire } from "module";
5
5
  import { cac } from "cac";
6
6
 
@@ -13,12 +13,17 @@ import YAML from "yaml";
13
13
  import os from "os";
14
14
  import path from "path";
15
15
  var DEFAULT_CONFIG_FILENAME = "sift.config.yaml";
16
- var DEFAULT_CONFIG_SEARCH_PATHS = [
17
- path.resolve(process.cwd(), "sift.config.yaml"),
18
- path.resolve(process.cwd(), "sift.config.yml"),
19
- path.join(os.homedir(), ".config", "sift", "config.yaml"),
20
- path.join(os.homedir(), ".config", "sift", "config.yml")
21
- ];
16
+ function getDefaultGlobalConfigPath() {
17
+ return path.join(os.homedir(), ".config", "sift", "config.yaml");
18
+ }
19
+ function getDefaultConfigSearchPaths() {
20
+ return [
21
+ path.resolve(process.cwd(), "sift.config.yaml"),
22
+ path.resolve(process.cwd(), "sift.config.yml"),
23
+ getDefaultGlobalConfigPath(),
24
+ path.join(os.homedir(), ".config", "sift", "config.yml")
25
+ ];
26
+ }
22
27
  var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
23
28
  var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
24
29
  var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
@@ -32,7 +37,7 @@ function findConfigPath(explicitPath) {
32
37
  }
33
38
  return resolved;
34
39
  }
35
- for (const candidate of DEFAULT_CONFIG_SEARCH_PATHS) {
40
+ for (const candidate of getDefaultConfigSearchPaths()) {
36
41
  if (fs.existsSync(candidate)) {
37
42
  return candidate;
38
43
  }
@@ -339,8 +344,11 @@ function resolveConfig(options = {}) {
339
344
  import fs2 from "fs";
340
345
  import path3 from "path";
341
346
  import YAML2 from "yaml";
342
- function writeExampleConfig(targetPath) {
343
- const resolved = path3.resolve(targetPath ?? DEFAULT_CONFIG_FILENAME);
347
+ function writeExampleConfig(options = {}) {
348
+ if (options.global && options.targetPath) {
349
+ throw new Error("Use either --path <path> or --global, not both.");
350
+ }
351
+ const resolved = options.global ? getDefaultGlobalConfigPath() : path3.resolve(options.targetPath ?? DEFAULT_CONFIG_FILENAME);
344
352
  if (fs2.existsSync(resolved)) {
345
353
  throw new Error(`Config file already exists at ${resolved}`);
346
354
  }
@@ -349,6 +357,414 @@ function writeExampleConfig(targetPath) {
349
357
  fs2.writeFileSync(resolved, yaml, "utf8");
350
358
  return resolved;
351
359
  }
360
+ function writeConfigFile(options) {
361
+ const resolved = path3.resolve(options.targetPath);
362
+ if (!options.overwrite && fs2.existsSync(resolved)) {
363
+ throw new Error(`Config file already exists at ${resolved}`);
364
+ }
365
+ const yaml = YAML2.stringify(options.config);
366
+ fs2.mkdirSync(path3.dirname(resolved), { recursive: true });
367
+ fs2.writeFileSync(resolved, yaml, {
368
+ encoding: "utf8",
369
+ mode: 384
370
+ });
371
+ try {
372
+ fs2.chmodSync(resolved, 384);
373
+ } catch {
374
+ }
375
+ return resolved;
376
+ }
377
+
378
+ // src/ui/presentation.ts
379
+ import pc from "picocolors";
380
+ function applyColor(enabled, formatter, value) {
381
+ return enabled ? formatter(value) : value;
382
+ }
383
+ function createPresentation(useColor) {
384
+ return {
385
+ useColor,
386
+ banner(_version) {
387
+ const mark = [
388
+ " \\\\ //",
389
+ " \\\\//",
390
+ " | |",
391
+ " o"
392
+ ].map((line) => applyColor(useColor, pc.cyan, line)).join("\n");
393
+ const tagline = applyColor(useColor, pc.dim, "Trim the noise. Keep the signal.");
394
+ return `${mark}
395
+ ${tagline}`;
396
+ },
397
+ welcome(text) {
398
+ return useColor ? `${pc.bold(pc.cyan("Welcome to sift."))} ${text}` : `Welcome to sift. ${text}`;
399
+ },
400
+ success(text) {
401
+ return useColor ? `${pc.green("\u2713")} ${text}` : text;
402
+ },
403
+ warning(text) {
404
+ return useColor ? `${pc.yellow("!")} ${text}` : text;
405
+ },
406
+ error(text) {
407
+ return useColor ? `${pc.red("x")} ${text}` : text;
408
+ },
409
+ info(text) {
410
+ return useColor ? `${pc.cyan("\u2022")} ${text}` : text;
411
+ },
412
+ note(text) {
413
+ return applyColor(useColor, pc.dim, text);
414
+ },
415
+ section(text) {
416
+ return applyColor(useColor, (value) => pc.bold(pc.yellow(value)), text);
417
+ },
418
+ labelValue(label, value) {
419
+ return `${applyColor(useColor, (entry) => pc.bold(pc.cyan(entry)), label)}: ${value}`;
420
+ },
421
+ command(text) {
422
+ return applyColor(useColor, (value) => pc.bold(pc.cyan(value)), text);
423
+ }
424
+ };
425
+ }
426
+
427
+ // src/commands/config-setup.ts
428
+ import fs3 from "fs";
429
+ import path4 from "path";
430
+ import { emitKeypressEvents } from "readline";
431
+ import { createInterface } from "readline/promises";
432
+ import { stderr as defaultStderr, stdin as defaultStdin2, stdout as defaultStdout } from "process";
433
+
434
+ // src/ui/terminal.ts
435
+ import { execFileSync } from "child_process";
436
+ import { clearScreenDown, cursorTo, moveCursor } from "readline";
437
+ import { stdin as defaultStdin } from "process";
438
+ function setPosixEcho(enabled) {
439
+ const command = enabled ? "echo" : "-echo";
440
+ try {
441
+ execFileSync("sh", ["-c", `stty ${command} < /dev/tty`], {
442
+ stdio: ["inherit", "inherit", "ignore"]
443
+ });
444
+ return;
445
+ } catch {
446
+ }
447
+ try {
448
+ execFileSync("stty", [command], {
449
+ stdio: ["inherit", "inherit", "ignore"]
450
+ });
451
+ } catch {
452
+ }
453
+ }
454
+ function renderSelectionBlock(args) {
455
+ return [
456
+ `${args.prompt} (use \u2191/\u2193 and Enter)`,
457
+ ...args.options.map(
458
+ (option, index) => `${index === args.selectedIndex ? "\u203A" : " "} ${option}${index === args.selectedIndex ? " (selected)" : ""}`
459
+ )
460
+ ];
461
+ }
462
+ async function promptSelect(args) {
463
+ const { input, output, prompt, options } = args;
464
+ const stream = output;
465
+ const selectedLabel = args.selectedLabel ?? prompt;
466
+ let index = 0;
467
+ let previousLineCount = 0;
468
+ const render = () => {
469
+ if (previousLineCount > 0) {
470
+ moveCursor(stream, 0, -previousLineCount);
471
+ cursorTo(stream, 0);
472
+ clearScreenDown(stream);
473
+ }
474
+ const lines = renderSelectionBlock({
475
+ prompt,
476
+ options,
477
+ selectedIndex: index
478
+ });
479
+ output.write(`${lines.join("\n")}
480
+ `);
481
+ previousLineCount = lines.length;
482
+ };
483
+ const cleanup = (selected) => {
484
+ if (previousLineCount > 0) {
485
+ moveCursor(stream, 0, -previousLineCount);
486
+ cursorTo(stream, 0);
487
+ clearScreenDown(stream);
488
+ }
489
+ if (selected) {
490
+ output.write(`${selectedLabel}: ${selected}
491
+ `);
492
+ }
493
+ };
494
+ input.resume();
495
+ const wasRaw = Boolean(input.isRaw);
496
+ input.setRawMode?.(true);
497
+ render();
498
+ return await new Promise((resolve, reject) => {
499
+ const onKeypress = (_value, key) => {
500
+ if (key.ctrl && key.name === "c") {
501
+ input.off("keypress", onKeypress);
502
+ cleanup();
503
+ input.setRawMode?.(wasRaw);
504
+ input.pause?.();
505
+ reject(new Error("Aborted."));
506
+ return;
507
+ }
508
+ if (key.name === "up") {
509
+ index = index === 0 ? options.length - 1 : index - 1;
510
+ render();
511
+ return;
512
+ }
513
+ if (key.name === "down") {
514
+ index = (index + 1) % options.length;
515
+ render();
516
+ return;
517
+ }
518
+ if (key.name === "return" || key.name === "enter") {
519
+ const selected = options[index] ?? options[0] ?? "";
520
+ input.off("keypress", onKeypress);
521
+ cleanup(selected);
522
+ input.setRawMode?.(wasRaw);
523
+ input.pause?.();
524
+ resolve(selected);
525
+ }
526
+ };
527
+ input.on("keypress", onKeypress);
528
+ });
529
+ }
530
+ async function promptSecret(args) {
531
+ const { input, output, prompt } = args;
532
+ let value = "";
533
+ const shouldToggleEcho = process.platform !== "win32" && input === defaultStdin && Boolean(defaultStdin.isTTY);
534
+ output.write(prompt);
535
+ input.resume();
536
+ const wasRaw = Boolean(input.isRaw);
537
+ input.setRawMode?.(true);
538
+ if (shouldToggleEcho) {
539
+ setPosixEcho(false);
540
+ }
541
+ return await new Promise((resolve, reject) => {
542
+ const restoreInputState = () => {
543
+ input.setRawMode?.(wasRaw);
544
+ input.pause?.();
545
+ if (shouldToggleEcho) {
546
+ setPosixEcho(true);
547
+ }
548
+ };
549
+ const onKeypress = (chunk, key) => {
550
+ if (key.ctrl && key.name === "c") {
551
+ input.off("keypress", onKeypress);
552
+ restoreInputState();
553
+ output.write("\n");
554
+ reject(new Error("Aborted."));
555
+ return;
556
+ }
557
+ if (key.name === "return" || key.name === "enter") {
558
+ input.off("keypress", onKeypress);
559
+ restoreInputState();
560
+ output.write("\n");
561
+ resolve(value);
562
+ return;
563
+ }
564
+ if (key.name === "backspace" || key.name === "delete") {
565
+ value = value.slice(0, -1);
566
+ return;
567
+ }
568
+ if (!key.ctrl && chunk.length > 0) {
569
+ value += chunk;
570
+ }
571
+ };
572
+ input.on("keypress", onKeypress);
573
+ });
574
+ }
575
+
576
+ // src/commands/config-setup.ts
577
+ function createTerminalIO() {
578
+ let rl;
579
+ function getInterface() {
580
+ if (!rl) {
581
+ rl = createInterface({
582
+ input: defaultStdin2,
583
+ output: defaultStdout,
584
+ terminal: true
585
+ });
586
+ }
587
+ return rl;
588
+ }
589
+ async function select(prompt, options) {
590
+ emitKeypressEvents(defaultStdin2);
591
+ return await promptSelect({
592
+ input: defaultStdin2,
593
+ output: defaultStdout,
594
+ prompt,
595
+ options,
596
+ selectedLabel: "Provider"
597
+ });
598
+ }
599
+ async function secret(prompt) {
600
+ emitKeypressEvents(defaultStdin2);
601
+ return await promptSecret({
602
+ input: defaultStdin2,
603
+ output: defaultStdout,
604
+ prompt
605
+ });
606
+ }
607
+ return {
608
+ stdinIsTTY: Boolean(defaultStdin2.isTTY),
609
+ stdoutIsTTY: Boolean(defaultStdout.isTTY),
610
+ ask(prompt) {
611
+ return getInterface().question(prompt);
612
+ },
613
+ select,
614
+ secret,
615
+ write(message) {
616
+ defaultStdout.write(message);
617
+ },
618
+ error(message) {
619
+ defaultStderr.write(message);
620
+ },
621
+ close() {
622
+ rl?.close();
623
+ }
624
+ };
625
+ }
626
+ function resolveSetupPath(targetPath) {
627
+ return targetPath ? path4.resolve(targetPath) : getDefaultGlobalConfigPath();
628
+ }
629
+ function buildOpenAISetupConfig(apiKey) {
630
+ return {
631
+ ...defaultConfig,
632
+ provider: {
633
+ ...defaultConfig.provider,
634
+ provider: "openai",
635
+ model: "gpt-5-nano",
636
+ baseUrl: "https://api.openai.com/v1",
637
+ apiKey
638
+ }
639
+ };
640
+ }
641
+ function getSetupPresenter(io) {
642
+ return createPresentation(io.stdoutIsTTY);
643
+ }
644
+ async function promptForProvider(io) {
645
+ if (io.select) {
646
+ const choice = await io.select("Select provider for this machine", ["OpenAI"]);
647
+ if (choice === "OpenAI") {
648
+ return "openai";
649
+ }
650
+ }
651
+ while (true) {
652
+ const answer = (await io.ask("Provider [OpenAI]: ")).trim().toLowerCase();
653
+ if (answer === "" || answer === "openai") {
654
+ return "openai";
655
+ }
656
+ io.error("Only OpenAI is supported in guided setup right now.\n");
657
+ }
658
+ }
659
+ async function promptForApiKey(io) {
660
+ while (true) {
661
+ const answer = (await (io.secret ? io.secret("Enter your OpenAI API key (input hidden): ") : io.ask("Enter your OpenAI API key: "))).trim();
662
+ if (answer.length > 0) {
663
+ return answer;
664
+ }
665
+ io.error("API key cannot be empty.\n");
666
+ }
667
+ }
668
+ async function promptForOverwrite(io, targetPath) {
669
+ while (true) {
670
+ const answer = (await io.ask(
671
+ `Config file already exists at ${targetPath}. Overwrite? [y/N]: `
672
+ )).trim().toLowerCase();
673
+ if (answer === "" || answer === "n" || answer === "no") {
674
+ return false;
675
+ }
676
+ if (answer === "y" || answer === "yes") {
677
+ return true;
678
+ }
679
+ io.error("Please answer y or n.\n");
680
+ }
681
+ }
682
+ function writeSetupSuccess(io, writtenPath) {
683
+ const ui = getSetupPresenter(io);
684
+ io.write(`
685
+ ${ui.success("You're set.")}
686
+ `);
687
+ io.write(`${ui.info(`Machine-wide config: ${writtenPath}`)}
688
+ `);
689
+ io.write(`${ui.note("sift is ready to use from any terminal on this machine.")}
690
+ `);
691
+ io.write(
692
+ `${ui.note("A repo-local sift.config.yaml can still override it when a project needs its own settings.")}
693
+ `
694
+ );
695
+ }
696
+ function writeOverrideWarning(io, activeConfigPath) {
697
+ const ui = getSetupPresenter(io);
698
+ io.write(
699
+ `${ui.warning(`Heads-up: ${activeConfigPath} currently overrides this machine-wide config in this directory.`)}
700
+ `
701
+ );
702
+ }
703
+ function writeNextSteps(io) {
704
+ const ui = getSetupPresenter(io);
705
+ io.write(`
706
+ ${ui.section("Try next")}
707
+ `);
708
+ io.write(` ${ui.command("sift doctor")}
709
+ `);
710
+ io.write(` ${ui.command("sift exec --preset test-status -- pytest")}
711
+ `);
712
+ }
713
+ async function configSetup(options = {}) {
714
+ void options.global;
715
+ const io = options.io ?? createTerminalIO();
716
+ const ui = getSetupPresenter(io);
717
+ try {
718
+ if (!io.stdinIsTTY || !io.stdoutIsTTY) {
719
+ io.error(
720
+ "sift config setup is interactive and requires a TTY. Use 'sift config init --global' for a non-interactive template.\n"
721
+ );
722
+ return 1;
723
+ }
724
+ io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
725
+ `);
726
+ const resolvedPath = resolveSetupPath(options.targetPath);
727
+ if (fs3.existsSync(resolvedPath)) {
728
+ const shouldOverwrite = await promptForOverwrite(io, resolvedPath);
729
+ if (!shouldOverwrite) {
730
+ io.write(`${ui.note("Aborted.")}
731
+ `);
732
+ return 1;
733
+ }
734
+ }
735
+ await promptForProvider(io);
736
+ io.write(`${ui.info("Using OpenAI defaults for your first run.")}
737
+ `);
738
+ io.write(`${ui.labelValue("Default model", "gpt-5-nano")}
739
+ `);
740
+ io.write(`${ui.labelValue("Default base URL", "https://api.openai.com/v1")}
741
+ `);
742
+ io.write(
743
+ `${ui.note(`Want to switch providers or tweak defaults later? Edit ${resolvedPath}.`)}
744
+ `
745
+ );
746
+ io.write(
747
+ `${ui.note("Want to inspect the active values first? Run 'sift config show --show-secrets'.")}
748
+ `
749
+ );
750
+ const apiKey = await promptForApiKey(io);
751
+ const config = buildOpenAISetupConfig(apiKey);
752
+ const writtenPath = writeConfigFile({
753
+ targetPath: resolvedPath,
754
+ config,
755
+ overwrite: true
756
+ });
757
+ writeSetupSuccess(io, writtenPath);
758
+ const activeConfigPath = findConfigPath();
759
+ if (activeConfigPath && path4.resolve(activeConfigPath) !== path4.resolve(writtenPath)) {
760
+ writeOverrideWarning(io, activeConfigPath);
761
+ }
762
+ writeNextSteps(io);
763
+ return 0;
764
+ } finally {
765
+ io.close?.();
766
+ }
767
+ }
352
768
 
353
769
  // src/commands/config.ts
354
770
  var MASKED_SECRET = "***";
@@ -369,10 +785,21 @@ function maskConfigSecrets(value) {
369
785
  }
370
786
  return output;
371
787
  }
372
- function configInit(targetPath) {
373
- const path4 = writeExampleConfig(targetPath);
374
- process.stdout.write(`${path4}
788
+ function configInit(targetPath, global = false) {
789
+ const path5 = writeExampleConfig({
790
+ targetPath,
791
+ global
792
+ });
793
+ if (!process.stdout.isTTY) {
794
+ process.stdout.write(`${path5}
375
795
  `);
796
+ return;
797
+ }
798
+ const ui = createPresentation(true);
799
+ process.stdout.write(
800
+ `${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path5}`)}
801
+ `
802
+ );
376
803
  }
377
804
  function configShow(configPath, showSecrets = false) {
378
805
  const config = resolveConfig({
@@ -389,24 +816,32 @@ function configValidate(configPath) {
389
816
  env: process.env
390
817
  });
391
818
  const resolvedPath = findConfigPath(configPath);
392
- process.stdout.write(
393
- `Resolved config is valid${resolvedPath ? ` (${resolvedPath})` : " (using defaults)"}.
394
- `
395
- );
819
+ const message = `Resolved config is valid${resolvedPath ? ` (${resolvedPath})` : " (using defaults)"}.`;
820
+ if (!process.stdout.isTTY) {
821
+ process.stdout.write(`${message}
822
+ `);
823
+ return;
824
+ }
825
+ const ui = createPresentation(true);
826
+ process.stdout.write(`${ui.success(message)}
827
+ `);
396
828
  }
397
829
 
398
830
  // src/commands/doctor.ts
399
- function runDoctor(config) {
831
+ function runDoctor(config, configPath) {
832
+ const ui = createPresentation(Boolean(process.stdout.isTTY));
400
833
  const lines = [
401
834
  "sift doctor",
835
+ "A quick check for your local setup.",
402
836
  "mode: local config completeness check",
403
- `provider: ${config.provider.provider}`,
404
- `model: ${config.provider.model}`,
405
- `baseUrl: ${config.provider.baseUrl}`,
406
- `apiKey: ${config.provider.apiKey ? "set" : "not set"}`,
407
- `maxCaptureChars: ${config.input.maxCaptureChars}`,
408
- `maxInputChars: ${config.input.maxInputChars}`,
409
- `rawFallback: ${config.runtime.rawFallback}`
837
+ ui.labelValue("configPath", configPath ?? "(defaults only)"),
838
+ ui.labelValue("provider", config.provider.provider),
839
+ ui.labelValue("model", config.provider.model),
840
+ ui.labelValue("baseUrl", config.provider.baseUrl),
841
+ ui.labelValue("apiKey", config.provider.apiKey ? "set" : "not set"),
842
+ ui.labelValue("maxCaptureChars", String(config.input.maxCaptureChars)),
843
+ ui.labelValue("maxInputChars", String(config.input.maxInputChars)),
844
+ ui.labelValue("rawFallback", String(config.runtime.rawFallback))
410
845
  ];
411
846
  process.stdout.write(`${lines.join("\n")}
412
847
  `);
@@ -427,8 +862,16 @@ function runDoctor(config) {
427
862
  );
428
863
  }
429
864
  if (problems.length > 0) {
430
- process.stderr.write(`${problems.join("\n")}
865
+ if (process.stderr.isTTY) {
866
+ const errorUi = createPresentation(true);
867
+ process.stderr.write(
868
+ `${problems.map((problem) => errorUi.error(problem)).join("\n")}
869
+ `
870
+ );
871
+ } else {
872
+ process.stderr.write(`${problems.join("\n")}
431
873
  `);
874
+ }
432
875
  return 1;
433
876
  }
434
877
  return 0;
@@ -457,7 +900,7 @@ function showPreset(config, name, includeInternal = false) {
457
900
  // src/core/exec.ts
458
901
  import { spawn } from "child_process";
459
902
  import { constants as osConstants } from "os";
460
- import pc2 from "picocolors";
903
+ import pc3 from "picocolors";
461
904
 
462
905
  // src/core/gate.ts
463
906
  var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
@@ -506,8 +949,31 @@ function evaluateGate(args) {
506
949
  return { shouldFail: false };
507
950
  }
508
951
 
952
+ // src/core/insufficient.ts
953
+ function isInsufficientSignalOutput(output) {
954
+ const trimmed = output.trim();
955
+ return trimmed === INSUFFICIENT_SIGNAL_TEXT || trimmed.startsWith(`${INSUFFICIENT_SIGNAL_TEXT}
956
+ Hint:`);
957
+ }
958
+ function buildInsufficientSignalOutput(input) {
959
+ let hint;
960
+ if (input.originalLength === 0) {
961
+ hint = "Hint: no command output was captured.";
962
+ } else if (input.truncatedApplied) {
963
+ hint = "Hint: captured output was truncated before a clear summary was found.";
964
+ } else if (input.presetName === "test-status" && input.exitCode === 0) {
965
+ hint = "Hint: command succeeded, but no recognizable test summary was found.";
966
+ } else if (input.presetName === "test-status" && typeof input.exitCode === "number") {
967
+ hint = "Hint: command failed, but the captured output did not include a recognizable test summary.";
968
+ } else {
969
+ hint = "Hint: the captured output did not contain a clear answer for this preset.";
970
+ }
971
+ return `${INSUFFICIENT_SIGNAL_TEXT}
972
+ ${hint}`;
973
+ }
974
+
509
975
  // src/core/run.ts
510
- import pc from "picocolors";
976
+ import pc2 from "picocolors";
511
977
 
512
978
  // src/providers/systemInstruction.ts
513
979
  var REDUCTION_SYSTEM_INSTRUCTION = "You reduce noisy command output into compact answers for agents and automation.";
@@ -583,7 +1049,7 @@ var OpenAIProvider = class {
583
1049
  if (!text) {
584
1050
  throw new Error("Provider returned an empty response");
585
1051
  }
586
- return {
1052
+ const result = {
587
1053
  text,
588
1054
  usage: data?.usage ? {
589
1055
  inputTokens: data.usage.input_tokens,
@@ -592,13 +1058,14 @@ var OpenAIProvider = class {
592
1058
  } : void 0,
593
1059
  raw: data
594
1060
  };
1061
+ clearTimeout(timeout);
1062
+ return result;
595
1063
  } catch (error) {
1064
+ clearTimeout(timeout);
596
1065
  if (error.name === "AbortError") {
597
1066
  throw new Error("Provider request timed out");
598
1067
  }
599
1068
  throw error;
600
- } finally {
601
- clearTimeout(timeout);
602
1069
  }
603
1070
  }
604
1071
  };
@@ -680,7 +1147,7 @@ var OpenAICompatibleProvider = class {
680
1147
  if (!text.trim()) {
681
1148
  throw new Error("Provider returned an empty response");
682
1149
  }
683
- return {
1150
+ const result = {
684
1151
  text,
685
1152
  usage: data?.usage ? {
686
1153
  inputTokens: data.usage.prompt_tokens,
@@ -689,13 +1156,14 @@ var OpenAICompatibleProvider = class {
689
1156
  } : void 0,
690
1157
  raw: data
691
1158
  };
1159
+ clearTimeout(timeout);
1160
+ return result;
692
1161
  } catch (error) {
1162
+ clearTimeout(timeout);
693
1163
  if (error.name === "AbortError") {
694
1164
  throw new Error("Provider request timed out");
695
1165
  }
696
1166
  throw error;
697
- } finally {
698
- clearTimeout(timeout);
699
1167
  }
700
1168
  }
701
1169
  };
@@ -900,6 +1368,19 @@ function buildPrompt(args) {
900
1368
  policyName: args.policyName,
901
1369
  outputContract: args.outputContract
902
1370
  });
1371
+ const detailRules = args.policyName === "test-status" && args.detail === "focused" ? [
1372
+ "Use a focused failure view.",
1373
+ "When the output clearly maps failures to specific tests or modules, group them by dominant error type first.",
1374
+ "Within each error group, prefer compact bullets in the form '- test-or-module -> dominant reason'.",
1375
+ "Cap focused entries at 6 per error group and end with '- and N more failing modules' if more clear mappings are visible.",
1376
+ "If per-test or per-module mapping is unclear, fall back to grouped root causes instead of guessing."
1377
+ ] : args.policyName === "test-status" && args.detail === "verbose" ? [
1378
+ "Use a verbose failure view.",
1379
+ "When the output clearly maps failures to specific tests or modules, list each visible failing test or module on its own line in the form '- test-or-module -> normalized reason'.",
1380
+ "Preserve the original file or module order when the mapping is visible.",
1381
+ "Prefer concrete normalized reasons such as missing modules or assertion failures over traceback plumbing.",
1382
+ "If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
1383
+ ] : [];
903
1384
  const prompt = [
904
1385
  "You are Sift, a CLI output reduction assistant for downstream agents and automation.",
905
1386
  "Hard rules:",
@@ -907,6 +1388,7 @@ function buildPrompt(args) {
907
1388
  "",
908
1389
  `Task policy: ${policy.name}`,
909
1390
  ...policy.taskRules.map((rule) => `- ${rule}`),
1391
+ ...detailRules.map((rule) => `- ${rule}`),
910
1392
  ...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
911
1393
  "",
912
1394
  `Question: ${args.question}`,
@@ -1019,6 +1501,410 @@ function inferPackage(line) {
1019
1501
  function inferRemediation(pkg2) {
1020
1502
  return `Upgrade ${pkg2} to a patched version.`;
1021
1503
  }
1504
+ function getCount(input, label) {
1505
+ const matches = [...input.matchAll(new RegExp(`(\\d+)\\s+${label}`, "gi"))];
1506
+ const lastMatch = matches.at(-1);
1507
+ return lastMatch ? Number(lastMatch[1]) : 0;
1508
+ }
1509
+ function formatCount(count, singular, plural = `${singular}s`) {
1510
+ return `${count} ${count === 1 ? singular : plural}`;
1511
+ }
1512
+ function countPattern(input, matcher) {
1513
+ return [...input.matchAll(matcher)].length;
1514
+ }
1515
+ function collectUniqueMatches(input, matcher, limit = 6) {
1516
+ const values = [];
1517
+ for (const match of input.matchAll(matcher)) {
1518
+ const candidate = match[1]?.trim();
1519
+ if (!candidate || values.includes(candidate)) {
1520
+ continue;
1521
+ }
1522
+ values.push(candidate);
1523
+ if (values.length >= limit) {
1524
+ break;
1525
+ }
1526
+ }
1527
+ return values;
1528
+ }
1529
+ function cleanFailureLabel(label) {
1530
+ return label.trim().replace(/^['"]|['"]$/g, "");
1531
+ }
1532
+ function isLowValueInternalReason(normalized) {
1533
+ return /^Hint:\s+make sure your test modules\/packages have valid Python names\.?$/i.test(
1534
+ normalized
1535
+ ) || /^Traceback\b/i.test(normalized) || /^return _bootstrap\._gcd_import/i.test(normalized) || /(?:^|[/\\])(?:site-packages[/\\])?_pytest(?:[/\\]|$)/i.test(normalized) || /(?:^|[/\\])importlib[/\\]__init__\.py:\d+:\s+in\s+import_module\b/i.test(
1536
+ normalized
1537
+ ) || /\bpython\.py:\d+:\s+in\s+importtestmodule\b/i.test(normalized) || /\bpython\.py:\d+:\s+in\s+import_path\b/i.test(normalized);
1538
+ }
1539
+ function scoreFailureReason(reason) {
1540
+ if (reason.startsWith("missing module:")) {
1541
+ return 5;
1542
+ }
1543
+ if (reason.startsWith("assertion failed:")) {
1544
+ return 4;
1545
+ }
1546
+ if (/^[A-Z][A-Za-z]+(?:Error|Exception):/.test(reason)) {
1547
+ return 3;
1548
+ }
1549
+ if (reason === "import error during collection") {
1550
+ return 2;
1551
+ }
1552
+ return 1;
1553
+ }
1554
+ function classifyFailureReason(line, options) {
1555
+ const normalized = line.trim().replace(/^[A-Z]\s+/, "");
1556
+ if (normalized.length === 0) {
1557
+ return null;
1558
+ }
1559
+ if (isLowValueInternalReason(normalized)) {
1560
+ return null;
1561
+ }
1562
+ const pythonMissingModule = normalized.match(
1563
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/i
1564
+ );
1565
+ if (pythonMissingModule) {
1566
+ return {
1567
+ reason: `missing module: ${pythonMissingModule[1]}`,
1568
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
1569
+ };
1570
+ }
1571
+ const nodeMissingModule = normalized.match(/Cannot find module ['"]([^'"]+)['"]/i);
1572
+ if (nodeMissingModule) {
1573
+ return {
1574
+ reason: `missing module: ${nodeMissingModule[1]}`,
1575
+ group: options.duringCollection ? "import/dependency errors during collection" : "missing dependency/module errors"
1576
+ };
1577
+ }
1578
+ const assertionFailure = normalized.match(/AssertionError:\s*(.+)$/i);
1579
+ if (assertionFailure) {
1580
+ return {
1581
+ reason: `assertion failed: ${assertionFailure[1]}`.slice(0, 120),
1582
+ group: "assertion failures"
1583
+ };
1584
+ }
1585
+ const genericError = normalized.match(/\b([A-Z][A-Za-z]+(?:Error|Exception)):\s*(.+)$/);
1586
+ if (genericError) {
1587
+ const errorType = genericError[1];
1588
+ return {
1589
+ reason: `${errorType}: ${genericError[2]}`.slice(0, 120),
1590
+ group: options.duringCollection && errorType === "ImportError" ? "import/dependency errors during collection" : `${errorType} failures`
1591
+ };
1592
+ }
1593
+ if (/ImportError while importing test module/i.test(normalized)) {
1594
+ return {
1595
+ reason: "import error during collection",
1596
+ group: "import/dependency errors during collection"
1597
+ };
1598
+ }
1599
+ if (!/[A-Za-z]/.test(normalized)) {
1600
+ return null;
1601
+ }
1602
+ return {
1603
+ reason: normalized.slice(0, 120),
1604
+ group: options.duringCollection ? "collection/import errors" : "other failures"
1605
+ };
1606
+ }
1607
+ function pushFocusedFailureItem(items, candidate) {
1608
+ if (items.some((item) => item.label === candidate.label && item.reason === candidate.reason)) {
1609
+ return;
1610
+ }
1611
+ items.push(candidate);
1612
+ }
1613
+ function chooseStrongestFailureItems(items) {
1614
+ const strongest = /* @__PURE__ */ new Map();
1615
+ const order = [];
1616
+ for (const item of items) {
1617
+ const existing = strongest.get(item.label);
1618
+ if (!existing) {
1619
+ strongest.set(item.label, item);
1620
+ order.push(item.label);
1621
+ continue;
1622
+ }
1623
+ if (scoreFailureReason(item.reason) > scoreFailureReason(existing.reason)) {
1624
+ strongest.set(item.label, item);
1625
+ }
1626
+ }
1627
+ return order.map((label) => strongest.get(label));
1628
+ }
1629
+ function collectCollectionFailureItems(input) {
1630
+ const items = [];
1631
+ const lines = input.split("\n");
1632
+ let currentLabel = null;
1633
+ let pendingGenericReason = null;
1634
+ for (const line of lines) {
1635
+ const collecting = line.match(/^_+\s+ERROR collecting\s+(.+?)\s+_+\s*$/);
1636
+ if (collecting) {
1637
+ if (currentLabel && pendingGenericReason) {
1638
+ pushFocusedFailureItem(
1639
+ items,
1640
+ {
1641
+ label: currentLabel,
1642
+ reason: pendingGenericReason.reason,
1643
+ group: pendingGenericReason.group
1644
+ }
1645
+ );
1646
+ }
1647
+ currentLabel = cleanFailureLabel(collecting[1]);
1648
+ pendingGenericReason = null;
1649
+ continue;
1650
+ }
1651
+ if (!currentLabel) {
1652
+ continue;
1653
+ }
1654
+ const classification = classifyFailureReason(line, {
1655
+ duringCollection: true
1656
+ });
1657
+ if (!classification) {
1658
+ continue;
1659
+ }
1660
+ if (classification.reason === "import error during collection") {
1661
+ pendingGenericReason = classification;
1662
+ continue;
1663
+ }
1664
+ pushFocusedFailureItem(
1665
+ items,
1666
+ {
1667
+ label: currentLabel,
1668
+ reason: classification.reason,
1669
+ group: classification.group
1670
+ }
1671
+ );
1672
+ currentLabel = null;
1673
+ pendingGenericReason = null;
1674
+ }
1675
+ if (currentLabel && pendingGenericReason) {
1676
+ pushFocusedFailureItem(
1677
+ items,
1678
+ {
1679
+ label: currentLabel,
1680
+ reason: pendingGenericReason.reason,
1681
+ group: pendingGenericReason.group
1682
+ }
1683
+ );
1684
+ }
1685
+ return items;
1686
+ }
1687
+ function collectInlineFailureItems(input) {
1688
+ const items = [];
1689
+ for (const line of input.split("\n")) {
1690
+ const inlineFailure = line.match(/^(FAILED|ERROR)\s+(.+?)\s+-\s+(.+)$/);
1691
+ if (!inlineFailure) {
1692
+ continue;
1693
+ }
1694
+ const classification = classifyFailureReason(inlineFailure[3], {
1695
+ duringCollection: false
1696
+ });
1697
+ if (!classification) {
1698
+ continue;
1699
+ }
1700
+ pushFocusedFailureItem(
1701
+ items,
1702
+ {
1703
+ label: cleanFailureLabel(inlineFailure[2]),
1704
+ reason: classification.reason,
1705
+ group: classification.group
1706
+ }
1707
+ );
1708
+ }
1709
+ return items;
1710
+ }
1711
+ function formatFocusedFailureGroups(args) {
1712
+ const maxGroups = args.maxGroups ?? 3;
1713
+ const maxPerGroup = args.maxPerGroup ?? 6;
1714
+ const grouped = /* @__PURE__ */ new Map();
1715
+ for (const item of args.items) {
1716
+ const entries = grouped.get(item.group) ?? [];
1717
+ entries.push(item);
1718
+ grouped.set(item.group, entries);
1719
+ }
1720
+ const lines = [];
1721
+ const visibleGroups = [...grouped.entries()].slice(0, maxGroups);
1722
+ for (const [group, entries] of visibleGroups) {
1723
+ lines.push(`- ${group}`);
1724
+ for (const item of entries.slice(0, maxPerGroup)) {
1725
+ lines.push(` - ${item.label} -> ${item.reason}`);
1726
+ }
1727
+ const remaining = entries.length - Math.min(entries.length, maxPerGroup);
1728
+ if (remaining > 0) {
1729
+ lines.push(` - and ${remaining} more failing ${args.remainderLabel}`);
1730
+ }
1731
+ }
1732
+ const hiddenGroups = grouped.size - visibleGroups.length;
1733
+ if (hiddenGroups > 0) {
1734
+ lines.push(`- and ${hiddenGroups} more error group${hiddenGroups === 1 ? "" : "s"}`);
1735
+ }
1736
+ return lines;
1737
+ }
1738
+ function formatVerboseFailureItems(args) {
1739
+ return chooseStrongestFailureItems(args.items).map(
1740
+ (item) => `- ${item.label} -> ${item.reason}`
1741
+ );
1742
+ }
1743
+ function summarizeRepeatedTestCauses(input, options) {
1744
+ const pythonMissingModules = collectUniqueMatches(
1745
+ input,
1746
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
1747
+ );
1748
+ const nodeMissingModules = collectUniqueMatches(
1749
+ input,
1750
+ /Cannot find module ['"]([^'"]+)['"]/gi
1751
+ );
1752
+ const missingModules = [...pythonMissingModules];
1753
+ for (const moduleName of nodeMissingModules) {
1754
+ if (!missingModules.includes(moduleName)) {
1755
+ missingModules.push(moduleName);
1756
+ }
1757
+ }
1758
+ const missingModuleHits = countPattern(
1759
+ input,
1760
+ /ModuleNotFoundError:\s+No module named ['"]([^'"]+)['"]/gi
1761
+ ) + countPattern(input, /Cannot find module ['"]([^'"]+)['"]/gi);
1762
+ const importCollectionHits = countPattern(input, /ImportError while importing test module/gi) + countPattern(input, /^\s*_+\s+ERROR collecting\b/gim);
1763
+ const genericErrorTypes = collectUniqueMatches(
1764
+ input,
1765
+ /\b((?:Assertion|Import|Type|Value|Runtime|Reference|Key|Attribute)[A-Za-z]*Error)\b/gi,
1766
+ 4
1767
+ );
1768
+ const bullets = [];
1769
+ if (options.duringCollection && (importCollectionHits >= 2 || missingModuleHits >= 2) || !options.duringCollection && missingModuleHits >= 2) {
1770
+ bullets.push(
1771
+ options.duringCollection ? "- Most failures are import/dependency errors during test collection." : "- Most failures are import/dependency errors."
1772
+ );
1773
+ }
1774
+ if (missingModules.length > 1) {
1775
+ bullets.push(`- Missing modules include ${missingModules.join(", ")}.`);
1776
+ } else if (missingModules.length === 1 && missingModuleHits >= 2) {
1777
+ bullets.push(`- Missing module repeated across failures: ${missingModules[0]}.`);
1778
+ }
1779
+ if (bullets.length < 2 && genericErrorTypes.length >= 2) {
1780
+ bullets.push(`- Repeated error types include ${genericErrorTypes.join(", ")}.`);
1781
+ }
1782
+ return bullets.slice(0, 2);
1783
+ }
1784
+ function testStatusHeuristic(input, detail = "standard") {
1785
+ const normalized = input.trim();
1786
+ if (normalized === "") {
1787
+ return null;
1788
+ }
1789
+ const passed = getCount(input, "passed");
1790
+ const failed = getCount(input, "failed");
1791
+ const errors = Math.max(
1792
+ getCount(input, "errors"),
1793
+ getCount(input, "error")
1794
+ );
1795
+ const skipped = getCount(input, "skipped");
1796
+ const collectionErrors = input.match(/(\d+)\s+errors?\s+during collection/i);
1797
+ const noTestsCollected = /\bcollected\s+0\s+items\b/i.test(input) || /\bno tests ran\b/i.test(input);
1798
+ const interrupted = /\binterrupted\b/i.test(input) || /\bKeyboardInterrupt\b/i.test(input);
1799
+ const inlineItems = collectInlineFailureItems(input);
1800
+ if (collectionErrors) {
1801
+ const count = Number(collectionErrors[1]);
1802
+ const items = chooseStrongestFailureItems(collectCollectionFailureItems(input));
1803
+ if (detail === "verbose") {
1804
+ if (items.length > 0) {
1805
+ return [
1806
+ "- Tests did not complete.",
1807
+ `- ${formatCount(count, "error")} occurred during collection.`,
1808
+ ...formatVerboseFailureItems({
1809
+ items
1810
+ })
1811
+ ].join("\n");
1812
+ }
1813
+ }
1814
+ if (detail === "focused") {
1815
+ if (items.length > 0) {
1816
+ const groupedLines = formatFocusedFailureGroups({
1817
+ items,
1818
+ remainderLabel: "modules"
1819
+ });
1820
+ if (groupedLines.length > 0) {
1821
+ return [
1822
+ "- Tests did not complete.",
1823
+ `- ${formatCount(count, "error")} occurred during collection.`,
1824
+ ...groupedLines
1825
+ ].join("\n");
1826
+ }
1827
+ }
1828
+ }
1829
+ const causes = summarizeRepeatedTestCauses(input, {
1830
+ duringCollection: true
1831
+ });
1832
+ return [
1833
+ "- Tests did not complete.",
1834
+ `- ${formatCount(count, "error")} occurred during collection.`,
1835
+ ...causes
1836
+ ].join("\n");
1837
+ }
1838
+ if (noTestsCollected) {
1839
+ return ["- Tests did not run.", "- Collected 0 items."].join("\n");
1840
+ }
1841
+ if (interrupted && failed === 0 && errors === 0) {
1842
+ return "- Test run was interrupted.";
1843
+ }
1844
+ if (failed === 0 && errors === 0 && passed > 0) {
1845
+ const details = [formatCount(passed, "test")];
1846
+ if (skipped > 0) {
1847
+ details.push(formatCount(skipped, "skip"));
1848
+ }
1849
+ return [
1850
+ "- Tests passed.",
1851
+ `- ${details.join(", ")}.`
1852
+ ].join("\n");
1853
+ }
1854
+ if (failed > 0 || errors > 0 || inlineItems.length > 0) {
1855
+ const summarizedInlineItems = chooseStrongestFailureItems(inlineItems);
1856
+ if (detail === "verbose") {
1857
+ if (summarizedInlineItems.length > 0) {
1858
+ const detailLines2 = [];
1859
+ if (failed > 0) {
1860
+ detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
1861
+ }
1862
+ if (errors > 0) {
1863
+ detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
1864
+ }
1865
+ return [
1866
+ "- Tests did not pass.",
1867
+ ...detailLines2,
1868
+ ...formatVerboseFailureItems({
1869
+ items: summarizedInlineItems
1870
+ })
1871
+ ].join("\n");
1872
+ }
1873
+ }
1874
+ if (detail === "focused") {
1875
+ if (summarizedInlineItems.length > 0) {
1876
+ const detailLines2 = [];
1877
+ if (failed > 0) {
1878
+ detailLines2.push(`- ${formatCount(failed, "test")} failed.`);
1879
+ }
1880
+ if (errors > 0) {
1881
+ detailLines2.push(`- ${formatCount(errors, "error")} occurred.`);
1882
+ }
1883
+ return [
1884
+ "- Tests did not pass.",
1885
+ ...detailLines2,
1886
+ ...formatFocusedFailureGroups({
1887
+ items: summarizedInlineItems,
1888
+ remainderLabel: "tests or modules"
1889
+ })
1890
+ ].join("\n");
1891
+ }
1892
+ }
1893
+ const detailLines = [];
1894
+ const causes = summarizeRepeatedTestCauses(input, {
1895
+ duringCollection: false
1896
+ });
1897
+ if (failed > 0) {
1898
+ detailLines.push(`- ${formatCount(failed, "test")} failed.`);
1899
+ }
1900
+ if (errors > 0) {
1901
+ detailLines.push(`- ${formatCount(errors, "error")} occurred.`);
1902
+ }
1903
+ const evidence = input.split("\n").map((line) => line.trim()).filter((line) => /\b(FAILED|ERROR)\b/.test(line)).slice(0, 3).map((line) => `- ${line}`);
1904
+ return ["- Tests did not pass.", ...detailLines, ...causes, ...evidence].join("\n");
1905
+ }
1906
+ return null;
1907
+ }
1022
1908
  function auditCriticalHeuristic(input) {
1023
1909
  const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
1024
1910
  if (!/\b(critical|high)\b/i.test(line)) {
@@ -1089,7 +1975,7 @@ function infraRiskHeuristic(input) {
1089
1975
  }
1090
1976
  return null;
1091
1977
  }
1092
- function applyHeuristicPolicy(policyName, input) {
1978
+ function applyHeuristicPolicy(policyName, input, detail) {
1093
1979
  if (!policyName) {
1094
1980
  return null;
1095
1981
  }
@@ -1099,6 +1985,9 @@ function applyHeuristicPolicy(policyName, input) {
1099
1985
  if (policyName === "infra-risk") {
1100
1986
  return infraRiskHeuristic(input);
1101
1987
  }
1988
+ if (policyName === "test-status") {
1989
+ return testStatusHeuristic(input, detail);
1990
+ }
1102
1991
  return null;
1103
1992
  }
1104
1993
 
@@ -1228,6 +2117,7 @@ function buildDryRunOutput(args) {
1228
2117
  },
1229
2118
  question: args.request.question,
1230
2119
  format: args.request.format,
2120
+ detail: args.request.detail ?? null,
1231
2121
  responseMode: args.responseMode,
1232
2122
  policy: args.request.policyName ?? null,
1233
2123
  heuristicOutput: args.heuristicOutput ?? null,
@@ -1247,35 +2137,42 @@ function buildDryRunOutput(args) {
1247
2137
  async function delay(ms) {
1248
2138
  await new Promise((resolve) => setTimeout(resolve, ms));
1249
2139
  }
2140
+ function withInsufficientHint(args) {
2141
+ if (!isInsufficientSignalOutput(args.output)) {
2142
+ return args.output;
2143
+ }
2144
+ return buildInsufficientSignalOutput({
2145
+ presetName: args.request.presetName,
2146
+ originalLength: args.prepared.meta.originalLength,
2147
+ truncatedApplied: args.prepared.meta.truncatedApplied
2148
+ });
2149
+ }
1250
2150
  async function generateWithRetry(args) {
1251
- let lastError;
1252
- for (let attempt = 0; attempt < 2; attempt += 1) {
1253
- try {
1254
- return await args.provider.generate({
1255
- model: args.request.config.provider.model,
1256
- prompt: args.prompt,
1257
- temperature: args.request.config.provider.temperature,
1258
- maxOutputTokens: args.request.config.provider.maxOutputTokens,
1259
- timeoutMs: args.request.config.provider.timeoutMs,
1260
- responseMode: args.responseMode,
1261
- jsonResponseFormat: args.request.config.provider.jsonResponseFormat
1262
- });
1263
- } catch (error) {
1264
- lastError = error;
1265
- const reason = error instanceof Error ? error.message : "unknown_error";
1266
- if (attempt > 0 || !isRetriableReason(reason)) {
1267
- throw error;
1268
- }
1269
- if (args.request.config.runtime.verbose) {
1270
- process.stderr.write(
1271
- `${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
2151
+ const generate = () => args.provider.generate({
2152
+ model: args.request.config.provider.model,
2153
+ prompt: args.prompt,
2154
+ temperature: args.request.config.provider.temperature,
2155
+ maxOutputTokens: args.request.config.provider.maxOutputTokens,
2156
+ timeoutMs: args.request.config.provider.timeoutMs,
2157
+ responseMode: args.responseMode,
2158
+ jsonResponseFormat: args.request.config.provider.jsonResponseFormat
2159
+ });
2160
+ try {
2161
+ return await generate();
2162
+ } catch (error) {
2163
+ const reason = error instanceof Error ? error.message : "unknown_error";
2164
+ if (!isRetriableReason(reason)) {
2165
+ throw error;
2166
+ }
2167
+ if (args.request.config.runtime.verbose) {
2168
+ process.stderr.write(
2169
+ `${pc2.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
1272
2170
  `
1273
- );
1274
- }
1275
- await delay(RETRY_DELAY_MS);
2171
+ );
1276
2172
  }
2173
+ await delay(RETRY_DELAY_MS);
1277
2174
  }
1278
- throw lastError instanceof Error ? lastError : new Error("unknown_error");
2175
+ return generate();
1279
2176
  }
1280
2177
  async function runSift(request) {
1281
2178
  const prepared = prepareInput(request.stdin, request.config.input);
@@ -1283,23 +2180,25 @@ async function runSift(request) {
1283
2180
  question: request.question,
1284
2181
  format: request.format,
1285
2182
  input: prepared.truncated,
2183
+ detail: request.detail,
1286
2184
  policyName: request.policyName,
1287
2185
  outputContract: request.outputContract
1288
2186
  });
1289
2187
  const provider = createProvider(request.config);
1290
2188
  if (request.config.runtime.verbose) {
1291
2189
  process.stderr.write(
1292
- `${pc.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
2190
+ `${pc2.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
1293
2191
  `
1294
2192
  );
1295
2193
  }
1296
2194
  const heuristicOutput = applyHeuristicPolicy(
1297
2195
  request.policyName,
1298
- prepared.truncated
2196
+ prepared.truncated,
2197
+ request.detail
1299
2198
  );
1300
2199
  if (heuristicOutput) {
1301
2200
  if (request.config.runtime.verbose) {
1302
- process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
2201
+ process.stderr.write(`${pc2.dim("sift")} heuristic=${request.policyName}
1303
2202
  `);
1304
2203
  }
1305
2204
  if (request.dryRun) {
@@ -1312,7 +2211,11 @@ async function runSift(request) {
1312
2211
  heuristicOutput
1313
2212
  });
1314
2213
  }
1315
- return heuristicOutput;
2214
+ return withInsufficientHint({
2215
+ output: heuristicOutput,
2216
+ request,
2217
+ prepared
2218
+ });
1316
2219
  }
1317
2220
  if (request.dryRun) {
1318
2221
  return buildDryRunOutput({
@@ -1338,15 +2241,23 @@ async function runSift(request) {
1338
2241
  })) {
1339
2242
  throw new Error("Model output rejected by quality gate");
1340
2243
  }
1341
- return normalizeOutput(result.text, responseMode);
2244
+ return withInsufficientHint({
2245
+ output: normalizeOutput(result.text, responseMode),
2246
+ request,
2247
+ prepared
2248
+ });
1342
2249
  } catch (error) {
1343
2250
  const reason = error instanceof Error ? error.message : "unknown_error";
1344
- return buildFallbackOutput({
1345
- format: request.format,
1346
- reason,
1347
- rawInput: prepared.truncated,
1348
- rawFallback: request.config.runtime.rawFallback,
1349
- jsonFallback: request.fallbackJson
2251
+ return withInsufficientHint({
2252
+ output: buildFallbackOutput({
2253
+ format: request.format,
2254
+ reason,
2255
+ rawInput: prepared.truncated,
2256
+ rawFallback: request.config.runtime.rawFallback,
2257
+ jsonFallback: request.fallbackJson
2258
+ }),
2259
+ request,
2260
+ prepared
1350
2261
  });
1351
2262
  }
1352
2263
  }
@@ -1429,6 +2340,15 @@ function buildCommandPreview(request) {
1429
2340
  }
1430
2341
  return (request.command ?? []).join(" ");
1431
2342
  }
2343
+ function getExecSuccessShortcut(args) {
2344
+ if (args.exitCode !== 0) {
2345
+ return null;
2346
+ }
2347
+ if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
2348
+ return "No type errors.";
2349
+ }
2350
+ return null;
2351
+ }
1432
2352
  async function runExec(request) {
1433
2353
  const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
1434
2354
  const hasShellCommand = typeof request.shellCommand === "string";
@@ -1438,7 +2358,7 @@ async function runExec(request) {
1438
2358
  const shellPath = process.env.SHELL || "/bin/bash";
1439
2359
  if (request.config.runtime.verbose) {
1440
2360
  process.stderr.write(
1441
- `${pc2.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${buildCommandPreview(request)}
2361
+ `${pc3.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${buildCommandPreview(request)}
1442
2362
  `
1443
2363
  );
1444
2364
  }
@@ -1447,7 +2367,6 @@ async function runExec(request) {
1447
2367
  let bypassed = false;
1448
2368
  let childStatus = null;
1449
2369
  let childSignal = null;
1450
- let childSpawnError = null;
1451
2370
  const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
1452
2371
  stdio: ["inherit", "pipe", "pipe"]
1453
2372
  }) : spawn(request.command[0], request.command.slice(1), {
@@ -1466,7 +2385,7 @@ async function runExec(request) {
1466
2385
  }
1467
2386
  bypassed = true;
1468
2387
  if (request.config.runtime.verbose) {
1469
- process.stderr.write(`${pc2.dim("sift")} bypass=interactive-prompt
2388
+ process.stderr.write(`${pc3.dim("sift")} bypass=interactive-prompt
1470
2389
  `);
1471
2390
  }
1472
2391
  process.stderr.write(capture.render());
@@ -1475,7 +2394,6 @@ async function runExec(request) {
1475
2394
  child.stderr.on("data", handleChunk);
1476
2395
  await new Promise((resolve, reject) => {
1477
2396
  child.on("error", (error) => {
1478
- childSpawnError = error;
1479
2397
  reject(error);
1480
2398
  });
1481
2399
  child.on("close", (status, signal) => {
@@ -1489,21 +2407,49 @@ async function runExec(request) {
1489
2407
  }
1490
2408
  throw new Error("Failed to start child process.");
1491
2409
  });
1492
- if (childSpawnError) {
1493
- throw childSpawnError;
1494
- }
1495
2410
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
2411
+ const capturedOutput = capture.render();
1496
2412
  if (request.config.runtime.verbose) {
1497
2413
  process.stderr.write(
1498
- `${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
2414
+ `${pc3.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
1499
2415
  `
1500
2416
  );
1501
2417
  }
1502
2418
  if (!bypassed) {
1503
- const output = await runSift({
2419
+ if (request.showRaw && capturedOutput.length > 0) {
2420
+ process.stderr.write(capturedOutput);
2421
+ if (!capturedOutput.endsWith("\n")) {
2422
+ process.stderr.write("\n");
2423
+ }
2424
+ }
2425
+ const execSuccessShortcut = getExecSuccessShortcut({
2426
+ presetName: request.presetName,
2427
+ exitCode,
2428
+ capturedOutput
2429
+ });
2430
+ if (execSuccessShortcut && !request.dryRun) {
2431
+ if (request.config.runtime.verbose) {
2432
+ process.stderr.write(
2433
+ `${pc3.dim("sift")} exec_shortcut=${request.presetName}
2434
+ `
2435
+ );
2436
+ }
2437
+ process.stdout.write(`${execSuccessShortcut}
2438
+ `);
2439
+ return exitCode;
2440
+ }
2441
+ let output = await runSift({
1504
2442
  ...request,
1505
- stdin: capture.render()
2443
+ stdin: capturedOutput
1506
2444
  });
2445
+ if (isInsufficientSignalOutput(output)) {
2446
+ output = buildInsufficientSignalOutput({
2447
+ presetName: request.presetName,
2448
+ originalLength: capture.getTotalChars(),
2449
+ truncatedApplied: capture.wasTruncated(),
2450
+ exitCode
2451
+ });
2452
+ }
1507
2453
  process.stdout.write(`${output}
1508
2454
  `);
1509
2455
  if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
@@ -1537,10 +2483,27 @@ function getPreset(config, name) {
1537
2483
  return preset;
1538
2484
  }
1539
2485
 
1540
- // src/cli.ts
2486
+ // src/cli-app.ts
1541
2487
  var require2 = createRequire(import.meta.url);
1542
2488
  var pkg = require2("../package.json");
1543
- var cli = cac("sift");
2489
+ var defaultCliDeps = {
2490
+ configInit,
2491
+ configSetup,
2492
+ configShow,
2493
+ configValidate,
2494
+ runDoctor,
2495
+ listPresets,
2496
+ showPreset,
2497
+ resolveConfig,
2498
+ findConfigPath,
2499
+ runExec,
2500
+ assertSupportedFailOnFormat,
2501
+ assertSupportedFailOnPreset,
2502
+ evaluateGate,
2503
+ readStdin,
2504
+ runSift,
2505
+ getPreset
2506
+ };
1544
2507
  function toNumber(value) {
1545
2508
  if (value === void 0 || value === null || value === "") {
1546
2509
  return void 0;
@@ -1585,44 +2548,32 @@ function applySharedOptions(command) {
1585
2548
  ).option(
1586
2549
  "--json-response-format <mode>",
1587
2550
  "JSON response format mode: auto | on | off"
1588
- ).option("--timeout-ms <ms>", "Request timeout in milliseconds").option("--format <format>", "brief | bullets | json | verdict").option("--max-capture-chars <n>", "Maximum raw child output chars kept in memory").option("--max-input-chars <n>", "Maximum input chars sent to the model").option("--head-chars <n>", "Head chars to preserve during truncation").option("--tail-chars <n>", "Tail chars to preserve during truncation").option("--strip-ansi", "Force ANSI stripping").option("--redact", "Enable standard redaction").option("--redact-strict", "Enable strict redaction").option("--raw-fallback", "Enable raw fallback text output").option("--dry-run", "Show the reduced input and prompt without calling the provider").option(
2551
+ ).option("--timeout-ms <ms>", "Request timeout in milliseconds").option("--format <format>", "brief | bullets | json | verdict").option(
2552
+ "--detail <mode>",
2553
+ "Detail level for supported presets: standard | focused | verbose"
2554
+ ).option("--max-capture-chars <n>", "Maximum raw child output chars kept in memory").option("--max-input-chars <n>", "Maximum input chars sent to the model").option("--head-chars <n>", "Head chars to preserve during truncation").option("--tail-chars <n>", "Tail chars to preserve during truncation").option("--strip-ansi", "Force ANSI stripping").option("--redact", "Enable standard redaction").option("--redact-strict", "Enable strict redaction").option("--raw-fallback", "Enable raw fallback text output").option("--dry-run", "Show the reduced input and prompt without calling the provider").option("--show-raw", "Print the captured raw input to stderr for debugging").option(
1589
2555
  "--fail-on",
1590
2556
  "Fail with exit code 1 when a supported built-in preset produces a blocking result"
1591
2557
  ).option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
1592
2558
  }
1593
- async function executeRun(args) {
1594
- if (Boolean(args.options.failOn)) {
1595
- assertSupportedFailOnPreset(args.presetName);
1596
- assertSupportedFailOnFormat({
1597
- presetName: args.presetName,
1598
- format: args.format
1599
- });
2559
+ function normalizeDetail(value) {
2560
+ if (value === void 0 || value === null || value === "") {
2561
+ return void 0;
1600
2562
  }
1601
- const config = resolveConfig({
1602
- configPath: args.options.config,
1603
- env: process.env,
1604
- cliOverrides: buildCliOverrides(args.options)
1605
- });
1606
- const stdin = await readStdin();
1607
- const output = await runSift({
1608
- question: args.question,
1609
- format: args.format,
1610
- stdin,
1611
- config,
1612
- dryRun: Boolean(args.options.dryRun),
1613
- presetName: args.presetName,
1614
- policyName: args.policyName,
1615
- outputContract: args.outputContract,
1616
- fallbackJson: args.fallbackJson
1617
- });
1618
- process.stdout.write(`${output}
1619
- `);
1620
- if (Boolean(args.options.failOn) && !Boolean(args.options.dryRun) && args.presetName && evaluateGate({
1621
- presetName: args.presetName,
1622
- output
1623
- }).shouldFail) {
1624
- process.exitCode = 1;
2563
+ if (value === "standard" || value === "focused" || value === "verbose") {
2564
+ return value;
1625
2565
  }
2566
+ throw new Error("Invalid --detail value. Use standard, focused, or verbose.");
2567
+ }
2568
+ function resolveDetail(args) {
2569
+ const requested = normalizeDetail(args.options.detail);
2570
+ if (!requested) {
2571
+ return args.presetName === "test-status" ? "standard" : void 0;
2572
+ }
2573
+ if (args.presetName !== "test-status") {
2574
+ throw new Error("--detail is supported only with --preset test-status.");
2575
+ }
2576
+ return requested;
1626
2577
  }
1627
2578
  function extractExecCommand(options) {
1628
2579
  const passthrough = Array.isArray(options["--"]) ? options["--"].map((value) => String(value)) : [];
@@ -1638,160 +2589,278 @@ function extractExecCommand(options) {
1638
2589
  shellCommand
1639
2590
  };
1640
2591
  }
1641
- async function executeExec(args) {
1642
- if (Boolean(args.options.failOn)) {
1643
- assertSupportedFailOnPreset(args.presetName);
1644
- assertSupportedFailOnFormat({
1645
- presetName: args.presetName,
1646
- format: args.format
2592
+ function cleanHelpSectionBody(body, escapedVersion) {
2593
+ return body.replace(
2594
+ new RegExp(`(^|\\n)sift/${escapedVersion}\\n\\n?`, "g"),
2595
+ "\n"
2596
+ );
2597
+ }
2598
+ function createCliApp(args = {}) {
2599
+ const deps = {
2600
+ ...defaultCliDeps,
2601
+ ...args.deps
2602
+ };
2603
+ const env = args.env ?? process.env;
2604
+ const stdout = args.stdout ?? process.stdout;
2605
+ const stderr = args.stderr ?? process.stderr;
2606
+ const version = args.version ?? pkg.version;
2607
+ const cli = cac("sift");
2608
+ const ui = createPresentation(Boolean(stdout.isTTY));
2609
+ const escapedVersion = version.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2610
+ async function executeRun(input) {
2611
+ if (Boolean(input.options.failOn)) {
2612
+ deps.assertSupportedFailOnPreset(input.presetName);
2613
+ deps.assertSupportedFailOnFormat({
2614
+ presetName: input.presetName,
2615
+ format: input.format
2616
+ });
2617
+ }
2618
+ const config = deps.resolveConfig({
2619
+ configPath: input.options.config,
2620
+ env,
2621
+ cliOverrides: buildCliOverrides(input.options)
2622
+ });
2623
+ const stdin = await deps.readStdin();
2624
+ if (Boolean(input.options.showRaw) && stdin.length > 0) {
2625
+ stderr.write(stdin);
2626
+ if (!stdin.endsWith("\n")) {
2627
+ stderr.write("\n");
2628
+ }
2629
+ }
2630
+ const output = await deps.runSift({
2631
+ question: input.question,
2632
+ format: input.format,
2633
+ stdin,
2634
+ config,
2635
+ dryRun: Boolean(input.options.dryRun),
2636
+ showRaw: Boolean(input.options.showRaw),
2637
+ detail: input.detail,
2638
+ presetName: input.presetName,
2639
+ policyName: input.policyName,
2640
+ outputContract: input.outputContract,
2641
+ fallbackJson: input.fallbackJson
1647
2642
  });
2643
+ stdout.write(`${output}
2644
+ `);
2645
+ if (Boolean(input.options.failOn) && !Boolean(input.options.dryRun) && input.presetName && deps.evaluateGate({
2646
+ presetName: input.presetName,
2647
+ output
2648
+ }).shouldFail) {
2649
+ process.exitCode = 1;
2650
+ }
1648
2651
  }
1649
- const config = resolveConfig({
1650
- configPath: args.options.config,
1651
- env: process.env,
1652
- cliOverrides: buildCliOverrides(args.options)
1653
- });
1654
- const command = extractExecCommand(args.options);
1655
- process.exitCode = await runExec({
1656
- question: args.question,
1657
- format: args.format,
1658
- config,
1659
- dryRun: Boolean(args.options.dryRun),
1660
- failOn: Boolean(args.options.failOn),
1661
- presetName: args.presetName,
1662
- policyName: args.policyName,
1663
- outputContract: args.outputContract,
1664
- fallbackJson: args.fallbackJson,
1665
- ...command
1666
- });
1667
- }
1668
- applySharedOptions(
1669
- cli.command("preset <name>", "Run a named preset against piped CLI output")
1670
- ).usage("preset <name> [options]").example("preset test-status < test-output.txt").action(async (name, options) => {
1671
- const config = resolveConfig({
1672
- configPath: options.config,
1673
- env: process.env,
1674
- cliOverrides: buildCliOverrides(options)
1675
- });
1676
- const preset = getPreset(config, name);
1677
- await executeRun({
1678
- question: preset.question,
1679
- format: options.format ?? preset.format,
1680
- presetName: name,
1681
- policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
1682
- options,
1683
- outputContract: preset.outputContract,
1684
- fallbackJson: preset.fallbackJson
1685
- });
1686
- });
1687
- applySharedOptions(
1688
- cli.command("exec [question]", "Run a command and reduce its output").allowUnknownOptions()
1689
- ).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- pytest").example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
1690
- if (question === "preset") {
1691
- throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
1692
- }
1693
- const presetName = typeof options.preset === "string" && options.preset.length > 0 ? options.preset : void 0;
1694
- if (presetName) {
1695
- if (question) {
1696
- throw new Error("Use either a freeform question or --preset <name>, not both.");
1697
- }
1698
- const preset = getPreset(
1699
- resolveConfig({
1700
- configPath: options.config,
1701
- env: process.env,
1702
- cliOverrides: buildCliOverrides(options)
1703
- }),
1704
- presetName
1705
- );
1706
- await executeExec({
2652
+ async function executeExec(input) {
2653
+ if (Boolean(input.options.failOn)) {
2654
+ deps.assertSupportedFailOnPreset(input.presetName);
2655
+ deps.assertSupportedFailOnFormat({
2656
+ presetName: input.presetName,
2657
+ format: input.format
2658
+ });
2659
+ }
2660
+ const config = deps.resolveConfig({
2661
+ configPath: input.options.config,
2662
+ env,
2663
+ cliOverrides: buildCliOverrides(input.options)
2664
+ });
2665
+ const command = extractExecCommand(input.options);
2666
+ process.exitCode = await deps.runExec({
2667
+ question: input.question,
2668
+ format: input.format,
2669
+ config,
2670
+ dryRun: Boolean(input.options.dryRun),
2671
+ failOn: Boolean(input.options.failOn),
2672
+ showRaw: Boolean(input.options.showRaw),
2673
+ detail: input.detail,
2674
+ presetName: input.presetName,
2675
+ policyName: input.policyName,
2676
+ outputContract: input.outputContract,
2677
+ fallbackJson: input.fallbackJson,
2678
+ ...command
2679
+ });
2680
+ }
2681
+ applySharedOptions(
2682
+ cli.command("preset <name>", "Run a named preset against piped output")
2683
+ ).usage("preset <name> [options]").example("preset test-status < test-output.txt").action(async (name, options) => {
2684
+ const config = deps.resolveConfig({
2685
+ configPath: options.config,
2686
+ env,
2687
+ cliOverrides: buildCliOverrides(options)
2688
+ });
2689
+ const preset = deps.getPreset(config, name);
2690
+ await executeRun({
1707
2691
  question: preset.question,
1708
2692
  format: options.format ?? preset.format,
1709
- presetName,
2693
+ presetName: name,
2694
+ detail: resolveDetail({
2695
+ presetName: name,
2696
+ options
2697
+ }),
1710
2698
  policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
1711
2699
  options,
1712
2700
  outputContract: preset.outputContract,
1713
2701
  fallbackJson: preset.fallbackJson
1714
2702
  });
1715
- return;
1716
- }
1717
- if (!question) {
1718
- throw new Error("Missing question or preset.");
1719
- }
1720
- const format = options.format ?? "brief";
1721
- await executeExec({
1722
- question,
1723
- format,
1724
- options
1725
2703
  });
1726
- });
1727
- cli.command(
1728
- "config <action>",
1729
- "Config commands: init | show | validate (show/validate use resolved runtime config)"
1730
- ).usage("config <init|show|validate> [options]").example("config init").example("config show").example("config validate --config ./sift.config.yaml").option("--path <path>", "Target config path for init").option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action((action, options) => {
1731
- if (action === "init") {
1732
- configInit(options.path);
1733
- return;
1734
- }
1735
- if (action === "show") {
1736
- configShow(
1737
- options.config,
1738
- Boolean(options.showSecrets)
1739
- );
1740
- return;
1741
- }
1742
- if (action === "validate") {
1743
- configValidate(options.config);
1744
- return;
1745
- }
1746
- throw new Error(`Unknown config action: ${action}`);
1747
- });
1748
- cli.command("doctor", "Check local runtime config completeness").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {
1749
- const config = resolveConfig({
1750
- configPath: options.config,
1751
- env: process.env
2704
+ applySharedOptions(
2705
+ cli.command("exec [question]", "Run a command and shrink its output for the model").allowUnknownOptions()
2706
+ ).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- pytest").example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
2707
+ if (question === "preset") {
2708
+ throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
2709
+ }
2710
+ const presetName = typeof options.preset === "string" && options.preset.length > 0 ? options.preset : void 0;
2711
+ if (presetName) {
2712
+ if (question) {
2713
+ throw new Error("Use either a freeform question or --preset <name>, not both.");
2714
+ }
2715
+ const preset = deps.getPreset(
2716
+ deps.resolveConfig({
2717
+ configPath: options.config,
2718
+ env,
2719
+ cliOverrides: buildCliOverrides(options)
2720
+ }),
2721
+ presetName
2722
+ );
2723
+ await executeExec({
2724
+ question: preset.question,
2725
+ format: options.format ?? preset.format,
2726
+ presetName,
2727
+ detail: resolveDetail({
2728
+ presetName,
2729
+ options
2730
+ }),
2731
+ policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
2732
+ options,
2733
+ outputContract: preset.outputContract,
2734
+ fallbackJson: preset.fallbackJson
2735
+ });
2736
+ return;
2737
+ }
2738
+ if (!question) {
2739
+ throw new Error("Missing question or preset.");
2740
+ }
2741
+ const format = options.format ?? "brief";
2742
+ await executeExec({
2743
+ question,
2744
+ format,
2745
+ detail: resolveDetail({
2746
+ options
2747
+ }),
2748
+ options
2749
+ });
1752
2750
  });
1753
- process.exitCode = runDoctor(config);
1754
- });
1755
- cli.command("presets <action> [name]", "Preset commands: list | show").usage("presets <list|show> [name] [options]").example("presets list").example("presets show infra-risk").option("--config <path>", "Path to config file").option("--internal", "Show internal preset fields in presets show").action((action, name, options) => {
1756
- const config = resolveConfig({
1757
- configPath: options.config,
1758
- env: process.env
2751
+ cli.command("config <action>", "Config commands: setup | init | show | validate").usage("config <setup|init|show|validate> [options]").example("config setup").example("config setup --global").example("config setup --path ~/.config/sift/config.yaml").example("config init").example("config init --global").example("config show").example("config validate --config ./sift.config.yaml").option("--path <path>", "Target config path for init or setup").option(
2752
+ "--global",
2753
+ "Use the machine-wide config path (~/.config/sift/config.yaml) for init or setup"
2754
+ ).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, options) => {
2755
+ if (action === "setup") {
2756
+ process.exitCode = await deps.configSetup({
2757
+ targetPath: options.path,
2758
+ global: Boolean(options.global)
2759
+ });
2760
+ return;
2761
+ }
2762
+ if (action === "init") {
2763
+ deps.configInit(options.path, Boolean(options.global));
2764
+ return;
2765
+ }
2766
+ if (action === "show") {
2767
+ deps.configShow(
2768
+ options.config,
2769
+ Boolean(options.showSecrets)
2770
+ );
2771
+ return;
2772
+ }
2773
+ if (action === "validate") {
2774
+ deps.configValidate(options.config);
2775
+ return;
2776
+ }
2777
+ throw new Error(`Unknown config action: ${action}`);
1759
2778
  });
1760
- if (action === "list") {
1761
- listPresets(config);
1762
- return;
1763
- }
1764
- if (action === "show") {
1765
- if (!name) {
1766
- throw new Error("Missing preset name.");
2779
+ cli.command("doctor", "Check which config is active and whether local setup looks complete").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {
2780
+ const configPath = deps.findConfigPath(options.config);
2781
+ const config = deps.resolveConfig({
2782
+ configPath: options.config,
2783
+ env
2784
+ });
2785
+ process.exitCode = deps.runDoctor(config, configPath);
2786
+ });
2787
+ cli.command("presets <action> [name]", "Preset commands: list | show").usage("presets <list|show> [name] [options]").example("presets list").example("presets show infra-risk").option("--config <path>", "Path to config file").option("--internal", "Show internal preset fields in presets show").action((action, name, options) => {
2788
+ const config = deps.resolveConfig({
2789
+ configPath: options.config,
2790
+ env
2791
+ });
2792
+ if (action === "list") {
2793
+ deps.listPresets(config);
2794
+ return;
1767
2795
  }
1768
- showPreset(config, name, Boolean(options.internal));
1769
- return;
1770
- }
1771
- throw new Error(`Unknown presets action: ${action}`);
1772
- });
1773
- applySharedOptions(
1774
- cli.command("[question]", "Ask a freeform question about piped CLI output")
1775
- ).action(async (question, options) => {
1776
- if (!question) {
1777
- throw new Error("Missing question.");
1778
- }
1779
- const format = options.format ?? "brief";
1780
- await executeRun({
1781
- question,
1782
- format,
1783
- options
2796
+ if (action === "show") {
2797
+ if (!name) {
2798
+ throw new Error("Missing preset name.");
2799
+ }
2800
+ deps.showPreset(config, name, Boolean(options.internal));
2801
+ return;
2802
+ }
2803
+ throw new Error(`Unknown presets action: ${action}`);
1784
2804
  });
1785
- });
1786
- cli.help();
1787
- cli.version(pkg.version);
1788
- async function main() {
1789
- cli.parse(process.argv, { run: false });
2805
+ applySharedOptions(cli.command("[question]", "Ask a question about piped output")).action(
2806
+ async (question, options) => {
2807
+ if (!question) {
2808
+ throw new Error("Missing question.");
2809
+ }
2810
+ const format = options.format ?? "brief";
2811
+ await executeRun({
2812
+ question,
2813
+ format,
2814
+ detail: resolveDetail({
2815
+ options
2816
+ }),
2817
+ options
2818
+ });
2819
+ }
2820
+ );
2821
+ cli.help((sections) => {
2822
+ const cleanedSections = sections.map((section) => ({
2823
+ ...section,
2824
+ body: cleanHelpSectionBody(section.body, escapedVersion)
2825
+ }));
2826
+ return [
2827
+ {
2828
+ body: `${ui.banner(version)}
2829
+ `
2830
+ },
2831
+ {
2832
+ title: ui.section("Quick start"),
2833
+ body: [
2834
+ ` ${ui.command("sift config setup")}`,
2835
+ ` ${ui.command("sift exec --preset test-status -- pytest")}`,
2836
+ ` ${ui.command("sift exec --preset test-status --show-raw -- pytest")}`,
2837
+ ` ${ui.command('sift exec "what changed?" -- git diff')}`
2838
+ ].join("\n")
2839
+ },
2840
+ ...cleanedSections
2841
+ ];
2842
+ });
2843
+ cli.version(version);
2844
+ return cli;
2845
+ }
2846
+ async function runCli(args = {}) {
2847
+ const cli = createCliApp(args);
2848
+ cli.parse(args.argv ?? process.argv, { run: false });
1790
2849
  await cli.runMatchedCommand();
1791
2850
  }
1792
- main().catch((error) => {
2851
+ function handleCliError(error, stderr = process.stderr) {
1793
2852
  const message = error instanceof Error ? error.message : "Unexpected error.";
1794
- process.stderr.write(`${message}
2853
+ if (stderr.isTTY) {
2854
+ stderr.write(`${createPresentation(true).error(message)}
2855
+ `);
2856
+ } else {
2857
+ stderr.write(`${message}
1795
2858
  `);
2859
+ }
1796
2860
  process.exitCode = 1;
2861
+ }
2862
+
2863
+ // src/cli.ts
2864
+ runCli().catch((error) => {
2865
+ handleCliError(error);
1797
2866
  });