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,36 @@
1
+ export function evaluateDurableWrite(candidate, policy, writesThisRun = 0) {
2
+ const reasons = [];
3
+
4
+ if (!policy?.allow_durable_writes) {
5
+ reasons.push("durable writes disabled");
6
+ }
7
+
8
+ if (policy?.max_writes_per_run != null && writesThisRun >= policy.max_writes_per_run) {
9
+ reasons.push("write budget exhausted");
10
+ }
11
+
12
+ const category = candidate.category || "unknown";
13
+ const allowed = policy?.categories?.allowed;
14
+ const blocked = policy?.categories?.blocked || [];
15
+
16
+ if (Array.isArray(allowed) && !allowed.includes(category)) {
17
+ reasons.push(`category not allowed: ${category}`);
18
+ }
19
+
20
+ if (blocked.includes(category)) {
21
+ reasons.push(`category blocked: ${category}`);
22
+ }
23
+
24
+ if (policy?.require_source_reference && (!candidate.sourceRefs || candidate.sourceRefs.length === 0)) {
25
+ reasons.push("source reference required");
26
+ }
27
+
28
+ if (policy?.require_write_reason && !candidate.reason) {
29
+ reasons.push("write reason required");
30
+ }
31
+
32
+ return {
33
+ accepted: reasons.length === 0,
34
+ reasons
35
+ };
36
+ }
@@ -0,0 +1,33 @@
1
+ /**
2
+ * CLI command alias resolver.
3
+ *
4
+ * resolveAlias(command, args) returns the resolved args array for known
5
+ * aliases, or null for unknown commands.
6
+ *
7
+ * Real command handlers in cli-main.js now own start/stop/logs/restart.
8
+ * This alias layer only maps legacy convenience shorthands.
9
+ */
10
+
11
+ /**
12
+ * @param {string} command
13
+ * @param {string[]} args
14
+ * @returns {string[] | null}
15
+ */
16
+ export function resolveAlias(command, args) {
17
+ switch (command) {
18
+ case "status":
19
+ return ["runtime-status"];
20
+
21
+ case "run": {
22
+ const [jobName, ...rest] = args;
23
+ if (!jobName) return ["execute-job", "provider"];
24
+ return ["execute-job", jobName, "provider", ...rest];
25
+ }
26
+
27
+ case "runs":
28
+ return ["review-runs", "10"];
29
+
30
+ default:
31
+ return null;
32
+ }
33
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Auth handlers: API key detection, validation, .env writing, and provider resolution.
3
+ */
4
+
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import os from "node:os";
8
+ import { buildAnthropicAuthHeaders } from "../../providers/anthropic.js";
9
+ import { resolveInstallDir } from "../../auth/auth-profiles.js";
10
+
11
+ // Env var names for each provider
12
+ const ENV_VAR_MAP = {
13
+ anthropic: ["NEMORIS_ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY"],
14
+ openai: ["NEMORIS_OPENAI_API_KEY", "OPENAI_API_KEY"],
15
+ openrouter: ["OPENROUTER_API_KEY"],
16
+ };
17
+
18
+ // Default paths to search for .env files (in priority order)
19
+ function defaultSearchPaths() {
20
+ const installDir = resolveInstallDir();
21
+ return [
22
+ path.join(installDir, ".env"),
23
+ path.join(os.homedir(), ".nemoris", ".env"),
24
+ path.join(os.homedir(), ".openclaw", ".env"),
25
+ ];
26
+ }
27
+
28
+ /**
29
+ * Parse a .env file into a key/value map.
30
+ * Handles lines of the form KEY=value, ignoring comments and blank lines.
31
+ *
32
+ * @param {string} filePath
33
+ * @returns {Record<string, string>}
34
+ */
35
+ function parseEnvFile(filePath) {
36
+ try {
37
+ const content = fs.readFileSync(filePath, "utf8");
38
+ const result = {};
39
+ for (const line of content.split(/\r?\n/)) {
40
+ const trimmed = line.trim();
41
+ if (!trimmed || trimmed.startsWith("#")) continue;
42
+ const eqIdx = trimmed.indexOf("=");
43
+ if (eqIdx === -1) continue;
44
+ const key = trimmed.slice(0, eqIdx).trim();
45
+ const value = trimmed.slice(eqIdx + 1).trim();
46
+ if (key) result[key] = value;
47
+ }
48
+ return result;
49
+ } catch {
50
+ return {};
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Detect existing API keys from environment variables and optional .env file search paths.
56
+ * Environment variables take priority over file values.
57
+ *
58
+ * @param {object} [options]
59
+ * @param {string[]} [options.searchPaths] - Ordered list of .env file paths to check
60
+ * @returns {{ anthropic: string|null, openai: string|null, openrouter: string|null }}
61
+ */
62
+ export function detectExistingKeys({ searchPaths } = {}) {
63
+ const paths = searchPaths ?? defaultSearchPaths();
64
+ const fileEnv = {};
65
+ for (let i = paths.length - 1; i >= 0; i--) {
66
+ const parsed = parseEnvFile(paths[i]);
67
+ Object.assign(fileEnv, parsed);
68
+ }
69
+
70
+ const result = {};
71
+ for (const [provider, envVars] of Object.entries(ENV_VAR_MAP)) {
72
+ result[provider] = null;
73
+ for (const envVar of envVars) {
74
+ const value = process.env[envVar] || fileEnv[envVar] || null;
75
+ if (value) {
76
+ result[provider] = value;
77
+ break;
78
+ }
79
+ }
80
+ }
81
+ return result;
82
+ }
83
+
84
+ export function validateApiKeyFormat(provider, key) {
85
+ const token = String(key || "").trim();
86
+ if (!token) {
87
+ return { ok: false, error: "missing token" };
88
+ }
89
+
90
+ const rules = {
91
+ anthropic: {
92
+ prefixes: ["sk-ant-oat-", "sk-ant-api-", "sk-ant-"],
93
+ minLength: 16,
94
+ label: "Anthropic token"
95
+ },
96
+ openai: {
97
+ prefixes: ["sk-"],
98
+ minLength: 12,
99
+ label: "OpenAI API key"
100
+ },
101
+ openrouter: {
102
+ prefixes: ["sk-or-"],
103
+ minLength: 12,
104
+ label: "OpenRouter API key"
105
+ }
106
+ };
107
+
108
+ const rule = rules[provider];
109
+ if (!rule) {
110
+ return { ok: true };
111
+ }
112
+
113
+ if (!rule.prefixes.some((prefix) => token.startsWith(prefix))) {
114
+ return {
115
+ ok: false,
116
+ error: `${rule.label} must start with ${rule.prefixes.join(" or ")}`
117
+ };
118
+ }
119
+
120
+ if (token.length < rule.minLength) {
121
+ return {
122
+ ok: false,
123
+ error: `${rule.label} looks too short`
124
+ };
125
+ }
126
+
127
+ return { ok: true };
128
+ }
129
+
130
+ /**
131
+ * Validate an API key against a provider's health endpoint.
132
+ * Ollama is not validated here (no auth needed) — use ollama-detect.js instead.
133
+ *
134
+ * @param {"anthropic"|"openai"|"openrouter"} provider
135
+ * @param {string} key
136
+ * @param {object} [options]
137
+ * @param {Function} [options.fetchImpl]
138
+ * @returns {Promise<{ ok: boolean, status?: number, error?: string }>}
139
+ */
140
+ export async function validateApiKey(provider, key, options = {}) {
141
+ if (provider === "ollama") {
142
+ return { ok: true };
143
+ }
144
+
145
+ const fetch = options.fetchImpl || globalThis.fetch;
146
+ const providerTargets = {
147
+ anthropic: {
148
+ url: "https://api.anthropic.com/v1/messages/count_tokens",
149
+ method: "POST",
150
+ headers: {
151
+ "content-type": "application/json",
152
+ ...buildAnthropicAuthHeaders(key)
153
+ },
154
+ body: JSON.stringify({
155
+ model: "claude-haiku-4-5",
156
+ messages: [{ role: "user", content: "ping" }]
157
+ })
158
+ },
159
+ openai: {
160
+ url: "https://api.openai.com/v1/models",
161
+ method: "GET",
162
+ headers: {
163
+ Authorization: `Bearer ${key}`
164
+ }
165
+ },
166
+ openrouter: {
167
+ url: "https://openrouter.ai/api/v1/models",
168
+ method: "GET",
169
+ headers: {
170
+ Authorization: `Bearer ${key}`
171
+ }
172
+ }
173
+ };
174
+
175
+ const target = providerTargets[provider];
176
+ if (!target) {
177
+ throw new Error(`Unknown provider: ${provider}`);
178
+ }
179
+
180
+ try {
181
+ const response = await fetch(target.url, {
182
+ method: target.method,
183
+ headers: target.headers,
184
+ body: target.body,
185
+ signal: AbortSignal.timeout(10000)
186
+ });
187
+ return {
188
+ ok: response.ok,
189
+ status: response.status
190
+ };
191
+ } catch (error) {
192
+ return { ok: false, error: error.message };
193
+ }
194
+ }
195
+
196
+ /**
197
+ * Write or merge keys into an .env file at installDir/.env.
198
+ * Existing keys not being overwritten are preserved.
199
+ *
200
+ * @param {string} installDir - Directory where .env will be written
201
+ * @param {Record<string, string>} keys - Key/value pairs to write
202
+ */
203
+ export function writeEnvFile(installDir, keys) {
204
+ const envPath = path.join(installDir, ".env");
205
+ const existing = parseEnvFile(envPath);
206
+ const merged = { ...existing, ...keys };
207
+ const lines = Object.entries(merged).map(([k, v]) => `${k}=${v}`);
208
+ fs.writeFileSync(envPath, lines.join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
209
+ }
210
+
211
+ /**
212
+ * Determine which provider IDs to generate TOML configs for based on available keys.
213
+ *
214
+ * @param {{ anthropic?: string|null, openrouter?: string|null, openai?: string|null, ollama?: boolean }} keys
215
+ * @returns {string[]} Array of provider IDs (e.g. ["anthropic", "openrouter", "ollama"])
216
+ */
217
+ export function resolveProviders(keys) {
218
+ const providers = [];
219
+ if (keys.anthropic) providers.push("anthropic");
220
+ if (keys.openrouter) providers.push("openrouter");
221
+ if (keys.openai) providers.push("openai");
222
+ if (keys.ollama) providers.push("ollama");
223
+ return providers;
224
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Ollama local instance detection.
3
+ * Probes localhost:11434/api/tags and returns model list metadata.
4
+ */
5
+
6
+ import { inspectOutboundUrl, OUTBOUND_ADDRESS_POLICY } from "../../security/ssrf-check.js";
7
+
8
+ /**
9
+ * Detect a running Ollama instance and enumerate available models.
10
+ *
11
+ * @param {object} [options]
12
+ * @param {Function} [options.fetchImpl] - Injectable fetch implementation (defaults to globalThis.fetch)
13
+ * @param {Function} [options.lookupImpl] - Injectable DNS lookup used for loopback verification
14
+ * @returns {Promise<{ ok: boolean, modelCount: number, models: string[] }>}
15
+ */
16
+ export async function detectOllama({ fetchImpl, lookupImpl } = {}) {
17
+ const fetch = fetchImpl || globalThis.fetch;
18
+ const baseUrl = process.env.OLLAMA_HOST || "http://localhost:11434";
19
+ const url = `${baseUrl}/api/tags`;
20
+
21
+ try {
22
+ const inspection = await inspectOutboundUrl(url, {
23
+ lookupImpl,
24
+ addressPolicy: OUTBOUND_ADDRESS_POLICY.REQUIRE_LOOPBACK,
25
+ loopbackOnlyMessage: `Ollama base URL must resolve to loopback only; refusing ${url}.`,
26
+ });
27
+ if (!inspection.ok) {
28
+ return { ok: false, modelCount: 0, models: [], error: inspection.reason };
29
+ }
30
+ const response = await fetch(url);
31
+ if (!response.ok) {
32
+ return { ok: false, modelCount: 0, models: [] };
33
+ }
34
+ const data = await response.json();
35
+ const models = Array.isArray(data?.models)
36
+ ? data.models.map((m) => m?.name || m?.model || null).filter(Boolean)
37
+ : [];
38
+ return { ok: true, modelCount: models.length, models };
39
+ } catch {
40
+ return { ok: false, modelCount: 0, models: [] };
41
+ }
42
+ }
@@ -0,0 +1,77 @@
1
+ import * as p from "@clack/prompts";
2
+
3
+ export class SetupCancelledError extends Error {
4
+ constructor(message = "Setup cancelled.") {
5
+ super(message);
6
+ this.name = "SetupCancelledError";
7
+ }
8
+ }
9
+
10
+ function guardCancel(value) {
11
+ if (p.isCancel(value)) {
12
+ p.cancel("Setup cancelled.");
13
+ throw new SetupCancelledError();
14
+ }
15
+ return value;
16
+ }
17
+
18
+ function normalizeOptions(options = []) {
19
+ return options.map((option) => ({
20
+ value: option.value,
21
+ label: option.label,
22
+ ...(option.hint ? { hint: option.hint } : {}),
23
+ }));
24
+ }
25
+
26
+ export function createClackPrompter() {
27
+ return {
28
+ intro(message) {
29
+ p.intro(message);
30
+ },
31
+ outro(message) {
32
+ p.outro(message);
33
+ },
34
+ cancel(message = "Setup cancelled.") {
35
+ p.cancel(message);
36
+ },
37
+ note(message, title) {
38
+ p.note(message, title);
39
+ },
40
+ async confirm({ message, initialValue = false }) {
41
+ return guardCancel(await p.confirm({ message, initialValue }));
42
+ },
43
+ async select({ message, options, initialValue }) {
44
+ return guardCancel(await p.select({
45
+ message,
46
+ options: normalizeOptions(options),
47
+ initialValue,
48
+ }));
49
+ },
50
+ async multiselect({ message, options, initialValues = [], required = false }) {
51
+ const value = guardCancel(await p.multiselect({
52
+ message,
53
+ options: normalizeOptions(options),
54
+ initialValues,
55
+ required,
56
+ }));
57
+ return Array.isArray(value) ? value : [];
58
+ },
59
+ async text({ message, placeholder, initialValue, validate }) {
60
+ return guardCancel(await p.text({
61
+ message,
62
+ placeholder,
63
+ initialValue,
64
+ validate,
65
+ }));
66
+ },
67
+ async password({ message, validate }) {
68
+ return guardCancel(await p.password({
69
+ message,
70
+ validate,
71
+ }));
72
+ },
73
+ spinner() {
74
+ return p.spinner();
75
+ },
76
+ };
77
+ }