@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/plugin.ts +202 -196
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-cli/opencode",
3
- "version": "0.0.1",
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>
@@ -133,11 +134,13 @@ async function runShip(
133
134
  }
134
135
 
135
136
  /**
136
- * Check if ship is configured in the given directory
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
- const result = await $`test -f .ship/config.yaml`.nothrow();
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
- * Ship tool for task management
207
+ * Create ship tool with captured $ from plugin context
205
208
  */
206
- const shipTool = createTool({
207
- description: `Linear task management for the current project.
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
- args: {
221
- action: createTool.schema
222
- .enum([
223
- "ready",
224
- "list",
225
- "blocked",
226
- "show",
227
- "start",
228
- "done",
229
- "create",
230
- "block",
231
- "unblock",
232
- "prime",
233
- "status",
234
- ])
235
- .describe(
236
- "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)"
237
- ),
238
- taskId: createTool.schema
239
- .string()
240
- .optional()
241
- .describe("Task identifier (e.g., BRI-123) - required for show, start, done"),
242
- title: createTool.schema
243
- .string()
244
- .optional()
245
- .describe("Task title - required for create"),
246
- description: createTool.schema
247
- .string()
248
- .optional()
249
- .describe("Task description - optional for create"),
250
- priority: createTool.schema
251
- .enum(["urgent", "high", "medium", "low", "none"])
252
- .optional()
253
- .describe("Task priority - optional for create"),
254
- blocker: createTool.schema
255
- .string()
256
- .optional()
257
- .describe("Blocker task ID - required for block/unblock"),
258
- blocked: createTool.schema
259
- .string()
260
- .optional()
261
- .describe("Blocked task ID - required for block/unblock"),
262
- filter: createTool.schema
263
- .object({
264
- status: createTool.schema
265
- .enum(["backlog", "todo", "in_progress", "in_review", "done", "cancelled"])
266
- .optional(),
267
- priority: createTool.schema.enum(["urgent", "high", "medium", "low", "none"]).optional(),
268
- mine: createTool.schema.boolean().optional(),
269
- })
270
- .optional()
271
- .describe("Filters for list action"),
272
- },
273
-
274
- async execute(args, ctx) {
275
- const $ = (ctx as any).$ as PluginInput["$"];
276
-
277
- // Check if ship is configured
278
- if (args.action !== "status") {
279
- const configured = await isShipConfigured($);
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
- case "ready": {
302
- const result = await runShip($, ["ready", "--json"]);
303
- if (!result.success) {
304
- 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.";
305
301
  }
306
- try {
307
- const tasks = JSON.parse(result.output);
308
- if (tasks.length === 0) {
309
- 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;
310
316
  }
311
- return `Ready tasks (no blockers):\n\n${formatTaskList(tasks)}`;
312
- } catch {
313
- return result.output;
314
317
  }
315
- }
316
318
 
317
- case "list": {
318
- const listArgs = ["list", "--json"];
319
- if (args.filter?.status) listArgs.push("--status", args.filter.status);
320
- if (args.filter?.priority) listArgs.push("--priority", args.filter.priority);
321
- 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");
322
324
 
323
- const result = await runShip($, listArgs);
324
- if (!result.success) {
325
- return `Failed to list tasks: ${result.output}`;
326
- }
327
- try {
328
- const tasks = JSON.parse(result.output);
329
- if (tasks.length === 0) {
330
- 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;
331
337
  }
332
- return `Tasks:\n\n${formatTaskList(tasks)}`;
333
- } catch {
334
- return result.output;
335
338
  }
336
- }
337
339
 
338
- case "blocked": {
339
- const result = await runShip($, ["blocked", "--json"]);
340
- if (!result.success) {
341
- return `Failed to get blocked tasks: ${result.output}`;
342
- }
343
- try {
344
- const tasks = JSON.parse(result.output);
345
- if (tasks.length === 0) {
346
- 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;
347
353
  }
348
- return `Blocked tasks:\n\n${formatTaskList(tasks)}`;
349
- } catch {
350
- return result.output;
351
354
  }
352
- }
353
355
 
354
- case "show": {
355
- if (!args.taskId) {
356
- return "Error: taskId is required for show action";
357
- }
358
- const result = await runShip($, ["show", args.taskId, "--json"]);
359
- if (!result.success) {
360
- return `Failed to get task: ${result.output}`;
361
- }
362
- try {
363
- const task = JSON.parse(result.output);
364
- return formatTaskDetails(task);
365
- } catch {
366
- 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
+ }
367
370
  }
368
- }
369
371
 
370
- case "start": {
371
- if (!args.taskId) {
372
- return "Error: taskId is required for start action";
373
- }
374
- const result = await runShip($, ["start", args.taskId]);
375
- if (!result.success) {
376
- 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}`;
377
381
  }
378
- return `Started working on ${args.taskId}`;
379
- }
380
382
 
381
- case "done": {
382
- if (!args.taskId) {
383
- return "Error: taskId is required for done action";
384
- }
385
- const result = await runShip($, ["done", args.taskId]);
386
- if (!result.success) {
387
- 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}`;
388
392
  }
389
- return `Completed ${args.taskId}`;
390
- }
391
393
 
392
- case "create": {
393
- if (!args.title) {
394
- return "Error: title is required for create action";
395
- }
396
- const createArgs = ["create", args.title, "--json"];
397
- if (args.description) createArgs.push("--description", args.description);
398
- 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);
399
401
 
400
- const result = await runShip($, createArgs);
401
- if (!result.success) {
402
- return `Failed to create task: ${result.output}`;
403
- }
404
- try {
405
- const task = JSON.parse(result.output);
406
- return `Created task ${task.identifier}: ${task.title}\nURL: ${task.url}`;
407
- } catch {
408
- 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
+ }
409
412
  }
410
- }
411
413
 
412
- case "block": {
413
- if (!args.blocker || !args.blocked) {
414
- return "Error: both blocker and blocked task IDs are required";
415
- }
416
- const result = await runShip($, ["block", args.blocker, args.blocked]);
417
- if (!result.success) {
418
- 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}`;
419
423
  }
420
- return `${args.blocker} now blocks ${args.blocked}`;
421
- }
422
424
 
423
- case "unblock": {
424
- if (!args.blocker || !args.blocked) {
425
- return "Error: both blocker and blocked task IDs are required";
426
- }
427
- const result = await runShip($, ["unblock", args.blocker, args.blocked]);
428
- if (!result.success) {
429
- 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}`;
430
434
  }
431
- return `Removed ${args.blocker} as blocker of ${args.blocked}`;
432
- }
433
435
 
434
- case "prime": {
435
- const result = await runShip($, ["prime"]);
436
- if (!result.success) {
437
- 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;
438
442
  }
439
- return result.output;
440
- }
441
443
 
442
- default:
443
- return `Unknown action: ${args.action}`;
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;