@nac3/forge-cli 1.8.4 → 1.8.6
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/dist/chat/claude.d.ts +11 -0
- package/dist/chat/claude.d.ts.map +1 -1
- package/dist/chat/claude.js.map +1 -1
- package/dist/chat/claude_cli.d.ts.map +1 -1
- package/dist/chat/claude_cli.js +11 -2
- package/dist/chat/claude_cli.js.map +1 -1
- package/dist/chat/panel.d.ts.map +1 -1
- package/dist/chat/panel.js +149 -106
- package/dist/chat/panel.js.map +1 -1
- package/dist/chat/server.d.ts.map +1 -1
- package/dist/chat/server.js +111 -14
- package/dist/chat/server.js.map +1 -1
- package/dist/core/budget.d.ts +1 -1
- package/dist/core/budget.d.ts.map +1 -1
- package/dist/core/budget.js +41 -3
- package/dist/core/budget.js.map +1 -1
- package/dist/core/projects.d.ts +10 -4
- package/dist/core/projects.d.ts.map +1 -1
- package/dist/core/projects.js +42 -2
- package/dist/core/projects.js.map +1 -1
- package/dist/pending/store.d.ts.map +1 -1
- package/dist/pending/store.js +9 -0
- package/dist/pending/store.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/chat/server.js
CHANGED
|
@@ -131,6 +131,49 @@ export async function startChatServer(opts) {
|
|
|
131
131
|
* CLI + panel converge. */
|
|
132
132
|
const { resolveProjectSlug } = await import('./project_slug.js');
|
|
133
133
|
const projectSlug = await resolveProjectSlug(opts.projectRoot);
|
|
134
|
+
/* COLISION DE SLUG (Pablo 2026-10-04). El slug es la clave de identidad de
|
|
135
|
+
* TODO lo que es por proyecto: el grafo semantico, los pendings, el wiring,
|
|
136
|
+
* el cerebro, y la entrada del registro que alimenta el selector del
|
|
137
|
+
* celular. Dos carpetas distintas que resuelven al mismo slug comparten
|
|
138
|
+
* todo eso en silencio.
|
|
139
|
+
*
|
|
140
|
+
* Paso de verdad: C:\yujin-kikan (el CRM generico) y C:\adip\kikan (el
|
|
141
|
+
* adaptado para ADIP, que es una copia y por eso tiene el MISMO
|
|
142
|
+
* package.json name) resolvian los dos a 'yujin-kikan'. Resultado: un solo
|
|
143
|
+
* pending.json de 354KB y un solo grafo para los dos proyectos, y la
|
|
144
|
+
* entrada del registro apuntando a la carpeta de quien arranco ultimo -- el
|
|
145
|
+
* celular mostraba un proyecto y abria el otro.
|
|
146
|
+
*
|
|
147
|
+
* No se renombra nada solo: cambiarle el slug a un proyecto le huerfana el
|
|
148
|
+
* grafo y los pendings que ya tiene. Se AVISA, fuerte, con el arreglo
|
|
149
|
+
* exacto -- un `project_slug` explicito en yujin.forge.json, que el
|
|
150
|
+
* resolver respeta antes que cualquier otra cosa. */
|
|
151
|
+
try {
|
|
152
|
+
const reg = await readRegistry();
|
|
153
|
+
const choque = (reg.projects || []).find((p) => {
|
|
154
|
+
if (p.slug !== projectSlug || !p.path)
|
|
155
|
+
return false;
|
|
156
|
+
const norm = (s) => s.replace(/[\\/]+/g, '/').replace(/\/+$/, '').toLowerCase();
|
|
157
|
+
return norm(p.path) !== norm(opts.projectRoot);
|
|
158
|
+
});
|
|
159
|
+
if (choque) {
|
|
160
|
+
const aviso = '[yf] ATENCION: colision de slug. "' + projectSlug + '" ya estaba usado por '
|
|
161
|
+
+ choque.path + ' y esta carpeta es ' + opts.projectRoot + '. Los dos proyectos comparten '
|
|
162
|
+
+ 'grafo, pendings, wiring y cerebro, y el selector del celular no puede distinguirlos. '
|
|
163
|
+
+ 'Arreglo: agregale "project_slug": "<un-slug-propio>" al yujin.forge.json de uno de los dos.';
|
|
164
|
+
try {
|
|
165
|
+
console.error(aviso);
|
|
166
|
+
}
|
|
167
|
+
catch { /* noop */ }
|
|
168
|
+
/* Que ademas llegue al chat, no solo al stdout que nadie mira. */
|
|
169
|
+
try {
|
|
170
|
+
const { enqueuePanelDispatch } = await import('./panel_queue.js');
|
|
171
|
+
enqueuePanelDispatch('system.chat-log', { text: aviso }, new Date().toISOString());
|
|
172
|
+
}
|
|
173
|
+
catch { /* best-effort */ }
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
catch { /* sin registro no hay con que comparar */ }
|
|
134
177
|
/* Cerebro por instancia (Pablo, 2026-09-30). Habia una sola config para
|
|
135
178
|
* todas: cambiar el cerebro en un `yf chat` lo cambiaba en todos. Este
|
|
136
179
|
* proceso queda atado al cerebro de SU proyecto -- lee el del proyecto si
|
|
@@ -164,7 +207,20 @@ export async function startChatServer(opts) {
|
|
|
164
207
|
* alpha.59v.2 -- registered via startAutoIngest so the
|
|
165
208
|
* /api/forge/graph-status endpoint can await it (avoiding
|
|
166
209
|
* the panel showing partial data during the ingest). */
|
|
210
|
+
/* AISLAMIENTO EN TESTS (Pablo 2026-10-04). Estos tres trabajos del boot son
|
|
211
|
+
* fire-and-forget y escriben en el store por slug (projects/<slug>/
|
|
212
|
+
* semantic_graph.db, pending.json). El path lo resuelven POR LLAMADA desde
|
|
213
|
+
* YF_CONFIG_DIR, pero sobreviven al test que los disparo: cuando corren, el
|
|
214
|
+
* afterEach ya restauro la env var, asi que terminan escribiendo en el
|
|
215
|
+
* config dir REAL del usuario. Evidencia: `projects/demo-scope/`,
|
|
216
|
+
* `projects/mode-console/`, `projects/lifecycle-test/` aparecieron en el
|
|
217
|
+
* ~/.yujin-forge de Pablo con fecha de las corridas de hoy. Y ese directorio
|
|
218
|
+
* es el que alimenta el selector de proyectos del celular, asi que la
|
|
219
|
+
* basura de los tests se ve en pantalla. En test no corren. */
|
|
220
|
+
const enTest = !!process.env.VITEST || process.env.NODE_ENV === 'test';
|
|
167
221
|
try {
|
|
222
|
+
if (enTest)
|
|
223
|
+
throw new Error('test env: sin auto-ingest');
|
|
168
224
|
const { startAutoIngest } = await import('./graph_auto_ingest.js');
|
|
169
225
|
startAutoIngest({
|
|
170
226
|
project_root: opts.projectRoot,
|
|
@@ -181,17 +237,18 @@ export async function startChatServer(opts) {
|
|
|
181
237
|
* background, para que al abrir el panel el grafo este completo.
|
|
182
238
|
* El boton Refresh + el watcher lo regeneran luego ante cambios.
|
|
183
239
|
* Fire-and-forget; nunca rompe el boot. */
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
const { regenerateAndPersist } = await import('../semantic_graph/derive.js');
|
|
187
|
-
const s = await regenerateAndPersist({ project_root: opts.projectRoot, project_slug: projectSlug, enrich: false });
|
|
240
|
+
if (!enTest)
|
|
241
|
+
void (async () => {
|
|
188
242
|
try {
|
|
189
|
-
|
|
243
|
+
const { regenerateAndPersist } = await import('../semantic_graph/derive.js');
|
|
244
|
+
const s = await regenerateAndPersist({ project_root: opts.projectRoot, project_slug: projectSlug, enrich: false });
|
|
245
|
+
try {
|
|
246
|
+
console.error('[graph-regen] boot -> ' + (s.module_edges + s.lib_edges) + ' edges (' + s.pruned + ' pruned)');
|
|
247
|
+
}
|
|
248
|
+
catch { /* */ }
|
|
190
249
|
}
|
|
191
|
-
catch { /* */ }
|
|
192
|
-
}
|
|
193
|
-
catch { /* never break boot */ }
|
|
194
|
-
})();
|
|
250
|
+
catch { /* never break boot */ }
|
|
251
|
+
})();
|
|
195
252
|
/* alpha.59w -- fs.watch over src/components, src/hooks, src/lib,
|
|
196
253
|
* src/core. When the user adds or modifies a TS/JS file, the
|
|
197
254
|
* watcher extracts top-level exports (regex), creates one
|
|
@@ -200,6 +257,8 @@ export async function startChatServer(opts) {
|
|
|
200
257
|
* attached to the returned StartedServer.close(). */
|
|
201
258
|
let componentWatcherHandle = null;
|
|
202
259
|
try {
|
|
260
|
+
if (enTest)
|
|
261
|
+
throw new Error('test env: sin component watcher');
|
|
203
262
|
const { startComponentWatcher } = await import('./graph_component_watcher.js');
|
|
204
263
|
componentWatcherHandle = await startComponentWatcher({
|
|
205
264
|
project_root: opts.projectRoot,
|
|
@@ -366,6 +425,15 @@ export async function startChatServer(opts) {
|
|
|
366
425
|
* ephemeral runs (vitest sets VITEST; also skip an obvious temp root). */
|
|
367
426
|
const isTestEnv = !!process.env.VITEST || process.env.NODE_ENV === 'test';
|
|
368
427
|
const isTempRoot = /[\\/](?:te?mp|yf-[a-z-]*proj|yf-startup)/i.test(opts.projectRoot);
|
|
428
|
+
/* El mismo guard, pero evaluable contra CUALQUIER root -- lo necesita el
|
|
429
|
+
* reanchor, que recibe uno nuevo (Pablo 2026-10-04). El reanchor escribia
|
|
430
|
+
* en el registro sin guard alguno, asi que un test que cambia de proyecto
|
|
431
|
+
* dejaba sus carpetas temporales en el registro REAL del usuario: y el
|
|
432
|
+
* registro alimenta el selector de proyectos del celular, de modo que la
|
|
433
|
+
* basura termina en pantalla. */
|
|
434
|
+
const puedeEscribirRegistro = (root) => !isTestEnv
|
|
435
|
+
&& !/[\\/](?:te?mp|yf-[a-z-]*proj|yf-startup)/i.test(root)
|
|
436
|
+
&& !opts.agentMode;
|
|
369
437
|
/* A headless agent sub-session must NEVER touch the global registry --
|
|
370
438
|
* it would flip the parent's active project (Pablo's scramble bug). */
|
|
371
439
|
if (!isTestEnv && !isTempRoot && !opts.agentMode) {
|
|
@@ -410,11 +478,13 @@ export async function startChatServer(opts) {
|
|
|
410
478
|
}
|
|
411
479
|
catch { /* best-effort */ }
|
|
412
480
|
/* keep the registry in sync after a hot switch (task #33) */
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
481
|
+
if (puedeEscribirRegistro(abs)) {
|
|
482
|
+
try {
|
|
483
|
+
await upsertProject({ slug: session.projectSlug, path: abs });
|
|
484
|
+
await setActive(session.projectSlug);
|
|
485
|
+
}
|
|
486
|
+
catch { /* best-effort */ }
|
|
416
487
|
}
|
|
417
|
-
catch { /* best-effort */ }
|
|
418
488
|
chatLog('Cambiando al proyecto ' + session.projectName + ' (' + abs + ')...');
|
|
419
489
|
/* re-hidratar ingest + reiniciar el watcher sobre el nuevo dir */
|
|
420
490
|
try {
|
|
@@ -459,6 +529,12 @@ export async function startChatServer(opts) {
|
|
|
459
529
|
* terminar dispara el re-tabs. */
|
|
460
530
|
const slugNow = session.projectSlug;
|
|
461
531
|
const nameNow = session.projectName;
|
|
532
|
+
/* Mismo aislamiento que el regen del boot (2026-10-04): este tambien es
|
|
533
|
+
* fire-and-forget y sobrevive al test que lo disparo, asi que terminaba
|
|
534
|
+
* creando projects/<slug>/semantic_graph.db en el config dir REAL. Se vio
|
|
535
|
+
* con los slugs 'pin-to' y 'switch-to', del test de reanchor. */
|
|
536
|
+
if (enTest)
|
|
537
|
+
return { projectRoot: session.projectRoot, projectName: nameNow, projectSlug: slugNow };
|
|
462
538
|
chatLog('Regenerando el grafo semantico de ' + nameNow + '...');
|
|
463
539
|
void (async () => {
|
|
464
540
|
try {
|
|
@@ -1083,7 +1159,16 @@ function publishBoardSnapshot() {
|
|
|
1083
1159
|
snap.projects = (reg.projects || []).slice(0, 30).map((p) => ({
|
|
1084
1160
|
slug: p.slug, path: p.path || '',
|
|
1085
1161
|
}));
|
|
1086
|
-
|
|
1162
|
+
/* El proyecto activo que el celular espeja es el de ESTA instancia,
|
|
1163
|
+
* no el puntero global del registro (Pablo 2026-10-04: "la otra la
|
|
1164
|
+
* sincroniza mal, me sincroniza el yujin-kikan en vez del que
|
|
1165
|
+
* tengo"). Con dos `yf chat` abiertos, active_slug es el del que
|
|
1166
|
+
* arranco o cambio de proyecto ultimo, asi que las DOS instancias
|
|
1167
|
+
* publicaban el mismo activeProject: el celular, espejando una,
|
|
1168
|
+
* mostraba el proyecto de la otra. cx.slug es el de la instancia que
|
|
1169
|
+
* publica este board. El global queda de respaldo para el caso en
|
|
1170
|
+
* que la instancia todavia no tenga slug. */
|
|
1171
|
+
snap.activeProject = cx.slug || reg.active_slug || null;
|
|
1087
1172
|
}
|
|
1088
1173
|
catch { /* leave unset */ }
|
|
1089
1174
|
/* Relay budget (bug 2026-07-10): the worker rejects events over 32KB
|
|
@@ -2244,6 +2329,12 @@ async function route(req, res, ctx) {
|
|
|
2244
2329
|
month: status.month,
|
|
2245
2330
|
renewal_at: status.renewal_at,
|
|
2246
2331
|
extension_url: status.extension_url,
|
|
2332
|
+
/* 2026-10-04 -- `source` (y el plan del worker) no se reenviaban, asi
|
|
2333
|
+
* que el chip de fuente del panel era codigo muerto: la tarjeta no
|
|
2334
|
+
* podia decir si el numero venia del worker o del medidor local.
|
|
2335
|
+
* Justo el dato que hace falta cuando los tokens se ven en cero. */
|
|
2336
|
+
source: status.source,
|
|
2337
|
+
server_plan: status.server_plan,
|
|
2247
2338
|
eco: {
|
|
2248
2339
|
cap: finite(status.eco.cap),
|
|
2249
2340
|
used: status.eco.used,
|
|
@@ -5925,6 +6016,12 @@ async function handleChat(req, res, ctx) {
|
|
|
5925
6016
|
* this block was byte-identical turn-to-turn yet re-sent uncached. */
|
|
5926
6017
|
systemDynamic: projectContextSection + wiringSection + toolingSection + recentSessionsRecap + graphSection + snapshotSection + workflowSection + approvalPostureSection + toolStatusSection(),
|
|
5927
6018
|
maxTokens: _maxTokensPerTurn,
|
|
6019
|
+
/* Tope del turno en Plan, POR TURNO (Pablo 2026-10-04). Se leia solo al
|
|
6020
|
+
* construir el cliente, asi que subir claude_cli_timeout_ms en el panel
|
|
6021
|
+
* no hacia nada hasta reiniciar: el panel decia 10000000 y el corte
|
|
6022
|
+
* seguia avisando 5000000. Sale del mismo `_limits` que ya se lee cada
|
|
6023
|
+
* turno para max_tokens_per_turn. Los clientes HTTP lo ignoran. */
|
|
6024
|
+
timeoutMs: _limits.claude_cli_timeout_ms,
|
|
5928
6025
|
/* Plan/CLI uses Claude Code's native tools (FORGE_TOOL_SPECS = just
|
|
5929
6026
|
* nac3_invoke via the MCP bridge); the HTTP path (BYOK/Console + managed)
|
|
5930
6027
|
* gets direct native file tools + nac3_invoke so the model is fluent with
|