@doingdev/opencode-claude-manager-plugin 0.1.35 → 0.1.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/dist/claude/claude-agent-sdk-adapter.js +1 -0
  2. package/dist/manager/git-operations.d.ts +10 -1
  3. package/dist/manager/git-operations.js +18 -3
  4. package/dist/manager/persistent-manager.d.ts +19 -3
  5. package/dist/manager/persistent-manager.js +21 -9
  6. package/dist/manager/session-controller.d.ts +8 -5
  7. package/dist/manager/session-controller.js +25 -20
  8. package/dist/metadata/claude-metadata.service.d.ts +12 -0
  9. package/dist/metadata/claude-metadata.service.js +38 -0
  10. package/dist/metadata/repo-claude-config-reader.d.ts +7 -0
  11. package/dist/metadata/repo-claude-config-reader.js +154 -0
  12. package/dist/plugin/agent-hierarchy.d.ts +9 -9
  13. package/dist/plugin/agent-hierarchy.js +25 -25
  14. package/dist/plugin/claude-manager.plugin.js +83 -46
  15. package/dist/plugin/orchestrator.plugin.d.ts +2 -0
  16. package/dist/plugin/orchestrator.plugin.js +116 -0
  17. package/dist/plugin/service-factory.js +3 -8
  18. package/dist/prompts/registry.js +100 -103
  19. package/dist/providers/claude-code-wrapper.d.ts +13 -0
  20. package/dist/providers/claude-code-wrapper.js +13 -0
  21. package/dist/safety/bash-safety.d.ts +21 -0
  22. package/dist/safety/bash-safety.js +62 -0
  23. package/dist/src/claude/claude-agent-sdk-adapter.d.ts +27 -0
  24. package/dist/src/claude/claude-agent-sdk-adapter.js +517 -0
  25. package/dist/src/claude/claude-session.service.d.ts +10 -0
  26. package/dist/src/claude/claude-session.service.js +18 -0
  27. package/dist/src/claude/session-live-tailer.d.ts +51 -0
  28. package/dist/src/claude/session-live-tailer.js +269 -0
  29. package/dist/src/claude/tool-approval-manager.d.ts +27 -0
  30. package/dist/src/claude/tool-approval-manager.js +232 -0
  31. package/dist/src/index.d.ts +6 -0
  32. package/dist/src/index.js +4 -0
  33. package/dist/src/manager/context-tracker.d.ts +33 -0
  34. package/dist/src/manager/context-tracker.js +106 -0
  35. package/dist/src/manager/git-operations.d.ts +12 -0
  36. package/dist/src/manager/git-operations.js +76 -0
  37. package/dist/src/manager/persistent-manager.d.ts +77 -0
  38. package/dist/src/manager/persistent-manager.js +170 -0
  39. package/dist/src/manager/session-controller.d.ts +44 -0
  40. package/dist/src/manager/session-controller.js +147 -0
  41. package/dist/src/plugin/agent-hierarchy.d.ts +60 -0
  42. package/dist/src/plugin/agent-hierarchy.js +157 -0
  43. package/dist/src/plugin/claude-manager.plugin.d.ts +2 -0
  44. package/dist/src/plugin/claude-manager.plugin.js +563 -0
  45. package/dist/src/plugin/service-factory.d.ts +12 -0
  46. package/dist/src/plugin/service-factory.js +38 -0
  47. package/dist/src/prompts/registry.d.ts +11 -0
  48. package/dist/src/prompts/registry.js +260 -0
  49. package/dist/src/state/file-run-state-store.d.ts +14 -0
  50. package/dist/src/state/file-run-state-store.js +85 -0
  51. package/dist/src/state/transcript-store.d.ts +15 -0
  52. package/dist/src/state/transcript-store.js +44 -0
  53. package/dist/src/types/contracts.d.ts +200 -0
  54. package/dist/src/types/contracts.js +1 -0
  55. package/dist/src/util/fs-helpers.d.ts +2 -0
  56. package/dist/src/util/fs-helpers.js +10 -0
  57. package/dist/src/util/project-context.d.ts +10 -0
  58. package/dist/src/util/project-context.js +105 -0
  59. package/dist/src/util/transcript-append.d.ts +7 -0
  60. package/dist/src/util/transcript-append.js +29 -0
  61. package/dist/test/claude-agent-sdk-adapter.test.d.ts +1 -0
  62. package/dist/test/claude-agent-sdk-adapter.test.js +459 -0
  63. package/dist/test/claude-manager.plugin.test.d.ts +1 -0
  64. package/dist/test/claude-manager.plugin.test.js +331 -0
  65. package/dist/test/context-tracker.test.d.ts +1 -0
  66. package/dist/test/context-tracker.test.js +138 -0
  67. package/dist/test/file-run-state-store.test.d.ts +1 -0
  68. package/dist/test/file-run-state-store.test.js +82 -0
  69. package/dist/test/git-operations.test.d.ts +1 -0
  70. package/dist/test/git-operations.test.js +90 -0
  71. package/dist/test/persistent-manager.test.d.ts +1 -0
  72. package/dist/test/persistent-manager.test.js +208 -0
  73. package/dist/test/project-context.test.d.ts +1 -0
  74. package/dist/test/project-context.test.js +92 -0
  75. package/dist/test/prompt-registry.test.d.ts +1 -0
  76. package/dist/test/prompt-registry.test.js +256 -0
  77. package/dist/test/session-controller.test.d.ts +1 -0
  78. package/dist/test/session-controller.test.js +149 -0
  79. package/dist/test/session-live-tailer.test.d.ts +1 -0
  80. package/dist/test/session-live-tailer.test.js +313 -0
  81. package/dist/test/tool-approval-manager.test.d.ts +1 -0
  82. package/dist/test/tool-approval-manager.test.js +264 -0
  83. package/dist/test/transcript-append.test.d.ts +1 -0
  84. package/dist/test/transcript-append.test.js +37 -0
  85. package/dist/test/transcript-store.test.d.ts +1 -0
  86. package/dist/test/transcript-store.test.js +50 -0
  87. package/dist/types/contracts.d.ts +3 -4
  88. package/dist/vitest.config.d.ts +2 -0
  89. package/dist/vitest.config.js +11 -0
  90. package/package.json +2 -2
@@ -0,0 +1,269 @@
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
+ }
@@ -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-skill',
95
+ toolPattern: 'Skill',
96
+ action: 'allow',
97
+ description: 'Allow Agent Skills (filesystem SKILL.md)',
98
+ },
99
+ {
100
+ id: 'allow-agent',
101
+ toolPattern: 'Agent',
102
+ action: 'allow',
103
+ description: 'Allow agent delegation',
104
+ },
105
+ {
106
+ id: 'allow-todowrite',
107
+ toolPattern: 'TodoWrite',
108
+ action: 'allow',
109
+ description: 'Allow todo tracking',
110
+ },
111
+ {
112
+ id: 'allow-todoread',
113
+ toolPattern: 'TodoRead',
114
+ action: 'allow',
115
+ description: 'Allow todo reading',
116
+ },
117
+ ];
118
+ }
119
+ export class ToolApprovalManager {
120
+ policy;
121
+ decisions = [];
122
+ maxDecisions;
123
+ constructor(policy, maxDecisions) {
124
+ this.policy = {
125
+ rules: policy?.rules ?? getDefaultRules(),
126
+ defaultAction: policy?.defaultAction ?? 'allow',
127
+ defaultDenyMessage: policy?.defaultDenyMessage ?? 'Tool call denied by approval policy.',
128
+ enabled: policy?.enabled ?? true,
129
+ };
130
+ this.maxDecisions = maxDecisions ?? DEFAULT_MAX_DECISIONS;
131
+ }
132
+ evaluate(toolName, input, options) {
133
+ if (!this.policy.enabled) {
134
+ return { behavior: 'allow' };
135
+ }
136
+ const inputJson = safeJsonStringify(input);
137
+ const matchedRule = this.findMatchingRule(toolName, inputJson);
138
+ const action = matchedRule?.action ?? this.policy.defaultAction;
139
+ const denyMessage = action === 'deny'
140
+ ? (matchedRule?.denyMessage ?? this.policy.defaultDenyMessage ?? 'Denied by policy.')
141
+ : undefined;
142
+ this.recordDecision({
143
+ timestamp: new Date().toISOString(),
144
+ toolName,
145
+ inputPreview: inputJson.slice(0, INPUT_PREVIEW_MAX),
146
+ title: options?.title,
147
+ matchedRuleId: matchedRule?.id ?? 'default',
148
+ action,
149
+ denyMessage,
150
+ agentId: options?.agentID,
151
+ });
152
+ if (action === 'deny') {
153
+ return { behavior: 'deny', message: denyMessage };
154
+ }
155
+ return { behavior: 'allow' };
156
+ }
157
+ getDecisions(limit) {
158
+ const all = [...this.decisions].reverse();
159
+ return limit ? all.slice(0, limit) : all;
160
+ }
161
+ getDeniedDecisions(limit) {
162
+ const denied = this.decisions.filter((d) => d.action === 'deny').reverse();
163
+ return limit ? denied.slice(0, limit) : denied;
164
+ }
165
+ clearDecisions() {
166
+ this.decisions = [];
167
+ }
168
+ getPolicy() {
169
+ return { ...this.policy, rules: [...this.policy.rules] };
170
+ }
171
+ setPolicy(policy) {
172
+ this.policy = { ...policy, rules: [...policy.rules] };
173
+ }
174
+ addRule(rule, position) {
175
+ if (position !== undefined && position >= 0 && position < this.policy.rules.length) {
176
+ this.policy.rules.splice(position, 0, rule);
177
+ }
178
+ else {
179
+ this.policy.rules.push(rule);
180
+ }
181
+ }
182
+ removeRule(ruleId) {
183
+ const index = this.policy.rules.findIndex((r) => r.id === ruleId);
184
+ if (index === -1) {
185
+ return false;
186
+ }
187
+ this.policy.rules.splice(index, 1);
188
+ return true;
189
+ }
190
+ setDefaultAction(action) {
191
+ this.policy.defaultAction = action;
192
+ }
193
+ setEnabled(enabled) {
194
+ this.policy.enabled = enabled;
195
+ }
196
+ findMatchingRule(toolName, inputJson) {
197
+ for (const rule of this.policy.rules) {
198
+ if (!matchesToolPattern(rule.toolPattern, toolName)) {
199
+ continue;
200
+ }
201
+ if (rule.inputPattern && !inputJson.includes(rule.inputPattern)) {
202
+ continue;
203
+ }
204
+ return rule;
205
+ }
206
+ return null;
207
+ }
208
+ recordDecision(decision) {
209
+ this.decisions.push(decision);
210
+ if (this.decisions.length > this.maxDecisions) {
211
+ this.decisions = this.decisions.slice(-this.maxDecisions);
212
+ }
213
+ }
214
+ }
215
+ function matchesToolPattern(pattern, toolName) {
216
+ if (pattern === '*') {
217
+ return true;
218
+ }
219
+ if (!pattern.includes('*')) {
220
+ return pattern === toolName;
221
+ }
222
+ const regex = new RegExp('^' + pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '.*') + '$');
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
+ }
@@ -0,0 +1,6 @@
1
+ import type { Plugin } from '@opencode-ai/plugin';
2
+ import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
3
+ export type { ClaudeCapabilitySnapshot, ClaudeSessionRunResult, ClaudeSessionSummary, ClaudeSessionTranscriptMessage, ManagerPromptRegistry, RunClaudeSessionInput, SessionContextSnapshot, GitDiffResult, GitOperationResult, PersistentRunRecord, PersistentRunResult, ActiveSessionState, ContextWarningLevel, SessionMode, LiveTailEvent, ToolOutputPreview, ToolApprovalRule, ToolApprovalPolicy, ToolApprovalDecision, } from './types/contracts.js';
4
+ export { SessionLiveTailer } from './claude/session-live-tailer.js';
5
+ export { ClaudeManagerPlugin };
6
+ export declare const plugin: Plugin;
@@ -0,0 +1,4 @@
1
+ import { ClaudeManagerPlugin } from './plugin/claude-manager.plugin.js';
2
+ export { SessionLiveTailer } from './claude/session-live-tailer.js';
3
+ export { ClaudeManagerPlugin };
4
+ export const plugin = ClaudeManagerPlugin;
@@ -0,0 +1,33 @@
1
+ import type { ContextWarningLevel, SessionContextSnapshot } from '../types/contracts.js';
2
+ export declare class ContextTracker {
3
+ private totalTurns;
4
+ private totalCostUsd;
5
+ private latestInputTokens;
6
+ private latestOutputTokens;
7
+ private contextWindowSize;
8
+ private compactionCount;
9
+ private sessionId;
10
+ recordResult(result: {
11
+ sessionId?: string;
12
+ turns?: number;
13
+ totalCostUsd?: number;
14
+ inputTokens?: number;
15
+ outputTokens?: number;
16
+ contextWindowSize?: number;
17
+ }): void;
18
+ recordCompaction(): void;
19
+ snapshot(): SessionContextSnapshot;
20
+ warningLevel(): ContextWarningLevel;
21
+ estimateContextPercent(): number | null;
22
+ isAboveTokenThreshold(thresholdTokens?: number): boolean;
23
+ reset(): void;
24
+ /** Restore from persisted active session state. */
25
+ restore(state: {
26
+ sessionId: string;
27
+ totalTurns: number;
28
+ totalCostUsd: number;
29
+ estimatedContextPercent: number | null;
30
+ contextWindowSize: number | null;
31
+ latestInputTokens: number | null;
32
+ }): void;
33
+ }
@@ -0,0 +1,106 @@
1
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
2
+ export class ContextTracker {
3
+ totalTurns = 0;
4
+ totalCostUsd = 0;
5
+ latestInputTokens = null;
6
+ latestOutputTokens = null;
7
+ contextWindowSize = null;
8
+ compactionCount = 0;
9
+ sessionId = null;
10
+ recordResult(result) {
11
+ if (result.sessionId) {
12
+ this.sessionId = result.sessionId;
13
+ }
14
+ if (result.turns !== undefined) {
15
+ this.totalTurns = result.turns;
16
+ }
17
+ if (result.totalCostUsd !== undefined) {
18
+ this.totalCostUsd = result.totalCostUsd;
19
+ }
20
+ if (result.inputTokens !== undefined) {
21
+ // If input tokens dropped significantly, compaction likely occurred
22
+ if (this.latestInputTokens !== null && result.inputTokens < this.latestInputTokens * 0.5) {
23
+ this.compactionCount++;
24
+ }
25
+ this.latestInputTokens = result.inputTokens;
26
+ }
27
+ if (result.outputTokens !== undefined) {
28
+ this.latestOutputTokens = result.outputTokens;
29
+ }
30
+ if (result.contextWindowSize !== undefined) {
31
+ this.contextWindowSize = result.contextWindowSize;
32
+ }
33
+ }
34
+ recordCompaction() {
35
+ this.compactionCount++;
36
+ }
37
+ snapshot() {
38
+ return {
39
+ sessionId: this.sessionId,
40
+ totalTurns: this.totalTurns,
41
+ totalCostUsd: this.totalCostUsd,
42
+ latestInputTokens: this.latestInputTokens,
43
+ latestOutputTokens: this.latestOutputTokens,
44
+ contextWindowSize: this.contextWindowSize,
45
+ estimatedContextPercent: this.estimateContextPercent(),
46
+ warningLevel: this.warningLevel(),
47
+ compactionCount: this.compactionCount,
48
+ };
49
+ }
50
+ warningLevel() {
51
+ const percent = this.estimateContextPercent();
52
+ if (percent === null) {
53
+ return 'ok';
54
+ }
55
+ if (percent >= 85) {
56
+ return 'critical';
57
+ }
58
+ if (percent >= 70) {
59
+ return 'high';
60
+ }
61
+ if (percent >= 50) {
62
+ return 'moderate';
63
+ }
64
+ return 'ok';
65
+ }
66
+ estimateContextPercent() {
67
+ // Tier 1: Token-based (most accurate)
68
+ if (this.latestInputTokens !== null) {
69
+ const window = this.contextWindowSize ?? DEFAULT_CONTEXT_WINDOW;
70
+ return Math.min(100, Math.round((this.latestInputTokens / window) * 100));
71
+ }
72
+ // Tier 2: Cost-based heuristic
73
+ if (this.totalCostUsd > 0) {
74
+ const estimatedTokens = this.totalCostUsd * 130_000;
75
+ const window = this.contextWindowSize ?? DEFAULT_CONTEXT_WINDOW;
76
+ return Math.min(100, Math.round((estimatedTokens / window) * 100));
77
+ }
78
+ // Tier 3: Turns-based fallback
79
+ if (this.totalTurns > 0) {
80
+ const estimatedTokens = this.totalTurns * 6_000;
81
+ const window = this.contextWindowSize ?? DEFAULT_CONTEXT_WINDOW;
82
+ return Math.min(100, Math.round((estimatedTokens / window) * 100));
83
+ }
84
+ return null;
85
+ }
86
+ isAboveTokenThreshold(thresholdTokens = 200_000) {
87
+ return this.latestInputTokens !== null && this.latestInputTokens >= thresholdTokens;
88
+ }
89
+ reset() {
90
+ this.totalTurns = 0;
91
+ this.totalCostUsd = 0;
92
+ this.latestInputTokens = null;
93
+ this.latestOutputTokens = null;
94
+ this.contextWindowSize = null;
95
+ this.compactionCount = 0;
96
+ this.sessionId = null;
97
+ }
98
+ /** Restore from persisted active session state. */
99
+ restore(state) {
100
+ this.sessionId = state.sessionId;
101
+ this.totalTurns = state.totalTurns;
102
+ this.totalCostUsd = state.totalCostUsd;
103
+ this.contextWindowSize = state.contextWindowSize;
104
+ this.latestInputTokens = state.latestInputTokens;
105
+ }
106
+ }
@@ -0,0 +1,12 @@
1
+ import type { GitDiffResult, GitOperationResult } from '../types/contracts.js';
2
+ export declare class GitOperations {
3
+ private readonly cwd;
4
+ constructor(cwd: string);
5
+ diff(): Promise<GitDiffResult>;
6
+ diffStat(): Promise<string>;
7
+ commit(message: string): Promise<GitOperationResult>;
8
+ resetHard(): Promise<GitOperationResult>;
9
+ currentBranch(): Promise<string>;
10
+ recentCommits(count?: number): Promise<string>;
11
+ private git;
12
+ }