@ship-cli/opencode 0.0.2 → 0.0.4
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/package.json +1 -1
- package/src/plugin.ts +204 -197
package/package.json
CHANGED
package/src/plugin.ts
CHANGED
|
@@ -79,12 +79,14 @@ async function injectShipContext(
|
|
|
79
79
|
context?: { model?: { providerID: string; modelID: string }; agent?: string }
|
|
80
80
|
): Promise<void> {
|
|
81
81
|
try {
|
|
82
|
-
|
|
82
|
+
// Use quiet() to prevent any output from bleeding into TUI
|
|
83
|
+
const primeOutput = await $`ship prime`.quiet().text();
|
|
83
84
|
|
|
84
85
|
if (!primeOutput || primeOutput.trim() === "") {
|
|
85
86
|
return;
|
|
86
87
|
}
|
|
87
88
|
|
|
89
|
+
// Wrap the plain markdown output with XML tags (like beads plugin does)
|
|
88
90
|
const shipContext = `<ship-context>
|
|
89
91
|
${primeOutput.trim()}
|
|
90
92
|
</ship-context>
|
|
@@ -115,7 +117,8 @@ async function runShip(
|
|
|
115
117
|
args: string[]
|
|
116
118
|
): Promise<{ success: boolean; output: string }> {
|
|
117
119
|
try {
|
|
118
|
-
|
|
120
|
+
// Use quiet() to prevent output from bleeding into TUI
|
|
121
|
+
const result = await $`ship ${args}`.quiet().nothrow();
|
|
119
122
|
const stdout = await new Response(result.stdout).text();
|
|
120
123
|
const stderr = await new Response(result.stderr).text();
|
|
121
124
|
|
|
@@ -139,7 +142,8 @@ async function runShip(
|
|
|
139
142
|
async function isShipConfigured($: PluginInput["$"]): Promise<boolean> {
|
|
140
143
|
try {
|
|
141
144
|
// Try running ship prime - it will fail if not configured
|
|
142
|
-
|
|
145
|
+
// Use quiet() to suppress stdout/stderr from bleeding into TUI
|
|
146
|
+
const result = await $`ship prime`.quiet().nothrow();
|
|
143
147
|
return result.exitCode === 0;
|
|
144
148
|
} catch {
|
|
145
149
|
return false;
|
|
@@ -203,10 +207,11 @@ function formatTaskDetails(task: {
|
|
|
203
207
|
}
|
|
204
208
|
|
|
205
209
|
/**
|
|
206
|
-
*
|
|
210
|
+
* Create ship tool with captured $ from plugin context
|
|
207
211
|
*/
|
|
208
|
-
|
|
209
|
-
|
|
212
|
+
function createShipTool($: PluginInput["$"]) {
|
|
213
|
+
return createTool({
|
|
214
|
+
description: `Linear task management for the current project.
|
|
210
215
|
|
|
211
216
|
Use this tool to:
|
|
212
217
|
- List tasks ready to work on (no blockers)
|
|
@@ -219,68 +224,66 @@ Use this tool to:
|
|
|
219
224
|
Requires ship to be configured in the project (.ship/config.yaml).
|
|
220
225
|
Run 'ship init' in the terminal first if not configured.`,
|
|
221
226
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
if (!configured) {
|
|
283
|
-
return `Ship is not configured in this project.
|
|
227
|
+
args: {
|
|
228
|
+
action: createTool.schema
|
|
229
|
+
.enum([
|
|
230
|
+
"ready",
|
|
231
|
+
"list",
|
|
232
|
+
"blocked",
|
|
233
|
+
"show",
|
|
234
|
+
"start",
|
|
235
|
+
"done",
|
|
236
|
+
"create",
|
|
237
|
+
"block",
|
|
238
|
+
"unblock",
|
|
239
|
+
"prime",
|
|
240
|
+
"status",
|
|
241
|
+
])
|
|
242
|
+
.describe(
|
|
243
|
+
"Action to perform: ready (unblocked tasks), list (all tasks), blocked (blocked tasks), show (task details), start (begin task), done (complete task), create (new task), block/unblock (dependencies), prime (AI context), status (current config)"
|
|
244
|
+
),
|
|
245
|
+
taskId: createTool.schema
|
|
246
|
+
.string()
|
|
247
|
+
.optional()
|
|
248
|
+
.describe("Task identifier (e.g., BRI-123) - required for show, start, done"),
|
|
249
|
+
title: createTool.schema
|
|
250
|
+
.string()
|
|
251
|
+
.optional()
|
|
252
|
+
.describe("Task title - required for create"),
|
|
253
|
+
description: createTool.schema
|
|
254
|
+
.string()
|
|
255
|
+
.optional()
|
|
256
|
+
.describe("Task description - optional for create"),
|
|
257
|
+
priority: createTool.schema
|
|
258
|
+
.enum(["urgent", "high", "medium", "low", "none"])
|
|
259
|
+
.optional()
|
|
260
|
+
.describe("Task priority - optional for create"),
|
|
261
|
+
blocker: createTool.schema
|
|
262
|
+
.string()
|
|
263
|
+
.optional()
|
|
264
|
+
.describe("Blocker task ID - required for block/unblock"),
|
|
265
|
+
blocked: createTool.schema
|
|
266
|
+
.string()
|
|
267
|
+
.optional()
|
|
268
|
+
.describe("Blocked task ID - required for block/unblock"),
|
|
269
|
+
filter: createTool.schema
|
|
270
|
+
.object({
|
|
271
|
+
status: createTool.schema
|
|
272
|
+
.enum(["backlog", "todo", "in_progress", "in_review", "done", "cancelled"])
|
|
273
|
+
.optional(),
|
|
274
|
+
priority: createTool.schema.enum(["urgent", "high", "medium", "low", "none"]).optional(),
|
|
275
|
+
mine: createTool.schema.boolean().optional(),
|
|
276
|
+
})
|
|
277
|
+
.optional()
|
|
278
|
+
.describe("Filters for list action"),
|
|
279
|
+
},
|
|
280
|
+
|
|
281
|
+
async execute(args) {
|
|
282
|
+
// Check if ship is configured
|
|
283
|
+
if (args.action !== "status") {
|
|
284
|
+
const configured = await isShipConfigured($);
|
|
285
|
+
if (!configured) {
|
|
286
|
+
return `Ship is not configured in this project.
|
|
284
287
|
|
|
285
288
|
Run 'ship init' in the terminal to:
|
|
286
289
|
1. Authenticate with Linear (paste your API key from https://linear.app/settings/api)
|
|
@@ -288,164 +291,165 @@ Run 'ship init' in the terminal to:
|
|
|
288
291
|
3. Optionally select a project
|
|
289
292
|
|
|
290
293
|
After that, you can use this tool to manage tasks.`;
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
switch (args.action) {
|
|
295
|
-
case "status": {
|
|
296
|
-
const configured = await isShipConfigured($);
|
|
297
|
-
if (!configured) {
|
|
298
|
-
return "Ship is not configured. Run 'ship init' first.";
|
|
299
294
|
}
|
|
300
|
-
return "Ship is configured in this project.";
|
|
301
295
|
}
|
|
302
296
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
297
|
+
switch (args.action) {
|
|
298
|
+
case "status": {
|
|
299
|
+
const configured = await isShipConfigured($);
|
|
300
|
+
if (!configured) {
|
|
301
|
+
return "Ship is not configured. Run 'ship init' first.";
|
|
302
|
+
}
|
|
303
|
+
return "Ship is configured in this project.";
|
|
307
304
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
305
|
+
|
|
306
|
+
case "ready": {
|
|
307
|
+
const result = await runShip($, ["ready", "--json"]);
|
|
308
|
+
if (!result.success) {
|
|
309
|
+
return `Failed to get ready tasks: ${result.output}`;
|
|
310
|
+
}
|
|
311
|
+
try {
|
|
312
|
+
const tasks = JSON.parse(result.output);
|
|
313
|
+
if (tasks.length === 0) {
|
|
314
|
+
return "No tasks ready to work on (all tasks are either blocked or completed).";
|
|
315
|
+
}
|
|
316
|
+
return `Ready tasks (no blockers):\n\n${formatTaskList(tasks)}`;
|
|
317
|
+
} catch {
|
|
318
|
+
return result.output;
|
|
312
319
|
}
|
|
313
|
-
return `Ready tasks (no blockers):\n\n${formatTaskList(tasks)}`;
|
|
314
|
-
} catch {
|
|
315
|
-
return result.output;
|
|
316
320
|
}
|
|
317
|
-
}
|
|
318
321
|
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
322
|
+
case "list": {
|
|
323
|
+
const listArgs = ["list", "--json"];
|
|
324
|
+
if (args.filter?.status) listArgs.push("--status", args.filter.status);
|
|
325
|
+
if (args.filter?.priority) listArgs.push("--priority", args.filter.priority);
|
|
326
|
+
if (args.filter?.mine) listArgs.push("--mine");
|
|
324
327
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
328
|
+
const result = await runShip($, listArgs);
|
|
329
|
+
if (!result.success) {
|
|
330
|
+
return `Failed to list tasks: ${result.output}`;
|
|
331
|
+
}
|
|
332
|
+
try {
|
|
333
|
+
const tasks = JSON.parse(result.output);
|
|
334
|
+
if (tasks.length === 0) {
|
|
335
|
+
return "No tasks found matching the filter.";
|
|
336
|
+
}
|
|
337
|
+
return `Tasks:\n\n${formatTaskList(tasks)}`;
|
|
338
|
+
} catch {
|
|
339
|
+
return result.output;
|
|
333
340
|
}
|
|
334
|
-
return `Tasks:\n\n${formatTaskList(tasks)}`;
|
|
335
|
-
} catch {
|
|
336
|
-
return result.output;
|
|
337
341
|
}
|
|
338
|
-
}
|
|
339
342
|
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
343
|
+
case "blocked": {
|
|
344
|
+
const result = await runShip($, ["blocked", "--json"]);
|
|
345
|
+
if (!result.success) {
|
|
346
|
+
return `Failed to get blocked tasks: ${result.output}`;
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const tasks = JSON.parse(result.output);
|
|
350
|
+
if (tasks.length === 0) {
|
|
351
|
+
return "No blocked tasks.";
|
|
352
|
+
}
|
|
353
|
+
return `Blocked tasks:\n\n${formatTaskList(tasks)}`;
|
|
354
|
+
} catch {
|
|
355
|
+
return result.output;
|
|
349
356
|
}
|
|
350
|
-
return `Blocked tasks:\n\n${formatTaskList(tasks)}`;
|
|
351
|
-
} catch {
|
|
352
|
-
return result.output;
|
|
353
357
|
}
|
|
354
|
-
}
|
|
355
358
|
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
359
|
+
case "show": {
|
|
360
|
+
if (!args.taskId) {
|
|
361
|
+
return "Error: taskId is required for show action";
|
|
362
|
+
}
|
|
363
|
+
const result = await runShip($, ["show", args.taskId, "--json"]);
|
|
364
|
+
if (!result.success) {
|
|
365
|
+
return `Failed to get task: ${result.output}`;
|
|
366
|
+
}
|
|
367
|
+
try {
|
|
368
|
+
const task = JSON.parse(result.output);
|
|
369
|
+
return formatTaskDetails(task);
|
|
370
|
+
} catch {
|
|
371
|
+
return result.output;
|
|
372
|
+
}
|
|
369
373
|
}
|
|
370
|
-
}
|
|
371
374
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
375
|
+
case "start": {
|
|
376
|
+
if (!args.taskId) {
|
|
377
|
+
return "Error: taskId is required for start action";
|
|
378
|
+
}
|
|
379
|
+
const result = await runShip($, ["start", args.taskId]);
|
|
380
|
+
if (!result.success) {
|
|
381
|
+
return `Failed to start task: ${result.output}`;
|
|
382
|
+
}
|
|
383
|
+
return `Started working on ${args.taskId}`;
|
|
379
384
|
}
|
|
380
|
-
return `Started working on ${args.taskId}`;
|
|
381
|
-
}
|
|
382
385
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
386
|
+
case "done": {
|
|
387
|
+
if (!args.taskId) {
|
|
388
|
+
return "Error: taskId is required for done action";
|
|
389
|
+
}
|
|
390
|
+
const result = await runShip($, ["done", args.taskId]);
|
|
391
|
+
if (!result.success) {
|
|
392
|
+
return `Failed to complete task: ${result.output}`;
|
|
393
|
+
}
|
|
394
|
+
return `Completed ${args.taskId}`;
|
|
390
395
|
}
|
|
391
|
-
return `Completed ${args.taskId}`;
|
|
392
|
-
}
|
|
393
396
|
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
397
|
+
case "create": {
|
|
398
|
+
if (!args.title) {
|
|
399
|
+
return "Error: title is required for create action";
|
|
400
|
+
}
|
|
401
|
+
const createArgs = ["create", args.title, "--json"];
|
|
402
|
+
if (args.description) createArgs.push("--description", args.description);
|
|
403
|
+
if (args.priority) createArgs.push("--priority", args.priority);
|
|
401
404
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
405
|
+
const result = await runShip($, createArgs);
|
|
406
|
+
if (!result.success) {
|
|
407
|
+
return `Failed to create task: ${result.output}`;
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const task = JSON.parse(result.output);
|
|
411
|
+
return `Created task ${task.identifier}: ${task.title}\nURL: ${task.url}`;
|
|
412
|
+
} catch {
|
|
413
|
+
return result.output;
|
|
414
|
+
}
|
|
411
415
|
}
|
|
412
|
-
}
|
|
413
416
|
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
417
|
+
case "block": {
|
|
418
|
+
if (!args.blocker || !args.blocked) {
|
|
419
|
+
return "Error: both blocker and blocked task IDs are required";
|
|
420
|
+
}
|
|
421
|
+
const result = await runShip($, ["block", args.blocker, args.blocked]);
|
|
422
|
+
if (!result.success) {
|
|
423
|
+
return `Failed to add blocker: ${result.output}`;
|
|
424
|
+
}
|
|
425
|
+
return `${args.blocker} now blocks ${args.blocked}`;
|
|
421
426
|
}
|
|
422
|
-
return `${args.blocker} now blocks ${args.blocked}`;
|
|
423
|
-
}
|
|
424
427
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
428
|
+
case "unblock": {
|
|
429
|
+
if (!args.blocker || !args.blocked) {
|
|
430
|
+
return "Error: both blocker and blocked task IDs are required";
|
|
431
|
+
}
|
|
432
|
+
const result = await runShip($, ["unblock", args.blocker, args.blocked]);
|
|
433
|
+
if (!result.success) {
|
|
434
|
+
return `Failed to remove blocker: ${result.output}`;
|
|
435
|
+
}
|
|
436
|
+
return `Removed ${args.blocker} as blocker of ${args.blocked}`;
|
|
432
437
|
}
|
|
433
|
-
return `Removed ${args.blocker} as blocker of ${args.blocked}`;
|
|
434
|
-
}
|
|
435
438
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
439
|
+
case "prime": {
|
|
440
|
+
const result = await runShip($, ["prime"]);
|
|
441
|
+
if (!result.success) {
|
|
442
|
+
return `Failed to get context: ${result.output}`;
|
|
443
|
+
}
|
|
444
|
+
return result.output;
|
|
440
445
|
}
|
|
441
|
-
return result.output;
|
|
442
|
-
}
|
|
443
446
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
});
|
|
447
|
+
default:
|
|
448
|
+
return `Unknown action: ${args.action}`;
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
});
|
|
452
|
+
}
|
|
449
453
|
|
|
450
454
|
/**
|
|
451
455
|
* Ship OpenCode Plugin
|
|
@@ -453,6 +457,9 @@ After that, you can use this tool to manage tasks.`;
|
|
|
453
457
|
export const ShipPlugin: Plugin = async ({ client, $ }) => {
|
|
454
458
|
const injectedSessions = new Set<string>();
|
|
455
459
|
|
|
460
|
+
// Create the ship tool with captured $
|
|
461
|
+
const shipTool = createShipTool($);
|
|
462
|
+
|
|
456
463
|
return {
|
|
457
464
|
"chat.message": async (_input, output) => {
|
|
458
465
|
const sessionID = output.message.sessionID;
|