@openpalm/lib 0.9.5 → 0.9.7

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.
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Stack specification file (openpalm.yaml) management.
3
+ *
4
+ * The stack spec is a YAML document that captures the high-level
5
+ * configuration of an OpenPalm installation: connections, capability
6
+ * assignments, and feature flags. It lives in CONFIG_HOME.
7
+ */
8
+ import { mkdirSync, writeFileSync, readFileSync, existsSync } from "node:fs";
9
+ import { stringify as yamlStringify, parse as yamlParse } from "yaml";
10
+
11
+ // ── Types ──────────────────────────────────────────────────────────────
12
+
13
+ export type StackSpecConnection = {
14
+ id: string;
15
+ name: string;
16
+ provider: string;
17
+ baseUrl: string;
18
+ };
19
+
20
+ export type StackSpecAssignments = {
21
+ llm: { connectionId: string; model: string; smallModel?: string };
22
+ embeddings: { connectionId: string; model: string; embeddingDims?: number };
23
+ };
24
+
25
+ export type StackSpec = {
26
+ version: 3;
27
+ connections: StackSpecConnection[];
28
+ assignments: StackSpecAssignments;
29
+ ollamaEnabled: boolean;
30
+ voice?: { tts?: string; stt?: string };
31
+ channels?: string[];
32
+ services?: Record<string, boolean>;
33
+ };
34
+
35
+ // ── Constants ──────────────────────────────────────────────────────────
36
+
37
+ export const STACK_SPEC_FILENAME = "openpalm.yaml";
38
+
39
+ // ── Read / Write ────────────────────────────────────────────────────────
40
+
41
+ export function stackSpecPath(configDir: string): string {
42
+ return `${configDir}/${STACK_SPEC_FILENAME}`;
43
+ }
44
+
45
+ export function writeStackSpec(configDir: string, spec: StackSpec): void {
46
+ mkdirSync(configDir, { recursive: true });
47
+ const content = yamlStringify(spec, { indent: 2 });
48
+ writeFileSync(stackSpecPath(configDir), content);
49
+ }
50
+
51
+ export function readStackSpec(configDir: string): StackSpec | null {
52
+ const path = stackSpecPath(configDir);
53
+ if (!existsSync(path)) return null;
54
+ let raw: unknown;
55
+ try {
56
+ raw = yamlParse(readFileSync(path, "utf-8"));
57
+ } catch {
58
+ return null;
59
+ }
60
+ if (typeof raw !== "object" || raw === null) return null;
61
+ const obj = raw as Record<string, unknown>;
62
+ if (obj.version !== 3) return null;
63
+ return obj as unknown as StackSpec;
64
+ }
@@ -20,6 +20,7 @@ import {
20
20
  readCoreCaddyfile,
21
21
  readCoreCompose,
22
22
  readOllamaCompose,
23
+ readAdminCompose,
23
24
  ensureSecretsSchema,
24
25
  ensureStackSchema,
25
26
  PUBLIC_ACCESS_IMPORT,
@@ -53,9 +54,23 @@ export function isOllamaEnabled(state: ControlPlaneState): boolean {
53
54
  return match?.[1]?.trim().toLowerCase() === "true";
54
55
  }
55
56
 
57
+ // ── Admin State ──────────────────────────────────────────────────────
58
+
59
+ /**
60
+ * Check whether admin is enabled in the stack by reading the
61
+ * OPENPALM_ADMIN_ENABLED flag from DATA_HOME/stack.env.
62
+ */
63
+ export function isAdminEnabled(state: ControlPlaneState): boolean {
64
+ const stackEnvPath = `${state.dataDir}/stack.env`;
65
+ if (!existsSync(stackEnvPath)) return false;
66
+ const content = readFileSync(stackEnvPath, "utf-8");
67
+ const match = content.match(/^OPENPALM_ADMIN_ENABLED=(.+)$/m);
68
+ return match?.[1]?.trim().toLowerCase() === "true";
69
+ }
70
+
56
71
  // ── Caddyfile Staging ─────────────────────────────────────────────────
57
72
 
58
- function withDefaultLanOnly(rawCaddy: string): string | null {
73
+ export function withDefaultLanOnly(rawCaddy: string): string | null {
59
74
  if (rawCaddy.includes(PUBLIC_ACCESS_IMPORT) || rawCaddy.includes(LAN_ONLY_IMPORT)) {
60
75
  return rawCaddy;
61
76
  }
@@ -75,7 +90,7 @@ function withDefaultLanOnly(rawCaddy: string): string | null {
75
90
  return null;
76
91
  }
77
92
 
78
- function stageChannelCaddyfiles(state: ControlPlaneState): void {
93
+ export function stageChannelCaddyfiles(state: ControlPlaneState): void {
79
94
  const stagedChannelsDir = `${state.stateDir}/artifacts/channels`;
80
95
  const stagedPublicDir = `${stagedChannelsDir}/public`;
81
96
  const stagedLanDir = `${stagedChannelsDir}/lan`;
@@ -126,7 +141,7 @@ function stageCompose(_state: ControlPlaneState, assets: CoreAssetProvider): str
126
141
 
127
142
  // ── Env Staging ───────────────────────────────────────────────────────
128
143
 
129
- function stageSecretsEnv(state: ControlPlaneState): void {
144
+ export function stageSecretsEnv(state: ControlPlaneState): void {
130
145
  const artifactDir = `${state.stateDir}/artifacts`;
131
146
  mkdirSync(artifactDir, { recursive: true });
132
147
 
@@ -168,8 +183,12 @@ function stageStackEnv(state: ControlPlaneState): void {
168
183
  writeFileSync(dataStackEnv, base);
169
184
  }
170
185
 
186
+ // Preserve existing OPENPALM_SETUP_COMPLETE=true from stack.env;
187
+ // only mark complete if it was already true (not inferred from token presence).
188
+ const alreadyComplete = /^OPENPALM_SETUP_COMPLETE=true$/mi.test(base);
189
+
171
190
  const adminManaged: Record<string, string> = {
172
- OPENPALM_SETUP_COMPLETE: state.adminToken ? "true" : "false"
191
+ OPENPALM_SETUP_COMPLETE: alreadyComplete ? "true" : "false"
173
192
  };
174
193
  for (const [ch, secret] of Object.entries(state.channelSecrets)) {
175
194
  adminManaged[`CHANNEL_${ch.toUpperCase()}_SECRET`] = secret;
@@ -223,7 +242,7 @@ function generateFallbackStackEnv(state: ControlPlaneState): string {
223
242
 
224
243
  // ── Channel YML Staging ───────────────────────────────────────────────
225
244
 
226
- function stageChannelYmlFiles(state: ControlPlaneState): void {
245
+ export function stageChannelYmlFiles(state: ControlPlaneState): void {
227
246
  const stagedChannelsDir = `${state.stateDir}/artifacts/channels`;
228
247
  mkdirSync(stagedChannelsDir, { recursive: true });
229
248
 
@@ -268,7 +287,7 @@ function validateAutomationContent(content: string, fileName: string): boolean {
268
287
  return parseAutomationYaml(content, fileName) !== null;
269
288
  }
270
289
 
271
- function stageAutomationFiles(state: ControlPlaneState): void {
290
+ export function stageAutomationFiles(state: ControlPlaneState): void {
272
291
  const stagedDir = `${state.stateDir}/automations`;
273
292
  mkdirSync(stagedDir, { recursive: true });
274
293
 
@@ -355,6 +374,10 @@ export function persistArtifacts(
355
374
  writeFileSync(`${artifactDir}/ollama.yml`, readOllamaCompose(assets));
356
375
  }
357
376
 
377
+ if (isAdminEnabled(state)) {
378
+ writeFileSync(`${artifactDir}/admin.yml`, readAdminCompose(assets));
379
+ }
380
+
358
381
  const allChannels = discoverChannels(state.configDir);
359
382
  for (const ch of allChannels) {
360
383
  if (!state.channelSecrets[ch.name]) {
@@ -8,10 +8,9 @@ export type CoreServiceName =
8
8
  | "assistant"
9
9
  | "guardian"
10
10
  | "memory"
11
- | "caddy"
12
11
  | "scheduler";
13
12
 
14
- export type OptionalServiceName = "admin" | "docker-socket-proxy";
13
+ export type OptionalServiceName = "admin" | "caddy" | "docker-socket-proxy";
15
14
 
16
15
  export type AccessScope = "host" | "lan";
17
16
  export type CallerType = "assistant" | "cli" | "ui" | "system" | "test" | "unknown";
@@ -133,7 +132,6 @@ export type ControlPlaneState = {
133
132
  // ── Constants ──────────────────────────────────────────────────────────
134
133
 
135
134
  export const CORE_SERVICES: CoreServiceName[] = [
136
- "caddy",
137
135
  "memory",
138
136
  "assistant",
139
137
  "guardian",
@@ -141,6 +139,7 @@ export const CORE_SERVICES: CoreServiceName[] = [
141
139
  ];
142
140
 
143
141
  export const OPTIONAL_SERVICES: OptionalServiceName[] = [
142
+ "caddy",
144
143
  "admin",
145
144
  "docker-socket-proxy",
146
145
  ];
package/src/index.ts CHANGED
@@ -196,6 +196,8 @@ export {
196
196
  readCoreCompose,
197
197
  ensureOllamaCompose,
198
198
  readOllamaCompose,
199
+ ensureAdminCompose,
200
+ readAdminCompose,
199
201
  ensureOpenCodeSystemConfig,
200
202
  ensureAdminOpenCodeConfig,
201
203
  ensureCoreAutomations,
@@ -207,6 +209,7 @@ export {
207
209
  sha256,
208
210
  randomHex,
209
211
  isOllamaEnabled,
212
+ isAdminEnabled,
210
213
  stagedEnvFile,
211
214
  stagedStackEnvFile,
212
215
  buildEnvFiles,
@@ -214,6 +217,11 @@ export {
214
217
  stageArtifacts,
215
218
  buildArtifactMeta,
216
219
  persistArtifacts,
220
+ withDefaultLanOnly,
221
+ stageChannelCaddyfiles,
222
+ stageChannelYmlFiles,
223
+ stageSecretsEnv,
224
+ stageAutomationFiles,
217
225
  } from "./control-plane/staging.js";
218
226
 
219
227
  // ── Lifecycle ───────────────────────────────────────────────────────────
@@ -278,6 +286,19 @@ export {
278
286
  export type { LocalProviderDetection } from "./control-plane/model-runner.js";
279
287
  export { detectLocalProviders } from "./control-plane/model-runner.js";
280
288
 
289
+ // ── Stack Spec ───────────────────────────────────────────────────────────
290
+ export type {
291
+ StackSpec,
292
+ StackSpecConnection,
293
+ StackSpecAssignments,
294
+ } from "./control-plane/stack-spec.js";
295
+ export {
296
+ STACK_SPEC_FILENAME,
297
+ stackSpecPath,
298
+ writeStackSpec,
299
+ readStackSpec,
300
+ } from "./control-plane/stack-spec.js";
301
+
281
302
  // ── Setup ────────────────────────────────────────────────────────────────
282
303
  export type {
283
304
  SetupConnection,
@@ -285,6 +306,10 @@ export type {
285
306
  SetupInput,
286
307
  SetupResult,
287
308
  DetectedProvider,
309
+ SetupConfig,
310
+ SetupConfigAssignments,
311
+ ChannelCredentials,
312
+ ServiceConfig,
288
313
  } from "./control-plane/setup.js";
289
314
  export {
290
315
  validateSetupInput,
@@ -292,4 +317,9 @@ export {
292
317
  buildConnectionEnvVarMap,
293
318
  performSetup,
294
319
  detectProviders,
320
+ CHANNEL_CREDENTIAL_ENV_MAP,
321
+ validateSetupConfig,
322
+ normalizeToSetupInput,
323
+ performSetupFromConfig,
324
+ buildChannelCredentialEnvVars,
295
325
  } from "./control-plane/setup.js";
@@ -8,7 +8,8 @@
8
8
  /** Supported LLM providers. */
9
9
  export const LLM_PROVIDERS = [
10
10
  "openai", "anthropic", "ollama", "groq", "together",
11
- "mistral", "deepseek", "xai", "lmstudio", "model-runner"
11
+ "mistral", "deepseek", "xai", "lmstudio", "model-runner",
12
+ "google", "huggingface"
12
13
  ] as const;
13
14
 
14
15
  /** Default base URLs per provider. */
@@ -22,6 +23,8 @@ export const PROVIDER_DEFAULT_URLS: Record<string, string> = {
22
23
  lmstudio: "http://host.docker.internal:1234",
23
24
  ollama: "http://host.docker.internal:11434",
24
25
  "model-runner": "http://model-runner.docker.internal/engines",
26
+ google: "https://generativelanguage.googleapis.com",
27
+ huggingface: "https://router.huggingface.co/v1",
25
28
  };
26
29
 
27
30
  /** Map provider name → env var for the API key. */
@@ -31,6 +34,10 @@ export const PROVIDER_KEY_MAP: Record<string, string> = {
31
34
  groq: "GROQ_API_KEY",
32
35
  mistral: "MISTRAL_API_KEY",
33
36
  google: "GOOGLE_API_KEY",
37
+ deepseek: "DEEPSEEK_API_KEY",
38
+ together: "TOGETHER_API_KEY",
39
+ xai: "XAI_API_KEY",
40
+ huggingface: "HF_TOKEN",
34
41
  };
35
42
 
36
43
  /** Known embedding model dimensions (cloud providers). */
@@ -42,6 +49,8 @@ export const EMBEDDING_DIMS: Record<string, number> = {
42
49
  "ollama/mxbai-embed-large": 1024,
43
50
  "ollama/all-minilm": 384,
44
51
  "ollama/snowflake-arctic-embed": 1024,
52
+ "google/text-embedding-004": 768,
53
+ "huggingface/sentence-transformers/all-MiniLM-L6-v2": 384,
45
54
  };
46
55
 
47
56
  /** Provider display labels for UI. */
@@ -56,17 +65,21 @@ export const PROVIDER_LABELS: Record<string, string> = {
56
65
  xai: "xAI (Grok)",
57
66
  lmstudio: "LM Studio",
58
67
  "model-runner": "Docker Model Runner",
68
+ google: "Google AI",
69
+ huggingface: "Hugging Face",
59
70
  };
60
71
 
61
72
  /**
62
- * Map provider name → mem0-compatible provider name.
63
- * mem0 doesn't know "model-runner" or "lmstudio" both speak OpenAI protocol.
64
- * Ollama also maps to "openai" because Ollama exposes an OpenAI-compatible API
65
- * at /v1, and using provider "ollama" in mem0 requires the `ollama` Python
66
- * package which we don't install in the memory container.
73
+ * Map provider name → memory-package-compatible provider name.
74
+ * The memory package (@openpalm/memory) has native adapters for openai,
75
+ * ollama, and lmstudio. model-runner speaks OpenAI protocol so it maps
76
+ * to "openai". Ollama has its own adapter. LM Studio has its own adapter
77
+ * that avoids response_format (which LM Studio doesn't reliably support).
67
78
  */
68
79
  export function mem0ProviderName(provider: string): string {
69
- if (provider === "model-runner" || provider === "lmstudio" || provider === "ollama") return "openai";
80
+ if (provider === "model-runner") return "openai";
81
+ if (provider === "ollama") return "ollama";
82
+ if (provider === "lmstudio") return "lmstudio";
70
83
  return provider;
71
84
  }
72
85
 
@@ -82,7 +95,7 @@ export function mem0BaseUrlConfig(
82
95
  const trimmed = baseUrl.trim();
83
96
  if (!trimmed) return null;
84
97
 
85
- const normalized = trimmed.replace(/\/+$/, "");
98
+ const normalized = trimmed.replace(/\/+$/, "").replace(/\/v1$/, "");
86
99
  return { key: "openai_base_url", value: `${normalized}/v1` };
87
100
  }
88
101