@sumicom/quicksave 0.6.0 → 0.6.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/dist/ai/asyncQueue.d.ts +7 -6
- package/dist/ai/asyncQueue.d.ts.map +1 -1
- package/dist/ai/asyncQueue.js +31 -21
- package/dist/ai/asyncQueue.js.map +1 -1
- package/dist/ai/cardBuilder.d.ts +22 -2
- package/dist/ai/cardBuilder.d.ts.map +1 -1
- package/dist/ai/cardBuilder.js +275 -30
- package/dist/ai/cardBuilder.js.map +1 -1
- package/dist/ai/claudeCliProvider.d.ts +21 -0
- package/dist/ai/claudeCliProvider.d.ts.map +1 -0
- package/dist/ai/claudeCliProvider.js +527 -0
- package/dist/ai/claudeCliProvider.js.map +1 -0
- package/dist/ai/claudeCodeProvider.d.ts +18 -0
- package/dist/ai/claudeCodeProvider.d.ts.map +1 -0
- package/dist/ai/claudeCodeProvider.js +30 -0
- package/dist/ai/claudeCodeProvider.js.map +1 -0
- package/dist/ai/claudeSdkProvider.d.ts +20 -0
- package/dist/ai/claudeSdkProvider.d.ts.map +1 -0
- package/dist/ai/claudeSdkProvider.js +428 -0
- package/dist/ai/claudeSdkProvider.js.map +1 -0
- package/dist/ai/codexMcpProvider.d.ts +15 -0
- package/dist/ai/codexMcpProvider.d.ts.map +1 -0
- package/dist/ai/codexMcpProvider.js +442 -0
- package/dist/ai/codexMcpProvider.js.map +1 -0
- package/dist/ai/codexSdkProvider.d.ts +35 -0
- package/dist/ai/codexSdkProvider.d.ts.map +1 -0
- package/dist/ai/codexSdkProvider.js +422 -0
- package/dist/ai/codexSdkProvider.js.map +1 -0
- package/dist/ai/commitSummary.d.ts +4 -0
- package/dist/ai/commitSummary.d.ts.map +1 -1
- package/dist/ai/commitSummary.js +80 -40
- package/dist/ai/commitSummary.js.map +1 -1
- package/dist/ai/debugLogger.d.ts +24 -0
- package/dist/ai/debugLogger.d.ts.map +1 -0
- package/dist/ai/debugLogger.js +55 -0
- package/dist/ai/debugLogger.js.map +1 -0
- package/dist/ai/provider.d.ts +57 -0
- package/dist/ai/provider.d.ts.map +1 -0
- package/dist/ai/provider.js +2 -0
- package/dist/ai/provider.js.map +1 -0
- package/dist/ai/sandboxMcp.d.ts +12 -0
- package/dist/ai/sandboxMcp.d.ts.map +1 -0
- package/dist/ai/sandboxMcp.js +12 -0
- package/dist/ai/sandboxMcp.js.map +1 -0
- package/dist/ai/sandboxMcpStdio.d.ts +16 -0
- package/dist/ai/sandboxMcpStdio.d.ts.map +1 -0
- package/dist/ai/sandboxMcpStdio.js +127 -0
- package/dist/ai/sandboxMcpStdio.js.map +1 -0
- package/dist/ai/sessionManager.d.ts +105 -0
- package/dist/ai/sessionManager.d.ts.map +1 -0
- package/dist/ai/sessionManager.js +716 -0
- package/dist/ai/sessionManager.js.map +1 -0
- package/dist/ai/sessionRegistry.d.ts +28 -0
- package/dist/ai/sessionRegistry.d.ts.map +1 -0
- package/dist/ai/sessionRegistry.js +148 -0
- package/dist/ai/sessionRegistry.js.map +1 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +12 -0
- package/dist/config.js.map +1 -1
- package/dist/connection/connection.d.ts +1 -0
- package/dist/connection/connection.d.ts.map +1 -1
- package/dist/connection/connection.js +19 -1
- package/dist/connection/connection.js.map +1 -1
- package/dist/git/operations.d.ts +26 -1
- package/dist/git/operations.d.ts.map +1 -1
- package/dist/git/operations.js +83 -2
- package/dist/git/operations.js.map +1 -1
- package/dist/handlers/messageHandler.d.ts +24 -3
- package/dist/handlers/messageHandler.d.ts.map +1 -1
- package/dist/handlers/messageHandler.js +378 -24
- package/dist/handlers/messageHandler.js.map +1 -1
- package/dist/index.js +23 -40
- package/dist/index.js.map +1 -1
- package/dist/service/debugHttpServer.d.ts +27 -0
- package/dist/service/debugHttpServer.d.ts.map +1 -0
- package/dist/service/debugHttpServer.js +189 -0
- package/dist/service/debugHttpServer.js.map +1 -0
- package/dist/service/ensureDaemon.d.ts.map +1 -1
- package/dist/service/ensureDaemon.js +15 -2
- package/dist/service/ensureDaemon.js.map +1 -1
- package/dist/service/ipcClient.js +1 -1
- package/dist/service/run.d.ts.map +1 -1
- package/dist/service/run.js +42 -15
- package/dist/service/run.js.map +1 -1
- package/dist/service/singleton.d.ts +4 -0
- package/dist/service/singleton.d.ts.map +1 -1
- package/dist/service/singleton.js +28 -2
- package/dist/service/singleton.js.map +1 -1
- package/package.json +7 -3
- package/dist/ai/asyncQueue.test.d.ts +0 -2
- package/dist/ai/asyncQueue.test.d.ts.map +0 -1
- package/dist/ai/asyncQueue.test.js +0 -56
- package/dist/ai/asyncQueue.test.js.map +0 -1
- package/dist/ai/claudeCodeService.d.ts +0 -122
- package/dist/ai/claudeCodeService.d.ts.map +0 -1
- package/dist/ai/claudeCodeService.js +0 -869
- package/dist/ai/claudeCodeService.js.map +0 -1
- package/dist/ai/sessionStore.test.d.ts +0 -2
- package/dist/ai/sessionStore.test.d.ts.map +0 -1
- package/dist/ai/sessionStore.test.js +0 -99
- package/dist/ai/sessionStore.test.js.map +0 -1
- package/dist/connection/pubsub.test.d.ts +0 -2
- package/dist/connection/pubsub.test.d.ts.map +0 -1
- package/dist/connection/pubsub.test.js +0 -96
- package/dist/connection/pubsub.test.js.map +0 -1
- package/dist/git/operations.test.d.ts +0 -2
- package/dist/git/operations.test.d.ts.map +0 -1
- package/dist/git/operations.test.js +0 -300
- package/dist/git/operations.test.js.map +0 -1
- package/dist/handlers/messageHandler.test.d.ts +0 -2
- package/dist/handlers/messageHandler.test.d.ts.map +0 -1
- package/dist/handlers/messageHandler.test.js +0 -361
- package/dist/handlers/messageHandler.test.js.map +0 -1
- package/dist/service/ipc.test.d.ts +0 -2
- package/dist/service/ipc.test.d.ts.map +0 -1
- package/dist/service/ipc.test.js +0 -140
- package/dist/service/ipc.test.js.map +0 -1
- package/dist/service/singleton.test.d.ts +0 -2
- package/dist/service/singleton.test.d.ts.map +0 -1
- package/dist/service/singleton.test.js +0 -39
- package/dist/service/singleton.test.js.map +0 -1
|
@@ -0,0 +1,716 @@
|
|
|
1
|
+
import { EventEmitter } from 'events';
|
|
2
|
+
import { resolve, join } from 'path';
|
|
3
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
4
|
+
import { DEFAULT_AGENT, DEFAULT_MODEL, matchAllowPattern, } from '@sumicom/quicksave-shared';
|
|
5
|
+
import { StreamCardBuilder, buildCardsFromHistory, loadPersistedCards } from './cardBuilder.js';
|
|
6
|
+
import { SANDBOX_BASH_TOOL, SET_TITLE_TOOL } from './sandboxMcp.js';
|
|
7
|
+
import { getSessionRegistry } from './sessionRegistry.js';
|
|
8
|
+
/**
|
|
9
|
+
* Events emitted:
|
|
10
|
+
* 'card-event' (event: CardEvent)
|
|
11
|
+
* 'card-stream-end' (result: CardStreamEnd)
|
|
12
|
+
* 'user-input-request' (request: ClaudeUserInputRequestPayload)
|
|
13
|
+
* 'user-input-resolved' ({ requestId, sessionId })
|
|
14
|
+
* 'session-updated' ({ sessionId, isActive, isStreaming, hasPendingInput, permissionMode })
|
|
15
|
+
* 'preferences-updated' (prefs: ClaudePreferences)
|
|
16
|
+
* 'session-config-updated' ({ sessionId, config: Record<string, ConfigValue> })
|
|
17
|
+
*/
|
|
18
|
+
/** Tools auto-approved at each permission level (no user prompt). */
|
|
19
|
+
const AUTO_APPROVE = {
|
|
20
|
+
bypassPermissions: new Set([
|
|
21
|
+
'Edit', 'Write', 'NotebookEdit', 'TodoWrite', 'Agent', 'EnterWorktree', 'ExitWorktree',
|
|
22
|
+
'WebFetch', 'WebSearch', 'Bash',
|
|
23
|
+
'Skill', 'ToolSearch', 'Config',
|
|
24
|
+
'CronCreate', 'CronDelete', 'CronList', 'RemoteTrigger',
|
|
25
|
+
'EnterPlanMode', 'ExitPlanMode',
|
|
26
|
+
'TaskOutput', 'TaskStop',
|
|
27
|
+
]),
|
|
28
|
+
acceptEdits: new Set(['Edit', 'Write', 'NotebookEdit', 'TodoWrite', 'Agent', 'EnterWorktree', 'ExitWorktree', 'EnterPlanMode']),
|
|
29
|
+
default: new Set(['TodoWrite', 'EnterWorktree', 'ExitWorktree', 'Agent', 'EnterPlanMode']),
|
|
30
|
+
plan: new Set(['EnterPlanMode']),
|
|
31
|
+
};
|
|
32
|
+
const DEFAULT_SYSTEM_PROMPT = [
|
|
33
|
+
'For non-destructive shell commands (ls, cat, find, git log, git status, git diff, etc.), prefer SandboxBash over Bash. SandboxBash runs in a sandboxed environment. Use Bash only for commands that modify state.',
|
|
34
|
+
'Call SetTitle to set a descriptive session title when you start a new task or switch context. Keep titles short (e.g. "Fixing auth middleware", "Adding unit tests for UserService"). Update it when the focus changes.',
|
|
35
|
+
].join('\n\n');
|
|
36
|
+
function buildSystemPrompt(extra) {
|
|
37
|
+
return extra ? `${DEFAULT_SYSTEM_PROMPT}\n\n${extra}` : DEFAULT_SYSTEM_PROMPT;
|
|
38
|
+
}
|
|
39
|
+
function normalizeAgentId(value) {
|
|
40
|
+
if (value === 'claude-code' || value === 'claude-cli' || value === 'claude-sdk') {
|
|
41
|
+
return 'claude-code';
|
|
42
|
+
}
|
|
43
|
+
if (value === 'codex' || value === 'codex-mcp') {
|
|
44
|
+
return 'codex';
|
|
45
|
+
}
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
export class SessionManager extends EventEmitter {
|
|
49
|
+
sessions = new Map();
|
|
50
|
+
sessionAgents = new Map();
|
|
51
|
+
sessionPermissions = new Map();
|
|
52
|
+
sessionSandboxed = new Map();
|
|
53
|
+
sessionConfigs = new Map();
|
|
54
|
+
pendingInputRequests = new Map();
|
|
55
|
+
requestCounter = 0;
|
|
56
|
+
runtimeAllowPatterns = [];
|
|
57
|
+
preferences = { model: DEFAULT_MODEL };
|
|
58
|
+
providers;
|
|
59
|
+
defaultAgentId;
|
|
60
|
+
/** Guards against concurrent cold resumes. Queues prompts arriving while a spawn is in flight. */
|
|
61
|
+
coldResumeInFlight = new Map();
|
|
62
|
+
constructor(providers, defaultAgentId = DEFAULT_AGENT) {
|
|
63
|
+
super();
|
|
64
|
+
this.providers = new Map(providers.map((provider) => [provider.id, provider]));
|
|
65
|
+
this.defaultAgentId = this.providers.has(defaultAgentId)
|
|
66
|
+
? defaultAgentId
|
|
67
|
+
: providers[0]?.id ?? DEFAULT_AGENT;
|
|
68
|
+
}
|
|
69
|
+
getProvider(agentId) {
|
|
70
|
+
const resolved = agentId && this.providers.has(agentId)
|
|
71
|
+
? agentId
|
|
72
|
+
: this.defaultAgentId;
|
|
73
|
+
const provider = this.providers.get(resolved);
|
|
74
|
+
if (!provider) {
|
|
75
|
+
throw new Error(`Unknown agent: ${agentId ?? resolved}`);
|
|
76
|
+
}
|
|
77
|
+
return provider;
|
|
78
|
+
}
|
|
79
|
+
resolveAgentId(sessionId, cwd, requestedAgent) {
|
|
80
|
+
const activeAgent = this.sessions.get(sessionId)?.agentId;
|
|
81
|
+
if (activeAgent)
|
|
82
|
+
return activeAgent;
|
|
83
|
+
const rememberedAgent = this.sessionAgents.get(sessionId);
|
|
84
|
+
if (rememberedAgent)
|
|
85
|
+
return rememberedAgent;
|
|
86
|
+
const rawConfig = this.sessionConfigs.get(sessionId);
|
|
87
|
+
const configAgent = normalizeAgentId(rawConfig?.agent ?? rawConfig?.provider);
|
|
88
|
+
if (configAgent && this.providers.has(configAgent)) {
|
|
89
|
+
return configAgent;
|
|
90
|
+
}
|
|
91
|
+
if (cwd) {
|
|
92
|
+
const registryEntry = getSessionRegistry().getEntry(cwd, sessionId);
|
|
93
|
+
const registryAgent = normalizeAgentId(registryEntry?.agent
|
|
94
|
+
?? (registryEntry?.provider));
|
|
95
|
+
if (registryAgent && this.providers.has(registryAgent)) {
|
|
96
|
+
return registryAgent;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
const normalizedRequest = normalizeAgentId(requestedAgent);
|
|
100
|
+
if (normalizedRequest && this.providers.has(normalizedRequest)) {
|
|
101
|
+
return normalizedRequest;
|
|
102
|
+
}
|
|
103
|
+
return this.defaultAgentId;
|
|
104
|
+
}
|
|
105
|
+
// ── Preferences ──
|
|
106
|
+
async initPreferences() {
|
|
107
|
+
// Model is tracked from the last session's init event via onModelDetected callback.
|
|
108
|
+
}
|
|
109
|
+
getPreferences() {
|
|
110
|
+
return { ...this.preferences };
|
|
111
|
+
}
|
|
112
|
+
setPreferences(prefs) {
|
|
113
|
+
const next = { ...this.preferences };
|
|
114
|
+
let changed = false;
|
|
115
|
+
if (prefs.model !== undefined && prefs.model !== this.preferences.model) {
|
|
116
|
+
next.model = prefs.model;
|
|
117
|
+
changed = true;
|
|
118
|
+
}
|
|
119
|
+
if (prefs.reasoningEffort !== undefined && prefs.reasoningEffort !== this.preferences.reasoningEffort) {
|
|
120
|
+
next.reasoningEffort = prefs.reasoningEffort;
|
|
121
|
+
changed = true;
|
|
122
|
+
}
|
|
123
|
+
if (changed) {
|
|
124
|
+
this.preferences = next;
|
|
125
|
+
this.emit('preferences-updated', this.preferences);
|
|
126
|
+
}
|
|
127
|
+
return { ...this.preferences };
|
|
128
|
+
}
|
|
129
|
+
// ── Session Config ──
|
|
130
|
+
getSessionConfig(sessionId) {
|
|
131
|
+
return { ...this.sessionConfigs.get(sessionId) };
|
|
132
|
+
}
|
|
133
|
+
setSessionConfig(sessionId, key, value) {
|
|
134
|
+
const prev = this.sessionConfigs.get(sessionId) ?? {};
|
|
135
|
+
const next = { ...prev, [key]: value };
|
|
136
|
+
this.sessionConfigs.set(sessionId, next);
|
|
137
|
+
if (key === 'model' && typeof value === 'string') {
|
|
138
|
+
this.setPreferences({ model: value });
|
|
139
|
+
}
|
|
140
|
+
else if (key === 'reasoningEffort' && (typeof value === 'string' || value === null)) {
|
|
141
|
+
this.setPreferences({ reasoningEffort: value });
|
|
142
|
+
}
|
|
143
|
+
else if (key === 'permissionMode' && typeof value === 'string') {
|
|
144
|
+
this.setPermissionLevel(sessionId, value);
|
|
145
|
+
this.persistRegistryField(sessionId, 'permissionMode', value);
|
|
146
|
+
}
|
|
147
|
+
else if (key === 'sandboxed' && typeof value === 'boolean') {
|
|
148
|
+
this.sessionSandboxed.set(sessionId, value);
|
|
149
|
+
const ps = this.sessions.get(sessionId);
|
|
150
|
+
if (ps)
|
|
151
|
+
ps.sandboxed = value;
|
|
152
|
+
this.persistRegistryField(sessionId, 'sandboxed', value || undefined);
|
|
153
|
+
}
|
|
154
|
+
else if (key === 'agent' || key === 'provider') {
|
|
155
|
+
// Reject agent changes on active sessions — agent type is immutable once spawned
|
|
156
|
+
const ps = this.sessions.get(sessionId);
|
|
157
|
+
if (ps) {
|
|
158
|
+
this.emit('session-config-updated', { sessionId, config: next });
|
|
159
|
+
return next;
|
|
160
|
+
}
|
|
161
|
+
const agentId = normalizeAgentId(value);
|
|
162
|
+
if (!agentId || !this.providers.has(agentId)) {
|
|
163
|
+
this.emit('session-config-updated', { sessionId, config: next });
|
|
164
|
+
return next;
|
|
165
|
+
}
|
|
166
|
+
this.sessionAgents.set(sessionId, agentId);
|
|
167
|
+
}
|
|
168
|
+
this.emit('session-config-updated', { sessionId, config: next });
|
|
169
|
+
return next;
|
|
170
|
+
}
|
|
171
|
+
// ── Session Lifecycle ──
|
|
172
|
+
async startSession(opts) {
|
|
173
|
+
const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
|
174
|
+
const level = validModes.includes(opts.permissionMode)
|
|
175
|
+
? opts.permissionMode : 'acceptEdits';
|
|
176
|
+
const sandboxed = !!opts.sandboxed;
|
|
177
|
+
const systemPrompt = buildSystemPrompt(opts.systemPrompt);
|
|
178
|
+
const provider = this.getProvider(opts.agent);
|
|
179
|
+
// Create cardBuilder with 'pending' sessionId — will be updated after provider returns real one
|
|
180
|
+
const cardBuilder = new StreamCardBuilder('pending', opts.streamId, opts.cwd);
|
|
181
|
+
const callbacks = this.makeCallbacks(provider.id);
|
|
182
|
+
const { sessionId, session: providerSession } = await provider.startSession({
|
|
183
|
+
prompt: opts.prompt,
|
|
184
|
+
cwd: opts.cwd,
|
|
185
|
+
streamId: opts.streamId,
|
|
186
|
+
model: opts.model,
|
|
187
|
+
permissionLevel: level,
|
|
188
|
+
sandboxed,
|
|
189
|
+
systemPrompt,
|
|
190
|
+
}, cardBuilder, callbacks);
|
|
191
|
+
// Update cardBuilder with real sessionId — new session has no prior JSONL
|
|
192
|
+
cardBuilder.updateSessionId(sessionId);
|
|
193
|
+
cardBuilder.jsonlCutoff = 0;
|
|
194
|
+
// Update callbacks to use real sessionId (re-bind the closures)
|
|
195
|
+
// The callbacks are already handed to the provider; for the permission handler
|
|
196
|
+
// we capture `sessionId` from the request parameter, so this is safe.
|
|
197
|
+
// Register session
|
|
198
|
+
this.sessionAgents.set(sessionId, provider.id);
|
|
199
|
+
this.sessionPermissions.set(sessionId, level);
|
|
200
|
+
if (sandboxed)
|
|
201
|
+
this.sessionSandboxed.set(sessionId, true);
|
|
202
|
+
const prevConfig = this.sessionConfigs.get(sessionId) ?? {};
|
|
203
|
+
this.sessionConfigs.set(sessionId, {
|
|
204
|
+
...prevConfig,
|
|
205
|
+
agent: provider.id,
|
|
206
|
+
...((provider.id !== 'codex' && opts.model)
|
|
207
|
+
? { model: opts.model }
|
|
208
|
+
: (provider.id !== 'codex' && !prevConfig.model)
|
|
209
|
+
? { model: DEFAULT_MODEL }
|
|
210
|
+
: {}),
|
|
211
|
+
permissionMode: level,
|
|
212
|
+
sandboxed,
|
|
213
|
+
});
|
|
214
|
+
const managed = {
|
|
215
|
+
sessionId,
|
|
216
|
+
agentId: provider.id,
|
|
217
|
+
providerSession,
|
|
218
|
+
cwd: opts.cwd,
|
|
219
|
+
streaming: true,
|
|
220
|
+
permissionLevel: level,
|
|
221
|
+
sandboxed,
|
|
222
|
+
cardBuilder,
|
|
223
|
+
};
|
|
224
|
+
this.sessions.set(sessionId, managed);
|
|
225
|
+
this.emitSessionUpdate(sessionId);
|
|
226
|
+
return sessionId;
|
|
227
|
+
}
|
|
228
|
+
async resumeSession(opts) {
|
|
229
|
+
const existing = this.sessions.get(opts.sessionId);
|
|
230
|
+
const agentId = this.resolveAgentId(opts.sessionId, opts.cwd, opts.agent);
|
|
231
|
+
// Hot resume: session is streaming and provider session is alive
|
|
232
|
+
if (existing?.streaming && existing.providerSession?.alive) {
|
|
233
|
+
console.log(`[session-manager] hot resume session=${opts.sessionId.slice(0, 8)} streaming=true`);
|
|
234
|
+
// Add user prompt to cardBuilder for getCards on reconnect
|
|
235
|
+
if (existing.cardBuilder) {
|
|
236
|
+
existing.cardBuilder.userMessage(opts.prompt);
|
|
237
|
+
}
|
|
238
|
+
// Queue the streamId so consumeStream picks it up after the current turn's result
|
|
239
|
+
const ps = existing.providerSession;
|
|
240
|
+
if (ps.pendingStreamIds) {
|
|
241
|
+
ps.pendingStreamIds.push(opts.streamId);
|
|
242
|
+
}
|
|
243
|
+
existing.providerSession.sendUserMessage(opts.prompt);
|
|
244
|
+
return opts.sessionId;
|
|
245
|
+
}
|
|
246
|
+
// Cold resume already in flight — queue the prompt instead of spawning again
|
|
247
|
+
const inFlight = this.coldResumeInFlight.get(opts.sessionId);
|
|
248
|
+
if (inFlight) {
|
|
249
|
+
console.log(`[session-manager] cold resume in flight, queuing prompt for session=${opts.sessionId.slice(0, 8)}`);
|
|
250
|
+
inFlight.queuedPrompts.push(opts.prompt);
|
|
251
|
+
return opts.sessionId;
|
|
252
|
+
}
|
|
253
|
+
// Cold resume: mark as in-flight, then delegate to provider
|
|
254
|
+
console.log(`[session-manager] cold resume session=${opts.sessionId.slice(0, 8)}`);
|
|
255
|
+
const flight = { queuedPrompts: [] };
|
|
256
|
+
this.coldResumeInFlight.set(opts.sessionId, flight);
|
|
257
|
+
try {
|
|
258
|
+
// Restore session settings from in-memory maps, falling back to persisted
|
|
259
|
+
// registry (survives daemon restarts).
|
|
260
|
+
const registryEntry = getSessionRegistry().getEntry(opts.cwd, opts.sessionId);
|
|
261
|
+
const validModes = ['default', 'acceptEdits', 'bypassPermissions', 'plan'];
|
|
262
|
+
const restoredLevel = this.sessionPermissions.get(opts.sessionId)
|
|
263
|
+
?? (validModes.includes(registryEntry?.permissionMode) ? registryEntry.permissionMode : undefined);
|
|
264
|
+
const level = restoredLevel ?? 'acceptEdits';
|
|
265
|
+
const sandboxed = this.sessionSandboxed.get(opts.sessionId) ?? registryEntry?.sandboxed ?? false;
|
|
266
|
+
const provider = this.getProvider(agentId);
|
|
267
|
+
const cardBuilder = existing?.cardBuilder ?? new StreamCardBuilder(opts.sessionId, opts.streamId, opts.cwd);
|
|
268
|
+
await cardBuilder.snapshotCutoff();
|
|
269
|
+
const callbacks = this.makeCallbacks(provider.id);
|
|
270
|
+
const { sessionId, session: providerSession } = await provider.resumeSession({
|
|
271
|
+
sessionId: opts.sessionId,
|
|
272
|
+
prompt: opts.prompt,
|
|
273
|
+
cwd: opts.cwd,
|
|
274
|
+
streamId: opts.streamId,
|
|
275
|
+
permissionLevel: level,
|
|
276
|
+
sandboxed,
|
|
277
|
+
systemPrompt: buildSystemPrompt(),
|
|
278
|
+
}, cardBuilder, callbacks);
|
|
279
|
+
// Update or create session entry
|
|
280
|
+
this.sessionAgents.set(sessionId, provider.id);
|
|
281
|
+
if (existing) {
|
|
282
|
+
existing.agentId = provider.id;
|
|
283
|
+
existing.providerSession = providerSession;
|
|
284
|
+
existing.streaming = true;
|
|
285
|
+
}
|
|
286
|
+
else {
|
|
287
|
+
const managed = {
|
|
288
|
+
sessionId,
|
|
289
|
+
agentId: provider.id,
|
|
290
|
+
providerSession,
|
|
291
|
+
cwd: opts.cwd,
|
|
292
|
+
streaming: true,
|
|
293
|
+
permissionLevel: level,
|
|
294
|
+
sandboxed,
|
|
295
|
+
cardBuilder,
|
|
296
|
+
};
|
|
297
|
+
this.sessions.set(sessionId, managed);
|
|
298
|
+
}
|
|
299
|
+
this.emitSessionUpdate(sessionId);
|
|
300
|
+
// Drain queued prompts that arrived while spawning
|
|
301
|
+
for (const queued of flight.queuedPrompts) {
|
|
302
|
+
console.log(`[session-manager] draining queued prompt for session=${sessionId.slice(0, 8)}`);
|
|
303
|
+
providerSession.sendUserMessage(queued);
|
|
304
|
+
}
|
|
305
|
+
return sessionId;
|
|
306
|
+
}
|
|
307
|
+
finally {
|
|
308
|
+
this.coldResumeInFlight.delete(opts.sessionId);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
async cancelSession(sessionId) {
|
|
312
|
+
const ps = this.sessions.get(sessionId);
|
|
313
|
+
if (!ps?.providerSession)
|
|
314
|
+
return false;
|
|
315
|
+
console.log(`[session-manager] cancel session=${sessionId.slice(0, 8)}`);
|
|
316
|
+
ps.providerSession.interrupt();
|
|
317
|
+
return true;
|
|
318
|
+
}
|
|
319
|
+
closeSession(sessionId) {
|
|
320
|
+
const ps = this.sessions.get(sessionId);
|
|
321
|
+
if (!ps)
|
|
322
|
+
return false;
|
|
323
|
+
if (ps.providerSession) {
|
|
324
|
+
ps.providerSession.kill();
|
|
325
|
+
ps.providerSession = null;
|
|
326
|
+
}
|
|
327
|
+
this.sessions.delete(sessionId);
|
|
328
|
+
this.emitSessionUpdate(sessionId);
|
|
329
|
+
return true;
|
|
330
|
+
}
|
|
331
|
+
// ── Permission ──
|
|
332
|
+
setPermissionLevel(sessionId, level) {
|
|
333
|
+
const ps = this.sessions.get(sessionId);
|
|
334
|
+
if (ps)
|
|
335
|
+
ps.permissionLevel = level;
|
|
336
|
+
this.sessionPermissions.set(sessionId, level);
|
|
337
|
+
this.emit('session-updated', {
|
|
338
|
+
sessionId,
|
|
339
|
+
isActive: !!ps,
|
|
340
|
+
isStreaming: ps?.streaming ?? false,
|
|
341
|
+
hasPendingInput: ps ? Array.from(this.pendingInputRequests.values()).some(p => p.request.sessionId === sessionId) : false,
|
|
342
|
+
permissionMode: level,
|
|
343
|
+
});
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
getPermissionLevel(sessionId) {
|
|
347
|
+
return this.sessions.get(sessionId)?.permissionLevel ?? 'acceptEdits';
|
|
348
|
+
}
|
|
349
|
+
resolveUserInput(response) {
|
|
350
|
+
const pending = this.pendingInputRequests.get(response.requestId);
|
|
351
|
+
if (!pending)
|
|
352
|
+
return false;
|
|
353
|
+
this.pendingInputRequests.delete(response.requestId);
|
|
354
|
+
pending.resolve(response);
|
|
355
|
+
// Clear pending input on the card
|
|
356
|
+
const ps = this.sessions.get(pending.request.sessionId);
|
|
357
|
+
if (ps?.cardBuilder) {
|
|
358
|
+
const cardEvt = ps.cardBuilder.clearPendingInput(response.requestId);
|
|
359
|
+
if (cardEvt)
|
|
360
|
+
this.emit('card-event', cardEvt);
|
|
361
|
+
}
|
|
362
|
+
// Persist allow pattern to settings.local.json if provided
|
|
363
|
+
if (response.allowPattern && ps?.cwd) {
|
|
364
|
+
this.persistAllowPattern(ps.cwd, response.allowPattern).catch((err) => {
|
|
365
|
+
console.error('[persistAllowPattern] failed:', err);
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
this.emit('user-input-resolved', { requestId: response.requestId, sessionId: pending.request.sessionId });
|
|
369
|
+
this.emitSessionUpdate(pending.request.sessionId);
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
// ── Query ──
|
|
373
|
+
getActiveSessions() {
|
|
374
|
+
const pendingSessionIds = new Set(Array.from(this.pendingInputRequests.values()).map(p => p.request.sessionId));
|
|
375
|
+
return Array.from(this.sessions.entries()).map(([id, ps]) => ({
|
|
376
|
+
sessionId: id,
|
|
377
|
+
cwd: ps.cwd,
|
|
378
|
+
agent: ps.agentId,
|
|
379
|
+
isStreaming: ps.streaming,
|
|
380
|
+
hasPendingInput: pendingSessionIds.has(id),
|
|
381
|
+
permissionMode: ps.permissionLevel,
|
|
382
|
+
sandboxed: ps.sandboxed || undefined,
|
|
383
|
+
}));
|
|
384
|
+
}
|
|
385
|
+
isStreaming(sessionId) {
|
|
386
|
+
return this.sessions.get(sessionId)?.streaming ?? false;
|
|
387
|
+
}
|
|
388
|
+
isOpen(sessionId) {
|
|
389
|
+
return this.sessions.has(sessionId);
|
|
390
|
+
}
|
|
391
|
+
getSessionCwd(sessionId) {
|
|
392
|
+
return this.sessions.get(sessionId)?.cwd;
|
|
393
|
+
}
|
|
394
|
+
getSessionAgent(sessionId, cwd) {
|
|
395
|
+
return this.resolveAgentId(sessionId, cwd);
|
|
396
|
+
}
|
|
397
|
+
getActiveSessionCount() {
|
|
398
|
+
return this.sessions.size;
|
|
399
|
+
}
|
|
400
|
+
getCardBuilder(sessionId) {
|
|
401
|
+
return this.sessions.get(sessionId)?.cardBuilder ?? null;
|
|
402
|
+
}
|
|
403
|
+
async listAvailableSessions(cwd) {
|
|
404
|
+
const registry = getSessionRegistry();
|
|
405
|
+
const registryEntries = registry.getEntriesForProject(cwd);
|
|
406
|
+
const pendingSessionIds = new Set(Array.from(this.pendingInputRequests.values()).map(p => p.request.sessionId));
|
|
407
|
+
return registryEntries.map(entry => ({
|
|
408
|
+
sessionId: entry.sessionId,
|
|
409
|
+
summary: entry.title ?? entry.firstPrompt ?? entry.sessionId.slice(0, 8),
|
|
410
|
+
lastModified: entry.lastAccessedAt,
|
|
411
|
+
createdAt: entry.createdAt,
|
|
412
|
+
cwd: entry.cwd,
|
|
413
|
+
agent: this.sessions.get(entry.sessionId)?.agentId
|
|
414
|
+
?? this.sessionAgents.get(entry.sessionId)
|
|
415
|
+
?? entry.agent
|
|
416
|
+
?? (entry.provider === 'codex-mcp' ? 'codex' : entry.provider ? 'claude-code' : undefined),
|
|
417
|
+
gitBranch: entry.gitBranch,
|
|
418
|
+
messageCount: entry.messageCount,
|
|
419
|
+
isActive: this.sessions.has(entry.sessionId),
|
|
420
|
+
isStreaming: this.sessions.get(entry.sessionId)?.streaming ?? false,
|
|
421
|
+
hasPendingInput: pendingSessionIds.has(entry.sessionId),
|
|
422
|
+
permissionMode: this.sessions.get(entry.sessionId)?.permissionLevel
|
|
423
|
+
?? this.sessionPermissions.get(entry.sessionId),
|
|
424
|
+
}));
|
|
425
|
+
}
|
|
426
|
+
async getCards(sessionId, cwd, offset = 0, limit = 50) {
|
|
427
|
+
const ps = this.sessions.get(sessionId);
|
|
428
|
+
const provider = this.getProvider(this.resolveAgentId(sessionId, cwd));
|
|
429
|
+
const cutoff = ps?.cardBuilder?.jsonlCutoff ?? undefined;
|
|
430
|
+
let result;
|
|
431
|
+
if (provider.historyMode === 'claude-jsonl') {
|
|
432
|
+
// Read JSONL history up to the cutoff (excludes the active turn's messages).
|
|
433
|
+
result = await buildCardsFromHistory(sessionId, cwd, offset, limit, cutoff);
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
// Memory-mode: use in-memory cards from active turn, falling back to persisted history
|
|
437
|
+
const streamCards = ps?.cardBuilder?.getCards() ?? [];
|
|
438
|
+
const persisted = await loadPersistedCards(sessionId);
|
|
439
|
+
const cards = [...persisted, ...streamCards];
|
|
440
|
+
const total = cards.length;
|
|
441
|
+
const start = Math.max(0, total - offset - limit);
|
|
442
|
+
const end = Math.max(0, total - offset);
|
|
443
|
+
result = {
|
|
444
|
+
cards: cards.slice(start, end),
|
|
445
|
+
total,
|
|
446
|
+
hasMore: start > 0,
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
// Append in-memory cards for the active turn (initial load only — pagination
|
|
450
|
+
// already has these cards in the PWA's array, so appending them again would duplicate).
|
|
451
|
+
if (offset === 0 && provider.historyMode === 'claude-jsonl' && ps?.cardBuilder) {
|
|
452
|
+
const streamingCards = ps.cardBuilder.getCards();
|
|
453
|
+
if (streamingCards.length > 0) {
|
|
454
|
+
result.cards.push(...streamingCards);
|
|
455
|
+
result.total += streamingCards.length;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
// Overlay pending inputs from pendingInputRequests onto matching cards.
|
|
459
|
+
const pendingByToolUseId = new Map();
|
|
460
|
+
for (const [, pending] of this.pendingInputRequests) {
|
|
461
|
+
if (pending.request.sessionId === sessionId && pending.request.toolUseId) {
|
|
462
|
+
pendingByToolUseId.set(pending.request.toolUseId, {
|
|
463
|
+
sessionId: pending.request.sessionId,
|
|
464
|
+
requestId: pending.request.requestId,
|
|
465
|
+
inputType: pending.request.inputType,
|
|
466
|
+
title: pending.request.title,
|
|
467
|
+
message: pending.request.message,
|
|
468
|
+
options: pending.request.options,
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
if (pendingByToolUseId.size > 0) {
|
|
473
|
+
for (const card of result.cards) {
|
|
474
|
+
if (card.type === 'tool_call' && card.toolUseId) {
|
|
475
|
+
const pending = pendingByToolUseId.get(card.toolUseId);
|
|
476
|
+
if (pending && !card.pendingInput) {
|
|
477
|
+
card.pendingInput = pending;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
// Attach session title from config or registry
|
|
483
|
+
const configTitle = this.sessionConfigs.get(sessionId)?.title;
|
|
484
|
+
if (configTitle) {
|
|
485
|
+
result.title = configTitle;
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
const registry = getSessionRegistry();
|
|
489
|
+
const entry = registry.getEntry(cwd, sessionId);
|
|
490
|
+
if (entry?.title)
|
|
491
|
+
result.title = entry.title;
|
|
492
|
+
}
|
|
493
|
+
return result;
|
|
494
|
+
}
|
|
495
|
+
getPendingInputRequests() {
|
|
496
|
+
return Array.from(this.pendingInputRequests.values()).map(p => p.request);
|
|
497
|
+
}
|
|
498
|
+
getDebugState() {
|
|
499
|
+
const pendingInputs = Array.from(this.pendingInputRequests.values()).map(p => ({
|
|
500
|
+
requestId: p.request.requestId,
|
|
501
|
+
sessionId: p.request.sessionId,
|
|
502
|
+
toolName: p.request.toolName,
|
|
503
|
+
agentId: p.request.agentId,
|
|
504
|
+
inputType: p.request.inputType,
|
|
505
|
+
}));
|
|
506
|
+
return { pendingInputs, activeSessions: this.getActiveSessions() };
|
|
507
|
+
}
|
|
508
|
+
cleanup() {
|
|
509
|
+
for (const [requestId, pending] of this.pendingInputRequests) {
|
|
510
|
+
pending.resolve({ sessionId: '', requestId, action: 'allow' });
|
|
511
|
+
}
|
|
512
|
+
this.pendingInputRequests.clear();
|
|
513
|
+
for (const [, ps] of this.sessions) {
|
|
514
|
+
if (ps.providerSession) {
|
|
515
|
+
try {
|
|
516
|
+
ps.providerSession.kill();
|
|
517
|
+
}
|
|
518
|
+
catch { }
|
|
519
|
+
ps.providerSession = null;
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
this.sessions.clear();
|
|
523
|
+
}
|
|
524
|
+
// ── Private: Callbacks Factory ──
|
|
525
|
+
makeCallbacks(agentId) {
|
|
526
|
+
return {
|
|
527
|
+
emitCardEvent: (event) => {
|
|
528
|
+
this.emit('card-event', event);
|
|
529
|
+
},
|
|
530
|
+
emitStreamEnd: (result) => {
|
|
531
|
+
this.emit('card-stream-end', result);
|
|
532
|
+
// Update session streaming state
|
|
533
|
+
const ps = this.sessions.get(result.sessionId);
|
|
534
|
+
if (ps) {
|
|
535
|
+
ps.streaming = false;
|
|
536
|
+
this.emitSessionUpdate(result.sessionId);
|
|
537
|
+
}
|
|
538
|
+
},
|
|
539
|
+
handlePermissionRequest: async (sessionId, req) => {
|
|
540
|
+
return this.handlePermissionRequest(sessionId, req);
|
|
541
|
+
},
|
|
542
|
+
onModelDetected: (model) => {
|
|
543
|
+
if (agentId === 'claude-code') {
|
|
544
|
+
this.preferences.model = model;
|
|
545
|
+
}
|
|
546
|
+
},
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
// ── Private: Permission Handling ──
|
|
550
|
+
async handlePermissionRequest(sessionId, req) {
|
|
551
|
+
const { toolName, toolInput, toolUseId } = req;
|
|
552
|
+
// Check auto-approve rules first
|
|
553
|
+
if (this.shouldAutoApprove(sessionId, toolName, toolInput)) {
|
|
554
|
+
return { action: 'allow' };
|
|
555
|
+
}
|
|
556
|
+
// Check runtime allow patterns
|
|
557
|
+
if (this.runtimeAllowPatterns.some(p => matchAllowPattern(p, toolName, toolInput))) {
|
|
558
|
+
return { action: 'allow' };
|
|
559
|
+
}
|
|
560
|
+
// Forward to PWA for user decision
|
|
561
|
+
const requestId = `perm-${++this.requestCounter}`;
|
|
562
|
+
const isQuestion = toolName === 'AskUserQuestion';
|
|
563
|
+
const questions = isQuestion ? toolInput.questions : undefined;
|
|
564
|
+
const request = {
|
|
565
|
+
sessionId,
|
|
566
|
+
requestId,
|
|
567
|
+
inputType: isQuestion ? 'question' : 'permission',
|
|
568
|
+
title: isQuestion
|
|
569
|
+
? questions?.[0]?.question ?? 'Question from Claude'
|
|
570
|
+
: `Allow ${toolName}?`,
|
|
571
|
+
message: isQuestion ? undefined : JSON.stringify(toolInput).slice(0, 500),
|
|
572
|
+
toolName,
|
|
573
|
+
toolInput,
|
|
574
|
+
toolUseId,
|
|
575
|
+
...(isQuestion && questions ? {
|
|
576
|
+
options: questions.flatMap((q) => (q.options ?? []).map((opt) => ({
|
|
577
|
+
key: opt.label,
|
|
578
|
+
label: opt.label,
|
|
579
|
+
description: opt.description,
|
|
580
|
+
}))),
|
|
581
|
+
} : {}),
|
|
582
|
+
};
|
|
583
|
+
// Create card with pending input
|
|
584
|
+
const ps = this.sessions.get(sessionId);
|
|
585
|
+
const cb = ps?.cardBuilder;
|
|
586
|
+
if (cb) {
|
|
587
|
+
const pendingAttachment = {
|
|
588
|
+
sessionId,
|
|
589
|
+
requestId,
|
|
590
|
+
inputType: request.inputType,
|
|
591
|
+
title: request.title,
|
|
592
|
+
message: request.message,
|
|
593
|
+
options: request.options,
|
|
594
|
+
};
|
|
595
|
+
const cardEvt = cb.toolCallFromPermission(toolName, toolInput, toolUseId, pendingAttachment);
|
|
596
|
+
this.emit('card-event', cardEvt);
|
|
597
|
+
}
|
|
598
|
+
this.emit('user-input-request', request);
|
|
599
|
+
// Wait for user response
|
|
600
|
+
const response = await this.waitForUserInput(requestId, request);
|
|
601
|
+
if (response.action === 'deny') {
|
|
602
|
+
return { action: 'deny', response: response.response || 'User denied permission' };
|
|
603
|
+
}
|
|
604
|
+
// For AskUserQuestion, inject answer
|
|
605
|
+
if (isQuestion && response.response) {
|
|
606
|
+
const answers = {};
|
|
607
|
+
if (questions?.[0]?.question) {
|
|
608
|
+
answers[questions[0].question] = response.response;
|
|
609
|
+
}
|
|
610
|
+
return { action: 'allow', updatedInput: { ...toolInput, answers } };
|
|
611
|
+
}
|
|
612
|
+
return { action: 'allow' };
|
|
613
|
+
}
|
|
614
|
+
shouldAutoApprove(sessionId, toolName, input) {
|
|
615
|
+
// SetTitle — always approve
|
|
616
|
+
if (toolName === SET_TITLE_TOOL) {
|
|
617
|
+
if (input.title)
|
|
618
|
+
this.updateSessionTitle(sessionId, input.title);
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
621
|
+
// SandboxBash: auto-approve when sandboxed, otherwise check as Bash
|
|
622
|
+
if (toolName === SANDBOX_BASH_TOOL) {
|
|
623
|
+
if (this.sessions.get(sessionId)?.sandboxed)
|
|
624
|
+
return true;
|
|
625
|
+
toolName = 'Bash'; // fall through to permission check as Bash
|
|
626
|
+
}
|
|
627
|
+
// Check permission level auto-approve set
|
|
628
|
+
const level = this.sessions.get(sessionId)?.permissionLevel
|
|
629
|
+
?? this.sessionPermissions.get(sessionId) ?? 'acceptEdits';
|
|
630
|
+
if (!AUTO_APPROVE[level].has(toolName))
|
|
631
|
+
return false;
|
|
632
|
+
// For file-writing tools, restrict to project cwd
|
|
633
|
+
const FILE_WRITE_TOOLS = new Set(['Write', 'Edit', 'NotebookEdit']);
|
|
634
|
+
if (FILE_WRITE_TOOLS.has(toolName)) {
|
|
635
|
+
const cwd = this.sessions.get(sessionId)?.cwd;
|
|
636
|
+
const filePath = (input.file_path ?? input.path);
|
|
637
|
+
if (filePath && cwd) {
|
|
638
|
+
const resolvedFile = resolve(filePath);
|
|
639
|
+
const resolvedCwd = resolve(cwd);
|
|
640
|
+
return resolvedFile === resolvedCwd || resolvedFile.startsWith(resolvedCwd + '/');
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
return true;
|
|
644
|
+
}
|
|
645
|
+
/** Update session title (triggered by SetTitle MCP tool). */
|
|
646
|
+
updateSessionTitle(sessionId, title) {
|
|
647
|
+
const config = this.sessionConfigs.get(sessionId) ?? {};
|
|
648
|
+
this.sessionConfigs.set(sessionId, { ...config, title });
|
|
649
|
+
this.emit('session-config-updated', { sessionId, config: this.sessionConfigs.get(sessionId) });
|
|
650
|
+
// Update session registry so it persists across restarts
|
|
651
|
+
const ps = this.sessions.get(sessionId);
|
|
652
|
+
if (ps?.cwd) {
|
|
653
|
+
const registry = getSessionRegistry();
|
|
654
|
+
const entry = registry.getEntry(ps.cwd, sessionId);
|
|
655
|
+
if (entry) {
|
|
656
|
+
registry.upsertEntry({ ...entry, title, lastAccessedAt: Date.now() });
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
emitSessionUpdate(sessionId) {
|
|
661
|
+
const ps = this.sessions.get(sessionId);
|
|
662
|
+
const hasPendingInput = Array.from(this.pendingInputRequests.values())
|
|
663
|
+
.some(p => p.request.sessionId === sessionId);
|
|
664
|
+
this.emit('session-updated', {
|
|
665
|
+
sessionId,
|
|
666
|
+
isActive: !!ps,
|
|
667
|
+
agent: ps?.agentId ?? this.sessionAgents.get(sessionId),
|
|
668
|
+
isStreaming: ps?.streaming ?? false,
|
|
669
|
+
hasPendingInput,
|
|
670
|
+
permissionMode: ps?.permissionLevel ?? this.sessionPermissions.get(sessionId),
|
|
671
|
+
sandboxed: ps?.sandboxed ?? this.sessionSandboxed.get(sessionId) ?? false,
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
persistRegistryField(sessionId, key, value) {
|
|
675
|
+
const registry = getSessionRegistry();
|
|
676
|
+
// Active session — cwd is in memory
|
|
677
|
+
const cwd = this.sessions.get(sessionId)?.cwd;
|
|
678
|
+
if (cwd) {
|
|
679
|
+
registry.updateEntry(cwd, sessionId, { [key]: value });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
// Inactive session — scan registry for the entry
|
|
683
|
+
for (const entry of registry.getEntriesForProject()) {
|
|
684
|
+
if (entry.sessionId === sessionId) {
|
|
685
|
+
registry.updateEntry(entry.cwd, sessionId, { [key]: value });
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
async persistAllowPattern(cwd, pattern) {
|
|
691
|
+
if (!this.runtimeAllowPatterns.includes(pattern)) {
|
|
692
|
+
this.runtimeAllowPatterns.push(pattern);
|
|
693
|
+
}
|
|
694
|
+
const settingsPath = join(cwd, '.claude', 'settings.local.json');
|
|
695
|
+
let settings = {};
|
|
696
|
+
try {
|
|
697
|
+
settings = JSON.parse(await readFile(settingsPath, 'utf-8'));
|
|
698
|
+
}
|
|
699
|
+
catch { /* file doesn't exist yet */ }
|
|
700
|
+
const permissions = settings.permissions ?? {};
|
|
701
|
+
const allow = Array.isArray(permissions.allow) ? [...permissions.allow] : [];
|
|
702
|
+
if (!allow.includes(pattern)) {
|
|
703
|
+
allow.push(pattern);
|
|
704
|
+
settings.permissions = { ...permissions, allow };
|
|
705
|
+
await mkdir(join(cwd, '.claude'), { recursive: true });
|
|
706
|
+
await writeFile(settingsPath, JSON.stringify(settings, null, 2) + '\n');
|
|
707
|
+
console.log(`[persistAllowPattern] saved ${pattern} to ${settingsPath}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
waitForUserInput(requestId, request) {
|
|
711
|
+
return new Promise((resolve) => {
|
|
712
|
+
this.pendingInputRequests.set(requestId, { resolve, request });
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
//# sourceMappingURL=sessionManager.js.map
|