mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-17 12:58:34 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d967154a6b |
@@ -1,43 +1,10 @@
|
||||
import { Effect } from "effect"
|
||||
import { sql } from "drizzle-orm"
|
||||
import type { DatabaseMigration } from "../migration.js"
|
||||
|
||||
const previousV2Marker = "20260730195856_optional_session_title"
|
||||
|
||||
const migration: DatabaseMigration.Migration = {
|
||||
id: "20260804233008_loose_psylocke",
|
||||
up(tx) {
|
||||
return Effect.gen(function* () {
|
||||
// This marker identifies the completed pre-split V2 lineage. Its V2 tables
|
||||
// are canonical, so rename them in place instead of replaying the V1 squash.
|
||||
if (yield* tx.get(sql`SELECT id FROM migration WHERE id = ${previousV2Marker}`)) {
|
||||
const v1Only = yield* tx.get(sql`
|
||||
SELECT 1
|
||||
FROM message
|
||||
WHERE NOT EXISTS (
|
||||
SELECT 1 FROM session_message WHERE session_message.session_id = message.session_id
|
||||
)
|
||||
LIMIT 1
|
||||
`)
|
||||
if (v1Only) return yield* Effect.die(new Error("Previous V2 database contains V1-only session history"))
|
||||
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_project_idx\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_workspace_idx\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_parent_idx\`;`)
|
||||
yield* tx.run(`DROP INDEX IF EXISTS \`session_time_suspended_idx\`;`)
|
||||
yield* tx.run(`ALTER TABLE \`session\` RENAME TO \`session_v2\`;`)
|
||||
yield* tx.run(`CREATE INDEX \`session_v2_project_idx\` ON \`session_v2\` (\`project_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_v2_workspace_idx\` ON \`session_v2\` (\`workspace_id\`);`)
|
||||
yield* tx.run(`CREATE INDEX \`session_v2_parent_idx\` ON \`session_v2\` (\`parent_id\`);`)
|
||||
yield* tx.run(
|
||||
`CREATE INDEX \`session_v2_time_suspended_idx\` ON \`session_v2\` (\`time_suspended\`) WHERE "session_v2"."time_suspended" is not null;`,
|
||||
)
|
||||
yield* tx.run(`DROP TABLE IF EXISTS \`data_migration\`;`)
|
||||
yield* tx.run(`DROP TABLE IF EXISTS \`session_context_epoch\`;`)
|
||||
yield* tx.run(`DROP TABLE IF EXISTS \`session_input\`;`)
|
||||
return
|
||||
}
|
||||
|
||||
yield* tx.run(`
|
||||
CREATE TABLE IF NOT EXISTS \`kv\` (
|
||||
\`key\` text PRIMARY KEY,
|
||||
|
||||
@@ -33,7 +33,7 @@ const layer = Layer.effect(
|
||||
` Workspace root folder: ${location.project.directory}`,
|
||||
` Is directory a git repo: ${location.vcs?.type === "git" ? "yes" : "no"}`,
|
||||
` Platform: ${process.platform}`,
|
||||
` Prefer ${global.tmp} over generic system temporary directories such as /tmp; it is pre-created and approved for external access.`,
|
||||
` Use ${global.tmp} for temporary work outside the workspace; it already exists and is pre-approved for external directory access.`,
|
||||
"</env>",
|
||||
].join("\n"),
|
||||
),
|
||||
|
||||
@@ -324,7 +324,7 @@ export const layer = (options?: ShellSelect.Options) =>
|
||||
runFork(
|
||||
handle.exitCode.pipe(
|
||||
Effect.flatMap((code) => finish("exited", code)),
|
||||
Effect.catch(() => finish("exited")),
|
||||
Effect.catch(() => Effect.void),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -13,10 +13,6 @@ import { tmpdir } from "./fixture/tmpdir"
|
||||
import type { SqlClient } from "effect/unstable/sql/SqlClient"
|
||||
import legacyCredentialsMigration from "@opencode-ai/core/database/migration/20260805200742_import_legacy_credentials"
|
||||
import worktreeMigration from "@opencode-ai/core/database/migration/20260812213948_worktree"
|
||||
import previousV2Migration from "@opencode-ai/core/database/migration/20260804233008_loose_psylocke"
|
||||
import workspaceMigration from "@opencode-ai/core/database/migration/20260808023530_workspace_domain"
|
||||
import executionClaimsMigration from "@opencode-ai/core/database/migration/20260811161259_execution_claim_attempts"
|
||||
import sessionInboxMigration from "@opencode-ai/core/database/migration/20260812181746_session_inbox"
|
||||
import { Global } from "@opencode-ai/util/global"
|
||||
|
||||
const run = <A, E>(
|
||||
@@ -132,142 +128,6 @@ describe("DatabaseMigration", () => {
|
||||
)
|
||||
})
|
||||
|
||||
test("preserves previous V2 state through the current migration lineage", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
yield* db.run(sql`PRAGMA foreign_keys = ON`)
|
||||
yield* db.run(sql`CREATE TABLE migration (id text PRIMARY KEY, time_completed integer NOT NULL)`)
|
||||
yield* db.run(sql`
|
||||
INSERT INTO migration (id, time_completed)
|
||||
VALUES ('20260730195856_optional_session_title', 1)
|
||||
`)
|
||||
yield* db.run(sql`CREATE TABLE project (id text PRIMARY KEY)`)
|
||||
yield* db.run(sql`
|
||||
CREATE TABLE project_directory (
|
||||
project_id text NOT NULL,
|
||||
directory text NOT NULL,
|
||||
type text,
|
||||
strategy text,
|
||||
time_created integer NOT NULL,
|
||||
PRIMARY KEY (project_id, directory)
|
||||
)
|
||||
`)
|
||||
yield* db.run(sql`
|
||||
CREATE TABLE workspace (
|
||||
id text PRIMARY KEY,
|
||||
type text NOT NULL,
|
||||
name text NOT NULL,
|
||||
project_id text NOT NULL,
|
||||
time_used integer NOT NULL
|
||||
)
|
||||
`)
|
||||
yield* db.run(sql`
|
||||
CREATE TABLE session (
|
||||
id text PRIMARY KEY,
|
||||
project_id text NOT NULL REFERENCES project(id) ON DELETE CASCADE,
|
||||
workspace_id text,
|
||||
parent_id text,
|
||||
time_suspended integer
|
||||
)
|
||||
`)
|
||||
yield* db.run(sql`CREATE INDEX session_project_idx ON session (project_id)`)
|
||||
yield* db.run(sql`CREATE INDEX session_workspace_idx ON session (workspace_id)`)
|
||||
yield* db.run(sql`CREATE INDEX session_parent_idx ON session (parent_id)`)
|
||||
yield* db.run(
|
||||
sql`CREATE INDEX session_time_suspended_idx ON session (time_suspended) WHERE "session"."time_suspended" IS NOT NULL`,
|
||||
)
|
||||
yield* db.run(sql`
|
||||
CREATE TABLE session_message (
|
||||
id text PRIMARY KEY,
|
||||
session_id text NOT NULL REFERENCES session(id) ON DELETE CASCADE,
|
||||
data text NOT NULL
|
||||
)
|
||||
`)
|
||||
yield* db.run(sql`CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL)`)
|
||||
yield* db.run(sql`
|
||||
CREATE TABLE session_pending (
|
||||
id text PRIMARY KEY,
|
||||
session_id text NOT NULL REFERENCES session(id) ON DELETE CASCADE
|
||||
)
|
||||
`)
|
||||
yield* db.run(sql`CREATE TABLE event_sequence (aggregate_id text PRIMARY KEY, seq integer NOT NULL)`)
|
||||
yield* db.run(sql`
|
||||
CREATE TABLE event (
|
||||
id text PRIMARY KEY,
|
||||
aggregate_id text NOT NULL REFERENCES event_sequence(aggregate_id) ON DELETE CASCADE,
|
||||
seq integer NOT NULL,
|
||||
created integer NOT NULL,
|
||||
type text NOT NULL,
|
||||
data text NOT NULL
|
||||
)
|
||||
`)
|
||||
yield* db.run(sql`CREATE TABLE data_migration (name text PRIMARY KEY)`)
|
||||
yield* db.run(sql`INSERT INTO project VALUES ('project')`)
|
||||
yield* db.run(sql`INSERT INTO project_directory VALUES ('project', '/repo', 'main', NULL, 1)`)
|
||||
yield* db.run(sql`INSERT INTO session VALUES ('session', 'project', NULL, NULL, NULL)`)
|
||||
yield* db.run(sql`INSERT INTO session_message VALUES ('message', 'session', '{"text":"preserved"}')`)
|
||||
yield* db.run(sql`INSERT INTO session_pending VALUES ('pending', 'session')`)
|
||||
yield* db.run(sql`INSERT INTO event_sequence VALUES ('session', 41)`)
|
||||
yield* db.run(sql`INSERT INTO event VALUES ('event', 'session', 41, 1, 'session.text.ended.1', '{}')`)
|
||||
|
||||
yield* DatabaseMigration.applyOnly(db, [
|
||||
previousV2Migration,
|
||||
workspaceMigration,
|
||||
executionClaimsMigration,
|
||||
sessionInboxMigration,
|
||||
worktreeMigration,
|
||||
])
|
||||
|
||||
expect(yield* db.get(sql`SELECT id, resume_attempts FROM session_v2`)).toEqual({
|
||||
id: "session",
|
||||
resume_attempts: 0,
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT id, data FROM session_message`)).toEqual({
|
||||
id: "message",
|
||||
data: '{"text":"preserved"}',
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT id FROM session_pending`)).toEqual({ id: "pending" })
|
||||
expect(yield* db.get(sql`SELECT seq FROM event_sequence`)).toEqual({ seq: 41 })
|
||||
expect(yield* db.get(sql`SELECT id, seq FROM event`)).toEqual({ id: "event", seq: 41 })
|
||||
expect(
|
||||
yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`),
|
||||
).toBeUndefined()
|
||||
expect(yield* db.get(sql`SELECT directory FROM worktree`)).toEqual({ directory: "/repo" })
|
||||
expect(yield* db.all<{ table: string }>(sql`PRAGMA foreign_key_list(session_message)`)).toContainEqual(
|
||||
expect.objectContaining({ table: "session_v2" }),
|
||||
)
|
||||
expect(yield* db.all<{ table: string }>(sql`PRAGMA foreign_key_list(session_pending)`)).toContainEqual(
|
||||
expect.objectContaining({ table: "session_v2" }),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("rejects previous V2 databases with V1-only session history", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
const db = yield* makeDb
|
||||
yield* db.run(sql`CREATE TABLE migration (id text PRIMARY KEY, time_completed integer NOT NULL)`)
|
||||
yield* db.run(sql`
|
||||
INSERT INTO migration (id, time_completed)
|
||||
VALUES ('20260730195856_optional_session_title', 1)
|
||||
`)
|
||||
yield* db.run(sql`CREATE TABLE session (id text PRIMARY KEY)`)
|
||||
yield* db.run(sql`CREATE TABLE session_message (id text PRIMARY KEY, session_id text NOT NULL)`)
|
||||
yield* db.run(sql`CREATE TABLE message (id text PRIMARY KEY, session_id text NOT NULL)`)
|
||||
yield* db.run(sql`INSERT INTO session VALUES ('session')`)
|
||||
yield* db.run(sql`INSERT INTO message VALUES ('message', 'session')`)
|
||||
|
||||
expect((yield* Effect.exit(DatabaseMigration.applyOnly(db, [previousV2Migration])))._tag).toBe("Failure")
|
||||
expect(yield* db.get(sql`SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'session'`)).toEqual({
|
||||
name: "session",
|
||||
})
|
||||
expect(yield* db.get(sql`SELECT id FROM migration WHERE id = ${previousV2Migration.id}`)).toBeUndefined()
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
test("copies project directories into worktrees without removing the old table", async () => {
|
||||
await run(
|
||||
Effect.gen(function* () {
|
||||
|
||||
@@ -51,7 +51,7 @@ describe("InstructionBuiltIns", () => {
|
||||
` Workspace root folder: ${projectDirectory}`,
|
||||
" Is directory a git repo: yes",
|
||||
` Platform: ${process.platform}`,
|
||||
` Prefer ${temporary} over generic system temporary directories such as /tmp; it is pre-created and approved for external access.`,
|
||||
` Use ${temporary} for temporary work outside the workspace; it already exists and is pre-approved for external directory access.`,
|
||||
"</env>",
|
||||
"",
|
||||
`Today's date: ${localDate(timestamp)}`,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, test } from "bun:test"
|
||||
import { refreshAuthorization } from "@modelcontextprotocol/sdk/client/auth.js"
|
||||
|
||||
describe("MCP OAuth", () => {
|
||||
test("shares concurrent refreshes for the same token", async () => {
|
||||
let requests = 0
|
||||
const pending = Promise.withResolvers<void>()
|
||||
const options = {
|
||||
metadata: {
|
||||
issuer: "https://auth.example.com",
|
||||
authorization_endpoint: "https://auth.example.com/authorize",
|
||||
token_endpoint: "https://auth.example.com/token",
|
||||
response_types_supported: ["code"],
|
||||
},
|
||||
clientInformation: { client_id: "client" },
|
||||
refreshToken: "refresh",
|
||||
fetchFn: async () => {
|
||||
requests++
|
||||
await pending.promise
|
||||
return Response.json({ access_token: "access", token_type: "Bearer", refresh_token: "next" })
|
||||
},
|
||||
}
|
||||
|
||||
const first = refreshAuthorization(new URL("https://auth.example.com"), options)
|
||||
const second = refreshAuthorization(new URL("https://auth.example.com"), options)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(requests).toBe(1)
|
||||
pending.resolve()
|
||||
expect(await Promise.all([first, second])).toEqual([
|
||||
{ access_token: "access", token_type: "Bearer", refresh_token: "next" },
|
||||
{ access_token: "access", token_type: "Bearer", refresh_token: "next" },
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -782,40 +782,6 @@ describe("ShellTool", () => {
|
||||
),
|
||||
)
|
||||
|
||||
if (!isWindows) {
|
||||
it.live("settles a shell terminated by an external signal", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
(tmp) => {
|
||||
reset()
|
||||
return withSession(tmp.path, (registry) =>
|
||||
Effect.gen(function* () {
|
||||
const shell = yield* Shell.Service
|
||||
const settled = yield* executeTool(
|
||||
registry,
|
||||
call({ command: idleCommand, background: true }, "call-external-signal"),
|
||||
)
|
||||
const shellID = settled.metadata?.shellID
|
||||
expect(typeof shellID).toBe("string")
|
||||
if (typeof shellID !== "string") return
|
||||
const id = ShellSchema.ID.make(shellID)
|
||||
const info = yield* shell.get(id)
|
||||
expect(typeof info.pid).toBe("number")
|
||||
if (info.pid === undefined) return
|
||||
|
||||
process.kill(-info.pid, "SIGTERM")
|
||||
const result = yield* shell.wait(id).pipe(Effect.timeoutOption(Duration.seconds(1)))
|
||||
expect(result._tag).toBe("Some")
|
||||
if (result._tag === "Some") expect(result.value.status).toBe("exited")
|
||||
expect((yield* shell.list()).map((item) => item.id)).not.toContain(id)
|
||||
}),
|
||||
)
|
||||
},
|
||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]().then(() => undefined)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
it.live("backgrounds a foreground command when the session is signaled", () =>
|
||||
Effect.acquireUseRelease(
|
||||
Effect.promise(() => tmpdir()),
|
||||
|
||||
@@ -452,6 +452,7 @@ export function Prompt(props: PromptProps) {
|
||||
title: "Queue prompt",
|
||||
name: "prompt.queue",
|
||||
category: "Prompt",
|
||||
palette: undefined,
|
||||
run: async (_input: string | undefined, event?: KeyEvent) => {
|
||||
event?.preventDefault()
|
||||
event?.stopPropagation()
|
||||
|
||||
@@ -23,7 +23,7 @@ import {
|
||||
NEW_SESSION_TAB_TITLE,
|
||||
sessionTabComplete,
|
||||
sessionTabDetail,
|
||||
sessionTabNumberLabel,
|
||||
sessionTabShortcutLabel,
|
||||
seedSessionTabMotion,
|
||||
sessionTabOverflowWidth,
|
||||
type SessionTab,
|
||||
@@ -426,7 +426,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
const value = session()
|
||||
return value ? data.project.get(value.projectID) : undefined
|
||||
})
|
||||
const numberWidth = () => Math.max(2, String(items().length).length)
|
||||
const numberWidth = () => 2
|
||||
const restingTitleWidth = () => Math.max(1, width() - numberWidth() - 2)
|
||||
const hoveredTitleWidth = () => Math.max(1, restingTitleWidth() - 1)
|
||||
const titleWidth = () => (hovered() === tab.sessionID ? hoveredTitleWidth() : restingTitleWidth())
|
||||
@@ -657,14 +657,14 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
backgroundColor={pulseBackground()}
|
||||
onLevel={setSweepLevel}
|
||||
/>
|
||||
<box zIndex={1} width="100%" flexDirection="row" paddingRight={1}>
|
||||
<box zIndex={1} width="100%" flexDirection="row" paddingLeft={1} paddingRight={1}>
|
||||
<text
|
||||
width={numberWidth() + 1}
|
||||
width={numberWidth()}
|
||||
fg={numberColor()}
|
||||
selectable={false}
|
||||
attributes={selected() ? TextAttributes.BOLD : undefined}
|
||||
>
|
||||
{sessionTabNumberLabel(index()).padStart(numberWidth())}
|
||||
{sessionTabShortcutLabel(index())}
|
||||
</text>
|
||||
<text
|
||||
width={titleWidth()}
|
||||
@@ -1040,7 +1040,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
const glows = () => !selected() && (status().attention || (!status().busy && status().unread !== undefined))
|
||||
const title = () => tab.title ?? "Untitled session"
|
||||
const tabNumber = createMemo(() => items().findIndex((item) => item.sessionID === tab.sessionID) + 1)
|
||||
const numberWidth = () => Math.max(2, String(items().length).length)
|
||||
// Shortcut labels stay one cell wide: 1-9, 0 for ten, then a neutral dot.
|
||||
const numberWidth = () => 2
|
||||
// Hovering reveals the close mark, so the title's right bound shifts left of it.
|
||||
const restingTitleWidth = () => Math.max(1, width() - 1 - numberWidth())
|
||||
const hoveredTitleWidth = () => Math.max(1, restingTitleWidth() - 2)
|
||||
@@ -1140,8 +1141,11 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
onLevel={setSweepLevel}
|
||||
/>
|
||||
<box zIndex={1} width="100%" flexDirection="row">
|
||||
<text width={numberWidth() + 1} fg={numberColor()} selectable={false} attributes={bold()}>
|
||||
{(tab === NEW_SESSION_TAB ? "+" : sessionTabNumberLabel(tabNumber() - 1)).padStart(numberWidth())}
|
||||
<text width={1} selectable={false}>
|
||||
{" "}
|
||||
</text>
|
||||
<text width={numberWidth()} fg={numberColor()} selectable={false} attributes={bold()}>
|
||||
{tab === NEW_SESSION_TAB ? "+" : sessionTabShortcutLabel(tabNumber() - 1)}
|
||||
</text>
|
||||
<text
|
||||
width={availableTitleWidth()}
|
||||
|
||||
@@ -178,7 +178,7 @@ export const Definitions = {
|
||||
"session.toggle.thinking": keybind("none", "Toggle thinking blocks visibility"),
|
||||
|
||||
"prompt.submit": keybind("none", "Submit prompt"),
|
||||
"prompt.queue": keybind("<leader>return", "Queue prompt"),
|
||||
"prompt.queue": keybind("alt+return", "Queue prompt"),
|
||||
"prompt.editor_context.clear": keybind("none", "Clear editor context"),
|
||||
"prompt.images.view": keybind("<leader>i", "View image attachments"),
|
||||
"prompt.skills": keybind("none", "Open skill selector"),
|
||||
|
||||
@@ -163,7 +163,7 @@ export const Definitions = {
|
||||
display_thinking: keybind("none", "Toggle thinking blocks visibility"),
|
||||
|
||||
prompt_submit: keybind("none", "Submit prompt"),
|
||||
prompt_queue: keybind("<leader>return", "Queue prompt"),
|
||||
prompt_queue: keybind("alt+return", "Queue prompt"),
|
||||
prompt_editor_context_clear: keybind("none", "Clear editor context"),
|
||||
prompt_images_view: keybind("<leader>i", "View image attachments"),
|
||||
prompt_skills: keybind("none", "Open skill selector"),
|
||||
|
||||
@@ -7,8 +7,10 @@ export type SessionTabUnread = "activity" | "error"
|
||||
|
||||
export const NEW_SESSION_TAB_TITLE = "New session"
|
||||
|
||||
export function sessionTabNumberLabel(index: number) {
|
||||
return String(index + 1)
|
||||
export function sessionTabShortcutLabel(index: number) {
|
||||
if (index >= 0 && index < 9) return String(index + 1)
|
||||
if (index === 9) return "0"
|
||||
return "·"
|
||||
}
|
||||
|
||||
export function sessionTabDetail(
|
||||
|
||||
@@ -1050,7 +1050,6 @@ export function createPromptState(input: PromptInput): PromptState {
|
||||
id: "prompt.queue",
|
||||
title: "Queue prompt",
|
||||
group: "Prompt",
|
||||
palette: true,
|
||||
run() {
|
||||
syncDraft()
|
||||
submitPrompt(promptCopy(draft), "queue")
|
||||
|
||||
@@ -595,7 +595,7 @@ export function RunFooterView(props: RunFooterViewProps) {
|
||||
{
|
||||
id: "session.queued_prompts",
|
||||
title: "View queued prompts",
|
||||
group: "Prompt",
|
||||
group: "Session",
|
||||
run: openQueuedMenu,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1062,7 +1062,7 @@ export function Session(props: { verticalTabsWidth: number }) {
|
||||
{
|
||||
title: "View queued prompts",
|
||||
id: "session.queued_prompts",
|
||||
group: "Prompt",
|
||||
group: "Session",
|
||||
enabled: queuedPrompts().length > 0,
|
||||
run: openQueuedPrompts,
|
||||
},
|
||||
|
||||
@@ -107,7 +107,6 @@ test("preserves migrated v1 keybind defaults", () => {
|
||||
const pairs = [
|
||||
["app.exit", "app_exit"],
|
||||
["prompt.paste", "input_paste"],
|
||||
["prompt.queue", "prompt_queue"],
|
||||
["session.delete", "session_delete"],
|
||||
["session.list", "session_list"],
|
||||
["agent.list", "agent_list"],
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
sessionTabComplete,
|
||||
sessionTabDetail,
|
||||
sessionTabOverflowWidth,
|
||||
sessionTabNumberLabel,
|
||||
sessionTabShortcutLabel,
|
||||
} from "../../src/context/session-tabs-model"
|
||||
|
||||
describe("session tabs", () => {
|
||||
@@ -25,8 +25,8 @@ describe("session tabs", () => {
|
||||
expect(sessionTabDetail("opencode", undefined, "main", true)).toBe("opencode")
|
||||
})
|
||||
|
||||
test("labels tabs by ordinal", () => {
|
||||
expect(Array.from({ length: 12 }, (_, index) => sessionTabNumberLabel(index))).toEqual([
|
||||
test("labels direct shortcut tabs and marks unbound tabs with a dot", () => {
|
||||
expect(Array.from({ length: 12 }, (_, index) => sessionTabShortcutLabel(index))).toEqual([
|
||||
"1",
|
||||
"2",
|
||||
"3",
|
||||
@@ -36,9 +36,9 @@ describe("session tabs", () => {
|
||||
"7",
|
||||
"8",
|
||||
"9",
|
||||
"10",
|
||||
"11",
|
||||
"12",
|
||||
"0",
|
||||
"·",
|
||||
"·",
|
||||
])
|
||||
})
|
||||
|
||||
|
||||
@@ -981,8 +981,7 @@ test("direct footer steers the oldest queued prompt from an empty composer", asy
|
||||
|
||||
try {
|
||||
await app.renderOnce()
|
||||
app.mockInput.pressKey("x", { ctrl: true })
|
||||
app.mockInput.pressEnter()
|
||||
app.mockInput.pressEnter({ meta: true })
|
||||
await Bun.sleep(0)
|
||||
expect(steered).toEqual([])
|
||||
app.mockInput.pressEnter()
|
||||
@@ -1035,8 +1034,7 @@ test("direct footer rejects local commands submitted with the queue shortcut", a
|
||||
try {
|
||||
await app.renderOnce()
|
||||
await app.mockInput.typeText("/settings ")
|
||||
app.mockInput.pressKey("x", { ctrl: true })
|
||||
app.mockInput.pressEnter()
|
||||
app.mockInput.pressEnter({ meta: true })
|
||||
await Bun.sleep(0)
|
||||
expect(submitted).toEqual([])
|
||||
expect(statuses).toContain("this prompt cannot be queued")
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("run runtime boot", () => {
|
||||
expect(result.keybinds.get("prompt.clear")?.[0]?.key).toBe("ctrl+c")
|
||||
expect(result.keybinds.get("input.submit")?.[0]?.key).toBe("return")
|
||||
expect(result.keybinds.get("input.newline")?.[0]?.key).toBe("shift+return,ctrl+return,ctrl+j")
|
||||
expect(result.keybinds.get("prompt.queue")?.[0]?.key).toBe("<leader>return")
|
||||
expect(result.keybinds.get("prompt.queue")?.[0]?.key).toBe("alt+return")
|
||||
})
|
||||
|
||||
test("preserves shared config while resolving independent Mini defaults", async () => {
|
||||
|
||||
@@ -1,5 +1,138 @@
|
||||
diff --git a/dist/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts
|
||||
index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..b4a3a3b33bc97206c6835e2ee221cc13456210e4 100644
|
||||
--- a/dist/cjs/client/auth.d.ts
|
||||
+++ b/dist/cjs/client/auth.d.ts
|
||||
@@ -205,6 +205,15 @@ export declare function selectClientAuthMethod(clientInformation: OAuthClientInf
|
||||
* @returns A Promise that resolves to an OAuthError instance
|
||||
*/
|
||||
export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
|
||||
+/**
|
||||
+ * Selects scopes per the MCP spec and augments them for refresh token support.
|
||||
+ */
|
||||
+export declare function determineScope(options: {
|
||||
+ requestedScope?: string;
|
||||
+ resourceMetadata?: OAuthProtectedResourceMetadata;
|
||||
+ authServerMetadata?: AuthorizationServerMetadata;
|
||||
+ clientMetadata: OAuthClientMetadata;
|
||||
+}): string | undefined;
|
||||
/**
|
||||
* Orchestrates the full auth flow with a server.
|
||||
*
|
||||
diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js
|
||||
index c2e4fa91d26f5336889f6afa416147db75fc4872..152eed7cbb6e39ce4d711cf28a3e8d8fcf6d699d 100644
|
||||
--- a/dist/cjs/client/auth.js
|
||||
+++ b/dist/cjs/client/auth.js
|
||||
@@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0;
|
||||
exports.selectClientAuthMethod = selectClientAuthMethod;
|
||||
exports.parseErrorResponse = parseErrorResponse;
|
||||
exports.auth = auth;
|
||||
+exports.determineScope = determineScope;
|
||||
exports.isHttpsUrl = isHttpsUrl;
|
||||
exports.selectResourceURL = selectResourceURL;
|
||||
exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams;
|
||||
@@ -186,6 +187,19 @@ async function auth(provider, options) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
+/**
|
||||
+ * Selects scopes per the MCP spec and augments them for refresh token support.
|
||||
+ */
|
||||
+function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
|
||||
+ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
|
||||
+ if (effectiveScope &&
|
||||
+ authServerMetadata?.scopes_supported?.includes('offline_access') &&
|
||||
+ !effectiveScope.split(' ').includes('offline_access') &&
|
||||
+ clientMetadata.grant_types?.includes('refresh_token')) {
|
||||
+ effectiveScope = `${effectiveScope} offline_access`;
|
||||
+ }
|
||||
+ return effectiveScope;
|
||||
+}
|
||||
async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
|
||||
// Check if the provider has cached discovery state to skip discovery
|
||||
const cachedState = await provider.discoveryState?.();
|
||||
@@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
||||
});
|
||||
}
|
||||
const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
|
||||
- // Apply scope selection strategy (SEP-835):
|
||||
- // 1. WWW-Authenticate scope (passed via `scope` param)
|
||||
- // 2. PRM scopes_supported
|
||||
- // 3. Client metadata scope (user-configured fallback)
|
||||
- // The resolved scope is used consistently for both DCR and the authorization request.
|
||||
- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
|
||||
+ const resolvedScope = determineScope({
|
||||
+ requestedScope: scope,
|
||||
+ resourceMetadata,
|
||||
+ authServerMetadata: metadata,
|
||||
+ clientMetadata: provider.clientMetadata
|
||||
+ });
|
||||
// Handle client registration if needed
|
||||
let clientInformation = await Promise.resolve(provider.clientInformation());
|
||||
if (!clientInformation) {
|
||||
@@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
|
||||
if (scope) {
|
||||
authorizationUrl.searchParams.set('scope', scope);
|
||||
}
|
||||
- if (scope?.includes('offline_access')) {
|
||||
+ if (scope?.split(' ').includes('offline_access')) {
|
||||
// if the request includes the OIDC-only "offline_access" scope,
|
||||
// we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
@@ -837,21 +851,38 @@ async function exchangeAuthorization(authorizationServerUrl, { metadata, clientI
|
||||
* @returns Promise resolving to OAuth tokens (preserves original refresh_token if not replaced)
|
||||
* @throws {Error} When token refresh fails or authentication is invalid
|
||||
*/
|
||||
+const refreshes = new Map();
|
||||
async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
|
||||
- const tokenRequestParams = new URLSearchParams({
|
||||
- grant_type: 'refresh_token',
|
||||
- refresh_token: refreshToken
|
||||
- });
|
||||
- const tokens = await executeTokenRequest(authorizationServerUrl, {
|
||||
- metadata,
|
||||
- tokenRequestParams,
|
||||
- clientInformation,
|
||||
- addClientAuthentication,
|
||||
- resource,
|
||||
- fetchFn
|
||||
- });
|
||||
- // Preserve original refresh token if server didn't return a new one
|
||||
- return { refresh_token: refreshToken, ...tokens };
|
||||
+ const key = `${authorizationServerUrl}\0${clientInformation.client_id}\0${refreshToken}`;
|
||||
+ const current = refreshes.get(key);
|
||||
+ if (current) {
|
||||
+ return current;
|
||||
+ }
|
||||
+ const refresh = (async () => {
|
||||
+ const tokenRequestParams = new URLSearchParams({
|
||||
+ grant_type: 'refresh_token',
|
||||
+ refresh_token: refreshToken
|
||||
+ });
|
||||
+ const tokens = await executeTokenRequest(authorizationServerUrl, {
|
||||
+ metadata,
|
||||
+ tokenRequestParams,
|
||||
+ clientInformation,
|
||||
+ addClientAuthentication,
|
||||
+ resource,
|
||||
+ fetchFn
|
||||
+ });
|
||||
+ // Preserve original refresh token if server didn't return a new one
|
||||
+ return { refresh_token: refreshToken, ...tokens };
|
||||
+ })();
|
||||
+ refreshes.set(key, refresh);
|
||||
+ try {
|
||||
+ return await refresh;
|
||||
+ }
|
||||
+ finally {
|
||||
+ if (refreshes.get(key) === refresh) {
|
||||
+ refreshes.delete(key);
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
/**
|
||||
* Unified token fetching that works with any grant type via provider.prepareTokenRequest().
|
||||
diff --git a/dist/cjs/client/index.d.ts b/dist/cjs/client/index.d.ts
|
||||
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
|
||||
index 6f567a193626587a2730b5a49293ca5dfd4181ea..5b7c841c000508e389ce617f559f7c2a5126ca9f 100644
|
||||
--- a/dist/cjs/client/index.d.ts
|
||||
+++ b/dist/cjs/client/index.d.ts
|
||||
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
|
||||
@@ -7,25 +140,12 @@ index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d01
|
||||
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
||||
*/
|
||||
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
|
||||
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
|
||||
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
|
||||
[x: string]: unknown;
|
||||
content: ({
|
||||
diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
|
||||
index 1822bf749aec71d2bb295083d832114ee187bb67..58b859a7b32222fb5cb9f2011fdc5d010f3d05fb 100644
|
||||
--- a/dist/esm/client/index.d.ts
|
||||
+++ b/dist/esm/client/index.d.ts
|
||||
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
|
||||
*
|
||||
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
||||
*/
|
||||
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
|
||||
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
|
||||
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
|
||||
[x: string]: unknown;
|
||||
content: ({
|
||||
diff --git a/dist/cjs/client/index.js b/dist/cjs/client/index.js
|
||||
index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c101584c84 100644
|
||||
index 6ac1da14dc7f6211ae70f7711c124b76098816d8..8a0200720454eac591e174f6a948212af8f852fc 100644
|
||||
--- a/dist/cjs/client/index.js
|
||||
+++ b/dist/cjs/client/index.js
|
||||
@@ -288,41 +288,16 @@ class Client extends protocol_js_1.Protocol {
|
||||
@@ -112,7 +232,8 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1
|
||||
/**
|
||||
* After initialization has completed, this will be populated with the server's reported capabilities.
|
||||
*/
|
||||
@@ -541,9 +547,11 @@ class Client extends protocol_js_1.Protocol {
|
||||
@@ -540,10 +546,12 @@ class Client extends protocol_js_1.Protocol {
|
||||
* Cache validators for tool output schemas.
|
||||
* Called after listTools() to pre-compile validators for better performance.
|
||||
*/
|
||||
- cacheToolMetadata(tools) {
|
||||
@@ -138,7 +259,7 @@ index 6ac1da14dc7f6211ae70f7711c124b76098816d8..adb5b7bd45514a406a0f7e40b64631c1
|
||||
}
|
||||
/**
|
||||
diff --git a/dist/cjs/client/streamableHttp.js b/dist/cjs/client/streamableHttp.js
|
||||
index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eedff5443c9 100644
|
||||
index a29a7d3a0f14d9cd800ef5b296485237350c666f..a55e7ed79d18c5fb913227d5b8e5ca6f44cb51e4 100644
|
||||
--- a/dist/cjs/client/streamableHttp.js
|
||||
+++ b/dist/cjs/client/streamableHttp.js
|
||||
@@ -204,7 +204,7 @@ class StreamableHTTPClientTransport {
|
||||
@@ -238,7 +359,7 @@ index a29a7d3a0f14d9cd800ef5b296485237350c666f..c362ae5fe6c62c8c8eae7e2e61de1eed
|
||||
}
|
||||
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
||||
diff --git a/dist/cjs/shared/protocol.js b/dist/cjs/shared/protocol.js
|
||||
index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb77538182b 100644
|
||||
index 3617e787f0ba70447c99501aee7aa67584d89758..4ee4d158391558fdc1f977f5134b7cacfc45e8c3 100644
|
||||
--- a/dist/cjs/shared/protocol.js
|
||||
+++ b/dist/cjs/shared/protocol.js
|
||||
@@ -744,7 +744,12 @@ class Protocol {
|
||||
@@ -255,91 +376,11 @@ index 3617e787f0ba70447c99501aee7aa67584d89758..4a96d6a0328fa348b96f3869ab7e0bb7
|
||||
this._cleanupTimeout(messageId);
|
||||
reject(error);
|
||||
});
|
||||
diff --git a/dist/cjs/client/auth.d.ts b/dist/cjs/client/auth.d.ts
|
||||
index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
|
||||
--- a/dist/cjs/client/auth.d.ts
|
||||
+++ b/dist/cjs/client/auth.d.ts
|
||||
@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
|
||||
* @returns A Promise that resolves to an OAuthError instance
|
||||
*/
|
||||
export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
|
||||
+/**
|
||||
+ * Selects scopes per the MCP spec and augments them for refresh token support.
|
||||
+ */
|
||||
+export declare function determineScope(options: {
|
||||
+ requestedScope?: string;
|
||||
+ resourceMetadata?: OAuthProtectedResourceMetadata;
|
||||
+ authServerMetadata?: AuthorizationServerMetadata;
|
||||
+ clientMetadata: OAuthClientMetadata;
|
||||
+}): string | undefined;
|
||||
/**
|
||||
* Orchestrates the full auth flow with a server.
|
||||
*
|
||||
diff --git a/dist/cjs/client/auth.js b/dist/cjs/client/auth.js
|
||||
index c2e4fa91d26f5336889f6afa416147db75fc4872..178d7cfd96412d53bc14bbc13a8f76c11f727ee7 100644
|
||||
--- a/dist/cjs/client/auth.js
|
||||
+++ b/dist/cjs/client/auth.js
|
||||
@@ -7,6 +7,7 @@ exports.UnauthorizedError = void 0;
|
||||
exports.selectClientAuthMethod = selectClientAuthMethod;
|
||||
exports.parseErrorResponse = parseErrorResponse;
|
||||
exports.auth = auth;
|
||||
+exports.determineScope = determineScope;
|
||||
exports.isHttpsUrl = isHttpsUrl;
|
||||
exports.selectResourceURL = selectResourceURL;
|
||||
exports.extractWWWAuthenticateParams = extractWWWAuthenticateParams;
|
||||
@@ -186,6 +187,19 @@ async function auth(provider, options) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
+/**
|
||||
+ * Selects scopes per the MCP spec and augments them for refresh token support.
|
||||
+ */
|
||||
+function determineScope({ requestedScope, resourceMetadata, authServerMetadata, clientMetadata }) {
|
||||
+ let effectiveScope = requestedScope || resourceMetadata?.scopes_supported?.join(' ') || clientMetadata.scope;
|
||||
+ if (effectiveScope &&
|
||||
+ authServerMetadata?.scopes_supported?.includes('offline_access') &&
|
||||
+ !effectiveScope.split(' ').includes('offline_access') &&
|
||||
+ clientMetadata.grant_types?.includes('refresh_token')) {
|
||||
+ effectiveScope = `${effectiveScope} offline_access`;
|
||||
+ }
|
||||
+ return effectiveScope;
|
||||
+}
|
||||
async function authInternal(provider, { serverUrl, authorizationCode, scope, resourceMetadataUrl, fetchFn }) {
|
||||
// Check if the provider has cached discovery state to skip discovery
|
||||
const cachedState = await provider.discoveryState?.();
|
||||
@@ -241,12 +255,12 @@ async function authInternal(provider, { serverUrl, authorizationCode, scope, res
|
||||
});
|
||||
}
|
||||
const resource = await selectResourceURL(serverUrl, provider, resourceMetadata);
|
||||
- // Apply scope selection strategy (SEP-835):
|
||||
- // 1. WWW-Authenticate scope (passed via `scope` param)
|
||||
- // 2. PRM scopes_supported
|
||||
- // 3. Client metadata scope (user-configured fallback)
|
||||
- // The resolved scope is used consistently for both DCR and the authorization request.
|
||||
- const resolvedScope = scope || resourceMetadata?.scopes_supported?.join(' ') || provider.clientMetadata.scope;
|
||||
+ const resolvedScope = determineScope({
|
||||
+ requestedScope: scope,
|
||||
+ resourceMetadata,
|
||||
+ authServerMetadata: metadata,
|
||||
+ clientMetadata: provider.clientMetadata
|
||||
+ });
|
||||
// Handle client registration if needed
|
||||
let clientInformation = await Promise.resolve(provider.clientInformation());
|
||||
if (!clientInformation) {
|
||||
@@ -741,7 +755,7 @@ async function startAuthorization(authorizationServerUrl, { metadata, clientInfo
|
||||
if (scope) {
|
||||
authorizationUrl.searchParams.set('scope', scope);
|
||||
}
|
||||
- if (scope?.includes('offline_access')) {
|
||||
+ if (scope?.split(' ').includes('offline_access')) {
|
||||
// if the request includes the OIDC-only "offline_access" scope,
|
||||
// we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
diff --git a/dist/esm/client/auth.d.ts b/dist/esm/client/auth.d.ts
|
||||
index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433beea1b32f04 100644
|
||||
index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..b4a3a3b33bc97206c6835e2ee221cc13456210e4 100644
|
||||
--- a/dist/esm/client/auth.d.ts
|
||||
+++ b/dist/esm/client/auth.d.ts
|
||||
@@ -205,6 +205,15 @@ export declare function parseErrorResponse(input: Response | string): Promise<OA
|
||||
@@ -205,6 +205,15 @@ export declare function selectClientAuthMethod(clientInformation: OAuthClientInf
|
||||
* @returns A Promise that resolves to an OAuthError instance
|
||||
*/
|
||||
export declare function parseErrorResponse(input: Response | string): Promise<OAuthError>;
|
||||
@@ -356,7 +397,7 @@ index f4363ce7c94fbddf0e1d5943b1b26682bdbaa40e..e7dd57096e4f056bcd735d5081433bee
|
||||
* Orchestrates the full auth flow with a server.
|
||||
*
|
||||
diff --git a/dist/esm/client/auth.js b/dist/esm/client/auth.js
|
||||
index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af61dcad98b 100644
|
||||
index e183040fc2bba22ca1ccc784984f3310854403b7..1fef5ff7926604d74d8bfae100a2be01040a0e99 100644
|
||||
--- a/dist/esm/client/auth.js
|
||||
+++ b/dist/esm/client/auth.js
|
||||
@@ -161,6 +161,19 @@ export async function auth(provider, options) {
|
||||
@@ -407,8 +448,74 @@ index e183040fc2bba22ca1ccc784984f3310854403b7..d367661e580ee61a96654f7af78b2af6
|
||||
// if the request includes the OIDC-only "offline_access" scope,
|
||||
// we need to set the prompt to "consent" to ensure the user is prompted to grant offline access
|
||||
// https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess
|
||||
@@ -812,21 +825,38 @@ export async function exchangeAuthorization(authorizationServerUrl, { metadata,
|
||||
* @returns Promise resolving to OAuth tokens (preserves original refresh_token if not replaced)
|
||||
* @throws {Error} When token refresh fails or authentication is invalid
|
||||
*/
|
||||
+const refreshes = new Map();
|
||||
export async function refreshAuthorization(authorizationServerUrl, { metadata, clientInformation, refreshToken, resource, addClientAuthentication, fetchFn }) {
|
||||
- const tokenRequestParams = new URLSearchParams({
|
||||
- grant_type: 'refresh_token',
|
||||
- refresh_token: refreshToken
|
||||
- });
|
||||
- const tokens = await executeTokenRequest(authorizationServerUrl, {
|
||||
- metadata,
|
||||
- tokenRequestParams,
|
||||
- clientInformation,
|
||||
- addClientAuthentication,
|
||||
- resource,
|
||||
- fetchFn
|
||||
- });
|
||||
- // Preserve original refresh token if server didn't return a new one
|
||||
- return { refresh_token: refreshToken, ...tokens };
|
||||
+ const key = `${authorizationServerUrl}\0${clientInformation.client_id}\0${refreshToken}`;
|
||||
+ const current = refreshes.get(key);
|
||||
+ if (current) {
|
||||
+ return current;
|
||||
+ }
|
||||
+ const refresh = (async () => {
|
||||
+ const tokenRequestParams = new URLSearchParams({
|
||||
+ grant_type: 'refresh_token',
|
||||
+ refresh_token: refreshToken
|
||||
+ });
|
||||
+ const tokens = await executeTokenRequest(authorizationServerUrl, {
|
||||
+ metadata,
|
||||
+ tokenRequestParams,
|
||||
+ clientInformation,
|
||||
+ addClientAuthentication,
|
||||
+ resource,
|
||||
+ fetchFn
|
||||
+ });
|
||||
+ // Preserve original refresh token if server didn't return a new one
|
||||
+ return { refresh_token: refreshToken, ...tokens };
|
||||
+ })();
|
||||
+ refreshes.set(key, refresh);
|
||||
+ try {
|
||||
+ return await refresh;
|
||||
+ }
|
||||
+ finally {
|
||||
+ if (refreshes.get(key) === refresh) {
|
||||
+ refreshes.delete(key);
|
||||
+ }
|
||||
+ }
|
||||
}
|
||||
/**
|
||||
* Unified token fetching that works with any grant type via provider.prepareTokenRequest().
|
||||
diff --git a/dist/esm/client/index.d.ts b/dist/esm/client/index.d.ts
|
||||
index 6f567a193626587a2730b5a49293ca5dfd4181ea..5b7c841c000508e389ce617f559f7c2a5126ca9f 100644
|
||||
--- a/dist/esm/client/index.d.ts
|
||||
+++ b/dist/esm/client/index.d.ts
|
||||
@@ -428,6 +428,8 @@ export declare class Client<RequestT extends Request = Request, NotificationT ex
|
||||
*
|
||||
* For task-based execution with streaming behavior, use client.experimental.tasks.callToolStream() instead.
|
||||
*/
|
||||
+ callTool(params: CallToolRequest['params'], resultSchema?: undefined, options?: RequestOptions): Promise<SchemaOutput<typeof CallToolResultSchema>>;
|
||||
+ callTool<T extends typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema>(params: CallToolRequest['params'], resultSchema: T, options?: RequestOptions): Promise<SchemaOutput<T>>;
|
||||
callTool(params: CallToolRequest['params'], resultSchema?: typeof CallToolResultSchema | typeof CompatibilityCallToolResultSchema, options?: RequestOptions): Promise<{
|
||||
[x: string]: unknown;
|
||||
content: ({
|
||||
diff --git a/dist/esm/client/index.js b/dist/esm/client/index.js
|
||||
index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8eb9e4caff 100644
|
||||
index 49b12c6cd918c457420fef7ad5528a9443d1a191..98c214181d9c4c1b197c53dfa79059788e2042e8 100644
|
||||
--- a/dist/esm/client/index.js
|
||||
+++ b/dist/esm/client/index.js
|
||||
@@ -284,41 +284,16 @@ export class Client extends Protocol {
|
||||
@@ -495,7 +602,8 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e
|
||||
/**
|
||||
* After initialization has completed, this will be populated with the server's reported capabilities.
|
||||
*/
|
||||
@@ -537,9 +543,11 @@ export class Client extends Protocol {
|
||||
@@ -536,10 +542,12 @@ export class Client extends Protocol {
|
||||
* Cache validators for tool output schemas.
|
||||
* Called after listTools() to pre-compile validators for better performance.
|
||||
*/
|
||||
- cacheToolMetadata(tools) {
|
||||
@@ -521,7 +629,7 @@ index 49b12c6cd918c457420fef7ad5528a9443d1a191..2afe2e22e960f26c9d516ef135d89f8e
|
||||
}
|
||||
/**
|
||||
diff --git a/dist/esm/client/streamableHttp.js b/dist/esm/client/streamableHttp.js
|
||||
index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da884fcc627a 100644
|
||||
index 624172aa24ae255a67c083f9c19053343e4a0581..f92c889456cab12de963959716846fb9770ed71d 100644
|
||||
--- a/dist/esm/client/streamableHttp.js
|
||||
+++ b/dist/esm/client/streamableHttp.js
|
||||
@@ -1,5 +1,5 @@
|
||||
@@ -628,7 +736,7 @@ index 624172aa24ae255a67c083f9c19053343e4a0581..ac75b14545fda44aff7ff4d97cc5da88
|
||||
}
|
||||
throw new StreamableHTTPError(response.status, `Error POSTing to endpoint: ${text}`);
|
||||
diff --git a/dist/esm/shared/protocol.js b/dist/esm/shared/protocol.js
|
||||
index bfa2b7120a0f50c569364ea5264e6f811076f44f..abd8dfd707c155f71dae7aeeeeaf7547368ac749 100644
|
||||
index bfa2b7120a0f50c569364ea5264e6f811076f44f..dec477d16a0fd796854542c1144279a6e86567f2 100644
|
||||
--- a/dist/esm/shared/protocol.js
|
||||
+++ b/dist/esm/shared/protocol.js
|
||||
@@ -740,7 +740,12 @@ export class Protocol {
|
||||
|
||||
Reference in New Issue
Block a user