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
package/src/Mcp/tools/Fetch.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { Tool } from '@effect/ai'
|
|
|
2
2
|
import { Effect, Schema, String } from 'effect'
|
|
3
3
|
import { ApiClient } from '../../ApiClient.js'
|
|
4
4
|
import { ConfigManager } from '../../Config.js'
|
|
5
|
+
import { Telemetry } from '../../Telemetry.js'
|
|
5
6
|
import {
|
|
6
7
|
CitedObjectModeResult,
|
|
7
8
|
CitedSnapshotSearchResult,
|
|
@@ -69,11 +70,15 @@ export const FetchTool = Tool.make('fetch', {
|
|
|
69
70
|
description: String.stripMargin(`
|
|
70
71
|
|Fetch and extract content from any URL.
|
|
71
72
|
|
|
|
73
|
+
|You MUST cite what you take from the result. The result carries a ready-made
|
|
74
|
+
|'citationBlock' — append it verbatim to the end of your answer. Do not retype it, do not
|
|
75
|
+
|substitute the URL you fetched, and do not build a link yourself: the citation points at
|
|
76
|
+
|expand.ai's stored capture, not at the original site. Drop only the markers you did not use.
|
|
77
|
+
|A 'Verify it' link without '?id=' is not a citation. Never invent or reuse an evidence id.
|
|
78
|
+
|
|
|
72
79
|
|Returns object-mode output with markdown, front-matter metadata, and optional extracted JSON.
|
|
73
80
|
|When 'search' is set, the response also carries ranked snippets under data.search.snippets.
|
|
74
|
-
|
|
|
75
|
-
|for State JSON sources, the extracted 'json' value. Return the citationUrl to the user so they
|
|
76
|
-
|can verify the source.`),
|
|
81
|
+
|For State JSON sources each snippet also carries the extracted 'json' value.`),
|
|
77
82
|
parameters: FetchParams.fields,
|
|
78
83
|
success: CitedObjectModeResult,
|
|
79
84
|
failure: Schema.Defect,
|
|
@@ -86,10 +91,15 @@ export const FetchSearchTool = Tool.make('fetch_search', {
|
|
|
86
91
|
description: String.stripMargin(`
|
|
87
92
|
|Search a previous fetch by snapshotId without starting a new capture.
|
|
88
93
|
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
|
94
|
+
|You MUST cite what you take from the result. The result carries a ready-made
|
|
95
|
+
|'citationBlock' — append it verbatim to the end of your answer. Do not retype it, do not
|
|
96
|
+
|substitute the URL you fetched, and do not build a link yourself: the citation points at
|
|
97
|
+
|expand.ai's stored capture, not at the original site. Drop only the markers you did not use.
|
|
98
|
+
|A 'Verify it' link without '?id=' is not a citation. Never invent or reuse an evidence id.
|
|
99
|
+
|
|
|
100
|
+
|Returns ranked snippets from stored markdown, appendix, or State JSON artifacts. For State JSON
|
|
101
|
+
|sources each snippet also carries the extracted 'json' value. The result also includes a
|
|
102
|
+
|whole-snapshot 'playground' link.`),
|
|
93
103
|
parameters: FetchSnapshotSearchParams.fields,
|
|
94
104
|
success: CitedSnapshotSearchResult,
|
|
95
105
|
failure: Schema.Defect,
|
|
@@ -104,10 +114,11 @@ export interface FetchService {
|
|
|
104
114
|
}
|
|
105
115
|
|
|
106
116
|
export class Fetch extends Effect.Service<Fetch>()('@expandai/cli/Mcp/Fetch', {
|
|
107
|
-
dependencies: [ApiClient.Default, ConfigManager.Default],
|
|
117
|
+
dependencies: [ApiClient.Default, ConfigManager.Default, Telemetry.Default],
|
|
108
118
|
scoped: Effect.gen(function* () {
|
|
109
119
|
const client = yield* ApiClient
|
|
110
120
|
const configManager = yield* ConfigManager
|
|
121
|
+
const telemetry = yield* Telemetry
|
|
111
122
|
const playgroundHost = yield* configManager.getPlaygroundHost()
|
|
112
123
|
|
|
113
124
|
const fetch: FetchService['fetch'] = Effect.fn('Fetch.fetch')(function* ({
|
|
@@ -117,9 +128,16 @@ export class Fetch extends Effect.Service<Fetch>()('@expandai/cli/Mcp/Fetch', {
|
|
|
117
128
|
includeAppendix,
|
|
118
129
|
includeJson,
|
|
119
130
|
}: FetchParams) {
|
|
120
|
-
yield* Effect.logDebug(
|
|
121
|
-
Effect.annotateLogs({
|
|
131
|
+
yield* Effect.logDebug('MCP fetch started').pipe(
|
|
132
|
+
Effect.annotateLogs({
|
|
133
|
+
operation: 'fetch',
|
|
134
|
+
hasSearch: search !== undefined,
|
|
135
|
+
includeMeta,
|
|
136
|
+
includeAppendix,
|
|
137
|
+
includeJson,
|
|
138
|
+
}),
|
|
122
139
|
)
|
|
140
|
+
yield* telemetry.capture('mcp_tool_called', { tool: 'fetch', has_search: search !== undefined })
|
|
123
141
|
|
|
124
142
|
const includeParam =
|
|
125
143
|
includeAppendix && includeJson
|
|
@@ -143,7 +161,14 @@ export class Fetch extends Effect.Service<Fetch>()('@expandai/cli/Mcp/Fetch', {
|
|
|
143
161
|
},
|
|
144
162
|
})
|
|
145
163
|
|
|
146
|
-
yield* Effect.logDebug(
|
|
164
|
+
yield* Effect.logDebug('MCP fetch completed').pipe(
|
|
165
|
+
Effect.annotateLogs({
|
|
166
|
+
operation: 'fetch',
|
|
167
|
+
hasSearch: search !== undefined,
|
|
168
|
+
searchResultCount: result.data?.search?.snippets.length ?? 0,
|
|
169
|
+
jsonEvidenceCount: result.json.length,
|
|
170
|
+
}),
|
|
171
|
+
)
|
|
147
172
|
|
|
148
173
|
return enrichObjectModeResult(result, playgroundHost)
|
|
149
174
|
})
|
|
@@ -155,9 +180,10 @@ export class Fetch extends Effect.Service<Fetch>()('@expandai/cli/Mcp/Fetch', {
|
|
|
155
180
|
includeAppendix,
|
|
156
181
|
includeJson,
|
|
157
182
|
}: FetchSnapshotSearchParams) {
|
|
158
|
-
yield* Effect.logDebug(
|
|
159
|
-
Effect.annotateLogs({
|
|
183
|
+
yield* Effect.logDebug('MCP snapshot search started').pipe(
|
|
184
|
+
Effect.annotateLogs({ operation: 'fetch_search', includeMarkdown, includeAppendix, includeJson }),
|
|
160
185
|
)
|
|
186
|
+
yield* telemetry.capture('mcp_tool_called', { tool: 'fetch_search', has_search: true })
|
|
161
187
|
|
|
162
188
|
const result = yield* client.fetchFetchSearch({
|
|
163
189
|
snapshotId,
|
|
@@ -169,7 +195,7 @@ export class Fetch extends Effect.Service<Fetch>()('@expandai/cli/Mcp/Fetch', {
|
|
|
169
195
|
},
|
|
170
196
|
})
|
|
171
197
|
|
|
172
|
-
return enrichSnapshotSearchResult(result, playgroundHost)
|
|
198
|
+
return enrichSnapshotSearchResult(result, playgroundHost, 'mcp-fetch-search')
|
|
173
199
|
})
|
|
174
200
|
|
|
175
201
|
return { fetch, fetchSearch } satisfies FetchService
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { FileSystem, Error as PlatformError } from '@effect/platform'
|
|
2
|
+
import { Effect, Exit, Option } from 'effect'
|
|
3
|
+
|
|
4
|
+
export interface CreatedFileIdentity {
|
|
5
|
+
readonly dev: number
|
|
6
|
+
readonly ino?: number
|
|
7
|
+
readonly mode: number
|
|
8
|
+
readonly type: FileSystem.File.Type
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ExclusiveFileCreation {
|
|
12
|
+
readonly identity: CreatedFileIdentity
|
|
13
|
+
readonly writeExit: Exit.Exit<void, PlatformError.PlatformError>
|
|
14
|
+
readonly pathMatchesHandle: boolean
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Creates a file exclusively and binds its identity to the opened handle before writing bytes. */
|
|
18
|
+
export function createExclusiveFile(
|
|
19
|
+
fileSystem: FileSystem.FileSystem,
|
|
20
|
+
targetPath: string,
|
|
21
|
+
contents: string,
|
|
22
|
+
mode: number,
|
|
23
|
+
): Effect.Effect<ExclusiveFileCreation, PlatformError.PlatformError> {
|
|
24
|
+
return Effect.scoped(
|
|
25
|
+
Effect.gen(function* () {
|
|
26
|
+
const file = yield* fileSystem.open(targetPath, { flag: 'wx', mode })
|
|
27
|
+
const info = yield* file.stat
|
|
28
|
+
const identity: CreatedFileIdentity = {
|
|
29
|
+
dev: info.dev,
|
|
30
|
+
ino: Option.getOrUndefined(info.ino),
|
|
31
|
+
mode: info.mode,
|
|
32
|
+
type: info.type,
|
|
33
|
+
}
|
|
34
|
+
const writeExit = yield* Effect.exit(
|
|
35
|
+
file.writeAll(new TextEncoder().encode(contents)).pipe(Effect.zipRight(file.sync)),
|
|
36
|
+
)
|
|
37
|
+
if (Exit.isFailure(writeExit)) return { identity, writeExit, pathMatchesHandle: false } as const
|
|
38
|
+
|
|
39
|
+
// Validate while the creating handle is still open. In particular, Windows
|
|
40
|
+
// may reuse a deleted file's numeric identifier immediately after its final
|
|
41
|
+
// handle closes, making a later pathname-only comparison ambiguous.
|
|
42
|
+
const pathInfo = yield* fileSystem.stat(targetPath)
|
|
43
|
+
const pathMatchesHandle = matchesCreatedFile(identity, {
|
|
44
|
+
exists: true,
|
|
45
|
+
dev: pathInfo.dev,
|
|
46
|
+
ino: Option.getOrUndefined(pathInfo.ino),
|
|
47
|
+
type: pathInfo.type,
|
|
48
|
+
})
|
|
49
|
+
return { identity, writeExit, pathMatchesHandle } as const
|
|
50
|
+
}),
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Compares a pathname snapshot with identity captured from the creating file handle. */
|
|
55
|
+
export function matchesCreatedFile(
|
|
56
|
+
identity: CreatedFileIdentity,
|
|
57
|
+
pathIdentity: {
|
|
58
|
+
readonly exists: boolean
|
|
59
|
+
readonly dev?: number
|
|
60
|
+
readonly ino?: number
|
|
61
|
+
readonly type?: FileSystem.File.Type
|
|
62
|
+
},
|
|
63
|
+
): boolean {
|
|
64
|
+
return (
|
|
65
|
+
pathIdentity.exists &&
|
|
66
|
+
pathIdentity.dev === identity.dev &&
|
|
67
|
+
pathIdentity.ino === identity.ino &&
|
|
68
|
+
pathIdentity.type === identity.type
|
|
69
|
+
)
|
|
70
|
+
}
|
package/src/Snippets.ts
CHANGED
|
@@ -1,19 +1,26 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { FetchObjectModeResultWithJson } from '@expandai/sdk/FetchJson'
|
|
2
|
+
import { citationUrl } from '@expandai/sdk/Playground'
|
|
2
3
|
import { Schema } from 'effect'
|
|
3
4
|
|
|
4
5
|
const encodeJson = Schema.encodeSync(Schema.parseJson(Schema.Unknown, { space: 2 }))
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
readonly stateJsonSourceId?: number | null
|
|
9
|
-
}
|
|
7
|
+
type SearchResultWithJson = NonNullable<NonNullable<FetchObjectModeResultWithJson['data']>['search']>
|
|
8
|
+
type RenderableSnippet = SearchResultWithJson['snippets'][number]
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Attaches the public citation link to each snippet. JSON consumers get the same
|
|
12
|
+
* auditable URL the text renderer prints in its header — without it, a caller has
|
|
13
|
+
* the snapshot and the evidence id but no host to build a link against.
|
|
14
|
+
*/
|
|
15
|
+
export function withCitationUrls<A extends RenderableSnippet>(
|
|
16
|
+
snapshotId: string,
|
|
17
|
+
host: string,
|
|
18
|
+
snippets: ReadonlyArray<A>,
|
|
19
|
+
): Array<A & { readonly citationUrl: string }> {
|
|
20
|
+
return snippets.map((snippet) => ({
|
|
21
|
+
...snippet,
|
|
22
|
+
citationUrl: citationUrl(snapshotId, snippet.location, host),
|
|
23
|
+
}))
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
/**
|
|
@@ -23,7 +30,7 @@ interface RenderableSnippet {
|
|
|
23
30
|
export function renderSnippets(snapshotId: string, host: string, snippets: ReadonlyArray<RenderableSnippet>): string {
|
|
24
31
|
return snippets
|
|
25
32
|
.map((snippet) => {
|
|
26
|
-
const url = citationUrl(snapshotId, snippet.
|
|
33
|
+
const url = citationUrl(snapshotId, snippet.location, host)
|
|
27
34
|
const header = `[${snippet.source} · score ${snippet.score.toFixed(2)}] ${url}`
|
|
28
35
|
const json = snippet.json == null ? '' : `\n\n${encodeJson(snippet.json)}`
|
|
29
36
|
return `${header}\n\n${snippet.text}${json}`
|
package/src/Telemetry.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anonymous usage telemetry (PostHog). Events carry a persisted machine UUID,
|
|
3
|
+
* the invoking harness, and coarse feature usage — never URLs, queries,
|
|
4
|
+
* keys, or tokens.
|
|
5
|
+
*
|
|
6
|
+
* Delivery is fire-and-forget: each capture forks into a FiberSet and `flush`
|
|
7
|
+
* caps shutdown at one second, so telemetry can never block a command, delay
|
|
8
|
+
* an MCP/hook payload, or write to stdout (stdout is a protocol surface).
|
|
9
|
+
*
|
|
10
|
+
* Settings live in the config file (`telemetry.enabled` / `distinctId` /
|
|
11
|
+
* `noticeShown`). `DO_NOT_TRACK` / `EXPANDAI_NO_TELEMETRY`, or a non-default
|
|
12
|
+
* effective API URL (env or config file — dev traffic), disable capture per
|
|
13
|
+
* invocation without touching the file. A config file that fails to parse
|
|
14
|
+
* also disables telemetry for the invocation and is never written to.
|
|
15
|
+
*/
|
|
16
|
+
import { randomUUID } from 'node:crypto'
|
|
17
|
+
import { HttpClient, HttpClientRequest } from '@effect/platform'
|
|
18
|
+
import { NodeHttpClient } from '@effect/platform-node'
|
|
19
|
+
import { Config, Console, Effect, FiberSet, Layer, Option } from 'effect'
|
|
20
|
+
import { ConfigManager, DEFAULT_API_URL, envFlag, envLiteral } from './Config.js'
|
|
21
|
+
import { Harness } from './Install/Domain.js'
|
|
22
|
+
import { CLI_VERSION } from './Version.js'
|
|
23
|
+
|
|
24
|
+
// Every field reads leniently (envFlag / envLiteral / plain strings) so a
|
|
25
|
+
// malformed env var can never fail service construction — telemetry must
|
|
26
|
+
// silently degrade, not crash the CLI.
|
|
27
|
+
export const TelemetryEnvironment = Config.all({
|
|
28
|
+
// PostHog project write-only key — safe to embed as the default (it ships
|
|
29
|
+
// in every browser bundle that uses posthog-js) and only permits ingestion.
|
|
30
|
+
apiKey: Config.string('POSTHOG_API_KEY').pipe(Config.withDefault('phc_sffcV4skT33mMKmo0BenNoE7t7AghUrOugyW9qSx43B')),
|
|
31
|
+
host: Config.string('POSTHOG_HOST').pipe(Config.withDefault('https://us.i.posthog.com')),
|
|
32
|
+
doNotTrack: envFlag('DO_NOT_TRACK'),
|
|
33
|
+
noTelemetry: envFlag('EXPANDAI_NO_TELEMETRY'),
|
|
34
|
+
apiUrl: Config.string('EXPAND_API_URL').pipe(Config.option),
|
|
35
|
+
// Baked into MCP/hook configs at install time; wins over ambient signals.
|
|
36
|
+
// An unrecognized value (e.g. written by a newer installer) reads as None.
|
|
37
|
+
harnessOverride: envLiteral('EXPANDAI_HARNESS', Harness),
|
|
38
|
+
claudeCode: envFlag('CLAUDECODE'),
|
|
39
|
+
cursor: Config.string('CURSOR_TRACE_ID').pipe(Config.option, Config.map(Option.isSome)),
|
|
40
|
+
codex: Config.string('CODEX_RUN_ID').pipe(Config.option, Config.map(Option.isSome)),
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
type TelemetryEnvironment = Config.Config.Success<typeof TelemetryEnvironment>
|
|
44
|
+
|
|
45
|
+
function detectHarness(environment: TelemetryEnvironment): Harness {
|
|
46
|
+
if (Option.isSome(environment.harnessOverride)) return environment.harnessOverride.value
|
|
47
|
+
if (environment.claudeCode) return 'claude-code'
|
|
48
|
+
if (environment.cursor) return 'cursor'
|
|
49
|
+
if (environment.codex) return 'codex'
|
|
50
|
+
return 'unknown'
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const generateDistinctId = Effect.sync(() => randomUUID())
|
|
54
|
+
|
|
55
|
+
// Lenient normalization for the non-default-API comparison: `new URL()` adds
|
|
56
|
+
// the trailing slash a bare origin string lacks, and a value that doesn't
|
|
57
|
+
// parse compares as non-default — never crash construction, and treat unknown
|
|
58
|
+
// endpoints as non-production traffic.
|
|
59
|
+
function normalizeApiUrl(url: string): string {
|
|
60
|
+
try {
|
|
61
|
+
return new URL(url).toString()
|
|
62
|
+
} catch {
|
|
63
|
+
return `unparseable:${url}`
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export class Telemetry extends Effect.Service<Telemetry>()('@expandai/cli/Telemetry', {
|
|
68
|
+
dependencies: [ConfigManager.Default, NodeHttpClient.layer],
|
|
69
|
+
scoped: Effect.gen(function* () {
|
|
70
|
+
const configManager = yield* ConfigManager
|
|
71
|
+
const httpClient = yield* HttpClient.HttpClient
|
|
72
|
+
const fibers = yield* FiberSet.make()
|
|
73
|
+
|
|
74
|
+
const environment = yield* TelemetryEnvironment
|
|
75
|
+
// EXPAND_API_KEY overrides the config file for requests (see ConfigManager),
|
|
76
|
+
// so it must win here too or env-key users all report 'none'.
|
|
77
|
+
const envApiKey = yield* Config.string('EXPAND_API_KEY').pipe(Config.option, Config.map(Option.isSome))
|
|
78
|
+
|
|
79
|
+
// The config file is read best-effort: telemetry must work (or silently
|
|
80
|
+
// no-op) before login and survive an unreadable config. A parse failure
|
|
81
|
+
// disables capture for the invocation — an unreadable file must never be
|
|
82
|
+
// mistaken for an empty one, and never be written to.
|
|
83
|
+
const { config, parseFailed } = yield* configManager.readConfigFile()
|
|
84
|
+
const settings = config.telemetry
|
|
85
|
+
|
|
86
|
+
// Same env-over-file precedence as ConfigManager: a self-hosted apiUrl in
|
|
87
|
+
// the config file is dev traffic too. Deliberately not Config.url /
|
|
88
|
+
// getApiUrl — those fail on a malformed env value, and telemetry must
|
|
89
|
+
// degrade instead of crashing the CLI.
|
|
90
|
+
const effectiveApiUrl = Option.firstSomeOf([
|
|
91
|
+
environment.apiUrl,
|
|
92
|
+
Option.map(Option.fromNullable(config.apiUrl), (url) => url.toString()),
|
|
93
|
+
])
|
|
94
|
+
const nonDefaultApi = Option.match(effectiveApiUrl, {
|
|
95
|
+
onNone: () => false,
|
|
96
|
+
onSome: (url) => normalizeApiUrl(url) !== normalizeApiUrl(DEFAULT_API_URL),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const enabled =
|
|
100
|
+
!environment.doNotTrack &&
|
|
101
|
+
!environment.noTelemetry &&
|
|
102
|
+
!nonDefaultApi &&
|
|
103
|
+
!parseFailed &&
|
|
104
|
+
settings?.enabled !== false
|
|
105
|
+
|
|
106
|
+
const harness = detectHarness(environment)
|
|
107
|
+
const authMethod = envApiKey
|
|
108
|
+
? 'api-key'
|
|
109
|
+
: config.authMethod?._tag === 'ApiKey'
|
|
110
|
+
? 'api-key'
|
|
111
|
+
: config.authMethod?._tag === 'OAuth'
|
|
112
|
+
? 'oauth'
|
|
113
|
+
: 'none'
|
|
114
|
+
|
|
115
|
+
const persistNewDistinctId = Effect.fnUntraced(function* () {
|
|
116
|
+
const generated = yield* generateDistinctId
|
|
117
|
+
// Best-effort persistence — an unwritable config yields a per-run id.
|
|
118
|
+
yield* configManager.updateConfig({ telemetry: { ...settings, distinctId: generated } }).pipe(Effect.ignore)
|
|
119
|
+
return generated
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
// Disabled runs never write: DO_NOT_TRACK users don't get an id persisted,
|
|
123
|
+
// and after a parse failure a write would clobber the original file.
|
|
124
|
+
const distinctId = settings?.distinctId ?? (enabled ? yield* persistNewDistinctId() : yield* generateDistinctId)
|
|
125
|
+
|
|
126
|
+
const capture = Effect.fn('Telemetry.capture')(function* (
|
|
127
|
+
event: string,
|
|
128
|
+
properties: Record<string, string | number | boolean>,
|
|
129
|
+
) {
|
|
130
|
+
if (!enabled) return
|
|
131
|
+
const post = HttpClientRequest.post(`${environment.host}/i/v0/e`).pipe(
|
|
132
|
+
HttpClientRequest.bodyJson({
|
|
133
|
+
api_key: environment.apiKey,
|
|
134
|
+
event,
|
|
135
|
+
distinct_id: distinctId,
|
|
136
|
+
timestamp: new Date().toISOString(),
|
|
137
|
+
properties: {
|
|
138
|
+
$lib: 'expandai-cli',
|
|
139
|
+
$lib_version: CLI_VERSION,
|
|
140
|
+
version: CLI_VERSION,
|
|
141
|
+
harness,
|
|
142
|
+
auth_method: authMethod,
|
|
143
|
+
...properties,
|
|
144
|
+
},
|
|
145
|
+
}),
|
|
146
|
+
)
|
|
147
|
+
yield* FiberSet.run(
|
|
148
|
+
fibers,
|
|
149
|
+
post.pipe(
|
|
150
|
+
Effect.flatMap((request) => httpClient.execute(request)),
|
|
151
|
+
Effect.scoped,
|
|
152
|
+
Effect.timeout('2 seconds'),
|
|
153
|
+
Effect.ignore,
|
|
154
|
+
),
|
|
155
|
+
)
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
// Cap delivery at flush time so short-lived processes get events out
|
|
159
|
+
// without ever holding the process open noticeably.
|
|
160
|
+
const flush = FiberSet.awaitEmpty(fibers).pipe(Effect.timeout('1 second'), Effect.ignore)
|
|
161
|
+
|
|
162
|
+
const showNoticeIfNeeded = Effect.fn('Telemetry.showNoticeIfNeeded')(function* () {
|
|
163
|
+
if (!enabled || settings?.noticeShown === true) return
|
|
164
|
+
yield* Console.error(
|
|
165
|
+
'expandai collects anonymous usage data to improve the CLI. Opt out with `expandai telemetry disable` or EXPANDAI_NO_TELEMETRY=1.',
|
|
166
|
+
)
|
|
167
|
+
yield* configManager
|
|
168
|
+
.updateConfig({ telemetry: { ...settings, distinctId, noticeShown: true } })
|
|
169
|
+
.pipe(Effect.ignore)
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
return { capture, flush, showNoticeIfNeeded, enabled, distinctId } as const
|
|
173
|
+
}),
|
|
174
|
+
}) {
|
|
175
|
+
/** A side-effect-free telemetry service for commands whose contract is read-only. */
|
|
176
|
+
static readonly DISABLED = Layer.succeed(
|
|
177
|
+
Telemetry,
|
|
178
|
+
Telemetry.make({
|
|
179
|
+
capture: () => Effect.void,
|
|
180
|
+
flush: Effect.void,
|
|
181
|
+
showNoticeIfNeeded: () => Effect.void,
|
|
182
|
+
enabled: false,
|
|
183
|
+
distinctId: 'disabled',
|
|
184
|
+
}),
|
|
185
|
+
)
|
|
186
|
+
}
|
package/src/Version.ts
ADDED
package/src/Xdg.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { homedir } from 'node:os'
|
|
2
|
+
import { Path } from '@effect/platform'
|
|
3
|
+
import { Config, Effect } from 'effect'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* XDG base-directory lookup, shared by everything that persists CLI state so
|
|
7
|
+
* the fallback rules cannot drift apart. The spec treats an unset and an empty
|
|
8
|
+
* variable identically: fall back to `$HOME/.config`. Without the empty check,
|
|
9
|
+
* `XDG_CONFIG_HOME=` resolves paths relative to the working directory — which
|
|
10
|
+
* would drop credentials inside whatever project the CLI happens to run in.
|
|
11
|
+
*/
|
|
12
|
+
export const xdgConfigDirectory = Effect.gen(function* () {
|
|
13
|
+
const path = yield* Path.Path
|
|
14
|
+
return yield* Config.string('XDG_CONFIG_HOME').pipe(
|
|
15
|
+
Config.withDefault(''),
|
|
16
|
+
Config.map((directory) => (directory.trim() === '' ? path.join(homedir(), '.config') : directory)),
|
|
17
|
+
)
|
|
18
|
+
})
|