mirror of
https://github.com/cline/cline.git
synced 2026-09-18 09:24:17 +08:00
* feat(core): support pathless sessions with temporary workspaces * fix(desktop): mark editor icons as decorative * fix(core): omit absent auth request IDs * test(sdk): restore request_id auth telemetry param in core-events test The branch's drive-by request_id -> requestId rename was dropped while resolving the merge conflict with #12444 (which added requestIdDetails on main), so the public captureAuthLoggedOut/captureAuthRefreshSoftFailure API keeps its original parameter name. * refactor(sdk): root pathless session workspaces under the cline data dir Move the workspace created for pathless session starts from <os.tmpdir()>/cline/sessions/<id>-temp/project to <cline-data-dir>/workspaces/<id>/project (default ~/.cline/data/workspaces/<id>/project), per PR review: - OS tmp reapers (macOS ~3-day purge, systemd-tmpfiles, reboot cleanup) silently delete user work created in 'New Project' sessions - /tmp is a shared namespace on Linux: the first user to create /tmp/cline owns it (EACCES for everyone else), and guessable session IDs let a local attacker pre-create the workspace directory - under the data dir the workspace shares the session store's lifecycle and the existing CLINE_DATA_DIR / CLINE_DIR overrides for tests and sandboxes isTemporaryWorkspacePath now matches the .cline/data/workspaces/<id>/project segment shape, and the -temp suffix is gone since the id-scoped directory no longer needs to mark itself as reapable. * feat(sdk): open pathless sessions in one shared chat workspace Instead of minting a workspace directory per session (<data>/workspaces/<session-id>/project), all sessions started without a cwd/workspaceRoot now share <cline-data-dir>/workspaces/chat (default ~/.cline/data/workspaces/chat). Starting a pathless session seeds the directory with an AGENTS.md rules file (only when missing, so users can edit it) that tells the agent to treat the session as a chat: don't create or edit files unprompted, ask where a project should live when the user wants one built, and default to a new named folder inside the chat directory that later sessions can reference. This avoids unbounded per-session directory sprawl, gives chat sessions a stable home the user can revisit, and groups them naturally in the desktop sidebar. The desktop app now labels the shared workspace "Chat" (menu action "Just chat") instead of "New Project", and isChatWorkspacePath matches only the chat directory itself, so project folders created inside it behave as regular workspaces. --------- Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
187 lines
5.2 KiB
TypeScript
187 lines
5.2 KiB
TypeScript
import {
|
|
buildRemoteConfigSessionBlobUploadMetadata,
|
|
ClineAccountService,
|
|
type ClineCoreStartInput,
|
|
createRemoteConfigSessionMessagesArtifactUploader,
|
|
ProviderSettingsManager,
|
|
prepareRemoteConfigCoreIntegration,
|
|
REMOTE_CONFIG_SESSION_BLOB_UPLOAD_METADATA_KEY,
|
|
readRemoteConfigSessionBlobUploadMetadata,
|
|
registerRemoteConfigSessionBlobUpload,
|
|
resolveLocalClineAuthToken,
|
|
type SessionMessagesArtifactUploader,
|
|
} from "@cline/core";
|
|
import {
|
|
getClineEnvironmentConfig,
|
|
type RemoteConfigBundle,
|
|
RemoteConfigSchema,
|
|
} from "@cline/shared";
|
|
import { getCliTelemetryService } from "./telemetry";
|
|
|
|
const initializedRemoteConfigKeys = new Set<string>();
|
|
let cliRemoteConfigBundlePromise:
|
|
| Promise<RemoteConfigBundle | undefined>
|
|
| undefined;
|
|
|
|
async function loadCliRemoteConfigBundle(): Promise<
|
|
RemoteConfigBundle | undefined
|
|
> {
|
|
cliRemoteConfigBundlePromise ??= loadCliRemoteConfigBundleUncached().finally(
|
|
() => {
|
|
cliRemoteConfigBundlePromise = undefined;
|
|
},
|
|
);
|
|
return await cliRemoteConfigBundlePromise;
|
|
}
|
|
|
|
async function loadCliRemoteConfigBundleUncached(): Promise<
|
|
RemoteConfigBundle | undefined
|
|
> {
|
|
const manager = new ProviderSettingsManager();
|
|
const settings = manager.getProviderSettings("cline");
|
|
const authToken = resolveLocalClineAuthToken(settings)?.trim();
|
|
if (!authToken) {
|
|
return undefined;
|
|
}
|
|
|
|
const service = new ClineAccountService({
|
|
apiBaseUrl:
|
|
settings?.baseUrl?.trim() || getClineEnvironmentConfig().apiBaseUrl,
|
|
getAuthToken: async () => authToken,
|
|
});
|
|
const response = await service.fetchRemoteConfig().catch(() => null);
|
|
if (!response?.enabled || !response.value?.trim()) {
|
|
return undefined;
|
|
}
|
|
|
|
let parsed: unknown;
|
|
try {
|
|
parsed = JSON.parse(response.value);
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
const remoteConfigResult = RemoteConfigSchema.safeParse(parsed);
|
|
if (!remoteConfigResult.success) {
|
|
return undefined;
|
|
}
|
|
|
|
return {
|
|
source: "cline-account",
|
|
version: response.organizationId?.trim() || "remote-config",
|
|
remoteConfig: remoteConfigResult.data,
|
|
};
|
|
}
|
|
|
|
export function createCliMessagesArtifactUploader() {
|
|
const uploader = createRemoteConfigSessionMessagesArtifactUploader();
|
|
const telemetry = getCliTelemetryService();
|
|
return {
|
|
async uploadMessagesFile(input) {
|
|
const metadata = readRemoteConfigSessionBlobUploadMetadata(input.row);
|
|
const startedAt = Date.now();
|
|
try {
|
|
await uploader.uploadMessagesFile(input);
|
|
if (!metadata) {
|
|
return;
|
|
}
|
|
telemetry?.capture?.({
|
|
event: "enterprise.prompt_upload_succeeded",
|
|
properties: {
|
|
sessionId: input.sessionId,
|
|
adapterType: metadata.storage.adapterType,
|
|
bucket: metadata.storage.bucket,
|
|
keyPrefix: metadata.keyPrefix,
|
|
bytes: input.contents.length,
|
|
durationMs: Date.now() - startedAt,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
telemetry?.capture?.({
|
|
event: "enterprise.prompt_upload_failed",
|
|
properties: {
|
|
sessionId: input.sessionId,
|
|
adapterType: metadata?.storage.adapterType,
|
|
bucket: metadata?.storage.bucket,
|
|
keyPrefix: metadata?.keyPrefix,
|
|
bytes: input.contents.length,
|
|
durationMs: Date.now() - startedAt,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
},
|
|
});
|
|
throw error;
|
|
}
|
|
},
|
|
} satisfies SessionMessagesArtifactUploader;
|
|
}
|
|
|
|
function captureRemoteConfigInitialized(bundle: RemoteConfigBundle): void {
|
|
const telemetry = getCliTelemetryService();
|
|
const key = `${bundle.source}:${bundle.version}`;
|
|
if (initializedRemoteConfigKeys.has(key)) {
|
|
return;
|
|
}
|
|
initializedRemoteConfigKeys.add(key);
|
|
const promptUploading =
|
|
bundle.remoteConfig?.enterpriseTelemetry?.promptUploading;
|
|
telemetry?.capture?.({
|
|
event: "enterprise.remote_config_initialized",
|
|
properties: {
|
|
source: bundle.source,
|
|
version: bundle.version,
|
|
hasPromptUploading: Boolean(promptUploading),
|
|
promptUploadingType: promptUploading?.type,
|
|
promptUploadingEnabled: promptUploading?.enabled !== false,
|
|
hasGlobalRules: (bundle.remoteConfig?.globalRules?.length ?? 0) > 0,
|
|
hasGlobalWorkflows:
|
|
(bundle.remoteConfig?.globalWorkflows?.length ?? 0) > 0,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function prepareCliEnterpriseIntegration(
|
|
input: ClineCoreStartInput,
|
|
) {
|
|
const workspacePath =
|
|
input.config.workspaceRoot?.trim() || input.config.cwd?.trim();
|
|
if (!workspacePath) {
|
|
return undefined;
|
|
}
|
|
const bundle = await loadCliRemoteConfigBundle();
|
|
if (!bundle) {
|
|
return undefined;
|
|
}
|
|
captureRemoteConfigInitialized(bundle);
|
|
return prepareRemoteConfigCoreIntegration({
|
|
workspacePath,
|
|
pluginName: "enterprise",
|
|
controlPlane: {
|
|
name: "cline-account",
|
|
async fetchBundle() {
|
|
return bundle;
|
|
},
|
|
},
|
|
requireBundle: false,
|
|
});
|
|
}
|
|
|
|
export async function resolveCliSessionMetadata(
|
|
sessionId?: string,
|
|
): Promise<Record<string, unknown> | undefined> {
|
|
const bundle = await loadCliRemoteConfigBundle();
|
|
if (bundle) {
|
|
captureRemoteConfigInitialized(bundle);
|
|
}
|
|
if (sessionId) {
|
|
registerRemoteConfigSessionBlobUpload(sessionId, bundle?.remoteConfig);
|
|
}
|
|
const blobUploadMetadata = buildRemoteConfigSessionBlobUploadMetadata(
|
|
bundle?.remoteConfig,
|
|
);
|
|
if (!blobUploadMetadata) {
|
|
return undefined;
|
|
}
|
|
return {
|
|
[REMOTE_CONFIG_SESSION_BLOB_UPLOAD_METADATA_KEY]: blobUploadMetadata,
|
|
};
|
|
}
|