@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/index.d.ts CHANGED
@@ -1,8 +1,8 @@
1
- type ProviderName = "openai-compatible";
1
+ type ProviderName = "openai" | "openai-compatible";
2
2
  type OutputFormat = "brief" | "bullets" | "json" | "verdict";
3
3
  type ResponseMode = "text" | "json";
4
4
  type JsonResponseFormatMode = "auto" | "on" | "off";
5
- type PromptPolicyName = "test-status" | "audit-critical" | "diff-summary" | "build-failure" | "log-errors" | "infra-risk";
5
+ type PromptPolicyName = "test-status" | "audit-critical" | "diff-summary" | "build-failure" | "log-errors" | "infra-risk" | "typecheck-summary" | "lint-failures";
6
6
  interface ProviderConfig {
7
7
  provider: ProviderName;
8
8
  model: string;
@@ -70,6 +70,7 @@ interface RunRequest {
70
70
  stdin: string;
71
71
  config: SiftConfig;
72
72
  dryRun?: boolean;
73
+ presetName?: string;
73
74
  policyName?: PromptPolicyName;
74
75
  outputContract?: string;
75
76
  fallbackJson?: unknown;
@@ -89,6 +90,7 @@ interface PreparedInput {
89
90
 
90
91
  interface ExecRequest extends Omit<RunRequest, "stdin"> {
91
92
  command?: string[];
93
+ failOn?: boolean;
92
94
  shellCommand?: string;
93
95
  }
94
96
  declare function runExec(request: ExecRequest): Promise<number>;
package/dist/index.js CHANGED
@@ -6,19 +6,150 @@ import pc2 from "picocolors";
6
6
  // src/constants.ts
7
7
  import os from "os";
8
8
  import path from "path";
9
- var DEFAULT_CONFIG_SEARCH_PATHS = [
10
- path.resolve(process.cwd(), "sift.config.yaml"),
11
- path.resolve(process.cwd(), "sift.config.yml"),
12
- path.join(os.homedir(), ".config", "sift", "config.yaml"),
13
- path.join(os.homedir(), ".config", "sift", "config.yml")
14
- ];
9
+ function getDefaultGlobalConfigPath() {
10
+ return path.join(os.homedir(), ".config", "sift", "config.yaml");
11
+ }
12
+ function getDefaultConfigSearchPaths() {
13
+ return [
14
+ path.resolve(process.cwd(), "sift.config.yaml"),
15
+ path.resolve(process.cwd(), "sift.config.yml"),
16
+ getDefaultGlobalConfigPath(),
17
+ path.join(os.homedir(), ".config", "sift", "config.yml")
18
+ ];
19
+ }
15
20
  var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
16
21
  var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
17
22
  var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
18
23
 
24
+ // src/core/gate.ts
25
+ var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
26
+ function parseJson(output) {
27
+ try {
28
+ return JSON.parse(output);
29
+ } catch {
30
+ return null;
31
+ }
32
+ }
33
+ function supportsFailOnPreset(presetName) {
34
+ return typeof presetName === "string" && FAIL_ON_SUPPORTED_PRESETS.has(presetName);
35
+ }
36
+ function evaluateGate(args) {
37
+ const parsed = parseJson(args.output);
38
+ if (!parsed || typeof parsed !== "object") {
39
+ return { shouldFail: false };
40
+ }
41
+ if (args.presetName === "infra-risk") {
42
+ return {
43
+ shouldFail: parsed["verdict"] === "fail"
44
+ };
45
+ }
46
+ if (args.presetName === "audit-critical") {
47
+ const status = parsed["status"];
48
+ const vulnerabilities = parsed["vulnerabilities"];
49
+ return {
50
+ shouldFail: status === "ok" && Array.isArray(vulnerabilities) && vulnerabilities.length > 0
51
+ };
52
+ }
53
+ return { shouldFail: false };
54
+ }
55
+
19
56
  // src/core/run.ts
20
57
  import pc from "picocolors";
21
58
 
59
+ // src/providers/systemInstruction.ts
60
+ var REDUCTION_SYSTEM_INSTRUCTION = "You reduce noisy command output into compact answers for agents and automation.";
61
+
62
+ // src/providers/openai.ts
63
+ function usesNativeJsonResponseFormat(mode) {
64
+ return mode !== "off";
65
+ }
66
+ function extractResponseText(payload) {
67
+ if (typeof payload?.output_text === "string") {
68
+ return payload.output_text.trim();
69
+ }
70
+ if (!Array.isArray(payload?.output)) {
71
+ return "";
72
+ }
73
+ 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();
74
+ }
75
+ async function buildOpenAIError(response) {
76
+ let detail = `Provider returned HTTP ${response.status}`;
77
+ try {
78
+ const data = await response.json();
79
+ const message = data?.error?.message;
80
+ if (typeof message === "string" && message.trim().length > 0) {
81
+ detail = `${detail}: ${message.trim()}`;
82
+ }
83
+ } catch {
84
+ }
85
+ return new Error(detail);
86
+ }
87
+ var OpenAIProvider = class {
88
+ name = "openai";
89
+ baseUrl;
90
+ apiKey;
91
+ constructor(options) {
92
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
93
+ this.apiKey = options.apiKey;
94
+ }
95
+ async generate(input) {
96
+ const controller = new AbortController();
97
+ const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
98
+ try {
99
+ const url = new URL("responses", `${this.baseUrl}/`);
100
+ const response = await fetch(url, {
101
+ method: "POST",
102
+ signal: controller.signal,
103
+ headers: {
104
+ "content-type": "application/json",
105
+ ...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
106
+ },
107
+ body: JSON.stringify({
108
+ model: input.model,
109
+ instructions: REDUCTION_SYSTEM_INSTRUCTION,
110
+ input: input.prompt,
111
+ reasoning: {
112
+ effort: "minimal"
113
+ },
114
+ text: {
115
+ verbosity: "low",
116
+ ...input.responseMode === "json" && usesNativeJsonResponseFormat(input.jsonResponseFormat) ? {
117
+ format: {
118
+ type: "json_object"
119
+ }
120
+ } : {}
121
+ },
122
+ max_output_tokens: input.maxOutputTokens
123
+ })
124
+ });
125
+ if (!response.ok) {
126
+ throw await buildOpenAIError(response);
127
+ }
128
+ const data = await response.json();
129
+ const text = extractResponseText(data);
130
+ if (!text) {
131
+ throw new Error("Provider returned an empty response");
132
+ }
133
+ return {
134
+ text,
135
+ usage: data?.usage ? {
136
+ inputTokens: data.usage.input_tokens,
137
+ outputTokens: data.usage.output_tokens,
138
+ totalTokens: data.usage.total_tokens
139
+ } : void 0,
140
+ raw: data
141
+ };
142
+ } catch (error) {
143
+ if (error.name === "AbortError") {
144
+ throw new Error("Provider request timed out");
145
+ }
146
+ throw error;
147
+ } finally {
148
+ clearTimeout(timeout);
149
+ }
150
+ }
151
+ };
152
+
22
153
  // src/providers/openaiCompatible.ts
23
154
  function supportsNativeJsonResponseFormat(baseUrl, mode) {
24
155
  if (mode === "off") {
@@ -39,6 +170,18 @@ function extractMessageText(payload) {
39
170
  }
40
171
  return "";
41
172
  }
173
+ async function buildOpenAICompatibleError(response) {
174
+ let detail = `Provider returned HTTP ${response.status}`;
175
+ try {
176
+ const data = await response.json();
177
+ const message = data?.error?.message;
178
+ if (typeof message === "string" && message.trim().length > 0) {
179
+ detail = `${detail}: ${message.trim()}`;
180
+ }
181
+ } catch {
182
+ }
183
+ return new Error(detail);
184
+ }
42
185
  var OpenAICompatibleProvider = class {
43
186
  name = "openai-compatible";
44
187
  baseUrl;
@@ -67,7 +210,7 @@ var OpenAICompatibleProvider = class {
67
210
  messages: [
68
211
  {
69
212
  role: "system",
70
- content: "You reduce noisy command output into compact answers for agents and automation."
213
+ content: REDUCTION_SYSTEM_INSTRUCTION
71
214
  },
72
215
  {
73
216
  role: "user",
@@ -77,7 +220,7 @@ var OpenAICompatibleProvider = class {
77
220
  })
78
221
  });
79
222
  if (!response.ok) {
80
- throw new Error(`Provider returned HTTP ${response.status}`);
223
+ throw await buildOpenAICompatibleError(response);
81
224
  }
82
225
  const data = await response.json();
83
226
  const text = extractMessageText(data);
@@ -106,6 +249,12 @@ var OpenAICompatibleProvider = class {
106
249
 
107
250
  // src/providers/factory.ts
108
251
  function createProvider(config) {
252
+ if (config.provider.provider === "openai") {
253
+ return new OpenAIProvider({
254
+ baseUrl: config.provider.baseUrl,
255
+ apiKey: config.provider.apiKey
256
+ });
257
+ }
109
258
  if (config.provider.provider === "openai-compatible") {
110
259
  return new OpenAICompatibleProvider({
111
260
  baseUrl: config.provider.baseUrl,
@@ -231,6 +380,33 @@ var BUILT_IN_POLICIES = {
231
380
  `If there is no clear error signal, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
232
381
  ]
233
382
  },
383
+ "typecheck-summary": {
384
+ name: "typecheck-summary",
385
+ responseMode: "text",
386
+ taskRules: [
387
+ "Return at most 5 short bullet points.",
388
+ "Determine whether the typecheck failed or passed.",
389
+ "Group repeated diagnostics into root-cause buckets instead of echoing many duplicate lines.",
390
+ "Mention the first concrete files, symbols, or error categories to fix when they are visible.",
391
+ "Prefer compiler or type-system errors over timing, progress, or summary noise.",
392
+ "If the output clearly indicates success, say that briefly and do not add extra bullets.",
393
+ `If you cannot tell whether the typecheck failed, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
394
+ ]
395
+ },
396
+ "lint-failures": {
397
+ name: "lint-failures",
398
+ responseMode: "text",
399
+ taskRules: [
400
+ "Return at most 5 short bullet points.",
401
+ "Determine whether lint failed or whether there are no blocking lint failures.",
402
+ "Group repeated rule violations instead of listing the same rule many times.",
403
+ "Mention the top offending files and rule names when they are visible.",
404
+ "Distinguish blocking failures from warnings only when that distinction is clearly visible in the input.",
405
+ "Do not invent autofixability; only mention autofix or --fix support when the tool output explicitly says so.",
406
+ "If the output clearly indicates success or no blocking failures, say that briefly and stop.",
407
+ `If there is not enough evidence to determine the lint result, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
408
+ ]
409
+ },
234
410
  "infra-risk": {
235
411
  name: "infra-risk",
236
412
  responseMode: "json",
@@ -800,6 +976,15 @@ function buildCommandPreview(request) {
800
976
  }
801
977
  return (request.command ?? []).join(" ");
802
978
  }
979
+ function getExecSuccessShortcut(args) {
980
+ if (args.exitCode !== 0) {
981
+ return null;
982
+ }
983
+ if (args.presetName === "typecheck-summary" && args.capturedOutput.trim() === "") {
984
+ return "No type errors.";
985
+ }
986
+ return null;
987
+ }
803
988
  async function runExec(request) {
804
989
  const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
805
990
  const hasShellCommand = typeof request.shellCommand === "string";
@@ -864,6 +1049,7 @@ async function runExec(request) {
864
1049
  throw childSpawnError;
865
1050
  }
866
1051
  const exitCode = normalizeChildExitCode(childStatus, childSignal);
1052
+ const capturedOutput = capture.render();
867
1053
  if (request.config.runtime.verbose) {
868
1054
  process.stderr.write(
869
1055
  `${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
@@ -871,12 +1057,34 @@ async function runExec(request) {
871
1057
  );
872
1058
  }
873
1059
  if (!bypassed) {
1060
+ const execSuccessShortcut = getExecSuccessShortcut({
1061
+ presetName: request.presetName,
1062
+ exitCode,
1063
+ capturedOutput
1064
+ });
1065
+ if (execSuccessShortcut && !request.dryRun) {
1066
+ if (request.config.runtime.verbose) {
1067
+ process.stderr.write(
1068
+ `${pc2.dim("sift")} exec_shortcut=${request.presetName}
1069
+ `
1070
+ );
1071
+ }
1072
+ process.stdout.write(`${execSuccessShortcut}
1073
+ `);
1074
+ return exitCode;
1075
+ }
874
1076
  const output = await runSift({
875
1077
  ...request,
876
- stdin: capture.render()
1078
+ stdin: capturedOutput
877
1079
  });
878
1080
  process.stdout.write(`${output}
879
1081
  `);
1082
+ if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
1083
+ presetName: request.presetName,
1084
+ output
1085
+ }).shouldFail) {
1086
+ return 1;
1087
+ }
880
1088
  }
881
1089
  return exitCode;
882
1090
  }
@@ -884,23 +1092,23 @@ async function runExec(request) {
884
1092
  // src/config/defaults.ts
885
1093
  var defaultConfig = {
886
1094
  provider: {
887
- provider: "openai-compatible",
888
- model: "gpt-4.1-mini",
1095
+ provider: "openai",
1096
+ model: "gpt-5-nano",
889
1097
  baseUrl: "https://api.openai.com/v1",
890
1098
  apiKey: "",
891
1099
  jsonResponseFormat: "auto",
892
1100
  timeoutMs: 2e4,
893
1101
  temperature: 0.1,
894
- maxOutputTokens: 220
1102
+ maxOutputTokens: 400
895
1103
  },
896
1104
  input: {
897
1105
  stripAnsi: true,
898
1106
  redact: false,
899
1107
  redactStrict: false,
900
- maxCaptureChars: 25e4,
901
- maxInputChars: 2e4,
902
- headChars: 6e3,
903
- tailChars: 6e3
1108
+ maxCaptureChars: 4e5,
1109
+ maxInputChars: 6e4,
1110
+ headChars: 2e4,
1111
+ tailChars: 2e4
904
1112
  },
905
1113
  runtime: {
906
1114
  rawFallback: true,
@@ -934,6 +1142,16 @@ var defaultConfig = {
934
1142
  format: "bullets",
935
1143
  policy: "log-errors"
936
1144
  },
1145
+ "typecheck-summary": {
1146
+ question: "Summarize the blocking typecheck failures. Group repeated errors by root cause and point to the first files or symbols to fix.",
1147
+ format: "bullets",
1148
+ policy: "typecheck-summary"
1149
+ },
1150
+ "lint-failures": {
1151
+ 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.",
1152
+ format: "bullets",
1153
+ policy: "lint-failures"
1154
+ },
937
1155
  "infra-risk": {
938
1156
  question: "Assess whether the infrastructure changes are risky and whether they look safe to apply.",
939
1157
  format: "verdict",
@@ -954,7 +1172,7 @@ function findConfigPath(explicitPath) {
954
1172
  }
955
1173
  return resolved;
956
1174
  }
957
- for (const candidate of DEFAULT_CONFIG_SEARCH_PATHS) {
1175
+ for (const candidate of getDefaultConfigSearchPaths()) {
958
1176
  if (fs.existsSync(candidate)) {
959
1177
  return candidate;
960
1178
  }
@@ -1002,23 +1220,26 @@ function resolveCompatibleEnvName(baseUrl) {
1002
1220
  return match?.envName;
1003
1221
  }
1004
1222
  function resolveProviderApiKey(provider, baseUrl, env) {
1005
- if (env.SIFT_PROVIDER_API_KEY) {
1006
- return env.SIFT_PROVIDER_API_KEY;
1007
- }
1008
1223
  if (provider === "openai-compatible") {
1224
+ if (env.SIFT_PROVIDER_API_KEY) {
1225
+ return env.SIFT_PROVIDER_API_KEY;
1226
+ }
1009
1227
  const envName2 = resolveCompatibleEnvName(baseUrl);
1010
1228
  return envName2 ? env[envName2] : void 0;
1011
1229
  }
1012
1230
  if (!provider) {
1013
- return void 0;
1231
+ return env.SIFT_PROVIDER_API_KEY;
1014
1232
  }
1015
1233
  const envName = PROVIDER_API_KEY_ENV[provider];
1016
- return envName ? env[envName] : void 0;
1234
+ if (envName && env[envName]) {
1235
+ return env[envName];
1236
+ }
1237
+ return env.SIFT_PROVIDER_API_KEY;
1017
1238
  }
1018
1239
 
1019
1240
  // src/config/schema.ts
1020
1241
  import { z } from "zod";
1021
- var providerNameSchema = z.enum(["openai-compatible"]);
1242
+ var providerNameSchema = z.enum(["openai", "openai-compatible"]);
1022
1243
  var outputFormatSchema = z.enum([
1023
1244
  "brief",
1024
1245
  "bullets",
@@ -1033,7 +1254,9 @@ var promptPolicyNameSchema = z.enum([
1033
1254
  "diff-summary",
1034
1255
  "build-failure",
1035
1256
  "log-errors",
1036
- "infra-risk"
1257
+ "infra-risk",
1258
+ "typecheck-summary",
1259
+ "lint-failures"
1037
1260
  ]);
1038
1261
  var providerConfigSchema = z.object({
1039
1262
  provider: providerNameSchema,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bilalimamoglu/sift",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Agent-first command-output reduction layer for agents, CI, and automation.",
5
5
  "type": "module",
6
6
  "bin": {