@bilalimamoglu/sift 0.2.0 → 0.2.2

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
@@ -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
  }
@@ -51,23 +56,23 @@ function loadRawConfig(explicitPath) {
51
56
  // src/config/defaults.ts
52
57
  var defaultConfig = {
53
58
  provider: {
54
- provider: "openai-compatible",
55
- model: "gpt-4.1-mini",
59
+ provider: "openai",
60
+ model: "gpt-5-nano",
56
61
  baseUrl: "https://api.openai.com/v1",
57
62
  apiKey: "",
58
63
  jsonResponseFormat: "auto",
59
64
  timeoutMs: 2e4,
60
65
  temperature: 0.1,
61
- maxOutputTokens: 220
66
+ maxOutputTokens: 400
62
67
  },
63
68
  input: {
64
69
  stripAnsi: true,
65
70
  redact: false,
66
71
  redactStrict: false,
67
- maxCaptureChars: 25e4,
68
- maxInputChars: 2e4,
69
- headChars: 6e3,
70
- tailChars: 6e3
72
+ maxCaptureChars: 4e5,
73
+ maxInputChars: 6e4,
74
+ headChars: 2e4,
75
+ tailChars: 2e4
71
76
  },
72
77
  runtime: {
73
78
  rawFallback: true,
@@ -101,6 +106,16 @@ var defaultConfig = {
101
106
  format: "bullets",
102
107
  policy: "log-errors"
103
108
  },
109
+ "typecheck-summary": {
110
+ question: "Summarize the blocking typecheck failures. Group repeated errors by root cause and point to the first files or symbols to fix.",
111
+ format: "bullets",
112
+ policy: "typecheck-summary"
113
+ },
114
+ "lint-failures": {
115
+ question: "Summarize the blocking lint failures. Group repeated rules, highlight the top offending files, and call out only failures that matter for fixing the run.",
116
+ format: "bullets",
117
+ policy: "lint-failures"
118
+ },
104
119
  "infra-risk": {
105
120
  question: "Assess whether the infrastructure changes are risky and whether they look safe to apply.",
106
121
  format: "verdict",
@@ -141,18 +156,21 @@ function resolveCompatibleEnvName(baseUrl) {
141
156
  return match?.envName;
142
157
  }
143
158
  function resolveProviderApiKey(provider, baseUrl, env) {
144
- if (env.SIFT_PROVIDER_API_KEY) {
145
- return env.SIFT_PROVIDER_API_KEY;
146
- }
147
159
  if (provider === "openai-compatible") {
160
+ if (env.SIFT_PROVIDER_API_KEY) {
161
+ return env.SIFT_PROVIDER_API_KEY;
162
+ }
148
163
  const envName2 = resolveCompatibleEnvName(baseUrl);
149
164
  return envName2 ? env[envName2] : void 0;
150
165
  }
151
166
  if (!provider) {
152
- return void 0;
167
+ return env.SIFT_PROVIDER_API_KEY;
153
168
  }
154
169
  const envName = PROVIDER_API_KEY_ENV[provider];
155
- return envName ? env[envName] : void 0;
170
+ if (envName && env[envName]) {
171
+ return env[envName];
172
+ }
173
+ return env.SIFT_PROVIDER_API_KEY;
156
174
  }
157
175
  function getProviderApiKeyEnvNames(provider, baseUrl) {
158
176
  const envNames = ["SIFT_PROVIDER_API_KEY"];
@@ -168,14 +186,14 @@ function getProviderApiKeyEnvNames(provider, baseUrl) {
168
186
  }
169
187
  const envName = PROVIDER_API_KEY_ENV[provider];
170
188
  if (envName) {
171
- envNames.push(envName);
189
+ return [envName, ...envNames];
172
190
  }
173
191
  return envNames;
174
192
  }
175
193
 
176
194
  // src/config/schema.ts
177
195
  import { z } from "zod";
178
- var providerNameSchema = z.enum(["openai-compatible"]);
196
+ var providerNameSchema = z.enum(["openai", "openai-compatible"]);
179
197
  var outputFormatSchema = z.enum([
180
198
  "brief",
181
199
  "bullets",
@@ -190,7 +208,9 @@ var promptPolicyNameSchema = z.enum([
190
208
  "diff-summary",
191
209
  "build-failure",
192
210
  "log-errors",
193
- "infra-risk"
211
+ "infra-risk",
212
+ "typecheck-summary",
213
+ "lint-failures"
194
214
  ]);
195
215
  var providerConfigSchema = z.object({
196
216
  provider: providerNameSchema,
@@ -324,8 +344,11 @@ function resolveConfig(options = {}) {
324
344
  import fs2 from "fs";
325
345
  import path3 from "path";
326
346
  import YAML2 from "yaml";
327
- function writeExampleConfig(targetPath) {
328
- 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);
329
352
  if (fs2.existsSync(resolved)) {
330
353
  throw new Error(`Config file already exists at ${resolved}`);
331
354
  }
@@ -334,6 +357,226 @@ function writeExampleConfig(targetPath) {
334
357
  fs2.writeFileSync(resolved, yaml, "utf8");
335
358
  return resolved;
336
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/commands/config-setup.ts
379
+ import fs3 from "fs";
380
+ import path4 from "path";
381
+ import { createInterface } from "readline/promises";
382
+ import { clearLine, cursorTo, emitKeypressEvents, moveCursor } from "readline";
383
+ import { stdin as defaultStdin, stdout as defaultStdout, stderr as defaultStderr } from "process";
384
+ function createTerminalIO() {
385
+ let rl;
386
+ function getInterface() {
387
+ if (!rl) {
388
+ rl = createInterface({
389
+ input: defaultStdin,
390
+ output: defaultStdout,
391
+ terminal: true
392
+ });
393
+ }
394
+ return rl;
395
+ }
396
+ async function select(prompt, options) {
397
+ const input = defaultStdin;
398
+ const output = defaultStdout;
399
+ const promptLine = `${prompt} (use \u2191/\u2193 and Enter)`;
400
+ let index = 0;
401
+ const lineCount = options.length + 1;
402
+ emitKeypressEvents(input);
403
+ input.resume();
404
+ const wasRaw = input.isTTY ? input.isRaw : false;
405
+ input.setRawMode?.(true);
406
+ const render = () => {
407
+ cursorTo(output, 0);
408
+ clearLine(output, 0);
409
+ output.write(`${promptLine}
410
+ `);
411
+ for (let optionIndex = 0; optionIndex < options.length; optionIndex += 1) {
412
+ clearLine(output, 0);
413
+ output.write(`${optionIndex === index ? "\u203A" : " "} ${options[optionIndex]}
414
+ `);
415
+ }
416
+ moveCursor(output, 0, -lineCount);
417
+ };
418
+ render();
419
+ return await new Promise((resolve, reject) => {
420
+ const onKeypress = (_value, key) => {
421
+ if (key.ctrl && key.name === "c") {
422
+ cleanup();
423
+ reject(new Error("Aborted."));
424
+ return;
425
+ }
426
+ if (key.name === "up") {
427
+ index = index === 0 ? options.length - 1 : index - 1;
428
+ render();
429
+ return;
430
+ }
431
+ if (key.name === "down") {
432
+ index = (index + 1) % options.length;
433
+ render();
434
+ return;
435
+ }
436
+ if (key.name === "return" || key.name === "enter") {
437
+ const selected = options[index] ?? options[0];
438
+ cleanup();
439
+ resolve(selected ?? "OpenAI");
440
+ }
441
+ };
442
+ const cleanup = () => {
443
+ input.off("keypress", onKeypress);
444
+ moveCursor(output, 0, lineCount);
445
+ cursorTo(output, 0);
446
+ clearLine(output, 0);
447
+ output.write("\n");
448
+ input.setRawMode?.(Boolean(wasRaw));
449
+ };
450
+ input.on("keypress", onKeypress);
451
+ });
452
+ }
453
+ return {
454
+ stdinIsTTY: Boolean(defaultStdin.isTTY),
455
+ stdoutIsTTY: Boolean(defaultStdout.isTTY),
456
+ ask(prompt) {
457
+ return getInterface().question(prompt);
458
+ },
459
+ select,
460
+ write(message) {
461
+ defaultStdout.write(message);
462
+ },
463
+ error(message) {
464
+ defaultStderr.write(message);
465
+ },
466
+ close() {
467
+ rl?.close();
468
+ }
469
+ };
470
+ }
471
+ function resolveSetupPath(targetPath) {
472
+ return targetPath ? path4.resolve(targetPath) : getDefaultGlobalConfigPath();
473
+ }
474
+ function buildOpenAISetupConfig(apiKey) {
475
+ return {
476
+ ...defaultConfig,
477
+ provider: {
478
+ ...defaultConfig.provider,
479
+ provider: "openai",
480
+ model: "gpt-5-nano",
481
+ baseUrl: "https://api.openai.com/v1",
482
+ apiKey
483
+ }
484
+ };
485
+ }
486
+ async function promptForProvider(io) {
487
+ if (io.select) {
488
+ const choice = await io.select("Select provider", ["OpenAI"]);
489
+ if (choice === "OpenAI") {
490
+ return "openai";
491
+ }
492
+ }
493
+ while (true) {
494
+ const answer = (await io.ask("Provider [OpenAI]: ")).trim().toLowerCase();
495
+ if (answer === "" || answer === "openai") {
496
+ return "openai";
497
+ }
498
+ io.error("Only OpenAI is supported in guided setup right now.\n");
499
+ }
500
+ }
501
+ async function promptForApiKey(io) {
502
+ while (true) {
503
+ const answer = (await io.ask("Enter your OpenAI API key: ")).trim();
504
+ if (answer.length > 0) {
505
+ return answer;
506
+ }
507
+ io.error("API key cannot be empty.\n");
508
+ }
509
+ }
510
+ async function promptForOverwrite(io, targetPath) {
511
+ while (true) {
512
+ const answer = (await io.ask(
513
+ `Config file already exists at ${targetPath}. Overwrite? [y/N]: `
514
+ )).trim().toLowerCase();
515
+ if (answer === "" || answer === "n" || answer === "no") {
516
+ return false;
517
+ }
518
+ if (answer === "y" || answer === "yes") {
519
+ return true;
520
+ }
521
+ io.error("Please answer y or n.\n");
522
+ }
523
+ }
524
+ async function configSetup(options = {}) {
525
+ void options.global;
526
+ const io = options.io ?? createTerminalIO();
527
+ try {
528
+ if (!io.stdinIsTTY || !io.stdoutIsTTY) {
529
+ io.error(
530
+ "sift config setup is interactive and requires a TTY. Use 'sift config init --global' for a non-interactive template.\n"
531
+ );
532
+ return 1;
533
+ }
534
+ const resolvedPath = resolveSetupPath(options.targetPath);
535
+ if (fs3.existsSync(resolvedPath)) {
536
+ const shouldOverwrite = await promptForOverwrite(io, resolvedPath);
537
+ if (!shouldOverwrite) {
538
+ io.write("Aborted.\n");
539
+ return 1;
540
+ }
541
+ }
542
+ const provider = await promptForProvider(io);
543
+ if (provider !== "openai") {
544
+ io.error("Unsupported provider selection.\n");
545
+ return 1;
546
+ }
547
+ io.write("Using OpenAI defaults.\n");
548
+ io.write("Default model: gpt-5-nano\n");
549
+ io.write("Default base URL: https://api.openai.com/v1\n");
550
+ io.write(
551
+ "You can change these later by editing the config file or running 'sift config show --show-secrets'.\n"
552
+ );
553
+ const apiKey = await promptForApiKey(io);
554
+ const config = buildOpenAISetupConfig(apiKey);
555
+ const writtenPath = writeConfigFile({
556
+ targetPath: resolvedPath,
557
+ config,
558
+ overwrite: true
559
+ });
560
+ io.write(`Wrote ${writtenPath}
561
+ `);
562
+ io.write(
563
+ "This is your machine-wide default config. Repo-local sift.config.yaml can still override it later.\n"
564
+ );
565
+ const activeConfigPath = findConfigPath();
566
+ if (activeConfigPath && path4.resolve(activeConfigPath) !== path4.resolve(writtenPath)) {
567
+ io.write(
568
+ `Note: ${activeConfigPath} currently overrides this machine-wide config in the current directory.
569
+ `
570
+ );
571
+ }
572
+ io.write("Try:\n");
573
+ io.write(" sift doctor\n");
574
+ io.write(" sift exec --preset test-status -- pytest\n");
575
+ return 0;
576
+ } finally {
577
+ io.close?.();
578
+ }
579
+ }
337
580
 
338
581
  // src/commands/config.ts
339
582
  var MASKED_SECRET = "***";
@@ -354,9 +597,12 @@ function maskConfigSecrets(value) {
354
597
  }
355
598
  return output;
356
599
  }
357
- function configInit(targetPath) {
358
- const path4 = writeExampleConfig(targetPath);
359
- process.stdout.write(`${path4}
600
+ function configInit(targetPath, global = false) {
601
+ const path5 = writeExampleConfig({
602
+ targetPath,
603
+ global
604
+ });
605
+ process.stdout.write(`${path5}
360
606
  `);
361
607
  }
362
608
  function configShow(configPath, showSecrets = false) {
@@ -381,10 +627,11 @@ function configValidate(configPath) {
381
627
  }
382
628
 
383
629
  // src/commands/doctor.ts
384
- function runDoctor(config) {
630
+ function runDoctor(config, configPath) {
385
631
  const lines = [
386
632
  "sift doctor",
387
633
  "mode: local config completeness check",
634
+ `configPath: ${configPath ?? "(defaults only)"}`,
388
635
  `provider: ${config.provider.provider}`,
389
636
  `model: ${config.provider.model}`,
390
637
  `baseUrl: ${config.provider.baseUrl}`,
@@ -402,7 +649,7 @@ function runDoctor(config) {
402
649
  if (!config.provider.model) {
403
650
  problems.push("Missing provider.model");
404
651
  }
405
- if (config.provider.provider === "openai-compatible" && !config.provider.apiKey) {
652
+ if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible") && !config.provider.apiKey) {
406
653
  problems.push("Missing provider.apiKey");
407
654
  problems.push(
408
655
  `Set one of: ${getProviderApiKeyEnvNames(
@@ -444,9 +691,150 @@ import { spawn } from "child_process";
444
691
  import { constants as osConstants } from "os";
445
692
  import pc2 from "picocolors";
446
693
 
694
+ // src/core/gate.ts
695
+ var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
696
+ function parseJson(output) {
697
+ try {
698
+ return JSON.parse(output);
699
+ } catch {
700
+ return null;
701
+ }
702
+ }
703
+ function supportsFailOnPreset(presetName) {
704
+ return typeof presetName === "string" && FAIL_ON_SUPPORTED_PRESETS.has(presetName);
705
+ }
706
+ function assertSupportedFailOnPreset(presetName) {
707
+ if (!supportsFailOnPreset(presetName)) {
708
+ throw new Error(
709
+ "--fail-on is supported only for built-in presets: infra-risk, audit-critical."
710
+ );
711
+ }
712
+ }
713
+ function assertSupportedFailOnFormat(args) {
714
+ const expectedFormat = args.presetName === "infra-risk" ? "verdict" : "json";
715
+ if (args.format !== expectedFormat) {
716
+ throw new Error(
717
+ `--fail-on requires the default ${expectedFormat} format for preset ${args.presetName}.`
718
+ );
719
+ }
720
+ }
721
+ function evaluateGate(args) {
722
+ const parsed = parseJson(args.output);
723
+ if (!parsed || typeof parsed !== "object") {
724
+ return { shouldFail: false };
725
+ }
726
+ if (args.presetName === "infra-risk") {
727
+ return {
728
+ shouldFail: parsed["verdict"] === "fail"
729
+ };
730
+ }
731
+ if (args.presetName === "audit-critical") {
732
+ const status = parsed["status"];
733
+ const vulnerabilities = parsed["vulnerabilities"];
734
+ return {
735
+ shouldFail: status === "ok" && Array.isArray(vulnerabilities) && vulnerabilities.length > 0
736
+ };
737
+ }
738
+ return { shouldFail: false };
739
+ }
740
+
447
741
  // src/core/run.ts
448
742
  import pc from "picocolors";
449
743
 
744
+ // src/providers/systemInstruction.ts
745
+ var REDUCTION_SYSTEM_INSTRUCTION = "You reduce noisy command output into compact answers for agents and automation.";
746
+
747
+ // src/providers/openai.ts
748
+ function usesNativeJsonResponseFormat(mode) {
749
+ return mode !== "off";
750
+ }
751
+ function extractResponseText(payload) {
752
+ if (typeof payload?.output_text === "string") {
753
+ return payload.output_text.trim();
754
+ }
755
+ if (!Array.isArray(payload?.output)) {
756
+ return "";
757
+ }
758
+ return payload.output.flatMap((item) => Array.isArray(item?.content) ? item.content : []).map((item) => item?.type === "output_text" ? item.text : "").filter((text) => typeof text === "string" && text.trim().length > 0).join("").trim();
759
+ }
760
+ async function buildOpenAIError(response) {
761
+ let detail = `Provider returned HTTP ${response.status}`;
762
+ try {
763
+ const data = await response.json();
764
+ const message = data?.error?.message;
765
+ if (typeof message === "string" && message.trim().length > 0) {
766
+ detail = `${detail}: ${message.trim()}`;
767
+ }
768
+ } catch {
769
+ }
770
+ return new Error(detail);
771
+ }
772
+ var OpenAIProvider = class {
773
+ name = "openai";
774
+ baseUrl;
775
+ apiKey;
776
+ constructor(options) {
777
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
778
+ this.apiKey = options.apiKey;
779
+ }
780
+ async generate(input) {
781
+ const controller = new AbortController();
782
+ const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
783
+ try {
784
+ const url = new URL("responses", `${this.baseUrl}/`);
785
+ const response = await fetch(url, {
786
+ method: "POST",
787
+ signal: controller.signal,
788
+ headers: {
789
+ "content-type": "application/json",
790
+ ...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
791
+ },
792
+ body: JSON.stringify({
793
+ model: input.model,
794
+ instructions: REDUCTION_SYSTEM_INSTRUCTION,
795
+ input: input.prompt,
796
+ reasoning: {
797
+ effort: "minimal"
798
+ },
799
+ text: {
800
+ verbosity: "low",
801
+ ...input.responseMode === "json" && usesNativeJsonResponseFormat(input.jsonResponseFormat) ? {
802
+ format: {
803
+ type: "json_object"
804
+ }
805
+ } : {}
806
+ },
807
+ max_output_tokens: input.maxOutputTokens
808
+ })
809
+ });
810
+ if (!response.ok) {
811
+ throw await buildOpenAIError(response);
812
+ }
813
+ const data = await response.json();
814
+ const text = extractResponseText(data);
815
+ if (!text) {
816
+ throw new Error("Provider returned an empty response");
817
+ }
818
+ return {
819
+ text,
820
+ usage: data?.usage ? {
821
+ inputTokens: data.usage.input_tokens,
822
+ outputTokens: data.usage.output_tokens,
823
+ totalTokens: data.usage.total_tokens
824
+ } : void 0,
825
+ raw: data
826
+ };
827
+ } catch (error) {
828
+ if (error.name === "AbortError") {
829
+ throw new Error("Provider request timed out");
830
+ }
831
+ throw error;
832
+ } finally {
833
+ clearTimeout(timeout);
834
+ }
835
+ }
836
+ };
837
+
450
838
  // src/providers/openaiCompatible.ts
451
839
  function supportsNativeJsonResponseFormat(baseUrl, mode) {
452
840
  if (mode === "off") {
@@ -467,6 +855,18 @@ function extractMessageText(payload) {
467
855
  }
468
856
  return "";
469
857
  }
858
+ async function buildOpenAICompatibleError(response) {
859
+ let detail = `Provider returned HTTP ${response.status}`;
860
+ try {
861
+ const data = await response.json();
862
+ const message = data?.error?.message;
863
+ if (typeof message === "string" && message.trim().length > 0) {
864
+ detail = `${detail}: ${message.trim()}`;
865
+ }
866
+ } catch {
867
+ }
868
+ return new Error(detail);
869
+ }
470
870
  var OpenAICompatibleProvider = class {
471
871
  name = "openai-compatible";
472
872
  baseUrl;
@@ -495,7 +895,7 @@ var OpenAICompatibleProvider = class {
495
895
  messages: [
496
896
  {
497
897
  role: "system",
498
- content: "You reduce noisy command output into compact answers for agents and automation."
898
+ content: REDUCTION_SYSTEM_INSTRUCTION
499
899
  },
500
900
  {
501
901
  role: "user",
@@ -505,7 +905,7 @@ var OpenAICompatibleProvider = class {
505
905
  })
506
906
  });
507
907
  if (!response.ok) {
508
- throw new Error(`Provider returned HTTP ${response.status}`);
908
+ throw await buildOpenAICompatibleError(response);
509
909
  }
510
910
  const data = await response.json();
511
911
  const text = extractMessageText(data);
@@ -534,6 +934,12 @@ var OpenAICompatibleProvider = class {
534
934
 
535
935
  // src/providers/factory.ts
536
936
  function createProvider(config) {
937
+ if (config.provider.provider === "openai") {
938
+ return new OpenAIProvider({
939
+ baseUrl: config.provider.baseUrl,
940
+ apiKey: config.provider.apiKey
941
+ });
942
+ }
537
943
  if (config.provider.provider === "openai-compatible") {
538
944
  return new OpenAICompatibleProvider({
539
945
  baseUrl: config.provider.baseUrl,
@@ -659,6 +1065,33 @@ var BUILT_IN_POLICIES = {
659
1065
  `If there is no clear error signal, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
660
1066
  ]
661
1067
  },
1068
+ "typecheck-summary": {
1069
+ name: "typecheck-summary",
1070
+ responseMode: "text",
1071
+ taskRules: [
1072
+ "Return at most 5 short bullet points.",
1073
+ "Determine whether the typecheck failed or passed.",
1074
+ "Group repeated diagnostics into root-cause buckets instead of echoing many duplicate lines.",
1075
+ "Mention the first concrete files, symbols, or error categories to fix when they are visible.",
1076
+ "Prefer compiler or type-system errors over timing, progress, or summary noise.",
1077
+ "If the output clearly indicates success, say that briefly and do not add extra bullets.",
1078
+ `If you cannot tell whether the typecheck failed, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
1079
+ ]
1080
+ },
1081
+ "lint-failures": {
1082
+ name: "lint-failures",
1083
+ responseMode: "text",
1084
+ taskRules: [
1085
+ "Return at most 5 short bullet points.",
1086
+ "Determine whether lint failed or whether there are no blocking lint failures.",
1087
+ "Group repeated rule violations instead of listing the same rule many times.",
1088
+ "Mention the top offending files and rule names when they are visible.",
1089
+ "Distinguish blocking failures from warnings only when that distinction is clearly visible in the input.",
1090
+ "Do not invent autofixability; only mention autofix or --fix support when the tool output explicitly says so.",
1091
+ "If the output clearly indicates success or no blocking failures, say that briefly and stop.",
1092
+ `If there is not enough evidence to determine the lint result, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
1093
+ ]
1094
+ },
662
1095
  "infra-risk": {
663
1096
  name: "infra-risk",
664
1097
  responseMode: "json",
@@ -1228,6 +1661,15 @@ function buildCommandPreview(request) {
1228
1661
  }
1229
1662
  return (request.command ?? []).join(" ");
1230
1663
  }
1664
+ function getExecSuccessShortcut(args) {
1665
+ if (args.exitCode !== 0) {
1666
+ return null;
1667
+ }
1668
+ if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
1669
+ return "No type errors.";
1670
+ }
1671
+ return null;
1672
+ }
1231
1673
  async function runExec(request) {
1232
1674
  const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
1233
1675
  const hasShellCommand = typeof request.shellCommand === "string";
@@ -1292,6 +1734,7 @@ async function runExec(request) {
1292
1734
  throw childSpawnError;
1293
1735
  }
1294
1736
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
1737
+ const capturedOutput = capture.render();
1295
1738
  if (request.config.runtime.verbose) {
1296
1739
  process.stderr.write(
1297
1740
  `${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
@@ -1299,12 +1742,34 @@ async function runExec(request) {
1299
1742
  );
1300
1743
  }
1301
1744
  if (!bypassed) {
1745
+ const execSuccessShortcut = getExecSuccessShortcut({
1746
+ presetName: request.presetName,
1747
+ exitCode,
1748
+ capturedOutput
1749
+ });
1750
+ if (execSuccessShortcut && !request.dryRun) {
1751
+ if (request.config.runtime.verbose) {
1752
+ process.stderr.write(
1753
+ `${pc2.dim("sift")} exec_shortcut=${request.presetName}
1754
+ `
1755
+ );
1756
+ }
1757
+ process.stdout.write(`${execSuccessShortcut}
1758
+ `);
1759
+ return exitCode;
1760
+ }
1302
1761
  const output = await runSift({
1303
1762
  ...request,
1304
- stdin: capture.render()
1763
+ stdin: capturedOutput
1305
1764
  });
1306
1765
  process.stdout.write(`${output}
1307
1766
  `);
1767
+ if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
1768
+ presetName: request.presetName,
1769
+ output
1770
+ }).shouldFail) {
1771
+ return 1;
1772
+ }
1308
1773
  }
1309
1774
  return exitCode;
1310
1775
  }
@@ -1334,6 +1799,12 @@ function getPreset(config, name) {
1334
1799
  var require2 = createRequire(import.meta.url);
1335
1800
  var pkg = require2("../package.json");
1336
1801
  var cli = cac("sift");
1802
+ var HELP_BANNER = [
1803
+ " \\\\ //",
1804
+ " \\\\//",
1805
+ " ||",
1806
+ " o"
1807
+ ].join("\n");
1337
1808
  function toNumber(value) {
1338
1809
  if (value === void 0 || value === null || value === "") {
1339
1810
  return void 0;
@@ -1372,15 +1843,25 @@ function buildCliOverrides(options) {
1372
1843
  return overrides;
1373
1844
  }
1374
1845
  function applySharedOptions(command) {
1375
- return command.option("--provider <provider>", "Provider: openai-compatible").option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
1846
+ return command.option("--provider <provider>", "Provider: openai | openai-compatible").option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
1376
1847
  "--api-key <key>",
1377
- "Provider API key (or set SIFT_PROVIDER_API_KEY; OPENAI_API_KEY also works for api.openai.com)"
1848
+ "Provider API key (or set OPENAI_API_KEY for provider=openai; use SIFT_PROVIDER_API_KEY or endpoint-native envs for openai-compatible)"
1378
1849
  ).option(
1379
1850
  "--json-response-format <mode>",
1380
1851
  "JSON response format mode: auto | on | off"
1381
- ).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("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
1852
+ ).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(
1853
+ "--fail-on",
1854
+ "Fail with exit code 1 when a supported built-in preset produces a blocking result"
1855
+ ).option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
1382
1856
  }
1383
1857
  async function executeRun(args) {
1858
+ if (Boolean(args.options.failOn)) {
1859
+ assertSupportedFailOnPreset(args.presetName);
1860
+ assertSupportedFailOnFormat({
1861
+ presetName: args.presetName,
1862
+ format: args.format
1863
+ });
1864
+ }
1384
1865
  const config = resolveConfig({
1385
1866
  configPath: args.options.config,
1386
1867
  env: process.env,
@@ -1393,12 +1874,19 @@ async function executeRun(args) {
1393
1874
  stdin,
1394
1875
  config,
1395
1876
  dryRun: Boolean(args.options.dryRun),
1877
+ presetName: args.presetName,
1396
1878
  policyName: args.policyName,
1397
1879
  outputContract: args.outputContract,
1398
1880
  fallbackJson: args.fallbackJson
1399
1881
  });
1400
1882
  process.stdout.write(`${output}
1401
1883
  `);
1884
+ if (Boolean(args.options.failOn) && !Boolean(args.options.dryRun) && args.presetName && evaluateGate({
1885
+ presetName: args.presetName,
1886
+ output
1887
+ }).shouldFail) {
1888
+ process.exitCode = 1;
1889
+ }
1402
1890
  }
1403
1891
  function extractExecCommand(options) {
1404
1892
  const passthrough = Array.isArray(options["--"]) ? options["--"].map((value) => String(value)) : [];
@@ -1415,6 +1903,13 @@ function extractExecCommand(options) {
1415
1903
  };
1416
1904
  }
1417
1905
  async function executeExec(args) {
1906
+ if (Boolean(args.options.failOn)) {
1907
+ assertSupportedFailOnPreset(args.presetName);
1908
+ assertSupportedFailOnFormat({
1909
+ presetName: args.presetName,
1910
+ format: args.format
1911
+ });
1912
+ }
1418
1913
  const config = resolveConfig({
1419
1914
  configPath: args.options.config,
1420
1915
  env: process.env,
@@ -1426,6 +1921,8 @@ async function executeExec(args) {
1426
1921
  format: args.format,
1427
1922
  config,
1428
1923
  dryRun: Boolean(args.options.dryRun),
1924
+ failOn: Boolean(args.options.failOn),
1925
+ presetName: args.presetName,
1429
1926
  policyName: args.policyName,
1430
1927
  outputContract: args.outputContract,
1431
1928
  fallbackJson: args.fallbackJson,
@@ -1444,6 +1941,7 @@ applySharedOptions(
1444
1941
  await executeRun({
1445
1942
  question: preset.question,
1446
1943
  format: options.format ?? preset.format,
1944
+ presetName: name,
1447
1945
  policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
1448
1946
  options,
1449
1947
  outputContract: preset.outputContract,
@@ -1472,6 +1970,7 @@ applySharedOptions(
1472
1970
  await executeExec({
1473
1971
  question: preset.question,
1474
1972
  format: options.format ?? preset.format,
1973
+ presetName,
1475
1974
  policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
1476
1975
  options,
1477
1976
  outputContract: preset.outputContract,
@@ -1491,10 +1990,23 @@ applySharedOptions(
1491
1990
  });
1492
1991
  cli.command(
1493
1992
  "config <action>",
1494
- "Config commands: init | show | validate (show/validate use resolved runtime config)"
1495
- ).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) => {
1993
+ "Config commands: setup | init | show | validate (show/validate use resolved runtime config)"
1994
+ ).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(
1995
+ "--global",
1996
+ "Use the machine-wide config path (~/.config/sift/config.yaml) for init or setup"
1997
+ ).option("--config <path>", "Path to config file").option("--show-secrets", "Show secret values in config show").action(async (action, options) => {
1998
+ if (action === "setup") {
1999
+ process.exitCode = await configSetup({
2000
+ targetPath: options.path,
2001
+ global: Boolean(options.global)
2002
+ });
2003
+ return;
2004
+ }
1496
2005
  if (action === "init") {
1497
- configInit(options.path);
2006
+ configInit(
2007
+ options.path,
2008
+ Boolean(options.global)
2009
+ );
1498
2010
  return;
1499
2011
  }
1500
2012
  if (action === "show") {
@@ -1511,11 +2023,12 @@ cli.command(
1511
2023
  throw new Error(`Unknown config action: ${action}`);
1512
2024
  });
1513
2025
  cli.command("doctor", "Check local runtime config completeness").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {
2026
+ const configPath = findConfigPath(options.config);
1514
2027
  const config = resolveConfig({
1515
2028
  configPath: options.config,
1516
2029
  env: process.env
1517
2030
  });
1518
- process.exitCode = runDoctor(config);
2031
+ process.exitCode = runDoctor(config, configPath);
1519
2032
  });
1520
2033
  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) => {
1521
2034
  const config = resolveConfig({
@@ -1548,7 +2061,13 @@ applySharedOptions(
1548
2061
  options
1549
2062
  });
1550
2063
  });
1551
- cli.help();
2064
+ cli.help((sections) => [
2065
+ {
2066
+ body: `${HELP_BANNER}
2067
+ `
2068
+ },
2069
+ ...sections
2070
+ ]);
1552
2071
  cli.version(pkg.version);
1553
2072
  async function main() {
1554
2073
  cli.parse(process.argv, { run: false });