@cortexkit/opencode-anthropic-auth 1.22.0 → 2.0.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.
Files changed (91) hide show
  1. package/README.md +259 -198
  2. package/dist/billing-lineage.d.ts +51 -0
  3. package/dist/cli-failure.d.ts +69 -0
  4. package/dist/cli.d.ts +16 -10
  5. package/dist/cli.js +111 -6
  6. package/dist/custody-dimensions.d.ts +19 -0
  7. package/dist/custody-mode.d.ts +46 -0
  8. package/dist/effort-history.d.ts +11 -2
  9. package/dist/fable-fallback.d.ts +1 -1
  10. package/dist/index-1jkyhgyr.js +2 -0
  11. package/dist/{index-bx5hsgf8.js → index-2arfb1y3.js} +2 -2
  12. package/dist/{index-926t2bzc.js → index-4ya27n12.js} +1 -1
  13. package/dist/{index-cgez4m0h.js → index-aaajxwzd.js} +1 -1
  14. package/dist/index-ae3fq4n6.js +42 -0
  15. package/dist/index-fds0s2j4.js +2 -0
  16. package/dist/index-nngmvczq.js +450 -0
  17. package/dist/index-pn597b6f.js +2 -0
  18. package/dist/{index-7f57ewkp.js → index-rqync0pc.js} +1 -1
  19. package/dist/index.js +1 -33
  20. package/dist/native-command.d.ts +19 -0
  21. package/dist/prime-manager-registry.d.ts +5 -1
  22. package/dist/rpc/notifications.d.ts +4 -3
  23. package/dist/rpc/port-file.js +1 -1
  24. package/dist/rpc/protocol.d.ts +15 -4
  25. package/dist/rpc/protocol.js +1 -1
  26. package/dist/rpc/rpc-client.d.ts +3 -1
  27. package/dist/rpc/rpc-client.js +1 -1
  28. package/dist/rpc/rpc-dir.js +1 -1
  29. package/dist/rpc/rpc-server.d.ts +5 -1
  30. package/dist/rpc/server-registry.d.ts +10 -0
  31. package/dist/setup/activation.d.ts +7 -0
  32. package/dist/setup/claustrum.d.ts +26 -0
  33. package/dist/setup/command-runner.d.ts +2 -0
  34. package/dist/setup/command.d.ts +20 -0
  35. package/dist/setup/command.test.d.ts +1 -0
  36. package/dist/setup/detect.d.ts +5 -0
  37. package/dist/setup/jsonc.d.ts +9 -0
  38. package/dist/setup/native-activation.d.ts +29 -0
  39. package/dist/setup/native-enrollment.d.ts +26 -0
  40. package/dist/setup/native-enrollment.test.d.ts +1 -0
  41. package/dist/setup/native-local.d.ts +20 -0
  42. package/dist/setup/native-login.d.ts +3 -0
  43. package/dist/setup/native-paths.d.ts +15 -0
  44. package/dist/setup/native-paths.test.d.ts +1 -0
  45. package/dist/setup/opencode-config.d.ts +18 -0
  46. package/dist/setup/paths.d.ts +15 -0
  47. package/dist/setup/pi.d.ts +18 -0
  48. package/dist/setup/process-fence.d.ts +20 -0
  49. package/dist/setup/process-fence.test.d.ts +1 -0
  50. package/dist/setup/types.d.ts +37 -0
  51. package/dist/sidebar-state.d.ts +3 -1
  52. package/dist/sidebar-state.js +1 -1
  53. package/dist/transform.d.ts +5 -1
  54. package/dist/tui/command-dialogs.d.ts +32 -2
  55. package/dist/tui-preferences.js +2 -2
  56. package/dist/v2/activation.d.ts +7 -0
  57. package/dist/v2/adapter.d.ts +276 -0
  58. package/dist/v2/install.d.ts +13 -0
  59. package/dist/v2/lifecycle.d.ts +8 -0
  60. package/dist/v2/menu-contract.d.ts +8 -0
  61. package/dist/v2/menu.d.ts +10 -0
  62. package/dist/v2/models.d.ts +6 -0
  63. package/dist/v2/ownership.d.ts +11 -0
  64. package/dist/v2/prime.d.ts +12 -0
  65. package/dist/v2/rpc.d.ts +50 -0
  66. package/dist/v2/server.d.ts +8 -0
  67. package/dist/v2/server.js +4 -0
  68. package/dist/v2/setup.d.ts +58 -0
  69. package/dist/v2/source.d.ts +41 -0
  70. package/dist/v2/tui.d.ts +13 -0
  71. package/dist/v2/tui.js +2 -0
  72. package/dist/v2/wire.d.ts +36 -0
  73. package/package.json +26 -6
  74. package/src/rpc/notifications.ts +37 -26
  75. package/src/rpc/protocol.ts +24 -7
  76. package/src/rpc/rpc-client.ts +18 -1
  77. package/src/rpc/rpc-server.ts +38 -8
  78. package/src/rpc/server-registry.ts +61 -0
  79. package/src/sidebar-state.ts +26 -2
  80. package/src/tui/command-dialogs.tsx +537 -68
  81. package/src/tui/entry.mjs +44 -18
  82. package/src/tui-compiled/rpc/protocol.ts +24 -7
  83. package/src/tui-compiled/rpc/rpc-client.ts +18 -1
  84. package/src/tui-compiled/sidebar-state.ts +26 -2
  85. package/src/tui-compiled/tui/command-dialogs.tsx +488 -95
  86. package/src/tui-compiled/tui.tsx +25 -7
  87. package/src/tui.tsx +38 -9
  88. package/dist/index-8aga2zwv.js +0 -4
  89. package/dist/index-d3f8g6ve.js +0 -428
  90. package/dist/index-sh51yqxs.js +0 -2
  91. package/dist/logger-jrkranbz.js +0 -1
@@ -0,0 +1,41 @@
1
+ import { type NativeAccountRuntime, type NativeAccountSnapshot, type NativeApiSubject, type NativeLocalCredentialValidation as NativeKnownCredentialSubject, type NativePoolPaths, type OAuthQuotaSnapshot } from '@cortexkit/anthropic-auth-core';
2
+ import type { RequestScope } from '@cortexkit/common-auth/opencode2';
3
+ import { type NativeAnthropicBodyOptions, type NativeAnthropicRequestContext, type NativeAnthropicResponseContext, type NativeAnthropicResponseOptions, type NativeAnthropicSource } from './adapter';
4
+ /**
5
+ * Identifies the account and credential version that served a send, so a
6
+ * later 401 or quota reading is recorded against that exact version. It never
7
+ * holds OAuth tokens, API keys or the private vault authorization receipt.
8
+ */
9
+ export type NativeSourceProvenance = {
10
+ readonly kind: 'local';
11
+ readonly subject: NativeKnownCredentialSubject;
12
+ } | {
13
+ readonly kind: 'vault';
14
+ readonly credentialId: string;
15
+ readonly recordVersion: number;
16
+ } | {
17
+ readonly kind: 'api';
18
+ readonly subject: NativeApiSubject;
19
+ };
20
+ export interface NativeSourceOptions {
21
+ readonly runtime: NativeAccountRuntime;
22
+ readonly paths: NativePoolPaths;
23
+ readonly requestOptions: (scope: NativeAnthropicRequestContext<NativeSourceProvenance>, snapshot: NativeAccountSnapshot) => NativeAnthropicBodyOptions;
24
+ readonly responseOptions?: (scope: NativeAnthropicResponseContext<NativeSourceProvenance>) => NativeAnthropicResponseOptions;
25
+ readonly onRequestId?: (scope: NativeAnthropicResponseContext<NativeSourceProvenance>, requestId: string) => void;
26
+ readonly onForgetSession?: (sessionID: string) => void;
27
+ readonly onOwnedRequest?: () => void;
28
+ readonly fetch?: typeof fetch;
29
+ /** Resolve a host model alias to the upstream ID before request-local policy. */
30
+ readonly resolveModelId?: (scope: RequestScope) => Promise<string>;
31
+ }
32
+ export interface NativeSource extends NativeAnthropicSource<OAuthQuotaSnapshot, NativeSourceProvenance> {
33
+ readonly close: () => Promise<void>;
34
+ readonly resetRouting: (sessionID: string) => Promise<void>;
35
+ readonly recordPrime: (provenance: NativeSourceProvenance, usage: {
36
+ inputTokens?: number;
37
+ outputTokens?: number;
38
+ }) => Promise<boolean>;
39
+ }
40
+ /** The native account service supplies fresh credentials; existing quota and routing rules choose each OpenCode 2 request's route. */
41
+ export declare function createNativeAnthropicSource(options: NativeSourceOptions): NativeSource;
@@ -0,0 +1,13 @@
1
+ import type { Plugin as Tui } from '@opencode/plugin/tui';
2
+ /**
3
+ * Shows the `claude` account menu in OpenCode 2 dialogs. The menu's sections,
4
+ * items and actions come from the plugin server over NATIVE_MENU_RPC, built by
5
+ * the same Core menu code that OpenCode 1 and Pi render, so every host shows
6
+ * the same menu.
7
+ */
8
+ export declare function setupAnthropicTui(ctx: Tui.Context): Promise<() => void>;
9
+ declare const _default: {
10
+ id: string;
11
+ setup: typeof setupAnthropicTui;
12
+ };
13
+ export default _default;
package/dist/v2/tui.js ADDED
@@ -0,0 +1,2 @@
1
+ import{s as F}from"../index-1jkyhgyr.js";import"../index-aaajxwzd.js";async function V(B){let Y=B.client.rpc(F),K=!0,$=!1,L=new AbortController,O=()=>{let w=B.ui.router.current();return w.type==="session"?w.sessionID:void 0};function M(w){return new Promise((J)=>{let j="",G=!1,Q=B.keymap.mode.push("cortexkit-protected-entry"),H=(z,D=!0)=>{if(G)return;G=!0,B.renderer.keyInput.off("keypress",X),B.renderer.keyInput.off("paste",Z),L.signal.removeEventListener("abort",W),Q();let q=z?j:void 0;if(j="",D)B.ui.dialog.clear();J(q)},W=()=>H(!1),X=(z)=>{if(z.preventDefault(),z.stopPropagation(),z.name==="escape"||z.ctrl&&z.name==="c")return H(!1);if(z.name==="return"||z.name==="enter")return H(!0);if(z.name==="backspace"){j=Array.from(j).slice(0,-1).join("");return}if(!z.ctrl&&!z.meta&&!z.option&&Array.from(z.sequence).every((D)=>D.charCodeAt(0)>=32&&D.charCodeAt(0)!==127))j=(j+z.sequence).slice(0,4096)},Z=(z)=>{z.preventDefault(),z.stopPropagation(),j=(j+Array.from(new TextDecoder().decode(z.bytes)).filter((D)=>D.charCodeAt(0)>=32&&D.charCodeAt(0)!==127).join("")).slice(0,4096)};if(B.renderer.keyInput.prependListener("keypress",X),B.renderer.keyInput.prependListener("paste",Z),L.signal.addEventListener("abort",W,{once:!0}),B.ui.dialog.alert({title:w,message:"Type or paste the value. Input is hidden. Enter accepts; Escape cancels."}).then(()=>H(!1,!1),()=>H(!1,!1)),L.signal.aborted)W()})}async function P(w){let J={};for(let j of w.knobs){if(!K)return;if(j.kind==="choice"){let G=await B.ui.dialog.select({title:j.label,options:j.choices.map((Q)=>({title:Q.label,value:Q.value})),current:j.value});if(G===void 0)return;J[j.id]=G}else if(j.kind==="toggle"){let G=await B.ui.dialog.select({title:j.label,options:[{title:"On",value:!0},{title:"Off",value:!1}],current:j.value});if(G===void 0)return;J[j.id]=G}else{let G=j.kind==="text"&&j.masked?await M(j.label):await B.ui.dialog.prompt({title:j.label,value:j.value===void 0?void 0:String(j.value),placeholder:j.kind==="text"?j.placeholder:void 0});if(G===void 0)return;J[j.id]=G===""&&!j.required?null:j.kind==="number"?Number(G):G}}return J}async function C(w,J){if(!K||$||O()!==w)return;$=!0;let j=J;try{while(K&&O()===w){let G=await B.ui.dialog.select({title:j.menu.title,options:j.menu.sections.map((D)=>({title:D.title,description:D.lines.join(`
2
+ `),value:D}))});if(!G||!K||O()!==w)return;let Q=[...G.actions.map((D)=>({action:D,itemId:void 0})),...G.items.flatMap((D)=>D.actions.map((q)=>({action:q,itemId:D.id})))],H=await B.ui.dialog.select({title:G.title,options:Q.map((D)=>({title:D.action.label,description:D.action.description,value:D}))});if(!H)continue;let W=await P(H.action);if(!W||!K||O()!==w)return;let X=H.action.confirm?await B.ui.dialog.confirm({title:H.action.label,message:H.action.confirm.message}):void 0;if(H.action.confirm&&X!==!0)continue;if(!K||O()!==w)return;let Z={command:"claude",sectionId:G.id,itemId:H.itemId,actionId:H.action.id,sessionId:w,values:W,confirmed:X},z=await Y.apply(Z,{signal:L.signal,location:B.location});if(!K||O()!==w)return;await B.ui.dialog.alert({title:z.ok?"Claude":"Claude action refused",message:z.text}),j={command:z.command,menu:z.menu}}}finally{$=!1}}let R=Y.events.on("opened",(w)=>C(w.data.sessionId,w.data.payload),{signal:L.signal}),U=Y.events.on("notification",(w)=>{if(K&&O()===w.data.sessionId)B.ui.toast.show({title:"Claude",sessionID:w.data.sessionId,message:w.data.message,variant:w.data.kind})},{signal:L.signal});return B.keymap.layer(()=>({commands:[{id:"cortexkit.claude",title:"Claude",group:"Claude",palette:!0,slash:{name:"claude"},async run(){let w=O();if(!w){await B.ui.dialog.alert({title:"Claude",message:"Open a session before using the Claude menu."});return}let J=await Y.open({sessionId:w},{signal:L.signal,location:B.location});await C(w,J)}}]})),()=>{K=!1,L.abort(),R(),U()}}var T={id:"cortexkit-anthropic-auth",setup:V};export{V as setupAnthropicTui,T as default};
@@ -0,0 +1,36 @@
1
+ import { type NativeAccountSnapshot } from '@cortexkit/anthropic-auth-core';
2
+ import type { Plugin } from '@opencode/plugin';
3
+ import type { Registration } from '@opencode/plugin/promise/registration';
4
+ import type { NativeAnthropicRequestContext, NativeAnthropicResponseContext } from './adapter';
5
+ import type { NativeSourceProvenance } from './source';
6
+ /**
7
+ * Claude Code's billing header names the user prompt a request belongs to and
8
+ * the provider request-id of the previous request for that prompt. Track that
9
+ * chain per user turn with OpenCode's message IDs: one session spans many
10
+ * turns, so a session ID alone cannot tell which turn a request serves.
11
+ */
12
+ export declare function createNativeWirePolicy(): {
13
+ install(ctx: Pick<Plugin.Context, 'session'>): Promise<Registration>;
14
+ requestOptions(input: NativeAnthropicRequestContext<NativeSourceProvenance>, snapshot: NativeAccountSnapshot): {
15
+ cache1hEnabled: boolean;
16
+ cache1hMode: "automatic" | "explicit" | "hybrid";
17
+ fastModeEnabled: boolean;
18
+ thinkingPrefixMismatchBehavior: "account-default" | "drop_block" | "error" | undefined;
19
+ midConversationEffortEnabled: boolean;
20
+ serverSideFallbackEnabled: boolean;
21
+ laneStart: boolean;
22
+ billingLineage: {
23
+ promptId: string;
24
+ previousRequestId: string | undefined;
25
+ } | undefined;
26
+ cacheDiagnosticsPreviousMessageId: string | undefined;
27
+ };
28
+ responseOptions(input: NativeAnthropicResponseContext<NativeSourceProvenance>): {
29
+ serverSideFallbackModel: string;
30
+ laneStart: boolean;
31
+ onMessageStart(message: Record<string, unknown>): void;
32
+ onStreamEnd(): void;
33
+ };
34
+ onRequestId(input: NativeAnthropicResponseContext<NativeSourceProvenance>, requestId: string): void;
35
+ forgetSession(sessionID: string): void;
36
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cortexkit/opencode-anthropic-auth",
3
- "version": "1.22.0",
3
+ "version": "2.0.0",
4
4
  "type": "module",
5
5
  "repository": {
6
6
  "type": "git",
@@ -18,6 +18,14 @@
18
18
  "types": "./dist/tui.d.ts",
19
19
  "import": "./src/tui/entry.mjs"
20
20
  },
21
+ "./server": {
22
+ "types": "./dist/v2/server.d.ts",
23
+ "import": "./dist/v2/server.js"
24
+ },
25
+ "./rpc": {
26
+ "types": "./dist/rpc/rpc-client.d.ts",
27
+ "import": "./dist/rpc/rpc-client.js"
28
+ },
21
29
  "./tui-prefs": {
22
30
  "types": "./dist/tui-preferences.d.ts",
23
31
  "import": "./dist/tui-preferences.js"
@@ -45,9 +53,10 @@
45
53
  "LICENSE"
46
54
  ],
47
55
  "scripts": {
48
- "build": "rm -rf dist && bun build src/index.ts src/cli.ts src/sidebar-state.ts src/tui-preferences.ts src/rpc/rpc-client.ts src/rpc/port-file.ts src/rpc/protocol.ts src/rpc/rpc-dir.ts --outdir dist --target node --format esm --splitting --external @opencode-ai/plugin --minify && tsc -p tsconfig.build.json --emitDeclarationOnly && bun run build:tui",
56
+ "build": "rm -rf dist && bun build src/index.ts src/v2/server.ts src/v2/tui.ts src/cli.ts src/sidebar-state.ts src/tui-preferences.ts src/rpc/rpc-client.ts src/rpc/port-file.ts src/rpc/protocol.ts src/rpc/rpc-dir.ts --outdir dist --root src --target node --format esm --splitting --external @opencode-ai/plugin --external @opencode/plugin --external jsonc-parser --minify && tsc -p tsconfig.build.json --emitDeclarationOnly && bun run build:tui && bun run check:bundle",
49
57
  "build:tui": "bun scripts/build-tui.ts",
50
58
  "smoke:tui": "bun scripts/smoke-tui-pack-install.ts",
59
+ "check:bundle": "bun scripts/check-bundle-globals.ts",
51
60
  "build:dev": "rm -rf dist && tsc -p tsconfig.build.json",
52
61
  "dev": "bun ../../scripts/dev.ts",
53
62
  "dev:clean": "bun ../../scripts/dev-clean.ts",
@@ -58,13 +67,24 @@
58
67
  "prepublishOnly": "bun run build"
59
68
  },
60
69
  "peerDependencies": {
61
- "@opencode-ai/plugin": "*"
70
+ "@opencode-ai/plugin": "*",
71
+ "@opencode/plugin": ">=2.0.21"
72
+ },
73
+ "peerDependenciesMeta": {
74
+ "@opencode/plugin": {
75
+ "optional": true
76
+ }
62
77
  },
63
78
  "dependencies": {
64
- "@cortexkit/anthropic-auth-core": "1.22.0",
65
- "@opentui/core": "0.5.7",
66
- "@opentui/solid": "0.5.7",
79
+ "@clack/prompts": "^1.6.0",
80
+ "@cortexkit/anthropic-auth-core": "2.0.0",
81
+ "@opentui/core": "0.5.11",
82
+ "@opentui/solid": "0.5.11",
67
83
  "jsonc-parser": "^3.3.1",
68
84
  "solid-js": "1.9.12"
85
+ },
86
+ "devDependencies": {
87
+ "@ai-sdk/anthropic": "4.0.58",
88
+ "@opencode/plugin": "2.0.22"
69
89
  }
70
90
  }
@@ -1,54 +1,65 @@
1
+ import type { CommandDialogPayload } from '@cortexkit/anthropic-auth-core'
1
2
  import type { OpenDialogPayload, RpcNotification } from './protocol'
2
3
 
3
4
  const QUEUE_CAP = 100
4
5
  const TUI_CONNECTED_WINDOW_MS = 3_000
5
6
 
7
+ // One queue serves every RPC server in the process, and a process can hold one
8
+ // server per project directory. Session ids are globally unique, so both
9
+ // producers and drains require one: an unscoped drain could otherwise expose
10
+ // another project's pending command dialog.
6
11
  let queue: RpcNotification[] = []
7
12
  let nextId = 1
8
- let lastDrainAtAny = 0
9
13
  const lastDrainAtBySession = new Map<string, number>()
10
14
 
15
+ function assertSessionId(sessionId: string): void {
16
+ if (typeof sessionId !== 'string' || sessionId.length === 0) {
17
+ throw new TypeError('sessionId is required')
18
+ }
19
+ }
20
+
11
21
  export function pushNotification(
12
- payload: OpenDialogPayload,
13
- sessionId?: string,
22
+ payload: OpenDialogPayload | CommandDialogPayload,
23
+ sessionId: string,
14
24
  ): void {
15
- queue.push({ id: nextId++, type: 'open-dialog', payload, sessionId })
25
+ assertSessionId(sessionId)
26
+ if ('menu' in payload) {
27
+ if (payload.command !== 'claude' || payload.menu.command !== 'claude')
28
+ throw new TypeError('Only the claude menu is supported')
29
+ queue.push({ id: nextId++, type: 'open-menu', payload, sessionId })
30
+ } else {
31
+ queue.push({ id: nextId++, type: 'open-dialog', payload, sessionId })
32
+ }
16
33
  if (queue.length > QUEUE_CAP) queue = queue.slice(queue.length - QUEUE_CAP)
17
34
  }
18
35
 
19
36
  export function drainNotifications(
20
- lastReceivedId = 0,
21
- sessionId?: string,
37
+ lastReceivedId: number,
38
+ sessionId: string,
22
39
  ): RpcNotification[] {
23
- const now = Date.now()
24
- lastDrainAtAny = now
25
- if (sessionId !== undefined) lastDrainAtBySession.set(sessionId, now)
26
- const matches = (n: RpcNotification) =>
27
- sessionId === undefined ||
28
- n.sessionId === undefined ||
29
- n.sessionId === sessionId
40
+ assertSessionId(sessionId)
41
+ lastDrainAtBySession.set(sessionId, Date.now())
30
42
  if (lastReceivedId > 0) {
31
- queue = queue.filter((n) => {
32
- if (n.id > lastReceivedId) return true
33
- if (sessionId === undefined) return false
34
- return n.sessionId !== sessionId
35
- })
43
+ queue = queue.filter(
44
+ (notification) =>
45
+ notification.id > lastReceivedId ||
46
+ notification.sessionId !== sessionId,
47
+ )
36
48
  }
37
- return queue.filter((n) => n.id > lastReceivedId && matches(n))
49
+ return queue.filter(
50
+ (notification) =>
51
+ notification.id > lastReceivedId && notification.sessionId === sessionId,
52
+ )
38
53
  }
39
54
 
40
- export function isTuiConnected(sessionId?: string): boolean {
55
+ export function isTuiConnected(sessionId: string): boolean {
41
56
  const now = Date.now()
42
- if (sessionId !== undefined) {
43
- const at = lastDrainAtBySession.get(sessionId) ?? 0
44
- return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS
45
- }
46
- return lastDrainAtAny > 0 && now - lastDrainAtAny < TUI_CONNECTED_WINDOW_MS
57
+ const at = lastDrainAtBySession.get(sessionId) ?? 0
58
+ return at > 0 && now - at < TUI_CONNECTED_WINDOW_MS
47
59
  }
48
60
 
49
61
  export function resetNotificationsForTest(): void {
50
62
  queue = []
51
63
  nextId = 1
52
- lastDrainAtAny = 0
53
64
  lastDrainAtBySession.clear()
54
65
  }
@@ -1,3 +1,8 @@
1
+ import type {
2
+ ClaustrumEnrollmentStatus,
3
+ CommandDialogPayload,
4
+ } from '@cortexkit/anthropic-auth-core'
5
+
1
6
  export const COMMAND_MODAL_NAMES = [
2
7
  'claude-account',
3
8
  'claude-cache',
@@ -17,17 +22,31 @@ export type CommandModalName = (typeof COMMAND_MODAL_NAMES)[number]
17
22
  export interface AccountDialogAccount {
18
23
  id: string
19
24
  label: string
20
- role: string
25
+ role: 'main' | 'fallback'
21
26
  enabled: boolean
22
27
  quotaPercent: number | null
23
28
  tierLabel?: string
24
29
  claustrumGate: 'on' | 'off' | 'na'
25
30
  vaultServed: boolean
31
+ vaultReauth: boolean
32
+ custodyState:
33
+ | 'na'
34
+ | 'off'
35
+ | 'on-vault-served'
36
+ | 'on-vault-reauth'
37
+ | 'on-cold'
38
+ | 'unknown-identity'
39
+ | 'on-identity-mismatch'
40
+ | 'on-corrupt-binding'
26
41
  }
27
42
 
28
43
  export interface AccountDialogKnobs {
29
44
  accounts: AccountDialogAccount[]
30
45
  claustrumDetection: string
46
+ custodyMode?: 'local' | 'claustrum'
47
+ custodyModeKnown?: boolean
48
+ enrollmentStatus?: string
49
+ claustrumEnrollment?: ClaustrumEnrollmentStatus
31
50
  [key: string]: unknown
32
51
  }
33
52
 
@@ -37,12 +56,10 @@ export interface OpenDialogPayload {
37
56
  knobs: Record<string, unknown>
38
57
  }
39
58
 
40
- export interface RpcNotification {
41
- id: number
42
- type: 'open-dialog'
43
- payload: OpenDialogPayload
44
- sessionId?: string
45
- }
59
+ export type RpcNotification = { id: number; sessionId: string } & (
60
+ | { type: 'open-dialog'; payload: OpenDialogPayload }
61
+ | { type: 'open-menu'; payload: CommandDialogPayload }
62
+ )
46
63
 
47
64
  export interface ApplyRequest {
48
65
  command: CommandModalName
@@ -1,12 +1,17 @@
1
+ import type {
2
+ CommandApplyRequest,
3
+ CommandApplyResult,
4
+ } from '@cortexkit/anthropic-auth-core'
1
5
  import { discoverPortFile } from './port-file'
2
6
  import type { ApplyRequest, ApplyResult, RpcNotification } from './protocol'
3
7
 
4
8
  export interface RpcClient {
5
9
  pending: (
6
10
  lastReceivedId: number,
7
- sessionId?: string,
11
+ sessionId: string,
8
12
  ) => Promise<RpcNotification[]>
9
13
  apply: (request: ApplyRequest) => Promise<ApplyResult>
14
+ applyMenu: (request: CommandApplyRequest) => Promise<CommandApplyResult>
10
15
  }
11
16
 
12
17
  async function call<T>(
@@ -58,5 +63,17 @@ export function createRpcClient(dir: string, expectedPid?: number): RpcClient {
58
63
  )
59
64
  return out ?? { text: 'apply failed', knobs: {} }
60
65
  },
66
+ async applyMenu(request) {
67
+ if (request.command !== 'claude' || !request.sessionId?.trim())
68
+ throw new TypeError('A claude menu sessionId is required')
69
+ const out = await call<CommandApplyResult>(
70
+ dir,
71
+ 'apply-menu',
72
+ { ...request },
73
+ expectedPid,
74
+ )
75
+ if (!out) throw new Error('Menu apply unavailable; outcome unknown')
76
+ return out
77
+ },
61
78
  }
62
79
  }
@@ -1,11 +1,16 @@
1
1
  import { randomBytes, timingSafeEqual } from 'node:crypto'
2
- import { unlink } from 'node:fs/promises'
2
+ import { readFile, unlink } from 'node:fs/promises'
3
3
  import {
4
4
  createServer,
5
5
  type IncomingMessage,
6
6
  type ServerResponse,
7
7
  } from 'node:http'
8
8
  import { join } from 'node:path'
9
+ import {
10
+ type CommandApplyRequest,
11
+ type CommandApplyResult,
12
+ parseApplyRequest,
13
+ } from '@cortexkit/anthropic-auth-core'
9
14
  import type { drainNotifications } from './notifications'
10
15
  import { writePortFile } from './port-file'
11
16
  import type { ApplyRequest, ApplyResult } from './protocol'
@@ -19,7 +24,10 @@ export interface RpcServerHandle {
19
24
  export interface RpcServerOptions {
20
25
  dir: string
21
26
  drain: typeof drainNotifications
22
- apply: (request: ApplyRequest) => Promise<ApplyResult>
27
+ /** Structured-menu hosts need no text parser; internal text consumers may still supply one. */
28
+ apply?: (request: ApplyRequest) => Promise<ApplyResult>
29
+ /** Run menu actions through the host's credential service, not legacy account-file writes. */
30
+ applyMenu?: (request: CommandApplyRequest) => Promise<CommandApplyResult>
23
31
  }
24
32
 
25
33
  function readBody(req: IncomingMessage): Promise<string> {
@@ -74,21 +82,36 @@ export async function startRpcServer(
74
82
  unknown
75
83
  >
76
84
  if (method === 'pending-notifications') {
85
+ if (
86
+ typeof params.sessionId !== 'string' ||
87
+ params.sessionId.length === 0
88
+ ) {
89
+ return json(400, { error: 'sessionId is required' })
90
+ }
77
91
  const messages = options.drain(
78
92
  Number(params.lastReceivedId ?? 0),
79
- typeof params.sessionId === 'string' ? params.sessionId : undefined,
93
+ params.sessionId,
80
94
  )
81
95
  return json(200, { messages })
82
96
  }
83
97
  if (method === 'apply') {
98
+ if (!options.apply)
99
+ return json(501, { error: 'text apply unavailable' })
84
100
  const result = await options.apply(params as unknown as ApplyRequest)
85
101
  return json(200, result)
86
102
  }
103
+ if (method === 'apply-menu') {
104
+ const request = parseApplyRequest(params)
105
+ if (request?.command !== 'claude' || !request.sessionId?.trim())
106
+ return json(400, { error: 'invalid menu request' })
107
+ if (!options.applyMenu) return json(501, { error: 'menu unavailable' })
108
+ return json(200, await options.applyMenu(request))
109
+ }
87
110
  return json(404, { error: 'unknown method' })
88
- } catch (error) {
111
+ } catch {
89
112
  if (!res.headersSent && !res.writableEnded && !res.destroyed) {
90
113
  json(500, {
91
- error: error instanceof Error ? error.message : String(error),
114
+ error: 'RPC request failed',
92
115
  })
93
116
  }
94
117
  }
@@ -115,9 +138,16 @@ export async function startRpcServer(
115
138
  token,
116
139
  async stop() {
117
140
  await new Promise<void>((resolve) => server.close(() => resolve()))
118
- await unlink(join(options.dir, `port-${process.pid}.json`)).catch(
119
- () => {},
120
- )
141
+ try {
142
+ const portFile = join(options.dir, `port-${process.pid}.json`)
143
+ const current = JSON.parse(await readFile(portFile, 'utf8')) as {
144
+ port?: unknown
145
+ pid?: unknown
146
+ }
147
+ if (current.port === port && current.pid === process.pid) {
148
+ await unlink(portFile)
149
+ }
150
+ } catch {}
121
151
  },
122
152
  }
123
153
  }
@@ -0,0 +1,61 @@
1
+ import type { RpcServerHandle } from './rpc-server'
2
+
3
+ type RpcServerGlobal = typeof globalThis & {
4
+ __anthropicAuthRpcServers?: Map<string, RpcServerHandle>
5
+ __anthropicAuthRpcServersPending?: Map<string, Promise<void>>
6
+ }
7
+
8
+ export interface RpcServerAdoption {
9
+ server: RpcServerHandle
10
+ release: () => Promise<void>
11
+ }
12
+
13
+ /**
14
+ * Serializes same-directory replacement and returns an identity-fenced lease.
15
+ * Distinct project directories remain independent.
16
+ */
17
+ export async function adoptRpcServer(
18
+ rpcDir: string,
19
+ create: () => Promise<RpcServerHandle>,
20
+ ): Promise<RpcServerAdoption> {
21
+ const rpcGlobal = globalThis as RpcServerGlobal
22
+ const servers =
23
+ rpcGlobal.__anthropicAuthRpcServers ?? new Map<string, RpcServerHandle>()
24
+ const pending =
25
+ rpcGlobal.__anthropicAuthRpcServersPending ??
26
+ new Map<string, Promise<void>>()
27
+ rpcGlobal.__anthropicAuthRpcServers = servers
28
+ rpcGlobal.__anthropicAuthRpcServersPending = pending
29
+
30
+ const predecessor = pending.get(rpcDir) ?? Promise.resolve()
31
+ let server: RpcServerHandle | undefined
32
+ const start = predecessor
33
+ .catch(() => {})
34
+ .then(async () => {
35
+ const previous = servers.get(rpcDir)
36
+ if (previous) {
37
+ await previous.stop()
38
+ if (servers.get(rpcDir) === previous) servers.delete(rpcDir)
39
+ }
40
+ server = await create()
41
+ servers.set(rpcDir, server)
42
+ })
43
+ pending.set(rpcDir, start)
44
+
45
+ try {
46
+ await start
47
+ } finally {
48
+ if (pending.get(rpcDir) === start) pending.delete(rpcDir)
49
+ }
50
+ const adoptedServer = server
51
+ if (!adoptedServer) throw new Error('RPC server failed to start')
52
+
53
+ return {
54
+ server: adoptedServer,
55
+ release: async () => {
56
+ if (servers.get(rpcDir) !== adoptedServer) return
57
+ await adoptedServer.stop()
58
+ if (servers.get(rpcDir) === adoptedServer) servers.delete(rpcDir)
59
+ },
60
+ }
61
+ }
@@ -36,6 +36,16 @@ export interface SidebarAccountState {
36
36
  needsReauth: boolean
37
37
  // True when the vault copy needs re-importing while the sidecar remains usable.
38
38
  vaultReauth?: boolean
39
+ // A gate alone does not prove the sidecar can serve a request.
40
+ vaultServed?: boolean
41
+ custodyState?:
42
+ | 'off'
43
+ | 'on-vault-served'
44
+ | 'on-vault-reauth'
45
+ | 'on-cold'
46
+ | 'unknown-identity'
47
+ | 'on-identity-mismatch'
48
+ | 'on-corrupt-binding'
39
49
  tierLabel?: string
40
50
  }
41
51
 
@@ -59,7 +69,7 @@ export interface PrimeSidebarAccountState {
59
69
  label: string
60
70
  nextDueAt?: number | null
61
71
  lastPrimedAt?: number | null
62
- lastResult?: 'ok' | 'error'
72
+ lastResult?: 'ok' | 'error' | 'skipped'
63
73
  usage?: PrimeUsageCounters
64
74
  estimatedCostUsd?: number
65
75
  }
@@ -180,7 +190,11 @@ function normalizePrimeAccount(
180
190
  } else if (isFiniteNumber(value.lastPrimedAt)) {
181
191
  account.lastPrimedAt = value.lastPrimedAt
182
192
  }
183
- if (value.lastResult === 'ok' || value.lastResult === 'error') {
193
+ if (
194
+ value.lastResult === 'ok' ||
195
+ value.lastResult === 'error' ||
196
+ value.lastResult === 'skipped'
197
+ ) {
184
198
  account.lastResult = value.lastResult
185
199
  }
186
200
  const usage = normalizePrimeUsage(value.usage)
@@ -338,6 +352,11 @@ export function normalizeSidebarState(raw: unknown): SidebarState {
338
352
  needsReauth:
339
353
  typeof entry.needsReauth === 'boolean' ? entry.needsReauth : false,
340
354
  ...(entry.vaultReauth === true && { vaultReauth: true }),
355
+ ...(entry.vaultServed === true && { vaultServed: true }),
356
+ ...(typeof entry.custodyState === 'string' && {
357
+ custodyState:
358
+ entry.custodyState as SidebarAccountState['custodyState'],
359
+ }),
341
360
  tierLabel:
342
361
  typeof entry.tierLabel === 'string' && entry.tierLabel.trim()
343
362
  ? entry.tierLabel.trim()
@@ -817,6 +836,8 @@ export function formatFallbackModelLabel(modelId: string | undefined): string {
817
836
  return 'Fable 5.1'
818
837
  if (modelId === 'claude-fable-5' || modelId?.startsWith('claude-fable-5-'))
819
838
  return 'Fable 5'
839
+ if (modelId === 'claude-opus-5-5' || modelId?.startsWith('claude-opus-5-5-'))
840
+ return 'Opus 5.5'
820
841
  if (modelId === 'claude-opus-5' || modelId?.startsWith('claude-opus-5-'))
821
842
  return 'Opus 5'
822
843
  if (modelId === 'claude-opus-4-8' || modelId?.startsWith('claude-opus-4-8-'))
@@ -844,6 +865,9 @@ export function formatPrimeAccountValue(account: PrimeSidebarAccountState): {
844
865
  if (account.lastResult === 'error') {
845
866
  return { text: 'err', hasError: true }
846
867
  }
868
+ if (account.lastResult === 'skipped') {
869
+ return { text: 'skip', hasError: false }
870
+ }
847
871
  if (account.nextDueAt && account.nextDueAt > Date.now()) {
848
872
  return { text: formatPrimeTime(account.nextDueAt), hasError: false }
849
873
  }