@ship-cli/opencode 0.0.1 → 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 +202 -196
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>
|
|
@@ -133,11 +134,13 @@ async function runShip(
|
|
|
133
134
|
}
|
|
134
135
|
|
|
135
136
|
/**
|
|
136
|
-
* Check if ship is configured
|
|
137
|
+
* Check if ship is configured by attempting to run a ship command.
|
|
138
|
+
* This handles both local (.ship/config.yaml) and global configurations.
|
|
137
139
|
*/
|
|
138
140
|
async function isShipConfigured($: PluginInput["$"]): Promise<boolean> {
|
|
139
141
|
try {
|
|
140
|
-
|
|
142
|
+
// Try running ship prime - it will fail if not configured
|
|
143
|
+
const result = await $`ship prime`.nothrow();
|
|
141
144
|
return result.exitCode === 0;
|
|
142
145
|
} catch {
|
|
143
146
|
return false;
|
|
@@ -201,10 +204,11 @@ function formatTaskDetails(task: {
|
|
|
201
204
|
}
|
|
202
205
|
|
|
203
206
|
/**
|
|
204
|
-
*
|
|
207
|
+
* Create ship tool with captured $ from plugin context
|
|
205
208
|
*/
|
|
206
|
-
|
|
207
|
-
|
|
209
|
+
function createShipTool($: PluginInput["$"]) {
|
|
210
|
+
return createTool({
|
|
211
|
+
description: `Linear task management for the current project.
|
|
208
212
|
|
|
209
213
|
Use this tool to:
|
|
210
214
|
- List tasks ready to work on (no blockers)
|
|
@@ -217,68 +221,66 @@ Use this tool to:
|
|
|
217
221
|
Requires ship to be configured in the project (.ship/config.yaml).
|
|
218
222
|
Run 'ship init' in the terminal first if not configured.`,
|
|
219
223
|
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
if (!configured) {
|
|
281
|
-
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.
|
|
282
284
|
|
|
283
285
|
Run 'ship init' in the terminal to:
|
|
284
286
|
1. Authenticate with Linear (paste your API key from https://linear.app/settings/api)
|
|
@@ -286,164 +288,165 @@ Run 'ship init' in the terminal to:
|
|
|
286
288
|
3. Optionally select a project
|
|
287
289
|
|
|
288
290
|
After that, you can use this tool to manage tasks.`;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
switch (args.action) {
|
|
293
|
-
case "status": {
|
|
294
|
-
const configured = await isShipConfigured($);
|
|
295
|
-
if (!configured) {
|
|
296
|
-
return "Ship is not configured. Run 'ship init' first.";
|
|
297
291
|
}
|
|
298
|
-
return "Ship is configured in this project.";
|
|
299
292
|
}
|
|
300
293
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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.";
|
|
305
301
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
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;
|
|
310
316
|
}
|
|
311
|
-
return `Ready tasks (no blockers):\n\n${formatTaskList(tasks)}`;
|
|
312
|
-
} catch {
|
|
313
|
-
return result.output;
|
|
314
317
|
}
|
|
315
|
-
}
|
|
316
318
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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");
|
|
322
324
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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;
|
|
331
337
|
}
|
|
332
|
-
return `Tasks:\n\n${formatTaskList(tasks)}`;
|
|
333
|
-
} catch {
|
|
334
|
-
return result.output;
|
|
335
338
|
}
|
|
336
|
-
}
|
|
337
339
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
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;
|
|
347
353
|
}
|
|
348
|
-
return `Blocked tasks:\n\n${formatTaskList(tasks)}`;
|
|
349
|
-
} catch {
|
|
350
|
-
return result.output;
|
|
351
354
|
}
|
|
352
|
-
}
|
|
353
355
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
+
}
|
|
367
370
|
}
|
|
368
|
-
}
|
|
369
371
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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}`;
|
|
377
381
|
}
|
|
378
|
-
return `Started working on ${args.taskId}`;
|
|
379
|
-
}
|
|
380
382
|
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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}`;
|
|
388
392
|
}
|
|
389
|
-
return `Completed ${args.taskId}`;
|
|
390
|
-
}
|
|
391
393
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
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);
|
|
399
401
|
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
}
|
|
409
412
|
}
|
|
410
|
-
}
|
|
411
413
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
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}`;
|
|
419
423
|
}
|
|
420
|
-
return `${args.blocker} now blocks ${args.blocked}`;
|
|
421
|
-
}
|
|
422
424
|
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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}`;
|
|
430
434
|
}
|
|
431
|
-
return `Removed ${args.blocker} as blocker of ${args.blocked}`;
|
|
432
|
-
}
|
|
433
435
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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;
|
|
438
442
|
}
|
|
439
|
-
return result.output;
|
|
440
|
-
}
|
|
441
443
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
});
|
|
444
|
+
default:
|
|
445
|
+
return `Unknown action: ${args.action}`;
|
|
446
|
+
}
|
|
447
|
+
},
|
|
448
|
+
});
|
|
449
|
+
}
|
|
447
450
|
|
|
448
451
|
/**
|
|
449
452
|
* Ship OpenCode Plugin
|
|
@@ -451,6 +454,9 @@ After that, you can use this tool to manage tasks.`;
|
|
|
451
454
|
export const ShipPlugin: Plugin = async ({ client, $ }) => {
|
|
452
455
|
const injectedSessions = new Set<string>();
|
|
453
456
|
|
|
457
|
+
// Create the ship tool with captured $
|
|
458
|
+
const shipTool = createShipTool($);
|
|
459
|
+
|
|
454
460
|
return {
|
|
455
461
|
"chat.message": async (_input, output) => {
|
|
456
462
|
const sessionID = output.message.sessionID;
|