opencode-task 0.1.1 → 0.1.3
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.js +436 -5
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -12329,22 +12329,453 @@ function tool(input) {
|
|
|
12329
12329
|
return input;
|
|
12330
12330
|
}
|
|
12331
12331
|
tool.schema = exports_external;
|
|
12332
|
+
// utils.ts
|
|
12333
|
+
import { appendFile } from "node:fs/promises";
|
|
12334
|
+
class Semaphore {
|
|
12335
|
+
maxConcurrent;
|
|
12336
|
+
permits;
|
|
12337
|
+
waiting = [];
|
|
12338
|
+
acquired = 0;
|
|
12339
|
+
draining = false;
|
|
12340
|
+
constructor(maxConcurrent) {
|
|
12341
|
+
this.maxConcurrent = maxConcurrent;
|
|
12342
|
+
this.permits = maxConcurrent;
|
|
12343
|
+
}
|
|
12344
|
+
async acquire() {
|
|
12345
|
+
if (this.draining)
|
|
12346
|
+
throw new Error("Semaphore is draining");
|
|
12347
|
+
if (this.permits > 0) {
|
|
12348
|
+
this.permits--;
|
|
12349
|
+
this.acquired++;
|
|
12350
|
+
return;
|
|
12351
|
+
}
|
|
12352
|
+
return new Promise((resolve, reject) => {
|
|
12353
|
+
this.waiting.push({ resolve, reject });
|
|
12354
|
+
});
|
|
12355
|
+
}
|
|
12356
|
+
release() {
|
|
12357
|
+
this.acquired--;
|
|
12358
|
+
const next = this.waiting.shift();
|
|
12359
|
+
if (next) {
|
|
12360
|
+
this.acquired++;
|
|
12361
|
+
next.resolve();
|
|
12362
|
+
} else {
|
|
12363
|
+
this.permits++;
|
|
12364
|
+
}
|
|
12365
|
+
}
|
|
12366
|
+
drain() {
|
|
12367
|
+
this.draining = true;
|
|
12368
|
+
for (const waiter of this.waiting) {
|
|
12369
|
+
waiter.reject(new Error("Semaphore drained"));
|
|
12370
|
+
}
|
|
12371
|
+
this.waiting = [];
|
|
12372
|
+
}
|
|
12373
|
+
get activeCount() {
|
|
12374
|
+
return this.acquired;
|
|
12375
|
+
}
|
|
12376
|
+
get waitingCount() {
|
|
12377
|
+
return this.waiting.length;
|
|
12378
|
+
}
|
|
12379
|
+
}
|
|
12380
|
+
function createTask(id, command, cwd, timeout, sessionID, metadata) {
|
|
12381
|
+
let resolveCompletion;
|
|
12382
|
+
const completionPromise = new Promise((r) => {
|
|
12383
|
+
resolveCompletion = r;
|
|
12384
|
+
});
|
|
12385
|
+
return {
|
|
12386
|
+
id,
|
|
12387
|
+
sessionID,
|
|
12388
|
+
command,
|
|
12389
|
+
cwd,
|
|
12390
|
+
status: "pending",
|
|
12391
|
+
stdout: `/tmp/${id}-stdout`,
|
|
12392
|
+
stderr: `/tmp/${id}-stderr`,
|
|
12393
|
+
metadata,
|
|
12394
|
+
createdAt: Date.now(),
|
|
12395
|
+
timeout,
|
|
12396
|
+
abortController: new AbortController,
|
|
12397
|
+
completionPromise,
|
|
12398
|
+
resolveCompletion
|
|
12399
|
+
};
|
|
12400
|
+
}
|
|
12401
|
+
function matchMetadata(taskMeta, filter) {
|
|
12402
|
+
if (!taskMeta)
|
|
12403
|
+
return false;
|
|
12404
|
+
for (const [key, value] of Object.entries(filter)) {
|
|
12405
|
+
if (taskMeta[key] !== value)
|
|
12406
|
+
return false;
|
|
12407
|
+
}
|
|
12408
|
+
return true;
|
|
12409
|
+
}
|
|
12410
|
+
|
|
12411
|
+
class TaskHistory {
|
|
12412
|
+
capacity;
|
|
12413
|
+
buffer;
|
|
12414
|
+
head = 0;
|
|
12415
|
+
count = 0;
|
|
12416
|
+
constructor(capacity) {
|
|
12417
|
+
this.capacity = capacity;
|
|
12418
|
+
this.buffer = new Array(capacity);
|
|
12419
|
+
}
|
|
12420
|
+
push(entry) {
|
|
12421
|
+
this.buffer[this.head] = entry;
|
|
12422
|
+
this.head = (this.head + 1) % this.capacity;
|
|
12423
|
+
if (this.count < this.capacity)
|
|
12424
|
+
this.count++;
|
|
12425
|
+
}
|
|
12426
|
+
entries(statusFilter, metadataFilter) {
|
|
12427
|
+
const result = [];
|
|
12428
|
+
const start = this.count < this.capacity ? 0 : this.head;
|
|
12429
|
+
for (let i = 0;i < this.count; i++) {
|
|
12430
|
+
const idx = (start + i) % this.capacity;
|
|
12431
|
+
const entry = this.buffer[idx];
|
|
12432
|
+
if (!entry)
|
|
12433
|
+
continue;
|
|
12434
|
+
if (statusFilter && entry.status !== statusFilter)
|
|
12435
|
+
continue;
|
|
12436
|
+
if (metadataFilter && !matchMetadata(entry.metadata, metadataFilter))
|
|
12437
|
+
continue;
|
|
12438
|
+
result.push({ ...entry });
|
|
12439
|
+
}
|
|
12440
|
+
return result;
|
|
12441
|
+
}
|
|
12442
|
+
get length() {
|
|
12443
|
+
return this.count;
|
|
12444
|
+
}
|
|
12445
|
+
clear() {
|
|
12446
|
+
this.buffer = new Array(this.capacity);
|
|
12447
|
+
this.head = 0;
|
|
12448
|
+
this.count = 0;
|
|
12449
|
+
}
|
|
12450
|
+
}
|
|
12451
|
+
|
|
12452
|
+
class TaskRegistry {
|
|
12453
|
+
tasks = new Map;
|
|
12454
|
+
semaphore;
|
|
12455
|
+
nextId = 0;
|
|
12456
|
+
evictionTimers = new Map;
|
|
12457
|
+
accepting = true;
|
|
12458
|
+
options;
|
|
12459
|
+
client;
|
|
12460
|
+
taskHistory;
|
|
12461
|
+
constructor(options = {}) {
|
|
12462
|
+
this.options = {
|
|
12463
|
+
maxConcurrent: options.maxConcurrent ?? 50,
|
|
12464
|
+
taskTTL: options.taskTTL ?? 30000,
|
|
12465
|
+
maxOutputSize: options.maxOutputSize ?? 5 * 1024 * 1024,
|
|
12466
|
+
maxHistory: options.maxHistory ?? 200
|
|
12467
|
+
};
|
|
12468
|
+
this.client = options.client;
|
|
12469
|
+
this.semaphore = new Semaphore(this.options.maxConcurrent);
|
|
12470
|
+
this.taskHistory = new TaskHistory(this.options.maxHistory);
|
|
12471
|
+
}
|
|
12472
|
+
async spawn(command, options = {}) {
|
|
12473
|
+
if (!this.accepting) {
|
|
12474
|
+
throw new Error("Registry is shutting down");
|
|
12475
|
+
}
|
|
12476
|
+
const id = `task-${this.nextId++}`;
|
|
12477
|
+
const cwd = options.cwd ?? process.cwd();
|
|
12478
|
+
const task = createTask(id, command, cwd, options.timeout, options.sessionID, options.metadata);
|
|
12479
|
+
this.tasks.set(id, task);
|
|
12480
|
+
this.executeTask(task);
|
|
12481
|
+
return id;
|
|
12482
|
+
}
|
|
12483
|
+
async executeTask(task) {
|
|
12484
|
+
try {
|
|
12485
|
+
await this.semaphore.acquire();
|
|
12486
|
+
} catch (e) {
|
|
12487
|
+
task.status = "cancelled";
|
|
12488
|
+
task.error = String(e);
|
|
12489
|
+
task.resolveCompletion();
|
|
12490
|
+
return;
|
|
12491
|
+
}
|
|
12492
|
+
try {
|
|
12493
|
+
task.status = "running";
|
|
12494
|
+
task.startedAt = Date.now();
|
|
12495
|
+
const timeoutId = task.timeout ? setTimeout(() => task.abortController.abort(), task.timeout) : null;
|
|
12496
|
+
try {
|
|
12497
|
+
const proc = Bun.spawn(["sh", "-c", task.command], {
|
|
12498
|
+
cwd: task.cwd,
|
|
12499
|
+
stdout: Bun.file(task.stdout),
|
|
12500
|
+
stderr: Bun.file(task.stderr)
|
|
12501
|
+
});
|
|
12502
|
+
task.proc = proc;
|
|
12503
|
+
task.abortController.signal.addEventListener("abort", () => {
|
|
12504
|
+
proc.kill();
|
|
12505
|
+
});
|
|
12506
|
+
const exitCode = await proc.exited;
|
|
12507
|
+
task.exitCode = exitCode;
|
|
12508
|
+
if (task.abortController.signal.aborted) {
|
|
12509
|
+
task.status = "cancelled";
|
|
12510
|
+
} else {
|
|
12511
|
+
task.status = exitCode === 0 ? "completed" : "failed";
|
|
12512
|
+
}
|
|
12513
|
+
} catch (error45) {
|
|
12514
|
+
const errorMsg = `
|
|
12515
|
+
[Task execution error: ${String(error45)}]
|
|
12516
|
+
`;
|
|
12517
|
+
await appendFile(task.stderr, errorMsg);
|
|
12518
|
+
if (task.abortController.signal.aborted) {
|
|
12519
|
+
task.status = "cancelled";
|
|
12520
|
+
} else {
|
|
12521
|
+
task.status = "failed";
|
|
12522
|
+
task.error = String(error45);
|
|
12523
|
+
}
|
|
12524
|
+
} finally {
|
|
12525
|
+
if (timeoutId)
|
|
12526
|
+
clearTimeout(timeoutId);
|
|
12527
|
+
task.completedAt = Date.now();
|
|
12528
|
+
}
|
|
12529
|
+
} finally {
|
|
12530
|
+
this.semaphore.release();
|
|
12531
|
+
task.resolveCompletion();
|
|
12532
|
+
this.scheduleEviction(task.id);
|
|
12533
|
+
this.notifyCompletion(task);
|
|
12534
|
+
}
|
|
12535
|
+
}
|
|
12536
|
+
notifyCompletion(task) {
|
|
12537
|
+
if (!this.client || !task.sessionID)
|
|
12538
|
+
return;
|
|
12539
|
+
this.client.session.promptAsync({
|
|
12540
|
+
path: { id: task.sessionID },
|
|
12541
|
+
body: {
|
|
12542
|
+
parts: [
|
|
12543
|
+
{
|
|
12544
|
+
type: "text",
|
|
12545
|
+
text: `<task-notification>
|
|
12546
|
+
<task-id>${task.id}</task-id>
|
|
12547
|
+
<status>${task.status}</status>
|
|
12548
|
+
<exit-code>${task.exitCode ?? "N/A"}</exit-code>
|
|
12549
|
+
<stdout-path>${task.stdout}</stdout-path>
|
|
12550
|
+
<stderr-path>${task.stderr}</stderr-path>
|
|
12551
|
+
</task-notification>`
|
|
12552
|
+
}
|
|
12553
|
+
]
|
|
12554
|
+
}
|
|
12555
|
+
});
|
|
12556
|
+
}
|
|
12557
|
+
scheduleEviction(id) {
|
|
12558
|
+
const existing = this.evictionTimers.get(id);
|
|
12559
|
+
if (existing)
|
|
12560
|
+
clearTimeout(existing);
|
|
12561
|
+
const timer = setTimeout(() => {
|
|
12562
|
+
const task = this.tasks.get(id);
|
|
12563
|
+
if (task) {
|
|
12564
|
+
this.taskHistory.push({
|
|
12565
|
+
id: task.id,
|
|
12566
|
+
command: task.command.slice(0, 200),
|
|
12567
|
+
status: task.status,
|
|
12568
|
+
exitCode: task.exitCode,
|
|
12569
|
+
metadata: task.metadata,
|
|
12570
|
+
stdout: task.stdout,
|
|
12571
|
+
stderr: task.stderr,
|
|
12572
|
+
createdAt: task.createdAt,
|
|
12573
|
+
startedAt: task.startedAt,
|
|
12574
|
+
completedAt: task.completedAt
|
|
12575
|
+
});
|
|
12576
|
+
}
|
|
12577
|
+
this.tasks.delete(id);
|
|
12578
|
+
this.evictionTimers.delete(id);
|
|
12579
|
+
}, this.options.taskTTL);
|
|
12580
|
+
this.evictionTimers.set(id, timer);
|
|
12581
|
+
}
|
|
12582
|
+
get(id) {
|
|
12583
|
+
const task = this.tasks.get(id);
|
|
12584
|
+
if (task?.completedAt) {
|
|
12585
|
+
this.scheduleEviction(id);
|
|
12586
|
+
}
|
|
12587
|
+
return task;
|
|
12588
|
+
}
|
|
12589
|
+
async wait(id, timeoutMs = 30000) {
|
|
12590
|
+
const task = this.tasks.get(id);
|
|
12591
|
+
if (!task)
|
|
12592
|
+
throw new Error(`Task ${id} not found`);
|
|
12593
|
+
if (task.status !== "pending" && task.status !== "running") {
|
|
12594
|
+
return task;
|
|
12595
|
+
}
|
|
12596
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
12597
|
+
setTimeout(() => reject(new Error(`Task ${id} wait timed out`)), timeoutMs);
|
|
12598
|
+
});
|
|
12599
|
+
await Promise.race([task.completionPromise, timeoutPromise]);
|
|
12600
|
+
return task;
|
|
12601
|
+
}
|
|
12602
|
+
stop(id, signal) {
|
|
12603
|
+
const task = this.tasks.get(id);
|
|
12604
|
+
if (!task)
|
|
12605
|
+
return false;
|
|
12606
|
+
if (task.status === "running" && task.proc) {
|
|
12607
|
+
if (signal === "SIGKILL") {
|
|
12608
|
+
task.proc.kill(9);
|
|
12609
|
+
} else {
|
|
12610
|
+
task.proc.kill();
|
|
12611
|
+
}
|
|
12612
|
+
task.abortController.abort();
|
|
12613
|
+
return true;
|
|
12614
|
+
}
|
|
12615
|
+
if (task.status === "pending") {
|
|
12616
|
+
task.abortController.abort();
|
|
12617
|
+
task.status = "cancelled";
|
|
12618
|
+
task.resolveCompletion();
|
|
12619
|
+
return true;
|
|
12620
|
+
}
|
|
12621
|
+
return false;
|
|
12622
|
+
}
|
|
12623
|
+
list(statusFilter, metadataFilter) {
|
|
12624
|
+
let tasks = Array.from(this.tasks.values());
|
|
12625
|
+
if (statusFilter && statusFilter !== "all") {
|
|
12626
|
+
tasks = tasks.filter((t) => t.status === statusFilter);
|
|
12627
|
+
}
|
|
12628
|
+
if (metadataFilter) {
|
|
12629
|
+
tasks = tasks.filter((t) => matchMetadata(t.metadata, metadataFilter));
|
|
12630
|
+
}
|
|
12631
|
+
return tasks;
|
|
12632
|
+
}
|
|
12633
|
+
async shutdown(gracePeriodMs = 3000) {
|
|
12634
|
+
this.accepting = false;
|
|
12635
|
+
this.semaphore.drain();
|
|
12636
|
+
for (const timer of this.evictionTimers.values()) {
|
|
12637
|
+
clearTimeout(timer);
|
|
12638
|
+
}
|
|
12639
|
+
this.evictionTimers.clear();
|
|
12640
|
+
const runningTasks = [];
|
|
12641
|
+
for (const task of this.tasks.values()) {
|
|
12642
|
+
if (task.status === "running" && task.proc) {
|
|
12643
|
+
task.proc.kill();
|
|
12644
|
+
task.abortController.abort();
|
|
12645
|
+
runningTasks.push(task);
|
|
12646
|
+
}
|
|
12647
|
+
}
|
|
12648
|
+
if (runningTasks.length === 0)
|
|
12649
|
+
return;
|
|
12650
|
+
const graceDeadline = Date.now() + gracePeriodMs;
|
|
12651
|
+
for (const task of runningTasks) {
|
|
12652
|
+
const remaining = graceDeadline - Date.now();
|
|
12653
|
+
if (remaining > 0) {
|
|
12654
|
+
try {
|
|
12655
|
+
await Promise.race([
|
|
12656
|
+
task.completionPromise,
|
|
12657
|
+
new Promise((r) => setTimeout(r, remaining))
|
|
12658
|
+
]);
|
|
12659
|
+
} catch {}
|
|
12660
|
+
}
|
|
12661
|
+
if (task.status === "running" && task.proc) {
|
|
12662
|
+
task.proc.kill(9);
|
|
12663
|
+
}
|
|
12664
|
+
}
|
|
12665
|
+
}
|
|
12666
|
+
get size() {
|
|
12667
|
+
return this.tasks.size;
|
|
12668
|
+
}
|
|
12669
|
+
get activeCount() {
|
|
12670
|
+
return this.semaphore.activeCount;
|
|
12671
|
+
}
|
|
12672
|
+
get waitingCount() {
|
|
12673
|
+
return this.semaphore.waitingCount;
|
|
12674
|
+
}
|
|
12675
|
+
history(statusFilter, metadataFilter) {
|
|
12676
|
+
return this.taskHistory.entries(statusFilter, metadataFilter);
|
|
12677
|
+
}
|
|
12678
|
+
clear() {
|
|
12679
|
+
for (const timer of this.evictionTimers.values()) {
|
|
12680
|
+
clearTimeout(timer);
|
|
12681
|
+
}
|
|
12682
|
+
this.evictionTimers.clear();
|
|
12683
|
+
this.tasks.clear();
|
|
12684
|
+
this.taskHistory.clear();
|
|
12685
|
+
}
|
|
12686
|
+
}
|
|
12687
|
+
|
|
12332
12688
|
// index.ts
|
|
12333
|
-
var CustomToolPlugin = async () => {
|
|
12689
|
+
var CustomToolPlugin = async (input) => {
|
|
12690
|
+
const registry2 = new TaskRegistry({
|
|
12691
|
+
maxConcurrent: 50,
|
|
12692
|
+
client: input.client
|
|
12693
|
+
});
|
|
12334
12694
|
return {
|
|
12335
12695
|
tool: {
|
|
12336
|
-
|
|
12337
|
-
description:
|
|
12696
|
+
Task: tool({
|
|
12697
|
+
description: `Spawn background task.
|
|
12698
|
+
|
|
12699
|
+
Returns:
|
|
12700
|
+
- taskId: unique identifier
|
|
12701
|
+
- stdout/stderr: file paths (LIVE - readable during execution, not just after)
|
|
12702
|
+
|
|
12703
|
+
You will receive a notification when it completes.`,
|
|
12704
|
+
args: {
|
|
12705
|
+
command: tool.schema.string().describe("Shell command to execute"),
|
|
12706
|
+
cwd: tool.schema.string().optional().describe("Working directory"),
|
|
12707
|
+
timeout: tool.schema.number().optional().describe("Timeout in milliseconds"),
|
|
12708
|
+
metadata: tool.schema.record(tool.schema.string(), tool.schema.string()).optional().describe("Key-value metadata tags for filtering")
|
|
12709
|
+
},
|
|
12710
|
+
async execute(args, context) {
|
|
12711
|
+
const id = await registry2.spawn(args.command, {
|
|
12712
|
+
cwd: args.cwd,
|
|
12713
|
+
timeout: args.timeout,
|
|
12714
|
+
sessionID: context.sessionID,
|
|
12715
|
+
metadata: args.metadata
|
|
12716
|
+
});
|
|
12717
|
+
const task = registry2.get(id);
|
|
12718
|
+
return JSON.stringify({
|
|
12719
|
+
taskId: id,
|
|
12720
|
+
stdout: task?.stdout,
|
|
12721
|
+
stderr: task?.stderr,
|
|
12722
|
+
message: `Task ${id} spawned. You will receive a notification when complete.`
|
|
12723
|
+
});
|
|
12724
|
+
}
|
|
12725
|
+
}),
|
|
12726
|
+
TaskStop: tool({
|
|
12727
|
+
description: "Stop/cancel a running background task",
|
|
12728
|
+
args: {
|
|
12729
|
+
taskId: tool.schema.string().describe("Task ID to stop"),
|
|
12730
|
+
signal: tool.schema.enum(["SIGTERM", "SIGKILL"]).optional().describe("Signal to send (default SIGTERM)")
|
|
12731
|
+
},
|
|
12732
|
+
async execute(args) {
|
|
12733
|
+
const stopped = registry2.stop(args.taskId, args.signal);
|
|
12734
|
+
return JSON.stringify({ success: stopped, taskId: args.taskId });
|
|
12735
|
+
}
|
|
12736
|
+
}),
|
|
12737
|
+
TaskList: tool({
|
|
12738
|
+
description: "List background tasks with status, output paths, and metadata. Supports filtering by status and metadata. Use includeHistory to see evicted tasks.",
|
|
12338
12739
|
args: {
|
|
12339
|
-
|
|
12740
|
+
status: tool.schema.enum([
|
|
12741
|
+
"all",
|
|
12742
|
+
"running",
|
|
12743
|
+
"completed",
|
|
12744
|
+
"failed",
|
|
12745
|
+
"pending",
|
|
12746
|
+
"cancelled"
|
|
12747
|
+
]).optional().describe("Filter by status"),
|
|
12748
|
+
metadata: tool.schema.record(tool.schema.string(), tool.schema.string()).optional().describe("Filter by metadata key-value pairs (all must match)"),
|
|
12749
|
+
includeHistory: tool.schema.boolean().optional().describe("Include evicted tasks from history buffer (default false)")
|
|
12340
12750
|
},
|
|
12341
12751
|
async execute(args) {
|
|
12342
|
-
|
|
12752
|
+
const tasks = registry2.list(args.status, args.metadata);
|
|
12753
|
+
const result = {
|
|
12754
|
+
count: tasks.length,
|
|
12755
|
+
tasks: tasks.map((t) => ({
|
|
12756
|
+
id: t.id,
|
|
12757
|
+
status: t.status,
|
|
12758
|
+
command: t.command.slice(0, 100),
|
|
12759
|
+
stdout: t.stdout,
|
|
12760
|
+
stderr: t.stderr,
|
|
12761
|
+
metadata: t.metadata,
|
|
12762
|
+
createdAt: t.createdAt,
|
|
12763
|
+
duration: t.completedAt && t.startedAt ? t.completedAt - t.startedAt : undefined
|
|
12764
|
+
}))
|
|
12765
|
+
};
|
|
12766
|
+
if (args.includeHistory) {
|
|
12767
|
+
const history = registry2.history(args.status, args.metadata);
|
|
12768
|
+
result.history = history;
|
|
12769
|
+
result.historyCount = history.length;
|
|
12770
|
+
}
|
|
12771
|
+
return JSON.stringify(result);
|
|
12343
12772
|
}
|
|
12344
12773
|
})
|
|
12345
12774
|
}
|
|
12346
12775
|
};
|
|
12347
12776
|
};
|
|
12777
|
+
var opencode_task_default = CustomToolPlugin;
|
|
12348
12778
|
export {
|
|
12779
|
+
opencode_task_default as default,
|
|
12349
12780
|
CustomToolPlugin
|
|
12350
12781
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-task",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"files": ["dist"],
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"devDependencies": {
|
|
18
18
|
"@biomejs/biome": "^1.9.0",
|
|
19
19
|
"@opencode-ai/plugin": "latest",
|
|
20
|
+
"@types/bun": "latest",
|
|
20
21
|
"typescript": "^5.0.0"
|
|
21
22
|
}
|
|
22
23
|
}
|