@bilalimamoglu/sift 0.4.4 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -21
- package/dist/cli.js +1266 -122
- package/dist/index.d.ts +11 -2
- package/dist/index.js +167 -13
- package/package.json +13 -3
package/dist/cli.js
CHANGED
|
@@ -109,6 +109,7 @@ var defaultConfig = {
|
|
|
109
109
|
tailChars: 2e4
|
|
110
110
|
},
|
|
111
111
|
runtime: {
|
|
112
|
+
operationMode: "agent-escalation",
|
|
112
113
|
rawFallback: true,
|
|
113
114
|
verbose: false
|
|
114
115
|
},
|
|
@@ -158,12 +159,62 @@ var defaultConfig = {
|
|
|
158
159
|
}
|
|
159
160
|
};
|
|
160
161
|
|
|
162
|
+
// src/config/provider-models.ts
|
|
163
|
+
var OPENAI_MODELS = [
|
|
164
|
+
{
|
|
165
|
+
model: "gpt-5-nano",
|
|
166
|
+
label: "gpt-5-nano",
|
|
167
|
+
note: "default, cheapest, fast enough for most fallback passes",
|
|
168
|
+
isDefault: true
|
|
169
|
+
},
|
|
170
|
+
{
|
|
171
|
+
model: "gpt-5.4-nano",
|
|
172
|
+
label: "gpt-5.4-nano",
|
|
173
|
+
note: "newer nano backup, a touch smarter, a touch pricier"
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
model: "gpt-5-mini",
|
|
177
|
+
label: "gpt-5-mini",
|
|
178
|
+
note: "smarter fallback, still saner than the expensive stuff"
|
|
179
|
+
}
|
|
180
|
+
];
|
|
181
|
+
var OPENROUTER_MODELS = [
|
|
182
|
+
{
|
|
183
|
+
model: "openrouter/free",
|
|
184
|
+
label: "openrouter/free",
|
|
185
|
+
note: "default, free, a little slower sometimes, still hard to argue with free",
|
|
186
|
+
isDefault: true
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
model: "qwen/qwen3-coder:free",
|
|
190
|
+
label: "qwen/qwen3-coder:free",
|
|
191
|
+
note: "free, code-focused, good when you want a named coding fallback"
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
model: "deepseek/deepseek-r1:free",
|
|
195
|
+
label: "deepseek/deepseek-r1:free",
|
|
196
|
+
note: "free, stronger reasoning, usually slower"
|
|
197
|
+
}
|
|
198
|
+
];
|
|
199
|
+
function getProviderModelOptions(provider) {
|
|
200
|
+
return provider === "openrouter" ? OPENROUTER_MODELS : OPENAI_MODELS;
|
|
201
|
+
}
|
|
202
|
+
function getDefaultProviderModel(provider) {
|
|
203
|
+
return getProviderModelOptions(provider).find((option) => option.isDefault)?.model ?? getProviderModelOptions(provider)[0]?.model ?? "";
|
|
204
|
+
}
|
|
205
|
+
function findProviderModelOption(provider, model) {
|
|
206
|
+
if (!model) {
|
|
207
|
+
return void 0;
|
|
208
|
+
}
|
|
209
|
+
return getProviderModelOptions(provider).find((option) => option.model === model);
|
|
210
|
+
}
|
|
211
|
+
|
|
161
212
|
// src/config/native-provider.ts
|
|
162
213
|
function getNativeProviderDefaults(provider) {
|
|
163
214
|
if (provider === "openrouter") {
|
|
164
215
|
return {
|
|
165
216
|
provider,
|
|
166
|
-
model: "openrouter
|
|
217
|
+
model: getDefaultProviderModel("openrouter"),
|
|
167
218
|
baseUrl: "https://openrouter.ai/api/v1"
|
|
168
219
|
};
|
|
169
220
|
}
|
|
@@ -305,6 +356,11 @@ function getProviderApiKeyEnvNames(provider, baseUrl) {
|
|
|
305
356
|
|
|
306
357
|
// src/config/schema.ts
|
|
307
358
|
import { z } from "zod";
|
|
359
|
+
var operationModeSchema = z.enum([
|
|
360
|
+
"agent-escalation",
|
|
361
|
+
"provider-assisted",
|
|
362
|
+
"local-only"
|
|
363
|
+
]);
|
|
308
364
|
var providerNameSchema = z.enum([
|
|
309
365
|
"openai",
|
|
310
366
|
"openai-compatible",
|
|
@@ -357,6 +413,7 @@ var inputConfigSchema = z.object({
|
|
|
357
413
|
tailChars: z.number().int().positive()
|
|
358
414
|
});
|
|
359
415
|
var runtimeConfigSchema = z.object({
|
|
416
|
+
operationMode: operationModeSchema,
|
|
360
417
|
rawFallback: z.boolean(),
|
|
361
418
|
verbose: z.boolean()
|
|
362
419
|
});
|
|
@@ -379,7 +436,7 @@ var siftConfigSchema = z.object({
|
|
|
379
436
|
var PROVIDER_DEFAULT_OVERRIDES = {
|
|
380
437
|
openrouter: {
|
|
381
438
|
provider: {
|
|
382
|
-
model: "openrouter
|
|
439
|
+
model: getDefaultProviderModel("openrouter"),
|
|
383
440
|
baseUrl: "https://openrouter.ai/api/v1"
|
|
384
441
|
}
|
|
385
442
|
}
|
|
@@ -419,13 +476,16 @@ function stripApiKey(overrides) {
|
|
|
419
476
|
}
|
|
420
477
|
function buildNonCredentialEnvOverrides(env) {
|
|
421
478
|
const overrides = {};
|
|
422
|
-
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS) {
|
|
479
|
+
if (env.SIFT_PROVIDER || env.SIFT_MODEL || env.SIFT_BASE_URL || env.SIFT_TIMEOUT_MS || env.SIFT_OPERATION_MODE) {
|
|
423
480
|
overrides.provider = {
|
|
424
481
|
provider: env.SIFT_PROVIDER,
|
|
425
482
|
model: env.SIFT_MODEL,
|
|
426
483
|
baseUrl: env.SIFT_BASE_URL,
|
|
427
484
|
timeoutMs: env.SIFT_TIMEOUT_MS ? Number(env.SIFT_TIMEOUT_MS) : void 0
|
|
428
485
|
};
|
|
486
|
+
overrides.runtime = {
|
|
487
|
+
operationMode: env.SIFT_OPERATION_MODE
|
|
488
|
+
};
|
|
429
489
|
}
|
|
430
490
|
if (env.SIFT_MAX_INPUT_CHARS || env.SIFT_MAX_CAPTURE_CHARS) {
|
|
431
491
|
overrides.input = {
|
|
@@ -492,6 +552,15 @@ function resolveConfig(options = {}) {
|
|
|
492
552
|
);
|
|
493
553
|
return siftConfigSchema.parse(merged);
|
|
494
554
|
}
|
|
555
|
+
function hasUsableProvider(config) {
|
|
556
|
+
return config.provider.apiKey !== void 0 && config.provider.apiKey.trim().length > 0;
|
|
557
|
+
}
|
|
558
|
+
function resolveEffectiveOperationMode(config) {
|
|
559
|
+
if (config.runtime.operationMode === "provider-assisted") {
|
|
560
|
+
return hasUsableProvider(config) ? "provider-assisted" : "agent-escalation";
|
|
561
|
+
}
|
|
562
|
+
return config.runtime.operationMode;
|
|
563
|
+
}
|
|
495
564
|
|
|
496
565
|
// src/config/write.ts
|
|
497
566
|
import fs3 from "fs";
|
|
@@ -616,10 +685,129 @@ import { emitKeypressEvents } from "readline";
|
|
|
616
685
|
import { createInterface } from "readline/promises";
|
|
617
686
|
import { stderr as defaultStderr, stdin as defaultStdin2, stdout as defaultStdout } from "process";
|
|
618
687
|
|
|
688
|
+
// src/config/operation-mode.ts
|
|
689
|
+
function getOperationModeLabel(mode) {
|
|
690
|
+
switch (mode) {
|
|
691
|
+
case "agent-escalation":
|
|
692
|
+
return "Agent escalation";
|
|
693
|
+
case "provider-assisted":
|
|
694
|
+
return "Provider-assisted";
|
|
695
|
+
case "local-only":
|
|
696
|
+
return "Local-only";
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
function describeOperationMode(mode) {
|
|
700
|
+
switch (mode) {
|
|
701
|
+
case "agent-escalation":
|
|
702
|
+
return "Best when you already have an agent open. sift does the quick first pass, then the agent can read code, tests, or logs and keep going.";
|
|
703
|
+
case "provider-assisted":
|
|
704
|
+
return "Best when you want sift itself to take one more cheap swing at the problem before you fall back to an agent or raw logs. Built-in rules first, API-backed backup second.";
|
|
705
|
+
case "local-only":
|
|
706
|
+
return "Best when you are not pairing sift with another agent and do not want API keys. Everything stays local.";
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
function describeInsufficientBehavior(mode) {
|
|
710
|
+
switch (mode) {
|
|
711
|
+
case "agent-escalation":
|
|
712
|
+
return "If sift is still not enough, that is the handoff point: log it, narrow the problem, and let the agent keep digging.";
|
|
713
|
+
case "provider-assisted":
|
|
714
|
+
return "If the first pass is still fuzzy, sift can ask the fallback model so you do not have to escalate every annoying edge case by hand.";
|
|
715
|
+
case "local-only":
|
|
716
|
+
return "If sift is still not enough, stay local: rerun something narrower, use a better preset, or read the relevant source and tests yourself.";
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
619
720
|
// src/ui/terminal.ts
|
|
620
721
|
import { execFileSync } from "child_process";
|
|
621
722
|
import { clearScreenDown, cursorTo, moveCursor } from "readline";
|
|
622
723
|
import { stdin as defaultStdin } from "process";
|
|
724
|
+
var PROMPT_BACK = "__sift_back__";
|
|
725
|
+
var PROMPT_BACK_LABEL = "\u2190 Back";
|
|
726
|
+
function color(text, rgb, args = {}) {
|
|
727
|
+
const codes = [];
|
|
728
|
+
if (args.bold) {
|
|
729
|
+
codes.push("1");
|
|
730
|
+
}
|
|
731
|
+
if (args.dim) {
|
|
732
|
+
codes.push("2");
|
|
733
|
+
}
|
|
734
|
+
codes.push(`38;2;${rgb[0]};${rgb[1]};${rgb[2]}`);
|
|
735
|
+
return `\x1B[${codes.join(";")}m${text}\x1B[0m`;
|
|
736
|
+
}
|
|
737
|
+
function splitOptionLeading(option) {
|
|
738
|
+
const boundaries = [" - ", ": ", ":", " ("].map((token) => {
|
|
739
|
+
const index = option.indexOf(token);
|
|
740
|
+
return index >= 0 ? { index, token } : void 0;
|
|
741
|
+
}).filter((entry) => Boolean(entry)).sort((left, right) => left.index - right.index);
|
|
742
|
+
const boundary = boundaries[0];
|
|
743
|
+
if (!boundary) {
|
|
744
|
+
return { leading: option, trailing: "" };
|
|
745
|
+
}
|
|
746
|
+
return {
|
|
747
|
+
leading: option.slice(0, boundary.index),
|
|
748
|
+
trailing: option.slice(boundary.index)
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
function getOptionPalette(leading) {
|
|
752
|
+
const normalized = leading.trim();
|
|
753
|
+
if (normalized.startsWith("With an agent")) {
|
|
754
|
+
return { rgb: [214, 168, 76] };
|
|
755
|
+
}
|
|
756
|
+
if (normalized.startsWith("With provider fallback")) {
|
|
757
|
+
return { rgb: [100, 141, 214] };
|
|
758
|
+
}
|
|
759
|
+
if (normalized.startsWith("Solo, local-only")) {
|
|
760
|
+
return { rgb: [122, 142, 116], dimWhenIdle: true };
|
|
761
|
+
}
|
|
762
|
+
if (normalized.startsWith("Codex")) {
|
|
763
|
+
return { rgb: [233, 183, 78] };
|
|
764
|
+
}
|
|
765
|
+
if (normalized.startsWith("Claude")) {
|
|
766
|
+
return { rgb: [171, 138, 224] };
|
|
767
|
+
}
|
|
768
|
+
if (normalized === "All") {
|
|
769
|
+
return { rgb: [95, 181, 201] };
|
|
770
|
+
}
|
|
771
|
+
if (normalized.startsWith("Global")) {
|
|
772
|
+
return { rgb: [205, 168, 83] };
|
|
773
|
+
}
|
|
774
|
+
if (normalized.startsWith("Local")) {
|
|
775
|
+
return { rgb: [138, 144, 150], dimWhenIdle: true };
|
|
776
|
+
}
|
|
777
|
+
if (normalized.startsWith("OpenAI")) {
|
|
778
|
+
return { rgb: [82, 177, 124] };
|
|
779
|
+
}
|
|
780
|
+
if (normalized.startsWith("OpenRouter")) {
|
|
781
|
+
return { rgb: [106, 144, 221] };
|
|
782
|
+
}
|
|
783
|
+
if (normalized.startsWith("Use saved key") || normalized.startsWith("Use existing key")) {
|
|
784
|
+
return { rgb: [111, 181, 123] };
|
|
785
|
+
}
|
|
786
|
+
if (normalized.startsWith("Use environment key")) {
|
|
787
|
+
return { rgb: [102, 146, 219] };
|
|
788
|
+
}
|
|
789
|
+
if (normalized.startsWith("Enter a different key") || normalized.startsWith("Custom model")) {
|
|
790
|
+
return { rgb: [191, 157, 92], dimWhenIdle: true };
|
|
791
|
+
}
|
|
792
|
+
return void 0;
|
|
793
|
+
}
|
|
794
|
+
function styleOption(option, selected, colorize) {
|
|
795
|
+
if (!colorize) {
|
|
796
|
+
return option;
|
|
797
|
+
}
|
|
798
|
+
if (option === PROMPT_BACK_LABEL) {
|
|
799
|
+
return color(option, [164, 169, 178], { bold: selected, dim: !selected });
|
|
800
|
+
}
|
|
801
|
+
const { leading, trailing } = splitOptionLeading(option);
|
|
802
|
+
const palette = getOptionPalette(leading);
|
|
803
|
+
if (!palette) {
|
|
804
|
+
return option;
|
|
805
|
+
}
|
|
806
|
+
return `${color(leading, palette.rgb, {
|
|
807
|
+
bold: selected,
|
|
808
|
+
dim: !selected && Boolean(palette.dimWhenIdle)
|
|
809
|
+
})}${trailing}`;
|
|
810
|
+
}
|
|
623
811
|
function setPosixEcho(enabled) {
|
|
624
812
|
const command = enabled ? "echo" : "-echo";
|
|
625
813
|
try {
|
|
@@ -637,10 +825,15 @@ function setPosixEcho(enabled) {
|
|
|
637
825
|
}
|
|
638
826
|
}
|
|
639
827
|
function renderSelectionBlock(args) {
|
|
828
|
+
const options = args.allowBack ? [...args.options, args.backLabel ?? PROMPT_BACK_LABEL] : args.options;
|
|
640
829
|
return [
|
|
641
|
-
`${args.prompt} (use \u2191/\u2193 and Enter)`,
|
|
642
|
-
...
|
|
643
|
-
(option, index) => `${index === args.selectedIndex ? "\u203A" : " "} ${
|
|
830
|
+
`${args.prompt}${args.allowBack ? " (use \u2191/\u2193 to move, Enter to select, Esc to go back)" : " (use \u2191/\u2193 and Enter)"}`,
|
|
831
|
+
...options.map(
|
|
832
|
+
(option, index) => `${index === args.selectedIndex ? "\u203A" : " "} ${styleOption(
|
|
833
|
+
option,
|
|
834
|
+
index === args.selectedIndex,
|
|
835
|
+
Boolean(args.colorize)
|
|
836
|
+
)}${index === args.selectedIndex ? " (selected)" : ""}`
|
|
644
837
|
)
|
|
645
838
|
];
|
|
646
839
|
}
|
|
@@ -648,6 +841,8 @@ async function promptSelect(args) {
|
|
|
648
841
|
const { input, output, prompt, options } = args;
|
|
649
842
|
const stream = output;
|
|
650
843
|
const selectedLabel = args.selectedLabel ?? prompt;
|
|
844
|
+
const backLabel = args.backLabel ?? PROMPT_BACK_LABEL;
|
|
845
|
+
const allOptions = args.allowBack ? [...options, backLabel] : options;
|
|
651
846
|
let index = 0;
|
|
652
847
|
let previousLineCount = 0;
|
|
653
848
|
const render = () => {
|
|
@@ -659,7 +854,10 @@ async function promptSelect(args) {
|
|
|
659
854
|
const lines = renderSelectionBlock({
|
|
660
855
|
prompt,
|
|
661
856
|
options,
|
|
662
|
-
selectedIndex: index
|
|
857
|
+
selectedIndex: index,
|
|
858
|
+
allowBack: args.allowBack,
|
|
859
|
+
backLabel,
|
|
860
|
+
colorize: Boolean(stream?.isTTY)
|
|
663
861
|
});
|
|
664
862
|
output.write(`${lines.join("\n")}
|
|
665
863
|
`);
|
|
@@ -691,22 +889,30 @@ async function promptSelect(args) {
|
|
|
691
889
|
return;
|
|
692
890
|
}
|
|
693
891
|
if (key.name === "up") {
|
|
694
|
-
index = index === 0 ?
|
|
892
|
+
index = index === 0 ? allOptions.length - 1 : index - 1;
|
|
695
893
|
render();
|
|
696
894
|
return;
|
|
697
895
|
}
|
|
698
896
|
if (key.name === "down") {
|
|
699
|
-
index = (index + 1) %
|
|
897
|
+
index = (index + 1) % allOptions.length;
|
|
700
898
|
render();
|
|
701
899
|
return;
|
|
702
900
|
}
|
|
901
|
+
if (args.allowBack && key.name === "escape") {
|
|
902
|
+
input.off("keypress", onKeypress);
|
|
903
|
+
cleanup();
|
|
904
|
+
input.setRawMode?.(wasRaw);
|
|
905
|
+
input.pause?.();
|
|
906
|
+
resolve(PROMPT_BACK);
|
|
907
|
+
return;
|
|
908
|
+
}
|
|
703
909
|
if (key.name === "return" || key.name === "enter") {
|
|
704
|
-
const selected =
|
|
910
|
+
const selected = allOptions[index] ?? allOptions[0] ?? "";
|
|
705
911
|
input.off("keypress", onKeypress);
|
|
706
|
-
cleanup(selected);
|
|
912
|
+
cleanup(selected === backLabel ? void 0 : selected);
|
|
707
913
|
input.setRawMode?.(wasRaw);
|
|
708
914
|
input.pause?.();
|
|
709
|
-
resolve(selected);
|
|
915
|
+
resolve(selected === backLabel ? PROMPT_BACK : selected);
|
|
710
916
|
}
|
|
711
917
|
};
|
|
712
918
|
input.on("keypress", onKeypress);
|
|
@@ -739,6 +945,13 @@ async function promptSecret(args) {
|
|
|
739
945
|
reject(new Error("Aborted."));
|
|
740
946
|
return;
|
|
741
947
|
}
|
|
948
|
+
if (args.allowBack && key.name === "escape") {
|
|
949
|
+
input.off("keypress", onKeypress);
|
|
950
|
+
restoreInputState();
|
|
951
|
+
output.write("\n");
|
|
952
|
+
resolve(PROMPT_BACK);
|
|
953
|
+
return;
|
|
954
|
+
}
|
|
742
955
|
if (key.name === "return" || key.name === "enter") {
|
|
743
956
|
input.off("keypress", onKeypress);
|
|
744
957
|
restoreInputState();
|
|
@@ -759,6 +972,7 @@ async function promptSecret(args) {
|
|
|
759
972
|
}
|
|
760
973
|
|
|
761
974
|
// src/commands/config-setup.ts
|
|
975
|
+
var CONFIG_SETUP_BACK = 2;
|
|
762
976
|
function createTerminalIO() {
|
|
763
977
|
let rl;
|
|
764
978
|
function getInterface() {
|
|
@@ -771,22 +985,24 @@ function createTerminalIO() {
|
|
|
771
985
|
}
|
|
772
986
|
return rl;
|
|
773
987
|
}
|
|
774
|
-
async function select(prompt, options) {
|
|
988
|
+
async function select(prompt, options, selectedLabel, allowBack) {
|
|
775
989
|
emitKeypressEvents(defaultStdin2);
|
|
776
990
|
return await promptSelect({
|
|
777
991
|
input: defaultStdin2,
|
|
778
992
|
output: defaultStdout,
|
|
779
993
|
prompt,
|
|
780
994
|
options,
|
|
781
|
-
selectedLabel
|
|
995
|
+
selectedLabel,
|
|
996
|
+
allowBack
|
|
782
997
|
});
|
|
783
998
|
}
|
|
784
|
-
async function secret(prompt) {
|
|
999
|
+
async function secret(prompt, allowBack) {
|
|
785
1000
|
emitKeypressEvents(defaultStdin2);
|
|
786
1001
|
return await promptSecret({
|
|
787
1002
|
input: defaultStdin2,
|
|
788
1003
|
output: defaultStdout,
|
|
789
|
-
prompt
|
|
1004
|
+
prompt,
|
|
1005
|
+
allowBack
|
|
790
1006
|
});
|
|
791
1007
|
}
|
|
792
1008
|
return {
|
|
@@ -817,12 +1033,58 @@ function getSetupPresenter(io) {
|
|
|
817
1033
|
function getProviderLabel(provider) {
|
|
818
1034
|
return provider === "openrouter" ? "OpenRouter" : "OpenAI";
|
|
819
1035
|
}
|
|
1036
|
+
function isBackSelection(value) {
|
|
1037
|
+
return value === PROMPT_BACK || value === PROMPT_BACK_LABEL;
|
|
1038
|
+
}
|
|
1039
|
+
async function promptForOperationMode(io) {
|
|
1040
|
+
if (io.select) {
|
|
1041
|
+
const choice = await io.select(
|
|
1042
|
+
"Choose how sift should work",
|
|
1043
|
+
[
|
|
1044
|
+
"With an agent: recommended if Codex or Claude is already with you; sift does the fast local first pass, the agent only steps in when repo context is truly needed",
|
|
1045
|
+
"With provider fallback: recommended if you want sift to finish more ambiguous cases on its own before handing them back to you or your agent; requires an API key, cheap model only when needed",
|
|
1046
|
+
"Solo, local-only: recommended if you want zero model calls; great for supported presets, ambiguous cases stay with you"
|
|
1047
|
+
],
|
|
1048
|
+
"Mode"
|
|
1049
|
+
);
|
|
1050
|
+
if (choice.startsWith("With an agent")) {
|
|
1051
|
+
return "agent-escalation";
|
|
1052
|
+
}
|
|
1053
|
+
if (choice.startsWith("With provider fallback")) {
|
|
1054
|
+
return "provider-assisted";
|
|
1055
|
+
}
|
|
1056
|
+
if (choice.startsWith("Solo, local-only")) {
|
|
1057
|
+
return "local-only";
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
while (true) {
|
|
1061
|
+
const answer = (await io.ask("Use style [agent/provider/local]: ")).trim().toLowerCase();
|
|
1062
|
+
if (answer === "" || answer === "agent" || answer === "agent-escalation") {
|
|
1063
|
+
return "agent-escalation";
|
|
1064
|
+
}
|
|
1065
|
+
if (answer === "provider" || answer === "provider-assisted") {
|
|
1066
|
+
return "provider-assisted";
|
|
1067
|
+
}
|
|
1068
|
+
if (answer === "local" || answer === "local-only") {
|
|
1069
|
+
return "local-only";
|
|
1070
|
+
}
|
|
1071
|
+
io.error("Please answer agent, provider, or local.\n");
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
820
1074
|
async function promptForProvider(io) {
|
|
821
1075
|
if (io.select) {
|
|
822
|
-
const choice = await io.select(
|
|
823
|
-
"
|
|
824
|
-
|
|
825
|
-
|
|
1076
|
+
const choice = await io.select(
|
|
1077
|
+
"Okay, whose API key are we borrowing for fallback duty?",
|
|
1078
|
+
[
|
|
1079
|
+
"OpenAI",
|
|
1080
|
+
"OpenRouter"
|
|
1081
|
+
],
|
|
1082
|
+
"Provider",
|
|
1083
|
+
true
|
|
1084
|
+
);
|
|
1085
|
+
if (isBackSelection(choice)) {
|
|
1086
|
+
return PROMPT_BACK;
|
|
1087
|
+
}
|
|
826
1088
|
if (choice === "OpenAI") {
|
|
827
1089
|
return "openai";
|
|
828
1090
|
}
|
|
@@ -832,6 +1094,9 @@ async function promptForProvider(io) {
|
|
|
832
1094
|
}
|
|
833
1095
|
while (true) {
|
|
834
1096
|
const answer = (await io.ask("Provider [OpenAI/OpenRouter]: ")).trim().toLowerCase();
|
|
1097
|
+
if (answer === "back" || answer === "b") {
|
|
1098
|
+
return PROMPT_BACK;
|
|
1099
|
+
}
|
|
835
1100
|
if (answer === "" || answer === "openai") {
|
|
836
1101
|
return "openai";
|
|
837
1102
|
}
|
|
@@ -846,7 +1111,13 @@ async function promptForApiKey(io, provider) {
|
|
|
846
1111
|
const promptText = `Enter your ${providerLabel} API key (input hidden): `;
|
|
847
1112
|
const visiblePromptText = `Enter your ${providerLabel} API key: `;
|
|
848
1113
|
while (true) {
|
|
849
|
-
const answer = (await (io.secret ? io.secret(promptText) : io.ask(visiblePromptText))).trim();
|
|
1114
|
+
const answer = (await (io.secret ? io.secret(promptText, true) : io.ask(visiblePromptText))).trim();
|
|
1115
|
+
if (answer === PROMPT_BACK) {
|
|
1116
|
+
return PROMPT_BACK;
|
|
1117
|
+
}
|
|
1118
|
+
if (!io.secret && (answer.toLowerCase() === "back" || answer.toLowerCase() === "b")) {
|
|
1119
|
+
return PROMPT_BACK;
|
|
1120
|
+
}
|
|
850
1121
|
if (answer.length > 0) {
|
|
851
1122
|
return answer;
|
|
852
1123
|
}
|
|
@@ -862,17 +1133,25 @@ async function promptForApiKeyChoice(args) {
|
|
|
862
1133
|
if (args.io.select) {
|
|
863
1134
|
const choice = await args.io.select(
|
|
864
1135
|
`Found both a saved ${providerLabel} API key and ${args.envName} in your environment`,
|
|
865
|
-
["Use saved key", "Use
|
|
1136
|
+
["Use saved key", "Use environment key", "Enter a different key"],
|
|
1137
|
+
"API key",
|
|
1138
|
+
true
|
|
866
1139
|
);
|
|
1140
|
+
if (isBackSelection(choice)) {
|
|
1141
|
+
return PROMPT_BACK;
|
|
1142
|
+
}
|
|
867
1143
|
if (choice === "Use saved key") {
|
|
868
1144
|
return "saved";
|
|
869
1145
|
}
|
|
870
|
-
if (choice === "Use
|
|
1146
|
+
if (choice === "Use environment key") {
|
|
871
1147
|
return "env";
|
|
872
1148
|
}
|
|
873
1149
|
}
|
|
874
1150
|
while (true) {
|
|
875
1151
|
const answer = (await args.io.ask("API key choice [saved/env/override]: ")).trim().toLowerCase();
|
|
1152
|
+
if (answer === "back" || answer === "b") {
|
|
1153
|
+
return PROMPT_BACK;
|
|
1154
|
+
}
|
|
876
1155
|
if (answer === "" || answer === "saved") {
|
|
877
1156
|
return "saved";
|
|
878
1157
|
}
|
|
@@ -889,15 +1168,23 @@ async function promptForApiKeyChoice(args) {
|
|
|
889
1168
|
if (args.io.select) {
|
|
890
1169
|
const choice = await args.io.select(
|
|
891
1170
|
`Found an existing ${providerLabel} API key via ${sourceLabel}`,
|
|
892
|
-
["Use
|
|
1171
|
+
["Use saved key", "Enter a different key"],
|
|
1172
|
+
"API key",
|
|
1173
|
+
true
|
|
893
1174
|
);
|
|
894
|
-
if (choice
|
|
1175
|
+
if (isBackSelection(choice)) {
|
|
1176
|
+
return PROMPT_BACK;
|
|
1177
|
+
}
|
|
1178
|
+
if (choice === "Enter a different key") {
|
|
895
1179
|
return "override";
|
|
896
1180
|
}
|
|
897
1181
|
return args.hasSavedKey ? "saved" : "env";
|
|
898
1182
|
}
|
|
899
1183
|
while (true) {
|
|
900
1184
|
const answer = (await args.io.ask("API key choice [existing/override]: ")).trim().toLowerCase();
|
|
1185
|
+
if (answer === "back" || answer === "b") {
|
|
1186
|
+
return PROMPT_BACK;
|
|
1187
|
+
}
|
|
901
1188
|
if (answer === "" || answer === "existing") {
|
|
902
1189
|
return args.hasSavedKey ? "saved" : "env";
|
|
903
1190
|
}
|
|
@@ -907,12 +1194,41 @@ async function promptForApiKeyChoice(args) {
|
|
|
907
1194
|
args.io.error("Please answer existing or override.\n");
|
|
908
1195
|
}
|
|
909
1196
|
}
|
|
910
|
-
function
|
|
1197
|
+
function writeModeSummary(io, mode) {
|
|
1198
|
+
const ui = getSetupPresenter(io);
|
|
1199
|
+
io.write(`${ui.info(`Operating mode: ${getOperationModeLabel(mode)}`)}
|
|
1200
|
+
`);
|
|
1201
|
+
io.write(`${ui.note(describeOperationMode(mode))}
|
|
1202
|
+
`);
|
|
1203
|
+
io.write(`${ui.note(describeInsufficientBehavior(mode))}
|
|
1204
|
+
`);
|
|
1205
|
+
if (mode === "agent-escalation") {
|
|
1206
|
+
io.write(
|
|
1207
|
+
`${ui.note("Plain English: pick this if you already use Codex or Claude. sift gives the first answer; the agent only steps in when deeper repo context is really needed. No API key.")}
|
|
1208
|
+
`
|
|
1209
|
+
);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1212
|
+
if (mode === "provider-assisted") {
|
|
1213
|
+
io.write(
|
|
1214
|
+
`${ui.note("Plain English: pick this if you want sift itself to finish more fuzzy cases before you have to step in or re-prompt an agent. Yes, that means an API key, but the model is intentionally the cheap backup, not the fancy main act.")}
|
|
1215
|
+
`
|
|
1216
|
+
);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
io.write(
|
|
1220
|
+
`${ui.note("Plain English: pick this if sift is working alone. No API key, no model fallback. If the answer is still fuzzy, you inspect the code or logs yourself.")}
|
|
1221
|
+
`
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
function writeSetupSuccess(io, writtenPath, mode) {
|
|
911
1225
|
const ui = getSetupPresenter(io);
|
|
912
1226
|
io.write(`
|
|
913
1227
|
${ui.success("You're set.")}
|
|
914
1228
|
`);
|
|
915
1229
|
io.write(`${ui.info(`Machine-wide config: ${writtenPath}`)}
|
|
1230
|
+
`);
|
|
1231
|
+
io.write(`${ui.labelValue("operation mode", getOperationModeLabel(mode))}
|
|
916
1232
|
`);
|
|
917
1233
|
io.write(`${ui.note("sift is ready to use from any terminal on this machine.")}
|
|
918
1234
|
`);
|
|
@@ -928,31 +1244,114 @@ function writeOverrideWarning(io, activeConfigPath) {
|
|
|
928
1244
|
`
|
|
929
1245
|
);
|
|
930
1246
|
}
|
|
931
|
-
function writeNextSteps(io) {
|
|
1247
|
+
function writeNextSteps(io, mode) {
|
|
932
1248
|
const ui = getSetupPresenter(io);
|
|
933
1249
|
io.write(`
|
|
934
1250
|
${ui.section("Try next")}
|
|
935
1251
|
`);
|
|
936
1252
|
io.write(` ${ui.command("sift doctor")}
|
|
937
1253
|
`);
|
|
1254
|
+
if (mode === "provider-assisted") {
|
|
1255
|
+
io.write(` ${ui.command("sift config show --show-secrets")}
|
|
1256
|
+
`);
|
|
1257
|
+
} else {
|
|
1258
|
+
io.write(
|
|
1259
|
+
` ${ui.command("sift config show")}${ui.note(" # rerun setup later if you want provider-assisted fallback")}
|
|
1260
|
+
`
|
|
1261
|
+
);
|
|
1262
|
+
}
|
|
938
1263
|
io.write(` ${ui.command("sift exec --preset test-status -- npm test")}
|
|
939
1264
|
`);
|
|
940
1265
|
}
|
|
941
|
-
function
|
|
1266
|
+
async function promptForProviderModel(args) {
|
|
1267
|
+
const options = getProviderModelOptions(args.provider);
|
|
1268
|
+
const customCurrent = args.currentModel && !findProviderModelOption(args.provider, args.currentModel) ? `Keep current custom model (${args.currentModel})` : "Custom model";
|
|
1269
|
+
if (args.io.select) {
|
|
1270
|
+
const labels = options.map((option) => `${option.label} - ${option.note}`);
|
|
1271
|
+
labels.push(customCurrent);
|
|
1272
|
+
const choice = await args.io.select(
|
|
1273
|
+
"Pick the fallback model. Cheap is usually the right answer here; this only wakes up when sift needs help.",
|
|
1274
|
+
labels,
|
|
1275
|
+
"Model",
|
|
1276
|
+
true
|
|
1277
|
+
);
|
|
1278
|
+
if (isBackSelection(choice)) {
|
|
1279
|
+
return PROMPT_BACK;
|
|
1280
|
+
}
|
|
1281
|
+
const match = options.find((option) => choice.startsWith(option.label));
|
|
1282
|
+
if (match) {
|
|
1283
|
+
return match.model;
|
|
1284
|
+
}
|
|
1285
|
+
if (customCurrent.startsWith("Keep current custom model")) {
|
|
1286
|
+
return args.currentModel ?? getDefaultProviderModel(args.provider);
|
|
1287
|
+
}
|
|
1288
|
+
} else {
|
|
1289
|
+
args.io.write("\nPick the fallback model.\n\n");
|
|
1290
|
+
options.forEach((option, index) => {
|
|
1291
|
+
args.io.write(` ${index + 1}) ${option.label} - ${option.note}
|
|
1292
|
+
`);
|
|
1293
|
+
});
|
|
1294
|
+
args.io.write(` ${options.length + 1}) ${customCurrent}
|
|
1295
|
+
|
|
1296
|
+
`);
|
|
1297
|
+
while (true) {
|
|
1298
|
+
const answer = (await args.io.ask("Model choice [1]: ")).trim();
|
|
1299
|
+
if (answer.toLowerCase() === "back" || answer.toLowerCase() === "b") {
|
|
1300
|
+
return PROMPT_BACK;
|
|
1301
|
+
}
|
|
1302
|
+
if (answer === "") {
|
|
1303
|
+
return options[0].model;
|
|
1304
|
+
}
|
|
1305
|
+
const index = Number(answer);
|
|
1306
|
+
if (Number.isInteger(index) && index >= 1 && index <= options.length) {
|
|
1307
|
+
return options[index - 1].model;
|
|
1308
|
+
}
|
|
1309
|
+
if (Number.isInteger(index) && index === options.length + 1) {
|
|
1310
|
+
break;
|
|
1311
|
+
}
|
|
1312
|
+
args.io.error(`Please enter a number between 1 and ${options.length + 1}.
|
|
1313
|
+
`);
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
while (true) {
|
|
1317
|
+
const answer = (await args.io.ask("Custom model id: ")).trim();
|
|
1318
|
+
if (answer.toLowerCase() === "back" || answer.toLowerCase() === "b") {
|
|
1319
|
+
return PROMPT_BACK;
|
|
1320
|
+
}
|
|
1321
|
+
if (answer.length > 0) {
|
|
1322
|
+
return answer;
|
|
1323
|
+
}
|
|
1324
|
+
args.io.error("Model id cannot be empty.\n");
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
function writeProviderDefaults(io, provider, selectedModel) {
|
|
942
1328
|
const ui = getSetupPresenter(io);
|
|
1329
|
+
const options = getProviderModelOptions(provider);
|
|
943
1330
|
if (provider === "openrouter") {
|
|
944
|
-
io.write(`${ui.info("
|
|
1331
|
+
io.write(`${ui.info("OpenRouter fallback it is. Free is lovely right up until latency develops a personality.")}
|
|
945
1332
|
`);
|
|
946
|
-
io.write(`${ui.labelValue("Default model", "openrouter
|
|
1333
|
+
io.write(`${ui.labelValue("Default model", getDefaultProviderModel("openrouter"))}
|
|
947
1334
|
`);
|
|
948
1335
|
io.write(`${ui.labelValue("Default base URL", "https://openrouter.ai/api/v1")}
|
|
949
1336
|
`);
|
|
950
1337
|
} else {
|
|
951
|
-
io.write(`${ui.info("
|
|
1338
|
+
io.write(`${ui.info("OpenAI fallback it is. Start cheap, save the fancy stuff for when the logs deserve it.")}
|
|
952
1339
|
`);
|
|
953
|
-
io.write(`${ui.labelValue("Default model", "
|
|
1340
|
+
io.write(`${ui.labelValue("Default model", getDefaultProviderModel("openai"))}
|
|
954
1341
|
`);
|
|
955
1342
|
io.write(`${ui.labelValue("Default base URL", "https://api.openai.com/v1")}
|
|
1343
|
+
`);
|
|
1344
|
+
}
|
|
1345
|
+
io.write(`${ui.labelValue("Selected model", selectedModel)}
|
|
1346
|
+
`);
|
|
1347
|
+
io.write(
|
|
1348
|
+
`${ui.note("This fallback only wakes up when sift's own rules are not enough. The idea is fewer dead ends, not paying for a second opinion on every command.")}
|
|
1349
|
+
`
|
|
1350
|
+
);
|
|
1351
|
+
io.write(`${ui.note("Popular alternatives:")}
|
|
1352
|
+
`);
|
|
1353
|
+
for (const option of options.filter((option2) => option2.model !== selectedModel)) {
|
|
1354
|
+
io.write(` ${ui.command(option.label)}${ui.note(` # ${option.note}`)}
|
|
956
1355
|
`);
|
|
957
1356
|
}
|
|
958
1357
|
io.write(
|
|
@@ -964,54 +1363,101 @@ function writeProviderDefaults(io, provider) {
|
|
|
964
1363
|
`
|
|
965
1364
|
);
|
|
966
1365
|
}
|
|
967
|
-
function materializeProfile(provider, profile,
|
|
1366
|
+
function materializeProfile(provider, profile, overrides = {}) {
|
|
968
1367
|
return {
|
|
1368
|
+
...profile,
|
|
969
1369
|
...getProfileProviderState(provider, profile),
|
|
970
|
-
...
|
|
1370
|
+
...overrides.model !== void 0 ? { model: overrides.model } : {},
|
|
1371
|
+
...overrides.apiKey !== void 0 ? { apiKey: overrides.apiKey } : {}
|
|
971
1372
|
};
|
|
972
1373
|
}
|
|
973
1374
|
function buildSetupConfig(args) {
|
|
974
1375
|
const preservedConfig = preserveActiveNativeProviderProfile(args.config);
|
|
1376
|
+
if (args.mode !== "provider-assisted") {
|
|
1377
|
+
return {
|
|
1378
|
+
...preservedConfig,
|
|
1379
|
+
runtime: {
|
|
1380
|
+
...preservedConfig.runtime,
|
|
1381
|
+
operationMode: args.mode
|
|
1382
|
+
}
|
|
1383
|
+
};
|
|
1384
|
+
}
|
|
1385
|
+
if (!args.provider || !args.apiKeyChoice) {
|
|
1386
|
+
throw new Error("Provider-assisted setup requires provider and API key choice.");
|
|
1387
|
+
}
|
|
975
1388
|
const storedProfile = getStoredProviderProfile(preservedConfig, args.provider);
|
|
976
1389
|
if (args.apiKeyChoice === "saved") {
|
|
977
1390
|
const profile2 = materializeProfile(
|
|
978
1391
|
args.provider,
|
|
979
1392
|
storedProfile,
|
|
980
|
-
|
|
1393
|
+
{
|
|
1394
|
+
apiKey: storedProfile?.apiKey ?? "",
|
|
1395
|
+
model: args.model
|
|
1396
|
+
}
|
|
981
1397
|
);
|
|
982
1398
|
const configWithProfile2 = setStoredProviderProfile(
|
|
983
1399
|
preservedConfig,
|
|
984
1400
|
args.provider,
|
|
985
1401
|
profile2
|
|
986
1402
|
);
|
|
987
|
-
|
|
1403
|
+
const applied2 = applyActiveProvider(
|
|
988
1404
|
configWithProfile2,
|
|
989
1405
|
args.provider,
|
|
990
1406
|
profile2,
|
|
991
1407
|
profile2.apiKey ?? ""
|
|
992
1408
|
);
|
|
1409
|
+
return {
|
|
1410
|
+
...applied2,
|
|
1411
|
+
runtime: {
|
|
1412
|
+
...applied2.runtime,
|
|
1413
|
+
operationMode: args.mode
|
|
1414
|
+
}
|
|
1415
|
+
};
|
|
993
1416
|
}
|
|
994
1417
|
if (args.apiKeyChoice === "env") {
|
|
995
|
-
const profile2 =
|
|
996
|
-
|
|
997
|
-
|
|
1418
|
+
const profile2 = materializeProfile(args.provider, storedProfile, {
|
|
1419
|
+
model: args.model
|
|
1420
|
+
});
|
|
1421
|
+
const configWithProfile2 = setStoredProviderProfile(
|
|
1422
|
+
preservedConfig,
|
|
1423
|
+
args.provider,
|
|
1424
|
+
profile2
|
|
1425
|
+
);
|
|
1426
|
+
const applied2 = applyActiveProvider(configWithProfile2, args.provider, profile2, "");
|
|
1427
|
+
return {
|
|
1428
|
+
...applied2,
|
|
1429
|
+
runtime: {
|
|
1430
|
+
...applied2.runtime,
|
|
1431
|
+
operationMode: args.mode
|
|
1432
|
+
}
|
|
1433
|
+
};
|
|
998
1434
|
}
|
|
999
1435
|
const profile = materializeProfile(
|
|
1000
1436
|
args.provider,
|
|
1001
1437
|
storedProfile,
|
|
1002
|
-
|
|
1438
|
+
{
|
|
1439
|
+
apiKey: args.nextApiKey ?? "",
|
|
1440
|
+
model: args.model
|
|
1441
|
+
}
|
|
1003
1442
|
);
|
|
1004
1443
|
const configWithProfile = setStoredProviderProfile(
|
|
1005
1444
|
preservedConfig,
|
|
1006
1445
|
args.provider,
|
|
1007
1446
|
profile
|
|
1008
1447
|
);
|
|
1009
|
-
|
|
1448
|
+
const applied = applyActiveProvider(
|
|
1010
1449
|
configWithProfile,
|
|
1011
1450
|
args.provider,
|
|
1012
1451
|
profile,
|
|
1013
1452
|
args.nextApiKey ?? ""
|
|
1014
1453
|
);
|
|
1454
|
+
return {
|
|
1455
|
+
...applied,
|
|
1456
|
+
runtime: {
|
|
1457
|
+
...applied.runtime,
|
|
1458
|
+
operationMode: args.mode
|
|
1459
|
+
}
|
|
1460
|
+
};
|
|
1015
1461
|
}
|
|
1016
1462
|
async function configSetup(options = {}) {
|
|
1017
1463
|
void options.global;
|
|
@@ -1025,29 +1471,119 @@ async function configSetup(options = {}) {
|
|
|
1025
1471
|
);
|
|
1026
1472
|
return 1;
|
|
1027
1473
|
}
|
|
1028
|
-
|
|
1474
|
+
if (!options.embedded) {
|
|
1475
|
+
io.write(`${ui.welcome("Let's keep the expensive model for the interesting bits.")}
|
|
1476
|
+
`);
|
|
1477
|
+
io.write(`${ui.note('"Sharp first, expensive later."')}
|
|
1478
|
+
`);
|
|
1479
|
+
} else {
|
|
1480
|
+
io.write(`${ui.info("Next: provider, model, and credentials. Press Esc any time if you want to step back.")}
|
|
1029
1481
|
`);
|
|
1482
|
+
}
|
|
1030
1483
|
const resolvedPath = resolveSetupPath(options.targetPath);
|
|
1031
1484
|
const { config: existingConfig, existed } = loadEditableConfig(resolvedPath);
|
|
1032
1485
|
if (existed) {
|
|
1033
1486
|
io.write(`${ui.info(`Updating existing config at ${resolvedPath}.`)}
|
|
1034
1487
|
`);
|
|
1035
1488
|
}
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1489
|
+
let mode = options.forcedMode ?? await promptForOperationMode(io);
|
|
1490
|
+
let provider;
|
|
1491
|
+
let model;
|
|
1492
|
+
let apiKeyChoice;
|
|
1493
|
+
let nextApiKey;
|
|
1494
|
+
let modeSummaryShown = false;
|
|
1495
|
+
while (true) {
|
|
1496
|
+
if (!modeSummaryShown) {
|
|
1497
|
+
writeModeSummary(io, mode);
|
|
1498
|
+
modeSummaryShown = true;
|
|
1499
|
+
}
|
|
1500
|
+
if (mode !== "provider-assisted") {
|
|
1501
|
+
io.write(
|
|
1502
|
+
`${ui.note("No provider credentials are required for this mode. You can switch later by running `sift config setup` again.")}
|
|
1503
|
+
`
|
|
1504
|
+
);
|
|
1505
|
+
break;
|
|
1506
|
+
}
|
|
1507
|
+
let providerStep = "provider";
|
|
1508
|
+
while (true) {
|
|
1509
|
+
if (providerStep === "provider") {
|
|
1510
|
+
const providerChoice = await promptForProvider(io);
|
|
1511
|
+
if (providerChoice === PROMPT_BACK) {
|
|
1512
|
+
if (options.forcedMode) {
|
|
1513
|
+
return options.embedded ? CONFIG_SETUP_BACK : 1;
|
|
1514
|
+
}
|
|
1515
|
+
mode = await promptForOperationMode(io);
|
|
1516
|
+
modeSummaryShown = false;
|
|
1517
|
+
provider = void 0;
|
|
1518
|
+
model = void 0;
|
|
1519
|
+
apiKeyChoice = void 0;
|
|
1520
|
+
nextApiKey = void 0;
|
|
1521
|
+
break;
|
|
1522
|
+
}
|
|
1523
|
+
provider = providerChoice;
|
|
1524
|
+
providerStep = "model";
|
|
1525
|
+
continue;
|
|
1526
|
+
}
|
|
1527
|
+
const storedProfile = getStoredProviderProfile(existingConfig, provider);
|
|
1528
|
+
if (providerStep === "model") {
|
|
1529
|
+
const modelChoice = await promptForProviderModel({
|
|
1530
|
+
io,
|
|
1531
|
+
provider,
|
|
1532
|
+
currentModel: storedProfile?.model
|
|
1533
|
+
});
|
|
1534
|
+
if (modelChoice === PROMPT_BACK) {
|
|
1535
|
+
providerStep = "provider";
|
|
1536
|
+
continue;
|
|
1537
|
+
}
|
|
1538
|
+
model = modelChoice;
|
|
1539
|
+
writeProviderDefaults(io, provider, model);
|
|
1540
|
+
providerStep = "api-key-choice";
|
|
1541
|
+
continue;
|
|
1542
|
+
}
|
|
1543
|
+
const envName = getNativeProviderApiKeyEnvName(provider);
|
|
1544
|
+
if (providerStep === "api-key-choice") {
|
|
1545
|
+
const keyChoice = await promptForApiKeyChoice({
|
|
1546
|
+
io,
|
|
1547
|
+
provider,
|
|
1548
|
+
envName,
|
|
1549
|
+
hasSavedKey: Boolean(storedProfile?.apiKey),
|
|
1550
|
+
hasEnvKey: Boolean(env[envName])
|
|
1551
|
+
});
|
|
1552
|
+
if (keyChoice === PROMPT_BACK) {
|
|
1553
|
+
providerStep = "model";
|
|
1554
|
+
continue;
|
|
1555
|
+
}
|
|
1556
|
+
apiKeyChoice = keyChoice;
|
|
1557
|
+
if (apiKeyChoice === "override") {
|
|
1558
|
+
io.write(`${ui.note("Press Esc if you want to go back instead of entering a key right now.")}
|
|
1559
|
+
`);
|
|
1560
|
+
providerStep = "api-key-entry";
|
|
1561
|
+
continue;
|
|
1562
|
+
}
|
|
1563
|
+
nextApiKey = void 0;
|
|
1564
|
+
break;
|
|
1565
|
+
}
|
|
1566
|
+
const apiKey = await promptForApiKey(io, provider);
|
|
1567
|
+
if (apiKey === PROMPT_BACK) {
|
|
1568
|
+
providerStep = "api-key-choice";
|
|
1569
|
+
continue;
|
|
1570
|
+
}
|
|
1571
|
+
nextApiKey = apiKey;
|
|
1572
|
+
break;
|
|
1573
|
+
}
|
|
1574
|
+
if (mode !== "provider-assisted") {
|
|
1575
|
+
continue;
|
|
1576
|
+
}
|
|
1577
|
+
if (!provider || !apiKeyChoice) {
|
|
1578
|
+
continue;
|
|
1579
|
+
}
|
|
1580
|
+
break;
|
|
1581
|
+
}
|
|
1048
1582
|
const config = buildSetupConfig({
|
|
1049
1583
|
config: existingConfig,
|
|
1584
|
+
mode,
|
|
1050
1585
|
provider,
|
|
1586
|
+
model,
|
|
1051
1587
|
apiKeyChoice,
|
|
1052
1588
|
nextApiKey
|
|
1053
1589
|
});
|
|
@@ -1056,18 +1592,19 @@ async function configSetup(options = {}) {
|
|
|
1056
1592
|
config,
|
|
1057
1593
|
overwrite: existed
|
|
1058
1594
|
});
|
|
1059
|
-
if (apiKeyChoice === "env") {
|
|
1595
|
+
if (mode === "provider-assisted" && provider && apiKeyChoice === "env") {
|
|
1596
|
+
const envName = getNativeProviderApiKeyEnvName(provider);
|
|
1060
1597
|
io.write(
|
|
1061
1598
|
`${ui.note(`Using ${envName} from the environment. No API key was written to config.`)}
|
|
1062
1599
|
`
|
|
1063
1600
|
);
|
|
1064
1601
|
}
|
|
1065
|
-
writeSetupSuccess(io, writtenPath);
|
|
1602
|
+
writeSetupSuccess(io, writtenPath, mode);
|
|
1066
1603
|
const activeConfigPath = findConfigPath();
|
|
1067
1604
|
if (activeConfigPath && path5.resolve(activeConfigPath) !== path5.resolve(writtenPath)) {
|
|
1068
1605
|
writeOverrideWarning(io, activeConfigPath);
|
|
1069
1606
|
}
|
|
1070
|
-
writeNextSteps(io);
|
|
1607
|
+
writeNextSteps(io, mode);
|
|
1071
1608
|
return 0;
|
|
1072
1609
|
} finally {
|
|
1073
1610
|
io.close?.();
|
|
@@ -1094,18 +1631,18 @@ function maskConfigSecrets(value) {
|
|
|
1094
1631
|
return output;
|
|
1095
1632
|
}
|
|
1096
1633
|
function configInit(targetPath, global = false) {
|
|
1097
|
-
const
|
|
1634
|
+
const path9 = writeExampleConfig({
|
|
1098
1635
|
targetPath,
|
|
1099
1636
|
global
|
|
1100
1637
|
});
|
|
1101
1638
|
if (!process.stdout.isTTY) {
|
|
1102
|
-
process.stdout.write(`${
|
|
1639
|
+
process.stdout.write(`${path9}
|
|
1103
1640
|
`);
|
|
1104
1641
|
return;
|
|
1105
1642
|
}
|
|
1106
1643
|
const ui = createPresentation(true);
|
|
1107
1644
|
process.stdout.write(
|
|
1108
|
-
`${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${
|
|
1645
|
+
`${ui.success(`${global ? "Machine-wide" : "Template"} config written to ${path9}`)}
|
|
1109
1646
|
`
|
|
1110
1647
|
);
|
|
1111
1648
|
}
|
|
@@ -1268,16 +1805,34 @@ function getManagedBlockMarkers(agent) {
|
|
|
1268
1805
|
end: `<!-- sift:end ${agent} -->`
|
|
1269
1806
|
};
|
|
1270
1807
|
}
|
|
1271
|
-
function
|
|
1808
|
+
function inferOperationMode(args) {
|
|
1809
|
+
if (args.operationMode) {
|
|
1810
|
+
return args.operationMode;
|
|
1811
|
+
}
|
|
1812
|
+
try {
|
|
1813
|
+
const cwd = args.cwd ?? process.cwd();
|
|
1814
|
+
const homeDir = args.homeDir ?? os2.homedir();
|
|
1815
|
+
const repoConfigPath = path6.resolve(cwd, "sift.config.yaml");
|
|
1816
|
+
const globalConfigPath = getDefaultGlobalConfigPath(homeDir);
|
|
1817
|
+
const configPath = fs5.existsSync(repoConfigPath) ? repoConfigPath : fs5.existsSync(globalConfigPath) ? globalConfigPath : void 0;
|
|
1818
|
+
const config = resolveConfig(configPath ? { configPath } : {});
|
|
1819
|
+
return resolveEffectiveOperationMode(config);
|
|
1820
|
+
} catch {
|
|
1821
|
+
return "agent-escalation";
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
function renderInstructionBody(mode) {
|
|
1272
1825
|
return [
|
|
1273
|
-
"Use `sift` as the default
|
|
1274
|
-
"The goal is to
|
|
1826
|
+
"Use `sift` as the default local-first output guidance layer for long, noisy, non-interactive command output.",
|
|
1827
|
+
"The goal is to turn failure walls into grouped issues, likely root causes, and the next useful step before reading raw logs.",
|
|
1828
|
+
"",
|
|
1829
|
+
`Default operating mode: ${getOperationModeLabel(mode)}.`,
|
|
1830
|
+
describeOperationMode(mode),
|
|
1831
|
+
describeInsufficientBehavior(mode),
|
|
1275
1832
|
"",
|
|
1276
1833
|
"Start with:",
|
|
1277
1834
|
'- `sift exec "question" -- <command> [args...]`',
|
|
1278
1835
|
"- `sift exec --preset test-status -- <test command>`",
|
|
1279
|
-
'- `sift watch "what changed between cycles?" < watcher-output.txt`',
|
|
1280
|
-
'- `sift exec --watch "what changed between cycles?" -- <command> [args...]`',
|
|
1281
1836
|
"- `sift exec --preset audit-critical -- npm audit`",
|
|
1282
1837
|
"- `sift exec --preset infra-risk -- terraform plan`",
|
|
1283
1838
|
"",
|
|
@@ -1316,9 +1871,9 @@ function renderInstructionBody() {
|
|
|
1316
1871
|
"Do not pass API keys inline."
|
|
1317
1872
|
].join("\n");
|
|
1318
1873
|
}
|
|
1319
|
-
function renderManagedBlock(agent, eol = "\n") {
|
|
1874
|
+
function renderManagedBlock(agent, eol = "\n", mode = "agent-escalation") {
|
|
1320
1875
|
const markers = getManagedBlockMarkers(agent);
|
|
1321
|
-
return [markers.start, renderInstructionBody(), markers.end].join(eol);
|
|
1876
|
+
return [markers.start, renderInstructionBody(mode), markers.end].join(eol);
|
|
1322
1877
|
}
|
|
1323
1878
|
function inspectManagedBlock(content, agent) {
|
|
1324
1879
|
const markers = getManagedBlockMarkers(agent);
|
|
@@ -1343,7 +1898,7 @@ function inspectManagedBlock(content, agent) {
|
|
|
1343
1898
|
}
|
|
1344
1899
|
function planManagedInstall(args) {
|
|
1345
1900
|
const eol = args.existingContent?.includes("\r\n") ? "\r\n" : "\n";
|
|
1346
|
-
const block = renderManagedBlock(args.agent, eol);
|
|
1901
|
+
const block = renderManagedBlock(args.agent, eol, args.operationMode ?? "agent-escalation");
|
|
1347
1902
|
if (args.existingContent === void 0) {
|
|
1348
1903
|
return {
|
|
1349
1904
|
action: "create",
|
|
@@ -1433,6 +1988,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1433
1988
|
const params = typeof args === "string" ? {
|
|
1434
1989
|
agent: args,
|
|
1435
1990
|
scope: "repo",
|
|
1991
|
+
operationMode: void 0,
|
|
1436
1992
|
raw: false,
|
|
1437
1993
|
targetPath: void 0,
|
|
1438
1994
|
cwd: void 0,
|
|
@@ -1441,6 +1997,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1441
1997
|
} : {
|
|
1442
1998
|
agent: args.agent,
|
|
1443
1999
|
scope: args.scope ?? "repo",
|
|
2000
|
+
operationMode: args.operationMode,
|
|
1444
2001
|
raw: args.raw ?? false,
|
|
1445
2002
|
targetPath: args.targetPath,
|
|
1446
2003
|
cwd: args.cwd,
|
|
@@ -1449,8 +2006,13 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1449
2006
|
};
|
|
1450
2007
|
const agent = normalizeAgentName(params.agent);
|
|
1451
2008
|
const io = params.io;
|
|
2009
|
+
const operationMode = inferOperationMode({
|
|
2010
|
+
cwd: params.cwd,
|
|
2011
|
+
homeDir: params.homeDir,
|
|
2012
|
+
operationMode: params.operationMode
|
|
2013
|
+
});
|
|
1452
2014
|
if (params.raw) {
|
|
1453
|
-
io.write(`${renderManagedBlock(agent)}
|
|
2015
|
+
io.write(`${renderManagedBlock(agent, "\n", operationMode)}
|
|
1454
2016
|
`);
|
|
1455
2017
|
return;
|
|
1456
2018
|
}
|
|
@@ -1489,6 +2051,8 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1489
2051
|
)}
|
|
1490
2052
|
`
|
|
1491
2053
|
);
|
|
2054
|
+
io.write(`${ui.labelValue("operation mode", getOperationModeLabel(operationMode))}
|
|
2055
|
+
`);
|
|
1492
2056
|
if (currentInstalled) {
|
|
1493
2057
|
io.write(`${ui.warning(`Already installed in ${params.scope} scope.`)}
|
|
1494
2058
|
`);
|
|
@@ -1506,13 +2070,17 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1506
2070
|
`
|
|
1507
2071
|
);
|
|
1508
2072
|
io.write(
|
|
1509
|
-
`${ui.info("The point is to
|
|
2073
|
+
`${ui.info("The point is to narrow long command output before your agent burns time and tokens on the raw log wall.")}
|
|
1510
2074
|
`
|
|
1511
2075
|
);
|
|
1512
2076
|
io.write(
|
|
1513
2077
|
`${ui.info("The managed block teaches the agent to default to sift first, keep raw as the last resort, and treat standard as the usual stop point.")}
|
|
1514
2078
|
`
|
|
1515
2079
|
);
|
|
2080
|
+
io.write(`${ui.note(describeOperationMode(operationMode))}
|
|
2081
|
+
`);
|
|
2082
|
+
io.write(`${ui.note(describeInsufficientBehavior(operationMode))}
|
|
2083
|
+
`);
|
|
1516
2084
|
io.write(` ${ui.command('sift exec "question" -- <command> [args...]')}
|
|
1517
2085
|
`);
|
|
1518
2086
|
io.write(` ${ui.command("sift exec --preset test-status -- <test command>")}
|
|
@@ -1522,7 +2090,7 @@ function showAgent(args, ioArg = createStdoutOnlyIO()) {
|
|
|
1522
2090
|
io.write(` ${ui.command("sift exec --preset infra-risk -- terraform plan")}
|
|
1523
2091
|
`);
|
|
1524
2092
|
io.write(
|
|
1525
|
-
`${ui.info("For test debugging, standard should usually be enough for first-pass
|
|
2093
|
+
`${ui.info("For test debugging, standard should usually be enough for first-pass guidance.")}
|
|
1526
2094
|
`
|
|
1527
2095
|
);
|
|
1528
2096
|
io.write(
|
|
@@ -1572,6 +2140,11 @@ async function installAgent(args) {
|
|
|
1572
2140
|
homeDir: args.homeDir
|
|
1573
2141
|
});
|
|
1574
2142
|
const ui = createPresentation(io.stdoutIsTTY);
|
|
2143
|
+
const operationMode = inferOperationMode({
|
|
2144
|
+
cwd: args.cwd,
|
|
2145
|
+
homeDir: args.homeDir,
|
|
2146
|
+
operationMode: args.operationMode
|
|
2147
|
+
});
|
|
1575
2148
|
try {
|
|
1576
2149
|
const existingContent = readOptionalFile(targetPath);
|
|
1577
2150
|
const fileExists = existingContent !== void 0;
|
|
@@ -1579,7 +2152,8 @@ async function installAgent(args) {
|
|
|
1579
2152
|
const plan = planManagedInstall({
|
|
1580
2153
|
agent,
|
|
1581
2154
|
targetPath,
|
|
1582
|
-
existingContent
|
|
2155
|
+
existingContent,
|
|
2156
|
+
operationMode
|
|
1583
2157
|
});
|
|
1584
2158
|
if (args.dryRun) {
|
|
1585
2159
|
if (args.raw) {
|
|
@@ -1635,6 +2209,8 @@ async function installAgent(args) {
|
|
|
1635
2209
|
io.write(`${ui.labelValue("scope", scope)}
|
|
1636
2210
|
`);
|
|
1637
2211
|
io.write(`${ui.labelValue("target", targetPath)}
|
|
2212
|
+
`);
|
|
2213
|
+
io.write(`${ui.labelValue("operation mode", getOperationModeLabel(operationMode))}
|
|
1638
2214
|
`);
|
|
1639
2215
|
io.write(`${ui.info("This will only manage the sift block.")}
|
|
1640
2216
|
`);
|
|
@@ -1835,6 +2411,465 @@ function escapeRegExp(value) {
|
|
|
1835
2411
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1836
2412
|
}
|
|
1837
2413
|
|
|
2414
|
+
// src/commands/install.ts
|
|
2415
|
+
import os3 from "os";
|
|
2416
|
+
import path7 from "path";
|
|
2417
|
+
import { emitKeypressEvents as emitKeypressEvents2 } from "readline";
|
|
2418
|
+
import { createInterface as createInterface3 } from "readline/promises";
|
|
2419
|
+
import {
|
|
2420
|
+
stderr as defaultStderr3,
|
|
2421
|
+
stdin as defaultStdin4,
|
|
2422
|
+
stdout as defaultStdout3
|
|
2423
|
+
} from "process";
|
|
2424
|
+
var INSTALL_TITLES = {
|
|
2425
|
+
codex: "Codex",
|
|
2426
|
+
claude: "Claude"
|
|
2427
|
+
};
|
|
2428
|
+
function createInstallTerminalIO() {
|
|
2429
|
+
let rl;
|
|
2430
|
+
function getInterface() {
|
|
2431
|
+
if (!rl) {
|
|
2432
|
+
rl = createInterface3({
|
|
2433
|
+
input: defaultStdin4,
|
|
2434
|
+
output: defaultStdout3,
|
|
2435
|
+
terminal: true
|
|
2436
|
+
});
|
|
2437
|
+
}
|
|
2438
|
+
return rl;
|
|
2439
|
+
}
|
|
2440
|
+
async function select(prompt, options, selectedLabel, allowBack) {
|
|
2441
|
+
emitKeypressEvents2(defaultStdin4);
|
|
2442
|
+
return await promptSelect({
|
|
2443
|
+
input: defaultStdin4,
|
|
2444
|
+
output: defaultStdout3,
|
|
2445
|
+
prompt,
|
|
2446
|
+
options,
|
|
2447
|
+
selectedLabel,
|
|
2448
|
+
allowBack
|
|
2449
|
+
});
|
|
2450
|
+
}
|
|
2451
|
+
return {
|
|
2452
|
+
stdinIsTTY: Boolean(defaultStdin4.isTTY),
|
|
2453
|
+
stdoutIsTTY: Boolean(defaultStdout3.isTTY),
|
|
2454
|
+
ask(prompt) {
|
|
2455
|
+
return getInterface().question(prompt);
|
|
2456
|
+
},
|
|
2457
|
+
select,
|
|
2458
|
+
write(message) {
|
|
2459
|
+
defaultStdout3.write(message);
|
|
2460
|
+
},
|
|
2461
|
+
error(message) {
|
|
2462
|
+
defaultStderr3.write(message);
|
|
2463
|
+
},
|
|
2464
|
+
close() {
|
|
2465
|
+
rl?.close();
|
|
2466
|
+
}
|
|
2467
|
+
};
|
|
2468
|
+
}
|
|
2469
|
+
function normalizeInstallRuntime(value) {
|
|
2470
|
+
if (value === void 0 || value === null || value === "") {
|
|
2471
|
+
return void 0;
|
|
2472
|
+
}
|
|
2473
|
+
if (value === "codex" || value === "claude" || value === "all") {
|
|
2474
|
+
return value;
|
|
2475
|
+
}
|
|
2476
|
+
throw new Error("Invalid runtime. Use codex, claude, or all.");
|
|
2477
|
+
}
|
|
2478
|
+
function normalizeInstallScope(value) {
|
|
2479
|
+
if (value === void 0 || value === null || value === "") {
|
|
2480
|
+
return void 0;
|
|
2481
|
+
}
|
|
2482
|
+
if (value === "global") {
|
|
2483
|
+
return "global";
|
|
2484
|
+
}
|
|
2485
|
+
if (value === "local" || value === "repo") {
|
|
2486
|
+
return "repo";
|
|
2487
|
+
}
|
|
2488
|
+
throw new Error("Invalid --scope value. Use local or global.");
|
|
2489
|
+
}
|
|
2490
|
+
function renderInstallBanner(version) {
|
|
2491
|
+
const teal = (text) => `\x1B[38;2;34;173;169m${text}\x1B[0m`;
|
|
2492
|
+
return [
|
|
2493
|
+
teal(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557"),
|
|
2494
|
+
teal(" \u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u2550\u2588\u2588\u2554\u2550\u2550\u255D"),
|
|
2495
|
+
teal(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551 "),
|
|
2496
|
+
teal(" \u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u255D \u2588\u2588\u2551 "),
|
|
2497
|
+
teal(" \u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551 "),
|
|
2498
|
+
teal(" \u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D "),
|
|
2499
|
+
"",
|
|
2500
|
+
` sift v${version}`,
|
|
2501
|
+
" Small, sharp, and mildly sarcastic output guidance.",
|
|
2502
|
+
' "Loading the loading screen..." energy, minus the loading screen.'
|
|
2503
|
+
].join("\n");
|
|
2504
|
+
}
|
|
2505
|
+
function getInstallTargets(runtime) {
|
|
2506
|
+
if (runtime === "all") {
|
|
2507
|
+
return ["codex", "claude"];
|
|
2508
|
+
}
|
|
2509
|
+
return [runtime];
|
|
2510
|
+
}
|
|
2511
|
+
function getGlobalTargetLabel(agent, homeDir = os3.homedir()) {
|
|
2512
|
+
return agent === "codex" ? getDefaultCodexGlobalInstructionsPath(homeDir) : getDefaultClaudeGlobalInstructionsPath(homeDir);
|
|
2513
|
+
}
|
|
2514
|
+
function getLocalTargetLabel(agent, cwd = process.cwd()) {
|
|
2515
|
+
return agent === "codex" ? path7.join(cwd, "AGENTS.md") : path7.join(cwd, "CLAUDE.md");
|
|
2516
|
+
}
|
|
2517
|
+
function describeScopeChoice(args) {
|
|
2518
|
+
const targets = getInstallTargets(args.runtime);
|
|
2519
|
+
const labels = targets.map(
|
|
2520
|
+
(agent) => args.scope === "global" ? getGlobalTargetLabel(agent, args.homeDir) : getLocalTargetLabel(agent, args.cwd)
|
|
2521
|
+
);
|
|
2522
|
+
return labels.join(" + ");
|
|
2523
|
+
}
|
|
2524
|
+
async function promptWithMenu(args) {
|
|
2525
|
+
const defaultIndex = args.defaultIndex ?? 0;
|
|
2526
|
+
if (args.io.select) {
|
|
2527
|
+
const labels = args.choices.map((choice) => choice.label);
|
|
2528
|
+
const selected = await args.io.select(args.prompt, labels, args.selectedLabel, args.allowBack);
|
|
2529
|
+
if (selected === PROMPT_BACK) {
|
|
2530
|
+
return PROMPT_BACK;
|
|
2531
|
+
}
|
|
2532
|
+
const match = args.choices.find((choice) => choice.label === selected);
|
|
2533
|
+
if (match) {
|
|
2534
|
+
return match.value;
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
args.io.write(`
|
|
2538
|
+
${args.prompt}
|
|
2539
|
+
|
|
2540
|
+
`);
|
|
2541
|
+
args.choices.forEach((choice, index) => {
|
|
2542
|
+
args.io.write(` ${index + 1}) ${choice.label}
|
|
2543
|
+
`);
|
|
2544
|
+
});
|
|
2545
|
+
if (args.allowBack) {
|
|
2546
|
+
args.io.write(` ${args.choices.length + 1}) Back
|
|
2547
|
+
`);
|
|
2548
|
+
}
|
|
2549
|
+
args.io.write("\n");
|
|
2550
|
+
while (true) {
|
|
2551
|
+
const answer = (await args.io.ask(`Choice [${defaultIndex + 1}]: `)).trim();
|
|
2552
|
+
if (args.allowBack && (answer.toLowerCase() === "back" || answer.toLowerCase() === "b")) {
|
|
2553
|
+
return PROMPT_BACK;
|
|
2554
|
+
}
|
|
2555
|
+
if (answer === "") {
|
|
2556
|
+
return args.choices[defaultIndex]?.value ?? args.choices[0].value;
|
|
2557
|
+
}
|
|
2558
|
+
const choiceIndex = Number(answer);
|
|
2559
|
+
if (Number.isInteger(choiceIndex) && choiceIndex >= 1 && choiceIndex <= args.choices.length) {
|
|
2560
|
+
return args.choices[choiceIndex - 1].value;
|
|
2561
|
+
}
|
|
2562
|
+
if (args.allowBack && Number.isInteger(choiceIndex) && choiceIndex === args.choices.length + 1) {
|
|
2563
|
+
return PROMPT_BACK;
|
|
2564
|
+
}
|
|
2565
|
+
const max = args.allowBack ? args.choices.length + 1 : args.choices.length;
|
|
2566
|
+
args.io.error(`Please enter a number between 1 and ${max}.
|
|
2567
|
+
`);
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
2570
|
+
async function promptForRuntime(io) {
|
|
2571
|
+
return await promptWithMenu({
|
|
2572
|
+
io,
|
|
2573
|
+
prompt: "Choose your runtime",
|
|
2574
|
+
selectedLabel: "Runtime",
|
|
2575
|
+
allowBack: true,
|
|
2576
|
+
choices: [
|
|
2577
|
+
{
|
|
2578
|
+
label: "Codex (AGENTS.md / ~/.codex/AGENTS.md) - first-class if you live in Codex",
|
|
2579
|
+
value: "codex"
|
|
2580
|
+
},
|
|
2581
|
+
{
|
|
2582
|
+
label: "Claude (CLAUDE.md / ~/.claude/CLAUDE.md) - same good manners, Claude-flavored",
|
|
2583
|
+
value: "claude"
|
|
2584
|
+
},
|
|
2585
|
+
{
|
|
2586
|
+
label: "All - if you refuse to pick favorites today",
|
|
2587
|
+
value: "all"
|
|
2588
|
+
}
|
|
2589
|
+
]
|
|
2590
|
+
});
|
|
2591
|
+
}
|
|
2592
|
+
async function promptForScope(args) {
|
|
2593
|
+
return await promptWithMenu({
|
|
2594
|
+
io: args.io,
|
|
2595
|
+
prompt: "Choose where to install the runtime support",
|
|
2596
|
+
selectedLabel: "Location",
|
|
2597
|
+
allowBack: true,
|
|
2598
|
+
choices: [
|
|
2599
|
+
{
|
|
2600
|
+
label: `Global (${describeScopeChoice({
|
|
2601
|
+
runtime: args.runtime,
|
|
2602
|
+
scope: "global",
|
|
2603
|
+
cwd: args.cwd,
|
|
2604
|
+
homeDir: args.homeDir
|
|
2605
|
+
})}) - use this if you want sift ready everywhere`,
|
|
2606
|
+
value: "global"
|
|
2607
|
+
},
|
|
2608
|
+
{
|
|
2609
|
+
label: `Local (${describeScopeChoice({
|
|
2610
|
+
runtime: args.runtime,
|
|
2611
|
+
scope: "repo",
|
|
2612
|
+
cwd: args.cwd,
|
|
2613
|
+
homeDir: args.homeDir
|
|
2614
|
+
})}) - keep it here if this repo is the only one that matters`,
|
|
2615
|
+
value: "repo"
|
|
2616
|
+
}
|
|
2617
|
+
]
|
|
2618
|
+
});
|
|
2619
|
+
}
|
|
2620
|
+
async function promptForOperationMode2(io) {
|
|
2621
|
+
return await promptWithMenu({
|
|
2622
|
+
io,
|
|
2623
|
+
prompt: "Choose how sift should work",
|
|
2624
|
+
selectedLabel: "Mode",
|
|
2625
|
+
allowBack: true,
|
|
2626
|
+
choices: [
|
|
2627
|
+
{
|
|
2628
|
+
label: "With an agent - recommended if Codex or Claude is already with you; sift does the fast local first pass, the agent only steps in when repo context is truly needed",
|
|
2629
|
+
value: "agent-escalation"
|
|
2630
|
+
},
|
|
2631
|
+
{
|
|
2632
|
+
label: "With provider fallback - recommended if you want sift to finish more ambiguous cases on its own before handing them back to you or your agent; needs an API key, cheap model only when needed",
|
|
2633
|
+
value: "provider-assisted"
|
|
2634
|
+
},
|
|
2635
|
+
{
|
|
2636
|
+
label: "Solo, local-only - recommended if you want zero model calls; great for supported presets, ambiguous cases stay with you",
|
|
2637
|
+
value: "local-only"
|
|
2638
|
+
}
|
|
2639
|
+
]
|
|
2640
|
+
});
|
|
2641
|
+
}
|
|
2642
|
+
function createNestedInstallIO(parent) {
|
|
2643
|
+
return {
|
|
2644
|
+
stdinIsTTY: parent.stdinIsTTY,
|
|
2645
|
+
stdoutIsTTY: parent.stdoutIsTTY,
|
|
2646
|
+
ask: async () => "",
|
|
2647
|
+
write() {
|
|
2648
|
+
},
|
|
2649
|
+
error(message) {
|
|
2650
|
+
parent.error(message);
|
|
2651
|
+
}
|
|
2652
|
+
};
|
|
2653
|
+
}
|
|
2654
|
+
function writeSuccessSummary(args) {
|
|
2655
|
+
const ui = createPresentation(args.io.stdoutIsTTY);
|
|
2656
|
+
const targets = getInstallTargets(args.runtime);
|
|
2657
|
+
const scopeLabel = args.scope === "global" ? "global" : "local";
|
|
2658
|
+
const targetLabel = describeScopeChoice({
|
|
2659
|
+
runtime: args.runtime,
|
|
2660
|
+
scope: args.scope,
|
|
2661
|
+
cwd: args.cwd,
|
|
2662
|
+
homeDir: args.homeDir
|
|
2663
|
+
});
|
|
2664
|
+
if (args.io.stdoutIsTTY) {
|
|
2665
|
+
args.io.write(`
|
|
2666
|
+
${ui.success("Installed runtime support.")}
|
|
2667
|
+
`);
|
|
2668
|
+
} else {
|
|
2669
|
+
args.io.write("Installed runtime support.\n");
|
|
2670
|
+
}
|
|
2671
|
+
args.io.write(
|
|
2672
|
+
`${ui.note(`sift v${args.version} now manages ${targets.map((target) => INSTALL_TITLES[target]).join(" + ")} in ${scopeLabel} scope.`)}
|
|
2673
|
+
`
|
|
2674
|
+
);
|
|
2675
|
+
args.io.write(`${ui.note(`Operating mode: ${getOperationModeLabel(args.operationMode)}`)}
|
|
2676
|
+
`);
|
|
2677
|
+
args.io.write(`${ui.note(describeOperationMode(args.operationMode))}
|
|
2678
|
+
`);
|
|
2679
|
+
args.io.write(`${ui.note(targetLabel)}
|
|
2680
|
+
`);
|
|
2681
|
+
args.io.write(`
|
|
2682
|
+
${ui.section("Try next")}
|
|
2683
|
+
`);
|
|
2684
|
+
args.io.write(` ${ui.command("sift doctor")}
|
|
2685
|
+
`);
|
|
2686
|
+
if (args.operationMode === "provider-assisted") {
|
|
2687
|
+
args.io.write(` ${ui.command("sift config show --show-secrets")}
|
|
2688
|
+
`);
|
|
2689
|
+
} else {
|
|
2690
|
+
args.io.write(
|
|
2691
|
+
` ${ui.command("sift config setup")}${ui.note(" # optional if you want provider-assisted fallback later")}
|
|
2692
|
+
`
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2695
|
+
args.io.write(` ${ui.command("sift exec --preset test-status -- npm test")}
|
|
2696
|
+
`);
|
|
2697
|
+
}
|
|
2698
|
+
async function installRuntimeSupport(options) {
|
|
2699
|
+
const io = options.io ?? createInstallTerminalIO();
|
|
2700
|
+
const getPreviousEditableStep = (step) => {
|
|
2701
|
+
if (step === "runtime") {
|
|
2702
|
+
return void 0;
|
|
2703
|
+
}
|
|
2704
|
+
if (step === "mode") {
|
|
2705
|
+
return options.runtime ? void 0 : "runtime";
|
|
2706
|
+
}
|
|
2707
|
+
if (step === "scope") {
|
|
2708
|
+
if (!options.operationMode) {
|
|
2709
|
+
return "mode";
|
|
2710
|
+
}
|
|
2711
|
+
if (!options.runtime) {
|
|
2712
|
+
return "runtime";
|
|
2713
|
+
}
|
|
2714
|
+
return void 0;
|
|
2715
|
+
}
|
|
2716
|
+
if (step === "provider") {
|
|
2717
|
+
if (!options.scope) {
|
|
2718
|
+
return "scope";
|
|
2719
|
+
}
|
|
2720
|
+
if (!options.operationMode) {
|
|
2721
|
+
return "mode";
|
|
2722
|
+
}
|
|
2723
|
+
if (!options.runtime) {
|
|
2724
|
+
return "runtime";
|
|
2725
|
+
}
|
|
2726
|
+
return void 0;
|
|
2727
|
+
}
|
|
2728
|
+
return void 0;
|
|
2729
|
+
};
|
|
2730
|
+
try {
|
|
2731
|
+
if ((!io.stdinIsTTY || !io.stdoutIsTTY) && (!options.runtime || !options.scope || !options.yes)) {
|
|
2732
|
+
io.error(
|
|
2733
|
+
"sift install is interactive and requires a TTY. For non-interactive use `sift install codex --scope global --yes`.\n"
|
|
2734
|
+
);
|
|
2735
|
+
return 1;
|
|
2736
|
+
}
|
|
2737
|
+
if (io.stdoutIsTTY) {
|
|
2738
|
+
io.write(`${renderInstallBanner(options.version)}
|
|
2739
|
+
`);
|
|
2740
|
+
}
|
|
2741
|
+
let runtime = options.runtime;
|
|
2742
|
+
let operationMode = options.operationMode;
|
|
2743
|
+
let scope = options.scope;
|
|
2744
|
+
let step;
|
|
2745
|
+
if (!io.stdinIsTTY || !io.stdoutIsTTY) {
|
|
2746
|
+
runtime ??= options.runtime;
|
|
2747
|
+
operationMode ??= "agent-escalation";
|
|
2748
|
+
step = void 0;
|
|
2749
|
+
} else if (!runtime) {
|
|
2750
|
+
step = "runtime";
|
|
2751
|
+
} else if (!operationMode) {
|
|
2752
|
+
step = "mode";
|
|
2753
|
+
} else if (!scope) {
|
|
2754
|
+
step = "scope";
|
|
2755
|
+
} else if (operationMode === "provider-assisted") {
|
|
2756
|
+
step = "provider";
|
|
2757
|
+
}
|
|
2758
|
+
while (step) {
|
|
2759
|
+
if (step === "runtime") {
|
|
2760
|
+
const runtimeChoice = await promptForRuntime(io);
|
|
2761
|
+
if (runtimeChoice === PROMPT_BACK) {
|
|
2762
|
+
io.write(`
|
|
2763
|
+
${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
|
|
2764
|
+
`);
|
|
2765
|
+
return 0;
|
|
2766
|
+
}
|
|
2767
|
+
runtime = runtimeChoice;
|
|
2768
|
+
step = !operationMode ? "mode" : !scope ? "scope" : operationMode === "provider-assisted" ? "provider" : void 0;
|
|
2769
|
+
continue;
|
|
2770
|
+
}
|
|
2771
|
+
if (step === "mode") {
|
|
2772
|
+
const modeChoice = await promptForOperationMode2(io);
|
|
2773
|
+
if (modeChoice === PROMPT_BACK) {
|
|
2774
|
+
const previous = getPreviousEditableStep("mode");
|
|
2775
|
+
if (!previous) {
|
|
2776
|
+
io.write(`
|
|
2777
|
+
${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
|
|
2778
|
+
`);
|
|
2779
|
+
return 0;
|
|
2780
|
+
}
|
|
2781
|
+
step = previous;
|
|
2782
|
+
continue;
|
|
2783
|
+
}
|
|
2784
|
+
operationMode = modeChoice;
|
|
2785
|
+
step = !scope ? "scope" : operationMode === "provider-assisted" ? "provider" : void 0;
|
|
2786
|
+
continue;
|
|
2787
|
+
}
|
|
2788
|
+
if (step === "scope") {
|
|
2789
|
+
const scopeChoice = await promptForScope({
|
|
2790
|
+
io,
|
|
2791
|
+
runtime,
|
|
2792
|
+
cwd: options.cwd,
|
|
2793
|
+
homeDir: options.homeDir
|
|
2794
|
+
});
|
|
2795
|
+
if (scopeChoice === PROMPT_BACK) {
|
|
2796
|
+
const previous = getPreviousEditableStep("scope");
|
|
2797
|
+
if (!previous) {
|
|
2798
|
+
io.write(`
|
|
2799
|
+
${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
|
|
2800
|
+
`);
|
|
2801
|
+
return 0;
|
|
2802
|
+
}
|
|
2803
|
+
step = previous;
|
|
2804
|
+
continue;
|
|
2805
|
+
}
|
|
2806
|
+
scope = scopeChoice;
|
|
2807
|
+
step = operationMode === "provider-assisted" ? "provider" : void 0;
|
|
2808
|
+
continue;
|
|
2809
|
+
}
|
|
2810
|
+
if (scope === "repo") {
|
|
2811
|
+
io.write(
|
|
2812
|
+
`
|
|
2813
|
+
${createPresentation(io.stdoutIsTTY).note("Local only applies to the runtime instructions in this repo. Provider fallback config is still machine-wide so sift can reuse it anywhere.")}
|
|
2814
|
+
`
|
|
2815
|
+
);
|
|
2816
|
+
}
|
|
2817
|
+
io.write(`
|
|
2818
|
+
${createPresentation(io.stdoutIsTTY).info("Next: provider setup. Press Esc at any step to go back.")}
|
|
2819
|
+
`);
|
|
2820
|
+
const setupStatus = await configSetup({
|
|
2821
|
+
io,
|
|
2822
|
+
env: process.env,
|
|
2823
|
+
embedded: true,
|
|
2824
|
+
forcedMode: "provider-assisted",
|
|
2825
|
+
targetPath: getDefaultGlobalConfigPath(options.homeDir)
|
|
2826
|
+
});
|
|
2827
|
+
if (setupStatus === CONFIG_SETUP_BACK) {
|
|
2828
|
+
const previous = getPreviousEditableStep("provider");
|
|
2829
|
+
if (!previous) {
|
|
2830
|
+
io.write(`
|
|
2831
|
+
${createPresentation(io.stdoutIsTTY).note("Install canceled before we touched anything.")}
|
|
2832
|
+
`);
|
|
2833
|
+
return 0;
|
|
2834
|
+
}
|
|
2835
|
+
step = previous;
|
|
2836
|
+
continue;
|
|
2837
|
+
}
|
|
2838
|
+
if (setupStatus !== 0) {
|
|
2839
|
+
return setupStatus;
|
|
2840
|
+
}
|
|
2841
|
+
step = void 0;
|
|
2842
|
+
}
|
|
2843
|
+
const nestedIo = createNestedInstallIO(io);
|
|
2844
|
+
for (const agent of getInstallTargets(runtime)) {
|
|
2845
|
+
const status = await installAgent({
|
|
2846
|
+
agent,
|
|
2847
|
+
scope,
|
|
2848
|
+
yes: true,
|
|
2849
|
+
io: nestedIo,
|
|
2850
|
+
operationMode,
|
|
2851
|
+
cwd: options.cwd,
|
|
2852
|
+
homeDir: options.homeDir
|
|
2853
|
+
});
|
|
2854
|
+
if (status !== 0) {
|
|
2855
|
+
return status;
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
writeSuccessSummary({
|
|
2859
|
+
io,
|
|
2860
|
+
version: options.version,
|
|
2861
|
+
runtime,
|
|
2862
|
+
scope,
|
|
2863
|
+
operationMode,
|
|
2864
|
+
cwd: options.cwd,
|
|
2865
|
+
homeDir: options.homeDir
|
|
2866
|
+
});
|
|
2867
|
+
return 0;
|
|
2868
|
+
} finally {
|
|
2869
|
+
io.close?.();
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
|
|
1838
2873
|
// src/commands/doctor.ts
|
|
1839
2874
|
var PLACEHOLDER_API_KEYS = [
|
|
1840
2875
|
"YOUR_API_KEY",
|
|
@@ -1858,16 +2893,21 @@ function isRealApiKey(key) {
|
|
|
1858
2893
|
}
|
|
1859
2894
|
function runDoctor(config, configPath) {
|
|
1860
2895
|
const ui = createPresentation(Boolean(process.stdout.isTTY));
|
|
2896
|
+
const effectiveMode = resolveEffectiveOperationMode(config);
|
|
1861
2897
|
const apiKeyStatus = isRealApiKey(config.provider.apiKey) ? "set" : isPlaceholderApiKey(config.provider.apiKey) ? "placeholder (not a real key)" : "not set";
|
|
1862
2898
|
const lines = [
|
|
1863
2899
|
"sift doctor",
|
|
1864
2900
|
"A quick check for your local setup.",
|
|
1865
|
-
"mode:
|
|
2901
|
+
"mode: operation-mode health check",
|
|
1866
2902
|
ui.labelValue("configPath", configPath ?? "(defaults only)"),
|
|
2903
|
+
ui.labelValue("configuredMode", getOperationModeLabel(config.runtime.operationMode)),
|
|
2904
|
+
ui.labelValue("effectiveMode", getOperationModeLabel(effectiveMode)),
|
|
1867
2905
|
ui.labelValue("provider", config.provider.provider),
|
|
1868
2906
|
ui.labelValue("model", config.provider.model),
|
|
1869
2907
|
ui.labelValue("baseUrl", config.provider.baseUrl),
|
|
1870
2908
|
ui.labelValue("apiKey", apiKeyStatus),
|
|
2909
|
+
ui.labelValue("modeSummary", describeOperationMode(effectiveMode)),
|
|
2910
|
+
ui.labelValue("insufficientBehavior", describeInsufficientBehavior(effectiveMode)),
|
|
1871
2911
|
ui.labelValue("maxCaptureChars", String(config.input.maxCaptureChars)),
|
|
1872
2912
|
ui.labelValue("maxInputChars", String(config.input.maxInputChars)),
|
|
1873
2913
|
ui.labelValue("rawFallback", String(config.runtime.rawFallback))
|
|
@@ -1875,24 +2915,31 @@ function runDoctor(config, configPath) {
|
|
|
1875
2915
|
process.stdout.write(`${lines.join("\n")}
|
|
1876
2916
|
`);
|
|
1877
2917
|
const problems = [];
|
|
1878
|
-
if (
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
if (
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
2918
|
+
if (config.runtime.operationMode === "provider-assisted") {
|
|
2919
|
+
if (!config.provider.baseUrl) {
|
|
2920
|
+
problems.push("Missing provider.baseUrl");
|
|
2921
|
+
}
|
|
2922
|
+
if (!config.provider.model) {
|
|
2923
|
+
problems.push("Missing provider.model");
|
|
2924
|
+
}
|
|
2925
|
+
if ((config.provider.provider === "openai" || config.provider.provider === "openai-compatible" || config.provider.provider === "openrouter") && !isRealApiKey(config.provider.apiKey)) {
|
|
2926
|
+
if (isPlaceholderApiKey(config.provider.apiKey)) {
|
|
2927
|
+
problems.push(`provider.apiKey looks like a placeholder: "${config.provider.apiKey}"`);
|
|
2928
|
+
} else {
|
|
2929
|
+
problems.push("Missing provider.apiKey");
|
|
2930
|
+
}
|
|
2931
|
+
problems.push(
|
|
2932
|
+
`Set one of: ${getProviderApiKeyEnvNames(
|
|
2933
|
+
config.provider.provider,
|
|
2934
|
+
config.provider.baseUrl
|
|
2935
|
+
).join(", ")}`
|
|
2936
|
+
);
|
|
2937
|
+
}
|
|
2938
|
+
if (effectiveMode !== "provider-assisted") {
|
|
2939
|
+
problems.push(
|
|
2940
|
+
"Configured provider-assisted mode cannot activate yet, so sift will fall back to agent-escalation until provider credentials are usable."
|
|
2941
|
+
);
|
|
1889
2942
|
}
|
|
1890
|
-
problems.push(
|
|
1891
|
-
`Set one of: ${getProviderApiKeyEnvNames(
|
|
1892
|
-
config.provider.provider,
|
|
1893
|
-
config.provider.baseUrl
|
|
1894
|
-
).join(", ")}`
|
|
1895
|
-
);
|
|
1896
2943
|
}
|
|
1897
2944
|
if (problems.length > 0) {
|
|
1898
2945
|
if (process.stderr.isTTY) {
|
|
@@ -2749,16 +3796,16 @@ function extractBucketPathCandidates(args) {
|
|
|
2749
3796
|
}
|
|
2750
3797
|
return [...candidates];
|
|
2751
3798
|
}
|
|
2752
|
-
function isConfigPathCandidate(
|
|
2753
|
-
return /^\.github\/workflows\/.+\.(?:yml|yaml)$/i.test(
|
|
2754
|
-
|
|
2755
|
-
) || /^(?:[A-Za-z0-9._/-]+\/)?(?:vitest|jest)\.config\.[A-Za-z0-9._-]+$/i.test(
|
|
3799
|
+
function isConfigPathCandidate(path9) {
|
|
3800
|
+
return /^\.github\/workflows\/.+\.(?:yml|yaml)$/i.test(path9) || /^(?:package\.json|pytest\.ini|pyproject\.toml|tox\.ini|(?:[A-Za-z0-9._/-]+\/)?conftest\.py)$/i.test(
|
|
3801
|
+
path9
|
|
3802
|
+
) || /^(?:[A-Za-z0-9._/-]+\/)?(?:vitest|jest)\.config\.[A-Za-z0-9._-]+$/i.test(path9) || /^(?:[A-Za-z0-9._/-]+\/)?tsconfig(?:\.[A-Za-z0-9_-]+)?\.json$/i.test(path9) || /^[A-Za-z0-9._/-]*config[A-Za-z0-9._/-]*\.(?:json|yml|yaml)$/i.test(path9);
|
|
2756
3803
|
}
|
|
2757
|
-
function isAppPathCandidate(
|
|
2758
|
-
return
|
|
3804
|
+
function isAppPathCandidate(path9) {
|
|
3805
|
+
return path9.startsWith("src/");
|
|
2759
3806
|
}
|
|
2760
|
-
function isTestPathCandidate(
|
|
2761
|
-
return
|
|
3807
|
+
function isTestPathCandidate(path9) {
|
|
3808
|
+
return path9.startsWith("test/") || path9.startsWith("tests/");
|
|
2762
3809
|
}
|
|
2763
3810
|
function looksLikeMatcherLiteralComparison(detail) {
|
|
2764
3811
|
return /\bexpected\b[\s\S]*\bto (?:be|contain)\b/i.test(detail);
|
|
@@ -3321,13 +4368,13 @@ function buildExtendedBucketSearchHint(bucket, anchor) {
|
|
|
3321
4368
|
return detail.replace(/^of\s+/i, "") || anchor.label;
|
|
3322
4369
|
}
|
|
3323
4370
|
if (extended.type === "file_not_found_failure") {
|
|
3324
|
-
const
|
|
3325
|
-
return
|
|
4371
|
+
const path9 = detail.match(/['"]([^'"]+)['"]/)?.[1];
|
|
4372
|
+
return path9 ?? detail;
|
|
3326
4373
|
}
|
|
3327
4374
|
if (extended.type === "permission_denied_failure") {
|
|
3328
|
-
const
|
|
4375
|
+
const path9 = detail.match(/['"]([^'"]+)['"]/)?.[1];
|
|
3329
4376
|
const port = detail.match(/\bport\s+(\d+)\b/i)?.[1];
|
|
3330
|
-
return
|
|
4377
|
+
return path9 ?? (port ? `port ${port}` : detail);
|
|
3331
4378
|
}
|
|
3332
4379
|
return detail;
|
|
3333
4380
|
}
|
|
@@ -4435,7 +5482,7 @@ function buildPrompt(args) {
|
|
|
4435
5482
|
"If per-test or per-module mapping is unclear, fall back to the focused grouped-cause view instead of guessing."
|
|
4436
5483
|
] : [];
|
|
4437
5484
|
const prompt = [
|
|
4438
|
-
"You are Sift, a CLI output reduction assistant for downstream agents and automation.",
|
|
5485
|
+
"You are Sift, a CLI output-guidance and reduction assistant for downstream agents and automation.",
|
|
4439
5486
|
"Hard rules:",
|
|
4440
5487
|
...policy.sharedRules.map((rule) => `- ${rule}`),
|
|
4441
5488
|
"",
|
|
@@ -6213,7 +7260,7 @@ function extractContractDriftEntities(input) {
|
|
|
6213
7260
|
}
|
|
6214
7261
|
function buildContractRepresentativeReason(args) {
|
|
6215
7262
|
if (/openapi/i.test(args.label) && args.entities.apiPaths.length > 0) {
|
|
6216
|
-
const nextPath = args.entities.apiPaths.find((
|
|
7263
|
+
const nextPath = args.entities.apiPaths.find((path9) => !args.usedPaths.has(path9)) ?? args.entities.apiPaths[0];
|
|
6217
7264
|
args.usedPaths.add(nextPath);
|
|
6218
7265
|
return `added path: ${nextPath}`;
|
|
6219
7266
|
}
|
|
@@ -8290,7 +9337,7 @@ function emitStatsFooter(args) {
|
|
|
8290
9337
|
|
|
8291
9338
|
// src/core/testStatusState.ts
|
|
8292
9339
|
import fs6 from "fs";
|
|
8293
|
-
import
|
|
9340
|
+
import path8 from "path";
|
|
8294
9341
|
import { z as z3 } from "zod";
|
|
8295
9342
|
var detailSchema = z3.enum(["standard", "focused", "verbose"]);
|
|
8296
9343
|
var failureBucketTypeSchema = z3.enum([
|
|
@@ -8452,7 +9499,7 @@ function buildBucketSignature(bucket) {
|
|
|
8452
9499
|
]);
|
|
8453
9500
|
}
|
|
8454
9501
|
function basenameMatches(value, matcher) {
|
|
8455
|
-
return matcher.test(
|
|
9502
|
+
return matcher.test(path8.basename(value));
|
|
8456
9503
|
}
|
|
8457
9504
|
function isPytestExecutable(value) {
|
|
8458
9505
|
return basenameMatches(value, /^pytest(?:\.exe)?$/i);
|
|
@@ -8611,7 +9658,7 @@ function buildCachedRunnerState(args) {
|
|
|
8611
9658
|
};
|
|
8612
9659
|
}
|
|
8613
9660
|
function normalizeCwd(value) {
|
|
8614
|
-
return
|
|
9661
|
+
return path8.resolve(value).replace(/\\/g, "/");
|
|
8615
9662
|
}
|
|
8616
9663
|
function buildTestStatusBaselineIdentity(args) {
|
|
8617
9664
|
const cwd = normalizeCwd(args.cwd);
|
|
@@ -8760,7 +9807,7 @@ function tryReadCachedTestStatusRun(statePath = getDefaultTestStatusStatePath())
|
|
|
8760
9807
|
}
|
|
8761
9808
|
}
|
|
8762
9809
|
function writeCachedTestStatusRun(state, statePath = getDefaultTestStatusStatePath()) {
|
|
8763
|
-
fs6.mkdirSync(
|
|
9810
|
+
fs6.mkdirSync(path8.dirname(statePath), {
|
|
8764
9811
|
recursive: true
|
|
8765
9812
|
});
|
|
8766
9813
|
fs6.writeFileSync(statePath, `${JSON.stringify(state, null, 2)}
|
|
@@ -9303,13 +10350,97 @@ function buildCommandPreview(request) {
|
|
|
9303
10350
|
}
|
|
9304
10351
|
return (request.command ?? []).join(" ");
|
|
9305
10352
|
}
|
|
10353
|
+
function detectPackageManagerScriptKind(commandPreview) {
|
|
10354
|
+
const trimmed = commandPreview.trim();
|
|
10355
|
+
if (/^npm(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
|
|
10356
|
+
return "npm";
|
|
10357
|
+
}
|
|
10358
|
+
if (/^pnpm(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
|
|
10359
|
+
return "pnpm";
|
|
10360
|
+
}
|
|
10361
|
+
if (/^yarn(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
|
|
10362
|
+
return "yarn";
|
|
10363
|
+
}
|
|
10364
|
+
if (/^bun(?:\s+--?[^\s]+(?:[=\s][^\s]+)?)?\s+run\s+\S+/i.test(trimmed)) {
|
|
10365
|
+
return "bun";
|
|
10366
|
+
}
|
|
10367
|
+
return null;
|
|
10368
|
+
}
|
|
10369
|
+
function normalizeScriptWrapperOutput(args) {
|
|
10370
|
+
const kind = detectPackageManagerScriptKind(args.commandPreview);
|
|
10371
|
+
if (!kind) {
|
|
10372
|
+
return args.capturedOutput;
|
|
10373
|
+
}
|
|
10374
|
+
const lines = args.capturedOutput.split(/\r?\n/);
|
|
10375
|
+
const trimBlankEdges = () => {
|
|
10376
|
+
while (lines.length > 0 && lines[0].trim() === "") {
|
|
10377
|
+
lines.shift();
|
|
10378
|
+
}
|
|
10379
|
+
while (lines.length > 0 && lines.at(-1).trim() === "") {
|
|
10380
|
+
lines.pop();
|
|
10381
|
+
}
|
|
10382
|
+
};
|
|
10383
|
+
const stripLeadingWrapperNoise = () => {
|
|
10384
|
+
let removed = false;
|
|
10385
|
+
while (lines.length > 0) {
|
|
10386
|
+
const line = lines[0];
|
|
10387
|
+
const trimmed = line.trim();
|
|
10388
|
+
if (trimmed === "") {
|
|
10389
|
+
lines.shift();
|
|
10390
|
+
removed = true;
|
|
10391
|
+
continue;
|
|
10392
|
+
}
|
|
10393
|
+
if (/^(?:npm|pnpm)\s+warn\s+unknown user config\b/i.test(trimmed) || /^(?:npm|pnpm)\s+warn\s+unknown env config\b/i.test(trimmed) || /^npm\s+warn\s+config\b/i.test(trimmed) || /^yarn\s+warning\b/i.test(trimmed) || /^bun\s+warn\b/i.test(trimmed)) {
|
|
10394
|
+
lines.shift();
|
|
10395
|
+
removed = true;
|
|
10396
|
+
continue;
|
|
10397
|
+
}
|
|
10398
|
+
break;
|
|
10399
|
+
}
|
|
10400
|
+
if (removed) {
|
|
10401
|
+
trimBlankEdges();
|
|
10402
|
+
}
|
|
10403
|
+
};
|
|
10404
|
+
trimBlankEdges();
|
|
10405
|
+
stripLeadingWrapperNoise();
|
|
10406
|
+
if (kind === "npm" || kind === "pnpm") {
|
|
10407
|
+
let removed = 0;
|
|
10408
|
+
while (lines.length > 0 && removed < 2 && /^\s*>\s+/.test(lines[0])) {
|
|
10409
|
+
lines.shift();
|
|
10410
|
+
removed += 1;
|
|
10411
|
+
}
|
|
10412
|
+
trimBlankEdges();
|
|
10413
|
+
}
|
|
10414
|
+
if (kind === "yarn") {
|
|
10415
|
+
if (lines[0] && /^\s*yarn run v/i.test(lines[0])) {
|
|
10416
|
+
lines.shift();
|
|
10417
|
+
}
|
|
10418
|
+
if (lines[0] && /^\s*\$\s+/.test(lines[0])) {
|
|
10419
|
+
lines.shift();
|
|
10420
|
+
}
|
|
10421
|
+
trimBlankEdges();
|
|
10422
|
+
if (lines.at(-1) && /^\s*Done in\b/i.test(lines.at(-1))) {
|
|
10423
|
+
lines.pop();
|
|
10424
|
+
}
|
|
10425
|
+
}
|
|
10426
|
+
if (kind === "bun") {
|
|
10427
|
+
if (lines[0] && /^\s*\$\s+/.test(lines[0])) {
|
|
10428
|
+
lines.shift();
|
|
10429
|
+
}
|
|
10430
|
+
}
|
|
10431
|
+
trimBlankEdges();
|
|
10432
|
+
return lines.join("\n");
|
|
10433
|
+
}
|
|
9306
10434
|
function getExecSuccessShortcut(args) {
|
|
9307
10435
|
if (args.exitCode !== 0) {
|
|
9308
10436
|
return null;
|
|
9309
10437
|
}
|
|
9310
|
-
if (args.presetName === "typecheck-summary" && args.
|
|
10438
|
+
if (args.presetName === "typecheck-summary" && args.normalizedOutput.trim() === "") {
|
|
9311
10439
|
return "No type errors.";
|
|
9312
10440
|
}
|
|
10441
|
+
if (args.presetName === "lint-failures" && args.normalizedOutput.trim() === "") {
|
|
10442
|
+
return "No lint failures.";
|
|
10443
|
+
}
|
|
9313
10444
|
return null;
|
|
9314
10445
|
}
|
|
9315
10446
|
async function runExec(request) {
|
|
@@ -9389,6 +10520,10 @@ async function runExec(request) {
|
|
|
9389
10520
|
}
|
|
9390
10521
|
const exitCode = normalizeChildExitCode(childStatus, childSignal);
|
|
9391
10522
|
const capturedOutput = capture.render();
|
|
10523
|
+
const normalizedOutput = normalizeScriptWrapperOutput({
|
|
10524
|
+
commandPreview,
|
|
10525
|
+
capturedOutput
|
|
10526
|
+
});
|
|
9392
10527
|
const autoWatchDetected = !request.watch && looksLikeWatchStream(capturedOutput);
|
|
9393
10528
|
const useWatchFlow = Boolean(request.watch) || autoWatchDetected;
|
|
9394
10529
|
const shouldBuildTestStatusState = isTestStatusPreset && !useWatchFlow;
|
|
@@ -9414,7 +10549,7 @@ async function runExec(request) {
|
|
|
9414
10549
|
const execSuccessShortcut = useWatchFlow ? null : getExecSuccessShortcut({
|
|
9415
10550
|
presetName: request.presetName,
|
|
9416
10551
|
exitCode,
|
|
9417
|
-
|
|
10552
|
+
normalizedOutput
|
|
9418
10553
|
});
|
|
9419
10554
|
if (execSuccessShortcut && !request.dryRun) {
|
|
9420
10555
|
if (request.config.runtime.verbose) {
|
|
@@ -9440,15 +10575,15 @@ async function runExec(request) {
|
|
|
9440
10575
|
if (useWatchFlow) {
|
|
9441
10576
|
let output2 = await runWatch({
|
|
9442
10577
|
...request,
|
|
9443
|
-
stdin:
|
|
10578
|
+
stdin: normalizedOutput
|
|
9444
10579
|
});
|
|
9445
10580
|
if (isInsufficientSignalOutput(output2)) {
|
|
9446
10581
|
output2 = buildInsufficientSignalOutput({
|
|
9447
10582
|
presetName: request.presetName,
|
|
9448
|
-
originalLength: capture.getTotalChars(),
|
|
10583
|
+
originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
|
|
9449
10584
|
truncatedApplied: capture.wasTruncated(),
|
|
9450
10585
|
exitCode,
|
|
9451
|
-
recognizedRunner: detectTestRunner(
|
|
10586
|
+
recognizedRunner: detectTestRunner(normalizedOutput)
|
|
9452
10587
|
});
|
|
9453
10588
|
}
|
|
9454
10589
|
process.stdout.write(`${output2}
|
|
@@ -9487,7 +10622,7 @@ async function runExec(request) {
|
|
|
9487
10622
|
}) : null;
|
|
9488
10623
|
const result = await runSiftWithStats({
|
|
9489
10624
|
...request,
|
|
9490
|
-
stdin:
|
|
10625
|
+
stdin: normalizedOutput,
|
|
9491
10626
|
analysisContext: request.testStatusContext?.remainingMode && request.testStatusContext.remainingMode !== "none" && request.presetName === "test-status" ? [
|
|
9492
10627
|
request.analysisContext,
|
|
9493
10628
|
"Zoom context:",
|
|
@@ -9510,10 +10645,10 @@ async function runExec(request) {
|
|
|
9510
10645
|
if (isInsufficientSignalOutput(output)) {
|
|
9511
10646
|
output = buildInsufficientSignalOutput({
|
|
9512
10647
|
presetName: request.presetName,
|
|
9513
|
-
originalLength: capture.getTotalChars(),
|
|
10648
|
+
originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
|
|
9514
10649
|
truncatedApplied: capture.wasTruncated(),
|
|
9515
10650
|
exitCode,
|
|
9516
|
-
recognizedRunner: detectTestRunner(
|
|
10651
|
+
recognizedRunner: detectTestRunner(normalizedOutput)
|
|
9517
10652
|
});
|
|
9518
10653
|
}
|
|
9519
10654
|
if (request.diff && !request.dryRun && previousCachedRun && currentCachedRun) {
|
|
@@ -9540,10 +10675,10 @@ ${output}`;
|
|
|
9540
10675
|
} else if (isInsufficientSignalOutput(output)) {
|
|
9541
10676
|
output = buildInsufficientSignalOutput({
|
|
9542
10677
|
presetName: request.presetName,
|
|
9543
|
-
originalLength: capture.getTotalChars(),
|
|
10678
|
+
originalLength: normalizedOutput.length > 0 ? normalizedOutput.length : capture.getTotalChars(),
|
|
9544
10679
|
truncatedApplied: capture.wasTruncated(),
|
|
9545
10680
|
exitCode,
|
|
9546
|
-
recognizedRunner: detectTestRunner(
|
|
10681
|
+
recognizedRunner: detectTestRunner(normalizedOutput)
|
|
9547
10682
|
});
|
|
9548
10683
|
}
|
|
9549
10684
|
process.stdout.write(`${output}
|
|
@@ -9654,6 +10789,7 @@ function getPreset(config, name) {
|
|
|
9654
10789
|
var require2 = createRequire(import.meta.url);
|
|
9655
10790
|
var pkg = require2("../package.json");
|
|
9656
10791
|
var defaultCliDeps = {
|
|
10792
|
+
installRuntimeSupport,
|
|
9657
10793
|
installAgent,
|
|
9658
10794
|
removeAgent,
|
|
9659
10795
|
showAgent,
|
|
@@ -10008,7 +11144,7 @@ function createCliApp(args = {}) {
|
|
|
10008
11144
|
});
|
|
10009
11145
|
});
|
|
10010
11146
|
applySharedOptions(
|
|
10011
|
-
cli.command("exec [question]", "Run a command and
|
|
11147
|
+
cli.command("exec [question]", "Run a command and turn noisy output into a smaller first pass for the model").allowUnknownOptions()
|
|
10012
11148
|
).usage("exec [question] [options] -- <program> [args...]").example('exec "what changed?" -- git diff').example("exec --preset test-status -- npm test").example("exec --preset test-status --diff -- npm test").example('exec --watch "summarize the stream" -- node watcher.js').example('exec --preset infra-risk --shell "terraform plan"').option("--shell <command>", "Execute a shell command string instead of argv mode").option("--preset <name>", "Run a named preset in exec mode").option("--watch", "Treat the command output as a watch/change-summary stream").option("--diff", "Prepend material changes versus the previous matching test-status run").action(async (question, options) => {
|
|
10013
11149
|
if (question === "preset") {
|
|
10014
11150
|
throw new Error("Use 'sift exec --preset <name> -- <program> ...' instead.");
|
|
@@ -10287,6 +11423,14 @@ function createCliApp(args = {}) {
|
|
|
10287
11423
|
stdout.write(`${output}
|
|
10288
11424
|
`);
|
|
10289
11425
|
});
|
|
11426
|
+
cli.command("install [runtime]", "Interactive runtime installer for Codex and Claude").usage("install [runtime] [options]").example("install").example("install codex").example("install codex --scope global --yes").example("install all --scope local --yes").option("--scope <scope>", "Install scope: local | global").option("--yes", "Skip prompts when runtime and scope are already provided").action(async (runtime, options) => {
|
|
11427
|
+
process.exitCode = await deps.installRuntimeSupport({
|
|
11428
|
+
runtime: normalizeInstallRuntime(runtime),
|
|
11429
|
+
scope: normalizeInstallScope(options.scope),
|
|
11430
|
+
yes: Boolean(options.yes),
|
|
11431
|
+
version
|
|
11432
|
+
});
|
|
11433
|
+
});
|
|
10290
11434
|
cli.command("agent <action> [name]", "Agent commands: show | install | remove | status").usage("agent <show|install|remove|status> [name] [options]").example("agent show codex").example("agent show codex --raw").example("agent install codex").example("agent install claude --scope global").example("agent install codex --dry-run").example("agent install codex --dry-run --raw").example("agent status").example("agent remove codex --scope repo").option("--scope <scope>", "Install scope: repo | global").option("--dry-run", "Show a short plan without changing files").option("--raw", "Print the exact managed block or dry-run file content").option("--yes", "Skip confirmation prompts when writing").option("--path <path>", "Explicit target path for install or remove").action(async (action, name, options) => {
|
|
10291
11435
|
const scope = normalizeAgentScope(options.scope);
|
|
10292
11436
|
if (action === "show") {
|
|
@@ -10430,14 +11574,14 @@ function createCliApp(args = {}) {
|
|
|
10430
11574
|
{
|
|
10431
11575
|
title: ui.section("Quick start"),
|
|
10432
11576
|
body: [
|
|
10433
|
-
` ${ui.command("sift
|
|
11577
|
+
` ${ui.command("sift install")}${ui.note(" # choose agent-escalation, provider-assisted, or local-only")}`,
|
|
10434
11578
|
` ${ui.command("sift exec --preset test-status -- npm test")}`,
|
|
10435
11579
|
` ${ui.command("sift exec --preset test-status -- npm test")}${ui.note(" # stop here if standard already shows the main buckets")}`,
|
|
10436
11580
|
` ${ui.command("sift rerun")}${ui.note(" # rerun the cached full suite after a fix")}`,
|
|
10437
11581
|
` ${ui.command("sift rerun --remaining --detail focused")}${ui.note(" # zoom into what is still failing")}`,
|
|
10438
11582
|
` ${ui.command("sift rerun --remaining --detail verbose --show-raw")}`,
|
|
10439
|
-
` ${ui.command(
|
|
10440
|
-
` ${ui.command(
|
|
11583
|
+
` ${ui.command("sift config setup")}${ui.note(" # optional if you want provider-assisted fallback")}`,
|
|
11584
|
+
` ${ui.command("sift install codex --scope global --yes")}`,
|
|
10441
11585
|
` ${ui.command("sift agent install codex --dry-run")}`,
|
|
10442
11586
|
` ${ui.command("sift agent install codex --dry-run --raw")}`,
|
|
10443
11587
|
` ${ui.command("sift agent status")}`,
|