@doingdev/opencode-claude-manager-plugin 0.1.10 → 0.1.12

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.
@@ -0,0 +1,135 @@
1
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
2
+ import { dirname, join } from 'node:path';
3
+ const ACTIVE_SESSION_FILE = '.claude-manager/active-session.json';
4
+ export class SessionController {
5
+ sdkAdapter;
6
+ contextTracker;
7
+ sessionPrompt;
8
+ activeSessionId = null;
9
+ constructor(sdkAdapter, contextTracker, sessionPrompt) {
10
+ this.sdkAdapter = sdkAdapter;
11
+ this.contextTracker = contextTracker;
12
+ this.sessionPrompt = sessionPrompt;
13
+ }
14
+ get isActive() {
15
+ return this.activeSessionId !== null;
16
+ }
17
+ get sessionId() {
18
+ return this.activeSessionId;
19
+ }
20
+ /**
21
+ * Send a message to the persistent session. Creates one if none exists.
22
+ * Returns the session result including usage data.
23
+ */
24
+ async sendMessage(cwd, message, options, onEvent) {
25
+ const input = {
26
+ cwd,
27
+ prompt: message,
28
+ persistSession: true,
29
+ permissionMode: 'acceptEdits',
30
+ includePartialMessages: true,
31
+ model: options?.model,
32
+ settingSources: options?.settingSources ?? ['user', 'project', 'local'],
33
+ };
34
+ if (this.activeSessionId) {
35
+ // Resume existing session
36
+ input.resumeSessionId = this.activeSessionId;
37
+ }
38
+ else {
39
+ // New session — apply the expert operator system prompt
40
+ input.systemPrompt = this.sessionPrompt;
41
+ }
42
+ const result = await this.sdkAdapter.runSession(input, onEvent);
43
+ // Track the session ID
44
+ if (result.sessionId) {
45
+ this.activeSessionId = result.sessionId;
46
+ }
47
+ // Update context tracking
48
+ this.contextTracker.recordResult({
49
+ sessionId: result.sessionId,
50
+ turns: result.turns,
51
+ totalCostUsd: result.totalCostUsd,
52
+ inputTokens: result.inputTokens,
53
+ outputTokens: result.outputTokens,
54
+ contextWindowSize: result.contextWindowSize,
55
+ });
56
+ // Persist active session state
57
+ await this.persistActiveSession(cwd);
58
+ return result;
59
+ }
60
+ /**
61
+ * Send /compact to the current session to compress context.
62
+ */
63
+ async compactSession(cwd, onEvent) {
64
+ if (!this.activeSessionId) {
65
+ throw new Error('No active session to compact');
66
+ }
67
+ const result = await this.sendMessage(cwd, '/compact', undefined, onEvent);
68
+ this.contextTracker.recordCompaction();
69
+ return result;
70
+ }
71
+ /**
72
+ * Clear the current session. The next sendMessage will create a fresh one.
73
+ */
74
+ async clearSession(cwd) {
75
+ const clearedId = this.activeSessionId;
76
+ this.activeSessionId = null;
77
+ this.contextTracker.reset();
78
+ await this.removeActiveSession(cwd);
79
+ return clearedId;
80
+ }
81
+ /**
82
+ * Get current context tracking snapshot.
83
+ */
84
+ getContextSnapshot() {
85
+ return this.contextTracker.snapshot();
86
+ }
87
+ /**
88
+ * Try to restore active session from persisted state on startup.
89
+ */
90
+ async tryRestore(cwd) {
91
+ const filePath = join(cwd, ACTIVE_SESSION_FILE);
92
+ try {
93
+ const raw = await readFile(filePath, 'utf-8');
94
+ const state = JSON.parse(raw);
95
+ if (state.sessionId && state.cwd === cwd) {
96
+ this.activeSessionId = state.sessionId;
97
+ this.contextTracker.restore(state);
98
+ return true;
99
+ }
100
+ }
101
+ catch {
102
+ // File doesn't exist or is corrupt — start fresh
103
+ }
104
+ return false;
105
+ }
106
+ async persistActiveSession(cwd) {
107
+ if (!this.activeSessionId) {
108
+ return;
109
+ }
110
+ const snap = this.contextTracker.snapshot();
111
+ const state = {
112
+ sessionId: this.activeSessionId,
113
+ cwd,
114
+ startedAt: new Date().toISOString(),
115
+ totalTurns: snap.totalTurns,
116
+ totalCostUsd: snap.totalCostUsd,
117
+ estimatedContextPercent: snap.estimatedContextPercent,
118
+ contextWindowSize: snap.contextWindowSize,
119
+ latestInputTokens: snap.latestInputTokens,
120
+ };
121
+ const filePath = join(cwd, ACTIVE_SESSION_FILE);
122
+ await mkdir(dirname(filePath), { recursive: true });
123
+ await writeFile(filePath, JSON.stringify(state, null, 2));
124
+ }
125
+ async removeActiveSession(cwd) {
126
+ const filePath = join(cwd, ACTIVE_SESSION_FILE);
127
+ try {
128
+ const { unlink } = await import('node:fs/promises');
129
+ await unlink(filePath);
130
+ }
131
+ catch {
132
+ // File doesn't exist — that's fine
133
+ }
134
+ }
135
+ }
@@ -1,5 +1,5 @@
1
- import type { ManagedSubtaskPlan, ManagerRunMode } from '../types/contracts.js';
1
+ import type { ManagedSubtaskPlan } from '../types/contracts.js';
2
2
  export declare class TaskPlanner {
3
- plan(task: string, mode: ManagerRunMode, maxSubagents: number): ManagedSubtaskPlan[];
3
+ plan(tasks: string[], maxSubagents: number): ManagedSubtaskPlan[];
4
4
  private createPlan;
5
5
  }
@@ -1,20 +1,9 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  export class TaskPlanner {
3
- plan(task, mode, maxSubagents) {
4
- if (mode === 'single') {
5
- return [this.createPlan('Primary task', task)];
6
- }
7
- const splitCandidates = extractTaskCandidates(task).slice(0, maxSubagents);
8
- if (mode === 'split' && splitCandidates.length === 0) {
9
- return [this.createPlan('Primary task', task)];
10
- }
11
- if (mode === 'auto' && splitCandidates.length < 2) {
12
- return [this.createPlan('Primary task', task)];
13
- }
14
- if (splitCandidates.length === 0) {
15
- return [this.createPlan('Primary task', task)];
16
- }
17
- return splitCandidates.map((candidate, index) => this.createPlan(`Subtask ${index + 1}`, candidate));
3
+ plan(tasks, maxSubagents) {
4
+ return tasks
5
+ .slice(0, maxSubagents)
6
+ .map((task, index) => this.createPlan(tasks.length === 1 ? 'Primary task' : `Subtask ${index + 1}`, task));
18
7
  }
19
8
  createPlan(title, prompt) {
20
9
  return {
@@ -24,19 +13,3 @@ export class TaskPlanner {
24
13
  };
25
14
  }
26
15
  }
27
- function extractTaskCandidates(task) {
28
- const listMatches = task
29
- .split(/\r?\n/)
30
- .map((line) => line.trim())
31
- .filter((line) => /^(-|\*|\d+\.)\s+/.test(line))
32
- .map((line) => line.replace(/^(-|\*|\d+\.)\s+/, '').trim())
33
- .filter(Boolean);
34
- if (listMatches.length > 0) {
35
- return listMatches;
36
- }
37
- const sentenceMatches = task
38
- .split(/;|\n{2,}/)
39
- .map((part) => part.trim())
40
- .filter((part) => part.length > 20);
41
- return sentenceMatches.length > 1 ? sentenceMatches : [];
42
- }
@@ -1,4 +1,2 @@
1
1
  import { type Plugin } from '@opencode-ai/plugin';
2
- import type { ManagerRunRecord } from '../types/contracts.js';
3
2
  export declare const ClaudeManagerPlugin: Plugin;
4
- export declare function formatManagerRunToolResult(run: ManagerRunRecord): string;