agentgram 2.0.0 → 2.0.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.
package/dist/index.js CHANGED
@@ -6,13 +6,8 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
7
  var __getProtoOf = Object.getPrototypeOf;
8
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
9
- var __esm = (fn, res, err) => function __init() {
10
- if (err) throw err[0];
11
- try {
12
- return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
13
- } catch (e) {
14
- throw err = [e], e;
15
- }
9
+ var __esm = (fn, res) => function __init() {
10
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
16
11
  };
17
12
  var __export = (target, all) => {
18
13
  for (var name in all)
@@ -394,12 +389,11 @@ function getGlobalConfigDir() {
394
389
  return AGENTGRAM_CONFIG_DIR;
395
390
  }
396
391
  function ensureConfigDirs() {
397
- const dir = getGlobalConfigDir();
398
- const stateDir = path2.join(dir, "state");
399
392
  try {
400
393
  if (!fs2.existsSync(AGENTGRAM_CONFIG_DIR)) fs2.mkdirSync(AGENTGRAM_CONFIG_DIR, { recursive: true, mode: 448 });
401
- if (!fs2.existsSync(path2.join(AGENTGRAM_CONFIG_DIR, "state"))) {
402
- fs2.mkdirSync(path2.join(AGENTGRAM_CONFIG_DIR, "state"), { recursive: true, mode: 448 });
394
+ const stateDir = path2.join(AGENTGRAM_CONFIG_DIR, "state");
395
+ if (!fs2.existsSync(stateDir)) {
396
+ fs2.mkdirSync(stateDir, { recursive: true, mode: 448 });
403
397
  }
404
398
  const legacyConfig = path2.join(LEGACY_CONFIG_DIR, "config.json");
405
399
  const agentgramConfig = path2.join(AGENTGRAM_CONFIG_DIR, "config.json");
@@ -477,7 +471,7 @@ var init_config_manager = __esm({
477
471
  engine: "opencode",
478
472
  denyPatterns: [],
479
473
  allowedPaths: [],
480
- workspaces: [resolvedTarget],
474
+ workspaces: resolvedTarget.startsWith(os.tmpdir()) ? [] : [resolvedTarget],
481
475
  extra: {}
482
476
  };
483
477
  this.load();
@@ -527,13 +521,17 @@ var init_config_manager = __esm({
527
521
  if (Array.isArray(data.deny_patterns)) this.config.denyPatterns = data.deny_patterns.map(String);
528
522
  if (Array.isArray(data.allowed_paths)) this.config.allowedPaths = data.allowed_paths.map(String);
529
523
  if (Array.isArray(data.workspaces)) {
524
+ this.config.workspaces = [];
530
525
  for (const w of data.workspaces) {
531
526
  const resolved = path2.resolve(String(w));
532
- if (!this.config.workspaces.includes(resolved)) {
527
+ if (fs2.existsSync(resolved) && !resolved.startsWith(os.tmpdir()) && !this.config.workspaces.includes(resolved)) {
533
528
  this.config.workspaces.push(resolved);
534
529
  }
535
530
  }
536
531
  }
532
+ if (fs2.existsSync(this.config.targetDir) && !this.config.targetDir.startsWith(os.tmpdir()) && !this.config.workspaces.includes(this.config.targetDir)) {
533
+ this.config.workspaces.unshift(this.config.targetDir);
534
+ }
537
535
  }
538
536
  saveGlobal() {
539
537
  ensureConfigDirs();
@@ -586,6 +584,16 @@ var init_config_manager = __esm({
586
584
  }
587
585
  return true;
588
586
  }
587
+ removeWorkspace(workspace) {
588
+ const resolved = path2.resolve(workspace);
589
+ const idx = this.config.workspaces.indexOf(resolved);
590
+ if (idx !== -1) {
591
+ this.config.workspaces.splice(idx, 1);
592
+ this.saveGlobal();
593
+ return true;
594
+ }
595
+ return false;
596
+ }
589
597
  validate() {
590
598
  const errors = [];
591
599
  const isTelegram = !this.config.channel || this.config.channel === "telegram";
@@ -605,7 +613,7 @@ var init_config_manager = __esm({
605
613
  });
606
614
 
607
615
  // src/telegram/telegram-client.ts
608
- function escapeHtml2(text) {
616
+ function escapeHtml(text) {
609
617
  if (!text) return "";
610
618
  return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
611
619
  }
@@ -774,8 +782,8 @@ function toTelegramMarkup(keyboard) {
774
782
  inline_keyboard: keyboard.rows.map(
775
783
  (row) => row.map((btn) => ({
776
784
  text: btn.text,
777
- callback_data: btn.callbackData,
778
- url: btn.url
785
+ callback_data: btn.callbackData ?? void 0,
786
+ url: btn.url ?? void 0
779
787
  }))
780
788
  )
781
789
  };
@@ -788,8 +796,8 @@ function fromTelegramMarkup(markup) {
788
796
  rows: markup.inline_keyboard.map(
789
797
  (row) => row.map((btn) => ({
790
798
  text: btn.text,
791
- callbackData: btn.callback_data,
792
- url: btn.url
799
+ callbackData: btn.callback_data ?? void 0,
800
+ url: btn.url ?? void 0
793
801
  }))
794
802
  )
795
803
  };
@@ -971,7 +979,7 @@ var init_console_adapter = __esm({
971
979
  timestamp: Date.now()
972
980
  };
973
981
  }
974
- async editMessage(recipientId, messageId, text, options) {
982
+ async editMessage(_recipientId, messageId, text, options) {
975
983
  const cleanText = text.replace(/<[^>]+>/g, "").replace(/&amp;/g, "&").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
976
984
  console.log(`
977
985
  \x1B[33m[Update #${messageId}]\x1B[0m ${cleanText}`);
@@ -1240,30 +1248,36 @@ var init_opencode_client = __esm({
1240
1248
  const actualPort = port > 0 ? port : await getFreePort();
1241
1249
  const proc = child_process.spawn(
1242
1250
  "opencode",
1243
- ["serve", "--port", String(actualPort), "--hostname", "127.0.0.1"],
1251
+ ["serve", "--port", String(actualPort), "--hostname", "127.0.0.1", "--pure"],
1244
1252
  {
1245
1253
  cwd: targetDir,
1246
1254
  env: {
1247
1255
  ...process.env,
1248
1256
  OPENCODE_SERVER_PASSWORD: password
1249
1257
  },
1250
- stdio: ["ignore", "pipe", "pipe"]
1258
+ stdio: ["ignore", "ignore", "ignore"]
1251
1259
  }
1252
1260
  );
1253
1261
  const client = new _OpenCodeClient(`http://127.0.0.1:${actualPort}`, password, targetDir);
1254
1262
  let ready = false;
1255
- for (let i = 0; i < 30; i++) {
1263
+ for (let i = 0; i < 60; i++) {
1264
+ if (proc.exitCode !== null) {
1265
+ throw new Error(`OpenCode process exited early with code ${proc.exitCode}`);
1266
+ }
1256
1267
  try {
1257
1268
  await client.request("GET", "/global/health");
1258
1269
  ready = true;
1259
1270
  break;
1260
1271
  } catch {
1261
- await new Promise((r) => setTimeout(r, 500));
1272
+ await new Promise((r) => setTimeout(r, 1e3));
1262
1273
  }
1263
1274
  }
1264
1275
  if (!ready) {
1265
- proc.kill();
1266
- throw new Error(`OpenCode server failed to start on port ${actualPort}`);
1276
+ try {
1277
+ proc.kill("SIGTERM");
1278
+ } catch {
1279
+ }
1280
+ throw new Error(`OpenCode server failed to start on port ${actualPort} within 60s`);
1267
1281
  }
1268
1282
  client.serverProcess = proc;
1269
1283
  return { client, proc, port: actualPort };
@@ -1342,6 +1356,12 @@ var init_opencode_adapter = __esm({
1342
1356
  this.client.on("event", this.boundEventHandler);
1343
1357
  this.client.connectEventStream();
1344
1358
  }
1359
+ async listModels() {
1360
+ return this.client.listModels();
1361
+ }
1362
+ async listAgents() {
1363
+ return this.client.listAgents();
1364
+ }
1345
1365
  };
1346
1366
  }
1347
1367
  });
@@ -1567,6 +1587,10 @@ var init_i18n = __esm({
1567
1587
  btn_handoff: "\u{1F501} Handoff",
1568
1588
  btn_help: "\u2753 Help",
1569
1589
  btn_language: "\u{1F310} Language",
1590
+ btn_back: "\u2B05\uFE0F Back",
1591
+ btn_all_models: "\u27A1\uFE0F All Models",
1592
+ btn_favorites: "\u2B50 Favorites",
1593
+ btn_next: "\u27A1\uFE0F Next",
1570
1594
  // Permission Buttons
1571
1595
  perm_allow_once: "\u2705 Allow Once",
1572
1596
  perm_always_allow: "\u267E\uFE0F Always Allow",
@@ -1612,6 +1636,10 @@ var init_i18n = __esm({
1612
1636
  btn_handoff: "\u{1F501} \u062A\u0633\u0644\u064A\u0645",
1613
1637
  btn_help: "\u2753 \u0645\u0633\u0627\u0639\u062F\u0629",
1614
1638
  btn_language: "\u{1F310} \u0627\u0644\u0644\u063A\u0629",
1639
+ btn_back: "\u2B05\uFE0F \u0631\u062C\u0648\u0639",
1640
+ btn_all_models: "\u27A1\uFE0F \u0627\u0644\u0645\u0632\u064A\u062F",
1641
+ btn_favorites: "\u2B50 \u0627\u0644\u0645\u0641\u0636\u0644\u0629",
1642
+ btn_next: "\u27A1\uFE0F \u0627\u0644\u062A\u0627\u0644\u064A",
1615
1643
  // Permission Buttons
1616
1644
  perm_allow_once: "\u2705 \u0645\u0631\u0629 \u0648\u0627\u062D\u062F\u0629",
1617
1645
  perm_always_allow: "\u267E\uFE0F \u0633\u0645\u0627\u062D \u062F\u0627\u0626\u0645\u0627\u064B",
@@ -1645,6 +1673,315 @@ var init_i18n = __esm({
1645
1673
  }
1646
1674
  });
1647
1675
 
1676
+ // src/domain/model-catalog.ts
1677
+ function formatModelName(modelId) {
1678
+ const parts = modelId.split("/");
1679
+ const base = parts[parts.length - 1] || modelId;
1680
+ return base.replace(/-free$/i, "").split("-").map((w) => w.charAt(0).toUpperCase() + w.slice(1)).join(" ");
1681
+ }
1682
+ function resolveModelMetadata(modelId) {
1683
+ if (!modelId) {
1684
+ return {
1685
+ id: "default",
1686
+ name: "Default Model",
1687
+ provider: "Default",
1688
+ contextWindow: "128k",
1689
+ tag: "Default Engine Model"
1690
+ };
1691
+ }
1692
+ if (KNOWN_MODELS[modelId]) {
1693
+ return KNOWN_MODELS[modelId];
1694
+ }
1695
+ const lower = modelId.toLowerCase();
1696
+ for (const [key, meta] of Object.entries(KNOWN_MODELS)) {
1697
+ if (lower === key.toLowerCase() || lower.endsWith(`/${key.toLowerCase()}`)) {
1698
+ return { ...meta, id: modelId };
1699
+ }
1700
+ }
1701
+ if (lower.includes("gemini-1.5-pro") || lower.includes("gemini-2.0-pro")) {
1702
+ return {
1703
+ id: modelId,
1704
+ name: formatModelName(modelId),
1705
+ provider: "Google",
1706
+ contextWindow: "2M",
1707
+ tag: "Ultra-Long Context (2M)",
1708
+ tier: "flagship"
1709
+ };
1710
+ }
1711
+ if (lower.includes("gemini")) {
1712
+ return {
1713
+ id: modelId,
1714
+ name: formatModelName(modelId),
1715
+ provider: "Google",
1716
+ contextWindow: "1M",
1717
+ tag: "1M Tokens Multimodal",
1718
+ tier: "standard"
1719
+ };
1720
+ }
1721
+ if (lower.includes("claude-3-5") || lower.includes("sonnet") || lower.includes("haiku") || lower.includes("opus")) {
1722
+ return {
1723
+ id: modelId,
1724
+ name: formatModelName(modelId),
1725
+ provider: "Anthropic",
1726
+ contextWindow: "200k",
1727
+ tag: "SOTA Coding & Tool Use",
1728
+ tier: "flagship"
1729
+ };
1730
+ }
1731
+ if (lower.includes("deepseek-r1") || lower.includes("reasoner")) {
1732
+ return {
1733
+ id: modelId,
1734
+ name: formatModelName(modelId),
1735
+ provider: "DeepSeek",
1736
+ contextWindow: "128k",
1737
+ tag: "Deep Reasoning CoT",
1738
+ tier: "flagship"
1739
+ };
1740
+ }
1741
+ if (lower.includes("deepseek")) {
1742
+ return {
1743
+ id: modelId,
1744
+ name: formatModelName(modelId),
1745
+ provider: "DeepSeek",
1746
+ contextWindow: "128k",
1747
+ tag: "High-Speed Coding & Logic",
1748
+ tier: "flagship"
1749
+ };
1750
+ }
1751
+ if (lower.includes("o3")) {
1752
+ return {
1753
+ id: modelId,
1754
+ name: formatModelName(modelId),
1755
+ provider: "OpenAI",
1756
+ contextWindow: "200k",
1757
+ tag: "Advanced Math & Code",
1758
+ tier: "flagship"
1759
+ };
1760
+ }
1761
+ if (lower.includes("o1") || lower.includes("gpt-4")) {
1762
+ return {
1763
+ id: modelId,
1764
+ name: formatModelName(modelId),
1765
+ provider: "OpenAI",
1766
+ contextWindow: "128k",
1767
+ tag: "Omni Reasoning & Logic",
1768
+ tier: "flagship"
1769
+ };
1770
+ }
1771
+ if (lower.includes("qwen") || lower.includes("llama") || lower.includes("mistral")) {
1772
+ return {
1773
+ id: modelId,
1774
+ name: formatModelName(modelId),
1775
+ provider: "Open Weights",
1776
+ contextWindow: "128k",
1777
+ tag: "Open-Source Architecture",
1778
+ tier: "standard"
1779
+ };
1780
+ }
1781
+ if (lower.includes("free") || lower.includes("space-bunny") || lower.includes("longcat") || lower.includes("mimo")) {
1782
+ return {
1783
+ id: modelId,
1784
+ name: formatModelName(modelId),
1785
+ provider: "OpenCode Free",
1786
+ contextWindow: "128k",
1787
+ tag: "Community Free Model",
1788
+ tier: "free"
1789
+ };
1790
+ }
1791
+ return {
1792
+ id: modelId,
1793
+ name: formatModelName(modelId),
1794
+ provider: "AI Provider",
1795
+ contextWindow: "128k",
1796
+ tag: "General Intelligence"
1797
+ };
1798
+ }
1799
+ function getStandardModelCatalog() {
1800
+ return Object.keys(KNOWN_MODELS);
1801
+ }
1802
+ var KNOWN_MODELS;
1803
+ var init_model_catalog = __esm({
1804
+ "src/domain/model-catalog.ts"() {
1805
+ "use strict";
1806
+ KNOWN_MODELS = {
1807
+ // Anthropic
1808
+ "claude-3-5-sonnet-20241022": {
1809
+ id: "claude-3-5-sonnet-20241022",
1810
+ name: "Claude 3.5 Sonnet (v2)",
1811
+ provider: "Anthropic",
1812
+ contextWindow: "200k",
1813
+ tag: "Coding Champion & Tool Use",
1814
+ tier: "flagship"
1815
+ },
1816
+ "claude-3-5-sonnet": {
1817
+ id: "claude-3-5-sonnet",
1818
+ name: "Claude 3.5 Sonnet",
1819
+ provider: "Anthropic",
1820
+ contextWindow: "200k",
1821
+ tag: "Coding & Agentic Automation",
1822
+ tier: "flagship"
1823
+ },
1824
+ "claude-3-5-haiku": {
1825
+ id: "claude-3-5-haiku",
1826
+ name: "Claude 3.5 Haiku",
1827
+ provider: "Anthropic",
1828
+ contextWindow: "200k",
1829
+ tag: "Ultra-Fast & Smart",
1830
+ tier: "standard"
1831
+ },
1832
+ "claude-3-opus": {
1833
+ id: "claude-3-opus",
1834
+ name: "Claude 3 Opus",
1835
+ provider: "Anthropic",
1836
+ contextWindow: "200k",
1837
+ tag: "Deep Reasoning & Synthesis",
1838
+ tier: "flagship"
1839
+ },
1840
+ // OpenAI
1841
+ "gpt-4o": {
1842
+ id: "gpt-4o",
1843
+ name: "GPT-4o (Omni)",
1844
+ provider: "OpenAI",
1845
+ contextWindow: "128k",
1846
+ tag: "Multimodal Flagship",
1847
+ tier: "flagship"
1848
+ },
1849
+ "gpt-4o-mini": {
1850
+ id: "gpt-4o-mini",
1851
+ name: "GPT-4o Mini",
1852
+ provider: "OpenAI",
1853
+ contextWindow: "128k",
1854
+ tag: "High Speed & Efficiency",
1855
+ tier: "standard"
1856
+ },
1857
+ "o1": {
1858
+ id: "o1",
1859
+ name: "OpenAI o1",
1860
+ provider: "OpenAI",
1861
+ contextWindow: "128k",
1862
+ tag: "Deep CoT Reflection & Logic",
1863
+ tier: "flagship"
1864
+ },
1865
+ "o1-mini": {
1866
+ id: "o1-mini",
1867
+ name: "OpenAI o1-mini",
1868
+ provider: "OpenAI",
1869
+ contextWindow: "128k",
1870
+ tag: "Math & STEM Reasoning",
1871
+ tier: "standard"
1872
+ },
1873
+ "o3-mini": {
1874
+ id: "o3-mini",
1875
+ name: "OpenAI o3-mini",
1876
+ provider: "OpenAI",
1877
+ contextWindow: "200k",
1878
+ tag: "High-Performance Math & Code",
1879
+ tier: "flagship"
1880
+ },
1881
+ // Google Gemini
1882
+ "gemini-1.5-pro": {
1883
+ id: "gemini-1.5-pro",
1884
+ name: "Gemini 1.5 Pro",
1885
+ provider: "Google",
1886
+ contextWindow: "2M",
1887
+ tag: "Massive 2M Tokens Repo Context",
1888
+ tier: "flagship"
1889
+ },
1890
+ "gemini-1.5-flash": {
1891
+ id: "gemini-1.5-flash",
1892
+ name: "Gemini 1.5 Flash",
1893
+ provider: "Google",
1894
+ contextWindow: "1M",
1895
+ tag: "Fast 1M Window & Multimodal",
1896
+ tier: "standard"
1897
+ },
1898
+ "gemini-2.0-flash": {
1899
+ id: "gemini-2.0-flash",
1900
+ name: "Gemini 2.0 Flash",
1901
+ provider: "Google",
1902
+ contextWindow: "1M",
1903
+ tag: "Next-Gen Realtime Multimodal",
1904
+ tier: "flagship"
1905
+ },
1906
+ // DeepSeek
1907
+ "deepseek-chat": {
1908
+ id: "deepseek-chat",
1909
+ name: "DeepSeek V3",
1910
+ provider: "DeepSeek",
1911
+ contextWindow: "128k",
1912
+ tag: "SOTA Coding & Architecture",
1913
+ tier: "flagship"
1914
+ },
1915
+ "deepseek-reasoner": {
1916
+ id: "deepseek-reasoner",
1917
+ name: "DeepSeek R1",
1918
+ provider: "DeepSeek",
1919
+ contextWindow: "128k",
1920
+ tag: "Open CoT Reasoning Engine",
1921
+ tier: "flagship"
1922
+ },
1923
+ // OpenCode Community Free Models
1924
+ "opencode/deepseek-v4.1-flash-free": {
1925
+ id: "opencode/deepseek-v4.1-flash-free",
1926
+ name: "DeepSeek v4.1 Flash",
1927
+ provider: "OpenCode Free",
1928
+ contextWindow: "128k",
1929
+ tag: "Fast Free Coding Agent",
1930
+ tier: "free"
1931
+ },
1932
+ "opencode/space-bunny-free": {
1933
+ id: "opencode/space-bunny-free",
1934
+ name: "Space Bunny Free",
1935
+ provider: "OpenCode Free",
1936
+ contextWindow: "128k",
1937
+ tag: "Community Automation Agent",
1938
+ tier: "free"
1939
+ },
1940
+ "opencode/glm-5.3-flash-free": {
1941
+ id: "opencode/glm-5.3-flash-free",
1942
+ name: "GLM 5.3 Flash Free",
1943
+ provider: "OpenCode Free",
1944
+ contextWindow: "128k",
1945
+ tag: "High Concurrency Coding",
1946
+ tier: "free"
1947
+ },
1948
+ "opencode/nemotron-3-ultra-free": {
1949
+ id: "opencode/nemotron-3-ultra-free",
1950
+ name: "Nemotron 3 Ultra Free",
1951
+ provider: "OpenCode Free",
1952
+ contextWindow: "128k",
1953
+ tag: "NVIDIA Hardware Accelerated",
1954
+ tier: "free"
1955
+ },
1956
+ "opencode/mimo-v2.6-flash-free": {
1957
+ id: "opencode/mimo-v2.6-flash-free",
1958
+ name: "Mimo v2.6 Flash Free",
1959
+ provider: "OpenCode Free",
1960
+ contextWindow: "128k",
1961
+ tag: "Lightweight Quick Edits",
1962
+ tier: "free"
1963
+ },
1964
+ "opencode/longcat-2.5-preview-free": {
1965
+ id: "opencode/longcat-2.5-preview-free",
1966
+ name: "LongCat 2.5 Preview",
1967
+ provider: "OpenCode Free",
1968
+ contextWindow: "128k",
1969
+ tag: "Long Context Free Preview",
1970
+ tier: "free"
1971
+ },
1972
+ // Open Weights
1973
+ "qwen2.5-coder-32b": {
1974
+ id: "qwen2.5-coder-32b",
1975
+ name: "Qwen 2.5 Coder 32B",
1976
+ provider: "Alibaba",
1977
+ contextWindow: "128k",
1978
+ tag: "Open-Weights Coding Champion",
1979
+ tier: "flagship"
1980
+ }
1981
+ };
1982
+ }
1983
+ });
1984
+
1648
1985
  // src/control-room/control-room.ts
1649
1986
  function parseCallback(data) {
1650
1987
  if (data.startsWith("pm:")) {
@@ -1699,18 +2036,21 @@ function permissionKeyboard(permId, lang = "en") {
1699
2036
  inline_keyboard: rows
1700
2037
  };
1701
2038
  }
1702
- var fs3, path3, SYSTEM_HEADER, ControlRoom;
2039
+ var fs3, os2, path3, child_process3, SYSTEM_HEADER, ControlRoom;
1703
2040
  var init_control_room = __esm({
1704
2041
  "src/control-room/control-room.ts"() {
1705
2042
  "use strict";
1706
2043
  fs3 = __toESM(require("fs"));
2044
+ os2 = __toESM(require("os"));
1707
2045
  path3 = __toESM(require("path"));
2046
+ child_process3 = __toESM(require("child_process"));
1708
2047
  init_telegram_client();
1709
2048
  init_opencode_client();
1710
2049
  init_telegram_adapter();
1711
2050
  init_opencode_adapter();
1712
2051
  init_policy();
1713
2052
  init_i18n();
2053
+ init_model_catalog();
1714
2054
  SYSTEM_HEADER = (targetDir) => `
1715
2055
  BRIDGE RULES (highest priority):
1716
2056
  - Primary project workspace: ${targetDir}.
@@ -1736,6 +2076,7 @@ BRIDGE RULES (highest priority):
1736
2076
  alwaysAllowedPerms = /* @__PURE__ */ new Set();
1737
2077
  missionCardMsgId = null;
1738
2078
  updateTimer = null;
2079
+ activeSkill = null;
1739
2080
  isListening = false;
1740
2081
  constructor(arg1, arg2, arg3) {
1741
2082
  if ("channel" in arg1 && "engine" in arg1) {
@@ -1786,13 +2127,23 @@ BRIDGE RULES (highest priority):
1786
2127
  });
1787
2128
  }
1788
2129
  async start() {
1789
- await this.engine.connect();
2130
+ try {
2131
+ await this.engine.connect();
2132
+ } catch (err) {
2133
+ console.warn("\u26A0\uFE0F Warning: Engine connection failed at startup:", err.message);
2134
+ }
1790
2135
  this.isListening = true;
1791
- await this.channel.sendMessage(
1792
- this.allowedChatId,
1793
- t("welcome", this.language, { target: escapeHtml2(this.targetDir) }),
1794
- { replyMarkup: this.getMainKeyboard() }
1795
- );
2136
+ if (this.allowedChatId && this.allowedChatId.trim()) {
2137
+ try {
2138
+ await this.channel.sendMessage(
2139
+ this.allowedChatId,
2140
+ t("welcome", this.language, { target: escapeHtml(this.targetDir) }),
2141
+ { replyMarkup: this.getMainKeyboard() }
2142
+ );
2143
+ } catch (err) {
2144
+ console.warn("\u26A0\uFE0F Warning: Could not deliver initial welcome card:", err.message);
2145
+ }
2146
+ }
1796
2147
  await this.channel.startListening({
1797
2148
  onMessage: async (msg) => {
1798
2149
  if (this.channel.id === "telegram" && this.allowedChatId && msg.chatId !== this.allowedChatId) {
@@ -1826,20 +2177,57 @@ BRIDGE RULES (highest priority):
1826
2177
  if (text === "/start" || text === "/help") {
1827
2178
  await this.channel.sendMessage(
1828
2179
  this.allowedChatId,
1829
- `\u26A1 <b>AgentGram v2.0.0</b>
2180
+ `\u26A1 <b>AgentGram v2.0.0 \xB7 Mission Control</b>
1830
2181
  ${t("help_title", this.language)}`,
1831
2182
  { replyMarkup: this.getMainKeyboard() }
1832
2183
  );
1833
2184
  return;
1834
2185
  }
2186
+ if (text === "/status" || text === "st") {
2187
+ await this.sendStatusReport();
2188
+ return;
2189
+ }
2190
+ if (text === "/agents") {
2191
+ await this.sendAgentsMenu();
2192
+ return;
2193
+ }
2194
+ if (text === "/models") {
2195
+ await this.sendModelsMenu();
2196
+ return;
2197
+ }
1835
2198
  if (text === "/mode") {
1836
2199
  await this.sendModeMenu();
1837
2200
  return;
1838
2201
  }
2202
+ if (text === "/sessions") {
2203
+ await this.sendSessionsMenu();
2204
+ return;
2205
+ }
2206
+ if (text === "/new") {
2207
+ this.activeRun = null;
2208
+ await this.channel.sendMessage(
2209
+ this.allowedChatId,
2210
+ "\u2728 <b>New Session Ready</b> \u2014 send your prompt to start.",
2211
+ { replyMarkup: this.getMainKeyboard() }
2212
+ );
2213
+ return;
2214
+ }
1839
2215
  if (text === "/diff") {
1840
2216
  await this.sendDiff();
1841
2217
  return;
1842
2218
  }
2219
+ if (text === "/skills") {
2220
+ await this.sendSkillsMenu();
2221
+ return;
2222
+ }
2223
+ if (text === "/usage") {
2224
+ await this.sendUsageReport();
2225
+ return;
2226
+ }
2227
+ if (text === "/handoff") {
2228
+ await this.sendHandoffReport();
2229
+ return;
2230
+ }
1843
2231
  if (text === "/stop") {
1844
2232
  await this.abortCurrentRun();
1845
2233
  return;
@@ -1865,7 +2253,7 @@ ${t("help_title", this.language)}`,
1865
2253
  if (hits.length > 0) {
1866
2254
  await this.channel.sendMessage(
1867
2255
  this.allowedChatId,
1868
- t("policy_denied", this.language, { hits: escapeHtml2(hits.slice(0, 3).join(", ")) })
2256
+ t("policy_denied", this.language, { hits: escapeHtml(hits.slice(0, 3).join(", ")) })
1869
2257
  );
1870
2258
  return;
1871
2259
  }
@@ -1874,7 +2262,7 @@ ${t("help_title", this.language)}`,
1874
2262
  await this.channel.sendMessage(
1875
2263
  this.allowedChatId,
1876
2264
  t("prompt_injection_blocked", this.language, {
1877
- reason: escapeHtml2(detection.reason),
2265
+ reason: escapeHtml(detection.reason),
1878
2266
  confidence: Math.round(detection.confidence * 100)
1879
2267
  })
1880
2268
  );
@@ -1915,12 +2303,18 @@ ${t("help_title", this.language)}`,
1915
2303
  );
1916
2304
  this.missionCardMsgId = cardMsg.messageId;
1917
2305
  this.startMissionCardTicker();
1918
- const sanitized = this.policy.sanitizePrompt(promptText);
2306
+ let finalPrompt = promptText;
2307
+ if (this.activeSkill) {
2308
+ finalPrompt = `[Active Skill: ${this.activeSkill}]
2309
+ ${promptText}`;
2310
+ this.activeSkill = null;
2311
+ }
2312
+ const sanitized = this.policy.sanitizePrompt(finalPrompt);
1919
2313
  await this.engine.submitPrompt(sessionId, sanitized, this.currentAgent, this.currentModel);
1920
2314
  } catch (err) {
1921
2315
  await this.channel.sendMessage(
1922
2316
  this.allowedChatId,
1923
- `\u274C Error: ${escapeHtml2(err.message)}`
2317
+ `\u274C Error: ${escapeHtml(err.message)}`
1924
2318
  );
1925
2319
  }
1926
2320
  }
@@ -1951,29 +2345,108 @@ ${t("help_title", this.language)}`,
1951
2345
  }
1952
2346
  return;
1953
2347
  }
2348
+ if (data === "menu:home" || data === "home") {
2349
+ await this.channel.sendMessage(
2350
+ this.allowedChatId,
2351
+ t("welcome", this.language, { target: escapeHtml(this.targetDir) }),
2352
+ { replyMarkup: this.getMainKeyboard() }
2353
+ );
2354
+ return;
2355
+ }
2356
+ if (data === "menu:ag") {
2357
+ await this.sendAgentsMenu();
2358
+ return;
2359
+ }
2360
+ if (data.startsWith("ag:")) {
2361
+ const agent = data.slice(3);
2362
+ this.currentAgent = agent;
2363
+ this.cfgManager.config.defaultAgent = agent;
2364
+ this.cfgManager.saveGlobal();
2365
+ await this.channel.sendMessage(
2366
+ this.allowedChatId,
2367
+ `\u{1F9E0} Agent changed to: <b>${escapeHtml(agent)}</b>`,
2368
+ { replyMarkup: this.getMainKeyboard() }
2369
+ );
2370
+ return;
2371
+ }
2372
+ if (data === "menu:md") {
2373
+ await this.sendModelsMenu(0);
2374
+ return;
2375
+ }
2376
+ if (data.startsWith("pg:")) {
2377
+ const page = parseInt(data.slice(3), 10) || 0;
2378
+ await this.sendModelsMenu(page);
2379
+ return;
2380
+ }
2381
+ if (data.startsWith("md:raw:")) {
2382
+ const model = data.slice(7);
2383
+ if (model) {
2384
+ this.currentModel = model;
2385
+ this.cfgManager.config.defaultModel = model;
2386
+ this.cfgManager.saveGlobal();
2387
+ await this.channel.sendMessage(
2388
+ this.allowedChatId,
2389
+ `\u{1F916} Model changed to: <b>${escapeHtml(model)}</b>`,
2390
+ { replyMarkup: this.getMainKeyboard() }
2391
+ );
2392
+ }
2393
+ return;
2394
+ }
2395
+ if (data.startsWith("md:")) {
2396
+ const idx = parseInt(data.slice(3), 10);
2397
+ const model = this.cfgManager.config.favoriteModels[idx];
2398
+ if (model) {
2399
+ this.currentModel = model;
2400
+ this.cfgManager.config.defaultModel = model;
2401
+ this.cfgManager.saveGlobal();
2402
+ await this.channel.sendMessage(
2403
+ this.allowedChatId,
2404
+ `\u{1F916} Model changed to: <b>${escapeHtml(model)}</b>`,
2405
+ { replyMarkup: this.getMainKeyboard() }
2406
+ );
2407
+ }
2408
+ return;
2409
+ }
2410
+ if (data === "menu:mo") {
2411
+ await this.sendModeMenu();
2412
+ return;
2413
+ }
1954
2414
  if (data.startsWith("mo:")) {
1955
2415
  const mode = data.slice(3);
1956
2416
  if (["read", "build", "auto"].includes(mode)) {
1957
2417
  this.currentMode = mode;
1958
- if (messageId !== void 0) {
1959
- await this.channel.sendMessage(
1960
- this.allowedChatId,
1961
- `\u{1F512} Mode changed to: <b>${mode}</b>`
1962
- );
1963
- }
2418
+ await this.channel.sendMessage(
2419
+ this.allowedChatId,
2420
+ `\u{1F512} Mode changed to: <b>${mode}</b>`,
2421
+ { replyMarkup: this.getMainKeyboard() }
2422
+ );
1964
2423
  }
1965
2424
  return;
1966
2425
  }
1967
- if (data === "stop") {
1968
- await this.abortCurrentRun();
2426
+ if (data === "menu:se") {
2427
+ await this.sendSessionsMenu();
1969
2428
  return;
1970
2429
  }
1971
- if (data === "diff") {
1972
- await this.sendDiff();
2430
+ if (data === "new") {
2431
+ this.activeRun = null;
2432
+ await this.channel.sendMessage(
2433
+ this.allowedChatId,
2434
+ "\u2728 <b>New Session Ready</b> \u2014 send your prompt to start fresh.",
2435
+ { replyMarkup: this.getMainKeyboard() }
2436
+ );
1973
2437
  return;
1974
2438
  }
1975
- if (data === "menu:mo") {
1976
- await this.sendModeMenu();
2439
+ if (data.startsWith("se:")) {
2440
+ const sid = data.slice(3);
2441
+ await this.channel.sendMessage(
2442
+ this.allowedChatId,
2443
+ `\u{1F4C2} Resumed session: <code>${escapeHtml(sid)}</code>`,
2444
+ { replyMarkup: this.getMainKeyboard() }
2445
+ );
2446
+ return;
2447
+ }
2448
+ if (data === "st") {
2449
+ await this.sendStatusReport();
1977
2450
  return;
1978
2451
  }
1979
2452
  if (data === "menu:ws") {
@@ -1987,6 +2460,69 @@ ${t("help_title", this.language)}`,
1987
2460
  if (target) {
1988
2461
  await this.switchWorkspace(target);
1989
2462
  }
2463
+ return;
2464
+ }
2465
+ if (data === "diff") {
2466
+ await this.sendDiff();
2467
+ return;
2468
+ }
2469
+ if (data === "menu:sk") {
2470
+ await this.sendSkillsMenu(0);
2471
+ return;
2472
+ }
2473
+ if (data.startsWith("skpage:")) {
2474
+ const page = parseInt(data.slice(7), 10) || 0;
2475
+ await this.sendSkillsMenu(page);
2476
+ return;
2477
+ }
2478
+ if (data.startsWith("sk:")) {
2479
+ const sk = data.slice(3);
2480
+ this.activeSkill = sk;
2481
+ await this.channel.sendMessage(
2482
+ this.allowedChatId,
2483
+ `\u{1F9F0} Selected Skill: <b>${escapeHtml(sk)}</b> \u2014 send your task to execute with this skill.`,
2484
+ { replyMarkup: this.getMainKeyboard() }
2485
+ );
2486
+ return;
2487
+ }
2488
+ if (data === "us") {
2489
+ await this.sendUsageReport();
2490
+ return;
2491
+ }
2492
+ if (data === "menu:lg") {
2493
+ await this.sendLanguageMenu();
2494
+ return;
2495
+ }
2496
+ if (data.startsWith("lang:")) {
2497
+ const lang = data.slice(5);
2498
+ if (lang === "en" || lang === "ar") {
2499
+ this.language = lang;
2500
+ this.cfgManager.config.language = lang;
2501
+ this.cfgManager.saveGlobal();
2502
+ await this.channel.sendMessage(
2503
+ this.allowedChatId,
2504
+ t("lang_changed", this.language),
2505
+ { replyMarkup: this.getMainKeyboard() }
2506
+ );
2507
+ }
2508
+ return;
2509
+ }
2510
+ if (data === "ho") {
2511
+ await this.sendHandoffReport();
2512
+ return;
2513
+ }
2514
+ if (data === "hp") {
2515
+ await this.channel.sendMessage(
2516
+ this.allowedChatId,
2517
+ `\u26A1 <b>AgentGram Commands & Mission Control</b>
2518
+ ${t("help_title", this.language)}`,
2519
+ { replyMarkup: this.getMainKeyboard() }
2520
+ );
2521
+ return;
2522
+ }
2523
+ if (data === "stop") {
2524
+ await this.abortCurrentRun();
2525
+ return;
1990
2526
  }
1991
2527
  }
1992
2528
  async handleEngineEvent(event) {
@@ -2006,7 +2542,7 @@ ${t("help_title", this.language)}`,
2006
2542
  if (decision === "reject") {
2007
2543
  await this.engine.replyPermission(permId, "reject");
2008
2544
  if (reason) {
2009
- await this.channel.sendMessage(this.allowedChatId, `\u26D4 ${escapeHtml2(reason)}`);
2545
+ await this.channel.sendMessage(this.allowedChatId, `\u26D4 ${escapeHtml(reason)}`);
2010
2546
  }
2011
2547
  return;
2012
2548
  }
@@ -2014,9 +2550,9 @@ ${t("help_title", this.language)}`,
2014
2550
  await this.channel.sendMessage(
2015
2551
  this.allowedChatId,
2016
2552
  t("permission_prompt", this.language, {
2017
- tool: escapeHtml2(tool),
2018
- pattern: escapeHtml2(String(props.patterns || "*")),
2019
- details: cmd ? `<code>${escapeHtml2(cmd.slice(0, 150))}</code>` : ""
2553
+ tool: escapeHtml(tool),
2554
+ pattern: escapeHtml(String(props.patterns || "*")),
2555
+ details: cmd ? `<code>${escapeHtml(cmd.slice(0, 150))}</code>` : ""
2020
2556
  }),
2021
2557
  { replyMarkup: kb }
2022
2558
  );
@@ -2057,11 +2593,11 @@ ${t("help_title", this.language)}`,
2057
2593
  const statusText = run.done ? "Finished" : run.busy ? "Executing" : "Waiting";
2058
2594
  return `
2059
2595
  \u26A1 <b>AgentGram \xB7 Live Mission Control</b>
2060
- Agent: \u{1F9E0} <code>${escapeHtml2(run.agent)}</code> (${escapeHtml2(this.engine.name)}) | Mode: \u{1F512} <b>${run.mode}</b>
2061
- Channel: \u{1F4E1} <b>${escapeHtml2(this.channel.name)}</b>
2596
+ Agent: \u{1F9E0} <code>${escapeHtml(run.agent)}</code> (${escapeHtml(this.engine.name)}) | Mode: \u{1F512} <b>${run.mode}</b>
2597
+ Channel: \u{1F4E1} <b>${escapeHtml(this.channel.name)}</b>
2062
2598
  Status: ${statusIcon} <b>${statusText}</b> \xB7 \u23F3 ${elapsed}s
2063
2599
  Commands: \u2699\uFE0F ${run.cmds} \xB7 Files: \u{1F4DD} ${run.files.size}
2064
- Workspace: \u{1F4C1} <code>${escapeHtml2(path3.basename(this.targetDir))}</code>
2600
+ Workspace: \u{1F4C1} <code>${escapeHtml(path3.basename(this.targetDir))}</code>
2065
2601
  `.trim();
2066
2602
  }
2067
2603
  async updateMissionCard(final = false) {
@@ -2080,49 +2616,382 @@ Workspace: \u{1F4C1} <code>${escapeHtml2(path3.basename(this.targetDir))}</code>
2080
2616
  } catch {
2081
2617
  }
2082
2618
  }
2619
+ /**
2620
+ * The complete, feature-rich 5-row interactive home keyboard with all 14 buttons.
2621
+ */
2083
2622
  getMainKeyboard() {
2084
2623
  const rows = [
2085
2624
  [
2086
- { text: t("btn_stop", this.language), callback_data: "stop" },
2087
- { text: t("btn_diff", this.language), callback_data: "diff" },
2625
+ { text: t("btn_agents", this.language), callback_data: "menu:ag" },
2626
+ { text: t("btn_models", this.language), callback_data: "menu:md" },
2088
2627
  { text: t("btn_mode", this.language), callback_data: "menu:mo" }
2089
2628
  ],
2090
2629
  [
2630
+ { text: t("btn_sessions", this.language), callback_data: "menu:se" },
2631
+ { text: t("btn_new", this.language), callback_data: "new" },
2632
+ { text: t("btn_stop", this.language), callback_data: "stop" }
2633
+ ],
2634
+ [
2635
+ { text: t("btn_status", this.language), callback_data: "st" },
2091
2636
  { text: t("btn_workspaces", this.language), callback_data: "menu:ws" },
2637
+ { text: t("btn_diff", this.language), callback_data: "diff" }
2638
+ ],
2639
+ [
2640
+ { text: t("btn_skills", this.language), callback_data: "menu:sk" },
2641
+ { text: t("btn_usage", this.language), callback_data: "us" },
2092
2642
  { text: t("btn_language", this.language), callback_data: "menu:lg" }
2643
+ ],
2644
+ [
2645
+ { text: t("btn_handoff", this.language), callback_data: "ho" },
2646
+ { text: t("btn_help", this.language), callback_data: "hp" }
2093
2647
  ]
2094
2648
  ];
2649
+ return this.wrapKeyboard(rows);
2650
+ }
2651
+ wrapKeyboard(rows) {
2652
+ const cleanRows = rows.map(
2653
+ (r) => r.map((btn) => ({
2654
+ text: stripHtml(btn.text).replace(/^[⚡\s]+/, "").trim(),
2655
+ callback_data: btn.callback_data
2656
+ }))
2657
+ );
2095
2658
  return {
2096
- rows: rows.map(
2659
+ rows: cleanRows.map(
2097
2660
  (r) => r.map((btn) => ({
2098
2661
  text: btn.text,
2099
2662
  callbackData: btn.callback_data,
2100
2663
  callback_data: btn.callback_data
2101
2664
  }))
2102
2665
  ),
2103
- inline_keyboard: rows
2666
+ inline_keyboard: cleanRows
2104
2667
  };
2105
2668
  }
2669
+ async sendAgentsMenu() {
2670
+ const agents = ["build", "delegate", "explorer", "code-review"];
2671
+ const rows = [];
2672
+ let currentRow = [];
2673
+ for (const a of agents) {
2674
+ const isCurrent = a === this.currentAgent;
2675
+ currentRow.push({
2676
+ text: `${isCurrent ? "\u25CF " : ""}${a}`,
2677
+ callback_data: `ag:${a}`
2678
+ });
2679
+ if (currentRow.length === 2) {
2680
+ rows.push(currentRow);
2681
+ currentRow = [];
2682
+ }
2683
+ }
2684
+ if (currentRow.length > 0) rows.push(currentRow);
2685
+ rows.push([{ text: t("btn_back", this.language), callback_data: "menu:home" }]);
2686
+ await this.channel.sendMessage(
2687
+ this.allowedChatId,
2688
+ t("select_agent", this.language, { agent: escapeHtml(this.currentAgent) }),
2689
+ { replyMarkup: this.wrapKeyboard(rows) }
2690
+ );
2691
+ }
2692
+ async sendModelsMenu(page = 0) {
2693
+ const favs = this.cfgManager.config.favoriteModels || [];
2694
+ let engineModels = [];
2695
+ if (typeof this.engine.listModels === "function") {
2696
+ try {
2697
+ const raw = await this.engine.listModels();
2698
+ engineModels = (raw || []).map((m) => m.id);
2699
+ } catch {
2700
+ }
2701
+ }
2702
+ const standardCatalog = getStandardModelCatalog();
2703
+ const combinedSet = /* @__PURE__ */ new Set([...favs, ...engineModels, ...standardCatalog]);
2704
+ const allModels = Array.from(combinedSet);
2705
+ const rest = allModels.filter((m) => !favs.includes(m));
2706
+ const pageSize = 6;
2707
+ const pool = page === 0 ? favs : rest.slice((page - 1) * pageSize, page * pageSize);
2708
+ const totalPages = Math.ceil(rest.length / pageSize) || 1;
2709
+ const rows = pool.map((m, idx) => {
2710
+ const meta = resolveModelMetadata(m);
2711
+ const isCurrent = m === this.currentModel;
2712
+ const cleanName = meta.name.length > 20 ? meta.name.slice(0, 18) + "\u2026" : meta.name;
2713
+ return [
2714
+ {
2715
+ text: `${isCurrent ? "\u25CF " : ""}${cleanName} \xB7 ${meta.contextWindow}`,
2716
+ callback_data: page === 0 ? `md:${idx}` : `md:raw:${m}`
2717
+ }
2718
+ ];
2719
+ });
2720
+ if (page === 0) {
2721
+ if (rest.length > 0) {
2722
+ rows.push([{ text: t("btn_all_models", this.language), callback_data: "pg:1" }]);
2723
+ }
2724
+ rows.push([{ text: t("btn_back", this.language), callback_data: "menu:home" }]);
2725
+ } else {
2726
+ const pagRow = [
2727
+ { text: t("btn_favorites", this.language), callback_data: "pg:0" }
2728
+ ];
2729
+ if (rest.length > page * pageSize) {
2730
+ pagRow.push({ text: t("btn_next", this.language), callback_data: `pg:${page + 1}` });
2731
+ }
2732
+ rows.push(pagRow);
2733
+ rows.push([{ text: t("btn_back", this.language), callback_data: "menu:md" }]);
2734
+ }
2735
+ const curMeta = resolveModelMetadata(this.currentModel);
2736
+ let cardText = this.language === "ar" ? `\u{1F916} <b>\u0646\u0645\u0627\u0630\u062C \u0627\u0644\u0630\u0643\u0627\u0621 \u0627\u0644\u0627\u0635\u0637\u0646\u0627\u0639\u064A \u0648\u0645\u0633\u0627\u062D\u0629 \u0627\u0644\u0633\u064A\u0627\u0642</b> ${page > 0 ? `(\u0635\u0641\u062D\u0629 ${page} \u0645\u0646 ${totalPages})` : "(\u0627\u0644\u0645\u0641\u0636\u0644\u0629)"}
2737
+ \u0627\u0644\u0646\u0645\u0648\u0630\u062C \u0627\u0644\u0646\u0634\u0637: <b>${escapeHtml(curMeta.name)}</b> (<code>${escapeHtml(curMeta.contextWindow)}</code>)
2738
+
2739
+ ` : `\u{1F916} <b>AI Models & Context Windows</b> ${page > 0 ? `(Page ${page} of ${totalPages})` : "(Favorites)"}
2740
+ Active: <b>${escapeHtml(curMeta.name)}</b> (<code>${escapeHtml(curMeta.contextWindow)}</code>)
2741
+
2742
+ `;
2743
+ for (const m of pool) {
2744
+ const meta = resolveModelMetadata(m);
2745
+ const isCurrent = m === this.currentModel;
2746
+ cardText += `${isCurrent ? "\u25CF" : "\u2022"} <b>${escapeHtml(meta.name)}</b> (<code>${escapeHtml(meta.provider)}</code>)
2747
+ `;
2748
+ cardText += ` <code>\u0627\u0644\u0633\u064A\u0627\u0642: ${escapeHtml(meta.contextWindow)} | ${escapeHtml(meta.tag)}</code>
2749
+ `;
2750
+ }
2751
+ cardText += this.language === "ar" ? `
2752
+ <i>\u0627\u0636\u063A\u0637 \u0639\u0644\u0649 \u0623\u064A \u0646\u0645\u0648\u0630\u062C \u0623\u062F\u0646\u0627\u0647 \u0644\u0644\u062A\u0628\u062F\u064A\u0644 \u0625\u0644\u064A\u0647 \u0641\u0648\u0631\u0627\u064B:</i>` : `
2753
+ <i>Tap any model below to activate immediately:</i>`;
2754
+ await this.channel.sendMessage(
2755
+ this.allowedChatId,
2756
+ cardText.trim(),
2757
+ { replyMarkup: this.wrapKeyboard(rows) }
2758
+ );
2759
+ }
2106
2760
  async sendModeMenu() {
2107
2761
  const rows = [
2108
2762
  [
2109
- { text: "\u{1F512} Read-Only", callback_data: "mo:read" },
2110
- { text: "\u{1F528} Build (Interactive)", callback_data: "mo:build" },
2111
- { text: "\u26A1 Auto Mode", callback_data: "mo:auto" }
2763
+ { text: `${this.currentMode === "read" ? "\u25CF " : ""}\u{1F512} Read-Only`, callback_data: "mo:read" },
2764
+ { text: `${this.currentMode === "build" ? "\u25CF " : ""}\u{1F528} Build (Interactive)`, callback_data: "mo:build" },
2765
+ { text: `${this.currentMode === "auto" ? "\u25CF " : ""}\u26A1 Auto Mode`, callback_data: "mo:auto" }
2766
+ ],
2767
+ [
2768
+ { text: t("btn_back", this.language), callback_data: "menu:home" }
2112
2769
  ]
2113
2770
  ];
2114
- const kb = {
2115
- rows: rows.map(
2116
- (r) => r.map((b) => ({ text: b.text, callbackData: b.callback_data, callback_data: b.callback_data }))
2117
- ),
2118
- inline_keyboard: rows
2119
- };
2120
2771
  await this.channel.sendMessage(
2121
2772
  this.allowedChatId,
2122
2773
  t("select_mode", this.language, { mode: this.currentMode }),
2123
- { replyMarkup: kb }
2774
+ { replyMarkup: this.wrapKeyboard(rows) }
2775
+ );
2776
+ }
2777
+ async sendSessionsMenu() {
2778
+ const rows = [
2779
+ [{ text: "\u2728 " + t("btn_new", this.language), callback_data: "new" }]
2780
+ ];
2781
+ if (this.activeRun && this.activeRun.sessionId) {
2782
+ rows.unshift([{
2783
+ text: `\u25CF Session: ${this.activeRun.sessionId.slice(0, 16)}\u2026`,
2784
+ callback_data: `se:${this.activeRun.sessionId}`
2785
+ }]);
2786
+ }
2787
+ rows.push([{ text: t("btn_back", this.language), callback_data: "menu:home" }]);
2788
+ await this.channel.sendMessage(
2789
+ this.allowedChatId,
2790
+ `\u{1F4C2} <b>Active Sessions</b>
2791
+ Current: <code>${escapeHtml(this.activeRun?.sessionId || "None (New)")}</code>`,
2792
+ { replyMarkup: this.wrapKeyboard(rows) }
2124
2793
  );
2125
2794
  }
2795
+ async sendStatusReport() {
2796
+ let branch = "unknown";
2797
+ let dirtyCount = 0;
2798
+ try {
2799
+ const bRes = child_process3.spawnSync("git", ["branch", "--show-current"], {
2800
+ cwd: this.targetDir,
2801
+ encoding: "utf-8",
2802
+ timeout: 5e3
2803
+ });
2804
+ if (bRes.status === 0) branch = bRes.stdout.trim() || "main";
2805
+ const sRes = child_process3.spawnSync("git", ["status", "--porcelain"], {
2806
+ cwd: this.targetDir,
2807
+ encoding: "utf-8",
2808
+ timeout: 5e3
2809
+ });
2810
+ if (sRes.status === 0) {
2811
+ dirtyCount = sRes.stdout.trim().split("\n").filter(Boolean).length;
2812
+ }
2813
+ } catch {
2814
+ }
2815
+ const isBusy = this.activeRun && this.activeRun.busy ? "\u{1F7E2} Running" : "\u23F8 Idle";
2816
+ const text = `
2817
+ \u{1F4CA} <b>System & Mission Control Status</b>
2818
+ \u{1F4C1} Workspace: <b>${escapeHtml(path3.basename(this.targetDir))}</b>
2819
+ <code>${escapeHtml(this.targetDir)}</code>
2820
+ \u{1F9E0} Agent: <b>${escapeHtml(this.currentAgent)}</b> (${escapeHtml(this.engine.name)})
2821
+ \u{1F916} Model: <b>${escapeHtml(this.currentModel || "Default")}</b>
2822
+ \u{1F512} Mode: <b>${escapeHtml(this.currentMode)}</b>
2823
+ \u{1F4E1} Channel: <b>${escapeHtml(this.channel.name)}</b>
2824
+ \u{1F4C2} Session: <code>${escapeHtml(this.activeRun?.sessionId || "None (Ready)")}</code>
2825
+ \u23F3 Status: <b>${isBusy}</b>
2826
+ \u{1F33F} Git Branch: <code>${escapeHtml(branch)}</code> \xB7 Modified files: <b>${dirtyCount}</b>
2827
+ `.trim();
2828
+ await this.channel.sendMessage(this.allowedChatId, text, {
2829
+ replyMarkup: this.getMainKeyboard()
2830
+ });
2831
+ }
2832
+ getAllAvailableSkills() {
2833
+ const candidates = [
2834
+ path3.join(this.targetDir, ".agents", "skills"),
2835
+ path3.join(this.targetDir, "docs", "ai", "skills"),
2836
+ path3.join(this.targetDir, ".gemini", "skills")
2837
+ ];
2838
+ const map = /* @__PURE__ */ new Map();
2839
+ for (const dir of candidates) {
2840
+ if (!fs3.existsSync(dir)) continue;
2841
+ try {
2842
+ const items = fs3.readdirSync(dir, { withFileTypes: true });
2843
+ for (const item of items) {
2844
+ if (!item.isDirectory() && !item.isSymbolicLink()) continue;
2845
+ if (item.name.startsWith(".")) continue;
2846
+ const raw = item.name;
2847
+ const clean = raw.replace(/^\d+-/, "");
2848
+ if (!map.has(clean)) {
2849
+ let desc;
2850
+ const skillMd = path3.join(dir, raw, "SKILL.md");
2851
+ if (fs3.existsSync(skillMd)) {
2852
+ try {
2853
+ const head = fs3.readFileSync(skillMd, "utf-8").slice(0, 600);
2854
+ const m = head.match(/description:\s*["']?([^"'\n\r]+)/i);
2855
+ if (m && m[1]) {
2856
+ desc = m[1].replace(/^[>"\s]+|[<"\s]+$/g, "").trim();
2857
+ }
2858
+ } catch {
2859
+ }
2860
+ }
2861
+ map.set(clean, {
2862
+ id: clean,
2863
+ name: clean,
2864
+ description: desc
2865
+ });
2866
+ }
2867
+ }
2868
+ } catch {
2869
+ }
2870
+ }
2871
+ if (map.size === 0) {
2872
+ const defaults = [
2873
+ "frontend",
2874
+ "query-performance",
2875
+ "erp-debug",
2876
+ "security-audit",
2877
+ "git-hygiene",
2878
+ "financial-expert",
2879
+ "rveta-core",
2880
+ "laravel-architecture"
2881
+ ];
2882
+ defaults.forEach((d) => map.set(d, { id: d, name: d }));
2883
+ }
2884
+ return Array.from(map.values()).sort((a, b) => a.name.localeCompare(b.name));
2885
+ }
2886
+ async sendSkillsMenu(page = 0) {
2887
+ const allSkills = this.getAllAvailableSkills();
2888
+ const pageSize = 8;
2889
+ const totalPages = Math.ceil(allSkills.length / pageSize) || 1;
2890
+ const safePage = Math.min(Math.max(0, page), totalPages - 1);
2891
+ const pool = allSkills.slice(safePage * pageSize, (safePage + 1) * pageSize);
2892
+ const rows = [];
2893
+ let currentRow = [];
2894
+ for (const sk of pool) {
2895
+ const isCurrent = sk.name === this.activeSkill;
2896
+ currentRow.push({
2897
+ text: `${isCurrent ? "\u25CF " : ""}\u{1F9F0} ${sk.name}`,
2898
+ callback_data: `sk:${sk.name}`
2899
+ });
2900
+ if (currentRow.length === 2) {
2901
+ rows.push(currentRow);
2902
+ currentRow = [];
2903
+ }
2904
+ }
2905
+ if (currentRow.length > 0) rows.push(currentRow);
2906
+ const pagRow = [];
2907
+ if (safePage > 0) {
2908
+ pagRow.push({ text: "\u2B05\uFE0F \u0627\u0644\u0633\u0627\u0628\u0642", callback_data: `skpage:${safePage - 1}` });
2909
+ }
2910
+ if (safePage < totalPages - 1) {
2911
+ pagRow.push({ text: "\u27A1\uFE0F \u0627\u0644\u0645\u0632\u064A\u062F \u0645\u0646 \u0627\u0644\u0645\u0647\u0627\u0631\u0627\u062A", callback_data: `skpage:${safePage + 1}` });
2912
+ }
2913
+ if (pagRow.length > 0) {
2914
+ rows.push(pagRow);
2915
+ }
2916
+ rows.push([{ text: t("btn_back", this.language), callback_data: "menu:home" }]);
2917
+ let cardText = this.language === "ar" ? `\u{1F9F0} <b>\u0645\u0647\u0627\u0631\u0627\u062A \u0648\u0643\u0644\u0627\u0621 \u0627\u0644\u0630\u0643\u0627\u0621 \u0627\u0644\u0627\u0635\u0637\u0646\u0627\u0639\u064A (Agent Skills)</b> (\u0635\u0641\u062D\u0629 ${safePage + 1} \u0645\u0646 ${totalPages} \xB7 \u0625\u062C\u0645\u0627\u0644\u064A ${allSkills.length} \u0645\u0647\u0627\u0631\u0629)
2918
+ \u0627\u0644\u0645\u0647\u0627\u0631\u0629 \u0627\u0644\u0645\u0641\u0639\u0644\u0629: <b>${escapeHtml(this.activeSkill || "\u0628\u062F\u0648\u0646 (\u0648\u0643\u064A\u0644 \u0639\u0627\u0645)")}</b>
2919
+
2920
+ ` : `\u{1F9F0} <b>Available Agent Skills</b> (Page ${safePage + 1} of ${totalPages} \xB7 Total: ${allSkills.length})
2921
+ Active: <b>${escapeHtml(this.activeSkill || "None (Standard Agent)")}</b>
2922
+
2923
+ `;
2924
+ for (const sk of pool) {
2925
+ const isCurrent = sk.name === this.activeSkill;
2926
+ const descSnippet = sk.description ? sk.description.slice(0, 60) + (sk.description.length > 60 ? "\u2026" : "") : "\u0645\u0647\u0627\u0631\u0629 \u0645\u062A\u062E\u0635\u0635\u0629";
2927
+ cardText += `${isCurrent ? "\u25CF" : "\u2022"} <b>${escapeHtml(sk.name)}</b>
2928
+ `;
2929
+ cardText += ` <code>${escapeHtml(descSnippet)}</code>
2930
+ `;
2931
+ }
2932
+ cardText += this.language === "ar" ? `
2933
+ <i>\u0627\u062E\u062A\u0631 \u0627\u0644\u0645\u0647\u0627\u0631\u0629 \u0644\u062A\u0648\u062C\u064A\u0647 \u0627\u0644\u0648\u0643\u064A\u0644 \u0627\u0644\u0628\u0631\u0645\u062C\u064A \u0644\u0644\u0639\u0645\u0644 \u0628\u0647\u0627 \u0641\u0648\u0631\u0627\u064B:</i>` : `
2934
+ <i>Select a skill to prime the agent for execution:</i>`;
2935
+ await this.channel.sendMessage(
2936
+ this.allowedChatId,
2937
+ cardText.trim(),
2938
+ { replyMarkup: this.wrapKeyboard(rows) }
2939
+ );
2940
+ }
2941
+ async sendUsageReport() {
2942
+ const cfg = this.cfgManager.config;
2943
+ const text = `
2944
+ \u{1F4C8} <b>Resource Usage & Quotas</b>
2945
+ \u2022 Daily Limit: <b>${cfg.dailyLimit} tasks</b>
2946
+ \u2022 Task Timeout: <b>${Math.round(cfg.runTimeout / 60)} minutes</b>
2947
+ \u2022 Workspace ID: <code>${escapeHtml(this.cfgManager.workspaceId)}</code>
2948
+ \u2022 Platform: <b>${os2.platform()} (${os2.arch()})</b>
2949
+ `.trim();
2950
+ await this.channel.sendMessage(this.allowedChatId, text, {
2951
+ replyMarkup: this.getMainKeyboard()
2952
+ });
2953
+ }
2954
+ async sendLanguageMenu() {
2955
+ const cur = this.language;
2956
+ const rows = [
2957
+ [{ text: `${cur === "en" ? "\u25CF " : ""}\u{1F1FA}\u{1F1F8} English`, callback_data: "lang:en" }],
2958
+ [{ text: `${cur === "ar" ? "\u25CF " : ""}\u{1F1F8}\u{1F1E6} \u0627\u0644\u0639\u0631\u0628\u064A\u0629`, callback_data: "lang:ar" }],
2959
+ [{ text: t("btn_back", this.language), callback_data: "menu:home" }]
2960
+ ];
2961
+ await this.channel.sendMessage(
2962
+ this.allowedChatId,
2963
+ t("select_language", this.language),
2964
+ { replyMarkup: this.wrapKeyboard(rows) }
2965
+ );
2966
+ }
2967
+ async sendHandoffReport() {
2968
+ let branch = "unknown";
2969
+ let dirtyLines = [];
2970
+ let commits = [];
2971
+ try {
2972
+ const b = child_process3.spawnSync("git", ["branch", "--show-current"], { cwd: this.targetDir, encoding: "utf-8", timeout: 5e3 });
2973
+ if (b.status === 0) branch = b.stdout.trim() || "main";
2974
+ const s = child_process3.spawnSync("git", ["status", "--porcelain"], { cwd: this.targetDir, encoding: "utf-8", timeout: 5e3 });
2975
+ if (s.status === 0) dirtyLines = s.stdout.trim().split("\n").filter(Boolean);
2976
+ const c = child_process3.spawnSync("git", ["log", "-5", "--oneline"], { cwd: this.targetDir, encoding: "utf-8", timeout: 5e3 });
2977
+ if (c.status === 0) commits = c.stdout.trim().split("\n").filter(Boolean);
2978
+ } catch {
2979
+ }
2980
+ const dirtyPreview = dirtyLines.slice(0, 10).map((l) => ` ${l}`).join("\n") || " (Clean working tree)";
2981
+ const commitsPreview = commits.map((c) => ` ${c}`).join("\n") || " (No commits)";
2982
+ const text = `
2983
+ <b>HANDOFF \u2014 Resume task in another tool</b>
2984
+ Repo: <code>${escapeHtml(this.targetDir)}</code> (Branch: <code>${escapeHtml(branch)}</code>)
2985
+ Uncommitted files: <b>${dirtyLines.length}</b>
2986
+ <pre><code>${escapeHtml(dirtyPreview)}</code></pre>
2987
+ Last commits:
2988
+ <pre><code>${escapeHtml(commitsPreview)}</code></pre>
2989
+ Agent: <b>${escapeHtml(this.currentAgent)}</b> | Mode: <b>${escapeHtml(this.currentMode)}</b>
2990
+ `.trim();
2991
+ await this.channel.sendMessage(this.allowedChatId, text, {
2992
+ replyMarkup: this.getMainKeyboard()
2993
+ });
2994
+ }
2126
2995
  async sendWorkspaceMenu() {
2127
2996
  const workspaces = this.cfgManager.config.workspaces;
2128
2997
  const rows = workspaces.map((ws, i) => [
@@ -2131,16 +3000,11 @@ Workspace: \u{1F4C1} <code>${escapeHtml2(path3.basename(this.targetDir))}</code>
2131
3000
  callback_data: `ws:${i}`
2132
3001
  }
2133
3002
  ]);
2134
- const kb = {
2135
- rows: rows.map(
2136
- (r) => r.map((b) => ({ text: b.text, callbackData: b.callback_data, callback_data: b.callback_data }))
2137
- ),
2138
- inline_keyboard: rows
2139
- };
3003
+ rows.push([{ text: t("btn_back", this.language), callback_data: "menu:home" }]);
2140
3004
  await this.channel.sendMessage(
2141
3005
  this.allowedChatId,
2142
- t("select_workspace", this.language, { ws: escapeHtml2(this.targetDir) }),
2143
- { replyMarkup: kb }
3006
+ t("select_workspace", this.language, { ws: escapeHtml(this.targetDir) }),
3007
+ { replyMarkup: this.wrapKeyboard(rows) }
2144
3008
  );
2145
3009
  }
2146
3010
  async switchWorkspace(newPath) {
@@ -2148,14 +3012,14 @@ Workspace: \u{1F4C1} <code>${escapeHtml2(path3.basename(this.targetDir))}</code>
2148
3012
  if (!fs3.existsSync(resolved)) {
2149
3013
  await this.channel.sendMessage(
2150
3014
  this.allowedChatId,
2151
- t("ws_not_found", this.language, { path: escapeHtml2(resolved) })
3015
+ t("ws_not_found", this.language, { path: escapeHtml(resolved) })
2152
3016
  );
2153
3017
  return;
2154
3018
  }
2155
3019
  if (this.activeRun && this.activeRun.busy) {
2156
3020
  await this.channel.sendMessage(
2157
3021
  this.allowedChatId,
2158
- t("ws_busy", this.language, { ws: escapeHtml2(this.targetDir) })
3022
+ t("ws_busy", this.language, { ws: escapeHtml(this.targetDir) })
2159
3023
  );
2160
3024
  return;
2161
3025
  }
@@ -2170,8 +3034,8 @@ Workspace: \u{1F4C1} <code>${escapeHtml2(path3.basename(this.targetDir))}</code>
2170
3034
  await this.channel.sendMessage(
2171
3035
  this.allowedChatId,
2172
3036
  t("ws_changed", this.language, {
2173
- name: escapeHtml2(path3.basename(resolved)),
2174
- ws: escapeHtml2(resolved),
3037
+ name: escapeHtml(path3.basename(resolved)),
3038
+ ws: escapeHtml(resolved),
2175
3039
  branch: "main",
2176
3040
  dirty: "0"
2177
3041
  }),
@@ -2186,7 +3050,7 @@ Workspace: \u{1F4C1} <code>${escapeHtml2(path3.basename(this.targetDir))}</code>
2186
3050
  return;
2187
3051
  }
2188
3052
  const preview = diff.length <= 3500 ? diff : diff.slice(0, 3500) + "\n\u2026 (truncated)";
2189
- await this.channel.sendMessage(this.allowedChatId, `<pre><code>${escapeHtml2(preview)}</code></pre>`);
3053
+ await this.channel.sendMessage(this.allowedChatId, `<pre><code>${escapeHtml(preview)}</code></pre>`);
2190
3054
  } catch (err) {
2191
3055
  await this.channel.sendMessage(this.allowedChatId, `\u274C Diff error: ${err.message}`);
2192
3056
  }
@@ -2211,36 +3075,36 @@ function getEntryPoint() {
2211
3075
  if (fs4.existsSync(distEntry)) return distEntry;
2212
3076
  return path4.join(root, "bin", "agentgram.js");
2213
3077
  }
2214
- var os2, path4, fs4, import_node_child_process, MACOS_PLIST_NAME, LEGACY_MACOS_PLIST_NAME, MACOS_PLIST_DIR, MACOS_PLIST_PATH, LEGACY_MACOS_PLIST_PATH, LINUX_SERVICE_NAME, LEGACY_LINUX_SERVICE_NAME, LINUX_SERVICE_DIR, LINUX_SERVICE_PATH, LEGACY_LINUX_SERVICE_PATH, ServiceManager;
3078
+ var os3, path4, fs4, import_node_child_process, MACOS_PLIST_NAME, LEGACY_MACOS_PLIST_NAME, MACOS_PLIST_DIR, MACOS_PLIST_PATH, LEGACY_MACOS_PLIST_PATH, LINUX_SERVICE_NAME, LEGACY_LINUX_SERVICE_NAME, LINUX_SERVICE_DIR, LINUX_SERVICE_PATH, LEGACY_LINUX_SERVICE_PATH, ServiceManager;
2215
3079
  var init_service_manager = __esm({
2216
3080
  "src/service/service-manager.ts"() {
2217
3081
  "use strict";
2218
- os2 = __toESM(require("os"));
3082
+ os3 = __toESM(require("os"));
2219
3083
  path4 = __toESM(require("path"));
2220
3084
  fs4 = __toESM(require("fs"));
2221
3085
  import_node_child_process = require("child_process");
2222
3086
  MACOS_PLIST_NAME = "com.agentgram.plist";
2223
3087
  LEGACY_MACOS_PLIST_NAME = "com.rumuze.edit.plist";
2224
- MACOS_PLIST_DIR = path4.join(os2.homedir(), "Library", "LaunchAgents");
3088
+ MACOS_PLIST_DIR = path4.join(os3.homedir(), "Library", "LaunchAgents");
2225
3089
  MACOS_PLIST_PATH = path4.join(MACOS_PLIST_DIR, MACOS_PLIST_NAME);
2226
3090
  LEGACY_MACOS_PLIST_PATH = path4.join(MACOS_PLIST_DIR, LEGACY_MACOS_PLIST_NAME);
2227
3091
  LINUX_SERVICE_NAME = "agentgram.service";
2228
3092
  LEGACY_LINUX_SERVICE_NAME = "rumuze-edit.service";
2229
- LINUX_SERVICE_DIR = path4.join(os2.homedir(), ".config", "systemd", "user");
3093
+ LINUX_SERVICE_DIR = path4.join(os3.homedir(), ".config", "systemd", "user");
2230
3094
  LINUX_SERVICE_PATH = path4.join(LINUX_SERVICE_DIR, LINUX_SERVICE_NAME);
2231
3095
  LEGACY_LINUX_SERVICE_PATH = path4.join(LINUX_SERVICE_DIR, LEGACY_LINUX_SERVICE_NAME);
2232
3096
  ServiceManager = class {
2233
3097
  static install(targetDir) {
2234
- const platform3 = os2.platform();
3098
+ const platform4 = os3.platform();
2235
3099
  const nodeExec = process.execPath;
2236
3100
  const script = getEntryPoint();
2237
3101
  const resolvedTarget = path4.resolve(targetDir || process.cwd());
2238
- const logDir = path4.join(os2.homedir(), ".config", "agentgram");
3102
+ const logDir = path4.join(os3.homedir(), ".config", "agentgram");
2239
3103
  try {
2240
3104
  if (!fs4.existsSync(logDir)) fs4.mkdirSync(logDir, { recursive: true, mode: 448 });
2241
3105
  } catch {
2242
3106
  }
2243
- if (platform3 === "darwin") {
3107
+ if (platform4 === "darwin") {
2244
3108
  try {
2245
3109
  if (!fs4.existsSync(MACOS_PLIST_DIR)) {
2246
3110
  fs4.mkdirSync(MACOS_PLIST_DIR, { recursive: true });
@@ -2248,7 +3112,7 @@ var init_service_manager = __esm({
2248
3112
  (0, import_node_child_process.spawnSync)("launchctl", ["unload", MACOS_PLIST_PATH]);
2249
3113
  (0, import_node_child_process.spawnSync)("launchctl", ["unload", LEGACY_MACOS_PLIST_PATH]);
2250
3114
  let pathEnv = process.env.PATH || "/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin";
2251
- const opencodeBin = path4.join(os2.homedir(), ".opencode", "bin");
3115
+ const opencodeBin = path4.join(os3.homedir(), ".opencode", "bin");
2252
3116
  if (!pathEnv.includes(opencodeBin)) {
2253
3117
  pathEnv = `${opencodeBin}:${pathEnv}`;
2254
3118
  }
@@ -2299,11 +3163,16 @@ var init_service_manager = __esm({
2299
3163
  } catch (err) {
2300
3164
  return { success: false, message: err.message };
2301
3165
  }
2302
- } else if (platform3 === "linux") {
3166
+ } else if (platform4 === "linux") {
2303
3167
  try {
2304
3168
  if (!fs4.existsSync(LINUX_SERVICE_DIR)) {
2305
3169
  fs4.mkdirSync(LINUX_SERVICE_DIR, { recursive: true });
2306
3170
  }
3171
+ let pathEnv = process.env.PATH || "/usr/local/bin:/usr/bin:/bin";
3172
+ const opencodeBin = path4.join(os3.homedir(), ".opencode", "bin");
3173
+ if (!pathEnv.includes(opencodeBin)) {
3174
+ pathEnv = `${opencodeBin}:${pathEnv}`;
3175
+ }
2307
3176
  const unitContent = `[Unit]
2308
3177
  Description=AgentGram AI Telegram Mission Control
2309
3178
  After=network.target
@@ -2311,6 +3180,7 @@ After=network.target
2311
3180
  [Service]
2312
3181
  Type=simple
2313
3182
  WorkingDirectory=${resolvedTarget}
3183
+ Environment="PATH=${pathEnv}"
2314
3184
  ExecStart=${nodeExec} ${script} start --dir ${resolvedTarget}
2315
3185
  Restart=always
2316
3186
  RestartSec=5
@@ -2333,12 +3203,12 @@ WantedBy=default.target
2333
3203
  }
2334
3204
  return {
2335
3205
  success: false,
2336
- message: `Background service automation is not currently supported on OS: ${platform3}`
3206
+ message: `Background service automation is not currently supported on OS: ${platform4}`
2337
3207
  };
2338
3208
  }
2339
3209
  static uninstall() {
2340
- const platform3 = os2.platform();
2341
- if (platform3 === "darwin") {
3210
+ const platform4 = os3.platform();
3211
+ if (platform4 === "darwin") {
2342
3212
  for (const p of [MACOS_PLIST_PATH, LEGACY_MACOS_PLIST_PATH]) {
2343
3213
  if (fs4.existsSync(p)) {
2344
3214
  (0, import_node_child_process.spawnSync)("launchctl", ["unload", p]);
@@ -2349,11 +3219,12 @@ WantedBy=default.target
2349
3219
  }
2350
3220
  }
2351
3221
  return { success: true, message: "Uninstalled macOS launchd service." };
2352
- } else if (platform3 === "linux") {
2353
- for (const [name, p] of [
3222
+ } else if (platform4 === "linux") {
3223
+ const services = [
2354
3224
  [LINUX_SERVICE_NAME, LINUX_SERVICE_PATH],
2355
3225
  [LEGACY_LINUX_SERVICE_NAME, LEGACY_LINUX_SERVICE_PATH]
2356
- ]) {
3226
+ ];
3227
+ for (const [name, p] of services) {
2357
3228
  if (fs4.existsSync(p)) {
2358
3229
  (0, import_node_child_process.spawnSync)("systemctl", ["--user", "stop", name]);
2359
3230
  (0, import_node_child_process.spawnSync)("systemctl", ["--user", "disable", name]);
@@ -2366,11 +3237,11 @@ WantedBy=default.target
2366
3237
  (0, import_node_child_process.spawnSync)("systemctl", ["--user", "daemon-reload"]);
2367
3238
  return { success: true, message: "Uninstalled Linux systemd service." };
2368
3239
  }
2369
- return { success: false, message: `Unsupported OS: ${platform3}` };
3240
+ return { success: false, message: `Unsupported OS: ${platform4}` };
2370
3241
  }
2371
3242
  static status() {
2372
- const platform3 = os2.platform();
2373
- if (platform3 === "darwin") {
3243
+ const platform4 = os3.platform();
3244
+ if (platform4 === "darwin") {
2374
3245
  const res = (0, import_node_child_process.spawnSync)("launchctl", ["list"], { encoding: "utf-8" });
2375
3246
  const running = res.stdout && (res.stdout.includes("com.agentgram") || res.stdout.includes("com.rumuze.edit")) || false;
2376
3247
  const installed = fs4.existsSync(MACOS_PLIST_PATH) || fs4.existsSync(LEGACY_MACOS_PLIST_PATH);
@@ -2378,10 +3249,10 @@ WantedBy=default.target
2378
3249
  return {
2379
3250
  installed,
2380
3251
  running,
2381
- system: platform3,
3252
+ system: platform4,
2382
3253
  path: installed ? activePath : void 0
2383
3254
  };
2384
- } else if (platform3 === "linux") {
3255
+ } else if (platform4 === "linux") {
2385
3256
  const res = (0, import_node_child_process.spawnSync)("systemctl", ["--user", "is-active", LINUX_SERVICE_NAME], {
2386
3257
  encoding: "utf-8"
2387
3258
  });
@@ -2399,11 +3270,11 @@ WantedBy=default.target
2399
3270
  return {
2400
3271
  installed,
2401
3272
  running,
2402
- system: platform3,
3273
+ system: platform4,
2403
3274
  path: installed ? activePath : void 0
2404
3275
  };
2405
3276
  }
2406
- return { installed: false, running: false, system: platform3 };
3277
+ return { installed: false, running: false, system: platform4 };
2407
3278
  }
2408
3279
  };
2409
3280
  }
@@ -2459,9 +3330,9 @@ function getPidFilePath() {
2459
3330
  }
2460
3331
  function checkOpenCodeCli() {
2461
3332
  try {
2462
- const isWin = os3.platform() === "win32";
3333
+ const isWin = os4.platform() === "win32";
2463
3334
  const cmd = isWin ? "where" : "which";
2464
- const res = child_process3.spawnSync(cmd, ["opencode"], { encoding: "utf-8" });
3335
+ const res = child_process4.spawnSync(cmd, ["opencode"], { encoding: "utf-8" });
2465
3336
  return res.status === 0 && res.stdout.trim().length > 0;
2466
3337
  } catch {
2467
3338
  return false;
@@ -2690,7 +3561,7 @@ Please run ${C.bold}${C.cyan}agentgram init${C.reset} to set up your Telegram cr
2690
3561
  "--engine",
2691
3562
  selectedEngine
2692
3563
  ];
2693
- const child = child_process3.spawn(process.execPath, spawnArgs, {
3564
+ const child = child_process4.spawn(process.execPath, spawnArgs, {
2694
3565
  cwd: targetDir,
2695
3566
  detached: true,
2696
3567
  stdio: ["ignore", out, err],
@@ -2723,15 +3594,22 @@ Please run ${C.bold}${C.cyan}agentgram init${C.reset} to set up your Telegram cr
2723
3594
  let engineAdapter;
2724
3595
  let spawnedOc;
2725
3596
  if (selectedEngine === "opencode") {
2726
- try {
2727
- info("Spawning private OpenCode server on loopback...");
2728
- spawnedOc = await OpenCodeClient.spawnServer(targetDir);
2729
- engineAdapter = new OpenCodeAdapter(spawnedOc.client, targetDir);
2730
- success(`OpenCode server online (port: ${spawnedOc.port}).`);
2731
- } catch (err) {
2732
- error(`Could not spawn OpenCode server: ${err.message}`);
2733
- log(`Make sure OpenCode is installed: ${C.cyan}npm i -g opencode-ai${C.reset}`);
2734
- process.exit(1);
3597
+ const envUrl = process.env.OPENCODE_URL;
3598
+ if (envUrl) {
3599
+ info(`Connecting to configured OpenCode server at: ${C.cyan}${envUrl}${C.reset}`);
3600
+ const ocClient = new OpenCodeClient(envUrl, process.env.OPENCODE_SERVER_PASSWORD, targetDir);
3601
+ engineAdapter = new OpenCodeAdapter(ocClient, targetDir);
3602
+ } else {
3603
+ try {
3604
+ info("Spawning private OpenCode server on loopback...");
3605
+ spawnedOc = await OpenCodeClient.spawnServer(targetDir);
3606
+ engineAdapter = new OpenCodeAdapter(spawnedOc.client, targetDir);
3607
+ success(`OpenCode server online (port: ${spawnedOc.port}).`);
3608
+ } catch (err) {
3609
+ warn(`Could not spawn OpenCode server: ${err.message}`);
3610
+ warn("Resilience mode active: Falling back to Shell CLI Engine so Telegram bot connects immediately.");
3611
+ engineAdapter = new ShellEngineAdapter({ workspaceDir: targetDir, cliCommand: "sh" });
3612
+ }
2735
3613
  }
2736
3614
  } else if (selectedEngine === "shell" || selectedEngine === "sh" || selectedEngine === "bash") {
2737
3615
  engineAdapter = new ShellEngineAdapter({ workspaceDir: targetDir, cliCommand: "sh" });
@@ -2823,7 +3701,7 @@ function cmdStatus() {
2823
3701
  info(`System Service (${svc.system}): Not installed`);
2824
3702
  }
2825
3703
  try {
2826
- const ps = child_process3.spawnSync("pgrep", ["-fl", "opencode serve"], { encoding: "utf-8" });
3704
+ const ps = child_process4.spawnSync("pgrep", ["-fl", "opencode serve"], { encoding: "utf-8" });
2827
3705
  if (ps.status === 0 && ps.stdout.trim()) {
2828
3706
  success(`OpenCode Server: Active (${ps.stdout.trim().split("\n")[0]})`);
2829
3707
  } else {
@@ -2924,15 +3802,15 @@ async function runCli(argv = process.argv.slice(2)) {
2924
3802
  process.exit(1);
2925
3803
  }
2926
3804
  }
2927
- var fs5, os3, path5, readline2, child_process3, VERSION, C;
3805
+ var fs5, os4, path5, readline2, child_process4, VERSION, C;
2928
3806
  var init_cli = __esm({
2929
3807
  "src/cli/index.ts"() {
2930
3808
  "use strict";
2931
3809
  fs5 = __toESM(require("fs"));
2932
- os3 = __toESM(require("os"));
3810
+ os4 = __toESM(require("os"));
2933
3811
  path5 = __toESM(require("path"));
2934
3812
  readline2 = __toESM(require("readline"));
2935
- child_process3 = __toESM(require("child_process"));
3813
+ child_process4 = __toESM(require("child_process"));
2936
3814
  init_config_manager();
2937
3815
  init_telegram_client();
2938
3816
  init_opencode_client();
@@ -2944,7 +3822,7 @@ var init_cli = __esm({
2944
3822
  init_shell_adapter();
2945
3823
  init_registry2();
2946
3824
  init_service_manager();
2947
- VERSION = "2.0.0";
3825
+ VERSION = "2.0.4";
2948
3826
  C = {
2949
3827
  reset: "\x1B[0m",
2950
3828
  bold: "\x1B[1m",
@@ -2985,7 +3863,7 @@ __export(index_exports, {
2985
3863
  computeWorkspaceId: () => computeWorkspaceId,
2986
3864
  decidePermission: () => decidePermission,
2987
3865
  ensureConfigDirs: () => ensureConfigDirs,
2988
- escapeHtml: () => escapeHtml2,
3866
+ escapeHtml: () => escapeHtml,
2989
3867
  fromTelegramMarkup: () => fromTelegramMarkup,
2990
3868
  getEntryPoint: () => getEntryPoint,
2991
3869
  getFreePort: () => getFreePort,