@ironcode-ai/telegram 1.17.2 → 1.17.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 +4 -1
  2. package/package.json +1 -1
  3. package/src/index.ts +150 -22
package/README.md CHANGED
@@ -74,16 +74,19 @@ The agent runs with the current directory as its working directory, so it can re
74
74
  | `/sessions` | List recent sessions — tap to switch |
75
75
  | `/new` | Start a new session |
76
76
  | `/info` | Show current session details (title, ID, file changes) |
77
+ | `/init` | Analyze the project and create an `AGENTS.md` config file |
78
+ | `/diff` | Show all file changes made in the current session |
77
79
  | `/start` | Show help |
78
80
 
79
81
  ## How It Works
80
82
 
81
83
  ```
82
84
  You send a message
85
+ → Bot shows 🤔 Thinking... while the agent starts up
83
86
  → Bot creates/resumes an ironcode session on your machine
84
87
  → Agent reads/writes files, runs bash, calls LLM
85
88
  → Each completed tool call is sent as a separate message
86
- → Text response is streamed live by editing a placeholder
89
+ → Text response is streamed live by editing the placeholder
87
90
  → Final response replaces the placeholder when done
88
91
  ```
89
92
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ironcode-ai/telegram",
3
- "version": "1.17.2",
3
+ "version": "1.17.4",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "bin": {
package/src/index.ts CHANGED
@@ -107,8 +107,24 @@ function sessionLabel(s: Session, isCurrent: boolean) {
107
107
  const bot = new Bot(cfg.token)
108
108
 
109
109
  console.log("🚀 Starting ironcode server...")
110
- const ironcode = await createIroncode({ port: 0 })
111
- console.log("✅ Ironcode server ready at", ironcode.server.url)
110
+ let localServer: Awaited<ReturnType<typeof createIroncode>>
111
+ try {
112
+ localServer = await createIroncode({ port: 0 })
113
+ } catch (err: any) {
114
+ const msg = err?.message ?? String(err)
115
+ if (msg.includes("exited with code 0") || msg.includes("ENOENT") || msg.includes("Illegal instruction")) {
116
+ console.error("❌ Failed to start ironcode server.\n")
117
+ console.error(" Make sure the ironcode CLI is installed and authenticated:")
118
+ console.error(" 1. npm install -g ironcode-ai")
119
+ console.error(" 2. ironcode auth login")
120
+ console.error(" 3. ironcode serve ← test manually first\n")
121
+ } else {
122
+ console.error("❌ Failed to start ironcode server:", msg)
123
+ }
124
+ process.exit(1)
125
+ }
126
+ console.log("✅ Ironcode server ready at", localServer.server.url)
127
+ const client = localServer.client
112
128
 
113
129
  type SessionState = {
114
130
  sessionId: string
@@ -117,6 +133,7 @@ type SessionState = {
117
133
  liveMessageId?: number
118
134
  liveText: string
119
135
  lastEditMs: number
136
+ currentTool?: string
120
137
  }
121
138
 
122
139
  const EDIT_INTERVAL_MS = 1200
@@ -136,7 +153,7 @@ async function editLive(state: SessionState, text: string) {
136
153
 
137
154
  // Event loop
138
155
  ;(async () => {
139
- const events = await ironcode.client.event.subscribe()
156
+ const events = await client.event.subscribe()
140
157
  for await (const event of events.stream) {
141
158
  const getState = (sessionID: string) =>
142
159
  [...sessions.values()].find((s) => s.sessionId === sessionID)
@@ -153,13 +170,26 @@ async function editLive(state: SessionState, text: string) {
153
170
  await editLive(state, state.liveText)
154
171
  state.lastEditMs = now
155
172
  }
156
- } else if (part.type === "tool" && part.state?.status === "completed") {
157
- await bot.api
158
- .sendMessage(state.chatId, `*${part.tool}* — ${part.state.title}`, {
159
- parse_mode: "Markdown",
160
- ...(state.threadId ? { message_thread_id: state.threadId } : {}),
161
- })
162
- .catch(() => {})
173
+ } else if (part.type === "tool") {
174
+ if (part.state?.status === "completed") {
175
+ state.currentTool = undefined
176
+ await bot.api
177
+ .sendMessage(state.chatId, `🔧 *${part.tool}* ${part.state.title}`, {
178
+ parse_mode: "Markdown",
179
+ ...(state.threadId ? { message_thread_id: state.threadId } : {}),
180
+ })
181
+ .catch(() => {})
182
+ } else if (state.currentTool !== part.tool) {
183
+ // Tool just started — show loading indicator if no text yet
184
+ state.currentTool = part.tool
185
+ if (!state.liveText.trim() && state.liveMessageId) {
186
+ const now = Date.now()
187
+ if (now - state.lastEditMs > 500) {
188
+ await editLive(state, `⏳ ${part.tool}...`)
189
+ state.lastEditMs = now
190
+ }
191
+ }
192
+ }
163
193
  }
164
194
  } else if (event.type === "message.updated") {
165
195
  const info = event.properties.info as any
@@ -172,18 +202,31 @@ async function editLive(state: SessionState, text: string) {
172
202
  await editLive(state, `❌ ${msg}`)
173
203
  state.liveMessageId = undefined
174
204
  state.liveText = ""
205
+ state.currentTool = undefined
175
206
  continue
176
207
  }
177
208
 
178
209
  if (info.finish && info.finish !== "tool-calls" && info.finish !== "unknown") {
179
210
  const finalText = state.liveText.trim()
211
+ const savedMessageId = state.liveMessageId
212
+
180
213
  if (finalText) {
181
214
  await editLive(state, finalText)
182
- } else if (state.liveMessageId) {
183
- await bot.api.deleteMessage(state.chatId, state.liveMessageId).catch(() => {})
215
+ } else if (savedMessageId) {
216
+ // No text output (only tools ran) show done in placeholder
217
+ await bot.api.editMessageText(state.chatId, savedMessageId, "✅ Done").catch(() => {})
218
+ }
219
+
220
+ // Add ✅ reaction to signal completion
221
+ if (savedMessageId) {
222
+ await bot.api
223
+ .setMessageReaction(state.chatId, savedMessageId, [{ type: "emoji", emoji: "👍" }])
224
+ .catch(() => {})
184
225
  }
226
+
185
227
  state.liveMessageId = undefined
186
228
  state.liveText = ""
229
+ state.currentTool = undefined
187
230
  }
188
231
  }
189
232
  }
@@ -203,7 +246,9 @@ bot.command("start", async (ctx) => {
203
246
  "Commands:\n" +
204
247
  "/sessions — list sessions\n" +
205
248
  "/new — start a new session\n" +
206
- "/info — current session details",
249
+ "/info — current session details\n" +
250
+ "/init — create AGENTS.md for current project\n" +
251
+ "/diff — show code changes in this session",
207
252
  { parse_mode: "Markdown" },
208
253
  )
209
254
  })
@@ -223,7 +268,7 @@ bot.command("info", async (ctx) => {
223
268
  return
224
269
  }
225
270
 
226
- const res = await ironcode.client.session.get({ path: { id: state.sessionId } })
271
+ const res = await client.session.get({ path: { id: state.sessionId } })
227
272
  if (res.error) {
228
273
  await ctx.reply(`❌ ${JSON.stringify(res.error)}`)
229
274
  return
@@ -244,19 +289,89 @@ bot.command("info", async (ctx) => {
244
289
  )
245
290
  })
246
291
 
292
+ bot.command("init", async (ctx) => {
293
+ const key = getChatKey(ctx.chat.id, ctx.message?.message_thread_id)
294
+ let state = sessions.get(key)
295
+
296
+ if (!state) {
297
+ const res = await client.session.create({
298
+ body: { title: `Telegram ${ctx.chat.type} ${key}` },
299
+ })
300
+ if (res.error) {
301
+ await ctx.reply(`❌ Failed to create session: ${JSON.stringify(res.error)}`)
302
+ return
303
+ }
304
+ state = { sessionId: res.data.id, chatId: ctx.chat.id, threadId: ctx.message?.message_thread_id, liveText: "", lastEditMs: 0 }
305
+ sessions.set(key, state)
306
+ }
307
+
308
+ const placeholder = await ctx.reply("⏳ Analyzing project and creating AGENTS.md...")
309
+
310
+ const model = cfg.model ? parseModel(cfg.model) : undefined
311
+ const res = await client.session.command({
312
+ path: { id: state.sessionId },
313
+ body: { command: "init", arguments: "", ...(model ? { model: `${model.providerID}/${model.modelID}` } : {}) },
314
+ })
315
+
316
+ if (res.error) {
317
+ await bot.api
318
+ .editMessageText(ctx.chat.id, placeholder.message_id, `❌ ${JSON.stringify(res.error)}`)
319
+ .catch(() => {})
320
+ return
321
+ }
322
+
323
+ await bot.api
324
+ .editMessageText(ctx.chat.id, placeholder.message_id, "✅ *AGENTS.md created!*\n\nThe AI agent has analyzed your project and written configuration.", {
325
+ parse_mode: "Markdown",
326
+ })
327
+ .catch(() => {})
328
+ })
329
+
330
+ bot.command("diff", async (ctx) => {
331
+ const key = getChatKey(ctx.chat.id, ctx.message?.message_thread_id)
332
+ const state = sessions.get(key)
333
+
334
+ if (!state) {
335
+ await ctx.reply("No active session. Send a message to create one.")
336
+ return
337
+ }
338
+
339
+ const res = await client.session.diff({ path: { id: state.sessionId } })
340
+ if (res.error) {
341
+ await ctx.reply(`❌ ${JSON.stringify(res.error)}`)
342
+ return
343
+ }
344
+
345
+ const diffs = res.data ?? []
346
+
347
+ if (diffs.length === 0) {
348
+ await ctx.reply("📊 No code changes in this session.")
349
+ return
350
+ }
351
+
352
+ const totalAdd = diffs.reduce((s, d) => s + d.additions, 0)
353
+ const totalDel = diffs.reduce((s, d) => s + d.deletions, 0)
354
+
355
+ const fileLines = diffs.map((d) => `✏️ \`${d.file}\` (+${d.additions}/-${d.deletions})`).join("\n")
356
+
357
+ const msg = `📝 *Code Changes* — ${diffs.length} files · +${totalAdd}/-${totalDel}\n\n${fileLines}`
358
+
359
+ await ctx.reply(msg.slice(0, 4096), { parse_mode: "Markdown" })
360
+ })
361
+
247
362
  bot.command("sessions", async (ctx) => {
248
363
  const key = getChatKey(ctx.chat.id, ctx.message?.message_thread_id)
249
364
  const currentState = sessions.get(key)
250
365
 
251
- const res = await ironcode.client.session.list()
366
+ const res = await client.session.list()
252
367
  if (res.error) {
253
368
  await ctx.reply(`❌ ${JSON.stringify(res.error)}`)
254
369
  return
255
370
  }
256
371
 
257
372
  const list = res.data!
258
- .filter((s) => !(s as any).time?.archived)
259
- .sort((a, b) => b.time.updated - a.time.updated)
373
+ .filter((s: any) => !s.time?.archived)
374
+ .sort((a: any, b: any) => b.time.updated - a.time.updated)
260
375
  .slice(0, 10)
261
376
 
262
377
  if (list.length === 0) {
@@ -278,7 +393,7 @@ bot.callbackQuery(/^switch:(.+)$/, async (ctx) => {
278
393
  const threadId = (ctx.callbackQuery.message as any)?.message_thread_id
279
394
  const key = getChatKey(chatId, threadId)
280
395
 
281
- const res = await ironcode.client.session.get({ path: { id: sessionId } })
396
+ const res = await client.session.get({ path: { id: sessionId } })
282
397
  if (res.error) {
283
398
  await ctx.answerCallbackQuery({ text: "❌ Session not found" })
284
399
  return
@@ -304,7 +419,7 @@ bot.on("message:text", async (ctx) => {
304
419
  let state = sessions.get(key)
305
420
 
306
421
  if (!state) {
307
- const res = await ironcode.client.session.create({
422
+ const res = await client.session.create({
308
423
  body: { title: `Telegram ${ctx.chat.type} ${key}` },
309
424
  })
310
425
  if (res.error) {
@@ -314,26 +429,39 @@ bot.on("message:text", async (ctx) => {
314
429
  state = { sessionId: res.data.id, chatId, threadId, liveText: "", lastEditMs: 0 }
315
430
  sessions.set(key, state)
316
431
 
317
- const share = await ironcode.client.session.share({ path: { id: res.data.id } })
432
+ const share = await client.session.share({ path: { id: res.data.id } })
318
433
  if (!share.error && share.data?.share?.url) {
319
434
  await ctx.reply(`🔗 Session: ${share.data.share.url}`)
320
435
  }
321
436
  }
322
437
 
323
- const placeholder = await ctx.reply("", {
438
+ const placeholder = await ctx.reply("🤔 Thinking\\.\\.\\.", {
439
+ parse_mode: "MarkdownV2",
324
440
  ...(threadId ? { message_thread_id: threadId } : {}),
325
441
  })
326
442
  state.liveMessageId = placeholder.message_id
327
443
  state.liveText = ""
328
444
  state.lastEditMs = 0
445
+ state.currentTool = undefined
446
+
447
+ // Show "typing..." indicator immediately and keep it alive every 4s
448
+ const sendTyping = () =>
449
+ bot.api.sendChatAction(chatId, "typing", threadId ? { message_thread_id: threadId } : {}).catch(() => {})
450
+ sendTyping()
451
+ const typingInterval = setInterval(() => {
452
+ if (!state.liveMessageId) return
453
+ sendTyping()
454
+ }, 4000)
329
455
 
330
456
  const model = cfg.model ? parseModel(cfg.model) : undefined
331
457
 
332
- const result = await ironcode.client.session.promptAsync({
458
+ const result = await client.session.promptAsync({
333
459
  path: { id: state.sessionId },
334
460
  body: { parts: [{ type: "text", text }], model },
335
461
  })
336
462
 
463
+ clearInterval(typingInterval)
464
+
337
465
  if (result.error) {
338
466
  await ctx.api.editMessageText(chatId, placeholder.message_id, `❌ ${JSON.stringify(result.error)}`)
339
467
  state.liveMessageId = undefined