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,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
|
+
}
|