mirror of
https://github.com/cline/cline.git
synced 2026-09-01 23:19:18 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8c4ec94c6b | |||
| dcdd79bea5 | |||
| 5214ec0f72 | |||
| 3ad5eb7e2d | |||
| de613c5312 |
@@ -1,14 +1,6 @@
|
||||
import { type ChildProcess, spawn } from "node:child_process";
|
||||
import { realpathSync } from "node:fs";
|
||||
import {
|
||||
clearHubDiscovery,
|
||||
probeHubServer,
|
||||
readHubDiscovery,
|
||||
resolveSharedHubOwnerContext,
|
||||
stopLocalHubServerGracefully,
|
||||
} from "@cline/core";
|
||||
import { version } from "../../package.json";
|
||||
import { ensureCliHubServer } from "../utils/hub-runtime";
|
||||
import { c, writeErr, writeln } from "../utils/output";
|
||||
import {
|
||||
getInstalledKanbanVersion,
|
||||
@@ -235,6 +227,7 @@ async function waitForHubToStop(
|
||||
url: string,
|
||||
timeoutMs: number,
|
||||
): Promise<boolean> {
|
||||
const { probeHubServer } = await import("@cline/core");
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const check = await probeHubServer(url).catch(() => undefined);
|
||||
@@ -250,6 +243,14 @@ async function waitForHubToStop(
|
||||
* clears stale discovery, then re-ensures a fresh instance is spawned.
|
||||
*/
|
||||
async function restartHubServerIfRunning(): Promise<void> {
|
||||
const {
|
||||
clearHubDiscovery,
|
||||
probeHubServer,
|
||||
readHubDiscovery,
|
||||
resolveSharedHubOwnerContext,
|
||||
stopLocalHubServerGracefully,
|
||||
} = await import("@cline/core");
|
||||
const { ensureCliHubServer } = await import("../utils/hub-runtime");
|
||||
const owner = resolveSharedHubOwnerContext();
|
||||
const discovery = await readHubDiscovery(owner.discoveryPath).catch(
|
||||
() => undefined,
|
||||
|
||||
@@ -28,23 +28,23 @@ if (!isMainThread) {
|
||||
};
|
||||
process.on("SIGINT", forwardSignalToRuntime);
|
||||
process.on("SIGTERM", forwardSignalToRuntime);
|
||||
const handleFatalProcessError = (kind: string, error: unknown) => {
|
||||
const handleFatalProcessError = async (kind: string, error: unknown) => {
|
||||
if (handlingFatalProcessError) {
|
||||
process.exit(1);
|
||||
}
|
||||
handlingFatalProcessError = true;
|
||||
logCliProcessError(kind, error);
|
||||
writeErr(
|
||||
error instanceof Error ? (error.stack ?? error.message) : String(error),
|
||||
);
|
||||
cleanupActiveRuntime();
|
||||
abortActiveRuntime();
|
||||
void disposeAll().finally(() => {
|
||||
await logCliProcessError(kind, error);
|
||||
await disposeAll().finally(() => {
|
||||
process.exit(1);
|
||||
});
|
||||
};
|
||||
process.on("uncaughtException", (error) => {
|
||||
handleFatalProcessError("uncaughtException", error);
|
||||
void handleFatalProcessError("uncaughtException", error);
|
||||
});
|
||||
process.on("unhandledRejection", (reason, promise) => {
|
||||
if (isAbortInProgress()) {
|
||||
@@ -53,7 +53,7 @@ if (!isMainThread) {
|
||||
promise.catch(() => {});
|
||||
return;
|
||||
}
|
||||
handleFatalProcessError("unhandledRejection", reason);
|
||||
void handleFatalProcessError("unhandledRejection", reason);
|
||||
});
|
||||
|
||||
void (async () => {
|
||||
@@ -67,10 +67,10 @@ if (!isMainThread) {
|
||||
const { runCli } = await import("./main");
|
||||
await runCli();
|
||||
} catch (err) {
|
||||
logCliProcessError("runCli", err);
|
||||
writeErr(err instanceof Error ? err.message : String(err));
|
||||
cleanupActiveRuntime();
|
||||
abortActiveRuntime();
|
||||
await logCliProcessError("runCli", err);
|
||||
exitCode = 1;
|
||||
} finally {
|
||||
await disposeAll();
|
||||
|
||||
@@ -116,7 +116,7 @@ describe("createCliLoggerAdapter", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("writes process-level errors to the CLI log", () => {
|
||||
it("writes process-level errors to the CLI log", async () => {
|
||||
const snapshot = withEnvSnapshot();
|
||||
const dataDir = mkdtempSync(join(tmpdir(), `${commandName}-process-log-`));
|
||||
process.env.CLINE_DATA_DIR = dataDir;
|
||||
@@ -126,7 +126,7 @@ describe("createCliLoggerAdapter", () => {
|
||||
delete process.env.CLINE_LOG_ENABLED;
|
||||
|
||||
try {
|
||||
logCliProcessError(
|
||||
await logCliProcessError(
|
||||
"unhandledRejection",
|
||||
new Error("message schema validation failed"),
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import type { BasicLogger } from "@cline/core";
|
||||
import { createCliLoggerAdapter, flushCliLoggerAdapters } from "./adapter";
|
||||
|
||||
export function logCliError(
|
||||
logger: BasicLogger | undefined,
|
||||
@@ -16,8 +15,14 @@ export function logCliError(
|
||||
});
|
||||
}
|
||||
|
||||
export function logCliProcessError(kind: string, error: unknown): void {
|
||||
export async function logCliProcessError(
|
||||
kind: string,
|
||||
error: unknown,
|
||||
): Promise<void> {
|
||||
try {
|
||||
const { createCliLoggerAdapter, flushCliLoggerAdapters } = await import(
|
||||
"./adapter"
|
||||
);
|
||||
const logger = createCliLoggerAdapter({
|
||||
runtime: "cli",
|
||||
component: "process",
|
||||
|
||||
@@ -444,7 +444,7 @@ describe("runCli lightweight command dispatch", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("loads live catalog models for default interactive model selection", async () => {
|
||||
it("uses bundled catalog models for default interactive startup", async () => {
|
||||
llmMocks.resolveProviderConfig.mockResolvedValue({
|
||||
knownModels: {
|
||||
"live-only-model": {
|
||||
@@ -461,8 +461,8 @@ describe("runCli lightweight command dispatch", () => {
|
||||
expect(llmMocks.resolveProviderConfig).toHaveBeenCalledWith(
|
||||
"cline",
|
||||
{
|
||||
loadLatestOnInit: true,
|
||||
loadPrivateOnAuth: true,
|
||||
loadLatestOnInit: false,
|
||||
loadPrivateOnAuth: false,
|
||||
failOnError: false,
|
||||
},
|
||||
undefined,
|
||||
@@ -490,8 +490,8 @@ describe("runCli lightweight command dispatch", () => {
|
||||
expect(llmMocks.resolveProviderConfig).toHaveBeenCalledWith(
|
||||
"cline",
|
||||
{
|
||||
loadLatestOnInit: true,
|
||||
loadPrivateOnAuth: true,
|
||||
loadLatestOnInit: false,
|
||||
loadPrivateOnAuth: false,
|
||||
failOnError: false,
|
||||
},
|
||||
undefined,
|
||||
|
||||
+24
-17
@@ -10,10 +10,6 @@ import {
|
||||
commanderToParsedArgs,
|
||||
createProgram,
|
||||
} from "./commands/program";
|
||||
import {
|
||||
autoUpdateOnStartup,
|
||||
getPreferredKanbanInstaller,
|
||||
} from "./commands/update";
|
||||
import { CLI_DEFAULT_CHECKPOINT_CONFIG } from "./runtime/defaults";
|
||||
import {
|
||||
buildCliCompactionConfig,
|
||||
@@ -38,11 +34,7 @@ import {
|
||||
normalizeProviderId,
|
||||
} from "./utils/provider-auth";
|
||||
import { rewriteTeamPrompt, TEAM_COMMAND_USAGE } from "./utils/team-command";
|
||||
import { captureCliExtensionActivated, getCliTelemetryService } from "./utils/telemetry";
|
||||
import type { Config } from "./utils/types";
|
||||
import { runConnectWizard } from "./wizards/connect";
|
||||
import { runMcpWizard } from "./wizards/mcp";
|
||||
import { runScheduleWizard } from "./wizards/schedule";
|
||||
|
||||
export function stdinHasPipedInput(): boolean {
|
||||
if (process.stdin.isTTY) return false;
|
||||
@@ -106,7 +98,12 @@ export function resolveConfigDirArg(argv: string[]): string | undefined {
|
||||
|
||||
export async function runCli(): Promise<void> {
|
||||
installStreamErrorGuards();
|
||||
autoUpdateOnStartup();
|
||||
const updateModulePromise = import("./commands/update");
|
||||
void updateModulePromise
|
||||
.then(({ autoUpdateOnStartup }) => {
|
||||
autoUpdateOnStartup();
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
const cliArgs = process.argv.slice(2);
|
||||
const configDir = resolveConfigDirArg(cliArgs);
|
||||
@@ -120,6 +117,7 @@ export async function runCli(): Promise<void> {
|
||||
// has been applied, so the telemetry singleton's persisted distinct-id
|
||||
// (and any other storage it touches) lands under the user-selected
|
||||
// `--config <dir>` rather than the default home/config location.
|
||||
const { captureCliExtensionActivated } = await import("./utils/telemetry");
|
||||
captureCliExtensionActivated();
|
||||
|
||||
let launchConfigView = false;
|
||||
@@ -310,6 +308,7 @@ export async function runCli(): Promise<void> {
|
||||
io,
|
||||
);
|
||||
} else if (process.stdin.isTTY && process.stdout.isTTY) {
|
||||
const { runConnectWizard } = await import("./wizards/connect");
|
||||
ctx.exitCode = await runConnectWizard();
|
||||
} else {
|
||||
writeln(`\nAdapters:\n${formatAdapterList()}`);
|
||||
@@ -322,6 +321,7 @@ export async function runCli(): Promise<void> {
|
||||
.description("Manage MCP servers")
|
||||
.action(async () => {
|
||||
if (process.stdin.isTTY && process.stdout.isTTY) {
|
||||
const { runMcpWizard } = await import("./wizards/mcp");
|
||||
ctx.exitCode = await runMcpWizard();
|
||||
} else {
|
||||
writeln(
|
||||
@@ -489,6 +489,7 @@ export async function runCli(): Promise<void> {
|
||||
process.stdin.isTTY &&
|
||||
process.stdout.isTTY
|
||||
) {
|
||||
const { runScheduleWizard } = await import("./wizards/schedule");
|
||||
ctx.exitCode = await runScheduleWizard();
|
||||
return;
|
||||
}
|
||||
@@ -534,6 +535,7 @@ export async function runCli(): Promise<void> {
|
||||
.description("Run the kanban app")
|
||||
.action(async () => {
|
||||
const { launchKanban } = await import("./commands/kanban");
|
||||
const { getPreferredKanbanInstaller } = await updateModulePromise;
|
||||
ctx.exitCode = await launchKanban({
|
||||
preferredInstaller: getPreferredKanbanInstaller(),
|
||||
});
|
||||
@@ -587,6 +589,7 @@ export async function runCli(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
const { launchKanban } = await import("./commands/kanban");
|
||||
const { getPreferredKanbanInstaller } = await updateModulePromise;
|
||||
process.exitCode = await launchKanban({
|
||||
preferredInstaller: getPreferredKanbanInstaller(),
|
||||
});
|
||||
@@ -771,6 +774,13 @@ export async function runCli(): Promise<void> {
|
||||
apiKey = oauthResult?.apiKey ?? apiKey;
|
||||
}
|
||||
|
||||
const systemPromptPromise = resolveSystemPrompt({
|
||||
cwd,
|
||||
explicitSystemPrompt: args.systemPrompt,
|
||||
providerId: provider,
|
||||
mode: args.mode ?? "act",
|
||||
});
|
||||
systemPromptPromise.catch(() => {});
|
||||
let knownModels: Config["knownModels"];
|
||||
try {
|
||||
const persistedProviderConfig = providerSettingsManager.getProviderConfig(
|
||||
@@ -781,8 +791,8 @@ export async function runCli(): Promise<void> {
|
||||
);
|
||||
const catalogOptions = isInteractive
|
||||
? {
|
||||
loadLatestOnInit: true,
|
||||
loadPrivateOnAuth: true,
|
||||
loadLatestOnInit: false,
|
||||
loadPrivateOnAuth: false,
|
||||
failOnError: false,
|
||||
}
|
||||
: undefined;
|
||||
@@ -823,6 +833,8 @@ export async function runCli(): Promise<void> {
|
||||
cwd,
|
||||
});
|
||||
|
||||
const { getCliTelemetryService } = await import("./utils/telemetry");
|
||||
|
||||
const config: Config = {
|
||||
providerId: provider,
|
||||
modelId:
|
||||
@@ -832,12 +844,7 @@ export async function runCli(): Promise<void> {
|
||||
"anthropic/claude-sonnet-4.6",
|
||||
apiKey: apiKey ?? "",
|
||||
knownModels,
|
||||
systemPrompt: await resolveSystemPrompt({
|
||||
cwd,
|
||||
explicitSystemPrompt: args.systemPrompt,
|
||||
providerId: provider,
|
||||
mode: args.mode ?? "act",
|
||||
}),
|
||||
systemPrompt: await systemPromptPromise,
|
||||
execution: {
|
||||
maxConsecutiveMistakes: args.retries ?? 3,
|
||||
},
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
writeln,
|
||||
} from "../utils/output";
|
||||
import { createWorkspaceChatCommandHost } from "../utils/plugin-chat-commands";
|
||||
import { readRepoStatus } from "../utils/repo-status";
|
||||
import type { Config } from "../utils/types";
|
||||
import {
|
||||
clearAbortInProgress,
|
||||
@@ -63,7 +62,6 @@ export async function runInteractive(
|
||||
): Promise<void> {
|
||||
assertInteractivePreflight(config);
|
||||
|
||||
const initialRepoStatus = await readRepoStatus(config.cwd);
|
||||
const workflowSlashCommands = listInteractiveSlashCommands(
|
||||
userInstructionService,
|
||||
);
|
||||
@@ -338,7 +336,6 @@ export async function runInteractive(
|
||||
initialNotice: options?.initialNotice,
|
||||
onInitialNoticeShown: options?.onInitialNoticeShown,
|
||||
loadDeferredInitialMessages,
|
||||
initialRepoStatus,
|
||||
workflowSlashCommands,
|
||||
loadAdditionalSlashCommands,
|
||||
loadWelcomeLine: async () =>
|
||||
|
||||
@@ -218,23 +218,20 @@ export function SessionProvider(props: {
|
||||
setLastTotalCost(0);
|
||||
}, []);
|
||||
|
||||
const addUsageDelta = useCallback(
|
||||
(usage: UsageDelta) => {
|
||||
const nextTotalTokens = nextUsageTokenDisplay(0, usage);
|
||||
if (nextTotalTokens > 0) {
|
||||
setLastTotalTokens((prev) => nextUsageTokenDisplay(prev, usage));
|
||||
}
|
||||
const costDelta = usage.cost;
|
||||
if (
|
||||
typeof costDelta === "number" &&
|
||||
Number.isFinite(costDelta) &&
|
||||
costDelta > 0
|
||||
) {
|
||||
setLastTotalCost((prev) => prev + costDelta);
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
const addUsageDelta = useCallback((usage: UsageDelta) => {
|
||||
const nextTotalTokens = nextUsageTokenDisplay(0, usage);
|
||||
if (nextTotalTokens > 0) {
|
||||
setLastTotalTokens((prev) => nextUsageTokenDisplay(prev, usage));
|
||||
}
|
||||
const costDelta = usage.cost;
|
||||
if (
|
||||
typeof costDelta === "number" &&
|
||||
Number.isFinite(costDelta) &&
|
||||
costDelta > 0
|
||||
) {
|
||||
setLastTotalCost((prev) => prev + costDelta);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const replaceEntries = useCallback((nextEntries: ChatEntry[]) => {
|
||||
setEntries(
|
||||
|
||||
@@ -125,6 +125,10 @@ function App(props: TuiProps) {
|
||||
.catch(() => {});
|
||||
}, [props.config.cwd]);
|
||||
|
||||
useEffect(() => {
|
||||
refreshRepoStatus();
|
||||
}, [refreshRepoStatus]);
|
||||
|
||||
const refocusTextareaRef = useRef<() => void>(() => {});
|
||||
const populateInputRef = useRef<(value: string) => void>(() => {});
|
||||
const insertSkillCommandRef = useRef<
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Llms, type ProviderSettings } from "@cline/core";
|
||||
import type { ProviderSettings } from "@cline/core";
|
||||
import { normalizeProviderId as normalizeLlmsProviderId } from "@cline/llms";
|
||||
import { isOAuthProviderId } from "@cline/shared";
|
||||
|
||||
export type OAuthCredentials = {
|
||||
@@ -11,7 +12,7 @@ export type OAuthCredentials = {
|
||||
};
|
||||
|
||||
export function normalizeProviderId(providerId: string): string {
|
||||
return Llms.normalizeProviderId(providerId.trim());
|
||||
return normalizeLlmsProviderId(providerId.trim());
|
||||
}
|
||||
|
||||
export function normalizeAuthProviderId(providerId: string): string {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { createTeamName } from "@cline/core";
|
||||
import { formatUserCommandBlock } from "@cline/shared";
|
||||
import type { Config } from "./types";
|
||||
|
||||
@@ -33,5 +32,6 @@ export async function enableTeamsForPrompt(config: Config): Promise<void> {
|
||||
return;
|
||||
}
|
||||
config.enableAgentTeams = true;
|
||||
const { createTeamName } = await import("@cline/core");
|
||||
config.teamName = config.teamName?.trim() || createTeamName();
|
||||
}
|
||||
|
||||
@@ -7,20 +7,15 @@ const hoisted = vi.hoisted(() => ({
|
||||
getCliTelemetryService: vi.fn(() => undefined),
|
||||
}));
|
||||
|
||||
vi.mock("@cline/core", () => ({
|
||||
vi.mock("@cline/core/telemetry", () => ({
|
||||
captureExtensionActivated: hoisted.captureExtensionActivated,
|
||||
identifyAccount: hoisted.identifyAccount,
|
||||
// CLI telemetry singleton path normally pulls in
|
||||
// `createConfiguredTelemetryHandle` and `createClineTelemetryServiceConfig`;
|
||||
// stub them so the test never spins up a real OpenTelemetry provider.
|
||||
createClineTelemetryServiceConfig: vi.fn(() => ({})),
|
||||
createConfiguredTelemetryHandle: vi.fn(() => ({
|
||||
telemetry: undefined,
|
||||
provider: undefined,
|
||||
flush: vi.fn(),
|
||||
dispose: vi.fn(),
|
||||
})),
|
||||
registerDisposable: vi.fn(),
|
||||
TelemetryLoggerSink: class {},
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
import {
|
||||
captureExtensionActivated,
|
||||
createConfiguredTelemetryHandle,
|
||||
identifyAccount,
|
||||
TelemetryLoggerSink,
|
||||
} from "@cline/core/telemetry";
|
||||
import {
|
||||
type BasicLogger,
|
||||
captureExtensionActivated,
|
||||
createClineTelemetryServiceConfig,
|
||||
createConfiguredTelemetryHandle,
|
||||
type ITelemetryService,
|
||||
identifyAccount,
|
||||
registerDisposable,
|
||||
TelemetryLoggerSink,
|
||||
} from "@cline/core";
|
||||
} from "@cline/shared";
|
||||
import { getCliBuildInfo } from "./common";
|
||||
import {
|
||||
markActivationCaptured,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Agent } from "@cline/sdk";
|
||||
import { createServer } from "node:http";
|
||||
import { Agent } from "@cline/sdk";
|
||||
|
||||
const PORT = Number(process.env.PORT || 3456);
|
||||
|
||||
|
||||
@@ -20,3 +20,11 @@ export {
|
||||
OpenTelemetryProvider,
|
||||
type OpenTelemetryProviderOptions,
|
||||
} from "./OpenTelemetryProvider";
|
||||
export {
|
||||
captureExtensionActivated,
|
||||
identifyAccount,
|
||||
} from "./core-events";
|
||||
export {
|
||||
TelemetryLoggerSink,
|
||||
type TelemetryLoggerSinkOptions,
|
||||
} from "./TelemetryLoggerSink";
|
||||
|
||||
Reference in New Issue
Block a user