@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/README.md +125 -196
- package/dist/cli.js +559 -40
- package/dist/index.d.ts +4 -2
- package/dist/index.js +247 -24
- package/package.json +1 -1
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
|
-
|
|
10
|
-
path.
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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:
|
|
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
|
|
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:
|
|
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
|
|
888
|
-
model: "gpt-
|
|
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:
|
|
1102
|
+
maxOutputTokens: 400
|
|
895
1103
|
},
|
|
896
1104
|
input: {
|
|
897
1105
|
stripAnsi: true,
|
|
898
1106
|
redact: false,
|
|
899
1107
|
redactStrict: false,
|
|
900
|
-
maxCaptureChars:
|
|
901
|
-
maxInputChars:
|
|
902
|
-
headChars:
|
|
903
|
-
tailChars:
|
|
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
|
|
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
|
|
1231
|
+
return env.SIFT_PROVIDER_API_KEY;
|
|
1014
1232
|
}
|
|
1015
1233
|
const envName = PROVIDER_API_KEY_ENV[provider];
|
|
1016
|
-
|
|
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,
|