@omnidev-ai/core 0.4.0 → 0.5.1

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 (41) hide show
  1. package/dist/index.d.ts +600 -664
  2. package/dist/index.js +1841 -1915
  3. package/dist/shared/chunk-1dqs11h6.js +20 -0
  4. package/dist/test-utils/index.d.ts +97 -101
  5. package/dist/test-utils/index.js +203 -234
  6. package/package.json +5 -3
  7. package/src/capability/AGENTS.md +58 -0
  8. package/src/capability/commands.ts +72 -0
  9. package/src/capability/docs.ts +48 -0
  10. package/src/capability/index.ts +20 -0
  11. package/src/capability/loader.ts +431 -0
  12. package/src/capability/registry.ts +55 -0
  13. package/src/capability/rules.ts +135 -0
  14. package/src/capability/skills.ts +58 -0
  15. package/src/capability/sources.ts +998 -0
  16. package/src/capability/subagents.ts +105 -0
  17. package/src/capability/yaml-parser.ts +81 -0
  18. package/src/config/AGENTS.md +46 -0
  19. package/src/config/capabilities.ts +54 -0
  20. package/src/config/env.ts +96 -0
  21. package/src/config/index.ts +6 -0
  22. package/src/config/loader.ts +207 -0
  23. package/src/config/parser.ts +55 -0
  24. package/src/config/profiles.ts +75 -0
  25. package/src/config/provider.ts +55 -0
  26. package/src/debug.ts +20 -0
  27. package/src/index.ts +37 -0
  28. package/src/mcp-json/index.ts +1 -0
  29. package/src/mcp-json/manager.ts +106 -0
  30. package/src/state/active-profile.ts +41 -0
  31. package/src/state/index.ts +3 -0
  32. package/src/state/manifest.ts +137 -0
  33. package/src/state/providers.ts +69 -0
  34. package/src/sync.ts +288 -0
  35. package/src/templates/agents.ts +14 -0
  36. package/src/templates/claude.ts +57 -0
  37. package/src/test-utils/helpers.ts +289 -0
  38. package/src/test-utils/index.ts +34 -0
  39. package/src/test-utils/mocks.ts +101 -0
  40. package/src/types/capability-export.ts +157 -0
  41. package/src/types/index.ts +314 -0
@@ -0,0 +1,75 @@
1
+ import { readActiveProfileState, writeActiveProfileState } from "../state/active-profile.js";
2
+ import type { OmniConfig, ProfileConfig } from "../types/index.js";
3
+ import { loadConfig, writeConfig } from "./loader.js";
4
+
5
+ /**
6
+ * Gets the name of the currently active profile.
7
+ * Reads from state file first, falls back to config.toml for backwards compatibility.
8
+ * Returns null if no profile is set.
9
+ */
10
+ 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;
20
+ }
21
+
22
+ /**
23
+ * Sets the active profile by writing to state file.
24
+ * @param name - The name of the profile to activate
25
+ */
26
+ export async function setActiveProfile(name: string): Promise<void> {
27
+ await writeActiveProfileState(name);
28
+ }
29
+
30
+ /**
31
+ * Resolves the enabled capabilities for a given profile
32
+ *
33
+ * @param config - The merged OmniConfig
34
+ * @param profileName - The name of the profile to apply, or null to use active
35
+ * @returns Array of capability IDs that should be enabled
36
+ */
37
+ export function resolveEnabledCapabilities(
38
+ config: OmniConfig,
39
+ profileName: string | null,
40
+ ): string[] {
41
+ // Determine which profile to use
42
+ const profile = profileName
43
+ ? config.profiles?.[profileName]
44
+ : config.profiles?.[config.active_profile ?? "default"];
45
+
46
+ const profileCapabilities = profile?.capabilities ?? [];
47
+ const alwaysEnabled = config.always_enabled_capabilities ?? [];
48
+
49
+ // Merge always-enabled capabilities with profile capabilities (no duplicates)
50
+ return [...new Set([...alwaysEnabled, ...profileCapabilities])];
51
+ }
52
+
53
+ /**
54
+ * Load a specific profile configuration from config.toml
55
+ * @param profileName - Name of the profile to load
56
+ * @returns ProfileConfig if found, undefined otherwise
57
+ */
58
+ export async function loadProfileConfig(profileName: string): Promise<ProfileConfig | undefined> {
59
+ const config = await loadConfig();
60
+ return config.profiles?.[profileName];
61
+ }
62
+
63
+ /**
64
+ * Set a profile configuration in config.toml
65
+ * @param profileName - Name of the profile to set
66
+ * @param profileConfig - Profile configuration
67
+ */
68
+ export async function setProfile(profileName: string, profileConfig: ProfileConfig): Promise<void> {
69
+ const config = await loadConfig();
70
+ if (!config.profiles) {
71
+ config.profiles = {};
72
+ }
73
+ config.profiles[profileName] = profileConfig;
74
+ await writeConfig(config);
75
+ }
@@ -0,0 +1,55 @@
1
+ import { existsSync } from "node:fs";
2
+ import { parse } from "smol-toml";
3
+ import type { Provider, ProviderConfig } from "../types/index.js";
4
+
5
+ const PROVIDER_CONFIG_PATH = ".omni/provider.toml";
6
+
7
+ export async function loadProviderConfig(): Promise<ProviderConfig> {
8
+ if (!existsSync(PROVIDER_CONFIG_PATH)) {
9
+ return { provider: "claude" };
10
+ }
11
+
12
+ const content = await Bun.file(PROVIDER_CONFIG_PATH).text();
13
+ const parsed = parse(content) as unknown as ProviderConfig;
14
+ return parsed;
15
+ }
16
+
17
+ export async function writeProviderConfig(config: ProviderConfig): Promise<void> {
18
+ const lines: string[] = [];
19
+
20
+ lines.push("# OmniDev Provider Configuration");
21
+ lines.push("# Selected AI provider(s) for this project");
22
+ lines.push("#");
23
+ lines.push("# This file controls which AI provider(s) you're using:");
24
+ lines.push("# - claude: Generates .claude/claude.md instruction file");
25
+ lines.push("# - codex: Generates AGENTS.md instruction file");
26
+ lines.push("# - both: Generates both instruction files");
27
+ lines.push("");
28
+
29
+ if (config.providers && config.providers.length > 1) {
30
+ lines.push("# Multiple providers enabled");
31
+ lines.push(`providers = [${config.providers.map((p) => `"${p}"`).join(", ")}]`);
32
+ } else if (config.providers && config.providers.length === 1) {
33
+ lines.push("# Single provider");
34
+ lines.push(`provider = "${config.providers[0]}"`);
35
+ } else if (config.provider) {
36
+ lines.push("# Single provider");
37
+ lines.push(`provider = "${config.provider}"`);
38
+ } else {
39
+ lines.push("# Default: Claude");
40
+ lines.push('provider = "claude"');
41
+ }
42
+
43
+ await Bun.write(PROVIDER_CONFIG_PATH, `${lines.join("\n")}\n`);
44
+ }
45
+
46
+ export function parseProviderFlag(flag: string): Provider[] {
47
+ const lower = flag.toLowerCase();
48
+ if (lower === "both") {
49
+ return ["claude", "codex"];
50
+ }
51
+ if (lower === "claude" || lower === "codex") {
52
+ return [lower as Provider];
53
+ }
54
+ throw new Error(`Invalid provider: ${flag}. Must be 'claude', 'codex', or 'both'.`);
55
+ }
package/src/debug.ts ADDED
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Debug logger that writes to stdout when OMNIDEV_DEBUG=1
3
+ */
4
+ export function debug(message: string, data?: unknown): void {
5
+ if (process.env["OMNIDEV_DEBUG"] !== "1") {
6
+ return;
7
+ }
8
+
9
+ const timestamp = new Date().toISOString();
10
+ let logLine: string;
11
+
12
+ if (data !== undefined) {
13
+ logLine = `[${timestamp}] [omnidev] ${message} ${JSON.stringify(data, null, 2)}`;
14
+ } else {
15
+ logLine = `[${timestamp}] [omnidev] ${message}`;
16
+ }
17
+
18
+ // Write to stdout
19
+ console.log(logLine);
20
+ }
package/src/index.ts ADDED
@@ -0,0 +1,37 @@
1
+ /**
2
+ * @omnidev-ai/core - Core functionality for OmniDev
3
+ *
4
+ * This package contains shared types, utilities, and core logic
5
+ * used across the CLI and capability tooling packages.
6
+ */
7
+
8
+ // Re-export @stricli/core for capabilities to use
9
+ // This ensures all capabilities use the same @stricli/core instance as the CLI
10
+ export { buildCommand, buildRouteMap } from "@stricli/core";
11
+
12
+ export const version = "0.1.0";
13
+
14
+ export function getVersion(): string {
15
+ return version;
16
+ }
17
+
18
+ // Export capability system
19
+ export * from "./capability";
20
+
21
+ // Export config functionality
22
+ export * from "./config";
23
+ // Export MCP JSON management
24
+ export * from "./mcp-json";
25
+ // Export state management
26
+ export * from "./state";
27
+ // Export sync functionality
28
+ export * from "./sync";
29
+
30
+ // Export templates
31
+ export * from "./templates/agents";
32
+ export * from "./templates/claude";
33
+ // Export core types
34
+ export * from "./types";
35
+
36
+ // Export debug utilities
37
+ export * from "./debug";
@@ -0,0 +1 @@
1
+ export * from "./manager";
@@ -0,0 +1,106 @@
1
+ import { existsSync } from "node:fs";
2
+ import type { LoadedCapability, McpConfig } from "../types";
3
+ import type { ResourceManifest } from "../state/manifest";
4
+
5
+ /**
6
+ * MCP server configuration in .mcp.json
7
+ */
8
+ export interface McpServerConfig {
9
+ command: string;
10
+ args?: string[];
11
+ env?: Record<string, string>;
12
+ }
13
+
14
+ /**
15
+ * Structure of .mcp.json file
16
+ */
17
+ export interface McpJsonConfig {
18
+ mcpServers: Record<string, McpServerConfig>;
19
+ }
20
+
21
+ const MCP_JSON_PATH = ".mcp.json";
22
+
23
+ /**
24
+ * Read .mcp.json or return empty config if doesn't exist
25
+ */
26
+ export async function readMcpJson(): Promise<McpJsonConfig> {
27
+ if (!existsSync(MCP_JSON_PATH)) {
28
+ return { mcpServers: {} };
29
+ }
30
+
31
+ try {
32
+ const content = await Bun.file(MCP_JSON_PATH).text();
33
+ const parsed = JSON.parse(content);
34
+ return {
35
+ mcpServers: parsed.mcpServers || {},
36
+ };
37
+ } catch {
38
+ // If file is invalid JSON, return empty config
39
+ return { mcpServers: {} };
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Write .mcp.json, preserving non-OmniDev entries
45
+ */
46
+ export async function writeMcpJson(config: McpJsonConfig): Promise<void> {
47
+ await Bun.write(MCP_JSON_PATH, JSON.stringify(config, null, 2));
48
+ }
49
+
50
+ /**
51
+ * Build MCP server config from capability's mcp section
52
+ */
53
+ function buildMcpServerConfig(mcp: McpConfig): McpServerConfig {
54
+ const config: McpServerConfig = {
55
+ command: mcp.command,
56
+ };
57
+ if (mcp.args) {
58
+ config.args = mcp.args;
59
+ }
60
+ if (mcp.env) {
61
+ config.env = mcp.env;
62
+ }
63
+ return config;
64
+ }
65
+
66
+ /**
67
+ * Sync .mcp.json with enabled capability MCP servers
68
+ *
69
+ * Each capability with an [mcp] section is registered using its capability ID.
70
+ * Uses the previous manifest to track which MCPs were managed by OmniDev.
71
+ */
72
+ export async function syncMcpJson(
73
+ capabilities: LoadedCapability[],
74
+ previousManifest: ResourceManifest,
75
+ options: { silent?: boolean } = {},
76
+ ): Promise<void> {
77
+ const mcpJson = await readMcpJson();
78
+
79
+ // Collect all MCP server names from previous manifest
80
+ const previouslyManagedMcps = new Set<string>();
81
+ for (const resources of Object.values(previousManifest.capabilities)) {
82
+ for (const mcpName of resources.mcps) {
83
+ previouslyManagedMcps.add(mcpName);
84
+ }
85
+ }
86
+
87
+ // Remove previously managed MCPs
88
+ for (const serverName of previouslyManagedMcps) {
89
+ delete mcpJson.mcpServers[serverName];
90
+ }
91
+
92
+ // Add MCPs from all enabled capabilities
93
+ let addedCount = 0;
94
+ for (const cap of capabilities) {
95
+ if (cap.config.mcp) {
96
+ mcpJson.mcpServers[cap.id] = buildMcpServerConfig(cap.config.mcp);
97
+ addedCount++;
98
+ }
99
+ }
100
+
101
+ await writeMcpJson(mcpJson);
102
+
103
+ if (!options.silent) {
104
+ console.log(` - .mcp.json (${addedCount} MCP server(s))`);
105
+ }
106
+ }
@@ -0,0 +1,41 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+
3
+ const STATE_DIR = ".omni/state";
4
+ const ACTIVE_PROFILE_PATH = `${STATE_DIR}/active-profile`;
5
+
6
+ /**
7
+ * Read the active profile from state file.
8
+ * Returns null if no active profile is set in state.
9
+ */
10
+ export async function readActiveProfileState(): Promise<string | null> {
11
+ if (!existsSync(ACTIVE_PROFILE_PATH)) {
12
+ return null;
13
+ }
14
+
15
+ try {
16
+ const content = await Bun.file(ACTIVE_PROFILE_PATH).text();
17
+ const trimmed = content.trim();
18
+ return trimmed || null;
19
+ } catch {
20
+ return null;
21
+ }
22
+ }
23
+
24
+ /**
25
+ * Write the active profile to state file.
26
+ * @param profileName - The name of the profile to set as active
27
+ */
28
+ export async function writeActiveProfileState(profileName: string): Promise<void> {
29
+ mkdirSync(STATE_DIR, { recursive: true });
30
+ await Bun.write(ACTIVE_PROFILE_PATH, profileName);
31
+ }
32
+
33
+ /**
34
+ * Clear the active profile state (delete the state file).
35
+ */
36
+ export async function clearActiveProfileState(): Promise<void> {
37
+ if (existsSync(ACTIVE_PROFILE_PATH)) {
38
+ const file = Bun.file(ACTIVE_PROFILE_PATH);
39
+ await file.delete();
40
+ }
41
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./active-profile";
2
+ export * from "./manifest";
3
+ export * from "./providers";
@@ -0,0 +1,137 @@
1
+ import { existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import type { LoadedCapability } from "../types";
3
+
4
+ /**
5
+ * Resources provided by a single capability
6
+ */
7
+ export interface CapabilityResources {
8
+ skills: string[];
9
+ rules: string[];
10
+ commands: string[];
11
+ subagents: string[];
12
+ mcps: string[];
13
+ }
14
+
15
+ /**
16
+ * Manifest tracking which resources each capability provides.
17
+ * Used to clean up stale resources when capabilities are disabled.
18
+ */
19
+ export interface ResourceManifest {
20
+ /** Schema version for future migrations */
21
+ version: 1;
22
+ /** Last sync timestamp (ISO 8601) */
23
+ syncedAt: string;
24
+ /** Map of capability ID → resources it provides */
25
+ capabilities: Record<string, CapabilityResources>;
26
+ }
27
+
28
+ /**
29
+ * Result of cleaning up stale resources
30
+ */
31
+ export interface CleanupResult {
32
+ deletedSkills: string[];
33
+ deletedRules: string[];
34
+ deletedCommands: string[];
35
+ deletedSubagents: string[];
36
+ deletedMcps: string[];
37
+ }
38
+
39
+ const MANIFEST_PATH = ".omni/state/manifest.json";
40
+ const CURRENT_VERSION = 1;
41
+
42
+ /**
43
+ * Load the previous manifest from disk.
44
+ * Returns an empty manifest if the file doesn't exist.
45
+ */
46
+ export async function loadManifest(): Promise<ResourceManifest> {
47
+ if (!existsSync(MANIFEST_PATH)) {
48
+ return {
49
+ version: CURRENT_VERSION,
50
+ syncedAt: new Date().toISOString(),
51
+ capabilities: {},
52
+ };
53
+ }
54
+
55
+ const content = await Bun.file(MANIFEST_PATH).text();
56
+ return JSON.parse(content) as ResourceManifest;
57
+ }
58
+
59
+ /**
60
+ * Save the manifest to disk.
61
+ */
62
+ export async function saveManifest(manifest: ResourceManifest): Promise<void> {
63
+ mkdirSync(".omni/state", { recursive: true });
64
+ await Bun.write(MANIFEST_PATH, JSON.stringify(manifest, null, 2));
65
+ }
66
+
67
+ /**
68
+ * Build a manifest from the current registry capabilities.
69
+ */
70
+ export function buildManifestFromCapabilities(capabilities: LoadedCapability[]): ResourceManifest {
71
+ const manifest: ResourceManifest = {
72
+ version: CURRENT_VERSION,
73
+ syncedAt: new Date().toISOString(),
74
+ capabilities: {},
75
+ };
76
+
77
+ for (const cap of capabilities) {
78
+ const resources: CapabilityResources = {
79
+ skills: cap.skills.map((s) => s.name),
80
+ rules: cap.rules.map((r) => r.name),
81
+ commands: cap.commands.map((c) => c.name),
82
+ subagents: cap.subagents.map((s) => s.name),
83
+ mcps: cap.config.mcp ? [cap.id] : [],
84
+ };
85
+
86
+ manifest.capabilities[cap.id] = resources;
87
+ }
88
+
89
+ return manifest;
90
+ }
91
+
92
+ /**
93
+ * Delete resources for capabilities that are no longer enabled.
94
+ * Compares the previous manifest against current capability IDs
95
+ * and removes files/directories for capabilities not in the current set.
96
+ */
97
+ export async function cleanupStaleResources(
98
+ previousManifest: ResourceManifest,
99
+ currentCapabilityIds: Set<string>,
100
+ ): Promise<CleanupResult> {
101
+ const result: CleanupResult = {
102
+ deletedSkills: [],
103
+ deletedRules: [],
104
+ deletedCommands: [],
105
+ deletedSubagents: [],
106
+ deletedMcps: [],
107
+ };
108
+
109
+ for (const [capId, resources] of Object.entries(previousManifest.capabilities)) {
110
+ // Skip if capability is still enabled
111
+ if (currentCapabilityIds.has(capId)) {
112
+ continue;
113
+ }
114
+
115
+ // Delete skills (directories)
116
+ for (const skillName of resources.skills) {
117
+ const skillDir = `.claude/skills/${skillName}`;
118
+ if (existsSync(skillDir)) {
119
+ rmSync(skillDir, { recursive: true });
120
+ result.deletedSkills.push(skillName);
121
+ }
122
+ }
123
+
124
+ // Delete rules (individual files)
125
+ for (const ruleName of resources.rules) {
126
+ const rulePath = `.cursor/rules/omnidev-${ruleName}.mdc`;
127
+ if (existsSync(rulePath)) {
128
+ rmSync(rulePath);
129
+ result.deletedRules.push(ruleName);
130
+ }
131
+ }
132
+
133
+ // Future: Delete commands and subagents if they become file-based
134
+ }
135
+
136
+ return result;
137
+ }
@@ -0,0 +1,69 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import type { ProviderId } from "../types/index.js";
3
+
4
+ const STATE_DIR = ".omni/state";
5
+ const PROVIDERS_PATH = `${STATE_DIR}/providers.json`;
6
+
7
+ export interface ProvidersState {
8
+ enabled: ProviderId[];
9
+ }
10
+
11
+ const DEFAULT_PROVIDERS: ProviderId[] = ["claude-code"];
12
+
13
+ /**
14
+ * Read the enabled providers from local state.
15
+ * Returns default providers if no state file exists.
16
+ */
17
+ export async function readEnabledProviders(): Promise<ProviderId[]> {
18
+ if (!existsSync(PROVIDERS_PATH)) {
19
+ return DEFAULT_PROVIDERS;
20
+ }
21
+
22
+ try {
23
+ const content = await Bun.file(PROVIDERS_PATH).text();
24
+ const state = JSON.parse(content) as ProvidersState;
25
+ return state.enabled.length > 0 ? state.enabled : DEFAULT_PROVIDERS;
26
+ } catch {
27
+ return DEFAULT_PROVIDERS;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Write enabled providers to local state.
33
+ * @param providers - List of provider IDs to enable
34
+ */
35
+ export async function writeEnabledProviders(providers: ProviderId[]): Promise<void> {
36
+ mkdirSync(STATE_DIR, { recursive: true });
37
+ const state: ProvidersState = { enabled: providers };
38
+ await Bun.write(PROVIDERS_PATH, JSON.stringify(state, null, 2));
39
+ }
40
+
41
+ /**
42
+ * Enable a specific provider.
43
+ * @param providerId - The provider to enable
44
+ */
45
+ export async function enableProvider(providerId: ProviderId): Promise<void> {
46
+ const current = await readEnabledProviders();
47
+ if (!current.includes(providerId)) {
48
+ await writeEnabledProviders([...current, providerId]);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Disable a specific provider.
54
+ * @param providerId - The provider to disable
55
+ */
56
+ export async function disableProvider(providerId: ProviderId): Promise<void> {
57
+ const current = await readEnabledProviders();
58
+ const filtered = current.filter((p) => p !== providerId);
59
+ await writeEnabledProviders(filtered);
60
+ }
61
+
62
+ /**
63
+ * Check if a provider is enabled.
64
+ * @param providerId - The provider to check
65
+ */
66
+ export async function isProviderEnabled(providerId: ProviderId): Promise<boolean> {
67
+ const current = await readEnabledProviders();
68
+ return current.includes(providerId);
69
+ }