@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/README.md +93 -145
- package/dist/cli.js +481 -30
- package/dist/index.d.ts +9 -3
- package/dist/index.js +392 -21
- package/package.json +9 -1
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
|
|
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:
|
|
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
|
|
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
|
|
607
|
-
|
|
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
|
|
797
|
-
model: "gpt-
|
|
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:
|
|
1071
|
+
maxOutputTokens: 400
|
|
803
1072
|
},
|
|
804
1073
|
input: {
|
|
805
1074
|
stripAnsi: true,
|
|
806
1075
|
redact: false,
|
|
807
1076
|
redactStrict: false,
|
|
808
|
-
maxCaptureChars:
|
|
809
|
-
maxInputChars:
|
|
810
|
-
headChars:
|
|
811
|
-
tailChars:
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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
|
|
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",
|