mirror of
https://github.com/cline/cline.git
synced 2026-09-15 04:14:34 +08:00
Compare commits
24
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cc0675d31f | ||
|
|
c3033d6f13 | ||
|
|
cfb1327a1b | ||
|
|
264af96e1b | ||
|
|
10cb9bd97a | ||
|
|
2ee18e7f0c | ||
|
|
0b65506a2b | ||
|
|
3502608081 | ||
|
|
ed3107f9ec | ||
|
|
6bce48aad4 | ||
|
|
1e1b6af51c | ||
|
|
ee49900232 | ||
|
|
a1d5589d19 | ||
|
|
721fda2e99 | ||
|
|
5e78861eb5 | ||
|
|
29798f59f3 | ||
|
|
177d0eb07f | ||
|
|
869a87a220 | ||
|
|
0cfd0bbe05 | ||
|
|
08f656532f | ||
|
|
10dece6677 | ||
|
|
885a2936b6 | ||
|
|
90c427740d | ||
|
|
e6028168f2 |
@@ -39,14 +39,6 @@
|
||||
"sdk/packages/core/src/auth/**"
|
||||
],
|
||||
"severity": "high"
|
||||
},
|
||||
{
|
||||
"id": "sdk-telemetry-doc-update",
|
||||
"rule": "Any PR that adds new event constants to CORE_TELEMETRY_EVENTS in packages/core/src/services/telemetry/core-events.ts, adds new capture* helper functions, or changes the payload shape of an existing event must update the Event Catalog section in DOC.md. Flag PRs that modify core-events.ts without a corresponding change to DOC.md.",
|
||||
"scope": [
|
||||
"sdk/packages/core/src/services/telemetry/core-events.ts"
|
||||
],
|
||||
"severity": "medium"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -16,13 +16,9 @@
|
||||
"path": "sdk/packages/core/src/services/telemetry/OpenTelemetryProvider.ts",
|
||||
"description": "OpenTelemetry-backed provider that wires logs/metrics/traces exporters. Contains createConfiguredTelemetryService and createConfiguredTelemetryHandle, the canonical factories every host should use."
|
||||
},
|
||||
{
|
||||
"path": "DOC.md",
|
||||
"description": "Public API and event documentation. The Event Catalog and 'Activation funnel' sections must be kept in sync with core-events.ts. Host integration rules (CLI dir ordering, hub daemon metadata forwarding) are documented here."
|
||||
},
|
||||
{
|
||||
"path": "sdk/ARCHITECTURE.md",
|
||||
"description": "Architecture reference. Telemetry design decisions, completion semantics (submit_and_exit anchoring), and hub-daemon telemetry forwarding are documented here. Use as ground truth for design intent."
|
||||
"description": "Architecture reference. Telemetry design decisions and completion semantics (submit_and_exit anchoring) are documented here. Use as ground truth for design intent."
|
||||
},
|
||||
{
|
||||
"path": "sdk/AGENTS.md",
|
||||
|
||||
+20
-17
@@ -36,8 +36,11 @@ event names. It exports:
|
||||
|
||||
1. Add the constant to `CORE_TELEMETRY_EVENTS`
|
||||
2. Add a typed `capture*()` helper alongside it (with a typed `properties` parameter)
|
||||
3. Update the Event Catalog section in `DOC.md`
|
||||
4. Add a unit test in `core-events.test.ts` asserting the event is dropped when telemetry is opted out
|
||||
3. Add a unit test in `core-events.test.ts` asserting the event flows through the
|
||||
opt-out-respecting `capture` path and never `captureRequired` (opt-out is enforced by
|
||||
`OptedOutTelemetryService`, whose `capture` is a no-op — the test convention is
|
||||
"emits X as a normal opt-out-respecting event"). Events that intentionally bypass
|
||||
opt-out must use `captureRequired` and assert that explicitly.
|
||||
|
||||
## The Activation Funnel
|
||||
|
||||
@@ -82,7 +85,7 @@ The CLI accepts `--config <dir>`. The CLI **must** apply `setClineDir(...)` and
|
||||
and any other on-disk telemetry state lands under `~/.cline` instead of the user's chosen
|
||||
config dir.
|
||||
|
||||
The canonical pattern is in `apps/cli/src/main.ts` (PR #357):
|
||||
The canonical pattern is in `apps/cli/src/main.ts`:
|
||||
|
||||
```ts
|
||||
if (configDir) setClineDir(configDir);
|
||||
@@ -90,18 +93,18 @@ setHomeDir(homedir());
|
||||
captureCliExtensionActivated(); // <-- after dir overrides
|
||||
```
|
||||
|
||||
## Hub Daemon Metadata Forwarding
|
||||
## Hub Daemon Telemetry
|
||||
|
||||
Hosts that spawn a detached `@cline/core/hub/daemon-entry` process must forward telemetry
|
||||
metadata into the daemon argv so the daemon can reconstruct an equivalent
|
||||
`ITelemetryService`. The expected payload is base64-encoded JSON with snake_case keys:
|
||||
The detached hub daemon (`sdk/packages/core/src/hub/daemon/entry.ts`) hosts the
|
||||
`LocalRuntimeHost` that emits `task.conversation_turn` and `task.tokens` for every
|
||||
hub-backed session, so the daemon must own its own `ITelemetryService`. It builds one via
|
||||
`createHubDaemonTelemetry()` (`sdk/packages/core/src/hub/daemon/telemetry.ts`), which
|
||||
identifies from the cached cline account (re-resolved periodically, since the daemon often
|
||||
starts before login) and flushes on every shutdown path, including startup failure.
|
||||
|
||||
```
|
||||
{ extension_version, cline_type, platform, platform_version, os_type, os_version, is_remote_workspace }
|
||||
```
|
||||
|
||||
The reference implementation is `apps/vscode/src/hub-daemon.ts` (PR #357). Without this
|
||||
forwarding, hub-backed sessions silently drop their lifecycle telemetry.
|
||||
Flag changes that remove this wiring, construct runtime hosts inside the daemon without
|
||||
passing its telemetry handle, or add daemon exit paths that skip the flush — hub-backed
|
||||
sessions would silently drop their lifecycle telemetry (this exact bug shipped once).
|
||||
|
||||
## Auth Lifecycle Completeness
|
||||
|
||||
@@ -120,10 +123,10 @@ canonical examples of all four phases.
|
||||
|
||||
## Single Telemetry Service Per Host
|
||||
|
||||
On VS Code, the telemetry handle is built **once** in `activate()`
|
||||
(`apps/vscode/src/telemetry.ts`) and the same instance is passed into the sidebar, panel
|
||||
command, and daemon spawn payload. Do not let individual controllers construct their own
|
||||
`ITelemetryService` — that fragments distinct-id state, opt-out tracking, and flush ownership.
|
||||
On VS Code, all callers go through the lazy `telemetryService` proxy in
|
||||
`apps/vscode/src/services/telemetry/index.ts`, which constructs the service once on first
|
||||
use. Do not let individual controllers construct their own `ITelemetryService` — that
|
||||
fragments distinct-id state, opt-out tracking, and flush ownership.
|
||||
|
||||
The CLI follows the same pattern via the `getCliTelemetryService()` singleton in
|
||||
`apps/cli/src/utils/telemetry.ts`, which is memoized by the activation gate in
|
||||
|
||||
Vendored
+7
-7
@@ -68,7 +68,7 @@
|
||||
"command": "bun run build:webview",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"isBackground": false,
|
||||
"label": "npm: build:webview",
|
||||
"dependsOn": [
|
||||
"npm: protos"
|
||||
@@ -89,7 +89,7 @@
|
||||
"command": "bun run build:webview:test",
|
||||
"group": "build",
|
||||
"problemMatcher": [],
|
||||
"isBackground": true,
|
||||
"isBackground": false,
|
||||
"label": "npm: build:webview:test",
|
||||
"dependsOn": [
|
||||
"npm: protos"
|
||||
@@ -114,16 +114,16 @@
|
||||
{
|
||||
"pattern": [
|
||||
{
|
||||
"regexp": ".",
|
||||
"regexp": "^(?!)((?:.*))$",
|
||||
"kind": "file",
|
||||
"file": 1,
|
||||
"location": 2,
|
||||
"message": 3
|
||||
"message": 1
|
||||
}
|
||||
],
|
||||
"background": {
|
||||
"activeOnStart": true,
|
||||
"beginsPattern": ".",
|
||||
"endsPattern": "."
|
||||
"beginsPattern": "^Building webview for|^\\s*VITE",
|
||||
"endsPattern": "^.*Local:\\s+http://127\\.0\\.0\\.1:[0-9]+/"
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# Cline CLI Changelog
|
||||
|
||||
## 3.0.39
|
||||
|
||||
- You can now select Cline free models on the ClinePass provider in the model picker
|
||||
- Removed the retired ClinePass GLM 5.1 model
|
||||
- Fixed OpenAI Codex model metadata under the GPT Subscription provider
|
||||
- `str_replace` edits now report accurate diffs
|
||||
- Fixed context compaction so canonical session history is preserved
|
||||
- The detached hub daemon now emits telemetry, and telemetry identity now includes `user_id`
|
||||
- Cline provider requests now send versioned client-identity headers
|
||||
|
||||
## 3.0.38
|
||||
|
||||
- New plan/act accent palette: act mode is now blue (`#79b8ff`) and plan mode amber, replacing the old cyan/yellow — applied across dialogs, the model selector, config, onboarding, markdown, and syntax highlighting, with light-theme variants tuned for contrast
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@cline/cli",
|
||||
"displayName": "cline",
|
||||
"version": "3.0.38",
|
||||
"version": "3.0.39",
|
||||
"description": "Autonomous coding agent CLI - capable of creating/editing files, running commands, using the browser, and more",
|
||||
"type": "module",
|
||||
"publishConfig": {
|
||||
@@ -68,19 +68,11 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
"@chat-adapter/discord": "^4.23.0",
|
||||
"@chat-adapter/gchat": "^4.23.0",
|
||||
"@chat-adapter/linear": "^4.23.0",
|
||||
"@chat-adapter/slack": "^4.23.0",
|
||||
"@chat-adapter/telegram": "^4.23.0",
|
||||
"@chat-adapter/whatsapp": "^4.23.0",
|
||||
"@clack/prompts": "^1.2.0",
|
||||
"@cline/cline-hub": "workspace:*",
|
||||
"@gramio/format": "^0.7.0",
|
||||
"@opentui-ui/dialog": "^0.1.2",
|
||||
"@opentui/core": "0.1.102",
|
||||
"@opentui/react": "0.1.102",
|
||||
"chat": "^4.23.0",
|
||||
"commander": "^14.0.3",
|
||||
"fzf": "^0.5.2",
|
||||
"marked": "^15.0.12",
|
||||
|
||||
@@ -21,6 +21,10 @@ import type {
|
||||
StopReason,
|
||||
} from "@agentclientprotocol/sdk";
|
||||
import { PROTOCOL_VERSION, RequestError } from "@agentclientprotocol/sdk";
|
||||
import {
|
||||
resolveSystemPrompt,
|
||||
resolveWorkspaceRoot,
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import {
|
||||
type AgentEvent,
|
||||
type ClineCore,
|
||||
@@ -30,11 +34,10 @@ import {
|
||||
} from "@cline/core";
|
||||
import type { Message } from "@cline/shared";
|
||||
import { getPersistedProviderApiKey } from "../commands/auth";
|
||||
import { resolveSystemPrompt } from "../runtime/prompt";
|
||||
import { subscribeToAgentEvents } from "../runtime/session-events";
|
||||
import { createCliCore } from "../session/session";
|
||||
import { getCliBuildInfo } from "../utils/common";
|
||||
import { randomSessionId, resolveWorkspaceRoot } from "../utils/helpers";
|
||||
import { randomSessionId } from "../utils/helpers";
|
||||
import type { Config } from "../utils/types";
|
||||
import {
|
||||
ACP_AUTH_METHODS,
|
||||
@@ -511,6 +514,7 @@ export class AcpAgent implements Agent {
|
||||
|
||||
private async buildConfig(session: SessionState): Promise<Config> {
|
||||
const cwd = session.cwd || process.cwd();
|
||||
const workspaceRoot = resolveWorkspaceRoot(cwd);
|
||||
// Resolve credentials: env vars take precedence, then session provider.
|
||||
const providerId = process.env.CLINE_PROVIDER ?? session.currentProviderId;
|
||||
const apiKey = process.env.CLINE_API_KEY ?? this.authResult?.apiKey ?? "";
|
||||
@@ -519,6 +523,7 @@ export class AcpAgent implements Agent {
|
||||
providerId,
|
||||
mode: session.currentMode,
|
||||
});
|
||||
const cliBuildInfo = getCliBuildInfo();
|
||||
|
||||
return {
|
||||
providerId,
|
||||
@@ -537,7 +542,23 @@ export class AcpAgent implements Agent {
|
||||
enableAgentTeams: false,
|
||||
enableTools: true,
|
||||
cwd,
|
||||
workspaceRoot: resolveWorkspaceRoot(cwd),
|
||||
workspaceRoot,
|
||||
extensionContext: {
|
||||
client: {
|
||||
name: "cline-acp",
|
||||
version: cliBuildInfo.version,
|
||||
platform: "cli",
|
||||
platformVersion: cliBuildInfo.version,
|
||||
isMultiRoot: false,
|
||||
},
|
||||
workspace: {
|
||||
rootPath: workspaceRoot,
|
||||
cwd,
|
||||
workspaceName: cwd,
|
||||
ide: "Terminal Shell",
|
||||
platform: process.platform,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,22 @@
|
||||
import { getConnector, listConnectors } from "../connectors/registry";
|
||||
import type { ConnectIo, ConnectStopResult } from "../connectors/types";
|
||||
import {
|
||||
type ConnectIo,
|
||||
type ConnectStopResult,
|
||||
getConnector,
|
||||
listConnectors,
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import { createCliLoggerAdapter } from "../logging/adapter";
|
||||
import { resolveCliSessionMetadata } from "../utils/enterprise";
|
||||
import { ensureOAuthProviderApiKey } from "./auth";
|
||||
|
||||
function withConnectorHost(io: ConnectIo): ConnectIo {
|
||||
return {
|
||||
...io,
|
||||
createLogger: createCliLoggerAdapter,
|
||||
resolveSessionMetadata: resolveCliSessionMetadata,
|
||||
ensureProviderApiKey: (input) =>
|
||||
ensureOAuthProviderApiKey({ ...input, io }),
|
||||
};
|
||||
}
|
||||
|
||||
export async function stopAllConnectors(
|
||||
io: ConnectIo,
|
||||
@@ -16,7 +33,7 @@ export async function stopAllConnectors(
|
||||
continue;
|
||||
}
|
||||
executed += 1;
|
||||
const result = await connector.stopAll(io);
|
||||
const result = await connector.stopAll(withConnectorHost(io));
|
||||
stoppedProcesses += result.stoppedProcesses;
|
||||
stoppedSessions += result.stoppedSessions;
|
||||
}
|
||||
@@ -49,7 +66,9 @@ export async function runStopConnector(
|
||||
io.writeErr(`connect adapter "${adapterName}" does not support stop`);
|
||||
return 1;
|
||||
}
|
||||
const result: ConnectStopResult = await connector.stopAll(io);
|
||||
const result: ConnectStopResult = await connector.stopAll(
|
||||
withConnectorHost(io),
|
||||
);
|
||||
io.writeln(
|
||||
`[connect] ${connector.name} stopped processes=${result.stoppedProcesses} sessions=${result.stoppedSessions}`,
|
||||
);
|
||||
@@ -66,7 +85,7 @@ export async function runConnectAdapter(
|
||||
io.writeErr(`unknown connect adapter "${adapterName}"`);
|
||||
return 1;
|
||||
}
|
||||
return connector.run(passthroughArgs, io);
|
||||
return connector.run(passthroughArgs, withConnectorHost(io));
|
||||
}
|
||||
|
||||
export function formatAdapterList(): string {
|
||||
|
||||
@@ -71,8 +71,9 @@ vi.mock("@cline/core", () => ({
|
||||
ensureFileExists: mockEnsureFileExists,
|
||||
}));
|
||||
|
||||
vi.mock("../connectors/common", () => ({
|
||||
vi.mock("@cline/cline-hub/connectors", () => ({
|
||||
isProcessRunning: vi.fn(() => false),
|
||||
listActiveConnectors: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
vi.mock("./connect", () => ({
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { existsSync, readFileSync, rmSync } from "node:fs";
|
||||
import { dirname, join } from "node:path";
|
||||
import {
|
||||
type ActiveConnectorRecord,
|
||||
isProcessRunning,
|
||||
listActiveConnectors,
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import {
|
||||
clearHubDiscovery,
|
||||
ensureFileExists,
|
||||
@@ -14,11 +19,6 @@ import {
|
||||
import { formatUptime, resolveClineBuildEnv } from "@cline/shared";
|
||||
import { Command } from "commander";
|
||||
import open from "open";
|
||||
import { isProcessRunning } from "../connectors/common";
|
||||
import {
|
||||
type ActiveConnectorRecord,
|
||||
listActiveConnectors,
|
||||
} from "../connectors/status";
|
||||
import { getCliBuildInfo } from "../utils/common";
|
||||
import { c, writeln } from "../utils/output";
|
||||
import { stopAllConnectors } from "./connect";
|
||||
|
||||
@@ -11,8 +11,8 @@ vi.mock("@cline/core", () => ({
|
||||
sendHubCommand: mockSendHubCommand,
|
||||
}));
|
||||
|
||||
vi.mock("../utils/hub-runtime", () => ({
|
||||
ensureCliHubServer: mockEnsureCliHubServer,
|
||||
vi.mock("@cline/cline-hub/connectors", () => ({
|
||||
ensureHubServer: mockEnsureCliHubServer,
|
||||
parseHubEndpointOverride: (rawAddress: string | undefined) => {
|
||||
const trimmed = rawAddress?.trim();
|
||||
if (!trimmed) {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import {
|
||||
ensureHubServer,
|
||||
parseHubEndpointOverride,
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import {
|
||||
createLocalHubScheduleRuntimeHandlers,
|
||||
HubScheduleCommandService,
|
||||
HubScheduleService,
|
||||
sendHubCommand,
|
||||
} from "@cline/core";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
} from "../../utils/hub-runtime";
|
||||
import type { CommandIo } from "./types";
|
||||
|
||||
export class HubScheduleClient {
|
||||
@@ -190,7 +190,7 @@ export async function ensureSchedulerHub(
|
||||
}
|
||||
try {
|
||||
const requestedEndpoint = parseHubEndpointOverride(address);
|
||||
const { url: hubUrl } = await ensureCliHubServer(
|
||||
const { url: hubUrl } = await ensureHubServer(
|
||||
workspaceRoot,
|
||||
requestedEndpoint,
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { realpathSync } from "node:fs";
|
||||
import { ensureHubServer } from "@cline/cline-hub/connectors";
|
||||
import {
|
||||
clearHubDiscovery,
|
||||
isAutoUpdateEnabledGlobally,
|
||||
@@ -11,7 +12,6 @@ import {
|
||||
} from "@cline/core";
|
||||
import { resolveClineBuildEnv } from "@cline/shared";
|
||||
import { version } from "../../package.json";
|
||||
import { ensureCliHubServer } from "../utils/hub-runtime";
|
||||
import { c, writeErr, writeln } from "../utils/output";
|
||||
import {
|
||||
getInstalledKanbanVersion,
|
||||
@@ -337,7 +337,7 @@ async function restartHubServerIfRunning(): Promise<void> {
|
||||
|
||||
// Re-ensure a fresh hub instance is spawned.
|
||||
try {
|
||||
await ensureCliHubServer(process.cwd()); // return value intentionally unused here
|
||||
await ensureHubServer(process.cwd()); // return value intentionally unused here
|
||||
writeln(`${c.green}✓${c.reset} ${c.dim}[hub] server restarted${c.reset}`);
|
||||
} catch (err) {
|
||||
writeErr(
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
export type ConnectIo = {
|
||||
writeln: (text?: string) => void;
|
||||
writeErr: (text: string) => void;
|
||||
};
|
||||
|
||||
export type ConnectStopResult = {
|
||||
stoppedProcesses: number;
|
||||
stoppedSessions: number;
|
||||
};
|
||||
|
||||
export interface ConnectCommandDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
run(args: string[], io: ConnectIo): Promise<number>;
|
||||
showHelp(io: ConnectIo): void;
|
||||
stopAll?(io: ConnectIo): Promise<ConnectStopResult>;
|
||||
}
|
||||
@@ -108,7 +108,7 @@ const loggingMocks = vi.hoisted(() => ({
|
||||
flushCliLoggerAdapters: vi.fn(),
|
||||
}));
|
||||
const hubRuntimeMocks = vi.hoisted(() => ({
|
||||
ensureCliHubServer: vi.fn(async () => ({
|
||||
ensureHubServer: vi.fn(async () => ({
|
||||
url: "ws://127.0.0.1:25463",
|
||||
authToken: "test-token",
|
||||
})),
|
||||
@@ -192,8 +192,10 @@ vi.mock("./utils/feature-flags", () => ({
|
||||
setCliFeatureFlagsAccountContext:
|
||||
featureFlagMocks.setCliFeatureFlagsAccountContext,
|
||||
}));
|
||||
vi.mock("./runtime/prompt", () => ({
|
||||
vi.mock("@cline/cline-hub/connectors", () => ({
|
||||
resolveSystemPrompt: promptMocks.resolveSystemPrompt,
|
||||
resolveWorkspaceRoot: (cwd: string) => cwd,
|
||||
...hubRuntimeMocks,
|
||||
}));
|
||||
vi.mock("./commands/kanban", () => kanbanMocks);
|
||||
vi.mock("./commands/dashboard", () => dashboardMocks);
|
||||
@@ -202,7 +204,6 @@ vi.mock("./commands/update", () => updateMocks);
|
||||
vi.mock("./commands/history", () => historyMocks);
|
||||
vi.mock("./utils/history-resume", () => historyResumeMocks);
|
||||
vi.mock("./logging/adapter", () => loggingMocks);
|
||||
vi.mock("./utils/hub-runtime", () => hubRuntimeMocks);
|
||||
vi.mock("./utils/telemetry", () => telemetryMocks);
|
||||
vi.mock("./utils/worktree", () => worktreeMocks);
|
||||
|
||||
@@ -239,8 +240,8 @@ describe("runCli lightweight command dispatch", () => {
|
||||
taskId: "task-1",
|
||||
repoRoot: "/tmp/source",
|
||||
});
|
||||
hubRuntimeMocks.ensureCliHubServer.mockReset();
|
||||
hubRuntimeMocks.ensureCliHubServer.mockResolvedValue({
|
||||
hubRuntimeMocks.ensureHubServer.mockReset();
|
||||
hubRuntimeMocks.ensureHubServer.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463",
|
||||
authToken: "test-token",
|
||||
});
|
||||
@@ -1013,6 +1014,80 @@ describe("runCli lightweight command dispatch", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("identifies saved Cline accountId for telemetry before runtime events", async () => {
|
||||
// CLINE-2406: when persisted Cline auth includes an accountId, the
|
||||
// runtime path must call identifyTelemetryAccount(accountContext) so
|
||||
// subsequent task.* and workspace.* events carry user_id.
|
||||
const clineSettings = {
|
||||
provider: "cline",
|
||||
model: "anthropic/claude-sonnet-4.6",
|
||||
auth: { accountId: "usr-abc-123", refreshToken: "rt-token" },
|
||||
};
|
||||
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue(
|
||||
clineSettings,
|
||||
);
|
||||
providerSettingsMocks.getProviderSettings.mockReturnValue(clineSettings);
|
||||
authMocks.normalizeProviderId.mockImplementation(
|
||||
(providerId?: string) => providerId ?? "cline",
|
||||
);
|
||||
process.argv = ["bun", "src/index.ts"];
|
||||
|
||||
const { runCli } = await import("./main");
|
||||
|
||||
await expect(runCli()).resolves.toBeUndefined();
|
||||
expect(telemetryMocks.identifyTelemetryAccount).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: "usr-abc-123",
|
||||
provider: "cline",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("does not call identifyTelemetryAccount in runtime path when no saved Cline accountId", async () => {
|
||||
// CLINE-2406: when no persisted accountId is found (anonymous/unauthenticated),
|
||||
// identifyTelemetryAccount should not be called from the runtime path.
|
||||
const clineSettings = {
|
||||
provider: "cline",
|
||||
model: "anthropic/claude-sonnet-4.6",
|
||||
// no auth / no accountId
|
||||
};
|
||||
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue(
|
||||
clineSettings,
|
||||
);
|
||||
providerSettingsMocks.getProviderSettings.mockReturnValue(clineSettings);
|
||||
authMocks.normalizeProviderId.mockImplementation(
|
||||
(providerId?: string) => providerId ?? "cline",
|
||||
);
|
||||
process.argv = ["bun", "src/index.ts"];
|
||||
|
||||
const { runCli } = await import("./main");
|
||||
|
||||
await expect(runCli()).resolves.toBeUndefined();
|
||||
expect(telemetryMocks.identifyTelemetryAccount).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not call identifyTelemetryAccount from runtime path when provider is not cline", async () => {
|
||||
// CLINE-2406: identity identification from saved settings only applies
|
||||
// to Cline-provider sessions; other providers use different auth flows.
|
||||
providerSettingsMocks.getLastUsedProviderSettings.mockReturnValue({
|
||||
provider: "openrouter",
|
||||
model: "openai/gpt-5",
|
||||
});
|
||||
providerSettingsMocks.getProviderSettings.mockReturnValue({
|
||||
provider: "openrouter",
|
||||
model: "openai/gpt-5",
|
||||
});
|
||||
authMocks.normalizeProviderId.mockImplementation(
|
||||
(providerId?: string) => providerId ?? "openrouter",
|
||||
);
|
||||
process.argv = ["bun", "src/index.ts"];
|
||||
|
||||
const { runCli } = await import("./main");
|
||||
|
||||
await expect(runCli()).resolves.toBeUndefined();
|
||||
expect(telemetryMocks.identifyTelemetryAccount).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("runs kanban before loading runtime modules", async () => {
|
||||
process.argv = ["bun", "src/index.ts", "kanban"];
|
||||
|
||||
@@ -1087,7 +1162,7 @@ describe("runCli lightweight command dispatch", () => {
|
||||
|
||||
await expect(runCli()).resolves.toBeUndefined();
|
||||
expect(runtimeMocks.runAgent).toHaveBeenCalledTimes(1);
|
||||
expect(hubRuntimeMocks.ensureCliHubServer).not.toHaveBeenCalled();
|
||||
expect(hubRuntimeMocks.ensureHubServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects yolo runs with a single bare prompt token", async () => {
|
||||
@@ -1105,7 +1180,7 @@ describe("runCli lightweight command dispatch", () => {
|
||||
expect.stringContaining("Unknown command or unquoted prompt: hello"),
|
||||
);
|
||||
expect(runtimeMocks.runAgent).not.toHaveBeenCalled();
|
||||
expect(hubRuntimeMocks.ensureCliHubServer).not.toHaveBeenCalled();
|
||||
expect(hubRuntimeMocks.ensureHubServer).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rewrites /team prompts and enables teams in single-prompt mode", async () => {
|
||||
|
||||
+25
-3
@@ -1,6 +1,7 @@
|
||||
import { fstatSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename } from "node:path";
|
||||
import { resolveWorkspaceRoot } from "@cline/cline-hub/connectors";
|
||||
import type { ToolPolicy } from "@cline/core";
|
||||
|
||||
import { registerDisposable } from "@cline/shared";
|
||||
@@ -15,6 +16,7 @@ import {
|
||||
getPreferredKanbanInstaller,
|
||||
} from "./commands/update";
|
||||
import { CLI_DEFAULT_CHECKPOINT_CONFIG } from "./runtime/defaults";
|
||||
import { getCliBuildInfo } from "./utils/common";
|
||||
import {
|
||||
buildCliCompactionConfig,
|
||||
CLI_COMPACTION_MODE_EXPECTED_TEXT,
|
||||
@@ -26,7 +28,6 @@ import {
|
||||
import {
|
||||
configureSandboxEnvironment,
|
||||
normalizeAutoApproveArgs,
|
||||
resolveWorkspaceRoot,
|
||||
} from "./utils/helpers";
|
||||
import {
|
||||
c,
|
||||
@@ -46,6 +47,7 @@ import { rewriteTeamPrompt, TEAM_COMMAND_USAGE } from "./utils/team-command";
|
||||
import {
|
||||
captureCliExtensionActivated,
|
||||
getCliTelemetryService,
|
||||
identifyTelemetryAccount,
|
||||
} from "./utils/telemetry";
|
||||
import type { Config } from "./utils/types";
|
||||
import { runConnectWizard } from "./wizards/connect";
|
||||
@@ -70,7 +72,7 @@ async function createProviderSettingsManager() {
|
||||
async function loadCliRuntimeModules() {
|
||||
const [coreServer, prompt, runAgentModule] = await Promise.all([
|
||||
import("@cline/core"),
|
||||
import("./runtime/prompt"),
|
||||
import("@cline/cline-hub/connectors"),
|
||||
import("./runtime/run-agent"),
|
||||
]);
|
||||
return {
|
||||
@@ -962,6 +964,19 @@ export async function runCli(): Promise<void> {
|
||||
);
|
||||
let selectedProviderSettings =
|
||||
providerSettingsManager.getProviderSettings(provider);
|
||||
|
||||
// Apply locally persisted Cline account identity so subsequent events
|
||||
// (task.*, workspace.initialized) carry user_id when available.
|
||||
// Note: user.extension_activated fires anonymously earlier in startup
|
||||
// and cannot be retroactively updated; this is by design for
|
||||
// lightweight subcommand and pre-auth CLI flows. See CLINE-2406.
|
||||
if (provider === "cline") {
|
||||
const savedAccountId = selectedProviderSettings?.auth?.accountId;
|
||||
if (savedAccountId) {
|
||||
identifyTelemetryAccount({ id: savedAccountId, provider: "cline" });
|
||||
}
|
||||
}
|
||||
|
||||
const persistedApiKey = getPersistedProviderApiKey(
|
||||
provider,
|
||||
selectedProviderSettings,
|
||||
@@ -1029,6 +1044,7 @@ export async function runCli(): Promise<void> {
|
||||
reasoningEffort: args.reasoningEffort,
|
||||
persistedReasoning: selectedProviderSettings?.reasoning,
|
||||
});
|
||||
const cliBuildInfo = getCliBuildInfo();
|
||||
const { createCliLoggerAdapter } = await import("./logging/adapter");
|
||||
const loggerAdapter = createCliLoggerAdapter({
|
||||
runtime: "cli",
|
||||
@@ -1079,7 +1095,13 @@ export async function runCli(): Promise<void> {
|
||||
cwd,
|
||||
workspaceRoot,
|
||||
extensionContext: {
|
||||
client: { name: "cline-cli" },
|
||||
client: {
|
||||
name: "cline-cli",
|
||||
version: cliBuildInfo.version,
|
||||
platform: "cli",
|
||||
platformVersion: cliBuildInfo.version,
|
||||
isMultiRoot: false,
|
||||
},
|
||||
workspace: {
|
||||
rootPath: workspaceRoot,
|
||||
cwd,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
type ChatCommandState,
|
||||
chatCommandHost,
|
||||
createChatCommandHost,
|
||||
} from "../../utils/chat-commands";
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { Config } from "../../utils/types";
|
||||
import {
|
||||
type InteractiveChatCommandRuntime,
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { InteractiveTurnResult } from "../../tui/types";
|
||||
import type { ChatCommandHost } from "../../utils/chat-commands";
|
||||
import type { ChatCommandHost } from "@cline/cline-hub/connectors";
|
||||
import {
|
||||
type ChatCommandState,
|
||||
maybeHandleChatCommand,
|
||||
} from "../../utils/chat-commands";
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import type { InteractiveTurnResult } from "../../tui/types";
|
||||
import {
|
||||
enableTeamsForPrompt,
|
||||
rewriteTeamPrompt,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { resolveSystemPrompt } from "@cline/cline-hub/connectors";
|
||||
import { createTool } from "@cline/shared";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { Config } from "../../utils/types";
|
||||
import { resolveSystemPrompt } from "../prompt";
|
||||
import {
|
||||
ACT_MODE_CONTINUATION_PROMPT,
|
||||
type AppliedModeChange,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
sendTurnWithActModeContinuation,
|
||||
} from "./mode";
|
||||
|
||||
vi.mock("../prompt", () => ({
|
||||
vi.mock("@cline/cline-hub/connectors", () => ({
|
||||
resolveSystemPrompt: vi.fn(async (input: { mode?: string }) => {
|
||||
return `system prompt for ${input.mode ?? "unknown"}`;
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { resolveSystemPrompt } from "@cline/cline-hub/connectors";
|
||||
import { createTool } from "@cline/shared";
|
||||
import type { Config } from "../../utils/types";
|
||||
import { resolveSystemPrompt } from "../prompt";
|
||||
|
||||
export type InteractiveUiMode = "plan" | "act";
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChatCommandState } from "@cline/cline-hub/connectors";
|
||||
import type { TeamEvent } from "@cline/core";
|
||||
import type { ChatCommandState } from "../../utils/chat-commands";
|
||||
import type { Config } from "../../utils/types";
|
||||
import {
|
||||
CLI_DEFAULT_CHECKPOINT_CONFIG,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { ChatCommandState } from "@cline/cline-hub/connectors";
|
||||
import {
|
||||
createSessionCompactionState,
|
||||
type ProviderSettingsManager,
|
||||
@@ -9,7 +10,6 @@ import {
|
||||
} from "@cline/core";
|
||||
import type { AgentTool, Message } from "@cline/shared";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { ChatCommandState } from "../../utils/chat-commands";
|
||||
import type { Config } from "../../utils/types";
|
||||
|
||||
const createCliCoreMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import type {
|
||||
ChatCommandState,
|
||||
ForkSessionResult,
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import {
|
||||
type AgentEvent,
|
||||
type AgentHooks,
|
||||
@@ -18,10 +22,6 @@ import {
|
||||
import type { Message } from "@cline/shared";
|
||||
import { createCliCore } from "../../session/session";
|
||||
import { submitAndExitInTerminal } from "../../utils/approval";
|
||||
import type {
|
||||
ChatCommandState,
|
||||
ForkSessionResult,
|
||||
} from "../../utils/chat-commands";
|
||||
import { createRuntimeHooks } from "../../utils/hooks";
|
||||
import { setActiveCliSession } from "../../utils/output";
|
||||
import { loadInteractiveResumeMessages } from "../../utils/resume";
|
||||
@@ -411,43 +411,43 @@ export function createInteractiveSessionRuntime(input: {
|
||||
});
|
||||
};
|
||||
|
||||
const restartWithMessages = async (
|
||||
messages: Message[],
|
||||
sessionMetadata?: Record<string, unknown>,
|
||||
initialCompactionState?: SessionCompactionState,
|
||||
): Promise<void> => {
|
||||
sessionStartGeneration += 1;
|
||||
pendingResumeSessionId = undefined;
|
||||
startupError = undefined;
|
||||
// Publish the restart as the in-flight startup. Teardown leaves a window
|
||||
// with no active session, and without this barrier a concurrent
|
||||
// ensureReady() (e.g. a message submitted right after a plan/act toggle)
|
||||
// reads that window as "no session" and boots an empty session that then
|
||||
// races the restarted one for the active slot.
|
||||
const restart = (async () => {
|
||||
await stopCurrentSession();
|
||||
clearActiveSession();
|
||||
await startFreshSession(
|
||||
messages,
|
||||
sessionMetadata,
|
||||
initialCompactionState,
|
||||
);
|
||||
})().catch((error) => {
|
||||
startupError = error;
|
||||
throw error;
|
||||
});
|
||||
startupPromise = restart;
|
||||
try {
|
||||
await restart;
|
||||
} finally {
|
||||
// Restore the pre-restart steady state (startupPromise unset) so a
|
||||
// failed restart stays retryable by the next ensureReady(). A newer
|
||||
// startup that already replaced the barrier is left alone.
|
||||
if (startupPromise === restart) {
|
||||
startupPromise = undefined;
|
||||
}
|
||||
const restartWithMessages = async (
|
||||
messages: Message[],
|
||||
sessionMetadata?: Record<string, unknown>,
|
||||
initialCompactionState?: SessionCompactionState,
|
||||
): Promise<void> => {
|
||||
sessionStartGeneration += 1;
|
||||
pendingResumeSessionId = undefined;
|
||||
startupError = undefined;
|
||||
// Publish the restart as the in-flight startup. Teardown leaves a window
|
||||
// with no active session, and without this barrier a concurrent
|
||||
// ensureReady() (e.g. a message submitted right after a plan/act toggle)
|
||||
// reads that window as "no session" and boots an empty session that then
|
||||
// races the restarted one for the active slot.
|
||||
const restart = (async () => {
|
||||
await stopCurrentSession();
|
||||
clearActiveSession();
|
||||
await startFreshSession(
|
||||
messages,
|
||||
sessionMetadata,
|
||||
initialCompactionState,
|
||||
);
|
||||
})().catch((error) => {
|
||||
startupError = error;
|
||||
throw error;
|
||||
});
|
||||
startupPromise = restart;
|
||||
try {
|
||||
await restart;
|
||||
} finally {
|
||||
// Restore the pre-restart steady state (startupPromise unset) so a
|
||||
// failed restart stays retryable by the next ensureReady(). A newer
|
||||
// startup that already replaced the barrier is left alone.
|
||||
if (startupPromise === restart) {
|
||||
startupPromise = undefined;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const restartWithCurrentMessages = async (): Promise<void> => {
|
||||
const [{ messages, status }, compactionState] = await Promise.all([
|
||||
|
||||
@@ -105,7 +105,7 @@ vi.mock("./interactive-welcome", () => ({
|
||||
resolveClineWelcomeLine: vi.fn(async () => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("./prompt", () => ({
|
||||
vi.mock("@cline/cline-hub/connectors", () => ({
|
||||
buildUserInputMessage: vi.fn(async () => ({
|
||||
prompt: "prompt",
|
||||
userImages: [],
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { buildUserInputMessage } from "@cline/cline-hub/connectors";
|
||||
import {
|
||||
type AgentEvent,
|
||||
type AgentResult,
|
||||
@@ -41,7 +42,6 @@ import {
|
||||
CLI_DEFAULT_LOOP_DETECTION,
|
||||
} from "./defaults";
|
||||
import { describeAbortSource, resolveMistakeLimitDecision } from "./format";
|
||||
import { buildUserInputMessage } from "./prompt";
|
||||
import { subscribeToAgentEvents } from "./session-events";
|
||||
|
||||
function printModelProviderInfo(config: Config): void {
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
import {
|
||||
buildUserInputMessage,
|
||||
type ChatCommandState,
|
||||
chatCommandHost,
|
||||
createWorkspaceChatCommandHost,
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import {
|
||||
getCurrentContextSize,
|
||||
type ProviderSettings,
|
||||
@@ -24,7 +30,6 @@ import {
|
||||
} from "../tui/interactive-welcome";
|
||||
import { disableOpenTuiGraphicsProbe } from "../tui/opentui-env";
|
||||
import type { QueuedPromptItem } from "../tui/types";
|
||||
import { type ChatCommandState, chatCommandHost } from "../utils/chat-commands";
|
||||
import { applyCliCompactionMode } from "../utils/compaction-mode";
|
||||
import {
|
||||
shouldZeroClineFreeModelCost,
|
||||
@@ -36,7 +41,6 @@ import {
|
||||
writeErr,
|
||||
writeln,
|
||||
} from "../utils/output";
|
||||
import { createWorkspaceChatCommandHost } from "../utils/plugin-chat-commands";
|
||||
import { readRepoStatus } from "../utils/repo-status";
|
||||
import type { Config } from "../utils/types";
|
||||
import {
|
||||
@@ -62,7 +66,6 @@ import {
|
||||
} from "./interactive/mode";
|
||||
import { assertInteractivePreflight } from "./interactive/preflight";
|
||||
import { createInteractiveSessionRuntime } from "./interactive/session-runtime";
|
||||
import { buildUserInputMessage } from "./prompt";
|
||||
import { getUIEventEmitter } from "./session-events";
|
||||
|
||||
type ModelChangeReasoningConfig = {
|
||||
|
||||
@@ -6,7 +6,7 @@ const {
|
||||
startRuntimeSession,
|
||||
sendRuntimeSession,
|
||||
buildUserInputMessage,
|
||||
ensureCliHubServer,
|
||||
ensureHubServer,
|
||||
emitJsonLine,
|
||||
writeErr,
|
||||
writeln,
|
||||
@@ -16,7 +16,7 @@ const {
|
||||
startRuntimeSession: vi.fn(),
|
||||
sendRuntimeSession: vi.fn(),
|
||||
buildUserInputMessage: vi.fn(),
|
||||
ensureCliHubServer: vi.fn(),
|
||||
ensureHubServer: vi.fn(),
|
||||
emitJsonLine: vi.fn(),
|
||||
writeErr: vi.fn(),
|
||||
writeln: vi.fn(),
|
||||
@@ -31,12 +31,9 @@ vi.mock("@cline/core", () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock("./prompt", () => ({
|
||||
vi.mock("@cline/cline-hub/connectors", () => ({
|
||||
buildUserInputMessage,
|
||||
}));
|
||||
|
||||
vi.mock("../utils/hub-runtime", () => ({
|
||||
ensureCliHubServer,
|
||||
ensureHubServer,
|
||||
}));
|
||||
|
||||
vi.mock("../utils/output", () => ({
|
||||
@@ -69,7 +66,7 @@ describe("runZen", () => {
|
||||
userImages: [],
|
||||
userFiles: [],
|
||||
});
|
||||
ensureCliHubServer.mockResolvedValue({
|
||||
ensureHubServer.mockResolvedValue({
|
||||
url: "ws://127.0.0.1:25463/hub",
|
||||
authToken: "test-token",
|
||||
});
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import {
|
||||
buildUserInputMessage,
|
||||
ensureHubServer,
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import type { UserInstructionConfigService } from "@cline/core";
|
||||
import { HubSessionClient } from "@cline/core";
|
||||
import type { ChatStartSessionRequest } from "@cline/shared";
|
||||
import { resolveCliSessionMetadata } from "../utils/enterprise";
|
||||
import { ensureCliHubServer } from "../utils/hub-runtime";
|
||||
import { c, emitJsonLine, writeErr, writeln } from "../utils/output";
|
||||
import type { Config } from "../utils/types";
|
||||
import { buildUserInputMessage } from "./prompt";
|
||||
|
||||
const ZEN_DISPATCH_ACK_TIMEOUT_MS = 5_000;
|
||||
|
||||
@@ -51,7 +53,7 @@ export async function runZen(
|
||||
let hubUrl: string;
|
||||
let hubAuthToken: string;
|
||||
try {
|
||||
const hub = await ensureCliHubServer(workspaceRoot);
|
||||
const hub = await ensureHubServer(workspaceRoot);
|
||||
hubUrl = hub.url;
|
||||
hubAuthToken = hub.authToken;
|
||||
} catch (error) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { resolveWorkspaceRoot } from "@cline/cline-hub/connectors";
|
||||
import type {
|
||||
AgentConfig,
|
||||
BasicLogger,
|
||||
@@ -16,7 +17,6 @@ import {
|
||||
prepareCliEnterpriseIntegration,
|
||||
} from "../utils/enterprise";
|
||||
import { getCliFeatureFlagsService } from "../utils/feature-flags";
|
||||
import { resolveWorkspaceRoot } from "../utils/helpers";
|
||||
import { getCliTelemetryService } from "../utils/telemetry";
|
||||
import type { ConversationHistory } from "./export";
|
||||
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import type {
|
||||
ClineRecommendedModel,
|
||||
ClineRecommendedModelsData,
|
||||
} from "@cline/core";
|
||||
|
||||
export type ClineModelPickerTier = "recommended" | "subscribed" | "free";
|
||||
|
||||
export interface ClineModelPickerItem {
|
||||
kind: "model";
|
||||
model: ClineRecommendedModel;
|
||||
tier: ClineModelPickerTier;
|
||||
}
|
||||
|
||||
export interface ClineModelPickerBrowse {
|
||||
kind: "browse";
|
||||
}
|
||||
|
||||
export type ClineModelPickerEntry =
|
||||
| ClineModelPickerItem
|
||||
| ClineModelPickerBrowse;
|
||||
|
||||
export const CLINE_MODEL_PICKER_TIER_LABELS: Record<
|
||||
ClineModelPickerTier,
|
||||
string
|
||||
> = {
|
||||
recommended: "Recommended",
|
||||
subscribed: "Subscribed",
|
||||
free: "Free",
|
||||
};
|
||||
|
||||
// Featured entries for the sectioned picker, keyed by provider: cline gets
|
||||
// Recommended/Free with a browse-all escape into the full catalog; cline-pass
|
||||
// gets Subscribed/Free (see buildClinePassModelEntries for why no browse-all).
|
||||
export function buildFeaturedModelEntries(
|
||||
providerId: string,
|
||||
data: ClineRecommendedModelsData,
|
||||
): ClineModelPickerEntry[] {
|
||||
return providerId === "cline-pass"
|
||||
? buildClinePassModelEntries(data)
|
||||
: buildClineModelEntries(data);
|
||||
}
|
||||
|
||||
function buildClineModelEntries(
|
||||
data: ClineRecommendedModelsData,
|
||||
): ClineModelPickerEntry[] {
|
||||
const entries: ClineModelPickerEntry[] = [];
|
||||
for (const m of data.recommended) {
|
||||
entries.push({ kind: "model", model: m, tier: "recommended" });
|
||||
}
|
||||
for (const m of data.free) {
|
||||
entries.push({ kind: "model", model: m, tier: "free" });
|
||||
}
|
||||
entries.push({ kind: "browse" });
|
||||
return entries;
|
||||
}
|
||||
|
||||
// Shown under the Free section header when picking a model for ClinePass
|
||||
export const CLINE_PASS_FREE_SECTION_DESCRIPTION =
|
||||
"Try with limited usage, separate from ClinePass quota.";
|
||||
|
||||
// ClinePass shows the subscription's models plus the Cline free models — both
|
||||
// providers hit the same Cline API, so free models are selectable in place
|
||||
// (they ride usage billing at $0 instead of the subscription quota).
|
||||
// No "browse all" entry when the clinePass bucket is populated: unlike cline,
|
||||
// the ClinePass catalog contains exactly these two buckets, so the sections
|
||||
// already list every selectable model. An empty clinePass bucket means the
|
||||
// fetch fell back to the bundled list (which has no pass models) — without an
|
||||
// escape into the full catalog a subscriber could only pick free models, so
|
||||
// browse-all comes back in that degraded mode.
|
||||
function buildClinePassModelEntries(
|
||||
data: ClineRecommendedModelsData,
|
||||
): ClineModelPickerEntry[] {
|
||||
const entries: ClineModelPickerEntry[] = [];
|
||||
for (const m of data.clinePass) {
|
||||
entries.push({ kind: "model", model: m, tier: "subscribed" });
|
||||
}
|
||||
for (const m of data.free) {
|
||||
entries.push({ kind: "model", model: m, tier: "free" });
|
||||
}
|
||||
if (data.clinePass.length === 0) {
|
||||
entries.push({ kind: "browse" });
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
// The quota explainer only makes sense in the ClinePass picker, which is the
|
||||
// only picker that has a "subscribed" section
|
||||
export function freeTierDescriptionFor(
|
||||
entries: ClineModelPickerEntry[],
|
||||
): string | undefined {
|
||||
const isClinePassPicker = entries.some(
|
||||
(entry) => entry.kind === "model" && entry.tier === "subscribed",
|
||||
);
|
||||
return isClinePassPicker ? CLINE_PASS_FREE_SECTION_DESCRIPTION : undefined;
|
||||
}
|
||||
|
||||
// OpenRouter marks free variants with "(free)" in names and ":free" in ids to
|
||||
// disambiguate them from their paid twins. Inside the sectioned pickers the
|
||||
// Free header already says it, so the markers are redundant — but keep them in
|
||||
// flat lists (e.g. browse-all), where both variants appear side by side.
|
||||
export function stripFreeMarker(displayName: string): string {
|
||||
return displayName
|
||||
.replace(/\s*\(free\)\s*$/i, "")
|
||||
.replace(/:free$/i, "")
|
||||
.trim();
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildFeaturedModelEntries,
|
||||
CLINE_PASS_FREE_SECTION_DESCRIPTION,
|
||||
freeTierDescriptionFor,
|
||||
stripFreeMarker,
|
||||
} from "./cline-model-entries";
|
||||
|
||||
const model = (id: string) => ({ id, name: id, description: "", tags: [] });
|
||||
|
||||
describe("cline model picker entries", () => {
|
||||
it("builds Recommended/Free sections for the cline provider", () => {
|
||||
const entries = buildFeaturedModelEntries("cline", {
|
||||
recommended: [model("anthropic/claude-sonnet-5")],
|
||||
free: [model("deepseek/deepseek-v4-flash")],
|
||||
clinePass: [model("cline-pass/glm-5.1")],
|
||||
});
|
||||
|
||||
expect(entries).toEqual([
|
||||
{
|
||||
kind: "model",
|
||||
model: model("anthropic/claude-sonnet-5"),
|
||||
tier: "recommended",
|
||||
},
|
||||
{
|
||||
kind: "model",
|
||||
model: model("deepseek/deepseek-v4-flash"),
|
||||
tier: "free",
|
||||
},
|
||||
{ kind: "browse" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("builds Subscribed/Free sections for the cline-pass provider", () => {
|
||||
const entries = buildFeaturedModelEntries("cline-pass", {
|
||||
recommended: [model("anthropic/claude-sonnet-5")],
|
||||
free: [model("deepseek/deepseek-v4-flash")],
|
||||
clinePass: [model("cline-pass/glm-5.1"), model("cline-pass/kimi-k2.6")],
|
||||
});
|
||||
|
||||
expect(entries).toEqual([
|
||||
{ kind: "model", model: model("cline-pass/glm-5.1"), tier: "subscribed" },
|
||||
{
|
||||
kind: "model",
|
||||
model: model("cline-pass/kimi-k2.6"),
|
||||
tier: "subscribed",
|
||||
},
|
||||
{
|
||||
kind: "model",
|
||||
model: model("deepseek/deepseek-v4-flash"),
|
||||
tier: "free",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("adds the browse-all escape when the clinePass bucket is empty", () => {
|
||||
// The fetch fell back to the bundled list (no pass models); the sections
|
||||
// alone would leave a subscriber able to pick only free models.
|
||||
const entries = buildFeaturedModelEntries("cline-pass", {
|
||||
recommended: [],
|
||||
free: [model("deepseek/deepseek-v4-flash")],
|
||||
clinePass: [],
|
||||
});
|
||||
|
||||
expect(entries).toEqual([
|
||||
{
|
||||
kind: "model",
|
||||
model: model("deepseek/deepseek-v4-flash"),
|
||||
tier: "free",
|
||||
},
|
||||
{ kind: "browse" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("attaches the quota explainer only to the ClinePass picker's free section", () => {
|
||||
const data = {
|
||||
recommended: [model("anthropic/claude-sonnet-5")],
|
||||
free: [model("deepseek/deepseek-v4-flash")],
|
||||
clinePass: [model("cline-pass/glm-5.1")],
|
||||
};
|
||||
|
||||
expect(
|
||||
freeTierDescriptionFor(buildFeaturedModelEntries("cline-pass", data)),
|
||||
).toBe(CLINE_PASS_FREE_SECTION_DESCRIPTION);
|
||||
expect(
|
||||
freeTierDescriptionFor(buildFeaturedModelEntries("cline", data)),
|
||||
).toBe(undefined);
|
||||
});
|
||||
|
||||
it("strips redundant free markers from display names", () => {
|
||||
expect(stripFreeMarker("Laguna M.1 (free)")).toBe("Laguna M.1");
|
||||
expect(stripFreeMarker("Trinity Large Preview (FREE)")).toBe(
|
||||
"Trinity Large Preview",
|
||||
);
|
||||
expect(stripFreeMarker("laguna-m.1:free")).toBe("laguna-m.1");
|
||||
expect(stripFreeMarker("DeepSeek V4 Flash")).toBe("DeepSeek V4 Flash");
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,6 @@
|
||||
// @jsxImportSource @opentui/react
|
||||
|
||||
import {
|
||||
type ClineRecommendedModel,
|
||||
type ClineRecommendedModelsData,
|
||||
fetchClineRecommendedModels,
|
||||
} from "@cline/core";
|
||||
@@ -9,20 +8,23 @@ import type { ReactNode } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import "opentui-spinner/react";
|
||||
import { palette } from "../../palette";
|
||||
import {
|
||||
CLINE_MODEL_PICKER_TIER_LABELS,
|
||||
type ClineModelPickerEntry,
|
||||
freeTierDescriptionFor,
|
||||
stripFreeMarker,
|
||||
} from "./cline-model-entries";
|
||||
|
||||
export interface ClineModelPickerItem {
|
||||
kind: "model";
|
||||
model: ClineRecommendedModel;
|
||||
tier: "recommended" | "free";
|
||||
}
|
||||
|
||||
export interface ClineModelPickerBrowse {
|
||||
kind: "browse";
|
||||
}
|
||||
|
||||
export type ClineModelPickerEntry =
|
||||
| ClineModelPickerItem
|
||||
| ClineModelPickerBrowse;
|
||||
export {
|
||||
buildFeaturedModelEntries,
|
||||
CLINE_MODEL_PICKER_TIER_LABELS,
|
||||
type ClineModelPickerBrowse,
|
||||
type ClineModelPickerEntry,
|
||||
type ClineModelPickerItem,
|
||||
type ClineModelPickerTier,
|
||||
freeTierDescriptionFor,
|
||||
stripFreeMarker,
|
||||
} from "./cline-model-entries";
|
||||
|
||||
function tagColor(tag: string): string {
|
||||
if (tag === "FREE") return palette.success;
|
||||
@@ -39,12 +41,13 @@ function resolveDisplayName(
|
||||
for (const key of candidates) {
|
||||
if (!key) continue;
|
||||
const hit = knownModels[key] as { name?: string } | undefined;
|
||||
if (hit?.name) return hit.name;
|
||||
if (hit?.name) return stripFreeMarker(hit.name);
|
||||
}
|
||||
}
|
||||
return modelId.includes("/")
|
||||
const fallback = modelId.includes("/")
|
||||
? (modelId.split("/").pop() ?? modelId)
|
||||
: modelId;
|
||||
return stripFreeMarker(fallback);
|
||||
}
|
||||
|
||||
export function useClineRecommendedModels() {
|
||||
@@ -68,20 +71,6 @@ export function useClineRecommendedModels() {
|
||||
return { data, loading };
|
||||
}
|
||||
|
||||
export function buildClineModelEntries(
|
||||
data: ClineRecommendedModelsData,
|
||||
): ClineModelPickerEntry[] {
|
||||
const entries: ClineModelPickerEntry[] = [];
|
||||
for (const m of data.recommended) {
|
||||
entries.push({ kind: "model", model: m, tier: "recommended" });
|
||||
}
|
||||
for (const m of data.free) {
|
||||
entries.push({ kind: "model", model: m, tier: "free" });
|
||||
}
|
||||
entries.push({ kind: "browse" });
|
||||
return entries;
|
||||
}
|
||||
|
||||
export function ClineModelPicker(props: {
|
||||
entries: ClineModelPickerEntry[];
|
||||
selected: number;
|
||||
@@ -103,6 +92,7 @@ export function ClineModelPicker(props: {
|
||||
let lastTier: string | null = null;
|
||||
let isFirstHeader = true;
|
||||
const rows: ReactNode[] = [];
|
||||
const freeTierDescription = freeTierDescriptionFor(entries);
|
||||
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
@@ -112,14 +102,20 @@ export function ClineModelPicker(props: {
|
||||
if (entry.kind === "model") {
|
||||
if (entry.tier !== lastTier) {
|
||||
lastTier = entry.tier;
|
||||
const label = entry.tier === "recommended" ? "Recommended" : "Free";
|
||||
const label = CLINE_MODEL_PICKER_TIER_LABELS[entry.tier];
|
||||
rows.push(
|
||||
<box
|
||||
key={`tier-${entry.tier}`}
|
||||
paddingX={1}
|
||||
marginTop={isFirstHeader ? 0 : 1}
|
||||
flexDirection="column"
|
||||
>
|
||||
<text fg="gray">{label}</text>
|
||||
{entry.tier === "free" && freeTierDescription && (
|
||||
<text fg="gray">
|
||||
<em>{freeTierDescription}</em>
|
||||
</text>
|
||||
)}
|
||||
</box>,
|
||||
);
|
||||
isFirstHeader = false;
|
||||
|
||||
@@ -3,7 +3,12 @@ import type { ChoiceContext } from "@opentui-ui/dialog";
|
||||
import { useDialogKeyboard } from "@opentui-ui/dialog/react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { palette } from "../../palette";
|
||||
import type { ClineModelPickerEntry } from "./cline-model-picker";
|
||||
import {
|
||||
CLINE_MODEL_PICKER_TIER_LABELS,
|
||||
type ClineModelPickerEntry,
|
||||
freeTierDescriptionFor,
|
||||
stripFreeMarker,
|
||||
} from "./cline-model-picker";
|
||||
import { CHANGE_PROVIDER_ACTION } from "./model-selector";
|
||||
import { ProviderRow } from "./provider-row";
|
||||
|
||||
@@ -29,12 +34,13 @@ function resolveDisplayName(
|
||||
for (const key of candidates) {
|
||||
if (!key) continue;
|
||||
const hit = knownModels[key] as { name?: string } | undefined;
|
||||
if (hit?.name) return hit.name;
|
||||
if (hit?.name) return stripFreeMarker(hit.name);
|
||||
}
|
||||
}
|
||||
return modelId.includes("/")
|
||||
const fallback = modelId.includes("/")
|
||||
? (modelId.split("/").pop() ?? modelId)
|
||||
: modelId;
|
||||
return stripFreeMarker(fallback);
|
||||
}
|
||||
|
||||
export function ClineModelSelectorContent(
|
||||
@@ -62,11 +68,13 @@ export function ClineModelSelectorContent(
|
||||
key: string;
|
||||
kind: "header" | "model" | "browse";
|
||||
label: string;
|
||||
description?: string;
|
||||
tags: string[];
|
||||
isCurrent: boolean;
|
||||
entryIndex: number;
|
||||
}[] = [];
|
||||
let lastTier: string | null = null;
|
||||
const freeTierDescription = freeTierDescriptionFor(entries);
|
||||
for (let i = 0; i < entries.length; i++) {
|
||||
const entry = entries[i];
|
||||
if (!entry) continue;
|
||||
@@ -76,7 +84,9 @@ export function ClineModelSelectorContent(
|
||||
rows.push({
|
||||
key: `tier-${entry.tier}`,
|
||||
kind: "header",
|
||||
label: entry.tier === "recommended" ? "Recommended" : "Free",
|
||||
label: CLINE_MODEL_PICKER_TIER_LABELS[entry.tier],
|
||||
description:
|
||||
entry.tier === "free" ? freeTierDescription : undefined,
|
||||
tags: [],
|
||||
isCurrent: false,
|
||||
entryIndex: -1,
|
||||
@@ -156,8 +166,18 @@ export function ClineModelSelectorContent(
|
||||
if (row.kind === "header") {
|
||||
const isFirst = idx === 0;
|
||||
return (
|
||||
<box key={row.key} paddingX={1} marginTop={isFirst ? 0 : 1}>
|
||||
<box
|
||||
key={row.key}
|
||||
paddingX={1}
|
||||
marginTop={isFirst ? 0 : 1}
|
||||
flexDirection="column"
|
||||
>
|
||||
<text fg="gray">{row.label}</text>
|
||||
{row.description && (
|
||||
<text fg="gray">
|
||||
<em>{row.description}</em>
|
||||
</text>
|
||||
)}
|
||||
</box>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
ProviderPickerContent,
|
||||
UseExistingOrReconfigureContent,
|
||||
} from "../components/dialogs/provider-picker";
|
||||
import { buildClineModelEntries } from "../components/model-selector/cline-model-picker";
|
||||
import { buildFeaturedModelEntries } from "../components/model-selector/cline-model-picker";
|
||||
import {
|
||||
BROWSE_ALL_ACTION,
|
||||
ClineModelSelectorDialogContent,
|
||||
@@ -341,7 +341,13 @@ export function useModelSelector(opts: {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (config.providerId === "cline") {
|
||||
if (
|
||||
config.providerId === "cline" ||
|
||||
config.providerId === "cline-pass"
|
||||
) {
|
||||
// ClinePass gets the same sectioned picker with Subscribed/Free
|
||||
// sections — free models are selectable while staying on ClinePass
|
||||
const featuredProviderId = config.providerId;
|
||||
const clineResult = await dialog.choice<string>({
|
||||
style: { maxHeight: termHeight - 2 },
|
||||
content: (ctx: ChoiceContext<string>) => (
|
||||
@@ -351,7 +357,10 @@ export function useModelSelector(opts: {
|
||||
currentProviderName={providerDisplayName}
|
||||
knownModels={config.knownModels as Record<string, unknown>}
|
||||
loadEntries={async () =>
|
||||
buildClineModelEntries(await fetchClineRecommendedModels())
|
||||
buildFeaturedModelEntries(
|
||||
featuredProviderId,
|
||||
await fetchClineRecommendedModels(),
|
||||
)
|
||||
}
|
||||
/>
|
||||
),
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
isImagePath,
|
||||
loadImageAsDataUrl,
|
||||
resolveExistingImagePath,
|
||||
} from "../../utils/image-attachments";
|
||||
} from "@cline/cline-hub/connectors";
|
||||
|
||||
const COMMAND_TIMEOUT_MS = 1500;
|
||||
const MAX_CLIPBOARD_BYTES = 20 * 1024 * 1024;
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
loadIndividualSubscriptionPlansFromProviderSettings,
|
||||
} from "../../cline-account";
|
||||
import {
|
||||
buildClineModelEntries,
|
||||
buildFeaturedModelEntries,
|
||||
type ClineModelPickerEntry,
|
||||
useClineRecommendedModels,
|
||||
} from "../../components/model-selector/cline-model-picker";
|
||||
@@ -206,11 +206,14 @@ export function useOnboardingController(props: OnboardingControllerProps) {
|
||||
|
||||
const modelList = useSearchableList(modelItems, createCustomModelItem);
|
||||
|
||||
// Cline featured model picker
|
||||
// Cline featured model picker (ClinePass gets Subscribed/Free sections)
|
||||
const recommended = useClineRecommendedModels();
|
||||
const clineEntries: ClineModelPickerEntry[] = useMemo(
|
||||
() => (recommended.data ? buildClineModelEntries(recommended.data) : []),
|
||||
[recommended.data],
|
||||
() =>
|
||||
recommended.data
|
||||
? buildFeaturedModelEntries(activeProviderId, recommended.data)
|
||||
: [],
|
||||
[recommended.data, activeProviderId],
|
||||
);
|
||||
const [clineModelSelected, setClineModelSelected] = useState(0);
|
||||
const [clineModelReasoningIds, setClineModelReasoningIds] = useState<
|
||||
@@ -221,20 +224,37 @@ export function useOnboardingController(props: OnboardingControllerProps) {
|
||||
>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
getLocalProviderModels("cline")
|
||||
.then(({ models }) => {
|
||||
const ids = new Set<string>();
|
||||
for (const m of models) {
|
||||
// The featured picker serves both cline and cline-pass, so pool reasoning
|
||||
// support and display names from both catalogs
|
||||
void Promise.allSettled(
|
||||
["cline", "cline-pass"].map((providerId) =>
|
||||
getLocalProviderModels(providerId),
|
||||
),
|
||||
).then((results) => {
|
||||
const ids = new Set<string>();
|
||||
for (const result of results) {
|
||||
if (result.status !== "fulfilled") continue;
|
||||
for (const m of result.value.models) {
|
||||
if (m.supportsReasoning) ids.add(m.id);
|
||||
}
|
||||
setClineModelReasoningIds(ids);
|
||||
})
|
||||
.catch(() => {});
|
||||
resolveProviderConfig("cline")
|
||||
.then((resolved) => {
|
||||
if (resolved?.knownModels) setClineKnownModels(resolved.knownModels);
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
setClineModelReasoningIds(ids);
|
||||
});
|
||||
void Promise.allSettled(
|
||||
["cline", "cline-pass"].map((providerId) =>
|
||||
resolveProviderConfig(providerId),
|
||||
),
|
||||
).then((results) => {
|
||||
const merged: Record<string, unknown> = {};
|
||||
for (const result of results) {
|
||||
if (result.status === "fulfilled" && result.value?.knownModels) {
|
||||
Object.assign(merged, result.value.knownModels);
|
||||
}
|
||||
}
|
||||
if (Object.keys(merged).length > 0) {
|
||||
setClineKnownModels(merged);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Thinking level
|
||||
|
||||
@@ -135,9 +135,9 @@ describe("onboarding model helpers", () => {
|
||||
expect(getOAuthProviderLabel("oca")).toBe("oca");
|
||||
});
|
||||
|
||||
it("uses the featured Cline model picker only for the Cline provider", () => {
|
||||
it("uses the featured Cline model picker for the Cline and ClinePass providers", () => {
|
||||
expect(shouldUseFeaturedClineModelPicker("cline")).toBe(true);
|
||||
expect(shouldUseFeaturedClineModelPicker("cline-pass")).toBe(false);
|
||||
expect(shouldUseFeaturedClineModelPicker("cline-pass")).toBe(true);
|
||||
expect(shouldUseFeaturedClineModelPicker("anthropic")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -207,5 +207,6 @@ export function getOAuthProviderLabel(providerId: string): string {
|
||||
}
|
||||
|
||||
export function shouldUseFeaturedClineModelPicker(providerId: string): boolean {
|
||||
return providerId === "cline";
|
||||
// ClinePass uses the featured picker too, with Subscribed/Free sections
|
||||
return providerId === "cline" || providerId === "cline-pass";
|
||||
}
|
||||
|
||||
@@ -1,9 +1,45 @@
|
||||
import type { AgentEvent, TeamEvent } from "@cline/core";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { handleEvent, handleTeamEvent } from "./events";
|
||||
import {
|
||||
handleEvent,
|
||||
handleTeamEvent,
|
||||
resolveStatusNoticeLabel,
|
||||
} from "./events";
|
||||
import { setCurrentOutputMode } from "./output";
|
||||
import type { Config } from "./types";
|
||||
|
||||
describe("resolveStatusNoticeLabel", () => {
|
||||
it("maps compaction status reasons to stable labels", () => {
|
||||
expect(
|
||||
resolveStatusNoticeLabel({
|
||||
type: "notice",
|
||||
noticeType: "status",
|
||||
displayRole: "status",
|
||||
message: "auto-compacting",
|
||||
reason: "auto_compaction",
|
||||
} as AgentEvent),
|
||||
).toBe("auto-compacting");
|
||||
expect(
|
||||
resolveStatusNoticeLabel({
|
||||
type: "notice",
|
||||
noticeType: "status",
|
||||
displayRole: "status",
|
||||
message: "manual",
|
||||
reason: "manual_compaction",
|
||||
} as AgentEvent),
|
||||
).toBe("compacting");
|
||||
expect(
|
||||
resolveStatusNoticeLabel({
|
||||
type: "notice",
|
||||
noticeType: "status",
|
||||
displayRole: "status",
|
||||
message: "compaction-budget-adjusted",
|
||||
reason: "compaction_budget_emergency",
|
||||
} as AgentEvent),
|
||||
).toBe("context budget adjusted");
|
||||
});
|
||||
});
|
||||
|
||||
describe("handleEvent text formatting", () => {
|
||||
let output = "";
|
||||
|
||||
|
||||
@@ -27,8 +27,13 @@ export function resolveStatusNoticeLabel(
|
||||
if (event.type !== "notice" || event.displayRole !== "status") {
|
||||
return undefined;
|
||||
}
|
||||
if (event.reason === "auto_compaction") {
|
||||
return "auto-compacting";
|
||||
switch (event.reason) {
|
||||
case "auto_compaction":
|
||||
return "auto-compacting";
|
||||
case "manual_compaction":
|
||||
return "compacting";
|
||||
case "compaction_budget_emergency":
|
||||
return "context budget adjusted";
|
||||
}
|
||||
return event.message.trim() || undefined;
|
||||
}
|
||||
|
||||
@@ -53,6 +53,37 @@ describe("shouldZeroClineFreeModelCost", () => {
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("zeros cost of free models selected on the cline-pass provider", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => {
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
free: [{ id: "deepseek/deepseek-v4-flash" }],
|
||||
}),
|
||||
{ status: 200, headers: { "content-type": "application/json" } },
|
||||
);
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
shouldZeroClineFreeModelCost({
|
||||
providerId: "cline-pass",
|
||||
modelId: "deepseek/deepseek-v4-flash",
|
||||
baseUrl: "https://cline.test/api/v1",
|
||||
}),
|
||||
).resolves.toBe(true);
|
||||
|
||||
// subscription (cline-pass/...) models are not in the free bucket
|
||||
await expect(
|
||||
shouldZeroClineFreeModelCost({
|
||||
providerId: "cline-pass",
|
||||
modelId: "cline-pass/glm-5.1",
|
||||
baseUrl: "https://cline.test/api/v1",
|
||||
}),
|
||||
).resolves.toBe(false);
|
||||
});
|
||||
|
||||
it("does not match a paid model by only the final path segment", async () => {
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
|
||||
@@ -73,7 +73,9 @@ function getClineFreeModelIds(baseUrl: string): Promise<readonly string[]> {
|
||||
export async function shouldZeroClineFreeModelCost(
|
||||
config: Pick<Config, "providerId" | "modelId" | "baseUrl">,
|
||||
): Promise<boolean> {
|
||||
if (config.providerId !== "cline") return false;
|
||||
// Free models are also selectable on ClinePass — they ride usage billing at $0
|
||||
if (config.providerId !== "cline" && config.providerId !== "cline-pass")
|
||||
return false;
|
||||
const modelId = normalizeModelId(config.modelId);
|
||||
if (!modelId) return false;
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { appendFileSync, existsSync, unlinkSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
@@ -40,19 +39,6 @@ export function randomSessionId(): string {
|
||||
return `${Date.now()}_${nanoid(5)}_cli`;
|
||||
}
|
||||
|
||||
export function resolveWorkspaceRoot(cwd: string): string {
|
||||
const result = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.status === 0) {
|
||||
const value = result.stdout.trim();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return cwd;
|
||||
}
|
||||
|
||||
export function truncate(str: string, maxLen: number): string {
|
||||
const oneLine = str.replace(/\n/g, " ").trim();
|
||||
if (oneLine.length <= maxLen) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { resolveCliLaunchSpec } from "./internal-launch";
|
||||
import { resolveCliLaunchSpec } from "@cline/cline-hub/connectors";
|
||||
|
||||
export interface HistoryResumeCommand {
|
||||
launcher: string;
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
"description": "Browser dashboard for the Cline hub: live clients, sessions, streaming chat, and hub restart.",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/server.ts"
|
||||
".": "./src/server.ts",
|
||||
"./connectors": "./src/connectors/index.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"build:webview": "bun run --cwd src/webview build",
|
||||
@@ -16,8 +17,18 @@
|
||||
"typecheck": "bun tsc -p tsconfig.json --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chat-adapter/discord": "^4.23.0",
|
||||
"@chat-adapter/gchat": "^4.23.0",
|
||||
"@chat-adapter/linear": "^4.23.0",
|
||||
"@chat-adapter/slack": "^4.23.0",
|
||||
"@chat-adapter/telegram": "^4.23.0",
|
||||
"@chat-adapter/whatsapp": "^4.23.0",
|
||||
"@cline/core": "workspace:*",
|
||||
"@cline/llms": "workspace:*",
|
||||
"@cline/shared": "workspace:*"
|
||||
"@cline/shared": "workspace:*",
|
||||
"@gramio/format": "^0.7.0",
|
||||
"chat": "^4.23.0",
|
||||
"commander": "^14.0.3",
|
||||
"zod": "^4.3.6"
|
||||
}
|
||||
}
|
||||
|
||||
+19
-21
@@ -2,24 +2,15 @@ import {
|
||||
createDiscordAdapter,
|
||||
type DiscordAdapter,
|
||||
} from "@chat-adapter/discord";
|
||||
import type { ChatStartSessionRequest } from "@cline/core";
|
||||
import {
|
||||
createUserInstructionConfigService,
|
||||
HubSessionClient,
|
||||
} from "@cline/core";
|
||||
import { createUserInstructionConfigService } from "@cline/core";
|
||||
import { HubSessionClient } from "@cline/core/hub";
|
||||
import type {
|
||||
ChatStartSessionRequest,
|
||||
ConnectDiscordOptions,
|
||||
DiscordConnectorState,
|
||||
} from "@cline/shared";
|
||||
import { Chat, ConsoleLogger, type Thread, ThreadImpl } from "chat";
|
||||
import type { Command } from "commander";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
import {
|
||||
createChatSdkLogger,
|
||||
@@ -33,6 +24,13 @@ import {
|
||||
maybeHandleConnectorApprovalReply,
|
||||
} from "../connector-host";
|
||||
import { dispatchConnectorHook } from "../hooks";
|
||||
import {
|
||||
ensureHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultHubRpcAddress,
|
||||
} from "../hub-runtime";
|
||||
import { createConnectorLogger } from "../logger";
|
||||
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
|
||||
import {
|
||||
type PendingConnectorApproval,
|
||||
truncateConnectorText,
|
||||
@@ -686,7 +684,7 @@ function isRestorableThread(
|
||||
async function restoreDiscordThreadSubscriptions(input: {
|
||||
bot: Pick<Chat, "reviver">;
|
||||
bindingsPath: string;
|
||||
logger: ReturnType<typeof createCliLoggerAdapter>;
|
||||
logger: ReturnType<typeof createConnectorLogger>;
|
||||
}): Promise<number> {
|
||||
const bindings = readBindings<DiscordThreadState>(input.bindingsPath);
|
||||
const restoredThreadIds = new Set<string>();
|
||||
@@ -761,7 +759,7 @@ class DiscordConnector extends ConnectorBase<
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
|
||||
)
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
@@ -859,7 +857,7 @@ class DiscordConnector extends ConnectorBase<
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
resolveDefaultHubRpcAddress(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
@@ -993,7 +991,7 @@ class DiscordConnector extends ConnectorBase<
|
||||
return 0;
|
||||
}
|
||||
|
||||
const loggerAdapter = createCliLoggerAdapter({
|
||||
const loggerAdapter = createConnectorLogger(io, {
|
||||
runtime: "cli",
|
||||
component: "discord-connect",
|
||||
});
|
||||
@@ -1039,11 +1037,10 @@ class DiscordConnector extends ConnectorBase<
|
||||
cwd: commandCwd,
|
||||
workspaceRoot: startRequest.workspaceRoot || commandCwd,
|
||||
});
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } =
|
||||
await ensureCliHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
|
||||
const clientId = `discord-${process.pid}-${Date.now()}`;
|
||||
const client = new HubSessionClient({
|
||||
@@ -1139,6 +1136,7 @@ class DiscordConnector extends ConnectorBase<
|
||||
resolveMuteTarget: ({ target }) => resolveDiscordMuteTarget(target),
|
||||
createEmptyRuntimeReplyResolver:
|
||||
createDiscordEmptyRuntimeReplyResolver,
|
||||
resolveSessionMetadata: io.resolveSessionMetadata,
|
||||
getSessionMetadata: (currentThread, _clientId, currentState) => ({
|
||||
userName: options.userName,
|
||||
applicationId: options.applicationId,
|
||||
+20
-22
@@ -1,23 +1,13 @@
|
||||
import { createGoogleChatAdapter } from "@chat-adapter/gchat";
|
||||
import type { ChatStartSessionRequest } from "@cline/core";
|
||||
import {
|
||||
createUserInstructionConfigService,
|
||||
HubSessionClient,
|
||||
} from "@cline/core";
|
||||
import { createUserInstructionConfigService } from "@cline/core";
|
||||
import { HubSessionClient } from "@cline/core/hub";
|
||||
import type {
|
||||
ChatStartSessionRequest,
|
||||
ConnectGoogleChatOptions,
|
||||
GoogleChatConnectorState,
|
||||
} from "@cline/shared";
|
||||
import { Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import type { Command } from "commander";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
import {
|
||||
createChatSdkLogger,
|
||||
@@ -31,6 +21,13 @@ import {
|
||||
maybeHandleConnectorApprovalReply,
|
||||
} from "../connector-host";
|
||||
import { dispatchConnectorHook } from "../hooks";
|
||||
import {
|
||||
ensureHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultHubRpcAddress,
|
||||
} from "../hub-runtime";
|
||||
import { createConnectorLogger } from "../logger";
|
||||
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
|
||||
import {
|
||||
type PendingConnectorApproval,
|
||||
truncateConnectorText,
|
||||
@@ -55,6 +52,7 @@ import {
|
||||
import type {
|
||||
ConnectCommandDefinition,
|
||||
ConnectIo,
|
||||
ConnectorLoggerAdapter,
|
||||
ConnectStopResult,
|
||||
} from "../types";
|
||||
import {
|
||||
@@ -166,7 +164,7 @@ async function persistGoogleChatThreadContext(input: {
|
||||
async function deliverScheduledResult(input: {
|
||||
bot: Chat;
|
||||
client: HubSessionClient;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
bindingsPath: string;
|
||||
userName: string;
|
||||
scheduleId: string;
|
||||
@@ -253,7 +251,7 @@ class GoogleChatConnector extends ConnectorBase<
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
|
||||
)
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
@@ -319,7 +317,7 @@ class GoogleChatConnector extends ConnectorBase<
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
resolveDefaultHubRpcAddress(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
@@ -446,7 +444,7 @@ class GoogleChatConnector extends ConnectorBase<
|
||||
return 0;
|
||||
}
|
||||
|
||||
const loggerAdapter = createCliLoggerAdapter({
|
||||
const loggerAdapter = createConnectorLogger(io, {
|
||||
runtime: "cli",
|
||||
component: "gchat-connect",
|
||||
});
|
||||
@@ -543,11 +541,10 @@ class GoogleChatConnector extends ConnectorBase<
|
||||
cwd: commandCwd,
|
||||
workspaceRoot: startRequest.workspaceRoot || commandCwd,
|
||||
});
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } =
|
||||
await ensureCliHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
|
||||
const clientId = `gchat-${process.pid}-${Date.now()}`;
|
||||
const client = new HubSessionClient({
|
||||
@@ -614,6 +611,7 @@ class GoogleChatConnector extends ConnectorBase<
|
||||
chatCommandHost,
|
||||
activeTurns,
|
||||
turnKey: queueKey,
|
||||
resolveSessionMetadata: io.resolveSessionMetadata,
|
||||
getSessionMetadata: (currentThread, _clientId, currentState) => ({
|
||||
userName: options.userName,
|
||||
gchatThreadId: currentThread.id,
|
||||
+24
-23
@@ -1,19 +1,12 @@
|
||||
import type { ChatStartSessionRequest } from "@cline/core";
|
||||
import {
|
||||
createUserInstructionConfigService,
|
||||
HubSessionClient,
|
||||
} from "@cline/core";
|
||||
import type { ConnectLinearOptions, LinearConnectorState } from "@cline/shared";
|
||||
import { createUserInstructionConfigService } from "@cline/core";
|
||||
import { HubSessionClient } from "@cline/core/hub";
|
||||
import type {
|
||||
ChatStartSessionRequest,
|
||||
ConnectLinearOptions,
|
||||
LinearConnectorState,
|
||||
} from "@cline/shared";
|
||||
import { type Adapter, Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import type { Command } from "commander";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
import {
|
||||
createChatSdkLogger,
|
||||
@@ -27,6 +20,13 @@ import {
|
||||
maybeHandleConnectorApprovalReply,
|
||||
} from "../connector-host";
|
||||
import { dispatchConnectorHook } from "../hooks";
|
||||
import {
|
||||
ensureHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultHubRpcAddress,
|
||||
} from "../hub-runtime";
|
||||
import { createConnectorLogger } from "../logger";
|
||||
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
|
||||
import {
|
||||
type PendingConnectorApproval,
|
||||
truncateConnectorText,
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
import type {
|
||||
ConnectCommandDefinition,
|
||||
ConnectIo,
|
||||
ConnectorLoggerAdapter,
|
||||
ConnectStopResult,
|
||||
} from "../types";
|
||||
import { getConnectorSystemPrompt } from "./prompts";
|
||||
@@ -204,7 +205,7 @@ async function persistLinearThreadContext(input: {
|
||||
async function deliverScheduledResult(input: {
|
||||
bot: Chat;
|
||||
client: HubSessionClient;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
bindingsPath: string;
|
||||
userName: string;
|
||||
scheduleId: string;
|
||||
@@ -313,7 +314,7 @@ class LinearConnector extends ConnectorBase<
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
|
||||
)
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
@@ -399,7 +400,7 @@ class LinearConnector extends ConnectorBase<
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
resolveDefaultHubRpcAddress(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
@@ -513,7 +514,7 @@ class LinearConnector extends ConnectorBase<
|
||||
return 0;
|
||||
}
|
||||
|
||||
const loggerAdapter = createCliLoggerAdapter({
|
||||
const loggerAdapter = createConnectorLogger(io, {
|
||||
runtime: "cli",
|
||||
component: "linear-connect",
|
||||
});
|
||||
@@ -578,11 +579,10 @@ class LinearConnector extends ConnectorBase<
|
||||
cwd: commandCwd,
|
||||
workspaceRoot: startRequest.workspaceRoot || commandCwd,
|
||||
});
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } =
|
||||
await ensureCliHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
|
||||
const clientId = `linear-${process.pid}-${Date.now()}`;
|
||||
const client = new HubSessionClient({
|
||||
@@ -651,6 +651,7 @@ class LinearConnector extends ConnectorBase<
|
||||
chatCommandHost,
|
||||
activeTurns,
|
||||
turnKey: queueKey,
|
||||
resolveSessionMetadata: io.resolveSessionMetadata,
|
||||
getSessionMetadata: (currentThread, _clientId, currentState) => ({
|
||||
userName: options.userName,
|
||||
linearThreadId: currentThread.id,
|
||||
+24
-23
@@ -1,10 +1,11 @@
|
||||
import { createSlackAdapter, type SlackAdapter } from "@chat-adapter/slack";
|
||||
import type { ChatStartSessionRequest } from "@cline/core";
|
||||
import {
|
||||
createUserInstructionConfigService,
|
||||
HubSessionClient,
|
||||
} from "@cline/core";
|
||||
import type { ConnectSlackOptions, SlackConnectorState } from "@cline/shared";
|
||||
import { createUserInstructionConfigService } from "@cline/core";
|
||||
import { HubSessionClient } from "@cline/core/hub";
|
||||
import type {
|
||||
ChatStartSessionRequest,
|
||||
ConnectSlackOptions,
|
||||
SlackConnectorState,
|
||||
} from "@cline/shared";
|
||||
import {
|
||||
type Adapter,
|
||||
Chat,
|
||||
@@ -14,14 +15,6 @@ import {
|
||||
ThreadImpl,
|
||||
} from "chat";
|
||||
import type { Command } from "commander";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
import {
|
||||
createChatSdkLogger,
|
||||
@@ -35,6 +28,13 @@ import {
|
||||
maybeHandleConnectorApprovalReply,
|
||||
} from "../connector-host";
|
||||
import { dispatchConnectorHook } from "../hooks";
|
||||
import {
|
||||
ensureHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultHubRpcAddress,
|
||||
} from "../hub-runtime";
|
||||
import { createConnectorLogger } from "../logger";
|
||||
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
|
||||
import {
|
||||
type PendingConnectorApproval,
|
||||
truncateConnectorText,
|
||||
@@ -61,6 +61,7 @@ import {
|
||||
import type {
|
||||
ConnectCommandDefinition,
|
||||
ConnectIo,
|
||||
ConnectorLoggerAdapter,
|
||||
ConnectStopResult,
|
||||
} from "../types";
|
||||
import {
|
||||
@@ -351,7 +352,7 @@ async function deliverScheduledResult(input: {
|
||||
bot: Chat;
|
||||
slack: SlackAdapter;
|
||||
client: HubSessionClient;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
bindingsPath: string;
|
||||
userName: string;
|
||||
scheduleId: string;
|
||||
@@ -481,7 +482,7 @@ class SlackConnector extends ConnectorBase<
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
|
||||
)
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
@@ -592,7 +593,7 @@ class SlackConnector extends ConnectorBase<
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
resolveDefaultHubRpcAddress(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
@@ -707,7 +708,7 @@ class SlackConnector extends ConnectorBase<
|
||||
return 0;
|
||||
}
|
||||
|
||||
const loggerAdapter = createCliLoggerAdapter({
|
||||
const loggerAdapter = createConnectorLogger(io, {
|
||||
runtime: "cli",
|
||||
component: "slack-connect",
|
||||
});
|
||||
@@ -772,11 +773,10 @@ class SlackConnector extends ConnectorBase<
|
||||
cwd: commandCwd,
|
||||
workspaceRoot: startRequest.workspaceRoot || commandCwd,
|
||||
});
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } =
|
||||
await ensureCliHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
|
||||
const clientId = `slack-${process.pid}-${Date.now()}`;
|
||||
const client = new HubSessionClient({
|
||||
@@ -856,6 +856,7 @@ class SlackConnector extends ConnectorBase<
|
||||
chatCommandHost,
|
||||
activeTurns,
|
||||
turnKey: queueKey,
|
||||
resolveSessionMetadata: io.resolveSessionMetadata,
|
||||
getSessionMetadata: (
|
||||
currentThread,
|
||||
_clientId,
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import type { SentMessage, Thread } from "chat";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import type { ConnectorThreadState } from "../thread-bindings";
|
||||
import type { ConnectorLoggerAdapter } from "../types";
|
||||
import {
|
||||
buildTelegramFormattedPayload,
|
||||
buildTelegramFormattedPayloads,
|
||||
@@ -16,7 +16,7 @@ function createLogger() {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
} as unknown as CliLoggerAdapter;
|
||||
} as unknown as ConnectorLoggerAdapter;
|
||||
}
|
||||
|
||||
function createThread(id = "telegram:123") {
|
||||
+2
-2
@@ -1,7 +1,7 @@
|
||||
import { markdownToFormattable } from "@gramio/format/markdown";
|
||||
import type { Thread } from "chat";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import type { ConnectorThreadState } from "../thread-bindings";
|
||||
import type { ConnectorLoggerAdapter } from "../types";
|
||||
|
||||
const TELEGRAM_API_BASE = "https://api.telegram.org";
|
||||
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
||||
@@ -211,7 +211,7 @@ export async function postTelegramFormattedReply<
|
||||
thread: Thread<TState>;
|
||||
text: string;
|
||||
botToken: string;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
apiBaseUrl?: string;
|
||||
fetchImpl?: FetchLike;
|
||||
}): Promise<void> {
|
||||
+20
-22
@@ -1,23 +1,13 @@
|
||||
import { createTelegramAdapter } from "@chat-adapter/telegram";
|
||||
import type { ChatStartSessionRequest } from "@cline/core";
|
||||
import {
|
||||
createUserInstructionConfigService,
|
||||
HubSessionClient,
|
||||
} from "@cline/core";
|
||||
import { createUserInstructionConfigService } from "@cline/core";
|
||||
import { HubSessionClient } from "@cline/core/hub";
|
||||
import type {
|
||||
ChatStartSessionRequest,
|
||||
ConnectTelegramOptions,
|
||||
TelegramConnectorState,
|
||||
} from "@cline/shared";
|
||||
import { Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import type { Command } from "commander";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
import { createChatSdkLogger, enqueueThreadTurn } from "../chat-runtime";
|
||||
import { isProcessRunning } from "../common";
|
||||
@@ -27,6 +17,13 @@ import {
|
||||
maybeHandleConnectorApprovalReply,
|
||||
} from "../connector-host";
|
||||
import { dispatchConnectorHook } from "../hooks";
|
||||
import {
|
||||
ensureHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultHubRpcAddress,
|
||||
} from "../hub-runtime";
|
||||
import { createConnectorLogger } from "../logger";
|
||||
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
|
||||
import {
|
||||
type PendingConnectorApproval,
|
||||
truncateConnectorText,
|
||||
@@ -51,6 +48,7 @@ import {
|
||||
import type {
|
||||
ConnectCommandDefinition,
|
||||
ConnectIo,
|
||||
ConnectorLoggerAdapter,
|
||||
ConnectStopResult,
|
||||
} from "../types";
|
||||
import {
|
||||
@@ -268,7 +266,7 @@ async function persistTelegramThreadContext(input: {
|
||||
async function deliverScheduledResult(input: {
|
||||
bot: Chat;
|
||||
client: HubSessionClient;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
bindingsPath: string;
|
||||
botUsername: string;
|
||||
scheduleId: string;
|
||||
@@ -443,7 +441,7 @@ class TelegramConnector extends ConnectorBase<
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
|
||||
)
|
||||
.addHelpText(
|
||||
"after",
|
||||
@@ -508,7 +506,7 @@ class TelegramConnector extends ConnectorBase<
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
resolveDefaultHubRpcAddress(),
|
||||
hookCommand: allowedUserId
|
||||
? buildTelegramAllowedUserHookCommand(
|
||||
normalizeAllowedTelegramUserId(allowedUserId),
|
||||
@@ -670,7 +668,7 @@ class TelegramConnector extends ConnectorBase<
|
||||
return 0;
|
||||
}
|
||||
|
||||
const loggerAdapter = createCliLoggerAdapter({
|
||||
const loggerAdapter = createConnectorLogger(io, {
|
||||
runtime: "cli",
|
||||
component: "telegram-connect",
|
||||
});
|
||||
@@ -713,11 +711,10 @@ class TelegramConnector extends ConnectorBase<
|
||||
cwd: commandCwd,
|
||||
workspaceRoot: startRequest.workspaceRoot || commandCwd,
|
||||
});
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } =
|
||||
await ensureCliHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
|
||||
const clientId = `telegram-${process.pid}-${Date.now()}`;
|
||||
const client = new HubSessionClient({
|
||||
@@ -826,6 +823,7 @@ class TelegramConnector extends ConnectorBase<
|
||||
logger: loggerAdapter,
|
||||
});
|
||||
},
|
||||
resolveSessionMetadata: io.resolveSessionMetadata,
|
||||
getSessionMetadata: (currentThread, _clientId, currentState) => ({
|
||||
botUserName: options.botUsername,
|
||||
telegramThreadId: currentThread.id,
|
||||
+20
-22
@@ -1,23 +1,13 @@
|
||||
import { createWhatsAppAdapter } from "@chat-adapter/whatsapp";
|
||||
import type { ChatStartSessionRequest } from "@cline/core";
|
||||
import {
|
||||
createUserInstructionConfigService,
|
||||
HubSessionClient,
|
||||
} from "@cline/core";
|
||||
import { createUserInstructionConfigService } from "@cline/core";
|
||||
import { HubSessionClient } from "@cline/core/hub";
|
||||
import type {
|
||||
ChatStartSessionRequest,
|
||||
ConnectWhatsAppOptions,
|
||||
WhatsAppConnectorState,
|
||||
} from "@cline/shared";
|
||||
import { Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import type { Command } from "commander";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
ensureCliHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultCliRpcAddress,
|
||||
} from "../../utils/hub-runtime";
|
||||
import { createWorkspaceChatCommandHost } from "../../utils/plugin-chat-commands";
|
||||
import { ConnectorBase } from "../base";
|
||||
import {
|
||||
createChatSdkLogger,
|
||||
@@ -31,6 +21,13 @@ import {
|
||||
maybeHandleConnectorApprovalReply,
|
||||
} from "../connector-host";
|
||||
import { dispatchConnectorHook } from "../hooks";
|
||||
import {
|
||||
ensureHubServer,
|
||||
parseHubEndpointOverride,
|
||||
resolveDefaultHubRpcAddress,
|
||||
} from "../hub-runtime";
|
||||
import { createConnectorLogger } from "../logger";
|
||||
import { createWorkspaceChatCommandHost } from "../plugin-chat-commands";
|
||||
import {
|
||||
type PendingConnectorApproval,
|
||||
truncateConnectorText,
|
||||
@@ -55,6 +52,7 @@ import {
|
||||
import type {
|
||||
ConnectCommandDefinition,
|
||||
ConnectIo,
|
||||
ConnectorLoggerAdapter,
|
||||
ConnectStopResult,
|
||||
} from "../types";
|
||||
import {
|
||||
@@ -190,7 +188,7 @@ async function persistWhatsAppThreadContext(input: {
|
||||
async function deliverScheduledResult(input: {
|
||||
bot: Chat;
|
||||
client: HubSessionClient;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
bindingsPath: string;
|
||||
options: ConnectWhatsAppOptions;
|
||||
scheduleId: string;
|
||||
@@ -296,7 +294,7 @@ class WhatsAppConnector extends ConnectorBase<
|
||||
.option(
|
||||
"--rpc-address <host:port>",
|
||||
"RPC address",
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultCliRpcAddress(),
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() || resolveDefaultHubRpcAddress(),
|
||||
)
|
||||
.option("--host <host>", "Webhook listen host")
|
||||
.option("--port <port>", "Webhook listen port")
|
||||
@@ -367,7 +365,7 @@ class WhatsAppConnector extends ConnectorBase<
|
||||
rpcAddress:
|
||||
opts.rpcAddress?.trim() ||
|
||||
process.env.CLINE_RPC_ADDRESS?.trim() ||
|
||||
resolveDefaultCliRpcAddress(),
|
||||
resolveDefaultHubRpcAddress(),
|
||||
hookCommand:
|
||||
opts.hookCommand?.trim() ||
|
||||
process.env.CLINE_CONNECT_HOOK_COMMAND?.trim(),
|
||||
@@ -485,7 +483,7 @@ class WhatsAppConnector extends ConnectorBase<
|
||||
return 0;
|
||||
}
|
||||
|
||||
const loggerAdapter = createCliLoggerAdapter({
|
||||
const loggerAdapter = createConnectorLogger(io, {
|
||||
runtime: "cli",
|
||||
component: "whatsapp-connect",
|
||||
});
|
||||
@@ -545,11 +543,10 @@ class WhatsAppConnector extends ConnectorBase<
|
||||
cwd: commandCwd,
|
||||
workspaceRoot: startRequest.workspaceRoot || commandCwd,
|
||||
});
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } =
|
||||
await ensureCliHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
const { url: rpcAddress, authToken: rpcAuthToken } = await ensureHubServer(
|
||||
startRequest.workspaceRoot || startRequest.cwd || process.cwd(),
|
||||
parseHubEndpointOverride(options.rpcAddress),
|
||||
);
|
||||
|
||||
const clientId = `whatsapp-${process.pid}-${Date.now()}`;
|
||||
const client = new HubSessionClient({
|
||||
@@ -623,6 +620,7 @@ class WhatsAppConnector extends ConnectorBase<
|
||||
chatCommandHost,
|
||||
activeTurns,
|
||||
turnKey: queueKey,
|
||||
resolveSessionMetadata: io.resolveSessionMetadata,
|
||||
getSessionMetadata: (currentThread, _clientId, currentState) => ({
|
||||
userName: options.userName,
|
||||
phoneNumberId: options.phoneNumberId,
|
||||
@@ -1,6 +1,6 @@
|
||||
import { existsSync, readdirSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveClineDataDir } from "@cline/core";
|
||||
import { resolveClineDataDir } from "@cline/shared/storage";
|
||||
import { Command, CommanderError } from "commander";
|
||||
import {
|
||||
isProcessRunning,
|
||||
@@ -158,6 +158,7 @@ export abstract class ConnectorBase<Options, State>
|
||||
["connect", this.name],
|
||||
input.rawArgs,
|
||||
input.childEnvVar,
|
||||
input.io,
|
||||
);
|
||||
if (!pid) {
|
||||
input.io.writeErr(input.launchFailureMessage);
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import { stat } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
import { resolveWorkspaceRoot } from "./helpers";
|
||||
import { resolveWorkspaceRoot } from "./workspace";
|
||||
|
||||
export type ChatCommandState = {
|
||||
enableTools: boolean;
|
||||
+13
-11
@@ -1,6 +1,6 @@
|
||||
import type { CliLoggerAdapter } from "../logging/adapter";
|
||||
import type { ConnectorLoggerAdapter } from "./types";
|
||||
|
||||
export function createChatSdkLogger(adapter: CliLoggerAdapter) {
|
||||
export function createChatSdkLogger(adapter: ConnectorLoggerAdapter) {
|
||||
return {
|
||||
child(prefix: string) {
|
||||
return createChatSdkLogger(adapter.child({ chatLogger: prefix }));
|
||||
@@ -80,18 +80,20 @@ export async function startConnectorWebhookServer(input: {
|
||||
const hostHeader = req.headers.host || `${input.host}:${input.port}`;
|
||||
const requestUrl = new URL(req.url || "/", `http://${hostHeader}`);
|
||||
const body = await readRequestBody(req);
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(req.headers)) {
|
||||
if (Array.isArray(value)) {
|
||||
for (const entry of value) {
|
||||
headers.append(key, entry);
|
||||
}
|
||||
} else if (typeof value === "string") {
|
||||
headers.append(key, value);
|
||||
}
|
||||
}
|
||||
const request = new Request(requestUrl.toString(), {
|
||||
method: req.method,
|
||||
headers: new Headers(
|
||||
Object.entries(req.headers).flatMap(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((entry) => [key, entry] as [string, string]);
|
||||
}
|
||||
return typeof value === "string" ? [[key, value]] : [];
|
||||
}),
|
||||
),
|
||||
headers,
|
||||
body,
|
||||
duplex: body ? "half" : undefined,
|
||||
});
|
||||
const handler =
|
||||
input.routes[requestUrl.pathname] ??
|
||||
+9
-11
@@ -1,4 +1,4 @@
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
@@ -26,16 +26,15 @@ describe("spawnDetachedConnector", () => {
|
||||
});
|
||||
|
||||
it("preserves bun conditions and resolves the cli entrypoint for detached launches", () => {
|
||||
const connectorsDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(connectorsDir, "../../../../");
|
||||
const entryPath = fileURLToPath(import.meta.url);
|
||||
expect(
|
||||
__test__.buildDetachedConnectorCommand(
|
||||
["connect", "telegram"],
|
||||
["-m", "ClineAdapterBot", "-k", "token-123"],
|
||||
"/Users/test/.bun/bin/bun",
|
||||
"./apps/cli/src/index.ts",
|
||||
entryPath,
|
||||
["--conditions=development"],
|
||||
repoRoot,
|
||||
dirname(entryPath),
|
||||
{},
|
||||
),
|
||||
).toEqual({
|
||||
@@ -44,7 +43,7 @@ describe("spawnDetachedConnector", () => {
|
||||
"--inspect=127.0.0.1:0",
|
||||
"--enable-source-maps",
|
||||
"--conditions=development",
|
||||
resolve(repoRoot, "apps/cli/src/index.ts"),
|
||||
entryPath,
|
||||
"connect",
|
||||
"telegram",
|
||||
"-m",
|
||||
@@ -57,16 +56,15 @@ describe("spawnDetachedConnector", () => {
|
||||
});
|
||||
|
||||
it("uses a dynamic connector inspector port for development node launches", () => {
|
||||
const connectorsDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(connectorsDir, "../../../../");
|
||||
const entryPath = fileURLToPath(import.meta.url);
|
||||
expect(
|
||||
__test__.buildDetachedConnectorCommand(
|
||||
["connect", "telegram"],
|
||||
["-m", "ClineAdapterBot"],
|
||||
"/usr/local/bin/node",
|
||||
"./apps/cli/src/index.ts",
|
||||
entryPath,
|
||||
[],
|
||||
repoRoot,
|
||||
dirname(entryPath),
|
||||
{ CLINE_BUILD_ENV: "development" },
|
||||
),
|
||||
).toEqual({
|
||||
@@ -74,7 +72,7 @@ describe("spawnDetachedConnector", () => {
|
||||
childArgs: [
|
||||
"--inspect=127.0.0.1:0",
|
||||
"--enable-source-maps",
|
||||
resolve(repoRoot, "apps/cli/src/index.ts"),
|
||||
entryPath,
|
||||
"connect",
|
||||
"telegram",
|
||||
"-m",
|
||||
@@ -8,12 +8,12 @@ import {
|
||||
writeFileSync,
|
||||
} from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import type { HubSessionClient, HubSessionRow } from "@cline/core";
|
||||
import { ensureParentDir, resolveClineDataDir } from "@cline/core";
|
||||
import type { HubSessionClient, HubSessionRow } from "@cline/core/hub";
|
||||
import { withResolvedClineBuildEnv } from "@cline/shared";
|
||||
import { createCliLoggerAdapter } from "../logging/adapter";
|
||||
import { logSpawnedProcess } from "../logging/process";
|
||||
import { resolveCliLaunchSpec } from "../utils/internal-launch";
|
||||
import { ensureParentDir, resolveClineDataDir } from "@cline/shared/storage";
|
||||
import { resolveCliLaunchSpec } from "./internal-launch";
|
||||
import { createConnectorLogger } from "./logger";
|
||||
import type { ConnectIo } from "./types";
|
||||
|
||||
export function parseBooleanFlag(rawArgs: string[], flag: string): boolean {
|
||||
return rawArgs.includes(flag);
|
||||
@@ -154,6 +154,7 @@ export function spawnDetachedConnector(
|
||||
commandPrefixArgs: string[],
|
||||
rawArgs: string[],
|
||||
childEnvKey: string,
|
||||
io: ConnectIo,
|
||||
options?: {
|
||||
logPath?: string;
|
||||
component?: string;
|
||||
@@ -163,7 +164,7 @@ export function spawnDetachedConnector(
|
||||
const command = buildDetachedConnectorCommand(commandPrefixArgs, rawArgs);
|
||||
if (!command) {
|
||||
try {
|
||||
const logger = createCliLoggerAdapter({
|
||||
const logger = createConnectorLogger(io, {
|
||||
runtime: "cli",
|
||||
component: options?.component ?? "connectors",
|
||||
});
|
||||
@@ -198,24 +199,26 @@ export function spawnDetachedConnector(
|
||||
// processes otherwise allocate a new visible console.
|
||||
windowsHide: true,
|
||||
});
|
||||
logSpawnedProcess({
|
||||
createConnectorLogger(io, {
|
||||
runtime: "cli",
|
||||
component: options?.component ?? "connectors",
|
||||
command: [command.launcher, ...command.childArgs],
|
||||
}).core.log("Process spawned", {
|
||||
command: [command.launcher, ...command.childArgs].join(" "),
|
||||
commandArgs: command.childArgs,
|
||||
executable: command.launcher,
|
||||
childPid: child.pid ?? undefined,
|
||||
cwd: process.cwd(),
|
||||
detached: true,
|
||||
metadata: {
|
||||
childEnvKey,
|
||||
purpose: "connector.detached",
|
||||
logPath: options?.logPath,
|
||||
...options?.metadata,
|
||||
},
|
||||
childEnvKey,
|
||||
purpose: "connector.detached",
|
||||
logPath: options?.logPath,
|
||||
...options?.metadata,
|
||||
});
|
||||
child.unref();
|
||||
return child.pid ?? 0;
|
||||
} catch (error) {
|
||||
try {
|
||||
const logger = createCliLoggerAdapter({
|
||||
const logger = createConnectorLogger(io, {
|
||||
runtime: "cli",
|
||||
component: options?.component ?? "connectors",
|
||||
});
|
||||
+9
-7
@@ -1,14 +1,12 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
import type { UserInstructionConfigService } from "@cline/core";
|
||||
import type { HubSessionClient } from "@cline/core/hub";
|
||||
import type {
|
||||
ChatRunTurnRequest,
|
||||
ChatStartSessionRequest,
|
||||
HubSessionClient,
|
||||
UserInstructionConfigService,
|
||||
} from "@cline/core";
|
||||
} from "@cline/shared";
|
||||
import type { SentMessage, Thread } from "chat";
|
||||
import type { CliLoggerAdapter } from "../logging/adapter";
|
||||
import { buildUserInputMessage, resolveSystemPrompt } from "../runtime/prompt";
|
||||
import {
|
||||
type ChatCommandHost,
|
||||
type ChatCommandState,
|
||||
@@ -16,8 +14,9 @@ import {
|
||||
type MuteCommandInput,
|
||||
maybeHandleChatCommand,
|
||||
normalizeCommandName,
|
||||
} from "../utils/chat-commands";
|
||||
} from "./chat-commands";
|
||||
import { authorizeConnectorEvent, dispatchConnectorHook } from "./hooks";
|
||||
import { buildUserInputMessage, resolveSystemPrompt } from "./prompt";
|
||||
import {
|
||||
createConnectorRuntimeTurnStream,
|
||||
formatConnectorApprovalPrompt,
|
||||
@@ -44,6 +43,7 @@ import {
|
||||
setParticipantMuted,
|
||||
setThreadMuted,
|
||||
} from "./thread-bindings";
|
||||
import type { ConnectIo, ConnectorLoggerAdapter } from "./types";
|
||||
|
||||
export type ActiveConnectorTurn = {
|
||||
sessionId: string;
|
||||
@@ -211,7 +211,7 @@ export async function handleConnectorUserTurn<
|
||||
baseStartRequest: ChatStartSessionRequest;
|
||||
explicitSystemPrompt: string | undefined;
|
||||
clientId: string;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
transport: string;
|
||||
botUserName?: string;
|
||||
ownerParticipantKeys?: string[];
|
||||
@@ -225,6 +225,7 @@ export async function handleConnectorUserTurn<
|
||||
clientId: string,
|
||||
currentState: TState,
|
||||
) => Record<string, unknown>;
|
||||
resolveSessionMetadata?: ConnectIo["resolveSessionMetadata"];
|
||||
getScheduleDeliveryMetadata?: (
|
||||
thread: Thread<TState>,
|
||||
) => Record<string, unknown>;
|
||||
@@ -939,6 +940,7 @@ export async function handleConnectorUserTurn<
|
||||
input.clientId,
|
||||
currentState,
|
||||
),
|
||||
resolveSessionMetadata: input.resolveSessionMetadata,
|
||||
reusedLogMessage: input.reusedLogMessage,
|
||||
startedLogMessage: input.startedLogMessage,
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
ConnectorHookEvent,
|
||||
} from "@cline/shared";
|
||||
import { z } from "zod";
|
||||
import type { CliLoggerAdapter } from "../logging/adapter";
|
||||
import type { ConnectorLoggerAdapter } from "./types";
|
||||
|
||||
const ConnectorAuthorizationDecisionSchema = z.object({
|
||||
action: z.enum(["allow", "deny"]).default("allow"),
|
||||
@@ -17,7 +17,7 @@ const ConnectorAuthorizationDecisionSchema = z.object({
|
||||
export async function dispatchConnectorHook(
|
||||
command: string | undefined,
|
||||
hookPayload: ConnectorHookEvent,
|
||||
logger: CliLoggerAdapter,
|
||||
logger: ConnectorLoggerAdapter,
|
||||
): Promise<void> {
|
||||
const trimmed = command?.trim();
|
||||
if (!trimmed) {
|
||||
@@ -70,7 +70,7 @@ export async function authorizeConnectorEvent(
|
||||
botUserName?: string;
|
||||
request: ConnectorAuthorizationRequest;
|
||||
},
|
||||
logger: CliLoggerAdapter,
|
||||
logger: ConnectorLoggerAdapter,
|
||||
): Promise<ConnectorAuthorizationDecision> {
|
||||
const trimmed = command?.trim();
|
||||
if (!trimmed) {
|
||||
@@ -4,14 +4,14 @@ import {
|
||||
type HubEndpointOverrides,
|
||||
resolveDefaultHubHost,
|
||||
resolveDefaultHubPort,
|
||||
} from "@cline/core";
|
||||
} from "@cline/core/hub";
|
||||
|
||||
/**
|
||||
* Build a `host:port` rpc address string that respects the current build
|
||||
* environment. In development, this picks the dev hub port to avoid
|
||||
* colliding with a production Cline hub on the standard port.
|
||||
*/
|
||||
export function resolveDefaultCliRpcAddress(): string {
|
||||
export function resolveDefaultHubRpcAddress(): string {
|
||||
return `${resolveDefaultHubHost()}:${resolveDefaultHubPort()}`;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export function parseHubEndpointOverride(
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureCliHubServer(
|
||||
export async function ensureHubServer(
|
||||
workspaceRoot: string,
|
||||
endpoint: HubEndpointOverrides = {},
|
||||
): Promise<DetachedHubResolution> {
|
||||
@@ -0,0 +1,12 @@
|
||||
export * from "./catalog";
|
||||
export * from "./chat-commands";
|
||||
export { isProcessRunning } from "./common";
|
||||
export * from "./hub-runtime";
|
||||
export * from "./image-attachments";
|
||||
export * from "./internal-launch";
|
||||
export * from "./plugin-chat-commands";
|
||||
export * from "./prompt";
|
||||
export * from "./registry";
|
||||
export * from "./status";
|
||||
export * from "./types";
|
||||
export * from "./workspace";
|
||||
+10
-12
@@ -1,4 +1,4 @@
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
@@ -14,13 +14,12 @@ describe("internal launch helpers", () => {
|
||||
});
|
||||
|
||||
it("resolves the source entrypoint when running from TypeScript", () => {
|
||||
const utilsDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(utilsDir, "../../../../");
|
||||
const entryPath = fileURLToPath(import.meta.url);
|
||||
const spec = resolveCliLaunchSpec({
|
||||
execPath: "/Users/test/.bun/bin/bun",
|
||||
argv: ["bun", "./apps/cli/src/index.ts"],
|
||||
argv: ["bun", entryPath],
|
||||
execArgv: ["--conditions=development"],
|
||||
cwd: repoRoot,
|
||||
cwd: dirname(entryPath),
|
||||
env: {},
|
||||
});
|
||||
|
||||
@@ -30,9 +29,9 @@ describe("internal launch helpers", () => {
|
||||
"--inspect=127.0.0.1:0",
|
||||
"--enable-source-maps",
|
||||
"--conditions=development",
|
||||
resolve(repoRoot, "apps/cli/src/index.ts"),
|
||||
entryPath,
|
||||
],
|
||||
identityPath: resolve(repoRoot, "apps/cli/src/index.ts"),
|
||||
identityPath: entryPath,
|
||||
mode: "source",
|
||||
});
|
||||
});
|
||||
@@ -52,13 +51,12 @@ describe("internal launch helpers", () => {
|
||||
});
|
||||
|
||||
it("adds node debug flags for development node launches", () => {
|
||||
const utilsDir = dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = resolve(utilsDir, "../../../../");
|
||||
const entryPath = fileURLToPath(import.meta.url);
|
||||
const command = buildCliSubcommandCommand("hub", ["start"], {
|
||||
execPath: "/usr/local/bin/node",
|
||||
argv: ["node", "./apps/cli/src/index.ts"],
|
||||
argv: ["node", entryPath],
|
||||
execArgv: [],
|
||||
cwd: repoRoot,
|
||||
cwd: dirname(entryPath),
|
||||
env: { CLINE_BUILD_ENV: "development" },
|
||||
});
|
||||
|
||||
@@ -67,7 +65,7 @@ describe("internal launch helpers", () => {
|
||||
childArgs: [
|
||||
"--inspect=127.0.0.1:0",
|
||||
"--enable-source-maps",
|
||||
resolve(repoRoot, "apps/cli/src/index.ts"),
|
||||
entryPath,
|
||||
"hub",
|
||||
"start",
|
||||
],
|
||||
@@ -0,0 +1,39 @@
|
||||
import { join } from "node:path";
|
||||
import type { BasicLogger, RuntimeLoggerConfig } from "@cline/shared";
|
||||
import { noopBasicLogger } from "@cline/shared";
|
||||
import { resolveClineDataDir } from "@cline/shared/storage";
|
||||
import type {
|
||||
ConnectIo,
|
||||
ConnectorLoggerAdapter,
|
||||
CreateConnectorLoggerInput,
|
||||
} from "./types";
|
||||
|
||||
function normalizeRuntimeLoggerConfig(
|
||||
input: CreateConnectorLoggerInput,
|
||||
): Required<RuntimeLoggerConfig> {
|
||||
return {
|
||||
enabled: input.runtimeConfig?.enabled ?? false,
|
||||
level: input.runtimeConfig?.level ?? "info",
|
||||
destination:
|
||||
input.runtimeConfig?.destination ??
|
||||
join(resolveClineDataDir(), "logs", "cline.log"),
|
||||
name: input.runtimeConfig?.name ?? `cline.${input.runtime}`,
|
||||
bindings: input.runtimeConfig?.bindings ?? {},
|
||||
};
|
||||
}
|
||||
|
||||
export function createConnectorLogger(
|
||||
io: ConnectIo,
|
||||
input: CreateConnectorLoggerInput,
|
||||
): ConnectorLoggerAdapter {
|
||||
const hosted = io.createLogger?.(input);
|
||||
if (hosted) {
|
||||
return hosted;
|
||||
}
|
||||
const fallback: ConnectorLoggerAdapter = {
|
||||
core: noopBasicLogger as BasicLogger,
|
||||
runtimeConfig: normalizeRuntimeLoggerConfig(input),
|
||||
child: () => fallback,
|
||||
};
|
||||
return fallback;
|
||||
}
|
||||
+4
-3
@@ -1,11 +1,12 @@
|
||||
import { resolveAndLoadAgentPlugins } from "@cline/core";
|
||||
import {
|
||||
type AgentExtensionCommand,
|
||||
type AgentExtensionCommandResult,
|
||||
type AgentTool,
|
||||
type BasicLogger,
|
||||
createContributionRegistry,
|
||||
resolveAndLoadAgentPlugins,
|
||||
} from "@cline/core";
|
||||
import type { AgentTool, Message } from "@cline/shared";
|
||||
type Message,
|
||||
} from "@cline/shared";
|
||||
import {
|
||||
type ChatCommandDefinition,
|
||||
type ChatCommandHost,
|
||||
@@ -1,13 +1,10 @@
|
||||
import { statSync } from "node:fs";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, resolve } from "node:path";
|
||||
import {
|
||||
buildWorkspaceMetadata,
|
||||
mergeRulesForSystemPrompt,
|
||||
type UserInstructionConfigService,
|
||||
} from "@cline/core";
|
||||
import type { UserInstructionConfigService } from "@cline/core";
|
||||
import { buildWorkspaceMetadata, mergeRulesForSystemPrompt } from "@cline/core";
|
||||
import { type AgentMode, buildClineSystemPrompt } from "@cline/shared";
|
||||
import { isImagePath, loadImageAsDataUrl } from "../utils/image-attachments";
|
||||
import { isImagePath, loadImageAsDataUrl } from "./image-attachments";
|
||||
|
||||
const MODE_TAG_INSTRUCTIONS = `# Plan / Act Modes
|
||||
|
||||
+5
-5
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { CliLoggerAdapter } from "../logging/adapter";
|
||||
import { createConnectorRuntimeTurnStream } from "./runtime-turn";
|
||||
import type { ConnectorLoggerAdapter } from "./types";
|
||||
|
||||
type StreamHandlers = {
|
||||
onEvent: (event: {
|
||||
@@ -50,7 +50,7 @@ describe("createConnectorRuntimeTurnStream", () => {
|
||||
sessionId: "session-1",
|
||||
request,
|
||||
clientId: "client-1",
|
||||
logger: { core: {} } as unknown as CliLoggerAdapter,
|
||||
logger: { core: {} } as unknown as ConnectorLoggerAdapter,
|
||||
transport: "telegram",
|
||||
conversationId: "thread-1",
|
||||
onToolStatus: async (message) => {
|
||||
@@ -82,7 +82,7 @@ describe("createConnectorRuntimeTurnStream", () => {
|
||||
sessionId: "session-1",
|
||||
request: { config: {} as never, prompt: "hi" },
|
||||
clientId: "client-1",
|
||||
logger: { core: { log } } as unknown as CliLoggerAdapter,
|
||||
logger: { core: { log } } as unknown as ConnectorLoggerAdapter,
|
||||
transport: "discord",
|
||||
conversationId: "thread-1",
|
||||
})) {
|
||||
@@ -132,7 +132,7 @@ describe("createConnectorRuntimeTurnStream", () => {
|
||||
sessionId: "session-1",
|
||||
request: { config: {} as never, prompt: "hi" },
|
||||
clientId: "client-1",
|
||||
logger: { core: {} } as unknown as CliLoggerAdapter,
|
||||
logger: { core: {} } as unknown as ConnectorLoggerAdapter,
|
||||
transport: "telegram",
|
||||
conversationId: "thread-1",
|
||||
onFailed: async (error) => {
|
||||
@@ -179,7 +179,7 @@ describe("createConnectorRuntimeTurnStream", () => {
|
||||
sessionId: "session-1",
|
||||
request: { config: {} as never, prompt: "hi" },
|
||||
clientId: "client-1",
|
||||
logger: { core: {} } as unknown as CliLoggerAdapter,
|
||||
logger: { core: {} } as unknown as ConnectorLoggerAdapter,
|
||||
transport: "telegram",
|
||||
conversationId: "thread-1",
|
||||
onFailed: async (error) => {
|
||||
+4
-3
@@ -1,5 +1,6 @@
|
||||
import type { ChatRunTurnRequest, HubSessionClient } from "@cline/core";
|
||||
import type { CliLoggerAdapter } from "../logging/adapter";
|
||||
import type { HubSessionClient } from "@cline/core/hub";
|
||||
import type { ChatRunTurnRequest } from "@cline/shared";
|
||||
import type { ConnectorLoggerAdapter } from "./types";
|
||||
|
||||
export type PendingConnectorApproval = {
|
||||
approvalId: string;
|
||||
@@ -136,7 +137,7 @@ export function createConnectorRuntimeTurnStream(input: {
|
||||
sessionId: string;
|
||||
request: ChatRunTurnRequest;
|
||||
clientId: string;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
transport: string;
|
||||
conversationId: string;
|
||||
onToolStatus?: (message: string) => Promise<void>;
|
||||
+16
-36
@@ -1,17 +1,15 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const {
|
||||
mockGetLastUsedProviderSettings,
|
||||
mockGetProviderSettings,
|
||||
mockResolveSystemPrompt,
|
||||
mockGetProviderCollection,
|
||||
mockGetBooleanFlagEnabled,
|
||||
} = vi.hoisted(() => ({
|
||||
mockGetLastUsedProviderSettings: vi.fn(),
|
||||
mockGetProviderSettings: vi.fn(),
|
||||
mockResolveSystemPrompt: vi.fn(),
|
||||
mockGetProviderCollection: vi.fn(),
|
||||
mockGetBooleanFlagEnabled: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@cline/core", async () => {
|
||||
@@ -28,47 +26,29 @@ vi.mock("@cline/core", async () => {
|
||||
return mockGetProviderSettings(providerId);
|
||||
}
|
||||
},
|
||||
CoreSessionService: class {},
|
||||
SqliteSessionStore: class {},
|
||||
Llms: {
|
||||
...actual.Llms,
|
||||
getProviderCollection: mockGetProviderCollection,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("../runtime/prompt", () => ({
|
||||
vi.mock("@cline/llms", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@cline/llms")>("@cline/llms");
|
||||
return {
|
||||
...actual,
|
||||
getProviderCollection: mockGetProviderCollection,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./prompt", () => ({
|
||||
resolveSystemPrompt: mockResolveSystemPrompt,
|
||||
}));
|
||||
|
||||
vi.mock("../utils/helpers", () => ({
|
||||
vi.mock("./workspace", () => ({
|
||||
resolveWorkspaceRoot: vi.fn((cwd: string) => cwd),
|
||||
}));
|
||||
|
||||
vi.mock("../utils/feature-flags", () => ({
|
||||
getCliFeatureFlagsService: () => ({
|
||||
getBooleanFlagEnabled: mockGetBooleanFlagEnabled,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("../commands/auth", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("../commands/auth")>(
|
||||
"../commands/auth",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
ensureOAuthProviderApiKey: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
import { buildConnectorStartRequest } from "./session-runtime";
|
||||
|
||||
describe("buildConnectorStartRequest", () => {
|
||||
beforeEach(() => {
|
||||
mockGetBooleanFlagEnabled.mockReturnValue(false);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
delete process.env.OPENROUTER_API_KEY;
|
||||
@@ -125,12 +105,12 @@ describe("buildConnectorStartRequest", () => {
|
||||
io: { writeln: vi.fn(), writeErr: vi.fn() },
|
||||
loggerConfig: { enabled: false, level: "info", destination: "stdout" },
|
||||
systemRules: "Rules",
|
||||
defaultModel: "cline-pass/glm-5.1",
|
||||
defaultModel: "cline-pass/glm-5.2",
|
||||
});
|
||||
|
||||
expect(request.provider).toBe("cline-pass");
|
||||
expect(request.apiKey).toBe("workos:resolved-token");
|
||||
expect(request.model).toBe("cline-pass/glm-5.1");
|
||||
expect(request.model).toBe("cline-pass/glm-5.2");
|
||||
});
|
||||
|
||||
it("uses auth material resolved by provider settings manager", async () => {
|
||||
@@ -153,11 +133,11 @@ describe("buildConnectorStartRequest", () => {
|
||||
io: { writeln: vi.fn(), writeErr: vi.fn() },
|
||||
loggerConfig: { enabled: false, level: "info", destination: "stdout" },
|
||||
systemRules: "Rules",
|
||||
defaultModel: "cline-pass/glm-5.1",
|
||||
defaultModel: "cline-pass/glm-5.2",
|
||||
});
|
||||
|
||||
expect(request.provider).toBe("cline-pass");
|
||||
expect(request.apiKey).toBe("workos:resolved-token");
|
||||
expect(request.model).toBe("cline-pass/glm-5.1");
|
||||
expect(request.model).toBe("cline-pass/glm-5.2");
|
||||
});
|
||||
});
|
||||
+24
-21
@@ -1,22 +1,18 @@
|
||||
import type { ChatStartSessionRequest, RuntimeLoggerConfig } from "@cline/core";
|
||||
import {
|
||||
CoreSessionService,
|
||||
HubSessionClient,
|
||||
Llms,
|
||||
ProviderSettingsManager,
|
||||
SqliteSessionStore,
|
||||
} from "@cline/core";
|
||||
import type { Thread } from "chat";
|
||||
import {
|
||||
ensureOAuthProviderApiKey,
|
||||
getPersistedProviderApiKey,
|
||||
isOAuthProvider,
|
||||
normalizeProviderId,
|
||||
} from "../commands/auth";
|
||||
import type { CliLoggerAdapter } from "../logging/adapter";
|
||||
import { resolveSystemPrompt } from "../runtime/prompt";
|
||||
import { resolveCliSessionMetadata } from "../utils/enterprise";
|
||||
import { resolveWorkspaceRoot } from "../utils/helpers";
|
||||
ProviderSettingsManager,
|
||||
SqliteSessionStore,
|
||||
} from "@cline/core";
|
||||
import { HubSessionClient } from "@cline/core/hub";
|
||||
import * as Llms from "@cline/llms";
|
||||
import type {
|
||||
ChatStartSessionRequest,
|
||||
RuntimeLoggerConfig,
|
||||
} from "@cline/shared";
|
||||
import type { Thread } from "chat";
|
||||
import {
|
||||
parseLocalRowMetadata,
|
||||
parseRowMetadata,
|
||||
@@ -24,12 +20,14 @@ import {
|
||||
readSessionReplyText,
|
||||
} from "./common";
|
||||
import { dispatchConnectorHook } from "./hooks";
|
||||
import { resolveSystemPrompt } from "./prompt";
|
||||
import {
|
||||
type ConnectorThreadState,
|
||||
loadThreadState,
|
||||
persistMergedThreadState,
|
||||
} from "./thread-bindings";
|
||||
import type { ConnectIo } from "./types";
|
||||
import type { ConnectIo, ConnectorLoggerAdapter } from "./types";
|
||||
import { resolveWorkspaceRoot } from "./workspace";
|
||||
|
||||
async function resolveProviderApiKeyFromEnv(
|
||||
provider: string,
|
||||
@@ -83,12 +81,16 @@ export async function buildConnectorStartRequest(input: {
|
||||
"";
|
||||
|
||||
if (!apiKey && isOAuthProvider(provider)) {
|
||||
const oauthResult = await ensureOAuthProviderApiKey({
|
||||
if (!input.io.ensureProviderApiKey) {
|
||||
throw new Error(
|
||||
`Connector host cannot authenticate OAuth provider "${provider}"`,
|
||||
);
|
||||
}
|
||||
const oauthResult = await input.io.ensureProviderApiKey({
|
||||
providerId: provider,
|
||||
currentApiKey: apiKey,
|
||||
existingSettings: selectedProviderSettings,
|
||||
providerSettingsManager,
|
||||
io: input.io,
|
||||
});
|
||||
selectedProviderSettings = oauthResult.selectedProviderSettings;
|
||||
apiKey = oauthResult.apiKey ?? "";
|
||||
@@ -143,7 +145,7 @@ export async function getOrCreateSessionId<
|
||||
thread: Thread<TState>;
|
||||
client: HubSessionClient;
|
||||
startRequest: ChatStartSessionRequest;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
clientId: string;
|
||||
transport: string;
|
||||
bindingsPath: string;
|
||||
@@ -151,6 +153,7 @@ export async function getOrCreateSessionId<
|
||||
hookCommand?: string;
|
||||
hookBotUserName?: string;
|
||||
sessionMetadata: Record<string, unknown>;
|
||||
resolveSessionMetadata?: ConnectIo["resolveSessionMetadata"];
|
||||
reusedLogMessage: string;
|
||||
startedLogMessage?: string;
|
||||
}): Promise<string> {
|
||||
@@ -219,9 +222,9 @@ export async function getOrCreateSessionId<
|
||||
if (!sessionId) {
|
||||
throw new Error("runtime start returned an empty session id");
|
||||
}
|
||||
const remoteConfigMetadata = await resolveCliSessionMetadata(sessionId).catch(
|
||||
() => undefined,
|
||||
);
|
||||
const remoteConfigMetadata = await input
|
||||
.resolveSessionMetadata?.(sessionId)
|
||||
.catch(() => undefined);
|
||||
|
||||
await input.client
|
||||
.updateSession({
|
||||
@@ -1,6 +1,6 @@
|
||||
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { resolveClineDataDir } from "@cline/core";
|
||||
import { resolveClineDataDir } from "@cline/shared/storage";
|
||||
|
||||
function isProcessRunning(pid: number): boolean {
|
||||
if (!Number.isInteger(pid) || pid <= 0) {
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import { ensureParentDir } from "@cline/core";
|
||||
import { ensureParentDir } from "@cline/shared/storage";
|
||||
import type { Lock, QueueEntry, StateAdapter } from "chat";
|
||||
|
||||
type PersistedStateSnapshot = {
|
||||
+3
-3
@@ -1,13 +1,13 @@
|
||||
import type { HubSessionClient } from "@cline/core";
|
||||
import type { HubSessionClient } from "@cline/core/hub";
|
||||
import type { TeamProgressProjectionEvent } from "@cline/shared";
|
||||
import type { Chat, Thread } from "chat";
|
||||
import type { CliLoggerAdapter } from "../logging/adapter";
|
||||
import { truncateConnectorText } from "./runtime-turn";
|
||||
import {
|
||||
type ConnectorThreadBinding,
|
||||
type ConnectorThreadState,
|
||||
readBindings,
|
||||
} from "./thread-bindings";
|
||||
import type { ConnectorLoggerAdapter } from "./types";
|
||||
|
||||
function formatCountLabel(input: {
|
||||
count: number;
|
||||
@@ -139,7 +139,7 @@ export function startConnectorTaskUpdateRelay<
|
||||
client: HubSessionClient;
|
||||
clientId: string;
|
||||
bot: Chat;
|
||||
logger: CliLoggerAdapter;
|
||||
logger: ConnectorLoggerAdapter;
|
||||
bindingsPath: string;
|
||||
transport: string;
|
||||
postToThread?: (input: {
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { ProviderSettings, ProviderSettingsManager } from "@cline/core";
|
||||
import type { BasicLogger, RuntimeLoggerConfig } from "@cline/shared";
|
||||
|
||||
export type ConnectorLoggerAdapter = {
|
||||
readonly core: BasicLogger;
|
||||
readonly runtimeConfig: RuntimeLoggerConfig;
|
||||
child(bindings: Record<string, unknown>): ConnectorLoggerAdapter;
|
||||
};
|
||||
|
||||
export type CreateConnectorLoggerInput = {
|
||||
runtime: "cli" | "rpc-runtime";
|
||||
component?: string;
|
||||
runtimeConfig?: RuntimeLoggerConfig;
|
||||
};
|
||||
|
||||
export type ConnectIo = {
|
||||
writeln: (text?: string) => void;
|
||||
writeErr: (text: string) => void;
|
||||
createLogger?: (input: CreateConnectorLoggerInput) => ConnectorLoggerAdapter;
|
||||
resolveSessionMetadata?: (
|
||||
sessionId: string,
|
||||
) => Promise<Record<string, unknown> | undefined>;
|
||||
ensureProviderApiKey?: (input: {
|
||||
providerId: string;
|
||||
currentApiKey?: string;
|
||||
existingSettings?: ProviderSettings;
|
||||
providerSettingsManager: ProviderSettingsManager;
|
||||
}) => Promise<{
|
||||
apiKey?: string;
|
||||
selectedProviderSettings?: ProviderSettings;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ConnectStopResult = {
|
||||
stoppedProcesses: number;
|
||||
stoppedSessions: number;
|
||||
};
|
||||
|
||||
export interface ConnectCommandDefinition {
|
||||
name: string;
|
||||
description: string;
|
||||
run(args: string[], io: ConnectIo): Promise<number>;
|
||||
showHelp(io: ConnectIo): void;
|
||||
stopAll?(io: ConnectIo): Promise<ConnectStopResult>;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { spawnSync } from "node:child_process";
|
||||
|
||||
export function resolveWorkspaceRoot(cwd: string): string {
|
||||
const result = spawnSync("git", ["-C", cwd, "rev-parse", "--show-toplevel"], {
|
||||
encoding: "utf8",
|
||||
});
|
||||
if (result.status === 0) {
|
||||
const value = result.stdout.trim();
|
||||
if (value) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
return cwd;
|
||||
}
|
||||
@@ -2,9 +2,11 @@ import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { basename } from "node:path";
|
||||
import process from "node:process";
|
||||
import {
|
||||
listActiveConnectors,
|
||||
listConnectorCatalog,
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import { withResolvedClineBuildEnv } from "@cline/shared";
|
||||
import { listConnectorCatalog } from "../../../cli/src/connectors/catalog";
|
||||
import { listActiveConnectors } from "../../../cli/src/connectors/status";
|
||||
import {
|
||||
PLATFORMS,
|
||||
shouldIncludeField,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { listActiveConnectors } from "../../../cli/src/connectors/status";
|
||||
import { listActiveConnectors } from "@cline/cline-hub/connectors";
|
||||
import type { WebviewHubState } from "../webview-protocol";
|
||||
import {
|
||||
clientSummariesPayload,
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"clean": "rm -rf webview/.next webview/out node_modules dist && (cd src-tauri && rm -rf target node_modules dist)"
|
||||
},
|
||||
"dependencies": {
|
||||
"@cline/cline-hub": "workspace:*",
|
||||
"@base-ui/react": "^1.2.0",
|
||||
"@cline/core": "workspace:*",
|
||||
"@cline/llms": "workspace:*",
|
||||
|
||||
@@ -2,9 +2,11 @@ import { spawn } from "node:child_process";
|
||||
import { existsSync } from "node:fs";
|
||||
import { basename, join, normalize } from "node:path";
|
||||
import process from "node:process";
|
||||
import {
|
||||
listActiveConnectors,
|
||||
listConnectorCatalog,
|
||||
} from "@cline/cline-hub/connectors";
|
||||
import { withResolvedClineBuildEnv } from "@cline/shared";
|
||||
import { listConnectorCatalog } from "../../../cli/src/connectors/catalog";
|
||||
import { listActiveConnectors } from "../../../cli/src/connectors/status";
|
||||
import {
|
||||
PLATFORMS,
|
||||
shouldIncludeField,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user