squad-kit 0.11.0 → 0.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +138 -0
- package/README.md +76 -10
- package/SECURITY.md +16 -2
- package/dist/cli.js +2115 -745
- 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,357 @@ 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
|
+
const tokenSource = typeof o.tokenSource === "string" ? o.tokenSource : void 0;
|
|
1460
|
+
const apiProvider = typeof o.apiProvider === "string" ? o.apiProvider : void 0;
|
|
1461
|
+
return {
|
|
1462
|
+
account: Object.keys(account).length > 0 ? account : void 0,
|
|
1463
|
+
apiKeySource,
|
|
1464
|
+
tokenSource,
|
|
1465
|
+
apiProvider
|
|
1466
|
+
};
|
|
1467
|
+
}
|
|
1468
|
+
var EXPIRED_HINT = /expire|refresh|revoked|no longer valid/i;
|
|
1469
|
+
var NO_BINARY_HINT = /native cli binary|executable not found|claude code executable/i;
|
|
1470
|
+
function mapProbeFailure(err2, auth2) {
|
|
1471
|
+
const message = err2 instanceof Error ? err2.message : String(err2);
|
|
1472
|
+
const signal = detectAuthShapedSdkError(message);
|
|
1473
|
+
if (signal === "authentication_failed") {
|
|
1474
|
+
return {
|
|
1475
|
+
ok: false,
|
|
1476
|
+
kind: EXPIRED_HINT.test(message) ? "expired" : "not-logged-in",
|
|
1477
|
+
// Reuse story 15's mapper rather than writing a second one: the copy a failed probe prints
|
|
1478
|
+
// must match the copy a failed run prints, or the two disagree about the same credential.
|
|
1479
|
+
detail: authErrorMessage("authentication_failed", authErrorContextFrom(auth2))
|
|
1480
|
+
};
|
|
1481
|
+
}
|
|
1482
|
+
if (signal === "oauth_org_not_allowed") {
|
|
1483
|
+
return {
|
|
1484
|
+
ok: false,
|
|
1485
|
+
kind: "org-not-allowed",
|
|
1486
|
+
detail: authErrorMessage("oauth_org_not_allowed", authErrorContextFrom(auth2))
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
if (signal) {
|
|
1490
|
+
return { ok: false, kind: "unknown", detail: authErrorMessage(signal, authErrorContextFrom(auth2)) };
|
|
1491
|
+
}
|
|
1492
|
+
if (NO_BINARY_HINT.test(message)) {
|
|
1493
|
+
return { ok: false, kind: "no-binary", detail: message.slice(0, 300) };
|
|
1494
|
+
}
|
|
1495
|
+
return { ok: false, kind: "unknown", detail: message.slice(0, 300) };
|
|
1496
|
+
}
|
|
1497
|
+
async function probeClaudeAuth(auth2, opts = {}) {
|
|
1498
|
+
if (!resolveClaudeBinary()) {
|
|
1499
|
+
return {
|
|
1500
|
+
ok: false,
|
|
1501
|
+
kind: "no-binary",
|
|
1502
|
+
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."
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1505
|
+
const timeoutMs = opts.timeoutMs ?? DEFAULT_AUTH_PROBE_TIMEOUT_MS;
|
|
1506
|
+
let releaseInput = () => {
|
|
1507
|
+
};
|
|
1508
|
+
const inputClosed = new Promise((resolve2) => {
|
|
1509
|
+
releaseInput = resolve2;
|
|
1510
|
+
});
|
|
1511
|
+
async function* neverSends() {
|
|
1512
|
+
await inputClosed;
|
|
1513
|
+
}
|
|
1514
|
+
let q;
|
|
1515
|
+
let timer;
|
|
1516
|
+
try {
|
|
1517
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
1518
|
+
q = query({
|
|
1519
|
+
prompt: neverSends(),
|
|
1520
|
+
options: {
|
|
1521
|
+
tools: [],
|
|
1522
|
+
settingSources: [],
|
|
1523
|
+
persistSession: false,
|
|
1524
|
+
maxTurns: 1,
|
|
1525
|
+
env: buildSdkEnv(auth2)
|
|
1526
|
+
}
|
|
1527
|
+
});
|
|
1528
|
+
const timedOut = /* @__PURE__ */ Symbol("timeout");
|
|
1529
|
+
const info2 = await Promise.race([
|
|
1530
|
+
typeof q.accountInfo === "function" ? q.accountInfo() : Promise.resolve(void 0),
|
|
1531
|
+
new Promise((resolve2) => {
|
|
1532
|
+
timer = setTimeout(() => resolve2(timedOut), timeoutMs);
|
|
1533
|
+
})
|
|
1534
|
+
]);
|
|
1535
|
+
if (info2 === timedOut) {
|
|
1536
|
+
return {
|
|
1537
|
+
ok: false,
|
|
1538
|
+
kind: "timeout",
|
|
1539
|
+
detail: `The Claude auth check did not answer within ${timeoutMs}ms.`
|
|
1540
|
+
};
|
|
1541
|
+
}
|
|
1542
|
+
const { account, apiKeySource, tokenSource, apiProvider } = readAccount(info2);
|
|
1543
|
+
const base = { ...apiKeySource ? { apiKeySource } : {}, ...account ? { account } : {} };
|
|
1544
|
+
if (auth2.mode !== "subscription") return { ok: true, ...base };
|
|
1545
|
+
if (account) return { ok: true, ...base };
|
|
1546
|
+
if (tokenSource || apiProvider) {
|
|
1547
|
+
return {
|
|
1548
|
+
ok: true,
|
|
1549
|
+
...base,
|
|
1550
|
+
...tokenSource ? { credentialSource: tokenSource } : {},
|
|
1551
|
+
unverifiable: `The Agent SDK resolved ${tokenSource ?? "a credential"}, but token auth reports no account, so its validity can only be confirmed by an actual planning run.`
|
|
1552
|
+
};
|
|
1553
|
+
}
|
|
1554
|
+
return {
|
|
1555
|
+
ok: false,
|
|
1556
|
+
kind: "unknown",
|
|
1557
|
+
detail: "The Claude login reported neither an account nor a credential source, so it could not be verified. Run `squad auth login` to sign in again."
|
|
1558
|
+
};
|
|
1559
|
+
} catch (err2) {
|
|
1560
|
+
return mapProbeFailure(err2, auth2);
|
|
1561
|
+
} finally {
|
|
1562
|
+
if (timer) clearTimeout(timer);
|
|
1563
|
+
releaseInput();
|
|
1564
|
+
try {
|
|
1565
|
+
await q?.return?.();
|
|
1566
|
+
} catch {
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1029
1571
|
// src/commands/config/shared.ts
|
|
1030
1572
|
import { input, password } from "@inquirer/prompts";
|
|
1031
1573
|
var notEmpty = (v) => v.trim().length > 0 || "required";
|
|
@@ -1106,9 +1648,12 @@ function mergePlannerKeyIntoSecrets(base, provider, key) {
|
|
|
1106
1648
|
}
|
|
1107
1649
|
return mergeSecrets(base, { planner: { google: key } });
|
|
1108
1650
|
}
|
|
1651
|
+
function mergePlannerOauthTokenIntoSecrets(base, token) {
|
|
1652
|
+
return mergeSecrets(base, { planner: { anthropicOauthToken: token } });
|
|
1653
|
+
}
|
|
1109
1654
|
|
|
1110
1655
|
// src/commands/config/unset-planner.ts
|
|
1111
|
-
import
|
|
1656
|
+
import fs10 from "fs";
|
|
1112
1657
|
import { confirm } from "@inquirer/prompts";
|
|
1113
1658
|
async function runConfigUnsetPlanner(opts = {}) {
|
|
1114
1659
|
const root = requireSquadRoot();
|
|
@@ -1129,24 +1674,248 @@ async function runConfigUnsetPlanner(opts = {}) {
|
|
|
1129
1674
|
delete next.planner;
|
|
1130
1675
|
saveConfig(paths.configFile, next);
|
|
1131
1676
|
if (opts.removeCredentials) {
|
|
1132
|
-
const s =
|
|
1677
|
+
const s = fs10.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
1133
1678
|
if (s.planner) {
|
|
1134
1679
|
delete s.planner;
|
|
1135
1680
|
saveSecrets(paths.secretsFile, s);
|
|
1136
1681
|
}
|
|
1137
|
-
success("Planner disabled; planner credentials were removed from .squad/secrets.yaml.");
|
|
1138
|
-
} else {
|
|
1139
|
-
success("Planner disabled; existing planner keys in .squad/secrets.yaml (if any) were preserved.");
|
|
1682
|
+
success("Planner disabled; planner credentials were removed from .squad/secrets.yaml.");
|
|
1683
|
+
} else {
|
|
1684
|
+
success("Planner disabled; existing planner keys in .squad/secrets.yaml (if any) were preserved.");
|
|
1685
|
+
}
|
|
1686
|
+
info("Re-enable any time with `squad config set planner`.");
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
// src/commands/auth/login.ts
|
|
1690
|
+
import fs11 from "fs";
|
|
1691
|
+
import { confirm as confirm2, password as password2 } from "@inquirer/prompts";
|
|
1692
|
+
|
|
1693
|
+
// src/core/ci-env.ts
|
|
1694
|
+
function skipExternalProbesInAutomation() {
|
|
1695
|
+
const c = process.env.CI;
|
|
1696
|
+
if (c === "true" || c === "1") return true;
|
|
1697
|
+
if (process.env.GITHUB_ACTIONS) return true;
|
|
1698
|
+
return false;
|
|
1699
|
+
}
|
|
1700
|
+
|
|
1701
|
+
// src/commands/auth/setup-token.ts
|
|
1702
|
+
import { spawn } from "child_process";
|
|
1703
|
+
function spawnSetupToken(binaryPath, opts = {}) {
|
|
1704
|
+
const tee = opts.tee !== false;
|
|
1705
|
+
return new Promise((resolve2, reject) => {
|
|
1706
|
+
const child = spawn(binaryPath, ["setup-token"], {
|
|
1707
|
+
stdio: tee ? ["inherit", "pipe", "inherit"] : "inherit",
|
|
1708
|
+
windowsHide: true
|
|
1709
|
+
});
|
|
1710
|
+
let output = "";
|
|
1711
|
+
if (tee && child.stdout) {
|
|
1712
|
+
child.stdout.setEncoding("utf8");
|
|
1713
|
+
child.stdout.on("data", (chunk) => {
|
|
1714
|
+
output += chunk;
|
|
1715
|
+
process.stderr.write(chunk);
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
child.once("error", reject);
|
|
1719
|
+
child.once("close", (code, signal) => {
|
|
1720
|
+
resolve2({ code, signal, output });
|
|
1721
|
+
});
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
// src/commands/auth/shared.ts
|
|
1726
|
+
var OAUTH_TOKEN_PREFIX = "sk-ant-oat";
|
|
1727
|
+
var OAUTH_TOKEN_PATTERN = /sk-ant-oat[0-9]{2}-[A-Za-z0-9_-]{16,}/;
|
|
1728
|
+
var MIN_TOKEN_LENGTH = 20;
|
|
1729
|
+
var MAX_TOKEN_LENGTH = 2048;
|
|
1730
|
+
var InvalidOauthTokenError = class extends Error {
|
|
1731
|
+
constructor(message) {
|
|
1732
|
+
super(message);
|
|
1733
|
+
this.name = "InvalidOauthTokenError";
|
|
1734
|
+
}
|
|
1735
|
+
};
|
|
1736
|
+
function validateOauthToken(raw) {
|
|
1737
|
+
const value = raw ?? "";
|
|
1738
|
+
if (value.trim().length === 0) {
|
|
1739
|
+
throw new InvalidOauthTokenError(
|
|
1740
|
+
"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`."
|
|
1741
|
+
);
|
|
1742
|
+
}
|
|
1743
|
+
if (/\s/.test(value.trim())) {
|
|
1744
|
+
throw new InvalidOauthTokenError(
|
|
1745
|
+
"That token contains whitespace. Paste only the single-line value printed by `claude setup-token` (quote it in your shell if needed)."
|
|
1746
|
+
);
|
|
1747
|
+
}
|
|
1748
|
+
const token = value.trim();
|
|
1749
|
+
if (token.startsWith("sk-ant-") && !token.startsWith(OAUTH_TOKEN_PREFIX)) {
|
|
1750
|
+
throw new InvalidOauthTokenError(
|
|
1751
|
+
"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`."
|
|
1752
|
+
);
|
|
1753
|
+
}
|
|
1754
|
+
if (token.length < MIN_TOKEN_LENGTH) {
|
|
1755
|
+
throw new InvalidOauthTokenError(
|
|
1756
|
+
`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.`
|
|
1757
|
+
);
|
|
1758
|
+
}
|
|
1759
|
+
if (token.length > MAX_TOKEN_LENGTH) {
|
|
1760
|
+
throw new InvalidOauthTokenError(
|
|
1761
|
+
"That token is implausibly long. Paste only the single line `claude setup-token` printed."
|
|
1762
|
+
);
|
|
1763
|
+
}
|
|
1764
|
+
return token;
|
|
1765
|
+
}
|
|
1766
|
+
function extractOauthToken(output) {
|
|
1767
|
+
const hit = OAUTH_TOKEN_PATTERN.exec(output);
|
|
1768
|
+
return hit ? hit[0] : void 0;
|
|
1769
|
+
}
|
|
1770
|
+
function authReasonText(reason, provider) {
|
|
1771
|
+
switch (reason) {
|
|
1772
|
+
case "explicit-config":
|
|
1773
|
+
return "explicit in .squad/config.yaml";
|
|
1774
|
+
case "auto-login-detected":
|
|
1775
|
+
return "auto \u2014 Claude login detected";
|
|
1776
|
+
case "auto-oauth-token":
|
|
1777
|
+
return "auto \u2014 stored OAuth token";
|
|
1778
|
+
case "auto-fallback-api-key":
|
|
1779
|
+
return "auto \u2014 no Claude login, fell back to an API key";
|
|
1780
|
+
case "provider-is-api-key-only":
|
|
1781
|
+
return `${provider} is API-key only`;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
function runtimeNameFor(provider, planner) {
|
|
1785
|
+
if (provider !== "anthropic") return "vercel";
|
|
1786
|
+
return planner?.runtime?.anthropic ?? "agent-sdk";
|
|
1787
|
+
}
|
|
1788
|
+
function credentialPresent(auth2) {
|
|
1789
|
+
return auth2.mode === "api-key" || auth2.loginHint !== "assumed";
|
|
1790
|
+
}
|
|
1791
|
+
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`.";
|
|
1792
|
+
|
|
1793
|
+
// src/commands/auth/login.ts
|
|
1794
|
+
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.";
|
|
1795
|
+
async function runAuthLogin(opts = {}) {
|
|
1796
|
+
if (opts.token && opts.printOnly) {
|
|
1797
|
+
throw new Error(
|
|
1798
|
+
"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."
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
const providedToken = opts.token !== void 0 ? validateOauthToken(opts.token) : void 0;
|
|
1802
|
+
const storing = !opts.printOnly;
|
|
1803
|
+
const workspace = storing ? openWorkspace() : void 0;
|
|
1804
|
+
if (workspace && !await confirmOverwrite(workspace.secrets, Boolean(opts.yes))) return;
|
|
1805
|
+
if (process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
1806
|
+
warning(
|
|
1807
|
+
"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."
|
|
1808
|
+
);
|
|
1809
|
+
}
|
|
1810
|
+
const token = providedToken ?? await runBrowserFlow();
|
|
1811
|
+
if (!storing) {
|
|
1812
|
+
blank();
|
|
1813
|
+
success("Login complete. Nothing was stored.");
|
|
1814
|
+
info("Token (copy it somewhere safe \u2014 squad-kit did not save it):");
|
|
1815
|
+
process.stdout.write(`${token}
|
|
1816
|
+
`);
|
|
1817
|
+
blank();
|
|
1818
|
+
info(SUBSCRIPTION_ALTERNATIVE_HINT);
|
|
1819
|
+
info("To store it in this workspace later: `squad auth login --token <value>`.");
|
|
1820
|
+
return;
|
|
1821
|
+
}
|
|
1822
|
+
const ws = workspace;
|
|
1823
|
+
saveSecrets(ws.paths.secretsFile, mergePlannerOauthTokenIntoSecrets(ws.secrets, token));
|
|
1824
|
+
success("OAuth token saved to .squad/secrets.yaml (chmod 0600 on POSIX).");
|
|
1825
|
+
const planner = ws.config.planner;
|
|
1826
|
+
if (planner) {
|
|
1827
|
+
saveConfig(ws.paths.configFile, {
|
|
1828
|
+
...ws.config,
|
|
1829
|
+
planner: { ...planner, auth: { ...planner.auth, anthropic: "subscription" } }
|
|
1830
|
+
});
|
|
1831
|
+
info("Set `planner.auth.anthropic: subscription` in .squad/config.yaml.");
|
|
1832
|
+
} else {
|
|
1833
|
+
warning(
|
|
1834
|
+
"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."
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
await verifyAndSummarise(planner ? { ...planner, auth: { anthropic: "subscription" } } : void 0);
|
|
1838
|
+
blank();
|
|
1839
|
+
info(SUBSCRIPTION_ALTERNATIVE_HINT);
|
|
1840
|
+
info("Next: `squad auth status` to confirm, then `squad new-plan --api`.");
|
|
1841
|
+
}
|
|
1842
|
+
function openWorkspace() {
|
|
1843
|
+
const paths = buildPaths(requireSquadRoot());
|
|
1844
|
+
return {
|
|
1845
|
+
paths,
|
|
1846
|
+
config: loadConfig(paths.configFile),
|
|
1847
|
+
secrets: fs11.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {}
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
async function confirmOverwrite(secrets, yes) {
|
|
1851
|
+
if (!secrets.planner?.anthropicOauthToken) return true;
|
|
1852
|
+
if (yes) return true;
|
|
1853
|
+
if (!isInteractive()) {
|
|
1854
|
+
throw new Error(
|
|
1855
|
+
"A Claude OAuth token is already stored in .squad/secrets.yaml. Pass -y to overwrite it non-interactively."
|
|
1856
|
+
);
|
|
1857
|
+
}
|
|
1858
|
+
const ok = await confirm2({
|
|
1859
|
+
message: "A Claude OAuth token is already stored. Replace it with a new one?",
|
|
1860
|
+
default: false
|
|
1861
|
+
});
|
|
1862
|
+
if (!ok) info("Keeping the existing token; nothing changed.");
|
|
1863
|
+
return ok;
|
|
1864
|
+
}
|
|
1865
|
+
async function runBrowserFlow() {
|
|
1866
|
+
if (!isInteractive()) throw new Error(NON_TTY_LOGIN_MESSAGE);
|
|
1867
|
+
const binary = resolveClaudeBinary();
|
|
1868
|
+
if (!binary) throw new Error(MISSING_CLAUDE_BINARY_MESSAGE);
|
|
1869
|
+
step("Starting the Anthropic browser authorization flow\u2026");
|
|
1870
|
+
info(`Using ${binary.source === "bundled" ? "the bundled Agent SDK binary" : "the `claude` on your PATH"}.`);
|
|
1871
|
+
blank();
|
|
1872
|
+
const result = await spawnSetupToken(binary.path);
|
|
1873
|
+
if (result.signal) {
|
|
1874
|
+
throw new SquadExit(130, "Login cancelled \u2014 nothing was stored.");
|
|
1875
|
+
}
|
|
1876
|
+
if (result.code !== 0) {
|
|
1877
|
+
throw new SquadExit(
|
|
1878
|
+
result.code ?? 1,
|
|
1879
|
+
`\`claude setup-token\` exited with code ${result.code}. Nothing was stored. Re-run \`squad auth login\` to try again.`
|
|
1880
|
+
);
|
|
1881
|
+
}
|
|
1882
|
+
const extracted = extractOauthToken(result.output);
|
|
1883
|
+
if (extracted) return validateOauthToken(extracted);
|
|
1884
|
+
blank();
|
|
1885
|
+
warning("Could not read the token from the login output. Paste it here:");
|
|
1886
|
+
const pasted = await password2({
|
|
1887
|
+
message: "Token (input hidden):",
|
|
1888
|
+
validate: (v) => {
|
|
1889
|
+
try {
|
|
1890
|
+
validateOauthToken(v);
|
|
1891
|
+
return true;
|
|
1892
|
+
} catch (e) {
|
|
1893
|
+
return e instanceof Error ? e.message : "invalid token";
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
});
|
|
1897
|
+
return validateOauthToken(pasted);
|
|
1898
|
+
}
|
|
1899
|
+
async function verifyAndSummarise(planner) {
|
|
1900
|
+
if (!planner || skipExternalProbesInAutomation()) return;
|
|
1901
|
+
const auth2 = resolvePlannerAuthForCwd("anthropic", planner);
|
|
1902
|
+
const spin = spinner("verifying the Claude login\u2026");
|
|
1903
|
+
const probe = await probeClaudeAuth(auth2);
|
|
1904
|
+
if (!probe.ok) {
|
|
1905
|
+
spin.fail(`could not verify the login (${probe.kind})`);
|
|
1906
|
+
warning(probe.detail);
|
|
1907
|
+
return;
|
|
1140
1908
|
}
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1909
|
+
if (probe.unverifiable) {
|
|
1910
|
+
spin.succeed("login stored");
|
|
1911
|
+
if (probe.credentialSource) kv("credential", probe.credentialSource, 15);
|
|
1912
|
+
info(probe.unverifiable);
|
|
1913
|
+
return;
|
|
1914
|
+
}
|
|
1915
|
+
spin.succeed("login verified");
|
|
1916
|
+
const account = [probe.account?.email, probe.account?.organization, probe.account?.subscriptionType].filter(Boolean).join(" \xB7 ");
|
|
1917
|
+
if (account) kv("account", account, 15);
|
|
1918
|
+
if (probe.apiKeySource) kv("api key source", probe.apiKeySource, 15);
|
|
1150
1919
|
}
|
|
1151
1920
|
|
|
1152
1921
|
// src/commands/config/set-planner.ts
|
|
@@ -1168,7 +1937,7 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1168
1937
|
const root = requireSquadRoot();
|
|
1169
1938
|
const paths = buildPaths(root);
|
|
1170
1939
|
const config2 = loadConfig(paths.configFile);
|
|
1171
|
-
const baseSecrets =
|
|
1940
|
+
const baseSecrets = fs12.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
1172
1941
|
const useYes = Boolean(opts.yes);
|
|
1173
1942
|
const interactive = !useYes && isInteractive();
|
|
1174
1943
|
if (interactive && config2.planner?.enabled === true) {
|
|
@@ -1205,7 +1974,7 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1205
1974
|
}
|
|
1206
1975
|
if (interactive) {
|
|
1207
1976
|
if (!config2.planner || config2.planner.enabled !== true) {
|
|
1208
|
-
const enable = await
|
|
1977
|
+
const enable = await confirm3({ message: "Enable the direct planner?", default: true });
|
|
1209
1978
|
if (!enable) {
|
|
1210
1979
|
return;
|
|
1211
1980
|
}
|
|
@@ -1218,42 +1987,45 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1218
1987
|
} else {
|
|
1219
1988
|
nextPlanner = { ...prev, provider, enabled: true };
|
|
1220
1989
|
}
|
|
1990
|
+
let authChoice;
|
|
1991
|
+
if (interactive && provider === "anthropic") {
|
|
1992
|
+
authChoice = await select({
|
|
1993
|
+
message: "How should squad-kit authenticate with Anthropic?",
|
|
1994
|
+
choices: [
|
|
1995
|
+
{
|
|
1996
|
+
name: "Use my Claude subscription (browser login) \u2014 recommended, no API key needed",
|
|
1997
|
+
value: "subscription"
|
|
1998
|
+
},
|
|
1999
|
+
{ name: "Use an Anthropic API key", value: "api-key" },
|
|
2000
|
+
{
|
|
2001
|
+
name: "Decide automatically (subscription if signed in, otherwise API key)",
|
|
2002
|
+
value: "auto"
|
|
2003
|
+
}
|
|
2004
|
+
],
|
|
2005
|
+
default: "subscription"
|
|
2006
|
+
});
|
|
2007
|
+
nextPlanner = { ...nextPlanner, auth: { ...nextPlanner.auth, anthropic: authChoice } };
|
|
2008
|
+
}
|
|
1221
2009
|
if (useYes) {
|
|
1222
2010
|
if (!resolveProviderKey(provider)) {
|
|
1223
2011
|
throw credentialError(provider);
|
|
1224
2012
|
}
|
|
1225
2013
|
} 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.");
|
|
2014
|
+
if (authChoice === "subscription") {
|
|
2015
|
+
await offerBrowserLogin(baseSecrets);
|
|
2016
|
+
} else if (authChoice === "auto") {
|
|
2017
|
+
info(
|
|
2018
|
+
"`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)."
|
|
2019
|
+
);
|
|
2020
|
+
if (!detectClaudeLogin(baseSecrets).present && !resolveProviderKey(provider)) {
|
|
2021
|
+
warning(
|
|
2022
|
+
"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."
|
|
2023
|
+
);
|
|
1244
2024
|
}
|
|
1245
2025
|
} 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)");
|
|
2026
|
+
await promptForApiKey(provider, paths.secretsFile, baseSecrets);
|
|
1255
2027
|
}
|
|
1256
|
-
const cacheEnabled = await
|
|
2028
|
+
const cacheEnabled = await confirm3({
|
|
1257
2029
|
message: "Enable prompt caching? (Recommended \u2014 reduces billed tokens by ~70% on most providers.)",
|
|
1258
2030
|
default: nextPlanner.cache?.enabled ?? true
|
|
1259
2031
|
});
|
|
@@ -1261,6 +2033,12 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1261
2033
|
}
|
|
1262
2034
|
const next = { ...config2, planner: nextPlanner };
|
|
1263
2035
|
saveConfig(paths.configFile, next);
|
|
2036
|
+
const auth2 = interactive && provider === "anthropic" ? tryResolveAuth(provider, nextPlanner) : void 0;
|
|
2037
|
+
const configuredMode = nextPlanner.auth?.anthropic;
|
|
2038
|
+
if (auth2?.mode === "subscription" && (configuredMode === "subscription" || configuredMode === "auto")) {
|
|
2039
|
+
await reportSubscriptionSetup(provider, nextPlanner, auth2);
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
1264
2042
|
const cred = resolveProviderKey(provider);
|
|
1265
2043
|
if (!cred) {
|
|
1266
2044
|
warning(
|
|
@@ -1278,7 +2056,7 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1278
2056
|
kv("credential", `${cred.source} (${sourceText})`, 10);
|
|
1279
2057
|
}
|
|
1280
2058
|
if (skipExternalProbesInAutomation() || !cred) {
|
|
1281
|
-
printPlannerNextSteps(Boolean(cred));
|
|
2059
|
+
printPlannerNextSteps(Boolean(cred), "api-key");
|
|
1282
2060
|
return;
|
|
1283
2061
|
}
|
|
1284
2062
|
try {
|
|
@@ -1299,11 +2077,90 @@ async function runConfigSetPlanner(opts = {}) {
|
|
|
1299
2077
|
`Could not reach provider model list (${msg.slice(0, 160)}). Key was saved; run \`squad doctor\` to verify.`
|
|
1300
2078
|
);
|
|
1301
2079
|
}
|
|
1302
|
-
printPlannerNextSteps(true);
|
|
2080
|
+
printPlannerNextSteps(true, "api-key");
|
|
2081
|
+
}
|
|
2082
|
+
async function promptForApiKey(provider, secretsFile, baseSecrets) {
|
|
2083
|
+
const envVar = providerEnvVar(provider);
|
|
2084
|
+
const existing = resolveProviderKey(provider);
|
|
2085
|
+
if (existing) {
|
|
2086
|
+
const shouldUpdate = await confirm3({
|
|
2087
|
+
message: "A planner credential is already available. Enter a new key and save it to .squad/secrets.yaml?",
|
|
2088
|
+
default: false
|
|
2089
|
+
});
|
|
2090
|
+
if (!shouldUpdate) {
|
|
2091
|
+
info("Keeping the existing planner credential; no change to .squad/secrets.yaml for the key.");
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
const key2 = await password3({
|
|
2095
|
+
message: `${envVar} value (input hidden):`,
|
|
2096
|
+
validate: (v) => v.length >= 20 ? true : "key looks too short"
|
|
2097
|
+
});
|
|
2098
|
+
saveSecrets(secretsFile, mergePlannerKeyIntoSecrets(baseSecrets, provider, key2));
|
|
2099
|
+
success("Planner key saved to .squad/secrets.yaml");
|
|
2100
|
+
info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
|
|
2101
|
+
return;
|
|
2102
|
+
}
|
|
2103
|
+
const key = await password3({
|
|
2104
|
+
message: `${envVar} value (input hidden) \u2014 required:`,
|
|
2105
|
+
validate: (v) => v.length >= 20 ? true : "key looks too short"
|
|
2106
|
+
});
|
|
2107
|
+
saveSecrets(secretsFile, mergePlannerKeyIntoSecrets(baseSecrets, provider, key));
|
|
2108
|
+
success("Planner key saved to .squad/secrets.yaml");
|
|
2109
|
+
info(".squad/secrets.yaml updated (chmod 0600 on POSIX)");
|
|
2110
|
+
}
|
|
2111
|
+
async function offerBrowserLogin(baseSecrets) {
|
|
2112
|
+
if (detectClaudeLogin(baseSecrets).present) {
|
|
2113
|
+
info("A Claude login is already available on this machine \u2014 no API key needed.");
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
const now = await confirm3({
|
|
2117
|
+
message: "No Claude login found. Run the browser login now?",
|
|
2118
|
+
default: true
|
|
2119
|
+
});
|
|
2120
|
+
if (!now) {
|
|
2121
|
+
info("Skipped. Run `squad auth login` when you are ready \u2014 planning needs it in subscription mode.");
|
|
2122
|
+
return;
|
|
2123
|
+
}
|
|
2124
|
+
await runAuthLogin({});
|
|
2125
|
+
}
|
|
2126
|
+
function tryResolveAuth(provider, planner) {
|
|
2127
|
+
try {
|
|
2128
|
+
return resolvePlannerAuthForCwd(provider, planner);
|
|
2129
|
+
} catch {
|
|
2130
|
+
return void 0;
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
async function reportSubscriptionSetup(provider, planner, auth2) {
|
|
2134
|
+
blank();
|
|
2135
|
+
success("Planner configuration updated.");
|
|
2136
|
+
kv("provider", provider, 10);
|
|
2137
|
+
kv("model (plan)", modelFor(provider, "plan", planner.modelOverride), 10);
|
|
2138
|
+
kv("model (execute)", modelFor(provider, "execute", planner.modelOverride), 10);
|
|
2139
|
+
kv("auth", `subscription (${describeAuth(auth2).credentialHint})`, 10);
|
|
2140
|
+
if (!skipExternalProbesInAutomation()) {
|
|
2141
|
+
const probe = await probeClaudeAuth(auth2);
|
|
2142
|
+
if (probe.ok) {
|
|
2143
|
+
const account = [probe.account?.email, probe.account?.organization, probe.account?.subscriptionType].filter(Boolean).join(" \xB7 ");
|
|
2144
|
+
info(account ? `Credential check: signed in as ${account}.` : "Credential check: the Claude login responded OK.");
|
|
2145
|
+
} else {
|
|
2146
|
+
warning(`Credential check failed (${probe.kind}). ${probe.detail}`);
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
printPlannerNextSteps(true, "subscription");
|
|
1303
2150
|
}
|
|
1304
|
-
function printPlannerNextSteps(credentialReady) {
|
|
2151
|
+
function printPlannerNextSteps(credentialReady, mode) {
|
|
1305
2152
|
blank();
|
|
1306
2153
|
step("Next:");
|
|
2154
|
+
if (mode === "subscription") {
|
|
2155
|
+
info("1) Confirm the login with `squad auth status` \u2014 it shows the account and how it resolved.");
|
|
2156
|
+
info("2) Verify with `squad doctor` \u2014 every planner check should be green.");
|
|
2157
|
+
info("3) Create a story: squad new-story <feature-slug> (or --no-tracker for a manual story).");
|
|
2158
|
+
info("4) Fill the generated intake.md, then run `squad new-plan --api` to generate the plan.");
|
|
2159
|
+
info(
|
|
2160
|
+
" 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`."
|
|
2161
|
+
);
|
|
2162
|
+
return;
|
|
2163
|
+
}
|
|
1307
2164
|
if (!credentialReady) {
|
|
1308
2165
|
info("1) Save a planner key: re-run `squad config set planner` and paste the key when prompted.");
|
|
1309
2166
|
info("2) Verify with `squad doctor` \u2014 all planner checks should turn green.");
|
|
@@ -1319,7 +2176,7 @@ function printPlannerNextSteps(credentialReady) {
|
|
|
1319
2176
|
}
|
|
1320
2177
|
|
|
1321
2178
|
// src/commands/config/set-tracker.ts
|
|
1322
|
-
import
|
|
2179
|
+
import fs14 from "fs";
|
|
1323
2180
|
import { select as select2 } from "@inquirer/prompts";
|
|
1324
2181
|
|
|
1325
2182
|
// src/tracker/types.ts
|
|
@@ -1335,13 +2192,13 @@ var TrackerError = class extends Error {
|
|
|
1335
2192
|
};
|
|
1336
2193
|
|
|
1337
2194
|
// src/tracker/attachments.ts
|
|
1338
|
-
import
|
|
1339
|
-
import
|
|
2195
|
+
import fs13 from "fs";
|
|
2196
|
+
import path6 from "path";
|
|
1340
2197
|
var DEFAULT_MAX_MB = 10;
|
|
1341
2198
|
async function downloadAttachmentsWith(refs, targetDir, authHeaders, opts = {}) {
|
|
1342
2199
|
const maxMb = opts.maxMegabytes ?? DEFAULT_MAX_MB;
|
|
1343
2200
|
const maxBytes = maxMb * 1024 * 1024;
|
|
1344
|
-
|
|
2201
|
+
fs13.mkdirSync(targetDir, { recursive: true });
|
|
1345
2202
|
const out = [];
|
|
1346
2203
|
for (const ref of refs) {
|
|
1347
2204
|
out.push(await one(ref, targetDir, authHeaders, maxBytes, maxMb));
|
|
@@ -1387,7 +2244,7 @@ async function one(ref, targetDir, headers, maxBytes, maxMb) {
|
|
|
1387
2244
|
skipReason: `${formatMb(buf.byteLength)} after download exceeds cap (${maxMb} MB)`
|
|
1388
2245
|
};
|
|
1389
2246
|
}
|
|
1390
|
-
|
|
2247
|
+
fs13.writeFileSync(dest, buf);
|
|
1391
2248
|
return {
|
|
1392
2249
|
source: ref,
|
|
1393
2250
|
outcome: "written",
|
|
@@ -1397,19 +2254,19 @@ async function one(ref, targetDir, headers, maxBytes, maxMb) {
|
|
|
1397
2254
|
}
|
|
1398
2255
|
function sanitizeFilename(raw) {
|
|
1399
2256
|
const normalized = raw.replace(/\\/g, "/");
|
|
1400
|
-
const base =
|
|
2257
|
+
const base = path6.basename(normalized).replace(/[\\/]/g, "_");
|
|
1401
2258
|
const cleaned = base.replace(/[\x00-\x1f]/g, "").replace(/^\.+/, "").replace(/\s+/g, " ").trim();
|
|
1402
2259
|
if (cleaned.length === 0) return "attachment";
|
|
1403
2260
|
return cleaned.slice(0, 180);
|
|
1404
2261
|
}
|
|
1405
2262
|
function uniqueDestination(dir, name) {
|
|
1406
|
-
let candidate =
|
|
1407
|
-
if (!
|
|
1408
|
-
const ext =
|
|
2263
|
+
let candidate = path6.join(dir, name);
|
|
2264
|
+
if (!fs13.existsSync(candidate)) return candidate;
|
|
2265
|
+
const ext = path6.extname(name);
|
|
1409
2266
|
const stem = name.slice(0, name.length - ext.length);
|
|
1410
2267
|
let n = 1;
|
|
1411
|
-
while (
|
|
1412
|
-
candidate =
|
|
2268
|
+
while (fs13.existsSync(candidate) && n < 1e3) {
|
|
2269
|
+
candidate = path6.join(dir, `${stem} (${n})${ext}`);
|
|
1413
2270
|
n += 1;
|
|
1414
2271
|
}
|
|
1415
2272
|
return candidate;
|
|
@@ -2023,7 +2880,7 @@ async function runConfigSetTracker(opts = {}) {
|
|
|
2023
2880
|
const root = requireSquadRoot();
|
|
2024
2881
|
const paths = buildPaths(root);
|
|
2025
2882
|
const config2 = loadConfig(paths.configFile);
|
|
2026
|
-
const baseSecrets =
|
|
2883
|
+
const baseSecrets = fs14.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
2027
2884
|
const useYes = Boolean(opts.yes);
|
|
2028
2885
|
const interactive = !useYes && isInteractive();
|
|
2029
2886
|
let type;
|
|
@@ -2146,7 +3003,7 @@ async function runConfigSetTracker(opts = {}) {
|
|
|
2146
3003
|
return;
|
|
2147
3004
|
}
|
|
2148
3005
|
const reloaded = loadConfig(paths.configFile);
|
|
2149
|
-
const s =
|
|
3006
|
+
const s = fs14.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
2150
3007
|
if (reloaded.tracker.type === "jira") {
|
|
2151
3008
|
const r = await probeJiraConnectivity(s, reloaded);
|
|
2152
3009
|
if (r.ok) {
|
|
@@ -2193,8 +3050,8 @@ function printTrackerNextSteps(type) {
|
|
|
2193
3050
|
}
|
|
2194
3051
|
|
|
2195
3052
|
// src/commands/config/unset-tracker.ts
|
|
2196
|
-
import
|
|
2197
|
-
import { confirm as
|
|
3053
|
+
import fs15 from "fs";
|
|
3054
|
+
import { confirm as confirm4 } from "@inquirer/prompts";
|
|
2198
3055
|
async function runConfigUnsetTracker(opts = {}) {
|
|
2199
3056
|
const root = requireSquadRoot();
|
|
2200
3057
|
const paths = buildPaths(root);
|
|
@@ -2205,7 +3062,7 @@ async function runConfigUnsetTracker(opts = {}) {
|
|
|
2205
3062
|
warning("Pass -y in non-interactive mode to confirm setting the tracker to none.");
|
|
2206
3063
|
return;
|
|
2207
3064
|
}
|
|
2208
|
-
const ok = await
|
|
3065
|
+
const ok = await confirm4({ message: "Set tracker to none? (keeps tracker tokens in .squad/secrets.yaml.)", default: false });
|
|
2209
3066
|
if (!ok) {
|
|
2210
3067
|
return;
|
|
2211
3068
|
}
|
|
@@ -2213,7 +3070,7 @@ async function runConfigUnsetTracker(opts = {}) {
|
|
|
2213
3070
|
const next = { ...config2, tracker: { type: "none" } };
|
|
2214
3071
|
saveConfig(paths.configFile, next);
|
|
2215
3072
|
if (opts.removeCredentials) {
|
|
2216
|
-
const s =
|
|
3073
|
+
const s = fs15.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
2217
3074
|
if (s.tracker) {
|
|
2218
3075
|
delete s.tracker;
|
|
2219
3076
|
saveSecrets(paths.secretsFile, s);
|
|
@@ -2226,8 +3083,8 @@ async function runConfigUnsetTracker(opts = {}) {
|
|
|
2226
3083
|
}
|
|
2227
3084
|
|
|
2228
3085
|
// src/commands/config/remove-credential.ts
|
|
2229
|
-
import
|
|
2230
|
-
import { confirm as
|
|
3086
|
+
import fs16 from "fs";
|
|
3087
|
+
import { confirm as confirm5 } from "@inquirer/prompts";
|
|
2231
3088
|
async function runConfigRemoveCredential(section, opts = {}) {
|
|
2232
3089
|
if (section !== "planner" && section !== "tracker") {
|
|
2233
3090
|
throw new Error(
|
|
@@ -2242,7 +3099,7 @@ async function runConfigRemoveCredential(section, opts = {}) {
|
|
|
2242
3099
|
warning("Pass -y in non-interactive mode to confirm, or run `squad config remove-credential` in a TTY.");
|
|
2243
3100
|
return;
|
|
2244
3101
|
}
|
|
2245
|
-
const ok = await
|
|
3102
|
+
const ok = await confirm5({
|
|
2246
3103
|
message: `Delete all ${section} credentials from .squad/secrets.yaml? (config is unchanged)`,
|
|
2247
3104
|
default: false
|
|
2248
3105
|
});
|
|
@@ -2250,7 +3107,7 @@ async function runConfigRemoveCredential(section, opts = {}) {
|
|
|
2250
3107
|
return;
|
|
2251
3108
|
}
|
|
2252
3109
|
}
|
|
2253
|
-
const s =
|
|
3110
|
+
const s = fs16.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
2254
3111
|
if (section === "planner") {
|
|
2255
3112
|
if (s.planner) {
|
|
2256
3113
|
delete s.planner;
|
|
@@ -2268,10 +3125,10 @@ async function runConfigRemoveCredential(section, opts = {}) {
|
|
|
2268
3125
|
// src/commands/init.ts
|
|
2269
3126
|
var SUPPORTED_AGENTS = ["claude-code", "cursor", "copilot", "gemini"];
|
|
2270
3127
|
var AGENT_INSTALL = {
|
|
2271
|
-
"claude-code": { subdir:
|
|
2272
|
-
cursor: { subdir:
|
|
2273
|
-
copilot: { subdir:
|
|
2274
|
-
gemini: { subdir:
|
|
3128
|
+
"claude-code": { subdir: path7.join(".claude", "commands"), srcDir: "claude-code" },
|
|
3129
|
+
cursor: { subdir: path7.join(".cursor", "commands"), srcDir: "cursor" },
|
|
3130
|
+
copilot: { subdir: path7.join(".github", "prompts"), srcDir: "copilot" },
|
|
3131
|
+
gemini: { subdir: path7.join(".gemini", "commands"), srcDir: "gemini" }
|
|
2275
3132
|
};
|
|
2276
3133
|
async function runInit(opts) {
|
|
2277
3134
|
const root = process.cwd();
|
|
@@ -2279,7 +3136,7 @@ async function runInit(opts) {
|
|
|
2279
3136
|
if (!opts.yes) {
|
|
2280
3137
|
banner();
|
|
2281
3138
|
}
|
|
2282
|
-
if (
|
|
3139
|
+
if (fs17.existsSync(paths.configFile) && !opts.force) {
|
|
2283
3140
|
if (!isInteractive() || opts.yes) {
|
|
2284
3141
|
warning(
|
|
2285
3142
|
`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 +3163,7 @@ async function runInit(opts) {
|
|
|
2306
3163
|
}
|
|
2307
3164
|
}
|
|
2308
3165
|
const defaults = {
|
|
2309
|
-
name: opts.name ??
|
|
3166
|
+
name: opts.name ?? path7.basename(root),
|
|
2310
3167
|
language: opts.language ?? "typescript",
|
|
2311
3168
|
tracker: opts.tracker ?? "none",
|
|
2312
3169
|
agents: parseAgentsFlag(opts.agents)
|
|
@@ -2382,11 +3239,11 @@ async function runInit(opts) {
|
|
|
2382
3239
|
};
|
|
2383
3240
|
}
|
|
2384
3241
|
} else {
|
|
2385
|
-
const
|
|
3242
|
+
const plannerEnabled2 = await confirm6({
|
|
2386
3243
|
message: "Enable automatic plan generation? (press Enter to skip \u2014 you can always edit .squad/config.yaml later)",
|
|
2387
3244
|
default: false
|
|
2388
3245
|
});
|
|
2389
|
-
if (
|
|
3246
|
+
if (plannerEnabled2) {
|
|
2390
3247
|
const provider = await select3({
|
|
2391
3248
|
message: "Planner provider:",
|
|
2392
3249
|
choices: [
|
|
@@ -2411,29 +3268,60 @@ async function runInit(opts) {
|
|
|
2411
3268
|
}
|
|
2412
3269
|
}
|
|
2413
3270
|
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?`,
|
|
3271
|
+
let needsApiKey = true;
|
|
3272
|
+
if (plannerBlock.provider === "anthropic") {
|
|
3273
|
+
const authMode = await select3({
|
|
3274
|
+
message: "How should squad-kit authenticate with Anthropic?",
|
|
2421
3275
|
choices: [
|
|
2422
|
-
{
|
|
2423
|
-
|
|
2424
|
-
|
|
3276
|
+
{
|
|
3277
|
+
name: "Use my Claude subscription (browser login) \u2014 recommended, no API key needed",
|
|
3278
|
+
value: "subscription"
|
|
3279
|
+
},
|
|
3280
|
+
{ name: "Use an Anthropic API key", value: "api-key" },
|
|
3281
|
+
{
|
|
3282
|
+
name: "Decide automatically (subscription if signed in, otherwise API key)",
|
|
3283
|
+
value: "auto"
|
|
3284
|
+
}
|
|
3285
|
+
],
|
|
3286
|
+
default: "subscription"
|
|
2425
3287
|
});
|
|
2426
|
-
|
|
2427
|
-
|
|
2428
|
-
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
|
|
2434
|
-
|
|
3288
|
+
plannerBlock = { ...plannerBlock, auth: { anthropic: authMode } };
|
|
3289
|
+
needsApiKey = authMode === "api-key";
|
|
3290
|
+
if (authMode === "subscription") {
|
|
3291
|
+
info("No API key needed. Run `squad auth login` after init to sign in with your Claude account.");
|
|
3292
|
+
info("Already signed in through Claude Code (`claude` then `/login`)? squad-kit picks that up automatically.");
|
|
3293
|
+
} else if (authMode === "auto") {
|
|
3294
|
+
info(
|
|
3295
|
+
"`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)."
|
|
3296
|
+
);
|
|
3297
|
+
info("Run `squad auth login` for the subscription, or `squad config set planner` to save a key.");
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
if (needsApiKey) {
|
|
3301
|
+
const envVar = providerEnvVar(plannerBlock.provider);
|
|
3302
|
+
const envPresent = Boolean(process.env[envVar]);
|
|
3303
|
+
if (envPresent) {
|
|
3304
|
+
info(`Planner key detected in ${envVar}; keeping env-var resolution as the primary source.`);
|
|
2435
3305
|
} else {
|
|
2436
|
-
|
|
3306
|
+
const storeKey = await select3({
|
|
3307
|
+
message: `No ${envVar} in your environment. How do you want to store your planner API key?`,
|
|
3308
|
+
choices: [
|
|
3309
|
+
{ name: `Enter it now and save to .squad/secrets.yaml (git-ignored)`, value: "secrets" },
|
|
3310
|
+
{ name: `I'll export ${envVar} in my shell profile myself`, value: "env" }
|
|
3311
|
+
]
|
|
3312
|
+
});
|
|
3313
|
+
if (storeKey === "secrets") {
|
|
3314
|
+
const key = await password4({
|
|
3315
|
+
message: `${envVar} value (input hidden):`,
|
|
3316
|
+
validate: (v) => v.length >= 20 ? true : "key looks too short"
|
|
3317
|
+
});
|
|
3318
|
+
const base = loadSecrets(paths.secretsFile);
|
|
3319
|
+
const merged = mergePlannerKeyIntoSecrets(base, plannerBlock.provider, key);
|
|
3320
|
+
saveSecrets(paths.secretsFile, merged);
|
|
3321
|
+
success("Planner key saved to .squad/secrets.yaml");
|
|
3322
|
+
} else {
|
|
3323
|
+
info(`Remember to export ${envVar} before running \`squad new-plan\`.`);
|
|
3324
|
+
}
|
|
2437
3325
|
}
|
|
2438
3326
|
}
|
|
2439
3327
|
}
|
|
@@ -2450,12 +3338,12 @@ async function runInit(opts) {
|
|
|
2450
3338
|
agents: answers.agents,
|
|
2451
3339
|
planner: plannerBlock
|
|
2452
3340
|
};
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
3341
|
+
fs17.mkdirSync(paths.squadDir, { recursive: true });
|
|
3342
|
+
fs17.mkdirSync(paths.storiesDir, { recursive: true });
|
|
3343
|
+
fs17.mkdirSync(paths.plansDir, { recursive: true });
|
|
2456
3344
|
writeFileSafe(
|
|
2457
3345
|
paths.indexFile,
|
|
2458
|
-
readFile(
|
|
3346
|
+
readFile(path7.join(templatesDir(), "index.md")),
|
|
2459
3347
|
!!opts.force
|
|
2460
3348
|
);
|
|
2461
3349
|
saveConfig(paths.configFile, config2);
|
|
@@ -2463,7 +3351,7 @@ async function runInit(opts) {
|
|
|
2463
3351
|
info("Updated .gitignore with squad-managed patterns (e.g. .squad/secrets.yaml).");
|
|
2464
3352
|
}
|
|
2465
3353
|
writeFileSafe(
|
|
2466
|
-
|
|
3354
|
+
path7.join(paths.squadDir, "README.md"),
|
|
2467
3355
|
renderSquadReadme(config2),
|
|
2468
3356
|
!!opts.force
|
|
2469
3357
|
);
|
|
@@ -2480,8 +3368,13 @@ async function runInit(opts) {
|
|
|
2480
3368
|
const model = modelFor(plannerBlock.provider, "plan", plannerBlock.modelOverride);
|
|
2481
3369
|
const overrideNote = plannerBlock.modelOverride?.[plannerBlock.provider] ? " (override)" : "";
|
|
2482
3370
|
kv("planner", `${plannerBlock.provider}/${model}${overrideNote}`);
|
|
2483
|
-
|
|
2484
|
-
if (
|
|
3371
|
+
const subscriptionMode = plannerBlock.auth?.anthropic === "subscription" && plannerBlock.provider === "anthropic";
|
|
3372
|
+
if (subscriptionMode) {
|
|
3373
|
+
kv("auth", "subscription (Claude login) \u2014 run `squad auth login` if you have not signed in");
|
|
3374
|
+
} else {
|
|
3375
|
+
kv("key", hasKey ? `\u2713 (${envVar} or .squad/secrets.yaml)` : `missing (set ${envVar})`);
|
|
3376
|
+
}
|
|
3377
|
+
if (!hasKey && !subscriptionMode) {
|
|
2485
3378
|
blank();
|
|
2486
3379
|
warning("Planner key not found in environment or .squad/secrets.yaml.");
|
|
2487
3380
|
info("Set one of these before running `squad new-plan`:");
|
|
@@ -2503,13 +3396,13 @@ function parseAgentsFlag(flag) {
|
|
|
2503
3396
|
}
|
|
2504
3397
|
async function confirmSafe(message, defaultValue, nonInteractive) {
|
|
2505
3398
|
if (nonInteractive) return defaultValue;
|
|
2506
|
-
return
|
|
3399
|
+
return confirm6({ message, default: defaultValue });
|
|
2507
3400
|
}
|
|
2508
3401
|
function installAgent(root, agent, overwrite) {
|
|
2509
3402
|
const config2 = AGENT_INSTALL[agent];
|
|
2510
|
-
const src =
|
|
2511
|
-
const dest =
|
|
2512
|
-
if (!
|
|
3403
|
+
const src = path7.join(templatesDir(), "agents", config2.srcDir);
|
|
3404
|
+
const dest = path7.join(root, config2.subdir);
|
|
3405
|
+
if (!fs17.existsSync(src)) return;
|
|
2513
3406
|
copyTree(src, dest, overwrite);
|
|
2514
3407
|
}
|
|
2515
3408
|
function renderSquadReadme(config2) {
|
|
@@ -2535,22 +3428,22 @@ Plan meta-prompts (\`generate-plan.md\`, \`story-skeleton.md\`) ship inside the
|
|
|
2535
3428
|
}
|
|
2536
3429
|
|
|
2537
3430
|
// src/commands/new-story.ts
|
|
2538
|
-
import
|
|
2539
|
-
import
|
|
3431
|
+
import fs23 from "fs";
|
|
3432
|
+
import path13 from "path";
|
|
2540
3433
|
import { input as input3, select as select5 } from "@inquirer/prompts";
|
|
2541
3434
|
|
|
2542
3435
|
// src/core/stories.ts
|
|
2543
|
-
import
|
|
2544
|
-
import
|
|
3436
|
+
import fs18 from "fs";
|
|
3437
|
+
import path8 from "path";
|
|
2545
3438
|
var STORY_FILE = /^(\d{2,})-story-.+\.md$/;
|
|
2546
3439
|
function listFeatures(paths) {
|
|
2547
|
-
if (!
|
|
2548
|
-
return
|
|
3440
|
+
if (!fs18.existsSync(paths.storiesDir)) return [];
|
|
3441
|
+
return fs18.readdirSync(paths.storiesDir, { withFileTypes: true }).filter((d) => d.isDirectory()).map((d) => d.name).sort();
|
|
2549
3442
|
}
|
|
2550
3443
|
function listPlanFeatureNames(paths) {
|
|
2551
3444
|
const names = new Set(listFeatures(paths));
|
|
2552
|
-
if (
|
|
2553
|
-
for (const e of
|
|
3445
|
+
if (fs18.existsSync(paths.plansDir)) {
|
|
3446
|
+
for (const e of fs18.readdirSync(paths.plansDir, { withFileTypes: true })) {
|
|
2554
3447
|
if (e.isDirectory()) names.add(e.name);
|
|
2555
3448
|
}
|
|
2556
3449
|
}
|
|
@@ -2561,16 +3454,16 @@ function listAllStories(paths) {
|
|
|
2561
3454
|
}
|
|
2562
3455
|
function listStories(paths, opts = {}) {
|
|
2563
3456
|
const rows = [];
|
|
2564
|
-
if (!
|
|
2565
|
-
for (const feature of
|
|
3457
|
+
if (!fs18.existsSync(paths.storiesDir)) return rows;
|
|
3458
|
+
for (const feature of fs18.readdirSync(paths.storiesDir, { withFileTypes: true })) {
|
|
2566
3459
|
if (!feature.isDirectory()) continue;
|
|
2567
3460
|
if (opts.feature && feature.name !== opts.feature) continue;
|
|
2568
|
-
const featureDir =
|
|
2569
|
-
for (const story of
|
|
3461
|
+
const featureDir = path8.join(paths.storiesDir, feature.name);
|
|
3462
|
+
for (const story of fs18.readdirSync(featureDir, { withFileTypes: true })) {
|
|
2570
3463
|
if (!story.isDirectory()) continue;
|
|
2571
|
-
const storyDir =
|
|
2572
|
-
const intakePath =
|
|
2573
|
-
if (!
|
|
3464
|
+
const storyDir = path8.join(featureDir, story.name);
|
|
3465
|
+
const intakePath = path8.join(storyDir, "intake.md");
|
|
3466
|
+
if (!fs18.existsSync(intakePath)) continue;
|
|
2574
3467
|
rows.push({
|
|
2575
3468
|
feature: feature.name,
|
|
2576
3469
|
id: story.name,
|
|
@@ -2584,10 +3477,10 @@ function listStories(paths, opts = {}) {
|
|
|
2584
3477
|
return rows;
|
|
2585
3478
|
}
|
|
2586
3479
|
function findPlanFor(plansDir, feature, storyId) {
|
|
2587
|
-
const featureDir =
|
|
2588
|
-
if (!
|
|
3480
|
+
const featureDir = path8.join(plansDir, feature);
|
|
3481
|
+
if (!fs18.existsSync(featureDir)) return void 0;
|
|
2589
3482
|
const candidates = [];
|
|
2590
|
-
for (const entry of
|
|
3483
|
+
for (const entry of fs18.readdirSync(featureDir)) {
|
|
2591
3484
|
if (!STORY_FILE.test(entry)) continue;
|
|
2592
3485
|
if (entry.includes(storyId)) candidates.push(entry);
|
|
2593
3486
|
}
|
|
@@ -2600,16 +3493,16 @@ function findStoryByIntake(records, intakePath) {
|
|
|
2600
3493
|
return records.find((r) => normalizeIntakePath(r.intakePath) === target);
|
|
2601
3494
|
}
|
|
2602
3495
|
function normalizeIntakePath(p) {
|
|
2603
|
-
const resolved =
|
|
3496
|
+
const resolved = path8.resolve(p);
|
|
2604
3497
|
try {
|
|
2605
|
-
return
|
|
3498
|
+
return fs18.realpathSync(resolved);
|
|
2606
3499
|
} catch {
|
|
2607
3500
|
return resolved;
|
|
2608
3501
|
}
|
|
2609
3502
|
}
|
|
2610
3503
|
function readTitleHint(intakePath) {
|
|
2611
3504
|
try {
|
|
2612
|
-
const raw =
|
|
3505
|
+
const raw = fs18.readFileSync(intakePath, "utf8");
|
|
2613
3506
|
const head = raw.split("\n").slice(0, 30).join("\n");
|
|
2614
3507
|
const hintLine = head.match(/>\s*\*\*Title hint \(from CLI\):\*\*\s*(.+?)\s*$/m);
|
|
2615
3508
|
const fromHint = hintLine?.[1]?.trim();
|
|
@@ -2646,7 +3539,7 @@ function parsePlanMetadataLine(line2) {
|
|
|
2646
3539
|
}
|
|
2647
3540
|
function readPlanMetadata(planFileAbsPath) {
|
|
2648
3541
|
try {
|
|
2649
|
-
const raw =
|
|
3542
|
+
const raw = fs18.readFileSync(planFileAbsPath, "utf8").replace(/^\uFEFF/, "");
|
|
2650
3543
|
const lines = raw.split(/\n/);
|
|
2651
3544
|
const scan = Math.min(lines.length, 40);
|
|
2652
3545
|
for (let i = 0; i < scan; i++) {
|
|
@@ -2678,8 +3571,8 @@ function trackerIdForFilename(type, id) {
|
|
|
2678
3571
|
}
|
|
2679
3572
|
|
|
2680
3573
|
// src/core/story-intake-helpers.ts
|
|
2681
|
-
import
|
|
2682
|
-
import
|
|
3574
|
+
import fs19 from "fs";
|
|
3575
|
+
import path9 from "path";
|
|
2683
3576
|
function buildIntakeTemplateVars(args) {
|
|
2684
3577
|
const e = escapeTemplateValue;
|
|
2685
3578
|
const { config: config2, featureSlug, storyFolderName, trackerId, title, fetchedIssue } = args;
|
|
@@ -2735,7 +3628,7 @@ function renderAttachmentsTable(downloads) {
|
|
|
2735
3628
|
const size = `${sizeNum | 0} KB`;
|
|
2736
3629
|
switch (d.outcome) {
|
|
2737
3630
|
case "written":
|
|
2738
|
-
return `| \`attachments/${
|
|
3631
|
+
return `| \`attachments/${path9.basename(d.absolutePath)}\` | ${size} | downloaded |`;
|
|
2739
3632
|
case "skipped-oversize":
|
|
2740
3633
|
return `| ${d.source.filename} | ${size} | **skipped** \u2014 ${d.skipReason} |`;
|
|
2741
3634
|
case "skipped-error":
|
|
@@ -2764,34 +3657,34 @@ function buildSkippedTrackerPreamble(trackerType) {
|
|
|
2764
3657
|
].join("\n");
|
|
2765
3658
|
}
|
|
2766
3659
|
function ensureFeatureOverview(featurePlanDir, slug) {
|
|
2767
|
-
|
|
2768
|
-
const overviewFile =
|
|
2769
|
-
if (
|
|
2770
|
-
const template = readFile(
|
|
3660
|
+
fs19.mkdirSync(featurePlanDir, { recursive: true });
|
|
3661
|
+
const overviewFile = path9.join(featurePlanDir, "00-overview.md");
|
|
3662
|
+
if (fs19.existsSync(overviewFile)) return;
|
|
3663
|
+
const template = readFile(path9.join(templatesDir(), "overview.md"));
|
|
2771
3664
|
writeFileSafe(overviewFile, render(template, { featureSlug: slug }), false);
|
|
2772
3665
|
}
|
|
2773
3666
|
|
|
2774
3667
|
// src/core/story-mutations.ts
|
|
2775
|
-
import
|
|
2776
|
-
import
|
|
3668
|
+
import fs22 from "fs";
|
|
3669
|
+
import path12 from "path";
|
|
2777
3670
|
|
|
2778
3671
|
// src/commands/rm/shared.ts
|
|
2779
|
-
import
|
|
2780
|
-
import
|
|
3672
|
+
import fs21 from "fs";
|
|
3673
|
+
import path11 from "path";
|
|
2781
3674
|
import { select as select4 } from "@inquirer/prompts";
|
|
2782
3675
|
|
|
2783
3676
|
// src/core/sequence.ts
|
|
2784
|
-
import
|
|
2785
|
-
import
|
|
3677
|
+
import fs20 from "fs";
|
|
3678
|
+
import path10 from "path";
|
|
2786
3679
|
var STORY_FILE2 = /^(\d{2,})-story-.+\.md$/;
|
|
2787
3680
|
function scanPlans(plansDir) {
|
|
2788
3681
|
const used = [];
|
|
2789
3682
|
const seen = /* @__PURE__ */ new Map();
|
|
2790
|
-
if (
|
|
2791
|
-
for (const feature of
|
|
3683
|
+
if (fs20.existsSync(plansDir)) {
|
|
3684
|
+
for (const feature of fs20.readdirSync(plansDir, { withFileTypes: true })) {
|
|
2792
3685
|
if (!feature.isDirectory()) continue;
|
|
2793
|
-
const featureDir =
|
|
2794
|
-
for (const entry of
|
|
3686
|
+
const featureDir = path10.join(plansDir, feature.name);
|
|
3687
|
+
for (const entry of fs20.readdirSync(featureDir)) {
|
|
2795
3688
|
const match = entry.match(STORY_FILE2);
|
|
2796
3689
|
if (!match) continue;
|
|
2797
3690
|
const n = parseInt(match[1], 10);
|
|
@@ -2815,9 +3708,9 @@ function canPromptForRm(opts) {
|
|
|
2815
3708
|
return isInteractive() && Boolean(process.stdin.isTTY);
|
|
2816
3709
|
}
|
|
2817
3710
|
function removeOverviewRow(featurePlanDir, storyId) {
|
|
2818
|
-
const overview =
|
|
2819
|
-
if (!
|
|
2820
|
-
const src =
|
|
3711
|
+
const overview = path11.join(featurePlanDir, "00-overview.md");
|
|
3712
|
+
if (!fs21.existsSync(overview)) return;
|
|
3713
|
+
const src = fs21.readFileSync(overview, "utf8");
|
|
2821
3714
|
const lines = src.split("\n");
|
|
2822
3715
|
const filtered = lines.filter((line2) => {
|
|
2823
3716
|
if (!line2.startsWith("|")) return true;
|
|
@@ -2825,12 +3718,12 @@ function removeOverviewRow(featurePlanDir, storyId) {
|
|
|
2825
3718
|
if (/^\|\s*-+/.test(line2)) return true;
|
|
2826
3719
|
return !line2.includes(storyId);
|
|
2827
3720
|
});
|
|
2828
|
-
|
|
3721
|
+
fs21.writeFileSync(overview, filtered.join("\n"), "utf8");
|
|
2829
3722
|
}
|
|
2830
3723
|
function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
|
|
2831
|
-
const overview =
|
|
2832
|
-
if (!
|
|
2833
|
-
const src =
|
|
3724
|
+
const overview = path11.join(featurePlanDir, "00-overview.md");
|
|
3725
|
+
if (!fs21.existsSync(overview)) return;
|
|
3726
|
+
const src = fs21.readFileSync(overview, "utf8");
|
|
2834
3727
|
const token = `\`${planBasename}\``;
|
|
2835
3728
|
const lines = src.split("\n");
|
|
2836
3729
|
const filtered = lines.filter((line2) => {
|
|
@@ -2839,25 +3732,25 @@ function removeOverviewRowForPlanFile(featurePlanDir, planBasename) {
|
|
|
2839
3732
|
if (/^\|\s*-+/.test(line2)) return true;
|
|
2840
3733
|
return !line2.includes(token);
|
|
2841
3734
|
});
|
|
2842
|
-
|
|
3735
|
+
fs21.writeFileSync(overview, filtered.join("\n"), "utf8");
|
|
2843
3736
|
}
|
|
2844
3737
|
function trashOrDelete(absPaths, trashRoot, useTrash) {
|
|
2845
3738
|
if (!useTrash) {
|
|
2846
|
-
for (const p of absPaths)
|
|
3739
|
+
for (const p of absPaths) fs21.rmSync(p, { recursive: true, force: true });
|
|
2847
3740
|
return { removed: absPaths };
|
|
2848
3741
|
}
|
|
2849
3742
|
const ts = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
2850
|
-
const bucket =
|
|
2851
|
-
|
|
3743
|
+
const bucket = path11.join(trashRoot, ts);
|
|
3744
|
+
fs21.mkdirSync(bucket, { recursive: true });
|
|
2852
3745
|
for (const src of absPaths) {
|
|
2853
|
-
const rel =
|
|
2854
|
-
let dst =
|
|
3746
|
+
const rel = path11.basename(src);
|
|
3747
|
+
let dst = path11.join(bucket, rel);
|
|
2855
3748
|
let n = 0;
|
|
2856
|
-
while (
|
|
3749
|
+
while (fs21.existsSync(dst)) {
|
|
2857
3750
|
n += 1;
|
|
2858
|
-
dst =
|
|
3751
|
+
dst = path11.join(bucket, `${n}_${rel}`);
|
|
2859
3752
|
}
|
|
2860
|
-
|
|
3753
|
+
fs21.renameSync(src, dst);
|
|
2861
3754
|
}
|
|
2862
3755
|
return { removed: absPaths, trashed: bucket };
|
|
2863
3756
|
}
|
|
@@ -2868,21 +3761,21 @@ function renderPreview(kind, targets, opts) {
|
|
|
2868
3761
|
kv(t.kind, t.label, 6);
|
|
2869
3762
|
}
|
|
2870
3763
|
if (opts.trash) {
|
|
2871
|
-
info(`Items will move to ${
|
|
3764
|
+
info(`Items will move to ${path11.relative(process.cwd(), opts.trashRoot)}/<timestamp>/`);
|
|
2872
3765
|
} else if (!opts.dryRun) {
|
|
2873
3766
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
2874
3767
|
}
|
|
2875
3768
|
}
|
|
2876
3769
|
function listAllPlanListEntries(paths, featureFilter) {
|
|
2877
|
-
if (!
|
|
3770
|
+
if (!fs21.existsSync(paths.plansDir)) return [];
|
|
2878
3771
|
const out = [];
|
|
2879
|
-
for (const ent of
|
|
3772
|
+
for (const ent of fs21.readdirSync(paths.plansDir, { withFileTypes: true })) {
|
|
2880
3773
|
if (!ent.isDirectory()) continue;
|
|
2881
3774
|
if (featureFilter && ent.name !== featureFilter) continue;
|
|
2882
|
-
const featureDir =
|
|
2883
|
-
for (const name of
|
|
3775
|
+
const featureDir = path11.join(paths.plansDir, ent.name);
|
|
3776
|
+
for (const name of fs21.readdirSync(featureDir)) {
|
|
2884
3777
|
if (!STORY_PLAN.test(name)) continue;
|
|
2885
|
-
out.push({ feature: ent.name, file: name, abs:
|
|
3778
|
+
out.push({ feature: ent.name, file: name, abs: path11.join(featureDir, name) });
|
|
2886
3779
|
}
|
|
2887
3780
|
}
|
|
2888
3781
|
return out.sort((a, b) => (a.feature + a.file).localeCompare(b.feature + b.file));
|
|
@@ -2933,34 +3826,34 @@ function resolveStoryFromArg(arg, stories, root) {
|
|
|
2933
3826
|
if (s.id === t) return s;
|
|
2934
3827
|
if (`${s.feature}/${s.id}` === t) return s;
|
|
2935
3828
|
}
|
|
2936
|
-
const abs =
|
|
2937
|
-
const withIntake = t.endsWith("intake.md") ? abs :
|
|
3829
|
+
const abs = path11.isAbsolute(t) ? t : path11.resolve(root, t);
|
|
3830
|
+
const withIntake = t.endsWith("intake.md") ? abs : path11.join(abs, "intake.md");
|
|
2938
3831
|
return findStoryByIntake(stories, withIntake) ?? findStoryByIntake(stories, abs);
|
|
2939
3832
|
}
|
|
2940
3833
|
function isSequenceToken(s) {
|
|
2941
3834
|
return /^\d{1,4}$/.test(s) && s.length > 0;
|
|
2942
3835
|
}
|
|
2943
3836
|
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(
|
|
3837
|
+
if (!fs21.existsSync(absPath) || !STORY_PLAN.test(path11.basename(absPath))) return void 0;
|
|
3838
|
+
const planRoot = fs21.realpathSync(path11.resolve(paths.plansDir));
|
|
3839
|
+
const fileReal = fs21.realpathSync(path11.resolve(absPath));
|
|
3840
|
+
const rel = path11.relative(planRoot, fileReal);
|
|
3841
|
+
if (rel.startsWith("..") || path11.isAbsolute(rel)) return void 0;
|
|
3842
|
+
const segs = rel.split(path11.sep);
|
|
2950
3843
|
if (segs.length < 2) return void 0;
|
|
2951
3844
|
const feature = segs[0];
|
|
2952
|
-
const file = segs.slice(1).join(
|
|
3845
|
+
const file = segs.slice(1).join(path11.sep);
|
|
2953
3846
|
if (featureFilter && feature !== featureFilter) return void 0;
|
|
2954
|
-
return { feature, file, abs:
|
|
3847
|
+
return { feature, file, abs: path11.resolve(absPath) };
|
|
2955
3848
|
}
|
|
2956
3849
|
function resolvePlanFromArg(arg, paths, featureFilter) {
|
|
2957
3850
|
const t = arg.trim();
|
|
2958
3851
|
if (!t) return void 0;
|
|
2959
|
-
const asPath =
|
|
3852
|
+
const asPath = path11.isAbsolute(t) ? t : path11.resolve(process.cwd(), t);
|
|
2960
3853
|
const a = tryParsePlanFilePath(asPath, paths, featureFilter);
|
|
2961
3854
|
if (a) return a;
|
|
2962
|
-
if (!
|
|
2963
|
-
const under = tryParsePlanFilePath(
|
|
3855
|
+
if (!path11.isAbsolute(t)) {
|
|
3856
|
+
const under = tryParsePlanFilePath(path11.join(paths.plansDir, t), paths, featureFilter);
|
|
2964
3857
|
if (under) return under;
|
|
2965
3858
|
}
|
|
2966
3859
|
if (isSequenceToken(t)) {
|
|
@@ -2990,24 +3883,24 @@ function countOverviewDataRows(overviewContent) {
|
|
|
2990
3883
|
return n;
|
|
2991
3884
|
}
|
|
2992
3885
|
function featureStoriesDirEmpty(paths, feature) {
|
|
2993
|
-
const d =
|
|
2994
|
-
if (!
|
|
2995
|
-
return
|
|
3886
|
+
const d = path11.join(paths.storiesDir, feature);
|
|
3887
|
+
if (!fs21.existsSync(d)) return true;
|
|
3888
|
+
return fs21.readdirSync(d).length === 0;
|
|
2996
3889
|
}
|
|
2997
3890
|
|
|
2998
3891
|
// src/core/story-mutations.ts
|
|
2999
3892
|
function writeNewStoryToDisk(input5) {
|
|
3000
3893
|
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 =
|
|
3894
|
+
const storyDir = path12.join(paths.storiesDir, featureSlug, storyFolderName);
|
|
3895
|
+
const intakeFile = path12.join(storyDir, "intake.md");
|
|
3896
|
+
const attachmentsDir = path12.join(storyDir, "attachments");
|
|
3897
|
+
if (fs22.existsSync(intakeFile)) {
|
|
3898
|
+
const rel = path12.relative(paths.root, intakeFile);
|
|
3006
3899
|
throw new Error(
|
|
3007
3900
|
`Intake already exists: ${intakeFile}. Run \`squad new-plan ${rel}\` to plan it, or \`squad rm story\` to remove the story first.`
|
|
3008
3901
|
);
|
|
3009
3902
|
}
|
|
3010
|
-
|
|
3903
|
+
fs22.mkdirSync(attachmentsDir, { recursive: true });
|
|
3011
3904
|
const template = readBundledPrompt("intake.md");
|
|
3012
3905
|
const rendered = render(
|
|
3013
3906
|
template,
|
|
@@ -3022,7 +3915,7 @@ function writeNewStoryToDisk(input5) {
|
|
|
3022
3915
|
);
|
|
3023
3916
|
const skippedPreamble = noTracker && config2.tracker.type !== "none" && !sourceBlock ? buildSkippedTrackerPreamble(config2.tracker.type) : "";
|
|
3024
3917
|
writeFileSafe(intakeFile, skippedPreamble + sourceBlock + rendered, false);
|
|
3025
|
-
ensureFeatureOverview(
|
|
3918
|
+
ensureFeatureOverview(path12.join(paths.plansDir, featureSlug), featureSlug);
|
|
3026
3919
|
return { storyDir, intakePath: intakeFile, feature: featureSlug, id: storyFolderName };
|
|
3027
3920
|
}
|
|
3028
3921
|
function createStoryRecord(input5) {
|
|
@@ -3071,16 +3964,16 @@ async function createStoryRecordFromIssue(input5) {
|
|
|
3071
3964
|
}
|
|
3072
3965
|
const title = input5.issue.title;
|
|
3073
3966
|
const storyFolderName = trackerIdForFilename(input5.config.tracker.type, id);
|
|
3074
|
-
const storyDir =
|
|
3075
|
-
const intakeFile =
|
|
3076
|
-
const attachmentsDir =
|
|
3077
|
-
if (
|
|
3078
|
-
const rel =
|
|
3967
|
+
const storyDir = path12.join(input5.paths.storiesDir, feature, storyFolderName);
|
|
3968
|
+
const intakeFile = path12.join(storyDir, "intake.md");
|
|
3969
|
+
const attachmentsDir = path12.join(storyDir, "attachments");
|
|
3970
|
+
if (fs22.existsSync(intakeFile)) {
|
|
3971
|
+
const rel = path12.relative(input5.paths.root, intakeFile);
|
|
3079
3972
|
throw new Error(
|
|
3080
3973
|
`Intake already exists: ${intakeFile}. Run \`squad new-plan ${rel}\` to plan it, or \`squad rm story\` to remove the story first.`
|
|
3081
3974
|
);
|
|
3082
3975
|
}
|
|
3083
|
-
|
|
3976
|
+
fs22.mkdirSync(attachmentsDir, { recursive: true });
|
|
3084
3977
|
let downloads = [];
|
|
3085
3978
|
if (input5.withAttachments && input5.issue.attachments.length > 0) {
|
|
3086
3979
|
downloads = await input5.client.downloadAttachments(input5.issue.attachments, attachmentsDir);
|
|
@@ -3108,10 +4001,10 @@ async function createStoryRecordFromIssue(input5) {
|
|
|
3108
4001
|
function deleteStoryRecord(input5) {
|
|
3109
4002
|
const targets = [input5.story.storyDir];
|
|
3110
4003
|
if (input5.story.planFile) {
|
|
3111
|
-
targets.push(
|
|
4004
|
+
targets.push(path12.join(input5.paths.plansDir, input5.story.feature, input5.story.planFile));
|
|
3112
4005
|
}
|
|
3113
4006
|
const result = trashOrDelete(targets, input5.paths.trashDir, input5.trash);
|
|
3114
|
-
removeOverviewRow(
|
|
4007
|
+
removeOverviewRow(path12.join(input5.paths.plansDir, input5.story.feature), input5.story.id);
|
|
3115
4008
|
return result;
|
|
3116
4009
|
}
|
|
3117
4010
|
|
|
@@ -3221,15 +4114,15 @@ async function runNewStory(featureSlug, opts) {
|
|
|
3221
4114
|
'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
4115
|
);
|
|
3223
4116
|
}
|
|
3224
|
-
const storyDir =
|
|
3225
|
-
const intakeFile =
|
|
3226
|
-
const attachmentsDir =
|
|
3227
|
-
if (
|
|
4117
|
+
const storyDir = path13.join(paths.storiesDir, slug, storyFolderName);
|
|
4118
|
+
const intakeFile = path13.join(storyDir, "intake.md");
|
|
4119
|
+
const attachmentsDir = path13.join(storyDir, "attachments");
|
|
4120
|
+
if (fs23.existsSync(intakeFile)) {
|
|
3228
4121
|
throw new Error(
|
|
3229
|
-
`Intake already exists: ${intakeFile}. Run \`squad new-plan ${
|
|
4122
|
+
`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
4123
|
);
|
|
3231
4124
|
}
|
|
3232
|
-
|
|
4125
|
+
fs23.mkdirSync(attachmentsDir, { recursive: true });
|
|
3233
4126
|
let sourceBlock = "";
|
|
3234
4127
|
let fetchedSummary;
|
|
3235
4128
|
let fetchedIssue;
|
|
@@ -3259,14 +4152,14 @@ async function runNewStory(featureSlug, opts) {
|
|
|
3259
4152
|
sourceBlock,
|
|
3260
4153
|
fetchedIssue
|
|
3261
4154
|
});
|
|
3262
|
-
const relativePath =
|
|
3263
|
-
const relAttachDir =
|
|
4155
|
+
const relativePath = path13.relative(root, intakeFile);
|
|
4156
|
+
const relAttachDir = path13.relative(root, attachmentsDir);
|
|
3264
4157
|
success("Created intake");
|
|
3265
4158
|
if (noTracker && config2.tracker.type !== "none") {
|
|
3266
4159
|
warning("Created without a tracker link. Use `squad tracker link` later to attach one.");
|
|
3267
4160
|
}
|
|
3268
4161
|
kv("intake", relativePath, 11);
|
|
3269
|
-
kv("attachments", relAttachDir +
|
|
4162
|
+
kv("attachments", relAttachDir + path13.sep, 11);
|
|
3270
4163
|
if (fetchedSummary && fetchedSummary.written > 0) {
|
|
3271
4164
|
kv("fetched", `${fetchedSummary.id} \xB7 ${fetchedSummary.written} attachments saved`, 11);
|
|
3272
4165
|
}
|
|
@@ -3275,7 +4168,7 @@ async function runNewStory(featureSlug, opts) {
|
|
|
3275
4168
|
info(`squad new-plan ${relativePath}`);
|
|
3276
4169
|
}
|
|
3277
4170
|
async function tryFetchIssue(config2, id, opts, paths, attachmentsDir) {
|
|
3278
|
-
const secretsFromFile =
|
|
4171
|
+
const secretsFromFile = fs23.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
3279
4172
|
const secrets = overlayTrackerEnv(secretsFromFile);
|
|
3280
4173
|
const resolution = clientFor(config2, secrets);
|
|
3281
4174
|
if (resolution.error) {
|
|
@@ -3332,12 +4225,12 @@ function truncate(s, n) {
|
|
|
3332
4225
|
}
|
|
3333
4226
|
|
|
3334
4227
|
// src/commands/new-plan.ts
|
|
3335
|
-
import
|
|
3336
|
-
import
|
|
3337
|
-
import { confirm as
|
|
4228
|
+
import fs35 from "fs";
|
|
4229
|
+
import path25 from "path";
|
|
4230
|
+
import { confirm as confirm7, select as select6 } from "@inquirer/prompts";
|
|
3338
4231
|
|
|
3339
4232
|
// src/utils/clipboard.ts
|
|
3340
|
-
import { spawn, spawnSync } from "child_process";
|
|
4233
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
3341
4234
|
async function copyToClipboard(text) {
|
|
3342
4235
|
const candidates = clipboardCandidates();
|
|
3343
4236
|
if (candidates.length === 0) {
|
|
@@ -3360,7 +4253,7 @@ function tryCopy(cmd, text) {
|
|
|
3360
4253
|
return new Promise((resolve2) => {
|
|
3361
4254
|
let child;
|
|
3362
4255
|
try {
|
|
3363
|
-
child =
|
|
4256
|
+
child = spawn2(cmd.command, cmd.args, { stdio: ["pipe", "ignore", "pipe"] });
|
|
3364
4257
|
} catch (err2) {
|
|
3365
4258
|
resolve2({ ok: false, reason: `${cmd.command} failed to spawn: ${err2.message}` });
|
|
3366
4259
|
return;
|
|
@@ -3425,16 +4318,16 @@ function noCandidateReason() {
|
|
|
3425
4318
|
}
|
|
3426
4319
|
|
|
3427
4320
|
// src/planner/loop.ts
|
|
3428
|
-
import
|
|
3429
|
-
import
|
|
4321
|
+
import path21 from "path";
|
|
4322
|
+
import fs31 from "fs";
|
|
3430
4323
|
import { APICallError as APICallError3 } from "ai";
|
|
3431
4324
|
|
|
3432
4325
|
// src/planner/tools/index.ts
|
|
3433
4326
|
import { randomUUID } from "crypto";
|
|
3434
4327
|
|
|
3435
4328
|
// src/planner/tools/read-file.ts
|
|
3436
|
-
import
|
|
3437
|
-
import
|
|
4329
|
+
import fs24 from "fs";
|
|
4330
|
+
import path14 from "path";
|
|
3438
4331
|
import { tool } from "ai";
|
|
3439
4332
|
import { z } from "zod";
|
|
3440
4333
|
var MAX_BYTES_PER_FULL_READ = 32e3;
|
|
@@ -3469,15 +4362,15 @@ function readFileTool(root, budget, input5) {
|
|
|
3469
4362
|
if (offset === void 0 !== (limit === void 0)) {
|
|
3470
4363
|
return err("read_file: pass both offset and limit for a ranged read, or neither for a full read.");
|
|
3471
4364
|
}
|
|
3472
|
-
const resolved =
|
|
3473
|
-
const relCheck =
|
|
3474
|
-
if (relCheck.startsWith("..") ||
|
|
4365
|
+
const resolved = path14.resolve(root, raw);
|
|
4366
|
+
const relCheck = path14.relative(root, resolved);
|
|
4367
|
+
if (relCheck.startsWith("..") || path14.isAbsolute(relCheck)) {
|
|
3475
4368
|
return err(`read_file: path "${raw}" escapes the project root and was refused.`);
|
|
3476
4369
|
}
|
|
3477
|
-
if (!
|
|
4370
|
+
if (!fs24.existsSync(resolved)) {
|
|
3478
4371
|
return err(`read_file: not found "${raw}".`);
|
|
3479
4372
|
}
|
|
3480
|
-
const stat =
|
|
4373
|
+
const stat = fs24.statSync(resolved);
|
|
3481
4374
|
if (stat.isDirectory()) {
|
|
3482
4375
|
return err(`read_file: "${raw}" is a directory. Ask for a specific file path.`);
|
|
3483
4376
|
}
|
|
@@ -3495,7 +4388,7 @@ function readFileTool(root, budget, input5) {
|
|
|
3495
4388
|
}
|
|
3496
4389
|
let buf;
|
|
3497
4390
|
try {
|
|
3498
|
-
buf =
|
|
4391
|
+
buf = fs24.readFileSync(resolved);
|
|
3499
4392
|
} catch (e) {
|
|
3500
4393
|
return err(`read_file: failed to read "${raw}" (${e.message}).`);
|
|
3501
4394
|
}
|
|
@@ -3518,7 +4411,7 @@ function readFileRanged(resolved, rawRel, budget, fileSize, offset, limit) {
|
|
|
3518
4411
|
}
|
|
3519
4412
|
let data;
|
|
3520
4413
|
try {
|
|
3521
|
-
data =
|
|
4414
|
+
data = fs24.readFileSync(resolved);
|
|
3522
4415
|
} catch (e) {
|
|
3523
4416
|
return err(`read_file: failed to read "${rawRel}" (${e.message}).`);
|
|
3524
4417
|
}
|
|
@@ -3574,14 +4467,14 @@ function looksBinary(buf) {
|
|
|
3574
4467
|
}
|
|
3575
4468
|
|
|
3576
4469
|
// src/planner/tools/grep.ts
|
|
3577
|
-
import
|
|
3578
|
-
import
|
|
4470
|
+
import fs26 from "fs";
|
|
4471
|
+
import path16 from "path";
|
|
3579
4472
|
import { tool as tool2 } from "ai";
|
|
3580
4473
|
import { z as z2 } from "zod";
|
|
3581
4474
|
|
|
3582
4475
|
// src/core/repo-map.ts
|
|
3583
|
-
import
|
|
3584
|
-
import
|
|
4476
|
+
import fs25 from "fs";
|
|
4477
|
+
import path15 from "path";
|
|
3585
4478
|
import ignore from "ignore";
|
|
3586
4479
|
var DEFAULT_IGNORES = [
|
|
3587
4480
|
"node_modules",
|
|
@@ -3597,10 +4490,10 @@ var DEFAULT_IGNORES = [
|
|
|
3597
4490
|
];
|
|
3598
4491
|
function buildRepoIgnore(root) {
|
|
3599
4492
|
const ig = ignore().add(DEFAULT_IGNORES);
|
|
3600
|
-
const gitignore =
|
|
3601
|
-
if (
|
|
4493
|
+
const gitignore = path15.join(root, ".gitignore");
|
|
4494
|
+
if (fs25.existsSync(gitignore)) {
|
|
3602
4495
|
try {
|
|
3603
|
-
ig.add(
|
|
4496
|
+
ig.add(fs25.readFileSync(gitignore, "utf8"));
|
|
3604
4497
|
} catch {
|
|
3605
4498
|
}
|
|
3606
4499
|
}
|
|
@@ -3656,7 +4549,7 @@ function renderDirTree(name, tree, depth) {
|
|
|
3656
4549
|
function renderRepoTree(entries) {
|
|
3657
4550
|
const root = { files: [], dirs: /* @__PURE__ */ new Map() };
|
|
3658
4551
|
for (const e of entries) {
|
|
3659
|
-
const parts = e.rel.split(
|
|
4552
|
+
const parts = e.rel.split(path15.sep).filter(Boolean);
|
|
3660
4553
|
if (parts.length === 0) continue;
|
|
3661
4554
|
insertTree(root, parts, e.size);
|
|
3662
4555
|
}
|
|
@@ -3666,26 +4559,26 @@ function collectFileEntries(root, dir, ig, out, max, includeHidden) {
|
|
|
3666
4559
|
if (out.length >= max) return out;
|
|
3667
4560
|
let dirents;
|
|
3668
4561
|
try {
|
|
3669
|
-
dirents =
|
|
4562
|
+
dirents = fs25.readdirSync(dir, { withFileTypes: true });
|
|
3670
4563
|
} catch {
|
|
3671
4564
|
return out;
|
|
3672
4565
|
}
|
|
3673
4566
|
for (const entry of dirents) {
|
|
3674
4567
|
if (out.length >= max) break;
|
|
3675
4568
|
if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
|
|
3676
|
-
const abs =
|
|
3677
|
-
const rel =
|
|
4569
|
+
const abs = path15.join(dir, entry.name);
|
|
4570
|
+
const rel = path15.relative(root, abs);
|
|
3678
4571
|
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
3679
4572
|
if (entry.isDirectory()) {
|
|
3680
4573
|
collectFileEntries(root, abs, ig, out, max, includeHidden);
|
|
3681
4574
|
} else if (entry.isFile()) {
|
|
3682
4575
|
let size = 0;
|
|
3683
4576
|
try {
|
|
3684
|
-
size =
|
|
4577
|
+
size = fs25.statSync(abs).size;
|
|
3685
4578
|
} catch {
|
|
3686
4579
|
size = 0;
|
|
3687
4580
|
}
|
|
3688
|
-
out.push({ rel: rel.split(
|
|
4581
|
+
out.push({ rel: rel.split(path15.sep).join("/"), size });
|
|
3689
4582
|
}
|
|
3690
4583
|
}
|
|
3691
4584
|
return out;
|
|
@@ -3694,20 +4587,20 @@ function walkFlat(root, dir, ig, out, max, includeHidden) {
|
|
|
3694
4587
|
if (out.length >= max) return;
|
|
3695
4588
|
let entries;
|
|
3696
4589
|
try {
|
|
3697
|
-
entries =
|
|
4590
|
+
entries = fs25.readdirSync(dir, { withFileTypes: true });
|
|
3698
4591
|
} catch {
|
|
3699
4592
|
return;
|
|
3700
4593
|
}
|
|
3701
4594
|
for (const entry of entries) {
|
|
3702
4595
|
if (out.length >= max) return;
|
|
3703
4596
|
if (!includeHidden && entry.name.startsWith(".") && entry.name !== ".squad") continue;
|
|
3704
|
-
const abs =
|
|
3705
|
-
const rel =
|
|
4597
|
+
const abs = path15.join(dir, entry.name);
|
|
4598
|
+
const rel = path15.relative(root, abs);
|
|
3706
4599
|
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
3707
4600
|
if (entry.isDirectory()) {
|
|
3708
4601
|
walkFlat(root, abs, ig, out, max, includeHidden);
|
|
3709
4602
|
} else if (entry.isFile()) {
|
|
3710
|
-
out.push(rel.split(
|
|
4603
|
+
out.push(rel.split(path15.sep).join("/"));
|
|
3711
4604
|
}
|
|
3712
4605
|
}
|
|
3713
4606
|
}
|
|
@@ -3743,12 +4636,12 @@ function runGrep(root, budget, args) {
|
|
|
3743
4636
|
return { content: `grep: invalid pattern (${e.message}).`, isError: true };
|
|
3744
4637
|
}
|
|
3745
4638
|
const ig = buildRepoIgnore(root);
|
|
3746
|
-
const scopeRoot = args.path?.trim() ?
|
|
3747
|
-
const relScope =
|
|
3748
|
-
if (relScope.startsWith("..") ||
|
|
4639
|
+
const scopeRoot = args.path?.trim() ? path16.resolve(root, args.path) : root;
|
|
4640
|
+
const relScope = path16.relative(root, scopeRoot);
|
|
4641
|
+
if (relScope.startsWith("..") || path16.isAbsolute(relScope)) {
|
|
3749
4642
|
return { content: `grep: scope path escapes the project root and was refused.`, isError: true };
|
|
3750
4643
|
}
|
|
3751
|
-
if (!
|
|
4644
|
+
if (!fs26.existsSync(scopeRoot)) {
|
|
3752
4645
|
return { content: `grep: scope path not found "${args.path}".`, isError: true };
|
|
3753
4646
|
}
|
|
3754
4647
|
const matches = [];
|
|
@@ -3756,28 +4649,28 @@ function runGrep(root, budget, args) {
|
|
|
3756
4649
|
if (matches.length >= MAX_GREP_MATCHES) return;
|
|
3757
4650
|
let entries;
|
|
3758
4651
|
try {
|
|
3759
|
-
entries =
|
|
4652
|
+
entries = fs26.readdirSync(dir, { withFileTypes: true });
|
|
3760
4653
|
} catch {
|
|
3761
4654
|
return;
|
|
3762
4655
|
}
|
|
3763
4656
|
for (const entry of entries) {
|
|
3764
4657
|
if (matches.length >= MAX_GREP_MATCHES) return;
|
|
3765
|
-
const abs =
|
|
3766
|
-
const rel =
|
|
4658
|
+
const abs = path16.join(dir, entry.name);
|
|
4659
|
+
const rel = path16.relative(root, abs).split(path16.sep).join("/");
|
|
3767
4660
|
if (!rel || ig.ignores(rel) || entry.isDirectory() && ig.ignores(rel + "/")) continue;
|
|
3768
4661
|
if (entry.isDirectory()) {
|
|
3769
4662
|
walk2(abs);
|
|
3770
4663
|
} else if (entry.isFile()) {
|
|
3771
4664
|
let st;
|
|
3772
4665
|
try {
|
|
3773
|
-
st =
|
|
4666
|
+
st = fs26.statSync(abs);
|
|
3774
4667
|
} catch {
|
|
3775
4668
|
continue;
|
|
3776
4669
|
}
|
|
3777
4670
|
if (st.size > MAX_GREP_FILE_SIZE_BYTES) continue;
|
|
3778
4671
|
let buf;
|
|
3779
4672
|
try {
|
|
3780
|
-
buf =
|
|
4673
|
+
buf = fs26.readFileSync(abs);
|
|
3781
4674
|
} catch {
|
|
3782
4675
|
continue;
|
|
3783
4676
|
}
|
|
@@ -3796,14 +4689,14 @@ function runGrep(root, budget, args) {
|
|
|
3796
4689
|
}
|
|
3797
4690
|
}
|
|
3798
4691
|
};
|
|
3799
|
-
if (
|
|
3800
|
-
const rel =
|
|
4692
|
+
if (fs26.statSync(scopeRoot).isFile()) {
|
|
4693
|
+
const rel = path16.relative(root, scopeRoot).split(path16.sep).join("/");
|
|
3801
4694
|
if (ig.ignores(rel)) {
|
|
3802
4695
|
return { content: `grep: file is ignored by repository filters.`, isError: true };
|
|
3803
4696
|
}
|
|
3804
4697
|
let st;
|
|
3805
4698
|
try {
|
|
3806
|
-
st =
|
|
4699
|
+
st = fs26.statSync(scopeRoot);
|
|
3807
4700
|
} catch {
|
|
3808
4701
|
return { content: `grep: not found.`, isError: true };
|
|
3809
4702
|
}
|
|
@@ -3812,7 +4705,7 @@ function runGrep(root, budget, args) {
|
|
|
3812
4705
|
}
|
|
3813
4706
|
let buf;
|
|
3814
4707
|
try {
|
|
3815
|
-
buf =
|
|
4708
|
+
buf = fs26.readFileSync(scopeRoot);
|
|
3816
4709
|
} catch (e) {
|
|
3817
4710
|
return { content: `grep: failed to read (${e.message}).`, isError: true };
|
|
3818
4711
|
}
|
|
@@ -3846,8 +4739,8 @@ ${matches.join("\n")}` : `No matches found for "${args.pattern}".`;
|
|
|
3846
4739
|
}
|
|
3847
4740
|
|
|
3848
4741
|
// src/planner/tools/list-dir.ts
|
|
3849
|
-
import
|
|
3850
|
-
import
|
|
4742
|
+
import fs27 from "fs";
|
|
4743
|
+
import path17 from "path";
|
|
3851
4744
|
import { tool as tool3 } from "ai";
|
|
3852
4745
|
import { z as z3 } from "zod";
|
|
3853
4746
|
var MAX_ENTRIES_PER_LIST = 200;
|
|
@@ -3865,44 +4758,44 @@ function runListDir(root, budget, rawPath) {
|
|
|
3865
4758
|
};
|
|
3866
4759
|
}
|
|
3867
4760
|
const normalized = rawPath.trim() === "" ? "." : rawPath.trim();
|
|
3868
|
-
const resolved =
|
|
3869
|
-
const relCheck =
|
|
3870
|
-
if (relCheck.startsWith("..") ||
|
|
4761
|
+
const resolved = path17.resolve(root, normalized);
|
|
4762
|
+
const relCheck = path17.relative(root, resolved);
|
|
4763
|
+
if (relCheck.startsWith("..") || path17.isAbsolute(relCheck)) {
|
|
3871
4764
|
return {
|
|
3872
4765
|
content: `list_dir: path "${rawPath}" escapes the project root and was refused.`,
|
|
3873
4766
|
isError: true
|
|
3874
4767
|
};
|
|
3875
4768
|
}
|
|
3876
|
-
const relNorm = relCheck.split(
|
|
4769
|
+
const relNorm = relCheck.split(path17.sep).join("/");
|
|
3877
4770
|
const ig = buildRepoIgnore(root);
|
|
3878
4771
|
if (relNorm && (ig.ignores(relNorm) || ig.ignores(`${relNorm}/`))) {
|
|
3879
4772
|
return { content: "list_dir: (directory ignored by repo-map filter)", isError: true };
|
|
3880
4773
|
}
|
|
3881
|
-
if (!
|
|
4774
|
+
if (!fs27.existsSync(resolved)) {
|
|
3882
4775
|
return { content: `list_dir: not found "${rawPath}".`, isError: true };
|
|
3883
4776
|
}
|
|
3884
|
-
const st =
|
|
4777
|
+
const st = fs27.statSync(resolved);
|
|
3885
4778
|
if (!st.isDirectory()) {
|
|
3886
4779
|
return { content: `list_dir: "${rawPath}" is not a directory.`, isError: true };
|
|
3887
4780
|
}
|
|
3888
4781
|
let dirents;
|
|
3889
4782
|
try {
|
|
3890
|
-
dirents =
|
|
4783
|
+
dirents = fs27.readdirSync(resolved, { withFileTypes: true });
|
|
3891
4784
|
} catch (e) {
|
|
3892
4785
|
return { content: `list_dir: failed to read (${e.message}).`, isError: true };
|
|
3893
4786
|
}
|
|
3894
4787
|
const rows = [];
|
|
3895
4788
|
for (const d of dirents) {
|
|
3896
4789
|
if (rows.length >= MAX_ENTRIES_PER_LIST) break;
|
|
3897
|
-
const childAbs =
|
|
3898
|
-
const childRel =
|
|
4790
|
+
const childAbs = path17.join(resolved, d.name);
|
|
4791
|
+
const childRel = path17.relative(root, childAbs).split(path17.sep).join("/");
|
|
3899
4792
|
if (childRel && (ig.ignores(childRel) || d.isDirectory() && ig.ignores(`${childRel}/`))) continue;
|
|
3900
4793
|
if (d.isDirectory()) {
|
|
3901
4794
|
rows.push({ type: "d", name: d.name + "/", size: null });
|
|
3902
4795
|
} else if (d.isFile()) {
|
|
3903
4796
|
let size = 0;
|
|
3904
4797
|
try {
|
|
3905
|
-
size =
|
|
4798
|
+
size = fs27.statSync(childAbs).size;
|
|
3906
4799
|
} catch {
|
|
3907
4800
|
size = 0;
|
|
3908
4801
|
}
|
|
@@ -4181,96 +5074,9 @@ function vercelToolsFromDefinitions(defs) {
|
|
|
4181
5074
|
return out;
|
|
4182
5075
|
}
|
|
4183
5076
|
|
|
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
5077
|
// src/core/runs.ts
|
|
4272
|
-
import
|
|
4273
|
-
import
|
|
5078
|
+
import fs29 from "fs/promises";
|
|
5079
|
+
import path19 from "path";
|
|
4274
5080
|
import { randomBytes } from "crypto";
|
|
4275
5081
|
|
|
4276
5082
|
// src/core/run-retention.ts
|
|
@@ -4278,9 +5084,9 @@ var RUN_HISTORY_RING_SIZE = 20;
|
|
|
4278
5084
|
var RUN_EVENT_JSONL_UNCOMPRESSED_HEAD = 5;
|
|
4279
5085
|
|
|
4280
5086
|
// src/core/run-events-store.ts
|
|
4281
|
-
import
|
|
5087
|
+
import fs28, { createWriteStream } from "fs";
|
|
4282
5088
|
import fsPromises from "fs/promises";
|
|
4283
|
-
import
|
|
5089
|
+
import path18 from "path";
|
|
4284
5090
|
import readline from "readline";
|
|
4285
5091
|
import { pipeline } from "stream/promises";
|
|
4286
5092
|
import zlib from "zlib";
|
|
@@ -4288,10 +5094,14 @@ var SUFFIX_JSONL = ".events.jsonl";
|
|
|
4288
5094
|
var SUFFIX_GZ = ".events.jsonl.gz";
|
|
4289
5095
|
function redactPlannerEventForDisk(e) {
|
|
4290
5096
|
if (e.kind === "thinking_delta") return { ...e, delta: "" };
|
|
5097
|
+
if (e.kind === "auth_info") {
|
|
5098
|
+
const { account: _account, ...rest } = e;
|
|
5099
|
+
return rest;
|
|
5100
|
+
}
|
|
4291
5101
|
return e;
|
|
4292
5102
|
}
|
|
4293
5103
|
function runsDir(paths) {
|
|
4294
|
-
return
|
|
5104
|
+
return path18.join(paths.squadDir, "runs");
|
|
4295
5105
|
}
|
|
4296
5106
|
function logAppendErrorOnce(err2) {
|
|
4297
5107
|
console.error("[run-events-store] append failed:", err2);
|
|
@@ -4348,8 +5158,8 @@ var RunEventsStoreImpl = class {
|
|
|
4348
5158
|
};
|
|
4349
5159
|
function createRunEventsStore(paths, runId) {
|
|
4350
5160
|
const dir = runsDir(paths);
|
|
4351
|
-
|
|
4352
|
-
const file =
|
|
5161
|
+
fs28.mkdirSync(dir, { recursive: true });
|
|
5162
|
+
const file = path18.join(dir, `${runId}${SUFFIX_JSONL}`);
|
|
4353
5163
|
return new RunEventsStoreImpl(file);
|
|
4354
5164
|
}
|
|
4355
5165
|
var RunEventsReaderImpl = class {
|
|
@@ -4374,7 +5184,7 @@ var RunEventsReaderImpl = class {
|
|
|
4374
5184
|
const limit = opts?.limit ?? Number.POSITIVE_INFINITY;
|
|
4375
5185
|
let logical = 0;
|
|
4376
5186
|
let yielded = 0;
|
|
4377
|
-
const stream =
|
|
5187
|
+
const stream = fs28.createReadStream(this.filePath);
|
|
4378
5188
|
const input5 = this.useGunzip ? stream.pipe(zlib.createGunzip()) : stream;
|
|
4379
5189
|
const rl = readline.createInterface({ input: input5, crlfDelay: Infinity });
|
|
4380
5190
|
try {
|
|
@@ -4402,8 +5212,8 @@ var RunEventsReaderImpl = class {
|
|
|
4402
5212
|
};
|
|
4403
5213
|
async function openRunEventsReader(paths, runId) {
|
|
4404
5214
|
const dir = runsDir(paths);
|
|
4405
|
-
const jsonl =
|
|
4406
|
-
const gz =
|
|
5215
|
+
const jsonl = path18.join(dir, `${runId}${SUFFIX_JSONL}`);
|
|
5216
|
+
const gz = path18.join(dir, `${runId}${SUFFIX_GZ}`);
|
|
4407
5217
|
let jsonlStat = null;
|
|
4408
5218
|
let gzStat = null;
|
|
4409
5219
|
try {
|
|
@@ -4435,10 +5245,10 @@ function collectEventLogs(dir, names) {
|
|
|
4435
5245
|
kind = "jsonl";
|
|
4436
5246
|
}
|
|
4437
5247
|
if (!runId || !kind) continue;
|
|
4438
|
-
const full =
|
|
5248
|
+
const full = path18.join(dir, name);
|
|
4439
5249
|
let st;
|
|
4440
5250
|
try {
|
|
4441
|
-
st = { mtimeMs:
|
|
5251
|
+
st = { mtimeMs: fs28.statSync(full).mtimeMs };
|
|
4442
5252
|
} catch {
|
|
4443
5253
|
continue;
|
|
4444
5254
|
}
|
|
@@ -4466,7 +5276,7 @@ async function fsyncFile(filePath) {
|
|
|
4466
5276
|
async function gzipAtomic(jsonlPath, gzPath) {
|
|
4467
5277
|
const gzTmp = `${gzPath}.tmp`;
|
|
4468
5278
|
await fsPromises.rm(gzTmp, { force: true });
|
|
4469
|
-
await pipeline(
|
|
5279
|
+
await pipeline(fs28.createReadStream(jsonlPath), zlib.createGzip(), fs28.createWriteStream(gzTmp));
|
|
4470
5280
|
await fsyncFile(gzTmp);
|
|
4471
5281
|
await fsPromises.rename(gzTmp, gzPath);
|
|
4472
5282
|
await fsPromises.unlink(jsonlPath);
|
|
@@ -4481,14 +5291,14 @@ async function rotateRunEventsUnlocked(paths) {
|
|
|
4481
5291
|
}
|
|
4482
5292
|
for (const name of names) {
|
|
4483
5293
|
if (name.endsWith(`${SUFFIX_GZ}.tmp`)) {
|
|
4484
|
-
await fsPromises.rm(
|
|
5294
|
+
await fsPromises.rm(path18.join(dir, name), { force: true });
|
|
4485
5295
|
}
|
|
4486
5296
|
}
|
|
4487
5297
|
names = await fsPromises.readdir(dir);
|
|
4488
5298
|
const ordered = collectEventLogs(dir, names);
|
|
4489
5299
|
for (let i = RUN_HISTORY_RING_SIZE; i < ordered.length; i++) {
|
|
4490
5300
|
const e = ordered[i];
|
|
4491
|
-
const summary =
|
|
5301
|
+
const summary = path18.join(dir, `${e.runId}.json`);
|
|
4492
5302
|
await fsPromises.rm(summary, { force: true });
|
|
4493
5303
|
if (e.jsonlPath) await fsPromises.rm(e.jsonlPath, { force: true });
|
|
4494
5304
|
if (e.gzPath) await fsPromises.rm(e.gzPath, { force: true });
|
|
@@ -4498,7 +5308,7 @@ async function rotateRunEventsUnlocked(paths) {
|
|
|
4498
5308
|
const e = keep[i];
|
|
4499
5309
|
const jsonl = e.jsonlPath;
|
|
4500
5310
|
if (!jsonl) continue;
|
|
4501
|
-
const gz =
|
|
5311
|
+
const gz = path18.join(dir, `${e.runId}${SUFFIX_GZ}`);
|
|
4502
5312
|
await gzipAtomic(jsonl, gz);
|
|
4503
5313
|
}
|
|
4504
5314
|
}
|
|
@@ -4515,34 +5325,34 @@ function newRunId() {
|
|
|
4515
5325
|
return `${ts}-${rnd}`;
|
|
4516
5326
|
}
|
|
4517
5327
|
async function pruneOldRuns(paths) {
|
|
4518
|
-
const dir =
|
|
4519
|
-
const entries = (await
|
|
5328
|
+
const dir = path19.join(paths.squadDir, "runs");
|
|
5329
|
+
const entries = (await fs29.readdir(dir)).filter((n) => n.endsWith(".json"));
|
|
4520
5330
|
if (entries.length <= RUN_HISTORY_RING_SIZE) return;
|
|
4521
5331
|
entries.sort();
|
|
4522
5332
|
const drop = entries.slice(0, entries.length - RUN_HISTORY_RING_SIZE);
|
|
4523
5333
|
await Promise.all(
|
|
4524
5334
|
drop.map(async (n) => {
|
|
4525
|
-
const runPath =
|
|
5335
|
+
const runPath = path19.join(dir, n);
|
|
4526
5336
|
const runId = n.replace(/\.json$/u, "");
|
|
4527
|
-
await
|
|
4528
|
-
await
|
|
4529
|
-
await
|
|
5337
|
+
await fs29.rm(runPath, { force: true });
|
|
5338
|
+
await fs29.rm(path19.join(dir, `${runId}.events.jsonl`), { force: true });
|
|
5339
|
+
await fs29.rm(path19.join(dir, `${runId}.events.jsonl.gz`), { force: true });
|
|
4530
5340
|
})
|
|
4531
5341
|
);
|
|
4532
5342
|
}
|
|
4533
5343
|
async function appendRun(paths, rec) {
|
|
4534
|
-
const dir =
|
|
4535
|
-
await
|
|
4536
|
-
const file =
|
|
4537
|
-
await
|
|
5344
|
+
const dir = path19.join(paths.squadDir, "runs");
|
|
5345
|
+
await fs29.mkdir(dir, { recursive: true });
|
|
5346
|
+
const file = path19.join(dir, `${rec.runId}.json`);
|
|
5347
|
+
await fs29.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
4538
5348
|
await pruneOldRuns(paths);
|
|
4539
5349
|
await rotateRunEvents(paths);
|
|
4540
5350
|
}
|
|
4541
5351
|
async function listRuns(paths) {
|
|
4542
|
-
const dir =
|
|
5352
|
+
const dir = path19.join(paths.squadDir, "runs");
|
|
4543
5353
|
let entries;
|
|
4544
5354
|
try {
|
|
4545
|
-
entries = await
|
|
5355
|
+
entries = await fs29.readdir(dir);
|
|
4546
5356
|
} catch {
|
|
4547
5357
|
return [];
|
|
4548
5358
|
}
|
|
@@ -4550,7 +5360,7 @@ async function listRuns(paths) {
|
|
|
4550
5360
|
const out = [];
|
|
4551
5361
|
for (const f of files) {
|
|
4552
5362
|
try {
|
|
4553
|
-
const raw = await
|
|
5363
|
+
const raw = await fs29.readFile(path19.join(dir, f), "utf8");
|
|
4554
5364
|
const parsed = JSON.parse(raw);
|
|
4555
5365
|
if (parsed.version === 1) out.push(parsed);
|
|
4556
5366
|
} catch {
|
|
@@ -4887,6 +5697,11 @@ var VercelRuntime = class {
|
|
|
4887
5697
|
if (typeof apiKeyOrModel !== "string") {
|
|
4888
5698
|
throw new Error("VercelRuntime: apiKey must be a string unless injectModel is true");
|
|
4889
5699
|
}
|
|
5700
|
+
if (!apiKeyOrModel) {
|
|
5701
|
+
throw new Error(
|
|
5702
|
+
`VercelRuntime: a non-empty ${provider} API key is required. Run \`squad config set planner\` to save one.`
|
|
5703
|
+
);
|
|
5704
|
+
}
|
|
4890
5705
|
this.languageModel = resolveModel(provider, modelId, apiKeyOrModel).model;
|
|
4891
5706
|
}
|
|
4892
5707
|
async runScout(input5) {
|
|
@@ -5339,8 +6154,8 @@ async function runScout(input5) {
|
|
|
5339
6154
|
}
|
|
5340
6155
|
|
|
5341
6156
|
// src/planner/validation.ts
|
|
5342
|
-
import
|
|
5343
|
-
import
|
|
6157
|
+
import fs30 from "fs";
|
|
6158
|
+
import path20 from "path";
|
|
5344
6159
|
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
6160
|
function summariseIssuesByKind(issues) {
|
|
5346
6161
|
const out = {};
|
|
@@ -5410,21 +6225,21 @@ function trigramSimilarity(a, b) {
|
|
|
5410
6225
|
return inter / Math.sqrt(A.size * B.size);
|
|
5411
6226
|
}
|
|
5412
6227
|
function suggestPaths(root, missingRel, max = 3) {
|
|
5413
|
-
const targetBase =
|
|
6228
|
+
const targetBase = path20.basename(missingRel).toLowerCase();
|
|
5414
6229
|
const ig = buildRepoIgnore(root);
|
|
5415
6230
|
const candidates = [];
|
|
5416
6231
|
const walk2 = (dir) => {
|
|
5417
6232
|
if (candidates.length >= 2e3) return;
|
|
5418
6233
|
let ents;
|
|
5419
6234
|
try {
|
|
5420
|
-
ents =
|
|
6235
|
+
ents = fs30.readdirSync(dir, { withFileTypes: true });
|
|
5421
6236
|
} catch {
|
|
5422
6237
|
return;
|
|
5423
6238
|
}
|
|
5424
6239
|
for (const ent of ents) {
|
|
5425
6240
|
if (candidates.length >= 2e3) return;
|
|
5426
|
-
const abs =
|
|
5427
|
-
const rel =
|
|
6241
|
+
const abs = path20.join(dir, ent.name);
|
|
6242
|
+
const rel = path20.relative(root, abs).split(path20.sep).join("/");
|
|
5428
6243
|
if (!rel || ig.ignores(rel) || ent.isDirectory() && ig.ignores(`${rel}/`)) continue;
|
|
5429
6244
|
if (ent.isDirectory()) walk2(abs);
|
|
5430
6245
|
else if (ent.isFile()) candidates.push(rel);
|
|
@@ -5433,7 +6248,7 @@ function suggestPaths(root, missingRel, max = 3) {
|
|
|
5433
6248
|
walk2(root);
|
|
5434
6249
|
const rows = [];
|
|
5435
6250
|
for (const rel of candidates) {
|
|
5436
|
-
const base =
|
|
6251
|
+
const base = path20.basename(rel).toLowerCase();
|
|
5437
6252
|
let score = 0;
|
|
5438
6253
|
if (base === targetBase) score = 1e3;
|
|
5439
6254
|
else if (base.startsWith(targetBase) || targetBase.startsWith(base)) score = 500;
|
|
@@ -5511,12 +6326,12 @@ function validatePlan(input5) {
|
|
|
5511
6326
|
const cache = /* @__PURE__ */ new Map();
|
|
5512
6327
|
const readCached = (rel) => {
|
|
5513
6328
|
if (cache.has(rel)) return cache.get(rel);
|
|
5514
|
-
const resolved =
|
|
5515
|
-
const rc =
|
|
5516
|
-
if (rc.startsWith("..") ||
|
|
5517
|
-
if (!
|
|
6329
|
+
const resolved = path20.resolve(input5.root, rel);
|
|
6330
|
+
const rc = path20.relative(input5.root, resolved);
|
|
6331
|
+
if (rc.startsWith("..") || path20.isAbsolute(rc)) return void 0;
|
|
6332
|
+
if (!fs30.existsSync(resolved) || !fs30.statSync(resolved).isFile()) return void 0;
|
|
5518
6333
|
try {
|
|
5519
|
-
const buf =
|
|
6334
|
+
const buf = fs30.readFileSync(resolved);
|
|
5520
6335
|
const isBinary = buf.includes(0);
|
|
5521
6336
|
const text = isBinary ? "" : buf.toString("utf8");
|
|
5522
6337
|
const lines = isBinary ? 0 : text.split(/\r?\n/).length;
|
|
@@ -5530,10 +6345,10 @@ function validatePlan(input5) {
|
|
|
5530
6345
|
const pathClaims = mergePathClaims(input5.planText);
|
|
5531
6346
|
for (const c of pathClaims) {
|
|
5532
6347
|
if (c.isCreate) continue;
|
|
5533
|
-
const resolved =
|
|
5534
|
-
const rel =
|
|
5535
|
-
if (rel.startsWith("..") ||
|
|
5536
|
-
if (!
|
|
6348
|
+
const resolved = path20.resolve(input5.root, c.path);
|
|
6349
|
+
const rel = path20.relative(input5.root, resolved);
|
|
6350
|
+
if (rel.startsWith("..") || path20.isAbsolute(rel)) continue;
|
|
6351
|
+
if (!fs30.existsSync(resolved)) {
|
|
5537
6352
|
const suggestions = suggestPaths(input5.root, c.path);
|
|
5538
6353
|
issues.push({
|
|
5539
6354
|
severity: "warning",
|
|
@@ -5553,8 +6368,8 @@ function validatePlan(input5) {
|
|
|
5553
6368
|
if (!Number.isFinite(lo) || !Number.isFinite(hi)) continue;
|
|
5554
6369
|
const filePath = pathNear(rangeText, rm2.index);
|
|
5555
6370
|
if (!filePath) continue;
|
|
5556
|
-
const
|
|
5557
|
-
const lines =
|
|
6371
|
+
const cached2 = readCached(filePath);
|
|
6372
|
+
const lines = cached2?.isBinary ? void 0 : cached2?.lines;
|
|
5558
6373
|
if (lines !== void 0 && hi > lines) {
|
|
5559
6374
|
issues.push({
|
|
5560
6375
|
severity: "warning",
|
|
@@ -5571,8 +6386,8 @@ function validatePlan(input5) {
|
|
|
5571
6386
|
if (!Number.isFinite(n)) continue;
|
|
5572
6387
|
const filePath = pathNear(rangeText, rm2.index);
|
|
5573
6388
|
if (!filePath) continue;
|
|
5574
|
-
const
|
|
5575
|
-
const lines =
|
|
6389
|
+
const cached2 = readCached(filePath);
|
|
6390
|
+
const lines = cached2?.isBinary ? void 0 : cached2?.lines;
|
|
5576
6391
|
if (lines !== void 0 && n > lines) {
|
|
5577
6392
|
issues.push({
|
|
5578
6393
|
severity: "warning",
|
|
@@ -5588,8 +6403,8 @@ function validatePlan(input5) {
|
|
|
5588
6403
|
const fp = rm2[1].trim().replace(/^\.\//, "");
|
|
5589
6404
|
const n = parseInt(rm2[2], 10);
|
|
5590
6405
|
if (!Number.isFinite(n)) continue;
|
|
5591
|
-
const
|
|
5592
|
-
const lines =
|
|
6406
|
+
const cached2 = readCached(fp);
|
|
6407
|
+
const lines = cached2?.isBinary ? void 0 : cached2?.lines;
|
|
5593
6408
|
if (lines !== void 0 && n > lines) {
|
|
5594
6409
|
issues.push({
|
|
5595
6410
|
severity: "warning",
|
|
@@ -5607,8 +6422,8 @@ function validatePlan(input5) {
|
|
|
5607
6422
|
const pathNearCell = near.match(/`([^`\n/]+\/[^`\n]+?\.[a-z0-9]{1,6})`/i);
|
|
5608
6423
|
if (!pathNearCell) continue;
|
|
5609
6424
|
const fp = pathNearCell[1].replace(/^\.\//, "");
|
|
5610
|
-
const
|
|
5611
|
-
if (!
|
|
6425
|
+
const cached2 = readCached(fp);
|
|
6426
|
+
if (!cached2 || cached2.isBinary) continue;
|
|
5612
6427
|
const symRe = /`([A-Za-z_][A-Za-z0-9_]{2,50})`/g;
|
|
5613
6428
|
let sm;
|
|
5614
6429
|
while ((sm = symRe.exec(line2)) !== null) {
|
|
@@ -5618,8 +6433,8 @@ function validatePlan(input5) {
|
|
|
5618
6433
|
)) {
|
|
5619
6434
|
continue;
|
|
5620
6435
|
}
|
|
5621
|
-
if (!
|
|
5622
|
-
const suggestions = suggestSymbols(
|
|
6436
|
+
if (!cached2.text.includes(sym)) {
|
|
6437
|
+
const suggestions = suggestSymbols(cached2.text, sym);
|
|
5623
6438
|
issues.push({
|
|
5624
6439
|
severity: "warning",
|
|
5625
6440
|
kind: "symbol_not_found",
|
|
@@ -5700,9 +6515,9 @@ ${result.content}
|
|
|
5700
6515
|
}
|
|
5701
6516
|
continue;
|
|
5702
6517
|
}
|
|
5703
|
-
const resolved =
|
|
5704
|
-
const rc =
|
|
5705
|
-
if (rc.startsWith("..") ||
|
|
6518
|
+
const resolved = path21.resolve(root, cleaned);
|
|
6519
|
+
const rc = path21.relative(root, resolved);
|
|
6520
|
+
if (rc.startsWith("..") || path21.isAbsolute(rc)) {
|
|
5706
6521
|
bus.emit({
|
|
5707
6522
|
kind: "validation_issue",
|
|
5708
6523
|
runId,
|
|
@@ -5713,7 +6528,7 @@ ${result.content}
|
|
|
5713
6528
|
});
|
|
5714
6529
|
continue;
|
|
5715
6530
|
}
|
|
5716
|
-
if (!
|
|
6531
|
+
if (!fs31.existsSync(resolved)) {
|
|
5717
6532
|
bus.emit({
|
|
5718
6533
|
kind: "validation_issue",
|
|
5719
6534
|
runId,
|
|
@@ -5724,7 +6539,7 @@ ${result.content}
|
|
|
5724
6539
|
});
|
|
5725
6540
|
continue;
|
|
5726
6541
|
}
|
|
5727
|
-
const st =
|
|
6542
|
+
const st = fs31.statSync(resolved);
|
|
5728
6543
|
if (!st.isFile()) continue;
|
|
5729
6544
|
const n = Math.min(st.size, MAX_BYTES_PER_FULL_READ);
|
|
5730
6545
|
const cap = budget.canRead(n);
|
|
@@ -5738,7 +6553,7 @@ _(skipped: ${cap.reason})_
|
|
|
5738
6553
|
}
|
|
5739
6554
|
let buf;
|
|
5740
6555
|
try {
|
|
5741
|
-
buf =
|
|
6556
|
+
buf = fs31.readFileSync(resolved);
|
|
5742
6557
|
} catch {
|
|
5743
6558
|
continue;
|
|
5744
6559
|
}
|
|
@@ -5931,6 +6746,9 @@ async function runPlanner(input5) {
|
|
|
5931
6746
|
}
|
|
5932
6747
|
} : void 0
|
|
5933
6748
|
});
|
|
6749
|
+
if (input5.auth) {
|
|
6750
|
+
bus.emit({ kind: "auth_info", runId, ...describeAuth(input5.auth) });
|
|
6751
|
+
}
|
|
5934
6752
|
const merged = { ...input5, events: bus, runId };
|
|
5935
6753
|
const systemPrompt = input5.systemPrompt;
|
|
5936
6754
|
let scoutSummary;
|
|
@@ -6071,8 +6889,16 @@ async function runPlanner(input5) {
|
|
|
6071
6889
|
}
|
|
6072
6890
|
|
|
6073
6891
|
// src/planner/planner-limit-messages.ts
|
|
6074
|
-
function printPlannerApiCostNotice() {
|
|
6892
|
+
function printPlannerApiCostNotice(authMode = "api-key") {
|
|
6075
6893
|
blank();
|
|
6894
|
+
if (authMode === "subscription") {
|
|
6895
|
+
step("Usage");
|
|
6896
|
+
info(
|
|
6897
|
+
"`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."
|
|
6898
|
+
);
|
|
6899
|
+
blank();
|
|
6900
|
+
return;
|
|
6901
|
+
}
|
|
6076
6902
|
step("Billing");
|
|
6077
6903
|
info(
|
|
6078
6904
|
"`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 +6921,12 @@ function tokenSummary(ctx) {
|
|
|
6095
6921
|
const snap = ctx.budgetSnapshot;
|
|
6096
6922
|
return `Tokens so far: ${snap.usage.inputTokens} in / ${snap.usage.outputTokens} out for this session.`;
|
|
6097
6923
|
}
|
|
6098
|
-
function kindDetail(ctx) {
|
|
6924
|
+
function kindDetail(ctx, authMode) {
|
|
6099
6925
|
switch (ctx.kind) {
|
|
6100
6926
|
case "max_output_tokens":
|
|
6101
6927
|
return [
|
|
6102
6928
|
`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).",
|
|
6929
|
+
"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
6930
|
tokenSummary(ctx)
|
|
6105
6931
|
];
|
|
6106
6932
|
case "max_iterations":
|
|
@@ -6123,9 +6949,9 @@ function kindDetail(ctx) {
|
|
|
6123
6949
|
];
|
|
6124
6950
|
}
|
|
6125
6951
|
}
|
|
6126
|
-
function printPlannerLimitExplanation(ctx) {
|
|
6952
|
+
function printPlannerLimitExplanation(ctx, authMode = "api-key") {
|
|
6127
6953
|
warning(kindTitle(ctx.kind));
|
|
6128
|
-
for (const line2 of kindDetail(ctx)) {
|
|
6954
|
+
for (const line2 of kindDetail(ctx, authMode)) {
|
|
6129
6955
|
info(line2);
|
|
6130
6956
|
}
|
|
6131
6957
|
kv("session so far", `${snapSummary(ctx)}`, 18);
|
|
@@ -6176,6 +7002,23 @@ function thinkingConfigFromProviderSpecific(ps) {
|
|
|
6176
7002
|
}
|
|
6177
7003
|
|
|
6178
7004
|
// src/planner/runtimes/agent-sdk-runtime.ts
|
|
7005
|
+
var ACCOUNT_INFO_TIMEOUT_MS = 2e3;
|
|
7006
|
+
var MAX_RATE_LIMIT_RETRY_SEC2 = 90;
|
|
7007
|
+
var enrichedAuthInfoRuns = /* @__PURE__ */ new WeakMap();
|
|
7008
|
+
function markAuthInfoEmitted(bus, runId) {
|
|
7009
|
+
let seen = enrichedAuthInfoRuns.get(bus);
|
|
7010
|
+
if (!seen) {
|
|
7011
|
+
seen = /* @__PURE__ */ new Set();
|
|
7012
|
+
enrichedAuthInfoRuns.set(bus, seen);
|
|
7013
|
+
}
|
|
7014
|
+
if (seen.has(runId)) return false;
|
|
7015
|
+
seen.add(runId);
|
|
7016
|
+
return true;
|
|
7017
|
+
}
|
|
7018
|
+
var API_KEY_SOURCES = ["user", "project", "org", "temporary", "oauth"];
|
|
7019
|
+
function asApiKeySource(v) {
|
|
7020
|
+
return API_KEY_SOURCES.find((s) => s === v);
|
|
7021
|
+
}
|
|
6179
7022
|
var BUILTIN_TOOL_DENY = [
|
|
6180
7023
|
"Read",
|
|
6181
7024
|
"Write",
|
|
@@ -6347,15 +7190,96 @@ function handleDecodedStreamEvent(decoded, input5) {
|
|
|
6347
7190
|
}
|
|
6348
7191
|
}
|
|
6349
7192
|
}
|
|
7193
|
+
async function readSdkAccountInfo(q) {
|
|
7194
|
+
const fn = q?.accountInfo;
|
|
7195
|
+
if (typeof fn !== "function") return void 0;
|
|
7196
|
+
let timer;
|
|
7197
|
+
try {
|
|
7198
|
+
const info2 = await Promise.race([
|
|
7199
|
+
fn.call(q),
|
|
7200
|
+
new Promise((resolve2) => {
|
|
7201
|
+
timer = setTimeout(() => resolve2(void 0), ACCOUNT_INFO_TIMEOUT_MS);
|
|
7202
|
+
})
|
|
7203
|
+
]);
|
|
7204
|
+
if (!info2 || typeof info2 !== "object") return void 0;
|
|
7205
|
+
const o = info2;
|
|
7206
|
+
const out = {};
|
|
7207
|
+
if (typeof o.email === "string") out.email = o.email;
|
|
7208
|
+
if (typeof o.organization === "string") out.organization = o.organization;
|
|
7209
|
+
if (typeof o.subscriptionType === "string") out.subscriptionType = o.subscriptionType;
|
|
7210
|
+
return Object.keys(out).length > 0 ? out : void 0;
|
|
7211
|
+
} catch {
|
|
7212
|
+
return void 0;
|
|
7213
|
+
} finally {
|
|
7214
|
+
if (timer) clearTimeout(timer);
|
|
7215
|
+
}
|
|
7216
|
+
}
|
|
6350
7217
|
var AgentSdkRuntime = class {
|
|
6351
|
-
constructor(modelId,
|
|
7218
|
+
constructor(modelId, auth2) {
|
|
6352
7219
|
this.modelId = modelId;
|
|
6353
|
-
this.
|
|
7220
|
+
this.auth = auth2;
|
|
6354
7221
|
}
|
|
6355
7222
|
modelId;
|
|
6356
|
-
|
|
7223
|
+
auth;
|
|
6357
7224
|
kind = "agent-sdk";
|
|
6358
7225
|
providerName = "anthropic";
|
|
7226
|
+
/** Safe-to-log auth projection — never carries the key or the token. */
|
|
7227
|
+
authDescriptor() {
|
|
7228
|
+
return describeAuth(this.auth);
|
|
7229
|
+
}
|
|
7230
|
+
/**
|
|
7231
|
+
* Handle a `system` message. On `init` this emits the enriched `auth_info` (once per run, even
|
|
7232
|
+
* though scout and draft are separate instances); on `api_retry` it forwards a rate limit through
|
|
7233
|
+
* the existing 0.8.0 rate-limit channel so the console countdown and CLI spinner keep working.
|
|
7234
|
+
*/
|
|
7235
|
+
async handleSystemMessage(m, input5, ctx) {
|
|
7236
|
+
if (m.subtype === "init") {
|
|
7237
|
+
const apiKeySource = asApiKeySource(m.apiKeySource);
|
|
7238
|
+
if (!apiKeySource || !markAuthInfoEmitted(input5.bus, input5.runId)) return;
|
|
7239
|
+
const account = ctx.withAccountInfo && this.auth.mode === "subscription" ? await readSdkAccountInfo(ctx.query) : void 0;
|
|
7240
|
+
input5.bus.emit({
|
|
7241
|
+
kind: "auth_info",
|
|
7242
|
+
runId: input5.runId,
|
|
7243
|
+
...this.authDescriptor(),
|
|
7244
|
+
apiKeySource,
|
|
7245
|
+
...account ? { account } : {}
|
|
7246
|
+
});
|
|
7247
|
+
return;
|
|
7248
|
+
}
|
|
7249
|
+
if (m.subtype === "api_retry" && detectAuthShapedSdkError(m.error) === "rate_limit") {
|
|
7250
|
+
const delayMs = typeof m.retry_delay_ms === "number" ? m.retry_delay_ms : 0;
|
|
7251
|
+
const waitSec = Math.max(1, Math.ceil(delayMs / 1e3));
|
|
7252
|
+
input5.bus.emit({
|
|
7253
|
+
kind: "rate_limit",
|
|
7254
|
+
runId: input5.runId,
|
|
7255
|
+
turn: ctx.turn,
|
|
7256
|
+
retryAfterSec: waitSec,
|
|
7257
|
+
waitSec,
|
|
7258
|
+
capSec: MAX_RATE_LIMIT_RETRY_SEC2,
|
|
7259
|
+
phase: "retrying",
|
|
7260
|
+
provider: "anthropic"
|
|
7261
|
+
});
|
|
7262
|
+
}
|
|
7263
|
+
}
|
|
7264
|
+
/**
|
|
7265
|
+
* Turn an auth-shaped SDK failure into an actionable error. Rate limits also emit the terminal
|
|
7266
|
+
* `rate_limit` event so the UI closes its countdown before the throw.
|
|
7267
|
+
*/
|
|
7268
|
+
authError(signal, input5, turn) {
|
|
7269
|
+
if (signal === "rate_limit") {
|
|
7270
|
+
input5.bus.emit({
|
|
7271
|
+
kind: "rate_limit",
|
|
7272
|
+
runId: input5.runId,
|
|
7273
|
+
turn,
|
|
7274
|
+
retryAfterSec: void 0,
|
|
7275
|
+
waitSec: 0,
|
|
7276
|
+
capSec: MAX_RATE_LIMIT_RETRY_SEC2,
|
|
7277
|
+
phase: "aborted",
|
|
7278
|
+
provider: "anthropic"
|
|
7279
|
+
});
|
|
7280
|
+
}
|
|
7281
|
+
return new Error(authErrorMessage(signal, authErrorContextFrom(this.auth)));
|
|
7282
|
+
}
|
|
6359
7283
|
async runDraft(input5) {
|
|
6360
7284
|
const { query, createSdkMcpServer, tool: tool5 } = await import("@anthropic-ai/claude-agent-sdk");
|
|
6361
7285
|
const sdkTools = input5.tools.map(
|
|
@@ -6390,7 +7314,7 @@ var AgentSdkRuntime = class {
|
|
|
6390
7314
|
persistSession: false,
|
|
6391
7315
|
settingSources: [],
|
|
6392
7316
|
abortController: ac,
|
|
6393
|
-
env:
|
|
7317
|
+
env: buildSdkEnv(this.auth),
|
|
6394
7318
|
thinking,
|
|
6395
7319
|
effort
|
|
6396
7320
|
};
|
|
@@ -6400,6 +7324,7 @@ var AgentSdkRuntime = class {
|
|
|
6400
7324
|
let finishedNormally = false;
|
|
6401
7325
|
let incompleteKind;
|
|
6402
7326
|
let userCancelled = false;
|
|
7327
|
+
let authSignal;
|
|
6403
7328
|
const think = initialThinkingState();
|
|
6404
7329
|
try {
|
|
6405
7330
|
const q = query({
|
|
@@ -6421,6 +7346,7 @@ var AgentSdkRuntime = class {
|
|
|
6421
7346
|
break;
|
|
6422
7347
|
}
|
|
6423
7348
|
case "assistant": {
|
|
7349
|
+
authSignal = detectAuthShapedSdkError(m.error) ?? authSignal;
|
|
6424
7350
|
think.blockIndexCounter = 0;
|
|
6425
7351
|
think.inThinkingBlock = false;
|
|
6426
7352
|
think.thinkingBlockIndex = -1;
|
|
@@ -6447,10 +7373,17 @@ var AgentSdkRuntime = class {
|
|
|
6447
7373
|
finishedNormally = false;
|
|
6448
7374
|
} else if (typeof sub === "string" && sub.startsWith("error")) {
|
|
6449
7375
|
finishedNormally = false;
|
|
7376
|
+
const signal = authSignal ?? detectAuthShapedSdkError(m.errors);
|
|
7377
|
+
if (signal) throw this.authError(signal, input5, assistantTurns);
|
|
6450
7378
|
}
|
|
6451
7379
|
break;
|
|
6452
7380
|
}
|
|
6453
7381
|
case "system":
|
|
7382
|
+
await this.handleSystemMessage(m, input5, {
|
|
7383
|
+
query: q,
|
|
7384
|
+
withAccountInfo: true,
|
|
7385
|
+
turn: assistantTurns
|
|
7386
|
+
});
|
|
6454
7387
|
break;
|
|
6455
7388
|
default:
|
|
6456
7389
|
break;
|
|
@@ -6509,12 +7442,13 @@ var AgentSdkRuntime = class {
|
|
|
6509
7442
|
includePartialMessages: true,
|
|
6510
7443
|
persistSession: false,
|
|
6511
7444
|
settingSources: [],
|
|
6512
|
-
env:
|
|
7445
|
+
env: buildSdkEnv(this.auth),
|
|
6513
7446
|
thinking,
|
|
6514
7447
|
effort
|
|
6515
7448
|
};
|
|
6516
7449
|
let usage = { inputTokens: 0, outputTokens: 0 };
|
|
6517
7450
|
let assistantTurns = 0;
|
|
7451
|
+
let authSignal;
|
|
6518
7452
|
const think = initialThinkingState();
|
|
6519
7453
|
const ac = new AbortController();
|
|
6520
7454
|
const onAbort = () => ac.abort();
|
|
@@ -6539,6 +7473,7 @@ var AgentSdkRuntime = class {
|
|
|
6539
7473
|
});
|
|
6540
7474
|
break;
|
|
6541
7475
|
case "assistant":
|
|
7476
|
+
authSignal = detectAuthShapedSdkError(m.error) ?? authSignal;
|
|
6542
7477
|
think.blockIndexCounter = 0;
|
|
6543
7478
|
think.inThinkingBlock = false;
|
|
6544
7479
|
think.thinkingBlockIndex = -1;
|
|
@@ -6549,8 +7484,17 @@ var AgentSdkRuntime = class {
|
|
|
6549
7484
|
usage = usageFromAgentSdkResult(
|
|
6550
7485
|
m.usage
|
|
6551
7486
|
);
|
|
7487
|
+
if (typeof m.subtype === "string" && m.subtype.startsWith("error")) {
|
|
7488
|
+
const signal = authSignal ?? detectAuthShapedSdkError(m.errors);
|
|
7489
|
+
if (signal) throw this.authError(signal, input5, assistantTurns);
|
|
7490
|
+
}
|
|
6552
7491
|
break;
|
|
6553
7492
|
case "system":
|
|
7493
|
+
await this.handleSystemMessage(m, input5, {
|
|
7494
|
+
query: q,
|
|
7495
|
+
withAccountInfo: false,
|
|
7496
|
+
turn: assistantTurns
|
|
7497
|
+
});
|
|
6554
7498
|
break;
|
|
6555
7499
|
default:
|
|
6556
7500
|
break;
|
|
@@ -6572,14 +7516,40 @@ var AgentSdkRuntime = class {
|
|
|
6572
7516
|
}
|
|
6573
7517
|
};
|
|
6574
7518
|
|
|
7519
|
+
// src/planner/runtimes/auth-runtime-mismatch.ts
|
|
7520
|
+
var SUBSCRIPTION_NEEDS_AGENT_SDK_LEAD = "Subscription auth needs the Claude Agent SDK runtime.";
|
|
7521
|
+
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.";
|
|
7522
|
+
function subscriptionProviderMismatchHint(provider) {
|
|
7523
|
+
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.`;
|
|
7524
|
+
}
|
|
7525
|
+
function plannerAuthRuntimeMismatchMessage(provider) {
|
|
7526
|
+
return `${SUBSCRIPTION_NEEDS_AGENT_SDK_LEAD} ${provider === "anthropic" ? SUBSCRIPTION_VERCEL_RUNTIME_HINT : subscriptionProviderMismatchHint(provider)}`;
|
|
7527
|
+
}
|
|
7528
|
+
|
|
6575
7529
|
// src/planner/runtimes/index.ts
|
|
7530
|
+
var PlannerAuthRuntimeMismatchError = class extends Error {
|
|
7531
|
+
constructor(provider, runtimeChoice) {
|
|
7532
|
+
super(plannerAuthRuntimeMismatchMessage(provider));
|
|
7533
|
+
this.provider = provider;
|
|
7534
|
+
this.runtimeChoice = runtimeChoice;
|
|
7535
|
+
this.name = "PlannerAuthRuntimeMismatchError";
|
|
7536
|
+
}
|
|
7537
|
+
provider;
|
|
7538
|
+
runtimeChoice;
|
|
7539
|
+
};
|
|
6576
7540
|
function resolveRuntime(input5) {
|
|
6577
|
-
|
|
6578
|
-
|
|
6579
|
-
|
|
6580
|
-
|
|
7541
|
+
const choice = input5.provider === "anthropic" ? input5.anthropicRuntime ?? "agent-sdk" : "vercel";
|
|
7542
|
+
const auth2 = input5.auth;
|
|
7543
|
+
if (auth2.mode === "subscription") {
|
|
7544
|
+
if (input5.provider !== "anthropic" || choice !== "agent-sdk") {
|
|
7545
|
+
throw new PlannerAuthRuntimeMismatchError(input5.provider, choice);
|
|
7546
|
+
}
|
|
7547
|
+
return new AgentSdkRuntime(input5.modelId, auth2);
|
|
7548
|
+
}
|
|
7549
|
+
if (input5.provider === "anthropic" && choice === "agent-sdk") {
|
|
7550
|
+
return new AgentSdkRuntime(input5.modelId, auth2);
|
|
6581
7551
|
}
|
|
6582
|
-
return new VercelRuntime(input5.provider, input5.modelId,
|
|
7552
|
+
return new VercelRuntime(input5.provider, input5.modelId, auth2.key);
|
|
6583
7553
|
}
|
|
6584
7554
|
|
|
6585
7555
|
// src/planner/budget.ts
|
|
@@ -6651,8 +7621,8 @@ var Budget = class {
|
|
|
6651
7621
|
};
|
|
6652
7622
|
|
|
6653
7623
|
// src/planner/writer.ts
|
|
6654
|
-
import
|
|
6655
|
-
import
|
|
7624
|
+
import fs32 from "fs";
|
|
7625
|
+
import path22 from "path";
|
|
6656
7626
|
function planFileSlugParts(config2, story) {
|
|
6657
7627
|
const baseSlug = slugify(story.id);
|
|
6658
7628
|
const trackerId = config2.naming.includeTrackerId && config2.tracker.type !== "none" ? trackerIdForFilename(config2.tracker.type, story.id) : "";
|
|
@@ -6661,8 +7631,8 @@ function planFileSlugParts(config2, story) {
|
|
|
6661
7631
|
}
|
|
6662
7632
|
function writePlanFile(input5) {
|
|
6663
7633
|
const { paths, config: config2, story } = input5;
|
|
6664
|
-
const featurePlanDir =
|
|
6665
|
-
|
|
7634
|
+
const featurePlanDir = path22.join(paths.plansDir, story.feature);
|
|
7635
|
+
fs32.mkdirSync(featurePlanDir, { recursive: true });
|
|
6666
7636
|
if (input5.partial) {
|
|
6667
7637
|
return writePartialPlanFile(input5, featurePlanDir, paths, config2, story);
|
|
6668
7638
|
}
|
|
@@ -6677,14 +7647,14 @@ function writePlanFile(input5) {
|
|
|
6677
7647
|
} else {
|
|
6678
7648
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
6679
7649
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
6680
|
-
targetPath =
|
|
7650
|
+
targetPath = path22.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.md`);
|
|
6681
7651
|
}
|
|
6682
7652
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
6683
7653
|
|
|
6684
7654
|
${input5.planBodyMarkdown.trimStart()}
|
|
6685
7655
|
`;
|
|
6686
|
-
|
|
6687
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
7656
|
+
fs32.writeFileSync(targetPath, body, "utf8");
|
|
7657
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path22.basename(targetPath), story, false);
|
|
6688
7658
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
6689
7659
|
}
|
|
6690
7660
|
function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
@@ -6699,42 +7669,42 @@ function writePartialPlanFile(input5, featurePlanDir, paths, config2, story) {
|
|
|
6699
7669
|
} else {
|
|
6700
7670
|
nn = scanPlans(paths.plansDir).nextGlobal;
|
|
6701
7671
|
const { baseSlug, trackerPart } = planFileSlugParts(config2, story);
|
|
6702
|
-
targetPath =
|
|
7672
|
+
targetPath = path22.join(featurePlanDir, `${formatSequence(nn)}-story-${baseSlug}${trackerPart}.partial.md`);
|
|
6703
7673
|
}
|
|
6704
7674
|
const body = `${input5.metadataHeader.trimEnd()}
|
|
6705
7675
|
|
|
6706
7676
|
${input5.planBodyMarkdown.trimStart()}
|
|
6707
7677
|
`;
|
|
6708
|
-
|
|
6709
|
-
upsertOverviewRow(featurePlanDir, story.feature, nn,
|
|
7678
|
+
fs32.writeFileSync(targetPath, body, "utf8");
|
|
7679
|
+
upsertOverviewRow(featurePlanDir, story.feature, nn, path22.basename(targetPath), story, true);
|
|
6710
7680
|
return { planFile: targetPath, sequenceNumber: nn, overwrote };
|
|
6711
7681
|
}
|
|
6712
7682
|
function findExistingPlan(featureDir, storyId) {
|
|
6713
|
-
if (!
|
|
7683
|
+
if (!fs32.existsSync(featureDir)) return void 0;
|
|
6714
7684
|
const pattern = /^(\d{2,})-story-.+\.md$/;
|
|
6715
|
-
for (const entry of
|
|
7685
|
+
for (const entry of fs32.readdirSync(featureDir)) {
|
|
6716
7686
|
if (entry.endsWith(".partial.md")) continue;
|
|
6717
7687
|
const m = entry.match(pattern);
|
|
6718
7688
|
if (!m) continue;
|
|
6719
|
-
if (entry.includes(storyId)) return { absPath:
|
|
7689
|
+
if (entry.includes(storyId)) return { absPath: path22.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
6720
7690
|
}
|
|
6721
7691
|
return void 0;
|
|
6722
7692
|
}
|
|
6723
7693
|
function findExistingPartialPlan(featureDir, storyId) {
|
|
6724
|
-
if (!
|
|
7694
|
+
if (!fs32.existsSync(featureDir)) return void 0;
|
|
6725
7695
|
const pattern = /^(\d{2,})-story-.+\.partial\.md$/;
|
|
6726
|
-
for (const entry of
|
|
7696
|
+
for (const entry of fs32.readdirSync(featureDir)) {
|
|
6727
7697
|
const m = entry.match(pattern);
|
|
6728
7698
|
if (!m) continue;
|
|
6729
|
-
if (entry.includes(storyId)) return { absPath:
|
|
7699
|
+
if (entry.includes(storyId)) return { absPath: path22.join(featureDir, entry), nn: parseInt(m[1], 10) };
|
|
6730
7700
|
}
|
|
6731
7701
|
return void 0;
|
|
6732
7702
|
}
|
|
6733
7703
|
function upsertOverviewRow(featureDir, feature, nn, filename, story, isPartial) {
|
|
6734
|
-
const overviewPath =
|
|
7704
|
+
const overviewPath = path22.join(featureDir, "00-overview.md");
|
|
6735
7705
|
const titleCell = isPartial ? `${story.titleHint ?? story.id} (partial)` : story.titleHint ?? story.id;
|
|
6736
7706
|
const row = `| ${formatSequence(nn)} | \`${filename}\` | ${titleCell} | ${story.id} | \u2014 |`;
|
|
6737
|
-
if (!
|
|
7707
|
+
if (!fs32.existsSync(overviewPath)) {
|
|
6738
7708
|
const content = `# ${feature} \u2014 plan overview
|
|
6739
7709
|
|
|
6740
7710
|
Entry point for the **${feature}** feature. Stories execute in order by their \`NN\` prefix.
|
|
@@ -6745,10 +7715,10 @@ Entry point for the **${feature}** feature. Stories execute in order by their \`
|
|
|
6745
7715
|
|----|------|-------|------------|------------|
|
|
6746
7716
|
${row}
|
|
6747
7717
|
`;
|
|
6748
|
-
|
|
7718
|
+
fs32.writeFileSync(overviewPath, content, "utf8");
|
|
6749
7719
|
return;
|
|
6750
7720
|
}
|
|
6751
|
-
const current =
|
|
7721
|
+
const current = fs32.readFileSync(overviewPath, "utf8");
|
|
6752
7722
|
const lines = current.split("\n");
|
|
6753
7723
|
const existingIdx = lines.findIndex((l) => l.includes(`\`${filename}\``) || l.includes(`| ${formatSequence(nn)} |`));
|
|
6754
7724
|
if (existingIdx >= 0) {
|
|
@@ -6757,7 +7727,7 @@ ${row}
|
|
|
6757
7727
|
const tableEnd = findTableEnd(lines);
|
|
6758
7728
|
lines.splice(tableEnd, 0, row);
|
|
6759
7729
|
}
|
|
6760
|
-
|
|
7730
|
+
fs32.writeFileSync(overviewPath, lines.join("\n"), "utf8");
|
|
6761
7731
|
}
|
|
6762
7732
|
function findTableEnd(lines) {
|
|
6763
7733
|
let inTable = false;
|
|
@@ -6810,16 +7780,16 @@ function buildCopyPlanPromptMarkdown(config2, intakeContent) {
|
|
|
6810
7780
|
}
|
|
6811
7781
|
|
|
6812
7782
|
// src/core/last-run.ts
|
|
6813
|
-
import
|
|
6814
|
-
import
|
|
7783
|
+
import fs33 from "fs/promises";
|
|
7784
|
+
import path23 from "path";
|
|
6815
7785
|
async function writeLastRun(paths, rec) {
|
|
6816
|
-
const file =
|
|
6817
|
-
await
|
|
7786
|
+
const file = path23.join(paths.squadDir, ".last-run.json");
|
|
7787
|
+
await fs33.writeFile(file, JSON.stringify({ ...rec, version: 1 }, null, 2) + "\n", "utf8");
|
|
6818
7788
|
}
|
|
6819
7789
|
async function readLastRun(paths) {
|
|
6820
|
-
const file =
|
|
7790
|
+
const file = path23.join(paths.squadDir, ".last-run.json");
|
|
6821
7791
|
try {
|
|
6822
|
-
const raw = await
|
|
7792
|
+
const raw = await fs33.readFile(file, "utf8");
|
|
6823
7793
|
const parsed = JSON.parse(raw);
|
|
6824
7794
|
if (parsed.version !== 1) return void 0;
|
|
6825
7795
|
return parsed;
|
|
@@ -6852,8 +7822,8 @@ function formatPlannerCacheLine(opts) {
|
|
|
6852
7822
|
}
|
|
6853
7823
|
|
|
6854
7824
|
// src/core/plans-index.ts
|
|
6855
|
-
import
|
|
6856
|
-
import
|
|
7825
|
+
import fs34 from "fs";
|
|
7826
|
+
import path24 from "path";
|
|
6857
7827
|
var MAX_ENTRIES = 30;
|
|
6858
7828
|
function firstMarkdownH1(src) {
|
|
6859
7829
|
const lines = src.split(/\r?\n/);
|
|
@@ -6868,12 +7838,12 @@ function collectPlanMarkdownFiles(plansDir) {
|
|
|
6868
7838
|
const walk2 = (dir) => {
|
|
6869
7839
|
let ents;
|
|
6870
7840
|
try {
|
|
6871
|
-
ents =
|
|
7841
|
+
ents = fs34.readdirSync(dir, { withFileTypes: true });
|
|
6872
7842
|
} catch {
|
|
6873
7843
|
return;
|
|
6874
7844
|
}
|
|
6875
7845
|
for (const ent of ents) {
|
|
6876
|
-
const abs =
|
|
7846
|
+
const abs = path24.join(dir, ent.name);
|
|
6877
7847
|
if (ent.isDirectory()) {
|
|
6878
7848
|
walk2(abs);
|
|
6879
7849
|
} else if (ent.isFile() && ent.name.endsWith(".md")) {
|
|
@@ -6882,7 +7852,7 @@ function collectPlanMarkdownFiles(plansDir) {
|
|
|
6882
7852
|
}
|
|
6883
7853
|
}
|
|
6884
7854
|
};
|
|
6885
|
-
if (
|
|
7855
|
+
if (fs34.existsSync(plansDir)) walk2(plansDir);
|
|
6886
7856
|
out.sort((a, b) => a.localeCompare(b));
|
|
6887
7857
|
return out.slice(0, MAX_ENTRIES);
|
|
6888
7858
|
}
|
|
@@ -6891,13 +7861,13 @@ function buildPlansIndex(paths) {
|
|
|
6891
7861
|
if (files.length === 0) return "(no prior plans yet)";
|
|
6892
7862
|
const bullets = [];
|
|
6893
7863
|
for (const abs of files) {
|
|
6894
|
-
const rel =
|
|
7864
|
+
const rel = path24.relative(paths.root, abs).split(path24.sep).join("/");
|
|
6895
7865
|
let title = "";
|
|
6896
7866
|
try {
|
|
6897
|
-
const raw =
|
|
6898
|
-
title = firstMarkdownH1(raw) ??
|
|
7867
|
+
const raw = fs34.readFileSync(abs, "utf8");
|
|
7868
|
+
title = firstMarkdownH1(raw) ?? path24.basename(abs);
|
|
6899
7869
|
} catch {
|
|
6900
|
-
title =
|
|
7870
|
+
title = path24.basename(abs);
|
|
6901
7871
|
}
|
|
6902
7872
|
bullets.push(`- ${rel} \u2014 "${title}"`);
|
|
6903
7873
|
}
|
|
@@ -7157,9 +8127,10 @@ function decideMode(opts, config2) {
|
|
|
7157
8127
|
}
|
|
7158
8128
|
if (opts.api) return "api";
|
|
7159
8129
|
if (opts.copy) return "copy";
|
|
7160
|
-
const
|
|
7161
|
-
|
|
7162
|
-
|
|
8130
|
+
const planner = config2.planner;
|
|
8131
|
+
if (!planner?.enabled || !planner.provider) return "copy";
|
|
8132
|
+
if (planner.provider === "anthropic" && planner.auth?.anthropic === "subscription") return "api";
|
|
8133
|
+
return readProviderKey(planner.provider) ? "api" : "copy";
|
|
7163
8134
|
}
|
|
7164
8135
|
async function runNewPlan(intakePath, opts) {
|
|
7165
8136
|
const root = requireSquadRoot();
|
|
@@ -7170,7 +8141,7 @@ async function runNewPlan(intakePath, opts) {
|
|
|
7170
8141
|
const story = intakePath ? resolveFromPath(intakePath, stories, root) : await pickStory(stories, { all: !!opts.all, interactive, feature: opts.feature });
|
|
7171
8142
|
if (!story) return;
|
|
7172
8143
|
if (story.planFile) {
|
|
7173
|
-
const proceed = await
|
|
8144
|
+
const proceed = await confirmOverwrite2(story, interactive, !!opts.yes);
|
|
7174
8145
|
if (!proceed) return;
|
|
7175
8146
|
}
|
|
7176
8147
|
const mode = decideMode(opts, config2);
|
|
@@ -7181,8 +8152,8 @@ async function runNewPlan(intakePath, opts) {
|
|
|
7181
8152
|
}
|
|
7182
8153
|
}
|
|
7183
8154
|
function resolveFromPath(intakePath, stories, root) {
|
|
7184
|
-
const resolved =
|
|
7185
|
-
if (!
|
|
8155
|
+
const resolved = path25.resolve(intakePath.trim());
|
|
8156
|
+
if (!fs35.existsSync(resolved)) {
|
|
7186
8157
|
throw new Error(
|
|
7187
8158
|
`Intake not found: ${resolved}. Run \`squad list\` to see intakes, or \`squad new-story\` to create one first.`
|
|
7188
8159
|
);
|
|
@@ -7194,7 +8165,7 @@ function resolveFromPath(intakePath, stories, root) {
|
|
|
7194
8165
|
);
|
|
7195
8166
|
}
|
|
7196
8167
|
function relativeStoriesDir(root) {
|
|
7197
|
-
return
|
|
8168
|
+
return path25.relative(root, path25.join(root, ".squad", "stories"));
|
|
7198
8169
|
}
|
|
7199
8170
|
async function pickStory(stories, opts) {
|
|
7200
8171
|
const candidates = opts.all ? stories : stories.filter((s) => !s.planFile);
|
|
@@ -7225,7 +8196,7 @@ async function pickStory(stories, opts) {
|
|
|
7225
8196
|
value: s.intakePath
|
|
7226
8197
|
}))
|
|
7227
8198
|
});
|
|
7228
|
-
return candidates.find((s) =>
|
|
8199
|
+
return candidates.find((s) => path25.resolve(s.intakePath) === path25.resolve(pick));
|
|
7229
8200
|
}
|
|
7230
8201
|
function formatLabel(s) {
|
|
7231
8202
|
const head = `${s.feature} / ${s.id}`;
|
|
@@ -7233,7 +8204,7 @@ function formatLabel(s) {
|
|
|
7233
8204
|
const plannedTag = s.planFile ? ` ${theme_exports.warn("(planned)")}` : "";
|
|
7234
8205
|
return head + tail + plannedTag;
|
|
7235
8206
|
}
|
|
7236
|
-
async function
|
|
8207
|
+
async function confirmOverwrite2(story, interactive, yes) {
|
|
7237
8208
|
warning(`A plan already exists for this intake:`);
|
|
7238
8209
|
kv("plan", `.squad/plans/${story.feature}/${story.planFile}`);
|
|
7239
8210
|
info("Regenerating will replace it when your planner writes the new version.");
|
|
@@ -7243,7 +8214,7 @@ async function confirmOverwrite(story, interactive, yes) {
|
|
|
7243
8214
|
`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
8215
|
);
|
|
7245
8216
|
}
|
|
7246
|
-
const go = await
|
|
8217
|
+
const go = await confirm7({ message: "Proceed and regenerate?", default: false });
|
|
7247
8218
|
return go;
|
|
7248
8219
|
}
|
|
7249
8220
|
function printPlannerRunIncompleteSummary(result, relPlanFile) {
|
|
@@ -7258,6 +8229,10 @@ function printPlannerRunIncompleteSummary(result, relPlanFile) {
|
|
|
7258
8229
|
info("The file uses the `.partial.md` suffix and YAML `squad-kit-plan-status: partial`.");
|
|
7259
8230
|
info("Raise limits in `.squad/config.yaml` (`planner.budget`, `planner.maxOutputTokens`) or re-run when ready.");
|
|
7260
8231
|
}
|
|
8232
|
+
function formatAuthRow(auth2) {
|
|
8233
|
+
if (auth2.mode === "subscription") return "subscription (Claude login)";
|
|
8234
|
+
return `api-key (${describeAuth(auth2).credentialHint})`;
|
|
8235
|
+
}
|
|
7261
8236
|
async function emitViaApi(story, paths, config2, opts) {
|
|
7262
8237
|
const planner = config2.planner;
|
|
7263
8238
|
if (!planner?.enabled) {
|
|
@@ -7265,18 +8240,13 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7265
8240
|
"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
8241
|
);
|
|
7267
8242
|
}
|
|
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
|
-
}
|
|
8243
|
+
const auth2 = resolvePlannerAuthForCwd(planner.provider, planner);
|
|
7274
8244
|
const modelId = modelFor(planner.provider, "plan", planner.modelOverride);
|
|
7275
8245
|
const anthropicRuntimeChoice = opts.anthropicRuntime ?? planner.runtime?.anthropic ?? "agent-sdk";
|
|
7276
8246
|
const draftRuntime = resolveRuntime({
|
|
7277
8247
|
provider: planner.provider,
|
|
7278
8248
|
modelId,
|
|
7279
|
-
|
|
8249
|
+
auth: auth2,
|
|
7280
8250
|
anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
|
|
7281
8251
|
});
|
|
7282
8252
|
const thinkingCli = opts.noThinking ? "off" : void 0;
|
|
@@ -7295,10 +8265,11 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7295
8265
|
step(`planning ${story.feature} / ${story.id}`);
|
|
7296
8266
|
kv("provider", planner.provider);
|
|
7297
8267
|
kv("model", modelId);
|
|
8268
|
+
kv("auth", formatAuthRow(auth2));
|
|
7298
8269
|
blank();
|
|
7299
8270
|
const interactive = !opts.yes && Boolean(process.stdin.isTTY);
|
|
7300
8271
|
if (interactive) {
|
|
7301
|
-
printPlannerApiCostNotice();
|
|
8272
|
+
printPlannerApiCostNotice(auth2.mode);
|
|
7302
8273
|
}
|
|
7303
8274
|
const mapSpinner = spinner("building repo map\u2026");
|
|
7304
8275
|
const repoMap = buildRepoMap(paths.root, { format: "tree" });
|
|
@@ -7330,7 +8301,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7330
8301
|
scoutRuntime = resolveRuntime({
|
|
7331
8302
|
provider: planner.provider,
|
|
7332
8303
|
modelId: scoutModelId,
|
|
7333
|
-
|
|
8304
|
+
auth: auth2,
|
|
7334
8305
|
anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
|
|
7335
8306
|
});
|
|
7336
8307
|
scoutSystemPrompt = composeScoutSystemPrompt({
|
|
@@ -7369,9 +8340,9 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7369
8340
|
dispatchNewPlanApiPlannerEvent(apiUiCtx, e);
|
|
7370
8341
|
});
|
|
7371
8342
|
const decideOnLimit = interactive ? async (ctx) => {
|
|
7372
|
-
printPlannerLimitExplanation(ctx);
|
|
8343
|
+
printPlannerLimitExplanation(ctx, auth2.mode);
|
|
7373
8344
|
const ans = await select6({
|
|
7374
|
-
message: "Continue this planning session? (Continuing sends more API requests and is billed.)",
|
|
8345
|
+
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
8346
|
choices: [
|
|
7376
8347
|
{ name: "Continue \u2014 extend limits for this run", value: "continue" },
|
|
7377
8348
|
{ name: "Stop \u2014 save partial plan and exit", value: "cancel" }
|
|
@@ -7387,6 +8358,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7387
8358
|
runtime: draftRuntime,
|
|
7388
8359
|
provider: planner.provider,
|
|
7389
8360
|
modelId,
|
|
8361
|
+
auth: auth2,
|
|
7390
8362
|
anthropicProviderSpecific,
|
|
7391
8363
|
systemPrompt,
|
|
7392
8364
|
userPrompt,
|
|
@@ -7452,7 +8424,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7452
8424
|
metadataHeader: header,
|
|
7453
8425
|
partial: !success2
|
|
7454
8426
|
});
|
|
7455
|
-
const relPlan =
|
|
8427
|
+
const relPlan = path25.relative(paths.root, planFile);
|
|
7456
8428
|
try {
|
|
7457
8429
|
await writeLastRun(paths, {
|
|
7458
8430
|
stats: result.stats,
|
|
@@ -7487,6 +8459,7 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7487
8459
|
durationMs: result.validation?.durationMs
|
|
7488
8460
|
},
|
|
7489
8461
|
plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
|
|
8462
|
+
authMode: describeAuth(auth2).mode,
|
|
7490
8463
|
providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
|
|
7491
8464
|
});
|
|
7492
8465
|
} catch {
|
|
@@ -7588,14 +8561,14 @@ async function emitViaApi(story, paths, config2, opts) {
|
|
|
7588
8561
|
info("Note: file-read budget was reached; the model finalised without further reads.");
|
|
7589
8562
|
}
|
|
7590
8563
|
blank();
|
|
7591
|
-
info(`next \u2192 open a new agent chat and attach only ${
|
|
8564
|
+
info(`next \u2192 open a new agent chat and attach only ${path25.basename(planFile)}`);
|
|
7592
8565
|
}
|
|
7593
8566
|
async function emitCopyPrompt(story, paths, config2, clipboard) {
|
|
7594
8567
|
const intakeContent = readFile(story.intakePath);
|
|
7595
8568
|
const composed = buildCopyPlanPromptMarkdown(config2, intakeContent);
|
|
7596
|
-
const promptFile =
|
|
7597
|
-
|
|
7598
|
-
const relPromptFile =
|
|
8569
|
+
const promptFile = path25.join(paths.squadDir, ".last-copy-prompt.md");
|
|
8570
|
+
fs35.writeFileSync(promptFile, composed, "utf8");
|
|
8571
|
+
const relPromptFile = path25.relative(paths.root, promptFile);
|
|
7599
8572
|
const bytes = Buffer.byteLength(composed, "utf8");
|
|
7600
8573
|
const estTokens = Math.round(bytes / 4);
|
|
7601
8574
|
const clipResult = clipboard ? await copyToClipboard(composed) : { ok: false, reason: "clipboard disabled (--no-clipboard)" };
|
|
@@ -7653,7 +8626,7 @@ function printModelBanner(config2) {
|
|
|
7653
8626
|
}
|
|
7654
8627
|
|
|
7655
8628
|
// src/commands/status.ts
|
|
7656
|
-
import
|
|
8629
|
+
import fs36 from "fs";
|
|
7657
8630
|
var kw = 28;
|
|
7658
8631
|
function probeTrackerCredential(config2, overlay) {
|
|
7659
8632
|
const { client, error } = clientFor(config2, overlay);
|
|
@@ -7679,7 +8652,7 @@ async function runStatus() {
|
|
|
7679
8652
|
const scan = scanPlans(paths.plansDir);
|
|
7680
8653
|
const storyCount = listStories(paths).length;
|
|
7681
8654
|
const planCount = scan.usedNumbers.length;
|
|
7682
|
-
const secrets =
|
|
8655
|
+
const secrets = fs36.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
7683
8656
|
const overlay = overlayTrackerEnv(secrets);
|
|
7684
8657
|
banner();
|
|
7685
8658
|
step("squad-kit status");
|
|
@@ -7695,12 +8668,7 @@ async function runStatus() {
|
|
|
7695
8668
|
const model = modelFor(config2.planner.provider, "plan", config2.planner.modelOverride);
|
|
7696
8669
|
const overrideNote = config2.planner.modelOverride?.[config2.planner.provider] ? " (override)" : "";
|
|
7697
8670
|
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
|
-
);
|
|
8671
|
+
kv("planner auth", buildPlannerAuthLine(config2.planner), kw);
|
|
7704
8672
|
} else {
|
|
7705
8673
|
kv("planner", "disabled (copy-paste flow)", kw);
|
|
7706
8674
|
}
|
|
@@ -7711,6 +8679,15 @@ async function runStatus() {
|
|
|
7711
8679
|
warning(`duplicate NN numbers: ${scan.duplicates.map(formatSequence).join(", ")}`);
|
|
7712
8680
|
}
|
|
7713
8681
|
}
|
|
8682
|
+
function buildPlannerAuthLine(planner) {
|
|
8683
|
+
try {
|
|
8684
|
+
const described = describeAuth(resolvePlannerAuthForCwd(planner.provider, planner));
|
|
8685
|
+
return `${described.mode} \xB7 ${described.credentialHint} (${authReasonText(described.reason, planner.provider)})`;
|
|
8686
|
+
} catch (err2) {
|
|
8687
|
+
if (!(err2 instanceof PlannerAuthUnavailableError)) throw err2;
|
|
8688
|
+
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\``;
|
|
8689
|
+
}
|
|
8690
|
+
}
|
|
7714
8691
|
function buildTrackerLine(config2, overlay) {
|
|
7715
8692
|
if (config2.tracker.type === "none") return "none";
|
|
7716
8693
|
let trackerLine = config2.tracker.type;
|
|
@@ -7733,13 +8710,13 @@ function buildTrackerLine(config2, overlay) {
|
|
|
7733
8710
|
}
|
|
7734
8711
|
|
|
7735
8712
|
// src/commands/list.ts
|
|
7736
|
-
import
|
|
8713
|
+
import path26 from "path";
|
|
7737
8714
|
async function runList(opts) {
|
|
7738
8715
|
const root = requireSquadRoot();
|
|
7739
8716
|
const paths = buildPaths(root);
|
|
7740
8717
|
const records = listStories(paths, { feature: opts.feature });
|
|
7741
8718
|
const rows = records.map((r) => {
|
|
7742
|
-
const planAbs = r.planFile ?
|
|
8719
|
+
const planAbs = r.planFile ? path26.join(paths.plansDir, r.feature, r.planFile) : void 0;
|
|
7743
8720
|
const meta = planAbs ? readPlanMetadata(planAbs) : void 0;
|
|
7744
8721
|
return {
|
|
7745
8722
|
feature: r.feature,
|
|
@@ -7778,8 +8755,8 @@ function stripAnsi(s) {
|
|
|
7778
8755
|
}
|
|
7779
8756
|
|
|
7780
8757
|
// src/commands/tracker-link.ts
|
|
7781
|
-
import
|
|
7782
|
-
import
|
|
8758
|
+
import fs37 from "fs";
|
|
8759
|
+
import path27 from "path";
|
|
7783
8760
|
import { input as input4, select as select7 } from "@inquirer/prompts";
|
|
7784
8761
|
async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
7785
8762
|
const root = requireSquadRoot();
|
|
@@ -7826,17 +8803,17 @@ async function runTrackerLink(storyPath, trackerId, opts = {}) {
|
|
|
7826
8803
|
`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
8804
|
);
|
|
7828
8805
|
}
|
|
7829
|
-
const resolved =
|
|
7830
|
-
const intakeFile = resolved.endsWith("intake.md") ? resolved :
|
|
7831
|
-
if (!
|
|
8806
|
+
const resolved = path27.resolve(resolvedStoryPath);
|
|
8807
|
+
const intakeFile = resolved.endsWith("intake.md") ? resolved : path27.join(resolved, "intake.md");
|
|
8808
|
+
if (!fs37.existsSync(intakeFile)) {
|
|
7832
8809
|
throw new Error(
|
|
7833
8810
|
`intake.md not found at ${intakeFile}. Run \`squad list\` to see valid paths, or \`squad new-story\` to create a story first.`
|
|
7834
8811
|
);
|
|
7835
8812
|
}
|
|
7836
|
-
const content =
|
|
8813
|
+
const content = fs37.readFileSync(intakeFile, "utf8");
|
|
7837
8814
|
const updated = upsertTrackerId(content, resolvedTrackerId);
|
|
7838
|
-
|
|
7839
|
-
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${
|
|
8815
|
+
fs37.writeFileSync(intakeFile, updated, "utf8");
|
|
8816
|
+
success(`Linked tracker id ${resolvedTrackerId} \u2192 ${path27.relative(root, intakeFile)}`);
|
|
7840
8817
|
blank();
|
|
7841
8818
|
step("Next:");
|
|
7842
8819
|
info("1) Review the intake \u2014 the Source block at the top now carries the tracker id.");
|
|
@@ -7854,8 +8831,8 @@ function upsertTrackerId(content, id) {
|
|
|
7854
8831
|
}
|
|
7855
8832
|
|
|
7856
8833
|
// src/commands/doctor-engine.ts
|
|
7857
|
-
import
|
|
7858
|
-
import
|
|
8834
|
+
import fs38 from "fs";
|
|
8835
|
+
import path28 from "path";
|
|
7859
8836
|
async function gatherContext(paths) {
|
|
7860
8837
|
const ctx = { hasLegacyPromptsDir: false };
|
|
7861
8838
|
try {
|
|
@@ -7864,29 +8841,30 @@ async function gatherContext(paths) {
|
|
|
7864
8841
|
ctx.configError = err2;
|
|
7865
8842
|
}
|
|
7866
8843
|
try {
|
|
7867
|
-
ctx.secrets =
|
|
8844
|
+
ctx.secrets = fs38.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
7868
8845
|
} catch (err2) {
|
|
7869
8846
|
ctx.secretsError = err2;
|
|
7870
8847
|
}
|
|
7871
|
-
ctx.hasLegacyPromptsDir =
|
|
8848
|
+
ctx.hasLegacyPromptsDir = fs38.existsSync(paths.promptsDir);
|
|
7872
8849
|
return ctx;
|
|
7873
8850
|
}
|
|
7874
8851
|
function gitignoreHasManagedBlock(repoRoot) {
|
|
7875
|
-
const gitignore =
|
|
7876
|
-
return
|
|
8852
|
+
const gitignore = path28.join(repoRoot, ".gitignore");
|
|
8853
|
+
return fs38.existsSync(gitignore) && fs38.readFileSync(gitignore, "utf8").includes(".squad/secrets.yaml");
|
|
7877
8854
|
}
|
|
7878
8855
|
function gitignoreHasTrashPattern(repoRoot) {
|
|
7879
|
-
const gitignore =
|
|
7880
|
-
return
|
|
8856
|
+
const gitignore = path28.join(repoRoot, ".gitignore");
|
|
8857
|
+
return fs38.existsSync(gitignore) && fs38.readFileSync(gitignore, "utf8").includes(SQUAD_TRASH_PATTERN);
|
|
7881
8858
|
}
|
|
8859
|
+
var DOCTOR_AUTH_PROBE_TIMEOUT_MS = 25e3;
|
|
7882
8860
|
async function checkDirStructure(paths, _ctx, fix) {
|
|
7883
|
-
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !
|
|
8861
|
+
const need = [paths.squadDir, paths.storiesDir, paths.plansDir].filter((p) => !fs38.existsSync(p));
|
|
7884
8862
|
if (need.length === 0) {
|
|
7885
8863
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok" };
|
|
7886
8864
|
}
|
|
7887
8865
|
if (fix) {
|
|
7888
8866
|
for (const p of need) {
|
|
7889
|
-
|
|
8867
|
+
fs38.mkdirSync(p, { recursive: true });
|
|
7890
8868
|
}
|
|
7891
8869
|
return { id: "dirs", name: ".squad/ directory structure", status: "ok", detail: "repaired" };
|
|
7892
8870
|
}
|
|
@@ -7894,7 +8872,7 @@ async function checkDirStructure(paths, _ctx, fix) {
|
|
|
7894
8872
|
id: "dirs",
|
|
7895
8873
|
name: ".squad/ directory structure",
|
|
7896
8874
|
status: "warn",
|
|
7897
|
-
detail: `missing: ${need.map((p) =>
|
|
8875
|
+
detail: `missing: ${need.map((p) => path28.relative(paths.root, p)).join(", ")}`,
|
|
7898
8876
|
fixable: true,
|
|
7899
8877
|
fixHint: "squad doctor --fix"
|
|
7900
8878
|
};
|
|
@@ -7949,15 +8927,15 @@ async function checkSecretsPermissions(paths, _ctx, fix) {
|
|
|
7949
8927
|
if (process.platform === "win32") {
|
|
7950
8928
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "skip", detail: "Windows" };
|
|
7951
8929
|
}
|
|
7952
|
-
if (!
|
|
8930
|
+
if (!fs38.existsSync(paths.secretsFile)) {
|
|
7953
8931
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "not present" };
|
|
7954
8932
|
}
|
|
7955
|
-
const mode =
|
|
8933
|
+
const mode = fs38.statSync(paths.secretsFile).mode & 511;
|
|
7956
8934
|
if (mode === 384) {
|
|
7957
8935
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok" };
|
|
7958
8936
|
}
|
|
7959
8937
|
if (fix) {
|
|
7960
|
-
|
|
8938
|
+
fs38.chmodSync(paths.secretsFile, 384);
|
|
7961
8939
|
return { id: "secrets-perms", name: ".squad/secrets.yaml permissions", status: "ok", detail: "repaired" };
|
|
7962
8940
|
}
|
|
7963
8941
|
return {
|
|
@@ -7978,7 +8956,7 @@ async function checkSecretsParseable(_paths, ctx) {
|
|
|
7978
8956
|
detail: ctx.secretsError.message
|
|
7979
8957
|
};
|
|
7980
8958
|
}
|
|
7981
|
-
if (!
|
|
8959
|
+
if (!fs38.existsSync(_paths.secretsFile)) {
|
|
7982
8960
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok", detail: "not present" };
|
|
7983
8961
|
}
|
|
7984
8962
|
return { id: "secrets-yaml", name: ".squad/secrets.yaml parseable", status: "ok" };
|
|
@@ -8040,36 +9018,142 @@ async function checkPlannerConfig(_paths, ctx) {
|
|
|
8040
9018
|
}
|
|
8041
9019
|
return { id: "planner-config", name: "planner configuration", status: "ok" };
|
|
8042
9020
|
}
|
|
8043
|
-
|
|
8044
|
-
|
|
8045
|
-
|
|
9021
|
+
var LOGIN_FIX_HINT = "Run `squad auth login` to sign in with your Claude account.";
|
|
9022
|
+
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.";
|
|
9023
|
+
function plannerEnabled(ctx) {
|
|
9024
|
+
return ctx.config?.planner?.enabled === true;
|
|
9025
|
+
}
|
|
9026
|
+
function authFor(paths, ctx, provider) {
|
|
9027
|
+
const memo = ctx.authMemo ??= {};
|
|
9028
|
+
if (!memo.resolution) {
|
|
9029
|
+
try {
|
|
9030
|
+
memo.resolution = { ok: true, auth: resolvePlannerAuthForPaths(paths, provider, ctx.config?.planner) };
|
|
9031
|
+
} catch (err2) {
|
|
9032
|
+
memo.resolution = { ok: false, error: err2 };
|
|
9033
|
+
}
|
|
9034
|
+
}
|
|
9035
|
+
return memo.resolution;
|
|
9036
|
+
}
|
|
9037
|
+
async function probeAuthOnce(ctx, auth2) {
|
|
9038
|
+
const memo = ctx.authMemo ??= {};
|
|
9039
|
+
if (!memo.probe) {
|
|
9040
|
+
memo.probe = skipExternalProbesInAutomation() ? { skipped: true } : await probeClaudeAuth(auth2, { timeoutMs: DOCTOR_AUTH_PROBE_TIMEOUT_MS });
|
|
9041
|
+
}
|
|
9042
|
+
return memo.probe;
|
|
9043
|
+
}
|
|
9044
|
+
function probeWasSkipped(outcome) {
|
|
9045
|
+
return "skipped" in outcome;
|
|
9046
|
+
}
|
|
9047
|
+
async function checkPlannerAuthMode(paths, ctx, _fix) {
|
|
9048
|
+
const id = "planner-auth-mode";
|
|
9049
|
+
const name = "planner auth mode";
|
|
9050
|
+
if (!plannerEnabled(ctx) || !ctx.config?.planner) {
|
|
9051
|
+
return { id, name, status: "skip", detail: "planner disabled" };
|
|
9052
|
+
}
|
|
9053
|
+
const planner = ctx.config.planner;
|
|
9054
|
+
const provider = planner.provider;
|
|
9055
|
+
if (provider !== "anthropic") {
|
|
9056
|
+
return { id, name, status: "skip", detail: `api key only for ${provider}` };
|
|
9057
|
+
}
|
|
9058
|
+
const resolution = authFor(paths, ctx, provider);
|
|
9059
|
+
if (!resolution.ok) {
|
|
9060
|
+
return { id, name, status: "fail", detail: resolution.error.message, fixHint: LOGIN_FIX_HINT };
|
|
9061
|
+
}
|
|
9062
|
+
const auth2 = resolution.auth;
|
|
9063
|
+
const described = describeAuth(auth2);
|
|
9064
|
+
let detail = `${described.mode} (${authReasonText(described.reason, provider)}) \u2192 ${described.credentialHint}`;
|
|
9065
|
+
if (auth2.mode === "subscription") {
|
|
9066
|
+
const ignoredKey = resolveProviderKeyForPaths(paths, provider);
|
|
9067
|
+
if (ignoredKey) detail += ` \xB7 ignoring the API key from ${ignoredKey.detail}`;
|
|
9068
|
+
}
|
|
9069
|
+
const implicitApiKey = (planner.auth?.anthropic ?? "auto") === "auto" && auth2.reason === "auto-fallback-api-key";
|
|
9070
|
+
if (implicitApiKey) {
|
|
9071
|
+
return {
|
|
9072
|
+
id,
|
|
9073
|
+
name,
|
|
9074
|
+
status: "ok",
|
|
9075
|
+
detail,
|
|
9076
|
+
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."
|
|
9077
|
+
};
|
|
9078
|
+
}
|
|
9079
|
+
return { id, name, status: "ok", detail };
|
|
9080
|
+
}
|
|
9081
|
+
function describeProbeAccount(probe) {
|
|
9082
|
+
const bits = [];
|
|
9083
|
+
const email = probe.account?.email;
|
|
9084
|
+
const plan = probe.account?.subscriptionType;
|
|
9085
|
+
if (email) {
|
|
9086
|
+
bits.push(`signed in as ${email}${plan ? ` (${plan})` : ""}`);
|
|
9087
|
+
} else if (probe.unverifiable) {
|
|
9088
|
+
bits.push(`credential resolved (${probe.credentialSource ?? "token auth"}) \xB7 not verifiable without a run`);
|
|
9089
|
+
} else {
|
|
9090
|
+
bits.push("Claude login verified");
|
|
9091
|
+
}
|
|
9092
|
+
if (probe.account?.organization) bits.push(probe.account.organization);
|
|
9093
|
+
if (probe.apiKeySource) bits.push(`apiKeySource=${probe.apiKeySource}`);
|
|
9094
|
+
return bits.join(" \xB7 ");
|
|
9095
|
+
}
|
|
9096
|
+
async function checkPlannerCredential(paths, ctx) {
|
|
9097
|
+
const id = "planner-cred";
|
|
9098
|
+
const name = "planner credential resolves";
|
|
9099
|
+
if (!plannerEnabled(ctx) || !ctx.config?.planner) {
|
|
9100
|
+
return { id, name, status: "skip", detail: "planner disabled" };
|
|
8046
9101
|
}
|
|
8047
9102
|
const provider = ctx.config.planner.provider;
|
|
8048
|
-
const
|
|
8049
|
-
if (!
|
|
9103
|
+
const resolution = authFor(paths, ctx, provider);
|
|
9104
|
+
if (!resolution.ok) {
|
|
8050
9105
|
const envVar = providerEnvVar(provider);
|
|
8051
9106
|
return {
|
|
8052
|
-
id
|
|
8053
|
-
name
|
|
9107
|
+
id,
|
|
9108
|
+
name,
|
|
8054
9109
|
status: "fail",
|
|
8055
9110
|
detail: `no API key found (${envVar} or .squad/secrets.yaml)`,
|
|
8056
|
-
fixHint:
|
|
9111
|
+
fixHint: resolution.error.message
|
|
8057
9112
|
};
|
|
8058
9113
|
}
|
|
8059
|
-
|
|
8060
|
-
|
|
8061
|
-
name: "
|
|
8062
|
-
|
|
8063
|
-
|
|
8064
|
-
|
|
9114
|
+
const auth2 = resolution.auth;
|
|
9115
|
+
if (auth2.mode === "api-key") {
|
|
9116
|
+
return { id, name, status: "ok", detail: `source=${auth2.source} (${auth2.detail})` };
|
|
9117
|
+
}
|
|
9118
|
+
const credentialHint = describeAuth(auth2).credentialHint;
|
|
9119
|
+
const probe = await probeAuthOnce(ctx, auth2);
|
|
9120
|
+
if (probeWasSkipped(probe)) {
|
|
9121
|
+
return credentialPresent(auth2) ? { id, name, status: "ok", detail: `${credentialHint} \xB7 offline check only` } : {
|
|
9122
|
+
id,
|
|
9123
|
+
name,
|
|
9124
|
+
status: "warn",
|
|
9125
|
+
detail: "no Claude login detected \xB7 offline check only",
|
|
9126
|
+
fixHint: LOGIN_FIX_HINT
|
|
9127
|
+
};
|
|
9128
|
+
}
|
|
9129
|
+
if (probe.ok) {
|
|
9130
|
+
return { id, name, status: "ok", detail: describeProbeAccount(probe) };
|
|
9131
|
+
}
|
|
9132
|
+
switch (probe.kind) {
|
|
9133
|
+
case "not-logged-in":
|
|
9134
|
+
case "expired":
|
|
9135
|
+
return { id, name, status: "fail", detail: probe.detail, fixHint: LOGIN_FIX_HINT };
|
|
9136
|
+
case "org-not-allowed":
|
|
9137
|
+
return { id, name, status: "fail", detail: probe.detail, fixHint: ORG_NOT_ALLOWED_FIX_HINT };
|
|
9138
|
+
case "no-binary":
|
|
9139
|
+
return { id, name, status: "warn", detail: probe.detail, fixHint: MISSING_CLAUDE_BINARY_MESSAGE };
|
|
9140
|
+
default:
|
|
9141
|
+
return credentialPresent(auth2) ? { id, name, status: "warn", detail: `${probe.kind}: ${probe.detail}` } : {
|
|
9142
|
+
id,
|
|
9143
|
+
name,
|
|
9144
|
+
status: "fail",
|
|
9145
|
+
detail: `no Claude login detected (live check: ${probe.kind})`,
|
|
9146
|
+
fixHint: LOGIN_FIX_HINT
|
|
9147
|
+
};
|
|
9148
|
+
}
|
|
8065
9149
|
}
|
|
8066
|
-
async function checkPlannerModel(
|
|
8067
|
-
if (!ctx
|
|
9150
|
+
async function checkPlannerModel(paths, ctx) {
|
|
9151
|
+
if (!plannerEnabled(ctx) || !ctx.config?.planner) {
|
|
8068
9152
|
return { id: "planner-model", name: "planner model resolves at provider", status: "skip", detail: "planner disabled" };
|
|
8069
9153
|
}
|
|
8070
9154
|
const provider = ctx.config.planner.provider;
|
|
8071
|
-
const
|
|
8072
|
-
if (!
|
|
9155
|
+
const resolution = authFor(paths, ctx, provider);
|
|
9156
|
+
if (!resolution.ok) {
|
|
8073
9157
|
return {
|
|
8074
9158
|
id: "planner-model",
|
|
8075
9159
|
name: "planner model resolves at provider",
|
|
@@ -8077,9 +9161,18 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
8077
9161
|
detail: "no credential"
|
|
8078
9162
|
};
|
|
8079
9163
|
}
|
|
9164
|
+
const auth2 = resolution.auth;
|
|
9165
|
+
if (auth2.mode === "subscription") {
|
|
9166
|
+
return {
|
|
9167
|
+
id: "planner-model",
|
|
9168
|
+
name: "planner model resolves at provider",
|
|
9169
|
+
status: "skip",
|
|
9170
|
+
detail: "model list needs an API key; skipped in subscription mode"
|
|
9171
|
+
};
|
|
9172
|
+
}
|
|
8080
9173
|
const model = modelFor(provider, "plan", ctx.config.planner.modelOverride);
|
|
8081
9174
|
try {
|
|
8082
|
-
const listed = await fetchProviderModelIds(provider,
|
|
9175
|
+
const listed = await fetchProviderModelIds(provider, auth2.key);
|
|
8083
9176
|
if (!listed.ok) {
|
|
8084
9177
|
const st = listed.status;
|
|
8085
9178
|
if (st === 401 || st === 403) {
|
|
@@ -8121,7 +9214,7 @@ async function checkPlannerModel(_paths, ctx) {
|
|
|
8121
9214
|
};
|
|
8122
9215
|
}
|
|
8123
9216
|
}
|
|
8124
|
-
async function checkPlannerTierAwareness(
|
|
9217
|
+
async function checkPlannerTierAwareness(paths, ctx) {
|
|
8125
9218
|
const id = "planner-tier";
|
|
8126
9219
|
const name = "planner tier vs. model";
|
|
8127
9220
|
if (!ctx.config || ctx.config.planner?.enabled !== true) {
|
|
@@ -8131,6 +9224,10 @@ async function checkPlannerTierAwareness(_paths, ctx) {
|
|
|
8131
9224
|
if (provider !== "anthropic") {
|
|
8132
9225
|
return { id, name, status: "skip", detail: `not applicable for ${provider}` };
|
|
8133
9226
|
}
|
|
9227
|
+
const tierAuth = authFor(paths, ctx, provider);
|
|
9228
|
+
if (tierAuth.ok && tierAuth.auth.mode === "subscription") {
|
|
9229
|
+
return { id, name, status: "skip", detail: "not applicable on subscription auth (no API rate tier)" };
|
|
9230
|
+
}
|
|
8134
9231
|
const planModel = modelFor(provider, "plan", ctx.config.planner.modelOverride);
|
|
8135
9232
|
if (!/opus/i.test(planModel)) {
|
|
8136
9233
|
return { id, name, status: "ok", detail: `${planModel} is comfortably under Tier 1` };
|
|
@@ -8259,18 +9356,57 @@ async function checkPlannerAnthropicRuntimeModelFit(_paths, ctx) {
|
|
|
8259
9356
|
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
9357
|
};
|
|
8261
9358
|
}
|
|
8262
|
-
async function
|
|
9359
|
+
async function checkPlannerAuthRuntimeFit(paths, ctx) {
|
|
9360
|
+
const id = "planner-auth-runtime-fit";
|
|
9361
|
+
const name = "planner auth vs. runtime";
|
|
9362
|
+
if (!plannerEnabled(ctx) || !ctx.config?.planner) {
|
|
9363
|
+
return { id, name, status: "skip", detail: "planner disabled" };
|
|
9364
|
+
}
|
|
9365
|
+
const planner = ctx.config.planner;
|
|
9366
|
+
const provider = planner.provider;
|
|
9367
|
+
const configuredMode = planner.auth?.anthropic ?? "auto";
|
|
9368
|
+
if (provider !== "anthropic") {
|
|
9369
|
+
if (configuredMode === "subscription") {
|
|
9370
|
+
return {
|
|
9371
|
+
id,
|
|
9372
|
+
name,
|
|
9373
|
+
status: "fail",
|
|
9374
|
+
detail: `planner.auth.anthropic: subscription with planner.provider: ${provider}`,
|
|
9375
|
+
fixHint: plannerAuthRuntimeMismatchMessage(provider)
|
|
9376
|
+
};
|
|
9377
|
+
}
|
|
9378
|
+
return { id, name, status: "skip", detail: `api key only for ${provider}` };
|
|
9379
|
+
}
|
|
9380
|
+
const runtime = runtimeNameFor(provider, planner);
|
|
9381
|
+
const resolution = authFor(paths, ctx, provider);
|
|
9382
|
+
if (!resolution.ok) {
|
|
9383
|
+
return { id, name, status: "skip", detail: "auth unresolved" };
|
|
9384
|
+
}
|
|
9385
|
+
const mode = resolution.auth.mode;
|
|
9386
|
+
if (mode === "subscription" && runtime !== "agent-sdk") {
|
|
9387
|
+
return {
|
|
9388
|
+
id,
|
|
9389
|
+
name,
|
|
9390
|
+
status: "fail",
|
|
9391
|
+
detail: `subscription auth with planner.runtime.anthropic: ${runtime}`,
|
|
9392
|
+
fixHint: plannerAuthRuntimeMismatchMessage(provider)
|
|
9393
|
+
};
|
|
9394
|
+
}
|
|
9395
|
+
return { id, name, status: "ok", detail: `${mode} + ${runtime} runtime` };
|
|
9396
|
+
}
|
|
9397
|
+
async function checkAgentSdkBinaryPresent(paths, ctx) {
|
|
8263
9398
|
const id = "agent-sdk-binary-present";
|
|
8264
9399
|
const name = "Anthropic Agent SDK install";
|
|
8265
9400
|
if (!ctx.config?.planner?.enabled || ctx.config.planner.provider !== "anthropic") {
|
|
8266
9401
|
return { id, name, status: "skip", detail: "not applicable" };
|
|
8267
9402
|
}
|
|
8268
|
-
|
|
9403
|
+
const resolution = authFor(paths, ctx, "anthropic");
|
|
9404
|
+
const subscription = resolution.ok && resolution.auth.mode === "subscription";
|
|
9405
|
+
if ((ctx.config.planner.runtime?.anthropic ?? "agent-sdk") === "vercel" && !subscription) {
|
|
8269
9406
|
return { id, name, status: "skip", detail: "vercel runtime selected" };
|
|
8270
9407
|
}
|
|
8271
9408
|
try {
|
|
8272
9409
|
await import("@anthropic-ai/claude-agent-sdk");
|
|
8273
|
-
return { id, name, status: "ok", detail: "package resolves" };
|
|
8274
9410
|
} catch (e) {
|
|
8275
9411
|
return {
|
|
8276
9412
|
id,
|
|
@@ -8280,6 +9416,22 @@ async function checkAgentSdkBinaryPresent(_paths, ctx) {
|
|
|
8280
9416
|
fixHint: "Run pnpm install / npm install in the squad-kit package so the Agent SDK (and its platform binary) is present."
|
|
8281
9417
|
};
|
|
8282
9418
|
}
|
|
9419
|
+
const binary = resolveClaudeBinary();
|
|
9420
|
+
if (!binary) {
|
|
9421
|
+
return {
|
|
9422
|
+
id,
|
|
9423
|
+
name,
|
|
9424
|
+
status: subscription ? "fail" : "warn",
|
|
9425
|
+
detail: "package resolves; no `claude` executable found",
|
|
9426
|
+
fixHint: MISSING_CLAUDE_BINARY_MESSAGE
|
|
9427
|
+
};
|
|
9428
|
+
}
|
|
9429
|
+
return {
|
|
9430
|
+
id,
|
|
9431
|
+
name,
|
|
9432
|
+
status: "ok",
|
|
9433
|
+
detail: `package + ${binary.source === "bundled" ? "bundled" : "PATH"} binary`
|
|
9434
|
+
};
|
|
8283
9435
|
}
|
|
8284
9436
|
async function checkTrackerConfig(_paths, ctx) {
|
|
8285
9437
|
if (!ctx.config) {
|
|
@@ -8418,11 +9570,13 @@ async function runAllChecks(paths, ctx, fix) {
|
|
|
8418
9570
|
await add(".squad/secrets.yaml parseable", () => checkSecretsParseable(paths, ctx));
|
|
8419
9571
|
await add("legacy .squad/prompts/ directory", () => checkLegacyPrompts(paths, ctx));
|
|
8420
9572
|
await add("planner configuration", () => checkPlannerConfig(paths, ctx));
|
|
9573
|
+
await add("planner auth mode", () => checkPlannerAuthMode(paths, ctx, fix));
|
|
8421
9574
|
await add("planner credential resolves", () => checkPlannerCredential(paths, ctx));
|
|
8422
9575
|
await add("planner model resolves at provider", () => checkPlannerModel(paths, ctx));
|
|
8423
9576
|
await add("planner tier vs. model", () => checkPlannerTierAwareness(paths, ctx));
|
|
8424
9577
|
await add("planner cache effectiveness", () => checkPlannerCache(paths, ctx));
|
|
8425
9578
|
await add("planner runtime (resolved)", () => checkPlannerRuntimeInfo(paths, ctx));
|
|
9579
|
+
await add("planner auth vs. runtime", () => checkPlannerAuthRuntimeFit(paths, ctx));
|
|
8426
9580
|
await add("Anthropic Opus 4.7+ vs Vercel runtime", () => checkPlannerAnthropicRuntimeModelFit(paths, ctx));
|
|
8427
9581
|
await add("Anthropic Agent SDK install", () => checkAgentSdkBinaryPresent(paths, ctx));
|
|
8428
9582
|
await add("tracker configuration", () => checkTrackerConfig(paths, ctx));
|
|
@@ -8532,23 +9686,23 @@ async function runDoctor(opts) {
|
|
|
8532
9686
|
}
|
|
8533
9687
|
|
|
8534
9688
|
// src/commands/migrate.ts
|
|
8535
|
-
import
|
|
8536
|
-
import
|
|
8537
|
-
import { confirm as
|
|
9689
|
+
import fs39 from "fs";
|
|
9690
|
+
import path29 from "path";
|
|
9691
|
+
import { confirm as confirm8 } from "@inquirer/prompts";
|
|
8538
9692
|
var LEGACY_PROMPTS_MIGRATION = {
|
|
8539
9693
|
id: "legacy-prompts",
|
|
8540
9694
|
name: "Remove legacy .squad/prompts/",
|
|
8541
|
-
isApplied: (paths) => !
|
|
8542
|
-
describe: (paths) => `delete ${
|
|
9695
|
+
isApplied: (paths) => !fs39.existsSync(paths.promptsDir),
|
|
9696
|
+
describe: (paths) => `delete ${path29.relative(paths.root, paths.promptsDir)} (prompts are now bundled in squad-kit)`,
|
|
8543
9697
|
apply: (paths) => {
|
|
8544
9698
|
const target = paths.promptsDir;
|
|
8545
|
-
if (!target.includes(
|
|
9699
|
+
if (!target.includes(path29.sep + ".squad" + path29.sep + "prompts")) {
|
|
8546
9700
|
throw new Error(
|
|
8547
9701
|
`Refusing to delete suspicious path: ${target}. This is a safety check \u2014 run \`squad doctor\` if .squad/ paths look wrong.`
|
|
8548
9702
|
);
|
|
8549
9703
|
}
|
|
8550
|
-
if (
|
|
8551
|
-
|
|
9704
|
+
if (fs39.existsSync(target)) {
|
|
9705
|
+
fs39.rmSync(target, { recursive: true, force: true });
|
|
8552
9706
|
}
|
|
8553
9707
|
}
|
|
8554
9708
|
};
|
|
@@ -8556,8 +9710,8 @@ var GITIGNORE_MIGRATION = {
|
|
|
8556
9710
|
id: "gitignore",
|
|
8557
9711
|
name: "Ensure .gitignore managed block",
|
|
8558
9712
|
isApplied: (paths) => {
|
|
8559
|
-
const f =
|
|
8560
|
-
return
|
|
9713
|
+
const f = path29.join(paths.root, ".gitignore");
|
|
9714
|
+
return fs39.existsSync(f) && fs39.readFileSync(f, "utf8").includes(".squad/secrets.yaml");
|
|
8561
9715
|
},
|
|
8562
9716
|
describe: () => "add squad-kit managed block to .gitignore",
|
|
8563
9717
|
apply: (paths) => {
|
|
@@ -8569,23 +9723,23 @@ var SECRETS_PERMS_MIGRATION = {
|
|
|
8569
9723
|
name: "Tighten .squad/secrets.yaml permissions",
|
|
8570
9724
|
isApplied: (paths) => {
|
|
8571
9725
|
if (process.platform === "win32") return true;
|
|
8572
|
-
if (!
|
|
8573
|
-
const st =
|
|
9726
|
+
if (!fs39.existsSync(paths.secretsFile)) return true;
|
|
9727
|
+
const st = fs39.statSync(paths.secretsFile);
|
|
8574
9728
|
return (st.mode & 511) === 384;
|
|
8575
9729
|
},
|
|
8576
|
-
describe: (paths) => `chmod 0600 ${
|
|
9730
|
+
describe: (paths) => `chmod 0600 ${path29.relative(paths.root, paths.secretsFile)}`,
|
|
8577
9731
|
apply: (paths) => {
|
|
8578
9732
|
if (process.platform === "win32") return;
|
|
8579
|
-
if (
|
|
9733
|
+
if (fs39.existsSync(paths.secretsFile)) fs39.chmodSync(paths.secretsFile, 384);
|
|
8580
9734
|
}
|
|
8581
9735
|
};
|
|
8582
9736
|
var PLANNER_BUDGET_DEFAULTS_MIGRATION = {
|
|
8583
9737
|
id: "planner-budget-defaults",
|
|
8584
9738
|
name: "Backfill planner.budget defaults in config.yaml",
|
|
8585
9739
|
isApplied: (paths) => {
|
|
8586
|
-
if (!
|
|
9740
|
+
if (!fs39.existsSync(paths.configFile)) return true;
|
|
8587
9741
|
try {
|
|
8588
|
-
const raw =
|
|
9742
|
+
const raw = fs39.readFileSync(paths.configFile, "utf8");
|
|
8589
9743
|
if (!/^\s*planner:/m.test(raw)) return true;
|
|
8590
9744
|
if (/^\s*budget:/m.test(raw)) return true;
|
|
8591
9745
|
const cfg = loadConfig(paths.configFile);
|
|
@@ -8604,9 +9758,9 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
8604
9758
|
id: "config-normalize",
|
|
8605
9759
|
name: "Normalise .squad/config.yaml formatting",
|
|
8606
9760
|
isApplied: (paths) => {
|
|
8607
|
-
if (!
|
|
9761
|
+
if (!fs39.existsSync(paths.configFile)) return true;
|
|
8608
9762
|
try {
|
|
8609
|
-
const raw =
|
|
9763
|
+
const raw = fs39.readFileSync(paths.configFile, "utf8");
|
|
8610
9764
|
const cfg = loadConfig(paths.configFile);
|
|
8611
9765
|
return raw.trim() === serializeConfig(cfg).trim();
|
|
8612
9766
|
} catch {
|
|
@@ -8615,7 +9769,7 @@ var CONFIG_NORMALIZE_MIGRATION = {
|
|
|
8615
9769
|
},
|
|
8616
9770
|
describe: () => "rewrite config.yaml with canonical YAML formatting (drops comments)",
|
|
8617
9771
|
apply: (paths) => {
|
|
8618
|
-
if (!
|
|
9772
|
+
if (!fs39.existsSync(paths.configFile)) return;
|
|
8619
9773
|
const cfg = loadConfig(paths.configFile);
|
|
8620
9774
|
saveConfig(paths.configFile, cfg);
|
|
8621
9775
|
}
|
|
@@ -8663,7 +9817,7 @@ async function runMigrate(opts) {
|
|
|
8663
9817
|
);
|
|
8664
9818
|
process.exit(1);
|
|
8665
9819
|
}
|
|
8666
|
-
const go = await
|
|
9820
|
+
const go = await confirm8({ message: `Apply ${pending.length} migration(s)?`, default: true });
|
|
8667
9821
|
if (!go) {
|
|
8668
9822
|
info("Cancelled. No changes applied.");
|
|
8669
9823
|
return;
|
|
@@ -8691,13 +9845,13 @@ async function runMigrate(opts) {
|
|
|
8691
9845
|
}
|
|
8692
9846
|
|
|
8693
9847
|
// src/commands/upgrade.ts
|
|
8694
|
-
import { spawn as
|
|
9848
|
+
import { spawn as spawn3 } from "child_process";
|
|
8695
9849
|
import process3 from "process";
|
|
8696
|
-
import { confirm as
|
|
9850
|
+
import { confirm as confirm9 } from "@inquirer/prompts";
|
|
8697
9851
|
|
|
8698
9852
|
// src/core/package-info.ts
|
|
8699
|
-
import
|
|
8700
|
-
import
|
|
9853
|
+
import fs40 from "fs";
|
|
9854
|
+
import path30 from "path";
|
|
8701
9855
|
function changelogBlobUrl(repositoryUrl, version) {
|
|
8702
9856
|
let base = repositoryUrl;
|
|
8703
9857
|
if (!base) base = "https://github.com/AzmSquad/squad-kit";
|
|
@@ -8708,9 +9862,9 @@ function changelogBlobUrl(repositoryUrl, version) {
|
|
|
8708
9862
|
}
|
|
8709
9863
|
function readInstalledPackage() {
|
|
8710
9864
|
const root = packageRoot();
|
|
8711
|
-
const pkgFile =
|
|
8712
|
-
const pkg = JSON.parse(
|
|
8713
|
-
const isDevInstall =
|
|
9865
|
+
const pkgFile = path30.join(root, "package.json");
|
|
9866
|
+
const pkg = JSON.parse(fs40.readFileSync(pkgFile, "utf8"));
|
|
9867
|
+
const isDevInstall = fs40.existsSync(path30.join(root, "src"));
|
|
8714
9868
|
return {
|
|
8715
9869
|
name: pkg.name,
|
|
8716
9870
|
version: pkg.version,
|
|
@@ -8750,7 +9904,7 @@ function isNewer(a, b) {
|
|
|
8750
9904
|
}
|
|
8751
9905
|
|
|
8752
9906
|
// src/core/package-manager.ts
|
|
8753
|
-
import
|
|
9907
|
+
import path31 from "path";
|
|
8754
9908
|
import process2 from "process";
|
|
8755
9909
|
function detectPackageManager(installRoot) {
|
|
8756
9910
|
const ua = process2.env.npm_config_user_agent;
|
|
@@ -8761,11 +9915,11 @@ function detectPackageManager(installRoot) {
|
|
|
8761
9915
|
if (ua.startsWith("npm/")) return { pm: "npm", reason: "npm_config_user_agent" };
|
|
8762
9916
|
}
|
|
8763
9917
|
const root = installRoot.toLowerCase();
|
|
8764
|
-
if (root.includes(`${
|
|
9918
|
+
if (root.includes(`${path31.sep}pnpm${path31.sep}`) || root.includes(`${path31.sep}.pnpm${path31.sep}`))
|
|
8765
9919
|
return { pm: "pnpm", reason: `install path contains pnpm` };
|
|
8766
|
-
if (root.includes(`${
|
|
9920
|
+
if (root.includes(`${path31.sep}.yarn${path31.sep}`) || root.includes(`${path31.sep}yarn${path31.sep}`))
|
|
8767
9921
|
return { pm: "yarn", reason: `install path contains yarn` };
|
|
8768
|
-
if (root.includes(`${
|
|
9922
|
+
if (root.includes(`${path31.sep}.bun${path31.sep}`) || root.includes(`${path31.sep}bun${path31.sep}`))
|
|
8769
9923
|
return { pm: "bun", reason: `install path contains bun` };
|
|
8770
9924
|
return { pm: "npm", reason: "default fallback" };
|
|
8771
9925
|
}
|
|
@@ -8838,7 +9992,7 @@ async function runUpgrade(opts) {
|
|
|
8838
9992
|
);
|
|
8839
9993
|
process3.exit(1);
|
|
8840
9994
|
}
|
|
8841
|
-
const go = await
|
|
9995
|
+
const go = await confirm9({ message: `Run "${cmd} ${args.join(" ")}"?`, default: true });
|
|
8842
9996
|
if (!go) {
|
|
8843
9997
|
info("Cancelled. No changes applied.");
|
|
8844
9998
|
return;
|
|
@@ -8859,7 +10013,7 @@ async function runUpgrade(opts) {
|
|
|
8859
10013
|
}
|
|
8860
10014
|
function spawnInherit(cmd, args) {
|
|
8861
10015
|
return new Promise((resolve2, reject) => {
|
|
8862
|
-
const child =
|
|
10016
|
+
const child = spawn3(cmd, args, { stdio: "inherit", shell: process3.platform === "win32" });
|
|
8863
10017
|
child.on("exit", (code) => resolve2(code ?? 0));
|
|
8864
10018
|
child.on("error", (err2) => reject(err2));
|
|
8865
10019
|
});
|
|
@@ -8878,14 +10032,14 @@ import { cors } from "hono/cors";
|
|
|
8878
10032
|
import { secureHeaders } from "hono/secure-headers";
|
|
8879
10033
|
|
|
8880
10034
|
// src/console/api/stories.ts
|
|
8881
|
-
import
|
|
10035
|
+
import fs41 from "fs";
|
|
8882
10036
|
import { z as z6 } from "zod";
|
|
8883
10037
|
|
|
8884
10038
|
// src/utils/path-safety.ts
|
|
8885
|
-
import
|
|
10039
|
+
import path32 from "path";
|
|
8886
10040
|
function isInside(abs, parent) {
|
|
8887
|
-
const a =
|
|
8888
|
-
const p =
|
|
10041
|
+
const a = path32.resolve(abs) + path32.sep;
|
|
10042
|
+
const p = path32.resolve(parent) + path32.sep;
|
|
8889
10043
|
return a.startsWith(p);
|
|
8890
10044
|
}
|
|
8891
10045
|
|
|
@@ -8925,7 +10079,7 @@ function mountStoriesApi(app, opts) {
|
|
|
8925
10079
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
8926
10080
|
let intakeContent = "";
|
|
8927
10081
|
try {
|
|
8928
|
-
intakeContent =
|
|
10082
|
+
intakeContent = fs41.readFileSync(hit.intakePath, "utf8");
|
|
8929
10083
|
} catch {
|
|
8930
10084
|
}
|
|
8931
10085
|
return c.json({
|
|
@@ -8969,7 +10123,7 @@ function mountStoriesApi(app, opts) {
|
|
|
8969
10123
|
const hit = stories.find((s) => s.id === id);
|
|
8970
10124
|
if (!hit) return c.json({ error: "not_found" }, 404);
|
|
8971
10125
|
if (!isInside(hit.intakePath, opts.paths.storiesDir)) return c.json({ error: "forbidden" }, 403);
|
|
8972
|
-
|
|
10126
|
+
fs41.writeFileSync(hit.intakePath, body.data.intakeContent, "utf8");
|
|
8973
10127
|
return c.json({ ok: true });
|
|
8974
10128
|
});
|
|
8975
10129
|
app.delete("/api/stories/:feature/:id", (c) => {
|
|
@@ -8986,8 +10140,8 @@ function mountStoriesApi(app, opts) {
|
|
|
8986
10140
|
}
|
|
8987
10141
|
|
|
8988
10142
|
// src/console/api/plans.ts
|
|
8989
|
-
import
|
|
8990
|
-
import
|
|
10143
|
+
import fs42 from "fs";
|
|
10144
|
+
import path33 from "path";
|
|
8991
10145
|
import { z as z7 } from "zod";
|
|
8992
10146
|
import { diffLines } from "diff";
|
|
8993
10147
|
var DiffQuery = z7.object({
|
|
@@ -9002,11 +10156,11 @@ function mountPlansApi(app, opts) {
|
|
|
9002
10156
|
app.get("/api/plans", (c) => {
|
|
9003
10157
|
const out = [];
|
|
9004
10158
|
for (const feature of listPlanFeatureNames(opts.paths)) {
|
|
9005
|
-
const featureDir =
|
|
9006
|
-
if (!
|
|
9007
|
-
for (const entry of
|
|
10159
|
+
const featureDir = path33.join(opts.paths.plansDir, feature);
|
|
10160
|
+
if (!fs42.existsSync(featureDir)) continue;
|
|
10161
|
+
for (const entry of fs42.readdirSync(featureDir)) {
|
|
9008
10162
|
if (!entry.endsWith(".md") || entry === "00-index.md") continue;
|
|
9009
|
-
const abs =
|
|
10163
|
+
const abs = path33.join(featureDir, entry);
|
|
9010
10164
|
out.push({ feature, planFile: entry, metadata: readPlanMetadata(abs) });
|
|
9011
10165
|
}
|
|
9012
10166
|
}
|
|
@@ -9015,10 +10169,10 @@ function mountPlansApi(app, opts) {
|
|
|
9015
10169
|
app.get("/api/plans/:feature/:planFile", (c) => {
|
|
9016
10170
|
const { feature, planFile } = c.req.param();
|
|
9017
10171
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
9018
|
-
const abs =
|
|
10172
|
+
const abs = path33.resolve(path33.join(opts.paths.plansDir, feature, planFile));
|
|
9019
10173
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
9020
|
-
if (!
|
|
9021
|
-
const content =
|
|
10174
|
+
if (!fs42.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
10175
|
+
const content = fs42.readFileSync(abs, "utf8");
|
|
9022
10176
|
return c.json({
|
|
9023
10177
|
feature,
|
|
9024
10178
|
planFile,
|
|
@@ -9036,15 +10190,15 @@ function mountPlansApi(app, opts) {
|
|
|
9036
10190
|
if (!parse.success) return c.json({ error: "invalid_query", issues: parse.error.issues }, 400);
|
|
9037
10191
|
const { feature, a, b } = parse.data;
|
|
9038
10192
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
9039
|
-
const featureDir =
|
|
9040
|
-
const aAbs =
|
|
9041
|
-
const bAbs =
|
|
10193
|
+
const featureDir = path33.join(opts.paths.plansDir, feature);
|
|
10194
|
+
const aAbs = path33.join(featureDir, a);
|
|
10195
|
+
const bAbs = path33.join(featureDir, b);
|
|
9042
10196
|
if (!isInside(aAbs, opts.paths.plansDir) || !isInside(bAbs, opts.paths.plansDir)) {
|
|
9043
10197
|
return c.json({ error: "forbidden" }, 403);
|
|
9044
10198
|
}
|
|
9045
|
-
if (!
|
|
9046
|
-
const aText =
|
|
9047
|
-
const bText =
|
|
10199
|
+
if (!fs42.existsSync(aAbs) || !fs42.existsSync(bAbs)) return c.json({ error: "not_found" }, 404);
|
|
10200
|
+
const aText = fs42.readFileSync(aAbs, "utf8");
|
|
10201
|
+
const bText = fs42.readFileSync(bAbs, "utf8");
|
|
9048
10202
|
const changes = diffLines(aText, bText);
|
|
9049
10203
|
return c.json({ feature, a, b, changes });
|
|
9050
10204
|
});
|
|
@@ -9052,16 +10206,16 @@ function mountPlansApi(app, opts) {
|
|
|
9052
10206
|
const trash = c.req.query("trash") === "1";
|
|
9053
10207
|
const { feature, planFile } = c.req.param();
|
|
9054
10208
|
if (!safeFeatureParam2(feature)) return c.json({ error: "forbidden" }, 403);
|
|
9055
|
-
const abs =
|
|
10209
|
+
const abs = path33.join(opts.paths.plansDir, feature, planFile);
|
|
9056
10210
|
if (!isInside(abs, opts.paths.plansDir)) return c.json({ error: "forbidden" }, 403);
|
|
9057
|
-
if (!
|
|
10211
|
+
if (!fs42.existsSync(abs)) return c.json({ error: "not_found" }, 404);
|
|
9058
10212
|
if (trash) {
|
|
9059
10213
|
const result = trashOrDelete([abs], opts.paths.trashDir, true);
|
|
9060
|
-
removeOverviewRowForPlanFile(
|
|
10214
|
+
removeOverviewRowForPlanFile(path33.join(opts.paths.plansDir, feature), planFile);
|
|
9061
10215
|
return c.json({ ok: true, trashed: result.trashed });
|
|
9062
10216
|
}
|
|
9063
|
-
|
|
9064
|
-
removeOverviewRowForPlanFile(
|
|
10217
|
+
fs42.rmSync(abs);
|
|
10218
|
+
removeOverviewRowForPlanFile(path33.join(opts.paths.plansDir, feature), planFile);
|
|
9065
10219
|
return c.json({ ok: true });
|
|
9066
10220
|
});
|
|
9067
10221
|
}
|
|
@@ -9109,8 +10263,8 @@ function mountMetaApi(app, opts) {
|
|
|
9109
10263
|
// src/console/api/runs.ts
|
|
9110
10264
|
import { streamSSE } from "hono/streaming";
|
|
9111
10265
|
import { z as z9 } from "zod";
|
|
9112
|
-
import
|
|
9113
|
-
import
|
|
10266
|
+
import path34 from "path";
|
|
10267
|
+
import fs43 from "fs";
|
|
9114
10268
|
var active = /* @__PURE__ */ new Map();
|
|
9115
10269
|
var EVENT_BUF_CAP = 4096;
|
|
9116
10270
|
var StartBody = z9.object({
|
|
@@ -9146,15 +10300,21 @@ function mountRunsApi(app, opts) {
|
|
|
9146
10300
|
if (!cfg.planner?.enabled) {
|
|
9147
10301
|
return c.json({ error: "planner_disabled", detail: "Run `squad config set planner` first." }, 400);
|
|
9148
10302
|
}
|
|
9149
|
-
|
|
9150
|
-
|
|
9151
|
-
|
|
9152
|
-
|
|
9153
|
-
|
|
9154
|
-
|
|
9155
|
-
|
|
9156
|
-
|
|
9157
|
-
|
|
10303
|
+
let auth2;
|
|
10304
|
+
try {
|
|
10305
|
+
auth2 = resolvePlannerAuthForPaths(opts.paths, cfg.planner.provider, cfg.planner);
|
|
10306
|
+
} catch (err2) {
|
|
10307
|
+
if (err2 instanceof PlannerAuthUnavailableError) {
|
|
10308
|
+
return c.json(
|
|
10309
|
+
{
|
|
10310
|
+
error: "auth_unavailable",
|
|
10311
|
+
mode: cfg.planner.auth?.anthropic ?? "auto",
|
|
10312
|
+
detail: err2.message
|
|
10313
|
+
},
|
|
10314
|
+
400
|
|
10315
|
+
);
|
|
10316
|
+
}
|
|
10317
|
+
throw err2;
|
|
9158
10318
|
}
|
|
9159
10319
|
const stories = listStories(opts.paths, { feature: body.data.feature });
|
|
9160
10320
|
const story = stories.find((s) => s.id === body.data.storyId);
|
|
@@ -9176,7 +10336,7 @@ function mountRunsApi(app, opts) {
|
|
|
9176
10336
|
const draftRuntime = resolveRuntime({
|
|
9177
10337
|
provider: planner.provider,
|
|
9178
10338
|
modelId,
|
|
9179
|
-
|
|
10339
|
+
auth: auth2,
|
|
9180
10340
|
anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
|
|
9181
10341
|
});
|
|
9182
10342
|
const anthropicProviderSpecific = planner.provider === "anthropic" ? {
|
|
@@ -9202,7 +10362,7 @@ function mountRunsApi(app, opts) {
|
|
|
9202
10362
|
scoutRuntime = resolveRuntime({
|
|
9203
10363
|
provider: planner.provider,
|
|
9204
10364
|
modelId: scoutModelId,
|
|
9205
|
-
|
|
10365
|
+
auth: auth2,
|
|
9206
10366
|
anthropicRuntime: planner.provider === "anthropic" ? anthropicRuntimeChoice : void 0
|
|
9207
10367
|
});
|
|
9208
10368
|
}
|
|
@@ -9223,7 +10383,7 @@ function mountRunsApi(app, opts) {
|
|
|
9223
10383
|
repoMap
|
|
9224
10384
|
});
|
|
9225
10385
|
const userPrompt = composeUserPrompt({
|
|
9226
|
-
intakeContent:
|
|
10386
|
+
intakeContent: fs43.readFileSync(story.intakePath, "utf8")
|
|
9227
10387
|
});
|
|
9228
10388
|
let result;
|
|
9229
10389
|
try {
|
|
@@ -9233,6 +10393,7 @@ function mountRunsApi(app, opts) {
|
|
|
9233
10393
|
runtime: draftRuntime,
|
|
9234
10394
|
provider: planner.provider,
|
|
9235
10395
|
modelId,
|
|
10396
|
+
auth: auth2,
|
|
9236
10397
|
anthropicProviderSpecific,
|
|
9237
10398
|
systemPrompt,
|
|
9238
10399
|
userPrompt,
|
|
@@ -9302,7 +10463,7 @@ function mountRunsApi(app, opts) {
|
|
|
9302
10463
|
metadataHeader: header,
|
|
9303
10464
|
partial: !runSuccess
|
|
9304
10465
|
});
|
|
9305
|
-
planFile =
|
|
10466
|
+
planFile = path34.relative(opts.paths.root, out.planFile);
|
|
9306
10467
|
}
|
|
9307
10468
|
try {
|
|
9308
10469
|
await writeLastRun(opts.paths, {
|
|
@@ -9338,6 +10499,7 @@ function mountRunsApi(app, opts) {
|
|
|
9338
10499
|
durationMs: result.validation?.durationMs
|
|
9339
10500
|
},
|
|
9340
10501
|
plannerRuntime: { kind: draftRuntime.kind, provider: planner.provider },
|
|
10502
|
+
authMode: describeAuth(auth2).mode,
|
|
9341
10503
|
providerOptionsSnapshot: anthropicProviderSpecific ? { anthropic: anthropicProviderSpecific } : void 0
|
|
9342
10504
|
});
|
|
9343
10505
|
} catch {
|
|
@@ -9469,7 +10631,13 @@ function mountSecretsApi(app, opts) {
|
|
|
9469
10631
|
planner: {
|
|
9470
10632
|
anthropic: maskToken(s.planner?.anthropic),
|
|
9471
10633
|
openai: maskToken(s.planner?.openai),
|
|
9472
|
-
google: maskToken(s.planner?.google)
|
|
10634
|
+
google: maskToken(s.planner?.google),
|
|
10635
|
+
/**
|
|
10636
|
+
* `claude setup-token` output. Masked exactly like the API keys — the Claude account
|
|
10637
|
+
* card shows it so a user can tell "a token is stored here" from "the OS login store
|
|
10638
|
+
* is being used". Never returned in full, and never by `/api/planner-auth`.
|
|
10639
|
+
*/
|
|
10640
|
+
anthropicOauthToken: maskToken(s.planner?.anthropicOauthToken)
|
|
9473
10641
|
},
|
|
9474
10642
|
tracker: {
|
|
9475
10643
|
jira: {
|
|
@@ -9493,7 +10661,8 @@ function mountSecretsApi(app, opts) {
|
|
|
9493
10661
|
planner: z10.object({
|
|
9494
10662
|
anthropic: z10.string().optional(),
|
|
9495
10663
|
openai: z10.string().optional(),
|
|
9496
|
-
google: z10.string().optional()
|
|
10664
|
+
google: z10.string().optional(),
|
|
10665
|
+
anthropicOauthToken: z10.string().optional()
|
|
9497
10666
|
}).optional(),
|
|
9498
10667
|
tracker: z10.object({
|
|
9499
10668
|
jira: z10.object({
|
|
@@ -9517,6 +10686,9 @@ function mountSecretsApi(app, opts) {
|
|
|
9517
10686
|
if (!parse.success) return c.json({ error: "invalid_body", issues: parse.error.issues }, 400);
|
|
9518
10687
|
const current = loadSecrets(opts.paths.secretsFile);
|
|
9519
10688
|
const merged = mergeSecrets(current, parse.data);
|
|
10689
|
+
if (parse.data.planner?.anthropicOauthToken === "" && merged.planner) {
|
|
10690
|
+
delete merged.planner.anthropicOauthToken;
|
|
10691
|
+
}
|
|
9520
10692
|
saveSecrets(opts.paths.secretsFile, merged);
|
|
9521
10693
|
return c.json({ ok: true });
|
|
9522
10694
|
});
|
|
@@ -9547,13 +10719,166 @@ function mountSecretsApi(app, opts) {
|
|
|
9547
10719
|
});
|
|
9548
10720
|
}
|
|
9549
10721
|
|
|
9550
|
-
// src/console/api/
|
|
10722
|
+
// src/console/api/planner-auth.ts
|
|
9551
10723
|
import { z as z11 } from "zod";
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
10724
|
+
|
|
10725
|
+
// src/commands/auth/status.ts
|
|
10726
|
+
async function runAuthStatus(opts = {}) {
|
|
10727
|
+
const paths = buildPaths(requireSquadRoot());
|
|
10728
|
+
const config2 = loadConfig(paths.configFile);
|
|
10729
|
+
const planner = config2.planner;
|
|
10730
|
+
const provider = planner?.provider ?? "anthropic";
|
|
10731
|
+
const auth2 = resolvePlannerAuthForCwd(provider, planner);
|
|
10732
|
+
const described = describeAuth(auth2);
|
|
10733
|
+
const probe = shouldProbe(opts, provider) ? await probeClaudeAuth(auth2) : void 0;
|
|
10734
|
+
const payload = buildPayload2({ auth: described, provider, planner, probe, fallbackLoggedIn: credentialPresent(auth2) });
|
|
10735
|
+
if (opts.json) {
|
|
10736
|
+
process.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
10737
|
+
`);
|
|
10738
|
+
return;
|
|
10739
|
+
}
|
|
10740
|
+
const kw2 = 15;
|
|
10741
|
+
step("squad-kit auth");
|
|
10742
|
+
blank();
|
|
10743
|
+
kv("auth mode", `${payload.mode} (${payload.reason})`, kw2);
|
|
10744
|
+
kv("credential", payload.credentialHint, kw2);
|
|
10745
|
+
if (payload.account) {
|
|
10746
|
+
const account = [payload.account.email, payload.account.organization, payload.account.subscriptionType].filter(Boolean).join(" \xB7 ");
|
|
10747
|
+
if (account) kv("account", account, kw2);
|
|
10748
|
+
}
|
|
10749
|
+
if (payload.apiKeySource) kv("api key source", payload.apiKeySource, kw2);
|
|
10750
|
+
kv("runtime", payload.runtime, kw2);
|
|
10751
|
+
if (probe && !probe.ok) {
|
|
10752
|
+
blank();
|
|
10753
|
+
warning(`Live check failed (${probe.kind}).`);
|
|
10754
|
+
info(probe.detail);
|
|
10755
|
+
if (probe.kind === "not-logged-in" || probe.kind === "expired") {
|
|
10756
|
+
info("Run `squad auth login` to sign in again.");
|
|
10757
|
+
}
|
|
10758
|
+
} else if (!probe) {
|
|
10759
|
+
blank();
|
|
10760
|
+
info("Account details skipped (offline). Re-run without --offline to check the live login.");
|
|
10761
|
+
}
|
|
10762
|
+
}
|
|
10763
|
+
function shouldProbe(opts, provider) {
|
|
10764
|
+
if (opts.offline) return false;
|
|
10765
|
+
if (skipExternalProbesInAutomation()) return false;
|
|
10766
|
+
return provider === "anthropic";
|
|
10767
|
+
}
|
|
10768
|
+
function buildPayload2(input5) {
|
|
10769
|
+
const { auth: auth2, provider, planner, probe } = input5;
|
|
10770
|
+
const payload = {
|
|
10771
|
+
mode: auth2.mode,
|
|
10772
|
+
reason: authReasonText(auth2.reason, provider),
|
|
10773
|
+
credentialHint: auth2.credentialHint,
|
|
10774
|
+
runtime: runtimeNameFor(provider, planner),
|
|
10775
|
+
// Without a probe the honest answer is "a credential resolved locally"; with one, the live
|
|
10776
|
+
// result wins. Either way the key is always present so scripts can rely on the shape.
|
|
10777
|
+
loggedIn: probe ? probe.ok : input5.fallbackLoggedIn
|
|
10778
|
+
};
|
|
10779
|
+
if (probe?.ok) {
|
|
10780
|
+
if (probe.account) payload.account = probe.account;
|
|
10781
|
+
if (probe.apiKeySource) payload.apiKeySource = probe.apiKeySource;
|
|
10782
|
+
}
|
|
10783
|
+
return payload;
|
|
10784
|
+
}
|
|
10785
|
+
|
|
10786
|
+
// src/console/api/planner-auth.ts
|
|
10787
|
+
var PLANNER_AUTH_LOGIN_COMMAND = "squad auth login";
|
|
10788
|
+
var PROBE_CACHE_TTL_MS = 6e4;
|
|
10789
|
+
var probeCache;
|
|
10790
|
+
function probeCacheKey(auth2) {
|
|
10791
|
+
const described = describeAuth(auth2);
|
|
10792
|
+
return `${described.mode}:${described.credentialHint}`;
|
|
10793
|
+
}
|
|
10794
|
+
async function probeWithCache(auth2, now) {
|
|
10795
|
+
const key = probeCacheKey(auth2);
|
|
10796
|
+
if (probeCache && probeCache.key === key && now - probeCache.at < PROBE_CACHE_TTL_MS) {
|
|
10797
|
+
return probeCache.result;
|
|
10798
|
+
}
|
|
10799
|
+
const result = await probeClaudeAuth(auth2);
|
|
10800
|
+
if (result.ok) probeCache = { key, at: now, result };
|
|
10801
|
+
return result;
|
|
10802
|
+
}
|
|
10803
|
+
function resetPlannerAuthProbeCache() {
|
|
10804
|
+
probeCache = void 0;
|
|
10805
|
+
}
|
|
10806
|
+
function subscriptionRuntimeConflict(provider, planner) {
|
|
10807
|
+
if (provider !== "anthropic") return plannerAuthRuntimeMismatchMessage(provider);
|
|
10808
|
+
if (runtimeNameFor(provider, planner) === "vercel") return plannerAuthRuntimeMismatchMessage(provider);
|
|
10809
|
+
return null;
|
|
10810
|
+
}
|
|
10811
|
+
function mountPlannerAuthApi(app, opts) {
|
|
10812
|
+
app.get("/api/planner-auth", async (c) => {
|
|
10813
|
+
const config2 = loadConfig(opts.paths.configFile);
|
|
10814
|
+
const planner = config2.planner;
|
|
10815
|
+
const provider = planner?.provider ?? "anthropic";
|
|
10816
|
+
const configuredMode = planner?.auth?.anthropic ?? "auto";
|
|
10817
|
+
const loginProbe = detectClaudeLogin(loadSecrets(opts.paths.secretsFile));
|
|
10818
|
+
const binary = resolveClaudeBinary();
|
|
10819
|
+
let auth2;
|
|
10820
|
+
let error = null;
|
|
10821
|
+
try {
|
|
10822
|
+
auth2 = resolvePlannerAuthForPaths(opts.paths, provider, planner);
|
|
10823
|
+
} catch (err2) {
|
|
10824
|
+
if (!(err2 instanceof PlannerAuthUnavailableError)) throw err2;
|
|
10825
|
+
error = err2.message;
|
|
10826
|
+
}
|
|
10827
|
+
const wantProbe = c.req.query("probe") === "1" && provider === "anthropic" && auth2 !== void 0;
|
|
10828
|
+
const probe = wantProbe ? await probeWithCache(auth2, Date.now()) : void 0;
|
|
10829
|
+
const status = auth2 ? buildPayload2({
|
|
10830
|
+
auth: describeAuth(auth2),
|
|
10831
|
+
provider,
|
|
10832
|
+
planner,
|
|
10833
|
+
probe,
|
|
10834
|
+
fallbackLoggedIn: credentialPresent(auth2)
|
|
10835
|
+
}) : void 0;
|
|
10836
|
+
const payload = {
|
|
10837
|
+
provider,
|
|
10838
|
+
configuredMode,
|
|
10839
|
+
resolved: status ? { mode: status.mode, reason: status.reason, credentialHint: status.credentialHint } : null,
|
|
10840
|
+
error,
|
|
10841
|
+
login: { present: loginProbe.present, hint: loginProbe.hint, detail: loginProbe.detail },
|
|
10842
|
+
account: status?.account ?? null,
|
|
10843
|
+
apiKeySource: status?.apiKeySource ?? null,
|
|
10844
|
+
runtime: runtimeNameFor(provider, planner) === "vercel" ? "vercel" : "agent-sdk",
|
|
10845
|
+
binary: { found: Boolean(binary), source: binary?.source ?? null },
|
|
10846
|
+
loginCommand: PLANNER_AUTH_LOGIN_COMMAND,
|
|
10847
|
+
loggedIn: status?.loggedIn ?? false,
|
|
10848
|
+
probe: probe ? { ran: true, ok: probe.ok, ...probe.ok ? {} : { kind: probe.kind, detail: probe.detail } } : null,
|
|
10849
|
+
runtimeConflict: subscriptionRuntimeConflict(provider, planner)
|
|
10850
|
+
};
|
|
10851
|
+
return c.json(payload);
|
|
10852
|
+
});
|
|
10853
|
+
const ModeBody = z11.object({ mode: z11.string() });
|
|
10854
|
+
app.post("/api/planner-auth/mode", async (c) => {
|
|
10855
|
+
const parsed = ModeBody.safeParse(await c.req.json().catch(() => null));
|
|
10856
|
+
if (!parsed.success) return c.json({ error: "invalid_body", issues: parsed.error.issues }, 400);
|
|
10857
|
+
const config2 = loadConfig(opts.paths.configFile);
|
|
10858
|
+
const next = {
|
|
10859
|
+
...config2,
|
|
10860
|
+
planner: {
|
|
10861
|
+
...config2.planner ?? {},
|
|
10862
|
+
auth: { ...config2.planner?.auth ?? {}, anthropic: parsed.data.mode }
|
|
10863
|
+
}
|
|
10864
|
+
};
|
|
10865
|
+
try {
|
|
10866
|
+
saveConfig(opts.paths.configFile, next);
|
|
10867
|
+
} catch (err2) {
|
|
10868
|
+
return c.json({ error: "invalid_auth_mode", detail: err2.message }, 400);
|
|
10869
|
+
}
|
|
10870
|
+
resetPlannerAuthProbeCache();
|
|
10871
|
+
return c.json({ ok: true, mode: parsed.data.mode });
|
|
10872
|
+
});
|
|
10873
|
+
}
|
|
10874
|
+
|
|
10875
|
+
// src/console/api/tracker.ts
|
|
10876
|
+
import { z as z12 } from "zod";
|
|
10877
|
+
var SearchQuery = z12.object({ q: z12.string().max(500) });
|
|
10878
|
+
var ImportBody = z12.object({
|
|
10879
|
+
issueId: z12.string().min(1),
|
|
10880
|
+
feature: z12.string().min(1).max(80),
|
|
10881
|
+
withAttachments: z12.boolean().optional()
|
|
9557
10882
|
});
|
|
9558
10883
|
function mountTrackerApi(app, opts) {
|
|
9559
10884
|
app.get("/api/tracker/search", async (c) => {
|
|
@@ -9694,17 +11019,17 @@ function countStories(stories) {
|
|
|
9694
11019
|
}
|
|
9695
11020
|
|
|
9696
11021
|
// src/console/api/projects.ts
|
|
9697
|
-
import
|
|
9698
|
-
import
|
|
9699
|
-
import
|
|
11022
|
+
import fs44 from "fs/promises";
|
|
11023
|
+
import path35 from "path";
|
|
11024
|
+
import os2 from "os";
|
|
9700
11025
|
var MAX = 10;
|
|
9701
11026
|
function recentFilePath() {
|
|
9702
|
-
return
|
|
11027
|
+
return path35.join(os2.homedir(), ".squad", "recent-projects.json");
|
|
9703
11028
|
}
|
|
9704
11029
|
async function loadRecent() {
|
|
9705
11030
|
const RECENT_FILE = recentFilePath();
|
|
9706
11031
|
try {
|
|
9707
|
-
const raw = await
|
|
11032
|
+
const raw = await fs44.readFile(RECENT_FILE, "utf8");
|
|
9708
11033
|
const parsed = JSON.parse(raw);
|
|
9709
11034
|
return Array.isArray(parsed) ? parsed.slice(0, MAX) : [];
|
|
9710
11035
|
} catch {
|
|
@@ -9713,8 +11038,8 @@ async function loadRecent() {
|
|
|
9713
11038
|
}
|
|
9714
11039
|
async function saveRecent(list) {
|
|
9715
11040
|
const RECENT_FILE = recentFilePath();
|
|
9716
|
-
await
|
|
9717
|
-
await
|
|
11041
|
+
await fs44.mkdir(path35.dirname(RECENT_FILE), { recursive: true });
|
|
11042
|
+
await fs44.writeFile(RECENT_FILE, JSON.stringify(list.slice(0, MAX), null, 2) + "\n", "utf8");
|
|
9718
11043
|
}
|
|
9719
11044
|
function mountProjectsApi(app, opts) {
|
|
9720
11045
|
app.get("/api/projects/recent", async (c) => c.json(await loadRecent()));
|
|
@@ -9737,14 +11062,15 @@ function mountApi(app, opts) {
|
|
|
9737
11062
|
mountConfigApi(app, opts);
|
|
9738
11063
|
mountRunsApi(app, opts);
|
|
9739
11064
|
mountSecretsApi(app, opts);
|
|
11065
|
+
mountPlannerAuthApi(app, opts);
|
|
9740
11066
|
mountTrackerApi(app, opts);
|
|
9741
11067
|
mountDoctorApi(app, opts);
|
|
9742
11068
|
mountCopyPlanPromptApi(app, opts);
|
|
9743
11069
|
}
|
|
9744
11070
|
|
|
9745
11071
|
// src/console/static-assets.ts
|
|
9746
|
-
import
|
|
9747
|
-
import
|
|
11072
|
+
import fs45 from "fs";
|
|
11073
|
+
import path36 from "path";
|
|
9748
11074
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
9749
11075
|
import { Readable } from "stream";
|
|
9750
11076
|
function pathnameForStatic(c) {
|
|
@@ -9774,18 +11100,18 @@ function mountStaticAssets(app, _opts) {
|
|
|
9774
11100
|
}
|
|
9775
11101
|
if (c.req.path.startsWith("/api/") || c.req.path.startsWith("/healthz")) return c.notFound();
|
|
9776
11102
|
if (!distDir) return c.text("UI not built. Run `pnpm -C console-ui build`.", 404);
|
|
9777
|
-
const indexHtml =
|
|
9778
|
-
if (!
|
|
9779
|
-
return c.html(
|
|
11103
|
+
const indexHtml = path36.join(distDir, "index.html");
|
|
11104
|
+
if (!fs45.existsSync(indexHtml)) return c.text("UI not built", 404);
|
|
11105
|
+
return c.html(fs45.readFileSync(indexHtml, "utf8"), 200, { "cache-control": "no-store" });
|
|
9780
11106
|
});
|
|
9781
11107
|
}
|
|
9782
11108
|
function assetResponse(c, distDir, pathname) {
|
|
9783
11109
|
if (!distDir) return c.text("UI not built", 404);
|
|
9784
11110
|
const rel = pathname.replace(/^\//, "");
|
|
9785
|
-
const abs =
|
|
11111
|
+
const abs = path36.normalize(path36.join(distDir, rel));
|
|
9786
11112
|
if (!isInside2(abs, distDir)) return c.text("forbidden", 403);
|
|
9787
|
-
if (!
|
|
9788
|
-
const body = Readable.toWeb(
|
|
11113
|
+
if (!fs45.existsSync(abs) || !fs45.statSync(abs).isFile()) return c.text("not found", 404);
|
|
11114
|
+
const body = Readable.toWeb(fs45.createReadStream(abs));
|
|
9789
11115
|
return new Response(body, {
|
|
9790
11116
|
headers: {
|
|
9791
11117
|
"content-type": contentTypeFor(abs),
|
|
@@ -9795,9 +11121,9 @@ function assetResponse(c, distDir, pathname) {
|
|
|
9795
11121
|
}
|
|
9796
11122
|
function serveFile(c, distDir, name, ct) {
|
|
9797
11123
|
if (!distDir) return c.text("UI not built", 404);
|
|
9798
|
-
const abs =
|
|
9799
|
-
if (!
|
|
9800
|
-
return new Response(
|
|
11124
|
+
const abs = path36.join(distDir, name);
|
|
11125
|
+
if (!fs45.existsSync(abs)) return c.text("not found", 404);
|
|
11126
|
+
return new Response(fs45.readFileSync(abs), {
|
|
9801
11127
|
headers: { "content-type": ct, "cache-control": "public, max-age=86400" }
|
|
9802
11128
|
});
|
|
9803
11129
|
}
|
|
@@ -9812,13 +11138,13 @@ function contentTypeFor(p) {
|
|
|
9812
11138
|
return "application/octet-stream";
|
|
9813
11139
|
}
|
|
9814
11140
|
function isInside2(abs, parent) {
|
|
9815
|
-
const a =
|
|
9816
|
-
const p =
|
|
11141
|
+
const a = path36.resolve(abs) + path36.sep;
|
|
11142
|
+
const p = path36.resolve(parent) + path36.sep;
|
|
9817
11143
|
return a.startsWith(p);
|
|
9818
11144
|
}
|
|
9819
11145
|
function resolveConsoleUiDist() {
|
|
9820
11146
|
const fromEnv = process.env.SQUAD_CONSOLE_UI_DIST;
|
|
9821
|
-
if (fromEnv &&
|
|
11147
|
+
if (fromEnv && fs45.existsSync(path36.join(fromEnv, "index.html"))) return fromEnv;
|
|
9822
11148
|
const candidates = [
|
|
9823
11149
|
fileURLToPath2(new URL("./console-ui/", import.meta.url)),
|
|
9824
11150
|
// dist/cli.js → dist/console-ui/
|
|
@@ -9830,7 +11156,7 @@ function resolveConsoleUiDist() {
|
|
|
9830
11156
|
// src/console/*.ts → project/console-ui/dist
|
|
9831
11157
|
];
|
|
9832
11158
|
for (const c of candidates) {
|
|
9833
|
-
if (
|
|
11159
|
+
if (fs45.existsSync(path36.join(c, "index.html"))) return c;
|
|
9834
11160
|
}
|
|
9835
11161
|
return null;
|
|
9836
11162
|
}
|
|
@@ -9846,9 +11172,9 @@ function authMiddleware(expected) {
|
|
|
9846
11172
|
};
|
|
9847
11173
|
}
|
|
9848
11174
|
function readPresentedToken(req) {
|
|
9849
|
-
const
|
|
9850
|
-
if (
|
|
9851
|
-
return
|
|
11175
|
+
const auth2 = req.headers.get("authorization");
|
|
11176
|
+
if (auth2 && auth2.toLowerCase().startsWith("bearer ")) {
|
|
11177
|
+
return auth2.slice("bearer ".length).trim();
|
|
9852
11178
|
}
|
|
9853
11179
|
const url = new URL(req.url);
|
|
9854
11180
|
return url.searchParams.get("t");
|
|
@@ -10012,18 +11338,18 @@ async function runConsole(opts) {
|
|
|
10012
11338
|
}
|
|
10013
11339
|
|
|
10014
11340
|
// src/commands/rm/story.ts
|
|
10015
|
-
import
|
|
10016
|
-
import
|
|
10017
|
-
import { confirm as
|
|
11341
|
+
import fs47 from "fs";
|
|
11342
|
+
import path38 from "path";
|
|
11343
|
+
import { confirm as confirm11 } from "@inquirer/prompts";
|
|
10018
11344
|
|
|
10019
11345
|
// src/commands/rm/feature.ts
|
|
10020
|
-
import
|
|
10021
|
-
import
|
|
10022
|
-
import { confirm as
|
|
11346
|
+
import fs46 from "fs";
|
|
11347
|
+
import path37 from "path";
|
|
11348
|
+
import { confirm as confirm10 } from "@inquirer/prompts";
|
|
10023
11349
|
function countStoryDirs(storiesDir, feature) {
|
|
10024
|
-
const d =
|
|
10025
|
-
if (!
|
|
10026
|
-
return
|
|
11350
|
+
const d = path37.join(storiesDir, feature);
|
|
11351
|
+
if (!fs46.existsSync(d)) return 0;
|
|
11352
|
+
return fs46.readdirSync(d, { withFileTypes: true }).filter((x) => x.isDirectory()).length;
|
|
10027
11353
|
}
|
|
10028
11354
|
async function runRmFeature(featureSlug, opts = {}) {
|
|
10029
11355
|
const root = requireSquadRoot();
|
|
@@ -10057,17 +11383,17 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
10057
11383
|
feature = await pickFeatureInteractive(paths);
|
|
10058
11384
|
}
|
|
10059
11385
|
}
|
|
10060
|
-
const storyDir =
|
|
10061
|
-
const planDir =
|
|
11386
|
+
const storyDir = path37.join(paths.storiesDir, feature);
|
|
11387
|
+
const planDir = path37.join(paths.plansDir, feature);
|
|
10062
11388
|
const nStories = countStoryDirs(paths.storiesDir, feature);
|
|
10063
11389
|
const nPlans = listAllPlanListEntries(paths, feature).length;
|
|
10064
|
-
const hasOverview =
|
|
11390
|
+
const hasOverview = fs46.existsSync(path37.join(planDir, "00-overview.md")) ? 1 : 0;
|
|
10065
11391
|
const targets = [];
|
|
10066
|
-
if (
|
|
10067
|
-
targets.push({ label:
|
|
11392
|
+
if (fs46.existsSync(storyDir)) {
|
|
11393
|
+
targets.push({ label: path37.relative(root, storyDir), absPath: storyDir, kind: "dir" });
|
|
10068
11394
|
}
|
|
10069
|
-
if (
|
|
10070
|
-
targets.push({ label:
|
|
11395
|
+
if (fs46.existsSync(planDir)) {
|
|
11396
|
+
targets.push({ label: path37.relative(root, planDir), absPath: planDir, kind: "dir" });
|
|
10071
11397
|
}
|
|
10072
11398
|
divider("squad rm feature");
|
|
10073
11399
|
info(`Feature: ${feature}`);
|
|
@@ -10082,7 +11408,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
10082
11408
|
kv(t.kind, t.label, 6);
|
|
10083
11409
|
}
|
|
10084
11410
|
if (useTrash) {
|
|
10085
|
-
info(`Items will move to ${
|
|
11411
|
+
info(`Items will move to ${path37.relative(process.cwd(), paths.trashDir)}/<timestamp>/`);
|
|
10086
11412
|
} else if (!dryRun) {
|
|
10087
11413
|
warning("This is a permanent delete. Pass --trash to move to .squad/.trash/ instead.");
|
|
10088
11414
|
}
|
|
@@ -10100,7 +11426,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
10100
11426
|
"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
11427
|
);
|
|
10102
11428
|
}
|
|
10103
|
-
const ok = await
|
|
11429
|
+
const ok = await confirm10({
|
|
10104
11430
|
message: `Delete all of feature "${feature}" (stories + plans)?`,
|
|
10105
11431
|
default: false
|
|
10106
11432
|
});
|
|
@@ -10115,7 +11441,7 @@ async function runRmFeature(featureSlug, opts = {}) {
|
|
|
10115
11441
|
useTrash
|
|
10116
11442
|
);
|
|
10117
11443
|
if (result.trashed) {
|
|
10118
|
-
success(`Feature removed. Items moved to ${
|
|
11444
|
+
success(`Feature removed. Items moved to ${path37.relative(root, result.trashed)}.`);
|
|
10119
11445
|
} else {
|
|
10120
11446
|
success("Feature removed (stories and plans directories).");
|
|
10121
11447
|
}
|
|
@@ -10156,11 +11482,11 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
10156
11482
|
}
|
|
10157
11483
|
}
|
|
10158
11484
|
const targets = [
|
|
10159
|
-
{ label:
|
|
11485
|
+
{ label: path38.relative(root, story.storyDir), absPath: story.storyDir, kind: "dir" }
|
|
10160
11486
|
];
|
|
10161
11487
|
if (story.planFile) {
|
|
10162
|
-
const planAbs =
|
|
10163
|
-
targets.push({ label:
|
|
11488
|
+
const planAbs = path38.join(paths.plansDir, story.feature, story.planFile);
|
|
11489
|
+
targets.push({ label: path38.relative(root, planAbs), absPath: planAbs, kind: "file" });
|
|
10164
11490
|
}
|
|
10165
11491
|
renderPreview("story", targets, {
|
|
10166
11492
|
dryRun,
|
|
@@ -10177,7 +11503,7 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
10177
11503
|
"Pass --yes to confirm deletion in a non-interactive environment, or run `squad rm story` in a TTY to confirm the prompt interactively."
|
|
10178
11504
|
);
|
|
10179
11505
|
}
|
|
10180
|
-
const ok = await
|
|
11506
|
+
const ok = await confirm11({ message: "Delete this story and related plan/overview row?", default: false });
|
|
10181
11507
|
if (!ok) {
|
|
10182
11508
|
info("Cancelled.");
|
|
10183
11509
|
return;
|
|
@@ -10185,18 +11511,18 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
10185
11511
|
}
|
|
10186
11512
|
const result = deleteStoryRecord({ paths, story, trash: useTrash });
|
|
10187
11513
|
if (result.trashed) {
|
|
10188
|
-
success(`Removed (moved to ${
|
|
11514
|
+
success(`Removed (moved to ${path38.relative(root, result.trashed)}).`);
|
|
10189
11515
|
} else {
|
|
10190
11516
|
success("Removed story folder, plan file (if any), and overview row.");
|
|
10191
11517
|
}
|
|
10192
|
-
const featurePlanDir =
|
|
10193
|
-
const overviewPath =
|
|
10194
|
-
if (
|
|
10195
|
-
const body =
|
|
11518
|
+
const featurePlanDir = path38.join(paths.plansDir, story.feature);
|
|
11519
|
+
const overviewPath = path38.join(featurePlanDir, "00-overview.md");
|
|
11520
|
+
if (fs47.existsSync(overviewPath)) {
|
|
11521
|
+
const body = fs47.readFileSync(overviewPath, "utf8");
|
|
10196
11522
|
const onlyShell = countOverviewDataRows(body) === 0;
|
|
10197
11523
|
if (onlyShell && featureStoriesDirEmpty(paths, story.feature)) {
|
|
10198
11524
|
if (canPrompt) {
|
|
10199
|
-
const also = await
|
|
11525
|
+
const also = await confirm11({
|
|
10200
11526
|
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
11527
|
default: false
|
|
10202
11528
|
});
|
|
@@ -10212,8 +11538,8 @@ async function runRmStory(storyPathOrId, opts = {}) {
|
|
|
10212
11538
|
}
|
|
10213
11539
|
|
|
10214
11540
|
// src/commands/rm/plan.ts
|
|
10215
|
-
import
|
|
10216
|
-
import { confirm as
|
|
11541
|
+
import path39 from "path";
|
|
11542
|
+
import { confirm as confirm12 } from "@inquirer/prompts";
|
|
10217
11543
|
async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
10218
11544
|
const root = requireSquadRoot();
|
|
10219
11545
|
const paths = buildPaths(root);
|
|
@@ -10248,7 +11574,7 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
10248
11574
|
}
|
|
10249
11575
|
const storyRows = listStories(paths, { feature: plan.feature });
|
|
10250
11576
|
const linked = storyRows.find((s) => s.planFile === plan.file);
|
|
10251
|
-
const targets = [{ label:
|
|
11577
|
+
const targets = [{ label: path39.relative(root, plan.abs), absPath: plan.abs, kind: "file" }];
|
|
10252
11578
|
renderPreview("plan", targets, {
|
|
10253
11579
|
dryRun,
|
|
10254
11580
|
trash: useTrash,
|
|
@@ -10264,18 +11590,54 @@ async function runRmPlan(planPathOrSequence, opts = {}) {
|
|
|
10264
11590
|
"Pass --yes to confirm deletion in a non-interactive environment, or run `squad rm plan` in a TTY."
|
|
10265
11591
|
);
|
|
10266
11592
|
}
|
|
10267
|
-
const ok = await
|
|
11593
|
+
const ok = await confirm12({ message: "Delete this plan file only? (intake and config are kept.)", default: false });
|
|
10268
11594
|
if (!ok) {
|
|
10269
11595
|
info("Cancelled.");
|
|
10270
11596
|
return;
|
|
10271
11597
|
}
|
|
10272
11598
|
}
|
|
10273
11599
|
trashOrDelete([plan.abs], paths.trashDir, useTrash);
|
|
10274
|
-
removeOverviewRowForPlanFile(
|
|
10275
|
-
const intakeHint = linked ?
|
|
11600
|
+
removeOverviewRowForPlanFile(path39.join(paths.plansDir, plan.feature), plan.file);
|
|
11601
|
+
const intakeHint = linked ? path39.relative(root, linked.intakePath) : `under .squad/stories/${plan.feature}/`;
|
|
10276
11602
|
success(`Plan removed. Run \`squad new-plan ${intakeHint}\` to regenerate.`);
|
|
10277
11603
|
}
|
|
10278
11604
|
|
|
11605
|
+
// src/commands/auth/logout.ts
|
|
11606
|
+
import fs48 from "fs";
|
|
11607
|
+
import { confirm as confirm13 } from "@inquirer/prompts";
|
|
11608
|
+
async function runAuthLogout(opts = {}) {
|
|
11609
|
+
const paths = buildPaths(requireSquadRoot());
|
|
11610
|
+
const secrets = fs48.existsSync(paths.secretsFile) ? loadSecrets(paths.secretsFile) : {};
|
|
11611
|
+
if (!secrets.planner?.anthropicOauthToken) {
|
|
11612
|
+
info("No Claude OAuth token is stored in .squad/secrets.yaml \u2014 nothing to remove.");
|
|
11613
|
+
info("Your Claude Code login (OS credential store) is separate and was not touched.");
|
|
11614
|
+
return;
|
|
11615
|
+
}
|
|
11616
|
+
if (!opts.yes) {
|
|
11617
|
+
if (!isInteractive()) {
|
|
11618
|
+
warning("Pass -y in non-interactive mode to confirm, or run `squad auth logout` in a TTY.");
|
|
11619
|
+
return;
|
|
11620
|
+
}
|
|
11621
|
+
const ok = await confirm13({
|
|
11622
|
+
message: "Remove the Claude OAuth token squad-kit stored in .squad/secrets.yaml?",
|
|
11623
|
+
default: false
|
|
11624
|
+
});
|
|
11625
|
+
if (!ok) return;
|
|
11626
|
+
}
|
|
11627
|
+
delete secrets.planner.anthropicOauthToken;
|
|
11628
|
+
saveSecrets(paths.secretsFile, secrets);
|
|
11629
|
+
success("Removed the OAuth token squad-kit stored.");
|
|
11630
|
+
info(
|
|
11631
|
+
"Your Claude Code login is untouched \u2014 run `claude` and `/logout` if you also want to sign out of Claude Code."
|
|
11632
|
+
);
|
|
11633
|
+
const mode = loadConfig(paths.configFile).planner?.auth?.anthropic;
|
|
11634
|
+
if (mode === "subscription") {
|
|
11635
|
+
info(
|
|
11636
|
+
"`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`."
|
|
11637
|
+
);
|
|
11638
|
+
}
|
|
11639
|
+
}
|
|
11640
|
+
|
|
10279
11641
|
// src/cli.ts
|
|
10280
11642
|
var program = new Command();
|
|
10281
11643
|
program.name("squad").description("Plan once, execute cheap. A 3-step SDD workflow CLI.").version(readInstalledPackage().version);
|
|
@@ -10344,6 +11706,14 @@ rm.command("plan [plan-path-or-sequence]").description("Delete a single plan fil
|
|
|
10344
11706
|
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
11707
|
var tracker = program.command("tracker").description("Tracker id helpers");
|
|
10346
11708
|
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));
|
|
11709
|
+
var auth = program.command("auth").description(
|
|
11710
|
+
"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"
|
|
11711
|
+
);
|
|
11712
|
+
auth.command("login").description(
|
|
11713
|
+
"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`."
|
|
11714
|
+
).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));
|
|
11715
|
+
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));
|
|
11716
|
+
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
11717
|
var config = program.command("config").description("Inspect and change .squad/ settings");
|
|
10348
11718
|
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
11719
|
var set = config.command("set").description("Interactively change a section");
|