codeep 2.5.0 → 2.5.2
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 +22 -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/codeepCloud.js +13 -1
- package/dist/utils/keychain.js +34 -12
- package/dist/utils/projectIntelligence.js +77 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -795,6 +795,28 @@ Hooks and MCP server configs are deliberately **not** synced: hooks run
|
|
|
795
795
|
arbitrary shell, and MCP configs often embed tokens, so both stay local to each
|
|
796
796
|
machine.
|
|
797
797
|
|
|
798
|
+
### Privacy & telemetry
|
|
799
|
+
|
|
800
|
+
Once your CLI is linked, Codeep automatically uploads usage stats (model,
|
|
801
|
+
provider, token counts, estimated cost), session transcripts, `progress.md`,
|
|
802
|
+
and project memory notes to power your dashboard. To opt out of **all** of these
|
|
803
|
+
automatic uploads:
|
|
804
|
+
|
|
805
|
+
```bash
|
|
806
|
+
export CODEEP_NO_TELEMETRY=1 # also honors the cross-tool DO_NOT_TRACK=1
|
|
807
|
+
```
|
|
808
|
+
|
|
809
|
+
…or set it permanently in `~/.codeep/config.json`:
|
|
810
|
+
|
|
811
|
+
```json
|
|
812
|
+
{ "telemetry": false }
|
|
813
|
+
```
|
|
814
|
+
|
|
815
|
+
With telemetry off, nothing is uploaded automatically — the agent still runs
|
|
816
|
+
fully and talks to your LLM provider directly. Explicit `codeep account push` /
|
|
817
|
+
`account sync` commands are always under your control and are never gated by
|
|
818
|
+
this flag.
|
|
819
|
+
|
|
798
820
|
### Tasks
|
|
799
821
|
|
|
800
822
|
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',
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* decoupled from global state. Import-heavy commands use dynamic imports
|
|
6
6
|
* to keep startup time low.
|
|
7
7
|
*/
|
|
8
|
-
import { config, getCurrentProvider, getModelsForCurrentProvider, PROTOCOLS, LANGUAGES, setProvider, setApiKey, clearApiKey, getApiKey, saveSession, startNewSession, loadSession, listSessionsWithInfo, deleteSession, renameSession, setProjectPermission, saveProfile, loadProfile, applyProfile, listProfiles, deleteProfile, initializeAsProject, isManuallyInitializedProject, } from '../config/index.js';
|
|
8
|
+
import { config, getCurrentProvider, getModelsForCurrentProvider, PROTOCOLS, LANGUAGES, setProvider, setApiKey, clearApiKey, getApiKey, isTelemetryEnabled, telemetryForcedOffByEnv, saveSession, startNewSession, loadSession, listSessionsWithInfo, deleteSession, renameSession, setProjectPermission, saveProfile, loadProfile, applyProfile, listProfiles, deleteProfile, initializeAsProject, isManuallyInitializedProject, } from '../config/index.js';
|
|
9
9
|
import { getProjectContext } from '../utils/project.js';
|
|
10
10
|
import { getCurrentVersion } from '../utils/update.js';
|
|
11
11
|
import { getProviderList, getProvider } from '../config/providers.js';
|
|
@@ -245,6 +245,35 @@ export async function handleCommand(command, args, ctx) {
|
|
|
245
245
|
ctx.app.addMessage({ role: 'system', content: lines.join('\n') });
|
|
246
246
|
break;
|
|
247
247
|
}
|
|
248
|
+
case 'telemetry': {
|
|
249
|
+
const sub = args[0]?.toLowerCase();
|
|
250
|
+
const envOff = telemetryForcedOffByEnv();
|
|
251
|
+
if (sub === 'on' || sub === 'off') {
|
|
252
|
+
if (envOff) {
|
|
253
|
+
ctx.app.notify('Telemetry is forced OFF by CODEEP_NO_TELEMETRY / DO_NOT_TRACK — unset that env var to change it.');
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
config.set('telemetry', sub === 'on');
|
|
257
|
+
ctx.app.notify(sub === 'on'
|
|
258
|
+
? 'Telemetry on — usage stats, transcripts, progress & notes sync to codeep.dev.'
|
|
259
|
+
: 'Telemetry off — no automatic cloud uploads.');
|
|
260
|
+
break;
|
|
261
|
+
}
|
|
262
|
+
if (sub && sub !== 'status') {
|
|
263
|
+
ctx.app.notify('Usage: /telemetry · /telemetry on · /telemetry off');
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
const flag = config.get('telemetry') !== false;
|
|
267
|
+
const tLines = ['## Telemetry', ''];
|
|
268
|
+
tLines.push(`**State** ${isTelemetryEnabled() ? 'on' : 'off'}`);
|
|
269
|
+
tLines.push(`**Flag** telemetry = ${flag}`);
|
|
270
|
+
if (envOff)
|
|
271
|
+
tLines.push('**Env** forced off by CODEEP_NO_TELEMETRY / DO_NOT_TRACK (overrides the flag)');
|
|
272
|
+
tLines.push('');
|
|
273
|
+
tLines.push('Toggle with `/telemetry on` or `/telemetry off`. Controls automatic uploads of usage stats, session transcripts, progress, and memory notes.');
|
|
274
|
+
ctx.app.addMessage({ role: 'system', content: tLines.join('\n') });
|
|
275
|
+
break;
|
|
276
|
+
}
|
|
248
277
|
case 'grant': {
|
|
249
278
|
setProjectPermission(ctx.projectPath, true, true);
|
|
250
279
|
ctx.setHasWriteAccess(true);
|
|
@@ -880,8 +909,13 @@ Format: use headers per category, only include categories where you found issues
|
|
|
880
909
|
ctx.app.showLogin(providers.map(p => ({ id: p.id, name: p.name, description: p.description, subscribeUrl: p.subscribeUrl, noApiKey: p.noApiKey })), async (result) => {
|
|
881
910
|
if (result) {
|
|
882
911
|
setProvider(result.providerId);
|
|
883
|
-
|
|
884
|
-
|
|
912
|
+
try {
|
|
913
|
+
await setApiKey(result.apiKey);
|
|
914
|
+
ctx.app.notify('Logged in successfully');
|
|
915
|
+
}
|
|
916
|
+
catch {
|
|
917
|
+
ctx.app.notify('Could not save the API key (secure storage unavailable).');
|
|
918
|
+
}
|
|
885
919
|
}
|
|
886
920
|
});
|
|
887
921
|
break;
|
|
@@ -901,11 +935,11 @@ Format: use headers per category, only include categories where you found issues
|
|
|
901
935
|
return;
|
|
902
936
|
if (result === 'all') {
|
|
903
937
|
for (const p of configuredProviders)
|
|
904
|
-
clearApiKey(p.id);
|
|
938
|
+
void clearApiKey(p.id);
|
|
905
939
|
ctx.app.notify('Logged out from all providers. Use /login to sign in.');
|
|
906
940
|
}
|
|
907
941
|
else {
|
|
908
|
-
clearApiKey(result);
|
|
942
|
+
void clearApiKey(result);
|
|
909
943
|
const provider = configuredProviders.find(p => p.id === result);
|
|
910
944
|
ctx.app.notify(`Logged out from ${provider?.name || result}`);
|
|
911
945
|
if (result === currentProvider.id) {
|
package/dist/renderer/main.js
CHANGED
|
@@ -278,7 +278,14 @@ async function showLoginFlow() {
|
|
|
278
278
|
renderCurrentStep();
|
|
279
279
|
return;
|
|
280
280
|
}
|
|
281
|
-
|
|
281
|
+
try {
|
|
282
|
+
await setApiKey(key);
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
loginError = 'Could not save the API key (secure storage unavailable). Please try again.';
|
|
286
|
+
renderCurrentStep();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
282
289
|
cleanup();
|
|
283
290
|
resolve(key);
|
|
284
291
|
},
|
|
@@ -414,11 +421,15 @@ Commands (in chat):
|
|
|
414
421
|
if (sub === 'sync' || sub === 'pull') {
|
|
415
422
|
// Pull API keys from codeep.dev and save to local config
|
|
416
423
|
const { pullKeys } = await import('../utils/codeepCloud.js');
|
|
417
|
-
const { getSyncToken, setApiKey } = await import('../config/index.js');
|
|
424
|
+
const { getSyncToken, setApiKey, loadAllApiKeys: loadKeys } = await import('../config/index.js');
|
|
418
425
|
if (!getSyncToken()) {
|
|
419
426
|
console.log('\n Not linked to codeep.dev. Run: codeep account\n');
|
|
420
427
|
process.exit(1);
|
|
421
428
|
}
|
|
429
|
+
// Run the one-time plaintext->keychain migration BEFORE storing any pulled
|
|
430
|
+
// key. Otherwise the first setApiKey flips keysSecured=true and any local
|
|
431
|
+
// legacy plaintext keys would never migrate (orphaned, invisible).
|
|
432
|
+
await loadKeys();
|
|
422
433
|
process.stdout.write(' Pulling keys from codeep.dev...');
|
|
423
434
|
const keys = await pullKeys();
|
|
424
435
|
if (!keys) {
|
|
@@ -430,10 +441,17 @@ Commands (in chat):
|
|
|
430
441
|
console.log(' no keys found.\n Add keys at codeep.dev/dashboard');
|
|
431
442
|
}
|
|
432
443
|
else {
|
|
444
|
+
let synced = 0;
|
|
433
445
|
for (const [provider, key] of Object.entries(keys)) {
|
|
434
|
-
|
|
446
|
+
try {
|
|
447
|
+
await setApiKey(key, provider);
|
|
448
|
+
synced++;
|
|
449
|
+
}
|
|
450
|
+
catch {
|
|
451
|
+
console.log(`\n Warning: could not securely store the key for ${provider}.`);
|
|
452
|
+
}
|
|
435
453
|
}
|
|
436
|
-
console.log(` synced ${
|
|
454
|
+
console.log(` synced ${synced} key${synced !== 1 ? 's' : ''}.`);
|
|
437
455
|
}
|
|
438
456
|
// Also pull portable personal config — personalities + custom commands +
|
|
439
457
|
// the user profile. Additive merge (never clobbers local files).
|
package/dist/utils/agent.d.ts
CHANGED
|
@@ -13,6 +13,15 @@ import { undoLastAction, undoAllActions, getCurrentSession, getRecentSessions, f
|
|
|
13
13
|
import { VerifyResult } from './verify';
|
|
14
14
|
import { TaskPlan, SubTask } from './taskPlanner';
|
|
15
15
|
export type PermissionOutcome = 'allow_once' | 'allow_always' | 'reject_once' | 'reject_always';
|
|
16
|
+
export type PermissionDecision = 'allow-once' | 'allow-always' | 'deny-once' | 'deny-always';
|
|
17
|
+
/**
|
|
18
|
+
* Map a permission outcome to a decision, FAILING CLOSED: a dangerous tool is
|
|
19
|
+
* allowed only on an explicit allow outcome. `reject_*` deny, and — critically —
|
|
20
|
+
* any unknown/malformed outcome from a buggy or hostile client also denies
|
|
21
|
+
* (deny-once) rather than slipping through to execution. Pure + exported so the
|
|
22
|
+
* invariant is unit-tested independently of the agent loop.
|
|
23
|
+
*/
|
|
24
|
+
export declare function classifyPermissionOutcome(outcome: string | undefined | null): PermissionDecision;
|
|
16
25
|
export interface AgentOptions {
|
|
17
26
|
maxIterations: number;
|
|
18
27
|
maxDuration: number;
|
package/dist/utils/agent.js
CHANGED
|
@@ -103,6 +103,22 @@ function compressMessages(messages, actions) {
|
|
|
103
103
|
debug(`Context compressed: ${totalChars} chars → keeping first + summary + last ${keep} messages`);
|
|
104
104
|
return [firstMessage, summaryMessage, ...recentMessages];
|
|
105
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* Map a permission outcome to a decision, FAILING CLOSED: a dangerous tool is
|
|
108
|
+
* allowed only on an explicit allow outcome. `reject_*` deny, and — critically —
|
|
109
|
+
* any unknown/malformed outcome from a buggy or hostile client also denies
|
|
110
|
+
* (deny-once) rather than slipping through to execution. Pure + exported so the
|
|
111
|
+
* invariant is unit-tested independently of the agent loop.
|
|
112
|
+
*/
|
|
113
|
+
export function classifyPermissionOutcome(outcome) {
|
|
114
|
+
if (outcome === 'allow_always')
|
|
115
|
+
return 'allow-always';
|
|
116
|
+
if (outcome === 'allow_once')
|
|
117
|
+
return 'allow-once';
|
|
118
|
+
if (outcome === 'reject_always')
|
|
119
|
+
return 'deny-always';
|
|
120
|
+
return 'deny-once'; // 'reject_once' OR anything unexpected → fail closed
|
|
121
|
+
}
|
|
106
122
|
/**
|
|
107
123
|
* Build the result for a run that paused at a safety limit. Pausing is a normal,
|
|
108
124
|
* resumable state — not an error — so the summary tells the user how to resume.
|
|
@@ -779,15 +795,18 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
779
795
|
continue;
|
|
780
796
|
}
|
|
781
797
|
const outcome = await opts.onRequestPermission(toolCall);
|
|
782
|
-
|
|
798
|
+
// Fail CLOSED: allow ONLY on an explicit allow outcome; reject_* and
|
|
799
|
+
// any malformed/unknown outcome deny (see classifyPermissionOutcome).
|
|
800
|
+
const decision = classifyPermissionOutcome(outcome);
|
|
801
|
+
if (decision === 'allow-always') {
|
|
783
802
|
alwaysAllowedTools.add(toolCall.tool);
|
|
784
803
|
}
|
|
785
|
-
else if (
|
|
786
|
-
|
|
787
|
-
rejectResult();
|
|
788
|
-
continue;
|
|
804
|
+
else if (decision === 'allow-once') {
|
|
805
|
+
// proceed this once
|
|
789
806
|
}
|
|
790
|
-
else
|
|
807
|
+
else {
|
|
808
|
+
if (decision === 'deny-always')
|
|
809
|
+
alwaysRejectedTools.add(toolCall.tool);
|
|
791
810
|
rejectResult();
|
|
792
811
|
continue;
|
|
793
812
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { randomBytes, createHash } from 'crypto';
|
|
9
9
|
import { spawn } from 'child_process';
|
|
10
|
-
import { getGithubId, getSyncToken, setGithubAccount, setSyncToken, getDeviceId } from '../config/index.js';
|
|
10
|
+
import { getGithubId, getSyncToken, setGithubAccount, setSyncToken, getDeviceId, isTelemetryEnabled } from '../config/index.js';
|
|
11
11
|
import { hostname } from 'os';
|
|
12
12
|
const API_BASE = 'https://codeep.dev';
|
|
13
13
|
const POLL_INTERVAL_MS = 2000;
|
|
@@ -110,6 +110,8 @@ export function generateProjectId(projectRoot) {
|
|
|
110
110
|
* Retries up to 2 times on network errors or 5xx responses.
|
|
111
111
|
*/
|
|
112
112
|
export function reportStats(payload) {
|
|
113
|
+
if (!isTelemetryEnabled())
|
|
114
|
+
return; // user opted out of automatic uploads
|
|
113
115
|
const githubId = getGithubId();
|
|
114
116
|
if (!githubId)
|
|
115
117
|
return; // not linked, skip silently
|
|
@@ -124,6 +126,8 @@ export function reportStats(payload) {
|
|
|
124
126
|
}).catch(() => { });
|
|
125
127
|
}
|
|
126
128
|
export async function reportStatsAsync(payload) {
|
|
129
|
+
if (!isTelemetryEnabled())
|
|
130
|
+
return; // user opted out of automatic uploads
|
|
127
131
|
const githubId = getGithubId();
|
|
128
132
|
if (!githubId)
|
|
129
133
|
return;
|
|
@@ -320,6 +324,8 @@ export const pushCommands = () => pushBundle('commands');
|
|
|
320
324
|
* Fire-and-forget. Only sends if linked and sync_token is available.
|
|
321
325
|
*/
|
|
322
326
|
export function syncSession(payload) {
|
|
327
|
+
if (!isTelemetryEnabled())
|
|
328
|
+
return; // user opted out of conversation/session upload
|
|
323
329
|
const githubId = getGithubId();
|
|
324
330
|
const syncToken = getSyncToken();
|
|
325
331
|
if (!githubId || !syncToken)
|
|
@@ -335,6 +341,8 @@ export function syncSession(payload) {
|
|
|
335
341
|
}).catch(() => { });
|
|
336
342
|
}
|
|
337
343
|
export async function syncSessionAsync(payload) {
|
|
344
|
+
if (!isTelemetryEnabled())
|
|
345
|
+
return; // user opted out of conversation/session upload
|
|
338
346
|
const githubId = getGithubId();
|
|
339
347
|
const syncToken = getSyncToken();
|
|
340
348
|
if (!githubId || !syncToken)
|
|
@@ -354,6 +362,8 @@ export async function syncSessionAsync(payload) {
|
|
|
354
362
|
* Fire-and-forget. Only sends if linked (githubId + syncToken).
|
|
355
363
|
*/
|
|
356
364
|
export function syncProgress(payload) {
|
|
365
|
+
if (!isTelemetryEnabled())
|
|
366
|
+
return; // user opted out of automatic uploads
|
|
357
367
|
const githubId = getGithubId();
|
|
358
368
|
const syncToken = getSyncToken();
|
|
359
369
|
if (!githubId || !syncToken)
|
|
@@ -483,6 +493,8 @@ export async function pullUserProfile() {
|
|
|
483
493
|
}
|
|
484
494
|
}
|
|
485
495
|
export async function syncMemoryNotes(projectName, notes) {
|
|
496
|
+
if (!isTelemetryEnabled())
|
|
497
|
+
return; // user opted out of automatic uploads
|
|
486
498
|
const syncToken = getSyncToken();
|
|
487
499
|
if (!syncToken)
|
|
488
500
|
return;
|
package/dist/utils/keychain.js
CHANGED
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { logger } from './logger.js';
|
|
2
|
-
// keytar is a native addon — load
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
// keytar is a native addon — load it LAZILY (on first use), never at module
|
|
3
|
+
// top level. A top-level `await import()` here gives the module a top-level
|
|
4
|
+
// await, which makes `bun build --compile` reject any CommonJS require() that
|
|
5
|
+
// transitively depends on this file (renderer/main.js → codeepCloud → config →
|
|
6
|
+
// keychain). Lazy loading keeps the module side-effect-free at import time.
|
|
7
|
+
let _keytar = null;
|
|
8
|
+
let _keytarTried = false;
|
|
9
|
+
async function loadKeytar() {
|
|
10
|
+
if (!_keytarTried) {
|
|
11
|
+
_keytarTried = true;
|
|
12
|
+
try {
|
|
13
|
+
_keytar = (await import('keytar')).default;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
_keytar = null; /* native addon unavailable */
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return _keytar;
|
|
6
20
|
}
|
|
7
|
-
catch { /* native addon unavailable */ }
|
|
8
21
|
const SERVICE_NAME = 'codeep';
|
|
9
22
|
class KeychainStorage {
|
|
10
23
|
getAccountName(providerId) {
|
|
@@ -12,9 +25,11 @@ class KeychainStorage {
|
|
|
12
25
|
}
|
|
13
26
|
async getApiKey(providerId) {
|
|
14
27
|
try {
|
|
28
|
+
const kt = await loadKeytar();
|
|
29
|
+
if (!kt)
|
|
30
|
+
return null;
|
|
15
31
|
const account = this.getAccountName(providerId);
|
|
16
|
-
|
|
17
|
-
return password;
|
|
32
|
+
return await kt.getPassword(SERVICE_NAME, account);
|
|
18
33
|
}
|
|
19
34
|
catch (error) {
|
|
20
35
|
logger.debug(`Failed to get API key from keychain: ${error}`);
|
|
@@ -23,8 +38,11 @@ class KeychainStorage {
|
|
|
23
38
|
}
|
|
24
39
|
async setApiKey(providerId, apiKey) {
|
|
25
40
|
try {
|
|
41
|
+
const kt = await loadKeytar();
|
|
42
|
+
if (!kt)
|
|
43
|
+
throw new Error('keytar unavailable');
|
|
26
44
|
const account = this.getAccountName(providerId);
|
|
27
|
-
await
|
|
45
|
+
await kt.setPassword(SERVICE_NAME, account, apiKey);
|
|
28
46
|
}
|
|
29
47
|
catch (error) {
|
|
30
48
|
throw new Error(`Failed to store API key in keychain: ${error}`);
|
|
@@ -32,8 +50,11 @@ class KeychainStorage {
|
|
|
32
50
|
}
|
|
33
51
|
async deleteApiKey(providerId) {
|
|
34
52
|
try {
|
|
53
|
+
const kt = await loadKeytar();
|
|
54
|
+
if (!kt)
|
|
55
|
+
return;
|
|
35
56
|
const account = this.getAccountName(providerId);
|
|
36
|
-
await
|
|
57
|
+
await kt.deletePassword(SERVICE_NAME, account);
|
|
37
58
|
}
|
|
38
59
|
catch (error) {
|
|
39
60
|
logger.debug(`Failed to delete API key from keychain: ${error}`);
|
|
@@ -89,10 +110,11 @@ class SmartStorage {
|
|
|
89
110
|
return;
|
|
90
111
|
try {
|
|
91
112
|
const testKey = '__codeep_test__';
|
|
92
|
-
|
|
113
|
+
const kt = await loadKeytar();
|
|
114
|
+
if (!kt)
|
|
93
115
|
throw new Error('keytar unavailable');
|
|
94
|
-
await
|
|
95
|
-
await
|
|
116
|
+
await kt.setPassword(SERVICE_NAME, testKey, 'test');
|
|
117
|
+
await kt.deletePassword(SERVICE_NAME, testKey);
|
|
96
118
|
this.useKeychain = true;
|
|
97
119
|
}
|
|
98
120
|
catch {
|
|
@@ -126,6 +126,74 @@ export function saveProjectIntelligence(projectPath, intelligence) {
|
|
|
126
126
|
return false;
|
|
127
127
|
}
|
|
128
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Complete default intelligence skeleton — every nested section is present so
|
|
131
|
+
* consumers (e.g. generateContextFromIntelligence) never dereference an
|
|
132
|
+
* undefined section. Used as the merge base when normalizing a loaded file.
|
|
133
|
+
*/
|
|
134
|
+
function createBaseIntelligence(projectPath) {
|
|
135
|
+
return {
|
|
136
|
+
version: INTELLIGENCE_VERSION,
|
|
137
|
+
scannedAt: new Date().toISOString(),
|
|
138
|
+
projectPath,
|
|
139
|
+
name: basename(projectPath),
|
|
140
|
+
type: 'Unknown',
|
|
141
|
+
description: '',
|
|
142
|
+
structure: {
|
|
143
|
+
totalFiles: 0,
|
|
144
|
+
totalDirectories: 0,
|
|
145
|
+
languages: {},
|
|
146
|
+
topDirectories: [],
|
|
147
|
+
},
|
|
148
|
+
dependencies: {
|
|
149
|
+
runtime: [],
|
|
150
|
+
dev: [],
|
|
151
|
+
frameworks: [],
|
|
152
|
+
},
|
|
153
|
+
keyFiles: [],
|
|
154
|
+
entryPoints: [],
|
|
155
|
+
scripts: {},
|
|
156
|
+
architecture: {
|
|
157
|
+
patterns: [],
|
|
158
|
+
mainModules: [],
|
|
159
|
+
ciSystem: null,
|
|
160
|
+
containerization: [],
|
|
161
|
+
monorepoTool: null,
|
|
162
|
+
},
|
|
163
|
+
conventions: {
|
|
164
|
+
indentation: 'spaces',
|
|
165
|
+
quotes: 'single',
|
|
166
|
+
semicolons: true,
|
|
167
|
+
namingStyle: 'camelCase',
|
|
168
|
+
},
|
|
169
|
+
testing: {
|
|
170
|
+
framework: null,
|
|
171
|
+
testDirectory: null,
|
|
172
|
+
hasTests: false,
|
|
173
|
+
},
|
|
174
|
+
notes: [],
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Merge a loaded (possibly partial or older-schema) intelligence object over
|
|
179
|
+
* the complete default skeleton so every nested section is guaranteed present.
|
|
180
|
+
* An interrupted scan or an older CLI can write a file that is missing whole
|
|
181
|
+
* sections (e.g. `conventions`); without this, reading `conventions.indentation`
|
|
182
|
+
* downstream throws "Cannot read properties of undefined". Nested objects are
|
|
183
|
+
* merged per-section so partial sub-objects are also backfilled.
|
|
184
|
+
*/
|
|
185
|
+
function normalizeIntelligence(data, projectPath) {
|
|
186
|
+
const base = createBaseIntelligence(projectPath);
|
|
187
|
+
return {
|
|
188
|
+
...base,
|
|
189
|
+
...data,
|
|
190
|
+
structure: { ...base.structure, ...data.structure },
|
|
191
|
+
dependencies: { ...base.dependencies, ...data.dependencies },
|
|
192
|
+
architecture: { ...base.architecture, ...data.architecture },
|
|
193
|
+
conventions: { ...base.conventions, ...data.conventions },
|
|
194
|
+
testing: { ...base.testing, ...data.testing },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
129
197
|
/**
|
|
130
198
|
* Load intelligence from .codeep/intelligence.json
|
|
131
199
|
*/
|
|
@@ -135,9 +203,13 @@ export function loadProjectIntelligence(projectPath) {
|
|
|
135
203
|
if (!existsSync(filePath))
|
|
136
204
|
return null;
|
|
137
205
|
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
206
|
+
if (!data || typeof data !== 'object')
|
|
207
|
+
return null;
|
|
138
208
|
if (data.version !== INTELLIGENCE_VERSION)
|
|
139
209
|
return null;
|
|
140
|
-
|
|
210
|
+
// Backfill any missing sections so a partial/interrupted file can't crash
|
|
211
|
+
// downstream consumers that assume a complete shape.
|
|
212
|
+
return normalizeIntelligence(data, projectPath);
|
|
141
213
|
}
|
|
142
214
|
catch {
|
|
143
215
|
return null;
|
|
@@ -159,6 +231,10 @@ export function isIntelligenceFresh(projectPath, maxAgeHours = 24) {
|
|
|
159
231
|
* Generate AI-friendly context from intelligence
|
|
160
232
|
*/
|
|
161
233
|
export function generateContextFromIntelligence(intelligence) {
|
|
234
|
+
// Defensive: backfill any missing sections so a partial object (e.g. from an
|
|
235
|
+
// older/interrupted scan, or an external SDK caller) can't crash the
|
|
236
|
+
// formatter on a nested dereference like `conventions.indentation`.
|
|
237
|
+
intelligence = normalizeIntelligence((intelligence ?? {}), intelligence?.projectPath ?? '');
|
|
162
238
|
const lines = [];
|
|
163
239
|
lines.push(`# Project: ${intelligence.name}`);
|
|
164
240
|
lines.push(`Type: ${intelligence.type}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "2.5.
|
|
3
|
+
"version": "2.5.2",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|