codeep 3.3.3 → 3.4.1
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/dist/acp/commands.d.ts +50 -1
- package/dist/acp/commands.js +545 -109
- package/dist/acp/protocol.d.ts +14 -5
- package/dist/acp/server.d.ts +36 -1
- package/dist/acp/server.js +581 -155
- package/dist/acp/serverHandlers.d.ts +2 -1
- package/dist/acp/serverHandlers.js +3 -0
- package/dist/acp/session.d.ts +28 -2
- package/dist/acp/session.js +25 -6
- package/dist/acp/transport.d.ts +40 -4
- package/dist/acp/transport.js +218 -25
- package/dist/acp/turns.d.ts +20 -0
- package/dist/acp/turns.js +30 -0
- package/dist/api/index.js +2 -0
- package/dist/api/ollamaNative.d.ts +3 -0
- package/dist/api/ollamaNative.js +35 -3
- package/dist/config/index.d.ts +21 -4
- package/dist/config/index.js +178 -123
- package/dist/renderer/agentExecution.d.ts +30 -2
- package/dist/renderer/agentExecution.js +248 -92
- package/dist/renderer/commands/helpers.d.ts +18 -2
- package/dist/renderer/commands/helpers.js +28 -5
- package/dist/renderer/commands.d.ts +2 -0
- package/dist/renderer/commands.js +180 -64
- package/dist/renderer/main.d.ts +41 -0
- package/dist/renderer/main.js +181 -80
- package/dist/utils/agent.d.ts +69 -4
- package/dist/utils/agent.js +416 -248
- package/dist/utils/agentChat.js +82 -10
- package/dist/utils/agents.d.ts +2 -1
- package/dist/utils/agents.js +100 -29
- package/dist/utils/auditLog.d.ts +4 -3
- package/dist/utils/auditLog.js +92 -9
- package/dist/utils/checkpoints.js +11 -6
- package/dist/utils/codeReview.js +28 -23
- package/dist/utils/codeepCloud.d.ts +14 -2
- package/dist/utils/codeepCloud.js +56 -20
- package/dist/utils/customCommands.js +7 -2
- package/dist/utils/git.d.ts +262 -4
- package/dist/utils/git.js +1928 -61
- package/dist/utils/gitHookInstaller.d.ts +32 -1
- package/dist/utils/gitHookInstaller.js +76 -8
- package/dist/utils/gitignore.d.ts +8 -0
- package/dist/utils/gitignore.js +41 -10
- package/dist/utils/headlessReview.d.ts +11 -0
- package/dist/utils/headlessReview.js +33 -5
- package/dist/utils/history.d.ts +22 -6
- package/dist/utils/history.js +140 -26
- package/dist/utils/logger.js +6 -7
- package/dist/utils/mcpConfig.d.ts +24 -0
- package/dist/utils/mcpConfig.js +36 -5
- package/dist/utils/mentions.d.ts +28 -5
- package/dist/utils/mentions.js +253 -45
- package/dist/utils/personalities.js +16 -6
- package/dist/utils/planMode.d.ts +13 -7
- package/dist/utils/planMode.js +32 -12
- package/dist/utils/projectIntelligence.d.ts +2 -0
- package/dist/utils/projectIntelligence.js +27 -8
- package/dist/utils/projectPaths.d.ts +53 -0
- package/dist/utils/projectPaths.js +146 -0
- package/dist/utils/shell.d.ts +119 -0
- package/dist/utils/shell.js +417 -45
- package/dist/utils/skillBundles.js +17 -7
- package/dist/utils/skillBundlesCloud.js +20 -3
- package/dist/utils/skills.d.ts +24 -2
- package/dist/utils/skills.js +235 -43
- package/dist/utils/smartContext.js +97 -23
- package/dist/utils/telegramApproval.d.ts +10 -2
- package/dist/utils/telegramApproval.js +22 -4
- package/dist/utils/toolExecution.d.ts +50 -2
- package/dist/utils/toolExecution.js +418 -16
- package/dist/utils/toolParsing.d.ts +7 -1
- package/dist/utils/toolParsing.js +12 -3
- package/dist/utils/userProfile.js +58 -16
- package/dist/utils/verify.d.ts +25 -4
- package/dist/utils/verify.js +259 -74
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/config/index.d.ts
CHANGED
|
@@ -291,8 +291,16 @@ export declare function resolveBaseUrl(providerId: string, protocol: 'openai' |
|
|
|
291
291
|
export { PROVIDERS } from './providers';
|
|
292
292
|
export declare function getCurrentSessionId(): string;
|
|
293
293
|
export declare function startNewSession(): string;
|
|
294
|
-
export declare function autoSaveSession(history: Message[], projectPath?: string): boolean;
|
|
294
|
+
export declare function autoSaveSession(history: Message[], projectPath?: string, sessionId?: string): boolean;
|
|
295
295
|
export declare function flushAutoSave(): boolean;
|
|
296
|
+
/**
|
|
297
|
+
* Why a session name cannot be used, or null when it can. A session is kept
|
|
298
|
+
* as <name>.json inside the sessions directory, so a name holding a path
|
|
299
|
+
* separator (`../../x`, `feature/auth`) would read or write a file elsewhere.
|
|
300
|
+
* A backslash is a separator only on Windows; elsewhere it is an ordinary
|
|
301
|
+
* filename character, and sessions named with one already exist.
|
|
302
|
+
*/
|
|
303
|
+
export declare function sessionNameProblem(name: string): string | null;
|
|
296
304
|
export declare function saveSession(name: string, history: Message[], projectPath?: string): boolean;
|
|
297
305
|
/**
|
|
298
306
|
* Generate and persist an AI title for a session, if it doesn't have
|
|
@@ -303,6 +311,13 @@ export declare function maybeGenerateSessionTitle(name: string, projectPath?: st
|
|
|
303
311
|
export declare function loadSession(name: string, projectPath?: string): Message[] | null;
|
|
304
312
|
export declare function listSessions(projectPath?: string): string[];
|
|
305
313
|
export declare function deleteSession(name: string, projectPath?: string): boolean;
|
|
314
|
+
/**
|
|
315
|
+
* True when `name` already belongs to a saved conversation other than
|
|
316
|
+
* `currentName`'s. A name differing only in case can resolve to the same file
|
|
317
|
+
* on a case-insensitive disk; that is the same conversation, not a clash.
|
|
318
|
+
* Inodes are compared as bigints: on Windows they can exceed 2^53.
|
|
319
|
+
*/
|
|
320
|
+
export declare function sessionNameTaken(name: string, currentName: string, projectPath?: string): boolean;
|
|
306
321
|
export declare function renameSession(oldName: string, newName: string, projectPath?: string): boolean;
|
|
307
322
|
export declare function getSessionInfo(name: string, projectPath?: string): {
|
|
308
323
|
name: string;
|
|
@@ -321,15 +336,17 @@ export interface SessionInfo {
|
|
|
321
336
|
*/
|
|
322
337
|
export declare function listSessionsWithInfo(projectPath?: string): SessionInfo[];
|
|
323
338
|
/**
|
|
324
|
-
* Get
|
|
339
|
+
* Get the permission the user granted for a project.
|
|
325
340
|
*/
|
|
326
341
|
export declare function getProjectPermission(projectPath: string): ProjectPermission | null;
|
|
327
342
|
/**
|
|
328
|
-
*
|
|
343
|
+
* Record a permission grant in the global config.
|
|
329
344
|
*/
|
|
330
345
|
export declare function setProjectPermission(projectPath: string, read: boolean, write: boolean): void;
|
|
331
346
|
/**
|
|
332
|
-
* Remove project permission from
|
|
347
|
+
* Remove a project's permission from the global config. A grant an older
|
|
348
|
+
* version wrote into the project for this same directory is removed too, so
|
|
349
|
+
* it does not linger in a file that may be committed.
|
|
333
350
|
*/
|
|
334
351
|
export declare function removeProjectPermission(projectPath: string): boolean;
|
|
335
352
|
export declare function hasReadPermission(projectPath: string): boolean;
|
package/dist/config/index.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import Conf from 'conf';
|
|
2
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, statSync } from 'fs';
|
|
3
|
-
import { join, dirname } from 'path';
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, unlinkSync, statSync, renameSync } from 'fs';
|
|
3
|
+
import { join, dirname, resolve, sep, isAbsolute } from 'path';
|
|
4
4
|
import { randomUUID } from 'crypto';
|
|
5
5
|
import { PROVIDERS, getProvider, getProviderBaseUrl, replacementModelFor } from './providers.js';
|
|
6
6
|
import { logSession } from '../utils/logger.js';
|
|
7
7
|
import { createSecureStorage } from '../utils/keychain.js';
|
|
8
|
+
import { isSafeProjectDir, leadsOutsideProject, writeFileNoFollow, writeProjectFile } from '../utils/projectPaths.js';
|
|
8
9
|
// We'll initialize GLOBAL_SESSIONS_DIR after config is created (to use config.path)
|
|
9
10
|
/**
|
|
10
11
|
* Get sessions directory - local .codeep/sessions/ if in project, otherwise global
|
|
@@ -12,25 +13,17 @@ import { createSecureStorage } from '../utils/keychain.js';
|
|
|
12
13
|
export function getSessionsDir(projectPath) {
|
|
13
14
|
if (projectPath && isProjectDirectory(projectPath)) {
|
|
14
15
|
const localDir = join(projectPath, '.codeep', 'sessions');
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
// .codeep/ can come with a cloned repo; a symlinked sessions directory
|
|
17
|
+
// would have every save written wherever it points.
|
|
18
|
+
if (isSafeProjectDir(projectPath, localDir)) {
|
|
19
|
+
if (!existsSync(localDir)) {
|
|
20
|
+
mkdirSync(localDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
return localDir;
|
|
17
23
|
}
|
|
18
|
-
return localDir;
|
|
19
24
|
}
|
|
20
25
|
return GLOBAL_SESSIONS_DIR;
|
|
21
26
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Get local project config path
|
|
24
|
-
*/
|
|
25
|
-
function getLocalConfigPath(projectPath) {
|
|
26
|
-
if (!isProjectDirectory(projectPath))
|
|
27
|
-
return null;
|
|
28
|
-
const configDir = join(projectPath, '.codeep');
|
|
29
|
-
if (!existsSync(configDir)) {
|
|
30
|
-
mkdirSync(configDir, { recursive: true });
|
|
31
|
-
}
|
|
32
|
-
return join(configDir, 'config.json');
|
|
33
|
-
}
|
|
34
27
|
/**
|
|
35
28
|
* Check if directory is a project
|
|
36
29
|
* Looks for common project indicators: package.json, pyproject.toml, Cargo.toml, go.mod, composer.json, etc.
|
|
@@ -80,17 +73,13 @@ export function hasStandardProjectMarkers(path) {
|
|
|
80
73
|
*/
|
|
81
74
|
export function initializeAsProject(path) {
|
|
82
75
|
try {
|
|
83
|
-
const
|
|
84
|
-
if (!existsSync(codeepDir)) {
|
|
85
|
-
mkdirSync(codeepDir, { recursive: true });
|
|
86
|
-
}
|
|
87
|
-
const projectFile = join(codeepDir, 'project.json');
|
|
76
|
+
const projectFile = join(path, '.codeep', 'project.json');
|
|
88
77
|
const projectData = {
|
|
89
78
|
name: path.split('/').pop() || 'project',
|
|
90
79
|
initializedAt: new Date().toISOString(),
|
|
91
80
|
version: '1.0',
|
|
92
81
|
};
|
|
93
|
-
|
|
82
|
+
writeProjectFile(path, projectFile, JSON.stringify(projectData, null, 2));
|
|
94
83
|
return true;
|
|
95
84
|
}
|
|
96
85
|
catch {
|
|
@@ -821,46 +810,87 @@ export function startNewSession() {
|
|
|
821
810
|
config.set('currentSessionId', sessionId);
|
|
822
811
|
return sessionId;
|
|
823
812
|
}
|
|
824
|
-
// Auto-save debounce state
|
|
813
|
+
// Auto-save debounce state. The target session is fixed when the save is
|
|
814
|
+
// requested, not when the timer fires: by then the user may have loaded or
|
|
815
|
+
// started another conversation, and this history would overwrite its file.
|
|
825
816
|
let autoSaveTimeout = null;
|
|
826
817
|
let pendingAutoSave = null;
|
|
827
|
-
|
|
828
|
-
|
|
818
|
+
function writePendingAutoSave() {
|
|
819
|
+
if (!pendingAutoSave)
|
|
820
|
+
return false;
|
|
821
|
+
const { sessionId, history, projectPath } = pendingAutoSave;
|
|
822
|
+
pendingAutoSave = null;
|
|
823
|
+
return saveSession(sessionId, history, projectPath);
|
|
824
|
+
}
|
|
825
|
+
// Auto-save a conversation (debounced - saves max every 5 seconds).
|
|
826
|
+
// Pass the id of the conversation the history belongs to. The global
|
|
827
|
+
// currentSessionId is only the fallback: it names one conversation, and
|
|
828
|
+
// callers holding another (a loaded session, a second ACP thread) would
|
|
829
|
+
// otherwise save over it.
|
|
830
|
+
export function autoSaveSession(history, projectPath, sessionId) {
|
|
829
831
|
if (!config.get('autoSave') || history.length === 0) {
|
|
830
832
|
return false;
|
|
831
833
|
}
|
|
832
|
-
|
|
833
|
-
|
|
834
|
+
const target = sessionId || getCurrentSessionId();
|
|
835
|
+
// Only one save is held back at a time. One queued for a different
|
|
836
|
+
// conversation would be dropped by the overwrite below, so write it now.
|
|
837
|
+
if (pendingAutoSave && (pendingAutoSave.sessionId !== target || pendingAutoSave.projectPath !== projectPath)) {
|
|
838
|
+
writePendingAutoSave();
|
|
839
|
+
}
|
|
840
|
+
pendingAutoSave = { history, projectPath, sessionId: target };
|
|
834
841
|
// If already scheduled, don't reschedule
|
|
835
842
|
if (autoSaveTimeout) {
|
|
836
843
|
return true;
|
|
837
844
|
}
|
|
838
845
|
// Schedule save after 5 seconds
|
|
839
846
|
autoSaveTimeout = setTimeout(() => {
|
|
840
|
-
if (pendingAutoSave) {
|
|
841
|
-
const sessionId = getCurrentSessionId();
|
|
842
|
-
saveSession(sessionId, pendingAutoSave.history, pendingAutoSave.projectPath);
|
|
843
|
-
pendingAutoSave = null;
|
|
844
|
-
}
|
|
845
847
|
autoSaveTimeout = null;
|
|
848
|
+
writePendingAutoSave();
|
|
846
849
|
}, 5000);
|
|
847
850
|
return true;
|
|
848
851
|
}
|
|
849
|
-
//
|
|
852
|
+
// Write any debounced save now. Anything about to exit must call this: the
|
|
853
|
+
// 5-second timer never fires once the process is gone.
|
|
850
854
|
export function flushAutoSave() {
|
|
851
855
|
if (autoSaveTimeout) {
|
|
852
856
|
clearTimeout(autoSaveTimeout);
|
|
853
857
|
autoSaveTimeout = null;
|
|
854
858
|
}
|
|
855
|
-
|
|
856
|
-
const sessionId = getCurrentSessionId();
|
|
857
|
-
const result = saveSession(sessionId, pendingAutoSave.history, pendingAutoSave.projectPath);
|
|
858
|
-
pendingAutoSave = null;
|
|
859
|
-
return result;
|
|
860
|
-
}
|
|
861
|
-
return false;
|
|
859
|
+
return writePendingAutoSave();
|
|
862
860
|
}
|
|
863
861
|
// Session management
|
|
862
|
+
/**
|
|
863
|
+
* Why a session name cannot be used, or null when it can. A session is kept
|
|
864
|
+
* as <name>.json inside the sessions directory, so a name holding a path
|
|
865
|
+
* separator (`../../x`, `feature/auth`) would read or write a file elsewhere.
|
|
866
|
+
* A backslash is a separator only on Windows; elsewhere it is an ordinary
|
|
867
|
+
* filename character, and sessions named with one already exist.
|
|
868
|
+
*/
|
|
869
|
+
export function sessionNameProblem(name) {
|
|
870
|
+
if (!name || !name.trim())
|
|
871
|
+
return 'Session name cannot be empty.';
|
|
872
|
+
if (sep === '\\') {
|
|
873
|
+
if (/[\/\\]/.test(name))
|
|
874
|
+
return `Session name "${name}" cannot contain "/" or "\\".`;
|
|
875
|
+
}
|
|
876
|
+
else if (name.includes('/')) {
|
|
877
|
+
return `Session name "${name}" cannot contain "/".`;
|
|
878
|
+
}
|
|
879
|
+
// eslint-disable-next-line no-control-regex
|
|
880
|
+
if (/[\x00-\x1f]/.test(name))
|
|
881
|
+
return 'Session name cannot contain control characters.';
|
|
882
|
+
if (name === '.' || name === '..')
|
|
883
|
+
return `Session name "${name}" is not allowed.`;
|
|
884
|
+
return null;
|
|
885
|
+
}
|
|
886
|
+
/** The file a session is kept in, or null for a name that would leave the sessions directory. */
|
|
887
|
+
function sessionFile(name, projectPath) {
|
|
888
|
+
if (sessionNameProblem(name))
|
|
889
|
+
return null;
|
|
890
|
+
const sessionsDir = getSessionsDir(projectPath);
|
|
891
|
+
const filePath = join(sessionsDir, `${name}.json`);
|
|
892
|
+
return dirname(resolve(filePath)) === resolve(sessionsDir) ? filePath : null;
|
|
893
|
+
}
|
|
864
894
|
export function saveSession(name, history, projectPath) {
|
|
865
895
|
try {
|
|
866
896
|
// Derive a human-readable title from the first user message
|
|
@@ -868,8 +898,11 @@ export function saveSession(name, history, projectPath) {
|
|
|
868
898
|
const title = firstUserMsg
|
|
869
899
|
? firstUserMsg.content.replace(/\n/g, ' ').trim().slice(0, 60)
|
|
870
900
|
: name;
|
|
871
|
-
const
|
|
872
|
-
|
|
901
|
+
const filePath = sessionFile(name, projectPath);
|
|
902
|
+
if (!filePath) {
|
|
903
|
+
logSession('save', name, false);
|
|
904
|
+
return false;
|
|
905
|
+
}
|
|
873
906
|
// Preserve an existing aiTitle across re-saves so we don't regenerate.
|
|
874
907
|
let existingAiTitle;
|
|
875
908
|
if (existsSync(filePath)) {
|
|
@@ -886,7 +919,7 @@ export function saveSession(name, history, projectPath) {
|
|
|
886
919
|
history,
|
|
887
920
|
createdAt: new Date().toISOString(),
|
|
888
921
|
};
|
|
889
|
-
|
|
922
|
+
writeFileNoFollow(filePath, JSON.stringify(session, null, 2));
|
|
890
923
|
logSession('save', name, true);
|
|
891
924
|
// Fire-and-forget: generate an AI title once the session has enough
|
|
892
925
|
// content. The orchestrator early-returns if one already exists, so
|
|
@@ -926,9 +959,8 @@ const titlesInFlight = new Set();
|
|
|
926
959
|
export async function maybeGenerateSessionTitle(name, projectPath) {
|
|
927
960
|
if (titlesInFlight.has(name))
|
|
928
961
|
return;
|
|
929
|
-
const
|
|
930
|
-
|
|
931
|
-
if (!existsSync(filePath))
|
|
962
|
+
const filePath = sessionFile(name, projectPath);
|
|
963
|
+
if (!filePath || !existsSync(filePath))
|
|
932
964
|
return;
|
|
933
965
|
let data;
|
|
934
966
|
try {
|
|
@@ -949,7 +981,7 @@ export async function maybeGenerateSessionTitle(name, projectPath) {
|
|
|
949
981
|
try {
|
|
950
982
|
const fresh = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
951
983
|
fresh.aiTitle = aiTitle;
|
|
952
|
-
|
|
984
|
+
writeFileNoFollow(filePath, JSON.stringify(fresh, null, 2));
|
|
953
985
|
}
|
|
954
986
|
catch { /* ignore */ }
|
|
955
987
|
}
|
|
@@ -960,9 +992,8 @@ export async function maybeGenerateSessionTitle(name, projectPath) {
|
|
|
960
992
|
}
|
|
961
993
|
export function loadSession(name, projectPath) {
|
|
962
994
|
try {
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
if (existsSync(filePath)) {
|
|
995
|
+
const filePath = sessionFile(name, projectPath);
|
|
996
|
+
if (filePath && existsSync(filePath)) {
|
|
966
997
|
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
967
998
|
logSession('load', name, true);
|
|
968
999
|
return data.history;
|
|
@@ -989,9 +1020,8 @@ export function listSessions(projectPath) {
|
|
|
989
1020
|
}
|
|
990
1021
|
export function deleteSession(name, projectPath) {
|
|
991
1022
|
try {
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
if (existsSync(filePath)) {
|
|
1023
|
+
const filePath = sessionFile(name, projectPath);
|
|
1024
|
+
if (filePath && existsSync(filePath)) {
|
|
995
1025
|
unlinkSync(filePath);
|
|
996
1026
|
logSession('delete', name, true);
|
|
997
1027
|
return true;
|
|
@@ -1004,26 +1034,72 @@ export function deleteSession(name, projectPath) {
|
|
|
1004
1034
|
return false;
|
|
1005
1035
|
}
|
|
1006
1036
|
}
|
|
1037
|
+
/**
|
|
1038
|
+
* True when `name` already belongs to a saved conversation other than
|
|
1039
|
+
* `currentName`'s. A name differing only in case can resolve to the same file
|
|
1040
|
+
* on a case-insensitive disk; that is the same conversation, not a clash.
|
|
1041
|
+
* Inodes are compared as bigints: on Windows they can exceed 2^53.
|
|
1042
|
+
*/
|
|
1043
|
+
export function sessionNameTaken(name, currentName, projectPath) {
|
|
1044
|
+
if (name === currentName)
|
|
1045
|
+
return false;
|
|
1046
|
+
const target = sessionFile(name, projectPath);
|
|
1047
|
+
if (!target || !existsSync(target))
|
|
1048
|
+
return false;
|
|
1049
|
+
const current = sessionFile(currentName, projectPath);
|
|
1050
|
+
if (!current || !existsSync(current))
|
|
1051
|
+
return true;
|
|
1052
|
+
const a = statSync(target, { bigint: true });
|
|
1053
|
+
const b = statSync(current, { bigint: true });
|
|
1054
|
+
return a.ino !== b.ino || a.dev !== b.dev;
|
|
1055
|
+
}
|
|
1007
1056
|
export function renameSession(oldName, newName, projectPath) {
|
|
1008
1057
|
try {
|
|
1009
1058
|
const sessionsDir = getSessionsDir(projectPath);
|
|
1010
|
-
const oldPath =
|
|
1011
|
-
const newPath =
|
|
1012
|
-
if (!existsSync(oldPath)) {
|
|
1059
|
+
const oldPath = sessionFile(oldName, projectPath);
|
|
1060
|
+
const newPath = sessionFile(newName, projectPath);
|
|
1061
|
+
if (!oldPath || !newPath || !existsSync(oldPath)) {
|
|
1062
|
+
logSession('rename', `${oldName} -> ${newName}`, false);
|
|
1063
|
+
return false;
|
|
1064
|
+
}
|
|
1065
|
+
// Renaming onto itself changes nothing.
|
|
1066
|
+
if (newName === oldName) {
|
|
1067
|
+
logSession('rename', `${oldName} -> ${newName}`, true);
|
|
1068
|
+
return true;
|
|
1069
|
+
}
|
|
1070
|
+
// Never replace another saved conversation: it would be gone for good.
|
|
1071
|
+
if (sessionNameTaken(newName, oldName, projectPath)) {
|
|
1013
1072
|
logSession('rename', `${oldName} -> ${newName}`, false);
|
|
1014
1073
|
return false;
|
|
1015
1074
|
}
|
|
1016
1075
|
// Read existing session
|
|
1017
1076
|
const data = JSON.parse(readFileSync(oldPath, 'utf-8'));
|
|
1018
|
-
// Update name and save to new path
|
|
1019
1077
|
data.name = newName;
|
|
1020
|
-
|
|
1021
|
-
//
|
|
1022
|
-
|
|
1078
|
+
// Write the renamed copy aside, move the session, then put the copy in
|
|
1079
|
+
// place. The old file is not touched until the move: writing the new
|
|
1080
|
+
// name into it first left a failed move (`feature/auth`) with a session
|
|
1081
|
+
// that lists under a name it cannot be loaded by. Writing the new path
|
|
1082
|
+
// and unlinking the old one would delete the session when a case-only
|
|
1083
|
+
// rename makes both one file.
|
|
1084
|
+
const staged = join(sessionsDir, `.${randomUUID()}.rename.tmp`);
|
|
1085
|
+
writeFileSync(staged, JSON.stringify(data, null, 2));
|
|
1086
|
+
try {
|
|
1087
|
+
renameSync(oldPath, newPath);
|
|
1088
|
+
renameSync(staged, newPath);
|
|
1089
|
+
}
|
|
1090
|
+
finally {
|
|
1091
|
+
if (existsSync(staged))
|
|
1092
|
+
unlinkSync(staged);
|
|
1093
|
+
}
|
|
1023
1094
|
// Update current session ID if it was the renamed one
|
|
1024
1095
|
if (config.get('currentSessionId') === oldName) {
|
|
1025
1096
|
config.set('currentSessionId', newName);
|
|
1026
1097
|
}
|
|
1098
|
+
// A debounced save still aimed at the old name would bring the old file
|
|
1099
|
+
// back beside the renamed one.
|
|
1100
|
+
if (pendingAutoSave?.sessionId === oldName && getSessionsDir(pendingAutoSave.projectPath) === sessionsDir) {
|
|
1101
|
+
pendingAutoSave.sessionId = newName;
|
|
1102
|
+
}
|
|
1027
1103
|
logSession('rename', `${oldName} -> ${newName}`, true);
|
|
1028
1104
|
return true;
|
|
1029
1105
|
}
|
|
@@ -1034,9 +1110,8 @@ export function renameSession(oldName, newName, projectPath) {
|
|
|
1034
1110
|
}
|
|
1035
1111
|
export function getSessionInfo(name, projectPath) {
|
|
1036
1112
|
try {
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
if (existsSync(filePath)) {
|
|
1113
|
+
const filePath = sessionFile(name, projectPath);
|
|
1114
|
+
if (filePath && existsSync(filePath)) {
|
|
1040
1115
|
const data = JSON.parse(readFileSync(filePath, 'utf-8'));
|
|
1041
1116
|
return {
|
|
1042
1117
|
name: data.name,
|
|
@@ -1092,91 +1167,71 @@ export function listSessionsWithInfo(projectPath) {
|
|
|
1092
1167
|
}
|
|
1093
1168
|
}
|
|
1094
1169
|
// Project permission management
|
|
1170
|
+
//
|
|
1171
|
+
// Grants live only in the user's global config, which no repository can
|
|
1172
|
+
// write. Older versions also stored them in the project's own
|
|
1173
|
+
// `.codeep/config.json`; that file can arrive with a cloned repo, and no rule
|
|
1174
|
+
// about its contents holds up (a relative `path` resolves against wherever
|
|
1175
|
+
// Codeep was started), so a grant there no longer counts. A user who granted
|
|
1176
|
+
// access with an older version is asked once more.
|
|
1177
|
+
/** The project's own config file, without creating anything. */
|
|
1178
|
+
function localConfigFile(projectPath) {
|
|
1179
|
+
return isProjectDirectory(projectPath) ? join(projectPath, '.codeep', 'config.json') : null;
|
|
1180
|
+
}
|
|
1181
|
+
function grantMatches(perm, projectPath) {
|
|
1182
|
+
return !!perm && typeof perm === 'object'
|
|
1183
|
+
&& typeof perm.path === 'string'
|
|
1184
|
+
&& isAbsolute(perm.path)
|
|
1185
|
+
&& resolve(perm.path) === resolve(projectPath);
|
|
1186
|
+
}
|
|
1095
1187
|
/**
|
|
1096
|
-
* Get
|
|
1188
|
+
* Get the permission the user granted for a project.
|
|
1097
1189
|
*/
|
|
1098
1190
|
export function getProjectPermission(projectPath) {
|
|
1099
|
-
// First try local .codeep/config.json (for recognized projects)
|
|
1100
|
-
const configPath = getLocalConfigPath(projectPath);
|
|
1101
|
-
if (configPath && existsSync(configPath)) {
|
|
1102
|
-
try {
|
|
1103
|
-
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
1104
|
-
if (data.permission && typeof data.permission === 'object')
|
|
1105
|
-
return data.permission;
|
|
1106
|
-
}
|
|
1107
|
-
catch {
|
|
1108
|
-
// Fall through to global config
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
// Fallback to global config for non-project folders
|
|
1112
1191
|
const permissions = config.get('projectPermissions') || [];
|
|
1113
|
-
return permissions.find((p) => p
|
|
1192
|
+
return permissions.find((p) => grantMatches(p, projectPath)) ?? null;
|
|
1114
1193
|
}
|
|
1115
1194
|
/**
|
|
1116
|
-
*
|
|
1195
|
+
* Record a permission grant in the global config.
|
|
1117
1196
|
*/
|
|
1118
1197
|
export function setProjectPermission(projectPath, read, write) {
|
|
1119
1198
|
const permission = {
|
|
1120
|
-
path: projectPath,
|
|
1199
|
+
path: resolve(projectPath),
|
|
1121
1200
|
readPermission: read,
|
|
1122
1201
|
writePermission: write,
|
|
1123
1202
|
grantedAt: new Date().toISOString(),
|
|
1124
1203
|
};
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
if (configPath) {
|
|
1128
|
-
let data = {};
|
|
1129
|
-
if (existsSync(configPath)) {
|
|
1130
|
-
try {
|
|
1131
|
-
data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
1132
|
-
}
|
|
1133
|
-
catch {
|
|
1134
|
-
// Invalid JSON, start fresh
|
|
1135
|
-
}
|
|
1136
|
-
}
|
|
1137
|
-
data.permission = permission;
|
|
1138
|
-
writeFileSync(configPath, JSON.stringify(data, null, 2));
|
|
1139
|
-
return;
|
|
1140
|
-
}
|
|
1141
|
-
// Fallback to global config for non-project folders
|
|
1142
|
-
const permissions = config.get('projectPermissions') || [];
|
|
1143
|
-
const existingIndex = permissions.findIndex((p) => p.path === projectPath);
|
|
1144
|
-
if (existingIndex >= 0) {
|
|
1145
|
-
permissions[existingIndex] = permission;
|
|
1146
|
-
}
|
|
1147
|
-
else {
|
|
1148
|
-
permissions.push(permission);
|
|
1149
|
-
}
|
|
1204
|
+
const permissions = (config.get('projectPermissions') || []).filter((p) => !grantMatches(p, projectPath));
|
|
1205
|
+
permissions.push(permission);
|
|
1150
1206
|
config.set('projectPermissions', permissions);
|
|
1151
1207
|
}
|
|
1152
1208
|
/**
|
|
1153
|
-
* Remove project permission from
|
|
1209
|
+
* Remove a project's permission from the global config. A grant an older
|
|
1210
|
+
* version wrote into the project for this same directory is removed too, so
|
|
1211
|
+
* it does not linger in a file that may be committed.
|
|
1154
1212
|
*/
|
|
1155
1213
|
export function removeProjectPermission(projectPath) {
|
|
1156
|
-
|
|
1157
|
-
const
|
|
1158
|
-
|
|
1214
|
+
let removed = false;
|
|
1215
|
+
const permissions = config.get('projectPermissions') || [];
|
|
1216
|
+
const kept = permissions.filter((p) => !grantMatches(p, projectPath));
|
|
1217
|
+
if (kept.length !== permissions.length) {
|
|
1218
|
+
config.set('projectPermissions', kept);
|
|
1219
|
+
removed = true;
|
|
1220
|
+
}
|
|
1221
|
+
const configPath = localConfigFile(projectPath);
|
|
1222
|
+
if (configPath && existsSync(configPath) && !leadsOutsideProject(configPath, projectPath)) {
|
|
1159
1223
|
try {
|
|
1160
1224
|
const data = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
1161
|
-
if (data.permission) {
|
|
1225
|
+
if (data && typeof data === 'object' && grantMatches(data.permission, projectPath)) {
|
|
1162
1226
|
delete data.permission;
|
|
1163
|
-
|
|
1164
|
-
return true;
|
|
1227
|
+
writeProjectFile(projectPath, configPath, JSON.stringify(data, null, 2));
|
|
1165
1228
|
}
|
|
1166
1229
|
}
|
|
1167
1230
|
catch {
|
|
1168
|
-
//
|
|
1231
|
+
// Unreadable, symlinked or unwritable: it no longer grants anything.
|
|
1169
1232
|
}
|
|
1170
1233
|
}
|
|
1171
|
-
|
|
1172
|
-
const permissions = config.get('projectPermissions') || [];
|
|
1173
|
-
const existingIndex = permissions.findIndex((p) => p.path === projectPath);
|
|
1174
|
-
if (existingIndex >= 0) {
|
|
1175
|
-
permissions.splice(existingIndex, 1);
|
|
1176
|
-
config.set('projectPermissions', permissions);
|
|
1177
|
-
return true;
|
|
1178
|
-
}
|
|
1179
|
-
return false;
|
|
1234
|
+
return removed;
|
|
1180
1235
|
}
|
|
1181
1236
|
export function hasReadPermission(projectPath) {
|
|
1182
1237
|
const perm = getProjectPermission(projectPath);
|
|
@@ -23,17 +23,45 @@ export interface AppExecutionContext {
|
|
|
23
23
|
setAbortController: (ctrl: AbortController | null) => void;
|
|
24
24
|
formatAddedFilesContext: () => string;
|
|
25
25
|
handleCommand: (command: string, args: string[]) => Promise<void>;
|
|
26
|
+
/** The conversation this run belongs to. Saved under this id; the global
|
|
27
|
+
* currentSessionId is only a fallback and can name a different one. */
|
|
28
|
+
sessionId?: string;
|
|
29
|
+
/** The conversation on screen now. A run can outlive a switch to another
|
|
30
|
+
* one (/new, /sessions, /rename), since tools do not stop on abort. */
|
|
31
|
+
getSessionId?: () => string;
|
|
26
32
|
sessionDisplayName?: string;
|
|
27
33
|
setSessionDisplayName?: (name: string | null) => void;
|
|
28
34
|
}
|
|
29
35
|
export declare function isDangerousTool(toolName: string, parameters: Record<string, unknown>): boolean;
|
|
30
36
|
export declare function requestToolConfirmation(app: App, tool: string, parameters: Record<string, unknown>, onConfirm: () => void, onCancel: () => void): void;
|
|
37
|
+
/**
|
|
38
|
+
* Model-written text shown in a permission dialog, with every character that
|
|
39
|
+
* could change how the rest of it looks spelled out: an ESC sequence would be
|
|
40
|
+
* read as a style (conceal, black on black) and a bidi override or zero-width
|
|
41
|
+
* character reorders or hides text, so the user could approve a command they
|
|
42
|
+
* were not shown. Newlines are left for the caller to lay out.
|
|
43
|
+
*/
|
|
44
|
+
export declare function showControls(text: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* The target of a tool call as dialog lines, each `width` terminal columns at
|
|
47
|
+
* most. Shown whole where it fits: an MCP call's arguments or a long command
|
|
48
|
+
* matter from the first character. Past `maxLines` the middle gives way, and a
|
|
49
|
+
* line says how much of it is not shown.
|
|
50
|
+
*/
|
|
51
|
+
export declare function wrapConfirmTarget(target: string, width: number, maxLines?: number): string[];
|
|
31
52
|
export interface PendingInteractiveContext {
|
|
32
53
|
originalTask: string;
|
|
33
54
|
context: import('../utils/interactive').InteractiveContext;
|
|
34
55
|
dryRun: boolean;
|
|
35
56
|
}
|
|
36
|
-
|
|
37
|
-
|
|
57
|
+
/** How a run ended. 'not-started' covers a run that was refused, declined at
|
|
58
|
+
* a confirmation, or held for clarifying questions. */
|
|
59
|
+
export type AgentRunOutcome = 'success' | 'failed' | 'aborted' | 'interrupted' | 'not-started';
|
|
60
|
+
export interface RunAgentTaskOptions {
|
|
61
|
+
/** Called once, when the run has ended or will not start. */
|
|
62
|
+
onFinished?: (outcome: AgentRunOutcome) => void;
|
|
63
|
+
}
|
|
64
|
+
export declare function runAgentTask(task: string, dryRun: boolean, ctx: AppExecutionContext, getPendingInteractive: () => PendingInteractiveContext | null, setPendingInteractive: (v: PendingInteractiveContext | null) => void, opts?: RunAgentTaskOptions): Promise<void>;
|
|
65
|
+
export declare function executeAgentTask(task: string, dryRun: boolean, ctx: AppExecutionContext): Promise<AgentRunOutcome>;
|
|
38
66
|
export declare function runSkill(nameOrShortcut: string, args: string[], ctx: AppExecutionContext): Promise<boolean>;
|
|
39
67
|
export declare function runCommandChain(commands: string[], index: number, ctx: AppExecutionContext): void;
|