@dyyz1993/pi-coding-agent 0.70.5 → 0.74.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/CHANGELOG.md +266 -80
- package/README.md +48 -20
- package/dist/bun/cli.d.ts.map +1 -1
- package/dist/bun/cli.js +4 -2
- package/dist/bun/cli.js.map +1 -1
- package/dist/bun/restore-sandbox-env.d.ts +13 -0
- package/dist/bun/restore-sandbox-env.d.ts.map +1 -0
- package/dist/bun/restore-sandbox-env.js +32 -0
- package/dist/bun/restore-sandbox-env.js.map +1 -0
- package/dist/cli/args.d.ts +2 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +34 -22
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/list-models.d.ts.map +1 -1
- package/dist/cli/list-models.js +2 -1
- package/dist/cli/list-models.js.map +1 -1
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +9 -4
- package/dist/cli.js.map +1 -1
- package/dist/config.d.ts +16 -8
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +238 -66
- package/dist/config.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +10 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +14 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/agent-session-services.d.ts +2 -1
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +1 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +25 -26
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +1042 -1116
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-types.d.ts +58 -0
- package/dist/core/agent-types.d.ts.map +1 -0
- package/dist/core/agent-types.js +203 -0
- package/dist/core/agent-types.js.map +1 -0
- package/dist/core/auth-guidance.d.ts +5 -0
- package/dist/core/auth-guidance.d.ts.map +1 -0
- package/dist/core/auth-guidance.js +21 -0
- package/dist/core/auth-guidance.js.map +1 -0
- package/dist/core/auth-storage.d.ts +9 -0
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +20 -1
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/bash-executor.d.ts.map +1 -1
- package/dist/core/bash-executor.js +9 -6
- package/dist/core/bash-executor.js.map +1 -1
- package/dist/core/compaction/compaction.d.ts +0 -1
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/export-html/ansi-to-html.d.ts.map +1 -1
- package/dist/core/export-html/ansi-to-html.js +1 -1
- package/dist/core/export-html/ansi-to-html.js.map +1 -1
- package/dist/core/export-html/template.css +53 -4
- package/dist/core/export-html/template.js +84 -20
- package/dist/core/export-html/tool-renderer.d.ts +0 -6
- package/dist/core/export-html/tool-renderer.d.ts.map +1 -1
- package/dist/core/export-html/tool-renderer.js +15 -2
- package/dist/core/export-html/tool-renderer.js.map +1 -1
- package/dist/core/extensions/channel-factory.d.ts +13 -0
- package/dist/core/extensions/channel-factory.d.ts.map +1 -0
- package/dist/core/extensions/channel-factory.js +19 -0
- package/dist/core/extensions/channel-factory.js.map +1 -0
- package/dist/core/extensions/channel-registry.d.ts +28 -0
- package/dist/core/extensions/channel-registry.d.ts.map +1 -0
- package/dist/core/extensions/channel-registry.js +12 -0
- package/dist/core/extensions/channel-registry.js.map +1 -0
- package/dist/core/extensions/index.d.ts +4 -1
- package/dist/core/extensions/index.d.ts.map +1 -1
- package/dist/core/extensions/index.js +1 -0
- package/dist/core/extensions/index.js.map +1 -1
- package/dist/core/extensions/loader.d.ts +0 -1
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +49 -137
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/extensions/runner.d.ts +24 -20
- package/dist/core/extensions/runner.d.ts.map +1 -1
- package/dist/core/extensions/runner.js +128 -253
- package/dist/core/extensions/runner.js.map +1 -1
- package/dist/core/extensions/server-channel.d.ts +8 -8
- package/dist/core/extensions/server-channel.d.ts.map +1 -1
- package/dist/core/extensions/server-channel.js.map +1 -1
- package/dist/core/extensions/types.d.ts +88 -60
- package/dist/core/extensions/types.d.ts.map +1 -1
- package/dist/core/extensions/types.js +10 -0
- package/dist/core/extensions/types.js.map +1 -1
- package/dist/core/file-store/file-snapshot-manager.d.ts +95 -0
- package/dist/core/file-store/file-snapshot-manager.d.ts.map +1 -0
- package/dist/core/file-store/file-snapshot-manager.js +508 -0
- package/dist/core/file-store/file-snapshot-manager.js.map +1 -0
- package/dist/core/file-store/index.d.ts +5 -0
- package/dist/core/file-store/index.d.ts.map +1 -0
- package/dist/core/file-store/index.js +3 -0
- package/dist/core/file-store/index.js.map +1 -0
- package/dist/core/messages.d.ts +10 -2
- package/dist/core/messages.d.ts.map +1 -1
- package/dist/core/messages.js +23 -6
- package/dist/core/messages.js.map +1 -1
- package/dist/core/model-registry.d.ts +19 -1
- package/dist/core/model-registry.d.ts.map +1 -1
- package/dist/core/model-registry.js +97 -16
- package/dist/core/model-registry.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +24 -15
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/package-manager.d.ts +1 -0
- package/dist/core/package-manager.d.ts.map +1 -1
- package/dist/core/package-manager.js +61 -35
- package/dist/core/package-manager.js.map +1 -1
- package/dist/core/provider-display-names.d.ts +2 -0
- package/dist/core/provider-display-names.d.ts.map +1 -0
- package/dist/core/provider-display-names.js +32 -0
- package/dist/core/provider-display-names.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +9 -21
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/sdk.d.ts +9 -1
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +39 -18
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-manager.d.ts +27 -17
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +133 -47
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/settings-manager.d.ts +21 -3
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +51 -6
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/skills.d.ts.map +1 -1
- package/dist/core/skills.js +3 -8
- package/dist/core/skills.js.map +1 -1
- package/dist/core/slash-commands.d.ts.map +1 -1
- package/dist/core/slash-commands.js +4 -3
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/core/tools/bash.d.ts +0 -2
- package/dist/core/tools/bash.d.ts.map +1 -1
- package/dist/core/tools/bash.js +108 -154
- package/dist/core/tools/bash.js.map +1 -1
- package/dist/core/tools/edit-diff.d.ts.map +1 -1
- package/dist/core/tools/edit-diff.js +3 -2
- package/dist/core/tools/edit-diff.js.map +1 -1
- package/dist/core/tools/edit.d.ts.map +1 -1
- package/dist/core/tools/edit.js +4 -3
- package/dist/core/tools/edit.js.map +1 -1
- package/dist/core/tools/find.d.ts.map +1 -1
- package/dist/core/tools/find.js +1 -1
- package/dist/core/tools/find.js.map +1 -1
- package/dist/core/tools/grep.d.ts.map +1 -1
- package/dist/core/tools/grep.js +1 -1
- package/dist/core/tools/grep.js.map +1 -1
- package/dist/core/tools/output-accumulator.d.ts +50 -0
- package/dist/core/tools/output-accumulator.d.ts.map +1 -0
- package/dist/core/tools/output-accumulator.js +178 -0
- package/dist/core/tools/output-accumulator.js.map +1 -0
- package/dist/core/tools/output-collector.d.ts +35 -0
- package/dist/core/tools/output-collector.d.ts.map +1 -0
- package/dist/core/tools/output-collector.js +79 -0
- package/dist/core/tools/output-collector.js.map +1 -0
- package/dist/core/tools/read.d.ts.map +1 -1
- package/dist/core/tools/read.js +70 -13
- package/dist/core/tools/read.js.map +1 -1
- package/dist/core/tools/spawn-managed.d.ts +18 -0
- package/dist/core/tools/spawn-managed.d.ts.map +1 -0
- package/dist/core/tools/spawn-managed.js +52 -0
- package/dist/core/tools/spawn-managed.js.map +1 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -2
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +17 -39
- package/dist/main.js.map +1 -1
- package/dist/migrations.d.ts +1 -1
- package/dist/migrations.d.ts.map +1 -1
- package/dist/migrations.js +3 -3
- package/dist/migrations.js.map +1 -1
- package/dist/modes/interactive/components/config-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/config-selector.js +3 -1
- package/dist/modes/interactive/components/config-selector.js.map +1 -1
- package/dist/modes/interactive/components/extension-selector.d.ts +1 -4
- package/dist/modes/interactive/components/extension-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/extension-selector.js +14 -56
- package/dist/modes/interactive/components/extension-selector.js.map +1 -1
- package/dist/modes/interactive/components/login-dialog.d.ts +5 -1
- package/dist/modes/interactive/components/login-dialog.d.ts.map +1 -1
- package/dist/modes/interactive/components/login-dialog.js +19 -4
- package/dist/modes/interactive/components/login-dialog.js.map +1 -1
- package/dist/modes/interactive/components/model-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/model-selector.js +1 -1
- package/dist/modes/interactive/components/model-selector.js.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.d.ts +18 -6
- package/dist/modes/interactive/components/oauth-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/oauth-selector.js +93 -25
- package/dist/modes/interactive/components/oauth-selector.js.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js +1 -1
- package/dist/modes/interactive/components/scoped-models-selector.js.map +1 -1
- package/dist/modes/interactive/components/session-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/session-selector.js +3 -7
- package/dist/modes/interactive/components/session-selector.js.map +1 -1
- package/dist/modes/interactive/components/settings-selector.d.ts +5 -0
- package/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/dist/modes/interactive/components/settings-selector.js +53 -1
- package/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +20 -4
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +423 -186
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/interactive/theme/dark.json +1 -1
- package/dist/modes/interactive/theme/light.json +1 -1
- package/dist/modes/print-mode.d.ts +3 -0
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +62 -19
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-client.d.ts +80 -60
- package/dist/modes/rpc/rpc-client.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client.js +108 -93
- package/dist/modes/rpc/rpc-client.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +106 -0
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +115 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/package-manager-cli.d.ts.map +1 -1
- package/dist/package-manager-cli.js +238 -12
- package/dist/package-manager-cli.js.map +1 -1
- package/dist/utils/child-process.d.ts +1 -0
- package/dist/utils/child-process.d.ts.map +1 -1
- package/dist/utils/child-process.js +8 -0
- package/dist/utils/child-process.js.map +1 -1
- package/dist/utils/clipboard-image.d.ts.map +1 -1
- package/dist/utils/clipboard-image.js +2 -2
- package/dist/utils/clipboard-image.js.map +1 -1
- package/dist/utils/clipboard.d.ts.map +1 -1
- package/dist/utils/clipboard.js +84 -45
- package/dist/utils/clipboard.js.map +1 -1
- package/dist/utils/paths.d.ts +9 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +31 -0
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/pi-user-agent.d.ts +2 -0
- package/dist/utils/pi-user-agent.d.ts.map +1 -0
- package/dist/utils/pi-user-agent.js +5 -0
- package/dist/utils/pi-user-agent.js.map +1 -0
- package/dist/utils/structured-output.d.ts +10 -0
- package/dist/utils/structured-output.d.ts.map +1 -0
- package/dist/utils/structured-output.js +57 -0
- package/dist/utils/structured-output.js.map +1 -0
- package/dist/utils/tools-manager.d.ts.map +1 -1
- package/dist/utils/tools-manager.js +6 -2
- package/dist/utils/tools-manager.js.map +1 -1
- package/dist/utils/version-check.d.ts +14 -0
- package/dist/utils/version-check.d.ts.map +1 -0
- package/dist/utils/version-check.js +77 -0
- package/dist/utils/version-check.js.map +1 -0
- package/docs/compaction.md +14 -14
- package/docs/custom-provider.md +40 -31
- package/docs/development.md +1 -1
- package/docs/docs.json +148 -0
- package/docs/extensions.md +116 -56
- package/docs/index.md +70 -0
- package/docs/json.md +4 -4
- package/docs/models.md +150 -3
- package/docs/packages.md +10 -5
- package/docs/providers.md +62 -17
- package/docs/quickstart.md +142 -0
- package/docs/rollback-architecture.md +693 -0
- package/docs/rollback-test-cases.md +412 -0
- package/docs/rpc.md +1 -1
- package/docs/sdk.md +26 -26
- package/docs/{session.md → session-format.md} +6 -6
- package/docs/sessions.md +137 -0
- package/docs/settings.md +52 -9
- package/docs/termux.md +1 -1
- package/docs/themes.md +2 -2
- package/docs/tui.md +20 -20
- package/docs/usage.md +277 -0
- package/examples/extensions/README.md +2 -4
- package/examples/extensions/border-status-editor.ts +150 -0
- package/examples/extensions/commands.ts +2 -2
- package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/custom-provider-qwen-cli/package.json +1 -1
- package/examples/extensions/dynamic-resources/dynamic.json +1 -1
- package/examples/extensions/git-checkpoint.ts +1 -1
- package/examples/extensions/handoff.ts +49 -11
- package/examples/extensions/plan-mode/index.ts +1 -1
- package/examples/extensions/sandbox/package-lock.json +5 -5
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/subagent/agents.ts +126 -0
- package/examples/extensions/with-deps/package-lock.json +2 -2
- package/examples/extensions/with-deps/package.json +1 -1
- package/examples/sdk/README.md +2 -2
- package/package.json +7 -15
- package/docs/tree.md +0 -233
- package/examples/extensions/antigravity-image-gen.ts +0 -418
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared agent types and discovery logic.
|
|
3
|
+
*
|
|
4
|
+
* Used by multiple extensions (subagent, subagent-v2, agent-permissions)
|
|
5
|
+
* and exported from the public API so extensions don't need cross-plugin imports.
|
|
6
|
+
*/
|
|
7
|
+
export type AgentScope = "user" | "project" | "both";
|
|
8
|
+
export type PermissionMode = "auto" | "acceptEdits" | "plan" | "dontAsk" | "always-allow" | "always-deny";
|
|
9
|
+
export type AgentColor = "red" | "blue" | "green" | "yellow" | "purple" | "orange";
|
|
10
|
+
export type MemoryScope = "user" | "project" | "local";
|
|
11
|
+
export type IsolationMode = "worktree" | "remote";
|
|
12
|
+
export interface AgentHookCommand {
|
|
13
|
+
type: "command";
|
|
14
|
+
command: string;
|
|
15
|
+
if?: string;
|
|
16
|
+
async?: boolean;
|
|
17
|
+
}
|
|
18
|
+
export interface AgentHookPrompt {
|
|
19
|
+
type: "prompt";
|
|
20
|
+
prompt: string;
|
|
21
|
+
if?: string;
|
|
22
|
+
}
|
|
23
|
+
export type AgentHook = AgentHookCommand | AgentHookPrompt;
|
|
24
|
+
export type AgentHooks = Partial<Record<string, AgentHook[]>>;
|
|
25
|
+
export interface AgentConfig {
|
|
26
|
+
name: string;
|
|
27
|
+
description: string;
|
|
28
|
+
tools?: string[];
|
|
29
|
+
disallowedTools?: string[];
|
|
30
|
+
model?: string;
|
|
31
|
+
systemPrompt: string;
|
|
32
|
+
source: AgentSource;
|
|
33
|
+
filePath: string;
|
|
34
|
+
permissionMode?: PermissionMode;
|
|
35
|
+
maxTurns?: number;
|
|
36
|
+
effort?: string;
|
|
37
|
+
color?: AgentColor;
|
|
38
|
+
background?: boolean;
|
|
39
|
+
memory?: MemoryScope;
|
|
40
|
+
isolation?: IsolationMode;
|
|
41
|
+
initialPrompt?: string;
|
|
42
|
+
skills?: string[];
|
|
43
|
+
hooks?: AgentHooks;
|
|
44
|
+
variables?: Record<string, string>;
|
|
45
|
+
}
|
|
46
|
+
export type AgentSource = "builtin" | "plugin" | "user" | "project" | "flag" | "policy";
|
|
47
|
+
export interface AgentDiscoveryResult {
|
|
48
|
+
agents: AgentConfig[];
|
|
49
|
+
projectAgentsDir: string | null;
|
|
50
|
+
}
|
|
51
|
+
export declare function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[];
|
|
52
|
+
export declare function mergeAgentsByPriority(...groups: AgentConfig[][]): AgentConfig[];
|
|
53
|
+
export declare function discoverAgents(cwd: string, scope: AgentScope, overrideAgents?: AgentConfig[]): AgentDiscoveryResult;
|
|
54
|
+
export declare function formatAgentList(agents: AgentConfig[], maxItems: number): {
|
|
55
|
+
text: string;
|
|
56
|
+
remaining: number;
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=agent-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-types.d.ts","sourceRoot":"","sources":["../../src/core/agent-types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAOH,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC;AAErD,MAAM,MAAM,cAAc,GAAG,MAAM,GAAG,aAAa,GAAG,MAAM,GAAG,SAAS,GAAG,cAAc,GAAG,aAAa,CAAC;AAE1G,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAEnF,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,CAAC;AAEvD,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,QAAQ,CAAC;AAElD,MAAM,WAAW,gBAAgB;IAChC,IAAI,EAAE,SAAS,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,OAAO,CAAC;CAChB;AAED,MAAM,WAAW,eAAe;IAC/B,IAAI,EAAE,QAAQ,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,EAAE,CAAC,EAAE,MAAM,CAAC;CACZ;AAED,MAAM,MAAM,SAAS,GAAG,gBAAgB,GAAG,eAAe,CAAC;AAE3D,MAAM,MAAM,UAAU,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;AAE9D,MAAM,WAAW,WAAW;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,EAAE,CAAC;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,WAAW,CAAC;IACpB,QAAQ,EAAE,MAAM,CAAC;IAEjB,cAAc,CAAC,EAAE,cAAc,CAAC;IAChC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,SAAS,CAAC,EAAE,aAAa,CAAC;IAC1B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACnC;AAED,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,QAAQ,CAAC;AAExF,MAAM,WAAW,oBAAoB;IACpC,MAAM,EAAE,WAAW,EAAE,CAAC;IACtB,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;CAChC;AAoFD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,WAAW,EAAE,CAiEjF;AAsBD,wBAAgB,qBAAqB,CAAC,GAAG,MAAM,EAAE,WAAW,EAAE,EAAE,GAAG,WAAW,EAAE,CAQ/E;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,UAAU,EAAE,cAAc,CAAC,EAAE,WAAW,EAAE,GAAG,oBAAoB,CAqBnH;AAED,wBAAgB,eAAe,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,QAAQ,EAAE,MAAM,GAAG;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAQ5G","sourcesContent":["/**\n * Shared agent types and discovery logic.\n *\n * Used by multiple extensions (subagent, subagent-v2, agent-permissions)\n * and exported from the public API so extensions don't need cross-plugin imports.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport { parseFrontmatter } from \"../utils/frontmatter.js\";\n\nexport type AgentScope = \"user\" | \"project\" | \"both\";\n\nexport type PermissionMode = \"auto\" | \"acceptEdits\" | \"plan\" | \"dontAsk\" | \"always-allow\" | \"always-deny\";\n\nexport type AgentColor = \"red\" | \"blue\" | \"green\" | \"yellow\" | \"purple\" | \"orange\";\n\nexport type MemoryScope = \"user\" | \"project\" | \"local\";\n\nexport type IsolationMode = \"worktree\" | \"remote\";\n\nexport interface AgentHookCommand {\n\ttype: \"command\";\n\tcommand: string;\n\tif?: string;\n\tasync?: boolean;\n}\n\nexport interface AgentHookPrompt {\n\ttype: \"prompt\";\n\tprompt: string;\n\tif?: string;\n}\n\nexport type AgentHook = AgentHookCommand | AgentHookPrompt;\n\nexport type AgentHooks = Partial<Record<string, AgentHook[]>>;\n\nexport interface AgentConfig {\n\tname: string;\n\tdescription: string;\n\ttools?: string[];\n\tdisallowedTools?: string[];\n\tmodel?: string;\n\tsystemPrompt: string;\n\tsource: AgentSource;\n\tfilePath: string;\n\n\tpermissionMode?: PermissionMode;\n\tmaxTurns?: number;\n\teffort?: string;\n\tcolor?: AgentColor;\n\tbackground?: boolean;\n\tmemory?: MemoryScope;\n\tisolation?: IsolationMode;\n\tinitialPrompt?: string;\n\tskills?: string[];\n\thooks?: AgentHooks;\n\tvariables?: Record<string, string>;\n}\n\nexport type AgentSource = \"builtin\" | \"plugin\" | \"user\" | \"project\" | \"flag\" | \"policy\";\n\nexport interface AgentDiscoveryResult {\n\tagents: AgentConfig[];\n\tprojectAgentsDir: string | null;\n}\n\nconst STRING_FIELDS: ReadonlySet<string> = new Set([\n\t\"description\",\n\t\"model\",\n\t\"permissionMode\",\n\t\"effort\",\n\t\"color\",\n\t\"memory\",\n\t\"isolation\",\n\t\"initialPrompt\",\n]);\n\nconst STRING_ARRAY_FIELDS: ReadonlySet<string> = new Set([\"tools\", \"disallowedTools\", \"skills\"]);\n\nconst BOOLEAN_FIELDS: ReadonlySet<string> = new Set([\"background\"]);\n\nconst NUMBER_FIELDS: ReadonlySet<string> = new Set([\"maxTurns\"]);\n\nfunction coerceField(key: string, raw: unknown): unknown {\n\tif (raw === undefined || raw === null) return undefined;\n\n\tif (STRING_FIELDS.has(key)) {\n\t\treturn typeof raw === \"string\" ? raw : String(raw);\n\t}\n\n\tif (STRING_ARRAY_FIELDS.has(key)) {\n\t\tif (Array.isArray(raw)) return raw.map(String);\n\t\tif (typeof raw === \"string\") {\n\t\t\treturn raw\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((s: string) => s.trim())\n\t\t\t\t.filter(Boolean);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tif (BOOLEAN_FIELDS.has(key)) {\n\t\tif (typeof raw === \"boolean\") return raw;\n\t\tif (typeof raw === \"string\") return raw === \"true\" || raw === \"yes\";\n\t\treturn undefined;\n\t}\n\n\tif (NUMBER_FIELDS.has(key)) {\n\t\tif (typeof raw === \"number\") return raw;\n\t\tif (typeof raw === \"string\") {\n\t\t\tconst n = Number.parseInt(raw, 10);\n\t\t\treturn Number.isFinite(n) ? n : undefined;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\treturn raw;\n}\n\nfunction parseHooks(raw: unknown): AgentHooks | undefined {\n\tif (!raw || typeof raw !== \"object\") return undefined;\n\tconst hooks: AgentHooks = {};\n\tfor (const [event, handlers] of Object.entries(raw as Record<string, unknown>)) {\n\t\tif (!Array.isArray(handlers)) continue;\n\t\tconst parsed: AgentHook[] = [];\n\t\tfor (const h of handlers) {\n\t\t\tif (!h || typeof h !== \"object\") continue;\n\t\t\tconst obj = h as Record<string, unknown>;\n\t\t\tif (obj.type === \"command\" && typeof obj.command === \"string\") {\n\t\t\t\tparsed.push({\n\t\t\t\t\ttype: \"command\",\n\t\t\t\t\tcommand: obj.command,\n\t\t\t\t\tif: typeof obj.if === \"string\" ? obj.if : undefined,\n\t\t\t\t\tasync: obj.async === true,\n\t\t\t\t});\n\t\t\t} else if (obj.type === \"prompt\" && typeof obj.prompt === \"string\") {\n\t\t\t\tparsed.push({\n\t\t\t\t\ttype: \"prompt\",\n\t\t\t\t\tprompt: obj.prompt,\n\t\t\t\t\tif: typeof obj.if === \"string\" ? obj.if : undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (parsed.length > 0) hooks[event] = parsed;\n\t}\n\treturn Object.keys(hooks).length > 0 ? hooks : undefined;\n}\n\nexport function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {\n\tconst agents: AgentConfig[] = [];\n\n\tif (!fs.existsSync(dir)) {\n\t\treturn agents;\n\t}\n\n\tlet entries: fs.Dirent[];\n\ttry {\n\t\tentries = fs.readdirSync(dir, { withFileTypes: true });\n\t} catch {\n\t\treturn agents;\n\t}\n\n\tfor (const entry of entries) {\n\t\tif (!entry.name.endsWith(\".md\")) continue;\n\t\tif (!entry.isFile() && !entry.isSymbolicLink()) continue;\n\n\t\tconst filePath = path.join(dir, entry.name);\n\t\tlet content: string;\n\t\ttry {\n\t\t\tcontent = fs.readFileSync(filePath, \"utf-8\");\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);\n\n\t\tif (!frontmatter.name || !frontmatter.description) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst tools = coerceField(\"tools\", frontmatter.tools) as string[] | undefined;\n\t\tconst disallowedTools = coerceField(\"disallowedTools\", frontmatter.disallowedTools) as string[] | undefined;\n\t\tconst skills = coerceField(\"skills\", frontmatter.skills) as string[] | undefined;\n\t\tconst hooks = parseHooks(frontmatter.hooks);\n\t\tconst variables =\n\t\t\tfrontmatter.variables && typeof frontmatter.variables === \"object\"\n\t\t\t\t? (frontmatter.variables as Record<string, string>)\n\t\t\t\t: undefined;\n\n\t\tagents.push({\n\t\t\tname: coerceField(\"name\", frontmatter.name) as string,\n\t\t\tdescription: coerceField(\"description\", frontmatter.description) as string,\n\t\t\ttools: tools && tools.length > 0 ? tools : undefined,\n\t\t\tdisallowedTools: disallowedTools && disallowedTools.length > 0 ? disallowedTools : undefined,\n\t\t\tmodel: coerceField(\"model\", frontmatter.model) as string | undefined,\n\t\t\tsystemPrompt: body,\n\t\t\tsource,\n\t\t\tfilePath,\n\t\t\tpermissionMode: coerceField(\"permissionMode\", frontmatter.permissionMode) as PermissionMode | undefined,\n\t\t\tmaxTurns: coerceField(\"maxTurns\", frontmatter.maxTurns) as number | undefined,\n\t\t\teffort: coerceField(\"effort\", frontmatter.effort) as string | undefined,\n\t\t\tcolor: coerceField(\"color\", frontmatter.color) as AgentColor | undefined,\n\t\t\tbackground: coerceField(\"background\", frontmatter.background) as boolean | undefined,\n\t\t\tmemory: coerceField(\"memory\", frontmatter.memory) as MemoryScope | undefined,\n\t\t\tisolation: coerceField(\"isolation\", frontmatter.isolation) as IsolationMode | undefined,\n\t\t\tinitialPrompt: coerceField(\"initialPrompt\", frontmatter.initialPrompt) as string | undefined,\n\t\t\tskills: skills && skills.length > 0 ? skills : undefined,\n\t\t\thooks,\n\t\t\tvariables,\n\t\t});\n\t}\n\n\treturn agents;\n}\n\nfunction isDirectory(p: string): boolean {\n\ttry {\n\t\treturn fs.statSync(p).isDirectory();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction findNearestProjectAgentsDir(cwd: string): string | null {\n\tlet currentDir = cwd;\n\twhile (true) {\n\t\tconst candidate = path.join(currentDir, \".pi\", \"agents\");\n\t\tif (isDirectory(candidate)) return candidate;\n\n\t\tconst parentDir = path.dirname(currentDir);\n\t\tif (parentDir === currentDir) return null;\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport function mergeAgentsByPriority(...groups: AgentConfig[][]): AgentConfig[] {\n\tconst agentMap = new Map<string, AgentConfig>();\n\tfor (const group of groups) {\n\t\tfor (const agent of group) {\n\t\t\tagentMap.set(agent.name, agent);\n\t\t}\n\t}\n\treturn Array.from(agentMap.values());\n}\n\nexport function discoverAgents(cwd: string, scope: AgentScope, overrideAgents?: AgentConfig[]): AgentDiscoveryResult {\n\tconst userDir = path.join(getAgentDir(), \"agents\");\n\tconst projectAgentsDir = findNearestProjectAgentsDir(cwd);\n\n\tconst builtinAgents: AgentConfig[] = [];\n\tconst pluginAgents: AgentConfig[] = [];\n\tconst userAgents = scope === \"project\" ? [] : loadAgentsFromDir(userDir, \"user\");\n\tconst projectAgents = scope === \"user\" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, \"project\");\n\tconst flagAgents = overrideAgents ?? [];\n\tconst policyAgents: AgentConfig[] = [];\n\n\tconst agents = mergeAgentsByPriority(\n\t\tbuiltinAgents,\n\t\tpluginAgents,\n\t\tuserAgents,\n\t\tprojectAgents,\n\t\tflagAgents,\n\t\tpolicyAgents,\n\t);\n\n\treturn { agents, projectAgentsDir };\n}\n\nexport function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {\n\tif (agents.length === 0) return { text: \"none\", remaining: 0 };\n\tconst listed = agents.slice(0, maxItems);\n\tconst remaining = agents.length - listed.length;\n\treturn {\n\t\ttext: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join(\"; \"),\n\t\tremaining,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared agent types and discovery logic.
|
|
3
|
+
*
|
|
4
|
+
* Used by multiple extensions (subagent, subagent-v2, agent-permissions)
|
|
5
|
+
* and exported from the public API so extensions don't need cross-plugin imports.
|
|
6
|
+
*/
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
import * as path from "path";
|
|
9
|
+
import { getAgentDir } from "../config.js";
|
|
10
|
+
import { parseFrontmatter } from "../utils/frontmatter.js";
|
|
11
|
+
const STRING_FIELDS = new Set([
|
|
12
|
+
"description",
|
|
13
|
+
"model",
|
|
14
|
+
"permissionMode",
|
|
15
|
+
"effort",
|
|
16
|
+
"color",
|
|
17
|
+
"memory",
|
|
18
|
+
"isolation",
|
|
19
|
+
"initialPrompt",
|
|
20
|
+
]);
|
|
21
|
+
const STRING_ARRAY_FIELDS = new Set(["tools", "disallowedTools", "skills"]);
|
|
22
|
+
const BOOLEAN_FIELDS = new Set(["background"]);
|
|
23
|
+
const NUMBER_FIELDS = new Set(["maxTurns"]);
|
|
24
|
+
function coerceField(key, raw) {
|
|
25
|
+
if (raw === undefined || raw === null)
|
|
26
|
+
return undefined;
|
|
27
|
+
if (STRING_FIELDS.has(key)) {
|
|
28
|
+
return typeof raw === "string" ? raw : String(raw);
|
|
29
|
+
}
|
|
30
|
+
if (STRING_ARRAY_FIELDS.has(key)) {
|
|
31
|
+
if (Array.isArray(raw))
|
|
32
|
+
return raw.map(String);
|
|
33
|
+
if (typeof raw === "string") {
|
|
34
|
+
return raw
|
|
35
|
+
.split(",")
|
|
36
|
+
.map((s) => s.trim())
|
|
37
|
+
.filter(Boolean);
|
|
38
|
+
}
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
if (BOOLEAN_FIELDS.has(key)) {
|
|
42
|
+
if (typeof raw === "boolean")
|
|
43
|
+
return raw;
|
|
44
|
+
if (typeof raw === "string")
|
|
45
|
+
return raw === "true" || raw === "yes";
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
48
|
+
if (NUMBER_FIELDS.has(key)) {
|
|
49
|
+
if (typeof raw === "number")
|
|
50
|
+
return raw;
|
|
51
|
+
if (typeof raw === "string") {
|
|
52
|
+
const n = Number.parseInt(raw, 10);
|
|
53
|
+
return Number.isFinite(n) ? n : undefined;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
return raw;
|
|
58
|
+
}
|
|
59
|
+
function parseHooks(raw) {
|
|
60
|
+
if (!raw || typeof raw !== "object")
|
|
61
|
+
return undefined;
|
|
62
|
+
const hooks = {};
|
|
63
|
+
for (const [event, handlers] of Object.entries(raw)) {
|
|
64
|
+
if (!Array.isArray(handlers))
|
|
65
|
+
continue;
|
|
66
|
+
const parsed = [];
|
|
67
|
+
for (const h of handlers) {
|
|
68
|
+
if (!h || typeof h !== "object")
|
|
69
|
+
continue;
|
|
70
|
+
const obj = h;
|
|
71
|
+
if (obj.type === "command" && typeof obj.command === "string") {
|
|
72
|
+
parsed.push({
|
|
73
|
+
type: "command",
|
|
74
|
+
command: obj.command,
|
|
75
|
+
if: typeof obj.if === "string" ? obj.if : undefined,
|
|
76
|
+
async: obj.async === true,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
else if (obj.type === "prompt" && typeof obj.prompt === "string") {
|
|
80
|
+
parsed.push({
|
|
81
|
+
type: "prompt",
|
|
82
|
+
prompt: obj.prompt,
|
|
83
|
+
if: typeof obj.if === "string" ? obj.if : undefined,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (parsed.length > 0)
|
|
88
|
+
hooks[event] = parsed;
|
|
89
|
+
}
|
|
90
|
+
return Object.keys(hooks).length > 0 ? hooks : undefined;
|
|
91
|
+
}
|
|
92
|
+
export function loadAgentsFromDir(dir, source) {
|
|
93
|
+
const agents = [];
|
|
94
|
+
if (!fs.existsSync(dir)) {
|
|
95
|
+
return agents;
|
|
96
|
+
}
|
|
97
|
+
let entries;
|
|
98
|
+
try {
|
|
99
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
return agents;
|
|
103
|
+
}
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!entry.name.endsWith(".md"))
|
|
106
|
+
continue;
|
|
107
|
+
if (!entry.isFile() && !entry.isSymbolicLink())
|
|
108
|
+
continue;
|
|
109
|
+
const filePath = path.join(dir, entry.name);
|
|
110
|
+
let content;
|
|
111
|
+
try {
|
|
112
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
118
|
+
if (!frontmatter.name || !frontmatter.description) {
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
const tools = coerceField("tools", frontmatter.tools);
|
|
122
|
+
const disallowedTools = coerceField("disallowedTools", frontmatter.disallowedTools);
|
|
123
|
+
const skills = coerceField("skills", frontmatter.skills);
|
|
124
|
+
const hooks = parseHooks(frontmatter.hooks);
|
|
125
|
+
const variables = frontmatter.variables && typeof frontmatter.variables === "object"
|
|
126
|
+
? frontmatter.variables
|
|
127
|
+
: undefined;
|
|
128
|
+
agents.push({
|
|
129
|
+
name: coerceField("name", frontmatter.name),
|
|
130
|
+
description: coerceField("description", frontmatter.description),
|
|
131
|
+
tools: tools && tools.length > 0 ? tools : undefined,
|
|
132
|
+
disallowedTools: disallowedTools && disallowedTools.length > 0 ? disallowedTools : undefined,
|
|
133
|
+
model: coerceField("model", frontmatter.model),
|
|
134
|
+
systemPrompt: body,
|
|
135
|
+
source,
|
|
136
|
+
filePath,
|
|
137
|
+
permissionMode: coerceField("permissionMode", frontmatter.permissionMode),
|
|
138
|
+
maxTurns: coerceField("maxTurns", frontmatter.maxTurns),
|
|
139
|
+
effort: coerceField("effort", frontmatter.effort),
|
|
140
|
+
color: coerceField("color", frontmatter.color),
|
|
141
|
+
background: coerceField("background", frontmatter.background),
|
|
142
|
+
memory: coerceField("memory", frontmatter.memory),
|
|
143
|
+
isolation: coerceField("isolation", frontmatter.isolation),
|
|
144
|
+
initialPrompt: coerceField("initialPrompt", frontmatter.initialPrompt),
|
|
145
|
+
skills: skills && skills.length > 0 ? skills : undefined,
|
|
146
|
+
hooks,
|
|
147
|
+
variables,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
return agents;
|
|
151
|
+
}
|
|
152
|
+
function isDirectory(p) {
|
|
153
|
+
try {
|
|
154
|
+
return fs.statSync(p).isDirectory();
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function findNearestProjectAgentsDir(cwd) {
|
|
161
|
+
let currentDir = cwd;
|
|
162
|
+
while (true) {
|
|
163
|
+
const candidate = path.join(currentDir, ".pi", "agents");
|
|
164
|
+
if (isDirectory(candidate))
|
|
165
|
+
return candidate;
|
|
166
|
+
const parentDir = path.dirname(currentDir);
|
|
167
|
+
if (parentDir === currentDir)
|
|
168
|
+
return null;
|
|
169
|
+
currentDir = parentDir;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export function mergeAgentsByPriority(...groups) {
|
|
173
|
+
const agentMap = new Map();
|
|
174
|
+
for (const group of groups) {
|
|
175
|
+
for (const agent of group) {
|
|
176
|
+
agentMap.set(agent.name, agent);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return Array.from(agentMap.values());
|
|
180
|
+
}
|
|
181
|
+
export function discoverAgents(cwd, scope, overrideAgents) {
|
|
182
|
+
const userDir = path.join(getAgentDir(), "agents");
|
|
183
|
+
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
184
|
+
const builtinAgents = [];
|
|
185
|
+
const pluginAgents = [];
|
|
186
|
+
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
187
|
+
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
188
|
+
const flagAgents = overrideAgents ?? [];
|
|
189
|
+
const policyAgents = [];
|
|
190
|
+
const agents = mergeAgentsByPriority(builtinAgents, pluginAgents, userAgents, projectAgents, flagAgents, policyAgents);
|
|
191
|
+
return { agents, projectAgentsDir };
|
|
192
|
+
}
|
|
193
|
+
export function formatAgentList(agents, maxItems) {
|
|
194
|
+
if (agents.length === 0)
|
|
195
|
+
return { text: "none", remaining: 0 };
|
|
196
|
+
const listed = agents.slice(0, maxItems);
|
|
197
|
+
const remaining = agents.length - listed.length;
|
|
198
|
+
return {
|
|
199
|
+
text: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join("; "),
|
|
200
|
+
remaining,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=agent-types.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-types.js","sourceRoot":"","sources":["../../src/core/agent-types.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared agent types and discovery logic.\n *\n * Used by multiple extensions (subagent, subagent-v2, agent-permissions)\n * and exported from the public API so extensions don't need cross-plugin imports.\n */\n\nimport * as fs from \"node:fs\";\nimport * as path from \"path\";\nimport { getAgentDir } from \"../config.js\";\nimport { parseFrontmatter } from \"../utils/frontmatter.js\";\n\nexport type AgentScope = \"user\" | \"project\" | \"both\";\n\nexport type PermissionMode = \"auto\" | \"acceptEdits\" | \"plan\" | \"dontAsk\" | \"always-allow\" | \"always-deny\";\n\nexport type AgentColor = \"red\" | \"blue\" | \"green\" | \"yellow\" | \"purple\" | \"orange\";\n\nexport type MemoryScope = \"user\" | \"project\" | \"local\";\n\nexport type IsolationMode = \"worktree\" | \"remote\";\n\nexport interface AgentHookCommand {\n\ttype: \"command\";\n\tcommand: string;\n\tif?: string;\n\tasync?: boolean;\n}\n\nexport interface AgentHookPrompt {\n\ttype: \"prompt\";\n\tprompt: string;\n\tif?: string;\n}\n\nexport type AgentHook = AgentHookCommand | AgentHookPrompt;\n\nexport type AgentHooks = Partial<Record<string, AgentHook[]>>;\n\nexport interface AgentConfig {\n\tname: string;\n\tdescription: string;\n\ttools?: string[];\n\tdisallowedTools?: string[];\n\tmodel?: string;\n\tsystemPrompt: string;\n\tsource: AgentSource;\n\tfilePath: string;\n\n\tpermissionMode?: PermissionMode;\n\tmaxTurns?: number;\n\teffort?: string;\n\tcolor?: AgentColor;\n\tbackground?: boolean;\n\tmemory?: MemoryScope;\n\tisolation?: IsolationMode;\n\tinitialPrompt?: string;\n\tskills?: string[];\n\thooks?: AgentHooks;\n\tvariables?: Record<string, string>;\n}\n\nexport type AgentSource = \"builtin\" | \"plugin\" | \"user\" | \"project\" | \"flag\" | \"policy\";\n\nexport interface AgentDiscoveryResult {\n\tagents: AgentConfig[];\n\tprojectAgentsDir: string | null;\n}\n\nconst STRING_FIELDS: ReadonlySet<string> = new Set([\n\t\"description\",\n\t\"model\",\n\t\"permissionMode\",\n\t\"effort\",\n\t\"color\",\n\t\"memory\",\n\t\"isolation\",\n\t\"initialPrompt\",\n]);\n\nconst STRING_ARRAY_FIELDS: ReadonlySet<string> = new Set([\"tools\", \"disallowedTools\", \"skills\"]);\n\nconst BOOLEAN_FIELDS: ReadonlySet<string> = new Set([\"background\"]);\n\nconst NUMBER_FIELDS: ReadonlySet<string> = new Set([\"maxTurns\"]);\n\nfunction coerceField(key: string, raw: unknown): unknown {\n\tif (raw === undefined || raw === null) return undefined;\n\n\tif (STRING_FIELDS.has(key)) {\n\t\treturn typeof raw === \"string\" ? raw : String(raw);\n\t}\n\n\tif (STRING_ARRAY_FIELDS.has(key)) {\n\t\tif (Array.isArray(raw)) return raw.map(String);\n\t\tif (typeof raw === \"string\") {\n\t\t\treturn raw\n\t\t\t\t.split(\",\")\n\t\t\t\t.map((s: string) => s.trim())\n\t\t\t\t.filter(Boolean);\n\t\t}\n\t\treturn undefined;\n\t}\n\n\tif (BOOLEAN_FIELDS.has(key)) {\n\t\tif (typeof raw === \"boolean\") return raw;\n\t\tif (typeof raw === \"string\") return raw === \"true\" || raw === \"yes\";\n\t\treturn undefined;\n\t}\n\n\tif (NUMBER_FIELDS.has(key)) {\n\t\tif (typeof raw === \"number\") return raw;\n\t\tif (typeof raw === \"string\") {\n\t\t\tconst n = Number.parseInt(raw, 10);\n\t\t\treturn Number.isFinite(n) ? n : undefined;\n\t\t}\n\t\treturn undefined;\n\t}\n\n\treturn raw;\n}\n\nfunction parseHooks(raw: unknown): AgentHooks | undefined {\n\tif (!raw || typeof raw !== \"object\") return undefined;\n\tconst hooks: AgentHooks = {};\n\tfor (const [event, handlers] of Object.entries(raw as Record<string, unknown>)) {\n\t\tif (!Array.isArray(handlers)) continue;\n\t\tconst parsed: AgentHook[] = [];\n\t\tfor (const h of handlers) {\n\t\t\tif (!h || typeof h !== \"object\") continue;\n\t\t\tconst obj = h as Record<string, unknown>;\n\t\t\tif (obj.type === \"command\" && typeof obj.command === \"string\") {\n\t\t\t\tparsed.push({\n\t\t\t\t\ttype: \"command\",\n\t\t\t\t\tcommand: obj.command,\n\t\t\t\t\tif: typeof obj.if === \"string\" ? obj.if : undefined,\n\t\t\t\t\tasync: obj.async === true,\n\t\t\t\t});\n\t\t\t} else if (obj.type === \"prompt\" && typeof obj.prompt === \"string\") {\n\t\t\t\tparsed.push({\n\t\t\t\t\ttype: \"prompt\",\n\t\t\t\t\tprompt: obj.prompt,\n\t\t\t\t\tif: typeof obj.if === \"string\" ? obj.if : undefined,\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\t\tif (parsed.length > 0) hooks[event] = parsed;\n\t}\n\treturn Object.keys(hooks).length > 0 ? hooks : undefined;\n}\n\nexport function loadAgentsFromDir(dir: string, source: AgentSource): AgentConfig[] {\n\tconst agents: AgentConfig[] = [];\n\n\tif (!fs.existsSync(dir)) {\n\t\treturn agents;\n\t}\n\n\tlet entries: fs.Dirent[];\n\ttry {\n\t\tentries = fs.readdirSync(dir, { withFileTypes: true });\n\t} catch {\n\t\treturn agents;\n\t}\n\n\tfor (const entry of entries) {\n\t\tif (!entry.name.endsWith(\".md\")) continue;\n\t\tif (!entry.isFile() && !entry.isSymbolicLink()) continue;\n\n\t\tconst filePath = path.join(dir, entry.name);\n\t\tlet content: string;\n\t\ttry {\n\t\t\tcontent = fs.readFileSync(filePath, \"utf-8\");\n\t\t} catch {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst { frontmatter, body } = parseFrontmatter<Record<string, unknown>>(content);\n\n\t\tif (!frontmatter.name || !frontmatter.description) {\n\t\t\tcontinue;\n\t\t}\n\n\t\tconst tools = coerceField(\"tools\", frontmatter.tools) as string[] | undefined;\n\t\tconst disallowedTools = coerceField(\"disallowedTools\", frontmatter.disallowedTools) as string[] | undefined;\n\t\tconst skills = coerceField(\"skills\", frontmatter.skills) as string[] | undefined;\n\t\tconst hooks = parseHooks(frontmatter.hooks);\n\t\tconst variables =\n\t\t\tfrontmatter.variables && typeof frontmatter.variables === \"object\"\n\t\t\t\t? (frontmatter.variables as Record<string, string>)\n\t\t\t\t: undefined;\n\n\t\tagents.push({\n\t\t\tname: coerceField(\"name\", frontmatter.name) as string,\n\t\t\tdescription: coerceField(\"description\", frontmatter.description) as string,\n\t\t\ttools: tools && tools.length > 0 ? tools : undefined,\n\t\t\tdisallowedTools: disallowedTools && disallowedTools.length > 0 ? disallowedTools : undefined,\n\t\t\tmodel: coerceField(\"model\", frontmatter.model) as string | undefined,\n\t\t\tsystemPrompt: body,\n\t\t\tsource,\n\t\t\tfilePath,\n\t\t\tpermissionMode: coerceField(\"permissionMode\", frontmatter.permissionMode) as PermissionMode | undefined,\n\t\t\tmaxTurns: coerceField(\"maxTurns\", frontmatter.maxTurns) as number | undefined,\n\t\t\teffort: coerceField(\"effort\", frontmatter.effort) as string | undefined,\n\t\t\tcolor: coerceField(\"color\", frontmatter.color) as AgentColor | undefined,\n\t\t\tbackground: coerceField(\"background\", frontmatter.background) as boolean | undefined,\n\t\t\tmemory: coerceField(\"memory\", frontmatter.memory) as MemoryScope | undefined,\n\t\t\tisolation: coerceField(\"isolation\", frontmatter.isolation) as IsolationMode | undefined,\n\t\t\tinitialPrompt: coerceField(\"initialPrompt\", frontmatter.initialPrompt) as string | undefined,\n\t\t\tskills: skills && skills.length > 0 ? skills : undefined,\n\t\t\thooks,\n\t\t\tvariables,\n\t\t});\n\t}\n\n\treturn agents;\n}\n\nfunction isDirectory(p: string): boolean {\n\ttry {\n\t\treturn fs.statSync(p).isDirectory();\n\t} catch {\n\t\treturn false;\n\t}\n}\n\nfunction findNearestProjectAgentsDir(cwd: string): string | null {\n\tlet currentDir = cwd;\n\twhile (true) {\n\t\tconst candidate = path.join(currentDir, \".pi\", \"agents\");\n\t\tif (isDirectory(candidate)) return candidate;\n\n\t\tconst parentDir = path.dirname(currentDir);\n\t\tif (parentDir === currentDir) return null;\n\t\tcurrentDir = parentDir;\n\t}\n}\n\nexport function mergeAgentsByPriority(...groups: AgentConfig[][]): AgentConfig[] {\n\tconst agentMap = new Map<string, AgentConfig>();\n\tfor (const group of groups) {\n\t\tfor (const agent of group) {\n\t\t\tagentMap.set(agent.name, agent);\n\t\t}\n\t}\n\treturn Array.from(agentMap.values());\n}\n\nexport function discoverAgents(cwd: string, scope: AgentScope, overrideAgents?: AgentConfig[]): AgentDiscoveryResult {\n\tconst userDir = path.join(getAgentDir(), \"agents\");\n\tconst projectAgentsDir = findNearestProjectAgentsDir(cwd);\n\n\tconst builtinAgents: AgentConfig[] = [];\n\tconst pluginAgents: AgentConfig[] = [];\n\tconst userAgents = scope === \"project\" ? [] : loadAgentsFromDir(userDir, \"user\");\n\tconst projectAgents = scope === \"user\" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, \"project\");\n\tconst flagAgents = overrideAgents ?? [];\n\tconst policyAgents: AgentConfig[] = [];\n\n\tconst agents = mergeAgentsByPriority(\n\t\tbuiltinAgents,\n\t\tpluginAgents,\n\t\tuserAgents,\n\t\tprojectAgents,\n\t\tflagAgents,\n\t\tpolicyAgents,\n\t);\n\n\treturn { agents, projectAgentsDir };\n}\n\nexport function formatAgentList(agents: AgentConfig[], maxItems: number): { text: string; remaining: number } {\n\tif (agents.length === 0) return { text: \"none\", remaining: 0 };\n\tconst listed = agents.slice(0, maxItems);\n\tconst remaining = agents.length - listed.length;\n\treturn {\n\t\ttext: listed.map((a) => `${a.name} (${a.source}): ${a.description}`).join(\"; \"),\n\t\tremaining,\n\t};\n}\n"]}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function getProviderLoginHelp(): string;
|
|
2
|
+
export declare function formatNoModelsAvailableMessage(): string;
|
|
3
|
+
export declare function formatNoModelSelectedMessage(): string;
|
|
4
|
+
export declare function formatNoApiKeyFoundMessage(provider: string): string;
|
|
5
|
+
//# sourceMappingURL=auth-guidance.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-guidance.d.ts","sourceRoot":"","sources":["../../src/core/auth-guidance.ts"],"names":[],"mappings":"AAKA,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAED,wBAAgB,8BAA8B,IAAI,MAAM,CAEvD;AAED,wBAAgB,4BAA4B,IAAI,MAAM,CAErD;AAED,wBAAgB,0BAA0B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAGnE","sourcesContent":["import { join } from \"node:path\";\nimport { getDocsPath } from \"../config.js\";\n\nconst UNKNOWN_PROVIDER = \"unknown\";\n\nexport function getProviderLoginHelp(): string {\n\treturn [\n\t\t\"Use /login to log into a provider via OAuth or API key. See:\",\n\t\t` ${join(getDocsPath(), \"providers.md\")}`,\n\t\t` ${join(getDocsPath(), \"models.md\")}`,\n\t].join(\"\\n\");\n}\n\nexport function formatNoModelsAvailableMessage(): string {\n\treturn `No models available. ${getProviderLoginHelp()}`;\n}\n\nexport function formatNoModelSelectedMessage(): string {\n\treturn `No model selected.\\n\\n${getProviderLoginHelp()}\\n\\nThen use /model to select a model.`;\n}\n\nexport function formatNoApiKeyFoundMessage(provider: string): string {\n\tconst providerDisplay = provider === UNKNOWN_PROVIDER ? \"the selected model\" : provider;\n\treturn `No API key found for ${providerDisplay}.\\n\\n${getProviderLoginHelp()}`;\n}\n"]}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { getDocsPath } from "../config.js";
|
|
3
|
+
const UNKNOWN_PROVIDER = "unknown";
|
|
4
|
+
export function getProviderLoginHelp() {
|
|
5
|
+
return [
|
|
6
|
+
"Use /login to log into a provider via OAuth or API key. See:",
|
|
7
|
+
` ${join(getDocsPath(), "providers.md")}`,
|
|
8
|
+
` ${join(getDocsPath(), "models.md")}`,
|
|
9
|
+
].join("\n");
|
|
10
|
+
}
|
|
11
|
+
export function formatNoModelsAvailableMessage() {
|
|
12
|
+
return `No models available. ${getProviderLoginHelp()}`;
|
|
13
|
+
}
|
|
14
|
+
export function formatNoModelSelectedMessage() {
|
|
15
|
+
return `No model selected.\n\n${getProviderLoginHelp()}\n\nThen use /model to select a model.`;
|
|
16
|
+
}
|
|
17
|
+
export function formatNoApiKeyFoundMessage(provider) {
|
|
18
|
+
const providerDisplay = provider === UNKNOWN_PROVIDER ? "the selected model" : provider;
|
|
19
|
+
return `No API key found for ${providerDisplay}.\n\n${getProviderLoginHelp()}`;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=auth-guidance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth-guidance.js","sourceRoot":"","sources":["../../src/core/auth-guidance.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAE3C,MAAM,gBAAgB,GAAG,SAAS,CAAC;AAEnC,MAAM,UAAU,oBAAoB,GAAW;IAC9C,OAAO;QACN,8DAA8D;QAC9D,KAAK,IAAI,CAAC,WAAW,EAAE,EAAE,cAAc,CAAC,EAAE;QAC1C,KAAK,IAAI,CAAC,WAAW,EAAE,EAAE,WAAW,CAAC,EAAE;KACvC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACb;AAED,MAAM,UAAU,8BAA8B,GAAW;IACxD,OAAO,wBAAwB,oBAAoB,EAAE,EAAE,CAAC;AAAA,CACxD;AAED,MAAM,UAAU,4BAA4B,GAAW;IACtD,OAAO,yBAAyB,oBAAoB,EAAE,wCAAwC,CAAC;AAAA,CAC/F;AAED,MAAM,UAAU,0BAA0B,CAAC,QAAgB,EAAU;IACpE,MAAM,eAAe,GAAG,QAAQ,KAAK,gBAAgB,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,QAAQ,CAAC;IACxF,OAAO,wBAAwB,eAAe,QAAQ,oBAAoB,EAAE,EAAE,CAAC;AAAA,CAC/E","sourcesContent":["import { join } from \"node:path\";\nimport { getDocsPath } from \"../config.js\";\n\nconst UNKNOWN_PROVIDER = \"unknown\";\n\nexport function getProviderLoginHelp(): string {\n\treturn [\n\t\t\"Use /login to log into a provider via OAuth or API key. See:\",\n\t\t` ${join(getDocsPath(), \"providers.md\")}`,\n\t\t` ${join(getDocsPath(), \"models.md\")}`,\n\t].join(\"\\n\");\n}\n\nexport function formatNoModelsAvailableMessage(): string {\n\treturn `No models available. ${getProviderLoginHelp()}`;\n}\n\nexport function formatNoModelSelectedMessage(): string {\n\treturn `No model selected.\\n\\n${getProviderLoginHelp()}\\n\\nThen use /model to select a model.`;\n}\n\nexport function formatNoApiKeyFoundMessage(provider: string): string {\n\tconst providerDisplay = provider === UNKNOWN_PROVIDER ? \"the selected model\" : provider;\n\treturn `No API key found for ${providerDisplay}.\\n\\n${getProviderLoginHelp()}`;\n}\n"]}
|
|
@@ -15,6 +15,11 @@ export type OAuthCredential = {
|
|
|
15
15
|
} & OAuthCredentials;
|
|
16
16
|
export type AuthCredential = ApiKeyCredential | OAuthCredential;
|
|
17
17
|
export type AuthStorageData = Record<string, AuthCredential>;
|
|
18
|
+
export type AuthStatus = {
|
|
19
|
+
configured: boolean;
|
|
20
|
+
source?: "stored" | "runtime" | "environment" | "fallback" | "models_json_key" | "models_json_command";
|
|
21
|
+
label?: string;
|
|
22
|
+
};
|
|
18
23
|
type LockResult<T> = {
|
|
19
24
|
result: T;
|
|
20
25
|
next?: string;
|
|
@@ -97,6 +102,10 @@ export declare class AuthStorage {
|
|
|
97
102
|
* Unlike getApiKey(), this doesn't refresh OAuth tokens.
|
|
98
103
|
*/
|
|
99
104
|
hasAuth(provider: string): boolean;
|
|
105
|
+
/**
|
|
106
|
+
* Return auth status without exposing credential values or refreshing tokens.
|
|
107
|
+
*/
|
|
108
|
+
getAuthStatus(provider: string): AuthStatus;
|
|
100
109
|
/**
|
|
101
110
|
* Get all credentials (for passing to getOAuthApiKey).
|
|
102
111
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auth-storage.d.ts","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n *\n * Uses file locking to prevent race conditions when multiple pi instances\n * try to refresh tokens simultaneously.\n */\n\nimport { getEnvApiKey, type OAuthCredentials, type OAuthLoginCallbacks, type OAuthProviderId } from \"@dyyz1993/pi-ai\";\nimport { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from \"@dyyz1993/pi-ai/oauth\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.js\";\nimport { resolveConfigValue } from \"./resolve-config-value.js\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\ntype LockResult<T> = {\n\tresult: T;\n\tnext?: string;\n};\n\nexport interface AuthStorageBackend {\n\twithLock<T>(fn: (current: string | undefined) => LockResult<T>): T;\n\twithLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;\n}\n\nexport class FileAuthStorageBackend implements AuthStorageBackend {\n\tconstructor(private authPath: string = join(getAgentDir(), \"auth.json\")) {}\n\n\tprivate ensureParentDir(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t}\n\n\tprivate ensureFileExists(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\twriteFileSync(this.authPath, \"{}\", \"utf-8\");\n\t\t\tchmodSync(this.authPath, 0o600);\n\t\t}\n\t}\n\n\tprivate acquireLockSyncWithRetry(path: string): () => void {\n\t\tconst maxAttempts = 10;\n\t\tconst delayMs = 20;\n\t\tlet lastError: unknown;\n\n\t\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\t\ttry {\n\t\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t\t} catch (error) {\n\t\t\t\tconst code =\n\t\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tif (code !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tlastError = error;\n\t\t\t\tconst start = Date.now();\n\t\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t\t// Sleep synchronously to avoid changing callers to async.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthrow (lastError as Error) ?? new Error(\"Failed to acquire auth storage lock\");\n\t}\n\n\twithLock<T>(fn: (current: string | undefined) => LockResult<T>): T {\n\t\tthis.ensureParentDir();\n\t\tthis.ensureFileExists();\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = this.acquireLockSyncWithRetry(this.authPath);\n\t\t\tconst current = existsSync(this.authPath) ? readFileSync(this.authPath, \"utf-8\") : undefined;\n\t\t\tconst { result, next } = fn(current);\n\t\t\tif (next !== undefined) {\n\t\t\t\twriteFileSync(this.authPath, next, \"utf-8\");\n\t\t\t\tchmodSync(this.authPath, 0o600);\n\t\t\t}\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\trelease();\n\t\t\t}\n\t\t}\n\t}\n\n\tasync withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T> {\n\t\tthis.ensureParentDir();\n\t\tthis.ensureFileExists();\n\n\t\tlet release: (() => Promise<void>) | undefined;\n\t\tlet lockCompromised = false;\n\t\tlet lockCompromisedError: Error | undefined;\n\t\tconst throwIfCompromised = () => {\n\t\t\tif (lockCompromised) {\n\t\t\t\tthrow lockCompromisedError ?? new Error(\"Auth storage lock was compromised\");\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\trelease = await lockfile.lock(this.authPath, {\n\t\t\t\tretries: {\n\t\t\t\t\tretries: 10,\n\t\t\t\t\tfactor: 2,\n\t\t\t\t\tminTimeout: 100,\n\t\t\t\t\tmaxTimeout: 10000,\n\t\t\t\t\trandomize: true,\n\t\t\t\t},\n\t\t\t\tstale: 30000,\n\t\t\t\tonCompromised: (err) => {\n\t\t\t\t\tlockCompromised = true;\n\t\t\t\t\tlockCompromisedError = err;\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tthrowIfCompromised();\n\t\t\tconst current = existsSync(this.authPath) ? readFileSync(this.authPath, \"utf-8\") : undefined;\n\t\t\tconst { result, next } = await fn(current);\n\t\t\tthrowIfCompromised();\n\t\t\tif (next !== undefined) {\n\t\t\t\twriteFileSync(this.authPath, next, \"utf-8\");\n\t\t\t\tchmodSync(this.authPath, 0o600);\n\t\t\t}\n\t\t\tthrowIfCompromised();\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\ttry {\n\t\t\t\t\tawait release();\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore unlock errors when lock is compromised.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport class InMemoryAuthStorageBackend implements AuthStorageBackend {\n\tprivate value: string | undefined;\n\n\twithLock<T>(fn: (current: string | undefined) => LockResult<T>): T {\n\t\tconst { result, next } = fn(this.value);\n\t\tif (next !== undefined) {\n\t\t\tthis.value = next;\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T> {\n\t\tconst { result, next } = await fn(this.value);\n\t\tif (next !== undefined) {\n\t\t\tthis.value = next;\n\t\t}\n\t\treturn result;\n\t}\n}\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\tprivate loadError: Error | null = null;\n\tprivate errors: Error[] = [];\n\n\tprivate constructor(private storage: AuthStorageBackend) {\n\t\tthis.reload();\n\t}\n\n\tstatic create(authPath?: string): AuthStorage {\n\t\treturn new AuthStorage(new FileAuthStorageBackend(authPath ?? join(getAgentDir(), \"auth.json\")));\n\t}\n\n\tstatic fromStorage(storage: AuthStorageBackend): AuthStorage {\n\t\treturn new AuthStorage(storage);\n\t}\n\n\tstatic inMemory(data: AuthStorageData = {}): AuthStorage {\n\t\tconst storage = new InMemoryAuthStorageBackend();\n\t\tstorage.withLock(() => ({ result: undefined, next: JSON.stringify(data, null, 2) }));\n\t\treturn AuthStorage.fromStorage(storage);\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\tprivate recordError(error: unknown): void {\n\t\tconst normalizedError = error instanceof Error ? error : new Error(String(error));\n\t\tthis.errors.push(normalizedError);\n\t}\n\n\tprivate parseStorageData(content: string | undefined): AuthStorageData {\n\t\tif (!content) {\n\t\t\treturn {};\n\t\t}\n\t\treturn JSON.parse(content) as AuthStorageData;\n\t}\n\n\t/**\n\t * Reload credentials from storage.\n\t */\n\treload(): void {\n\t\tlet content: string | undefined;\n\t\ttry {\n\t\t\tthis.storage.withLock((current) => {\n\t\t\t\tcontent = current;\n\t\t\t\treturn { result: undefined };\n\t\t\t});\n\t\t\tthis.data = this.parseStorageData(content);\n\t\t\tthis.loadError = null;\n\t\t} catch (error) {\n\t\t\tthis.loadError = error as Error;\n\t\t\tthis.recordError(error);\n\t\t}\n\t}\n\n\tprivate persistProviderChange(provider: string, credential: AuthCredential | undefined): void {\n\t\tif (this.loadError) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.storage.withLock((current) => {\n\t\t\t\tconst currentData = this.parseStorageData(current);\n\t\t\t\tconst merged: AuthStorageData = { ...currentData };\n\t\t\t\tif (credential) {\n\t\t\t\t\tmerged[provider] = credential;\n\t\t\t\t} else {\n\t\t\t\t\tdelete merged[provider];\n\t\t\t\t}\n\t\t\t\treturn { result: undefined, next: JSON.stringify(merged, null, 2) };\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.recordError(error);\n\t\t}\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | undefined {\n\t\treturn this.data[provider] ?? undefined;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.persistProviderChange(provider, credential);\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.persistProviderChange(provider, undefined);\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider in auth.json.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Check if any form of auth is configured for a provider.\n\t * Unlike getApiKey(), this doesn't refresh OAuth tokens.\n\t */\n\thasAuth(provider: string): boolean {\n\t\tif (this.runtimeOverrides.has(provider)) return true;\n\t\tif (this.data[provider]) return true;\n\t\tif (getEnvApiKey(provider)) return true;\n\t\tif (this.fallbackResolver?.(provider)) return true;\n\t\treturn false;\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\tdrainErrors(): Error[] {\n\t\tconst drained = [...this.errors];\n\t\tthis.errors = [];\n\t\treturn drained;\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(providerId: OAuthProviderId, callbacks: OAuthLoginCallbacks): Promise<void> {\n\t\tconst provider = getOAuthProvider(providerId);\n\t\tif (!provider) {\n\t\t\tthrow new Error(`Unknown OAuth provider: ${providerId}`);\n\t\t}\n\n\t\tconst credentials = await provider.login(callbacks);\n\t\tthis.set(providerId, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Refresh OAuth token with backend locking to prevent race conditions.\n\t * Multiple pi instances may try to refresh simultaneously when tokens expire.\n\t */\n\tprivate async refreshOAuthTokenWithLock(\n\t\tproviderId: OAuthProviderId,\n\t): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> {\n\t\tconst provider = getOAuthProvider(providerId);\n\t\tif (!provider) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst result = await this.storage.withLockAsync(async (current) => {\n\t\t\tconst currentData = this.parseStorageData(current);\n\t\t\tthis.data = currentData;\n\t\t\tthis.loadError = null;\n\n\t\t\tconst cred = currentData[providerId];\n\t\t\tif (cred?.type !== \"oauth\") {\n\t\t\t\treturn { result: null };\n\t\t\t}\n\n\t\t\tif (Date.now() < cred.expires) {\n\t\t\t\treturn { result: { apiKey: provider.getApiKey(cred), newCredentials: cred } };\n\t\t\t}\n\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(currentData)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst refreshed = await getOAuthApiKey(providerId, oauthCreds);\n\t\t\tif (!refreshed) {\n\t\t\t\treturn { result: null };\n\t\t\t}\n\n\t\t\tconst merged: AuthStorageData = {\n\t\t\t\t...currentData,\n\t\t\t\t[providerId]: { type: \"oauth\", ...refreshed.newCredentials },\n\t\t\t};\n\t\t\tthis.data = merged;\n\t\t\tthis.loadError = null;\n\t\t\treturn { result: refreshed, next: JSON.stringify(merged, null, 2) };\n\t\t});\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed with locking)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(providerId: string, options?: { includeFallback?: boolean }): Promise<string | undefined> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(providerId);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[providerId];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn resolveConfigValue(cred.key);\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\tconst provider = getOAuthProvider(providerId);\n\t\t\tif (!provider) {\n\t\t\t\t// Unknown OAuth provider, can't get API key\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// Check if token needs refresh\n\t\t\tconst needsRefresh = Date.now() >= cred.expires;\n\n\t\t\tif (needsRefresh) {\n\t\t\t\t// Use locked refresh to prevent race conditions\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.refreshOAuthTokenWithLock(providerId);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\treturn result.apiKey;\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.recordError(error);\n\t\t\t\t\t// Refresh failed - re-read file to check if another instance succeeded\n\t\t\t\t\tthis.reload();\n\t\t\t\t\tconst updatedCred = this.data[providerId];\n\n\t\t\t\t\tif (updatedCred?.type === \"oauth\" && Date.now() < updatedCred.expires) {\n\t\t\t\t\t\t// Another instance refreshed successfully, use those credentials\n\t\t\t\t\t\treturn provider.getApiKey(updatedCred);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Refresh truly failed - return undefined so model discovery skips this provider\n\t\t\t\t\t// User can /login to re-authenticate (credentials preserved for retry)\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Token not expired, use current access token\n\t\t\t\treturn provider.getApiKey(cred);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(providerId);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\tif (options?.includeFallback !== false) {\n\t\t\treturn this.fallbackResolver?.(providerId) ?? undefined;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get all registered OAuth providers\n\t */\n\tgetOAuthProviders() {\n\t\treturn getOAuthProviders();\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"auth-storage.d.ts","sourceRoot":"","sources":["../../src/core/auth-storage.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Credential storage for API keys and OAuth tokens.\n * Handles loading, saving, and refreshing credentials from auth.json.\n *\n * Uses file locking to prevent race conditions when multiple pi instances\n * try to refresh tokens simultaneously.\n */\n\nimport {\n\tfindEnvKeys,\n\tgetEnvApiKey,\n\ttype OAuthCredentials,\n\ttype OAuthLoginCallbacks,\n\ttype OAuthProviderId,\n} from \"@dyyz1993/pi-ai\";\nimport { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from \"@dyyz1993/pi-ai/oauth\";\nimport { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from \"fs\";\nimport { dirname, join } from \"path\";\nimport lockfile from \"proper-lockfile\";\nimport { getAgentDir } from \"../config.js\";\nimport { resolveConfigValue } from \"./resolve-config-value.js\";\n\nexport type ApiKeyCredential = {\n\ttype: \"api_key\";\n\tkey: string;\n};\n\nexport type OAuthCredential = {\n\ttype: \"oauth\";\n} & OAuthCredentials;\n\nexport type AuthCredential = ApiKeyCredential | OAuthCredential;\n\nexport type AuthStorageData = Record<string, AuthCredential>;\n\nexport type AuthStatus = {\n\tconfigured: boolean;\n\tsource?: \"stored\" | \"runtime\" | \"environment\" | \"fallback\" | \"models_json_key\" | \"models_json_command\";\n\tlabel?: string;\n};\n\ntype LockResult<T> = {\n\tresult: T;\n\tnext?: string;\n};\n\nexport interface AuthStorageBackend {\n\twithLock<T>(fn: (current: string | undefined) => LockResult<T>): T;\n\twithLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T>;\n}\n\nexport class FileAuthStorageBackend implements AuthStorageBackend {\n\tconstructor(private authPath: string = join(getAgentDir(), \"auth.json\")) {}\n\n\tprivate ensureParentDir(): void {\n\t\tconst dir = dirname(this.authPath);\n\t\tif (!existsSync(dir)) {\n\t\t\tmkdirSync(dir, { recursive: true, mode: 0o700 });\n\t\t}\n\t}\n\n\tprivate ensureFileExists(): void {\n\t\tif (!existsSync(this.authPath)) {\n\t\t\twriteFileSync(this.authPath, \"{}\", \"utf-8\");\n\t\t\tchmodSync(this.authPath, 0o600);\n\t\t}\n\t}\n\n\tprivate acquireLockSyncWithRetry(path: string): () => void {\n\t\tconst maxAttempts = 10;\n\t\tconst delayMs = 20;\n\t\tlet lastError: unknown;\n\n\t\tfor (let attempt = 1; attempt <= maxAttempts; attempt++) {\n\t\t\ttry {\n\t\t\t\treturn lockfile.lockSync(path, { realpath: false });\n\t\t\t} catch (error) {\n\t\t\t\tconst code =\n\t\t\t\t\ttypeof error === \"object\" && error !== null && \"code\" in error\n\t\t\t\t\t\t? String((error as { code?: unknown }).code)\n\t\t\t\t\t\t: undefined;\n\t\t\t\tif (code !== \"ELOCKED\" || attempt === maxAttempts) {\n\t\t\t\t\tthrow error;\n\t\t\t\t}\n\t\t\t\tlastError = error;\n\t\t\t\tconst start = Date.now();\n\t\t\t\twhile (Date.now() - start < delayMs) {\n\t\t\t\t\t// Sleep synchronously to avoid changing callers to async.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tthrow (lastError as Error) ?? new Error(\"Failed to acquire auth storage lock\");\n\t}\n\n\twithLock<T>(fn: (current: string | undefined) => LockResult<T>): T {\n\t\tthis.ensureParentDir();\n\t\tthis.ensureFileExists();\n\n\t\tlet release: (() => void) | undefined;\n\t\ttry {\n\t\t\trelease = this.acquireLockSyncWithRetry(this.authPath);\n\t\t\tconst current = existsSync(this.authPath) ? readFileSync(this.authPath, \"utf-8\") : undefined;\n\t\t\tconst { result, next } = fn(current);\n\t\t\tif (next !== undefined) {\n\t\t\t\twriteFileSync(this.authPath, next, \"utf-8\");\n\t\t\t\tchmodSync(this.authPath, 0o600);\n\t\t\t}\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\trelease();\n\t\t\t}\n\t\t}\n\t}\n\n\tasync withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T> {\n\t\tthis.ensureParentDir();\n\t\tthis.ensureFileExists();\n\n\t\tlet release: (() => Promise<void>) | undefined;\n\t\tlet lockCompromised = false;\n\t\tlet lockCompromisedError: Error | undefined;\n\t\tconst throwIfCompromised = () => {\n\t\t\tif (lockCompromised) {\n\t\t\t\tthrow lockCompromisedError ?? new Error(\"Auth storage lock was compromised\");\n\t\t\t}\n\t\t};\n\n\t\ttry {\n\t\t\trelease = await lockfile.lock(this.authPath, {\n\t\t\t\tretries: {\n\t\t\t\t\tretries: 10,\n\t\t\t\t\tfactor: 2,\n\t\t\t\t\tminTimeout: 100,\n\t\t\t\t\tmaxTimeout: 10000,\n\t\t\t\t\trandomize: true,\n\t\t\t\t},\n\t\t\t\tstale: 30000,\n\t\t\t\tonCompromised: (err) => {\n\t\t\t\t\tlockCompromised = true;\n\t\t\t\t\tlockCompromisedError = err;\n\t\t\t\t},\n\t\t\t});\n\n\t\t\tthrowIfCompromised();\n\t\t\tconst current = existsSync(this.authPath) ? readFileSync(this.authPath, \"utf-8\") : undefined;\n\t\t\tconst { result, next } = await fn(current);\n\t\t\tthrowIfCompromised();\n\t\t\tif (next !== undefined) {\n\t\t\t\twriteFileSync(this.authPath, next, \"utf-8\");\n\t\t\t\tchmodSync(this.authPath, 0o600);\n\t\t\t}\n\t\t\tthrowIfCompromised();\n\t\t\treturn result;\n\t\t} finally {\n\t\t\tif (release) {\n\t\t\t\ttry {\n\t\t\t\t\tawait release();\n\t\t\t\t} catch {\n\t\t\t\t\t// Ignore unlock errors when lock is compromised.\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nexport class InMemoryAuthStorageBackend implements AuthStorageBackend {\n\tprivate value: string | undefined;\n\n\twithLock<T>(fn: (current: string | undefined) => LockResult<T>): T {\n\t\tconst { result, next } = fn(this.value);\n\t\tif (next !== undefined) {\n\t\t\tthis.value = next;\n\t\t}\n\t\treturn result;\n\t}\n\n\tasync withLockAsync<T>(fn: (current: string | undefined) => Promise<LockResult<T>>): Promise<T> {\n\t\tconst { result, next } = await fn(this.value);\n\t\tif (next !== undefined) {\n\t\t\tthis.value = next;\n\t\t}\n\t\treturn result;\n\t}\n}\n\n/**\n * Credential storage backed by a JSON file.\n */\nexport class AuthStorage {\n\tprivate data: AuthStorageData = {};\n\tprivate runtimeOverrides: Map<string, string> = new Map();\n\tprivate fallbackResolver?: (provider: string) => string | undefined;\n\tprivate loadError: Error | null = null;\n\tprivate errors: Error[] = [];\n\n\tprivate constructor(private storage: AuthStorageBackend) {\n\t\tthis.reload();\n\t}\n\n\tstatic create(authPath?: string): AuthStorage {\n\t\treturn new AuthStorage(new FileAuthStorageBackend(authPath ?? join(getAgentDir(), \"auth.json\")));\n\t}\n\n\tstatic fromStorage(storage: AuthStorageBackend): AuthStorage {\n\t\treturn new AuthStorage(storage);\n\t}\n\n\tstatic inMemory(data: AuthStorageData = {}): AuthStorage {\n\t\tconst storage = new InMemoryAuthStorageBackend();\n\t\tstorage.withLock(() => ({ result: undefined, next: JSON.stringify(data, null, 2) }));\n\t\treturn AuthStorage.fromStorage(storage);\n\t}\n\n\t/**\n\t * Set a runtime API key override (not persisted to disk).\n\t * Used for CLI --api-key flag.\n\t */\n\tsetRuntimeApiKey(provider: string, apiKey: string): void {\n\t\tthis.runtimeOverrides.set(provider, apiKey);\n\t}\n\n\t/**\n\t * Remove a runtime API key override.\n\t */\n\tremoveRuntimeApiKey(provider: string): void {\n\t\tthis.runtimeOverrides.delete(provider);\n\t}\n\n\t/**\n\t * Set a fallback resolver for API keys not found in auth.json or env vars.\n\t * Used for custom provider keys from models.json.\n\t */\n\tsetFallbackResolver(resolver: (provider: string) => string | undefined): void {\n\t\tthis.fallbackResolver = resolver;\n\t}\n\n\tprivate recordError(error: unknown): void {\n\t\tconst normalizedError = error instanceof Error ? error : new Error(String(error));\n\t\tthis.errors.push(normalizedError);\n\t}\n\n\tprivate parseStorageData(content: string | undefined): AuthStorageData {\n\t\tif (!content) {\n\t\t\treturn {};\n\t\t}\n\t\treturn JSON.parse(content) as AuthStorageData;\n\t}\n\n\t/**\n\t * Reload credentials from storage.\n\t */\n\treload(): void {\n\t\tlet content: string | undefined;\n\t\ttry {\n\t\t\tthis.storage.withLock((current) => {\n\t\t\t\tcontent = current;\n\t\t\t\treturn { result: undefined };\n\t\t\t});\n\t\t\tthis.data = this.parseStorageData(content);\n\t\t\tthis.loadError = null;\n\t\t} catch (error) {\n\t\t\tthis.loadError = error as Error;\n\t\t\tthis.recordError(error);\n\t\t}\n\t}\n\n\tprivate persistProviderChange(provider: string, credential: AuthCredential | undefined): void {\n\t\tif (this.loadError) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tthis.storage.withLock((current) => {\n\t\t\t\tconst currentData = this.parseStorageData(current);\n\t\t\t\tconst merged: AuthStorageData = { ...currentData };\n\t\t\t\tif (credential) {\n\t\t\t\t\tmerged[provider] = credential;\n\t\t\t\t} else {\n\t\t\t\t\tdelete merged[provider];\n\t\t\t\t}\n\t\t\t\treturn { result: undefined, next: JSON.stringify(merged, null, 2) };\n\t\t\t});\n\t\t} catch (error) {\n\t\t\tthis.recordError(error);\n\t\t}\n\t}\n\n\t/**\n\t * Get credential for a provider.\n\t */\n\tget(provider: string): AuthCredential | undefined {\n\t\treturn this.data[provider] ?? undefined;\n\t}\n\n\t/**\n\t * Set credential for a provider.\n\t */\n\tset(provider: string, credential: AuthCredential): void {\n\t\tthis.data[provider] = credential;\n\t\tthis.persistProviderChange(provider, credential);\n\t}\n\n\t/**\n\t * Remove credential for a provider.\n\t */\n\tremove(provider: string): void {\n\t\tdelete this.data[provider];\n\t\tthis.persistProviderChange(provider, undefined);\n\t}\n\n\t/**\n\t * List all providers with credentials.\n\t */\n\tlist(): string[] {\n\t\treturn Object.keys(this.data);\n\t}\n\n\t/**\n\t * Check if credentials exist for a provider in auth.json.\n\t */\n\thas(provider: string): boolean {\n\t\treturn provider in this.data;\n\t}\n\n\t/**\n\t * Check if any form of auth is configured for a provider.\n\t * Unlike getApiKey(), this doesn't refresh OAuth tokens.\n\t */\n\thasAuth(provider: string): boolean {\n\t\tif (this.runtimeOverrides.has(provider)) return true;\n\t\tif (this.data[provider]) return true;\n\t\tif (getEnvApiKey(provider)) return true;\n\t\tif (this.fallbackResolver?.(provider)) return true;\n\t\treturn false;\n\t}\n\n\t/**\n\t * Return auth status without exposing credential values or refreshing tokens.\n\t */\n\tgetAuthStatus(provider: string): AuthStatus {\n\t\tif (this.data[provider]) {\n\t\t\treturn { configured: true, source: \"stored\" };\n\t\t}\n\n\t\tif (this.runtimeOverrides.has(provider)) {\n\t\t\treturn { configured: false, source: \"runtime\", label: \"--api-key\" };\n\t\t}\n\n\t\tconst envKeys = findEnvKeys(provider);\n\t\tif (envKeys?.[0]) {\n\t\t\treturn { configured: false, source: \"environment\", label: envKeys[0] };\n\t\t}\n\n\t\tif (this.fallbackResolver?.(provider)) {\n\t\t\treturn { configured: false, source: \"fallback\", label: \"custom provider config\" };\n\t\t}\n\n\t\treturn { configured: false };\n\t}\n\n\t/**\n\t * Get all credentials (for passing to getOAuthApiKey).\n\t */\n\tgetAll(): AuthStorageData {\n\t\treturn { ...this.data };\n\t}\n\n\tdrainErrors(): Error[] {\n\t\tconst drained = [...this.errors];\n\t\tthis.errors = [];\n\t\treturn drained;\n\t}\n\n\t/**\n\t * Login to an OAuth provider.\n\t */\n\tasync login(providerId: OAuthProviderId, callbacks: OAuthLoginCallbacks): Promise<void> {\n\t\tconst provider = getOAuthProvider(providerId);\n\t\tif (!provider) {\n\t\t\tthrow new Error(`Unknown OAuth provider: ${providerId}`);\n\t\t}\n\n\t\tconst credentials = await provider.login(callbacks);\n\t\tthis.set(providerId, { type: \"oauth\", ...credentials });\n\t}\n\n\t/**\n\t * Logout from a provider.\n\t */\n\tlogout(provider: string): void {\n\t\tthis.remove(provider);\n\t}\n\n\t/**\n\t * Refresh OAuth token with backend locking to prevent race conditions.\n\t * Multiple pi instances may try to refresh simultaneously when tokens expire.\n\t */\n\tprivate async refreshOAuthTokenWithLock(\n\t\tproviderId: OAuthProviderId,\n\t): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> {\n\t\tconst provider = getOAuthProvider(providerId);\n\t\tif (!provider) {\n\t\t\treturn null;\n\t\t}\n\n\t\tconst result = await this.storage.withLockAsync(async (current) => {\n\t\t\tconst currentData = this.parseStorageData(current);\n\t\t\tthis.data = currentData;\n\t\t\tthis.loadError = null;\n\n\t\t\tconst cred = currentData[providerId];\n\t\t\tif (cred?.type !== \"oauth\") {\n\t\t\t\treturn { result: null };\n\t\t\t}\n\n\t\t\tif (Date.now() < cred.expires) {\n\t\t\t\treturn { result: { apiKey: provider.getApiKey(cred), newCredentials: cred } };\n\t\t\t}\n\n\t\t\tconst oauthCreds: Record<string, OAuthCredentials> = {};\n\t\t\tfor (const [key, value] of Object.entries(currentData)) {\n\t\t\t\tif (value.type === \"oauth\") {\n\t\t\t\t\toauthCreds[key] = value;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tconst refreshed = await getOAuthApiKey(providerId, oauthCreds);\n\t\t\tif (!refreshed) {\n\t\t\t\treturn { result: null };\n\t\t\t}\n\n\t\t\tconst merged: AuthStorageData = {\n\t\t\t\t...currentData,\n\t\t\t\t[providerId]: { type: \"oauth\", ...refreshed.newCredentials },\n\t\t\t};\n\t\t\tthis.data = merged;\n\t\t\tthis.loadError = null;\n\t\t\treturn { result: refreshed, next: JSON.stringify(merged, null, 2) };\n\t\t});\n\n\t\treturn result;\n\t}\n\n\t/**\n\t * Get API key for a provider.\n\t * Priority:\n\t * 1. Runtime override (CLI --api-key)\n\t * 2. API key from auth.json\n\t * 3. OAuth token from auth.json (auto-refreshed with locking)\n\t * 4. Environment variable\n\t * 5. Fallback resolver (models.json custom providers)\n\t */\n\tasync getApiKey(providerId: string, options?: { includeFallback?: boolean }): Promise<string | undefined> {\n\t\t// Runtime override takes highest priority\n\t\tconst runtimeKey = this.runtimeOverrides.get(providerId);\n\t\tif (runtimeKey) {\n\t\t\treturn runtimeKey;\n\t\t}\n\n\t\tconst cred = this.data[providerId];\n\n\t\tif (cred?.type === \"api_key\") {\n\t\t\treturn resolveConfigValue(cred.key);\n\t\t}\n\n\t\tif (cred?.type === \"oauth\") {\n\t\t\tconst provider = getOAuthProvider(providerId);\n\t\t\tif (!provider) {\n\t\t\t\t// Unknown OAuth provider, can't get API key\n\t\t\t\treturn undefined;\n\t\t\t}\n\n\t\t\t// Check if token needs refresh\n\t\t\tconst needsRefresh = Date.now() >= cred.expires;\n\n\t\t\tif (needsRefresh) {\n\t\t\t\t// Use locked refresh to prevent race conditions\n\t\t\t\ttry {\n\t\t\t\t\tconst result = await this.refreshOAuthTokenWithLock(providerId);\n\t\t\t\t\tif (result) {\n\t\t\t\t\t\treturn result.apiKey;\n\t\t\t\t\t}\n\t\t\t\t} catch (error) {\n\t\t\t\t\tthis.recordError(error);\n\t\t\t\t\t// Refresh failed - re-read file to check if another instance succeeded\n\t\t\t\t\tthis.reload();\n\t\t\t\t\tconst updatedCred = this.data[providerId];\n\n\t\t\t\t\tif (updatedCred?.type === \"oauth\" && Date.now() < updatedCred.expires) {\n\t\t\t\t\t\t// Another instance refreshed successfully, use those credentials\n\t\t\t\t\t\treturn provider.getApiKey(updatedCred);\n\t\t\t\t\t}\n\n\t\t\t\t\t// Refresh truly failed - return undefined so model discovery skips this provider\n\t\t\t\t\t// User can /login to re-authenticate (credentials preserved for retry)\n\t\t\t\t\treturn undefined;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Token not expired, use current access token\n\t\t\t\treturn provider.getApiKey(cred);\n\t\t\t}\n\t\t}\n\n\t\t// Fall back to environment variable\n\t\tconst envKey = getEnvApiKey(providerId);\n\t\tif (envKey) return envKey;\n\n\t\t// Fall back to custom resolver (e.g., models.json custom providers)\n\t\tif (options?.includeFallback !== false) {\n\t\t\treturn this.fallbackResolver?.(providerId) ?? undefined;\n\t\t}\n\n\t\treturn undefined;\n\t}\n\n\t/**\n\t * Get all registered OAuth providers\n\t */\n\tgetOAuthProviders() {\n\t\treturn getOAuthProviders();\n\t}\n}\n"]}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Uses file locking to prevent race conditions when multiple pi instances
|
|
6
6
|
* try to refresh tokens simultaneously.
|
|
7
7
|
*/
|
|
8
|
-
import { getEnvApiKey } from "@dyyz1993/pi-ai";
|
|
8
|
+
import { findEnvKeys, getEnvApiKey, } from "@dyyz1993/pi-ai";
|
|
9
9
|
import { getOAuthApiKey, getOAuthProvider, getOAuthProviders } from "@dyyz1993/pi-ai/oauth";
|
|
10
10
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
11
11
|
import { dirname, join } from "path";
|
|
@@ -280,6 +280,25 @@ export class AuthStorage {
|
|
|
280
280
|
return true;
|
|
281
281
|
return false;
|
|
282
282
|
}
|
|
283
|
+
/**
|
|
284
|
+
* Return auth status without exposing credential values or refreshing tokens.
|
|
285
|
+
*/
|
|
286
|
+
getAuthStatus(provider) {
|
|
287
|
+
if (this.data[provider]) {
|
|
288
|
+
return { configured: true, source: "stored" };
|
|
289
|
+
}
|
|
290
|
+
if (this.runtimeOverrides.has(provider)) {
|
|
291
|
+
return { configured: false, source: "runtime", label: "--api-key" };
|
|
292
|
+
}
|
|
293
|
+
const envKeys = findEnvKeys(provider);
|
|
294
|
+
if (envKeys?.[0]) {
|
|
295
|
+
return { configured: false, source: "environment", label: envKeys[0] };
|
|
296
|
+
}
|
|
297
|
+
if (this.fallbackResolver?.(provider)) {
|
|
298
|
+
return { configured: false, source: "fallback", label: "custom provider config" };
|
|
299
|
+
}
|
|
300
|
+
return { configured: false };
|
|
301
|
+
}
|
|
283
302
|
/**
|
|
284
303
|
* Get all credentials (for passing to getOAuthApiKey).
|
|
285
304
|
*/
|