Skip to content

Commit 58e5f71

Browse files
juliusmarmingecursoragentcodexcursor[bot]
authored
Add provider skill discovery (#1905)
Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: codex <codex@users.noreply.github.com> Co-authored-by: cursor[bot] <206951365+cursor[bot]@users.noreply.github.com>
1 parent 4ae9de3 commit 58e5f71

34 files changed

Lines changed: 2062 additions & 230 deletions

apps/server/src/provider/Layers/ClaudeProvider.ts

Lines changed: 102 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,16 @@ import type {
44
ServerProvider,
55
ServerProviderModel,
66
ServerProviderAuth,
7+
ServerProviderSlashCommand,
78
ServerProviderState,
89
} from "@t3tools/contracts";
910
import { Cache, Duration, Effect, Equal, Layer, Option, Result, Schema, Stream } from "effect";
1011
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process";
1112
import { decodeJsonResult } from "@t3tools/shared/schemaJson";
12-
import { query as claudeQuery } from "@anthropic-ai/claude-agent-sdk";
13+
import {
14+
query as claudeQuery,
15+
type SlashCommand as ClaudeSlashCommand,
16+
} from "@anthropic-ai/claude-agent-sdk";
1317

1418
import {
1519
buildServerProvider,
@@ -340,6 +344,74 @@ function claudeAuthMetadata(input: {
340344

341345
const CAPABILITIES_PROBE_TIMEOUT_MS = 8_000;
342346

347+
function nonEmptyProbeString(value: string): string | undefined {
348+
const candidate = value.trim();
349+
return candidate ? candidate : undefined;
350+
}
351+
352+
function parseClaudeInitializationCommands(
353+
commands: ReadonlyArray<ClaudeSlashCommand> | undefined,
354+
): ReadonlyArray<ServerProviderSlashCommand> {
355+
return dedupeSlashCommands(
356+
(commands ?? []).flatMap((command) => {
357+
const name = nonEmptyProbeString(command.name);
358+
if (!name) {
359+
return [];
360+
}
361+
362+
const description = nonEmptyProbeString(command.description);
363+
const argumentHint = nonEmptyProbeString(command.argumentHint);
364+
365+
return [
366+
{
367+
name,
368+
...(description ? { description } : {}),
369+
...(argumentHint ? { input: { hint: argumentHint } } : {}),
370+
} satisfies ServerProviderSlashCommand,
371+
];
372+
}),
373+
);
374+
}
375+
376+
function dedupeSlashCommands(
377+
commands: ReadonlyArray<ServerProviderSlashCommand>,
378+
): ReadonlyArray<ServerProviderSlashCommand> {
379+
const commandsByName = new Map<string, ServerProviderSlashCommand>();
380+
381+
for (const command of commands) {
382+
const name = nonEmptyProbeString(command.name);
383+
if (!name) {
384+
continue;
385+
}
386+
387+
const key = name.toLowerCase();
388+
const existing = commandsByName.get(key);
389+
if (!existing) {
390+
commandsByName.set(key, {
391+
...command,
392+
name,
393+
});
394+
continue;
395+
}
396+
397+
commandsByName.set(key, {
398+
...existing,
399+
...(existing.description
400+
? {}
401+
: command.description
402+
? { description: command.description }
403+
: {}),
404+
...(existing.input?.hint
405+
? {}
406+
: command.input?.hint
407+
? { input: { hint: command.input.hint } }
408+
: {}),
409+
});
410+
}
411+
412+
return [...commandsByName.values()];
413+
}
414+
343415
/**
344416
* Probe account information by spawning a lightweight Claude Agent SDK
345417
* session and reading the initialization result.
@@ -361,13 +433,16 @@ const probeClaudeCapabilities = (binaryPath: string) => {
361433
pathToClaudeCodeExecutable: binaryPath,
362434
abortController: abort,
363435
maxTurns: 0,
364-
settingSources: [],
436+
settingSources: ["user", "project", "local"],
365437
allowedTools: [],
366438
stderr: () => {},
367439
},
368440
});
369441
const init = await q.initializationResult();
370-
return { subscriptionType: init.account?.subscriptionType };
442+
return {
443+
subscriptionType: init.account?.subscriptionType,
444+
slashCommands: parseClaudeInitializationCommands(init.commands),
445+
};
371446
}).pipe(
372447
Effect.ensuring(
373448
Effect.sync(() => {
@@ -396,6 +471,9 @@ const runClaudeCommand = Effect.fn("runClaudeCommand")(function* (args: Readonly
396471

397472
export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(function* (
398473
resolveSubscriptionType?: (binaryPath: string) => Effect.Effect<string | undefined>,
474+
resolveSlashCommands?: (
475+
binaryPath: string,
476+
) => Effect.Effect<ReadonlyArray<ServerProviderSlashCommand> | undefined>,
399477
): Effect.fn.Return<
400478
ServerProvider,
401479
ServerSettingsError,
@@ -491,6 +569,14 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
491569
});
492570
}
493571

572+
const slashCommands =
573+
(resolveSlashCommands
574+
? yield* resolveSlashCommands(claudeSettings.binaryPath).pipe(
575+
Effect.orElseSucceed(() => undefined),
576+
)
577+
: undefined) ?? [];
578+
const dedupedSlashCommands = dedupeSlashCommands(slashCommands);
579+
494580
// ── Auth check + subscription detection ────────────────────────────
495581

496582
const authProbe = yield* runClaudeCommand(["auth", "status"]).pipe(
@@ -525,6 +611,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
525611
enabled: claudeSettings.enabled,
526612
checkedAt,
527613
models,
614+
slashCommands: dedupedSlashCommands,
528615
probe: {
529616
installed: true,
530617
version: parsedVersion,
@@ -544,6 +631,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
544631
enabled: claudeSettings.enabled,
545632
checkedAt,
546633
models,
634+
slashCommands: dedupedSlashCommands,
547635
probe: {
548636
installed: true,
549637
version: parsedVersion,
@@ -561,6 +649,7 @@ export const checkClaudeProviderStatus = Effect.fn("checkClaudeProviderStatus")(
561649
enabled: claudeSettings.enabled,
562650
checkedAt,
563651
models,
652+
slashCommands: dedupedSlashCommands,
564653
probe: {
565654
installed: true,
566655
version: parsedVersion,
@@ -583,12 +672,18 @@ export const ClaudeProviderLive = Layer.effect(
583672
const subscriptionProbeCache = yield* Cache.make({
584673
capacity: 1,
585674
timeToLive: Duration.minutes(5),
586-
lookup: (binaryPath: string) =>
587-
probeClaudeCapabilities(binaryPath).pipe(Effect.map((r) => r?.subscriptionType)),
675+
lookup: (binaryPath: string) => probeClaudeCapabilities(binaryPath),
588676
});
589677

590-
const checkProvider = checkClaudeProviderStatus((binaryPath) =>
591-
Cache.get(subscriptionProbeCache, binaryPath),
678+
const checkProvider = checkClaudeProviderStatus(
679+
(binaryPath) =>
680+
Cache.get(subscriptionProbeCache, binaryPath).pipe(
681+
Effect.map((probe) => probe?.subscriptionType),
682+
),
683+
(binaryPath) =>
684+
Cache.get(subscriptionProbeCache, binaryPath).pipe(
685+
Effect.map((probe) => probe?.slashCommands),
686+
),
592687
).pipe(
593688
Effect.provideService(ServerSettingsService, serverSettings),
594689
Effect.provideService(ChildProcessSpawner.ChildProcessSpawner, spawner),

apps/server/src/provider/Layers/CodexProvider.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import type {
55
ServerProvider,
66
ServerProviderModel,
77
ServerProviderAuth,
8+
ServerProviderSkill,
89
ServerProviderState,
910
} from "@t3tools/contracts";
1011
import {
@@ -44,7 +45,7 @@ import {
4445
codexAuthSubType,
4546
type CodexAccountSnapshot,
4647
} from "../codexAccount";
47-
import { probeCodexAccount } from "../codexAppServer";
48+
import { probeCodexDiscovery } from "../codexAppServer";
4849
import { CodexProvider } from "../Services/CodexProvider";
4950
import { ServerSettingsService } from "../../serverSettings";
5051
import { ServerSettingsError } from "@t3tools/contracts";
@@ -304,8 +305,9 @@ const CAPABILITIES_PROBE_TIMEOUT_MS = 8_000;
304305
const probeCodexCapabilities = (input: {
305306
readonly binaryPath: string;
306307
readonly homePath?: string;
308+
readonly cwd: string;
307309
}) =>
308-
Effect.tryPromise((signal) => probeCodexAccount({ ...input, signal })).pipe(
310+
Effect.tryPromise((signal) => probeCodexDiscovery({ ...input, signal })).pipe(
309311
Effect.timeoutOption(CAPABILITIES_PROBE_TIMEOUT_MS),
310312
Effect.result,
311313
Effect.map((result) => {
@@ -334,6 +336,11 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
334336
readonly binaryPath: string;
335337
readonly homePath?: string;
336338
}) => Effect.Effect<CodexAccountSnapshot | undefined>,
339+
resolveSkills?: (input: {
340+
readonly binaryPath: string;
341+
readonly homePath?: string;
342+
readonly cwd: string;
343+
}) => Effect.Effect<ReadonlyArray<ServerProviderSkill> | undefined>,
337344
): Effect.fn.Return<
338345
ServerProvider,
339346
ServerSettingsError,
@@ -449,12 +456,22 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
449456
});
450457
}
451458

459+
const skills =
460+
(resolveSkills
461+
? yield* resolveSkills({
462+
binaryPath: codexSettings.binaryPath,
463+
homePath: codexSettings.homePath,
464+
cwd: process.cwd(),
465+
}).pipe(Effect.orElseSucceed(() => undefined))
466+
: undefined) ?? [];
467+
452468
if (yield* hasCustomModelProvider) {
453469
return buildServerProvider({
454470
provider: PROVIDER,
455471
enabled: codexSettings.enabled,
456472
checkedAt,
457473
models,
474+
skills,
458475
probe: {
459476
installed: true,
460477
version: parsedVersion,
@@ -484,6 +501,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
484501
enabled: codexSettings.enabled,
485502
checkedAt,
486503
models: resolvedModels,
504+
skills,
487505
probe: {
488506
installed: true,
489507
version: parsedVersion,
@@ -500,6 +518,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
500518
enabled: codexSettings.enabled,
501519
checkedAt,
502520
models: resolvedModels,
521+
skills,
503522
probe: {
504523
installed: true,
505524
version: parsedVersion,
@@ -518,6 +537,7 @@ export const checkCodexProviderStatus = Effect.fn("checkCodexProviderStatus")(fu
518537
enabled: codexSettings.enabled,
519538
checkedAt,
520539
models: resolvedModels,
540+
skills,
521541
probe: {
522542
installed: true,
523543
version: parsedVersion,
@@ -543,16 +563,29 @@ export const CodexProviderLive = Layer.effect(
543563
capacity: 4,
544564
timeToLive: Duration.minutes(5),
545565
lookup: (key: string) => {
546-
const [binaryPath, homePath] = JSON.parse(key) as [string, string | undefined];
566+
const [binaryPath, homePath, cwd] = JSON.parse(key) as [string, string | undefined, string];
547567
return probeCodexCapabilities({
548568
binaryPath,
569+
cwd,
549570
...(homePath ? { homePath } : {}),
550571
});
551572
},
552573
});
553574

554-
const checkProvider = checkCodexProviderStatus((input) =>
555-
Cache.get(accountProbeCache, JSON.stringify([input.binaryPath, input.homePath])),
575+
const getDiscovery = (input: {
576+
readonly binaryPath: string;
577+
readonly homePath?: string;
578+
readonly cwd: string;
579+
}) =>
580+
Cache.get(accountProbeCache, JSON.stringify([input.binaryPath, input.homePath, input.cwd]));
581+
582+
const checkProvider = checkCodexProviderStatus(
583+
(input) =>
584+
getDiscovery({
585+
...input,
586+
cwd: process.cwd(),
587+
}).pipe(Effect.map((discovery) => discovery?.account)),
588+
(input) => getDiscovery(input).pipe(Effect.map((discovery) => discovery?.skills)),
556589
).pipe(
557590
Effect.provideService(ServerSettingsService, serverSettings),
558591
Effect.provideService(FileSystem.FileSystem, fileSystem),

0 commit comments

Comments
 (0)