@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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/plugin.ts +204 -197
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.4",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Ship - Linear task management integration",
6
6
  "license": "MIT",
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
- const primeOutput = await $`ship prime`.text();
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
- const result = await $`ship ${args}`.nothrow();
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
- const result = await $`ship prime`.nothrow();
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
- * Ship tool for task management
210
+ * Create ship tool with captured $ from plugin context
207
211
  */
208
- const shipTool = createTool({
209
- description: `Linear task management for the current project.
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
- 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.
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
- case "ready": {
304
- const result = await runShip($, ["ready", "--json"]);
305
- if (!result.success) {
306
- return `Failed to get ready tasks: ${result.output}`;
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
- 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).";
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
- 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
+ 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
- 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.";
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
- 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.";
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
- 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;
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
- 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}`;
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
- 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}`;
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
- 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);
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
- 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;
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
- 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}`;
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
- 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}`;
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
- case "prime": {
437
- const result = await runShip($, ["prime"]);
438
- if (!result.success) {
439
- return `Failed to get context: ${result.output}`;
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
- default:
445
- return `Unknown action: ${args.action}`;
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;