@doingdev/opencode-claude-manager-plugin 0.1.19 → 0.1.21
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 +40 -89
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -4
- package/dist/plugin/orchestrator.plugin.d.ts +14 -0
- package/dist/plugin/orchestrator.plugin.js +108 -0
- package/dist/prompts/registry.d.ts +8 -2
- package/dist/prompts/registry.js +30 -133
- package/dist/safety/bash-safety.d.ts +21 -0
- package/dist/safety/bash-safety.js +62 -0
- package/package.json +3 -6
- package/dist/claude/claude-agent-sdk-adapter.d.ts +0 -27
- package/dist/claude/claude-agent-sdk-adapter.js +0 -520
- package/dist/claude/claude-session.service.d.ts +0 -15
- package/dist/claude/claude-session.service.js +0 -23
- package/dist/claude/delegated-can-use-tool.d.ts +0 -7
- package/dist/claude/delegated-can-use-tool.js +0 -178
- package/dist/claude/session-live-tailer.d.ts +0 -51
- package/dist/claude/session-live-tailer.js +0 -269
- package/dist/claude/tool-approval-manager.d.ts +0 -27
- package/dist/claude/tool-approval-manager.js +0 -238
- package/dist/manager/context-tracker.d.ts +0 -33
- package/dist/manager/context-tracker.js +0 -108
- package/dist/manager/git-operations.d.ts +0 -12
- package/dist/manager/git-operations.js +0 -76
- package/dist/manager/manager-orchestrator.d.ts +0 -17
- package/dist/manager/manager-orchestrator.js +0 -178
- package/dist/manager/persistent-manager.d.ts +0 -73
- package/dist/manager/persistent-manager.js +0 -167
- package/dist/manager/session-controller.d.ts +0 -45
- package/dist/manager/session-controller.js +0 -147
- package/dist/manager/task-planner.d.ts +0 -5
- package/dist/manager/task-planner.js +0 -15
- package/dist/metadata/claude-metadata.service.d.ts +0 -12
- package/dist/metadata/claude-metadata.service.js +0 -38
- package/dist/metadata/repo-claude-config-reader.d.ts +0 -7
- package/dist/metadata/repo-claude-config-reader.js +0 -154
- package/dist/plugin/claude-code-permission-bridge.d.ts +0 -15
- package/dist/plugin/claude-code-permission-bridge.js +0 -184
- package/dist/plugin/claude-manager.plugin.d.ts +0 -2
- package/dist/plugin/claude-manager.plugin.js +0 -578
- package/dist/plugin/service-factory.d.ts +0 -12
- package/dist/plugin/service-factory.js +0 -41
- package/dist/state/file-run-state-store.d.ts +0 -14
- package/dist/state/file-run-state-store.js +0 -87
- package/dist/state/transcript-store.d.ts +0 -15
- package/dist/state/transcript-store.js +0 -44
- package/dist/types/contracts.d.ts +0 -215
- package/dist/types/contracts.js +0 -1
- package/dist/util/fs-helpers.d.ts +0 -2
- package/dist/util/fs-helpers.js +0 -12
- package/dist/util/transcript-append.d.ts +0 -7
- package/dist/util/transcript-append.js +0 -29
- package/dist/worktree/worktree-coordinator.d.ts +0 -21
- package/dist/worktree/worktree-coordinator.js +0 -64
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import type { ClaudeSessionEvent, ClaudeSessionRunResult, PersistentRunRecord, PersistentRunResult, SessionContextSnapshot, GitDiffResult, GitOperationResult } from '../types/contracts.js';
|
|
2
|
-
import type { ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
|
|
3
|
-
import type { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
4
|
-
import type { TranscriptStore } from '../state/transcript-store.js';
|
|
5
|
-
import type { SessionController } from './session-controller.js';
|
|
6
|
-
import type { GitOperations } from './git-operations.js';
|
|
7
|
-
import type { ContextTracker } from './context-tracker.js';
|
|
8
|
-
type PersistentManagerProgressHandler = (run: PersistentRunRecord) => void | Promise<void>;
|
|
9
|
-
export declare class PersistentManager {
|
|
10
|
-
private readonly sessionController;
|
|
11
|
-
private readonly gitOps;
|
|
12
|
-
private readonly stateStore;
|
|
13
|
-
private readonly contextTracker;
|
|
14
|
-
private readonly transcriptStore;
|
|
15
|
-
constructor(sessionController: SessionController, gitOps: GitOperations, stateStore: FileRunStateStore, contextTracker: ContextTracker, transcriptStore: TranscriptStore);
|
|
16
|
-
/**
|
|
17
|
-
* Send a message to the persistent Claude Code session.
|
|
18
|
-
* Creates a new session if none exists.
|
|
19
|
-
*/
|
|
20
|
-
sendMessage(cwd: string, message: string, options?: {
|
|
21
|
-
model?: string;
|
|
22
|
-
mode?: 'plan' | 'free';
|
|
23
|
-
abortSignal?: AbortSignal;
|
|
24
|
-
}, onEvent?: ClaudeSessionEventHandler): Promise<{
|
|
25
|
-
sessionId: string | undefined;
|
|
26
|
-
finalText: string;
|
|
27
|
-
turns?: number;
|
|
28
|
-
totalCostUsd?: number;
|
|
29
|
-
context: SessionContextSnapshot;
|
|
30
|
-
}>;
|
|
31
|
-
/**
|
|
32
|
-
* Get the current git diff.
|
|
33
|
-
*/
|
|
34
|
-
gitDiff(): Promise<GitDiffResult>;
|
|
35
|
-
/**
|
|
36
|
-
* Commit all current changes.
|
|
37
|
-
*/
|
|
38
|
-
gitCommit(message: string): Promise<GitOperationResult>;
|
|
39
|
-
/**
|
|
40
|
-
* Hard reset to discard all uncommitted changes.
|
|
41
|
-
*/
|
|
42
|
-
gitReset(): Promise<GitOperationResult>;
|
|
43
|
-
/**
|
|
44
|
-
* Get current session status and context health.
|
|
45
|
-
*/
|
|
46
|
-
getStatus(): SessionContextSnapshot;
|
|
47
|
-
/**
|
|
48
|
-
* Clear the active session. Next send creates a fresh one.
|
|
49
|
-
*/
|
|
50
|
-
clearSession(cwd: string): Promise<string | null>;
|
|
51
|
-
/**
|
|
52
|
-
* Compact the current session to free context.
|
|
53
|
-
*/
|
|
54
|
-
compactSession(cwd: string, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
55
|
-
/**
|
|
56
|
-
* Read persisted transcript events for a session.
|
|
57
|
-
*/
|
|
58
|
-
getTranscriptEvents(cwd: string, sessionId: string): Promise<ClaudeSessionEvent[]>;
|
|
59
|
-
/**
|
|
60
|
-
* Execute a full task with run tracking.
|
|
61
|
-
* Creates a run record, sends the message, and persists the result.
|
|
62
|
-
*/
|
|
63
|
-
executeTask(cwd: string, task: string, options?: {
|
|
64
|
-
model?: string;
|
|
65
|
-
}, onProgress?: PersistentManagerProgressHandler): Promise<PersistentRunResult>;
|
|
66
|
-
/**
|
|
67
|
-
* Try to restore session state from disk on startup.
|
|
68
|
-
*/
|
|
69
|
-
tryRestore(cwd: string): Promise<boolean>;
|
|
70
|
-
listRuns(cwd: string): Promise<PersistentRunRecord[]>;
|
|
71
|
-
getRun(cwd: string, runId: string): Promise<PersistentRunRecord | null>;
|
|
72
|
-
}
|
|
73
|
-
export {};
|
|
@@ -1,167 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
export class PersistentManager {
|
|
3
|
-
sessionController;
|
|
4
|
-
gitOps;
|
|
5
|
-
stateStore;
|
|
6
|
-
contextTracker;
|
|
7
|
-
transcriptStore;
|
|
8
|
-
constructor(sessionController, gitOps, stateStore, contextTracker, transcriptStore) {
|
|
9
|
-
this.sessionController = sessionController;
|
|
10
|
-
this.gitOps = gitOps;
|
|
11
|
-
this.stateStore = stateStore;
|
|
12
|
-
this.contextTracker = contextTracker;
|
|
13
|
-
this.transcriptStore = transcriptStore;
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Send a message to the persistent Claude Code session.
|
|
17
|
-
* Creates a new session if none exists.
|
|
18
|
-
*/
|
|
19
|
-
async sendMessage(cwd, message, options, onEvent) {
|
|
20
|
-
const result = await this.sessionController.sendMessage(cwd, message, options, onEvent);
|
|
21
|
-
if (result.sessionId && result.events.length > 0) {
|
|
22
|
-
await this.transcriptStore.appendEvents(cwd, result.sessionId, result.events);
|
|
23
|
-
}
|
|
24
|
-
return {
|
|
25
|
-
sessionId: result.sessionId,
|
|
26
|
-
finalText: result.finalText,
|
|
27
|
-
turns: result.turns,
|
|
28
|
-
totalCostUsd: result.totalCostUsd,
|
|
29
|
-
context: this.sessionController.getContextSnapshot(),
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Get the current git diff.
|
|
34
|
-
*/
|
|
35
|
-
async gitDiff() {
|
|
36
|
-
return this.gitOps.diff();
|
|
37
|
-
}
|
|
38
|
-
/**
|
|
39
|
-
* Commit all current changes.
|
|
40
|
-
*/
|
|
41
|
-
async gitCommit(message) {
|
|
42
|
-
return this.gitOps.commit(message);
|
|
43
|
-
}
|
|
44
|
-
/**
|
|
45
|
-
* Hard reset to discard all uncommitted changes.
|
|
46
|
-
*/
|
|
47
|
-
async gitReset() {
|
|
48
|
-
return this.gitOps.resetHard();
|
|
49
|
-
}
|
|
50
|
-
/**
|
|
51
|
-
* Get current session status and context health.
|
|
52
|
-
*/
|
|
53
|
-
getStatus() {
|
|
54
|
-
return this.sessionController.getContextSnapshot();
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Clear the active session. Next send creates a fresh one.
|
|
58
|
-
*/
|
|
59
|
-
async clearSession(cwd) {
|
|
60
|
-
return this.sessionController.clearSession(cwd);
|
|
61
|
-
}
|
|
62
|
-
/**
|
|
63
|
-
* Compact the current session to free context.
|
|
64
|
-
*/
|
|
65
|
-
async compactSession(cwd, onEvent) {
|
|
66
|
-
const result = await this.sessionController.compactSession(cwd, onEvent);
|
|
67
|
-
if (result.sessionId && result.events.length > 0) {
|
|
68
|
-
await this.transcriptStore.appendEvents(cwd, result.sessionId, result.events);
|
|
69
|
-
}
|
|
70
|
-
return result;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Read persisted transcript events for a session.
|
|
74
|
-
*/
|
|
75
|
-
getTranscriptEvents(cwd, sessionId) {
|
|
76
|
-
return this.transcriptStore.readEvents(cwd, sessionId);
|
|
77
|
-
}
|
|
78
|
-
/**
|
|
79
|
-
* Execute a full task with run tracking.
|
|
80
|
-
* Creates a run record, sends the message, and persists the result.
|
|
81
|
-
*/
|
|
82
|
-
async executeTask(cwd, task, options, onProgress) {
|
|
83
|
-
const runId = randomUUID();
|
|
84
|
-
const createdAt = new Date().toISOString();
|
|
85
|
-
const runRecord = {
|
|
86
|
-
id: runId,
|
|
87
|
-
cwd,
|
|
88
|
-
task,
|
|
89
|
-
status: 'running',
|
|
90
|
-
createdAt,
|
|
91
|
-
updatedAt: createdAt,
|
|
92
|
-
sessionId: this.sessionController.sessionId,
|
|
93
|
-
sessionHistory: [],
|
|
94
|
-
messages: [
|
|
95
|
-
{
|
|
96
|
-
timestamp: createdAt,
|
|
97
|
-
direction: 'sent',
|
|
98
|
-
text: task,
|
|
99
|
-
},
|
|
100
|
-
],
|
|
101
|
-
actions: [],
|
|
102
|
-
commits: [],
|
|
103
|
-
context: this.sessionController.getContextSnapshot(),
|
|
104
|
-
};
|
|
105
|
-
await this.stateStore.saveRun(runRecord);
|
|
106
|
-
await onProgress?.(runRecord);
|
|
107
|
-
try {
|
|
108
|
-
const result = await this.sessionController.sendMessage(cwd, task, options, async (event) => {
|
|
109
|
-
// Update run record with progress events
|
|
110
|
-
const currentRun = await this.stateStore.getRun(cwd, runId);
|
|
111
|
-
if (currentRun) {
|
|
112
|
-
const updated = {
|
|
113
|
-
...currentRun,
|
|
114
|
-
updatedAt: new Date().toISOString(),
|
|
115
|
-
sessionId: event.sessionId ?? currentRun.sessionId,
|
|
116
|
-
context: this.sessionController.getContextSnapshot(),
|
|
117
|
-
};
|
|
118
|
-
await this.stateStore.saveRun(updated);
|
|
119
|
-
await onProgress?.(updated);
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
const completedRun = await this.stateStore.updateRun(cwd, runId, (run) => ({
|
|
123
|
-
...run,
|
|
124
|
-
status: 'completed',
|
|
125
|
-
updatedAt: new Date().toISOString(),
|
|
126
|
-
sessionId: result.sessionId ?? run.sessionId,
|
|
127
|
-
messages: [
|
|
128
|
-
...run.messages,
|
|
129
|
-
{
|
|
130
|
-
timestamp: new Date().toISOString(),
|
|
131
|
-
direction: 'received',
|
|
132
|
-
text: result.finalText,
|
|
133
|
-
turns: result.turns,
|
|
134
|
-
totalCostUsd: result.totalCostUsd,
|
|
135
|
-
inputTokens: result.inputTokens,
|
|
136
|
-
outputTokens: result.outputTokens,
|
|
137
|
-
},
|
|
138
|
-
],
|
|
139
|
-
context: this.sessionController.getContextSnapshot(),
|
|
140
|
-
finalSummary: result.finalText,
|
|
141
|
-
}));
|
|
142
|
-
return { run: completedRun };
|
|
143
|
-
}
|
|
144
|
-
catch (error) {
|
|
145
|
-
const failedRun = await this.stateStore.updateRun(cwd, runId, (run) => ({
|
|
146
|
-
...run,
|
|
147
|
-
status: 'failed',
|
|
148
|
-
updatedAt: new Date().toISOString(),
|
|
149
|
-
context: this.sessionController.getContextSnapshot(),
|
|
150
|
-
finalSummary: error instanceof Error ? error.message : String(error),
|
|
151
|
-
}));
|
|
152
|
-
return { run: failedRun };
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Try to restore session state from disk on startup.
|
|
157
|
-
*/
|
|
158
|
-
async tryRestore(cwd) {
|
|
159
|
-
return this.sessionController.tryRestore(cwd);
|
|
160
|
-
}
|
|
161
|
-
listRuns(cwd) {
|
|
162
|
-
return this.stateStore.listRuns(cwd);
|
|
163
|
-
}
|
|
164
|
-
getRun(cwd, runId) {
|
|
165
|
-
return this.stateStore.getRun(cwd, runId);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import type { ClaudeAgentSdkAdapter, ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
|
|
2
|
-
import type { ClaudeSessionRunResult, SessionContextSnapshot, SessionMode } from '../types/contracts.js';
|
|
3
|
-
import type { ContextTracker } from './context-tracker.js';
|
|
4
|
-
export declare class SessionController {
|
|
5
|
-
private readonly sdkAdapter;
|
|
6
|
-
private readonly contextTracker;
|
|
7
|
-
private readonly sessionPrompt;
|
|
8
|
-
private readonly modePrefixes;
|
|
9
|
-
private activeSessionId;
|
|
10
|
-
constructor(sdkAdapter: ClaudeAgentSdkAdapter, contextTracker: ContextTracker, sessionPrompt: string, modePrefixes?: {
|
|
11
|
-
plan: string;
|
|
12
|
-
free: string;
|
|
13
|
-
});
|
|
14
|
-
get isActive(): boolean;
|
|
15
|
-
get sessionId(): string | null;
|
|
16
|
-
/**
|
|
17
|
-
* Send a message to the persistent session. Creates one if none exists.
|
|
18
|
-
* Returns the session result including usage data.
|
|
19
|
-
*/
|
|
20
|
-
sendMessage(cwd: string, message: string, options?: {
|
|
21
|
-
model?: string;
|
|
22
|
-
effort?: 'low' | 'medium' | 'high' | 'max';
|
|
23
|
-
mode?: SessionMode;
|
|
24
|
-
settingSources?: Array<'user' | 'project' | 'local'>;
|
|
25
|
-
abortSignal?: AbortSignal;
|
|
26
|
-
}, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
27
|
-
/**
|
|
28
|
-
* Send /compact to the current session to compress context.
|
|
29
|
-
*/
|
|
30
|
-
compactSession(cwd: string, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
31
|
-
/**
|
|
32
|
-
* Clear the current session. The next sendMessage will create a fresh one.
|
|
33
|
-
*/
|
|
34
|
-
clearSession(cwd: string): Promise<string | null>;
|
|
35
|
-
/**
|
|
36
|
-
* Get current context tracking snapshot.
|
|
37
|
-
*/
|
|
38
|
-
getContextSnapshot(): SessionContextSnapshot;
|
|
39
|
-
/**
|
|
40
|
-
* Try to restore active session from persisted state on startup.
|
|
41
|
-
*/
|
|
42
|
-
tryRestore(cwd: string): Promise<boolean>;
|
|
43
|
-
private persistActiveSession;
|
|
44
|
-
private removeActiveSession;
|
|
45
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
-
import { dirname, join } from 'node:path';
|
|
3
|
-
const ACTIVE_SESSION_FILE = '.claude-manager/active-session.json';
|
|
4
|
-
export class SessionController {
|
|
5
|
-
sdkAdapter;
|
|
6
|
-
contextTracker;
|
|
7
|
-
sessionPrompt;
|
|
8
|
-
modePrefixes;
|
|
9
|
-
activeSessionId = null;
|
|
10
|
-
constructor(sdkAdapter, contextTracker, sessionPrompt, modePrefixes = {
|
|
11
|
-
plan: '',
|
|
12
|
-
free: '',
|
|
13
|
-
}) {
|
|
14
|
-
this.sdkAdapter = sdkAdapter;
|
|
15
|
-
this.contextTracker = contextTracker;
|
|
16
|
-
this.sessionPrompt = sessionPrompt;
|
|
17
|
-
this.modePrefixes = modePrefixes;
|
|
18
|
-
}
|
|
19
|
-
get isActive() {
|
|
20
|
-
return this.activeSessionId !== null;
|
|
21
|
-
}
|
|
22
|
-
get sessionId() {
|
|
23
|
-
return this.activeSessionId;
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Send a message to the persistent session. Creates one if none exists.
|
|
27
|
-
* Returns the session result including usage data.
|
|
28
|
-
*/
|
|
29
|
-
async sendMessage(cwd, message, options, onEvent) {
|
|
30
|
-
const mode = options?.mode ?? 'free';
|
|
31
|
-
const prefix = this.modePrefixes[mode];
|
|
32
|
-
const prompt = prefix ? `${prefix}\n\n${message}` : message;
|
|
33
|
-
const input = {
|
|
34
|
-
cwd,
|
|
35
|
-
prompt,
|
|
36
|
-
persistSession: true,
|
|
37
|
-
permissionMode: mode === 'plan' ? 'plan' : 'acceptEdits',
|
|
38
|
-
includePartialMessages: true,
|
|
39
|
-
model: options?.model,
|
|
40
|
-
effort: options?.effort,
|
|
41
|
-
settingSources: options?.settingSources ?? ['user', 'project', 'local'],
|
|
42
|
-
abortSignal: options?.abortSignal,
|
|
43
|
-
};
|
|
44
|
-
if (this.activeSessionId) {
|
|
45
|
-
// Resume existing session
|
|
46
|
-
input.resumeSessionId = this.activeSessionId;
|
|
47
|
-
}
|
|
48
|
-
else {
|
|
49
|
-
// New session — apply the expert operator system prompt and defaults
|
|
50
|
-
input.systemPrompt = this.sessionPrompt;
|
|
51
|
-
input.model ??= 'claude-opus-4-6';
|
|
52
|
-
input.effort ??= 'high';
|
|
53
|
-
}
|
|
54
|
-
const result = await this.sdkAdapter.runSession(input, onEvent);
|
|
55
|
-
// Track the session ID
|
|
56
|
-
if (result.sessionId) {
|
|
57
|
-
this.activeSessionId = result.sessionId;
|
|
58
|
-
}
|
|
59
|
-
// Update context tracking
|
|
60
|
-
this.contextTracker.recordResult({
|
|
61
|
-
sessionId: result.sessionId,
|
|
62
|
-
turns: result.turns,
|
|
63
|
-
totalCostUsd: result.totalCostUsd,
|
|
64
|
-
inputTokens: result.inputTokens,
|
|
65
|
-
outputTokens: result.outputTokens,
|
|
66
|
-
contextWindowSize: result.contextWindowSize,
|
|
67
|
-
});
|
|
68
|
-
// Persist active session state
|
|
69
|
-
await this.persistActiveSession(cwd);
|
|
70
|
-
return result;
|
|
71
|
-
}
|
|
72
|
-
/**
|
|
73
|
-
* Send /compact to the current session to compress context.
|
|
74
|
-
*/
|
|
75
|
-
async compactSession(cwd, onEvent) {
|
|
76
|
-
if (!this.activeSessionId) {
|
|
77
|
-
throw new Error('No active session to compact');
|
|
78
|
-
}
|
|
79
|
-
const result = await this.sendMessage(cwd, '/compact', undefined, onEvent);
|
|
80
|
-
this.contextTracker.recordCompaction();
|
|
81
|
-
return result;
|
|
82
|
-
}
|
|
83
|
-
/**
|
|
84
|
-
* Clear the current session. The next sendMessage will create a fresh one.
|
|
85
|
-
*/
|
|
86
|
-
async clearSession(cwd) {
|
|
87
|
-
const clearedId = this.activeSessionId;
|
|
88
|
-
this.activeSessionId = null;
|
|
89
|
-
this.contextTracker.reset();
|
|
90
|
-
await this.removeActiveSession(cwd);
|
|
91
|
-
return clearedId;
|
|
92
|
-
}
|
|
93
|
-
/**
|
|
94
|
-
* Get current context tracking snapshot.
|
|
95
|
-
*/
|
|
96
|
-
getContextSnapshot() {
|
|
97
|
-
return this.contextTracker.snapshot();
|
|
98
|
-
}
|
|
99
|
-
/**
|
|
100
|
-
* Try to restore active session from persisted state on startup.
|
|
101
|
-
*/
|
|
102
|
-
async tryRestore(cwd) {
|
|
103
|
-
const filePath = join(cwd, ACTIVE_SESSION_FILE);
|
|
104
|
-
try {
|
|
105
|
-
const raw = await readFile(filePath, 'utf-8');
|
|
106
|
-
const state = JSON.parse(raw);
|
|
107
|
-
if (state.sessionId && state.cwd === cwd) {
|
|
108
|
-
this.activeSessionId = state.sessionId;
|
|
109
|
-
this.contextTracker.restore(state);
|
|
110
|
-
return true;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
catch {
|
|
114
|
-
// File doesn't exist or is corrupt — start fresh
|
|
115
|
-
}
|
|
116
|
-
return false;
|
|
117
|
-
}
|
|
118
|
-
async persistActiveSession(cwd) {
|
|
119
|
-
if (!this.activeSessionId) {
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
122
|
-
const snap = this.contextTracker.snapshot();
|
|
123
|
-
const state = {
|
|
124
|
-
sessionId: this.activeSessionId,
|
|
125
|
-
cwd,
|
|
126
|
-
startedAt: new Date().toISOString(),
|
|
127
|
-
totalTurns: snap.totalTurns,
|
|
128
|
-
totalCostUsd: snap.totalCostUsd,
|
|
129
|
-
estimatedContextPercent: snap.estimatedContextPercent,
|
|
130
|
-
contextWindowSize: snap.contextWindowSize,
|
|
131
|
-
latestInputTokens: snap.latestInputTokens,
|
|
132
|
-
};
|
|
133
|
-
const filePath = join(cwd, ACTIVE_SESSION_FILE);
|
|
134
|
-
await mkdir(dirname(filePath), { recursive: true });
|
|
135
|
-
await writeFile(filePath, JSON.stringify(state, null, 2));
|
|
136
|
-
}
|
|
137
|
-
async removeActiveSession(cwd) {
|
|
138
|
-
const filePath = join(cwd, ACTIVE_SESSION_FILE);
|
|
139
|
-
try {
|
|
140
|
-
const { unlink } = await import('node:fs/promises');
|
|
141
|
-
await unlink(filePath);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
// File doesn't exist — that's fine
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
export class TaskPlanner {
|
|
3
|
-
plan(tasks, maxSubagents) {
|
|
4
|
-
return tasks
|
|
5
|
-
.slice(0, maxSubagents)
|
|
6
|
-
.map((task, index) => this.createPlan(tasks.length === 1 ? 'Primary task' : `Subtask ${index + 1}`, task));
|
|
7
|
-
}
|
|
8
|
-
createPlan(title, prompt) {
|
|
9
|
-
return {
|
|
10
|
-
id: randomUUID(),
|
|
11
|
-
title,
|
|
12
|
-
prompt,
|
|
13
|
-
};
|
|
14
|
-
}
|
|
15
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ClaudeMetadataSnapshot, ClaudeSettingSource } from '../types/contracts.js';
|
|
2
|
-
import type { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
|
|
3
|
-
import type { RepoClaudeConfigReader } from './repo-claude-config-reader.js';
|
|
4
|
-
export declare class ClaudeMetadataService {
|
|
5
|
-
private readonly configReader;
|
|
6
|
-
private readonly sdkAdapter;
|
|
7
|
-
constructor(configReader: RepoClaudeConfigReader, sdkAdapter: ClaudeAgentSdkAdapter);
|
|
8
|
-
collect(cwd: string, options?: {
|
|
9
|
-
includeSdkProbe?: boolean;
|
|
10
|
-
settingSources?: ClaudeSettingSource[];
|
|
11
|
-
}): Promise<ClaudeMetadataSnapshot>;
|
|
12
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export class ClaudeMetadataService {
|
|
2
|
-
configReader;
|
|
3
|
-
sdkAdapter;
|
|
4
|
-
constructor(configReader, sdkAdapter) {
|
|
5
|
-
this.configReader = configReader;
|
|
6
|
-
this.sdkAdapter = sdkAdapter;
|
|
7
|
-
}
|
|
8
|
-
async collect(cwd, options = {}) {
|
|
9
|
-
const baseSnapshot = await this.configReader.read(cwd);
|
|
10
|
-
if (!options.includeSdkProbe) {
|
|
11
|
-
return dedupeSnapshot(baseSnapshot);
|
|
12
|
-
}
|
|
13
|
-
const capabilities = await this.sdkAdapter.probeCapabilities(cwd, options.settingSources);
|
|
14
|
-
return dedupeSnapshot({
|
|
15
|
-
...baseSnapshot,
|
|
16
|
-
commands: [...baseSnapshot.commands, ...capabilities.commands],
|
|
17
|
-
agents: capabilities.agents,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function dedupeSnapshot(snapshot) {
|
|
22
|
-
return {
|
|
23
|
-
...snapshot,
|
|
24
|
-
commands: dedupeByName(snapshot.commands),
|
|
25
|
-
skills: dedupeByName(snapshot.skills),
|
|
26
|
-
hooks: dedupeByName(snapshot.hooks),
|
|
27
|
-
agents: dedupeByName(snapshot.agents),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function dedupeByName(items) {
|
|
31
|
-
const seen = new Map();
|
|
32
|
-
for (const item of items) {
|
|
33
|
-
if (!seen.has(item.name)) {
|
|
34
|
-
seen.set(item.name, item);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return [...seen.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
38
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import JSON5 from 'json5';
|
|
4
|
-
export class RepoClaudeConfigReader {
|
|
5
|
-
async read(cwd) {
|
|
6
|
-
const claudeDirectory = path.join(cwd, '.claude');
|
|
7
|
-
const skillsDirectory = path.join(claudeDirectory, 'skills');
|
|
8
|
-
const commandsDirectory = path.join(claudeDirectory, 'commands');
|
|
9
|
-
const claudeMdCandidates = [
|
|
10
|
-
path.join(cwd, 'CLAUDE.md'),
|
|
11
|
-
path.join(claudeDirectory, 'CLAUDE.md'),
|
|
12
|
-
];
|
|
13
|
-
const collectedAt = new Date().toISOString();
|
|
14
|
-
const [skills, commands, settingsResult, claudeMdPath] = await Promise.all([
|
|
15
|
-
this.readSkills(skillsDirectory),
|
|
16
|
-
this.readCommands(commandsDirectory),
|
|
17
|
-
this.readSettings(claudeDirectory),
|
|
18
|
-
findFirstExistingPath(claudeMdCandidates),
|
|
19
|
-
]);
|
|
20
|
-
return {
|
|
21
|
-
collectedAt,
|
|
22
|
-
cwd,
|
|
23
|
-
commands: [...skillsToCommands(skills), ...commands],
|
|
24
|
-
skills,
|
|
25
|
-
hooks: settingsResult.hooks,
|
|
26
|
-
agents: [],
|
|
27
|
-
claudeMdPath: claudeMdPath ?? undefined,
|
|
28
|
-
settingsPaths: settingsResult.settingsPaths,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
async readSkills(directory) {
|
|
32
|
-
if (!(await pathExists(directory))) {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
36
|
-
const skills = await Promise.all(entries
|
|
37
|
-
.filter((entry) => entry.isDirectory())
|
|
38
|
-
.map(async (entry) => {
|
|
39
|
-
const skillPath = path.join(directory, entry.name, 'SKILL.md');
|
|
40
|
-
if (!(await pathExists(skillPath))) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
const content = await fs.readFile(skillPath, 'utf8');
|
|
44
|
-
return {
|
|
45
|
-
name: entry.name,
|
|
46
|
-
description: extractMarkdownDescription(content),
|
|
47
|
-
path: skillPath,
|
|
48
|
-
source: 'skill',
|
|
49
|
-
};
|
|
50
|
-
}));
|
|
51
|
-
return skills.filter((skill) => skill !== null);
|
|
52
|
-
}
|
|
53
|
-
async readCommands(directory) {
|
|
54
|
-
if (!(await pathExists(directory))) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const commandFiles = await collectMarkdownFiles(directory);
|
|
58
|
-
const commands = await Promise.all(commandFiles.map(async (commandPath) => {
|
|
59
|
-
const content = await fs.readFile(commandPath, 'utf8');
|
|
60
|
-
return {
|
|
61
|
-
name: path.basename(commandPath, path.extname(commandPath)),
|
|
62
|
-
description: extractMarkdownDescription(content),
|
|
63
|
-
source: 'command',
|
|
64
|
-
path: commandPath,
|
|
65
|
-
};
|
|
66
|
-
}));
|
|
67
|
-
return commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
68
|
-
}
|
|
69
|
-
async readSettings(claudeDirectory) {
|
|
70
|
-
const candidatePaths = [
|
|
71
|
-
path.join(claudeDirectory, 'settings.json'),
|
|
72
|
-
path.join(claudeDirectory, 'settings.local.json'),
|
|
73
|
-
];
|
|
74
|
-
const settingsPaths = [];
|
|
75
|
-
const hooks = [];
|
|
76
|
-
for (const candidatePath of candidatePaths) {
|
|
77
|
-
if (!(await pathExists(candidatePath))) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
settingsPaths.push(candidatePath);
|
|
81
|
-
const content = await fs.readFile(candidatePath, 'utf8');
|
|
82
|
-
const parsed = JSON5.parse(content);
|
|
83
|
-
const hookEntries = Object.entries(parsed.hooks ?? {});
|
|
84
|
-
for (const [hookName, hookValue] of hookEntries) {
|
|
85
|
-
const hookMatchers = Array.isArray(hookValue) ? hookValue : [hookValue];
|
|
86
|
-
for (const hookMatcher of hookMatchers) {
|
|
87
|
-
if (!hookMatcher || typeof hookMatcher !== 'object') {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const matcher = typeof hookMatcher.matcher === 'string'
|
|
91
|
-
? hookMatcher.matcher
|
|
92
|
-
: undefined;
|
|
93
|
-
const commandCount = Array.isArray(hookMatcher.hooks)
|
|
94
|
-
? (hookMatcher.hooks?.length ?? 0)
|
|
95
|
-
: 0;
|
|
96
|
-
hooks.push({
|
|
97
|
-
name: hookName,
|
|
98
|
-
matcher,
|
|
99
|
-
sourcePath: candidatePath,
|
|
100
|
-
commandCount,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
settingsPaths,
|
|
107
|
-
hooks,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function extractMarkdownDescription(markdown) {
|
|
112
|
-
const lines = markdown
|
|
113
|
-
.split(/\r?\n/)
|
|
114
|
-
.map((line) => line.trim())
|
|
115
|
-
.filter(Boolean);
|
|
116
|
-
const descriptionLine = lines.find((line) => !line.startsWith('#') && !line.startsWith('---'));
|
|
117
|
-
return descriptionLine ?? 'No description provided.';
|
|
118
|
-
}
|
|
119
|
-
async function collectMarkdownFiles(directory) {
|
|
120
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
121
|
-
const files = await Promise.all(entries.map(async (entry) => {
|
|
122
|
-
const resolvedPath = path.join(directory, entry.name);
|
|
123
|
-
if (entry.isDirectory()) {
|
|
124
|
-
return collectMarkdownFiles(resolvedPath);
|
|
125
|
-
}
|
|
126
|
-
return entry.name.endsWith('.md') ? [resolvedPath] : [];
|
|
127
|
-
}));
|
|
128
|
-
return files.flat();
|
|
129
|
-
}
|
|
130
|
-
async function pathExists(candidatePath) {
|
|
131
|
-
try {
|
|
132
|
-
await fs.access(candidatePath);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
async function findFirstExistingPath(candidatePaths) {
|
|
140
|
-
for (const candidatePath of candidatePaths) {
|
|
141
|
-
if (await pathExists(candidatePath)) {
|
|
142
|
-
return candidatePath;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
function skillsToCommands(skills) {
|
|
148
|
-
return skills.map((skill) => ({
|
|
149
|
-
name: skill.name,
|
|
150
|
-
description: skill.description,
|
|
151
|
-
source: 'skill',
|
|
152
|
-
path: skill.path,
|
|
153
|
-
}));
|
|
154
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { ToolContext } from '@opencode-ai/plugin';
|
|
2
|
-
import type { ManagerSessionCanUseToolFactory } from '../types/contracts.js';
|
|
3
|
-
export interface ClaudeCodePermissionBridge {
|
|
4
|
-
createCanUseTool: ManagerSessionCanUseToolFactory;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Bridges Claude Agent SDK tool permission prompts to OpenCode `ToolContext.ask`.
|
|
8
|
-
* Serializes concurrent asks: parallel manager sub-sessions share one OpenCode tool context.
|
|
9
|
-
*
|
|
10
|
-
* AskUserQuestion: OpenCode `ask` does not return selected labels. After approval we return
|
|
11
|
-
* allow with answers set to each question's first option label so the session can proceed;
|
|
12
|
-
* the UI should still show full choices — users needing a different option should answer via
|
|
13
|
-
* the primary agent and re-run.
|
|
14
|
-
*/
|
|
15
|
-
export declare function createClaudeCodePermissionBridge(context: ToolContext): ClaudeCodePermissionBridge;
|