ghosty-acp 0.0.2 → 0.0.4
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 +36 -0
- package/bridge.mjs +41 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,6 +52,23 @@ Install the **ACP Client** extension, then in `settings.json`:
|
|
|
52
52
|
|
|
53
53
|
The token is optional — omit it if the other end doesn't require one.
|
|
54
54
|
|
|
55
|
+
## The working directory (`cwd`)
|
|
56
|
+
|
|
57
|
+
Your editor runs on your machine; the agent lives in a remote box, and they share no disk.
|
|
58
|
+
Every ACP client sends the path of the project **open on your machine** in `session/new` —
|
|
59
|
+
a path that does not exist over there. The agent rejects it (`Invalid params: invalid
|
|
60
|
+
directory path`) and editors show it as *"Failed to connect"*, which sends you off to check
|
|
61
|
+
the URL and the token, the two things that were fine.
|
|
62
|
+
|
|
63
|
+
So the bridge rewrites that one field: `cwd` becomes `/root`, or whatever you set in
|
|
64
|
+
`GHOSTY_ACP_CWD`. Nothing else in the message is touched.
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
"env": { "GHOSTY_ACP_TOKEN": "…", "GHOSTY_ACP_CWD": "/workspace" }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The agent edits **what is in the box**, not your working copy.
|
|
71
|
+
|
|
55
72
|
## ⚠️ Put the token in `env`, never in `args`
|
|
56
73
|
|
|
57
74
|
Any process on your machine can read another process's arguments with `ps`. The token is
|
|
@@ -67,6 +84,7 @@ else there breaks the editor.
|
|
|
67
84
|
| `rechazado (1006)` | Bad credential, or the server refused the connection |
|
|
68
85
|
| `la máquina estaba en reposo…` | The agent was asleep; connecting wakes it |
|
|
69
86
|
| `conexión cerrada (…)` | The other end closed; the bridge exits so the editor notices |
|
|
87
|
+
| `cwd … → …` | The project path was remapped to one the agent can see |
|
|
70
88
|
|
|
71
89
|
Requires Node 22+. MIT.
|
|
72
90
|
|
|
@@ -127,6 +145,23 @@ En `settings.json`:
|
|
|
127
145
|
|
|
128
146
|
El token es opcional: si el otro extremo no pide credencial, omítelo.
|
|
129
147
|
|
|
148
|
+
## El directorio de trabajo (`cwd`)
|
|
149
|
+
|
|
150
|
+
Tu editor corre en tu máquina y el agente vive en una caja remota: no comparten disco.
|
|
151
|
+
Todo cliente ACP manda en `session/new` la ruta del proyecto **abierto en tu máquina**, una
|
|
152
|
+
ruta que allá no existe. El agente la rechaza (`Invalid params: invalid directory path`) y
|
|
153
|
+
los editores lo pintan como *«Failed to connect»*, que manda a revisar la URL y el token
|
|
154
|
+
—justo lo que sí estaba bien—.
|
|
155
|
+
|
|
156
|
+
Por eso el puente reescribe ese único campo: `cwd` pasa a ser `/root`, o lo que pongas en
|
|
157
|
+
`GHOSTY_ACP_CWD`. Del resto del mensaje no toca nada.
|
|
158
|
+
|
|
159
|
+
```json
|
|
160
|
+
"env": { "GHOSTY_ACP_TOKEN": "…", "GHOSTY_ACP_CWD": "/workspace" }
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
El agente edita **lo que hay en la caja**, no tu copia de trabajo.
|
|
164
|
+
|
|
130
165
|
## ⚠️ El token va en `env`, nunca en `args`
|
|
131
166
|
|
|
132
167
|
Los argumentos de un proceso los puede leer cualquier otro proceso de la máquina con un
|
|
@@ -142,5 +177,6 @@ flujo JSON-RPC, porque cualquier otra cosa ahí rompe al editor.
|
|
|
142
177
|
| `rechazado (1006)` | La credencial no vale, o el servidor no aceptó la conexión |
|
|
143
178
|
| `la máquina estaba en reposo…` | El agente estaba dormido; la conexión lo despierta |
|
|
144
179
|
| `conexión cerrada (…)` | El otro extremo cerró; el puente sale para que el editor se entere |
|
|
180
|
+
| `cwd … → …` | Se remapeó la ruta del proyecto a una que el agente sí ve |
|
|
145
181
|
|
|
146
182
|
Necesita Node 22 o superior. MIT.
|
package/bridge.mjs
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
|
|
18
18
|
const url = process.argv[2];
|
|
19
19
|
const token = process.env.GHOSTY_ACP_TOKEN || "";
|
|
20
|
+
const cwdRemoto = process.env.GHOSTY_ACP_CWD || "/root";
|
|
20
21
|
|
|
21
22
|
if (!url) {
|
|
22
23
|
console.error("uso: node bridge.mjs <wss://…/acp> (token en GHOSTY_ACP_TOKEN)");
|
|
@@ -47,8 +48,11 @@ const ws = new WebSocket(target);
|
|
|
47
48
|
// sueltan en orden al abrir.
|
|
48
49
|
let cola = [];
|
|
49
50
|
|
|
51
|
+
let abierto = false;
|
|
52
|
+
|
|
50
53
|
ws.onopen = () => {
|
|
51
54
|
clearTimeout(avisoLento);
|
|
55
|
+
abierto = true;
|
|
52
56
|
diag("listo");
|
|
53
57
|
for (const linea of cola) ws.send(linea + "\n");
|
|
54
58
|
cola = [];
|
|
@@ -65,7 +69,9 @@ ws.onclose = (e) => {
|
|
|
65
69
|
clearTimeout(avisoLento);
|
|
66
70
|
// 1006 sin motivo es lo que se ve cuando el servidor rechaza el upgrade: casi siempre
|
|
67
71
|
// el token. Decirlo aquí evita el rato de mirar el editor pensando que es cosa suya.
|
|
68
|
-
|
|
72
|
+
// Sólo es un rechazo si nunca llegó a abrir: un 1006 tras «listo» es la caja o la red
|
|
73
|
+
// cortando una conexión que sí autenticó, y mandar a revisar el token ahí despista.
|
|
74
|
+
if (e.code === 1006 && !e.reason && !abierto) {
|
|
69
75
|
diag(token ? "rechazado (1006): revisa el token de este agente" : "rechazado (1006): falta GHOSTY_ACP_TOKEN");
|
|
70
76
|
} else {
|
|
71
77
|
diag(`conexión cerrada (${e.code}${e.reason ? `: ${e.reason}` : ""})`);
|
|
@@ -91,6 +97,35 @@ ws.onmessage = (e) => {
|
|
|
91
97
|
}
|
|
92
98
|
};
|
|
93
99
|
|
|
100
|
+
// ── `cwd`: LO ÚNICO que este puente traduce ──────────────────────────────────────
|
|
101
|
+
//
|
|
102
|
+
// Rompe a propósito el "no traduce nada" de arriba, y vale la pena decir por qué. El
|
|
103
|
+
// editor corre en tu máquina y el agente vive en una microVM: NO comparten disco. Todo
|
|
104
|
+
// cliente ACP manda en `session/new` el directorio del proyecto ABIERTO EN TU MÁQUINA
|
|
105
|
+
// (`/Users/tu/proyecto`), una ruta que dentro de la caja sencillamente no existe. goose
|
|
106
|
+
// la rechaza con `-32602 Invalid params: invalid directory path`, y los editores pintan
|
|
107
|
+
// eso como «Failed to connect» — un mensaje que manda a revisar la URL y el token, que
|
|
108
|
+
// son justamente lo que sí estaba bien. Se pierde la tarde.
|
|
109
|
+
//
|
|
110
|
+
// Se reescribe SÓLO el campo `cwd` y SÓLO en los dos métodos que lo llevan. El resto del
|
|
111
|
+
// mensaje pasa intacto; si no es JSON, o no es uno de esos métodos, no se toca nada.
|
|
112
|
+
const METODOS_CON_CWD = new Set(["session/new", "session/load"]);
|
|
113
|
+
|
|
114
|
+
const remapearCwd = (linea) => {
|
|
115
|
+
// Un mensaje que no parsea no es asunto nuestro: se mueve tal cual, como todo lo demás.
|
|
116
|
+
let msg;
|
|
117
|
+
try {
|
|
118
|
+
msg = JSON.parse(linea);
|
|
119
|
+
} catch {
|
|
120
|
+
return linea;
|
|
121
|
+
}
|
|
122
|
+
if (!METODOS_CON_CWD.has(msg?.method) || typeof msg?.params?.cwd !== "string") return linea;
|
|
123
|
+
if (msg.params.cwd === cwdRemoto) return linea;
|
|
124
|
+
diag(`cwd ${msg.params.cwd} → ${cwdRemoto} (el agente no ve tu disco)`);
|
|
125
|
+
msg.params.cwd = cwdRemoto;
|
|
126
|
+
return JSON.stringify(msg);
|
|
127
|
+
};
|
|
128
|
+
|
|
94
129
|
// ── editor → caja ────────────────────────────────────────────────────────────────
|
|
95
130
|
//
|
|
96
131
|
// Aquí SÍ hace falta acumular: stdin es un flujo y un mensaje puede llegar partido en
|
|
@@ -101,9 +136,12 @@ process.stdin.on("data", (chunk) => {
|
|
|
101
136
|
buffer += chunk;
|
|
102
137
|
let nl;
|
|
103
138
|
while ((nl = buffer.indexOf("\n")) !== -1) {
|
|
104
|
-
const
|
|
139
|
+
const cruda = buffer.slice(0, nl).trim();
|
|
105
140
|
buffer = buffer.slice(nl + 1);
|
|
106
|
-
if (!
|
|
141
|
+
if (!cruda) continue;
|
|
142
|
+
// Antes de la cola y antes del envío: así la línea que se guarda dormida ya va
|
|
143
|
+
// corregida y no hay dos caminos que mantener.
|
|
144
|
+
const linea = remapearCwd(cruda);
|
|
107
145
|
// Si el socket aún no está abierto la línea se descarta y se dice: mandarla al vacío
|
|
108
146
|
// en silencio dejaría al editor esperando para siempre una respuesta que nadie oyó.
|
|
109
147
|
if (ws.readyState === WebSocket.CONNECTING) {
|
package/package.json
CHANGED