@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/plugin.ts +198 -194
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-cli/opencode",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Ship - Linear task management integration",
6
6
  "license": "MIT",
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
- * Ship tool for task management
207
+ * Create ship tool with captured $ from plugin context
207
208
  */
208
- const shipTool = createTool({
209
- description: `Linear task management for the current project.
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
- args: {
223
- action: createTool.schema
224
- .enum([
225
- "ready",
226
- "list",
227
- "blocked",
228
- "show",
229
- "start",
230
- "done",
231
- "create",
232
- "block",
233
- "unblock",
234
- "prime",
235
- "status",
236
- ])
237
- .describe(
238
- "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)"
239
- ),
240
- taskId: createTool.schema
241
- .string()
242
- .optional()
243
- .describe("Task identifier (e.g., BRI-123) - required for show, start, done"),
244
- title: createTool.schema
245
- .string()
246
- .optional()
247
- .describe("Task title - required for create"),
248
- description: createTool.schema
249
- .string()
250
- .optional()
251
- .describe("Task description - optional for create"),
252
- priority: createTool.schema
253
- .enum(["urgent", "high", "medium", "low", "none"])
254
- .optional()
255
- .describe("Task priority - optional for create"),
256
- blocker: createTool.schema
257
- .string()
258
- .optional()
259
- .describe("Blocker task ID - required for block/unblock"),
260
- blocked: createTool.schema
261
- .string()
262
- .optional()
263
- .describe("Blocked task ID - required for block/unblock"),
264
- filter: createTool.schema
265
- .object({
266
- status: createTool.schema
267
- .enum(["backlog", "todo", "in_progress", "in_review", "done", "cancelled"])
268
- .optional(),
269
- priority: createTool.schema.enum(["urgent", "high", "medium", "low", "none"]).optional(),
270
- mine: createTool.schema.boolean().optional(),
271
- })
272
- .optional()
273
- .describe("Filters for list action"),
274
- },
275
-
276
- async execute(args, ctx) {
277
- const $ = (ctx as any).$ as PluginInput["$"];
278
-
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.
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
- case "ready": {
304
- const result = await runShip($, ["ready", "--json"]);
305
- if (!result.success) {
306
- return `Failed to get ready tasks: ${result.output}`;
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
- 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).";
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
- 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");
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
- 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.";
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
- 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.";
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
- 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;
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
- 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}`;
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
- 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}`;
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
- 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);
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
- 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;
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
- 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}`;
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
- 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}`;
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
- case "prime": {
437
- const result = await runShip($, ["prime"]);
438
- if (!result.success) {
439
- return `Failed to get context: ${result.output}`;
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
- default:
445
- return `Unknown action: ${args.action}`;
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;