nemoris 0.1.0
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/.env.example +49 -0
- package/LICENSE +21 -0
- package/README.md +209 -0
- package/SECURITY.md +119 -0
- package/bin/nemoris +46 -0
- package/config/agents/agent.toml.example +28 -0
- package/config/agents/default.toml +22 -0
- package/config/agents/orchestrator.toml +18 -0
- package/config/delivery.toml +73 -0
- package/config/embeddings.toml +5 -0
- package/config/identity/default-purpose.md +1 -0
- package/config/identity/default-soul.md +3 -0
- package/config/identity/orchestrator-purpose.md +1 -0
- package/config/identity/orchestrator-soul.md +1 -0
- package/config/improvement-targets.toml +15 -0
- package/config/jobs/heartbeat-check.toml +30 -0
- package/config/jobs/memory-rollup.toml +46 -0
- package/config/jobs/workspace-health.toml +63 -0
- package/config/mcp.toml +16 -0
- package/config/output-contracts.toml +17 -0
- package/config/peers.toml +32 -0
- package/config/peers.toml.example +32 -0
- package/config/policies/memory-default.toml +10 -0
- package/config/policies/memory-heartbeat.toml +5 -0
- package/config/policies/memory-ops.toml +10 -0
- package/config/policies/tools-heartbeat-minimal.toml +8 -0
- package/config/policies/tools-interactive-safe.toml +8 -0
- package/config/policies/tools-ops-bounded.toml +8 -0
- package/config/policies/tools-orchestrator.toml +7 -0
- package/config/providers/anthropic.toml +15 -0
- package/config/providers/ollama.toml +5 -0
- package/config/providers/openai-codex.toml +9 -0
- package/config/providers/openrouter.toml +5 -0
- package/config/router.toml +22 -0
- package/config/runtime.toml +114 -0
- package/config/skills/self-improvement.toml +15 -0
- package/config/skills/telegram-onboarding-spec.md +240 -0
- package/config/skills/workspace-monitor.toml +15 -0
- package/config/task-router.toml +42 -0
- package/install.sh +50 -0
- package/package.json +90 -0
- package/src/auth/auth-profiles.js +169 -0
- package/src/auth/openai-codex-oauth.js +285 -0
- package/src/battle.js +449 -0
- package/src/cli/help.js +265 -0
- package/src/cli/output-filter.js +49 -0
- package/src/cli/runtime-control.js +704 -0
- package/src/cli-main.js +2763 -0
- package/src/cli.js +78 -0
- package/src/config/loader.js +332 -0
- package/src/config/schema-validator.js +214 -0
- package/src/config/toml-lite.js +8 -0
- package/src/daemon/action-handlers.js +71 -0
- package/src/daemon/healing-tick.js +87 -0
- package/src/daemon/health-probes.js +90 -0
- package/src/daemon/notifier.js +57 -0
- package/src/daemon/nurse.js +218 -0
- package/src/daemon/repair-log.js +106 -0
- package/src/daemon/rule-staging.js +90 -0
- package/src/daemon/rules.js +29 -0
- package/src/daemon/telegram-commands.js +54 -0
- package/src/daemon/updater.js +85 -0
- package/src/jobs/job-runner.js +78 -0
- package/src/mcp/consumer.js +129 -0
- package/src/memory/active-recall.js +171 -0
- package/src/memory/backend-manager.js +97 -0
- package/src/memory/backends/file-backend.js +38 -0
- package/src/memory/backends/qmd-backend.js +219 -0
- package/src/memory/embedding-guards.js +24 -0
- package/src/memory/embedding-index.js +118 -0
- package/src/memory/embedding-service.js +179 -0
- package/src/memory/file-index.js +177 -0
- package/src/memory/memory-signature.js +5 -0
- package/src/memory/memory-store.js +648 -0
- package/src/memory/retrieval-planner.js +66 -0
- package/src/memory/scoring.js +145 -0
- package/src/memory/simhash.js +78 -0
- package/src/memory/sqlite-active-store.js +824 -0
- package/src/memory/write-policy.js +36 -0
- package/src/onboarding/aliases.js +33 -0
- package/src/onboarding/auth/api-key.js +224 -0
- package/src/onboarding/auth/ollama-detect.js +42 -0
- package/src/onboarding/clack-prompter.js +77 -0
- package/src/onboarding/doctor.js +530 -0
- package/src/onboarding/lock.js +42 -0
- package/src/onboarding/model-catalog.js +344 -0
- package/src/onboarding/phases/auth.js +589 -0
- package/src/onboarding/phases/build.js +130 -0
- package/src/onboarding/phases/choose.js +82 -0
- package/src/onboarding/phases/detect.js +98 -0
- package/src/onboarding/phases/hatch.js +216 -0
- package/src/onboarding/phases/identity.js +79 -0
- package/src/onboarding/phases/ollama.js +345 -0
- package/src/onboarding/phases/scaffold.js +99 -0
- package/src/onboarding/phases/telegram.js +377 -0
- package/src/onboarding/phases/validate.js +204 -0
- package/src/onboarding/phases/verify.js +206 -0
- package/src/onboarding/platform.js +482 -0
- package/src/onboarding/status-bar.js +95 -0
- package/src/onboarding/templates.js +794 -0
- package/src/onboarding/toml-writer.js +38 -0
- package/src/onboarding/tui.js +250 -0
- package/src/onboarding/uninstall.js +153 -0
- package/src/onboarding/wizard.js +499 -0
- package/src/providers/anthropic.js +168 -0
- package/src/providers/base.js +247 -0
- package/src/providers/circuit-breaker.js +136 -0
- package/src/providers/ollama.js +163 -0
- package/src/providers/openai-codex.js +149 -0
- package/src/providers/openrouter.js +136 -0
- package/src/providers/registry.js +36 -0
- package/src/providers/router.js +16 -0
- package/src/runtime/bootstrap-cache.js +47 -0
- package/src/runtime/capabilities-prompt.js +25 -0
- package/src/runtime/completion-ping.js +99 -0
- package/src/runtime/config-validator.js +121 -0
- package/src/runtime/context-ledger.js +360 -0
- package/src/runtime/cutover-readiness.js +42 -0
- package/src/runtime/daemon.js +729 -0
- package/src/runtime/delivery-ack.js +195 -0
- package/src/runtime/delivery-adapters/local-file.js +41 -0
- package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
- package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
- package/src/runtime/delivery-adapters/shadow.js +13 -0
- package/src/runtime/delivery-adapters/standalone-http.js +98 -0
- package/src/runtime/delivery-adapters/telegram.js +104 -0
- package/src/runtime/delivery-adapters/tui.js +128 -0
- package/src/runtime/delivery-manager.js +807 -0
- package/src/runtime/delivery-store.js +168 -0
- package/src/runtime/dependency-health.js +118 -0
- package/src/runtime/envelope.js +114 -0
- package/src/runtime/evaluation.js +1089 -0
- package/src/runtime/exec-approvals.js +216 -0
- package/src/runtime/executor.js +500 -0
- package/src/runtime/failure-ping.js +67 -0
- package/src/runtime/flows.js +83 -0
- package/src/runtime/guards.js +45 -0
- package/src/runtime/handoff.js +51 -0
- package/src/runtime/identity-cache.js +28 -0
- package/src/runtime/improvement-engine.js +109 -0
- package/src/runtime/improvement-harness.js +581 -0
- package/src/runtime/input-sanitiser.js +72 -0
- package/src/runtime/interaction-contract.js +347 -0
- package/src/runtime/lane-readiness.js +226 -0
- package/src/runtime/migration.js +323 -0
- package/src/runtime/model-resolution.js +78 -0
- package/src/runtime/network.js +64 -0
- package/src/runtime/notification-store.js +97 -0
- package/src/runtime/notifier.js +256 -0
- package/src/runtime/orchestrator.js +53 -0
- package/src/runtime/orphan-reaper.js +41 -0
- package/src/runtime/output-contract-schema.js +139 -0
- package/src/runtime/output-contract-validator.js +439 -0
- package/src/runtime/peer-readiness.js +69 -0
- package/src/runtime/peer-registry.js +133 -0
- package/src/runtime/pilot-status.js +108 -0
- package/src/runtime/prompt-builder.js +261 -0
- package/src/runtime/provider-attempt.js +582 -0
- package/src/runtime/report-fallback.js +71 -0
- package/src/runtime/result-normalizer.js +183 -0
- package/src/runtime/retention.js +74 -0
- package/src/runtime/review.js +244 -0
- package/src/runtime/route-job.js +15 -0
- package/src/runtime/run-store.js +38 -0
- package/src/runtime/schedule.js +88 -0
- package/src/runtime/scheduler-state.js +434 -0
- package/src/runtime/scheduler.js +656 -0
- package/src/runtime/session-compactor.js +182 -0
- package/src/runtime/session-search.js +155 -0
- package/src/runtime/slack-inbound.js +249 -0
- package/src/runtime/ssrf.js +102 -0
- package/src/runtime/status-aggregator.js +330 -0
- package/src/runtime/task-contract.js +140 -0
- package/src/runtime/task-packet.js +107 -0
- package/src/runtime/task-router.js +140 -0
- package/src/runtime/telegram-inbound.js +1565 -0
- package/src/runtime/token-counter.js +134 -0
- package/src/runtime/token-estimator.js +59 -0
- package/src/runtime/tool-loop.js +200 -0
- package/src/runtime/transport-server.js +311 -0
- package/src/runtime/tui-server.js +411 -0
- package/src/runtime/ulid.js +44 -0
- package/src/security/ssrf-check.js +197 -0
- package/src/setup.js +369 -0
- package/src/shadow/bridge.js +303 -0
- package/src/skills/loader.js +84 -0
- package/src/tools/catalog.json +49 -0
- package/src/tools/cli-delegate.js +44 -0
- package/src/tools/mcp-client.js +106 -0
- package/src/tools/micro/cancel-task.js +6 -0
- package/src/tools/micro/complete-task.js +6 -0
- package/src/tools/micro/fail-task.js +6 -0
- package/src/tools/micro/http-fetch.js +74 -0
- package/src/tools/micro/index.js +36 -0
- package/src/tools/micro/lcm-recall.js +60 -0
- package/src/tools/micro/list-dir.js +17 -0
- package/src/tools/micro/list-skills.js +46 -0
- package/src/tools/micro/load-skill.js +38 -0
- package/src/tools/micro/memory-search.js +45 -0
- package/src/tools/micro/read-file.js +11 -0
- package/src/tools/micro/session-search.js +54 -0
- package/src/tools/micro/shell-exec.js +43 -0
- package/src/tools/micro/trigger-job.js +79 -0
- package/src/tools/micro/web-search.js +58 -0
- package/src/tools/micro/workspace-paths.js +39 -0
- package/src/tools/micro/write-file.js +14 -0
- package/src/tools/micro/write-memory.js +41 -0
- package/src/tools/registry.js +348 -0
- package/src/tools/tool-result-contract.js +36 -0
- package/src/tui/chat.js +835 -0
- package/src/tui/renderer.js +175 -0
- package/src/tui/socket-client.js +217 -0
- package/src/utils/canonical-json.js +29 -0
- package/src/utils/compaction.js +30 -0
- package/src/utils/env-loader.js +5 -0
- package/src/utils/errors.js +80 -0
- package/src/utils/fs.js +101 -0
- package/src/utils/ids.js +5 -0
- package/src/utils/model-context-limits.js +30 -0
- package/src/utils/token-budget.js +74 -0
- package/src/utils/usage-cost.js +25 -0
- package/src/utils/usage-metrics.js +14 -0
- package/vendor/smol-toml-1.5.2.tgz +0 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Spec: Telegram Onboarding TUI Phase
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Add Telegram setup as an optional sub-phase inside the Build phase (Phase 3) of the onboarding wizard. Users can connect Telegram during `nemoris init` or defer to `nemoris setup telegram` later.
|
|
6
|
+
|
|
7
|
+
## Placement
|
|
8
|
+
|
|
9
|
+
Inside `buildFresh()` and `buildShadow()` in `src/onboarding/phases/build.js`, after auth completes:
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
scaffold → identity → auth → telegram (optional) → done
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Not a new top-level phase. Lock file carries `telegramConfigured: boolean` and `telegramVerified: boolean` in wizard state — no phase index change.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Interactive Flow
|
|
20
|
+
|
|
21
|
+
### Gate
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
Connect Telegram? (Y/n)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
- `n` → skip, `state.telegramConfigured = false`
|
|
28
|
+
- `Y` → enter sub-flow
|
|
29
|
+
|
|
30
|
+
### Step 1: Bot Token
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
Telegram Bot Token (from @BotFather): ●●●●●●
|
|
34
|
+
✓ Token validated @kodi_nemoris_bot
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
- Input via `promptSecret()`
|
|
38
|
+
- Validate by calling Telegram `getMe` API — new function in `telegram-inbound.js`:
|
|
39
|
+
```js
|
|
40
|
+
export async function getMe(botToken, { fetchImpl = globalThis.fetch } = {}) {
|
|
41
|
+
const res = await fetchImpl(`https://api.telegram.org/bot${botToken}/getMe`, { method: "GET" });
|
|
42
|
+
const body = await res.json();
|
|
43
|
+
if (!body.ok) return { ok: false, error: body.description || "getMe failed" };
|
|
44
|
+
return { ok: true, username: body.result.username, firstName: body.result.first_name };
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
- On failure: show error, offer re-entry or skip
|
|
48
|
+
- On success: write token to `.env` as `NEMORIS_TELEGRAM_BOT_TOKEN=<value>`
|
|
49
|
+
- macOS only: call `launchctl setenv NEMORIS_TELEGRAM_BOT_TOKEN <value>` so the running daemon picks it up without restart. Guard with `process.platform === "darwin"`.
|
|
50
|
+
|
|
51
|
+
### Step 2: Chat ID Discovery (daemon-aware)
|
|
52
|
+
|
|
53
|
+
Two paths based on whether the daemon is currently running.
|
|
54
|
+
|
|
55
|
+
#### Detection
|
|
56
|
+
|
|
57
|
+
Check if daemon is running:
|
|
58
|
+
1. macOS: `launchctl list ai.nanoclaw.daemon` — exit code 0 means loaded
|
|
59
|
+
2. Fallback: check for `state/daemon.pid` or similar runtime indicator
|
|
60
|
+
|
|
61
|
+
#### Path A: Daemon is running
|
|
62
|
+
|
|
63
|
+
```
|
|
64
|
+
Daemon is running. Send any message to @kodi_nemoris_bot on Telegram...
|
|
65
|
+
Waiting for your message...
|
|
66
|
+
✓ Found you chat_id: 7781763328, @leeUsername
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- Poll the SQLite state store (`state/active.db`) for the first interactive job row where `source = 'telegram'`
|
|
70
|
+
- Query: `SELECT chat_id FROM interactive_jobs WHERE source = 'telegram' ORDER BY created_at DESC LIMIT 1`
|
|
71
|
+
- Poll interval: 2s, timeout: 120s
|
|
72
|
+
- On timeout: offer to enter chat_id manually or retry
|
|
73
|
+
|
|
74
|
+
This avoids any conflict with the daemon's getUpdates polling connection. No `deleteWebhook`, no `getUpdates` — the daemon handles message ingestion, the wizard just reads the result.
|
|
75
|
+
|
|
76
|
+
#### Path B: Daemon is not running
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
Send any message to @kodi_nemoris_bot on Telegram, then press Enter...
|
|
80
|
+
✓ Found you chat_id: 7781763328, @leeUsername
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
- Reuses existing `whoami(botToken)` function (deleteWebhook → getUpdates → extract chat)
|
|
84
|
+
- No conflict because no daemon is competing for getUpdates
|
|
85
|
+
- On failure (no messages found): prompt user to send a message first and press Enter to retry
|
|
86
|
+
|
|
87
|
+
#### Both paths
|
|
88
|
+
|
|
89
|
+
- Auto-fill `operator_chat_id` and `authorized_chat_ids[0]` with discovered chat_id
|
|
90
|
+
- If chat_id was already in `config/runtime.toml`, show it and ask to confirm or replace
|
|
91
|
+
|
|
92
|
+
### Step 3: Delivery Mode
|
|
93
|
+
|
|
94
|
+
```
|
|
95
|
+
Delivery mode:
|
|
96
|
+
[1] Long polling (recommended — no tunnel needed)
|
|
97
|
+
[2] Webhook (requires public URL)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
- Uses `select()` from tui.js
|
|
101
|
+
- **Polling** (default): `polling_mode = true`, `webhook_url = ""`
|
|
102
|
+
- **Webhook**: prompts for public URL, validates format, calls `registerWebhook(token, url)`. On failure: show error, offer retry or fall back to polling.
|
|
103
|
+
|
|
104
|
+
### Step 4: Write Config
|
|
105
|
+
|
|
106
|
+
Writes the `[telegram]` section to `config/runtime.toml`:
|
|
107
|
+
|
|
108
|
+
```toml
|
|
109
|
+
[telegram]
|
|
110
|
+
bot_token_env = "NEMORIS_TELEGRAM_BOT_TOKEN"
|
|
111
|
+
polling_mode = true
|
|
112
|
+
webhook_url = ""
|
|
113
|
+
operator_chat_id = "7781763328"
|
|
114
|
+
authorized_chat_ids = ["7781763328"]
|
|
115
|
+
default_agent = "kodi"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
- `default_agent` is set to `state.agentId` from the Build phase
|
|
119
|
+
- Preserves all other sections in runtime.toml (read → patch → write)
|
|
120
|
+
- Uses TOML serialization consistent with existing config writer patterns
|
|
121
|
+
|
|
122
|
+
### Step 5: Smoke Test (with failure handling)
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
Sending test message...
|
|
126
|
+
✓ Telegram connected "Hello from Nemoris"
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Sends a test message via `sendMessage` to the discovered `chat_id` using the validated token.
|
|
130
|
+
|
|
131
|
+
#### Failure path
|
|
132
|
+
|
|
133
|
+
```
|
|
134
|
+
Sending test message...
|
|
135
|
+
✗ Delivery failed: 403 Forbidden — bot was blocked by the user
|
|
136
|
+
|
|
137
|
+
[1] Retry
|
|
138
|
+
[2] Skip — finish setup without verification
|
|
139
|
+
[3] Re-enter bot token
|
|
140
|
+
|
|
141
|
+
Choice:
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
- **Retry**: loops back to sendMessage
|
|
145
|
+
- **Skip**: sets `state.telegramConfigured = true`, `state.telegramVerified = false`. Verify phase will show a warning: `"⚠ Telegram configured but not verified — run nemoris setup telegram to test"`
|
|
146
|
+
- **Re-enter token**: loops back to Step 1
|
|
147
|
+
|
|
148
|
+
The wizard does NOT exit cleanly on a failed smoke test. It always gives the user a choice.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
## Non-Interactive Mode
|
|
153
|
+
|
|
154
|
+
Env-var driven, zero prompts. Suitable for Docker/CI.
|
|
155
|
+
|
|
156
|
+
| Env Var | Required | Default | Purpose |
|
|
157
|
+
|---------|----------|---------|---------|
|
|
158
|
+
| `NEMORIS_TELEGRAM_BOT_TOKEN` | Yes (to enable) | — | If set, Telegram phase runs |
|
|
159
|
+
| `NEMORIS_TELEGRAM_CHAT_ID` | No | — | Skips whoami/state-store probe |
|
|
160
|
+
| `NEMORIS_TELEGRAM_MODE` | No | `polling` | `polling` or `webhook` |
|
|
161
|
+
| `NEMORIS_TELEGRAM_WEBHOOK_URL` | If mode=webhook | — | Public URL for webhook registration |
|
|
162
|
+
| `NEMORIS_SKIP_TELEGRAM` | No | — | Set to `true` to skip entirely |
|
|
163
|
+
|
|
164
|
+
If `NEMORIS_TELEGRAM_BOT_TOKEN` is not set, Telegram phase is silently skipped. No error.
|
|
165
|
+
|
|
166
|
+
If `NEMORIS_TELEGRAM_CHAT_ID` is not set and daemon is not running, the whoami probe runs automatically (no stdin needed). If it returns null, Telegram is configured without a chat_id — the user must set it manually later.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## Verify Phase Changes
|
|
171
|
+
|
|
172
|
+
### Telegram configured + verified
|
|
173
|
+
|
|
174
|
+
```
|
|
175
|
+
What's next:
|
|
176
|
+
|
|
177
|
+
nemoris start start the daemon
|
|
178
|
+
nemoris status see your agent's state
|
|
179
|
+
Message @kodi_nemoris_bot talk to your agent via Telegram
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
### Telegram configured + NOT verified
|
|
183
|
+
|
|
184
|
+
```
|
|
185
|
+
⚠ Telegram configured but not verified
|
|
186
|
+
|
|
187
|
+
What's next:
|
|
188
|
+
|
|
189
|
+
nemoris setup telegram verify Telegram connection
|
|
190
|
+
nemoris start start the daemon
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Telegram not configured
|
|
194
|
+
|
|
195
|
+
```
|
|
196
|
+
What's next:
|
|
197
|
+
|
|
198
|
+
nemoris start start the daemon
|
|
199
|
+
nemoris status see your agent's state
|
|
200
|
+
nemoris setup telegram connect Telegram later
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## CLI Refactor
|
|
206
|
+
|
|
207
|
+
Extract shared logic from `setupTelegram()` and `telegramWhoami()` in `cli.js` so both the wizard phase and the standalone commands use the same code paths:
|
|
208
|
+
|
|
209
|
+
- `validateBotToken(token)` → calls `getMe`, returns `{ ok, username, error }`
|
|
210
|
+
- `discoverChatId(token, { daemonRunning, stateStore, webhookUrl })` → daemon-aware chat_id discovery
|
|
211
|
+
- `writeTelegramConfig(installDir, config)` → patches `[telegram]` section in runtime.toml
|
|
212
|
+
- `sendTestMessage(token, chatId)` → smoke test delivery
|
|
213
|
+
|
|
214
|
+
These live in `src/onboarding/phases/telegram.js` and are imported by `cli.js`.
|
|
215
|
+
|
|
216
|
+
The standalone `nemoris setup telegram` command becomes a thin wrapper that calls the same phase functions with an interactive readline session, outside the wizard context.
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Files
|
|
221
|
+
|
|
222
|
+
| File | Change |
|
|
223
|
+
|------|--------|
|
|
224
|
+
| `src/onboarding/phases/telegram.js` | **New** — Telegram sub-phase: token validation, daemon-aware chat_id discovery, mode selection, config writing, smoke test with failure handling |
|
|
225
|
+
| `src/onboarding/phases/build.js` | Import and call `runTelegramPhase()` after auth in both `buildFresh()` and `buildShadow()` |
|
|
226
|
+
| `src/onboarding/phases/verify.js` | Conditional "What's Next" based on `telegramConfigured` / `telegramVerified` |
|
|
227
|
+
| `src/onboarding/tui.js` | Add `waitForEnter(message)` helper |
|
|
228
|
+
| `src/runtime/telegram-inbound.js` | Add `getMe(botToken)` function |
|
|
229
|
+
| `src/cli.js` | Refactor `setupTelegram()` / `telegramWhoami()` to use shared functions from `telegram.js` |
|
|
230
|
+
|
|
231
|
+
## Scope
|
|
232
|
+
|
|
233
|
+
- `telegram.js`: ~150 lines
|
|
234
|
+
- `build.js` changes: ~15 lines
|
|
235
|
+
- `verify.js` changes: ~15 lines
|
|
236
|
+
- `telegram-inbound.js` addition: ~10 lines
|
|
237
|
+
- `tui.js` addition: ~10 lines
|
|
238
|
+
- `cli.js` refactor: ~30 lines changed
|
|
239
|
+
|
|
240
|
+
Total: ~200 lines new, ~60 lines modified. One new file.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
[skill]
|
|
2
|
+
id = "workspace_monitor"
|
|
3
|
+
description = "Monitor workspace directory for changes and summarise"
|
|
4
|
+
agent_scope = ["ops", "main"]
|
|
5
|
+
|
|
6
|
+
[skill.context]
|
|
7
|
+
prompt = "You are monitoring a workspace directory for changes. Compare current state against last known checkpoint. Report: new files, modified files, deleted files, key content changes. Keep summary under 200 words."
|
|
8
|
+
|
|
9
|
+
[skill.tools]
|
|
10
|
+
required = ["read_file", "list_dir"]
|
|
11
|
+
optional = ["shell_exec"]
|
|
12
|
+
|
|
13
|
+
[skill.budget]
|
|
14
|
+
max_tokens = 4096
|
|
15
|
+
max_tool_calls = 10
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[defaults]
|
|
2
|
+
enabled = true
|
|
3
|
+
default_route_mode = "primary"
|
|
4
|
+
|
|
5
|
+
[rules.local_reporting]
|
|
6
|
+
description = "Keep bounded reporting jobs on the stronger local reporting lane."
|
|
7
|
+
target_lane = "local_report"
|
|
8
|
+
match_task_types = ["workspace_health", "memory_rollup"]
|
|
9
|
+
priority = 90
|
|
10
|
+
|
|
11
|
+
[rules.coding_work]
|
|
12
|
+
description = "Escalate code-edit and repo-fix work to a stronger coding lane before model resolution."
|
|
13
|
+
target_lane = "job_heavy"
|
|
14
|
+
route_mode = "primary"
|
|
15
|
+
match_keywords = ["code", "coding", "repo", "repository", "bug", "fix", "patch", "refactor", "test", "file fix"]
|
|
16
|
+
match_task_types = ["code_fix", "repo_maintenance", "code_review", "test_repair"]
|
|
17
|
+
require_tools = ["apply_patch"]
|
|
18
|
+
priority = 100
|
|
19
|
+
|
|
20
|
+
[rules.memory_heavy_reporting]
|
|
21
|
+
description = "Use the report lane for memory-heavy summaries and handoff work."
|
|
22
|
+
target_lane = "local_report"
|
|
23
|
+
route_mode = "primary"
|
|
24
|
+
match_keywords = ["summary", "rollup", "handoff", "memory", "backlog", "projects"]
|
|
25
|
+
match_task_types = ["memory_rollup", "handoff_summary", "project_rollup"]
|
|
26
|
+
priority = 80
|
|
27
|
+
|
|
28
|
+
[rules.user_visible_handoff]
|
|
29
|
+
description = "Promote cheap local maintenance work to the stronger report lane when it owes the user a visible handoff or pingback."
|
|
30
|
+
target_lane = "local_report"
|
|
31
|
+
route_mode = "primary"
|
|
32
|
+
match_task_types = ["heartbeat", "heartbeat_check", "light_maintenance", "classification", "triage"]
|
|
33
|
+
require_pingback = true
|
|
34
|
+
priority = 70
|
|
35
|
+
|
|
36
|
+
[rules.cheap_maintenance]
|
|
37
|
+
description = "Keep low-risk heartbeat and maintenance loops on the cheap local lane."
|
|
38
|
+
target_lane = "local_cheap"
|
|
39
|
+
route_mode = "primary"
|
|
40
|
+
match_keywords = ["heartbeat", "status", "maintenance", "triage", "check"]
|
|
41
|
+
match_task_types = ["heartbeat", "heartbeat_check", "light_maintenance", "classification", "triage"]
|
|
42
|
+
priority = 40
|
package/install.sh
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
echo ""
|
|
5
|
+
echo " 🌿 Nemoris Installer"
|
|
6
|
+
echo ""
|
|
7
|
+
|
|
8
|
+
OS="unknown"
|
|
9
|
+
case "$(uname -s)" in
|
|
10
|
+
Darwin*) OS="macOS" ;;
|
|
11
|
+
Linux*) OS="Linux" ;;
|
|
12
|
+
MINGW*|MSYS*|CYGWIN*) OS="Windows (WSL recommended)" ;;
|
|
13
|
+
esac
|
|
14
|
+
echo " Platform: $OS ($(uname -m))"
|
|
15
|
+
|
|
16
|
+
check_node() {
|
|
17
|
+
if ! command -v node &>/dev/null; then
|
|
18
|
+
return 1
|
|
19
|
+
fi
|
|
20
|
+
local major
|
|
21
|
+
major=$(node -e "console.log(process.version.split('.')[0].replace('v',''))")
|
|
22
|
+
[ "$major" -ge 22 ]
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if check_node; then
|
|
26
|
+
echo " Node.js: $(node --version) ✓"
|
|
27
|
+
else
|
|
28
|
+
echo " Node.js >= 22.5 required."
|
|
29
|
+
if command -v nvm &>/dev/null; then
|
|
30
|
+
echo " nvm detected — installing Node.js 22..."
|
|
31
|
+
nvm install 22
|
|
32
|
+
nvm use 22
|
|
33
|
+
else
|
|
34
|
+
echo " Install Node.js 22+: https://nodejs.org"
|
|
35
|
+
echo " Or install nvm: curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash"
|
|
36
|
+
exit 1
|
|
37
|
+
fi
|
|
38
|
+
fi
|
|
39
|
+
|
|
40
|
+
if ! command -v git &>/dev/null; then
|
|
41
|
+
echo " ⚠ Git required: https://git-scm.com"
|
|
42
|
+
exit 1
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
echo ""
|
|
46
|
+
echo " Installing nemoris..."
|
|
47
|
+
npm install -g nemoris
|
|
48
|
+
|
|
49
|
+
echo ""
|
|
50
|
+
nemoris setup
|
package/package.json
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nemoris",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"description": "Personal AI agent runtime — persistent memory, delivery guarantees, task contracts, self-healing. Local-first, no cloud.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Lee <amzer24@gmail.com> (https://github.com/amzer24)",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/amzer24/nemoris.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://nemoris.dev",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/amzer24/nemoris/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"ai",
|
|
18
|
+
"agent",
|
|
19
|
+
"runtime",
|
|
20
|
+
"personal-agent",
|
|
21
|
+
"llm",
|
|
22
|
+
"anthropic",
|
|
23
|
+
"claude",
|
|
24
|
+
"ollama",
|
|
25
|
+
"telegram",
|
|
26
|
+
"memory",
|
|
27
|
+
"self-healing",
|
|
28
|
+
"daemon",
|
|
29
|
+
"local-first",
|
|
30
|
+
"mcp"
|
|
31
|
+
],
|
|
32
|
+
"bin": {
|
|
33
|
+
"nemoris": "bin/nemoris"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
".env.example",
|
|
37
|
+
"bin/",
|
|
38
|
+
"src/",
|
|
39
|
+
"config/",
|
|
40
|
+
"vendor/",
|
|
41
|
+
"install.sh",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"README.md",
|
|
44
|
+
"SECURITY.md"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"init": "node src/cli.js init",
|
|
48
|
+
"demo": "node src/cli.js demo",
|
|
49
|
+
"run:heartbeat": "node src/cli.js execute-job heartbeat-check dry-run",
|
|
50
|
+
"run:heartbeat:provider": "NEMORIS_ALLOW_PROVIDER_MODE=1 node src/cli.js execute-job heartbeat-check provider",
|
|
51
|
+
"compare:heartbeat": "node src/cli.js shadow-compare heartbeat-check",
|
|
52
|
+
"scheduler:due": "node src/cli.js due-jobs",
|
|
53
|
+
"scheduler:tick": "node src/cli.js tick-scheduler dry-run",
|
|
54
|
+
"runs:review": "node src/cli.js review-runs 10",
|
|
55
|
+
"runs:evaluate": "node src/cli.js evaluate-runs 20",
|
|
56
|
+
"embeddings:index": "NEMORIS_ALLOW_EMBEDDINGS=1 node src/cli.js index-embeddings heartbeat",
|
|
57
|
+
"embeddings:query": "NEMORIS_ALLOW_EMBEDDINGS=1 node src/cli.js query-embeddings heartbeat \"heartbeat memory\"",
|
|
58
|
+
"memory:backends": "node src/cli.js memory-backends heartbeat",
|
|
59
|
+
"memory:qmd": "node src/cli.js query-qmd heartbeat \"memory heartbeat\"",
|
|
60
|
+
"memory:plan": "node src/cli.js plan-job heartbeat-check",
|
|
61
|
+
"inspect": "node src/cli.js inspect-memory main \"memory heartbeat\"",
|
|
62
|
+
"manifest:summary": "node src/cli.js manifest-summary",
|
|
63
|
+
"plan:heartbeat": "node src/cli.js plan-job heartbeat-check",
|
|
64
|
+
"cron:live": "node src/cli.js live-cron-summary",
|
|
65
|
+
"compare:jobs": "node src/cli.js compare-jobs",
|
|
66
|
+
"shadow:summary": "node src/cli.js shadow-summary main",
|
|
67
|
+
"shadow:import": "node src/cli.js shadow-import main",
|
|
68
|
+
"provider:policy": "node src/cli.js provider-mode-policy",
|
|
69
|
+
"setup": "node src/cli.js setup",
|
|
70
|
+
"battle": "NEMORIS_ALLOW_PROVIDER_MODE=1 node src/cli.js battle",
|
|
71
|
+
"lint": "eslint .",
|
|
72
|
+
"publish:check": "node scripts/check-publish-dry-run.js",
|
|
73
|
+
"status": "node src/cli.js runtime-status",
|
|
74
|
+
"test": "node --test",
|
|
75
|
+
"test:e2e": "node tests/e2e/run-report.js"
|
|
76
|
+
},
|
|
77
|
+
"engines": {
|
|
78
|
+
"node": ">=22.5.0"
|
|
79
|
+
},
|
|
80
|
+
"dependencies": {
|
|
81
|
+
"@clack/prompts": "^1.1.0",
|
|
82
|
+
"@mariozechner/pi-ai": "^0.60.0",
|
|
83
|
+
"js-tiktoken": "^1.0.21",
|
|
84
|
+
"smol-toml": "file:vendor/smol-toml-1.5.2.tgz"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@eslint/js": "^10.0.1",
|
|
88
|
+
"eslint": "^10.0.3"
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
export const AUTH_PROFILE_FILE_NAME = "auth-profiles.json";
|
|
6
|
+
export const AUTH_PROFILE_DIR_NAME = "state";
|
|
7
|
+
|
|
8
|
+
function dedupe(items = []) {
|
|
9
|
+
return [...new Set(items.filter(Boolean))];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveInstallDir({ env = process.env, cwd: _cwd = process.cwd() } = {}) {
|
|
13
|
+
if (env.NEMORIS_INSTALL_DIR) {
|
|
14
|
+
return env.NEMORIS_INSTALL_DIR;
|
|
15
|
+
}
|
|
16
|
+
if (_cwd) {
|
|
17
|
+
const hasPackageJson = fs.existsSync(path.join(_cwd, "package.json"));
|
|
18
|
+
const hasCliEntry = fs.existsSync(path.join(_cwd, "src", "cli.js"));
|
|
19
|
+
if (hasPackageJson && hasCliEntry) {
|
|
20
|
+
return _cwd;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return path.join(os.homedir(), ".nemoris");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function resolveAuthProfilesPath({ env = process.env, cwd = process.cwd() } = {}) {
|
|
27
|
+
if (env.NEMORIS_AUTH_PROFILES_PATH) {
|
|
28
|
+
return path.resolve(env.NEMORIS_AUTH_PROFILES_PATH);
|
|
29
|
+
}
|
|
30
|
+
const installDir = resolveInstallDir({ env, cwd });
|
|
31
|
+
return path.join(installDir, AUTH_PROFILE_DIR_NAME, AUTH_PROFILE_FILE_NAME);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function defaultAuthProfilesPath() {
|
|
35
|
+
return resolveAuthProfilesPath();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function defaultAuthProfileSearchPaths({ env = process.env, cwd = process.cwd() } = {}) {
|
|
39
|
+
const installDir = resolveInstallDir({ env, cwd });
|
|
40
|
+
return dedupe([
|
|
41
|
+
resolveAuthProfilesPath({ env, cwd }),
|
|
42
|
+
path.join(cwd, AUTH_PROFILE_DIR_NAME, AUTH_PROFILE_FILE_NAME),
|
|
43
|
+
path.join(installDir, AUTH_PROFILE_DIR_NAME, AUTH_PROFILE_FILE_NAME),
|
|
44
|
+
path.join(os.homedir(), ".openclaw", "agents", "main", "agent", AUTH_PROFILE_FILE_NAME)
|
|
45
|
+
]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function readJsonFile(filePath) {
|
|
49
|
+
try {
|
|
50
|
+
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function secureChmod(filePath) {
|
|
57
|
+
if (process.platform === "win32") return;
|
|
58
|
+
try {
|
|
59
|
+
fs.chmodSync(filePath, 0o600);
|
|
60
|
+
} catch {
|
|
61
|
+
// Best effort only.
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function writeFileAtomic(filePath, content) {
|
|
66
|
+
const dirPath = path.dirname(filePath);
|
|
67
|
+
fs.mkdirSync(dirPath, { recursive: true, mode: 0o700 });
|
|
68
|
+
const tempPath = path.join(dirPath, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
69
|
+
fs.writeFileSync(tempPath, content, { encoding: "utf8", mode: 0o600 });
|
|
70
|
+
secureChmod(tempPath);
|
|
71
|
+
fs.renameSync(tempPath, filePath);
|
|
72
|
+
secureChmod(filePath);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function resolveEnvRef(ref) {
|
|
76
|
+
if (!ref || typeof ref !== "object") return null;
|
|
77
|
+
if (ref.source !== "env" || !ref.id) return null;
|
|
78
|
+
return process.env[ref.id] || null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function readAuthProfiles(filePath = defaultAuthProfilesPath()) {
|
|
82
|
+
const parsed = readJsonFile(filePath);
|
|
83
|
+
const profiles = parsed?.profiles && typeof parsed.profiles === "object" ? parsed.profiles : {};
|
|
84
|
+
return {
|
|
85
|
+
version: Number(parsed?.version || 1),
|
|
86
|
+
profiles
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function writeAuthProfiles(document, filePath = defaultAuthProfilesPath()) {
|
|
91
|
+
writeFileAtomic(filePath, `${JSON.stringify(document, null, 2)}\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function updateAuthProfile(profileId, updater, filePath = defaultAuthProfilesPath()) {
|
|
95
|
+
const document = readAuthProfiles(filePath);
|
|
96
|
+
const nextProfile = updater(document.profiles?.[profileId] || null);
|
|
97
|
+
if (nextProfile === null) {
|
|
98
|
+
delete document.profiles[profileId];
|
|
99
|
+
} else {
|
|
100
|
+
document.profiles[profileId] = nextProfile;
|
|
101
|
+
}
|
|
102
|
+
writeAuthProfiles(document, filePath);
|
|
103
|
+
return document;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function upsertAuthProfile(profileId, profile, filePath = defaultAuthProfilesPath()) {
|
|
107
|
+
return updateAuthProfile(profileId, () => profile, filePath);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function getAuthProfile(profileId, filePath = defaultAuthProfilesPath()) {
|
|
111
|
+
return readAuthProfiles(filePath).profiles?.[profileId] || null;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export function resolveProfileSecret(profile) {
|
|
115
|
+
if (!profile || typeof profile !== "object") return null;
|
|
116
|
+
if (profile.type === "oauth") return profile.access || resolveEnvRef(profile.accessRef) || null;
|
|
117
|
+
if (profile.type === "api_key") return profile.key || resolveEnvRef(profile.keyRef) || null;
|
|
118
|
+
if (profile.type === "token") return profile.token || resolveEnvRef(profile.tokenRef) || null;
|
|
119
|
+
return (
|
|
120
|
+
profile.access ||
|
|
121
|
+
profile.key ||
|
|
122
|
+
profile.token ||
|
|
123
|
+
resolveEnvRef(profile.accessRef) ||
|
|
124
|
+
resolveEnvRef(profile.keyRef) ||
|
|
125
|
+
resolveEnvRef(profile.tokenRef) ||
|
|
126
|
+
null
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function describeAuthRef(authRef, { filePath = defaultAuthProfilesPath() } = {}) {
|
|
131
|
+
if (!authRef) {
|
|
132
|
+
return {
|
|
133
|
+
authRef: null,
|
|
134
|
+
present: false,
|
|
135
|
+
source: null,
|
|
136
|
+
detail: "missing_auth_ref"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (String(authRef).startsWith("env:")) {
|
|
141
|
+
const envName = String(authRef).slice(4);
|
|
142
|
+
return {
|
|
143
|
+
authRef,
|
|
144
|
+
present: Boolean(process.env[envName]),
|
|
145
|
+
source: "env",
|
|
146
|
+
envName,
|
|
147
|
+
detail: process.env[envName] ? "ok" : `missing ${envName}`
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (String(authRef).startsWith("profile:")) {
|
|
152
|
+
const profileId = String(authRef).slice("profile:".length);
|
|
153
|
+
const profile = getAuthProfile(profileId, filePath);
|
|
154
|
+
return {
|
|
155
|
+
authRef,
|
|
156
|
+
present: Boolean(resolveProfileSecret(profile)),
|
|
157
|
+
source: "profile",
|
|
158
|
+
profileId,
|
|
159
|
+
detail: profile ? "ok" : `missing profile ${profileId}`
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
authRef,
|
|
165
|
+
present: false,
|
|
166
|
+
source: "unknown",
|
|
167
|
+
detail: "unsupported_auth_ref"
|
|
168
|
+
};
|
|
169
|
+
}
|