@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
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { commandOf } from "./find.js";
|
|
2
|
+
|
|
3
|
+
/** What a shell is, derived from its command — never a label anyone has to maintain by hand. */
|
|
4
|
+
|
|
5
|
+
const TESTS = /\b(test|tests|vitest|jest|mocha|pytest|rspec|phpunit|playwright|cypress|karma)\b/i;
|
|
6
|
+
const SERVER = /\b(dev|serve|server|start|preview|nodemon|uvicorn|gunicorn|rails s|php -S|docker[\s-]compose\s+up|ngrok|tunnel)\b/i;
|
|
7
|
+
const WATCHER = /(--watch|\bwatch\b|watchexec|fswatch|chokidar|\btsc\s+-w\b|--hot|\bentr\b)/i;
|
|
8
|
+
const BUILD = /\b(build|assemble|compile|bundle|tsc|webpack|rollup|esbuild|tsup|make|gradlew?|mvn|cargo build)\b/i;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Order matters: a test runner in watch mode is still about tests, and `vite build` is a build even
|
|
12
|
+
* though `vite` usually serves.
|
|
13
|
+
*/
|
|
14
|
+
export function classify(command, custom) {
|
|
15
|
+
// Config patterns win: only the user knows their own toolchain.
|
|
16
|
+
for (const [name, pattern] of Object.entries(custom ?? {})) {
|
|
17
|
+
try {
|
|
18
|
+
if (new RegExp(pattern, "i").test(command)) return name;
|
|
19
|
+
} catch {
|
|
20
|
+
// a bad pattern in config is ignored, like any other unusable setting
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (TESTS.test(command)) return "tests";
|
|
24
|
+
if (BUILD.test(command) && !SERVER.test(command)) return "build";
|
|
25
|
+
if (SERVER.test(command)) return "server";
|
|
26
|
+
if (WATCHER.test(command)) return "watcher";
|
|
27
|
+
return "task";
|
|
28
|
+
}
|
|
29
|
+
export function kindOfShell(s, custom) {
|
|
30
|
+
return classify(commandOf(s), custom);
|
|
31
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -1,152 +1,2 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import { describeStatus, formatLines } from "./tools/format.js";
|
|
4
|
-
import { createTools } from "./tools/index.js";
|
|
5
|
-
const GUIDANCE = `## Background shells (opencode-cockpit)
|
|
6
|
-
Long-running or interactive commands (dev servers, watchers, slow builds/tests, REPLs) go in shell_start, not bash with "&".
|
|
7
|
-
Block with shell_wait (pattern, port, idle, exit) instead of sleeping; follow output with shell_read(after=cursor).
|
|
8
|
-
You are messaged when a shell you started exits.`;
|
|
9
|
-
export const SHELL_PACKAGE = "@opencode-cockpit/shell";
|
|
10
|
-
/** Shell's server half as a factory, so bundles such as `opencode-cockpit` can include it. */
|
|
11
|
-
export function createShellServer({
|
|
12
|
-
source = SHELL_PACKAGE
|
|
13
|
-
} = {}) {
|
|
14
|
-
return async input => {
|
|
15
|
-
const claim = claimFeature(input, "shell", source);
|
|
16
|
-
if (!claim.active) {
|
|
17
|
-
// Logging through the server during plugin initialisation could wait on ourselves; defer it.
|
|
18
|
-
setTimeout(() => {
|
|
19
|
-
void input.client.app.log({
|
|
20
|
-
body: {
|
|
21
|
-
service: "opencode-cockpit",
|
|
22
|
-
level: "warn",
|
|
23
|
-
message: duplicateFeatureMessage("Shell", claim.owner, source)
|
|
24
|
-
}
|
|
25
|
-
}).catch(() => {});
|
|
26
|
-
}, 0);
|
|
27
|
-
return {};
|
|
28
|
-
}
|
|
29
|
-
const hooks = await shellHooks(input);
|
|
30
|
-
const dispose = hooks.dispose;
|
|
31
|
-
return {
|
|
32
|
-
...hooks,
|
|
33
|
-
dispose: async () => {
|
|
34
|
-
claim.release();
|
|
35
|
-
await dispose?.();
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
async function shellHooks({
|
|
41
|
-
client: opencode,
|
|
42
|
-
directory
|
|
43
|
-
}) {
|
|
44
|
-
const cockpit = createClient("opencode-cockpit/server");
|
|
45
|
-
const instance = crypto.randomUUID();
|
|
46
|
-
const quiet = new Set();
|
|
47
|
-
const userShell = process.env.SHELL && /(bash|zsh|fish|sh)$/.test(process.env.SHELL) ? process.env.SHELL : "/bin/bash";
|
|
48
|
-
const env = () => {
|
|
49
|
-
const out = {};
|
|
50
|
-
for (const [k, v] of Object.entries(process.env)) if (v !== undefined && !k.startsWith("OPENCODE_")) out[k] = v;
|
|
51
|
-
return out;
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Session titles rarely change; cache them so listing shells stays one round trip.
|
|
55
|
-
const titles = new Map();
|
|
56
|
-
const sessionTitle = async sessionID => {
|
|
57
|
-
const hit = titles.get(sessionID);
|
|
58
|
-
if (hit && Date.now() - hit.at < 60_000) return hit.title;
|
|
59
|
-
const result = await opencode.session.get({
|
|
60
|
-
path: {
|
|
61
|
-
id: sessionID
|
|
62
|
-
}
|
|
63
|
-
}).catch(() => undefined);
|
|
64
|
-
const title = result?.data?.title;
|
|
65
|
-
titles.set(sessionID, {
|
|
66
|
-
title,
|
|
67
|
-
at: Date.now()
|
|
68
|
-
});
|
|
69
|
-
return title;
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
// Wake the agent when a shell it owns ends on its own.
|
|
73
|
-
cockpit.on("shell.exited", info => {
|
|
74
|
-
if (info.owner.instance !== instance || !info.owner.session) return;
|
|
75
|
-
if (quiet.delete(info.id)) return;
|
|
76
|
-
void notifyExit(info).catch(() => {});
|
|
77
|
-
});
|
|
78
|
-
async function notifyExit(info) {
|
|
79
|
-
const session = info.owner.session;
|
|
80
|
-
const page = await cockpit.call("shell.read", {
|
|
81
|
-
id: info.id,
|
|
82
|
-
tail: 15
|
|
83
|
-
});
|
|
84
|
-
const failed = info.status === "failed" || info.status === "exited" && info.exitCode !== 0 || info.status === "killed";
|
|
85
|
-
const text = [`<shell_exited id="${info.id}" title="${info.title}">`, describeStatus(info), page.lines.length > 0 ? `last output:\n${formatLines(page.lines)}` : "(no output)", "</shell_exited>", failed ? `Investigate with shell_read id=${info.id} grep="error|fail" if the failure matters to the task.` : `Full output: shell_read id=${info.id}.`].join("\n");
|
|
86
|
-
await opencode.session.promptAsync({
|
|
87
|
-
path: {
|
|
88
|
-
id: session
|
|
89
|
-
},
|
|
90
|
-
body: {
|
|
91
|
-
parts: [{
|
|
92
|
-
type: "text",
|
|
93
|
-
text,
|
|
94
|
-
synthetic: true
|
|
95
|
-
}]
|
|
96
|
-
}
|
|
97
|
-
});
|
|
98
|
-
}
|
|
99
|
-
return {
|
|
100
|
-
tool: createTools({
|
|
101
|
-
client: cockpit,
|
|
102
|
-
instance,
|
|
103
|
-
quiet,
|
|
104
|
-
env,
|
|
105
|
-
sessionTitle,
|
|
106
|
-
shellCommand: command => ({
|
|
107
|
-
command: userShell,
|
|
108
|
-
args: ["-c", command]
|
|
109
|
-
})
|
|
110
|
-
}),
|
|
111
|
-
"experimental.chat.system.transform": async (input, output) => {
|
|
112
|
-
output.system.push(GUIDANCE);
|
|
113
|
-
const running = await cockpit.call("shell.list", {
|
|
114
|
-
owner: {
|
|
115
|
-
project: directory
|
|
116
|
-
},
|
|
117
|
-
includeExited: false
|
|
118
|
-
}).catch(() => []);
|
|
119
|
-
if (running.length > 0) {
|
|
120
|
-
output.system.push(`Background shells currently running in this project:\n${running.slice(0, 15).map(s => {
|
|
121
|
-
const from = !s.owner.session ? ", started by the user" : s.owner.session === input.sessionID ? "" : ", another session";
|
|
122
|
-
return `- ${s.id} "${s.title}" (${describeStatus(s)}${from})`;
|
|
123
|
-
}).join("\n")}`);
|
|
124
|
-
}
|
|
125
|
-
},
|
|
126
|
-
event: async ({
|
|
127
|
-
event
|
|
128
|
-
}) => {
|
|
129
|
-
if (event.type !== "session.deleted") return;
|
|
130
|
-
const sessionID = event.properties.info.id;
|
|
131
|
-
const owned = await cockpit.call("shell.list", {
|
|
132
|
-
owner: {
|
|
133
|
-
session: sessionID
|
|
134
|
-
}
|
|
135
|
-
}).catch(() => []);
|
|
136
|
-
for (const shell of owned) {
|
|
137
|
-
quiet.add(shell.id);
|
|
138
|
-
await cockpit.call("shell.remove", {
|
|
139
|
-
id: shell.id
|
|
140
|
-
}).catch(() => {});
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
dispose: async () => {
|
|
144
|
-
cockpit.close();
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
const plugin = {
|
|
149
|
-
id: "opencode-cockpit.shell",
|
|
150
|
-
server: createShellServer()
|
|
151
|
-
};
|
|
152
|
-
export default plugin;
|
|
1
|
+
/** Published entry point: `@opencode-cockpit/shell/server`. */
|
|
2
|
+
export { createShellServer, default, SHELL_PACKAGE } from "./agent/plugin.js";
|
|
@@ -5,7 +5,7 @@ import { setProp as _$setProp } from "@opentui/solid";
|
|
|
5
5
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
6
6
|
/** @jsxImportSource @opentui/solid */
|
|
7
7
|
|
|
8
|
-
import { badgeText, kindColor, kindOf } from "
|
|
8
|
+
import { badgeText, kindColor, kindOf } from "../lib/view.js";
|
|
9
9
|
|
|
10
10
|
/** Status pill: label on a coloured background, readable in any font and colour scheme. */
|
|
11
11
|
export function Badge(props) {
|