@ship-cli/opencode 0.0.4 → 0.0.5

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 +190 -52
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ship-cli/opencode",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "type": "module",
5
5
  "description": "OpenCode plugin for Ship - Linear task management integration",
6
6
  "license": "MIT",
package/src/plugin.ts CHANGED
@@ -14,28 +14,92 @@ import { tool as createTool } from "@opencode-ai/plugin";
14
14
 
15
15
  type OpencodeClient = PluginInput["client"];
16
16
 
17
- const SHIP_GUIDANCE = `## Ship CLI Guidance
17
+ const SHIP_GUIDANCE = `## Ship Tool Guidance
18
+
19
+ **IMPORTANT: Always use the \`ship\` tool, NEVER run \`ship\` or \`pnpm ship\` via bash/terminal.**
20
+
21
+ The \`ship\` tool is available for Linear task management. Use it instead of CLI commands.
22
+
23
+ ### Available Actions (via ship tool)
24
+ - \`ready\` - Tasks you can work on (no blockers)
25
+ - \`blocked\` - Tasks waiting on dependencies
26
+ - \`list\` - All tasks (with optional filters)
27
+ - \`show\` - Task details (requires taskId)
28
+ - \`start\` - Begin working on task (requires taskId)
29
+ - \`done\` - Mark task complete (requires taskId)
30
+ - \`create\` - Create new task (requires title)
31
+ - \`update\` - Update task (requires taskId + fields to update)
32
+ - \`block\` - Add blocking relationship (requires blocker + blocked)
33
+ - \`unblock\` - Remove blocking relationship (requires blocker + blocked)
34
+ - \`relate\` - Link tasks as related (requires taskId + relatedTaskId)
35
+ - \`prime\` - Get AI context
36
+ - \`status\` - Check configuration
18
37
 
19
- Ship integrates Linear issue tracking with your development workflow. Use it to:
20
- - View and manage tasks assigned to you
21
- - Track task dependencies (blockers)
22
- - Start/complete work on tasks
23
- - Create new tasks
38
+ ### Best Practices
39
+ 1. Use \`ship\` tool with action \`ready\` to see available work
40
+ 2. Use \`ship\` tool with action \`start\` before beginning work
41
+ 3. Use \`ship\` tool with action \`done\` when completing tasks
42
+ 4. Use \`ship\` tool with action \`block\` for dependency relationships
24
43
 
25
- ### Quick Commands
26
- - \`ship ready\` - Tasks you can work on (no blockers)
27
- - \`ship blocked\` - Tasks waiting on dependencies
28
- - \`ship list\` - All tasks
29
- - \`ship show <ID>\` - Task details
30
- - \`ship start <ID>\` - Begin working on task
31
- - \`ship done <ID>\` - Mark task complete
32
- - \`ship create "title"\` - Create new task
44
+ ### Linear Task Relationships
33
45
 
34
- ### Best Practices
35
- 1. Check \`ship ready\` to see what can be worked on
36
- 2. Use \`ship start\` before beginning work
37
- 3. Use \`ship done\` when completing tasks
38
- 4. Check blockers before starting dependent tasks`;
46
+ Linear has native relationship types. **Always use these instead of writing dependencies in text:**
47
+
48
+ **Blocking (for dependencies):**
49
+ - Use ship tool: action=\`block\`, blocker=\`BRI-100\`, blocked=\`BRI-101\`
50
+ - Use ship tool: action=\`unblock\` to remove relationships
51
+ - Use ship tool: action=\`blocked\` to see waiting tasks
52
+
53
+ **Related (for cross-references):**
54
+ - Use ship tool: action=\`relate\`, taskId=\`BRI-100\`, relatedTaskId=\`BRI-101\`
55
+ - Use this when tasks are conceptually related but not blocking each other
56
+
57
+ **Mentioning Tasks in Descriptions (Clickable Pills):**
58
+ To create clickable task pills in descriptions, use full markdown links:
59
+ \`[BRI-123](https://linear.app/WORKSPACE/issue/BRI-123/slug)\`
60
+
61
+ Get the full URL from ship tool (action=\`show\`, taskId=\`BRI-123\`) and use it in markdown link format.
62
+ Plain text \`BRI-123\` will NOT create clickable pills.
63
+
64
+ ### Task Description Template
65
+
66
+ \`\`\`markdown
67
+ ## Context
68
+ Brief explanation of why this task exists and where it fits.
69
+
70
+ ## Problem Statement
71
+ What specific problem does this task solve? Current vs desired behavior.
72
+
73
+ ## Implementation Notes
74
+ - Key files: \`path/to/file.ts\`
75
+ - Patterns: Reference existing implementations
76
+ - Technical constraints
77
+
78
+ ## Acceptance Criteria
79
+ - [ ] Specific, testable requirement 1
80
+ - [ ] Specific, testable requirement 2
81
+ - [ ] Tests pass
82
+
83
+ ## Out of Scope
84
+ - What NOT to include
85
+
86
+ ## Dependencies
87
+ - Blocked by: [BRI-XXX](url) (brief reason)
88
+ - Blocks: [BRI-YYY](url) (brief reason)
89
+ \`\`\`
90
+
91
+ **Important:**
92
+ 1. Set blocking relationships via ship tool action=\`block\` (appears in Linear sidebar)
93
+ 2. ALSO document in description using markdown links for context
94
+ 3. Get task URLs from ship tool action=\`show\`
95
+
96
+ ### Task Quality Checklist
97
+ - Title is actionable and specific (not "Fix bug" but "Fix null pointer in UserService.getById")
98
+ - Context explains WHY, not just WHAT
99
+ - Acceptance criteria are testable
100
+ - **Dependencies set via \`ship block\`** AND documented with markdown links
101
+ - Links use full URL format: \`[BRI-123](https://linear.app/...)\`
102
+ - Priority reflects business impact (urgent/high/medium/low)`;
39
103
 
40
104
  /**
41
105
  * Get the current model/agent context for a session by querying messages.
@@ -66,46 +130,45 @@ async function getSessionContext(
66
130
  return undefined;
67
131
  }
68
132
 
133
+ /**
134
+ * Get the ship command based on NODE_ENV.
135
+ *
136
+ * - NODE_ENV=development: Use "pnpm ship" (for developing the CLI in this repo)
137
+ * - Otherwise: Use "ship" (globally installed CLI)
138
+ */
139
+ function getShipCommand(): string[] {
140
+ if (process.env.NODE_ENV === "development") {
141
+ return ["pnpm", "ship"];
142
+ }
143
+ return ["ship"];
144
+ }
145
+
69
146
  /**
70
147
  * Inject ship context into a session.
71
148
  *
72
- * Runs `ship prime` and injects the output along with CLI guidance.
73
- * Silently skips if ship is not installed or not initialized.
149
+ * Injects static guidance for using the ship tool. Does NOT fetch live data
150
+ * from Linear - the AI should use the ship tool to get task data.
151
+ * This ensures instant response on first message.
74
152
  */
75
153
  async function injectShipContext(
76
154
  client: OpencodeClient,
77
- $: PluginInput["$"],
78
155
  sessionID: string,
79
156
  context?: { model?: { providerID: string; modelID: string }; agent?: string }
80
157
  ): Promise<void> {
81
158
  try {
82
- // Use quiet() to prevent any output from bleeding into TUI
83
- const primeOutput = await $`ship prime`.quiet().text();
84
-
85
- if (!primeOutput || primeOutput.trim() === "") {
86
- return;
87
- }
88
-
89
- // Wrap the plain markdown output with XML tags (like beads plugin does)
90
- const shipContext = `<ship-context>
91
- ${primeOutput.trim()}
92
- </ship-context>
93
-
94
- ${SHIP_GUIDANCE}`;
95
-
96
- // Inject content via noReply + synthetic
97
- // Must pass model and agent to prevent mode/model switching
159
+ // Inject only the static guidance - no API calls
160
+ // The AI will use the ship tool to fetch live data when needed
98
161
  await client.session.prompt({
99
162
  path: { id: sessionID },
100
163
  body: {
101
164
  noReply: true,
102
165
  model: context?.model,
103
166
  agent: context?.agent,
104
- parts: [{ type: "text", text: shipContext, synthetic: true }],
167
+ parts: [{ type: "text", text: SHIP_GUIDANCE, synthetic: true }],
105
168
  },
106
169
  });
107
170
  } catch {
108
- // Silent skip if ship prime fails (not installed or not initialized)
171
+ // Silent skip on error
109
172
  }
110
173
  }
111
174
 
@@ -117,8 +180,9 @@ async function runShip(
117
180
  args: string[]
118
181
  ): Promise<{ success: boolean; output: string }> {
119
182
  try {
183
+ const cmd = getShipCommand();
120
184
  // Use quiet() to prevent output from bleeding into TUI
121
- const result = await $`ship ${args}`.quiet().nothrow();
185
+ const result = await $`${cmd} ${args}`.quiet().nothrow();
122
186
  const stdout = await new Response(result.stdout).text();
123
187
  const stderr = await new Response(result.stderr).text();
124
188
 
@@ -141,9 +205,10 @@ async function runShip(
141
205
  */
142
206
  async function isShipConfigured($: PluginInput["$"]): Promise<boolean> {
143
207
  try {
208
+ const cmd = getShipCommand();
144
209
  // Try running ship prime - it will fail if not configured
145
210
  // Use quiet() to suppress stdout/stderr from bleeding into TUI
146
- const result = await $`ship prime`.quiet().nothrow();
211
+ const result = await $`${cmd} prime`.quiet().nothrow();
147
212
  return result.exitCode === 0;
148
213
  } catch {
149
214
  return false;
@@ -234,30 +299,36 @@ Run 'ship init' in the terminal first if not configured.`,
234
299
  "start",
235
300
  "done",
236
301
  "create",
302
+ "update",
237
303
  "block",
238
304
  "unblock",
305
+ "relate",
239
306
  "prime",
240
307
  "status",
241
308
  ])
242
309
  .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)"
310
+ "Action to perform: ready (unblocked tasks), list (all tasks), blocked (blocked tasks), show (task details), start (begin task), done (complete task), create (new task), update (modify task), block/unblock (dependencies), relate (link related tasks), prime (AI context), status (current config)"
244
311
  ),
245
312
  taskId: createTool.schema
246
313
  .string()
247
314
  .optional()
248
- .describe("Task identifier (e.g., BRI-123) - required for show, start, done"),
315
+ .describe("Task identifier (e.g., BRI-123) - required for show, start, done, update"),
249
316
  title: createTool.schema
250
317
  .string()
251
318
  .optional()
252
- .describe("Task title - required for create"),
319
+ .describe("Task title - required for create, optional for update"),
253
320
  description: createTool.schema
254
321
  .string()
255
322
  .optional()
256
- .describe("Task description - optional for create"),
323
+ .describe("Task description - optional for create/update"),
257
324
  priority: createTool.schema
258
325
  .enum(["urgent", "high", "medium", "low", "none"])
259
326
  .optional()
260
- .describe("Task priority - optional for create"),
327
+ .describe("Task priority - optional for create/update"),
328
+ status: createTool.schema
329
+ .enum(["backlog", "todo", "in_progress", "in_review", "done", "cancelled"])
330
+ .optional()
331
+ .describe("Task status - optional for update"),
261
332
  blocker: createTool.schema
262
333
  .string()
263
334
  .optional()
@@ -266,6 +337,10 @@ Run 'ship init' in the terminal first if not configured.`,
266
337
  .string()
267
338
  .optional()
268
339
  .describe("Blocked task ID - required for block/unblock"),
340
+ relatedTaskId: createTool.schema
341
+ .string()
342
+ .optional()
343
+ .describe("Related task ID - required for relate (use with taskId)"),
269
344
  filter: createTool.schema
270
345
  .object({
271
346
  status: createTool.schema
@@ -360,7 +435,7 @@ After that, you can use this tool to manage tasks.`;
360
435
  if (!args.taskId) {
361
436
  return "Error: taskId is required for show action";
362
437
  }
363
- const result = await runShip($, ["show", args.taskId, "--json"]);
438
+ const result = await runShip($, ["show", "--json", args.taskId]);
364
439
  if (!result.success) {
365
440
  return `Failed to get task: ${result.output}`;
366
441
  }
@@ -398,22 +473,51 @@ After that, you can use this tool to manage tasks.`;
398
473
  if (!args.title) {
399
474
  return "Error: title is required for create action";
400
475
  }
401
- const createArgs = ["create", args.title, "--json"];
476
+ const createArgs = ["create", "--json"];
402
477
  if (args.description) createArgs.push("--description", args.description);
403
478
  if (args.priority) createArgs.push("--priority", args.priority);
479
+ createArgs.push(args.title);
404
480
 
405
481
  const result = await runShip($, createArgs);
406
482
  if (!result.success) {
407
483
  return `Failed to create task: ${result.output}`;
408
484
  }
409
485
  try {
410
- const task = JSON.parse(result.output);
486
+ const response = JSON.parse(result.output);
487
+ const task = response.task;
411
488
  return `Created task ${task.identifier}: ${task.title}\nURL: ${task.url}`;
412
489
  } catch {
413
490
  return result.output;
414
491
  }
415
492
  }
416
493
 
494
+ case "update": {
495
+ if (!args.taskId) {
496
+ return "Error: taskId is required for update action";
497
+ }
498
+ if (!args.title && !args.description && !args.priority && !args.status) {
499
+ return "Error: at least one of title, description, priority, or status is required for update";
500
+ }
501
+ const updateArgs = ["update", "--json"];
502
+ if (args.title) updateArgs.push("--title", args.title);
503
+ if (args.description) updateArgs.push("--description", args.description);
504
+ if (args.priority) updateArgs.push("--priority", args.priority);
505
+ if (args.status) updateArgs.push("--status", args.status);
506
+ updateArgs.push(args.taskId);
507
+
508
+ const result = await runShip($, updateArgs);
509
+ if (!result.success) {
510
+ return `Failed to update task: ${result.output}`;
511
+ }
512
+ try {
513
+ const response = JSON.parse(result.output);
514
+ const task = response.task;
515
+ return `Updated task ${task.identifier}: ${task.title}\nURL: ${task.url}`;
516
+ } catch {
517
+ return result.output;
518
+ }
519
+ }
520
+
417
521
  case "block": {
418
522
  if (!args.blocker || !args.blocked) {
419
523
  return "Error: both blocker and blocked task IDs are required";
@@ -436,6 +540,17 @@ After that, you can use this tool to manage tasks.`;
436
540
  return `Removed ${args.blocker} as blocker of ${args.blocked}`;
437
541
  }
438
542
 
543
+ case "relate": {
544
+ if (!args.taskId || !args.relatedTaskId) {
545
+ return "Error: both taskId and relatedTaskId are required for relate action";
546
+ }
547
+ const result = await runShip($, ["relate", args.taskId, args.relatedTaskId]);
548
+ if (!result.success) {
549
+ return `Failed to relate tasks: ${result.output}`;
550
+ }
551
+ return `Linked ${args.taskId} ↔ ${args.relatedTaskId} as related`;
552
+ }
553
+
439
554
  case "prime": {
440
555
  const result = await runShip($, ["prime"]);
441
556
  if (!result.success) {
@@ -454,6 +569,24 @@ After that, you can use this tool to manage tasks.`;
454
569
  /**
455
570
  * Ship OpenCode Plugin
456
571
  */
572
+ // Pre-define commands (loaded at plugin init, not lazily in config hook)
573
+ const SHIP_COMMANDS = {
574
+ ready: {
575
+ description: "Find ready-to-work tasks with no blockers",
576
+ template: `Use the \`ship\` tool with action \`ready\` to find tasks that are ready to work on (no blocking dependencies).
577
+
578
+ Present the results in a clear format showing:
579
+ - Task ID (e.g., BRI-123)
580
+ - Title
581
+ - Priority
582
+ - URL
583
+
584
+ If there are ready tasks, ask the user which one they'd like to work on. If they choose one, use the \`ship\` tool with action \`start\` to begin work on it.
585
+
586
+ If there are no ready tasks, suggest checking blocked tasks (action \`blocked\`) or creating a new task (action \`create\`).`,
587
+ },
588
+ };
589
+
457
590
  export const ShipPlugin: Plugin = async ({ client, $ }) => {
458
591
  const injectedSessions = new Set<string>();
459
592
 
@@ -495,7 +628,7 @@ export const ShipPlugin: Plugin = async ({ client, $ }) => {
495
628
  injectedSessions.add(sessionID);
496
629
 
497
630
  // Use output.message which has the resolved model/agent values
498
- await injectShipContext(client, $, sessionID, {
631
+ await injectShipContext(client, sessionID, {
499
632
  model: output.message.model,
500
633
  agent: output.message.agent,
501
634
  });
@@ -505,10 +638,15 @@ export const ShipPlugin: Plugin = async ({ client, $ }) => {
505
638
  if (event.type === "session.compacted") {
506
639
  const sessionID = event.properties.sessionID;
507
640
  const context = await getSessionContext(client, sessionID);
508
- await injectShipContext(client, $, sessionID, context);
641
+ await injectShipContext(client, sessionID, context);
509
642
  }
510
643
  },
511
644
 
645
+ config: async (config) => {
646
+ // Register commands (using pre-defined SHIP_COMMANDS for reliability)
647
+ config.command = { ...config.command, ...SHIP_COMMANDS };
648
+ },
649
+
512
650
  // Register the ship tool
513
651
  tool: {
514
652
  ship: shipTool,