opencode-remote-login 0.1.0 → 0.1.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 +60 -37
- package/package.json +1 -1
- package/NOTES.md +0 -100
package/README.md
CHANGED
|
@@ -1,75 +1,98 @@
|
|
|
1
1
|
# opencode-remote-login
|
|
2
2
|
|
|
3
|
-
Dispatch opencode sessions to remote hosts
|
|
3
|
+
Dispatch opencode sessions to remote hosts via SSH. Migrate context, execute tasks remotely, and optionally pull results back.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Install
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
```bash
|
|
8
|
+
opencode plug opencode-remote-login
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This adds the plugin to your `opencode.jsonc`. The `remote_login` tool becomes available immediately.
|
|
12
|
+
|
|
13
|
+
## Configure hosts
|
|
14
|
+
|
|
15
|
+
Hosts are defined via a `hosts` entry in one of these locations (checked in order):
|
|
16
|
+
|
|
17
|
+
**Option 1 — `~/.config/opencode/hosts.json`**
|
|
8
18
|
|
|
9
19
|
```json
|
|
10
20
|
{
|
|
11
21
|
"hosts": {
|
|
12
|
-
"pi":
|
|
13
|
-
|
|
22
|
+
"pi": {
|
|
23
|
+
"host": "wenjun@192.168.100.100",
|
|
24
|
+
"agent": "build",
|
|
25
|
+
"model": "opencode/big-pickle"
|
|
26
|
+
}
|
|
14
27
|
}
|
|
15
28
|
}
|
|
16
29
|
```
|
|
17
30
|
|
|
18
|
-
2
|
|
31
|
+
**Option 2 — Inline in `opencode.jsonc`**
|
|
19
32
|
|
|
20
|
-
```
|
|
21
|
-
|
|
33
|
+
```jsonc
|
|
34
|
+
{
|
|
35
|
+
"plugin": [
|
|
36
|
+
["opencode-remote-login", {
|
|
37
|
+
"hosts": {
|
|
38
|
+
"pi": { "host": "wenjun@192.168.100.100" }
|
|
39
|
+
}
|
|
40
|
+
}]
|
|
41
|
+
]
|
|
42
|
+
}
|
|
22
43
|
```
|
|
23
44
|
|
|
24
|
-
|
|
45
|
+
### Host fields
|
|
25
46
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
47
|
+
| Field | Required | Description |
|
|
48
|
+
| ------- | -------- | -------------------------------------- |
|
|
49
|
+
| `host` | Yes | SSH address (`user@host`) |
|
|
50
|
+
| `agent` | No | Override agent on the remote |
|
|
51
|
+
| `model` | No | Override model in `provider/id` format |
|
|
52
|
+
|
|
53
|
+
Only host **names** are shown to the LLM; SSH addresses stay on disk.
|
|
54
|
+
|
|
55
|
+
### SSH setup
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
ssh-copy-id user@192.168.100.100
|
|
30
59
|
```
|
|
31
60
|
|
|
32
61
|
## Usage
|
|
33
62
|
|
|
34
|
-
### One-way
|
|
63
|
+
### One-way
|
|
35
64
|
|
|
36
|
-
|
|
65
|
+
Hand off the session and return. The remote host picks up the task autonomously.
|
|
37
66
|
|
|
38
67
|
```
|
|
39
|
-
> call remote_login host=pi,
|
|
68
|
+
> call remote_login host=pi, fix the auth bug
|
|
40
69
|
```
|
|
41
70
|
|
|
42
|
-
### Round-trip
|
|
71
|
+
### Round-trip
|
|
43
72
|
|
|
44
|
-
|
|
73
|
+
Wait for the remote to finish, then pull results back. The original agent and model are restored automatically on return.
|
|
45
74
|
|
|
46
75
|
```
|
|
47
|
-
> call remote_login mode=round-trip host=pi,
|
|
76
|
+
> call remote_login mode=round-trip host=pi, find the memory leak
|
|
48
77
|
```
|
|
49
78
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
Only host **names** are exposed to the LLM in the tool description. Actual SSH addresses remain private.
|
|
79
|
+
### Target directory
|
|
53
80
|
|
|
54
|
-
```
|
|
55
|
-
|
|
56
|
-
"hosts": {
|
|
57
|
-
"pi": "wenjun@192.168.100.100"
|
|
58
|
-
}
|
|
59
|
-
}
|
|
81
|
+
```
|
|
82
|
+
> call remote_login host=pi directory=/home/wenjun/projects/app
|
|
60
83
|
```
|
|
61
84
|
|
|
62
|
-
|
|
85
|
+
Omit to default to `~`.
|
|
63
86
|
|
|
64
87
|
## How it works
|
|
65
88
|
|
|
66
89
|
```
|
|
67
|
-
local
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
local remote
|
|
91
|
+
───── ──────
|
|
92
|
+
export session JSON
|
|
93
|
+
patch agent/model per host config ────→ SCP + import session
|
|
94
|
+
nohup opencode run --session=<id>
|
|
95
|
+
[round-trip] poll for completion ←── export session JSON
|
|
96
|
+
[round-trip] restore agent/model
|
|
97
|
+
[round-trip] import back
|
|
75
98
|
```
|
package/package.json
CHANGED
package/NOTES.md
DELETED
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
# Implementation Notes
|
|
2
|
-
|
|
3
|
-
## Architecture
|
|
4
|
-
|
|
5
|
-
```
|
|
6
|
-
opencode-remote-login/
|
|
7
|
-
├── package.json # exports: ./server (plugin entry)
|
|
8
|
-
├── hosts.json # host aliases (gitignored)
|
|
9
|
-
├── hosts.example.json # template
|
|
10
|
-
└── src/
|
|
11
|
-
└── index.ts # server plugin (~250 lines)
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
The plugin registers a single tool `remote_login` with two modes (`one-way` / `round-trip`) and a `tool.execute.after` hook that triggers `promptAsync` after round-trip completes.
|
|
15
|
-
|
|
16
|
-
## Round-trip Flow
|
|
17
|
-
|
|
18
|
-
```
|
|
19
|
-
local remote
|
|
20
|
-
───── ──────
|
|
21
|
-
1. opencode export <sid> → stdout JSON
|
|
22
|
-
2. fixPendingRemoteLogin() ← patch pending tool-call in JSON
|
|
23
|
-
3. writeFile → SCP → /tmp/opencode-import-<sid>.json
|
|
24
|
-
4. SSH opencode import → writes to remote SQLite
|
|
25
|
-
5. SCP script → SSH bash → nohup opencode run --session=<sid>
|
|
26
|
-
6. poll: SSH "test -f back-file" → wait for remote export
|
|
27
|
-
7. SCP back-file ← ← opencode export <sid>
|
|
28
|
-
8. opencode import back-file → writes to local SQLite
|
|
29
|
-
9. [hook] promptAsync fork → LLM processes imported context
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Pitfalls and Lessons
|
|
33
|
-
|
|
34
|
-
### 1. `opencode export` captures the tool mid-execution
|
|
35
|
-
|
|
36
|
-
The export subprocess runs while the `remote_login` tool is still executing. The exported JSON shows the tool call in `"pending"` state with empty input. The remote LLM sees an incomplete tool call and gets confused.
|
|
37
|
-
|
|
38
|
-
**Fix**: `fixPendingRemoteLogin()` patches the last assistant message's `remote_login` tool part from `pending` → `completed` with proper output and a `step-finish` part.
|
|
39
|
-
|
|
40
|
-
### 2. Shell quoting across SSH boundaries
|
|
41
|
-
|
|
42
|
-
Building shell commands with nested quoting (ssh → bash → opencode run) is fragile. Double quotes in the continuation message were passed literally to the remote LLM.
|
|
43
|
-
|
|
44
|
-
**Fix**: Write a shell script file locally, SCP it to remote, then execute it. Avoids quoting hell entirely.
|
|
45
|
-
|
|
46
|
-
### 3. `execSync` blocks the event loop
|
|
47
|
-
|
|
48
|
-
`child_process.execSync` is synchronous and blocks Node's event loop. This is fine for shell commands but incompatible with async SDK calls.
|
|
49
|
-
|
|
50
|
-
**Fix**: All SDK calls (`promptAsync`) are done OUTSIDE `execSync` blocks, either after all sync work is done or in hooks.
|
|
51
|
-
|
|
52
|
-
### 4. `session.prompt({ noReply: true })` does not persist
|
|
53
|
-
|
|
54
|
-
Looking at the source (`prompt.ts:1631`): when `noReply === true`, the message is created in memory and returned via HTTP but never written to SQLite. No SSE events are emitted. The TUI never sees it.
|
|
55
|
-
|
|
56
|
-
**Lesson**: Always verify persistence behavior in the source code.
|
|
57
|
-
|
|
58
|
-
### 5. `session.prompt({ noReply: false })` blocks the tool pipeline
|
|
59
|
-
|
|
60
|
-
Calling `session.prompt` (without `noReply`) from within `tool.execute.after` hook blocks the entire tool execution pipeline waiting for LLM processing. The session state transitions cause conflicts.
|
|
61
|
-
|
|
62
|
-
**Lesson**: `tool.execute.after` runs synchronously within the tool execution Effect fiber. Don't block it.
|
|
63
|
-
|
|
64
|
-
### 6. `promptAsync` has a different API shape than `prompt`
|
|
65
|
-
|
|
66
|
-
```ts
|
|
67
|
-
// prompt (v1 SDK)
|
|
68
|
-
session.prompt({ id: string, parts: [...] })
|
|
69
|
-
|
|
70
|
-
// promptAsync (v1 SDK)
|
|
71
|
-
session.promptAsync({ path: { id: string }, body: { parts: [...] } })
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Using the `prompt` shape for `promptAsync` resulted in `{id}` not being substituted in the URL path, causing `%7Bid%7D` errors.
|
|
75
|
-
|
|
76
|
-
**Lesson**: Always check the generated SDK types. Don't assume consistent API shapes.
|
|
77
|
-
|
|
78
|
-
### 7. `promptAsync` forks via `Effect.forkIn(scope)` — scope matters
|
|
79
|
-
|
|
80
|
-
The `promptAsync` handler forks the LLM loop into the HTTP request's scope. The forked fiber runs independently of the caller. The hook returns immediately while the LLM processes in the background. SSE events are emitted for the response.
|
|
81
|
-
|
|
82
|
-
**Key insight**: This is the only way to trigger LLM processing from a plugin hook without blocking. The `Effect.forkIn(scope, { startImmediately: true })` pattern makes the prompt fire-and-forget.
|
|
83
|
-
|
|
84
|
-
### 8. TUI session messages are loaded once, then updated via SSE events only
|
|
85
|
-
|
|
86
|
-
The TUI loads session messages on first entry (`createResource` → `sync.session.sync()`). After that, messages are only added/updated via SSE events (`message.updated`). Direct DB writes by subprocesses produce no SSE events, so the TUI never shows them.
|
|
87
|
-
|
|
88
|
-
**Fix**: Use `promptAsync` (which goes through the server API and emits SSE) to trigger a visible response. The full imported history is in the DB and visible after restart.
|
|
89
|
-
|
|
90
|
-
### 9. Cross-platform `sleep` needs a JavaScript spin-loop
|
|
91
|
-
|
|
92
|
-
`child_process.execSync('sleep 2')` fails on Windows. `execSync('timeout /t 2')` fails on Linux. Using `process.platform` branching is fragile.
|
|
93
|
-
|
|
94
|
-
**Fix**: Simple spin-loop `while (Date.now() < end) {}` works everywhere.
|
|
95
|
-
|
|
96
|
-
### 10. Host config should be separate from code
|
|
97
|
-
|
|
98
|
-
Hardcoding SSH addresses in the plugin is inflexible and leaks info to the LLM.
|
|
99
|
-
|
|
100
|
-
**Fix**: `hosts.json` maps host names to addresses. Only names are injected into the tool description. The file is gitignored; `hosts.example.json` serves as a template.
|