opencode-orchestrator 1.2.11 → 1.2.13
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/core/agents/index.d.ts +1 -0
- package/dist/core/agents/manager/task-poller.d.ts +1 -0
- package/dist/core/cleanup/cleanup-scheduler.d.ts +11 -0
- package/dist/core/notification/task-toast-manager.d.ts +6 -0
- package/dist/core/sync/todo-parser.d.ts +5 -0
- package/dist/core/sync/todo-sync-service.d.ts +35 -0
- package/dist/index.js +345 -49
- package/package.json +1 -1
|
@@ -15,6 +15,7 @@ export declare class TaskPoller {
|
|
|
15
15
|
private pruneExpiredTasks;
|
|
16
16
|
private onTaskComplete?;
|
|
17
17
|
private pollingInterval?;
|
|
18
|
+
private messageCache;
|
|
18
19
|
constructor(client: OpencodeClient, store: TaskStore, concurrency: ConcurrencyController, notifyParentIfAllComplete: (parentSessionID: string) => Promise<void>, scheduleCleanup: (taskId: string) => void, pruneExpiredTasks: () => void, onTaskComplete?: ((task: ParallelTask) => void | Promise<void>) | undefined);
|
|
19
20
|
start(): void;
|
|
20
21
|
stop(): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare class CleanupScheduler {
|
|
2
|
+
private intervals;
|
|
3
|
+
private directory;
|
|
4
|
+
constructor(directory: string);
|
|
5
|
+
start(): void;
|
|
6
|
+
private schedule;
|
|
7
|
+
stop(): void;
|
|
8
|
+
compactWAL(): Promise<void>;
|
|
9
|
+
cleanDocs(): Promise<void>;
|
|
10
|
+
rotateHistory(): Promise<void>;
|
|
11
|
+
}
|
|
@@ -13,12 +13,14 @@
|
|
|
13
13
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
14
14
|
import type { ConcurrencyController } from "../agents/concurrency.js";
|
|
15
15
|
import { type TaskStatus, type TrackedTask, type TaskCompletionInfo } from "../../shared/index.js";
|
|
16
|
+
import type { TodoSyncService } from "../sync/todo-sync-service.js";
|
|
16
17
|
export type { TaskStatus, TrackedTask, TaskCompletionInfo } from "../../shared/index.js";
|
|
17
18
|
type OpencodeClient = PluginInput["client"];
|
|
18
19
|
export declare class TaskToastManager {
|
|
19
20
|
private tasks;
|
|
20
21
|
private client;
|
|
21
22
|
private concurrency;
|
|
23
|
+
private todoSync;
|
|
22
24
|
/**
|
|
23
25
|
* Initialize the manager with OpenCode client
|
|
24
26
|
*/
|
|
@@ -27,6 +29,10 @@ export declare class TaskToastManager {
|
|
|
27
29
|
* Set concurrency controller (can be set after init)
|
|
28
30
|
*/
|
|
29
31
|
setConcurrencyController(concurrency: ConcurrencyController): void;
|
|
32
|
+
/**
|
|
33
|
+
* Set TodoSyncService for TUI status synchronization
|
|
34
|
+
*/
|
|
35
|
+
setTodoSync(service: TodoSyncService): void;
|
|
30
36
|
/**
|
|
31
37
|
* Add a new task and show consolidated toast
|
|
32
38
|
*/
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
+
type OpencodeClient = PluginInput["client"];
|
|
3
|
+
interface TrackedTaskTodo {
|
|
4
|
+
id: string;
|
|
5
|
+
description: string;
|
|
6
|
+
status: string;
|
|
7
|
+
agent: string;
|
|
8
|
+
isBackground: boolean;
|
|
9
|
+
parentSessionID?: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class TodoSyncService {
|
|
12
|
+
private client;
|
|
13
|
+
private directory;
|
|
14
|
+
private todoPath;
|
|
15
|
+
private fileTodos;
|
|
16
|
+
private taskTodos;
|
|
17
|
+
private updateTimeout;
|
|
18
|
+
private watcher;
|
|
19
|
+
private activeSessions;
|
|
20
|
+
constructor(client: OpencodeClient, directory: string);
|
|
21
|
+
start(): Promise<void>;
|
|
22
|
+
registerSession(sessionID: string): void;
|
|
23
|
+
unregisterSession(sessionID: string): void;
|
|
24
|
+
private reloadFileTodos;
|
|
25
|
+
/**
|
|
26
|
+
* Called by TaskToastManager when tasks change
|
|
27
|
+
*/
|
|
28
|
+
updateTaskStatus(task: TrackedTaskTodo): void;
|
|
29
|
+
removeTask(taskId: string): void;
|
|
30
|
+
private broadcastUpdate;
|
|
31
|
+
private scheduleUpdate;
|
|
32
|
+
private sendTodosToSession;
|
|
33
|
+
stop(): void;
|
|
34
|
+
}
|
|
35
|
+
export {};
|
package/dist/index.js
CHANGED
|
@@ -5978,15 +5978,15 @@ function mergeDefs(...defs) {
|
|
|
5978
5978
|
function cloneDef(schema) {
|
|
5979
5979
|
return mergeDefs(schema._zod.def);
|
|
5980
5980
|
}
|
|
5981
|
-
function getElementAtPath(obj,
|
|
5982
|
-
if (!
|
|
5981
|
+
function getElementAtPath(obj, path12) {
|
|
5982
|
+
if (!path12)
|
|
5983
5983
|
return obj;
|
|
5984
|
-
return
|
|
5984
|
+
return path12.reduce((acc, key) => acc?.[key], obj);
|
|
5985
5985
|
}
|
|
5986
5986
|
function promiseAllObject(promisesObj) {
|
|
5987
5987
|
const keys = Object.keys(promisesObj);
|
|
5988
|
-
const
|
|
5989
|
-
return Promise.all(
|
|
5988
|
+
const promises4 = keys.map((key) => promisesObj[key]);
|
|
5989
|
+
return Promise.all(promises4).then((results) => {
|
|
5990
5990
|
const resolvedObj = {};
|
|
5991
5991
|
for (let i = 0; i < keys.length; i++) {
|
|
5992
5992
|
resolvedObj[keys[i]] = results[i];
|
|
@@ -6342,11 +6342,11 @@ function aborted(x, startIndex = 0) {
|
|
|
6342
6342
|
}
|
|
6343
6343
|
return false;
|
|
6344
6344
|
}
|
|
6345
|
-
function prefixIssues(
|
|
6345
|
+
function prefixIssues(path12, issues) {
|
|
6346
6346
|
return issues.map((iss) => {
|
|
6347
6347
|
var _a2;
|
|
6348
6348
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
6349
|
-
iss.path.unshift(
|
|
6349
|
+
iss.path.unshift(path12);
|
|
6350
6350
|
return iss;
|
|
6351
6351
|
});
|
|
6352
6352
|
}
|
|
@@ -6514,7 +6514,7 @@ function treeifyError(error92, _mapper) {
|
|
|
6514
6514
|
return issue3.message;
|
|
6515
6515
|
};
|
|
6516
6516
|
const result = { errors: [] };
|
|
6517
|
-
const processError = (error93,
|
|
6517
|
+
const processError = (error93, path12 = []) => {
|
|
6518
6518
|
var _a2, _b;
|
|
6519
6519
|
for (const issue3 of error93.issues) {
|
|
6520
6520
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
@@ -6524,7 +6524,7 @@ function treeifyError(error92, _mapper) {
|
|
|
6524
6524
|
} else if (issue3.code === "invalid_element") {
|
|
6525
6525
|
processError({ issues: issue3.issues }, issue3.path);
|
|
6526
6526
|
} else {
|
|
6527
|
-
const fullpath = [...
|
|
6527
|
+
const fullpath = [...path12, ...issue3.path];
|
|
6528
6528
|
if (fullpath.length === 0) {
|
|
6529
6529
|
result.errors.push(mapper(issue3));
|
|
6530
6530
|
continue;
|
|
@@ -6556,8 +6556,8 @@ function treeifyError(error92, _mapper) {
|
|
|
6556
6556
|
}
|
|
6557
6557
|
function toDotPath(_path) {
|
|
6558
6558
|
const segs = [];
|
|
6559
|
-
const
|
|
6560
|
-
for (const seg of
|
|
6559
|
+
const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
6560
|
+
for (const seg of path12) {
|
|
6561
6561
|
if (typeof seg === "number")
|
|
6562
6562
|
segs.push(`[${seg}]`);
|
|
6563
6563
|
else if (typeof seg === "symbol")
|
|
@@ -18829,6 +18829,7 @@ var TaskToastManager = class {
|
|
|
18829
18829
|
tasks = /* @__PURE__ */ new Map();
|
|
18830
18830
|
client = null;
|
|
18831
18831
|
concurrency = null;
|
|
18832
|
+
todoSync = null;
|
|
18832
18833
|
/**
|
|
18833
18834
|
* Initialize the manager with OpenCode client
|
|
18834
18835
|
*/
|
|
@@ -18842,6 +18843,12 @@ var TaskToastManager = class {
|
|
|
18842
18843
|
setConcurrencyController(concurrency) {
|
|
18843
18844
|
this.concurrency = concurrency;
|
|
18844
18845
|
}
|
|
18846
|
+
/**
|
|
18847
|
+
* Set TodoSyncService for TUI status synchronization
|
|
18848
|
+
*/
|
|
18849
|
+
setTodoSync(service) {
|
|
18850
|
+
this.todoSync = service;
|
|
18851
|
+
}
|
|
18845
18852
|
/**
|
|
18846
18853
|
* Add a new task and show consolidated toast
|
|
18847
18854
|
*/
|
|
@@ -18857,6 +18864,7 @@ var TaskToastManager = class {
|
|
|
18857
18864
|
sessionID: task.sessionID
|
|
18858
18865
|
};
|
|
18859
18866
|
this.tasks.set(task.id, trackedTask);
|
|
18867
|
+
this.todoSync?.updateTaskStatus(trackedTask);
|
|
18860
18868
|
this.showTaskListToast(trackedTask);
|
|
18861
18869
|
}
|
|
18862
18870
|
/**
|
|
@@ -18866,6 +18874,7 @@ var TaskToastManager = class {
|
|
|
18866
18874
|
const task = this.tasks.get(id);
|
|
18867
18875
|
if (task) {
|
|
18868
18876
|
task.status = status;
|
|
18877
|
+
this.todoSync?.updateTaskStatus(task);
|
|
18869
18878
|
}
|
|
18870
18879
|
}
|
|
18871
18880
|
/**
|
|
@@ -18873,6 +18882,7 @@ var TaskToastManager = class {
|
|
|
18873
18882
|
*/
|
|
18874
18883
|
removeTask(id) {
|
|
18875
18884
|
this.tasks.delete(id);
|
|
18885
|
+
this.todoSync?.removeTask(id);
|
|
18876
18886
|
}
|
|
18877
18887
|
/**
|
|
18878
18888
|
* Get all running tasks (newest first)
|
|
@@ -20215,15 +20225,15 @@ function mergeDefs2(...defs) {
|
|
|
20215
20225
|
function cloneDef2(schema) {
|
|
20216
20226
|
return mergeDefs2(schema._zod.def);
|
|
20217
20227
|
}
|
|
20218
|
-
function getElementAtPath2(obj,
|
|
20219
|
-
if (!
|
|
20228
|
+
function getElementAtPath2(obj, path12) {
|
|
20229
|
+
if (!path12)
|
|
20220
20230
|
return obj;
|
|
20221
|
-
return
|
|
20231
|
+
return path12.reduce((acc, key) => acc?.[key], obj);
|
|
20222
20232
|
}
|
|
20223
20233
|
function promiseAllObject2(promisesObj) {
|
|
20224
20234
|
const keys = Object.keys(promisesObj);
|
|
20225
|
-
const
|
|
20226
|
-
return Promise.all(
|
|
20235
|
+
const promises4 = keys.map((key) => promisesObj[key]);
|
|
20236
|
+
return Promise.all(promises4).then((results) => {
|
|
20227
20237
|
const resolvedObj = {};
|
|
20228
20238
|
for (let i = 0; i < keys.length; i++) {
|
|
20229
20239
|
resolvedObj[keys[i]] = results[i];
|
|
@@ -20601,11 +20611,11 @@ function aborted2(x, startIndex = 0) {
|
|
|
20601
20611
|
}
|
|
20602
20612
|
return false;
|
|
20603
20613
|
}
|
|
20604
|
-
function prefixIssues2(
|
|
20614
|
+
function prefixIssues2(path12, issues) {
|
|
20605
20615
|
return issues.map((iss) => {
|
|
20606
20616
|
var _a2;
|
|
20607
20617
|
(_a2 = iss).path ?? (_a2.path = []);
|
|
20608
|
-
iss.path.unshift(
|
|
20618
|
+
iss.path.unshift(path12);
|
|
20609
20619
|
return iss;
|
|
20610
20620
|
});
|
|
20611
20621
|
}
|
|
@@ -20788,7 +20798,7 @@ function formatError2(error92, mapper = (issue3) => issue3.message) {
|
|
|
20788
20798
|
}
|
|
20789
20799
|
function treeifyError2(error92, mapper = (issue3) => issue3.message) {
|
|
20790
20800
|
const result = { errors: [] };
|
|
20791
|
-
const processError = (error93,
|
|
20801
|
+
const processError = (error93, path12 = []) => {
|
|
20792
20802
|
var _a2, _b;
|
|
20793
20803
|
for (const issue3 of error93.issues) {
|
|
20794
20804
|
if (issue3.code === "invalid_union" && issue3.errors.length) {
|
|
@@ -20798,7 +20808,7 @@ function treeifyError2(error92, mapper = (issue3) => issue3.message) {
|
|
|
20798
20808
|
} else if (issue3.code === "invalid_element") {
|
|
20799
20809
|
processError({ issues: issue3.issues }, issue3.path);
|
|
20800
20810
|
} else {
|
|
20801
|
-
const fullpath = [...
|
|
20811
|
+
const fullpath = [...path12, ...issue3.path];
|
|
20802
20812
|
if (fullpath.length === 0) {
|
|
20803
20813
|
result.errors.push(mapper(issue3));
|
|
20804
20814
|
continue;
|
|
@@ -20830,8 +20840,8 @@ function treeifyError2(error92, mapper = (issue3) => issue3.message) {
|
|
|
20830
20840
|
}
|
|
20831
20841
|
function toDotPath2(_path) {
|
|
20832
20842
|
const segs = [];
|
|
20833
|
-
const
|
|
20834
|
-
for (const seg of
|
|
20843
|
+
const path12 = _path.map((seg) => typeof seg === "object" ? seg.key : seg);
|
|
20844
|
+
for (const seg of path12) {
|
|
20835
20845
|
if (typeof seg === "number")
|
|
20836
20846
|
segs.push(`[${seg}]`);
|
|
20837
20847
|
else if (typeof seg === "symbol")
|
|
@@ -32808,13 +32818,13 @@ function resolveRef(ref, ctx) {
|
|
|
32808
32818
|
if (!ref.startsWith("#")) {
|
|
32809
32819
|
throw new Error("External $ref is not supported, only local refs (#/...) are allowed");
|
|
32810
32820
|
}
|
|
32811
|
-
const
|
|
32812
|
-
if (
|
|
32821
|
+
const path12 = ref.slice(1).split("/").filter(Boolean);
|
|
32822
|
+
if (path12.length === 0) {
|
|
32813
32823
|
return ctx.rootSchema;
|
|
32814
32824
|
}
|
|
32815
32825
|
const defsKey = ctx.version === "draft-2020-12" ? "$defs" : "definitions";
|
|
32816
|
-
if (
|
|
32817
|
-
const key =
|
|
32826
|
+
if (path12[0] === defsKey) {
|
|
32827
|
+
const key = path12[1];
|
|
32818
32828
|
if (!key || !ctx.defs[key]) {
|
|
32819
32829
|
throw new Error(`Reference not found: ${ref}`);
|
|
32820
32830
|
}
|
|
@@ -33625,6 +33635,7 @@ var TaskPoller = class {
|
|
|
33625
33635
|
this.onTaskComplete = onTaskComplete;
|
|
33626
33636
|
}
|
|
33627
33637
|
pollingInterval;
|
|
33638
|
+
messageCache = /* @__PURE__ */ new Map();
|
|
33628
33639
|
start() {
|
|
33629
33640
|
if (this.pollingInterval) return;
|
|
33630
33641
|
log("[task-poller.ts] start() - polling started");
|
|
@@ -33717,7 +33728,16 @@ var TaskPoller = class {
|
|
|
33717
33728
|
}
|
|
33718
33729
|
async updateTaskProgress(task) {
|
|
33719
33730
|
try {
|
|
33731
|
+
const cached3 = this.messageCache.get(task.sessionID);
|
|
33732
|
+
const statusResult = await this.client.session.status();
|
|
33733
|
+
const sessionInfo = statusResult.data?.[task.sessionID];
|
|
33734
|
+
const currentMsgCount = sessionInfo?.messageCount ?? 0;
|
|
33735
|
+
if (cached3 && cached3.count === currentMsgCount) {
|
|
33736
|
+
task.stablePolls = (task.stablePolls ?? 0) + 1;
|
|
33737
|
+
return;
|
|
33738
|
+
}
|
|
33720
33739
|
const result = await this.client.session.messages({ path: { id: task.sessionID } });
|
|
33740
|
+
this.messageCache.set(task.sessionID, { count: currentMsgCount, lastChecked: /* @__PURE__ */ new Date() });
|
|
33721
33741
|
if (result.error) return;
|
|
33722
33742
|
const messages = result.data ?? [];
|
|
33723
33743
|
const assistantMsgs = messages.filter((m) => m.info?.role === MESSAGE_ROLES.ASSISTANT);
|
|
@@ -33741,9 +33761,8 @@ var TaskPoller = class {
|
|
|
33741
33761
|
lastMessage: lastMessage?.slice(0, 100),
|
|
33742
33762
|
lastUpdate: /* @__PURE__ */ new Date()
|
|
33743
33763
|
};
|
|
33744
|
-
const currentMsgCount = messages.length;
|
|
33745
33764
|
if (task.lastMsgCount === currentMsgCount) {
|
|
33746
|
-
task.stablePolls =
|
|
33765
|
+
task.stablePolls = 0;
|
|
33747
33766
|
} else {
|
|
33748
33767
|
task.stablePolls = 0;
|
|
33749
33768
|
}
|
|
@@ -37135,10 +37154,10 @@ async function resolveCommandPath(key, commandName) {
|
|
|
37135
37154
|
const currentPending = pending.get(key);
|
|
37136
37155
|
if (currentPending) return currentPending;
|
|
37137
37156
|
const promise3 = (async () => {
|
|
37138
|
-
const
|
|
37139
|
-
cache[key] =
|
|
37157
|
+
const path12 = await findCommand(commandName);
|
|
37158
|
+
cache[key] = path12;
|
|
37140
37159
|
pending.delete(key);
|
|
37141
|
-
return
|
|
37160
|
+
return path12;
|
|
37142
37161
|
})();
|
|
37143
37162
|
pending.set(key, promise3);
|
|
37144
37163
|
return promise3;
|
|
@@ -37147,21 +37166,21 @@ async function resolveCommandPath(key, commandName) {
|
|
|
37147
37166
|
// src/core/notification/os-notify/notifier.ts
|
|
37148
37167
|
var execAsync2 = promisify2(exec2);
|
|
37149
37168
|
async function notifyDarwin(title, message) {
|
|
37150
|
-
const
|
|
37169
|
+
const path12 = await resolveCommandPath(
|
|
37151
37170
|
NOTIFICATION_COMMAND_KEYS.OSASCRIPT,
|
|
37152
37171
|
NOTIFICATION_COMMANDS.OSASCRIPT
|
|
37153
37172
|
);
|
|
37154
|
-
if (!
|
|
37173
|
+
if (!path12) return;
|
|
37155
37174
|
const escT = title.replace(/"/g, '\\"');
|
|
37156
37175
|
const escM = message.replace(/"/g, '\\"');
|
|
37157
|
-
await execAsync2(`${
|
|
37176
|
+
await execAsync2(`${path12} -e 'display notification "${escM}" with title "${escT}" sound name "Glass"'`);
|
|
37158
37177
|
}
|
|
37159
37178
|
async function notifyLinux(title, message) {
|
|
37160
|
-
const
|
|
37179
|
+
const path12 = await resolveCommandPath(
|
|
37161
37180
|
NOTIFICATION_COMMAND_KEYS.NOTIFY_SEND,
|
|
37162
37181
|
NOTIFICATION_COMMANDS.NOTIFY_SEND
|
|
37163
37182
|
);
|
|
37164
|
-
if (
|
|
37183
|
+
if (path12) await execAsync2(`${path12} "${title}" "${message}" 2>/dev/null`);
|
|
37165
37184
|
}
|
|
37166
37185
|
async function notifyWindows(title, message) {
|
|
37167
37186
|
const ps = await resolveCommandPath(
|
|
@@ -37208,11 +37227,11 @@ init_os();
|
|
|
37208
37227
|
async function playDarwin(soundPath) {
|
|
37209
37228
|
if (!soundPath) return;
|
|
37210
37229
|
try {
|
|
37211
|
-
const
|
|
37230
|
+
const path12 = await resolveCommandPath(
|
|
37212
37231
|
NOTIFICATION_COMMAND_KEYS.AFPLAY,
|
|
37213
37232
|
NOTIFICATION_COMMANDS.AFPLAY
|
|
37214
37233
|
);
|
|
37215
|
-
if (
|
|
37234
|
+
if (path12) exec3(`"${path12}" "${soundPath}"`);
|
|
37216
37235
|
} catch (err) {
|
|
37217
37236
|
log(`[session-notify] Error playing sound (Darwin): ${err}`);
|
|
37218
37237
|
}
|
|
@@ -38364,6 +38383,265 @@ var PluginManager = class _PluginManager {
|
|
|
38364
38383
|
}
|
|
38365
38384
|
};
|
|
38366
38385
|
|
|
38386
|
+
// src/core/sync/todo-sync-service.ts
|
|
38387
|
+
init_shared();
|
|
38388
|
+
import * as fs10 from "node:fs";
|
|
38389
|
+
import * as path9 from "node:path";
|
|
38390
|
+
|
|
38391
|
+
// src/core/sync/todo-parser.ts
|
|
38392
|
+
function parseTodoMd(content) {
|
|
38393
|
+
const lines = content.split("\n");
|
|
38394
|
+
const todos = [];
|
|
38395
|
+
const generateId = (text, index2) => {
|
|
38396
|
+
return `file-task-${index2}-${text.substring(0, 10).replace(/[^a-zA-Z0-9]/g, "")}`;
|
|
38397
|
+
};
|
|
38398
|
+
let index = 0;
|
|
38399
|
+
for (const line of lines) {
|
|
38400
|
+
const match = line.match(/^\s*-\s*\[([ xX\/\-\.])\]\s*(.+)$/);
|
|
38401
|
+
if (match) {
|
|
38402
|
+
const [, statusChar, text] = match;
|
|
38403
|
+
const content2 = text.trim();
|
|
38404
|
+
let status = "pending";
|
|
38405
|
+
switch (statusChar.toLowerCase()) {
|
|
38406
|
+
case "x":
|
|
38407
|
+
status = "completed";
|
|
38408
|
+
break;
|
|
38409
|
+
case "/":
|
|
38410
|
+
case ".":
|
|
38411
|
+
status = "in_progress";
|
|
38412
|
+
break;
|
|
38413
|
+
case "-":
|
|
38414
|
+
status = "cancelled";
|
|
38415
|
+
break;
|
|
38416
|
+
case " ":
|
|
38417
|
+
default:
|
|
38418
|
+
status = "pending";
|
|
38419
|
+
break;
|
|
38420
|
+
}
|
|
38421
|
+
todos.push({
|
|
38422
|
+
id: generateId(content2, index),
|
|
38423
|
+
content: content2,
|
|
38424
|
+
status,
|
|
38425
|
+
priority: "medium",
|
|
38426
|
+
// Default priority for file items
|
|
38427
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
38428
|
+
});
|
|
38429
|
+
index++;
|
|
38430
|
+
}
|
|
38431
|
+
}
|
|
38432
|
+
return todos;
|
|
38433
|
+
}
|
|
38434
|
+
|
|
38435
|
+
// src/core/sync/todo-sync-service.ts
|
|
38436
|
+
var TodoSyncService = class {
|
|
38437
|
+
client;
|
|
38438
|
+
directory;
|
|
38439
|
+
todoPath;
|
|
38440
|
+
fileTodos = [];
|
|
38441
|
+
taskTodos = /* @__PURE__ */ new Map();
|
|
38442
|
+
updateTimeout = null;
|
|
38443
|
+
watcher = null;
|
|
38444
|
+
// We only want to sync to the "primary" session or all sessions?
|
|
38445
|
+
// The design says `syncTaskStore(sessionID)`.
|
|
38446
|
+
// Usually TUI TODO is per session.
|
|
38447
|
+
// However, `todo.md` is global (project level).
|
|
38448
|
+
// So we should probably broadcast to active sessions or just the one associated with the tasks?
|
|
38449
|
+
// Current TUI limitation: we might need to know which session to update.
|
|
38450
|
+
// For TUI sidebar, we usually update the session the user is looking at.
|
|
38451
|
+
// But we don't know that.
|
|
38452
|
+
// We will maintain a set of "active sessions" provided by index.ts or just update relevant ones.
|
|
38453
|
+
// For Phase 1, we might just update the sessions we know about (parents of tasks) or register sessions.
|
|
38454
|
+
activeSessions = /* @__PURE__ */ new Set();
|
|
38455
|
+
constructor(client, directory) {
|
|
38456
|
+
this.client = client;
|
|
38457
|
+
this.directory = directory;
|
|
38458
|
+
this.todoPath = path9.join(this.directory, PATHS.TODO);
|
|
38459
|
+
}
|
|
38460
|
+
async start() {
|
|
38461
|
+
await this.reloadFileTodos();
|
|
38462
|
+
if (fs10.existsSync(this.todoPath)) {
|
|
38463
|
+
let timer;
|
|
38464
|
+
this.watcher = fs10.watch(this.todoPath, (eventType) => {
|
|
38465
|
+
if (eventType === "change" || eventType === "rename") {
|
|
38466
|
+
clearTimeout(timer);
|
|
38467
|
+
timer = setTimeout(() => {
|
|
38468
|
+
this.reloadFileTodos().catch((err) => log(`[TodoSync] Error reloading: ${err}`));
|
|
38469
|
+
}, 500);
|
|
38470
|
+
}
|
|
38471
|
+
});
|
|
38472
|
+
}
|
|
38473
|
+
}
|
|
38474
|
+
registerSession(sessionID) {
|
|
38475
|
+
this.activeSessions.add(sessionID);
|
|
38476
|
+
this.scheduleUpdate(sessionID);
|
|
38477
|
+
}
|
|
38478
|
+
unregisterSession(sessionID) {
|
|
38479
|
+
this.activeSessions.delete(sessionID);
|
|
38480
|
+
}
|
|
38481
|
+
async reloadFileTodos() {
|
|
38482
|
+
try {
|
|
38483
|
+
if (fs10.existsSync(this.todoPath)) {
|
|
38484
|
+
const content = await fs10.promises.readFile(this.todoPath, "utf-8");
|
|
38485
|
+
this.fileTodos = parseTodoMd(content);
|
|
38486
|
+
this.broadcastUpdate();
|
|
38487
|
+
}
|
|
38488
|
+
} catch (error92) {
|
|
38489
|
+
log(`[TodoSync] Failed to read todo.md: ${error92}`);
|
|
38490
|
+
}
|
|
38491
|
+
}
|
|
38492
|
+
/**
|
|
38493
|
+
* Called by TaskToastManager when tasks change
|
|
38494
|
+
*/
|
|
38495
|
+
updateTaskStatus(task) {
|
|
38496
|
+
this.taskTodos.set(task.id, task);
|
|
38497
|
+
if (task.parentSessionID) {
|
|
38498
|
+
this.scheduleUpdate(task.parentSessionID);
|
|
38499
|
+
} else {
|
|
38500
|
+
this.broadcastUpdate();
|
|
38501
|
+
}
|
|
38502
|
+
}
|
|
38503
|
+
removeTask(taskId) {
|
|
38504
|
+
const task = this.taskTodos.get(taskId);
|
|
38505
|
+
if (task) {
|
|
38506
|
+
this.taskTodos.delete(taskId);
|
|
38507
|
+
if (task.parentSessionID) {
|
|
38508
|
+
this.scheduleUpdate(task.parentSessionID);
|
|
38509
|
+
} else {
|
|
38510
|
+
this.broadcastUpdate();
|
|
38511
|
+
}
|
|
38512
|
+
}
|
|
38513
|
+
}
|
|
38514
|
+
broadcastUpdate() {
|
|
38515
|
+
for (const sessionID of this.activeSessions) {
|
|
38516
|
+
this.scheduleUpdate(sessionID);
|
|
38517
|
+
}
|
|
38518
|
+
}
|
|
38519
|
+
scheduleUpdate(sessionID) {
|
|
38520
|
+
this.sendTodosToSession(sessionID).catch((err) => {
|
|
38521
|
+
});
|
|
38522
|
+
}
|
|
38523
|
+
async sendTodosToSession(sessionID) {
|
|
38524
|
+
const taskTodosList = Array.from(this.taskTodos.values()).map((t) => {
|
|
38525
|
+
let status = "pending";
|
|
38526
|
+
const s = t.status.toLowerCase();
|
|
38527
|
+
if (s.includes("run") || s.includes("wait") || s.includes("que")) status = "in_progress";
|
|
38528
|
+
else if (s.includes("complete") || s.includes("done")) status = "completed";
|
|
38529
|
+
else if (s.includes("fail") || s.includes("error")) status = "cancelled";
|
|
38530
|
+
else if (s.includes("cancel")) status = "cancelled";
|
|
38531
|
+
return {
|
|
38532
|
+
id: `task-${t.id}`,
|
|
38533
|
+
// Prefix to avoid collision
|
|
38534
|
+
content: `[${t.agent.toUpperCase()}] ${t.description}`,
|
|
38535
|
+
status,
|
|
38536
|
+
priority: t.isBackground ? "low" : "high",
|
|
38537
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
38538
|
+
};
|
|
38539
|
+
});
|
|
38540
|
+
const merged = [
|
|
38541
|
+
...this.fileTodos,
|
|
38542
|
+
...taskTodosList
|
|
38543
|
+
];
|
|
38544
|
+
try {
|
|
38545
|
+
await this.client.session.todo({
|
|
38546
|
+
path: { id: sessionID },
|
|
38547
|
+
// Standardize to id
|
|
38548
|
+
body: { todos: merged }
|
|
38549
|
+
});
|
|
38550
|
+
} catch (error92) {
|
|
38551
|
+
}
|
|
38552
|
+
}
|
|
38553
|
+
stop() {
|
|
38554
|
+
if (this.watcher) {
|
|
38555
|
+
this.watcher.close();
|
|
38556
|
+
}
|
|
38557
|
+
}
|
|
38558
|
+
};
|
|
38559
|
+
|
|
38560
|
+
// src/core/cleanup/cleanup-scheduler.ts
|
|
38561
|
+
import * as fs11 from "node:fs";
|
|
38562
|
+
import * as path10 from "node:path";
|
|
38563
|
+
init_shared();
|
|
38564
|
+
var CleanupScheduler = class {
|
|
38565
|
+
intervals = /* @__PURE__ */ new Map();
|
|
38566
|
+
directory;
|
|
38567
|
+
constructor(directory) {
|
|
38568
|
+
this.directory = directory;
|
|
38569
|
+
}
|
|
38570
|
+
start() {
|
|
38571
|
+
this.schedule("wal-compact", () => this.compactWAL(), 10 * 60 * 1e3);
|
|
38572
|
+
this.schedule("docs-clean", () => this.cleanDocs(), 60 * 60 * 1e3);
|
|
38573
|
+
this.schedule("history-rotate", () => this.rotateHistory(), 24 * 60 * 60 * 1e3);
|
|
38574
|
+
log(`[Cleanup] Scheduler started`);
|
|
38575
|
+
}
|
|
38576
|
+
schedule(name, fn, intervalMs) {
|
|
38577
|
+
const timer = setInterval(() => {
|
|
38578
|
+
fn().catch((err) => log(`[Cleanup] ${name} failed:`, err));
|
|
38579
|
+
}, intervalMs);
|
|
38580
|
+
if (timer.unref) timer.unref();
|
|
38581
|
+
this.intervals.set(name, timer);
|
|
38582
|
+
}
|
|
38583
|
+
stop() {
|
|
38584
|
+
for (const timer of this.intervals.values()) {
|
|
38585
|
+
clearInterval(timer);
|
|
38586
|
+
}
|
|
38587
|
+
this.intervals.clear();
|
|
38588
|
+
log(`[Cleanup] Scheduler stopped`);
|
|
38589
|
+
}
|
|
38590
|
+
async compactWAL() {
|
|
38591
|
+
try {
|
|
38592
|
+
const manager = parallelAgentManager.getInstance();
|
|
38593
|
+
const activeTasks = manager.getAllTasks().filter((t) => t.status === TASK_STATUS.RUNNING || t.status === TASK_STATUS.PENDING);
|
|
38594
|
+
await taskWAL.compact(activeTasks);
|
|
38595
|
+
} catch (error92) {
|
|
38596
|
+
log(`[Cleanup] Failed to compact WAL: ${error92}`);
|
|
38597
|
+
}
|
|
38598
|
+
}
|
|
38599
|
+
async cleanDocs() {
|
|
38600
|
+
try {
|
|
38601
|
+
const stats2 = await stats();
|
|
38602
|
+
if (stats2.totalSize > 10 * 1024 * 1024) {
|
|
38603
|
+
const allDocs = await list();
|
|
38604
|
+
allDocs.sort((a, b) => new Date(a.fetchedAt).getTime() - new Date(b.fetchedAt).getTime());
|
|
38605
|
+
const toDelete = allDocs.slice(0, Math.floor(allDocs.length / 2));
|
|
38606
|
+
for (const doc of toDelete) {
|
|
38607
|
+
await remove(doc.url);
|
|
38608
|
+
}
|
|
38609
|
+
log(`[Cleanup] Pruned ${toDelete.length} documents due to size limit`);
|
|
38610
|
+
}
|
|
38611
|
+
} catch (error92) {
|
|
38612
|
+
}
|
|
38613
|
+
}
|
|
38614
|
+
async rotateHistory() {
|
|
38615
|
+
try {
|
|
38616
|
+
const historyPath = path10.join(this.directory, ".opencode/archive/todo_history.jsonl");
|
|
38617
|
+
if (!fs11.existsSync(historyPath)) return;
|
|
38618
|
+
const stat = await fs11.promises.stat(historyPath);
|
|
38619
|
+
if (stat.size === 0) return;
|
|
38620
|
+
const dateStr = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
38621
|
+
const archivePath = path10.join(
|
|
38622
|
+
this.directory,
|
|
38623
|
+
`.opencode/archive/todo_history.${dateStr}.jsonl`
|
|
38624
|
+
);
|
|
38625
|
+
await fs11.promises.rename(historyPath, archivePath);
|
|
38626
|
+
await fs11.promises.writeFile(historyPath, "");
|
|
38627
|
+
const archiveDir = path10.dirname(historyPath);
|
|
38628
|
+
const files = await fs11.promises.readdir(archiveDir);
|
|
38629
|
+
const cutoff = Date.now() - 30 * 24 * 60 * 60 * 1e3;
|
|
38630
|
+
for (const file3 of files) {
|
|
38631
|
+
if (file3.startsWith("todo_history.") && file3.endsWith(".jsonl")) {
|
|
38632
|
+
const filePath = path10.join(archiveDir, file3);
|
|
38633
|
+
const fStat = await fs11.promises.stat(filePath);
|
|
38634
|
+
if (fStat.mtimeMs < cutoff) {
|
|
38635
|
+
await fs11.promises.unlink(filePath);
|
|
38636
|
+
}
|
|
38637
|
+
}
|
|
38638
|
+
}
|
|
38639
|
+
} catch (error92) {
|
|
38640
|
+
log(`[Cleanup] History rotation error: ${error92}`);
|
|
38641
|
+
}
|
|
38642
|
+
}
|
|
38643
|
+
};
|
|
38644
|
+
|
|
38367
38645
|
// src/plugin-handlers/tool-execute-pre-handler.ts
|
|
38368
38646
|
function createToolExecuteBeforeHandler(ctx) {
|
|
38369
38647
|
const { sessions, directory } = ctx;
|
|
@@ -38427,17 +38705,17 @@ function createChatMessageHandler(ctx) {
|
|
|
38427
38705
|
init_shared();
|
|
38428
38706
|
|
|
38429
38707
|
// src/utils/compatibility/claude.ts
|
|
38430
|
-
import
|
|
38431
|
-
import
|
|
38708
|
+
import fs12 from "fs";
|
|
38709
|
+
import path11 from "path";
|
|
38432
38710
|
function findClaudeRules(startDir = process.cwd()) {
|
|
38433
38711
|
try {
|
|
38434
38712
|
let currentDir = startDir;
|
|
38435
|
-
const root =
|
|
38713
|
+
const root = path11.parse(startDir).root;
|
|
38436
38714
|
while (true) {
|
|
38437
|
-
const claudeMdPath =
|
|
38438
|
-
if (
|
|
38715
|
+
const claudeMdPath = path11.join(currentDir, "CLAUDE.md");
|
|
38716
|
+
if (fs12.existsSync(claudeMdPath)) {
|
|
38439
38717
|
try {
|
|
38440
|
-
const content =
|
|
38718
|
+
const content = fs12.readFileSync(claudeMdPath, "utf-8");
|
|
38441
38719
|
log(`[compatibility] Loaded CLAUDE.md from ${claudeMdPath}`);
|
|
38442
38720
|
return formatRules("CLAUDE.md", content);
|
|
38443
38721
|
} catch (e) {
|
|
@@ -38445,11 +38723,11 @@ function findClaudeRules(startDir = process.cwd()) {
|
|
|
38445
38723
|
}
|
|
38446
38724
|
}
|
|
38447
38725
|
if (currentDir === root) break;
|
|
38448
|
-
currentDir =
|
|
38726
|
+
currentDir = path11.dirname(currentDir);
|
|
38449
38727
|
}
|
|
38450
|
-
const copilotPath =
|
|
38451
|
-
if (
|
|
38452
|
-
return formatRules("Copilot Instructions",
|
|
38728
|
+
const copilotPath = path11.join(startDir, ".github", "copilot-instructions.md");
|
|
38729
|
+
if (fs12.existsSync(copilotPath)) {
|
|
38730
|
+
return formatRules("Copilot Instructions", fs12.readFileSync(copilotPath, "utf-8"));
|
|
38453
38731
|
}
|
|
38454
38732
|
return null;
|
|
38455
38733
|
} catch (error92) {
|
|
@@ -39054,6 +39332,11 @@ var OrchestratorPlugin = async (input) => {
|
|
|
39054
39332
|
await pluginManager.initialize(directory);
|
|
39055
39333
|
const dynamicTools = pluginManager.getDynamicTools();
|
|
39056
39334
|
taskToastManager.setConcurrencyController(parallelAgentManager2.getConcurrency());
|
|
39335
|
+
const todoSync = new TodoSyncService(client, directory);
|
|
39336
|
+
await todoSync.start();
|
|
39337
|
+
taskToastManager.setTodoSync(todoSync);
|
|
39338
|
+
const cleanupScheduler = new CleanupScheduler(directory);
|
|
39339
|
+
cleanupScheduler.start();
|
|
39057
39340
|
const handlerContext = {
|
|
39058
39341
|
client,
|
|
39059
39342
|
directory,
|
|
@@ -39110,7 +39393,20 @@ var OrchestratorPlugin = async (input) => {
|
|
|
39110
39393
|
// -----------------------------------------------------------------
|
|
39111
39394
|
// Event hook - handles OpenCode events
|
|
39112
39395
|
// -----------------------------------------------------------------
|
|
39113
|
-
|
|
39396
|
+
// -----------------------------------------------------------------
|
|
39397
|
+
// Event hook - handles OpenCode events
|
|
39398
|
+
// -----------------------------------------------------------------
|
|
39399
|
+
event: async (payload) => {
|
|
39400
|
+
const result = await createEventHandler(handlerContext)(payload);
|
|
39401
|
+
const { event } = payload;
|
|
39402
|
+
if (event.type === "session.created" && event.properties) {
|
|
39403
|
+
const sessionID = event.properties.sessionID || event.properties.id || event.properties.info?.sessionID;
|
|
39404
|
+
if (sessionID) {
|
|
39405
|
+
todoSync.registerSession(sessionID);
|
|
39406
|
+
}
|
|
39407
|
+
}
|
|
39408
|
+
return result;
|
|
39409
|
+
},
|
|
39114
39410
|
// -----------------------------------------------------------------
|
|
39115
39411
|
// chat.message hook - intercepts commands and sets up sessions
|
|
39116
39412
|
// -----------------------------------------------------------------
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "opencode-orchestrator",
|
|
3
3
|
"displayName": "OpenCode Orchestrator",
|
|
4
4
|
"description": "Distributed Cognitive Architecture for OpenCode. Turns simple prompts into specialized multi-agent workflows (Planner, Coder, Reviewer).",
|
|
5
|
-
"version": "1.2.
|
|
5
|
+
"version": "1.2.13",
|
|
6
6
|
"author": "agnusdei1207",
|
|
7
7
|
"license": "MIT",
|
|
8
8
|
"repository": {
|