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.
Files changed (223) hide show
  1. package/.env.example +49 -0
  2. package/LICENSE +21 -0
  3. package/README.md +209 -0
  4. package/SECURITY.md +119 -0
  5. package/bin/nemoris +46 -0
  6. package/config/agents/agent.toml.example +28 -0
  7. package/config/agents/default.toml +22 -0
  8. package/config/agents/orchestrator.toml +18 -0
  9. package/config/delivery.toml +73 -0
  10. package/config/embeddings.toml +5 -0
  11. package/config/identity/default-purpose.md +1 -0
  12. package/config/identity/default-soul.md +3 -0
  13. package/config/identity/orchestrator-purpose.md +1 -0
  14. package/config/identity/orchestrator-soul.md +1 -0
  15. package/config/improvement-targets.toml +15 -0
  16. package/config/jobs/heartbeat-check.toml +30 -0
  17. package/config/jobs/memory-rollup.toml +46 -0
  18. package/config/jobs/workspace-health.toml +63 -0
  19. package/config/mcp.toml +16 -0
  20. package/config/output-contracts.toml +17 -0
  21. package/config/peers.toml +32 -0
  22. package/config/peers.toml.example +32 -0
  23. package/config/policies/memory-default.toml +10 -0
  24. package/config/policies/memory-heartbeat.toml +5 -0
  25. package/config/policies/memory-ops.toml +10 -0
  26. package/config/policies/tools-heartbeat-minimal.toml +8 -0
  27. package/config/policies/tools-interactive-safe.toml +8 -0
  28. package/config/policies/tools-ops-bounded.toml +8 -0
  29. package/config/policies/tools-orchestrator.toml +7 -0
  30. package/config/providers/anthropic.toml +15 -0
  31. package/config/providers/ollama.toml +5 -0
  32. package/config/providers/openai-codex.toml +9 -0
  33. package/config/providers/openrouter.toml +5 -0
  34. package/config/router.toml +22 -0
  35. package/config/runtime.toml +114 -0
  36. package/config/skills/self-improvement.toml +15 -0
  37. package/config/skills/telegram-onboarding-spec.md +240 -0
  38. package/config/skills/workspace-monitor.toml +15 -0
  39. package/config/task-router.toml +42 -0
  40. package/install.sh +50 -0
  41. package/package.json +90 -0
  42. package/src/auth/auth-profiles.js +169 -0
  43. package/src/auth/openai-codex-oauth.js +285 -0
  44. package/src/battle.js +449 -0
  45. package/src/cli/help.js +265 -0
  46. package/src/cli/output-filter.js +49 -0
  47. package/src/cli/runtime-control.js +704 -0
  48. package/src/cli-main.js +2763 -0
  49. package/src/cli.js +78 -0
  50. package/src/config/loader.js +332 -0
  51. package/src/config/schema-validator.js +214 -0
  52. package/src/config/toml-lite.js +8 -0
  53. package/src/daemon/action-handlers.js +71 -0
  54. package/src/daemon/healing-tick.js +87 -0
  55. package/src/daemon/health-probes.js +90 -0
  56. package/src/daemon/notifier.js +57 -0
  57. package/src/daemon/nurse.js +218 -0
  58. package/src/daemon/repair-log.js +106 -0
  59. package/src/daemon/rule-staging.js +90 -0
  60. package/src/daemon/rules.js +29 -0
  61. package/src/daemon/telegram-commands.js +54 -0
  62. package/src/daemon/updater.js +85 -0
  63. package/src/jobs/job-runner.js +78 -0
  64. package/src/mcp/consumer.js +129 -0
  65. package/src/memory/active-recall.js +171 -0
  66. package/src/memory/backend-manager.js +97 -0
  67. package/src/memory/backends/file-backend.js +38 -0
  68. package/src/memory/backends/qmd-backend.js +219 -0
  69. package/src/memory/embedding-guards.js +24 -0
  70. package/src/memory/embedding-index.js +118 -0
  71. package/src/memory/embedding-service.js +179 -0
  72. package/src/memory/file-index.js +177 -0
  73. package/src/memory/memory-signature.js +5 -0
  74. package/src/memory/memory-store.js +648 -0
  75. package/src/memory/retrieval-planner.js +66 -0
  76. package/src/memory/scoring.js +145 -0
  77. package/src/memory/simhash.js +78 -0
  78. package/src/memory/sqlite-active-store.js +824 -0
  79. package/src/memory/write-policy.js +36 -0
  80. package/src/onboarding/aliases.js +33 -0
  81. package/src/onboarding/auth/api-key.js +224 -0
  82. package/src/onboarding/auth/ollama-detect.js +42 -0
  83. package/src/onboarding/clack-prompter.js +77 -0
  84. package/src/onboarding/doctor.js +530 -0
  85. package/src/onboarding/lock.js +42 -0
  86. package/src/onboarding/model-catalog.js +344 -0
  87. package/src/onboarding/phases/auth.js +589 -0
  88. package/src/onboarding/phases/build.js +130 -0
  89. package/src/onboarding/phases/choose.js +82 -0
  90. package/src/onboarding/phases/detect.js +98 -0
  91. package/src/onboarding/phases/hatch.js +216 -0
  92. package/src/onboarding/phases/identity.js +79 -0
  93. package/src/onboarding/phases/ollama.js +345 -0
  94. package/src/onboarding/phases/scaffold.js +99 -0
  95. package/src/onboarding/phases/telegram.js +377 -0
  96. package/src/onboarding/phases/validate.js +204 -0
  97. package/src/onboarding/phases/verify.js +206 -0
  98. package/src/onboarding/platform.js +482 -0
  99. package/src/onboarding/status-bar.js +95 -0
  100. package/src/onboarding/templates.js +794 -0
  101. package/src/onboarding/toml-writer.js +38 -0
  102. package/src/onboarding/tui.js +250 -0
  103. package/src/onboarding/uninstall.js +153 -0
  104. package/src/onboarding/wizard.js +499 -0
  105. package/src/providers/anthropic.js +168 -0
  106. package/src/providers/base.js +247 -0
  107. package/src/providers/circuit-breaker.js +136 -0
  108. package/src/providers/ollama.js +163 -0
  109. package/src/providers/openai-codex.js +149 -0
  110. package/src/providers/openrouter.js +136 -0
  111. package/src/providers/registry.js +36 -0
  112. package/src/providers/router.js +16 -0
  113. package/src/runtime/bootstrap-cache.js +47 -0
  114. package/src/runtime/capabilities-prompt.js +25 -0
  115. package/src/runtime/completion-ping.js +99 -0
  116. package/src/runtime/config-validator.js +121 -0
  117. package/src/runtime/context-ledger.js +360 -0
  118. package/src/runtime/cutover-readiness.js +42 -0
  119. package/src/runtime/daemon.js +729 -0
  120. package/src/runtime/delivery-ack.js +195 -0
  121. package/src/runtime/delivery-adapters/local-file.js +41 -0
  122. package/src/runtime/delivery-adapters/openclaw-cli.js +94 -0
  123. package/src/runtime/delivery-adapters/openclaw-peer.js +98 -0
  124. package/src/runtime/delivery-adapters/shadow.js +13 -0
  125. package/src/runtime/delivery-adapters/standalone-http.js +98 -0
  126. package/src/runtime/delivery-adapters/telegram.js +104 -0
  127. package/src/runtime/delivery-adapters/tui.js +128 -0
  128. package/src/runtime/delivery-manager.js +807 -0
  129. package/src/runtime/delivery-store.js +168 -0
  130. package/src/runtime/dependency-health.js +118 -0
  131. package/src/runtime/envelope.js +114 -0
  132. package/src/runtime/evaluation.js +1089 -0
  133. package/src/runtime/exec-approvals.js +216 -0
  134. package/src/runtime/executor.js +500 -0
  135. package/src/runtime/failure-ping.js +67 -0
  136. package/src/runtime/flows.js +83 -0
  137. package/src/runtime/guards.js +45 -0
  138. package/src/runtime/handoff.js +51 -0
  139. package/src/runtime/identity-cache.js +28 -0
  140. package/src/runtime/improvement-engine.js +109 -0
  141. package/src/runtime/improvement-harness.js +581 -0
  142. package/src/runtime/input-sanitiser.js +72 -0
  143. package/src/runtime/interaction-contract.js +347 -0
  144. package/src/runtime/lane-readiness.js +226 -0
  145. package/src/runtime/migration.js +323 -0
  146. package/src/runtime/model-resolution.js +78 -0
  147. package/src/runtime/network.js +64 -0
  148. package/src/runtime/notification-store.js +97 -0
  149. package/src/runtime/notifier.js +256 -0
  150. package/src/runtime/orchestrator.js +53 -0
  151. package/src/runtime/orphan-reaper.js +41 -0
  152. package/src/runtime/output-contract-schema.js +139 -0
  153. package/src/runtime/output-contract-validator.js +439 -0
  154. package/src/runtime/peer-readiness.js +69 -0
  155. package/src/runtime/peer-registry.js +133 -0
  156. package/src/runtime/pilot-status.js +108 -0
  157. package/src/runtime/prompt-builder.js +261 -0
  158. package/src/runtime/provider-attempt.js +582 -0
  159. package/src/runtime/report-fallback.js +71 -0
  160. package/src/runtime/result-normalizer.js +183 -0
  161. package/src/runtime/retention.js +74 -0
  162. package/src/runtime/review.js +244 -0
  163. package/src/runtime/route-job.js +15 -0
  164. package/src/runtime/run-store.js +38 -0
  165. package/src/runtime/schedule.js +88 -0
  166. package/src/runtime/scheduler-state.js +434 -0
  167. package/src/runtime/scheduler.js +656 -0
  168. package/src/runtime/session-compactor.js +182 -0
  169. package/src/runtime/session-search.js +155 -0
  170. package/src/runtime/slack-inbound.js +249 -0
  171. package/src/runtime/ssrf.js +102 -0
  172. package/src/runtime/status-aggregator.js +330 -0
  173. package/src/runtime/task-contract.js +140 -0
  174. package/src/runtime/task-packet.js +107 -0
  175. package/src/runtime/task-router.js +140 -0
  176. package/src/runtime/telegram-inbound.js +1565 -0
  177. package/src/runtime/token-counter.js +134 -0
  178. package/src/runtime/token-estimator.js +59 -0
  179. package/src/runtime/tool-loop.js +200 -0
  180. package/src/runtime/transport-server.js +311 -0
  181. package/src/runtime/tui-server.js +411 -0
  182. package/src/runtime/ulid.js +44 -0
  183. package/src/security/ssrf-check.js +197 -0
  184. package/src/setup.js +369 -0
  185. package/src/shadow/bridge.js +303 -0
  186. package/src/skills/loader.js +84 -0
  187. package/src/tools/catalog.json +49 -0
  188. package/src/tools/cli-delegate.js +44 -0
  189. package/src/tools/mcp-client.js +106 -0
  190. package/src/tools/micro/cancel-task.js +6 -0
  191. package/src/tools/micro/complete-task.js +6 -0
  192. package/src/tools/micro/fail-task.js +6 -0
  193. package/src/tools/micro/http-fetch.js +74 -0
  194. package/src/tools/micro/index.js +36 -0
  195. package/src/tools/micro/lcm-recall.js +60 -0
  196. package/src/tools/micro/list-dir.js +17 -0
  197. package/src/tools/micro/list-skills.js +46 -0
  198. package/src/tools/micro/load-skill.js +38 -0
  199. package/src/tools/micro/memory-search.js +45 -0
  200. package/src/tools/micro/read-file.js +11 -0
  201. package/src/tools/micro/session-search.js +54 -0
  202. package/src/tools/micro/shell-exec.js +43 -0
  203. package/src/tools/micro/trigger-job.js +79 -0
  204. package/src/tools/micro/web-search.js +58 -0
  205. package/src/tools/micro/workspace-paths.js +39 -0
  206. package/src/tools/micro/write-file.js +14 -0
  207. package/src/tools/micro/write-memory.js +41 -0
  208. package/src/tools/registry.js +348 -0
  209. package/src/tools/tool-result-contract.js +36 -0
  210. package/src/tui/chat.js +835 -0
  211. package/src/tui/renderer.js +175 -0
  212. package/src/tui/socket-client.js +217 -0
  213. package/src/utils/canonical-json.js +29 -0
  214. package/src/utils/compaction.js +30 -0
  215. package/src/utils/env-loader.js +5 -0
  216. package/src/utils/errors.js +80 -0
  217. package/src/utils/fs.js +101 -0
  218. package/src/utils/ids.js +5 -0
  219. package/src/utils/model-context-limits.js +30 -0
  220. package/src/utils/token-budget.js +74 -0
  221. package/src/utils/usage-cost.js +25 -0
  222. package/src/utils/usage-metrics.js +14 -0
  223. 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
+ }