expandai 0.0.2 → 0.0.3
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 +42 -5
- package/build/dts/ApiClient.d.ts +128 -194
- package/build/dts/ApiClient.d.ts.map +1 -1
- package/build/dts/Auth/Domain.d.ts +95 -0
- package/build/dts/Auth/Domain.d.ts.map +1 -0
- package/build/dts/Auth/ManualAuthorization.d.ts +107 -0
- package/build/dts/Auth/ManualAuthorization.d.ts.map +1 -0
- package/build/dts/Auth/PendingInit.d.ts +35 -0
- package/build/dts/Auth/PendingInit.d.ts.map +1 -0
- package/build/dts/Auth/Pkce.d.ts +10 -0
- package/build/dts/Auth/Pkce.d.ts.map +1 -0
- package/build/dts/Auth/SetupGrantAuthorization.d.ts +34 -0
- package/build/dts/Auth/SetupGrantAuthorization.d.ts.map +1 -0
- package/build/dts/Authentication.d.ts +10 -9
- package/build/dts/Authentication.d.ts.map +1 -1
- package/build/dts/BrowserOAuth.d.ts +7 -7
- package/build/dts/BrowserOAuth.d.ts.map +1 -1
- package/build/dts/CommandPath.d.ts +2 -0
- package/build/dts/CommandPath.d.ts.map +1 -0
- package/build/dts/Commands/Fetch.d.ts.map +1 -1
- package/build/dts/Commands/Hook.d.ts +5 -5
- package/build/dts/Commands/Init.d.ts +55 -0
- package/build/dts/Commands/Init.d.ts.map +1 -0
- package/build/dts/Commands/Mcp.d.ts +6 -6
- package/build/dts/Commands/Search.d.ts.map +1 -1
- package/build/dts/Commands/Skill.d.ts +4 -4
- package/build/dts/Commands/Status.d.ts +27 -0
- package/build/dts/Commands/Status.d.ts.map +1 -0
- package/build/dts/Commands/Telemetry.d.ts +7 -0
- package/build/dts/Commands/Telemetry.d.ts.map +1 -0
- package/build/dts/Commands/Uninstall.d.ts +4 -4
- package/build/dts/Config.d.ts +108 -17
- package/build/dts/Config.d.ts.map +1 -1
- package/build/dts/Hook/SnapshotNote.d.ts +8 -4
- package/build/dts/Hook/SnapshotNote.d.ts.map +1 -1
- package/build/dts/Init/ClaudeAdapter.d.ts +61 -0
- package/build/dts/Init/ClaudeAdapter.d.ts.map +1 -0
- package/build/dts/Init/ClaudeProfile.d.ts +78 -0
- package/build/dts/Init/ClaudeProfile.d.ts.map +1 -0
- package/build/dts/Init/Domain.d.ts +139 -0
- package/build/dts/Init/Domain.d.ts.map +1 -0
- package/build/dts/Init/HarnessAdapter.d.ts +33 -0
- package/build/dts/Init/HarnessAdapter.d.ts.map +1 -0
- package/build/dts/Init/HarnessDetector.d.ts +19 -0
- package/build/dts/Init/HarnessDetector.d.ts.map +1 -0
- package/build/dts/Init/HarnessRegistry.d.ts +50 -0
- package/build/dts/Init/HarnessRegistry.d.ts.map +1 -0
- package/build/dts/Init/MutationTransaction.d.ts +96 -0
- package/build/dts/Init/MutationTransaction.d.ts.map +1 -0
- package/build/dts/Init/Service.d.ts +72 -0
- package/build/dts/Init/Service.d.ts.map +1 -0
- package/build/dts/Init/StandardAdapters.d.ts +42 -0
- package/build/dts/Init/StandardAdapters.d.ts.map +1 -0
- package/build/dts/Init/Status.d.ts +149 -0
- package/build/dts/Init/Status.d.ts.map +1 -0
- package/build/dts/Install/Claude/Hook.d.ts +85 -25
- package/build/dts/Install/Claude/Hook.d.ts.map +1 -1
- package/build/dts/Install/Claude/Mcp.d.ts +21 -18
- package/build/dts/Install/Claude/Mcp.d.ts.map +1 -1
- package/build/dts/Install/Claude/Skill.d.ts +14 -15
- package/build/dts/Install/Claude/Skill.d.ts.map +1 -1
- package/build/dts/Install/Codex/Mcp.d.ts +14 -8
- package/build/dts/Install/Codex/Mcp.d.ts.map +1 -1
- package/build/dts/Install/Codex/Skill.d.ts +4 -2
- package/build/dts/Install/Codex/Skill.d.ts.map +1 -1
- package/build/dts/Install/Cursor/Mcp.d.ts +13 -13
- package/build/dts/Install/Cursor/Mcp.d.ts.map +1 -1
- package/build/dts/Install/Cursor/Skill.d.ts +4 -2
- package/build/dts/Install/Cursor/Skill.d.ts.map +1 -1
- package/build/dts/Install/Domain.d.ts +35 -5
- package/build/dts/Install/Domain.d.ts.map +1 -1
- package/build/dts/Install/Hook.d.ts +4 -4
- package/build/dts/Install/Hook.d.ts.map +1 -1
- package/build/dts/Install/Mcp.d.ts +5 -5
- package/build/dts/Install/Mcp.d.ts.map +1 -1
- package/build/dts/Install/Opencode/Hook.d.ts +4 -3
- package/build/dts/Install/Opencode/Hook.d.ts.map +1 -1
- package/build/dts/Install/Opencode/Mcp.d.ts +11 -11
- package/build/dts/Install/Opencode/Mcp.d.ts.map +1 -1
- package/build/dts/Install/Opencode/Skill.d.ts +4 -2
- package/build/dts/Install/Opencode/Skill.d.ts.map +1 -1
- package/build/dts/Install/Skill.d.ts +3 -3
- package/build/dts/Install/Uninstall.d.ts +6 -6
- package/build/dts/Install/Uninstall.d.ts.map +1 -1
- package/build/dts/MarkdownAnsi.d.ts +29 -0
- package/build/dts/MarkdownAnsi.d.ts.map +1 -0
- package/build/dts/Mcp/ExpandDocs.d.ts +17 -5
- package/build/dts/Mcp/ExpandDocs.d.ts.map +1 -1
- package/build/dts/Mcp/Server.d.ts +1 -1
- package/build/dts/Mcp/Server.d.ts.map +1 -1
- package/build/dts/Mcp/tools/Citations.d.ts +100 -71
- package/build/dts/Mcp/tools/Citations.d.ts.map +1 -1
- package/build/dts/Mcp/tools/Fetch.d.ts +16 -15
- package/build/dts/Mcp/tools/Fetch.d.ts.map +1 -1
- package/build/dts/SecureFile.d.ts +23 -0
- package/build/dts/SecureFile.d.ts.map +1 -0
- package/build/dts/Snippets.d.ts +11 -12
- package/build/dts/Snippets.d.ts.map +1 -1
- package/build/dts/Telemetry.d.ts +30 -0
- package/build/dts/Telemetry.d.ts.map +1 -0
- package/build/dts/Version.d.ts +2 -0
- package/build/dts/Version.d.ts.map +1 -0
- package/build/dts/Xdg.d.ts +11 -0
- package/build/dts/Xdg.d.ts.map +1 -0
- package/build/esm/ApiClient.js +133 -10
- package/build/esm/ApiClient.js.map +1 -1
- package/build/esm/Auth/Domain.js +56 -0
- package/build/esm/Auth/Domain.js.map +1 -0
- package/build/esm/Auth/ManualAuthorization.js +253 -0
- package/build/esm/Auth/ManualAuthorization.js.map +1 -0
- package/build/esm/Auth/PendingInit.js +267 -0
- package/build/esm/Auth/PendingInit.js.map +1 -0
- package/build/esm/Auth/Pkce.js +23 -0
- package/build/esm/Auth/Pkce.js.map +1 -0
- package/build/esm/Auth/SetupGrantAuthorization.js +91 -0
- package/build/esm/Auth/SetupGrantAuthorization.js.map +1 -0
- package/build/esm/Authentication.js +38 -7
- package/build/esm/Authentication.js.map +1 -1
- package/build/esm/BrowserOAuth.js +7 -10
- package/build/esm/BrowserOAuth.js.map +1 -1
- package/build/esm/CommandPath.js +31 -0
- package/build/esm/CommandPath.js.map +1 -0
- package/build/esm/Commands/Fetch.js +30 -6
- package/build/esm/Commands/Fetch.js.map +1 -1
- package/build/esm/Commands/Init.js +371 -0
- package/build/esm/Commands/Init.js.map +1 -0
- package/build/esm/Commands/Search.js +6 -3
- package/build/esm/Commands/Search.js.map +1 -1
- package/build/esm/Commands/Status.js +186 -0
- package/build/esm/Commands/Status.js.map +1 -0
- package/build/esm/Commands/Telemetry.js +23 -0
- package/build/esm/Commands/Telemetry.js.map +1 -0
- package/build/esm/Config.js +502 -28
- package/build/esm/Config.js.map +1 -1
- package/build/esm/Hook/Claude.js +1 -1
- package/build/esm/Hook/Claude.js.map +1 -1
- package/build/esm/Hook/SnapshotNote.js +54 -7
- package/build/esm/Hook/SnapshotNote.js.map +1 -1
- package/build/esm/Init/ClaudeAdapter.js +137 -0
- package/build/esm/Init/ClaudeAdapter.js.map +1 -0
- package/build/esm/Init/ClaudeProfile.js +380 -0
- package/build/esm/Init/ClaudeProfile.js.map +1 -0
- package/build/esm/Init/Domain.js +118 -0
- package/build/esm/Init/Domain.js.map +1 -0
- package/build/esm/Init/HarnessAdapter.js +21 -0
- package/build/esm/Init/HarnessAdapter.js.map +1 -0
- package/build/esm/Init/HarnessDetector.js +65 -0
- package/build/esm/Init/HarnessDetector.js.map +1 -0
- package/build/esm/Init/HarnessRegistry.js +58 -0
- package/build/esm/Init/HarnessRegistry.js.map +1 -0
- package/build/esm/Init/MutationTransaction.js +494 -0
- package/build/esm/Init/MutationTransaction.js.map +1 -0
- package/build/esm/Init/Service.js +319 -0
- package/build/esm/Init/Service.js.map +1 -0
- package/build/esm/Init/StandardAdapters.js +116 -0
- package/build/esm/Init/StandardAdapters.js.map +1 -0
- package/build/esm/Init/Status.js +215 -0
- package/build/esm/Init/Status.js.map +1 -0
- package/build/esm/Install/Claude/Hook.js +182 -50
- package/build/esm/Install/Claude/Hook.js.map +1 -1
- package/build/esm/Install/Claude/Mcp.js +56 -26
- package/build/esm/Install/Claude/Mcp.js.map +1 -1
- package/build/esm/Install/Claude/Skill.js +49 -27
- package/build/esm/Install/Claude/Skill.js.map +1 -1
- package/build/esm/Install/Codex/Mcp.js +9 -4
- package/build/esm/Install/Codex/Mcp.js.map +1 -1
- package/build/esm/Install/Codex/Skill.js +4 -1
- package/build/esm/Install/Codex/Skill.js.map +1 -1
- package/build/esm/Install/Cursor/Mcp.js +7 -4
- package/build/esm/Install/Cursor/Mcp.js.map +1 -1
- package/build/esm/Install/Cursor/Skill.js +4 -1
- package/build/esm/Install/Cursor/Skill.js.map +1 -1
- package/build/esm/Install/Domain.js +60 -3
- package/build/esm/Install/Domain.js.map +1 -1
- package/build/esm/Install/Hook.js.map +1 -1
- package/build/esm/Install/Mcp.js +1 -1
- package/build/esm/Install/Mcp.js.map +1 -1
- package/build/esm/Install/Opencode/Hook.js +1 -1
- package/build/esm/Install/Opencode/Hook.js.map +1 -1
- package/build/esm/Install/Opencode/Mcp.js +6 -3
- package/build/esm/Install/Opencode/Mcp.js.map +1 -1
- package/build/esm/Install/Opencode/Skill.js +4 -1
- package/build/esm/Install/Opencode/Skill.js.map +1 -1
- package/build/esm/Install/Opencode/expandai-webfetch.js +27 -9
- package/build/esm/Install/Skill.js.map +1 -1
- package/build/esm/Install/Uninstall.js +2 -2
- package/build/esm/Install/Uninstall.js.map +1 -1
- package/build/esm/MarkdownAnsi.js +142 -0
- package/build/esm/MarkdownAnsi.js.map +1 -0
- package/build/esm/Mcp/ExpandDocs.js +44 -52
- package/build/esm/Mcp/ExpandDocs.js.map +1 -1
- package/build/esm/Mcp/RequestHeaders.js.map +1 -1
- package/build/esm/Mcp/Server.js +2 -1
- package/build/esm/Mcp/Server.js.map +1 -1
- package/build/esm/Mcp/tools/Citations.js +109 -8
- package/build/esm/Mcp/tools/Citations.js.map +1 -1
- package/build/esm/Mcp/tools/Fetch.js +36 -12
- package/build/esm/Mcp/tools/Fetch.js.map +1 -1
- package/build/esm/SecureFile.js +36 -0
- package/build/esm/SecureFile.js.map +1 -0
- package/build/esm/Snippets.js +12 -1
- package/build/esm/Snippets.js.map +1 -1
- package/build/esm/Telemetry.js +157 -0
- package/build/esm/Telemetry.js.map +1 -0
- package/build/esm/Version.js +4 -0
- package/build/esm/Version.js.map +1 -0
- package/build/esm/Xdg.js +15 -0
- package/build/esm/Xdg.js.map +1 -0
- package/build/esm/main.js +122 -14
- package/build/esm/main.js.map +1 -1
- package/build/skills/expandai/SKILL.md +26 -3
- package/package.json +4 -4
- package/src/ApiClient.ts +206 -13
- package/src/Auth/Domain.ts +63 -0
- package/src/Auth/ManualAuthorization.ts +296 -0
- package/src/Auth/PendingInit.ts +338 -0
- package/src/Auth/Pkce.ts +26 -0
- package/src/Auth/SetupGrantAuthorization.ts +116 -0
- package/src/Authentication.ts +49 -11
- package/src/BrowserOAuth.ts +10 -12
- package/src/CommandPath.ts +30 -0
- package/src/Commands/Fetch.ts +42 -11
- package/src/Commands/Init.ts +489 -0
- package/src/Commands/Search.ts +8 -3
- package/src/Commands/Status.ts +228 -0
- package/src/Commands/Telemetry.ts +42 -0
- package/src/Config.ts +695 -50
- package/src/Hook/Claude.ts +1 -1
- package/src/Hook/SnapshotNote.ts +55 -7
- package/src/Init/ClaudeAdapter.ts +191 -0
- package/src/Init/ClaudeProfile.ts +496 -0
- package/src/Init/Domain.ts +153 -0
- package/src/Init/HarnessAdapter.ts +35 -0
- package/src/Init/HarnessDetector.ts +77 -0
- package/src/Init/HarnessRegistry.ts +87 -0
- package/src/Init/MutationTransaction.ts +747 -0
- package/src/Init/Service.ts +385 -0
- package/src/Init/StandardAdapters.ts +141 -0
- package/src/Init/Status.ts +253 -0
- package/src/Install/Claude/Hook.ts +234 -81
- package/src/Install/Claude/Mcp.ts +80 -33
- package/src/Install/Claude/Skill.ts +70 -37
- package/src/Install/Codex/Mcp.ts +14 -9
- package/src/Install/Codex/Skill.ts +9 -5
- package/src/Install/Cursor/Mcp.ts +12 -9
- package/src/Install/Cursor/Skill.ts +9 -5
- package/src/Install/Domain.ts +77 -4
- package/src/Install/Hook.ts +3 -3
- package/src/Install/Mcp.ts +6 -6
- package/src/Install/Opencode/Hook.ts +5 -5
- package/src/Install/Opencode/Mcp.ts +11 -8
- package/src/Install/Opencode/Skill.ts +9 -5
- package/src/Install/Opencode/expandai-webfetch.js +27 -9
- package/src/Install/Skill.ts +3 -3
- package/src/Install/Uninstall.ts +3 -3
- package/src/MarkdownAnsi.ts +162 -0
- package/src/Mcp/ExpandDocs.ts +46 -55
- package/src/Mcp/Server.ts +2 -1
- package/src/Mcp/tools/Citations.ts +117 -8
- package/src/Mcp/tools/Fetch.ts +40 -14
- package/src/SecureFile.ts +70 -0
- package/src/Snippets.ts +19 -12
- package/src/Telemetry.ts +186 -0
- package/src/Version.ts +3 -0
- package/src/Xdg.ts +18 -0
- package/src/main.ts +171 -12
- package/build/dts/DeviceAuth.d.ts +0 -87
- package/build/dts/DeviceAuth.d.ts.map +0 -1
- package/build/esm/DeviceAuth.js +0 -127
- package/build/esm/DeviceAuth.js.map +0 -1
- package/src/DeviceAuth.ts +0 -193
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import { Config, Console, Effect, Match, Record, Schema } from 'effect'
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
3
|
+
import {
|
|
4
|
+
type CliCommand,
|
|
5
|
+
ConfigIo,
|
|
6
|
+
DIRECT_CLI,
|
|
7
|
+
McpParseError,
|
|
8
|
+
McpServersConfigSchema,
|
|
9
|
+
McpWriteError,
|
|
10
|
+
SERVER_KEY,
|
|
11
|
+
} from '../Domain.js'
|
|
12
|
+
import type { InstallScope } from '../Mcp.js'
|
|
13
|
+
|
|
14
|
+
/** Claude Code MCP descriptor stored under the Expand-owned server key. */
|
|
13
15
|
export class Entry extends Schema.Class<Entry>('ClaudeMcpEntry')({
|
|
14
16
|
type: Schema.Literal('stdio'),
|
|
15
17
|
command: Schema.NonEmptyTrimmedString,
|
|
@@ -17,6 +19,64 @@ export class Entry extends Schema.Class<Entry>('ClaudeMcpEntry')({
|
|
|
17
19
|
env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
|
|
18
20
|
}) {}
|
|
19
21
|
|
|
22
|
+
export interface ClaudeMcpPlan {
|
|
23
|
+
readonly changed: boolean
|
|
24
|
+
readonly config: typeof McpServersConfigSchema.Type
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MCP_ARGS = ['mcp', 'run'] as const
|
|
28
|
+
|
|
29
|
+
/** Claude execs the descriptor directly, so `command`/`args` stay separate. */
|
|
30
|
+
export function claudeMcpEntry(cli: CliCommand = DIRECT_CLI): Entry {
|
|
31
|
+
return new Entry({
|
|
32
|
+
type: 'stdio',
|
|
33
|
+
command: cli.command,
|
|
34
|
+
args: [...cli.args, ...MCP_ARGS],
|
|
35
|
+
env: { EXPANDAI_HARNESS: 'claude-code' },
|
|
36
|
+
})
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const CLAUDE_MCP_ENTRY = claudeMcpEntry()
|
|
40
|
+
|
|
41
|
+
function isUnknownRecord(value: unknown): value is Record<string, unknown> {
|
|
42
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isCurrentEntry(value: unknown, expected: Entry): boolean {
|
|
46
|
+
if (!isUnknownRecord(value)) return false
|
|
47
|
+
const args = value.args
|
|
48
|
+
const env = value.env
|
|
49
|
+
return (
|
|
50
|
+
value.type === expected.type &&
|
|
51
|
+
value.command === expected.command &&
|
|
52
|
+
Array.isArray(args) &&
|
|
53
|
+
args.length === expected.args.length &&
|
|
54
|
+
args.every((arg, index) => arg === expected.args[index]) &&
|
|
55
|
+
isUnknownRecord(env) &&
|
|
56
|
+
env.EXPANDAI_HARNESS === 'claude-code'
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function planClaudeMcp(config: typeof McpServersConfigSchema.Type, cli: CliCommand = DIRECT_CLI): ClaudeMcpPlan {
|
|
61
|
+
const entry = claudeMcpEntry(cli)
|
|
62
|
+
if (isCurrentEntry(config.mcpServers[SERVER_KEY], entry)) return { changed: false, config }
|
|
63
|
+
return {
|
|
64
|
+
changed: true,
|
|
65
|
+
config: {
|
|
66
|
+
...config,
|
|
67
|
+
mcpServers: { ...config.mcpServers, [SERVER_KEY]: entry },
|
|
68
|
+
},
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function planClaudeMcpRemoval(config: typeof McpServersConfigSchema.Type): ClaudeMcpPlan {
|
|
73
|
+
if (!(SERVER_KEY in config.mcpServers)) return { changed: false, config }
|
|
74
|
+
return {
|
|
75
|
+
changed: true,
|
|
76
|
+
config: { ...config, mcpServers: Record.remove(config.mcpServers, SERVER_KEY) },
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
20
80
|
export class ClaudeMcp extends Effect.Service<ClaudeMcp>()('@expandai/cli/Install/Claude/Mcp', {
|
|
21
81
|
dependencies: [ConfigIo.Default],
|
|
22
82
|
effect: Effect.gen(function* () {
|
|
@@ -24,7 +84,7 @@ export class ClaudeMcp extends Effect.Service<ClaudeMcp>()('@expandai/cli/Instal
|
|
|
24
84
|
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
25
85
|
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
26
86
|
|
|
27
|
-
const pathFor = Effect.fn(function* (scope:
|
|
87
|
+
const pathFor = Effect.fn(function* (scope: InstallScope) {
|
|
28
88
|
return yield* Match.value(scope).pipe(
|
|
29
89
|
Match.when('global', () => Effect.succeed(`${home}/.claude.json`)),
|
|
30
90
|
Match.when('project', () => Effect.succeed(`${cwd}/.mcp.json`)),
|
|
@@ -44,37 +104,24 @@ export class ClaudeMcp extends Effect.Service<ClaudeMcp>()('@expandai/cli/Instal
|
|
|
44
104
|
.pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
|
|
45
105
|
})
|
|
46
106
|
|
|
47
|
-
const install = Effect.fn(function* (scope:
|
|
107
|
+
const install = Effect.fn(function* (scope: InstallScope) {
|
|
48
108
|
const file = yield* pathFor(scope)
|
|
49
|
-
const
|
|
50
|
-
yield* writeConfig(file,
|
|
51
|
-
...config,
|
|
52
|
-
mcpServers: {
|
|
53
|
-
...config.mcpServers,
|
|
54
|
-
[SERVER_KEY]: new Entry({
|
|
55
|
-
type: 'stdio',
|
|
56
|
-
command: 'expandai',
|
|
57
|
-
args: ['mcp', 'run'],
|
|
58
|
-
}),
|
|
59
|
-
},
|
|
60
|
-
})
|
|
109
|
+
const plan = planClaudeMcp(yield* readConfig(file))
|
|
110
|
+
if (plan.changed) yield* writeConfig(file, plan.config)
|
|
61
111
|
yield* Console.log(`Installed expandai MCP server into ${file}`)
|
|
62
112
|
})
|
|
63
113
|
|
|
64
|
-
const uninstall = Effect.fn(function* (scope:
|
|
114
|
+
const uninstall = Effect.fn(function* (scope: InstallScope) {
|
|
65
115
|
const file = yield* pathFor(scope)
|
|
66
|
-
const
|
|
67
|
-
if (!
|
|
68
|
-
|
|
69
|
-
}
|
|
70
|
-
yield* writeConfig(file, { ...config, mcpServers: Record.remove(config.mcpServers, SERVER_KEY) })
|
|
116
|
+
const plan = planClaudeMcpRemoval(yield* readConfig(file))
|
|
117
|
+
if (!plan.changed) return
|
|
118
|
+
yield* writeConfig(file, plan.config)
|
|
71
119
|
yield* Console.log(`Removed expandai MCP server from ${file}`)
|
|
72
120
|
})
|
|
73
121
|
|
|
74
|
-
const isInstalled = Effect.fn(function* (scope:
|
|
122
|
+
const isInstalled = Effect.fn(function* (scope: InstallScope) {
|
|
75
123
|
const file = yield* pathFor(scope)
|
|
76
|
-
|
|
77
|
-
return SERVER_KEY in config.mcpServers
|
|
124
|
+
return SERVER_KEY in (yield* readConfig(file)).mcpServers
|
|
78
125
|
})
|
|
79
126
|
|
|
80
127
|
return { install, uninstall, isInstalled, pathFor } as const
|
|
@@ -1,44 +1,77 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
|
+
import { FileSystem, Path } from '@effect/platform'
|
|
3
|
+
import { NodeFileSystem, NodePath } from '@effect/platform-node'
|
|
2
4
|
import { Config, Effect, Match } from 'effect'
|
|
3
|
-
import {
|
|
4
|
-
import type {
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* Claude Code skills live under `<scope>/.claude/skills/<name>/SKILL.md`.
|
|
8
|
-
*
|
|
9
|
-
* - global → `~/.claude/skills/expandai/SKILL.md`
|
|
10
|
-
* - project → `<cwd>/.claude/skills/expandai/SKILL.md`
|
|
11
|
-
*
|
|
12
|
-
* (Globally-scoped Claude Code skills are also reachable via `npx skills`,
|
|
13
|
-
* but file-copy gives us a uniform install path with OpenCode and works
|
|
14
|
-
* without npm.)
|
|
15
|
-
*/
|
|
5
|
+
import { CLI_VERSION } from '../../Version.js'
|
|
6
|
+
import type { InstallScope } from '../Mcp.js'
|
|
16
7
|
|
|
17
8
|
export const SKILL_NAME = 'expandai'
|
|
9
|
+
export const SKILL_FILE_NAME = 'SKILL.md'
|
|
10
|
+
const PROFILE_VERSION_LINE = /^(\s*expandai-profile-version:\s*).+$/mu
|
|
11
|
+
|
|
12
|
+
export interface ClaudeSkillPlan {
|
|
13
|
+
readonly changed: boolean
|
|
14
|
+
readonly text: string
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function renderClaudeSkill(source: string, profileVersion: string = CLI_VERSION): string {
|
|
18
|
+
if (!PROFILE_VERSION_LINE.test(source)) {
|
|
19
|
+
throw new Error('Bundled Expand skill is missing expandai-profile-version metadata')
|
|
20
|
+
}
|
|
21
|
+
return source.replace(PROFILE_VERSION_LINE, `$1${profileVersion}`)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function planClaudeSkill(
|
|
25
|
+
current: string | undefined,
|
|
26
|
+
source: string,
|
|
27
|
+
profileVersion: string = CLI_VERSION,
|
|
28
|
+
): ClaudeSkillPlan {
|
|
29
|
+
const text = renderClaudeSkill(source, profileVersion)
|
|
30
|
+
return { changed: current !== text, text }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const makeClaudeSkill = Effect.gen(function* () {
|
|
34
|
+
const fileSystem = yield* FileSystem.FileSystem
|
|
35
|
+
const path = yield* Path.Path
|
|
36
|
+
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
37
|
+
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
38
|
+
const moduleFile = yield* path.fromFileUrl(new URL(import.meta.url))
|
|
39
|
+
const sourceFile = path.resolve(path.dirname(moduleFile), '..', '..', '..', 'skills', SKILL_NAME, SKILL_FILE_NAME)
|
|
40
|
+
|
|
41
|
+
const pathFor = Effect.fn(function* (scope: InstallScope) {
|
|
42
|
+
return yield* Match.value(scope).pipe(
|
|
43
|
+
Match.when('global', () => Effect.succeed(path.join(home, '.claude', 'skills', SKILL_NAME))),
|
|
44
|
+
Match.when('project', () => Effect.succeed(path.join(cwd, '.claude', 'skills', SKILL_NAME))),
|
|
45
|
+
Match.exhaustive,
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const source = Effect.fn('ClaudeSkill.source')(function* () {
|
|
50
|
+
return yield* fileSystem.readFileString(sourceFile)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const install = Effect.fn('ClaudeSkill.install')(function* (scope: InstallScope) {
|
|
54
|
+
const target = yield* pathFor(scope)
|
|
55
|
+
const targetFile = path.join(target, SKILL_FILE_NAME)
|
|
56
|
+
const current = (yield* fileSystem.exists(targetFile)) ? yield* fileSystem.readFileString(targetFile) : undefined
|
|
57
|
+
const plan = planClaudeSkill(current, yield* source())
|
|
58
|
+
if (plan.changed) {
|
|
59
|
+
yield* fileSystem.makeDirectory(target, { recursive: true })
|
|
60
|
+
yield* fileSystem.writeFileString(targetFile, plan.text)
|
|
61
|
+
}
|
|
62
|
+
return target
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const uninstall = Effect.fn('ClaudeSkill.uninstall')(function* (scope: InstallScope) {
|
|
66
|
+
const target = yield* pathFor(scope)
|
|
67
|
+
if (yield* fileSystem.exists(target)) yield* fileSystem.remove(target, { recursive: true })
|
|
68
|
+
return target
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
return { install, uninstall, pathFor, source } as const
|
|
72
|
+
})
|
|
18
73
|
|
|
19
74
|
export class ClaudeSkill extends Effect.Service<ClaudeSkill>()('@expandai/cli/Install/Claude/Skill', {
|
|
20
|
-
dependencies: [
|
|
21
|
-
effect:
|
|
22
|
-
const io = yield* SkillIo
|
|
23
|
-
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
24
|
-
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
25
|
-
|
|
26
|
-
const pathFor = Effect.fn(function* (scope: Scope) {
|
|
27
|
-
return yield* Match.value(scope).pipe(
|
|
28
|
-
Match.when('global', () => Effect.succeed(`${home}/.claude/skills/${SKILL_NAME}`)),
|
|
29
|
-
Match.when('project', () => Effect.succeed(`${cwd}/.claude/skills/${SKILL_NAME}`)),
|
|
30
|
-
Match.exhaustive,
|
|
31
|
-
)
|
|
32
|
-
})
|
|
33
|
-
|
|
34
|
-
const install = Effect.fn(function* (scope: Scope) {
|
|
35
|
-
return yield* io.install(yield* pathFor(scope))
|
|
36
|
-
})
|
|
37
|
-
|
|
38
|
-
const uninstall = Effect.fn(function* (scope: Scope) {
|
|
39
|
-
return yield* io.uninstall(yield* pathFor(scope))
|
|
40
|
-
})
|
|
41
|
-
|
|
42
|
-
return { install, uninstall } as const
|
|
43
|
-
}),
|
|
75
|
+
dependencies: [NodeFileSystem.layer, NodePath.layer],
|
|
76
|
+
effect: makeClaudeSkill,
|
|
44
77
|
}) {}
|
package/src/Install/Codex/Mcp.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import { Config, Console, Effect, Match, Record, Schema } from 'effect'
|
|
3
3
|
import { parse, stringify, type TomlTableWithoutBigInt } from 'smol-toml'
|
|
4
|
-
import { ConfigIo, McpParseError, McpWriteError, SERVER_KEY } from '../Domain.js'
|
|
5
|
-
import type {
|
|
4
|
+
import { CliInvocation, ConfigIo, McpParseError, McpWriteError, SERVER_KEY } from '../Domain.js'
|
|
5
|
+
import type { InstallScope } from '../Mcp.js'
|
|
6
6
|
|
|
7
7
|
export class Entry extends Schema.Class<Entry>('CodexMcpEntry')({
|
|
8
8
|
command: Schema.NonEmptyTrimmedString,
|
|
9
9
|
args: Schema.Array(Schema.String),
|
|
10
|
+
env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
|
|
10
11
|
enabled: Schema.Boolean,
|
|
11
12
|
}) {}
|
|
12
13
|
|
|
@@ -32,13 +33,14 @@ function toStringifyValue(value: TomlTableWithoutBigInt) {
|
|
|
32
33
|
* - project → `<cwd>/.codex/config.toml`
|
|
33
34
|
*/
|
|
34
35
|
export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/Codex/Mcp', {
|
|
35
|
-
dependencies: [ConfigIo.Default],
|
|
36
|
+
dependencies: [ConfigIo.Default, CliInvocation.Default],
|
|
36
37
|
effect: Effect.gen(function* () {
|
|
37
38
|
const io = yield* ConfigIo
|
|
39
|
+
const cli = yield* CliInvocation
|
|
38
40
|
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
39
41
|
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
40
42
|
|
|
41
|
-
const pathFor = Effect.fn(function* (scope:
|
|
43
|
+
const pathFor = Effect.fn(function* (scope: InstallScope) {
|
|
42
44
|
return yield* Match.value(scope).pipe(
|
|
43
45
|
Match.when('global', () => Effect.succeed(`${home}/.codex/config.toml`)),
|
|
44
46
|
Match.when('project', () => Effect.succeed(`${cwd}/.codex/config.toml`)),
|
|
@@ -62,20 +64,23 @@ export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/
|
|
|
62
64
|
yield* io.writeText(file, encoded).pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
|
|
63
65
|
})
|
|
64
66
|
|
|
65
|
-
const install = Effect.fn(function* (scope:
|
|
67
|
+
const install = Effect.fn(function* (scope: InstallScope) {
|
|
66
68
|
const file = yield* pathFor(scope)
|
|
67
69
|
const config = yield* readConfig(file)
|
|
68
70
|
const servers = yield* getMcpServers(config)
|
|
69
71
|
|
|
72
|
+
const invocation = yield* cli.resolve()
|
|
70
73
|
const entryModel = new Entry({
|
|
71
|
-
command:
|
|
72
|
-
args: ['mcp', 'run'],
|
|
74
|
+
command: invocation.command,
|
|
75
|
+
args: [...invocation.args, 'mcp', 'run'],
|
|
76
|
+
env: { EXPANDAI_HARNESS: 'codex' },
|
|
73
77
|
enabled: true,
|
|
74
78
|
})
|
|
75
79
|
|
|
76
80
|
const entry: TomlTableWithoutBigInt = {
|
|
77
81
|
command: entryModel.command,
|
|
78
82
|
args: [...entryModel.args],
|
|
83
|
+
env: { ...entryModel.env },
|
|
79
84
|
enabled: entryModel.enabled,
|
|
80
85
|
}
|
|
81
86
|
|
|
@@ -86,7 +91,7 @@ export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/
|
|
|
86
91
|
yield* Console.log(`Installed expandai MCP server into ${file}`)
|
|
87
92
|
})
|
|
88
93
|
|
|
89
|
-
const uninstall = Effect.fn(function* (scope:
|
|
94
|
+
const uninstall = Effect.fn(function* (scope: InstallScope) {
|
|
90
95
|
const file = yield* pathFor(scope)
|
|
91
96
|
const config = yield* readConfig(file)
|
|
92
97
|
const servers = yield* getMcpServers(config)
|
|
@@ -97,7 +102,7 @@ export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/
|
|
|
97
102
|
yield* Console.log(`Removed expandai MCP server from ${file}`)
|
|
98
103
|
})
|
|
99
104
|
|
|
100
|
-
const isInstalled = Effect.fn(function* (scope:
|
|
105
|
+
const isInstalled = Effect.fn(function* (scope: InstallScope) {
|
|
101
106
|
const file = yield* pathFor(scope)
|
|
102
107
|
const config = yield* readConfig(file)
|
|
103
108
|
const servers = yield* getMcpServers(config)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import { Config, Effect, Match } from 'effect'
|
|
3
3
|
import { SkillIo } from '../Domain.js'
|
|
4
|
-
import type {
|
|
4
|
+
import type { InstallScope } from '../Mcp.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Codex skills use the open agent skills locations:
|
|
@@ -19,7 +19,7 @@ export class CodexSkill extends Effect.Service<CodexSkill>()('@expandai/cli/Inst
|
|
|
19
19
|
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
20
20
|
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
21
21
|
|
|
22
|
-
const pathFor = Effect.fn(function* (scope:
|
|
22
|
+
const pathFor = Effect.fn(function* (scope: InstallScope) {
|
|
23
23
|
return yield* Match.value(scope).pipe(
|
|
24
24
|
Match.when('global', () => Effect.succeed(`${home}/.agents/skills/${SKILL_NAME}`)),
|
|
25
25
|
Match.when('project', () => Effect.succeed(`${cwd}/.agents/skills/${SKILL_NAME}`)),
|
|
@@ -27,14 +27,18 @@ export class CodexSkill extends Effect.Service<CodexSkill>()('@expandai/cli/Inst
|
|
|
27
27
|
)
|
|
28
28
|
})
|
|
29
29
|
|
|
30
|
-
const install = Effect.fn(function* (scope:
|
|
30
|
+
const install = Effect.fn(function* (scope: InstallScope) {
|
|
31
31
|
return yield* io.install(yield* pathFor(scope))
|
|
32
32
|
})
|
|
33
33
|
|
|
34
|
-
const uninstall = Effect.fn(function* (scope:
|
|
34
|
+
const uninstall = Effect.fn(function* (scope: InstallScope) {
|
|
35
35
|
return yield* io.uninstall(yield* pathFor(scope))
|
|
36
36
|
})
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
const isCurrent = Effect.fn(function* (scope: InstallScope) {
|
|
39
|
+
return yield* io.isCurrent(yield* pathFor(scope))
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
return { install, uninstall, isCurrent, pathFor } as const
|
|
39
43
|
}),
|
|
40
44
|
}) {}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import { Config, Console, Effect, Match, Record, Schema } from 'effect'
|
|
3
|
-
import { ConfigIo, McpParseError, McpServersConfigSchema, McpWriteError, SERVER_KEY } from '../Domain.js'
|
|
4
|
-
import type {
|
|
3
|
+
import { CliInvocation, ConfigIo, McpParseError, McpServersConfigSchema, McpWriteError, SERVER_KEY } from '../Domain.js'
|
|
4
|
+
import type { InstallScope } from '../Mcp.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Cursor MCP descriptor — same `mcpServers` shape as Claude Code, different
|
|
@@ -18,13 +18,14 @@ export class Entry extends Schema.Class<Entry>('CursorMcpEntry')({
|
|
|
18
18
|
}) {}
|
|
19
19
|
|
|
20
20
|
export class CursorMcp extends Effect.Service<CursorMcp>()('@expandai/cli/Install/Cursor/Mcp', {
|
|
21
|
-
dependencies: [ConfigIo.Default],
|
|
21
|
+
dependencies: [ConfigIo.Default, CliInvocation.Default],
|
|
22
22
|
effect: Effect.gen(function* () {
|
|
23
23
|
const io = yield* ConfigIo
|
|
24
|
+
const cli = yield* CliInvocation
|
|
24
25
|
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
25
26
|
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
26
27
|
|
|
27
|
-
const pathFor = Effect.fn(function* (scope:
|
|
28
|
+
const pathFor = Effect.fn(function* (scope: InstallScope) {
|
|
28
29
|
return yield* Match.value(scope).pipe(
|
|
29
30
|
Match.when('global', () => Effect.succeed(`${home}/.cursor/mcp.json`)),
|
|
30
31
|
Match.when('project', () => Effect.succeed(`${cwd}/.cursor/mcp.json`)),
|
|
@@ -44,24 +45,26 @@ export class CursorMcp extends Effect.Service<CursorMcp>()('@expandai/cli/Instal
|
|
|
44
45
|
.pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
|
|
45
46
|
})
|
|
46
47
|
|
|
47
|
-
const install = Effect.fn(function* (scope:
|
|
48
|
+
const install = Effect.fn(function* (scope: InstallScope) {
|
|
48
49
|
const file = yield* pathFor(scope)
|
|
49
50
|
const config = yield* readConfig(file)
|
|
51
|
+
const invocation = yield* cli.resolve()
|
|
50
52
|
yield* writeConfig(file, {
|
|
51
53
|
...config,
|
|
52
54
|
mcpServers: {
|
|
53
55
|
...config.mcpServers,
|
|
54
56
|
[SERVER_KEY]: new Entry({
|
|
55
57
|
type: 'stdio',
|
|
56
|
-
command:
|
|
57
|
-
args: ['mcp', 'run'],
|
|
58
|
+
command: invocation.command,
|
|
59
|
+
args: [...invocation.args, 'mcp', 'run'],
|
|
60
|
+
env: { EXPANDAI_HARNESS: 'cursor' },
|
|
58
61
|
}),
|
|
59
62
|
},
|
|
60
63
|
})
|
|
61
64
|
yield* Console.log(`Installed expandai MCP server into ${file}`)
|
|
62
65
|
})
|
|
63
66
|
|
|
64
|
-
const uninstall = Effect.fn(function* (scope:
|
|
67
|
+
const uninstall = Effect.fn(function* (scope: InstallScope) {
|
|
65
68
|
const file = yield* pathFor(scope)
|
|
66
69
|
const config = yield* readConfig(file)
|
|
67
70
|
if (!(SERVER_KEY in config.mcpServers)) {
|
|
@@ -71,7 +74,7 @@ export class CursorMcp extends Effect.Service<CursorMcp>()('@expandai/cli/Instal
|
|
|
71
74
|
yield* Console.log(`Removed expandai MCP server from ${file}`)
|
|
72
75
|
})
|
|
73
76
|
|
|
74
|
-
const isInstalled = Effect.fn(function* (scope:
|
|
77
|
+
const isInstalled = Effect.fn(function* (scope: InstallScope) {
|
|
75
78
|
const file = yield* pathFor(scope)
|
|
76
79
|
const config = yield* readConfig(file)
|
|
77
80
|
return SERVER_KEY in config.mcpServers
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
2
|
import { Config, Effect, Match } from 'effect'
|
|
3
3
|
import { SkillIo } from '../Domain.js'
|
|
4
|
-
import type {
|
|
4
|
+
import type { InstallScope } from '../Mcp.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Cursor skills are auto-loaded from these locations (per
|
|
@@ -26,7 +26,7 @@ export class CursorSkill extends Effect.Service<CursorSkill>()('@expandai/cli/In
|
|
|
26
26
|
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
27
27
|
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
28
28
|
|
|
29
|
-
const pathFor = Effect.fn(function* (scope:
|
|
29
|
+
const pathFor = Effect.fn(function* (scope: InstallScope) {
|
|
30
30
|
return yield* Match.value(scope).pipe(
|
|
31
31
|
Match.when('global', () => Effect.succeed(`${home}/.cursor/skills/${SKILL_NAME}`)),
|
|
32
32
|
Match.when('project', () => Effect.succeed(`${cwd}/.cursor/skills/${SKILL_NAME}`)),
|
|
@@ -34,14 +34,18 @@ export class CursorSkill extends Effect.Service<CursorSkill>()('@expandai/cli/In
|
|
|
34
34
|
)
|
|
35
35
|
})
|
|
36
36
|
|
|
37
|
-
const install = Effect.fn(function* (scope:
|
|
37
|
+
const install = Effect.fn(function* (scope: InstallScope) {
|
|
38
38
|
return yield* io.install(yield* pathFor(scope))
|
|
39
39
|
})
|
|
40
40
|
|
|
41
|
-
const uninstall = Effect.fn(function* (scope:
|
|
41
|
+
const uninstall = Effect.fn(function* (scope: InstallScope) {
|
|
42
42
|
return yield* io.uninstall(yield* pathFor(scope))
|
|
43
43
|
})
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
const isCurrent = Effect.fn(function* (scope: InstallScope) {
|
|
46
|
+
return yield* io.isCurrent(yield* pathFor(scope))
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
return { install, uninstall, isCurrent, pathFor } as const
|
|
46
50
|
}),
|
|
47
51
|
}) {}
|
package/src/Install/Domain.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { FileSystem, Path } from '@effect/platform'
|
|
2
2
|
import { NodeFileSystem, NodePath } from '@effect/platform-node'
|
|
3
|
-
import { Effect, Inspectable, Schema } from 'effect'
|
|
3
|
+
import { Array, Config, Effect, Inspectable, Layer, Schema, String } from 'effect'
|
|
4
4
|
|
|
5
5
|
export class McpParseError extends Schema.TaggedError<McpParseError>()('McpParseError', {
|
|
6
6
|
path: Schema.String,
|
|
@@ -23,8 +23,16 @@ export class McpWriteError extends Schema.TaggedError<McpWriteError>()('McpWrite
|
|
|
23
23
|
export const Host = Schema.Literal('claude-code', 'cursor', 'opencode', 'codex')
|
|
24
24
|
export type Host = typeof Host.Type
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
/**
|
|
27
|
+
* The agent harness invoking the CLI — every install target plus 'unknown'
|
|
28
|
+
* for uninstrumented callers. Installers bake `EXPANDAI_HARNESS=<host>` into
|
|
29
|
+
* MCP/hook configs; telemetry reads it back.
|
|
30
|
+
*/
|
|
31
|
+
export const Harness = Schema.Literal(...Host.literals, 'unknown')
|
|
32
|
+
export type Harness = typeof Harness.Type
|
|
33
|
+
|
|
34
|
+
export const InstallScope = Schema.Literal('project', 'global')
|
|
35
|
+
export type InstallScope = typeof InstallScope.Type
|
|
28
36
|
|
|
29
37
|
const McpServerMap = Schema.Record({ key: Schema.String, value: Schema.Unknown })
|
|
30
38
|
|
|
@@ -40,6 +48,56 @@ export const OpencodeConfigSchema = Schema.parseJson(
|
|
|
40
48
|
|
|
41
49
|
export const SERVER_KEY = 'expandai'
|
|
42
50
|
|
|
51
|
+
/** How a host config should launch the CLI: `command` plus leading args. */
|
|
52
|
+
export interface CliCommand {
|
|
53
|
+
readonly command: string
|
|
54
|
+
readonly args: ReadonlyArray<string>
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export const DIRECT_CLI: CliCommand = { command: 'expandai', args: [] }
|
|
58
|
+
const NPX_CLI: CliCommand = { command: 'npx', args: ['-y', 'expandai'] }
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Renders a `CliCommand` as the single shell string Claude Code stores for hook
|
|
62
|
+
* entries. MCP descriptors keep `command`/`args` separate because they exec
|
|
63
|
+
* directly; hooks are run through a shell, so they need one flat line.
|
|
64
|
+
*/
|
|
65
|
+
export function cliShellCommand(cli: CliCommand, args: ReadonlyArray<string>): string {
|
|
66
|
+
return [cli.command, ...cli.args, ...args].join(' ')
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Resolves how installed host configs should invoke the expandai CLI. MCP
|
|
71
|
+
* entries exec their `command` directly (no shell), so the fallback decision
|
|
72
|
+
* happens at install time: prefer the `expandai` binary when it is on PATH,
|
|
73
|
+
* otherwise write an `npx -y expandai` invocation so installs performed via
|
|
74
|
+
* `npx expandai …` keep working without a global CLI install.
|
|
75
|
+
*/
|
|
76
|
+
const makeCliInvocation = Effect.gen(function* () {
|
|
77
|
+
const fs = yield* FileSystem.FileSystem
|
|
78
|
+
const path = yield* Path.Path
|
|
79
|
+
const searchPath = yield* Config.string('PATH').pipe(Config.withDefault(''))
|
|
80
|
+
|
|
81
|
+
const resolve = Effect.fn('CliInvocation.resolve')(function* () {
|
|
82
|
+
// ':' is the POSIX PATH delimiter — the install flow already assumes
|
|
83
|
+
// POSIX conventions (HOME-based config paths, shell hooks).
|
|
84
|
+
const onPath = yield* Effect.exists(Array.filter(searchPath.split(':'), String.isNonEmpty), (dir) =>
|
|
85
|
+
fs.exists(path.join(dir, 'expandai')).pipe(Effect.orElseSucceed(() => false)),
|
|
86
|
+
)
|
|
87
|
+
return onPath ? DIRECT_CLI : NPX_CLI
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
return { resolve } as const
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
export class CliInvocation extends Effect.Service<CliInvocation>()('@expandai/cli/Install/CliInvocation', {
|
|
94
|
+
dependencies: [NodeFileSystem.layer, NodePath.layer],
|
|
95
|
+
effect: makeCliInvocation,
|
|
96
|
+
}) {
|
|
97
|
+
/** Dependency-open layer so fixtures can supply their own file system and PATH. */
|
|
98
|
+
static readonly layer = Layer.effect(CliInvocation, makeCliInvocation.pipe(Effect.map(CliInvocation.make)))
|
|
99
|
+
}
|
|
100
|
+
|
|
43
101
|
export const readConfigText = Effect.fn(function* (file: string) {
|
|
44
102
|
const fs = yield* FileSystem.FileSystem
|
|
45
103
|
const exists = yield* fs.exists(file)
|
|
@@ -143,6 +201,21 @@ export class SkillIo extends Effect.Service<SkillIo>()('@expandai/cli/Install/Sk
|
|
|
143
201
|
return target
|
|
144
202
|
})
|
|
145
203
|
|
|
146
|
-
|
|
204
|
+
/**
|
|
205
|
+
* Whether the installed skill matches the bundled one. Presence alone is not
|
|
206
|
+
* enough: an upgraded CLI ships new skill text, and a harness whose install is
|
|
207
|
+
* gated on "the file exists" would keep serving the old instructions forever.
|
|
208
|
+
*/
|
|
209
|
+
const isCurrent = Effect.fn('SkillIo.isCurrent')(function* (target: string) {
|
|
210
|
+
const targetFile = path.join(target, 'SKILL.md')
|
|
211
|
+
if (!(yield* fs.exists(targetFile))) return false
|
|
212
|
+
const [installed, bundled] = yield* Effect.all([
|
|
213
|
+
fs.readFileString(targetFile),
|
|
214
|
+
fs.readFileString(path.join(sourceDir, 'SKILL.md')),
|
|
215
|
+
])
|
|
216
|
+
return installed === bundled
|
|
217
|
+
})
|
|
218
|
+
|
|
219
|
+
return { install, uninstall, isCurrent } as const
|
|
147
220
|
}),
|
|
148
221
|
}) {}
|
package/src/Install/Hook.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Effect, Match, Schema } from 'effect'
|
|
2
2
|
import { ClaudeHook } from './Claude/Hook.js'
|
|
3
|
-
import {
|
|
3
|
+
import { type InstallScope, Mcp } from './Mcp.js'
|
|
4
4
|
import { OpencodeHook } from './Opencode/Hook.js'
|
|
5
5
|
|
|
6
6
|
/**
|
|
@@ -26,7 +26,7 @@ export class HookInstaller extends Effect.Service<HookInstaller>()('@expandai/cl
|
|
|
26
26
|
const claudeHook = yield* ClaudeHook
|
|
27
27
|
const opencodeHook = yield* OpencodeHook
|
|
28
28
|
|
|
29
|
-
const install = Effect.fn('HookInstaller.install')(function* (host: HookHost, scope:
|
|
29
|
+
const install = Effect.fn('HookInstaller.install')(function* (host: HookHost, scope: InstallScope) {
|
|
30
30
|
// Bundle: MCP install first, then host-specific hook config.
|
|
31
31
|
yield* mcp.install(host, scope)
|
|
32
32
|
yield* Match.value(host).pipe(
|
|
@@ -36,7 +36,7 @@ export class HookInstaller extends Effect.Service<HookInstaller>()('@expandai/cl
|
|
|
36
36
|
)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
const uninstall = Effect.fn('HookInstaller.uninstall')(function* (host: HookHost, scope:
|
|
39
|
+
const uninstall = Effect.fn('HookInstaller.uninstall')(function* (host: HookHost, scope: InstallScope) {
|
|
40
40
|
yield* Match.value(host).pipe(
|
|
41
41
|
Match.when('claude-code', () => claudeHook.uninstall(scope)),
|
|
42
42
|
Match.when('opencode', () => opencodeHook.uninstall(scope)),
|
package/src/Install/Mcp.ts
CHANGED
|
@@ -2,10 +2,10 @@ import { Effect, Match } from 'effect'
|
|
|
2
2
|
import { ClaudeMcp } from './Claude/Mcp.js'
|
|
3
3
|
import { CodexMcp } from './Codex/Mcp.js'
|
|
4
4
|
import { CursorMcp } from './Cursor/Mcp.js'
|
|
5
|
-
import type { Host,
|
|
5
|
+
import type { Host, InstallScope } from './Domain.js'
|
|
6
6
|
import { OpencodeMcp } from './Opencode/Mcp.js'
|
|
7
7
|
|
|
8
|
-
export { Host, McpParseError, McpWriteError
|
|
8
|
+
export { Host, InstallScope, McpParseError, McpWriteError } from './Domain.js'
|
|
9
9
|
|
|
10
10
|
/**
|
|
11
11
|
* MCP install dispatcher. Per-host descriptors live in `Install/<Host>/Mcp.ts`
|
|
@@ -26,7 +26,7 @@ export class Mcp extends Effect.Service<Mcp>()('@expandai/cli/Install/Mcp', {
|
|
|
26
26
|
const opencode = yield* OpencodeMcp
|
|
27
27
|
const codex = yield* CodexMcp
|
|
28
28
|
|
|
29
|
-
const pathFor = Effect.fn('Mcp.pathFor')(function* (host: Host, scope:
|
|
29
|
+
const pathFor = Effect.fn('Mcp.pathFor')(function* (host: Host, scope: InstallScope) {
|
|
30
30
|
return yield* Match.value(host).pipe(
|
|
31
31
|
Match.when('claude-code', () => claude.pathFor(scope)),
|
|
32
32
|
Match.when('cursor', () => cursor.pathFor(scope)),
|
|
@@ -36,7 +36,7 @@ export class Mcp extends Effect.Service<Mcp>()('@expandai/cli/Install/Mcp', {
|
|
|
36
36
|
)
|
|
37
37
|
})
|
|
38
38
|
|
|
39
|
-
const install = Effect.fn('Mcp.install')(function* (host: Host, scope:
|
|
39
|
+
const install = Effect.fn('Mcp.install')(function* (host: Host, scope: InstallScope) {
|
|
40
40
|
yield* Match.value(host).pipe(
|
|
41
41
|
Match.when('claude-code', () => claude.install(scope)),
|
|
42
42
|
Match.when('cursor', () => cursor.install(scope)),
|
|
@@ -46,7 +46,7 @@ export class Mcp extends Effect.Service<Mcp>()('@expandai/cli/Install/Mcp', {
|
|
|
46
46
|
)
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
const uninstall = Effect.fn('Mcp.uninstall')(function* (host: Host, scope:
|
|
49
|
+
const uninstall = Effect.fn('Mcp.uninstall')(function* (host: Host, scope: InstallScope) {
|
|
50
50
|
yield* Match.value(host).pipe(
|
|
51
51
|
Match.when('claude-code', () => claude.uninstall(scope)),
|
|
52
52
|
Match.when('cursor', () => cursor.uninstall(scope)),
|
|
@@ -56,7 +56,7 @@ export class Mcp extends Effect.Service<Mcp>()('@expandai/cli/Install/Mcp', {
|
|
|
56
56
|
)
|
|
57
57
|
})
|
|
58
58
|
|
|
59
|
-
const isInstalled = Effect.fn('Mcp.isInstalled')(function* (host: Host, scope:
|
|
59
|
+
const isInstalled = Effect.fn('Mcp.isInstalled')(function* (host: Host, scope: InstallScope) {
|
|
60
60
|
return yield* Match.value(host).pipe(
|
|
61
61
|
Match.when('claude-code', () => claude.isInstalled(scope)),
|
|
62
62
|
Match.when('cursor', () => cursor.isInstalled(scope)),
|