@cjhyy/code-shell-core 0.9.5 → 0.9.6

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 (48) hide show
  1. package/dist/credentials/access.d.ts +9 -1
  2. package/dist/credentials/access.js +15 -4
  3. package/dist/credentials/store.d.ts +11 -0
  4. package/dist/credentials/store.js +44 -9
  5. package/dist/credentials/types.d.ts +4 -0
  6. package/dist/credentials/types.js +4 -0
  7. package/dist/credentials/use-credential-tool.js +12 -2
  8. package/dist/engine/engine-workspace-authority.js +10 -3
  9. package/dist/engine/engine.d.ts +3 -0
  10. package/dist/engine/engine.js +38 -6
  11. package/dist/engine/prompt-cache-diagnostics.js +10 -1
  12. package/dist/engine/run-goal.js +7 -3
  13. package/dist/engine/run-types.d.ts +16 -0
  14. package/dist/engine/run-workspace.js +5 -21
  15. package/dist/engine/turn-loop.d.ts +4 -4
  16. package/dist/engine/turn-loop.js +29 -9
  17. package/dist/index.d.ts +4 -4
  18. package/dist/index.js +2 -2
  19. package/dist/links/index.d.ts +1 -0
  20. package/dist/links/index.js +1 -0
  21. package/dist/links/link-action-tool.d.ts +2 -1
  22. package/dist/links/link-action-tool.js +80 -44
  23. package/dist/links/status.d.ts +53 -0
  24. package/dist/links/status.js +175 -0
  25. package/dist/llm/prompt-cache.d.ts +35 -3
  26. package/dist/llm/prompt-cache.js +63 -3
  27. package/dist/llm/providers/openai.d.ts +3 -0
  28. package/dist/llm/providers/openai.js +57 -14
  29. package/dist/protocol/background-result-wakeup.d.ts +8 -1
  30. package/dist/protocol/background-result-wakeup.js +76 -38
  31. package/dist/protocol/chat-session-manager.d.ts +7 -1
  32. package/dist/protocol/chat-session-manager.js +44 -8
  33. package/dist/protocol/chat-session.d.ts +2 -0
  34. package/dist/protocol/chat-session.js +4 -1
  35. package/dist/protocol/server.d.ts +3 -0
  36. package/dist/protocol/server.js +148 -43
  37. package/dist/protocol/session-message-result.d.ts +12 -0
  38. package/dist/protocol/session-message-result.js +42 -0
  39. package/dist/protocol/session-message-workspace.d.ts +21 -0
  40. package/dist/protocol/session-message-workspace.js +57 -0
  41. package/dist/protocol/types.d.ts +2 -0
  42. package/dist/session/session-message.d.ts +17 -2
  43. package/dist/tool-system/browser-bridge.d.ts +18 -0
  44. package/dist/tool-system/builtin/agent.js +23 -9
  45. package/dist/tool-system/builtin/browser-tools.js +18 -1
  46. package/dist/tool-system/builtin/index.js +3 -3
  47. package/dist/tool-system/builtin/send-message-to-session.js +19 -3
  48. package/package.json +1 -1
package/dist/index.d.ts CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export declare const VERSION = "0.9.5";
6
+ export declare const VERSION = "0.9.6";
7
7
  export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionKind, SessionWorkspace, SessionProjectBinding, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, TurnCompletionKind, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
8
8
  export type { GoalConfig, GoalLifecycleConfig, GoalLifecyclePhase, GoalLifecycleTerminalReason, GoalLifecycleV1, } from "./goal/lifecycle.js";
9
9
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, CompositionError, SandboxUnavailableError, } from "./exceptions.js";
@@ -71,7 +71,7 @@ export { latestUndoTarget, earliestSnapshotsPerFile, latestTurnUndoTargets, late
71
71
  export { diffLines, renderDiffPreview, type DiffLine } from "./session/simple-diff.js";
72
72
  export { MemoryManager } from "./session/memory.js";
73
73
  export type { MemoryEntry, MemoryOrigin, MemoryScope } from "./session/memory.js";
74
- export type { RouteSessionMessageInput, SessionMessageRouter, SessionMessageTarget, SessionMessageToolService, } from "./session/session-message.js";
74
+ export type { RouteSessionMessageInput, SessionMessageRouter, SessionMessageReceipt, SessionMessageTarget, SessionMessageToolService, } from "./session/session-message.js";
75
75
  export { runDreamConsolidation, type DreamConsolidationInput, type DreamConsolidationResult, } from "./services/dream-consolidation.js";
76
76
  export { authorize, refreshToken, generatePKCE, createHardenedOAuthFetch, type OAuthConfig, type OAuthTokens, type OAuthAuthorizeOptions, type OAuthRefreshOptions, type HardenedOAuthFetchOptions, } from "./services/oauth.js";
77
77
  export { PromptComposer } from "./prompt/composer.js";
@@ -118,11 +118,11 @@ export { SettingsSchema, validateSettings } from "./settings/schema.js";
118
118
  export { settingsJsonSchema, writeSettingsSchemaFile } from "./settings/schema-export.js";
119
119
  export { personalizationFrom, type PersonalizationConfig } from "./settings/personalization.js";
120
120
  export { CredentialStore, type CredentialScope, type MaskedCredential, type Credential, type CredentialType, type CredentialStoreFile, type OAuthCredentialPublicStatus, type OAuthCredentialSecret, type OAuthTokenResponse, buildOAuthRefreshRequest, isOAuthAccessTokenExpired, isBrowserOAuthLinkCredential, oauthCredentialStatus, parseOAuthCredentialSecret, resolveLinkCredentialAccessToken, mergeOAuthTokenResponse, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, type OAuthClockOptions, type OAuthRefreshHandler, type OAuthRefreshRequest, formatNetscapeCookies, parseCookieJar, type CookieLike, useCredentialToolDef, useCredentialToolDefFor, sweepStaleCredentialCookies, getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, credentialAllowsEnvExposure, credentialSecretHint, isCredentialSecretAvailable, materializeCookieSecret, type CredentialAccess, type CredentialAccessScope, type CredentialMetadata, type CredentialSnapshot, type CredentialSnapshotEntry, type EncryptionCipher, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./credentials/index.js";
121
- export { LOCAL_LINK_PROVIDERS, getLocalLinkProvider, listLocalLinkProviders, validateLocalLinkToken, connectCliLink, executeCliLinkAction, getCliLinkStatus, isCliLinkProvider, managedLinkCliPath, resolveLinkCliExecutable, runCliLinkCommand, linkActionToolDef, linkActionTool, isLinkActionAvailable, type LinkActionRisk, type LocalLinkIdentity, type LocalLinkValidationResult, type LocalLinkActionSummary, type LocalLinkProviderSummary, type LocalLinkActionContext, type LocalLinkActionSpec, type LocalLinkProviderSpec, type CliLinkCommandResult, type CliLinkCommandRunner, type CliLinkProviderId, type CliLinkRunOptions, type CliLinkStatus, } from "./links/index.js";
121
+ export { LOCAL_LINK_PROVIDERS, getLocalLinkProvider, listLocalLinkProviders, validateLocalLinkToken, connectCliLink, executeCliLinkAction, getCliLinkStatus, isCliLinkProvider, managedLinkCliPath, resolveLinkCliExecutable, runCliLinkCommand, linkActionToolDef, linkActionTool, isLinkActionAvailable, getLinkStatus, type LinkStatusOptions, type LinkStatusDependencies, type LinkConnectionStatus, type LinkCliStatus, type LinkProviderStatus, type LinkStatusResult, type LinkActionRisk, type LocalLinkIdentity, type LocalLinkValidationResult, type LocalLinkActionSummary, type LocalLinkProviderSummary, type LocalLinkActionContext, type LocalLinkActionSpec, type LocalLinkProviderSpec, type CliLinkCommandResult, type CliLinkCommandRunner, type CliLinkProviderId, type CliLinkRunOptions, type CliLinkStatus, } from "./links/index.js";
122
122
  export { CostTracker, costTracker, installCostTracking } from "./cost-tracker.js";
123
123
  export { NOOP_COLORIZER, type Colorizer } from "./colorizer.js";
124
124
  export { expandPluginCommandBody, describePluginCommands, scanPluginCommands, MAX_PLUGIN_COMMAND_FILE_BYTES, MAX_PLUGIN_COMMAND_ARGUMENT_CHARS, MAX_PLUGIN_COMMAND_EXPANDED_CHARS, type PluginCommand, type PluginCommandDescriptor, } from "./plugins/pluginCommandsLoader.js";
125
125
  export { syncOpenRouterCatalog, getOpenRouterSnapshot } from "./data/openrouter-sync.js";
126
126
  export { flattenAxTree, renderElementList, cleanPageText, CONTENT_CHAR_CAP, buildExtractLinksScript, EXTRACT_LINK_CAP, } from "./tool-system/browser-bridge.js";
127
- export type { BrowserBridge, BrowserElement, BrowserSnapshot, BrowserResult, BrowserResultCode, BrowserScrollState, BrowserReadOptions, BrowserContent, BrowserExtract, BrowserLink, BrowserImage, BrowserVideo, BrowserImageData, BrowserTab, AXNode, } from "./tool-system/browser-bridge.js";
127
+ export type { BrowserBridge, BrowserElement, BrowserIdentity, BrowserSnapshot, BrowserResult, BrowserResultCode, BrowserScrollState, BrowserReadOptions, BrowserContent, BrowserExtract, BrowserLink, BrowserImage, BrowserVideo, BrowserImageData, BrowserTab, AXNode, } from "./tool-system/browser-bridge.js";
128
128
  export type { WorkspaceBridge } from "./tool-system/workspace-bridge.js";
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export const VERSION = "0.9.5";
6
+ export const VERSION = "0.9.6";
7
7
  // ─── Exceptions ──────────────────────────────────────────────────
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, CompositionError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  // ─── Composition (AgentModule / ResolvedComposition) ─────────────
@@ -122,7 +122,7 @@ export { SettingsSchema, validateSettings } from "./settings/schema.js";
122
122
  export { settingsJsonSchema, writeSettingsSchemaFile } from "./settings/schema-export.js";
123
123
  export { personalizationFrom } from "./settings/personalization.js";
124
124
  export { CredentialStore, buildOAuthRefreshRequest, isOAuthAccessTokenExpired, isBrowserOAuthLinkCredential, oauthCredentialStatus, parseOAuthCredentialSecret, resolveLinkCredentialAccessToken, mergeOAuthTokenResponse, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, formatNetscapeCookies, parseCookieJar, useCredentialToolDef, useCredentialToolDefFor, sweepStaleCredentialCookies, getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, credentialAllowsEnvExposure, credentialSecretHint, isCredentialSecretAvailable, materializeCookieSecret, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./credentials/index.js";
125
- export { LOCAL_LINK_PROVIDERS, getLocalLinkProvider, listLocalLinkProviders, validateLocalLinkToken, connectCliLink, executeCliLinkAction, getCliLinkStatus, isCliLinkProvider, managedLinkCliPath, resolveLinkCliExecutable, runCliLinkCommand, linkActionToolDef, linkActionTool, isLinkActionAvailable, } from "./links/index.js";
125
+ export { LOCAL_LINK_PROVIDERS, getLocalLinkProvider, listLocalLinkProviders, validateLocalLinkToken, connectCliLink, executeCliLinkAction, getCliLinkStatus, isCliLinkProvider, managedLinkCliPath, resolveLinkCliExecutable, runCliLinkCommand, linkActionToolDef, linkActionTool, isLinkActionAvailable, getLinkStatus, } from "./links/index.js";
126
126
  // ─── Cost Tracker ────────────────────────────────────────────────
127
127
  export { CostTracker, costTracker, installCostTracking } from "./cost-tracker.js";
128
128
  export { NOOP_COLORIZER } from "./colorizer.js";
@@ -1,4 +1,5 @@
1
1
  export { LOCAL_LINK_PROVIDERS, getLocalLinkProvider, listLocalLinkProviders, validateLocalLinkToken, } from "./providers.js";
2
2
  export { linkActionToolDef, linkActionTool, isLinkActionAvailable } from "./link-action-tool.js";
3
+ export { getLinkStatus, type LinkStatusOptions, type LinkStatusDependencies, type LinkConnectionStatus, type LinkCliStatus, type LinkProviderStatus, type LinkStatusResult, } from "./status.js";
3
4
  export { assertCliLinkAccount, connectCliLink, executeCliLinkAction, getCliLinkStatus, isCliLinkProvider, managedLinkCliPath, resolveLinkCliExecutable, runCliLinkCommand, type CliLinkCommandResult, type CliLinkCommandRunner, type CliLinkProviderId, type CliLinkRunOptions, type CliLinkStatus, } from "./cli.js";
4
5
  export type { LinkActionRisk, LocalLinkIdentity, LocalLinkValidationResult, LocalLinkActionSummary, LocalLinkProviderSummary, LocalLinkActionContext, LocalLinkActionSpec, LocalLinkProviderSpec, } from "./types.js";
@@ -1,3 +1,4 @@
1
1
  export { LOCAL_LINK_PROVIDERS, getLocalLinkProvider, listLocalLinkProviders, validateLocalLinkToken, } from "./providers.js";
2
2
  export { linkActionToolDef, linkActionTool, isLinkActionAvailable } from "./link-action-tool.js";
3
+ export { getLinkStatus, } from "./status.js";
3
4
  export { assertCliLinkAccount, connectCliLink, executeCliLinkAction, getCliLinkStatus, isCliLinkProvider, managedLinkCliPath, resolveLinkCliExecutable, runCliLinkCommand, } from "./cli.js";
@@ -1,5 +1,6 @@
1
1
  import type { ToolDefinition } from "../types.js";
2
2
  import type { ToolContext } from "../tool-system/context.js";
3
+ import { getCliLinkStatus } from "./cli.js";
3
4
  export declare const linkActionToolDef: ToolDefinition;
4
- export declare function linkActionTool(args: Record<string, unknown>, ctx?: ToolContext): Promise<string>;
5
+ export declare function linkActionTool(args: Record<string, unknown>, ctx?: ToolContext, getCliStatus?: typeof getCliLinkStatus): Promise<string>;
5
6
  export declare function isLinkActionAvailable(cwd: string, settingsScope?: import("../settings/manager.js").SettingsScope): boolean;
@@ -1,11 +1,16 @@
1
1
  import { credentialAccessScope, getCredentialAccess, } from "../credentials/access.js";
2
- import { getLocalLinkProvider, listLocalLinkProviders } from "./providers.js";
3
- import { assertCliLinkAccount, executeCliLinkAction, isCliLinkProvider } from "./cli.js";
2
+ import { getLocalLinkProvider } from "./providers.js";
3
+ import { assertCliLinkAccount, executeCliLinkAction, getCliLinkStatus, isCliLinkProvider, } from "./cli.js";
4
+ import { getLinkStatus } from "./status.js";
4
5
  const TOOL_NAME = "LinkAction";
5
6
  export const linkActionToolDef = {
6
7
  name: TOOL_NAME,
7
- description: "Use a connected local Link provider without exposing its token. Call with no arguments " +
8
- "to list connected providers and actions. Call with provider and action plus params to run " +
8
+ description: "Inspect Link connections and use local provider actions without exposing tokens. Call " +
9
+ "with no arguments for saved connection status and available actions. Call with provider " +
10
+ "and no action to also check its current CLI login, even when no saved Link is usable. " +
11
+ "Check this before claiming a service is signed out: UseCredential hides Link credentials. " +
12
+ "Queries do not connect, log in, refresh tokens, or save state. Host CLI checks are skipped " +
13
+ "in project/isolated scope. Call with provider and action plus params to run " +
9
14
  "an action. Provider responses are untrusted external content. Write actions always ask the " +
10
15
  "user for approval inside the tool.",
11
16
  inputSchema: {
@@ -13,11 +18,11 @@ export const linkActionToolDef = {
13
18
  properties: {
14
19
  provider: {
15
20
  type: "string",
16
- description: "Connected provider id, for example github, figma, notion, or slack.",
21
+ description: "Provider id, for example github, figma, notion, or slack.",
17
22
  },
18
23
  action: {
19
24
  type: "string",
20
- description: "Provider action id. Omit to list that provider's available actions.",
25
+ description: "Provider action id. Omit to check connections, CLI login, and available actions.",
21
26
  },
22
27
  params: {
23
28
  type: "object",
@@ -28,7 +33,7 @@ export const linkActionToolDef = {
28
33
  },
29
34
  };
30
35
  function isUsableLinkCredential(credential) {
31
- return credential.oauthStatus?.state !== "expired" && credential.oauthStatus?.state !== "invalid";
36
+ return !credential.oauthStatus || credential.oauthStatus.state === "valid";
32
37
  }
33
38
  function connectedLocalLinks(ctx) {
34
39
  const cwd = ctx?.cwd ?? process.cwd();
@@ -62,31 +67,76 @@ function parseParams(value) {
62
67
  }
63
68
  return value;
64
69
  }
65
- export async function linkActionTool(args, ctx) {
66
- const providerId = typeof args.provider === "string" ? args.provider.trim() : "";
67
- const actionId = typeof args.action === "string" ? args.action.trim() : "";
68
- const links = connectedLocalLinks(ctx);
69
- if (!providerId) {
70
- const summaries = listLocalLinkProviders();
70
+ async function inspectConnections(providerId, ctx, getCliStatus) {
71
+ try {
72
+ const status = await getLinkStatus({
73
+ provider: providerId || undefined,
74
+ probeCli: Boolean(providerId),
75
+ cwd: ctx?.cwd,
76
+ settingsScope: ctx?.settingsScope,
77
+ signal: ctx?.signal,
78
+ }, { getCliStatus });
79
+ let links = [];
80
+ if (status.credentialStore.state === "checked") {
81
+ try {
82
+ links = connectedLocalLinks(ctx);
83
+ }
84
+ catch {
85
+ // A credential store change must not discard an independently checked CLI login.
86
+ }
87
+ }
88
+ const providers = status.providers.map((provider) => {
89
+ const connection = newestConnection(links.filter((candidate) => candidate.providerId === provider.id &&
90
+ provider.connections.some((saved) => saved.id === candidate.credential.id && saved.state === "ready")));
91
+ const capabilities = connection?.credential.meta?.linkCapabilityIds;
92
+ return {
93
+ ...provider,
94
+ cliSupported: isCliLinkProvider(provider.id),
95
+ account: connection?.credential.meta?.linkAccountLabel,
96
+ verifiedAt: connection?.credential.meta?.linkLastVerifiedAt,
97
+ actions: connection
98
+ ? getLocalLinkProvider(provider.id)
99
+ .actions.filter((action) => !capabilities?.length || capabilities.includes(`${provider.id}.${action.id}`))
100
+ .map(({ id, title, description, risk }) => ({ id, title, description, risk }))
101
+ : [],
102
+ };
103
+ });
104
+ const metadata = {
105
+ checkedAt: status.checkedAt,
106
+ credentialStore: status.credentialStore,
107
+ notice: status.guidance,
108
+ };
109
+ return JSON.stringify(providerId
110
+ ? { kind: "provider_actions", provider: providerId, ...providers[0], ...metadata }
111
+ : { kind: "providers", runtimePreference: "local-first", providers, ...metadata });
112
+ }
113
+ catch (error) {
114
+ if (ctx?.signal?.aborted || (error instanceof Error && error.name === "AbortError")) {
115
+ return JSON.stringify({ kind: "cancelled", message: "Link status check cancelled." });
116
+ }
71
117
  return JSON.stringify({
72
- kind: "connected_providers",
73
- runtimePreference: "local-first",
74
- providers: summaries.flatMap((provider) => {
75
- const connection = newestConnection(links.filter((candidate) => candidate.providerId === provider.id));
76
- if (!connection)
77
- return [];
78
- return [
79
- {
80
- id: provider.id,
81
- name: provider.displayName,
82
- account: connection.credential.meta?.linkAccountLabel,
83
- verifiedAt: connection.credential.meta?.linkLastVerifiedAt,
84
- actions: provider.actions,
85
- },
86
- ];
87
- }),
118
+ kind: "error",
119
+ error: error instanceof Error && error.message.startsWith("Unknown local Link provider:")
120
+ ? error.message
121
+ : "Link status check failed.",
88
122
  });
89
123
  }
124
+ }
125
+ export async function linkActionTool(args, ctx, getCliStatus = getCliLinkStatus) {
126
+ if (args.provider !== undefined && typeof args.provider !== "string") {
127
+ return JSON.stringify({ kind: "error", error: "LinkAction provider must be a string." });
128
+ }
129
+ if (args.action !== undefined && typeof args.action !== "string") {
130
+ return JSON.stringify({ kind: "error", error: "LinkAction action must be a string." });
131
+ }
132
+ const providerId = typeof args.provider === "string" ? args.provider.trim().toLowerCase() : "";
133
+ const actionId = typeof args.action === "string" ? args.action.trim() : "";
134
+ if (!actionId)
135
+ return inspectConnections(providerId, ctx, getCliStatus);
136
+ if (!providerId) {
137
+ return JSON.stringify({ kind: "error", error: "Provider is required to run an action." });
138
+ }
139
+ const links = connectedLocalLinks(ctx);
90
140
  const provider = getLocalLinkProvider(providerId);
91
141
  if (!provider) {
92
142
  return JSON.stringify({ kind: "error", error: `Unknown local Link provider: ${providerId}` });
@@ -95,21 +145,7 @@ export async function linkActionTool(args, ctx) {
95
145
  if (!connection) {
96
146
  return JSON.stringify({
97
147
  kind: "error",
98
- error: `${provider.displayName} is not connected locally. Connect it in Credentials Link.`,
99
- });
100
- }
101
- if (!actionId) {
102
- return JSON.stringify({
103
- kind: "provider_actions",
104
- provider: provider.id,
105
- name: provider.displayName,
106
- account: connection.credential.meta?.linkAccountLabel,
107
- actions: provider.actions.map(({ id, title, description, risk }) => ({
108
- id,
109
- title,
110
- description,
111
- risk,
112
- })),
148
+ error: `No usable saved ${provider.displayName} Link is available. Query LinkAction with this provider and no action to inspect saved credentials and existing CLI login.`,
113
149
  });
114
150
  }
115
151
  const action = provider.actions.find((candidate) => candidate.id === actionId);
@@ -0,0 +1,53 @@
1
+ import { type CredentialAccess } from "../credentials/access.js";
2
+ import type { SettingsScope } from "../settings/manager.js";
3
+ import { getCliLinkStatus, type CliLinkStatus } from "./cli.js";
4
+ export interface LinkStatusOptions {
5
+ provider?: string;
6
+ cwd?: string;
7
+ settingsScope?: SettingsScope;
8
+ signal?: AbortSignal;
9
+ /** Check installed CLIs and their current account, without starting authorization. */
10
+ probeCli?: boolean;
11
+ }
12
+ export interface LinkStatusDependencies {
13
+ credentialAccess?: Pick<CredentialAccess, "listMasked" | "listMaskedWithStatus">;
14
+ getCliStatus?: typeof getCliLinkStatus;
15
+ }
16
+ export interface LinkConnectionStatus {
17
+ id: string;
18
+ account?: string;
19
+ backend: "http-token" | "cli" | "oauth";
20
+ runtime?: "local" | "server";
21
+ verifiedAt?: string;
22
+ /** Metadata readiness only; this does not revalidate a token or a CLI account binding. */
23
+ state: "ready" | "unavailable" | "expired" | "invalid";
24
+ reason?: string;
25
+ }
26
+ export type LinkCliStatus = (CliLinkStatus & {
27
+ state: "checked";
28
+ }) | {
29
+ state: "skipped";
30
+ reasonCode: "unsupported" | "scope" | "disabled";
31
+ reason: string;
32
+ } | {
33
+ state: "error";
34
+ reason: string;
35
+ };
36
+ export interface LinkProviderStatus {
37
+ id: string;
38
+ name: string;
39
+ connections: LinkConnectionStatus[];
40
+ cli: LinkCliStatus;
41
+ }
42
+ export interface LinkStatusResult {
43
+ kind: "link_status";
44
+ checkedAt: string;
45
+ credentialStore: {
46
+ state: "checked" | "error";
47
+ reason?: string;
48
+ };
49
+ providers: LinkProviderStatus[];
50
+ guidance: string;
51
+ }
52
+ /** Read stored connections and live CLI status without resolving, refreshing, or saving credentials. */
53
+ export declare function getLinkStatus(options?: LinkStatusOptions, dependencies?: LinkStatusDependencies): Promise<LinkStatusResult>;
@@ -0,0 +1,175 @@
1
+ import { credentialAccessScope, getCredentialAccess, } from "../credentials/access.js";
2
+ import { getCliLinkStatus, isCliLinkProvider } from "./cli.js";
3
+ import { listLocalLinkProviders } from "./providers.js";
4
+ const GUIDANCE = "Saved connection readiness describes stored metadata, not live token validity or a CLI " +
5
+ "account binding check. Check runtime: LinkAction handles registered local actions and " +
6
+ "rechecks access when used; server OAuth connections use their configured server/MCP tools. " +
7
+ "A CLI login can exist without " +
8
+ "a usable saved Link: use the provider CLI under normal tool permissions, or reconnect the " +
9
+ "Link in Credentials → Link. UseCredential omits Link credentials and cannot prove logout. " +
10
+ "A failed CLI check may be a network or service failure, not a signed-out account.";
11
+ // Do not return entire CredentialMetadata objects, even though they are called
12
+ // masked: they can contain secret hints and unrelated provider metadata.
13
+ function connectionStatus(credential) {
14
+ const meta = credential.meta;
15
+ const result = {
16
+ id: credential.id,
17
+ account: meta?.linkAccountLabel,
18
+ backend: meta?.linkExecutionBackend ?? (credential.type === "oauth" ? "oauth" : "http-token"),
19
+ runtime: meta?.linkExecutionRuntime ?? (credential.type === "oauth" ? "server" : undefined),
20
+ verifiedAt: meta?.linkLastVerifiedAt,
21
+ state: "ready",
22
+ };
23
+ if (!credential.hasSecret || credential.oauthStatus?.state === "missing") {
24
+ result.state = "unavailable";
25
+ result.reason = "Saved credential is missing or cannot be read. Reconnect this Link.";
26
+ }
27
+ else if (credential.oauthStatus?.state === "expired") {
28
+ result.state = "expired";
29
+ result.reason = "Saved authorization has expired. Reconnect this Link.";
30
+ }
31
+ else if (credential.oauthStatus?.state === "invalid") {
32
+ result.state = "invalid";
33
+ result.reason = "Saved authorization is invalid. Reconnect this Link.";
34
+ }
35
+ else if (!result.runtime) {
36
+ result.state = "unavailable";
37
+ result.reason = "Saved Link has no execution runtime. Reconnect it to enable local LinkAction.";
38
+ }
39
+ else if (meta?.linkExecutionBackend === "cli" && !meta.linkAccountId) {
40
+ result.state = "invalid";
41
+ result.reason = "Saved CLI connection has no account binding. Reconnect this Link.";
42
+ }
43
+ return result;
44
+ }
45
+ function credentialProvider(credential) {
46
+ if (credential.type !== "link" && credential.type !== "oauth")
47
+ return undefined;
48
+ // Match the Link page, including OAuth records saved before provider metadata
49
+ // was introduced. An arbitrary MCP credential is included only if its provider
50
+ // matches the supported Link catalog below.
51
+ return (credential.meta?.linkProvider ||
52
+ credential.meta?.oauthProvider ||
53
+ (credential.id.endsWith("-oauth") ? credential.id.slice(0, -"-oauth".length) : undefined));
54
+ }
55
+ /** CLI diagnostics may echo environment values or tokens. Return fixed reasons only. */
56
+ function cliFailureMessage(status) {
57
+ if (!status.installed)
58
+ return `${status.command} is not installed`;
59
+ const message = status.message ?? "";
60
+ if (/timed?\s*out|timeout|ETIMEDOUT/i.test(message)) {
61
+ return "CLI status check timed out; login state could not be confirmed.";
62
+ }
63
+ if (/ENOTFOUND|EAI_AGAIN|ECONN|network|DNS|socket|fetch failed|connection|TLS|certificate/i.test(message)) {
64
+ return "CLI status check could not reach the provider; login state could not be confirmed.";
65
+ }
66
+ if (/not (?:signed|logged) in|not authenticated|authentication|unauthorized|invalid.*token|\b401\b/i.test(message)) {
67
+ return "CLI authentication was not accepted. Check the CLI account or reconnect this Link.";
68
+ }
69
+ return "CLI login could not be confirmed. Check the provider CLI status for details.";
70
+ }
71
+ function throwIfCancelled(signal) {
72
+ if (signal?.aborted)
73
+ throw new DOMException("Link status check cancelled", "AbortError");
74
+ }
75
+ /** Read stored connections and live CLI status without resolving, refreshing, or saving credentials. */
76
+ export async function getLinkStatus(options = {}, dependencies = {}) {
77
+ throwIfCancelled(options.signal);
78
+ const providerId = options.provider?.trim().toLowerCase();
79
+ const catalog = listLocalLinkProviders();
80
+ if (providerId && !catalog.some((provider) => provider.id === providerId)) {
81
+ throw new Error(`Unknown local Link provider: ${providerId}`);
82
+ }
83
+ const selected = providerId ? catalog.filter((provider) => provider.id === providerId) : catalog;
84
+ let credentials = [];
85
+ const credentialStore = { state: "checked" };
86
+ try {
87
+ const access = dependencies.credentialAccess ?? getCredentialAccess();
88
+ const cwd = options.cwd ?? process.cwd();
89
+ const scope = credentialAccessScope(options.settingsScope);
90
+ if (access.listMaskedWithStatus) {
91
+ const snapshot = access.listMaskedWithStatus(cwd, scope);
92
+ credentials = snapshot.credentials;
93
+ if (!snapshot.readable)
94
+ credentialStore.state = "error";
95
+ }
96
+ else {
97
+ credentials = access.listMasked(cwd, scope);
98
+ }
99
+ }
100
+ catch {
101
+ credentialStore.state = "error";
102
+ }
103
+ if (credentialStore.state === "error") {
104
+ credentialStore.reason =
105
+ "Saved Link credentials could not be read; connection state is unknown.";
106
+ }
107
+ throwIfCancelled(options.signal);
108
+ const providers = new Array(selected.length);
109
+ let nextIndex = 0;
110
+ // Five providers currently have a CLI adapter. Bound subprocess/network work
111
+ // so an all-provider check cannot fan out without limit as the catalog grows.
112
+ await Promise.all(Array.from({ length: Math.min(3, selected.length) }, async () => {
113
+ while (nextIndex < selected.length) {
114
+ throwIfCancelled(options.signal);
115
+ const index = nextIndex++;
116
+ const provider = selected[index];
117
+ const connections = credentials
118
+ .filter((credential) => credentialProvider(credential) === provider.id)
119
+ .map(connectionStatus);
120
+ let cli;
121
+ if (!isCliLinkProvider(provider.id)) {
122
+ cli = {
123
+ state: "skipped",
124
+ reasonCode: "unsupported",
125
+ reason: "This provider has no supported CLI status check.",
126
+ };
127
+ }
128
+ else if (options.settingsScope && options.settingsScope !== "full") {
129
+ cli = {
130
+ state: "skipped",
131
+ reasonCode: "scope",
132
+ reason: "Host CLI login checks are unavailable in project or isolated settings scope.",
133
+ };
134
+ }
135
+ else if (options.probeCli === false) {
136
+ cli = {
137
+ state: "skipped",
138
+ reasonCode: "disabled",
139
+ reason: "CLI status check was disabled for this request.",
140
+ };
141
+ }
142
+ else {
143
+ try {
144
+ const status = await (dependencies.getCliStatus ?? getCliLinkStatus)(provider.id, {
145
+ cwd: options.cwd,
146
+ signal: options.signal,
147
+ });
148
+ throwIfCancelled(options.signal);
149
+ cli = {
150
+ state: "checked",
151
+ providerId: status.providerId,
152
+ command: status.command,
153
+ installed: status.installed,
154
+ authenticated: status.authenticated,
155
+ ...(status.authenticated && status.account ? { account: status.account } : {}),
156
+ ...(!status.authenticated ? { message: cliFailureMessage(status) } : {}),
157
+ };
158
+ }
159
+ catch {
160
+ throwIfCancelled(options.signal);
161
+ cli = { state: "error", reason: "CLI status check failed; login state is unknown." };
162
+ }
163
+ }
164
+ providers[index] = { id: provider.id, name: provider.displayName, connections, cli };
165
+ }
166
+ }));
167
+ throwIfCancelled(options.signal);
168
+ return {
169
+ kind: "link_status",
170
+ checkedAt: new Date().toISOString(),
171
+ credentialStore,
172
+ providers,
173
+ guidance: GUIDANCE,
174
+ };
175
+ }
@@ -5,7 +5,7 @@
5
5
  * prefix boundaries). Provider clients only translate the plan to their wire
6
6
  * format: OpenAI `prompt_cache_*` fields or Anthropic `cache_control` blocks.
7
7
  */
8
- export type PromptCacheStrategy = "openai-explicit" | "openai-implicit" | "anthropic-explicit" | "provider-managed";
8
+ export type PromptCacheStrategy = "openai-explicit" | "openai-hybrid" | "openai-implicit" | "anthropic-explicit" | "provider-managed";
9
9
  export type PromptCacheBreakpoint = "system" | "tools" | "stable-history" | "rolling-history";
10
10
  export interface PromptCacheRequestContext {
11
11
  /** Stable run/session namespace used for provider cache affinity. */
@@ -23,9 +23,9 @@ export interface PromptCachePolicy {
23
23
  breakpoints: readonly PromptCacheBreakpoint[];
24
24
  /** Opaque and <=64 chars, as required by OpenAI's prompt_cache_key. */
25
25
  cacheKey?: string;
26
- /** GPT-5.6+ explicit-cache request mode. */
26
+ /** GPT-5.6+ request mode; implicit also permits explicit read boundaries. */
27
27
  promptCacheOptions?: {
28
- mode: "explicit";
28
+ mode: "explicit" | "implicit";
29
29
  ttl: "30m";
30
30
  };
31
31
  }
@@ -46,3 +46,35 @@ export declare function createPromptCacheKey(scopeId: string, namespace: string)
46
46
  export declare function resolvePromptCachePolicy(input: ResolvePromptCachePolicyInput): PromptCachePolicy;
47
47
  /** Deduplicate semantic boundaries while preserving their left-to-right order. */
48
48
  export declare function uniquePromptCacheBreakpointIndexes(indexes: readonly (number | undefined)[]): number[];
49
+ interface PromptCacheHistoryBoundary {
50
+ index: number;
51
+ prefixHash: string;
52
+ }
53
+ export interface PromptCacheHistoryPlan {
54
+ scopeKey: string;
55
+ requestOrder: number;
56
+ /** Prior successful boundary, only when its entire request prefix still matches. */
57
+ readBoundaryIndex?: number;
58
+ nextBoundary?: PromptCacheHistoryBoundary;
59
+ }
60
+ /**
61
+ * Remember one successful rolling boundary per scope, without retaining prompt
62
+ * text. Preparing a request is read-only: failed requests must not advance the
63
+ * boundary. The provider supplies normalized, unmarked messages and the index
64
+ * of the latest eligible message, then commits only after a successful response.
65
+ */
66
+ export declare class PromptCacheHistory {
67
+ private readonly maxScopes;
68
+ private readonly ttlMs;
69
+ private readonly boundaries;
70
+ private requestOrder;
71
+ constructor(maxScopes?: number, ttlMs?: number);
72
+ prepare(input: {
73
+ scopeKey: string;
74
+ messages: readonly unknown[];
75
+ latestBoundaryIndex: number;
76
+ requestIdentity: unknown;
77
+ }, now?: number): PromptCacheHistoryPlan;
78
+ commit(plan: PromptCacheHistoryPlan, now?: number): void;
79
+ }
80
+ export {};
@@ -65,11 +65,11 @@ export function resolvePromptCachePolicy(input) {
65
65
  const openAIRoute = kind === "openai" || (kind === "openrouter" && isOpenAIModel(model));
66
66
  if (openAIRoute && supportsOpenAIExplicitCaching(model) && input.explicitDisabled !== true) {
67
67
  return {
68
- strategy: "openai-explicit",
69
- layoutVersion: "system-stable-rolling-v1",
68
+ strategy: "openai-hybrid",
69
+ layoutVersion: "system-stable-previous-implicit-v2",
70
70
  breakpoints: OPENAI_EXPLICIT_BREAKPOINTS,
71
71
  ...(key ? { cacheKey: key } : {}),
72
- promptCacheOptions: { mode: "explicit", ttl: "30m" },
72
+ promptCacheOptions: { mode: "implicit", ttl: "30m" },
73
73
  };
74
74
  }
75
75
  if (openAIRoute) {
@@ -98,3 +98,63 @@ export function uniquePromptCacheBreakpointIndexes(indexes) {
98
98
  }
99
99
  return result;
100
100
  }
101
+ /**
102
+ * Remember one successful rolling boundary per scope, without retaining prompt
103
+ * text. Preparing a request is read-only: failed requests must not advance the
104
+ * boundary. The provider supplies normalized, unmarked messages and the index
105
+ * of the latest eligible message, then commits only after a successful response.
106
+ */
107
+ export class PromptCacheHistory {
108
+ maxScopes;
109
+ ttlMs;
110
+ boundaries = new Map();
111
+ requestOrder = 0;
112
+ constructor(maxScopes = 128, ttlMs = 30 * 60 * 1000) {
113
+ this.maxScopes = maxScopes;
114
+ this.ttlMs = ttlMs;
115
+ }
116
+ prepare(input, now = Date.now()) {
117
+ const plan = {
118
+ scopeKey: input.scopeKey,
119
+ requestOrder: ++this.requestOrder,
120
+ };
121
+ const stored = this.boundaries.get(input.scopeKey);
122
+ const previous = stored && now - stored.updatedAt < this.ttlMs ? stored : undefined;
123
+ // Include tools and request settings as well as message content: identical
124
+ // history with different reasoning or tools is not the same rendered prefix.
125
+ const hash = createHash("sha256")
126
+ .update("codeshell-cache-history-v1\0")
127
+ .update(JSON.stringify(input.requestIdentity) ?? "null")
128
+ .update("\0");
129
+ for (let index = 0; index < input.messages.length; index++) {
130
+ hash.update(JSON.stringify(input.messages[index]) ?? "null").update("\0");
131
+ if (index === previous?.index || index === input.latestBoundaryIndex) {
132
+ const prefixHash = hash.copy().digest("hex");
133
+ if (index === previous?.index && prefixHash === previous.prefixHash) {
134
+ plan.readBoundaryIndex = index;
135
+ }
136
+ if (index === input.latestBoundaryIndex) {
137
+ plan.nextBoundary = { index, prefixHash };
138
+ }
139
+ }
140
+ }
141
+ return plan;
142
+ }
143
+ commit(plan, now = Date.now()) {
144
+ // Concurrent requests can finish in reverse order. An older request must
145
+ // not replace the boundary established by a newer successful request.
146
+ if ((this.boundaries.get(plan.scopeKey)?.requestOrder ?? -1) > plan.requestOrder)
147
+ return;
148
+ this.boundaries.delete(plan.scopeKey);
149
+ if (plan.nextBoundary && this.maxScopes > 0) {
150
+ this.boundaries.set(plan.scopeKey, {
151
+ ...plan.nextBoundary,
152
+ requestOrder: plan.requestOrder,
153
+ updatedAt: now,
154
+ });
155
+ }
156
+ while (this.boundaries.size > this.maxScopes && this.boundaries.size > 0) {
157
+ this.boundaries.delete(this.boundaries.keys().next().value);
158
+ }
159
+ }
160
+ }
@@ -10,6 +10,7 @@
10
10
  import type { ClientDefaults, LLMConfig, LLMResponse } from "../../types.js";
11
11
  import type { CreateMessageOptions } from "../types.js";
12
12
  import { LLMClientBase } from "../client-base.js";
13
+ import { PromptCacheHistory } from "../prompt-cache.js";
13
14
  interface RunStreamOpts {
14
15
  idleTimeoutMs?: number;
15
16
  requestId?: string;
@@ -49,6 +50,7 @@ interface RunStreamOpts {
49
50
  export declare function runStreamWithWatchdog<T = any>(stream: AsyncIterable<T>, opts?: RunStreamOpts): Promise<string>;
50
51
  export declare class OpenAIClient extends LLMClientBase {
51
52
  private _client;
53
+ private readonly promptCacheHistory;
52
54
  private readonly dangerouslyAllowBrowser;
53
55
  private _forceMaxCompletionTokens;
54
56
  private _dropReasoningEffort;
@@ -57,6 +59,7 @@ export declare class OpenAIClient extends LLMClientBase {
57
59
  private _disablePromptCacheKey;
58
60
  constructor(config: LLMConfig, defaults?: ClientDefaults, runtimeOptions?: {
59
61
  dangerouslyAllowBrowser?: boolean;
62
+ promptCacheHistory?: PromptCacheHistory;
60
63
  });
61
64
  protected initClient(): void;
62
65
  private get client();