@oh-my-pi/pi-coding-agent 12.8.2 → 12.10.0

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 (43) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/package.json +7 -7
  3. package/src/capability/rule.ts +173 -5
  4. package/src/cli/args.ts +3 -0
  5. package/src/cli/update-cli.ts +1 -66
  6. package/src/commands/launch.ts +3 -0
  7. package/src/config/model-registry.ts +300 -21
  8. package/src/config/model-resolver.ts +4 -4
  9. package/src/config/settings-schema.ts +12 -0
  10. package/src/discovery/agents.ts +175 -12
  11. package/src/discovery/builtin.ts +3 -13
  12. package/src/discovery/cline.ts +4 -45
  13. package/src/discovery/cursor.ts +2 -29
  14. package/src/discovery/helpers.ts +43 -0
  15. package/src/discovery/index.ts +1 -0
  16. package/src/discovery/opencode.ts +394 -0
  17. package/src/discovery/windsurf.ts +5 -44
  18. package/src/export/ttsr.ts +324 -54
  19. package/src/extensibility/custom-tools/wrapper.ts +1 -11
  20. package/src/internal-urls/index.ts +4 -2
  21. package/src/internal-urls/memory-protocol.ts +133 -0
  22. package/src/internal-urls/router.ts +4 -2
  23. package/src/internal-urls/skill-protocol.ts +1 -1
  24. package/src/internal-urls/types.ts +6 -2
  25. package/src/main.ts +5 -0
  26. package/src/memories/index.ts +6 -13
  27. package/src/modes/components/settings-defs.ts +6 -0
  28. package/src/modes/components/status-line/segments.ts +3 -2
  29. package/src/modes/rpc/rpc-client.ts +16 -0
  30. package/src/prompts/memories/consolidation.md +1 -1
  31. package/src/prompts/memories/read_path.md +4 -4
  32. package/src/prompts/memories/stage_one_input.md +1 -2
  33. package/src/prompts/tools/bash.md +10 -23
  34. package/src/prompts/tools/read.md +2 -0
  35. package/src/sdk.ts +25 -10
  36. package/src/session/agent-session.ts +252 -44
  37. package/src/session/session-manager.ts +79 -36
  38. package/src/tools/bash-skill-urls.ts +177 -0
  39. package/src/tools/bash.ts +7 -1
  40. package/src/tools/fetch.ts +6 -2
  41. package/src/tools/index.ts +2 -2
  42. package/src/tools/output-meta.ts +49 -42
  43. package/src/tools/read.ts +2 -2
@@ -1,26 +1,49 @@
1
1
  import {
2
2
  type Api,
3
3
  type AssistantMessageEventStream,
4
+ anthropicModelManagerOptions,
4
5
  type Context,
6
+ cerebrasModelManagerOptions,
7
+ createModelManager,
8
+ cursorModelManagerOptions,
9
+ getBundledModels,
10
+ getBundledProviders,
5
11
  getGitHubCopilotBaseUrl,
6
- getModels,
7
- getProviders,
12
+ githubCopilotModelManagerOptions,
13
+ googleAntigravityModelManagerOptions,
14
+ googleGeminiCliModelManagerOptions,
15
+ googleModelManagerOptions,
16
+ groqModelManagerOptions,
17
+ kimiCodeModelManagerOptions,
8
18
  type Model,
19
+ type ModelManagerOptions,
20
+ mistralModelManagerOptions,
9
21
  normalizeDomain,
10
22
  type OAuthCredentials,
11
23
  type OAuthLoginCallbacks,
24
+ openaiCodexModelManagerOptions,
25
+ openaiModelManagerOptions,
26
+ opencodeModelManagerOptions,
27
+ openrouterModelManagerOptions,
12
28
  registerCustomApi,
13
29
  registerOAuthProvider,
14
30
  type SimpleStreamOptions,
15
31
  unregisterCustomApis,
16
32
  unregisterOAuthProviders,
33
+ vercelAiGatewayModelManagerOptions,
34
+ xaiModelManagerOptions,
17
35
  } from "@oh-my-pi/pi-ai";
18
36
  import { logger } from "@oh-my-pi/pi-utils";
19
37
  import { type Static, Type } from "@sinclair/typebox";
20
- import AjvModule from "ajv";
21
38
  import { type ConfigError, ConfigFile } from "../config";
22
39
  import type { ThemeColor } from "../modes/theme/theme";
23
- import type { AuthStorage } from "../session/auth-storage";
40
+ import type { AuthStorage, OAuthCredential } from "../session/auth-storage";
41
+
42
+ export const kNoAuth = "N/A";
43
+
44
+ export function isAuthenticated(apiKey: string | undefined | null): apiKey is string {
45
+ return Boolean(apiKey) && apiKey !== kNoAuth;
46
+ }
24
47
 
25
48
  export type ModelRole = "default" | "smol" | "slow" | "plan" | "commit";
26
49
 
@@ -40,8 +63,6 @@ export const MODEL_ROLES: Record<ModelRole, ModelRoleInfo> = {
40
63
 
41
64
  export const MODEL_ROLE_IDS: ModelRole[] = ["default", "smol", "slow", "plan", "commit"];
42
65
 
43
- const _Ajv = (AjvModule as any).default || AjvModule;
44
-
45
66
  const OpenRouterRoutingSchema = Type.Object({
46
67
  only: Type.Optional(Type.Array(Type.String())),
47
68
  order: Type.Optional(Type.Array(Type.String())),
@@ -252,19 +273,61 @@ function resolveApiKeyConfig(keyConfig: string): string | undefined {
252
273
  return keyConfig;
253
274
  }
254
275
 
276
+ function extractGoogleOAuthToken(value: string | undefined): string | undefined {
277
+ if (!isAuthenticated(value)) return undefined;
278
+ try {
279
+ const parsed = JSON.parse(value) as { token?: unknown };
280
+ if (Object.hasOwn(parsed, "token")) {
281
+ if (typeof parsed.token !== "string") {
282
+ return undefined;
283
+ }
284
+ const token = parsed.token.trim();
285
+ return token.length > 0 ? token : undefined;
286
+ }
287
+ } catch {
288
+ // OAuth values for Google providers are expected to be JSON, but custom setups may already provide raw token.
289
+ }
290
+ return value;
291
+ }
292
+
293
+ function getOAuthCredentialsForProvider(authStorage: AuthStorage, provider: string): OAuthCredential[] {
294
+ const providerEntry = authStorage.getAll()[provider];
295
+ if (!providerEntry) {
296
+ return [];
297
+ }
298
+ const entries = Array.isArray(providerEntry) ? providerEntry : [providerEntry];
299
+ return entries.filter((entry): entry is OAuthCredential => entry.type === "oauth");
300
+ }
301
+
302
+ function resolveOAuthAccountIdForAccessToken(
303
+ authStorage: AuthStorage,
304
+ provider: string,
305
+ accessToken: string,
306
+ ): string | undefined {
307
+ const oauthCredentials = getOAuthCredentialsForProvider(authStorage, provider);
308
+ const matchingCredential = oauthCredentials.find(credential => credential.access === accessToken);
309
+ if (matchingCredential) {
310
+ return matchingCredential.accountId;
311
+ }
312
+ if (oauthCredentials.length === 1) {
313
+ return oauthCredentials[0].accountId;
314
+ }
315
+ return undefined;
316
+ }
317
+
255
318
  function mergeCompat(
256
319
  baseCompat: Model<Api>["compat"],
257
320
  overrideCompat: ModelOverride["compat"],
258
321
  ): Model<Api>["compat"] | undefined {
259
322
  if (!overrideCompat) return baseCompat;
260
- const base = baseCompat as any;
261
- const override = overrideCompat as any;
262
- const merged = { ...base, ...override };
263
- if (base?.openRouterRouting || override.openRouterRouting) {
264
- merged.openRouterRouting = { ...base?.openRouterRouting, ...override.openRouterRouting };
323
+ const base = baseCompat ?? {};
324
+ const override = overrideCompat;
325
+ const merged: NonNullable<Model<Api>["compat"]> = { ...base, ...override };
326
+ if (baseCompat?.openRouterRouting || overrideCompat.openRouterRouting) {
327
+ merged.openRouterRouting = { ...baseCompat?.openRouterRouting, ...overrideCompat.openRouterRouting };
265
328
  }
266
- if (base?.vercelGatewayRouting || override.vercelGatewayRouting) {
267
- merged.vercelGatewayRouting = { ...base?.vercelGatewayRouting, ...override.vercelGatewayRouting };
329
+ if (baseCompat?.vercelGatewayRouting || overrideCompat.vercelGatewayRouting) {
330
+ merged.vercelGatewayRouting = { ...baseCompat?.vercelGatewayRouting, ...overrideCompat.vercelGatewayRouting };
268
331
  }
269
332
  return merged;
270
333
  }
@@ -384,8 +447,8 @@ export class ModelRegistry {
384
447
  overrides: Map<string, ProviderOverride>,
385
448
  modelOverrides: Map<string, Map<string, ModelOverride>>,
386
449
  ): Model<Api>[] {
387
- return getProviders().flatMap(provider => {
388
- const models = getModels(provider as any) as Model<Api>[];
450
+ return getBundledProviders().flatMap(provider => {
451
+ const models = getBundledModels(provider as Parameters<typeof getBundledModels>[0]) as Model<Api>[];
389
452
  const providerOverride = overrides.get(provider);
390
453
  const perModelOverrides = modelOverrides.get(provider);
391
454
 
@@ -516,11 +579,35 @@ export class ModelRegistry {
516
579
  }
517
580
 
518
581
  async #refreshRuntimeDiscoveries(): Promise<void> {
519
- if (this.#discoverableProviders.length === 0) return;
520
- const discovered = await Promise.all(
521
- this.#discoverableProviders.map(provider => this.#discoverProviderModels(provider)),
582
+ const configuredDiscoveriesPromise =
583
+ this.#discoverableProviders.length === 0
584
+ ? Promise.resolve<Model<Api>[]>([])
585
+ : Promise.all(this.#discoverableProviders.map(provider => this.#discoverProviderModels(provider))).then(
586
+ results => results.flat(),
587
+ );
588
+ const [configuredDiscovered, builtInDiscovered] = await Promise.all([
589
+ configuredDiscoveriesPromise,
590
+ this.#discoverBuiltInProviderModels(),
591
+ ]);
592
+ const discovered = [...configuredDiscovered, ...builtInDiscovered];
593
+ if (discovered.length === 0) {
594
+ return;
595
+ }
596
+ const merged = this.#mergeCustomModels(
597
+ this.#models,
598
+ discovered.map(model => {
599
+ const existing =
600
+ this.find(model.provider, model.id) ??
601
+ this.#models.find(candidate => candidate.provider === model.provider);
602
+ return existing
603
+ ? {
604
+ ...model,
605
+ baseUrl: existing.baseUrl,
606
+ headers: existing.headers ? { ...existing.headers, ...model.headers } : model.headers,
607
+ }
608
+ : model;
609
+ }),
522
610
  );
523
- const merged = this.#mergeCustomModels(this.#models, discovered.flat());
524
611
  this.#models = this.#applyModelOverrides(merged, this.#modelOverrides);
525
612
  }
526
613
 
@@ -531,6 +618,198 @@ export class ModelRegistry {
531
618
  }
532
619
  }
533
620
 
621
+ async #discoverBuiltInProviderModels(): Promise<Model<Api>[]> {
622
+ const managerOptions = await this.#collectBuiltInModelManagerOptions();
623
+ if (managerOptions.length === 0) {
624
+ return [];
625
+ }
626
+ const discoveries = await Promise.all(managerOptions.map(options => this.#discoverWithModelManager(options)));
627
+ return discoveries.flat();
628
+ }
629
+
630
+ async #collectBuiltInModelManagerOptions(): Promise<ModelManagerOptions<Api>[]> {
631
+ const [
632
+ anthropicApiKey,
633
+ openaiApiKey,
634
+ groqApiKey,
635
+ cerebrasApiKey,
636
+ xaiApiKey,
637
+ mistralApiKey,
638
+ opencodeApiKey,
639
+ openrouterApiKey,
640
+ vercelGatewayApiKey,
641
+ kimiApiKey,
642
+ githubCopilotApiKey,
643
+ googleApiKey,
644
+ cursorApiKey,
645
+ googleAntigravityApiKey,
646
+ googleGeminiCliApiKey,
647
+ codexAccessToken,
648
+ ] = await Promise.all([
649
+ this.getApiKeyForProvider("anthropic"),
650
+ this.getApiKeyForProvider("openai"),
651
+ this.getApiKeyForProvider("groq"),
652
+ this.getApiKeyForProvider("cerebras"),
653
+ this.getApiKeyForProvider("xai"),
654
+ this.getApiKeyForProvider("mistral"),
655
+ this.getApiKeyForProvider("opencode"),
656
+ this.getApiKeyForProvider("openrouter"),
657
+ this.getApiKeyForProvider("vercel-ai-gateway"),
658
+ this.getApiKeyForProvider("kimi-code"),
659
+ this.getApiKeyForProvider("github-copilot"),
660
+ this.getApiKeyForProvider("google"),
661
+ this.getApiKeyForProvider("cursor"),
662
+ this.getApiKeyForProvider("google-antigravity"),
663
+ this.getApiKeyForProvider("google-gemini-cli"),
664
+ this.getApiKeyForProvider("openai-codex"),
665
+ ]);
666
+
667
+ const options: ModelManagerOptions<Api>[] = [];
668
+ if (isAuthenticated(anthropicApiKey)) {
669
+ options.push(
670
+ anthropicModelManagerOptions({
671
+ apiKey: anthropicApiKey,
672
+ baseUrl: this.getProviderBaseUrl("anthropic"),
673
+ }),
674
+ );
675
+ }
676
+ if (isAuthenticated(openaiApiKey)) {
677
+ options.push(
678
+ openaiModelManagerOptions({
679
+ apiKey: openaiApiKey,
680
+ baseUrl: this.getProviderBaseUrl("openai"),
681
+ }),
682
+ );
683
+ }
684
+ if (isAuthenticated(groqApiKey)) {
685
+ options.push(
686
+ groqModelManagerOptions({
687
+ apiKey: groqApiKey,
688
+ baseUrl: this.getProviderBaseUrl("groq"),
689
+ }),
690
+ );
691
+ }
692
+ if (isAuthenticated(cerebrasApiKey)) {
693
+ options.push(
694
+ cerebrasModelManagerOptions({
695
+ apiKey: cerebrasApiKey,
696
+ baseUrl: this.getProviderBaseUrl("cerebras"),
697
+ }),
698
+ );
699
+ }
700
+ if (isAuthenticated(xaiApiKey)) {
701
+ options.push(
702
+ xaiModelManagerOptions({
703
+ apiKey: xaiApiKey,
704
+ baseUrl: this.getProviderBaseUrl("xai"),
705
+ }),
706
+ );
707
+ }
708
+ if (isAuthenticated(mistralApiKey)) {
709
+ options.push(
710
+ mistralModelManagerOptions({
711
+ apiKey: mistralApiKey,
712
+ baseUrl: this.getProviderBaseUrl("mistral"),
713
+ }),
714
+ );
715
+ }
716
+ if (isAuthenticated(opencodeApiKey)) {
717
+ options.push(
718
+ opencodeModelManagerOptions({
719
+ apiKey: opencodeApiKey,
720
+ baseUrl: this.getProviderBaseUrl("opencode"),
721
+ }),
722
+ );
723
+ }
724
+ if (isAuthenticated(openrouterApiKey)) {
725
+ options.push(
726
+ openrouterModelManagerOptions({
727
+ apiKey: openrouterApiKey,
728
+ baseUrl: this.getProviderBaseUrl("openrouter"),
729
+ }),
730
+ );
731
+ }
732
+ if (isAuthenticated(vercelGatewayApiKey)) {
733
+ options.push(
734
+ vercelAiGatewayModelManagerOptions({
735
+ apiKey: vercelGatewayApiKey,
736
+ baseUrl: this.getProviderBaseUrl("vercel-ai-gateway"),
737
+ }),
738
+ );
739
+ }
740
+ if (isAuthenticated(kimiApiKey)) {
741
+ options.push(
742
+ kimiCodeModelManagerOptions({
743
+ apiKey: kimiApiKey,
744
+ baseUrl: this.getProviderBaseUrl("kimi-code"),
745
+ }),
746
+ );
747
+ }
748
+ if (isAuthenticated(githubCopilotApiKey)) {
749
+ options.push(
750
+ githubCopilotModelManagerOptions({
751
+ apiKey: githubCopilotApiKey,
752
+ baseUrl: this.getProviderBaseUrl("github-copilot"),
753
+ }),
754
+ );
755
+ }
756
+ if (isAuthenticated(googleApiKey)) options.push(googleModelManagerOptions({ apiKey: googleApiKey }));
757
+ if (isAuthenticated(cursorApiKey)) {
758
+ options.push(
759
+ cursorModelManagerOptions({
760
+ apiKey: cursorApiKey,
761
+ baseUrl: this.getProviderBaseUrl("cursor"),
762
+ }),
763
+ );
764
+ }
765
+
766
+ const antigravityToken = extractGoogleOAuthToken(googleAntigravityApiKey);
767
+ if (isAuthenticated(antigravityToken)) {
768
+ options.push(
769
+ googleAntigravityModelManagerOptions({
770
+ oauthToken: antigravityToken,
771
+ endpoint: this.getProviderBaseUrl("google-antigravity"),
772
+ }),
773
+ );
774
+ }
775
+
776
+ const geminiCliToken = extractGoogleOAuthToken(googleGeminiCliApiKey);
777
+ if (isAuthenticated(geminiCliToken)) {
778
+ options.push(
779
+ googleGeminiCliModelManagerOptions({
780
+ oauthToken: geminiCliToken,
781
+ endpoint: this.getProviderBaseUrl("google-gemini-cli"),
782
+ }),
783
+ );
784
+ }
785
+
786
+ if (isAuthenticated(codexAccessToken)) {
787
+ const codexAccountId = resolveOAuthAccountIdForAccessToken(this.authStorage, "openai-codex", codexAccessToken);
788
+ options.push(
789
+ openaiCodexModelManagerOptions({
790
+ accessToken: codexAccessToken,
791
+ accountId: codexAccountId,
792
+ }),
793
+ );
794
+ }
795
+
796
+ return options;
797
+ }
798
+
799
+ async #discoverWithModelManager(options: ModelManagerOptions<Api>): Promise<Model<Api>[]> {
800
+ try {
801
+ const manager = createModelManager(options);
802
+ const result = await manager.refresh("online");
803
+ return result.models;
804
+ } catch (error) {
805
+ logger.warn("model discovery failed for provider", {
806
+ provider: options.providerId,
807
+ error: error instanceof Error ? error.message : String(error),
808
+ });
809
+ return [];
810
+ }
811
+ }
812
+
534
813
  async #discoverOllamaModels(providerConfig: DiscoveryProviderConfig): Promise<Model<Api>[]> {
535
814
  const endpoint = this.#normalizeOllamaBaseUrl(providerConfig.baseUrl);
536
815
  const tagsUrl = `${endpoint}/api/tags`;
@@ -698,7 +977,7 @@ export class ModelRegistry {
698
977
  */
699
978
  async getApiKey(model: Model<Api>, sessionId?: string): Promise<string | undefined> {
700
979
  if (this.#keylessProviders.has(model.provider)) {
701
- return "<no-auth>";
980
+ return kNoAuth;
702
981
  }
703
982
  return this.authStorage.getApiKey(model.provider, sessionId, { baseUrl: model.baseUrl });
704
983
  }
@@ -708,7 +987,7 @@ export class ModelRegistry {
708
987
  */
709
988
  async getApiKeyForProvider(provider: string, sessionId?: string, baseUrl?: string): Promise<string | undefined> {
710
989
  if (this.#keylessProviders.has(provider)) {
711
- return "<no-auth>";
990
+ return kNoAuth;
712
991
  }
713
992
  return this.authStorage.getApiKey(provider, sessionId, { baseUrl });
714
993
  }
@@ -12,7 +12,7 @@ import type { Settings } from "./settings";
12
12
  /** Default model IDs for each known provider */
13
13
  export const defaultModelPerProvider: Record<KnownProvider, string> = {
14
14
  "amazon-bedrock": "us.anthropic.claude-opus-4-6-v1",
15
- anthropic: "claude-opus-4-6",
15
+ anthropic: "claude-sonnet-4-6",
16
16
  openai: "gpt-5.1-codex",
17
17
  "openai-codex": "gpt-5.3-codex",
18
18
  google: "gemini-2.5-pro",
@@ -20,9 +20,9 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
20
20
  "google-antigravity": "gemini-3-pro-high",
21
21
  "google-vertex": "gemini-3-pro-preview",
22
22
  "github-copilot": "gpt-4o",
23
- cursor: "claude-opus-4-6",
23
+ cursor: "claude-sonnet-4-6",
24
24
  openrouter: "openai/gpt-5.1-codex",
25
- "vercel-ai-gateway": "anthropic/claude-opus-4-6",
25
+ "vercel-ai-gateway": "anthropic/claude-sonnet-4-6",
26
26
  xai: "grok-4-fast-non-reasoning",
27
27
  groq: "openai/gpt-oss-120b",
28
28
  cerebras: "zai-glm-4.6",
@@ -31,7 +31,7 @@ export const defaultModelPerProvider: Record<KnownProvider, string> = {
31
31
  minimax: "MiniMax-M2.5",
32
32
  "minimax-code": "MiniMax-M2.5",
33
33
  "minimax-code-cn": "MiniMax-M2.5",
34
- opencode: "claude-opus-4-6",
34
+ opencode: "claude-sonnet-4-6",
35
35
  "kimi-code": "kimi-k2.5",
36
36
  };
37
37
 
@@ -889,6 +889,17 @@ export const SETTINGS_SCHEMA = {
889
889
  default: "discard",
890
890
  ui: { tab: "ttsr", label: "TTSR context mode", description: "What to do with partial output when TTSR triggers" },
891
891
  },
892
+ "ttsr.interruptMode": {
893
+ type: "enum",
894
+ values: ["never", "prose-only", "tool-only", "always"] as const,
895
+ default: "always",
896
+ ui: {
897
+ tab: "ttsr",
898
+ label: "TTSR interrupt mode",
899
+ description: "When to interrupt mid-stream vs inject warning after completion",
900
+ submenu: true,
901
+ },
902
+ },
892
903
  "ttsr.repeatMode": {
893
904
  type: "enum",
894
905
  values: ["once", "after-gap"] as const,
@@ -1107,6 +1118,7 @@ export interface CommitSettings {
1107
1118
  export interface TtsrSettings {
1108
1119
  enabled: boolean;
1109
1120
  contextMode: "discard" | "keep";
1121
+ interruptMode: "never" | "prose-only" | "tool-only" | "always";
1110
1122
  repeatMode: "once" | "after-gap";
1111
1123
  repeatGap: number;
1112
1124
  }
@@ -1,36 +1,199 @@
1
1
  /**
2
2
  * Agents (standard) Provider
3
3
  *
4
- * Loads user-level skills from ~/.agents/skills.
4
+ * Loads user-level skills, rules, prompts, commands, context files, and system prompts from ~/.agent/.
5
5
  */
6
6
  import * as path from "node:path";
7
7
  import { registerProvider } from "../capability";
8
+ import { type ContextFile, contextFileCapability } from "../capability/context-file";
9
+ import { readFile } from "../capability/fs";
10
+ import { type Prompt, promptCapability } from "../capability/prompt";
11
+ import { type Rule, ruleCapability } from "../capability/rule";
8
12
  import { type Skill, skillCapability } from "../capability/skill";
13
+ import { type SlashCommand, slashCommandCapability } from "../capability/slash-command";
14
+ import { type SystemPrompt, systemPromptCapability } from "../capability/system-prompt";
9
15
  import type { LoadContext, LoadResult } from "../capability/types";
10
- import { loadSkillsFromDir } from "./helpers";
16
+ import { buildRuleFromMarkdown, createSourceMeta, loadFilesFromDir, loadSkillsFromDir } from "./helpers";
11
17
 
12
18
  const PROVIDER_ID = "agents";
13
19
  const DISPLAY_NAME = "Agents (standard)";
14
20
  const PRIORITY = 70;
21
+ const USER_AGENT_DIR_CANDIDATES = [".agent", ".agents"] as const;
15
22
 
16
- async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
17
- const userSkillsDir = path.join(ctx.home, ".agents", "skills");
18
- const result = await loadSkillsFromDir(ctx, {
19
- dir: userSkillsDir,
20
- providerId: PROVIDER_ID,
21
- level: "user",
22
- });
23
+ function getUserAgentPathCandidates(ctx: LoadContext, ...segments: string[]): string[] {
24
+ return USER_AGENT_DIR_CANDIDATES.map(baseDir => path.join(ctx.home, baseDir, ...segments));
25
+ }
23
26
 
27
+ async function loadSkills(ctx: LoadContext): Promise<LoadResult<Skill>> {
28
+ const items: Skill[] = [];
29
+ const warnings: string[] = [];
30
+ for (const userSkillsDir of getUserAgentPathCandidates(ctx, "skills")) {
31
+ const result = await loadSkillsFromDir(ctx, {
32
+ dir: userSkillsDir,
33
+ providerId: PROVIDER_ID,
34
+ level: "user",
35
+ });
36
+ items.push(...result.items);
37
+ warnings.push(...(result.warnings ?? []));
38
+ }
24
39
  return {
25
- items: result.items,
26
- warnings: result.warnings ?? [],
40
+ items,
41
+ warnings,
27
42
  };
28
43
  }
29
44
 
30
45
  registerProvider<Skill>(skillCapability.id, {
31
46
  id: PROVIDER_ID,
32
47
  displayName: DISPLAY_NAME,
33
- description: "Load skills from ~/.agents/skills",
48
+ description: "Load skills from ~/.agent/skills (fallback ~/.agents/skills)",
34
49
  priority: PRIORITY,
35
50
  load: loadSkills,
36
51
  });
52
+
53
+ // Rules
54
+ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
55
+ const items: Rule[] = [];
56
+ const warnings: string[] = [];
57
+ for (const userRulesDir of getUserAgentPathCandidates(ctx, "rules")) {
58
+ const result = await loadFilesFromDir<Rule>(ctx, userRulesDir, PROVIDER_ID, "user", {
59
+ extensions: ["md", "mdc"],
60
+ transform: (name, content, filePath, source) =>
61
+ buildRuleFromMarkdown(name, content, filePath, source, { stripNamePattern: /\.(md|mdc)$/ }),
62
+ });
63
+ items.push(...result.items);
64
+ warnings.push(...(result.warnings ?? []));
65
+ }
66
+ return {
67
+ items,
68
+ warnings,
69
+ };
70
+ }
71
+
72
+ registerProvider<Rule>(ruleCapability.id, {
73
+ id: PROVIDER_ID,
74
+ displayName: DISPLAY_NAME,
75
+ description: "Load rules from ~/.agent/rules (fallback ~/.agents/rules)",
76
+ priority: PRIORITY,
77
+ load: loadRules,
78
+ });
79
+
80
+ // Prompts
81
+ async function loadPrompts(ctx: LoadContext): Promise<LoadResult<Prompt>> {
82
+ const items: Prompt[] = [];
83
+ const warnings: string[] = [];
84
+ for (const userPromptsDir of getUserAgentPathCandidates(ctx, "prompts")) {
85
+ const result = await loadFilesFromDir<Prompt>(ctx, userPromptsDir, PROVIDER_ID, "user", {
86
+ extensions: ["md"],
87
+ transform: (name, content, filePath, source) => ({
88
+ name: name.replace(/\.md$/, ""),
89
+ path: filePath,
90
+ content,
91
+ _source: source,
92
+ }),
93
+ });
94
+ items.push(...result.items);
95
+ warnings.push(...(result.warnings ?? []));
96
+ }
97
+ return {
98
+ items,
99
+ warnings,
100
+ };
101
+ }
102
+
103
+ registerProvider<Prompt>(promptCapability.id, {
104
+ id: PROVIDER_ID,
105
+ displayName: DISPLAY_NAME,
106
+ description: "Load prompts from ~/.agent/prompts (fallback ~/.agents/prompts)",
107
+ priority: PRIORITY,
108
+ load: loadPrompts,
109
+ });
110
+
111
+ // Slash Commands
112
+ async function loadSlashCommands(ctx: LoadContext): Promise<LoadResult<SlashCommand>> {
113
+ const items: SlashCommand[] = [];
114
+ const warnings: string[] = [];
115
+ for (const userCommandsDir of getUserAgentPathCandidates(ctx, "commands")) {
116
+ const result = await loadFilesFromDir<SlashCommand>(ctx, userCommandsDir, PROVIDER_ID, "user", {
117
+ extensions: ["md"],
118
+ transform: (name, content, filePath, source) => ({
119
+ name: name.replace(/\.md$/, ""),
120
+ path: filePath,
121
+ content,
122
+ level: "user",
123
+ _source: source,
124
+ }),
125
+ });
126
+ items.push(...result.items);
127
+ warnings.push(...(result.warnings ?? []));
128
+ }
129
+ return {
130
+ items,
131
+ warnings,
132
+ };
133
+ }
134
+
135
+ registerProvider<SlashCommand>(slashCommandCapability.id, {
136
+ id: PROVIDER_ID,
137
+ displayName: DISPLAY_NAME,
138
+ description: "Load commands from ~/.agent/commands (fallback ~/.agents/commands)",
139
+ priority: PRIORITY,
140
+ load: loadSlashCommands,
141
+ });
142
+
143
+ // Context Files (AGENTS.md)
144
+ async function loadContextFiles(ctx: LoadContext): Promise<LoadResult<ContextFile>> {
145
+ const items: ContextFile[] = [];
146
+ for (const agentsPath of getUserAgentPathCandidates(ctx, "AGENTS.md")) {
147
+ const content = await readFile(agentsPath);
148
+ if (!content) {
149
+ continue;
150
+ }
151
+ items.push({
152
+ path: agentsPath,
153
+ content,
154
+ level: "user",
155
+ _source: createSourceMeta(PROVIDER_ID, agentsPath, "user"),
156
+ });
157
+ }
158
+ return {
159
+ items,
160
+ warnings: [],
161
+ };
162
+ }
163
+
164
+ registerProvider<ContextFile>(contextFileCapability.id, {
165
+ id: PROVIDER_ID,
166
+ displayName: DISPLAY_NAME,
167
+ description: "Load AGENTS.md from ~/.agent (fallback ~/.agents)",
168
+ priority: PRIORITY,
169
+ load: loadContextFiles,
170
+ });
171
+
172
+ // System Prompt (SYSTEM.md)
173
+ async function loadSystemPrompt(ctx: LoadContext): Promise<LoadResult<SystemPrompt>> {
174
+ const items: SystemPrompt[] = [];
175
+ for (const systemPath of getUserAgentPathCandidates(ctx, "SYSTEM.md")) {
176
+ const content = await readFile(systemPath);
177
+ if (!content) {
178
+ continue;
179
+ }
180
+ items.push({
181
+ path: systemPath,
182
+ content,
183
+ level: "user",
184
+ _source: createSourceMeta(PROVIDER_ID, systemPath, "user"),
185
+ });
186
+ }
187
+ return {
188
+ items,
189
+ warnings: [],
190
+ };
191
+ }
192
+
193
+ registerProvider<SystemPrompt>(systemPromptCapability.id, {
194
+ id: PROVIDER_ID,
195
+ displayName: DISPLAY_NAME,
196
+ description: "Load SYSTEM.md from ~/.agent (fallback ~/.agents)",
197
+ priority: PRIORITY,
198
+ load: loadSystemPrompt,
199
+ });
@@ -23,6 +23,7 @@ import { type CustomTool, toolCapability } from "../capability/tool";
23
23
  import type { LoadContext, LoadResult } from "../capability/types";
24
24
  import { parseFrontmatter } from "../utils/frontmatter";
25
25
  import {
26
+ buildRuleFromMarkdown,
26
27
  createSourceMeta,
27
28
  discoverExtensionModulePaths,
28
29
  expandEnvVarsDeep,
@@ -307,19 +308,8 @@ async function loadRules(ctx: LoadContext): Promise<LoadResult<Rule>> {
307
308
  const rulesDir = path.join(dir, "rules");
308
309
  const result = await loadFilesFromDir<Rule>(ctx, rulesDir, PROVIDER_ID, level, {
309
310
  extensions: ["md", "mdc"],
310
- transform: (name, content, path, source) => {
311
- const { frontmatter, body } = parseFrontmatter(content, { source: path });
312
- return {
313
- name: name.replace(/\.(md|mdc)$/, ""),
314
- path,
315
- content: body,
316
- globs: frontmatter.globs as string[] | undefined,
317
- alwaysApply: frontmatter.alwaysApply as boolean | undefined,
318
- description: frontmatter.description as string | undefined,
319
- ttsrTrigger: typeof frontmatter.ttsr_trigger === "string" ? frontmatter.ttsr_trigger : undefined,
320
- _source: source,
321
- };
322
- },
311
+ transform: (name, content, path, source) =>
312
+ buildRuleFromMarkdown(name, content, path, source, { stripNamePattern: /\.(md|mdc)$/ }),
323
313
  });
324
314
  items.push(...result.items);
325
315
  if (result.warnings) warnings.push(...result.warnings);