codeep 2.5.1 → 2.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +59 -0
- package/dist/acp/commands.js +48 -8
- package/dist/acp/server.js +20 -8
- package/dist/config/index.d.ts +26 -3
- package/dist/config/index.js +148 -65
- package/dist/renderer/App.js +2 -1
- package/dist/renderer/commands.js +39 -5
- package/dist/renderer/main.js +22 -4
- package/dist/utils/agent.d.ts +9 -0
- package/dist/utils/agent.js +25 -6
- package/dist/utils/codeReview.d.ts +14 -0
- package/dist/utils/codeReview.js +67 -9
- package/dist/utils/codeepCloud.js +13 -1
- package/dist/utils/keychain.js +34 -12
- package/dist/utils/reviewConfig.d.ts +10 -0
- package/dist/utils/reviewConfig.js +129 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -217,6 +217,43 @@ AI-powered review of your git diff with `/review`:
|
|
|
217
217
|
|
|
218
218
|
If there are no git changes, falls back to static analysis automatically.
|
|
219
219
|
|
|
220
|
+
#### Custom rules (`.codeep/review.json`)
|
|
221
|
+
|
|
222
|
+
The static reviewer (`codeep review` / `/review --static`) ships a set of
|
|
223
|
+
built-in rules, but a project can tailor them — check a `.codeep/review.json`
|
|
224
|
+
into the repo and the CLI **and** the [Codeep GitHub Action](https://github.com/VladoIvankovic/codeep-action)
|
|
225
|
+
both pick it up automatically (zero LLM cost):
|
|
226
|
+
|
|
227
|
+
```json
|
|
228
|
+
{
|
|
229
|
+
"rules": [
|
|
230
|
+
{
|
|
231
|
+
"id": "no-internal-import",
|
|
232
|
+
"pattern": "from ['\"]@acme/internal",
|
|
233
|
+
"category": "best-practice",
|
|
234
|
+
"severity": "error",
|
|
235
|
+
"message": "Don't import from @acme/internal outside the platform team",
|
|
236
|
+
"suggestion": "Use the public @acme/sdk package",
|
|
237
|
+
"extensions": [".ts", ".tsx"]
|
|
238
|
+
}
|
|
239
|
+
],
|
|
240
|
+
"disable": ["todo-comment", "anonymous-function"],
|
|
241
|
+
"include": ["src/**"],
|
|
242
|
+
"exclude": ["**/*.test.ts", "vendor/**"]
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
- **`rules`** — your own checks. `id`, `pattern` (a regex string), and `message`
|
|
247
|
+
are required; `flags` (default `g`), `category`, `severity`
|
|
248
|
+
(`error|warning|info|suggestion`), `suggestion`, and `extensions` are optional.
|
|
249
|
+
- **`disable`** — turn off built-in rules by id (e.g. `eval-usage`,
|
|
250
|
+
`hardcoded-password`, `todo-comment`, `any-type`, `console-statement`,
|
|
251
|
+
`long-file`, `long-function`, …).
|
|
252
|
+
- **`include` / `exclude`** — glob scoping (`**`, `*`, `?`); `include` empty = all files.
|
|
253
|
+
|
|
254
|
+
A missing, malformed, or partially-invalid config never breaks a review — bad
|
|
255
|
+
entries are skipped and the run proceeds with whatever is valid.
|
|
256
|
+
|
|
220
257
|
### Interactive Mode
|
|
221
258
|
Agent asks clarifying questions when tasks are ambiguous:
|
|
222
259
|
```
|
|
@@ -795,6 +832,28 @@ Hooks and MCP server configs are deliberately **not** synced: hooks run
|
|
|
795
832
|
arbitrary shell, and MCP configs often embed tokens, so both stay local to each
|
|
796
833
|
machine.
|
|
797
834
|
|
|
835
|
+
### Privacy & telemetry
|
|
836
|
+
|
|
837
|
+
Once your CLI is linked, Codeep automatically uploads usage stats (model,
|
|
838
|
+
provider, token counts, estimated cost), session transcripts, `progress.md`,
|
|
839
|
+
and project memory notes to power your dashboard. To opt out of **all** of these
|
|
840
|
+
automatic uploads:
|
|
841
|
+
|
|
842
|
+
```bash
|
|
843
|
+
export CODEEP_NO_TELEMETRY=1 # also honors the cross-tool DO_NOT_TRACK=1
|
|
844
|
+
```
|
|
845
|
+
|
|
846
|
+
…or set it permanently in `~/.codeep/config.json`:
|
|
847
|
+
|
|
848
|
+
```json
|
|
849
|
+
{ "telemetry": false }
|
|
850
|
+
```
|
|
851
|
+
|
|
852
|
+
With telemetry off, nothing is uploaded automatically — the agent still runs
|
|
853
|
+
fully and talks to your LLM provider directly. Explicit `codeep account push` /
|
|
854
|
+
`account sync` commands are always under your control and are never gated by
|
|
855
|
+
this flag.
|
|
856
|
+
|
|
798
857
|
### Tasks
|
|
799
858
|
|
|
800
859
|
Create, view, and complete tasks directly from the CLI — or manage them on the codeep.dev dashboard:
|
package/dist/acp/commands.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// Slash command handler for ACP sessions.
|
|
3
3
|
// Mirrors CLI commands from renderer/commands.ts but returns plain text
|
|
4
4
|
// responses (no TUI) suitable for streaming back via session/update.
|
|
5
|
-
import { config, getCurrentProvider, getModelsForCurrentProvider, setProvider, setApiKey, isConfigured, listSessionsWithInfo, startNewSession, loadSession, saveSession, initializeAsProject, isManuallyInitializedProject, setProjectPermission, hasWritePermission, hasReadPermission, } from '../config/index.js';
|
|
5
|
+
import { config, getCurrentProvider, getModelsForCurrentProvider, setProvider, setApiKey, isConfigured, listSessionsWithInfo, startNewSession, loadSession, saveSession, initializeAsProject, isManuallyInitializedProject, setProjectPermission, hasWritePermission, hasReadPermission, isTelemetryEnabled, telemetryForcedOffByEnv, } from '../config/index.js';
|
|
6
6
|
import { getProviderList, getProvider } from '../config/providers.js';
|
|
7
7
|
import { getProjectContext } from '../utils/project.js';
|
|
8
8
|
import { loadCustomCommands } from '../utils/customCommands.js';
|
|
@@ -207,14 +207,42 @@ export async function handleCommand(input, session, onChunk, abortSignal) {
|
|
|
207
207
|
case 'apikey': {
|
|
208
208
|
if (!args.length)
|
|
209
209
|
return { handled: true, response: showApiKey() };
|
|
210
|
-
return { handled: true, response: setApiKeyCmd(args[0]) };
|
|
210
|
+
return { handled: true, response: await setApiKeyCmd(args[0]) };
|
|
211
|
+
}
|
|
212
|
+
case 'telemetry': {
|
|
213
|
+
const sub = args[0]?.toLowerCase();
|
|
214
|
+
const envOff = telemetryForcedOffByEnv();
|
|
215
|
+
if (sub === 'on' || sub === 'off') {
|
|
216
|
+
if (envOff) {
|
|
217
|
+
return { handled: true, response: 'Telemetry is forced **off** by the `CODEEP_NO_TELEMETRY` / `DO_NOT_TRACK` env var — unset it to change this. The config flag can\'t override an env var.' };
|
|
218
|
+
}
|
|
219
|
+
config.set('telemetry', sub === 'on');
|
|
220
|
+
return {
|
|
221
|
+
handled: true,
|
|
222
|
+
response: sub === 'on'
|
|
223
|
+
? 'Telemetry **on** — usage stats, session transcripts, progress and memory notes sync to codeep.dev.'
|
|
224
|
+
: 'Telemetry **off** — no automatic cloud uploads. Explicit `/account push` still works.',
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
if (sub && sub !== 'status') {
|
|
228
|
+
return { handled: true, response: 'Usage: `/telemetry` · `/telemetry on` · `/telemetry off`' };
|
|
229
|
+
}
|
|
230
|
+
const flag = config.get('telemetry') !== false;
|
|
231
|
+
const lines = [
|
|
232
|
+
`**Telemetry:** ${isTelemetryEnabled() ? 'on' : 'off'}`,
|
|
233
|
+
`- Config flag \`telemetry\`: ${flag}`,
|
|
234
|
+
];
|
|
235
|
+
if (envOff)
|
|
236
|
+
lines.push('- Forced **off** by `CODEEP_NO_TELEMETRY` / `DO_NOT_TRACK` (env overrides the flag).');
|
|
237
|
+
lines.push('', 'Toggle with `/telemetry on` | `/telemetry off`. Controls automatic uploads of usage stats, session transcripts, progress, and memory notes.');
|
|
238
|
+
return { handled: true, response: lines.join('\n') };
|
|
211
239
|
}
|
|
212
240
|
case 'login': {
|
|
213
241
|
const [providerId, apiKey] = args;
|
|
214
242
|
if (!providerId || !apiKey) {
|
|
215
243
|
return { handled: true, response: 'Usage: `/login <providerId> <apiKey>`\n\n' + buildProviderList() };
|
|
216
244
|
}
|
|
217
|
-
return { handled: true, response: loginCmd(providerId, apiKey) };
|
|
245
|
+
return { handled: true, response: await loginCmd(providerId, apiKey) };
|
|
218
246
|
}
|
|
219
247
|
case 'sessions':
|
|
220
248
|
case 'session': {
|
|
@@ -1414,6 +1442,7 @@ function buildHelp() {
|
|
|
1414
1442
|
'| `/model [id]` | List or switch model |',
|
|
1415
1443
|
'| `/login <provider> <key>` | Set API key for a provider |',
|
|
1416
1444
|
'| `/apikey [key]` | Show or set API key |',
|
|
1445
|
+
'| `/telemetry [on\\|off]` | Show or toggle automatic cloud telemetry |',
|
|
1417
1446
|
'| `/lang [code]` | Set response language (`en`, `hr`, `auto`…) |',
|
|
1418
1447
|
'| `/grant` | Grant write access for workspace |',
|
|
1419
1448
|
'',
|
|
@@ -1545,18 +1574,29 @@ const INLINE_KEY_WARNING = '\n\n> ⚠️ The key you just typed is now in your
|
|
|
1545
1574
|
' Prefer setting the provider env var (see `/provider`) or using the' +
|
|
1546
1575
|
' settings UI in the VS Code extension. Clear the line from history if' +
|
|
1547
1576
|
' the machine is shared.';
|
|
1548
|
-
function setApiKeyCmd(key) {
|
|
1577
|
+
async function setApiKeyCmd(key) {
|
|
1549
1578
|
const providerId = getCurrentProvider().id;
|
|
1550
|
-
//
|
|
1551
|
-
|
|
1579
|
+
// Await persistence so the confirmation is only returned once the key is
|
|
1580
|
+
// actually stored (keychain write resolves before the process can exit).
|
|
1581
|
+
try {
|
|
1582
|
+
await setApiKey(key, providerId);
|
|
1583
|
+
}
|
|
1584
|
+
catch {
|
|
1585
|
+
return `Failed to save API key for \`${providerId}\` — secure storage was unavailable. Please try again.`;
|
|
1586
|
+
}
|
|
1552
1587
|
return `API key for \`${providerId}\` saved.${INLINE_KEY_WARNING}`;
|
|
1553
1588
|
}
|
|
1554
|
-
function loginCmd(providerId, apiKey) {
|
|
1589
|
+
async function loginCmd(providerId, apiKey) {
|
|
1555
1590
|
const provider = getProvider(providerId);
|
|
1556
1591
|
if (!provider)
|
|
1557
1592
|
return `Provider \`${providerId}\` not found.\n\n${buildProviderList()}`;
|
|
1558
1593
|
setProvider(providerId);
|
|
1559
|
-
|
|
1594
|
+
try {
|
|
1595
|
+
await setApiKey(apiKey, providerId);
|
|
1596
|
+
}
|
|
1597
|
+
catch {
|
|
1598
|
+
return `Failed to save API key for \`${providerId}\` — secure storage was unavailable. Please try again.`;
|
|
1599
|
+
}
|
|
1560
1600
|
return `Logged in as **${provider.name}** (\`${providerId}\`). Model: \`${provider.defaultModel}\`.${INLINE_KEY_WARNING}`;
|
|
1561
1601
|
}
|
|
1562
1602
|
const PREVIEW_MESSAGES = 6; // last N messages to show on session restore
|
package/dist/acp/server.js
CHANGED
|
@@ -12,7 +12,7 @@ import { loadMcpServerConfig, mergeMcpServers } from '../utils/mcpConfig.js';
|
|
|
12
12
|
import { handleMcpSamplingRequest } from '../utils/mcpSamplingBridge.js';
|
|
13
13
|
import { executeCommandAsync } from '../utils/shell.js';
|
|
14
14
|
import { initWorkspace, loadWorkspace, handleCommand } from './commands.js';
|
|
15
|
-
import { autoSaveSession, config, setProvider, setApiKey, listSessionsWithInfo, deleteSession as deleteSessionFile } from '../config/index.js';
|
|
15
|
+
import { autoSaveSession, config, setProvider, setApiKey, getApiKey, getConfiguredProviders, listSessionsWithInfo, deleteSession as deleteSessionFile } from '../config/index.js';
|
|
16
16
|
import { ApiError } from '../api/index.js';
|
|
17
17
|
import { PROVIDERS } from '../config/providers.js';
|
|
18
18
|
import { getCurrentVersion } from '../utils/update.js';
|
|
@@ -238,15 +238,19 @@ async function collectEmbeddedContext(blocks) {
|
|
|
238
238
|
}
|
|
239
239
|
return snippets.join('\n\n');
|
|
240
240
|
}
|
|
241
|
-
/** Check if a provider has an API key stored (
|
|
241
|
+
/** Check if a provider has an API key stored (synchronous; relies on the cache
|
|
242
|
+
* loaded at startup via loadAllApiKeys and the non-secret configuredProviderIds
|
|
243
|
+
* index — never reads plaintext key material). */
|
|
242
244
|
function providerHasKey(providerId) {
|
|
243
245
|
// Check environment variable first
|
|
244
246
|
const envKey = PROVIDERS[providerId]?.envKey;
|
|
245
247
|
if (envKey && process.env[envKey])
|
|
246
248
|
return true;
|
|
247
|
-
//
|
|
248
|
-
|
|
249
|
-
|
|
249
|
+
// In-memory cache (populated from secure storage at startup)
|
|
250
|
+
if (getApiKey(providerId))
|
|
251
|
+
return true;
|
|
252
|
+
// Non-secret index of providers that have a key in secure storage
|
|
253
|
+
return getConfiguredProviders().some(p => p.id === providerId);
|
|
250
254
|
}
|
|
251
255
|
function buildConfigOptions() {
|
|
252
256
|
const currentModel = config.get('model') ?? '';
|
|
@@ -721,8 +725,12 @@ export function startAcpServer() {
|
|
|
721
725
|
return;
|
|
722
726
|
}
|
|
723
727
|
session.currentModeId = modeId;
|
|
724
|
-
//
|
|
725
|
-
|
|
728
|
+
// Do NOT persist the global `agentConfirmation` config here. The ACP
|
|
729
|
+
// permission gate is driven per-session by `session.currentModeId` (see the
|
|
730
|
+
// onRequestPermission wiring in handleSessionPrompt). Writing it globally
|
|
731
|
+
// would leak this session's mode into other processes — e.g. switching ACP
|
|
732
|
+
// to 'auto' would silently disarm the TUI's confirmation gate. Mode stays
|
|
733
|
+
// on the in-memory session object only.
|
|
726
734
|
transport.respond(msg.id, {});
|
|
727
735
|
// Notify Zed of the mode change
|
|
728
736
|
transport.notify('session/update', {
|
|
@@ -786,7 +794,11 @@ export function startAcpServer() {
|
|
|
786
794
|
const apiKey = value.slice(colonIdx + 1);
|
|
787
795
|
if (providerId && apiKey) {
|
|
788
796
|
setProvider(providerId);
|
|
789
|
-
setApiKey
|
|
797
|
+
// Cache is updated synchronously inside setApiKey; the keychain write
|
|
798
|
+
// resolves while the long-lived server runs. Swallow a persistence
|
|
799
|
+
// failure here (secondary path) so it can't become an unhandled
|
|
800
|
+
// rejection — the key still works for this session via the cache.
|
|
801
|
+
setApiKey(apiKey, providerId).catch(() => { });
|
|
790
802
|
}
|
|
791
803
|
}
|
|
792
804
|
}
|
package/dist/config/index.d.ts
CHANGED
|
@@ -85,7 +85,21 @@ interface ConfigSchema {
|
|
|
85
85
|
agentApiTimeout: number;
|
|
86
86
|
agentInteractive: boolean;
|
|
87
87
|
projectPermissions: ProjectPermission[];
|
|
88
|
+
/** @deprecated Legacy PLAINTEXT key store. Kept only so the one-time
|
|
89
|
+
* migration into secure storage can read it; emptied afterwards. New keys
|
|
90
|
+
* go to the OS keychain via utils/keychain.ts — never written here. */
|
|
88
91
|
providerApiKeys: ProviderApiKey[];
|
|
92
|
+
/** Non-secret index of provider IDs that have a key in secure storage, so we
|
|
93
|
+
* can list/load configured providers without probing the keychain for all
|
|
94
|
+
* providers. Secrets themselves never live here. */
|
|
95
|
+
configuredProviderIds: string[];
|
|
96
|
+
/** True once legacy plaintext keys (providerApiKeys / apiKey) have been
|
|
97
|
+
* migrated into secure storage and wiped from the config file. */
|
|
98
|
+
keysSecured: boolean;
|
|
99
|
+
/** Master switch for automatic cloud uploads (usage stats, session
|
|
100
|
+
* transcripts, progress, memory notes). Default true; set false to opt out.
|
|
101
|
+
* The CODEEP_NO_TELEMETRY / DO_NOT_TRACK env vars also force it off. */
|
|
102
|
+
telemetry: boolean;
|
|
89
103
|
githubId: string;
|
|
90
104
|
githubUsername: string;
|
|
91
105
|
syncToken: string;
|
|
@@ -138,9 +152,11 @@ export declare function loadAllApiKeys(): Promise<void>;
|
|
|
138
152
|
*/
|
|
139
153
|
export declare function getApiKey(providerId?: string): string;
|
|
140
154
|
/**
|
|
141
|
-
* Set API key
|
|
155
|
+
* Set API key — persists to secure storage (OS keychain), never plaintext.
|
|
156
|
+
* Returns a promise, but updates the synchronous cache first so callers that
|
|
157
|
+
* fire-and-forget still see the key immediately via getApiKey().
|
|
142
158
|
*/
|
|
143
|
-
export declare function setApiKey(key: string, providerId?: string): void
|
|
159
|
+
export declare function setApiKey(key: string, providerId?: string): Promise<void>;
|
|
144
160
|
export declare function getMaskedApiKey(providerId?: string): string;
|
|
145
161
|
/**
|
|
146
162
|
* Get list of providers that have API keys configured
|
|
@@ -149,10 +165,17 @@ export declare function getConfiguredProviders(): {
|
|
|
149
165
|
id: string;
|
|
150
166
|
name: string;
|
|
151
167
|
}[];
|
|
168
|
+
export declare function isTelemetryEnabled(): boolean;
|
|
169
|
+
/**
|
|
170
|
+
* True when an env var (CODEEP_NO_TELEMETRY / DO_NOT_TRACK) is forcing telemetry
|
|
171
|
+
* off — in which case the `telemetry` config flag can't turn it back on. Lets
|
|
172
|
+
* the /telemetry command explain why a toggle had no effect.
|
|
173
|
+
*/
|
|
174
|
+
export declare function telemetryForcedOffByEnv(): boolean;
|
|
152
175
|
/**
|
|
153
176
|
* Clear API key for a specific provider
|
|
154
177
|
*/
|
|
155
|
-
export declare function clearApiKey(providerId: string): void
|
|
178
|
+
export declare function clearApiKey(providerId: string): Promise<void>;
|
|
156
179
|
export declare function isConfiguredAsync(providerId?: string): Promise<boolean>;
|
|
157
180
|
export declare function isConfigured(providerId?: string): boolean;
|
|
158
181
|
export declare function getCurrentProvider(): {
|
package/dist/config/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { join, dirname } from 'path';
|
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
5
|
import { PROVIDERS, getProvider, getProviderBaseUrl } from './providers.js';
|
|
6
6
|
import { logSession } from '../utils/logger.js';
|
|
7
|
+
import { createSecureStorage } from '../utils/keychain.js';
|
|
7
8
|
// We'll initialize GLOBAL_SESSIONS_DIR after config is created (to use config.path)
|
|
8
9
|
/**
|
|
9
10
|
* Get sessions directory - local .codeep/sessions/ if in project, otherwise global
|
|
@@ -183,6 +184,9 @@ function createConfig() {
|
|
|
183
184
|
rateLimitCommands: 10000,
|
|
184
185
|
projectPermissions: [],
|
|
185
186
|
providerApiKeys: [],
|
|
187
|
+
configuredProviderIds: [],
|
|
188
|
+
keysSecured: false,
|
|
189
|
+
telemetry: true,
|
|
186
190
|
githubId: '',
|
|
187
191
|
githubUsername: '',
|
|
188
192
|
syncToken: '',
|
|
@@ -272,6 +276,89 @@ if (!existsSync(GLOBAL_SESSIONS_DIR)) {
|
|
|
272
276
|
}
|
|
273
277
|
// In-memory cache for API keys (populated on first access)
|
|
274
278
|
const apiKeyCache = new Map();
|
|
279
|
+
// ── Secure key storage (OS keychain, plaintext-config fallback) ──────────────
|
|
280
|
+
// API keys persist in the OS keychain via utils/keychain.ts. The cache above is
|
|
281
|
+
// the synchronous read path (getApiKey); the keychain is the async persistence
|
|
282
|
+
// layer (set/load). A non-secret `configuredProviderIds` index in config tells
|
|
283
|
+
// us which providers have a key without probing the keychain for every provider.
|
|
284
|
+
let _secureKeyStore = null;
|
|
285
|
+
function secureKeyStore() {
|
|
286
|
+
if (!_secureKeyStore)
|
|
287
|
+
_secureKeyStore = createSecureStorage(config);
|
|
288
|
+
return _secureKeyStore;
|
|
289
|
+
}
|
|
290
|
+
function addConfiguredProviderId(providerId) {
|
|
291
|
+
const ids = config.get('configuredProviderIds') || [];
|
|
292
|
+
if (!ids.includes(providerId)) {
|
|
293
|
+
config.set('configuredProviderIds', [...ids, providerId]);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
function removeConfiguredProviderId(providerId) {
|
|
297
|
+
const ids = config.get('configuredProviderIds') || [];
|
|
298
|
+
if (ids.includes(providerId)) {
|
|
299
|
+
config.set('configuredProviderIds', ids.filter(id => id !== providerId));
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
let _migrationPromise = null;
|
|
303
|
+
/**
|
|
304
|
+
* One-time migration of legacy PLAINTEXT keys (the `providerApiKeys` array and
|
|
305
|
+
* the very-old single `apiKey` field) into secure storage, then wipe the
|
|
306
|
+
* plaintext from the config file. Idempotent and guarded so concurrent callers
|
|
307
|
+
* share a single run. Each key's plaintext is wiped only after its write to
|
|
308
|
+
* secure storage actually succeeds; any key that fails to persist is retained
|
|
309
|
+
* and `keysSecured` stays false so the next startup retries — the last copy of
|
|
310
|
+
* a secret is never destroyed before a new copy is confirmed.
|
|
311
|
+
*/
|
|
312
|
+
async function migrateKeysToSecureStorage() {
|
|
313
|
+
if (config.get('keysSecured'))
|
|
314
|
+
return;
|
|
315
|
+
// Dedupe concurrent callers (loadApiKey + loadAllApiKeys at startup) onto a
|
|
316
|
+
// single run; cleared in finally so a later un-secured state can re-migrate.
|
|
317
|
+
if (!_migrationPromise) {
|
|
318
|
+
_migrationPromise = (async () => {
|
|
319
|
+
const store = secureKeyStore();
|
|
320
|
+
const legacyArray = config.get('providerApiKeys') || [];
|
|
321
|
+
// Only drain entries we actually persisted; keep failures so the next
|
|
322
|
+
// startup retries instead of destroying the last copy of a key.
|
|
323
|
+
const failed = [];
|
|
324
|
+
for (const entry of legacyArray) {
|
|
325
|
+
if (!entry?.providerId || !entry?.apiKey)
|
|
326
|
+
continue;
|
|
327
|
+
try {
|
|
328
|
+
await store.setApiKey(entry.providerId, entry.apiKey);
|
|
329
|
+
addConfiguredProviderId(entry.providerId);
|
|
330
|
+
}
|
|
331
|
+
catch {
|
|
332
|
+
failed.push(entry);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
const legacySingle = config.get('apiKey');
|
|
336
|
+
let legacySingleMigrated = false;
|
|
337
|
+
if (legacySingle) {
|
|
338
|
+
try {
|
|
339
|
+
await store.setApiKey('z.ai', legacySingle);
|
|
340
|
+
addConfiguredProviderId('z.ai');
|
|
341
|
+
legacySingleMigrated = true;
|
|
342
|
+
}
|
|
343
|
+
catch { /* keep the legacy field for retry */ }
|
|
344
|
+
}
|
|
345
|
+
// Wipe only the plaintext that made it into secure storage.
|
|
346
|
+
config.set('providerApiKeys', failed);
|
|
347
|
+
if (legacySingle && legacySingleMigrated)
|
|
348
|
+
config.set('apiKey', '');
|
|
349
|
+
// Mark secured only when everything migrated; a partial failure retries.
|
|
350
|
+
if (failed.length === 0 && (!legacySingle || legacySingleMigrated)) {
|
|
351
|
+
config.set('keysSecured', true);
|
|
352
|
+
}
|
|
353
|
+
})();
|
|
354
|
+
}
|
|
355
|
+
try {
|
|
356
|
+
await _migrationPromise;
|
|
357
|
+
}
|
|
358
|
+
finally {
|
|
359
|
+
_migrationPromise = null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
275
362
|
export const LANGUAGES = {
|
|
276
363
|
'auto': 'Auto-detect',
|
|
277
364
|
'en': 'English',
|
|
@@ -316,20 +403,12 @@ export async function loadApiKey(providerId) {
|
|
|
316
403
|
return process.env.ZHIPUAI_API_KEY;
|
|
317
404
|
}
|
|
318
405
|
}
|
|
319
|
-
//
|
|
320
|
-
|
|
321
|
-
const stored =
|
|
322
|
-
if (stored
|
|
323
|
-
apiKeyCache.set(provider, stored
|
|
324
|
-
return stored
|
|
325
|
-
}
|
|
326
|
-
// Fallback to legacy apiKey field (for z.ai)
|
|
327
|
-
if (provider === 'z.ai') {
|
|
328
|
-
const legacyKey = config.get('apiKey') || '';
|
|
329
|
-
if (legacyKey) {
|
|
330
|
-
apiKeyCache.set(provider, legacyKey);
|
|
331
|
-
return legacyKey;
|
|
332
|
-
}
|
|
406
|
+
// Secure storage (OS keychain). Migrate any legacy plaintext keys first.
|
|
407
|
+
await migrateKeysToSecureStorage();
|
|
408
|
+
const stored = await secureKeyStore().getApiKey(provider);
|
|
409
|
+
if (stored) {
|
|
410
|
+
apiKeyCache.set(provider, stored);
|
|
411
|
+
return stored;
|
|
333
412
|
}
|
|
334
413
|
return '';
|
|
335
414
|
}
|
|
@@ -338,12 +417,14 @@ export async function loadApiKey(providerId) {
|
|
|
338
417
|
* Should be called at app startup
|
|
339
418
|
*/
|
|
340
419
|
export async function loadAllApiKeys() {
|
|
341
|
-
//
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
420
|
+
// Migrate any legacy plaintext keys, then load all from secure storage using
|
|
421
|
+
// the non-secret configuredProviderIds index (no need to probe every provider).
|
|
422
|
+
await migrateKeysToSecureStorage();
|
|
423
|
+
const store = secureKeyStore();
|
|
424
|
+
for (const providerId of (config.get('configuredProviderIds') || [])) {
|
|
425
|
+
const key = await store.getApiKey(providerId);
|
|
426
|
+
if (key)
|
|
427
|
+
apiKeyCache.set(providerId, key);
|
|
347
428
|
}
|
|
348
429
|
// Also check environment variables for each provider
|
|
349
430
|
for (const [providerId, providerConfig] of Object.entries(PROVIDERS)) {
|
|
@@ -354,7 +435,8 @@ export async function loadAllApiKeys() {
|
|
|
354
435
|
}
|
|
355
436
|
}
|
|
356
437
|
}
|
|
357
|
-
// Legacy env vars for z.ai
|
|
438
|
+
// Legacy env vars for z.ai (the legacy `apiKey` config field is migrated into
|
|
439
|
+
// secure storage by migrateKeysToSecureStorage above).
|
|
358
440
|
if (!apiKeyCache.get('z.ai')) {
|
|
359
441
|
if (process.env.ZAI_API_KEY) {
|
|
360
442
|
apiKeyCache.set('z.ai', process.env.ZAI_API_KEY);
|
|
@@ -362,13 +444,6 @@ export async function loadAllApiKeys() {
|
|
|
362
444
|
else if (process.env.ZHIPUAI_API_KEY) {
|
|
363
445
|
apiKeyCache.set('z.ai', process.env.ZHIPUAI_API_KEY);
|
|
364
446
|
}
|
|
365
|
-
else {
|
|
366
|
-
// Fallback to legacy apiKey field
|
|
367
|
-
const legacyKey = config.get('apiKey') || '';
|
|
368
|
-
if (legacyKey) {
|
|
369
|
-
apiKeyCache.set('z.ai', legacyKey);
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
447
|
}
|
|
373
448
|
}
|
|
374
449
|
/**
|
|
@@ -379,26 +454,22 @@ export function getApiKey(providerId) {
|
|
|
379
454
|
return apiKeyCache.get(provider) || '';
|
|
380
455
|
}
|
|
381
456
|
/**
|
|
382
|
-
* Set API key
|
|
457
|
+
* Set API key — persists to secure storage (OS keychain), never plaintext.
|
|
458
|
+
* Returns a promise, but updates the synchronous cache first so callers that
|
|
459
|
+
* fire-and-forget still see the key immediately via getApiKey().
|
|
383
460
|
*/
|
|
384
|
-
export function setApiKey(key, providerId) {
|
|
461
|
+
export async function setApiKey(key, providerId) {
|
|
385
462
|
const provider = providerId || config.get('provider');
|
|
386
|
-
// Update cache immediately
|
|
463
|
+
// Update cache immediately so synchronous getApiKey() works right away.
|
|
387
464
|
apiKeyCache.set(provider, key);
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
}
|
|
397
|
-
config.set('providerApiKeys', providerKeys);
|
|
398
|
-
// Also set legacy field for backwards compatibility (z.ai only)
|
|
399
|
-
if (provider === 'z.ai') {
|
|
400
|
-
config.set('apiKey', key);
|
|
401
|
-
}
|
|
465
|
+
// Persist to secure storage (keychain, or plaintext-config fallback if the
|
|
466
|
+
// keychain is unavailable — utils/keychain.ts warns in that case). Let a hard
|
|
467
|
+
// persistence failure propagate so callers can report it instead of claiming
|
|
468
|
+
// success; the index + secured flag are set only after a confirmed write.
|
|
469
|
+
await secureKeyStore().setApiKey(provider, key);
|
|
470
|
+
addConfiguredProviderId(provider);
|
|
471
|
+
// No plaintext key was written here, so the store is already "secured".
|
|
472
|
+
config.set('keysSecured', true);
|
|
402
473
|
}
|
|
403
474
|
export function getMaskedApiKey(providerId) {
|
|
404
475
|
const key = getApiKey(providerId);
|
|
@@ -411,33 +482,45 @@ export function getMaskedApiKey(providerId) {
|
|
|
411
482
|
* Get list of providers that have API keys configured
|
|
412
483
|
*/
|
|
413
484
|
export function getConfiguredProviders() {
|
|
414
|
-
const
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
485
|
+
const ids = config.get('configuredProviderIds') || [];
|
|
486
|
+
return ids.map(id => ({ id, name: getProvider(id)?.name || id }));
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* Whether automatic cloud uploads are allowed (usage stats, session
|
|
490
|
+
* transcripts, progress, memory notes). Opt out via the `telemetry: false`
|
|
491
|
+
* config flag, the CODEEP_NO_TELEMETRY env var, or the cross-tool DO_NOT_TRACK
|
|
492
|
+
* convention. Explicit `codeep account push/sync` commands are user-initiated
|
|
493
|
+
* and are NOT gated by this.
|
|
494
|
+
*/
|
|
495
|
+
function envForcesTelemetryOff() {
|
|
496
|
+
const off = (v) => !!v && !/^(0|false|no|off)$/i.test(v.trim());
|
|
497
|
+
return off(process.env.CODEEP_NO_TELEMETRY) || off(process.env.DO_NOT_TRACK);
|
|
498
|
+
}
|
|
499
|
+
export function isTelemetryEnabled() {
|
|
500
|
+
if (envForcesTelemetryOff())
|
|
501
|
+
return false;
|
|
502
|
+
return config.get('telemetry') !== false;
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* True when an env var (CODEEP_NO_TELEMETRY / DO_NOT_TRACK) is forcing telemetry
|
|
506
|
+
* off — in which case the `telemetry` config flag can't turn it back on. Lets
|
|
507
|
+
* the /telemetry command explain why a toggle had no effect.
|
|
508
|
+
*/
|
|
509
|
+
export function telemetryForcedOffByEnv() {
|
|
510
|
+
return envForcesTelemetryOff();
|
|
426
511
|
}
|
|
427
512
|
/**
|
|
428
513
|
* Clear API key for a specific provider
|
|
429
514
|
*/
|
|
430
|
-
export function clearApiKey(providerId) {
|
|
515
|
+
export async function clearApiKey(providerId) {
|
|
431
516
|
// Clear from cache
|
|
432
517
|
apiKeyCache.delete(providerId);
|
|
433
|
-
// Clear from
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
config.set('providerApiKeys', filtered);
|
|
437
|
-
// Clear legacy field if z.ai
|
|
438
|
-
if (providerId === 'z.ai') {
|
|
439
|
-
config.set('apiKey', '');
|
|
518
|
+
// Clear from secure storage + the non-secret index
|
|
519
|
+
try {
|
|
520
|
+
await secureKeyStore().deleteApiKey(providerId);
|
|
440
521
|
}
|
|
522
|
+
catch { /* ignore */ }
|
|
523
|
+
removeConfiguredProviderId(providerId);
|
|
441
524
|
}
|
|
442
525
|
export async function isConfiguredAsync(providerId) {
|
|
443
526
|
const key = await loadApiKey(providerId);
|
package/dist/renderer/App.js
CHANGED
|
@@ -82,6 +82,7 @@ const COMMAND_DESCRIPTIONS = {
|
|
|
82
82
|
'profile': 'Save/load settings profiles',
|
|
83
83
|
'tasks': 'Show pending tasks from codeep.dev dashboard',
|
|
84
84
|
'sync': 'Sync learning preferences and profiles to codeep.dev',
|
|
85
|
+
'telemetry': 'Show or toggle automatic cloud telemetry (on/off)',
|
|
85
86
|
// 2.0 — surfaced for `/` autocomplete; documented in /help too.
|
|
86
87
|
'compact': 'Summarize older messages to free up context',
|
|
87
88
|
'commands': 'List custom slash commands in .codeep/commands/*.md',
|
|
@@ -232,7 +233,7 @@ export class App {
|
|
|
232
233
|
'multiline', 'memory', 'init',
|
|
233
234
|
'provider', 'model', 'protocol', 'lang', 'grant', 'login', 'logout',
|
|
234
235
|
'context-save', 'context-load', 'context-clear', 'learn',
|
|
235
|
-
'cost', 'tasks', 'account', 'sync',
|
|
236
|
+
'cost', 'tasks', 'account', 'sync', 'telemetry',
|
|
236
237
|
// 2.0 — extensions, checkpoints, MCP, custom commands, OpenRouter prefs.
|
|
237
238
|
// Keep in lockstep with COMMAND_DESCRIPTIONS below and helpCategories.
|
|
238
239
|
'compact', 'commands', 'checkpoint', 'checkpoints', 'rewind',
|