@ship-cli/opencode 0.0.1
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/LICENSE +21 -0
- package/package.json +43 -0
- package/src/plugin.ts +511 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024-present <PLACEHOLDER>
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ship-cli/opencode",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "OpenCode plugin for Ship - Linear task management integration",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"main": "src/plugin.ts",
|
|
8
|
+
"files": [
|
|
9
|
+
"src",
|
|
10
|
+
"README.md"
|
|
11
|
+
],
|
|
12
|
+
"keywords": [
|
|
13
|
+
"opencode",
|
|
14
|
+
"plugin",
|
|
15
|
+
"ship",
|
|
16
|
+
"linear",
|
|
17
|
+
"task-management",
|
|
18
|
+
"ai"
|
|
19
|
+
],
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/user/ship",
|
|
23
|
+
"directory": "packages/opencode-plugin"
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@opencode-ai/plugin": "^1.0.143",
|
|
30
|
+
"@opencode-ai/sdk": "^1.0.143"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@types/bun": "latest",
|
|
34
|
+
"@types/node": "^22.0.0",
|
|
35
|
+
"typescript": "~5.6.2"
|
|
36
|
+
},
|
|
37
|
+
"engines": {
|
|
38
|
+
"bun": ">=1.0.0"
|
|
39
|
+
},
|
|
40
|
+
"scripts": {
|
|
41
|
+
"typecheck": "tsc --noEmit"
|
|
42
|
+
}
|
|
43
|
+
}
|
package/src/plugin.ts
ADDED
|
@@ -0,0 +1,511 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ship OpenCode Plugin
|
|
3
|
+
*
|
|
4
|
+
* Integrates the Ship CLI (Linear task management) with OpenCode.
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Context injection via `ship prime` on session start and after compaction
|
|
8
|
+
* - Ship tool for task management operations
|
|
9
|
+
* - Task agent for autonomous issue completion
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { Plugin, PluginInput } from "@opencode-ai/plugin";
|
|
13
|
+
import { tool as createTool } from "@opencode-ai/plugin";
|
|
14
|
+
|
|
15
|
+
type OpencodeClient = PluginInput["client"];
|
|
16
|
+
|
|
17
|
+
const SHIP_GUIDANCE = `## Ship CLI Guidance
|
|
18
|
+
|
|
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
|
|
24
|
+
|
|
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
|
|
33
|
+
|
|
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`;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get the current model/agent context for a session by querying messages.
|
|
42
|
+
*/
|
|
43
|
+
async function getSessionContext(
|
|
44
|
+
client: OpencodeClient,
|
|
45
|
+
sessionID: string
|
|
46
|
+
): Promise<
|
|
47
|
+
{ model?: { providerID: string; modelID: string }; agent?: string } | undefined
|
|
48
|
+
> {
|
|
49
|
+
try {
|
|
50
|
+
const response = await client.session.messages({
|
|
51
|
+
path: { id: sessionID },
|
|
52
|
+
query: { limit: 50 },
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
if (response.data) {
|
|
56
|
+
for (const msg of response.data) {
|
|
57
|
+
if (msg.info.role === "user" && "model" in msg.info && msg.info.model) {
|
|
58
|
+
return { model: msg.info.model, agent: msg.info.agent };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} catch {
|
|
63
|
+
// On error, return undefined (let opencode use its default)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Inject ship context into a session.
|
|
71
|
+
*
|
|
72
|
+
* Runs `ship prime` and injects the output along with CLI guidance.
|
|
73
|
+
* Silently skips if ship is not installed or not initialized.
|
|
74
|
+
*/
|
|
75
|
+
async function injectShipContext(
|
|
76
|
+
client: OpencodeClient,
|
|
77
|
+
$: PluginInput["$"],
|
|
78
|
+
sessionID: string,
|
|
79
|
+
context?: { model?: { providerID: string; modelID: string }; agent?: string }
|
|
80
|
+
): Promise<void> {
|
|
81
|
+
try {
|
|
82
|
+
const primeOutput = await $`ship prime`.text();
|
|
83
|
+
|
|
84
|
+
if (!primeOutput || primeOutput.trim() === "") {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const shipContext = `<ship-context>
|
|
89
|
+
${primeOutput.trim()}
|
|
90
|
+
</ship-context>
|
|
91
|
+
|
|
92
|
+
${SHIP_GUIDANCE}`;
|
|
93
|
+
|
|
94
|
+
// Inject content via noReply + synthetic
|
|
95
|
+
// Must pass model and agent to prevent mode/model switching
|
|
96
|
+
await client.session.prompt({
|
|
97
|
+
path: { id: sessionID },
|
|
98
|
+
body: {
|
|
99
|
+
noReply: true,
|
|
100
|
+
model: context?.model,
|
|
101
|
+
agent: context?.agent,
|
|
102
|
+
parts: [{ type: "text", text: shipContext, synthetic: true }],
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
} catch {
|
|
106
|
+
// Silent skip if ship prime fails (not installed or not initialized)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Execute ship CLI command and return output
|
|
112
|
+
*/
|
|
113
|
+
async function runShip(
|
|
114
|
+
$: PluginInput["$"],
|
|
115
|
+
args: string[]
|
|
116
|
+
): Promise<{ success: boolean; output: string }> {
|
|
117
|
+
try {
|
|
118
|
+
const result = await $`ship ${args}`.nothrow();
|
|
119
|
+
const stdout = await new Response(result.stdout).text();
|
|
120
|
+
const stderr = await new Response(result.stderr).text();
|
|
121
|
+
|
|
122
|
+
if (result.exitCode !== 0) {
|
|
123
|
+
return { success: false, output: stderr || stdout };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { success: true, output: stdout };
|
|
127
|
+
} catch (error) {
|
|
128
|
+
return {
|
|
129
|
+
success: false,
|
|
130
|
+
output: `Failed to run ship: ${error instanceof Error ? error.message : String(error)}`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Check if ship is configured in the given directory
|
|
137
|
+
*/
|
|
138
|
+
async function isShipConfigured($: PluginInput["$"]): Promise<boolean> {
|
|
139
|
+
try {
|
|
140
|
+
const result = await $`test -f .ship/config.yaml`.nothrow();
|
|
141
|
+
return result.exitCode === 0;
|
|
142
|
+
} catch {
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Format a list of tasks for display
|
|
149
|
+
*/
|
|
150
|
+
function formatTaskList(
|
|
151
|
+
tasks: Array<{
|
|
152
|
+
identifier: string;
|
|
153
|
+
title: string;
|
|
154
|
+
priority: string;
|
|
155
|
+
status: string;
|
|
156
|
+
url: string;
|
|
157
|
+
}>
|
|
158
|
+
): string {
|
|
159
|
+
return tasks
|
|
160
|
+
.map((t) => {
|
|
161
|
+
const priority =
|
|
162
|
+
t.priority === "urgent"
|
|
163
|
+
? "[!]"
|
|
164
|
+
: t.priority === "high"
|
|
165
|
+
? "[^]"
|
|
166
|
+
: " ";
|
|
167
|
+
return `${priority} ${t.identifier.padEnd(10)} ${t.status.padEnd(12)} ${t.title}`;
|
|
168
|
+
})
|
|
169
|
+
.join("\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Format task details for display
|
|
174
|
+
*/
|
|
175
|
+
function formatTaskDetails(task: {
|
|
176
|
+
identifier: string;
|
|
177
|
+
title: string;
|
|
178
|
+
description?: string;
|
|
179
|
+
priority: string;
|
|
180
|
+
status: string;
|
|
181
|
+
labels: string[];
|
|
182
|
+
url: string;
|
|
183
|
+
branchName?: string;
|
|
184
|
+
}): string {
|
|
185
|
+
let output = `# ${task.identifier}: ${task.title}
|
|
186
|
+
|
|
187
|
+
**Status:** ${task.status}
|
|
188
|
+
**Priority:** ${task.priority}
|
|
189
|
+
**Labels:** ${task.labels.length > 0 ? task.labels.join(", ") : "none"}
|
|
190
|
+
**URL:** ${task.url}`;
|
|
191
|
+
|
|
192
|
+
if (task.branchName) {
|
|
193
|
+
output += `\n**Branch:** ${task.branchName}`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (task.description) {
|
|
197
|
+
output += `\n\n## Description\n\n${task.description}`;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return output;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Ship tool for task management
|
|
205
|
+
*/
|
|
206
|
+
const shipTool = createTool({
|
|
207
|
+
description: `Linear task management for the current project.
|
|
208
|
+
|
|
209
|
+
Use this tool to:
|
|
210
|
+
- List tasks ready to work on (no blockers)
|
|
211
|
+
- View task details
|
|
212
|
+
- Start/complete tasks
|
|
213
|
+
- Create new tasks
|
|
214
|
+
- Manage task dependencies (blocking relationships)
|
|
215
|
+
- Get AI-optimized context about current work
|
|
216
|
+
|
|
217
|
+
Requires ship to be configured in the project (.ship/config.yaml).
|
|
218
|
+
Run 'ship init' in the terminal first if not configured.`,
|
|
219
|
+
|
|
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.
|
|
282
|
+
|
|
283
|
+
Run 'ship init' in the terminal to:
|
|
284
|
+
1. Authenticate with Linear (paste your API key from https://linear.app/settings/api)
|
|
285
|
+
2. Select your team
|
|
286
|
+
3. Optionally select a project
|
|
287
|
+
|
|
288
|
+
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
|
+
}
|
|
298
|
+
return "Ship is configured in this project.";
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
case "ready": {
|
|
302
|
+
const result = await runShip($, ["ready", "--json"]);
|
|
303
|
+
if (!result.success) {
|
|
304
|
+
return `Failed to get ready tasks: ${result.output}`;
|
|
305
|
+
}
|
|
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).";
|
|
310
|
+
}
|
|
311
|
+
return `Ready tasks (no blockers):\n\n${formatTaskList(tasks)}`;
|
|
312
|
+
} catch {
|
|
313
|
+
return result.output;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
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");
|
|
322
|
+
|
|
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.";
|
|
331
|
+
}
|
|
332
|
+
return `Tasks:\n\n${formatTaskList(tasks)}`;
|
|
333
|
+
} catch {
|
|
334
|
+
return result.output;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
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.";
|
|
347
|
+
}
|
|
348
|
+
return `Blocked tasks:\n\n${formatTaskList(tasks)}`;
|
|
349
|
+
} catch {
|
|
350
|
+
return result.output;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
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;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
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}`;
|
|
377
|
+
}
|
|
378
|
+
return `Started working on ${args.taskId}`;
|
|
379
|
+
}
|
|
380
|
+
|
|
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}`;
|
|
388
|
+
}
|
|
389
|
+
return `Completed ${args.taskId}`;
|
|
390
|
+
}
|
|
391
|
+
|
|
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);
|
|
399
|
+
|
|
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;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
|
|
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}`;
|
|
419
|
+
}
|
|
420
|
+
return `${args.blocker} now blocks ${args.blocked}`;
|
|
421
|
+
}
|
|
422
|
+
|
|
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}`;
|
|
430
|
+
}
|
|
431
|
+
return `Removed ${args.blocker} as blocker of ${args.blocked}`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
case "prime": {
|
|
435
|
+
const result = await runShip($, ["prime"]);
|
|
436
|
+
if (!result.success) {
|
|
437
|
+
return `Failed to get context: ${result.output}`;
|
|
438
|
+
}
|
|
439
|
+
return result.output;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
default:
|
|
443
|
+
return `Unknown action: ${args.action}`;
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Ship OpenCode Plugin
|
|
450
|
+
*/
|
|
451
|
+
export const ShipPlugin: Plugin = async ({ client, $ }) => {
|
|
452
|
+
const injectedSessions = new Set<string>();
|
|
453
|
+
|
|
454
|
+
return {
|
|
455
|
+
"chat.message": async (_input, output) => {
|
|
456
|
+
const sessionID = output.message.sessionID;
|
|
457
|
+
|
|
458
|
+
// Skip if already injected this session
|
|
459
|
+
if (injectedSessions.has(sessionID)) return;
|
|
460
|
+
|
|
461
|
+
// Check if ship-context was already injected (handles plugin reload/reconnection)
|
|
462
|
+
try {
|
|
463
|
+
const existing = await client.session.messages({
|
|
464
|
+
path: { id: sessionID },
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
if (existing.data) {
|
|
468
|
+
const hasShipContext = existing.data.some((msg) => {
|
|
469
|
+
const parts = (msg as any).parts || (msg.info as any).parts;
|
|
470
|
+
if (!parts) return false;
|
|
471
|
+
return parts.some(
|
|
472
|
+
(part: any) =>
|
|
473
|
+
part.type === "text" && part.text?.includes("<ship-context>")
|
|
474
|
+
);
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
if (hasShipContext) {
|
|
478
|
+
injectedSessions.add(sessionID);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
// On error, proceed with injection
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
injectedSessions.add(sessionID);
|
|
487
|
+
|
|
488
|
+
// Use output.message which has the resolved model/agent values
|
|
489
|
+
await injectShipContext(client, $, sessionID, {
|
|
490
|
+
model: output.message.model,
|
|
491
|
+
agent: output.message.agent,
|
|
492
|
+
});
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
event: async ({ event }) => {
|
|
496
|
+
if (event.type === "session.compacted") {
|
|
497
|
+
const sessionID = event.properties.sessionID;
|
|
498
|
+
const context = await getSessionContext(client, sessionID);
|
|
499
|
+
await injectShipContext(client, $, sessionID, context);
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
|
|
503
|
+
// Register the ship tool
|
|
504
|
+
tool: {
|
|
505
|
+
ship: shipTool,
|
|
506
|
+
},
|
|
507
|
+
};
|
|
508
|
+
};
|
|
509
|
+
|
|
510
|
+
// Default export for OpenCode
|
|
511
|
+
export default ShipPlugin;
|