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.
Files changed (3) hide show
  1. package/README.md +36 -0
  2. package/bridge.mjs +41 -3
  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
- if (e.code === 1006 && !e.reason) {
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 linea = buffer.slice(0, nl).trim();
139
+ const cruda = buffer.slice(0, nl).trim();
105
140
  buffer = buffer.slice(nl + 1);
106
- if (!linea) continue;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghosty-acp",
3
- "version": "0.0.2",
3
+ "version": "0.0.4",
4
4
  "description": "Conecta tu editor a un agente ACP remoto. Puente entre entrada/salida estándar y WebSocket, sin dependencias.",
5
5
  "type": "module",
6
6
  "bin": { "ghosty-acp": "bridge.mjs" },