@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.
- package/package.json +1 -1
- package/src/plugin.ts +190 -52
package/package.json
CHANGED
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
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
*
|
|
73
|
-
*
|
|
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
|
-
//
|
|
83
|
-
|
|
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:
|
|
167
|
+
parts: [{ type: "text", text: SHIP_GUIDANCE, synthetic: true }],
|
|
105
168
|
},
|
|
106
169
|
});
|
|
107
170
|
} catch {
|
|
108
|
-
// Silent skip
|
|
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
|
|
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
|
|
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",
|
|
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",
|
|
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
|
|
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,
|
|
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,
|
|
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,
|