nemoris 0.1.0 → 0.1.2

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 (248) hide show
  1. package/.env.example +49 -49
  2. package/LICENSE +21 -21
  3. package/README.md +209 -209
  4. package/SECURITY.md +59 -119
  5. package/bin/nemoris +46 -46
  6. package/config/agents/agent.toml.example +28 -28
  7. package/config/agents/content.toml +23 -0
  8. package/config/agents/default.toml +22 -22
  9. package/config/agents/heartbeat.toml +35 -0
  10. package/config/agents/iris.toml +23 -0
  11. package/config/agents/lab.toml +23 -0
  12. package/config/agents/main.toml +45 -0
  13. package/config/agents/nemo.toml +21 -0
  14. package/config/agents/ops.toml +38 -0
  15. package/config/agents/orchestrator.toml +18 -18
  16. package/config/agents/revenue.toml +23 -0
  17. package/config/agents/testyboo.toml +19 -0
  18. package/config/delivery.toml +73 -73
  19. package/config/embeddings.toml +5 -5
  20. package/config/identity/content-purpose.md +11 -0
  21. package/config/identity/content-soul.md +45 -0
  22. package/config/identity/default-purpose.md +1 -1
  23. package/config/identity/default-soul.md +3 -3
  24. package/config/identity/heartbeat-purpose.md +9 -0
  25. package/config/identity/heartbeat-soul.md +16 -0
  26. package/config/identity/iris-purpose.md +17 -0
  27. package/config/identity/iris-soul.md +68 -0
  28. package/config/identity/lab-purpose.md +10 -0
  29. package/config/identity/lab-soul.md +38 -0
  30. package/config/identity/main-purpose.md +17 -0
  31. package/config/identity/main-soul.md +66 -0
  32. package/config/identity/main-user.md +22 -0
  33. package/config/identity/ops-purpose.md +9 -0
  34. package/config/identity/ops-soul.md +16 -0
  35. package/config/identity/orchestrator-purpose.md +1 -1
  36. package/config/identity/orchestrator-soul.md +1 -1
  37. package/config/identity/revenue-purpose.md +9 -0
  38. package/config/identity/revenue-soul.md +41 -0
  39. package/config/identity/testyboo-purpose.md +13 -0
  40. package/config/identity/testyboo-soul.md +20 -0
  41. package/config/improvement-targets.toml +15 -15
  42. package/config/jobs/heartbeat-check.toml +30 -30
  43. package/config/jobs/memory-rollup.toml +46 -46
  44. package/config/jobs/workspace-health.toml +63 -63
  45. package/config/mcp.toml +16 -16
  46. package/config/output-contracts.toml +17 -17
  47. package/config/peers.toml +32 -32
  48. package/config/peers.toml.example +32 -32
  49. package/config/policies/memory-default.toml +10 -10
  50. package/config/policies/memory-heartbeat.toml +5 -5
  51. package/config/policies/memory-ops.toml +10 -10
  52. package/config/policies/tools-heartbeat-minimal.toml +8 -8
  53. package/config/policies/tools-interactive-safe.toml +8 -8
  54. package/config/policies/tools-ops-bounded.toml +8 -8
  55. package/config/policies/tools-orchestrator.toml +7 -7
  56. package/config/providers/anthropic.toml +15 -15
  57. package/config/providers/ollama.toml +5 -5
  58. package/config/providers/openai-codex.toml +9 -9
  59. package/config/providers/openrouter.toml +5 -5
  60. package/config/router.toml +22 -22
  61. package/config/runtime.toml +114 -114
  62. package/config/skills/self-improvement.toml +15 -15
  63. package/config/skills/telegram-onboarding-spec.md +240 -240
  64. package/config/skills/workspace-monitor.toml +15 -15
  65. package/config/task-router.toml +42 -42
  66. package/install.sh +50 -50
  67. package/package.json +91 -90
  68. package/src/auth/auth-profiles.js +169 -169
  69. package/src/auth/openai-codex-oauth.js +285 -285
  70. package/src/battle.js +449 -449
  71. package/src/cli/help.js +265 -265
  72. package/src/cli/output-filter.js +49 -49
  73. package/src/cli/runtime-control.js +704 -704
  74. package/src/cli-main.js +2763 -2763
  75. package/src/cli.js +78 -78
  76. package/src/config/loader.js +332 -332
  77. package/src/config/schema-validator.js +214 -214
  78. package/src/config/toml-lite.js +8 -8
  79. package/src/daemon/action-handlers.js +71 -71
  80. package/src/daemon/healing-tick.js +87 -87
  81. package/src/daemon/health-probes.js +90 -90
  82. package/src/daemon/notifier.js +57 -57
  83. package/src/daemon/nurse.js +218 -218
  84. package/src/daemon/repair-log.js +106 -106
  85. package/src/daemon/rule-staging.js +90 -90
  86. package/src/daemon/rules.js +29 -29
  87. package/src/daemon/telegram-commands.js +54 -54
  88. package/src/daemon/updater.js +85 -85
  89. package/src/jobs/job-runner.js +78 -78
  90. package/src/mcp/consumer.js +129 -129
  91. package/src/memory/active-recall.js +171 -171
  92. package/src/memory/backend-manager.js +97 -97
  93. package/src/memory/backends/file-backend.js +38 -38
  94. package/src/memory/backends/qmd-backend.js +219 -219
  95. package/src/memory/embedding-guards.js +24 -24
  96. package/src/memory/embedding-index.js +118 -118
  97. package/src/memory/embedding-service.js +179 -179
  98. package/src/memory/file-index.js +177 -177
  99. package/src/memory/memory-signature.js +5 -5
  100. package/src/memory/memory-store.js +648 -648
  101. package/src/memory/retrieval-planner.js +66 -66
  102. package/src/memory/scoring.js +145 -145
  103. package/src/memory/simhash.js +78 -78
  104. package/src/memory/sqlite-active-store.js +824 -824
  105. package/src/memory/write-policy.js +36 -36
  106. package/src/onboarding/aliases.js +33 -33
  107. package/src/onboarding/auth/api-key.js +224 -224
  108. package/src/onboarding/auth/ollama-detect.js +42 -42
  109. package/src/onboarding/clack-prompter.js +77 -77
  110. package/src/onboarding/doctor.js +530 -530
  111. package/src/onboarding/lock.js +42 -42
  112. package/src/onboarding/model-catalog.js +344 -344
  113. package/src/onboarding/phases/auth.js +576 -589
  114. package/src/onboarding/phases/build.js +130 -130
  115. package/src/onboarding/phases/choose.js +82 -82
  116. package/src/onboarding/phases/detect.js +98 -98
  117. package/src/onboarding/phases/hatch.js +216 -216
  118. package/src/onboarding/phases/identity.js +79 -79
  119. package/src/onboarding/phases/ollama.js +345 -345
  120. package/src/onboarding/phases/scaffold.js +99 -99
  121. package/src/onboarding/phases/telegram.js +377 -377
  122. package/src/onboarding/phases/validate.js +204 -204
  123. package/src/onboarding/phases/verify.js +206 -206
  124. package/src/onboarding/platform.js +482 -482
  125. package/src/onboarding/status-bar.js +95 -95
  126. package/src/onboarding/templates.js +794 -794
  127. package/src/onboarding/toml-writer.js +38 -38
  128. package/src/onboarding/tui.js +250 -250
  129. package/src/onboarding/uninstall.js +153 -153
  130. package/src/onboarding/wizard.js +516 -499
  131. package/src/providers/anthropic.js +168 -168
  132. package/src/providers/base.js +247 -247
  133. package/src/providers/circuit-breaker.js +136 -136
  134. package/src/providers/ollama.js +163 -163
  135. package/src/providers/openai-codex.js +149 -149
  136. package/src/providers/openrouter.js +136 -136
  137. package/src/providers/registry.js +36 -36
  138. package/src/providers/router.js +16 -16
  139. package/src/runtime/bootstrap-cache.js +47 -47
  140. package/src/runtime/capabilities-prompt.js +25 -25
  141. package/src/runtime/completion-ping.js +99 -99
  142. package/src/runtime/config-validator.js +121 -121
  143. package/src/runtime/context-ledger.js +360 -360
  144. package/src/runtime/cutover-readiness.js +42 -42
  145. package/src/runtime/daemon.js +729 -729
  146. package/src/runtime/delivery-ack.js +195 -195
  147. package/src/runtime/delivery-adapters/local-file.js +41 -41
  148. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -94
  149. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -98
  150. package/src/runtime/delivery-adapters/shadow.js +13 -13
  151. package/src/runtime/delivery-adapters/standalone-http.js +98 -98
  152. package/src/runtime/delivery-adapters/telegram.js +104 -104
  153. package/src/runtime/delivery-adapters/tui.js +128 -128
  154. package/src/runtime/delivery-manager.js +807 -807
  155. package/src/runtime/delivery-store.js +168 -168
  156. package/src/runtime/dependency-health.js +118 -118
  157. package/src/runtime/envelope.js +114 -114
  158. package/src/runtime/evaluation.js +1089 -1089
  159. package/src/runtime/exec-approvals.js +216 -216
  160. package/src/runtime/executor.js +500 -500
  161. package/src/runtime/failure-ping.js +67 -67
  162. package/src/runtime/flows.js +83 -83
  163. package/src/runtime/guards.js +45 -45
  164. package/src/runtime/handoff.js +51 -51
  165. package/src/runtime/identity-cache.js +28 -28
  166. package/src/runtime/improvement-engine.js +109 -109
  167. package/src/runtime/improvement-harness.js +581 -581
  168. package/src/runtime/input-sanitiser.js +72 -72
  169. package/src/runtime/interaction-contract.js +347 -347
  170. package/src/runtime/lane-readiness.js +226 -226
  171. package/src/runtime/migration.js +323 -323
  172. package/src/runtime/model-resolution.js +78 -78
  173. package/src/runtime/network.js +64 -64
  174. package/src/runtime/notification-store.js +97 -97
  175. package/src/runtime/notifier.js +256 -256
  176. package/src/runtime/orchestrator.js +53 -53
  177. package/src/runtime/orphan-reaper.js +41 -41
  178. package/src/runtime/output-contract-schema.js +139 -139
  179. package/src/runtime/output-contract-validator.js +439 -439
  180. package/src/runtime/peer-readiness.js +69 -69
  181. package/src/runtime/peer-registry.js +133 -133
  182. package/src/runtime/pilot-status.js +108 -108
  183. package/src/runtime/prompt-builder.js +261 -261
  184. package/src/runtime/provider-attempt.js +582 -582
  185. package/src/runtime/report-fallback.js +71 -71
  186. package/src/runtime/result-normalizer.js +183 -183
  187. package/src/runtime/retention.js +74 -74
  188. package/src/runtime/review.js +244 -244
  189. package/src/runtime/route-job.js +15 -15
  190. package/src/runtime/run-store.js +38 -38
  191. package/src/runtime/schedule.js +88 -88
  192. package/src/runtime/scheduler-state.js +434 -434
  193. package/src/runtime/scheduler.js +656 -656
  194. package/src/runtime/session-compactor.js +182 -182
  195. package/src/runtime/session-search.js +155 -155
  196. package/src/runtime/slack-inbound.js +249 -249
  197. package/src/runtime/ssrf.js +102 -102
  198. package/src/runtime/status-aggregator.js +330 -330
  199. package/src/runtime/task-contract.js +140 -140
  200. package/src/runtime/task-packet.js +107 -107
  201. package/src/runtime/task-router.js +140 -140
  202. package/src/runtime/telegram-inbound.js +1565 -1565
  203. package/src/runtime/token-counter.js +134 -134
  204. package/src/runtime/token-estimator.js +59 -59
  205. package/src/runtime/tool-loop.js +200 -200
  206. package/src/runtime/transport-server.js +311 -311
  207. package/src/runtime/tui-server.js +411 -411
  208. package/src/runtime/ulid.js +44 -44
  209. package/src/security/ssrf-check.js +197 -197
  210. package/src/setup.js +369 -369
  211. package/src/shadow/bridge.js +303 -303
  212. package/src/skills/loader.js +84 -84
  213. package/src/tools/catalog.json +49 -49
  214. package/src/tools/cli-delegate.js +44 -44
  215. package/src/tools/mcp-client.js +106 -106
  216. package/src/tools/micro/cancel-task.js +6 -6
  217. package/src/tools/micro/complete-task.js +6 -6
  218. package/src/tools/micro/fail-task.js +6 -6
  219. package/src/tools/micro/http-fetch.js +74 -74
  220. package/src/tools/micro/index.js +36 -36
  221. package/src/tools/micro/lcm-recall.js +60 -60
  222. package/src/tools/micro/list-dir.js +17 -17
  223. package/src/tools/micro/list-skills.js +46 -46
  224. package/src/tools/micro/load-skill.js +38 -38
  225. package/src/tools/micro/memory-search.js +45 -45
  226. package/src/tools/micro/read-file.js +11 -11
  227. package/src/tools/micro/session-search.js +54 -54
  228. package/src/tools/micro/shell-exec.js +43 -43
  229. package/src/tools/micro/trigger-job.js +79 -79
  230. package/src/tools/micro/web-search.js +58 -58
  231. package/src/tools/micro/workspace-paths.js +39 -39
  232. package/src/tools/micro/write-file.js +14 -14
  233. package/src/tools/micro/write-memory.js +41 -41
  234. package/src/tools/registry.js +348 -348
  235. package/src/tools/tool-result-contract.js +36 -36
  236. package/src/tui/chat.js +835 -835
  237. package/src/tui/renderer.js +175 -175
  238. package/src/tui/socket-client.js +217 -217
  239. package/src/utils/canonical-json.js +29 -29
  240. package/src/utils/compaction.js +30 -30
  241. package/src/utils/env-loader.js +5 -5
  242. package/src/utils/errors.js +80 -80
  243. package/src/utils/fs.js +101 -101
  244. package/src/utils/ids.js +5 -5
  245. package/src/utils/model-context-limits.js +30 -30
  246. package/src/utils/token-budget.js +74 -74
  247. package/src/utils/usage-cost.js +25 -25
  248. package/src/utils/usage-metrics.js +14 -14
@@ -1,377 +1,377 @@
1
- /**
2
- * Telegram onboarding sub-phase — optional, runs inside Build (Phase 3).
3
- *
4
- * Steps: token validation → chat_id discovery (daemon-aware) → mode selection
5
- * → config write → smoke test with failure handling.
6
- */
7
-
8
- import fs from "node:fs";
9
- import path from "node:path";
10
- import { createRequire } from "node:module";
11
- import { getMe, whoami } from "../../runtime/telegram-inbound.js";
12
- import { writeEnvFile } from "../auth/api-key.js";
13
- import { isDaemonRunning, setDaemonEnv } from "../platform.js";
14
- import { writeTomlSection } from "../toml-writer.js";
15
- import {
16
- confirm, prompt, promptSecret, select, waitForEnter,
17
- bold, green, red, yellow, dim, cyan, progressLine,
18
- } from "../tui.js";
19
-
20
- export const TELEGRAM_START_INSTRUCTION =
21
- "Before continuing, open Telegram, search for your new bot, and send it /start — otherwise it won't be able to receive messages.";
22
-
23
- // ── Helpers ──────────────────────────────────────────────────────
24
-
25
- export function stripAnsi(str) {
26
- // eslint-disable-next-line no-control-regex
27
- return str.replace(/\x1b\[[0-9;]*m/g, "");
28
- }
29
-
30
- export function buildTelegramToml({ botTokenEnv, pollingMode, webhookUrl, operatorChatId, authorizedChatIds, defaultAgent }) {
31
- const authIds = (authorizedChatIds || []).map((id) => `"${id}"`).join(", ");
32
- return [
33
- "",
34
- "[telegram]",
35
- `bot_token_env = "${botTokenEnv}"`,
36
- `polling_mode = ${pollingMode}`,
37
- `webhook_url = "${webhookUrl || ""}"`,
38
- `operator_chat_id = "${operatorChatId || ""}"`,
39
- `authorized_chat_ids = [${authIds}]`,
40
- `default_agent = "${defaultAgent}"`,
41
- "",
42
- ].join("\n");
43
- }
44
-
45
- export function patchRuntimeToml(installDir, telegramToml) {
46
- const runtimePath = path.join(installDir, "config", "runtime.toml");
47
- writeTomlSection(runtimePath, "telegram", telegramToml);
48
- }
49
-
50
- async function sendTestMessage(token, chatId, text, { fetchImpl = globalThis.fetch } = {}) {
51
- try {
52
- const url = `https://api.telegram.org/bot${token}/sendMessage`;
53
- const res = await fetchImpl(url, {
54
- method: "POST",
55
- headers: { "content-type": "application/json" },
56
- body: JSON.stringify({ chat_id: chatId, text }),
57
- });
58
- const body = await res.json();
59
- if (!body.ok) return { ok: false, error: `${res.status}: ${body.description || "unknown error"}` };
60
- return { ok: true };
61
- } catch (error) {
62
- return { ok: false, error: error.message };
63
- }
64
- }
65
-
66
- /**
67
- * Discover chat_id when the daemon is running — polls active.db for a
68
- * telegram interactive job created after `since` (ISO string).
69
- */
70
- function discoverChatIdFromStore(installDir, since, { timeoutMs = 120_000, intervalMs = 2000 } = {}) {
71
- const dbPath = path.join(installDir, "state", "active.db");
72
- return new Promise((resolve) => {
73
- const deadline = Date.now() + timeoutMs;
74
-
75
- const tick = () => {
76
- try {
77
- // Lazy-require to avoid hard dep on better-sqlite3 at module level
78
- const require = createRequire(import.meta.url);
79
- const Database = require("better-sqlite3");
80
- const db = new Database(dbPath, { readonly: true, fileMustExist: true });
81
- const row = db.prepare(
82
- "SELECT chat_id FROM interactive_jobs WHERE source = 'telegram' AND created_at > ? ORDER BY created_at DESC LIMIT 1"
83
- ).get(since);
84
- db.close();
85
- if (row?.chat_id) return resolve(row.chat_id);
86
- } catch {
87
- // DB may not exist yet or table missing — keep polling
88
- }
89
-
90
- if (Date.now() >= deadline) return resolve(null);
91
- setTimeout(tick, intervalMs);
92
- };
93
- tick();
94
- });
95
- }
96
-
97
- // ── Main Phase ───────────────────────────────────────────────────
98
-
99
- /**
100
- * Run the Telegram onboarding sub-phase (interactive).
101
- *
102
- * @param {{ installDir: string, agentId: string, nonInteractive?: boolean, skipGate?: boolean }} options
103
- * @returns {Promise<{ configured: boolean, verified: boolean, botUsername?: string }>}
104
- */
105
- export async function runTelegramPhase({ installDir, agentId, nonInteractive = false, skipGate = false, tui = {} }) {
106
- const botTokenEnv = "NEMORIS_TELEGRAM_BOT_TOKEN";
107
- const confirmImpl = tui.confirm || confirm;
108
- const promptImpl = tui.prompt || prompt;
109
- const promptSecretImpl = tui.promptSecret || promptSecret;
110
- const selectImpl = tui.select || select;
111
- const waitForEnterImpl = tui.waitForEnter || waitForEnter;
112
- const redImpl = tui.red || red;
113
- const yellowImpl = tui.yellow || yellow;
114
- const dimImpl = tui.dim || dim;
115
- const cyanImpl = tui.cyan || cyan;
116
- const progressLineImpl = tui.progressLine || progressLine;
117
-
118
- // ── Non-interactive mode ───────────────────────────────────────
119
- if (nonInteractive) {
120
- const token = process.env[botTokenEnv];
121
- if (!token || process.env.NEMORIS_SKIP_TELEGRAM === "true") {
122
- return { configured: false, verified: false };
123
- }
124
-
125
- const me = await getMe(token);
126
- if (!me.ok) return { configured: false, verified: false };
127
-
128
- const chatId = process.env.NEMORIS_TELEGRAM_CHAT_ID || "";
129
- const mode = process.env.NEMORIS_TELEGRAM_MODE || "polling";
130
- const webhookUrl = process.env.NEMORIS_TELEGRAM_WEBHOOK_URL || "";
131
-
132
- // Security: empty authorized_chat_ids with no chat_id means any sender is accepted.
133
- // Require NEMORIS_ALLOW_ANY_CHAT=true to opt in to this open configuration.
134
- if (!chatId && process.env.NEMORIS_ALLOW_ANY_CHAT !== "true") {
135
- console.error("NEMORIS_TELEGRAM_CHAT_ID not set. Set it, or set NEMORIS_ALLOW_ANY_CHAT=true to allow any sender.");
136
- return { configured: false, verified: false };
137
- }
138
-
139
- const authorizedChatIds = chatId ? [chatId] : [];
140
-
141
- writeEnvFile(installDir, { [botTokenEnv]: token });
142
- patchRuntimeToml(installDir, buildTelegramToml({
143
- botTokenEnv, pollingMode: mode === "polling", webhookUrl,
144
- operatorChatId: chatId, authorizedChatIds, defaultAgent: agentId,
145
- }));
146
-
147
- return { configured: true, verified: false, botUsername: me.username, botToken: token, operatorChatId: chatId };
148
- }
149
-
150
- // ── Interactive mode ───────────────────────────────────────────
151
-
152
- // Gate
153
- if (!skipGate) {
154
- const gate = await confirmImpl(`Connect via ${cyanImpl("Telegram")}? This lets you message your agent from your phone.`, true);
155
- if (!gate) {
156
- return { configured: false, verified: false };
157
- }
158
- }
159
-
160
- // Step 1: Bot token
161
- let token = null;
162
- let botUsername = null;
163
-
164
- console.log(`\n ${dimImpl("Need a bot token? Open Telegram → search @BotFather → send /newbot → follow the steps.")}`);
165
- console.log(` ${dimImpl("After creating your bot, open it in Telegram and tap Start before coming back here.")}`);
166
- console.log(` ${dimImpl("Your input will be hidden as you type — just paste and press Enter.")}\n`);
167
-
168
- while (!token) {
169
- const raw = await promptSecretImpl(`Your bot token`);
170
- if (!raw) {
171
- console.log(` ${dimImpl("Skipping Telegram — you can add it later with `nemoris setup telegram`.")}`);
172
- return { configured: false, verified: false };
173
- }
174
-
175
- const me = await getMe(raw);
176
- if (me.ok) {
177
- token = raw;
178
- botUsername = me.username;
179
- console.log(progressLineImpl("Token validated", `@${me.username}`));
180
- console.log(` ${dimImpl(TELEGRAM_START_INSTRUCTION)}`);
181
- } else {
182
- console.log(` ${redImpl("✗")} That token didn't work — double-check it in BotFather and try again.`);
183
- console.log(` ${dimImpl("Try again or press Enter to skip.")}`);
184
- }
185
- }
186
-
187
- // Write token to .env + daemon env
188
- writeEnvFile(installDir, { [botTokenEnv]: token });
189
- await setDaemonEnv(botTokenEnv, token);
190
-
191
- // Step 2: Chat ID discovery
192
- let chatId = null;
193
- const daemonRunning = isDaemonRunning();
194
-
195
- if (daemonRunning) {
196
- // Path A: daemon is running — read from state store
197
- console.log(`\n Open Telegram, find ${cyanImpl(`@${botUsername}`)}, send it any message — then press Enter.`);
198
- const since = new Date().toISOString();
199
- chatId = await discoverChatIdFromStore(installDir, since);
200
-
201
- if (chatId) {
202
- console.log(progressLineImpl("Found you", `chat_id: ${chatId}`));
203
- } else {
204
- console.log(` ${yellowImpl("!")} Timed out waiting for a message. Try opening Telegram and sending a message to @${botUsername} first.`);
205
- const manual = await promptImpl("Enter chat_id manually (or press Enter to skip)", "");
206
- chatId = manual || null;
207
- }
208
- } else {
209
- // Path B: daemon not running — use whoami probe
210
- await waitForEnterImpl(`Open Telegram, find ${cyanImpl(`@${botUsername}`)}, send it a message — anything works. Then come back and press Enter.`);
211
- const result = await whoami(token);
212
- if (result) {
213
- chatId = String(result.chatId);
214
- const label = result.username ? `chat_id: ${chatId}, @${result.username}` : `chat_id: ${chatId}`;
215
- console.log(progressLineImpl("Found you", label));
216
- } else {
217
- console.log(` ${yellowImpl("!")} No messages found.`);
218
- console.log(` ${dimImpl("The bot may not have received your message yet.")}`);
219
- const manual = await promptImpl("Enter chat_id manually (or press Enter to skip)", "");
220
- chatId = manual || null;
221
- }
222
- }
223
-
224
- const authorizedChatIds = chatId ? [chatId] : [];
225
-
226
- // Step 3: Delivery mode
227
- const mode = await selectImpl("Delivery mode:", [
228
- { label: `Long polling ${dimImpl("(recommended — no tunnel needed)")}`, value: "polling" },
229
- { label: `Webhook — for servers with a public address (advanced)`, value: "webhook" },
230
- ]);
231
-
232
- let webhookUrl = "";
233
- if (mode === "webhook") {
234
- webhookUrl = await promptImpl("Public webhook URL", "");
235
- if (webhookUrl) {
236
- const { registerWebhook } = await import("../../runtime/telegram-inbound.js");
237
- const reg = await registerWebhook(token, webhookUrl);
238
- if (reg.ok) {
239
- console.log(progressLineImpl("Webhook registered", `${webhookUrl}/telegram/webhook`));
240
- } else {
241
- console.log(` ${redImpl("✗")} Webhook registration failed — make sure the URL is publicly reachable.`);
242
- console.log(` ${dimImpl("Falling back to polling mode.")}`);
243
- webhookUrl = "";
244
- }
245
- }
246
- }
247
-
248
- const pollingMode = !webhookUrl;
249
-
250
- // Step 4: Write config
251
- patchRuntimeToml(installDir, buildTelegramToml({
252
- botTokenEnv, pollingMode, webhookUrl,
253
- operatorChatId: chatId || "", authorizedChatIds, defaultAgent: agentId,
254
- }));
255
-
256
- // Store chat_id in state for later phases (e.g. hatch)
257
- const result = { configured: true, verified: false, botUsername, botToken: token, operatorChatId: chatId || "" };
258
-
259
- console.log(progressLineImpl("Telegram configured", webhookUrl ? "webhook" : "long-poll"));
260
-
261
- // Step 5: Smoke test (with failure handling)
262
- if (!chatId) {
263
- console.log(` ${yellowImpl("!")} No chat ID — skipping test message.`);
264
- return result;
265
- }
266
-
267
- let verified = false;
268
- let smokeAttempt = true;
269
-
270
- while (smokeAttempt) {
271
- console.log(progressLineImpl("Sending test message", chatId));
272
- const smokeResult = await sendTestMessage(token, chatId, "Nemoris is running. Send me a message and I'll get to work.");
273
-
274
- if (smokeResult.ok) {
275
- console.log(progressLineImpl("Telegram connected", `message delivered to ${chatId}`));
276
- verified = true;
277
- smokeAttempt = false;
278
- } else {
279
- console.log(` ${redImpl("✗")} Delivery failed: ${smokeResult.error}\n`);
280
- const choice = await selectImpl("What next?", [
281
- { label: "Retry", value: "retry" },
282
- { label: `Skip — finish setup without verification`, value: "skip" },
283
- { label: "Re-enter bot token", value: "retoken" },
284
- ]);
285
-
286
- if (choice === "retry") continue;
287
- if (choice === "skip") {
288
- smokeAttempt = false;
289
- }
290
- if (choice === "retoken") {
291
- // Recursive — restart from Step 1
292
- return runTelegramPhase({ installDir, agentId, nonInteractive, skipGate: true });
293
- }
294
- }
295
- }
296
-
297
- return { ...result, verified };
298
- }
299
-
300
- /**
301
- * Send the agent's "hatch" moment message to Telegram.
302
- *
303
- * When providerConfig and identity content are supplied, generates a
304
- * personalised AI greeting via the provider stack. Falls back to the
305
- * static greeting on provider failure or when no config is available.
306
- *
307
- * @param {{ token: string, chatId: string, agentName?: string, userName?: string, installDir?: string, agentId?: string, userGoal?: string, soulContent?: string, purposeContent?: string, providerConfig?: object, fetchImpl?: Function }} opts
308
- * @returns {Promise<boolean>}
309
- */
310
- export async function sendHatchMessage({
311
- token,
312
- chatId,
313
- agentName,
314
- userName,
315
- installDir = null,
316
- agentId = null,
317
- userGoal = "",
318
- soulContent = "",
319
- purposeContent = "",
320
- providerConfig = null,
321
- migrated = false,
322
- fetchImpl = globalThis.fetch,
323
- }) {
324
- const name = stripAnsi(userName || "there");
325
- const agent = stripAnsi(agentName || "your agent");
326
-
327
- // Attempt provider-generated greeting
328
- let text = null;
329
- if (providerConfig) {
330
- try {
331
- const { generateGreeting } = await import("./hatch.js");
332
- const result = await generateGreeting({
333
- agentName: agent,
334
- userName: name,
335
- userGoal,
336
- soulContent,
337
- purposeContent,
338
- providerConfig,
339
- migrated,
340
- });
341
- if (result.fromProvider) {
342
- text = result.greeting;
343
- }
344
- } catch {
345
- // Fall through to static greeting
346
- }
347
- }
348
-
349
- // Static fallback — tone matches migrated vs fresh
350
- if (!text) {
351
- const { staticGreeting } = await import("./hatch.js");
352
- text = staticGreeting(agent, name, migrated);
353
- }
354
-
355
- // Write birth fact to memory (best-effort, non-blocking)
356
- if (installDir && agentId) {
357
- try {
358
- const { writeBirthFact } = await import("./hatch.js");
359
- await writeBirthFact({ installDir, agentId, agentName: agent, userName: name, migrated });
360
- } catch {
361
- // Non-fatal — don't fail the Telegram send
362
- }
363
- }
364
-
365
- // Send to Telegram
366
- const url = `https://api.telegram.org/bot${token}/sendMessage`;
367
- try {
368
- const res = await fetchImpl(url, {
369
- method: "POST",
370
- headers: { "Content-Type": "application/json" },
371
- body: JSON.stringify({ chat_id: chatId, text }),
372
- });
373
- return res.ok;
374
- } catch {
375
- return false;
376
- }
377
- }
1
+ /**
2
+ * Telegram onboarding sub-phase — optional, runs inside Build (Phase 3).
3
+ *
4
+ * Steps: token validation → chat_id discovery (daemon-aware) → mode selection
5
+ * → config write → smoke test with failure handling.
6
+ */
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { createRequire } from "node:module";
11
+ import { getMe, whoami } from "../../runtime/telegram-inbound.js";
12
+ import { writeEnvFile } from "../auth/api-key.js";
13
+ import { isDaemonRunning, setDaemonEnv } from "../platform.js";
14
+ import { writeTomlSection } from "../toml-writer.js";
15
+ import {
16
+ confirm, prompt, promptSecret, select, waitForEnter,
17
+ bold, green, red, yellow, dim, cyan, progressLine,
18
+ } from "../tui.js";
19
+
20
+ export const TELEGRAM_START_INSTRUCTION =
21
+ "Before continuing, open Telegram, search for your new bot, and send it /start — otherwise it won't be able to receive messages.";
22
+
23
+ // ── Helpers ──────────────────────────────────────────────────────
24
+
25
+ export function stripAnsi(str) {
26
+ // eslint-disable-next-line no-control-regex
27
+ return str.replace(/\x1b\[[0-9;]*m/g, "");
28
+ }
29
+
30
+ export function buildTelegramToml({ botTokenEnv, pollingMode, webhookUrl, operatorChatId, authorizedChatIds, defaultAgent }) {
31
+ const authIds = (authorizedChatIds || []).map((id) => `"${id}"`).join(", ");
32
+ return [
33
+ "",
34
+ "[telegram]",
35
+ `bot_token_env = "${botTokenEnv}"`,
36
+ `polling_mode = ${pollingMode}`,
37
+ `webhook_url = "${webhookUrl || ""}"`,
38
+ `operator_chat_id = "${operatorChatId || ""}"`,
39
+ `authorized_chat_ids = [${authIds}]`,
40
+ `default_agent = "${defaultAgent}"`,
41
+ "",
42
+ ].join("\n");
43
+ }
44
+
45
+ export function patchRuntimeToml(installDir, telegramToml) {
46
+ const runtimePath = path.join(installDir, "config", "runtime.toml");
47
+ writeTomlSection(runtimePath, "telegram", telegramToml);
48
+ }
49
+
50
+ async function sendTestMessage(token, chatId, text, { fetchImpl = globalThis.fetch } = {}) {
51
+ try {
52
+ const url = `https://api.telegram.org/bot${token}/sendMessage`;
53
+ const res = await fetchImpl(url, {
54
+ method: "POST",
55
+ headers: { "content-type": "application/json" },
56
+ body: JSON.stringify({ chat_id: chatId, text }),
57
+ });
58
+ const body = await res.json();
59
+ if (!body.ok) return { ok: false, error: `${res.status}: ${body.description || "unknown error"}` };
60
+ return { ok: true };
61
+ } catch (error) {
62
+ return { ok: false, error: error.message };
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Discover chat_id when the daemon is running — polls active.db for a
68
+ * telegram interactive job created after `since` (ISO string).
69
+ */
70
+ function discoverChatIdFromStore(installDir, since, { timeoutMs = 120_000, intervalMs = 2000 } = {}) {
71
+ const dbPath = path.join(installDir, "state", "active.db");
72
+ return new Promise((resolve) => {
73
+ const deadline = Date.now() + timeoutMs;
74
+
75
+ const tick = () => {
76
+ try {
77
+ // Lazy-require to avoid hard dep on better-sqlite3 at module level
78
+ const require = createRequire(import.meta.url);
79
+ const Database = require("better-sqlite3");
80
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
81
+ const row = db.prepare(
82
+ "SELECT chat_id FROM interactive_jobs WHERE source = 'telegram' AND created_at > ? ORDER BY created_at DESC LIMIT 1"
83
+ ).get(since);
84
+ db.close();
85
+ if (row?.chat_id) return resolve(row.chat_id);
86
+ } catch {
87
+ // DB may not exist yet or table missing — keep polling
88
+ }
89
+
90
+ if (Date.now() >= deadline) return resolve(null);
91
+ setTimeout(tick, intervalMs);
92
+ };
93
+ tick();
94
+ });
95
+ }
96
+
97
+ // ── Main Phase ───────────────────────────────────────────────────
98
+
99
+ /**
100
+ * Run the Telegram onboarding sub-phase (interactive).
101
+ *
102
+ * @param {{ installDir: string, agentId: string, nonInteractive?: boolean, skipGate?: boolean }} options
103
+ * @returns {Promise<{ configured: boolean, verified: boolean, botUsername?: string }>}
104
+ */
105
+ export async function runTelegramPhase({ installDir, agentId, nonInteractive = false, skipGate = false, tui = {} }) {
106
+ const botTokenEnv = "NEMORIS_TELEGRAM_BOT_TOKEN";
107
+ const confirmImpl = tui.confirm || confirm;
108
+ const promptImpl = tui.prompt || prompt;
109
+ const promptSecretImpl = tui.promptSecret || promptSecret;
110
+ const selectImpl = tui.select || select;
111
+ const waitForEnterImpl = tui.waitForEnter || waitForEnter;
112
+ const redImpl = tui.red || red;
113
+ const yellowImpl = tui.yellow || yellow;
114
+ const dimImpl = tui.dim || dim;
115
+ const cyanImpl = tui.cyan || cyan;
116
+ const progressLineImpl = tui.progressLine || progressLine;
117
+
118
+ // ── Non-interactive mode ───────────────────────────────────────
119
+ if (nonInteractive) {
120
+ const token = process.env[botTokenEnv];
121
+ if (!token || process.env.NEMORIS_SKIP_TELEGRAM === "true") {
122
+ return { configured: false, verified: false };
123
+ }
124
+
125
+ const me = await getMe(token);
126
+ if (!me.ok) return { configured: false, verified: false };
127
+
128
+ const chatId = process.env.NEMORIS_TELEGRAM_CHAT_ID || "";
129
+ const mode = process.env.NEMORIS_TELEGRAM_MODE || "polling";
130
+ const webhookUrl = process.env.NEMORIS_TELEGRAM_WEBHOOK_URL || "";
131
+
132
+ // Security: empty authorized_chat_ids with no chat_id means any sender is accepted.
133
+ // Require NEMORIS_ALLOW_ANY_CHAT=true to opt in to this open configuration.
134
+ if (!chatId && process.env.NEMORIS_ALLOW_ANY_CHAT !== "true") {
135
+ console.error("NEMORIS_TELEGRAM_CHAT_ID not set. Set it, or set NEMORIS_ALLOW_ANY_CHAT=true to allow any sender.");
136
+ return { configured: false, verified: false };
137
+ }
138
+
139
+ const authorizedChatIds = chatId ? [chatId] : [];
140
+
141
+ writeEnvFile(installDir, { [botTokenEnv]: token });
142
+ patchRuntimeToml(installDir, buildTelegramToml({
143
+ botTokenEnv, pollingMode: mode === "polling", webhookUrl,
144
+ operatorChatId: chatId, authorizedChatIds, defaultAgent: agentId,
145
+ }));
146
+
147
+ return { configured: true, verified: false, botUsername: me.username, botToken: token, operatorChatId: chatId };
148
+ }
149
+
150
+ // ── Interactive mode ───────────────────────────────────────────
151
+
152
+ // Gate
153
+ if (!skipGate) {
154
+ const gate = await confirmImpl(`Connect via ${cyanImpl("Telegram")}? This lets you message your agent from your phone.`, true);
155
+ if (!gate) {
156
+ return { configured: false, verified: false };
157
+ }
158
+ }
159
+
160
+ // Step 1: Bot token
161
+ let token = null;
162
+ let botUsername = null;
163
+
164
+ console.log(`\n ${dimImpl("Need a bot token? Open Telegram → search @BotFather → send /newbot → follow the steps.")}`);
165
+ console.log(` ${dimImpl("After creating your bot, open it in Telegram and tap Start before coming back here.")}`);
166
+ console.log(` ${dimImpl("Your input will be hidden as you type — just paste and press Enter.")}\n`);
167
+
168
+ while (!token) {
169
+ const raw = await promptSecretImpl(`Your bot token`);
170
+ if (!raw) {
171
+ console.log(` ${dimImpl("Skipping Telegram — you can add it later with `nemoris setup telegram`.")}`);
172
+ return { configured: false, verified: false };
173
+ }
174
+
175
+ const me = await getMe(raw);
176
+ if (me.ok) {
177
+ token = raw;
178
+ botUsername = me.username;
179
+ console.log(progressLineImpl("Token validated", `@${me.username}`));
180
+ console.log(` ${dimImpl(TELEGRAM_START_INSTRUCTION)}`);
181
+ } else {
182
+ console.log(` ${redImpl("✗")} That token didn't work — double-check it in BotFather and try again.`);
183
+ console.log(` ${dimImpl("Try again or press Enter to skip.")}`);
184
+ }
185
+ }
186
+
187
+ // Write token to .env + daemon env
188
+ writeEnvFile(installDir, { [botTokenEnv]: token });
189
+ await setDaemonEnv(botTokenEnv, token);
190
+
191
+ // Step 2: Chat ID discovery
192
+ let chatId = null;
193
+ const daemonRunning = isDaemonRunning();
194
+
195
+ if (daemonRunning) {
196
+ // Path A: daemon is running — read from state store
197
+ console.log(`\n Open Telegram, find ${cyanImpl(`@${botUsername}`)}, send it any message — then press Enter.`);
198
+ const since = new Date().toISOString();
199
+ chatId = await discoverChatIdFromStore(installDir, since);
200
+
201
+ if (chatId) {
202
+ console.log(progressLineImpl("Found you", `chat_id: ${chatId}`));
203
+ } else {
204
+ console.log(` ${yellowImpl("!")} Timed out waiting for a message. Try opening Telegram and sending a message to @${botUsername} first.`);
205
+ const manual = await promptImpl("Enter chat_id manually (or press Enter to skip)", "");
206
+ chatId = manual || null;
207
+ }
208
+ } else {
209
+ // Path B: daemon not running — use whoami probe
210
+ await waitForEnterImpl(`Open Telegram, find ${cyanImpl(`@${botUsername}`)}, send it a message — anything works. Then come back and press Enter.`);
211
+ const result = await whoami(token);
212
+ if (result) {
213
+ chatId = String(result.chatId);
214
+ const label = result.username ? `chat_id: ${chatId}, @${result.username}` : `chat_id: ${chatId}`;
215
+ console.log(progressLineImpl("Found you", label));
216
+ } else {
217
+ console.log(` ${yellowImpl("!")} No messages found.`);
218
+ console.log(` ${dimImpl("The bot may not have received your message yet.")}`);
219
+ const manual = await promptImpl("Enter chat_id manually (or press Enter to skip)", "");
220
+ chatId = manual || null;
221
+ }
222
+ }
223
+
224
+ const authorizedChatIds = chatId ? [chatId] : [];
225
+
226
+ // Step 3: Delivery mode
227
+ const mode = await selectImpl("Delivery mode:", [
228
+ { label: `Long polling ${dimImpl("(recommended — no tunnel needed)")}`, value: "polling" },
229
+ { label: `Webhook — for servers with a public address (advanced)`, value: "webhook" },
230
+ ]);
231
+
232
+ let webhookUrl = "";
233
+ if (mode === "webhook") {
234
+ webhookUrl = await promptImpl("Public webhook URL", "");
235
+ if (webhookUrl) {
236
+ const { registerWebhook } = await import("../../runtime/telegram-inbound.js");
237
+ const reg = await registerWebhook(token, webhookUrl);
238
+ if (reg.ok) {
239
+ console.log(progressLineImpl("Webhook registered", `${webhookUrl}/telegram/webhook`));
240
+ } else {
241
+ console.log(` ${redImpl("✗")} Webhook registration failed — make sure the URL is publicly reachable.`);
242
+ console.log(` ${dimImpl("Falling back to polling mode.")}`);
243
+ webhookUrl = "";
244
+ }
245
+ }
246
+ }
247
+
248
+ const pollingMode = !webhookUrl;
249
+
250
+ // Step 4: Write config
251
+ patchRuntimeToml(installDir, buildTelegramToml({
252
+ botTokenEnv, pollingMode, webhookUrl,
253
+ operatorChatId: chatId || "", authorizedChatIds, defaultAgent: agentId,
254
+ }));
255
+
256
+ // Store chat_id in state for later phases (e.g. hatch)
257
+ const result = { configured: true, verified: false, botUsername, botToken: token, operatorChatId: chatId || "" };
258
+
259
+ console.log(progressLineImpl("Telegram configured", webhookUrl ? "webhook" : "long-poll"));
260
+
261
+ // Step 5: Smoke test (with failure handling)
262
+ if (!chatId) {
263
+ console.log(` ${yellowImpl("!")} No chat ID — skipping test message.`);
264
+ return result;
265
+ }
266
+
267
+ let verified = false;
268
+ let smokeAttempt = true;
269
+
270
+ while (smokeAttempt) {
271
+ console.log(progressLineImpl("Sending test message", chatId));
272
+ const smokeResult = await sendTestMessage(token, chatId, "Nemoris is running. Send me a message and I'll get to work.");
273
+
274
+ if (smokeResult.ok) {
275
+ console.log(progressLineImpl("Telegram connected", `message delivered to ${chatId}`));
276
+ verified = true;
277
+ smokeAttempt = false;
278
+ } else {
279
+ console.log(` ${redImpl("✗")} Delivery failed: ${smokeResult.error}\n`);
280
+ const choice = await selectImpl("What next?", [
281
+ { label: "Retry", value: "retry" },
282
+ { label: `Skip — finish setup without verification`, value: "skip" },
283
+ { label: "Re-enter bot token", value: "retoken" },
284
+ ]);
285
+
286
+ if (choice === "retry") continue;
287
+ if (choice === "skip") {
288
+ smokeAttempt = false;
289
+ }
290
+ if (choice === "retoken") {
291
+ // Recursive — restart from Step 1
292
+ return runTelegramPhase({ installDir, agentId, nonInteractive, skipGate: true });
293
+ }
294
+ }
295
+ }
296
+
297
+ return { ...result, verified };
298
+ }
299
+
300
+ /**
301
+ * Send the agent's "hatch" moment message to Telegram.
302
+ *
303
+ * When providerConfig and identity content are supplied, generates a
304
+ * personalised AI greeting via the provider stack. Falls back to the
305
+ * static greeting on provider failure or when no config is available.
306
+ *
307
+ * @param {{ token: string, chatId: string, agentName?: string, userName?: string, installDir?: string, agentId?: string, userGoal?: string, soulContent?: string, purposeContent?: string, providerConfig?: object, fetchImpl?: Function }} opts
308
+ * @returns {Promise<boolean>}
309
+ */
310
+ export async function sendHatchMessage({
311
+ token,
312
+ chatId,
313
+ agentName,
314
+ userName,
315
+ installDir = null,
316
+ agentId = null,
317
+ userGoal = "",
318
+ soulContent = "",
319
+ purposeContent = "",
320
+ providerConfig = null,
321
+ migrated = false,
322
+ fetchImpl = globalThis.fetch,
323
+ }) {
324
+ const name = stripAnsi(userName || "there");
325
+ const agent = stripAnsi(agentName || "your agent");
326
+
327
+ // Attempt provider-generated greeting
328
+ let text = null;
329
+ if (providerConfig) {
330
+ try {
331
+ const { generateGreeting } = await import("./hatch.js");
332
+ const result = await generateGreeting({
333
+ agentName: agent,
334
+ userName: name,
335
+ userGoal,
336
+ soulContent,
337
+ purposeContent,
338
+ providerConfig,
339
+ migrated,
340
+ });
341
+ if (result.fromProvider) {
342
+ text = result.greeting;
343
+ }
344
+ } catch {
345
+ // Fall through to static greeting
346
+ }
347
+ }
348
+
349
+ // Static fallback — tone matches migrated vs fresh
350
+ if (!text) {
351
+ const { staticGreeting } = await import("./hatch.js");
352
+ text = staticGreeting(agent, name, migrated);
353
+ }
354
+
355
+ // Write birth fact to memory (best-effort, non-blocking)
356
+ if (installDir && agentId) {
357
+ try {
358
+ const { writeBirthFact } = await import("./hatch.js");
359
+ await writeBirthFact({ installDir, agentId, agentName: agent, userName: name, migrated });
360
+ } catch {
361
+ // Non-fatal — don't fail the Telegram send
362
+ }
363
+ }
364
+
365
+ // Send to Telegram
366
+ const url = `https://api.telegram.org/bot${token}/sendMessage`;
367
+ try {
368
+ const res = await fetchImpl(url, {
369
+ method: "POST",
370
+ headers: { "Content-Type": "application/json" },
371
+ body: JSON.stringify({ chat_id: chatId, text }),
372
+ });
373
+ return res.ok;
374
+ } catch {
375
+ return false;
376
+ }
377
+ }