@huanlin/dsh-plugin-copilot 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/cordis.patch.yml +17 -0
- package/lib/client.js +788 -0
- package/lib/client.js.map +1 -0
- package/lib/index.js +553 -0
- package/lib/invariant.js +23 -0
- package/lib/types/client/CopilotAuthCard.d.ts +16 -0
- package/lib/types/client/bindSnapshotSelector.d.ts +7 -0
- package/lib/types/client/controller.d.ts +98 -0
- package/lib/types/client/index.d.ts +24 -0
- package/lib/types/client/locales.d.ts +13 -0
- package/lib/types/gateway.d.ts +102 -0
- package/lib/types/index.d.ts +53 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/status.d.ts +82 -0
- package/lib/types/tools.d.ts +15 -0
- package/package.json +126 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.js","names":["cardStyle: CSSProperties","headerStyle: CSSProperties","headTextStyle: CSSProperties","titleStyle: CSSProperties","descStyle: CSSProperties","bodyStyle: CSSProperties","noticeStyle: CSSProperties","errorStyle: CSSProperties","successStyle: CSSProperties","codeRowStyle: CSSProperties","codeStyle: CSSProperties","btnBase: CSSProperties","btnPrimary: CSSProperties","actionsStyle: CSSProperties","inputStyle: CSSProperties","modelsStyle: CSSProperties","en: Record<CopilotKey, string>","zh: Record<CopilotKey, string>"],"sources":["../src/client/CopilotAuthCard.tsx","../src/client/controller.ts","../src/client/bindSnapshotSelector.ts","../src/client/locales.ts","../src/client/index.ts"],"sourcesContent":["/**\n * CopilotAuthCard — the `settings.plugin.item` slot occupant (key\n * `dsh-plugin-copilot`).\n *\n * A card in the Plugin Config page rendering the onboarding state machine:\n * unsupported (no pi-ai flow) / logged-out / pending (device code + polling\n * + cancel + prompts) / success / error, plus logged-in actions (sign out,\n * activate-route autofill when the profile is missing).\n *\n * @module @huanlin/dsh-plugin-copilot/client/CopilotAuthCard\n */\nimport { useState, type CSSProperties } from 'react'\nimport type { PropsLocale } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { CopilotCardState } from './controller.ts'\nimport type { CopilotAuthController } from './controller.ts'\n\n/** Inject face: the shared controller. */\nexport interface CopilotCardInjected {\n readonly controller: CopilotAuthController\n readonly useCard: <S>(select: (state: CopilotCardState) => S) => S\n}\n\n/** Full props: locale seat + inject. */\nexport type CopilotCardProps = PropsLocale<'dsh-plugin-copilot'> & CopilotCardInjected\n\nconst cardStyle: CSSProperties = {\n border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.22))',\n background: 'var(--dsw-alias-bg-layer-3, transparent)',\n borderRadius: 12,\n listStyle: 'none',\n}\n\nconst headerStyle: CSSProperties = {\n width: '100%',\n font: 'inherit',\n color: 'inherit',\n textAlign: 'left',\n cursor: 'pointer',\n background: 'transparent',\n border: 0,\n borderRadius: 12,\n alignItems: 'center',\n gap: 12,\n padding: '14px 16px',\n display: 'flex',\n boxSizing: 'border-box',\n}\n\nconst headTextStyle: CSSProperties = {\n flexDirection: 'column',\n flex: 1,\n gap: 4,\n minWidth: 0,\n display: 'flex',\n}\n\nconst titleStyle: CSSProperties = {\n color: 'var(--dsw-alias-label-primary, inherit)',\n fontSize: 15,\n fontWeight: 600,\n lineHeight: 1.4,\n}\n\nconst descStyle: CSSProperties = {\n color: 'var(--dsw-alias-label-tertiary, rgba(128,128,128,0.7))',\n fontSize: 13,\n lineHeight: 1.5,\n}\n\nconst badgeStyle = (tone: 'ok' | 'warn'): CSSProperties => ({\n whiteSpace: 'nowrap',\n background: tone === 'ok'\n ? 'var(--dsw-alias-state-success-bg, rgba(48,209,88,0.14))'\n : 'var(--dsw-alias-bg-module-platform, rgba(128,128,128,0.12))',\n color: tone === 'ok'\n ? 'var(--dsw-alias-state-success-primary, #30d158)'\n : 'var(--dsw-alias-label-secondary, inherit)',\n borderRadius: 999,\n flex: 'none',\n padding: '1px 8px',\n fontSize: 11,\n fontWeight: 500,\n lineHeight: '17px',\n})\n\nconst bodyStyle: CSSProperties = {\n borderTop: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.22))',\n margin: '0 16px',\n padding: '12px 0 4px',\n display: 'flex',\n flexDirection: 'column',\n gap: 12,\n}\n\nconst noticeStyle: CSSProperties = {\n color: 'var(--dsw-alias-label-secondary, inherit)',\n margin: 0,\n fontSize: 12,\n lineHeight: 1.6,\n}\n\nconst errorStyle: CSSProperties = {\n color: 'var(--dsw-alias-label-error, #ff453a)',\n margin: 0,\n fontSize: 12,\n lineHeight: 1.6,\n}\n\nconst successStyle: CSSProperties = {\n color: 'var(--dsw-alias-state-success-primary, #30d158)',\n margin: 0,\n fontSize: 12,\n lineHeight: 1.6,\n}\n\nconst codeRowStyle: CSSProperties = {\n alignItems: 'center',\n gap: 10,\n display: 'flex',\n flexWrap: 'wrap',\n}\n\nconst codeStyle: CSSProperties = {\n fontFamily: 'var(--dsw-font-mono, ui-monospace, monospace)',\n fontSize: 20,\n fontWeight: 700,\n letterSpacing: 2,\n color: 'var(--dsw-alias-label-primary, inherit)',\n}\n\nconst btnBase: CSSProperties = {\n appearance: 'none',\n font: 'inherit',\n cursor: 'pointer',\n border: '1px solid transparent',\n borderRadius: 8,\n padding: '5px 14px',\n fontSize: 13,\n fontWeight: 500,\n lineHeight: '20px',\n color: 'var(--dsw-alias-label-primary, inherit)',\n background: 'var(--dsw-alias-bg-module-platform, rgba(128,128,128,0.12))',\n}\n\nconst btnPrimary: CSSProperties = {\n ...btnBase,\n background: 'var(--dsw-alias-brand-primary, #0a84ff)',\n color: 'var(--dsw-alias-bg-layer-1, #fff)',\n}\n\nconst actionsStyle: CSSProperties = {\n display: 'flex',\n gap: 8,\n flexWrap: 'wrap',\n paddingBottom: 10,\n}\n\nconst inputStyle: CSSProperties = {\n flex: 1,\n padding: '6px 10px',\n fontSize: 13,\n borderRadius: 8,\n border: '1px solid var(--dsw-alias-border-l2, rgba(128,128,128,0.3))',\n background: 'var(--dsw-alias-bg-layer-3, transparent)',\n color: 'var(--dsw-alias-label-primary, inherit)',\n boxSizing: 'border-box',\n fontFamily: 'inherit',\n minWidth: 0,\n}\n\nconst modelsStyle: CSSProperties = {\n fontFamily: 'var(--dsw-font-mono, ui-monospace, monospace)',\n fontSize: 12,\n color: 'var(--dsw-alias-label-secondary, inherit)',\n overflowWrap: 'anywhere',\n}\n\nconst CHEVRON_SVG = '<svg width=\"14\" height=\"14\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><path d=\"M6 9l6 6 6-6\"/></svg>'\n\n/**\n * Render the Copilot onboarding card.\n * @param props - locale + controller inject.\n * @returns a `<li>` card element.\n */\nexport function CopilotAuthCard({ t, controller, useCard }: CopilotCardProps) {\n const state = useCard(snapshot => snapshot)\n const [open, setOpen] = useState(false)\n const [promptAnswer, setPromptAnswer] = useState('')\n if (!state.loaded) void controller.load()\n\n const unsupported = state.loaded && !state.status.flowAvailable\n // A login lifecycle in flight or freshly settled keeps the card open: the\n // device code and its outcome must never hide behind a collapsed header.\n const loginRevealed = state.login.kind !== 'idle'\n const expanded = open || unsupported || loginRevealed\n const busy = state.busy\n\n const header = (\n <button\n type=\"button\"\n style={headerStyle}\n aria-expanded={expanded}\n aria-label={t('card.title')}\n onClick={() => { setOpen(!open) }}\n >\n <span style={headTextStyle}>\n <span style={titleStyle}>{t('card.title')}</span>\n <span style={descStyle}>{t('card.intro')}</span>\n </span>\n {state.status.loggedIn\n ? <span style={badgeStyle('ok')}>{t('card.signedIn')}</span>\n : state.loaded && state.status.flowAvailable\n ? <span style={badgeStyle('warn')}>{t('card.signedOut')}</span>\n : null}\n {state.status.loggedIn\n ? <span style={badgeStyle(state.status.profileActivated ? 'ok' : 'warn')}>\n {t(state.status.profileActivated ? 'card.routeActive' : 'card.routeDormant')}\n </span>\n : null}\n <span\n style={{ color: 'var(--dsw-alias-label-tertiary, inherit)', flex: 'none', display: 'inline-flex', transform: expanded ? 'rotate(180deg)' : 'none' }}\n dangerouslySetInnerHTML={{ __html: CHEVRON_SVG }}\n />\n </button>\n )\n\n if (!expanded) {\n return <li style={cardStyle}>{header}</li>\n }\n\n return (\n <li style={cardStyle}>\n {header}\n <div style={bodyStyle}>\n {unsupported ? <p style={noticeStyle} role=\"status\">{t('card.unsupported')}</p> : null}\n\n {!unsupported && state.login.kind === 'idle' ? (\n <>\n {state.status.loggedIn ? null : (\n <div style={actionsStyle}>\n <button\n type=\"button\"\n style={btnPrimary}\n disabled={busy}\n onClick={() => { void controller.login() }}\n >\n {t('action.signIn')}\n </button>\n </div>\n )}\n {state.status.loggedIn ? (\n <div style={actionsStyle}>\n {!state.status.profileActivated ? (\n <button\n type=\"button\"\n style={btnPrimary}\n disabled={busy}\n onClick={() => { void controller.autofill() }}\n >\n {t('action.activate')}\n </button>\n ) : null}\n <button\n type=\"button\"\n style={btnBase}\n disabled={busy}\n onClick={() => { void controller.autofill() }}\n >\n {t('action.syncModels')}\n </button>\n <button\n type=\"button\"\n style={btnBase}\n disabled={busy}\n onClick={() => { void controller.logout() }}\n >\n {t('action.signOut')}\n </button>\n </div>\n ) : null}\n </>\n ) : null}\n\n {state.login.kind === 'running' ? (\n <>\n <p style={noticeStyle} role=\"status\">{t('state.pending')}</p>\n {state.deviceCode !== undefined ? (\n <div style={codeRowStyle}>\n <span style={noticeStyle}>{t('notice.deviceCode')}</span>\n <span style={codeStyle}>{state.deviceCode}</span>\n <button\n type=\"button\"\n style={btnBase}\n onClick={() => {\n void navigator.clipboard?.writeText(state.deviceCode ?? '')\n controller.markCopied()\n }}\n >\n {t(state.copied ? 'action.copied' : 'action.copyCode')}\n </button>\n {state.verificationUrl !== undefined ? (\n // window.open with a features string instead of an anchor:\n // in-page link clicks are intercepted by sidebar extensions\n // (dsh-better-sidebar renders them inside the app shell), while\n // a programmatic open with size features always lands in a\n // real browser window outside the app.\n <button\n type=\"button\"\n style={btnBase}\n onClick={() => {\n window.open(state.verificationUrl, '_blank', 'noopener,noreferrer,width=960,height=760')\n }}\n >\n {t('action.openUrl')}\n </button>\n ) : null}\n </div>\n ) : null}\n {state.events.filter(event => event.kind === 'notice' && event.code === undefined && event.message)\n .slice(-2)\n .map(event => <p key={event.seq} style={noticeStyle}>{event.message}</p>)}\n <div style={actionsStyle}>\n <button type=\"button\" style={btnBase} disabled={busy} onClick={() => { void controller.cancel() }}>\n {t('action.cancel')}\n </button>\n </div>\n </>\n ) : null}\n\n {state.openPrompt !== undefined && state.openPrompt.prompt !== undefined ? (\n <>\n <p style={noticeStyle} role=\"status\">{t('state.pendingPrompt')}</p>\n <p style={noticeStyle}>{state.openPrompt.prompt.message}</p>\n <div style={codeRowStyle}>\n {state.openPrompt.prompt.options === undefined ? (\n <>\n <input\n type={state.openPrompt.prompt.kind === 'secret' ? 'password' : 'text'}\n style={inputStyle}\n value={promptAnswer}\n placeholder={state.openPrompt.prompt.placeholder ?? t('prompt.placeholder')}\n onChange={(event) => { setPromptAnswer(event.target.value) }}\n />\n <button\n type=\"button\"\n style={btnPrimary}\n disabled={busy}\n onClick={() => {\n void controller.answer(promptAnswer, false)\n setPromptAnswer('')\n }}\n >\n {t('action.submit')}\n </button>\n </>\n ) : state.openPrompt.prompt.options.map(option => (\n <button\n key={option.id}\n type=\"button\"\n style={btnBase}\n onClick={() => { void controller.answer(option.id, false) }}\n >\n {option.label}\n </button>\n ))}\n <button type=\"button\" style={btnBase} onClick={() => { void controller.answer('', true) }}>\n {t('action.decline')}\n </button>\n </div>\n </>\n ) : null}\n\n {state.login.kind === 'success' ? (\n <p style={successStyle} role=\"status\">{t('state.success')}</p>\n ) : null}\n\n {(state.login.kind === 'idle' || state.login.kind === 'success') && state.status.models !== undefined ? (\n <p style={noticeStyle}>\n {t('card.models')}\n {' '}\n <span style={modelsStyle}>\n {state.status.models.length > 0 ? state.status.models.join(', ') : '—'}\n </span>\n </p>\n ) : null}\n\n {state.login.kind === 'cancelled' ? (\n <p style={noticeStyle} role=\"status\">\n {t('state.error')}\n {' '}\n <button type=\"button\" style={btnBase} disabled={busy} onClick={() => { void controller.login() }}>\n {t('action.retry')}\n </button>\n </p>\n ) : null}\n\n {state.login.kind === 'error' ? (\n <p style={errorStyle} role=\"status\">\n {t('state.error')}: {state.login.message}\n {' '}\n <button type=\"button\" style={btnBase} disabled={busy} onClick={() => { void controller.login() }}>\n {t('action.retry')}\n </button>\n </p>\n ) : null}\n </div>\n </li>\n )\n}\n","/**\n * `CopilotAuthController` — client-side state machine for the Copilot card.\n *\n * Drives the host's `/copilot/api` gateway: one `status` load, a login flow\n * that starts the attempt and then polls the sequenced `events` stream\n * (device-code notices, progress, prompts to answer) until the attempt\n * settles, and logout / autofill actions. The store is the single render\n * source; the card is a pure projection of it.\n *\n * @module @huanlin/dsh-plugin-copilot/client/controller\n */\nimport { createSnapshotStore, type SnapshotStore } from '@deepseek-ai/dsh-client-store'\nimport type { CopilotStatus } from '../status.ts'\n\n/** One sequenced gateway event as the client consumes it. */\nexport interface CopilotClientEvent {\n seq: number\n kind: 'notice' | 'prompt'\n message?: string\n url?: string\n code?: string\n prompt?: { kind: 'text' | 'secret' | 'select', message: string, placeholder?: string, options?: readonly { id: string, label: string }[] }\n}\n\n/** The card's render state. */\nexport interface CopilotCardState {\n /** Whether the first status read answered. */\n loaded: boolean\n /** The joined host status. */\n status: CopilotStatus\n /** Login lifecycle: idle / running / terminal outcome. */\n login:\n | { kind: 'idle' }\n | { kind: 'running' }\n | { kind: 'success' }\n | { kind: 'error', message: string }\n | { kind: 'cancelled' }\n /** Notices and the latest unanswered prompt, in arrival order. */\n events: readonly CopilotClientEvent[]\n /** The last device code shown (for the copy button). */\n deviceCode: string | undefined\n /** The verification URL of the latest device-code notice. */\n verificationUrl: string | undefined\n /** The prompt awaiting an answer, when one is open. */\n openPrompt: CopilotClientEvent | undefined\n /** Copy button feedback. */\n copied: boolean\n /** Action in flight (disables buttons). */\n busy: boolean\n}\n\n/** Initial state before the first load. */\nfunction initialState(): CopilotCardState {\n return {\n loaded: false,\n status: { flowAvailable: false, loggedIn: false, profileActivated: false, inFlight: false, models: undefined },\n login: { kind: 'idle' },\n events: [],\n deviceCode: undefined,\n verificationUrl: undefined,\n openPrompt: undefined,\n copied: false,\n busy: false,\n }\n}\n\n/** One gateway call's result envelope. */\ninterface Envelope<T> {\n ok?: boolean\n value?: T\n error?: { code?: string, message?: string }\n}\n\n/** Call one `/copilot/api/<method>` endpoint. */\nasync function call<T>(method: string, payload: Record<string, unknown> = {}): Promise<T> {\n const response = await fetch(`/copilot/api/${method}`, {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: JSON.stringify(payload),\n })\n const parsed = await response.json().catch(() => null) as Envelope<T> | null\n if (!response.ok || parsed === null || parsed.ok !== true || parsed.value === undefined) {\n throw new Error(parsed?.error?.message ?? `HTTP ${response.status}`)\n }\n return parsed.value\n}\n\n/**\n * Controller managing the Copilot card lifecycle. Constructed once in the\n * client `apply()`; polls only while a login attempt is running or the card\n * holds an unanswered prompt.\n */\nexport class CopilotAuthController {\n readonly store: SnapshotStore<CopilotCardState>\n private cursor = 0\n private pollTimer: ReturnType<typeof setTimeout> | undefined\n private disposed = false\n\n constructor() {\n this.store = createSnapshotStore<CopilotCardState>(initialState())\n }\n\n /** Load the joined status from the host. */\n async load(): Promise<void> {\n try {\n const status = await call<CopilotStatus>('status')\n this.store.update((s) => {\n s.loaded = true\n s.status = status\n if (status.inFlight && s.login.kind === 'idle') {\n s.login = { kind: 'running' }\n this.schedulePoll(0)\n }\n })\n } catch {\n this.store.update((s) => { s.loaded = true })\n }\n }\n\n /** Start a login attempt, then poll events until settlement. */\n async login(): Promise<void> {\n if (this.disposed) return\n this.store.update((s) => {\n s.busy = true\n s.login = { kind: 'running' }\n s.events = []\n s.deviceCode = undefined\n s.verificationUrl = undefined\n s.openPrompt = undefined\n })\n try {\n const start = await call<{ status: string, cursor?: number }>('login')\n this.cursor = start.cursor ?? this.cursor\n } catch (error) {\n this.finishLogin('error', error instanceof Error ? error.message : String(error))\n return\n }\n this.store.update((s) => { s.busy = false })\n // The attempt runs on the host; the event stream carries device codes,\n // prompts, and the terminal settlement.\n this.schedulePoll(0)\n }\n\n /** Withdraw the running attempt. */\n async cancel(): Promise<void> {\n this.store.update((s) => { s.busy = true })\n try {\n await call('cancel')\n } catch {\n // The settlement poll below is the authority; ignore transport noise.\n }\n this.store.update((s) => { s.busy = false })\n }\n\n /** Answer the open prompt. */\n async answer(answer: string, declined: boolean): Promise<void> {\n const prompt = this.store.getSnapshot().openPrompt\n if (prompt === undefined) return\n this.store.update((s) => {\n s.busy = true\n s.openPrompt = undefined\n })\n try {\n await call('answer', { seq: prompt.seq, answer, declined })\n } catch {\n // A withdrawn prompt re-appears through the next event poll if the\n // attempt still needs an answer; nothing else to do here.\n }\n this.store.update((s) => { s.busy = false })\n this.schedulePoll(0)\n }\n\n /** Delete the stored credential record. */\n async logout(): Promise<void> {\n this.store.update((s) => { s.busy = true })\n try {\n await call('logout')\n await this.load()\n } catch {\n // The next card mount re-reads status; surface nothing extra.\n }\n this.store.update((s) => { s.busy = false })\n }\n\n /** Write the provider profile (idempotent on the host side). */\n async autofill(): Promise<void> {\n this.store.update((s) => { s.busy = true })\n try {\n await call('autofill')\n await this.load()\n } catch {\n // Status reload below keeps the card honest even when the write failed.\n await this.load()\n }\n this.store.update((s) => { s.busy = false })\n }\n\n /** Copy-button feedback. */\n markCopied(): void {\n this.store.update((s) => { s.copied = true })\n setTimeout(() => {\n if (!this.disposed) this.store.update((s) => { s.copied = false })\n }, 1500)\n }\n\n /** Stop polling and further actions; called on fiber disposal. */\n dispose(): void {\n this.disposed = true\n if (this.pollTimer !== undefined) clearTimeout(this.pollTimer)\n this.pollTimer = undefined\n }\n\n /** Fold one settlement into the card state and refresh the join. */\n private finishLogin(status: string, message?: string): void {\n this.store.update((s) => {\n s.busy = false\n if (status === 'authorized') {\n s.login = { kind: 'success' }\n s.status = { ...s.status, loggedIn: true, profileActivated: true }\n } else if (status === 'cancelled') {\n s.login = { kind: 'cancelled' }\n } else {\n s.login = { kind: 'error', message: message ?? 'Sign-in failed' }\n }\n })\n void this.load()\n }\n\n /** Poll the event stream once; reschedules while the attempt runs. */\n private async pollOnce(): Promise<void> {\n if (this.disposed) return\n let delay = 1000\n try {\n const answer = await call<{\n events: CopilotClientEvent[]\n cursor: number\n inFlight: boolean\n settlement?: { status: string, message?: string }\n }>('events', { since: this.cursor })\n this.cursor = answer.cursor\n for (const event of answer.events) this.applyEvent(event)\n if (answer.settlement !== undefined && this.store.getSnapshot().login.kind === 'running') {\n this.finishLogin(answer.settlement.status, answer.settlement.message)\n return\n }\n if (answer.inFlight) delay = 1000\n else {\n // The attempt is over and no settlement is carried (e.g. it settled\n // before this controller started polling): reload the join and stop.\n void this.load()\n return\n }\n } catch {\n delay = 2000\n }\n this.schedulePoll(delay)\n }\n\n /** Fold one sequenced event into the card state. */\n private applyEvent(event: CopilotClientEvent): void {\n this.store.update((s) => {\n s.events = [...s.events, event].slice(-24)\n if (event.kind === 'prompt') {\n s.openPrompt = event\n return\n }\n if (event.code !== undefined) {\n s.deviceCode = event.code\n s.verificationUrl = event.url\n }\n })\n }\n\n /** Schedule the next poll, collapsing overlapping timers. */\n private schedulePoll(delayMs: number): void {\n if (this.disposed) return\n if (this.pollTimer !== undefined) clearTimeout(this.pollTimer)\n this.pollTimer = setTimeout(() => {\n this.pollTimer = undefined\n void this.pollOnce()\n }, delayMs)\n }\n}\n","/**\n * Inlined `bindSnapshotSelector` — rc.8 dropped this from\n * `@deepseek-ai/dsh-client-ui-renderer`'s package root, so business plugins\n * carry their own copy (same approach as the sidebar-brand-text plugin).\n *\n * @module @huanlin/dsh-plugin-copilot/client/bindSnapshotSelector\n */\nimport { useRef, useSyncExternalStore } from 'react'\nimport type { HostObservable, SnapshotSelectorHook } from '@deepseek-ai/dsh-client-ui-slots'\n\n/**\n * Bind a React selector hook to a {@link HostObservable} snapshot source.\n * @param source - the observable snapshot store.\n * @returns a `useSelector(sel, eq?)` hook.\n */\nexport function bindSnapshotSelector<T>(source: HostObservable<T>): SnapshotSelectorHook<T> {\n const subscribe = (fn: () => void): (() => void) => source.subscribe(fn)\n const getSnapshot = (): T => source.getSnapshot()\n return function useSelector<S>(sel: (s: T) => S): S {\n const snapshot = useSyncExternalStore(subscribe, getSnapshot)\n const prevSnapshotRef = useRef<T | undefined>(undefined)\n const prevSelectedRef = useRef<S | undefined>(undefined)\n if (prevSnapshotRef.current !== snapshot) {\n prevSnapshotRef.current = snapshot\n prevSelectedRef.current = sel(snapshot)\n }\n return prevSelectedRef.current as S\n }\n}\n","/**\n * Locale dictionaries for the `dsh-plugin-copilot` card namespace.\n *\n * @module @huanlin/dsh-plugin-copilot/client/locales\n */\n\n/** The locale keys the Copilot card reads. */\nexport type CopilotKey =\n | 'card.title'\n | 'card.intro'\n | 'card.unsupported'\n | 'card.signedIn'\n | 'card.signedOut'\n | 'card.routeActive'\n | 'card.routeDormant'\n | 'card.models'\n | 'action.signIn'\n | 'action.signOut'\n | 'action.cancel'\n | 'action.retry'\n | 'action.activate'\n | 'action.syncModels'\n | 'action.openUrl'\n | 'action.copyCode'\n | 'action.copied'\n | 'action.decline'\n | 'action.submit'\n | 'state.pending'\n | 'state.pendingPrompt'\n | 'state.working'\n | 'state.success'\n | 'state.error'\n | 'notice.deviceCode'\n | 'notice.polling'\n | 'prompt.placeholder'\n | 'prompt.answer'\n\n/** The locale namespace name; matches the `locale: NS` passed at slot register. */\nexport const NS = 'dsh-plugin-copilot'\n\n/** English dictionary. */\nexport const en: Record<CopilotKey, string> = {\n 'card.title': 'GitHub Copilot',\n 'card.intro': 'Sign in to GitHub Copilot and activate its model route (served by dsh-llm-pi-ai).',\n 'card.unsupported': 'Copilot sign-in needs dsh-llm-pi-ai (0.1.2-alpha.1 or later) with the github-copilot catalog provider.',\n 'card.signedIn': 'Signed in',\n 'card.signedOut': 'Not signed in',\n 'card.routeActive': 'Route active',\n 'card.routeDormant': 'Route not activated',\n 'card.models': 'Models available to this account:',\n 'action.signIn': 'Sign in with GitHub',\n 'action.signOut': 'Sign out',\n 'action.cancel': 'Cancel',\n 'action.retry': 'Retry',\n 'action.activate': 'Activate route',\n 'action.syncModels': 'Sync model list',\n 'action.openUrl': 'Open verification page',\n 'action.copyCode': 'Copy code',\n 'action.copied': 'Copied',\n 'action.decline': 'Decline',\n 'action.submit': 'Submit',\n 'state.pending': 'Waiting for authorization…',\n 'state.pendingPrompt': 'Answer the question below to continue signing in — the device code appears right after.',\n 'state.working': 'Working…',\n 'state.success': 'Signed in. The Copilot models are ready — pick one on the Models page.',\n 'state.error': 'Failed',\n 'notice.deviceCode': 'Open the verification page and enter this code:',\n 'notice.polling': 'Waiting for you to finish in the browser…',\n 'prompt.placeholder': 'Your answer',\n 'prompt.answer': 'GitHub asks',\n}\n\n/** Chinese dictionary. */\nexport const zh: Record<CopilotKey, string> = {\n 'card.title': 'GitHub Copilot',\n 'card.intro': '登录 GitHub Copilot 并激活其模型路由(由 dsh-llm-pi-ai 提供)。',\n 'card.unsupported': 'Copilot 登录需要 dsh-llm-pi-ai(0.1.2-alpha.1 或更高)内置的 github-copilot 供应商。',\n 'card.signedIn': '已登录',\n 'card.signedOut': '未登录',\n 'card.routeActive': '路由已激活',\n 'card.routeDormant': '路由未激活',\n 'card.models': '当前账号可用模型:',\n 'action.signIn': '使用 GitHub 登录',\n 'action.signOut': '退出登录',\n 'action.cancel': '取消',\n 'action.retry': '重试',\n 'action.activate': '激活路由',\n 'action.syncModels': '同步模型列表',\n 'action.openUrl': '打开验证页面',\n 'action.copyCode': '复制代码',\n 'action.copied': '已复制',\n 'action.decline': '拒绝',\n 'action.submit': '提交',\n 'state.pending': '等待授权中…',\n 'state.pendingPrompt': '回答下面的问题即可继续登录,随后会显示设备码和验证页面。',\n 'state.working': '处理中…',\n 'state.success': '已登录,Copilot 模型就绪 — 去 Models 页选择即可。',\n 'state.error': '失败',\n 'notice.deviceCode': '打开验证页面并输入此代码:',\n 'notice.polling': '等待你在浏览器中完成授权…',\n 'prompt.placeholder': '输入你的回答',\n 'prompt.answer': 'GitHub 需要你回答',\n}\n","/**\n * dsh-plugin-copilot — browser half.\n *\n * One registration: a `settings.plugin.item` card (key `dsh-plugin-copilot`)\n * in the Plugins settings page, rendering the Copilot onboarding state\n * machine (sign-in device-flow panel, route activation, sign-out) through\n * the host's `/copilot/api` gateway.\n *\n * @module @huanlin/dsh-plugin-copilot/client\n */\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\n// Type-only: pulls the client Context merges (ctx.slots, ctx.locale).\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-renderer/client'\nimport type {} from '@deepseek-ai/dsh-client-ui-settings-plugins/client'\nimport { CopilotAuthCard } from './CopilotAuthCard.tsx'\nimport type { CopilotCardInjected } from './CopilotAuthCard.tsx'\nimport { CopilotAuthController } from './controller.ts'\nimport { bindSnapshotSelector } from './bindSnapshotSelector.ts'\nimport { en, zh, NS } from './locales.ts'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** The Copilot onboarding card labels. */\n 'dsh-plugin-copilot': import('./locales.ts').CopilotKey\n }\n}\n\n/** Required services: the slot ledger and the locale dictionaries. */\nexport const inject = ['slots', 'locale']\n\n/**\n * Client plugin body: register the locale dictionaries and the settings card.\n * @param ctx - client root context.\n */\nexport function apply(ctx: ClientContext): void {\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-plugin-copilot: dictionaries')\n\n const controller = new CopilotAuthController()\n ctx.effect(() => () => { controller.dispose() }, 'dsh-plugin-copilot: card controller')\n const useCard = bindSnapshotSelector(controller.store)\n\n const injected = (): CopilotCardInjected => ({ controller, useCard })\n ctx.slots.inject('settings.plugin.item', function* () {\n yield ctx.slots.register(\n {\n name: 'settings.plugin.item',\n key: 'dsh-plugin-copilot',\n locale: NS,\n inject: injected,\n },\n CopilotAuthCard,\n )\n })\n}\n"],"mappings":"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"}
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,553 @@
|
|
|
1
|
+
import { settingsNamespace } from "@deepseek-ai/dsh-settings";
|
|
2
|
+
import z from "schemastery";
|
|
3
|
+
import { AuthorizationDeclinedError } from "@deepseek-ai/dsh-authorization";
|
|
4
|
+
import { credentialKey } from "@deepseek-ai/dsh-credentials";
|
|
5
|
+
import { defineTool } from "@deepseek-ai/dsh-tools";
|
|
6
|
+
|
|
7
|
+
//#region src/status.ts
|
|
8
|
+
/** The provider route and record id this plugin bootstraps. */
|
|
9
|
+
const COPILOT_PROVIDER = "github-copilot";
|
|
10
|
+
/** Scope owning the Copilot credential record (dsh-llm-pi-ai's plugin name). */
|
|
11
|
+
const COPILOT_SCOPE = "llm-pi-ai";
|
|
12
|
+
/** Settings namespace whose `providers` dict carries the Copilot profile. */
|
|
13
|
+
const COPILOT_SETTINGS_NS = "llm-pi-ai";
|
|
14
|
+
/**
|
|
15
|
+
* The credential record a pi-ai Copilot login writes.
|
|
16
|
+
*
|
|
17
|
+
* Spelled from the two public constants rather than imported from
|
|
18
|
+
* `@deepseek-ai/dsh-llm-pi-ai`: a third-party plugin must not take a
|
|
19
|
+
* runtime dependency on an internal plugin package, and the record address
|
|
20
|
+
* is exactly `credentialKey('llm-pi-ai', 'github-copilot')`.
|
|
21
|
+
*/
|
|
22
|
+
const COPILOT_RECORD_KEY = credentialKey(COPILOT_SCOPE, COPILOT_PROVIDER);
|
|
23
|
+
/** The joined record address, as the authorization flow registry reports it. */
|
|
24
|
+
function recordAddress(key) {
|
|
25
|
+
return `${key.slice(0, key.indexOf("/"))}/${key.slice(key.indexOf("/") + 1)}`;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Find the pi-ai Copilot authorization flow without hand-composing its key:
|
|
29
|
+
* the flow registry is the authority on which keys exist, so the join is by
|
|
30
|
+
* scope + id segments of each registered flow's key.
|
|
31
|
+
* @param entries - every registered authorization flow.
|
|
32
|
+
* @returns the entry whose record is the Copilot one, or undefined.
|
|
33
|
+
*/
|
|
34
|
+
function findCopilotFlow(entries) {
|
|
35
|
+
return entries.find((entry) => {
|
|
36
|
+
return recordAddress(entry.key) === `${COPILOT_SCOPE}/${COPILOT_PROVIDER}`;
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Pull the model ids a stored pi-ai OAuth grant reports as usable by this
|
|
41
|
+
* account. pi-ai writes `availableModelIds` at login and rewrites it on every
|
|
42
|
+
* token refresh; anything else — an api-key record, an absent store, an
|
|
43
|
+
* unexpected payload — reads as unknown rather than empty, so a surface can
|
|
44
|
+
* stay silent instead of claiming the account has no models.
|
|
45
|
+
* @param record - the credential record as stored, or undefined.
|
|
46
|
+
* @returns the usable model ids, or undefined when unknown.
|
|
47
|
+
*/
|
|
48
|
+
function grantModelIds(record) {
|
|
49
|
+
if (record?.kind !== "grant") return void 0;
|
|
50
|
+
const models = record.payload?.availableModelIds;
|
|
51
|
+
if (!Array.isArray(models) || !models.every((id) => typeof id === "string")) return void 0;
|
|
52
|
+
return models;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Join the facts the card shows.
|
|
56
|
+
* @param sources - the live host reads.
|
|
57
|
+
* @returns the card status; `loggedIn` is false when no credential store is mounted.
|
|
58
|
+
*/
|
|
59
|
+
async function joinStatus(sources) {
|
|
60
|
+
const flow = findCopilotFlow(sources.listFlows());
|
|
61
|
+
let loggedIn = false;
|
|
62
|
+
if (flow !== void 0) {
|
|
63
|
+
const record = await sources.describeRecord(flow.key);
|
|
64
|
+
loggedIn = record?.configured === true && record.kind === "grant";
|
|
65
|
+
}
|
|
66
|
+
const providers = sources.settingsSection()?.providers;
|
|
67
|
+
return {
|
|
68
|
+
flowAvailable: flow !== void 0,
|
|
69
|
+
loggedIn,
|
|
70
|
+
profileActivated: typeof providers === "object" && providers !== null && Object.hasOwn(providers, COPILOT_PROVIDER),
|
|
71
|
+
inFlight: flow?.inFlight ?? false,
|
|
72
|
+
models: loggedIn ? await sources.models() : void 0
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
//#endregion
|
|
77
|
+
//#region src/gateway.ts
|
|
78
|
+
/** HTTP route prefix owning every copilot API request. */
|
|
79
|
+
const API_PREFIX = "/copilot/api";
|
|
80
|
+
/**
|
|
81
|
+
* The pi-ai Copilot flow opens by asking for the enterprise deployment, where
|
|
82
|
+
* blank is the normal answer ("blank for github.com"). Matched by the concept
|
|
83
|
+
* its message names rather than the exact copy, so a wording tweak upstream
|
|
84
|
+
* degrades into the manual prompt path instead of a wrong auto-answer.
|
|
85
|
+
* @param prompt - the prompt the flow raised.
|
|
86
|
+
* @returns whether this is the enterprise-domain question.
|
|
87
|
+
*/
|
|
88
|
+
function isEnterpriseDomainPrompt(prompt) {
|
|
89
|
+
return prompt.kind === "text" && /enterprise/i.test(prompt.message);
|
|
90
|
+
}
|
|
91
|
+
/** Upper bound on the buffered notice ring the client replays from. */
|
|
92
|
+
const MAX_BUFFERED_NOTICES = 64;
|
|
93
|
+
/**
|
|
94
|
+
* Register the `/copilot/api` route.
|
|
95
|
+
*
|
|
96
|
+
* @param ctx - host context carrying `webServer`.
|
|
97
|
+
* @param deps - the host capabilities the route drives.
|
|
98
|
+
* @returns disposer removing the route.
|
|
99
|
+
*/
|
|
100
|
+
function registerCopilotGateway(ctx, deps) {
|
|
101
|
+
const webServer = ctx.webServer;
|
|
102
|
+
if (webServer === void 0 || typeof webServer.register !== "function") return () => {};
|
|
103
|
+
let seq = 0;
|
|
104
|
+
let ring = [];
|
|
105
|
+
let runningLogin;
|
|
106
|
+
/** Last settlement, as a terminal event the client polls for. */
|
|
107
|
+
let settlement;
|
|
108
|
+
const pushEvent = (event) => {
|
|
109
|
+
ring.push(event);
|
|
110
|
+
if (ring.length > MAX_BUFFERED_NOTICES) ring = ring.slice(-MAX_BUFFERED_NOTICES);
|
|
111
|
+
return event.seq;
|
|
112
|
+
};
|
|
113
|
+
const pushNotice = (notice) => {
|
|
114
|
+
seq += 1;
|
|
115
|
+
return pushEvent({
|
|
116
|
+
kind: "notice",
|
|
117
|
+
seq,
|
|
118
|
+
notice
|
|
119
|
+
});
|
|
120
|
+
};
|
|
121
|
+
const pushPrompt = (prompt) => {
|
|
122
|
+
seq += 1;
|
|
123
|
+
return pushEvent({
|
|
124
|
+
kind: "prompt",
|
|
125
|
+
seq,
|
|
126
|
+
prompt
|
|
127
|
+
});
|
|
128
|
+
};
|
|
129
|
+
const settle = (outcome) => {
|
|
130
|
+
runningLogin = void 0;
|
|
131
|
+
settlement = outcome;
|
|
132
|
+
pushNotice({ message: outcome.status === "authorized" ? "Authorized. The Copilot route is being activated…" : outcome.status === "cancelled" ? "Sign-in was cancelled." : `Sign-in failed: ${outcome.message}` });
|
|
133
|
+
};
|
|
134
|
+
const copilotKey = () => {
|
|
135
|
+
return findCopilotFlow(deps.listFlows())?.key;
|
|
136
|
+
};
|
|
137
|
+
const status = () => joinStatus(deps);
|
|
138
|
+
const runLogin = () => {
|
|
139
|
+
const key = copilotKey();
|
|
140
|
+
if (key === void 0) {
|
|
141
|
+
settle({
|
|
142
|
+
status: "error",
|
|
143
|
+
message: "no authorization flow is registered for llm-pi-ai/github-copilot (requires dsh-llm-pi-ai)"
|
|
144
|
+
});
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
if (runningLogin !== void 0) return;
|
|
148
|
+
const controller = new AbortController();
|
|
149
|
+
runningLogin = {
|
|
150
|
+
key,
|
|
151
|
+
controller
|
|
152
|
+
};
|
|
153
|
+
settlement = void 0;
|
|
154
|
+
pushNotice({ message: "Starting GitHub sign-in…" });
|
|
155
|
+
deps.begin({
|
|
156
|
+
key,
|
|
157
|
+
signal: controller.signal,
|
|
158
|
+
interaction: {
|
|
159
|
+
notify: (notice) => {
|
|
160
|
+
pushNotice(notice);
|
|
161
|
+
},
|
|
162
|
+
prompt: (prompt) => {
|
|
163
|
+
if (isEnterpriseDomainPrompt(prompt)) {
|
|
164
|
+
const domain = (deps.enterpriseDomain ?? "").trim();
|
|
165
|
+
if (domain !== "") pushNotice({ message: `Using GitHub Enterprise domain ${domain}.` });
|
|
166
|
+
return Promise.resolve(domain);
|
|
167
|
+
}
|
|
168
|
+
return new Promise((resolve, reject) => {
|
|
169
|
+
const at = pushPrompt(prompt);
|
|
170
|
+
prompt.signal?.addEventListener("abort", () => {
|
|
171
|
+
pendingAnswers.delete(at);
|
|
172
|
+
reject(/* @__PURE__ */ new Error("prompt withdrawn"));
|
|
173
|
+
}, { once: true });
|
|
174
|
+
pendingAnswers.set(at, {
|
|
175
|
+
resolve,
|
|
176
|
+
reject
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}).then(async (outcome) => {
|
|
182
|
+
if (outcome.status === "authorized") try {
|
|
183
|
+
await autofill();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
pushNotice({ message: `Credential stored, but activating the provider profile failed: ${error instanceof Error ? error.message : String(error)} — retry from the card.` });
|
|
186
|
+
}
|
|
187
|
+
settle(outcome);
|
|
188
|
+
}, (error) => {
|
|
189
|
+
settle(error instanceof AuthorizationDeclinedError ? { status: "cancelled" } : {
|
|
190
|
+
status: "error",
|
|
191
|
+
message: error instanceof Error ? error.message : String(error)
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
/** Start the attempt and answer with the event cursor the card polls from. */
|
|
196
|
+
const startLoginImmediate = () => {
|
|
197
|
+
runLogin();
|
|
198
|
+
return seq;
|
|
199
|
+
};
|
|
200
|
+
const pendingAnswers = /* @__PURE__ */ new Map();
|
|
201
|
+
const answerPrompt = (answerSeq, answer, declined) => {
|
|
202
|
+
const waiter = pendingAnswers.get(answerSeq);
|
|
203
|
+
if (waiter === void 0) return false;
|
|
204
|
+
pendingAnswers.delete(answerSeq);
|
|
205
|
+
if (declined) waiter.reject(new AuthorizationDeclinedError());
|
|
206
|
+
else waiter.resolve(answer);
|
|
207
|
+
return true;
|
|
208
|
+
};
|
|
209
|
+
/** The `github-copilot` profile of the resolved `llm-pi-ai` section, or undefined. */
|
|
210
|
+
const copilotProfile = () => {
|
|
211
|
+
const profile = (deps.settingsSection()?.providers)?.[COPILOT_PROVIDER];
|
|
212
|
+
return typeof profile === "object" && profile !== null ? profile : void 0;
|
|
213
|
+
};
|
|
214
|
+
/**
|
|
215
|
+
* The grant's available model ids narrowed to the installed catalog, in
|
|
216
|
+
grant order. `undefined` when either side is unknown or the intersection
|
|
217
|
+
is empty — narrowing to nothing would refuse the route at write time, so
|
|
218
|
+
an unknown list leaves the installed catalog serving untouched.
|
|
219
|
+
*/
|
|
220
|
+
const narrowedModels = async () => {
|
|
221
|
+
const [available, catalog] = await Promise.all([deps.models(), deps.catalogModels()]);
|
|
222
|
+
if (available === void 0 || catalog === void 0) return void 0;
|
|
223
|
+
if (available.length === 0 || catalog.length === 0) return void 0;
|
|
224
|
+
const known = new Set(catalog.map((model) => model.id));
|
|
225
|
+
const ids = available.filter((id) => known.has(id));
|
|
226
|
+
return ids.length > 0 ? ids : void 0;
|
|
227
|
+
};
|
|
228
|
+
/** The `models` list ids a profile already carries, or undefined when it has none. */
|
|
229
|
+
const profileModelIds = (profile) => {
|
|
230
|
+
const models = profile?.models;
|
|
231
|
+
if (!Array.isArray(models) || !models.every((entry) => typeof entry?.id === "string")) return;
|
|
232
|
+
return models.map((entry) => entry.id);
|
|
233
|
+
};
|
|
234
|
+
const sameStrings = (left, right) => left !== void 0 && right !== void 0 && left.length === right.length && left.every((id, at) => id === right[at]);
|
|
235
|
+
/**
|
|
236
|
+
* Ensure the provider profile exists and its `models` list matches what the
|
|
237
|
+
* stored grant reports. The harness model picker serves the profile's
|
|
238
|
+
* resolved list, so this — not pi-ai's request-time `filterModels`, which
|
|
239
|
+
* the harness never consults — is what keeps unavailable models out of the
|
|
240
|
+
* picker. One merged write carries profile creation and model narrowing
|
|
241
|
+
* together; a profile already carrying the derived list costs no revision.
|
|
242
|
+
* A list a user hand-wrote is treated as derived data and re-narrowed on
|
|
243
|
+
* the next sync.
|
|
244
|
+
* @returns what the call did, for the response envelope.
|
|
245
|
+
*/
|
|
246
|
+
const autofill = async () => {
|
|
247
|
+
const profile = copilotProfile();
|
|
248
|
+
const available = await narrowedModels();
|
|
249
|
+
const narrowed = available?.map((id) => ({ id }));
|
|
250
|
+
const changed = narrowed !== void 0 && !sameStrings(profileModelIds(profile), available);
|
|
251
|
+
if (profile !== void 0 && !changed) return "unchanged";
|
|
252
|
+
await deps.updateSettings({ providers: { [COPILOT_PROVIDER]: changed && narrowed !== void 0 ? { models: narrowed } : {} } });
|
|
253
|
+
return profile === void 0 ? "created" : "updated";
|
|
254
|
+
};
|
|
255
|
+
return webServer.register({
|
|
256
|
+
kind: "prefix",
|
|
257
|
+
path: API_PREFIX,
|
|
258
|
+
handler: async (req, res) => {
|
|
259
|
+
if ((req.method ?? "") !== "POST") {
|
|
260
|
+
writeJson(res, 405, errorEnvelope("method-not-allowed", "POST only"));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
const originCheck = sameOrigin(req);
|
|
264
|
+
if (originCheck !== void 0) {
|
|
265
|
+
writeJson(res, originCheck.status, errorEnvelope(originCheck.code, originCheck.message));
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (!String(req.headers["content-type"] ?? "").toLowerCase().startsWith("application/json")) {
|
|
269
|
+
writeJson(res, 415, errorEnvelope("content-type-not-supported", "application/json required"));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
const pathname = new URL(req.url ?? "/", "http://dsh.internal").pathname;
|
|
273
|
+
const method = pathname.startsWith(`${API_PREFIX}/`) ? pathname.slice(`${API_PREFIX}/`.length) : void 0;
|
|
274
|
+
if (method === void 0 || method.includes("/")) {
|
|
275
|
+
writeJson(res, 404, errorEnvelope("not-found", "unknown copilot API method"));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const body = await readJsonBody(req);
|
|
280
|
+
switch (method) {
|
|
281
|
+
case "status":
|
|
282
|
+
writeJson(res, 200, okEnvelope(await status()));
|
|
283
|
+
return;
|
|
284
|
+
case "login":
|
|
285
|
+
if (runningLogin !== void 0) {
|
|
286
|
+
writeJson(res, 200, okEnvelope({
|
|
287
|
+
status: "in-progress",
|
|
288
|
+
cursor: seq
|
|
289
|
+
}));
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
writeJson(res, 200, okEnvelope({
|
|
293
|
+
status: "started",
|
|
294
|
+
cursor: startLoginImmediate()
|
|
295
|
+
}));
|
|
296
|
+
return;
|
|
297
|
+
case "cancel":
|
|
298
|
+
if (runningLogin === void 0) {
|
|
299
|
+
writeJson(res, 200, okEnvelope({ status: "not-running" }));
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
runningLogin.controller.abort();
|
|
303
|
+
writeJson(res, 200, okEnvelope({ status: "cancelling" }));
|
|
304
|
+
return;
|
|
305
|
+
case "events": {
|
|
306
|
+
const since = typeof body?.since === "number" ? body.since : 0;
|
|
307
|
+
writeJson(res, 200, okEnvelope({
|
|
308
|
+
events: ring.filter((event) => event.seq > since).map((event) => event.kind === "notice" ? {
|
|
309
|
+
seq: event.seq,
|
|
310
|
+
kind: "notice",
|
|
311
|
+
message: event.notice.message,
|
|
312
|
+
...event.notice.url === void 0 ? {} : { url: event.notice.url },
|
|
313
|
+
...event.notice.code === void 0 ? {} : { code: event.notice.code }
|
|
314
|
+
} : {
|
|
315
|
+
seq: event.seq,
|
|
316
|
+
kind: "prompt",
|
|
317
|
+
prompt: event.prompt
|
|
318
|
+
}),
|
|
319
|
+
cursor: seq,
|
|
320
|
+
inFlight: runningLogin !== void 0,
|
|
321
|
+
...settlement === void 0 ? {} : { settlement }
|
|
322
|
+
}));
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
case "answer": {
|
|
326
|
+
const payload = body;
|
|
327
|
+
if (typeof payload.seq !== "number") {
|
|
328
|
+
writeJson(res, 400, errorEnvelope("bad-request", "answer needs a numeric seq"));
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
const declined = payload.declined === true;
|
|
332
|
+
const answer = typeof payload.answer === "string" ? payload.answer : "";
|
|
333
|
+
writeJson(res, 200, okEnvelope({ accepted: answerPrompt(payload.seq, answer, declined) }));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
case "logout": {
|
|
337
|
+
const key = copilotKey();
|
|
338
|
+
if (key === void 0) {
|
|
339
|
+
writeJson(res, 200, okEnvelope({ status: "not-running" }));
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
if ((await deps.describeRecord(key))?.configured !== true) {
|
|
343
|
+
writeJson(res, 200, okEnvelope({ status: "not-logged-in" }));
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
await deps.deleteRecord(key);
|
|
347
|
+
writeJson(res, 200, okEnvelope({ status: "cleared" }));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
case "autofill": {
|
|
351
|
+
const before = copilotProfile() !== void 0;
|
|
352
|
+
await autofill();
|
|
353
|
+
writeJson(res, 200, okEnvelope({ status: before ? "already-active" : "activated" }));
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
default: writeJson(res, 404, errorEnvelope("not-found", `unknown copilot API method "${method}"`));
|
|
357
|
+
}
|
|
358
|
+
} catch (error) {
|
|
359
|
+
writeJson(res, 500, errorEnvelope("internal", error instanceof Error ? error.message : String(error)));
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
/** Reject cross-site browser requests: same-origin only, like every plugin route. */
|
|
365
|
+
function sameOrigin(req) {
|
|
366
|
+
const origin = req.headers.origin;
|
|
367
|
+
if (typeof origin === "string" && origin !== "") {
|
|
368
|
+
let originHost;
|
|
369
|
+
try {
|
|
370
|
+
originHost = new URL(origin).host;
|
|
371
|
+
} catch {
|
|
372
|
+
return {
|
|
373
|
+
status: 400,
|
|
374
|
+
code: "invalid-origin",
|
|
375
|
+
message: "invalid Origin header"
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const reqHost = req.headers.host;
|
|
379
|
+
if (typeof reqHost === "string" && originHost !== reqHost) return {
|
|
380
|
+
status: 403,
|
|
381
|
+
code: "origin-not-allowed",
|
|
382
|
+
message: "same-origin requests only"
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
/** Read and parse a JSON body from a node:http request. */
|
|
387
|
+
async function readJsonBody(req, maxBytes = 8192) {
|
|
388
|
+
const chunks = [];
|
|
389
|
+
let bytes = 0;
|
|
390
|
+
for await (const chunk of req) {
|
|
391
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
392
|
+
bytes += buffer.length;
|
|
393
|
+
if (bytes > maxBytes) throw new Error("request body too large");
|
|
394
|
+
chunks.push(buffer);
|
|
395
|
+
}
|
|
396
|
+
const text = Buffer.concat(chunks).toString("utf8");
|
|
397
|
+
if (text === "") return {};
|
|
398
|
+
return JSON.parse(text);
|
|
399
|
+
}
|
|
400
|
+
/** Write a JSON response envelope. */
|
|
401
|
+
function writeJson(res, status, body) {
|
|
402
|
+
res.writeHead(status, { "content-type": "application/json" });
|
|
403
|
+
res.end(JSON.stringify(body));
|
|
404
|
+
}
|
|
405
|
+
/** Build a success envelope. */
|
|
406
|
+
function okEnvelope(value) {
|
|
407
|
+
return {
|
|
408
|
+
ok: true,
|
|
409
|
+
value
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
/** Build an error envelope. */
|
|
413
|
+
function errorEnvelope(code, message) {
|
|
414
|
+
return {
|
|
415
|
+
ok: false,
|
|
416
|
+
error: {
|
|
417
|
+
code,
|
|
418
|
+
message
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
//#endregion
|
|
424
|
+
//#region src/tools.ts
|
|
425
|
+
/** Loose canonical-JSON output declaration. */
|
|
426
|
+
const JSON_OUTPUT = {
|
|
427
|
+
schema: { type: "json" },
|
|
428
|
+
render: (_args, value) => [{
|
|
429
|
+
type: "text",
|
|
430
|
+
text: JSON.stringify(value)
|
|
431
|
+
}]
|
|
432
|
+
};
|
|
433
|
+
/**
|
|
434
|
+
* Register the status tool.
|
|
435
|
+
* @param ctx - host plugin context carrying `ctx.tools`.
|
|
436
|
+
* @param sources - the live host reads the status join uses.
|
|
437
|
+
*/
|
|
438
|
+
function registerCopilotTools(ctx, sources) {
|
|
439
|
+
ctx.tools.register(defineTool({
|
|
440
|
+
name: "copilot_status",
|
|
441
|
+
description: "Report the GitHub Copilot onboarding state: whether the pi-ai authorization flow is registered, whether a credential record is stored (signed in), whether the llm-pi-ai settings section activates the github-copilot provider route, and which model ids the signed-in account can use. Read-only. Signing in is a WebUI action: Settings → Plugins → GitHub Copilot.",
|
|
442
|
+
parameters: {},
|
|
443
|
+
output: JSON_OUTPUT,
|
|
444
|
+
async execute() {
|
|
445
|
+
const status = await joinStatus(sources);
|
|
446
|
+
const result = {
|
|
447
|
+
flow_available: status.flowAvailable,
|
|
448
|
+
signed_in: status.loggedIn,
|
|
449
|
+
profile_activated: status.profileActivated,
|
|
450
|
+
login_in_flight: status.inFlight
|
|
451
|
+
};
|
|
452
|
+
if (!status.flowAvailable) result.hint = "dsh-llm-pi-ai with the github-copilot catalog provider is required for Copilot sign-in.";
|
|
453
|
+
else if (!status.loggedIn) result.hint = "Sign in from the WebUI: Settings → Plugins → GitHub Copilot → Sign in.";
|
|
454
|
+
else if (!status.profileActivated) result.hint = `Signed in but the route is dormant; the settings card can activate it (writes ${COPILOT_SETTINGS_NS}.providers.github-copilot).`;
|
|
455
|
+
if (status.models !== void 0) result.available_models = [...status.models];
|
|
456
|
+
return result;
|
|
457
|
+
},
|
|
458
|
+
presentCall: () => ({
|
|
459
|
+
card: "generic",
|
|
460
|
+
title: "Copilot auth status",
|
|
461
|
+
kind: "read"
|
|
462
|
+
})
|
|
463
|
+
}));
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
//#endregion
|
|
467
|
+
//#region src/index.ts
|
|
468
|
+
const name = "dsh-plugin-copilot";
|
|
469
|
+
const inject = ["tools", "webServer"];
|
|
470
|
+
/**
|
|
471
|
+
* The plugin's own settings namespace: the card owns no configurable fields,
|
|
472
|
+
* but the flow's enterprise question is answered from here.
|
|
473
|
+
*/
|
|
474
|
+
const CARD_NAMESPACE = settingsNamespace("dsh-plugin-copilot");
|
|
475
|
+
const Config = z.object({ enterpriseDomain: z.string().default("").description("GitHub Enterprise domain (e.g. company.ghe.com); blank serves github.com") });
|
|
476
|
+
/** The pi-ai settings namespace as a branded settings-namespace value. */
|
|
477
|
+
const PI_AI_NS = COPILOT_SETTINGS_NS;
|
|
478
|
+
/**
|
|
479
|
+
* Plugin body: register the card namespace, the HTTP gateway, and the
|
|
480
|
+
* status tool. Every host read goes through optional services (`ctx.get`)
|
|
481
|
+
* so a composition without the authorization or credentials seam still
|
|
482
|
+
* boots — the card then reports the missing pieces instead of failing load.
|
|
483
|
+
* @param ctx - host plugin context.
|
|
484
|
+
*/
|
|
485
|
+
function apply(ctx, config = { enterpriseDomain: "" }) {
|
|
486
|
+
ctx.inject(["settings"], (sctx) => {
|
|
487
|
+
try {
|
|
488
|
+
sctx.settings.register(CARD_NAMESPACE, Config);
|
|
489
|
+
} catch (error) {
|
|
490
|
+
if (!(error instanceof Error) || !error.message.includes("already registered")) throw error;
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
const authorization = () => ctx.get("authorization");
|
|
494
|
+
const credentials = () => ctx.get("credentials");
|
|
495
|
+
const piAiSection = () => {
|
|
496
|
+
const settings = ctx.get("settings");
|
|
497
|
+
if (settings === void 0) return void 0;
|
|
498
|
+
const raw = settings.get(PI_AI_NS);
|
|
499
|
+
return typeof raw === "object" && raw !== null ? raw : void 0;
|
|
500
|
+
};
|
|
501
|
+
const sources = {
|
|
502
|
+
listFlows: () => authorization()?.list() ?? [],
|
|
503
|
+
describeRecord: async (key) => credentials()?.describeRecord(key),
|
|
504
|
+
settingsSection: piAiSection,
|
|
505
|
+
models: async () => grantModelIds(await credentials()?.readRecord(COPILOT_RECORD_KEY))
|
|
506
|
+
};
|
|
507
|
+
/**
|
|
508
|
+
* The installed pi-ai catalog for the Copilot route, through the llm
|
|
509
|
+
* service's public discovery — a catalog route answers from the registry
|
|
510
|
+
* with no network call. `undefined` (and a swallowed failure, which would
|
|
511
|
+
* only degrade the models narrowing to the untouched catalog) keeps a
|
|
512
|
+
* composition without the llm seam booting.
|
|
513
|
+
*/
|
|
514
|
+
const catalogModels = async () => {
|
|
515
|
+
const llm = ctx.get("llm");
|
|
516
|
+
if (llm === void 0) return void 0;
|
|
517
|
+
try {
|
|
518
|
+
return await llm.discoverModels(COPILOT_SETTINGS_NS, { provider: COPILOT_PROVIDER });
|
|
519
|
+
} catch {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
};
|
|
523
|
+
ctx.effect(() => registerCopilotGateway(ctx, {
|
|
524
|
+
enterpriseDomain: config.enterpriseDomain,
|
|
525
|
+
listFlows: sources.listFlows,
|
|
526
|
+
models: sources.models,
|
|
527
|
+
catalogModels,
|
|
528
|
+
begin: (request) => {
|
|
529
|
+
const seam = authorization();
|
|
530
|
+
if (seam === void 0) throw new Error("the authorization service is not mounted in this composition");
|
|
531
|
+
return seam.begin(request);
|
|
532
|
+
},
|
|
533
|
+
cancel: (key) => {
|
|
534
|
+
authorization()?.cancel(key);
|
|
535
|
+
},
|
|
536
|
+
describeRecord: sources.describeRecord,
|
|
537
|
+
deleteRecord: (key) => {
|
|
538
|
+
const seam = credentials();
|
|
539
|
+
if (seam === void 0) throw new Error("the credentials service is not mounted in this composition");
|
|
540
|
+
return seam.deleteRecord(key);
|
|
541
|
+
},
|
|
542
|
+
settingsSection: piAiSection,
|
|
543
|
+
updateSettings: async (patch) => {
|
|
544
|
+
const settings = ctx.get("settings");
|
|
545
|
+
if (settings === void 0) throw new Error("the settings service is not mounted; the provider profile cannot be activated");
|
|
546
|
+
await settings.update(PI_AI_NS, patch);
|
|
547
|
+
}
|
|
548
|
+
}), "dsh-plugin-copilot: /copilot/api gateway");
|
|
549
|
+
registerCopilotTools(ctx, sources);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
//#endregion
|
|
553
|
+
export { CARD_NAMESPACE, COPILOT_PROVIDER, COPILOT_RECORD_KEY, COPILOT_SCOPE, COPILOT_SETTINGS_NS, Config, apply, findCopilotFlow, grantModelIds, inject, joinStatus, name, recordAddress, registerCopilotGateway, registerCopilotTools };
|