opencode-cockpit 0.1.3 → 0.1.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/README.md CHANGED
@@ -1,15 +1,91 @@
1
1
  # opencode-cockpit
2
2
 
3
3
  [![CI](https://github.com/Codestz/opencode-cockpit/actions/workflows/ci.yml/badge.svg)](https://github.com/Codestz/opencode-cockpit/actions/workflows/ci.yml)
4
- [![npm](https://img.shields.io/npm/v/opencode-cockpit)](https://www.npmjs.com/package/opencode-cockpit)
4
+ [![npm](https://img.shields.io/npm/v/opencode-cockpit?color=%23cb3837&label=opencode-cockpit)](https://www.npmjs.com/package/opencode-cockpit)
5
+ [![npm](https://img.shields.io/npm/v/@opencode-cockpit/shell?color=%23cb3837&label=%40opencode-cockpit%2Fshell)](https://www.npmjs.com/package/@opencode-cockpit/shell)
5
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Codestz/opencode-cockpit/blob/main/LICENSE)
6
7
 
7
- Superpowers for [OpenCode](https://opencode.ai). Take all of them, or only the ones you want.
8
+ **Superpowers for [OpenCode](https://opencode.ai) take all of them, or just the one you need.**
8
9
 
9
- | Feature | What it gives you | Package |
10
+ Coding agents are stuck in a one-command-at-a-time world: they run something, wait for it to
11
+ finish, and paste the whole log back into their context. Cockpit gives your agent the things a
12
+ developer actually has — long-running terminals, a way to wait for "ready", and output it can read
13
+ without drowning in it — and gives *you* a live view of all of it, inside OpenCode.
14
+
15
+ ```
16
+ ────────────────────────────────────────────────────────────────────────────────────────
17
+ Shells ⠹ RUN dev server FAIL unit tests ▸ 3 more ctrl+x i console · ctrl+x o hide
18
+ VITE v7.3.1 ready in 431 ms
19
+ ➜ Local: http://localhost:5173/
20
+ ✓ 142 modules transformed
21
+ 2m14s sh_k4tq8b2p · $ npm run dev
22
+ ────────────────────────────────────────────────────────────────────────────────────────
23
+ ```
24
+
25
+ ## Features
26
+
27
+ | Feature | What your agent gains | Package |
28
+ |---|---|---|
29
+ | **Shell** | Background terminals it starts, waits on, reads and types into — dev servers, watchers, test suites, REPLs | [`@opencode-cockpit/shell`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/shell) |
30
+ | **Agents** *(next)* | A live, keyboard-first view of every subagent, without leaving your chat | `@opencode-cockpit/agents` |
31
+
32
+ ## Shell, by example
33
+
34
+ **"Start the dev server and wait until it's actually ready."**
35
+ The agent starts it in a real terminal and blocks on the port opening — not a guess, not a sleep:
36
+ ```
37
+ shell_start command="npm run dev" waitFor={ port: 5173 }
38
+ → condition met: port is accepting connections
39
+ 1| VITE v7.3.1 ready in 431 ms
40
+ ```
41
+
42
+ **"Run the tests, keep working, tell me if they fail."**
43
+ The suite runs in the background. When it exits, the agent is messaged once, with the error line
44
+ already picked out:
45
+ ```
46
+ <shell_exited id="sh_9wq2f1ab" title="unit tests">
47
+ exited with code 1 after 48s
48
+ last output: 37| FAIL src/auth.test.ts > refresh token expiry
49
+ ```
50
+
51
+ **"How is DB Monitoring doing?"**
52
+ Shells are shared across sessions and can be addressed by name:
53
+ ```
54
+ shell_read name="DB Monitoring"
55
+ ```
56
+
57
+ **Logs that don't eat your context.** Colour codes stripped, progress-bar redraws collapsed to
58
+ their final frame, repeated lines folded to `(×12)`, and every read returns a cursor so the next
59
+ one only brings what's new. For full-screen programs (`vitest --ui`, `htop`, prompts) the agent can
60
+ ask for the *screen* instead of the log.
61
+
62
+ **It can type.** Prompts, REPLs, migration wizards: `shell_send` sends text or named keys
63
+ (`ctrl+c`, `up`, `enter`) and returns whatever the program printed back.
64
+
65
+ ### Why not just `bash`?
66
+
67
+ | | Built-in `bash` tool | Cockpit Shell |
10
68
  |---|---|---|
11
- | **Shell** | Background terminals the agent starts, waits on and drives, with a docked panel and console for you | [`@opencode-cockpit/shell`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/shell) |
12
- | **Agents** *(coming)* | A live, keyboard-first view of every subagent, without leaving your chat | `@opencode-cockpit/agents` |
69
+ | Long-running processes | Blocks until exit | Runs in the background, survives the turn |
70
+ | Knowing something is ready | Guess, or sleep and poll | Blocks on a port, a pattern, silence or exit |
71
+ | Interactive programs | Not possible (no TTY) | Real PTY: prompts, REPLs, ctrl+c |
72
+ | Reading output | Whole log, every time | Clean lines from a cursor, with grep |
73
+ | Your visibility | None until it finishes | Live panel, console and sidebar |
74
+ | After OpenCode restarts | Gone | Still running |
75
+
76
+ ## For you, not just the agent
77
+
78
+ | Key / command | Does |
79
+ |---|---|
80
+ | `ctrl+x o` · `/shells` | Toggle the shells panel under the chat |
81
+ | `ctrl+x i` · `/shell` | Open the shell console |
82
+ | `/shell-new` | Start a shell yourself |
83
+ | `/shells-clear` | Remove finished shells |
84
+
85
+ Status reads the same everywhere — `RUN` (with a spinner), `FAIL`, `STOP`, `DONE` — running shells
86
+ and recent failures stay in view, the rest folds behind `▸ N more`. In the console: `i` types
87
+ straight into the program (`ctrl+]` to stop), `c` sends ctrl+c, `r` restarts, `x` stops, `tab`
88
+ switches between the live screen and the scrollback, `?` shows details.
13
89
 
14
90
  ## Install
15
91
 
@@ -25,34 +101,24 @@ opencode plugin opencode-cockpit --global
25
101
  opencode plugin @opencode-cockpit/shell --global
26
102
  ```
27
103
 
28
- **Everything except some features**: switch them off on the plugin entry, in both `opencode.json`
29
- and `tui.json`:
30
-
31
- ```json
32
- { "plugin": [["opencode-cockpit", { "features": { "shell": false } }]] }
33
- ```
34
-
35
- Restart OpenCode after installing. Requires OpenCode 1.18 or newer on macOS or Linux.
36
-
37
- > Install a feature **either** through `opencode-cockpit` **or** on its own, not both. If both are
38
- > configured, the first one loaded is used and OpenCode shows a warning telling you which entry to
39
- > remove.
40
-
41
- ## Configuring features
104
+ Restart OpenCode. Requires OpenCode 1.18+ on macOS or Linux. Install a feature either through
105
+ `opencode-cockpit` or on its own — if both are configured, the first one loaded is used and
106
+ OpenCode warns you which entry to remove.
42
107
 
43
- Each feature's README lists its options. With `opencode-cockpit`, nest them under the feature's
44
- name:
108
+ **Turn features off, or pass them options** (in both `opencode.json` and `tui.json`):
45
109
 
46
110
  ```json
47
111
  {
48
112
  "plugin": [
49
- ["opencode-cockpit", { "shell": { "dockHeight": 16, "historyMinutes": 60 } }]
113
+ ["opencode-cockpit", {
114
+ "features": { "shell": true },
115
+ "shell": { "dockHeight": 16, "historyMinutes": 60 }
116
+ }]
50
117
  ]
51
118
  }
52
119
  ```
53
120
 
54
- With a standalone package, put them directly on its entry:
55
- `["@opencode-cockpit/shell", { "dockHeight": 16 }]`.
121
+ Each feature's README documents its own options: [Shell](https://github.com/Codestz/opencode-cockpit/tree/main/packages/shell#configuration).
56
122
 
57
123
  ## How it works
58
124
 
@@ -62,25 +128,43 @@ OpenCode TUI thread ── feature plugins (tui) ──┐
62
128
  OpenCode server worker ─ feature plugins (server) ┘
63
129
  ```
64
130
 
65
- Each feature is a complete OpenCode plugin. Features that need long-lived processes share one
66
- small daemon, `cockpitd`, which starts on demand, is shared by every OpenCode window, upgrades
67
- itself when a newer plugin connects and exits when idle. See [CONTRIBUTING.md](https://github.com/Codestz/opencode-cockpit/blob/main/CONTRIBUTING.md).
131
+ OpenCode runs its interface and its server in separate threads, so a plugin's two halves can't
132
+ share memory. Both talk to **`cockpitd`**, a small daemon that owns every long-lived process: it
133
+ starts on demand, is shared by every OpenCode window, upgrades itself when a newer plugin connects,
134
+ cleans up processes left by a crash, and exits when idle. That's why shells outlive OpenCode
135
+ restarts, and why one session can look at a shell another session started.
136
+
137
+ Each shell's output feeds three views at once: a normalized **log** for the agent, an emulated
138
+ **screen** for you, and a raw ring buffer so a panel opened late can catch up.
68
139
 
69
140
  | Package | Role |
70
141
  |---|---|
71
- | [`opencode-cockpit`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/opencode) | All features in one plugin, each can be switched off |
142
+ | [`opencode-cockpit`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/opencode) | All features in one plugin |
72
143
  | [`@opencode-cockpit/shell`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/shell) | Shell feature |
73
144
  | [`@opencode-cockpit/daemon`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/daemon) | `cockpitd`, the shared process host |
74
- | [`@opencode-cockpit/client`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/client) | Typed, auto-spawning client and shared plugin helpers |
145
+ | [`@opencode-cockpit/client`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/client) | Typed, auto-spawning client and plugin helpers |
75
146
  | [`@opencode-cockpit/protocol`](https://github.com/Codestz/opencode-cockpit/tree/main/packages/protocol) | Wire contracts |
76
147
 
77
148
  ## Roadmap
78
149
 
79
- - **Watchers** (Shell): `tsc`, `eslint` and `vitest` shells that report only state changes.
80
- - **Agents**: live subagent tree with a peek overlay.
81
- - **Doctor**: one command that checks your setup (duplicate features, daemon health, config) and
82
- tells you how to fix it.
83
- - Coloured output in the Shell panel and console.
150
+ - **Watchers** `tsc`, `eslint` and `vitest` that report only *state changes*: "ok → 3 errors in
151
+ auth.ts", never a wall of repeated output.
152
+ - **Agents** live subagent tree with a peek overlay.
153
+ - **Doctor** — one command that checks your setup and tells you how to fix it.
154
+ - Coloured output in the panel and console.
155
+
156
+ ## Contributing
157
+
158
+ Issues and pull requests are welcome. [CONTRIBUTING.md](https://github.com/Codestz/opencode-cockpit/blob/main/CONTRIBUTING.md) covers the architecture,
159
+ the invariants worth knowing before changing anything, and how to run your working copy inside
160
+ OpenCode.
161
+
162
+ ```sh
163
+ bun install
164
+ bun run check # lint, typecheck, tests (real PTYs, real daemon)
165
+ bun run pack:check # pack, install the tarballs, run a shell through them
166
+ bun run smoke:tui # drive a real OpenCode against the packed plugin
167
+ ```
84
168
 
85
169
  ## License
86
170
 
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Merges the server hooks of several features into one plugin. Tool maps are unioned (a name
3
+ * clash is a bug, so it throws); function hooks run in feature order, each awaited, which is how
4
+ * OpenCode itself runs hooks from separate plugins.
5
+ */
6
+ export function composeHooks(parts) {
7
+ const tools = {};
8
+ const functions = new Map();
9
+ const singles = {};
10
+ for (const part of parts) {
11
+ for (const [key, value] of Object.entries(part)) {
12
+ if (value === undefined) continue;
13
+ if (key === "tool") {
14
+ for (const [id, def] of Object.entries(value)) {
15
+ if (id in tools) throw new Error(`tool "${id}" is registered by more than one cockpit feature`);
16
+ tools[id] = def;
17
+ }
18
+ } else if (typeof value === "function") {
19
+ const list = functions.get(key) ?? [];
20
+ list.push(value);
21
+ functions.set(key, list);
22
+ } else {
23
+ if (key in singles) throw new Error(`hook "${key}" is provided by more than one cockpit feature`);
24
+ singles[key] = value;
25
+ }
26
+ }
27
+ }
28
+ const hooks = {
29
+ ...singles
30
+ };
31
+ for (const [key, list] of functions) {
32
+ hooks[key] = list.length === 1 ? list[0] : async (...args) => {
33
+ for (const fn of list) await fn(...args);
34
+ };
35
+ }
36
+ if (Object.keys(tools).length > 0) hooks.tool = tools;
37
+ return hooks;
38
+ }
@@ -0,0 +1,23 @@
1
+ /** Every feature the bundle can load. Adding one: list it here and wire it in server.ts / tui.ts. */
2
+ export const FEATURES = ["shell"];
3
+ export const BUNDLE = "opencode-cockpit";
4
+ export function isEnabled(options, feature) {
5
+ return options?.features?.[feature] !== false;
6
+ }
7
+
8
+ /**
9
+ * Options for one feature. Shell also accepts options at the top level, which is where 0.1.x
10
+ * (when `opencode-cockpit` was Shell) read them from.
11
+ */
12
+ export function featureOptions(options, feature) {
13
+ const own = options?.[feature] ?? {};
14
+ if (feature !== "shell" || !options) return own;
15
+ const legacy = {};
16
+ for (const [key, value] of Object.entries(options)) {
17
+ if (key !== "features" && !FEATURES.includes(key)) legacy[key] = value;
18
+ }
19
+ return {
20
+ ...legacy,
21
+ ...own
22
+ };
23
+ }
package/dist/server.js ADDED
@@ -0,0 +1,17 @@
1
+ import { createShellServer } from "@opencode-cockpit/shell/server";
2
+ import { composeHooks } from "./compose.js";
3
+ import { BUNDLE, featureOptions, isEnabled } from "./features.js";
4
+ const shell = createShellServer({
5
+ source: BUNDLE
6
+ });
7
+ const server = async (input, rawOptions) => {
8
+ const options = rawOptions;
9
+ const parts = [];
10
+ if (isEnabled(options, "shell")) parts.push(await shell(input, featureOptions(options, "shell")));
11
+ return composeHooks(parts);
12
+ };
13
+ const plugin = {
14
+ id: BUNDLE,
15
+ server
16
+ };
17
+ export default plugin;
package/dist/tui.js ADDED
@@ -0,0 +1,14 @@
1
+ import { createShellTui } from "@opencode-cockpit/shell/tui";
2
+ import { BUNDLE, featureOptions, isEnabled } from "./features.js";
3
+ const shell = createShellTui({
4
+ source: BUNDLE
5
+ });
6
+ const tui = async (api, rawOptions, meta) => {
7
+ const options = rawOptions;
8
+ if (isEnabled(options, "shell")) await shell(api, featureOptions(options, "shell"), meta);
9
+ };
10
+ const plugin = {
11
+ id: BUNDLE,
12
+ tui
13
+ };
14
+ export default plugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-cockpit",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "description": "OpenCode superpowers, all in one: installs every opencode-cockpit feature (Shell today), each can be switched off",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -25,15 +25,22 @@
25
25
  "agents"
26
26
  ],
27
27
  "exports": {
28
- "./server": "./src/server.ts",
29
- "./tui": "./src/tui.ts"
28
+ "./server": {
29
+ "types": "./types/server.d.ts",
30
+ "default": "./dist/server.js"
31
+ },
32
+ "./tui": {
33
+ "types": "./types/tui.d.ts",
34
+ "default": "./dist/tui.js"
35
+ }
30
36
  },
31
37
  "engines": {
32
38
  "opencode": ">=1.18.0",
33
39
  "bun": ">=1.3.5"
34
40
  },
35
41
  "files": [
36
- "src",
42
+ "dist",
43
+ "types",
37
44
  "README.md",
38
45
  "LICENSE"
39
46
  ],
@@ -41,10 +48,10 @@
41
48
  "access": "public"
42
49
  },
43
50
  "dependencies": {
44
- "@opencode-cockpit/shell": "0.1.3",
51
+ "@opencode-cockpit/shell": "0.1.5",
45
52
  "@opencode-ai/plugin": "1.18.31"
46
53
  },
47
54
  "devDependencies": {
48
- "@opencode-cockpit/client": "0.1.3"
55
+ "@opencode-cockpit/client": "0.1.5"
49
56
  }
50
57
  }
@@ -0,0 +1,7 @@
1
+ import type { Hooks } from "@opencode-ai/plugin";
2
+ /**
3
+ * Merges the server hooks of several features into one plugin. Tool maps are unioned (a name
4
+ * clash is a bug, so it throws); function hooks run in feature order, each awaited, which is how
5
+ * OpenCode itself runs hooks from separate plugins.
6
+ */
7
+ export declare function composeHooks(parts: Hooks[]): Hooks;
@@ -0,0 +1,17 @@
1
+ /** Every feature the bundle can load. Adding one: list it here and wire it in server.ts / tui.ts. */
2
+ export declare const FEATURES: readonly ["shell"];
3
+ export type Feature = (typeof FEATURES)[number];
4
+ export interface CockpitOptions {
5
+ /** Switch features off, e.g. `{ "shell": false }`. Everything is on by default. */
6
+ features?: Partial<Record<Feature, boolean>>;
7
+ /** Options passed to a single feature, keyed by feature name. */
8
+ shell?: Record<string, unknown>;
9
+ [key: string]: unknown;
10
+ }
11
+ export declare const BUNDLE = "opencode-cockpit";
12
+ export declare function isEnabled(options: CockpitOptions | undefined, feature: Feature): boolean;
13
+ /**
14
+ * Options for one feature. Shell also accepts options at the top level, which is where 0.1.x
15
+ * (when `opencode-cockpit` was Shell) read them from.
16
+ */
17
+ export declare function featureOptions(options: CockpitOptions | undefined, feature: Feature): Record<string, unknown>;
@@ -0,0 +1,5 @@
1
+ import type { PluginModule } from "@opencode-ai/plugin";
2
+ declare const plugin: PluginModule & {
3
+ id: string;
4
+ };
5
+ export default plugin;
package/types/tui.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ import type { TuiPluginModule } from "@opencode-ai/plugin/tui";
2
+ declare const plugin: TuiPluginModule & {
3
+ id: string;
4
+ };
5
+ export default plugin;
package/src/compose.ts DELETED
@@ -1,43 +0,0 @@
1
- import type { Hooks } from "@opencode-ai/plugin"
2
-
3
- /**
4
- * Merges the server hooks of several features into one plugin. Tool maps are unioned (a name
5
- * clash is a bug, so it throws); function hooks run in feature order, each awaited, which is how
6
- * OpenCode itself runs hooks from separate plugins.
7
- */
8
- export function composeHooks(parts: Hooks[]): Hooks {
9
- const tools: NonNullable<Hooks["tool"]> = {}
10
- const functions = new Map<string, ((...args: unknown[]) => unknown)[]>()
11
- const singles: Record<string, unknown> = {}
12
-
13
- for (const part of parts) {
14
- for (const [key, value] of Object.entries(part)) {
15
- if (value === undefined) continue
16
- if (key === "tool") {
17
- for (const [id, def] of Object.entries(value as NonNullable<Hooks["tool"]>)) {
18
- if (id in tools) throw new Error(`tool "${id}" is registered by more than one cockpit feature`)
19
- tools[id] = def
20
- }
21
- } else if (typeof value === "function") {
22
- const list = functions.get(key) ?? []
23
- list.push(value as (...args: unknown[]) => unknown)
24
- functions.set(key, list)
25
- } else {
26
- if (key in singles) throw new Error(`hook "${key}" is provided by more than one cockpit feature`)
27
- singles[key] = value
28
- }
29
- }
30
- }
31
-
32
- const hooks: Record<string, unknown> = { ...singles }
33
- for (const [key, list] of functions) {
34
- hooks[key] =
35
- list.length === 1
36
- ? list[0]
37
- : async (...args: unknown[]) => {
38
- for (const fn of list) await fn(...args)
39
- }
40
- }
41
- if (Object.keys(tools).length > 0) hooks.tool = tools
42
- return hooks as Hooks
43
- }
package/src/features.ts DELETED
@@ -1,34 +0,0 @@
1
- /** Every feature the bundle can load. Adding one: list it here and wire it in server.ts / tui.ts. */
2
- export const FEATURES = ["shell"] as const
3
- export type Feature = (typeof FEATURES)[number]
4
-
5
- export interface CockpitOptions {
6
- /** Switch features off, e.g. `{ "shell": false }`. Everything is on by default. */
7
- features?: Partial<Record<Feature, boolean>>
8
- /** Options passed to a single feature, keyed by feature name. */
9
- shell?: Record<string, unknown>
10
- [key: string]: unknown
11
- }
12
-
13
- export const BUNDLE = "opencode-cockpit"
14
-
15
- export function isEnabled(options: CockpitOptions | undefined, feature: Feature): boolean {
16
- return options?.features?.[feature] !== false
17
- }
18
-
19
- /**
20
- * Options for one feature. Shell also accepts options at the top level, which is where 0.1.x
21
- * (when `opencode-cockpit` was Shell) read them from.
22
- */
23
- export function featureOptions(
24
- options: CockpitOptions | undefined,
25
- feature: Feature,
26
- ): Record<string, unknown> {
27
- const own = (options?.[feature] as Record<string, unknown> | undefined) ?? {}
28
- if (feature !== "shell" || !options) return own
29
- const legacy: Record<string, unknown> = {}
30
- for (const [key, value] of Object.entries(options)) {
31
- if (key !== "features" && !(FEATURES as readonly string[]).includes(key)) legacy[key] = value
32
- }
33
- return { ...legacy, ...own }
34
- }
package/src/server.ts DELETED
@@ -1,16 +0,0 @@
1
- import type { Hooks, Plugin, PluginModule } from "@opencode-ai/plugin"
2
- import { createShellServer } from "@opencode-cockpit/shell/server"
3
- import { composeHooks } from "./compose.ts"
4
- import { BUNDLE, type CockpitOptions, featureOptions, isEnabled } from "./features.ts"
5
-
6
- const shell = createShellServer({ source: BUNDLE })
7
-
8
- const server: Plugin = async (input, rawOptions) => {
9
- const options = rawOptions as CockpitOptions | undefined
10
- const parts: Hooks[] = []
11
- if (isEnabled(options, "shell")) parts.push(await shell(input, featureOptions(options, "shell")))
12
- return composeHooks(parts)
13
- }
14
-
15
- const plugin: PluginModule & { id: string } = { id: BUNDLE, server }
16
- export default plugin
package/src/tui.ts DELETED
@@ -1,13 +0,0 @@
1
- import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
2
- import { createShellTui } from "@opencode-cockpit/shell/tui"
3
- import { BUNDLE, type CockpitOptions, featureOptions, isEnabled } from "./features.ts"
4
-
5
- const shell = createShellTui({ source: BUNDLE })
6
-
7
- const tui: TuiPlugin = async (api, rawOptions, meta) => {
8
- const options = rawOptions as CockpitOptions | undefined
9
- if (isEnabled(options, "shell")) await shell(api, featureOptions(options, "shell"), meta)
10
- }
11
-
12
- const plugin: TuiPluginModule & { id: string } = { id: BUNDLE, tui }
13
- export default plugin