@jskit-ai/assistant-core 0.1.200 → 0.1.202

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 (164) hide show
  1. package/examples/conversation/README.md +8 -1
  2. package/examples/conversation/package.json +5 -5
  3. package/fixtures/responsive-assistant/App.vue +4 -1
  4. package/package.json +99 -36
  5. package/src/client/conversation/AssistantConversationElement.vue +27 -4
  6. package/src/client/conversation/AssistantConversationStatus.vue +24 -0
  7. package/src/client/conversation/AssistantGoalControl.vue +8 -5
  8. package/src/client/conversation/AssistantTranscript.vue +10 -3
  9. package/src/client/conversation/index.js +2 -0
  10. package/src/client/conversation/messageDelivery.js +115 -32
  11. package/src/client/conversation/retainedConversation.js +32 -0
  12. package/src/client/conversation/useAssistantQuestions.js +76 -0
  13. package/src/client/lib/assistantApi.js +72 -2
  14. package/src/server/conversation/agentRun.js +222 -0
  15. package/src/server/conversation/attachments.js +93 -0
  16. package/src/server/conversation/claudeClient.js +221 -0
  17. package/src/server/conversation/claudeHistory.js +256 -0
  18. package/src/server/conversation/claudeProcess.js +327 -0
  19. package/src/server/conversation/claudeTools.js +42 -0
  20. package/src/server/conversation/claudeTurn.js +1082 -0
  21. package/src/server/conversation/codexConfiguration.js +1183 -0
  22. package/src/server/conversation/codexConversations.js +1082 -0
  23. package/src/server/conversation/codexDetachedTurn.js +332 -0
  24. package/src/server/conversation/codexEvents.js +194 -1
  25. package/src/server/conversation/codexGoalCommands.js +124 -0
  26. package/src/server/conversation/codexHelperExecution.js +306 -0
  27. package/src/server/conversation/codexHelperThreadLedger.js +804 -0
  28. package/src/server/conversation/codexHelperThreadRecovery.js +395 -0
  29. package/src/server/conversation/codexHelperThreads.js +638 -0
  30. package/src/server/conversation/codexHistoryAdapter.js +369 -0
  31. package/src/server/conversation/codexMessageCommands.js +663 -0
  32. package/src/server/conversation/codexMessageDelivery.js +211 -0
  33. package/src/server/conversation/codexNativeHistoryExport.js +95 -0
  34. package/src/server/conversation/codexNotificationObserver.js +482 -0
  35. package/src/server/conversation/codexNotificationQueue.js +103 -0
  36. package/src/server/conversation/codexObservationControl.js +174 -0
  37. package/src/server/conversation/codexOutput.js +931 -0
  38. package/src/server/conversation/codexProcess.js +1559 -0
  39. package/src/server/conversation/codexProcessLeader.js +14 -0
  40. package/src/server/conversation/codexProvider.js +2710 -0
  41. package/src/server/conversation/codexProviderOwner.js +1058 -0
  42. package/src/server/conversation/codexProviderSelection.js +218 -0
  43. package/src/server/conversation/codexReasoning.js +314 -0
  44. package/src/server/conversation/codexRenewalCommands.js +290 -0
  45. package/src/server/conversation/codexResultSettlement.js +409 -0
  46. package/src/server/conversation/codexRunJournal.js +658 -0
  47. package/src/server/conversation/codexRuntimeLifecycle.js +288 -0
  48. package/src/server/conversation/codexSessionCleanup.js +96 -0
  49. package/src/server/conversation/codexSessionReconciliation.js +284 -0
  50. package/src/server/conversation/codexThreadReadiness.js +397 -0
  51. package/src/server/conversation/codexThreadStatus.js +354 -0
  52. package/src/server/conversation/codexTurn.js +701 -255
  53. package/src/server/conversation/codexTurnControl.js +317 -0
  54. package/src/server/conversation/codexTurnRecovery.js +990 -0
  55. package/src/server/conversation/codexTurnState.js +549 -0
  56. package/src/server/conversation/commandHook.js +18 -0
  57. package/src/server/conversation/commandWrapper.js +37 -0
  58. package/src/server/conversation/configuration.js +43 -0
  59. package/src/server/conversation/continuity.js +289 -0
  60. package/src/server/conversation/fileStorage.js +55 -0
  61. package/src/server/conversation/hookBridge.js +19 -0
  62. package/src/server/conversation/index.js +13 -0
  63. package/src/server/conversation/localExecution.js +665 -0
  64. package/src/server/conversation/memoryStorage.js +7 -72
  65. package/src/server/conversation/nativeHistoryExport.js +90 -0
  66. package/src/server/conversation/nativeStorage.js +23 -0
  67. package/src/server/conversation/openCodeClient.js +209 -19
  68. package/src/server/conversation/openCodePlugin.js +84 -0
  69. package/src/server/conversation/openCodeProcess.js +476 -0
  70. package/src/server/conversation/openCodeRuntime.js +1578 -0
  71. package/src/server/conversation/openCodeTools.js +60 -0
  72. package/src/server/conversation/openCodeTurn.js +557 -0
  73. package/src/server/conversation/provider.js +30 -0
  74. package/src/server/conversation/providers/api.js +136 -0
  75. package/src/server/conversation/providers/claude.js +63 -0
  76. package/src/server/conversation/providers/claudeDriver.js +425 -0
  77. package/src/server/conversation/providers/codex.js +248 -0
  78. package/src/server/conversation/providers/codexDriver.js +1078 -0
  79. package/src/server/conversation/providers/opencode.js +49 -0
  80. package/src/server/conversation/providers/opencodeDriver.js +650 -0
  81. package/src/server/conversation/providers/scoped.js +312 -0
  82. package/src/server/conversation/runtime.js +2220 -0
  83. package/src/server/conversation/runtimeStateUpgrade.js +125 -0
  84. package/src/server/conversation/storage.js +184 -0
  85. package/src/server/conversation/streams.js +7 -2
  86. package/src/server/conversation/structuredOutput.js +240 -0
  87. package/src/server/conversation/systemPrompt.js +54 -0
  88. package/src/server/conversation/tools.js +84 -0
  89. package/src/server/conversation/transcript.js +5 -4
  90. package/src/server/hosts/claudeHistory.js +5 -0
  91. package/src/server/hosts/claudeProcess.js +11 -0
  92. package/src/server/hosts/claudeTurn.js +4 -0
  93. package/src/server/hosts/codexConfiguration.js +20 -0
  94. package/src/server/hosts/codexEvents.js +13 -0
  95. package/src/server/hosts/codexProcess.js +12 -0
  96. package/src/server/hosts/codexProvider.js +23 -0
  97. package/src/server/hosts/codexTurn.js +20 -0
  98. package/src/server/hosts/nativeHistory.js +3 -0
  99. package/src/server/hosts/openCodeClient.js +3 -0
  100. package/src/server/hosts/openCodeProcess.js +23 -0
  101. package/src/server/hosts/openCodeTurn.js +7 -0
  102. package/src/server/index.js +2 -0
  103. package/src/server/lib/aiConnectionClient.js +13 -6
  104. package/src/server/lib/assistantCompletion.js +190 -0
  105. package/src/server/lib/assistantToolLoop.js +314 -0
  106. package/src/server/lib/serviceToolCatalog.js +25 -10
  107. package/src/shared/assistantResponseText.js +29 -0
  108. package/src/shared/conversation/attachments.js +19 -0
  109. package/src/shared/conversation/index.js +3 -0
  110. package/src/shared/conversation/pagination.js +78 -0
  111. package/src/shared/conversation/patches.js +150 -0
  112. package/src/shared/conversation/questions.js +48 -0
  113. package/src/shared/conversation/streaming.js +7 -4
  114. package/src/shared/index.js +2 -0
  115. package/src/shared/nativeProviders.js +45 -0
  116. package/src/testing/conversationStorageContract.js +31 -1
  117. package/src/testing/nativeCodex.js +11 -0
  118. package/src/testing/nativeOpenCode.js +3 -0
  119. package/test/aiConnectionClient.test.js +2 -0
  120. package/test/apiConversationDriver.test.js +73 -0
  121. package/test/assistantApiSurfaceHeader.test.js +85 -0
  122. package/test/assistantBoundedToolLoop.test.js +285 -0
  123. package/test/assistantScroll.browser.test.js +16 -0
  124. package/test/claudeClient.test.js +189 -0
  125. package/test/claudeConversationSession.test.js +1173 -0
  126. package/test/claudeInstructionResume.test.js +82 -0
  127. package/test/claudeProcess.test.js +204 -0
  128. package/test/claudeTurn.test.js +253 -0
  129. package/test/codexConversationSession.test.js +2122 -0
  130. package/test/codexGoal.test.js +37 -0
  131. package/test/codexHelperDeletion.test.js +163 -0
  132. package/test/codexHelperThreadLedger.test.js +494 -0
  133. package/test/codexHistoryAdapter.test.js +541 -0
  134. package/test/codexInteractiveArguments.test.js +30 -0
  135. package/test/codexIsolation.test.js +756 -0
  136. package/test/codexModelCatalog.test.js +362 -0
  137. package/test/codexNativeHistoryExport.test.js +81 -0
  138. package/test/codexThreadSelection.test.js +136 -0
  139. package/test/codexTurn.test.js +2158 -0
  140. package/test/conversation.test.js +110 -0
  141. package/test/conversationAgentRun.test.js +326 -0
  142. package/test/conversationApiFacilities.test.js +90 -0
  143. package/test/conversationAttachments.test.js +238 -0
  144. package/test/conversationCommandWrapper.test.js +48 -0
  145. package/test/conversationConfiguration.test.js +174 -0
  146. package/test/conversationNativeStorage.test.js +25 -0
  147. package/test/conversationReplacement.test.js +172 -0
  148. package/test/conversationRuntime.test.js +241 -0
  149. package/test/conversationRuntimeStateUpgrade.test.js +144 -0
  150. package/test/conversationSession.test.js +1080 -0
  151. package/test/conversationStreaming.test.js +68 -0
  152. package/test/conversationSystemPrompt.test.js +84 -0
  153. package/test/conversationTools.test.js +416 -0
  154. package/test/fileConversationStorage.test.js +73 -0
  155. package/test/fixtures/codexHelperThreadLedger.js +39 -0
  156. package/test/localConversationExecution.test.js +90 -0
  157. package/test/messageDelivery.test.js +234 -1
  158. package/test/nativeConversationRetirement.test.js +215 -0
  159. package/test/openCodeClient.test.js +515 -0
  160. package/test/openCodeProcess.test.js +177 -0
  161. package/test/openCodeRuntime.test.js +322 -0
  162. package/test/openCodeTools.test.js +287 -0
  163. package/test/opencodeConversationSession.test.js +789 -0
  164. package/test/serviceToolCatalog.test.js +24 -0
@@ -0,0 +1,327 @@
1
+ import { nativeAiProvider } from "../../shared/nativeProviders.js";
2
+ import { Duplex } from "node:stream";
3
+ import { stat } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import path from "node:path";
6
+ import { createClaudeJsonClient } from "./claudeClient.js";
7
+ import { createLocalConversationExecution } from "./localExecution.js";
8
+ import { CLAUDE_APPLICATION_TOOL_SERVER } from "./claudeTools.js";
9
+ import { nativeCommandHook, validateCommandWrapper } from "./commandWrapper.js";
10
+
11
+ const statusReads = new WeakMap();
12
+
13
+ export async function readClaudeCodeAuthStatus({
14
+ env = process.env, credentialHome = { home: env.HOME || homedir() }, command = "claude", commandRunner, signal
15
+ } = {}) {
16
+ if (credentialHome?.ok === false) return { loggedIn: false, error: credentialHome.error };
17
+ if (typeof commandRunner !== "function") throw new TypeError("Claude account status requires its host capture runner.");
18
+ const configRoot = env.CLAUDE_CONFIG_DIR || path.join(credentialHome.home, ".claude");
19
+ // Inspect file metadata only. Claude still owns reading and validating credentials.
20
+ const revisions = await Promise.all([
21
+ path.join(configRoot, ".credentials.json"), path.join(configRoot, ".claude.json"),
22
+ path.join(credentialHome.home, ".claude.json")
23
+ ].map(async (file) => {
24
+ try {
25
+ const value = await stat(file, { bigint: true });
26
+ return [value.dev, value.ino, value.size, value.mtimeNs, value.ctimeNs].map(String);
27
+ } catch (error) {
28
+ if (error.code === "ENOENT") return null;
29
+ throw error;
30
+ }
31
+ }));
32
+ let reads = statusReads.get(commandRunner);
33
+ if (!reads) statusReads.set(commandRunner, reads = new Map());
34
+ const key = JSON.stringify([command, env, credentialHome]);
35
+ const revision = JSON.stringify(revisions);
36
+ const previous = reads.get(key);
37
+ if (previous?.revision === revision && (previous.pending || previous.expiresAt > Date.now())) return previous.value;
38
+ const entry = { revision, pending: true, expiresAt: 0 };
39
+ reads.set(key, entry);
40
+ entry.value = readNativeClaudeCodeAuthStatus({ env, credentialHome, command, commandRunner, signal }).then((status) => {
41
+ // Keychain changes cannot be observed with file metadata on macOS.
42
+ if (!status.error && process.platform !== "darwin") entry.expiresAt = Date.now() + 30_000;
43
+ return Object.freeze(status);
44
+ }).finally(() => { entry.pending = false; });
45
+ return entry.value;
46
+ }
47
+
48
+ async function readNativeClaudeCodeAuthStatus({ env, credentialHome, command, commandRunner, signal }) {
49
+ const result = await commandRunner({
50
+ command, args: ["auth", "status", "--json"], baseEnv: env,
51
+ credentialHome, cwd: credentialHome.home, mode: "capture",
52
+ timeout: 30_000, maxBuffer: 64 * 1024, ...(signal ? { signal } : {})
53
+ });
54
+ const failure = { loggedIn: false, error: result.error || "Claude Code is not connected." };
55
+ if (!result.ok && (result.exitCode !== 1 || result.timedOut || result.signal)) return failure;
56
+ try {
57
+ const status = JSON.parse(result.stdout || result.output);
58
+ // Native auth status exits 1 for a normal signed-out JSON response.
59
+ if (!result.ok && status?.loggedIn !== false) return failure;
60
+ if (typeof status?.loggedIn !== "boolean") throw new Error("Invalid account status.");
61
+ return { loggedIn: status.loggedIn === true, email: String(status.email || ""),
62
+ authMethod: String(status.authMethod || ""), subscriptionType: String(status.subscriptionType || "") };
63
+ } catch {
64
+ return result.ok ? { loggedIn: false, error: "Claude Code returned an invalid account status." } : failure;
65
+ }
66
+ }
67
+
68
+ /** Native provider routing; the application supplies only its authorized selection. */
69
+ export function claudeModelConfiguration({ providerId, model: modelId }, connection) {
70
+ const provider = nativeAiProvider(providerId);
71
+ const definition = provider?.models.find(({ id }) => id === modelId);
72
+ if (provider && (!definition || !connection?.apiKey)) throw new Error("Choose a connected Claude Code model.");
73
+ const model = definition?.contextWindow >= 1000000 ? `${definition.id}[1m]` : modelId;
74
+ const externalModel = provider ? model : "";
75
+ // Reset every provider override when returning to the native subscription.
76
+ // Background calls and subagents must stay on the selected provider too.
77
+ const env = {
78
+ ANTHROPIC_BASE_URL: connection?.baseUrl || "https://api.anthropic.com",
79
+ ANTHROPIC_AUTH_TOKEN: connection?.apiKey || "", ANTHROPIC_API_KEY: "", CLAUDE_CODE_OAUTH_TOKEN: "",
80
+ ANTHROPIC_MODEL: externalModel, ANTHROPIC_DEFAULT_MODEL: externalModel,
81
+ ANTHROPIC_DEFAULT_OPUS_MODEL: externalModel,
82
+ ANTHROPIC_DEFAULT_SONNET_MODEL: externalModel,
83
+ ANTHROPIC_DEFAULT_FABLE_MODEL: externalModel,
84
+ ANTHROPIC_DEFAULT_HAIKU_MODEL: definition?.id || "",
85
+ CLAUDE_CODE_SUBAGENT_MODEL: definition?.id || "",
86
+ CLAUDE_CODE_AUTO_COMPACT_WINDOW: provider ? String(provider.claudeAutoCompactWindow) : "",
87
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: provider ? "1" : "",
88
+ CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST: "1"
89
+ };
90
+ return { model, env };
91
+ }
92
+
93
+ export async function verifyClaudeProviderKey(provider, apiKey, fetchImpl) {
94
+ let response;
95
+ try {
96
+ response = await fetchImpl(`${provider.claudeBaseUrl}/v1/messages`, {
97
+ method: "POST", redirect: "error", signal: AbortSignal.timeout(30_000),
98
+ headers: { Authorization: `Bearer ${apiKey}`, "anthropic-version": "2023-06-01", "Content-Type": "application/json" },
99
+ body: JSON.stringify({ model: provider.models[0].id, max_tokens: 64,
100
+ messages: [{ role: "user", content: "Reply with OK." }] })
101
+ });
102
+ } catch {
103
+ throw new Error(`${provider.label} could not be reached through Claude Code. Your previous connection is unchanged; try again.`);
104
+ }
105
+ if (!response.ok) {
106
+ await response.body?.cancel?.();
107
+ throw new Error(response.status === 401 || response.status === 403
108
+ ? `${provider.label} rejected this key. Check the key and the required account plan.`
109
+ : `${provider.label} could not complete a Claude Code test request (HTTP ${response.status}). Check your plan or API credit.`);
110
+ }
111
+ const result = await response.json().catch(() => null);
112
+ if (!result?.id || result.type !== "message" || result.error || !Array.isArray(result.content)) {
113
+ throw new Error(`${provider.label} did not return a usable Messages API result. The key was not saved.`);
114
+ }
115
+ }
116
+
117
+ export function claudeFlagSettings({ effort = "", providerEnv, hooks, commandHook } = {}) {
118
+ const nativeHooks = commandHook ? {
119
+ PreToolUse: [{ matcher: "Bash", hooks: [{ type: "command", timeout: commandHook.timeout, command: commandHook.command }] }]
120
+ } : hooks;
121
+ return { fallbackModel: [], effortLevel: effort || null,
122
+ ...(nativeHooks ? { hooks: nativeHooks } : {}),
123
+ ...(providerEnv ? { env: providerEnv } : {}) };
124
+ }
125
+
126
+ export function claudeCodeArguments({
127
+ sessionId, resume = false, model = "", effort = "", toolFree = false, applicationTools = false,
128
+ outputSchema, terminal = false, instructionArguments = [], permissionMode, isolated = false,
129
+ settings = { fallbackModel: [] }
130
+ } = {}) {
131
+ const args = terminal ? [] : ["--print", "--input-format", "stream-json", "--output-format", "stream-json",
132
+ "--verbose", "--include-partial-messages", "--replay-user-messages", "--thinking-display", "summarized"];
133
+ if (sessionId) args.push(resume ? "--resume" : "--session-id", sessionId);
134
+ args.push(...instructionArguments);
135
+ if (model) args.push("--model", model);
136
+ if (effort) args.push("--effort", effort);
137
+ if (toolFree) {
138
+ args.push("--restricted", "--tools", "", "--settings", '{"fallbackModel":[]}');
139
+ if (applicationTools) args.push("--allowedTools", `mcp__${CLAUDE_APPLICATION_TOOL_SERVER}__*`, "--permission-mode", "dontAsk");
140
+ } else {
141
+ if (permissionMode) args.push("--permission-mode", permissionMode);
142
+ args.push("--settings", JSON.stringify(settings));
143
+ }
144
+ if (toolFree || isolated) {
145
+ args.push("--safe-mode", "--strict-mcp-config", "--mcp-config", JSON.stringify({ mcpServers: applicationTools ? {
146
+ [CLAUDE_APPLICATION_TOOL_SERVER]: { type: "sdk", name: CLAUDE_APPLICATION_TOOL_SERVER }
147
+ } : {} }));
148
+ if (!applicationTools) args.push("--disallowedTools", "mcp__*");
149
+ if (!toolFree) args.push("--tools", "Bash,Read,Edit,Write,Glob,Grep,NotebookEdit");
150
+ }
151
+ if (outputSchema) args.push("--json-schema", JSON.stringify(outputSchema));
152
+ return args;
153
+ }
154
+
155
+ export async function stopClaudeCodeProcess({ process: native, executionId, stopExecution }) {
156
+ let proof = { scopeEmpty: true };
157
+ if (native) {
158
+ proof = await native.stop();
159
+ } else if (executionId) {
160
+ proof = await stopExecution(executionId, {
161
+ allowMissingRecordScopeRecovery: true,
162
+ reason: "claude-code-stop"
163
+ });
164
+ }
165
+ if (proof.scopeEmpty !== true && proof.exited !== true) {
166
+ throw Object.assign(new Error("Claude process cleanup could not be confirmed. Restore its execution host before continuing."), {
167
+ code: "claude_stop_unconfirmed", stopProof: proof
168
+ });
169
+ }
170
+ return { ...proof, exited: true };
171
+ }
172
+
173
+ export async function bindClaudeConversationAccount(entry, { identity, providerId, stop, save }) {
174
+ entry.accountIdentities ||= {};
175
+ if (!nativeAiProvider(providerId) && entry.accountIdentities[providerId] && entry.accountIdentities[providerId] !== identity) {
176
+ await stop(entry, "The signed-in Claude account changed.");
177
+ throw Object.assign(new Error("This conversation belongs to another Claude account. Reconnect that account or start a new session."), {
178
+ code: "claude_account_changed"
179
+ });
180
+ }
181
+ if (entry.accountIdentity !== identity) {
182
+ entry.accountIdentity = identity;
183
+ entry.accountIdentities[providerId] = identity;
184
+ await save(entry);
185
+ }
186
+ }
187
+
188
+ export function claudePlanUsage(value) {
189
+ const windows = Object.entries(value.rate_limits || {}).flatMap(([id, window]) => {
190
+ if (!window || !Number.isFinite(window.utilization) || !/^(five_hour|seven_day(?:_.*)?)$/u.test(id)) return [];
191
+ const reset = window.resets_at ? Date.parse(window.resets_at) / 1000 : null;
192
+ if (reset !== null && (!Number.isFinite(reset) || reset * 1000 <= Date.now())) return [];
193
+ return [{ id, remainingPercent: Math.max(0, Math.min(100, 100 - window.utilization)),
194
+ windowDurationMins: id === "five_hour" ? 300 : 10080, resetsAt: reset }];
195
+ });
196
+ return { status: !value.rate_limits_available ? "unsupported" : windows.length ? "available" : "unavailable",
197
+ windows, checkedAt: Date.now() };
198
+ }
199
+
200
+ export function claudeCatalogueModels(initialization) {
201
+ return (initialization.models || []).map((model) => ({
202
+ id: model.value, label: model.displayName || model.value, description: model.description || "",
203
+ status: "available", variants: (model.supportedEffortLevels || []).map((id) => ({
204
+ id, label: id[0].toUpperCase() + id.slice(1)
205
+ }))
206
+ }));
207
+ }
208
+
209
+ /** Native account queries share process reuse, caches and retryable cleanup. */
210
+ export function createClaudeAccountQueries({ createProcess, accountIdentity, processes = () => [], isClosing = () => false }) {
211
+ const error = message => Object.assign(new Error(message), { code: "claude_account_query_failed" });
212
+ const accountProcessStops = new Set();
213
+ let catalog;
214
+ let catalogStart;
215
+ let planUsage;
216
+ let planUsagePending;
217
+
218
+ async function stopAccountProcess(native) {
219
+ accountProcessStops.add(native);
220
+ if (!(await native.stop()).scopeEmpty) throw error("Claude account query process cleanup could not be confirmed.");
221
+ accountProcessStops.delete(native);
222
+ }
223
+
224
+ async function readAccountProcess(identity, read) {
225
+ if (isClosing()) throw error("Claude is reconnecting.");
226
+ for (const native of accountProcessStops) await stopAccountProcess(native);
227
+ const owned = [...processes()].find((entry) =>
228
+ entry.process && !entry.stopping && entry.accountIdentity === identity);
229
+ if (owned) return read(owned.process);
230
+ const native = await createProcess();
231
+ try {
232
+ return await read(native);
233
+ } finally {
234
+ await stopAccountProcess(native);
235
+ }
236
+ }
237
+
238
+ return Object.freeze({
239
+ async readCatalogue(context) {
240
+ if (isClosing()) throw error("Claude is reconnecting.");
241
+ const identity = await accountIdentity(context);
242
+ if (!catalog || catalog.identity !== identity || Date.now() - catalog.at > 600_000) {
243
+ catalogStart ||= (async () => {
244
+ const value = await readAccountProcess(identity, (native) => native.initialization);
245
+ if (identity !== await accountIdentity(context)) throw error("The signed-in Claude account changed. Refresh the model list.");
246
+ catalog = { at: Date.now(), identity, value };
247
+ })().finally(() => { catalogStart = null; });
248
+ await catalogStart;
249
+ }
250
+ return catalog.value;
251
+ },
252
+ async readPlanUsage(context) {
253
+ const identity = await accountIdentity(context);
254
+ if (planUsage?.identity === identity && Date.now() - planUsage.checkedAt < 60_000) return planUsage;
255
+ if (planUsagePending) return planUsagePending;
256
+ planUsagePending = (async () => {
257
+ const value = await readAccountProcess(identity, (native) => native.client.request(
258
+ { subtype: "get_usage", skip_behaviors: true }));
259
+ if (identity !== await accountIdentity(context)) return { status: "unavailable", windows: [] };
260
+ planUsage = { ...claudePlanUsage(value), identity };
261
+ return planUsage;
262
+ })().finally(() => { planUsagePending = null; });
263
+ return planUsagePending;
264
+ },
265
+ invalidateUsage() { planUsage = null; },
266
+ async invalidate() {
267
+ await Promise.allSettled([catalogStart, planUsagePending]);
268
+ catalog = null;
269
+ planUsage = null;
270
+ for (const native of accountProcessStops) await stopAccountProcess(native);
271
+ }
272
+ });
273
+ }
274
+
275
+ /** Native startup and handshake; an execution host owns processes and their identity. */
276
+ export async function createClaudeCodeProcess({
277
+ command = "claude", execution = createLocalConversationExecution(), env = process.env,
278
+ workdir, onEvent, onControlRequest, onFailure, onStarted, executionLimits, signal, commandWrapper, ...options
279
+ } = {}) {
280
+ if (commandWrapper !== undefined) validateCommandWrapper(commandWrapper);
281
+ const environment = { ...env, DISABLE_AUTOUPDATER: "1" };
282
+ for (const name of ["DBUS_SESSION_BUS_ADDRESS", "DBUS_STARTER_ADDRESS", "DBUS_STARTER_BUS_TYPE",
283
+ "CLAUDECODE", "CLAUDE_CODE_ENTRYPOINT"]) delete environment[name];
284
+ let native;
285
+ let client;
286
+ let stopped = false;
287
+ let stopping;
288
+ const abort = () => { void stop().catch(() => {}); };
289
+ async function stop() {
290
+ if (stopped) return { exited: true, scopeEmpty: true };
291
+ if (stopping) return stopping;
292
+ stopping = (async () => {
293
+ client?.close();
294
+ const proof = native ? await execution.stop(native.id) : { scopeEmpty: true };
295
+ stopped = proof?.scopeEmpty === true;
296
+ return { ...proof, exited: stopped };
297
+ })().finally(() => { stopping = null; });
298
+ return stopping;
299
+ }
300
+ try {
301
+ signal?.throwIfAborted();
302
+ native = await execution.start({ command, args: claudeCodeArguments(options), cwd: workdir,
303
+ env: environment, stream: true, limits: executionLimits });
304
+ await onStarted?.(native.id);
305
+ signal?.throwIfAborted();
306
+ client = createClaudeJsonClient({ stream: Duplex.from({ readable: native.stdout, writable: native.stdin }), onEvent, onFailure,
307
+ onControlRequest(request, context) {
308
+ if (request.subtype === "hook_callback" && commandWrapper && request.callback_id === "jskit-command-wrapper") {
309
+ return nativeCommandHook(commandWrapper, request.input);
310
+ }
311
+ if (!onControlRequest) throw new Error("This Claude control request has no owner.");
312
+ return onControlRequest(request, context);
313
+ }
314
+ });
315
+ signal?.addEventListener("abort", abort, { once: true });
316
+ const initialization = await client.initialize(commandWrapper ? { hooks: {
317
+ PreToolUse: [{ matcher: "^Bash$", hookCallbackIds: ["jskit-command-wrapper"], timeout: 30 }]
318
+ } } : {});
319
+ return { client, initialization, executionId: native.id, stop };
320
+ } catch (error) {
321
+ const proof = await stop();
322
+ error.stopProof = proof;
323
+ throw error;
324
+ } finally {
325
+ signal?.removeEventListener("abort", abort);
326
+ }
327
+ }
@@ -0,0 +1,42 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+
3
+ export const CLAUDE_APPLICATION_TOOL_SERVER = "application";
4
+
5
+ // The CLI carries this SDK MCP exchange over its existing control pipe. There
6
+ // is no listening socket or separate tool process. The shared executor owns effects.
7
+ export async function claudeApplicationToolResponse(request, { schemas, turn, signal }) {
8
+ if (request?.subtype !== "mcp_message" || request.server_name !== CLAUDE_APPLICATION_TOOL_SERVER ||
9
+ request.message?.jsonrpc !== "2.0") throw new Error("This Claude control request is not an application tool exchange.");
10
+ const message = request.message;
11
+ signal.throwIfAborted();
12
+ let result;
13
+ switch (message.method) {
14
+ case "initialize":
15
+ result = { protocolVersion: "2025-11-25", capabilities: { tools: {} },
16
+ serverInfo: { name: CLAUDE_APPLICATION_TOOL_SERVER, version: "1.0.0" } };
17
+ break;
18
+ case "notifications/initialized":
19
+ case "ping":
20
+ result = {};
21
+ break;
22
+ case "tools/list":
23
+ result = { tools: schemas.map(({ function: tool }) => ({ name: tool.name, description: tool.description,
24
+ inputSchema: tool.parameters })) };
25
+ break;
26
+ case "tools/call": {
27
+ const params = message.params;
28
+ const id = params?._meta?.["claudecode/toolUseId"];
29
+ const native = turn?.nativeTools.get(id);
30
+ if (!turn?.admitted || !turn.tools || !native || native.name !== `mcp__${CLAUDE_APPLICATION_TOOL_SERVER}__${params.name}` ||
31
+ !isDeepStrictEqual(native.input, params.arguments || {})) {
32
+ throw new Error("Claude's application call does not match an admitted native tool use. Update Claude if its tool identity is unavailable.");
33
+ }
34
+ const value = await turn.tools.execute({ id, name: params.name, arguments: JSON.stringify(params.arguments || {}) }, { signal, messageId: native.messageId });
35
+ result = { content: [{ type: "text", text: JSON.stringify(value) }], isError: value.ok === false };
36
+ break;
37
+ }
38
+ default:
39
+ throw new Error("Unsupported Claude application tool protocol method.");
40
+ }
41
+ return { mcp_response: { jsonrpc: "2.0", ...(message.id === undefined ? {} : { id: message.id }), result } };
42
+ }