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,247 @@
1
+ import { annotateError } from "../runtime/network.js";
2
+ import { fetchWithOutboundPolicy } from "../runtime/ssrf.js";
3
+ import { OUTBOUND_ADDRESS_POLICY } from "../security/ssrf-check.js";
4
+ import { getAuthProfile, resolveProfileSecret } from "../auth/auth-profiles.js";
5
+
6
+ export class ProviderAdapter {
7
+ constructor(config = {}, dependencies = {}) {
8
+ this.config = config;
9
+ this.fetchImpl = dependencies.fetchImpl || globalThis.fetch;
10
+ this.lookupImpl = dependencies.lookupImpl;
11
+ }
12
+
13
+ requireFetch() {
14
+ if (!this.fetchImpl) {
15
+ throw new Error("No fetch implementation available");
16
+ }
17
+ }
18
+
19
+ get authRef() {
20
+ return this.config.authRef || "";
21
+ }
22
+
23
+ get authProfileId() {
24
+ if (!this.authRef.startsWith("profile:")) return null;
25
+ return this.authRef.slice("profile:".length);
26
+ }
27
+
28
+ resolveAuthProfile() {
29
+ const profileId = this.authProfileId;
30
+ if (!profileId) return null;
31
+ return getAuthProfile(profileId);
32
+ }
33
+
34
+ resolveAuthToken() {
35
+ if (this.authRef.startsWith("env:")) {
36
+ const envName = this.authRef.slice(4);
37
+ return process.env[envName] || null;
38
+ }
39
+ if (this.authRef.startsWith("profile:")) {
40
+ return resolveProfileSecret(this.resolveAuthProfile());
41
+ }
42
+ return null;
43
+ }
44
+
45
+ ensureAuthToken() {
46
+ const token = this.resolveAuthToken();
47
+ if (!token) {
48
+ throw new Error(`Missing auth token for ${this.config.id || "provider"} (${this.authRef})`);
49
+ }
50
+ return token;
51
+ }
52
+
53
+ buildUrl(endpoint) {
54
+ const baseUrl = String(this.config.baseUrl || "").replace(/\/$/, "");
55
+ const pathPart = String(endpoint || "").replace(/^\//, "");
56
+ return `${baseUrl}/${pathPart}`;
57
+ }
58
+
59
+ buildRequestOptions(options = {}, runtimeOptions = {}) {
60
+ const timeoutMs = Number((runtimeOptions.timeoutMs ?? this.config.defaultTimeoutMs) || 0);
61
+ if (!timeoutMs || typeof AbortSignal?.timeout !== "function") {
62
+ return options;
63
+ }
64
+
65
+ return {
66
+ ...options,
67
+ signal: options.signal || AbortSignal.timeout(timeoutMs)
68
+ };
69
+ }
70
+
71
+ resolveRequestUrl(target) {
72
+ if (typeof target === "string" && /^[a-z]+:\/\//i.test(target)) {
73
+ return target;
74
+ }
75
+ return this.buildUrl(target);
76
+ }
77
+
78
+ async fetchResponse(target, options = {}, runtimeOptions = {}) {
79
+ this.requireFetch();
80
+ const timeoutMs = Number((runtimeOptions.timeoutMs ?? this.config.defaultTimeoutMs) || 0);
81
+ const isOllama = this.config.id === "ollama" || this.config.adapter === "ollama";
82
+ try {
83
+ // Provider URLs come from runtime configuration. Only Ollama is meant to
84
+ // stay local, and even then it must resolve to loopback.
85
+ return await fetchWithOutboundPolicy(
86
+ this.resolveRequestUrl(target),
87
+ this.buildRequestOptions(options, runtimeOptions),
88
+ {
89
+ fetchImpl: this.fetchImpl,
90
+ lookupImpl: this.lookupImpl,
91
+ surface: "provider",
92
+ privateAddressMessage: `Provider request blocked — target resolves to a private/reserved IP address for ${this.config.id || "provider"}.`,
93
+ loopbackOnlyMessage: `Ollama base URL must resolve to loopback only; refusing ${this.resolveRequestUrl(target)}.`,
94
+ addressPolicy: isOllama ? OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK : OUTBOUND_ADDRESS_POLICY.BLOCK_PRIVATE,
95
+ }
96
+ );
97
+ } catch (error) {
98
+ if (error?.name === "TimeoutError" || error?.name === "AbortError") {
99
+ throw annotateError(
100
+ new Error(`Provider request timed out after ${timeoutMs || "configured"} ms for ${this.config.id || "provider"}`),
101
+ {
102
+ surface: "provider"
103
+ }
104
+ );
105
+ }
106
+ throw annotateError(error, { surface: "provider" });
107
+ }
108
+ }
109
+
110
+ getCapabilities() {
111
+ return {
112
+ supportsToolDefinitions: false,
113
+ supportsToolCalls: false,
114
+ structuredOutputMode: "none",
115
+ supportsReasoningSchema: false,
116
+ tokenCountingMode: "heuristic",
117
+ toolReliabilityTier: "unsupported",
118
+ supportsVision: false
119
+ };
120
+ }
121
+
122
+ supportsNativeStructuredOutput() {
123
+ return new Set(["response_format", "forced_tool", "constrained_decoding"]).has(
124
+ this.getCapabilities().structuredOutputMode
125
+ );
126
+ }
127
+
128
+ buildStructuredOutputFormat(_input) {
129
+ return null;
130
+ }
131
+
132
+ async countTokens(_input) {
133
+ return null;
134
+ }
135
+
136
+ async listModels() {
137
+ return null;
138
+ }
139
+
140
+ normalizeResponse(raw) {
141
+ if (!raw) {
142
+ return {
143
+ summary: "Provider returned no data.",
144
+ output: "",
145
+ nextActions: [],
146
+ reasoning: null,
147
+ raw
148
+ };
149
+ }
150
+
151
+ const text = this.extractText(raw);
152
+ const parsedJson = text ? this.parseStructuredJson(text) : null;
153
+ if (parsedJson) {
154
+ return {
155
+ summary: parsedJson.summary || "Provider returned structured JSON output.",
156
+ output: parsedJson.output || text,
157
+ nextActions: Array.isArray(parsedJson.nextActions) ? parsedJson.nextActions : [],
158
+ reasoning: parsedJson.analysis || null,
159
+ raw
160
+ };
161
+ }
162
+
163
+ if (text) {
164
+ return {
165
+ summary: text.slice(0, 200) || "Provider responded.",
166
+ output: text,
167
+ nextActions: [],
168
+ reasoning: null,
169
+ raw
170
+ };
171
+ }
172
+
173
+ return {
174
+ summary: "Provider responded with structured data.",
175
+ output: JSON.stringify(raw, null, 2),
176
+ nextActions: [],
177
+ reasoning: null,
178
+ raw
179
+ };
180
+ }
181
+
182
+ extractText(raw) {
183
+ return raw?.content && Array.isArray(raw.content)
184
+ ? raw.content
185
+ .map((item) => item.text || item.content || "")
186
+ .filter(Boolean)
187
+ .join("\n")
188
+ : raw?.choices?.[0]?.message?.content || raw?.message?.content
189
+ ? String(raw?.choices?.[0]?.message?.content || raw?.message?.content)
190
+ : null;
191
+ }
192
+
193
+ parseStructuredJson(text) {
194
+ const trimmed = String(text).trim();
195
+ const fencedMatch = trimmed.match(/^```(?:json)?\s*([\s\S]*?)\s*```$/i);
196
+ const candidate = fencedMatch ? fencedMatch[1].trim() : trimmed;
197
+ if (!(candidate.startsWith("{") && candidate.endsWith("}"))) {
198
+ return null;
199
+ }
200
+
201
+ try {
202
+ return JSON.parse(candidate);
203
+ } catch {
204
+ return null;
205
+ }
206
+ }
207
+
208
+ async postJson(endpoint, body, headers = {}, runtimeOptions = {}) {
209
+ let response;
210
+ try {
211
+ response = await this.fetchResponse(
212
+ endpoint,
213
+ {
214
+ method: "POST",
215
+ headers: {
216
+ "content-type": "application/json",
217
+ ...headers
218
+ },
219
+ body: JSON.stringify(body)
220
+ },
221
+ runtimeOptions
222
+ );
223
+ } catch (error) {
224
+ throw error;
225
+ }
226
+ const text = await response.text();
227
+ let data = null;
228
+ if (text) {
229
+ try {
230
+ data = JSON.parse(text);
231
+ } catch {
232
+ data = text;
233
+ }
234
+ }
235
+
236
+ if (!response.ok) {
237
+ throw annotateError(
238
+ new Error(`Provider error ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data)}`),
239
+ {
240
+ surface: "provider"
241
+ }
242
+ );
243
+ }
244
+
245
+ return data;
246
+ }
247
+ }
@@ -0,0 +1,136 @@
1
+ const DEFAULT_TRANSIENT_CODES = new Set([408, 429, 500, 502, 503, 504]);
2
+
3
+ export function ensureBreakerSchema(db) {
4
+ db.exec(`
5
+ create table if not exists provider_breakers (
6
+ provider_key text primary key,
7
+ state text not null default 'closed',
8
+ failure_count integer not null default 0,
9
+ last_failure_at text,
10
+ last_success_at text,
11
+ opened_at text,
12
+ half_open_at text
13
+ );
14
+ `);
15
+ }
16
+
17
+ export class CircuitBreaker {
18
+ constructor(providerKey, db, config = {}) {
19
+ this._key = providerKey;
20
+ this._db = db;
21
+ this._failureThreshold = config.failureThreshold ?? 5;
22
+ this._resetTimeoutSeconds = config.resetTimeoutSeconds ?? 30;
23
+ this._transientCodes = new Set(config.transientCodes ?? DEFAULT_TRANSIENT_CODES);
24
+
25
+ ensureBreakerSchema(db);
26
+ this._load();
27
+ }
28
+
29
+ static forProvider(providerKey, db, config = {}) {
30
+ return new CircuitBreaker(providerKey, db, config);
31
+ }
32
+
33
+ _load() {
34
+ const row = this._db.prepare("select * from provider_breakers where provider_key = ?").get(this._key);
35
+ if (row) {
36
+ this._state = row.state;
37
+ this._failureCount = row.failure_count;
38
+ this._lastFailureAt = row.last_failure_at;
39
+ this._lastSuccessAt = row.last_success_at;
40
+ this._openedAt = row.opened_at;
41
+ this._halfOpenAt = row.half_open_at;
42
+ } else {
43
+ this._state = "closed";
44
+ this._failureCount = 0;
45
+ this._lastFailureAt = null;
46
+ this._lastSuccessAt = null;
47
+ this._openedAt = null;
48
+ this._halfOpenAt = null;
49
+ }
50
+ }
51
+
52
+ _persist() {
53
+ this._db.prepare(`
54
+ insert into provider_breakers (provider_key, state, failure_count, last_failure_at, last_success_at, opened_at, half_open_at)
55
+ values (?, ?, ?, ?, ?, ?, ?)
56
+ on conflict(provider_key) do update set
57
+ state = excluded.state,
58
+ failure_count = excluded.failure_count,
59
+ last_failure_at = excluded.last_failure_at,
60
+ last_success_at = excluded.last_success_at,
61
+ opened_at = excluded.opened_at,
62
+ half_open_at = excluded.half_open_at
63
+ `).run(
64
+ this._key,
65
+ this._state,
66
+ this._failureCount,
67
+ this._lastFailureAt,
68
+ this._lastSuccessAt,
69
+ this._openedAt,
70
+ this._halfOpenAt
71
+ );
72
+ }
73
+
74
+ _checkHalfOpen() {
75
+ if (this._state !== "open") return;
76
+ if (!this._openedAt) return;
77
+ const elapsed = (Date.now() - new Date(this._openedAt).getTime()) / 1000;
78
+ if (elapsed >= this._resetTimeoutSeconds) {
79
+ this._state = "half_open";
80
+ this._halfOpenAt = new Date().toISOString();
81
+ this._persist();
82
+ }
83
+ }
84
+
85
+ isClosed() {
86
+ this._checkHalfOpen();
87
+ return this._state === "closed";
88
+ }
89
+
90
+ isOpen() {
91
+ this._checkHalfOpen();
92
+ return this._state === "open";
93
+ }
94
+
95
+ isHalfOpen() {
96
+ this._checkHalfOpen();
97
+ return this._state === "half_open";
98
+ }
99
+
100
+ failureCount() {
101
+ return this._failureCount;
102
+ }
103
+
104
+ retryAfter() {
105
+ if (this._state !== "open" || !this._openedAt) return 0;
106
+ const elapsed = (Date.now() - new Date(this._openedAt).getTime()) / 1000;
107
+ return Math.max(0, this._resetTimeoutSeconds - elapsed);
108
+ }
109
+
110
+ recordSuccess() {
111
+ this._state = "closed";
112
+ this._failureCount = 0;
113
+ this._lastSuccessAt = new Date().toISOString();
114
+ this._openedAt = null;
115
+ this._halfOpenAt = null;
116
+ this._persist();
117
+ }
118
+
119
+ recordFailure(errorCode) {
120
+ if (!this._transientCodes.has(errorCode)) return;
121
+
122
+ this._failureCount += 1;
123
+ this._lastFailureAt = new Date().toISOString();
124
+
125
+ if (this._state === "half_open") {
126
+ this._state = "open";
127
+ this._openedAt = new Date().toISOString();
128
+ this._halfOpenAt = null;
129
+ } else if (this._failureCount >= this._failureThreshold) {
130
+ this._state = "open";
131
+ this._openedAt = new Date().toISOString();
132
+ }
133
+
134
+ this._persist();
135
+ }
136
+ }
@@ -0,0 +1,163 @@
1
+ import { ProviderAdapter } from "./base.js";
2
+
3
+ export class OllamaAdapter extends ProviderAdapter {
4
+ static providerId = "ollama";
5
+
6
+ getCapabilities() {
7
+ return {
8
+ supportsToolDefinitions: true,
9
+ supportsToolCalls: true,
10
+ structuredOutputMode: "prompt_contract",
11
+ supportsReasoningSchema: false,
12
+ tokenCountingMode: "heuristic",
13
+ toolReliabilityTier: "medium",
14
+ supportsVision: false
15
+ };
16
+ }
17
+
18
+ normalizeModel(model) {
19
+ if (typeof model !== "string") return model;
20
+ return model.startsWith("ollama/") ? model.slice("ollama/".length) : model;
21
+ }
22
+
23
+ buildInvokePayload({ model, system, messages, tools, options }) {
24
+ const payload = {
25
+ model: this.normalizeModel(model),
26
+ stream: false,
27
+ system,
28
+ messages,
29
+ options
30
+ };
31
+ // Ollama /api/chat supports OpenAI-compatible tools array
32
+ if (tools?.length) payload.tools = tools;
33
+ return payload;
34
+ }
35
+
36
+ async invoke(input) {
37
+ return this.postJson("api/chat", this.buildInvokePayload(input), {}, {
38
+ timeoutMs: input.timeoutMs
39
+ });
40
+ }
41
+
42
+ normalizeResponse(raw) {
43
+ // Handle Ollama tool_calls (OpenAI-compatible format)
44
+ const toolCalls = raw?.message?.tool_calls;
45
+ if (Array.isArray(toolCalls) && toolCalls.length > 0) {
46
+ // Convert to Anthropic-style tool_use blocks for executor compatibility
47
+ const content = toolCalls.map((tc) => ({
48
+ type: "tool_use",
49
+ id: tc.id || `ollama_tool_${Date.now()}`,
50
+ name: tc.function?.name || tc.name,
51
+ input: tc.function?.arguments
52
+ ? (typeof tc.function.arguments === "string"
53
+ ? JSON.parse(tc.function.arguments)
54
+ : tc.function.arguments)
55
+ : {}
56
+ }));
57
+ return {
58
+ summary: `Tool call: ${content.map(c => c.name).join(", ")}`,
59
+ output: null,
60
+ nextActions: [],
61
+ reasoning: null,
62
+ raw,
63
+ content
64
+ };
65
+ }
66
+
67
+ const text = raw?.message?.content ? String(raw.message.content) : null;
68
+ const parsedJson = text ? this.parseStructuredJson(text) : null;
69
+ if (parsedJson) {
70
+ return {
71
+ summary: parsedJson.summary || "Provider returned structured JSON output.",
72
+ output: parsedJson.output || text,
73
+ nextActions: Array.isArray(parsedJson.nextActions) ? parsedJson.nextActions : [],
74
+ reasoning: parsedJson.analysis || null,
75
+ raw
76
+ };
77
+ }
78
+ if (text) {
79
+ return {
80
+ summary: text.slice(0, 200) || "Provider responded.",
81
+ output: text,
82
+ nextActions: [],
83
+ reasoning: null,
84
+ raw
85
+ };
86
+ }
87
+
88
+ return super.normalizeResponse(raw);
89
+ }
90
+
91
+ async embed({ model, input }) {
92
+ const payload = {
93
+ model: this.normalizeModel(model),
94
+ input
95
+ };
96
+
97
+ try {
98
+ const result = await this.postJson("api/embed", payload, {}, {
99
+ timeoutMs: input.timeoutMs
100
+ });
101
+ if (Array.isArray(result.embeddings) && result.embeddings.length > 0) {
102
+ return {
103
+ embeddings: result.embeddings
104
+ };
105
+ }
106
+ if (Array.isArray(result.embedding)) {
107
+ return {
108
+ embeddings: [result.embedding]
109
+ };
110
+ }
111
+ return result;
112
+ } catch (error) {
113
+ if (!String(error.message || "").includes("404")) throw error;
114
+ const fallback = await this.postJson("api/embeddings", payload, {}, {
115
+ timeoutMs: input.timeoutMs
116
+ });
117
+ if (Array.isArray(fallback.embedding)) {
118
+ return {
119
+ embeddings: [fallback.embedding]
120
+ };
121
+ }
122
+ return fallback;
123
+ }
124
+ }
125
+
126
+ async healthCheck() {
127
+ // Ollama is intentionally local-only. fetchResponse enforces loopback-only
128
+ // on the configured baseUrl before any health probe leaves the process.
129
+ const response = await this.fetchResponse("api/tags", {
130
+ method: "GET"
131
+ });
132
+ return {
133
+ ok: response.ok,
134
+ status: response.status
135
+ };
136
+ }
137
+
138
+ async listModels() {
139
+ const response = await this.fetchResponse("api/tags", {
140
+ method: "GET"
141
+ });
142
+ const text = await response.text();
143
+ let data = null;
144
+ if (text) {
145
+ try {
146
+ data = JSON.parse(text);
147
+ } catch {
148
+ data = null;
149
+ }
150
+ }
151
+ if (!response.ok) {
152
+ throw new Error(`Provider error ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data)}`);
153
+ }
154
+
155
+ const models = Array.isArray(data?.models)
156
+ ? data.models
157
+ .map((item) => item?.name || item?.model || null)
158
+ .filter(Boolean)
159
+ : [];
160
+
161
+ return models;
162
+ }
163
+ }
@@ -0,0 +1,149 @@
1
+ import { resolveOpenAICodexAccess } from "../auth/openai-codex-oauth.js";
2
+ import { ProviderAdapter } from "./base.js";
3
+
4
+ export class OpenAICodexAdapter extends ProviderAdapter {
5
+ static providerId = "openai-codex";
6
+
7
+ normalizeModel(model) {
8
+ if (typeof model !== "string") return model;
9
+ return model.startsWith("openai-codex/") ? model.slice("openai-codex/".length) : model;
10
+ }
11
+
12
+ getCapabilities() {
13
+ return {
14
+ supportsToolDefinitions: true,
15
+ supportsToolCalls: true,
16
+ structuredOutputMode: "response_format",
17
+ supportsReasoningSchema: false,
18
+ tokenCountingMode: "heuristic",
19
+ toolReliabilityTier: "high",
20
+ supportsVision: true
21
+ };
22
+ }
23
+
24
+ buildStructuredOutputFormat(input) {
25
+ if (!input?.responseSchema?.schema) return null;
26
+ return {
27
+ type: "json_schema",
28
+ json_schema: {
29
+ name: input.responseSchema.name || "nemoris_response",
30
+ strict: true,
31
+ schema: input.responseSchema.schema
32
+ }
33
+ };
34
+ }
35
+
36
+ buildInvokePayload(input) {
37
+ const { model: rawModel, system, messages, maxTokens = 2048, tools, responseSchema } = input;
38
+ const model = this.normalizeModel(rawModel);
39
+ const normalizedMessages = [];
40
+ if (system) {
41
+ normalizedMessages.push({
42
+ role: "system",
43
+ content: system
44
+ });
45
+ }
46
+ normalizedMessages.push(...messages);
47
+
48
+ const payload = {
49
+ model,
50
+ messages: normalizedMessages,
51
+ max_tokens: maxTokens
52
+ };
53
+ if (tools?.length) payload.tools = tools;
54
+ const responseFormat = this.buildStructuredOutputFormat({ responseSchema });
55
+ if (responseFormat) payload.response_format = responseFormat;
56
+ return payload;
57
+ }
58
+
59
+ async getAccessToken() {
60
+ if (this.authRef.startsWith("profile:")) {
61
+ const profileId = this.authRef.slice("profile:".length);
62
+ const resolved = await resolveOpenAICodexAccess(profileId, {
63
+ fetchImpl: this.fetchImpl
64
+ });
65
+ return resolved.token;
66
+ }
67
+ return this.ensureAuthToken();
68
+ }
69
+
70
+ async invoke(input) {
71
+ const token = await this.getAccessToken();
72
+ return this.postJson(
73
+ "chat/completions",
74
+ this.buildInvokePayload(input),
75
+ {
76
+ authorization: `Bearer ${token}`
77
+ },
78
+ {
79
+ timeoutMs: input.timeoutMs
80
+ }
81
+ );
82
+ }
83
+
84
+ normalizeResponse(raw) {
85
+ const text = raw?.choices?.[0]?.message?.content
86
+ ? String(raw.choices[0].message.content)
87
+ : null;
88
+ const parsedJson = text ? this.parseStructuredJson(text) : null;
89
+ if (parsedJson) {
90
+ return {
91
+ summary: parsedJson.summary || "Provider returned structured JSON output.",
92
+ output: parsedJson.output || text,
93
+ nextActions: Array.isArray(parsedJson.nextActions) ? parsedJson.nextActions : [],
94
+ reasoning: parsedJson.analysis || null,
95
+ raw
96
+ };
97
+ }
98
+ if (text) {
99
+ return {
100
+ summary: text.slice(0, 200) || "Provider responded.",
101
+ output: text,
102
+ nextActions: [],
103
+ reasoning: null,
104
+ raw
105
+ };
106
+ }
107
+
108
+ return super.normalizeResponse(raw);
109
+ }
110
+
111
+ async healthCheck() {
112
+ const token = await this.getAccessToken();
113
+ const response = await this.fetchResponse("models", {
114
+ method: "GET",
115
+ headers: {
116
+ authorization: `Bearer ${token}`
117
+ }
118
+ });
119
+ return {
120
+ ok: response.ok,
121
+ status: response.status
122
+ };
123
+ }
124
+
125
+ async listModels() {
126
+ const token = await this.getAccessToken();
127
+ const response = await this.fetchResponse("models", {
128
+ method: "GET",
129
+ headers: {
130
+ authorization: `Bearer ${token}`
131
+ }
132
+ });
133
+ const text = await response.text();
134
+ let data = null;
135
+ if (text) {
136
+ try {
137
+ data = JSON.parse(text);
138
+ } catch {
139
+ data = null;
140
+ }
141
+ }
142
+ if (!response.ok) {
143
+ throw new Error(`Provider error ${response.status}: ${typeof data === "string" ? data : JSON.stringify(data)}`);
144
+ }
145
+ return Array.isArray(data?.data)
146
+ ? data.data.map((item) => item?.id).filter(Boolean)
147
+ : [];
148
+ }
149
+ }