@doingdev/opencode-claude-manager-plugin 0.1.65 → 0.1.66
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/index.d.ts +1 -1
- package/dist/manager/team-orchestrator.js +1 -1
- package/dist/plugin/agents/common.d.ts +2 -2
- package/dist/plugin/agents/common.js +5 -0
- package/dist/plugin/claude-manager.plugin.js +104 -0
- package/dist/plugin/inbox-ops.d.ts +50 -0
- package/dist/plugin/inbox-ops.js +166 -0
- package/dist/types/contracts.d.ts +18 -0
- package/package.json +1 -1
- package/dist/claude/session-live-tailer.d.ts +0 -51
- package/dist/claude/session-live-tailer.js +0 -269
- package/dist/manager/session-controller.d.ts +0 -41
- package/dist/manager/session-controller.js +0 -97
- 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/orchestrator.plugin.d.ts +0 -2
- package/dist/plugin/orchestrator.plugin.js +0 -116
- package/dist/providers/claude-code-wrapper.d.ts +0 -13
- package/dist/providers/claude-code-wrapper.js +0 -13
- package/dist/safety/bash-safety.d.ts +0 -21
- package/dist/safety/bash-safety.js +0 -62
- package/dist/src/claude/claude-agent-sdk-adapter.d.ts +0 -28
- package/dist/src/claude/claude-agent-sdk-adapter.js +0 -559
- package/dist/src/claude/claude-session.service.d.ts +0 -9
- package/dist/src/claude/claude-session.service.js +0 -15
- package/dist/src/claude/session-live-tailer.d.ts +0 -51
- package/dist/src/claude/session-live-tailer.js +0 -269
- package/dist/src/claude/tool-approval-manager.d.ts +0 -30
- package/dist/src/claude/tool-approval-manager.js +0 -279
- package/dist/src/index.d.ts +0 -5
- package/dist/src/index.js +0 -3
- package/dist/src/manager/context-tracker.d.ts +0 -32
- package/dist/src/manager/context-tracker.js +0 -103
- package/dist/src/manager/git-operations.d.ts +0 -18
- package/dist/src/manager/git-operations.js +0 -86
- package/dist/src/manager/persistent-manager.d.ts +0 -39
- package/dist/src/manager/persistent-manager.js +0 -44
- package/dist/src/manager/session-controller.d.ts +0 -41
- package/dist/src/manager/session-controller.js +0 -97
- package/dist/src/manager/team-orchestrator.d.ts +0 -81
- package/dist/src/manager/team-orchestrator.js +0 -612
- package/dist/src/plugin/agent-hierarchy.d.ts +0 -1
- package/dist/src/plugin/agent-hierarchy.js +0 -2
- package/dist/src/plugin/agents/browser-qa.d.ts +0 -14
- package/dist/src/plugin/agents/browser-qa.js +0 -31
- package/dist/src/plugin/agents/common.d.ts +0 -36
- package/dist/src/plugin/agents/common.js +0 -59
- package/dist/src/plugin/agents/cto.d.ts +0 -9
- package/dist/src/plugin/agents/cto.js +0 -39
- package/dist/src/plugin/agents/engineers.d.ts +0 -9
- package/dist/src/plugin/agents/engineers.js +0 -11
- package/dist/src/plugin/agents/index.d.ts +0 -5
- package/dist/src/plugin/agents/index.js +0 -5
- package/dist/src/plugin/agents/team-planner.d.ts +0 -10
- package/dist/src/plugin/agents/team-planner.js +0 -23
- package/dist/src/plugin/claude-manager.plugin.d.ts +0 -10
- package/dist/src/plugin/claude-manager.plugin.js +0 -950
- package/dist/src/plugin/service-factory.d.ts +0 -38
- package/dist/src/plugin/service-factory.js +0 -101
- package/dist/src/prompts/registry.d.ts +0 -2
- package/dist/src/prompts/registry.js +0 -210
- package/dist/src/state/file-run-state-store.d.ts +0 -14
- package/dist/src/state/file-run-state-store.js +0 -85
- package/dist/src/state/team-state-store.d.ts +0 -14
- package/dist/src/state/team-state-store.js +0 -88
- package/dist/src/state/transcript-store.d.ts +0 -15
- package/dist/src/state/transcript-store.js +0 -44
- package/dist/src/team/roster.d.ts +0 -5
- package/dist/src/team/roster.js +0 -40
- package/dist/src/types/contracts.d.ts +0 -261
- package/dist/src/types/contracts.js +0 -2
- package/dist/src/util/fs-helpers.d.ts +0 -8
- package/dist/src/util/fs-helpers.js +0 -21
- package/dist/src/util/project-context.d.ts +0 -10
- package/dist/src/util/project-context.js +0 -105
- package/dist/src/util/transcript-append.d.ts +0 -7
- package/dist/src/util/transcript-append.js +0 -29
- package/dist/state/file-run-state-store.d.ts +0 -14
- package/dist/state/file-run-state-store.js +0 -85
- package/dist/test/claude-agent-sdk-adapter.test.d.ts +0 -1
- package/dist/test/claude-agent-sdk-adapter.test.js +0 -707
- package/dist/test/claude-manager.plugin.test.d.ts +0 -1
- package/dist/test/claude-manager.plugin.test.js +0 -316
- package/dist/test/context-tracker.test.d.ts +0 -1
- package/dist/test/context-tracker.test.js +0 -130
- package/dist/test/cto-active-team.test.d.ts +0 -1
- package/dist/test/cto-active-team.test.js +0 -199
- package/dist/test/file-run-state-store.test.d.ts +0 -1
- package/dist/test/file-run-state-store.test.js +0 -82
- package/dist/test/fs-helpers.test.d.ts +0 -1
- package/dist/test/fs-helpers.test.js +0 -56
- package/dist/test/git-operations.test.d.ts +0 -1
- package/dist/test/git-operations.test.js +0 -133
- package/dist/test/persistent-manager.test.d.ts +0 -1
- package/dist/test/persistent-manager.test.js +0 -48
- package/dist/test/project-context.test.d.ts +0 -1
- package/dist/test/project-context.test.js +0 -92
- package/dist/test/prompt-registry.test.d.ts +0 -1
- package/dist/test/prompt-registry.test.js +0 -117
- package/dist/test/report-claude-event.test.d.ts +0 -1
- package/dist/test/report-claude-event.test.js +0 -304
- package/dist/test/session-controller.test.d.ts +0 -1
- package/dist/test/session-controller.test.js +0 -149
- package/dist/test/session-live-tailer.test.d.ts +0 -1
- package/dist/test/session-live-tailer.test.js +0 -313
- package/dist/test/team-orchestrator.test.d.ts +0 -1
- package/dist/test/team-orchestrator.test.js +0 -583
- package/dist/test/team-state-store.test.d.ts +0 -1
- package/dist/test/team-state-store.test.js +0 -54
- package/dist/test/tool-approval-manager.test.d.ts +0 -1
- package/dist/test/tool-approval-manager.test.js +0 -260
- package/dist/test/transcript-append.test.d.ts +0 -1
- package/dist/test/transcript-append.test.js +0 -37
- package/dist/test/transcript-store.test.d.ts +0 -1
- package/dist/test/transcript-store.test.js +0 -50
- package/dist/test/undo-propagation.test.d.ts +0 -1
- package/dist/test/undo-propagation.test.js +0 -837
- package/dist/util/project-context.d.ts +0 -10
- package/dist/util/project-context.js +0 -105
- package/dist/vitest.config.d.ts +0 -2
- package/dist/vitest.config.js +0 -11
|
@@ -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 += typeof data === 'string' ? data : data.toString('utf8');
|
|
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 '[non-serializable]';
|
|
268
|
-
}
|
|
269
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { ClaudeAgentSdkAdapter, ClaudeSessionEventHandler } from '../claude/claude-agent-sdk-adapter.js';
|
|
2
|
-
import type { ClaudeSessionRunResult, SessionContextSnapshot, SessionMode } from '../types/contracts.js';
|
|
3
|
-
import type { ContextTracker } from './context-tracker.js';
|
|
4
|
-
export declare class SessionController {
|
|
5
|
-
private readonly sdkAdapter;
|
|
6
|
-
private readonly contextTracker;
|
|
7
|
-
private readonly sessionPrompt;
|
|
8
|
-
private readonly wrapperType;
|
|
9
|
-
private readonly worktree;
|
|
10
|
-
private readonly modePrefixes;
|
|
11
|
-
private activeSessionId;
|
|
12
|
-
constructor(sdkAdapter: ClaudeAgentSdkAdapter, contextTracker: ContextTracker, sessionPrompt: string | undefined, wrapperType: string, worktree: string, modePrefixes?: {
|
|
13
|
-
plan: string;
|
|
14
|
-
free: string;
|
|
15
|
-
});
|
|
16
|
-
get isActive(): boolean;
|
|
17
|
-
get sessionId(): string | null;
|
|
18
|
-
/**
|
|
19
|
-
* Send a message to the persistent session. Creates one if none exists.
|
|
20
|
-
* Returns the session result including usage data.
|
|
21
|
-
*/
|
|
22
|
-
sendMessage(message: string, options?: {
|
|
23
|
-
model?: string;
|
|
24
|
-
effort?: 'low' | 'medium' | 'high' | 'max';
|
|
25
|
-
mode?: SessionMode;
|
|
26
|
-
sessionSystemPrompt?: string;
|
|
27
|
-
abortSignal?: AbortSignal;
|
|
28
|
-
}, onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
29
|
-
/**
|
|
30
|
-
* Send /compact to the current session to compress context.
|
|
31
|
-
*/
|
|
32
|
-
compactSession(onEvent?: ClaudeSessionEventHandler): Promise<ClaudeSessionRunResult>;
|
|
33
|
-
/**
|
|
34
|
-
* Clear the current session. The next sendMessage will create a fresh one.
|
|
35
|
-
*/
|
|
36
|
-
clearSession(): Promise<string | null>;
|
|
37
|
-
/**
|
|
38
|
-
* Get current context tracking snapshot.
|
|
39
|
-
*/
|
|
40
|
-
getContextSnapshot(): SessionContextSnapshot;
|
|
41
|
-
}
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
export class SessionController {
|
|
2
|
-
sdkAdapter;
|
|
3
|
-
contextTracker;
|
|
4
|
-
sessionPrompt;
|
|
5
|
-
wrapperType;
|
|
6
|
-
worktree;
|
|
7
|
-
modePrefixes;
|
|
8
|
-
activeSessionId = null;
|
|
9
|
-
constructor(sdkAdapter, contextTracker, sessionPrompt, wrapperType, worktree, modePrefixes = {
|
|
10
|
-
plan: '',
|
|
11
|
-
free: '',
|
|
12
|
-
}) {
|
|
13
|
-
this.sdkAdapter = sdkAdapter;
|
|
14
|
-
this.contextTracker = contextTracker;
|
|
15
|
-
this.sessionPrompt = sessionPrompt;
|
|
16
|
-
this.wrapperType = wrapperType;
|
|
17
|
-
this.worktree = worktree;
|
|
18
|
-
this.modePrefixes = modePrefixes;
|
|
19
|
-
}
|
|
20
|
-
get isActive() {
|
|
21
|
-
return this.activeSessionId !== null;
|
|
22
|
-
}
|
|
23
|
-
get sessionId() {
|
|
24
|
-
return this.activeSessionId;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Send a message to the persistent session. Creates one if none exists.
|
|
28
|
-
* Returns the session result including usage data.
|
|
29
|
-
*/
|
|
30
|
-
async sendMessage(message, options, onEvent) {
|
|
31
|
-
const mode = options?.mode ?? 'free';
|
|
32
|
-
const prefix = this.modePrefixes[mode];
|
|
33
|
-
const prompt = prefix ? `${prefix}\n\n${message}` : message;
|
|
34
|
-
const input = {
|
|
35
|
-
cwd: this.worktree,
|
|
36
|
-
prompt,
|
|
37
|
-
persistSession: true,
|
|
38
|
-
permissionMode: mode === 'plan' ? 'plan' : 'acceptEdits',
|
|
39
|
-
includePartialMessages: true,
|
|
40
|
-
model: options?.model,
|
|
41
|
-
effort: options?.effort,
|
|
42
|
-
settingSources: ['user'],
|
|
43
|
-
abortSignal: options?.abortSignal,
|
|
44
|
-
};
|
|
45
|
-
if (this.activeSessionId) {
|
|
46
|
-
// Resume existing session
|
|
47
|
-
input.resumeSessionId = this.activeSessionId;
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
// New session — prefer dynamically constructed prompt from wrapper, fall back to static default
|
|
51
|
-
input.systemPrompt = options?.sessionSystemPrompt ?? this.sessionPrompt;
|
|
52
|
-
input.model ??= 'claude-opus-4-6';
|
|
53
|
-
input.effort ??= 'high';
|
|
54
|
-
}
|
|
55
|
-
const result = await this.sdkAdapter.runSession(input, onEvent);
|
|
56
|
-
// Track the session ID
|
|
57
|
-
if (result.sessionId) {
|
|
58
|
-
this.activeSessionId = result.sessionId;
|
|
59
|
-
}
|
|
60
|
-
// Update context tracking
|
|
61
|
-
this.contextTracker.recordResult({
|
|
62
|
-
sessionId: result.sessionId,
|
|
63
|
-
turns: result.turns,
|
|
64
|
-
totalCostUsd: result.totalCostUsd,
|
|
65
|
-
inputTokens: result.inputTokens,
|
|
66
|
-
outputTokens: result.outputTokens,
|
|
67
|
-
contextWindowSize: result.contextWindowSize,
|
|
68
|
-
});
|
|
69
|
-
return result;
|
|
70
|
-
}
|
|
71
|
-
/**
|
|
72
|
-
* Send /compact to the current session to compress context.
|
|
73
|
-
*/
|
|
74
|
-
async compactSession(onEvent) {
|
|
75
|
-
if (!this.activeSessionId) {
|
|
76
|
-
throw new Error('No active session to compact');
|
|
77
|
-
}
|
|
78
|
-
const result = await this.sendMessage('/compact', undefined, onEvent);
|
|
79
|
-
this.contextTracker.recordCompaction();
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Clear the current session. The next sendMessage will create a fresh one.
|
|
84
|
-
*/
|
|
85
|
-
async clearSession() {
|
|
86
|
-
const clearedId = this.activeSessionId;
|
|
87
|
-
this.activeSessionId = null;
|
|
88
|
-
this.contextTracker.reset();
|
|
89
|
-
return clearedId;
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Get current context tracking snapshot.
|
|
93
|
-
*/
|
|
94
|
-
getContextSnapshot() {
|
|
95
|
-
return this.contextTracker.snapshot();
|
|
96
|
-
}
|
|
97
|
-
}
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import type { ClaudeMetadataSnapshot, ClaudeSettingSource } from '../types/contracts.js';
|
|
2
|
-
import type { ClaudeAgentSdkAdapter } from '../claude/claude-agent-sdk-adapter.js';
|
|
3
|
-
import type { RepoClaudeConfigReader } from './repo-claude-config-reader.js';
|
|
4
|
-
export declare class ClaudeMetadataService {
|
|
5
|
-
private readonly configReader;
|
|
6
|
-
private readonly sdkAdapter;
|
|
7
|
-
constructor(configReader: RepoClaudeConfigReader, sdkAdapter: ClaudeAgentSdkAdapter);
|
|
8
|
-
collect(cwd: string, options?: {
|
|
9
|
-
includeSdkProbe?: boolean;
|
|
10
|
-
settingSources?: ClaudeSettingSource[];
|
|
11
|
-
}): Promise<ClaudeMetadataSnapshot>;
|
|
12
|
-
}
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
export class ClaudeMetadataService {
|
|
2
|
-
configReader;
|
|
3
|
-
sdkAdapter;
|
|
4
|
-
constructor(configReader, sdkAdapter) {
|
|
5
|
-
this.configReader = configReader;
|
|
6
|
-
this.sdkAdapter = sdkAdapter;
|
|
7
|
-
}
|
|
8
|
-
async collect(cwd, options = {}) {
|
|
9
|
-
const baseSnapshot = await this.configReader.read(cwd);
|
|
10
|
-
if (!options.includeSdkProbe) {
|
|
11
|
-
return dedupeSnapshot(baseSnapshot);
|
|
12
|
-
}
|
|
13
|
-
const capabilities = await this.sdkAdapter.probeCapabilities(cwd, options.settingSources);
|
|
14
|
-
return dedupeSnapshot({
|
|
15
|
-
...baseSnapshot,
|
|
16
|
-
commands: [...baseSnapshot.commands, ...capabilities.commands],
|
|
17
|
-
agents: capabilities.agents,
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
function dedupeSnapshot(snapshot) {
|
|
22
|
-
return {
|
|
23
|
-
...snapshot,
|
|
24
|
-
commands: dedupeByName(snapshot.commands),
|
|
25
|
-
skills: dedupeByName(snapshot.skills),
|
|
26
|
-
hooks: dedupeByName(snapshot.hooks),
|
|
27
|
-
agents: dedupeByName(snapshot.agents),
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
function dedupeByName(items) {
|
|
31
|
-
const seen = new Map();
|
|
32
|
-
for (const item of items) {
|
|
33
|
-
if (!seen.has(item.name)) {
|
|
34
|
-
seen.set(item.name, item);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return [...seen.values()].sort((left, right) => left.name.localeCompare(right.name));
|
|
38
|
-
}
|
|
@@ -1,154 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import JSON5 from 'json5';
|
|
4
|
-
export class RepoClaudeConfigReader {
|
|
5
|
-
async read(cwd) {
|
|
6
|
-
const claudeDirectory = path.join(cwd, '.claude');
|
|
7
|
-
const skillsDirectory = path.join(claudeDirectory, 'skills');
|
|
8
|
-
const commandsDirectory = path.join(claudeDirectory, 'commands');
|
|
9
|
-
const claudeMdCandidates = [
|
|
10
|
-
path.join(cwd, 'CLAUDE.md'),
|
|
11
|
-
path.join(claudeDirectory, 'CLAUDE.md'),
|
|
12
|
-
];
|
|
13
|
-
const collectedAt = new Date().toISOString();
|
|
14
|
-
const [skills, commands, settingsResult, claudeMdPath] = await Promise.all([
|
|
15
|
-
this.readSkills(skillsDirectory),
|
|
16
|
-
this.readCommands(commandsDirectory),
|
|
17
|
-
this.readSettings(claudeDirectory),
|
|
18
|
-
findFirstExistingPath(claudeMdCandidates),
|
|
19
|
-
]);
|
|
20
|
-
return {
|
|
21
|
-
collectedAt,
|
|
22
|
-
cwd,
|
|
23
|
-
commands: [...skillsToCommands(skills), ...commands],
|
|
24
|
-
skills,
|
|
25
|
-
hooks: settingsResult.hooks,
|
|
26
|
-
agents: [],
|
|
27
|
-
claudeMdPath: claudeMdPath ?? undefined,
|
|
28
|
-
settingsPaths: settingsResult.settingsPaths,
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
async readSkills(directory) {
|
|
32
|
-
if (!(await pathExists(directory))) {
|
|
33
|
-
return [];
|
|
34
|
-
}
|
|
35
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
36
|
-
const skills = await Promise.all(entries
|
|
37
|
-
.filter((entry) => entry.isDirectory())
|
|
38
|
-
.map(async (entry) => {
|
|
39
|
-
const skillPath = path.join(directory, entry.name, 'SKILL.md');
|
|
40
|
-
if (!(await pathExists(skillPath))) {
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
43
|
-
const content = await fs.readFile(skillPath, 'utf8');
|
|
44
|
-
return {
|
|
45
|
-
name: entry.name,
|
|
46
|
-
description: extractMarkdownDescription(content),
|
|
47
|
-
path: skillPath,
|
|
48
|
-
source: 'skill',
|
|
49
|
-
};
|
|
50
|
-
}));
|
|
51
|
-
return skills.filter((skill) => skill !== null);
|
|
52
|
-
}
|
|
53
|
-
async readCommands(directory) {
|
|
54
|
-
if (!(await pathExists(directory))) {
|
|
55
|
-
return [];
|
|
56
|
-
}
|
|
57
|
-
const commandFiles = await collectMarkdownFiles(directory);
|
|
58
|
-
const commands = await Promise.all(commandFiles.map(async (commandPath) => {
|
|
59
|
-
const content = await fs.readFile(commandPath, 'utf8');
|
|
60
|
-
return {
|
|
61
|
-
name: path.basename(commandPath, path.extname(commandPath)),
|
|
62
|
-
description: extractMarkdownDescription(content),
|
|
63
|
-
source: 'command',
|
|
64
|
-
path: commandPath,
|
|
65
|
-
};
|
|
66
|
-
}));
|
|
67
|
-
return commands.sort((left, right) => left.name.localeCompare(right.name));
|
|
68
|
-
}
|
|
69
|
-
async readSettings(claudeDirectory) {
|
|
70
|
-
const candidatePaths = [
|
|
71
|
-
path.join(claudeDirectory, 'settings.json'),
|
|
72
|
-
path.join(claudeDirectory, 'settings.local.json'),
|
|
73
|
-
];
|
|
74
|
-
const settingsPaths = [];
|
|
75
|
-
const hooks = [];
|
|
76
|
-
for (const candidatePath of candidatePaths) {
|
|
77
|
-
if (!(await pathExists(candidatePath))) {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
settingsPaths.push(candidatePath);
|
|
81
|
-
const content = await fs.readFile(candidatePath, 'utf8');
|
|
82
|
-
const parsed = JSON5.parse(content);
|
|
83
|
-
const hookEntries = Object.entries(parsed.hooks ?? {});
|
|
84
|
-
for (const [hookName, hookValue] of hookEntries) {
|
|
85
|
-
const hookMatchers = Array.isArray(hookValue) ? hookValue : [hookValue];
|
|
86
|
-
for (const hookMatcher of hookMatchers) {
|
|
87
|
-
if (!hookMatcher || typeof hookMatcher !== 'object') {
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
const matcher = typeof hookMatcher.matcher === 'string'
|
|
91
|
-
? hookMatcher.matcher
|
|
92
|
-
: undefined;
|
|
93
|
-
const commandCount = Array.isArray(hookMatcher.hooks)
|
|
94
|
-
? (hookMatcher.hooks?.length ?? 0)
|
|
95
|
-
: 0;
|
|
96
|
-
hooks.push({
|
|
97
|
-
name: hookName,
|
|
98
|
-
matcher,
|
|
99
|
-
sourcePath: candidatePath,
|
|
100
|
-
commandCount,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
return {
|
|
106
|
-
settingsPaths,
|
|
107
|
-
hooks,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
function extractMarkdownDescription(markdown) {
|
|
112
|
-
const lines = markdown
|
|
113
|
-
.split(/\r?\n/)
|
|
114
|
-
.map((line) => line.trim())
|
|
115
|
-
.filter(Boolean);
|
|
116
|
-
const descriptionLine = lines.find((line) => !line.startsWith('#') && !line.startsWith('---'));
|
|
117
|
-
return descriptionLine ?? 'No description provided.';
|
|
118
|
-
}
|
|
119
|
-
async function collectMarkdownFiles(directory) {
|
|
120
|
-
const entries = await fs.readdir(directory, { withFileTypes: true });
|
|
121
|
-
const files = await Promise.all(entries.map(async (entry) => {
|
|
122
|
-
const resolvedPath = path.join(directory, entry.name);
|
|
123
|
-
if (entry.isDirectory()) {
|
|
124
|
-
return collectMarkdownFiles(resolvedPath);
|
|
125
|
-
}
|
|
126
|
-
return entry.name.endsWith('.md') ? [resolvedPath] : [];
|
|
127
|
-
}));
|
|
128
|
-
return files.flat();
|
|
129
|
-
}
|
|
130
|
-
async function pathExists(candidatePath) {
|
|
131
|
-
try {
|
|
132
|
-
await fs.access(candidatePath);
|
|
133
|
-
return true;
|
|
134
|
-
}
|
|
135
|
-
catch {
|
|
136
|
-
return false;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
async function findFirstExistingPath(candidatePaths) {
|
|
140
|
-
for (const candidatePath of candidatePaths) {
|
|
141
|
-
if (await pathExists(candidatePath)) {
|
|
142
|
-
return candidatePath;
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
function skillsToCommands(skills) {
|
|
148
|
-
return skills.map((skill) => ({
|
|
149
|
-
name: skill.name,
|
|
150
|
-
description: skill.description,
|
|
151
|
-
source: 'skill',
|
|
152
|
-
path: skill.path,
|
|
153
|
-
}));
|
|
154
|
-
}
|