opencode-miniterm 1.0.0 → 1.0.2

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
@@ -2,4 +2,167 @@
2
2
 
3
3
  A small front-end terminal UI for [OpenCode](https://github.com/anomalyco/opencode).
4
4
 
5
- This project is not affiliated with OpenCode.
5
+ > **Note:** This project is not affiliated with OpenCode.
6
+
7
+ ## Features
8
+
9
+ - **Slash Commands** - Quick access to common operations
10
+ - **File Auto-Completion** - Type `@` followed by file path for intelligent completions
11
+ - **Real-Time Streaming** - See AI responses as they're being generated
12
+ - **Logging Support** - Optional conversation logging for debugging
13
+ - **Keyboard Navigation** - Readline support with history and editing
14
+
15
+ ## Installation
16
+
17
+ ### Prerequisites
18
+
19
+ - [OpenCode](https://github.com/anomalyco/opencode) - OpenCode server
20
+ - [Bun](https://bun.sh/) - Required runtime
21
+
22
+ ### Install from npm
23
+
24
+ ```bash
25
+ npm install -g opencode-miniterm
26
+ # or
27
+ pnpm add -g opencode-miniterm
28
+ ```
29
+
30
+ ### Install from Source
31
+
32
+ ```bash
33
+ git clone https://github.com/your-repo/opencode-miniterm.git
34
+ cd opencode-miniterm
35
+ bun install
36
+ bun link
37
+ ```
38
+
39
+ ### Quick Start
40
+
41
+ ```bash
42
+ ocmt
43
+ ```
44
+
45
+ This will:
46
+
47
+ 1. Start the OpenCode server (if not already running)
48
+ 2. Create or resume a session for the current directory
49
+ 3. Present the interactive prompt
50
+
51
+ ## Configuration
52
+
53
+ Configuration is stored in `~/.config/opencode-miniterm/opencode-miniterm.json`:
54
+
55
+ ```json
56
+ {
57
+ "providerID": "opencode",
58
+ "modelID": "big-pickle",
59
+ "agentID": "build",
60
+ "sessionIDs": {
61
+ "/path/to/project1": "session-id-1",
62
+ "/path/to/project2": "session-id-2"
63
+ },
64
+ "loggingEnabled": false
65
+ }
66
+ ```
67
+
68
+ ### Environment Variables
69
+
70
+ - `OPENCODE_SERVER_USERNAME` - Server username (default: "opencode")
71
+ - `OPENCODE_SERVER_PASSWORD` - Server password (required if server has auth)
72
+ - `OPENCODE_MT_CONFIG_CONTENT` - Override config as JSON string
73
+
74
+ ## Usage
75
+
76
+ ### Basic Interaction
77
+
78
+ Simply type your question or request at the prompt and press Enter:
79
+
80
+ ```
81
+ > Help me fix the bug in auth.ts
82
+ ```
83
+
84
+ ### Slash Commands
85
+
86
+ | Command | Description |
87
+ | ------------------ | ------------------------------------------- |
88
+ | `/help` | Show available commands |
89
+ | `/init` | Analyze project and create/update AGENTS.md |
90
+ | `/new` | Create a new session |
91
+ | `/sessions` | List and switch sessions |
92
+ | `/diff` | Show file additions and deletions |
93
+ | `/undo` | Undo last assistant request |
94
+ | `/details` | Show detailed info for the previous request |
95
+ | `/page` | Page through the detailed info |
96
+ | `/agents` | Show available agents |
97
+ | `/models` | Show available models |
98
+ | `/log` | Enable/disable logging |
99
+ | `/run <cmd>` | Run a shell command from within miniterm |
100
+ | `/exit` or `/quit` | Exit the application |
101
+
102
+ ### File References
103
+
104
+ Reference files in your conversation using `@` followed by the path:
105
+
106
+ ```
107
+ > Review @src/index.ts and suggest improvements
108
+ ```
109
+
110
+ Tab completion is supported for file paths:
111
+
112
+ ```
113
+ > @sr<tab> → @src/
114
+ > @src/in<tab> → @src/index.ts
115
+ ```
116
+
117
+ ### Keyboard Shortcuts
118
+
119
+ | Key | Action |
120
+ | ---------------------- | ---------------------------- |
121
+ | `↑` / `↓` | Navigate command history |
122
+ | `←` / `→` | Move cursor |
123
+ | `Opt+←` / `Opt+→` | Move by word boundaries |
124
+ | `Tab` | Auto-complete commands/files |
125
+ | `Backspace` / `Delete` | Delete characters |
126
+ | `Esc` | Cancel current request |
127
+ | `Ctrl+C` | Force quit application |
128
+
129
+ ## Session Management
130
+
131
+ OpenCode Miniterm automatically manages sessions per directory:
132
+
133
+ - **First Launch**: Creates a new session for the current directory
134
+ - **Subsequent Launches**: Resumes the last session for that directory
135
+ - **New Session**: Use `/new` to create a fresh session
136
+ - **Switch Sessions**: Use `/sessions` to browse and switch between all your sessions
137
+
138
+ ## Development
139
+
140
+ ### Running Locally
141
+
142
+ ```bash
143
+ bun run dev
144
+ # or
145
+ bun src/index.ts
146
+ ```
147
+
148
+ ### Build
149
+
150
+ ```bash
151
+ bun build src/index.ts --outdir dist
152
+ ```
153
+
154
+ ### Type Check
155
+
156
+ ```bash
157
+ bun run check
158
+ ```
159
+
160
+ ### Formatting
161
+
162
+ ```bash
163
+ bunx prettier --write "**/*.{ts,json,md}"
164
+ ```
165
+
166
+ ## License
167
+
168
+ ISC
package/bun.lock CHANGED
@@ -5,7 +5,7 @@
5
5
  "": {
6
6
  "name": "opencode-miniterm",
7
7
  "dependencies": {
8
- "@opencode-ai/sdk": "^1.2.10",
8
+ "@opencode-ai/sdk": "^1.2.14",
9
9
  "allmark": "^1.0.0",
10
10
  },
11
11
  "devDependencies": {
@@ -47,7 +47,7 @@
47
47
 
48
48
  "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
49
49
 
50
- "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.10", "", {}, "sha512-SyXcVqry2hitPVvQtvXOhqsWyFhSycG/+LTLYXrcq8AFmd9FR7dyBSDB3f5Ol6IPkYOegk8P2Eg2kKPNSNiKGw=="],
50
+ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.2.14", "", {}, "sha512-nPkWAmzgPJYyfCJAV4NG7HTfN/iuO3B6fv8sT26NhPiR+EqD9i8sh4X1LwI7wEbbMOwWOX1PhrssW6gXQOOQZQ=="],
51
51
 
52
52
  "@trivago/prettier-plugin-sort-imports": ["@trivago/prettier-plugin-sort-imports@6.0.2", "", { "dependencies": { "@babel/generator": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "javascript-natural-sort": "^0.7.1", "lodash-es": "^4.17.21", "minimatch": "^9.0.0", "parse-imports-exports": "^0.2.4" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", "prettier-plugin-ember-template-tag": ">= 2.0.0", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, "optionalPeers": ["@vue/compiler-sfc", "prettier-plugin-ember-template-tag", "prettier-plugin-svelte", "svelte"] }, "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA=="],
53
53
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-miniterm",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "A small front-end terminal UI for OpenCode",
5
5
  "main": "src/index.ts",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "typescript": "^5"
29
29
  },
30
30
  "dependencies": {
31
- "@opencode-ai/sdk": "^1.2.10",
31
+ "@opencode-ai/sdk": "^1.2.14",
32
32
  "allmark": "^1.0.0"
33
33
  }
34
34
  }
@@ -20,7 +20,8 @@ interface DiffLine {
20
20
  }
21
21
 
22
22
  async function run(client: OpencodeClient, state: State): Promise<void> {
23
- if (!config.sessionID) {
23
+ const cwd = process.cwd();
24
+ if (!config.sessionIDs[cwd]) {
24
25
  console.log("No active session.\n");
25
26
  return;
26
27
  }
@@ -28,7 +29,7 @@ async function run(client: OpencodeClient, state: State): Promise<void> {
28
29
  console.log("Fetching file changes...");
29
30
 
30
31
  const result = await client.session.diff({
31
- path: { id: config.sessionID },
32
+ path: { id: config.sessionIDs[cwd] },
32
33
  });
33
34
 
34
35
  if (result.error) {
@@ -14,11 +14,12 @@ let command: Command = {
14
14
  export default command;
15
15
 
16
16
  async function run(_client: OpencodeClient, _state: State): Promise<void> {
17
- if (!config.sessionID) return;
17
+ const cwd = process.cwd();
18
+ if (!config.sessionIDs[cwd]) return;
18
19
 
19
20
  console.log("Running /init command (analyzing project and creating AGENTS.md)...");
20
21
  const result = await _client.session.init({
21
- path: { id: config.sessionID },
22
+ path: { id: config.sessionIDs[cwd] },
22
23
  });
23
24
 
24
25
  if (result.error) {
@@ -16,7 +16,7 @@ export default command;
16
16
 
17
17
  async function run(client: OpencodeClient, state: State): Promise<void> {
18
18
  state.sessionID = await createSession(client);
19
- config.sessionID = state.sessionID;
19
+ config.sessionIDs[process.cwd()] = state.sessionID;
20
20
  saveConfig();
21
21
 
22
22
  await updateSessionTitle();
@@ -44,7 +44,7 @@ async function run(client: OpencodeClient, state: State): Promise<void> {
44
44
  if (sessions.length === 0) {
45
45
  console.log("No sessions found. Creating a new session...");
46
46
  state.sessionID = await createSession(client);
47
- config.sessionID = state.sessionID;
47
+ config.sessionIDs[process.cwd()] = state.sessionID;
48
48
  saveConfig();
49
49
  console.log(`Created new session: ${state.sessionID}...\n`);
50
50
  await updateSessionTitle();
@@ -135,7 +135,7 @@ async function handleKey(_client: OpencodeClient, key: Key, str?: string) {
135
135
  readline.cursorTo(process.stdout, 0);
136
136
  readline.clearScreenDown(process.stdout);
137
137
  if (selected) {
138
- config.sessionID = selected.id;
138
+ config.sessionIDs[process.cwd()] = selected.id;
139
139
  saveConfig();
140
140
  console.log(`Switched to session: ${selected.id.substring(0, 8)}...`);
141
141
  if (selected.title) {
@@ -222,7 +222,7 @@ function renderSessionList(): void {
222
222
  const globalIndex = sessionList.indexOf(session);
223
223
  const filteredIndex = sessionFilteredIndices.indexOf(globalIndex);
224
224
  const isSelected = filteredIndex === selectedSessionIndex;
225
- const isActive = session.id === config.sessionID;
225
+ const isActive = session.id === config.sessionIDs[process.cwd()];
226
226
  const prefix = isSelected ? " >" : " -";
227
227
  const title = session.title || "(no title)";
228
228
  const name = isSelected ? `\x1b[33;1m${title}\x1b[0m` : title;
@@ -250,7 +250,7 @@ function updateSessionFilter(): void {
250
250
  }
251
251
  if (sessionFilteredIndices.length > 0) {
252
252
  selectedSessionIndex = sessionFilteredIndices.indexOf(
253
- sessionList.findIndex((s) => s.id === config.sessionID),
253
+ sessionList.findIndex((s) => s.id === config.sessionIDs[process.cwd()]),
254
254
  );
255
255
  if (selectedSessionIndex === -1) selectedSessionIndex = 0;
256
256
  }
@@ -13,12 +13,13 @@ let command: Command = {
13
13
  export default command;
14
14
 
15
15
  async function run(client: OpencodeClient, _state: State): Promise<void> {
16
- if (!config.sessionID) return;
16
+ const cwd = process.cwd();
17
+ if (!config.sessionIDs[cwd]) return;
17
18
 
18
19
  console.log("Fetching session messages...");
19
20
 
20
21
  const messagesRes = await client.session.messages({
21
- path: { id: config.sessionID },
22
+ path: { id: config.sessionIDs[cwd] },
22
23
  });
23
24
 
24
25
  if (messagesRes.error) {
@@ -49,7 +50,7 @@ async function run(client: OpencodeClient, _state: State): Promise<void> {
49
50
  console.log(`Reverting last assistant message (${lastMessage.info.id})...`);
50
51
 
51
52
  const revertRes = await client.session.revert({
52
- path: { id: config.sessionID },
53
+ path: { id: config.sessionIDs[process.cwd()] },
53
54
  body: {
54
55
  messageID: lastMessage.info.id,
55
56
  },
package/src/config.ts CHANGED
@@ -5,7 +5,7 @@ export interface Config {
5
5
  providerID: string;
6
6
  modelID: string;
7
7
  agentID: string;
8
- sessionID?: string;
8
+ sessionIDs: Record<string, string>;
9
9
  loggingEnabled: boolean;
10
10
  }
11
11
 
@@ -15,6 +15,7 @@ export const config: Config = {
15
15
  providerID: "opencode",
16
16
  modelID: "big-pickle",
17
17
  agentID: "build",
18
+ sessionIDs: {},
18
19
  loggingEnabled: false,
19
20
  };
20
21
 
package/src/index.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env bun
2
2
 
3
- import { createOpencodeClient } from "@opencode-ai/sdk";
3
+ import { createOpencodeClient, createOpencodeServer } from "@opencode-ai/sdk";
4
4
  import type { Event, FileDiff, Part, Todo, ToolPart } from "@opencode-ai/sdk";
5
- import { spawn } from "node:child_process";
6
5
  import { mkdir } from "node:fs/promises";
7
6
  import { glob } from "node:fs/promises";
8
7
  import { stat } from "node:fs/promises";
@@ -15,7 +14,6 @@ import detailsCommand from "./commands/details";
15
14
  import diffCommand from "./commands/diff";
16
15
  import exitCommand from "./commands/exit";
17
16
  import initCommand from "./commands/init";
18
- import killCommand from "./commands/kill";
19
17
  import logCommand, { isLoggingEnabled } from "./commands/log";
20
18
  import modelsCommand from "./commands/models";
21
19
  import newCommand from "./commands/new";
@@ -43,7 +41,6 @@ const SLASH_COMMANDS = [
43
41
  debugCommand,
44
42
  logCommand,
45
43
  pageCommand,
46
- killCommand,
47
44
  exitCommand,
48
45
  quitCommand,
49
46
  runCommand,
@@ -53,7 +50,6 @@ let client: ReturnType<typeof createOpencodeClient>;
53
50
 
54
51
  let processing = true;
55
52
  let retryInterval: ReturnType<typeof setInterval> | null = null;
56
- let messageAbortController: AbortController | null = null;
57
53
 
58
54
  interface AccumulatedPart {
59
55
  key: string;
@@ -93,8 +89,17 @@ let logFilePath: string | null = null;
93
89
  async function main() {
94
90
  loadConfig();
95
91
 
96
- const serverProcess = await startOpenCodeServer();
92
+ console.log(`\n${ansi.BRIGHT_BLACK}Connecting to OpenCode server...${ansi.RESET}\n`);
97
93
 
94
+ let server: Awaited<ReturnType<typeof createOpencodeServer>> | undefined;
95
+ try {
96
+ server = await createOpencodeServer();
97
+ } catch {
98
+ // Probably the server already exists?
99
+ // Should figure out a better way to check this
100
+ }
101
+
102
+ const cwd = process.cwd();
98
103
  client = createOpencodeClient({
99
104
  baseUrl: SERVER_URL,
100
105
  headers: AUTH_PASSWORD
@@ -102,16 +107,24 @@ async function main() {
102
107
  Authorization: `Basic ${Buffer.from(`${AUTH_USERNAME}:${AUTH_PASSWORD}`).toString("base64")}`,
103
108
  }
104
109
  : undefined,
110
+ directory: cwd,
111
+ });
112
+
113
+ process.on("SIGINT", () => {
114
+ console.log("\nShutting down...");
115
+ saveConfig();
116
+ server?.close();
117
+ process.exit(0);
105
118
  });
106
119
 
107
120
  try {
108
121
  let isNewSession = false;
109
122
 
110
- const initialSessionID = config.sessionID;
123
+ const initialSessionID = config.sessionIDs[cwd];
111
124
  if (!initialSessionID || !(await validateSession(initialSessionID))) {
112
125
  state.sessionID = await createSession();
113
126
  isNewSession = true;
114
- config.sessionID = state.sessionID;
127
+ config.sessionIDs[cwd] = state.sessionID;
115
128
  saveConfig();
116
129
  } else {
117
130
  state.sessionID = initialSessionID;
@@ -330,21 +343,13 @@ async function main() {
330
343
  return;
331
344
  }
332
345
  case "escape": {
333
- if (messageAbortController) {
334
- messageAbortController.abort();
335
- stopAnimation();
336
- process.stdout.write(ansi.CURSOR_SHOW);
337
- process.stdout.write(`\r${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
338
- } else {
339
- inputBuffer = "";
340
- cursorPosition = 0;
341
- showCompletions = false;
342
- completionCycling = false;
343
- completions = [];
344
- readline.cursorTo(process.stdout, 0);
345
- readline.clearScreenDown(process.stdout);
346
- writePrompt();
346
+ if (state.sessionID) {
347
+ client.session.abort({ path: { id: state.sessionID } }).catch(() => {});
347
348
  }
349
+ stopAnimation();
350
+ process.stdout.write(ansi.CURSOR_SHOW);
351
+ process.stdout.write(`\r${ansi.BRIGHT_BLACK}Cancelled request${ansi.RESET}\n`);
352
+ writePrompt();
348
353
  return;
349
354
  }
350
355
  case "return": {
@@ -400,7 +405,7 @@ async function main() {
400
405
  writePrompt();
401
406
  } catch (error: any) {
402
407
  console.error("Error:", error.message);
403
- serverProcess.kill();
408
+ server?.close();
404
409
  process.exit(1);
405
410
  }
406
411
  }
@@ -409,46 +414,6 @@ async function main() {
409
414
  // SERVER COMMUNICATION
410
415
  // ====================
411
416
 
412
- async function startOpenCodeServer() {
413
- const serverProcess = spawn("opencode", ["serve"], {
414
- stdio: ["ignore", "pipe", "pipe"],
415
- shell: true,
416
- cwd: process.cwd(),
417
- });
418
-
419
- let started = false;
420
-
421
- console.log(`\n${ansi.BRIGHT_BLACK}Starting OpenCode server...${ansi.RESET}\n`);
422
-
423
- serverProcess.stdout.on("data", (data) => {
424
- if (!started) {
425
- process.stdout.write(`${ansi.CLEAR_SCREEN_UP}${ansi.CLEAR_FROM_CURSOR}`);
426
- process.stdout.write(ansi.CURSOR_HOME);
427
- started = true;
428
- console.log(`${ansi.BRIGHT_BLACK}Server started, connecting...${ansi.RESET}\n`);
429
- }
430
- });
431
-
432
- serverProcess.on("error", (error) => {
433
- console.error("Failed to start OpenCode server:", error.message);
434
- process.exit(1);
435
- });
436
-
437
- serverProcess.on("exit", (code) => {
438
- console.log(`OpenCode server exited with code ${code}`);
439
- process.exit(0);
440
- });
441
-
442
- process.on("SIGINT", () => {
443
- console.log("\nShutting down...");
444
- saveConfig();
445
- serverProcess.kill("SIGINT");
446
- });
447
-
448
- await new Promise((resolve) => setTimeout(resolve, 3000));
449
- return serverProcess;
450
- }
451
-
452
417
  async function createSession(): Promise<string> {
453
418
  const result = await client.session.create({
454
419
  body: {},
@@ -533,8 +498,6 @@ async function sendMessage(sessionID: string, message: string) {
533
498
 
534
499
  await writeToLog(`User: ${message}\n\n`);
535
500
 
536
- messageAbortController = new AbortController();
537
-
538
501
  const requestStartTime = Date.now();
539
502
 
540
503
  try {
@@ -547,7 +510,6 @@ async function sendMessage(sessionID: string, message: string) {
547
510
  },
548
511
  parts: [{ type: "text", text: message }],
549
512
  },
550
- signal: messageAbortController.signal,
551
513
  });
552
514
 
553
515
  if (result.error) {
@@ -566,14 +528,14 @@ async function sendMessage(sessionID: string, message: string) {
566
528
  console.log(`${ansi.BRIGHT_BLACK}Completed in ${durationText}${ansi.RESET}\n`);
567
529
 
568
530
  writePrompt();
569
- readline.cursorTo(process.stdout, 2);
531
+
532
+ // HACK:
533
+ setTimeout(() => {
534
+ readline.cursorTo(process.stdout, 2);
535
+ }, 200);
570
536
  } catch (error: any) {
571
- if (error.name === "AbortError" || messageAbortController?.signal.aborted) {
572
- throw new Error("Request cancelled");
573
- }
574
537
  throw error;
575
538
  } finally {
576
- messageAbortController = null;
577
539
  await closeLogFile();
578
540
  }
579
541
  }
@@ -1,33 +0,0 @@
1
- import type { OpencodeClient } from "@opencode-ai/sdk";
2
- import type { State } from "../index";
3
- import type { Command } from "../types";
4
-
5
- let command: Command = {
6
- name: "/kill",
7
- description: "Abort a session (e.g. `/kill ses_123`)",
8
- run,
9
- running: false,
10
- };
11
-
12
- export default command;
13
-
14
- async function run(client: OpencodeClient, _state: State, input?: string): Promise<void> {
15
- if (!input) {
16
- console.log("Usage: /kill <session_id>");
17
- console.log();
18
- return;
19
- }
20
-
21
- const result = await client.session.abort({
22
- path: { id: input },
23
- });
24
-
25
- if (result.error) {
26
- throw new Error(
27
- `Failed to abort session (${result.response.status}): ${JSON.stringify(result.error)}`,
28
- );
29
- }
30
-
31
- console.log(`Session aborted successfully.`);
32
- console.log();
33
- }