@nac3/forge-cli 1.16.0 → 1.17.0

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.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Reatar el panel a la sesion viva.
3
+ *
4
+ * POR QUE EXISTE (Pablo, 13-oct-2026): se le cerro por error la ventana de
5
+ * Chrome del panel con un turno de E7.5 corriendo. El turno NO se perdio -- el
6
+ * servidor es un proceso aparte y siguio hasta el final -- pero al reabrir el
7
+ * panel estaba VACIO, y la pestana Coding tampoco volvio.
8
+ *
9
+ * Las dos perdidas tienen la misma causa: el panel guardaba en el navegador
10
+ * solo `yf-lang` y `yf_coding_mode`, el transcript renderizado era estado en
11
+ * memoria de ESA ventana, y no habia ningun endpoint que lo reconstruyera
12
+ * (medido: ninguno de los 25 que el panel consume sirve historial). Y el
13
+ * lienzo Coding se abre cuando LLEGAN eventos de actividad, asi que una
14
+ * ventana nueva, que no esta enganchada al stream SSE del turno en vuelo, no
15
+ * tiene de donde enterarse de que hay algo corriendo.
16
+ *
17
+ * O sea que no era "el historial no persiste": era "la ventana nueva no se
18
+ * reata a la sesion". Un solo endpoint arregla las dos cosas, porque las dos
19
+ * necesitan el mismo dato: que dijo la conversacion, y si hay un turno en
20
+ * vuelo ahora.
21
+ *
22
+ * Lo que este modulo NO hace: inventar durabilidad. El transcript ya se
23
+ * persiste en `.yujin-forge/cache/chat/` desde la primera version (SPEC 5) y
24
+ * el turno en vuelo ya es un estado consultable (`in_flight_turn.ts`). Lo que
25
+ * faltaba era servirlos.
26
+ *
27
+ * ASCII-only.
28
+ */
29
+ import type { ChatMessage } from './claude.js';
30
+ import type { InFlightTurn } from '../relay/in_flight_turn.js';
31
+ /**
32
+ * Cuantos mensajes se devuelven, desde el final.
33
+ *
34
+ * Por que un tope y no la sesion entera: una sesion de un dia entero son
35
+ * megabytes, y el panel ya recorta lo que PINTA. Devolver todo haria del
36
+ * arranque del panel la peticion mas cara del producto para mostrar lo mismo.
37
+ * 60 es el mismo numero que usa el espejo del telefono, por consistencia.
38
+ */
39
+ export declare const TOPE_MENSAJES = 60;
40
+ /**
41
+ * Tope de bytes del payload. El tope de mensajes no alcanza: un solo turno con
42
+ * un volcado de consola pegado adentro puede ser de megabytes (hoy mismo hubo
43
+ * uno de 11.870 caracteres). Se recorta desde el final -- lo mas nuevo es lo
44
+ * que el usuario quiere ver.
45
+ */
46
+ export declare const TOPE_BYTES: number;
47
+ export interface MensajeRestaurado {
48
+ role: 'user' | 'assistant';
49
+ content: string;
50
+ }
51
+ export interface TurnoEnVuelo {
52
+ id: string;
53
+ user: string;
54
+ started_at: string;
55
+ /** Segundos que lleva corriendo, para que el panel pueda decir "3h en curso"
56
+ * sin tener que calcularlo contra un reloj que puede no coincidir. */
57
+ elapsed_s: number;
58
+ }
59
+ export interface PayloadRestore {
60
+ ok: true;
61
+ /** Los mensajes, del mas viejo al mas nuevo, ya recortados. */
62
+ messages: MensajeRestaurado[];
63
+ /** True cuando se dejaron mensajes afuera por alguno de los dos topes. */
64
+ truncated: boolean;
65
+ /** Cuantos mensajes tiene la sesion en total. */
66
+ total: number;
67
+ /** El turno en vuelo, o null. Es lo que hace que el panel reabierto muestre
68
+ * la burbuja pendiente Y vuelva a abrir el lienzo Coding. */
69
+ in_flight: TurnoEnVuelo | null;
70
+ project_root: string;
71
+ session_started_at: string | null;
72
+ }
73
+ /**
74
+ * Arma el payload. Pura: toda la entrada es argumento, y por eso se puede
75
+ * probar el recorte sin levantar un servidor.
76
+ *
77
+ * `redactar` entra como funcion y no se importa adentro para que el test pueda
78
+ * afirmar QUE SE LLAMO. El transcript en disco ya se escribe redactado, pero
79
+ * los mensajes en MEMORIA no pasaron por ahi todavia, y este endpoint los
80
+ * sirve: redactar aca no es ceremonia, es el unico lugar donde pasa.
81
+ */
82
+ export declare function construirRestore(args: {
83
+ messages: readonly ChatMessage[];
84
+ inFlight: InFlightTurn | null;
85
+ projectRoot: string;
86
+ sessionStartedAt?: string | null;
87
+ now?: number;
88
+ redactar?: (s: string) => string;
89
+ }): PayloadRestore;
90
+ //# sourceMappingURL=session_restore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session_restore.d.ts","sourceRoot":"","sources":["../../src/chat/session_restore.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC/C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAE/D;;;;;;;GAOG;AACH,eAAO,MAAM,aAAa,KAAK,CAAC;AAEhC;;;;;GAKG;AACH,eAAO,MAAM,UAAU,QAAa,CAAC;AAErC,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,GAAG,WAAW,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,EAAE,MAAM,CAAC;IACnB;2EACuE;IACvE,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,cAAc;IAC7B,EAAE,EAAE,IAAI,CAAC;IACT,+DAA+D;IAC/D,QAAQ,EAAE,iBAAiB,EAAE,CAAC;IAC9B,0EAA0E;IAC1E,SAAS,EAAE,OAAO,CAAC;IACnB,iDAAiD;IACjD,KAAK,EAAE,MAAM,CAAC;IACd;kEAC8D;IAC9D,SAAS,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,YAAY,EAAE,MAAM,CAAC;IACrB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;CACnC;AAOD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE;IACrC,QAAQ,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACjC,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,CAAC,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC,GAAG,cAAc,CA8CjB"}
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Cuantos mensajes se devuelven, desde el final.
3
+ *
4
+ * Por que un tope y no la sesion entera: una sesion de un dia entero son
5
+ * megabytes, y el panel ya recorta lo que PINTA. Devolver todo haria del
6
+ * arranque del panel la peticion mas cara del producto para mostrar lo mismo.
7
+ * 60 es el mismo numero que usa el espejo del telefono, por consistencia.
8
+ */
9
+ export const TOPE_MENSAJES = 60;
10
+ /**
11
+ * Tope de bytes del payload. El tope de mensajes no alcanza: un solo turno con
12
+ * un volcado de consola pegado adentro puede ser de megabytes (hoy mismo hubo
13
+ * uno de 11.870 caracteres). Se recorta desde el final -- lo mas nuevo es lo
14
+ * que el usuario quiere ver.
15
+ */
16
+ export const TOPE_BYTES = 512 * 1024;
17
+ /** Cuantos bytes ocupa en el payload (UTF-8, mas el envoltorio JSON). */
18
+ function pesa(m) {
19
+ return Buffer.byteLength(m.content, 'utf8') + Buffer.byteLength(m.role, 'utf8') + 24;
20
+ }
21
+ /**
22
+ * Arma el payload. Pura: toda la entrada es argumento, y por eso se puede
23
+ * probar el recorte sin levantar un servidor.
24
+ *
25
+ * `redactar` entra como funcion y no se importa adentro para que el test pueda
26
+ * afirmar QUE SE LLAMO. El transcript en disco ya se escribe redactado, pero
27
+ * los mensajes en MEMORIA no pasaron por ahi todavia, y este endpoint los
28
+ * sirve: redactar aca no es ceremonia, es el unico lugar donde pasa.
29
+ */
30
+ export function construirRestore(args) {
31
+ const { messages, inFlight, projectRoot } = args;
32
+ const now = args.now ?? Date.now();
33
+ const redactar = args.redactar ?? ((s) => s);
34
+ /* Solo lo que el panel pinta como conversacion. Un mensaje sin contenido no
35
+ * aporta una burbuja: aporta una vacia. */
36
+ const utiles = messages.filter((m) => (m.role === 'user' || m.role === 'assistant') && typeof m.content === 'string' && m.content !== '');
37
+ const porTope = utiles.slice(-TOPE_MENSAJES);
38
+ /* El recorte por bytes va desde el final hacia atras. */
39
+ const elegidos = [];
40
+ let bytes = 0;
41
+ for (let i = porTope.length - 1; i >= 0; i--) {
42
+ const m = porTope[i];
43
+ const item = { role: m.role, content: redactar(m.content) };
44
+ const b = pesa(item);
45
+ if (bytes + b > TOPE_BYTES && elegidos.length > 0)
46
+ break;
47
+ elegidos.unshift(item);
48
+ bytes += b;
49
+ }
50
+ let enVuelo = null;
51
+ if (inFlight && inFlight.id) {
52
+ const desde = Date.parse(inFlight.started_at);
53
+ const elapsed = Number.isFinite(desde) ? Math.max(0, Math.round((now - desde) / 1000)) : 0;
54
+ enVuelo = {
55
+ id: inFlight.id,
56
+ user: redactar(inFlight.user),
57
+ started_at: inFlight.started_at,
58
+ elapsed_s: elapsed,
59
+ };
60
+ }
61
+ return {
62
+ ok: true,
63
+ messages: elegidos,
64
+ truncated: elegidos.length < utiles.length,
65
+ total: utiles.length,
66
+ in_flight: enVuelo,
67
+ project_root: projectRoot,
68
+ session_started_at: args.sessionStartedAt ?? null,
69
+ };
70
+ }
71
+ //# sourceMappingURL=session_restore.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"session_restore.js","sourceRoot":"","sources":["../../src/chat/session_restore.ts"],"names":[],"mappings":"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"}
@@ -33,23 +33,56 @@ export interface InFlightTurn {
33
33
  started_at: string;
34
34
  }
35
35
  /**
36
- * Tope de edad. Un turno mas viejo que esto se considera muerto y deja de
37
- * anunciarse, aunque nadie lo haya limpiado.
36
+ * Cada cuanto el turno da senal de vida. Es el periodo del latido de actividad
37
+ * del servidor (`startRelayHeartbeat`), que corre SOLO entre el arranque y el
38
+ * cierre del turno.
38
39
  *
39
- * No es decoracion: `clearInFlightTurn` corre al final del handler, y un
40
- * proceso que muere a mitad de turno (o una excepcion por un camino que no
41
- * pasa por ahi) dejaria al telefono con una burbuja "pensando" para siempre.
42
- * Mentir sobre lo que el sistema esta haciendo es peor que no decir nada.
40
+ * Vive aca y no en server.ts a proposito: es la mitad de un par acoplado, y la
41
+ * otra mitad es `SILENCIO_MAX_MS`. El bug anterior nacio de tener dos numeros
42
+ * acoplados en dos archivos distintos sin nada que los ligara.
43
43
  */
44
- export declare const IN_FLIGHT_MAX_AGE_MS: number;
45
- export declare function setInFlightTurn(t: InFlightTurn): void;
44
+ export declare const PULSO_DE_PROGRESO_MS: number;
45
+ /**
46
+ * Tope de SILENCIO. Un turno que no da senal de vida por mas que esto se
47
+ * considera muerto y deja de anunciarse, aunque nadie lo haya limpiado.
48
+ *
49
+ * ANTES ERA UN TOPE DE EDAD, y eso estaba mal (Pablo, 13-oct-2026: "hay turnos
50
+ * de mas de 3 horas en un desarrollo"). La proteccion que se queria es contra
51
+ * un turno COLGADO -- un proceso que muere a mitad de camino, o una excepcion
52
+ * por un camino que no pasa por `clearInFlightTurn` -- porque mentir sobre lo
53
+ * que el sistema esta haciendo es peor que no decir nada. Pero "colgado" no es
54
+ * "viejo": es "sin progreso". Un tope de edad confunde las dos cosas y castiga
55
+ * justo al turno largo, que es el que mas necesita anunciarse.
56
+ *
57
+ * Medido: un turno de 29 minutos en ADIP-Kikan dejaba al telefono diciendo que
58
+ * no habia nada corriendo durante sus ultimos 19 minutos. Con turnos de 3 horas
59
+ * el anuncio serviria para el 5% del turno.
60
+ *
61
+ * Diez pulsos de margen: tolera que se pierdan nueve latidos seguidos antes de
62
+ * dar por muerto un turno que esta vivo.
63
+ */
64
+ export declare const SILENCIO_MAX_MS: number;
65
+ export declare function setInFlightTurn(t: InFlightTurn, now?: number): void;
66
+ /**
67
+ * Senal de vida del turno en curso. La llama el latido de actividad del
68
+ * servidor, que corre solo mientras hay turno.
69
+ *
70
+ * No crea nada: si no hay turno registrado, no hace nada. Asi un latido
71
+ * rezagado no puede resucitar un turno ya cerrado.
72
+ */
73
+ export declare function touchInFlightTurn(now?: number): void;
46
74
  /**
47
75
  * Limpia el turno en vuelo. Con `id`, SOLO si coincide: dos turnos encadenados
48
76
  * pueden cerrar fuera de orden, y un cierre tardio del viejo no tiene que
49
77
  * borrar el anuncio del que recien arranco.
50
78
  */
51
79
  export declare function clearInFlightTurn(id?: string): void;
52
- /** El turno en vuelo, o null si no hay o si ya es demasiado viejo. */
80
+ /**
81
+ * El turno en vuelo, o null si no hay o si se quedo CALLADO.
82
+ *
83
+ * La edad del turno no entra en la decision: un turno de tres horas que sigue
84
+ * latiendo es un turno vivo y se anuncia. Lo que lo saca es el silencio.
85
+ */
53
86
  export declare function inFlightTurn(now?: number): InFlightTurn | null;
54
87
  /** Solo para tests. */
55
88
  export declare function _resetInFlightTurn(): void;
@@ -1 +1 @@
1
- {"version":3,"file":"in_flight_turn.d.ts","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,QAAiB,CAAC;AAInD,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,GAAG,IAAI,CAGrD;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAInD;AAED,sEAAsE;AACtE,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAmB,GAAG,YAAY,GAAG,IAAI,CAQ1E;AAED,uBAAuB;AACvB,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
1
+ {"version":3,"file":"in_flight_turn.d.ts","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AAEH,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,wBAAwB;IACxB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,oBAAoB,QAAY,CAAC;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,eAAe,QAA4B,CAAC;AASzD,wBAAgB,eAAe,CAAC,CAAC,EAAE,YAAY,EAAE,GAAG,GAAE,MAAmB,GAAG,IAAI,CAQ/E;AAED;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,GAAE,MAAmB,GAAG,IAAI,CAGhE;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI,CAInD;AAED;;;;;GAKG;AACH,wBAAgB,YAAY,CAAC,GAAG,GAAE,MAAmB,GAAG,YAAY,GAAG,IAAI,CAU1E;AAED,uBAAuB;AACvB,wBAAgB,kBAAkB,IAAI,IAAI,CAEzC"}
@@ -27,20 +27,57 @@
27
27
  * ASCII puro.
28
28
  */
29
29
  /**
30
- * Tope de edad. Un turno mas viejo que esto se considera muerto y deja de
31
- * anunciarse, aunque nadie lo haya limpiado.
30
+ * Cada cuanto el turno da senal de vida. Es el periodo del latido de actividad
31
+ * del servidor (`startRelayHeartbeat`), que corre SOLO entre el arranque y el
32
+ * cierre del turno.
32
33
  *
33
- * No es decoracion: `clearInFlightTurn` corre al final del handler, y un
34
- * proceso que muere a mitad de turno (o una excepcion por un camino que no
35
- * pasa por ahi) dejaria al telefono con una burbuja "pensando" para siempre.
36
- * Mentir sobre lo que el sistema esta haciendo es peor que no decir nada.
34
+ * Vive aca y no en server.ts a proposito: es la mitad de un par acoplado, y la
35
+ * otra mitad es `SILENCIO_MAX_MS`. El bug anterior nacio de tener dos numeros
36
+ * acoplados en dos archivos distintos sin nada que los ligara.
37
37
  */
38
- export const IN_FLIGHT_MAX_AGE_MS = 10 * 60 * 1000;
38
+ export const PULSO_DE_PROGRESO_MS = 60 * 1000;
39
+ /**
40
+ * Tope de SILENCIO. Un turno que no da senal de vida por mas que esto se
41
+ * considera muerto y deja de anunciarse, aunque nadie lo haya limpiado.
42
+ *
43
+ * ANTES ERA UN TOPE DE EDAD, y eso estaba mal (Pablo, 13-oct-2026: "hay turnos
44
+ * de mas de 3 horas en un desarrollo"). La proteccion que se queria es contra
45
+ * un turno COLGADO -- un proceso que muere a mitad de camino, o una excepcion
46
+ * por un camino que no pasa por `clearInFlightTurn` -- porque mentir sobre lo
47
+ * que el sistema esta haciendo es peor que no decir nada. Pero "colgado" no es
48
+ * "viejo": es "sin progreso". Un tope de edad confunde las dos cosas y castiga
49
+ * justo al turno largo, que es el que mas necesita anunciarse.
50
+ *
51
+ * Medido: un turno de 29 minutos en ADIP-Kikan dejaba al telefono diciendo que
52
+ * no habia nada corriendo durante sus ultimos 19 minutos. Con turnos de 3 horas
53
+ * el anuncio serviria para el 5% del turno.
54
+ *
55
+ * Diez pulsos de margen: tolera que se pierdan nueve latidos seguidos antes de
56
+ * dar por muerto un turno que esta vivo.
57
+ */
58
+ export const SILENCIO_MAX_MS = PULSO_DE_PROGRESO_MS * 10;
39
59
  let current = null;
40
- export function setInFlightTurn(t) {
60
+ export function setInFlightTurn(t, now = Date.now()) {
41
61
  if (!t || !t.id)
42
62
  return;
43
- current = { id: String(t.id), user: String(t.user || ''), started_at: String(t.started_at || new Date().toISOString()) };
63
+ current = {
64
+ id: String(t.id),
65
+ user: String(t.user || ''),
66
+ started_at: String(t.started_at || new Date(now).toISOString()),
67
+ visto: now,
68
+ };
69
+ }
70
+ /**
71
+ * Senal de vida del turno en curso. La llama el latido de actividad del
72
+ * servidor, que corre solo mientras hay turno.
73
+ *
74
+ * No crea nada: si no hay turno registrado, no hace nada. Asi un latido
75
+ * rezagado no puede resucitar un turno ya cerrado.
76
+ */
77
+ export function touchInFlightTurn(now = Date.now()) {
78
+ if (!current)
79
+ return;
80
+ current.visto = now;
44
81
  }
45
82
  /**
46
83
  * Limpia el turno en vuelo. Con `id`, SOLO si coincide: dos turnos encadenados
@@ -54,16 +91,23 @@ export function clearInFlightTurn(id) {
54
91
  return;
55
92
  current = null;
56
93
  }
57
- /** El turno en vuelo, o null si no hay o si ya es demasiado viejo. */
94
+ /**
95
+ * El turno en vuelo, o null si no hay o si se quedo CALLADO.
96
+ *
97
+ * La edad del turno no entra en la decision: un turno de tres horas que sigue
98
+ * latiendo es un turno vivo y se anuncia. Lo que lo saca es el silencio.
99
+ */
58
100
  export function inFlightTurn(now = Date.now()) {
59
101
  if (!current)
60
102
  return null;
61
- const started = Date.parse(current.started_at);
62
- if (Number.isFinite(started) && now - started > IN_FLIGHT_MAX_AGE_MS) {
103
+ /* Un reloj que salta hacia atras da silencio negativo: no vence. Preferible
104
+ * anunciar un turno de mas que descartar uno vivo por un salto de NTP. */
105
+ if (now - current.visto > SILENCIO_MAX_MS) {
63
106
  current = null;
64
107
  return null;
65
108
  }
66
- return current;
109
+ const { id, user, started_at } = current;
110
+ return { id, user, started_at };
67
111
  }
68
112
  /** Solo para tests. */
69
113
  export function _resetInFlightTurn() {
@@ -1 +1 @@
1
- {"version":3,"file":"in_flight_turn.js","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC;AAEnD,IAAI,OAAO,GAAwB,IAAI,CAAC;AAExC,MAAM,UAAU,eAAe,CAAC,CAAe;IAC7C,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO;IACxB,OAAO,GAAG,EAAE,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,EAAE,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,EAAE,CAAC;AAC3H,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAW;IAC3C,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,IAAI,EAAE,IAAI,OAAO,CAAC,EAAE,KAAK,EAAE;QAAE,OAAO;IACpC,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,YAAY,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACnD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/C,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,IAAI,GAAG,GAAG,OAAO,GAAG,oBAAoB,EAAE,CAAC;QACrE,OAAO,GAAG,IAAI,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,OAAO,CAAC;AACjB,CAAC;AAED,uBAAuB;AACvB,MAAM,UAAU,kBAAkB;IAChC,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC"}
1
+ {"version":3,"file":"in_flight_turn.js","sourceRoot":"","sources":["../../src/relay/in_flight_turn.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AASH;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,CAAC;AAE9C;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,oBAAoB,GAAG,EAAE,CAAC;AAOzD,IAAI,OAAO,GAAoB,IAAI,CAAC;AAEpC,MAAM,UAAU,eAAe,CAAC,CAAe,EAAE,MAAc,IAAI,CAAC,GAAG,EAAE;IACvE,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE;QAAE,OAAO;IACxB,OAAO,GAAG;QACR,EAAE,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC;QAC1B,UAAU,EAAE,MAAM,CAAC,CAAC,CAAC,UAAU,IAAI,IAAI,IAAI,CAAC,GAAG,CAAC,CAAC,WAAW,EAAE,CAAC;QAC/D,KAAK,EAAE,GAAG;KACX,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,iBAAiB,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACxD,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,OAAO,CAAC,KAAK,GAAG,GAAG,CAAC;AACtB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,EAAW;IAC3C,IAAI,CAAC,OAAO;QAAE,OAAO;IACrB,IAAI,EAAE,IAAI,OAAO,CAAC,EAAE,KAAK,EAAE;QAAE,OAAO;IACpC,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,YAAY,CAAC,MAAc,IAAI,CAAC,GAAG,EAAE;IACnD,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAC1B;8EAC0E;IAC1E,IAAI,GAAG,GAAG,OAAO,CAAC,KAAK,GAAG,eAAe,EAAE,CAAC;QAC1C,OAAO,GAAG,IAAI,CAAC;QACf,OAAO,IAAI,CAAC;IACd,CAAC;IACD,MAAM,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,GAAG,OAAO,CAAC;IACzC,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;AAClC,CAAC;AAED,uBAAuB;AACvB,MAAM,UAAU,kBAAkB;IAChC,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC"}
package/dist/version.d.ts CHANGED
@@ -2,5 +2,5 @@
2
2
  * Yujin Forge CLI version. Kept in sync with package.json by the npm
3
3
  * "version" lifecycle (scripts/sync_version.mjs) so the two never drift.
4
4
  */
5
- export declare const VERSION = "1.16.0";
5
+ export declare const VERSION = "1.17.0";
6
6
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -2,5 +2,5 @@
2
2
  * Yujin Forge CLI version. Kept in sync with package.json by the npm
3
3
  * "version" lifecycle (scripts/sync_version.mjs) so the two never drift.
4
4
  */
5
- export const VERSION = '1.16.0';
5
+ export const VERSION = '1.17.0';
6
6
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nac3/forge-cli",
3
- "version": "1.16.0",
3
+ "version": "1.17.0",
4
4
  "description": "Yujin Forge -- voice-first NAC-3 React development framework. CLI + chat panel + spec ingest + 10-format document reader + voice loop.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Pablo Kuschnirof <pablo@rpaforce.com>",