gsd-pi 2.38.0-dev.63ad7e5 → 2.38.0-dev.7209774
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/resource-loader.js +34 -1
- package/dist/resources/extensions/browser-tools/index.js +3 -1
- package/dist/resources/extensions/browser-tools/tools/verify.js +97 -0
- package/dist/resources/extensions/github-sync/cli.js +284 -0
- package/dist/resources/extensions/github-sync/index.js +73 -0
- package/dist/resources/extensions/github-sync/mapping.js +67 -0
- package/dist/resources/extensions/github-sync/sync.js +424 -0
- package/dist/resources/extensions/github-sync/templates.js +118 -0
- package/dist/resources/extensions/github-sync/types.js +7 -0
- package/dist/resources/extensions/gsd/auto-dispatch.js +1 -1
- package/dist/resources/extensions/gsd/auto-loop.js +593 -516
- package/dist/resources/extensions/gsd/auto-post-unit.js +28 -3
- package/dist/resources/extensions/gsd/auto-prompts.js +26 -15
- package/dist/resources/extensions/gsd/auto-worktree.js +3 -3
- package/dist/resources/extensions/gsd/commands.js +2 -1
- package/dist/resources/extensions/gsd/doctor.js +20 -1
- package/dist/resources/extensions/gsd/exit-command.js +2 -1
- package/dist/resources/extensions/gsd/files.js +4 -0
- package/dist/resources/extensions/gsd/git-service.js +30 -12
- package/dist/resources/extensions/gsd/guided-flow.js +82 -32
- package/dist/resources/extensions/gsd/index.js +22 -19
- package/dist/resources/extensions/gsd/native-git-bridge.js +37 -0
- package/dist/resources/extensions/gsd/paths.js +3 -0
- package/dist/resources/extensions/gsd/preferences-types.js +1 -0
- package/dist/resources/extensions/gsd/preferences-validation.js +58 -0
- package/dist/resources/extensions/gsd/preferences.js +3 -0
- package/dist/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/dist/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/dist/resources/extensions/gsd/roadmap-mutations.js +24 -0
- package/dist/resources/extensions/gsd/templates/runtime.md +21 -0
- package/dist/resources/extensions/mcp-client/index.js +14 -1
- package/package.json +1 -1
- package/packages/pi-ai/dist/utils/oauth/anthropic.js +2 -2
- package/packages/pi-ai/dist/utils/oauth/anthropic.js.map +1 -1
- package/packages/pi-ai/src/utils/oauth/anthropic.ts +2 -2
- package/packages/pi-coding-agent/dist/core/extensions/loader.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/extensions/loader.js +205 -7
- package/packages/pi-coding-agent/dist/core/extensions/loader.js.map +1 -1
- package/packages/pi-coding-agent/src/core/extensions/loader.ts +223 -7
- package/src/resources/extensions/browser-tools/index.ts +3 -0
- package/src/resources/extensions/browser-tools/tools/verify.ts +117 -0
- package/src/resources/extensions/github-sync/cli.ts +364 -0
- package/src/resources/extensions/github-sync/index.ts +93 -0
- package/src/resources/extensions/github-sync/mapping.ts +81 -0
- package/src/resources/extensions/github-sync/sync.ts +556 -0
- package/src/resources/extensions/github-sync/templates.ts +183 -0
- package/src/resources/extensions/github-sync/tests/cli.test.ts +20 -0
- package/src/resources/extensions/github-sync/tests/commit-linking.test.ts +39 -0
- package/src/resources/extensions/github-sync/tests/mapping.test.ts +104 -0
- package/src/resources/extensions/github-sync/tests/templates.test.ts +110 -0
- package/src/resources/extensions/github-sync/types.ts +47 -0
- package/src/resources/extensions/gsd/auto-dispatch.ts +1 -1
- package/src/resources/extensions/gsd/auto-loop.ts +472 -434
- package/src/resources/extensions/gsd/auto-post-unit.ts +29 -3
- package/src/resources/extensions/gsd/auto-prompts.ts +29 -15
- package/src/resources/extensions/gsd/auto-worktree.ts +3 -3
- package/src/resources/extensions/gsd/commands.ts +2 -2
- package/src/resources/extensions/gsd/doctor.ts +22 -1
- package/src/resources/extensions/gsd/exit-command.ts +2 -2
- package/src/resources/extensions/gsd/files.ts +3 -1
- package/src/resources/extensions/gsd/git-service.ts +44 -10
- package/src/resources/extensions/gsd/guided-flow.ts +110 -38
- package/src/resources/extensions/gsd/index.ts +21 -16
- package/src/resources/extensions/gsd/native-git-bridge.ts +37 -0
- package/src/resources/extensions/gsd/paths.ts +4 -0
- package/src/resources/extensions/gsd/preferences-types.ts +4 -0
- package/src/resources/extensions/gsd/preferences-validation.ts +50 -0
- package/src/resources/extensions/gsd/preferences.ts +3 -0
- package/src/resources/extensions/gsd/prompts/execute-task.md +2 -0
- package/src/resources/extensions/gsd/prompts/run-uat.md +2 -0
- package/src/resources/extensions/gsd/roadmap-mutations.ts +29 -0
- package/src/resources/extensions/gsd/templates/runtime.md +21 -0
- package/src/resources/extensions/gsd/tests/auto-loop.test.ts +111 -37
- package/src/resources/extensions/gsd/types.ts +8 -0
- package/src/resources/extensions/gsd/verification-evidence.ts +16 -0
- package/src/resources/extensions/mcp-client/index.ts +17 -1
|
@@ -28,6 +28,7 @@ import { showConfirm } from "../shared/mod.js";
|
|
|
28
28
|
import { debugLog } from "./debug-logger.js";
|
|
29
29
|
import { findMilestoneIds, nextMilestoneId } from "./milestone-ids.js";
|
|
30
30
|
import { parkMilestone, discardMilestone } from "./milestone-actions.js";
|
|
31
|
+
import { resolveModelWithFallbacksForUnit } from "./preferences-models.js";
|
|
31
32
|
// ─── Re-exports (preserve public API for existing importers) ────────────────
|
|
32
33
|
export { MILESTONE_ID_RE, generateMilestoneSuffix, nextMilestoneId, extractMilestoneSeq, parseMilestoneId, milestoneIdSort, maxMilestoneNum, findMilestoneIds, } from "./milestone-ids.js";
|
|
33
34
|
export { showQueue, handleQueueReorder, showQueueAdd, buildExistingMilestonesContext, } from "./guided-flow-queue.js";
|
|
@@ -153,8 +154,32 @@ function parseMilestoneSequenceFromProject(content) {
|
|
|
153
154
|
/**
|
|
154
155
|
* Read GSD-WORKFLOW.md and dispatch it to the LLM with a contextual note.
|
|
155
156
|
* This is the only way the wizard triggers work — everything else is the LLM's job.
|
|
157
|
+
*
|
|
158
|
+
* When a unitType is provided, resolves the user's model preference for that
|
|
159
|
+
* phase (e.g., models.planning → "plan-milestone") and applies it before
|
|
160
|
+
* dispatching. This ensures guided-flow dispatches respect the same
|
|
161
|
+
* per-phase model preferences that auto-mode uses.
|
|
156
162
|
*/
|
|
157
|
-
function dispatchWorkflow(pi, note, customType = "gsd-run") {
|
|
163
|
+
async function dispatchWorkflow(pi, note, customType = "gsd-run", ctx, unitType) {
|
|
164
|
+
// Apply model preference for this unit type (if configured)
|
|
165
|
+
if (ctx && unitType) {
|
|
166
|
+
const modelConfig = resolveModelWithFallbacksForUnit(unitType);
|
|
167
|
+
if (modelConfig) {
|
|
168
|
+
const availableModels = ctx.modelRegistry.getAvailable();
|
|
169
|
+
const modelsToTry = [modelConfig.primary, ...modelConfig.fallbacks];
|
|
170
|
+
for (const modelId of modelsToTry) {
|
|
171
|
+
// Resolve model from available models (same logic as auto-model-selection)
|
|
172
|
+
const model = resolveAvailableModel(modelId, availableModels, ctx.model?.provider);
|
|
173
|
+
if (!model)
|
|
174
|
+
continue;
|
|
175
|
+
const ok = await pi.setModel(model, { persist: false });
|
|
176
|
+
if (ok) {
|
|
177
|
+
debugLog("guided-flow-model-applied", { unitType, model: `${model.provider}/${model.id}` });
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
158
183
|
const workflowPath = process.env.GSD_WORKFLOW_PATH ?? join(process.env.HOME ?? "~", ".gsd", "agent", "GSD-WORKFLOW.md");
|
|
159
184
|
const workflow = readFileSync(workflowPath, "utf-8");
|
|
160
185
|
pi.sendMessage({
|
|
@@ -163,6 +188,31 @@ function dispatchWorkflow(pi, note, customType = "gsd-run") {
|
|
|
163
188
|
display: false,
|
|
164
189
|
}, { triggerTurn: true });
|
|
165
190
|
}
|
|
191
|
+
/**
|
|
192
|
+
* Resolve a model ID string to a model object from available models.
|
|
193
|
+
* Handles "provider/model" and bare ID formats.
|
|
194
|
+
*/
|
|
195
|
+
function resolveAvailableModel(modelId, availableModels, currentProvider) {
|
|
196
|
+
const slashIdx = modelId.indexOf("/");
|
|
197
|
+
if (slashIdx !== -1) {
|
|
198
|
+
const maybeProvider = modelId.substring(0, slashIdx);
|
|
199
|
+
const id = modelId.substring(slashIdx + 1);
|
|
200
|
+
const knownProviders = new Set(availableModels.map(m => m.provider.toLowerCase()));
|
|
201
|
+
if (knownProviders.has(maybeProvider.toLowerCase())) {
|
|
202
|
+
const match = availableModels.find(m => m.provider.toLowerCase() === maybeProvider.toLowerCase()
|
|
203
|
+
&& m.id.toLowerCase() === id.toLowerCase());
|
|
204
|
+
if (match)
|
|
205
|
+
return match;
|
|
206
|
+
}
|
|
207
|
+
// Try matching the full string as a model ID (OpenRouter-style)
|
|
208
|
+
const lower = modelId.toLowerCase();
|
|
209
|
+
return availableModels.find(m => m.id.toLowerCase() === lower
|
|
210
|
+
|| `${m.provider}/${m.id}`.toLowerCase() === lower);
|
|
211
|
+
}
|
|
212
|
+
// Bare ID — prefer current provider, then first available
|
|
213
|
+
const exactProviderMatch = availableModels.find(m => m.id === modelId && m.provider === currentProvider);
|
|
214
|
+
return exactProviderMatch ?? availableModels.find(m => m.id === modelId);
|
|
215
|
+
}
|
|
166
216
|
/**
|
|
167
217
|
* Build the discuss-and-plan prompt for a new milestone.
|
|
168
218
|
* Used by all three "new milestone" paths (first ever, no active, all complete).
|
|
@@ -244,8 +294,8 @@ export async function showHeadlessMilestoneCreation(ctx, pi, basePath, seedConte
|
|
|
244
294
|
const prompt = buildHeadlessDiscussPrompt(nextId, seedContext, basePath);
|
|
245
295
|
// Set pending auto start (auto-mode triggers on "Milestone X ready." via checkAutoStartAfterDiscuss)
|
|
246
296
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId };
|
|
247
|
-
// Dispatch
|
|
248
|
-
dispatchWorkflow(pi, prompt);
|
|
297
|
+
// Dispatch — headless milestone creation is a planning activity
|
|
298
|
+
await dispatchWorkflow(pi, prompt, "gsd-run", ctx, "plan-milestone");
|
|
249
299
|
}
|
|
250
300
|
// ─── Discuss Flow ─────────────────────────────────────────────────────────────
|
|
251
301
|
/**
|
|
@@ -381,23 +431,23 @@ export async function showDiscuss(ctx, pi, basePath) {
|
|
|
381
431
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
382
432
|
: basePrompt;
|
|
383
433
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
384
|
-
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
434
|
+
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
|
|
385
435
|
}
|
|
386
436
|
else if (choice === "discuss_fresh") {
|
|
387
437
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
388
438
|
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
389
439
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: mid, step: false };
|
|
390
|
-
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
440
|
+
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
391
441
|
milestoneId: mid, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
392
442
|
commitInstruction: buildDocsCommitInstruction(`docs(${mid}): milestone context from discuss`),
|
|
393
|
-
}), "gsd-discuss");
|
|
443
|
+
}), "gsd-discuss", ctx, "plan-milestone");
|
|
394
444
|
}
|
|
395
445
|
else if (choice === "skip_milestone") {
|
|
396
446
|
const milestoneIds = findMilestoneIds(basePath);
|
|
397
447
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
398
448
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
399
449
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: false };
|
|
400
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
450
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
401
451
|
}
|
|
402
452
|
return;
|
|
403
453
|
}
|
|
@@ -484,7 +534,7 @@ export async function showDiscuss(ctx, pi, basePath) {
|
|
|
484
534
|
continue;
|
|
485
535
|
}
|
|
486
536
|
const prompt = await buildDiscussSlicePrompt(mid, chosen.id, chosen.title, basePath, { rediscuss: isRediscuss });
|
|
487
|
-
dispatchWorkflow(pi, prompt, "gsd-discuss");
|
|
537
|
+
await dispatchWorkflow(pi, prompt, "gsd-discuss", ctx, "plan-slice");
|
|
488
538
|
// Wait for the discuss session to finish, then loop back to the picker
|
|
489
539
|
await ctx.waitForIdle();
|
|
490
540
|
invalidateAllCaches();
|
|
@@ -611,7 +661,7 @@ async function handleMilestoneActions(ctx, pi, basePath, milestoneId, milestoneT
|
|
|
611
661
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
612
662
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
613
663
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
614
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
664
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
615
665
|
return true;
|
|
616
666
|
}
|
|
617
667
|
// "back" or null
|
|
@@ -729,7 +779,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
729
779
|
if (isFirst) {
|
|
730
780
|
// First ever — skip wizard, just ask directly
|
|
731
781
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
732
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath));
|
|
782
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New project, milestone ${nextId}. Do NOT read or explore .gsd/ — it's empty scaffolding.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
733
783
|
}
|
|
734
784
|
else {
|
|
735
785
|
const choice = await showNextAction(ctx, {
|
|
@@ -747,7 +797,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
747
797
|
});
|
|
748
798
|
if (choice === "new_milestone") {
|
|
749
799
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
750
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
800
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
751
801
|
}
|
|
752
802
|
}
|
|
753
803
|
return;
|
|
@@ -779,7 +829,7 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
779
829
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
780
830
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
781
831
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
782
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
832
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
783
833
|
}
|
|
784
834
|
else if (choice === "status") {
|
|
785
835
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -825,23 +875,23 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
825
875
|
? `${basePrompt}\n\n## Prior Discussion (Draft Seed)\n\n${draftContent}`
|
|
826
876
|
: basePrompt;
|
|
827
877
|
pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
|
|
828
|
-
dispatchWorkflow(pi, seed, "gsd-discuss");
|
|
878
|
+
await dispatchWorkflow(pi, seed, "gsd-discuss", ctx, "plan-milestone");
|
|
829
879
|
}
|
|
830
880
|
else if (choice === "discuss_fresh") {
|
|
831
881
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
832
882
|
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
833
883
|
pendingAutoStart = { ctx, pi, basePath, milestoneId, step: stepMode };
|
|
834
|
-
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
884
|
+
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
835
885
|
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
836
886
|
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
|
837
|
-
}), "gsd-discuss");
|
|
887
|
+
}), "gsd-discuss", ctx, "plan-milestone");
|
|
838
888
|
}
|
|
839
889
|
else if (choice === "skip_milestone") {
|
|
840
890
|
const milestoneIds = findMilestoneIds(basePath);
|
|
841
891
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
842
892
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
843
893
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
844
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
894
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
845
895
|
}
|
|
846
896
|
return;
|
|
847
897
|
}
|
|
@@ -893,24 +943,24 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
893
943
|
inlineTemplate("secrets-manifest", "Secrets Manifest"),
|
|
894
944
|
].join("\n\n---\n\n");
|
|
895
945
|
const secretsOutputPath = relMilestoneFile(basePath, milestoneId, "SECRETS");
|
|
896
|
-
dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
|
|
946
|
+
await dispatchWorkflow(pi, loadPrompt("guided-plan-milestone", {
|
|
897
947
|
milestoneId, milestoneTitle, secretsOutputPath, inlinedTemplates: planMilestoneTemplates,
|
|
898
|
-
}));
|
|
948
|
+
}), "gsd-run", ctx, "plan-milestone");
|
|
899
949
|
}
|
|
900
950
|
else if (choice === "discuss") {
|
|
901
951
|
const discussMilestoneTemplates = inlineTemplate("context", "Context");
|
|
902
952
|
const structuredQuestionsAvailable = pi.getActiveTools().includes("ask_user_questions") ? "true" : "false";
|
|
903
|
-
dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
953
|
+
await dispatchWorkflow(pi, loadPrompt("guided-discuss-milestone", {
|
|
904
954
|
milestoneId, milestoneTitle, inlinedTemplates: discussMilestoneTemplates, structuredQuestionsAvailable,
|
|
905
955
|
commitInstruction: buildDocsCommitInstruction(`docs(${milestoneId}): milestone context from discuss`),
|
|
906
|
-
}));
|
|
956
|
+
}), "gsd-run", ctx, "plan-milestone");
|
|
907
957
|
}
|
|
908
958
|
else if (choice === "skip_milestone") {
|
|
909
959
|
const milestoneIds = findMilestoneIds(basePath);
|
|
910
960
|
const uniqueMilestoneIds = !!loadEffectiveGSDPreferences()?.preferences?.unique_milestone_ids;
|
|
911
961
|
const nextId = nextMilestoneId(milestoneIds, uniqueMilestoneIds);
|
|
912
962
|
pendingAutoStart = { ctx, pi, basePath, milestoneId: nextId, step: stepMode };
|
|
913
|
-
dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath));
|
|
963
|
+
await dispatchWorkflow(pi, buildDiscussPrompt(nextId, `New milestone ${nextId}.`, basePath), "gsd-run", ctx, "plan-milestone");
|
|
914
964
|
}
|
|
915
965
|
else if (choice === "discard_milestone") {
|
|
916
966
|
const confirmed = await showConfirm(ctx, {
|
|
@@ -1021,18 +1071,18 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1021
1071
|
inlineTemplate("plan", "Slice Plan"),
|
|
1022
1072
|
inlineTemplate("task-plan", "Task Plan"),
|
|
1023
1073
|
].join("\n\n---\n\n");
|
|
1024
|
-
dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
|
|
1074
|
+
await dispatchWorkflow(pi, loadPrompt("guided-plan-slice", {
|
|
1025
1075
|
milestoneId, sliceId, sliceTitle, inlinedTemplates: planSliceTemplates,
|
|
1026
|
-
}));
|
|
1076
|
+
}), "gsd-run", ctx, "plan-slice");
|
|
1027
1077
|
}
|
|
1028
1078
|
else if (choice === "discuss") {
|
|
1029
|
-
dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }));
|
|
1079
|
+
await dispatchWorkflow(pi, await buildDiscussSlicePrompt(milestoneId, sliceId, sliceTitle, basePath, { rediscuss: hasContext }), "gsd-run", ctx, "plan-slice");
|
|
1030
1080
|
}
|
|
1031
1081
|
else if (choice === "research") {
|
|
1032
1082
|
const researchTemplates = inlineTemplate("research", "Research");
|
|
1033
|
-
dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
|
|
1083
|
+
await dispatchWorkflow(pi, loadPrompt("guided-research-slice", {
|
|
1034
1084
|
milestoneId, sliceId, sliceTitle, inlinedTemplates: researchTemplates,
|
|
1035
|
-
}));
|
|
1085
|
+
}), "gsd-run", ctx, "research-slice");
|
|
1036
1086
|
}
|
|
1037
1087
|
else if (choice === "status") {
|
|
1038
1088
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -1075,9 +1125,9 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1075
1125
|
inlineTemplate("slice-summary", "Slice Summary"),
|
|
1076
1126
|
inlineTemplate("uat", "UAT"),
|
|
1077
1127
|
].join("\n\n---\n\n");
|
|
1078
|
-
dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
|
|
1128
|
+
await dispatchWorkflow(pi, loadPrompt("guided-complete-slice", {
|
|
1079
1129
|
workingDirectory: basePath, milestoneId, sliceId, sliceTitle, inlinedTemplates: completeSliceTemplates,
|
|
1080
|
-
}));
|
|
1130
|
+
}), "gsd-run", ctx, "complete-slice");
|
|
1081
1131
|
}
|
|
1082
1132
|
else if (choice === "status") {
|
|
1083
1133
|
const { fireStatusViaCommand } = await import("./commands.js");
|
|
@@ -1138,15 +1188,15 @@ export async function showSmartEntry(ctx, pi, basePath, options) {
|
|
|
1138
1188
|
}
|
|
1139
1189
|
if (choice === "execute") {
|
|
1140
1190
|
if (hasInterrupted) {
|
|
1141
|
-
dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
|
|
1191
|
+
await dispatchWorkflow(pi, loadPrompt("guided-resume-task", {
|
|
1142
1192
|
milestoneId, sliceId,
|
|
1143
|
-
}));
|
|
1193
|
+
}), "gsd-run", ctx, "execute-task");
|
|
1144
1194
|
}
|
|
1145
1195
|
else {
|
|
1146
1196
|
const executeTaskTemplates = inlineTemplate("task-summary", "Task Summary");
|
|
1147
|
-
dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
|
|
1197
|
+
await dispatchWorkflow(pi, loadPrompt("guided-execute-task", {
|
|
1148
1198
|
milestoneId, sliceId, taskId, taskTitle, inlinedTemplates: executeTaskTemplates,
|
|
1149
|
-
}));
|
|
1199
|
+
}), "gsd-run", ctx, "execute-task");
|
|
1150
1200
|
}
|
|
1151
1201
|
}
|
|
1152
1202
|
else if (choice === "status") {
|
|
@@ -66,6 +66,24 @@ function warnDeprecatedAgentInstructions() {
|
|
|
66
66
|
}
|
|
67
67
|
// ── Depth verification state ──────────────────────────────────────────────
|
|
68
68
|
let depthVerificationDone = false;
|
|
69
|
+
// ── DB lazy-open helper ───────────────────────────────────────────────────
|
|
70
|
+
// In manual sessions (no auto-mode), the DB is never opened by bootstrapAutoSession.
|
|
71
|
+
// This helper ensures the DB is lazily opened on first tool call that needs it.
|
|
72
|
+
async function ensureDbOpen() {
|
|
73
|
+
try {
|
|
74
|
+
const db = await import("./gsd-db.js");
|
|
75
|
+
if (db.isDbAvailable())
|
|
76
|
+
return true;
|
|
77
|
+
const dbPath = join(process.cwd(), ".gsd", "gsd.db");
|
|
78
|
+
if (existsSync(dbPath)) {
|
|
79
|
+
return db.openDatabase(dbPath);
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
69
87
|
// ── Queue phase tracking ──────────────────────────────────────────────────
|
|
70
88
|
// When true, the LLM is in a queue flow writing CONTEXT.md files.
|
|
71
89
|
// The write-gate applies during queue flows just like discussion flows.
|
|
@@ -228,13 +246,8 @@ export default function (pi) {
|
|
|
228
246
|
when_context: Type.Optional(Type.String({ description: "When/context for the decision (e.g. milestone ID)" })),
|
|
229
247
|
}),
|
|
230
248
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
231
|
-
//
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
const db = await import("./gsd-db.js");
|
|
235
|
-
dbAvailable = db.isDbAvailable();
|
|
236
|
-
}
|
|
237
|
-
catch { /* dynamic import failed */ }
|
|
249
|
+
// Ensure DB is open (lazy-open on first tool call in manual sessions)
|
|
250
|
+
const dbAvailable = await ensureDbOpen();
|
|
238
251
|
if (!dbAvailable) {
|
|
239
252
|
return {
|
|
240
253
|
content: [{ type: "text", text: "Error: GSD database is not available. Cannot save decision." }],
|
|
@@ -290,12 +303,7 @@ export default function (pi) {
|
|
|
290
303
|
supporting_slices: Type.Optional(Type.String({ description: "Supporting slices" })),
|
|
291
304
|
}),
|
|
292
305
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
293
|
-
|
|
294
|
-
try {
|
|
295
|
-
const db = await import("./gsd-db.js");
|
|
296
|
-
dbAvailable = db.isDbAvailable();
|
|
297
|
-
}
|
|
298
|
-
catch { /* dynamic import failed */ }
|
|
306
|
+
const dbAvailable = await ensureDbOpen();
|
|
299
307
|
if (!dbAvailable) {
|
|
300
308
|
return {
|
|
301
309
|
content: [{ type: "text", text: "Error: GSD database is not available. Cannot update requirement." }],
|
|
@@ -365,12 +373,7 @@ export default function (pi) {
|
|
|
365
373
|
content: Type.String({ description: "The full markdown content of the artifact" }),
|
|
366
374
|
}),
|
|
367
375
|
async execute(_toolCallId, params, _signal, _onUpdate, _ctx) {
|
|
368
|
-
|
|
369
|
-
try {
|
|
370
|
-
const db = await import("./gsd-db.js");
|
|
371
|
-
dbAvailable = db.isDbAvailable();
|
|
372
|
-
}
|
|
373
|
-
catch { /* dynamic import failed */ }
|
|
376
|
+
const dbAvailable = await ensureDbOpen();
|
|
374
377
|
if (!dbAvailable) {
|
|
375
378
|
return {
|
|
376
379
|
content: [{ type: "text", text: "Error: GSD database is not available. Cannot save artifact." }],
|
|
@@ -518,6 +518,43 @@ export function nativeAddAll(basePath) {
|
|
|
518
518
|
}
|
|
519
519
|
gitFileExec(basePath, ["add", "-A"]);
|
|
520
520
|
}
|
|
521
|
+
/**
|
|
522
|
+
* Stage all files with pathspec exclusions (git add -A -- ':!pattern' ...).
|
|
523
|
+
* Excluded paths are never hashed by git, preventing hangs on large
|
|
524
|
+
* untracked artifact trees (57GB+, 11K+ files). See #1605.
|
|
525
|
+
*
|
|
526
|
+
* Falls back to plain `git add -A` when no exclusions are provided.
|
|
527
|
+
* Always uses the CLI path (not libgit2) because libgit2's add_all
|
|
528
|
+
* does not support pathspec exclusion syntax.
|
|
529
|
+
*
|
|
530
|
+
* When excluded paths are already covered by .gitignore, git may exit
|
|
531
|
+
* with code 1 and an "ignored by .gitignore" warning. This is harmless
|
|
532
|
+
* (the staging succeeds for all non-ignored files) and is suppressed.
|
|
533
|
+
*/
|
|
534
|
+
export function nativeAddAllWithExclusions(basePath, exclusions) {
|
|
535
|
+
if (exclusions.length === 0) {
|
|
536
|
+
nativeAddAll(basePath);
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
539
|
+
const pathspecs = exclusions.map(e => `:!${e}`);
|
|
540
|
+
try {
|
|
541
|
+
execFileSync("git", ["add", "-A", "--", ...pathspecs], {
|
|
542
|
+
cwd: basePath,
|
|
543
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
544
|
+
encoding: "utf-8",
|
|
545
|
+
env: GIT_NO_PROMPT_ENV,
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
catch (err) {
|
|
549
|
+
// git exits 1 when pathspec exclusions reference paths already covered
|
|
550
|
+
// by .gitignore. The staging itself succeeds — only suppress that case.
|
|
551
|
+
const stderr = err?.stderr ?? "";
|
|
552
|
+
if (stderr.includes("ignored by one of your .gitignore files")) {
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
throw new GSDError(GSD_GIT_ERROR, `git add -A with exclusions failed in ${basePath}: ${getErrorMessage(err)}`);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
521
558
|
/**
|
|
522
559
|
* Stage specific files.
|
|
523
560
|
* Native: libgit2 index add.
|
|
@@ -343,6 +343,9 @@ function probeGsdRoot(rawBasePath) {
|
|
|
343
343
|
export function milestonesDir(basePath) {
|
|
344
344
|
return join(gsdRoot(basePath), "milestones");
|
|
345
345
|
}
|
|
346
|
+
export function resolveRuntimeFile(basePath) {
|
|
347
|
+
return join(gsdRoot(basePath), "RUNTIME.md");
|
|
348
|
+
}
|
|
346
349
|
export function resolveGsdRootFile(basePath, key) {
|
|
347
350
|
const root = gsdRoot(basePath);
|
|
348
351
|
const canonical = join(root, GSD_ROOT_FILES[key]);
|
|
@@ -717,5 +717,63 @@ export function validatePreferences(preferences) {
|
|
|
717
717
|
errors.push(`context_selection must be one of: full, smart`);
|
|
718
718
|
}
|
|
719
719
|
}
|
|
720
|
+
// ─── GitHub Sync ────────────────────────────────────────────────────────
|
|
721
|
+
if (preferences.github !== undefined) {
|
|
722
|
+
if (typeof preferences.github === "object" && preferences.github !== null) {
|
|
723
|
+
const gh = preferences.github;
|
|
724
|
+
const validGh = {};
|
|
725
|
+
if (gh.enabled !== undefined) {
|
|
726
|
+
if (typeof gh.enabled === "boolean")
|
|
727
|
+
validGh.enabled = gh.enabled;
|
|
728
|
+
else
|
|
729
|
+
errors.push("github.enabled must be a boolean");
|
|
730
|
+
}
|
|
731
|
+
if (gh.repo !== undefined) {
|
|
732
|
+
if (typeof gh.repo === "string" && gh.repo.includes("/"))
|
|
733
|
+
validGh.repo = gh.repo;
|
|
734
|
+
else
|
|
735
|
+
errors.push('github.repo must be a string in "owner/repo" format');
|
|
736
|
+
}
|
|
737
|
+
if (gh.project !== undefined) {
|
|
738
|
+
const p = typeof gh.project === "number" ? gh.project : Number(gh.project);
|
|
739
|
+
if (Number.isFinite(p) && p > 0)
|
|
740
|
+
validGh.project = Math.floor(p);
|
|
741
|
+
else
|
|
742
|
+
errors.push("github.project must be a positive number");
|
|
743
|
+
}
|
|
744
|
+
if (gh.labels !== undefined) {
|
|
745
|
+
if (Array.isArray(gh.labels) && gh.labels.every((l) => typeof l === "string")) {
|
|
746
|
+
validGh.labels = gh.labels;
|
|
747
|
+
}
|
|
748
|
+
else {
|
|
749
|
+
errors.push("github.labels must be an array of strings");
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (gh.auto_link_commits !== undefined) {
|
|
753
|
+
if (typeof gh.auto_link_commits === "boolean")
|
|
754
|
+
validGh.auto_link_commits = gh.auto_link_commits;
|
|
755
|
+
else
|
|
756
|
+
errors.push("github.auto_link_commits must be a boolean");
|
|
757
|
+
}
|
|
758
|
+
if (gh.slice_prs !== undefined) {
|
|
759
|
+
if (typeof gh.slice_prs === "boolean")
|
|
760
|
+
validGh.slice_prs = gh.slice_prs;
|
|
761
|
+
else
|
|
762
|
+
errors.push("github.slice_prs must be a boolean");
|
|
763
|
+
}
|
|
764
|
+
const knownGhKeys = new Set(["enabled", "repo", "project", "labels", "auto_link_commits", "slice_prs"]);
|
|
765
|
+
for (const key of Object.keys(gh)) {
|
|
766
|
+
if (!knownGhKeys.has(key)) {
|
|
767
|
+
warnings.push(`unknown github key "${key}" — ignored`);
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
if (Object.keys(validGh).length > 0) {
|
|
771
|
+
validated.github = validGh;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
else {
|
|
775
|
+
errors.push("github must be an object");
|
|
776
|
+
}
|
|
777
|
+
}
|
|
720
778
|
return { preferences: validated, errors, warnings };
|
|
721
779
|
}
|
|
@@ -203,6 +203,9 @@ function mergePreferences(base, override) {
|
|
|
203
203
|
context_selection: override.context_selection ?? base.context_selection,
|
|
204
204
|
auto_visualize: override.auto_visualize ?? base.auto_visualize,
|
|
205
205
|
auto_report: override.auto_report ?? base.auto_report,
|
|
206
|
+
github: (base.github || override.github)
|
|
207
|
+
? { ...(base.github ?? {}), ...(override.github ?? {}) }
|
|
208
|
+
: undefined,
|
|
206
209
|
};
|
|
207
210
|
}
|
|
208
211
|
function mergeStringLists(base, override) {
|
|
@@ -25,6 +25,8 @@ You are the UAT runner. Execute every check defined in `{{uatPath}}` as deeply a
|
|
|
25
25
|
### Automation rules by mode
|
|
26
26
|
|
|
27
27
|
- `artifact-driven` — verify with shell commands, scripts, file reads, and artifact structure checks.
|
|
28
|
+
- `browser-executable` — use browser tools to navigate to the target URL and verify expected behavior. Capture screenshots as evidence. Record pass/fail with specific assertions.
|
|
29
|
+
- `runtime-executable` — execute the specified command or script. Capture stdout/stderr as evidence. Record pass/fail based on exit code and output.
|
|
28
30
|
- `live-runtime` — exercise the real runtime path. Start or connect to the app/service if needed, use browser/runtime/network checks, and verify observable behavior.
|
|
29
31
|
- `mixed` — run all automatable artifact-driven and live-runtime checks. Separate any remaining human-only checks explicitly.
|
|
30
32
|
- `human-experience` — automate setup, preconditions, screenshots, logs, and objective checks, but do **not** invent subjective PASS results. Mark taste-based, experiential, or purely human-judgment checks as `NEEDS-HUMAN` and use an overall verdict of `PARTIAL` unless every required check was objective and passed.
|
|
@@ -32,6 +32,30 @@ export function markSliceDoneInRoadmap(basePath, mid, sid) {
|
|
|
32
32
|
clearParseCache();
|
|
33
33
|
return true;
|
|
34
34
|
}
|
|
35
|
+
/**
|
|
36
|
+
* Mark a slice as not done ([ ]) in the milestone roadmap.
|
|
37
|
+
* Idempotent — no-op if already unchecked or if the slice isn't found.
|
|
38
|
+
*
|
|
39
|
+
* @returns true if the roadmap was modified, false if no change was needed
|
|
40
|
+
*/
|
|
41
|
+
export function markSliceUndoneInRoadmap(basePath, mid, sid) {
|
|
42
|
+
const roadmapFile = resolveMilestoneFile(basePath, mid, "ROADMAP");
|
|
43
|
+
if (!roadmapFile)
|
|
44
|
+
return false;
|
|
45
|
+
let content;
|
|
46
|
+
try {
|
|
47
|
+
content = readFileSync(roadmapFile, "utf-8");
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
const updated = content.replace(new RegExp(`^(\\s*-\\s+)\\[x\\]\\s+\\*\\*${sid}:`, "m"), `$1[ ] **${sid}:`);
|
|
53
|
+
if (updated === content)
|
|
54
|
+
return false;
|
|
55
|
+
atomicWriteSync(roadmapFile, updated);
|
|
56
|
+
clearParseCache();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
35
59
|
/**
|
|
36
60
|
* Mark a task as done ([x]) in the slice plan.
|
|
37
61
|
* Idempotent — no-op if already checked or if the task isn't found.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Runtime Context
|
|
2
|
+
|
|
3
|
+
## Stack
|
|
4
|
+
- **Language:** (e.g., TypeScript, Python, Go)
|
|
5
|
+
- **Framework:** (e.g., Next.js, FastAPI, Gin)
|
|
6
|
+
- **Build:** (e.g., npm run build, cargo build)
|
|
7
|
+
- **Test:** (e.g., npm run test, pytest)
|
|
8
|
+
- **Lint:** (e.g., npm run lint, ruff check)
|
|
9
|
+
|
|
10
|
+
## Environment
|
|
11
|
+
- **Node version:** (e.g., 20.x)
|
|
12
|
+
- **Package manager:** (e.g., npm, pnpm, yarn)
|
|
13
|
+
- **Required env vars:** (list any needed for local dev)
|
|
14
|
+
|
|
15
|
+
## Dev Server
|
|
16
|
+
- **Start command:** (e.g., npm run dev)
|
|
17
|
+
- **Default port:** (e.g., 3000)
|
|
18
|
+
- **Health check:** (e.g., curl http://localhost:3000/health)
|
|
19
|
+
|
|
20
|
+
## Notes
|
|
21
|
+
(Any runtime-specific context the executor needs to know)
|
|
@@ -76,6 +76,19 @@ function readConfigs() {
|
|
|
76
76
|
function getServerConfig(name) {
|
|
77
77
|
return readConfigs().find((s) => s.name === name);
|
|
78
78
|
}
|
|
79
|
+
/** Resolve ${VAR} references in env values against process.env. */
|
|
80
|
+
function resolveEnv(env) {
|
|
81
|
+
const resolved = {};
|
|
82
|
+
for (const [key, value] of Object.entries(env)) {
|
|
83
|
+
if (typeof value === "string") {
|
|
84
|
+
resolved[key] = value.replace(/\$\{([^}]+)\}/g, (_match, varName) => process.env[varName] ?? "");
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
resolved[key] = value;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return resolved;
|
|
91
|
+
}
|
|
79
92
|
async function getOrConnect(name, signal) {
|
|
80
93
|
const existing = connections.get(name);
|
|
81
94
|
if (existing)
|
|
@@ -89,7 +102,7 @@ async function getOrConnect(name, signal) {
|
|
|
89
102
|
transport = new StdioClientTransport({
|
|
90
103
|
command: config.command,
|
|
91
104
|
args: config.args,
|
|
92
|
-
env: config.env ? { ...process.env, ...config.env } : undefined,
|
|
105
|
+
env: config.env ? { ...process.env, ...resolveEnv(config.env) } : undefined,
|
|
93
106
|
cwd: config.cwd,
|
|
94
107
|
stderr: "pipe",
|
|
95
108
|
});
|
package/package.json
CHANGED
|
@@ -5,8 +5,8 @@ import { generatePKCE } from "./pkce.js";
|
|
|
5
5
|
const decode = (s) => atob(s);
|
|
6
6
|
const CLIENT_ID = decode("OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl");
|
|
7
7
|
const AUTHORIZE_URL = "https://claude.ai/oauth/authorize";
|
|
8
|
-
const TOKEN_URL = "https://
|
|
9
|
-
const REDIRECT_URI = "https://
|
|
8
|
+
const TOKEN_URL = "https://platform.claude.com/v1/oauth/token";
|
|
9
|
+
const REDIRECT_URI = "https://platform.claude.com/oauth/code/callback";
|
|
10
10
|
const SCOPES = "org:create_api_key user:profile user:inference";
|
|
11
11
|
/**
|
|
12
12
|
* Login with Anthropic OAuth (device code flow)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../src/utils/oauth/anthropic.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAGzC,MAAM,MAAM,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AACtC,MAAM,SAAS,GAAG,MAAM,CAAC,kDAAkD,CAAC,CAAC;AAC7E,MAAM,aAAa,GAAG,mCAAmC,CAAC;AAC1D,MAAM,SAAS,GAAG,
|
|
1
|
+
{"version":3,"file":"anthropic.js","sourceRoot":"","sources":["../../../src/utils/oauth/anthropic.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Anthropic OAuth flow (Claude Pro/Max)\n */\n\nimport { generatePKCE } from \"./pkce.js\";\nimport type { OAuthCredentials, OAuthLoginCallbacks, OAuthProviderInterface } from \"./types.js\";\n\nconst decode = (s: string) => atob(s);\nconst CLIENT_ID = decode(\"OWQxYzI1MGEtZTYxYi00NGQ5LTg4ZWQtNTk0NGQxOTYyZjVl\");\nconst AUTHORIZE_URL = \"https://claude.ai/oauth/authorize\";\nconst TOKEN_URL = \"https://platform.claude.com/v1/oauth/token\";\nconst REDIRECT_URI = \"https://platform.claude.com/oauth/code/callback\";\nconst SCOPES = \"org:create_api_key user:profile user:inference\";\n\n/**\n * Login with Anthropic OAuth (device code flow)\n *\n * @param onAuthUrl - Callback to handle the authorization URL (e.g., open browser)\n * @param onPromptCode - Callback to prompt user for the authorization code\n */\nexport async function loginAnthropic(\n\tonAuthUrl: (url: string) => void,\n\tonPromptCode: () => Promise<string>,\n): Promise<OAuthCredentials> {\n\tconst { verifier, challenge } = await generatePKCE();\n\n\t// Build authorization URL\n\tconst authParams = new URLSearchParams({\n\t\tcode: \"true\",\n\t\tclient_id: CLIENT_ID,\n\t\tresponse_type: \"code\",\n\t\tredirect_uri: REDIRECT_URI,\n\t\tscope: SCOPES,\n\t\tcode_challenge: challenge,\n\t\tcode_challenge_method: \"S256\",\n\t\tstate: verifier,\n\t});\n\n\tconst authUrl = `${AUTHORIZE_URL}?${authParams.toString()}`;\n\n\t// Notify caller with URL to open\n\tonAuthUrl(authUrl);\n\n\t// Wait for user to paste authorization code (format: code#state)\n\tconst authCode = await onPromptCode();\n\tconst splits = authCode.split(\"#\");\n\tconst code = splits[0];\n\tconst state = splits[1];\n\n\t// Exchange code for tokens\n\tconst tokenResponse = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: {\n\t\t\t\"Content-Type\": \"application/json\",\n\t\t},\n\t\tbody: JSON.stringify({\n\t\t\tgrant_type: \"authorization_code\",\n\t\t\tclient_id: CLIENT_ID,\n\t\t\tcode: code,\n\t\t\tstate: state,\n\t\t\tredirect_uri: REDIRECT_URI,\n\t\t\tcode_verifier: verifier,\n\t\t}),\n\t\tsignal: AbortSignal.timeout(30_000),\n\t});\n\n\tif (!tokenResponse.ok) {\n\t\tconst error = await tokenResponse.text();\n\t\tthrow new Error(`Token exchange failed: ${error}`);\n\t}\n\n\tconst tokenData = (await tokenResponse.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t};\n\n\t// Calculate expiry time (current time + expires_in seconds - 5 min buffer)\n\tconst expiresAt = Date.now() + tokenData.expires_in * 1000 - 5 * 60 * 1000;\n\n\t// Save credentials\n\treturn {\n\t\trefresh: tokenData.refresh_token,\n\t\taccess: tokenData.access_token,\n\t\texpires: expiresAt,\n\t};\n}\n\n/**\n * Refresh Anthropic OAuth token\n */\nexport async function refreshAnthropicToken(refreshToken: string): Promise<OAuthCredentials> {\n\tconst response = await fetch(TOKEN_URL, {\n\t\tmethod: \"POST\",\n\t\theaders: { \"Content-Type\": \"application/json\" },\n\t\tbody: JSON.stringify({\n\t\t\tgrant_type: \"refresh_token\",\n\t\t\tclient_id: CLIENT_ID,\n\t\t\trefresh_token: refreshToken,\n\t\t}),\n\t\tsignal: AbortSignal.timeout(30_000),\n\t});\n\n\tif (!response.ok) {\n\t\tconst error = await response.text();\n\t\tthrow new Error(`Anthropic token refresh failed: ${error}`);\n\t}\n\n\tconst data = (await response.json()) as {\n\t\taccess_token: string;\n\t\trefresh_token: string;\n\t\texpires_in: number;\n\t};\n\n\treturn {\n\t\trefresh: data.refresh_token,\n\t\taccess: data.access_token,\n\t\texpires: Date.now() + data.expires_in * 1000 - 5 * 60 * 1000,\n\t};\n}\n\nexport const anthropicOAuthProvider: OAuthProviderInterface = {\n\tid: \"anthropic\",\n\tname: \"Anthropic (Claude Pro/Max)\",\n\n\tasync login(callbacks: OAuthLoginCallbacks): Promise<OAuthCredentials> {\n\t\treturn loginAnthropic(\n\t\t\t(url) => callbacks.onAuth({ url }),\n\t\t\t() => callbacks.onPrompt({ message: \"Paste the authorization code:\" }),\n\t\t);\n\t},\n\n\tasync refreshToken(credentials: OAuthCredentials): Promise<OAuthCredentials> {\n\t\treturn refreshAnthropicToken(credentials.refresh);\n\t},\n\n\tgetApiKey(credentials: OAuthCredentials): string {\n\t\treturn credentials.access;\n\t},\n};\n"]}
|