@bilalimamoglu/sift 0.1.0

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.js ADDED
@@ -0,0 +1,990 @@
1
+ // src/core/exec.ts
2
+ import { spawn } from "child_process";
3
+ import { constants as osConstants } from "os";
4
+ import pc2 from "picocolors";
5
+
6
+ // src/constants.ts
7
+ import os from "os";
8
+ import path from "path";
9
+ var DEFAULT_CONFIG_SEARCH_PATHS = [
10
+ path.resolve(process.cwd(), "sift.config.yaml"),
11
+ path.resolve(process.cwd(), "sift.config.yml"),
12
+ path.join(os.homedir(), ".config", "sift", "config.yaml"),
13
+ path.join(os.homedir(), ".config", "sift", "config.yml")
14
+ ];
15
+ var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
16
+ var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
17
+ var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
18
+
19
+ // src/core/run.ts
20
+ import pc from "picocolors";
21
+
22
+ // src/providers/openaiCompatible.ts
23
+ function extractMessageText(payload) {
24
+ const content = payload?.choices?.[0]?.message?.content;
25
+ if (typeof content === "string") {
26
+ return content;
27
+ }
28
+ if (Array.isArray(content)) {
29
+ return content.map((item) => typeof item?.text === "string" ? item.text : "").join("").trim();
30
+ }
31
+ return "";
32
+ }
33
+ var OpenAICompatibleProvider = class {
34
+ name = "openai-compatible";
35
+ baseUrl;
36
+ apiKey;
37
+ constructor(options) {
38
+ this.baseUrl = options.baseUrl.replace(/\/$/, "");
39
+ this.apiKey = options.apiKey;
40
+ }
41
+ async generate(input) {
42
+ const controller = new AbortController();
43
+ const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
44
+ try {
45
+ const url = new URL("chat/completions", `${this.baseUrl}/`);
46
+ const response = await fetch(url, {
47
+ method: "POST",
48
+ signal: controller.signal,
49
+ headers: {
50
+ "content-type": "application/json",
51
+ ...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
52
+ },
53
+ body: JSON.stringify({
54
+ model: input.model,
55
+ temperature: input.temperature,
56
+ max_tokens: input.maxOutputTokens,
57
+ messages: [
58
+ {
59
+ role: "system",
60
+ content: "You reduce noisy command output into compact answers for agents and automation."
61
+ },
62
+ {
63
+ role: "user",
64
+ content: input.prompt
65
+ }
66
+ ]
67
+ })
68
+ });
69
+ if (!response.ok) {
70
+ throw new Error(`Provider returned HTTP ${response.status}`);
71
+ }
72
+ const data = await response.json();
73
+ const text = extractMessageText(data);
74
+ if (!text.trim()) {
75
+ throw new Error("Provider returned an empty response");
76
+ }
77
+ return {
78
+ text,
79
+ usage: data?.usage ? {
80
+ inputTokens: data.usage.prompt_tokens,
81
+ outputTokens: data.usage.completion_tokens,
82
+ totalTokens: data.usage.total_tokens
83
+ } : void 0,
84
+ raw: data
85
+ };
86
+ } catch (error) {
87
+ if (error.name === "AbortError") {
88
+ throw new Error("Provider request timed out");
89
+ }
90
+ throw error;
91
+ } finally {
92
+ clearTimeout(timeout);
93
+ }
94
+ }
95
+ };
96
+
97
+ // src/providers/factory.ts
98
+ function createProvider(config) {
99
+ if (config.provider.provider === "openai-compatible") {
100
+ return new OpenAICompatibleProvider({
101
+ baseUrl: config.provider.baseUrl,
102
+ apiKey: config.provider.apiKey
103
+ });
104
+ }
105
+ throw new Error(`Unsupported provider: ${config.provider.provider}`);
106
+ }
107
+
108
+ // src/prompts/formats.ts
109
+ function getGenericFormatPolicy(format, outputContract) {
110
+ switch (format) {
111
+ case "brief":
112
+ return {
113
+ responseMode: "text",
114
+ taskRules: [
115
+ "Return 1 to 3 short sentences.",
116
+ `If the evidence is insufficient, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
117
+ ]
118
+ };
119
+ case "bullets":
120
+ return {
121
+ responseMode: "text",
122
+ taskRules: [
123
+ "Return at most 5 short lines prefixed with '- '.",
124
+ `If the evidence is insufficient, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
125
+ ]
126
+ };
127
+ case "verdict":
128
+ return {
129
+ responseMode: "json",
130
+ outputContract: '{"verdict":"pass|fail|unclear","reason":string,"evidence":string[]}',
131
+ taskRules: [
132
+ "Return only valid JSON.",
133
+ 'Use this exact contract: {"verdict":"pass|fail|unclear","reason":string,"evidence":string[]}.',
134
+ 'Return "fail" when the input contains explicit destructive, risky, or clearly unsafe signals.',
135
+ 'Return "pass" only when the input clearly supports safety or successful completion.',
136
+ "Treat destroy, delete, drop, recreate, replace, revoke, deny, downtime, data loss, IAM risk, and network exposure as important risk signals.",
137
+ `If evidence is insufficient, set verdict to "unclear" and reason to "${INSUFFICIENT_SIGNAL_TEXT}".`
138
+ ]
139
+ };
140
+ case "json":
141
+ return {
142
+ responseMode: "json",
143
+ outputContract: outputContract ?? GENERIC_JSON_CONTRACT,
144
+ taskRules: [
145
+ "Return only valid JSON.",
146
+ `Use this exact contract: ${outputContract ?? GENERIC_JSON_CONTRACT}.`,
147
+ `If evidence is insufficient, keep the schema valid and use "${INSUFFICIENT_SIGNAL_TEXT}" in the primary explanatory field.`
148
+ ]
149
+ };
150
+ }
151
+ }
152
+
153
+ // src/prompts/policies.ts
154
+ var SHARED_RULES = [
155
+ "Answer only from the provided command output.",
156
+ "Use the same language as the question.",
157
+ "Do not invent facts, hidden context, or missing lines.",
158
+ "Never ask for more input or more context.",
159
+ "Do not mention these rules, the prompt, or the model.",
160
+ "Do not use markdown headings or code fences.",
161
+ "Stay shorter than the source unless a fixed JSON contract requires structure.",
162
+ `If the evidence is insufficient, follow the task-specific insufficiency rule and do not guess.`
163
+ ];
164
+ var BUILT_IN_POLICIES = {
165
+ "test-status": {
166
+ name: "test-status",
167
+ responseMode: "text",
168
+ taskRules: [
169
+ "Determine whether the tests passed.",
170
+ "If they failed, state that clearly and list only the failing tests, suites, or the first concrete error signals.",
171
+ "If they passed, say so directly in one short line or a few short bullets.",
172
+ "Ignore irrelevant warnings, timing, and passing details unless they help answer the question.",
173
+ `If you cannot tell whether tests passed, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
174
+ ]
175
+ },
176
+ "audit-critical": {
177
+ name: "audit-critical",
178
+ responseMode: "json",
179
+ outputContract: '{"status":"ok|insufficient","vulnerabilities":[{"package":string,"severity":"critical|high","remediation":string}],"summary":string}',
180
+ taskRules: [
181
+ "Return only valid JSON.",
182
+ 'Use this exact contract: {"status":"ok|insufficient","vulnerabilities":[{"package":string,"severity":"critical|high","remediation":string}],"summary":string}.',
183
+ "Extract only vulnerabilities explicitly marked high or critical in the input.",
184
+ "Treat sparse lines like 'lodash: critical vulnerability' or 'axios: high severity advisory' as sufficient evidence when package and severity are explicit.",
185
+ "Do not invent package names, severities, CVEs, or remediations.",
186
+ 'If the input clearly contains no qualifying vulnerabilities, return {"status":"ok","vulnerabilities":[],"summary":"No high or critical vulnerabilities found in the provided input."}.',
187
+ `If the input does not provide enough evidence to determine vulnerability status, return status "insufficient" and use "${INSUFFICIENT_SIGNAL_TEXT}" in summary.`
188
+ ]
189
+ },
190
+ "diff-summary": {
191
+ name: "diff-summary",
192
+ responseMode: "json",
193
+ outputContract: '{"status":"ok|insufficient","answer":string,"evidence":string[],"risks":string[]}',
194
+ taskRules: [
195
+ "Return only valid JSON.",
196
+ 'Use this exact contract: {"status":"ok|insufficient","answer":string,"evidence":string[],"risks":string[]}.',
197
+ "Summarize what changed at a high level, grounded only in the visible diff or output.",
198
+ "Evidence should cite the most important visible files, modules, resources, or actions.",
199
+ "Risks should include migrations, config changes, security changes, destructive actions, or unknown impact when visible.",
200
+ `If the change signal is incomplete, return status "insufficient" and use "${INSUFFICIENT_SIGNAL_TEXT}" in answer.`
201
+ ]
202
+ },
203
+ "build-failure": {
204
+ name: "build-failure",
205
+ responseMode: "text",
206
+ taskRules: [
207
+ "Identify the most likely root cause of the build failure.",
208
+ "Give the first concrete fix or next step in the same answer.",
209
+ "Keep the response to 1 or 2 short sentences.",
210
+ `If the root cause is not visible, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
211
+ ]
212
+ },
213
+ "log-errors": {
214
+ name: "log-errors",
215
+ responseMode: "text",
216
+ taskRules: [
217
+ "Return at most 5 short bullet points.",
218
+ "Extract only the most relevant error or failure signals.",
219
+ "Prefer recurring or top-level errors over long stack traces.",
220
+ "Do not dump full traces unless a single trace line is the key signal.",
221
+ `If there is no clear error signal, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
222
+ ]
223
+ },
224
+ "infra-risk": {
225
+ name: "infra-risk",
226
+ responseMode: "json",
227
+ outputContract: '{"verdict":"pass|fail|unclear","reason":string,"evidence":string[]}',
228
+ taskRules: [
229
+ "Return only valid JSON.",
230
+ 'Use this exact contract: {"verdict":"pass|fail|unclear","reason":string,"evidence":string[]}.',
231
+ 'Return "fail" when the input contains explicit destructive or clearly risky signals such as destroy, delete, drop, recreate, replace, revoke, deny, downtime, data loss, IAM risk, or network exposure.',
232
+ 'Treat short plan summaries like "1 to destroy" or "resources to destroy" as enough evidence for "fail".',
233
+ 'Return "pass" only when the input clearly shows no risky changes or explicitly safe behavior.',
234
+ 'Return "unclear" when the input is incomplete, ambiguous, or does not show enough evidence to judge safety.',
235
+ "Evidence should contain the shortest concrete lines or phrases that justify the verdict."
236
+ ]
237
+ }
238
+ };
239
+ function resolvePromptPolicy(args) {
240
+ if (args.policyName) {
241
+ const policy = BUILT_IN_POLICIES[args.policyName];
242
+ return {
243
+ ...policy,
244
+ sharedRules: SHARED_RULES
245
+ };
246
+ }
247
+ const genericPolicy = getGenericFormatPolicy(args.format, args.outputContract);
248
+ return {
249
+ name: `generic-${args.format}`,
250
+ responseMode: genericPolicy.responseMode,
251
+ outputContract: genericPolicy.outputContract,
252
+ sharedRules: SHARED_RULES,
253
+ taskRules: genericPolicy.taskRules
254
+ };
255
+ }
256
+
257
+ // src/prompts/buildPrompt.ts
258
+ function buildPrompt(args) {
259
+ const policy = resolvePromptPolicy({
260
+ format: args.format,
261
+ policyName: args.policyName,
262
+ outputContract: args.outputContract
263
+ });
264
+ const prompt = [
265
+ "You are Sift, a CLI output reduction assistant for downstream agents and automation.",
266
+ "Hard rules:",
267
+ ...policy.sharedRules.map((rule) => `- ${rule}`),
268
+ "",
269
+ `Task policy: ${policy.name}`,
270
+ ...policy.taskRules.map((rule) => `- ${rule}`),
271
+ ...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
272
+ "",
273
+ `Question: ${args.question}`,
274
+ "",
275
+ "Command output:",
276
+ '"""',
277
+ args.input,
278
+ '"""'
279
+ ].join("\n");
280
+ return {
281
+ prompt,
282
+ responseMode: policy.responseMode
283
+ };
284
+ }
285
+
286
+ // src/core/quality.ts
287
+ var META_PATTERNS = [
288
+ /please provide/i,
289
+ /need more (?:input|context|information|details)/i,
290
+ /provided command output/i,
291
+ /based on the provided/i,
292
+ /as an ai/i,
293
+ /here(?:'s| is) (?:the )?(?:json|answer)/i,
294
+ /cannot determine without/i
295
+ ];
296
+ function normalizeForComparison(input) {
297
+ return input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\s+/g, " ").trim();
298
+ }
299
+ function isRetriableReason(reason) {
300
+ return /timed out|http 408|http 409|http 425|http 429|http 5\d\d|network/i.test(
301
+ reason.toLowerCase()
302
+ );
303
+ }
304
+ function looksLikeRejectedModelOutput(args) {
305
+ const source = normalizeForComparison(args.source);
306
+ const candidate = normalizeForComparison(args.candidate);
307
+ if (!candidate) {
308
+ return true;
309
+ }
310
+ if (candidate === INSUFFICIENT_SIGNAL_TEXT) {
311
+ return false;
312
+ }
313
+ if (candidate.includes("```")) {
314
+ return true;
315
+ }
316
+ if (META_PATTERNS.some((pattern) => pattern.test(candidate))) {
317
+ return true;
318
+ }
319
+ if (args.responseMode === "json") {
320
+ const trimmed = args.candidate.trim();
321
+ if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
322
+ return true;
323
+ }
324
+ }
325
+ if (source.length >= 800 && candidate.length > source.length * 0.8) {
326
+ return true;
327
+ }
328
+ if (source.length > 0 && source.length < 800 && candidate.length > source.length + 160) {
329
+ return true;
330
+ }
331
+ return false;
332
+ }
333
+
334
+ // src/core/fallback.ts
335
+ var RAW_FALLBACK_SLICE = 1200;
336
+ function buildStructuredError(reason) {
337
+ return {
338
+ status: "error",
339
+ reason,
340
+ retriable: isRetriableReason(reason)
341
+ };
342
+ }
343
+ function buildFallbackOutput(args) {
344
+ if (args.format === "verdict") {
345
+ return JSON.stringify(
346
+ {
347
+ ...buildStructuredError(args.reason),
348
+ verdict: "unclear",
349
+ reason: `Sift fallback: ${args.reason}`,
350
+ evidence: []
351
+ },
352
+ null,
353
+ 2
354
+ );
355
+ }
356
+ if (args.format === "json") {
357
+ return JSON.stringify(buildStructuredError(args.reason), null, 2);
358
+ }
359
+ const prefix = `Sift fallback triggered (${args.reason}).`;
360
+ if (!args.rawFallback) {
361
+ return prefix;
362
+ }
363
+ return [prefix, "", args.rawInput.slice(-RAW_FALLBACK_SLICE)].join("\n");
364
+ }
365
+
366
+ // src/core/heuristics.ts
367
+ var RISK_LINE_PATTERN = /(destroy|delete|drop|recreate|replace|revoke|deny|downtime|data loss|iam|network exposure)/i;
368
+ var ZERO_DESTRUCTIVE_SUMMARY_PATTERN = /\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i;
369
+ var SAFE_LINE_PATTERN = /(no changes|up-to-date|up to date|no risky changes|safe to apply)/i;
370
+ function collectEvidence(input, matcher, limit = 3) {
371
+ return input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && matcher.test(line)).slice(0, limit);
372
+ }
373
+ function inferSeverity(token) {
374
+ return token.toLowerCase().includes("critical") ? "critical" : "high";
375
+ }
376
+ function inferPackage(line) {
377
+ const match = line.match(/^\s*([@a-z0-9._/-]+)\s*:/i);
378
+ return match?.[1] ?? null;
379
+ }
380
+ function inferRemediation(pkg) {
381
+ return `Upgrade ${pkg} to a patched version.`;
382
+ }
383
+ function auditCriticalHeuristic(input) {
384
+ const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
385
+ if (!/\b(critical|high)\b/i.test(line)) {
386
+ return null;
387
+ }
388
+ const pkg = inferPackage(line);
389
+ if (!pkg) {
390
+ return null;
391
+ }
392
+ return {
393
+ package: pkg,
394
+ severity: inferSeverity(line),
395
+ remediation: inferRemediation(pkg)
396
+ };
397
+ }).filter((item) => item !== null);
398
+ if (vulnerabilities.length === 0) {
399
+ return null;
400
+ }
401
+ const firstVulnerability = vulnerabilities[0];
402
+ return JSON.stringify(
403
+ {
404
+ status: "ok",
405
+ vulnerabilities,
406
+ summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
407
+ },
408
+ null,
409
+ 2
410
+ );
411
+ }
412
+ function infraRiskHeuristic(input) {
413
+ const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
414
+ const riskEvidence = input.split("\n").map((line) => line.trim()).filter(
415
+ (line) => line.length > 0 && RISK_LINE_PATTERN.test(line) && !ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)
416
+ ).slice(0, 3);
417
+ if (riskEvidence.length > 0) {
418
+ return JSON.stringify(
419
+ {
420
+ verdict: "fail",
421
+ reason: "Destructive or clearly risky infrastructure change signals are present.",
422
+ evidence: riskEvidence
423
+ },
424
+ null,
425
+ 2
426
+ );
427
+ }
428
+ if (zeroDestructiveEvidence.length > 0) {
429
+ return JSON.stringify(
430
+ {
431
+ verdict: "pass",
432
+ reason: "The provided input explicitly indicates zero destructive changes.",
433
+ evidence: zeroDestructiveEvidence
434
+ },
435
+ null,
436
+ 2
437
+ );
438
+ }
439
+ const safeEvidence = collectEvidence(input, SAFE_LINE_PATTERN);
440
+ if (safeEvidence.length > 0) {
441
+ return JSON.stringify(
442
+ {
443
+ verdict: "pass",
444
+ reason: "The provided input explicitly indicates no risky infrastructure changes.",
445
+ evidence: safeEvidence
446
+ },
447
+ null,
448
+ 2
449
+ );
450
+ }
451
+ return null;
452
+ }
453
+ function applyHeuristicPolicy(policyName, input) {
454
+ if (!policyName) {
455
+ return null;
456
+ }
457
+ if (policyName === "audit-critical") {
458
+ return auditCriticalHeuristic(input);
459
+ }
460
+ if (policyName === "infra-risk") {
461
+ return infraRiskHeuristic(input);
462
+ }
463
+ return null;
464
+ }
465
+
466
+ // src/core/redact.ts
467
+ var BASE_PATTERNS = [
468
+ [/\bBearer\s+[A-Za-z0-9._-]+\b/gi, "Bearer ***"],
469
+ [/\bsk-[A-Za-z0-9_-]+\b/g, "sk-***"],
470
+ [/\b(api[_-]?key)\s*[:=]\s*([^\s]+)/gi, "$1=***"],
471
+ [/\b(token)\s*[:=]\s*([^\s]+)/gi, "$1=***"],
472
+ [/\b(password|passwd|pwd)\s*[:=]\s*([^\s]+)/gi, "$1=***"],
473
+ [/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "***@***"],
474
+ [/\beyJ[A-Za-z0-9._-]+\b/g, "***JWT***"]
475
+ ];
476
+ var STRICT_PATTERNS = [
477
+ [/([?&](?:token|key|api_key|access_token)=)[^&\s]+/gi, "$1***"],
478
+ [/\b[0-9a-f]{32,}\b/gi, "***HEX***"]
479
+ ];
480
+ function redactInput(input, options) {
481
+ let output = input;
482
+ for (const [pattern, replacement] of BASE_PATTERNS) {
483
+ output = output.replace(pattern, replacement);
484
+ }
485
+ if (options.strict) {
486
+ for (const [pattern, replacement] of STRICT_PATTERNS) {
487
+ output = output.replace(pattern, replacement);
488
+ }
489
+ }
490
+ return output;
491
+ }
492
+
493
+ // src/core/sanitize.ts
494
+ import stripAnsi from "strip-ansi";
495
+ function sanitizeInput(input, stripAnsiEnabled) {
496
+ let output = input;
497
+ if (stripAnsiEnabled) {
498
+ output = stripAnsi(output);
499
+ }
500
+ return output.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\u0000/g, "").replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").trim();
501
+ }
502
+
503
+ // src/core/truncate.ts
504
+ var SIGNAL_PATTERN = /(error|fail|failed|exception|panic|fatal|critical|denied|timeout|traceback)/i;
505
+ var OMITTED_MARKER = "\n...[middle content omitted]...\n";
506
+ var SIGNAL_MARKER = "\n...[selected signal lines]...\n";
507
+ function collectSignalLines(input) {
508
+ const deduped = /* @__PURE__ */ new Set();
509
+ for (const line of input.split("\n")) {
510
+ if (SIGNAL_PATTERN.test(line)) {
511
+ deduped.add(line.trimEnd());
512
+ }
513
+ }
514
+ return [...deduped].slice(0, 120);
515
+ }
516
+ function truncateInput(input, options) {
517
+ if (input.length <= options.maxInputChars) {
518
+ return { text: input, truncatedApplied: false };
519
+ }
520
+ let headLength = Math.min(options.headChars, input.length, options.maxInputChars);
521
+ let tailLength = Math.min(options.tailChars, input.length - headLength, options.maxInputChars);
522
+ const signalLines = collectSignalLines(input).join("\n");
523
+ while (headLength + tailLength + OMITTED_MARKER.length > options.maxInputChars) {
524
+ if (headLength >= tailLength && headLength > 0) {
525
+ headLength = Math.max(0, headLength - 250);
526
+ } else if (tailLength > 0) {
527
+ tailLength = Math.max(0, tailLength - 250);
528
+ } else {
529
+ break;
530
+ }
531
+ }
532
+ const head = input.slice(0, headLength);
533
+ const tail = input.slice(input.length - tailLength);
534
+ const signalBudget = options.maxInputChars - head.length - tail.length - OMITTED_MARKER.length - (signalLines ? SIGNAL_MARKER.length : 0);
535
+ const signalSnippet = signalBudget > 0 && signalLines ? signalLines.slice(0, signalBudget) : "";
536
+ const text = [head, OMITTED_MARKER, signalSnippet ? `${SIGNAL_MARKER}${signalSnippet}` : "", tail].join("").slice(0, options.maxInputChars);
537
+ return {
538
+ text,
539
+ truncatedApplied: true
540
+ };
541
+ }
542
+
543
+ // src/core/pipeline.ts
544
+ function prepareInput(raw, config) {
545
+ const sanitized = sanitizeInput(raw, config.stripAnsi);
546
+ const redacted = config.redact || config.redactStrict ? redactInput(sanitized, { strict: config.redactStrict }) : sanitized;
547
+ const truncated = truncateInput(redacted, {
548
+ maxInputChars: config.maxInputChars,
549
+ headChars: config.headChars,
550
+ tailChars: config.tailChars
551
+ });
552
+ return {
553
+ raw,
554
+ sanitized,
555
+ redacted,
556
+ truncated: truncated.text,
557
+ meta: {
558
+ originalLength: raw.length,
559
+ finalLength: truncated.text.length,
560
+ redactionApplied: config.redact || config.redactStrict,
561
+ truncatedApplied: truncated.truncatedApplied
562
+ }
563
+ };
564
+ }
565
+
566
+ // src/core/run.ts
567
+ function normalizeOutput(text, responseMode) {
568
+ if (responseMode !== "json") {
569
+ return text.trim();
570
+ }
571
+ try {
572
+ const parsed = JSON.parse(text);
573
+ return JSON.stringify(parsed, null, 2);
574
+ } catch {
575
+ throw new Error("Provider returned invalid JSON");
576
+ }
577
+ }
578
+ async function runSift(request) {
579
+ const prepared = prepareInput(request.stdin, request.config.input);
580
+ const { prompt, responseMode } = buildPrompt({
581
+ question: request.question,
582
+ format: request.format,
583
+ input: prepared.truncated,
584
+ policyName: request.policyName,
585
+ outputContract: request.outputContract
586
+ });
587
+ const provider = createProvider(request.config);
588
+ if (request.config.runtime.verbose) {
589
+ process.stderr.write(
590
+ `${pc.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
591
+ `
592
+ );
593
+ }
594
+ const heuristicOutput = applyHeuristicPolicy(
595
+ request.policyName,
596
+ prepared.truncated
597
+ );
598
+ if (heuristicOutput) {
599
+ if (request.config.runtime.verbose) {
600
+ process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
601
+ `);
602
+ }
603
+ return heuristicOutput;
604
+ }
605
+ try {
606
+ const result = await provider.generate({
607
+ model: request.config.provider.model,
608
+ prompt,
609
+ temperature: request.config.provider.temperature,
610
+ maxOutputTokens: request.config.provider.maxOutputTokens,
611
+ timeoutMs: request.config.provider.timeoutMs,
612
+ responseMode
613
+ });
614
+ if (looksLikeRejectedModelOutput({
615
+ source: prepared.truncated,
616
+ candidate: result.text,
617
+ responseMode
618
+ })) {
619
+ throw new Error("Model output rejected by quality gate");
620
+ }
621
+ return normalizeOutput(result.text, responseMode);
622
+ } catch (error) {
623
+ const reason = error instanceof Error ? error.message : "unknown_error";
624
+ return buildFallbackOutput({
625
+ format: request.format,
626
+ reason,
627
+ rawInput: prepared.truncated,
628
+ rawFallback: request.config.runtime.rawFallback,
629
+ jsonFallback: request.fallbackJson
630
+ });
631
+ }
632
+ }
633
+
634
+ // src/core/exec.ts
635
+ var PROMPT_PATTERNS = [
636
+ /\[[^\]]*y\/n[^\]]*\]\s*$/i,
637
+ /\([^)]+y\/n[^)]*\)\s*$/i,
638
+ /continue\?\s*$/i,
639
+ /password:\s*$/i,
640
+ /passphrase:\s*$/i,
641
+ /otp:\s*$/i,
642
+ /enter choice:\s*$/i
643
+ ];
644
+ var PROMPT_WINDOW_CHARS = 512;
645
+ var BoundedCapture = class {
646
+ headBudget;
647
+ tailBudget;
648
+ maxChars;
649
+ full = "";
650
+ head = "";
651
+ tail = "";
652
+ overflowed = false;
653
+ totalChars = 0;
654
+ constructor(maxChars) {
655
+ this.maxChars = maxChars;
656
+ this.headBudget = Math.max(1, Math.floor(maxChars / 2));
657
+ this.tailBudget = Math.max(1, maxChars - this.headBudget);
658
+ }
659
+ push(chunk) {
660
+ this.totalChars += chunk.length;
661
+ if (!this.overflowed) {
662
+ this.full += chunk;
663
+ if (this.full.length <= this.maxChars) {
664
+ return;
665
+ }
666
+ this.overflowed = true;
667
+ this.head = this.full.slice(0, this.headBudget);
668
+ this.tail = this.full.slice(-this.tailBudget);
669
+ this.full = "";
670
+ return;
671
+ }
672
+ this.tail = `${this.tail}${chunk}`.slice(-this.tailBudget);
673
+ }
674
+ render() {
675
+ if (!this.overflowed) {
676
+ return this.full;
677
+ }
678
+ return `${this.head}${CAPTURE_OMITTED_MARKER}${this.tail}`;
679
+ }
680
+ getTotalChars() {
681
+ return this.totalChars;
682
+ }
683
+ wasTruncated() {
684
+ return this.overflowed;
685
+ }
686
+ };
687
+ function looksInteractivePrompt(windowText) {
688
+ return PROMPT_PATTERNS.some((pattern) => pattern.test(windowText));
689
+ }
690
+ function signalToExitCode(signal) {
691
+ if (!signal) {
692
+ return 1;
693
+ }
694
+ const signalNumber = osConstants.signals[signal];
695
+ if (typeof signalNumber !== "number") {
696
+ return 1;
697
+ }
698
+ return 128 + signalNumber;
699
+ }
700
+ function normalizeChildExitCode(status, signal) {
701
+ if (typeof status === "number") {
702
+ return status;
703
+ }
704
+ return signalToExitCode(signal);
705
+ }
706
+ function buildCommandPreview(request) {
707
+ if (request.shellCommand) {
708
+ return request.shellCommand;
709
+ }
710
+ return (request.command ?? []).join(" ");
711
+ }
712
+ async function runExec(request) {
713
+ const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
714
+ const hasShellCommand = typeof request.shellCommand === "string";
715
+ if (hasArgvCommand === hasShellCommand) {
716
+ throw new Error("Provide either --shell <command> or -- <program> [args...].");
717
+ }
718
+ const shellPath = process.env.SHELL || "/bin/bash";
719
+ if (request.config.runtime.verbose) {
720
+ process.stderr.write(
721
+ `${pc2.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${buildCommandPreview(request)}
722
+ `
723
+ );
724
+ }
725
+ const capture = new BoundedCapture(request.config.input.maxCaptureChars);
726
+ let promptWindow = "";
727
+ let bypassed = false;
728
+ let childStatus = null;
729
+ let childSignal = null;
730
+ let childSpawnError = null;
731
+ const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
732
+ stdio: ["inherit", "pipe", "pipe"]
733
+ }) : spawn(request.command[0], request.command.slice(1), {
734
+ stdio: ["inherit", "pipe", "pipe"]
735
+ });
736
+ const handleChunk = (chunk) => {
737
+ const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
738
+ if (bypassed) {
739
+ process.stderr.write(text);
740
+ return;
741
+ }
742
+ capture.push(text);
743
+ promptWindow = `${promptWindow}${text}`.slice(-PROMPT_WINDOW_CHARS);
744
+ if (!looksInteractivePrompt(promptWindow)) {
745
+ return;
746
+ }
747
+ bypassed = true;
748
+ if (request.config.runtime.verbose) {
749
+ process.stderr.write(`${pc2.dim("sift")} bypass=interactive-prompt
750
+ `);
751
+ }
752
+ process.stderr.write(capture.render());
753
+ };
754
+ child.stdout.on("data", handleChunk);
755
+ child.stderr.on("data", handleChunk);
756
+ await new Promise((resolve, reject) => {
757
+ child.on("error", (error) => {
758
+ childSpawnError = error;
759
+ reject(error);
760
+ });
761
+ child.on("close", (status, signal) => {
762
+ childStatus = status;
763
+ childSignal = signal;
764
+ resolve();
765
+ });
766
+ }).catch((error) => {
767
+ if (error instanceof Error) {
768
+ throw error;
769
+ }
770
+ throw new Error("Failed to start child process.");
771
+ });
772
+ if (childSpawnError) {
773
+ throw childSpawnError;
774
+ }
775
+ const exitCode = normalizeChildExitCode(childStatus, childSignal);
776
+ if (request.config.runtime.verbose) {
777
+ process.stderr.write(
778
+ `${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
779
+ `
780
+ );
781
+ }
782
+ if (!bypassed) {
783
+ const output = await runSift({
784
+ ...request,
785
+ stdin: capture.render()
786
+ });
787
+ process.stdout.write(`${output}
788
+ `);
789
+ }
790
+ return exitCode;
791
+ }
792
+
793
+ // src/config/defaults.ts
794
+ var defaultConfig = {
795
+ provider: {
796
+ provider: "openai-compatible",
797
+ model: "gpt-4.1-mini",
798
+ baseUrl: "https://api.openai.com/v1",
799
+ apiKey: "",
800
+ timeoutMs: 2e4,
801
+ temperature: 0.1,
802
+ maxOutputTokens: 220
803
+ },
804
+ input: {
805
+ stripAnsi: true,
806
+ redact: false,
807
+ redactStrict: false,
808
+ maxCaptureChars: 25e4,
809
+ maxInputChars: 2e4,
810
+ headChars: 6e3,
811
+ tailChars: 6e3
812
+ },
813
+ runtime: {
814
+ rawFallback: true,
815
+ verbose: false
816
+ },
817
+ presets: {
818
+ "test-status": {
819
+ question: "Did the tests pass? If not, list only the failing tests or suites.",
820
+ format: "bullets",
821
+ policy: "test-status"
822
+ },
823
+ "audit-critical": {
824
+ question: "Extract only high and critical vulnerabilities. Include package, severity, and a short remediation note.",
825
+ format: "json",
826
+ policy: "audit-critical",
827
+ outputContract: '{"status":"ok|insufficient","vulnerabilities":[{"package":string,"severity":"critical|high","remediation":string}],"summary":string}'
828
+ },
829
+ "diff-summary": {
830
+ question: "Summarize the code changes and mention any risky or high-impact areas.",
831
+ format: "json",
832
+ policy: "diff-summary",
833
+ outputContract: '{"status":"ok|insufficient","answer":string,"evidence":string[],"risks":string[]}'
834
+ },
835
+ "build-failure": {
836
+ question: "Identify the most likely root cause of the build failure and the first thing to fix.",
837
+ format: "brief",
838
+ policy: "build-failure"
839
+ },
840
+ "log-errors": {
841
+ question: "Extract only the most relevant errors or failure signals.",
842
+ format: "bullets",
843
+ policy: "log-errors"
844
+ },
845
+ "infra-risk": {
846
+ question: "Assess whether the infrastructure changes are risky and whether they look safe to apply.",
847
+ format: "verdict",
848
+ policy: "infra-risk"
849
+ }
850
+ }
851
+ };
852
+
853
+ // src/config/load.ts
854
+ import fs from "fs";
855
+ import path2 from "path";
856
+ import YAML from "yaml";
857
+ function findConfigPath(explicitPath) {
858
+ if (explicitPath) {
859
+ const resolved = path2.resolve(explicitPath);
860
+ if (!fs.existsSync(resolved)) {
861
+ throw new Error(`Config file not found: ${resolved}`);
862
+ }
863
+ return resolved;
864
+ }
865
+ for (const candidate of DEFAULT_CONFIG_SEARCH_PATHS) {
866
+ if (fs.existsSync(candidate)) {
867
+ return candidate;
868
+ }
869
+ }
870
+ return null;
871
+ }
872
+ function loadRawConfig(explicitPath) {
873
+ const configPath = findConfigPath(explicitPath);
874
+ if (!configPath) {
875
+ return {};
876
+ }
877
+ const content = fs.readFileSync(configPath, "utf8");
878
+ return YAML.parse(content) ?? {};
879
+ }
880
+
881
+ // src/config/schema.ts
882
+ import { z } from "zod";
883
+ var providerNameSchema = z.enum(["openai-compatible"]);
884
+ var outputFormatSchema = z.enum([
885
+ "brief",
886
+ "bullets",
887
+ "json",
888
+ "verdict"
889
+ ]);
890
+ var responseModeSchema = z.enum(["text", "json"]);
891
+ var promptPolicyNameSchema = z.enum([
892
+ "test-status",
893
+ "audit-critical",
894
+ "diff-summary",
895
+ "build-failure",
896
+ "log-errors",
897
+ "infra-risk"
898
+ ]);
899
+ var providerConfigSchema = z.object({
900
+ provider: providerNameSchema,
901
+ model: z.string().min(1),
902
+ baseUrl: z.string().url(),
903
+ apiKey: z.string().optional(),
904
+ timeoutMs: z.number().int().positive(),
905
+ temperature: z.number().min(0).max(2),
906
+ maxOutputTokens: z.number().int().positive()
907
+ });
908
+ var inputConfigSchema = z.object({
909
+ stripAnsi: z.boolean(),
910
+ redact: z.boolean(),
911
+ redactStrict: z.boolean(),
912
+ maxCaptureChars: z.number().int().positive(),
913
+ maxInputChars: z.number().int().positive(),
914
+ headChars: z.number().int().positive(),
915
+ tailChars: z.number().int().positive()
916
+ });
917
+ var runtimeConfigSchema = z.object({
918
+ rawFallback: z.boolean(),
919
+ verbose: z.boolean()
920
+ });
921
+ var presetDefinitionSchema = z.object({
922
+ question: z.string().min(1),
923
+ format: outputFormatSchema,
924
+ policy: promptPolicyNameSchema.optional(),
925
+ outputContract: z.string().optional(),
926
+ fallbackJson: z.unknown().optional()
927
+ });
928
+ var siftConfigSchema = z.object({
929
+ provider: providerConfigSchema,
930
+ input: inputConfigSchema,
931
+ runtime: runtimeConfigSchema,
932
+ presets: z.record(presetDefinitionSchema)
933
+ });
934
+
935
+ // src/config/resolve.ts
936
+ function isRecord(value) {
937
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
938
+ }
939
+ function mergeDefined(base, override) {
940
+ if (!isRecord(override)) {
941
+ return base;
942
+ }
943
+ const result = isRecord(base) ? { ...base } : {};
944
+ for (const [key, value] of Object.entries(override)) {
945
+ if (value === void 0) {
946
+ continue;
947
+ }
948
+ const existing = result[key];
949
+ if (isRecord(existing) && isRecord(value)) {
950
+ result[key] = mergeDefined(existing, value);
951
+ continue;
952
+ }
953
+ result[key] = value;
954
+ }
955
+ return result;
956
+ }
957
+ function buildEnvOverrides(env) {
958
+ const overrides = {};
959
+ if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_API_KEY || env.SIFT_TIMEOUT_MS) {
960
+ overrides.provider = {
961
+ provider: env.SIFT_PROVIDER,
962
+ model: env.SIFT_MODEL,
963
+ baseUrl: env.SIFT_BASE_URL,
964
+ apiKey: env.SIFT_API_KEY,
965
+ timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
966
+ };
967
+ }
968
+ if (env.SIFT_MAX_INPUT_CHARS || env.SIFT_MAX_CAPTURE_CHARS) {
969
+ overrides.input = {
970
+ maxCaptureChars: env.SIFT_MAX_CAPTURE_CHARS ? Number(env.SIFT_MAX_CAPTURE_CHARS) : void 0,
971
+ maxInputChars: env.SIFT_MAX_INPUT_CHARS ? Number(env.SIFT_MAX_INPUT_CHARS) : void 0
972
+ };
973
+ }
974
+ return overrides;
975
+ }
976
+ function resolveConfig(options = {}) {
977
+ const env = options.env ?? process.env;
978
+ const fileConfig = loadRawConfig(options.configPath);
979
+ const envConfig = buildEnvOverrides(env);
980
+ const merged = mergeDefined(
981
+ mergeDefined(mergeDefined(defaultConfig, fileConfig), envConfig),
982
+ options.cliOverrides ?? {}
983
+ );
984
+ return siftConfigSchema.parse(merged);
985
+ }
986
+ export {
987
+ resolveConfig,
988
+ runExec,
989
+ runSift
990
+ };