nemoris 0.1.4 → 0.1.5
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/package.json +1 -1
- package/src/cli-main.js +13 -1
- package/src/onboarding/lock.js +5 -4
- package/src/onboarding/phases/telegram.js +102 -29
- package/src/onboarding/setup-checklist.js +108 -0
- package/src/onboarding/templates.js +32 -0
- package/src/onboarding/wizard.js +252 -0
- package/src/runtime/status-aggregator.js +12 -1
- package/src/runtime/telegram-inbound.js +32 -1
package/package.json
CHANGED
package/src/cli-main.js
CHANGED
|
@@ -840,7 +840,7 @@ async function serveDaemon(mode = "dry-run", intervalMs = "30000", installDir =
|
|
|
840
840
|
const execApprovalGate = handler.getExecApprovalGate();
|
|
841
841
|
localDaemon.executor.setExecApprovalGate(execApprovalGate);
|
|
842
842
|
localExecutor.setExecApprovalGate(execApprovalGate);
|
|
843
|
-
telegramPoller = startTelegramPolling({ botToken: token, handler });
|
|
843
|
+
telegramPoller = startTelegramPolling({ botToken: token, handler, projectRoot: root });
|
|
844
844
|
await stateStore.setMeta("telegram_inbound", { status: "READY", mode: "polling" });
|
|
845
845
|
console.log(JSON.stringify({ service: "telegram_inbound", status: "READY", mode: "polling" }));
|
|
846
846
|
} else if (token && telegramConfig.webhookUrl) {
|
|
@@ -2684,6 +2684,16 @@ export async function main(argv = process.argv) {
|
|
|
2684
2684
|
await showModelsOverview(resolveRuntimeInstallDir());
|
|
2685
2685
|
} else if (command === "setup" && rest[0] === "telegram") {
|
|
2686
2686
|
await setupTelegram();
|
|
2687
|
+
} else if (command === "setup" && rest[0] === "provider") {
|
|
2688
|
+
const { runProviderSubWizard } = await import("./onboarding/wizard.js");
|
|
2689
|
+
return runProviderSubWizard({
|
|
2690
|
+
installDir: resolveRuntimeInstallDir(),
|
|
2691
|
+
});
|
|
2692
|
+
} else if (command === "setup" && rest[0] === "ollama") {
|
|
2693
|
+
const { runOllamaSubWizard } = await import("./onboarding/wizard.js");
|
|
2694
|
+
return runOllamaSubWizard({
|
|
2695
|
+
installDir: resolveRuntimeInstallDir(),
|
|
2696
|
+
});
|
|
2687
2697
|
} else if (command === "telegram" && rest[0] === "whoami") {
|
|
2688
2698
|
await telegramWhoami();
|
|
2689
2699
|
} else if (command === "uninstall") {
|
|
@@ -2698,6 +2708,7 @@ export async function main(argv = process.argv) {
|
|
|
2698
2708
|
installDir: resolveRuntimeInstallDir(),
|
|
2699
2709
|
nonInteractive,
|
|
2700
2710
|
advanced,
|
|
2711
|
+
manual: rest.includes("--manual"),
|
|
2701
2712
|
acceptRisk: rest.includes("--accept-risk"),
|
|
2702
2713
|
flow: parseFlagValue(rest, "--flow"),
|
|
2703
2714
|
anthropicKey: parseFlagValue(rest, "--anthropic-key"),
|
|
@@ -2713,6 +2724,7 @@ export async function main(argv = process.argv) {
|
|
|
2713
2724
|
installDir: resolveRuntimeInstallDir(),
|
|
2714
2725
|
nonInteractive,
|
|
2715
2726
|
advanced,
|
|
2727
|
+
manual: rest.includes("--manual"),
|
|
2716
2728
|
acceptRisk: rest.includes("--accept-risk"),
|
|
2717
2729
|
flow: parseFlagValue(rest, "--flow"),
|
|
2718
2730
|
anthropicKey: parseFlagValue(rest, "--anthropic-key"),
|
package/src/onboarding/lock.js
CHANGED
|
@@ -33,16 +33,17 @@ export function readLock(installDir) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
export function writeWizardMetadata(installDir, metadata) {
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
...
|
|
36
|
+
const existing = readLock(installDir) || {};
|
|
37
|
+
const state = {
|
|
38
|
+
...existing,
|
|
39
39
|
lastRunAt: metadata.lastRunAt || new Date().toISOString(),
|
|
40
40
|
lastRunVersion: metadata.lastRunVersion || "",
|
|
41
41
|
lastRunCommand: metadata.lastRunCommand || "setup",
|
|
42
42
|
lastRunMode: metadata.lastRunMode || "quickstart",
|
|
43
43
|
lastRunFlow: metadata.lastRunFlow || "interactive",
|
|
44
|
+
capabilities: { ...(existing.capabilities || {}), ...(metadata.capabilities || {}) },
|
|
44
45
|
};
|
|
45
|
-
writeLock(installDir,
|
|
46
|
+
writeLock(installDir, state);
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
export function deleteLock(installDir) {
|
|
@@ -12,6 +12,7 @@ import { getMe, whoami } from "../../runtime/telegram-inbound.js";
|
|
|
12
12
|
import { writeEnvFile } from "../auth/api-key.js";
|
|
13
13
|
import { isDaemonRunning, setDaemonEnv } from "../platform.js";
|
|
14
14
|
import { writeTomlSection } from "../toml-writer.js";
|
|
15
|
+
import { deliveryTelegramPatch } from "../templates.js";
|
|
15
16
|
import {
|
|
16
17
|
confirm, prompt, promptSecret, select, waitForEnter,
|
|
17
18
|
bold, green, red, yellow, dim, cyan, progressLine,
|
|
@@ -47,6 +48,87 @@ export function patchRuntimeToml(installDir, telegramToml) {
|
|
|
47
48
|
writeTomlSection(runtimePath, "telegram", telegramToml);
|
|
48
49
|
}
|
|
49
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Called at runtime when the daemon receives the first inbound Telegram
|
|
53
|
+
* message and chat_id_pending is set. Writes the discovered chat_id
|
|
54
|
+
* back to runtime.toml and delivery.toml, clearing the pending flag.
|
|
55
|
+
*/
|
|
56
|
+
export function resolvePendingChatId(installDir, chatId) {
|
|
57
|
+
// Patch runtime.toml: add operator_chat_id, remove chat_id_pending
|
|
58
|
+
const runtimePath = path.join(installDir, "config", "runtime.toml");
|
|
59
|
+
let runtime = fs.readFileSync(runtimePath, "utf8");
|
|
60
|
+
runtime = runtime.replace(/chat_id_pending\s*=\s*true\n?/, "");
|
|
61
|
+
runtime = runtime.replace(
|
|
62
|
+
/(\[telegram\][^\[]*)/s,
|
|
63
|
+
(section) => section.trimEnd() + `\noperator_chat_id = "${chatId}"\n`
|
|
64
|
+
);
|
|
65
|
+
fs.writeFileSync(runtimePath, runtime);
|
|
66
|
+
|
|
67
|
+
// Patch delivery.toml: replace YOUR_CHAT_ID with real chat_id
|
|
68
|
+
const deliveryPath = path.join(installDir, "config", "delivery.toml");
|
|
69
|
+
if (fs.existsSync(deliveryPath)) {
|
|
70
|
+
let delivery = fs.readFileSync(deliveryPath, "utf8");
|
|
71
|
+
delivery = delivery.replace(/chat_id\s*=\s*"YOUR_CHAT_ID"/g, `chat_id = "${chatId}"`);
|
|
72
|
+
fs.writeFileSync(deliveryPath, delivery);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export async function patchDeliveryToml(installDir, { chatId, botTokenEnv, chatIdPending }) {
|
|
77
|
+
const deliveryPath = path.join(installDir, "config", "delivery.toml");
|
|
78
|
+
let existing = "";
|
|
79
|
+
try {
|
|
80
|
+
existing = fs.readFileSync(deliveryPath, "utf8");
|
|
81
|
+
} catch { /* no existing file */ }
|
|
82
|
+
|
|
83
|
+
const patch = deliveryTelegramPatch({ chatId, botTokenEnv, chatIdPending });
|
|
84
|
+
|
|
85
|
+
// Replace default_interactive_profile line if present
|
|
86
|
+
let merged = existing.replace(
|
|
87
|
+
/^default_interactive_profile\s*=\s*"[^"]*"/m,
|
|
88
|
+
'default_interactive_profile = "gateway_telegram_main"'
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
// Remove existing profile sections that will be replaced
|
|
92
|
+
for (const profileName of ["gateway_telegram_main", "gateway_preview_main"]) {
|
|
93
|
+
const sectionRegex = new RegExp(
|
|
94
|
+
`\\[profiles\\.${profileName}\\][\\s\\S]*?(?=\\n\\[|$)`,
|
|
95
|
+
"g"
|
|
96
|
+
);
|
|
97
|
+
merged = merged.replace(sectionRegex, "");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Append patch
|
|
101
|
+
merged = merged.trimEnd() + "\n\n" + patch + "\n";
|
|
102
|
+
|
|
103
|
+
fs.writeFileSync(deliveryPath, merged);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export async function discoverChatIdFromUpdates(token, options = {}) {
|
|
107
|
+
const { timeoutSeconds = 30, fetchImpl = globalThis.fetch } = options;
|
|
108
|
+
const deadline = Date.now() + timeoutSeconds * 1000;
|
|
109
|
+
let offset = 0;
|
|
110
|
+
|
|
111
|
+
while (Date.now() < deadline) {
|
|
112
|
+
const remaining = Math.max(1, Math.ceil((deadline - Date.now()) / 1000));
|
|
113
|
+
const pollSeconds = Math.min(remaining, 5);
|
|
114
|
+
const url = `https://api.telegram.org/bot${token}/getUpdates?offset=${offset}&timeout=${pollSeconds}`;
|
|
115
|
+
try {
|
|
116
|
+
const resp = await fetchImpl(url);
|
|
117
|
+
const data = await resp.json();
|
|
118
|
+
if (data.ok && data.result?.length > 0) {
|
|
119
|
+
for (const update of data.result) {
|
|
120
|
+
offset = update.update_id + 1;
|
|
121
|
+
const chatId = update.message?.chat?.id;
|
|
122
|
+
if (chatId) return String(chatId);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
await new Promise((r) => setTimeout(r, 1000));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
50
132
|
async function sendTestMessage(token, chatId, text, { fetchImpl = globalThis.fetch } = {}) {
|
|
51
133
|
try {
|
|
52
134
|
const url = `https://api.telegram.org/bot${token}/sendMessage`;
|
|
@@ -177,7 +259,8 @@ export async function runTelegramPhase({ installDir, agentId, nonInteractive = f
|
|
|
177
259
|
token = raw;
|
|
178
260
|
botUsername = me.username;
|
|
179
261
|
console.log(progressLineImpl("Token validated", `@${me.username}`));
|
|
180
|
-
console.log(
|
|
262
|
+
console.log(`\n ${dimImpl("Open this link to start your bot:")}`);
|
|
263
|
+
console.log(` ${cyanImpl(`https://t.me/${me.username}?start=setup`)}\n`);
|
|
181
264
|
} else {
|
|
182
265
|
console.log(` ${redImpl("✗")} That token didn't work — double-check it in BotFather and try again.`);
|
|
183
266
|
console.log(` ${dimImpl("Try again or press Enter to skip.")}`);
|
|
@@ -188,37 +271,20 @@ export async function runTelegramPhase({ installDir, agentId, nonInteractive = f
|
|
|
188
271
|
writeEnvFile(installDir, { [botTokenEnv]: token });
|
|
189
272
|
await setDaemonEnv(botTokenEnv, token);
|
|
190
273
|
|
|
191
|
-
// Step 2: Chat ID discovery
|
|
274
|
+
// Step 2: Chat ID discovery via Telegram getUpdates (30s poll)
|
|
192
275
|
let chatId = null;
|
|
193
|
-
const
|
|
276
|
+
const fetchImpl = tui.fetchImpl || globalThis.fetch;
|
|
194
277
|
|
|
195
|
-
|
|
196
|
-
|
|
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);
|
|
278
|
+
console.log(` ${dimImpl("Waiting up to 30 seconds for your message...")}`);
|
|
279
|
+
chatId = await discoverChatIdFromUpdates(token, { timeoutSeconds: 30, fetchImpl });
|
|
200
280
|
|
|
201
|
-
|
|
202
|
-
|
|
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
|
-
}
|
|
281
|
+
if (chatId) {
|
|
282
|
+
console.log(progressLineImpl("Found you", `chat_id: ${chatId}`));
|
|
208
283
|
} else {
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
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
|
-
}
|
|
284
|
+
console.log(` ${yellowImpl("!")} No message received within 30 seconds.`);
|
|
285
|
+
console.log(` ${dimImpl("You can finish setup now and add your chat_id later.")}`);
|
|
286
|
+
const manual = await promptImpl("Enter chat_id manually (or press Enter to skip)", "");
|
|
287
|
+
chatId = manual || null;
|
|
222
288
|
}
|
|
223
289
|
|
|
224
290
|
const authorizedChatIds = chatId ? [chatId] : [];
|
|
@@ -253,6 +319,13 @@ export async function runTelegramPhase({ installDir, agentId, nonInteractive = f
|
|
|
253
319
|
operatorChatId: chatId || "", authorizedChatIds, defaultAgent: agentId,
|
|
254
320
|
}));
|
|
255
321
|
|
|
322
|
+
// Wire delivery.toml so the telegram profile is enabled
|
|
323
|
+
await patchDeliveryToml(installDir, {
|
|
324
|
+
chatId: chatId || null,
|
|
325
|
+
botTokenEnv,
|
|
326
|
+
chatIdPending: !chatId,
|
|
327
|
+
});
|
|
328
|
+
|
|
256
329
|
// Store chat_id in state for later phases (e.g. hatch)
|
|
257
330
|
const result = { configured: true, verified: false, botUsername, botToken: token, operatorChatId: chatId || "" };
|
|
258
331
|
|
|
@@ -260,7 +333,7 @@ export async function runTelegramPhase({ installDir, agentId, nonInteractive = f
|
|
|
260
333
|
|
|
261
334
|
// Step 5: Smoke test (with failure handling)
|
|
262
335
|
if (!chatId) {
|
|
263
|
-
console.log(` ${yellowImpl("!")} No chat ID — skipping test message.`);
|
|
336
|
+
console.log(` ${yellowImpl("!")} No chat ID — skipping test message. You can set it later in config/delivery.toml.`);
|
|
264
337
|
return result;
|
|
265
338
|
}
|
|
266
339
|
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { parseToml } from "../config/toml-lite.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Scans an install directory and returns a map of which capabilities
|
|
7
|
+
* are configured, pending, or unconfigured.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} installDir - Path to the nemoris install directory
|
|
10
|
+
* @returns {{ identity, provider, telegram, ollama }} checklist
|
|
11
|
+
*/
|
|
12
|
+
export function buildSetupChecklist(installDir) {
|
|
13
|
+
return {
|
|
14
|
+
identity: detectIdentity(installDir),
|
|
15
|
+
provider: detectProvider(installDir),
|
|
16
|
+
telegram: detectTelegram(installDir),
|
|
17
|
+
ollama: detectOllama(installDir),
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function detectIdentity(installDir) {
|
|
22
|
+
try {
|
|
23
|
+
const agentsDir = path.join(installDir, "config", "agents");
|
|
24
|
+
if (!fs.existsSync(agentsDir)) return { configured: false };
|
|
25
|
+
const files = fs.readdirSync(agentsDir).filter((f) => f.endsWith(".toml"));
|
|
26
|
+
if (files.length === 0) return { configured: false };
|
|
27
|
+
const agentId = files[0].replace(/\.toml$/, "");
|
|
28
|
+
const soulDir = path.join(installDir, "config", "identity");
|
|
29
|
+
const hasSoul = fs.existsSync(soulDir) &&
|
|
30
|
+
fs.readdirSync(soulDir).some((f) => f.endsWith("-soul.md"));
|
|
31
|
+
return { configured: hasSoul, agentId };
|
|
32
|
+
} catch {
|
|
33
|
+
return { configured: false };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function detectProvider(installDir) {
|
|
38
|
+
try {
|
|
39
|
+
const providersDir = path.join(installDir, "config", "providers");
|
|
40
|
+
if (!fs.existsSync(providersDir)) return { configured: false };
|
|
41
|
+
const files = fs.readdirSync(providersDir).filter((f) => f.endsWith(".toml"));
|
|
42
|
+
if (files.length === 0) return { configured: false };
|
|
43
|
+
const name = files[0].replace(/\.toml$/, "");
|
|
44
|
+
return { configured: true, name, count: files.length };
|
|
45
|
+
} catch {
|
|
46
|
+
return { configured: false };
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function detectTelegram(installDir) {
|
|
51
|
+
try {
|
|
52
|
+
const runtimePath = path.join(installDir, "config", "runtime.toml");
|
|
53
|
+
if (!fs.existsSync(runtimePath)) return { configured: false };
|
|
54
|
+
const content = fs.readFileSync(runtimePath, "utf8");
|
|
55
|
+
const config = parseToml(content);
|
|
56
|
+
const tg = config.telegram;
|
|
57
|
+
if (!tg || !tg.bot_token_env) return { configured: false };
|
|
58
|
+
if (tg.chat_id_pending) return { configured: false, pending: true };
|
|
59
|
+
if (tg.operator_chat_id) return { configured: true };
|
|
60
|
+
return { configured: false, pending: true };
|
|
61
|
+
} catch {
|
|
62
|
+
return { configured: false };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function detectOllama(installDir) {
|
|
67
|
+
try {
|
|
68
|
+
const providersDir = path.join(installDir, "config", "providers");
|
|
69
|
+
if (!fs.existsSync(providersDir)) return { configured: false };
|
|
70
|
+
const ollamaPath = path.join(providersDir, "ollama.toml");
|
|
71
|
+
return { configured: fs.existsSync(ollamaPath) };
|
|
72
|
+
} catch {
|
|
73
|
+
return { configured: false };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Format checklist for display in `nemoris status`.
|
|
79
|
+
* @param {object} checklist - from buildSetupChecklist()
|
|
80
|
+
* @returns {string} formatted text block
|
|
81
|
+
*/
|
|
82
|
+
export function formatSetupChecklist(checklist) {
|
|
83
|
+
const lines = ["Setup"];
|
|
84
|
+
const item = (check, label, hint) => {
|
|
85
|
+
if (check.configured) {
|
|
86
|
+
lines.push(` ✅ ${label}`);
|
|
87
|
+
} else if (check.pending) {
|
|
88
|
+
lines.push(` ⚠️ ${label.padEnd(16)} pending auto-discovery`);
|
|
89
|
+
} else {
|
|
90
|
+
lines.push(` ◻ ${label.padEnd(16)} ${hint}`);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const idLabel = checklist.identity.configured
|
|
95
|
+
? `Identity ${checklist.identity.agentId}`
|
|
96
|
+
: "Identity";
|
|
97
|
+
item(checklist.identity, idLabel, "nemoris setup");
|
|
98
|
+
|
|
99
|
+
const provLabel = checklist.provider.configured
|
|
100
|
+
? `Provider ${checklist.provider.name}${checklist.provider.count > 1 ? ` (+${checklist.provider.count - 1})` : ""}`
|
|
101
|
+
: "Provider";
|
|
102
|
+
item(checklist.provider, provLabel, "nemoris setup");
|
|
103
|
+
|
|
104
|
+
item(checklist.telegram, "Telegram", "nemoris setup telegram");
|
|
105
|
+
item(checklist.ollama, "Local models", "nemoris setup ollama");
|
|
106
|
+
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
@@ -322,6 +322,38 @@ target = "peer_queue"
|
|
|
322
322
|
`;
|
|
323
323
|
}
|
|
324
324
|
|
|
325
|
+
// ── Delivery Telegram Patch ──────────────────────────────────────────────────
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Generates a delivery.toml patch that wires the Telegram gateway profiles.
|
|
329
|
+
*/
|
|
330
|
+
export function deliveryTelegramPatch({ chatId, botTokenEnv, chatIdPending }) {
|
|
331
|
+
const chatIdValue = chatIdPending ? "YOUR_CHAT_ID" : (chatId || "YOUR_CHAT_ID");
|
|
332
|
+
return `
|
|
333
|
+
default_interactive_profile = "gateway_telegram_main"
|
|
334
|
+
|
|
335
|
+
[profiles.gateway_telegram_main]
|
|
336
|
+
adapter = "openclaw_cli"
|
|
337
|
+
enabled = true
|
|
338
|
+
channel = "telegram"
|
|
339
|
+
account_id = "default"
|
|
340
|
+
chat_id = "${chatIdValue}"
|
|
341
|
+
bot_token_env = "${botTokenEnv}"
|
|
342
|
+
silent = true
|
|
343
|
+
dry_run = false
|
|
344
|
+
|
|
345
|
+
[profiles.gateway_preview_main]
|
|
346
|
+
adapter = "openclaw_cli"
|
|
347
|
+
enabled = true
|
|
348
|
+
channel = "telegram"
|
|
349
|
+
account_id = "default"
|
|
350
|
+
chat_id = "${chatIdValue}"
|
|
351
|
+
bot_token_env = "${botTokenEnv}"
|
|
352
|
+
silent = true
|
|
353
|
+
dry_run = true
|
|
354
|
+
`.trim();
|
|
355
|
+
}
|
|
356
|
+
|
|
325
357
|
// ── Peers ────────────────────────────────────────────────────────────────────
|
|
326
358
|
|
|
327
359
|
/**
|
package/src/onboarding/wizard.js
CHANGED
|
@@ -197,9 +197,259 @@ async function launchChat() {
|
|
|
197
197
|
});
|
|
198
198
|
}
|
|
199
199
|
|
|
200
|
+
async function runFastPathWizard({ installDir }) {
|
|
201
|
+
const prompter = createClackPrompter();
|
|
202
|
+
|
|
203
|
+
await prompter.intro("nemoris setup");
|
|
204
|
+
|
|
205
|
+
// D2 security gate — brief consent
|
|
206
|
+
await prompter.note(
|
|
207
|
+
"This agent will have access to your filesystem and network.\nRun with --security-details for full risk disclosure.",
|
|
208
|
+
"Security"
|
|
209
|
+
);
|
|
210
|
+
const consent = await prompter.confirm({
|
|
211
|
+
message: "Continue?",
|
|
212
|
+
initialValue: true,
|
|
213
|
+
});
|
|
214
|
+
if (!consent) {
|
|
215
|
+
await prompter.cancel("Setup cancelled.");
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Existing config? → delegate to update menu (wired in Task 7, for now just exit)
|
|
220
|
+
const existing = readExistingIdentity(installDir);
|
|
221
|
+
if (existing.agentId && fs.existsSync(path.join(installDir, "config", "runtime.toml"))) {
|
|
222
|
+
return runExistingConfigMenu({ installDir, existing, prompter });
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Step 1: Identity
|
|
226
|
+
const userName = await prompter.text({
|
|
227
|
+
message: "What's your name?",
|
|
228
|
+
initialValue: process.env.USER || "",
|
|
229
|
+
validate: (v) => (v.trim() ? undefined : "Name required"),
|
|
230
|
+
});
|
|
231
|
+
const agentName = await prompter.text({
|
|
232
|
+
message: "Name your agent?",
|
|
233
|
+
initialValue: "Nemo",
|
|
234
|
+
});
|
|
235
|
+
const agentId = String(agentName).toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-");
|
|
236
|
+
|
|
237
|
+
// Step 2: Detect + scaffold (silent)
|
|
238
|
+
const detection = await detect(installDir);
|
|
239
|
+
await scaffold({ installDir });
|
|
240
|
+
|
|
241
|
+
// Step 3: Provider selection
|
|
242
|
+
const providerOptions = detectProviderOptions({
|
|
243
|
+
env: process.env,
|
|
244
|
+
ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
|
|
245
|
+
});
|
|
246
|
+
let provider;
|
|
247
|
+
if (providerOptions.length === 1) {
|
|
248
|
+
provider = providerOptions[0].value;
|
|
249
|
+
} else if (providerOptions.length > 1) {
|
|
250
|
+
provider = await prompter.select({
|
|
251
|
+
message: "Pick a provider",
|
|
252
|
+
options: providerOptions.map((o) => ({
|
|
253
|
+
value: o.value,
|
|
254
|
+
label: o.label,
|
|
255
|
+
hint: o.hint,
|
|
256
|
+
})),
|
|
257
|
+
});
|
|
258
|
+
} else {
|
|
259
|
+
provider = await prompter.select({
|
|
260
|
+
message: "Pick a provider",
|
|
261
|
+
options: [
|
|
262
|
+
{ value: "anthropic", label: "Anthropic" },
|
|
263
|
+
{ value: "openai", label: "OpenAI" },
|
|
264
|
+
{ value: "openrouter", label: "OpenRouter" },
|
|
265
|
+
],
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Step 4: Auth + model selection
|
|
270
|
+
writeIdentity({
|
|
271
|
+
installDir,
|
|
272
|
+
userName: userName || process.env.USER || "operator",
|
|
273
|
+
agentName,
|
|
274
|
+
agentId,
|
|
275
|
+
userGoal: "build software",
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
if (provider !== "skip" && provider !== "ollama") {
|
|
279
|
+
await runAuthPhase(installDir, {
|
|
280
|
+
tui: createLegacyPromptAdapter(prompter),
|
|
281
|
+
detectionCache: {
|
|
282
|
+
rawKeys: detection.apiKeys,
|
|
283
|
+
ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
|
|
284
|
+
},
|
|
285
|
+
providerOrder: [provider],
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Auto-start daemon (best-effort)
|
|
290
|
+
try {
|
|
291
|
+
await writeDaemonUnit(installDir);
|
|
292
|
+
await loadDaemon(installDir);
|
|
293
|
+
} catch {
|
|
294
|
+
// Daemon start failure is non-fatal in fast path
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
await prompter.outro(`Agent "${agentName}" is ready.\n Run: nemoris chat`);
|
|
298
|
+
|
|
299
|
+
try {
|
|
300
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
301
|
+
writeWizardMetadata(installDir, {
|
|
302
|
+
lastRunVersion: pkg.version || "",
|
|
303
|
+
lastRunCommand: "setup",
|
|
304
|
+
lastRunMode: "fast",
|
|
305
|
+
lastRunFlow: "interactive",
|
|
306
|
+
capabilities: { identity: true, provider: true },
|
|
307
|
+
});
|
|
308
|
+
} catch {
|
|
309
|
+
// Non-fatal
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
return 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function runExistingConfigMenu({ installDir, existing, prompter }) {
|
|
316
|
+
const { buildSetupChecklist } = await import("./setup-checklist.js");
|
|
317
|
+
|
|
318
|
+
const action = await prompter.select({
|
|
319
|
+
message: `Agent "${existing.agentId || existing.agentName}" is already configured.`,
|
|
320
|
+
options: [
|
|
321
|
+
{ value: "update_provider", label: "Update provider or model" },
|
|
322
|
+
{ value: "add_capability", label: "Add a capability" },
|
|
323
|
+
{ value: "reset", label: "Reset everything" },
|
|
324
|
+
{ value: "exit", label: "Exit" },
|
|
325
|
+
],
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (action === "update_provider") {
|
|
329
|
+
return runProviderSubWizard({ installDir });
|
|
330
|
+
}
|
|
331
|
+
if (action === "add_capability") {
|
|
332
|
+
const checklist = buildSetupChecklist(installDir);
|
|
333
|
+
const unconfigured = [];
|
|
334
|
+
if (!checklist.telegram.configured) unconfigured.push({ value: "telegram", label: "Telegram" });
|
|
335
|
+
if (!checklist.ollama.configured) unconfigured.push({ value: "ollama", label: "Local models (Ollama)" });
|
|
336
|
+
if (unconfigured.length === 0) {
|
|
337
|
+
await prompter.outro("All capabilities are configured!");
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
const capability = await prompter.select({ message: "What would you like to add?", options: unconfigured });
|
|
341
|
+
if (capability === "telegram") {
|
|
342
|
+
return runTelegramPhase({ installDir, agentId: existing.agentId });
|
|
343
|
+
}
|
|
344
|
+
if (capability === "ollama") {
|
|
345
|
+
return runOllamaSubWizard({ installDir });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
if (action === "reset") {
|
|
349
|
+
const scope = await prompter.select({
|
|
350
|
+
message: "What should be reset?",
|
|
351
|
+
options: [
|
|
352
|
+
{ value: "config", label: "Config only" },
|
|
353
|
+
{ value: "config+state", label: "Config + state" },
|
|
354
|
+
{ value: "", label: "Everything" },
|
|
355
|
+
],
|
|
356
|
+
});
|
|
357
|
+
resetInstallArtifacts(installDir, scope);
|
|
358
|
+
await prompter.outro("Reset complete. Run: nemoris setup");
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
// exit — do nothing
|
|
362
|
+
await prompter.outro("No changes made.");
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
export async function runProviderSubWizard({ installDir }) {
|
|
366
|
+
const prompter = createClackPrompter();
|
|
367
|
+
await prompter.intro("Add a provider");
|
|
368
|
+
|
|
369
|
+
const detection = await detect(installDir);
|
|
370
|
+
const options = detectProviderOptions({
|
|
371
|
+
env: process.env,
|
|
372
|
+
ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
// Filter out already-configured providers
|
|
376
|
+
const providersDir = path.join(installDir, "config", "providers");
|
|
377
|
+
const existingProviders = fs.existsSync(providersDir)
|
|
378
|
+
? fs.readdirSync(providersDir).filter((f) => f.endsWith(".toml")).map((f) => f.replace(/\.toml$/, ""))
|
|
379
|
+
: [];
|
|
380
|
+
const filtered = options.filter((o) => !existingProviders.includes(o.value));
|
|
381
|
+
|
|
382
|
+
if (filtered.length === 0) {
|
|
383
|
+
await prompter.outro("All detected providers are already configured.");
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const provider = await prompter.select({ message: "Which provider?", options: filtered });
|
|
388
|
+
|
|
389
|
+
// Reuse existing auth phase
|
|
390
|
+
if (provider !== "skip" && provider !== "ollama") {
|
|
391
|
+
await runAuthPhase(installDir, {
|
|
392
|
+
tui: createLegacyPromptAdapter(prompter),
|
|
393
|
+
detectionCache: {
|
|
394
|
+
rawKeys: detection.apiKeys,
|
|
395
|
+
ollamaResult: detection.ollama ? { ok: true, models: detection.ollama.models } : { ok: false, models: [] },
|
|
396
|
+
},
|
|
397
|
+
providerOrder: [provider],
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
try {
|
|
402
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
403
|
+
writeWizardMetadata(installDir, {
|
|
404
|
+
lastRunVersion: pkg.version || "",
|
|
405
|
+
lastRunCommand: "setup provider",
|
|
406
|
+
lastRunMode: "fast",
|
|
407
|
+
lastRunFlow: "interactive",
|
|
408
|
+
capabilities: { [`provider_${provider}`]: true },
|
|
409
|
+
});
|
|
410
|
+
} catch {
|
|
411
|
+
// Non-fatal
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
await prompter.outro(`${provider} configured. Run: nemoris status`);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
export async function runOllamaSubWizard({ installDir }) {
|
|
418
|
+
const prompter = createClackPrompter();
|
|
419
|
+
await prompter.intro("Add local models (Ollama)");
|
|
420
|
+
|
|
421
|
+
await runOllamaPhase({ installDir });
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
const pkg = JSON.parse(fs.readFileSync(new URL("../../package.json", import.meta.url), "utf8"));
|
|
425
|
+
writeWizardMetadata(installDir, {
|
|
426
|
+
lastRunVersion: pkg.version || "",
|
|
427
|
+
lastRunCommand: "setup ollama",
|
|
428
|
+
lastRunMode: "fast",
|
|
429
|
+
lastRunFlow: "interactive",
|
|
430
|
+
capabilities: { ollama: true },
|
|
431
|
+
});
|
|
432
|
+
} catch {
|
|
433
|
+
// Non-fatal
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
await prompter.outro("Ollama configured. Run: nemoris status");
|
|
437
|
+
}
|
|
438
|
+
|
|
200
439
|
async function runInteractiveWizard({
|
|
201
440
|
installDir,
|
|
202
441
|
flowOverride = null,
|
|
442
|
+
manual = false,
|
|
443
|
+
}) {
|
|
444
|
+
if (manual) {
|
|
445
|
+
return runManualWizard({ installDir, flowOverride });
|
|
446
|
+
}
|
|
447
|
+
return runFastPathWizard({ installDir });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
async function runManualWizard({
|
|
451
|
+
installDir,
|
|
452
|
+
flowOverride = null,
|
|
203
453
|
}) {
|
|
204
454
|
const prompter = createClackPrompter();
|
|
205
455
|
|
|
@@ -587,6 +837,7 @@ export async function runWizard({
|
|
|
587
837
|
nonInteractive = false,
|
|
588
838
|
acceptRisk = false,
|
|
589
839
|
flow = null,
|
|
840
|
+
manual = false,
|
|
590
841
|
anthropicKey = null,
|
|
591
842
|
openaiKey = null,
|
|
592
843
|
openrouterKey = null,
|
|
@@ -609,6 +860,7 @@ export async function runWizard({
|
|
|
609
860
|
: await runInteractiveWizard({
|
|
610
861
|
installDir,
|
|
611
862
|
flowOverride: flow,
|
|
863
|
+
manual,
|
|
612
864
|
});
|
|
613
865
|
} catch (error) {
|
|
614
866
|
if (error instanceof SetupCancelledError) {
|
|
@@ -7,6 +7,7 @@ import { PeerRegistry } from "./peer-registry.js";
|
|
|
7
7
|
import { SchedulerStateStore } from "./scheduler-state.js";
|
|
8
8
|
import { computeNextRun } from "./schedule.js";
|
|
9
9
|
import { ProviderRegistry } from "../providers/registry.js";
|
|
10
|
+
import { buildSetupChecklist, formatSetupChecklist } from "../onboarding/setup-checklist.js";
|
|
10
11
|
|
|
11
12
|
function _safeCount(arr) {
|
|
12
13
|
return Array.isArray(arr) ? arr.length : 0;
|
|
@@ -245,7 +246,8 @@ export async function buildRuntimeStatus(options = {}) {
|
|
|
245
246
|
providers,
|
|
246
247
|
delivery: deliveryStatus,
|
|
247
248
|
peers,
|
|
248
|
-
runtime: runtimeStats
|
|
249
|
+
runtime: runtimeStats,
|
|
250
|
+
_installDir: projectRoot
|
|
249
251
|
};
|
|
250
252
|
}
|
|
251
253
|
|
|
@@ -279,6 +281,15 @@ export function formatRuntimeStatus(status) {
|
|
|
279
281
|
}
|
|
280
282
|
lines.push("");
|
|
281
283
|
|
|
284
|
+
if (status._installDir) {
|
|
285
|
+
const checklist = buildSetupChecklist(status._installDir);
|
|
286
|
+
const allConfigured = Object.values(checklist).every((c) => c.configured);
|
|
287
|
+
if (!allConfigured) {
|
|
288
|
+
lines.push(formatSetupChecklist(checklist));
|
|
289
|
+
lines.push("");
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
282
293
|
// Agents
|
|
283
294
|
lines.push(`--- Agents (${status.agents.length}) ---`);
|
|
284
295
|
for (const agent of status.agents) {
|
|
@@ -466,12 +466,23 @@ export async function whoami(botToken, { fetchImpl = globalThis.fetch, webhookUr
|
|
|
466
466
|
*
|
|
467
467
|
* Returns a stop() function that cleanly terminates the loop.
|
|
468
468
|
*/
|
|
469
|
-
export function startTelegramPolling({ botToken, handler, logger = console, fetchImpl = globalThis.fetch }) {
|
|
469
|
+
export function startTelegramPolling({ botToken, handler, logger = console, fetchImpl = globalThis.fetch, projectRoot = null }) {
|
|
470
470
|
const api = (method) => `https://api.telegram.org/bot${botToken}/${method}`;
|
|
471
471
|
let running = true;
|
|
472
472
|
let offset = 0;
|
|
473
473
|
const stateStore = handler?.store || null;
|
|
474
474
|
|
|
475
|
+
// One-shot flag: true when runtime.toml has chat_id_pending = true
|
|
476
|
+
let pendingChatIdDiscovery = false;
|
|
477
|
+
if (projectRoot) {
|
|
478
|
+
try {
|
|
479
|
+
const rtp = path.join(projectRoot, "config", "runtime.toml");
|
|
480
|
+
if (fs.existsSync(rtp) && fs.readFileSync(rtp, "utf8").includes("chat_id_pending = true")) {
|
|
481
|
+
pendingChatIdDiscovery = true;
|
|
482
|
+
}
|
|
483
|
+
} catch (_) { /* ignore — best effort */ }
|
|
484
|
+
}
|
|
485
|
+
|
|
475
486
|
const persistOffset = async (nextOffset) => {
|
|
476
487
|
if (!stateStore || typeof stateStore.setMeta !== "function") {
|
|
477
488
|
return;
|
|
@@ -529,6 +540,26 @@ export function startTelegramPolling({ botToken, handler, logger = console, fetc
|
|
|
529
540
|
const chatId = update?.message?.chat?.id;
|
|
530
541
|
const messageId = update?.message?.message_id;
|
|
531
542
|
|
|
543
|
+
// Auto-discover chat_id when onboarding left chat_id_pending = true
|
|
544
|
+
if (chatId && pendingChatIdDiscovery) {
|
|
545
|
+
try {
|
|
546
|
+
const { resolvePendingChatId } = await import("../onboarding/phases/telegram.js");
|
|
547
|
+
resolvePendingChatId(projectRoot, String(chatId));
|
|
548
|
+
pendingChatIdDiscovery = false;
|
|
549
|
+
logger.info(JSON.stringify({
|
|
550
|
+
service: "telegram_polling",
|
|
551
|
+
event: "chat_id_auto_discovered",
|
|
552
|
+
chatId: String(chatId),
|
|
553
|
+
}));
|
|
554
|
+
} catch (err) {
|
|
555
|
+
logger.error(JSON.stringify({
|
|
556
|
+
service: "telegram_polling",
|
|
557
|
+
event: "chat_id_discovery_error",
|
|
558
|
+
error: err.message,
|
|
559
|
+
}));
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
532
563
|
// React 👀 on enqueue (job accepted, processing starting)
|
|
533
564
|
if (result?.action === "enqueued" && chatId && messageId) {
|
|
534
565
|
fetchImpl(`https://api.telegram.org/bot${botToken}/setMessageReaction`, {
|