@bilalimamoglu/sift 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,12 +1,14 @@
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
- type PromptPolicyName = "test-status" | "audit-critical" | "diff-summary" | "build-failure" | "log-errors" | "infra-risk";
4
+ type JsonResponseFormatMode = "auto" | "on" | "off";
5
+ type PromptPolicyName = "test-status" | "audit-critical" | "diff-summary" | "build-failure" | "log-errors" | "infra-risk" | "typecheck-summary" | "lint-failures";
5
6
  interface ProviderConfig {
6
7
  provider: ProviderName;
7
8
  model: string;
8
9
  baseUrl: string;
9
10
  apiKey?: string;
11
+ jsonResponseFormat: JsonResponseFormatMode;
10
12
  timeoutMs: number;
11
13
  temperature: number;
12
14
  maxOutputTokens: number;
@@ -50,6 +52,7 @@ interface GenerateInput {
50
52
  maxOutputTokens: number;
51
53
  timeoutMs: number;
52
54
  responseMode: ResponseMode;
55
+ jsonResponseFormat: JsonResponseFormatMode;
53
56
  }
54
57
  interface UsageInfo {
55
58
  inputTokens?: number;
@@ -66,6 +69,8 @@ interface RunRequest {
66
69
  format: OutputFormat;
67
70
  stdin: string;
68
71
  config: SiftConfig;
72
+ dryRun?: boolean;
73
+ presetName?: string;
69
74
  policyName?: PromptPolicyName;
70
75
  outputContract?: string;
71
76
  fallbackJson?: unknown;
@@ -85,6 +90,7 @@ interface PreparedInput {
85
90
 
86
91
  interface ExecRequest extends Omit<RunRequest, "stdin"> {
87
92
  command?: string[];
93
+ failOn?: boolean;
88
94
  shellCommand?: string;
89
95
  }
90
96
  declare function runExec(request: ExecRequest): Promise<number>;
@@ -103,4 +109,4 @@ interface ResolveOptions {
103
109
  }
104
110
  declare function resolveConfig(options?: ResolveOptions): SiftConfig;
105
111
 
106
- export { type ExecRequest, type GenerateInput, type GenerateResult, type InputConfig, type LLMProvider, type OutputFormat, type PartialSiftConfig, type PreparedInput, type PresetDefinition, type PromptPolicyName, type ProviderConfig, type ProviderName, type ResolveOptions, type ResponseMode, type RunRequest, type RuntimeConfig, type SiftConfig, type UsageInfo, resolveConfig, runExec, runSift };
112
+ export { type ExecRequest, type GenerateInput, type GenerateResult, type InputConfig, type JsonResponseFormatMode, type LLMProvider, type OutputFormat, type PartialSiftConfig, type PreparedInput, type PresetDefinition, type PromptPolicyName, type ProviderConfig, type ProviderName, type ResolveOptions, type ResponseMode, type RunRequest, type RuntimeConfig, type SiftConfig, type UsageInfo, resolveConfig, runExec, runSift };
package/dist/index.js CHANGED
@@ -16,10 +16,145 @@ var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
16
16
  var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
17
17
  var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
18
18
 
19
+ // src/core/gate.ts
20
+ var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
21
+ function parseJson(output) {
22
+ try {
23
+ return JSON.parse(output);
24
+ } catch {
25
+ return null;
26
+ }
27
+ }
28
+ function supportsFailOnPreset(presetName) {
29
+ return typeof presetName === "string" && FAIL_ON_SUPPORTED_PRESETS.has(presetName);
30
+ }
31
+ function evaluateGate(args) {
32
+ const parsed = parseJson(args.output);
33
+ if (!parsed || typeof parsed !== "object") {
34
+ return { shouldFail: false };
35
+ }
36
+ if (args.presetName === "infra-risk") {
37
+ return {
38
+ shouldFail: parsed["verdict"] === "fail"
39
+ };
40
+ }
41
+ if (args.presetName === "audit-critical") {
42
+ const status = parsed["status"];
43
+ const vulnerabilities = parsed["vulnerabilities"];
44
+ return {
45
+ shouldFail: status === "ok" && Array.isArray(vulnerabilities) && vulnerabilities.length > 0
46
+ };
47
+ }
48
+ return { shouldFail: false };
49
+ }
50
+
19
51
  // src/core/run.ts
20
52
  import pc from "picocolors";
21
53
 
54
+ // src/providers/systemInstruction.ts
55
+ var REDUCTION_SYSTEM_INSTRUCTION = "You reduce noisy command output into compact answers for agents and automation.";
56
+
57
+ // src/providers/openai.ts
58
+ function usesNativeJsonResponseFormat(mode) {
59
+ return mode !== "off";
60
+ }
61
+ function extractResponseText(payload) {
62
+ if (typeof payload?.output_text === "string") {
63
+ return payload.output_text.trim();
64
+ }
65
+ if (!Array.isArray(payload?.output)) {
66
+ return "";
67
+ }
68
+ 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();
69
+ }
70
+ async function buildOpenAIError(response) {
71
+ let detail = `Provider returned HTTP ${response.status}`;
72
+ try {
73
+ const data = await response.json();
74
+ const message = data?.error?.message;
75
+ if (typeof message === "string" && message.trim().length > 0) {
76
+ detail = `${detail}: ${message.trim()}`;
77
+ }
78
+ } catch {
79
+ }
80
+ return new Error(detail);
81
+ }
82
+ var OpenAIProvider = class {
83
+ name = "openai";
84
+ baseUrl;
85
+ apiKey;
86
+ constructor(options) {
87
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
88
+ this.apiKey = options.apiKey;
89
+ }
90
+ async generate(input) {
91
+ const controller = new AbortController();
92
+ const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
93
+ try {
94
+ const url = new URL("responses", `${this.baseUrl}/`);
95
+ const response = await fetch(url, {
96
+ method: "POST",
97
+ signal: controller.signal,
98
+ headers: {
99
+ "content-type": "application/json",
100
+ ...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
101
+ },
102
+ body: JSON.stringify({
103
+ model: input.model,
104
+ instructions: REDUCTION_SYSTEM_INSTRUCTION,
105
+ input: input.prompt,
106
+ reasoning: {
107
+ effort: "minimal"
108
+ },
109
+ text: {
110
+ verbosity: "low",
111
+ ...input.responseMode === "json" && usesNativeJsonResponseFormat(input.jsonResponseFormat) ? {
112
+ format: {
113
+ type: "json_object"
114
+ }
115
+ } : {}
116
+ },
117
+ max_output_tokens: input.maxOutputTokens
118
+ })
119
+ });
120
+ if (!response.ok) {
121
+ throw await buildOpenAIError(response);
122
+ }
123
+ const data = await response.json();
124
+ const text = extractResponseText(data);
125
+ if (!text) {
126
+ throw new Error("Provider returned an empty response");
127
+ }
128
+ return {
129
+ text,
130
+ usage: data?.usage ? {
131
+ inputTokens: data.usage.input_tokens,
132
+ outputTokens: data.usage.output_tokens,
133
+ totalTokens: data.usage.total_tokens
134
+ } : void 0,
135
+ raw: data
136
+ };
137
+ } catch (error) {
138
+ if (error.name === "AbortError") {
139
+ throw new Error("Provider request timed out");
140
+ }
141
+ throw error;
142
+ } finally {
143
+ clearTimeout(timeout);
144
+ }
145
+ }
146
+ };
147
+
22
148
  // src/providers/openaiCompatible.ts
149
+ function supportsNativeJsonResponseFormat(baseUrl, mode) {
150
+ if (mode === "off") {
151
+ return false;
152
+ }
153
+ if (mode === "on") {
154
+ return true;
155
+ }
156
+ return /^https:\/\/api\.openai\.com(?:\/|$)/i.test(baseUrl);
157
+ }
23
158
  function extractMessageText(payload) {
24
159
  const content = payload?.choices?.[0]?.message?.content;
25
160
  if (typeof content === "string") {
@@ -30,6 +165,18 @@ function extractMessageText(payload) {
30
165
  }
31
166
  return "";
32
167
  }
168
+ async function buildOpenAICompatibleError(response) {
169
+ let detail = `Provider returned HTTP ${response.status}`;
170
+ try {
171
+ const data = await response.json();
172
+ const message = data?.error?.message;
173
+ if (typeof message === "string" && message.trim().length > 0) {
174
+ detail = `${detail}: ${message.trim()}`;
175
+ }
176
+ } catch {
177
+ }
178
+ return new Error(detail);
179
+ }
33
180
  var OpenAICompatibleProvider = class {
34
181
  name = "openai-compatible";
35
182
  baseUrl;
@@ -54,10 +201,11 @@ var OpenAICompatibleProvider = class {
54
201
  model: input.model,
55
202
  temperature: input.temperature,
56
203
  max_tokens: input.maxOutputTokens,
204
+ ...input.responseMode === "json" && supportsNativeJsonResponseFormat(this.baseUrl, input.jsonResponseFormat) ? { response_format: { type: "json_object" } } : {},
57
205
  messages: [
58
206
  {
59
207
  role: "system",
60
- content: "You reduce noisy command output into compact answers for agents and automation."
208
+ content: REDUCTION_SYSTEM_INSTRUCTION
61
209
  },
62
210
  {
63
211
  role: "user",
@@ -67,7 +215,7 @@ var OpenAICompatibleProvider = class {
67
215
  })
68
216
  });
69
217
  if (!response.ok) {
70
- throw new Error(`Provider returned HTTP ${response.status}`);
218
+ throw await buildOpenAICompatibleError(response);
71
219
  }
72
220
  const data = await response.json();
73
221
  const text = extractMessageText(data);
@@ -96,6 +244,12 @@ var OpenAICompatibleProvider = class {
96
244
 
97
245
  // src/providers/factory.ts
98
246
  function createProvider(config) {
247
+ if (config.provider.provider === "openai") {
248
+ return new OpenAIProvider({
249
+ baseUrl: config.provider.baseUrl,
250
+ apiKey: config.provider.apiKey
251
+ });
252
+ }
99
253
  if (config.provider.provider === "openai-compatible") {
100
254
  return new OpenAICompatibleProvider({
101
255
  baseUrl: config.provider.baseUrl,
@@ -221,6 +375,33 @@ var BUILT_IN_POLICIES = {
221
375
  `If there is no clear error signal, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
222
376
  ]
223
377
  },
378
+ "typecheck-summary": {
379
+ name: "typecheck-summary",
380
+ responseMode: "text",
381
+ taskRules: [
382
+ "Return at most 5 short bullet points.",
383
+ "Determine whether the typecheck failed or passed.",
384
+ "Group repeated diagnostics into root-cause buckets instead of echoing many duplicate lines.",
385
+ "Mention the first concrete files, symbols, or error categories to fix when they are visible.",
386
+ "Prefer compiler or type-system errors over timing, progress, or summary noise.",
387
+ "If the output clearly indicates success, say that briefly and do not add extra bullets.",
388
+ `If you cannot tell whether the typecheck failed, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
389
+ ]
390
+ },
391
+ "lint-failures": {
392
+ name: "lint-failures",
393
+ responseMode: "text",
394
+ taskRules: [
395
+ "Return at most 5 short bullet points.",
396
+ "Determine whether lint failed or whether there are no blocking lint failures.",
397
+ "Group repeated rule violations instead of listing the same rule many times.",
398
+ "Mention the top offending files and rule names when they are visible.",
399
+ "Distinguish blocking failures from warnings only when that distinction is clearly visible in the input.",
400
+ "Do not invent autofixability; only mention autofix or --fix support when the tool output explicitly says so.",
401
+ "If the output clearly indicates success or no blocking failures, say that briefly and stop.",
402
+ `If there is not enough evidence to determine the lint result, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
403
+ ]
404
+ },
224
405
  "infra-risk": {
225
406
  name: "infra-risk",
226
407
  responseMode: "json",
@@ -564,6 +745,7 @@ function prepareInput(raw, config) {
564
745
  }
565
746
 
566
747
  // src/core/run.ts
748
+ var RETRY_DELAY_MS = 300;
567
749
  function normalizeOutput(text, responseMode) {
568
750
  if (responseMode !== "json") {
569
751
  return text.trim();
@@ -575,6 +757,68 @@ function normalizeOutput(text, responseMode) {
575
757
  throw new Error("Provider returned invalid JSON");
576
758
  }
577
759
  }
760
+ function buildDryRunOutput(args) {
761
+ return JSON.stringify(
762
+ {
763
+ status: "dry-run",
764
+ strategy: args.heuristicOutput ? "heuristic" : "provider",
765
+ provider: {
766
+ name: args.providerName,
767
+ model: args.request.config.provider.model,
768
+ baseUrl: args.request.config.provider.baseUrl,
769
+ jsonResponseFormat: args.request.config.provider.jsonResponseFormat
770
+ },
771
+ question: args.request.question,
772
+ format: args.request.format,
773
+ responseMode: args.responseMode,
774
+ policy: args.request.policyName ?? null,
775
+ heuristicOutput: args.heuristicOutput ?? null,
776
+ input: {
777
+ originalLength: args.prepared.meta.originalLength,
778
+ finalLength: args.prepared.meta.finalLength,
779
+ redactionApplied: args.prepared.meta.redactionApplied,
780
+ truncatedApplied: args.prepared.meta.truncatedApplied,
781
+ text: args.prepared.truncated
782
+ },
783
+ prompt: args.prompt
784
+ },
785
+ null,
786
+ 2
787
+ );
788
+ }
789
+ async function delay(ms) {
790
+ await new Promise((resolve) => setTimeout(resolve, ms));
791
+ }
792
+ async function generateWithRetry(args) {
793
+ let lastError;
794
+ for (let attempt = 0; attempt < 2; attempt += 1) {
795
+ try {
796
+ return await args.provider.generate({
797
+ model: args.request.config.provider.model,
798
+ prompt: args.prompt,
799
+ temperature: args.request.config.provider.temperature,
800
+ maxOutputTokens: args.request.config.provider.maxOutputTokens,
801
+ timeoutMs: args.request.config.provider.timeoutMs,
802
+ responseMode: args.responseMode,
803
+ jsonResponseFormat: args.request.config.provider.jsonResponseFormat
804
+ });
805
+ } catch (error) {
806
+ lastError = error;
807
+ const reason = error instanceof Error ? error.message : "unknown_error";
808
+ if (attempt > 0 || !isRetriableReason(reason)) {
809
+ throw error;
810
+ }
811
+ if (args.request.config.runtime.verbose) {
812
+ process.stderr.write(
813
+ `${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
814
+ `
815
+ );
816
+ }
817
+ await delay(RETRY_DELAY_MS);
818
+ }
819
+ }
820
+ throw lastError instanceof Error ? lastError : new Error("unknown_error");
821
+ }
578
822
  async function runSift(request) {
579
823
  const prepared = prepareInput(request.stdin, request.config.input);
580
824
  const { prompt, responseMode } = buildPrompt({
@@ -600,15 +844,33 @@ async function runSift(request) {
600
844
  process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
601
845
  `);
602
846
  }
847
+ if (request.dryRun) {
848
+ return buildDryRunOutput({
849
+ request,
850
+ providerName: provider.name,
851
+ prompt,
852
+ responseMode,
853
+ prepared,
854
+ heuristicOutput
855
+ });
856
+ }
603
857
  return heuristicOutput;
604
858
  }
859
+ if (request.dryRun) {
860
+ return buildDryRunOutput({
861
+ request,
862
+ providerName: provider.name,
863
+ prompt,
864
+ responseMode,
865
+ prepared,
866
+ heuristicOutput: null
867
+ });
868
+ }
605
869
  try {
606
- const result = await provider.generate({
607
- model: request.config.provider.model,
870
+ const result = await generateWithRetry({
871
+ provider,
872
+ request,
608
873
  prompt,
609
- temperature: request.config.provider.temperature,
610
- maxOutputTokens: request.config.provider.maxOutputTokens,
611
- timeoutMs: request.config.provider.timeoutMs,
612
874
  responseMode
613
875
  });
614
876
  if (looksLikeRejectedModelOutput({
@@ -786,6 +1048,12 @@ async function runExec(request) {
786
1048
  });
787
1049
  process.stdout.write(`${output}
788
1050
  `);
1051
+ if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
1052
+ presetName: request.presetName,
1053
+ output
1054
+ }).shouldFail) {
1055
+ return 1;
1056
+ }
789
1057
  }
790
1058
  return exitCode;
791
1059
  }
@@ -793,22 +1061,23 @@ async function runExec(request) {
793
1061
  // src/config/defaults.ts
794
1062
  var defaultConfig = {
795
1063
  provider: {
796
- provider: "openai-compatible",
797
- model: "gpt-4.1-mini",
1064
+ provider: "openai",
1065
+ model: "gpt-5-nano",
798
1066
  baseUrl: "https://api.openai.com/v1",
799
1067
  apiKey: "",
1068
+ jsonResponseFormat: "auto",
800
1069
  timeoutMs: 2e4,
801
1070
  temperature: 0.1,
802
- maxOutputTokens: 220
1071
+ maxOutputTokens: 400
803
1072
  },
804
1073
  input: {
805
1074
  stripAnsi: true,
806
1075
  redact: false,
807
1076
  redactStrict: false,
808
- maxCaptureChars: 25e4,
809
- maxInputChars: 2e4,
810
- headChars: 6e3,
811
- tailChars: 6e3
1077
+ maxCaptureChars: 4e5,
1078
+ maxInputChars: 6e4,
1079
+ headChars: 2e4,
1080
+ tailChars: 2e4
812
1081
  },
813
1082
  runtime: {
814
1083
  rawFallback: true,
@@ -842,6 +1111,16 @@ var defaultConfig = {
842
1111
  format: "bullets",
843
1112
  policy: "log-errors"
844
1113
  },
1114
+ "typecheck-summary": {
1115
+ question: "Summarize the blocking typecheck failures. Group repeated errors by root cause and point to the first files or symbols to fix.",
1116
+ format: "bullets",
1117
+ policy: "typecheck-summary"
1118
+ },
1119
+ "lint-failures": {
1120
+ 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.",
1121
+ format: "bullets",
1122
+ policy: "lint-failures"
1123
+ },
845
1124
  "infra-risk": {
846
1125
  question: "Assess whether the infrastructure changes are risky and whether they look safe to apply.",
847
1126
  format: "verdict",
@@ -878,9 +1157,58 @@ function loadRawConfig(explicitPath) {
878
1157
  return YAML.parse(content) ?? {};
879
1158
  }
880
1159
 
1160
+ // src/config/provider-api-key.ts
1161
+ var OPENAI_COMPATIBLE_BASE_URL_ENV = [
1162
+ { prefix: "https://api.openai.com/", envName: "OPENAI_API_KEY" },
1163
+ { prefix: "https://openrouter.ai/api/", envName: "OPENROUTER_API_KEY" },
1164
+ { prefix: "https://api.together.xyz/", envName: "TOGETHER_API_KEY" },
1165
+ { prefix: "https://api.groq.com/openai/", envName: "GROQ_API_KEY" }
1166
+ ];
1167
+ var PROVIDER_API_KEY_ENV = {
1168
+ anthropic: "ANTHROPIC_API_KEY",
1169
+ claude: "ANTHROPIC_API_KEY",
1170
+ groq: "GROQ_API_KEY",
1171
+ openai: "OPENAI_API_KEY",
1172
+ openrouter: "OPENROUTER_API_KEY",
1173
+ together: "TOGETHER_API_KEY"
1174
+ };
1175
+ function normalizeBaseUrl(baseUrl) {
1176
+ if (!baseUrl) {
1177
+ return void 0;
1178
+ }
1179
+ return `${baseUrl.replace(/\/+$/, "")}/`.toLowerCase();
1180
+ }
1181
+ function resolveCompatibleEnvName(baseUrl) {
1182
+ const normalized = normalizeBaseUrl(baseUrl);
1183
+ if (!normalized) {
1184
+ return void 0;
1185
+ }
1186
+ const match = OPENAI_COMPATIBLE_BASE_URL_ENV.find(
1187
+ (entry) => normalized.startsWith(entry.prefix)
1188
+ );
1189
+ return match?.envName;
1190
+ }
1191
+ function resolveProviderApiKey(provider, baseUrl, env) {
1192
+ if (provider === "openai-compatible") {
1193
+ if (env.SIFT_PROVIDER_API_KEY) {
1194
+ return env.SIFT_PROVIDER_API_KEY;
1195
+ }
1196
+ const envName2 = resolveCompatibleEnvName(baseUrl);
1197
+ return envName2 ? env[envName2] : void 0;
1198
+ }
1199
+ if (!provider) {
1200
+ return env.SIFT_PROVIDER_API_KEY;
1201
+ }
1202
+ const envName = PROVIDER_API_KEY_ENV[provider];
1203
+ if (envName && env[envName]) {
1204
+ return env[envName];
1205
+ }
1206
+ return env.SIFT_PROVIDER_API_KEY;
1207
+ }
1208
+
881
1209
  // src/config/schema.ts
882
1210
  import { z } from "zod";
883
- var providerNameSchema = z.enum(["openai-compatible"]);
1211
+ var providerNameSchema = z.enum(["openai", "openai-compatible"]);
884
1212
  var outputFormatSchema = z.enum([
885
1213
  "brief",
886
1214
  "bullets",
@@ -888,19 +1216,23 @@ var outputFormatSchema = z.enum([
888
1216
  "verdict"
889
1217
  ]);
890
1218
  var responseModeSchema = z.enum(["text", "json"]);
1219
+ var jsonResponseFormatModeSchema = z.enum(["auto", "on", "off"]);
891
1220
  var promptPolicyNameSchema = z.enum([
892
1221
  "test-status",
893
1222
  "audit-critical",
894
1223
  "diff-summary",
895
1224
  "build-failure",
896
1225
  "log-errors",
897
- "infra-risk"
1226
+ "infra-risk",
1227
+ "typecheck-summary",
1228
+ "lint-failures"
898
1229
  ]);
899
1230
  var providerConfigSchema = z.object({
900
1231
  provider: providerNameSchema,
901
1232
  model: z.string().min(1),
902
1233
  baseUrl: z.string().url(),
903
1234
  apiKey: z.string().optional(),
1235
+ jsonResponseFormat: jsonResponseFormatModeSchema,
904
1236
  timeoutMs: z.number().int().positive(),
905
1237
  temperature: z.number().min(0).max(2),
906
1238
  maxOutputTokens: z.number().int().positive()
@@ -954,14 +1286,25 @@ function mergeDefined(base, override) {
954
1286
  }
955
1287
  return result;
956
1288
  }
957
- function buildEnvOverrides(env) {
1289
+ function stripApiKey(overrides) {
1290
+ if (!overrides?.provider || overrides.provider.apiKey === void 0) {
1291
+ return overrides;
1292
+ }
1293
+ return {
1294
+ ...overrides,
1295
+ provider: {
1296
+ ...overrides.provider,
1297
+ apiKey: void 0
1298
+ }
1299
+ };
1300
+ }
1301
+ function buildNonCredentialEnvOverrides(env) {
958
1302
  const overrides = {};
959
- if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_API_KEY || env.SIFT_TIMEOUT_MS) {
1303
+ if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS) {
960
1304
  overrides.provider = {
961
1305
  provider: env.SIFT_PROVIDER,
962
1306
  model: env.SIFT_MODEL,
963
1307
  baseUrl: env.SIFT_BASE_URL,
964
- apiKey: env.SIFT_API_KEY,
965
1308
  timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
966
1309
  };
967
1310
  }
@@ -973,12 +1316,40 @@ function buildEnvOverrides(env) {
973
1316
  }
974
1317
  return overrides;
975
1318
  }
1319
+ function buildCredentialEnvOverrides(env, context) {
1320
+ const apiKey = resolveProviderApiKey(context.provider, context.baseUrl, env);
1321
+ if (apiKey === void 0) {
1322
+ return {};
1323
+ }
1324
+ return {
1325
+ provider: {
1326
+ apiKey
1327
+ }
1328
+ };
1329
+ }
976
1330
  function resolveConfig(options = {}) {
977
1331
  const env = options.env ?? process.env;
978
1332
  const fileConfig = loadRawConfig(options.configPath);
979
- const envConfig = buildEnvOverrides(env);
1333
+ const nonCredentialEnvConfig = buildNonCredentialEnvOverrides(env);
1334
+ const contextConfig = mergeDefined(
1335
+ mergeDefined(
1336
+ mergeDefined(defaultConfig, fileConfig),
1337
+ nonCredentialEnvConfig
1338
+ ),
1339
+ stripApiKey(options.cliOverrides) ?? {}
1340
+ );
1341
+ const credentialEnvConfig = buildCredentialEnvOverrides(env, {
1342
+ provider: contextConfig.provider.provider,
1343
+ baseUrl: contextConfig.provider.baseUrl
1344
+ });
980
1345
  const merged = mergeDefined(
981
- mergeDefined(mergeDefined(defaultConfig, fileConfig), envConfig),
1346
+ mergeDefined(
1347
+ mergeDefined(
1348
+ mergeDefined(defaultConfig, fileConfig),
1349
+ nonCredentialEnvConfig
1350
+ ),
1351
+ credentialEnvConfig
1352
+ ),
982
1353
  options.cliOverrides ?? {}
983
1354
  );
984
1355
  return siftConfigSchema.parse(merged);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bilalimamoglu/sift",
3
- "version": "0.1.0",
3
+ "version": "0.2.1",
4
4
  "description": "Agent-first command-output reduction layer for agents, CI, and automation.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,6 +37,14 @@
37
37
  ],
38
38
  "author": "Bilal Imamoglu",
39
39
  "license": "MIT",
40
+ "repository": {
41
+ "type": "git",
42
+ "url": "git+https://github.com/bilalimamoglu/sift.git"
43
+ },
44
+ "homepage": "https://github.com/bilalimamoglu/sift#readme",
45
+ "bugs": {
46
+ "url": "https://github.com/bilalimamoglu/sift/issues"
47
+ },
40
48
  "dependencies": {
41
49
  "cac": "^6.7.14",
42
50
  "picocolors": "^1.1.1",