1import { TrimmedNonEmptyString } from "@t3tools/contracts";
2import { compareSemverVersions, parseSemver } from "@t3tools/shared/semver";
3import * as Option from "effect/Option";
4import * as Schema from "effect/Schema";
5
6export const ClaudeCodeProfileSchema = Schema.Struct({
7 effortMap: Schema.optional(
8 Schema.Record(TrimmedNonEmptyString, Schema.NullOr(TrimmedNonEmptyString)),
9 ),
10 modelSuffixes: Schema.optional(
11 Schema.Record(
12 TrimmedNonEmptyString,
13 Schema.Record(TrimmedNonEmptyString, TrimmedNonEmptyString),
14 ),
15 ),
16 contextWindowTokens: Schema.optional(Schema.Record(TrimmedNonEmptyString, Schema.Number)),
17 fixedContextWindowTokens: Schema.optional(Schema.Number),
18});
19
20export const ClaudeProfileAdapterSchema = Schema.Struct({
21 claudeCode: Schema.optional(ClaudeCodeProfileSchema),
22});
23
24const ClaudeVersionSchema = TrimmedNonEmptyString.pipe(
25 Schema.check(
26 Schema.makeFilter((version) => parseSemver(version) !== null, {
27 expected: "a supported semantic version",
28 }),
29 ),
30);
31
32const ClaudeCodeCompatibilitySchema = Schema.Struct({
33 minVersion: Schema.optional(ClaudeVersionSchema),
34 maxVersionExclusive: Schema.optional(ClaudeVersionSchema),
35}).pipe(
36 Schema.check(
37 Schema.makeFilter(
38 ({ minVersion, maxVersionExclusive }) =>
39 minVersion === undefined ||
40 maxVersionExclusive === undefined ||
41 compareSemverVersions(minVersion, maxVersionExclusive) < 0,
42 { expected: "a minimum version below the exclusive maximum version" },
43 ),
44 ),
45);
46
47export const ClaudeModelAdapterSchema = Schema.Struct({
48 claudeCode: Schema.optional(ClaudeCodeCompatibilitySchema),
49});
50
51export type ClaudeCodeProfile = typeof ClaudeCodeProfileSchema.Type;
52export type ClaudeCodeCompatibility = NonNullable<typeof ClaudeModelAdapterSchema.Type.claudeCode>;
53
54export const decodeClaudeProfileAdapter = Schema.decodeUnknownOption(ClaudeProfileAdapterSchema);
55export const decodeClaudeModelAdapter = Schema.decodeUnknownOption(ClaudeModelAdapterSchema);
56
57interface ClaudeManifestAdapterInput {
58 readonly providers?:
59 | Readonly<
60 Record<
61 string,
62 | {
63 readonly profiles: Readonly<Record<string, { readonly adapter?: unknown }>>;
64 readonly models: ReadonlyArray<{ readonly adapter?: unknown }>;
65 }
66 | undefined
67 >
68 >
69 | undefined;
70}
71
72export function hasValidClaudeManifestAdapters(manifest: ClaudeManifestAdapterInput): boolean {
73 const catalog = manifest.providers?.claudeAgent;
74 if (!catalog) return true;
75
76 return (
77 Object.values(catalog.profiles).every((profile) =>
78 Option.isSome(decodeClaudeProfileAdapter(profile.adapter ?? {})),
79 ) &&
80 catalog.models.every((model) => Option.isSome(decodeClaudeModelAdapter(model.adapter ?? {})))
81 );
82}