@doingdev/opencode-claude-manager-plugin 0.1.11 → 0.1.15
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.d.ts +5 -2
- package/dist/claude/claude-agent-sdk-adapter.js +28 -1
- package/dist/claude/tool-approval-manager.d.ts +27 -0
- package/dist/claude/tool-approval-manager.js +232 -0
- package/dist/index.d.ts +1 -1
- package/dist/manager/persistent-manager.d.ts +11 -3
- package/dist/manager/persistent-manager.js +17 -2
- package/dist/manager/session-controller.d.ts +2 -1
- package/dist/manager/session-controller.js +4 -3
- package/dist/plugin/claude-manager.plugin.d.ts +0 -2
- package/dist/plugin/claude-manager.plugin.js +208 -26
- package/dist/plugin/service-factory.d.ts +4 -1
- package/dist/plugin/service-factory.js +7 -2
- package/dist/state/file-run-state-store.js +1 -11
- package/dist/state/transcript-store.d.ts +15 -0
- package/dist/state/transcript-store.js +44 -0
- package/dist/types/contracts.d.ts +27 -0
- package/dist/util/fs-helpers.d.ts +2 -0
- package/dist/util/fs-helpers.js +12 -0
- package/package.json +3 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { type Options, type Query, type SDKSessionInfo, type SessionMessage, type SettingSource } from '@anthropic-ai/claude-agent-sdk';
|
|
2
2
|
import type { ClaudeCapabilitySnapshot, ClaudeSessionEvent, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, RunClaudeSessionInput } from '../types/contracts.js';
|
|
3
|
+
import type { ToolApprovalManager } from './tool-approval-manager.js';
|
|
3
4
|
export type ClaudeSessionEventHandler = (event: ClaudeSessionEvent) => void | Promise<void>;
|
|
4
|
-
|
|
5
|
+
interface ClaudeAgentSdkFacade {
|
|
5
6
|
query(params: {
|
|
6
7
|
prompt: string;
|
|
7
8
|
options?: Options;
|
|
@@ -15,10 +16,12 @@ export interface ClaudeAgentSdkFacade {
|
|
|
15
16
|
}
|
|
16
17
|
export declare class ClaudeAgentSdkAdapter {
|
|
17
18
|
private readonly sdkFacade;
|
|
18
|
-
|
|
19
|
+
private readonly approvalManager?;
|
|
20
|
+
constructor(sdkFacade?: ClaudeAgentSdkFacade, approvalManager?: ToolApprovalManager | undefined);
|
|
19
21
|
runSession(input: RunClaudeSessionInput, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
20
22
|
listSavedSessions(cwd?: string): Promise<ClaudeSessionSummary[]>;
|
|
21
23
|
getTranscript(sessionId: string, cwd?: string): Promise<ClaudeSessionTranscriptMessage[]>;
|
|
22
24
|
probeCapabilities(cwd: string, settingSources?: SettingSource[]): Promise<ClaudeCapabilitySnapshot>;
|
|
23
25
|
private buildOptions;
|
|
24
26
|
}
|
|
27
|
+
export {};
|
|
@@ -9,12 +9,27 @@ const TOOL_INPUT_PREVIEW_MAX = 2000;
|
|
|
9
9
|
const USER_MESSAGE_MAX = 4000;
|
|
10
10
|
export class ClaudeAgentSdkAdapter {
|
|
11
11
|
sdkFacade;
|
|
12
|
-
|
|
12
|
+
approvalManager;
|
|
13
|
+
constructor(sdkFacade = defaultFacade, approvalManager) {
|
|
13
14
|
this.sdkFacade = sdkFacade;
|
|
15
|
+
this.approvalManager = approvalManager;
|
|
14
16
|
}
|
|
15
17
|
async runSession(input, onEvent) {
|
|
16
18
|
const options = this.buildOptions(input);
|
|
17
19
|
const includePartials = options.includePartialMessages === true;
|
|
20
|
+
const abortController = new AbortController();
|
|
21
|
+
options.abortController = abortController;
|
|
22
|
+
const externalSignal = input.abortSignal;
|
|
23
|
+
let onAbort;
|
|
24
|
+
if (externalSignal) {
|
|
25
|
+
if (externalSignal.aborted) {
|
|
26
|
+
abortController.abort();
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
onAbort = () => abortController.abort();
|
|
30
|
+
externalSignal.addEventListener('abort', onAbort, { once: true });
|
|
31
|
+
}
|
|
32
|
+
}
|
|
18
33
|
const sessionQuery = this.sdkFacade.query({
|
|
19
34
|
prompt: input.prompt,
|
|
20
35
|
options,
|
|
@@ -58,6 +73,9 @@ export class ClaudeAgentSdkAdapter {
|
|
|
58
73
|
}
|
|
59
74
|
}
|
|
60
75
|
finally {
|
|
76
|
+
if (onAbort && externalSignal) {
|
|
77
|
+
externalSignal.removeEventListener('abort', onAbort);
|
|
78
|
+
}
|
|
61
79
|
sessionQuery.close();
|
|
62
80
|
}
|
|
63
81
|
return {
|
|
@@ -145,6 +163,15 @@ export class ClaudeAgentSdkAdapter {
|
|
|
145
163
|
if (!input.resumeSessionId) {
|
|
146
164
|
delete options.resume;
|
|
147
165
|
}
|
|
166
|
+
if (this.approvalManager) {
|
|
167
|
+
const manager = this.approvalManager;
|
|
168
|
+
options.canUseTool = async (toolName, toolInput, opts) => {
|
|
169
|
+
return manager.evaluate(toolName, toolInput, {
|
|
170
|
+
title: opts.title,
|
|
171
|
+
agentID: opts.agentID,
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
}
|
|
148
175
|
return options;
|
|
149
176
|
}
|
|
150
177
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { ToolApprovalDecision, ToolApprovalPolicy, ToolApprovalRule } from '../types/contracts.js';
|
|
2
|
+
export declare class ToolApprovalManager {
|
|
3
|
+
private policy;
|
|
4
|
+
private decisions;
|
|
5
|
+
private readonly maxDecisions;
|
|
6
|
+
constructor(policy?: Partial<ToolApprovalPolicy>, maxDecisions?: number);
|
|
7
|
+
evaluate(toolName: string, input: Record<string, unknown>, options?: {
|
|
8
|
+
title?: string;
|
|
9
|
+
agentID?: string;
|
|
10
|
+
}): {
|
|
11
|
+
behavior: 'allow';
|
|
12
|
+
} | {
|
|
13
|
+
behavior: 'deny';
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
getDecisions(limit?: number): ToolApprovalDecision[];
|
|
17
|
+
getDeniedDecisions(limit?: number): ToolApprovalDecision[];
|
|
18
|
+
clearDecisions(): void;
|
|
19
|
+
getPolicy(): ToolApprovalPolicy;
|
|
20
|
+
setPolicy(policy: ToolApprovalPolicy): void;
|
|
21
|
+
addRule(rule: ToolApprovalRule, position?: number): void;
|
|
22
|
+
removeRule(ruleId: string): boolean;
|
|
23
|
+
setDefaultAction(action: 'allow' | 'deny'): void;
|
|
24
|
+
setEnabled(enabled: boolean): void;
|
|
25
|
+
private findMatchingRule;
|
|
26
|
+
private recordDecision;
|
|
27
|
+
}
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const DEFAULT_MAX_DECISIONS = 500;
|
|
2
|
+
const INPUT_PREVIEW_MAX = 300;
|
|
3
|
+
function getDefaultRules() {
|
|
4
|
+
return [
|
|
5
|
+
// Safe read-only tools
|
|
6
|
+
{
|
|
7
|
+
id: 'allow-read',
|
|
8
|
+
toolPattern: 'Read',
|
|
9
|
+
action: 'allow',
|
|
10
|
+
description: 'Allow reading files',
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'allow-grep',
|
|
14
|
+
toolPattern: 'Grep',
|
|
15
|
+
action: 'allow',
|
|
16
|
+
description: 'Allow grep searches',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'allow-glob',
|
|
20
|
+
toolPattern: 'Glob',
|
|
21
|
+
action: 'allow',
|
|
22
|
+
description: 'Allow glob file searches',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: 'allow-ls',
|
|
26
|
+
toolPattern: 'LS',
|
|
27
|
+
action: 'allow',
|
|
28
|
+
description: 'Allow directory listing',
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'allow-list',
|
|
32
|
+
toolPattern: 'ListDirectory',
|
|
33
|
+
action: 'allow',
|
|
34
|
+
description: 'Allow listing directories',
|
|
35
|
+
},
|
|
36
|
+
// Edit tools
|
|
37
|
+
{
|
|
38
|
+
id: 'allow-edit',
|
|
39
|
+
toolPattern: 'Edit',
|
|
40
|
+
action: 'allow',
|
|
41
|
+
description: 'Allow file edits',
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'allow-multiedit',
|
|
45
|
+
toolPattern: 'MultiEdit',
|
|
46
|
+
action: 'allow',
|
|
47
|
+
description: 'Allow multi-edits',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
id: 'allow-write',
|
|
51
|
+
toolPattern: 'Write',
|
|
52
|
+
action: 'allow',
|
|
53
|
+
description: 'Allow file writes',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
id: 'allow-notebook',
|
|
57
|
+
toolPattern: 'NotebookEdit',
|
|
58
|
+
action: 'allow',
|
|
59
|
+
description: 'Allow notebook edits',
|
|
60
|
+
},
|
|
61
|
+
// Bash - deny dangerous patterns first, then allow the rest
|
|
62
|
+
{
|
|
63
|
+
id: 'deny-bash-rm-rf-root',
|
|
64
|
+
toolPattern: 'Bash',
|
|
65
|
+
inputPattern: 'rm -rf /',
|
|
66
|
+
action: 'deny',
|
|
67
|
+
denyMessage: 'Destructive rm -rf on root path is not allowed.',
|
|
68
|
+
description: 'Block rm -rf on root',
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: 'deny-bash-force-push',
|
|
72
|
+
toolPattern: 'Bash',
|
|
73
|
+
inputPattern: 'git push --force',
|
|
74
|
+
action: 'deny',
|
|
75
|
+
denyMessage: 'Force push is not allowed.',
|
|
76
|
+
description: 'Block git force push',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'deny-bash-reset-hard',
|
|
80
|
+
toolPattern: 'Bash',
|
|
81
|
+
inputPattern: 'git reset --hard',
|
|
82
|
+
action: 'deny',
|
|
83
|
+
denyMessage: 'git reset --hard is not allowed from Claude Code. Use the manager git_reset tool instead.',
|
|
84
|
+
description: 'Block git reset --hard',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'allow-bash',
|
|
88
|
+
toolPattern: 'Bash',
|
|
89
|
+
action: 'allow',
|
|
90
|
+
description: 'Allow bash commands (after dangerous patterns filtered)',
|
|
91
|
+
},
|
|
92
|
+
// Agent / misc
|
|
93
|
+
{
|
|
94
|
+
id: 'allow-agent',
|
|
95
|
+
toolPattern: 'Agent',
|
|
96
|
+
action: 'allow',
|
|
97
|
+
description: 'Allow agent delegation',
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'allow-todowrite',
|
|
101
|
+
toolPattern: 'TodoWrite',
|
|
102
|
+
action: 'allow',
|
|
103
|
+
description: 'Allow todo tracking',
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
id: 'allow-todoread',
|
|
107
|
+
toolPattern: 'TodoRead',
|
|
108
|
+
action: 'allow',
|
|
109
|
+
description: 'Allow todo reading',
|
|
110
|
+
},
|
|
111
|
+
];
|
|
112
|
+
}
|
|
113
|
+
export class ToolApprovalManager {
|
|
114
|
+
policy;
|
|
115
|
+
decisions = [];
|
|
116
|
+
maxDecisions;
|
|
117
|
+
constructor(policy, maxDecisions) {
|
|
118
|
+
this.policy = {
|
|
119
|
+
rules: policy?.rules ?? getDefaultRules(),
|
|
120
|
+
defaultAction: policy?.defaultAction ?? 'allow',
|
|
121
|
+
defaultDenyMessage: policy?.defaultDenyMessage ?? 'Tool call denied by approval policy.',
|
|
122
|
+
enabled: policy?.enabled ?? true,
|
|
123
|
+
};
|
|
124
|
+
this.maxDecisions = maxDecisions ?? DEFAULT_MAX_DECISIONS;
|
|
125
|
+
}
|
|
126
|
+
evaluate(toolName, input, options) {
|
|
127
|
+
if (!this.policy.enabled) {
|
|
128
|
+
return { behavior: 'allow' };
|
|
129
|
+
}
|
|
130
|
+
const inputJson = safeJsonStringify(input);
|
|
131
|
+
const matchedRule = this.findMatchingRule(toolName, inputJson);
|
|
132
|
+
const action = matchedRule?.action ?? this.policy.defaultAction;
|
|
133
|
+
const denyMessage = action === 'deny'
|
|
134
|
+
? (matchedRule?.denyMessage ??
|
|
135
|
+
this.policy.defaultDenyMessage ??
|
|
136
|
+
'Denied by policy.')
|
|
137
|
+
: undefined;
|
|
138
|
+
this.recordDecision({
|
|
139
|
+
timestamp: new Date().toISOString(),
|
|
140
|
+
toolName,
|
|
141
|
+
inputPreview: inputJson.slice(0, INPUT_PREVIEW_MAX),
|
|
142
|
+
title: options?.title,
|
|
143
|
+
matchedRuleId: matchedRule?.id ?? 'default',
|
|
144
|
+
action,
|
|
145
|
+
denyMessage,
|
|
146
|
+
agentId: options?.agentID,
|
|
147
|
+
});
|
|
148
|
+
if (action === 'deny') {
|
|
149
|
+
return { behavior: 'deny', message: denyMessage };
|
|
150
|
+
}
|
|
151
|
+
return { behavior: 'allow' };
|
|
152
|
+
}
|
|
153
|
+
getDecisions(limit) {
|
|
154
|
+
const all = [...this.decisions].reverse();
|
|
155
|
+
return limit ? all.slice(0, limit) : all;
|
|
156
|
+
}
|
|
157
|
+
getDeniedDecisions(limit) {
|
|
158
|
+
const denied = this.decisions.filter((d) => d.action === 'deny').reverse();
|
|
159
|
+
return limit ? denied.slice(0, limit) : denied;
|
|
160
|
+
}
|
|
161
|
+
clearDecisions() {
|
|
162
|
+
this.decisions = [];
|
|
163
|
+
}
|
|
164
|
+
getPolicy() {
|
|
165
|
+
return { ...this.policy, rules: [...this.policy.rules] };
|
|
166
|
+
}
|
|
167
|
+
setPolicy(policy) {
|
|
168
|
+
this.policy = { ...policy, rules: [...policy.rules] };
|
|
169
|
+
}
|
|
170
|
+
addRule(rule, position) {
|
|
171
|
+
if (position !== undefined &&
|
|
172
|
+
position >= 0 &&
|
|
173
|
+
position < this.policy.rules.length) {
|
|
174
|
+
this.policy.rules.splice(position, 0, rule);
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
this.policy.rules.push(rule);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
removeRule(ruleId) {
|
|
181
|
+
const index = this.policy.rules.findIndex((r) => r.id === ruleId);
|
|
182
|
+
if (index === -1) {
|
|
183
|
+
return false;
|
|
184
|
+
}
|
|
185
|
+
this.policy.rules.splice(index, 1);
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
setDefaultAction(action) {
|
|
189
|
+
this.policy.defaultAction = action;
|
|
190
|
+
}
|
|
191
|
+
setEnabled(enabled) {
|
|
192
|
+
this.policy.enabled = enabled;
|
|
193
|
+
}
|
|
194
|
+
findMatchingRule(toolName, inputJson) {
|
|
195
|
+
for (const rule of this.policy.rules) {
|
|
196
|
+
if (!matchesToolPattern(rule.toolPattern, toolName)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
if (rule.inputPattern && !inputJson.includes(rule.inputPattern)) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
return rule;
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
recordDecision(decision) {
|
|
207
|
+
this.decisions.push(decision);
|
|
208
|
+
if (this.decisions.length > this.maxDecisions) {
|
|
209
|
+
this.decisions = this.decisions.slice(-this.maxDecisions);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function matchesToolPattern(pattern, toolName) {
|
|
214
|
+
if (pattern === '*') {
|
|
215
|
+
return true;
|
|
216
|
+
}
|
|
217
|
+
if (!pattern.includes('*')) {
|
|
218
|
+
return pattern === toolName;
|
|
219
|
+
}
|
|
220
|
+
const regex = new RegExp('^' +
|
|
221
|
+
pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') +
|
|
222
|
+
'$');
|
|
223
|
+
return regex.test(toolName);
|
|
224
|
+
}
|
|
225
|
+
function safeJsonStringify(value) {
|
|
226
|
+
try {
|
|
227
|
+
return JSON.stringify(value);
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
return String(value);
|
|
231
|
+
}
|
|
232
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Plugin } from '@opencode-ai/plugin';
|
|
2
2
|
import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
|
|
3
|
-
export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, } from './types/contracts.js';
|
|
3
|
+
export type { ClaudeCapabilitySnapshot, ClaudeMetadataSnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
|
|
4
4
|
export { ClaudeManagerPlugin };
|
|
5
5
|
export declare const plugin: Plugin;
|
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import type { ClaudeSessionRunResult, PersistentRunRecord, PersistentRunResult, SessionContextSnapshot, GitDiffResult, GitOperationResult } from '../types/contracts.js';
|
|
1
|
+
import type { ClaudeSessionEvent, ClaudeSessionRunResult, PersistentRunRecord, PersistentRunResult, SessionContextSnapshot, GitDiffResult, GitOperationResult } from '../types/contracts.js';
|
|
2
2
|
import type { ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
|
|
3
3
|
import type { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
4
|
+
import type { TranscriptStore } from '../state/transcript-store.js';
|
|
4
5
|
import type { SessionController } from './session-controller.js';
|
|
5
6
|
import type { GitOperations } from './git-operations.js';
|
|
6
7
|
import type { ContextTracker } from './context-tracker.js';
|
|
7
|
-
|
|
8
|
+
type PersistentManagerProgressHandler = (run: PersistentRunRecord) => void | Promise<void>;
|
|
8
9
|
export declare class PersistentManager {
|
|
9
10
|
private readonly sessionController;
|
|
10
11
|
private readonly gitOps;
|
|
11
12
|
private readonly stateStore;
|
|
12
13
|
private readonly contextTracker;
|
|
13
|
-
|
|
14
|
+
private readonly transcriptStore;
|
|
15
|
+
constructor(sessionController: SessionController, gitOps: GitOperations, stateStore: FileRunStateStore, contextTracker: ContextTracker, transcriptStore: TranscriptStore);
|
|
14
16
|
/**
|
|
15
17
|
* Send a message to the persistent Claude Code session.
|
|
16
18
|
* Creates a new session if none exists.
|
|
17
19
|
*/
|
|
18
20
|
sendMessage(cwd: string, message: string, options?: {
|
|
19
21
|
model?: string;
|
|
22
|
+
abortSignal?: AbortSignal;
|
|
20
23
|
}, onEvent?: ClaudeSessionEventHandler): Promise<{
|
|
21
24
|
sessionId: string | undefined;
|
|
22
25
|
finalText: string;
|
|
@@ -48,6 +51,10 @@ export declare class PersistentManager {
|
|
|
48
51
|
* Compact the current session to free context.
|
|
49
52
|
*/
|
|
50
53
|
compactSession(cwd: string, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Read persisted transcript events for a session.
|
|
56
|
+
*/
|
|
57
|
+
getTranscriptEvents(cwd: string, sessionId: string): Promise<ClaudeSessionEvent[]>;
|
|
51
58
|
/**
|
|
52
59
|
* Execute a full task with run tracking.
|
|
53
60
|
* Creates a run record, sends the message, and persists the result.
|
|
@@ -62,3 +69,4 @@ export declare class PersistentManager {
|
|
|
62
69
|
listRuns(cwd: string): Promise<PersistentRunRecord[]>;
|
|
63
70
|
getRun(cwd: string, runId: string): Promise<PersistentRunRecord | null>;
|
|
64
71
|
}
|
|
72
|
+
export {};
|
|
@@ -4,11 +4,13 @@ export class PersistentManager {
|
|
|
4
4
|
gitOps;
|
|
5
5
|
stateStore;
|
|
6
6
|
contextTracker;
|
|
7
|
-
|
|
7
|
+
transcriptStore;
|
|
8
|
+
constructor(sessionController, gitOps, stateStore, contextTracker, transcriptStore) {
|
|
8
9
|
this.sessionController = sessionController;
|
|
9
10
|
this.gitOps = gitOps;
|
|
10
11
|
this.stateStore = stateStore;
|
|
11
12
|
this.contextTracker = contextTracker;
|
|
13
|
+
this.transcriptStore = transcriptStore;
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Send a message to the persistent Claude Code session.
|
|
@@ -16,6 +18,9 @@ export class PersistentManager {
|
|
|
16
18
|
*/
|
|
17
19
|
async sendMessage(cwd, message, options, onEvent) {
|
|
18
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
|
+
}
|
|
19
24
|
return {
|
|
20
25
|
sessionId: result.sessionId,
|
|
21
26
|
finalText: result.finalText,
|
|
@@ -58,7 +63,17 @@ export class PersistentManager {
|
|
|
58
63
|
* Compact the current session to free context.
|
|
59
64
|
*/
|
|
60
65
|
async compactSession(cwd, onEvent) {
|
|
61
|
-
|
|
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);
|
|
62
77
|
}
|
|
63
78
|
/**
|
|
64
79
|
* Execute a full task with run tracking.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import type { ClaudeSessionRunResult, SessionContextSnapshot } from '../types/contracts.js';
|
|
2
1
|
import type { ClaudeAgentSdkAdapter, ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
|
|
2
|
+
import type { ClaudeSessionRunResult, SessionContextSnapshot } from '../types/contracts.js';
|
|
3
3
|
import type { ContextTracker } from './context-tracker.js';
|
|
4
4
|
export declare class SessionController {
|
|
5
5
|
private readonly sdkAdapter;
|
|
@@ -16,6 +16,7 @@ export declare class SessionController {
|
|
|
16
16
|
sendMessage(cwd: string, message: string, options?: {
|
|
17
17
|
model?: string;
|
|
18
18
|
settingSources?: Array<'user' | 'project' | 'local'>;
|
|
19
|
+
abortSignal?: AbortSignal;
|
|
19
20
|
}, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
20
21
|
/**
|
|
21
22
|
* Send /compact to the current session to compress context.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { readFile, writeFile
|
|
2
|
-
import {
|
|
1
|
+
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
3
|
const ACTIVE_SESSION_FILE = '.claude-manager/active-session.json';
|
|
4
4
|
export class SessionController {
|
|
5
5
|
sdkAdapter;
|
|
@@ -29,7 +29,8 @@ export class SessionController {
|
|
|
29
29
|
permissionMode: 'acceptEdits',
|
|
30
30
|
includePartialMessages: true,
|
|
31
31
|
model: options?.model,
|
|
32
|
-
settingSources: options?.settingSources ?? ['project', 'local'],
|
|
32
|
+
settingSources: options?.settingSources ?? ['user', 'project', 'local'],
|
|
33
|
+
abortSignal: options?.abortSignal,
|
|
33
34
|
};
|
|
34
35
|
if (this.activeSessionId) {
|
|
35
36
|
// Resume existing session
|
|
@@ -11,6 +11,9 @@ const MANAGER_TOOL_IDS = [
|
|
|
11
11
|
'claude_manager_metadata',
|
|
12
12
|
'claude_manager_sessions',
|
|
13
13
|
'claude_manager_runs',
|
|
14
|
+
'claude_manager_approval_policy',
|
|
15
|
+
'claude_manager_approval_decisions',
|
|
16
|
+
'claude_manager_approval_update',
|
|
14
17
|
];
|
|
15
18
|
export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
16
19
|
const services = getOrCreatePluginServices(worktree);
|
|
@@ -30,16 +33,23 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
30
33
|
toolId === 'claude_manager_send' ||
|
|
31
34
|
toolId === 'claude_manager_git_commit' ||
|
|
32
35
|
toolId === 'claude_manager_git_reset' ||
|
|
33
|
-
toolId === 'claude_manager_clear'
|
|
36
|
+
toolId === 'claude_manager_clear' ||
|
|
37
|
+
toolId === 'claude_manager_approval_update'
|
|
34
38
|
? 'deny'
|
|
35
39
|
: 'allow';
|
|
36
40
|
}
|
|
37
41
|
config.agent['claude-manager'] ??= {
|
|
38
42
|
description: 'Primary agent that operates Claude Code through a persistent session, reviews work via git diff, and commits/resets changes.',
|
|
39
43
|
mode: 'primary',
|
|
40
|
-
color: '
|
|
44
|
+
color: '#D97757',
|
|
41
45
|
permission: {
|
|
42
46
|
'*': 'deny',
|
|
47
|
+
read: 'allow',
|
|
48
|
+
grep: 'allow',
|
|
49
|
+
glob: 'allow',
|
|
50
|
+
codesearch: 'allow',
|
|
51
|
+
webfetch: 'allow',
|
|
52
|
+
websearch: 'allow',
|
|
43
53
|
...managerPermissions,
|
|
44
54
|
},
|
|
45
55
|
prompt: managerPromptRegistry.managerSystemPrompt,
|
|
@@ -104,27 +114,113 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
104
114
|
cwd: tool.schema.string().optional(),
|
|
105
115
|
},
|
|
106
116
|
async execute(args, context) {
|
|
107
|
-
|
|
108
|
-
|
|
117
|
+
const cwd = args.cwd ?? context.worktree;
|
|
118
|
+
const hasActiveSession = services.manager.getStatus().sessionId !== null;
|
|
119
|
+
const promptPreview = args.message.length > 100
|
|
120
|
+
? args.message.slice(0, 100) + '...'
|
|
121
|
+
: args.message;
|
|
122
|
+
context.metadata({
|
|
123
|
+
title: hasActiveSession
|
|
124
|
+
? 'Claude Code: Resuming session...'
|
|
125
|
+
: 'Claude Code: Initializing...',
|
|
126
|
+
metadata: {
|
|
127
|
+
sessionId: services.manager.getStatus().sessionId,
|
|
128
|
+
prompt: promptPreview,
|
|
129
|
+
},
|
|
109
130
|
});
|
|
110
|
-
|
|
111
|
-
|
|
131
|
+
let turnsSoFar = 0;
|
|
132
|
+
let costSoFar = 0;
|
|
133
|
+
const result = await services.manager.sendMessage(cwd, args.message, { model: args.model, abortSignal: context.abort }, (event) => {
|
|
134
|
+
if (event.turns !== undefined) {
|
|
135
|
+
turnsSoFar = event.turns;
|
|
136
|
+
}
|
|
137
|
+
if (event.totalCostUsd !== undefined) {
|
|
138
|
+
costSoFar = event.totalCostUsd;
|
|
139
|
+
}
|
|
140
|
+
const costLabel = `$${costSoFar.toFixed(4)}`;
|
|
141
|
+
if (event.type === 'tool_call') {
|
|
142
|
+
let toolName = 'tool';
|
|
143
|
+
let inputPreview = '';
|
|
144
|
+
try {
|
|
145
|
+
const parsed = JSON.parse(event.text);
|
|
146
|
+
toolName = parsed.name ?? 'tool';
|
|
147
|
+
if (parsed.input) {
|
|
148
|
+
const inputStr = typeof parsed.input === 'string'
|
|
149
|
+
? parsed.input
|
|
150
|
+
: JSON.stringify(parsed.input);
|
|
151
|
+
inputPreview =
|
|
152
|
+
inputStr.length > 150
|
|
153
|
+
? inputStr.slice(0, 150) + '...'
|
|
154
|
+
: inputStr;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
// ignore parse errors
|
|
159
|
+
}
|
|
160
|
+
context.metadata({
|
|
161
|
+
title: `Claude Code: Running ${toolName}... (${turnsSoFar} turns, ${costLabel})`,
|
|
162
|
+
metadata: {
|
|
163
|
+
sessionId: event.sessionId,
|
|
164
|
+
type: event.type,
|
|
165
|
+
tool: toolName,
|
|
166
|
+
input: inputPreview,
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
else if (event.type === 'assistant') {
|
|
171
|
+
const thinkingPreview = event.text.length > 150
|
|
172
|
+
? event.text.slice(0, 150) + '...'
|
|
173
|
+
: event.text;
|
|
112
174
|
context.metadata({
|
|
113
|
-
title:
|
|
175
|
+
title: `Claude Code: Thinking... (${turnsSoFar} turns, ${costLabel})`,
|
|
114
176
|
metadata: {
|
|
177
|
+
sessionId: event.sessionId,
|
|
115
178
|
type: event.type,
|
|
116
|
-
|
|
179
|
+
thinking: thinkingPreview,
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
else if (event.type === 'init') {
|
|
184
|
+
context.metadata({
|
|
185
|
+
title: `Claude Code: Session started`,
|
|
186
|
+
metadata: {
|
|
187
|
+
sessionId: event.sessionId,
|
|
188
|
+
prompt: promptPreview,
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
else if (event.type === 'error') {
|
|
193
|
+
context.metadata({
|
|
194
|
+
title: `Claude Code: Error`,
|
|
195
|
+
metadata: {
|
|
196
|
+
sessionId: event.sessionId,
|
|
197
|
+
error: event.text.slice(0, 200),
|
|
117
198
|
},
|
|
118
199
|
});
|
|
119
200
|
}
|
|
120
201
|
});
|
|
202
|
+
const costLabel = `$${(result.totalCostUsd ?? 0).toFixed(4)}`;
|
|
203
|
+
const turns = result.turns ?? 0;
|
|
204
|
+
const contextWarning = formatContextWarning(result.context);
|
|
205
|
+
if (contextWarning) {
|
|
206
|
+
context.metadata({
|
|
207
|
+
title: `Claude Code: Context at ${result.context.estimatedContextPercent}% (${turns} turns)`,
|
|
208
|
+
metadata: { sessionId: result.sessionId, contextWarning },
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
context.metadata({
|
|
213
|
+
title: `Claude Code: Complete (${turns} turns, ${costLabel})`,
|
|
214
|
+
metadata: { sessionId: result.sessionId },
|
|
215
|
+
});
|
|
216
|
+
}
|
|
121
217
|
return JSON.stringify({
|
|
122
218
|
sessionId: result.sessionId,
|
|
123
219
|
finalText: result.finalText,
|
|
124
220
|
turns: result.turns,
|
|
125
221
|
totalCostUsd: result.totalCostUsd,
|
|
126
222
|
context: result.context,
|
|
127
|
-
contextWarning
|
|
223
|
+
contextWarning,
|
|
128
224
|
}, null, 2);
|
|
129
225
|
},
|
|
130
226
|
}),
|
|
@@ -189,6 +285,9 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
189
285
|
const status = services.manager.getStatus();
|
|
190
286
|
return JSON.stringify({
|
|
191
287
|
...status,
|
|
288
|
+
transcriptFile: status.sessionId
|
|
289
|
+
? `.claude-manager/transcripts/${status.sessionId}.json`
|
|
290
|
+
: null,
|
|
192
291
|
contextWarning: formatContextWarning(status),
|
|
193
292
|
}, null, 2);
|
|
194
293
|
},
|
|
@@ -210,18 +309,26 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
210
309
|
},
|
|
211
310
|
}),
|
|
212
311
|
claude_manager_sessions: tool({
|
|
213
|
-
description: 'List Claude sessions or inspect a saved transcript.'
|
|
312
|
+
description: 'List Claude sessions or inspect a saved transcript. ' +
|
|
313
|
+
'When sessionId is provided, returns both SDK transcript and local manager events.',
|
|
214
314
|
args: {
|
|
215
315
|
cwd: tool.schema.string().optional(),
|
|
216
316
|
sessionId: tool.schema.string().optional(),
|
|
217
317
|
},
|
|
218
318
|
async execute(args, context) {
|
|
219
319
|
annotateToolRun(context, 'Inspecting Claude session history', {});
|
|
320
|
+
const cwd = args.cwd ?? context.worktree;
|
|
220
321
|
if (args.sessionId) {
|
|
221
|
-
const
|
|
222
|
-
|
|
322
|
+
const [sdkTranscript, localEvents] = await Promise.all([
|
|
323
|
+
services.sessions.getTranscript(args.sessionId, cwd),
|
|
324
|
+
services.manager.getTranscriptEvents(cwd, args.sessionId),
|
|
325
|
+
]);
|
|
326
|
+
return JSON.stringify({
|
|
327
|
+
sdkTranscript,
|
|
328
|
+
localEvents: localEvents.length > 0 ? localEvents : undefined,
|
|
329
|
+
}, null, 2);
|
|
223
330
|
}
|
|
224
|
-
const sessions = await services.sessions.listSessions(
|
|
331
|
+
const sessions = await services.sessions.listSessions(cwd);
|
|
225
332
|
return JSON.stringify(sessions, null, 2);
|
|
226
333
|
},
|
|
227
334
|
}),
|
|
@@ -241,6 +348,94 @@ export const ClaudeManagerPlugin = async ({ worktree }) => {
|
|
|
241
348
|
return JSON.stringify(runs, null, 2);
|
|
242
349
|
},
|
|
243
350
|
}),
|
|
351
|
+
claude_manager_approval_policy: tool({
|
|
352
|
+
description: 'View the current tool approval policy: rules, default action, and enabled status.',
|
|
353
|
+
args: {},
|
|
354
|
+
async execute(_args, context) {
|
|
355
|
+
annotateToolRun(context, 'Reading approval policy', {});
|
|
356
|
+
return JSON.stringify(services.approvalManager.getPolicy(), null, 2);
|
|
357
|
+
},
|
|
358
|
+
}),
|
|
359
|
+
claude_manager_approval_decisions: tool({
|
|
360
|
+
description: 'View recent tool approval decisions. Shows what tools were allowed or denied. ' +
|
|
361
|
+
'Use deniedOnly to see only denied calls.',
|
|
362
|
+
args: {
|
|
363
|
+
limit: tool.schema.number().optional(),
|
|
364
|
+
deniedOnly: tool.schema.boolean().optional(),
|
|
365
|
+
},
|
|
366
|
+
async execute(args, context) {
|
|
367
|
+
annotateToolRun(context, 'Reading approval decisions', {});
|
|
368
|
+
const decisions = args.deniedOnly
|
|
369
|
+
? services.approvalManager.getDeniedDecisions(args.limit)
|
|
370
|
+
: services.approvalManager.getDecisions(args.limit);
|
|
371
|
+
return JSON.stringify({ total: decisions.length, decisions }, null, 2);
|
|
372
|
+
},
|
|
373
|
+
}),
|
|
374
|
+
claude_manager_approval_update: tool({
|
|
375
|
+
description: 'Update the tool approval policy. Add/remove rules, change default action, or enable/disable. ' +
|
|
376
|
+
'Rules are evaluated top-to-bottom; first match wins.',
|
|
377
|
+
args: {
|
|
378
|
+
action: tool.schema.enum([
|
|
379
|
+
'addRule',
|
|
380
|
+
'removeRule',
|
|
381
|
+
'setDefault',
|
|
382
|
+
'setEnabled',
|
|
383
|
+
'clearDecisions',
|
|
384
|
+
]),
|
|
385
|
+
ruleId: tool.schema.string().optional(),
|
|
386
|
+
toolPattern: tool.schema.string().optional(),
|
|
387
|
+
inputPattern: tool.schema.string().optional(),
|
|
388
|
+
ruleAction: tool.schema.enum(['allow', 'deny']).optional(),
|
|
389
|
+
denyMessage: tool.schema.string().optional(),
|
|
390
|
+
description: tool.schema.string().optional(),
|
|
391
|
+
position: tool.schema.number().optional(),
|
|
392
|
+
defaultAction: tool.schema.enum(['allow', 'deny']).optional(),
|
|
393
|
+
enabled: tool.schema.boolean().optional(),
|
|
394
|
+
},
|
|
395
|
+
async execute(args, context) {
|
|
396
|
+
annotateToolRun(context, `Updating approval: ${args.action}`, {});
|
|
397
|
+
if (args.action === 'addRule') {
|
|
398
|
+
if (!args.ruleId || !args.toolPattern || !args.ruleAction) {
|
|
399
|
+
return JSON.stringify({
|
|
400
|
+
error: 'addRule requires ruleId, toolPattern, and ruleAction',
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
services.approvalManager.addRule({
|
|
404
|
+
id: args.ruleId,
|
|
405
|
+
toolPattern: args.toolPattern,
|
|
406
|
+
inputPattern: args.inputPattern,
|
|
407
|
+
action: args.ruleAction,
|
|
408
|
+
denyMessage: args.denyMessage,
|
|
409
|
+
description: args.description,
|
|
410
|
+
}, args.position);
|
|
411
|
+
}
|
|
412
|
+
else if (args.action === 'removeRule') {
|
|
413
|
+
if (!args.ruleId) {
|
|
414
|
+
return JSON.stringify({ error: 'removeRule requires ruleId' });
|
|
415
|
+
}
|
|
416
|
+
const removed = services.approvalManager.removeRule(args.ruleId);
|
|
417
|
+
return JSON.stringify({ removed });
|
|
418
|
+
}
|
|
419
|
+
else if (args.action === 'setDefault') {
|
|
420
|
+
if (!args.defaultAction) {
|
|
421
|
+
return JSON.stringify({
|
|
422
|
+
error: 'setDefault requires defaultAction',
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
services.approvalManager.setDefaultAction(args.defaultAction);
|
|
426
|
+
}
|
|
427
|
+
else if (args.action === 'setEnabled') {
|
|
428
|
+
if (args.enabled === undefined) {
|
|
429
|
+
return JSON.stringify({ error: 'setEnabled requires enabled' });
|
|
430
|
+
}
|
|
431
|
+
services.approvalManager.setEnabled(args.enabled);
|
|
432
|
+
}
|
|
433
|
+
else if (args.action === 'clearDecisions') {
|
|
434
|
+
services.approvalManager.clearDecisions();
|
|
435
|
+
}
|
|
436
|
+
return JSON.stringify(services.approvalManager.getPolicy(), null, 2);
|
|
437
|
+
},
|
|
438
|
+
}),
|
|
244
439
|
},
|
|
245
440
|
};
|
|
246
441
|
};
|
|
@@ -304,16 +499,3 @@ function formatContextWarning(context) {
|
|
|
304
499
|
.replace('{turns}', String(totalTurns))
|
|
305
500
|
.replace('{cost}', totalCostUsd.toFixed(2));
|
|
306
501
|
}
|
|
307
|
-
export function formatRunToolResult(run) {
|
|
308
|
-
return JSON.stringify({
|
|
309
|
-
runId: run.id,
|
|
310
|
-
status: run.status,
|
|
311
|
-
task: run.task,
|
|
312
|
-
sessionId: run.sessionId,
|
|
313
|
-
finalSummary: run.finalSummary,
|
|
314
|
-
messageCount: run.messages.length,
|
|
315
|
-
actionCount: run.actions.length,
|
|
316
|
-
commits: run.commits,
|
|
317
|
-
context: run.context,
|
|
318
|
-
}, null, 2);
|
|
319
|
-
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
2
|
+
import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
|
|
2
3
|
import { PersistentManager } from '../manager/persistent-manager.js';
|
|
3
|
-
|
|
4
|
+
interface ClaudeManagerPluginServices {
|
|
4
5
|
manager: PersistentManager;
|
|
5
6
|
sessions: ClaudeSessionService;
|
|
7
|
+
approvalManager: ToolApprovalManager;
|
|
6
8
|
}
|
|
7
9
|
export declare function getOrCreatePluginServices(worktree: string): ClaudeManagerPluginServices;
|
|
10
|
+
export {};
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
|
|
2
2
|
import { ClaudeSessionService } from '../claude/claude-session.service.js';
|
|
3
|
+
import { ToolApprovalManager } from '../claude/tool-approval-manager.js';
|
|
3
4
|
import { ClaudeMetadataService } from '../metadata/claude-metadata.service.js';
|
|
4
5
|
import { RepoClaudeConfigReader } from '../metadata/repo-claude-config-reader.js';
|
|
5
6
|
import { FileRunStateStore } from '../state/file-run-state-store.js';
|
|
7
|
+
import { TranscriptStore } from '../state/transcript-store.js';
|
|
6
8
|
import { ContextTracker } from '../manager/context-tracker.js';
|
|
7
9
|
import { GitOperations } from '../manager/git-operations.js';
|
|
8
10
|
import { SessionController } from '../manager/session-controller.js';
|
|
@@ -14,19 +16,22 @@ export function getOrCreatePluginServices(worktree) {
|
|
|
14
16
|
if (cachedServices) {
|
|
15
17
|
return cachedServices;
|
|
16
18
|
}
|
|
17
|
-
const
|
|
19
|
+
const approvalManager = new ToolApprovalManager();
|
|
20
|
+
const sdkAdapter = new ClaudeAgentSdkAdapter(undefined, approvalManager);
|
|
18
21
|
const metadataService = new ClaudeMetadataService(new RepoClaudeConfigReader(), sdkAdapter);
|
|
19
22
|
const sessionService = new ClaudeSessionService(sdkAdapter, metadataService);
|
|
20
23
|
const contextTracker = new ContextTracker();
|
|
21
24
|
const sessionController = new SessionController(sdkAdapter, contextTracker, managerPromptRegistry.claudeCodeSessionPrompt);
|
|
22
25
|
const gitOps = new GitOperations(worktree);
|
|
23
26
|
const stateStore = new FileRunStateStore();
|
|
24
|
-
const
|
|
27
|
+
const transcriptStore = new TranscriptStore();
|
|
28
|
+
const manager = new PersistentManager(sessionController, gitOps, stateStore, contextTracker, transcriptStore);
|
|
25
29
|
// Try to restore active session state (fire and forget)
|
|
26
30
|
manager.tryRestore(worktree).catch(() => { });
|
|
27
31
|
const services = {
|
|
28
32
|
manager,
|
|
29
33
|
sessions: sessionService,
|
|
34
|
+
approvalManager,
|
|
30
35
|
};
|
|
31
36
|
serviceCache.set(worktree, services);
|
|
32
37
|
return services;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
1
|
import { promises as fs } from 'node:fs';
|
|
3
2
|
import path from 'node:path';
|
|
3
|
+
import { isFileNotFoundError, writeJsonAtomically, } from '../util/fs-helpers.js';
|
|
4
4
|
export class FileRunStateStore {
|
|
5
5
|
baseDirectoryName;
|
|
6
6
|
writeQueues = new Map();
|
|
@@ -85,13 +85,3 @@ export class FileRunStateStore {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
async function writeJsonAtomically(filePath, data) {
|
|
89
|
-
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
90
|
-
await fs.writeFile(tempPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
91
|
-
await fs.rename(tempPath, filePath);
|
|
92
|
-
}
|
|
93
|
-
function isFileNotFoundError(error) {
|
|
94
|
-
return (error instanceof Error &&
|
|
95
|
-
'code' in error &&
|
|
96
|
-
error.code === 'ENOENT');
|
|
97
|
-
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ClaudeSessionEvent } from '../types/contracts.js';
|
|
2
|
+
export declare class TranscriptStore {
|
|
3
|
+
private readonly baseDirectoryName;
|
|
4
|
+
constructor(baseDirectoryName?: string);
|
|
5
|
+
/**
|
|
6
|
+
* Append new events to the transcript file for the given session.
|
|
7
|
+
* Creates the file if it does not exist. Strips trailing partials before persisting.
|
|
8
|
+
*/
|
|
9
|
+
appendEvents(cwd: string, sessionId: string, newEvents: ClaudeSessionEvent[]): Promise<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Read all persisted transcript events for a session.
|
|
12
|
+
*/
|
|
13
|
+
readEvents(cwd: string, sessionId: string): Promise<ClaudeSessionEvent[]>;
|
|
14
|
+
private getTranscriptPath;
|
|
15
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { appendTranscriptEvents, stripTrailingPartials, } from '../util/transcript-append.js';
|
|
4
|
+
import { isFileNotFoundError, writeJsonAtomically, } from '../util/fs-helpers.js';
|
|
5
|
+
export class TranscriptStore {
|
|
6
|
+
baseDirectoryName;
|
|
7
|
+
constructor(baseDirectoryName = '.claude-manager') {
|
|
8
|
+
this.baseDirectoryName = baseDirectoryName;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Append new events to the transcript file for the given session.
|
|
12
|
+
* Creates the file if it does not exist. Strips trailing partials before persisting.
|
|
13
|
+
*/
|
|
14
|
+
async appendEvents(cwd, sessionId, newEvents) {
|
|
15
|
+
if (newEvents.length === 0) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
const filePath = this.getTranscriptPath(cwd, sessionId);
|
|
19
|
+
const existing = await this.readEvents(cwd, sessionId);
|
|
20
|
+
const merged = appendTranscriptEvents(existing, newEvents);
|
|
21
|
+
const cleaned = stripTrailingPartials(merged);
|
|
22
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
23
|
+
await writeJsonAtomically(filePath, cleaned);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Read all persisted transcript events for a session.
|
|
27
|
+
*/
|
|
28
|
+
async readEvents(cwd, sessionId) {
|
|
29
|
+
const filePath = this.getTranscriptPath(cwd, sessionId);
|
|
30
|
+
try {
|
|
31
|
+
const content = await fs.readFile(filePath, 'utf8');
|
|
32
|
+
return JSON.parse(content);
|
|
33
|
+
}
|
|
34
|
+
catch (error) {
|
|
35
|
+
if (isFileNotFoundError(error)) {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
getTranscriptPath(cwd, sessionId) {
|
|
42
|
+
return path.join(cwd, this.baseDirectoryName, 'transcripts', `${sessionId}.json`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -67,6 +67,7 @@ export interface RunClaudeSessionInput {
|
|
|
67
67
|
includePartialMessages?: boolean;
|
|
68
68
|
settingSources?: ClaudeSettingSource[];
|
|
69
69
|
maxTurns?: number;
|
|
70
|
+
abortSignal?: AbortSignal;
|
|
70
71
|
}
|
|
71
72
|
export interface ClaudeSessionRunResult {
|
|
72
73
|
sessionId?: string;
|
|
@@ -166,3 +167,29 @@ export interface PersistentRunRecord {
|
|
|
166
167
|
export interface PersistentRunResult {
|
|
167
168
|
run: PersistentRunRecord;
|
|
168
169
|
}
|
|
170
|
+
export interface ToolApprovalRule {
|
|
171
|
+
id: string;
|
|
172
|
+
description?: string;
|
|
173
|
+
/** Tool name — exact match or glob with * wildcard */
|
|
174
|
+
toolPattern: string;
|
|
175
|
+
/** Optional substring match against JSON-serialized tool input */
|
|
176
|
+
inputPattern?: string;
|
|
177
|
+
action: 'allow' | 'deny';
|
|
178
|
+
denyMessage?: string;
|
|
179
|
+
}
|
|
180
|
+
export interface ToolApprovalPolicy {
|
|
181
|
+
rules: ToolApprovalRule[];
|
|
182
|
+
defaultAction: 'allow' | 'deny';
|
|
183
|
+
defaultDenyMessage?: string;
|
|
184
|
+
enabled: boolean;
|
|
185
|
+
}
|
|
186
|
+
export interface ToolApprovalDecision {
|
|
187
|
+
timestamp: string;
|
|
188
|
+
toolName: string;
|
|
189
|
+
inputPreview: string;
|
|
190
|
+
title?: string;
|
|
191
|
+
matchedRuleId: string;
|
|
192
|
+
action: 'allow' | 'deny';
|
|
193
|
+
denyMessage?: string;
|
|
194
|
+
agentId?: string;
|
|
195
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { promises as fs } from 'node:fs';
|
|
3
|
+
export async function writeJsonAtomically(filePath, data) {
|
|
4
|
+
const tempPath = `${filePath}.${randomUUID()}.tmp`;
|
|
5
|
+
await fs.writeFile(tempPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
|
|
6
|
+
await fs.rename(tempPath, filePath);
|
|
7
|
+
}
|
|
8
|
+
export function isFileNotFoundError(error) {
|
|
9
|
+
return (error instanceof Error &&
|
|
10
|
+
'code' in error &&
|
|
11
|
+
error.code === 'ENOENT');
|
|
12
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@doingdev/opencode-claude-manager-plugin",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.15",
|
|
4
4
|
"description": "OpenCode plugin that orchestrates Claude Code sessions.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"opencode",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"@vitest/coverage-v8": "^4.1.0",
|
|
41
41
|
"eslint": "^9.22.0",
|
|
42
42
|
"globals": "^15.15.0",
|
|
43
|
+
"knip": "^6.0.2",
|
|
43
44
|
"prettier": "^3.8.1",
|
|
44
45
|
"typescript": "^5.9.3",
|
|
45
46
|
"typescript-eslint": "^8.57.1",
|
|
@@ -51,6 +52,7 @@
|
|
|
51
52
|
"lint": "eslint .",
|
|
52
53
|
"format": "prettier --write .",
|
|
53
54
|
"test": "vitest run",
|
|
55
|
+
"knip": "knip",
|
|
54
56
|
"release": "pnpm run build && pnpm version patch && pnpm publish"
|
|
55
57
|
}
|
|
56
58
|
}
|