0agent 1.0.43 → 1.0.45

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/bin/chat.js +157 -93
  2. package/dist/daemon.mjs +277 -7
  3. package/package.json +1 -1
package/bin/chat.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * /model to switch. /key to add provider keys. Never forgets previous keys.
8
8
  */
9
9
 
10
- import { createInterface, emitKeypressEvents, moveCursor, clearLine } from 'node:readline';
10
+ import { createInterface, emitKeypressEvents } from 'node:readline';
11
11
  import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
12
12
  import { resolve } from 'node:path';
13
13
  import { homedir } from 'node:os';
@@ -31,6 +31,7 @@ const SLASH_COMMANDS = [
31
31
  { cmd: '/security-audit',desc: 'Security audit — find vulnerabilities' },
32
32
  { cmd: '/design-review', desc: 'Design review — architecture and patterns' },
33
33
  // Built-ins
34
+ { cmd: '/telegram', desc: 'Connect Telegram bot — forward messages to 0agent'},
34
35
  { cmd: '/model', desc: 'Show or switch the LLM model' },
35
36
  { cmd: '/key', desc: 'Update a stored API key' },
36
37
  { cmd: '/status', desc: 'Daemon health, graph stats, active sessions' },
@@ -754,6 +755,33 @@ async function handleCommand(input) {
754
755
  break;
755
756
  }
756
757
 
758
+ // /telegram — configure Telegram bot token
759
+ case '/telegram': {
760
+ if (!cfg) { console.log(fmt(C.red, ' No config found. Run: 0agent init')); break; }
761
+ const existingToken = cfg?.telegram?.token;
762
+ if (existingToken) {
763
+ console.log(`\n Telegram bot: ${fmt(C.green, '✓ configured')}`);
764
+ console.log(` Token: ${existingToken.slice(0, 10)}••••`);
765
+ console.log(` ${fmt(C.dim, 'To update: /telegram <new-token>\n')}`);
766
+ }
767
+ const token = parts[1];
768
+ if (!token) {
769
+ if (!existingToken) {
770
+ console.log('\n Connect your Telegram bot to 0agent:\n');
771
+ console.log(` 1. Create a bot: ${fmt(C.cyan, 'https://t.me/BotFather')} → /newbot`);
772
+ console.log(` 2. Copy the token and run: ${fmt(C.cyan, '/telegram <token>')}`);
773
+ console.log(` 3. Restart daemon: ${fmt(C.dim, '0agent stop && 0agent start')}\n`);
774
+ }
775
+ break;
776
+ }
777
+ if (!cfg.telegram) cfg.telegram = {};
778
+ cfg.telegram.token = token;
779
+ saveConfig(cfg);
780
+ console.log(` ${fmt(C.green, '✓')} Telegram token saved`);
781
+ console.log(` ${fmt(C.dim, 'Restart daemon for changes to take effect: 0agent stop && 0agent start\n')}`);
782
+ break;
783
+ }
784
+
757
785
  case '/skills': {
758
786
  try {
759
787
  const skills = await fetch(`${BASE_URL}/api/skills`).then(r => r.json());
@@ -808,61 +836,114 @@ async function handleCommand(input) {
808
836
  }
809
837
  }
810
838
 
811
- // ─── Live slash-command menu ──────────────────────────────────────────────────
812
- // Drawn below the prompt as the user types. Uses moveCursor to avoid cursor
813
- // save/restore conflicts with readline.
814
- let _menuLines = 0; // how many lines the current menu occupies below the cursor
839
+ // ─── Command palette ──────────────────────────────────────────────────────────
840
+ // Fully takes over stdin when open. No cursor tricks pauses readline,
841
+ // reads raw bytes directly, draws with \x1b[NA\x1b[0J (up + clear-to-end).
842
+ // Returns the selected command string, or null if cancelled.
815
843
 
816
- function _drawMenu(filter) {
817
- if (pendingResolve) { _clearMenu(); return; } // don't show while session running
844
+ let _paletteOpen = false;
818
845
 
819
- const items = filter === null ? [] :
820
- SLASH_COMMANDS.filter(c =>
821
- !filter || c.cmd.slice(1).toLowerCase().startsWith(filter.toLowerCase())
822
- ).slice(0, 10);
846
+ async function openPalette(initialFilter = '') {
847
+ if (_paletteOpen || pendingResolve) return null;
848
+ _paletteOpen = true;
823
849
 
824
- // If nothing changed (same count), skip redraw to avoid flicker
825
- if (items.length === 0) { _clearMenu(); return; }
850
+ // Pause readline so it stops consuming stdin events
851
+ rl.pause();
852
+ // Resume the raw stream so our data handler receives bytes
853
+ process.stdin.resume();
854
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
826
855
 
827
- const needed = items.length + 1; // +1 for blank line
856
+ let filter = initialFilter.toLowerCase();
857
+ let idx = 0;
858
+ let drawn = 0; // number of lines we've printed so far
828
859
 
829
- // Move down past existing menu lines (or 0), then clear downward
830
- const existingLines = _menuLines;
831
- if (existingLines > 0) {
832
- moveCursor(process.stdout, 0, existingLines);
833
- for (let i = 0; i < existingLines; i++) {
834
- clearLine(process.stdout, 0);
835
- if (i < existingLines - 1) moveCursor(process.stdout, 0, -1);
836
- }
837
- moveCursor(process.stdout, 0, -(existingLines - 1));
838
- }
860
+ const getItems = () => SLASH_COMMANDS.filter(c =>
861
+ !filter || c.cmd.slice(1).startsWith(filter)
862
+ );
839
863
 
840
- // Print blank separator + menu items, tracking column 0
841
- process.stdout.write('\n');
842
- for (const m of items) {
843
- process.stdout.write(
844
- ` ${fmt(C.cyan, m.cmd.padEnd(20))} ${fmt(C.dim, m.desc)}\x1b[K\n`
845
- );
846
- }
864
+ const paint = () => {
865
+ // Erase previous draw: move up `drawn` lines, clear everything below
866
+ if (drawn > 0) process.stdout.write(`\x1b[${drawn}A\x1b[0J`);
847
867
 
848
- // Move back up to the prompt line and restore cursor after the typed text
849
- moveCursor(process.stdout, 0, -(needed));
850
- // Jump to end of current line (readline already put cursor there)
851
- moveCursor(process.stdout, 999, 0);
868
+ const items = getItems();
869
+ const show = items.slice(0, 14);
870
+ if (idx >= items.length) idx = Math.max(0, items.length - 1);
852
871
 
853
- _menuLines = needed;
854
- }
872
+ const lines = [];
855
873
 
856
- function _clearMenu() {
857
- if (_menuLines === 0) return;
858
- const n = _menuLines;
859
- _menuLines = 0;
860
- moveCursor(process.stdout, 0, n);
861
- for (let i = 0; i < n; i++) {
862
- clearLine(process.stdout, 0);
863
- moveCursor(process.stdout, 0, -1);
864
- }
865
- moveCursor(process.stdout, 0, 1); // back to prompt line
874
+ // Top border
875
+ lines.push(` \x1b[2m${'─'.repeat(58)}\x1b[0m`);
876
+
877
+ if (show.length === 0) {
878
+ lines.push(` \x1b[2m no commands match "/${filter}"\x1b[0m`);
879
+ } else {
880
+ for (let i = 0; i < show.length; i++) {
881
+ const m = show[i];
882
+ const sel = i === idx;
883
+ if (sel) {
884
+ lines.push(
885
+ ` \x1b[36;1m›\x1b[0m \x1b[36;1m${m.cmd.padEnd(22)}\x1b[0m \x1b[0m${m.desc}\x1b[0m`
886
+ );
887
+ } else {
888
+ lines.push(
889
+ ` \x1b[36m${m.cmd.padEnd(22)}\x1b[0m \x1b[2m${m.desc}\x1b[0m`
890
+ );
891
+ }
892
+ }
893
+ }
894
+
895
+ if (items.length > 14) lines.push(` \x1b[2m …${items.length - 14} more\x1b[0m`);
896
+
897
+ // Bottom: search input line
898
+ lines.push(` \x1b[2m${'─'.repeat(58)}\x1b[0m`);
899
+ lines.push(` ${fmt(C.cyan, '/')}${filter}\x1b[K \x1b[2m↑↓ navigate · Enter select · Esc cancel\x1b[0m`);
900
+
901
+ const out = lines.join('\n') + '\n';
902
+ process.stdout.write(out);
903
+ drawn = lines.length;
904
+ };
905
+
906
+ paint();
907
+
908
+ return new Promise((resolve) => {
909
+ const onData = (chunk) => {
910
+ const s = chunk.toString();
911
+
912
+ if (s === '\r' || s === '\n') { // Enter — select
913
+ const sel = getItems()[idx]?.cmd ?? null;
914
+ finish(sel);
915
+ } else if (s === '\x1b' || s === '\x03') { // Esc / Ctrl-C — cancel
916
+ finish(null);
917
+ } else if (s === '\x1b[A') { // Up arrow
918
+ idx = Math.max(0, idx - 1);
919
+ paint();
920
+ } else if (s === '\x1b[B') { // Down arrow
921
+ idx = Math.min(getItems().length - 1, idx + 1);
922
+ paint();
923
+ } else if (s === '\x7f' || s === '\x08') { // Backspace
924
+ filter = filter.slice(0, -1);
925
+ idx = 0;
926
+ paint();
927
+ } else if (/^[a-z0-9\-_]$/i.test(s)) { // Printable letter/digit/hyphen
928
+ filter += s.toLowerCase();
929
+ idx = 0;
930
+ paint();
931
+ }
932
+ };
933
+
934
+ const finish = (selected) => {
935
+ process.stdin.removeListener('data', onData);
936
+ // Erase the palette
937
+ if (drawn > 0) process.stdout.write(`\x1b[${drawn}A\x1b[0J`);
938
+ drawn = 0;
939
+ _paletteOpen = false;
940
+ // Hand control back to readline
941
+ rl.resume();
942
+ resolve(selected);
943
+ };
944
+
945
+ process.stdin.on('data', onData);
946
+ });
866
947
  }
867
948
 
868
949
  // ─── Main REPL ────────────────────────────────────────────────────────────────
@@ -874,50 +955,31 @@ const rl = createInterface({
874
955
  completer: (line, callback) => {
875
956
  if (!line.startsWith('/')) return callback(null, [[], line]);
876
957
 
877
- const filter = line.slice(1).toLowerCase();
878
- const matches = SLASH_COMMANDS.filter(c =>
879
- !filter || c.cmd.slice(1).startsWith(filter)
880
- );
881
-
882
- if (matches.length === 0) return callback(null, [[], line]);
958
+ const filter = line.slice(1).toLowerCase();
959
+ const matches = SLASH_COMMANDS.filter(c => !filter || c.cmd.slice(1).startsWith(filter));
883
960
 
884
- // Single match — let readline silently auto-complete
885
- if (matches.length === 1) {
886
- _clearMenu();
887
- return callback(null, [[matches[0].cmd + ' '], line]);
888
- }
961
+ // Single exact match — let readline silently auto-complete
962
+ if (matches.length === 1) return callback(null, [[matches[0].cmd + ' '], line]);
889
963
 
890
- // Multiple matches print formatted menu, suppress readline's plain list
891
- _clearMenu();
892
- process.stdout.write('\n\n');
893
- for (const m of matches.slice(0, 12)) {
894
- process.stdout.write(` ${fmt(C.cyan, m.cmd.padEnd(22))} ${fmt(C.dim, m.desc)}\n`);
895
- }
896
- if (matches.length > 12) {
897
- process.stdout.write(` ${fmt(C.dim, `…and ${matches.length - 12} more`)}\n`);
898
- }
899
- process.stdout.write('\n');
900
- setImmediate(() => rl.prompt(true));
964
+ // Multiple (or bare /) open interactive palette
965
+ // Schedule after this tick so readline finishes its Tab handling first
966
+ setImmediate(async () => {
967
+ // Clear the typed fragment from the prompt line before opening palette
968
+ process.stdout.write('\r\x1b[2K');
969
+ const selected = await openPalette(filter);
970
+ if (selected) {
971
+ await executeInput(selected);
972
+ } else {
973
+ rl.prompt();
974
+ }
975
+ });
901
976
  return callback(null, [[], line]);
902
977
  },
903
978
  });
904
979
 
905
- // Live menu on keypress draws below the prompt as user types
980
+ // Trigger palette when user types exactly '/' and presses Tab or Enter isn't needed —
981
+ // the completer above handles Tab. For bare '/' + Enter, handled in rl.on('line') below.
906
982
  emitKeypressEvents(process.stdin, rl);
907
- process.stdin.on('keypress', (_char, key) => {
908
- if (key?.name === 'return' || key?.name === 'enter') {
909
- _clearMenu(); // clear before readline processes the line
910
- return;
911
- }
912
- setImmediate(() => {
913
- const line = rl.line ?? '';
914
- if (line.startsWith('/') && !pendingResolve) {
915
- _drawMenu(line.slice(1));
916
- } else {
917
- _clearMenu();
918
- }
919
- });
920
- });
921
983
 
922
984
  printHeader();
923
985
  printInsights();
@@ -1225,19 +1287,21 @@ async function drainQueue() {
1225
1287
  }
1226
1288
 
1227
1289
  rl.on('line', async (input) => {
1228
- _clearMenu(); // always clear menu when a line is submitted
1229
1290
  const line = input.trim();
1230
1291
  if (!line) { rl.prompt(); return; }
1231
1292
 
1232
- // Bare `/` → show full command palette
1233
- if (line === '/') {
1234
- console.log('');
1235
- for (const m of SLASH_COMMANDS) {
1236
- console.log(` ${fmt(C.cyan, m.cmd.padEnd(22))} ${fmt(C.dim, m.desc)}`);
1293
+ // Bare `/` or partial `/cmd` with no session running open interactive palette
1294
+ if (line.startsWith('/') && !pendingResolve) {
1295
+ const filter = line === '/' ? '' : line.slice(1);
1296
+ const exact = SLASH_COMMANDS.find(c => c.cmd === line);
1297
+ if (!exact) {
1298
+ // Clear the echoed input line before opening palette
1299
+ process.stdout.write('\x1b[1A\r\x1b[2K');
1300
+ const selected = await openPalette(filter);
1301
+ if (selected) await executeInput(selected);
1302
+ else rl.prompt();
1303
+ return;
1237
1304
  }
1238
- console.log('');
1239
- rl.prompt();
1240
- return;
1241
1305
  }
1242
1306
 
1243
1307
  // If a session is already running, queue the message.
package/dist/daemon.mjs CHANGED
@@ -3027,13 +3027,19 @@ content = element.text if element else page.get_all_text()` : `content = page.ge
3027
3027
  `- Be concise in your final response: state what was done and where to find it`,
3028
3028
  ...hasMemory ? [
3029
3029
  ``,
3030
- `Memory (IMPORTANT):`,
3031
- `- ALWAYS call memory_write after discovering important facts:`,
3032
- ` \xB7 Live URLs (ngrok, deployed apps, APIs): memory_write({label:"ngrok_url", content:"https://...", type:"url"})`,
3030
+ `Memory (CRITICAL \u2014 write EVERYTHING you learn):`,
3031
+ `- Call memory_write for ANY fact you discover \u2014 conversational OR from tools:`,
3032
+ ` \xB7 User's name/identity: memory_write({label:"user_name", content:"Sahil", type:"identity"})`,
3033
+ ` \xB7 Projects they mention: memory_write({label:"project_telegram_bot", content:"user has a Telegram bot", type:"project"})`,
3034
+ ` \xB7 Tech stack / tools: memory_write({label:"tech_stack", content:"Node.js, Telegram", type:"tech"})`,
3035
+ ` \xB7 Preferences and decisions they express`,
3036
+ ` \xB7 Live URLs (ngrok, deployed apps): memory_write({label:"ngrok_url", content:"https://...", type:"url"})`,
3033
3037
  ` \xB7 Server ports: memory_write({label:"dev_server_port", content:"3000", type:"config"})`,
3034
3038
  ` \xB7 File paths of created projects: memory_write({label:"project_path", content:"/path/to/project", type:"path"})`,
3035
- ` \xB7 Task outcomes: memory_write({label:"last_task_result", content:"...", type:"outcome"})`,
3036
- `- Call memory_write immediately when you find the value, not just at the end`
3039
+ ` \xB7 Task outcomes: memory_write({label:"last_outcome", content:"...", type:"outcome"})`,
3040
+ `- Write to memory FIRST when the user tells you something about themselves or their work`,
3041
+ `- If the user says "my name is X" \u2192 memory_write immediately, before anything else`,
3042
+ `- If they say "we have a Y" or "our Y" \u2192 memory_write it as a project fact`
3037
3043
  ] : []
3038
3044
  ];
3039
3045
  if (isSelfMod && this.agentRoot) {
@@ -4516,6 +4522,7 @@ var SessionManager = class {
4516
4522
  weightUpdater;
4517
4523
  anthropicFetcher = new AnthropicSkillFetcher();
4518
4524
  agentRoot;
4525
+ onMemoryWritten;
4519
4526
  constructor(deps = {}) {
4520
4527
  this.inferenceEngine = deps.inferenceEngine;
4521
4528
  this.eventBus = deps.eventBus;
@@ -4525,6 +4532,7 @@ var SessionManager = class {
4525
4532
  this.identity = deps.identity;
4526
4533
  this.projectContext = deps.projectContext;
4527
4534
  this.agentRoot = deps.agentRoot;
4535
+ this.onMemoryWritten = deps.onMemoryWritten;
4528
4536
  if (deps.adapter) {
4529
4537
  this.conversationStore = new ConversationStore(deps.adapter);
4530
4538
  this.conversationStore.init();
@@ -4816,6 +4824,8 @@ Current task:`;
4816
4824
  this.addStep(sessionId, `Commands run: ${agentResult.commands_run.length}`);
4817
4825
  }
4818
4826
  this.addStep(sessionId, `Done (${agentResult.tokens_used} tokens, ${agentResult.iterations} LLM turns)`);
4827
+ this._extractAndPersistFacts(enrichedReq.task, agentResult.output, activeLLM).catch(() => {
4828
+ });
4819
4829
  this.completeSession(sessionId, {
4820
4830
  output: agentResult.output,
4821
4831
  files_written: agentResult.files_written,
@@ -4884,6 +4894,68 @@ Current task:`;
4884
4894
  }
4885
4895
  return this.llm;
4886
4896
  }
4897
+ /**
4898
+ * After every session, run a lightweight LLM pass to extract factual entities
4899
+ * (name, projects, tech, preferences, URLs) and persist them to the graph.
4900
+ * This catches everything the agent didn't explicitly memory_write during execution.
4901
+ */
4902
+ async _extractAndPersistFacts(task, output, llm) {
4903
+ if (!this.graph || !llm.isConfigured) return;
4904
+ const prompt = `Extract factual entities from this conversation that should be remembered long-term.
4905
+ Return ONLY a JSON array, no other text, max 12 items.
4906
+
4907
+ Types: identity (name/role), project (apps/products), tech (stack/tools), preference, url, path, config, outcome
4908
+
4909
+ Format: [{"label":"snake_case_key","content":"value to remember","type":"type"}]
4910
+
4911
+ Examples:
4912
+ - User says "my name is Sahil" \u2192 {"label":"user_name","content":"Sahil","type":"identity"}
4913
+ - User says "we have a telegram bot" \u2192 {"label":"project_telegram_bot","content":"user has a Telegram bot project","type":"project"}
4914
+ - User says "I use React and Next.js" \u2192 {"label":"tech_stack","content":"React, Next.js","type":"tech"}
4915
+
4916
+ Conversation:
4917
+ User: ${task.slice(0, 600)}
4918
+ Agent: ${output.slice(0, 400)}`;
4919
+ try {
4920
+ const resp = await llm.complete(
4921
+ [{ role: "user", content: prompt }],
4922
+ "You are a concise memory extraction system. Extract only factual, durable information. Skip generic statements."
4923
+ );
4924
+ const jsonMatch = resp.content.match(/\[[\s\S]*?\]/);
4925
+ if (!jsonMatch) return;
4926
+ const entities = JSON.parse(jsonMatch[0]);
4927
+ if (!Array.isArray(entities) || entities.length === 0) return;
4928
+ let wrote = 0;
4929
+ for (const e of entities.slice(0, 12)) {
4930
+ if (!e.label?.trim() || !e.content?.trim()) continue;
4931
+ const nodeId = `memory:${e.label.toLowerCase().replace(/[^a-z0-9_]/g, "_")}`;
4932
+ try {
4933
+ const existing = this.graph.getNode(nodeId);
4934
+ if (existing) {
4935
+ this.graph.updateNode(nodeId, {
4936
+ label: e.label,
4937
+ metadata: { ...existing.metadata, content: e.content, type: e.type ?? "note", updated_at: (/* @__PURE__ */ new Date()).toISOString() }
4938
+ });
4939
+ } else {
4940
+ this.graph.addNode(createNode({
4941
+ id: nodeId,
4942
+ graph_id: "root",
4943
+ label: e.label,
4944
+ type: "context" /* CONTEXT */,
4945
+ metadata: { content: e.content, type: e.type ?? "note", saved_at: (/* @__PURE__ */ new Date()).toISOString() }
4946
+ }));
4947
+ }
4948
+ wrote++;
4949
+ } catch {
4950
+ }
4951
+ }
4952
+ if (wrote > 0) {
4953
+ console.log(`[0agent] Memory: extracted ${wrote} facts from session`);
4954
+ this.onMemoryWritten?.();
4955
+ }
4956
+ } catch {
4957
+ }
4958
+ }
4887
4959
  /**
4888
4960
  * Convert a task result into a weight signal for the knowledge graph.
4889
4961
  *
@@ -6930,6 +7002,192 @@ var CodespaceManager = class {
6930
7002
 
6931
7003
  // packages/daemon/src/ZeroAgentDaemon.ts
6932
7004
  init_RuntimeSelfHeal();
7005
+
7006
+ // packages/daemon/src/TelegramBridge.ts
7007
+ var TelegramBridge = class {
7008
+ constructor(config, sessions, eventBus) {
7009
+ this.config = config;
7010
+ this.sessions = sessions;
7011
+ this.eventBus = eventBus;
7012
+ this.token = config.token;
7013
+ this.allowedUsers = new Set(config.allowed_users ?? []);
7014
+ }
7015
+ token;
7016
+ allowedUsers;
7017
+ offset = 0;
7018
+ pollTimer = null;
7019
+ running = false;
7020
+ // session_id per chat for streaming
7021
+ pendingSessions = /* @__PURE__ */ new Map();
7022
+ start() {
7023
+ if (this.running) return;
7024
+ this.running = true;
7025
+ console.log("[0agent] Telegram: bot polling started");
7026
+ this._poll();
7027
+ this.eventBus.onEvent((event) => {
7028
+ const chatId = this._getChatIdForSession(String(event.session_id ?? ""));
7029
+ if (!chatId) return;
7030
+ if (event.type === "session.completed") {
7031
+ const result = event.result;
7032
+ const output = String(result?.output ?? "").trim();
7033
+ if (output && output !== "(no output)") {
7034
+ this._send(chatId, output).catch(() => {
7035
+ });
7036
+ }
7037
+ this.pendingSessions.delete(chatId);
7038
+ } else if (event.type === "session.failed") {
7039
+ const err = String(event.error ?? "Task failed");
7040
+ this._send(chatId, `\u26A0\uFE0F ${err}`).catch(() => {
7041
+ });
7042
+ this.pendingSessions.delete(chatId);
7043
+ }
7044
+ });
7045
+ }
7046
+ stop() {
7047
+ this.running = false;
7048
+ if (this.pollTimer) {
7049
+ clearTimeout(this.pollTimer);
7050
+ this.pollTimer = null;
7051
+ }
7052
+ }
7053
+ _getChatIdForSession(sessionId) {
7054
+ for (const [chatId, sid] of this.pendingSessions) {
7055
+ if (sid === sessionId) return chatId;
7056
+ }
7057
+ return null;
7058
+ }
7059
+ async _poll() {
7060
+ if (!this.running) return;
7061
+ try {
7062
+ const updates = await this._getUpdates();
7063
+ for (const u of updates) {
7064
+ await this._handleUpdate(u);
7065
+ }
7066
+ } catch {
7067
+ }
7068
+ if (this.running) {
7069
+ this.pollTimer = setTimeout(() => this._poll(), 1e3);
7070
+ }
7071
+ }
7072
+ async _getUpdates() {
7073
+ const res = await fetch(
7074
+ `https://api.telegram.org/bot${this.token}/getUpdates?offset=${this.offset}&timeout=10&limit=20`,
7075
+ { signal: AbortSignal.timeout(15e3) }
7076
+ );
7077
+ if (!res.ok) return [];
7078
+ const data = await res.json();
7079
+ if (!data.ok || !data.result.length) return [];
7080
+ this.offset = data.result[data.result.length - 1].update_id + 1;
7081
+ return data.result;
7082
+ }
7083
+ async _handleUpdate(u) {
7084
+ const msg = u.message;
7085
+ if (!msg?.text || !msg.from) return;
7086
+ const chatId = msg.chat.id;
7087
+ const userId = msg.from.id;
7088
+ const text = msg.text.trim();
7089
+ const userName = msg.from.first_name ?? msg.from.username ?? "User";
7090
+ if (this.allowedUsers.size > 0 && !this.allowedUsers.has(userId)) {
7091
+ await this._send(chatId, "\u26D4 You are not authorised to use this agent.");
7092
+ return;
7093
+ }
7094
+ if (text === "/start" || text === "/help") {
7095
+ await this._send(
7096
+ chatId,
7097
+ `\u{1F44B} Hi ${userName}! I'm 0agent \u2014 your AI that runs on your machine.
7098
+
7099
+ Send me any task and I'll get it done:
7100
+ \u2022 "make a website for my coffee shop"
7101
+ \u2022 "research my competitor's pricing"
7102
+ \u2022 "fix the bug in auth.ts"
7103
+
7104
+ I remember everything across sessions.`
7105
+ );
7106
+ return;
7107
+ }
7108
+ if (text === "/status") {
7109
+ try {
7110
+ const r = await fetch("http://localhost:4200/api/health", { signal: AbortSignal.timeout(2e3) });
7111
+ const h = await r.json();
7112
+ await this._send(
7113
+ chatId,
7114
+ `\u2705 Daemon running
7115
+ Graph: ${h.graph_nodes} nodes \xB7 ${h.graph_edges} edges
7116
+ Sessions: ${h.active_sessions} active`
7117
+ );
7118
+ } catch {
7119
+ await this._send(chatId, "\u26A0\uFE0F Daemon not reachable");
7120
+ }
7121
+ return;
7122
+ }
7123
+ await this._sendAction(chatId, "typing");
7124
+ await this._send(chatId, `\u23F3 Working on it\u2026`);
7125
+ try {
7126
+ const res = await fetch("http://localhost:4200/api/sessions", {
7127
+ method: "POST",
7128
+ headers: { "Content-Type": "application/json" },
7129
+ body: JSON.stringify({
7130
+ task: text,
7131
+ context: { system_context: `User's name: ${userName}. Message from Telegram.` }
7132
+ }),
7133
+ signal: AbortSignal.timeout(5e3)
7134
+ });
7135
+ const session = await res.json();
7136
+ const sessionId = session.session_id ?? session.id;
7137
+ if (sessionId) {
7138
+ this.pendingSessions.set(chatId, sessionId);
7139
+ } else {
7140
+ await this._send(chatId, "\u26A0\uFE0F Could not start session");
7141
+ }
7142
+ } catch (e) {
7143
+ await this._send(chatId, `\u26A0\uFE0F Error: ${e instanceof Error ? e.message : String(e)}`);
7144
+ }
7145
+ }
7146
+ async _send(chatId, text) {
7147
+ const chunks = this._splitMessage(text);
7148
+ for (const chunk of chunks) {
7149
+ await fetch(`https://api.telegram.org/bot${this.token}/sendMessage`, {
7150
+ method: "POST",
7151
+ headers: { "Content-Type": "application/json" },
7152
+ body: JSON.stringify({ chat_id: chatId, text: chunk, parse_mode: "Markdown" }),
7153
+ signal: AbortSignal.timeout(1e4)
7154
+ }).catch(() => {
7155
+ return fetch(`https://api.telegram.org/bot${this.token}/sendMessage`, {
7156
+ method: "POST",
7157
+ headers: { "Content-Type": "application/json" },
7158
+ body: JSON.stringify({ chat_id: chatId, text: chunk }),
7159
+ signal: AbortSignal.timeout(1e4)
7160
+ }).catch(() => {
7161
+ });
7162
+ });
7163
+ }
7164
+ }
7165
+ async _sendAction(chatId, action) {
7166
+ await fetch(`https://api.telegram.org/bot${this.token}/sendChatAction`, {
7167
+ method: "POST",
7168
+ headers: { "Content-Type": "application/json" },
7169
+ body: JSON.stringify({ chat_id: chatId, action }),
7170
+ signal: AbortSignal.timeout(5e3)
7171
+ }).catch(() => {
7172
+ });
7173
+ }
7174
+ _splitMessage(text) {
7175
+ if (text.length <= 4e3) return [text];
7176
+ const chunks = [];
7177
+ let i = 0;
7178
+ while (i < text.length) {
7179
+ chunks.push(text.slice(i, i + 4e3));
7180
+ i += 4e3;
7181
+ }
7182
+ return chunks;
7183
+ }
7184
+ static isConfigured(config) {
7185
+ const c = config;
7186
+ return !!(c?.token && typeof c.token === "string" && c.token.length > 10);
7187
+ }
7188
+ };
7189
+
7190
+ // packages/daemon/src/ZeroAgentDaemon.ts
6933
7191
  import { fileURLToPath as fileURLToPath3 } from "node:url";
6934
7192
  import { dirname as dirname6 } from "node:path";
6935
7193
  var ZeroAgentDaemon = class {
@@ -6949,6 +7207,7 @@ var ZeroAgentDaemon = class {
6949
7207
  codespaceManager = null;
6950
7208
  schedulerManager = null;
6951
7209
  runtimeHealer = null;
7210
+ telegramBridge = null;
6952
7211
  startedAt = 0;
6953
7212
  pidFilePath;
6954
7213
  constructor() {
@@ -7042,8 +7301,12 @@ var ZeroAgentDaemon = class {
7042
7301
  identity: identity ?? void 0,
7043
7302
  projectContext: projectContext ?? void 0,
7044
7303
  adapter: this.adapter,
7045
- agentRoot
7304
+ agentRoot,
7046
7305
  // agent source path — self-improvement tasks read the right files
7306
+ // Mark GitHub memory dirty immediately when facts are extracted — pushes within 2min
7307
+ onMemoryWritten: () => {
7308
+ this.githubMemorySync?.markDirty();
7309
+ }
7047
7310
  });
7048
7311
  const teamSync = identity && teams.length > 0 ? new TeamSync(teamManager, this.adapter, identity.entity_node_id) : null;
7049
7312
  if (this.githubMemorySync) {
@@ -7055,7 +7318,7 @@ var ZeroAgentDaemon = class {
7055
7318
  console.log(`[0agent] Memory auto-synced: ${result.nodes_synced} nodes`);
7056
7319
  }
7057
7320
  }
7058
- }, 30 * 60 * 1e3);
7321
+ }, 2 * 60 * 1e3);
7059
7322
  if (typeof this.memorySyncTimer === "object") this.memorySyncTimer.unref?.();
7060
7323
  }
7061
7324
  let proactiveSurface = null;
@@ -7085,6 +7348,11 @@ var ZeroAgentDaemon = class {
7085
7348
  }
7086
7349
  this.schedulerManager = new SchedulerManager(this.adapter, this.sessionManager, this.eventBus);
7087
7350
  this.schedulerManager.start();
7351
+ const tgCfg = this.config["telegram"];
7352
+ if (TelegramBridge.isConfigured(tgCfg) && this.sessionManager && this.eventBus) {
7353
+ this.telegramBridge = new TelegramBridge(tgCfg, this.sessionManager, this.eventBus);
7354
+ this.telegramBridge.start();
7355
+ }
7088
7356
  this.backgroundWorkers = new BackgroundWorkers({
7089
7357
  graph: this.graph,
7090
7358
  traceStore: this.traceStore,
@@ -7156,6 +7424,8 @@ var ZeroAgentDaemon = class {
7156
7424
  this.memorySyncTimer = null;
7157
7425
  }
7158
7426
  this.githubMemorySync = null;
7427
+ this.telegramBridge?.stop();
7428
+ this.telegramBridge = null;
7159
7429
  this.schedulerManager?.stop();
7160
7430
  this.schedulerManager = null;
7161
7431
  this.codespaceManager?.closeTunnel();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "0agent",
3
- "version": "1.0.43",
3
+ "version": "1.0.45",
4
4
  "description": "A persistent, learning AI agent that runs on your machine. An agent that learns.",
5
5
  "private": false,
6
6
  "license": "Apache-2.0",