expandai 0.0.2 → 0.0.4
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 +77 -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 +57 -29
- 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 +344 -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 +52 -64
- 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 +37 -8
- 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 +453 -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 +81 -91
- 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
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { DateTime, Effect, Option, Schema } from 'effect'
|
|
2
|
+
import {
|
|
3
|
+
CLI_AUTHORIZATION_CLIENT_ID,
|
|
4
|
+
PendingInitCorrupt,
|
|
5
|
+
type PendingInitKey,
|
|
6
|
+
PendingInitRecord,
|
|
7
|
+
} from '../Auth/Domain.js'
|
|
8
|
+
import { PendingInitStore } from '../Auth/PendingInit.js'
|
|
9
|
+
import { authMethodState } from '../Authentication.js'
|
|
10
|
+
import { ConfigManager, ConfigParseError, CredentialStoreInsecure, NotAuthenticated } from '../Config.js'
|
|
11
|
+
import { CLI_VERSION } from '../Version.js'
|
|
12
|
+
import { CLAUDE_POLICY_READ_ONLY } from './ClaudeProfile.js'
|
|
13
|
+
import { InitHarness, InitScope } from './Domain.js'
|
|
14
|
+
import type { HarnessInspection } from './HarnessAdapter.js'
|
|
15
|
+
import { HarnessRegistry } from './HarnessRegistry.js'
|
|
16
|
+
|
|
17
|
+
const VERSION_PATTERN = /^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u
|
|
18
|
+
const BoundedVersion = Schema.String.pipe(Schema.minLength(1), Schema.maxLength(64), Schema.pattern(VERSION_PATTERN))
|
|
19
|
+
|
|
20
|
+
export class StatusRequest extends Schema.Class<StatusRequest>('StatusRequest')({
|
|
21
|
+
harness: InitHarness,
|
|
22
|
+
scope: InitScope,
|
|
23
|
+
}) {}
|
|
24
|
+
|
|
25
|
+
const StatusAuth = Schema.Struct({
|
|
26
|
+
// `healthy` means a call would authenticate right now. An expired access
|
|
27
|
+
// token with a refresh token on disk reports `refresh_required`, because the
|
|
28
|
+
// renewal has not happened and can fail.
|
|
29
|
+
state: Schema.Literal('absent', 'unusable', 'refresh_required', 'healthy'),
|
|
30
|
+
healthy: Schema.Boolean,
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const PendingAuthorization = Schema.Union(
|
|
34
|
+
Schema.Struct({ state: Schema.Literal('none'), count: Schema.Literal(0) }),
|
|
35
|
+
Schema.Struct({
|
|
36
|
+
state: Schema.Literal('approval_pending'),
|
|
37
|
+
count: Schema.Literal(1),
|
|
38
|
+
approvalExpiresAt: Schema.DateTimeUtc,
|
|
39
|
+
}),
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
const StatusInstallation = Schema.Struct({
|
|
43
|
+
configured: Schema.Boolean,
|
|
44
|
+
integrationVersion: Schema.optional(BoundedVersion),
|
|
45
|
+
components: Schema.Struct({
|
|
46
|
+
webfetchHook: Schema.Boolean,
|
|
47
|
+
runtimeSkill: Schema.Boolean,
|
|
48
|
+
mcp: Schema.Boolean,
|
|
49
|
+
}),
|
|
50
|
+
runtimeState: Schema.Literal('unverified', 'reload_required', 'blocked'),
|
|
51
|
+
reloadState: Schema.Literal('not_required', 'required'),
|
|
52
|
+
consentState: Schema.Literal('not_required', 'may_be_required'),
|
|
53
|
+
policyState: Schema.Literal('unverified', 'blocked'),
|
|
54
|
+
policyCodes: Schema.Array(Schema.Literal('managed_policy_blocked', 'target_read_only')).pipe(Schema.maxItems(1)),
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
export class StatusResult extends Schema.Class<StatusResult>('StatusResult')({
|
|
58
|
+
schemaVersion: Schema.Literal(1),
|
|
59
|
+
status: Schema.Literal('ok'),
|
|
60
|
+
harness: InitHarness,
|
|
61
|
+
scope: InitScope,
|
|
62
|
+
cli: Schema.Struct({ version: BoundedVersion }),
|
|
63
|
+
auth: StatusAuth,
|
|
64
|
+
pendingAuthorization: PendingAuthorization,
|
|
65
|
+
installation: StatusInstallation,
|
|
66
|
+
}) {}
|
|
67
|
+
|
|
68
|
+
export const StatusServiceErrorCode = Schema.Literal(
|
|
69
|
+
'credential_config_malformed',
|
|
70
|
+
'credential_storage_insecure',
|
|
71
|
+
'credential_health_failed',
|
|
72
|
+
'pending_state_malformed',
|
|
73
|
+
'pending_state_unavailable',
|
|
74
|
+
'harness_inspection_failed',
|
|
75
|
+
'invalid_status_result',
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
export class StatusServiceError extends Schema.TaggedError<StatusServiceError>()('StatusServiceError', {
|
|
79
|
+
code: StatusServiceErrorCode,
|
|
80
|
+
recoverable: Schema.Boolean,
|
|
81
|
+
}) {
|
|
82
|
+
get message() {
|
|
83
|
+
switch (this.code) {
|
|
84
|
+
case 'credential_config_malformed':
|
|
85
|
+
return 'Stored credential configuration is malformed.'
|
|
86
|
+
case 'credential_storage_insecure':
|
|
87
|
+
return 'Credential storage is not secure enough to inspect.'
|
|
88
|
+
case 'credential_health_failed':
|
|
89
|
+
return 'Stored credentials could not be inspected safely.'
|
|
90
|
+
case 'pending_state_malformed':
|
|
91
|
+
return 'Pending authorization state is malformed.'
|
|
92
|
+
case 'pending_state_unavailable':
|
|
93
|
+
return 'Pending authorization state could not be inspected safely.'
|
|
94
|
+
case 'harness_inspection_failed':
|
|
95
|
+
return 'The harness configuration could not be inspected safely.'
|
|
96
|
+
case 'invalid_status_result':
|
|
97
|
+
return 'Local diagnostics returned an invalid result.'
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function statusError(code: typeof StatusServiceErrorCode.Type, recoverable: boolean): StatusServiceError {
|
|
103
|
+
return new StatusServiceError({ code, recoverable })
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function mapCredentialError(error: unknown): StatusServiceError {
|
|
107
|
+
if (error instanceof ConfigParseError) return statusError('credential_config_malformed', false)
|
|
108
|
+
if (error instanceof CredentialStoreInsecure) return statusError('credential_storage_insecure', false)
|
|
109
|
+
return statusError('credential_health_failed', false)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function mapPendingError(error: unknown): StatusServiceError {
|
|
113
|
+
if (error instanceof CredentialStoreInsecure) return statusError('credential_storage_insecure', false)
|
|
114
|
+
return error instanceof PendingInitCorrupt
|
|
115
|
+
? statusError('pending_state_malformed', true)
|
|
116
|
+
: statusError('pending_state_unavailable', true)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function authStatus(configManager: ConfigManager) {
|
|
120
|
+
return Effect.gen(function* () {
|
|
121
|
+
const loaded = yield* Effect.either(configManager.loadConfig())
|
|
122
|
+
if (loaded._tag === 'Left') {
|
|
123
|
+
if (loaded.left instanceof NotAuthenticated) return { state: 'absent' as const, healthy: false }
|
|
124
|
+
return yield* mapCredentialError(loaded.left)
|
|
125
|
+
}
|
|
126
|
+
const now = yield* DateTime.now
|
|
127
|
+
const state = authMethodState(loaded.right.authMethod, now)
|
|
128
|
+
return { state, healthy: state === 'healthy' }
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function pendingStatus(record: Option.Option<PendingInitRecord>) {
|
|
133
|
+
return Effect.gen(function* () {
|
|
134
|
+
if (Option.isNone(record)) return { state: 'none' as const, count: 0 as const }
|
|
135
|
+
const now = DateTime.toEpochMillis(yield* DateTime.now)
|
|
136
|
+
if (DateTime.toEpochMillis(record.value.approvalExpiresAt) <= now) {
|
|
137
|
+
return { state: 'none' as const, count: 0 as const }
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
state: 'approval_pending' as const,
|
|
141
|
+
count: 1 as const,
|
|
142
|
+
approvalExpiresAt: DateTime.formatIso(record.value.approvalExpiresAt),
|
|
143
|
+
}
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function validateInspection(
|
|
148
|
+
harness: typeof InitHarness.Type,
|
|
149
|
+
inspection: HarnessInspection,
|
|
150
|
+
): Effect.Effect<HarnessInspection, StatusServiceError> {
|
|
151
|
+
const componentSet = new Set(inspection.components)
|
|
152
|
+
// Only the harnesses with a web-fetch hook install three components; cursor
|
|
153
|
+
// and codex are complete with two. Demanding three of them reported a
|
|
154
|
+
// correctly configured machine as an invalid result.
|
|
155
|
+
const expectsHook = harness === 'opencode'
|
|
156
|
+
const allComponents =
|
|
157
|
+
componentSet.has('runtime-skill') &&
|
|
158
|
+
componentSet.has('mcp') &&
|
|
159
|
+
(expectsHook ? componentSet.has('webfetch-hook') && componentSet.size === 3 : componentSet.size === 2)
|
|
160
|
+
const versionValid =
|
|
161
|
+
inspection.integrationVersion === undefined ||
|
|
162
|
+
(inspection.integrationVersion.length <= 64 && VERSION_PATTERN.test(inspection.integrationVersion))
|
|
163
|
+
const configuredValid = inspection.configured === allComponents
|
|
164
|
+
const runtimeValid = inspection.runtimeState !== 'observed'
|
|
165
|
+
return componentSet.size === inspection.components.length && versionValid && configuredValid && runtimeValid
|
|
166
|
+
? Effect.succeed(inspection)
|
|
167
|
+
: statusError('invalid_status_result', false)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function installationStatus(inspection: HarnessInspection, scope: typeof InitScope.Type) {
|
|
171
|
+
const components = new Set(inspection.components)
|
|
172
|
+
const policyBlocked = inspection.policyBlock !== undefined || inspection.runtimeState === 'blocked'
|
|
173
|
+
const policyCodes =
|
|
174
|
+
inspection.policyBlock === CLAUDE_POLICY_READ_ONLY
|
|
175
|
+
? (['target_read_only'] as const)
|
|
176
|
+
: policyBlocked
|
|
177
|
+
? (['managed_policy_blocked'] as const)
|
|
178
|
+
: []
|
|
179
|
+
const reloadRequired = !policyBlocked && inspection.runtimeState === 'reload_required'
|
|
180
|
+
return {
|
|
181
|
+
configured: inspection.configured,
|
|
182
|
+
...(inspection.integrationVersion === undefined ? {} : { integrationVersion: inspection.integrationVersion }),
|
|
183
|
+
components: {
|
|
184
|
+
webfetchHook: components.has('webfetch-hook'),
|
|
185
|
+
runtimeSkill: components.has('runtime-skill'),
|
|
186
|
+
mcp: components.has('mcp'),
|
|
187
|
+
},
|
|
188
|
+
runtimeState: policyBlocked
|
|
189
|
+
? ('blocked' as const)
|
|
190
|
+
: reloadRequired
|
|
191
|
+
? ('reload_required' as const)
|
|
192
|
+
: ('unverified' as const),
|
|
193
|
+
reloadState: reloadRequired ? ('required' as const) : ('not_required' as const),
|
|
194
|
+
consentState: scope === 'project' ? ('may_be_required' as const) : ('not_required' as const),
|
|
195
|
+
policyState: policyBlocked ? ('blocked' as const) : ('unverified' as const),
|
|
196
|
+
policyCodes,
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export class StatusService extends Effect.Service<StatusService>()('@expandai/cli/Init/StatusService', {
|
|
201
|
+
dependencies: [ConfigManager.Default, PendingInitStore.Default, HarnessRegistry.Default],
|
|
202
|
+
effect: Effect.gen(function* () {
|
|
203
|
+
const configManager = yield* ConfigManager
|
|
204
|
+
const pendingStore = yield* PendingInitStore
|
|
205
|
+
const registry = yield* HarnessRegistry
|
|
206
|
+
|
|
207
|
+
const run = Effect.fn('StatusService.run')(function* (request: StatusRequest) {
|
|
208
|
+
yield* Effect.annotateCurrentSpan({ harness: request.harness, scope: request.scope })
|
|
209
|
+
return yield* Effect.gen(function* () {
|
|
210
|
+
yield* configManager
|
|
211
|
+
.inspectSecureStorage()
|
|
212
|
+
.pipe(Effect.withTracerEnabled(false), Effect.mapError(mapCredentialError))
|
|
213
|
+
const auth = yield* authStatus(configManager).pipe(Effect.withTracerEnabled(false))
|
|
214
|
+
const apiUrl = yield* configManager
|
|
215
|
+
.getApiUrl()
|
|
216
|
+
.pipe(Effect.withTracerEnabled(false), Effect.mapError(mapCredentialError))
|
|
217
|
+
const canonicalScopeRoot = yield* pendingStore
|
|
218
|
+
.canonicalScopeRoot(request.scope)
|
|
219
|
+
.pipe(Effect.withTracerEnabled(false), Effect.mapError(mapPendingError))
|
|
220
|
+
const key: PendingInitKey = {
|
|
221
|
+
apiOrigin: apiUrl.origin,
|
|
222
|
+
clientId: CLI_AUTHORIZATION_CLIENT_ID,
|
|
223
|
+
harness: 'agent-harnesses',
|
|
224
|
+
scope: request.scope,
|
|
225
|
+
canonicalScopeRoot,
|
|
226
|
+
}
|
|
227
|
+
const pending = yield* pendingStore
|
|
228
|
+
.peek(key)
|
|
229
|
+
.pipe(Effect.withTracerEnabled(false), Effect.mapError(mapPendingError))
|
|
230
|
+
const inspection = yield* registry.inspect(request.harness, request.scope).pipe(
|
|
231
|
+
Effect.withTracerEnabled(false),
|
|
232
|
+
Effect.mapError(() => statusError('harness_inspection_failed', true)),
|
|
233
|
+
Effect.flatMap((inspection) => validateInspection(request.harness, inspection)),
|
|
234
|
+
)
|
|
235
|
+
const candidate = {
|
|
236
|
+
schemaVersion: 1 as const,
|
|
237
|
+
status: 'ok' as const,
|
|
238
|
+
harness: request.harness,
|
|
239
|
+
scope: request.scope,
|
|
240
|
+
cli: { version: CLI_VERSION },
|
|
241
|
+
auth,
|
|
242
|
+
pendingAuthorization: yield* pendingStatus(pending),
|
|
243
|
+
installation: installationStatus(inspection, request.scope),
|
|
244
|
+
}
|
|
245
|
+
return yield* Schema.decodeUnknown(StatusResult)(candidate).pipe(
|
|
246
|
+
Effect.mapError(() => statusError('invalid_status_result', false)),
|
|
247
|
+
)
|
|
248
|
+
})
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
return { run } as const
|
|
252
|
+
}),
|
|
253
|
+
}) {}
|
|
@@ -1,53 +1,100 @@
|
|
|
1
1
|
import { homedir } from 'node:os'
|
|
2
|
-
import {
|
|
3
|
-
import { ConfigIo } from '../Domain.js'
|
|
4
|
-
import type {
|
|
2
|
+
import { Config, Console, Effect, Match, Schema } from 'effect'
|
|
3
|
+
import { type CliCommand, ConfigIo, cliShellCommand, DIRECT_CLI } from '../Domain.js'
|
|
4
|
+
import type { InstallScope } from '../Mcp.js'
|
|
5
|
+
|
|
6
|
+
/** Claude Code WebFetch hook invocation persisted in harness settings. */
|
|
7
|
+
export const HOOK_INVOCATION = 'expandai hook claude-code'
|
|
8
|
+
export const HOOK_COMMAND = HOOK_INVOCATION
|
|
9
|
+
const HOOK_ARGS = ['hook', 'claude-code'] as const
|
|
5
10
|
|
|
6
11
|
/**
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
12
|
+
* The hook line for a resolved CLI. Defaults to the bare `expandai` command;
|
|
13
|
+
* when the CLI is not on PATH — an `npx expandai init` with no global install —
|
|
14
|
+
* `CliInvocation` resolves to the npx form and the written hook stays runnable.
|
|
10
15
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
16
|
+
export function hookCommandFor(cli: CliCommand = DIRECT_CLI): string {
|
|
17
|
+
return cliShellCommand(cli, HOOK_ARGS)
|
|
18
|
+
}
|
|
13
19
|
export const WEBFETCH_MATCHER = 'WebFetch'
|
|
14
20
|
|
|
15
|
-
export const HookCommandEntry = Schema.Struct(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
21
|
+
export const HookCommandEntry = Schema.Struct(
|
|
22
|
+
{
|
|
23
|
+
type: Schema.Literal('command'),
|
|
24
|
+
command: Schema.String,
|
|
25
|
+
expandaiProfileVersion: Schema.optional(Schema.String),
|
|
26
|
+
/** Whether this install added the `WebFetch` deny rule — see `makeExpandaiMatcher`. */
|
|
27
|
+
expandaiManagedDeny: Schema.optional(Schema.Boolean),
|
|
28
|
+
},
|
|
29
|
+
Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
30
|
+
)
|
|
19
31
|
|
|
20
|
-
export const HookMatcher = Schema.Struct(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
32
|
+
export const HookMatcher = Schema.Struct(
|
|
33
|
+
{
|
|
34
|
+
matcher: Schema.optional(Schema.String),
|
|
35
|
+
hooks: Schema.Array(HookCommandEntry),
|
|
36
|
+
},
|
|
37
|
+
Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
38
|
+
)
|
|
24
39
|
|
|
25
40
|
const HooksSection = Schema.Struct(
|
|
26
41
|
{ PreToolUse: Schema.optional(Schema.Array(HookMatcher)) },
|
|
27
42
|
Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
28
43
|
)
|
|
29
44
|
|
|
45
|
+
const PermissionsSection = Schema.Struct(
|
|
46
|
+
{ deny: Schema.optional(Schema.Array(Schema.String)) },
|
|
47
|
+
Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
48
|
+
)
|
|
49
|
+
|
|
30
50
|
export const SettingsSchema = Schema.parseJson(
|
|
31
51
|
Schema.Struct(
|
|
32
|
-
{
|
|
52
|
+
{
|
|
53
|
+
hooks: Schema.optionalWith(HooksSection, { default: () => ({}) as typeof HooksSection.Type }),
|
|
54
|
+
permissions: Schema.optional(PermissionsSection),
|
|
55
|
+
},
|
|
33
56
|
Schema.Record({ key: Schema.String, value: Schema.Unknown }),
|
|
34
57
|
),
|
|
35
58
|
{ space: 2 },
|
|
36
59
|
)
|
|
37
60
|
|
|
38
|
-
export
|
|
39
|
-
|
|
40
|
-
|
|
61
|
+
export interface ClaudeHookPlan {
|
|
62
|
+
readonly changed: boolean
|
|
63
|
+
readonly settings: typeof SettingsSchema.Type
|
|
41
64
|
}
|
|
42
65
|
|
|
66
|
+
export function isExpandaiHookCommand(command: string): boolean {
|
|
67
|
+
const value = command.trim()
|
|
68
|
+
const invocation = String.raw`expandai(?:\.exe)?\s+hook\s+(?:claude-code|claude)`
|
|
69
|
+
const direct = new RegExp(String.raw`^(?:npx\s+-y\s+)?${invocation}$`, 'u')
|
|
70
|
+
const absolute = new RegExp(
|
|
71
|
+
String.raw`^(?:"(?:[A-Za-z]:\\|/)[^"]*[\\/]expandai(?:\.exe)?"|'(?:[A-Za-z]:\\|/)[^']*[\\/]expandai(?:\.exe)?'|(?:[A-Za-z]:\\|/)[^\s]*[\\/]expandai(?:\.exe)?)\s+hook\s+(?:claude-code|claude)$`,
|
|
72
|
+
'u',
|
|
73
|
+
)
|
|
74
|
+
const shellFallback =
|
|
75
|
+
/^if\s+command\s+-v\s+expandai\s+>\/dev\/null\s+2>&1;\s*then\s+expandai\s+hook\s+(?:claude-code|claude);\s*else\s+npx\s+-y\s+expandai\s+hook\s+(?:claude-code|claude);\s*fi$/u
|
|
76
|
+
return direct.test(value) || absolute.test(value) || shellFallback.test(value)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Nothing here writes to `settings.json`, and nothing here removes from it.
|
|
81
|
+
*
|
|
82
|
+
* A `WebFetch` deny entry is a bare string with nowhere to record who wrote it,
|
|
83
|
+
* and our hook is not proof either: `hook install` used to write the hook on its
|
|
84
|
+
* own, so a settings file carrying our hook and a `WebFetch` deny may well hold a
|
|
85
|
+
* rule the user added afterwards. Removing it would revoke a restriction its
|
|
86
|
+
* owner chose. Since Claude Code no longer gets a hook or a deny rule from us,
|
|
87
|
+
* there is nothing of ours left to clean up, and guessing is the only thing a
|
|
88
|
+
* cleanup pass could do.
|
|
89
|
+
*/
|
|
90
|
+
|
|
43
91
|
export class ClaudeHook extends Effect.Service<ClaudeHook>()('@expandai/cli/Install/Claude/Hook', {
|
|
44
92
|
dependencies: [ConfigIo.Default],
|
|
45
93
|
effect: Effect.gen(function* () {
|
|
46
|
-
const io = yield* ConfigIo
|
|
47
94
|
const home = yield* Config.string('HOME').pipe(Config.withDefault(homedir()))
|
|
48
95
|
const cwd = yield* Config.string('PWD').pipe(Config.withDefault(process.cwd()))
|
|
49
96
|
|
|
50
|
-
const pathFor = Effect.fn(function* (scope:
|
|
97
|
+
const pathFor = Effect.fn(function* (scope: InstallScope) {
|
|
51
98
|
return yield* Match.value(scope).pipe(
|
|
52
99
|
Match.when('global', () => Effect.succeed(`${home}/.claude/settings.json`)),
|
|
53
100
|
Match.when('project', () => Effect.succeed(`${cwd}/.claude/settings.json`)),
|
|
@@ -55,79 +102,22 @@ export class ClaudeHook extends Effect.Service<ClaudeHook>()('@expandai/cli/Inst
|
|
|
55
102
|
)
|
|
56
103
|
})
|
|
57
104
|
|
|
58
|
-
const
|
|
59
|
-
|
|
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
|
-
)
|
|
105
|
+
const NOTICE =
|
|
106
|
+
'expandai does not install a WebFetch hook for Claude Code — the MCP server and runtime skill replace it, and neither can disable a built-in tool.'
|
|
66
107
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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}`)
|
|
108
|
+
/**
|
|
109
|
+
* Kept as a no-op rather than deleted so `hook install|uninstall claude-code`
|
|
110
|
+
* still resolves. Both say what the profile is instead of touching
|
|
111
|
+
* `settings.json`, which expandai no longer writes for Claude Code.
|
|
112
|
+
*/
|
|
113
|
+
const install = Effect.fn('ClaudeHook.install')(function* (_scope: InstallScope) {
|
|
114
|
+
yield* Console.log(NOTICE)
|
|
84
115
|
})
|
|
85
116
|
|
|
86
|
-
const uninstall = Effect.fn('ClaudeHook.uninstall')(function* (
|
|
87
|
-
|
|
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}`)
|
|
117
|
+
const uninstall = Effect.fn('ClaudeHook.uninstall')(function* (_scope: InstallScope) {
|
|
118
|
+
yield* Console.log(NOTICE)
|
|
129
119
|
})
|
|
130
120
|
|
|
131
|
-
return { install, uninstall } as const
|
|
121
|
+
return { install, uninstall, pathFor } as const
|
|
132
122
|
}),
|
|
133
123
|
}) {}
|
|
@@ -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
|