@omnidev-ai/cli 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/package.json +35 -0
- package/src/commands/AGENTS.md +43 -0
- package/src/commands/capability.test.ts +483 -0
- package/src/commands/capability.ts +163 -0
- package/src/commands/doctor.test.ts +197 -0
- package/src/commands/doctor.ts +164 -0
- package/src/commands/init.test.ts +265 -0
- package/src/commands/init.ts +192 -0
- package/src/commands/mcp.ts +113 -0
- package/src/commands/profile.test.ts +352 -0
- package/src/commands/profile.ts +151 -0
- package/src/commands/serve.test.ts +184 -0
- package/src/commands/serve.ts +63 -0
- package/src/commands/sync.ts +43 -0
- package/src/index.ts +55 -0
- package/src/lib/debug.ts +4 -0
- package/src/lib/dynamic-app.ts +182 -0
- package/src/prompts/provider.ts +15 -0
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { runInit } from "./init";
|
|
5
|
+
|
|
6
|
+
describe("init command", () => {
|
|
7
|
+
let testDir: string;
|
|
8
|
+
let originalCwd: string;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
originalCwd = process.cwd();
|
|
12
|
+
testDir = join(import.meta.dir, `test-init-${Date.now()}`);
|
|
13
|
+
mkdirSync(testDir, { recursive: true });
|
|
14
|
+
process.chdir(testDir);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
process.chdir(originalCwd);
|
|
19
|
+
if (existsSync(testDir)) {
|
|
20
|
+
rmSync(testDir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("creates .omni/ directory", async () => {
|
|
25
|
+
await runInit({}, "claude");
|
|
26
|
+
|
|
27
|
+
expect(existsSync(".omni")).toBe(true);
|
|
28
|
+
expect(existsSync(".omni/capabilities")).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("creates omni.toml with default config", async () => {
|
|
32
|
+
await runInit({}, "claude");
|
|
33
|
+
|
|
34
|
+
expect(existsSync("omni.toml")).toBe(true);
|
|
35
|
+
|
|
36
|
+
const content = readFileSync("omni.toml", "utf-8");
|
|
37
|
+
expect(content).toContain('project = "my-project"');
|
|
38
|
+
// active_profile is stored in state file, not config.toml
|
|
39
|
+
expect(content).not.toContain("active_profile");
|
|
40
|
+
// profiles should be in config.toml
|
|
41
|
+
expect(content).toContain("[profiles.default]");
|
|
42
|
+
expect(content).toContain("[profiles.planning]");
|
|
43
|
+
expect(content).toContain("[profiles.coding]");
|
|
44
|
+
// providers should be in config.toml
|
|
45
|
+
expect(content).toContain("[providers]");
|
|
46
|
+
// should have documentation comments
|
|
47
|
+
expect(content).toContain("# OmniDev Configuration");
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("creates active profile in state file", async () => {
|
|
51
|
+
await runInit({}, "claude");
|
|
52
|
+
|
|
53
|
+
expect(existsSync(".omni/state/active-profile")).toBe(true);
|
|
54
|
+
|
|
55
|
+
const content = readFileSync(".omni/state/active-profile", "utf-8");
|
|
56
|
+
expect(content).toBe("default");
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("does not create separate capabilities.toml file", async () => {
|
|
60
|
+
await runInit({}, "claude");
|
|
61
|
+
|
|
62
|
+
// All config is unified in config.toml
|
|
63
|
+
expect(existsSync(".omni/capabilities.toml")).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("does not create separate profiles.toml file", async () => {
|
|
67
|
+
await runInit({}, "claude");
|
|
68
|
+
|
|
69
|
+
// Profiles are in config.toml
|
|
70
|
+
expect(existsSync(".omni/profiles.toml")).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("creates .omni/ directory with subdirectories", async () => {
|
|
74
|
+
await runInit({}, "claude");
|
|
75
|
+
|
|
76
|
+
expect(existsSync(".omni")).toBe(true);
|
|
77
|
+
expect(existsSync(".omni/state")).toBe(true);
|
|
78
|
+
expect(existsSync(".omni/sandbox")).toBe(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("does not create separate provider.toml file", async () => {
|
|
82
|
+
await runInit({}, "claude");
|
|
83
|
+
|
|
84
|
+
// Providers are in config.toml
|
|
85
|
+
expect(existsSync(".omni/provider.toml")).toBe(false);
|
|
86
|
+
|
|
87
|
+
// Verify provider is in config.toml instead
|
|
88
|
+
const content = readFileSync("omni.toml", "utf-8");
|
|
89
|
+
expect(content).toContain('enabled = ["claude"]');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("creates AGENTS.md for Codex provider", async () => {
|
|
93
|
+
await runInit({}, "codex");
|
|
94
|
+
|
|
95
|
+
expect(existsSync("AGENTS.md")).toBe(true);
|
|
96
|
+
|
|
97
|
+
const content = readFileSync("AGENTS.md", "utf-8");
|
|
98
|
+
expect(content).toContain("# Project Instructions");
|
|
99
|
+
expect(content).toContain("@import .omni/instructions.md");
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("creates .omni/instructions.md", async () => {
|
|
103
|
+
await runInit({}, "codex");
|
|
104
|
+
|
|
105
|
+
expect(existsSync(".omni/instructions.md")).toBe(true);
|
|
106
|
+
|
|
107
|
+
const content = readFileSync(".omni/instructions.md", "utf-8");
|
|
108
|
+
expect(content).toContain("# OmniDev Instructions");
|
|
109
|
+
expect(content).toContain("## Project Description");
|
|
110
|
+
expect(content).toContain("<!-- TODO: Add 2-3 sentences describing your project -->");
|
|
111
|
+
expect(content).toContain("## Capabilities");
|
|
112
|
+
expect(content).toContain("No capabilities enabled yet");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("does not create AGENTS.md for Claude provider", async () => {
|
|
116
|
+
await runInit({}, "claude");
|
|
117
|
+
|
|
118
|
+
expect(existsSync("AGENTS.md")).toBe(false);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("creates CLAUDE.md for Claude provider", async () => {
|
|
122
|
+
await runInit({}, "claude");
|
|
123
|
+
|
|
124
|
+
expect(existsSync("CLAUDE.md")).toBe(true);
|
|
125
|
+
|
|
126
|
+
const content = readFileSync("CLAUDE.md", "utf-8");
|
|
127
|
+
expect(content).toContain("# Project Instructions");
|
|
128
|
+
expect(content).toContain("@import .omni/instructions.md");
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("does not create CLAUDE.md for Codex provider", async () => {
|
|
132
|
+
await runInit({}, "codex");
|
|
133
|
+
|
|
134
|
+
expect(existsSync("CLAUDE.md")).toBe(false);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("creates both AGENTS.md and CLAUDE.md for 'both' providers", async () => {
|
|
138
|
+
await runInit({}, "both");
|
|
139
|
+
|
|
140
|
+
expect(existsSync("AGENTS.md")).toBe(true);
|
|
141
|
+
expect(existsSync("CLAUDE.md")).toBe(true);
|
|
142
|
+
|
|
143
|
+
const agentsContent = readFileSync("AGENTS.md", "utf-8");
|
|
144
|
+
expect(agentsContent).toContain("# Project Instructions");
|
|
145
|
+
expect(agentsContent).toContain("@import .omni/instructions.md");
|
|
146
|
+
|
|
147
|
+
const claudeContent = readFileSync("CLAUDE.md", "utf-8");
|
|
148
|
+
expect(claudeContent).toContain("# Project Instructions");
|
|
149
|
+
expect(claudeContent).toContain("@import .omni/instructions.md");
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("does not modify existing CLAUDE.md", async () => {
|
|
153
|
+
const existingContent = "# My Existing Config\n\nExisting content here.\n";
|
|
154
|
+
await Bun.write("CLAUDE.md", existingContent);
|
|
155
|
+
|
|
156
|
+
await runInit({}, "claude");
|
|
157
|
+
|
|
158
|
+
const content = readFileSync("CLAUDE.md", "utf-8");
|
|
159
|
+
expect(content).toBe(existingContent);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("does not modify existing AGENTS.md", async () => {
|
|
163
|
+
const existingContent = "# My Existing Agents\n\nExisting content here.\n";
|
|
164
|
+
await Bun.write("AGENTS.md", existingContent);
|
|
165
|
+
|
|
166
|
+
await runInit({}, "codex");
|
|
167
|
+
|
|
168
|
+
const content = readFileSync("AGENTS.md", "utf-8");
|
|
169
|
+
expect(content).toBe(existingContent);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("creates .omni/.gitignore with internal patterns", async () => {
|
|
173
|
+
await runInit({}, "claude");
|
|
174
|
+
|
|
175
|
+
expect(existsSync(".omni/.gitignore")).toBe(true);
|
|
176
|
+
|
|
177
|
+
const content = readFileSync(".omni/.gitignore", "utf-8");
|
|
178
|
+
expect(content).toContain("# OmniDev working files - always ignored");
|
|
179
|
+
expect(content).toContain(".env");
|
|
180
|
+
expect(content).toContain("state/");
|
|
181
|
+
expect(content).toContain("sandbox/");
|
|
182
|
+
expect(content).toContain("*.log");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("does not modify project's root .gitignore", async () => {
|
|
186
|
+
// Create a root .gitignore with custom content
|
|
187
|
+
await Bun.write(".gitignore", "node_modules/\n*.log\n");
|
|
188
|
+
|
|
189
|
+
await runInit({}, "claude");
|
|
190
|
+
|
|
191
|
+
// Verify .gitignore was not modified
|
|
192
|
+
const content = readFileSync(".gitignore", "utf-8");
|
|
193
|
+
expect(content).toBe("node_modules/\n*.log\n");
|
|
194
|
+
expect(content).not.toContain(".omni/");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
test("does not create root .gitignore if it doesn't exist", async () => {
|
|
198
|
+
await runInit({}, "claude");
|
|
199
|
+
|
|
200
|
+
expect(existsSync(".gitignore")).toBe(false);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("is idempotent - safe to run multiple times", async () => {
|
|
204
|
+
await runInit({}, "claude");
|
|
205
|
+
await runInit({}, "claude");
|
|
206
|
+
await runInit({}, "claude");
|
|
207
|
+
|
|
208
|
+
expect(existsSync("omni.toml")).toBe(true);
|
|
209
|
+
expect(existsSync(".omni")).toBe(true);
|
|
210
|
+
expect(existsSync("CLAUDE.md")).toBe(true);
|
|
211
|
+
|
|
212
|
+
// Should not create AGENTS.md for Claude
|
|
213
|
+
expect(existsSync("AGENTS.md")).toBe(false);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("does not overwrite existing config.toml", async () => {
|
|
217
|
+
const customConfig = 'project = "custom"\n';
|
|
218
|
+
mkdirSync(".omni", { recursive: true });
|
|
219
|
+
await Bun.write("omni.toml", customConfig);
|
|
220
|
+
|
|
221
|
+
await runInit({}, "claude");
|
|
222
|
+
|
|
223
|
+
const content = readFileSync("omni.toml", "utf-8");
|
|
224
|
+
expect(content).toBe(customConfig);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test("does not overwrite existing AGENTS.md", async () => {
|
|
228
|
+
const customAgents = "# Custom agents\n";
|
|
229
|
+
await Bun.write("AGENTS.md", customAgents);
|
|
230
|
+
|
|
231
|
+
await runInit({}, "codex");
|
|
232
|
+
|
|
233
|
+
const content = readFileSync("AGENTS.md", "utf-8");
|
|
234
|
+
expect(content).toBe(customAgents);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
test("creates all directories even if some already exist", async () => {
|
|
238
|
+
mkdirSync(".omni", { recursive: true });
|
|
239
|
+
|
|
240
|
+
await runInit({}, "claude");
|
|
241
|
+
|
|
242
|
+
expect(existsSync(".omni/capabilities")).toBe(true);
|
|
243
|
+
expect(existsSync(".omni")).toBe(true);
|
|
244
|
+
expect(existsSync(".omni/state")).toBe(true);
|
|
245
|
+
expect(existsSync(".omni/sandbox")).toBe(true);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test("accepts provider via positional parameter", async () => {
|
|
249
|
+
await runInit({}, "codex");
|
|
250
|
+
|
|
251
|
+
expect(existsSync(".omni/provider.toml")).toBe(false);
|
|
252
|
+
|
|
253
|
+
const content = readFileSync("omni.toml", "utf-8");
|
|
254
|
+
expect(content).toContain('enabled = ["codex"]');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("accepts 'both' as provider parameter", async () => {
|
|
258
|
+
await runInit({}, "both");
|
|
259
|
+
|
|
260
|
+
expect(existsSync(".omni/provider.toml")).toBe(false);
|
|
261
|
+
|
|
262
|
+
const content = readFileSync("omni.toml", "utf-8");
|
|
263
|
+
expect(content).toContain('enabled = ["claude", "codex"]');
|
|
264
|
+
});
|
|
265
|
+
});
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
2
|
+
import type { Provider } from "@omnidev-ai/core";
|
|
3
|
+
import {
|
|
4
|
+
generateAgentsTemplate,
|
|
5
|
+
generateClaudeTemplate,
|
|
6
|
+
generateInstructionsTemplate,
|
|
7
|
+
parseProviderFlag,
|
|
8
|
+
setActiveProfile,
|
|
9
|
+
syncAgentConfiguration,
|
|
10
|
+
writeConfig,
|
|
11
|
+
} from "@omnidev-ai/core";
|
|
12
|
+
import { buildCommand } from "@stricli/core";
|
|
13
|
+
import { promptForProvider } from "../prompts/provider.js";
|
|
14
|
+
|
|
15
|
+
export async function runInit(_flags: Record<string, never>, provider?: string) {
|
|
16
|
+
console.log("Initializing OmniDev...");
|
|
17
|
+
|
|
18
|
+
// Create .omni/ directory structure
|
|
19
|
+
mkdirSync(".omni", { recursive: true });
|
|
20
|
+
mkdirSync(".omni/capabilities", { recursive: true });
|
|
21
|
+
mkdirSync(".omni/state", { recursive: true });
|
|
22
|
+
mkdirSync(".omni/sandbox", { recursive: true });
|
|
23
|
+
|
|
24
|
+
// Create .omni/.gitignore for internal working files
|
|
25
|
+
if (!existsSync(".omni/.gitignore")) {
|
|
26
|
+
await Bun.write(".omni/.gitignore", internalGitignore());
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Get provider selection
|
|
30
|
+
let providers: Provider[];
|
|
31
|
+
if (provider) {
|
|
32
|
+
providers = parseProviderFlag(provider);
|
|
33
|
+
} else {
|
|
34
|
+
providers = await promptForProvider();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Create omni.toml at project root
|
|
38
|
+
if (!existsSync("omni.toml")) {
|
|
39
|
+
await writeConfig({
|
|
40
|
+
project: "my-project",
|
|
41
|
+
providers: {
|
|
42
|
+
enabled: providers,
|
|
43
|
+
},
|
|
44
|
+
profiles: {
|
|
45
|
+
default: {
|
|
46
|
+
capabilities: [],
|
|
47
|
+
},
|
|
48
|
+
planning: {
|
|
49
|
+
capabilities: [],
|
|
50
|
+
},
|
|
51
|
+
coding: {
|
|
52
|
+
capabilities: [],
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
// Set active profile in state file (not omni.toml)
|
|
57
|
+
await setActiveProfile("default");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Create .omni/instructions.md
|
|
61
|
+
if (!existsSync(".omni/instructions.md")) {
|
|
62
|
+
await Bun.write(".omni/instructions.md", generateInstructionsTemplate());
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Create provider-specific files
|
|
66
|
+
const fileStatus = await createProviderFiles(providers);
|
|
67
|
+
|
|
68
|
+
// Run initial sync
|
|
69
|
+
await syncAgentConfiguration({ silent: false });
|
|
70
|
+
|
|
71
|
+
console.log("");
|
|
72
|
+
console.log(`✓ OmniDev initialized for ${providers.join(" and ")}!`);
|
|
73
|
+
console.log("");
|
|
74
|
+
|
|
75
|
+
// Show appropriate message based on file status
|
|
76
|
+
const hasNewFiles = fileStatus.created.length > 0;
|
|
77
|
+
const hasExistingFiles = fileStatus.existing.length > 0;
|
|
78
|
+
|
|
79
|
+
if (hasNewFiles) {
|
|
80
|
+
console.log("📝 Don't forget to add your project description to:");
|
|
81
|
+
console.log(" • .omni/instructions.md");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (hasExistingFiles) {
|
|
85
|
+
console.log("📝 Add this line to your existing file(s):");
|
|
86
|
+
for (const file of fileStatus.existing) {
|
|
87
|
+
console.log(` • ${file}: @import .omni/instructions.md`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
console.log("");
|
|
92
|
+
console.log("🔌 Add OmniDev MCP Server to your AI provider:");
|
|
93
|
+
console.log("");
|
|
94
|
+
console.log(" Add to Claude Desktop config:");
|
|
95
|
+
console.log(" {");
|
|
96
|
+
console.log(' "mcpServers": {');
|
|
97
|
+
console.log(' "omnidev": {');
|
|
98
|
+
console.log(' "command": "npx",');
|
|
99
|
+
console.log(' "args": ["-y", "@omnidev-ai/cli", "serve"]');
|
|
100
|
+
console.log(" }");
|
|
101
|
+
console.log(" }");
|
|
102
|
+
console.log(" }");
|
|
103
|
+
console.log("");
|
|
104
|
+
console.log(" Or for local development:");
|
|
105
|
+
console.log(" {");
|
|
106
|
+
console.log(' "mcpServers": {');
|
|
107
|
+
console.log(' "omnidev": {');
|
|
108
|
+
console.log(' "command": "bun",');
|
|
109
|
+
console.log(' "args": ["run", "omnidev", "serve"],');
|
|
110
|
+
console.log(' "cwd": "/path/to/your/project"');
|
|
111
|
+
console.log(" }");
|
|
112
|
+
console.log(" }");
|
|
113
|
+
console.log(" }");
|
|
114
|
+
console.log("");
|
|
115
|
+
console.log("📁 File structure:");
|
|
116
|
+
console.log(" • omni.toml - Main config (commit to share with team)");
|
|
117
|
+
console.log(" • omni.lock.toml - Lock file (commit for reproducibility)");
|
|
118
|
+
console.log(" • omni.local.toml - Local overrides (add to .gitignore)");
|
|
119
|
+
console.log(" • .omni/ - Runtime directory (add to .gitignore)");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export const initCommand = buildCommand({
|
|
123
|
+
parameters: {
|
|
124
|
+
flags: {},
|
|
125
|
+
positional: {
|
|
126
|
+
kind: "tuple" as const,
|
|
127
|
+
parameters: [
|
|
128
|
+
{
|
|
129
|
+
brief: "AI provider: claude, codex, or both",
|
|
130
|
+
parse: String,
|
|
131
|
+
optional: true,
|
|
132
|
+
},
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
docs: {
|
|
137
|
+
brief: "Initialize OmniDev in the current project",
|
|
138
|
+
},
|
|
139
|
+
func: runInit,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
async function createProviderFiles(
|
|
143
|
+
providers: Provider[],
|
|
144
|
+
): Promise<{ created: string[]; existing: string[] }> {
|
|
145
|
+
const created: string[] = [];
|
|
146
|
+
const existing: string[] = [];
|
|
147
|
+
|
|
148
|
+
// Create AGENTS.md for Codex
|
|
149
|
+
if (providers.includes("codex")) {
|
|
150
|
+
if (!existsSync("AGENTS.md")) {
|
|
151
|
+
await Bun.write("AGENTS.md", generateAgentsTemplate());
|
|
152
|
+
created.push("AGENTS.md");
|
|
153
|
+
} else {
|
|
154
|
+
existing.push("AGENTS.md");
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Create CLAUDE.md for Claude
|
|
159
|
+
if (providers.includes("claude")) {
|
|
160
|
+
if (!existsSync("CLAUDE.md")) {
|
|
161
|
+
await Bun.write("CLAUDE.md", generateClaudeTemplate());
|
|
162
|
+
created.push("CLAUDE.md");
|
|
163
|
+
} else {
|
|
164
|
+
existing.push("CLAUDE.md");
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { created, existing };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function internalGitignore(): string {
|
|
172
|
+
return `# OmniDev working files - always ignored
|
|
173
|
+
# These files change frequently and are machine-specific
|
|
174
|
+
|
|
175
|
+
# Secrets
|
|
176
|
+
.env
|
|
177
|
+
|
|
178
|
+
# Runtime state
|
|
179
|
+
state/
|
|
180
|
+
|
|
181
|
+
# Sandbox execution
|
|
182
|
+
sandbox/
|
|
183
|
+
|
|
184
|
+
# Logs
|
|
185
|
+
*.log
|
|
186
|
+
|
|
187
|
+
# MCP server process ID
|
|
188
|
+
server.pid
|
|
189
|
+
|
|
190
|
+
# Capability-specific patterns are appended below by each capability
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { buildCommand, buildRouteMap } from "@stricli/core";
|
|
3
|
+
|
|
4
|
+
const STATUS_FILE = ".omni/state/mcp-status.json";
|
|
5
|
+
|
|
6
|
+
// Local type definition to avoid circular dependency with @omnidev-ai/mcp
|
|
7
|
+
interface McpChildProcess {
|
|
8
|
+
capabilityId: string;
|
|
9
|
+
pid: number | null;
|
|
10
|
+
status: "starting" | "connected" | "disconnected" | "error";
|
|
11
|
+
transport: "stdio" | "sse" | "http";
|
|
12
|
+
lastHealthCheck?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
toolCount?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
interface McpStatusFile {
|
|
18
|
+
lastUpdated: string;
|
|
19
|
+
relayPort: number;
|
|
20
|
+
children: McpChildProcess[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Run the mcp status command.
|
|
25
|
+
*/
|
|
26
|
+
export async function runMcpStatus(): Promise<void> {
|
|
27
|
+
try {
|
|
28
|
+
if (!existsSync(STATUS_FILE)) {
|
|
29
|
+
console.log("No MCP status found.");
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log("Is the OmniDev server running? Start it with:");
|
|
32
|
+
console.log(" omnidev serve");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const statusText = await Bun.file(STATUS_FILE).text();
|
|
37
|
+
const status = JSON.parse(statusText) as McpStatusFile;
|
|
38
|
+
|
|
39
|
+
console.log("");
|
|
40
|
+
console.log("=== MCP Controller Status ===");
|
|
41
|
+
console.log("");
|
|
42
|
+
console.log(`Last updated: ${status.lastUpdated}`);
|
|
43
|
+
console.log(`Relay port: ${status.relayPort}`);
|
|
44
|
+
console.log("");
|
|
45
|
+
|
|
46
|
+
if (status.children.length === 0) {
|
|
47
|
+
console.log("No MCP children running.");
|
|
48
|
+
console.log("");
|
|
49
|
+
console.log("Enable a capability with [mcp] configuration to spawn child MCPs.");
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
console.log(`Child Processes (${status.children.length}):`);
|
|
54
|
+
console.log("");
|
|
55
|
+
|
|
56
|
+
for (const child of status.children) {
|
|
57
|
+
const statusIcon = getStatusIcon(child.status);
|
|
58
|
+
console.log(` ${statusIcon} ${child.capabilityId}`);
|
|
59
|
+
console.log(` Status: ${child.status}`);
|
|
60
|
+
console.log(` Transport: ${child.transport}`);
|
|
61
|
+
if (child.pid) {
|
|
62
|
+
console.log(` PID: ${child.pid}`);
|
|
63
|
+
}
|
|
64
|
+
if (child.toolCount !== undefined) {
|
|
65
|
+
console.log(` Tools: ${child.toolCount}`);
|
|
66
|
+
}
|
|
67
|
+
if (child.lastHealthCheck) {
|
|
68
|
+
console.log(` Last check: ${child.lastHealthCheck}`);
|
|
69
|
+
}
|
|
70
|
+
if (child.error) {
|
|
71
|
+
console.log(` Error: ${child.error}`);
|
|
72
|
+
}
|
|
73
|
+
console.log("");
|
|
74
|
+
}
|
|
75
|
+
} catch (error) {
|
|
76
|
+
console.error("Error reading MCP status:", error);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getStatusIcon(status: string): string {
|
|
82
|
+
switch (status) {
|
|
83
|
+
case "connected":
|
|
84
|
+
return "\u2713"; // checkmark
|
|
85
|
+
case "starting":
|
|
86
|
+
return "\u2022"; // bullet
|
|
87
|
+
case "disconnected":
|
|
88
|
+
return "\u2717"; // x mark
|
|
89
|
+
case "error":
|
|
90
|
+
return "\u2717"; // x mark
|
|
91
|
+
default:
|
|
92
|
+
return "?";
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const statusCommand = buildCommand({
|
|
97
|
+
docs: {
|
|
98
|
+
brief: "Show MCP controller status",
|
|
99
|
+
},
|
|
100
|
+
parameters: {},
|
|
101
|
+
async func() {
|
|
102
|
+
await runMcpStatus();
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
export const mcpRoutes = buildRouteMap({
|
|
107
|
+
routes: {
|
|
108
|
+
status: statusCommand,
|
|
109
|
+
},
|
|
110
|
+
docs: {
|
|
111
|
+
brief: "MCP controller commands",
|
|
112
|
+
},
|
|
113
|
+
});
|