@ship-cli/opencode 0.0.3 → 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 +192 -51
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,45 +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
|
-
if (!primeOutput || primeOutput.trim() === "") {
|
|
85
|
-
return;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Wrap the plain markdown output with XML tags (like beads plugin does)
|
|
89
|
-
const shipContext = `<ship-context>
|
|
90
|
-
${primeOutput.trim()}
|
|
91
|
-
</ship-context>
|
|
92
|
-
|
|
93
|
-
${SHIP_GUIDANCE}`;
|
|
94
|
-
|
|
95
|
-
// Inject content via noReply + synthetic
|
|
96
|
-
// 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
|
|
97
161
|
await client.session.prompt({
|
|
98
162
|
path: { id: sessionID },
|
|
99
163
|
body: {
|
|
100
164
|
noReply: true,
|
|
101
165
|
model: context?.model,
|
|
102
166
|
agent: context?.agent,
|
|
103
|
-
parts: [{ type: "text", text:
|
|
167
|
+
parts: [{ type: "text", text: SHIP_GUIDANCE, synthetic: true }],
|
|
104
168
|
},
|
|
105
169
|
});
|
|
106
170
|
} catch {
|
|
107
|
-
// Silent skip
|
|
171
|
+
// Silent skip on error
|
|
108
172
|
}
|
|
109
173
|
}
|
|
110
174
|
|
|
@@ -116,7 +180,9 @@ async function runShip(
|
|
|
116
180
|
args: string[]
|
|
117
181
|
): Promise<{ success: boolean; output: string }> {
|
|
118
182
|
try {
|
|
119
|
-
const
|
|
183
|
+
const cmd = getShipCommand();
|
|
184
|
+
// Use quiet() to prevent output from bleeding into TUI
|
|
185
|
+
const result = await $`${cmd} ${args}`.quiet().nothrow();
|
|
120
186
|
const stdout = await new Response(result.stdout).text();
|
|
121
187
|
const stderr = await new Response(result.stderr).text();
|
|
122
188
|
|
|
@@ -139,8 +205,10 @@ async function runShip(
|
|
|
139
205
|
*/
|
|
140
206
|
async function isShipConfigured($: PluginInput["$"]): Promise<boolean> {
|
|
141
207
|
try {
|
|
208
|
+
const cmd = getShipCommand();
|
|
142
209
|
// Try running ship prime - it will fail if not configured
|
|
143
|
-
|
|
210
|
+
// Use quiet() to suppress stdout/stderr from bleeding into TUI
|
|
211
|
+
const result = await $`${cmd} prime`.quiet().nothrow();
|
|
144
212
|
return result.exitCode === 0;
|
|
145
213
|
} catch {
|
|
146
214
|
return false;
|
|
@@ -231,30 +299,36 @@ Run 'ship init' in the terminal first if not configured.`,
|
|
|
231
299
|
"start",
|
|
232
300
|
"done",
|
|
233
301
|
"create",
|
|
302
|
+
"update",
|
|
234
303
|
"block",
|
|
235
304
|
"unblock",
|
|
305
|
+
"relate",
|
|
236
306
|
"prime",
|
|
237
307
|
"status",
|
|
238
308
|
])
|
|
239
309
|
.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)"
|
|
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)"
|
|
241
311
|
),
|
|
242
312
|
taskId: createTool.schema
|
|
243
313
|
.string()
|
|
244
314
|
.optional()
|
|
245
|
-
.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"),
|
|
246
316
|
title: createTool.schema
|
|
247
317
|
.string()
|
|
248
318
|
.optional()
|
|
249
|
-
.describe("Task title - required for create"),
|
|
319
|
+
.describe("Task title - required for create, optional for update"),
|
|
250
320
|
description: createTool.schema
|
|
251
321
|
.string()
|
|
252
322
|
.optional()
|
|
253
|
-
.describe("Task description - optional for create"),
|
|
323
|
+
.describe("Task description - optional for create/update"),
|
|
254
324
|
priority: createTool.schema
|
|
255
325
|
.enum(["urgent", "high", "medium", "low", "none"])
|
|
256
326
|
.optional()
|
|
257
|
-
.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"),
|
|
258
332
|
blocker: createTool.schema
|
|
259
333
|
.string()
|
|
260
334
|
.optional()
|
|
@@ -263,6 +337,10 @@ Run 'ship init' in the terminal first if not configured.`,
|
|
|
263
337
|
.string()
|
|
264
338
|
.optional()
|
|
265
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)"),
|
|
266
344
|
filter: createTool.schema
|
|
267
345
|
.object({
|
|
268
346
|
status: createTool.schema
|
|
@@ -357,7 +435,7 @@ After that, you can use this tool to manage tasks.`;
|
|
|
357
435
|
if (!args.taskId) {
|
|
358
436
|
return "Error: taskId is required for show action";
|
|
359
437
|
}
|
|
360
|
-
const result = await runShip($, ["show",
|
|
438
|
+
const result = await runShip($, ["show", "--json", args.taskId]);
|
|
361
439
|
if (!result.success) {
|
|
362
440
|
return `Failed to get task: ${result.output}`;
|
|
363
441
|
}
|
|
@@ -395,22 +473,51 @@ After that, you can use this tool to manage tasks.`;
|
|
|
395
473
|
if (!args.title) {
|
|
396
474
|
return "Error: title is required for create action";
|
|
397
475
|
}
|
|
398
|
-
const createArgs = ["create",
|
|
476
|
+
const createArgs = ["create", "--json"];
|
|
399
477
|
if (args.description) createArgs.push("--description", args.description);
|
|
400
478
|
if (args.priority) createArgs.push("--priority", args.priority);
|
|
479
|
+
createArgs.push(args.title);
|
|
401
480
|
|
|
402
481
|
const result = await runShip($, createArgs);
|
|
403
482
|
if (!result.success) {
|
|
404
483
|
return `Failed to create task: ${result.output}`;
|
|
405
484
|
}
|
|
406
485
|
try {
|
|
407
|
-
const
|
|
486
|
+
const response = JSON.parse(result.output);
|
|
487
|
+
const task = response.task;
|
|
408
488
|
return `Created task ${task.identifier}: ${task.title}\nURL: ${task.url}`;
|
|
409
489
|
} catch {
|
|
410
490
|
return result.output;
|
|
411
491
|
}
|
|
412
492
|
}
|
|
413
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
|
+
|
|
414
521
|
case "block": {
|
|
415
522
|
if (!args.blocker || !args.blocked) {
|
|
416
523
|
return "Error: both blocker and blocked task IDs are required";
|
|
@@ -433,6 +540,17 @@ After that, you can use this tool to manage tasks.`;
|
|
|
433
540
|
return `Removed ${args.blocker} as blocker of ${args.blocked}`;
|
|
434
541
|
}
|
|
435
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
|
+
|
|
436
554
|
case "prime": {
|
|
437
555
|
const result = await runShip($, ["prime"]);
|
|
438
556
|
if (!result.success) {
|
|
@@ -451,6 +569,24 @@ After that, you can use this tool to manage tasks.`;
|
|
|
451
569
|
/**
|
|
452
570
|
* Ship OpenCode Plugin
|
|
453
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
|
+
|
|
454
590
|
export const ShipPlugin: Plugin = async ({ client, $ }) => {
|
|
455
591
|
const injectedSessions = new Set<string>();
|
|
456
592
|
|
|
@@ -492,7 +628,7 @@ export const ShipPlugin: Plugin = async ({ client, $ }) => {
|
|
|
492
628
|
injectedSessions.add(sessionID);
|
|
493
629
|
|
|
494
630
|
// Use output.message which has the resolved model/agent values
|
|
495
|
-
await injectShipContext(client,
|
|
631
|
+
await injectShipContext(client, sessionID, {
|
|
496
632
|
model: output.message.model,
|
|
497
633
|
agent: output.message.agent,
|
|
498
634
|
});
|
|
@@ -502,10 +638,15 @@ export const ShipPlugin: Plugin = async ({ client, $ }) => {
|
|
|
502
638
|
if (event.type === "session.compacted") {
|
|
503
639
|
const sessionID = event.properties.sessionID;
|
|
504
640
|
const context = await getSessionContext(client, sessionID);
|
|
505
|
-
await injectShipContext(client,
|
|
641
|
+
await injectShipContext(client, sessionID, context);
|
|
506
642
|
}
|
|
507
643
|
},
|
|
508
644
|
|
|
645
|
+
config: async (config) => {
|
|
646
|
+
// Register commands (using pre-defined SHIP_COMMANDS for reliability)
|
|
647
|
+
config.command = { ...config.command, ...SHIP_COMMANDS };
|
|
648
|
+
},
|
|
649
|
+
|
|
509
650
|
// Register the ship tool
|
|
510
651
|
tool: {
|
|
511
652
|
ship: shipTool,
|