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