@openpalm/lib 0.10.2 → 0.11.0-beta.10
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/README.md +4 -2
- package/package.json +11 -3
- package/src/control-plane/akm-vault.test.ts +105 -0
- package/src/control-plane/akm-vault.ts +311 -0
- package/src/control-plane/channels.ts +11 -9
- package/src/control-plane/cleanup-guardrails.test.ts +8 -9
- package/src/control-plane/compose-args.test.ts +25 -33
- package/src/control-plane/compose-args.ts +0 -4
- package/src/control-plane/compose-errors.test.ts +106 -0
- package/src/control-plane/compose-errors.ts +117 -0
- package/src/control-plane/config-persistence.ts +148 -73
- package/src/control-plane/core-assets.test.ts +104 -0
- package/src/control-plane/core-assets.ts +111 -58
- package/src/control-plane/docker.ts +70 -25
- package/src/control-plane/env.test.ts +25 -1
- package/src/control-plane/env.ts +84 -1
- package/src/control-plane/home.ts +66 -69
- package/src/control-plane/host-opencode.test.ts +260 -0
- package/src/control-plane/host-opencode.ts +229 -0
- package/src/control-plane/install-edge-cases.test.ts +190 -292
- package/src/control-plane/install-lock.ts +157 -0
- package/src/control-plane/lifecycle.ts +65 -75
- package/src/control-plane/markdown-task.ts +200 -0
- package/src/control-plane/migrate-0110.test.ts +177 -0
- package/src/control-plane/migrate-0110.ts +99 -0
- package/src/control-plane/operator-ids.test.ts +130 -0
- package/src/control-plane/operator-ids.ts +89 -0
- package/src/control-plane/paths.ts +80 -0
- package/src/control-plane/provider-models.ts +154 -0
- package/src/control-plane/registry-components.test.ts +105 -27
- package/src/control-plane/registry.test.ts +247 -51
- package/src/control-plane/registry.ts +404 -54
- package/src/control-plane/rollback.ts +17 -16
- package/src/control-plane/scheduler.ts +75 -262
- package/src/control-plane/secret-mappings.ts +4 -8
- package/src/control-plane/secrets.ts +97 -55
- package/src/control-plane/setup-config.schema.json +5 -17
- package/src/control-plane/setup-status.ts +9 -29
- package/src/control-plane/setup-validation.ts +23 -23
- package/src/control-plane/setup.test.ts +143 -244
- package/src/control-plane/setup.ts +216 -133
- package/src/control-plane/skeleton-guardrail.test.ts +151 -0
- package/src/control-plane/spec-to-env.test.ts +75 -60
- package/src/control-plane/spec-to-env.ts +68 -153
- package/src/control-plane/stack-spec.test.ts +22 -84
- package/src/control-plane/stack-spec.ts +9 -89
- package/src/control-plane/types.ts +9 -29
- package/src/control-plane/ui-assets.ts +385 -0
- package/src/control-plane/validate.ts +44 -79
- package/src/index.ts +102 -56
- package/src/logger.test.ts +228 -0
- package/src/logger.ts +71 -1
- package/src/provider-constants.ts +22 -1
- package/src/control-plane/audit.ts +0 -40
- package/src/control-plane/env-schema-validation.test.ts +0 -118
- package/src/control-plane/lock.test.ts +0 -194
- package/src/control-plane/lock.ts +0 -176
- package/src/control-plane/memory-config.ts +0 -298
- package/src/control-plane/provider-config.ts +0 -34
- package/src/control-plane/redact-schema.ts +0 -50
- package/src/control-plane/secret-backend.test.ts +0 -359
- package/src/control-plane/secret-backend.ts +0 -322
- package/src/control-plane/spec-validator.ts +0 -159
|
@@ -1,298 +0,0 @@
|
|
|
1
|
-
/** Memory LLM & Embedding configuration management. */
|
|
2
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from "node:fs";
|
|
3
|
-
import { readStackEnv } from "./secrets.js";
|
|
4
|
-
import { EMBEDDING_DIMS, PROVIDER_DEFAULT_URLS } from "../provider-constants.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export type MemoryConfig = {
|
|
8
|
-
mem0: {
|
|
9
|
-
llm: { provider: string; config: Record<string, unknown> };
|
|
10
|
-
embedder: { provider: string; config: Record<string, unknown> };
|
|
11
|
-
vector_store: {
|
|
12
|
-
provider: "sqlite-vec" | "qdrant";
|
|
13
|
-
config: {
|
|
14
|
-
collection_name: string;
|
|
15
|
-
db_path?: string;
|
|
16
|
-
path?: string;
|
|
17
|
-
embedding_model_dims: number;
|
|
18
|
-
};
|
|
19
|
-
};
|
|
20
|
-
};
|
|
21
|
-
memory: { custom_instructions: string };
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
export const EMBED_PROVIDERS = [
|
|
26
|
-
"openai", "ollama", "huggingface", "lmstudio"
|
|
27
|
-
] as const;
|
|
28
|
-
|
|
29
|
-
/** Static model list for Anthropic (no listing API available). */
|
|
30
|
-
const ANTHROPIC_MODELS = [
|
|
31
|
-
"claude-opus-4-6",
|
|
32
|
-
"claude-sonnet-4-6",
|
|
33
|
-
"claude-opus-4-20250514",
|
|
34
|
-
"claude-sonnet-4-20250514",
|
|
35
|
-
"claude-haiku-4-5-20251001",
|
|
36
|
-
"claude-3-5-sonnet-20241022",
|
|
37
|
-
"claude-3-5-haiku-20241022",
|
|
38
|
-
];
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
export function resolveApiKey(apiKeyRef: string, configDir: string): string {
|
|
42
|
-
if (!apiKeyRef) return "";
|
|
43
|
-
if (!apiKeyRef.startsWith("env:")) return apiKeyRef;
|
|
44
|
-
|
|
45
|
-
const varName = apiKeyRef.slice(4);
|
|
46
|
-
if (process.env[varName]) return process.env[varName]!;
|
|
47
|
-
|
|
48
|
-
const secrets = readStackEnv(configDir);
|
|
49
|
-
return secrets[varName] ?? "";
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
export type ModelDiscoveryReason =
|
|
54
|
-
| 'none'
|
|
55
|
-
| 'provider_static'
|
|
56
|
-
| 'provider_http'
|
|
57
|
-
| 'missing_base_url'
|
|
58
|
-
| 'timeout'
|
|
59
|
-
| 'network';
|
|
60
|
-
|
|
61
|
-
export type ProviderModelsResult = {
|
|
62
|
-
models: string[];
|
|
63
|
-
status: 'ok' | 'recoverable_error';
|
|
64
|
-
reason: ModelDiscoveryReason;
|
|
65
|
-
error?: string;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
const HTTP_STATUS_LABELS: Record<number, string> = {
|
|
69
|
-
401: 'Invalid or missing API key',
|
|
70
|
-
403: 'Access denied — check API key permissions',
|
|
71
|
-
404: 'Endpoint not found — verify the base URL',
|
|
72
|
-
429: 'Rate limited — try again shortly',
|
|
73
|
-
500: 'Provider internal error',
|
|
74
|
-
502: 'Provider returned a bad gateway error',
|
|
75
|
-
503: 'Provider is temporarily unavailable',
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
export async function fetchProviderModels(
|
|
79
|
-
provider: string,
|
|
80
|
-
apiKeyRef: string,
|
|
81
|
-
baseUrl: string,
|
|
82
|
-
configDir: string
|
|
83
|
-
): Promise<ProviderModelsResult> {
|
|
84
|
-
try {
|
|
85
|
-
if (provider === "anthropic") {
|
|
86
|
-
return { models: [...ANTHROPIC_MODELS], status: 'ok', reason: 'provider_static' };
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const resolvedKey = resolveApiKey(apiKeyRef, configDir);
|
|
90
|
-
|
|
91
|
-
if (provider === "ollama") {
|
|
92
|
-
const base = baseUrl?.trim() || PROVIDER_DEFAULT_URLS.ollama;
|
|
93
|
-
const url = `${base.replace(/\/+$/, "")}/api/tags`;
|
|
94
|
-
const res = await fetch(url, { signal: AbortSignal.timeout(5000) });
|
|
95
|
-
if (!res.ok) {
|
|
96
|
-
return {
|
|
97
|
-
models: [],
|
|
98
|
-
status: 'recoverable_error',
|
|
99
|
-
reason: 'provider_http',
|
|
100
|
-
error: `Ollama API returned ${res.status}: ${(HTTP_STATUS_LABELS[res.status] ?? `HTTP ${res.status}`)}`,
|
|
101
|
-
};
|
|
102
|
-
}
|
|
103
|
-
const data = (await res.json()) as { models?: { name: string }[] };
|
|
104
|
-
const models = (data.models ?? []).map((m) => m.name).sort();
|
|
105
|
-
return { models, status: 'ok', reason: 'none' };
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const base = baseUrl?.trim() || PROVIDER_DEFAULT_URLS[provider] || "";
|
|
109
|
-
if (!base) {
|
|
110
|
-
return {
|
|
111
|
-
models: [],
|
|
112
|
-
status: 'recoverable_error',
|
|
113
|
-
reason: 'missing_base_url',
|
|
114
|
-
error: `No base URL configured for provider "${provider}"`,
|
|
115
|
-
};
|
|
116
|
-
}
|
|
117
|
-
const url = `${base.replace(/\/+$/, "")}/v1/models`;
|
|
118
|
-
|
|
119
|
-
const headers: Record<string, string> = {};
|
|
120
|
-
if (resolvedKey) {
|
|
121
|
-
headers["Authorization"] = `Bearer ${resolvedKey}`;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
const res = await fetch(url, { headers, signal: AbortSignal.timeout(5000) });
|
|
125
|
-
if (!res.ok) {
|
|
126
|
-
let detail = '';
|
|
127
|
-
try {
|
|
128
|
-
const json = JSON.parse(await res.text()) as Record<string, unknown>;
|
|
129
|
-
const errObj = json.error as Record<string, unknown> | string | undefined;
|
|
130
|
-
detail = (typeof errObj === 'object' && errObj !== null && typeof errObj.message === 'string') ? errObj.message
|
|
131
|
-
: typeof errObj === 'string' ? errObj
|
|
132
|
-
: typeof json.message === 'string' ? json.message
|
|
133
|
-
: typeof json.detail === 'string' ? json.detail : '';
|
|
134
|
-
} catch { /* ignore parse errors */ }
|
|
135
|
-
return {
|
|
136
|
-
models: [],
|
|
137
|
-
status: 'recoverable_error',
|
|
138
|
-
reason: 'provider_http',
|
|
139
|
-
error: detail
|
|
140
|
-
? `Provider API returned ${res.status}: ${detail}`
|
|
141
|
-
: `Provider API returned ${res.status}: ${(HTTP_STATUS_LABELS[res.status] ?? `HTTP ${res.status}`)}`,
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
const data = (await res.json()) as { data?: { id: string }[] };
|
|
145
|
-
const models = (data.data ?? []).map((m) => m.id).sort();
|
|
146
|
-
return { models, status: 'ok', reason: 'none' };
|
|
147
|
-
} catch (err) {
|
|
148
|
-
const message =
|
|
149
|
-
err instanceof Error && err.name === "TimeoutError"
|
|
150
|
-
? "Request timed out after 5s"
|
|
151
|
-
: String(err);
|
|
152
|
-
return {
|
|
153
|
-
models: [],
|
|
154
|
-
status: 'recoverable_error',
|
|
155
|
-
reason: err instanceof Error && err.name === 'TimeoutError' ? 'timeout' : 'network',
|
|
156
|
-
error: message,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
export function getDefaultConfig(): MemoryConfig {
|
|
163
|
-
return {
|
|
164
|
-
mem0: {
|
|
165
|
-
llm: {
|
|
166
|
-
provider: "openai",
|
|
167
|
-
config: {
|
|
168
|
-
model: "gpt-4o-mini",
|
|
169
|
-
temperature: 0.1,
|
|
170
|
-
max_tokens: 2000,
|
|
171
|
-
api_key: "env:OPENAI_API_KEY",
|
|
172
|
-
},
|
|
173
|
-
},
|
|
174
|
-
embedder: {
|
|
175
|
-
provider: "openai",
|
|
176
|
-
config: {
|
|
177
|
-
model: "text-embedding-3-small",
|
|
178
|
-
api_key: "env:OPENAI_API_KEY",
|
|
179
|
-
},
|
|
180
|
-
},
|
|
181
|
-
vector_store: {
|
|
182
|
-
provider: "sqlite-vec",
|
|
183
|
-
config: {
|
|
184
|
-
collection_name: "memory",
|
|
185
|
-
db_path: "/data/memory.db",
|
|
186
|
-
embedding_model_dims: 1536,
|
|
187
|
-
},
|
|
188
|
-
},
|
|
189
|
-
},
|
|
190
|
-
memory: { custom_instructions: "" },
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
export function readMemoryConfig(dataDir: string): MemoryConfig {
|
|
196
|
-
const path = `${dataDir}/memory/default_config.json`;
|
|
197
|
-
if (!existsSync(path)) return getDefaultConfig();
|
|
198
|
-
try {
|
|
199
|
-
return JSON.parse(readFileSync(path, "utf-8")) as MemoryConfig;
|
|
200
|
-
} catch {
|
|
201
|
-
return getDefaultConfig();
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
export function writeMemoryConfig(dataDir: string, config: MemoryConfig): void {
|
|
206
|
-
mkdirSync(`${dataDir}/memory`, { recursive: true });
|
|
207
|
-
writeFileSync(`${dataDir}/memory/default_config.json`, JSON.stringify(config, null, 2) + "\n");
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
export function ensureMemoryConfig(dataDir: string): void {
|
|
211
|
-
if (existsSync(`${dataDir}/memory/default_config.json`)) return;
|
|
212
|
-
writeMemoryConfig(dataDir, getDefaultConfig());
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
export type VectorDimensionResult = {
|
|
217
|
-
match: boolean;
|
|
218
|
-
currentDims?: number;
|
|
219
|
-
expectedDims: number;
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
export function checkVectorDimensions(
|
|
223
|
-
dataDir: string,
|
|
224
|
-
newConfig: MemoryConfig
|
|
225
|
-
): VectorDimensionResult {
|
|
226
|
-
const expectedDims = newConfig.mem0.vector_store.config.embedding_model_dims;
|
|
227
|
-
const persisted = readMemoryConfig(dataDir);
|
|
228
|
-
const currentDims = persisted.mem0.vector_store.config.embedding_model_dims;
|
|
229
|
-
return { match: currentDims === expectedDims, currentDims, expectedDims };
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function resetVectorStore(
|
|
233
|
-
dataDir: string
|
|
234
|
-
): { ok: boolean; error?: string } {
|
|
235
|
-
const persisted = readMemoryConfig(dataDir);
|
|
236
|
-
const configuredPath = persisted.mem0.vector_store.config.db_path;
|
|
237
|
-
|
|
238
|
-
let dbPath: string;
|
|
239
|
-
if (configuredPath && configuredPath.startsWith('/data/')) {
|
|
240
|
-
dbPath = `${dataDir}/memory/${configuredPath.slice('/data/'.length)}`;
|
|
241
|
-
} else if (configuredPath && !configuredPath.startsWith('/')) {
|
|
242
|
-
dbPath = `${dataDir}/memory/${configuredPath}`;
|
|
243
|
-
} else {
|
|
244
|
-
dbPath = `${dataDir}/memory/memory.db`;
|
|
245
|
-
}
|
|
246
|
-
const qdrantPath = `${dataDir}/memory/qdrant`;
|
|
247
|
-
try {
|
|
248
|
-
if (existsSync(dbPath)) {
|
|
249
|
-
rmSync(dbPath, { force: true });
|
|
250
|
-
}
|
|
251
|
-
for (const suffix of ['-wal', '-shm']) {
|
|
252
|
-
const walPath = `${dbPath}${suffix}`;
|
|
253
|
-
if (existsSync(walPath)) rmSync(walPath, { force: true });
|
|
254
|
-
}
|
|
255
|
-
if (existsSync(qdrantPath)) {
|
|
256
|
-
rmSync(qdrantPath, { recursive: true, force: true });
|
|
257
|
-
}
|
|
258
|
-
return { ok: true };
|
|
259
|
-
} catch (err) {
|
|
260
|
-
return { ok: false, error: String(err) };
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
async function callMemoryApi(path: string, init?: RequestInit): Promise<Response> {
|
|
266
|
-
const configured = process.env.MEMORY_API_URL?.trim() || process.env.OP_MEMORY_API_URL?.trim();
|
|
267
|
-
const bases = configured ? [configured.replace(/\/+$/, "")] : ["http://memory:8765", "http://127.0.0.1:8765"];
|
|
268
|
-
const token = process.env.MEMORY_AUTH_TOKEN?.trim();
|
|
269
|
-
const authHeaders: Record<string, string> = token ? { authorization: `Bearer ${token}` } : {};
|
|
270
|
-
let lastError: unknown;
|
|
271
|
-
|
|
272
|
-
for (let i = 0; i < bases.length; i++) {
|
|
273
|
-
try {
|
|
274
|
-
const headers = { ...authHeaders, ...(init?.headers as Record<string, string>) };
|
|
275
|
-
return await fetch(`${bases[i]}${path}`, { ...init, headers });
|
|
276
|
-
} catch (err) {
|
|
277
|
-
lastError = err;
|
|
278
|
-
if (i === bases.length - 1) throw err;
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
throw lastError ?? new Error("Memory API request failed");
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
export async function provisionMemoryUser(
|
|
285
|
-
userId: string,
|
|
286
|
-
): Promise<{ ok: boolean; error?: string }> {
|
|
287
|
-
try {
|
|
288
|
-
const res = await callMemoryApi("/api/v1/users", {
|
|
289
|
-
method: "POST",
|
|
290
|
-
headers: { "content-type": "application/json" },
|
|
291
|
-
body: JSON.stringify({ user_id: userId }),
|
|
292
|
-
signal: AbortSignal.timeout(5_000),
|
|
293
|
-
});
|
|
294
|
-
return { ok: res.ok };
|
|
295
|
-
} catch (err) {
|
|
296
|
-
return { ok: false, error: String(err) };
|
|
297
|
-
}
|
|
298
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
|
-
import type { ControlPlaneState } from './types.js';
|
|
3
|
-
|
|
4
|
-
export type SecretProviderConfig = {
|
|
5
|
-
provider: 'plaintext' | 'pass';
|
|
6
|
-
passwordStoreDir?: string;
|
|
7
|
-
passPrefix?: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
function providerConfigPath(state: ControlPlaneState): string {
|
|
11
|
-
return `${state.dataDir}/secrets/provider.json`;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function readSecretProviderConfig(state: ControlPlaneState): SecretProviderConfig | null {
|
|
15
|
-
const path = providerConfigPath(state);
|
|
16
|
-
if (!existsSync(path)) return null;
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
const parsed = JSON.parse(readFileSync(path, 'utf-8')) as SecretProviderConfig;
|
|
20
|
-
if (parsed?.provider === 'plaintext' || parsed?.provider === 'pass') {
|
|
21
|
-
return parsed;
|
|
22
|
-
}
|
|
23
|
-
} catch {
|
|
24
|
-
// ignore malformed provider config and fall back to schema detection
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
return null;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export function writeSecretProviderConfig(state: ControlPlaneState, config: SecretProviderConfig): void {
|
|
31
|
-
const dir = `${state.dataDir}/secrets`;
|
|
32
|
-
mkdirSync(dir, { recursive: true });
|
|
33
|
-
writeFileSync(providerConfigPath(state), JSON.stringify(config, null, 2) + '\n');
|
|
34
|
-
}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Auto-generates a `redact.env.schema` from the canonical secret mappings.
|
|
3
|
-
*
|
|
4
|
-
* This ensures that every env var carrying a secret is marked for redaction
|
|
5
|
-
* by varlock, without requiring manual maintenance of the schema file.
|
|
6
|
-
*/
|
|
7
|
-
import { getCoreSecretMappings } from './secret-mappings.js';
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Generate a redact.env.schema string from the canonical secret mappings.
|
|
11
|
-
*
|
|
12
|
-
* @param systemEnv - The current system env (used to discover dynamic channel secrets)
|
|
13
|
-
* @returns A complete `@env-spec` schema suitable for varlock redaction
|
|
14
|
-
*/
|
|
15
|
-
export function generateRedactSchema(systemEnv: Record<string, string>): string {
|
|
16
|
-
const lines: string[] = [
|
|
17
|
-
'# OpenPalm — Runtime Redaction Schema (auto-generated)',
|
|
18
|
-
'# Marks env vars as @sensitive so varlock redacts their values from',
|
|
19
|
-
'# stdout/stderr before they reach docker compose logs.',
|
|
20
|
-
'#',
|
|
21
|
-
'# @defaultSensitive=true',
|
|
22
|
-
'# @defaultRequired=false',
|
|
23
|
-
'# ---',
|
|
24
|
-
'',
|
|
25
|
-
];
|
|
26
|
-
|
|
27
|
-
const envKeys = new Set<string>();
|
|
28
|
-
for (const mapping of getCoreSecretMappings(systemEnv)) {
|
|
29
|
-
envKeys.add(mapping.envKey);
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
// Include container-runtime env names that differ from env-file keys
|
|
33
|
-
// (compose maps OP_MEMORY_TOKEN -> MEMORY_AUTH_TOKEN, etc.)
|
|
34
|
-
envKeys.add('ADMIN_TOKEN');
|
|
35
|
-
envKeys.add('MEMORY_AUTH_TOKEN');
|
|
36
|
-
envKeys.add('OPENCODE_SERVER_PASSWORD');
|
|
37
|
-
|
|
38
|
-
// Resolved capability API keys (written to stack.env by spec-to-env)
|
|
39
|
-
envKeys.add('OP_CAP_LLM_API_KEY');
|
|
40
|
-
envKeys.add('OP_CAP_EMBEDDINGS_API_KEY');
|
|
41
|
-
envKeys.add('OP_CAP_TTS_API_KEY');
|
|
42
|
-
envKeys.add('OP_CAP_STT_API_KEY');
|
|
43
|
-
envKeys.add('OP_CAP_SLM_API_KEY');
|
|
44
|
-
|
|
45
|
-
for (const key of [...envKeys].sort()) {
|
|
46
|
-
lines.push(`${key}=`);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return lines.join('\n') + '\n';
|
|
50
|
-
}
|