@effect-x/ultimate-search 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/dist/cli.js +1 -1
  2. package/dist/cli.js.map +1 -1
  3. package/package.json +3 -3
  4. package/src/cli.ts +21 -0
  5. package/src/commands/fetch.ts +95 -0
  6. package/src/commands/map.ts +97 -0
  7. package/src/commands/mcp/stdio.ts +27 -0
  8. package/src/commands/mcp.ts +7 -0
  9. package/src/commands/root.ts +10 -0
  10. package/src/commands/search/dual.ts +88 -0
  11. package/src/commands/search/grok.ts +70 -0
  12. package/src/commands/search/tavily.ts +99 -0
  13. package/src/commands/search.ts +9 -0
  14. package/src/config/settings.ts +261 -0
  15. package/src/providers/firecrawl/client.ts +75 -0
  16. package/src/providers/firecrawl/schema.ts +31 -0
  17. package/src/providers/grok/client.ts +77 -0
  18. package/src/providers/grok/schema.ts +107 -0
  19. package/src/providers/tavily/client.ts +143 -0
  20. package/src/providers/tavily/schema.ts +207 -0
  21. package/src/services/dual-search.ts +150 -0
  22. package/src/services/firecrawl-fetch.ts +101 -0
  23. package/src/services/grok-search.ts +68 -0
  24. package/src/services/read-only-mcp.ts +275 -0
  25. package/src/services/tavily-extract.ts +66 -0
  26. package/src/services/tavily-map.ts +38 -0
  27. package/src/services/tavily-search.ts +40 -0
  28. package/src/services/web-fetch-schema.ts +72 -0
  29. package/src/services/web-fetch.ts +100 -0
  30. package/src/shared/cli-flags.ts +25 -0
  31. package/src/shared/command-output.ts +21 -0
  32. package/src/shared/effect.ts +52 -0
  33. package/src/shared/errors.ts +56 -0
  34. package/src/shared/output.ts +210 -0
  35. package/src/shared/provider-http-client.ts +73 -0
  36. package/src/shared/render-error.ts +101 -0
  37. package/src/shared/schema.ts +42 -0
  38. package/src/shared/tracing.ts +53 -0
@@ -0,0 +1,56 @@
1
+ import { Schema } from "effect";
2
+
3
+ export const SearchProvider = Schema.Literals(["shared", "grok", "tavily", "firecrawl"]);
4
+
5
+ export class ConfigValidationError extends Schema.TaggedErrorClass<ConfigValidationError>()(
6
+ "ConfigValidationError",
7
+ {
8
+ provider: SearchProvider,
9
+ message: Schema.String,
10
+ details: Schema.optional(Schema.Array(Schema.String)),
11
+ cause: Schema.optional(Schema.Unknown),
12
+ },
13
+ ) {}
14
+
15
+ export class ProviderRequestError extends Schema.TaggedErrorClass<ProviderRequestError>()(
16
+ "ProviderRequestError",
17
+ {
18
+ provider: SearchProvider,
19
+ message: Schema.String,
20
+ },
21
+ ) {}
22
+
23
+ export class ProviderResponseError extends Schema.TaggedErrorClass<ProviderResponseError>()(
24
+ "ProviderResponseError",
25
+ {
26
+ provider: SearchProvider,
27
+ message: Schema.String,
28
+ status: Schema.Number,
29
+ body: Schema.String,
30
+ cause: Schema.optional(Schema.Unknown),
31
+ },
32
+ ) {}
33
+
34
+ export class ProviderContentError extends Schema.TaggedErrorClass<ProviderContentError>()(
35
+ "ProviderContentError",
36
+ {
37
+ provider: SearchProvider,
38
+ message: Schema.String,
39
+ },
40
+ ) {}
41
+
42
+ export class ProviderDecodeError extends Schema.TaggedErrorClass<ProviderDecodeError>()(
43
+ "ProviderDecodeError",
44
+ {
45
+ provider: SearchProvider,
46
+ message: Schema.String,
47
+ cause: Schema.optional(Schema.Unknown),
48
+ },
49
+ ) {}
50
+
51
+ export type UltimateSearchError =
52
+ | ConfigValidationError
53
+ | ProviderContentError
54
+ | ProviderRequestError
55
+ | ProviderResponseError
56
+ | ProviderDecodeError;
@@ -0,0 +1,210 @@
1
+ import { Config, Effect, Layer, Logger, Option, Schema, ServiceMap, Console } from "effect";
2
+ import { Flag } from "effect/unstable/cli";
3
+ import {
4
+ ConfigValidationError,
5
+ ProviderContentError,
6
+ ProviderDecodeError,
7
+ ProviderRequestError,
8
+ ProviderResponseError,
9
+ } from "./errors";
10
+ import { renderStructuredError } from "./render-error";
11
+
12
+ const stringify = (value: unknown) => JSON.stringify(value, null, 2);
13
+ const maxHumanErrorBodyLength = 500;
14
+
15
+ export const OutputModeSchema = Schema.Literals(["human", "llm"] as const);
16
+ export type OutputMode = typeof OutputModeSchema.Type;
17
+
18
+ export const outputFlag = Flag.optional(
19
+ Flag.choice("output", OutputModeSchema.literals).pipe(
20
+ Flag.withDescription("Output mode: human for readable text, llm for structured JSON."),
21
+ ),
22
+ );
23
+
24
+ const trimTrailingWhitespace = (text: string) => text.trimEnd();
25
+
26
+ const truncate = (text: string, maxLength: number) =>
27
+ text.length <= maxLength ? text : `${text.slice(0, maxLength - 1)}...`;
28
+
29
+ const configErrorDetails = (error: ConfigValidationError): Array<string> => {
30
+ const details = Array.isArray(error.details)
31
+ ? error.details.filter((detail) => detail.length > 0)
32
+ : [];
33
+
34
+ if (details.length > 0) {
35
+ return details;
36
+ }
37
+
38
+ if (error.cause instanceof Error && error.cause.message.length > 0) {
39
+ return [error.cause.message];
40
+ }
41
+
42
+ if (typeof error.cause === "string" && error.cause.length > 0) {
43
+ return [error.cause];
44
+ }
45
+
46
+ return [];
47
+ };
48
+
49
+ export const writeJsonStdout = (value: unknown) => Console.log(stringify(value));
50
+ export const renderJsonText = (value: unknown) => stringify(value);
51
+
52
+ const defaultOutputModeConfig = Config.string("AGENT").pipe(
53
+ Config.withDefault(""),
54
+ Config.map((agent): OutputMode => (agent.trim().length > 0 ? "llm" : "human")),
55
+ );
56
+
57
+ export const resolveOutputMode = (
58
+ selected: Option.Option<OutputMode>,
59
+ fallback: OutputMode,
60
+ ): OutputMode => Option.getOrElse(selected, () => fallback);
61
+
62
+ const makeCliOutput = (defaultMode: OutputMode) =>
63
+ CliOutput.of({
64
+ defaultMode,
65
+ writeOutput: ({ human, llm }, mode = defaultMode) =>
66
+ mode === "llm" ? writeJsonStdout(llm) : Console.log(human),
67
+ logError: (error, mode = defaultMode) => logCliError(error, mode),
68
+ });
69
+
70
+ const renderHumanError = (error: unknown) => {
71
+ if (error instanceof ConfigValidationError) {
72
+ const details = configErrorDetails(error);
73
+ const lines = [`Configuration error (${error.provider})`, error.message];
74
+
75
+ if (details.length > 0) {
76
+ lines.push("", "What to fix:");
77
+ lines.push(...details.map((detail) => `- ${detail}`));
78
+ }
79
+
80
+ return trimTrailingWhitespace(lines.join("\n"));
81
+ }
82
+
83
+ if (error instanceof ProviderRequestError) {
84
+ return trimTrailingWhitespace([`Request failed (${error.provider})`, error.message].join("\n"));
85
+ }
86
+
87
+ if (error instanceof ProviderContentError) {
88
+ return trimTrailingWhitespace(
89
+ [`Provider returned no usable content (${error.provider})`, error.message].join("\n"),
90
+ );
91
+ }
92
+
93
+ if (error instanceof ProviderResponseError) {
94
+ const lines = [
95
+ `Provider response error (${error.provider})`,
96
+ error.message,
97
+ `HTTP status: ${error.status}`,
98
+ ];
99
+ const body = error.body.trim();
100
+
101
+ if (body.length > 0) {
102
+ lines.push(`Response body: ${truncate(body, maxHumanErrorBodyLength)}`);
103
+ }
104
+
105
+ return trimTrailingWhitespace(lines.join("\n"));
106
+ }
107
+
108
+ if (error instanceof ProviderDecodeError) {
109
+ return trimTrailingWhitespace(
110
+ [`Provider decode error (${error.provider})`, error.message].join("\n"),
111
+ );
112
+ }
113
+
114
+ if (error instanceof Error) {
115
+ return trimTrailingWhitespace([error.name, error.message].join("\n"));
116
+ }
117
+
118
+ return `Unknown error\n${String(error)}`;
119
+ };
120
+
121
+ const cliErrorLogMessageTag = "UltimateSearchCliError";
122
+
123
+ interface CliErrorLogMessage {
124
+ readonly _tag: typeof cliErrorLogMessageTag;
125
+ readonly mode: OutputMode;
126
+ readonly error: unknown;
127
+ }
128
+
129
+ const isCliErrorLogMessage = (value: unknown): value is CliErrorLogMessage =>
130
+ typeof value === "object" &&
131
+ value !== null &&
132
+ "_tag" in value &&
133
+ value._tag === cliErrorLogMessageTag &&
134
+ "mode" in value &&
135
+ (value.mode === "human" || value.mode === "llm") &&
136
+ "error" in value;
137
+
138
+ const normalizeLogMessage = (message: unknown): unknown =>
139
+ Array.isArray(message) && message.length === 1 ? message[0] : message;
140
+
141
+ const renderGenericLogMessage = (message: unknown): string => {
142
+ const normalized = normalizeLogMessage(message);
143
+
144
+ if (Array.isArray(normalized)) {
145
+ return normalized.map((item) => renderGenericLogMessage(item)).join(" ");
146
+ }
147
+
148
+ if (typeof normalized === "string") {
149
+ return normalized;
150
+ }
151
+
152
+ if (normalized instanceof Error) {
153
+ return trimTrailingWhitespace([normalized.name, normalized.message].join("\n"));
154
+ }
155
+
156
+ try {
157
+ return stringify(normalized);
158
+ } catch {
159
+ return String(normalized);
160
+ }
161
+ };
162
+
163
+ const formatCliLogMessage = (message: unknown): string => {
164
+ const normalized = normalizeLogMessage(message);
165
+
166
+ if (isCliErrorLogMessage(normalized)) {
167
+ return normalized.mode === "llm"
168
+ ? stringify({ error: renderStructuredError(normalized.error) })
169
+ : renderHumanError(normalized.error);
170
+ }
171
+
172
+ return renderGenericLogMessage(normalized);
173
+ };
174
+
175
+ export const logCliError = (error: unknown, mode: OutputMode) =>
176
+ Effect.logError({
177
+ _tag: cliErrorLogMessageTag,
178
+ mode,
179
+ error,
180
+ } satisfies CliErrorLogMessage);
181
+
182
+ const cliConsoleLogger = Logger.withConsoleError(
183
+ Logger.make((options) => formatCliLogMessage(options.message)),
184
+ );
185
+
186
+ export const cliLoggerLayer = Logger.layer([cliConsoleLogger, Logger.tracerLogger]);
187
+
188
+ export class CliOutput extends ServiceMap.Service<
189
+ CliOutput,
190
+ {
191
+ readonly defaultMode: OutputMode;
192
+ readonly writeOutput: (
193
+ output: {
194
+ readonly human: string;
195
+ readonly llm: unknown;
196
+ },
197
+ mode?: OutputMode,
198
+ ) => Effect.Effect<void>;
199
+ readonly logError: (error: unknown, mode?: OutputMode) => Effect.Effect<void>;
200
+ }
201
+ >()("CliOutput") {
202
+ static readonly layer = Layer.effect(
203
+ CliOutput,
204
+ Effect.map(defaultOutputModeConfig.asEffect(), makeCliOutput),
205
+ );
206
+
207
+ static layerForMode(mode: OutputMode) {
208
+ return Layer.succeed(CliOutput, makeCliOutput(mode));
209
+ }
210
+ }
@@ -0,0 +1,73 @@
1
+ import { Effect } from "effect";
2
+ import { HttpClient, HttpClientError, type HttpClientResponse } from "effect/unstable/http";
3
+ import { ProviderRequestError, ProviderResponseError } from "./errors";
4
+
5
+ export type ProviderHttpClientName = "shared" | "grok" | "tavily" | "firecrawl";
6
+
7
+ export const mapProviderRequestError = (
8
+ provider: ProviderHttpClientName,
9
+ error: unknown,
10
+ fallback: string,
11
+ ) =>
12
+ new ProviderRequestError({
13
+ provider,
14
+ message: error instanceof Error && error.message.length > 0 ? error.message : fallback,
15
+ });
16
+
17
+ export const makeProviderHttpClient = (
18
+ client: HttpClient.HttpClient,
19
+ ): HttpClient.HttpClient.With<HttpClientError.HttpClientError, never> =>
20
+ client.pipe(
21
+ HttpClient.filterStatusOk,
22
+ HttpClient.retryTransient({
23
+ retryOn: "errors-and-responses",
24
+ times: 2,
25
+ }),
26
+ ) as HttpClient.HttpClient.With<HttpClientError.HttpClientError, never>;
27
+
28
+ export const catchProviderHttpError =
29
+ (
30
+ provider: ProviderHttpClientName,
31
+ requestErrorMessage: string,
32
+ responseErrorMessage: (status: number) => string,
33
+ ) =>
34
+ <A>(
35
+ effect: Effect.Effect<A, HttpClientError.HttpClientError, never>,
36
+ ): Effect.Effect<A, ProviderRequestError | ProviderResponseError, never> =>
37
+ effect.pipe(
38
+ Effect.catchTag(
39
+ "HttpClientError",
40
+ (error): Effect.Effect<never, ProviderRequestError | ProviderResponseError, never> => {
41
+ const response = error.response;
42
+
43
+ if (response === undefined) {
44
+ return Effect.fail(mapProviderRequestError(provider, error, requestErrorMessage));
45
+ }
46
+
47
+ return response.text.pipe(
48
+ Effect.catch(() => Effect.succeed("")),
49
+ Effect.flatMap((body) =>
50
+ Effect.fail(
51
+ new ProviderResponseError({
52
+ provider,
53
+ message: responseErrorMessage(response.status),
54
+ status: response.status,
55
+ body,
56
+ cause: error,
57
+ }),
58
+ ),
59
+ ),
60
+ );
61
+ },
62
+ ),
63
+ );
64
+
65
+ export const decodeJsonResponse = <A, E1, E2>(
66
+ response: HttpClientResponse.HttpClientResponse,
67
+ decode: (value: unknown) => Effect.Effect<A, E1, never>,
68
+ mapDecodeError: (error: unknown) => E2,
69
+ ): Effect.Effect<A, E2, never> =>
70
+ response.json.pipe(
71
+ Effect.mapError(mapDecodeError),
72
+ Effect.flatMap((json) => decode(json).pipe(Effect.mapError(mapDecodeError))),
73
+ );
@@ -0,0 +1,101 @@
1
+ import { Config } from "effect";
2
+ import { Schema } from "effect";
3
+ import {
4
+ ConfigValidationError,
5
+ ProviderDecodeError,
6
+ ProviderRequestError,
7
+ ProviderResponseError,
8
+ } from "./errors";
9
+
10
+ export interface RenderedError {
11
+ readonly type: string;
12
+ readonly provider?: string;
13
+ readonly message: string;
14
+ readonly details?: ReadonlyArray<string>;
15
+ readonly status?: number;
16
+ readonly body?: string;
17
+ }
18
+
19
+ export const RenderedErrorSchema = Schema.Struct({
20
+ type: Schema.String,
21
+ provider: Schema.optional(Schema.String),
22
+ message: Schema.String,
23
+ details: Schema.optional(Schema.Array(Schema.String)),
24
+ status: Schema.optional(Schema.Number),
25
+ body: Schema.optional(Schema.String),
26
+ });
27
+
28
+ const configErrorDetails = (error: ConfigValidationError): Array<string> => {
29
+ const details = Array.isArray(error.details)
30
+ ? error.details.filter((detail) => detail.length > 0)
31
+ : [];
32
+
33
+ if (details.length > 0) {
34
+ return details;
35
+ }
36
+
37
+ if (error.cause instanceof Config.ConfigError && error.cause.message.length > 0) {
38
+ return [error.cause.message];
39
+ }
40
+
41
+ if (error.cause instanceof Error && error.cause.message.length > 0) {
42
+ return [error.cause.message];
43
+ }
44
+
45
+ if (typeof error.cause === "string" && error.cause.length > 0) {
46
+ return [error.cause];
47
+ }
48
+
49
+ return [];
50
+ };
51
+
52
+ export const renderStructuredError = (error: unknown): RenderedError => {
53
+ if (error instanceof ConfigValidationError) {
54
+ const details = configErrorDetails(error);
55
+
56
+ return {
57
+ type: error._tag,
58
+ provider: error.provider,
59
+ message: error.message,
60
+ ...(details.length > 0 ? { details } : {}),
61
+ };
62
+ }
63
+
64
+ if (error instanceof ProviderRequestError) {
65
+ return {
66
+ type: error._tag,
67
+ provider: error.provider,
68
+ message: error.message,
69
+ };
70
+ }
71
+
72
+ if (error instanceof ProviderResponseError) {
73
+ return {
74
+ type: error._tag,
75
+ provider: error.provider,
76
+ message: error.message,
77
+ status: error.status,
78
+ body: error.body,
79
+ };
80
+ }
81
+
82
+ if (error instanceof ProviderDecodeError) {
83
+ return {
84
+ type: error._tag,
85
+ provider: error.provider,
86
+ message: error.message,
87
+ };
88
+ }
89
+
90
+ if (error instanceof Error) {
91
+ return {
92
+ type: error.name,
93
+ message: error.message,
94
+ };
95
+ }
96
+
97
+ return {
98
+ type: "UnknownError",
99
+ message: String(error),
100
+ };
101
+ };
@@ -0,0 +1,42 @@
1
+ import { Effect, Option, Schema, SchemaIssue, SchemaTransformation } from "effect";
2
+
3
+ const stripTrailingSlashes = (value: string) => value.replace(/\/+$/u, "");
4
+
5
+ export const trimmedNonEmptyStringSchema = (message: string) =>
6
+ Schema.Trim.pipe(Schema.decodeTo(Schema.NonEmptyString), Schema.annotate({ message }));
7
+
8
+ export const optionalTrimmedNonEmptyStringFromStringSchema = Schema.Trim.pipe(
9
+ Schema.decodeTo(
10
+ Schema.Option(Schema.NonEmptyString),
11
+ SchemaTransformation.transform({
12
+ decode: (value) => (value.length === 0 ? Option.none<string>() : Option.some(value)),
13
+ encode: (value) => Option.getOrElse(value, () => ""),
14
+ }),
15
+ ),
16
+ );
17
+
18
+ export const absoluteUrlStringSchema = (message: string) =>
19
+ Schema.Trim.pipe(
20
+ Schema.decodeTo(
21
+ Schema.String,
22
+ SchemaTransformation.transformOrFail({
23
+ decode: (value) =>
24
+ Effect.try({
25
+ try: () => stripTrailingSlashes(new URL(value).toString()),
26
+ catch: () => new SchemaIssue.InvalidValue(Option.some(value), { message }),
27
+ }),
28
+ encode: (value) => Effect.succeed(value),
29
+ }),
30
+ ),
31
+ );
32
+
33
+ export const optionalAbsoluteUrlStringFromStringSchema = (message: string) =>
34
+ Schema.Trim.pipe(
35
+ Schema.decodeTo(
36
+ Schema.Option(absoluteUrlStringSchema(message)),
37
+ SchemaTransformation.transform({
38
+ decode: (value) => (value.length === 0 ? Option.none<string>() : Option.some(value)),
39
+ encode: (value) => Option.getOrElse(value, () => ""),
40
+ }),
41
+ ),
42
+ );
@@ -0,0 +1,53 @@
1
+ import { Cause, Duration, Effect, Fiber, Layer, Logger, LogLevel, Tracer } from "effect";
2
+ import { CurrentLoggers } from "effect/Logger";
3
+ import { MinimumLogLevel } from "effect/References";
4
+
5
+ export const TracingLayer = Layer.unwrap(
6
+ Effect.gen(function* () {
7
+ const logLevel = yield* MinimumLogLevel;
8
+
9
+ if (LogLevel.isLessThan("Trace", logLevel)) {
10
+ return Layer.empty;
11
+ }
12
+
13
+ return tracerLayer;
14
+ }),
15
+ );
16
+
17
+ const tracerLayer = Effect.gen(function* () {
18
+ const loggers = yield* CurrentLoggers;
19
+ const tracer = yield* Tracer.Tracer;
20
+ const fiber = Fiber.getCurrent()!;
21
+
22
+ const log = (message: string, time: bigint) => {
23
+ const date = new Date(Number(time / BigInt(1e6)));
24
+ const options: Logger.Options<string> = {
25
+ message,
26
+ fiber,
27
+ date,
28
+ logLevel: "Trace",
29
+ cause: Cause.empty,
30
+ };
31
+
32
+ loggers.forEach((logger) => {
33
+ logger.log(options);
34
+ });
35
+ };
36
+
37
+ return Tracer.make({
38
+ span(options) {
39
+ const span = tracer.span(options);
40
+ log(`${options.name}: started`, options.startTime);
41
+ const originalEnd = span.end;
42
+
43
+ span.end = (endTime, cause) => {
44
+ const duration = Duration.nanos(endTime - span.status.startTime);
45
+ log(`${options.name}: completed. Took ${Duration.format(duration)}`, endTime);
46
+
47
+ return originalEnd.call(span, endTime, cause);
48
+ };
49
+
50
+ return span;
51
+ },
52
+ });
53
+ }).pipe(Layer.effect(Tracer.Tracer));