@opencode-cockpit/shell 0.1.5 → 0.2.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/README.md +105 -10
- package/dist/agent/plugin.js +180 -0
- package/dist/agent/tools/index.js +23 -0
- package/dist/agent/tools/list.js +68 -0
- package/dist/agent/tools/read.js +59 -0
- package/dist/agent/tools/restart.js +44 -0
- package/dist/agent/tools/send.js +71 -0
- package/dist/agent/tools/shared.js +94 -0
- package/dist/agent/tools/start.js +152 -0
- package/dist/agent/tools/stop.js +42 -0
- package/dist/agent/tools/wait.js +79 -0
- package/dist/agent/tools/watch-args.js +60 -0
- package/dist/agent/tools/watch.js +72 -0
- package/dist/core/config.js +97 -0
- package/dist/{tools → core}/find.js +3 -0
- package/dist/{tools → core}/format.js +27 -2
- package/dist/core/kind.js +31 -0
- package/dist/server.js +2 -152
- package/dist/tui/{badge.js → components/badge.js} +1 -1
- package/dist/tui/{console.js → components/console.js} +270 -139
- package/dist/tui/{dock.js → components/dock.js} +75 -19
- package/dist/tui/{sidebar.js → components/sidebar.js} +16 -1
- package/dist/tui/dialogs.js +163 -0
- package/dist/tui/index.js +35 -94
- package/dist/tui/lib/details.js +23 -0
- package/dist/tui/lib/search.js +39 -0
- package/dist/tui/lib/update.js +58 -0
- package/dist/tui/{view.js → lib/view.js} +55 -2
- package/dist/tui/{store.js → state/store.js} +4 -2
- package/package.json +4 -5
- package/types/agent/plugin.d.ts +12 -0
- package/types/agent/tools/index.d.ts +5 -0
- package/types/agent/tools/list.d.ts +3 -0
- package/types/agent/tools/read.d.ts +3 -0
- package/types/agent/tools/restart.d.ts +3 -0
- package/types/agent/tools/send.d.ts +3 -0
- package/types/agent/tools/shared.d.ts +40 -0
- package/types/agent/tools/start.d.ts +3 -0
- package/types/agent/tools/stop.d.ts +3 -0
- package/types/agent/tools/wait.d.ts +3 -0
- package/types/agent/tools/watch-args.d.ts +18 -0
- package/types/agent/tools/watch.d.ts +3 -0
- package/types/core/config.d.ts +60 -0
- package/types/{tools → core}/find.d.ts +5 -0
- package/types/core/kind.d.ts +9 -0
- package/types/server.d.ts +2 -12
- package/types/tui/{console.d.ts → components/console.d.ts} +7 -1
- package/types/tui/{dock.d.ts → components/dock.d.ts} +3 -1
- package/types/tui/{sidebar.d.ts → components/sidebar.d.ts} +1 -1
- package/types/tui/dialogs.d.ts +10 -0
- package/types/tui/index.d.ts +3 -9
- package/types/tui/lib/details.d.ts +3 -0
- package/types/tui/lib/search.d.ts +7 -0
- package/types/tui/lib/update.d.ts +25 -0
- package/types/tui/{view.d.ts → lib/view.d.ts} +16 -1
- package/types/tui/{store.d.ts → state/store.d.ts} +9 -0
- package/dist/tools/index.js +0 -433
- package/types/tools/index.d.ts +0 -17
- /package/dist/{tools → agent/tools}/keys.js +0 -0
- /package/dist/tui/{keys.js → lib/keys.js} +0 -0
- /package/types/{tools → agent/tools}/keys.d.ts +0 -0
- /package/types/{tools → core}/format.d.ts +0 -0
- /package/types/tui/{badge.d.ts → components/badge.d.ts} +0 -0
- /package/types/tui/{keys.d.ts → lib/keys.d.ts} +0 -0
package/dist/tools/index.js
DELETED
|
@@ -1,433 +0,0 @@
|
|
|
1
|
-
import { tool } from "@opencode-ai/plugin";
|
|
2
|
-
import { RpcError } from "@opencode-cockpit/protocol";
|
|
3
|
-
import { commandOf, filterShells, matchByName } from "./find.js";
|
|
4
|
-
import { describeStatus, formatLines, formatRead, formatWait, header } from "./format.js";
|
|
5
|
-
import { encodeKey, KEY_NAMES } from "./keys.js";
|
|
6
|
-
const z = tool.schema;
|
|
7
|
-
/** Every per-shell tool takes either an id or a name. */
|
|
8
|
-
const TARGET = {
|
|
9
|
-
id: z.string().optional().describe("Shell id from shell_start or shell_list, e.g. sh_ab12cd34"),
|
|
10
|
-
name: z.string().optional().describe('Instead of id: the shell\'s name (the description it was started with), e.g. "DB Monitoring". Partial names and command text also match.')
|
|
11
|
-
};
|
|
12
|
-
const START = `Start a command in a background terminal (PTY) that keeps running while you continue working.
|
|
13
|
-
|
|
14
|
-
Use this instead of bash for anything long-running or interactive:
|
|
15
|
-
- dev servers, watchers (tsc --watch, vitest), local APIs, databases, tunnels
|
|
16
|
-
- builds or test suites that take more than ~30 seconds
|
|
17
|
-
- REPLs and prompts that need input later (use shell_send)
|
|
18
|
-
|
|
19
|
-
Do not append "&" or use nohup; the shell already runs in the background.
|
|
20
|
-
|
|
21
|
-
Readiness: pass waitFor to block until the process is actually ready, for example
|
|
22
|
-
waitFor={ port: 3000 } for a dev server or waitFor={ pattern: "compiled successfully" }.
|
|
23
|
-
Without waitFor the call returns after the first moment of quiet with the initial output.
|
|
24
|
-
|
|
25
|
-
You are notified automatically when the process exits (disable with notifyOnExit=false). Never
|
|
26
|
-
sleep and poll: use shell_wait to block on a condition, and shell_read(after=cursor) for new output.`;
|
|
27
|
-
const READ = `Read a background shell's output.
|
|
28
|
-
|
|
29
|
-
- Default: the last lines of the log (colours removed, progress-bar redraws collapsed).
|
|
30
|
-
- after=<cursor>: only lines newer than a cursor returned by a previous call. Use this to follow output.
|
|
31
|
-
- grep=<regex>: only matching lines (e.g. "error|warn").
|
|
32
|
-
- view="screen": what the terminal shows right now. Use for full-screen programs (htop, vitest UI, prompts that redraw).`;
|
|
33
|
-
const SEND = `Send input to a running background shell, then return the output it produced.
|
|
34
|
-
|
|
35
|
-
- text: literal characters. Set submit=true to press enter afterwards.
|
|
36
|
-
- keys: named keys pressed in order, e.g. ["ctrl+c"], ["down", "enter"]. Supported: ${KEY_NAMES.join(", ")}.`;
|
|
37
|
-
const WAIT = `Block until a condition holds in a background shell. This is the only correct way to wait:
|
|
38
|
-
never sleep and poll.
|
|
39
|
-
|
|
40
|
-
Conditions (combine freely; the first to happen wins, and the process exiting always ends the wait):
|
|
41
|
-
- pattern: regex matched against output lines (also matches an unfinished prompt line)
|
|
42
|
-
- port: something accepts TCP connections on this port
|
|
43
|
-
- idleSeconds: no output for this long (often means waiting for input or finished a step)
|
|
44
|
-
- exit: the process ends
|
|
45
|
-
|
|
46
|
-
Pattern matching includes output produced before this call in the current run, so "wait until ready"
|
|
47
|
-
succeeds immediately if it is already ready.`;
|
|
48
|
-
export function createTools(deps) {
|
|
49
|
-
const {
|
|
50
|
-
client
|
|
51
|
-
} = deps;
|
|
52
|
-
const peek = async (info, tail = 30) => {
|
|
53
|
-
const current = await client.call("shell.get", {
|
|
54
|
-
id: info.id
|
|
55
|
-
});
|
|
56
|
-
const page = await client.call("shell.read", {
|
|
57
|
-
id: info.id,
|
|
58
|
-
tail
|
|
59
|
-
});
|
|
60
|
-
return formatRead(current, page);
|
|
61
|
-
};
|
|
62
|
-
const sessionLabel = async (s, ctx) => {
|
|
63
|
-
const session = s.owner.session;
|
|
64
|
-
if (!session) return "started by the user";
|
|
65
|
-
if (session === ctx.sessionID) return "this session";
|
|
66
|
-
const title = await deps.sessionTitle?.(session).catch(() => undefined);
|
|
67
|
-
return title ? `session "${title}"` : `another session (${session})`;
|
|
68
|
-
};
|
|
69
|
-
|
|
70
|
-
/** Turns `{ id }` or `{ name }` into a shell id, or explains why it cannot. */
|
|
71
|
-
const resolve = async (args, ctx) => {
|
|
72
|
-
if (args.id) return {
|
|
73
|
-
id: args.id,
|
|
74
|
-
note: ""
|
|
75
|
-
};
|
|
76
|
-
if (!args.name) throw new Error("pass the shell's id or name");
|
|
77
|
-
const shells = await client.call("shell.list", {
|
|
78
|
-
owner: {
|
|
79
|
-
project: ctx.directory
|
|
80
|
-
}
|
|
81
|
-
});
|
|
82
|
-
const match = matchByName(shells, args.name);
|
|
83
|
-
const describe = async list => (await Promise.all(list.map(async s => `- ${s.id} "${s.title}" · ${s.status} · ${await sessionLabel(s, ctx)} · $ ${commandOf(s).slice(0, 80)}`))).join("\n");
|
|
84
|
-
if (match.kind === "found") {
|
|
85
|
-
const note = match.alsoMatched.length > 0 ? `(name "${args.name}" also matched ${match.alsoMatched.length} finished shell${match.alsoMatched.length === 1 ? "" : "s"}; using the running one, ${match.shell.id})\n` : "";
|
|
86
|
-
return {
|
|
87
|
-
id: match.shell.id,
|
|
88
|
-
note
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
if (match.kind === "ambiguous") {
|
|
92
|
-
throw new Error(`"${args.name}" matches several shells; pass one of these ids:\n${await describe(match.candidates)}`);
|
|
93
|
-
}
|
|
94
|
-
throw new Error(match.available.length === 0 ? `no shell matches "${args.name}": there are no shells in this project` : `no shell matches "${args.name}". Shells in this project:\n${await describe(match.available.slice(0, 15))}`);
|
|
95
|
-
};
|
|
96
|
-
return {
|
|
97
|
-
shell_start: tool({
|
|
98
|
-
description: START,
|
|
99
|
-
args: {
|
|
100
|
-
command: z.string().min(1).describe("Command line, run by your shell (pipes, && and env vars work)"),
|
|
101
|
-
description: z.string().min(3).describe("What this shell is for, 3-8 words, e.g. 'Next.js dev server'"),
|
|
102
|
-
workdir: z.string().optional().describe("Working directory; defaults to the project directory"),
|
|
103
|
-
env: z.record(z.string(), z.string()).optional().describe("Extra environment variables"),
|
|
104
|
-
waitFor: z.object({
|
|
105
|
-
pattern: z.string().optional(),
|
|
106
|
-
port: z.number().int().min(1).max(65535).optional(),
|
|
107
|
-
idleSeconds: z.number().positive().optional(),
|
|
108
|
-
exit: z.boolean().optional(),
|
|
109
|
-
timeoutSeconds: z.number().positive().max(3600).default(120)
|
|
110
|
-
}).optional().describe("Block until ready. Same conditions as shell_wait."),
|
|
111
|
-
notifyOnExit: z.boolean().default(true).describe("Message you when the process exits"),
|
|
112
|
-
timeoutSeconds: z.number().int().positive().optional().describe("Stop the process after this long. Only for commands expected to finish.")
|
|
113
|
-
},
|
|
114
|
-
async execute(args, ctx) {
|
|
115
|
-
await askPermission(ctx, args.command);
|
|
116
|
-
const shell = deps.shellCommand(args.command);
|
|
117
|
-
const info = await client.call("shell.start", {
|
|
118
|
-
command: shell.command,
|
|
119
|
-
args: shell.args,
|
|
120
|
-
cwd: args.workdir || ctx.directory,
|
|
121
|
-
env: {
|
|
122
|
-
...deps.env(),
|
|
123
|
-
...args.env
|
|
124
|
-
},
|
|
125
|
-
title: args.description,
|
|
126
|
-
owner: {
|
|
127
|
-
project: ctx.directory,
|
|
128
|
-
session: ctx.sessionID,
|
|
129
|
-
instance: deps.instance
|
|
130
|
-
},
|
|
131
|
-
timeoutMs: args.timeoutSeconds ? Math.round(args.timeoutSeconds * 1000) : undefined,
|
|
132
|
-
reuse: true
|
|
133
|
-
});
|
|
134
|
-
if (args.notifyOnExit === false) deps.quiet.add(info.id);else deps.quiet.delete(info.id);
|
|
135
|
-
ctx.metadata({
|
|
136
|
-
title: args.description,
|
|
137
|
-
metadata: {
|
|
138
|
-
shellId: info.id,
|
|
139
|
-
command: args.command
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
if (info.status === "failed") return `${header(info)}\n${describeStatus(info)}\n</shell>`;
|
|
143
|
-
const lines = [info.run > 1 ? `Restarted ${info.id} (run ${info.run}): same command as an earlier finished shell in this session. Earlier output is above line ${info.lines.last}.` : `Started ${info.id}: ${args.command}`];
|
|
144
|
-
if (args.waitFor) {
|
|
145
|
-
const {
|
|
146
|
-
timeoutSeconds,
|
|
147
|
-
idleSeconds,
|
|
148
|
-
...rest
|
|
149
|
-
} = args.waitFor;
|
|
150
|
-
const result = await abortable(ctx, client.call("shell.wait", {
|
|
151
|
-
id: info.id,
|
|
152
|
-
until: {
|
|
153
|
-
pattern: rest.pattern ?? undefined,
|
|
154
|
-
port: rest.port ?? undefined,
|
|
155
|
-
exit: rest.exit ?? undefined,
|
|
156
|
-
idleMs: idleSeconds ? Math.round(idleSeconds * 1000) : undefined
|
|
157
|
-
},
|
|
158
|
-
timeoutMs: Math.round((timeoutSeconds ?? 120) * 1000)
|
|
159
|
-
})).catch(err => {
|
|
160
|
-
lines.push(`wait failed: ${err instanceof Error ? err.message : String(err)} (the shell is still running)`);
|
|
161
|
-
return undefined;
|
|
162
|
-
});
|
|
163
|
-
if (result) lines.push(formatWait(result, timeoutSeconds ?? 120));
|
|
164
|
-
} else {
|
|
165
|
-
await abortable(ctx, client.call("shell.wait", {
|
|
166
|
-
id: info.id,
|
|
167
|
-
until: {
|
|
168
|
-
idleMs: 700,
|
|
169
|
-
exit: true
|
|
170
|
-
},
|
|
171
|
-
timeoutMs: 2500
|
|
172
|
-
}));
|
|
173
|
-
}
|
|
174
|
-
lines.push(await peek(info));
|
|
175
|
-
return lines.join("\n");
|
|
176
|
-
}
|
|
177
|
-
}),
|
|
178
|
-
shell_read: tool({
|
|
179
|
-
description: READ,
|
|
180
|
-
args: {
|
|
181
|
-
...TARGET,
|
|
182
|
-
view: z.enum(["log", "screen"]).default("log"),
|
|
183
|
-
after: z.number().int().min(0).optional().describe("Cursor from a previous result; returns only newer lines"),
|
|
184
|
-
tail: z.number().int().positive().max(2000).default(60).describe("Lines from the end when no cursor is given"),
|
|
185
|
-
grep: z.string().optional().describe("Regex filter"),
|
|
186
|
-
ignoreCase: z.boolean().default(false),
|
|
187
|
-
limit: z.number().int().positive().max(2000).default(300)
|
|
188
|
-
},
|
|
189
|
-
async execute(args, ctx) {
|
|
190
|
-
const {
|
|
191
|
-
id,
|
|
192
|
-
note
|
|
193
|
-
} = await resolve(args, ctx);
|
|
194
|
-
const info = await client.call("shell.get", {
|
|
195
|
-
id
|
|
196
|
-
});
|
|
197
|
-
if (args.view === "screen") {
|
|
198
|
-
const screen = await client.call("shell.screen", {
|
|
199
|
-
id
|
|
200
|
-
});
|
|
201
|
-
return [`${note}${header(info)}`, `status: ${describeStatus(info)}`, `screen ${screen.cols}x${screen.rows}:`, screen.text || "(blank)", "</shell>"].join("\n");
|
|
202
|
-
}
|
|
203
|
-
const page = await client.call("shell.read", {
|
|
204
|
-
id,
|
|
205
|
-
after: args.after ?? undefined,
|
|
206
|
-
tail: args.tail ?? 60,
|
|
207
|
-
grep: args.grep ?? undefined,
|
|
208
|
-
ignoreCase: args.ignoreCase ?? false,
|
|
209
|
-
limit: args.limit ?? 300
|
|
210
|
-
});
|
|
211
|
-
return note + formatRead(info, page, args.after != null ? "(no new output)" : "(no output yet)");
|
|
212
|
-
}
|
|
213
|
-
}),
|
|
214
|
-
shell_send: tool({
|
|
215
|
-
description: SEND,
|
|
216
|
-
args: {
|
|
217
|
-
...TARGET,
|
|
218
|
-
text: z.string().optional(),
|
|
219
|
-
keys: z.array(z.string()).optional(),
|
|
220
|
-
submit: z.boolean().default(false).describe("Press enter after text"),
|
|
221
|
-
waitSeconds: z.number().min(0).max(30).default(1).describe("Max time to collect the response")
|
|
222
|
-
},
|
|
223
|
-
async execute(args, ctx) {
|
|
224
|
-
if (!args.text && !args.keys?.length) throw new Error("provide text and/or keys");
|
|
225
|
-
let data = args.text ?? "";
|
|
226
|
-
for (const key of args.keys ?? []) data += encodeKey(key);
|
|
227
|
-
if (args.submit === true) data += "\r";
|
|
228
|
-
const {
|
|
229
|
-
id,
|
|
230
|
-
note
|
|
231
|
-
} = await resolve(args, ctx);
|
|
232
|
-
const before = await client.call("shell.get", {
|
|
233
|
-
id
|
|
234
|
-
});
|
|
235
|
-
await client.call("shell.write", {
|
|
236
|
-
id,
|
|
237
|
-
data
|
|
238
|
-
});
|
|
239
|
-
const waitSeconds = args.waitSeconds ?? 1;
|
|
240
|
-
if (waitSeconds > 0) {
|
|
241
|
-
await abortable(ctx, client.call("shell.wait", {
|
|
242
|
-
id,
|
|
243
|
-
until: {
|
|
244
|
-
idleMs: 400,
|
|
245
|
-
exit: true
|
|
246
|
-
},
|
|
247
|
-
timeoutMs: Math.round(waitSeconds * 1000),
|
|
248
|
-
after: before.lines.last
|
|
249
|
-
}));
|
|
250
|
-
}
|
|
251
|
-
const info = await client.call("shell.get", {
|
|
252
|
-
id
|
|
253
|
-
});
|
|
254
|
-
const page = await client.call("shell.read", {
|
|
255
|
-
id,
|
|
256
|
-
after: before.lines.last,
|
|
257
|
-
limit: 300
|
|
258
|
-
});
|
|
259
|
-
return note + formatRead(info, page, "(no new output lines; if this is a full-screen program use shell_read view=screen)");
|
|
260
|
-
}
|
|
261
|
-
}),
|
|
262
|
-
shell_wait: tool({
|
|
263
|
-
description: WAIT,
|
|
264
|
-
args: {
|
|
265
|
-
...TARGET,
|
|
266
|
-
pattern: z.string().optional(),
|
|
267
|
-
ignoreCase: z.boolean().optional(),
|
|
268
|
-
port: z.number().int().min(1).max(65535).optional(),
|
|
269
|
-
host: z.string().optional(),
|
|
270
|
-
idleSeconds: z.number().positive().optional(),
|
|
271
|
-
exit: z.boolean().optional(),
|
|
272
|
-
timeoutSeconds: z.number().positive().max(3600).default(300)
|
|
273
|
-
},
|
|
274
|
-
async execute(args, ctx) {
|
|
275
|
-
const {
|
|
276
|
-
id,
|
|
277
|
-
note
|
|
278
|
-
} = await resolve(args, ctx);
|
|
279
|
-
const start = await client.call("shell.get", {
|
|
280
|
-
id
|
|
281
|
-
});
|
|
282
|
-
const result = await abortable(ctx, client.call("shell.wait", {
|
|
283
|
-
id,
|
|
284
|
-
until: {
|
|
285
|
-
pattern: args.pattern ?? undefined,
|
|
286
|
-
ignoreCase: args.ignoreCase ?? undefined,
|
|
287
|
-
port: args.port ?? undefined,
|
|
288
|
-
host: args.host ?? undefined,
|
|
289
|
-
exit: args.exit ?? undefined,
|
|
290
|
-
idleMs: args.idleSeconds ? Math.round(args.idleSeconds * 1000) : undefined
|
|
291
|
-
},
|
|
292
|
-
timeoutMs: Math.round((args.timeoutSeconds ?? 300) * 1000)
|
|
293
|
-
}));
|
|
294
|
-
if (!result) return "wait cancelled";
|
|
295
|
-
const newLines = result.info.lines.last - start.lines.last;
|
|
296
|
-
const page = newLines > 80 ? await client.call("shell.read", {
|
|
297
|
-
id,
|
|
298
|
-
tail: 80
|
|
299
|
-
}) : await client.call("shell.read", {
|
|
300
|
-
id,
|
|
301
|
-
after: start.lines.last,
|
|
302
|
-
limit: 80
|
|
303
|
-
});
|
|
304
|
-
const recent = page.lines;
|
|
305
|
-
if (newLines > 80) recent.unshift({
|
|
306
|
-
n: start.lines.last,
|
|
307
|
-
text: `… ${newLines - 80} earlier lines omitted (shell_read after=${start.lines.last})`
|
|
308
|
-
});
|
|
309
|
-
return [note + formatWait(result, args.timeoutSeconds ?? 300), header(result.info), recent.length > 0 ? formatLines(recent) : "(no new output during the wait)", "</shell>", `cursor: ${result.info.lines.last}`].join("\n");
|
|
310
|
-
}
|
|
311
|
-
}),
|
|
312
|
-
shell_list: tool({
|
|
313
|
-
description: `List background shells in this project: name, status, which session started it, and the last output line.
|
|
314
|
-
|
|
315
|
-
Filter to find the one you need instead of reading them all:
|
|
316
|
-
- query: text in the name or command, e.g. "db" or "vitest"
|
|
317
|
-
- status: running, failed, finished
|
|
318
|
-
- session: this (started by you in this session), others (other sessions or the user)`,
|
|
319
|
-
args: {
|
|
320
|
-
query: z.string().optional().describe("Case-insensitive text in the shell name or command"),
|
|
321
|
-
status: z.enum(["running", "failed", "finished", "any"]).default("any"),
|
|
322
|
-
session: z.enum(["this", "others", "any"]).default("any"),
|
|
323
|
-
all: z.boolean().default(false).describe("Include shells from other projects")
|
|
324
|
-
},
|
|
325
|
-
async execute(args, ctx) {
|
|
326
|
-
const everything = await client.call("shell.list", args.all === true ? {} : {
|
|
327
|
-
owner: {
|
|
328
|
-
project: ctx.directory
|
|
329
|
-
}
|
|
330
|
-
});
|
|
331
|
-
const shells = filterShells(everything, {
|
|
332
|
-
query: args.query ?? undefined,
|
|
333
|
-
status: args.status ?? "any",
|
|
334
|
-
session: args.session ?? "any",
|
|
335
|
-
currentSession: ctx.sessionID
|
|
336
|
-
});
|
|
337
|
-
if (everything.length === 0) return "No background shells.";
|
|
338
|
-
if (shells.length === 0) return `No shells match those filters (${everything.length} shell${everything.length === 1 ? "" : "s"} in total).`;
|
|
339
|
-
const rows = await Promise.all(shells.map(async s => {
|
|
340
|
-
const last = await client.call("shell.read", {
|
|
341
|
-
id: s.id,
|
|
342
|
-
tail: 1
|
|
343
|
-
}).catch(() => undefined);
|
|
344
|
-
const tail = last?.lines[0]?.text ?? "";
|
|
345
|
-
const failure = s.summary && s.status !== "running" ? `\n summary: ${s.summary.slice(0, 200)}` : "";
|
|
346
|
-
return [`${s.id} ${s.status.padEnd(7)} "${s.title}"${s.run > 1 ? ` (run ${s.run})` : ""} · ${await sessionLabel(s, ctx)}`, ` $ ${commandOf(s).slice(0, 200)}${failure}`, ` ${describeStatus(s)}${tail ? `\n last: ${tail.slice(0, 200)}` : ""}`].join("\n");
|
|
347
|
-
}));
|
|
348
|
-
const hidden = everything.length - shells.length;
|
|
349
|
-
return rows.join("\n") + (hidden > 0 ? `\n(${hidden} more shell${hidden === 1 ? "" : "s"} hidden by filters)` : "");
|
|
350
|
-
}
|
|
351
|
-
}),
|
|
352
|
-
shell_stop: tool({
|
|
353
|
-
description: "Stop a background shell (SIGTERM to its whole process group, then SIGKILL after a grace period). Set remove=true to also forget it.",
|
|
354
|
-
args: {
|
|
355
|
-
...TARGET,
|
|
356
|
-
remove: z.boolean().default(false),
|
|
357
|
-
force: z.boolean().default(false).describe("Send SIGKILL immediately")
|
|
358
|
-
},
|
|
359
|
-
async execute(args, ctx) {
|
|
360
|
-
const {
|
|
361
|
-
id,
|
|
362
|
-
note
|
|
363
|
-
} = await resolve(args, ctx);
|
|
364
|
-
deps.quiet.add(id);
|
|
365
|
-
const info = await client.call("shell.stop", {
|
|
366
|
-
id,
|
|
367
|
-
signal: args.force === true ? "SIGKILL" : "SIGTERM",
|
|
368
|
-
graceMs: 3000
|
|
369
|
-
});
|
|
370
|
-
if (args.remove === true) await client.call("shell.remove", {
|
|
371
|
-
id
|
|
372
|
-
});
|
|
373
|
-
return `${note}${info.id} ${describeStatus(info)}${args.remove === true ? " and removed" : ""}`;
|
|
374
|
-
}
|
|
375
|
-
}),
|
|
376
|
-
shell_restart: tool({
|
|
377
|
-
description: "Restart a background shell with the same command. Keeps the id; output continues after a restart marker.",
|
|
378
|
-
args: {
|
|
379
|
-
...TARGET
|
|
380
|
-
},
|
|
381
|
-
async execute(args, ctx) {
|
|
382
|
-
const {
|
|
383
|
-
id,
|
|
384
|
-
note
|
|
385
|
-
} = await resolve(args, ctx);
|
|
386
|
-
const info = await client.call("shell.restart", {
|
|
387
|
-
id
|
|
388
|
-
});
|
|
389
|
-
deps.quiet.delete(id);
|
|
390
|
-
await abortable(ctx, client.call("shell.wait", {
|
|
391
|
-
id: info.id,
|
|
392
|
-
until: {
|
|
393
|
-
idleMs: 700,
|
|
394
|
-
exit: true
|
|
395
|
-
},
|
|
396
|
-
timeoutMs: 2500
|
|
397
|
-
}));
|
|
398
|
-
return `${note}Restarted ${info.id} (run ${info.run})\n${await peek(info)}`;
|
|
399
|
-
}
|
|
400
|
-
})
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
async function askPermission(ctx, command) {
|
|
404
|
-
const words = command.trim().split(/\s+/);
|
|
405
|
-
const prefix = words.slice(0, Math.min(2, words.length)).join(" ");
|
|
406
|
-
await ctx.ask({
|
|
407
|
-
permission: "bash",
|
|
408
|
-
patterns: [command],
|
|
409
|
-
always: [`${prefix} *`],
|
|
410
|
-
metadata: {
|
|
411
|
-
command,
|
|
412
|
-
description: "background shell"
|
|
413
|
-
}
|
|
414
|
-
});
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
/** Resolves undefined when the tool call is aborted; the daemon keeps running the shell. */
|
|
418
|
-
function abortable(ctx, promise) {
|
|
419
|
-
if (ctx.abort.aborted) return Promise.resolve(undefined);
|
|
420
|
-
return new Promise((resolve, reject) => {
|
|
421
|
-
const onAbort = () => resolve(undefined);
|
|
422
|
-
ctx.abort.addEventListener("abort", onAbort, {
|
|
423
|
-
once: true
|
|
424
|
-
});
|
|
425
|
-
promise.then(v => {
|
|
426
|
-
ctx.abort.removeEventListener("abort", onAbort);
|
|
427
|
-
resolve(v);
|
|
428
|
-
}, err => {
|
|
429
|
-
ctx.abort.removeEventListener("abort", onAbort);
|
|
430
|
-
reject(err instanceof RpcError ? new Error(err.message) : err);
|
|
431
|
-
});
|
|
432
|
-
});
|
|
433
|
-
}
|
package/types/tools/index.d.ts
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
import { type ToolDefinition } from "@opencode-ai/plugin";
|
|
2
|
-
import type { CockpitClient } from "@opencode-cockpit/client";
|
|
3
|
-
export interface ToolDeps {
|
|
4
|
-
client: CockpitClient;
|
|
5
|
-
/** Identifies this OpenCode instance so only it notifies the owning session. */
|
|
6
|
-
instance: string;
|
|
7
|
-
/** Shells whose exit should not message the agent (it stopped them itself, or opted out). */
|
|
8
|
-
quiet: Set<string>;
|
|
9
|
-
shellCommand(command: string): {
|
|
10
|
-
command: string;
|
|
11
|
-
args: string[];
|
|
12
|
-
};
|
|
13
|
-
env(): Record<string, string>;
|
|
14
|
-
/** Human title of an OpenCode session, for telling agents which session started a shell. */
|
|
15
|
-
sessionTitle?(sessionID: string): Promise<string | undefined>;
|
|
16
|
-
}
|
|
17
|
-
export declare function createTools(deps: ToolDeps): Record<string, ToolDefinition>;
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|