@iaforged/context-code 1.0.57 → 1.0.59

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.
@@ -118,6 +118,55 @@ function truncate(text, max) {
118
118
  return text;
119
119
  return `${text.slice(0, max - 1)}…`;
120
120
  }
121
+ /** Escapa caracteres especiales de HTML para envío seguro a Telegram con parse_mode HTML. */
122
+ export function escapeHTML(text) {
123
+ return text
124
+ .replace(/&/g, '&')
125
+ .replace(/</g, '&lt;')
126
+ .replace(/>/g, '&gt;')
127
+ .replace(/"/g, '&quot;');
128
+ }
129
+ /**
130
+ * Convierte Markdown básico a HTML compatible con Telegram.
131
+ * Soporta: bloques de código, código inline, negritas, itálicas, tachado.
132
+ * El texto fuera de esos patrones se escapa para evitar errores de parseo.
133
+ */
134
+ export function markdownToTelegramHTML(text) {
135
+ const result = [];
136
+ // Procesa bloque a bloque para no escapar lo que ya es HTML
137
+ const codeBlockRe = /```(\w*)\n?([\s\S]*?)```/g;
138
+ let lastIndex = 0;
139
+ let match;
140
+ while ((match = codeBlockRe.exec(text)) !== null) {
141
+ // Texto antes del bloque de código
142
+ if (match.index > lastIndex) {
143
+ result.push(inlineMarkdownToHTML(text.slice(lastIndex, match.index)));
144
+ }
145
+ const code = match[2] ?? '';
146
+ result.push(`<pre><code>${escapeHTML(code.trimEnd())}</code></pre>`);
147
+ lastIndex = match.index + match[0].length;
148
+ }
149
+ // Resto del texto
150
+ if (lastIndex < text.length) {
151
+ result.push(inlineMarkdownToHTML(text.slice(lastIndex)));
152
+ }
153
+ return result.join('');
154
+ }
155
+ function inlineMarkdownToHTML(text) {
156
+ // Escape HTML primero, luego restauramos los marcadores de formato
157
+ let out = escapeHTML(text);
158
+ // Código inline: `...`
159
+ out = out.replace(/`([^`]+)`/g, '<code>$1</code>');
160
+ // Negrita: **texto** o __texto__
161
+ out = out.replace(/\*\*(.+?)\*\*/g, '<b>$1</b>');
162
+ out = out.replace(/__(.+?)__/g, '<b>$1</b>');
163
+ // Itálica: *texto* o _texto_ (no conflicta con negrita ya procesada)
164
+ out = out.replace(/\*(.+?)\*/g, '<i>$1</i>');
165
+ out = out.replace(/_(.+?)_/g, '<i>$1</i>');
166
+ // Tachado: ~~texto~~
167
+ out = out.replace(/~~(.+?)~~/g, '<s>$1</s>');
168
+ return out;
169
+ }
121
170
  /** Divide un texto en trozos ≤ `max` chars, cortando en newline cuando se puede. */
122
171
  export function splitForChannel(text, max) {
123
172
  if (text.length <= max)
@@ -5,6 +5,7 @@ import { getOpenAICompatibleAccessToken } from '../utils/auth.js';
5
5
  import { getConfiguredProviderBaseUrl } from '../utils/model/providerBaseUrls.js';
6
6
  import { getStoredActiveProviderPreference, getStoredLastModelForProvider, setStoredActiveProviderPreference, setStoredLastModelForProvider, } from '../utils/model/providerProfilesDb.js';
7
7
  import { setMirrorBot } from './mirror.js';
8
+ import { markdownToTelegramHTML, escapeHTML } from '../mirrors/shared.js';
8
9
  let activeBotInstance = null;
9
10
  let inboundHandler = null;
10
11
  export function setTelegramInboundHandler(handler) {
@@ -159,14 +160,14 @@ export async function startTelegramBridge() {
159
160
  }
160
161
  const model = getActiveModel();
161
162
  const provider = getActiveProvider();
162
- await ctx.reply(`🚀 *Context Code Bridge Activo*\n\n` +
163
- `📡 Proveedor: *${provider}*\n` +
164
- `🤖 Modelo: *${model}*\n\n` +
163
+ await ctx.reply(`🚀 <b>Context Code Bridge Activo</b>\n\n` +
164
+ `📡 Proveedor: <b>${escapeHTML(provider)}</b>\n` +
165
+ `🤖 Modelo: <b>${escapeHTML(model)}</b>\n\n` +
165
166
  `Escríbeme cualquier pregunta y la enviaré al modelo.\n\n` +
166
- `Comandos disponibles:\n` +
167
+ `<b>Comandos disponibles:</b>\n` +
167
168
  `/status - Ver estado actual\n` +
168
- `/model <nombre> - Cambiar modelo\n` +
169
- `/provider <nombre> - Cambiar proveedor`, { parse_mode: 'Markdown' });
169
+ `/model &lt;nombre&gt; - Cambiar modelo\n` +
170
+ `/provider &lt;nombre&gt; - Cambiar proveedor`, { parse_mode: 'HTML' });
170
171
  });
171
172
  // Comando /status
172
173
  bot.command('status', async (ctx) => {
@@ -176,10 +177,10 @@ export async function startTelegramBridge() {
176
177
  return;
177
178
  const model = getActiveModel();
178
179
  const provider = getActiveProvider();
179
- await ctx.reply(`📊 *Estado de Context Code*\n\n` +
180
- `📡 Proveedor: *${provider}*\n` +
181
- `🤖 Modelo: *${model}*\n` +
182
- `🔗 Bridge: ✅ Conectado`, { parse_mode: 'Markdown' });
180
+ await ctx.reply(`📊 <b>Estado de Context Code</b>\n\n` +
181
+ `📡 Proveedor: <b>${escapeHTML(provider)}</b>\n` +
182
+ `🤖 Modelo: <b>${escapeHTML(model)}</b>\n` +
183
+ `🔗 Bridge: ✅ Conectado`, { parse_mode: 'HTML' });
183
184
  });
184
185
  // Comando /model
185
186
  bot.command('model', async (ctx) => {
@@ -190,15 +191,15 @@ export async function startTelegramBridge() {
190
191
  const newModel = ctx.match?.trim();
191
192
  if (!newModel) {
192
193
  const model = getActiveModel();
193
- await ctx.reply(`🤖 Modelo actual: *${model}*`, {
194
- parse_mode: 'Markdown',
194
+ await ctx.reply(`🤖 Modelo actual: <b>${escapeHTML(model)}</b>`, {
195
+ parse_mode: 'HTML',
195
196
  });
196
197
  return;
197
198
  }
198
199
  const provider = getActiveProvider();
199
200
  setStoredLastModelForProvider(provider, newModel);
200
- await ctx.reply(`✅ Modelo cambiado a *${newModel}*`, {
201
- parse_mode: 'Markdown',
201
+ await ctx.reply(`✅ Modelo cambiado a <b>${escapeHTML(newModel)}</b>`, {
202
+ parse_mode: 'HTML',
202
203
  });
203
204
  });
204
205
  // Comando /provider
@@ -210,8 +211,8 @@ export async function startTelegramBridge() {
210
211
  const newProvider = ctx.match?.trim();
211
212
  if (!newProvider) {
212
213
  const provider = getActiveProvider();
213
- await ctx.reply(`📡 Proveedor actual: *${provider}*`, {
214
- parse_mode: 'Markdown',
214
+ await ctx.reply(`📡 Proveedor actual: <b>${escapeHTML(provider)}</b>`, {
215
+ parse_mode: 'HTML',
215
216
  });
216
217
  return;
217
218
  }
@@ -229,8 +230,8 @@ export async function startTelegramBridge() {
229
230
  return;
230
231
  }
231
232
  setStoredActiveProviderPreference(newProvider.toLowerCase());
232
- await ctx.reply(`✅ Proveedor cambiado a *${newProvider}*`, {
233
- parse_mode: 'Markdown',
233
+ await ctx.reply(`✅ Proveedor cambiado a <b>${escapeHTML(newProvider)}</b>`, {
234
+ parse_mode: 'HTML',
234
235
  });
235
236
  });
236
237
  // Mensajes de texto libre
@@ -266,13 +267,15 @@ export async function startTelegramBridge() {
266
267
  const provider = getActiveProvider();
267
268
  await ctx.replyWithChatAction('typing');
268
269
  const response = await queryModel(ctx.message.text, model, provider);
269
- const chunks = splitMessage(response);
270
+ const htmlResponse = markdownToTelegramHTML(response);
271
+ const chunks = splitMessage(htmlResponse);
270
272
  for (const chunk of chunks) {
271
273
  try {
272
- await ctx.reply(chunk, { parse_mode: 'Markdown' });
274
+ await ctx.reply(chunk, { parse_mode: 'HTML' });
273
275
  }
274
276
  catch {
275
- await ctx.reply(chunk);
277
+ // Fallback: texto plano sin formato
278
+ await ctx.reply(chunk.replace(/<[^>]+>/g, ''));
276
279
  }
277
280
  }
278
281
  });
@@ -1,10 +1,23 @@
1
1
  import { getPrimaryChatId } from './config.js';
2
- import { consumeInjectedMarkIfSameOrigin, markInjected, splitForChannel, } from '../mirrors/shared.js';
2
+ import { consumeInjectedMarkIfSameOrigin, markInjected, markdownToTelegramHTML, escapeHTML, splitForChannel, } from '../mirrors/shared.js';
3
3
  const PREFIX = {
4
4
  user: '👤',
5
5
  assistant: '🤖',
6
6
  tool: '🔧',
7
7
  };
8
+ function formatMirrorMessage(role, text) {
9
+ const icon = PREFIX[role];
10
+ if (role === 'assistant') {
11
+ // Convierte Markdown del modelo a HTML
12
+ return `${icon} ${markdownToTelegramHTML(text)}`;
13
+ }
14
+ if (role === 'tool') {
15
+ // Herramientas en bloque de código inline para distinguirlas
16
+ return `${icon} <code>${escapeHTML(text)}</code>`;
17
+ }
18
+ // Usuario: texto plano escapado
19
+ return `${icon} <b>${escapeHTML(text)}</b>`;
20
+ }
8
21
  const TELEGRAM_MAX = 4000;
9
22
  const SEND_INTERVAL_MS = 150;
10
23
  const MAX_QUEUE = 200;
@@ -49,14 +62,20 @@ async function drain() {
49
62
  const chatId = getPrimaryChatId();
50
63
  if (!chatId)
51
64
  break;
52
- const chunks = splitForChannel(`${PREFIX[item.role]} ${item.text}`, TELEGRAM_MAX);
65
+ const formatted = formatMirrorMessage(item.role, item.text);
66
+ const chunks = splitForChannel(formatted, TELEGRAM_MAX);
53
67
  for (const chunk of chunks) {
54
68
  try {
55
- await botRef.api.sendMessage(chatId, chunk);
69
+ await botRef.api.sendMessage(chatId, chunk, { parse_mode: 'HTML' });
56
70
  }
57
71
  catch {
58
- // Si el send falla (token inválido, user bloqueó, etc.) no reintentamos
59
- // este item — seguimos con la cola para no bloquear mensajes nuevos.
72
+ // Fallback sin formato si el HTML falla (e.g. tag roto por corte de chunk)
73
+ try {
74
+ await botRef.api.sendMessage(chatId, `${PREFIX[item.role]} ${item.text}`.slice(0, TELEGRAM_MAX));
75
+ }
76
+ catch {
77
+ // Si el send falla (token inválido, user bloqueó, etc.) no reintentamos
78
+ }
60
79
  break;
61
80
  }
62
81
  await sleep(SEND_INTERVAL_MS);
@@ -17,6 +17,9 @@ function getDatabasePath() {
17
17
  function getKeyPath() {
18
18
  return join(getClaudeConfigHomeDir(), KEY_FILENAME);
19
19
  }
20
+ function getLegacyCredentialsPath() {
21
+ return join(getClaudeConfigHomeDir(), '.credentials.json');
22
+ }
20
23
  let _db = null;
21
24
  let _legacyMigrationAttempted = false;
22
25
  let _cachedEncryptionKey = null;
@@ -81,6 +84,20 @@ function savePayload(db, plainText) {
81
84
  ON CONFLICT(key) DO UPDATE SET value = excluded.value
82
85
  `).run('main_storage', encryptPayload(plainText));
83
86
  }
87
+ function getStoredPayload() {
88
+ const db = getDb();
89
+ if (!db)
90
+ return null;
91
+ try {
92
+ const row = db
93
+ .prepare('SELECT value FROM secure_storage WHERE key = ?')
94
+ .get('main_storage');
95
+ return typeof row?.value === 'string' ? row.value : null;
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ }
84
101
  function getDb() {
85
102
  if (_db)
86
103
  return _db;
@@ -177,3 +194,24 @@ export const sqliteStorage = {
177
194
  }
178
195
  }
179
196
  };
197
+ export function getSecureStorageDbPath() {
198
+ return getDatabasePath();
199
+ }
200
+ export function getSecureStorageKeyPath() {
201
+ return getKeyPath();
202
+ }
203
+ export function getLegacyCredentialsFilePath() {
204
+ return getLegacyCredentialsPath();
205
+ }
206
+ export function hasLegacyCredentialsFile() {
207
+ try {
208
+ return getFsImplementation().existsSync(getLegacyCredentialsPath());
209
+ }
210
+ catch {
211
+ return false;
212
+ }
213
+ }
214
+ export function isSecureStorageEncrypted() {
215
+ const payload = getStoredPayload();
216
+ return payload ? payload.startsWith(ENCRYPTED_PREFIX) : false;
217
+ }
@@ -20,6 +20,7 @@ import { checkInstall } from './nativeInstaller/index.js';
20
20
  import { getProxyUrl } from './proxy.js';
21
21
  import { SandboxManager } from './sandbox/sandbox-adapter.js';
22
22
  import { getSecureStorage } from './secureStorage/index.js';
23
+ import { getLegacyCredentialsFilePath, getSecureStorageDbPath, getSecureStorageKeyPath, hasLegacyCredentialsFile, isSecureStorageEncrypted, } from './secureStorage/sqliteStorage.js';
23
24
  import { getSettingsWithAllErrors } from './settings/allErrors.js';
24
25
  import { getEnabledSettingSources, getSettingSourceDisplayNameCapitalized } from './settings/constants.js';
25
26
  import { getManagedFileSettingsPresence, getPolicySettingsOrigin, getSettingsForSource } from './settings/settings.js';
@@ -313,8 +314,30 @@ export function buildAPIProviderProperties() {
313
314
  });
314
315
  properties.push({
315
316
  label: 'Credenciales providers',
316
- value: getSecureStorage().name === 'sqlite' ? 'SQLite' : getSecureStorage().name,
317
+ value: getSecureStorage().name === 'sqlite'
318
+ ? 'SQLite (cifrado local)'
319
+ : getSecureStorage().name,
317
320
  });
321
+ if (getSecureStorage().name === 'sqlite') {
322
+ properties.push({
323
+ label: 'Credenciales DB',
324
+ value: getSecureStorageDbPath(),
325
+ });
326
+ properties.push({
327
+ label: 'Clave cifrado',
328
+ value: getSecureStorageKeyPath(),
329
+ });
330
+ properties.push({
331
+ label: 'Estado cifrado credenciales',
332
+ value: isSecureStorageEncrypted() ? 'Cifrado activo' : 'Sin payload o pendiente',
333
+ });
334
+ properties.push({
335
+ label: 'Legacy credentials file',
336
+ value: hasLegacyCredentialsFile()
337
+ ? `Presente (${getLegacyCredentialsFilePath()})`
338
+ : 'No presente',
339
+ });
340
+ }
318
341
  if (apiProvider !== 'firstParty') {
319
342
  const providerLabels = {
320
343
  bedrock: 'AWS Bedrock',
@@ -180,6 +180,15 @@ export async function startLoginWithQr(options) {
180
180
  }
181
181
  }
182
182
  closeSocketSafe(sock);
183
+ // Force-save credentials before closing so they are on disk even if Baileys
184
+ // hasn't emitted a creds.update yet. This is the only explicit save point that
185
+ // guarantees the JID is available to startWhatsAppBridge on the next launch.
186
+ try {
187
+ await saveCreds();
188
+ }
189
+ catch {
190
+ // non-fatal: continue and let readSelfIdentity report what was persisted
191
+ }
183
192
  const identity = await readSelfIdentity(authDir);
184
193
  if (identity.jid) {
185
194
  setSelfIdentity(identity.jid, identity.e164);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iaforged/context-code",
3
- "version": "1.0.57",
3
+ "version": "1.0.59",
4
4
  "bin": {
5
5
  "context": "context-bootstrap.js",
6
6
  "context-recovery": "context-bootstrap.js"