diff --git a/.github/workflows/lifecycle.yml b/.github/workflows/lifecycle.yml index 440be56..b53addc 100644 --- a/.github/workflows/lifecycle.yml +++ b/.github/workflows/lifecycle.yml @@ -15,7 +15,7 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} - timeout-minutes: 10 + timeout-minutes: 15 steps: - uses: actions/checkout@v4 with: @@ -31,12 +31,12 @@ jobs: bun-version: '1.3.14' - uses: astral-sh/setup-uv@v6 - run: uv sync --extra dev + - run: bun install --frozen-lockfile + working-directory: ts # Dependency installation above may use the network. All tests below use # deterministic local children, no provider CLI or credentials. - name: Python offline conformance run: PYTHONPATH=src uv run --offline pytest tests/ - - run: bun install --frozen-lockfile - working-directory: ts - name: TypeScript offline conformance (Bun) run: bun test working-directory: ts @@ -66,6 +66,9 @@ jobs: - name: Built-package Claude SDK conformance (Node) run: node tests/node-claude-sdk.mjs working-directory: ts + - name: Built-package Cline SDK conformance (Node) + run: node tests/node-cline-sdk.mjs + working-directory: ts - name: Built-package caller-owned OpenHands conformance (Node) run: node tests/node-openhands.mjs working-directory: ts diff --git a/ADAPTER-MATRIX.md b/ADAPTER-MATRIX.md index d51abc2..f770abf 100644 --- a/ADAPTER-MATRIX.md +++ b/ADAPTER-MATRIX.md @@ -210,6 +210,14 @@ worker per operation. Exact thread resume and interruption preserve the native thread; approval replies and bypass are unsupported. Local execution still requires the native Amp thread service. See the [Amp SDK contract and separate qualification evidence](SPEC.md#optional-amp-sdk-sessions). +Cline SDK sessions use `getSessionCapabilities("cline", "sdk")` / +`get_session_capabilities("cline", "sdk")`: the official TypeScript SDK 0.0.82 +in explicit local mode, through the same Node >=22.14 bridge in both languages. +`features: "builtin-only"` is required; detached hooks/plugins/MCP/subagents +are excluded, and the Harness parent owns command execution through the +public bash hook. Native identity, raw usage/events and callback approvals +are retained. Native synthetic-provider checks do not imply authenticated +provider success. See the [Cline SDK contract](SPEC.md#optional-cline-sdk-sessions). OpenCode HTTP sessions use `getSessionCapabilities("opencode", "rpc")` / `get_session_capabilities("opencode", "rpc")` against an explicitly selected caller-owned 1.18.29 endpoint. Both languages implement native HTTP/SSE, diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 5ed7be7..a485112 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to harness -Harness is a small library for uniform coding-agent integration. CLI execution, Pi RPC, optional OMP/Amp SDK bridges, native Claude/Factory Droid SDK sessions and caller-owned OpenCode/OpenHands sessions ship today. Additional backends must satisfy the shared [SPEC](SPEC.md#backend-and-session-implementation-gates) without adding a fleet manager or application. +Harness is a small library for uniform coding-agent integration. CLI execution, Pi RPC, optional OMP/Amp/Cline SDK bridges, native Claude/Factory Droid SDK sessions and caller-owned OpenCode/OpenHands sessions ship today. Additional backends must satisfy the shared [SPEC](SPEC.md#backend-and-session-implementation-gates) without adding a fleet manager or application. ## Before you open a PR @@ -22,6 +22,10 @@ PYTHONPATH=src uv run pytest tests/ cd ts && bun test ``` +Install TypeScript development dependencies (`bun install` from `ts/`) before +the Python suite as well: both language Cline cases use its optional pinned +`@cline/sdk` package with a finite synthetic loopback provider. + All tests must pass in both. If you add a fixture, both impls must parse it. ### Offline conformance versus live smoke @@ -38,14 +42,20 @@ The `subprocess lifecycle` workflow gates macOS and Linux (GitHub After `bun run build`, run `node tests/node-lifecycle.mjs` and `node tests/node-sessions.mjs`, `node tests/node-omp-sdk.mjs`, `node tests/node-claude-sdk.mjs`, `node tests/node-amp-sdk.mjs`, -`node tests/node-opencode.mjs`, `node tests/node-openhands.mjs` and -`node tests/node-droid-sdk.mjs` from `ts/` to check packaged subprocess, RPC, -SDK and HTTP/SSE/WebSocket behavior under Node as well as source under Bun. -OMP uses the real Bun worker with a synthetic SDK; Amp uses the real Node -worker with a synthetic SDK and finite CLI fixture. Claude and Factory use -pinned real SDK development dependencies with finite synthetic CLIs, never -bundled CLIs or providers. Install Python development dependencies with -`uv sync --extra dev` before these cases. No Windows lifecycle support is claimed. +`node tests/node-cline-sdk.mjs`, `node tests/node-opencode.mjs`, +`node tests/node-openhands.mjs` and `node tests/node-droid-sdk.mjs` from `ts/` +to check packaged subprocess, RPC, SDK and HTTP/SSE/WebSocket behavior under +Node as well as source under Bun. OMP uses the real Bun worker with a synthetic +SDK; Amp uses the real Node worker with a synthetic SDK and finite CLI fixture. +Claude and Factory use pinned real SDK development dependencies with finite +synthetic CLIs, never bundled CLIs or providers. Install Python development +dependencies with `uv sync --extra dev` before these cases. +No Windows lifecycle support is claimed. +Cline uses the pinned real SDK in a Node >=22.14 worker for Python, Bun and +packaged Node. `tests/cline_sdk_cases.json` shares expected outcomes; its +provider peer is finite and synthetic. Test-only event/failure injection is +separate from private unmodified native-runtime and authenticated-provider +qualification. The remote cases share `tests/opencode_cases.json` and `tests/openhands_cases.json` with isolated synthetic peers, finite lifetimes and handle-owned cleanup. They are not native OpenCode/OpenHands or authenticated-provider qualification; @@ -205,7 +215,7 @@ Add a row to [ADAPTER-MATRIX.md](ADAPTER-MATRIX.md) covering: CLI binary name, i Streaming, controlled sessions and optional agent SDK integrations are eligible when they implement the [SPEC gates](SPEC.md#backend-and-session-implementation-gates) in both languages. This supersedes the historical blanket SDK exclusion. -Pi RPC, OMP/Amp SDK bridges, native Claude/Factory Droid SDK sessions and +Pi RPC, OMP/Amp/Cline SDK bridges, native Claude/Factory Droid SDK sessions and caller-owned OpenCode/OpenHands sessions are implemented; other protocols/SDKs need qualification. Keep optional SDK loading isolated from ordinary CLI imports and preserve existing caller-selected configuration. diff --git a/README.md b/README.md index 12af8ac..aa0aa5a 100644 --- a/README.md +++ b/README.md @@ -449,6 +449,75 @@ that operation before allowing follow-up in the same thread. Native events, usage and terminal results stay verbatim. See the [Amp dependency, lifecycle and qualification contract](SPEC.md#optional-amp-sdk-sessions). +### Optional Cline SDK sessions + +Select `harness="cline", backend="sdk"` with the official +`@cline/sdk@0.0.82` installed separately. Python and TypeScript use the same +**Node >=22.14 bridge**, not a native Python SDK or the Cline CLI. + +```python +from pathlib import Path +from harness import ClineSdkOptions, SessionSpec, open_session + +async def review(workdir: Path, sdk_package: Path, profile: Path): + session = await open_session(SessionSpec( + harness="cline", backend="sdk", workdir=workdir, + cline_sdk=ClineSdkOptions( + package_root=sdk_package, config_dir=profile, + provider="openai-compatible", features="builtin-only", + approval="callback", + ), + )) + try: + turn = session.start_turn("Review the repository without editing files.") + async for event in turn.events: + if event.type == "cline_permission": + await session.respond_approval(event.raw["id"], "reject") + return await turn.result + finally: + await session.close() +``` + +```typescript +import { openSession } from '@twaldin/harness-ts' + +async function review(workdir: string, packageRoot: string, configDir: string) { + const session = await openSession({ + harness: 'cline', backend: 'sdk', workdir, + clineSdk: { + packageRoot, configDir, provider: 'openai-compatible', + features: 'builtin-only', approval: 'callback', + }, + }) + try { + const turn = session.startTurn('Review the repository without editing files.') + for await (const event of turn.events) { + if (event.type === 'cline_permission' && typeof event.raw.id === 'string') { + await session.respondApproval(event.raw.id, 'reject') + } + } + return await turn.result + } finally { + await session.close() + } +} +``` + +Use absolute paths and a dedicated writable Cline profile with the selected +provider/model configured. Explicit `model` overrides that profile's model; +`executable` selects Node. The SDK remains local: no hub attachment/startup. +`features: "builtin-only"` deliberately excludes hooks, plugins, MCP, +subagents and other unqualified native extensions. Harness owns command +execution through the public bash hook and cancels command groups even when +the SDK worker dies. This is process ownership, not a sandbox. + +The example rejects observed tool requests. Use `"once"` only after your +application approves the unchanged native request. Omitted `approval` means +`"upstream"` and retains Cline SDK's **auto-approved defaults**; no interactive +permission guarantee is implied. Native events, exact resume and per-turn/ +cumulative usage remain distinct. See the +[Cline SDK contract and evidence limits](SPEC.md#optional-cline-sdk-sessions). + ### Caller-owned OpenCode HTTP sessions Select `harness="opencode", backend="rpc"` with an explicit `OpenCodeOptions` diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index 132127f..c0176ce 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -73,6 +73,12 @@ resume; interruption and disposal use the existing session lifecycle. Harness imports do not initialize OMP, and CLI behavior stays unchanged. See [SDK setup, runtime and qualification limits](SPEC.md#optional-omp-sdk-sessions). +- Added explicit local Cline SDK sessions in Python and TypeScript through an + owned Node >=22.14 worker using caller-installed `@cline/sdk` 0.0.82. + Harness owns command execution through the public bash hook; native events, + usage and exact session identity survive follow-up and resume. Escaping native + features are unsupported, with no CLI/ACP fallback. + See [SDK setup, ownership and qualification limits](SPEC.md#optional-cline-sdk-sessions). - Added typed Claude Code effort and Codex sandbox options, validated before file writes. Codex sandbox and bypass cannot be combined. - Aligned eager registry initialization, collision errors, model selection, diff --git a/SPEC.md b/SPEC.md index ae90a9c..2edec0f 100644 --- a/SPEC.md +++ b/SPEC.md @@ -1,6 +1,6 @@ # harness — specification -This is the shared contract for `harness` (Python) and `@twaldin/harness-ts` (TypeScript). It provides CLI command construction, one-shot execution, output parsing, Pi RPC, optional OMP/Amp SDK bridges, native Claude/Factory Droid SDK sessions, caller-owned OpenCode/OpenHands sessions, and externally hosted pane/log helpers. The [backend and session implementation gates](#backend-and-session-implementation-gates) apply to all controlled sessions. +This is the shared contract for `harness` (Python) and `@twaldin/harness-ts` (TypeScript). It provides CLI command construction, one-shot execution, output parsing, Pi RPC, optional OMP/Amp/Cline SDK bridges, native Claude/Factory Droid SDK sessions, caller-owned OpenCode/OpenHands sessions, and externally hosted pane/log helpers. The [backend and session implementation gates](#backend-and-session-implementation-gates) apply to all controlled sessions. **Repo layout (monorepo):** ``` @@ -1332,7 +1332,7 @@ explicitly; Harness does not install, change provider accounts, or fall back to another binary/backend. Older Pi protocols and OMP RPC are not interchangeable: OMP has different framing, acknowledgement and local-command completion rules. Other pairings reject with `unsupported-backend`, except the separately -documented OMP/Amp/Claude/Factory Droid SDK and caller-owned OpenCode/OpenHands sessions below. Unknown +documented OMP/Amp/Claude/Cline/Factory Droid SDK and caller-owned OpenCode/OpenHands sessions below. Unknown names still produce `unknown-harness`. ### Session inputs and capabilities @@ -1825,6 +1825,152 @@ Sources refreshed September 9: [pinned SDK package](https://www.npmjs.com/package/@ampcode/sdk/v/0.1.0-20260823161614-g3631dc6), [pinned CLI package](https://www.npmjs.com/package/@ampcode/cli/v/0.0.1788883237-g0b98e3). +## Optional Cline SDK sessions + +`open_session` / `openSession` accept `harness: "cline", backend: "sdk"`. +Both languages launch the same owned **Node >=22.14** worker. Python uses a +named Node bridge, not a native Python SDK. The official TypeScript-only +**`@cline/sdk@0.0.82`** embeds the Cline agent runtime; Harness does not replace +it with a raw model API. The existing Cline one-shot CLI adapter is unchanged. + +### Explicit dependencies, profile and permissions + +Both package roots export `ClineSdkOptions`, `ClineFeatures` and +`ClineApproval`. `SessionSpec.cline_sdk` / `clineSdk` is required for this +pairing and rejected on other harnesses: + +| TypeScript field | Selection | +|---|---| +| `packageRoot` | Absolute installed `@cline/sdk` package directory, not a project root | +| `configDir` | Absolute caller-selected, writable Cline profile root | +| `provider` | Nonempty native provider ID; never inferred from the model | +| `features` | Required literal `"builtin-only"`; other feature selections are unsupported | +| `approval` | `"upstream"` (default) or `"callback"` | + +Python uses `package_root` and `config_dir`. `SessionSpec.executable` selects +Node, default `node`, even when the caller runs under Bun. The SDK itself +declares Node >=22; this bridge needs Node >=22.14 for public ESM package +resolution. Startup verifies `@cline/sdk`, `@cline/core`, `@cline/shared`, +`@cline/agents` and `@cline/llms` are all 0.0.82 before SDK initialization. +Optional packages are not imported by ordinary CLI imports or capability +queries. Missing/unqualified dependencies fail with `launch-failed`; no +installation, CLI fallback, ACP fallback or permission bypass occurs. + +The worker pins `backendMode: "local"`: no hub discovery, prewarming, shared +hub attachment or spoke management. It pins native data/session/DB paths below +`configDir`, and owns a unique `TMPDIR` for the session. This also prevents +the SDK's startup detached-log reaper from inspecting other Cline processes' +temporary files. Conflicting caller-owned environment selections are rejected; +other `CLINE_*_DIR` / `CLINE_*_PATH` redirects are unsupported, and caller +`TMPDIR` is rejected. Inherited storage redirects are cleared in the worker. +The parent environment is never mutated. + +Use a dedicated native profile: Cline owns its writable state and may migrate +legacy provider settings or refresh selected credentials there. Harness does +not copy authentication, discover another profile, or call native session +listing/reconciliation APIs. `model` is passed verbatim; omission reads the +selected provider's model from this profile and fails if none is configured. +Native custom model IDs remain allowed, including upstream's metadata-free +unregistered-model behavior; no substitute model or catalog requirement is +invented. `CLINE_API_KEY`, when explicitly supplied in the child environment, +is passed to native configuration; otherwise native profile authentication +applies. No authenticated-provider success is implied by local execution. + +`approval: "upstream"` preserves the SDK's native **auto-approved defaults**; +it is not an interactive approval policy. `"callback"` explicitly sets the +native wildcard tool policy to require approval and publishes +`cline_permission` events with a bridge-local `id` and the complete native +request. Use `respondApproval(id, "once" | "reject")` / +`respond_approval(...)`. No persistent `"always"` rule, modified input or +bypass is supported. Pending requests are cancelled with their turn; stale or +duplicate replies fail. Approval responses in upstream mode are unsupported. +`permissionPolicy: "bypass"` is unsupported for this backend. + +### Supported native surface and owned commands + +`"builtin-only"` is an explicit restricted profile, not silent disabling of a +requested native configuration. It excludes file hooks, plugins, rules, +skills, workflows, MCP settings tools, subagents, teams, checkpoints and +caller-injected runtime extensions. The worker supplies +`localRuntime.configExtensions: []`, disables spawn/teams/MCP/checkpoints, +and exposes no arbitrary runtime-options channel. Native mode changes, +steering, forks, automation, command detachment and hub operations are not +available through this API. Native file hooks are not made safe by a shell +executor override: they are excluded independently. + +For `run_commands`, the public `capabilities.toolExecutors.bash` hook sends +native shell/argv invocations to the Harness parent. The parent uses the +existing shared subprocess runner, one recorded process group per command, +with native shell invocation semantics and streamed native tool updates. +At most 32 commands may be in flight. Commands have a 30-second wall deadline +and a 48,000-byte prefix cap **per output stream**, with explicit truncation +markers; this replaces native middle-truncation, not an unbounded capture. +Nonzero exits, cancellation, timeout and launch failures remain tool errors. + +Closing, a turn deadline, protocol failure or forced worker loss cancels and +awaits all parent-owned command groups alongside the worker group before +restoring instructions or releasing the workdir lease. Cleanup failure is +reported and retains ownership rather than implying successful disposal. +Ordinary native git/search/indexer children remain in the worker's owned +process group. As with other Harness subprocesses, deliberate descendant +`setsid`/`setpgid` escapes are unsupported; this is not an OS sandbox. +`instructions` uses the existing owned `CLINE.md` projection and native +`@./CLINE.md` mention, including follow-up turns. + +### Identity, events and results + +Capabilities report events, interrupt, follow-up, resume and approval true; +concurrent turns false. Resume requires the full returned reference: +`sessionFile` names the native manifest under the selected profile, not a +Pi/OMP JSONL header. Persistence may be lazy until the first turn. +The worker verifies native record/manifest ID, cwd/workspace, provider/model, +root-session flags and nonempty history before a read-only resume start with +no prompt. Unknown/corrupt/empty history, a different profile or workspace, +spawn/team-enabled history and a manifest still marked running are rejected. +No latest-session selection, silent fresh session or fork is substituted. +Returned native identity is checked again after start. + +Every complete native `CoreSessionEvent` wrapper is retained in `raw`, +including unknown types and fields; `agent_event.payload.event` contains the +agent event, while `chunk` may duplicate it. Native `Error` values are +serialized with name/message/stack instead of becoming empty JSON objects. +Callback notifications are the explicitly named bridge events above. +Only the resolution of native `core.send` settles the turn, not an +intermediate `done`, `ended`, status or chunk. Result `raw` is the native +`AgentResult`, including its unchanged `finishReason`: `completed` maps to +`completed`, `aborted` to `interrupted`, and `error`, `max_iterations` or +`mistake_limit` to `agent-error`. An unknown reason is a protocol error. +The latter two can accompany a native completed session status; Harness +classifies the turn from its finish reason, not that status. + +Turn usage in `AgentResult`/native done events is distinct from cumulative +manifest and snapshot usage. Repeated snapshots and duplicated chunks must +not be summed. Optional native cost, including reported zero, is retained +without estimation. Interrupt calls native abort, waits for tool cleanup and +settlement, and keeps the session available for follow-up; close disposes it. + +### Qualification boundary + +`tests/cline_sdk_cases.json` drives Python, Bun and packaged Node against the +real optional SDK with a finite synthetic loopback provider. A test-only +wrapper injects a future event, malformed wire data and worker self-termination +to exercise transport failure; it does not substitute the agent or provider +logic. Private native probes separately exercised the public bash override, +permission rejection, abort, exact resume and forced worker loss on +Node 22.22.2, with disposable configuration and synthetic credentials. +File-hook exclusion was checked with an enabled/disabled native hook marker. +These are native-runtime/synthetic-provider checks, **not authenticated +provider generation**; that evidence remains unavailable. + +The unmodified SDK shell executor and native ACP in CLI 3.0.61 both allowed +an ordinary foreground shell to survive forced parent-group cleanup. ACP also +drops native usage and unknown events. Neither is used as a fallback. +Sources: [SDK overview](https://docs.cline.bot/sdk/overview.md), +[permission policy](https://docs.cline.bot/sdk/guides/permission-handling.md), +[pinned local bootstrap](https://github.com/cline/cline/blob/595f1dbf2ea819e987afeadb4ed4dd9a0ae9a55e/sdk/packages/core/src/services/local-runtime-bootstrap.ts), +[native bash executor](https://github.com/cline/cline/blob/595f1dbf2ea819e987afeadb4ed4dd9a0ae9a55e/sdk/packages/core/src/extensions/tools/executors/bash.ts), +[ACP event translator](https://github.com/cline/cline/blob/cli-v3.0.61/apps/cli/src/acp/session-updates.ts). + ## Caller-owned OpenCode HTTP sessions `open_session` / `openSession` accept `harness: "opencode", backend: "rpc"`. @@ -2324,7 +2470,7 @@ Pinned sources: ## Backend and session implementation gates -These requirements govern the shipped Pi RPC, OMP/Amp/Claude/Factory Droid SDK +These requirements govern the shipped Pi RPC, OMP/Amp/Claude/Cline/Factory Droid SDK and caller-owned OpenCode/OpenHands implementations above and future backends. They keep the common library small; they do not enable additional SDKs or native protocols by themselves. @@ -2419,7 +2565,7 @@ historical hard-coded cache prices, a mandatory thin Session facade, automatic OAuth proxy configuration, consumer/fleet migrations, staged single-language API PRs or source-text/member-count tests as parity proof. CLI chunk streaming now ships under the execution contract above; controlled Pi RPC and optional -OMP/Amp/Claude/Factory Droid SDK and caller-owned OpenCode/OpenHands sessions +OMP/Amp/Claude/Cline/Factory Droid SDK and caller-owned OpenCode/OpenHands sessions ship through their explicit APIs. Additional protocols and SDKs remain implementation-gated. No package release is implied. @@ -2456,7 +2602,7 @@ Harness provides CLI command construction, output parsing and headless execution Optional agent SDK/protocol integrations are now in scope for the library. This supersedes the historical blanket SDK exclusion in CONTRIBUTING, not the -consumer-owned host/fleet boundary. The optional OMP/Amp SDK bridges, native +consumer-owned host/fleet boundary. The optional OMP/Amp/Cline SDK bridges, native Claude SDK sessions and caller-owned OpenCode/OpenHands clients implement the dependency and behavior requirements above. A raw model API, fleet manager, Linear engine or application is not an agent backend. diff --git a/pyproject.toml b/pyproject.toml index 37a0e11..95fc751 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "harness-cli" -version = "0.3.21" +version = "0.3.22" description = "Unified Python interface for invoking AI coding-agent CLIs (claude-code, opencode, codex, gemini, aider, swe-agent) as subprocesses." requires-python = ">=3.10" authors = [{ name = "Tim Waldin" }] diff --git a/src/harness/__init__.py b/src/harness/__init__.py index d8409af..ea96111 100644 --- a/src/harness/__init__.py +++ b/src/harness/__init__.py @@ -64,6 +64,9 @@ AmpVisibility, ClaudeSdkOptions, ClaudeSettingSource, + ClineApproval, + ClineFeatures, + ClineSdkOptions, FactoryDroidAutonomy, FactoryDroidCallback, FactoryDroidOptions, @@ -100,7 +103,10 @@ "ClaudeCodeOptions", "ClaudeSdkOptions", "ClaudeSettingSource", + "ClineApproval", + "ClineFeatures", "ClineOptions", + "ClineSdkOptions", "CodexOptions", "CopilotOptions", "ErrorCode", diff --git a/src/harness/_cline_sdk.mjs b/src/harness/_cline_sdk.mjs new file mode 100644 index 0000000..85e18c4 --- /dev/null +++ b/src/harness/_cline_sdk.mjs @@ -0,0 +1,257 @@ +// Owned Node bridge for the caller-installed Cline SDK. The Harness parent +// owns shell groups; this worker never launches a replacement shell itself. +import { readFileSync, realpathSync, statSync } from 'node:fs' +import { dirname, isAbsolute, join } from 'node:path' +import * as module from 'node:module' +import { createInterface } from 'node:readline' +import { pathToFileURL } from 'node:url' + +const MAX_BYTES = 1_048_576 +const options = JSON.parse(process.argv[2]) +let core, session, shared +let active = null +let closing = false +let shutdownPromise +let unsubscribe +let sequence = 0 +const shells = new Map() +const approvals = new Map() +const describe = error => error instanceof Error ? `${error.name}: ${error.message}` : String(error) +const object = value => value !== null && typeof value === 'object' && !Array.isArray(value) +const samePath = (a, b) => typeof a === 'string' && typeof b === 'string' && isAbsolute(a) && isAbsolute(b) && (a === b || realpathSync(a) === realpathSync(b)) + +// SDK diagnostics must not enter the strict JSONL protocol. +for (const name of ['log', 'info', 'warn', 'error', 'debug']) { + console[name] = (...values) => process.stderr.write(`${values.map(describe).join(' ')}\n`) +} +function send(frame) { + if (closing) return + const line = `${JSON.stringify(frame, (_key, value) => value instanceof Error ? { ...value, name: value.name, message: value.message, stack: value.stack } : value)}\n` + const bytes = Buffer.byteLength(line) + if (bytes > MAX_BYTES || process.stdout.writableLength + bytes > MAX_BYTES) { + throw new Error('Cline SDK output exceeded the 1 MiB transport bound') + } + process.stdout.write(line) +} +function response(request, success, data) { + send({ type: 'response', id: request.id, command: request.type, success, + ...(success ? { data } : { error: describe(data) }) }) +} +function readObject(file) { + if (statSync(file).size > MAX_BYTES) throw new Error('Cline metadata exceeds the 1 MiB bound') + const value = JSON.parse(readFileSync(file, 'utf8')) + if (!object(value)) throw new Error('Cline metadata is not an object') + return value +} +function packageEntry(root, name) { + const metadata = readObject(join(root, 'package.json')) + if (metadata.name !== name || metadata.version !== '0.0.82') { + throw new Error(`Cline SDK requires ${name} 0.0.82`) + } + const entry = metadata.exports?.['.']?.import ?? metadata.main + if (typeof entry !== 'string') throw new Error(`${name} has no import entry point`) + return join(root, entry) +} +function dependencyEntry(name, parent) { + const metadata = module.findPackageJSON(name, pathToFileURL(parent).href) + if (!metadata) throw new Error(`Cline SDK dependency ${name} is not installed`) + return packageEntry(dirname(metadata), name) +} + +async function bash(command, cwd, context) { + if (closing || context.signal?.aborted) throw new Error('Cline command aborted before launch') + const direct = typeof command !== 'string' && 'args' in command + const shell = shared.getDefaultShell(process.platform) + const invocation = direct ? { args: command.args ?? [] } : shared.getShellInvocation(shell, typeof command === 'string' ? command : command.command) + const cmd = [direct ? command.command : shell, ...invocation.args] + const id = `shell-${++sequence}` + const cancel = () => send({ type: 'shell_cancel', id }) + try { + return await new Promise((resolve, reject) => { + shells.set(id, { resolve, reject, context }) + context.signal?.addEventListener('abort', cancel, { once: true }) + send({ type: 'shell_request', id, cmd, cwd, stdin: invocation.input ?? null }) + if (context.signal?.aborted) cancel() + }) + } finally { + context.signal?.removeEventListener('abort', cancel) + shells.delete(id) + } +} +function requestApproval(request) { + if (closing) return { approved: false, reason: 'Session closing' } + const id = `permission-${++sequence}` + return new Promise(resolve => { + approvals.set(id, resolve) + send({ type: 'sdk_event', event: { type: 'cline_permission', id, request } }) + }) +} +function cancelApprovals() { + for (const [id, resolve] of approvals) { + resolve({ approved: false, reason: 'Session interrupted' }) + send({ type: 'sdk_event', event: { type: 'cline_permission_cancelled', id } }) + } + approvals.clear() +} + +async function initialize() { + const [major, minor] = process.versions.node.split('.').map(Number) + if (typeof Bun !== 'undefined' || major < 22 || major === 22 && minor < 14 || typeof module.findPackageJSON !== 'function') { + throw new Error('Cline SDK bridge requires Node >=22.14; select executable explicitly') + } + if (options.features !== 'builtin-only') throw new Error('Cline SDK supports only explicit builtin-only features') + if (!['upstream', 'callback'].includes(options.approval)) throw new Error('Unsupported Cline approval selection') + for (const name of ['packageRoot', 'configDir', 'cwd', 'tempDir']) { + if (typeof options[name] !== 'string' || !isAbsolute(options[name])) throw new Error(`Cline ${name} must be absolute`) + } + // Every native storage override is confined to the selected profile. In + // particular, the local host's detached-log recovery must see only our temp. + const data = join(options.configDir, 'data') + for (const name of Object.keys(process.env)) { + if (name.startsWith('CLINE_') && (name.endsWith('_DIR') || name.endsWith('_PATH'))) delete process.env[name] + } + Object.assign(process.env, { + CLINE_DIR: options.configDir, CLINE_DATA_DIR: data, + CLINE_SESSION_DATA_DIR: join(data, 'sessions'), CLINE_DB_DATA_DIR: join(data, 'db'), + CLINE_SESSION_BACKEND_MODE: 'local', CLINE_NO_AUTO_UPDATE: '1', CLINE_RUN_AS_HUB_DAEMON: '0', + TMPDIR: options.tempDir, + }) + const entry = packageEntry(options.packageRoot, '@cline/sdk') + const coreEntry = dependencyEntry('@cline/core', entry) + const sharedEntry = dependencyEntry('@cline/shared', coreEntry) + dependencyEntry('@cline/agents', coreEntry) + dependencyEntry('@cline/llms', coreEntry) + const sdk = await import(pathToFileURL(entry).href) + shared = await import(pathToFileURL(sharedEntry).href) + // Explicit identity avoids upstream machine-id discovery or persistence. + core = await sdk.ClineCore.create({ backendMode: 'local', distinctId: 'harness', + ...(options.approval === 'callback' ? { toolPolicies: { '*': { autoApprove: false } } } : {}), + capabilities: { toolExecutors: { bash }, ...(options.approval === 'callback' ? { requestToolApproval: requestApproval } : {}) }, + }) + const settings = new sdk.ProviderSettingsManager({ filePath: sdk.resolveProviderSettingsPath() }) + const stored = settings.getProviderSettings(options.provider) + const model = options.model ?? stored?.model + if (typeof model !== 'string' || !model.trim()) throw new Error('Select a Cline model explicitly or in the selected provider profile') + let manifest, initialMessages, initialCompactionState + const resume = options.resume + if (resume !== null) { + if (!/^[0-9A-Za-z_-]+$/.test(resume.sessionId)) throw new Error('Invalid full native Cline session ID') + const expected = join(data, 'sessions', resume.sessionId, `${resume.sessionId}.json`) + if (!samePath(expected, resume.sessionFile)) throw new Error('Cline resume manifest is outside the selected session profile') + manifest = readObject(resume.sessionFile) + if (manifest.session_id !== resume.sessionId || !samePath(manifest.cwd, options.cwd) || !samePath(manifest.workspace_root, options.cwd)) { + throw new Error('Cline resume manifest has a different native ID or workdir') + } + if (manifest.enable_spawn !== false || manifest.enable_teams !== false || manifest.status === 'running' || manifest.provider !== options.provider || manifest.model !== model) { + throw new Error('Cline resume profile, model or active-session state is unsupported') + } + const record = await core.get(resume.sessionId) + if (!record || record.sessionId !== resume.sessionId || !samePath(record.cwd, options.cwd) || !samePath(record.workspaceRoot, options.cwd) || record.isSubagent || record.parentSessionId) throw new Error('Unknown or inconsistent native Cline root session record') + const messagesPath = join(dirname(expected), `${resume.sessionId}.messages.json`) + if (!samePath(manifest.messages_path, messagesPath) || !samePath(record.messagesPath, messagesPath)) throw new Error('Cline resume messages are outside the selected session') + const messageFile = readObject(messagesPath) + if (messageFile.sessionId !== resume.sessionId || messageFile.agent !== 'lead' || !Array.isArray(messageFile.messages) || messageFile.messages.length === 0) throw new Error('Cline resume requires readable nonempty native root history') + initialMessages = messageFile.messages + initialCompactionState = await core.readSessionCompactionState(resume.sessionId) + } + unsubscribe = core.subscribe(event => { + try { send({ type: 'sdk_event', event }) } catch (error) { void shutdown(error) } + }) + session = await core.start({ interactive: true, + // No prompt on start: otherwise upstream can overwrite a resumed manifest. + ...(resume === null ? {} : { initialMessages, initialCompactionState }), + localRuntime: { configExtensions: [] }, + config: { + ...(resume === null ? {} : { sessionId: resume.sessionId }), + cwd: options.cwd, workspaceRoot: options.cwd, providerId: options.provider, modelId: model, + ...(process.env.CLINE_API_KEY ? { apiKey: process.env.CLINE_API_KEY } : {}), + systemPrompt: sdk.getClineDefaultSystemPrompt({ rootPath: options.cwd, providerId: options.provider, planModeSwitchTool: false }), + enableTools: true, enableSpawnAgent: false, enableAgentTeams: false, disableMcpSettingsTools: true, + checkpoint: { enabled: false }, + }, + }) + if (!session || !samePath(session.manifest.cwd, options.cwd) || !samePath(session.manifest.workspace_root, options.cwd) || resume !== null && session.sessionId !== resume.sessionId) { + throw new Error('Cline started a different native session or workdir') + } + const expected = join(data, 'sessions', session.sessionId, `${session.sessionId}.json`) + if (!samePath(session.manifestPath, expected)) throw new Error('Cline selected an unexpected native manifest location') +} + +function shellFrame(frame) { + const pending = shells.get(frame.id) + if (!pending) throw new Error('Unknown Cline shell response ID') + if (frame.type === 'shell_output') { + if (!['stdout', 'stderr'].includes(frame.stream) || typeof frame.chunk !== 'string') throw new Error('Malformed Cline shell output') + pending.context.emitUpdate?.({ stream: frame.stream, chunk: frame.chunk }) + } else { + if (typeof frame.output !== 'string' || frame.error !== undefined && typeof frame.error !== 'string') throw new Error('Malformed Cline shell result') + shells.delete(frame.id) + frame.error === undefined ? pending.resolve(frame.output) : pending.reject(new Error(`${frame.error}\n${frame.output}`)) + } +} +async function command(request) { + if (!object(request) || typeof request.id !== 'string' || typeof request.type !== 'string') throw new Error('Malformed Cline worker request') + if (request.type === 'shell_output' || request.type === 'shell_result') { shellFrame(request); return } + await opening + if (closing) return + if (request.type === 'get_state') { + response(request, true, { sessionId: session.sessionId, sessionFile: session.manifestPath, workdir: session.manifest.cwd, isStreaming: active !== null }) + } else if (request.type === 'prompt') { + if (active || typeof request.message !== 'string') throw new Error('Invalid or concurrent Cline prompt') + response(request, true, {}) + active = (async () => { + let result, error + try { + const prompt = options.instructions ? `Follow the instructions in @./CLINE.md\n\n${request.message}` : request.message + result = await core.send({ sessionId: session.sessionId, prompt }) + if (!object(result)) throw new Error('Cline returned no native turn result') + } catch (cause) { error = describe(cause) } + active = null + cancelApprovals() + send({ type: 'sdk_settled', ...(error === undefined ? { result } : { error }) }) + })() + active.catch(error => { void shutdown(error) }) + } else if (request.type === 'abort') { + try { cancelApprovals(); await core.abort(session.sessionId); await active; response(request, true, {}) } + catch (error) { response(request, false, error) } + } else if (request.type === 'approval') { + const resolve = approvals.get(request.approvalId) + if (!resolve || !['once', 'reject'].includes(request.response)) { response(request, false, 'Unknown or invalid Cline approval'); return } + approvals.delete(request.approvalId) + resolve({ approved: request.response === 'once', ...(request.response === 'reject' ? { reason: 'Rejected by Harness caller' } : {}) }) + response(request, true, {}) + } else throw new Error(`Unsupported Cline worker request: ${request.type}`) +} +function drain(stream) { + return new Promise(resolve => { if (stream.destroyed) resolve(); else stream.write('', resolve) }) +} +function shutdown(error) { + if (shutdownPromise) return shutdownPromise + closing = true + unsubscribe?.() + shutdownPromise = (async () => { + let failed = Boolean(error) + if (error) process.stderr.write(`Cline SDK bridge: ${describe(error)}\n`) + cancelApprovals() + for (const pending of shells.values()) pending.reject(new Error('Cline session closing')) + shells.clear() + try { await opening } catch (cause) { failed = true; if (!error) process.stderr.write(`Cline startup: ${describe(cause)}\n`) } + try { unsubscribe?.(); await core?.dispose(); await active } catch (cause) { failed = true; process.stderr.write(`Cline disposal: ${describe(cause)}\n`) } + await Promise.all([drain(process.stdout), drain(process.stderr)]) + process.exit(failed ? 1 : 0) + })() + return shutdownPromise +} +const opening = initialize() +opening.catch(error => { void shutdown(error) }) +const input = createInterface({ input: process.stdin, crlfDelay: Infinity }) +input.on('line', line => { + if (closing) return + try { + if (Buffer.byteLength(line) > MAX_BYTES) throw new Error('Cline worker request exceeds 1 MiB') + void command(JSON.parse(line)).catch(error => shutdown(error)) + } catch (error) { void shutdown(error) } +}) +input.on('close', () => { void shutdown() }) +process.on('SIGTERM', () => { void shutdown() }) +process.on('SIGINT', () => { void shutdown() }) diff --git a/src/harness/sessions.py b/src/harness/sessions.py index 717837a..62ef620 100644 --- a/src/harness/sessions.py +++ b/src/harness/sessions.py @@ -2,7 +2,7 @@ OpenCode / OpenHands server driven over HTTP plus a live event stream, or Amp threads driven through one-shot workers. -Seven (harness, backend) pairs share one public surface: +Eight (harness, backend) pairs share one public surface: - `pi` / `rpc`: `pi --mode rpc`, the native JSONL protocol on stdio. - `omp` / `sdk`: an owned Bun child running the sibling `_omp_sdk.mjs` bridge, @@ -39,6 +39,15 @@ - `amp` / `sdk`: one finite Node worker (`_amp_sdk.mjs`) per operation, loading the caller-installed `@ampcode/sdk` from `AmpSdkOptions.package_root` and driving the pinned native CLI at `AmpSdkOptions.cli_path`. See `_amp`. +- `cline` / `sdk`: an owned Node child running the sibling `_cline_sdk.mjs` + worker, which loads the caller-installed `@cline/sdk` from + `ClineSdkOptions.package_root` and drives one local `ClineCore` session + under the caller-selected `config_dir`. Same framing as the OMP bridge + plus: every native `CoreSessionEvent` arrives as `sdk_event`, `sdk_settled` + carries the native `AgentResult`, permission prompts surface as + `cline_permission` events (only with `approval="callback"`) answered with + `respond_approval`, and every native command runs *here*, in a + parent-owned process group, in answer to a `shell_request` frame. `open_session(spec)` spawns the child in its own POSIX process group (or opens the HTTP transport), completes the identity handshake and returns a @@ -79,15 +88,19 @@ import math import os import re +import shutil import signal import sys +import tempfile +import threading from collections import deque from dataclasses import dataclass, field, replace from pathlib import Path, PurePath -from typing import Awaitable, Callable, Coroutine, Literal, Sequence +from typing import Awaitable, Callable, Coroutine, Literal, Sequence, cast from urllib.parse import urlsplit from harness._instructions import PreparedCommand, cleanup_command, prepare_command +from harness._subproc import SubprocOutcome, run_subprocess_async from harness.base import ( BACKENDS, PERMISSION_POLICIES, @@ -95,6 +108,7 @@ BuildCommand, ErrorCode, HarnessError, + OutputStream, PermissionPolicy, absolute_workdir, ) @@ -134,6 +148,14 @@ SUPPORTED_AMP_SDK_DISTRIBUTION = "@ampcode/sdk 0.1.0-20260823161614-g3631dc6" #: Native Amp CLI the worker verifies through `--version` before any thread call. SUPPORTED_AMP_CLI_VERSION = "0.0.1788883237-g0b98e3" +#: Exact `@cline/sdk` distribution the `_cline_sdk.mjs` worker loads from +#: `ClineSdkOptions.package_root`; any other is `launch-failed`. +SUPPORTED_CLINE_SDK_DISTRIBUTION = "@cline/sdk 0.0.82" +#: Node the Cline worker requires: `module.findPackageJSON` (public since +#: 22.14) resolves the caller's exact ESM-only native transitives without a +#: deep-import bypass. The worker rejects an older runtime (and Bun); +#: nothing is probed here. +MINIMUM_CLINE_NODE_VERSION = "22.14.0" #: Endpoint used when neither `spec.env` nor the process env sets `AMP_URL`. DEFAULT_AMP_ENDPOINT = "https://ampcode.com" #: Exact `droid_sdk` package version the factory-droid sdk backend drives; @@ -158,9 +180,10 @@ _DRAIN_BUDGET = 1.0 #: Qualified (harness, backend) pairs; every other combination is rejected. #: `openhands` is session-only: it has no CLI adapter and no one-shot run. -_SESSION_BACKENDS: dict[str, Backend] = {"pi": "rpc", "omp": "sdk", "opencode": "rpc", "openhands": "rpc", "claude-code": "sdk", "amp": "sdk", "factory-droid": "sdk"} +_SESSION_BACKENDS: dict[str, Backend] = {"pi": "rpc", "omp": "sdk", "opencode": "rpc", "openhands": "rpc", "claude-code": "sdk", "amp": "sdk", "cline": "sdk", "factory-droid": "sdk"} _SDK_WORKER = Path(__file__).with_name("_omp_sdk.mjs") _CLAUDE_WORKER = Path(__file__).with_name("_claude_sdk.py") +_CLINE_WORKER = Path(__file__).with_name("_cline_sdk.mjs") #: Child environment the SDK bridge owns (both pinned to `agent_dir` so the #: selected profile is also the config root); conflicting caller entries are #: rejected. `bun --no-env-file` only silences Bun's own dotenv loading; the @@ -169,6 +192,31 @@ #: Child environment the Claude worker owns (pinned to `config_dir`); a #: conflicting caller entry is rejected. The worker inherits everything else. _CLAUDE_OWNED_ENV = "CLAUDE_CONFIG_DIR" +#: Child environment the Cline worker owns: the caller's Cline root, its +#: data / session / database directories, the local session backend, the +#: update check and the hub daemon switch. The worker re-pins all of them +#: (and drops every other inherited `CLINE_*_DIR` / `CLINE_*_PATH`) before +#: the SDK import; a conflicting caller entry is rejected. `TMPDIR` is owned +#: too, but its value is the parent-owned per-session directory, so any +#: caller entry for it is rejected outright. +_CLINE_OWNED_ENV = ( + "CLINE_DIR", + "CLINE_DATA_DIR", + "CLINE_SESSION_DATA_DIR", + "CLINE_DB_DATA_DIR", + "CLINE_SESSION_BACKEND_MODE", + "CLINE_NO_AUTO_UPDATE", + "CLINE_RUN_AS_HUB_DAEMON", +) +#: Wall-clock cap on one parent-owned native command (the native bash default). +_CLINE_COMMAND_TIMEOUT_SECONDS = 30.0 +#: Capture cap per stream for one parent-owned native command. +_CLINE_COMMAND_MAX_OUTPUT_BYTES = 48_000 +#: Concurrent `shell_request` commands the worker may keep outstanding. +_CLINE_MAX_ACTIVE_COMMANDS = 32 +#: Budget for the bounded cleanup of cancelled commands during teardown; the +#: shared runner escalates SIGTERM -> SIGKILL and drains inside it. +_CLINE_COMMAND_CLEANUP_BUDGET = 2.0 #: Child environment the Factory SDK stamps on its process (client type and #: `language/version` identity); caller entries are rejected, never overlaid. _FACTORY_OWNED_ENV = ("FACTORY_UPSTREAM_CLIENT_TYPE", "FACTORY_UPSTREAM_SDK") @@ -213,6 +261,13 @@ AmpVisibility = Literal["private", "unlisted", "workspace", "group"] _AMP_EFFORTS: tuple[str, ...] = ("none", "minimal", "low", "medium", "high", "xhigh", "max") _AMP_VISIBILITIES: tuple[str, ...] = ("private", "unlisted", "workspace", "group") +#: Native surface the Cline worker may use: the builtin tools and nothing +#: else (no hooks, plugins, rules, skills, workflows, MCP servers, +#: subagents, teams, checkpoints or detached commands). +ClineFeatures = Literal["builtin-only"] +#: "upstream" keeps the native auto-approval configuration; "callback" +#: routes every native tool approval to `LiveSession.respond_approval`. +ClineApproval = Literal["upstream", "callback"] # ── public types ──────────────────────────────────────────────────────────── @@ -231,6 +286,7 @@ class SessionReference: OpenCode and Amp, whose history lives on a server. Claude: None until a native hook reports transcript_path; the exact supplied transcript on resume. + Cline: the native session manifest path. `workdir` — absolute directory the session was opened in. For OpenCode this is the literal server-side directory. `endpoint` — normalized OpenCode server or Amp service origin @@ -299,6 +355,42 @@ class ClaudeSdkOptions: settings_file: Path | None = None +@dataclass(frozen=True) +class ClineSdkOptions: + """Where the `cline` / `sdk` worker finds the caller's SDK and Cline root + and which native surface it may use. Nothing is guessed, discovered, + installed or copied. + + `package_root` — absolute directory of the caller-installed `@cline/sdk` + package the worker imports; the worker requires exactly + `SUPPORTED_CLINE_SDK_DISTRIBUTION` and resolves the + native transitives from it. + `config_dir` — absolute caller-selected Cline root, exported as + `CLINE_DIR` with the data / session / database + directories pinned under `config_dir/data`. + `provider` — non-blank native provider ID, passed verbatim. With + `SessionSpec.model` unset the worker uses that + provider's configured model and fails when it has none; + a selected model ID is never normalized or mapped. + `features` — only "builtin-only": native hooks, plugins, rules, + skills, workflows, MCP servers, subagents, teams, + checkpoints and command detachment are switched off + explicitly, never left to a default. Any other value is + `unsupported-capability`. + `approval` — "upstream" leaves the native auto-approval configuration + untouched (no implicit bypass); "callback" clears native + auto-approval for every tool and surfaces each request + as a `cline_permission` event answered with + `LiveSession.respond_approval`. + """ + + package_root: Path + config_dir: Path + provider: str + features: ClineFeatures + approval: ClineApproval = "upstream" + + @dataclass(frozen=True) class OpenCodeOptions: """Where the caller's `opencode serve` instance is and how to authenticate. @@ -431,8 +523,8 @@ class FactoryDroidOptions: class SessionSpec: """Everything needed to open a live session. - `harness` — Pi/OpenCode/OpenHands (rpc), OMP/Amp/Claude/Factory - Droid (sdk). Other registered harnesses raise + `harness` — Pi/OpenCode/OpenHands (rpc), OMP/Amp/Claude/Cline/ + Factory Droid (sdk). Other registered harnesses raise `unsupported-backend`; unknown names `unknown-harness`. OpenHands is session-only, with no CLI adapter. `workdir` — cwd for the child (absolute against the process cwd). @@ -442,7 +534,9 @@ class SessionSpec: Noncanonical forms are `invalid-options`. `backend` — rpc or sdk as qualified above; cli and other pairings raise `unsupported-backend`. - `model` — trimmed native selector for Pi / OMP / Claude; + `model` — trimmed native selector for Pi / OMP / Claude; Cline + passes the ID through exactly and falls back to the + selected provider profile's model when unset. Factory sends it as the native `modelId` and rejects it with `resume` (the saved model is restored); OpenCode requires provider/model. None preserves @@ -457,17 +551,25 @@ class SessionSpec: `CLAUDE_CONFIG_DIR` (config_dir). Conflicting explicit entries reject. Amp reads AMP_URL from the overlay or inherited environment and pins AMP_SKIP_UPDATE_CHECK=1. + Cline owns `CLINE_DIR`, the data / session / database + directories under it, `CLINE_SESSION_BACKEND_MODE`, + `CLINE_NO_AUTO_UPDATE`, `CLINE_RUN_AS_HUB_DAEMON` and + `TMPDIR` (the parent-owned per-session directory; a + caller entry is rejected). The commands the parent + runs for the Cline worker get the same overlay. Factory requires a nonempty FACTORY_API_KEY before spawn and protects native SDK attribution. OpenCode/OpenHands reject nonempty env. `executable` — bare binary name or absolute path; default "pi" for - pi, "bun" for omp, "node" for amp, "droid" for Factory, - and sys.executable for the Claude SDK worker (not its - native CLI). OpenCode/OpenHands reject it. + pi, "bun" for omp, "node" for amp and cline, "droid" + for Factory, and sys.executable for the Claude SDK + worker (not its native CLI). OpenCode/OpenHands + reject it. `permission_policy` — only "upstream"; "bypass" raises `unsupported-capability`. `instructions` — projected to the adapter's instructions file (`AGENTS.md`; `CLAUDE.md` for claude-code, which - requires "project" in `claude_sdk.setting_sources`) + requires "project" in `claude_sdk.setting_sources`; + `CLINE.md` for cline) under the workdir lease for the life of the process tree. Rejected for OpenCode / OpenHands (no local filesystem to project into). @@ -493,10 +595,15 @@ class SessionSpec: created when missing). Amp requires `session_file=None`, the same `workdir`, the computed `AMP_URL` endpoint and a full `T-` UUID, verified through the SDK before - the first turn. + the first turn. Cline requires an existing absolute + native manifest and the same workdir; no Pi/OMP header + is expected (the worker verifies the native manifest + identity, path and cwd before and after the native + start). `omp_sdk` — required with harness "omp", rejected otherwise. `claude_sdk` — required with harness "claude-code", rejected otherwise. `amp_sdk` — required with harness "amp", rejected otherwise. + `cline_sdk` — required with harness "cline", rejected otherwise. `factory_droid` — optional with harness "factory-droid" (None = native defaults), rejected otherwise. `opencode` — required with harness "opencode", rejected otherwise. @@ -535,6 +642,7 @@ class SessionSpec: openhands: OpenHandsOptions | None = None amp_sdk: AmpSdkOptions | None = None claude_sdk: ClaudeSdkOptions | None = None + cline_sdk: ClineSdkOptions | None = None factory_droid: FactoryDroidOptions | None = None @@ -558,11 +666,12 @@ class SessionEvent: or amp_event (Amp), Factory inner notification / request-response envelope, OpenCode SSE object, or the whole OpenHands session-socket envelope (`{"type", "seq"?, "event"?, ...}`). Claude also reports claude_permission / - claude_permission_cancelled / claude_session_hook / claude_interrupt; - permission request_id is explicitly bridge-local.""" + claude_permission_cancelled / claude_session_hook / claude_interrupt, and + Cline cline_permission / cline_permission_cancelled; permission request_id + is explicitly bridge-local.""" backend: Literal["rpc", "sdk"] - harness: Literal["pi", "omp", "opencode", "openhands", "claude-code", "amp", "factory-droid"] + harness: Literal["pi", "omp", "opencode", "openhands", "claude-code", "amp", "cline", "factory-droid"] session_id: str turn_id: str | None request_id: str | None @@ -697,7 +806,7 @@ def get_session_capabilities(name: str, backend: Backend = "rpc") -> SessionCapa follow_up=True, resume=True, concurrent_turns=False, - approval=name in ("opencode", "claude-code"), + approval=name in ("opencode", "claude-code", "cline"), ) @@ -1003,9 +1112,12 @@ def _validate_session_spec(spec: SessionSpec) -> SessionSpec: raise HarnessError(f"resume.session_id {resume.session_id!r} must be a Claude session UUID", code="invalid-options") omp_sdk = _validate_omp_sdk(spec) claude_sdk = _validate_claude_sdk(spec) + cline_sdk = _validate_cline_sdk(spec) if spec.harness == "claude-code" and not spec.executable and not sys.executable: raise HarnessError("claude-code sdk sessions need executable: sys.executable is empty in this interpreter", code="invalid-options") - return replace(spec, workdir=workdir, model=model, env=dict(spec.env), resume=resume, omp_sdk=omp_sdk, claude_sdk=claude_sdk) + return replace( + spec, workdir=workdir, model=model, env=dict(spec.env), resume=resume, omp_sdk=omp_sdk, claude_sdk=claude_sdk, cline_sdk=cline_sdk + ) def _validate_factory_droid_spec(spec: SessionSpec, model: str | None) -> SessionSpec: @@ -1017,6 +1129,8 @@ def _validate_factory_droid_spec(spec: SessionSpec, model: str | None) -> Sessio raise HarnessError("claude_sdk applies only to claude-code sdk sessions, not factory-droid sdk", code="invalid-options") if spec.amp_sdk is not None: raise HarnessError("amp_sdk applies only to amp sdk sessions, not factory-droid sdk", code="invalid-options") + if spec.cline_sdk is not None: + raise HarnessError("cline_sdk applies only to cline sdk sessions, not factory-droid sdk", code="invalid-options") options = _validate_factory_droid_options(spec.factory_droid) for key in _FACTORY_OWNED_ENV: if key in spec.env: @@ -1085,6 +1199,8 @@ def _validate_opencode_spec(spec: SessionSpec, model: str | None) -> SessionSpec raise HarnessError("claude_sdk applies only to claude-code sdk sessions, not opencode rpc", code="invalid-options") if spec.amp_sdk is not None: raise HarnessError("amp_sdk applies only to amp sdk sessions, not opencode rpc", code="invalid-options") + if spec.cline_sdk is not None: + raise HarnessError("cline_sdk applies only to cline sdk sessions, not opencode rpc", code="invalid-options") if spec.factory_droid is not None: raise HarnessError("factory_droid applies only to factory-droid sdk sessions, not opencode rpc", code="invalid-options") if model is not None: @@ -1115,6 +1231,8 @@ def _validate_openhands_spec(spec: SessionSpec, model: str | None) -> SessionSpe raise HarnessError("amp_sdk applies only to amp sdk sessions, not openhands rpc", code="invalid-options") if spec.opencode is not None: raise HarnessError("opencode applies only to opencode rpc sessions, not openhands rpc", code="invalid-options") + if spec.cline_sdk is not None: + raise HarnessError("cline_sdk applies only to cline sdk sessions, not openhands rpc", code="invalid-options") if spec.factory_droid is not None: raise HarnessError("factory_droid applies only to factory-droid sdk sessions, not openhands rpc", code="invalid-options") if model is None: @@ -1243,6 +1361,75 @@ def _validate_claude_sdk(spec: SessionSpec) -> ClaudeSdkOptions | None: ) +def _cline_owned_env(config_dir: Path) -> dict[str, str]: + """The Cline child environment the worker re-pins before the SDK import; + the parent exports the same values to the worker and to every command it + runs on the worker's behalf.""" + data = config_dir / "data" + return { + "CLINE_DIR": str(config_dir), + "CLINE_DATA_DIR": str(data), + "CLINE_SESSION_DATA_DIR": str(data / "sessions"), + "CLINE_DB_DATA_DIR": str(data / "db"), + "CLINE_SESSION_BACKEND_MODE": "local", + "CLINE_NO_AUTO_UPDATE": "1", + "CLINE_RUN_AS_HUB_DAEMON": "0", + } + + +def _validate_cline_sdk(spec: SessionSpec) -> ClineSdkOptions | None: + """Snapshot `spec.cline_sdk`: required for cline, rejected otherwise. + The native surface is an explicit selection, not a narrowed default, and + the environment the session owns cannot be supplied by the caller. A + missing / wrong-version SDK or runtime is the worker's startup failure + (`launch-failed`).""" + options = spec.cline_sdk + if spec.harness != "cline": + if options is not None: + raise HarnessError(f"cline_sdk applies only to cline sdk sessions, not {spec.harness} {spec.backend}", code="invalid-options") + return None + if not isinstance(options, ClineSdkOptions): + raise HarnessError( + "cline sdk sessions require cline_sdk=ClineSdkOptions(package_root, config_dir, provider, features)", + code="invalid-options", + ) + package_root = _absolute_option("cline_sdk.package_root", options.package_root) + config_dir = _absolute_option("cline_sdk.config_dir", options.config_dir) + provider = options.provider + if not isinstance(provider, str) or not provider.strip() or "\0" in provider: + raise HarnessError("cline_sdk.provider must be a non-blank native provider ID without NUL bytes", code="invalid-options") + if options.features != "builtin-only": + raise HarnessError( + f"cline_sdk.features must be 'builtin-only', got {options.features!r}; native hooks, plugins, rules, skills, " + "workflows, MCP servers, subagents, teams, checkpoints and command detachment have no owned mapping", + code="unsupported-capability", + ) + if options.approval not in ("upstream", "callback"): + raise HarnessError(f"cline_sdk.approval must be 'upstream' or 'callback', got {options.approval!r}", code="invalid-options") + owned = _cline_owned_env(config_dir) + for key in _CLINE_OWNED_ENV: + if key in spec.env and spec.env[key] != owned[key]: + raise HarnessError(f"env[{key!r}]={spec.env[key]!r} conflicts with the value {owned[key]!r} the cline worker owns; omit it", code="invalid-options") + for key in spec.env: + if key.startswith("CLINE_") and (key.endswith("_DIR") or key.endswith("_PATH")) and key not in owned: + raise HarnessError( + f"env[{key!r}] is a native Cline storage override; the worker drops every unowned CLINE_*_DIR / CLINE_*_PATH " + "before the SDK import so nothing is written outside the selected profile, so it has no mapping here", + code="unsupported-capability", + ) + if "TMPDIR" in spec.env: + raise HarnessError( + "env['TMPDIR'] is owned by the session: each cline session runs the worker and its commands in its own temporary directory; omit it", + code="invalid-options", + ) + return ClineSdkOptions( + package_root=package_root, + config_dir=config_dir, + provider=provider, + features="builtin-only", + approval=options.approval, + ) + def _amp_endpoint(env: dict[str, str]) -> str: """Origin the Amp CLI will talk to: `AMP_URL` from `env`, else the inherited process environment, else `DEFAULT_AMP_ENDPOINT`; normalized @@ -1315,6 +1502,8 @@ def _validate_amp_spec(spec: SessionSpec, model: str | None) -> SessionSpec: raise HarnessError("claude_sdk applies only to claude-code sdk sessions, not amp sdk", code="invalid-options") if spec.omp_sdk is not None: raise HarnessError("omp_sdk applies only to omp sdk sessions, not amp sdk", code="invalid-options") + if spec.cline_sdk is not None: + raise HarnessError("cline_sdk applies only to cline sdk sessions, not amp sdk", code="invalid-options") if model is not None: raise HarnessError("model is not supported for amp; amp_sdk.mode selects routing", code="invalid-options") skip = spec.env.get("AMP_SKIP_UPDATE_CHECK") @@ -1331,7 +1520,7 @@ def _validate_amp_spec(spec: SessionSpec, model: str | None) -> SessionSpec: _DROID_EXEC_ARGS = ("exec", "--input-format", "stream-jsonrpc", "--output-format", "stream-jsonrpc") -def _build(spec: SessionSpec) -> BuildCommand: +def _build(spec: SessionSpec, temp_dir: Path | None = None) -> BuildCommand: adapter_cls = _adapter_class(spec.harness) env = dict(spec.env) resume = None @@ -1363,6 +1552,24 @@ def _build(spec: SessionSpec) -> BuildCommand: cmd = spec.executable or sys.executable args = [str(_CLAUDE_WORKER), json.dumps(launch)] env[_CLAUDE_OWNED_ENV] = str(spec.claude_sdk.config_dir) + elif spec.harness == "cline": + assert spec.cline_sdk is not None and temp_dir is not None + launch = { + "packageRoot": str(spec.cline_sdk.package_root), + "configDir": str(spec.cline_sdk.config_dir), + "provider": spec.cline_sdk.provider, + "features": spec.cline_sdk.features, + "approval": spec.cline_sdk.approval, + "cwd": str(spec.workdir), + "model": spec.model, + "resume": resume, + "instructions": spec.instructions is not None, + "tempDir": str(temp_dir), + } + cmd = spec.executable or "node" + args = [str(_CLINE_WORKER), json.dumps(launch)] + env.update(_cline_owned_env(spec.cline_sdk.config_dir)) + env["TMPDIR"] = str(temp_dir) elif spec.backend == "sdk": assert spec.omp_sdk is not None launch = { @@ -1427,6 +1634,19 @@ class _Pending: timer: asyncio.TimerHandle +@dataclass +class _ShellJob: + """One native command the parent owns for the cline worker: its task and + cancel handles are kept apart from the session's reader tasks so teardown + can stop and await them explicitly.""" + + id: str + cancel: threading.Event + task: asyncio.Task[None] | None = None + #: Set when the shared runner could not finish its bounded group cleanup. + cleanup_error: str | None = None + + @dataclass class _TurnBase: """State every backend keeps for the active turn.""" @@ -1447,8 +1667,9 @@ class _Turn(_TurnBase): #: sdk: the `sdk_settled` bridge frame that reported an error, if any. sdk_failure: dict[str, object] | None = None #: claude-code: native `result` events observed during the turn (exactly - #: one is required at settlement), the settled native result and the - #: `sdk_failure` frame, if any. + #: one is required at settlement). `native_result` is the settled native + #: result (Claude's `result` message or the native Cline `AgentResult`) + #: and `failure_frame` the `sdk_failure` frame, if any. native_results: int = 0 native_result: dict[str, object] | None = None failure_frame: dict[str, object] | None = None @@ -1877,9 +2098,12 @@ def _signal_group(self, sig: int) -> tuple[bool, PermissionError | None]: return True, exc return True, None - async def dispose(self) -> _Disposal: + async def dispose(self, owner_tasks: Sequence[asyncio.Task[None]] = ()) -> _Disposal: """Bounded teardown of the whole group; every observer task has - finished when this returns. Must complete even under cancellation.""" + finished when this returns. Must complete even under cancellation. + `owner_tasks` are already-cancelled tasks the owner still holds + (Cline's parent-owned commands): the drain ends early only once they + are done too, and this never cancels or awaits them itself.""" proc = self.proc assert proc is not None stdin_error: Exception | None = None @@ -1899,7 +2123,7 @@ async def dispose(self) -> _Disposal: group_exists, group_error = self._signal_group(signal.SIGKILL) escalated = group_error is None readers = [t for t in readers if not t.done()] - if not group_exists and self.exited.is_set() and not readers: + if not group_exists and self.exited.is_set() and not readers and all(t.done() for t in owner_tasks): break if now >= drain_end: break @@ -1925,20 +2149,29 @@ async def dispose(self) -> _Disposal: class _ProcessSession(LiveSession): - """An owned session child: `pi --mode rpc`, the OMP SDK bridge or the - Claude SDK worker.""" + """An owned session child: `pi --mode rpc`, the OMP SDK bridge, the + Claude SDK worker or the Cline SDK worker.""" _active: _Turn | None - def __init__(self, spec: SessionSpec, prepared: PreparedCommand) -> None: + def __init__(self, spec: SessionSpec, prepared: PreparedCommand, temp_dir: Path | None = None) -> None: super().__init__(spec) self._prepared = prepared self._sdk = spec.backend == "sdk" self._claude = spec.harness == "claude-code" - self._child = _OwnedChild("claude sdk worker" if self._claude else "omp sdk bridge" if self._sdk else "pi", self._loop, self._stderr, self._record_exit) - #: claude-code: bridge approval IDs announced by `claude_permission` - #: and not yet answered, cancelled or ended with their turn. + self._cline = spec.harness == "cline" + name = "claude sdk worker" if self._claude else "cline sdk worker" if self._cline else "omp sdk bridge" if self._sdk else "pi" + self._child = _OwnedChild(name, self._loop, self._stderr, self._record_exit) + #: claude-code / cline: bridge approval IDs announced by a permission + #: event and not yet answered, cancelled or ended with their turn. self._approvals: set[str] = set() + #: cline: the temporary directory this session owns — `TMPDIR` for the + #: worker and for every command run on its behalf — removed once every + #: owned process group is gone. + self._temp_dir = temp_dir + #: cline: the commands the parent runs for the worker, by request id. + self._commands: dict[str, _ShellJob] = {} + self._command_seq = 0 self._pending: dict[str, _Pending] = {} self._write_lock = asyncio.Lock() self._request_seq = 0 @@ -1949,18 +2182,29 @@ def _record_exit(self, code: int) -> None: # ---- public surface --------------------------------------------------- async def respond_approval(self, request_id: str, response: OpenCodeApprovalResponse) -> None: - """Answer a Claude `claude_permission` event (`request_id` is its - bridge-local `id`). `once` allows the call with its original input; - `reject` denies it with a fixed message. No persistent rule, updated - input, permission-mode change or bypass can be granted here: `always` - is refused before any request. The worker validates the ID again; - unknown, already answered, cancelled or ended requests are - `invalid-options`. Cancelling the caller closes the session.""" - if not self._claude: + """Answer a `claude_permission` / `cline_permission` event + (`request_id` is its bridge-local `id`). `once` allows the call with + its original input; `reject` denies it with a fixed message. No + persistent rule, updated input, permission-mode change or bypass can + be granted here: `always` is refused before any request. Cline needs + `cline_sdk.approval='callback'`; with 'upstream' every permission + stays with the native configuration and there is no channel. The + worker validates the ID again; unknown, already answered, cancelled + or ended requests are `invalid-options`. Cancelling the caller closes + the session.""" + if self._cline: + assert self._spec.cline_sdk is not None + if self._spec.cline_sdk.approval != "callback": + raise HarnessError( + "cline_sdk.approval='upstream' leaves every permission to the native configuration; " + "open the session with approval='callback' to answer them here", + code="unsupported-capability", + ) + elif not self._claude: await super().respond_approval(request_id, response) return if not isinstance(request_id, str) or not request_id: - raise HarnessError("request_id must be the non-empty id of a claude_permission event", code="invalid-options") + raise HarnessError("request_id must be the non-empty id of a permission event", code="invalid-options") if response == "always": raise HarnessError( "approval response 'always' would persist a native permission rule; only 'once' and 'reject' are supported", @@ -2008,7 +2252,7 @@ async def _abort(self, turn: _TurnBase) -> None: turn.abort_id = f"req-{self._request_seq}" self._register(turn.abort_id, "abort", turn) await self._uncancellable(self._loop.create_task(self._send({"id": turn.abort_id, "type": "abort"}))) - if self._claude: + if self._claude or self._cline: pending = self._pending.get(turn.abort_id) if pending is not None: await asyncio.shield(pending.future) @@ -2093,16 +2337,20 @@ def _handle_line(self, line: bytes) -> None: elif self._sdk: # The bridge speaks exactly three frame types: correlated # responses, wrapped native events and its own settlement; the - # Claude worker adds identity updates and fatal failures. + # Claude worker adds identity updates and fatal failures, the + # Cline worker fatal failures and its command requests. if kind == "sdk_settled": self._settle_sdk(turn, frame) return - if self._claude and kind == "sdk_failure": + if (self._claude or self._cline) and kind == "sdk_failure": self._fail_sdk(turn, frame) return if self._claude and kind == "sdk_reference": self._deliver(frame, kind, None, len(line)) return + if self._cline and kind in ("shell_request", "shell_cancel"): + self._handle_shell(frame, kind, turn) + return if kind != "sdk_event": self._fail("protocol-error", f"unexpected {kind!r} frame from the {self._child.name}") return @@ -2136,17 +2384,22 @@ def _match_response(self, frame: dict[str, object]) -> _Pending | None: return pending def _deliver(self, frame: dict[str, object], kind: str, turn: _Turn | None, size: int) -> None: + request_id = frame.get("id") self._emit(kind, frame, request_id if isinstance(request_id, str) else None, turn, size) def _emit(self, kind: str, raw: dict[str, object], request_id: str | None, turn: _TurnBase | None, size: int) -> None: - """Claude frames are screened once the identity handshake is done — - also for frames replayed out of the prelude, which arrive here again.""" - if self._claude and self._reference is not None: - if kind == "sdk_reference": - self._apply_reference(raw) - return - if not self._admit_claude(raw, kind, turn): # type: ignore[arg-type] # every turn of an owned child is a _Turn + """Claude / Cline frames are screened once the identity handshake is + done — also for frames replayed out of the prelude, which arrive here + again.""" + if self._reference is not None: + if self._claude: + if kind == "sdk_reference": + self._apply_reference(raw) + return + if not self._admit_claude(raw, kind, turn): # type: ignore[arg-type] # every turn of an owned child is a _Turn + return + elif self._cline and not self._admit_cline(raw, kind, turn): # type: ignore[arg-type] # every turn of an owned child is a _Turn return super()._emit(kind, raw, request_id, turn, size) @@ -2182,6 +2435,35 @@ def _admit_claude(self, frame: dict[str, object], kind: str, turn: _Turn | None) self._approvals.discard(approval) return True + def _admit_cline(self, frame: dict[str, object], kind: str, turn: _Turn | None) -> bool: + """Native Cline events name their session in `payload.sessionId` + where they carry one; synthetic permission notifications are tracked + for `respond_approval`. False when the session failed instead.""" + assert self._reference is not None + payload = frame.get("payload") + if isinstance(payload, dict): + session_id = payload.get("sessionId") + if isinstance(session_id, str) and session_id != self._reference.session_id: + self._fail("protocol-error", f"native {kind!r} event names session {session_id!r}, not the selected {self._reference.session_id!r}") + return False + if kind == "cline_permission": + approval = frame.get("id") + if not isinstance(approval, str) or not approval: + self._fail("protocol-error", "cline_permission event has no string id") + return False + if turn is None: + self._fail("protocol-error", f"cline_permission {approval!r} arrived while no turn was active") + return False + if approval in self._approvals: + self._fail("protocol-error", f"duplicate cline_permission id {approval!r}") + return False + self._approvals.add(approval) + elif kind == "cline_permission_cancelled": + approval = frame.get("id") + if isinstance(approval, str): + self._approvals.discard(approval) + return True + def _apply_reference(self, frame: dict[str, object]) -> None: """`sdk_reference`: the worker observed the native transcript path of the already selected session (same ID and workdir required).""" @@ -2247,6 +2529,8 @@ def _settle_sdk(self, turn: _Turn | None, frame: dict[str, object]) -> None: turn.sdk_failure = frame elif self._claude and not self._settle_claude(turn, frame): return + elif self._cline and not self._settle_cline(turn, frame): + return turn.settled = True self._maybe_finish(turn) @@ -2267,6 +2551,22 @@ def _settle_claude(self, turn: _Turn, frame: dict[str, object]) -> bool: turn.native_result = result return True + def _settle_cline(self, turn: _Turn, frame: dict[str, object]) -> bool: + """The settlement carries the exact native `AgentResult` (which has + no native `type` field) and its finish reason must be one the native + enum defines; anything else is a protocol error, never a guessed + outcome.""" + result = frame.get("result") + if not isinstance(result, dict): + self._fail("protocol-error", "sdk_settled carries no native AgentResult object") + return False + reason = result.get("finishReason") + if not isinstance(reason, str) or reason not in _CLINE_FINISH_REASONS: + self._fail("protocol-error", f"native result finishReason {reason!r} is not one of {', '.join(_CLINE_FINISH_REASONS)}") + return False + turn.native_result = result + return True + def _maybe_finish(self, turn: _Turn) -> None: if turn.finished or self._failure is not None: return @@ -2288,6 +2588,11 @@ def _maybe_finish(self, turn: _Turn) -> None: status, error = _classify_claude_result(turn.native_result) self._finish(turn, status, turn.native_result, error) return + if self._cline: + assert turn.native_result is not None + status, error = _classify_cline_result(turn.native_result) + self._finish(turn, status, turn.native_result, error) + return status, error = self._classify(turn) self._finish(turn, status, turn.last_end, error) @@ -2308,6 +2613,93 @@ def _classify(self, turn: _Turn) -> tuple[SessionTurnStatus, str | None]: return "interrupted", None return "completed", None + # ---- parent-owned commands (cline) ------------------------------------ + + def _handle_shell(self, frame: dict[str, object], kind: str, turn: _Turn | None) -> None: + """`shell_request` / `shell_cancel` from the worker. The harness, not + the worker, owns every native command process group; these frames are + control traffic and never enter an event queue.""" + request_id = frame.get("id") + if not isinstance(request_id, str) or not request_id: + self._fail("protocol-error", f"{kind} frame has no string 'id'") + return + if kind == "shell_cancel": + # The native tool aborted the call; a command that already + # finished or is racing its result makes this a no-op. + job = self._commands.get(request_id) + if job is not None: + job.cancel.set() + return + if turn is None: + self._fail("protocol-error", f"shell_request {request_id!r} arrived while no turn was active") + return + sequence = re.fullmatch(r"shell-([1-9][0-9]{0,15})", request_id) + if sequence is None or not self._command_seq < int(sequence[1]) <= 2**53 - 1: + self._fail("protocol-error", f"shell_request has an invalid or reused sequence {request_id!r}") + return + if len(self._commands) >= _CLINE_MAX_ACTIVE_COMMANDS: + self._fail("protocol-error", f"more than {_CLINE_MAX_ACTIVE_COMMANDS} concurrent shell_request commands") + return + cmd = frame.get("cmd") + if not isinstance(cmd, list) or not cmd or any(not isinstance(arg, str) or "\0" in arg for arg in cmd) or not cmd[0]: + self._fail("protocol-error", f"shell_request {request_id!r} needs NUL-free argv and a non-empty executable") + return + cwd = frame.get("cwd") + if not isinstance(cwd, str) or not cwd or "\0" in cwd or not os.path.isabs(cwd): + self._fail("protocol-error", f"shell_request {request_id!r} cwd {cwd!r} is not an absolute path") + return + stdin = frame.get("stdin") + if stdin is not None and not isinstance(stdin, str): + self._fail("protocol-error", f"shell_request {request_id!r} stdin must be a string or null") + return + job = _ShellJob(id=request_id, cancel=threading.Event()) + self._command_seq = int(sequence[1]) + self._commands[request_id] = job + job.task = self._loop.create_task(self._run_shell(job, cast(list[str], cmd), cwd, stdin)) + + async def _run_shell(self, job: _ShellJob, cmd: list[str], cwd: str, stdin: str | None) -> None: + """Run one native command in its own owned process group, stream its + output to the worker as it arrives and answer with exactly one + `shell_result` after the shared runner released the group.""" + + async def on_output(chunk: str, stream: OutputStream) -> None: + await self._send({"type": "shell_output", "id": job.id, "stream": stream, "chunk": chunk}) + + try: + try: + outcome = await run_subprocess_async( + cmd, + cwd=cwd, + timeout_seconds=_CLINE_COMMAND_TIMEOUT_SECONDS, + extra_env=dict(self._prepared.command.env), + stdin=stdin, + on_output=on_output, + max_output_bytes=_CLINE_COMMAND_MAX_OUTPUT_BYTES, + cancel=job.cancel, + ) + except asyncio.CancelledError as cancelled: + # The runner already ran its bounded group cleanup; a chained + # cause means it could not finish it, which teardown must see. + cause = cancelled.__cause__ + if cause is not None: + job.cleanup_error = f"command {job.id} could not release its process group: {type(cause).__name__}: {cause}" + raise + except Exception as exc: + # Unexpected runner errors can mean failed group cleanup. Keep + # this job and the lease: a native tool error is not proof that + # all of its processes stopped. + job.cleanup_error = f"command {job.id} cleanup could not be proved: {type(exc).__name__}: {exc}" + self._fail("agent-error", job.cleanup_error) + return + result: dict[str, object] = {"type": "shell_result", "id": job.id, "output": _cline_command_output(outcome)} + error = _cline_command_error(outcome) + if error is not None: + result["error"] = error + await self._send(result) + finally: + if job.cleanup_error is None: + self._commands.pop(job.id, None) + # ---- child I/O -------------------------------------------------------- async def _read_stdout(self, stream: asyncio.StreamReader) -> None: @@ -2326,6 +2718,20 @@ async def _classify_transport_loss(self, detail: str) -> None: # ---- teardown --------------------------------------------------------- + def _discard_temp_dir(self) -> HarnessError | None: + """Remove the temporary directory this session owns; only called once + every owned process group is gone. A directory that could not be + removed is reported, never silently leaked.""" + path = self._temp_dir + if path is None: + return None + try: + shutil.rmtree(path) + except OSError as exc: + return HarnessError(f"could not remove the owned temporary directory {path}: {exc.strerror or exc}", code="adapter-error") + self._temp_dir = None + return None + async def _teardown(self) -> None: failure = self._failure proc = self._child.proc @@ -2334,7 +2740,17 @@ async def _teardown(self) -> None: pending.timer.cancel() if self._active is not None and self._active.timer is not None: self._active.timer.cancel() - disposal = await self._child.dispose() + # The session is failing: no further frame is handled once `_failure` + # is set, so no command can be launched, and the ones we already own + # are stopped here so their groups die with the worker's own group. + commands = [job for job in self._commands.values() if job.task is not None] + command_tasks: list[asyncio.Task[None]] = [] + for job in commands: + assert job.task is not None + job.cancel.set() + job.task.cancel() + command_tasks.append(job.task) + disposal = await self._child.dispose(command_tasks) for task in list(self._tasks): if task.done(): continue @@ -2342,6 +2758,10 @@ async def _teardown(self) -> None: await task except (asyncio.CancelledError, Exception): pass + pending_commands = [t for t in command_tasks if not t.done()] + if pending_commands: + await asyncio.wait(pending_commands, timeout=_CLINE_COMMAND_CLEANUP_BUDGET) + command_error = _command_cleanup_error(commands) for pending in list(self._pending.values()): if not pending.future.done(): pending.future.set_result(None) @@ -2355,6 +2775,13 @@ async def _teardown(self) -> None: raise disposal.group_error if not self._child.exited.is_set(): raise HarnessError(f"{self._child.name} process {proc.pid} could not be reaped within the teardown budget", code="adapter-error") + if command_error is not None: + # Command groups may still be live: keep the lease and the + # temporary directory rather than report a successful disposal. + raise HarnessError(command_error, code="adapter-error") + # Every owned group is gone: the temporary directory goes before the + # lease is released, and a failed release keeps both. + temp_error = self._discard_temp_dir() cleanup_command(self._prepared) code = self._returncode if self._sdk and failure.status not in ("exited", "signaled") and code not in (0, -signal.SIGTERM): @@ -2377,6 +2804,8 @@ async def _teardown(self) -> None: ) if disposal.stdin_error is not None: raise disposal.stdin_error + if temp_error is not None: + raise temp_error # ---- startup ---------------------------------------------------------- @@ -2384,6 +2813,7 @@ async def _startup(self, argv: list[str], env: dict[str, str]) -> None: try: await self._child.spawn(argv, self._spec.workdir, env, self._read_stdout) except OSError as exc: + self._discard_temp_dir() cleanup_command(self._prepared) raise HarnessError(f"failed to launch {argv[0]!r}: {exc.strerror or exc}", code="launch-failed") from None if self._abandoned: @@ -2426,6 +2856,14 @@ def _reference_from_state(self, response: dict[str, object]) -> SessionReference raise HarnessError(f"{self._child.name} resumed session {session_id!r}, expected {resume.session_id!r}") if self._claude and _UUID.fullmatch(session_id) is None: raise HarnessError(f"{self._child.name} selected a non-UUID session ID {session_id!r}") + if self._cline: + if not isinstance(session_file, str) or not session_file: + raise HarnessError(f"{self._child.name} reported no native session manifest path") + workdir = data.get("workdir") + if not isinstance(workdir, str) or not _same_dir(workdir, self._spec.workdir): + raise HarnessError(f"{self._child.name} opened the session in {workdir!r}, not the requested {str(self._spec.workdir)!r}") + if resume is not None and resume.session_file is not None and not _same_dir(session_file, resume.session_file): + raise HarnessError(f"{self._child.name} resumed the manifest {session_file!r}, expected {str(resume.session_file)!r}") return SessionReference( session_id=session_id, session_file=Path(session_file) if session_file else None, @@ -2490,11 +2928,83 @@ def _claude_result_error(result: dict[str, object]) -> str: return "native result reported an error" +#: Native `AgentFinishReason` -> turn status. Any other reason is a protocol +#: error: an unmapped reason is never guessed into a turn outcome. +_CLINE_FINISH_REASONS: dict[str, SessionTurnStatus] = { + "completed": "completed", + "aborted": "interrupted", + "max_iterations": "agent-error", + "mistake_limit": "agent-error", + "error": "agent-error", +} + + +def _classify_cline_result(result: dict[str, object]) -> tuple[SessionTurnStatus, str | None]: + """Native `AgentResult` -> turn status; the finish reason was validated + when the settlement arrived.""" + reason = str(result.get("finishReason")) + status = _CLINE_FINISH_REASONS[reason] + if status != "agent-error": + return status, None + return status, f"native run finished with reason {reason}" + + +def _cline_command_output(outcome: SubprocOutcome) -> str: + """`shell_result.output`: captured stdout, then stderr under its own + marker, with an explicit marker wherever the cap cut the capture off.""" + limit = _CLINE_COMMAND_MAX_OUTPUT_BYTES + output = outcome.stdout + if outcome.stdout_truncated: + output += f"\n[stdout truncated at {limit} bytes]" + if outcome.stderr or outcome.stderr_truncated: + output += "\n[stderr]\n" + outcome.stderr + if outcome.stderr_truncated: + output += f"\n[stderr truncated at {limit} bytes]" + return output + + +def _cline_command_error(outcome: SubprocOutcome) -> str | None: + """`shell_result.error`: every outcome the native tool must see as a + failure. Captured output alone never implies success.""" + if outcome.launch_error is not None: + return f"command could not be launched: {outcome.launch_error}" + if outcome.termination == "timed-out": + return f"command exceeded the {_CLINE_COMMAND_TIMEOUT_SECONDS:g}s limit and its process group was stopped" + if outcome.termination == "cancelled": + return "command was cancelled and its process group was stopped" + if outcome.callback_error is not None: + return f"command output could not be forwarded to the worker: {outcome.callback_error}" + if outcome.signal is not None: + return f"command was killed by {outcome.signal}" + if outcome.exit_code != 0: + return f"command exited with code {outcome.exit_code}" + return None + + +def _command_cleanup_error(jobs: list[_ShellJob]) -> str | None: + """Whether the parent-owned commands really released their process + groups. Anything unfinished or failed means live processes may remain, + which must keep the workdir lease and the temporary directory.""" + problems: list[str] = [] + for job in jobs: + task = job.task + assert task is not None + if not task.done(): + problems.append(f"command {job.id} did not release its process group within {_CLINE_COMMAND_CLEANUP_BUDGET:g}s") + elif job.cleanup_error is not None: + problems.append(job.cleanup_error) + elif not task.cancelled() and task.exception() is not None: + error = task.exception() + problems.append(f"command {job.id} failed during cleanup: {type(error).__name__}: {error}") + return "; ".join(problems) if problems else None + + async def open_session(spec: SessionSpec) -> LiveSession: """Open the live session for `spec`: spawn the session child (`pi --mode - rpc`, OMP Bun bridge, Python Claude SDK worker, or Factory SDK-driven - `droid exec`), connect to caller-owned OpenCode/OpenHands, or open the Amp - Node worker. Every backend verifies explicit identity before turns. + rpc`, OMP Bun bridge, Python Claude SDK worker, Cline Node worker, or + Factory SDK-driven `droid exec`), connect to caller-owned + OpenCode/OpenHands, or open the Amp Node worker. Every backend verifies + explicit identity before turns. Validation (harness, backend, options, resume header) happens before any filesystem, process or network side effect. Instructions are projected @@ -2524,11 +3034,21 @@ async def open_session(spec: SessionSpec) -> LiveSession: if spec.resume is not None: if spec.harness == "claude-code": _verify_claude_transcript(spec.resume) - else: + elif spec.harness != "cline": + # A native Cline manifest carries no Pi / OMP session header: the + # worker verifies its identity, path and cwd around the start. _verify_session_header(spec.resume, header_type="session", title_preamble=spec.backend == "sdk") - built = _build(spec) - prepared = prepare_command(built) - session = _ProcessSession(spec, prepared) + # The cline worker and every command it asks for run inside a temporary + # directory this session owns and removes once their groups are gone. + temp_dir = Path(tempfile.mkdtemp(prefix="harness-cline-")) if spec.harness == "cline" else None + try: + built = _build(spec, temp_dir) + prepared = prepare_command(built) + except BaseException: + if temp_dir is not None: + shutil.rmtree(temp_dir, ignore_errors=True) + raise + session = _ProcessSession(spec, prepared, temp_dir) env = os.environ.copy() env.update(built.env) await _await_startup(session, session._startup([built.cmd] + built.args, env)) @@ -2564,6 +3084,9 @@ async def _await_startup(session: LiveSession, startup: Coroutine[object, object "AmpVisibility", "ClaudeSdkOptions", "ClaudeSettingSource", + "ClineApproval", + "ClineFeatures", + "ClineSdkOptions", "FactoryDroidAutonomy", "FactoryDroidCallback", "FactoryDroidOptions", diff --git a/tests/cline_sdk_cases.json b/tests/cline_sdk_cases.json new file mode 100644 index 0000000..5d01f3b --- /dev/null +++ b/tests/cline_sdk_cases.json @@ -0,0 +1,10 @@ +[ + {"name": "success", "status": "completed"}, + {"name": "reject", "status": "completed", "reply": "reject"}, + {"name": "interrupt", "status": "interrupted", "operation": "interrupt"}, + {"name": "close", "status": "closed", "operation": "close"}, + {"name": "worker-loss", "status": "signaled"}, + {"name": "command-error", "status": "completed"}, + {"name": "provider-error", "status": "agent-error"}, + {"name": "partial-wire", "status": "protocol-error"} +] diff --git a/tests/helpers/cline_faults.mjs b/tests/helpers/cline_faults.mjs new file mode 100644 index 0000000..977f6bc --- /dev/null +++ b/tests/helpers/cline_faults.mjs @@ -0,0 +1,36 @@ +// Test-only additions around the real SDK. No provider/model substitution. +// Exercises future event retention and malformed native-wire/worker-loss paths. +import { join } from 'node:path' +import { pathToFileURL } from 'node:url' + +if (/_cline_sdk\.mjs$|cline-sdk\.mjs$/.test(process.argv[1] ?? '')) { + const { ClineCore } = await import(pathToFileURL(join(process.env.HARNESS_TEST_CLINE_PACKAGE_ROOT, 'dist/index.js')).href) + const create = ClineCore.create + ClineCore.create = async function (...args) { + const core = await create.apply(this, args) + const subscribe = core.subscribe.bind(core) + core.subscribe = (listener, ...options) => subscribe(event => { + listener(event) + if (event.type === 'agent_event' && event.payload.event.type === 'iteration_start') { + listener({ type: 'future_native_event', payload: { sessionId: event.payload.sessionId, future: { preserved: true } } }) + } + if (process.env.HARNESS_TEST_CLINE_SCENARIO === 'partial-wire' && event.type === 'agent_event' && event.payload.event.type === 'content_start' && event.payload.event.contentType === 'text') { + process.stdout.write('[malformed-native-frame\n') + } + }, ...options) + return core + } + if (process.env.HARNESS_TEST_CLINE_SCENARIO === 'worker-loss') { + let buffer = '' + process.stdin.on('data', bytes => { + buffer += bytes.toString('utf8') + for (;;) { + const newline = buffer.indexOf('\n') + if (newline < 0) break + const frame = JSON.parse(buffer.slice(0, newline)) + buffer = buffer.slice(newline + 1) + if (frame.type === 'shell_output') process.kill(process.pid, 'SIGKILL') + } + }) + } +} diff --git a/tests/helpers/cline_provider.py b/tests/helpers/cline_provider.py new file mode 100644 index 0000000..28d52ff --- /dev/null +++ b/tests/helpers/cline_provider.py @@ -0,0 +1,78 @@ +"""Finite loopback OpenAI-compatible peer for the real optional Cline SDK. + +All inputs, credentials and replies are synthetic. Closing stdin stops this +owned peer; the independent deadline also bounds a caller that disappears. +""" +from __future__ import annotations + +from http.server import BaseHTTPRequestHandler +import json +from pathlib import Path +from socketserver import ThreadingTCPServer +import sys +import threading + +workdir = Path(sys.argv[1]) +scenario = sys.argv[2] + + +class Provider(BaseHTTPRequestHandler): + def log_message(self, *_args): + pass + + def do_POST(self): + self.connection.settimeout(5) + request = json.loads(self.rfile.read(int(self.headers["Content-Length"]))) + with (workdir / "provider-requests.jsonl").open("a") as log: + log.write(json.dumps(request) + "\n") + if scenario == "provider-error": + data = json.dumps({"error": {"message": "synthetic provider rejection", "type": "invalid_request_error"}}).encode() + self.send_response(400) + self.send_header("content-type", "application/json") + else: + tool = scenario in {"reject", "interrupt", "close", "worker-loss", "command-error"} and request["messages"][-1]["role"] != "tool" + command = "printf synthetic-command-error; exit 7" if scenario == "command-error" else "printf owned-tool-running; sleep 3; printf survived > survived" + delta = { + "role": "assistant", + "tool_calls": [{"index": 0, "id": "synthetic-tool", "type": "function", "function": { + "name": "run_commands", "arguments": json.dumps({"commands": [command]}), + }}], + } if tool else {"role": "assistant", "content": "synthetic native reply"} + + def chunk(value, reason=None): + return {"id": "synthetic", "object": "chat.completion.chunk", "created": 1, "model": "synthetic", "choices": [{"index": 0, "delta": value, "finish_reason": reason}]} + + frames = [chunk(delta), {**chunk({}, "tool_calls" if tool else "stop"), "usage": {"prompt_tokens": 11, "completion_tokens": 3, "total_tokens": 14}}] + data = ("".join("data: " + json.dumps(frame) + "\n\n" for frame in frames) + "data: [DONE]\n\n").encode() + self.send_response(200) + self.send_header("content-type", "text/event-stream") + self.send_header("content-length", str(len(data))) + self.end_headers() + try: + self.wfile.write(data) + except (BrokenPipeError, ConnectionResetError): + pass + + +# HTTPServer performs reverse DNS during bind; this loopback peer needs none. +server = ThreadingTCPServer(("127.0.0.1", 0), Provider) +server.daemon_threads = True +finished = threading.Event() + + +def stop_on_eof(): + sys.stdin.buffer.read() + finished.set() + + +reader = threading.Thread(target=stop_on_eof, daemon=True) +serving = threading.Thread(target=server.serve_forever) +reader.start() +serving.start() +print(json.dumps({"endpoint": f"http://127.0.0.1:{server.server_address[1]}/v1"}), flush=True) +try: + finished.wait(60) +finally: + server.shutdown() + server.server_close() + serving.join() diff --git a/tests/test_cline_sdk.py b/tests/test_cline_sdk.py new file mode 100644 index 0000000..c369e37 --- /dev/null +++ b/tests/test_cline_sdk.py @@ -0,0 +1,173 @@ +"""Real optional Cline SDK, synthetic loopback provider; no provider account.""" +from __future__ import annotations + +import asyncio +from dataclasses import replace +import json +import os +from pathlib import Path +import signal +import sys + +import pytest + +from harness import ClineSdkOptions, HarnessError, SessionSpec, open_session + +ROOT = Path(__file__).parent +SDK = ROOT.parent / "ts/node_modules/@cline/sdk" +CASES = json.loads((ROOT / "cline_sdk_cases.json").read_text()) +pytestmark = pytest.mark.skipif(sys.platform not in ("darwin", "linux") or not SDK.exists(), reason="POSIX and optional Cline SDK development dependency required") + + +@pytest.fixture +async def configuration(tmp_path): + providers = [] + + async def make(scenario): + directory = tmp_path / scenario + workdir = directory / "work" + workdir.mkdir(parents=True) + home = directory / "home" + home.mkdir() + provider = await asyncio.create_subprocess_exec( + sys.executable, str(ROOT / "helpers/cline_provider.py"), str(workdir), scenario, + stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.PIPE, + start_new_session=True, + ) + errors = bytearray() + + async def drain_errors(): + while chunk := await provider.stderr.read(4096): + errors.extend(chunk) + del errors[:-8192] + + draining = asyncio.create_task(drain_errors()) + providers.append((provider, draining)) + try: + handshake = await asyncio.wait_for(provider.stdout.readline(), 5) + except asyncio.TimeoutError as error: + raise AssertionError(f"synthetic provider readiness deadline; stderr: {errors.decode(errors='replace')}") from error + assert handshake, f"synthetic provider produced no endpoint; stderr: {errors.decode(errors='replace')}" + endpoint = json.loads(handshake)["endpoint"] + config = directory / "config" + settings = config / "data/settings" + settings.mkdir(parents=True) + (settings / "providers.json").write_text(json.dumps({"version": 1, "modes": {}, "lastUsedProvider": "openai-compatible", "providers": { + "openai-compatible": {"settings": {"provider": "openai-compatible", "model": "synthetic", "apiKey": "synthetic", "baseUrl": endpoint}, "updatedAt": "2026-09-10T00:00:00Z", "tokenSource": "manual"}, + }})) + return SessionSpec( + "cline", workdir, "sdk", executable=os.environ.get("HARNESS_TEST_NODE", "node"), model="synthetic", timeout_seconds=12, request_timeout_seconds=10, + env={"HOME": str(home), "NODE_OPTIONS": "--import=" + (ROOT / "helpers/cline_faults.mjs").as_uri(), "HARNESS_TEST_CLINE_PACKAGE_ROOT": str(SDK), "HARNESS_TEST_CLINE_SCENARIO": scenario}, + cline_sdk=ClineSdkOptions(package_root=SDK, config_dir=config, provider="openai-compatible", features="builtin-only", approval="callback"), + ) + + yield make + for provider, draining in providers: + provider.stdin.close() + try: + await asyncio.wait_for(provider.wait(), 5) + except asyncio.TimeoutError: + assert provider.returncode is None and os.getpgid(provider.pid) == provider.pid + os.killpg(provider.pid, signal.SIGKILL) + await asyncio.wait_for(provider.wait(), 2) + await draining + + +async def collect(turn): + return [event async for event in turn.events] + + +@pytest.mark.parametrize("case", CASES, ids=[case["name"] for case in CASES]) +async def test_native_cline_contract(configuration, case): + spec = await configuration(case["name"]) + session = await open_session(spec) + events = [] + operation = None + try: + turn = session.start_turn(case["name"]) + for_event = turn.events + async for event in for_event: + events.append(event) + if event.type == "cline_permission": + await session.respond_approval(event.raw["id"], case.get("reply", "once")) + native = event.raw.get("payload", {}).get("event", {}) + if native.get("contentType") == "tool" and native.get("type") == "content_update" and case.get("operation") and operation is None: + operation = asyncio.create_task(session.interrupt() if case["operation"] == "interrupt" else session.close()) + result = await turn.result + if operation: + await operation + assert result.status == case["status"] + assert result.session_id == session.reference.session_id + future = next(event for event in events if event.type == "future_native_event") + assert future.raw["payload"]["future"] == {"preserved": True} + if case["name"] == "success": + assert result.raw["text"] == "synthetic native reply" + assert result.raw["usage"]["inputTokens"] == 11 + assert result.raw["usage"]["outputTokens"] == 3 + if case["name"] == "command-error": + tool = next(event.raw["payload"]["event"] for event in events if event.type == "agent_event" and event.raw["payload"]["event"].get("contentType") == "tool" and event.raw["payload"]["event"]["type"] == "content_end") + assert tool["output"][0]["success"] is False + if case["name"] == "partial-wire": + assert any(event.raw.get("payload", {}).get("event", {}).get("contentType") == "text" for event in events) + if case["name"] == "worker-loss": + assert result.signal == "SIGKILL" + finally: + await session.close() + assert not (spec.workdir / ".harness-run.lock").exists() + if case["name"] in {"interrupt", "close", "worker-loss"}: + await asyncio.sleep(3.2) + assert not (spec.workdir / "survived").exists() + + +async def test_native_cline_followup_resume_and_identity_refusal(configuration): + spec = replace(await configuration("followup"), instructions="Cline-owned instruction sentinel 8f26") + instruction_file = spec.workdir / "CLINE.md" + instruction_file.write_text("original caller instructions") + session = await open_session(spec) + try: + for prompt in ("original-history", "followup-history"): + turn = session.start_turn(prompt) + await collect(turn) + assert (await turn.result).status == "completed" + reference = session.reference + finally: + await session.close() + assert instruction_file.read_text() == "original caller instructions" + session = await open_session(replace(spec, resume=reference)) + try: + assert session.reference == reference + turn = session.start_turn("resumed-history") + await collect(turn) + result = await turn.result + assert result.status == "completed" + assert result.raw["usage"]["inputTokens"] == 11 # per-turn, never summed snapshots + finally: + await session.close() + requests = [json.loads(line) for line in (spec.workdir / "provider-requests.jsonl").read_text().splitlines()] + history = json.dumps(requests[-1]["messages"]) + assert "original-history" in history and "followup-history" in history and "resumed-history" in history + assert spec.instructions in history + assert instruction_file.read_text() == "original caller instructions" + manifest = json.loads(reference.session_file.read_text()) + assert manifest["metadata"]["usage"]["inputTokens"] == 33 + assert manifest["metadata"]["usage"]["outputTokens"] == 9 + before = reference.session_file.read_bytes() + with pytest.raises(HarnessError): + await open_session(replace(spec, resume=replace(reference, session_id="unknown-exact-id"))) + assert reference.session_file.read_bytes() == before + manifest["cwd"] = str(spec.workdir.parent) + reference.session_file.write_text(json.dumps(manifest)) + with pytest.raises(HarnessError): + await open_session(replace(spec, resume=reference)) + assert not (spec.workdir / ".harness-run.lock").exists() + + +async def test_cline_rejects_unsupported_features_and_missing_dependency(configuration): + spec = await configuration("invalid") + with pytest.raises(HarnessError) as unsupported: + await open_session(replace(spec, cline_sdk=replace(spec.cline_sdk, features="native-hooks"))) + assert unsupported.value.code == "unsupported-capability" + with pytest.raises(HarnessError) as missing: + await open_session(replace(spec, cline_sdk=replace(spec.cline_sdk, package_root=spec.workdir / "missing-sdk"))) + assert missing.value.code == "launch-failed" + assert not (spec.workdir / ".harness-run.lock").exists() diff --git a/tests/test_openhands_sessions.py b/tests/test_openhands_sessions.py index aa35e9a..4de165a 100644 --- a/tests/test_openhands_sessions.py +++ b/tests/test_openhands_sessions.py @@ -488,6 +488,7 @@ async def test_validation_precedes_all_network_effects(tmp_path): {"model": " "}, {"omp_sdk": spec.openhands}, {"claude_sdk": spec.openhands}, + {"cline_sdk": spec.openhands}, {"backend": "sdk"}, {"backend": "cli"}, ): diff --git a/tests/test_sessions.py b/tests/test_sessions.py index 0d3358f..06d454a 100644 --- a/tests/test_sessions.py +++ b/tests/test_sessions.py @@ -16,6 +16,8 @@ import pytest from harness import ( + ClineSdkOptions, + FactoryDroidOptions, HarnessError, SessionReference, SessionSpec, @@ -177,6 +179,23 @@ async def test_open_session_rejects_before_spawn(tmp_path: Path, overrides: dict assert not (tmp_path / "synthetic-session.jsonl").exists() +@pytest.mark.parametrize("harness", ["factory-droid", "cline"]) +async def test_sdk_option_families_reject_outside_their_backend(tmp_path: Path, harness: str): + """Each otherwise-valid SDK selection refuses the foreign option family.""" + with pytest.raises(HarnessError) as info: + async with await open_session(SessionSpec( + harness=harness, + workdir=tmp_path, + backend="sdk", + executable=str(tmp_path / "missing-sdk-executable"), + cline_sdk=ClineSdkOptions(tmp_path / "sdk", tmp_path / "profile", "openai-compatible", "builtin-only"), + factory_droid=FactoryDroidOptions(), + )): + pass + assert info.value.code == "invalid-options" + assert not (tmp_path / LOCK_DIRNAME).exists() + + async def test_open_session_missing_executable_is_launch_failed(tmp_path: Path): with pytest.raises(HarnessError) as info: async with await open_session(_spec("success", tmp_path, executable=str(tmp_path / "missing-pi"), instructions="X")): diff --git a/ts/README.md b/ts/README.md index 04a037d..3ebf84d 100644 --- a/ts/README.md +++ b/ts/README.md @@ -106,6 +106,25 @@ not this JavaScript worker. See the [paired examples](../README.md#optional-claude-agent-sdk-sessions) and [version/configuration/event contract](../SPEC.md#optional-claude-agent-sdk-sessions). +## Optional Cline SDK backend + +`openSession({ harness: 'cline', backend: 'sdk', workdir, clineSdk })` runs +the official `@cline/sdk@0.0.82` in an owned **Node >=22.14** worker, including +when called from Bun. `clineSdk` requires absolute `packageRoot` and +`configDir`, a native `provider`, and explicit `features: 'builtin-only'`. +The profile is writable native Cline state; use a dedicated profile. +Model omission reads that provider's configured model. + +The SDK stays local, without hub discovery/startup. Native hooks, plugins, +MCP, subagents and command detachment are excluded independently of the +public bash executor hook. Harness owns command groups through its shared +subprocess runner and stops them on forced worker loss before releasing the +workdir lease. `approval: 'callback'` requires explicit `once`/`reject` +replies; the default `upstream` preserves native auto-approved defaults. +Exact native resume, raw events/usage and bounded failure behavior use the +shared session API. See the [paired examples](../README.md#optional-cline-sdk-sessions) +and [full contract](../SPEC.md#optional-cline-sdk-sessions). + ## Caller-owned OpenCode HTTP backend `openSession({ harness: 'opencode', backend: 'rpc', workdir, opencode })` diff --git a/ts/bun.lock b/ts/bun.lock index d55ae19..1f947fd 100644 --- a/ts/bun.lock +++ b/ts/bun.lock @@ -9,6 +9,7 @@ }, "devDependencies": { "@anthropic-ai/claude-agent-sdk": "0.3.263", + "@cline/sdk": "0.0.82", "@factory/droid-sdk": "0.9.1", "@types/better-sqlite3": "^7.6.0", "@types/bun": "latest", @@ -27,6 +28,28 @@ }, }, "packages": { + "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@5.0.81", "", { "dependencies": { "@ai-sdk/anthropic": "4.0.52", "@ai-sdk/openai": "4.0.65", "@ai-sdk/provider": "4.0.13", "@ai-sdk/provider-utils": "5.0.39", "@smithy/eventstream-codec": "^4.3.3", "@smithy/util-utf8": "^4.3.3", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-7+08k3qroccRqOf1NCEXlUSJnz87APKZvdq/damTiVSMKXw9bIcgDRs3STwo7XaVzDyf2RZ6bOThKmhJCvYgpg=="], + + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@4.0.52", "", { "dependencies": { "@ai-sdk/provider": "4.0.13", "@ai-sdk/provider-utils": "5.0.39" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-IQt/y++kSUdqa9EbFOJntxLGjZMURTm/MnTUikNBZ5X86K5l96t/BwsRsX/SrH5kx+7yRPrmJ1WBNlmE8quSVg=="], + + "@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.78", "", { "dependencies": { "@ai-sdk/provider": "4.0.13", "@ai-sdk/provider-utils": "5.0.39", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-fcamzmFlcy+c/tIc7QcATLe/kArgSVGQ1QxaddMjhOSX8zypc2hmD5sHB2bp1DSFcSxDD32d6usF9a7xLCTULg=="], + + "@ai-sdk/google": ["@ai-sdk/google@4.0.67", "", { "dependencies": { "@ai-sdk/provider": "4.0.13", "@ai-sdk/provider-utils": "5.0.39" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-F+o3bRDz0ua2hJbpnuEsoNsKR11NT+nAhq1eZfaRJr2rzs5eKi/E9wJw8ZXeJx7NlYeBF6il5/+itTBdkM1kJg=="], + + "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@5.0.79", "", { "dependencies": { "@ai-sdk/anthropic": "4.0.52", "@ai-sdk/google": "4.0.67", "@ai-sdk/openai-compatible": "3.0.47", "@ai-sdk/provider": "4.0.13", "@ai-sdk/provider-utils": "5.0.39", "google-auth-library": "^10.6.2" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-R4Qf7U1N6kKZY3WE7sZShFIU9pZGEysluekbE43p6AnvIeiN9XEX0MHzDaZdFarn/fWmbZjSyCkFkJ9UpXVB+w=="], + + "@ai-sdk/mistral": ["@ai-sdk/mistral@4.0.42", "", { "dependencies": { "@ai-sdk/provider": "4.0.13", "@ai-sdk/provider-utils": "5.0.39" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-N/h84m0j/6qxjiKvaf8UsNy+E9XnrIHMwZ86Q9Jw9dqaKKdM9w4ed4b6v1a3Fi5A3F/H0Ll16k499MIprwimjw=="], + + "@ai-sdk/openai": ["@ai-sdk/openai@4.0.65", "", { "dependencies": { "@ai-sdk/provider": "4.0.13", "@ai-sdk/provider-utils": "5.0.39" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-rpk1Tv7TR0H338zTKmSVMxv4yo++HDOxioiuvBySb8cWkx2fS10tWl/5cEwL0diJbcV/+p3+9gpIOre52UoQJA=="], + + "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@3.0.47", "", { "dependencies": { "@ai-sdk/provider": "4.0.13", "@ai-sdk/provider-utils": "5.0.39" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6w3ymyP2KV4qzKlMH/Fm6Tmk+2ksiqktZFBbKCGIaoPSBWX+M4HmMW0BFUh9Ar0CtjOia3E8GlYAoEERONuhyg=="], + + "@ai-sdk/otel": ["@ai-sdk/otel@1.0.97", "", { "dependencies": { "@ai-sdk/provider": "4.0.13", "@opentelemetry/api": "1.9.1", "ai": "7.0.97" } }, "sha512-VZN8+DbgLacYZCRJ7u0Bw+iBSn9+hfmXDneywkgSEYo82BavLVgsrAR/vrFDmbaDV7pD8wCWfkj4a/qfwvdQcg=="], + + "@ai-sdk/provider": ["@ai-sdk/provider@4.0.13", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-sJvnbFIFLzmKFXIjfwS8XCOAj6puHCawItwvA9PBZgk2f/l/TiC4OSfK3ueDbUVXfRCOn5eJN97EIF10toIOmQ=="], + + "@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.39", "", { "dependencies": { "@ai-sdk/provider": "4.0.13", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8", "undici": "^7.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VIFp4Qv3j+V941crFB1y2VG5yeGbeLOFRxiPaZJETkkpo1H5WviLx6H5yRkFkrXIGxnTGygKsPgCAJG0X61Auw=="], + "@anthropic-ai/claude-agent-sdk": ["@anthropic-ai/claude-agent-sdk@0.3.263", "", { "optionalDependencies": { "@anthropic-ai/claude-agent-sdk-darwin-arm64": "0.3.263", "@anthropic-ai/claude-agent-sdk-darwin-x64": "0.3.263", "@anthropic-ai/claude-agent-sdk-linux-arm64": "0.3.263", "@anthropic-ai/claude-agent-sdk-linux-arm64-musl": "0.3.263", "@anthropic-ai/claude-agent-sdk-linux-x64": "0.3.263", "@anthropic-ai/claude-agent-sdk-linux-x64-musl": "0.3.263", "@anthropic-ai/claude-agent-sdk-win32-arm64": "0.3.263", "@anthropic-ai/claude-agent-sdk-win32-x64": "0.3.263" }, "peerDependencies": { "@anthropic-ai/sdk": ">=0.93.0", "@modelcontextprotocol/sdk": "^1.29.0", "zod": "^4.0.0" } }, "sha512-0QWoHgWWlSmgXfEqZRbVYRoXT9p4tx/yKaZ4eLJ8l1MR65SIvMGR+cd5ZXUpGYZpj9RzbYiYU9LoFg90mdIjmw=="], "@anthropic-ai/claude-agent-sdk-darwin-arm64": ["@anthropic-ai/claude-agent-sdk-darwin-arm64@0.3.263", "", { "os": "darwin", "cpu": "arm64" }, "sha512-H4eLd4Tkx3rJkt739CHb+9AcaKiiOpibU4tYsmma47mV+2zAPjUyFxpuE2N57VSmpAgbLQxu44du0TFN+UH5dg=="], @@ -47,44 +70,241 @@ "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.124.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1", "standardwebhooks": "^1.0.0" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-cN5O8i9UVxHeOQAzj/XjshWXG8KiibJDw9OGpH2Z/eR3n/RBxdoLxDJOcfqAJWvjaMDFfHTBADU04hWRJVkDyA=="], + "@aws-sdk/core": ["@aws-sdk/core@3.978.0", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-2yX9LUmxPklVjSGTb8dfnWRJSiFQ3TeH2nn7G1mdKHTfnabzF0+gfrS8rYfLWmZrQ8A3mEcxMJjRc51dL5KWaA=="], + + "@aws-sdk/credential-provider-cognito-identity": ["@aws-sdk/credential-provider-cognito-identity@3.972.70", "", { "dependencies": { "@aws-sdk/nested-clients": "^3.997.45", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-KlU89w6Hmb4oZB5zFz/MNIhPOBQGVE7KrDr3BTPCwC4W+q566YH8tGNsAML781LKATtqmNCGFry8XvsJ2XPusg=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.71", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-JN+JHruYZw3GUZB8YGAlDk4wTDPOEAEEdEzj5nS0xodWR4smzHsN7PnK2j6IeOsDIj2aqua5DSbhXl9Gtf90FQ=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.73", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-uyYYnJOnlis8uQzaYGPd7N1JoioCoNpXgnkXYixsWJXHXgXyYi8WXJSDfofxJeWfQIGWLe2Nwyq60Uc7MZdVOg=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.16", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/credential-provider-env": "^3.972.71", "@aws-sdk/credential-provider-http": "^3.972.73", "@aws-sdk/credential-provider-login": "^3.972.78", "@aws-sdk/credential-provider-process": "^3.972.71", "@aws-sdk/credential-provider-sso": "^3.973.15", "@aws-sdk/credential-provider-web-identity": "^3.972.77", "@aws-sdk/nested-clients": "^3.997.45", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-i++ly+0Uxa+u3ebSSyr0S/3CFhFJDxCXT3+Zj+mW2bXenEx5bKGCdTIKFu39SgXBNhWDjex/8cXUx9MUTMCrTw=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.78", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/nested-clients": "^3.997.45", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-eUtswnXu0+Ii9ieRK+0L7aPFV3Z/dnW2VntJzjBP9xs8s+8p5nBNuymIXtXwZ+5r5+XJP3e32nMkuZ/r0HozEA=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.83", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.71", "@aws-sdk/credential-provider-http": "^3.972.73", "@aws-sdk/credential-provider-ini": "^3.973.16", "@aws-sdk/credential-provider-process": "^3.972.71", "@aws-sdk/credential-provider-sso": "^3.973.15", "@aws-sdk/credential-provider-web-identity": "^3.972.77", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-jdso7ejzfRnatxMUZK4S/U6KbaDPCvfIV4XL+IQAPFDBt5rj5Fq595euqlK8Le4lNCMFR9oUpt+1l0aMgaayOQ=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.71", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-lYmXJa4gvq4xN1lrT5NiP5vIYYKcGWAdj8y+8o6dlcateB5eF3Dn8DtmjjHKfMBrTPAMr2pebIiX/UOj8c1/UA=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.15", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/nested-clients": "^3.997.45", "@aws-sdk/token-providers": "3.1129.0", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-6Jhcf4v0pSFdjk1EW2kvzuEBKD+UZ2uNcHUIglKKLndD20YhvkL2kdmDOV5/j4mYuWWwe/a1FQ1aomU86/Cg5Q=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.77", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/nested-clients": "^3.997.45", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-uylIQSUWpfLuH2LovxEEfwzJGM/SabLOfLMg6YXu/E8jJEKUdpdILCVCQCdFvHyu/7dLJOHPMfrSwduxO56NkQ=="], + + "@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1129.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/credential-provider-cognito-identity": "^3.972.70", "@aws-sdk/credential-provider-env": "^3.972.71", "@aws-sdk/credential-provider-http": "^3.972.73", "@aws-sdk/credential-provider-ini": "^3.973.16", "@aws-sdk/credential-provider-login": "^3.972.78", "@aws-sdk/credential-provider-node": "^3.972.83", "@aws-sdk/credential-provider-process": "^3.972.71", "@aws-sdk/credential-provider-sso": "^3.973.15", "@aws-sdk/credential-provider-web-identity": "^3.972.77", "@aws-sdk/nested-clients": "^3.997.45", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-iEmi02TO6nVRlUukfBIReSZ2ysNrTOxWIt3k2OejGR5A2lcLfgWx0SvyOaWyLovMTVLnaQA9D2L171socJsJXA=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.45", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-mooq9Q+jLa18VoM7HouczmslZU60iiB0aKc/Ztnq/luIL1ud0z4DnYprLR/ZO1gp331S9tJctM1HZr7u6YKBXQ=="], + + "@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.46", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1129.0", "", { "dependencies": { "@aws-sdk/core": "^3.978.0", "@aws-sdk/nested-clients": "^3.997.45", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Sbl3rpzQdsG4ZK2zh0JWUYyZPKKorJlVOddA2T0DVbKJFrsW8J6wgnslxxUH04+WaBMr4A1HzJZvZX0xUvkniA=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="], + "@babel/runtime": ["@babel/runtime@7.29.7", "", {}, "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw=="], + "@cline/agents": ["@cline/agents@0.0.82", "", { "dependencies": { "@cline/llms": "0.0.82", "@cline/shared": "0.0.82", "nanoid": "^5.1.7" } }, "sha512-J9MKxG2UIsWjdaqGfD9tj1NIn4kKvmjb0ap4IziORaI/RbhlliIw8/DTi62g2Xx0caWv+VEtekICVAGHaY1vSQ=="], + + "@cline/core": ["@cline/core@0.0.82", "", { "dependencies": { "@cline/agents": "0.0.82", "@cline/llms": "0.0.82", "@cline/shared": "0.0.82", "@modelcontextprotocol/sdk": "^1.29.0", "@opentelemetry/api": "^1.9.0", "@opentelemetry/api-logs": "^0.214.0", "@opentelemetry/exporter-logs-otlp-http": "^0.214.0", "@opentelemetry/exporter-metrics-otlp-http": "^0.214.0", "@opentelemetry/exporter-trace-otlp-http": "^0.214.0", "@opentelemetry/resources": "^2.6.1", "@opentelemetry/sdk-logs": "^0.214.0", "@opentelemetry/sdk-metrics": "^2.6.1", "@opentelemetry/sdk-trace-base": "^2.6.1", "@opentelemetry/sdk-trace-node": "^2.6.1", "@opentelemetry/semantic-conventions": "^1.40.0", "jiti": "^2.7.0", "nanoid": "^5.1.7", "node-machine-id": "^1.1.12", "simple-git": "3.36.0", "ws": "^8.20.0", "yaml": "^2.8.2", "zod": "^4.3.6" }, "peerDependencies": { "posthog-node": "^5.8.0" }, "optionalPeers": ["posthog-node"] }, "sha512-2aGd2Upker6cEJdrXRR/IMlRsbiMXi41n8FM4Pu3gXL4IWDiBees7aXErqKPK/6N8N4NV9lJKsb0n5YR1MZjPQ=="], + + "@cline/llms": ["@cline/llms@0.0.82", "", { "dependencies": { "@ai-sdk/amazon-bedrock": "^5.0.50", "@ai-sdk/anthropic": "^4.0.36", "@ai-sdk/gateway": "^4", "@ai-sdk/google": "^4.0.39", "@ai-sdk/google-vertex": "^5.0.47", "@ai-sdk/mistral": "^4.0.27", "@ai-sdk/openai": "^4.0.36", "@ai-sdk/openai-compatible": "^3.0.27", "@ai-sdk/otel": "^1.0.58", "@ai-sdk/provider": "^4.0.7", "@ai-sdk/provider-utils": "^5", "@aws-sdk/credential-providers": "^3.922.0", "@cline/shared": "0.0.82", "@jerome-benoit/sap-ai-provider": "4.8.0", "@langfuse/otel": "5.10.1", "@langfuse/tracing": "5.10.1", "@langfuse/vercel-ai-sdk": "5.9.1", "@openrouter/ai-sdk-provider": "^3", "@opentelemetry/api": "^1.9.0", "@opentelemetry/sdk-trace-node": "^2.6.1", "@streamparser/json": "^0.0.21", "ai": "^7.0.58", "ai-sdk-provider-opencode-sdk": "^3.0.1", "dify-ai-provider": "^1.1.0", "nanoid": "^5.1.7", "ollama-ai-provider-v2": "^4", "zod": "^4.3.6" }, "peerDependencies": { "@aws-sdk/client-bedrock-runtime": "^3.0.0", "ai-sdk-provider-claude-code": "^4", "ai-sdk-provider-codex-cli": "^2" }, "optionalPeers": ["@aws-sdk/client-bedrock-runtime", "ai-sdk-provider-claude-code", "ai-sdk-provider-codex-cli"] }, "sha512-kzS0f/xIZDzXJ19f2qFu5VtzzMaCAzfSNWnGLX6LIDlwIwWnJCAxw6agz/iNxMHnxESDY5Qtcr8jHnndm1/CCg=="], + + "@cline/sdk": ["@cline/sdk@0.0.82", "", { "dependencies": { "@cline/core": "0.0.82" } }, "sha512-RAkOGPCS4d5LsE41aedfNbcHu9tJR0O3wv5xK2pzhNB1sP28vGXb+gS4+QezlyISqzF7SHwJ0jlXIs3kuZo8zQ=="], + + "@cline/shared": ["@cline/shared@0.0.82", "", { "dependencies": { "aws4fetch": "^1.0.20", "jsonrepair": "^3.13.2", "zod": "^4.3.6", "zod-to-json-schema": "^3.25.1" } }, "sha512-vOG9Pa7pDxjcXptnUth8H8i0TnJAa9n//yQeR3tlIpgCbdkvr4SaQ75gV3CXxIglJzYZsP7BOr7r4KYiXWjEpQ=="], + + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.9", "", { "dependencies": { "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-R6siwR65Hm+3yfgP7o8DKhNvputQAwfoz9zTc3kyDudnomj2/BcLmD+uGQQPICjuFUp8ounPBU+jmKsocwVVAg=="], + + "@fastify/busboy": ["@fastify/busboy@2.1.1", "", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="], "@factory/droid-sdk": ["@factory/droid-sdk@0.9.1", "", { "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "@opentelemetry/api": "^1.9.0", "eventemitter3": "^5.0.1", "fast-deep-equal": "^3.1.3", "isows": "^1.0.7", "uuid": "^9.0.0", "ws": "^8.21.0", "zod": "^3.24.0" } }, "sha512-KhMAQQejZWt4jTobuE1QK0UQIc+JtRvm4NcocyaFayE7RQIdsQM0d5ClfcrOvxjND9BYLq1f45KjySjREzJUwQ=="], "@hono/node-server": ["@hono/node-server@2.1.1", "", { "peerDependencies": { "hono": "^4" } }, "sha512-ELuehkj5VCBdgEw9zs+ivkKwyzzUCSQuE96YmiPvn1ECBoZCczbFXJLeEGMTYjphP6gydh4pHMqEYPVMYUVgQg=="], + "@jerome-benoit/sap-ai-provider": ["@jerome-benoit/sap-ai-provider@4.8.0", "", { "dependencies": { "@ai-sdk/provider": "^3.0.13", "@ai-sdk/provider-utils": "^4.0.35", "@sap-ai-sdk/foundation-models": "^2.12.0", "@sap-ai-sdk/orchestration": "^2.12.0", "zod": "^4.4.3" }, "peerDependencies": { "ai": "^5.0.0 || ^6.0.0" } }, "sha512-rJvAW9eDFj+IFTJQ+UK0CrPyq78BG+PF6AgmHE5C0bHAalJewT119rryHqttITVpCNuX49k2ajBSax8r0+SXcg=="], + + "@kwsites/file-exists": ["@kwsites/file-exists@1.1.1", "", { "dependencies": { "debug": "^4.1.1" } }, "sha512-m9/5YGR18lIwxSFDwfE3oA7bWuq9kdau6ugN4H2rJeyhFQZcG9AgSHkQtSD15a8WvTgfz9aikZMrKPHvbpqFiw=="], + + "@kwsites/promise-deferred": ["@kwsites/promise-deferred@1.1.1", "", {}, "sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw=="], + + "@langfuse/core": ["@langfuse/core@5.11.1", "", { "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-Oor1z4BlgI5RaUrRYYKf3ijyCAx0tiGCWsABzfm6ZDAgbaaFuiwvJVtXGohiq5evcnjGGE5h4Mf4ZjH1yd5vjw=="], + + "@langfuse/otel": ["@langfuse/otel@5.10.1", "", { "dependencies": { "@langfuse/core": "^5.10.1" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "@opentelemetry/core": "^2.0.1", "@opentelemetry/exporter-trace-otlp-http": ">=0.202.0 <1.0.0", "@opentelemetry/sdk-trace-base": "^2.0.1" } }, "sha512-F2153e4PoJ1cN+5tM/xnsS44aQCQwK3p0nPk4NEpITV5pMTqiQVyvpkAvly8GKQ5Qjjr7heJ1dFtghW43ysyPQ=="], + + "@langfuse/tracing": ["@langfuse/tracing@5.10.1", "", { "dependencies": { "@langfuse/core": "^5.10.1" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0" } }, "sha512-m2kK4D0MsH8g4Og6KpnlYk8NLdQTYe0JR5M4KKpfNj99XXLlbdpXE/g3uJSqkcrWFhpiIb+3cyS9+uV6wQ6WtA=="], + + "@langfuse/vercel-ai-sdk": ["@langfuse/vercel-ai-sdk@5.9.1", "", { "dependencies": { "@ai-sdk/otel": "1.0.2", "@langfuse/core": "^5.9.1" }, "peerDependencies": { "@opentelemetry/api": "^1.9.0", "ai": ">=7.0.0 <8" } }, "sha512-gZPZJ3JXYTkT7W61VhbNHXgn/88+laoAQGMc082ESeXBwfuMVVPKDueZzM0xy6s0BTtPLW5WmbJ5NkLNCtR3Nw=="], + "@modelcontextprotocol/sdk": ["@modelcontextprotocol/sdk@1.30.0", "", { "dependencies": { "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", "cors": "^2.8.5", "cross-spawn": "^7.0.5", "eventsource": "^3.0.2", "eventsource-parser": "^3.0.0", "express": "^5.2.1", "express-rate-limit": "^8.2.1", "hono": "^4.11.4", "jose": "^6.1.3", "json-schema-typed": "^8.0.2", "pkce-challenge": "^5.0.0", "raw-body": "^3.0.0", "zod": "^3.25 || ^4.0", "zod-to-json-schema": "^3.25.1" }, "peerDependencies": { "@cfworker/json-schema": "^4.1.1" }, "optionalPeers": ["@cfworker/json-schema"] }, "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA=="], + "@opencode-ai/sdk": ["@opencode-ai/sdk@1.18.30", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-uviJ+PZLc/D1szt33WGzzt/rqJ+IaNmRGBb+UcOXrcQANl8XXKJrp4CNQQ2UAnUJ3Ek+VqcamIwjtKHUW/OjOQ=="], + + "@openrouter/ai-sdk-provider": ["@openrouter/ai-sdk-provider@3.0.0", "", { "peerDependencies": { "ai": "^7.0.0", "zod": "^3.25.76 || ^4.1.8" } }, "sha512-m9XTSWoODH2RM5OsZpaGiN7QRR8cdP5paBWq699Tu3JVmGPBKT8xF8XwV0ZBVVsjikD/JgWfak4VSsTR4wAVbg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.1", "", {}, "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q=="], + "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="], + + "@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.11.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Tr79DyWI8itsBdg+jH+opjfrwLzX+erk1/ExkIwhWoAVjVrJIn2y5+cGjTC0Vy8fyNIA/y8wuJPZwr1T3xCZeQ=="], + + "@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="], + + "@opentelemetry/exporter-logs-otlp-http": ["@opentelemetry/exporter-logs-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/sdk-logs": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9qv2Tl/Hq6qc5pJCbzFJnzA0uvlb9DgM70yGJPYf3bA5LlLkRCpcn81i4JbcIH4grlQIWY6A+W7YG0LLvS1BAw=="], + + "@opentelemetry/exporter-metrics-otlp-http": ["@opentelemetry/exporter-metrics-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-metrics": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-Tx/59RmjBgkXJ3qnsD04rpDrVWL53LU/czpgLJh+Ab98nAroe91I7vZ3uGN9mxwPS0jsZEnmqmHygVwB2vRMlA=="], + + "@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="], + + "@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="], + + "@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="], + + "@opentelemetry/resources": ["@opentelemetry/resources@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA=="], + + "@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="], + + "@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/resources": "2.11.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-7GXXcObyHyDUUSG+L+kJoquty01bzm7ivE7+SSgXXJcHuPzGviptxwARmI2c+bnnxjexGQbJnyNlN8HxBP/Y7A=="], + + "@opentelemetry/sdk-trace": ["@opentelemetry/sdk-trace@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/resources": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA=="], + + "@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.11.0", "", { "dependencies": { "@opentelemetry/core": "2.11.0", "@opentelemetry/resources": "2.11.0", "@opentelemetry/sdk-trace": "2.11.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A=="], + + "@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.11.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.11.0", "@opentelemetry/core": "2.11.0", "@opentelemetry/sdk-trace-base": "2.11.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-CuvCMJmZxswhNLlM2LfuLOW3h3fZujA4hsG4B+Sz4dX2zvaXO8Ng74cnDHWD64gLszTlhiG3c0iNUjj4g+0/sA=="], + + "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="], + + "@sap-ai-sdk/ai-api": ["@sap-ai-sdk/ai-api@2.15.0", "", { "dependencies": { "@sap-ai-sdk/core": "^2.15.0", "@sap-cloud-sdk/connectivity": "^4.8.0", "@sap-cloud-sdk/util": "^4.8.0" } }, "sha512-tW2XwemZ5mWuYQW9P2mFxaH7xNBWwr1+AU8jnfleCE7IQ494+OKmjhWOASrfNWRRnRrcepaAbjk2earpjcIcBA=="], + + "@sap-ai-sdk/core": ["@sap-ai-sdk/core@2.15.0", "", { "dependencies": { "@sap-cloud-sdk/connectivity": "^4.8.0", "@sap-cloud-sdk/http-client": "^4.8.0", "@sap-cloud-sdk/openapi": "^4.8.0", "@sap-cloud-sdk/util": "^4.8.0" } }, "sha512-GbPq4GC6LNVoGDMDnNsAdK0PDQSl3QsJQ5Dy7Zx+1f14i6DDcHpoJWBeyDzlfEBtQ0CUgF7sRKRUt2ufnzCpdg=="], + + "@sap-ai-sdk/foundation-models": ["@sap-ai-sdk/foundation-models@2.15.0", "", { "dependencies": { "@sap-ai-sdk/ai-api": "^2.15.0", "@sap-ai-sdk/core": "^2.15.0", "@sap-cloud-sdk/connectivity": "^4.8.0", "@sap-cloud-sdk/http-client": "^4.8.0", "@sap-cloud-sdk/util": "^4.8.0" } }, "sha512-UTuFBJUQfaK1xVx041v0iWkCC/LD4DJxlvxTOCNTu1AuC1p+TTLAR2WcvxqW6vIduREMXIxOjbaDJwfr1d+kgg=="], + + "@sap-ai-sdk/orchestration": ["@sap-ai-sdk/orchestration@2.15.0", "", { "dependencies": { "@sap-ai-sdk/ai-api": "^2.15.0", "@sap-ai-sdk/core": "^2.15.0", "@sap-ai-sdk/prompt-registry": "^2.15.0", "@sap-cloud-sdk/util": "^4.8.0", "yaml": "^2.9.0" } }, "sha512-wxpzZLzhi47eFMLJbymeO9XmZajpqQVHyk6iZ0Plwa7V/aD2UlEJVDbC9FvzVgdm6z6+jkx8VD1irapW1YtfdA=="], + + "@sap-ai-sdk/prompt-registry": ["@sap-ai-sdk/prompt-registry@2.15.0", "", { "dependencies": { "@sap-ai-sdk/core": "^2.15.0", "zod": "^4.4.3" } }, "sha512-qC3ACUsV+Cw+DfjMj/GSw5DY0XymwCQDzclvr4LArZ2OH0DhMrbzJMPqBSIYCyVF8zq57PNMnJoJrx1vE8CLHQ=="], + + "@sap-cloud-sdk/connectivity": ["@sap-cloud-sdk/connectivity@4.9.1", "", { "dependencies": { "@sap-cloud-sdk/resilience": "^4.9.1", "@sap-cloud-sdk/util": "^4.9.1", "@sap/xsenv": "^6.2.0", "@sap/xssec": "^4.13.0", "async-retry": "^1.3.3", "axios": "^1.15.0", "jks-js": "^1.1.6", "jsonwebtoken": "^9.0.3", "safe-stable-stringify": "^2.5.0" } }, "sha512-0deJBmdUd1LD1O3CX/wqYjTEKmnrnLsxnrSE8CyGtzbyomjjL1ePEXB3g6QUpQKmClVyOgJG/IbpyJFGGXXL5A=="], + + "@sap-cloud-sdk/http-client": ["@sap-cloud-sdk/http-client@4.9.1", "", { "dependencies": { "@sap-cloud-sdk/connectivity": "^4.9.1", "@sap-cloud-sdk/resilience": "^4.9.1", "@sap-cloud-sdk/util": "^4.9.1", "axios": "^1.15.0" } }, "sha512-bsF8T/syitm+GI8eInOB1AfvHsTYJ8Qvg5dfGt5SGXEQjqV2amFEulPp7edNTzCLo0+BBbBa/jhvqLCvXSoV6A=="], + + "@sap-cloud-sdk/openapi": ["@sap-cloud-sdk/openapi@4.9.1", "", { "dependencies": { "@sap-cloud-sdk/connectivity": "^4.9.1", "@sap-cloud-sdk/http-client": "^4.9.1", "@sap-cloud-sdk/resilience": "^4.9.1", "@sap-cloud-sdk/util": "^4.9.1", "axios": "^1.15.0" } }, "sha512-o46XbWWjo4piTPsLWPE9yuGCNS8jIj1GfZSJpaHdWDO6ZF+CqaUwF21UtGTg0SJggoe6AsJlhVObL7QA6urcJg=="], + + "@sap-cloud-sdk/resilience": ["@sap-cloud-sdk/resilience@4.9.1", "", { "dependencies": { "@sap-cloud-sdk/util": "^4.9.1", "async-retry": "^1.3.3", "axios": "^1.15.0", "opossum": "^10.0.0" } }, "sha512-14Hdd2VqEd3BarikR/ChZjquQuelC+CP69HKdMycADmmsuQEwaJkSXURW8cDl19bfx8Re1ldM/4Ig0gbCY4x/A=="], + + "@sap-cloud-sdk/util": ["@sap-cloud-sdk/util@4.9.1", "", { "dependencies": { "axios": "^1.15.0", "logform": "^2.7.0", "voca": "^1.4.1", "winston": "^3.19.0", "winston-transport": "^4.9.0" } }, "sha512-1tuv+c6o2Y8/1F3UEdqiv3TPWzZDpCjxuDpAFtFBM8C+weum8ixgwc2YXEWvBzWvtSEl7C5L/o8jBqiuhcXF4w=="], + + "@sap/xsenv": ["@sap/xsenv@6.2.2", "", { "dependencies": { "debug": "4.4.3", "node-cache": "^5.1.2", "verror": "1.10.1" } }, "sha512-SGn+yaVKeXND/rQaQhqTZC+M6g5ASW1YnqAdQiZqNL74E1T1kUsDHttgOjQNkLObMZ0fsxgQxRPMnuVcAgIODg=="], + + "@sap/xssec": ["@sap/xssec@4.15.0", "", { "dependencies": { "debug": "^4.4.3", "jwt-decode": "^4" } }, "sha512-I9TMMaWq4BNDNt9blJy8o2FV9uXjs2RyNTT7KBv9fK83bGoQxcipQ8e4DrxYb296GXg+HhE420kHuqmyXZSOuQ=="], + + "@simple-git/args-pathspec": ["@simple-git/args-pathspec@1.0.3", "", {}, "sha512-ngJMaHlsWDTfjyq9F3VIQ8b7NXbBLq5j9i5bJ6XLYtD6qlDXT7fdKY2KscWWUF8t18xx052Y/PUO1K1TRc9yKA=="], + + "@simple-git/argv-parser": ["@simple-git/argv-parser@1.1.1", "", { "dependencies": { "@simple-git/args-pathspec": "^1.0.3" } }, "sha512-Q9lBcfQ+VQCpQqGJFHe5yooOS5hGdLFFbJ5R+R5aDsnkPCahtn1hSkMcORX65J2Z5lxSkD0lQorMsncuBQxYUw=="], + + "@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-u4EAkaviMcQYlorFSnxFUgF/Dnbgiu2TzGmqOU4h3CL4MbvrYyNCXpFkcBwvAEU11EvspKX2zzS2uX7q+43VdQ=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.8.0", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.12.1", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.18.0", "tslib": "^2.6.2" } }, "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.7.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow=="], + + "@smithy/types": ["@smithy/types@4.18.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.5.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "tslib": "^2.6.2" } }, "sha512-7wNWV7SugHpcMA7uzEawJNpE0GrasXM7a9E+1+Wm6NxVuDClESac/AKt+G7jMZNUz5vBLWqKlqVV7Sv7AtUr6Q=="], + + "@so-ric/colorspace": ["@so-ric/colorspace@1.1.6", "", { "dependencies": { "color": "^5.0.2", "text-hex": "1.0.x" } }, "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw=="], + "@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@streamparser/json": ["@streamparser/json@0.0.21", "", {}, "sha512-v+49JBiG1kmc/9Ug79Lz9wyKaRocBgCnpRaLpdy7p0d3ICKtOAfc/H/Epa1j3F6YdnzjnZKKrnJ8xnh/v1P8Aw=="], + "@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="], "@types/bun": ["@types/bun@1.3.12", "", { "dependencies": { "bun-types": "1.3.12" } }, "sha512-DBv81elK+/VSwXHDlnH3Qduw+KxkTIWi7TXkAeh24zpi5l0B2kUg9Ga3tb4nJaPcOFswflgi/yAvMVBPrxMB+A=="], "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + + "@workflow/serde": ["@workflow/serde@4.1.0", "", {}, "sha512-pav4F2BoirECWR7Nf1TKt+2eETcBj7jj4cBefQ8VXQCA6NPkaKeLfj/zMgi+3zYV5ZIBT4GuUiphsj0/b9hPQQ=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], "accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ai": ["ai@7.0.97", "", { "dependencies": { "@ai-sdk/gateway": "4.0.78", "@ai-sdk/provider": "4.0.13", "@ai-sdk/provider-utils": "5.0.39" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-tQGZ234p/bk5X/LNQdB43a5/z1Bve7wK0EOR1AwN6xrosZih4MIC/3pXhcnytcpBJHMbhLISW6J1dFTd/tnwPA=="], + + "ai-sdk-provider-opencode-sdk": ["ai-sdk-provider-opencode-sdk@3.0.6", "", { "dependencies": { "@ai-sdk/provider": "^3.0.8", "@ai-sdk/provider-utils": "^4.0.15", "@opencode-ai/sdk": "^1.2.15" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CZ2I5z96HUdEKAT1ExhdxLE1bjD8efzT7I5GjBevWgwNGgYZjMPE7ZfQCJFVAzWqODwdTdxp7KSZjnmcd4h1Bw=="], + "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "assert-plus": ["assert-plus@1.0.0", "", {}, "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "async-retry": ["async-retry@1.3.3", "", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="], + + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], + + "aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="], + + "axios": ["axios@1.20.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.6", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^2.1.0" } }, "sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg=="], + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], "better-sqlite3": ["better-sqlite3@12.9.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-wqUv4Gm3toFpHDQmaKD4QhZm3g1DjUBI0yzS4UBl6lElUmXFYdTQmmEDpAFa5o8FiFiymURypEnfVHzILKaxqQ=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], "body-parser": ["body-parser@2.3.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^2.0.0", "debug": "^4.4.3", "http-errors": "^2.0.1", "iconv-lite": "^0.7.2", "on-finished": "^2.4.1", "qs": "^6.15.2", "raw-body": "^3.0.2", "type-is": "^2.1.0" } }, "sha512-2cGmJupaNgg+QUwVLAucDuWuoMZ6EX9iHDRswZ5lsNYEmwPaRknMPCLZz07yTzVq/83p4o/wzbDZbBrTvGGTIw=="], + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + "bun-types": ["bun-types@1.3.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-HqOLj5PoFajAQciOMRiIZGNoKxDJSr6qigAttOX40vJuSp6DN/CxWp9s3C1Xwm4oH7ybueITwiaOcWXoYVoRkA=="], "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], @@ -95,6 +315,18 @@ "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], + + "color": ["color@5.0.3", "", { "dependencies": { "color-convert": "^3.1.3", "color-string": "^2.1.3" } }, "sha512-ezmVcLR3xAVp8kYOm4GS45ZLLgIE6SPAFoduLr6hTDajwb3KZ2F46gulK3XpcwRFb5KKGCSezCBAY4Dw4HsyXA=="], + + "color-convert": ["color-convert@3.1.3", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-fasDH2ont2GqF5HpyO4w0+BcewlhHEZOFn9c1ckZdHpJ56Qb7MHhH/IcJZbBGgvdtwdwNbLvxiBEdg336iA9Sg=="], + + "color-name": ["color-name@2.1.1", "", {}, "sha512-p2FdgwVx1a9yWBHP2wI0VgShkDpgN4kZISkxdNipGBJWpa5G6b04OINlVWCyJj0JmfvcPrgqt95E9k8yvaOJFg=="], + + "color-string": ["color-string@2.1.4", "", { "dependencies": { "color-name": "^2.0.0" } }, "sha512-Bb6Cq8oq0IjDOe8wJmi4JeNn763Xs9cfrBcaylK1tPypWzyoy2G3l90v9k64kjphl/ZJjPIShFztenRomi8WTg=="], + + "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], + "content-disposition": ["content-disposition@1.1.0", "", {}, "sha512-5jRCH9Z/+DRP7rkvY83B+yGIGX96OYdJmzngqnw2SBSxqCFPd0w2km3s5iawpGX8krnwSGmF0FW5Nhr0Hfai3g=="], "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], @@ -103,24 +335,36 @@ "cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="], + "core-util-is": ["core-util-is@1.0.2", "", {}, "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="], + "cors": ["cors@2.8.6", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw=="], "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], + "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "dify-ai-provider": ["dify-ai-provider@1.1.1", "", { "dependencies": { "@ai-sdk/provider": "^2.0.0", "@ai-sdk/provider-utils": "^3.0.3", "zod": "^3.25.76" } }, "sha512-a32UiyP6AjElbic6Aao7opBCCM+5RwD34TP5YczE70UW/UvwucRAK76UMDVMAM6gqhc3FvaBeSu3NcK+NAUg4g=="], + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], @@ -131,6 +375,8 @@ "es-object-atoms": ["es-object-atoms@1.1.2", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw=="], + "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], @@ -147,16 +393,32 @@ "express-rate-limit": ["express-rate-limit@8.7.0", "", { "dependencies": { "debug": "^4.4.3", "ip-address": "^10.2.0" }, "peerDependencies": { "express": ">= 4.11" } }, "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g=="], + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "extsprintf": ["extsprintf@1.4.1", "", {}, "sha512-Wrk35e8ydCKDj/ArClo1VrPVmN8zph5V4AtHwIuHhvMXsKf73UT3BOD+azBIW+3wOJ4FhEH7zyaJCFvChjYvMA=="], + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="], "fast-uri": ["fast-uri@3.1.7", "", {}, "sha512-dOvZVzjdZdz7phd9v6jCbwxrBW3fK6n8Rc0CtdmM4bumzMnxywBYhuph6J819RRw/ku+rLbelwfMunktuzVVHg=="], + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + "file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="], "finalhandler": ["finalhandler@2.1.1", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA=="], + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "follow-redirects": ["follow-redirects@1.16.0", "", { "peerDependencies": { "debug": "*" }, "optionalPeers": ["debug"] }, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + + "form-data": ["form-data@4.0.6", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.4", "mime-types": "^2.1.35" } }, "sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], "fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="], @@ -165,22 +427,34 @@ "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + "gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], + "google-auth-library": ["google-auth-library@10.9.1", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.1.4", "gcp-metadata": "8.1.2", "google-logging-utils": "1.1.3", "jws": "^4.0.0" } }, "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], + "hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="], "hono": ["hono@4.13.7", "", {}, "sha512-c8/gF9ac8Y78/agExVocyLevgR+JlpNB444Py0FSX8pJoPdYUfUzRcXtYEYGwt6l19qIlVZPN5Mfsw9jFShmQQ=="], "http-errors": ["http-errors@2.0.1", "", { "dependencies": { "depd": "~2.0.0", "inherits": "~2.0.4", "setprototypeof": "~1.2.0", "statuses": "~2.0.2", "toidentifier": "~1.0.1" } }, "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + "iconv-lite": ["iconv-lite@0.7.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ=="], "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], @@ -195,18 +469,57 @@ "is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="], + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], + + "jks-js": ["jks-js@1.1.7", "", { "dependencies": { "node-forge": "^1.4.0", "node-int64": "^0.4.0", "node-rsa": "^1.1.1" } }, "sha512-BeiDRKsAi1NwEwgx2JB/9/0tar5BNGIv+foGm1G5GgiyR35s/iUnfd/BWqYd16mLDD8qTaAVBrIcOOuVqXJZNQ=="], "isows": ["isows@1.0.7", "", { "peerDependencies": { "ws": "*" } }, "sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg=="], "jose": ["jose@6.2.12", "", {}, "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw=="], + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + + "json-schema": ["json-schema@0.4.0", "", {}, "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="], + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "json-schema-typed": ["json-schema-typed@8.0.2", "", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="], + "jsonrepair": ["jsonrepair@3.15.0", "", { "bin": { "jsonrepair": "bin/cli.js" } }, "sha512-wy8OTjwsJwQRnQJkKnMJJ9vcytRdBPAgIF/Hy6+s1dAj42BHMKiyL8JzEieIl3JY7idt8eyHwBWTO8mh/+mtwA=="], + + "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + + "jwt-decode": ["jwt-decode@4.0.0", "", {}, "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], + + "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], + + "lodash.isinteger": ["lodash.isinteger@4.0.4", "", {}, "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA=="], + + "lodash.isnumber": ["lodash.isnumber@3.0.3", "", {}, "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw=="], + + "lodash.isplainobject": ["lodash.isplainobject@4.0.6", "", {}, "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA=="], + + "lodash.isstring": ["lodash.isstring@4.0.1", "", {}, "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw=="], + + "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "media-typer": ["media-typer@1.1.1", "", {}, "sha512-yz3xRaG20c6/BOzvYoDaGtPmGscs7YivItZEEqe6GbwNfHuxu9YNmvnEkMzKldAGY4/80pRcQRZSEnhquk9XuQ=="], @@ -225,20 +538,42 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "nanoid": ["nanoid@5.1.16", "", { "bin": { "nanoid": "bin/nanoid.js" } }, "sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ=="], + "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], "negotiator": ["negotiator@1.1.0", "", { "dependencies": { "content-type": "^2.1.0" } }, "sha512-NMPBRMJgiQHjbd8phG3Vebdx4kZ1H121rbl5IkMqeOsahptB9BKo/d7oJ3zTXqTgagn2bWlNSXkh0QUGM31RYg=="], "node-abi": ["node-abi@3.89.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-6u9UwL0HlAl21+agMN3YAMXcKByMqwGx+pq+P76vii5f7hTPtKDp08/H9py6DY+cfDw7kQNTGEj/rly3IgbNQA=="], + "node-cache": ["node-cache@5.1.2", "", { "dependencies": { "clone": "2.x" } }, "sha512-t1QzWwnk4sjLWaQAS8CHgOJ+RAfmHpxFWmc36IWTiWHQfs0w5JDMBS1b1ZxQteo0vVVuWJvIUKHDkkeK7vIGCg=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "node-forge": ["node-forge@1.4.0", "", {}, "sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ=="], + + "node-int64": ["node-int64@0.4.0", "", {}, "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw=="], + + "node-machine-id": ["node-machine-id@1.1.12", "", {}, "sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ=="], + + "node-rsa": ["node-rsa@1.1.1", "", { "dependencies": { "asn1": "^0.2.4" } }, "sha512-Jd4cvbJMryN21r5HgxQOpMEqv+ooke/korixNNK3mGqfGJmy0M77WDDzo/05969+OkMy3XW1UuZsSmW9KQm7Fw=="], + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + "ollama-ai-provider-v2": ["ollama-ai-provider-v2@4.0.1", "", { "dependencies": { "@ai-sdk/provider": "^4.0.2", "@ai-sdk/provider-utils": "^5.0.5" }, "peerDependencies": { "ai": "^7.0.0", "zod": "^4.0.16" } }, "sha512-JHw/Knt4kOTSwQ0oqorbdgzukK6iE73U3MPUyOuCEBpAQGAzm80CQDwyaXMwF80yFjY21RY9qfzkoLyPFFts/w=="], + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "opossum": ["opossum@10.0.0", "", {}, "sha512-sghtqL8Usj+et06Zui0nyn0R6FFsl7cyuoU+d7MctYU0nbS7Htzjleh+tWohFqk1Rp1srrFwbFa8Vxd0O64w6A=="], + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], @@ -249,8 +584,12 @@ "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], + "protobufjs": ["protobufjs@7.6.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg=="], + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + "proxy-from-env": ["proxy-from-env@2.1.0", "", {}, "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA=="], + "pump": ["pump@3.0.4", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA=="], "qs": ["qs@6.16.0", "", { "dependencies": { "es-define-property": "^1.0.1", "side-channel": "^1.1.1" } }, "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA=="], @@ -265,10 +604,14 @@ "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "retry": ["retry@0.13.1", "", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="], + "router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], "semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], @@ -295,6 +638,10 @@ "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], + "simple-git": ["simple-git@3.36.0", "", { "dependencies": { "@kwsites/file-exists": "^1.1.1", "@kwsites/promise-deferred": "^1.1.1", "@simple-git/args-pathspec": "^1.0.3", "@simple-git/argv-parser": "^1.1.0", "debug": "^4.4.0" } }, "sha512-cGQjLjK8bxJw4QuYT7gxHw3/IouVESbhahSsHrX97MzCL1gu2u7oy38W6L2ZIGECEfIBG4BabsWDPjBxJENv9Q=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + "standardwebhooks": ["standardwebhooks@1.1.1", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ=="], "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], @@ -307,16 +654,24 @@ "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], "type-is": ["type-is@2.1.0", "", { "dependencies": { "content-type": "^2.0.0", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-faYHw0anBbc/kWF3zFTEnxSFOAGUX9GFbOBthvDdLsIlEoWOFOtS0zgCiQYwIskL9iGXZL3kAXD8OoZ4GmMATA=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "undici": ["undici@7.29.1", "", {}, "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q=="], + "undici-types": ["undici-types@7.19.2", "", {}, "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg=="], "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], @@ -327,22 +682,98 @@ "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + "verror": ["verror@1.10.1", "", { "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, "sha512-veufcmxri4e3XSrT0xwfUR7kguIkaxBeosDg00yDWhk49wdwkSUrvvsm7nc75e1PUyvIeZj6nS8VQRYz2/S4Xg=="], + + "voca": ["voca@1.4.1", "", {}, "sha512-NJC/BzESaHT1p4B5k4JykxedeltmNbau4cummStd4RjFojgq/kLew5TzYge9N2geeWyI2w8T30wUET5v+F7ZHA=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "winston": ["winston@3.19.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-LZNJgPzfKR+/J3cHkxcpHKpKKvGfDZVPS4hfJCc4cCG0CgYzvlD6yE/S3CIL/Yt91ak327YCpiF/0MyeZHEHKA=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="], + "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], "zod": ["zod@4.5.4", "", {}, "sha512-sC95tT5iHHH9gtpj6A81kh+NEaRAUFN+qlUPDUbRfOMvNf5QCBqsb3WgvnpVtK5Y+4UfA6KqufotuTvMGiTlsA=="], "zod-to-json-schema": ["zod-to-json-schema@3.25.2", "", { "peerDependencies": { "zod": "^3.25.28 || ^4" } }, "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA=="], + "@jerome-benoit/sap-ai-provider/@ai-sdk/provider": ["@ai-sdk/provider@3.0.16", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-9Av6kg0t/IN/dcYAAmEJ4B9OPhcEJqwSR+GfHEA8olRkindXpUHadc3p3cyvgFUQFTVm1G5thtsmgZ9yVb2w3A=="], + + "@jerome-benoit/sap-ai-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.16", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ukLTs9x1Xm6lxSIwbJIxYQxbZHmVeIczNntARZrBcOa6pjdBhqeZnATNnJMHa7M9dZ8Ji5NJbpKpvz7o5XyBtg=="], + + "@langfuse/vercel-ai-sdk/@ai-sdk/otel": ["@ai-sdk/otel@1.0.2", "", { "dependencies": { "@ai-sdk/provider": "4.0.0", "@opentelemetry/api": "1.9.1", "ai": "7.0.2" } }, "sha512-vmuuwOxJ5OuwMYDktABTLb9V33hwR2k77QIDPZ8mSHSo04seT88IXCmzZwLjTskHfIa5C9IRizzNy6sr2qOQWQ=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/exporter-metrics-otlp-http/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/exporter-trace-otlp-http/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="], + + "@opentelemetry/otlp-transformer/@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="], + + "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="], + + "@opentelemetry/sdk-logs/@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="], + + "@opentelemetry/sdk-metrics/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="], + + "@opentelemetry/sdk-trace/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="], + + "@opentelemetry/sdk-trace-base/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="], + + "@opentelemetry/sdk-trace-node/@opentelemetry/core": ["@opentelemetry/core@2.11.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA=="], + + "ai-sdk-provider-opencode-sdk/@ai-sdk/provider": ["@ai-sdk/provider@3.0.16", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-9Av6kg0t/IN/dcYAAmEJ4B9OPhcEJqwSR+GfHEA8olRkindXpUHadc3p3cyvgFUQFTVm1G5thtsmgZ9yVb2w3A=="], + + "ai-sdk-provider-opencode-sdk/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.51", "", { "dependencies": { "@ai-sdk/provider": "3.0.16", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8", "undici": "^6.28.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-ukLTs9x1Xm6lxSIwbJIxYQxbZHmVeIczNntARZrBcOa6pjdBhqeZnATNnJMHa7M9dZ8Ji5NJbpKpvz7o5XyBtg=="], + + "axios/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "@factory/droid-sdk/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], "body-parser/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + "dify-ai-provider/@ai-sdk/provider": ["@ai-sdk/provider@2.0.4", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-B0M50w0W43jTzZGyZrNwwaK1M79aKPw6PD/dQhkBR7vkR68yCINWBo2CbBLJrbrqYc46TaiSfsTiT+S9EmVl0w=="], + + "dify-ai-provider/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@3.0.37", "", { "dependencies": { "@ai-sdk/provider": "2.0.4", "@standard-schema/spec": "^1.0.0", "eventsource-parser": "^3.0.6", "undici": "^5.29.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mLx1SgE20xKQ87xME+9uG6pFlYPTqGDrMFrZ6XcytDnmbWGwmCIh2Xb9zFrfXUrKycSFRDet4Zovj388L9b1dg=="], + + "dify-ai-provider/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "negotiator/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], "type-is/content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="], + + "winston/@colors/colors": ["@colors/colors@1.6.1", "", {}, "sha512-dTmUJzXSuayBK+hZydEaXd2mhx61qWQwkwaBBY6LyEOVx/L9aQU5ac8eFNEsd9nrD1+zb9zvDCphLSe8g1F4Qw=="], + + "@jerome-benoit/sap-ai-provider/@ai-sdk/provider-utils/undici": ["undici@6.28.1", "", {}, "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA=="], + + "@langfuse/vercel-ai-sdk/@ai-sdk/otel/@ai-sdk/provider": ["@ai-sdk/provider@4.0.0", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-fr9Gs89prDWiuox/T+kCA+i2cJkHpxU5S+tr4megjTzRC27ZsvFhwjU/+XrqqMbvBUlfmXxTOYWy8ng45dsjIg=="], + + "@langfuse/vercel-ai-sdk/@ai-sdk/otel/ai": ["ai@7.0.2", "", { "dependencies": { "@ai-sdk/gateway": "4.0.2", "@ai-sdk/provider": "4.0.0", "@ai-sdk/provider-utils": "5.0.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-VMU08jHIDJnnKDrbC9AFa5ZsPpOTfAPRLvTRHtJk4FGAoeldmJROMxvZ2ak5lCjEJ2GP2OLPQbMRyEK8w0+S4A=="], + + "ai-sdk-provider-opencode-sdk/@ai-sdk/provider-utils/undici": ["undici@6.28.1", "", {}, "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA=="], + + "axios/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + + "dify-ai-provider/@ai-sdk/provider-utils/undici": ["undici@5.29.0", "", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="], + + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "@langfuse/vercel-ai-sdk/@ai-sdk/otel/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@4.0.2", "", { "dependencies": { "@ai-sdk/provider": "4.0.0", "@ai-sdk/provider-utils": "5.0.0", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Jz1BiiTSvhDsCBJrkFRSqLHDRMVjFtYk9GdbSi3UOqY+/epza+oIESMDzfN4m+YHT/1IYmNEmxaMfjXOvxKDjQ=="], + + "@langfuse/vercel-ai-sdk/@ai-sdk/otel/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@5.0.0", "", { "dependencies": { "@ai-sdk/provider": "4.0.0", "@standard-schema/spec": "^1.1.0", "@workflow/serde": "4.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-zj66M02jc6ASYwIgWZowsooDUwaVngeNZQ3H10GwcPMZ+KR6gHMhcUuKl6tkai+JPXTKDyHY1pnszuxRtw2D4A=="], } } diff --git a/ts/package.json b/ts/package.json index 2a0550b..2a1a771 100644 --- a/ts/package.json +++ b/ts/package.json @@ -1,6 +1,6 @@ { "name": "@twaldin/harness-ts", - "version": "0.2.26", + "version": "0.2.27", "type": "module", "description": "Harness: CLI command construction and output parsing for AI coding agents", "main": "./dist/index.js", @@ -17,7 +17,7 @@ "LICENSE" ], "scripts": { - "build": "bun build src/index.ts --outdir=dist --target=node --external @factory/droid-sdk/node --external ws && cp ../src/harness/_omp_sdk.mjs dist/omp-sdk.mjs && cp ../src/harness/_amp_sdk.mjs dist/amp-sdk.mjs && cp src/claude-sdk-worker.mjs dist/claude-sdk-worker.mjs && tsc -p tsconfig.build.json", + "build": "bun build src/index.ts --outdir=dist --target=node --external @factory/droid-sdk/node --external ws && cp ../src/harness/_omp_sdk.mjs dist/omp-sdk.mjs && cp ../src/harness/_amp_sdk.mjs dist/amp-sdk.mjs && cp ../src/harness/_cline_sdk.mjs dist/cline-sdk.mjs && cp src/claude-sdk-worker.mjs dist/claude-sdk-worker.mjs && tsc -p tsconfig.build.json", "prepare": "bun run build", "prepublishOnly": "bun run build", "test": "bun test" @@ -36,6 +36,7 @@ "devDependencies": { "@factory/droid-sdk": "0.9.1", "@anthropic-ai/claude-agent-sdk": "0.3.263", + "@cline/sdk": "0.0.82", "@types/better-sqlite3": "^7.6.0", "@types/bun": "latest", "@types/ws": "^8.18.0", diff --git a/ts/src/index.ts b/ts/src/index.ts index b8805ac..1fbdcaa 100644 --- a/ts/src/index.ts +++ b/ts/src/index.ts @@ -11,6 +11,7 @@ export type { InstructionProjection, ProjectInstructionsOptions, PreparedCommand export type { RunSubprocessOptions } from './subproc.js' export type { SessionBackend, JsonObject, SessionReference, OmpSdkOptions, ClaudeSdkOptions, ClaudeSettingSource, AmpSdkOptions, AmpEffort, AmpVisibility, + ClineSdkOptions, ClineApproval, ClineFeatures, OpenCodeOptions, OpenCodeAuth, OpenCodeApprovalResponse, OpenHandsOptions, FactoryDroidOptions, FactoryDroidCallback, FactoryDroidAutonomy, SessionSpec, SessionCapabilities, SessionTurnStatus, SessionEvent, SessionTurnResult, SessionTurn, diff --git a/ts/src/owned-process.ts b/ts/src/owned-process.ts index c122573..23bd716 100644 --- a/ts/src/owned-process.ts +++ b/ts/src/owned-process.ts @@ -1,5 +1,5 @@ -// One owned child process per live session, shared by every process-backed -// backend (Pi RPC, the OMP SDK bridge and the Factory Droid SDK transport): +// One owned child process, shared by Pi RPC, the OMP/Claude/Cline SDK workers +// and the Factory Droid SDK transport: // bounded stderr capture, strict LF framing of stdout within MAX_FRAME_BYTES, // serialized backpressured stdin writes, exit/EOF classification and the // bounded teardown TERM → GRACE_MS → KILL → DRAIN_MS reap/pipe drain of the @@ -174,8 +174,14 @@ export class OwnedChild { this.stopped = true } - /** Serialized, backpressured stdin write; a write failure is reported as `disconnected`. */ - write(line: string): void { + /** + * Serialized, backpressured stdin write; a write failure is reported as + * `disconnected`. The returned promise settles (never rejects) when this + * queued write finishes or the transport stops, so a caller streaming on someone else's behalf + * — the Cline session forwarding a parent-owned command's output — can + * hand this child's backpressure back to its own producer. + */ + write(line: string): Promise { this.#writes = this.#writes.then(async () => { if (this.stopped) return const stdin = this.#child.stdin @@ -198,6 +204,7 @@ export class OwnedChild { }).catch((err: unknown) => { this.#fail('disconnected', `stdin write failed: ${describeError(err)}`) }) + return this.#writes } /** diff --git a/ts/src/sessions.ts b/ts/src/sessions.ts index 918ba7a..b4a7162 100644 --- a/ts/src/sessions.ts +++ b/ts/src/sessions.ts @@ -1,7 +1,7 @@ // Live sessions: single-consumer bounded event iterators, serial turns and an // owned transport per session. `LiveSession` is the public abstract handle; // the process implementation below owns one child per session with strict -// JSONL framing on stdout and correlated requests on stdin, shared by three +// JSONL framing on stdout and correlated requests on stdin, shared by four // harnesses: // // - `pi` on `rpc`: `pi --mode rpc` (protocol of @earendil-works/pi-coding-agent @@ -23,6 +23,15 @@ // the exact native `result` message, the persisted transcript is learned // from `{type:'sdk_reference'}` once a native hook observed it, and fatal // native failures arrive as `{type:'sdk_failure', status, error, raw?}`. +// - `cline` on `sdk`: an owned Node child running the shared worker +// `_cline_sdk.mjs`, which loads the caller-installed @cline/sdk 0.0.82 +// against an explicit builtin-only profile. Same request framing plus +// `approval`; native `CoreSessionEvent`s arrive as `sdk_event`, a turn +// completes on `{type:'sdk_settled', result}` carrying the exact native +// `AgentResult`, and every native command is delegated back to this +// process (`shell_request` / `shell_cancel`), which owns the command +// process groups through the shared subprocess runner and answers with +// `shell_output` / `shell_result`. // // Ownership mirrors the one-shot engine in lifecycle.ts through the shared // `OwnedChild` (owned-process.ts): the child leads a fresh POSIX process @@ -44,25 +53,26 @@ // Agent Server (openhands.ts, with the optional `ws` peer). Session-only: // `getAdapter('openhands')` stays unknown-harness. import type { ChildProcess } from 'node:child_process' -import { closeSync, existsSync, openSync, readSync, realpathSync } from 'node:fs' +import { closeSync, existsSync, mkdtempSync, openSync, readSync, realpathSync, rmSync } from 'node:fs' +import { tmpdir } from 'node:os' import { basename, isAbsolute, join, resolve, sep } from 'node:path' import { fileURLToPath } from 'node:url' -import type { Backend, ErrorCode, PermissionPolicy } from './base.js' +import type { Backend, ErrorCode, OutputStream, PermissionPolicy, SubprocOutcome } from './base.js' import { HarnessError } from './base.js' import { cleanupCommand, prepareCommand } from './instructions.js' import type { PreparedCommand } from './instructions.js' -import { assertSupportedPlatform, describeError } from './lifecycle.js' +import { DRAIN_MS, GRACE_MS, assertSupportedPlatform, describeError, prepareLaunch, runLifecycle } from './lifecycle.js' import { MAX_FRAME_BYTES, OwnedChild, inheritedEnv, spawnGroupLeader } from './owned-process.js' import type { OwnedChildFailure } from './owned-process.js' import { getAdapter } from './registry.js' export { MAX_FRAME_BYTES } -/** `rpc`: Pi and caller-owned OpenCode/OpenHands servers. `sdk`: OMP, Claude, Amp and Factory Droid through their qualified local runtimes. Other pairings (including `cli`) reject. */ +/** `rpc`: Pi and caller-owned OpenCode/OpenHands servers. `sdk`: OMP, Claude, Amp, Cline and Factory Droid through their qualified local runtimes. Other pairings (including `cli`) reject. */ export type SessionBackend = 'rpc' | 'sdk' /** The harnesses with a live session backend. `openhands` has no CLI adapter. */ -type SessionHarness = 'pi' | 'omp' | 'opencode' | 'claude-code' | 'amp' | 'factory-droid' | 'openhands' +type SessionHarness = 'pi' | 'omp' | 'opencode' | 'claude-code' | 'amp' | 'cline' | 'factory-droid' | 'openhands' /** * Selection of the caller-installed OMP SDK. Nothing here is guessed: the @@ -100,6 +110,41 @@ export interface ClaudeSdkOptions { settingsFile?: string } +/** Native Cline feature selection; only the explicit builtin-only profile is qualified. */ +export type ClineFeatures = 'builtin-only' + +/** How native tool approvals are answered. */ +export type ClineApproval = 'upstream' | 'callback' + +/** + * Selection of the caller-installed Cline local SDK (@cline/sdk 0.0.82 with + * its @cline/core / @cline/shared transitives, resolved by the worker). + * Nothing is discovered or defaulted: the package, the Cline root and the + * native provider are explicit, and `features` names the one profile this + * integration qualified. `builtin-only` is not a silently accepted default: + * it selects the native builtin tools and explicitly excludes hooks, + * plugins, rules, skills, workflows, MCP servers, subagents, teams, + * checkpoints, custom runtime hooks and command detachment, none of which + * this session owns. + */ +export interface ClineSdkOptions { + /** Absolute path to the caller-installed `@cline/sdk` package directory (loaded by the worker, never by this process). */ + packageRoot: string + /** Absolute caller-selected Cline root: the worker pins `CLINE_DIR` to it and every native data root under its `data` subdirectory. */ + configDir: string + /** Native provider ID, passed through verbatim (trimmed); the worker takes the model from that provider's stored profile when `model` is omitted. */ + provider: string + /** Must be `builtin-only`; any other selection is `unsupported-capability`. */ + features: ClineFeatures + /** `upstream` (default) leaves the SDK's native auto-approval defaults intact; `callback` sets the native `*` policy to `autoApprove: false` and routes every request through `respondApproval`. */ + approval?: ClineApproval +} + +/** `ClineSdkOptions` after validation; `approval` carries its resolved default. */ +export interface ResolvedClineSdkOptions extends ClineSdkOptions { + readonly approval: ClineApproval +} + /** Native Amp reasoning effort; omitted leaves the CLI default. */ export type AmpEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high' | 'xhigh' | 'max' @@ -240,13 +285,13 @@ export interface SessionSpec { harness: string /** Local absolute directory for process sessions; literal absolute POSIX directory on OpenCode/OpenHands servers (never resolved or created locally). */ workdir: string - /** Required; `rpc` for Pi/OpenCode/OpenHands, `sdk` for OMP/Claude/Amp/Factory Droid. */ + /** Required; `rpc` for Pi/OpenCode/OpenHands, `sdk` for OMP/Claude/Amp/Cline/Factory Droid. */ backend: SessionBackend - /** Native trimmed selector; Factory sends `modelId` for new sessions only; OpenCode requires provider/model. Amp rejects this (ampSdk.mode routes models). OpenHands requires the exact model declared by the selected profile. Omission otherwise preserves native selection. */ + /** Native trimmed selector; passed to the OMP or Cline SDK worker on `sdk`; Cline falls back to the selected provider profile's stored model (which the worker requires to exist). Factory sends `modelId` for new sessions only; OpenCode requires provider/model. Amp rejects this (ampSdk.mode routes models). OpenHands requires the exact model declared by the selected profile. Omission otherwise preserves native selection. */ model?: string - /** Overlay on inherited process env, never mutated. OMP/Claude profile variables and Amp's AMP_SKIP_UPDATE_CHECK are protected; AMP_URL selects Amp's endpoint. Factory requires a nonempty FACTORY_API_KEY and protects SDK attribution. Must be empty on server sessions. */ + /** Overlay on inherited process env, never mutated. OMP/Claude profile variables and Amp's AMP_SKIP_UPDATE_CHECK are protected; AMP_URL selects Amp's endpoint. Cline protects its session-owned roots and mode variables, refuses every other `CLINE_*_DIR` / `CLINE_*_PATH` override (the worker drops them before importing the SDK) and owns `TMPDIR` (one temporary directory per session). Factory requires a nonempty FACTORY_API_KEY and protects SDK attribution. Must be empty on server sessions. */ env?: Record - /** Bare name or absolute path: Pi/Droid CLI, Bun OMP worker, Node Amp worker, or Claude worker's JavaScript runtime (default process.execPath; Bun gets --no-env-file). Rejected on server sessions. */ + /** Bare name or absolute path: Pi/Droid CLI, Bun OMP worker, Node Amp or Cline worker, or Claude worker's JavaScript runtime (default process.execPath; Bun gets --no-env-file). Rejected on server sessions. */ executable?: string /** Only `upstream` is supported; `bypass` is rejected. */ permissionPolicy?: PermissionPolicy @@ -268,6 +313,8 @@ export interface SessionSpec { opencode?: OpenCodeOptions /** Required for harness `claude-code` (backend `sdk`), rejected otherwise. */ claudeSdk?: ClaudeSdkOptions + /** Required for harness `cline` (backend `sdk`), rejected otherwise. */ + clineSdk?: ClineSdkOptions /** Optional for harness `factory-droid` on `sdk` (omitted = upstream defaults), rejected otherwise. */ factoryDroid?: FactoryDroidOptions /** Required for harness `openhands`, rejected otherwise. */ @@ -297,6 +344,8 @@ export interface ResolvedSessionSpec { readonly opencode: Readonly | null /** Frozen copy of the caller's selection for `claude-code`; null otherwise. */ readonly claudeSdk: Readonly | null + /** Frozen copy of the caller's selection for `cline` plus the resolved approval mode; null otherwise. */ + readonly clineSdk: ResolvedClineSdkOptions | null /** Frozen native policy for `factory-droid`; null for every other harness. */ readonly factoryDroid: Readonly> | null /** Frozen, endpoint-normalized selection for `openhands`; null otherwise. */ @@ -396,6 +445,14 @@ export const AMP_THREAD_ID = /^T-[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4} const AMP_DEFAULT_ENDPOINT = 'https://ampcode.com' const AMP_EFFORTS: readonly string[] = ['none', 'minimal', 'low', 'medium', 'high', 'xhigh', 'max'] const AMP_VISIBILITIES: readonly string[] = ['private', 'unlisted', 'workspace', 'group'] +/** Native Cline bash-tool wall clock, reproduced by this process as the owning executor. */ +const CLINE_COMMAND_TIMEOUT_SECONDS = 30 +/** Per stream, matching the native tool's own bound; what the worker receives is that bounded prefix. */ +const CLINE_COMMAND_MAX_OUTPUT_BYTES = 48_000 +/** Native command requests that may be in flight at once; beyond it the worker is violating the protocol. */ +const CLINE_MAX_ACTIVE_COMMANDS = 32 +/** Bound on the owned command groups' teardown; the runner's own TERM → GRACE → KILL → DRAIN budget plus slack for the final result write. */ +const CLINE_COMMAND_TEARDOWN_MS = GRACE_MS + DRAIN_MS + 1_000 export function invalid(message: string): HarnessError { return new HarnessError(message, 'invalid-options') @@ -461,11 +518,75 @@ function ompSdkEnv(options: Readonly): Readonly): Readonly> { + const data = join(options.configDir, 'data') + return { + CLINE_DIR: options.configDir, + CLINE_DATA_DIR: data, + CLINE_SESSION_DATA_DIR: join(data, 'sessions'), + CLINE_DB_DATA_DIR: join(data, 'db'), + CLINE_SESSION_BACKEND_MODE: 'local', + CLINE_NO_AUTO_UPDATE: '1', + CLINE_RUN_AS_HUB_DAEMON: '0', + } +} + +/** A native argv from the worker: NUL-free strings only; null when the frame is malformed. */ +function stringArray(value: unknown): string[] | null { + if (!Array.isArray(value)) return null + const entries: readonly unknown[] = value + const argv: string[] = [] + for (const entry of entries) { + if (typeof entry !== 'string' || entry.includes('\0')) return null + argv.push(entry) + } + return argv +} + +/** + * What the native tool receives as the command's output: the captured + * stdout prefix, then a marked stderr section when there is one. This + * process is the owning executor, so a bound it enforced is stated in the + * output instead of silently shortening it. + */ +function commandOutput(outcome: Required): string { + const stdout = outcome.stdout + (outcome.stdoutTruncated ? `\n[stdout truncated at ${CLINE_COMMAND_MAX_OUTPUT_BYTES} bytes]` : '') + if (outcome.stderr === '' && !outcome.stderrTruncated) return stdout + return `${stdout}\n[stderr]\n${outcome.stderr}${outcome.stderrTruncated ? `\n[stderr truncated at ${CLINE_COMMAND_MAX_OUTPUT_BYTES} bytes]` : ''}` +} + +/** Anything but a clean exit 0 is a native tool error; the captured output still travels with it, never as a silent success. */ +function commandError(outcome: Required): string | null { + switch (outcome.termination) { + case 'exited': + if (outcome.exitCode !== 0) return `command exited with code ${outcome.exitCode}` + break + case 'signaled': + return `command was terminated by ${outcome.signal ?? 'a signal'}` + case 'timed-out': + return `command exceeded the ${CLINE_COMMAND_TIMEOUT_SECONDS}s timeout` + case 'cancelled': + return 'command was cancelled' + case 'launch-failed': + return `command could not be launched (${outcome.launchError ?? 'unknown error'})` + default: + return outcome.callbackError ?? `command ended with termination ${JSON.stringify(outcome.termination)}` + } + return outcome.callbackError +} + /** Which backend each session harness is qualified on; anything else is `unsupported-backend`. */ -const SESSION_BACKENDS: Readonly> = { pi: 'rpc', omp: 'sdk', opencode: 'rpc', 'claude-code': 'sdk', amp: 'sdk', 'factory-droid': 'sdk', openhands: 'rpc' } +const SESSION_BACKENDS: Readonly> = { pi: 'rpc', omp: 'sdk', opencode: 'rpc', 'claude-code': 'sdk', amp: 'sdk', cline: 'sdk', 'factory-droid': 'sdk', openhands: 'rpc' } function isSessionHarness(name: string): name is SessionHarness { - return name === 'pi' || name === 'omp' || name === 'opencode' || name === 'claude-code' || name === 'amp' || name === 'factory-droid' || name === 'openhands' + return name === 'pi' || name === 'omp' || name === 'opencode' || name === 'claude-code' || name === 'amp' || name === 'cline' || name === 'factory-droid' || name === 'openhands' } /** Resolve the harness first, then validate the backend before rejecting an unsupported pairing. `openhands` is session-only: it never goes through the adapter registry. */ @@ -476,7 +597,7 @@ function requireSessionHarness(name: unknown, backend: unknown): { harness: Sess throw invalid(`Unknown backend: ${JSON.stringify(backend)}. Expected one of: cli, rpc, sdk`) } if (!isSessionHarness(resolved)) { - throw new HarnessError(`Harness "${name}" has no live session backend; only "pi" (rpc), "opencode" (rpc), "openhands" (rpc), "omp" (sdk), "claude-code" (sdk), "amp" (sdk) and "factory-droid" (sdk) are supported`, 'unsupported-backend') + throw new HarnessError(`Harness "${name}" has no live session backend; only "pi" (rpc), "opencode" (rpc), "openhands" (rpc), "omp" (sdk), "claude-code" (sdk), "amp" (sdk), "cline" (sdk) and "factory-droid" (sdk) are supported`, 'unsupported-backend') } const qualified = SESSION_BACKENDS[resolved] if (backend !== qualified) { @@ -504,7 +625,7 @@ export function getSessionCapabilities(name: string, backend: Backend = 'rpc'): followUp: true, resume: true, concurrentTurns: false, - approval: resolved.harness === 'opencode' || resolved.harness === 'claude-code', + approval: resolved.harness === 'opencode' || resolved.harness === 'claude-code' || resolved.harness === 'cline', } } @@ -609,6 +730,33 @@ function resolveClaudeSdk(raw: unknown): Readonly { }) } +/** Validate the caller's Cline SDK selection; every field is explicit, nothing is probed on disk (the worker qualifies the runtime and the package versions). */ +function resolveClineSdk(raw: unknown): ResolvedClineSdkOptions { + if (!isJsonObject(raw)) throw invalid('clineSdk must be a ClineSdkOptions object') + const unsupported = Object.keys(raw).find((key) => !['packageRoot', 'configDir', 'provider', 'features', 'approval'].includes(key)) + if (unsupported !== undefined) throw invalid(`clineSdk contains an unsupported option ${JSON.stringify(unsupported)}`) + const { packageRoot, configDir, provider, features, approval } = raw + if (typeof packageRoot !== 'string' || !isAbsolute(packageRoot) || packageRoot.includes('\0')) { + throw invalid('clineSdk.packageRoot must be the absolute path of the installed @cline/sdk package') + } + if (typeof configDir !== 'string' || !isAbsolute(configDir) || configDir.includes('\0')) { + throw invalid('clineSdk.configDir must be the absolute path of the caller-selected Cline root') + } + if (typeof provider !== 'string' || provider.trim() === '' || provider.includes('\0')) { + throw invalid('clineSdk.provider must be a non-blank native provider ID') + } + if (features !== 'builtin-only') { + throw new HarnessError( + `clineSdk.features must be "builtin-only", got ${JSON.stringify(features)}: native hooks, plugins, rules, skills, workflows, MCP servers, subagents, teams, checkpoints and detached commands are not owned by this session`, + 'unsupported-capability', + ) + } + if (approval !== undefined && approval !== 'upstream' && approval !== 'callback') { + throw invalid(`clineSdk.approval must be "upstream" or "callback", got ${JSON.stringify(approval)}`) + } + return Object.freeze({ packageRoot, configDir, provider: provider.trim(), features: 'builtin-only', approval: approval ?? 'upstream' }) +} + function isAmpEffort(value: unknown): value is AmpEffort { return typeof value === 'string' && AMP_EFFORTS.includes(value) } @@ -828,6 +976,16 @@ function resolveSessionSpec(spec: SessionSpec): ResolvedSessionSpec { } else if (rawClaudeSdk !== undefined) { throw invalid('claudeSdk is only accepted for harness "claude-code" on backend "sdk"') } + const rawClineSdk: unknown = spec.clineSdk + let clineSdk: ResolvedClineSdkOptions | null = null + if (harness === 'cline') { + if (rawClineSdk === undefined) { + throw invalid('clineSdk is required for harness "cline" on backend "sdk": packageRoot, configDir, provider and features are never guessed') + } + clineSdk = resolveClineSdk(rawClineSdk) + } else if (rawClineSdk !== undefined) { + throw invalid('clineSdk is only accepted for harness "cline" on backend "sdk"') + } const rawAmpSdk: unknown = spec.ampSdk if (harness === 'amp') { if (rawAmpSdk === undefined) throw invalid('ampSdk is required for harness "amp": packageRoot, cliPath, executor and mode are never guessed') @@ -911,6 +1069,24 @@ function resolveSessionSpec(spec: SessionSpec): ResolvedSessionSpec { throw invalid(`env.CLAUDE_CONFIG_DIR ${JSON.stringify(explicit)} conflicts with claudeSdk.configDir ${JSON.stringify(claudeSdk.configDir)}`) } } + if (clineSdk !== null) { + const owned = clineSdkEnv(clineSdk) + for (const [key, value] of Object.entries(owned)) { + const explicit = env[key] + if (explicit !== undefined && explicit !== value) { + throw invalid(`env.${key} ${JSON.stringify(explicit)} conflicts with the session-owned value ${JSON.stringify(value)} derived from clineSdk.configDir`) + } + } + for (const key of Object.keys(env)) { + // The worker deletes every other native path override before importing the SDK: honoring one here would be a silent lie. + if (key.startsWith('CLINE_') && (key.endsWith('_DIR') || key.endsWith('_PATH')) && owned[key] === undefined) { + throw new HarnessError(`env.${key} is an unsupported Cline storage override: every native path stays inside clineSdk.configDir`, 'unsupported-capability') + } + } + if (env.TMPDIR !== undefined) { + throw invalid('env.TMPDIR is not supported for cline: the session owns one temporary directory per session and pins TMPDIR to it') + } + } // The Amp endpoint derives from the layered env, so the selection resolves after it. let ampSdk: ResolvedAmpSdkOptions | null = null if (harness === 'amp') { @@ -922,7 +1098,7 @@ function resolveSessionSpec(spec: SessionSpec): ResolvedSessionSpec { } let executable: string | null = - claudeSdk !== null ? process.execPath : harness === 'omp' ? 'bun' : harness === 'amp' ? 'node' : harness === 'factory-droid' ? 'droid' : 'pi' + claudeSdk !== null ? process.execPath : harness === 'omp' ? 'bun' : harness === 'amp' || harness === 'cline' ? 'node' : harness === 'factory-droid' ? 'droid' : 'pi' const rawExecutable: unknown = spec.executable if (remote) { if (rawExecutable !== undefined) throw invalid(`executable is not supported for ${harness}: no local process is launched`) @@ -1014,6 +1190,7 @@ function resolveSessionSpec(spec: SessionSpec): ResolvedSessionSpec { ampSdk, opencode, claudeSdk, + clineSdk, factoryDroid, openhands, }) @@ -1039,9 +1216,37 @@ function claudeSdkWorkerPath(): string { return fileURLToPath(new URL('./claude-sdk-worker.mjs', import.meta.url)) } +/** + * The Cline SDK worker: `src/harness/_cline_sdk.mjs` next to the Python + * package when running from source (`sessions.ts`), the build-time copy + * `cline-sdk.mjs` beside the bundle otherwise. Resolved by path only; the + * optional SDK is loaded by the worker, never imported here. + */ +function clineSdkWorkerPath(): string { + const source = basename(fileURLToPath(import.meta.url)) === 'sessions.ts' + return fileURLToPath(new URL(source ? '../../src/harness/_cline_sdk.mjs' : './cline-sdk.mjs', import.meta.url)) +} + +/** + * Drop a session-owned temporary directory after a failed setup: the launch + * error is what the caller must see, and the directory sits under the system + * temporary root. A live session removes it through the reported cleanup + * path instead, and keeps it when that cleanup fails. + */ +function discardTempDir(dir: string | null): void { + if (dir === null) return + try { + rmSync(dir, { recursive: true, force: true }) + } catch { + // the launch failure is the reported error + } +} + /** What the leader process is called in diagnostics. */ function leaderName(spec: ResolvedSessionSpec): string { - return spec.claudeSdk !== null ? 'the Claude SDK worker' : spec.harness === 'omp' ? 'the OMP SDK bridge' : 'pi' + if (spec.claudeSdk !== null) return 'the Claude SDK worker' + if (spec.clineSdk !== null) return 'the Cline SDK worker' + return spec.harness === 'omp' ? 'the OMP SDK bridge' : 'pi' } /** Bounded native header reader shared by the process backends; OMP alone permits one leading title record. */ @@ -1370,9 +1575,10 @@ export abstract class LiveSession { /** * Answer an outstanding native permission request of this session. - * `opencode` and `claude-code` support it (`getSessionCapabilities(...).approval`); - * the request must have been observed as a `permission.asked` (opencode) - * or `claude_permission` (claude-code) event of the selected session and + * `opencode`, `claude-code` and `cline` support it (`getSessionCapabilities(...).approval`); + * the request must have been observed as a `permission.asked` (opencode), + * `claude_permission` (claude-code) or `cline_permission` (cline with + * `clineSdk.approval` set to `callback`) event of the selected session and * still be unanswered. Factory answers through onPermission/onQuestion. * OpenHands refuses: native waiting_for_confirmation fails the turn. */ @@ -1382,7 +1588,15 @@ export abstract class LiveSession { abstract close(): Promise } -/** Pi RPC, the OMP SDK bridge and the Claude SDK worker: one owned child process per session. Constructed only through `LiveSession.open`. */ +/** One owned command group serving a native `shell_request`. */ +interface CommandJob { + readonly cancel: AbortController + /** Settles once the group is gone and its result was queued; assigned right after registration, so teardown always finds it. */ + done: Promise + failure: unknown +} + +/** Pi RPC, the OMP SDK bridge, the Claude SDK worker and the Cline SDK worker: one owned child process per session. Constructed only through `LiveSession.open`. */ class ProcessSession extends LiveSession { readonly #prepared: PreparedCommand readonly #child: OwnedChild @@ -1391,8 +1605,16 @@ class ProcessSession extends LiveSession { #prelude: { frame: JsonObject; requestId: string | null; bytes: number }[] = [] #preludeBytes = 0 #reference: SessionReference | null = null - /** Bridge-local `claude_permission` IDs observed and not yet answered or cancelled (claude-code only). */ + /** Worker-local permission IDs observed and not yet answered or cancelled (`claude_permission` on claude-code, `cline_permission` on cline). */ readonly #pendingApprovals = new Set() + /** Unique temporary directory this session owns (cline only): pinned as `TMPDIR` for the worker and every command, removed once every owned group is gone. */ + readonly #tempDir: string | null + /** Environment every owned command inherits: the session-owned Cline roots, modes and `TMPDIR`, exactly what the worker pins for itself. */ + readonly #commandEnv: Readonly> + /** Live command groups by native request ID; their cancel and completion handles are kept independently of the stdout reader. */ + readonly #commands = new Map() + /** Worker-owned monotonic IDs detect replay without accumulating session-long history. */ + #commandSeq = 0 #requestSeq = 0 #turnSeq = 0 #active: ProcessTurn | null = null @@ -1400,9 +1622,11 @@ class ProcessSession extends LiveSession { #teardown: Promise | null = null #cleanupError: unknown = null - private constructor(spec: ResolvedSessionSpec, prepared: PreparedCommand, child: ChildProcess) { + private constructor(spec: ResolvedSessionSpec, prepared: PreparedCommand, child: ChildProcess, tempDir: string | null) { super(spec) this.#prepared = prepared + this.#tempDir = tempDir + this.#commandEnv = spec.clineSdk === null || tempDir === null ? {} : Object.freeze({ ...spec.env, ...clineSdkEnv(spec.clineSdk), TMPDIR: tempDir }) this.#sessionQueue = new EventQueue(spec.maxBufferBytes, () => { void this.#invalidate('protocol-error', `unconsumed session events exceeded maxBufferBytes (${spec.maxBufferBytes})`) }) @@ -1419,12 +1643,14 @@ class ProcessSession extends LiveSession { assertSupportedPlatform() const resume = spec.resume if (resume !== null && resume.sessionFile !== null) { - if (spec.claudeSdk === null) verifySessionHeader(resume, resume.sessionFile, spec.backend) - else verifyClaudeTranscript(resume, resume.sessionFile) + // Cline: the worker verifies the native manifest's ID, cwd and expected path before starting; there is no Pi/OMP JSONL header here. + if (spec.claudeSdk !== null) verifyClaudeTranscript(resume, resume.sessionFile) + else if (spec.clineSdk === null) verifySessionHeader(resume, resume.sessionFile, spec.backend) } const args: string[] = [] // Explicit layering, never a parent mutation: inherited env, then the caller's entries, then the session-owned ones. const layered: Record = { ...spec.env } + let tempDir: string | null = null if (spec.ompSdk !== null) { const worker = ompSdkWorkerPath() if (!existsSync(worker)) throw new HarnessError(`OMP SDK bridge worker is missing at ${worker}`, 'launch-failed') @@ -1441,28 +1667,51 @@ class ProcessSession extends LiveSession { packageRoot, cliPath, configDir, settingSources, settingsFile: settingsFile ?? null, cwd: spec.workdir, model: spec.model, resume, })) layered.CLAUDE_CONFIG_DIR = configDir + } else if (spec.clineSdk !== null) { + const worker = clineSdkWorkerPath() + if (!existsSync(worker)) throw new HarnessError(`Cline SDK worker is missing at ${worker}`, 'launch-failed') + const { packageRoot, configDir, provider, features, approval } = spec.clineSdk + // Owned and unique per session: the native detached-log recovery must never see another session's temporary files. + try { + tempDir = mkdtempSync(join(tmpdir(), 'harness-cline-')) + } catch (err) { + throw new HarnessError(`the Cline session temporary directory could not be created: ${describeError(err)}`, 'launch-failed') + } + // `instructions` is a flag, not content: the lease already projected CLINE.md, and the worker adds the native file mention to each prompt. + args.push(worker, JSON.stringify({ + packageRoot, configDir, provider, features, approval, cwd: spec.workdir, model: spec.model, resume, tempDir, + instructions: spec.instructions !== null, + })) + Object.assign(layered, clineSdkEnv(spec.clineSdk), { TMPDIR: tempDir }) } else { args.push(...PI_ARGS) if (spec.model !== null) args.push('--model', spec.model) if (resume !== null && resume.sessionFile !== null) args.push('--session', resume.sessionFile) } const adapter = getAdapter(spec.harness) - const prepared = prepareCommand({ - cmd: executable, - args, - cwd: spec.workdir, - env: layered, - instructionsFile: join(spec.workdir, adapter.instructionsFilename), - ...(spec.instructions === null ? {} : { instructionContent: spec.instructions }), - }) + let prepared: PreparedCommand + try { + prepared = prepareCommand({ + cmd: executable, + args, + cwd: spec.workdir, + env: layered, + instructionsFile: join(spec.workdir, adapter.instructionsFilename), + ...(spec.instructions === null ? {} : { instructionContent: spec.instructions }), + }) + } catch (err) { + discardTempDir(tempDir) + throw err + } let child: ChildProcess try { child = spawnGroupLeader(executable, args, spec.workdir, inheritedEnv(layered)) } catch (err) { cleanupCommand(prepared) + discardTempDir(tempDir) throw new HarnessError(`${leaderName(spec)} could not be launched: ${describeError(err)}`, 'launch-failed') } - const session = new ProcessSession(spec, prepared, child) + const session = new ProcessSession(spec, prepared, child, tempDir) try { await session.#request('get_state', {}) } catch (err) { @@ -1490,6 +1739,14 @@ class ProcessSession extends LiveSession { return 'get_state returned a malformed sessionFile' } if (isStreaming !== false) return 'get_state reports the agent is already streaming' + if (this.spec.clineSdk !== null) { + // The native manifest path is the session file, and the worker must have started in the requested workdir. + if (typeof sessionFile !== 'string') return 'get_state returned no native Cline manifest path' + const workdir = data.workdir + if (typeof workdir !== 'string' || !samePath(workdir, this.spec.workdir)) { + return `get_state reports workdir ${JSON.stringify(workdir)}, not the requested ${JSON.stringify(this.spec.workdir)}` + } + } const resume = this.spec.resume if (resume !== null && sessionId !== resume.sessionId) { return `${leaderName(this.spec)} resumed session ${JSON.stringify(sessionId)}, not ${JSON.stringify(resume.sessionId)}` @@ -1592,23 +1849,29 @@ class ProcessSession extends LiveSession { } /** - * Answer an outstanding `claude_permission` request (claude-code only) with - * `once` (native allow with the original input) or `reject` (native deny - * with a fixed message). The request must have been observed as an event of - * this session and be neither answered nor cancelled; the worker validates - * it again before resolving the native `canUseTool` callback. Pi RPC and the - * OMP SDK bridge expose no native permission replies. + * Answer an outstanding native permission request with `once` (native + * allow with the original input) or `reject` (native deny with a fixed + * message). Supported on `claude-code` (`claude_permission`) and on + * `cline` with `clineSdk.approval` set to `callback` (`cline_permission`): + * the request must have been observed as an event of this session and be + * neither answered nor cancelled, and the worker validates it again before + * resolving the native callback. Pi RPC, the OMP SDK bridge and a Cline + * session left on upstream approvals expose no native permission replies. */ async respondApproval(requestId: string, response: OpenCodeApprovalResponse): Promise { - if (this.spec.claudeSdk === null) { - throw new HarnessError(`${this.spec.harness} live sessions cannot answer permission requests; only "opencode" and "claude-code" support respondApproval`, 'unsupported-capability') + const cline = this.spec.clineSdk + if (this.spec.claudeSdk === null && cline === null) { + throw new HarnessError(`${this.spec.harness} live sessions cannot answer permission requests; only "opencode", "claude-code" and "cline" support respondApproval`, 'unsupported-capability') + } + if (cline !== null && cline.approval !== 'callback') { + throw new HarnessError('clineSdk.approval is "upstream": native approvals stay with the SDK, so this session never receives a request to answer', 'unsupported-capability') } const reply: unknown = response if (reply === 'always') { throw new HarnessError('permission reply "always" is unsupported: it would store a permission rule beyond this session', 'unsupported-capability') } if (reply !== 'once' && reply !== 'reject') throw invalid(`response must be "once" or "reject", got ${JSON.stringify(reply)}`) - if (typeof requestId !== 'string' || requestId === '') throw invalid('requestId must be the bridge approval ID of an observed claude_permission event') + if (typeof requestId !== 'string' || requestId === '') throw invalid('requestId must be the worker approval ID of an observed permission event') if (this.#dead) throw new HarnessError('session is closed', 'session-closed') if (!this.#pendingApprovals.has(requestId)) { throw invalid(`no outstanding permission request ${JSON.stringify(requestId)} was observed for session ${this.reference.sessionId}`) @@ -1708,9 +1971,14 @@ class ProcessSession extends LiveSession { turn.settled = true } } - if (this.spec.claudeSdk !== null && typeof frame.id === 'string') { - if (frame.type === 'claude_permission') this.#pendingApprovals.add(frame.id) - else if (frame.type === 'claude_permission_cancelled') this.#pendingApprovals.delete(frame.id) + if (typeof frame.id === 'string') { + if (this.spec.claudeSdk !== null) { + if (frame.type === 'claude_permission') this.#pendingApprovals.add(frame.id) + else if (frame.type === 'claude_permission_cancelled') this.#pendingApprovals.delete(frame.id) + } else if (this.spec.clineSdk !== null) { + if (frame.type === 'cline_permission') this.#pendingApprovals.add(frame.id) + else if (frame.type === 'cline_permission_cancelled') this.#pendingApprovals.delete(frame.id) + } } this.#route(frame, typeof frame.id === 'string' ? frame.id : null, bytes) if (turn !== null && settles) this.#maybeComplete(turn) @@ -1720,10 +1988,12 @@ class ProcessSession extends LiveSession { * Frames from an SDK worker other than responses: `sdk_event` unwraps to * the exact native event and goes through event routing; `sdk_settled` is * the worker's authoritative turn completion (carrying the exact native - * `result` on claude-code) and is never exposed as an event. The Claude - * worker additionally reports the persisted transcript (`sdk_reference`) - * and fatal native failures (`sdk_failure`). Anything else is a protocol - * violation. + * `result` on claude-code and cline) and is never exposed as an event. The + * Claude worker additionally reports the persisted transcript + * (`sdk_reference`), and either SDK worker may report a fatal native + * failure (`sdk_failure`). The Cline worker delegates its native commands + * back here (`shell_request` / `shell_cancel`), which are not + * request/response frames. Anything else is a protocol violation. */ #onBridgeFrame(frame: JsonObject, bytes: number): void { if (frame.type === 'sdk_event') { @@ -1736,11 +2006,16 @@ class ProcessSession extends LiveSession { return } const claude = this.spec.claudeSdk !== null + const cline = this.spec.clineSdk !== null + if (cline && (frame.type === 'shell_request' || frame.type === 'shell_cancel')) { + this.#onShellFrame(frame) + return + } if (claude && frame.type === 'sdk_reference') { this.#onReference(frame) return } - if (claude && frame.type === 'sdk_failure') { + if ((claude || cline) && frame.type === 'sdk_failure') { const { status, error, raw } = frame if (typeof error !== 'string' || error === '') { void this.#invalidate('protocol-error', 'sdk_failure frame has no non-empty "error"') @@ -1773,13 +2048,18 @@ class ProcessSession extends LiveSession { return } } + // The native Cline AgentResult is opaque: it carries no discriminating `type`, and its `finishReason` alone decides the turn. + if (cline && result !== undefined && !isJsonObject(result)) { + void this.#invalidate('protocol-error', 'sdk_settled "result" is not a native Cline AgentResult object') + return + } const turn = this.#active if (turn === null || turn.done) { - // OMP: an idle settle mirrors an idle agent_settled. Claude: exactly one result per prompt, so a stray settle is a violation. - if (claude) void this.#invalidate('protocol-error', 'sdk_settled arrived without an active turn') + // OMP: an idle settle mirrors an idle agent_settled. Claude and Cline: exactly one settlement per prompt, so a stray one is a violation. + if (claude || cline) void this.#invalidate('protocol-error', 'sdk_settled arrived without an active turn') return } - if (claude && typeof error !== 'string' && result === undefined) { + if ((claude || cline) && typeof error !== 'string' && result === undefined) { void this.#invalidate('protocol-error', 'sdk_settled carries neither a native result nor an error') return } @@ -1863,6 +2143,22 @@ class ProcessSession extends LiveSession { } return } + if (this.spec.clineSdk !== null) { + const result = turn.result + if (result === null) return + const reason = result.finishReason + if (reason === 'completed') { + this.#finishTurn(turn, 'completed', result, null) + } else if (reason === 'aborted') { + this.#finishTurn(turn, 'interrupted', result, null) + } else if (reason === 'max_iterations' || reason === 'mistake_limit' || reason === 'error') { + this.#finishTurn(turn, 'agent-error', result, `native run finished with reason ${reason}`) + } else { + // The native reason set is qualified; an unknown one is a protocol violation, never a guessed status. + void this.#invalidate('protocol-error', `sdk_settled result has unknown finishReason ${JSON.stringify(reason)}`, result) + } + return + } const assistant = turn.lastAssistant const stopReason = assistant?.stopReason if (stopReason === 'aborted') { @@ -1914,6 +2210,12 @@ class ProcessSession extends LiveSession { * The exit metadata of `SessionTurnResult` always describes the owned * leader (worker); a native Claude exit is described by the `sdk_failure` * frame passed as `raw`. + * + * On `cline` the owned command groups are cancelled and awaited here as + * well, alongside the worker group and before the turn settles, so no + * command outlives the session that asked for it. A command group that + * will not stop keeps the lease and the owned temporary directory and is + * reported as a cleanup failure, never as a clean disposal. */ #invalidate(status: SessionTurnStatus, error: string | null, raw: JsonObject | null = null): Promise { if (this.#teardown !== null) return this.#teardown @@ -1933,15 +2235,17 @@ class ProcessSession extends LiveSession { const turn = this.#active if (turn !== null) clearTimeout(turn.timer) const disposing = this.spec.backend === 'sdk' && child.pid !== null && child.leader === null - this.#teardown = child.stopGroup().catch((err: unknown) => { + this.#teardown = this.#stopAll().catch((err: unknown) => { this.#cleanupError = err }).then(() => { if (turn !== null) this.#finishTurn(turn, status, raw ?? turn.lastAgentEnd, error) this.#sessionQueue.end() - // A failed reap/termination must not release ownership of live resources. + // A failed reap/termination, of the worker group or of an owned command group, must not release ownership of live resources. if (this.#cleanupError !== null) return try { cleanupCommand(this.#prepared) + // Last: the temporary directory belongs to this process and outlives every owned group. + if (this.#tempDir !== null) rmSync(this.#tempDir, { recursive: true, force: true }) } catch (err) { this.#cleanupError = err return @@ -1958,26 +2262,147 @@ class ProcessSession extends LiveSession { }) return this.#teardown } + + /** Stop the worker group and every owned command group concurrently; both are awaited, and the first failure is raised once they settled. */ + async #stopAll(): Promise { + const failures: unknown[] = [] + const commands = this.#stopCommands().catch((err: unknown) => { failures.push(err) }) + const group = this.#child.stopGroup().catch((err: unknown) => { failures.push(err) }) + await commands + await group + if (failures.length > 0) throw failures[0] + } + + /** + * Cancel every owned command group and await its bounded teardown. No new + * command is accepted from here on (`#dead` is already set), and a group + * that outlasts the runner's own TERM → KILL → drain budget is a cleanup + * failure: the caller keeps the lease and the temporary directory. + */ + async #stopCommands(): Promise { + const jobs = [...this.#commands.values()] + if (jobs.length === 0) return + for (const job of jobs) job.cancel.abort() + const stopped = Promise.allSettled(jobs.map((job) => job.done)).then(() => true) + let timer: NodeJS.Timeout | undefined + const budget = new Promise((done) => { timer = setTimeout(() => done(false), CLINE_COMMAND_TEARDOWN_MS) }) + const complete = await Promise.race([stopped, budget]).finally(() => clearTimeout(timer)) + if (!complete) { + throw new HarnessError(`${this.#commands.size} owned command process group(s) did not stop within ${CLINE_COMMAND_TEARDOWN_MS}ms`, 'adapter-error') + } + const failed = jobs.find((job) => job.failure !== null) + if (failed) throw new HarnessError(`owned command cleanup could not be proved: ${describeError(failed.failure)}`, 'adapter-error') + } + + // ---- owned command execution (cline) ---- + + /** + * The Cline worker asked this process to run a native command, or to + * cancel one. The harness owns every command process group: the worker + * resolved the argv through the native shell helpers and never spawns + * anything itself, so `cmd` is executed verbatim and never interpolated + * into a shell. + */ + #onShellFrame(frame: JsonObject): void { + const id = frame.id + if (typeof id !== 'string' || id === '') { + void this.#invalidate('protocol-error', `${JSON.stringify(frame.type)} frame has no non-empty string "id"`) + return + } + if (frame.type === 'shell_cancel') { + // Idempotent by contract: a command that already finished, or whose result is in flight, has nothing left to cancel. + this.#commands.get(id)?.cancel.abort() + return + } + const turn = this.#active + if (turn === null || turn.done) { + void this.#invalidate('protocol-error', `shell_request ${JSON.stringify(id)} arrived outside an active turn`) + return + } + const sequence = /^shell-([1-9][0-9]{0,15})$/.exec(id) + const number = sequence === null ? NaN : Number(sequence[1]) + if (!Number.isSafeInteger(number) || number <= this.#commandSeq) { + void this.#invalidate('protocol-error', `shell_request has an invalid or reused sequence ${JSON.stringify(id)}`) + return + } + if (this.#commands.size >= CLINE_MAX_ACTIVE_COMMANDS) { + void this.#invalidate('protocol-error', `more than ${CLINE_MAX_ACTIVE_COMMANDS} native commands are in flight at once`) + return + } + const cmd = stringArray(frame.cmd) + if (cmd === null || cmd.length === 0 || cmd[0] === '') { + void this.#invalidate('protocol-error', `shell_request ${JSON.stringify(id)} has no "cmd" argv of NUL-free strings`) + return + } + const cwd = frame.cwd + if (typeof cwd !== 'string' || !isAbsolute(cwd) || cwd.includes('\0')) { + void this.#invalidate('protocol-error', `shell_request ${JSON.stringify(id)} has no absolute "cwd"`) + return + } + const stdin = frame.stdin + if (stdin !== null && typeof stdin !== 'string') { + void this.#invalidate('protocol-error', `shell_request ${JSON.stringify(id)} has a non-string "stdin"`) + return + } + this.#commandSeq = number + // Registered before the run starts, so teardown finds the job even if the runner refuses it synchronously. + const job: CommandJob = { cancel: new AbortController(), done: Promise.resolve(), failure: null } + this.#commands.set(id, job) + job.done = this.#runCommand(id, cmd, cwd, stdin, job) + } + + /** + * Run one native command on the shared subprocess runner: its own process + * group, the native bash tool's wall clock and per-stream bound, streamed + * progress the worker turns into native tool updates, and the runner's + * bounded teardown on cancellation. An unexpected runner error retains + * ownership rather than treating unproven cleanup as a native tool error. + */ + async #runCommand(id: string, cmd: string[], cwd: string, stdin: string | null, job: CommandJob): Promise { + const cancel = job.cancel.signal + try { + const request = prepareLaunch(cmd, { + cwd, + timeoutSeconds: CLINE_COMMAND_TIMEOUT_SECONDS, + maxOutputBytes: CLINE_COMMAND_MAX_OUTPUT_BYTES, + stdin, + extraEnv: this.#commandEnv, + cancel, + }) + const outcome = await runLifecycle(request, cancel, undefined, (chunk: string, stream: OutputStream) => + this.#child.write(`${JSON.stringify({ type: 'shell_output', id, stream, chunk })}\n`)) + const output = commandOutput(outcome) + const error = commandError(outcome) + await this.#child.write(`${JSON.stringify(error === null ? { type: 'shell_result', id, output } : { type: 'shell_result', id, output, error })}\n`) + } catch (err) { + job.failure = err + void this.#invalidate('agent-error', `command cleanup could not be proved: ${describeError(err)}`) + } finally { + if (job.failure === null) this.#commands.delete(id) + } + } } /** - * Validate the spec and open the native session. For `pi`/`omp`/`claude-code`: + * Validate the spec and open the native session. For `pi`/`omp`/`claude-code`/`cline`: * verify any resume target, take the workdir lease (projecting `instructions` - * into AGENTS.md / CLAUDE.md), spawn the leader (`pi --mode rpc`, `bun` - * running the OMP SDK bridge worker, or the JavaScript runtime running the - * Claude SDK worker, which qualifies the SDK/CLI versions and selects the - * native session ID) and complete the `get_state` handshake; rejects with - * the lease released and the process group stopped when any step fails. For - * `amp`: take the lease, run one finite `open` worker that creates the thread - * (or verifies the resume target through the SDK) and adopt its identity; - * rejects with the worker's process group stopped and the lease released. - * For `factory-droid`: verify the SDK pin and FACTORY_API_KEY, verify any - * saved resume target, take the lease, spawn droid exec stream-jsonrpc and - * complete native initialize_session/load_session through the installed SDK. - * For OpenCode/OpenHands: qualify the caller-owned server (version, profile, + * into AGENTS.md / CLAUDE.md / CLINE.md), spawn the leader (`pi --mode rpc`, + * `bun` running the OMP SDK bridge worker, `node` running the Cline SDK + * worker, which qualifies the runtime and the package versions and verifies + * the native manifest identity, or the JavaScript runtime running the Claude + * SDK worker, which qualifies the SDK/CLI versions and selects the native + * session ID) and complete the `get_state` handshake; rejects with the lease + * released, the process group stopped and any owned temporary directory + * removed when any step fails. For `amp`: take the lease, run one finite + * `open` worker that creates the thread (or verifies the resume target + * through the SDK) and adopt its identity; rejects with the worker's process + * group stopped and the lease released. For `factory-droid`: verify the SDK + * pin and FACTORY_API_KEY, verify any saved resume target, take the lease, + * spawn droid exec stream-jsonrpc and complete native + * initialize_session/load_session through the installed SDK. For + * OpenCode/OpenHands: qualify the caller-owned server (version, profile, * directory, identity) and subscribe to its stream/socket; failure closes - * owned connections without touching - * the server itself. + * owned connections without touching the server itself. */ export async function openSession(spec: SessionSpec): Promise { return LiveSession.open(spec) diff --git a/ts/tests/cline-sdk-conformance.mjs b/ts/tests/cline-sdk-conformance.mjs new file mode 100644 index 0000000..2ab83b2 --- /dev/null +++ b/ts/tests/cline-sdk-conformance.mjs @@ -0,0 +1,145 @@ +// Real optional Cline SDK and finite synthetic provider; no provider account. +import assert from 'node:assert/strict' +import { spawn } from 'node:child_process' +import { mkdtemp, mkdir, readFile, writeFile, access, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { dirname, join, resolve } from 'node:path' +import { fileURLToPath, pathToFileURL } from 'node:url' +import { createInterface } from 'node:readline' + +const root = resolve(dirname(fileURLToPath(import.meta.url)), '../..') +const packageRoot = join(root, 'ts/node_modules/@cline/sdk') +const cases = JSON.parse(await readFile(join(root, 'tests/cline_sdk_cases.json'), 'utf8')) +const collect = async turn => { const events = []; for await (const event of turn.events) events.push(event); return events } +const missing = path => assert.rejects(access(path), { code: 'ENOENT' }) +const delay = ms => new Promise(resolve => setTimeout(resolve, ms)) +async function within(promise, ms, phase) { + let timer + try { return await Promise.race([promise, new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`synthetic peer ${phase} deadline`)), ms) })]) } + finally { clearTimeout(timer) } +} + +async function configure(directory, scenario) { + const workdir = join(directory, scenario, 'work') + const home = join(directory, scenario, 'home') + const configDir = join(directory, scenario, 'config') + await Promise.all([mkdir(workdir, { recursive: true }), mkdir(home, { recursive: true }), mkdir(join(configDir, 'data/settings'), { recursive: true })]) + const provider = spawn(process.env.PYTHON ?? 'python3', [join(root, 'tests/helpers/cline_provider.py'), workdir, scenario], { detached: true, stdio: ['pipe', 'pipe', 'pipe'] }) + const exit = new Promise((resolve, reject) => { provider.once('exit', resolve); provider.once('error', reject) }) + let stderr = '' + provider.stderr.setEncoding('utf8').on('data', chunk => { stderr = (stderr + chunk).slice(-8192) }) + const lines = createInterface({ input: provider.stdout }) + const stop = async () => { + provider.stdin.end() + try { await within(exit, 5000, 'EOF shutdown') } + catch (error) { + if (provider.pid && provider.exitCode === null && provider.signalCode === null) process.kill(-provider.pid, 'SIGKILL') + await within(exit, 2000, 'forced shutdown') + throw new Error(`synthetic peer ${scenario} shutdown failed; stderr=${stderr}`, { cause: error }) + } finally { lines.close(); provider.stderr.destroy() } + } + try { + const next = await within(lines[Symbol.asyncIterator]().next(), 5000, 'readiness') + const { endpoint } = JSON.parse(next.value) + await writeFile(join(configDir, 'data/settings/providers.json'), JSON.stringify({ version: 1, modes: {}, lastUsedProvider: 'openai-compatible', providers: { + 'openai-compatible': { settings: { provider: 'openai-compatible', model: 'synthetic', apiKey: 'synthetic', baseUrl: endpoint }, updatedAt: '2026-09-10T00:00:00Z', tokenSource: 'manual' }, + } })) + return { stop, spec: { + harness: 'cline', backend: 'sdk', workdir, executable: process.env.HARNESS_TEST_NODE ?? 'node', model: 'synthetic', requestTimeoutSeconds: 10, timeoutSeconds: 12, + env: { HOME: home, NODE_OPTIONS: `--import=${pathToFileURL(join(root, 'tests/helpers/cline_faults.mjs')).href}`, HARNESS_TEST_CLINE_PACKAGE_ROOT: packageRoot, HARNESS_TEST_CLINE_SCENARIO: scenario }, + clineSdk: { packageRoot, configDir, provider: 'openai-compatible', features: 'builtin-only', approval: 'callback' }, + } } + } catch (error) { + try { await stop() } + catch (cleanup) { throw new AggregateError([error, cleanup], `synthetic peer ${scenario} startup/cleanup failed; stderr=${stderr}`) } + throw new Error(`synthetic peer ${scenario} startup failed; stderr=${stderr}`, { cause: error }) + } +} + +export async function clineConformance(api) { + const directory = await mkdtemp(join(tmpdir(), 'harness-cline-sdk-')) + let completed = false + try { + for (const scenario of cases) { + const { spec, stop } = await configure(directory, scenario.name) + let session + try { + session = await api.openSession(spec) + const turn = session.startTurn(scenario.name) + const events = [] + let operation + for await (const event of turn.events) { + events.push(event) + if (event.type === 'cline_permission') await session.respondApproval(event.raw.id, scenario.reply ?? 'once') + const native = event.raw.payload?.event + if (native?.contentType === 'tool' && native.type === 'content_update' && scenario.operation && !operation) { + operation = scenario.operation === 'interrupt' ? session.interrupt() : session.close() + } + } + const result = await turn.result + await operation + assert.equal(result.status, scenario.status, scenario.name) + assert.equal(result.sessionId, session.reference.sessionId) + assert.deepEqual(events.find(event => event.type === 'future_native_event')?.raw.payload.future, { preserved: true }) + if (scenario.name === 'success') { + assert.equal(result.raw.text, 'synthetic native reply') + assert.equal(result.raw.usage.inputTokens, 11) + assert.equal(result.raw.usage.outputTokens, 3) + } + if (scenario.name === 'command-error') { + const tool = events.find(event => event.type === 'agent_event' && event.raw.payload.event.contentType === 'tool' && event.raw.payload.event.type === 'content_end') + assert.equal(tool.raw.payload.event.output[0].success, false) + } + if (scenario.name === 'partial-wire') assert.ok(events.some(event => event.raw.payload?.event?.contentType === 'text')) + if (scenario.name === 'worker-loss') assert.equal(result.signal, 'SIGKILL') + await session.close() + await missing(join(spec.workdir, '.harness-run.lock')) + if (['interrupt', 'close', 'worker-loss'].includes(scenario.name)) await delay(3200) + await missing(join(spec.workdir, 'survived')) + } finally { try { await session?.close() } finally { await stop() } } + } + const { spec, stop } = await configure(directory, 'followup') + spec.instructions = 'Cline-owned instruction sentinel 8f26' + const instructionFile = join(spec.workdir, 'CLINE.md') + await writeFile(instructionFile, 'original caller instructions') + let session + try { + session = await api.openSession(spec) + for (const prompt of ['original-history', 'followup-history']) { + const turn = session.startTurn(prompt) + await collect(turn) + assert.equal((await turn.result).status, 'completed') + } + const reference = session.reference + await session.close() + assert.equal(await readFile(instructionFile, 'utf8'), 'original caller instructions') + session = await api.openSession({ ...spec, resume: reference }) + assert.deepEqual(session.reference, reference) + const turn = session.startTurn('resumed-history') + await collect(turn) + const result = await turn.result + assert.equal(result.status, 'completed') + assert.equal(result.raw.usage.inputTokens, 11) + await session.close() + const requests = (await readFile(join(spec.workdir, 'provider-requests.jsonl'), 'utf8')).trim().split('\n').map(line => JSON.parse(line)) + const history = JSON.stringify(requests.at(-1).messages) + for (const prompt of ['original-history', 'followup-history', 'resumed-history']) assert.ok(history.includes(prompt)) + assert.ok(history.includes(spec.instructions)) + assert.equal(await readFile(instructionFile, 'utf8'), 'original caller instructions') + const before = await readFile(reference.sessionFile, 'utf8') + const manifest = JSON.parse(before) + assert.equal(manifest.metadata.usage.inputTokens, 33) + assert.equal(manifest.metadata.usage.outputTokens, 9) + await assert.rejects(api.openSession({ ...spec, resume: { ...reference, sessionId: 'unknown-exact-id' } })) + assert.equal(await readFile(reference.sessionFile, 'utf8'), before) + manifest.cwd = dirname(spec.workdir) + await writeFile(reference.sessionFile, JSON.stringify(manifest)) + await assert.rejects(api.openSession({ ...spec, resume: reference })) + await assert.rejects(api.openSession({ ...spec, clineSdk: { ...spec.clineSdk, features: 'native-hooks' } }), { code: 'unsupported-capability' }) + await assert.rejects(api.openSession({ ...spec, clineSdk: { ...spec.clineSdk, packageRoot: join(spec.workdir, 'missing-sdk') } }), { code: 'launch-failed' }) + await assert.rejects(api.openSession({ ...spec, factoryDroid: { autonomy: 'low' } }), { code: 'invalid-options' }) + await missing(join(spec.workdir, '.harness-run.lock')) + } finally { try { await session?.close() } finally { await stop() } } + completed = true + } finally { if (completed) await rm(directory, { recursive: true }) } +} diff --git a/ts/tests/cline-sdk.test.ts b/ts/tests/cline-sdk.test.ts new file mode 100644 index 0000000..eef99ff --- /dev/null +++ b/ts/tests/cline-sdk.test.ts @@ -0,0 +1,7 @@ +import { test } from 'bun:test' +import * as api from '../src/index.js' +import { clineConformance } from './cline-sdk-conformance.mjs' + +test('Cline SDK native session conformance', async () => { + await clineConformance(api) +}, 120_000) diff --git a/ts/tests/droid-sdk-conformance.mjs b/ts/tests/droid-sdk-conformance.mjs index 445b3ef..2a19969 100644 --- a/ts/tests/droid-sdk-conformance.mjs +++ b/ts/tests/droid-sdk-conformance.mjs @@ -474,6 +474,7 @@ export async function runDroidConformance(api) { await rejects(api.openSession({ ...spec, factoryDroid }), 'invalid-options') } await rejects(api.openSession({ ...spec, ompSdk: { packageRoot: box.dir, agentDir: box.dir, auth: 'environment' } }), 'invalid-options') + await rejects(api.openSession({ ...spec, clineSdk: { packageRoot: box.dir, configDir: box.dir, provider: 'anthropic', features: 'builtin-only' } }), 'invalid-options') await rejects(api.openSession({ ...spec, opencode: { endpoint: 'http://127.0.0.1:1', auth: 'none' } }), 'invalid-options') await rejects(api.openSession({ ...spec, env: { ...spec.env, FACTORY_UPSTREAM_CLIENT_TYPE: 'cli' } }), 'invalid-options') await rejects(api.openSession({ harness: 'pi', backend: 'rpc', workdir: box.workdir, factoryDroid: {} }), 'invalid-options') diff --git a/ts/tests/node-cline-sdk.mjs b/ts/tests/node-cline-sdk.mjs new file mode 100644 index 0000000..8642821 --- /dev/null +++ b/ts/tests/node-cline-sdk.mjs @@ -0,0 +1,4 @@ +import * as api from '../dist/index.js' +import { clineConformance } from './cline-sdk-conformance.mjs' +await clineConformance(api) +console.log('Packaged Node Cline SDK conformance passed') diff --git a/ts/tests/openhands-conformance.mjs b/ts/tests/openhands-conformance.mjs index 8907119..2c408d1 100644 --- a/ts/tests/openhands-conformance.mjs +++ b/ts/tests/openhands-conformance.mjs @@ -512,6 +512,7 @@ export async function runOpenHandsConformance(api) { { instructions: 'must not write' }, { env: { HOME: '/synthetic' } }, { executable: 'python3' }, { permissionPolicy: 'bypass' }, { openhands: undefined }, { model: undefined }, { model: ' ' }, { ompSdk: spec.openhands }, { backend: 'sdk' }, { backend: 'cli' }, { claudeSdk: spec.openhands }, + { clineSdk: spec.openhands }, ]) { await assert.rejects(api.openSession({ ...spec, ...override })) } diff --git a/uv.lock b/uv.lock index d5426cc..ccb3dae 100644 --- a/uv.lock +++ b/uv.lock @@ -311,7 +311,7 @@ wheels = [ [[package]] name = "harness-cli" -version = "0.3.21" +version = "0.3.22" source = { editable = "." } dependencies = [ { name = "rich" },