@cjhyy/code-shell-core 0.7.0 → 0.7.1

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 (118) hide show
  1. package/dist/arena/arena.d.ts +2 -0
  2. package/dist/arena/arena.js +30 -1
  3. package/dist/arena/phases/adjudication.d.ts +2 -1
  4. package/dist/arena/phases/adjudication.js +2 -1
  5. package/dist/arena/phases/build-consensus.d.ts +2 -1
  6. package/dist/arena/phases/build-consensus.js +3 -1
  7. package/dist/arena/phases/cross-review.d.ts +3 -1
  8. package/dist/arena/phases/cross-review.js +8 -2
  9. package/dist/arena/phases/debate-rounds.d.ts +2 -1
  10. package/dist/arena/phases/debate-rounds.js +4 -2
  11. package/dist/arena/phases/participant-research.d.ts +2 -1
  12. package/dist/arena/phases/participant-research.js +3 -1
  13. package/dist/arena/phases/planning-detail-expansion.d.ts +2 -1
  14. package/dist/arena/phases/planning-detail-expansion.js +2 -1
  15. package/dist/arena/planner.d.ts +2 -1
  16. package/dist/arena/planner.js +2 -1
  17. package/dist/arena/types.d.ts +5 -1
  18. package/dist/cli/agent-server-tcp.js +13 -2
  19. package/dist/context/manager.d.ts +3 -3
  20. package/dist/context/manager.js +6 -6
  21. package/dist/context/token-counter.js +5 -3
  22. package/dist/context/tool-result-storage.d.ts +6 -0
  23. package/dist/context/tool-result-storage.js +25 -4
  24. package/dist/credentials/access.d.ts +10 -1
  25. package/dist/credentials/access.js +76 -2
  26. package/dist/credentials/index.d.ts +3 -2
  27. package/dist/credentials/index.js +2 -1
  28. package/dist/credentials/oauth.d.ts +6 -1
  29. package/dist/credentials/oauth.js +66 -1
  30. package/dist/credentials/store.d.ts +1 -1
  31. package/dist/credentials/store.js +16 -7
  32. package/dist/credentials/types.d.ts +38 -1
  33. package/dist/credentials/types.js +16 -1
  34. package/dist/engine/engine.d.ts +61 -32
  35. package/dist/engine/engine.js +289 -55
  36. package/dist/engine/goal.d.ts +5 -3
  37. package/dist/engine/session-title.d.ts +2 -1
  38. package/dist/engine/session-title.js +4 -1
  39. package/dist/engine/turn-loop.d.ts +18 -0
  40. package/dist/engine/turn-loop.js +202 -27
  41. package/dist/engine/types.d.ts +6 -2
  42. package/dist/git/worktree/crud.d.ts +3 -0
  43. package/dist/git/worktree/crud.js +32 -3
  44. package/dist/git/worktree/git-exec.d.ts +2 -2
  45. package/dist/git/worktree/git-exec.js +47 -11
  46. package/dist/git/worktree/query.d.ts +8 -7
  47. package/dist/git/worktree/query.js +27 -20
  48. package/dist/hooks/events.d.ts +3 -0
  49. package/dist/hooks/events.js +0 -3
  50. package/dist/hooks/goal-stop-hook.d.ts +14 -4
  51. package/dist/hooks/goal-stop-hook.js +626 -71
  52. package/dist/hooks/registry.js +3 -0
  53. package/dist/hooks/shell-runner.d.ts +12 -1
  54. package/dist/hooks/shell-runner.js +160 -9
  55. package/dist/index.d.ts +7 -6
  56. package/dist/index.js +6 -5
  57. package/dist/llm/client-base.js +12 -10
  58. package/dist/llm/types.d.ts +12 -5
  59. package/dist/plugins/pluginCommandHook.d.ts +4 -4
  60. package/dist/plugins/pluginCommandHook.js +111 -13
  61. package/dist/protocol/chat-session-manager.d.ts +13 -2
  62. package/dist/protocol/chat-session-manager.js +90 -18
  63. package/dist/protocol/chat-session.d.ts +12 -0
  64. package/dist/protocol/chat-session.js +30 -5
  65. package/dist/protocol/client.d.ts +5 -2
  66. package/dist/protocol/client.js +22 -1
  67. package/dist/protocol/server.d.ts +25 -11
  68. package/dist/protocol/server.js +290 -72
  69. package/dist/protocol/types.d.ts +34 -2
  70. package/dist/protocol/types.js +2 -0
  71. package/dist/services/dream-consolidation.d.ts +3 -0
  72. package/dist/services/dream-consolidation.js +4 -1
  73. package/dist/services/index.d.ts +1 -1
  74. package/dist/services/index.js +1 -1
  75. package/dist/services/oauth.d.ts +34 -10
  76. package/dist/services/oauth.js +233 -98
  77. package/dist/session/session-manager.d.ts +35 -6
  78. package/dist/session/session-manager.js +362 -26
  79. package/dist/session/transcript.d.ts +30 -1
  80. package/dist/session/transcript.js +119 -4
  81. package/dist/tool-system/builtin/agent.js +5 -1
  82. package/dist/tool-system/builtin/arena.js +1 -0
  83. package/dist/tool-system/builtin/background-jobs.d.ts +8 -5
  84. package/dist/tool-system/builtin/background-jobs.js +15 -11
  85. package/dist/tool-system/builtin/background-work.d.ts +6 -1
  86. package/dist/tool-system/builtin/background-work.js +5 -1
  87. package/dist/tool-system/builtin/bash.d.ts +3 -5
  88. package/dist/tool-system/builtin/bash.js +10 -5
  89. package/dist/tool-system/builtin/browser-tools.d.ts +2 -2
  90. package/dist/tool-system/builtin/edit.d.ts +2 -1
  91. package/dist/tool-system/builtin/edit.js +12 -4
  92. package/dist/tool-system/builtin/generate-video.d.ts +4 -0
  93. package/dist/tool-system/builtin/generate-video.js +138 -21
  94. package/dist/tool-system/builtin/glob.d.ts +2 -1
  95. package/dist/tool-system/builtin/glob.js +28 -3
  96. package/dist/tool-system/builtin/grep.d.ts +1 -0
  97. package/dist/tool-system/builtin/grep.js +82 -17
  98. package/dist/tool-system/builtin/index.d.ts +25 -11
  99. package/dist/tool-system/builtin/index.js +45 -2
  100. package/dist/tool-system/builtin/video-providers.d.ts +12 -15
  101. package/dist/tool-system/builtin/video-providers.js +1 -0
  102. package/dist/tool-system/builtin/view-image.d.ts +2 -2
  103. package/dist/tool-system/builtin/web-fetch.js +44 -3
  104. package/dist/tool-system/builtin/worktree.js +25 -7
  105. package/dist/tool-system/builtin/write.d.ts +2 -1
  106. package/dist/tool-system/builtin/write.js +14 -4
  107. package/dist/tool-system/context.d.ts +17 -5
  108. package/dist/tool-system/executor.js +24 -8
  109. package/dist/tool-system/mcp-manager.d.ts +6 -0
  110. package/dist/tool-system/mcp-manager.js +55 -5
  111. package/dist/tool-system/path-policy.d.ts +19 -0
  112. package/dist/tool-system/path-policy.js +62 -1
  113. package/dist/tool-system/permission.d.ts +43 -3
  114. package/dist/tool-system/permission.js +383 -30
  115. package/dist/tool-system/registry.d.ts +3 -2
  116. package/dist/tool-system/registry.js +52 -34
  117. package/dist/types.d.ts +15 -0
  118. package/package.json +1 -1
@@ -101,6 +101,9 @@ export class HookRegistry {
101
101
  if (result.continueSession) {
102
102
  aggregated.continueSession = true;
103
103
  }
104
+ if (result.goalTermination !== undefined) {
105
+ aggregated.goalTermination = result.goalTermination;
106
+ }
104
107
  if (result.stop) {
105
108
  aggregated.stop = true;
106
109
  break;
@@ -28,16 +28,27 @@
28
28
  * timeout Defaults to 60_000 ms; settings.timeout_ms overrides.
29
29
  * On timeout we SIGTERM then SIGKILL, log, and return {}.
30
30
  */
31
+ import type { Writable } from "node:stream";
31
32
  import type { HookContext, HookResult } from "./events.js";
32
33
  import type { SettingsHookConfig } from "../types.js";
33
34
  import { validateHookResult } from "./hook-output.js";
34
35
  export { validateHookResult };
36
+ export declare const MAX_HOOK_STDIN_BYTES: number;
37
+ /**
38
+ * Write one hook envelope without leaving a Writable `error` event unhandled.
39
+ * The listener is installed before write(), remains through end(), and the
40
+ * promise does not resolve until both the write callback and any required
41
+ * drain have completed.
42
+ */
43
+ export declare function writeHookStdin(stdin: Writable | null | undefined, envelope: string): Promise<void>;
44
+ /** Close stdin for an oversized envelope while still absorbing stream errors. */
45
+ export declare function closeHookStdin(stdin: Writable | null | undefined): void;
35
46
  /**
36
47
  * Run one shell-hook command and return the parsed HookResult. Catches
37
48
  * every failure mode (spawn error, timeout, malformed JSON) and
38
49
  * returns an empty result so the registry's chain keeps going.
39
50
  */
40
- export declare function runShellHook(config: SettingsHookConfig, ctx: HookContext): Promise<HookResult>;
51
+ export declare function runShellHook(config: SettingsHookConfig, ctx: HookContext, abortSignal?: AbortSignal): Promise<HookResult>;
41
52
  /**
42
53
  * Check whether the hook should fire for the current ctx, respecting
43
54
  * the optional `matcher` regex. Returns false for non-tool events
@@ -35,13 +35,105 @@ import { killChildTree } from "../runtime/spawn-common.js";
35
35
  // "./shell-runner.js" (e.g. tests) keep compiling.
36
36
  export { validateHookResult };
37
37
  const DEFAULT_TIMEOUT_MS = 60_000;
38
+ export const MAX_HOOK_STDIN_BYTES = 256 * 1024;
39
+ function stdinFailure(error) {
40
+ const err = error;
41
+ return {
42
+ type: "stdin_error",
43
+ code: typeof err?.code === "string" ? err.code : "UNKNOWN",
44
+ message: err instanceof Error ? err.message : String(error),
45
+ };
46
+ }
47
+ /**
48
+ * Write one hook envelope without leaving a Writable `error` event unhandled.
49
+ * The listener is installed before write(), remains through end(), and the
50
+ * promise does not resolve until both the write callback and any required
51
+ * drain have completed.
52
+ */
53
+ export function writeHookStdin(stdin, envelope) {
54
+ if (!stdin)
55
+ return Promise.resolve();
56
+ return new Promise((resolve, reject) => {
57
+ let settled = false;
58
+ let writeComplete = false;
59
+ let drained = false;
60
+ let ending = false;
61
+ const cleanup = (removeErrorListener = true) => {
62
+ if (removeErrorListener)
63
+ stdin.off("error", onError);
64
+ stdin.off("drain", onDrain);
65
+ };
66
+ const finish = (error, keepErrorListener = false) => {
67
+ if (settled)
68
+ return;
69
+ settled = true;
70
+ cleanup(!keepErrorListener);
71
+ if (error !== undefined)
72
+ reject(error);
73
+ else
74
+ resolve();
75
+ };
76
+ const onError = (error) => finish(error);
77
+ const onDrain = () => {
78
+ drained = true;
79
+ maybeEnd();
80
+ };
81
+ const maybeEnd = () => {
82
+ if (!writeComplete || !drained || ending || settled)
83
+ return;
84
+ ending = true;
85
+ try {
86
+ stdin.end(() => finish());
87
+ }
88
+ catch (error) {
89
+ finish(error, true);
90
+ }
91
+ };
92
+ // EventEmitter treats an unhandled stream `error` as fatal. Register the
93
+ // one-shot handler before the first byte is handed to the child.
94
+ stdin.once("error", onError);
95
+ try {
96
+ drained = stdin.write(envelope, (error) => {
97
+ if (error) {
98
+ // Some Writable implementations invoke the callback before emitting
99
+ // their fatal `error`. Keep the one-shot guard installed for it.
100
+ finish(error, true);
101
+ return;
102
+ }
103
+ writeComplete = true;
104
+ maybeEnd();
105
+ });
106
+ if (!drained)
107
+ stdin.once("drain", onDrain);
108
+ maybeEnd();
109
+ }
110
+ catch (error) {
111
+ finish(error, true);
112
+ }
113
+ });
114
+ }
115
+ /** Close stdin for an oversized envelope while still absorbing stream errors. */
116
+ export function closeHookStdin(stdin) {
117
+ if (!stdin)
118
+ return;
119
+ stdin.once("error", () => { });
120
+ try {
121
+ stdin.end();
122
+ }
123
+ catch {
124
+ // No envelope was written; the hook is still allowed to run without args.
125
+ }
126
+ }
38
127
  /**
39
128
  * Run one shell-hook command and return the parsed HookResult. Catches
40
129
  * every failure mode (spawn error, timeout, malformed JSON) and
41
130
  * returns an empty result so the registry's chain keeps going.
42
131
  */
43
- export async function runShellHook(config, ctx) {
132
+ export async function runShellHook(config, ctx, abortSignal) {
44
133
  const timeoutMs = config.timeout_ms ?? DEFAULT_TIMEOUT_MS;
134
+ const signal = abortSignal ?? ctx.data.signal;
135
+ if (signal?.aborted)
136
+ return {};
45
137
  return new Promise((resolve) => {
46
138
  let child;
47
139
  try {
@@ -64,13 +156,20 @@ export async function runShellHook(config, ctx) {
64
156
  let stdout = "";
65
157
  let stderr = "";
66
158
  let settled = false;
159
+ let timer;
67
160
  const settle = (value) => {
68
161
  if (settled)
69
162
  return;
70
163
  settled = true;
164
+ clearTimeout(timer);
165
+ signal?.removeEventListener("abort", onAbort);
71
166
  resolve(value);
72
167
  };
73
- const timer = setTimeout(() => {
168
+ const onAbort = () => {
169
+ killChildTree(child, 1000);
170
+ settle({});
171
+ };
172
+ timer = setTimeout(() => {
74
173
  // eslint-disable-next-line no-console
75
174
  console.warn(`[hooks] ${config.event} hook timed out after ${timeoutMs}ms: ${config.command}`);
76
175
  // win32: taskkill /T reaps the shell tree; POSIX: SIGTERM → grace →
@@ -78,6 +177,9 @@ export async function runShellHook(config, ctx) {
78
177
  killChildTree(child, 1000);
79
178
  settle({});
80
179
  }, timeoutMs);
180
+ signal?.addEventListener("abort", onAbort, { once: true });
181
+ if (signal?.aborted)
182
+ onAbort();
81
183
  // Byte cap: once we cross MAX_HOOK_OUTPUT_BYTES on stdout, kill the
82
184
  // child and surface a 'cap exceeded' failure. A misbehaving handler
83
185
  // can otherwise hold the engine memory hostage while we accumulate
@@ -119,8 +221,9 @@ export async function runShellHook(config, ctx) {
119
221
  console.error(`[hooks] ${config.event} hook failed: ${config.command}`, err.message);
120
222
  settle({});
121
223
  });
122
- child.on("close", (code) => {
123
- clearTimeout(timer);
224
+ let stdinFinished = false;
225
+ let pendingClose;
226
+ const handleClose = (code) => {
124
227
  if (stdoutCapped) {
125
228
  // eslint-disable-next-line no-console
126
229
  console.error(`[hooks] ${config.event} hook killed due to oversized stdout (> ${MAX_HOOK_OUTPUT_BYTES} bytes)`);
@@ -166,17 +269,65 @@ export async function runShellHook(config, ctx) {
166
269
  // eslint-disable-next-line no-console
167
270
  console.error(`[hooks] ${config.event} hook exited with code ${code}:`, stderr.slice(0, 500));
168
271
  settle({});
272
+ };
273
+ child.on("close", (code) => {
274
+ clearTimeout(timer);
275
+ if (settled)
276
+ return;
277
+ // If close wins the event-loop race, wait for stdin's callback/error so
278
+ // an EPIPE cannot be overwritten by an apparently clean child exit.
279
+ if (!stdinFinished && !stdoutCapped) {
280
+ pendingClose = { code };
281
+ return;
282
+ }
283
+ handleClose(code);
169
284
  });
170
- // Write the context envelope on stdin then close.
285
+ const finishStdin = () => {
286
+ stdinFinished = true;
287
+ if (pendingClose && !settled) {
288
+ const { code } = pendingClose;
289
+ pendingClose = undefined;
290
+ handleClose(code);
291
+ }
292
+ };
293
+ let envelope;
171
294
  try {
172
- child.stdin?.write(JSON.stringify({ eventName: ctx.eventName, data: ctx.data }));
173
- child.stdin?.end();
295
+ envelope = JSON.stringify({ eventName: ctx.eventName, data: ctx.data });
174
296
  }
175
- catch (err) {
297
+ catch (error) {
176
298
  // eslint-disable-next-line no-console
177
- console.error(`[hooks] failed to write stdin for ${config.event}:`, err.message);
299
+ console.error(`[hooks] failed to serialize stdin for ${config.event}:`, error instanceof Error ? error.message : String(error));
178
300
  settle({});
301
+ return;
179
302
  }
303
+ if (Buffer.byteLength(envelope, "utf8") > MAX_HOOK_STDIN_BYTES) {
304
+ // eslint-disable-next-line no-console
305
+ console.warn(`[hooks] ${config.event} hook stdin exceeded ${MAX_HOOK_STDIN_BYTES} bytes; running without envelope`);
306
+ closeHookStdin(child.stdin);
307
+ finishStdin();
308
+ return;
309
+ }
310
+ // Let the spawned shell enter user code before writing. Besides avoiding a
311
+ // needless race, this makes an intentional early stdin close observable as
312
+ // EPIPE/ECONNRESET while the child is still alive.
313
+ setImmediate(() => {
314
+ void (async () => {
315
+ if (settled)
316
+ return;
317
+ try {
318
+ await writeHookStdin(child.stdin, envelope);
319
+ finishStdin();
320
+ }
321
+ catch (error) {
322
+ clearTimeout(timer);
323
+ const failure = stdinFailure(error);
324
+ // eslint-disable-next-line no-console
325
+ console.error(`[hooks] failed to write stdin for ${config.event}:`, failure.message);
326
+ killChildTree(child, 1000);
327
+ settle({ data: { hookFailure: failure } });
328
+ }
329
+ })();
330
+ });
180
331
  });
181
332
  }
182
333
  /**
package/dist/index.d.ts CHANGED
@@ -3,8 +3,8 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export declare const VERSION = "0.7.0";
7
- export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionWorkspace, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
6
+ export declare const VERSION = "0.7.1";
7
+ export type { Message, ContentBlock, ToolDefinition, ToolCall, ToolResult, RegisteredTool, TranscriptEvent, TranscriptEventType, SessionState, SessionWorkspace, SessionForkLineage, ContextUsageAnchor, SessionStatus, TokenUsage, CompiledInput, PermissionDecision, PermissionMode, PermissionRule, TurnPhase, TurnResult, TerminalReason, StreamEvent, StreamCallback, LLMConfig, ClientDefaults, LLMResponse, Settings, MCPServerConfig, } from "./types.js";
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  export { Engine, loadAgentDefinitionsForCwd } from "./engine/engine.js";
10
10
  export type { EngineConfig, EngineHookConfig, EngineResult } from "./engine/types.js";
@@ -30,7 +30,7 @@ export type { ApprovalBackend } from "./tool-system/permission.js";
30
30
  export { BUILTIN_TOOLS } from "./tool-system/builtin/index.js";
31
31
  export type { BuiltinTool, BuiltinToolFn } from "./tool-system/builtin/index.js";
32
32
  export { makeUpdateAutomationMemoryTool } from "./tool-system/builtin/update-automation-memory.js";
33
- export { MCPManager, buildHttpHeaders, buildStdioEnv } from "./tool-system/mcp-manager.js";
33
+ export { MCPManager, buildHttpHeaders, buildStdioEnv, createMcpAuthenticatedFetch, } from "./tool-system/mcp-manager.js";
34
34
  export type { AskUserFn } from "./tool-system/builtin/ask-user.js";
35
35
  export type { Task, TaskStatus } from "./tool-system/builtin/task.js";
36
36
  export { HookRegistry } from "./hooks/registry.js";
@@ -41,9 +41,9 @@ export { AgentClient, type BackgroundAgentCompletedHandler } from "./protocol/cl
41
41
  export { createInProcessTransport, StdioTransport, type Transport } from "./protocol/transport.js";
42
42
  export { SocketTransport, listenTcp, type TcpListenResult } from "./protocol/tcp-transport.js";
43
43
  export { createServer, createClient, type CreateServerOptions, type CreateClientOptions, type ServerHandle, } from "./protocol/factories.js";
44
- export { Methods, ErrorCodes, type RpcMessage, type RunResult } from "./protocol/types.js";
44
+ export { Methods, ErrorCodes, type RpcMessage, type RunResult, type ForkSessionParams, type ForkSessionResult as ProtocolForkSessionResult, } from "./protocol/types.js";
45
45
  export { Transcript } from "./session/transcript.js";
46
- export { SessionManager } from "./session/session-manager.js";
46
+ export { SessionManager, buildForkState, buildForkTranscript, type ForkSessionOptions, type ForkSessionResult, } from "./session/session-manager.js";
47
47
  export { FileHistory } from "./session/file-history.js";
48
48
  export { createWorktree, currentBranch, DEFAULT_WORKTREE_BRANCH_PREFIX, getWorktreeDiff, isManagedWorktreeBranch, isValidWorktreeBranchPrefix, findMainWorktreeRoot, listWorktrees, listWorktreesFast, normalizeWorktreeBranchPrefix, removeWorktree, validateWorktreeSlug, worktreeHasUncommittedOrAheadChanges, worktreeHasUncommittedChanges, type CreateWorktreeOptions, type ListWorktreesOptions, type ListWorktreesFastOptions, type RemoveWorktreeResult, type RemoveWorktreeOptions, type WorktreeDiffSummary, type WorktreeInfo, type WorktreeWorkspaceOwner, } from "./git/worktree.js";
49
49
  export { latestUndoTarget, earliestSnapshotsPerFile, latestTurnUndoTargets, latestRedoTargets, } from "./session/undo-target.js";
@@ -51,6 +51,7 @@ export { diffLines, renderDiffPreview, type DiffLine } from "./session/simple-di
51
51
  export { MemoryManager } from "./session/memory.js";
52
52
  export type { MemoryEntry, MemoryOrigin, MemoryScope } from "./session/memory.js";
53
53
  export { runDreamConsolidation, type DreamConsolidationInput, type DreamConsolidationResult, } from "./services/dream-consolidation.js";
54
+ export { authorize, refreshToken, generatePKCE, createHardenedOAuthFetch, type OAuthConfig, type OAuthTokens, type OAuthAuthorizeOptions, type OAuthRefreshOptions, type HardenedOAuthFetchOptions, } from "./services/oauth.js";
54
55
  export type { FileSnapshot, RedoRecord } from "./session/file-history.js";
55
56
  export { PromptComposer } from "./prompt/composer.js";
56
57
  export { SectionCache } from "./prompt/section-cache.js";
@@ -109,7 +110,7 @@ export { migrateConfig, configVersionOf, CURRENT_CONFIG_VERSION, type MigrationS
109
110
  export { SettingsSchema, validateSettings } from "./settings/schema.js";
110
111
  export { settingsJsonSchema, writeSettingsSchemaFile } from "./settings/schema-export.js";
111
112
  export { personalizationFrom, type PersonalizationConfig } from "./settings/personalization.js";
112
- export { CredentialStore, type CredentialScope, type MaskedCredential, type Credential, type CredentialType, type CredentialStoreFile, type OAuthCredentialPublicStatus, type OAuthCredentialSecret, buildOAuthRefreshRequest, isOAuthAccessTokenExpired, oauthCredentialStatus, parseOAuthCredentialSecret, summarizeOAuthCredentialSecret, type OAuthClockOptions, type OAuthRefreshHandler, type OAuthRefreshRequest, formatNetscapeCookies, parseCookieJar, type CookieLike, useCredentialToolDef, useCredentialToolDefFor, sweepStaleCredentialCookies, getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, isCredentialSecretAvailable, materializeCookieSecret, type CredentialAccess, type CredentialAccessScope, type CredentialMetadata, type CredentialSnapshot, type CredentialSnapshotEntry, type EncryptionCipher, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./credentials/index.js";
113
+ export { CredentialStore, type CredentialScope, type MaskedCredential, type Credential, type CredentialType, type CredentialStoreFile, type OAuthCredentialPublicStatus, type OAuthCredentialSecret, type OAuthTokenResponse, buildOAuthRefreshRequest, isOAuthAccessTokenExpired, oauthCredentialStatus, parseOAuthCredentialSecret, 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";
113
114
  /** @internal Shared with the in-repo TUI/desktop hosts; not stable SDK surface. */
114
115
  export { getSessionId, switchSession, getOriginalCwd, setOriginalCwd, getProjectRoot, setProjectRoot, getCwdState, getIsInteractive, updateLastInteractionTime, flushInteractionTime, markScrollActivity, type AttributedCounter, type ChannelEntry, } from "./state.js";
115
116
  /** @internal Shared primitives for the in-repo TUI/desktop hosts; not stable SDK surface. */
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Public API exports.
5
5
  */
6
- export const VERSION = "0.7.0";
6
+ export const VERSION = "0.7.1";
7
7
  // ─── Exceptions ──────────────────────────────────────────────────
8
8
  export { FrameworkError, LLMError, LLMRateLimitError, ContextLimitError, ToolError, ToolNotFoundError, ToolExecutionError, ToolTimeoutError, PermissionDeniedError, SessionError, TranscriptError, ConfigError, SandboxUnavailableError, } from "./exceptions.js";
9
9
  // ─── Engine (primary API) ────────────────────────────────────────
@@ -27,7 +27,7 @@ export { ToolExecutor } from "./tool-system/executor.js";
27
27
  export { PermissionClassifier, HeadlessApprovalBackend, AutoApprovalBackend, } from "./tool-system/permission.js";
28
28
  export { BUILTIN_TOOLS } from "./tool-system/builtin/index.js";
29
29
  export { makeUpdateAutomationMemoryTool } from "./tool-system/builtin/update-automation-memory.js";
30
- export { MCPManager, buildHttpHeaders, buildStdioEnv } from "./tool-system/mcp-manager.js";
30
+ export { MCPManager, buildHttpHeaders, buildStdioEnv, createMcpAuthenticatedFetch, } from "./tool-system/mcp-manager.js";
31
31
  // ─── Hooks ───────────────────────────────────────────────────────
32
32
  export { HookRegistry } from "./hooks/registry.js";
33
33
  export { wrapHookMessages } from "./hooks/inject.js";
@@ -39,16 +39,17 @@ export { SocketTransport, listenTcp } from "./protocol/tcp-transport.js";
39
39
  // Recommended public factories — see protocol/factories.ts for the
40
40
  // stable construction contract referenced by standard §7.
41
41
  export { createServer, createClient, } from "./protocol/factories.js";
42
- export { Methods, ErrorCodes } from "./protocol/types.js";
42
+ export { Methods, ErrorCodes, } from "./protocol/types.js";
43
43
  // ─── Session ─────────────────────────────────────────────────────
44
44
  export { Transcript } from "./session/transcript.js";
45
- export { SessionManager } from "./session/session-manager.js";
45
+ export { SessionManager, buildForkState, buildForkTranscript, } from "./session/session-manager.js";
46
46
  export { FileHistory } from "./session/file-history.js";
47
47
  export { createWorktree, currentBranch, DEFAULT_WORKTREE_BRANCH_PREFIX, getWorktreeDiff, isManagedWorktreeBranch, isValidWorktreeBranchPrefix, findMainWorktreeRoot, listWorktrees, listWorktreesFast, normalizeWorktreeBranchPrefix, removeWorktree, validateWorktreeSlug, worktreeHasUncommittedOrAheadChanges, worktreeHasUncommittedChanges, } from "./git/worktree.js";
48
48
  export { latestUndoTarget, earliestSnapshotsPerFile, latestTurnUndoTargets, latestRedoTargets, } from "./session/undo-target.js";
49
49
  export { diffLines, renderDiffPreview } from "./session/simple-diff.js";
50
50
  export { MemoryManager } from "./session/memory.js";
51
51
  export { runDreamConsolidation, } from "./services/dream-consolidation.js";
52
+ export { authorize, refreshToken, generatePKCE, createHardenedOAuthFetch, } from "./services/oauth.js";
52
53
  // ─── Prompt ──────────────────────────────────────────────────────
53
54
  export { PromptComposer } from "./prompt/composer.js";
54
55
  export { SectionCache } from "./prompt/section-cache.js";
@@ -127,7 +128,7 @@ export { migrateConfig, configVersionOf, CURRENT_CONFIG_VERSION, } from "./setti
127
128
  export { SettingsSchema, validateSettings } from "./settings/schema.js";
128
129
  export { settingsJsonSchema, writeSettingsSchemaFile } from "./settings/schema-export.js";
129
130
  export { personalizationFrom } from "./settings/personalization.js";
130
- export { CredentialStore, buildOAuthRefreshRequest, isOAuthAccessTokenExpired, oauthCredentialStatus, parseOAuthCredentialSecret, summarizeOAuthCredentialSecret, formatNetscapeCookies, parseCookieJar, useCredentialToolDef, useCredentialToolDefFor, sweepStaleCredentialCookies, getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, isCredentialSecretAvailable, materializeCookieSecret, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./credentials/index.js";
131
+ export { CredentialStore, buildOAuthRefreshRequest, isOAuthAccessTokenExpired, oauthCredentialStatus, parseOAuthCredentialSecret, mergeOAuthTokenResponse, shouldRefreshOAuthCredential, summarizeOAuthCredentialSecret, formatNetscapeCookies, parseCookieJar, useCredentialToolDef, useCredentialToolDefFor, sweepStaleCredentialCookies, getCredentialAccess, setDefaultCredentialAccess, createIpcCredentialAccess, localCredentialAccess, credentialAccessScope, credentialAllowsEnvExposure, credentialSecretHint, isCredentialSecretAvailable, materializeCookieSecret, PlaintextCipher, setDefaultCredentialCipher, getDefaultCredentialCipher, } from "./credentials/index.js";
131
132
  // ─── State (runtime singletons shared with TUI) ──────────────────
132
133
  /** @internal Shared with the in-repo TUI/desktop hosts; not stable SDK surface. */
133
134
  export { getSessionId, switchSession, getOriginalCwd, setOriginalCwd, getProjectRoot, setProjectRoot, getCwdState, getIsInteractive, updateLastInteractionTime, flushInteractionTime, markScrollActivity, } from "./state.js";
@@ -54,16 +54,18 @@ export class LLMClientBase {
54
54
  this.initClient();
55
55
  }
56
56
  recordUsage(usage, options) {
57
- if (options?.recordUsage === false)
58
- return;
59
- this.usage.records.push(usage);
60
- this.usage.totalPromptTokens += usage.promptTokens;
61
- this.usage.totalCompletionTokens += usage.completionTokens;
62
- this.usage.totalTokens += usage.totalTokens;
63
- this.usage.totalCacheReadTokens += usage.cacheReadTokens ?? 0;
64
- this.usage.totalCacheCreationTokens += usage.cacheCreationTokens ?? 0;
65
- this.usage.requestCount++;
66
- LLMClientBase.onUsage?.(this.model, usage);
57
+ if (options?.requestVisible !== false) {
58
+ this.usage.records.push(usage);
59
+ this.usage.totalPromptTokens += usage.promptTokens;
60
+ this.usage.totalCompletionTokens += usage.completionTokens;
61
+ this.usage.totalTokens += usage.totalTokens;
62
+ this.usage.totalCacheReadTokens += usage.cacheReadTokens ?? 0;
63
+ this.usage.totalCacheCreationTokens += usage.cacheCreationTokens ?? 0;
64
+ this.usage.requestCount++;
65
+ }
66
+ if (options?.billingEnabled !== false) {
67
+ LLMClientBase.onUsage?.(this.model, usage);
68
+ }
67
69
  }
68
70
  getUsage() {
69
71
  return { ...this.usage };
@@ -12,13 +12,20 @@ export interface CreateMessageOptions {
12
12
  onChunk?: (chunk: LLMStreamChunk) => void;
13
13
  signal?: AbortSignal;
14
14
  /**
15
- * If false, this call's usage is excluded from the client's usage tracker
16
- * and from the process-wide onUsage hook. Used for auxiliary sub-calls
17
- * (tool-summary, future helper prompts) that should not appear in
18
- * session_end.cost or skew turn/requestCount semantics.
15
+ * Whether this real provider request is billed through the process-wide
16
+ * usage hook. This is independent of request visibility: auxiliary calls
17
+ * are normally billed while remaining hidden from the foreground request
18
+ * tracker. Only explicit manual-audit paths should disable billing.
19
19
  * @default true
20
20
  */
21
- recordUsage?: boolean;
21
+ billingEnabled?: boolean;
22
+ /**
23
+ * Whether this request appears in the client's foreground usage tracker.
24
+ * Hidden auxiliary calls must report their usage to their owning Engine's
25
+ * session/Goal recorder instead.
26
+ * @default true
27
+ */
28
+ requestVisible?: boolean;
22
29
  /**
23
30
  * Reasoning/thinking setting for this call. Overrides LLMConfig.reasoning.
24
31
  * Rich shape ({mode:"off"|"on"} | {mode:"effort",effort} |
@@ -42,13 +42,13 @@ export interface PluginCommandHookSpec {
42
42
  timeoutMs?: number;
43
43
  }
44
44
  /**
45
- * Run one plugin hook command. Returns an empty HookResult on any failure
46
- * (spawn error, timeout, non-zero exit, unparseable stdout) so the hook
47
- * chain keeps moving — a misbehaving plugin must not wedge the engine.
45
+ * Run one plugin hook command. CC's exit-code 2 and explicit deny/reject JSON
46
+ * are normalized to the native HookResult deny contract. Other failures stay
47
+ * non-blocking so a broken plugin does not wedge the engine.
48
48
  *
49
49
  * Stdin carries the same envelope shape the codeshell-native shell-runner
50
50
  * uses (eventName + data) so plugin authors who want richer context can
51
51
  * read it; CC plugins typically ignore stdin and emit context derived from
52
52
  * their own state (e.g. reading a SKILL.md off disk via PLUGIN_ROOT).
53
53
  */
54
- export declare function runPluginCommandHook(spec: PluginCommandHookSpec, ctx: HookContext): Promise<HookResult>;
54
+ export declare function runPluginCommandHook(spec: PluginCommandHookSpec, ctx: HookContext, abortSignal?: AbortSignal): Promise<HookResult>;
@@ -32,6 +32,7 @@
32
32
  */
33
33
  import { spawn } from "node:child_process";
34
34
  import { MAX_HOOK_OUTPUT_BYTES } from "../hooks/hook-output.js";
35
+ import { closeHookStdin, MAX_HOOK_STDIN_BYTES, writeHookStdin } from "../hooks/shell-runner.js";
35
36
  import { killChildTree } from "../runtime/spawn-common.js";
36
37
  const DEFAULT_TIMEOUT_MS = 60_000;
37
38
  /**
@@ -60,18 +61,49 @@ function extractAdditionalContext(parsed) {
60
61
  }
61
62
  return null;
62
63
  }
64
+ function extractExplicitDenial(parsed) {
65
+ if (!parsed || typeof parsed !== "object")
66
+ return null;
67
+ const obj = parsed;
68
+ const nested = obj.hookSpecificOutput && typeof obj.hookSpecificOutput === "object"
69
+ ? obj.hookSpecificOutput
70
+ : undefined;
71
+ const decision = obj.decision ?? obj.permissionDecision ?? nested?.decision ?? nested?.permissionDecision;
72
+ if (decision !== "deny" && decision !== "reject")
73
+ return null;
74
+ const message = obj.reason ??
75
+ obj.message ??
76
+ obj.permissionDecisionReason ??
77
+ nested?.reason ??
78
+ nested?.message ??
79
+ nested?.permissionDecisionReason;
80
+ return typeof message === "string" && message.trim() ? { message: message.trim() } : {};
81
+ }
82
+ function parseJson(stdout) {
83
+ if (!stdout)
84
+ return undefined;
85
+ try {
86
+ return JSON.parse(stdout);
87
+ }
88
+ catch {
89
+ return undefined;
90
+ }
91
+ }
63
92
  /**
64
- * Run one plugin hook command. Returns an empty HookResult on any failure
65
- * (spawn error, timeout, non-zero exit, unparseable stdout) so the hook
66
- * chain keeps moving — a misbehaving plugin must not wedge the engine.
93
+ * Run one plugin hook command. CC's exit-code 2 and explicit deny/reject JSON
94
+ * are normalized to the native HookResult deny contract. Other failures stay
95
+ * non-blocking so a broken plugin does not wedge the engine.
67
96
  *
68
97
  * Stdin carries the same envelope shape the codeshell-native shell-runner
69
98
  * uses (eventName + data) so plugin authors who want richer context can
70
99
  * read it; CC plugins typically ignore stdin and emit context derived from
71
100
  * their own state (e.g. reading a SKILL.md off disk via PLUGIN_ROOT).
72
101
  */
73
- export async function runPluginCommandHook(spec, ctx) {
102
+ export async function runPluginCommandHook(spec, ctx, abortSignal) {
74
103
  const timeoutMs = spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
104
+ const signal = abortSignal ?? ctx.data.signal;
105
+ if (signal?.aborted)
106
+ return {};
75
107
  return new Promise((resolve) => {
76
108
  let child;
77
109
  try {
@@ -101,6 +133,7 @@ export async function runPluginCommandHook(spec, ctx) {
101
133
  let stdout = "";
102
134
  let stderr = "";
103
135
  let settled = false;
136
+ let timer;
104
137
  // Output byte caps mirror src/hooks/shell-runner.ts so a chatty plugin
105
138
  // hook can't hold engine memory hostage. Past the cap we SIGTERM the
106
139
  // child and treat the hook as failed; stderr cap truncates with a
@@ -112,15 +145,24 @@ export async function runPluginCommandHook(spec, ctx) {
112
145
  if (settled)
113
146
  return;
114
147
  settled = true;
148
+ clearTimeout(timer);
149
+ signal?.removeEventListener("abort", onAbort);
115
150
  resolve(value);
116
151
  };
117
- const timer = setTimeout(() => {
152
+ const onAbort = () => {
153
+ killChildTree(child, 1000);
154
+ settle({});
155
+ };
156
+ timer = setTimeout(() => {
118
157
  // eslint-disable-next-line no-console
119
158
  console.warn(`[plugin-hook] ${spec.pluginKey} ${ctx.eventName} timed out after ${timeoutMs}ms`);
120
159
  // win32: taskkill /T reaps the tree; POSIX: SIGTERM → grace → SIGKILL.
121
160
  killChildTree(child, 1000);
122
161
  settle({});
123
162
  }, timeoutMs);
163
+ signal?.addEventListener("abort", onAbort, { once: true });
164
+ if (signal?.aborted)
165
+ onAbort();
124
166
  child.stdout?.on("data", (chunk) => {
125
167
  stdoutBytes += chunk.length;
126
168
  if (stdoutBytes > MAX_HOOK_OUTPUT_BYTES) {
@@ -150,21 +192,29 @@ export async function runPluginCommandHook(spec, ctx) {
150
192
  console.error(`[plugin-hook] ${spec.pluginKey} ${ctx.eventName} spawn error:`, err.message);
151
193
  settle({});
152
194
  });
153
- child.on("close", (code) => {
154
- clearTimeout(timer);
195
+ let stdinFinished = false;
196
+ let pendingClose;
197
+ const handleClose = (code) => {
155
198
  if (stdoutCapped) {
156
199
  // eslint-disable-next-line no-console
157
200
  console.error(`[plugin-hook] ${spec.pluginKey} ${ctx.eventName} killed due to oversized stdout (> ${MAX_HOOK_OUTPUT_BYTES} bytes)`);
158
201
  settle({});
159
202
  return;
160
203
  }
204
+ const trimmed = stdout.trim();
205
+ const parsedForDecision = parseJson(trimmed);
206
+ const explicitDenial = extractExplicitDenial(parsedForDecision);
207
+ if (code === 2 || explicitDenial) {
208
+ const message = stderr.trim() || explicitDenial?.message || "Plugin denied this tool operation";
209
+ settle({ decision: "deny", messages: [message] });
210
+ return;
211
+ }
161
212
  if (code !== 0) {
162
213
  // eslint-disable-next-line no-console
163
214
  console.warn(`[plugin-hook] ${spec.pluginKey} ${ctx.eventName} exited ${code}; stderr:`, stderr.slice(0, 500));
164
215
  settle({});
165
216
  return;
166
217
  }
167
- const trimmed = stdout.trim();
168
218
  if (trimmed.length === 0) {
169
219
  settle({});
170
220
  return;
@@ -185,16 +235,64 @@ export async function runPluginCommandHook(spec, ctx) {
185
235
  return;
186
236
  }
187
237
  settle({ messages: [additional] });
238
+ };
239
+ child.on("close", (code) => {
240
+ clearTimeout(timer);
241
+ if (settled)
242
+ return;
243
+ if (!stdinFinished && !stdoutCapped) {
244
+ pendingClose = { code };
245
+ return;
246
+ }
247
+ handleClose(code);
188
248
  });
249
+ const finishStdin = () => {
250
+ stdinFinished = true;
251
+ if (pendingClose && !settled) {
252
+ const { code } = pendingClose;
253
+ pendingClose = undefined;
254
+ handleClose(code);
255
+ }
256
+ };
189
257
  // Stdin envelope: matches the shape codeshell-native shell hooks use.
190
- // CC plugins generally don't read stdin, but writing it is cheap and
191
- // keeps the protocol open for plugin authors who want it.
258
+ let envelope;
192
259
  try {
193
- child.stdin?.write(JSON.stringify({ eventName: ctx.eventName, data: ctx.data }));
194
- child.stdin?.end();
260
+ envelope = JSON.stringify({ eventName: ctx.eventName, data: ctx.data });
195
261
  }
196
262
  catch {
197
- // Child may have closed stdin already; not fatal.
263
+ closeHookStdin(child.stdin);
264
+ finishStdin();
265
+ return;
198
266
  }
267
+ if (Buffer.byteLength(envelope, "utf8") > MAX_HOOK_STDIN_BYTES) {
268
+ // eslint-disable-next-line no-console
269
+ console.warn(`[plugin-hook] ${spec.pluginKey} ${ctx.eventName} stdin exceeded ${MAX_HOOK_STDIN_BYTES} bytes; running without envelope`);
270
+ closeHookStdin(child.stdin);
271
+ finishStdin();
272
+ return;
273
+ }
274
+ setImmediate(() => {
275
+ void (async () => {
276
+ if (settled)
277
+ return;
278
+ try {
279
+ await writeHookStdin(child.stdin, envelope);
280
+ finishStdin();
281
+ }
282
+ catch (error) {
283
+ clearTimeout(timer);
284
+ const err = error;
285
+ const failure = {
286
+ type: "stdin_error",
287
+ code: typeof err?.code === "string" ? err.code : "UNKNOWN",
288
+ message: err instanceof Error ? err.message : String(error),
289
+ };
290
+ // eslint-disable-next-line no-console
291
+ console.error(`[plugin-hook] ${spec.pluginKey} ${ctx.eventName} stdin error:`, failure.message);
292
+ killChildTree(child, 1000);
293
+ settle({ data: { hookFailure: failure } });
294
+ }
295
+ })();
296
+ });
199
297
  });
200
298
  }