@omnidev-ai/core 0.10.0 → 0.11.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@omnidev-ai/core",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,7 +1,6 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { readFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
- import { validateEnv } from "../config/env";
5
4
  import { parseCapabilityConfig } from "../config/parser";
6
5
  import { loadCapabilityHooks } from "../hooks/loader.js";
7
6
  import type {
@@ -339,25 +338,14 @@ function convertCommandExports(commandExports: unknown[], capabilityId: string):
339
338
 
340
339
  /**
341
340
  * Loads a complete capability including config, skills, rules, docs, and exports.
342
- * Validates environment requirements before loading.
343
- *
344
341
  * @param capabilityPath - Path to the capability directory
345
- * @param env - Environment variables to validate against
346
342
  * @returns Fully loaded capability
347
- * @throws Error if validation fails or loading errors occur
343
+ * @throws Error if loading errors occur
348
344
  */
349
- export async function loadCapability(
350
- capabilityPath: string,
351
- env: Record<string, string>,
352
- ): Promise<LoadedCapability> {
345
+ export async function loadCapability(capabilityPath: string): Promise<LoadedCapability> {
353
346
  const config = await loadCapabilityConfig(capabilityPath);
354
347
  const id = config.capability.id;
355
348
 
356
- // Validate environment
357
- if (config.env) {
358
- validateEnv(config.env, env, id);
359
- }
360
-
361
349
  // Load content - programmatic takes precedence
362
350
  const exports = await importCapabilityExports(capabilityPath);
363
351
 
@@ -1,5 +1,4 @@
1
1
  import { getEnabledCapabilities } from "../config/capabilities";
2
- import { loadEnvironment } from "../config/env";
3
2
  import { mergeHooksConfigs } from "../hooks/merger.js";
4
3
  import type { HooksConfig, CapabilityHooks, Doc, LoadedCapability, Rule, Skill } from "../types";
5
4
  import { discoverCapabilities, loadCapability } from "./loader";
@@ -27,7 +26,6 @@ export interface CapabilityRegistry {
27
26
  * @returns Capability registry with helper functions
28
27
  */
29
28
  export async function buildCapabilityRegistry(): Promise<CapabilityRegistry> {
30
- const env = await loadEnvironment();
31
29
  const enabledIds = await getEnabledCapabilities();
32
30
 
33
31
  const capabilityPaths = await discoverCapabilities();
@@ -35,7 +33,7 @@ export async function buildCapabilityRegistry(): Promise<CapabilityRegistry> {
35
33
 
36
34
  for (const path of capabilityPaths) {
37
35
  try {
38
- const cap = await loadCapability(path, env);
36
+ const cap = await loadCapability(path);
39
37
 
40
38
  // Only add if enabled
41
39
  if (enabledIds.includes(cap.id)) {
@@ -8,7 +8,7 @@ import { getActiveProfile, resolveEnabledCapabilities } from "./profiles.js";
8
8
  */
9
9
  export async function getEnabledCapabilities(): Promise<string[]> {
10
10
  const config = await loadConfig();
11
- const activeProfile = (await getActiveProfile()) ?? config.active_profile ?? "default";
11
+ const activeProfile = (await getActiveProfile()) ?? "default";
12
12
  return resolveEnabledCapabilities(config, activeProfile);
13
13
  }
14
14
 
@@ -19,7 +19,7 @@ export async function getEnabledCapabilities(): Promise<string[]> {
19
19
  export async function enableCapability(capabilityId: string): Promise<void> {
20
20
  // Use loadBaseConfig to avoid writing local overrides to omni.toml
21
21
  const config = await loadBaseConfig();
22
- const activeProfile = (await getActiveProfile()) ?? config.active_profile ?? "default";
22
+ const activeProfile = (await getActiveProfile()) ?? "default";
23
23
 
24
24
  if (!config.profiles) {
25
25
  config.profiles = {};
@@ -42,7 +42,7 @@ export async function enableCapability(capabilityId: string): Promise<void> {
42
42
  export async function disableCapability(capabilityId: string): Promise<void> {
43
43
  // Use loadBaseConfig to avoid writing local overrides to omni.toml
44
44
  const config = await loadBaseConfig();
45
- const activeProfile = (await getActiveProfile()) ?? config.active_profile ?? "default";
45
+ const activeProfile = (await getActiveProfile()) ?? "default";
46
46
 
47
47
  if (!config.profiles?.[activeProfile]) {
48
48
  return; // Nothing to disable
@@ -15,9 +15,6 @@ const LOCAL_CONFIG = "omni.local.toml";
15
15
  function mergeConfigs(base: OmniConfig, override: OmniConfig): OmniConfig {
16
16
  const merged: OmniConfig = { ...base, ...override };
17
17
 
18
- // Deep merge env
19
- merged.env = { ...base.env, ...override.env };
20
-
21
18
  // Deep merge profiles
22
19
  merged.profiles = { ...base.profiles };
23
20
  for (const [name, profile] of Object.entries(override.profiles || {})) {
@@ -84,26 +81,6 @@ export async function writeConfig(config: OmniConfig): Promise<void> {
84
81
  function generateConfigToml(config: OmniConfig): string {
85
82
  const lines: string[] = [];
86
83
 
87
- lines.push("# =============================================================================");
88
- lines.push("# OmniDev Configuration");
89
- lines.push("# =============================================================================");
90
- lines.push("# This file defines your project's capabilities, profiles, and settings.");
91
- lines.push("#");
92
- lines.push("# Files:");
93
- lines.push("# • omni.toml - Main config (commit to share with team)");
94
- lines.push("# • omni.local.toml - Local overrides (add to .gitignore)");
95
- lines.push("# • omni.lock.toml - Version lock file (commit for reproducibility)");
96
- lines.push("#");
97
- lines.push("# Quick start:");
98
- lines.push("# 1. Add capability sources to [capabilities.sources]");
99
- lines.push("# 2. Reference them in your profiles");
100
- lines.push("# 3. Run: omnidev sync");
101
- lines.push("# 4. Switch profiles: omnidev profile use <name>");
102
- lines.push("");
103
-
104
- // Note: active_profile is stored in .omni/state/active-profile, not in config.toml
105
- // We still read it from config.toml for backwards compatibility, but don't write it here
106
-
107
84
  // Providers
108
85
  if (config.providers?.enabled && config.providers.enabled.length > 0) {
109
86
  lines.push("# AI providers to enable (claude, codex, or both)");
@@ -112,28 +89,6 @@ function generateConfigToml(config: OmniConfig): string {
112
89
  lines.push("");
113
90
  }
114
91
 
115
- // Environment variables
116
- lines.push("# =============================================================================");
117
- lines.push("# Environment Variables");
118
- lines.push("# =============================================================================");
119
- lines.push("# Global environment variables available to all capabilities.");
120
- // biome-ignore lint/suspicious/noTemplateCurlyInString: Example of env var syntax
121
- lines.push("# Use ${VAR_NAME} syntax to reference shell environment variables.");
122
- lines.push("#");
123
- if (config.env && Object.keys(config.env).length > 0) {
124
- lines.push("[env]");
125
- for (const [key, value] of Object.entries(config.env)) {
126
- lines.push(`${key} = "${value}"`);
127
- }
128
- } else {
129
- lines.push("# [env]");
130
- // biome-ignore lint/suspicious/noTemplateCurlyInString: Example of env var syntax
131
- lines.push('# DATABASE_URL = "${DATABASE_URL}"');
132
- // biome-ignore lint/suspicious/noTemplateCurlyInString: Example of env var syntax
133
- lines.push('# API_KEY = "${MY_API_KEY}"');
134
- }
135
- lines.push("");
136
-
137
92
  // Capability sources
138
93
  lines.push("# =============================================================================");
139
94
  lines.push("# Capability Sources");
@@ -198,6 +153,24 @@ function generateConfigToml(config: OmniConfig): string {
198
153
  }
199
154
  lines.push("");
200
155
 
156
+ // Always enabled capabilities (under [capabilities] section)
157
+ lines.push("# =============================================================================");
158
+ lines.push("# Always Enabled Capabilities");
159
+ lines.push("# =============================================================================");
160
+ lines.push("# Capabilities that load in ALL profiles, regardless of profile config.");
161
+ lines.push("# Useful for essential tools needed everywhere.");
162
+ lines.push("#");
163
+ const alwaysEnabled = config.capabilities?.always_enabled;
164
+ if (alwaysEnabled && alwaysEnabled.length > 0) {
165
+ const caps = alwaysEnabled.map((c) => `"${c}"`).join(", ");
166
+ lines.push(`[capabilities]`);
167
+ lines.push(`always_enabled = [${caps}]`);
168
+ } else {
169
+ lines.push("# [capabilities]");
170
+ lines.push('# always_enabled = ["git-tools", "linting"]');
171
+ }
172
+ lines.push("");
173
+
201
174
  // MCP servers
202
175
  lines.push("# =============================================================================");
203
176
  lines.push("# MCP Servers");
@@ -235,12 +208,10 @@ function generateConfigToml(config: OmniConfig): string {
235
208
  lines.push(`url = "${mcpConfig.url}"`);
236
209
  }
237
210
 
238
- // Environment variables (sub-table)
211
+ // Environment variables (inline table)
239
212
  if (mcpConfig.env && Object.keys(mcpConfig.env).length > 0) {
240
- lines.push(`[mcps.${name}.env]`);
241
- for (const [key, value] of Object.entries(mcpConfig.env)) {
242
- lines.push(`${key} = "${value}"`);
243
- }
213
+ const entries = Object.entries(mcpConfig.env).map(([key, value]) => `${key} = "${value}"`);
214
+ lines.push(`env = { ${entries.join(", ")} }`);
244
215
  }
245
216
 
246
217
  // Headers (sub-table)
@@ -263,27 +234,11 @@ function generateConfigToml(config: OmniConfig): string {
263
234
  lines.push('# command = "node"');
264
235
  lines.push('# args = ["./servers/database.js"]');
265
236
  lines.push('# cwd = "./mcp-servers"');
266
- lines.push("# [mcps.database.env]");
267
237
  // biome-ignore lint/suspicious/noTemplateCurlyInString: Example of env var syntax
268
- lines.push('# DB_URL = "${DATABASE_URL}"');
238
+ lines.push('# env = { DB_URL = "${DATABASE_URL}" }');
269
239
  lines.push("");
270
240
  }
271
241
 
272
- // Always enabled capabilities
273
- lines.push("# =============================================================================");
274
- lines.push("# Always Enabled Capabilities");
275
- lines.push("# =============================================================================");
276
- lines.push("# Capabilities that load in ALL profiles, regardless of profile config.");
277
- lines.push("# Useful for essential tools needed everywhere.");
278
- lines.push("#");
279
- if (config.always_enabled_capabilities && config.always_enabled_capabilities.length > 0) {
280
- const caps = config.always_enabled_capabilities.map((c) => `"${c}"`).join(", ");
281
- lines.push(`always_enabled_capabilities = [${caps}]`);
282
- } else {
283
- lines.push('# always_enabled_capabilities = ["git-tools", "linting"]');
284
- }
285
- lines.push("");
286
-
287
242
  // Profiles
288
243
  lines.push("# =============================================================================");
289
244
  lines.push("# Profiles");
@@ -1,5 +1,4 @@
1
1
  export * from "./capabilities";
2
- export * from "./env";
3
2
  export * from "./config";
4
3
  export * from "./parser";
5
4
  export * from "./profiles";
@@ -3,20 +3,11 @@ import type { OmniConfig, ProfileConfig } from "../types/index.js";
3
3
  import { loadConfig, writeConfig } from "./config.js";
4
4
 
5
5
  /**
6
- * Gets the name of the currently active profile.
7
- * Reads from state file first, falls back to config.toml for backwards compatibility.
6
+ * Gets the name of the currently active profile from the state file.
8
7
  * Returns null if no profile is set.
9
8
  */
10
9
  export async function getActiveProfile(): Promise<string | null> {
11
- // First check state file (new location)
12
- const stateProfile = await readActiveProfileState();
13
- if (stateProfile) {
14
- return stateProfile;
15
- }
16
-
17
- // Fall back to config.toml for backwards compatibility
18
- const config = await loadConfig();
19
- return config.active_profile ?? null;
10
+ return await readActiveProfileState();
20
11
  }
21
12
 
22
13
  /**
@@ -38,13 +29,11 @@ export function resolveEnabledCapabilities(
38
29
  config: OmniConfig,
39
30
  profileName: string | null,
40
31
  ): string[] {
41
- // Determine which profile to use
42
- const profile = profileName
43
- ? config.profiles?.[profileName]
44
- : config.profiles?.[config.active_profile ?? "default"];
32
+ // Use the default profile if no profile name is specified
33
+ const profile = profileName ? config.profiles?.[profileName] : config.profiles?.["default"];
45
34
 
46
35
  const profileCapabilities = profile?.capabilities ?? [];
47
- const alwaysEnabled = config.always_enabled_capabilities ?? [];
36
+ const alwaysEnabled = config.capabilities?.always_enabled ?? [];
48
37
  const groups = config.capabilities?.groups ?? {};
49
38
 
50
39
  // Expand group references (group:name -> constituent capabilities)
@@ -150,12 +150,10 @@ function formatMcpConfig(name: string, config: McpConfig): string[] {
150
150
  lines.push(`url = "${config.url}"`);
151
151
  }
152
152
 
153
- // Environment variables
153
+ // Environment variables (inline table)
154
154
  if (config.env && Object.keys(config.env).length > 0) {
155
- lines.push(`[mcps.${name}.env]`);
156
- for (const [key, value] of Object.entries(config.env)) {
157
- lines.push(`${key} = "${value}"`);
158
- }
155
+ const entries = Object.entries(config.env).map(([key, value]) => `${key} = "${value}"`);
156
+ lines.push(`env = { ${entries.join(", ")} }`);
159
157
  }
160
158
 
161
159
  // Headers
package/src/sync.ts CHANGED
@@ -266,45 +266,24 @@ export async function syncAgentConfiguration(options?: SyncOptions): Promise<Syn
266
266
  }
267
267
 
268
268
  /**
269
- * Generate instructions.md content from rules and docs.
269
+ * Generate instructions.md content from rules.
270
270
  */
271
- function generateInstructionsContent(rules: SyncBundle["rules"], docs: SyncBundle["docs"]): string {
272
- if (rules.length === 0 && docs.length === 0) {
273
- return `## Capabilities
274
-
275
- No capabilities enabled yet. Run \`omnidev capability enable <name>\` to enable capabilities.`;
271
+ function generateInstructionsContent(
272
+ rules: SyncBundle["rules"],
273
+ _docs: SyncBundle["docs"],
274
+ ): string {
275
+ if (rules.length === 0) {
276
+ return "";
276
277
  }
277
278
 
278
- let content = `## Capabilities
279
-
280
- `;
281
-
282
- // Add documentation section if there are docs
283
- if (docs.length > 0) {
284
- content += `### Documentation
279
+ let content = `## Rules
285
280
 
286
281
  `;
287
- for (const doc of docs) {
288
- content += `#### ${doc.name} (from ${doc.capabilityId})
289
282
 
290
- ${doc.content}
283
+ for (const rule of rules) {
284
+ content += `${rule.content}
291
285
 
292
286
  `;
293
- }
294
- }
295
-
296
- // Add rules section if there are rules
297
- if (rules.length > 0) {
298
- content += `### Rules
299
-
300
- `;
301
- for (const rule of rules) {
302
- content += `#### ${rule.name} (from ${rule.capabilityId})
303
-
304
- ${rule.content}
305
-
306
- `;
307
- }
308
287
  }
309
288
 
310
289
  return content.trim();
@@ -71,35 +71,15 @@ TODO: Add example usage
71
71
 
72
72
  /**
73
73
  * Generate a rule markdown template file.
74
+ * Rules should start with a ### header and contain guidelines for AI agents.
74
75
  */
75
76
  export function generateRuleTemplate(ruleName: string): string {
76
- return `# ${formatDisplayName(ruleName)}
77
+ return `### ${formatDisplayName(ruleName)}
77
78
 
78
79
  <!-- Rules are guidelines that the AI agent should follow when working in this project -->
80
+ <!-- Each rule should start with a ### header -->
79
81
 
80
- ## Overview
81
-
82
- TODO: Describe what this rule enforces or guides.
83
-
84
- ## Guidelines
85
-
86
- - TODO: Add specific guidelines the AI should follow
87
- - Be specific and actionable
88
- - Include examples where helpful
89
-
90
- ## Examples
91
-
92
- ### Good
93
-
94
- \`\`\`
95
- TODO: Add example of correct behavior
96
- \`\`\`
97
-
98
- ### Bad
99
-
100
- \`\`\`
101
- TODO: Add example of incorrect behavior
102
- \`\`\`
82
+ TODO: Add specific guidelines the AI should follow. Be specific and actionable.
103
83
  `;
104
84
  }
105
85
 
@@ -6,9 +6,5 @@ export function generateClaudeTemplate(): string {
6
6
  return `# Project Instructions
7
7
 
8
8
  <!-- Add your project-specific instructions here -->
9
-
10
- ## OmniDev
11
-
12
- <!-- This section is populated during sync with capability rules and docs -->
13
9
  `;
14
10
  }
@@ -48,12 +48,6 @@ export interface CapabilityExports {
48
48
  gitignore?: string[];
49
49
  }
50
50
 
51
- export interface EnvDeclaration {
52
- required?: boolean;
53
- secret?: boolean;
54
- default?: string;
55
- }
56
-
57
51
  export interface SyncConfig {
58
52
  on_sync?: string;
59
53
  }
@@ -65,7 +59,6 @@ export interface CliConfig {
65
59
  export interface CapabilityConfig {
66
60
  capability: CapabilityMetadata;
67
61
  exports?: CapabilityExports;
68
- env?: Record<string, EnvDeclaration | Record<string, never>>;
69
62
  mcp?: McpConfig;
70
63
  sync?: SyncConfig;
71
64
  cli?: CliConfig;
@@ -84,7 +77,7 @@ export interface McpToolSchema {
84
77
  *
85
78
  * - **stdio**: Local process using stdin/stdout (default)
86
79
  * - Requires: command
87
- * - Optional: args, env, cwd
80
+ * - Optional: args, cwd
88
81
  *
89
82
  * - **http**: Remote HTTP server (recommended for remote servers)
90
83
  * - Requires: url
@@ -258,6 +251,8 @@ export interface CapabilitiesConfig {
258
251
  sources?: Record<string, CapabilitySourceConfig>;
259
252
  /** Capability groups: group name -> array of capability IDs */
260
253
  groups?: Record<string, string[]>;
254
+ /** Capabilities that load in ALL profiles, regardless of profile config */
255
+ always_enabled?: string[];
261
256
  }
262
257
 
263
258
  // Config Types
@@ -266,15 +261,11 @@ export interface ProfileConfig {
266
261
  }
267
262
 
268
263
  export interface OmniConfig {
269
- project?: string;
270
- active_profile?: string;
271
- always_enabled_capabilities?: string[];
272
- env?: Record<string, string>;
273
264
  profiles?: Record<string, ProfileConfig>;
274
265
  providers?: {
275
266
  enabled?: Provider[];
276
267
  };
277
- /** Capabilities configuration (enable/disable, sources) */
268
+ /** Capabilities configuration (enable/disable, sources, always_enabled) */
278
269
  capabilities?: CapabilitiesConfig;
279
270
  /** MCP server definitions that auto-generate capabilities */
280
271
  mcps?: Record<string, McpConfig>;
@@ -1,58 +0,0 @@
1
- # PROJECT KNOWLEDGE BASE
2
-
3
- **Generated:** 2026-01-12
4
- **Commit:** (not specified)
5
- **Branch:** (not specified)
6
-
7
- ## OVERVIEW
8
- Core capability loading system: discovers capabilities from .omni/capabilities/ & capabilities/, validates TOML configs, loads TypeScript exports (skills/rules/docs/commands/subagents), and builds runtime registry.
9
-
10
- ## WHERE TO LOOK
11
- | Task | Location | Notes |
12
- |------|----------|-------|
13
- | Capability discovery | loader.ts | Scans .omni/capabilities/ & capabilities/ for capability.toml |
14
- | Load TOML config | loader.ts | Validates required fields, checks reserved names |
15
- | Dynamic imports | loader.ts | Imports index.ts exports, handles missing deps gracefully |
16
- | Registry build | registry.ts | Filters by enabled caps, aggregates all content types |
17
- | Skills loading | skills.ts | Parses SKILL.md with YAML frontmatter |
18
- | Rules loading | rules.ts | Loads *.md from rules/ directory |
19
- | Docs loading | docs.ts | Loads definition.md + docs/*.md |
20
- | Commands loading | commands.ts | Parses COMMAND.md with YAML frontmatter |
21
- | Subagents loading | subagents.ts | Parses SUBAGENT.md, supports tools/skills/models |
22
- | Remote sources | sources.ts | Git clone/fetch, wrap external repos, lock file mgmt |
23
-
24
- ## CONVENTIONS
25
-
26
- **Capability Structure:**
27
- - Must have capability.toml in root
28
- - Optional: index.ts (exports: skills/rules/docs/commands/subagents/gitignore)
29
- - Optional directories: skills/, rules/, docs/, commands/, subagents/
30
- - Optional types.d.ts (for LLM type hints)
31
-
32
- **YAML Frontmatter Format:**
33
- - Skills/Commands: name, description (required)
34
- - Subagents: name, description + optional tools/disallowedTools/model/permissionMode/skills
35
- - Supports kebab-case keys (converted to camelCase)
36
-
37
- **Content Loading Priority:**
38
- - Programmatic exports (index.ts) take precedence over file-based content
39
- - Loader converts both old and new export formats automatically
40
-
41
- **Reserved Names:**
42
- - Node builtins (fs, path, http, crypto, os, etc.)
43
- - Common libs (react, vue, lodash, axios, express, typescript)
44
- - Prevents import conflicts with capability modules
45
-
46
- **Remote Capability Sources:**
47
- - Shorthand: "github:user/repo#ref"
48
- - Lock file: .omni/capabilities.lock.toml (tracks versions/commits)
49
- - Wrap mode: discovers skills/agents/commands in repo without capability.toml
50
- - Version: from package.json if available, else short commit hash
51
-
52
- ## ANTI-PATTERNS (THIS MODULE)
53
-
54
- - **NEVER** use reserved capability names (fs, path, react, typescript, etc.)
55
- - **NEVER** commit capabilities.lock.toml modifications - auto-generated
56
- - **NEVER** skip YAML frontmatter validation - name/description required
57
- - **NEVER** modify generated capability.toml in wrapped repos
58
- - **NEVER** assume index.ts exists - loader returns empty object if missing
@@ -1,46 +0,0 @@
1
- # CONFIGURATION SUBSYSTEM
2
-
3
- **Generated:** 2026-01-12T10:36:46
4
- **Commit:** (not specified)
5
- **Branch:** (not specified)
6
-
7
- ## OVERVIEW
8
- Configuration loading and parsing system using TOML format with profile-based capability management.
9
-
10
- ## WHERE TO LOOK
11
- | Task | Location | Notes |
12
- |------|----------|-------|
13
- | Config merge logic | loader.ts | Merges config.toml + config.local.toml |
14
- | Environment loading | env.ts | Loads .omni/.env, validates declarations |
15
- | TOML parsing | parser.ts | Uses smol-toml, validates capability.toml |
16
- | Provider selection | provider.ts | Loads provider.toml, parses CLI flags |
17
- | Profile management | profiles.ts | Active profile tracking, resolves capabilities |
18
- | Capability enable/disable | capabilities.ts | Updates profiles + gitignore patterns |
19
-
20
- ## CONVENTIONS
21
-
22
- **Config File Locations:**
23
- - `.omni/config.toml` - main config (project name, default profile, profiles)
24
- - `.omni/config.local.toml` - local overrides (gitignored)
25
- - `.omni/provider.toml` - provider selection (claude/codex/both)
26
- - `.omni/.env` - secrets, always gitignored
27
-
28
- **Profile-Based Capability Management:**
29
- - Profiles define capability sets in `[profiles.name].capabilities`
30
- - `active_profile` in config.toml selects current profile
31
- - `always_enabled_capabilities` list merged with profile capabilities
32
- - Use `enableCapability()` / `disableCapability()` to update active profile
33
-
34
- **Environment Variable Handling:**
35
- - capability.toml `[env]` section declares requirements
36
- - `required = true` → must be present or default
37
- - `secret = true` → masked in logs/error messages
38
- - `default = "value"` → optional with fallback
39
-
40
- ## ANTI-PATTERNS (THIS SUBSYSTEM)
41
-
42
- - **NEVER** edit config.toml directly for capability changes - use enableCapability()/disableCapability()
43
- - **NEVER** commit .omni/.env or config.local.toml - both gitignored
44
- - **NEVER** parse TOML manually - use parseOmniConfig() / parseCapabilityConfig()
45
- - **NEVER** ignore validateEnv() errors - required env vars block capability load
46
- - **NEVER** assume process.env is complete - merge with .omni/.env via loadEnvironment()
package/src/config/env.ts DELETED
@@ -1,97 +0,0 @@
1
- import { existsSync } from "node:fs";
2
- import { readFile } from "node:fs/promises";
3
- import type { EnvDeclaration } from "../types";
4
-
5
- const ENV_FILE = ".omni/.env";
6
-
7
- /**
8
- * Load environment variables from .omni/.env file and merge with process.env.
9
- * Process environment variables take precedence over file values.
10
- *
11
- * @returns Merged environment variables
12
- */
13
- export async function loadEnvironment(): Promise<Record<string, string>> {
14
- const env: Record<string, string> = {};
15
-
16
- // Load from .omni/.env
17
- if (existsSync(ENV_FILE)) {
18
- const content = await readFile(ENV_FILE, "utf-8");
19
- for (const line of content.split("\n")) {
20
- const trimmed = line.trim();
21
- // Skip empty lines and comments
22
- if (trimmed && !trimmed.startsWith("#")) {
23
- const eqIndex = trimmed.indexOf("=");
24
- if (eqIndex > 0) {
25
- const key = trimmed.slice(0, eqIndex).trim();
26
- const value = trimmed.slice(eqIndex + 1).trim();
27
- // Remove quotes if present
28
- const unquotedValue =
29
- (value.startsWith('"') && value.endsWith('"')) ||
30
- (value.startsWith("'") && value.endsWith("'"))
31
- ? value.slice(1, -1)
32
- : value;
33
- env[key] = unquotedValue;
34
- }
35
- }
36
- }
37
- }
38
-
39
- // Process env takes precedence - filter out undefined values
40
- const processEnv: Record<string, string> = {};
41
- for (const [key, value] of Object.entries(process.env)) {
42
- if (value !== undefined) {
43
- processEnv[key] = value;
44
- }
45
- }
46
-
47
- return { ...env, ...processEnv };
48
- }
49
-
50
- /**
51
- * Validate that all required environment variables are present.
52
- * Checks declarations from capability config and throws descriptive errors for missing vars.
53
- *
54
- * @param declarations - Environment variable declarations from capability.toml
55
- * @param env - Loaded environment variables
56
- * @param capabilityId - ID of the capability being validated
57
- * @throws Error if required environment variables are missing
58
- */
59
- export function validateEnv(
60
- declarations: Record<string, EnvDeclaration | Record<string, never>>,
61
- env: Record<string, string | undefined>,
62
- capabilityId: string,
63
- ): void {
64
- const missing: string[] = [];
65
-
66
- for (const [key, decl] of Object.entries(declarations)) {
67
- const declaration = decl as EnvDeclaration;
68
- const value = env[key] ?? declaration.default;
69
-
70
- if (declaration.required && !value) {
71
- missing.push(key);
72
- }
73
- }
74
-
75
- if (missing.length > 0) {
76
- throw new Error(
77
- `Missing required environment variable${missing.length > 1 ? "s" : ""} for capability "${capabilityId}": ${missing.join(", ")}. ` +
78
- `Set ${missing.length > 1 ? "them" : "it"} in .omni/.env or as environment variable${missing.length > 1 ? "s" : ""}.`,
79
- );
80
- }
81
- }
82
-
83
- /**
84
- * Check if an environment variable should be treated as a secret.
85
- * Secrets should be masked in logs and error messages.
86
- *
87
- * @param key - Environment variable name
88
- * @param declarations - Environment variable declarations from capability.toml
89
- * @returns true if the variable is marked as secret
90
- */
91
- export function isSecretEnvVar(
92
- key: string,
93
- declarations: Record<string, EnvDeclaration | Record<string, never>>,
94
- ): boolean {
95
- const decl = declarations[key] as EnvDeclaration | undefined;
96
- return decl?.secret === true;
97
- }