@doingdev/opencode-claude-manager-plugin 0.1.35 → 0.1.43
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/claude/claude-agent-sdk-adapter.js +1 -0
- package/dist/manager/git-operations.d.ts +10 -1
- package/dist/manager/git-operations.js +18 -3
- package/dist/manager/persistent-manager.d.ts +19 -3
- package/dist/manager/persistent-manager.js +21 -9
- package/dist/manager/session-controller.d.ts +8 -5
- package/dist/manager/session-controller.js +25 -20
- package/dist/metadata/claude-metadata.service.d.ts +12 -0
- package/dist/metadata/claude-metadata.service.js +38 -0
- package/dist/metadata/repo-claude-config-reader.d.ts +7 -0
- package/dist/metadata/repo-claude-config-reader.js +154 -0
- package/dist/plugin/agent-hierarchy.d.ts +9 -9
- package/dist/plugin/agent-hierarchy.js +25 -25
- package/dist/plugin/claude-manager.plugin.js +83 -46
- package/dist/plugin/orchestrator.plugin.d.ts +2 -0
- package/dist/plugin/orchestrator.plugin.js +116 -0
- package/dist/plugin/service-factory.js +3 -8
- package/dist/prompts/registry.js +100 -103
- package/dist/providers/claude-code-wrapper.d.ts +13 -0
- package/dist/providers/claude-code-wrapper.js +13 -0
- package/dist/safety/bash-safety.d.ts +21 -0
- package/dist/safety/bash-safety.js +62 -0
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +27 -0
- package/dist/src/claude/claude-agent-sdk-adapter.js +517 -0
- package/dist/src/claude/claude-session.service.d.ts +10 -0
- package/dist/src/claude/claude-session.service.js +18 -0
- package/dist/src/claude/session-live-tailer.d.ts +51 -0
- package/dist/src/claude/session-live-tailer.js +269 -0
- package/dist/src/claude/tool-approval-manager.d.ts +27 -0
- package/dist/src/claude/tool-approval-manager.js +232 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +4 -0
- package/dist/src/manager/context-tracker.d.ts +33 -0
- package/dist/src/manager/context-tracker.js +106 -0
- package/dist/src/manager/git-operations.d.ts +12 -0
- package/dist/src/manager/git-operations.js +76 -0
- package/dist/src/manager/persistent-manager.d.ts +77 -0
- package/dist/src/manager/persistent-manager.js +170 -0
- package/dist/src/manager/session-controller.d.ts +44 -0
- package/dist/src/manager/session-controller.js +147 -0
- package/dist/src/plugin/agent-hierarchy.d.ts +60 -0
- package/dist/src/plugin/agent-hierarchy.js +157 -0
- package/dist/src/plugin/claude-manager.plugin.d.ts +2 -0
- package/dist/src/plugin/claude-manager.plugin.js +563 -0
- package/dist/src/plugin/service-factory.d.ts +12 -0
- package/dist/src/plugin/service-factory.js +38 -0
- package/dist/src/prompts/registry.d.ts +11 -0
- package/dist/src/prompts/registry.js +260 -0
- package/dist/src/state/file-run-state-store.d.ts +14 -0
- package/dist/src/state/file-run-state-store.js +85 -0
- package/dist/src/state/transcript-store.d.ts +15 -0
- package/dist/src/state/transcript-store.js +44 -0
- package/dist/src/types/contracts.d.ts +200 -0
- package/dist/src/types/contracts.js +1 -0
- package/dist/src/util/fs-helpers.d.ts +2 -0
- package/dist/src/util/fs-helpers.js +10 -0
- package/dist/src/util/project-context.d.ts +10 -0
- package/dist/src/util/project-context.js +105 -0
- package/dist/src/util/transcript-append.d.ts +7 -0
- package/dist/src/util/transcript-append.js +29 -0
- package/dist/test/claude-agent-sdk-adapter.test.d.ts +1 -0
- package/dist/test/claude-agent-sdk-adapter.test.js +459 -0
- package/dist/test/claude-manager.plugin.test.d.ts +1 -0
- package/dist/test/claude-manager.plugin.test.js +331 -0
- package/dist/test/context-tracker.test.d.ts +1 -0
- package/dist/test/context-tracker.test.js +138 -0
- package/dist/test/file-run-state-store.test.d.ts +1 -0
- package/dist/test/file-run-state-store.test.js +82 -0
- package/dist/test/git-operations.test.d.ts +1 -0
- package/dist/test/git-operations.test.js +90 -0
- package/dist/test/persistent-manager.test.d.ts +1 -0
- package/dist/test/persistent-manager.test.js +208 -0
- package/dist/test/project-context.test.d.ts +1 -0
- package/dist/test/project-context.test.js +92 -0
- package/dist/test/prompt-registry.test.d.ts +1 -0
- package/dist/test/prompt-registry.test.js +256 -0
- package/dist/test/session-controller.test.d.ts +1 -0
- package/dist/test/session-controller.test.js +149 -0
- package/dist/test/session-live-tailer.test.d.ts +1 -0
- package/dist/test/session-live-tailer.test.js +313 -0
- package/dist/test/tool-approval-manager.test.d.ts +1 -0
- package/dist/test/tool-approval-manager.test.js +264 -0
- package/dist/test/transcript-append.test.d.ts +1 -0
- package/dist/test/transcript-append.test.js +37 -0
- package/dist/test/transcript-store.test.d.ts +1 -0
- package/dist/test/transcript-store.test.js +50 -0
- package/dist/types/contracts.d.ts +3 -4
- package/dist/vitest.config.d.ts +2 -0
- package/dist/vitest.config.js +11 -0
- package/package.json +2 -2
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
export class GitOperations {
|
|
5
|
+
cwd;
|
|
6
|
+
constructor(cwd) {
|
|
7
|
+
this.cwd = cwd;
|
|
8
|
+
}
|
|
9
|
+
async diff() {
|
|
10
|
+
const [diffText, statOutput] = await Promise.all([
|
|
11
|
+
this.git(['diff', 'HEAD']),
|
|
12
|
+
this.git(['diff', 'HEAD', '--stat']),
|
|
13
|
+
]);
|
|
14
|
+
const stats = parseStatLine(statOutput);
|
|
15
|
+
return {
|
|
16
|
+
hasDiff: diffText.trim().length > 0,
|
|
17
|
+
diffText,
|
|
18
|
+
stats,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
async diffStat() {
|
|
22
|
+
return this.git(['diff', 'HEAD', '--stat']);
|
|
23
|
+
}
|
|
24
|
+
async commit(message) {
|
|
25
|
+
try {
|
|
26
|
+
await this.git(['add', '-A']);
|
|
27
|
+
const output = await this.git(['commit', '-m', message]);
|
|
28
|
+
return { success: true, output };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
success: false,
|
|
33
|
+
output: '',
|
|
34
|
+
error: error instanceof Error ? error.message : String(error),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
async resetHard() {
|
|
39
|
+
try {
|
|
40
|
+
const output = await this.git(['reset', '--hard', 'HEAD']);
|
|
41
|
+
// Also clean untracked files
|
|
42
|
+
const cleanOutput = await this.git(['clean', '-fd']);
|
|
43
|
+
return { success: true, output: `${output}\n${cleanOutput}`.trim() };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return {
|
|
47
|
+
success: false,
|
|
48
|
+
output: '',
|
|
49
|
+
error: error instanceof Error ? error.message : String(error),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async currentBranch() {
|
|
54
|
+
const branch = await this.git(['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
55
|
+
return branch.trim();
|
|
56
|
+
}
|
|
57
|
+
async recentCommits(count = 5) {
|
|
58
|
+
return this.git(['log', `--oneline`, `-${count}`]);
|
|
59
|
+
}
|
|
60
|
+
async git(args) {
|
|
61
|
+
const { stdout } = await execFileAsync('git', args, { cwd: this.cwd });
|
|
62
|
+
return stdout;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function parseStatLine(statOutput) {
|
|
66
|
+
// Parse the summary line like: "3 files changed, 10 insertions(+), 2 deletions(-)"
|
|
67
|
+
const summaryLine = statOutput.trim().split('\n').pop() ?? '';
|
|
68
|
+
const filesMatch = summaryLine.match(/(\d+)\s+files?\s+changed/);
|
|
69
|
+
const insertionsMatch = summaryLine.match(/(\d+)\s+insertions?\(\+\)/);
|
|
70
|
+
const deletionsMatch = summaryLine.match(/(\d+)\s+deletions?\(-\)/);
|
|
71
|
+
return {
|
|
72
|
+
filesChanged: filesMatch ? parseInt(filesMatch[1], 10) : 0,
|
|
73
|
+
insertions: insertionsMatch ? parseInt(insertionsMatch[1], 10) : 0,
|
|
74
|
+
deletions: deletionsMatch ? parseInt(deletionsMatch[1], 10) : 0,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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
|
+
effort?: 'low' | 'medium' | 'high' | 'max';
|
|
23
|
+
mode?: 'plan' | 'free';
|
|
24
|
+
abortSignal?: AbortSignal;
|
|
25
|
+
}, onEvent?: ClaudeSessionEventHandler): Promise<{
|
|
26
|
+
sessionId: string | undefined;
|
|
27
|
+
finalText: string;
|
|
28
|
+
turns?: number;
|
|
29
|
+
totalCostUsd?: number;
|
|
30
|
+
inputTokens?: number;
|
|
31
|
+
outputTokens?: number;
|
|
32
|
+
contextWindowSize?: number;
|
|
33
|
+
context: SessionContextSnapshot;
|
|
34
|
+
}>;
|
|
35
|
+
/**
|
|
36
|
+
* Get the current git diff.
|
|
37
|
+
*/
|
|
38
|
+
gitDiff(): Promise<GitDiffResult>;
|
|
39
|
+
/**
|
|
40
|
+
* Commit all current changes.
|
|
41
|
+
*/
|
|
42
|
+
gitCommit(message: string): Promise<GitOperationResult>;
|
|
43
|
+
/**
|
|
44
|
+
* Hard reset to discard all uncommitted changes.
|
|
45
|
+
*/
|
|
46
|
+
gitReset(): Promise<GitOperationResult>;
|
|
47
|
+
/**
|
|
48
|
+
* Get current session status and context health.
|
|
49
|
+
*/
|
|
50
|
+
getStatus(): SessionContextSnapshot;
|
|
51
|
+
/**
|
|
52
|
+
* Clear the active session. Next send creates a fresh one.
|
|
53
|
+
*/
|
|
54
|
+
clearSession(cwd: string): Promise<string | null>;
|
|
55
|
+
/**
|
|
56
|
+
* Compact the current session to free context.
|
|
57
|
+
*/
|
|
58
|
+
compactSession(cwd: string, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
59
|
+
/**
|
|
60
|
+
* Read persisted transcript events for a session.
|
|
61
|
+
*/
|
|
62
|
+
getTranscriptEvents(cwd: string, sessionId: string): Promise<ClaudeSessionEvent[]>;
|
|
63
|
+
/**
|
|
64
|
+
* Execute a full task with run tracking.
|
|
65
|
+
* Creates a run record, sends the message, and persists the result.
|
|
66
|
+
*/
|
|
67
|
+
executeTask(cwd: string, task: string, options?: {
|
|
68
|
+
model?: string;
|
|
69
|
+
}, onProgress?: PersistentManagerProgressHandler): Promise<PersistentRunResult>;
|
|
70
|
+
/**
|
|
71
|
+
* Try to restore session state from disk on startup.
|
|
72
|
+
*/
|
|
73
|
+
tryRestore(cwd: string): Promise<boolean>;
|
|
74
|
+
listRuns(cwd: string): Promise<PersistentRunRecord[]>;
|
|
75
|
+
getRun(cwd: string, runId: string): Promise<PersistentRunRecord | null>;
|
|
76
|
+
}
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,170 @@
|
|
|
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
|
+
inputTokens: result.inputTokens,
|
|
30
|
+
outputTokens: result.outputTokens,
|
|
31
|
+
contextWindowSize: result.contextWindowSize,
|
|
32
|
+
context: this.sessionController.getContextSnapshot(),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Get the current git diff.
|
|
37
|
+
*/
|
|
38
|
+
async gitDiff() {
|
|
39
|
+
return this.gitOps.diff();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Commit all current changes.
|
|
43
|
+
*/
|
|
44
|
+
async gitCommit(message) {
|
|
45
|
+
return this.gitOps.commit(message);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Hard reset to discard all uncommitted changes.
|
|
49
|
+
*/
|
|
50
|
+
async gitReset() {
|
|
51
|
+
return this.gitOps.resetHard();
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get current session status and context health.
|
|
55
|
+
*/
|
|
56
|
+
getStatus() {
|
|
57
|
+
return this.sessionController.getContextSnapshot();
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Clear the active session. Next send creates a fresh one.
|
|
61
|
+
*/
|
|
62
|
+
async clearSession(cwd) {
|
|
63
|
+
return this.sessionController.clearSession(cwd);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Compact the current session to free context.
|
|
67
|
+
*/
|
|
68
|
+
async compactSession(cwd, onEvent) {
|
|
69
|
+
const result = await this.sessionController.compactSession(cwd, onEvent);
|
|
70
|
+
if (result.sessionId && result.events.length > 0) {
|
|
71
|
+
await this.transcriptStore.appendEvents(cwd, result.sessionId, result.events);
|
|
72
|
+
}
|
|
73
|
+
return result;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Read persisted transcript events for a session.
|
|
77
|
+
*/
|
|
78
|
+
getTranscriptEvents(cwd, sessionId) {
|
|
79
|
+
return this.transcriptStore.readEvents(cwd, sessionId);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Execute a full task with run tracking.
|
|
83
|
+
* Creates a run record, sends the message, and persists the result.
|
|
84
|
+
*/
|
|
85
|
+
async executeTask(cwd, task, options, onProgress) {
|
|
86
|
+
const runId = randomUUID();
|
|
87
|
+
const createdAt = new Date().toISOString();
|
|
88
|
+
const runRecord = {
|
|
89
|
+
id: runId,
|
|
90
|
+
cwd,
|
|
91
|
+
task,
|
|
92
|
+
status: 'running',
|
|
93
|
+
createdAt,
|
|
94
|
+
updatedAt: createdAt,
|
|
95
|
+
sessionId: this.sessionController.sessionId,
|
|
96
|
+
sessionHistory: [],
|
|
97
|
+
messages: [
|
|
98
|
+
{
|
|
99
|
+
timestamp: createdAt,
|
|
100
|
+
direction: 'sent',
|
|
101
|
+
text: task,
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
actions: [],
|
|
105
|
+
commits: [],
|
|
106
|
+
context: this.sessionController.getContextSnapshot(),
|
|
107
|
+
};
|
|
108
|
+
await this.stateStore.saveRun(runRecord);
|
|
109
|
+
await onProgress?.(runRecord);
|
|
110
|
+
try {
|
|
111
|
+
const result = await this.sessionController.sendMessage(cwd, task, options, async (event) => {
|
|
112
|
+
// Update run record with progress events
|
|
113
|
+
const currentRun = await this.stateStore.getRun(cwd, runId);
|
|
114
|
+
if (currentRun) {
|
|
115
|
+
const updated = {
|
|
116
|
+
...currentRun,
|
|
117
|
+
updatedAt: new Date().toISOString(),
|
|
118
|
+
sessionId: event.sessionId ?? currentRun.sessionId,
|
|
119
|
+
context: this.sessionController.getContextSnapshot(),
|
|
120
|
+
};
|
|
121
|
+
await this.stateStore.saveRun(updated);
|
|
122
|
+
await onProgress?.(updated);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const completedRun = await this.stateStore.updateRun(cwd, runId, (run) => ({
|
|
126
|
+
...run,
|
|
127
|
+
status: 'completed',
|
|
128
|
+
updatedAt: new Date().toISOString(),
|
|
129
|
+
sessionId: result.sessionId ?? run.sessionId,
|
|
130
|
+
messages: [
|
|
131
|
+
...run.messages,
|
|
132
|
+
{
|
|
133
|
+
timestamp: new Date().toISOString(),
|
|
134
|
+
direction: 'received',
|
|
135
|
+
text: result.finalText,
|
|
136
|
+
turns: result.turns,
|
|
137
|
+
totalCostUsd: result.totalCostUsd,
|
|
138
|
+
inputTokens: result.inputTokens,
|
|
139
|
+
outputTokens: result.outputTokens,
|
|
140
|
+
},
|
|
141
|
+
],
|
|
142
|
+
context: this.sessionController.getContextSnapshot(),
|
|
143
|
+
finalSummary: result.finalText,
|
|
144
|
+
}));
|
|
145
|
+
return { run: completedRun };
|
|
146
|
+
}
|
|
147
|
+
catch (error) {
|
|
148
|
+
const failedRun = await this.stateStore.updateRun(cwd, runId, (run) => ({
|
|
149
|
+
...run,
|
|
150
|
+
status: 'failed',
|
|
151
|
+
updatedAt: new Date().toISOString(),
|
|
152
|
+
context: this.sessionController.getContextSnapshot(),
|
|
153
|
+
finalSummary: error instanceof Error ? error.message : String(error),
|
|
154
|
+
}));
|
|
155
|
+
return { run: failedRun };
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Try to restore session state from disk on startup.
|
|
160
|
+
*/
|
|
161
|
+
async tryRestore(cwd) {
|
|
162
|
+
return this.sessionController.tryRestore(cwd);
|
|
163
|
+
}
|
|
164
|
+
listRuns(cwd) {
|
|
165
|
+
return this.stateStore.listRuns(cwd);
|
|
166
|
+
}
|
|
167
|
+
getRun(cwd, runId) {
|
|
168
|
+
return this.stateStore.getRun(cwd, runId);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
abortSignal?: AbortSignal;
|
|
25
|
+
}, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
26
|
+
/**
|
|
27
|
+
* Send /compact to the current session to compress context.
|
|
28
|
+
*/
|
|
29
|
+
compactSession(cwd: string, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
30
|
+
/**
|
|
31
|
+
* Clear the current session. The next sendMessage will create a fresh one.
|
|
32
|
+
*/
|
|
33
|
+
clearSession(cwd: string): Promise<string | null>;
|
|
34
|
+
/**
|
|
35
|
+
* Get current context tracking snapshot.
|
|
36
|
+
*/
|
|
37
|
+
getContextSnapshot(): SessionContextSnapshot;
|
|
38
|
+
/**
|
|
39
|
+
* Try to restore active session from persisted state on startup.
|
|
40
|
+
*/
|
|
41
|
+
tryRestore(cwd: string): Promise<boolean>;
|
|
42
|
+
private persistActiveSession;
|
|
43
|
+
private removeActiveSession;
|
|
44
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
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: ['user'],
|
|
42
|
+
abortSignal: options?.abortSignal,
|
|
43
|
+
};
|
|
44
|
+
if (this.activeSessionId) {
|
|
45
|
+
// Resume existing session
|
|
46
|
+
input.resumeSessionId = this.activeSessionId;
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
// New session — use the base session prompt (project context is in the wrapper)
|
|
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
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent hierarchy configuration for the CTO + Engineer Wrapper architecture.
|
|
3
|
+
*
|
|
4
|
+
* CTO (cto) — pure orchestrator, spawns engineers, reviews diffs, commits
|
|
5
|
+
* Engineer Plan (engineer_plan) — manages a Claude Code session for read-only investigation
|
|
6
|
+
* Engineer Build (engineer_build) — manages a Claude Code session for implementation
|
|
7
|
+
* Claude Code session — the underlying AI session (prompt only, no OpenCode agent)
|
|
8
|
+
*/
|
|
9
|
+
import type { ManagerPromptRegistry } from '../types/contracts.js';
|
|
10
|
+
export declare const AGENT_CTO = "cto";
|
|
11
|
+
export declare const AGENT_ENGINEER_PLAN = "engineer_plan";
|
|
12
|
+
export declare const AGENT_ENGINEER_BUILD = "engineer_build";
|
|
13
|
+
/** All restricted tool IDs (union of all domain groups) */
|
|
14
|
+
export declare const ALL_RESTRICTED_TOOL_IDS: readonly ["explore", "implement", "compact_context", "clear_session", "session_health", "list_transcripts", "list_history", "git_diff", "git_commit", "git_reset", "approval_policy", "approval_decisions", "approval_update"];
|
|
15
|
+
type ToolPermission = 'allow' | 'ask' | 'deny';
|
|
16
|
+
type AgentPermission = {
|
|
17
|
+
'*'?: ToolPermission;
|
|
18
|
+
read?: ToolPermission;
|
|
19
|
+
grep?: ToolPermission;
|
|
20
|
+
glob?: ToolPermission;
|
|
21
|
+
list?: ToolPermission;
|
|
22
|
+
codesearch?: ToolPermission;
|
|
23
|
+
webfetch?: ToolPermission;
|
|
24
|
+
websearch?: ToolPermission;
|
|
25
|
+
lsp?: ToolPermission;
|
|
26
|
+
/** OpenCode built-in: manage session todo list */
|
|
27
|
+
todowrite?: ToolPermission;
|
|
28
|
+
/** OpenCode built-in: read session todo list */
|
|
29
|
+
todoread?: ToolPermission;
|
|
30
|
+
/** OpenCode built-in: ask the user structured questions with options */
|
|
31
|
+
question?: ToolPermission;
|
|
32
|
+
/** OpenCode built-in: launch subagents (matches subagent type, last-match-wins) */
|
|
33
|
+
task?: ToolPermission | Record<string, ToolPermission>;
|
|
34
|
+
bash?: ToolPermission | Record<string, ToolPermission>;
|
|
35
|
+
[tool: string]: ToolPermission | Record<string, ToolPermission> | undefined;
|
|
36
|
+
};
|
|
37
|
+
export declare function buildCtoAgentConfig(prompts: ManagerPromptRegistry): {
|
|
38
|
+
description: string;
|
|
39
|
+
mode: "primary";
|
|
40
|
+
color: string;
|
|
41
|
+
permission: AgentPermission;
|
|
42
|
+
prompt: string;
|
|
43
|
+
};
|
|
44
|
+
export declare function buildEngineerPlanAgentConfig(prompts: ManagerPromptRegistry): {
|
|
45
|
+
description: string;
|
|
46
|
+
mode: "subagent";
|
|
47
|
+
color: string;
|
|
48
|
+
permission: AgentPermission;
|
|
49
|
+
prompt: string;
|
|
50
|
+
};
|
|
51
|
+
export declare function buildEngineerBuildAgentConfig(prompts: ManagerPromptRegistry): {
|
|
52
|
+
description: string;
|
|
53
|
+
mode: "subagent";
|
|
54
|
+
color: string;
|
|
55
|
+
permission: AgentPermission;
|
|
56
|
+
prompt: string;
|
|
57
|
+
};
|
|
58
|
+
/** Deny all restricted tools at the global level so only designated agents can use them. */
|
|
59
|
+
export declare function denyRestrictedToolsGlobally(permissions: Record<string, ToolPermission>): void;
|
|
60
|
+
export {};
|