mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-26 01:55:50 +00:00
Compare commits
10 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8246ffa392 | ||
|
|
0753dafccc | ||
|
|
10327e0fab | ||
|
|
106fd5043e | ||
|
|
c0945164d3 | ||
|
|
10855cf735 | ||
|
|
751efb827c | ||
|
|
2c801502dc | ||
|
|
d3121f0dfb | ||
|
|
8f6d4f88dd |
5
bun.lock
5
bun.lock
@@ -352,6 +352,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.81.0",
|
"@anthropic-ai/sdk": "^0.81.0",
|
||||||
|
"bun-types": "latest",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1644,7 +1645,7 @@
|
|||||||
|
|
||||||
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "https://registry.npmmirror.com/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
|
||||||
|
|
||||||
"bun-types": ["bun-types@1.3.12", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.12.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
|
||||||
|
|
||||||
"bundle-name": ["bundle-name@4.1.0", "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
"bundle-name": ["bundle-name@4.1.0", "https://registry.npmmirror.com/bundle-name/-/bundle-name-4.1.0.tgz", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q=="],
|
||||||
|
|
||||||
@@ -3338,6 +3339,8 @@
|
|||||||
|
|
||||||
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
|
"@types/bun/bun-types": ["bun-types@1.3.12", "https://registry.npmmirror.com/bun-types/-/bun-types-1.3.12.tgz", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="],
|
||||||
|
|
||||||
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
"@typespec/ts-http-runtime/https-proxy-agent": ["https-proxy-agent@7.0.6", "https://registry.npmmirror.com/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="],
|
||||||
|
|
||||||
"aggregate-error/indent-string": ["indent-string@4.0.0", "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
"aggregate-error/indent-string": ["indent-string@4.0.0", "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "claude-code-best",
|
"name": "claude-code-best",
|
||||||
"version": "2.8.1",
|
"version": "2.8.2",
|
||||||
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
"description": "Reverse-engineered Anthropic Claude Code CLI — interactive AI coding assistant in the terminal",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"author": "claude-code-best <claude-code-best@proton.me>",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
|
|||||||
@@ -137,7 +137,10 @@ export const GoalTool = buildTool({
|
|||||||
return `Updating goal: ${input.status}${input.reason ? ` — ${input.reason}` : ''}`
|
return `Updating goal: ${input.status}${input.reason ? ` — ${input.reason}` : ''}`
|
||||||
},
|
},
|
||||||
renderToolResultMessage(output: Output) {
|
renderToolResultMessage(output: Output) {
|
||||||
if (output.error) return `Goal error: ${output.error}`
|
if (!output) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (output?.error) return `Goal error: ${output.error}`
|
||||||
if (output.report) return output.report
|
if (output.report) return output.report
|
||||||
if (output.goal) {
|
if (output.goal) {
|
||||||
return `Goal "${output.goal.objective}" — ${output.goal.status}`
|
return `Goal "${output.goal.objective}" — ${output.goal.status}`
|
||||||
|
|||||||
@@ -547,7 +547,7 @@ export const LocalMemoryRecallTool = buildTool({
|
|||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
tool_use_id: toolUseID,
|
tool_use_id: toolUseID,
|
||||||
content: jsonStringify(output),
|
content: jsonStringify(output),
|
||||||
is_error: output.error !== undefined,
|
is_error: output?.error !== undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} satisfies ToolDef<InputSchema, Output>)
|
} satisfies ToolDef<InputSchema, Output>)
|
||||||
|
|||||||
@@ -33,6 +33,9 @@ export function renderToolResultMessage(
|
|||||||
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
|
_progressMessagesForMessage: ProgressMessage<ToolProgressData>[],
|
||||||
{ verbose }: { verbose: boolean },
|
{ verbose }: { verbose: boolean },
|
||||||
): React.ReactNode {
|
): React.ReactNode {
|
||||||
|
// Defense in depth: framework validates via outputSchema, but resumed
|
||||||
|
// transcripts can still produce null here via deserialization edge cases.
|
||||||
|
if (!output) return null;
|
||||||
if (output.error) {
|
if (output.error) {
|
||||||
return (
|
return (
|
||||||
<MessageResponse height={1}>
|
<MessageResponse height={1}>
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ export const VaultHttpFetchTool = buildTool({
|
|||||||
type: 'tool_result',
|
type: 'tool_result',
|
||||||
tool_use_id: toolUseID,
|
tool_use_id: toolUseID,
|
||||||
content: jsonStringify(output),
|
content: jsonStringify(output),
|
||||||
is_error: output.error !== undefined,
|
is_error: output?.error !== undefined,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
} satisfies ToolDef<InputSchema, Output>)
|
} satisfies ToolDef<InputSchema, Output>)
|
||||||
|
|||||||
21
packages/workflow-engine/LICENSE
Normal file
21
packages/workflow-engine/LICENSE
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 claude-code-best
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
71
packages/workflow-engine/README.md
Normal file
71
packages/workflow-engine/README.md
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
# @claude-code-best/workflow-engine
|
||||||
|
|
||||||
|
Deterministic JS script orchestration engine for multi-agent workflows. The core layer has zero runtime dependencies and talks to the outside world exclusively through **port adapters** — you bring your own agent backend, journal store, and progress sink.
|
||||||
|
|
||||||
|
## Why
|
||||||
|
|
||||||
|
When you orchestrate multiple LLM agents, you want the orchestration itself to be **deterministic, replayable, and testable**. This engine runs a plain JS script (compiled by Bun's transpiler) with primitives like `agent()`, `phase()`, `parallel()` and `pipeline()`. The non-deterministic parts (the LLM, the file system, the clock) are isolated behind ports, so the same script produces the same journal on every replay.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun add @claude-code-best/workflow-engine
|
||||||
|
# or
|
||||||
|
npm install @claude-code-best/workflow-engine
|
||||||
|
```
|
||||||
|
|
||||||
|
Runtime peer requirements: `ajv` and `zod` are pulled in automatically as dependencies.
|
||||||
|
|
||||||
|
## Minimal example
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import {
|
||||||
|
createFileJournalStore,
|
||||||
|
createHostHandle,
|
||||||
|
runWorkflow,
|
||||||
|
type WorkflowPorts,
|
||||||
|
} from '@claude-code-best/workflow-engine'
|
||||||
|
|
||||||
|
const script = `
|
||||||
|
export const meta = { name: 'hello', description: 'minimal demo' }
|
||||||
|
phase('Greet')
|
||||||
|
const reply = await agent({ prompt: 'Say hi in one short sentence.' })
|
||||||
|
emit('result', { reply })
|
||||||
|
`
|
||||||
|
|
||||||
|
const ports: WorkflowPorts = {
|
||||||
|
// Provide your own agent runner + journal + progress emitter.
|
||||||
|
// See examples/smoke.ts for a complete Anthropic SDK wiring.
|
||||||
|
} as WorkflowPorts
|
||||||
|
|
||||||
|
const handle = createHostHandle()
|
||||||
|
await runWorkflow({
|
||||||
|
script,
|
||||||
|
ports,
|
||||||
|
workflowDir: '.wfe/runs/hello',
|
||||||
|
hostHandle: handle,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
For a fully wired end-to-end example with the Anthropic SDK, see [`examples/smoke.ts`](./examples/smoke.ts).
|
||||||
|
|
||||||
|
## Core primitives
|
||||||
|
|
||||||
|
- `agent(params)` — call the configured AgentRunner; supports structured-output via JSON Schema.
|
||||||
|
- `phase(name)` — declare a logical phase (display + progress grouping).
|
||||||
|
- `parallel([...])` — barrier-style fan-out with bounded concurrency.
|
||||||
|
- `pipeline(stream, fn)` — streaming pipeline with per-item hooks.
|
||||||
|
- `emit(type, payload)` — emit a progress event to the host.
|
||||||
|
- `log.*` / hooks / budgets — see the TypeScript definitions for the full surface.
|
||||||
|
|
||||||
|
## Building from source
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bun install # from the repo root
|
||||||
|
bun run build # outputs dist/index.js + dist/**/*.d.ts
|
||||||
|
bun test # 178 tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT © claude-code-best
|
||||||
@@ -1,19 +1,69 @@
|
|||||||
{
|
{
|
||||||
"name": "@claude-code-best/workflow-engine",
|
"name": "@claude-code-best/workflow-engine",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"description": "Deterministic JS script orchestration engine for multi-agent workflows. Zero core-layer runtime dependencies; talks to the world via port adapters.",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "./src/index.ts",
|
"license": "MIT",
|
||||||
"types": "./src/index.ts",
|
"author": "claude-code-best <claude-code-best@proton.me>",
|
||||||
|
"homepage": "https://github.com/claude-code-best/claude-code/tree/main/packages/workflow-engine#readme",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "git+https://github.com/claude-code-best/claude-code.git",
|
||||||
|
"directory": "packages/workflow-engine"
|
||||||
|
},
|
||||||
|
"bugs": {
|
||||||
|
"url": "https://github.com/claude-code-best/claude-code/issues"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"workflow",
|
||||||
|
"orchestration",
|
||||||
|
"multi-agent",
|
||||||
|
"claude",
|
||||||
|
"automation",
|
||||||
|
"scripting",
|
||||||
|
"deterministic"
|
||||||
|
],
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"module": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"default": "./dist/index.js"
|
||||||
|
},
|
||||||
"./package.json": "./package.json"
|
"./package.json": "./package.json"
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"dist",
|
||||||
|
"src",
|
||||||
|
"!src/**/__tests__",
|
||||||
|
"!src/**/*.test.ts",
|
||||||
|
"scripts/build.ts",
|
||||||
|
"tsconfig.json",
|
||||||
|
"tsconfig.build.json",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE"
|
||||||
|
],
|
||||||
|
"sideEffects": false,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=20"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"ajv": "^8.18.0",
|
"ajv": "^8.18.0",
|
||||||
"zod": "^4.3.6"
|
"zod": "^4.3.6"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@anthropic-ai/sdk": "^0.81.0"
|
"@anthropic-ai/sdk": "^0.81.0",
|
||||||
|
"bun-types": "latest"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "bun run scripts/build.ts",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"test": "bun test",
|
||||||
|
"prepublishOnly": "bun run test && bun run build"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
21
packages/workflow-engine/scripts/build.ts
Normal file
21
packages/workflow-engine/scripts/build.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { mkdir, rm } from 'node:fs/promises'
|
||||||
|
|
||||||
|
const ROOT = new URL('../', import.meta.url)
|
||||||
|
const DIST = new URL('../dist/', import.meta.url)
|
||||||
|
|
||||||
|
await rm(DIST, { recursive: true, force: true })
|
||||||
|
await mkdir(DIST, { recursive: true })
|
||||||
|
|
||||||
|
// Emit dist/**/*.js + dist/**/*.d.ts (+ maps) via tsc.
|
||||||
|
const proc = Bun.spawn(['bunx', 'tsc', '-p', 'tsconfig.build.json'], {
|
||||||
|
cwd: ROOT.pathname,
|
||||||
|
stdout: 'inherit',
|
||||||
|
stderr: 'inherit',
|
||||||
|
})
|
||||||
|
const exitCode = await proc.exited
|
||||||
|
if (exitCode !== 0) {
|
||||||
|
console.error('tsc emit failed')
|
||||||
|
process.exit(exitCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('✓ build complete')
|
||||||
@@ -16,10 +16,16 @@ export * from './engine/context.js'
|
|||||||
export * from './engine/hooks.js'
|
export * from './engine/hooks.js'
|
||||||
export * from './engine/runWorkflow.js'
|
export * from './engine/runWorkflow.js'
|
||||||
export * from './progress/events.js'
|
export * from './progress/events.js'
|
||||||
export {
|
import {
|
||||||
createWorkflowTool,
|
createWorkflowTool,
|
||||||
type WorkflowToolDescriptor,
|
type WorkflowToolDescriptor,
|
||||||
} from './tool/WorkflowTool.js'
|
} from './tool/WorkflowTool.js'
|
||||||
export { workflowInputSchema, type WorkflowInput } from './tool/schema.js'
|
import { workflowInputSchema, type WorkflowInput } from './tool/schema.js'
|
||||||
export { persistInlineScript } from './tool/persistInline.js'
|
import { persistInlineScript } from './tool/persistInline.js'
|
||||||
export { WORKFLOW_TOOL_NAME } from './tool/constants.js'
|
export {
|
||||||
|
createWorkflowTool,
|
||||||
|
type WorkflowToolDescriptor,
|
||||||
|
workflowInputSchema,
|
||||||
|
type WorkflowInput,
|
||||||
|
persistInlineScript,
|
||||||
|
}
|
||||||
|
|||||||
21
packages/workflow-engine/tsconfig.build.json
Normal file
21
packages/workflow-engine/tsconfig.build.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": false,
|
||||||
|
"sourceMap": false,
|
||||||
|
"outDir": "dist",
|
||||||
|
"rootDir": "src",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"target": "ESNext"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*"],
|
||||||
|
"exclude": [
|
||||||
|
"node_modules",
|
||||||
|
"src/**/__tests__/**",
|
||||||
|
"examples/**",
|
||||||
|
"scripts/**"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -67,7 +67,14 @@ export function UserToolSuccessMessage({
|
|||||||
if (parsedOutput && !parsedOutput.success) {
|
if (parsedOutput && !parsedOutput.success) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const toolResult = parsedOutput?.data ?? message.toolUseResult;
|
// Only trust schema-validated output. Fall back to raw toolUseResult only
|
||||||
|
// when it's a non-null object — schemas without outputSchema, or successful
|
||||||
|
// parses that yield null/undefined data, must not reach renderToolResultMessage
|
||||||
|
// (tool UIs access output.error / output.action on first line and crash).
|
||||||
|
const toolResult = parsedOutput?.success ? parsedOutput.data : message.toolUseResult;
|
||||||
|
if (!toolResult || typeof toolResult !== 'object') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
// Collapse diff display for old messages (verbose/ctrl+o overrides)
|
// Collapse diff display for old messages (verbose/ctrl+o overrides)
|
||||||
const effectiveStyle = shouldCollapseDiffs && !verbose ? 'condensed' : style;
|
const effectiveStyle = shouldCollapseDiffs && !verbose ? 'condensed' : style;
|
||||||
@@ -88,6 +95,11 @@ export function UserToolSuccessMessage({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ink requires text strings to be inside <Text>. Tools that return plain
|
||||||
|
// multi-line strings (e.g. GoalTool's usage report) crash without the wrap.
|
||||||
|
// React elements from UI.tsx files pass through unchanged.
|
||||||
|
const wrappedMessage = typeof renderedMessage === 'string' ? <Text>{renderedMessage}</Text> : renderedMessage;
|
||||||
|
|
||||||
// Tools that return '' from userFacingName opt out of tool chrome and
|
// Tools that return '' from userFacingName opt out of tool chrome and
|
||||||
// render like plain assistant text. Skip the tool-result width constraint
|
// render like plain assistant text. Skip the tool-result width constraint
|
||||||
// so MarkdownTable's SAFETY_MARGIN=4 (tuned for the assistant-text 2-col
|
// so MarkdownTable's SAFETY_MARGIN=4 (tuned for the assistant-text 2-col
|
||||||
@@ -97,7 +109,7 @@ export function UserToolSuccessMessage({
|
|||||||
return (
|
return (
|
||||||
<Box flexDirection="column">
|
<Box flexDirection="column">
|
||||||
<Box flexDirection="column" width={rendersAsAssistantText ? undefined : width}>
|
<Box flexDirection="column" width={rendersAsAssistantText ? undefined : width}>
|
||||||
{renderedMessage}
|
{wrappedMessage}
|
||||||
{feature('BASH_CLASSIFIER')
|
{feature('BASH_CLASSIFIER')
|
||||||
? classifierRule && (
|
? classifierRule && (
|
||||||
<MessageResponse height={1}>
|
<MessageResponse height={1}>
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
// Pure path-normalisation helper used by toolInfo / toolResults / forwarding.
|
// Pure path-normalisation helper used by toolInfo / toolResults / forwarding.
|
||||||
import { isAbsolute, resolve } from 'node:path'
|
//
|
||||||
|
// POSIX semantics are used so that emitted paths are platform-independent:
|
||||||
|
// ACP v1 spec (tool-calls.mdx:304-306) requires ToolCallLocation.path /
|
||||||
|
// Diff.path to be absolute, and the wire format is POSIX-style regardless of
|
||||||
|
// the host OS. Using the platform-specific `node:path` here would prepend the
|
||||||
|
// Windows drive letter (e.g. "D:\...") to POSIX-style inputs like
|
||||||
|
// "/Users/test/project" — silently corrupting paths emitted to ACP clients.
|
||||||
|
import { isAbsolute, resolve } from 'node:path/posix'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalises an emitted file path against the session cwd so that
|
* Normalises an emitted file path against the session cwd so that
|
||||||
|
|||||||
@@ -172,7 +172,9 @@ export function sanitizeTitle(text: string): string {
|
|||||||
|
|
||||||
// ── Path display helpers ──────────────────────────────────────────
|
// ── Path display helpers ──────────────────────────────────────────
|
||||||
|
|
||||||
import * as path from 'node:path'
|
// POSIX semantics so paths are normalised consistently regardless of host OS.
|
||||||
|
// ACP paths are always POSIX-style (see bridge/paths.ts for the same rationale).
|
||||||
|
import * as path from 'node:path/posix'
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert an absolute file path to a project-relative path for display.
|
* Convert an absolute file path to a project-relative path for display.
|
||||||
@@ -186,7 +188,7 @@ export function toDisplayPath(filePath: string, cwd?: string): string {
|
|||||||
resolvedFile.startsWith(resolvedCwd + path.sep) ||
|
resolvedFile.startsWith(resolvedCwd + path.sep) ||
|
||||||
resolvedFile === resolvedCwd
|
resolvedFile === resolvedCwd
|
||||||
) {
|
) {
|
||||||
return path.relative(resolvedCwd, resolvedFile).replaceAll('\\', '/')
|
return path.relative(resolvedCwd, resolvedFile)
|
||||||
}
|
}
|
||||||
return filePath
|
return filePath
|
||||||
}
|
}
|
||||||
|
|||||||
18
src/setup.ts
18
src/setup.ts
@@ -94,10 +94,20 @@ export async function setup(
|
|||||||
// (SessionStart in particular) can spawn and snapshot process.env.
|
// (SessionStart in particular) can spawn and snapshot process.env.
|
||||||
if (feature('UDS_INBOX')) {
|
if (feature('UDS_INBOX')) {
|
||||||
const m = await import('./utils/udsMessaging.js')
|
const m = await import('./utils/udsMessaging.js')
|
||||||
await m.startUdsMessaging(
|
try {
|
||||||
messagingSocketPath ?? m.getDefaultUdsSocketPath(),
|
await m.startUdsMessaging(
|
||||||
{ isExplicit: messagingSocketPath !== undefined },
|
messagingSocketPath ?? m.getDefaultUdsSocketPath(),
|
||||||
)
|
{ isExplicit: messagingSocketPath !== undefined },
|
||||||
|
)
|
||||||
|
} catch (error) {
|
||||||
|
logError(error)
|
||||||
|
console.error(
|
||||||
|
chalk.red(
|
||||||
|
`Error: Failed to start messaging socket (UDS_INBOX): ${errorMessage(error)}`,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ import {
|
|||||||
MAX_UDS_INBOX_BYTES,
|
MAX_UDS_INBOX_BYTES,
|
||||||
MAX_UDS_FRAME_BYTES,
|
MAX_UDS_FRAME_BYTES,
|
||||||
MAX_UDS_CLIENTS,
|
MAX_UDS_CLIENTS,
|
||||||
|
MAX_UNIX_SOCKET_PATH_LENGTH,
|
||||||
|
assertValidUnixSocketPath,
|
||||||
formatUdsAddress,
|
formatUdsAddress,
|
||||||
parseUdsTarget,
|
parseUdsTarget,
|
||||||
sendUdsMessage,
|
sendUdsMessage,
|
||||||
@@ -34,11 +36,23 @@ let previousConfigDir: string | undefined
|
|||||||
let tempConfigDir = ''
|
let tempConfigDir = ''
|
||||||
|
|
||||||
function socketPath(label: string): string {
|
function socketPath(label: string): string {
|
||||||
const suffix = `${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}-${label}`
|
const suffix = `${process.pid}-${Math.random().toString(16).slice(2)}-${label}`
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
return `\\\\.\\pipe\\claude-code-test-${suffix}`
|
return `\\\\.\\pipe\\claude-code-test-${suffix}`
|
||||||
}
|
}
|
||||||
return join(tmpdir(), 'claude-code-test', `${suffix}.sock`)
|
const base =
|
||||||
|
process.platform === 'darwin'
|
||||||
|
? '/tmp/claude-uds-test'
|
||||||
|
: join(tmpdir(), 'cc-uds-test')
|
||||||
|
return join(base, `${suffix}.sock`)
|
||||||
|
}
|
||||||
|
|
||||||
|
function shortTestDir(prefix: string): string {
|
||||||
|
const id = `${process.pid}-${Math.random().toString(16).slice(2)}`
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
return join('/tmp', `${prefix}-${id}`)
|
||||||
|
}
|
||||||
|
return join(tmpdir(), `${prefix}-${id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sleep(ms: number): Promise<void> {
|
function sleep(ms: number): Promise<void> {
|
||||||
@@ -499,6 +513,27 @@ describe('UDS inbox retention', () => {
|
|||||||
expect(getDefaultUdsSocketPath()).not.toBe(firstPath)
|
expect(getDefaultUdsSocketPath()).not.toBe(firstPath)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('default socket path stays within AF_UNIX length limit', () => {
|
||||||
|
const path = getDefaultUdsSocketPath()
|
||||||
|
if (process.platform === 'win32') return
|
||||||
|
expect(Buffer.byteLength(path, 'utf8')).toBeLessThanOrEqual(
|
||||||
|
MAX_UNIX_SOCKET_PATH_LENGTH,
|
||||||
|
)
|
||||||
|
expect(() => assertValidUnixSocketPath(path)).not.toThrow()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('rejects socket paths longer than AF_UNIX limit', () => {
|
||||||
|
if (process.platform === 'win32') return
|
||||||
|
const longPath = `/tmp/${'x'.repeat(MAX_UNIX_SOCKET_PATH_LENGTH)}.sock`
|
||||||
|
expect(() => assertValidUnixSocketPath(longPath)).toThrow(/max 104/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('default socket path can bind on Node.js', async () => {
|
||||||
|
const path = getDefaultUdsSocketPath()
|
||||||
|
await startUdsMessaging(path, { isExplicit: true })
|
||||||
|
await stopUdsMessaging()
|
||||||
|
})
|
||||||
|
|
||||||
test('rejects oversized receiver responses before retaining them', async () => {
|
test('rejects oversized receiver responses before retaining them', async () => {
|
||||||
const path = socketPath('oversized-response')
|
const path = socketPath('oversized-response')
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
@@ -688,10 +723,7 @@ describe('UDS inbox retention', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('fails closed when an explicit socket parent is not private', async () => {
|
test('fails closed when an explicit socket parent is not private', async () => {
|
||||||
const parent = join(
|
const parent = shortTestDir('uds-sp')
|
||||||
tmpdir(),
|
|
||||||
`uds-socket-parent-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
||||||
)
|
|
||||||
await mkdir(parent, { recursive: true, mode: 0o755 })
|
await mkdir(parent, { recursive: true, mode: 0o755 })
|
||||||
await chmod(parent, 0o755)
|
await chmod(parent, 0o755)
|
||||||
|
|
||||||
@@ -707,10 +739,7 @@ describe('UDS inbox retention', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
test('fails closed when an explicit socket parent is a file', async () => {
|
test('fails closed when an explicit socket parent is a file', async () => {
|
||||||
const parentFile = join(
|
const parentFile = shortTestDir('uds-spf')
|
||||||
tmpdir(),
|
|
||||||
`uds-socket-parent-file-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`,
|
|
||||||
)
|
|
||||||
await writeFile(parentFile, 'not a directory', 'utf-8')
|
await writeFile(parentFile, 'not a directory', 'utf-8')
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -753,6 +753,7 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
|
|||||||
// and remains true for all subsequent messages in the normalization process.
|
// and remains true for all subsequent messages in the normalization process.
|
||||||
let isNewChain = false
|
let isNewChain = false
|
||||||
return messages.flatMap(message => {
|
return messages.flatMap(message => {
|
||||||
|
if (!message) return []
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'assistant': {
|
case 'assistant': {
|
||||||
const aMsg = message as AssistantMessage
|
const aMsg = message as AssistantMessage
|
||||||
@@ -776,7 +777,7 @@ export function normalizeMessages(messages: Message[]): NormalizedMessage[] {
|
|||||||
isVirtual: message.isVirtual,
|
isVirtual: message.isVirtual,
|
||||||
requestId: message.requestId,
|
requestId: message.requestId,
|
||||||
uuid,
|
uuid,
|
||||||
error: message.error,
|
error: message?.error,
|
||||||
isApiErrorMessage: message.isApiErrorMessage,
|
isApiErrorMessage: message.isApiErrorMessage,
|
||||||
advisorModel: message.advisorModel,
|
advisorModel: message.advisorModel,
|
||||||
} as NormalizedAssistantMessage
|
} as NormalizedAssistantMessage
|
||||||
@@ -2376,6 +2377,7 @@ export function normalizeMessagesForAPI(
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
.forEach(message => {
|
.forEach(message => {
|
||||||
|
if (!message) return []
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'system': {
|
case 'system': {
|
||||||
// local_command system messages need to be included as user messages
|
// local_command system messages need to be included as user messages
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function toInternalMessages(
|
|||||||
messages: readonly DeepImmutable<SDKMessage>[],
|
messages: readonly DeepImmutable<SDKMessage>[],
|
||||||
): Message[] {
|
): Message[] {
|
||||||
return messages.flatMap(message => {
|
return messages.flatMap(message => {
|
||||||
|
if (!message) return []
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'assistant':
|
case 'assistant':
|
||||||
return [
|
return [
|
||||||
@@ -127,6 +128,7 @@ export function fromSDKCompactMetadata(
|
|||||||
|
|
||||||
export function toSDKMessages(messages: Message[]): SDKMessage[] {
|
export function toSDKMessages(messages: Message[]): SDKMessage[] {
|
||||||
return messages.flatMap((message): SDKMessage[] => {
|
return messages.flatMap((message): SDKMessage[] => {
|
||||||
|
if (!message) return []
|
||||||
switch (message.type) {
|
switch (message.type) {
|
||||||
case 'assistant':
|
case 'assistant':
|
||||||
return [
|
return [
|
||||||
@@ -138,7 +140,7 @@ export function toSDKMessages(messages: Message[]): SDKMessage[] {
|
|||||||
session_id: getSessionId(),
|
session_id: getSessionId(),
|
||||||
parent_tool_use_id: null,
|
parent_tool_use_id: null,
|
||||||
uuid: message.uuid,
|
uuid: message.uuid,
|
||||||
error: message.error,
|
error: message?.error,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
case 'user':
|
case 'user':
|
||||||
|
|||||||
@@ -85,13 +85,26 @@ export const MAX_UDS_CLIENTS = 128
|
|||||||
export const UDS_AUTH_TIMEOUT_MS = 2_000
|
export const UDS_AUTH_TIMEOUT_MS = 2_000
|
||||||
export const UDS_IDLE_TIMEOUT_MS = 30_000
|
export const UDS_IDLE_TIMEOUT_MS = 30_000
|
||||||
|
|
||||||
|
/** macOS/BSD AF_UNIX `sun_path` limit (bytes, excluding NUL). */
|
||||||
|
export const MAX_UNIX_SOCKET_PATH_LENGTH = 104
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Public API — socket path helpers
|
// Public API — socket path helpers
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
export function assertValidUnixSocketPath(path: string): void {
|
||||||
|
if (process.platform === 'win32') return
|
||||||
|
const byteLength = Buffer.byteLength(path, 'utf8')
|
||||||
|
if (byteLength > MAX_UNIX_SOCKET_PATH_LENGTH) {
|
||||||
|
throw new Error(
|
||||||
|
`[udsMessaging] socket path is ${byteLength} bytes (max ${MAX_UNIX_SOCKET_PATH_LENGTH}): ${path}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Default socket path based on PID, placed in a tmpdir subdirectory so it
|
* Default socket path based on PID. Uses a flat file under a short temp
|
||||||
* survives across config-home changes and avoids polluting ~/.claude.
|
* directory so the path stays within the AF_UNIX limit on macOS.
|
||||||
*
|
*
|
||||||
* On Windows, Node.js requires named pipe paths in the `\\.\pipe\` namespace;
|
* On Windows, Node.js requires named pipe paths in the `\\.\pipe\` namespace;
|
||||||
* file-system paths like `C:\...\Temp\x.sock` cause EACCES. Bun handles both
|
* file-system paths like `C:\...\Temp\x.sock` cause EACCES. Bun handles both
|
||||||
@@ -99,17 +112,19 @@ export const UDS_IDLE_TIMEOUT_MS = 30_000
|
|||||||
*/
|
*/
|
||||||
export function getDefaultUdsSocketPath(): string {
|
export function getDefaultUdsSocketPath(): string {
|
||||||
if (defaultSocketPath) return defaultSocketPath
|
if (defaultSocketPath) return defaultSocketPath
|
||||||
const nonce = randomBytes(16).toString('hex')
|
const nonce = randomBytes(8).toString('hex')
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
defaultSocketPath = `\\\\.\\pipe\\claude-code-${process.pid}-${nonce}`
|
defaultSocketPath = `\\\\.\\pipe\\claude-code-${process.pid}-${nonce}`
|
||||||
return defaultSocketPath
|
return defaultSocketPath
|
||||||
}
|
}
|
||||||
|
|
||||||
defaultSocketPath = join(
|
defaultSocketPath = join(
|
||||||
tmpdir(),
|
tmpdir(),
|
||||||
'claude-code-socks',
|
'cc-socks',
|
||||||
`${process.pid}-${nonce}`,
|
`${process.pid}-${nonce}`,
|
||||||
'messaging.sock',
|
'messaging.sock',
|
||||||
)
|
)
|
||||||
|
assertValidUnixSocketPath(defaultSocketPath)
|
||||||
return defaultSocketPath
|
return defaultSocketPath
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -416,6 +431,8 @@ export async function startUdsMessaging(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
assertValidUnixSocketPath(path)
|
||||||
|
|
||||||
// Ensure parent directory exists (skip on Windows — pipe paths aren't files)
|
// Ensure parent directory exists (skip on Windows — pipe paths aren't files)
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') {
|
||||||
await ensureSocketParent(path)
|
await ensureSocketParent(path)
|
||||||
|
|||||||
Reference in New Issue
Block a user