codeep 2.14.0 → 2.15.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 +35 -24
- package/dist/acp/commands.js +22 -1
- package/dist/acp/server.js +13 -2
- package/dist/config/index.d.ts +10 -0
- package/dist/config/index.js +2 -2
- package/dist/config/providers.js +15 -10
- package/dist/renderer/App.d.ts +0 -30
- package/dist/renderer/App.js +149 -659
- package/dist/renderer/agentExecution.d.ts +1 -0
- package/dist/renderer/agentExecution.js +3 -2
- package/dist/renderer/commands/helpers.d.ts +63 -0
- package/dist/renderer/commands/helpers.js +108 -0
- package/dist/renderer/commands/registry.js +5 -0
- package/dist/renderer/commands.d.ts +4 -0
- package/dist/renderer/commands.js +179 -63
- package/dist/renderer/components/ActionFormatting.d.ts +17 -0
- package/dist/renderer/components/ActionFormatting.js +67 -0
- package/dist/renderer/components/Autocomplete.d.ts +33 -0
- package/dist/renderer/components/Autocomplete.js +40 -0
- package/dist/renderer/components/Intro.d.ts +9 -0
- package/dist/renderer/components/Intro.js +5 -15
- package/dist/renderer/components/MessageFormatter.d.ts +96 -0
- package/dist/renderer/components/MessageFormatter.js +375 -0
- package/dist/renderer/components/Permission.d.ts +4 -0
- package/dist/renderer/components/Permission.js +1 -1
- package/dist/renderer/components/Status.d.ts +4 -0
- package/dist/renderer/components/Status.js +2 -3
- package/dist/renderer/components/WelcomeFormatter.d.ts +19 -0
- package/dist/renderer/components/WelcomeFormatter.js +79 -0
- package/dist/renderer/components/uiConstants.d.ts +8 -0
- package/dist/renderer/components/uiConstants.js +24 -0
- package/dist/renderer/inputParsing.d.ts +22 -0
- package/dist/renderer/inputParsing.js +28 -0
- package/dist/renderer/layout.d.ts +215 -0
- package/dist/renderer/layout.js +326 -0
- package/dist/renderer/main.d.ts +2 -1
- package/dist/renderer/main.js +45 -10
- package/dist/renderer/ollamaHint.d.ts +12 -0
- package/dist/renderer/ollamaHint.js +29 -0
- package/dist/utils/agentChat.js +23 -1
- package/dist/utils/codeepCloud.d.ts +54 -0
- package/dist/utils/codeepCloud.js +95 -0
- package/dist/utils/export.d.ts +12 -0
- package/dist/utils/export.js +3 -3
- package/dist/utils/hooks.d.ts +26 -0
- package/dist/utils/hooks.js +69 -1
- package/dist/utils/keychain.js +45 -29
- package/dist/utils/logger.d.ts +12 -0
- package/dist/utils/logger.js +1 -1
- package/dist/utils/mcpConfig.d.ts +26 -0
- package/dist/utils/mcpConfig.js +109 -4
- package/dist/utils/skillBundles.d.ts +14 -0
- package/dist/utils/skillBundles.js +3 -3
- package/dist/utils/skillBundlesCloud.d.ts +7 -0
- package/dist/utils/skillBundlesCloud.js +1 -1
- package/dist/utils/tokenTracker.js +12 -2
- package/dist/utils/toolParsing.d.ts +11 -0
- package/dist/utils/toolParsing.js +6 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +2 -2
package/dist/utils/export.d.ts
CHANGED
|
@@ -5,6 +5,18 @@ export interface ExportOptions {
|
|
|
5
5
|
sessionName?: string;
|
|
6
6
|
timestamp?: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Export messages to Markdown format
|
|
10
|
+
*/
|
|
11
|
+
export declare function exportToMarkdown(messages: Message[], sessionName?: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* Export messages to JSON format
|
|
14
|
+
*/
|
|
15
|
+
export declare function exportToJson(messages: Message[], sessionName?: string): string;
|
|
16
|
+
/**
|
|
17
|
+
* Export messages to plain text format
|
|
18
|
+
*/
|
|
19
|
+
export declare function exportToText(messages: Message[], sessionName?: string): string;
|
|
8
20
|
/**
|
|
9
21
|
* Export messages to specified format
|
|
10
22
|
*/
|
package/dist/utils/export.js
CHANGED
|
@@ -3,7 +3,7 @@ import { join } from 'path';
|
|
|
3
3
|
/**
|
|
4
4
|
* Export messages to Markdown format
|
|
5
5
|
*/
|
|
6
|
-
function exportToMarkdown(messages, sessionName) {
|
|
6
|
+
export function exportToMarkdown(messages, sessionName) {
|
|
7
7
|
const timestamp = new Date().toLocaleString('hr-HR');
|
|
8
8
|
let markdown = `# Codeep Chat Export\n\n`;
|
|
9
9
|
if (sessionName) {
|
|
@@ -24,7 +24,7 @@ function exportToMarkdown(messages, sessionName) {
|
|
|
24
24
|
/**
|
|
25
25
|
* Export messages to JSON format
|
|
26
26
|
*/
|
|
27
|
-
function exportToJson(messages, sessionName) {
|
|
27
|
+
export function exportToJson(messages, sessionName) {
|
|
28
28
|
const exportData = {
|
|
29
29
|
session: sessionName || 'Unnamed',
|
|
30
30
|
exportedAt: new Date().toISOString(),
|
|
@@ -36,7 +36,7 @@ function exportToJson(messages, sessionName) {
|
|
|
36
36
|
/**
|
|
37
37
|
* Export messages to plain text format
|
|
38
38
|
*/
|
|
39
|
-
function exportToText(messages, sessionName) {
|
|
39
|
+
export function exportToText(messages, sessionName) {
|
|
40
40
|
const timestamp = new Date().toLocaleString('hr-HR');
|
|
41
41
|
let text = `Codeep Chat Export\n`;
|
|
42
42
|
text += `===================\n\n`;
|
package/dist/utils/hooks.d.ts
CHANGED
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
* machine the first time they trigger an agent tool call. The welcome
|
|
45
45
|
* banner warns when hooks exist (see `summarizeHooks`); we do not run
|
|
46
46
|
* hooks from `~/.codeep/hooks/` (global) for that reason.
|
|
47
|
+
*
|
|
48
|
+
* Platform note: hooks are POSIX shell (`.sh`) scripts. On macOS/Linux they
|
|
49
|
+
* run directly; on Windows they run through Git Bash's `sh` if installed.
|
|
50
|
+
* Windows without a POSIX shell → hooks are reported `unsupported` and skipped
|
|
51
|
+
* (never blocking). See `resolveShellMode` and the README "Windows notes".
|
|
47
52
|
*/
|
|
48
53
|
export declare function isHooksTrusted(workspaceRoot: string): boolean;
|
|
49
54
|
export declare function trustWorkspaceHooks(workspaceRoot: string): void;
|
|
@@ -75,7 +80,28 @@ export interface HookResult {
|
|
|
75
80
|
/** True when a hook script exists but the workspace isn't trusted, so it was
|
|
76
81
|
* skipped (not run). Lets callers surface "run /hooks trust to enable". */
|
|
77
82
|
untrusted?: boolean;
|
|
83
|
+
/** True when a hook script exists but this OS can't run it — Codeep hooks are
|
|
84
|
+
* POSIX shell (`.sh`) scripts and no `sh` was found (e.g. Windows without
|
|
85
|
+
* Git Bash). A non-blocking skip; surfaced by `/hooks` + the welcome banner. */
|
|
86
|
+
unsupported?: boolean;
|
|
78
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Decide how a `.sh` hook runs on this platform. On POSIX it's executed
|
|
90
|
+
* directly (shebang); on Windows it needs a `sh`/`bash` interpreter, and if
|
|
91
|
+
* none is installed hooks are `unsupported` (skipped, never blocking). Pure +
|
|
92
|
+
* injectable so the platform matrix is unit-testable.
|
|
93
|
+
*/
|
|
94
|
+
export declare function resolveShellMode(platform?: NodeJS.Platform, findShell?: () => string | null): {
|
|
95
|
+
mode: 'direct';
|
|
96
|
+
} | {
|
|
97
|
+
mode: 'shell';
|
|
98
|
+
shell: string;
|
|
99
|
+
} | {
|
|
100
|
+
mode: 'unsupported';
|
|
101
|
+
};
|
|
102
|
+
/** True when this OS can actually run `.sh` hooks. Drives the `/hooks` and
|
|
103
|
+
* welcome-banner "unsupported" state. */
|
|
104
|
+
export declare function hooksExecutable(platform?: NodeJS.Platform, findShell?: () => string | null): boolean;
|
|
79
105
|
/**
|
|
80
106
|
* Execute the configured hook for an event, if any. Returns `executed: false`
|
|
81
107
|
* if no script exists. Caller is responsible for checking `blocked` and
|
package/dist/utils/hooks.js
CHANGED
|
@@ -44,6 +44,11 @@
|
|
|
44
44
|
* machine the first time they trigger an agent tool call. The welcome
|
|
45
45
|
* banner warns when hooks exist (see `summarizeHooks`); we do not run
|
|
46
46
|
* hooks from `~/.codeep/hooks/` (global) for that reason.
|
|
47
|
+
*
|
|
48
|
+
* Platform note: hooks are POSIX shell (`.sh`) scripts. On macOS/Linux they
|
|
49
|
+
* run directly; on Windows they run through Git Bash's `sh` if installed.
|
|
50
|
+
* Windows without a POSIX shell → hooks are reported `unsupported` and skipped
|
|
51
|
+
* (never blocking). See `resolveShellMode` and the README "Windows notes".
|
|
47
52
|
*/
|
|
48
53
|
import { existsSync, readdirSync, statSync, accessSync, constants } from 'fs';
|
|
49
54
|
import { join } from 'path';
|
|
@@ -80,6 +85,45 @@ const NOT_EXECUTED = { executed: false, exitCode: 0, stdout: '', stderr: '', blo
|
|
|
80
85
|
function getHooksDir(workspaceRoot) {
|
|
81
86
|
return join(workspaceRoot, '.codeep', 'hooks');
|
|
82
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Locate a POSIX shell able to run `.sh` hooks on Windows. `.sh` scripts can't
|
|
90
|
+
* be `spawn`ed directly there (no shebang support), so we invoke them through
|
|
91
|
+
* `sh`/`bash`. Scans PATH plus the default Git-for-Windows install locations.
|
|
92
|
+
* Returns the shell's path, or null if none is installed.
|
|
93
|
+
*/
|
|
94
|
+
function findWindowsPosixShell() {
|
|
95
|
+
const candidates = [];
|
|
96
|
+
for (const dir of (process.env.PATH ?? '').split(';')) {
|
|
97
|
+
if (dir)
|
|
98
|
+
candidates.push(join(dir, 'sh.exe'), join(dir, 'bash.exe'));
|
|
99
|
+
}
|
|
100
|
+
candidates.push('C:\\Program Files\\Git\\bin\\sh.exe', 'C:\\Program Files\\Git\\usr\\bin\\sh.exe', 'C:\\Program Files (x86)\\Git\\bin\\sh.exe');
|
|
101
|
+
for (const c of candidates) {
|
|
102
|
+
try {
|
|
103
|
+
if (statSync(c).isFile())
|
|
104
|
+
return c;
|
|
105
|
+
}
|
|
106
|
+
catch { /* next candidate */ }
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Decide how a `.sh` hook runs on this platform. On POSIX it's executed
|
|
112
|
+
* directly (shebang); on Windows it needs a `sh`/`bash` interpreter, and if
|
|
113
|
+
* none is installed hooks are `unsupported` (skipped, never blocking). Pure +
|
|
114
|
+
* injectable so the platform matrix is unit-testable.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveShellMode(platform = process.platform, findShell = findWindowsPosixShell) {
|
|
117
|
+
if (platform !== 'win32')
|
|
118
|
+
return { mode: 'direct' };
|
|
119
|
+
const shell = findShell();
|
|
120
|
+
return shell ? { mode: 'shell', shell } : { mode: 'unsupported' };
|
|
121
|
+
}
|
|
122
|
+
/** True when this OS can actually run `.sh` hooks. Drives the `/hooks` and
|
|
123
|
+
* welcome-banner "unsupported" state. */
|
|
124
|
+
export function hooksExecutable(platform = process.platform, findShell = findWindowsPosixShell) {
|
|
125
|
+
return resolveShellMode(platform, findShell).mode !== 'unsupported';
|
|
126
|
+
}
|
|
83
127
|
function findHookScript(workspaceRoot, event) {
|
|
84
128
|
const dir = getHooksDir(workspaceRoot);
|
|
85
129
|
if (!existsSync(dir))
|
|
@@ -120,6 +164,14 @@ export function runHook(ctx, opts = {}) {
|
|
|
120
164
|
if (!isHooksTrusted(ctx.workspaceRoot)) {
|
|
121
165
|
return { executed: false, exitCode: 0, stdout: '', stderr: '', blocked: false, untrusted: true, scriptPath: script };
|
|
122
166
|
}
|
|
167
|
+
// Platform gate: a `.sh` hook needs a POSIX shell. On Windows without one we
|
|
168
|
+
// must NOT spawn the script directly — that fails, and for a blocking event
|
|
169
|
+
// (pre_tool_call / pre_commit) a spawn error would wedge every tool call.
|
|
170
|
+
// Skip cleanly instead and let `/hooks` explain why.
|
|
171
|
+
const shellMode = resolveShellMode();
|
|
172
|
+
if (shellMode.mode === 'unsupported') {
|
|
173
|
+
return { executed: false, exitCode: 0, stdout: '', stderr: '', blocked: false, unsupported: true, scriptPath: script };
|
|
174
|
+
}
|
|
123
175
|
const env = {
|
|
124
176
|
...process.env,
|
|
125
177
|
CODEEP_HOOK_EVENT: ctx.event,
|
|
@@ -146,9 +198,12 @@ export function runHook(ctx, opts = {}) {
|
|
|
146
198
|
// 30s ceiling so a runaway lint / test command can't wedge the agent
|
|
147
199
|
// loop. Configurable per-call so tests can use a tight timeout.
|
|
148
200
|
const timeout = opts.timeoutMs ?? 30_000;
|
|
201
|
+
// POSIX: run the script directly (shebang). Windows-with-shell: invoke it
|
|
202
|
+
// through the located `sh`/`bash` so the shebang isn't required.
|
|
203
|
+
const [cmd, cmdArgs] = shellMode.mode === 'shell' ? [shellMode.shell, [script]] : [script, []];
|
|
149
204
|
let proc;
|
|
150
205
|
try {
|
|
151
|
-
proc = spawnSync(
|
|
206
|
+
proc = spawnSync(cmd, cmdArgs, {
|
|
152
207
|
cwd: ctx.workspaceRoot,
|
|
153
208
|
env,
|
|
154
209
|
timeout,
|
|
@@ -250,6 +305,16 @@ export function formatHookTrust(workspaceRoot) {
|
|
|
250
305
|
const hooks = listInstalledHooks(workspaceRoot);
|
|
251
306
|
if (hooks.length === 0)
|
|
252
307
|
return '';
|
|
308
|
+
if (!hooksExecutable()) {
|
|
309
|
+
return [
|
|
310
|
+
'⚠️ These hooks **cannot run on this system.** Codeep hooks are POSIX shell',
|
|
311
|
+
'(`.sh`) scripts, and no `sh` was found — on Windows this means Git Bash',
|
|
312
|
+
"isn't installed or isn't on your PATH.",
|
|
313
|
+
'',
|
|
314
|
+
'Install [Git for Windows](https://git-scm.com/download/win) (it provides',
|
|
315
|
+
'`sh.exe`) or add a POSIX shell to PATH. See the “Windows notes” in the README.',
|
|
316
|
+
].join('\n');
|
|
317
|
+
}
|
|
253
318
|
if (isHooksTrusted(workspaceRoot)) {
|
|
254
319
|
return '✓ This workspace is **trusted** — its hooks will run. Use `/hooks untrust` to revoke.';
|
|
255
320
|
}
|
|
@@ -267,6 +332,9 @@ export function summarizeHooks(workspaceRoot) {
|
|
|
267
332
|
if (hooks.length === 0)
|
|
268
333
|
return '';
|
|
269
334
|
const list = hooks.map(h => h.event).join(', ');
|
|
335
|
+
if (!hooksExecutable()) {
|
|
336
|
+
return `${hooks.length} hook${hooks.length === 1 ? '' : 's'} present but this system has no POSIX shell — they won't run (see README “Windows notes”) (${list})`;
|
|
337
|
+
}
|
|
270
338
|
if (!isHooksTrusted(workspaceRoot)) {
|
|
271
339
|
return `${hooks.length} hook${hooks.length === 1 ? '' : 's'} present but NOT trusted — run /hooks trust to enable (${list})`;
|
|
272
340
|
}
|
package/dist/utils/keychain.js
CHANGED
|
@@ -1,22 +1,17 @@
|
|
|
1
1
|
import { logger } from './logger.js';
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
let _keytar = null;
|
|
8
|
-
let _keytarTried = false;
|
|
9
|
-
async function loadKeytar() {
|
|
10
|
-
if (!_keytarTried) {
|
|
11
|
-
_keytarTried = true;
|
|
2
|
+
let _keyring = null;
|
|
3
|
+
let _keyringTried = false;
|
|
4
|
+
async function loadKeyring() {
|
|
5
|
+
if (!_keyringTried) {
|
|
6
|
+
_keyringTried = true;
|
|
12
7
|
try {
|
|
13
|
-
|
|
8
|
+
_keyring = (await import('@napi-rs/keyring'));
|
|
14
9
|
}
|
|
15
10
|
catch {
|
|
16
|
-
|
|
11
|
+
_keyring = null; /* native binary unavailable (headless / minimal install) */
|
|
17
12
|
}
|
|
18
13
|
}
|
|
19
|
-
return
|
|
14
|
+
return _keyring;
|
|
20
15
|
}
|
|
21
16
|
const SERVICE_NAME = 'codeep';
|
|
22
17
|
class KeychainStorage {
|
|
@@ -25,11 +20,12 @@ class KeychainStorage {
|
|
|
25
20
|
}
|
|
26
21
|
async getApiKey(providerId) {
|
|
27
22
|
try {
|
|
28
|
-
const
|
|
29
|
-
if (!
|
|
23
|
+
const kr = await loadKeyring();
|
|
24
|
+
if (!kr)
|
|
30
25
|
return null;
|
|
31
26
|
const account = this.getAccountName(providerId);
|
|
32
|
-
|
|
27
|
+
const entry = new kr.AsyncEntry(SERVICE_NAME, account);
|
|
28
|
+
return await entry.getPassword();
|
|
33
29
|
}
|
|
34
30
|
catch (error) {
|
|
35
31
|
logger.debug(`Failed to get API key from keychain: ${error}`);
|
|
@@ -38,11 +34,12 @@ class KeychainStorage {
|
|
|
38
34
|
}
|
|
39
35
|
async setApiKey(providerId, apiKey) {
|
|
40
36
|
try {
|
|
41
|
-
const
|
|
42
|
-
if (!
|
|
43
|
-
throw new Error('
|
|
37
|
+
const kr = await loadKeyring();
|
|
38
|
+
if (!kr)
|
|
39
|
+
throw new Error('keyring unavailable');
|
|
44
40
|
const account = this.getAccountName(providerId);
|
|
45
|
-
|
|
41
|
+
const entry = new kr.AsyncEntry(SERVICE_NAME, account);
|
|
42
|
+
await entry.setPassword(apiKey);
|
|
46
43
|
}
|
|
47
44
|
catch (error) {
|
|
48
45
|
throw new Error(`Failed to store API key in keychain: ${error}`);
|
|
@@ -50,11 +47,12 @@ class KeychainStorage {
|
|
|
50
47
|
}
|
|
51
48
|
async deleteApiKey(providerId) {
|
|
52
49
|
try {
|
|
53
|
-
const
|
|
54
|
-
if (!
|
|
50
|
+
const kr = await loadKeyring();
|
|
51
|
+
if (!kr)
|
|
55
52
|
return;
|
|
56
53
|
const account = this.getAccountName(providerId);
|
|
57
|
-
|
|
54
|
+
const entry = new kr.AsyncEntry(SERVICE_NAME, account);
|
|
55
|
+
await entry.deletePassword();
|
|
58
56
|
}
|
|
59
57
|
catch (error) {
|
|
60
58
|
logger.debug(`Failed to delete API key from keychain: ${error}`);
|
|
@@ -99,22 +97,26 @@ class FallbackStorage {
|
|
|
99
97
|
class SmartStorage {
|
|
100
98
|
keychain;
|
|
101
99
|
fallback;
|
|
100
|
+
config;
|
|
102
101
|
useKeychain = true;
|
|
103
102
|
keychainTested = false;
|
|
103
|
+
warnedLegacyKeytar = false;
|
|
104
104
|
constructor(config) {
|
|
105
105
|
this.keychain = new KeychainStorage();
|
|
106
106
|
this.fallback = new FallbackStorage(config);
|
|
107
|
+
this.config = config;
|
|
107
108
|
}
|
|
108
109
|
async ensureKeychainTested() {
|
|
109
110
|
if (this.keychainTested)
|
|
110
111
|
return;
|
|
111
112
|
try {
|
|
112
113
|
const testKey = '__codeep_test__';
|
|
113
|
-
const
|
|
114
|
-
if (!
|
|
115
|
-
throw new Error('
|
|
116
|
-
|
|
117
|
-
await
|
|
114
|
+
const kr = await loadKeyring();
|
|
115
|
+
if (!kr)
|
|
116
|
+
throw new Error('keyring unavailable');
|
|
117
|
+
const entry = new kr.AsyncEntry(SERVICE_NAME, testKey);
|
|
118
|
+
await entry.setPassword('test');
|
|
119
|
+
await entry.deletePassword();
|
|
118
120
|
this.useKeychain = true;
|
|
119
121
|
}
|
|
120
122
|
catch {
|
|
@@ -135,7 +137,21 @@ class SmartStorage {
|
|
|
135
137
|
if (key)
|
|
136
138
|
return key;
|
|
137
139
|
}
|
|
138
|
-
|
|
140
|
+
const fromFallback = await this.fallback.getApiKey(providerId);
|
|
141
|
+
// Migration note (Linux/Windows only): keys stored by the old keytar
|
|
142
|
+
// addon live under a different credential-store naming than keyring-rs
|
|
143
|
+
// uses, so they're invisible here — and the plaintext copies were
|
|
144
|
+
// already purged by the keysSecured migration. Nothing to silently
|
|
145
|
+
// recover; tell the user once so a missing key isn't a mystery.
|
|
146
|
+
// (macOS is unaffected — both libraries share the same Keychain items.)
|
|
147
|
+
if (fromFallback === null && !this.warnedLegacyKeytar
|
|
148
|
+
&& process.platform !== 'darwin' && this.useKeychain
|
|
149
|
+
&& this.config?.get?.('keysSecured') === true) {
|
|
150
|
+
this.warnedLegacyKeytar = true;
|
|
151
|
+
logger.warn('API keys saved by Codeep ≤ 2.14 (keytar) can\'t be read by the new keychain backend on this OS. ' +
|
|
152
|
+
'Re-add the affected key with /login <provider> <key> — it will be stored under the new backend.');
|
|
153
|
+
}
|
|
154
|
+
return fromFallback;
|
|
139
155
|
}
|
|
140
156
|
async setApiKey(providerId, apiKey) {
|
|
141
157
|
await this.ensureKeychainTested();
|
package/dist/utils/logger.d.ts
CHANGED
|
@@ -2,6 +2,17 @@
|
|
|
2
2
|
* Set current project path for local logging
|
|
3
3
|
*/
|
|
4
4
|
export declare function setLogProjectPath(projectPath: string | null): void;
|
|
5
|
+
type LogLevel = 'info' | 'warn' | 'error' | 'debug';
|
|
6
|
+
export interface LogEntry {
|
|
7
|
+
timestamp: string;
|
|
8
|
+
level: LogLevel;
|
|
9
|
+
message: string;
|
|
10
|
+
data?: any;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Format log entry as string
|
|
14
|
+
*/
|
|
15
|
+
export declare function formatLogEntry(entry: LogEntry): string;
|
|
5
16
|
/**
|
|
6
17
|
* Logger API
|
|
7
18
|
*/
|
|
@@ -31,3 +42,4 @@ export declare function logStartup(version: string): void;
|
|
|
31
42
|
* Log application error
|
|
32
43
|
*/
|
|
33
44
|
export declare function logAppError(error: Error, context?: string): void;
|
|
45
|
+
export {};
|
package/dist/utils/logger.js
CHANGED
|
@@ -49,7 +49,7 @@ function getLogFilePaths() {
|
|
|
49
49
|
/**
|
|
50
50
|
* Format log entry as string
|
|
51
51
|
*/
|
|
52
|
-
function formatLogEntry(entry) {
|
|
52
|
+
export function formatLogEntry(entry) {
|
|
53
53
|
const dataStr = entry.data ? ` ${JSON.stringify(entry.data)}` : '';
|
|
54
54
|
return `[${entry.timestamp}] [${entry.level.toUpperCase()}] ${entry.message}${dataStr}\n`;
|
|
55
55
|
}
|
|
@@ -34,8 +34,34 @@ export interface McpConfigFile {
|
|
|
34
34
|
* Load MCP server definitions for a workspace. Project entries shadow
|
|
35
35
|
* global entries with the same server name. Workspace-less calls
|
|
36
36
|
* (TUI without project) return only the global config.
|
|
37
|
+
*
|
|
38
|
+
* Sources read (highest precedence first on name collisions):
|
|
39
|
+
* 1. <workspace>/.codeep/mcp_servers.json (Codeep-native project file)
|
|
40
|
+
* 2. <workspace>/.mcp.json (cross-tool standard — same
|
|
41
|
+
* shape Claude Code/Cursor/Kilo Code read, so users can keep one MCP
|
|
42
|
+
* config for their whole fleet)
|
|
43
|
+
* 3. ~/.codeep/mcp_servers.json (global — user's machine)
|
|
37
44
|
*/
|
|
38
45
|
export declare function loadMcpServerConfig(workspaceRoot?: string): McpServer[];
|
|
46
|
+
/**
|
|
47
|
+
* Same sources as `loadMcpServerConfig`, but split by trust domain:
|
|
48
|
+
* `global` (~/.codeep — the user's own machine-wide file) vs `workspace`
|
|
49
|
+
* (files that arrive WITH a repo: `.codeep/mcp_servers.json` + `.mcp.json`).
|
|
50
|
+
*
|
|
51
|
+
* Workspace entries are attacker-controllable — anyone who clones a repo
|
|
52
|
+
* containing one of these files would otherwise spawn arbitrary commands
|
|
53
|
+
* at startup — so callers must gate them behind `isWorkspaceMcpTrusted`
|
|
54
|
+
* before spawning (mirrors the `trustedHookProjects` gate for hooks).
|
|
55
|
+
* On name collisions a workspace entry shadows a global one, matching
|
|
56
|
+
* the merged loader's precedence.
|
|
57
|
+
*/
|
|
58
|
+
export declare function loadMcpServerConfigSplit(workspaceRoot?: string): {
|
|
59
|
+
global: McpServer[];
|
|
60
|
+
workspace: McpServer[];
|
|
61
|
+
};
|
|
62
|
+
export declare function isWorkspaceMcpTrusted(workspaceRoot: string): boolean;
|
|
63
|
+
export declare function trustWorkspaceMcp(workspaceRoot: string): void;
|
|
64
|
+
export declare function untrustWorkspaceMcp(workspaceRoot: string): void;
|
|
39
65
|
/**
|
|
40
66
|
* Merge two server lists: ACP-provided + on-disk. ACP wins on collisions
|
|
41
67
|
* — the client knows its own config, so a Zed-passed server overrides a
|
package/dist/utils/mcpConfig.js
CHANGED
|
@@ -28,8 +28,50 @@
|
|
|
28
28
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
|
|
29
29
|
import { join, dirname } from 'path';
|
|
30
30
|
import { homedir } from 'os';
|
|
31
|
+
import { config } from '../config/index.js';
|
|
31
32
|
const PROJECT_CONFIG_PATH = '.codeep/mcp_servers.json';
|
|
33
|
+
const PROJECT_DOTMCP_PATH = '.mcp.json';
|
|
32
34
|
const GLOBAL_CONFIG_PATH = '.codeep/mcp_servers.json';
|
|
35
|
+
/**
|
|
36
|
+
* Expand `${VAR}` / `${VAR:-default}` references from the process env —
|
|
37
|
+
* the same substitution Claude Code applies to `.mcp.json`, so a shared
|
|
38
|
+
* config with `"env": {"GITHUB_TOKEN": "${GITHUB_TOKEN}"}` works verbatim.
|
|
39
|
+
*
|
|
40
|
+
* Unset vars WITHOUT a default keep the literal `${VAR}` text: silently
|
|
41
|
+
* substituting '' would hide the misconfiguration, and the literal at
|
|
42
|
+
* least shows up as-is in error messages / `/mcp` output. (Previously the
|
|
43
|
+
* literal also SHADOWED a real env var of the same name at spawn time —
|
|
44
|
+
* expansion when the var IS set fixes that.)
|
|
45
|
+
*/
|
|
46
|
+
function expandEnvRefs(value) {
|
|
47
|
+
return value.replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (match, name, def) => {
|
|
48
|
+
const fromEnv = process.env[name];
|
|
49
|
+
if (fromEnv !== undefined)
|
|
50
|
+
return fromEnv;
|
|
51
|
+
if (def !== undefined)
|
|
52
|
+
return def;
|
|
53
|
+
return match;
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
function expandRecord(rec) {
|
|
57
|
+
if (!rec)
|
|
58
|
+
return undefined;
|
|
59
|
+
const out = {};
|
|
60
|
+
for (const [k, v] of Object.entries(rec))
|
|
61
|
+
out[k] = typeof v === 'string' ? expandEnvRefs(v) : v;
|
|
62
|
+
return out;
|
|
63
|
+
}
|
|
64
|
+
/** Apply ${VAR} expansion to every string field of a server entry. */
|
|
65
|
+
function expandServer(s) {
|
|
66
|
+
return {
|
|
67
|
+
...s,
|
|
68
|
+
command: typeof s.command === 'string' ? expandEnvRefs(s.command) : s.command,
|
|
69
|
+
args: Array.isArray(s.args) ? s.args.map(a => expandEnvRefs(a)) : s.args,
|
|
70
|
+
env: expandRecord(s.env),
|
|
71
|
+
url: typeof s.url === 'string' ? expandEnvRefs(s.url) : s.url,
|
|
72
|
+
headers: expandRecord(s.headers),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
33
75
|
function parseEntries(raw, source) {
|
|
34
76
|
let parsed;
|
|
35
77
|
try {
|
|
@@ -46,7 +88,9 @@ function parseEntries(raw, source) {
|
|
|
46
88
|
return [];
|
|
47
89
|
if (Array.isArray(servers)) {
|
|
48
90
|
// Defensively filter — any entry needs name + either command or url.
|
|
49
|
-
return servers
|
|
91
|
+
return servers
|
|
92
|
+
.filter(s => s && typeof s.name === 'string' && (typeof s.command === 'string' || typeof s.url === 'string'))
|
|
93
|
+
.map(expandServer);
|
|
50
94
|
}
|
|
51
95
|
return Object.entries(servers).flatMap(([name, cfg]) => {
|
|
52
96
|
if (!cfg)
|
|
@@ -56,7 +100,7 @@ function parseEntries(raw, source) {
|
|
|
56
100
|
const hasHttp = typeof cfg.url === 'string';
|
|
57
101
|
if (!hasStdio && !hasHttp)
|
|
58
102
|
return [];
|
|
59
|
-
return [{
|
|
103
|
+
return [expandServer({
|
|
60
104
|
name,
|
|
61
105
|
command: hasStdio ? cfg.command : undefined,
|
|
62
106
|
args: Array.isArray(cfg.args) ? cfg.args.filter(a => typeof a === 'string') : [],
|
|
@@ -65,7 +109,7 @@ function parseEntries(raw, source) {
|
|
|
65
109
|
headers: cfg.headers && typeof cfg.headers === 'object'
|
|
66
110
|
? cfg.headers
|
|
67
111
|
: undefined,
|
|
68
|
-
}];
|
|
112
|
+
})];
|
|
69
113
|
});
|
|
70
114
|
}
|
|
71
115
|
function loadFromFile(path) {
|
|
@@ -83,20 +127,81 @@ function loadFromFile(path) {
|
|
|
83
127
|
* Load MCP server definitions for a workspace. Project entries shadow
|
|
84
128
|
* global entries with the same server name. Workspace-less calls
|
|
85
129
|
* (TUI without project) return only the global config.
|
|
130
|
+
*
|
|
131
|
+
* Sources read (highest precedence first on name collisions):
|
|
132
|
+
* 1. <workspace>/.codeep/mcp_servers.json (Codeep-native project file)
|
|
133
|
+
* 2. <workspace>/.mcp.json (cross-tool standard — same
|
|
134
|
+
* shape Claude Code/Cursor/Kilo Code read, so users can keep one MCP
|
|
135
|
+
* config for their whole fleet)
|
|
136
|
+
* 3. ~/.codeep/mcp_servers.json (global — user's machine)
|
|
86
137
|
*/
|
|
87
138
|
export function loadMcpServerConfig(workspaceRoot) {
|
|
88
139
|
const globalServers = loadFromFile(join(homedir(), GLOBAL_CONFIG_PATH));
|
|
89
140
|
const projectServers = workspaceRoot
|
|
90
141
|
? loadFromFile(join(workspaceRoot, PROJECT_CONFIG_PATH))
|
|
91
142
|
: [];
|
|
92
|
-
|
|
143
|
+
const dotMcpServers = workspaceRoot
|
|
144
|
+
? loadFromFile(join(workspaceRoot, PROJECT_DOTMCP_PATH))
|
|
145
|
+
: [];
|
|
146
|
+
// Higher-precedence sources win on name collisions: project (Codeep-native)
|
|
147
|
+
// beats .mcp.json beats global.
|
|
93
148
|
const byName = new Map();
|
|
94
149
|
for (const s of globalServers)
|
|
95
150
|
byName.set(s.name, s);
|
|
151
|
+
for (const s of dotMcpServers)
|
|
152
|
+
byName.set(s.name, s);
|
|
96
153
|
for (const s of projectServers)
|
|
97
154
|
byName.set(s.name, s);
|
|
98
155
|
return [...byName.values()];
|
|
99
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Same sources as `loadMcpServerConfig`, but split by trust domain:
|
|
159
|
+
* `global` (~/.codeep — the user's own machine-wide file) vs `workspace`
|
|
160
|
+
* (files that arrive WITH a repo: `.codeep/mcp_servers.json` + `.mcp.json`).
|
|
161
|
+
*
|
|
162
|
+
* Workspace entries are attacker-controllable — anyone who clones a repo
|
|
163
|
+
* containing one of these files would otherwise spawn arbitrary commands
|
|
164
|
+
* at startup — so callers must gate them behind `isWorkspaceMcpTrusted`
|
|
165
|
+
* before spawning (mirrors the `trustedHookProjects` gate for hooks).
|
|
166
|
+
* On name collisions a workspace entry shadows a global one, matching
|
|
167
|
+
* the merged loader's precedence.
|
|
168
|
+
*/
|
|
169
|
+
export function loadMcpServerConfigSplit(workspaceRoot) {
|
|
170
|
+
const globalServers = loadFromFile(join(homedir(), GLOBAL_CONFIG_PATH));
|
|
171
|
+
if (!workspaceRoot)
|
|
172
|
+
return { global: globalServers, workspace: [] };
|
|
173
|
+
const projectServers = loadFromFile(join(workspaceRoot, PROJECT_CONFIG_PATH));
|
|
174
|
+
const dotMcpServers = loadFromFile(join(workspaceRoot, PROJECT_DOTMCP_PATH));
|
|
175
|
+
const byName = new Map();
|
|
176
|
+
for (const s of dotMcpServers)
|
|
177
|
+
byName.set(s.name, s);
|
|
178
|
+
for (const s of projectServers)
|
|
179
|
+
byName.set(s.name, s);
|
|
180
|
+
const workspace = [...byName.values()];
|
|
181
|
+
const workspaceNames = new Set(workspace.map(s => s.name));
|
|
182
|
+
return {
|
|
183
|
+
global: globalServers.filter(s => !workspaceNames.has(s.name)),
|
|
184
|
+
workspace,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
// ── Workspace MCP trust ────────────────────────────────────────────────────────
|
|
188
|
+
// Workspace-sourced MCP servers spawn child processes with repo-author-chosen
|
|
189
|
+
// command/args/env, so they need a one-time per-workspace approval — the same
|
|
190
|
+
// model as `trustedHookProjects` for hooks. Global (~/.codeep) servers are the
|
|
191
|
+
// user's own config and never need approval.
|
|
192
|
+
export function isWorkspaceMcpTrusted(workspaceRoot) {
|
|
193
|
+
const cur = config.get('trustedMcpProjects') ?? [];
|
|
194
|
+
return cur.includes(workspaceRoot);
|
|
195
|
+
}
|
|
196
|
+
export function trustWorkspaceMcp(workspaceRoot) {
|
|
197
|
+
const cur = config.get('trustedMcpProjects') ?? [];
|
|
198
|
+
if (!cur.includes(workspaceRoot))
|
|
199
|
+
config.set('trustedMcpProjects', [...cur, workspaceRoot]);
|
|
200
|
+
}
|
|
201
|
+
export function untrustWorkspaceMcp(workspaceRoot) {
|
|
202
|
+
const cur = config.get('trustedMcpProjects') ?? [];
|
|
203
|
+
config.set('trustedMcpProjects', cur.filter((p) => p !== workspaceRoot));
|
|
204
|
+
}
|
|
100
205
|
/**
|
|
101
206
|
* Merge two server lists: ACP-provided + on-disk. ACP wins on collisions
|
|
102
207
|
* — the client knows its own config, so a Zed-passed server overrides a
|
|
@@ -61,6 +61,19 @@ export interface SkillBundle extends SkillBundleMeta {
|
|
|
61
61
|
/** Body content (everything after the frontmatter). */
|
|
62
62
|
body: string;
|
|
63
63
|
}
|
|
64
|
+
interface ParsedFrontmatter {
|
|
65
|
+
meta: Record<string, unknown>;
|
|
66
|
+
body: string;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Tolerant YAML-frontmatter parser — handles `key: value`, `key: [a, b]`,
|
|
70
|
+
* and `key:` followed by `- item` block-list lines. Quoted strings are
|
|
71
|
+
* unquoted. We don't ship a real YAML dep for this — the keys we care
|
|
72
|
+
* about are scalars or simple arrays.
|
|
73
|
+
*/
|
|
74
|
+
export declare function parseFrontmatter(raw: string): ParsedFrontmatter;
|
|
75
|
+
export declare function stripQuotes(s: string): string;
|
|
76
|
+
export declare function asStringArray(v: unknown): string[] | null;
|
|
64
77
|
/**
|
|
65
78
|
* Load all skill bundles available in this workspace. Project entries
|
|
66
79
|
* shadow global entries with the same name.
|
|
@@ -82,3 +95,4 @@ export declare function formatBundleList(bundles: SkillBundle[]): string;
|
|
|
82
95
|
* pattern as custom commands and hooks. Empty string if no bundles.
|
|
83
96
|
*/
|
|
84
97
|
export declare function summarizeBundles(workspaceRoot: string): string;
|
|
98
|
+
export {};
|
|
@@ -42,7 +42,7 @@ import { homedir } from 'os';
|
|
|
42
42
|
* unquoted. We don't ship a real YAML dep for this — the keys we care
|
|
43
43
|
* about are scalars or simple arrays.
|
|
44
44
|
*/
|
|
45
|
-
function parseFrontmatter(raw) {
|
|
45
|
+
export function parseFrontmatter(raw) {
|
|
46
46
|
// BOM + CRLF normalisation. Real-world files copy/paste from various
|
|
47
47
|
// editors and pick up either; YAML strictly forbids tabs in scalars
|
|
48
48
|
// but we don't care for the keys we read.
|
|
@@ -94,7 +94,7 @@ function parseFrontmatter(raw) {
|
|
|
94
94
|
}
|
|
95
95
|
return { meta, body: match[2].trimStart() };
|
|
96
96
|
}
|
|
97
|
-
function stripQuotes(s) {
|
|
97
|
+
export function stripQuotes(s) {
|
|
98
98
|
return s.replace(/^["']|["']$/g, '');
|
|
99
99
|
}
|
|
100
100
|
function loadFromDir(dir, scope) {
|
|
@@ -158,7 +158,7 @@ function loadFromDir(dir, scope) {
|
|
|
158
158
|
}
|
|
159
159
|
return bundles;
|
|
160
160
|
}
|
|
161
|
-
function asStringArray(v) {
|
|
161
|
+
export function asStringArray(v) {
|
|
162
162
|
if (Array.isArray(v))
|
|
163
163
|
return v.filter(x => typeof x === 'string');
|
|
164
164
|
if (typeof v === 'string')
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* Auth uses the same `x-sync-token` header `codeepCloud.ts` already sends
|
|
11
11
|
* for /api/tasks and friends.
|
|
12
12
|
*/
|
|
13
|
+
import { type SkillBundle } from './skillBundles.js';
|
|
13
14
|
export interface RemoteSkill {
|
|
14
15
|
id: number;
|
|
15
16
|
github_id: string;
|
|
@@ -63,6 +64,12 @@ export declare function unpublishBundle(idOrPath: string): Promise<{
|
|
|
63
64
|
ok: boolean;
|
|
64
65
|
error?: string;
|
|
65
66
|
}>;
|
|
67
|
+
/**
|
|
68
|
+
* Re-serialise a loaded SkillBundle back into the SKILL.md text format.
|
|
69
|
+
* Used by publish so the round-trip is lossless (sort of — we drop
|
|
70
|
+
* unknown frontmatter keys for now to keep the published format stable).
|
|
71
|
+
*/
|
|
72
|
+
export declare function serialiseSkillMd(bundle: SkillBundle): string;
|
|
66
73
|
/** Read raw SKILL.md from disk — used when we want the unmodified bytes. */
|
|
67
74
|
export declare function readRawSkillMd(workspaceRoot: string, slug: string): string | null;
|
|
68
75
|
/** Delete the local copy of an installed skill bundle (for /skills uninstall). */
|
|
@@ -144,7 +144,7 @@ export async function unpublishBundle(idOrPath) {
|
|
|
144
144
|
* Used by publish so the round-trip is lossless (sort of — we drop
|
|
145
145
|
* unknown frontmatter keys for now to keep the published format stable).
|
|
146
146
|
*/
|
|
147
|
-
function serialiseSkillMd(bundle) {
|
|
147
|
+
export function serialiseSkillMd(bundle) {
|
|
148
148
|
const meta = ['---'];
|
|
149
149
|
meta.push(`name: ${bundle.name}`);
|
|
150
150
|
meta.push(`description: ${bundle.description}`);
|