squad-kit 0.11.0 → 0.12.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/CHANGELOG.md +114 -0
- package/README.md +76 -10
- package/SECURITY.md +16 -2
- package/dist/cli.js +2098 -752
- package/dist/console-ui/assets/index-DP-vFdYe.css +1 -0
- package/dist/console-ui/assets/{index-n5R1pqsY.js → index-DYo5Xwvw.js} +149 -149
- package/dist/console-ui/index.html +2 -2
- package/package.json +3 -2
- package/dist/console-ui/assets/index-BBs6jnF_.css +0 -1
package/dist/cli.js
CHANGED
|
@@ -259,9 +259,9 @@ function isSquadExit(err2) {
|
|
|
259
259
|
}
|
|
260
260
|
|
|
261
261
|
// src/commands/init.ts
|
|
262
|
-
import
|
|
263
|
-
import
|
|
264
|
-
import { input as input2, select as select3, checkbox, confirm as
|
|
262
|
+
import fs17 from "fs";
|
|
263
|
+
import path7 from "path";
|
|
264
|
+
import { input as input2, select as select3, checkbox, confirm as confirm6, password as password4 } from "@inquirer/prompts";
|
|
265
265
|
|
|
266
266
|
// src/core/paths.ts
|
|
267
267
|
import path from "path";
|
|
@@ -352,8 +352,158 @@ function ensureGitignore(repoRoot) {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
// src/core/config.ts
|
|
355
|
-
import
|
|
355
|
+
import fs4 from "fs";
|
|
356
356
|
import yaml from "js-yaml";
|
|
357
|
+
|
|
358
|
+
// src/core/planner-auth.ts
|
|
359
|
+
import { execFileSync } from "child_process";
|
|
360
|
+
import fs3 from "fs";
|
|
361
|
+
import os from "os";
|
|
362
|
+
import path3 from "path";
|
|
363
|
+
var PLANNER_AUTH_MODES = ["subscription", "api-key", "auto"];
|
|
364
|
+
var PROVIDER_ENV_VAR = {
|
|
365
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
366
|
+
openai: "OPENAI_API_KEY",
|
|
367
|
+
google: "GOOGLE_API_KEY"
|
|
368
|
+
};
|
|
369
|
+
function buildUnavailableMessage(provider, configuredMode) {
|
|
370
|
+
if (provider === "anthropic") {
|
|
371
|
+
const lead = configuredMode === "api-key" ? "planner.auth.anthropic is `api-key`, but no Anthropic API key resolved." : "No Anthropic credential available.";
|
|
372
|
+
return `${lead} Either log in with your Claude subscription (\`squad auth login\`) or save an API key (\`squad config set planner\`, or export ANTHROPIC_API_KEY).`;
|
|
373
|
+
}
|
|
374
|
+
return `No ${provider} API key available. Run \`squad config set planner\` to save one, or export ${PROVIDER_ENV_VAR[provider]}.`;
|
|
375
|
+
}
|
|
376
|
+
var PlannerAuthUnavailableError = class extends Error {
|
|
377
|
+
constructor(provider, configuredMode) {
|
|
378
|
+
super(buildUnavailableMessage(provider, configuredMode));
|
|
379
|
+
this.provider = provider;
|
|
380
|
+
this.configuredMode = configuredMode;
|
|
381
|
+
this.name = "PlannerAuthUnavailableError";
|
|
382
|
+
}
|
|
383
|
+
provider;
|
|
384
|
+
configuredMode;
|
|
385
|
+
};
|
|
386
|
+
var KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
387
|
+
var NOT_LOGGED_IN = { present: false, hint: "none", detail: "no Claude login detected" };
|
|
388
|
+
function claudeConfigDir() {
|
|
389
|
+
const override = process.env.CLAUDE_CONFIG_DIR;
|
|
390
|
+
if (override && override.trim().length > 0) return path3.resolve(process.cwd(), override.trim());
|
|
391
|
+
try {
|
|
392
|
+
return path3.join(os.homedir(), ".claude");
|
|
393
|
+
} catch {
|
|
394
|
+
return void 0;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
function keychainItemExists() {
|
|
398
|
+
try {
|
|
399
|
+
execFileSync("security", ["find-generic-password", "-s", KEYCHAIN_SERVICE], {
|
|
400
|
+
stdio: "ignore",
|
|
401
|
+
timeout: 2e3
|
|
402
|
+
});
|
|
403
|
+
return true;
|
|
404
|
+
} catch {
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
function detectClaudeLogin(secrets) {
|
|
409
|
+
const envToken = process.env.CLAUDE_CODE_OAUTH_TOKEN;
|
|
410
|
+
if (envToken && envToken.trim().length > 0) {
|
|
411
|
+
return {
|
|
412
|
+
present: true,
|
|
413
|
+
hint: "oauth-token-env",
|
|
414
|
+
detail: "CLAUDE_CODE_OAUTH_TOKEN",
|
|
415
|
+
token: envToken.trim()
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const savedToken = secrets?.planner?.anthropicOauthToken;
|
|
419
|
+
if (savedToken && savedToken.trim().length > 0) {
|
|
420
|
+
return {
|
|
421
|
+
present: true,
|
|
422
|
+
hint: "oauth-token-secrets",
|
|
423
|
+
detail: ".squad/secrets.yaml",
|
|
424
|
+
token: savedToken.trim()
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
if (process.platform === "darwin") {
|
|
428
|
+
return keychainItemExists() ? { present: true, hint: "credential-store", detail: "macOS Keychain" } : NOT_LOGGED_IN;
|
|
429
|
+
}
|
|
430
|
+
const configDir = claudeConfigDir();
|
|
431
|
+
if (!configDir) return NOT_LOGGED_IN;
|
|
432
|
+
const credentialsFile = path3.join(configDir, ".credentials.json");
|
|
433
|
+
try {
|
|
434
|
+
if (fs3.existsSync(credentialsFile)) {
|
|
435
|
+
return { present: true, hint: "credential-store", detail: credentialsFile };
|
|
436
|
+
}
|
|
437
|
+
} catch {
|
|
438
|
+
return NOT_LOGGED_IN;
|
|
439
|
+
}
|
|
440
|
+
return NOT_LOGGED_IN;
|
|
441
|
+
}
|
|
442
|
+
function apiKeyAuth(apiKey, reason) {
|
|
443
|
+
return {
|
|
444
|
+
mode: "api-key",
|
|
445
|
+
reason,
|
|
446
|
+
key: apiKey.value,
|
|
447
|
+
source: apiKey.source,
|
|
448
|
+
detail: apiKey.detail
|
|
449
|
+
};
|
|
450
|
+
}
|
|
451
|
+
function subscriptionAuth(login, reason) {
|
|
452
|
+
if (login.hint === "oauth-token-env" || login.hint === "oauth-token-secrets") {
|
|
453
|
+
return {
|
|
454
|
+
mode: "subscription",
|
|
455
|
+
reason,
|
|
456
|
+
oauthToken: login.token,
|
|
457
|
+
oauthTokenSource: login.hint === "oauth-token-env" ? "env" : "secrets",
|
|
458
|
+
loginHint: login.hint
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
mode: "subscription",
|
|
463
|
+
reason,
|
|
464
|
+
loginHint: login.hint === "credential-store" ? "credential-store" : "assumed"
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
function resolvePlannerAuth(input5) {
|
|
468
|
+
const { provider, apiKey, login } = input5;
|
|
469
|
+
const configuredMode = input5.configuredMode ?? "auto";
|
|
470
|
+
if (provider !== "anthropic") {
|
|
471
|
+
if (apiKey) return apiKeyAuth(apiKey, "provider-is-api-key-only");
|
|
472
|
+
throw new PlannerAuthUnavailableError(provider, configuredMode);
|
|
473
|
+
}
|
|
474
|
+
if (configuredMode === "api-key") {
|
|
475
|
+
if (apiKey) return apiKeyAuth(apiKey, "explicit-config");
|
|
476
|
+
throw new PlannerAuthUnavailableError(provider, configuredMode);
|
|
477
|
+
}
|
|
478
|
+
if (configuredMode === "subscription") {
|
|
479
|
+
return subscriptionAuth(login, "explicit-config");
|
|
480
|
+
}
|
|
481
|
+
if (login.present) {
|
|
482
|
+
return subscriptionAuth(login, login.hint === "credential-store" ? "auto-login-detected" : "auto-oauth-token");
|
|
483
|
+
}
|
|
484
|
+
if (apiKey) return apiKeyAuth(apiKey, "auto-fallback-api-key");
|
|
485
|
+
throw new PlannerAuthUnavailableError(provider, configuredMode);
|
|
486
|
+
}
|
|
487
|
+
function describeAuth(auth2) {
|
|
488
|
+
if (auth2.mode === "api-key") {
|
|
489
|
+
return { mode: "api-key", reason: auth2.reason, credentialHint: auth2.detail };
|
|
490
|
+
}
|
|
491
|
+
return { mode: "subscription", reason: auth2.reason, credentialHint: loginCredentialHint(auth2.loginHint) };
|
|
492
|
+
}
|
|
493
|
+
function loginCredentialHint(loginHint) {
|
|
494
|
+
switch (loginHint) {
|
|
495
|
+
case "oauth-token-env":
|
|
496
|
+
return "Claude login (CLAUDE_CODE_OAUTH_TOKEN)";
|
|
497
|
+
case "oauth-token-secrets":
|
|
498
|
+
return "Claude login (.squad/secrets.yaml)";
|
|
499
|
+
case "credential-store":
|
|
500
|
+
return process.platform === "darwin" ? "Claude login (macOS Keychain)" : "Claude login (~/.claude/.credentials.json)";
|
|
501
|
+
case "assumed":
|
|
502
|
+
return "Claude login (assumed \u2014 not verified locally)";
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// src/core/config.ts
|
|
357
507
|
var DEFAULT_CONFIG = {
|
|
358
508
|
version: 1,
|
|
359
509
|
project: {
|
|
@@ -366,7 +516,16 @@ var DEFAULT_CONFIG = {
|
|
|
366
516
|
agents: [],
|
|
367
517
|
planner: void 0
|
|
368
518
|
};
|
|
369
|
-
var FORBIDDEN_KEYS = [
|
|
519
|
+
var FORBIDDEN_KEYS = [
|
|
520
|
+
"apikey",
|
|
521
|
+
"api_key",
|
|
522
|
+
"token",
|
|
523
|
+
"oauthtoken",
|
|
524
|
+
"anthropicoauthtoken",
|
|
525
|
+
"secret",
|
|
526
|
+
"credential",
|
|
527
|
+
"credentials"
|
|
528
|
+
];
|
|
370
529
|
var DEFAULT_PLANNER_MAX_OUTPUT_TOKENS = 16384;
|
|
371
530
|
var MIN_PLANNER_MAX_OUTPUT_TOKENS = 1024;
|
|
372
531
|
var MAX_PLANNER_MAX_OUTPUT_TOKENS = 128e3;
|
|
@@ -379,23 +538,34 @@ function clampPlannerMaxOut(raw) {
|
|
|
379
538
|
Math.max(MIN_PLANNER_MAX_OUTPUT_TOKENS, Math.round(n))
|
|
380
539
|
);
|
|
381
540
|
}
|
|
382
|
-
function rejectSecretsInYaml(node, configFile,
|
|
541
|
+
function rejectSecretsInYaml(node, configFile, path40 = []) {
|
|
383
542
|
if (node === null || typeof node !== "object") return;
|
|
384
543
|
for (const [key, value] of Object.entries(node)) {
|
|
385
544
|
if (FORBIDDEN_KEYS.includes(key.toLowerCase())) {
|
|
386
545
|
throw new Error(
|
|
387
|
-
`Refusing to load ${configFile}: key "${[...
|
|
546
|
+
`Refusing to load ${configFile}: key "${[...path40, key].join(".")}" looks like a secret. Run \`squad config set planner\` or \`squad config set tracker\` to save it to .squad/secrets.yaml, then remove the key from config.yaml. Provider keys also work via ANTHROPIC_API_KEY / OPENAI_API_KEY / GOOGLE_API_KEY.`
|
|
388
547
|
);
|
|
389
548
|
}
|
|
390
|
-
if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...
|
|
549
|
+
if (typeof value === "object") rejectSecretsInYaml(value, configFile, [...path40, key]);
|
|
391
550
|
}
|
|
392
551
|
}
|
|
393
|
-
function
|
|
552
|
+
function normalizeAuthMode(raw, configFile) {
|
|
553
|
+
if (raw === void 0 || raw === null) return void 0;
|
|
554
|
+
const value = typeof raw === "string" ? raw.trim().toLowerCase() : "";
|
|
555
|
+
if (PLANNER_AUTH_MODES.includes(value)) return value;
|
|
556
|
+
throw new Error(
|
|
557
|
+
`Invalid planner.auth.anthropic ${JSON.stringify(raw)} in ${configFile}: must be ${PLANNER_AUTH_MODES.join(", ")}. Run \`squad config set planner\` to fix it.`
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
function mergePlanner(base, override, configFile) {
|
|
394
561
|
if (!override) return base;
|
|
395
562
|
const merged = {
|
|
396
563
|
enabled: override.enabled ?? base?.enabled ?? false,
|
|
397
564
|
provider: override.provider ?? base?.provider ?? "anthropic",
|
|
398
565
|
mode: override.mode ?? base?.mode ?? "auto",
|
|
566
|
+
auth: {
|
|
567
|
+
anthropic: normalizeAuthMode(override.auth?.anthropic ?? base?.auth?.anthropic, configFile) ?? "auto"
|
|
568
|
+
},
|
|
399
569
|
budget: {
|
|
400
570
|
maxFileReads: override.budget?.maxFileReads ?? base?.budget?.maxFileReads ?? 25,
|
|
401
571
|
maxContextBytes: override.budget?.maxContextBytes ?? base?.budget?.maxContextBytes ?? 5e4,
|
|
@@ -480,7 +650,7 @@ function parseConfig(raw, configFile) {
|
|
|
480
650
|
);
|
|
481
651
|
}
|
|
482
652
|
rejectSecretsInYaml(parsed, configFile);
|
|
483
|
-
const merged = mergeConfig(DEFAULT_CONFIG, parsed);
|
|
653
|
+
const merged = mergeConfig(DEFAULT_CONFIG, parsed, configFile);
|
|
484
654
|
validateModelOverride(merged.planner?.modelOverride, configFile);
|
|
485
655
|
if (merged.planner?.cache !== void 0) {
|
|
486
656
|
const c = merged.planner.cache;
|
|
@@ -527,7 +697,7 @@ function parseConfig(raw, configFile) {
|
|
|
527
697
|
return merged;
|
|
528
698
|
}
|
|
529
699
|
function loadConfig(configFile) {
|
|
530
|
-
const raw =
|
|
700
|
+
const raw = fs4.readFileSync(configFile, "utf8");
|
|
531
701
|
return parseConfig(raw, configFile);
|
|
532
702
|
}
|
|
533
703
|
function serializeConfig(config2) {
|
|
@@ -536,32 +706,32 @@ function serializeConfig(config2) {
|
|
|
536
706
|
function saveConfig(configFile, config2) {
|
|
537
707
|
const body = serializeConfig(config2);
|
|
538
708
|
parseConfig(body, configFile);
|
|
539
|
-
|
|
709
|
+
fs4.writeFileSync(configFile, body, "utf8");
|
|
540
710
|
}
|
|
541
|
-
function mergeConfig(base, override) {
|
|
711
|
+
function mergeConfig(base, override, configFile) {
|
|
542
712
|
return {
|
|
543
713
|
version: override.version ?? base.version,
|
|
544
714
|
project: { ...base.project, ...override.project ?? {} },
|
|
545
715
|
tracker: { ...base.tracker, ...override.tracker ?? {} },
|
|
546
716
|
naming: { ...base.naming, ...override.naming ?? {} },
|
|
547
717
|
agents: override.agents ?? base.agents,
|
|
548
|
-
planner: mergePlanner(base.planner, override.planner)
|
|
718
|
+
planner: mergePlanner(base.planner, override.planner, configFile)
|
|
549
719
|
};
|
|
550
720
|
}
|
|
551
721
|
|
|
552
722
|
// src/core/planner-models.ts
|
|
553
|
-
import
|
|
723
|
+
import fs6 from "fs";
|
|
554
724
|
|
|
555
725
|
// src/core/secrets.ts
|
|
556
|
-
import
|
|
557
|
-
import
|
|
726
|
+
import fs5 from "fs";
|
|
727
|
+
import path4 from "path";
|
|
558
728
|
import yaml2 from "js-yaml";
|
|
559
729
|
var EMPTY = {};
|
|
560
730
|
function loadSecrets(secretsFile) {
|
|
561
|
-
if (!
|
|
731
|
+
if (!fs5.existsSync(secretsFile)) return EMPTY;
|
|
562
732
|
let raw;
|
|
563
733
|
try {
|
|
564
|
-
raw =
|
|
734
|
+
raw = fs5.readFileSync(secretsFile, "utf8");
|
|
565
735
|
} catch (err2) {
|
|
566
736
|
throw new Error(
|
|
567
737
|
`Failed to read ${secretsFile}: ${err2.message}. Run \`squad doctor\` to diagnose, or delete the file and run \`squad config set planner\` or \`squad config set tracker\` to re-enter credentials.`
|
|
@@ -584,15 +754,15 @@ function loadSecrets(secretsFile) {
|
|
|
584
754
|
return parsed;
|
|
585
755
|
}
|
|
586
756
|
function saveSecrets(secretsFile, secrets) {
|
|
587
|
-
|
|
757
|
+
fs5.mkdirSync(path4.dirname(secretsFile), { recursive: true });
|
|
588
758
|
const body = yaml2.dump(secrets, { lineWidth: 100, noRefs: true, sortKeys: false });
|
|
589
|
-
|
|
759
|
+
fs5.writeFileSync(secretsFile, body, "utf8");
|
|
590
760
|
tightenPermissions(secretsFile);
|
|
591
761
|
}
|
|
592
762
|
function tightenPermissions(file) {
|
|
593
763
|
if (process.platform === "win32") return;
|
|
594
764
|
try {
|
|
595
|
-
|
|
765
|
+
fs5.chmodSync(file, 384);
|
|
596
766
|
} catch {
|
|
597
767
|
}
|
|
598
768
|
}
|
|
@@ -667,7 +837,7 @@ function resolveProviderKey(provider) {
|
|
|
667
837
|
const root = findSquadRoot();
|
|
668
838
|
if (root) {
|
|
669
839
|
const paths = buildPaths(root);
|
|
670
|
-
if (
|
|
840
|
+
if (fs6.existsSync(paths.secretsFile)) {
|
|
671
841
|
const secrets = loadSecrets(paths.secretsFile);
|
|
672
842
|
const fromFile = secrets.planner?.[provider];
|
|
673
843
|
if (fromFile) return { value: fromFile, source: "secrets", detail: ".squad/secrets.yaml" };
|
|
@@ -678,68 +848,89 @@ function resolveProviderKey(provider) {
|
|
|
678
848
|
function readProviderKey(provider) {
|
|
679
849
|
return resolveProviderKey(provider)?.value;
|
|
680
850
|
}
|
|
681
|
-
function
|
|
851
|
+
function resolveProviderKeyForPaths(paths, provider) {
|
|
682
852
|
const envVar = providerEnvVar(provider);
|
|
683
853
|
const envValue = process.env[envVar];
|
|
684
|
-
if (envValue) return envValue;
|
|
854
|
+
if (envValue) return { value: envValue, source: "env", detail: envVar };
|
|
685
855
|
const fallback = process.env.SQUAD_PLANNER_API_KEY;
|
|
686
|
-
if (fallback) return fallback;
|
|
687
|
-
if (
|
|
856
|
+
if (fallback) return { value: fallback, source: "fallback-env", detail: "SQUAD_PLANNER_API_KEY" };
|
|
857
|
+
if (fs6.existsSync(paths.secretsFile)) {
|
|
688
858
|
const secrets = loadSecrets(paths.secretsFile);
|
|
689
859
|
const fromFile = secrets.planner?.[provider];
|
|
690
|
-
if (fromFile) return fromFile;
|
|
860
|
+
if (fromFile) return { value: fromFile, source: "secrets", detail: ".squad/secrets.yaml" };
|
|
691
861
|
}
|
|
692
862
|
return void 0;
|
|
693
863
|
}
|
|
864
|
+
function configuredAuthMode(provider, config2) {
|
|
865
|
+
return provider === "anthropic" ? config2?.auth?.anthropic : void 0;
|
|
866
|
+
}
|
|
867
|
+
function resolvePlannerAuthForCwd(provider, config2) {
|
|
868
|
+
const root = findSquadRoot();
|
|
869
|
+
const secrets = root ? loadSecrets(buildPaths(root).secretsFile) : void 0;
|
|
870
|
+
return resolvePlannerAuth({
|
|
871
|
+
provider,
|
|
872
|
+
configuredMode: configuredAuthMode(provider, config2),
|
|
873
|
+
apiKey: resolveProviderKey(provider),
|
|
874
|
+
login: detectClaudeLogin(secrets)
|
|
875
|
+
});
|
|
876
|
+
}
|
|
877
|
+
function resolvePlannerAuthForPaths(paths, provider, config2) {
|
|
878
|
+
return resolvePlannerAuth({
|
|
879
|
+
provider,
|
|
880
|
+
configuredMode: configuredAuthMode(provider, config2),
|
|
881
|
+
apiKey: resolveProviderKeyForPaths(paths, provider),
|
|
882
|
+
login: detectClaudeLogin(loadSecrets(paths.secretsFile))
|
|
883
|
+
});
|
|
884
|
+
}
|
|
694
885
|
|
|
695
886
|
// src/utils/fs.ts
|
|
696
|
-
import
|
|
697
|
-
import
|
|
887
|
+
import fs7 from "fs";
|
|
888
|
+
import path5 from "path";
|
|
698
889
|
import { fileURLToPath } from "url";
|
|
699
890
|
function readFile(file) {
|
|
700
|
-
return
|
|
891
|
+
return fs7.readFileSync(file, "utf8");
|
|
701
892
|
}
|
|
702
893
|
function writeFileSafe(file, content, overwrite = false) {
|
|
703
|
-
if (!overwrite &&
|
|
704
|
-
|
|
705
|
-
|
|
894
|
+
if (!overwrite && fs7.existsSync(file)) return false;
|
|
895
|
+
fs7.mkdirSync(path5.dirname(file), { recursive: true });
|
|
896
|
+
fs7.writeFileSync(file, content, "utf8");
|
|
706
897
|
return true;
|
|
707
898
|
}
|
|
708
899
|
function copyTree(srcDir, destDir, overwrite = false) {
|
|
709
|
-
|
|
710
|
-
for (const entry of
|
|
711
|
-
const src =
|
|
712
|
-
const dest =
|
|
900
|
+
fs7.mkdirSync(destDir, { recursive: true });
|
|
901
|
+
for (const entry of fs7.readdirSync(srcDir, { withFileTypes: true })) {
|
|
902
|
+
const src = path5.join(srcDir, entry.name);
|
|
903
|
+
const dest = path5.join(destDir, entry.name);
|
|
713
904
|
if (entry.isDirectory()) {
|
|
714
905
|
copyTree(src, dest, overwrite);
|
|
715
906
|
} else if (entry.isFile()) {
|
|
716
|
-
if (!overwrite &&
|
|
717
|
-
|
|
907
|
+
if (!overwrite && fs7.existsSync(dest)) continue;
|
|
908
|
+
fs7.copyFileSync(src, dest);
|
|
718
909
|
}
|
|
719
910
|
}
|
|
720
911
|
}
|
|
721
912
|
function packageRoot() {
|
|
722
|
-
const here =
|
|
913
|
+
const here = path5.dirname(fileURLToPath(import.meta.url));
|
|
723
914
|
let dir = here;
|
|
724
915
|
for (let i = 0; i < 6; i++) {
|
|
725
|
-
if (
|
|
916
|
+
if (fs7.existsSync(path5.join(dir, "package.json")) && fs7.existsSync(path5.join(dir, "templates"))) {
|
|
726
917
|
return dir;
|
|
727
918
|
}
|
|
728
|
-
dir =
|
|
919
|
+
dir = path5.dirname(dir);
|
|
729
920
|
}
|
|
730
921
|
throw new Error(`Could not locate squad-kit package root from ${here}`);
|
|
731
922
|
}
|
|
732
923
|
function templatesDir() {
|
|
733
|
-
return
|
|
924
|
+
return path5.join(packageRoot(), "templates");
|
|
734
925
|
}
|
|
735
926
|
function readBundledPrompt(name) {
|
|
736
|
-
const file =
|
|
737
|
-
if (!
|
|
927
|
+
const file = path5.join(templatesDir(), "prompts", name);
|
|
928
|
+
if (!fs7.existsSync(file)) {
|
|
738
929
|
throw new Error(
|
|
739
930
|
`Bundled prompt "${name}" not found at ${file}. This indicates a broken squad-kit install \u2014 reinstall with: pnpm add -g squad-kit`
|
|
740
931
|
);
|
|
741
932
|
}
|
|
742
|
-
return
|
|
933
|
+
return fs7.readFileSync(file, "utf8");
|
|
743
934
|
}
|
|
744
935
|
|
|
745
936
|
// src/core/template.ts
|
|
@@ -755,7 +946,7 @@ function escapeTemplateValue(s) {
|
|
|
755
946
|
}
|
|
756
947
|
|
|
757
948
|
// src/commands/config/show.ts
|
|
758
|
-
import
|
|
949
|
+
import fs8 from "fs";
|
|
759
950
|
|
|
760
951
|
// src/commands/config/masks.ts
|
|
761
952
|
function maskStringKey(value) {
|
|
@@ -866,7 +1057,7 @@ async function runConfigShow(opts) {
|
|
|
866
1057
|
const root = requireSquadRoot();
|
|
867
1058
|
const paths = buildPaths(root);
|
|
868
1059
|
const config2 = loadConfig(paths.configFile);
|
|
869
|
-
const secrets =
|
|
1060
|
+
const secrets = fs8.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
870
1061
|
const payload = buildPayload(config2, secrets);
|
|
871
1062
|
if (opts.json) {
|
|
872
1063
|
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
@@ -877,8 +1068,8 @@ async function runConfigShow(opts) {
|
|
|
877
1068
|
}
|
|
878
1069
|
|
|
879
1070
|
// src/commands/config/set-planner.ts
|
|
880
|
-
import
|
|
881
|
-
import { confirm as
|
|
1071
|
+
import fs12 from "fs";
|
|
1072
|
+
import { confirm as confirm3, password as password3, select } from "@inquirer/prompts";
|
|
882
1073
|
|
|
883
1074
|
// src/tracker/env-overlay.ts
|
|
884
1075
|
function overlayTrackerEnv(base) {
|
|
@@ -978,11 +1169,11 @@ async function probeJiraConnectivity(secrets, config2) {
|
|
|
978
1169
|
const host = jira.host ?? config2.tracker.workspace ?? "";
|
|
979
1170
|
const email = jira.email ?? "";
|
|
980
1171
|
const token = jira.token ?? "";
|
|
981
|
-
const
|
|
1172
|
+
const auth2 = Buffer.from(`${email}:${token}`).toString("base64");
|
|
982
1173
|
const url = jiraMyselfUrl(host);
|
|
983
1174
|
try {
|
|
984
1175
|
const res = await fetch(url, {
|
|
985
|
-
headers: { Authorization: `Basic ${
|
|
1176
|
+
headers: { Authorization: `Basic ${auth2}`, Accept: "application/json" }
|
|
986
1177
|
});
|
|
987
1178
|
if (res.ok) return { ok: true };
|
|
988
1179
|
return { ok: false, status: res.status, detail: (await res.text()).slice(0, 200) };
|
|
@@ -994,10 +1185,10 @@ async function probeAzureConnectivity(secrets, config2) {
|
|
|
994
1185
|
const az = overlayTrackerEnv(secrets).tracker?.azure ?? {};
|
|
995
1186
|
const org = az.organization ?? config2.tracker.workspace ?? "";
|
|
996
1187
|
const pat = az.pat ?? "";
|
|
997
|
-
const
|
|
1188
|
+
const auth2 = Buffer.from(`:${pat}`).toString("base64");
|
|
998
1189
|
const url = `https://dev.azure.com/${encodeURIComponent(org)}/_apis/projects?api-version=7.1`;
|
|
999
1190
|
try {
|
|
1000
|
-
const res = await fetch(url, { headers: { Authorization: `Basic ${
|
|
1191
|
+
const res = await fetch(url, { headers: { Authorization: `Basic ${auth2}` } });
|
|
1001
1192
|
if (res.ok) return { ok: true };
|
|
1002
1193
|
return { ok: false, status: res.status, detail: (await res.text()).slice(0, 200) };
|
|
1003
1194
|
} catch (err2) {
|
|
@@ -1026,6 +1217,345 @@ async function probeGitHubConnectivity(secrets, _config) {
|
|
|
1026
1217
|
}
|
|
1027
1218
|
}
|
|
1028
1219
|
|
|
1220
|
+
// src/core/claude-binary.ts
|
|
1221
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
1222
|
+
import fs9 from "fs";
|
|
1223
|
+
import { createRequire } from "module";
|
|
1224
|
+
function claudeBinaryPackageSpecs(platform = process.platform, arch = process.arch) {
|
|
1225
|
+
const packages = platform === "linux" ? [
|
|
1226
|
+
`@anthropic-ai/claude-agent-sdk-linux-${arch}-musl`,
|
|
1227
|
+
`@anthropic-ai/claude-agent-sdk-linux-${arch}`
|
|
1228
|
+
] : [`@anthropic-ai/claude-agent-sdk-${platform}-${arch}`];
|
|
1229
|
+
const fileNames = platform === "win32" ? ["claude.exe", "claude"] : ["claude"];
|
|
1230
|
+
return packages.flatMap((pkg) => fileNames.map((file) => `${pkg}/${file}`));
|
|
1231
|
+
}
|
|
1232
|
+
function requireAnchors() {
|
|
1233
|
+
const own = createRequire(import.meta.url);
|
|
1234
|
+
const anchors = [own];
|
|
1235
|
+
try {
|
|
1236
|
+
const sdkEntry = own.resolve("@anthropic-ai/claude-agent-sdk");
|
|
1237
|
+
anchors.push(createRequire(sdkEntry));
|
|
1238
|
+
const realEntry = fs9.realpathSync(sdkEntry);
|
|
1239
|
+
if (realEntry !== sdkEntry) anchors.push(createRequire(realEntry));
|
|
1240
|
+
} catch {
|
|
1241
|
+
}
|
|
1242
|
+
return anchors;
|
|
1243
|
+
}
|
|
1244
|
+
function resolveBundled() {
|
|
1245
|
+
const specs = claudeBinaryPackageSpecs();
|
|
1246
|
+
for (const req of requireAnchors()) {
|
|
1247
|
+
for (const spec of specs) {
|
|
1248
|
+
try {
|
|
1249
|
+
const resolved = req.resolve(spec);
|
|
1250
|
+
if (fs9.existsSync(resolved)) return resolved;
|
|
1251
|
+
} catch {
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
return void 0;
|
|
1256
|
+
}
|
|
1257
|
+
function resolveOnPath() {
|
|
1258
|
+
const finder = process.platform === "win32" ? "where" : "which";
|
|
1259
|
+
try {
|
|
1260
|
+
const out = execFileSync2(finder, ["claude"], {
|
|
1261
|
+
encoding: "utf8",
|
|
1262
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
1263
|
+
timeout: 3e3
|
|
1264
|
+
});
|
|
1265
|
+
const first = out.split(/\r?\n/).map((l) => l.trim()).find((l) => l.length > 0);
|
|
1266
|
+
if (first && fs9.existsSync(first)) return first;
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
return void 0;
|
|
1270
|
+
}
|
|
1271
|
+
var cached;
|
|
1272
|
+
function resolveClaudeBinary() {
|
|
1273
|
+
if (cached) return cached.value;
|
|
1274
|
+
const bundled = resolveBundled();
|
|
1275
|
+
if (bundled) {
|
|
1276
|
+
cached = { value: { path: bundled, source: "bundled" } };
|
|
1277
|
+
return cached.value;
|
|
1278
|
+
}
|
|
1279
|
+
const onPath = resolveOnPath();
|
|
1280
|
+
cached = { value: onPath ? { path: onPath, source: "path" } : void 0 };
|
|
1281
|
+
return cached.value;
|
|
1282
|
+
}
|
|
1283
|
+
var MISSING_CLAUDE_BINARY_MESSAGE = "Could not find a `claude` executable. Reinstall squad-kit so the Agent SDK's platform binary is present, or install Claude Code and ensure `claude` is on your PATH.";
|
|
1284
|
+
|
|
1285
|
+
// src/planner/provider-errors.ts
|
|
1286
|
+
function detectModelNotFound(provider, model, status, rawBody) {
|
|
1287
|
+
if (status !== 404) return void 0;
|
|
1288
|
+
const body = rawBody.toLowerCase();
|
|
1289
|
+
const hints = ["not_found_error", "model_not_found", "not_found", "model:"];
|
|
1290
|
+
if (!hints.some((h) => body.includes(h))) return void 0;
|
|
1291
|
+
return { provider, model, status, rawBody };
|
|
1292
|
+
}
|
|
1293
|
+
function modelNotFoundMessage(err2) {
|
|
1294
|
+
return [
|
|
1295
|
+
`The ${err2.provider} planner model "${err2.model}" is no longer available.`,
|
|
1296
|
+
"",
|
|
1297
|
+
"Recovery options:",
|
|
1298
|
+
` 1. Run \`squad upgrade\` to install a newer squad-kit (or install manually from npm).`,
|
|
1299
|
+
` 2. Run \`squad config set planner --provider openai\` (or another \`--provider\`) to switch models via a different provider, then save a key when prompted.`,
|
|
1300
|
+
` 3. Run \`squad config set planner\` to set \`planner.modelOverride.${err2.provider}\` to a still-valid id (no raw YAML), then \`squad doctor\` to confirm.`,
|
|
1301
|
+
"",
|
|
1302
|
+
`Raw provider response: ${err2.rawBody.slice(0, 200)}`
|
|
1303
|
+
].join("\n");
|
|
1304
|
+
}
|
|
1305
|
+
function detectRateLimit(provider, status, headers, rawBody) {
|
|
1306
|
+
if (status !== 429) return void 0;
|
|
1307
|
+
return {
|
|
1308
|
+
provider,
|
|
1309
|
+
status: 429,
|
|
1310
|
+
retryAfterSec: parseRetryAfter(headers, rawBody),
|
|
1311
|
+
rawBody
|
|
1312
|
+
};
|
|
1313
|
+
}
|
|
1314
|
+
function parseRetryAfterSec(headers) {
|
|
1315
|
+
const raw = headerGet(headers, "retry-after");
|
|
1316
|
+
if (!raw) return void 0;
|
|
1317
|
+
const sec = Number(raw);
|
|
1318
|
+
if (Number.isFinite(sec) && sec >= 0) return Math.ceil(sec);
|
|
1319
|
+
const asDate = Date.parse(raw);
|
|
1320
|
+
if (Number.isFinite(asDate)) {
|
|
1321
|
+
return Math.max(0, Math.ceil((asDate - Date.now()) / 1e3));
|
|
1322
|
+
}
|
|
1323
|
+
return void 0;
|
|
1324
|
+
}
|
|
1325
|
+
function parseRetryAfter(headers, body) {
|
|
1326
|
+
const fromHeader = parseRetryAfterSec(headers);
|
|
1327
|
+
if (fromHeader !== void 0) return fromHeader;
|
|
1328
|
+
const bodyMatch = /"retryDelay"\s*:\s*"(\d+)(?:\.\d+)?s"/i.exec(body);
|
|
1329
|
+
if (bodyMatch) {
|
|
1330
|
+
const sec = Number(bodyMatch[1]);
|
|
1331
|
+
if (Number.isFinite(sec) && sec >= 0) return sec;
|
|
1332
|
+
}
|
|
1333
|
+
return void 0;
|
|
1334
|
+
}
|
|
1335
|
+
function headerGet(h, name) {
|
|
1336
|
+
if (typeof h.get === "function") {
|
|
1337
|
+
return h.get(name);
|
|
1338
|
+
}
|
|
1339
|
+
const key = name.toLowerCase();
|
|
1340
|
+
const rec = h;
|
|
1341
|
+
for (const k of Object.keys(rec)) {
|
|
1342
|
+
if (k.toLowerCase() === key) return rec[k] ?? null;
|
|
1343
|
+
}
|
|
1344
|
+
return null;
|
|
1345
|
+
}
|
|
1346
|
+
function rateLimitMessage(err2) {
|
|
1347
|
+
const waitHint = err2.retryAfterSec ?? 60;
|
|
1348
|
+
const limitsUrl = {
|
|
1349
|
+
anthropic: "https://console.anthropic.com/settings/limits",
|
|
1350
|
+
openai: "https://platform.openai.com/settings/organization/limits",
|
|
1351
|
+
google: "https://aistudio.google.com/app/plan_information"
|
|
1352
|
+
};
|
|
1353
|
+
const headline = err2.retryAfterSec ? `${err2.provider} rate limit hit \u2014 provider asked us to wait ${err2.retryAfterSec}s before retrying.` : `${err2.provider} rate limit hit.`;
|
|
1354
|
+
const cap = err2.maxRetrySec ?? 90;
|
|
1355
|
+
const retried = err2.retrySkippedReason === "retry_after_too_long" ? `squad-kit did not auto-retry: the provider's ${err2.retryAfterSec}s wait is longer than our ${cap}s cap, so retrying would just burn another request inside the same throttle window.` : err2.retryAlreadyAttempted ? "squad-kit already retried once automatically and was throttled again \u2014 your org is firmly over its per-minute quota." : "squad-kit aborted before retrying.";
|
|
1356
|
+
return [
|
|
1357
|
+
headline,
|
|
1358
|
+
retried,
|
|
1359
|
+
"",
|
|
1360
|
+
"Recovery options:",
|
|
1361
|
+
` 1. Wait ${waitHint}s and rerun \`squad new-plan --api\`. Per-minute limits reset quickly.`,
|
|
1362
|
+
` 2. Switch to a smaller planner model: \`squad config set planner\` (pick a cheaper model id).`,
|
|
1363
|
+
` 3. Tighten \`planner.budget\` in \`.squad/config.yaml\` (smaller \`maxContextBytes\` / \`maxFileReads\`) so each request carries fewer tokens.`,
|
|
1364
|
+
` 4. Upgrade your ${err2.provider} tier: ${limitsUrl[err2.provider]}.`,
|
|
1365
|
+
"",
|
|
1366
|
+
"Full runbook: docs/migrating-from-0.1.md (see \xA78. If something goes wrong).",
|
|
1367
|
+
"",
|
|
1368
|
+
`Raw provider response: ${err2.rawBody.slice(0, 200)}`
|
|
1369
|
+
].join("\n");
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// src/planner/auth-errors.ts
|
|
1373
|
+
var AUTH_SHAPED_SDK_ERRORS = [
|
|
1374
|
+
"authentication_failed",
|
|
1375
|
+
"oauth_org_not_allowed",
|
|
1376
|
+
"billing_error",
|
|
1377
|
+
"rate_limit"
|
|
1378
|
+
];
|
|
1379
|
+
function authErrorContextFrom(auth2) {
|
|
1380
|
+
if (auth2.mode === "api-key") return { mode: "api-key" };
|
|
1381
|
+
return auth2.oauthTokenSource ? { mode: "subscription", oauthTokenSource: auth2.oauthTokenSource } : { mode: "subscription" };
|
|
1382
|
+
}
|
|
1383
|
+
function detectAuthShapedSdkError(raw) {
|
|
1384
|
+
if (typeof raw === "string") {
|
|
1385
|
+
const direct = AUTH_SHAPED_SDK_ERRORS.find((s) => s === raw);
|
|
1386
|
+
if (direct) return direct;
|
|
1387
|
+
return AUTH_SHAPED_SDK_ERRORS.find((s) => raw.includes(s));
|
|
1388
|
+
}
|
|
1389
|
+
if (Array.isArray(raw)) {
|
|
1390
|
+
for (const entry of raw) {
|
|
1391
|
+
const hit = detectAuthShapedSdkError(entry);
|
|
1392
|
+
if (hit) return hit;
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
return void 0;
|
|
1396
|
+
}
|
|
1397
|
+
function authenticationFailedMessage(ctx) {
|
|
1398
|
+
if (ctx.mode === "api-key") {
|
|
1399
|
+
return "Anthropic rejected the planner API key. Save a valid key with `squad config set planner`, or fix the exported ANTHROPIC_API_KEY, then re-run.";
|
|
1400
|
+
}
|
|
1401
|
+
if (ctx.oauthTokenSource === "env") {
|
|
1402
|
+
return "Claude login failed or expired. This run used the token in CLAUDE_CODE_OAUTH_TOKEN \u2014 unset it (or replace it with a fresh `claude setup-token` value), then run `squad auth login` to sign in again and re-run.";
|
|
1403
|
+
}
|
|
1404
|
+
if (ctx.oauthTokenSource === "secrets") {
|
|
1405
|
+
return "Claude login failed or expired. This run used the token saved in `.squad/secrets.yaml` \u2014 run `squad auth logout` to clear it, then `squad auth login` to sign in again, then re-run.";
|
|
1406
|
+
}
|
|
1407
|
+
return "Claude login failed or expired. Run `squad auth login` to sign in again, then re-run.";
|
|
1408
|
+
}
|
|
1409
|
+
function billingErrorMessage(ctx) {
|
|
1410
|
+
if (ctx.mode === "api-key") {
|
|
1411
|
+
return "Anthropic refused the request for billing reasons. Check your credit balance at https://console.anthropic.com/settings/billing, then re-run.";
|
|
1412
|
+
}
|
|
1413
|
+
return "Your Claude plan cannot run this request. Check your plan at claude.ai/settings, or switch to an API key with `squad config set planner`.";
|
|
1414
|
+
}
|
|
1415
|
+
function rateLimitCopy(ctx, rawBody) {
|
|
1416
|
+
if (ctx.mode === "api-key") {
|
|
1417
|
+
return rateLimitMessage({ provider: "anthropic", rawBody });
|
|
1418
|
+
}
|
|
1419
|
+
return "Claude subscription usage limit reached. Planning draws on the same limits as Claude and Claude Code \u2014 wait for your usage window to reset, or switch to an API key for this run with `squad config set planner`.";
|
|
1420
|
+
}
|
|
1421
|
+
function authErrorMessage(signal, ctx, rawBody = "") {
|
|
1422
|
+
switch (signal) {
|
|
1423
|
+
case "authentication_failed":
|
|
1424
|
+
return authenticationFailedMessage(ctx);
|
|
1425
|
+
case "oauth_org_not_allowed":
|
|
1426
|
+
return "Your Claude account's organization is not allowed to use this login. Ask your admin, or switch to an API key with `squad config set planner`.";
|
|
1427
|
+
case "billing_error":
|
|
1428
|
+
return billingErrorMessage(ctx);
|
|
1429
|
+
case "rate_limit":
|
|
1430
|
+
return rateLimitCopy(ctx, rawBody);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// src/planner/runtimes/sdk-env.ts
|
|
1435
|
+
var SUBSCRIPTION_WITHHELD_ENV_KEYS = ["ANTHROPIC_API_KEY", "ANTHROPIC_AUTH_TOKEN"];
|
|
1436
|
+
function buildSdkEnv(auth2) {
|
|
1437
|
+
const env = { ...process.env };
|
|
1438
|
+
if (auth2.mode === "api-key") {
|
|
1439
|
+
env.ANTHROPIC_API_KEY = auth2.key;
|
|
1440
|
+
return env;
|
|
1441
|
+
}
|
|
1442
|
+
for (const key of Object.keys(env)) {
|
|
1443
|
+
if (SUBSCRIPTION_WITHHELD_ENV_KEYS.includes(key.toUpperCase())) delete env[key];
|
|
1444
|
+
}
|
|
1445
|
+
if (auth2.oauthToken) env.CLAUDE_CODE_OAUTH_TOKEN = auth2.oauthToken;
|
|
1446
|
+
return env;
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// src/planner/runtimes/auth-probe.ts
|
|
1450
|
+
var DEFAULT_AUTH_PROBE_TIMEOUT_MS = 1e4;
|
|
1451
|
+
function readAccount(raw) {
|
|
1452
|
+
if (!raw || typeof raw !== "object") return {};
|
|
1453
|
+
const o = raw;
|
|
1454
|
+
const account = {};
|
|
1455
|
+
if (typeof o.email === "string") account.email = o.email;
|
|
1456
|
+
if (typeof o.organization === "string") account.organization = o.organization;
|
|
1457
|
+
if (typeof o.subscriptionType === "string") account.subscriptionType = o.subscriptionType;
|
|
1458
|
+
const apiKeySource = typeof o.apiKeySource === "string" ? o.apiKeySource : void 0;
|
|
1459
|
+
return {
|
|
1460
|
+
account: Object.keys(account).length > 0 ? account : void 0,
|
|
1461
|
+
apiKeySource
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
var EXPIRED_HINT = /expire|refresh|revoked|no longer valid/i;
|
|
1465
|
+
var NO_BINARY_HINT = /native cli binary|executable not found|claude code executable/i;
|
|
1466
|
+
function mapProbeFailure(err2, auth2) {
|
|
1467
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1468
|
+
const signal = detectAuthShapedSdkError(message);
|
|
1469
|
+
if (signal === "authentication_failed") {
|
|
1470
|
+
return {
|
|
1471
|
+
ok: false,
|
|
1472
|
+
kind: EXPIRED_HINT.test(message) ? "expired" : "not-logged-in",
|
|
1473
|
+
// Reuse story 15's mapper rather than writing a second one: the copy a failed probe prints
|
|
1474
|
+
// must match the copy a failed run prints, or the two disagree about the same credential.
|
|
1475
|
+
detail: authErrorMessage("authentication_failed", authErrorContextFrom(auth2))
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
if (signal === "oauth_org_not_allowed") {
|
|
1479
|
+
return {
|
|
1480
|
+
ok: false,
|
|
1481
|
+
kind: "org-not-allowed",
|
|
1482
|
+
detail: authErrorMessage("oauth_org_not_allowed", authErrorContextFrom(auth2))
|
|
1483
|
+
};
|
|
1484
|
+
}
|
|
1485
|
+
if (signal) {
|
|
1486
|
+
return { ok: false, kind: "unknown", detail: authErrorMessage(signal, authErrorContextFrom(auth2)) };
|
|
1487
|
+
}
|
|
1488
|
+
if (NO_BINARY_HINT.test(message)) {
|
|
1489
|
+
return { ok: false, kind: "no-binary", detail: message.slice(0, 300) };
|
|
1490
|
+
}
|
|
1491
|
+
return { ok: false, kind: "unknown", detail: message.slice(0, 300) };
|
|
1492
|
+
}
|
|
1493
|
+
async function probeClaudeAuth(auth2, opts = {}) {
|
|
1494
|
+
if (!resolveClaudeBinary()) {
|
|
1495
|
+
return {
|
|
1496
|
+
ok: false,
|
|
1497
|
+
kind: "no-binary",
|
|
1498
|
+
detail: "No `claude` executable found. Reinstall squad-kit so the Agent SDK platform binary is present, or install Claude Code and put `claude` on your PATH."
|
|
1499
|
+
};
|
|
1500
|
+
}
|
|
1501
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_AUTH_PROBE_TIMEOUT_MS;
|
|
1502
|
+
let releaseInput = () => {
|
|
1503
|
+
};
|
|
1504
|
+
const inputClosed = new Promise((resolve2) => {
|
|
1505
|
+
releaseInput = resolve2;
|
|
1506
|
+
});
|
|
1507
|
+
async function* neverSends() {
|
|
1508
|
+
await inputClosed;
|
|
1509
|
+
}
|
|
1510
|
+
let q;
|
|
1511
|
+
let timer;
|
|
1512
|
+
try {
|
|
1513
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
1514
|
+
q = query({
|
|
1515
|
+
prompt: neverSends(),
|
|
1516
|
+
options: {
|
|
1517
|
+
tools: [],
|
|
1518
|
+
settingSources: [],
|
|
1519
|
+
persistSession: false,
|
|
1520
|
+
maxTurns: 1,
|
|
1521
|
+
env: buildSdkEnv(auth2)
|
|
1522
|
+
}
|
|
1523
|
+
});
|
|
1524
|
+
const timedOut = /* @__PURE__ */ Symbol("timeout");
|
|
1525
|
+
const info2 = await Promise.race([
|
|
1526
|
+
typeof q.accountInfo === "function" ? q.accountInfo() : Promise.resolve(void 0),
|
|
1527
|
+
new Promise((resolve2) => {
|
|
1528
|
+
timer = setTimeout(() => resolve2(timedOut), timeoutMs);
|
|
1529
|
+
})
|
|
1530
|
+
]);
|
|
1531
|
+
if (info2 === timedOut) {
|
|
1532
|
+
return {
|
|
1533
|
+
ok: false,
|
|
1534
|
+
kind: "timeout",
|
|
1535
|
+
detail: `The Claude auth check did not answer within ${timeoutMs}ms.`
|
|
1536
|
+
};
|
|
1537
|
+
}
|
|
1538
|
+
const { account, apiKeySource } = readAccount(info2);
|
|
1539
|
+
if (auth2.mode === "subscription" && !account && !apiKeySource) {
|
|
1540
|
+
return {
|
|
1541
|
+
ok: false,
|
|
1542
|
+
kind: "unknown",
|
|
1543
|
+
detail: "The Claude login did not report an account, so it could not be verified. The credential may be invalid or expired \u2014 run `squad auth login` to sign in again."
|
|
1544
|
+
};
|
|
1545
|
+
}
|
|
1546
|
+
return { ok: true, ...apiKeySource ? { apiKeySource } : {}, ...account ? { account } : {} };
|
|
1547
|
+
} catch (err2) {
|
|
1548
|
+
return mapProbeFailure(err2, auth2);
|
|
1549
|
+
} finally {
|
|
1550
|
+
if (timer) clearTimeout(timer);
|
|
1551
|
+
releaseInput();
|
|
1552
|
+
try {
|
|
1553
|
+
await q?.return?.();
|
|
1554
|
+
} catch {
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
|
|
1029
1559
|
// src/commands/config/shared.ts
|
|
1030
1560
|
import { input, password } from "@inquirer/prompts";
|
|
1031
1561
|
var notEmpty = (v) => v.trim().length > 0 || "required";
|
|
@@ -1106,9 +1636,12 @@ function mergePlannerKeyIntoSecrets(base, provider, key) {
|
|
|
1106
1636
|
}
|
|
1107
1637
|
return mergeSecrets(base, { planner: { google: key } });
|
|
1108
1638
|
}
|
|
1639
|
+
function mergePlannerOauthTokenIntoSecrets(base, token) {
|
|
1640
|
+
return mergeSecrets(base, { planner: { anthropicOauthToken: token } });
|
|
1641
|
+
}
|
|
1109
1642
|
|
|
1110
1643
|
// src/commands/config/unset-planner.ts
|
|
1111
|
-
import
|
|
1644
|
+
import fs10 from "fs";
|
|
1112
1645
|
import { confirm } from "@inquirer/prompts";
|
|
1113
1646
|
async function runConfigUnsetPlanner(opts = {}) {
|
|
1114
1647
|
const root = requireSquadRoot();
|
|
@@ -1125,28 +1658,246 @@ async function runConfigUnsetPlanner(opts = {}) {
|
|
|
1125
1658
|
return;
|
|
1126
1659
|
}
|
|
1127
1660
|
}
|
|
1128
|
-
const next = { ...config2 };
|
|
1129
|
-
delete next.planner;
|
|
1130
|
-
saveConfig(paths.configFile, next);
|
|
1131
|
-
if (opts.removeCredentials) {
|
|
1132
|
-
const s =
|
|
1133
|
-
if (s.planner) {
|
|
1134
|
-
delete s.planner;
|
|
1135
|
-
saveSecrets(paths.secretsFile, s);
|
|
1661
|
+
const next = { ...config2 };
|
|
1662
|
+
delete next.planner;
|
|
1663
|
+
saveConfig(paths.configFile, next);
|
|
1664
|
+
if (opts.removeCredentials) {
|
|
1665
|
+
const s = fs10.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
1666
|
+
if (s.planner) {
|
|
1667
|
+
delete s.planner;
|
|
1668
|
+
saveSecrets(paths.secretsFile, s);
|
|
1669
|
+
}
|
|
1670
|
+
success("Planner disabled; planner credentials were removed from .squad/secrets.yaml.");
|
|
1671
|
+
} else {
|
|
1672
|
+
success("Planner disabled; existing planner keys in .squad/secrets.yaml (if any) were preserved.");
|
|
1673
|
+
}
|
|
1674
|
+
info("Re-enable any time with `squad config set planner`.");
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
// src/commands/auth/login.ts
|
|
1678
|
+
import fs11 from "fs";
|
|
1679
|
+
import { confirm as confirm2, password as password2 } from "@inquirer/prompts";
|
|
1680
|
+
|
|
1681
|
+
// src/core/ci-env.ts
|
|
1682
|
+
function skipExternalProbesInAutomation() {
|
|
1683
|
+
const c = process.env.CI;
|
|
1684
|
+
if (c === "true" || c === "1") return true;
|
|
1685
|
+
if (process.env.GITHUB_ACTIONS) return true;
|
|
1686
|
+
return false;
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// src/commands/auth/setup-token.ts
|
|
1690
|
+
import { spawn } from "child_process";
|
|
1691
|
+
function spawnSetupToken(binaryPath, opts = {}) {
|
|
1692
|
+
const tee = opts.tee !== false;
|
|
1693
|
+
return new Promise((resolve2, reject) => {
|
|
1694
|
+
const child = spawn(binaryPath, ["setup-token"], {
|
|
1695
|
+
stdio: tee ? ["inherit", "pipe", "inherit"] : "inherit",
|
|
1696
|
+
windowsHide: true
|
|
1697
|
+
});
|
|
1698
|
+
let output = "";
|
|
1699
|
+
if (tee && child.stdout) {
|
|
1700
|
+
child.stdout.setEncoding("utf8");
|
|
1701
|
+
child.stdout.on("data", (chunk) => {
|
|
1702
|
+
output += chunk;
|
|
1703
|
+
process.stderr.write(chunk);
|
|
1704
|
+
});
|
|
1705
|
+
}
|
|
1706
|
+
child.once("error", reject);
|
|
1707
|
+
child.once("close", (code, signal) => {
|
|
1708
|
+
resolve2({ code, signal, output });
|
|
1709
|
+
});
|
|
1710
|
+
});
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
// src/commands/auth/shared.ts
|
|
1714
|
+
var OAUTH_TOKEN_PREFIX = "sk-ant-oat";
|
|
1715
|
+
var OAUTH_TOKEN_PATTERN = /sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]{16,}/;
|
|
1716
|
+
var MIN_TOKEN_LENGTH = 20;
|
|
1717
|
+
var MAX_TOKEN_LENGTH = 2048;
|
|
1718
|
+
var InvalidOauthTokenError = class extends Error {
|
|
1719
|
+
constructor(message) {
|
|
1720
|
+
super(message);
|
|
1721
|
+
this.name = "InvalidOauthTokenError";
|
|
1722
|
+
}
|
|
1723
|
+
};
|
|
1724
|
+
function validateOauthToken(raw) {
|
|
1725
|
+
const value = raw ?? "";
|
|
1726
|
+
if (value.trim().length === 0) {
|
|
1727
|
+
throw new InvalidOauthTokenError(
|
|
1728
|
+
"The token is empty. Run `squad auth login` without --token to sign in through the browser, or pass the value printed by `claude setup-token`."
|
|
1729
|
+
);
|
|
1730
|
+
}
|
|
1731
|
+
if (/\s/.test(value.trim())) {
|
|
1732
|
+
throw new InvalidOauthTokenError(
|
|
1733
|
+
"That token contains whitespace. Paste only the single-line value printed by `claude setup-token` (quote it in your shell if needed)."
|
|
1734
|
+
);
|
|
1735
|
+
}
|
|
1736
|
+
const token = value.trim();
|
|
1737
|
+
if (token.startsWith("sk-ant-") && !token.startsWith(OAUTH_TOKEN_PREFIX)) {
|
|
1738
|
+
throw new InvalidOauthTokenError(
|
|
1739
|
+
"That looks like an Anthropic API key; use `squad config set planner` for keys. `squad auth login --token` takes the OAuth token printed by `claude setup-token`."
|
|
1740
|
+
);
|
|
1741
|
+
}
|
|
1742
|
+
if (token.length < MIN_TOKEN_LENGTH) {
|
|
1743
|
+
throw new InvalidOauthTokenError(
|
|
1744
|
+
`That token is only ${token.length} characters \u2014 too short to be a \`claude setup-token\` value. Re-run \`claude setup-token\` and paste the whole line.`
|
|
1745
|
+
);
|
|
1746
|
+
}
|
|
1747
|
+
if (token.length > MAX_TOKEN_LENGTH) {
|
|
1748
|
+
throw new InvalidOauthTokenError(
|
|
1749
|
+
"That token is implausibly long. Paste only the single line `claude setup-token` printed."
|
|
1750
|
+
);
|
|
1751
|
+
}
|
|
1752
|
+
return token;
|
|
1753
|
+
}
|
|
1754
|
+
function extractOauthToken(output) {
|
|
1755
|
+
const hit = OAUTH_TOKEN_PATTERN.exec(output);
|
|
1756
|
+
return hit ? hit[0] : void 0;
|
|
1757
|
+
}
|
|
1758
|
+
function authReasonText(reason, provider) {
|
|
1759
|
+
switch (reason) {
|
|
1760
|
+
case "explicit-config":
|
|
1761
|
+
return "explicit in .squad/config.yaml";
|
|
1762
|
+
case "auto-login-detected":
|
|
1763
|
+
return "auto \u2014 Claude login detected";
|
|
1764
|
+
case "auto-oauth-token":
|
|
1765
|
+
return "auto \u2014 stored OAuth token";
|
|
1766
|
+
case "auto-fallback-api-key":
|
|
1767
|
+
return "auto \u2014 no Claude login, fell back to an API key";
|
|
1768
|
+
case "provider-is-api-key-only":
|
|
1769
|
+
return `${provider} is API-key only`;
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
function runtimeNameFor(provider, planner) {
|
|
1773
|
+
if (provider !== "anthropic") return "vercel";
|
|
1774
|
+
return planner?.runtime?.anthropic ?? "agent-sdk";
|
|
1775
|
+
}
|
|
1776
|
+
function credentialPresent(auth2) {
|
|
1777
|
+
return auth2.mode === "api-key" || auth2.loginHint !== "assumed";
|
|
1778
|
+
}
|
|
1779
|
+
var SUBSCRIPTION_ALTERNATIVE_HINT = "Prefer not to store a token? Run `claude` and use `/login` instead \u2014 squad-kit picks the OS credential store up automatically under `auth: subscription`.";
|
|
1780
|
+
|
|
1781
|
+
// src/commands/auth/login.ts
|
|
1782
|
+
var NON_TTY_LOGIN_MESSAGE = "A browser login needs a terminal. Generate a token elsewhere with `claude setup-token` and pass it with `squad auth login --token <value>`, or set CLAUDE_CODE_OAUTH_TOKEN.";
|
|
1783
|
+
async function runAuthLogin(opts = {}) {
|
|
1784
|
+
if (opts.token && opts.printOnly) {
|
|
1785
|
+
throw new Error(
|
|
1786
|
+
"Pass either --token or --print-only, not both. --print-only runs the browser flow and prints the token it produces; --token consumes one you already have."
|
|
1787
|
+
);
|
|
1788
|
+
}
|
|
1789
|
+
const providedToken = opts.token !== void 0 ? validateOauthToken(opts.token) : void 0;
|
|
1790
|
+
const storing = !opts.printOnly;
|
|
1791
|
+
const workspace = storing ? openWorkspace() : void 0;
|
|
1792
|
+
if (workspace && !await confirmOverwrite(workspace.secrets, Boolean(opts.yes))) return;
|
|
1793
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
1794
|
+
warning(
|
|
1795
|
+
"CLAUDE_CODE_OAUTH_TOKEN is exported in this shell. It outranks the token squad-kit stores, so the stored one is ignored until you unset it."
|
|
1796
|
+
);
|
|
1797
|
+
}
|
|
1798
|
+
const token = providedToken ?? await runBrowserFlow();
|
|
1799
|
+
if (!storing) {
|
|
1800
|
+
blank();
|
|
1801
|
+
success("Login complete. Nothing was stored.");
|
|
1802
|
+
info("Token (copy it somewhere safe \u2014 squad-kit did not save it):");
|
|
1803
|
+
process.stdout.write(`${token}
|
|
1804
|
+
`);
|
|
1805
|
+
blank();
|
|
1806
|
+
info(SUBSCRIPTION_ALTERNATIVE_HINT);
|
|
1807
|
+
info("To store it in this workspace later: `squad auth login --token <value>`.");
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
const ws = workspace;
|
|
1811
|
+
saveSecrets(ws.paths.secretsFile, mergePlannerOauthTokenIntoSecrets(ws.secrets, token));
|
|
1812
|
+
success("OAuth token saved to .squad/secrets.yaml (chmod 0600 on POSIX).");
|
|
1813
|
+
const planner = ws.config.planner;
|
|
1814
|
+
if (planner) {
|
|
1815
|
+
saveConfig(ws.paths.configFile, {
|
|
1816
|
+
...ws.config,
|
|
1817
|
+
planner: { ...planner, auth: { ...planner.auth, anthropic: "subscription" } }
|
|
1818
|
+
});
|
|
1819
|
+
info("Set `planner.auth.anthropic: subscription` in .squad/config.yaml.");
|
|
1820
|
+
} else {
|
|
1821
|
+
warning(
|
|
1822
|
+
"The direct planner is not configured in this workspace, so no auth mode was written. Run `squad config set planner` and choose Anthropic to finish setup."
|
|
1823
|
+
);
|
|
1824
|
+
}
|
|
1825
|
+
await verifyAndSummarise(planner ? { ...planner, auth: { anthropic: "subscription" } } : void 0);
|
|
1826
|
+
blank();
|
|
1827
|
+
info(SUBSCRIPTION_ALTERNATIVE_HINT);
|
|
1828
|
+
info("Next: `squad auth status` to confirm, then `squad new-plan --api`.");
|
|
1829
|
+
}
|
|
1830
|
+
function openWorkspace() {
|
|
1831
|
+
const paths = buildPaths(requireSquadRoot());
|
|
1832
|
+
return {
|
|
1833
|
+
paths,
|
|
1834
|
+
config: loadConfig(paths.configFile),
|
|
1835
|
+
secrets: fs11.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {}
|
|
1836
|
+
};
|
|
1837
|
+
}
|
|
1838
|
+
async function confirmOverwrite(secrets, yes) {
|
|
1839
|
+
if (!secrets.planner?.anthropicOauthToken) return true;
|
|
1840
|
+
if (yes) return true;
|
|
1841
|
+
if (!isInteractive()) {
|
|
1842
|
+
throw new Error(
|
|
1843
|
+
"A Claude OAuth token is already stored in .squad/secrets.yaml. Pass -y to overwrite it non-interactively."
|
|
1844
|
+
);
|
|
1845
|
+
}
|
|
1846
|
+
const ok = await confirm2({
|
|
1847
|
+
message: "A Claude OAuth token is already stored. Replace it with a new one?",
|
|
1848
|
+
default: false
|
|
1849
|
+
});
|
|
1850
|
+
if (!ok) info("Keeping the existing token; nothing changed.");
|
|
1851
|
+
return ok;
|
|
1852
|
+
}
|
|
1853
|
+
async function runBrowserFlow() {
|
|
1854
|
+
if (!isInteractive()) throw new Error(NON_TTY_LOGIN_MESSAGE);
|
|
1855
|
+
const binary = resolveClaudeBinary();
|
|
1856
|
+
if (!binary) throw new Error(MISSING_CLAUDE_BINARY_MESSAGE);
|
|
1857
|
+
step("Starting the Anthropic browser authorization flow\u2026");
|
|
1858
|
+
info(`Using ${binary.source === "bundled" ? "the bundled Agent SDK binary" : "the `claude` on your PATH"}.`);
|
|
1859
|
+
blank();
|
|
1860
|
+
const result = await spawnSetupToken(binary.path);
|
|
1861
|
+
if (result.signal) {
|
|
1862
|
+
throw new SquadExit(130, "Login cancelled \u2014 nothing was stored.");
|
|
1863
|
+
}
|
|
1864
|
+
if (result.code !== 0) {
|
|
1865
|
+
throw new SquadExit(
|
|
1866
|
+
result.code ?? 1,
|
|
1867
|
+
`\`claude setup-token\` exited with code ${result.code}. Nothing was stored. Re-run \`squad auth login\` to try again.`
|
|
1868
|
+
);
|
|
1869
|
+
}
|
|
1870
|
+
const extracted = extractOauthToken(result.output);
|
|
1871
|
+
if (extracted) return validateOauthToken(extracted);
|
|
1872
|
+
blank();
|
|
1873
|
+
warning("Could not read the token from the login output. Paste it here:");
|
|
1874
|
+
const pasted = await password2({
|
|
1875
|
+
message: "Token (input hidden):",
|
|
1876
|
+
validate: (v) => {
|
|
1877
|
+
try {
|
|
1878
|
+
validateOauthToken(v);
|
|
1879
|
+
return true;
|
|
1880
|
+
} catch (e) {
|
|
1881
|
+
return e instanceof Error ? e.message : "invalid token";
|
|
1882
|
+
}
|
|
1136
1883
|
}
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1884
|
+
});
|
|
1885
|
+
return validateOauthToken(pasted);
|
|
1886
|
+
}
|
|
1887
|
+
async function verifyAndSummarise(planner) {
|
|
1888
|
+
if (!planner || skipExternalProbesInAutomation()) return;
|
|
1889
|
+
const auth2 = resolvePlannerAuthForCwd("anthropic", planner);
|
|
1890
|
+
const spin = spinner("verifying the Claude login\u2026");
|
|
1891
|
+
const probe = await probeClaudeAuth(auth2);
|
|
1892
|
+
if (!probe.ok) {
|
|
1893
|
+
spin.fail(`could not verify the login (${probe.kind})`);
|
|
1894
|
+
warning(probe.detail);
|
|
1895
|
+
return;
|
|
1140
1896
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
function skipExternalProbesInAutomation() {
|
|
1146
|
-
const c = process.env.CI;
|
|
1147
|
-
if (c === "true" || c === "1") return true;
|
|
1148
|
-
if (process.env.GITHUB_ACTIONS) return true;
|
|
1149
|
-
return false;
|
|
1897
|
+
spin.succeed("login verified");
|
|
1898
|
+
const account = [probe.account?.email, probe.account?.organization, probe.account?.subscriptionType].filter(Boolean).join(" \xB7 ");
|
|
1899
|
+
if (account) kv("account", account, 15);
|
|
1900
|
+
if (probe.apiKeySource) kv("api key source", probe.apiKeySource, 15);
|
|
1150
1901
|
}
|
|
1151
1902
|
|
|
1152
1903
|
// src/commands/config/set-planner.ts
|
|
@@ -1168,7 +1919,7 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1168
1919
|
const root = requireSquadRoot();
|
|
1169
1920
|
const paths = buildPaths(root);
|
|
1170
1921
|
const config2 = loadConfig(paths.configFile);
|
|
1171
|
-
const baseSecrets =
|
|
1922
|
+
const baseSecrets = fs12.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
1172
1923
|
const useYes = Boolean(opts.yes);
|
|
1173
1924
|
const interactive = !useYes && isInteractive();
|
|
1174
1925
|
if (interactive && config2.planner?.enabled === true) {
|
|
@@ -1205,7 +1956,7 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1205
1956
|
}
|
|
1206
1957
|
if (interactive) {
|
|
1207
1958
|
if (!config2.planner || config2.planner.enabled !== true) {
|
|
1208
|
-
const enable = await
|
|
1959
|
+
const enable = await confirm3({ message: "Enable the direct planner?", default: true });
|
|
1209
1960
|
if (!enable) {
|
|
1210
1961
|
return;
|
|
1211
1962
|
}
|
|
@@ -1218,42 +1969,45 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1218
1969
|
} else {
|
|
1219
1970
|
nextPlanner = { ...prev, provider, enabled: true };
|
|
1220
1971
|
}
|
|
1972
|
+
let authChoice;
|
|
1973
|
+
if (interactive && provider === "anthropic") {
|
|
1974
|
+
authChoice = await select({
|
|
1975
|
+
message: "How should squad-kit authenticate with Anthropic?",
|
|
1976
|
+
choices: [
|
|
1977
|
+
{
|
|
1978
|
+
name: "Use my Claude subscription (browser login) \u2014 recommended, no API key needed",
|
|
1979
|
+
value: "subscription"
|
|
1980
|
+
},
|
|
1981
|
+
{ name: "Use an Anthropic API key", value: "api-key" },
|
|
1982
|
+
{
|
|
1983
|
+
name: "Decide automatically (subscription if signed in, otherwise API key)",
|
|
1984
|
+
value: "auto"
|
|
1985
|
+
}
|
|
1986
|
+
],
|
|
1987
|
+
default: "subscription"
|
|
1988
|
+
});
|
|
1989
|
+
nextPlanner = { ...nextPlanner, auth: { ...nextPlanner.auth, anthropic: authChoice } };
|
|
1990
|
+
}
|
|
1221
1991
|
if (useYes) {
|
|
1222
1992
|
if (!resolveProviderKey(provider)) {
|
|
1223
1993
|
throw credentialError(provider);
|
|
1224
1994
|
}
|
|
1225
1995
|
} else {
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
if (
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
validate: (v) => v.length >= 20 ? true : "key looks too short"
|
|
1237
|
-
});
|
|
1238
|
-
const merged = mergePlannerKeyIntoSecrets(baseSecrets, provider, key);
|
|
1239
|
-
saveSecrets(paths.secretsFile, merged);
|
|
1240
|
-
success("Planner key saved to .squad/secrets.yaml");
|
|
1241
|
-
info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
|
|
1242
|
-
} else {
|
|
1243
|
-
info("Keeping the existing planner credential; no change to .squad/secrets.yaml for the key.");
|
|
1996
|
+
if (authChoice === "subscription") {
|
|
1997
|
+
await offerBrowserLogin(baseSecrets);
|
|
1998
|
+
} else if (authChoice === "auto") {
|
|
1999
|
+
info(
|
|
2000
|
+
"`auto` resolves in this order: a detected Claude login first, then a resolvable API key (ANTHROPIC_API_KEY, SQUAD_PLANNER_API_KEY, or .squad/secrets.yaml)."
|
|
2001
|
+
);
|
|
2002
|
+
if (!detectClaudeLogin(baseSecrets).present && !resolveProviderKey(provider)) {
|
|
2003
|
+
warning(
|
|
2004
|
+
"Neither a Claude login nor an API key resolves right now. Run `squad auth login`, or re-run `squad config set planner` and choose the API-key option."
|
|
2005
|
+
);
|
|
1244
2006
|
}
|
|
1245
2007
|
} else {
|
|
1246
|
-
|
|
1247
|
-
const key = await password2({
|
|
1248
|
-
message: `${envVar} value (input hidden) \u2014 required:`,
|
|
1249
|
-
validate: (v) => v.length >= 20 ? true : "key looks too short"
|
|
1250
|
-
});
|
|
1251
|
-
const merged = mergePlannerKeyIntoSecrets(baseSecrets, provider, key);
|
|
1252
|
-
saveSecrets(paths.secretsFile, merged);
|
|
1253
|
-
success("Planner key saved to .squad/secrets.yaml");
|
|
1254
|
-
info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
|
|
2008
|
+
await promptForApiKey(provider, paths.secretsFile, baseSecrets);
|
|
1255
2009
|
}
|
|
1256
|
-
const cacheEnabled = await
|
|
2010
|
+
const cacheEnabled = await confirm3({
|
|
1257
2011
|
message: "Enable prompt caching? (Recommended \u2014 reduces billed tokens by ~70% on most providers.)",
|
|
1258
2012
|
default: nextPlanner.cache?.enabled ?? true
|
|
1259
2013
|
});
|
|
@@ -1261,6 +2015,12 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1261
2015
|
}
|
|
1262
2016
|
const next = { ...config2, planner: nextPlanner };
|
|
1263
2017
|
saveConfig(paths.configFile, next);
|
|
2018
|
+
const auth2 = interactive && provider === "anthropic" ? tryResolveAuth(provider, nextPlanner) : void 0;
|
|
2019
|
+
const configuredMode = nextPlanner.auth?.anthropic;
|
|
2020
|
+
if (auth2?.mode === "subscription" && (configuredMode === "subscription" || configuredMode === "auto")) {
|
|
2021
|
+
await reportSubscriptionSetup(provider, nextPlanner, auth2);
|
|
2022
|
+
return;
|
|
2023
|
+
}
|
|
1264
2024
|
const cred = resolveProviderKey(provider);
|
|
1265
2025
|
if (!cred) {
|
|
1266
2026
|
warning(
|
|
@@ -1278,7 +2038,7 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1278
2038
|
kv("credential", `${cred.source} (${sourceText})`, 10);
|
|
1279
2039
|
}
|
|
1280
2040
|
if (skipExternalProbesInAutomation() || !cred) {
|
|
1281
|
-
printPlannerNextSteps(Boolean(cred));
|
|
2041
|
+
printPlannerNextSteps(Boolean(cred), "api-key");
|
|
1282
2042
|
return;
|
|
1283
2043
|
}
|
|
1284
2044
|
try {
|
|
@@ -1299,11 +2059,90 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1299
2059
|
`Could not reach provider model list (${msg.slice(0, 160)}). Key was saved; run \`squad doctor\` to verify.`
|
|
1300
2060
|
);
|
|
1301
2061
|
}
|
|
1302
|
-
printPlannerNextSteps(true);
|
|
2062
|
+
printPlannerNextSteps(true, "api-key");
|
|
2063
|
+
}
|
|
2064
|
+
async function promptForApiKey(provider, secretsFile, baseSecrets) {
|
|
2065
|
+
const envVar = providerEnvVar(provider);
|
|
2066
|
+
const existing = resolveProviderKey(provider);
|
|
2067
|
+
if (existing) {
|
|
2068
|
+
const shouldUpdate = await confirm3({
|
|
2069
|
+
message: "A planner credential is already available. Enter a new key and save it to .squad/secrets.yaml?",
|
|
2070
|
+
default: false
|
|
2071
|
+
});
|
|
2072
|
+
if (!shouldUpdate) {
|
|
2073
|
+
info("Keeping the existing planner credential; no change to .squad/secrets.yaml for the key.");
|
|
2074
|
+
return;
|
|
2075
|
+
}
|
|
2076
|
+
const key2 = await password3({
|
|
2077
|
+
message: `${envVar} value (input hidden):`,
|
|
2078
|
+
validate: (v) => v.length >= 20 ? true : "key looks too short"
|
|
2079
|
+
});
|
|
2080
|
+
saveSecrets(secretsFile, mergePlannerKeyIntoSecrets(baseSecrets, provider, key2));
|
|
2081
|
+
success("Planner key saved to .squad/secrets.yaml");
|
|
2082
|
+
info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
|
|
2083
|
+
return;
|
|
2084
|
+
}
|
|
2085
|
+
const key = await password3({
|
|
2086
|
+
message: `${envVar} value (input hidden) \u2014 required:`,
|
|
2087
|
+
validate: (v) => v.length >= 20 ? true : "key looks too short"
|
|
2088
|
+
});
|
|
2089
|
+
saveSecrets(secretsFile, mergePlannerKeyIntoSecrets(baseSecrets, provider, key));
|
|
2090
|
+
success("Planner key saved to .squad/secrets.yaml");
|
|
2091
|
+
info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
|
|
2092
|
+
}
|
|
2093
|
+
async function offerBrowserLogin(baseSecrets) {
|
|
2094
|
+
if (detectClaudeLogin(baseSecrets).present) {
|
|
2095
|
+
info("A Claude login is already available on this machine \u2014 no API key needed.");
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
const now = await confirm3({
|
|
2099
|
+
message: "No Claude login found. Run the browser login now?",
|
|
2100
|
+
default: true
|
|
2101
|
+
});
|
|
2102
|
+
if (!now) {
|
|
2103
|
+
info("Skipped. Run `squad auth login` when you are ready \u2014 planning needs it in subscription mode.");
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
await runAuthLogin({});
|
|
2107
|
+
}
|
|
2108
|
+
function tryResolveAuth(provider, planner) {
|
|
2109
|
+
try {
|
|
2110
|
+
return resolvePlannerAuthForCwd(provider, planner);
|
|
2111
|
+
} catch {
|
|
2112
|
+
return void 0;
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
async function reportSubscriptionSetup(provider, planner, auth2) {
|
|
2116
|
+
blank();
|
|
2117
|
+
success("Planner configuration updated.");
|
|
2118
|
+
kv("provider", provider, 10);
|
|
2119
|
+
kv("model (plan)", modelFor(provider, "plan", planner.modelOverride), 10);
|
|
2120
|
+
kv("model (execute)", modelFor(provider, "execute", planner.modelOverride), 10);
|
|
2121
|
+
kv("auth", `subscription (${describeAuth(auth2).credentialHint})`, 10);
|
|
2122
|
+
if (!skipExternalProbesInAutomation()) {
|
|
2123
|
+
const probe = await probeClaudeAuth(auth2);
|
|
2124
|
+
if (probe.ok) {
|
|
2125
|
+
const account = [probe.account?.email, probe.account?.organization, probe.account?.subscriptionType].filter(Boolean).join(" \xB7 ");
|
|
2126
|
+
info(account ? `Credential check: signed in as ${account}.` : "Credential check: the Claude login responded OK.");
|
|
2127
|
+
} else {
|
|
2128
|
+
warning(`Credential check failed (${probe.kind}). ${probe.detail}`);
|
|
2129
|
+
}
|
|
2130
|
+
}
|
|
2131
|
+
printPlannerNextSteps(true, "subscription");
|
|
1303
2132
|
}
|
|
1304
|
-
function printPlannerNextSteps(credentialReady) {
|
|
2133
|
+
function printPlannerNextSteps(credentialReady, mode) {
|
|
1305
2134
|
blank();
|
|
1306
2135
|
step("Next:");
|
|
2136
|
+
if (mode === "subscription") {
|
|
2137
|
+
info("1) Confirm the login with `squad auth status` \u2014 it shows the account and how it resolved.");
|
|
2138
|
+
info("2) Verify with `squad doctor` \u2014 every planner check should be green.");
|
|
2139
|
+
info("3) Create a story: squad new-story <feature-slug> (or --no-tracker for a manual story).");
|
|
2140
|
+
info("4) Fill the generated intake.md, then run `squad new-plan --api` to generate the plan.");
|
|
2141
|
+
info(
|
|
2142
|
+
" Planning draws on your Claude usage limits \u2014 there is no per-token API bill. To switch to an API key later, re-run `squad config set planner`."
|
|
2143
|
+
);
|
|
2144
|
+
return;
|
|
2145
|
+
}
|
|
1307
2146
|
if (!credentialReady) {
|
|
1308
2147
|
info("1) Save a planner key: re-run `squad config set planner` and paste the key when prompted.");
|
|
1309
2148
|
info("2) Verify with `squad doctor` \u2014 all planner checks should turn green.");
|
|
@@ -1319,7 +2158,7 @@ function printPlannerNextSteps(credentialReady) {
|
|
|
1319
2158
|
}
|
|
1320
2159
|
|
|
1321
2160
|
// src/commands/config/set-tracker.ts
|
|
1322
|
-
import
|
|
2161
|
+
import fs14 from "fs";
|
|
1323
2162
|
import { select as select2 } from "@inquirer/prompts";
|
|
1324
2163
|
|
|
1325
2164
|
// src/tracker/types.ts
|
|
@@ -1335,13 +2174,13 @@ var TrackerError = class extends Error {
|
|
|
1335
2174
|
};
|
|
1336
2175
|
|
|
1337
2176
|
// src/tracker/attachments.ts
|
|
1338
|
-
import
|
|
1339
|
-
import
|
|
2177
|
+
import fs13 from "fs";
|
|
2178
|
+
import path6 from "path";
|
|
1340
2179
|
var DEFAULT_MAX_MB = 10;
|
|
1341
2180
|
async function downloadAttachmentsWith(refs, targetDir, authHeaders, opts = {}) {
|
|
1342
2181
|
const maxMb = opts.maxMegabytes ?? DEFAULT_MAX_MB;
|
|
1343
2182
|
const maxBytes = maxMb * 1024 * 1024;
|
|
1344
|
-
|
|
2183
|
+
fs13.mkdirSync(targetDir, { recursive: true });
|
|
1345
2184
|
const out = [];
|
|
1346
2185
|
for (const ref of refs) {
|
|
1347
2186
|
out.push(await one(ref, targetDir, authHeaders, maxBytes, maxMb));
|
|
@@ -1387,7 +2226,7 @@ async function one(ref, targetDir, headers, maxBytes, maxMb) {
|
|
|
1387
2226
|
skipReason: `${formatMb(buf.byteLength)} after download exceeds cap (${maxMb} MB)`
|
|
1388
2227
|
};
|
|
1389
2228
|
}
|
|
1390
|
-
|
|
2229
|
+
fs13.writeFileSync(dest, buf);
|
|
1391
2230
|
return {
|
|
1392
2231
|
source: ref,
|
|
1393
2232
|
outcome: "written",
|
|
@@ -1397,19 +2236,19 @@ async function one(ref, targetDir, headers, maxBytes, maxMb) {
|
|
|
1397
2236
|
}
|
|
1398
2237
|
function sanitizeFilename(raw) {
|
|
1399
2238
|
const normalized = raw.replace(/\\/g, "/");
|
|
1400
|
-
const base =
|
|
2239
|
+
const base = path6.basename(normalized).replace(/[\\/]/g, "_");
|
|
1401
2240
|
const cleaned = base.replace(/[\x00-\x1f]/g, "").replace(/^\.+/, "").replace(/\s+/g, " ").trim();
|
|
1402
2241
|
if (cleaned.length === 0) return "attachment";
|
|
1403
2242
|
return cleaned.slice(0, 180);
|
|
1404
2243
|
}
|
|
1405
2244
|
function uniqueDestination(dir, name) {
|
|
1406
|
-
let candidate =
|
|
1407
|
-
if (!
|
|
1408
|
-
const ext =
|
|
2245
|
+
let candidate = path6.join(dir, name);
|
|
2246
|
+
if (!fs13.existsSync(candidate)) return candidate;
|
|
2247
|
+
const ext = path6.extname(name);
|
|
1409
2248
|
const stem = name.slice(0, name.length - ext.length);
|
|
1410
2249
|
let n = 1;
|
|
1411
|
-
while (
|
|
1412
|
-
candidate =
|
|
2250
|
+
while (fs13.existsSync(candidate) && n < 1e3) {
|
|
2251
|
+
candidate = path6.join(dir, `${stem} (${n})${ext}`);
|
|
1413
2252
|
n += 1;
|
|
1414
2253
|
}
|
|
1415
2254
|
return candidate;
|
|
@@ -2023,7 +2862,7 @@ async function runConfigSetTracker(opts = {}) {
|
|
|
2023
2862
|
const root = requireSquadRoot();
|
|
2024
2863
|
const paths = buildPaths(root);
|
|
2025
2864
|
const config2 = loadConfig(paths.configFile);
|
|
2026
|
-
const baseSecrets =
|
|
2865
|
+
const baseSecrets = fs14.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
2027
2866
|
const useYes = Boolean(opts.yes);
|
|
2028
2867
|
const interactive = !useYes && isInteractive();
|
|
2029
2868
|
let type;
|
|
@@ -2146,7 +2985,7 @@ async function runConfigSetTracker(opts = {}) {
|
|
|
2146
2985
|
return;
|
|
2147
2986
|
}
|
|
2148
2987
|
const reloaded = loadConfig(paths.configFile);
|
|
2149
|
-
const s =
|
|
2988
|
+
const s = fs14.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
2150
2989
|
if (reloaded.tracker.type === "jira") {
|
|
2151
2990
|
const r = await probeJiraConnectivity(s, reloaded);
|
|
2152
2991
|
if (r.ok) {
|
|
@@ -2193,8 +3032,8 @@ function printTrackerNextSteps(type) {
|
|
|
2193
3032
|
}
|
|
2194
3033
|
|
|
2195
3034
|
// src/commands/config/unset-tracker.ts
|
|
2196
|
-
import
|
|
2197
|
-
import { confirm as
|
|
3035
|
+
import fs15 from "fs";
|
|
3036
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
2198
3037
|
async function runConfigUnsetTracker(opts = {}) {
|
|
2199
3038
|
const root = requireSquadRoot();
|
|
2200
3039
|
const paths = buildPaths(root);
|
|
@@ -2205,7 +3044,7 @@ async function runConfigUnsetTracker(opts = {}) {
|
|
|
2205
3044
|
warning("Pass -y in non-interactive mode to confirm setting the tracker to none.");
|
|
2206
3045
|
return;
|
|
2207
3046
|
}
|
|
2208
|
-
const ok = await
|
|
3047
|
+
const ok = await confirm4({ message: "Set tracker to none? (keeps tracker tokens in .squad/secrets.yaml.)", default: false });
|
|
2209
3048
|
if (!ok) {
|
|
2210
3049
|
return;
|
|
2211
3050
|
}
|
|
@@ -2213,7 +3052,7 @@ async function runConfigUnsetTracker(opts = {}) {
|
|
|
2213
3052
|
const next = { ...config2, tracker: { type: "none" } };
|
|
2214
3053
|
saveConfig(paths.configFile, next);
|
|
2215
3054
|
if (opts.removeCredentials) {
|
|
2216
|
-
const s =
|
|
3055
|
+
const s = fs15.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
2217
3056
|
if (s.tracker) {
|
|
2218
3057
|
delete s.tracker;
|
|
2219
3058
|
saveSecrets(paths.secretsFile, s);
|
|
@@ -2226,8 +3065,8 @@ async function runConfigUnsetTracker(opts = {}) {
|
|
|
2226
3065
|
}
|
|
2227
3066
|
|
|
2228
3067
|
// src/commands/config/remove-credential.ts
|
|
2229
|
-
import
|
|
2230
|
-
import { confirm as
|
|
3068
|
+
import fs16 from "fs";
|
|
3069
|
+
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
2231
3070
|
async function runConfigRemoveCredential(section, opts = {}) {
|
|
2232
3071
|
if (section !== "planner" && section !== "tracker") {
|
|
2233
3072
|
throw new Error(
|
|
@@ -2242,7 +3081,7 @@ async function runConfigRemoveCredential(section, opts = {}) {
|
|
|
2242
3081
|
warning("Pass -y in non-interactive mode to confirm, or run `squad config remove-credential` in a TTY.");
|
|
2243
3082
|
return;
|
|
2244
3083
|
}
|
|
2245
|
-
const ok = await
|
|
3084
|
+
const ok = await confirm5({
|
|
2246
3085
|
message: `Delete all ${section} credentials from .squad/secrets.yaml? (config is unchanged)`,
|
|
2247
3086
|
default: false
|
|
2248
3087
|
});
|
|
@@ -2250,7 +3089,7 @@ async function runConfigRemoveCredential(section, opts = {}) {
|
|
|
2250
3089
|
return;
|
|
2251
3090
|
}
|
|
2252
3091
|
}
|
|
2253
|
-
const s =
|
|
3092
|
+
const s = fs16.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
2254
3093
|
if (section === "planner") {
|
|
2255
3094
|
if (s.planner) {
|
|
2256
3095
|
delete s.planner;
|
|
@@ -2268,10 +3107,10 @@ async function runConfigRemoveCredential(section, opts = {}) {
|
|
|
2268
3107
|
// src/commands/init.ts
|
|
2269
3108
|
var SUPPORTED_AGENTS = ["claude-code", "cursor", "copilot", "gemini"];
|
|
2270
3109
|
var AGENT_INSTALL = {
|
|
2271
|
-
"claude-code": { subdir:
|
|
2272
|
-
cursor: { subdir:
|
|
2273
|
-
copilot: { subdir:
|
|
2274
|
-
gemini: { subdir:
|
|
3110
|
+
"claude-code": { subdir: path7.join(".claude", "commands"), srcDir: "claude-code" },
|
|
3111
|
+
cursor: { subdir: path7.join(".cursor", "commands"), srcDir: "cursor" },
|
|
3112
|
+
copilot: { subdir: path7.join(".github", "prompts"), srcDir: "copilot" },
|
|
3113
|
+
gemini: { subdir: path7.join(".gemini", "commands"), srcDir: "gemini" }
|
|
2275
3114
|
};
|
|
2276
3115
|
async function runInit(opts) {
|
|
2277
3116
|
const root = process.cwd();
|
|
@@ -2279,7 +3118,7 @@ async function runInit(opts) {
|
|
|
2279
3118
|
if (!opts.yes) {
|
|
2280
3119
|
banner();
|
|
2281
3120
|
}
|
|
2282
|
-
if (
|
|
3121
|
+
if (fs17.existsSync(paths.configFile) && !opts.force) {
|
|
2283
3122
|
if (!isInteractive() || opts.yes) {
|
|
2284
3123
|
warning(
|
|
2285
3124
|
`A ${SQUAD_DIR}/config.yaml already exists. Run \`squad init --force\` to overwrite, or \`squad config set planner\` / \`squad config set tracker\` to change individual sections.`
|
|
@@ -2306,7 +3145,7 @@ async function runInit(opts) {
|
|
|
2306
3145
|
}
|
|
2307
3146
|
}
|
|
2308
3147
|
const defaults = {
|
|
2309
|
-
name: opts.name ??
|
|
3148
|
+
name: opts.name ?? path7.basename(root),
|
|
2310
3149
|
language: opts.language ?? "typescript",
|
|
2311
3150
|
tracker: opts.tracker ?? "none",
|
|
2312
3151
|
agents: parseAgentsFlag(opts.agents)
|
|
@@ -2382,11 +3221,11 @@ async function runInit(opts) {
|
|
|
2382
3221
|
};
|
|
2383
3222
|
}
|
|
2384
3223
|
} else {
|
|
2385
|
-
const
|
|
3224
|
+
const plannerEnabled2 = await confirm6({
|
|
2386
3225
|
message: "Enable automatic plan generation? (press Enter to skip \u2014 you can always edit .squad/config.yaml later)",
|
|
2387
3226
|
default: false
|
|
2388
3227
|
});
|
|
2389
|
-
if (
|
|
3228
|
+
if (plannerEnabled2) {
|
|
2390
3229
|
const provider = await select3({
|
|
2391
3230
|
message: "Planner provider:",
|
|
2392
3231
|
choices: [
|
|
@@ -2411,29 +3250,60 @@ async function runInit(opts) {
|
|
|
2411
3250
|
}
|
|
2412
3251
|
}
|
|
2413
3252
|
if (plannerBlock?.enabled && allowSecretPrompts) {
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
} else {
|
|
2419
|
-
const storeKey = await select3({
|
|
2420
|
-
message: `No ${envVar} in your environment. How do you want to store your planner API key?`,
|
|
3253
|
+
let needsApiKey = true;
|
|
3254
|
+
if (plannerBlock.provider === "anthropic") {
|
|
3255
|
+
const authMode = await select3({
|
|
3256
|
+
message: "How should squad-kit authenticate with Anthropic?",
|
|
2421
3257
|
choices: [
|
|
2422
|
-
{
|
|
2423
|
-
|
|
2424
|
-
|
|
3258
|
+
{
|
|
3259
|
+
name: "Use my Claude subscription (browser login) \u2014 recommended, no API key needed",
|
|
3260
|
+
value: "subscription"
|
|
3261
|
+
},
|
|
3262
|
+
{ name: "Use an Anthropic API key", value: "api-key" },
|
|
3263
|
+
{
|
|
3264
|
+
name: "Decide automatically (subscription if signed in, otherwise API key)",
|
|
3265
|
+
value: "auto"
|
|
3266
|
+
}
|
|
3267
|
+
],
|
|
3268
|
+
default: "subscription"
|
|
2425
3269
|
});
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
3270
|
+
plannerBlock = { ...plannerBlock, auth: { anthropic: authMode } };
|
|
3271
|
+
needsApiKey = authMode === "api-key";
|
|
3272
|
+
if (authMode === "subscription") {
|
|
3273
|
+
info("No API key needed. Run `squad auth login` after init to sign in with your Claude account.");
|
|
3274
|
+
info("Already signed in through Claude Code (`claude` then `/login`)? squad-kit picks that up automatically.");
|
|
3275
|
+
} else if (authMode === "auto") {
|
|
3276
|
+
info(
|
|
3277
|
+
"`auto` resolves in this order: a detected Claude login first, then a resolvable API key (ANTHROPIC_API_KEY, SQUAD_PLANNER_API_KEY, or .squad/secrets.yaml)."
|
|
3278
|
+
);
|
|
3279
|
+
info("Run `squad auth login` for the subscription, or `squad config set planner` to save a key.");
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
if (needsApiKey) {
|
|
3283
|
+
const envVar = providerEnvVar(plannerBlock.provider);
|
|
3284
|
+
const envPresent = Boolean(process.env[envVar]);
|
|
3285
|
+
if (envPresent) {
|
|
3286
|
+
info(`Planner key detected in ${envVar}; keeping env-var resolution as the primary source.`);
|
|
2435
3287
|
} else {
|
|
2436
|
-
|
|
3288
|
+
const storeKey = await select3({
|
|
3289
|
+
message: `No ${envVar} in your environment. How do you want to store your planner API key?`,
|
|
3290
|
+
choices: [
|
|
3291
|
+
{ name: `Enter it now and save to .squad/secrets.yaml (git-ignored)`, value: "secrets" },
|
|
3292
|
+
{ name: `I'll export ${envVar} in my shell profile myself`, value: "env" }
|
|
3293
|
+
]
|
|
3294
|
+
});
|
|
3295
|
+
if (storeKey === "secrets") {
|
|
3296
|
+
const key = await password4({
|
|
3297
|
+
message: `${envVar} value (input hidden):`,
|
|
3298
|
+
validate: (v) => v.length >= 20 ? true : "key looks too short"
|
|
3299
|
+
});
|
|
3300
|
+
const base = loadSecrets(paths.secretsFile);
|
|
3301
|
+
const merged = mergePlannerKeyIntoSecrets(base, plannerBlock.provider, key);
|
|
3302
|
+
saveSecrets(paths.secretsFile, merged);
|
|
3303
|
+
success("Planner key saved to .squad/secrets.yaml");
|
|
3304
|
+
} else {
|
|
3305
|
+
info(`Remember to export ${envVar} before running \`squad new-plan\`.`);
|
|
3306
|
+
}
|
|
2437
3307
|
}
|
|
2438
3308
|
}
|
|
2439
3309
|
}
|
|
@@ -2450,12 +3320,12 @@ async function runInit(opts) {
|
|
|
2450
3320
|
agents: answers.agents,
|
|
2451
3321
|
planner: plannerBlock
|
|
2452
3322
|
};
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
3323
|
+
fs17.mkdirSync(paths.squadDir, { recursive: true });
|
|
3324
|
+
fs17.mkdirSync(paths.storiesDir, { recursive: true });
|
|
3325
|
+
fs17.mkdirSync(paths.plansDir, { recursive: true });
|
|
2456
3326
|
writeFileSafe(
|
|
2457
3327
|
paths.indexFile,
|
|
2458
|
-
readFile(
|
|
3328
|
+
readFile(path7.join(templatesDir(), "index.md")),
|
|
2459
3329
|
!!opts.force
|
|
2460
3330
|
);
|
|
2461
3331
|
saveConfig(paths.configFile, config2);
|
|
@@ -2463,7 +3333,7 @@ async function runInit(opts) {
|
|
|
2463
3333
|
info("Updated .gitignore with squad-managed patterns (e.g. .squad/secrets.yaml).");
|
|
2464
3334
|
}
|
|
2465
3335
|
writeFileSafe(
|
|
2466
|
-
|
|
3336
|
+
path7.join(paths.squadDir, "README.md"),
|
|
2467
3337
|
renderSquadReadme(config2),
|
|
2468
3338
|
!!opts.force
|
|
2469
3339
|
);
|
|
@@ -2480,8 +3350,13 @@ async function runInit(opts) {
|
|
|
2480
3350
|
const model = modelFor(plannerBlock.provider, "plan", plannerBlock.modelOverride);
|
|
2481
3351
|
const overrideNote = plannerBlock.modelOverride?.[plannerBlock.provider] ? " (override)" : "";
|
|
2482
3352
|
kv("planner", `${plannerBlock.provider}/${model}${overrideNote}`);
|
|
2483
|
-
|
|
2484
|
-
if (
|
|
3353
|
+
const subscriptionMode = plannerBlock.auth?.anthropic === "subscription" && plannerBlock.provider === "anthropic";
|
|
3354
|
+
if (subscriptionMode) {
|
|
3355
|
+
kv("auth", "subscription (Claude login) \u2014 run `squad auth login` if you have not signed in");
|
|
3356
|
+
} else {
|
|
3357
|
+
kv("key", hasKey ? `\u2713 (${envVar} or .squad/secrets.yaml)` : `missing (set ${envVar})`);
|
|
3358
|
+
}
|
|
3359
|
+
if (!hasKey && !subscriptionMode) {
|
|
2485
3360
|
blank();
|
|
2486
3361
|
warning("Planner key not found in environment or .squad/secrets.yaml.");
|
|
2487
3362
|
info("Set one of these before running `squad new-plan`:");
|
|
@@ -2503,13 +3378,13 @@ function parseAgentsFlag(flag) {
|
|
|
2503
3378
|
}
|
|
2504
3379
|
async function confirmSafe(message, defaultValue, nonInteractive) {
|
|
2505
3380
|
if (nonInteractive) return defaultValue;
|
|
2506
|
-
return
|
|
3381
|
+
return confirm6({ message, default: defaultValue });
|
|
2507
3382
|
}
|
|
2508
3383
|
function installAgent(root, agent, overwrite) {
|
|
2509
3384
|
const config2 = AGENT_INSTALL[agent];
|
|
2510
|
-
const src =
|
|
2511
|
-
const dest =
|
|
2512
|
-
if (!
|
|
3385
|
+
const src = path7.join(templatesDir(), "agents", config2.srcDir);
|
|
3386
|
+
const dest = path7.join(root, config2.subdir);
|
|
3387
|
+
if (!fs17.existsSync(src)) return;
|
|
2513
3388
|
copyTree(src, dest, overwrite);
|
|
2514
3389
|
}
|
|
2515
3390
|
function renderSquadReadme(config2) {
|
|
@@ -2535,22 +3410,22 @@ Plan meta-prompts (\`generate-plan.md\`, \`story-skeleton.md\`) ship inside the
|
|
|
2535
3410
|
}
|
|
2536
3411
|
|
|
2537
3412
|
// src/commands/new-story.ts
|
|
2538
|
-
import
|
|
2539
|
-
import
|
|
3413
|
+
import fs23 from "fs";
|
|
3414
|
+
import path13 from "path";
|
|
2540
3415
|
import { input as input3, select as select5 } from "@inquirer/prompts";
|
|
2541
3416
|
|
|
2542
3417
|
// src/core/stories.ts
|
|
2543
|
-
import
|
|
2544
|
-
import
|
|
3418
|
+
import fs18 from "fs";
|
|
3419
|
+
import path8 from "path";
|
|
2545
3420
|
var STORY_FILE = /^(\d{2,})-story-.+\.md$/;
|
|
2546
3421
|
function listFeatures(paths) {
|
|
2547
|
-
if (!
|
|
2548
|
-
return
|
|
3422
|
+
if (!fs18.existsSync(paths.storiesDir)) return [];
|
|
3423
|
+
return fs18.readdirSync(paths.storiesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
2549
3424
|
}
|
|
2550
3425
|
function listPlanFeatureNames(paths) {
|
|
2551
3426
|
const names = new Set(listFeatures(paths));
|
|
2552
|
-
if (
|
|
2553
|
-
for (const e of
|
|
3427
|
+
if (fs18.existsSync(paths.plansDir)) {
|
|
3428
|
+
for (const e of fs18.readdirSync(paths.plansDir, { withFileTypes: true })) {
|
|
2554
3429
|
if (e.isDirectory()) names.add(e.name);
|
|
2555
3430
|
}
|
|
2556
3431
|
}
|
|
@@ -2561,16 +3436,16 @@ function listAllStories(paths) {
|
|
|
2561
3436
|
}
|
|
2562
3437
|
function listStories(paths, opts = {}) {
|
|
2563
3438
|
const rows = [];
|
|
2564
|
-
if (!
|
|
2565
|
-
for (const feature of
|
|
3439
|
+
if (!fs18.existsSync(paths.storiesDir)) return rows;
|
|
3440
|
+
for (const feature of fs18.readdirSync(paths.storiesDir, { withFileTypes: true })) {
|
|
2566
3441
|
if (!feature.isDirectory()) continue;
|
|
2567
3442
|
if (opts.feature && feature.name !== opts.feature) continue;
|
|
2568
|
-
const featureDir =
|
|
2569
|
-
for (const story of
|
|
3443
|
+
const featureDir = path8.join(paths.storiesDir, feature.name);
|
|
3444
|
+
for (const story of fs18.readdirSync(featureDir, { withFileTypes: true })) {
|
|
2570
3445
|
if (!story.isDirectory()) continue;
|
|
2571
|
-
const storyDir =
|
|
2572
|
-
const intakePath =
|
|
2573
|
-
if (!
|
|
3446
|
+
const storyDir = path8.join(featureDir, story.name);
|
|
3447
|
+
const intakePath = path8.join(storyDir, "intake.md");
|
|
3448
|
+
if (!fs18.existsSync(intakePath)) continue;
|
|
2574
3449
|
rows.push({
|
|
2575
3450
|
feature: feature.name,
|
|
2576
3451
|
id: story.name,
|
|
@@ -2584,10 +3459,10 @@ function listStories(paths, opts = {}) {
|
|
|
2584
3459
|
return rows;
|
|
2585
3460
|
}
|
|
2586
3461
|
function findPlanFor(plansDir, feature, storyId) {
|
|
2587
|
-
const featureDir =
|
|
2588
|
-
if (!
|
|
3462
|
+
const featureDir = path8.join(plansDir, feature);
|
|
3463
|
+
if (!fs18.existsSync(featureDir)) return void 0;
|
|
2589
3464
|
const candidates = [];
|
|
2590
|
-
for (const entry of
|
|
3465
|
+
for (const entry of fs18.readdirSync(featureDir)) {
|
|
2591
3466
|
if (!STORY_FILE.test(entry)) continue;
|
|
2592
3467
|
if (entry.includes(storyId)) candidates.push(entry);
|
|
2593
3468
|
}
|
|
@@ -2600,16 +3475,16 @@ function findStoryByIntake(records, intakePath) {
|
|
|
2600
3475
|
return records.find((r) => normalizeIntakePath(r.intakePath) === target);
|
|
2601
3476
|
}
|
|
2602
3477
|
function normalizeIntakePath(p) {
|
|
2603
|
-
const resolved =
|
|
3478
|
+
const resolved = path8.resolve(p);
|
|
2604
3479
|
try {
|
|
2605
|
-
return
|
|
3480
|
+
return fs18.realpathSync(resolved);
|
|
2606
3481
|
} catch {
|
|
2607
3482
|
return resolved;
|
|
2608
3483
|
}
|
|
2609
3484
|
}
|
|
2610
3485
|
function readTitleHint(intakePath) {
|
|
2611
3486
|
try {
|
|
2612
|
-
const raw =
|
|
3487
|
+
const raw = fs18.readFileSync(intakePath, "utf8");
|
|
2613
3488
|
const head = raw.split("\n").slice(0, 30).join("\n");
|
|
2614
3489
|
const hintLine = head.match(/>\s*\*\*Title hint \(from CLI\):\*\*\s*(.+?)\s*$/m);
|
|
2615
3490
|
const fromHint = hintLine?.[1]?.trim();
|
|
@@ -2646,7 +3521,7 @@ function parsePlanMetadataLine(line2) {
|
|
|
2646
3521
|
}
|
|
2647
3522
|
function readPlanMetadata(planFileAbsPath) {
|
|
2648
3523
|
try {
|
|
2649
|
-
const raw =
|
|
3524
|
+
const raw = fs18.readFileSync(planFileAbsPath, "utf8").replace(/^\uFEFF/, "");
|
|
2650
3525
|
const lines = raw.split(/\n/);
|
|
2651
3526
|
const scan = Math.min(lines.length, 40);
|
|
2652
3527
|
for (let i = 0; i < scan; i++) {
|
|
@@ -2678,8 +3553,8 @@ function trackerIdForFilename(type, id) {
|
|
|
2678
3553
|
}
|
|
2679
3554
|
|
|
2680
3555
|
// src/core/story-intake-helpers.ts
|
|
2681
|
-
import
|
|
2682
|
-
import
|
|
3556
|
+
import fs19 from "fs";
|
|
3557
|
+
import path9 from "path";
|
|
2683
3558
|
function buildIntakeTemplateVars(args) {
|
|
2684
3559
|
const e = escapeTemplateValue;
|
|
2685
3560
|
const { config: config2, featureSlug, storyFolderName, trackerId, title, fetchedIssue } = args;
|
|
@@ -2735,7 +3610,7 @@ function renderAttachmentsTable(downloads) {
|
|
|
2735
3610
|
const size = `${sizeNum | 0} KB`;
|
|
2736
3611
|
switch (d.outcome) {
|
|
2737
3612
|
case "written":
|
|
2738
|
-
return `| \`attachments/${
|
|
3613
|
+
return `| \`attachments/${path9.basename(d.absolutePath)}\` | ${size} | downloaded |`;
|
|
2739
3614
|
case "skipped-oversize":
|
|
2740
3615
|
return `| ${d.source.filename} | ${size} | **skipped** \u2014 ${d.skipReason} |`;
|
|
2741
3616
|
case "skipped-error":
|
|
@@ -2764,34 +3639,34 @@ function buildSkippedTrackerPreamble(trackerType) {
|
|
|
2764
3639
|
].join("\n");
|
|
2765
3640
|
}
|
|
2766
3641
|
function ensureFeatureOverview(featurePlanDir, slug) {
|
|
2767
|
-
|
|
2768
|
-
const overviewFile =
|
|
2769
|
-
if (
|
|
2770
|
-
const template = readFile(
|
|
3642
|
+
fs19.mkdirSync(featurePlanDir, { recursive: true });
|
|
3643
|
+
const overviewFile = path9.join(featurePlanDir, "00-overview.md");
|
|
3644
|
+
if (fs19.existsSync(overviewFile)) return;
|
|
3645
|
+
const template = readFile(path9.join(templatesDir(), "overview.md"));
|
|
2771
3646
|
writeFileSafe(overviewFile, render(template, { featureSlug: slug }), false);
|
|
2772
3647
|
}
|
|
2773
3648
|
|
|
2774
3649
|
// src/core/story-mutations.ts
|
|
2775
|
-
import
|
|
2776
|
-
import
|
|
3650
|
+
import fs22 from "fs";
|
|
3651
|
+
import path12 from "path";
|
|
2777
3652
|
|
|
2778
3653
|
// src/commands/rm/shared.ts
|
|
2779
|
-
import
|
|
2780
|
-
import
|
|
3654
|
+
import fs21 from "fs";
|
|
3655
|
+
import path11 from "path";
|
|
2781
3656
|
import { select as select4 } from "@inquirer/prompts";
|
|
2782
3657
|
|
|
2783
3658
|
// src/core/sequence.ts
|
|
2784
|
-
import
|
|
2785
|
-
import
|
|
3659
|
+
import fs20 from "fs";
|
|
3660
|
+
import path10 from "path";
|
|
2786
3661
|
var STORY_FILE2 = /^(\d{2,})-story-.+\.md$/;
|
|
2787
3662
|
function scanPlans(plansDir) {
|
|
2788
3663
|
const used = [];
|
|
2789
3664
|
const seen = /* @__PURE__ */ new Map();
|
|
2790
|
-
if (
|
|
2791
|
-
for (const feature of
|
|
3665
|
+
if (fs20.existsSync(plansDir)) {
|
|
3666
|
+
for (const feature of fs20.readdirSync(plansDir, { withFileTypes: true })) {
|
|
2792
3667
|
if (!feature.isDirectory()) continue;
|
|
2793
|
-
const featureDir =
|
|
2794
|
-
for (const entry of
|
|
3668
|
+
const featureDir = path10.join(plansDir, feature.name);
|
|
3669
|
+
for (const entry of fs20.readdirSync(featureDir)) {
|
|
2795
3670
|
const match = entry.match(STORY_FILE2);
|
|
2796
3671
|
if (!match) continue;
|
|
2797
3672
|
const n = parseInt(match[1], 10);
|
|
@@ -2815,9 +3690,9 @@ function canPromptForRm(opts) {
|
|
|
2815
3690
|
return isInteractive() && Boolean(process.stdin.isTTY);
|
|
2816
3691
|
}
|
|
2817
3692
|
function removeOverviewRow(featurePlanDir, storyId) {
|
|
2818
|
-
const overview =
|
|
2819
|
-
if (!
|
|
2820
|
-
const src =
|
|
3693
|
+
const overview = path11.join(featurePlanDir, "00-overview.md");
|
|
3694
|
+
if (!fs21.existsSync(overview)) return;
|
|
3695
|
+
const src = fs21.readFileSync(overview, "utf8");
|
|
2821
3696
|
const lines = src.split("\n");
|
|
2822
3697
|
const filtered = lines.filter((line2) => {
|
|
2823
3698
|
if (!line2.startsWith("|")) return true;
|
|
@@ -2825,12 +3700,12 @@ function removeOverviewRow(featurePlanDir, storyId) {
|
|
|
2825
3700
|
if (/^\|\s*-+/.test(line2)) return true;
|
|
2826
3701
|
return !line2.includes(storyId);
|
|
2827
3702
|
});
|
|
2828
|
-
|
|
3703
|
+
fs21.writeFileSync(overview, filtered.join("\n"), "utf8");
|
|
2829
3704
|
}
|
|
2830
3705
|
function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
|
|
2831
|
-
const overview =
|
|
2832
|
-
if (!
|
|
2833
|
-
const src =
|
|
3706
|
+
const overview = path11.join(featurePlanDir, "00-overview.md");
|
|
3707
|
+
if (!fs21.existsSync(overview)) return;
|
|
3708
|
+
const src = fs21.readFileSync(overview, "utf8");
|
|
2834
3709
|
const token = `\`${planBasename}\``;
|
|
2835
3710
|
const lines = src.split("\n");
|
|
2836
3711
|
const filtered = lines.filter((line2) => {
|
|
@@ -2839,25 +3714,25 @@ function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
|
|
|
2839
3714
|
if (/^\|\s*-+/.test(line2)) return true;
|
|
2840
3715
|
return !line2.includes(token);
|
|
2841
3716
|
});
|
|
2842
|
-
|
|
3717
|
+
fs21.writeFileSync(overview, filtered.join("\n"), "utf8");
|
|
2843
3718
|
}
|
|
2844
3719
|
function trashOrDelete(absPaths, trashRoot, useTrash) {
|
|
2845
3720
|
if (!useTrash) {
|
|
2846
|
-
for (const p of absPaths)
|
|
3721
|
+
for (const p of absPaths) fs21.rmSync(p, { recursive: true, force: true });
|
|
2847
3722
|
return { removed: absPaths };
|
|
2848
3723
|
}
|
|
2849
3724
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2850
|
-
const bucket =
|
|
2851
|
-
|
|
3725
|
+
const bucket = path11.join(trashRoot, ts);
|
|
3726
|
+
fs21.mkdirSync(bucket, { recursive: true });
|
|
2852
3727
|
for (const src of absPaths) {
|
|
2853
|
-
const rel =
|
|
2854
|
-
let dst =
|
|
3728
|
+
const rel = path11.basename(src);
|
|
3729
|
+
let dst = path11.join(bucket, rel);
|
|
2855
3730
|
let n = 0;
|
|
2856
|
-
while (
|
|
3731
|
+
while (fs21.existsSync(dst)) {
|
|
2857
3732
|
n += 1;
|
|
2858
|
-
dst =
|
|
3733
|
+
dst = path11.join(bucket, `${n}_${rel}`);
|
|
2859
3734
|
}
|
|
2860
|
-
|
|
3735
|
+
fs21.renameSync(src, dst);
|
|
2861
3736
|
}
|
|
2862
3737
|
return { removed: absPaths, trashed: bucket };
|
|
2863
3738
|
}
|
|
@@ -2868,21 +3743,21 @@ function renderPreview(kind, targets, opts) {
|
|
|
2868
3743
|
kv(t.kind, t.label, 6);
|
|
2869
3744
|
}
|
|
2870
3745
|
if (opts.trash) {
|
|
2871
|
-
info(`Items will move to ${
|
|
3746
|
+
info(`Items will move to ${path11.relative(process.cwd(), opts.trashRoot)}/<timestamp>/`);
|
|
2872
3747
|
} else if (!opts.dryRun) {
|
|
2873
3748
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
2874
3749
|
}
|
|
2875
3750
|
}
|
|
2876
3751
|
function listAllPlanListEntries(paths, featureFilter) {
|
|
2877
|
-
if (!
|
|
3752
|
+
if (!fs21.existsSync(paths.plansDir)) return [];
|
|
2878
3753
|
const out = [];
|
|
2879
|
-
for (const ent of
|
|
3754
|
+
for (const ent of fs21.readdirSync(paths.plansDir, { withFileTypes: true })) {
|
|
2880
3755
|
if (!ent.isDirectory()) continue;
|
|
2881
3756
|
if (featureFilter && ent.name !== featureFilter) continue;
|
|
2882
|
-
const featureDir =
|
|
2883
|
-
for (const name of
|
|
3757
|
+
const featureDir = path11.join(paths.plansDir, ent.name);
|
|
3758
|
+
for (const name of fs21.readdirSync(featureDir)) {
|
|
2884
3759
|
if (!STORY_PLAN.test(name)) continue;
|
|
2885
|
-
out.push({ feature: ent.name, file: name, abs:
|
|
3760
|
+
out.push({ feature: ent.name, file: name, abs: path11.join(featureDir, name) });
|
|
2886
3761
|
}
|
|
2887
3762
|
}
|
|
2888
3763
|
return out.sort((a, b) => (a.feature + a.file).localeCompare(b.feature + b.file));
|
|
@@ -2933,34 +3808,34 @@ function resolveStoryFromArg(arg, stories, root) {
|
|
|
2933
3808
|
if (s.id === t) return s;
|
|
2934
3809
|
if (`${s.feature}/${s.id}` === t) return s;
|
|
2935
3810
|
}
|
|
2936
|
-
const abs =
|
|
2937
|
-
const withIntake = t.endsWith("intake.md") ? abs :
|
|
3811
|
+
const abs = path11.isAbsolute(t) ? t : path11.resolve(root, t);
|
|
3812
|
+
const withIntake = t.endsWith("intake.md") ? abs : path11.join(abs, "intake.md");
|
|
2938
3813
|
return findStoryByIntake(stories, withIntake) ?? findStoryByIntake(stories, abs);
|
|
2939
3814
|
}
|
|
2940
3815
|
function isSequenceToken(s) {
|
|
2941
3816
|
return /^\d{1,4}$/.test(s) && s.length > 0;
|
|
2942
3817
|
}
|
|
2943
3818
|
function tryParsePlanFilePath(absPath, paths, featureFilter) {
|
|
2944
|
-
if (!
|
|
2945
|
-
const planRoot =
|
|
2946
|
-
const fileReal =
|
|
2947
|
-
const rel =
|
|
2948
|
-
if (rel.startsWith("..") ||
|
|
2949
|
-
const segs = rel.split(
|
|
3819
|
+
if (!fs21.existsSync(absPath) || !STORY_PLAN.test(path11.basename(absPath))) return void 0;
|
|
3820
|
+
const planRoot = fs21.realpathSync(path11.resolve(paths.plansDir));
|
|
3821
|
+
const fileReal = fs21.realpathSync(path11.resolve(absPath));
|
|
3822
|
+
const rel = path11.relative(planRoot, fileReal);
|
|
3823
|
+
if (rel.startsWith("..") || path11.isAbsolute(rel)) return void 0;
|
|
3824
|
+
const segs = rel.split(path11.sep);
|
|
2950
3825
|
if (segs.length < 2) return void 0;
|
|
2951
3826
|
const feature = segs[0];
|
|
2952
|
-
const file = segs.slice(1).join(
|
|
3827
|
+
const file = segs.slice(1).join(path11.sep);
|
|
2953
3828
|
if (featureFilter && feature !== featureFilter) return void 0;
|
|
2954
|
-
return { feature, file, abs:
|
|
3829
|
+
return { feature, file, abs: path11.resolve(absPath) };
|
|
2955
3830
|
}
|
|
2956
3831
|
function resolvePlanFromArg(arg, paths, featureFilter) {
|
|
2957
3832
|
const t = arg.trim();
|
|
2958
3833
|
if (!t) return void 0;
|
|
2959
|
-
const asPath =
|
|
3834
|
+
const asPath = path11.isAbsolute(t) ? t : path11.resolve(process.cwd(), t);
|
|
2960
3835
|
const a = tryParsePlanFilePath(asPath, paths, featureFilter);
|
|
2961
3836
|
if (a) return a;
|
|
2962
|
-
if (!
|
|
2963
|
-
const under = tryParsePlanFilePath(
|
|
3837
|
+
if (!path11.isAbsolute(t)) {
|
|
3838
|
+
const under = tryParsePlanFilePath(path11.join(paths.plansDir, t), paths, featureFilter);
|
|
2964
3839
|
if (under) return under;
|
|
2965
3840
|
}
|
|
2966
3841
|
if (isSequenceToken(t)) {
|
|
@@ -2990,24 +3865,24 @@ function countOverviewDataRows(overviewContent) {
|
|
|
2990
3865
|
return n;
|
|
2991
3866
|
}
|
|
2992
3867
|
function featureStoriesDirEmpty(paths, feature) {
|
|
2993
|
-
const d =
|
|
2994
|
-
if (!
|
|
2995
|
-
return
|
|
3868
|
+
const d = path11.join(paths.storiesDir, feature);
|
|
3869
|
+
if (!fs21.existsSync(d)) return true;
|
|
3870
|
+
return fs21.readdirSync(d).length === 0;
|
|
2996
3871
|
}
|
|
2997
3872
|
|
|
2998
3873
|
// src/core/story-mutations.ts
|
|
2999
3874
|
function writeNewStoryToDisk(input5) {
|
|
3000
3875
|
const { paths, config: config2, featureSlug, storyFolderName, id, title, noTracker, sourceBlock, fetchedIssue } = input5;
|
|
3001
|
-
const storyDir =
|
|
3002
|
-
const intakeFile =
|
|
3003
|
-
const attachmentsDir =
|
|
3004
|
-
if (
|
|
3005
|
-
const rel =
|
|
3876
|
+
const storyDir = path12.join(paths.storiesDir, featureSlug, storyFolderName);
|
|
3877
|
+
const intakeFile = path12.join(storyDir, "intake.md");
|
|
3878
|
+
const attachmentsDir = path12.join(storyDir, "attachments");
|
|
3879
|
+
if (fs22.existsSync(intakeFile)) {
|
|
3880
|
+
const rel = path12.relative(paths.root, intakeFile);
|
|
3006
3881
|
throw new Error(
|
|
3007
3882
|
`Intake already exists: ${intakeFile}. Run \`squad new-plan ${rel}\` to plan it, or \`squad rm story\` to remove the story first.`
|
|
3008
3883
|
);
|
|
3009
3884
|
}
|
|
3010
|
-
|
|
3885
|
+
fs22.mkdirSync(attachmentsDir, { recursive: true });
|
|
3011
3886
|
const template = readBundledPrompt("intake.md");
|
|
3012
3887
|
const rendered = render(
|
|
3013
3888
|
template,
|
|
@@ -3022,7 +3897,7 @@ function writeNewStoryToDisk(input5) {
|
|
|
3022
3897
|
);
|
|
3023
3898
|
const skippedPreamble = noTracker && config2.tracker.type !== "none" && !sourceBlock ? buildSkippedTrackerPreamble(config2.tracker.type) : "";
|
|
3024
3899
|
writeFileSafe(intakeFile, skippedPreamble + sourceBlock + rendered, false);
|
|
3025
|
-
ensureFeatureOverview(
|
|
3900
|
+
ensureFeatureOverview(path12.join(paths.plansDir, featureSlug), featureSlug);
|
|
3026
3901
|
return { storyDir, intakePath: intakeFile, feature: featureSlug, id: storyFolderName };
|
|
3027
3902
|
}
|
|
3028
3903
|
function createStoryRecord(input5) {
|
|
@@ -3071,16 +3946,16 @@ async function createStoryRecordFromIssue(input5) {
|
|
|
3071
3946
|
}
|
|
3072
3947
|
const title = input5.issue.title;
|
|
3073
3948
|
const storyFolderName = trackerIdForFilename(input5.config.tracker.type, id);
|
|
3074
|
-
const storyDir =
|
|
3075
|
-
const intakeFile =
|
|
3076
|
-
const attachmentsDir =
|
|
3077
|
-
if (
|
|
3078
|
-
const rel =
|
|
3949
|
+
const storyDir = path12.join(input5.paths.storiesDir, feature, storyFolderName);
|
|
3950
|
+
const intakeFile = path12.join(storyDir, "intake.md");
|
|
3951
|
+
const attachmentsDir = path12.join(storyDir, "attachments");
|
|
3952
|
+
if (fs22.existsSync(intakeFile)) {
|
|
3953
|
+
const rel = path12.relative(input5.paths.root, intakeFile);
|
|
3079
3954
|
throw new Error(
|
|
3080
3955
|
`Intake already exists: ${intakeFile}. Run \`squad new-plan ${rel}\` to plan it, or \`squad rm story\` to remove the story first.`
|
|
3081
3956
|
);
|
|
3082
3957
|
}
|
|
3083
|
-
|
|
3958
|
+
fs22.mkdirSync(attachmentsDir, { recursive: true });
|
|
3084
3959
|
let downloads = [];
|
|
3085
3960
|
if (input5.withAttachments && input5.issue.attachments.length > 0) {
|
|
3086
3961
|
downloads = await input5.client.downloadAttachments(input5.issue.attachments, attachmentsDir);
|
|
@@ -3108,10 +3983,10 @@ async function createStoryRecordFromIssue(input5) {
|
|
|
3108
3983
|
function deleteStoryRecord(input5) {
|
|
3109
3984
|
const targets = [input5.story.storyDir];
|
|
3110
3985
|
if (input5.story.planFile) {
|
|
3111
|
-
targets.push(
|
|
3986
|
+
targets.push(path12.join(input5.paths.plansDir, input5.story.feature, input5.story.planFile));
|
|
3112
3987
|
}
|
|
3113
3988
|
const result = trashOrDelete(targets, input5.paths.trashDir, input5.trash);
|
|
3114
|
-
removeOverviewRow(
|
|
3989
|
+
removeOverviewRow(path12.join(input5.paths.plansDir, input5.story.feature), input5.story.id);
|
|
3115
3990
|
return result;
|
|
3116
3991
|
}
|
|
3117
3992
|
|
|
@@ -3221,15 +4096,15 @@ async function runNewStory(featureSlug, opts) {
|
|
|
3221
4096
|
'Could not derive a story folder name. Pass `--title "<hint>"` when using `--no-tracker`, or `--id <tracker-id>` to name the folder after the tracker id.'
|
|
3222
4097
|
);
|
|
3223
4098
|
}
|
|
3224
|
-
const storyDir =
|
|
3225
|
-
const intakeFile =
|
|
3226
|
-
const attachmentsDir =
|
|
3227
|
-
if (
|
|
4099
|
+
const storyDir = path13.join(paths.storiesDir, slug, storyFolderName);
|
|
4100
|
+
const intakeFile = path13.join(storyDir, "intake.md");
|
|
4101
|
+
const attachmentsDir = path13.join(storyDir, "attachments");
|
|
4102
|
+
if (fs23.existsSync(intakeFile)) {
|
|
3228
4103
|
throw new Error(
|
|
3229
|
-
`Intake already exists: ${intakeFile}. Run \`squad new-plan ${
|
|
4104
|
+
`Intake already exists: ${intakeFile}. Run \`squad new-plan ${path13.relative(root, intakeFile)}\` to plan it, or \`squad rm story\` to remove the story first.`
|
|
3230
4105
|
);
|
|
3231
4106
|
}
|
|
3232
|
-
|
|
4107
|
+
fs23.mkdirSync(attachmentsDir, { recursive: true });
|
|
3233
4108
|
let sourceBlock = "";
|
|
3234
4109
|
let fetchedSummary;
|
|
3235
4110
|
let fetchedIssue;
|
|
@@ -3259,14 +4134,14 @@ async function runNewStory(featureSlug, opts) {
|
|
|
3259
4134
|
sourceBlock,
|
|
3260
4135
|
fetchedIssue
|
|
3261
4136
|
});
|
|
3262
|
-
const relativePath =
|
|
3263
|
-
const relAttachDir =
|
|
4137
|
+
const relativePath = path13.relative(root, intakeFile);
|
|
4138
|
+
const relAttachDir = path13.relative(root, attachmentsDir);
|
|
3264
4139
|
success("Created intake");
|
|
3265
4140
|
if (noTracker && config2.tracker.type !== "none") {
|
|
3266
4141
|
warning("Created without a tracker link. Use `squad tracker link` later to attach one.");
|
|
3267
4142
|
}
|
|
3268
4143
|
kv("intake", relativePath, 11);
|
|
3269
|
-
kv("attachments", relAttachDir +
|
|
4144
|
+
kv("attachments", relAttachDir + path13.sep, 11);
|
|
3270
4145
|
if (fetchedSummary && fetchedSummary.written > 0) {
|
|
3271
4146
|
kv("fetched", `${fetchedSummary.id} \xB7 ${fetchedSummary.written} attachments saved`, 11);
|
|
3272
4147
|
}
|
|
@@ -3275,7 +4150,7 @@ async function runNewStory(featureSlug, opts) {
|
|
|
3275
4150
|
info(`squad new-plan ${relativePath}`);
|
|
3276
4151
|
}
|
|
3277
4152
|
async function tryFetchIssue(config2, id, opts, paths, attachmentsDir) {
|
|
3278
|
-
const secretsFromFile =
|
|
4153
|
+
const secretsFromFile = fs23.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
3279
4154
|
const secrets = overlayTrackerEnv(secretsFromFile);
|
|
3280
4155
|
const resolution = clientFor(config2, secrets);
|
|
3281
4156
|
if (resolution.error) {
|
|
@@ -3332,12 +4207,12 @@ function truncate(s, n) {
|
|
|
3332
4207
|
}
|
|
3333
4208
|
|
|
3334
4209
|
// src/commands/new-plan.ts
|
|
3335
|
-
import
|
|
3336
|
-
import
|
|
3337
|
-
import { confirm as
|
|
4210
|
+
import fs35 from "fs";
|
|
4211
|
+
import path25 from "path";
|
|
4212
|
+
import { confirm as confirm7, select as select6 } from "@inquirer/prompts";
|
|
3338
4213
|
|
|
3339
4214
|
// src/utils/clipboard.ts
|
|
3340
|
-
import { spawn, spawnSync } from "child_process";
|
|
4215
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
3341
4216
|
async function copyToClipboard(text) {
|
|
3342
4217
|
const candidates = clipboardCandidates();
|
|
3343
4218
|
if (candidates.length === 0) {
|
|
@@ -3360,7 +4235,7 @@ function tryCopy(cmd, text) {
|
|
|
3360
4235
|
return new Promise((resolve2) => {
|
|
3361
4236
|
let child;
|
|
3362
4237
|
try {
|
|
3363
|
-
child =
|
|
4238
|
+
child = spawn2(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
3364
4239
|
} catch (err2) {
|
|
3365
4240
|
resolve2({ ok: false, reason: `${cmd.command} failed to spawn: ${err2.message}` });
|
|
3366
4241
|
return;
|
|
@@ -3425,16 +4300,16 @@ function noCandidateReason() {
|
|
|
3425
4300
|
}
|
|
3426
4301
|
|
|
3427
4302
|
// src/planner/loop.ts
|
|
3428
|
-
import
|
|
3429
|
-
import
|
|
4303
|
+
import path21 from "path";
|
|
4304
|
+
import fs31 from "fs";
|
|
3430
4305
|
import { APICallError as APICallError3 } from "ai";
|
|
3431
4306
|
|
|
3432
4307
|
// src/planner/tools/index.ts
|
|
3433
4308
|
import { randomUUID } from "crypto";
|
|
3434
4309
|
|
|
3435
4310
|
// src/planner/tools/read-file.ts
|
|
3436
|
-
import
|
|
3437
|
-
import
|
|
4311
|
+
import fs24 from "fs";
|
|
4312
|
+
import path14 from "path";
|
|
3438
4313
|
import { tool } from "ai";
|
|
3439
4314
|
import { z } from "zod";
|
|
3440
4315
|
var MAX_BYTES_PER_FULL_READ = 32e3;
|
|
@@ -3469,15 +4344,15 @@ function readFileTool(root, budget, input5) {
|
|
|
3469
4344
|
if (offset === void 0 !== (limit === void 0)) {
|
|
3470
4345
|
return err("read_file: pass both offset and limit for a ranged read, or neither for a full read.");
|
|
3471
4346
|
}
|
|
3472
|
-
const resolved =
|
|
3473
|
-
const relCheck =
|
|
3474
|
-
if (relCheck.startsWith("..") ||
|
|
4347
|
+
const resolved = path14.resolve(root, raw);
|
|
4348
|
+
const relCheck = path14.relative(root, resolved);
|
|
4349
|
+
if (relCheck.startsWith("..") || path14.isAbsolute(relCheck)) {
|
|
3475
4350
|
return err(`read_file: path "${raw}" escapes the project root and was refused.`);
|
|
3476
4351
|
}
|
|
3477
|
-
if (!
|
|
4352
|
+
if (!fs24.existsSync(resolved)) {
|
|
3478
4353
|
return err(`read_file: not found "${raw}".`);
|
|
3479
4354
|
}
|
|
3480
|
-
const stat =
|
|
4355
|
+
const stat = fs24.statSync(resolved);
|
|
3481
4356
|
if (stat.isDirectory()) {
|
|
3482
4357
|
return err(`read_file: "${raw}" is a directory. Ask for a specific file path.`);
|
|
3483
4358
|
}
|
|
@@ -3495,7 +4370,7 @@ function readFileTool(root, budget, input5) {
|
|
|
3495
4370
|
}
|
|
3496
4371
|
let buf;
|
|
3497
4372
|
try {
|
|
3498
|
-
buf =
|
|
4373
|
+
buf = fs24.readFileSync(resolved);
|
|
3499
4374
|
} catch (e) {
|
|
3500
4375
|
return err(`read_file: failed to read "${raw}" (${e.message}).`);
|
|
3501
4376
|
}
|
|
@@ -3518,7 +4393,7 @@ function readFileRanged(resolved, rawRel, budget, fileSize, offset, limit) {
|
|
|
3518
4393
|
}
|
|
3519
4394
|
let data;
|
|
3520
4395
|
try {
|
|
3521
|
-
data =
|
|
4396
|
+
data = fs24.readFileSync(resolved);
|
|
3522
4397
|
} catch (e) {
|
|
3523
4398
|
return err(`read_file: failed to read "${rawRel}" (${e.message}).`);
|
|
3524
4399
|
}
|
|
@@ -3574,14 +4449,14 @@ function looksBinary(buf) {
|
|
|
3574
4449
|
}
|
|
3575
4450
|
|
|
3576
4451
|
// src/planner/tools/grep.ts
|
|
3577
|
-
import
|
|
3578
|
-
import
|
|
4452
|
+
import fs26 from "fs";
|
|
4453
|
+
import path16 from "path";
|
|
3579
4454
|
import { tool as tool2 } from "ai";
|
|
3580
4455
|
import { z as z2 } from "zod";
|
|
3581
4456
|
|
|
3582
4457
|
// src/core/repo-map.ts
|
|
3583
|
-
import
|
|
3584
|
-
import
|
|
4458
|
+
import fs25 from "fs";
|
|
4459
|
+
import path15 from "path";
|
|
3585
4460
|
import ignore from "ignore";
|
|
3586
4461
|
var DEFAULT_IGNORES = [
|
|
3587
4462
|
"node_modules",
|
|
@@ -3597,10 +4472,10 @@ var DEFAULT_IGNORES = [
|
|
|
3597
4472
|
];
|
|
3598
4473
|
function buildRepoIgnore(root) {
|
|
3599
4474
|
const ig = ignore().add(DEFAULT_IGNORES);
|
|
3600
|
-
const gitignore =
|
|
3601
|
-
if (
|
|
4475
|
+
const gitignore = path15.join(root, ".gitignore");
|
|
4476
|
+
if (fs25.existsSync(gitignore)) {
|
|
3602
4477
|
try {
|
|
3603
|
-
ig.add(
|
|
4478
|
+
ig.add(fs25.readFileSync(gitignore, "utf8"));
|
|
3604
4479
|
} catch {
|
|
3605
4480
|
}
|
|
3606
4481
|
}
|
|
@@ -3656,7 +4531,7 @@ function renderDirTree(name, tree, depth) {
|
|
|
3656
4531
|
function renderRepoTree(entries) {
|
|
3657
4532
|
const root = { files: [], dirs: /* @__PURE__ */ new Map() };
|
|
3658
4533
|
for (const e of entries) {
|
|
3659
|
-
const parts = e.rel.split(
|
|
4534
|
+
const parts = e.rel.split(path15.sep).filter(Boolean);
|
|
3660
4535
|
if (parts.length === 0) continue;
|
|
3661
4536
|
insertTree(root, parts, e.size);
|
|
3662
4537
|
}
|
|
@@ -3666,26 +4541,26 @@ function collectFileEntries(root, dir, ig, out, max, includeHidden) {
|
|
|
3666
4541
|
if (out.length >= max) return out;
|
|
3667
4542
|
let dirents;
|
|
3668
4543
|
try {
|
|
3669
|
-
dirents =
|
|
4544
|
+
dirents = fs25.readdirSync(dir, { withFileTypes: true });
|
|
3670
4545
|
} catch {
|
|
3671
4546
|
return out;
|
|
3672
4547
|
}
|
|
3673
4548
|
for (const entry of dirents) {
|
|
3674
4549
|
if (out.length >= max) break;
|
|
3675
4550
|
if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
|
|
3676
|
-
const abs =
|
|
3677
|
-
const rel =
|
|
4551
|
+
const abs = path15.join(dir, entry.name);
|
|
4552
|
+
const rel = path15.relative(root, abs);
|
|
3678
4553
|
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
3679
4554
|
if (entry.isDirectory()) {
|
|
3680
4555
|
collectFileEntries(root, abs, ig, out, max, includeHidden);
|
|
3681
4556
|
} else if (entry.isFile()) {
|
|
3682
4557
|
let size = 0;
|
|
3683
4558
|
try {
|
|
3684
|
-
size =
|
|
4559
|
+
size = fs25.statSync(abs).size;
|
|
3685
4560
|
} catch {
|
|
3686
4561
|
size = 0;
|
|
3687
4562
|
}
|
|
3688
|
-
out.push({ rel: rel.split(
|
|
4563
|
+
out.push({ rel: rel.split(path15.sep).join("/"), size });
|
|
3689
4564
|
}
|
|
3690
4565
|
}
|
|
3691
4566
|
return out;
|
|
@@ -3694,20 +4569,20 @@ function walkFlat(root, dir, ig, out, max, includeHidden) {
|
|
|
3694
4569
|
if (out.length >= max) return;
|
|
3695
4570
|
let entries;
|
|
3696
4571
|
try {
|
|
3697
|
-
entries =
|
|
4572
|
+
entries = fs25.readdirSync(dir, { withFileTypes: true });
|
|
3698
4573
|
} catch {
|
|
3699
4574
|
return;
|
|
3700
4575
|
}
|
|
3701
4576
|
for (const entry of entries) {
|
|
3702
4577
|
if (out.length >= max) return;
|
|
3703
4578
|
if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
|
|
3704
|
-
const abs =
|
|
3705
|
-
const rel =
|
|
4579
|
+
const abs = path15.join(dir, entry.name);
|
|
4580
|
+
const rel = path15.relative(root, abs);
|
|
3706
4581
|
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
3707
4582
|
if (entry.isDirectory()) {
|
|
3708
4583
|
walkFlat(root, abs, ig, out, max, includeHidden);
|
|
3709
4584
|
} else if (entry.isFile()) {
|
|
3710
|
-
out.push(rel.split(
|
|
4585
|
+
out.push(rel.split(path15.sep).join("/"));
|
|
3711
4586
|
}
|
|
3712
4587
|
}
|
|
3713
4588
|
}
|
|
@@ -3743,12 +4618,12 @@ function runGrep(root, budget, args) {
|
|
|
3743
4618
|
return { content: `grep: invalid pattern (${e.message}).`, isError: true };
|
|
3744
4619
|
}
|
|
3745
4620
|
const ig = buildRepoIgnore(root);
|
|
3746
|
-
const scopeRoot = args.path?.trim() ?
|
|
3747
|
-
const relScope =
|
|
3748
|
-
if (relScope.startsWith("..") ||
|
|
4621
|
+
const scopeRoot = args.path?.trim() ? path16.resolve(root, args.path) : root;
|
|
4622
|
+
const relScope = path16.relative(root, scopeRoot);
|
|
4623
|
+
if (relScope.startsWith("..") || path16.isAbsolute(relScope)) {
|
|
3749
4624
|
return { content: `grep: scope path escapes the project root and was refused.`, isError: true };
|
|
3750
4625
|
}
|
|
3751
|
-
if (!
|
|
4626
|
+
if (!fs26.existsSync(scopeRoot)) {
|
|
3752
4627
|
return { content: `grep: scope path not found "${args.path}".`, isError: true };
|
|
3753
4628
|
}
|
|
3754
4629
|
const matches = [];
|
|
@@ -3756,28 +4631,28 @@ function runGrep(root, budget, args) {
|
|
|
3756
4631
|
if (matches.length >= MAX_GREP_MATCHES) return;
|
|
3757
4632
|
let entries;
|
|
3758
4633
|
try {
|
|
3759
|
-
entries =
|
|
4634
|
+
entries = fs26.readdirSync(dir, { withFileTypes: true });
|
|
3760
4635
|
} catch {
|
|
3761
4636
|
return;
|
|
3762
4637
|
}
|
|
3763
4638
|
for (const entry of entries) {
|
|
3764
4639
|
if (matches.length >= MAX_GREP_MATCHES) return;
|
|
3765
|
-
const abs =
|
|
3766
|
-
const rel =
|
|
4640
|
+
const abs = path16.join(dir, entry.name);
|
|
4641
|
+
const rel = path16.relative(root, abs).split(path16.sep).join("/");
|
|
3767
4642
|
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
3768
4643
|
if (entry.isDirectory()) {
|
|
3769
4644
|
walk2(abs);
|
|
3770
4645
|
} else if (entry.isFile()) {
|
|
3771
4646
|
let st;
|
|
3772
4647
|
try {
|
|
3773
|
-
st =
|
|
4648
|
+
st = fs26.statSync(abs);
|
|
3774
4649
|
} catch {
|
|
3775
4650
|
continue;
|
|
3776
4651
|
}
|
|
3777
4652
|
if (st.size > MAX_GREP_FILE_SIZE_BYTES) continue;
|
|
3778
4653
|
let buf;
|
|
3779
4654
|
try {
|
|
3780
|
-
buf =
|
|
4655
|
+
buf = fs26.readFileSync(abs);
|
|
3781
4656
|
} catch {
|
|
3782
4657
|
continue;
|
|
3783
4658
|
}
|
|
@@ -3796,14 +4671,14 @@ function runGrep(root, budget, args) {
|
|
|
3796
4671
|
}
|
|
3797
4672
|
}
|
|
3798
4673
|
};
|
|
3799
|
-
if (
|
|
3800
|
-
const rel =
|
|
4674
|
+
if (fs26.statSync(scopeRoot).isFile()) {
|
|
4675
|
+
const rel = path16.relative(root, scopeRoot).split(path16.sep).join("/");
|
|
3801
4676
|
if (ig.ignores(rel)) {
|
|
3802
4677
|
return { content: `grep: file is ignored by repository filters.`, isError: true };
|
|
3803
4678
|
}
|
|
3804
4679
|
let st;
|
|
3805
4680
|
try {
|
|
3806
|
-
st =
|
|
4681
|
+
st = fs26.statSync(scopeRoot);
|
|
3807
4682
|
} catch {
|
|
3808
4683
|
return { content: `grep: not found.`, isError: true };
|
|
3809
4684
|
}
|
|
@@ -3812,7 +4687,7 @@ function runGrep(root, budget, args) {
|
|
|
3812
4687
|
}
|
|
3813
4688
|
let buf;
|
|
3814
4689
|
try {
|
|
3815
|
-
buf =
|
|
4690
|
+
buf = fs26.readFileSync(scopeRoot);
|
|
3816
4691
|
} catch (e) {
|
|
3817
4692
|
return { content: `grep: failed to read (${e.message}).`, isError: true };
|
|
3818
4693
|
}
|
|
@@ -3846,8 +4721,8 @@ ${matches.join("\n")}` : `No matches found for "${args.pattern}".`;
|
|
|
3846
4721
|
}
|
|
3847
4722
|
|
|
3848
4723
|
// src/planner/tools/list-dir.ts
|
|
3849
|
-
import
|
|
3850
|
-
import
|
|
4724
|
+
import fs27 from "fs";
|
|
4725
|
+
import path17 from "path";
|
|
3851
4726
|
import { tool as tool3 } from "ai";
|
|
3852
4727
|
import { z as z3 } from "zod";
|
|
3853
4728
|
var MAX_ENTRIES_PER_LIST = 200;
|
|
@@ -3865,44 +4740,44 @@ function runListDir(root, budget, rawPath) {
|
|
|
3865
4740
|
};
|
|
3866
4741
|
}
|
|
3867
4742
|
const normalized = rawPath.trim() === "" ? "." : rawPath.trim();
|
|
3868
|
-
const resolved =
|
|
3869
|
-
const relCheck =
|
|
3870
|
-
if (relCheck.startsWith("..") ||
|
|
4743
|
+
const resolved = path17.resolve(root, normalized);
|
|
4744
|
+
const relCheck = path17.relative(root, resolved);
|
|
4745
|
+
if (relCheck.startsWith("..") || path17.isAbsolute(relCheck)) {
|
|
3871
4746
|
return {
|
|
3872
4747
|
content: `list_dir: path "${rawPath}" escapes the project root and was refused.`,
|
|
3873
4748
|
isError: true
|
|
3874
4749
|
};
|
|
3875
4750
|
}
|
|
3876
|
-
const relNorm = relCheck.split(
|
|
4751
|
+
const relNorm = relCheck.split(path17.sep).join("/");
|
|
3877
4752
|
const ig = buildRepoIgnore(root);
|
|
3878
4753
|
if (relNorm && (ig.ignores(relNorm) || ig.ignores(`${relNorm}/`))) {
|
|
3879
4754
|
return { content: "list_dir: (directory ignored by repo-map filter)", isError: true };
|
|
3880
4755
|
}
|
|
3881
|
-
if (!
|
|
4756
|
+
if (!fs27.existsSync(resolved)) {
|
|
3882
4757
|
return { content: `list_dir: not found "${rawPath}".`, isError: true };
|
|
3883
4758
|
}
|
|
3884
|
-
const st =
|
|
4759
|
+
const st = fs27.statSync(resolved);
|
|
3885
4760
|
if (!st.isDirectory()) {
|
|
3886
4761
|
return { content: `list_dir: "${rawPath}" is not a directory.`, isError: true };
|
|
3887
4762
|
}
|
|
3888
4763
|
let dirents;
|
|
3889
4764
|
try {
|
|
3890
|
-
dirents =
|
|
4765
|
+
dirents = fs27.readdirSync(resolved, { withFileTypes: true });
|
|
3891
4766
|
} catch (e) {
|
|
3892
4767
|
return { content: `list_dir: failed to read (${e.message}).`, isError: true };
|
|
3893
4768
|
}
|
|
3894
4769
|
const rows = [];
|
|
3895
4770
|
for (const d of dirents) {
|
|
3896
4771
|
if (rows.length >= MAX_ENTRIES_PER_LIST) break;
|
|
3897
|
-
const childAbs =
|
|
3898
|
-
const childRel =
|
|
4772
|
+
const childAbs = path17.join(resolved, d.name);
|
|
4773
|
+
const childRel = path17.relative(root, childAbs).split(path17.sep).join("/");
|
|
3899
4774
|
if (childRel && (ig.ignores(childRel) || d.isDirectory() && ig.ignores(`${childRel}/`))) continue;
|
|
3900
4775
|
if (d.isDirectory()) {
|
|
3901
4776
|
rows.push({ type: "d", name: d.name + "/", size: null });
|
|
3902
4777
|
} else if (d.isFile()) {
|
|
3903
4778
|
let size = 0;
|
|
3904
4779
|
try {
|
|
3905
|
-
size =
|
|
4780
|
+
size = fs27.statSync(childAbs).size;
|
|
3906
4781
|
} catch {
|
|
3907
4782
|
size = 0;
|
|
3908
4783
|
}
|
|
@@ -4181,96 +5056,9 @@ function vercelToolsFromDefinitions(defs) {
|
|
|
4181
5056
|
return out;
|
|
4182
5057
|
}
|
|
4183
5058
|
|
|
4184
|
-
// src/planner/provider-errors.ts
|
|
4185
|
-
function detectModelNotFound(provider, model, status, rawBody) {
|
|
4186
|
-
if (status !== 404) return void 0;
|
|
4187
|
-
const body = rawBody.toLowerCase();
|
|
4188
|
-
const hints = ["not_found_error", "model_not_found", "not_found", "model:"];
|
|
4189
|
-
if (!hints.some((h) => body.includes(h))) return void 0;
|
|
4190
|
-
return { provider, model, status, rawBody };
|
|
4191
|
-
}
|
|
4192
|
-
function modelNotFoundMessage(err2) {
|
|
4193
|
-
return [
|
|
4194
|
-
`The ${err2.provider} planner model "${err2.model}" is no longer available.`,
|
|
4195
|
-
"",
|
|
4196
|
-
"Recovery options:",
|
|
4197
|
-
` 1. Run \`squad upgrade\` to install a newer squad-kit (or install manually from npm).`,
|
|
4198
|
-
` 2. Run \`squad config set planner --provider openai\` (or another \`--provider\`) to switch models via a different provider, then save a key when prompted.`,
|
|
4199
|
-
` 3. Run \`squad config set planner\` to set \`planner.modelOverride.${err2.provider}\` to a still-valid id (no raw YAML), then \`squad doctor\` to confirm.`,
|
|
4200
|
-
"",
|
|
4201
|
-
`Raw provider response: ${err2.rawBody.slice(0, 200)}`
|
|
4202
|
-
].join("\n");
|
|
4203
|
-
}
|
|
4204
|
-
function detectRateLimit(provider, status, headers, rawBody) {
|
|
4205
|
-
if (status !== 429) return void 0;
|
|
4206
|
-
return {
|
|
4207
|
-
provider,
|
|
4208
|
-
status: 429,
|
|
4209
|
-
retryAfterSec: parseRetryAfter(headers, rawBody),
|
|
4210
|
-
rawBody
|
|
4211
|
-
};
|
|
4212
|
-
}
|
|
4213
|
-
function parseRetryAfterSec(headers) {
|
|
4214
|
-
const raw = headerGet(headers, "retry-after");
|
|
4215
|
-
if (!raw) return void 0;
|
|
4216
|
-
const sec = Number(raw);
|
|
4217
|
-
if (Number.isFinite(sec) && sec >= 0) return Math.ceil(sec);
|
|
4218
|
-
const asDate = Date.parse(raw);
|
|
4219
|
-
if (Number.isFinite(asDate)) {
|
|
4220
|
-
return Math.max(0, Math.ceil((asDate - Date.now()) / 1e3));
|
|
4221
|
-
}
|
|
4222
|
-
return void 0;
|
|
4223
|
-
}
|
|
4224
|
-
function parseRetryAfter(headers, body) {
|
|
4225
|
-
const fromHeader = parseRetryAfterSec(headers);
|
|
4226
|
-
if (fromHeader !== void 0) return fromHeader;
|
|
4227
|
-
const bodyMatch = /"retryDelay"\s*:\s*"(\d+)(?:\.\d+)?s"/i.exec(body);
|
|
4228
|
-
if (bodyMatch) {
|
|
4229
|
-
const sec = Number(bodyMatch[1]);
|
|
4230
|
-
if (Number.isFinite(sec) && sec >= 0) return sec;
|
|
4231
|
-
}
|
|
4232
|
-
return void 0;
|
|
4233
|
-
}
|
|
4234
|
-
function headerGet(h, name) {
|
|
4235
|
-
if (typeof h.get === "function") {
|
|
4236
|
-
return h.get(name);
|
|
4237
|
-
}
|
|
4238
|
-
const key = name.toLowerCase();
|
|
4239
|
-
const rec = h;
|
|
4240
|
-
for (const k of Object.keys(rec)) {
|
|
4241
|
-
if (k.toLowerCase() === key) return rec[k] ?? null;
|
|
4242
|
-
}
|
|
4243
|
-
return null;
|
|
4244
|
-
}
|
|
4245
|
-
function rateLimitMessage(err2) {
|
|
4246
|
-
const waitHint = err2.retryAfterSec ?? 60;
|
|
4247
|
-
const limitsUrl = {
|
|
4248
|
-
anthropic: "https://console.anthropic.com/settings/limits",
|
|
4249
|
-
openai: "https://platform.openai.com/settings/organization/limits",
|
|
4250
|
-
google: "https://aistudio.google.com/app/plan_information"
|
|
4251
|
-
};
|
|
4252
|
-
const headline = err2.retryAfterSec ? `${err2.provider} rate limit hit \u2014 provider asked us to wait ${err2.retryAfterSec}s before retrying.` : `${err2.provider} rate limit hit.`;
|
|
4253
|
-
const cap = err2.maxRetrySec ?? 90;
|
|
4254
|
-
const retried = err2.retrySkippedReason === "retry_after_too_long" ? `squad-kit did not auto-retry: the provider's ${err2.retryAfterSec}s wait is longer than our ${cap}s cap, so retrying would just burn another request inside the same throttle window.` : err2.retryAlreadyAttempted ? "squad-kit already retried once automatically and was throttled again \u2014 your org is firmly over its per-minute quota." : "squad-kit aborted before retrying.";
|
|
4255
|
-
return [
|
|
4256
|
-
headline,
|
|
4257
|
-
retried,
|
|
4258
|
-
"",
|
|
4259
|
-
"Recovery options:",
|
|
4260
|
-
` 1. Wait ${waitHint}s and rerun \`squad new-plan --api\`. Per-minute limits reset quickly.`,
|
|
4261
|
-
` 2. Switch to a smaller planner model: \`squad config set planner\` (pick a cheaper model id).`,
|
|
4262
|
-
` 3. Tighten \`planner.budget\` in \`.squad/config.yaml\` (smaller \`maxContextBytes\` / \`maxFileReads\`) so each request carries fewer tokens.`,
|
|
4263
|
-
` 4. Upgrade your ${err2.provider} tier: ${limitsUrl[err2.provider]}.`,
|
|
4264
|
-
"",
|
|
4265
|
-
"Full runbook: docs/migrating-from-0.1.md (see \xA78. If something goes wrong).",
|
|
4266
|
-
"",
|
|
4267
|
-
`Raw provider response: ${err2.rawBody.slice(0, 200)}`
|
|
4268
|
-
].join("\n");
|
|
4269
|
-
}
|
|
4270
|
-
|
|
4271
5059
|
// src/core/runs.ts
|
|
4272
|
-
import
|
|
4273
|
-
import
|
|
5060
|
+
import fs29 from "fs/promises";
|
|
5061
|
+
import path19 from "path";
|
|
4274
5062
|
import { randomBytes } from "crypto";
|
|
4275
5063
|
|
|
4276
5064
|
// src/core/run-retention.ts
|
|
@@ -4278,9 +5066,9 @@ var RUN_HISTORY_RING_SIZE = 20;
|
|
|
4278
5066
|
var RUN_EVENT_JSONL_UNCOMPRESSED_HEAD = 5;
|
|
4279
5067
|
|
|
4280
5068
|
// src/core/run-events-store.ts
|
|
4281
|
-
import
|
|
5069
|
+
import fs28, { createWriteStream } from "fs";
|
|
4282
5070
|
import fsPromises from "fs/promises";
|
|
4283
|
-
import
|
|
5071
|
+
import path18 from "path";
|
|
4284
5072
|
import readline from "readline";
|
|
4285
5073
|
import { pipeline } from "stream/promises";
|
|
4286
5074
|
import zlib from "zlib";
|
|
@@ -4288,10 +5076,14 @@ var SUFFIX_JSONL = ".events.jsonl";
|
|
|
4288
5076
|
var SUFFIX_GZ = ".events.jsonl.gz";
|
|
4289
5077
|
function redactPlannerEventForDisk(e) {
|
|
4290
5078
|
if (e.kind === "thinking_delta") return { ...e, delta: "" };
|
|
5079
|
+
if (e.kind === "auth_info") {
|
|
5080
|
+
const { account: _account, ...rest } = e;
|
|
5081
|
+
return rest;
|
|
5082
|
+
}
|
|
4291
5083
|
return e;
|
|
4292
5084
|
}
|
|
4293
5085
|
function runsDir(paths) {
|
|
4294
|
-
return
|
|
5086
|
+
return path18.join(paths.squadDir, "runs");
|
|
4295
5087
|
}
|
|
4296
5088
|
function logAppendErrorOnce(err2) {
|
|
4297
5089
|
console.error("[run-events-store] append failed:", err2);
|
|
@@ -4348,8 +5140,8 @@ var RunEventsStoreImpl = class {
|
|
|
4348
5140
|
};
|
|
4349
5141
|
function createRunEventsStore(paths, runId) {
|
|
4350
5142
|
const dir = runsDir(paths);
|
|
4351
|
-
|
|
4352
|
-
const file =
|
|
5143
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
5144
|
+
const file = path18.join(dir, `${runId}${SUFFIX_JSONL}`);
|
|
4353
5145
|
return new RunEventsStoreImpl(file);
|
|
4354
5146
|
}
|
|
4355
5147
|
var RunEventsReaderImpl = class {
|
|
@@ -4374,7 +5166,7 @@ var RunEventsReaderImpl = class {
|
|
|
4374
5166
|
const limit = opts?.limit ?? Number.POSITIVE_INFINITY;
|
|
4375
5167
|
let logical = 0;
|
|
4376
5168
|
let yielded = 0;
|
|
4377
|
-
const stream =
|
|
5169
|
+
const stream = fs28.createReadStream(this.filePath);
|
|
4378
5170
|
const input5 = this.useGunzip ? stream.pipe(zlib.createGunzip()) : stream;
|
|
4379
5171
|
const rl = readline.createInterface({ input: input5, crlfDelay: Infinity });
|
|
4380
5172
|
try {
|
|
@@ -4402,8 +5194,8 @@ var RunEventsReaderImpl = class {
|
|
|
4402
5194
|
};
|
|
4403
5195
|
async function openRunEventsReader(paths, runId) {
|
|
4404
5196
|
const dir = runsDir(paths);
|
|
4405
|
-
const jsonl =
|
|
4406
|
-
const gz =
|
|
5197
|
+
const jsonl = path18.join(dir, `${runId}${SUFFIX_JSONL}`);
|
|
5198
|
+
const gz = path18.join(dir, `${runId}${SUFFIX_GZ}`);
|
|
4407
5199
|
let jsonlStat = null;
|
|
4408
5200
|
let gzStat = null;
|
|
4409
5201
|
try {
|
|
@@ -4435,10 +5227,10 @@ function collectEventLogs(dir, names) {
|
|
|
4435
5227
|
kind = "jsonl";
|
|
4436
5228
|
}
|
|
4437
5229
|
if (!runId || !kind) continue;
|
|
4438
|
-
const full =
|
|
5230
|
+
const full = path18.join(dir, name);
|
|
4439
5231
|
let st;
|
|
4440
5232
|
try {
|
|
4441
|
-
st = { mtimeMs:
|
|
5233
|
+
st = { mtimeMs: fs28.statSync(full).mtimeMs };
|
|
4442
5234
|
} catch {
|
|
4443
5235
|
continue;
|
|
4444
5236
|
}
|
|
@@ -4466,7 +5258,7 @@ async function fsyncFile(filePath) {
|
|
|
4466
5258
|
async function gzipAtomic(jsonlPath, gzPath) {
|
|
4467
5259
|
const gzTmp = `${gzPath}.tmp`;
|
|
4468
5260
|
await fsPromises.rm(gzTmp, { force: true });
|
|
4469
|
-
await pipeline(
|
|
5261
|
+
await pipeline(fs28.createReadStream(jsonlPath), zlib.createGzip(), fs28.createWriteStream(gzTmp));
|
|
4470
5262
|
await fsyncFile(gzTmp);
|
|
4471
5263
|
await fsPromises.rename(gzTmp, gzPath);
|
|
4472
5264
|
await fsPromises.unlink(jsonlPath);
|
|
@@ -4481,14 +5273,14 @@ async function rotateRunEventsUnlocked(paths) {
|
|
|
4481
5273
|
}
|
|
4482
5274
|
for (const name of names) {
|
|
4483
5275
|
if (name.endsWith(`${SUFFIX_GZ}.tmp`)) {
|
|
4484
|
-
await fsPromises.rm(
|
|
5276
|
+
await fsPromises.rm(path18.join(dir, name), { force: true });
|
|
4485
5277
|
}
|
|
4486
5278
|
}
|
|
4487
5279
|
names = await fsPromises.readdir(dir);
|
|
4488
5280
|
const ordered = collectEventLogs(dir, names);
|
|
4489
5281
|
for (let i = RUN_HISTORY_RING_SIZE; i < ordered.length; i++) {
|
|
4490
5282
|
const e = ordered[i];
|
|
4491
|
-
const summary =
|
|
5283
|
+
const summary = path18.join(dir, `${e.runId}.json`);
|
|
4492
5284
|
await fsPromises.rm(summary, { force: true });
|
|
4493
5285
|
if (e.jsonlPath) await fsPromises.rm(e.jsonlPath, { force: true });
|
|
4494
5286
|
if (e.gzPath) await fsPromises.rm(e.gzPath, { force: true });
|
|
@@ -4498,7 +5290,7 @@ async function rotateRunEventsUnlocked(paths) {
|
|
|
4498
5290
|
const e = keep[i];
|
|
4499
5291
|
const jsonl = e.jsonlPath;
|
|
4500
5292
|
if (!jsonl) continue;
|
|
4501
|
-
const gz =
|
|
5293
|
+
const gz = path18.join(dir, `${e.runId}${SUFFIX_GZ}`);
|
|
4502
5294
|
await gzipAtomic(jsonl, gz);
|
|
4503
5295
|
}
|
|
4504
5296
|
}
|
|
@@ -4515,34 +5307,34 @@ function newRunId() {
|
|
|
4515
5307
|
return `${ts}-${rnd}`;
|
|
4516
5308
|
}
|
|
4517
5309
|
async function pruneOldRuns(paths) {
|
|
4518
|
-
const dir =
|
|
4519
|
-
const entries = (await
|
|
5310
|
+
const dir = path19.join(paths.squadDir, "runs");
|
|
5311
|
+
const entries = (await fs29.readdir(dir)).filter((n) => n.endsWith(".json"));
|
|
4520
5312
|
if (entries.length <= RUN_HISTORY_RING_SIZE) return;
|
|
4521
5313
|
entries.sort();
|
|
4522
5314
|
const drop = entries.slice(0, entries.length - RUN_HISTORY_RING_SIZE);
|
|
4523
5315
|
await Promise.all(
|
|
4524
5316
|
drop.map(async (n) => {
|
|
4525
|
-
const runPath =
|
|
5317
|
+
const runPath = path19.join(dir, n);
|
|
4526
5318
|
const runId = n.replace(/\.json$/u, "");
|
|
4527
|
-
await
|
|
4528
|
-
await
|
|
4529
|
-
await
|
|
5319
|
+
await fs29.rm(runPath, { force: true });
|
|
5320
|
+
await fs29.rm(path19.join(dir, `${runId}.events.jsonl`), { force: true });
|
|
5321
|
+
await fs29.rm(path19.join(dir, `${runId}.events.jsonl.gz`), { force: true });
|
|
4530
5322
|
})
|
|
4531
5323
|
);
|
|
4532
5324
|
}
|
|
4533
5325
|
async function appendRun(paths, rec) {
|
|
4534
|
-
const dir =
|
|
4535
|
-
await
|
|
4536
|
-
const file =
|
|
4537
|
-
await
|
|
5326
|
+
const dir = path19.join(paths.squadDir, "runs");
|
|
5327
|
+
await fs29.mkdir(dir, { recursive: true });
|
|
5328
|
+
const file = path19.join(dir, `${rec.runId}.json`);
|
|
5329
|
+
await fs29.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
4538
5330
|
await pruneOldRuns(paths);
|
|
4539
5331
|
await rotateRunEvents(paths);
|
|
4540
5332
|
}
|
|
4541
5333
|
async function listRuns(paths) {
|
|
4542
|
-
const dir =
|
|
5334
|
+
const dir = path19.join(paths.squadDir, "runs");
|
|
4543
5335
|
let entries;
|
|
4544
5336
|
try {
|
|
4545
|
-
entries = await
|
|
5337
|
+
entries = await fs29.readdir(dir);
|
|
4546
5338
|
} catch {
|
|
4547
5339
|
return [];
|
|
4548
5340
|
}
|
|
@@ -4550,7 +5342,7 @@ async function listRuns(paths) {
|
|
|
4550
5342
|
const out = [];
|
|
4551
5343
|
for (const f of files) {
|
|
4552
5344
|
try {
|
|
4553
|
-
const raw = await
|
|
5345
|
+
const raw = await fs29.readFile(path19.join(dir, f), "utf8");
|
|
4554
5346
|
const parsed = JSON.parse(raw);
|
|
4555
5347
|
if (parsed.version === 1) out.push(parsed);
|
|
4556
5348
|
} catch {
|
|
@@ -4887,6 +5679,11 @@ var VercelRuntime = class {
|
|
|
4887
5679
|
if (typeof apiKeyOrModel !== "string") {
|
|
4888
5680
|
throw new Error("VercelRuntime: apiKey must be a string unless injectModel is true");
|
|
4889
5681
|
}
|
|
5682
|
+
if (!apiKeyOrModel) {
|
|
5683
|
+
throw new Error(
|
|
5684
|
+
`VercelRuntime: a non-empty ${provider} API key is required. Run \`squad config set planner\` to save one.`
|
|
5685
|
+
);
|
|
5686
|
+
}
|
|
4890
5687
|
this.languageModel = resolveModel(provider, modelId, apiKeyOrModel).model;
|
|
4891
5688
|
}
|
|
4892
5689
|
async runScout(input5) {
|
|
@@ -5339,8 +6136,8 @@ async function runScout(input5) {
|
|
|
5339
6136
|
}
|
|
5340
6137
|
|
|
5341
6138
|
// src/planner/validation.ts
|
|
5342
|
-
import
|
|
5343
|
-
import
|
|
6139
|
+
import fs30 from "fs";
|
|
6140
|
+
import path20 from "path";
|
|
5344
6141
|
var CODE_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|md|json|ya?ml|css|html|py|go|rs|cs|java|kt|kts|swift|rb|php|vue|svelte)$/i;
|
|
5345
6142
|
function summariseIssuesByKind(issues) {
|
|
5346
6143
|
const out = {};
|
|
@@ -5410,21 +6207,21 @@ function trigramSimilarity(a, b) {
|
|
|
5410
6207
|
return inter / Math.sqrt(A.size * B.size);
|
|
5411
6208
|
}
|
|
5412
6209
|
function suggestPaths(root, missingRel, max = 3) {
|
|
5413
|
-
const targetBase =
|
|
6210
|
+
const targetBase = path20.basename(missingRel).toLowerCase();
|
|
5414
6211
|
const ig = buildRepoIgnore(root);
|
|
5415
6212
|
const candidates = [];
|
|
5416
6213
|
const walk2 = (dir) => {
|
|
5417
6214
|
if (candidates.length >= 2e3) return;
|
|
5418
6215
|
let ents;
|
|
5419
6216
|
try {
|
|
5420
|
-
ents =
|
|
6217
|
+
ents = fs30.readdirSync(dir, { withFileTypes: true });
|
|
5421
6218
|
} catch {
|
|
5422
6219
|
return;
|
|
5423
6220
|
}
|
|
5424
6221
|
for (const ent of ents) {
|
|
5425
6222
|
if (candidates.length >= 2e3) return;
|
|
5426
|
-
const abs =
|
|
5427
|
-
const rel =
|
|
6223
|
+
const abs = path20.join(dir, ent.name);
|
|
6224
|
+
const rel = path20.relative(root, abs).split(path20.sep).join("/");
|
|
5428
6225
|
if (!rel || ig.ignores(rel) || ent.isDirectory() && ig.ignores(`${rel}/`)) continue;
|
|
5429
6226
|
if (ent.isDirectory()) walk2(abs);
|
|
5430
6227
|
else if (ent.isFile()) candidates.push(rel);
|
|
@@ -5433,7 +6230,7 @@ function suggestPaths(root, missingRel, max = 3) {
|
|
|
5433
6230
|
walk2(root);
|
|
5434
6231
|
const rows = [];
|
|
5435
6232
|
for (const rel of candidates) {
|
|
5436
|
-
const base =
|
|
6233
|
+
const base = path20.basename(rel).toLowerCase();
|
|
5437
6234
|
let score = 0;
|
|
5438
6235
|
if (base === targetBase) score = 1e3;
|
|
5439
6236
|
else if (base.startsWith(targetBase) || targetBase.startsWith(base)) score = 500;
|
|
@@ -5511,12 +6308,12 @@ function validatePlan(input5) {
|
|
|
5511
6308
|
const cache = /* @__PURE__ */ new Map();
|
|
5512
6309
|
const readCached = (rel) => {
|
|
5513
6310
|
if (cache.has(rel)) return cache.get(rel);
|
|
5514
|
-
const resolved =
|
|
5515
|
-
const rc =
|
|
5516
|
-
if (rc.startsWith("..") ||
|
|
5517
|
-
if (!
|
|
6311
|
+
const resolved = path20.resolve(input5.root, rel);
|
|
6312
|
+
const rc = path20.relative(input5.root, resolved);
|
|
6313
|
+
if (rc.startsWith("..") || path20.isAbsolute(rc)) return void 0;
|
|
6314
|
+
if (!fs30.existsSync(resolved) || !fs30.statSync(resolved).isFile()) return void 0;
|
|
5518
6315
|
try {
|
|
5519
|
-
const buf =
|
|
6316
|
+
const buf = fs30.readFileSync(resolved);
|
|
5520
6317
|
const isBinary = buf.includes(0);
|
|
5521
6318
|
const text = isBinary ? "" : buf.toString("utf8");
|
|
5522
6319
|
const lines = isBinary ? 0 : text.split(/\r?\n/).length;
|
|
@@ -5530,10 +6327,10 @@ function validatePlan(input5) {
|
|
|
5530
6327
|
const pathClaims = mergePathClaims(input5.planText);
|
|
5531
6328
|
for (const c of pathClaims) {
|
|
5532
6329
|
if (c.isCreate) continue;
|
|
5533
|
-
const resolved =
|
|
5534
|
-
const rel =
|
|
5535
|
-
if (rel.startsWith("..") ||
|
|
5536
|
-
if (!
|
|
6330
|
+
const resolved = path20.resolve(input5.root, c.path);
|
|
6331
|
+
const rel = path20.relative(input5.root, resolved);
|
|
6332
|
+
if (rel.startsWith("..") || path20.isAbsolute(rel)) continue;
|
|
6333
|
+
if (!fs30.existsSync(resolved)) {
|
|
5537
6334
|
const suggestions = suggestPaths(input5.root, c.path);
|
|
5538
6335
|
issues.push({
|
|
5539
6336
|
severity: "warning",
|
|
@@ -5553,8 +6350,8 @@ function validatePlan(input5) {
|
|
|
5553
6350
|
if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;
|
|
5554
6351
|
const filePath = pathNear(rangeText, rm2.index);
|
|
5555
6352
|
if (!filePath) continue;
|
|
5556
|
-
const
|
|
5557
|
-
const lines =
|
|
6353
|
+
const cached2 = readCached(filePath);
|
|
6354
|
+
const lines = cached2?.isBinary ? void 0 : cached2?.lines;
|
|
5558
6355
|
if (lines !== void 0 && hi > lines) {
|
|
5559
6356
|
issues.push({
|
|
5560
6357
|
severity: "warning",
|
|
@@ -5571,8 +6368,8 @@ function validatePlan(input5) {
|
|
|
5571
6368
|
if (!Number.isFinite(n)) continue;
|
|
5572
6369
|
const filePath = pathNear(rangeText, rm2.index);
|
|
5573
6370
|
if (!filePath) continue;
|
|
5574
|
-
const
|
|
5575
|
-
const lines =
|
|
6371
|
+
const cached2 = readCached(filePath);
|
|
6372
|
+
const lines = cached2?.isBinary ? void 0 : cached2?.lines;
|
|
5576
6373
|
if (lines !== void 0 && n > lines) {
|
|
5577
6374
|
issues.push({
|
|
5578
6375
|
severity: "warning",
|
|
@@ -5588,8 +6385,8 @@ function validatePlan(input5) {
|
|
|
5588
6385
|
const fp = rm2[1].trim().replace(/^\.\//, "");
|
|
5589
6386
|
const n = parseInt(rm2[2], 10);
|
|
5590
6387
|
if (!Number.isFinite(n)) continue;
|
|
5591
|
-
const
|
|
5592
|
-
const lines =
|
|
6388
|
+
const cached2 = readCached(fp);
|
|
6389
|
+
const lines = cached2?.isBinary ? void 0 : cached2?.lines;
|
|
5593
6390
|
if (lines !== void 0 && n > lines) {
|
|
5594
6391
|
issues.push({
|
|
5595
6392
|
severity: "warning",
|
|
@@ -5607,8 +6404,8 @@ function validatePlan(input5) {
|
|
|
5607
6404
|
const pathNearCell = near.match(/`([^`\n/]+\/[^`\n]+?\.[a-z0-9]{1,6})`/i);
|
|
5608
6405
|
if (!pathNearCell) continue;
|
|
5609
6406
|
const fp = pathNearCell[1].replace(/^\.\//, "");
|
|
5610
|
-
const
|
|
5611
|
-
if (!
|
|
6407
|
+
const cached2 = readCached(fp);
|
|
6408
|
+
if (!cached2 || cached2.isBinary) continue;
|
|
5612
6409
|
const symRe = /`([A-Za-z_][A-Za-z0-9_]{2,50})`/g;
|
|
5613
6410
|
let sm;
|
|
5614
6411
|
while ((sm = symRe.exec(line2)) !== null) {
|
|
@@ -5618,8 +6415,8 @@ function validatePlan(input5) {
|
|
|
5618
6415
|
)) {
|
|
5619
6416
|
continue;
|
|
5620
6417
|
}
|
|
5621
|
-
if (!
|
|
5622
|
-
const suggestions = suggestSymbols(
|
|
6418
|
+
if (!cached2.text.includes(sym)) {
|
|
6419
|
+
const suggestions = suggestSymbols(cached2.text, sym);
|
|
5623
6420
|
issues.push({
|
|
5624
6421
|
severity: "warning",
|
|
5625
6422
|
kind: "symbol_not_found",
|
|
@@ -5700,9 +6497,9 @@ ${result.content}
|
|
|
5700
6497
|
}
|
|
5701
6498
|
continue;
|
|
5702
6499
|
}
|
|
5703
|
-
const resolved =
|
|
5704
|
-
const rc =
|
|
5705
|
-
if (rc.startsWith("..") ||
|
|
6500
|
+
const resolved = path21.resolve(root, cleaned);
|
|
6501
|
+
const rc = path21.relative(root, resolved);
|
|
6502
|
+
if (rc.startsWith("..") || path21.isAbsolute(rc)) {
|
|
5706
6503
|
bus.emit({
|
|
5707
6504
|
kind: "validation_issue",
|
|
5708
6505
|
runId,
|
|
@@ -5713,7 +6510,7 @@ ${result.content}
|
|
|
5713
6510
|
});
|
|
5714
6511
|
continue;
|
|
5715
6512
|
}
|
|
5716
|
-
if (!
|
|
6513
|
+
if (!fs31.existsSync(resolved)) {
|
|
5717
6514
|
bus.emit({
|
|
5718
6515
|
kind: "validation_issue",
|
|
5719
6516
|
runId,
|
|
@@ -5724,7 +6521,7 @@ ${result.content}
|
|
|
5724
6521
|
});
|
|
5725
6522
|
continue;
|
|
5726
6523
|
}
|
|
5727
|
-
const st =
|
|
6524
|
+
const st = fs31.statSync(resolved);
|
|
5728
6525
|
if (!st.isFile()) continue;
|
|
5729
6526
|
const n = Math.min(st.size, MAX_BYTES_PER_FULL_READ);
|
|
5730
6527
|
const cap = budget.canRead(n);
|
|
@@ -5738,7 +6535,7 @@ _(skipped: ${cap.reason})_
|
|
|
5738
6535
|
}
|
|
5739
6536
|
let buf;
|
|
5740
6537
|
try {
|
|
5741
|
-
buf =
|
|
6538
|
+
buf = fs31.readFileSync(resolved);
|
|
5742
6539
|
} catch {
|
|
5743
6540
|
continue;
|
|
5744
6541
|
}
|
|
@@ -5931,6 +6728,9 @@ async function runPlanner(input5) {
|
|
|
5931
6728
|
}
|
|
5932
6729
|
} : void 0
|
|
5933
6730
|
});
|
|
6731
|
+
if (input5.auth) {
|
|
6732
|
+
bus.emit({ kind: "auth_info", runId, ...describeAuth(input5.auth) });
|
|
6733
|
+
}
|
|
5934
6734
|
const merged = { ...input5, events: bus, runId };
|
|
5935
6735
|
const systemPrompt = input5.systemPrompt;
|
|
5936
6736
|
let scoutSummary;
|
|
@@ -6071,8 +6871,16 @@ async function runPlanner(input5) {
|
|
|
6071
6871
|
}
|
|
6072
6872
|
|
|
6073
6873
|
// src/planner/planner-limit-messages.ts
|
|
6074
|
-
function printPlannerApiCostNotice() {
|
|
6874
|
+
function printPlannerApiCostNotice(authMode = "api-key") {
|
|
6075
6875
|
blank();
|
|
6876
|
+
if (authMode === "subscription") {
|
|
6877
|
+
step("Usage");
|
|
6878
|
+
info(
|
|
6879
|
+
"`squad new-plan --api` runs on your Claude subscription \u2014 there is no per-token API bill. Agent SDK usage draws on the same usage limits as Claude and Claude Code, so a long planning run consumes part of your window. Hitting the limit means waiting for the reset, not a larger invoice."
|
|
6880
|
+
);
|
|
6881
|
+
blank();
|
|
6882
|
+
return;
|
|
6883
|
+
}
|
|
6076
6884
|
step("Billing");
|
|
6077
6885
|
info(
|
|
6078
6886
|
"`squad new-plan --api` sends your intake and repo context to the configured provider. Each model round is billed like any other API usage (input tokens, output tokens, and cache-related tokens when applicable). Choosing \u201CContinue\u201D after a limit runs more rounds and usually increases cost."
|
|
@@ -6095,12 +6903,12 @@ function tokenSummary(ctx) {
|
|
|
6095
6903
|
const snap = ctx.budgetSnapshot;
|
|
6096
6904
|
return `Tokens so far: ${snap.usage.inputTokens} in / ${snap.usage.outputTokens} out for this session.`;
|
|
6097
6905
|
}
|
|
6098
|
-
function kindDetail(ctx) {
|
|
6906
|
+
function kindDetail(ctx, authMode) {
|
|
6099
6907
|
switch (ctx.kind) {
|
|
6100
6908
|
case "max_output_tokens":
|
|
6101
6909
|
return [
|
|
6102
6910
|
`The model hit the per-request output cap (${ctx.maxOutputTokens} completion tokens). Long plans can stop mid-markdown even though the session is otherwise healthy.`,
|
|
6103
|
-
"You can continue: squad-kit will ask the model to append the rest of the plan in a follow-up request (extra tokens apply).",
|
|
6911
|
+
"You can continue: squad-kit will ask the model to append the rest of the plan in a follow-up request " + (authMode === "subscription" ? "(it draws further on your Claude usage limits)." : "(extra tokens apply)."),
|
|
6104
6912
|
tokenSummary(ctx)
|
|
6105
6913
|
];
|
|
6106
6914
|
case "max_iterations":
|
|
@@ -6123,9 +6931,9 @@ function kindDetail(ctx) {
|
|
|
6123
6931
|
];
|
|
6124
6932
|
}
|
|
6125
6933
|
}
|
|
6126
|
-
function printPlannerLimitExplanation(ctx) {
|
|
6934
|
+
function printPlannerLimitExplanation(ctx, authMode = "api-key") {
|
|
6127
6935
|
warning(kindTitle(ctx.kind));
|
|
6128
|
-
for (const line2 of kindDetail(ctx)) {
|
|
6936
|
+
for (const line2 of kindDetail(ctx, authMode)) {
|
|
6129
6937
|
info(line2);
|
|
6130
6938
|
}
|
|
6131
6939
|
kv("session so far", `${snapSummary(ctx)}`, 18);
|
|
@@ -6176,6 +6984,23 @@ function thinkingConfigFromProviderSpecific(ps) {
|
|
|
6176
6984
|
}
|
|
6177
6985
|
|
|
6178
6986
|
// src/planner/runtimes/agent-sdk-runtime.ts
|
|
6987
|
+
var ACCOUNT_INFO_TIMEOUT_MS = 2e3;
|
|
6988
|
+
var MAX_RATE_LIMIT_RETRY_SEC2 = 90;
|
|
6989
|
+
var enrichedAuthInfoRuns = /* @__PURE__ */ new WeakMap();
|
|
6990
|
+
function markAuthInfoEmitted(bus, runId) {
|
|
6991
|
+
let seen = enrichedAuthInfoRuns.get(bus);
|
|
6992
|
+
if (!seen) {
|
|
6993
|
+
seen = /* @__PURE__ */ new Set();
|
|
6994
|
+
enrichedAuthInfoRuns.set(bus, seen);
|
|
6995
|
+
}
|
|
6996
|
+
if (seen.has(runId)) return false;
|
|
6997
|
+
seen.add(runId);
|
|
6998
|
+
return true;
|
|
6999
|
+
}
|
|
7000
|
+
var API_KEY_SOURCES = ["user", "project", "org", "temporary", "oauth"];
|
|
7001
|
+
function asApiKeySource(v) {
|
|
7002
|
+
return API_KEY_SOURCES.find((s) => s === v);
|
|
7003
|
+
}
|
|
6179
7004
|
var BUILTIN_TOOL_DENY = [
|
|
6180
7005
|
"Read",
|
|
6181
7006
|
"Write",
|
|
@@ -6347,15 +7172,96 @@ function handleDecodedStreamEvent(decoded, input5) {
|
|
|
6347
7172
|
}
|
|
6348
7173
|
}
|
|
6349
7174
|
}
|
|
7175
|
+
async function readSdkAccountInfo(q) {
|
|
7176
|
+
const fn = q?.accountInfo;
|
|
7177
|
+
if (typeof fn !== "function") return void 0;
|
|
7178
|
+
let timer;
|
|
7179
|
+
try {
|
|
7180
|
+
const info2 = await Promise.race([
|
|
7181
|
+
fn.call(q),
|
|
7182
|
+
new Promise((resolve2) => {
|
|
7183
|
+
timer = setTimeout(() => resolve2(void 0), ACCOUNT_INFO_TIMEOUT_MS);
|
|
7184
|
+
})
|
|
7185
|
+
]);
|
|
7186
|
+
if (!info2 || typeof info2 !== "object") return void 0;
|
|
7187
|
+
const o = info2;
|
|
7188
|
+
const out = {};
|
|
7189
|
+
if (typeof o.email === "string") out.email = o.email;
|
|
7190
|
+
if (typeof o.organization === "string") out.organization = o.organization;
|
|
7191
|
+
if (typeof o.subscriptionType === "string") out.subscriptionType = o.subscriptionType;
|
|
7192
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
7193
|
+
} catch {
|
|
7194
|
+
return void 0;
|
|
7195
|
+
} finally {
|
|
7196
|
+
if (timer) clearTimeout(timer);
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
6350
7199
|
var AgentSdkRuntime = class {
|
|
6351
|
-
constructor(modelId,
|
|
7200
|
+
constructor(modelId, auth2) {
|
|
6352
7201
|
this.modelId = modelId;
|
|
6353
|
-
this.
|
|
7202
|
+
this.auth = auth2;
|
|
6354
7203
|
}
|
|
6355
7204
|
modelId;
|
|
6356
|
-
|
|
7205
|
+
auth;
|
|
6357
7206
|
kind = "agent-sdk";
|
|
6358
7207
|
providerName = "anthropic";
|
|
7208
|
+
/** Safe-to-log auth projection — never carries the key or the token. */
|
|
7209
|
+
authDescriptor() {
|
|
7210
|
+
return describeAuth(this.auth);
|
|
7211
|
+
}
|
|
7212
|
+
/**
|
|
7213
|
+
* Handle a `system` message. On `init` this emits the enriched `auth_info` (once per run, even
|
|
7214
|
+
* though scout and draft are separate instances); on `api_retry` it forwards a rate limit through
|
|
7215
|
+
* the existing 0.8.0 rate-limit channel so the console countdown and CLI spinner keep working.
|
|
7216
|
+
*/
|
|
7217
|
+
async handleSystemMessage(m, input5, ctx) {
|
|
7218
|
+
if (m.subtype === "init") {
|
|
7219
|
+
const apiKeySource = asApiKeySource(m.apiKeySource);
|
|
7220
|
+
if (!apiKeySource || !markAuthInfoEmitted(input5.bus, input5.runId)) return;
|
|
7221
|
+
const account = ctx.withAccountInfo && this.auth.mode === "subscription" ? await readSdkAccountInfo(ctx.query) : void 0;
|
|
7222
|
+
input5.bus.emit({
|
|
7223
|
+
kind: "auth_info",
|
|
7224
|
+
runId: input5.runId,
|
|
7225
|
+
...this.authDescriptor(),
|
|
7226
|
+
apiKeySource,
|
|
7227
|
+
...account ? { account } : {}
|
|
7228
|
+
});
|
|
7229
|
+
return;
|
|
7230
|
+
}
|
|
7231
|
+
if (m.subtype === "api_retry" && detectAuthShapedSdkError(m.error) === "rate_limit") {
|
|
7232
|
+
const delayMs = typeof m.retry_delay_ms === "number" ? m.retry_delay_ms : 0;
|
|
7233
|
+
const waitSec = Math.max(1, Math.ceil(delayMs / 1e3));
|
|
7234
|
+
input5.bus.emit({
|
|
7235
|
+
kind: "rate_limit",
|
|
7236
|
+
runId: input5.runId,
|
|
7237
|
+
turn: ctx.turn,
|
|
7238
|
+
retryAfterSec: waitSec,
|
|
7239
|
+
waitSec,
|
|
7240
|
+
capSec: MAX_RATE_LIMIT_RETRY_SEC2,
|
|
7241
|
+
phase: "retrying",
|
|
7242
|
+
provider: "anthropic"
|
|
7243
|
+
});
|
|
7244
|
+
}
|
|
7245
|
+
}
|
|
7246
|
+
/**
|
|
7247
|
+
* Turn an auth-shaped SDK failure into an actionable error. Rate limits also emit the terminal
|
|
7248
|
+
* `rate_limit` event so the UI closes its countdown before the throw.
|
|
7249
|
+
*/
|
|
7250
|
+
authError(signal, input5, turn) {
|
|
7251
|
+
if (signal === "rate_limit") {
|
|
7252
|
+
input5.bus.emit({
|
|
7253
|
+
kind: "rate_limit",
|
|
7254
|
+
runId: input5.runId,
|
|
7255
|
+
turn,
|
|
7256
|
+
retryAfterSec: void 0,
|
|
7257
|
+
waitSec: 0,
|
|
7258
|
+
capSec: MAX_RATE_LIMIT_RETRY_SEC2,
|
|
7259
|
+
phase: "aborted",
|
|
7260
|
+
provider: "anthropic"
|
|
7261
|
+
});
|
|
7262
|
+
}
|
|
7263
|
+
return new Error(authErrorMessage(signal, authErrorContextFrom(this.auth)));
|
|
7264
|
+
}
|
|
6359
7265
|
async runDraft(input5) {
|
|
6360
7266
|
const { query, createSdkMcpServer, tool: tool5 } = await import("@anthropic-ai/claude-agent-sdk");
|
|
6361
7267
|
const sdkTools = input5.tools.map(
|
|
@@ -6390,7 +7296,7 @@ var AgentSdkRuntime = class {
|
|
|
6390
7296
|
persistSession: false,
|
|
6391
7297
|
settingSources: [],
|
|
6392
7298
|
abortController: ac,
|
|
6393
|
-
env:
|
|
7299
|
+
env: buildSdkEnv(this.auth),
|
|
6394
7300
|
thinking,
|
|
6395
7301
|
effort
|
|
6396
7302
|
};
|
|
@@ -6400,6 +7306,7 @@ var AgentSdkRuntime = class {
|
|
|
6400
7306
|
let finishedNormally = false;
|
|
6401
7307
|
let incompleteKind;
|
|
6402
7308
|
let userCancelled = false;
|
|
7309
|
+
let authSignal;
|
|
6403
7310
|
const think = initialThinkingState();
|
|
6404
7311
|
try {
|
|
6405
7312
|
const q = query({
|
|
@@ -6421,6 +7328,7 @@ var AgentSdkRuntime = class {
|
|
|
6421
7328
|
break;
|
|
6422
7329
|
}
|
|
6423
7330
|
case "assistant": {
|
|
7331
|
+
authSignal = detectAuthShapedSdkError(m.error) ?? authSignal;
|
|
6424
7332
|
think.blockIndexCounter = 0;
|
|
6425
7333
|
think.inThinkingBlock = false;
|
|
6426
7334
|
think.thinkingBlockIndex = -1;
|
|
@@ -6447,10 +7355,17 @@ var AgentSdkRuntime = class {
|
|
|
6447
7355
|
finishedNormally = false;
|
|
6448
7356
|
} else if (typeof sub === "string" && sub.startsWith("error")) {
|
|
6449
7357
|
finishedNormally = false;
|
|
7358
|
+
const signal = authSignal ?? detectAuthShapedSdkError(m.errors);
|
|
7359
|
+
if (signal) throw this.authError(signal, input5, assistantTurns);
|
|
6450
7360
|
}
|
|
6451
7361
|
break;
|
|
6452
7362
|
}
|
|
6453
7363
|
case "system":
|
|
7364
|
+
await this.handleSystemMessage(m, input5, {
|
|
7365
|
+
query: q,
|
|
7366
|
+
withAccountInfo: true,
|
|
7367
|
+
turn: assistantTurns
|
|
7368
|
+
});
|
|
6454
7369
|
break;
|
|
6455
7370
|
default:
|
|
6456
7371
|
break;
|
|
@@ -6509,12 +7424,13 @@ var AgentSdkRuntime = class {
|
|
|
6509
7424
|
includePartialMessages: true,
|
|
6510
7425
|
persistSession: false,
|
|
6511
7426
|
settingSources: [],
|
|
6512
|
-
env:
|
|
7427
|
+
env: buildSdkEnv(this.auth),
|
|
6513
7428
|
thinking,
|
|
6514
7429
|
effort
|
|
6515
7430
|
};
|
|
6516
7431
|
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
6517
7432
|
let assistantTurns = 0;
|
|
7433
|
+
let authSignal;
|
|
6518
7434
|
const think = initialThinkingState();
|
|
6519
7435
|
const ac = new AbortController();
|
|
6520
7436
|
const onAbort = () => ac.abort();
|
|
@@ -6539,6 +7455,7 @@ var AgentSdkRuntime = class {
|
|
|
6539
7455
|
});
|
|
6540
7456
|
break;
|
|
6541
7457
|
case "assistant":
|
|
7458
|
+
authSignal = detectAuthShapedSdkError(m.error) ?? authSignal;
|
|
6542
7459
|
think.blockIndexCounter = 0;
|
|
6543
7460
|
think.inThinkingBlock = false;
|
|
6544
7461
|
think.thinkingBlockIndex = -1;
|
|
@@ -6549,8 +7466,17 @@ var AgentSdkRuntime = class {
|
|
|
6549
7466
|
usage = usageFromAgentSdkResult(
|
|
6550
7467
|
m.usage
|
|
6551
7468
|
);
|
|
7469
|
+
if (typeof m.subtype === "string" && m.subtype.startsWith("error")) {
|
|
7470
|
+
const signal = authSignal ?? detectAuthShapedSdkError(m.errors);
|
|
7471
|
+
if (signal) throw this.authError(signal, input5, assistantTurns);
|
|
7472
|
+
}
|
|
6552
7473
|
break;
|
|
6553
7474
|
case "system":
|
|
7475
|
+
await this.handleSystemMessage(m, input5, {
|
|
7476
|
+
query: q,
|
|
7477
|
+
withAccountInfo: false,
|
|
7478
|
+
turn: assistantTurns
|
|
7479
|
+
});
|
|
6554
7480
|
break;
|
|
6555
7481
|
default:
|
|
6556
7482
|
break;
|
|
@@ -6572,14 +7498,40 @@ var AgentSdkRuntime = class {
|
|
|
6572
7498
|
}
|
|
6573
7499
|
};
|
|
6574
7500
|
|
|
7501
|
+
// src/planner/runtimes/auth-runtime-mismatch.ts
|
|
7502
|
+
var SUBSCRIPTION_NEEDS_AGENT_SDK_LEAD = "Subscription auth needs the Claude Agent SDK runtime.";
|
|
7503
|
+
var SUBSCRIPTION_VERCEL_RUNTIME_HINT = "Remove `planner.runtime.anthropic: vercel` from .squad/config.yaml (the default is agent-sdk), or set `planner.auth.anthropic: api-key` and provide an ANTHROPIC_API_KEY.";
|
|
7504
|
+
function subscriptionProviderMismatchHint(provider) {
|
|
7505
|
+
return `The planner provider is \`${provider}\`, which is API-key only. Switch to Anthropic with \`squad config set planner\`, or set \`planner.auth.anthropic: api-key\` and provide an ANTHROPIC_API_KEY.`;
|
|
7506
|
+
}
|
|
7507
|
+
function plannerAuthRuntimeMismatchMessage(provider) {
|
|
7508
|
+
return `${SUBSCRIPTION_NEEDS_AGENT_SDK_LEAD} ${provider === "anthropic" ? SUBSCRIPTION_VERCEL_RUNTIME_HINT : subscriptionProviderMismatchHint(provider)}`;
|
|
7509
|
+
}
|
|
7510
|
+
|
|
6575
7511
|
// src/planner/runtimes/index.ts
|
|
7512
|
+
var PlannerAuthRuntimeMismatchError = class extends Error {
|
|
7513
|
+
constructor(provider, runtimeChoice) {
|
|
7514
|
+
super(plannerAuthRuntimeMismatchMessage(provider));
|
|
7515
|
+
this.provider = provider;
|
|
7516
|
+
this.runtimeChoice = runtimeChoice;
|
|
7517
|
+
this.name = "PlannerAuthRuntimeMismatchError";
|
|
7518
|
+
}
|
|
7519
|
+
provider;
|
|
7520
|
+
runtimeChoice;
|
|
7521
|
+
};
|
|
6576
7522
|
function resolveRuntime(input5) {
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
7523
|
+
const choice = input5.provider === "anthropic" ? input5.anthropicRuntime ?? "agent-sdk" : "vercel";
|
|
7524
|
+
const auth2 = input5.auth;
|
|
7525
|
+
if (auth2.mode === "subscription") {
|
|
7526
|
+
if (input5.provider !== "anthropic" || choice !== "agent-sdk") {
|
|
7527
|
+
throw new PlannerAuthRuntimeMismatchError(input5.provider, choice);
|
|
7528
|
+
}
|
|
7529
|
+
return new AgentSdkRuntime(input5.modelId, auth2);
|
|
7530
|
+
}
|
|
7531
|
+
if (input5.provider === "anthropic" && choice === "agent-sdk") {
|
|
7532
|
+
return new AgentSdkRuntime(input5.modelId, auth2);
|
|
6581
7533
|
}
|
|
6582
|
-
return new VercelRuntime(input5.provider, input5.modelId,
|
|
7534
|
+
return new VercelRuntime(input5.provider, input5.modelId, auth2.key);
|
|
6583
7535
|
}
|
|
6584
7536
|
|
|
6585
7537
|
// src/planner/budget.ts
|
|
@@ -6651,8 +7603,8 @@ var Budget = class {
|
|
|
6651
7603
|
};
|
|
6652
7604
|
|
|
6653
7605
|
// src/planner/writer.ts
|
|
6654
|
-
import
|
|
6655
|
-
import
|
|
7606
|
+
import fs32 from "fs";
|
|
7607
|
+
import path22 from "path";
|
|
6656
7608
|
function planFileSlugParts(config2, story) {
|
|
6657
7609
|
const baseSlug = slugify(story.id);
|
|
6658
7610
|
const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
|
|
@@ -6661,8 +7613,8 @@ function planFileSlugParts(config2, story) {
|
|
|
6661
7613
|
}
|
|
6662
7614
|
function writePlanFile(input5) {
|
|
6663
7615
|
const { paths, config: config2, story } = input5;
|
|
6664
|
-
const featurePlanDir =
|
|
6665
|
-
|
|
7616
|
+
const featurePlanDir = path22.join(paths.plansDir, story.feature);
|
|
7617
|
+
fs32.mkdirSync(featurePlanDir, { recursive: true });
|
|
6666
7618
|
if (input5.partial) {
|
|
6667
7619
|
return writePartialPlanFile(input5, featurePlanDir, paths, config2, story);
|
|
6668
7620
|
}
|
|
@@ -6677,14 +7629,14 @@ function writePlanFile(input5) {
|
|
|
6677
7629
|
} else {
|
|
6678
7630
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
6679
7631
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
6680
|
-
targetPath =
|
|
7632
|
+
targetPath = path22.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
|
|
6681
7633
|
}
|
|
6682
7634
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
6683
7635
|
|
|
6684
7636
|
${input5.planBodyMarkdown.trimStart()}
|
|
6685
7637
|
`;
|
|
6686
|
-
|
|
6687
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
7638
|
+
fs32.writeFileSync(targetPath, body, "utf8");
|
|
7639
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path22.basename(targetPath), story, false);
|
|
6688
7640
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
6689
7641
|
}
|
|
6690
7642
|
function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
@@ -6699,42 +7651,42 @@ function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
|
6699
7651
|
} else {
|
|
6700
7652
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
6701
7653
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
6702
|
-
targetPath =
|
|
7654
|
+
targetPath = path22.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
|
|
6703
7655
|
}
|
|
6704
7656
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
6705
7657
|
|
|
6706
7658
|
${input5.planBodyMarkdown.trimStart()}
|
|
6707
7659
|
`;
|
|
6708
|
-
|
|
6709
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
7660
|
+
fs32.writeFileSync(targetPath, body, "utf8");
|
|
7661
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path22.basename(targetPath), story, true);
|
|
6710
7662
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
6711
7663
|
}
|
|
6712
7664
|
function findExistingPlan(featureDir, storyId) {
|
|
6713
|
-
if (!
|
|
7665
|
+
if (!fs32.existsSync(featureDir)) return void 0;
|
|
6714
7666
|
const pattern = /^(\d{2,})-story-.+\.md$/;
|
|
6715
|
-
for (const entry of
|
|
7667
|
+
for (const entry of fs32.readdirSync(featureDir)) {
|
|
6716
7668
|
if (entry.endsWith(".partial.md")) continue;
|
|
6717
7669
|
const m = entry.match(pattern);
|
|
6718
7670
|
if (!m) continue;
|
|
6719
|
-
if (entry.includes(storyId)) return { absPath:
|
|
7671
|
+
if (entry.includes(storyId)) return { absPath: path22.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
6720
7672
|
}
|
|
6721
7673
|
return void 0;
|
|
6722
7674
|
}
|
|
6723
7675
|
function findExistingPartialPlan(featureDir, storyId) {
|
|
6724
|
-
if (!
|
|
7676
|
+
if (!fs32.existsSync(featureDir)) return void 0;
|
|
6725
7677
|
const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
|
|
6726
|
-
for (const entry of
|
|
7678
|
+
for (const entry of fs32.readdirSync(featureDir)) {
|
|
6727
7679
|
const m = entry.match(pattern);
|
|
6728
7680
|
if (!m) continue;
|
|
6729
|
-
if (entry.includes(storyId)) return { absPath:
|
|
7681
|
+
if (entry.includes(storyId)) return { absPath: path22.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
6730
7682
|
}
|
|
6731
7683
|
return void 0;
|
|
6732
7684
|
}
|
|
6733
7685
|
function upsertOverviewRow(featureDir, feature, nn, filename, story, isPartial) {
|
|
6734
|
-
const overviewPath =
|
|
7686
|
+
const overviewPath = path22.join(featureDir, "00-overview.md");
|
|
6735
7687
|
const titleCell = isPartial ? `${story.titleHint ?? story.id} (partial)` : story.titleHint ?? story.id;
|
|
6736
7688
|
const row = `| ${formatSequence(nn)} | \`${filename}\` | ${titleCell} | ${story.id} | \u2014 |`;
|
|
6737
|
-
if (!
|
|
7689
|
+
if (!fs32.existsSync(overviewPath)) {
|
|
6738
7690
|
const content = `# ${feature} \u2014 plan overview
|
|
6739
7691
|
|
|
6740
7692
|
Entry point for the **${feature}** feature. Stories execute in order by their \`NN\` prefix.
|
|
@@ -6745,10 +7697,10 @@ Entry point for the **${feature}** feature. Stories execute in order by their \`
|
|
|
6745
7697
|
|----|------|-------|------------|------------|
|
|
6746
7698
|
${row}
|
|
6747
7699
|
`;
|
|
6748
|
-
|
|
7700
|
+
fs32.writeFileSync(overviewPath, content, "utf8");
|
|
6749
7701
|
return;
|
|
6750
7702
|
}
|
|
6751
|
-
const current =
|
|
7703
|
+
const current = fs32.readFileSync(overviewPath, "utf8");
|
|
6752
7704
|
const lines = current.split("\n");
|
|
6753
7705
|
const existingIdx = lines.findIndex((l) => l.includes(`\`${filename}\``) || l.includes(`| ${formatSequence(nn)} |`));
|
|
6754
7706
|
if (existingIdx >= 0) {
|
|
@@ -6757,7 +7709,7 @@ ${row}
|
|
|
6757
7709
|
const tableEnd = findTableEnd(lines);
|
|
6758
7710
|
lines.splice(tableEnd, 0, row);
|
|
6759
7711
|
}
|
|
6760
|
-
|
|
7712
|
+
fs32.writeFileSync(overviewPath, lines.join("\n"), "utf8");
|
|
6761
7713
|
}
|
|
6762
7714
|
function findTableEnd(lines) {
|
|
6763
7715
|
let inTable = false;
|
|
@@ -6810,16 +7762,16 @@ function buildCopyPlanPromptMarkdown(config2, intakeContent) {
|
|
|
6810
7762
|
}
|
|
6811
7763
|
|
|
6812
7764
|
// src/core/last-run.ts
|
|
6813
|
-
import
|
|
6814
|
-
import
|
|
7765
|
+
import fs33 from "fs/promises";
|
|
7766
|
+
import path23 from "path";
|
|
6815
7767
|
async function writeLastRun(paths, rec) {
|
|
6816
|
-
const file =
|
|
6817
|
-
await
|
|
7768
|
+
const file = path23.join(paths.squadDir, ".last-run.json");
|
|
7769
|
+
await fs33.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
6818
7770
|
}
|
|
6819
7771
|
async function readLastRun(paths) {
|
|
6820
|
-
const file =
|
|
7772
|
+
const file = path23.join(paths.squadDir, ".last-run.json");
|
|
6821
7773
|
try {
|
|
6822
|
-
const raw = await
|
|
7774
|
+
const raw = await fs33.readFile(file, "utf8");
|
|
6823
7775
|
const parsed = JSON.parse(raw);
|
|
6824
7776
|
if (parsed.version !== 1) return void 0;
|
|
6825
7777
|
return parsed;
|
|
@@ -6852,8 +7804,8 @@ function formatPlannerCacheLine(opts) {
|
|
|
6852
7804
|
}
|
|
6853
7805
|
|
|
6854
7806
|
// src/core/plans-index.ts
|
|
6855
|
-
import
|
|
6856
|
-
import
|
|
7807
|
+
import fs34 from "fs";
|
|
7808
|
+
import path24 from "path";
|
|
6857
7809
|
var MAX_ENTRIES = 30;
|
|
6858
7810
|
function firstMarkdownH1(src) {
|
|
6859
7811
|
const lines = src.split(/\r?\n/);
|
|
@@ -6868,12 +7820,12 @@ function collectPlanMarkdownFiles(plansDir) {
|
|
|
6868
7820
|
const walk2 = (dir) => {
|
|
6869
7821
|
let ents;
|
|
6870
7822
|
try {
|
|
6871
|
-
ents =
|
|
7823
|
+
ents = fs34.readdirSync(dir, { withFileTypes: true });
|
|
6872
7824
|
} catch {
|
|
6873
7825
|
return;
|
|
6874
7826
|
}
|
|
6875
7827
|
for (const ent of ents) {
|
|
6876
|
-
const abs =
|
|
7828
|
+
const abs = path24.join(dir, ent.name);
|
|
6877
7829
|
if (ent.isDirectory()) {
|
|
6878
7830
|
walk2(abs);
|
|
6879
7831
|
} else if (ent.isFile() && ent.name.endsWith(".md")) {
|
|
@@ -6882,7 +7834,7 @@ function collectPlanMarkdownFiles(plansDir) {
|
|
|
6882
7834
|
}
|
|
6883
7835
|
}
|
|
6884
7836
|
};
|
|
6885
|
-
if (
|
|
7837
|
+
if (fs34.existsSync(plansDir)) walk2(plansDir);
|
|
6886
7838
|
out.sort((a, b) => a.localeCompare(b));
|
|
6887
7839
|
return out.slice(0, MAX_ENTRIES);
|
|
6888
7840
|
}
|
|
@@ -6891,13 +7843,13 @@ function buildPlansIndex(paths) {
|
|
|
6891
7843
|
if (files.length === 0) return "(no prior plans yet)";
|
|
6892
7844
|
const bullets = [];
|
|
6893
7845
|
for (const abs of files) {
|
|
6894
|
-
const rel =
|
|
7846
|
+
const rel = path24.relative(paths.root, abs).split(path24.sep).join("/");
|
|
6895
7847
|
let title = "";
|
|
6896
7848
|
try {
|
|
6897
|
-
const raw =
|
|
6898
|
-
title = firstMarkdownH1(raw) ??
|
|
7849
|
+
const raw = fs34.readFileSync(abs, "utf8");
|
|
7850
|
+
title = firstMarkdownH1(raw) ?? path24.basename(abs);
|
|
6899
7851
|
} catch {
|
|
6900
|
-
title =
|
|
7852
|
+
title = path24.basename(abs);
|
|
6901
7853
|
}
|
|
6902
7854
|
bullets.push(`- ${rel} \u2014 "${title}"`);
|
|
6903
7855
|
}
|
|
@@ -7157,9 +8109,10 @@ function decideMode(opts, config2) {
|
|
|
7157
8109
|
}
|
|
7158
8110
|
if (opts.api) return "api";
|
|
7159
8111
|
if (opts.copy) return "copy";
|
|
7160
|
-
const
|
|
7161
|
-
|
|
7162
|
-
|
|
8112
|
+
const planner = config2.planner;
|
|
8113
|
+
if (!planner?.enabled || !planner.provider) return "copy";
|
|
8114
|
+
if (planner.provider === "anthropic" && planner.auth?.anthropic === "subscription") return "api";
|
|
8115
|
+
return readProviderKey(planner.provider) ? "api" : "copy";
|
|
7163
8116
|
}
|
|
7164
8117
|
async function runNewPlan(intakePath, opts) {
|
|
7165
8118
|
const root = requireSquadRoot();
|
|
@@ -7170,7 +8123,7 @@ async function runNewPlan(intakePath, opts) {
|
|
|
7170
8123
|
const story = intakePath ? resolveFromPath(intakePath, stories, root) : await pickStory(stories, { all: !!opts.all, interactive, feature: opts.feature });
|
|
7171
8124
|
if (!story) return;
|
|
7172
8125
|
if (story.planFile) {
|
|
7173
|
-
const proceed = await
|
|
8126
|
+
const proceed = await confirmOverwrite2(story, interactive, !!opts.yes);
|
|
7174
8127
|
if (!proceed) return;
|
|
7175
8128
|
}
|
|
7176
8129
|
const mode = decideMode(opts, config2);
|
|
@@ -7181,8 +8134,8 @@ async function runNewPlan(intakePath, opts) {
|
|
|
7181
8134
|
}
|
|
7182
8135
|
}
|
|
7183
8136
|
function resolveFromPath(intakePath, stories, root) {
|
|
7184
|
-
const resolved =
|
|
7185
|
-
if (!
|
|
8137
|
+
const resolved = path25.resolve(intakePath.trim());
|
|
8138
|
+
if (!fs35.existsSync(resolved)) {
|
|
7186
8139
|
throw new Error(
|
|
7187
8140
|
`Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
|
|
7188
8141
|
);
|
|
@@ -7194,7 +8147,7 @@ function resolveFromPath(intakePath, stories, root) {
|
|
|
7194
8147
|
);
|
|
7195
8148
|
}
|
|
7196
8149
|
function relativeStoriesDir(root) {
|
|
7197
|
-
return
|
|
8150
|
+
return path25.relative(root, path25.join(root, ".squad", "stories"));
|
|
7198
8151
|
}
|
|
7199
8152
|
async function pickStory(stories, opts) {
|
|
7200
8153
|
const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
|
|
@@ -7225,7 +8178,7 @@ async function pickStory(stories, opts) {
|
|
|
7225
8178
|
value: s.intakePath
|
|
7226
8179
|
}))
|
|
7227
8180
|
});
|
|
7228
|
-
return candidates.find((s) =>
|
|
8181
|
+
return candidates.find((s) => path25.resolve(s.intakePath) === path25.resolve(pick));
|
|
7229
8182
|
}
|
|
7230
8183
|
function formatLabel(s) {
|
|
7231
8184
|
const head = `${s.feature} / ${s.id}`;
|
|
@@ -7233,7 +8186,7 @@ function formatLabel(s) {
|
|
|
7233
8186
|
const plannedTag = s.planFile ? ` ${theme_exports.warn("(planned)")}` : "";
|
|
7234
8187
|
return head + tail + plannedTag;
|
|
7235
8188
|
}
|
|
7236
|
-
async function
|
|
8189
|
+
async function confirmOverwrite2(story, interactive, yes) {
|
|
7237
8190
|
warning(`A plan already exists for this intake:`);
|
|
7238
8191
|
kv("plan", `.squad/plans/${story.feature}/${story.planFile}`);
|
|
7239
8192
|
info("Regenerating will replace it when your planner writes the new version.");
|
|
@@ -7243,7 +8196,7 @@ async function confirmOverwrite(story, interactive, yes) {
|
|
|
7243
8196
|
`Plan already exists (.squad/plans/${story.feature}/${story.planFile}). Run \`squad new-plan\` with \`--yes\` to overwrite non-interactively, or in a TTY to confirm in the prompt.`
|
|
7244
8197
|
);
|
|
7245
8198
|
}
|
|
7246
|
-
const go = await
|
|
8199
|
+
const go = await confirm7({ message: "Proceed and regenerate?", default: false });
|
|
7247
8200
|
return go;
|
|
7248
8201
|
}
|
|
7249
8202
|
function printPlannerRunIncompleteSummary(result, relPlanFile) {
|
|
@@ -7258,6 +8211,10 @@ function printPlannerRunIncompleteSummary(result, relPlanFile) {
|
|
|
7258
8211
|
info("The file uses the `.partial.md` suffix and YAML `squad-kit-plan-status: partial`.");
|
|
7259
8212
|
info("Raise limits in `.squad/config.yaml` (`planner.budget`, `planner.maxOutputTokens`) or re-run when ready.");
|
|
7260
8213
|
}
|
|
8214
|
+
function formatAuthRow(auth2) {
|
|
8215
|
+
if (auth2.mode === "subscription") return "subscription (Claude login)";
|
|
8216
|
+
return `api-key (${describeAuth(auth2).credentialHint})`;
|
|
8217
|
+
}
|
|
7261
8218
|
async function emitViaApi(story, paths, config2, opts) {
|
|
7262
8219
|
const planner = config2.planner;
|
|
7263
8220
|
if (!planner?.enabled) {
|
|
@@ -7265,18 +8222,13 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7265
8222
|
"Direct planner API is not configured. Run `squad init --force` to enable it, or `squad new-plan --copy` for the manual copy-paste flow."
|
|
7266
8223
|
);
|
|
7267
8224
|
}
|
|
7268
|
-
const
|
|
7269
|
-
if (!apiKey) {
|
|
7270
|
-
throw new Error(
|
|
7271
|
-
`Missing ${providerEnvVar(planner.provider)}. Run \`squad config set planner\` to save a key to secrets, or export the env var, or run \`squad new-plan --copy\` without the API.`
|
|
7272
|
-
);
|
|
7273
|
-
}
|
|
8225
|
+
const auth2 = resolvePlannerAuthForCwd(planner.provider, planner);
|
|
7274
8226
|
const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
|
|
7275
8227
|
const anthropicRuntimeChoice = opts.anthropicRuntime ?? planner.runtime?.anthropic ?? "agent-sdk";
|
|
7276
8228
|
const draftRuntime = resolveRuntime({
|
|
7277
8229
|
provider: planner.provider,
|
|
7278
8230
|
modelId,
|
|
7279
|
-
|
|
8231
|
+
auth: auth2,
|
|
7280
8232
|
anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
|
|
7281
8233
|
});
|
|
7282
8234
|
const thinkingCli = opts.noThinking ? "off" : void 0;
|
|
@@ -7295,10 +8247,11 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7295
8247
|
step(`planning ${story.feature} / ${story.id}`);
|
|
7296
8248
|
kv("provider", planner.provider);
|
|
7297
8249
|
kv("model", modelId);
|
|
8250
|
+
kv("auth", formatAuthRow(auth2));
|
|
7298
8251
|
blank();
|
|
7299
8252
|
const interactive = !opts.yes && Boolean(process.stdin.isTTY);
|
|
7300
8253
|
if (interactive) {
|
|
7301
|
-
printPlannerApiCostNotice();
|
|
8254
|
+
printPlannerApiCostNotice(auth2.mode);
|
|
7302
8255
|
}
|
|
7303
8256
|
const mapSpinner = spinner("building repo map\u2026");
|
|
7304
8257
|
const repoMap = buildRepoMap(paths.root, { format: "tree" });
|
|
@@ -7330,7 +8283,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7330
8283
|
scoutRuntime = resolveRuntime({
|
|
7331
8284
|
provider: planner.provider,
|
|
7332
8285
|
modelId: scoutModelId,
|
|
7333
|
-
|
|
8286
|
+
auth: auth2,
|
|
7334
8287
|
anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
|
|
7335
8288
|
});
|
|
7336
8289
|
scoutSystemPrompt = composeScoutSystemPrompt({
|
|
@@ -7369,9 +8322,9 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7369
8322
|
dispatchNewPlanApiPlannerEvent(apiUiCtx, e);
|
|
7370
8323
|
});
|
|
7371
8324
|
const decideOnLimit = interactive ? async (ctx) => {
|
|
7372
|
-
printPlannerLimitExplanation(ctx);
|
|
8325
|
+
printPlannerLimitExplanation(ctx, auth2.mode);
|
|
7373
8326
|
const ans = await select6({
|
|
7374
|
-
message: "Continue this planning session? (Continuing sends more API requests and is billed.)",
|
|
8327
|
+
message: auth2.mode === "subscription" ? "Continue this planning session? (Continuing runs more model rounds against your Claude usage limits.)" : "Continue this planning session? (Continuing sends more API requests and is billed.)",
|
|
7375
8328
|
choices: [
|
|
7376
8329
|
{ name: "Continue \u2014 extend limits for this run", value: "continue" },
|
|
7377
8330
|
{ name: "Stop \u2014 save partial plan and exit", value: "cancel" }
|
|
@@ -7387,6 +8340,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7387
8340
|
runtime: draftRuntime,
|
|
7388
8341
|
provider: planner.provider,
|
|
7389
8342
|
modelId,
|
|
8343
|
+
auth: auth2,
|
|
7390
8344
|
anthropicProviderSpecific,
|
|
7391
8345
|
systemPrompt,
|
|
7392
8346
|
userPrompt,
|
|
@@ -7452,7 +8406,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7452
8406
|
metadataHeader: header,
|
|
7453
8407
|
partial: !success2
|
|
7454
8408
|
});
|
|
7455
|
-
const relPlan =
|
|
8409
|
+
const relPlan = path25.relative(paths.root, planFile);
|
|
7456
8410
|
try {
|
|
7457
8411
|
await writeLastRun(paths, {
|
|
7458
8412
|
stats: result.stats,
|
|
@@ -7487,6 +8441,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7487
8441
|
durationMs: result.validation?.durationMs
|
|
7488
8442
|
},
|
|
7489
8443
|
plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
|
|
8444
|
+
authMode: describeAuth(auth2).mode,
|
|
7490
8445
|
providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
|
|
7491
8446
|
});
|
|
7492
8447
|
} catch {
|
|
@@ -7588,14 +8543,14 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7588
8543
|
info("Note: file-read budget was reached; the model finalised without further reads.");
|
|
7589
8544
|
}
|
|
7590
8545
|
blank();
|
|
7591
|
-
info(`next \u2192 open a new agent chat and attach only ${
|
|
8546
|
+
info(`next \u2192 open a new agent chat and attach only ${path25.basename(planFile)}`);
|
|
7592
8547
|
}
|
|
7593
8548
|
async function emitCopyPrompt(story, paths, config2, clipboard) {
|
|
7594
8549
|
const intakeContent = readFile(story.intakePath);
|
|
7595
8550
|
const composed = buildCopyPlanPromptMarkdown(config2, intakeContent);
|
|
7596
|
-
const promptFile =
|
|
7597
|
-
|
|
7598
|
-
const relPromptFile =
|
|
8551
|
+
const promptFile = path25.join(paths.squadDir, ".last-copy-prompt.md");
|
|
8552
|
+
fs35.writeFileSync(promptFile, composed, "utf8");
|
|
8553
|
+
const relPromptFile = path25.relative(paths.root, promptFile);
|
|
7599
8554
|
const bytes = Buffer.byteLength(composed, "utf8");
|
|
7600
8555
|
const estTokens = Math.round(bytes / 4);
|
|
7601
8556
|
const clipResult = clipboard ? await copyToClipboard(composed) : { ok: false, reason: "clipboard disabled (--no-clipboard)" };
|
|
@@ -7653,7 +8608,7 @@ function printModelBanner(config2) {
|
|
|
7653
8608
|
}
|
|
7654
8609
|
|
|
7655
8610
|
// src/commands/status.ts
|
|
7656
|
-
import
|
|
8611
|
+
import fs36 from "fs";
|
|
7657
8612
|
var kw = 28;
|
|
7658
8613
|
function probeTrackerCredential(config2, overlay) {
|
|
7659
8614
|
const { client, error } = clientFor(config2, overlay);
|
|
@@ -7679,7 +8634,7 @@ async function runStatus() {
|
|
|
7679
8634
|
const scan = scanPlans(paths.plansDir);
|
|
7680
8635
|
const storyCount = listStories(paths).length;
|
|
7681
8636
|
const planCount = scan.usedNumbers.length;
|
|
7682
|
-
const secrets =
|
|
8637
|
+
const secrets = fs36.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
7683
8638
|
const overlay = overlayTrackerEnv(secrets);
|
|
7684
8639
|
banner();
|
|
7685
8640
|
step("squad-kit status");
|
|
@@ -7695,12 +8650,7 @@ async function runStatus() {
|
|
|
7695
8650
|
const model = modelFor(config2.planner.provider, "plan", config2.planner.modelOverride);
|
|
7696
8651
|
const overrideNote = config2.planner.modelOverride?.[config2.planner.provider] ? " (override)" : "";
|
|
7697
8652
|
kv("planner", `${config2.planner.provider}/${model}${overrideNote}`, kw);
|
|
7698
|
-
|
|
7699
|
-
kv(
|
|
7700
|
-
"planner key",
|
|
7701
|
-
cred ? `set via ${cred.detail}` : `missing \u2014 set ${providerEnvVar(config2.planner.provider)} or run \`squad init\``,
|
|
7702
|
-
kw
|
|
7703
|
-
);
|
|
8653
|
+
kv("planner auth", buildPlannerAuthLine(config2.planner), kw);
|
|
7704
8654
|
} else {
|
|
7705
8655
|
kv("planner", "disabled (copy-paste flow)", kw);
|
|
7706
8656
|
}
|
|
@@ -7711,6 +8661,15 @@ async function runStatus() {
|
|
|
7711
8661
|
warning(`duplicate NN numbers: ${scan.duplicates.map(formatSequence).join(", ")}`);
|
|
7712
8662
|
}
|
|
7713
8663
|
}
|
|
8664
|
+
function buildPlannerAuthLine(planner) {
|
|
8665
|
+
try {
|
|
8666
|
+
const described = describeAuth(resolvePlannerAuthForCwd(planner.provider, planner));
|
|
8667
|
+
return `${described.mode} \xB7 ${described.credentialHint} (${authReasonText(described.reason, planner.provider)})`;
|
|
8668
|
+
} catch (err2) {
|
|
8669
|
+
if (!(err2 instanceof PlannerAuthUnavailableError)) throw err2;
|
|
8670
|
+
return planner.provider === "anthropic" ? "missing \u2014 run `squad auth login` for your Claude subscription, or `squad config set planner` for an API key" : `missing \u2014 set ${providerEnvVar(planner.provider)} or run \`squad config set planner\``;
|
|
8671
|
+
}
|
|
8672
|
+
}
|
|
7714
8673
|
function buildTrackerLine(config2, overlay) {
|
|
7715
8674
|
if (config2.tracker.type === "none") return "none";
|
|
7716
8675
|
let trackerLine = config2.tracker.type;
|
|
@@ -7733,13 +8692,13 @@ function buildTrackerLine(config2, overlay) {
|
|
|
7733
8692
|
}
|
|
7734
8693
|
|
|
7735
8694
|
// src/commands/list.ts
|
|
7736
|
-
import
|
|
8695
|
+
import path26 from "path";
|
|
7737
8696
|
async function runList(opts) {
|
|
7738
8697
|
const root = requireSquadRoot();
|
|
7739
8698
|
const paths = buildPaths(root);
|
|
7740
8699
|
const records = listStories(paths, { feature: opts.feature });
|
|
7741
8700
|
const rows = records.map((r) => {
|
|
7742
|
-
const planAbs = r.planFile ?
|
|
8701
|
+
const planAbs = r.planFile ? path26.join(paths.plansDir, r.feature, r.planFile) : void 0;
|
|
7743
8702
|
const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
|
|
7744
8703
|
return {
|
|
7745
8704
|
feature: r.feature,
|
|
@@ -7778,8 +8737,8 @@ function stripAnsi(s) {
|
|
|
7778
8737
|
}
|
|
7779
8738
|
|
|
7780
8739
|
// src/commands/tracker-link.ts
|
|
7781
|
-
import
|
|
7782
|
-
import
|
|
8740
|
+
import fs37 from "fs";
|
|
8741
|
+
import path27 from "path";
|
|
7783
8742
|
import { input as input4, select as select7 } from "@inquirer/prompts";
|
|
7784
8743
|
async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
7785
8744
|
const root = requireSquadRoot();
|
|
@@ -7826,17 +8785,17 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
7826
8785
|
`Invalid tracker id "${resolvedTrackerId}" for tracker type "${config2.tracker.type}". Run \`squad config show\` to review the expected format, or \`squad config set tracker\` if the type is wrong.`
|
|
7827
8786
|
);
|
|
7828
8787
|
}
|
|
7829
|
-
const resolved =
|
|
7830
|
-
const intakeFile = resolved.endsWith("intake.md") ? resolved :
|
|
7831
|
-
if (!
|
|
8788
|
+
const resolved = path27.resolve(resolvedStoryPath);
|
|
8789
|
+
const intakeFile = resolved.endsWith("intake.md") ? resolved : path27.join(resolved, "intake.md");
|
|
8790
|
+
if (!fs37.existsSync(intakeFile)) {
|
|
7832
8791
|
throw new Error(
|
|
7833
8792
|
`intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
|
|
7834
8793
|
);
|
|
7835
8794
|
}
|
|
7836
|
-
const content =
|
|
8795
|
+
const content = fs37.readFileSync(intakeFile, "utf8");
|
|
7837
8796
|
const updated = upsertTrackerId(content, resolvedTrackerId);
|
|
7838
|
-
|
|
7839
|
-
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${
|
|
8797
|
+
fs37.writeFileSync(intakeFile, updated, "utf8");
|
|
8798
|
+
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path27.relative(root, intakeFile)}`);
|
|
7840
8799
|
blank();
|
|
7841
8800
|
step("Next:");
|
|
7842
8801
|
info("1) Review the intake \u2014 the Source block at the top now carries the tracker id.");
|
|
@@ -7854,8 +8813,8 @@ function upsertTrackerId(content, id) {
|
|
|
7854
8813
|
}
|
|
7855
8814
|
|
|
7856
8815
|
// src/commands/doctor-engine.ts
|
|
7857
|
-
import
|
|
7858
|
-
import
|
|
8816
|
+
import fs38 from "fs";
|
|
8817
|
+
import path28 from "path";
|
|
7859
8818
|
async function gatherContext(paths) {
|
|
7860
8819
|
const ctx = { hasLegacyPromptsDir: false };
|
|
7861
8820
|
try {
|
|
@@ -7864,29 +8823,30 @@ async function gatherContext(paths) {
|
|
|
7864
8823
|
ctx.configError = err2;
|
|
7865
8824
|
}
|
|
7866
8825
|
try {
|
|
7867
|
-
ctx.secrets =
|
|
8826
|
+
ctx.secrets = fs38.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
7868
8827
|
} catch (err2) {
|
|
7869
8828
|
ctx.secretsError = err2;
|
|
7870
8829
|
}
|
|
7871
|
-
ctx.hasLegacyPromptsDir =
|
|
8830
|
+
ctx.hasLegacyPromptsDir = fs38.existsSync(paths.promptsDir);
|
|
7872
8831
|
return ctx;
|
|
7873
8832
|
}
|
|
7874
8833
|
function gitignoreHasManagedBlock(repoRoot) {
|
|
7875
|
-
const gitignore =
|
|
7876
|
-
return
|
|
8834
|
+
const gitignore = path28.join(repoRoot, ".gitignore");
|
|
8835
|
+
return fs38.existsSync(gitignore) && fs38.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
|
|
7877
8836
|
}
|
|
7878
8837
|
function gitignoreHasTrashPattern(repoRoot) {
|
|
7879
|
-
const gitignore =
|
|
7880
|
-
return
|
|
8838
|
+
const gitignore = path28.join(repoRoot, ".gitignore");
|
|
8839
|
+
return fs38.existsSync(gitignore) && fs38.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
|
|
7881
8840
|
}
|
|
8841
|
+
var DOCTOR_AUTH_PROBE_TIMEOUT_MS = 25e3;
|
|
7882
8842
|
async function checkDirStructure(paths, _ctx, fix) {
|
|
7883
|
-
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !
|
|
8843
|
+
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs38.existsSync(p));
|
|
7884
8844
|
if (need.length === 0) {
|
|
7885
8845
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
|
|
7886
8846
|
}
|
|
7887
8847
|
if (fix) {
|
|
7888
8848
|
for (const p of need) {
|
|
7889
|
-
|
|
8849
|
+
fs38.mkdirSync(p, { recursive: true });
|
|
7890
8850
|
}
|
|
7891
8851
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
|
|
7892
8852
|
}
|
|
@@ -7894,7 +8854,7 @@ async function checkDirStructure(paths, _ctx, fix) {
|
|
|
7894
8854
|
id: "dirs",
|
|
7895
8855
|
name: ".squad/ directory structure",
|
|
7896
8856
|
status: "warn",
|
|
7897
|
-
detail: `missing: ${need.map((p) =>
|
|
8857
|
+
detail: `missing: ${need.map((p) => path28.relative(paths.root, p)).join(", ")}`,
|
|
7898
8858
|
fixable: true,
|
|
7899
8859
|
fixHint: "squad doctor --fix"
|
|
7900
8860
|
};
|
|
@@ -7949,15 +8909,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
|
|
|
7949
8909
|
if (process.platform === "win32") {
|
|
7950
8910
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
|
|
7951
8911
|
}
|
|
7952
|
-
if (!
|
|
8912
|
+
if (!fs38.existsSync(paths.secretsFile)) {
|
|
7953
8913
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
|
|
7954
8914
|
}
|
|
7955
|
-
const mode =
|
|
8915
|
+
const mode = fs38.statSync(paths.secretsFile).mode & 511;
|
|
7956
8916
|
if (mode === 384) {
|
|
7957
8917
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
|
|
7958
8918
|
}
|
|
7959
8919
|
if (fix) {
|
|
7960
|
-
|
|
8920
|
+
fs38.chmodSync(paths.secretsFile, 384);
|
|
7961
8921
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
|
|
7962
8922
|
}
|
|
7963
8923
|
return {
|
|
@@ -7978,7 +8938,7 @@ async function checkSecretsParseable(_paths, ctx) {
|
|
|
7978
8938
|
detail: ctx.secretsError.message
|
|
7979
8939
|
};
|
|
7980
8940
|
}
|
|
7981
|
-
if (!
|
|
8941
|
+
if (!fs38.existsSync(_paths.secretsFile)) {
|
|
7982
8942
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
|
|
7983
8943
|
}
|
|
7984
8944
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
|
|
@@ -8040,36 +9000,136 @@ async function checkPlannerConfig(_paths, ctx) {
|
|
|
8040
9000
|
}
|
|
8041
9001
|
return { id: "planner-config", name: "planner configuration", status: "ok" };
|
|
8042
9002
|
}
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
9003
|
+
var LOGIN_FIX_HINT = "Run `squad auth login` to sign in with your Claude account.";
|
|
9004
|
+
var ORG_NOT_ALLOWED_FIX_HINT = "That Claude account is signed in, but its organization policy blocks this kind of access. Ask an administrator to allow Claude Agent SDK usage, or set `planner.auth.anthropic: api-key` in .squad/config.yaml and provide an ANTHROPIC_API_KEY.";
|
|
9005
|
+
function plannerEnabled(ctx) {
|
|
9006
|
+
return ctx.config?.planner?.enabled === true;
|
|
9007
|
+
}
|
|
9008
|
+
function authFor(paths, ctx, provider) {
|
|
9009
|
+
const memo = ctx.authMemo ??= {};
|
|
9010
|
+
if (!memo.resolution) {
|
|
9011
|
+
try {
|
|
9012
|
+
memo.resolution = { ok: true, auth: resolvePlannerAuthForPaths(paths, provider, ctx.config?.planner) };
|
|
9013
|
+
} catch (err2) {
|
|
9014
|
+
memo.resolution = { ok: false, error: err2 };
|
|
9015
|
+
}
|
|
9016
|
+
}
|
|
9017
|
+
return memo.resolution;
|
|
9018
|
+
}
|
|
9019
|
+
async function probeAuthOnce(ctx, auth2) {
|
|
9020
|
+
const memo = ctx.authMemo ??= {};
|
|
9021
|
+
if (!memo.probe) {
|
|
9022
|
+
memo.probe = skipExternalProbesInAutomation() ? { skipped: true } : await probeClaudeAuth(auth2, { timeoutMs: DOCTOR_AUTH_PROBE_TIMEOUT_MS });
|
|
9023
|
+
}
|
|
9024
|
+
return memo.probe;
|
|
9025
|
+
}
|
|
9026
|
+
function probeWasSkipped(outcome) {
|
|
9027
|
+
return "skipped" in outcome;
|
|
9028
|
+
}
|
|
9029
|
+
async function checkPlannerAuthMode(paths, ctx, _fix) {
|
|
9030
|
+
const id = "planner-auth-mode";
|
|
9031
|
+
const name = "planner auth mode";
|
|
9032
|
+
if (!plannerEnabled(ctx) || !ctx.config?.planner) {
|
|
9033
|
+
return { id, name, status: "skip", detail: "planner disabled" };
|
|
9034
|
+
}
|
|
9035
|
+
const planner = ctx.config.planner;
|
|
9036
|
+
const provider = planner.provider;
|
|
9037
|
+
if (provider !== "anthropic") {
|
|
9038
|
+
return { id, name, status: "skip", detail: `api key only for ${provider}` };
|
|
9039
|
+
}
|
|
9040
|
+
const resolution = authFor(paths, ctx, provider);
|
|
9041
|
+
if (!resolution.ok) {
|
|
9042
|
+
return { id, name, status: "fail", detail: resolution.error.message, fixHint: LOGIN_FIX_HINT };
|
|
9043
|
+
}
|
|
9044
|
+
const auth2 = resolution.auth;
|
|
9045
|
+
const described = describeAuth(auth2);
|
|
9046
|
+
let detail = `${described.mode} (${authReasonText(described.reason, provider)}) \u2192 ${described.credentialHint}`;
|
|
9047
|
+
if (auth2.mode === "subscription") {
|
|
9048
|
+
const ignoredKey = resolveProviderKeyForPaths(paths, provider);
|
|
9049
|
+
if (ignoredKey) detail += ` \xB7 ignoring the API key from ${ignoredKey.detail}`;
|
|
9050
|
+
}
|
|
9051
|
+
const implicitApiKey = (planner.auth?.anthropic ?? "auto") === "auto" && auth2.reason === "auto-fallback-api-key";
|
|
9052
|
+
if (implicitApiKey) {
|
|
9053
|
+
return {
|
|
9054
|
+
id,
|
|
9055
|
+
name,
|
|
9056
|
+
status: "ok",
|
|
9057
|
+
detail,
|
|
9058
|
+
fixHint: "Resolving by fallback. Set `planner.auth.anthropic` explicitly (`squad config set planner`) if you want to pin this, or run `squad auth login` to plan on your Claude subscription instead."
|
|
9059
|
+
};
|
|
9060
|
+
}
|
|
9061
|
+
return { id, name, status: "ok", detail };
|
|
9062
|
+
}
|
|
9063
|
+
function describeProbeAccount(probe) {
|
|
9064
|
+
const bits = [];
|
|
9065
|
+
const email = probe.account?.email;
|
|
9066
|
+
const plan = probe.account?.subscriptionType;
|
|
9067
|
+
bits.push(email ? `signed in as ${email}${plan ? ` (${plan})` : ""}` : "Claude login verified");
|
|
9068
|
+
if (probe.account?.organization) bits.push(probe.account.organization);
|
|
9069
|
+
if (probe.apiKeySource) bits.push(`apiKeySource=${probe.apiKeySource}`);
|
|
9070
|
+
return bits.join(" \xB7 ");
|
|
9071
|
+
}
|
|
9072
|
+
async function checkPlannerCredential(paths, ctx) {
|
|
9073
|
+
const id = "planner-cred";
|
|
9074
|
+
const name = "planner credential resolves";
|
|
9075
|
+
if (!plannerEnabled(ctx) || !ctx.config?.planner) {
|
|
9076
|
+
return { id, name, status: "skip", detail: "planner disabled" };
|
|
8046
9077
|
}
|
|
8047
9078
|
const provider = ctx.config.planner.provider;
|
|
8048
|
-
const
|
|
8049
|
-
if (!
|
|
9079
|
+
const resolution = authFor(paths, ctx, provider);
|
|
9080
|
+
if (!resolution.ok) {
|
|
8050
9081
|
const envVar = providerEnvVar(provider);
|
|
8051
9082
|
return {
|
|
8052
|
-
id
|
|
8053
|
-
name
|
|
9083
|
+
id,
|
|
9084
|
+
name,
|
|
8054
9085
|
status: "fail",
|
|
8055
9086
|
detail: `no API key found (${envVar} or .squad/secrets.yaml)`,
|
|
8056
|
-
fixHint:
|
|
9087
|
+
fixHint: resolution.error.message
|
|
8057
9088
|
};
|
|
8058
9089
|
}
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
name: "
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
9090
|
+
const auth2 = resolution.auth;
|
|
9091
|
+
if (auth2.mode === "api-key") {
|
|
9092
|
+
return { id, name, status: "ok", detail: `source=${auth2.source} (${auth2.detail})` };
|
|
9093
|
+
}
|
|
9094
|
+
const credentialHint = describeAuth(auth2).credentialHint;
|
|
9095
|
+
const probe = await probeAuthOnce(ctx, auth2);
|
|
9096
|
+
if (probeWasSkipped(probe)) {
|
|
9097
|
+
return credentialPresent(auth2) ? { id, name, status: "ok", detail: `${credentialHint} \xB7 offline check only` } : {
|
|
9098
|
+
id,
|
|
9099
|
+
name,
|
|
9100
|
+
status: "warn",
|
|
9101
|
+
detail: "no Claude login detected \xB7 offline check only",
|
|
9102
|
+
fixHint: LOGIN_FIX_HINT
|
|
9103
|
+
};
|
|
9104
|
+
}
|
|
9105
|
+
if (probe.ok) {
|
|
9106
|
+
return { id, name, status: "ok", detail: describeProbeAccount(probe) };
|
|
9107
|
+
}
|
|
9108
|
+
switch (probe.kind) {
|
|
9109
|
+
case "not-logged-in":
|
|
9110
|
+
case "expired":
|
|
9111
|
+
return { id, name, status: "fail", detail: probe.detail, fixHint: LOGIN_FIX_HINT };
|
|
9112
|
+
case "org-not-allowed":
|
|
9113
|
+
return { id, name, status: "fail", detail: probe.detail, fixHint: ORG_NOT_ALLOWED_FIX_HINT };
|
|
9114
|
+
case "no-binary":
|
|
9115
|
+
return { id, name, status: "warn", detail: probe.detail, fixHint: MISSING_CLAUDE_BINARY_MESSAGE };
|
|
9116
|
+
default:
|
|
9117
|
+
return credentialPresent(auth2) ? { id, name, status: "warn", detail: `${probe.kind}: ${probe.detail}` } : {
|
|
9118
|
+
id,
|
|
9119
|
+
name,
|
|
9120
|
+
status: "fail",
|
|
9121
|
+
detail: `no Claude login detected (live check: ${probe.kind})`,
|
|
9122
|
+
fixHint: LOGIN_FIX_HINT
|
|
9123
|
+
};
|
|
9124
|
+
}
|
|
8065
9125
|
}
|
|
8066
|
-
async function checkPlannerModel(
|
|
8067
|
-
if (!ctx
|
|
9126
|
+
async function checkPlannerModel(paths, ctx) {
|
|
9127
|
+
if (!plannerEnabled(ctx) || !ctx.config?.planner) {
|
|
8068
9128
|
return { id: "planner-model", name: "planner model resolves at provider", status: "skip", detail: "planner disabled" };
|
|
8069
9129
|
}
|
|
8070
9130
|
const provider = ctx.config.planner.provider;
|
|
8071
|
-
const
|
|
8072
|
-
if (!
|
|
9131
|
+
const resolution = authFor(paths, ctx, provider);
|
|
9132
|
+
if (!resolution.ok) {
|
|
8073
9133
|
return {
|
|
8074
9134
|
id: "planner-model",
|
|
8075
9135
|
name: "planner model resolves at provider",
|
|
@@ -8077,9 +9137,18 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
8077
9137
|
detail: "no credential"
|
|
8078
9138
|
};
|
|
8079
9139
|
}
|
|
9140
|
+
const auth2 = resolution.auth;
|
|
9141
|
+
if (auth2.mode === "subscription") {
|
|
9142
|
+
return {
|
|
9143
|
+
id: "planner-model",
|
|
9144
|
+
name: "planner model resolves at provider",
|
|
9145
|
+
status: "skip",
|
|
9146
|
+
detail: "model list needs an API key; skipped in subscription mode"
|
|
9147
|
+
};
|
|
9148
|
+
}
|
|
8080
9149
|
const model = modelFor(provider, "plan", ctx.config.planner.modelOverride);
|
|
8081
9150
|
try {
|
|
8082
|
-
const listed = await fetchProviderModelIds(provider,
|
|
9151
|
+
const listed = await fetchProviderModelIds(provider, auth2.key);
|
|
8083
9152
|
if (!listed.ok) {
|
|
8084
9153
|
const st = listed.status;
|
|
8085
9154
|
if (st === 401 || st === 403) {
|
|
@@ -8121,7 +9190,7 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
8121
9190
|
};
|
|
8122
9191
|
}
|
|
8123
9192
|
}
|
|
8124
|
-
async function checkPlannerTierAwareness(
|
|
9193
|
+
async function checkPlannerTierAwareness(paths, ctx) {
|
|
8125
9194
|
const id = "planner-tier";
|
|
8126
9195
|
const name = "planner tier vs. model";
|
|
8127
9196
|
if (!ctx.config || ctx.config.planner?.enabled !== true) {
|
|
@@ -8131,6 +9200,10 @@ async function checkPlannerTierAwareness(_paths, ctx) {
|
|
|
8131
9200
|
if (provider !== "anthropic") {
|
|
8132
9201
|
return { id, name, status: "skip", detail: `not applicable for ${provider}` };
|
|
8133
9202
|
}
|
|
9203
|
+
const tierAuth = authFor(paths, ctx, provider);
|
|
9204
|
+
if (tierAuth.ok && tierAuth.auth.mode === "subscription") {
|
|
9205
|
+
return { id, name, status: "skip", detail: "not applicable on subscription auth (no API rate tier)" };
|
|
9206
|
+
}
|
|
8134
9207
|
const planModel = modelFor(provider, "plan", ctx.config.planner.modelOverride);
|
|
8135
9208
|
if (!/opus/i.test(planModel)) {
|
|
8136
9209
|
return { id, name, status: "ok", detail: `${planModel} is comfortably under Tier 1` };
|
|
@@ -8259,18 +9332,57 @@ async function checkPlannerAnthropicRuntimeModelFit(_paths, ctx) {
|
|
|
8259
9332
|
fixHint: "Remove planner.runtime.anthropic: vercel (default is agent-sdk), or set planner.modelOverride.anthropic to a pre-4.7 id (e.g. claude-sonnet-4-5-20250929)."
|
|
8260
9333
|
};
|
|
8261
9334
|
}
|
|
8262
|
-
async function
|
|
9335
|
+
async function checkPlannerAuthRuntimeFit(paths, ctx) {
|
|
9336
|
+
const id = "planner-auth-runtime-fit";
|
|
9337
|
+
const name = "planner auth vs. runtime";
|
|
9338
|
+
if (!plannerEnabled(ctx) || !ctx.config?.planner) {
|
|
9339
|
+
return { id, name, status: "skip", detail: "planner disabled" };
|
|
9340
|
+
}
|
|
9341
|
+
const planner = ctx.config.planner;
|
|
9342
|
+
const provider = planner.provider;
|
|
9343
|
+
const configuredMode = planner.auth?.anthropic ?? "auto";
|
|
9344
|
+
if (provider !== "anthropic") {
|
|
9345
|
+
if (configuredMode === "subscription") {
|
|
9346
|
+
return {
|
|
9347
|
+
id,
|
|
9348
|
+
name,
|
|
9349
|
+
status: "fail",
|
|
9350
|
+
detail: `planner.auth.anthropic: subscription with planner.provider: ${provider}`,
|
|
9351
|
+
fixHint: plannerAuthRuntimeMismatchMessage(provider)
|
|
9352
|
+
};
|
|
9353
|
+
}
|
|
9354
|
+
return { id, name, status: "skip", detail: `api key only for ${provider}` };
|
|
9355
|
+
}
|
|
9356
|
+
const runtime = runtimeNameFor(provider, planner);
|
|
9357
|
+
const resolution = authFor(paths, ctx, provider);
|
|
9358
|
+
if (!resolution.ok) {
|
|
9359
|
+
return { id, name, status: "skip", detail: "auth unresolved" };
|
|
9360
|
+
}
|
|
9361
|
+
const mode = resolution.auth.mode;
|
|
9362
|
+
if (mode === "subscription" && runtime !== "agent-sdk") {
|
|
9363
|
+
return {
|
|
9364
|
+
id,
|
|
9365
|
+
name,
|
|
9366
|
+
status: "fail",
|
|
9367
|
+
detail: `subscription auth with planner.runtime.anthropic: ${runtime}`,
|
|
9368
|
+
fixHint: plannerAuthRuntimeMismatchMessage(provider)
|
|
9369
|
+
};
|
|
9370
|
+
}
|
|
9371
|
+
return { id, name, status: "ok", detail: `${mode} + ${runtime} runtime` };
|
|
9372
|
+
}
|
|
9373
|
+
async function checkAgentSdkBinaryPresent(paths, ctx) {
|
|
8263
9374
|
const id = "agent-sdk-binary-present";
|
|
8264
9375
|
const name = "Anthropic Agent SDK install";
|
|
8265
9376
|
if (!ctx.config?.planner?.enabled || ctx.config.planner.provider !== "anthropic") {
|
|
8266
9377
|
return { id, name, status: "skip", detail: "not applicable" };
|
|
8267
9378
|
}
|
|
8268
|
-
|
|
9379
|
+
const resolution = authFor(paths, ctx, "anthropic");
|
|
9380
|
+
const subscription = resolution.ok && resolution.auth.mode === "subscription";
|
|
9381
|
+
if ((ctx.config.planner.runtime?.anthropic ?? "agent-sdk") === "vercel" && !subscription) {
|
|
8269
9382
|
return { id, name, status: "skip", detail: "vercel runtime selected" };
|
|
8270
9383
|
}
|
|
8271
9384
|
try {
|
|
8272
9385
|
await import("@anthropic-ai/claude-agent-sdk");
|
|
8273
|
-
return { id, name, status: "ok", detail: "package resolves" };
|
|
8274
9386
|
} catch (e) {
|
|
8275
9387
|
return {
|
|
8276
9388
|
id,
|
|
@@ -8280,6 +9392,22 @@ async function checkAgentSdkBinaryPresent(_paths, ctx) {
|
|
|
8280
9392
|
fixHint: "Run pnpm install / npm install in the squad-kit package so the Agent SDK (and its platform binary) is present."
|
|
8281
9393
|
};
|
|
8282
9394
|
}
|
|
9395
|
+
const binary = resolveClaudeBinary();
|
|
9396
|
+
if (!binary) {
|
|
9397
|
+
return {
|
|
9398
|
+
id,
|
|
9399
|
+
name,
|
|
9400
|
+
status: subscription ? "fail" : "warn",
|
|
9401
|
+
detail: "package resolves; no `claude` executable found",
|
|
9402
|
+
fixHint: MISSING_CLAUDE_BINARY_MESSAGE
|
|
9403
|
+
};
|
|
9404
|
+
}
|
|
9405
|
+
return {
|
|
9406
|
+
id,
|
|
9407
|
+
name,
|
|
9408
|
+
status: "ok",
|
|
9409
|
+
detail: `package + ${binary.source === "bundled" ? "bundled" : "PATH"} binary`
|
|
9410
|
+
};
|
|
8283
9411
|
}
|
|
8284
9412
|
async function checkTrackerConfig(_paths, ctx) {
|
|
8285
9413
|
if (!ctx.config) {
|
|
@@ -8418,11 +9546,13 @@ async function runAllChecks(paths, ctx, fix) {
|
|
|
8418
9546
|
await add(".squad/secrets.yaml parseable", () => checkSecretsParseable(paths, ctx));
|
|
8419
9547
|
await add("legacy .squad/prompts/ directory", () => checkLegacyPrompts(paths, ctx));
|
|
8420
9548
|
await add("planner configuration", () => checkPlannerConfig(paths, ctx));
|
|
9549
|
+
await add("planner auth mode", () => checkPlannerAuthMode(paths, ctx, fix));
|
|
8421
9550
|
await add("planner credential resolves", () => checkPlannerCredential(paths, ctx));
|
|
8422
9551
|
await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
|
|
8423
9552
|
await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
|
|
8424
9553
|
await add("planner cache effectiveness", () => checkPlannerCache(paths, ctx));
|
|
8425
9554
|
await add("planner runtime (resolved)", () => checkPlannerRuntimeInfo(paths, ctx));
|
|
9555
|
+
await add("planner auth vs. runtime", () => checkPlannerAuthRuntimeFit(paths, ctx));
|
|
8426
9556
|
await add("Anthropic Opus 4.7+ vs Vercel runtime", () => checkPlannerAnthropicRuntimeModelFit(paths, ctx));
|
|
8427
9557
|
await add("Anthropic Agent SDK install", () => checkAgentSdkBinaryPresent(paths, ctx));
|
|
8428
9558
|
await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
|
|
@@ -8532,23 +9662,23 @@ async function runDoctor(opts) {
|
|
|
8532
9662
|
}
|
|
8533
9663
|
|
|
8534
9664
|
// src/commands/migrate.ts
|
|
8535
|
-
import
|
|
8536
|
-
import
|
|
8537
|
-
import { confirm as
|
|
9665
|
+
import fs39 from "fs";
|
|
9666
|
+
import path29 from "path";
|
|
9667
|
+
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
8538
9668
|
var LEGACY_PROMPTS_MIGRATION = {
|
|
8539
9669
|
id: "legacy-prompts",
|
|
8540
9670
|
name: "Remove legacy .squad/prompts/",
|
|
8541
|
-
isApplied: (paths) => !
|
|
8542
|
-
describe: (paths) => `delete ${
|
|
9671
|
+
isApplied: (paths) => !fs39.existsSync(paths.promptsDir),
|
|
9672
|
+
describe: (paths) => `delete ${path29.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
|
|
8543
9673
|
apply: (paths) => {
|
|
8544
9674
|
const target = paths.promptsDir;
|
|
8545
|
-
if (!target.includes(
|
|
9675
|
+
if (!target.includes(path29.sep + ".squad" + path29.sep + "prompts")) {
|
|
8546
9676
|
throw new Error(
|
|
8547
9677
|
`Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
|
|
8548
9678
|
);
|
|
8549
9679
|
}
|
|
8550
|
-
if (
|
|
8551
|
-
|
|
9680
|
+
if (fs39.existsSync(target)) {
|
|
9681
|
+
fs39.rmSync(target, { recursive: true, force: true });
|
|
8552
9682
|
}
|
|
8553
9683
|
}
|
|
8554
9684
|
};
|
|
@@ -8556,8 +9686,8 @@ var GITIGNORE_MIGRATION = {
|
|
|
8556
9686
|
id: "gitignore",
|
|
8557
9687
|
name: "Ensure .gitignore managed block",
|
|
8558
9688
|
isApplied: (paths) => {
|
|
8559
|
-
const f =
|
|
8560
|
-
return
|
|
9689
|
+
const f = path29.join(paths.root, ".gitignore");
|
|
9690
|
+
return fs39.existsSync(f) && fs39.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
|
|
8561
9691
|
},
|
|
8562
9692
|
describe: () => "add squad-kit managed block to .gitignore",
|
|
8563
9693
|
apply: (paths) => {
|
|
@@ -8569,23 +9699,23 @@ var SECRETS_PERMS_MIGRATION = {
|
|
|
8569
9699
|
name: "Tighten .squad/secrets.yaml permissions",
|
|
8570
9700
|
isApplied: (paths) => {
|
|
8571
9701
|
if (process.platform === "win32") return true;
|
|
8572
|
-
if (!
|
|
8573
|
-
const st =
|
|
9702
|
+
if (!fs39.existsSync(paths.secretsFile)) return true;
|
|
9703
|
+
const st = fs39.statSync(paths.secretsFile);
|
|
8574
9704
|
return (st.mode & 511) === 384;
|
|
8575
9705
|
},
|
|
8576
|
-
describe: (paths) => `chmod 0600 ${
|
|
9706
|
+
describe: (paths) => `chmod 0600 ${path29.relative(paths.root, paths.secretsFile)}`,
|
|
8577
9707
|
apply: (paths) => {
|
|
8578
9708
|
if (process.platform === "win32") return;
|
|
8579
|
-
if (
|
|
9709
|
+
if (fs39.existsSync(paths.secretsFile)) fs39.chmodSync(paths.secretsFile, 384);
|
|
8580
9710
|
}
|
|
8581
9711
|
};
|
|
8582
9712
|
var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
|
|
8583
9713
|
id: "planner-budget-defaults",
|
|
8584
9714
|
name: "Backfill planner.budget defaults in config.yaml",
|
|
8585
9715
|
isApplied: (paths) => {
|
|
8586
|
-
if (!
|
|
9716
|
+
if (!fs39.existsSync(paths.configFile)) return true;
|
|
8587
9717
|
try {
|
|
8588
|
-
const raw =
|
|
9718
|
+
const raw = fs39.readFileSync(paths.configFile, "utf8");
|
|
8589
9719
|
if (!/^\s*planner:/m.test(raw)) return true;
|
|
8590
9720
|
if (/^\s*budget:/m.test(raw)) return true;
|
|
8591
9721
|
const cfg = loadConfig(paths.configFile);
|
|
@@ -8604,9 +9734,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
8604
9734
|
id: "config-normalize",
|
|
8605
9735
|
name: "Normalise .squad/config.yaml formatting",
|
|
8606
9736
|
isApplied: (paths) => {
|
|
8607
|
-
if (!
|
|
9737
|
+
if (!fs39.existsSync(paths.configFile)) return true;
|
|
8608
9738
|
try {
|
|
8609
|
-
const raw =
|
|
9739
|
+
const raw = fs39.readFileSync(paths.configFile, "utf8");
|
|
8610
9740
|
const cfg = loadConfig(paths.configFile);
|
|
8611
9741
|
return raw.trim() === serializeConfig(cfg).trim();
|
|
8612
9742
|
} catch {
|
|
@@ -8615,7 +9745,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
8615
9745
|
},
|
|
8616
9746
|
describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
|
|
8617
9747
|
apply: (paths) => {
|
|
8618
|
-
if (!
|
|
9748
|
+
if (!fs39.existsSync(paths.configFile)) return;
|
|
8619
9749
|
const cfg = loadConfig(paths.configFile);
|
|
8620
9750
|
saveConfig(paths.configFile, cfg);
|
|
8621
9751
|
}
|
|
@@ -8663,7 +9793,7 @@ async function runMigrate(opts) {
|
|
|
8663
9793
|
);
|
|
8664
9794
|
process.exit(1);
|
|
8665
9795
|
}
|
|
8666
|
-
const go = await
|
|
9796
|
+
const go = await confirm8({ message: `Apply ${pending.length} migration(s)?`, default: true });
|
|
8667
9797
|
if (!go) {
|
|
8668
9798
|
info("Cancelled. No changes applied.");
|
|
8669
9799
|
return;
|
|
@@ -8691,13 +9821,13 @@ async function runMigrate(opts) {
|
|
|
8691
9821
|
}
|
|
8692
9822
|
|
|
8693
9823
|
// src/commands/upgrade.ts
|
|
8694
|
-
import { spawn as
|
|
9824
|
+
import { spawn as spawn3 } from "child_process";
|
|
8695
9825
|
import process3 from "process";
|
|
8696
|
-
import { confirm as
|
|
9826
|
+
import { confirm as confirm9 } from "@inquirer/prompts";
|
|
8697
9827
|
|
|
8698
9828
|
// src/core/package-info.ts
|
|
8699
|
-
import
|
|
8700
|
-
import
|
|
9829
|
+
import fs40 from "fs";
|
|
9830
|
+
import path30 from "path";
|
|
8701
9831
|
function changelogBlobUrl(repositoryUrl, version) {
|
|
8702
9832
|
let base = repositoryUrl;
|
|
8703
9833
|
if (!base) base = "https://github.com/AzmSquad/squad-kit";
|
|
@@ -8708,9 +9838,9 @@ function changelogBlobUrl(repositoryUrl, version) {
|
|
|
8708
9838
|
}
|
|
8709
9839
|
function readInstalledPackage() {
|
|
8710
9840
|
const root = packageRoot();
|
|
8711
|
-
const pkgFile =
|
|
8712
|
-
const pkg = JSON.parse(
|
|
8713
|
-
const isDevInstall =
|
|
9841
|
+
const pkgFile = path30.join(root, "package.json");
|
|
9842
|
+
const pkg = JSON.parse(fs40.readFileSync(pkgFile, "utf8"));
|
|
9843
|
+
const isDevInstall = fs40.existsSync(path30.join(root, "src"));
|
|
8714
9844
|
return {
|
|
8715
9845
|
name: pkg.name,
|
|
8716
9846
|
version: pkg.version,
|
|
@@ -8750,7 +9880,7 @@ function isNewer(a, b) {
|
|
|
8750
9880
|
}
|
|
8751
9881
|
|
|
8752
9882
|
// src/core/package-manager.ts
|
|
8753
|
-
import
|
|
9883
|
+
import path31 from "path";
|
|
8754
9884
|
import process2 from "process";
|
|
8755
9885
|
function detectPackageManager(installRoot) {
|
|
8756
9886
|
const ua = process2.env.npm_config_user_agent;
|
|
@@ -8761,11 +9891,11 @@ function detectPackageManager(installRoot) {
|
|
|
8761
9891
|
if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
|
|
8762
9892
|
}
|
|
8763
9893
|
const root = installRoot.toLowerCase();
|
|
8764
|
-
if (root.includes(`${
|
|
9894
|
+
if (root.includes(`${path31.sep}pnpm${path31.sep}`) || root.includes(`${path31.sep}.pnpm${path31.sep}`))
|
|
8765
9895
|
return { pm: "pnpm", reason: `install path contains pnpm` };
|
|
8766
|
-
if (root.includes(`${
|
|
9896
|
+
if (root.includes(`${path31.sep}.yarn${path31.sep}`) || root.includes(`${path31.sep}yarn${path31.sep}`))
|
|
8767
9897
|
return { pm: "yarn", reason: `install path contains yarn` };
|
|
8768
|
-
if (root.includes(`${
|
|
9898
|
+
if (root.includes(`${path31.sep}.bun${path31.sep}`) || root.includes(`${path31.sep}bun${path31.sep}`))
|
|
8769
9899
|
return { pm: "bun", reason: `install path contains bun` };
|
|
8770
9900
|
return { pm: "npm", reason: "default fallback" };
|
|
8771
9901
|
}
|
|
@@ -8838,7 +9968,7 @@ async function runUpgrade(opts) {
|
|
|
8838
9968
|
);
|
|
8839
9969
|
process3.exit(1);
|
|
8840
9970
|
}
|
|
8841
|
-
const go = await
|
|
9971
|
+
const go = await confirm9({ message: `Run "${cmd} ${args.join(" ")}"?`, default: true });
|
|
8842
9972
|
if (!go) {
|
|
8843
9973
|
info("Cancelled. No changes applied.");
|
|
8844
9974
|
return;
|
|
@@ -8859,7 +9989,7 @@ async function runUpgrade(opts) {
|
|
|
8859
9989
|
}
|
|
8860
9990
|
function spawnInherit(cmd, args) {
|
|
8861
9991
|
return new Promise((resolve2, reject) => {
|
|
8862
|
-
const child =
|
|
9992
|
+
const child = spawn3(cmd, args, { stdio: "inherit", shell: process3.platform === "win32" });
|
|
8863
9993
|
child.on("exit", (code) => resolve2(code ?? 0));
|
|
8864
9994
|
child.on("error", (err2) => reject(err2));
|
|
8865
9995
|
});
|
|
@@ -8878,14 +10008,14 @@ import { cors } from "hono/cors";
|
|
|
8878
10008
|
import { secureHeaders } from "hono/secure-headers";
|
|
8879
10009
|
|
|
8880
10010
|
// src/console/api/stories.ts
|
|
8881
|
-
import
|
|
10011
|
+
import fs41 from "fs";
|
|
8882
10012
|
import { z as z6 } from "zod";
|
|
8883
10013
|
|
|
8884
10014
|
// src/utils/path-safety.ts
|
|
8885
|
-
import
|
|
10015
|
+
import path32 from "path";
|
|
8886
10016
|
function isInside(abs, parent) {
|
|
8887
|
-
const a =
|
|
8888
|
-
const p =
|
|
10017
|
+
const a = path32.resolve(abs) + path32.sep;
|
|
10018
|
+
const p = path32.resolve(parent) + path32.sep;
|
|
8889
10019
|
return a.startsWith(p);
|
|
8890
10020
|
}
|
|
8891
10021
|
|
|
@@ -8925,7 +10055,7 @@ function mountStoriesApi(app, opts) {
|
|
|
8925
10055
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
8926
10056
|
let intakeContent = "";
|
|
8927
10057
|
try {
|
|
8928
|
-
intakeContent =
|
|
10058
|
+
intakeContent = fs41.readFileSync(hit.intakePath, "utf8");
|
|
8929
10059
|
} catch {
|
|
8930
10060
|
}
|
|
8931
10061
|
return c.json({
|
|
@@ -8969,7 +10099,7 @@ function mountStoriesApi(app, opts) {
|
|
|
8969
10099
|
const hit = stories.find((s) => s.id === id);
|
|
8970
10100
|
if (!hit) return c.json({ error: "not_found" }, 404);
|
|
8971
10101
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
8972
|
-
|
|
10102
|
+
fs41.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
|
|
8973
10103
|
return c.json({ ok: true });
|
|
8974
10104
|
});
|
|
8975
10105
|
app.delete("/api/stories/:feature/:id", (c) => {
|
|
@@ -8986,8 +10116,8 @@ function mountStoriesApi(app, opts) {
|
|
|
8986
10116
|
}
|
|
8987
10117
|
|
|
8988
10118
|
// src/console/api/plans.ts
|
|
8989
|
-
import
|
|
8990
|
-
import
|
|
10119
|
+
import fs42 from "fs";
|
|
10120
|
+
import path33 from "path";
|
|
8991
10121
|
import { z as z7 } from "zod";
|
|
8992
10122
|
import { diffLines } from "diff";
|
|
8993
10123
|
var DiffQuery = z7.object({
|
|
@@ -9002,11 +10132,11 @@ function mountPlansApi(app, opts) {
|
|
|
9002
10132
|
app.get("/api/plans", (c) => {
|
|
9003
10133
|
const out = [];
|
|
9004
10134
|
for (const feature of listPlanFeatureNames(opts.paths)) {
|
|
9005
|
-
const featureDir =
|
|
9006
|
-
if (!
|
|
9007
|
-
for (const entry of
|
|
10135
|
+
const featureDir = path33.join(opts.paths.plansDir, feature);
|
|
10136
|
+
if (!fs42.existsSync(featureDir)) continue;
|
|
10137
|
+
for (const entry of fs42.readdirSync(featureDir)) {
|
|
9008
10138
|
if (!entry.endsWith(".md") || entry === "00-index.md") continue;
|
|
9009
|
-
const abs =
|
|
10139
|
+
const abs = path33.join(featureDir, entry);
|
|
9010
10140
|
out.push({ feature, planFile: entry, metadata: readPlanMetadata(abs) });
|
|
9011
10141
|
}
|
|
9012
10142
|
}
|
|
@@ -9015,10 +10145,10 @@ function mountPlansApi(app, opts) {
|
|
|
9015
10145
|
app.get("/api/plans/:feature/:planFile", (c) => {
|
|
9016
10146
|
const { feature, planFile } = c.req.param();
|
|
9017
10147
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
9018
|
-
const abs =
|
|
10148
|
+
const abs = path33.resolve(path33.join(opts.paths.plansDir, feature, planFile));
|
|
9019
10149
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
9020
|
-
if (!
|
|
9021
|
-
const content =
|
|
10150
|
+
if (!fs42.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
10151
|
+
const content = fs42.readFileSync(abs, "utf8");
|
|
9022
10152
|
return c.json({
|
|
9023
10153
|
feature,
|
|
9024
10154
|
planFile,
|
|
@@ -9036,15 +10166,15 @@ function mountPlansApi(app, opts) {
|
|
|
9036
10166
|
if (!parse.success) return c.json({ error: "invalid_query", issues: parse.error.issues }, 400);
|
|
9037
10167
|
const { feature, a, b } = parse.data;
|
|
9038
10168
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
9039
|
-
const featureDir =
|
|
9040
|
-
const aAbs =
|
|
9041
|
-
const bAbs =
|
|
10169
|
+
const featureDir = path33.join(opts.paths.plansDir, feature);
|
|
10170
|
+
const aAbs = path33.join(featureDir, a);
|
|
10171
|
+
const bAbs = path33.join(featureDir, b);
|
|
9042
10172
|
if (!isInside(aAbs, opts.paths.plansDir) || !isInside(bAbs, opts.paths.plansDir)) {
|
|
9043
10173
|
return c.json({ error: "forbidden" }, 403);
|
|
9044
10174
|
}
|
|
9045
|
-
if (!
|
|
9046
|
-
const aText =
|
|
9047
|
-
const bText =
|
|
10175
|
+
if (!fs42.existsSync(aAbs) || !fs42.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
|
|
10176
|
+
const aText = fs42.readFileSync(aAbs, "utf8");
|
|
10177
|
+
const bText = fs42.readFileSync(bAbs, "utf8");
|
|
9048
10178
|
const changes = diffLines(aText, bText);
|
|
9049
10179
|
return c.json({ feature, a, b, changes });
|
|
9050
10180
|
});
|
|
@@ -9052,16 +10182,16 @@ function mountPlansApi(app, opts) {
|
|
|
9052
10182
|
const trash = c.req.query("trash") === "1";
|
|
9053
10183
|
const { feature, planFile } = c.req.param();
|
|
9054
10184
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
9055
|
-
const abs =
|
|
10185
|
+
const abs = path33.join(opts.paths.plansDir, feature, planFile);
|
|
9056
10186
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
9057
|
-
if (!
|
|
10187
|
+
if (!fs42.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
9058
10188
|
if (trash) {
|
|
9059
10189
|
const result = trashOrDelete([abs], opts.paths.trashDir, true);
|
|
9060
|
-
removeOverviewRowForPlanFile(
|
|
10190
|
+
removeOverviewRowForPlanFile(path33.join(opts.paths.plansDir, feature), planFile);
|
|
9061
10191
|
return c.json({ ok: true, trashed: result.trashed });
|
|
9062
10192
|
}
|
|
9063
|
-
|
|
9064
|
-
removeOverviewRowForPlanFile(
|
|
10193
|
+
fs42.rmSync(abs);
|
|
10194
|
+
removeOverviewRowForPlanFile(path33.join(opts.paths.plansDir, feature), planFile);
|
|
9065
10195
|
return c.json({ ok: true });
|
|
9066
10196
|
});
|
|
9067
10197
|
}
|
|
@@ -9109,8 +10239,8 @@ function mountMetaApi(app, opts) {
|
|
|
9109
10239
|
// src/console/api/runs.ts
|
|
9110
10240
|
import { streamSSE } from "hono/streaming";
|
|
9111
10241
|
import { z as z9 } from "zod";
|
|
9112
|
-
import
|
|
9113
|
-
import
|
|
10242
|
+
import path34 from "path";
|
|
10243
|
+
import fs43 from "fs";
|
|
9114
10244
|
var active = /* @__PURE__ */ new Map();
|
|
9115
10245
|
var EVENT_BUF_CAP = 4096;
|
|
9116
10246
|
var StartBody = z9.object({
|
|
@@ -9146,15 +10276,21 @@ function mountRunsApi(app, opts) {
|
|
|
9146
10276
|
if (!cfg.planner?.enabled) {
|
|
9147
10277
|
return c.json({ error: "planner_disabled", detail: "Run `squad config set planner` first." }, 400);
|
|
9148
10278
|
}
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
10279
|
+
let auth2;
|
|
10280
|
+
try {
|
|
10281
|
+
auth2 = resolvePlannerAuthForPaths(opts.paths, cfg.planner.provider, cfg.planner);
|
|
10282
|
+
} catch (err2) {
|
|
10283
|
+
if (err2 instanceof PlannerAuthUnavailableError) {
|
|
10284
|
+
return c.json(
|
|
10285
|
+
{
|
|
10286
|
+
error: "auth_unavailable",
|
|
10287
|
+
mode: cfg.planner.auth?.anthropic ?? "auto",
|
|
10288
|
+
detail: err2.message
|
|
10289
|
+
},
|
|
10290
|
+
400
|
|
10291
|
+
);
|
|
10292
|
+
}
|
|
10293
|
+
throw err2;
|
|
9158
10294
|
}
|
|
9159
10295
|
const stories = listStories(opts.paths, { feature: body.data.feature });
|
|
9160
10296
|
const story = stories.find((s) => s.id === body.data.storyId);
|
|
@@ -9176,7 +10312,7 @@ function mountRunsApi(app, opts) {
|
|
|
9176
10312
|
const draftRuntime = resolveRuntime({
|
|
9177
10313
|
provider: planner.provider,
|
|
9178
10314
|
modelId,
|
|
9179
|
-
|
|
10315
|
+
auth: auth2,
|
|
9180
10316
|
anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
|
|
9181
10317
|
});
|
|
9182
10318
|
const anthropicProviderSpecific = planner.provider === "anthropic" ? {
|
|
@@ -9202,7 +10338,7 @@ function mountRunsApi(app, opts) {
|
|
|
9202
10338
|
scoutRuntime = resolveRuntime({
|
|
9203
10339
|
provider: planner.provider,
|
|
9204
10340
|
modelId: scoutModelId,
|
|
9205
|
-
|
|
10341
|
+
auth: auth2,
|
|
9206
10342
|
anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
|
|
9207
10343
|
});
|
|
9208
10344
|
}
|
|
@@ -9223,7 +10359,7 @@ function mountRunsApi(app, opts) {
|
|
|
9223
10359
|
repoMap
|
|
9224
10360
|
});
|
|
9225
10361
|
const userPrompt = composeUserPrompt({
|
|
9226
|
-
intakeContent:
|
|
10362
|
+
intakeContent: fs43.readFileSync(story.intakePath, "utf8")
|
|
9227
10363
|
});
|
|
9228
10364
|
let result;
|
|
9229
10365
|
try {
|
|
@@ -9233,6 +10369,7 @@ function mountRunsApi(app, opts) {
|
|
|
9233
10369
|
runtime: draftRuntime,
|
|
9234
10370
|
provider: planner.provider,
|
|
9235
10371
|
modelId,
|
|
10372
|
+
auth: auth2,
|
|
9236
10373
|
anthropicProviderSpecific,
|
|
9237
10374
|
systemPrompt,
|
|
9238
10375
|
userPrompt,
|
|
@@ -9302,7 +10439,7 @@ function mountRunsApi(app, opts) {
|
|
|
9302
10439
|
metadataHeader: header,
|
|
9303
10440
|
partial: !runSuccess
|
|
9304
10441
|
});
|
|
9305
|
-
planFile =
|
|
10442
|
+
planFile = path34.relative(opts.paths.root, out.planFile);
|
|
9306
10443
|
}
|
|
9307
10444
|
try {
|
|
9308
10445
|
await writeLastRun(opts.paths, {
|
|
@@ -9338,6 +10475,7 @@ function mountRunsApi(app, opts) {
|
|
|
9338
10475
|
durationMs: result.validation?.durationMs
|
|
9339
10476
|
},
|
|
9340
10477
|
plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
|
|
10478
|
+
authMode: describeAuth(auth2).mode,
|
|
9341
10479
|
providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
|
|
9342
10480
|
});
|
|
9343
10481
|
} catch {
|
|
@@ -9469,7 +10607,13 @@ function mountSecretsApi(app, opts) {
|
|
|
9469
10607
|
planner: {
|
|
9470
10608
|
anthropic: maskToken(s.planner?.anthropic),
|
|
9471
10609
|
openai: maskToken(s.planner?.openai),
|
|
9472
|
-
google: maskToken(s.planner?.google)
|
|
10610
|
+
google: maskToken(s.planner?.google),
|
|
10611
|
+
/**
|
|
10612
|
+
* `claude setup-token` output. Masked exactly like the API keys — the Claude account
|
|
10613
|
+
* card shows it so a user can tell "a token is stored here" from "the OS login store
|
|
10614
|
+
* is being used". Never returned in full, and never by `/api/planner-auth`.
|
|
10615
|
+
*/
|
|
10616
|
+
anthropicOauthToken: maskToken(s.planner?.anthropicOauthToken)
|
|
9473
10617
|
},
|
|
9474
10618
|
tracker: {
|
|
9475
10619
|
jira: {
|
|
@@ -9493,7 +10637,8 @@ function mountSecretsApi(app, opts) {
|
|
|
9493
10637
|
planner: z10.object({
|
|
9494
10638
|
anthropic: z10.string().optional(),
|
|
9495
10639
|
openai: z10.string().optional(),
|
|
9496
|
-
google: z10.string().optional()
|
|
10640
|
+
google: z10.string().optional(),
|
|
10641
|
+
anthropicOauthToken: z10.string().optional()
|
|
9497
10642
|
}).optional(),
|
|
9498
10643
|
tracker: z10.object({
|
|
9499
10644
|
jira: z10.object({
|
|
@@ -9517,6 +10662,9 @@ function mountSecretsApi(app, opts) {
|
|
|
9517
10662
|
if (!parse.success) return c.json({ error: "invalid_body", issues: parse.error.issues }, 400);
|
|
9518
10663
|
const current = loadSecrets(opts.paths.secretsFile);
|
|
9519
10664
|
const merged = mergeSecrets(current, parse.data);
|
|
10665
|
+
if (parse.data.planner?.anthropicOauthToken === "" && merged.planner) {
|
|
10666
|
+
delete merged.planner.anthropicOauthToken;
|
|
10667
|
+
}
|
|
9520
10668
|
saveSecrets(opts.paths.secretsFile, merged);
|
|
9521
10669
|
return c.json({ ok: true });
|
|
9522
10670
|
});
|
|
@@ -9547,13 +10695,166 @@ function mountSecretsApi(app, opts) {
|
|
|
9547
10695
|
});
|
|
9548
10696
|
}
|
|
9549
10697
|
|
|
9550
|
-
// src/console/api/
|
|
10698
|
+
// src/console/api/planner-auth.ts
|
|
9551
10699
|
import { z as z11 } from "zod";
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
10700
|
+
|
|
10701
|
+
// src/commands/auth/status.ts
|
|
10702
|
+
async function runAuthStatus(opts = {}) {
|
|
10703
|
+
const paths = buildPaths(requireSquadRoot());
|
|
10704
|
+
const config2 = loadConfig(paths.configFile);
|
|
10705
|
+
const planner = config2.planner;
|
|
10706
|
+
const provider = planner?.provider ?? "anthropic";
|
|
10707
|
+
const auth2 = resolvePlannerAuthForCwd(provider, planner);
|
|
10708
|
+
const described = describeAuth(auth2);
|
|
10709
|
+
const probe = shouldProbe(opts, provider) ? await probeClaudeAuth(auth2) : void 0;
|
|
10710
|
+
const payload = buildPayload2({ auth: described, provider, planner, probe, fallbackLoggedIn: credentialPresent(auth2) });
|
|
10711
|
+
if (opts.json) {
|
|
10712
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
10713
|
+
`);
|
|
10714
|
+
return;
|
|
10715
|
+
}
|
|
10716
|
+
const kw2 = 15;
|
|
10717
|
+
step("squad-kit auth");
|
|
10718
|
+
blank();
|
|
10719
|
+
kv("auth mode", `${payload.mode} (${payload.reason})`, kw2);
|
|
10720
|
+
kv("credential", payload.credentialHint, kw2);
|
|
10721
|
+
if (payload.account) {
|
|
10722
|
+
const account = [payload.account.email, payload.account.organization, payload.account.subscriptionType].filter(Boolean).join(" \xB7 ");
|
|
10723
|
+
if (account) kv("account", account, kw2);
|
|
10724
|
+
}
|
|
10725
|
+
if (payload.apiKeySource) kv("api key source", payload.apiKeySource, kw2);
|
|
10726
|
+
kv("runtime", payload.runtime, kw2);
|
|
10727
|
+
if (probe && !probe.ok) {
|
|
10728
|
+
blank();
|
|
10729
|
+
warning(`Live check failed (${probe.kind}).`);
|
|
10730
|
+
info(probe.detail);
|
|
10731
|
+
if (probe.kind === "not-logged-in" || probe.kind === "expired") {
|
|
10732
|
+
info("Run `squad auth login` to sign in again.");
|
|
10733
|
+
}
|
|
10734
|
+
} else if (!probe) {
|
|
10735
|
+
blank();
|
|
10736
|
+
info("Account details skipped (offline). Re-run without --offline to check the live login.");
|
|
10737
|
+
}
|
|
10738
|
+
}
|
|
10739
|
+
function shouldProbe(opts, provider) {
|
|
10740
|
+
if (opts.offline) return false;
|
|
10741
|
+
if (skipExternalProbesInAutomation()) return false;
|
|
10742
|
+
return provider === "anthropic";
|
|
10743
|
+
}
|
|
10744
|
+
function buildPayload2(input5) {
|
|
10745
|
+
const { auth: auth2, provider, planner, probe } = input5;
|
|
10746
|
+
const payload = {
|
|
10747
|
+
mode: auth2.mode,
|
|
10748
|
+
reason: authReasonText(auth2.reason, provider),
|
|
10749
|
+
credentialHint: auth2.credentialHint,
|
|
10750
|
+
runtime: runtimeNameFor(provider, planner),
|
|
10751
|
+
// Without a probe the honest answer is "a credential resolved locally"; with one, the live
|
|
10752
|
+
// result wins. Either way the key is always present so scripts can rely on the shape.
|
|
10753
|
+
loggedIn: probe ? probe.ok : input5.fallbackLoggedIn
|
|
10754
|
+
};
|
|
10755
|
+
if (probe?.ok) {
|
|
10756
|
+
if (probe.account) payload.account = probe.account;
|
|
10757
|
+
if (probe.apiKeySource) payload.apiKeySource = probe.apiKeySource;
|
|
10758
|
+
}
|
|
10759
|
+
return payload;
|
|
10760
|
+
}
|
|
10761
|
+
|
|
10762
|
+
// src/console/api/planner-auth.ts
|
|
10763
|
+
var PLANNER_AUTH_LOGIN_COMMAND = "squad auth login";
|
|
10764
|
+
var PROBE_CACHE_TTL_MS = 6e4;
|
|
10765
|
+
var probeCache;
|
|
10766
|
+
function probeCacheKey(auth2) {
|
|
10767
|
+
const described = describeAuth(auth2);
|
|
10768
|
+
return `${described.mode}:${described.credentialHint}`;
|
|
10769
|
+
}
|
|
10770
|
+
async function probeWithCache(auth2, now) {
|
|
10771
|
+
const key = probeCacheKey(auth2);
|
|
10772
|
+
if (probeCache && probeCache.key === key && now - probeCache.at < PROBE_CACHE_TTL_MS) {
|
|
10773
|
+
return probeCache.result;
|
|
10774
|
+
}
|
|
10775
|
+
const result = await probeClaudeAuth(auth2);
|
|
10776
|
+
if (result.ok) probeCache = { key, at: now, result };
|
|
10777
|
+
return result;
|
|
10778
|
+
}
|
|
10779
|
+
function resetPlannerAuthProbeCache() {
|
|
10780
|
+
probeCache = void 0;
|
|
10781
|
+
}
|
|
10782
|
+
function subscriptionRuntimeConflict(provider, planner) {
|
|
10783
|
+
if (provider !== "anthropic") return plannerAuthRuntimeMismatchMessage(provider);
|
|
10784
|
+
if (runtimeNameFor(provider, planner) === "vercel") return plannerAuthRuntimeMismatchMessage(provider);
|
|
10785
|
+
return null;
|
|
10786
|
+
}
|
|
10787
|
+
function mountPlannerAuthApi(app, opts) {
|
|
10788
|
+
app.get("/api/planner-auth", async (c) => {
|
|
10789
|
+
const config2 = loadConfig(opts.paths.configFile);
|
|
10790
|
+
const planner = config2.planner;
|
|
10791
|
+
const provider = planner?.provider ?? "anthropic";
|
|
10792
|
+
const configuredMode = planner?.auth?.anthropic ?? "auto";
|
|
10793
|
+
const loginProbe = detectClaudeLogin(loadSecrets(opts.paths.secretsFile));
|
|
10794
|
+
const binary = resolveClaudeBinary();
|
|
10795
|
+
let auth2;
|
|
10796
|
+
let error = null;
|
|
10797
|
+
try {
|
|
10798
|
+
auth2 = resolvePlannerAuthForPaths(opts.paths, provider, planner);
|
|
10799
|
+
} catch (err2) {
|
|
10800
|
+
if (!(err2 instanceof PlannerAuthUnavailableError)) throw err2;
|
|
10801
|
+
error = err2.message;
|
|
10802
|
+
}
|
|
10803
|
+
const wantProbe = c.req.query("probe") === "1" && provider === "anthropic" && auth2 !== void 0;
|
|
10804
|
+
const probe = wantProbe ? await probeWithCache(auth2, Date.now()) : void 0;
|
|
10805
|
+
const status = auth2 ? buildPayload2({
|
|
10806
|
+
auth: describeAuth(auth2),
|
|
10807
|
+
provider,
|
|
10808
|
+
planner,
|
|
10809
|
+
probe,
|
|
10810
|
+
fallbackLoggedIn: credentialPresent(auth2)
|
|
10811
|
+
}) : void 0;
|
|
10812
|
+
const payload = {
|
|
10813
|
+
provider,
|
|
10814
|
+
configuredMode,
|
|
10815
|
+
resolved: status ? { mode: status.mode, reason: status.reason, credentialHint: status.credentialHint } : null,
|
|
10816
|
+
error,
|
|
10817
|
+
login: { present: loginProbe.present, hint: loginProbe.hint, detail: loginProbe.detail },
|
|
10818
|
+
account: status?.account ?? null,
|
|
10819
|
+
apiKeySource: status?.apiKeySource ?? null,
|
|
10820
|
+
runtime: runtimeNameFor(provider, planner) === "vercel" ? "vercel" : "agent-sdk",
|
|
10821
|
+
binary: { found: Boolean(binary), source: binary?.source ?? null },
|
|
10822
|
+
loginCommand: PLANNER_AUTH_LOGIN_COMMAND,
|
|
10823
|
+
loggedIn: status?.loggedIn ?? false,
|
|
10824
|
+
probe: probe ? { ran: true, ok: probe.ok, ...probe.ok ? {} : { kind: probe.kind, detail: probe.detail } } : null,
|
|
10825
|
+
runtimeConflict: subscriptionRuntimeConflict(provider, planner)
|
|
10826
|
+
};
|
|
10827
|
+
return c.json(payload);
|
|
10828
|
+
});
|
|
10829
|
+
const ModeBody = z11.object({ mode: z11.string() });
|
|
10830
|
+
app.post("/api/planner-auth/mode", async (c) => {
|
|
10831
|
+
const parsed = ModeBody.safeParse(await c.req.json().catch(() => null));
|
|
10832
|
+
if (!parsed.success) return c.json({ error: "invalid_body", issues: parsed.error.issues }, 400);
|
|
10833
|
+
const config2 = loadConfig(opts.paths.configFile);
|
|
10834
|
+
const next = {
|
|
10835
|
+
...config2,
|
|
10836
|
+
planner: {
|
|
10837
|
+
...config2.planner ?? {},
|
|
10838
|
+
auth: { ...config2.planner?.auth ?? {}, anthropic: parsed.data.mode }
|
|
10839
|
+
}
|
|
10840
|
+
};
|
|
10841
|
+
try {
|
|
10842
|
+
saveConfig(opts.paths.configFile, next);
|
|
10843
|
+
} catch (err2) {
|
|
10844
|
+
return c.json({ error: "invalid_auth_mode", detail: err2.message }, 400);
|
|
10845
|
+
}
|
|
10846
|
+
resetPlannerAuthProbeCache();
|
|
10847
|
+
return c.json({ ok: true, mode: parsed.data.mode });
|
|
10848
|
+
});
|
|
10849
|
+
}
|
|
10850
|
+
|
|
10851
|
+
// src/console/api/tracker.ts
|
|
10852
|
+
import { z as z12 } from "zod";
|
|
10853
|
+
var SearchQuery = z12.object({ q: z12.string().max(500) });
|
|
10854
|
+
var ImportBody = z12.object({
|
|
10855
|
+
issueId: z12.string().min(1),
|
|
10856
|
+
feature: z12.string().min(1).max(80),
|
|
10857
|
+
withAttachments: z12.boolean().optional()
|
|
9557
10858
|
});
|
|
9558
10859
|
function mountTrackerApi(app, opts) {
|
|
9559
10860
|
app.get("/api/tracker/search", async (c) => {
|
|
@@ -9694,17 +10995,17 @@ function countStories(stories) {
|
|
|
9694
10995
|
}
|
|
9695
10996
|
|
|
9696
10997
|
// src/console/api/projects.ts
|
|
9697
|
-
import
|
|
9698
|
-
import
|
|
9699
|
-
import
|
|
10998
|
+
import fs44 from "fs/promises";
|
|
10999
|
+
import path35 from "path";
|
|
11000
|
+
import os2 from "os";
|
|
9700
11001
|
var MAX = 10;
|
|
9701
11002
|
function recentFilePath() {
|
|
9702
|
-
return
|
|
11003
|
+
return path35.join(os2.homedir(), ".squad", "recent-projects.json");
|
|
9703
11004
|
}
|
|
9704
11005
|
async function loadRecent() {
|
|
9705
11006
|
const RECENT_FILE = recentFilePath();
|
|
9706
11007
|
try {
|
|
9707
|
-
const raw = await
|
|
11008
|
+
const raw = await fs44.readFile(RECENT_FILE, "utf8");
|
|
9708
11009
|
const parsed = JSON.parse(raw);
|
|
9709
11010
|
return Array.isArray(parsed) ? parsed.slice(0, MAX) : [];
|
|
9710
11011
|
} catch {
|
|
@@ -9713,8 +11014,8 @@ async function loadRecent() {
|
|
|
9713
11014
|
}
|
|
9714
11015
|
async function saveRecent(list) {
|
|
9715
11016
|
const RECENT_FILE = recentFilePath();
|
|
9716
|
-
await
|
|
9717
|
-
await
|
|
11017
|
+
await fs44.mkdir(path35.dirname(RECENT_FILE), { recursive: true });
|
|
11018
|
+
await fs44.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
|
|
9718
11019
|
}
|
|
9719
11020
|
function mountProjectsApi(app, opts) {
|
|
9720
11021
|
app.get("/api/projects/recent", async (c) => c.json(await loadRecent()));
|
|
@@ -9737,14 +11038,15 @@ function mountApi(app, opts) {
|
|
|
9737
11038
|
mountConfigApi(app, opts);
|
|
9738
11039
|
mountRunsApi(app, opts);
|
|
9739
11040
|
mountSecretsApi(app, opts);
|
|
11041
|
+
mountPlannerAuthApi(app, opts);
|
|
9740
11042
|
mountTrackerApi(app, opts);
|
|
9741
11043
|
mountDoctorApi(app, opts);
|
|
9742
11044
|
mountCopyPlanPromptApi(app, opts);
|
|
9743
11045
|
}
|
|
9744
11046
|
|
|
9745
11047
|
// src/console/static-assets.ts
|
|
9746
|
-
import
|
|
9747
|
-
import
|
|
11048
|
+
import fs45 from "fs";
|
|
11049
|
+
import path36 from "path";
|
|
9748
11050
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9749
11051
|
import { Readable } from "stream";
|
|
9750
11052
|
function pathnameForStatic(c) {
|
|
@@ -9774,18 +11076,18 @@ function mountStaticAssets(app, _opts) {
|
|
|
9774
11076
|
}
|
|
9775
11077
|
if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/healthz")) return c.notFound();
|
|
9776
11078
|
if (!distDir) return c.text("UI not built. Run `pnpm -C console-ui build`.", 404);
|
|
9777
|
-
const indexHtml =
|
|
9778
|
-
if (!
|
|
9779
|
-
return c.html(
|
|
11079
|
+
const indexHtml = path36.join(distDir, "index.html");
|
|
11080
|
+
if (!fs45.existsSync(indexHtml)) return c.text("UI not built", 404);
|
|
11081
|
+
return c.html(fs45.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
|
|
9780
11082
|
});
|
|
9781
11083
|
}
|
|
9782
11084
|
function assetResponse(c, distDir, pathname) {
|
|
9783
11085
|
if (!distDir) return c.text("UI not built", 404);
|
|
9784
11086
|
const rel = pathname.replace(/^\//, "");
|
|
9785
|
-
const abs =
|
|
11087
|
+
const abs = path36.normalize(path36.join(distDir, rel));
|
|
9786
11088
|
if (!isInside2(abs, distDir)) return c.text("forbidden", 403);
|
|
9787
|
-
if (!
|
|
9788
|
-
const body = Readable.toWeb(
|
|
11089
|
+
if (!fs45.existsSync(abs) || !fs45.statSync(abs).isFile()) return c.text("not found", 404);
|
|
11090
|
+
const body = Readable.toWeb(fs45.createReadStream(abs));
|
|
9789
11091
|
return new Response(body, {
|
|
9790
11092
|
headers: {
|
|
9791
11093
|
"content-type": contentTypeFor(abs),
|
|
@@ -9795,9 +11097,9 @@ function assetResponse(c, distDir, pathname) {
|
|
|
9795
11097
|
}
|
|
9796
11098
|
function serveFile(c, distDir, name, ct) {
|
|
9797
11099
|
if (!distDir) return c.text("UI not built", 404);
|
|
9798
|
-
const abs =
|
|
9799
|
-
if (!
|
|
9800
|
-
return new Response(
|
|
11100
|
+
const abs = path36.join(distDir, name);
|
|
11101
|
+
if (!fs45.existsSync(abs)) return c.text("not found", 404);
|
|
11102
|
+
return new Response(fs45.readFileSync(abs), {
|
|
9801
11103
|
headers: { "content-type": ct, "cache-control": "public, max-age=86400" }
|
|
9802
11104
|
});
|
|
9803
11105
|
}
|
|
@@ -9812,13 +11114,13 @@ function contentTypeFor(p) {
|
|
|
9812
11114
|
return "application/octet-stream";
|
|
9813
11115
|
}
|
|
9814
11116
|
function isInside2(abs, parent) {
|
|
9815
|
-
const a =
|
|
9816
|
-
const p =
|
|
11117
|
+
const a = path36.resolve(abs) + path36.sep;
|
|
11118
|
+
const p = path36.resolve(parent) + path36.sep;
|
|
9817
11119
|
return a.startsWith(p);
|
|
9818
11120
|
}
|
|
9819
11121
|
function resolveConsoleUiDist() {
|
|
9820
11122
|
const fromEnv = process.env.SQUAD_CONSOLE_UI_DIST;
|
|
9821
|
-
if (fromEnv &&
|
|
11123
|
+
if (fromEnv && fs45.existsSync(path36.join(fromEnv, "index.html"))) return fromEnv;
|
|
9822
11124
|
const candidates = [
|
|
9823
11125
|
fileURLToPath2(new URL("./console-ui/", import.meta.url)),
|
|
9824
11126
|
// dist/cli.js → dist/console-ui/
|
|
@@ -9830,7 +11132,7 @@ function resolveConsoleUiDist() {
|
|
|
9830
11132
|
// src/console/*.ts → project/console-ui/dist
|
|
9831
11133
|
];
|
|
9832
11134
|
for (const c of candidates) {
|
|
9833
|
-
if (
|
|
11135
|
+
if (fs45.existsSync(path36.join(c, "index.html"))) return c;
|
|
9834
11136
|
}
|
|
9835
11137
|
return null;
|
|
9836
11138
|
}
|
|
@@ -9846,9 +11148,9 @@ function authMiddleware(expected) {
|
|
|
9846
11148
|
};
|
|
9847
11149
|
}
|
|
9848
11150
|
function readPresentedToken(req) {
|
|
9849
|
-
const
|
|
9850
|
-
if (
|
|
9851
|
-
return
|
|
11151
|
+
const auth2 = req.headers.get("authorization");
|
|
11152
|
+
if (auth2 && auth2.toLowerCase().startsWith("bearer ")) {
|
|
11153
|
+
return auth2.slice("bearer ".length).trim();
|
|
9852
11154
|
}
|
|
9853
11155
|
const url = new URL(req.url);
|
|
9854
11156
|
return url.searchParams.get("t");
|
|
@@ -10012,18 +11314,18 @@ async function runConsole(opts) {
|
|
|
10012
11314
|
}
|
|
10013
11315
|
|
|
10014
11316
|
// src/commands/rm/story.ts
|
|
10015
|
-
import
|
|
10016
|
-
import
|
|
10017
|
-
import { confirm as
|
|
11317
|
+
import fs47 from "fs";
|
|
11318
|
+
import path38 from "path";
|
|
11319
|
+
import { confirm as confirm11 } from "@inquirer/prompts";
|
|
10018
11320
|
|
|
10019
11321
|
// src/commands/rm/feature.ts
|
|
10020
|
-
import
|
|
10021
|
-
import
|
|
10022
|
-
import { confirm as
|
|
11322
|
+
import fs46 from "fs";
|
|
11323
|
+
import path37 from "path";
|
|
11324
|
+
import { confirm as confirm10 } from "@inquirer/prompts";
|
|
10023
11325
|
function countStoryDirs(storiesDir, feature) {
|
|
10024
|
-
const d =
|
|
10025
|
-
if (!
|
|
10026
|
-
return
|
|
11326
|
+
const d = path37.join(storiesDir, feature);
|
|
11327
|
+
if (!fs46.existsSync(d)) return 0;
|
|
11328
|
+
return fs46.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
|
|
10027
11329
|
}
|
|
10028
11330
|
async function runRmFeature(featureSlug, opts = {}) {
|
|
10029
11331
|
const root = requireSquadRoot();
|
|
@@ -10057,17 +11359,17 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
10057
11359
|
feature = await pickFeatureInteractive(paths);
|
|
10058
11360
|
}
|
|
10059
11361
|
}
|
|
10060
|
-
const storyDir =
|
|
10061
|
-
const planDir =
|
|
11362
|
+
const storyDir = path37.join(paths.storiesDir, feature);
|
|
11363
|
+
const planDir = path37.join(paths.plansDir, feature);
|
|
10062
11364
|
const nStories = countStoryDirs(paths.storiesDir, feature);
|
|
10063
11365
|
const nPlans = listAllPlanListEntries(paths, feature).length;
|
|
10064
|
-
const hasOverview =
|
|
11366
|
+
const hasOverview = fs46.existsSync(path37.join(planDir, "00-overview.md")) ? 1 : 0;
|
|
10065
11367
|
const targets = [];
|
|
10066
|
-
if (
|
|
10067
|
-
targets.push({ label:
|
|
11368
|
+
if (fs46.existsSync(storyDir)) {
|
|
11369
|
+
targets.push({ label: path37.relative(root, storyDir), absPath: storyDir, kind: "dir" });
|
|
10068
11370
|
}
|
|
10069
|
-
if (
|
|
10070
|
-
targets.push({ label:
|
|
11371
|
+
if (fs46.existsSync(planDir)) {
|
|
11372
|
+
targets.push({ label: path37.relative(root, planDir), absPath: planDir, kind: "dir" });
|
|
10071
11373
|
}
|
|
10072
11374
|
divider("squad rm feature");
|
|
10073
11375
|
info(`Feature: ${feature}`);
|
|
@@ -10082,7 +11384,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
10082
11384
|
kv(t.kind, t.label, 6);
|
|
10083
11385
|
}
|
|
10084
11386
|
if (useTrash) {
|
|
10085
|
-
info(`Items will move to ${
|
|
11387
|
+
info(`Items will move to ${path37.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
|
|
10086
11388
|
} else if (!dryRun) {
|
|
10087
11389
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
10088
11390
|
}
|
|
@@ -10100,7 +11402,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
10100
11402
|
"This command removes an entire feature. Run `squad rm feature --yes` in CI, or `squad rm feature` in a TTY to confirm the prompt interactively."
|
|
10101
11403
|
);
|
|
10102
11404
|
}
|
|
10103
|
-
const ok = await
|
|
11405
|
+
const ok = await confirm10({
|
|
10104
11406
|
message: `Delete all of feature "${feature}" (stories + plans)?`,
|
|
10105
11407
|
default: false
|
|
10106
11408
|
});
|
|
@@ -10115,7 +11417,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
10115
11417
|
useTrash
|
|
10116
11418
|
);
|
|
10117
11419
|
if (result.trashed) {
|
|
10118
|
-
success(`Feature removed. Items moved to ${
|
|
11420
|
+
success(`Feature removed. Items moved to ${path37.relative(root, result.trashed)}.`);
|
|
10119
11421
|
} else {
|
|
10120
11422
|
success("Feature removed (stories and plans directories).");
|
|
10121
11423
|
}
|
|
@@ -10156,11 +11458,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
10156
11458
|
}
|
|
10157
11459
|
}
|
|
10158
11460
|
const targets = [
|
|
10159
|
-
{ label:
|
|
11461
|
+
{ label: path38.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
|
|
10160
11462
|
];
|
|
10161
11463
|
if (story.planFile) {
|
|
10162
|
-
const planAbs =
|
|
10163
|
-
targets.push({ label:
|
|
11464
|
+
const planAbs = path38.join(paths.plansDir, story.feature, story.planFile);
|
|
11465
|
+
targets.push({ label: path38.relative(root, planAbs), absPath: planAbs, kind: "file" });
|
|
10164
11466
|
}
|
|
10165
11467
|
renderPreview("story", targets, {
|
|
10166
11468
|
dryRun,
|
|
@@ -10177,7 +11479,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
10177
11479
|
"Pass --yes to confirm deletion in a non-interactive environment, or run `squad rm story` in a TTY to confirm the prompt interactively."
|
|
10178
11480
|
);
|
|
10179
11481
|
}
|
|
10180
|
-
const ok = await
|
|
11482
|
+
const ok = await confirm11({ message: "Delete this story and related plan/overview row?", default: false });
|
|
10181
11483
|
if (!ok) {
|
|
10182
11484
|
info("Cancelled.");
|
|
10183
11485
|
return;
|
|
@@ -10185,18 +11487,18 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
10185
11487
|
}
|
|
10186
11488
|
const result = deleteStoryRecord({ paths, story, trash: useTrash });
|
|
10187
11489
|
if (result.trashed) {
|
|
10188
|
-
success(`Removed (moved to ${
|
|
11490
|
+
success(`Removed (moved to ${path38.relative(root, result.trashed)}).`);
|
|
10189
11491
|
} else {
|
|
10190
11492
|
success("Removed story folder, plan file (if any), and overview row.");
|
|
10191
11493
|
}
|
|
10192
|
-
const featurePlanDir =
|
|
10193
|
-
const overviewPath =
|
|
10194
|
-
if (
|
|
10195
|
-
const body =
|
|
11494
|
+
const featurePlanDir = path38.join(paths.plansDir, story.feature);
|
|
11495
|
+
const overviewPath = path38.join(featurePlanDir, "00-overview.md");
|
|
11496
|
+
if (fs47.existsSync(overviewPath)) {
|
|
11497
|
+
const body = fs47.readFileSync(overviewPath, "utf8");
|
|
10196
11498
|
const onlyShell = countOverviewDataRows(body) === 0;
|
|
10197
11499
|
if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
|
|
10198
11500
|
if (canPrompt) {
|
|
10199
|
-
const also = await
|
|
11501
|
+
const also = await confirm11({
|
|
10200
11502
|
message: `Feature "${story.feature}" has no stories left and an empty overview table. Remove the whole feature with \`squad rm feature ${story.feature}\` now?`,
|
|
10201
11503
|
default: false
|
|
10202
11504
|
});
|
|
@@ -10212,8 +11514,8 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
10212
11514
|
}
|
|
10213
11515
|
|
|
10214
11516
|
// src/commands/rm/plan.ts
|
|
10215
|
-
import
|
|
10216
|
-
import { confirm as
|
|
11517
|
+
import path39 from "path";
|
|
11518
|
+
import { confirm as confirm12 } from "@inquirer/prompts";
|
|
10217
11519
|
async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
10218
11520
|
const root = requireSquadRoot();
|
|
10219
11521
|
const paths = buildPaths(root);
|
|
@@ -10248,7 +11550,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
10248
11550
|
}
|
|
10249
11551
|
const storyRows = listStories(paths, { feature: plan.feature });
|
|
10250
11552
|
const linked = storyRows.find((s) => s.planFile === plan.file);
|
|
10251
|
-
const targets = [{ label:
|
|
11553
|
+
const targets = [{ label: path39.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
|
|
10252
11554
|
renderPreview("plan", targets, {
|
|
10253
11555
|
dryRun,
|
|
10254
11556
|
trash: useTrash,
|
|
@@ -10264,18 +11566,54 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
10264
11566
|
"Pass --yes to confirm deletion in a non-interactive environment, or run `squad rm plan` in a TTY."
|
|
10265
11567
|
);
|
|
10266
11568
|
}
|
|
10267
|
-
const ok = await
|
|
11569
|
+
const ok = await confirm12({ message: "Delete this plan file only? (intake and config are kept.)", default: false });
|
|
10268
11570
|
if (!ok) {
|
|
10269
11571
|
info("Cancelled.");
|
|
10270
11572
|
return;
|
|
10271
11573
|
}
|
|
10272
11574
|
}
|
|
10273
11575
|
trashOrDelete([plan.abs], paths.trashDir, useTrash);
|
|
10274
|
-
removeOverviewRowForPlanFile(
|
|
10275
|
-
const intakeHint = linked ?
|
|
11576
|
+
removeOverviewRowForPlanFile(path39.join(paths.plansDir, plan.feature), plan.file);
|
|
11577
|
+
const intakeHint = linked ? path39.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
|
|
10276
11578
|
success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
|
|
10277
11579
|
}
|
|
10278
11580
|
|
|
11581
|
+
// src/commands/auth/logout.ts
|
|
11582
|
+
import fs48 from "fs";
|
|
11583
|
+
import { confirm as confirm13 } from "@inquirer/prompts";
|
|
11584
|
+
async function runAuthLogout(opts = {}) {
|
|
11585
|
+
const paths = buildPaths(requireSquadRoot());
|
|
11586
|
+
const secrets = fs48.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
11587
|
+
if (!secrets.planner?.anthropicOauthToken) {
|
|
11588
|
+
info("No Claude OAuth token is stored in .squad/secrets.yaml \u2014 nothing to remove.");
|
|
11589
|
+
info("Your Claude Code login (OS credential store) is separate and was not touched.");
|
|
11590
|
+
return;
|
|
11591
|
+
}
|
|
11592
|
+
if (!opts.yes) {
|
|
11593
|
+
if (!isInteractive()) {
|
|
11594
|
+
warning("Pass -y in non-interactive mode to confirm, or run `squad auth logout` in a TTY.");
|
|
11595
|
+
return;
|
|
11596
|
+
}
|
|
11597
|
+
const ok = await confirm13({
|
|
11598
|
+
message: "Remove the Claude OAuth token squad-kit stored in .squad/secrets.yaml?",
|
|
11599
|
+
default: false
|
|
11600
|
+
});
|
|
11601
|
+
if (!ok) return;
|
|
11602
|
+
}
|
|
11603
|
+
delete secrets.planner.anthropicOauthToken;
|
|
11604
|
+
saveSecrets(paths.secretsFile, secrets);
|
|
11605
|
+
success("Removed the OAuth token squad-kit stored.");
|
|
11606
|
+
info(
|
|
11607
|
+
"Your Claude Code login is untouched \u2014 run `claude` and `/logout` if you also want to sign out of Claude Code."
|
|
11608
|
+
);
|
|
11609
|
+
const mode = loadConfig(paths.configFile).planner?.auth?.anthropic;
|
|
11610
|
+
if (mode === "subscription") {
|
|
11611
|
+
info(
|
|
11612
|
+
"`planner.auth.anthropic` is still `subscription`. If no Claude login remains on this machine, the next run fails with a message pointing back at `squad auth login`."
|
|
11613
|
+
);
|
|
11614
|
+
}
|
|
11615
|
+
}
|
|
11616
|
+
|
|
10279
11617
|
// src/cli.ts
|
|
10280
11618
|
var program = new Command();
|
|
10281
11619
|
program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version(readInstalledPackage().version);
|
|
@@ -10344,6 +11682,14 @@ rm.command("plan [plan-path-or-sequence]").description("Delete a single plan fil
|
|
|
10344
11682
|
rm.command("feature [feature-slug]").description("Delete an entire feature: every story, every plan, the overview file").option("--dry-run", "Show what would be deleted without touching the filesystem", false).option("--trash", "Move into .squad/.trash/<ts>/ instead of permanent delete", false).option("-y, --yes", "Skip the confirmation prompt", false).action(wrapArgs(runRmFeature));
|
|
10345
11683
|
var tracker = program.command("tracker").description("Tracker id helpers");
|
|
10346
11684
|
tracker.command("link [story-path] [tracker-id]").description("Attach/update a tracker id on an existing story intake").option("-y, --yes", "Fail fast instead of prompting for missing values", false).action(wrapArgs(runTrackerLink));
|
|
11685
|
+
var auth = program.command("auth").description(
|
|
11686
|
+
"Sign in to Anthropic with your Claude subscription, inspect it, or clear the stored token. Precedence, CI setup, usage limits, and troubleshooting: https://squad-kit.com/docs/auth"
|
|
11687
|
+
);
|
|
11688
|
+
auth.command("login").description(
|
|
11689
|
+
"Run the Anthropic browser authorization flow and store the resulting token in .squad/secrets.yaml. Prefer not to store a token? Run `claude` and use `/login` instead \u2014 squad-kit picks up the OS credential store automatically under `auth: subscription`."
|
|
11690
|
+
).option("--token <value>", "Use a `claude setup-token` value you already have (CI / remote machines; no browser)").option("--print-only", "Run the flow, print the token, store nothing and change no config", false).option("-y, --yes", "Overwrite an already-stored token without confirming", false).action(wrap(runAuthLogin));
|
|
11691
|
+
auth.command("status").description("Show the resolved auth mode, the credential behind it, and the signed-in account").option("--json", "Emit a stable JSON object for scripting (never contains the token)", false).option("--offline", "Skip the live account check; report only what resolves locally", false).action(wrap(runAuthStatus));
|
|
11692
|
+
auth.command("logout").description("Remove only the OAuth token squad-kit stored; your Claude Code login is untouched").option("-y, --yes", "Skip the confirmation prompt", false).action(wrap(runAuthLogout));
|
|
10347
11693
|
var config = program.command("config").description("Inspect and change .squad/ settings");
|
|
10348
11694
|
config.command("show").description("Show the current .squad/config.yaml and .squad/secrets.yaml state (secrets masked)").option("--json", "Emit as JSON to stdout (secrets still masked)", false).action(wrap(runConfigShow));
|
|
10349
11695
|
var set = config.command("set").description("Interactively change a section");
|