expandai 0.0.0 → 0.0.2
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 +241 -6
- package/build/dts/ApiClient.d.ts +231 -0
- package/build/dts/ApiClient.d.ts.map +1 -0
- package/build/dts/Authentication.d.ts +23 -0
- package/build/dts/Authentication.d.ts.map +1 -0
- package/build/dts/BrowserOAuth.d.ts +99 -0
- package/build/dts/BrowserOAuth.d.ts.map +1 -0
- package/build/dts/Commands/Fetch.d.ts +30 -0
- package/build/dts/Commands/Fetch.d.ts.map +1 -0
- package/build/dts/Commands/Hook.d.ts +17 -0
- package/build/dts/Commands/Hook.d.ts.map +1 -0
- package/build/dts/Commands/Mcp.d.ts +11 -0
- package/build/dts/Commands/Mcp.d.ts.map +1 -0
- package/build/dts/Commands/McpRun.d.ts +5 -0
- package/build/dts/Commands/McpRun.d.ts.map +1 -0
- package/build/dts/Commands/Search.d.ts +21 -0
- package/build/dts/Commands/Search.d.ts.map +1 -0
- package/build/dts/Commands/Skill.d.ts +12 -0
- package/build/dts/Commands/Skill.d.ts.map +1 -0
- package/build/dts/Commands/Uninstall.d.ts +9 -0
- package/build/dts/Commands/Uninstall.d.ts.map +1 -0
- package/build/dts/Config.d.ts +105 -0
- package/build/dts/Config.d.ts.map +1 -0
- package/build/dts/DeviceAuth.d.ts +87 -0
- package/build/dts/DeviceAuth.d.ts.map +1 -0
- package/build/dts/Hook/Claude.d.ts +13 -0
- package/build/dts/Hook/Claude.d.ts.map +1 -0
- package/build/dts/Hook/Errors.d.ts +20 -0
- package/build/dts/Hook/Errors.d.ts.map +1 -0
- package/build/dts/Hook/Opencode.d.ts +13 -0
- package/build/dts/Hook/Opencode.d.ts.map +1 -0
- package/build/dts/Hook/SnapshotNote.d.ts +8 -0
- package/build/dts/Hook/SnapshotNote.d.ts.map +1 -0
- package/build/dts/Install/Claude/Hook.d.ts +60 -0
- package/build/dts/Install/Claude/Hook.d.ts.map +1 -0
- package/build/dts/Install/Claude/Mcp.d.ts +45 -0
- package/build/dts/Install/Claude/Mcp.d.ts.map +1 -0
- package/build/dts/Install/Claude/Skill.d.ts +24 -0
- package/build/dts/Install/Claude/Skill.d.ts.map +1 -0
- package/build/dts/Install/Codex/Mcp.d.ts +39 -0
- package/build/dts/Install/Codex/Mcp.d.ts.map +1 -0
- package/build/dts/Install/Codex/Skill.d.ts +20 -0
- package/build/dts/Install/Codex/Skill.d.ts.map +1 -0
- package/build/dts/Install/Cursor/Mcp.d.ts +45 -0
- package/build/dts/Install/Cursor/Mcp.d.ts.map +1 -0
- package/build/dts/Install/Cursor/Skill.d.ts +27 -0
- package/build/dts/Install/Cursor/Skill.d.ts.map +1 -0
- package/build/dts/Install/Domain.d.ts +73 -0
- package/build/dts/Install/Domain.d.ts.map +1 -0
- package/build/dts/Install/Hook.d.ts +30 -0
- package/build/dts/Install/Hook.d.ts.map +1 -0
- package/build/dts/Install/Mcp.d.ts +28 -0
- package/build/dts/Install/Mcp.d.ts.map +1 -0
- package/build/dts/Install/Opencode/Hook.d.ts +23 -0
- package/build/dts/Install/Opencode/Hook.d.ts.map +1 -0
- package/build/dts/Install/Opencode/Mcp.d.ts +47 -0
- package/build/dts/Install/Opencode/Mcp.d.ts.map +1 -0
- package/build/dts/Install/Opencode/Skill.d.ts +24 -0
- package/build/dts/Install/Opencode/Skill.d.ts.map +1 -0
- package/build/dts/Install/Skill.d.ts +32 -0
- package/build/dts/Install/Skill.d.ts.map +1 -0
- package/build/dts/Install/Uninstall.d.ts +32 -0
- package/build/dts/Install/Uninstall.d.ts.map +1 -0
- package/build/dts/Mcp/ExpandDocs.d.ts +7 -0
- package/build/dts/Mcp/ExpandDocs.d.ts.map +1 -0
- package/build/dts/Mcp/RequestHeaders.d.ts +15 -0
- package/build/dts/Mcp/RequestHeaders.d.ts.map +1 -0
- package/build/dts/Mcp/Server.d.ts +10 -0
- package/build/dts/Mcp/Server.d.ts.map +1 -0
- package/build/dts/Mcp/Toolkit.d.ts +3 -0
- package/build/dts/Mcp/Toolkit.d.ts.map +1 -0
- package/build/dts/Mcp/tools/Citations.d.ts +237 -0
- package/build/dts/Mcp/tools/Citations.d.ts.map +1 -0
- package/build/dts/Mcp/tools/Fetch.d.ts +167 -0
- package/build/dts/Mcp/tools/Fetch.d.ts.map +1 -0
- package/build/dts/Snippets.d.ts +19 -0
- package/build/dts/Snippets.d.ts.map +1 -0
- package/build/dts/index.d.ts +2 -0
- package/build/dts/index.d.ts.map +1 -0
- package/build/dts/main.d.ts +3 -0
- package/build/dts/main.d.ts.map +1 -0
- package/build/esm/ApiClient.js +182 -0
- package/build/esm/ApiClient.js.map +1 -0
- package/build/esm/Authentication.js +112 -0
- package/build/esm/Authentication.js.map +1 -0
- package/build/esm/BrowserOAuth.js +254 -0
- package/build/esm/BrowserOAuth.js.map +1 -0
- package/build/esm/Commands/Fetch.js +95 -0
- package/build/esm/Commands/Fetch.js.map +1 -0
- package/build/esm/Commands/Hook.js +20 -0
- package/build/esm/Commands/Hook.js.map +1 -0
- package/build/esm/Commands/Mcp.js +19 -0
- package/build/esm/Commands/Mcp.js.map +1 -0
- package/build/esm/Commands/McpRun.js +15 -0
- package/build/esm/Commands/McpRun.js.map +1 -0
- package/build/esm/Commands/Search.js +43 -0
- package/build/esm/Commands/Search.js.map +1 -0
- package/build/esm/Commands/Skill.js +17 -0
- package/build/esm/Commands/Skill.js.map +1 -0
- package/build/esm/Commands/Uninstall.js +28 -0
- package/build/esm/Commands/Uninstall.js.map +1 -0
- package/build/esm/Config.js +145 -0
- package/build/esm/Config.js.map +1 -0
- package/build/esm/DeviceAuth.js +127 -0
- package/build/esm/DeviceAuth.js.map +1 -0
- package/build/esm/Hook/Claude.js +74 -0
- package/build/esm/Hook/Claude.js.map +1 -0
- package/build/esm/Hook/Errors.js +18 -0
- package/build/esm/Hook/Errors.js.map +1 -0
- package/build/esm/Hook/Opencode.js +50 -0
- package/build/esm/Hook/Opencode.js.map +1 -0
- package/build/esm/Hook/SnapshotNote.js +18 -0
- package/build/esm/Hook/SnapshotNote.js.map +1 -0
- package/build/esm/Install/Claude/Hook.js +89 -0
- package/build/esm/Install/Claude/Hook.js.map +1 -0
- package/build/esm/Install/Claude/Mcp.js +71 -0
- package/build/esm/Install/Claude/Mcp.js.map +1 -0
- package/build/esm/Install/Claude/Skill.js +34 -0
- package/build/esm/Install/Claude/Skill.js.map +1 -0
- package/build/esm/Install/Codex/Mcp.js +92 -0
- package/build/esm/Install/Codex/Mcp.js.map +1 -0
- package/build/esm/Install/Codex/Skill.js +30 -0
- package/build/esm/Install/Codex/Skill.js.map +1 -0
- package/build/esm/Install/Cursor/Mcp.js +71 -0
- package/build/esm/Install/Cursor/Mcp.js.map +1 -0
- package/build/esm/Install/Cursor/Skill.js +37 -0
- package/build/esm/Install/Cursor/Skill.js.map +1 -0
- package/build/esm/Install/Domain.js +105 -0
- package/build/esm/Install/Domain.js.map +1 -0
- package/build/esm/Install/Hook.js +38 -0
- package/build/esm/Install/Hook.js.map +1 -0
- package/build/esm/Install/Mcp.js +40 -0
- package/build/esm/Install/Mcp.js.map +1 -0
- package/build/esm/Install/Opencode/Hook.js +44 -0
- package/build/esm/Install/Opencode/Hook.js.map +1 -0
- package/build/esm/Install/Opencode/Mcp.js +73 -0
- package/build/esm/Install/Opencode/Mcp.js.map +1 -0
- package/build/esm/Install/Opencode/Skill.js +34 -0
- package/build/esm/Install/Opencode/Skill.js.map +1 -0
- package/build/esm/Install/Opencode/expandai-webfetch.js +38 -0
- package/build/esm/Install/Skill.js +41 -0
- package/build/esm/Install/Skill.js.map +1 -0
- package/build/esm/Install/Uninstall.js +49 -0
- package/build/esm/Install/Uninstall.js.map +1 -0
- package/build/esm/Mcp/ExpandDocs.js +64 -0
- package/build/esm/Mcp/ExpandDocs.js.map +1 -0
- package/build/esm/Mcp/RequestHeaders.js +13 -0
- package/build/esm/Mcp/RequestHeaders.js.map +1 -0
- package/build/esm/Mcp/Server.js +11 -0
- package/build/esm/Mcp/Server.js.map +1 -0
- package/build/esm/Mcp/Toolkit.js +16 -0
- package/build/esm/Mcp/Toolkit.js.map +1 -0
- package/build/esm/Mcp/tools/Citations.js +92 -0
- package/build/esm/Mcp/tools/Citations.js.map +1 -0
- package/build/esm/Mcp/tools/Fetch.js +141 -0
- package/build/esm/Mcp/tools/Fetch.js.map +1 -0
- package/build/esm/Snippets.js +18 -0
- package/build/esm/Snippets.js.map +1 -0
- package/build/esm/index.js +2 -0
- package/build/esm/index.js.map +1 -0
- package/build/esm/main.js +97 -0
- package/build/esm/main.js.map +1 -0
- package/build/skills/expandai/SKILL.md +72 -0
- package/package.json +72 -10
- package/src/ApiClient.ts +257 -0
- package/src/Authentication.ts +164 -0
- package/src/BrowserOAuth.ts +361 -0
- package/src/Commands/Fetch.ts +157 -0
- package/src/Commands/Hook.ts +48 -0
- package/src/Commands/Mcp.ts +30 -0
- package/src/Commands/McpRun.ts +25 -0
- package/src/Commands/Search.ts +92 -0
- package/src/Commands/Skill.ts +30 -0
- package/src/Commands/Uninstall.ts +38 -0
- package/src/Config.ts +195 -0
- package/src/DeviceAuth.ts +193 -0
- package/src/Hook/Claude.ts +81 -0
- package/src/Hook/Errors.ts +19 -0
- package/src/Hook/Opencode.ts +58 -0
- package/src/Hook/SnapshotNote.ts +18 -0
- package/src/Install/Claude/Hook.ts +133 -0
- package/src/Install/Claude/Mcp.ts +82 -0
- package/src/Install/Claude/Skill.ts +44 -0
- package/src/Install/Codex/Mcp.ts +109 -0
- package/src/Install/Codex/Skill.ts +40 -0
- package/src/Install/Cursor/Mcp.ts +82 -0
- package/src/Install/Cursor/Skill.ts +47 -0
- package/src/Install/Domain.ts +148 -0
- package/src/Install/Hook.ts +50 -0
- package/src/Install/Mcp.ts +71 -0
- package/src/Install/Opencode/Hook.ts +55 -0
- package/src/Install/Opencode/Mcp.ts +84 -0
- package/src/Install/Opencode/Skill.ts +44 -0
- package/src/Install/Opencode/expandai-webfetch.d.ts +23 -0
- package/src/Install/Opencode/expandai-webfetch.js +38 -0
- package/src/Install/Skill.ts +57 -0
- package/src/Install/Uninstall.ts +82 -0
- package/src/Mcp/ExpandDocs.ts +71 -0
- package/src/Mcp/RequestHeaders.ts +16 -0
- package/src/Mcp/Server.ts +14 -0
- package/src/Mcp/Toolkit.ts +21 -0
- package/src/Mcp/tools/Citations.ts +110 -0
- package/src/Mcp/tools/Fetch.ts +177 -0
- package/src/Snippets.ts +32 -0
- package/src/index.ts +1 -0
- package/src/main.ts +146 -0
- package/bun.lockb +0 -0
- package/index.ts +0 -1
- package/tsconfig.json +0 -27
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { NodeStream } from '@effect/platform-node'
|
|
2
|
+
import { Console, Effect, Schema, String } from 'effect'
|
|
3
|
+
import { ApiClient } from '../ApiClient.js'
|
|
4
|
+
import { MarkdownUnavailable } from '../Commands/Fetch.js'
|
|
5
|
+
import { HookProtocolFailed, StdinReadError } from './Errors.js'
|
|
6
|
+
import { withSnapshotNote } from './SnapshotNote.js'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Runner for `expandai hook opencode`. Reads `{ url }` from stdin (sent by
|
|
10
|
+
* the OpenCode plugin), fetches via expand.ai, and writes `{ markdown }`
|
|
11
|
+
* to stdout for the plugin to splice back into webfetch's result.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
class ProtocolInput extends Schema.Class<ProtocolInput>('OpencodeWebfetchInput')({
|
|
15
|
+
url: Schema.String,
|
|
16
|
+
}) {}
|
|
17
|
+
|
|
18
|
+
const decodeProtocolInput = Schema.decode(Schema.parseJson(ProtocolInput))
|
|
19
|
+
const encodeProtocolOutput = Schema.encode(Schema.parseJson(Schema.Unknown))
|
|
20
|
+
|
|
21
|
+
const readStdin = NodeStream.toString(() => process.stdin, {
|
|
22
|
+
onFailure: (error) => new StdinReadError({ error }),
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
export class OpencodeHook extends Effect.Service<OpencodeHook>()('@expandai/cli/Hook/Opencode', {
|
|
27
|
+
dependencies: [ApiClient.Default],
|
|
28
|
+
effect: Effect.gen(function* () {
|
|
29
|
+
const apiClient = yield* ApiClient
|
|
30
|
+
|
|
31
|
+
const runProtocol = Effect.fn('OpencodeHook.runProtocol')(function* () {
|
|
32
|
+
const raw = yield* readStdin.pipe(Effect.mapError((error) => new HookProtocolFailed({ error })))
|
|
33
|
+
const payload = yield* decodeProtocolInput(raw).pipe(
|
|
34
|
+
Effect.mapError((error) => new HookProtocolFailed({ error })),
|
|
35
|
+
)
|
|
36
|
+
const url = payload.url
|
|
37
|
+
|
|
38
|
+
yield* Effect.gen(function* () {
|
|
39
|
+
// Object mode (`/v1/fetch/json`) over markdown mode so the response carries
|
|
40
|
+
// `meta.snapshotId` — the handle the agent uses to search this same capture
|
|
41
|
+
// via `fetch_search` instead of fetching the URL again. The note rides inside
|
|
42
|
+
// `markdown`, so the installed plugin needs no protocol change.
|
|
43
|
+
const result = yield* apiClient.fetchFetchResult({
|
|
44
|
+
params: {},
|
|
45
|
+
payload: { url, include: { markdown: true } },
|
|
46
|
+
})
|
|
47
|
+
if (String.isEmpty(result.markdown)) {
|
|
48
|
+
return yield* new MarkdownUnavailable({ url })
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const markdown = withSnapshotNote(result.markdown, result.meta.snapshotId, result.meta.playground)
|
|
52
|
+
yield* Console.log(yield* encodeProtocolOutput({ markdown }))
|
|
53
|
+
}).pipe(Effect.mapError((error) => new HookProtocolFailed({ error, url })))
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
return { runProtocol } as const
|
|
57
|
+
}),
|
|
58
|
+
}) {}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { String } from 'effect'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Prepends a short note to fetched markdown carrying the snapshot handle and the public
|
|
5
|
+
* playground link, and telling the agent how to search the already-captured page by handle
|
|
6
|
+
* (rather than fetching the URL again) and cite specific passages back to the user. Both the
|
|
7
|
+
* Claude Code and OpenCode hooks inject this so the surfaced guidance stays identical.
|
|
8
|
+
*/
|
|
9
|
+
export function withSnapshotNote(markdown: string, snapshotId: string, playgroundUrl: string): string {
|
|
10
|
+
const note = String.stripMargin(`
|
|
11
|
+
|> Fetched via expand.ai (snapshot \`${snapshotId}\`). View or share this capture at
|
|
12
|
+
|> ${playgroundUrl}
|
|
13
|
+
|> To extract specific passages, search the snapshot by handle — call the \`fetch_search\`
|
|
14
|
+
|> tool with \`snapshotId: "${snapshotId}"\`, or run \`expandai search ${snapshotId} "<query>"\`.
|
|
15
|
+
|> Each snippet returns a \`citationUrl\` you can hand the user to audit the exact source.
|
|
16
|
+
|> Do not fetch this URL again.`)
|
|
17
|
+
return `${note}\n\n---\n\n${markdown}`
|
|
18
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { Array, Config, Console, Effect, Match, Schema } from 'effect'
|
|
3
|
+
import { ConfigIo } from '../Domain.js'
|
|
4
|
+
import type { Scope } from '../Mcp.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Claude Code hook config — `~/.claude/settings.json` (global) or
|
|
8
|
+
* `<cwd>/.claude/settings.json` (project). PreToolUse hooks fire on built-in
|
|
9
|
+
* tools; we register the WebFetch matcher to route to `expandai hook claude-code`.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export const HOOK_COMMAND = 'expandai hook claude-code'
|
|
13
|
+
export const WEBFETCH_MATCHER = 'WebFetch'
|
|
14
|
+
|
|
15
|
+
export const HookCommandEntry = Schema.Struct({
|
|
16
|
+
type: Schema.Literal('command'),
|
|
17
|
+
command: Schema.String,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
export const HookMatcher = Schema.Struct({
|
|
21
|
+
matcher: Schema.optional(Schema.String),
|
|
22
|
+
hooks: Schema.Array(HookCommandEntry),
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const HooksSection = Schema.Struct(
|
|
26
|
+
{ PreToolUse: Schema.optional(Schema.Array(HookMatcher)) },
|
|
27
|
+
Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
export const SettingsSchema = Schema.parseJson(
|
|
31
|
+
Schema.Struct(
|
|
32
|
+
{ hooks: Schema.optionalWith(HooksSection, { default: () => ({}) as typeof HooksSection.Type }) },
|
|
33
|
+
Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
34
|
+
),
|
|
35
|
+
{ space: 2 },
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
export const expandaiMatcher = {
|
|
39
|
+
matcher: WEBFETCH_MATCHER,
|
|
40
|
+
hooks: [{ type: 'command' as const, command: HOOK_COMMAND }],
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export class ClaudeHook extends Effect.Service<ClaudeHook>()('@expandai/cli/Install/Claude/Hook', {
|
|
44
|
+
dependencies: [ConfigIo.Default],
|
|
45
|
+
effect: Effect.gen(function* () {
|
|
46
|
+
const io = yield* ConfigIo
|
|
47
|
+
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
48
|
+
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
49
|
+
|
|
50
|
+
const pathFor = Effect.fn(function* (scope: Scope) {
|
|
51
|
+
return yield* Match.value(scope).pipe(
|
|
52
|
+
Match.when('global', () => Effect.succeed(`${home}/.claude/settings.json`)),
|
|
53
|
+
Match.when('project', () => Effect.succeed(`${cwd}/.claude/settings.json`)),
|
|
54
|
+
Match.exhaustive,
|
|
55
|
+
)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
const install = Effect.fn('ClaudeHook.install')(function* (scope: Scope) {
|
|
59
|
+
const target = yield* pathFor(scope)
|
|
60
|
+
const settings = yield* io.read(target, SettingsSchema)
|
|
61
|
+
|
|
62
|
+
const preToolUse = settings.hooks.PreToolUse ?? []
|
|
63
|
+
const alreadyInstalled = preToolUse.some(
|
|
64
|
+
(entry) => entry.matcher === WEBFETCH_MATCHER && entry.hooks.some((hook) => hook.command === HOOK_COMMAND),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
if (alreadyInstalled) {
|
|
68
|
+
yield* Console.log(`expandai WebFetch hook already installed at ${target}`)
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
yield* io.write(
|
|
73
|
+
target,
|
|
74
|
+
{
|
|
75
|
+
...settings,
|
|
76
|
+
hooks: {
|
|
77
|
+
...settings.hooks,
|
|
78
|
+
PreToolUse: [...preToolUse, expandaiMatcher],
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
SettingsSchema,
|
|
82
|
+
)
|
|
83
|
+
yield* Console.log(`Installed expandai WebFetch hook at ${target}`)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
const uninstall = Effect.fn('ClaudeHook.uninstall')(function* (scope: Scope) {
|
|
87
|
+
const target = yield* pathFor(scope)
|
|
88
|
+
const exists = yield* io.exists(target)
|
|
89
|
+
if (!exists) {
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const settings = yield* io.read(target, SettingsSchema)
|
|
94
|
+
const preToolUse = settings.hooks.PreToolUse ?? []
|
|
95
|
+
const updatedPreToolUse = Array.filter(
|
|
96
|
+
Array.map(preToolUse, (entry) => ({
|
|
97
|
+
...entry,
|
|
98
|
+
hooks: Array.filter(entry.hooks, (hook) => hook.command !== HOOK_COMMAND),
|
|
99
|
+
})),
|
|
100
|
+
(entry) => entry.matcher !== WEBFETCH_MATCHER || entry.hooks.length > 0,
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Compare total hook-command count rather than outer-array length:
|
|
104
|
+
// our HOOK_COMMAND can sit alongside other hooks in a shared matcher
|
|
105
|
+
// entry (e.g. a user merged it in by hand), in which case the entry
|
|
106
|
+
// stays after filtering but the inner hook count drops.
|
|
107
|
+
const countCommands = Effect.fn(function* (entries: typeof preToolUse) {
|
|
108
|
+
return Array.reduce(entries, 0, (n, entry) => n + entry.hooks.length)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
const before = yield* countCommands(preToolUse)
|
|
112
|
+
const after = yield* countCommands(updatedPreToolUse)
|
|
113
|
+
if (before === after) {
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
yield* io.write(
|
|
118
|
+
target,
|
|
119
|
+
{
|
|
120
|
+
...settings,
|
|
121
|
+
hooks: {
|
|
122
|
+
...settings.hooks,
|
|
123
|
+
PreToolUse: updatedPreToolUse,
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
SettingsSchema,
|
|
127
|
+
)
|
|
128
|
+
yield* Console.log(`Removed expandai WebFetch hook from ${target}`)
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
return { install, uninstall } as const
|
|
132
|
+
}),
|
|
133
|
+
}) {}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { Config, Console, Effect, Match, Record, Schema } from 'effect'
|
|
3
|
+
import { ConfigIo, McpParseError, McpServersConfigSchema, McpWriteError, SERVER_KEY } from '../Domain.js'
|
|
4
|
+
import type { Scope } from '../Mcp.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Claude Code MCP descriptor — `mcpServers` shape (de facto Claude Desktop
|
|
8
|
+
* convention; also read by Cursor and other clients).
|
|
9
|
+
*
|
|
10
|
+
* - global → `~/.claude.json` (Claude Code's user-scope store)
|
|
11
|
+
* - project → `<cwd>/.mcp.json` (the agent-agnostic project file)
|
|
12
|
+
*/
|
|
13
|
+
export class Entry extends Schema.Class<Entry>('ClaudeMcpEntry')({
|
|
14
|
+
type: Schema.Literal('stdio'),
|
|
15
|
+
command: Schema.NonEmptyTrimmedString,
|
|
16
|
+
args: Schema.Array(Schema.String),
|
|
17
|
+
env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
|
|
18
|
+
}) {}
|
|
19
|
+
|
|
20
|
+
export class ClaudeMcp extends Effect.Service<ClaudeMcp>()('@expandai/cli/Install/Claude/Mcp', {
|
|
21
|
+
dependencies: [ConfigIo.Default],
|
|
22
|
+
effect: Effect.gen(function* () {
|
|
23
|
+
const io = yield* ConfigIo
|
|
24
|
+
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
25
|
+
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
26
|
+
|
|
27
|
+
const pathFor = Effect.fn(function* (scope: Scope) {
|
|
28
|
+
return yield* Match.value(scope).pipe(
|
|
29
|
+
Match.when('global', () => Effect.succeed(`${home}/.claude.json`)),
|
|
30
|
+
Match.when('project', () => Effect.succeed(`${cwd}/.mcp.json`)),
|
|
31
|
+
Match.exhaustive,
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const readConfig = Effect.fn(function* (file: string) {
|
|
36
|
+
return yield* io
|
|
37
|
+
.read(file, McpServersConfigSchema)
|
|
38
|
+
.pipe(Effect.mapError((error) => new McpParseError({ path: file, error })))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const writeConfig = Effect.fn(function* (file: string, value: unknown) {
|
|
42
|
+
yield* io
|
|
43
|
+
.write(file, value, McpServersConfigSchema)
|
|
44
|
+
.pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const install = Effect.fn(function* (scope: Scope) {
|
|
48
|
+
const file = yield* pathFor(scope)
|
|
49
|
+
const config = yield* readConfig(file)
|
|
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
|
+
})
|
|
61
|
+
yield* Console.log(`Installed expandai MCP server into ${file}`)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const uninstall = Effect.fn(function* (scope: Scope) {
|
|
65
|
+
const file = yield* pathFor(scope)
|
|
66
|
+
const config = yield* readConfig(file)
|
|
67
|
+
if (!(SERVER_KEY in config.mcpServers)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
yield* writeConfig(file, { ...config, mcpServers: Record.remove(config.mcpServers, SERVER_KEY) })
|
|
71
|
+
yield* Console.log(`Removed expandai MCP server from ${file}`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const isInstalled = Effect.fn(function* (scope: Scope) {
|
|
75
|
+
const file = yield* pathFor(scope)
|
|
76
|
+
const config = yield* readConfig(file)
|
|
77
|
+
return SERVER_KEY in config.mcpServers
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return { install, uninstall, isInstalled, pathFor } as const
|
|
81
|
+
}),
|
|
82
|
+
}) {}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { Config, Effect, Match } from 'effect'
|
|
3
|
+
import { SkillIo } from '../Domain.js'
|
|
4
|
+
import type { Scope } from '../Mcp.js'
|
|
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
|
+
*/
|
|
16
|
+
|
|
17
|
+
export const SKILL_NAME = 'expandai'
|
|
18
|
+
|
|
19
|
+
export class ClaudeSkill extends Effect.Service<ClaudeSkill>()('@expandai/cli/Install/Claude/Skill', {
|
|
20
|
+
dependencies: [SkillIo.Default],
|
|
21
|
+
effect: Effect.gen(function* () {
|
|
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
|
+
}),
|
|
44
|
+
}) {}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { Config, Console, Effect, Match, Record, Schema } from 'effect'
|
|
3
|
+
import { parse, stringify, type TomlTableWithoutBigInt } from 'smol-toml'
|
|
4
|
+
import { ConfigIo, McpParseError, McpWriteError, SERVER_KEY } from '../Domain.js'
|
|
5
|
+
import type { Scope } from '../Mcp.js'
|
|
6
|
+
|
|
7
|
+
export class Entry extends Schema.Class<Entry>('CodexMcpEntry')({
|
|
8
|
+
command: Schema.NonEmptyTrimmedString,
|
|
9
|
+
args: Schema.Array(Schema.String),
|
|
10
|
+
enabled: Schema.Boolean,
|
|
11
|
+
}) {}
|
|
12
|
+
|
|
13
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
14
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const getMcpServers = Effect.fn(function* (config: TomlTableWithoutBigInt) {
|
|
18
|
+
const servers = config.mcp_servers
|
|
19
|
+
return isRecord(servers) ? servers : {}
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
function toStringifyValue(value: TomlTableWithoutBigInt) {
|
|
23
|
+
// smol-toml's stringify type accepts any; keep the unsafe boundary local.
|
|
24
|
+
return value as unknown as Record<string, unknown>
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Codex MCP descriptor — Codex stores MCP servers in TOML config under
|
|
29
|
+
* `[mcp_servers.<name>]`.
|
|
30
|
+
*
|
|
31
|
+
* - global → `~/.codex/config.toml`
|
|
32
|
+
* - project → `<cwd>/.codex/config.toml`
|
|
33
|
+
*/
|
|
34
|
+
export class CodexMcp extends Effect.Service<CodexMcp>()('@expandai/cli/Install/Codex/Mcp', {
|
|
35
|
+
dependencies: [ConfigIo.Default],
|
|
36
|
+
effect: Effect.gen(function* () {
|
|
37
|
+
const io = yield* ConfigIo
|
|
38
|
+
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
39
|
+
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
40
|
+
|
|
41
|
+
const pathFor = Effect.fn(function* (scope: Scope) {
|
|
42
|
+
return yield* Match.value(scope).pipe(
|
|
43
|
+
Match.when('global', () => Effect.succeed(`${home}/.codex/config.toml`)),
|
|
44
|
+
Match.when('project', () => Effect.succeed(`${cwd}/.codex/config.toml`)),
|
|
45
|
+
Match.exhaustive,
|
|
46
|
+
)
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const readConfig = Effect.fn(function* (file: string) {
|
|
50
|
+
const text = yield* io.readText(file)
|
|
51
|
+
return yield* Effect.try({
|
|
52
|
+
try: () => parse(text, { integersAsBigInt: false }),
|
|
53
|
+
catch: (error) => new McpParseError({ path: file, error }),
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
const writeConfig = Effect.fn(function* (file: string, value: TomlTableWithoutBigInt) {
|
|
58
|
+
const encoded = yield* Effect.try({
|
|
59
|
+
try: () => stringify(toStringifyValue(value)),
|
|
60
|
+
catch: (error) => new McpWriteError({ path: file, error }),
|
|
61
|
+
})
|
|
62
|
+
yield* io.writeText(file, encoded).pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const install = Effect.fn(function* (scope: Scope) {
|
|
66
|
+
const file = yield* pathFor(scope)
|
|
67
|
+
const config = yield* readConfig(file)
|
|
68
|
+
const servers = yield* getMcpServers(config)
|
|
69
|
+
|
|
70
|
+
const entryModel = new Entry({
|
|
71
|
+
command: 'expandai',
|
|
72
|
+
args: ['mcp', 'run'],
|
|
73
|
+
enabled: true,
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
const entry: TomlTableWithoutBigInt = {
|
|
77
|
+
command: entryModel.command,
|
|
78
|
+
args: [...entryModel.args],
|
|
79
|
+
enabled: entryModel.enabled,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
yield* writeConfig(file, {
|
|
83
|
+
...config,
|
|
84
|
+
mcp_servers: Record.set(servers, SERVER_KEY, entry),
|
|
85
|
+
})
|
|
86
|
+
yield* Console.log(`Installed expandai MCP server into ${file}`)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
const uninstall = Effect.fn(function* (scope: Scope) {
|
|
90
|
+
const file = yield* pathFor(scope)
|
|
91
|
+
const config = yield* readConfig(file)
|
|
92
|
+
const servers = yield* getMcpServers(config)
|
|
93
|
+
if (!(SERVER_KEY in servers)) {
|
|
94
|
+
return
|
|
95
|
+
}
|
|
96
|
+
yield* writeConfig(file, { ...config, mcp_servers: Record.remove(servers, SERVER_KEY) })
|
|
97
|
+
yield* Console.log(`Removed expandai MCP server from ${file}`)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const isInstalled = Effect.fn(function* (scope: Scope) {
|
|
101
|
+
const file = yield* pathFor(scope)
|
|
102
|
+
const config = yield* readConfig(file)
|
|
103
|
+
const servers = yield* getMcpServers(config)
|
|
104
|
+
return SERVER_KEY in servers
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
return { install, uninstall, isInstalled, pathFor } as const
|
|
108
|
+
}),
|
|
109
|
+
}) {}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { Config, Effect, Match } from 'effect'
|
|
3
|
+
import { SkillIo } from '../Domain.js'
|
|
4
|
+
import type { Scope } from '../Mcp.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Codex skills use the open agent skills locations:
|
|
8
|
+
*
|
|
9
|
+
* - global → `~/.agents/skills/<name>/`
|
|
10
|
+
* - project → `<cwd>/.agents/skills/<name>/`
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export const SKILL_NAME = 'expandai'
|
|
14
|
+
|
|
15
|
+
export class CodexSkill extends Effect.Service<CodexSkill>()('@expandai/cli/Install/Codex/Skill', {
|
|
16
|
+
dependencies: [SkillIo.Default],
|
|
17
|
+
effect: Effect.gen(function* () {
|
|
18
|
+
const io = yield* SkillIo
|
|
19
|
+
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
20
|
+
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
21
|
+
|
|
22
|
+
const pathFor = Effect.fn(function* (scope: Scope) {
|
|
23
|
+
return yield* Match.value(scope).pipe(
|
|
24
|
+
Match.when('global', () => Effect.succeed(`${home}/.agents/skills/${SKILL_NAME}`)),
|
|
25
|
+
Match.when('project', () => Effect.succeed(`${cwd}/.agents/skills/${SKILL_NAME}`)),
|
|
26
|
+
Match.exhaustive,
|
|
27
|
+
)
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const install = Effect.fn(function* (scope: Scope) {
|
|
31
|
+
return yield* io.install(yield* pathFor(scope))
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
const uninstall = Effect.fn(function* (scope: Scope) {
|
|
35
|
+
return yield* io.uninstall(yield* pathFor(scope))
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
return { install, uninstall } as const
|
|
39
|
+
}),
|
|
40
|
+
}) {}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { Config, Console, Effect, Match, Record, Schema } from 'effect'
|
|
3
|
+
import { ConfigIo, McpParseError, McpServersConfigSchema, McpWriteError, SERVER_KEY } from '../Domain.js'
|
|
4
|
+
import type { Scope } from '../Mcp.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cursor MCP descriptor — same `mcpServers` shape as Claude Code, different
|
|
8
|
+
* paths.
|
|
9
|
+
*
|
|
10
|
+
* - global → `~/.cursor/mcp.json`
|
|
11
|
+
* - project → `<cwd>/.cursor/mcp.json`
|
|
12
|
+
*/
|
|
13
|
+
export class Entry extends Schema.Class<Entry>('CursorMcpEntry')({
|
|
14
|
+
type: Schema.Literal('stdio'),
|
|
15
|
+
command: Schema.NonEmptyTrimmedString,
|
|
16
|
+
args: Schema.Array(Schema.String),
|
|
17
|
+
env: Schema.optional(Schema.Record({ key: Schema.String, value: Schema.String })),
|
|
18
|
+
}) {}
|
|
19
|
+
|
|
20
|
+
export class CursorMcp extends Effect.Service<CursorMcp>()('@expandai/cli/Install/Cursor/Mcp', {
|
|
21
|
+
dependencies: [ConfigIo.Default],
|
|
22
|
+
effect: Effect.gen(function* () {
|
|
23
|
+
const io = yield* ConfigIo
|
|
24
|
+
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
25
|
+
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
26
|
+
|
|
27
|
+
const pathFor = Effect.fn(function* (scope: Scope) {
|
|
28
|
+
return yield* Match.value(scope).pipe(
|
|
29
|
+
Match.when('global', () => Effect.succeed(`${home}/.cursor/mcp.json`)),
|
|
30
|
+
Match.when('project', () => Effect.succeed(`${cwd}/.cursor/mcp.json`)),
|
|
31
|
+
Match.exhaustive,
|
|
32
|
+
)
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
const readConfig = Effect.fn(function* (file: string) {
|
|
36
|
+
return yield* io
|
|
37
|
+
.read(file, McpServersConfigSchema)
|
|
38
|
+
.pipe(Effect.mapError((error) => new McpParseError({ path: file, error })))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const writeConfig = Effect.fn(function* (file: string, value: unknown) {
|
|
42
|
+
yield* io
|
|
43
|
+
.write(file, value, McpServersConfigSchema)
|
|
44
|
+
.pipe(Effect.mapError((error) => new McpWriteError({ path: file, error })))
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
const install = Effect.fn(function* (scope: Scope) {
|
|
48
|
+
const file = yield* pathFor(scope)
|
|
49
|
+
const config = yield* readConfig(file)
|
|
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
|
+
})
|
|
61
|
+
yield* Console.log(`Installed expandai MCP server into ${file}`)
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
const uninstall = Effect.fn(function* (scope: Scope) {
|
|
65
|
+
const file = yield* pathFor(scope)
|
|
66
|
+
const config = yield* readConfig(file)
|
|
67
|
+
if (!(SERVER_KEY in config.mcpServers)) {
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
yield* writeConfig(file, { ...config, mcpServers: Record.remove(config.mcpServers, SERVER_KEY) })
|
|
71
|
+
yield* Console.log(`Removed expandai MCP server from ${file}`)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const isInstalled = Effect.fn(function* (scope: Scope) {
|
|
75
|
+
const file = yield* pathFor(scope)
|
|
76
|
+
const config = yield* readConfig(file)
|
|
77
|
+
return SERVER_KEY in config.mcpServers
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
return { install, uninstall, isInstalled, pathFor } as const
|
|
81
|
+
}),
|
|
82
|
+
}) {}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { Config, Effect, Match } from 'effect'
|
|
3
|
+
import { SkillIo } from '../Domain.js'
|
|
4
|
+
import type { Scope } from '../Mcp.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cursor skills are auto-loaded from these locations (per
|
|
8
|
+
* cursor.com/docs/skills):
|
|
9
|
+
*
|
|
10
|
+
* - global → `~/.cursor/skills/<name>/`
|
|
11
|
+
* - project → `<cwd>/.cursor/skills/<name>/`
|
|
12
|
+
*
|
|
13
|
+
* Cursor also compat-loads `.claude/skills/`, `.codex/skills/`, `.agents/skills/`
|
|
14
|
+
* at both project and user scope, but we ship to Cursor's native paths.
|
|
15
|
+
*
|
|
16
|
+
* Format is the same SKILL.md + YAML frontmatter open standard as Claude Code
|
|
17
|
+
* and OpenCode, so the bundled `skills/expandai/` directory copies in unchanged.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const SKILL_NAME = 'expandai'
|
|
21
|
+
|
|
22
|
+
export class CursorSkill extends Effect.Service<CursorSkill>()('@expandai/cli/Install/Cursor/Skill', {
|
|
23
|
+
dependencies: [SkillIo.Default],
|
|
24
|
+
effect: Effect.gen(function* () {
|
|
25
|
+
const io = yield* SkillIo
|
|
26
|
+
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
27
|
+
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
28
|
+
|
|
29
|
+
const pathFor = Effect.fn(function* (scope: Scope) {
|
|
30
|
+
return yield* Match.value(scope).pipe(
|
|
31
|
+
Match.when('global', () => Effect.succeed(`${home}/.cursor/skills/${SKILL_NAME}`)),
|
|
32
|
+
Match.when('project', () => Effect.succeed(`${cwd}/.cursor/skills/${SKILL_NAME}`)),
|
|
33
|
+
Match.exhaustive,
|
|
34
|
+
)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const install = Effect.fn(function* (scope: Scope) {
|
|
38
|
+
return yield* io.install(yield* pathFor(scope))
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const uninstall = Effect.fn(function* (scope: Scope) {
|
|
42
|
+
return yield* io.uninstall(yield* pathFor(scope))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
return { install, uninstall } as const
|
|
46
|
+
}),
|
|
47
|
+
}) {}
|