@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/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1346 -0
- package/dist/index.d.ts +106 -0
- package/dist/index.js +990 -0
- package/package.json +57 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,1346 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { createRequire } from "module";
|
|
5
|
+
import { cac } from "cac";
|
|
6
|
+
|
|
7
|
+
// src/config/load.ts
|
|
8
|
+
import fs from "fs";
|
|
9
|
+
import path2 from "path";
|
|
10
|
+
import YAML from "yaml";
|
|
11
|
+
|
|
12
|
+
// src/constants.ts
|
|
13
|
+
import os from "os";
|
|
14
|
+
import path from "path";
|
|
15
|
+
var DEFAULT_CONFIG_FILENAME = "sift.config.yaml";
|
|
16
|
+
var DEFAULT_CONFIG_SEARCH_PATHS = [
|
|
17
|
+
path.resolve(process.cwd(), "sift.config.yaml"),
|
|
18
|
+
path.resolve(process.cwd(), "sift.config.yml"),
|
|
19
|
+
path.join(os.homedir(), ".config", "sift", "config.yaml"),
|
|
20
|
+
path.join(os.homedir(), ".config", "sift", "config.yml")
|
|
21
|
+
];
|
|
22
|
+
var INSUFFICIENT_SIGNAL_TEXT = "Insufficient signal in the provided input.";
|
|
23
|
+
var GENERIC_JSON_CONTRACT = '{"answer":string,"evidence":string[],"risks":string[]}';
|
|
24
|
+
var CAPTURE_OMITTED_MARKER = "\n...[captured output omitted]...\n";
|
|
25
|
+
|
|
26
|
+
// src/config/load.ts
|
|
27
|
+
function findConfigPath(explicitPath) {
|
|
28
|
+
if (explicitPath) {
|
|
29
|
+
const resolved = path2.resolve(explicitPath);
|
|
30
|
+
if (!fs.existsSync(resolved)) {
|
|
31
|
+
throw new Error(`Config file not found: ${resolved}`);
|
|
32
|
+
}
|
|
33
|
+
return resolved;
|
|
34
|
+
}
|
|
35
|
+
for (const candidate of DEFAULT_CONFIG_SEARCH_PATHS) {
|
|
36
|
+
if (fs.existsSync(candidate)) {
|
|
37
|
+
return candidate;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
function loadRawConfig(explicitPath) {
|
|
43
|
+
const configPath = findConfigPath(explicitPath);
|
|
44
|
+
if (!configPath) {
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
48
|
+
return YAML.parse(content) ?? {};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/config/defaults.ts
|
|
52
|
+
var defaultConfig = {
|
|
53
|
+
provider: {
|
|
54
|
+
provider: "openai-compatible",
|
|
55
|
+
model: "gpt-4.1-mini",
|
|
56
|
+
baseUrl: "https://api.openai.com/v1",
|
|
57
|
+
apiKey: "",
|
|
58
|
+
timeoutMs: 2e4,
|
|
59
|
+
temperature: 0.1,
|
|
60
|
+
maxOutputTokens: 220
|
|
61
|
+
},
|
|
62
|
+
input: {
|
|
63
|
+
stripAnsi: true,
|
|
64
|
+
redact: false,
|
|
65
|
+
redactStrict: false,
|
|
66
|
+
maxCaptureChars: 25e4,
|
|
67
|
+
maxInputChars: 2e4,
|
|
68
|
+
headChars: 6e3,
|
|
69
|
+
tailChars: 6e3
|
|
70
|
+
},
|
|
71
|
+
runtime: {
|
|
72
|
+
rawFallback: true,
|
|
73
|
+
verbose: false
|
|
74
|
+
},
|
|
75
|
+
presets: {
|
|
76
|
+
"test-status": {
|
|
77
|
+
question: "Did the tests pass? If not, list only the failing tests or suites.",
|
|
78
|
+
format: "bullets",
|
|
79
|
+
policy: "test-status"
|
|
80
|
+
},
|
|
81
|
+
"audit-critical": {
|
|
82
|
+
question: "Extract only high and critical vulnerabilities. Include package, severity, and a short remediation note.",
|
|
83
|
+
format: "json",
|
|
84
|
+
policy: "audit-critical",
|
|
85
|
+
outputContract: '{"status":"ok|insufficient","vulnerabilities":[{"package":string,"severity":"critical|high","remediation":string}],"summary":string}'
|
|
86
|
+
},
|
|
87
|
+
"diff-summary": {
|
|
88
|
+
question: "Summarize the code changes and mention any risky or high-impact areas.",
|
|
89
|
+
format: "json",
|
|
90
|
+
policy: "diff-summary",
|
|
91
|
+
outputContract: '{"status":"ok|insufficient","answer":string,"evidence":string[],"risks":string[]}'
|
|
92
|
+
},
|
|
93
|
+
"build-failure": {
|
|
94
|
+
question: "Identify the most likely root cause of the build failure and the first thing to fix.",
|
|
95
|
+
format: "brief",
|
|
96
|
+
policy: "build-failure"
|
|
97
|
+
},
|
|
98
|
+
"log-errors": {
|
|
99
|
+
question: "Extract only the most relevant errors or failure signals.",
|
|
100
|
+
format: "bullets",
|
|
101
|
+
policy: "log-errors"
|
|
102
|
+
},
|
|
103
|
+
"infra-risk": {
|
|
104
|
+
question: "Assess whether the infrastructure changes are risky and whether they look safe to apply.",
|
|
105
|
+
format: "verdict",
|
|
106
|
+
policy: "infra-risk"
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/config/schema.ts
|
|
112
|
+
import { z } from "zod";
|
|
113
|
+
var providerNameSchema = z.enum(["openai-compatible"]);
|
|
114
|
+
var outputFormatSchema = z.enum([
|
|
115
|
+
"brief",
|
|
116
|
+
"bullets",
|
|
117
|
+
"json",
|
|
118
|
+
"verdict"
|
|
119
|
+
]);
|
|
120
|
+
var responseModeSchema = z.enum(["text", "json"]);
|
|
121
|
+
var promptPolicyNameSchema = z.enum([
|
|
122
|
+
"test-status",
|
|
123
|
+
"audit-critical",
|
|
124
|
+
"diff-summary",
|
|
125
|
+
"build-failure",
|
|
126
|
+
"log-errors",
|
|
127
|
+
"infra-risk"
|
|
128
|
+
]);
|
|
129
|
+
var providerConfigSchema = z.object({
|
|
130
|
+
provider: providerNameSchema,
|
|
131
|
+
model: z.string().min(1),
|
|
132
|
+
baseUrl: z.string().url(),
|
|
133
|
+
apiKey: z.string().optional(),
|
|
134
|
+
timeoutMs: z.number().int().positive(),
|
|
135
|
+
temperature: z.number().min(0).max(2),
|
|
136
|
+
maxOutputTokens: z.number().int().positive()
|
|
137
|
+
});
|
|
138
|
+
var inputConfigSchema = z.object({
|
|
139
|
+
stripAnsi: z.boolean(),
|
|
140
|
+
redact: z.boolean(),
|
|
141
|
+
redactStrict: z.boolean(),
|
|
142
|
+
maxCaptureChars: z.number().int().positive(),
|
|
143
|
+
maxInputChars: z.number().int().positive(),
|
|
144
|
+
headChars: z.number().int().positive(),
|
|
145
|
+
tailChars: z.number().int().positive()
|
|
146
|
+
});
|
|
147
|
+
var runtimeConfigSchema = z.object({
|
|
148
|
+
rawFallback: z.boolean(),
|
|
149
|
+
verbose: z.boolean()
|
|
150
|
+
});
|
|
151
|
+
var presetDefinitionSchema = z.object({
|
|
152
|
+
question: z.string().min(1),
|
|
153
|
+
format: outputFormatSchema,
|
|
154
|
+
policy: promptPolicyNameSchema.optional(),
|
|
155
|
+
outputContract: z.string().optional(),
|
|
156
|
+
fallbackJson: z.unknown().optional()
|
|
157
|
+
});
|
|
158
|
+
var siftConfigSchema = z.object({
|
|
159
|
+
provider: providerConfigSchema,
|
|
160
|
+
input: inputConfigSchema,
|
|
161
|
+
runtime: runtimeConfigSchema,
|
|
162
|
+
presets: z.record(presetDefinitionSchema)
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// src/config/resolve.ts
|
|
166
|
+
function isRecord(value) {
|
|
167
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
168
|
+
}
|
|
169
|
+
function mergeDefined(base, override) {
|
|
170
|
+
if (!isRecord(override)) {
|
|
171
|
+
return base;
|
|
172
|
+
}
|
|
173
|
+
const result = isRecord(base) ? { ...base } : {};
|
|
174
|
+
for (const [key, value] of Object.entries(override)) {
|
|
175
|
+
if (value === void 0) {
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
const existing = result[key];
|
|
179
|
+
if (isRecord(existing) && isRecord(value)) {
|
|
180
|
+
result[key] = mergeDefined(existing, value);
|
|
181
|
+
continue;
|
|
182
|
+
}
|
|
183
|
+
result[key] = value;
|
|
184
|
+
}
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
function buildEnvOverrides(env) {
|
|
188
|
+
const overrides = {};
|
|
189
|
+
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_API_KEY || env.SIFT_TIMEOUT_MS) {
|
|
190
|
+
overrides.provider = {
|
|
191
|
+
provider: env.SIFT_PROVIDER,
|
|
192
|
+
model: env.SIFT_MODEL,
|
|
193
|
+
baseUrl: env.SIFT_BASE_URL,
|
|
194
|
+
apiKey: env.SIFT_API_KEY,
|
|
195
|
+
timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
if (env.SIFT_MAX_INPUT_CHARS || env.SIFT_MAX_CAPTURE_CHARS) {
|
|
199
|
+
overrides.input = {
|
|
200
|
+
maxCaptureChars: env.SIFT_MAX_CAPTURE_CHARS ? Number(env.SIFT_MAX_CAPTURE_CHARS) : void 0,
|
|
201
|
+
maxInputChars: env.SIFT_MAX_INPUT_CHARS ? Number(env.SIFT_MAX_INPUT_CHARS) : void 0
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
return overrides;
|
|
205
|
+
}
|
|
206
|
+
function resolveConfig(options = {}) {
|
|
207
|
+
const env = options.env ?? process.env;
|
|
208
|
+
const fileConfig = loadRawConfig(options.configPath);
|
|
209
|
+
const envConfig = buildEnvOverrides(env);
|
|
210
|
+
const merged = mergeDefined(
|
|
211
|
+
mergeDefined(mergeDefined(defaultConfig, fileConfig), envConfig),
|
|
212
|
+
options.cliOverrides ?? {}
|
|
213
|
+
);
|
|
214
|
+
return siftConfigSchema.parse(merged);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// src/config/write.ts
|
|
218
|
+
import fs2 from "fs";
|
|
219
|
+
import path3 from "path";
|
|
220
|
+
import YAML2 from "yaml";
|
|
221
|
+
function writeExampleConfig(targetPath) {
|
|
222
|
+
const resolved = path3.resolve(targetPath ?? DEFAULT_CONFIG_FILENAME);
|
|
223
|
+
if (fs2.existsSync(resolved)) {
|
|
224
|
+
throw new Error(`Config file already exists at ${resolved}`);
|
|
225
|
+
}
|
|
226
|
+
const yaml = YAML2.stringify(defaultConfig);
|
|
227
|
+
fs2.mkdirSync(path3.dirname(resolved), { recursive: true });
|
|
228
|
+
fs2.writeFileSync(resolved, yaml, "utf8");
|
|
229
|
+
return resolved;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/commands/config.ts
|
|
233
|
+
var MASKED_SECRET = "***";
|
|
234
|
+
function maskConfigSecrets(value) {
|
|
235
|
+
if (Array.isArray(value)) {
|
|
236
|
+
return value.map(maskConfigSecrets);
|
|
237
|
+
}
|
|
238
|
+
if (!value || typeof value !== "object") {
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
const output = {};
|
|
242
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
243
|
+
if (key === "apiKey" && typeof entry === "string" && entry.length > 0) {
|
|
244
|
+
output[key] = MASKED_SECRET;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
output[key] = maskConfigSecrets(entry);
|
|
248
|
+
}
|
|
249
|
+
return output;
|
|
250
|
+
}
|
|
251
|
+
function configInit(targetPath) {
|
|
252
|
+
const path4 = writeExampleConfig(targetPath);
|
|
253
|
+
process.stdout.write(`${path4}
|
|
254
|
+
`);
|
|
255
|
+
}
|
|
256
|
+
function configShow(configPath, showSecrets = false) {
|
|
257
|
+
const config = resolveConfig({
|
|
258
|
+
configPath,
|
|
259
|
+
env: process.env
|
|
260
|
+
});
|
|
261
|
+
const printable = showSecrets ? config : maskConfigSecrets(config);
|
|
262
|
+
process.stdout.write(`${JSON.stringify(printable, null, 2)}
|
|
263
|
+
`);
|
|
264
|
+
}
|
|
265
|
+
function configValidate(configPath) {
|
|
266
|
+
resolveConfig({
|
|
267
|
+
configPath,
|
|
268
|
+
env: process.env
|
|
269
|
+
});
|
|
270
|
+
const resolvedPath = findConfigPath(configPath);
|
|
271
|
+
process.stdout.write(
|
|
272
|
+
`Config is valid${resolvedPath ? ` (${resolvedPath})` : " (using defaults)"}.
|
|
273
|
+
`
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// src/commands/doctor.ts
|
|
278
|
+
function runDoctor(config) {
|
|
279
|
+
const lines = [
|
|
280
|
+
"sift doctor",
|
|
281
|
+
`provider: ${config.provider.provider}`,
|
|
282
|
+
`model: ${config.provider.model}`,
|
|
283
|
+
`baseUrl: ${config.provider.baseUrl}`,
|
|
284
|
+
`apiKey: ${config.provider.apiKey ? "set" : "not set"}`,
|
|
285
|
+
`maxCaptureChars: ${config.input.maxCaptureChars}`,
|
|
286
|
+
`maxInputChars: ${config.input.maxInputChars}`,
|
|
287
|
+
`rawFallback: ${config.runtime.rawFallback}`
|
|
288
|
+
];
|
|
289
|
+
process.stdout.write(`${lines.join("\n")}
|
|
290
|
+
`);
|
|
291
|
+
const problems = [];
|
|
292
|
+
if (!config.provider.baseUrl) {
|
|
293
|
+
problems.push("Missing provider.baseUrl");
|
|
294
|
+
}
|
|
295
|
+
if (!config.provider.model) {
|
|
296
|
+
problems.push("Missing provider.model");
|
|
297
|
+
}
|
|
298
|
+
if (config.provider.provider === "openai-compatible" && !config.provider.apiKey) {
|
|
299
|
+
problems.push("Missing provider.apiKey");
|
|
300
|
+
}
|
|
301
|
+
if (problems.length > 0) {
|
|
302
|
+
process.stderr.write(`${problems.join("\n")}
|
|
303
|
+
`);
|
|
304
|
+
return 1;
|
|
305
|
+
}
|
|
306
|
+
return 0;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// src/commands/presets.ts
|
|
310
|
+
function listPresets(config) {
|
|
311
|
+
const names = Object.keys(config.presets).sort();
|
|
312
|
+
process.stdout.write(`${names.join("\n")}
|
|
313
|
+
`);
|
|
314
|
+
}
|
|
315
|
+
function showPreset(config, name, includeInternal = false) {
|
|
316
|
+
const preset = config.presets[name];
|
|
317
|
+
if (!preset) {
|
|
318
|
+
throw new Error(`Unknown preset: ${name}`);
|
|
319
|
+
}
|
|
320
|
+
const output = includeInternal ? { name, ...preset } : {
|
|
321
|
+
name,
|
|
322
|
+
question: preset.question,
|
|
323
|
+
format: preset.format
|
|
324
|
+
};
|
|
325
|
+
process.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
326
|
+
`);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// src/core/exec.ts
|
|
330
|
+
import { spawn } from "child_process";
|
|
331
|
+
import { constants as osConstants } from "os";
|
|
332
|
+
import pc2 from "picocolors";
|
|
333
|
+
|
|
334
|
+
// src/core/run.ts
|
|
335
|
+
import pc from "picocolors";
|
|
336
|
+
|
|
337
|
+
// src/providers/openaiCompatible.ts
|
|
338
|
+
function extractMessageText(payload) {
|
|
339
|
+
const content = payload?.choices?.[0]?.message?.content;
|
|
340
|
+
if (typeof content === "string") {
|
|
341
|
+
return content;
|
|
342
|
+
}
|
|
343
|
+
if (Array.isArray(content)) {
|
|
344
|
+
return content.map((item) => typeof item?.text === "string" ? item.text : "").join("").trim();
|
|
345
|
+
}
|
|
346
|
+
return "";
|
|
347
|
+
}
|
|
348
|
+
var OpenAICompatibleProvider = class {
|
|
349
|
+
name = "openai-compatible";
|
|
350
|
+
baseUrl;
|
|
351
|
+
apiKey;
|
|
352
|
+
constructor(options) {
|
|
353
|
+
this.baseUrl = options.baseUrl.replace(/\/$/, "");
|
|
354
|
+
this.apiKey = options.apiKey;
|
|
355
|
+
}
|
|
356
|
+
async generate(input) {
|
|
357
|
+
const controller = new AbortController();
|
|
358
|
+
const timeout = setTimeout(() => controller.abort(), input.timeoutMs);
|
|
359
|
+
try {
|
|
360
|
+
const url = new URL("chat/completions", `${this.baseUrl}/`);
|
|
361
|
+
const response = await fetch(url, {
|
|
362
|
+
method: "POST",
|
|
363
|
+
signal: controller.signal,
|
|
364
|
+
headers: {
|
|
365
|
+
"content-type": "application/json",
|
|
366
|
+
...this.apiKey ? { authorization: `Bearer ${this.apiKey}` } : {}
|
|
367
|
+
},
|
|
368
|
+
body: JSON.stringify({
|
|
369
|
+
model: input.model,
|
|
370
|
+
temperature: input.temperature,
|
|
371
|
+
max_tokens: input.maxOutputTokens,
|
|
372
|
+
messages: [
|
|
373
|
+
{
|
|
374
|
+
role: "system",
|
|
375
|
+
content: "You reduce noisy command output into compact answers for agents and automation."
|
|
376
|
+
},
|
|
377
|
+
{
|
|
378
|
+
role: "user",
|
|
379
|
+
content: input.prompt
|
|
380
|
+
}
|
|
381
|
+
]
|
|
382
|
+
})
|
|
383
|
+
});
|
|
384
|
+
if (!response.ok) {
|
|
385
|
+
throw new Error(`Provider returned HTTP ${response.status}`);
|
|
386
|
+
}
|
|
387
|
+
const data = await response.json();
|
|
388
|
+
const text = extractMessageText(data);
|
|
389
|
+
if (!text.trim()) {
|
|
390
|
+
throw new Error("Provider returned an empty response");
|
|
391
|
+
}
|
|
392
|
+
return {
|
|
393
|
+
text,
|
|
394
|
+
usage: data?.usage ? {
|
|
395
|
+
inputTokens: data.usage.prompt_tokens,
|
|
396
|
+
outputTokens: data.usage.completion_tokens,
|
|
397
|
+
totalTokens: data.usage.total_tokens
|
|
398
|
+
} : void 0,
|
|
399
|
+
raw: data
|
|
400
|
+
};
|
|
401
|
+
} catch (error) {
|
|
402
|
+
if (error.name === "AbortError") {
|
|
403
|
+
throw new Error("Provider request timed out");
|
|
404
|
+
}
|
|
405
|
+
throw error;
|
|
406
|
+
} finally {
|
|
407
|
+
clearTimeout(timeout);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
// src/providers/factory.ts
|
|
413
|
+
function createProvider(config) {
|
|
414
|
+
if (config.provider.provider === "openai-compatible") {
|
|
415
|
+
return new OpenAICompatibleProvider({
|
|
416
|
+
baseUrl: config.provider.baseUrl,
|
|
417
|
+
apiKey: config.provider.apiKey
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
throw new Error(`Unsupported provider: ${config.provider.provider}`);
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
// src/prompts/formats.ts
|
|
424
|
+
function getGenericFormatPolicy(format, outputContract) {
|
|
425
|
+
switch (format) {
|
|
426
|
+
case "brief":
|
|
427
|
+
return {
|
|
428
|
+
responseMode: "text",
|
|
429
|
+
taskRules: [
|
|
430
|
+
"Return 1 to 3 short sentences.",
|
|
431
|
+
`If the evidence is insufficient, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
432
|
+
]
|
|
433
|
+
};
|
|
434
|
+
case "bullets":
|
|
435
|
+
return {
|
|
436
|
+
responseMode: "text",
|
|
437
|
+
taskRules: [
|
|
438
|
+
"Return at most 5 short lines prefixed with '- '.",
|
|
439
|
+
`If the evidence is insufficient, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
440
|
+
]
|
|
441
|
+
};
|
|
442
|
+
case "verdict":
|
|
443
|
+
return {
|
|
444
|
+
responseMode: "json",
|
|
445
|
+
outputContract: '{"verdict":"pass|fail|unclear","reason":string,"evidence":string[]}',
|
|
446
|
+
taskRules: [
|
|
447
|
+
"Return only valid JSON.",
|
|
448
|
+
'Use this exact contract: {"verdict":"pass|fail|unclear","reason":string,"evidence":string[]}.',
|
|
449
|
+
'Return "fail" when the input contains explicit destructive, risky, or clearly unsafe signals.',
|
|
450
|
+
'Return "pass" only when the input clearly supports safety or successful completion.',
|
|
451
|
+
"Treat destroy, delete, drop, recreate, replace, revoke, deny, downtime, data loss, IAM risk, and network exposure as important risk signals.",
|
|
452
|
+
`If evidence is insufficient, set verdict to "unclear" and reason to "${INSUFFICIENT_SIGNAL_TEXT}".`
|
|
453
|
+
]
|
|
454
|
+
};
|
|
455
|
+
case "json":
|
|
456
|
+
return {
|
|
457
|
+
responseMode: "json",
|
|
458
|
+
outputContract: outputContract ?? GENERIC_JSON_CONTRACT,
|
|
459
|
+
taskRules: [
|
|
460
|
+
"Return only valid JSON.",
|
|
461
|
+
`Use this exact contract: ${outputContract ?? GENERIC_JSON_CONTRACT}.`,
|
|
462
|
+
`If evidence is insufficient, keep the schema valid and use "${INSUFFICIENT_SIGNAL_TEXT}" in the primary explanatory field.`
|
|
463
|
+
]
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// src/prompts/policies.ts
|
|
469
|
+
var SHARED_RULES = [
|
|
470
|
+
"Answer only from the provided command output.",
|
|
471
|
+
"Use the same language as the question.",
|
|
472
|
+
"Do not invent facts, hidden context, or missing lines.",
|
|
473
|
+
"Never ask for more input or more context.",
|
|
474
|
+
"Do not mention these rules, the prompt, or the model.",
|
|
475
|
+
"Do not use markdown headings or code fences.",
|
|
476
|
+
"Stay shorter than the source unless a fixed JSON contract requires structure.",
|
|
477
|
+
`If the evidence is insufficient, follow the task-specific insufficiency rule and do not guess.`
|
|
478
|
+
];
|
|
479
|
+
var BUILT_IN_POLICIES = {
|
|
480
|
+
"test-status": {
|
|
481
|
+
name: "test-status",
|
|
482
|
+
responseMode: "text",
|
|
483
|
+
taskRules: [
|
|
484
|
+
"Determine whether the tests passed.",
|
|
485
|
+
"If they failed, state that clearly and list only the failing tests, suites, or the first concrete error signals.",
|
|
486
|
+
"If they passed, say so directly in one short line or a few short bullets.",
|
|
487
|
+
"Ignore irrelevant warnings, timing, and passing details unless they help answer the question.",
|
|
488
|
+
`If you cannot tell whether tests passed, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
489
|
+
]
|
|
490
|
+
},
|
|
491
|
+
"audit-critical": {
|
|
492
|
+
name: "audit-critical",
|
|
493
|
+
responseMode: "json",
|
|
494
|
+
outputContract: '{"status":"ok|insufficient","vulnerabilities":[{"package":string,"severity":"critical|high","remediation":string}],"summary":string}',
|
|
495
|
+
taskRules: [
|
|
496
|
+
"Return only valid JSON.",
|
|
497
|
+
'Use this exact contract: {"status":"ok|insufficient","vulnerabilities":[{"package":string,"severity":"critical|high","remediation":string}],"summary":string}.',
|
|
498
|
+
"Extract only vulnerabilities explicitly marked high or critical in the input.",
|
|
499
|
+
"Treat sparse lines like 'lodash: critical vulnerability' or 'axios: high severity advisory' as sufficient evidence when package and severity are explicit.",
|
|
500
|
+
"Do not invent package names, severities, CVEs, or remediations.",
|
|
501
|
+
'If the input clearly contains no qualifying vulnerabilities, return {"status":"ok","vulnerabilities":[],"summary":"No high or critical vulnerabilities found in the provided input."}.',
|
|
502
|
+
`If the input does not provide enough evidence to determine vulnerability status, return status "insufficient" and use "${INSUFFICIENT_SIGNAL_TEXT}" in summary.`
|
|
503
|
+
]
|
|
504
|
+
},
|
|
505
|
+
"diff-summary": {
|
|
506
|
+
name: "diff-summary",
|
|
507
|
+
responseMode: "json",
|
|
508
|
+
outputContract: '{"status":"ok|insufficient","answer":string,"evidence":string[],"risks":string[]}',
|
|
509
|
+
taskRules: [
|
|
510
|
+
"Return only valid JSON.",
|
|
511
|
+
'Use this exact contract: {"status":"ok|insufficient","answer":string,"evidence":string[],"risks":string[]}.',
|
|
512
|
+
"Summarize what changed at a high level, grounded only in the visible diff or output.",
|
|
513
|
+
"Evidence should cite the most important visible files, modules, resources, or actions.",
|
|
514
|
+
"Risks should include migrations, config changes, security changes, destructive actions, or unknown impact when visible.",
|
|
515
|
+
`If the change signal is incomplete, return status "insufficient" and use "${INSUFFICIENT_SIGNAL_TEXT}" in answer.`
|
|
516
|
+
]
|
|
517
|
+
},
|
|
518
|
+
"build-failure": {
|
|
519
|
+
name: "build-failure",
|
|
520
|
+
responseMode: "text",
|
|
521
|
+
taskRules: [
|
|
522
|
+
"Identify the most likely root cause of the build failure.",
|
|
523
|
+
"Give the first concrete fix or next step in the same answer.",
|
|
524
|
+
"Keep the response to 1 or 2 short sentences.",
|
|
525
|
+
`If the root cause is not visible, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
526
|
+
]
|
|
527
|
+
},
|
|
528
|
+
"log-errors": {
|
|
529
|
+
name: "log-errors",
|
|
530
|
+
responseMode: "text",
|
|
531
|
+
taskRules: [
|
|
532
|
+
"Return at most 5 short bullet points.",
|
|
533
|
+
"Extract only the most relevant error or failure signals.",
|
|
534
|
+
"Prefer recurring or top-level errors over long stack traces.",
|
|
535
|
+
"Do not dump full traces unless a single trace line is the key signal.",
|
|
536
|
+
`If there is no clear error signal, reply exactly with: ${INSUFFICIENT_SIGNAL_TEXT}`
|
|
537
|
+
]
|
|
538
|
+
},
|
|
539
|
+
"infra-risk": {
|
|
540
|
+
name: "infra-risk",
|
|
541
|
+
responseMode: "json",
|
|
542
|
+
outputContract: '{"verdict":"pass|fail|unclear","reason":string,"evidence":string[]}',
|
|
543
|
+
taskRules: [
|
|
544
|
+
"Return only valid JSON.",
|
|
545
|
+
'Use this exact contract: {"verdict":"pass|fail|unclear","reason":string,"evidence":string[]}.',
|
|
546
|
+
'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.',
|
|
547
|
+
'Treat short plan summaries like "1 to destroy" or "resources to destroy" as enough evidence for "fail".',
|
|
548
|
+
'Return "pass" only when the input clearly shows no risky changes or explicitly safe behavior.',
|
|
549
|
+
'Return "unclear" when the input is incomplete, ambiguous, or does not show enough evidence to judge safety.',
|
|
550
|
+
"Evidence should contain the shortest concrete lines or phrases that justify the verdict."
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
function resolvePromptPolicy(args) {
|
|
555
|
+
if (args.policyName) {
|
|
556
|
+
const policy = BUILT_IN_POLICIES[args.policyName];
|
|
557
|
+
return {
|
|
558
|
+
...policy,
|
|
559
|
+
sharedRules: SHARED_RULES
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
const genericPolicy = getGenericFormatPolicy(args.format, args.outputContract);
|
|
563
|
+
return {
|
|
564
|
+
name: `generic-${args.format}`,
|
|
565
|
+
responseMode: genericPolicy.responseMode,
|
|
566
|
+
outputContract: genericPolicy.outputContract,
|
|
567
|
+
sharedRules: SHARED_RULES,
|
|
568
|
+
taskRules: genericPolicy.taskRules
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// src/prompts/buildPrompt.ts
|
|
573
|
+
function buildPrompt(args) {
|
|
574
|
+
const policy = resolvePromptPolicy({
|
|
575
|
+
format: args.format,
|
|
576
|
+
policyName: args.policyName,
|
|
577
|
+
outputContract: args.outputContract
|
|
578
|
+
});
|
|
579
|
+
const prompt = [
|
|
580
|
+
"You are Sift, a CLI output reduction assistant for downstream agents and automation.",
|
|
581
|
+
"Hard rules:",
|
|
582
|
+
...policy.sharedRules.map((rule) => `- ${rule}`),
|
|
583
|
+
"",
|
|
584
|
+
`Task policy: ${policy.name}`,
|
|
585
|
+
...policy.taskRules.map((rule) => `- ${rule}`),
|
|
586
|
+
...policy.outputContract ? ["", `Output contract: ${policy.outputContract}`] : [],
|
|
587
|
+
"",
|
|
588
|
+
`Question: ${args.question}`,
|
|
589
|
+
"",
|
|
590
|
+
"Command output:",
|
|
591
|
+
'"""',
|
|
592
|
+
args.input,
|
|
593
|
+
'"""'
|
|
594
|
+
].join("\n");
|
|
595
|
+
return {
|
|
596
|
+
prompt,
|
|
597
|
+
responseMode: policy.responseMode
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
// src/core/quality.ts
|
|
602
|
+
var META_PATTERNS = [
|
|
603
|
+
/please provide/i,
|
|
604
|
+
/need more (?:input|context|information|details)/i,
|
|
605
|
+
/provided command output/i,
|
|
606
|
+
/based on the provided/i,
|
|
607
|
+
/as an ai/i,
|
|
608
|
+
/here(?:'s| is) (?:the )?(?:json|answer)/i,
|
|
609
|
+
/cannot determine without/i
|
|
610
|
+
];
|
|
611
|
+
function normalizeForComparison(input) {
|
|
612
|
+
return input.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\s+/g, " ").trim();
|
|
613
|
+
}
|
|
614
|
+
function isRetriableReason(reason) {
|
|
615
|
+
return /timed out|http 408|http 409|http 425|http 429|http 5\d\d|network/i.test(
|
|
616
|
+
reason.toLowerCase()
|
|
617
|
+
);
|
|
618
|
+
}
|
|
619
|
+
function looksLikeRejectedModelOutput(args) {
|
|
620
|
+
const source = normalizeForComparison(args.source);
|
|
621
|
+
const candidate = normalizeForComparison(args.candidate);
|
|
622
|
+
if (!candidate) {
|
|
623
|
+
return true;
|
|
624
|
+
}
|
|
625
|
+
if (candidate === INSUFFICIENT_SIGNAL_TEXT) {
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
if (candidate.includes("```")) {
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
if (META_PATTERNS.some((pattern) => pattern.test(candidate))) {
|
|
632
|
+
return true;
|
|
633
|
+
}
|
|
634
|
+
if (args.responseMode === "json") {
|
|
635
|
+
const trimmed = args.candidate.trim();
|
|
636
|
+
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) {
|
|
637
|
+
return true;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
if (source.length >= 800 && candidate.length > source.length * 0.8) {
|
|
641
|
+
return true;
|
|
642
|
+
}
|
|
643
|
+
if (source.length > 0 && source.length < 800 && candidate.length > source.length + 160) {
|
|
644
|
+
return true;
|
|
645
|
+
}
|
|
646
|
+
return false;
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
// src/core/fallback.ts
|
|
650
|
+
var RAW_FALLBACK_SLICE = 1200;
|
|
651
|
+
function buildStructuredError(reason) {
|
|
652
|
+
return {
|
|
653
|
+
status: "error",
|
|
654
|
+
reason,
|
|
655
|
+
retriable: isRetriableReason(reason)
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
function buildFallbackOutput(args) {
|
|
659
|
+
if (args.format === "verdict") {
|
|
660
|
+
return JSON.stringify(
|
|
661
|
+
{
|
|
662
|
+
...buildStructuredError(args.reason),
|
|
663
|
+
verdict: "unclear",
|
|
664
|
+
reason: `Sift fallback: ${args.reason}`,
|
|
665
|
+
evidence: []
|
|
666
|
+
},
|
|
667
|
+
null,
|
|
668
|
+
2
|
|
669
|
+
);
|
|
670
|
+
}
|
|
671
|
+
if (args.format === "json") {
|
|
672
|
+
return JSON.stringify(buildStructuredError(args.reason), null, 2);
|
|
673
|
+
}
|
|
674
|
+
const prefix = `Sift fallback triggered (${args.reason}).`;
|
|
675
|
+
if (!args.rawFallback) {
|
|
676
|
+
return prefix;
|
|
677
|
+
}
|
|
678
|
+
return [prefix, "", args.rawInput.slice(-RAW_FALLBACK_SLICE)].join("\n");
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/core/heuristics.ts
|
|
682
|
+
var RISK_LINE_PATTERN = /(destroy|delete|drop|recreate|replace|revoke|deny|downtime|data loss|iam|network exposure)/i;
|
|
683
|
+
var ZERO_DESTRUCTIVE_SUMMARY_PATTERN = /\b0\s+to\s+(destroy|delete|drop|recreate|replace|revoke)\b/i;
|
|
684
|
+
var SAFE_LINE_PATTERN = /(no changes|up-to-date|up to date|no risky changes|safe to apply)/i;
|
|
685
|
+
function collectEvidence(input, matcher, limit = 3) {
|
|
686
|
+
return input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && matcher.test(line)).slice(0, limit);
|
|
687
|
+
}
|
|
688
|
+
function inferSeverity(token) {
|
|
689
|
+
return token.toLowerCase().includes("critical") ? "critical" : "high";
|
|
690
|
+
}
|
|
691
|
+
function inferPackage(line) {
|
|
692
|
+
const match = line.match(/^\s*([@a-z0-9._/-]+)\s*:/i);
|
|
693
|
+
return match?.[1] ?? null;
|
|
694
|
+
}
|
|
695
|
+
function inferRemediation(pkg2) {
|
|
696
|
+
return `Upgrade ${pkg2} to a patched version.`;
|
|
697
|
+
}
|
|
698
|
+
function auditCriticalHeuristic(input) {
|
|
699
|
+
const vulnerabilities = input.split("\n").map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
700
|
+
if (!/\b(critical|high)\b/i.test(line)) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
const pkg2 = inferPackage(line);
|
|
704
|
+
if (!pkg2) {
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
return {
|
|
708
|
+
package: pkg2,
|
|
709
|
+
severity: inferSeverity(line),
|
|
710
|
+
remediation: inferRemediation(pkg2)
|
|
711
|
+
};
|
|
712
|
+
}).filter((item) => item !== null);
|
|
713
|
+
if (vulnerabilities.length === 0) {
|
|
714
|
+
return null;
|
|
715
|
+
}
|
|
716
|
+
const firstVulnerability = vulnerabilities[0];
|
|
717
|
+
return JSON.stringify(
|
|
718
|
+
{
|
|
719
|
+
status: "ok",
|
|
720
|
+
vulnerabilities,
|
|
721
|
+
summary: vulnerabilities.length === 1 ? `One ${firstVulnerability.severity} vulnerability found in ${firstVulnerability.package}.` : `${vulnerabilities.length} high or critical vulnerabilities found in the provided input.`
|
|
722
|
+
},
|
|
723
|
+
null,
|
|
724
|
+
2
|
|
725
|
+
);
|
|
726
|
+
}
|
|
727
|
+
function infraRiskHeuristic(input) {
|
|
728
|
+
const zeroDestructiveEvidence = input.split("\n").map((line) => line.trim()).filter((line) => line.length > 0 && ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)).slice(0, 3);
|
|
729
|
+
const riskEvidence = input.split("\n").map((line) => line.trim()).filter(
|
|
730
|
+
(line) => line.length > 0 && RISK_LINE_PATTERN.test(line) && !ZERO_DESTRUCTIVE_SUMMARY_PATTERN.test(line)
|
|
731
|
+
).slice(0, 3);
|
|
732
|
+
if (riskEvidence.length > 0) {
|
|
733
|
+
return JSON.stringify(
|
|
734
|
+
{
|
|
735
|
+
verdict: "fail",
|
|
736
|
+
reason: "Destructive or clearly risky infrastructure change signals are present.",
|
|
737
|
+
evidence: riskEvidence
|
|
738
|
+
},
|
|
739
|
+
null,
|
|
740
|
+
2
|
|
741
|
+
);
|
|
742
|
+
}
|
|
743
|
+
if (zeroDestructiveEvidence.length > 0) {
|
|
744
|
+
return JSON.stringify(
|
|
745
|
+
{
|
|
746
|
+
verdict: "pass",
|
|
747
|
+
reason: "The provided input explicitly indicates zero destructive changes.",
|
|
748
|
+
evidence: zeroDestructiveEvidence
|
|
749
|
+
},
|
|
750
|
+
null,
|
|
751
|
+
2
|
|
752
|
+
);
|
|
753
|
+
}
|
|
754
|
+
const safeEvidence = collectEvidence(input, SAFE_LINE_PATTERN);
|
|
755
|
+
if (safeEvidence.length > 0) {
|
|
756
|
+
return JSON.stringify(
|
|
757
|
+
{
|
|
758
|
+
verdict: "pass",
|
|
759
|
+
reason: "The provided input explicitly indicates no risky infrastructure changes.",
|
|
760
|
+
evidence: safeEvidence
|
|
761
|
+
},
|
|
762
|
+
null,
|
|
763
|
+
2
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
return null;
|
|
767
|
+
}
|
|
768
|
+
function applyHeuristicPolicy(policyName, input) {
|
|
769
|
+
if (!policyName) {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
if (policyName === "audit-critical") {
|
|
773
|
+
return auditCriticalHeuristic(input);
|
|
774
|
+
}
|
|
775
|
+
if (policyName === "infra-risk") {
|
|
776
|
+
return infraRiskHeuristic(input);
|
|
777
|
+
}
|
|
778
|
+
return null;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// src/core/redact.ts
|
|
782
|
+
var BASE_PATTERNS = [
|
|
783
|
+
[/\bBearer\s+[A-Za-z0-9._-]+\b/gi, "Bearer ***"],
|
|
784
|
+
[/\bsk-[A-Za-z0-9_-]+\b/g, "sk-***"],
|
|
785
|
+
[/\b(api[_-]?key)\s*[:=]\s*([^\s]+)/gi, "$1=***"],
|
|
786
|
+
[/\b(token)\s*[:=]\s*([^\s]+)/gi, "$1=***"],
|
|
787
|
+
[/\b(password|passwd|pwd)\s*[:=]\s*([^\s]+)/gi, "$1=***"],
|
|
788
|
+
[/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi, "***@***"],
|
|
789
|
+
[/\beyJ[A-Za-z0-9._-]+\b/g, "***JWT***"]
|
|
790
|
+
];
|
|
791
|
+
var STRICT_PATTERNS = [
|
|
792
|
+
[/([?&](?:token|key|api_key|access_token)=)[^&\s]+/gi, "$1***"],
|
|
793
|
+
[/\b[0-9a-f]{32,}\b/gi, "***HEX***"]
|
|
794
|
+
];
|
|
795
|
+
function redactInput(input, options) {
|
|
796
|
+
let output = input;
|
|
797
|
+
for (const [pattern, replacement] of BASE_PATTERNS) {
|
|
798
|
+
output = output.replace(pattern, replacement);
|
|
799
|
+
}
|
|
800
|
+
if (options.strict) {
|
|
801
|
+
for (const [pattern, replacement] of STRICT_PATTERNS) {
|
|
802
|
+
output = output.replace(pattern, replacement);
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
return output;
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/core/sanitize.ts
|
|
809
|
+
import stripAnsi from "strip-ansi";
|
|
810
|
+
function sanitizeInput(input, stripAnsiEnabled) {
|
|
811
|
+
let output = input;
|
|
812
|
+
if (stripAnsiEnabled) {
|
|
813
|
+
output = stripAnsi(output);
|
|
814
|
+
}
|
|
815
|
+
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();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// src/core/truncate.ts
|
|
819
|
+
var SIGNAL_PATTERN = /(error|fail|failed|exception|panic|fatal|critical|denied|timeout|traceback)/i;
|
|
820
|
+
var OMITTED_MARKER = "\n...[middle content omitted]...\n";
|
|
821
|
+
var SIGNAL_MARKER = "\n...[selected signal lines]...\n";
|
|
822
|
+
function collectSignalLines(input) {
|
|
823
|
+
const deduped = /* @__PURE__ */ new Set();
|
|
824
|
+
for (const line of input.split("\n")) {
|
|
825
|
+
if (SIGNAL_PATTERN.test(line)) {
|
|
826
|
+
deduped.add(line.trimEnd());
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return [...deduped].slice(0, 120);
|
|
830
|
+
}
|
|
831
|
+
function truncateInput(input, options) {
|
|
832
|
+
if (input.length <= options.maxInputChars) {
|
|
833
|
+
return { text: input, truncatedApplied: false };
|
|
834
|
+
}
|
|
835
|
+
let headLength = Math.min(options.headChars, input.length, options.maxInputChars);
|
|
836
|
+
let tailLength = Math.min(options.tailChars, input.length - headLength, options.maxInputChars);
|
|
837
|
+
const signalLines = collectSignalLines(input).join("\n");
|
|
838
|
+
while (headLength + tailLength + OMITTED_MARKER.length > options.maxInputChars) {
|
|
839
|
+
if (headLength >= tailLength && headLength > 0) {
|
|
840
|
+
headLength = Math.max(0, headLength - 250);
|
|
841
|
+
} else if (tailLength > 0) {
|
|
842
|
+
tailLength = Math.max(0, tailLength - 250);
|
|
843
|
+
} else {
|
|
844
|
+
break;
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
const head = input.slice(0, headLength);
|
|
848
|
+
const tail = input.slice(input.length - tailLength);
|
|
849
|
+
const signalBudget = options.maxInputChars - head.length - tail.length - OMITTED_MARKER.length - (signalLines ? SIGNAL_MARKER.length : 0);
|
|
850
|
+
const signalSnippet = signalBudget > 0 && signalLines ? signalLines.slice(0, signalBudget) : "";
|
|
851
|
+
const text = [head, OMITTED_MARKER, signalSnippet ? `${SIGNAL_MARKER}${signalSnippet}` : "", tail].join("").slice(0, options.maxInputChars);
|
|
852
|
+
return {
|
|
853
|
+
text,
|
|
854
|
+
truncatedApplied: true
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// src/core/pipeline.ts
|
|
859
|
+
function prepareInput(raw, config) {
|
|
860
|
+
const sanitized = sanitizeInput(raw, config.stripAnsi);
|
|
861
|
+
const redacted = config.redact || config.redactStrict ? redactInput(sanitized, { strict: config.redactStrict }) : sanitized;
|
|
862
|
+
const truncated = truncateInput(redacted, {
|
|
863
|
+
maxInputChars: config.maxInputChars,
|
|
864
|
+
headChars: config.headChars,
|
|
865
|
+
tailChars: config.tailChars
|
|
866
|
+
});
|
|
867
|
+
return {
|
|
868
|
+
raw,
|
|
869
|
+
sanitized,
|
|
870
|
+
redacted,
|
|
871
|
+
truncated: truncated.text,
|
|
872
|
+
meta: {
|
|
873
|
+
originalLength: raw.length,
|
|
874
|
+
finalLength: truncated.text.length,
|
|
875
|
+
redactionApplied: config.redact || config.redactStrict,
|
|
876
|
+
truncatedApplied: truncated.truncatedApplied
|
|
877
|
+
}
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
// src/core/run.ts
|
|
882
|
+
function normalizeOutput(text, responseMode) {
|
|
883
|
+
if (responseMode !== "json") {
|
|
884
|
+
return text.trim();
|
|
885
|
+
}
|
|
886
|
+
try {
|
|
887
|
+
const parsed = JSON.parse(text);
|
|
888
|
+
return JSON.stringify(parsed, null, 2);
|
|
889
|
+
} catch {
|
|
890
|
+
throw new Error("Provider returned invalid JSON");
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
async function runSift(request) {
|
|
894
|
+
const prepared = prepareInput(request.stdin, request.config.input);
|
|
895
|
+
const { prompt, responseMode } = buildPrompt({
|
|
896
|
+
question: request.question,
|
|
897
|
+
format: request.format,
|
|
898
|
+
input: prepared.truncated,
|
|
899
|
+
policyName: request.policyName,
|
|
900
|
+
outputContract: request.outputContract
|
|
901
|
+
});
|
|
902
|
+
const provider = createProvider(request.config);
|
|
903
|
+
if (request.config.runtime.verbose) {
|
|
904
|
+
process.stderr.write(
|
|
905
|
+
`${pc.dim("sift")} provider=${provider.name} model=${request.config.provider.model} base_url=${request.config.provider.baseUrl} input_chars=${prepared.meta.finalLength}
|
|
906
|
+
`
|
|
907
|
+
);
|
|
908
|
+
}
|
|
909
|
+
const heuristicOutput = applyHeuristicPolicy(
|
|
910
|
+
request.policyName,
|
|
911
|
+
prepared.truncated
|
|
912
|
+
);
|
|
913
|
+
if (heuristicOutput) {
|
|
914
|
+
if (request.config.runtime.verbose) {
|
|
915
|
+
process.stderr.write(`${pc.dim("sift")} heuristic=${request.policyName}
|
|
916
|
+
`);
|
|
917
|
+
}
|
|
918
|
+
return heuristicOutput;
|
|
919
|
+
}
|
|
920
|
+
try {
|
|
921
|
+
const result = await provider.generate({
|
|
922
|
+
model: request.config.provider.model,
|
|
923
|
+
prompt,
|
|
924
|
+
temperature: request.config.provider.temperature,
|
|
925
|
+
maxOutputTokens: request.config.provider.maxOutputTokens,
|
|
926
|
+
timeoutMs: request.config.provider.timeoutMs,
|
|
927
|
+
responseMode
|
|
928
|
+
});
|
|
929
|
+
if (looksLikeRejectedModelOutput({
|
|
930
|
+
source: prepared.truncated,
|
|
931
|
+
candidate: result.text,
|
|
932
|
+
responseMode
|
|
933
|
+
})) {
|
|
934
|
+
throw new Error("Model output rejected by quality gate");
|
|
935
|
+
}
|
|
936
|
+
return normalizeOutput(result.text, responseMode);
|
|
937
|
+
} catch (error) {
|
|
938
|
+
const reason = error instanceof Error ? error.message : "unknown_error";
|
|
939
|
+
return buildFallbackOutput({
|
|
940
|
+
format: request.format,
|
|
941
|
+
reason,
|
|
942
|
+
rawInput: prepared.truncated,
|
|
943
|
+
rawFallback: request.config.runtime.rawFallback,
|
|
944
|
+
jsonFallback: request.fallbackJson
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/core/exec.ts
|
|
950
|
+
var PROMPT_PATTERNS = [
|
|
951
|
+
/\[[^\]]*y\/n[^\]]*\]\s*$/i,
|
|
952
|
+
/\([^)]+y\/n[^)]*\)\s*$/i,
|
|
953
|
+
/continue\?\s*$/i,
|
|
954
|
+
/password:\s*$/i,
|
|
955
|
+
/passphrase:\s*$/i,
|
|
956
|
+
/otp:\s*$/i,
|
|
957
|
+
/enter choice:\s*$/i
|
|
958
|
+
];
|
|
959
|
+
var PROMPT_WINDOW_CHARS = 512;
|
|
960
|
+
var BoundedCapture = class {
|
|
961
|
+
headBudget;
|
|
962
|
+
tailBudget;
|
|
963
|
+
maxChars;
|
|
964
|
+
full = "";
|
|
965
|
+
head = "";
|
|
966
|
+
tail = "";
|
|
967
|
+
overflowed = false;
|
|
968
|
+
totalChars = 0;
|
|
969
|
+
constructor(maxChars) {
|
|
970
|
+
this.maxChars = maxChars;
|
|
971
|
+
this.headBudget = Math.max(1, Math.floor(maxChars / 2));
|
|
972
|
+
this.tailBudget = Math.max(1, maxChars - this.headBudget);
|
|
973
|
+
}
|
|
974
|
+
push(chunk) {
|
|
975
|
+
this.totalChars += chunk.length;
|
|
976
|
+
if (!this.overflowed) {
|
|
977
|
+
this.full += chunk;
|
|
978
|
+
if (this.full.length <= this.maxChars) {
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
this.overflowed = true;
|
|
982
|
+
this.head = this.full.slice(0, this.headBudget);
|
|
983
|
+
this.tail = this.full.slice(-this.tailBudget);
|
|
984
|
+
this.full = "";
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
this.tail = `${this.tail}${chunk}`.slice(-this.tailBudget);
|
|
988
|
+
}
|
|
989
|
+
render() {
|
|
990
|
+
if (!this.overflowed) {
|
|
991
|
+
return this.full;
|
|
992
|
+
}
|
|
993
|
+
return `${this.head}${CAPTURE_OMITTED_MARKER}${this.tail}`;
|
|
994
|
+
}
|
|
995
|
+
getTotalChars() {
|
|
996
|
+
return this.totalChars;
|
|
997
|
+
}
|
|
998
|
+
wasTruncated() {
|
|
999
|
+
return this.overflowed;
|
|
1000
|
+
}
|
|
1001
|
+
};
|
|
1002
|
+
function looksInteractivePrompt(windowText) {
|
|
1003
|
+
return PROMPT_PATTERNS.some((pattern) => pattern.test(windowText));
|
|
1004
|
+
}
|
|
1005
|
+
function signalToExitCode(signal) {
|
|
1006
|
+
if (!signal) {
|
|
1007
|
+
return 1;
|
|
1008
|
+
}
|
|
1009
|
+
const signalNumber = osConstants.signals[signal];
|
|
1010
|
+
if (typeof signalNumber !== "number") {
|
|
1011
|
+
return 1;
|
|
1012
|
+
}
|
|
1013
|
+
return 128 + signalNumber;
|
|
1014
|
+
}
|
|
1015
|
+
function normalizeChildExitCode(status, signal) {
|
|
1016
|
+
if (typeof status === "number") {
|
|
1017
|
+
return status;
|
|
1018
|
+
}
|
|
1019
|
+
return signalToExitCode(signal);
|
|
1020
|
+
}
|
|
1021
|
+
function buildCommandPreview(request) {
|
|
1022
|
+
if (request.shellCommand) {
|
|
1023
|
+
return request.shellCommand;
|
|
1024
|
+
}
|
|
1025
|
+
return (request.command ?? []).join(" ");
|
|
1026
|
+
}
|
|
1027
|
+
async function runExec(request) {
|
|
1028
|
+
const hasArgvCommand = Array.isArray(request.command) && request.command.length > 0;
|
|
1029
|
+
const hasShellCommand = typeof request.shellCommand === "string";
|
|
1030
|
+
if (hasArgvCommand === hasShellCommand) {
|
|
1031
|
+
throw new Error("Provide either --shell <command> or -- <program> [args...].");
|
|
1032
|
+
}
|
|
1033
|
+
const shellPath = process.env.SHELL || "/bin/bash";
|
|
1034
|
+
if (request.config.runtime.verbose) {
|
|
1035
|
+
process.stderr.write(
|
|
1036
|
+
`${pc2.dim("sift")} exec mode=${hasShellCommand ? "shell" : "argv"} command=${buildCommandPreview(request)}
|
|
1037
|
+
`
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
const capture = new BoundedCapture(request.config.input.maxCaptureChars);
|
|
1041
|
+
let promptWindow = "";
|
|
1042
|
+
let bypassed = false;
|
|
1043
|
+
let childStatus = null;
|
|
1044
|
+
let childSignal = null;
|
|
1045
|
+
let childSpawnError = null;
|
|
1046
|
+
const child = hasShellCommand ? spawn(shellPath, ["-lc", request.shellCommand], {
|
|
1047
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
1048
|
+
}) : spawn(request.command[0], request.command.slice(1), {
|
|
1049
|
+
stdio: ["inherit", "pipe", "pipe"]
|
|
1050
|
+
});
|
|
1051
|
+
const handleChunk = (chunk) => {
|
|
1052
|
+
const text = Buffer.isBuffer(chunk) ? chunk.toString("utf8") : String(chunk);
|
|
1053
|
+
if (bypassed) {
|
|
1054
|
+
process.stderr.write(text);
|
|
1055
|
+
return;
|
|
1056
|
+
}
|
|
1057
|
+
capture.push(text);
|
|
1058
|
+
promptWindow = `${promptWindow}${text}`.slice(-PROMPT_WINDOW_CHARS);
|
|
1059
|
+
if (!looksInteractivePrompt(promptWindow)) {
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
bypassed = true;
|
|
1063
|
+
if (request.config.runtime.verbose) {
|
|
1064
|
+
process.stderr.write(`${pc2.dim("sift")} bypass=interactive-prompt
|
|
1065
|
+
`);
|
|
1066
|
+
}
|
|
1067
|
+
process.stderr.write(capture.render());
|
|
1068
|
+
};
|
|
1069
|
+
child.stdout.on("data", handleChunk);
|
|
1070
|
+
child.stderr.on("data", handleChunk);
|
|
1071
|
+
await new Promise((resolve, reject) => {
|
|
1072
|
+
child.on("error", (error) => {
|
|
1073
|
+
childSpawnError = error;
|
|
1074
|
+
reject(error);
|
|
1075
|
+
});
|
|
1076
|
+
child.on("close", (status, signal) => {
|
|
1077
|
+
childStatus = status;
|
|
1078
|
+
childSignal = signal;
|
|
1079
|
+
resolve();
|
|
1080
|
+
});
|
|
1081
|
+
}).catch((error) => {
|
|
1082
|
+
if (error instanceof Error) {
|
|
1083
|
+
throw error;
|
|
1084
|
+
}
|
|
1085
|
+
throw new Error("Failed to start child process.");
|
|
1086
|
+
});
|
|
1087
|
+
if (childSpawnError) {
|
|
1088
|
+
throw childSpawnError;
|
|
1089
|
+
}
|
|
1090
|
+
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
1091
|
+
if (request.config.runtime.verbose) {
|
|
1092
|
+
process.stderr.write(
|
|
1093
|
+
`${pc2.dim("sift")} child_exit=${exitCode} captured_chars=${capture.getTotalChars()} capture_truncated=${capture.wasTruncated()}
|
|
1094
|
+
`
|
|
1095
|
+
);
|
|
1096
|
+
}
|
|
1097
|
+
if (!bypassed) {
|
|
1098
|
+
const output = await runSift({
|
|
1099
|
+
...request,
|
|
1100
|
+
stdin: capture.render()
|
|
1101
|
+
});
|
|
1102
|
+
process.stdout.write(`${output}
|
|
1103
|
+
`);
|
|
1104
|
+
}
|
|
1105
|
+
return exitCode;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// src/core/stdin.ts
|
|
1109
|
+
async function readStdin() {
|
|
1110
|
+
if (process.stdin.isTTY) {
|
|
1111
|
+
throw new Error("No stdin detected. Pipe command output into sift.");
|
|
1112
|
+
}
|
|
1113
|
+
const chunks = [];
|
|
1114
|
+
for await (const chunk of process.stdin) {
|
|
1115
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
1116
|
+
}
|
|
1117
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// src/prompts/presets.ts
|
|
1121
|
+
function getPreset(config, name) {
|
|
1122
|
+
const preset = config.presets[name];
|
|
1123
|
+
if (!preset) {
|
|
1124
|
+
throw new Error(`Unknown preset: ${name}`);
|
|
1125
|
+
}
|
|
1126
|
+
return preset;
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// src/cli.ts
|
|
1130
|
+
var require2 = createRequire(import.meta.url);
|
|
1131
|
+
var pkg = require2("../package.json");
|
|
1132
|
+
var cli = cac("sift");
|
|
1133
|
+
function toNumber(value) {
|
|
1134
|
+
if (value === void 0 || value === null || value === "") {
|
|
1135
|
+
return void 0;
|
|
1136
|
+
}
|
|
1137
|
+
return Number(value);
|
|
1138
|
+
}
|
|
1139
|
+
function buildCliOverrides(options) {
|
|
1140
|
+
const overrides = {};
|
|
1141
|
+
if (options.provider !== void 0 || options.model !== void 0 || options.baseUrl !== void 0 || options.apiKey !== void 0 || options.timeoutMs !== void 0) {
|
|
1142
|
+
overrides.provider = {
|
|
1143
|
+
provider: options.provider,
|
|
1144
|
+
model: options.model,
|
|
1145
|
+
baseUrl: options.baseUrl,
|
|
1146
|
+
apiKey: options.apiKey,
|
|
1147
|
+
timeoutMs: toNumber(options.timeoutMs)
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
if (options.maxCaptureChars !== void 0 || options.maxInputChars !== void 0 || options.headChars !== void 0 || options.tailChars !== void 0 || options.redact !== void 0 || options.redactStrict !== void 0 || options.stripAnsi !== void 0) {
|
|
1151
|
+
overrides.input = {
|
|
1152
|
+
maxCaptureChars: toNumber(options.maxCaptureChars),
|
|
1153
|
+
maxInputChars: toNumber(options.maxInputChars),
|
|
1154
|
+
headChars: toNumber(options.headChars),
|
|
1155
|
+
tailChars: toNumber(options.tailChars),
|
|
1156
|
+
redact: options.redact,
|
|
1157
|
+
redactStrict: options.redactStrict,
|
|
1158
|
+
stripAnsi: options.stripAnsi
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1161
|
+
if (options.rawFallback !== void 0 || options.verbose !== void 0) {
|
|
1162
|
+
overrides.runtime = {
|
|
1163
|
+
rawFallback: options.rawFallback,
|
|
1164
|
+
verbose: options.verbose
|
|
1165
|
+
};
|
|
1166
|
+
}
|
|
1167
|
+
return overrides;
|
|
1168
|
+
}
|
|
1169
|
+
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("--api-key <key>", "Provider API key").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("--config <path>", "Path to config file").option("--verbose", "Enable verbose stderr logging");
|
|
1171
|
+
}
|
|
1172
|
+
async function executeRun(args) {
|
|
1173
|
+
const config = resolveConfig({
|
|
1174
|
+
configPath: args.options.config,
|
|
1175
|
+
env: process.env,
|
|
1176
|
+
cliOverrides: buildCliOverrides(args.options)
|
|
1177
|
+
});
|
|
1178
|
+
const stdin = await readStdin();
|
|
1179
|
+
const output = await runSift({
|
|
1180
|
+
question: args.question,
|
|
1181
|
+
format: args.format,
|
|
1182
|
+
stdin,
|
|
1183
|
+
config,
|
|
1184
|
+
policyName: args.policyName,
|
|
1185
|
+
outputContract: args.outputContract,
|
|
1186
|
+
fallbackJson: args.fallbackJson
|
|
1187
|
+
});
|
|
1188
|
+
process.stdout.write(`${output}
|
|
1189
|
+
`);
|
|
1190
|
+
}
|
|
1191
|
+
function extractExecCommand(options) {
|
|
1192
|
+
const passthrough = Array.isArray(options["--"]) ? options["--"].map((value) => String(value)) : [];
|
|
1193
|
+
const shellCommand = typeof options.shell === "string" && options.shell.trim().length > 0 ? options.shell : void 0;
|
|
1194
|
+
if (shellCommand && passthrough.length > 0) {
|
|
1195
|
+
throw new Error("Use either --shell <command> or -- <program> [args...], not both.");
|
|
1196
|
+
}
|
|
1197
|
+
if (!shellCommand && passthrough.length === 0) {
|
|
1198
|
+
throw new Error("Missing command to execute.");
|
|
1199
|
+
}
|
|
1200
|
+
return {
|
|
1201
|
+
command: passthrough.length > 0 ? passthrough : void 0,
|
|
1202
|
+
shellCommand
|
|
1203
|
+
};
|
|
1204
|
+
}
|
|
1205
|
+
async function executeExec(args) {
|
|
1206
|
+
const config = resolveConfig({
|
|
1207
|
+
configPath: args.options.config,
|
|
1208
|
+
env: process.env,
|
|
1209
|
+
cliOverrides: buildCliOverrides(args.options)
|
|
1210
|
+
});
|
|
1211
|
+
const command = extractExecCommand(args.options);
|
|
1212
|
+
process.exitCode = await runExec({
|
|
1213
|
+
question: args.question,
|
|
1214
|
+
format: args.format,
|
|
1215
|
+
config,
|
|
1216
|
+
policyName: args.policyName,
|
|
1217
|
+
outputContract: args.outputContract,
|
|
1218
|
+
fallbackJson: args.fallbackJson,
|
|
1219
|
+
...command
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
applySharedOptions(
|
|
1223
|
+
cli.command("preset <name>", "Run a named preset against piped CLI output")
|
|
1224
|
+
).action(async (name, options) => {
|
|
1225
|
+
const config = resolveConfig({
|
|
1226
|
+
configPath: options.config,
|
|
1227
|
+
env: process.env,
|
|
1228
|
+
cliOverrides: buildCliOverrides(options)
|
|
1229
|
+
});
|
|
1230
|
+
const preset = getPreset(config, name);
|
|
1231
|
+
await executeRun({
|
|
1232
|
+
question: preset.question,
|
|
1233
|
+
format: options.format ?? preset.format,
|
|
1234
|
+
policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
|
|
1235
|
+
options,
|
|
1236
|
+
outputContract: preset.outputContract,
|
|
1237
|
+
fallbackJson: preset.fallbackJson
|
|
1238
|
+
});
|
|
1239
|
+
});
|
|
1240
|
+
applySharedOptions(
|
|
1241
|
+
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) => {
|
|
1243
|
+
if (question === "preset") {
|
|
1244
|
+
throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
|
|
1245
|
+
}
|
|
1246
|
+
const presetName = typeof options.preset === "string" && options.preset.length > 0 ? options.preset : void 0;
|
|
1247
|
+
if (presetName) {
|
|
1248
|
+
if (question) {
|
|
1249
|
+
throw new Error("Use either a freeform question or --preset <name>, not both.");
|
|
1250
|
+
}
|
|
1251
|
+
const preset = getPreset(
|
|
1252
|
+
resolveConfig({
|
|
1253
|
+
configPath: options.config,
|
|
1254
|
+
env: process.env,
|
|
1255
|
+
cliOverrides: buildCliOverrides(options)
|
|
1256
|
+
}),
|
|
1257
|
+
presetName
|
|
1258
|
+
);
|
|
1259
|
+
await executeExec({
|
|
1260
|
+
question: preset.question,
|
|
1261
|
+
format: options.format ?? preset.format,
|
|
1262
|
+
policyName: options.format === void 0 || options.format === preset.format ? preset.policy : void 0,
|
|
1263
|
+
options,
|
|
1264
|
+
outputContract: preset.outputContract,
|
|
1265
|
+
fallbackJson: preset.fallbackJson
|
|
1266
|
+
});
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (!question) {
|
|
1270
|
+
throw new Error("Missing question or preset.");
|
|
1271
|
+
}
|
|
1272
|
+
const format = options.format ?? "brief";
|
|
1273
|
+
await executeExec({
|
|
1274
|
+
question,
|
|
1275
|
+
format,
|
|
1276
|
+
options
|
|
1277
|
+
});
|
|
1278
|
+
});
|
|
1279
|
+
cli.command("config <action>", "Config commands: init | show | validate").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
|
+
if (action === "init") {
|
|
1281
|
+
configInit(options.path);
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
if (action === "show") {
|
|
1285
|
+
configShow(
|
|
1286
|
+
options.config,
|
|
1287
|
+
Boolean(options.showSecrets)
|
|
1288
|
+
);
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
if (action === "validate") {
|
|
1292
|
+
configValidate(options.config);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
throw new Error(`Unknown config action: ${action}`);
|
|
1296
|
+
});
|
|
1297
|
+
cli.command("doctor", "Validate runtime configuration").option("--config <path>", "Path to config file").action((options) => {
|
|
1298
|
+
const config = resolveConfig({
|
|
1299
|
+
configPath: options.config,
|
|
1300
|
+
env: process.env
|
|
1301
|
+
});
|
|
1302
|
+
process.exitCode = runDoctor(config);
|
|
1303
|
+
});
|
|
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) => {
|
|
1305
|
+
const config = resolveConfig({
|
|
1306
|
+
configPath: options.config,
|
|
1307
|
+
env: process.env
|
|
1308
|
+
});
|
|
1309
|
+
if (action === "list") {
|
|
1310
|
+
listPresets(config);
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (action === "show") {
|
|
1314
|
+
if (!name) {
|
|
1315
|
+
throw new Error("Missing preset name.");
|
|
1316
|
+
}
|
|
1317
|
+
showPreset(config, name, Boolean(options.internal));
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
throw new Error(`Unknown presets action: ${action}`);
|
|
1321
|
+
});
|
|
1322
|
+
applySharedOptions(
|
|
1323
|
+
cli.command("[question]", "Ask a freeform question about piped CLI output")
|
|
1324
|
+
).action(async (question, options) => {
|
|
1325
|
+
if (!question) {
|
|
1326
|
+
throw new Error("Missing question.");
|
|
1327
|
+
}
|
|
1328
|
+
const format = options.format ?? "brief";
|
|
1329
|
+
await executeRun({
|
|
1330
|
+
question,
|
|
1331
|
+
format,
|
|
1332
|
+
options
|
|
1333
|
+
});
|
|
1334
|
+
});
|
|
1335
|
+
cli.help();
|
|
1336
|
+
cli.version(pkg.version);
|
|
1337
|
+
async function main() {
|
|
1338
|
+
cli.parse(process.argv, { run: false });
|
|
1339
|
+
await cli.runMatchedCommand();
|
|
1340
|
+
}
|
|
1341
|
+
main().catch((error) => {
|
|
1342
|
+
const message = error instanceof Error ? error.message : "Unexpected error.";
|
|
1343
|
+
process.stderr.write(`${message}
|
|
1344
|
+
`);
|
|
1345
|
+
process.exitCode = 1;
|
|
1346
|
+
});
|