@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/cli.js
CHANGED
|
@@ -51,22 +51,23 @@ function loadRawConfig(explicitPath) {
|
|
|
51
51
|
// src/config/defaults.ts
|
|
52
52
|
var defaultConfig = {
|
|
53
53
|
provider: {
|
|
54
|
-
provider: "openai
|
|
55
|
-
model: "gpt-
|
|
54
|
+
provider: "openai",
|
|
55
|
+
model: "gpt-5-nano",
|
|
56
56
|
baseUrl: "https://api.openai.com/v1",
|
|
57
57
|
apiKey: "",
|
|
58
|
+
jsonResponseFormat: "auto",
|
|
58
59
|
timeoutMs: 2e4,
|
|
59
60
|
temperature: 0.1,
|
|
60
|
-
maxOutputTokens:
|
|
61
|
+
maxOutputTokens: 400
|
|
61
62
|
},
|
|
62
63
|
input: {
|
|
63
64
|
stripAnsi: true,
|
|
64
65
|
redact: false,
|
|
65
66
|
redactStrict: false,
|
|
66
|
-
maxCaptureChars:
|
|
67
|
-
maxInputChars:
|
|
68
|
-
headChars:
|
|
69
|
-
tailChars:
|
|
67
|
+
maxCaptureChars: 4e5,
|
|
68
|
+
maxInputChars: 6e4,
|
|
69
|
+
headChars: 2e4,
|
|
70
|
+
tailChars: 2e4
|
|
70
71
|
},
|
|
71
72
|
runtime: {
|
|
72
73
|
rawFallback: true,
|
|
@@ -100,6 +101,16 @@ var defaultConfig = {
|
|
|
100
101
|
format: "bullets",
|
|
101
102
|
policy: "log-errors"
|
|
102
103
|
},
|
|
104
|
+
"typecheck-summary": {
|
|
105
|
+
question: "Summarize the blocking typecheck failures. Group repeated errors by root cause and point to the first files or symbols to fix.",
|
|
106
|
+
format: "bullets",
|
|
107
|
+
policy: "typecheck-summary"
|
|
108
|
+
},
|
|
109
|
+
"lint-failures": {
|
|
110
|
+
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.",
|
|
111
|
+
format: "bullets",
|
|
112
|
+
policy: "lint-failures"
|
|
113
|
+
},
|
|
103
114
|
"infra-risk": {
|
|
104
115
|
question: "Assess whether the infrastructure changes are risky and whether they look safe to apply.",
|
|
105
116
|
format: "verdict",
|
|
@@ -108,9 +119,76 @@ var defaultConfig = {
|
|
|
108
119
|
}
|
|
109
120
|
};
|
|
110
121
|
|
|
122
|
+
// src/config/provider-api-key.ts
|
|
123
|
+
var OPENAI_COMPATIBLE_BASE_URL_ENV = [
|
|
124
|
+
{ prefix: "https://api.openai.com/", envName: "OPENAI_API_KEY" },
|
|
125
|
+
{ prefix: "https://openrouter.ai/api/", envName: "OPENROUTER_API_KEY" },
|
|
126
|
+
{ prefix: "https://api.together.xyz/", envName: "TOGETHER_API_KEY" },
|
|
127
|
+
{ prefix: "https://api.groq.com/openai/", envName: "GROQ_API_KEY" }
|
|
128
|
+
];
|
|
129
|
+
var PROVIDER_API_KEY_ENV = {
|
|
130
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
131
|
+
claude: "ANTHROPIC_API_KEY",
|
|
132
|
+
groq: "GROQ_API_KEY",
|
|
133
|
+
openai: "OPENAI_API_KEY",
|
|
134
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
135
|
+
together: "TOGETHER_API_KEY"
|
|
136
|
+
};
|
|
137
|
+
function normalizeBaseUrl(baseUrl) {
|
|
138
|
+
if (!baseUrl) {
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
return `${baseUrl.replace(/\/+$/, "")}/`.toLowerCase();
|
|
142
|
+
}
|
|
143
|
+
function resolveCompatibleEnvName(baseUrl) {
|
|
144
|
+
const normalized = normalizeBaseUrl(baseUrl);
|
|
145
|
+
if (!normalized) {
|
|
146
|
+
return void 0;
|
|
147
|
+
}
|
|
148
|
+
const match = OPENAI_COMPATIBLE_BASE_URL_ENV.find(
|
|
149
|
+
(entry) => normalized.startsWith(entry.prefix)
|
|
150
|
+
);
|
|
151
|
+
return match?.envName;
|
|
152
|
+
}
|
|
153
|
+
function resolveProviderApiKey(provider, baseUrl, env) {
|
|
154
|
+
if (provider === "openai-compatible") {
|
|
155
|
+
if (env.SIFT_PROVIDER_API_KEY) {
|
|
156
|
+
return env.SIFT_PROVIDER_API_KEY;
|
|
157
|
+
}
|
|
158
|
+
const envName2 = resolveCompatibleEnvName(baseUrl);
|
|
159
|
+
return envName2 ? env[envName2] : void 0;
|
|
160
|
+
}
|
|
161
|
+
if (!provider) {
|
|
162
|
+
return env.SIFT_PROVIDER_API_KEY;
|
|
163
|
+
}
|
|
164
|
+
const envName = PROVIDER_API_KEY_ENV[provider];
|
|
165
|
+
if (envName && env[envName]) {
|
|
166
|
+
return env[envName];
|
|
167
|
+
}
|
|
168
|
+
return env.SIFT_PROVIDER_API_KEY;
|
|
169
|
+
}
|
|
170
|
+
function getProviderApiKeyEnvNames(provider, baseUrl) {
|
|
171
|
+
const envNames = ["SIFT_PROVIDER_API_KEY"];
|
|
172
|
+
if (provider === "openai-compatible") {
|
|
173
|
+
const envName2 = resolveCompatibleEnvName(baseUrl);
|
|
174
|
+
if (envName2) {
|
|
175
|
+
envNames.push(envName2);
|
|
176
|
+
}
|
|
177
|
+
return envNames;
|
|
178
|
+
}
|
|
179
|
+
if (!provider) {
|
|
180
|
+
return envNames;
|
|
181
|
+
}
|
|
182
|
+
const envName = PROVIDER_API_KEY_ENV[provider];
|
|
183
|
+
if (envName) {
|
|
184
|
+
return [envName, ...envNames];
|
|
185
|
+
}
|
|
186
|
+
return envNames;
|
|
187
|
+
}
|
|
188
|
+
|
|
111
189
|
// src/config/schema.ts
|
|
112
190
|
import { z } from "zod";
|
|
113
|
-
var providerNameSchema = z.enum(["openai-compatible"]);
|
|
191
|
+
var providerNameSchema = z.enum(["openai", "openai-compatible"]);
|
|
114
192
|
var outputFormatSchema = z.enum([
|
|
115
193
|
"brief",
|
|
116
194
|
"bullets",
|
|
@@ -118,19 +196,23 @@ var outputFormatSchema = z.enum([
|
|
|
118
196
|
"verdict"
|
|
119
197
|
]);
|
|
120
198
|
var responseModeSchema = z.enum(["text", "json"]);
|
|
199
|
+
var jsonResponseFormatModeSchema = z.enum(["auto", "on", "off"]);
|
|
121
200
|
var promptPolicyNameSchema = z.enum([
|
|
122
201
|
"test-status",
|
|
123
202
|
"audit-critical",
|
|
124
203
|
"diff-summary",
|
|
125
204
|
"build-failure",
|
|
126
205
|
"log-errors",
|
|
127
|
-
"infra-risk"
|
|
206
|
+
"infra-risk",
|
|
207
|
+
"typecheck-summary",
|
|
208
|
+
"lint-failures"
|
|
128
209
|
]);
|
|
129
210
|
var providerConfigSchema = z.object({
|
|
130
211
|
provider: providerNameSchema,
|
|
131
212
|
model: z.string().min(1),
|
|
132
213
|
baseUrl: z.string().url(),
|
|
133
214
|
apiKey: z.string().optional(),
|
|
215
|
+
jsonResponseFormat: jsonResponseFormatModeSchema,
|
|
134
216
|
timeoutMs: z.number().int().positive(),
|
|
135
217
|
temperature: z.number().min(0).max(2),
|
|
136
218
|
maxOutputTokens: z.number().int().positive()
|
|
@@ -184,14 +266,25 @@ function mergeDefined(base, override) {
|
|
|
184
266
|
}
|
|
185
267
|
return result;
|
|
186
268
|
}
|
|
187
|
-
function
|
|
269
|
+
function stripApiKey(overrides) {
|
|
270
|
+
if (!overrides?.provider || overrides.provider.apiKey === void 0) {
|
|
271
|
+
return overrides;
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
...overrides,
|
|
275
|
+
provider: {
|
|
276
|
+
...overrides.provider,
|
|
277
|
+
apiKey: void 0
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
function buildNonCredentialEnvOverrides(env) {
|
|
188
282
|
const overrides = {};
|
|
189
|
-
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.
|
|
283
|
+
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS) {
|
|
190
284
|
overrides.provider = {
|
|
191
285
|
provider: env.SIFT_PROVIDER,
|
|
192
286
|
model: env.SIFT_MODEL,
|
|
193
287
|
baseUrl: env.SIFT_BASE_URL,
|
|
194
|
-
apiKey: env.SIFT_API_KEY,
|
|
195
288
|
timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
|
|
196
289
|
};
|
|
197
290
|
}
|
|
@@ -203,12 +296,40 @@ function buildEnvOverrides(env) {
|
|
|
203
296
|
}
|
|
204
297
|
return overrides;
|
|
205
298
|
}
|
|
299
|
+
function buildCredentialEnvOverrides(env, context) {
|
|
300
|
+
const apiKey = resolveProviderApiKey(context.provider, context.baseUrl, env);
|
|
301
|
+
if (apiKey === void 0) {
|
|
302
|
+
return {};
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
provider: {
|
|
306
|
+
apiKey
|
|
307
|
+
}
|
|
308
|
+
};
|
|
309
|
+
}
|
|
206
310
|
function resolveConfig(options = {}) {
|
|
207
311
|
const env = options.env ?? process.env;
|
|
208
312
|
const fileConfig = loadRawConfig(options.configPath);
|
|
209
|
-
const
|
|
313
|
+
const nonCredentialEnvConfig = buildNonCredentialEnvOverrides(env);
|
|
314
|
+
const contextConfig = mergeDefined(
|
|
315
|
+
mergeDefined(
|
|
316
|
+
mergeDefined(defaultConfig, fileConfig),
|
|
317
|
+
nonCredentialEnvConfig
|
|
318
|
+
),
|
|
319
|
+
stripApiKey(options.cliOverrides) ?? {}
|
|
320
|
+
);
|
|
321
|
+
const credentialEnvConfig = buildCredentialEnvOverrides(env, {
|
|
322
|
+
provider: contextConfig.provider.provider,
|
|
323
|
+
baseUrl: contextConfig.provider.baseUrl
|
|
324
|
+
});
|
|
210
325
|
const merged = mergeDefined(
|
|
211
|
-
mergeDefined(
|
|
326
|
+
mergeDefined(
|
|
327
|
+
mergeDefined(
|
|
328
|
+
mergeDefined(defaultConfig, fileConfig),
|
|
329
|
+
nonCredentialEnvConfig
|
|
330
|
+
),
|
|
331
|
+
credentialEnvConfig
|
|
332
|
+
),
|
|
212
333
|
options.cliOverrides ?? {}
|
|
213
334
|
);
|
|
214
335
|
return siftConfigSchema.parse(merged);
|
|
@@ -269,7 +390,7 @@ function configValidate(configPath) {
|
|
|
269
390
|
});
|
|
270
391
|
const resolvedPath = findConfigPath(configPath);
|
|
271
392
|
process.stdout.write(
|
|
272
|
-
`
|
|
393
|
+
`Resolved config is valid${resolvedPath ? ` (${resolvedPath})` : " (using defaults)"}.
|
|
273
394
|
`
|
|
274
395
|
);
|
|
275
396
|
}
|
|
@@ -278,6 +399,7 @@ function configValidate(configPath) {
|
|
|
278
399
|
function runDoctor(config) {
|
|
279
400
|
const lines = [
|
|
280
401
|
"sift doctor",
|
|
402
|
+
"mode: local config completeness check",
|
|
281
403
|
`provider: ${config.provider.provider}`,
|
|
282
404
|
`model: ${config.provider.model}`,
|
|
283
405
|
`baseUrl: ${config.provider.baseUrl}`,
|
|
@@ -295,8 +417,14 @@ function runDoctor(config) {
|
|
|
295
417
|
if (!config.provider.model) {
|
|
296
418
|
problems.push("Missing provider.model");
|
|
297
419
|
}
|
|
298
|
-
if (config.provider.provider === "openai-compatible" && !config.provider.apiKey) {
|
|
420
|
+
if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible") && !config.provider.apiKey) {
|
|
299
421
|
problems.push("Missing provider.apiKey");
|
|
422
|
+
problems.push(
|
|
423
|
+
`Set one of: ${getProviderApiKeyEnvNames(
|
|
424
|
+
config.provider.provider,
|
|
425
|
+
config.provider.baseUrl
|
|
426
|
+
).join(", ")}`
|
|
427
|
+
);
|
|
300
428
|
}
|
|
301
429
|
if (problems.length > 0) {
|
|
302
430
|
process.stderr.write(`${problems.join("\n")}
|
|
@@ -331,10 +459,160 @@ import { spawn } from "child_process";
|
|
|
331
459
|
import { constants as osConstants } from "os";
|
|
332
460
|
import pc2 from "picocolors";
|
|
333
461
|
|
|
462
|
+
// src/core/gate.ts
|
|
463
|
+
var FAIL_ON_SUPPORTED_PRESETS = /* @__PURE__ */ new Set(["infra-risk", "audit-critical"]);
|
|
464
|
+
function parseJson(output) {
|
|
465
|
+
try {
|
|
466
|
+
return JSON.parse(output);
|
|
467
|
+
} catch {
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
function supportsFailOnPreset(presetName) {
|
|
472
|
+
return typeof presetName === "string" && FAIL_ON_SUPPORTED_PRESETS.has(presetName);
|
|
473
|
+
}
|
|
474
|
+
function assertSupportedFailOnPreset(presetName) {
|
|
475
|
+
if (!supportsFailOnPreset(presetName)) {
|
|
476
|
+
throw new Error(
|
|
477
|
+
"--fail-on is supported only for built-in presets: infra-risk, audit-critical."
|
|
478
|
+
);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
function assertSupportedFailOnFormat(args) {
|
|
482
|
+
const expectedFormat = args.presetName === "infra-risk" ? "verdict" : "json";
|
|
483
|
+
if (args.format !== expectedFormat) {
|
|
484
|
+
throw new Error(
|
|
485
|
+
`--fail-on requires the default ${expectedFormat} format for preset ${args.presetName}.`
|
|
486
|
+
);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
function evaluateGate(args) {
|
|
490
|
+
const parsed = parseJson(args.output);
|
|
491
|
+
if (!parsed || typeof parsed !== "object") {
|
|
492
|
+
return { shouldFail: false };
|
|
493
|
+
}
|
|
494
|
+
if (args.presetName === "infra-risk") {
|
|
495
|
+
return {
|
|
496
|
+
shouldFail: parsed["verdict"] === "fail"
|
|
497
|
+
};
|
|
498
|
+
}
|
|
499
|
+
if (args.presetName === "audit-critical") {
|
|
500
|
+
const status = parsed["status"];
|
|
501
|
+
const vulnerabilities = parsed["vulnerabilities"];
|
|
502
|
+
return {
|
|
503
|
+
shouldFail: status === "ok" && Array.isArray(vulnerabilities) && vulnerabilities.length > 0
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
return { shouldFail: false };
|
|
507
|
+
}
|
|
508
|
+
|
|
334
509
|
// src/core/run.ts
|
|
335
510
|
import pc from "picocolors";
|
|
336
511
|
|
|
512
|
+
// src/providers/systemInstruction.ts
|
|
513
|
+
var REDUCTION_SYSTEM_INSTRUCTION = "You reduce noisy command output into compact answers for agents and automation.";
|
|
514
|
+
|
|
515
|
+
// src/providers/openai.ts
|
|
516
|
+
function usesNativeJsonResponseFormat(mode) {
|
|
517
|
+
return mode !== "off";
|
|
518
|
+
}
|
|
519
|
+
function extractResponseText(payload) {
|
|
520
|
+
if (typeof payload?.output_text === "string") {
|
|
521
|
+
return payload.output_text.trim();
|
|
522
|
+
}
|
|
523
|
+
if (!Array.isArray(payload?.output)) {
|
|
524
|
+
return "";
|
|
525
|
+
}
|
|
526
|
+
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();
|
|
527
|
+
}
|
|
528
|
+
async function buildOpenAIError(response) {
|
|
529
|
+
let detail = `Provider returned HTTP ${response.status}`;
|
|
530
|
+
try {
|
|
531
|
+
const data = await response.json();
|
|
532
|
+
const message = data?.error?.message;
|
|
533
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
534
|
+
detail = `${detail}: ${message.trim()}`;
|
|
535
|
+
}
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
return new Error(detail);
|
|
539
|
+
}
|
|
540
|
+
var OpenAIProvider = class {
|
|
541
|
+
name = "openai";
|
|
542
|
+
baseUrl;
|
|
543
|
+
apiKey;
|
|
544
|
+
constructor(options) {
|
|
545
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
546
|
+
this.apiKey = options.apiKey;
|
|
547
|
+
}
|
|
548
|
+
async generate(input) {
|
|
549
|
+
const controller = new AbortController();
|
|
550
|
+
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
551
|
+
try {
|
|
552
|
+
const url = new URL("responses", `${this.baseUrl}/`);
|
|
553
|
+
const response = await fetch(url, {
|
|
554
|
+
method: "POST",
|
|
555
|
+
signal: controller.signal,
|
|
556
|
+
headers: {
|
|
557
|
+
"content-type": "application/json",
|
|
558
|
+
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
|
|
559
|
+
},
|
|
560
|
+
body: JSON.stringify({
|
|
561
|
+
model: input.model,
|
|
562
|
+
instructions: REDUCTION_SYSTEM_INSTRUCTION,
|
|
563
|
+
input: input.prompt,
|
|
564
|
+
reasoning: {
|
|
565
|
+
effort: "minimal"
|
|
566
|
+
},
|
|
567
|
+
text: {
|
|
568
|
+
verbosity: "low",
|
|
569
|
+
...input.responseMode === "json" && usesNativeJsonResponseFormat(input.jsonResponseFormat) ? {
|
|
570
|
+
format: {
|
|
571
|
+
type: "json_object"
|
|
572
|
+
}
|
|
573
|
+
} : {}
|
|
574
|
+
},
|
|
575
|
+
max_output_tokens: input.maxOutputTokens
|
|
576
|
+
})
|
|
577
|
+
});
|
|
578
|
+
if (!response.ok) {
|
|
579
|
+
throw await buildOpenAIError(response);
|
|
580
|
+
}
|
|
581
|
+
const data = await response.json();
|
|
582
|
+
const text = extractResponseText(data);
|
|
583
|
+
if (!text) {
|
|
584
|
+
throw new Error("Provider returned an empty response");
|
|
585
|
+
}
|
|
586
|
+
return {
|
|
587
|
+
text,
|
|
588
|
+
usage: data?.usage ? {
|
|
589
|
+
inputTokens: data.usage.input_tokens,
|
|
590
|
+
outputTokens: data.usage.output_tokens,
|
|
591
|
+
totalTokens: data.usage.total_tokens
|
|
592
|
+
} : void 0,
|
|
593
|
+
raw: data
|
|
594
|
+
};
|
|
595
|
+
} catch (error) {
|
|
596
|
+
if (error.name === "AbortError") {
|
|
597
|
+
throw new Error("Provider request timed out");
|
|
598
|
+
}
|
|
599
|
+
throw error;
|
|
600
|
+
} finally {
|
|
601
|
+
clearTimeout(timeout);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
};
|
|
605
|
+
|
|
337
606
|
// src/providers/openaiCompatible.ts
|
|
607
|
+
function supportsNativeJsonResponseFormat(baseUrl, mode) {
|
|
608
|
+
if (mode === "off") {
|
|
609
|
+
return false;
|
|
610
|
+
}
|
|
611
|
+
if (mode === "on") {
|
|
612
|
+
return true;
|
|
613
|
+
}
|
|
614
|
+
return /^https:\/\/api\.openai\.com(?:\/|$)/i.test(baseUrl);
|
|
615
|
+
}
|
|
338
616
|
function extractMessageText(payload) {
|
|
339
617
|
const content = payload?.choices?.[0]?.message?.content;
|
|
340
618
|
if (typeof content === "string") {
|
|
@@ -345,6 +623,18 @@ function extractMessageText(payload) {
|
|
|
345
623
|
}
|
|
346
624
|
return "";
|
|
347
625
|
}
|
|
626
|
+
async function buildOpenAICompatibleError(response) {
|
|
627
|
+
let detail = `Provider returned HTTP ${response.status}`;
|
|
628
|
+
try {
|
|
629
|
+
const data = await response.json();
|
|
630
|
+
const message = data?.error?.message;
|
|
631
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
632
|
+
detail = `${detail}: ${message.trim()}`;
|
|
633
|
+
}
|
|
634
|
+
} catch {
|
|
635
|
+
}
|
|
636
|
+
return new Error(detail);
|
|
637
|
+
}
|
|
348
638
|
var OpenAICompatibleProvider = class {
|
|
349
639
|
name = "openai-compatible";
|
|
350
640
|
baseUrl;
|
|
@@ -369,10 +659,11 @@ var OpenAICompatibleProvider = class {
|
|
|
369
659
|
model: input.model,
|
|
370
660
|
temperature: input.temperature,
|
|
371
661
|
max_tokens: input.maxOutputTokens,
|
|
662
|
+
...input.responseMode === "json" && supportsNativeJsonResponseFormat(this.baseUrl, input.jsonResponseFormat) ? { response_format: { type: "json_object" } } : {},
|
|
372
663
|
messages: [
|
|
373
664
|
{
|
|
374
665
|
role: "system",
|
|
375
|
-
content:
|
|
666
|
+
content: REDUCTION_SYSTEM_INSTRUCTION
|
|
376
667
|
},
|
|
377
668
|
{
|
|
378
669
|
role: "user",
|
|
@@ -382,7 +673,7 @@ var OpenAICompatibleProvider = class {
|
|
|
382
673
|
})
|
|
383
674
|
});
|
|
384
675
|
if (!response.ok) {
|
|
385
|
-
throw
|
|
676
|
+
throw await buildOpenAICompatibleError(response);
|
|
386
677
|
}
|
|
387
678
|
const data = await response.json();
|
|
388
679
|
const text = extractMessageText(data);
|
|
@@ -411,6 +702,12 @@ var OpenAICompatibleProvider = class {
|
|
|
411
702
|
|
|
412
703
|
// src/providers/factory.ts
|
|
413
704
|
function createProvider(config) {
|
|
705
|
+
if (config.provider.provider === "openai") {
|
|
706
|
+
return new OpenAIProvider({
|
|
707
|
+
baseUrl: config.provider.baseUrl,
|
|
708
|
+
apiKey: config.provider.apiKey
|
|
709
|
+
});
|
|
710
|
+
}
|
|
414
711
|
if (config.provider.provider === "openai-compatible") {
|
|
415
712
|
return new OpenAICompatibleProvider({
|
|
416
713
|
baseUrl: config.provider.baseUrl,
|
|
@@ -536,6 +833,33 @@ var BUILT_IN_POLICIES = {
|
|
|
536
833
|
`If there is no clear error signal, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
537
834
|
]
|
|
538
835
|
},
|
|
836
|
+
"typecheck-summary": {
|
|
837
|
+
name: "typecheck-summary",
|
|
838
|
+
responseMode: "text",
|
|
839
|
+
taskRules: [
|
|
840
|
+
"Return at most 5 short bullet points.",
|
|
841
|
+
"Determine whether the typecheck failed or passed.",
|
|
842
|
+
"Group repeated diagnostics into root-cause buckets instead of echoing many duplicate lines.",
|
|
843
|
+
"Mention the first concrete files, symbols, or error categories to fix when they are visible.",
|
|
844
|
+
"Prefer compiler or type-system errors over timing, progress, or summary noise.",
|
|
845
|
+
"If the output clearly indicates success, say that briefly and do not add extra bullets.",
|
|
846
|
+
`If you cannot tell whether the typecheck failed, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
847
|
+
]
|
|
848
|
+
},
|
|
849
|
+
"lint-failures": {
|
|
850
|
+
name: "lint-failures",
|
|
851
|
+
responseMode: "text",
|
|
852
|
+
taskRules: [
|
|
853
|
+
"Return at most 5 short bullet points.",
|
|
854
|
+
"Determine whether lint failed or whether there are no blocking lint failures.",
|
|
855
|
+
"Group repeated rule violations instead of listing the same rule many times.",
|
|
856
|
+
"Mention the top offending files and rule names when they are visible.",
|
|
857
|
+
"Distinguish blocking failures from warnings only when that distinction is clearly visible in the input.",
|
|
858
|
+
"Do not invent autofixability; only mention autofix or --fix support when the tool output explicitly says so.",
|
|
859
|
+
"If the output clearly indicates success or no blocking failures, say that briefly and stop.",
|
|
860
|
+
`If there is not enough evidence to determine the lint result, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
861
|
+
]
|
|
862
|
+
},
|
|
539
863
|
"infra-risk": {
|
|
540
864
|
name: "infra-risk",
|
|
541
865
|
responseMode: "json",
|
|
@@ -879,6 +1203,7 @@ function prepareInput(raw, config) {
|
|
|
879
1203
|
}
|
|
880
1204
|
|
|
881
1205
|
// src/core/run.ts
|
|
1206
|
+
var RETRY_DELAY_MS = 300;
|
|
882
1207
|
function normalizeOutput(text, responseMode) {
|
|
883
1208
|
if (responseMode !== "json") {
|
|
884
1209
|
return text.trim();
|
|
@@ -890,6 +1215,68 @@ function normalizeOutput(text, responseMode) {
|
|
|
890
1215
|
throw new Error("Provider returned invalid JSON");
|
|
891
1216
|
}
|
|
892
1217
|
}
|
|
1218
|
+
function buildDryRunOutput(args) {
|
|
1219
|
+
return JSON.stringify(
|
|
1220
|
+
{
|
|
1221
|
+
status: "dry-run",
|
|
1222
|
+
strategy: args.heuristicOutput ? "heuristic" : "provider",
|
|
1223
|
+
provider: {
|
|
1224
|
+
name: args.providerName,
|
|
1225
|
+
model: args.request.config.provider.model,
|
|
1226
|
+
baseUrl: args.request.config.provider.baseUrl,
|
|
1227
|
+
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
1228
|
+
},
|
|
1229
|
+
question: args.request.question,
|
|
1230
|
+
format: args.request.format,
|
|
1231
|
+
responseMode: args.responseMode,
|
|
1232
|
+
policy: args.request.policyName ?? null,
|
|
1233
|
+
heuristicOutput: args.heuristicOutput ?? null,
|
|
1234
|
+
input: {
|
|
1235
|
+
originalLength: args.prepared.meta.originalLength,
|
|
1236
|
+
finalLength: args.prepared.meta.finalLength,
|
|
1237
|
+
redactionApplied: args.prepared.meta.redactionApplied,
|
|
1238
|
+
truncatedApplied: args.prepared.meta.truncatedApplied,
|
|
1239
|
+
text: args.prepared.truncated
|
|
1240
|
+
},
|
|
1241
|
+
prompt: args.prompt
|
|
1242
|
+
},
|
|
1243
|
+
null,
|
|
1244
|
+
2
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
async function delay(ms) {
|
|
1248
|
+
await new Promise((resolve) => setTimeout(resolve, ms));
|
|
1249
|
+
}
|
|
1250
|
+
async function generateWithRetry(args) {
|
|
1251
|
+
let lastError;
|
|
1252
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1253
|
+
try {
|
|
1254
|
+
return await args.provider.generate({
|
|
1255
|
+
model: args.request.config.provider.model,
|
|
1256
|
+
prompt: args.prompt,
|
|
1257
|
+
temperature: args.request.config.provider.temperature,
|
|
1258
|
+
maxOutputTokens: args.request.config.provider.maxOutputTokens,
|
|
1259
|
+
timeoutMs: args.request.config.provider.timeoutMs,
|
|
1260
|
+
responseMode: args.responseMode,
|
|
1261
|
+
jsonResponseFormat: args.request.config.provider.jsonResponseFormat
|
|
1262
|
+
});
|
|
1263
|
+
} catch (error) {
|
|
1264
|
+
lastError = error;
|
|
1265
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
1266
|
+
if (attempt > 0 || !isRetriableReason(reason)) {
|
|
1267
|
+
throw error;
|
|
1268
|
+
}
|
|
1269
|
+
if (args.request.config.runtime.verbose) {
|
|
1270
|
+
process.stderr.write(
|
|
1271
|
+
`${pc.dim("sift")} retry=1 reason=${reason} delay_ms=${RETRY_DELAY_MS}
|
|
1272
|
+
`
|
|
1273
|
+
);
|
|
1274
|
+
}
|
|
1275
|
+
await delay(RETRY_DELAY_MS);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
throw lastError instanceof Error ? lastError : new Error("unknown_error");
|
|
1279
|
+
}
|
|
893
1280
|
async function runSift(request) {
|
|
894
1281
|
const prepared = prepareInput(request.stdin, request.config.input);
|
|
895
1282
|
const { prompt, responseMode } = buildPrompt({
|
|
@@ -915,15 +1302,33 @@ async function runSift(request) {
|
|
|
915
1302
|
process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
|
|
916
1303
|
`);
|
|
917
1304
|
}
|
|
1305
|
+
if (request.dryRun) {
|
|
1306
|
+
return buildDryRunOutput({
|
|
1307
|
+
request,
|
|
1308
|
+
providerName: provider.name,
|
|
1309
|
+
prompt,
|
|
1310
|
+
responseMode,
|
|
1311
|
+
prepared,
|
|
1312
|
+
heuristicOutput
|
|
1313
|
+
});
|
|
1314
|
+
}
|
|
918
1315
|
return heuristicOutput;
|
|
919
1316
|
}
|
|
1317
|
+
if (request.dryRun) {
|
|
1318
|
+
return buildDryRunOutput({
|
|
1319
|
+
request,
|
|
1320
|
+
providerName: provider.name,
|
|
1321
|
+
prompt,
|
|
1322
|
+
responseMode,
|
|
1323
|
+
prepared,
|
|
1324
|
+
heuristicOutput: null
|
|
1325
|
+
});
|
|
1326
|
+
}
|
|
920
1327
|
try {
|
|
921
|
-
const result = await
|
|
922
|
-
|
|
1328
|
+
const result = await generateWithRetry({
|
|
1329
|
+
provider,
|
|
1330
|
+
request,
|
|
923
1331
|
prompt,
|
|
924
|
-
temperature: request.config.provider.temperature,
|
|
925
|
-
maxOutputTokens: request.config.provider.maxOutputTokens,
|
|
926
|
-
timeoutMs: request.config.provider.timeoutMs,
|
|
927
1332
|
responseMode
|
|
928
1333
|
});
|
|
929
1334
|
if (looksLikeRejectedModelOutput({
|
|
@@ -1101,6 +1506,12 @@ async function runExec(request) {
|
|
|
1101
1506
|
});
|
|
1102
1507
|
process.stdout.write(`${output}
|
|
1103
1508
|
`);
|
|
1509
|
+
if (request.failOn && !request.dryRun && exitCode === 0 && supportsFailOnPreset(request.presetName) && evaluateGate({
|
|
1510
|
+
presetName: request.presetName,
|
|
1511
|
+
output
|
|
1512
|
+
}).shouldFail) {
|
|
1513
|
+
return 1;
|
|
1514
|
+
}
|
|
1104
1515
|
}
|
|
1105
1516
|
return exitCode;
|
|
1106
1517
|
}
|
|
@@ -1138,12 +1549,13 @@ function toNumber(value) {
|
|
|
1138
1549
|
}
|
|
1139
1550
|
function buildCliOverrides(options) {
|
|
1140
1551
|
const overrides = {};
|
|
1141
|
-
if (options.provider !== void 0 || options.model !== void 0 || options.baseUrl !== void 0 || options.apiKey !== void 0 || options.timeoutMs !== void 0) {
|
|
1552
|
+
if (options.provider !== void 0 || options.model !== void 0 || options.baseUrl !== void 0 || options.apiKey !== void 0 || options.jsonResponseFormat !== void 0 || options.timeoutMs !== void 0) {
|
|
1142
1553
|
overrides.provider = {
|
|
1143
1554
|
provider: options.provider,
|
|
1144
1555
|
model: options.model,
|
|
1145
1556
|
baseUrl: options.baseUrl,
|
|
1146
1557
|
apiKey: options.apiKey,
|
|
1558
|
+
jsonResponseFormat: options.jsonResponseFormat,
|
|
1147
1559
|
timeoutMs: toNumber(options.timeoutMs)
|
|
1148
1560
|
};
|
|
1149
1561
|
}
|
|
@@ -1167,9 +1579,25 @@ function buildCliOverrides(options) {
|
|
|
1167
1579
|
return overrides;
|
|
1168
1580
|
}
|
|
1169
1581
|
function applySharedOptions(command) {
|
|
1170
|
-
return command.option("--provider <provider>", "Provider: openai-compatible").option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
|
|
1582
|
+
return command.option("--provider <provider>", "Provider: openai | openai-compatible").option("--model <model>", "Model name").option("--base-url <url>", "Provider base URL").option(
|
|
1583
|
+
"--api-key <key>",
|
|
1584
|
+
"Provider API key (or set OPENAI_API_KEY for provider=openai; use SIFT_PROVIDER_API_KEY or endpoint-native envs for openai-compatible)"
|
|
1585
|
+
).option(
|
|
1586
|
+
"--json-response-format <mode>",
|
|
1587
|
+
"JSON response format mode: auto | on | off"
|
|
1588
|
+
).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(
|
|
1589
|
+
"--fail-on",
|
|
1590
|
+
"Fail with exit code 1 when a supported built-in preset produces a blocking result"
|
|
1591
|
+
).option("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
|
|
1171
1592
|
}
|
|
1172
1593
|
async function executeRun(args) {
|
|
1594
|
+
if (Boolean(args.options.failOn)) {
|
|
1595
|
+
assertSupportedFailOnPreset(args.presetName);
|
|
1596
|
+
assertSupportedFailOnFormat({
|
|
1597
|
+
presetName: args.presetName,
|
|
1598
|
+
format: args.format
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1173
1601
|
const config = resolveConfig({
|
|
1174
1602
|
configPath: args.options.config,
|
|
1175
1603
|
env: process.env,
|
|
@@ -1181,12 +1609,20 @@ async function executeRun(args) {
|
|
|
1181
1609
|
format: args.format,
|
|
1182
1610
|
stdin,
|
|
1183
1611
|
config,
|
|
1612
|
+
dryRun: Boolean(args.options.dryRun),
|
|
1613
|
+
presetName: args.presetName,
|
|
1184
1614
|
policyName: args.policyName,
|
|
1185
1615
|
outputContract: args.outputContract,
|
|
1186
1616
|
fallbackJson: args.fallbackJson
|
|
1187
1617
|
});
|
|
1188
1618
|
process.stdout.write(`${output}
|
|
1189
1619
|
`);
|
|
1620
|
+
if (Boolean(args.options.failOn) && !Boolean(args.options.dryRun) && args.presetName && evaluateGate({
|
|
1621
|
+
presetName: args.presetName,
|
|
1622
|
+
output
|
|
1623
|
+
}).shouldFail) {
|
|
1624
|
+
process.exitCode = 1;
|
|
1625
|
+
}
|
|
1190
1626
|
}
|
|
1191
1627
|
function extractExecCommand(options) {
|
|
1192
1628
|
const passthrough = Array.isArray(options["--"]) ? options["--"].map((value) => String(value)) : [];
|
|
@@ -1203,6 +1639,13 @@ function extractExecCommand(options) {
|
|
|
1203
1639
|
};
|
|
1204
1640
|
}
|
|
1205
1641
|
async function executeExec(args) {
|
|
1642
|
+
if (Boolean(args.options.failOn)) {
|
|
1643
|
+
assertSupportedFailOnPreset(args.presetName);
|
|
1644
|
+
assertSupportedFailOnFormat({
|
|
1645
|
+
presetName: args.presetName,
|
|
1646
|
+
format: args.format
|
|
1647
|
+
});
|
|
1648
|
+
}
|
|
1206
1649
|
const config = resolveConfig({
|
|
1207
1650
|
configPath: args.options.config,
|
|
1208
1651
|
env: process.env,
|
|
@@ -1213,6 +1656,9 @@ async function executeExec(args) {
|
|
|
1213
1656
|
question: args.question,
|
|
1214
1657
|
format: args.format,
|
|
1215
1658
|
config,
|
|
1659
|
+
dryRun: Boolean(args.options.dryRun),
|
|
1660
|
+
failOn: Boolean(args.options.failOn),
|
|
1661
|
+
presetName: args.presetName,
|
|
1216
1662
|
policyName: args.policyName,
|
|
1217
1663
|
outputContract: args.outputContract,
|
|
1218
1664
|
fallbackJson: args.fallbackJson,
|
|
@@ -1221,7 +1667,7 @@ async function executeExec(args) {
|
|
|
1221
1667
|
}
|
|
1222
1668
|
applySharedOptions(
|
|
1223
1669
|
cli.command("preset <name>", "Run a named preset against piped CLI output")
|
|
1224
|
-
).action(async (name, options) => {
|
|
1670
|
+
).usage("preset <name> [options]").example("preset test-status < test-output.txt").action(async (name, options) => {
|
|
1225
1671
|
const config = resolveConfig({
|
|
1226
1672
|
configPath: options.config,
|
|
1227
1673
|
env: process.env,
|
|
@@ -1231,6 +1677,7 @@ applySharedOptions(
|
|
|
1231
1677
|
await executeRun({
|
|
1232
1678
|
question: preset.question,
|
|
1233
1679
|
format: options.format ?? preset.format,
|
|
1680
|
+
presetName: name,
|
|
1234
1681
|
policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
|
|
1235
1682
|
options,
|
|
1236
1683
|
outputContract: preset.outputContract,
|
|
@@ -1239,7 +1686,7 @@ applySharedOptions(
|
|
|
1239
1686
|
});
|
|
1240
1687
|
applySharedOptions(
|
|
1241
1688
|
cli.command("exec [question]", "Run a command and reduce its output").allowUnknownOptions()
|
|
1242
|
-
).option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
|
|
1689
|
+
).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- pytest").example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").action(async (question, options) => {
|
|
1243
1690
|
if (question === "preset") {
|
|
1244
1691
|
throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
|
|
1245
1692
|
}
|
|
@@ -1259,6 +1706,7 @@ applySharedOptions(
|
|
|
1259
1706
|
await executeExec({
|
|
1260
1707
|
question: preset.question,
|
|
1261
1708
|
format: options.format ?? preset.format,
|
|
1709
|
+
presetName,
|
|
1262
1710
|
policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
|
|
1263
1711
|
options,
|
|
1264
1712
|
outputContract: preset.outputContract,
|
|
@@ -1276,7 +1724,10 @@ applySharedOptions(
|
|
|
1276
1724
|
options
|
|
1277
1725
|
});
|
|
1278
1726
|
});
|
|
1279
|
-
cli.command(
|
|
1727
|
+
cli.command(
|
|
1728
|
+
"config <action>",
|
|
1729
|
+
"Config commands: init | show | validate (show/validate use resolved runtime config)"
|
|
1730
|
+
).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) => {
|
|
1280
1731
|
if (action === "init") {
|
|
1281
1732
|
configInit(options.path);
|
|
1282
1733
|
return;
|
|
@@ -1294,14 +1745,14 @@ cli.command("config <action>", "Config commands: init | show | validate").option
|
|
|
1294
1745
|
}
|
|
1295
1746
|
throw new Error(`Unknown config action: ${action}`);
|
|
1296
1747
|
});
|
|
1297
|
-
cli.command("doctor", "
|
|
1748
|
+
cli.command("doctor", "Check local runtime config completeness").usage("doctor [options]").option("--config <path>", "Path to config file").action((options) => {
|
|
1298
1749
|
const config = resolveConfig({
|
|
1299
1750
|
configPath: options.config,
|
|
1300
1751
|
env: process.env
|
|
1301
1752
|
});
|
|
1302
1753
|
process.exitCode = runDoctor(config);
|
|
1303
1754
|
});
|
|
1304
|
-
cli.command("presets <action> [name]", "Preset commands: list | show").option("--config <path>", "Path to config file").option("--internal", "Show internal preset fields in presets show").action((action, name, options) => {
|
|
1755
|
+
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) => {
|
|
1305
1756
|
const config = resolveConfig({
|
|
1306
1757
|
configPath: options.config,
|
|
1307
1758
|
env: process.env
|