@uluops/setup 0.2.0 → 0.4.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/README.md +56 -53
- package/assets/agents/anxiety-reader-agent.md +464 -0
- package/assets/commands/agents/anxiety-reader.md +160 -0
- package/assets/commands/agents/api-contract.md +1 -0
- package/assets/commands/agents/architect.md +1 -0
- package/assets/commands/agents/aristotle-analyst.md +1 -0
- package/assets/commands/agents/aristotle-explorer.md +1 -0
- package/assets/commands/agents/aristotle-forecaster.md +1 -0
- package/assets/commands/agents/aristotle-validator.md +1 -0
- package/assets/commands/agents/assumption-excavator.md +1 -0
- package/assets/commands/agents/audit.md +1 -0
- package/assets/commands/agents/{validate.md → code-validate.md} +6 -5
- package/assets/commands/agents/docs-validate.md +1 -0
- package/assets/commands/agents/frontend.md +1 -0
- package/assets/commands/agents/mcp-validate.md +1 -0
- package/assets/commands/agents/optimize.md +1 -0
- package/assets/commands/agents/pattern-analyzer.md +1 -0
- package/assets/commands/agents/prompt-quality.md +1 -0
- package/assets/commands/agents/prompt-validate.md +1 -0
- package/assets/commands/agents/public-interface.md +1 -0
- package/assets/commands/agents/release.md +1 -0
- package/assets/commands/agents/security.md +1 -0
- package/assets/commands/agents/test-review.md +1 -0
- package/assets/commands/agents/type-safety.md +1 -0
- package/assets/commands/agents/workflow-synthesis.md +1 -0
- package/assets/commands/pipelines/aristotle.md +143 -0
- package/assets/commands/pipelines/ship.md +188 -0
- package/assets/commands/workflows/prompt-audit.md +37 -747
- package/dist/cli.js +251 -207
- package/dist/harnesses/claude-code.d.ts +8 -0
- package/dist/harnesses/claude-code.js +72 -0
- package/dist/harnesses/codex.d.ts +15 -0
- package/dist/harnesses/codex.js +53 -0
- package/dist/harnesses/gemini-cli.d.ts +16 -0
- package/dist/harnesses/gemini-cli.js +54 -0
- package/dist/harnesses/index.d.ts +18 -0
- package/dist/harnesses/index.js +45 -0
- package/dist/harnesses/opencode.d.ts +14 -0
- package/dist/harnesses/opencode.js +130 -0
- package/dist/harnesses/types.d.ts +87 -0
- package/dist/harnesses/types.js +24 -0
- package/dist/lib/agent-transform.d.ts +12 -0
- package/dist/lib/agent-transform.js +129 -0
- package/dist/lib/asset-catalog.d.ts +9 -0
- package/dist/lib/asset-catalog.js +56 -0
- package/dist/lib/atomic-write.d.ts +11 -0
- package/dist/lib/atomic-write.js +28 -0
- package/dist/lib/config-merger.d.ts +7 -1
- package/dist/lib/config-merger.js +34 -5
- package/dist/lib/display.d.ts +14 -0
- package/dist/lib/display.js +66 -0
- package/dist/lib/file-ops.d.ts +6 -0
- package/dist/lib/file-ops.js +22 -1
- package/dist/lib/hash.d.ts +1 -0
- package/dist/lib/hash.js +1 -0
- package/dist/lib/health.d.ts +2 -0
- package/dist/lib/health.js +10 -0
- package/dist/lib/manifest.d.ts +22 -5
- package/dist/lib/manifest.js +148 -13
- package/dist/lib/paths.d.ts +15 -3
- package/dist/lib/paths.js +71 -13
- package/dist/lib/settings-merger.d.ts +9 -1
- package/dist/lib/settings-merger.js +45 -17
- package/dist/steps/agents.d.ts +5 -1
- package/dist/steps/agents.js +59 -9
- package/dist/steps/auth.js +26 -10
- package/dist/steps/commands.d.ts +6 -1
- package/dist/steps/commands.js +87 -9
- package/dist/steps/detect.d.ts +3 -0
- package/dist/steps/detect.js +7 -0
- package/dist/steps/mcp.d.ts +6 -2
- package/dist/steps/mcp.js +46 -21
- package/dist/steps/metrics.d.ts +14 -10
- package/dist/steps/metrics.js +59 -89
- package/dist/steps/shell.d.ts +2 -0
- package/dist/steps/shell.js +16 -9
- package/dist/steps/signup.d.ts +6 -3
- package/dist/steps/signup.js +26 -14
- package/dist/steps/verify.d.ts +2 -2
- package/dist/steps/verify.js +84 -117
- package/package.json +32 -7
- package/assets/commands/workflows/aristotle.md +0 -543
- package/assets/commands/workflows/ship.md +0 -721
- package/dist/test/auth.test.d.ts +0 -1
- package/dist/test/auth.test.js +0 -43
- package/dist/test/config-io.test.d.ts +0 -1
- package/dist/test/config-io.test.js +0 -56
- package/dist/test/config-merger.test.d.ts +0 -1
- package/dist/test/config-merger.test.js +0 -94
- package/dist/test/detect.test.d.ts +0 -1
- package/dist/test/detect.test.js +0 -25
- package/dist/test/file-ops.test.d.ts +0 -1
- package/dist/test/file-ops.test.js +0 -100
- package/dist/test/hash.test.d.ts +0 -1
- package/dist/test/hash.test.js +0 -14
- package/dist/test/manifest.test.d.ts +0 -1
- package/dist/test/manifest.test.js +0 -78
- package/dist/test/paths.test.d.ts +0 -1
- package/dist/test/paths.test.js +0 -30
- package/dist/test/settings-merger.test.d.ts +0 -1
- package/dist/test/settings-merger.test.js +0 -167
- package/dist/test/shell-profile.test.d.ts +0 -1
- package/dist/test/shell-profile.test.js +0 -40
- package/dist/test/shell.test.d.ts +0 -1
- package/dist/test/shell.test.js +0 -71
- package/dist/test/signup.test.d.ts +0 -1
- package/dist/test/signup.test.js +0 -83
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claude Code Harness Profile
|
|
3
|
+
*
|
|
4
|
+
* Wraps existing config-merger.ts and settings-merger.ts logic
|
|
5
|
+
* behind the HarnessProfile abstraction.
|
|
6
|
+
*/
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
import { readConfig, mergeUluopsMcp, removeUluopsMcp, writeConfig, } from "../lib/config-merger.js";
|
|
9
|
+
import { readSettings, writeSettings, mergeUluopsHook, removeUluopsHook, hasUluopsHook, } from "../lib/settings-merger.js";
|
|
10
|
+
import { getClaudeHome, getClaudeJsonPath } from "../lib/paths.js";
|
|
11
|
+
const ULUOPS_SERVERS = ["uluops-tracker", "uluops-registry"];
|
|
12
|
+
class ClaudeCodeMcpConfig {
|
|
13
|
+
async read(path) {
|
|
14
|
+
return readConfig(path);
|
|
15
|
+
}
|
|
16
|
+
merge(config, apiKey) {
|
|
17
|
+
return mergeUluopsMcp(config, apiKey);
|
|
18
|
+
}
|
|
19
|
+
remove(config) {
|
|
20
|
+
return removeUluopsMcp(config);
|
|
21
|
+
}
|
|
22
|
+
async write(path, config) {
|
|
23
|
+
await writeConfig(path, config);
|
|
24
|
+
}
|
|
25
|
+
check(config) {
|
|
26
|
+
const servers = config["mcpServers"];
|
|
27
|
+
if (!servers)
|
|
28
|
+
return false;
|
|
29
|
+
return ULUOPS_SERVERS.every((name) => name in servers);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
class ClaudeCodeHooks {
|
|
33
|
+
async install(settingsPath, hookCommand, dryRun) {
|
|
34
|
+
if (dryRun)
|
|
35
|
+
return true;
|
|
36
|
+
const settings = await readSettings(settingsPath);
|
|
37
|
+
const merged = mergeUluopsHook(settings, hookCommand);
|
|
38
|
+
await writeSettings(settingsPath, merged);
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
async remove(settingsPath, dryRun) {
|
|
42
|
+
if (dryRun)
|
|
43
|
+
return;
|
|
44
|
+
const settings = await readSettings(settingsPath);
|
|
45
|
+
const cleaned = removeUluopsHook(settings);
|
|
46
|
+
await writeSettings(settingsPath, cleaned);
|
|
47
|
+
}
|
|
48
|
+
async check(settingsPath) {
|
|
49
|
+
const settings = await readSettings(settingsPath);
|
|
50
|
+
return hasUluopsHook(settings);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const home = getClaudeHome();
|
|
54
|
+
export const claudeCodeProfile = {
|
|
55
|
+
name: "claude-code",
|
|
56
|
+
displayName: "Claude Code",
|
|
57
|
+
homeDir: home,
|
|
58
|
+
agentFormat: "markdown",
|
|
59
|
+
factoryTarget: "claude-code",
|
|
60
|
+
agentExtension: ".md",
|
|
61
|
+
paths: {
|
|
62
|
+
home,
|
|
63
|
+
globalMcpConfig: getClaudeJsonPath(),
|
|
64
|
+
localMcpConfig: ".mcp.json",
|
|
65
|
+
agentsDir: join(home, "agents"),
|
|
66
|
+
commandsDir: join(home, "commands"),
|
|
67
|
+
settingsPath: join(home, "settings.json"),
|
|
68
|
+
toolsDir: join(home, "tools", "agent-metrics"),
|
|
69
|
+
},
|
|
70
|
+
mcpConfig: new ClaudeCodeMcpConfig(),
|
|
71
|
+
hooks: new ClaudeCodeHooks(),
|
|
72
|
+
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Harness Profile (Scaffold)
|
|
3
|
+
*
|
|
4
|
+
* Paths and metadata are verified from vendor docs.
|
|
5
|
+
* Codex uses TOML config with `mcp_servers` key (nested tables).
|
|
6
|
+
* Agent definitions are TOML, not markdown.
|
|
7
|
+
* Skills use a different path ($HOME/.agents/skills/) than agents (~/.codex/agents/).
|
|
8
|
+
*
|
|
9
|
+
* NOT YET TESTED with UluOps agents. McpConfigStrategy throws
|
|
10
|
+
* until integration testing is complete.
|
|
11
|
+
*
|
|
12
|
+
* Will require `smol-toml` dependency when fully implemented.
|
|
13
|
+
*/
|
|
14
|
+
import type { HarnessProfile } from "./types.js";
|
|
15
|
+
export declare const codexProfile: HarnessProfile;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex Harness Profile (Scaffold)
|
|
3
|
+
*
|
|
4
|
+
* Paths and metadata are verified from vendor docs.
|
|
5
|
+
* Codex uses TOML config with `mcp_servers` key (nested tables).
|
|
6
|
+
* Agent definitions are TOML, not markdown.
|
|
7
|
+
* Skills use a different path ($HOME/.agents/skills/) than agents (~/.codex/agents/).
|
|
8
|
+
*
|
|
9
|
+
* NOT YET TESTED with UluOps agents. McpConfigStrategy throws
|
|
10
|
+
* until integration testing is complete.
|
|
11
|
+
*
|
|
12
|
+
* Will require `smol-toml` dependency when fully implemented.
|
|
13
|
+
*/
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { HarnessNotTestedError } from "./types.js";
|
|
17
|
+
class CodexMcpConfig {
|
|
18
|
+
async read() {
|
|
19
|
+
throw new HarnessNotTestedError("Codex");
|
|
20
|
+
}
|
|
21
|
+
merge() {
|
|
22
|
+
throw new HarnessNotTestedError("Codex");
|
|
23
|
+
}
|
|
24
|
+
remove() {
|
|
25
|
+
throw new HarnessNotTestedError("Codex");
|
|
26
|
+
}
|
|
27
|
+
async write() {
|
|
28
|
+
throw new HarnessNotTestedError("Codex");
|
|
29
|
+
}
|
|
30
|
+
check() {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const home = join(homedir(), ".codex");
|
|
35
|
+
export const codexProfile = {
|
|
36
|
+
name: "codex",
|
|
37
|
+
displayName: "Codex",
|
|
38
|
+
homeDir: home,
|
|
39
|
+
agentFormat: "toml",
|
|
40
|
+
factoryTarget: "codex",
|
|
41
|
+
agentExtension: ".toml",
|
|
42
|
+
paths: {
|
|
43
|
+
home,
|
|
44
|
+
globalMcpConfig: join(home, "config.toml"),
|
|
45
|
+
localMcpConfig: ".codex/config.toml",
|
|
46
|
+
agentsDir: join(home, "agents"),
|
|
47
|
+
commandsDir: join(homedir(), ".agents", "skills"),
|
|
48
|
+
settingsPath: null,
|
|
49
|
+
toolsDir: null,
|
|
50
|
+
},
|
|
51
|
+
mcpConfig: new CodexMcpConfig(),
|
|
52
|
+
hooks: null,
|
|
53
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Harness Profile (Scaffold)
|
|
3
|
+
*
|
|
4
|
+
* Paths and metadata are verified from vendor docs.
|
|
5
|
+
* MCP config uses `mcpServers` key (same shape as Claude Code)
|
|
6
|
+
* but at a different file path (~/.gemini/settings.json).
|
|
7
|
+
*
|
|
8
|
+
* NOT YET TESTED with UluOps agents. McpConfigStrategy throws
|
|
9
|
+
* until integration testing is complete.
|
|
10
|
+
*
|
|
11
|
+
* Note: Gemini CLI cannot have underscores in MCP server names
|
|
12
|
+
* (FQN format mcp_serverName_toolName uses underscore as delimiter).
|
|
13
|
+
* Our names use hyphens — safe.
|
|
14
|
+
*/
|
|
15
|
+
import type { HarnessProfile } from "./types.js";
|
|
16
|
+
export declare const geminiCliProfile: HarnessProfile;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gemini CLI Harness Profile (Scaffold)
|
|
3
|
+
*
|
|
4
|
+
* Paths and metadata are verified from vendor docs.
|
|
5
|
+
* MCP config uses `mcpServers` key (same shape as Claude Code)
|
|
6
|
+
* but at a different file path (~/.gemini/settings.json).
|
|
7
|
+
*
|
|
8
|
+
* NOT YET TESTED with UluOps agents. McpConfigStrategy throws
|
|
9
|
+
* until integration testing is complete.
|
|
10
|
+
*
|
|
11
|
+
* Note: Gemini CLI cannot have underscores in MCP server names
|
|
12
|
+
* (FQN format mcp_serverName_toolName uses underscore as delimiter).
|
|
13
|
+
* Our names use hyphens — safe.
|
|
14
|
+
*/
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { HarnessNotTestedError } from "./types.js";
|
|
18
|
+
class GeminiMcpConfig {
|
|
19
|
+
async read() {
|
|
20
|
+
throw new HarnessNotTestedError("Gemini CLI");
|
|
21
|
+
}
|
|
22
|
+
merge() {
|
|
23
|
+
throw new HarnessNotTestedError("Gemini CLI");
|
|
24
|
+
}
|
|
25
|
+
remove() {
|
|
26
|
+
throw new HarnessNotTestedError("Gemini CLI");
|
|
27
|
+
}
|
|
28
|
+
async write() {
|
|
29
|
+
throw new HarnessNotTestedError("Gemini CLI");
|
|
30
|
+
}
|
|
31
|
+
check() {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const home = join(homedir(), ".gemini");
|
|
36
|
+
export const geminiCliProfile = {
|
|
37
|
+
name: "gemini-cli",
|
|
38
|
+
displayName: "Gemini CLI",
|
|
39
|
+
homeDir: home,
|
|
40
|
+
agentFormat: "markdown",
|
|
41
|
+
factoryTarget: "gemini-cli",
|
|
42
|
+
agentExtension: ".md",
|
|
43
|
+
paths: {
|
|
44
|
+
home,
|
|
45
|
+
globalMcpConfig: join(home, "settings.json"),
|
|
46
|
+
localMcpConfig: ".gemini/settings.json",
|
|
47
|
+
agentsDir: join(home, "agents"),
|
|
48
|
+
commandsDir: join(home, "commands"),
|
|
49
|
+
settingsPath: null,
|
|
50
|
+
toolsDir: null,
|
|
51
|
+
},
|
|
52
|
+
mcpConfig: new GeminiMcpConfig(),
|
|
53
|
+
hooks: null,
|
|
54
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness Registry
|
|
3
|
+
*
|
|
4
|
+
* Central registry for harness profiles. Resolves names/aliases
|
|
5
|
+
* and detects installed harnesses.
|
|
6
|
+
*/
|
|
7
|
+
import type { HarnessProfile } from "./types.js";
|
|
8
|
+
export type { HarnessProfile, HarnessPaths, McpConfigStrategy, HookStrategy, } from "./types.js";
|
|
9
|
+
export { ConfigParseError, HarnessNotTestedError, } from "./types.js";
|
|
10
|
+
export declare const ALL_PROFILES: readonly HarnessProfile[];
|
|
11
|
+
/** Resolve a harness name or alias to a canonical name. */
|
|
12
|
+
export declare function resolveHarnessName(input: string): string;
|
|
13
|
+
/** Get a harness profile by name or alias. Throws if not found. */
|
|
14
|
+
export declare function getProfile(name: string): HarnessProfile;
|
|
15
|
+
/** Detect which harnesses are installed by probing home directories. */
|
|
16
|
+
export declare function detectHarnesses(): HarnessProfile[];
|
|
17
|
+
/** List all available harness names (not aliases). */
|
|
18
|
+
export declare function listHarnesses(): string[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness Registry
|
|
3
|
+
*
|
|
4
|
+
* Central registry for harness profiles. Resolves names/aliases
|
|
5
|
+
* and detects installed harnesses.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { claudeCodeProfile } from "./claude-code.js";
|
|
9
|
+
import { opencodeProfile } from "./opencode.js";
|
|
10
|
+
import { geminiCliProfile } from "./gemini-cli.js";
|
|
11
|
+
import { codexProfile } from "./codex.js";
|
|
12
|
+
export { ConfigParseError, HarnessNotTestedError, } from "./types.js";
|
|
13
|
+
export const ALL_PROFILES = [
|
|
14
|
+
claudeCodeProfile,
|
|
15
|
+
opencodeProfile,
|
|
16
|
+
geminiCliProfile,
|
|
17
|
+
codexProfile,
|
|
18
|
+
];
|
|
19
|
+
const aliases = new Map([
|
|
20
|
+
["claude", "claude-code"],
|
|
21
|
+
["oc", "opencode"],
|
|
22
|
+
["gemini", "gemini-cli"],
|
|
23
|
+
]);
|
|
24
|
+
/** Resolve a harness name or alias to a canonical name. */
|
|
25
|
+
export function resolveHarnessName(input) {
|
|
26
|
+
return aliases.get(input) ?? input;
|
|
27
|
+
}
|
|
28
|
+
/** Get a harness profile by name or alias. Throws if not found. */
|
|
29
|
+
export function getProfile(name) {
|
|
30
|
+
const resolved = resolveHarnessName(name);
|
|
31
|
+
const profile = ALL_PROFILES.find((p) => p.name === resolved);
|
|
32
|
+
if (!profile) {
|
|
33
|
+
const available = ALL_PROFILES.map((p) => p.name).join(", ");
|
|
34
|
+
throw new Error(`Unknown harness "${name}". Available: ${available}`);
|
|
35
|
+
}
|
|
36
|
+
return profile;
|
|
37
|
+
}
|
|
38
|
+
/** Detect which harnesses are installed by probing home directories. */
|
|
39
|
+
export function detectHarnesses() {
|
|
40
|
+
return ALL_PROFILES.filter((p) => existsSync(p.paths.home));
|
|
41
|
+
}
|
|
42
|
+
/** List all available harness names (not aliases). */
|
|
43
|
+
export function listHarnesses() {
|
|
44
|
+
return ALL_PROFILES.map((p) => p.name);
|
|
45
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Harness Profile
|
|
3
|
+
*
|
|
4
|
+
* OpenCode uses JSON/JSONC config with a structurally different MCP shape:
|
|
5
|
+
* - `mcp` key (not `mcpServers`)
|
|
6
|
+
* - `type: "local"` required
|
|
7
|
+
* - `command` as flat array (not separate command/args)
|
|
8
|
+
* - `environment` (not `env`)
|
|
9
|
+
* - `enabled: true` and `timeout: 30000` recommended
|
|
10
|
+
*
|
|
11
|
+
* Verified working shape from ~/opencode.jsonc (2026-04-30).
|
|
12
|
+
*/
|
|
13
|
+
import type { HarnessProfile } from "./types.js";
|
|
14
|
+
export declare const opencodeProfile: HarnessProfile;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OpenCode Harness Profile
|
|
3
|
+
*
|
|
4
|
+
* OpenCode uses JSON/JSONC config with a structurally different MCP shape:
|
|
5
|
+
* - `mcp` key (not `mcpServers`)
|
|
6
|
+
* - `type: "local"` required
|
|
7
|
+
* - `command` as flat array (not separate command/args)
|
|
8
|
+
* - `environment` (not `env`)
|
|
9
|
+
* - `enabled: true` and `timeout: 30000` recommended
|
|
10
|
+
*
|
|
11
|
+
* Verified working shape from ~/opencode.jsonc (2026-04-30).
|
|
12
|
+
*/
|
|
13
|
+
import { readFile } from "node:fs/promises";
|
|
14
|
+
import { homedir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
17
|
+
import { ConfigParseError } from "./types.js";
|
|
18
|
+
import { atomicWrite } from "../lib/atomic-write.js";
|
|
19
|
+
const ULUOPS_SERVERS = ["uluops-tracker", "uluops-registry"];
|
|
20
|
+
class OpenCodeMcpConfig {
|
|
21
|
+
/** Maps requested path → actual resolved path (for .jsonc fallback). */
|
|
22
|
+
resolvedPaths = new Map();
|
|
23
|
+
async read(path) {
|
|
24
|
+
// Try the given path, then probe for .jsonc variant
|
|
25
|
+
let raw = null;
|
|
26
|
+
const candidates = [path, path.replace(/\.json$/, ".jsonc")];
|
|
27
|
+
for (const p of candidates) {
|
|
28
|
+
try {
|
|
29
|
+
raw = await readFile(p, "utf-8");
|
|
30
|
+
this.resolvedPaths.set(path, p);
|
|
31
|
+
break;
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Try next
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (raw === null) {
|
|
38
|
+
this.resolvedPaths.set(path, path);
|
|
39
|
+
return {};
|
|
40
|
+
}
|
|
41
|
+
try {
|
|
42
|
+
return parseJsonc(raw);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
throw new ConfigParseError(path, err);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
merge(config, apiKey) {
|
|
49
|
+
const raw = config["mcp"];
|
|
50
|
+
const existing = (typeof raw === "object" && raw !== null ? raw : {});
|
|
51
|
+
const tracker = {
|
|
52
|
+
type: "local",
|
|
53
|
+
command: ["npx", "-y", "uluops-tracker-mcp-client"],
|
|
54
|
+
enabled: true,
|
|
55
|
+
timeout: 30000,
|
|
56
|
+
environment: {
|
|
57
|
+
ULUOPS_TRACKER_API_URL: "https://api.uluops.ai/api/v1",
|
|
58
|
+
ULUOPS_TRACKER_API_KEY: apiKey,
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
const registry = {
|
|
62
|
+
type: "local",
|
|
63
|
+
command: ["npx", "-y", "uluops-registry-mcp-client"],
|
|
64
|
+
enabled: true,
|
|
65
|
+
timeout: 30000,
|
|
66
|
+
environment: {
|
|
67
|
+
ULUOPS_REGISTRY_URL: "https://api.uluops.ai/api/v1/registry",
|
|
68
|
+
ULUOPS_API_KEY: apiKey,
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
return {
|
|
72
|
+
...config,
|
|
73
|
+
mcp: {
|
|
74
|
+
...existing,
|
|
75
|
+
"uluops-tracker": tracker,
|
|
76
|
+
"uluops-registry": registry,
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
remove(config) {
|
|
81
|
+
if (!config["mcp"] || typeof config["mcp"] !== "object")
|
|
82
|
+
return config;
|
|
83
|
+
const mcp = { ...config["mcp"] };
|
|
84
|
+
for (const name of ULUOPS_SERVERS) {
|
|
85
|
+
delete mcp[name];
|
|
86
|
+
}
|
|
87
|
+
const result = { ...config };
|
|
88
|
+
if (Object.keys(mcp).length === 0) {
|
|
89
|
+
delete result["mcp"];
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
result["mcp"] = mcp;
|
|
93
|
+
}
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
async write(path, config) {
|
|
97
|
+
// Write back to the path that was actually read (may be .jsonc)
|
|
98
|
+
const target = this.resolvedPaths.get(path) ?? path;
|
|
99
|
+
await atomicWrite(target, JSON.stringify(config, null, 2) + "\n", {
|
|
100
|
+
mode: 0o600,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
check(config) {
|
|
104
|
+
const mcp = config["mcp"];
|
|
105
|
+
if (!mcp)
|
|
106
|
+
return false;
|
|
107
|
+
return ULUOPS_SERVERS.every((name) => name in mcp);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
const xdgConfig = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
|
|
111
|
+
const home = join(xdgConfig, "opencode");
|
|
112
|
+
export const opencodeProfile = {
|
|
113
|
+
name: "opencode",
|
|
114
|
+
displayName: "OpenCode",
|
|
115
|
+
homeDir: home,
|
|
116
|
+
agentFormat: "markdown",
|
|
117
|
+
factoryTarget: "opencode",
|
|
118
|
+
agentExtension: ".md",
|
|
119
|
+
paths: {
|
|
120
|
+
home,
|
|
121
|
+
globalMcpConfig: join(home, "opencode.json"),
|
|
122
|
+
localMcpConfig: "opencode.json",
|
|
123
|
+
agentsDir: join(home, "agents"),
|
|
124
|
+
commandsDir: join(home, "commands"),
|
|
125
|
+
settingsPath: null,
|
|
126
|
+
toolsDir: null,
|
|
127
|
+
},
|
|
128
|
+
mcpConfig: new OpenCodeMcpConfig(),
|
|
129
|
+
hooks: null,
|
|
130
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness Type System
|
|
3
|
+
*
|
|
4
|
+
* Defines the abstraction layer for multi-harness support.
|
|
5
|
+
* Each harness (Claude Code, OpenCode, Codex, Gemini CLI) implements
|
|
6
|
+
* these interfaces to encapsulate its config format, paths, and capabilities.
|
|
7
|
+
*/
|
|
8
|
+
/** Static definition of a harness — no runtime state. */
|
|
9
|
+
export interface HarnessProfile {
|
|
10
|
+
/** Canonical name (e.g., 'claude-code', 'opencode', 'codex', 'gemini-cli') */
|
|
11
|
+
readonly name: string;
|
|
12
|
+
/** Display name for CLI output (e.g., 'Claude Code', 'OpenCode') */
|
|
13
|
+
readonly displayName: string;
|
|
14
|
+
/** Home directory for this harness */
|
|
15
|
+
readonly homeDir: string;
|
|
16
|
+
/** Agent definition format */
|
|
17
|
+
readonly agentFormat: "markdown" | "toml";
|
|
18
|
+
/** Target name for definition-factory rendering */
|
|
19
|
+
readonly factoryTarget: string;
|
|
20
|
+
/** File extension for agent definitions */
|
|
21
|
+
readonly agentExtension: ".md" | ".toml";
|
|
22
|
+
/** Paths for this harness */
|
|
23
|
+
readonly paths: HarnessPaths;
|
|
24
|
+
/** MCP config strategy for this harness's config format */
|
|
25
|
+
readonly mcpConfig: McpConfigStrategy;
|
|
26
|
+
/** Hook operations (null if harness doesn't support post-agent hooks) */
|
|
27
|
+
readonly hooks: HookStrategy | null;
|
|
28
|
+
}
|
|
29
|
+
export interface HarnessPaths {
|
|
30
|
+
/** Home dir (e.g., ~/.claude/) */
|
|
31
|
+
readonly home: string;
|
|
32
|
+
/** Global MCP config (e.g., ~/.claude.json, ~/.config/opencode/opencode.json) */
|
|
33
|
+
readonly globalMcpConfig: string;
|
|
34
|
+
/** Project-scoped MCP config filename (e.g., .mcp.json, opencode.json) — resolved relative to project root */
|
|
35
|
+
readonly localMcpConfig: string;
|
|
36
|
+
/** Global agent definitions dir */
|
|
37
|
+
readonly agentsDir: string;
|
|
38
|
+
/** Global commands/skills dir */
|
|
39
|
+
readonly commandsDir: string;
|
|
40
|
+
/** Settings file path, or null if harness has no settings file */
|
|
41
|
+
readonly settingsPath: string | null;
|
|
42
|
+
/** Tool installation dir, or null if harness has no tool installation */
|
|
43
|
+
readonly toolsDir: string | null;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Encapsulates format-specific MCP config read/merge/write.
|
|
47
|
+
* Claude Code/Gemini use JSON with `mcpServers` key.
|
|
48
|
+
* OpenCode uses JSON/JSONC with `mcp` key and different entry shape.
|
|
49
|
+
* Codex uses TOML with `mcp_servers` key.
|
|
50
|
+
*/
|
|
51
|
+
export interface McpConfigStrategy {
|
|
52
|
+
/** Read existing config from disk. Returns parsed object. Returns {} if file missing. Throws ConfigParseError if malformed. */
|
|
53
|
+
read(path: string): Promise<Record<string, unknown>>;
|
|
54
|
+
/** Merge UluOps MCP servers into the parsed config. Format-aware. */
|
|
55
|
+
merge(config: Record<string, unknown>, apiKey: string): Record<string, unknown>;
|
|
56
|
+
/** Remove UluOps MCP servers from the parsed config. */
|
|
57
|
+
remove(config: Record<string, unknown>): Record<string, unknown>;
|
|
58
|
+
/** Write config back to disk in the harness's native format. Uses atomic write. */
|
|
59
|
+
write(path: string, config: Record<string, unknown>): Promise<void>;
|
|
60
|
+
/** Check if UluOps MCP servers are present in the parsed config. */
|
|
61
|
+
check(config: Record<string, unknown>): boolean;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Hook strategy for harnesses that support post-agent execution hooks.
|
|
65
|
+
* Currently only Claude Code supports this (SubagentStop event).
|
|
66
|
+
*/
|
|
67
|
+
export interface HookStrategy {
|
|
68
|
+
/**
|
|
69
|
+
* Install the agent-metrics post-execution hook.
|
|
70
|
+
* @returns true if hook was installed or updated, false if already current (no-op).
|
|
71
|
+
* In dry-run mode, returns true (would install) without writing.
|
|
72
|
+
*/
|
|
73
|
+
install(settingsPath: string, hookCommand: string, dryRun: boolean): Promise<boolean>;
|
|
74
|
+
/** Remove the hook. No-op if not installed. */
|
|
75
|
+
remove(settingsPath: string, dryRun: boolean): Promise<void>;
|
|
76
|
+
/** Check if hook is currently installed. */
|
|
77
|
+
check(settingsPath: string): Promise<boolean>;
|
|
78
|
+
}
|
|
79
|
+
/** Thrown when a harness config file cannot be parsed. */
|
|
80
|
+
export declare class ConfigParseError extends Error {
|
|
81
|
+
readonly path: string;
|
|
82
|
+
constructor(path: string, cause: unknown);
|
|
83
|
+
}
|
|
84
|
+
/** Thrown when a harness is scaffolded but not yet tested/implemented. */
|
|
85
|
+
export declare class HarnessNotTestedError extends Error {
|
|
86
|
+
constructor(harnessName: string);
|
|
87
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Harness Type System
|
|
3
|
+
*
|
|
4
|
+
* Defines the abstraction layer for multi-harness support.
|
|
5
|
+
* Each harness (Claude Code, OpenCode, Codex, Gemini CLI) implements
|
|
6
|
+
* these interfaces to encapsulate its config format, paths, and capabilities.
|
|
7
|
+
*/
|
|
8
|
+
/** Thrown when a harness config file cannot be parsed. */
|
|
9
|
+
export class ConfigParseError extends Error {
|
|
10
|
+
path;
|
|
11
|
+
constructor(path, cause) {
|
|
12
|
+
const msg = cause instanceof Error ? cause.message : String(cause);
|
|
13
|
+
super(`Failed to parse config at ${path}: ${msg}`);
|
|
14
|
+
this.path = path;
|
|
15
|
+
this.name = "ConfigParseError";
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Thrown when a harness is scaffolded but not yet tested/implemented. */
|
|
19
|
+
export class HarnessNotTestedError extends Error {
|
|
20
|
+
constructor(harnessName) {
|
|
21
|
+
super(`${harnessName} harness is not yet tested. Use --harness claude-code or --harness opencode.`);
|
|
22
|
+
this.name = "HarnessNotTestedError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Transform
|
|
3
|
+
*
|
|
4
|
+
* Transforms Claude Code agent markdown to other harness formats at install time.
|
|
5
|
+
* Single source of truth: assets/agents/ contains Claude Code format only.
|
|
6
|
+
* Each harness gets its frontmatter rewritten; the body is identical across all targets.
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Transform a Claude Code agent markdown file to a target harness format.
|
|
10
|
+
* Returns the original content for claude-code; rewrites frontmatter for others.
|
|
11
|
+
*/
|
|
12
|
+
export declare function transformAgent(markdown: string, harnessName: string): string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Transform
|
|
3
|
+
*
|
|
4
|
+
* Transforms Claude Code agent markdown to other harness formats at install time.
|
|
5
|
+
* Single source of truth: assets/agents/ contains Claude Code format only.
|
|
6
|
+
* Each harness gets its frontmatter rewritten; the body is identical across all targets.
|
|
7
|
+
*/
|
|
8
|
+
function parseAgentMarkdown(markdown) {
|
|
9
|
+
const first = markdown.indexOf("---");
|
|
10
|
+
if (first === -1)
|
|
11
|
+
return { frontmatter: {}, body: markdown };
|
|
12
|
+
const second = markdown.indexOf("---", first + 3);
|
|
13
|
+
if (second === -1)
|
|
14
|
+
return { frontmatter: {}, body: markdown };
|
|
15
|
+
const fmBlock = markdown.substring(first + 3, second).trim();
|
|
16
|
+
const body = markdown.substring(second + 3);
|
|
17
|
+
const frontmatter = {};
|
|
18
|
+
for (const line of fmBlock.split("\n")) {
|
|
19
|
+
const colonIdx = line.indexOf(":");
|
|
20
|
+
if (colonIdx === -1)
|
|
21
|
+
continue;
|
|
22
|
+
const key = line.substring(0, colonIdx).trim();
|
|
23
|
+
const value = line.substring(colonIdx + 1).trim();
|
|
24
|
+
if (key)
|
|
25
|
+
frontmatter[key] = value;
|
|
26
|
+
}
|
|
27
|
+
return { frontmatter, body };
|
|
28
|
+
}
|
|
29
|
+
// --- Tool mapping ---
|
|
30
|
+
const GEMINI_TOOL_MAP = {
|
|
31
|
+
Read: "read_file",
|
|
32
|
+
Grep: "grep_search",
|
|
33
|
+
Glob: "glob",
|
|
34
|
+
Bash: "run_shell_command",
|
|
35
|
+
};
|
|
36
|
+
const OPENCODE_PERMISSION_MAP = {
|
|
37
|
+
Read: { key: "read", level: "allow" },
|
|
38
|
+
Grep: { key: "grep", level: "allow" },
|
|
39
|
+
Glob: { key: "glob", level: "allow" },
|
|
40
|
+
Bash: { key: "bash", level: "ask" },
|
|
41
|
+
};
|
|
42
|
+
function parseToolsList(toolsStr) {
|
|
43
|
+
// Handle both "Read, Grep, Glob" and "[Read, Grep, Glob]" formats
|
|
44
|
+
return toolsStr
|
|
45
|
+
.replace(/^\[|\]$/g, "")
|
|
46
|
+
.split(",")
|
|
47
|
+
.map((t) => t.trim())
|
|
48
|
+
.filter(Boolean);
|
|
49
|
+
}
|
|
50
|
+
// --- Harness-specific frontmatter builders ---
|
|
51
|
+
function buildGeminiFrontmatter(fm) {
|
|
52
|
+
const lines = [];
|
|
53
|
+
lines.push(`name: ${fm["name"] ?? "unknown"}`);
|
|
54
|
+
const desc = fm["description"] ?? "";
|
|
55
|
+
// Quote description if it contains YAML-special chars
|
|
56
|
+
if (/[:#{}[\],&*?|>!%@`"']/.test(desc) || desc === "") {
|
|
57
|
+
lines.push(`description: "${desc.replace(/"/g, '\\"')}"`);
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
lines.push(`description: ${desc}`);
|
|
61
|
+
}
|
|
62
|
+
lines.push("kind: local");
|
|
63
|
+
// Map tools
|
|
64
|
+
const tools = parseToolsList(fm["tools"] ?? "");
|
|
65
|
+
const geminiTools = tools
|
|
66
|
+
.map((t) => GEMINI_TOOL_MAP[t])
|
|
67
|
+
.filter(Boolean);
|
|
68
|
+
if (geminiTools.length > 0) {
|
|
69
|
+
lines.push("tools:");
|
|
70
|
+
for (const t of geminiTools) {
|
|
71
|
+
lines.push(` - ${t}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
lines.push("model: gemini-3-preview");
|
|
75
|
+
lines.push("temperature: 0.2");
|
|
76
|
+
lines.push("max_turns: 30");
|
|
77
|
+
lines.push("timeout_mins: 10");
|
|
78
|
+
return lines.join("\n");
|
|
79
|
+
}
|
|
80
|
+
function buildOpenCodeFrontmatter(fm) {
|
|
81
|
+
const lines = [];
|
|
82
|
+
lines.push(`name: ${fm["name"] ?? "unknown"}`);
|
|
83
|
+
const desc = fm["description"] ?? "";
|
|
84
|
+
if (/[:#{}[\],&*?|>!%@`"']/.test(desc) || desc === "") {
|
|
85
|
+
lines.push(`description: "${desc.replace(/"/g, '\\"')}"`);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
lines.push(`description: ${desc}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push("mode: subagent");
|
|
91
|
+
// Map tools to permissions
|
|
92
|
+
const tools = parseToolsList(fm["tools"] ?? "");
|
|
93
|
+
const permissions = {};
|
|
94
|
+
for (const t of tools) {
|
|
95
|
+
const mapping = OPENCODE_PERMISSION_MAP[t];
|
|
96
|
+
if (mapping)
|
|
97
|
+
permissions[mapping.key] = mapping.level;
|
|
98
|
+
}
|
|
99
|
+
if (Object.keys(permissions).length > 0) {
|
|
100
|
+
permissions["list"] = "allow";
|
|
101
|
+
lines.push("permission:");
|
|
102
|
+
for (const [key, level] of Object.entries(permissions)) {
|
|
103
|
+
lines.push(` ${key}: ${level}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push("model: openai/gpt-5");
|
|
107
|
+
return lines.join("\n");
|
|
108
|
+
}
|
|
109
|
+
// --- Public API ---
|
|
110
|
+
/**
|
|
111
|
+
* Transform a Claude Code agent markdown file to a target harness format.
|
|
112
|
+
* Returns the original content for claude-code; rewrites frontmatter for others.
|
|
113
|
+
*/
|
|
114
|
+
export function transformAgent(markdown, harnessName) {
|
|
115
|
+
if (harnessName === "claude-code")
|
|
116
|
+
return markdown;
|
|
117
|
+
const { frontmatter, body } = parseAgentMarkdown(markdown);
|
|
118
|
+
let newFrontmatter;
|
|
119
|
+
if (harnessName === "gemini-cli") {
|
|
120
|
+
newFrontmatter = buildGeminiFrontmatter(frontmatter);
|
|
121
|
+
}
|
|
122
|
+
else if (harnessName === "opencode") {
|
|
123
|
+
newFrontmatter = buildOpenCodeFrontmatter(frontmatter);
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
return markdown; // Unknown harness — pass through unchanged
|
|
127
|
+
}
|
|
128
|
+
return `---\n${newFrontmatter}\n---${body}`;
|
|
129
|
+
}
|