@doingdev/opencode-claude-manager-plugin 0.1.20 → 0.1.22
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 -129
- package/dist/index.d.ts +5 -4
- package/dist/index.js +5 -4
- package/dist/plugin/orchestrator.plugin.d.ts +2 -0
- package/dist/plugin/orchestrator.plugin.js +114 -0
- package/dist/prompts/registry.d.ts +8 -2
- package/dist/prompts/registry.js +30 -159
- 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/package.json +3 -5
- 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/parallel-session-job-manager.d.ts +0 -49
- package/dist/manager/parallel-session-job-manager.js +0 -177
- package/dist/manager/persistent-manager.d.ts +0 -74
- 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 -627
- 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,178 +0,0 @@
|
|
|
1
|
-
import { createInterface } from 'node:readline/promises';
|
|
2
|
-
function defaultIsInteractiveTerminal() {
|
|
3
|
-
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
4
|
-
}
|
|
5
|
-
function isRecord(value) {
|
|
6
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
7
|
-
}
|
|
8
|
-
function formatToolRequest(toolName, input, meta) {
|
|
9
|
-
const headline = meta.title?.trim() ||
|
|
10
|
-
[meta.displayName, toolName].filter(Boolean).join(': ') ||
|
|
11
|
-
toolName;
|
|
12
|
-
const lines = [headline];
|
|
13
|
-
if (meta.description?.trim()) {
|
|
14
|
-
lines.push(meta.description.trim());
|
|
15
|
-
}
|
|
16
|
-
if (meta.decisionReason?.trim()) {
|
|
17
|
-
lines.push(`Reason: ${meta.decisionReason.trim()}`);
|
|
18
|
-
}
|
|
19
|
-
if (toolName === 'Bash' && typeof input.command === 'string') {
|
|
20
|
-
lines.push(`Command: ${input.command}`);
|
|
21
|
-
if (typeof input.description === 'string' && input.description) {
|
|
22
|
-
lines.push(`Intent: ${input.description}`);
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
else if (toolName !== 'AskUserQuestion') {
|
|
26
|
-
lines.push(`Input: ${JSON.stringify(input, null, 2)}`);
|
|
27
|
-
}
|
|
28
|
-
return lines.join('\n');
|
|
29
|
-
}
|
|
30
|
-
function applyDefaultAskUserAnswers(input) {
|
|
31
|
-
const rawQuestions = input.questions;
|
|
32
|
-
if (!Array.isArray(rawQuestions)) {
|
|
33
|
-
return input;
|
|
34
|
-
}
|
|
35
|
-
const answers = {};
|
|
36
|
-
for (const entry of rawQuestions) {
|
|
37
|
-
if (!isRecord(entry)) {
|
|
38
|
-
continue;
|
|
39
|
-
}
|
|
40
|
-
const questionText = entry.question;
|
|
41
|
-
if (typeof questionText !== 'string') {
|
|
42
|
-
continue;
|
|
43
|
-
}
|
|
44
|
-
const options = entry.options;
|
|
45
|
-
if (!Array.isArray(options) || options.length === 0) {
|
|
46
|
-
continue;
|
|
47
|
-
}
|
|
48
|
-
const first = options[0];
|
|
49
|
-
if (isRecord(first) && typeof first.label === 'string') {
|
|
50
|
-
answers[questionText] = first.label;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
...input,
|
|
55
|
-
questions: rawQuestions,
|
|
56
|
-
answers,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
async function promptLine(question) {
|
|
60
|
-
const rl = createInterface({
|
|
61
|
-
input: process.stdin,
|
|
62
|
-
output: process.stdout,
|
|
63
|
-
});
|
|
64
|
-
try {
|
|
65
|
-
return (await rl.question(question)).trim();
|
|
66
|
-
}
|
|
67
|
-
finally {
|
|
68
|
-
rl.close();
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
async function handleAskUserQuestionInteractive(input, toolUseID) {
|
|
72
|
-
const rawQuestions = input.questions;
|
|
73
|
-
if (!Array.isArray(rawQuestions)) {
|
|
74
|
-
return {
|
|
75
|
-
behavior: 'deny',
|
|
76
|
-
message: 'AskUserQuestion invoked without a questions array; cannot collect answers interactively.',
|
|
77
|
-
toolUseID,
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
const answers = {};
|
|
81
|
-
for (const entry of rawQuestions) {
|
|
82
|
-
if (!isRecord(entry)) {
|
|
83
|
-
continue;
|
|
84
|
-
}
|
|
85
|
-
const questionText = entry.question;
|
|
86
|
-
const header = entry.header;
|
|
87
|
-
if (typeof questionText !== 'string') {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const label = typeof header === 'string' && header.trim()
|
|
91
|
-
? `${header.trim()}: ${questionText}`
|
|
92
|
-
: questionText;
|
|
93
|
-
const options = entry.options;
|
|
94
|
-
if (!Array.isArray(options) || options.length === 0) {
|
|
95
|
-
return {
|
|
96
|
-
behavior: 'deny',
|
|
97
|
-
message: `Question "${questionText}" has no options to choose from.`,
|
|
98
|
-
toolUseID,
|
|
99
|
-
};
|
|
100
|
-
}
|
|
101
|
-
process.stdout.write(`\n${label}\n`);
|
|
102
|
-
for (const [index, opt] of options.entries()) {
|
|
103
|
-
if (isRecord(opt) && typeof opt.label === 'string') {
|
|
104
|
-
const desc = typeof opt.description === 'string' ? ` — ${opt.description}` : '';
|
|
105
|
-
process.stdout.write(` ${index + 1}. ${opt.label}${desc}\n`);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
const multi = entry.multiSelect === true
|
|
109
|
-
? ' (numbers separated by commas, or type your own answer)'
|
|
110
|
-
: ' (number, or type your own answer)';
|
|
111
|
-
const response = await promptLine(`Your choice${multi}: `);
|
|
112
|
-
const parsed = parseChoiceResponse(response, options);
|
|
113
|
-
answers[questionText] = parsed;
|
|
114
|
-
}
|
|
115
|
-
return {
|
|
116
|
-
behavior: 'allow',
|
|
117
|
-
updatedInput: {
|
|
118
|
-
...input,
|
|
119
|
-
questions: rawQuestions,
|
|
120
|
-
answers,
|
|
121
|
-
},
|
|
122
|
-
toolUseID,
|
|
123
|
-
};
|
|
124
|
-
}
|
|
125
|
-
function parseChoiceResponse(response, options) {
|
|
126
|
-
const indices = response
|
|
127
|
-
.split(',')
|
|
128
|
-
.map((part) => Number.parseInt(part.trim(), 10) - 1)
|
|
129
|
-
.filter((index) => Number.isFinite(index));
|
|
130
|
-
const labels = indices
|
|
131
|
-
.filter((index) => index >= 0 && index < options.length)
|
|
132
|
-
.map((index) => {
|
|
133
|
-
const opt = options[index];
|
|
134
|
-
if (isRecord(opt) && typeof opt.label === 'string') {
|
|
135
|
-
return opt.label;
|
|
136
|
-
}
|
|
137
|
-
return '';
|
|
138
|
-
})
|
|
139
|
-
.filter(Boolean);
|
|
140
|
-
return labels.length > 0 ? labels.join(', ') : response;
|
|
141
|
-
}
|
|
142
|
-
export function createDelegatedCanUseTool(policy, factoryOptions) {
|
|
143
|
-
const isInteractiveTerminal = factoryOptions?.isInteractiveTerminal ?? defaultIsInteractiveTerminal;
|
|
144
|
-
return async (toolName, input, options) => {
|
|
145
|
-
const usePrompt = policy === 'prompt_if_tty' && isInteractiveTerminal();
|
|
146
|
-
if (toolName === 'AskUserQuestion') {
|
|
147
|
-
if (usePrompt) {
|
|
148
|
-
return handleAskUserQuestionInteractive(input, options.toolUseID);
|
|
149
|
-
}
|
|
150
|
-
return {
|
|
151
|
-
behavior: 'allow',
|
|
152
|
-
updatedInput: applyDefaultAskUserAnswers(input),
|
|
153
|
-
toolUseID: options.toolUseID,
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
if (policy === 'allow_all' || !usePrompt) {
|
|
157
|
-
return {
|
|
158
|
-
behavior: 'allow',
|
|
159
|
-
updatedInput: input,
|
|
160
|
-
toolUseID: options.toolUseID,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
const summary = formatToolRequest(toolName, input, options);
|
|
164
|
-
const answer = await promptLine(`${summary}\nAllow this action? [y/N] `);
|
|
165
|
-
if (answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes') {
|
|
166
|
-
return {
|
|
167
|
-
behavior: 'allow',
|
|
168
|
-
updatedInput: input,
|
|
169
|
-
toolUseID: options.toolUseID,
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
return {
|
|
173
|
-
behavior: 'deny',
|
|
174
|
-
message: 'User denied this tool action in the delegated Claude Code session (TTY prompt).',
|
|
175
|
-
toolUseID: options.toolUseID,
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
|
-
}
|
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
import type { LiveTailEvent, ToolOutputPreview } from '../types/contracts.js';
|
|
2
|
-
/**
|
|
3
|
-
* Tails Claude Code session JSONL files for live tool output.
|
|
4
|
-
*
|
|
5
|
-
* The SDK streams high-level events (assistant text, tool_call summaries, results)
|
|
6
|
-
* but does not expose the raw tool output that Claude Code writes to the JSONL
|
|
7
|
-
* transcript on disk. This service fills that gap by polling the file for new
|
|
8
|
-
* lines, parsing each one, and forwarding parsed events to a caller-supplied
|
|
9
|
-
* callback.
|
|
10
|
-
*/
|
|
11
|
-
export declare class SessionLiveTailer {
|
|
12
|
-
private activeTails;
|
|
13
|
-
/**
|
|
14
|
-
* Locate the JSONL file for a session.
|
|
15
|
-
*
|
|
16
|
-
* Claude Code stores transcripts at:
|
|
17
|
-
* ~/.claude/projects/<sanitized-cwd>/sessions/<session-id>.jsonl
|
|
18
|
-
*
|
|
19
|
-
* The `<sanitized-cwd>` folder name is an internal implementation detail that
|
|
20
|
-
* can change across Claude Code versions, so we search all project directories
|
|
21
|
-
* for the session file rather than attempting to replicate the sanitisation.
|
|
22
|
-
*/
|
|
23
|
-
findSessionFile(sessionId: string, cwd?: string): string | null;
|
|
24
|
-
/**
|
|
25
|
-
* Check whether we can locate a JSONL file for the given session.
|
|
26
|
-
*/
|
|
27
|
-
sessionFileExists(sessionId: string, cwd?: string): boolean;
|
|
28
|
-
/**
|
|
29
|
-
* Poll a session's JSONL file for new lines and emit parsed events.
|
|
30
|
-
*
|
|
31
|
-
* Returns a stop function. The caller **must** invoke it when tailing is no
|
|
32
|
-
* longer needed (e.g. when the session completes or the tool is interrupted).
|
|
33
|
-
*/
|
|
34
|
-
startTailing(sessionId: string, cwd: string | undefined, onEvent: (event: LiveTailEvent) => void, pollIntervalMs?: number): () => void;
|
|
35
|
-
/**
|
|
36
|
-
* Stop tailing a specific session.
|
|
37
|
-
*/
|
|
38
|
-
stopTailing(sessionId: string): void;
|
|
39
|
-
/**
|
|
40
|
-
* Stop all active tails (cleanup on shutdown).
|
|
41
|
-
*/
|
|
42
|
-
stopAll(): void;
|
|
43
|
-
/**
|
|
44
|
-
* Read the last N lines from a session JSONL file.
|
|
45
|
-
*/
|
|
46
|
-
getLastLines(sessionId: string, cwd: string | undefined, lineCount?: number): Promise<string[]>;
|
|
47
|
-
/**
|
|
48
|
-
* Extract a preview of recent tool output from the tail of a session file.
|
|
49
|
-
*/
|
|
50
|
-
getToolOutputPreview(sessionId: string, cwd: string | undefined, maxEntries?: number): Promise<ToolOutputPreview[]>;
|
|
51
|
-
}
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { createReadStream, existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
-
import { createInterface } from 'node:readline';
|
|
3
|
-
import path from 'node:path';
|
|
4
|
-
import os from 'node:os';
|
|
5
|
-
/**
|
|
6
|
-
* Tails Claude Code session JSONL files for live tool output.
|
|
7
|
-
*
|
|
8
|
-
* The SDK streams high-level events (assistant text, tool_call summaries, results)
|
|
9
|
-
* but does not expose the raw tool output that Claude Code writes to the JSONL
|
|
10
|
-
* transcript on disk. This service fills that gap by polling the file for new
|
|
11
|
-
* lines, parsing each one, and forwarding parsed events to a caller-supplied
|
|
12
|
-
* callback.
|
|
13
|
-
*/
|
|
14
|
-
export class SessionLiveTailer {
|
|
15
|
-
activeTails = new Map();
|
|
16
|
-
// ── Path discovery ──────────────────────────────────────────────────
|
|
17
|
-
/**
|
|
18
|
-
* Locate the JSONL file for a session.
|
|
19
|
-
*
|
|
20
|
-
* Claude Code stores transcripts at:
|
|
21
|
-
* ~/.claude/projects/<sanitized-cwd>/sessions/<session-id>.jsonl
|
|
22
|
-
*
|
|
23
|
-
* The `<sanitized-cwd>` folder name is an internal implementation detail that
|
|
24
|
-
* can change across Claude Code versions, so we search all project directories
|
|
25
|
-
* for the session file rather than attempting to replicate the sanitisation.
|
|
26
|
-
*/
|
|
27
|
-
findSessionFile(sessionId, cwd) {
|
|
28
|
-
const projectsDir = path.join(os.homedir(), '.claude', 'projects');
|
|
29
|
-
if (!existsSync(projectsDir)) {
|
|
30
|
-
return null;
|
|
31
|
-
}
|
|
32
|
-
// If cwd is provided, try the sanitised form first (best-effort fast path).
|
|
33
|
-
if (cwd) {
|
|
34
|
-
const sanitised = cwd.replace(/\//g, '-');
|
|
35
|
-
const candidate = path.join(projectsDir, sanitised, 'sessions', `${sessionId}.jsonl`);
|
|
36
|
-
if (existsSync(candidate)) {
|
|
37
|
-
return candidate;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
// Fall back to scanning all project directories.
|
|
41
|
-
try {
|
|
42
|
-
for (const entry of readdirSync(projectsDir, { withFileTypes: true })) {
|
|
43
|
-
if (!entry.isDirectory())
|
|
44
|
-
continue;
|
|
45
|
-
const candidate = path.join(projectsDir, entry.name, 'sessions', `${sessionId}.jsonl`);
|
|
46
|
-
if (existsSync(candidate)) {
|
|
47
|
-
return candidate;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
// Permission denied or similar — nothing we can do.
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Check whether we can locate a JSONL file for the given session.
|
|
58
|
-
*/
|
|
59
|
-
sessionFileExists(sessionId, cwd) {
|
|
60
|
-
return this.findSessionFile(sessionId, cwd) !== null;
|
|
61
|
-
}
|
|
62
|
-
// ── Live tailing ────────────────────────────────────────────────────
|
|
63
|
-
/**
|
|
64
|
-
* Poll a session's JSONL file for new lines and emit parsed events.
|
|
65
|
-
*
|
|
66
|
-
* Returns a stop function. The caller **must** invoke it when tailing is no
|
|
67
|
-
* longer needed (e.g. when the session completes or the tool is interrupted).
|
|
68
|
-
*/
|
|
69
|
-
startTailing(sessionId, cwd, onEvent, pollIntervalMs = 150) {
|
|
70
|
-
const filePath = this.findSessionFile(sessionId, cwd);
|
|
71
|
-
if (!filePath) {
|
|
72
|
-
onEvent({
|
|
73
|
-
type: 'error',
|
|
74
|
-
sessionId,
|
|
75
|
-
error: `Session JSONL not found for ${sessionId}`,
|
|
76
|
-
});
|
|
77
|
-
return () => { };
|
|
78
|
-
}
|
|
79
|
-
let offset = statSync(filePath).size;
|
|
80
|
-
let buffer = '';
|
|
81
|
-
let reading = false; // guard against overlapping reads
|
|
82
|
-
const interval = setInterval(() => {
|
|
83
|
-
if (reading)
|
|
84
|
-
return;
|
|
85
|
-
try {
|
|
86
|
-
const currentSize = statSync(filePath).size;
|
|
87
|
-
if (currentSize < offset) {
|
|
88
|
-
// File was truncated (rotation / compaction) — reset.
|
|
89
|
-
offset = 0;
|
|
90
|
-
buffer = '';
|
|
91
|
-
}
|
|
92
|
-
if (currentSize <= offset)
|
|
93
|
-
return;
|
|
94
|
-
reading = true;
|
|
95
|
-
const stream = createReadStream(filePath, {
|
|
96
|
-
start: offset,
|
|
97
|
-
end: currentSize - 1,
|
|
98
|
-
encoding: 'utf8',
|
|
99
|
-
});
|
|
100
|
-
let chunk = '';
|
|
101
|
-
stream.on('data', (data) => {
|
|
102
|
-
chunk += data;
|
|
103
|
-
});
|
|
104
|
-
stream.on('end', () => {
|
|
105
|
-
reading = false;
|
|
106
|
-
offset = currentSize;
|
|
107
|
-
buffer += chunk;
|
|
108
|
-
const lines = buffer.split('\n');
|
|
109
|
-
// Keep the last (possibly incomplete) segment in the buffer.
|
|
110
|
-
buffer = lines.pop() ?? '';
|
|
111
|
-
for (const line of lines) {
|
|
112
|
-
const trimmed = line.trim();
|
|
113
|
-
if (!trimmed)
|
|
114
|
-
continue;
|
|
115
|
-
try {
|
|
116
|
-
const parsed = JSON.parse(trimmed);
|
|
117
|
-
onEvent({
|
|
118
|
-
type: 'line',
|
|
119
|
-
sessionId,
|
|
120
|
-
data: parsed,
|
|
121
|
-
rawLine: trimmed,
|
|
122
|
-
});
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
onEvent({
|
|
126
|
-
type: 'line',
|
|
127
|
-
sessionId,
|
|
128
|
-
data: null,
|
|
129
|
-
rawLine: trimmed,
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
});
|
|
134
|
-
stream.on('error', (err) => {
|
|
135
|
-
reading = false;
|
|
136
|
-
onEvent({
|
|
137
|
-
type: 'error',
|
|
138
|
-
sessionId,
|
|
139
|
-
error: err.message,
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
catch (err) {
|
|
144
|
-
reading = false;
|
|
145
|
-
onEvent({
|
|
146
|
-
type: 'error',
|
|
147
|
-
sessionId,
|
|
148
|
-
error: err instanceof Error ? err.message : String(err),
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
}, pollIntervalMs);
|
|
152
|
-
this.activeTails.set(sessionId, interval);
|
|
153
|
-
return () => this.stopTailing(sessionId);
|
|
154
|
-
}
|
|
155
|
-
/**
|
|
156
|
-
* Stop tailing a specific session.
|
|
157
|
-
*/
|
|
158
|
-
stopTailing(sessionId) {
|
|
159
|
-
const interval = this.activeTails.get(sessionId);
|
|
160
|
-
if (interval) {
|
|
161
|
-
clearInterval(interval);
|
|
162
|
-
this.activeTails.delete(sessionId);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Stop all active tails (cleanup on shutdown).
|
|
167
|
-
*/
|
|
168
|
-
stopAll() {
|
|
169
|
-
for (const [, interval] of this.activeTails) {
|
|
170
|
-
clearInterval(interval);
|
|
171
|
-
}
|
|
172
|
-
this.activeTails.clear();
|
|
173
|
-
}
|
|
174
|
-
// ── Snapshot helpers ────────────────────────────────────────────────
|
|
175
|
-
/**
|
|
176
|
-
* Read the last N lines from a session JSONL file.
|
|
177
|
-
*/
|
|
178
|
-
async getLastLines(sessionId, cwd, lineCount = 20) {
|
|
179
|
-
const filePath = this.findSessionFile(sessionId, cwd);
|
|
180
|
-
if (!filePath)
|
|
181
|
-
return [];
|
|
182
|
-
const lines = [];
|
|
183
|
-
const rl = createInterface({
|
|
184
|
-
input: createReadStream(filePath, { encoding: 'utf8' }),
|
|
185
|
-
});
|
|
186
|
-
for await (const line of rl) {
|
|
187
|
-
lines.push(line);
|
|
188
|
-
if (lines.length > lineCount) {
|
|
189
|
-
lines.shift();
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
return lines;
|
|
193
|
-
}
|
|
194
|
-
/**
|
|
195
|
-
* Extract a preview of recent tool output from the tail of a session file.
|
|
196
|
-
*/
|
|
197
|
-
async getToolOutputPreview(sessionId, cwd, maxEntries = 5) {
|
|
198
|
-
const lastLines = await this.getLastLines(sessionId, cwd, 100);
|
|
199
|
-
const previews = [];
|
|
200
|
-
for (const line of lastLines) {
|
|
201
|
-
const trimmed = line.trim();
|
|
202
|
-
if (!trimmed)
|
|
203
|
-
continue;
|
|
204
|
-
try {
|
|
205
|
-
const parsed = JSON.parse(trimmed);
|
|
206
|
-
const preview = extractToolOutput(parsed);
|
|
207
|
-
if (preview) {
|
|
208
|
-
previews.push(preview);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
catch {
|
|
212
|
-
// skip unparseable lines
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
return previews.slice(-maxEntries);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
// ── Internal helpers ────────────────────────────────────────────────────
|
|
219
|
-
/**
|
|
220
|
-
* Attempt to extract tool output information from a JSONL record.
|
|
221
|
-
*
|
|
222
|
-
* Claude Code JSONL records with tool results can appear as:
|
|
223
|
-
* 1. A top-level `{ type: "user", message: { content: [{ type: "tool_result", ... }] } }`
|
|
224
|
-
* 2. Direct `tool_result` entries (less common).
|
|
225
|
-
*/
|
|
226
|
-
function extractToolOutput(record) {
|
|
227
|
-
// Pattern 1: user message wrapping tool_result content blocks
|
|
228
|
-
if (record.type === 'user') {
|
|
229
|
-
const message = record.message;
|
|
230
|
-
if (!message)
|
|
231
|
-
return null;
|
|
232
|
-
const content = message.content;
|
|
233
|
-
if (!Array.isArray(content))
|
|
234
|
-
return null;
|
|
235
|
-
for (const block of content) {
|
|
236
|
-
if (block &&
|
|
237
|
-
typeof block === 'object' &&
|
|
238
|
-
block.type === 'tool_result') {
|
|
239
|
-
const b = block;
|
|
240
|
-
return {
|
|
241
|
-
toolUseId: typeof b.tool_use_id === 'string' ? b.tool_use_id : '',
|
|
242
|
-
content: stringifyContent(b.content),
|
|
243
|
-
isError: b.is_error === true,
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
// Pattern 2: direct tool_result record (varies by Claude Code version)
|
|
249
|
-
if (record.type === 'tool_result') {
|
|
250
|
-
return {
|
|
251
|
-
toolUseId: typeof record.tool_use_id === 'string' ? record.tool_use_id : '',
|
|
252
|
-
content: stringifyContent(record.content),
|
|
253
|
-
isError: record.is_error === true,
|
|
254
|
-
};
|
|
255
|
-
}
|
|
256
|
-
return null;
|
|
257
|
-
}
|
|
258
|
-
function stringifyContent(value) {
|
|
259
|
-
if (typeof value === 'string')
|
|
260
|
-
return value;
|
|
261
|
-
if (value === undefined || value === null)
|
|
262
|
-
return '';
|
|
263
|
-
try {
|
|
264
|
-
return JSON.stringify(value);
|
|
265
|
-
}
|
|
266
|
-
catch {
|
|
267
|
-
return String(value);
|
|
268
|
-
}
|
|
269
|
-
}
|
|
@@ -1,27 +0,0 @@
|
|
|
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
|
-
}
|