@agi-cli/sdk 0.1.53 → 0.1.55

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 (81) hide show
  1. package/package.json +52 -27
  2. package/src/agent/types.ts +1 -1
  3. package/src/auth/src/index.ts +70 -0
  4. package/src/auth/src/oauth.ts +172 -0
  5. package/src/config/src/index.ts +120 -0
  6. package/src/config/src/manager.ts +102 -0
  7. package/src/config/src/paths.ts +98 -0
  8. package/src/core/src/errors.ts +102 -0
  9. package/src/core/src/index.ts +75 -0
  10. package/src/core/src/providers/resolver.ts +84 -0
  11. package/src/core/src/streaming/artifacts.ts +41 -0
  12. package/src/core/src/tools/builtin/bash.ts +90 -0
  13. package/src/core/src/tools/builtin/bash.txt +7 -0
  14. package/src/core/src/tools/builtin/edit.ts +152 -0
  15. package/src/core/src/tools/builtin/edit.txt +7 -0
  16. package/src/core/src/tools/builtin/file-cache.ts +39 -0
  17. package/src/core/src/tools/builtin/finish.ts +11 -0
  18. package/src/core/src/tools/builtin/finish.txt +5 -0
  19. package/src/core/src/tools/builtin/fs/cd.ts +19 -0
  20. package/src/core/src/tools/builtin/fs/cd.txt +5 -0
  21. package/src/core/src/tools/builtin/fs/index.ts +20 -0
  22. package/src/core/src/tools/builtin/fs/ls.ts +60 -0
  23. package/src/core/src/tools/builtin/fs/ls.txt +8 -0
  24. package/src/core/src/tools/builtin/fs/pwd.ts +17 -0
  25. package/src/core/src/tools/builtin/fs/pwd.txt +5 -0
  26. package/src/core/src/tools/builtin/fs/read.ts +80 -0
  27. package/src/core/src/tools/builtin/fs/read.txt +8 -0
  28. package/src/core/src/tools/builtin/fs/tree.ts +71 -0
  29. package/src/core/src/tools/builtin/fs/tree.txt +8 -0
  30. package/src/core/src/tools/builtin/fs/util.ts +95 -0
  31. package/src/core/src/tools/builtin/fs/write.ts +61 -0
  32. package/src/core/src/tools/builtin/fs/write.txt +8 -0
  33. package/src/core/src/tools/builtin/git.commit.txt +6 -0
  34. package/src/core/src/tools/builtin/git.diff.txt +5 -0
  35. package/src/core/src/tools/builtin/git.status.txt +5 -0
  36. package/src/core/src/tools/builtin/git.ts +128 -0
  37. package/src/core/src/tools/builtin/grep.ts +140 -0
  38. package/src/core/src/tools/builtin/grep.txt +9 -0
  39. package/src/core/src/tools/builtin/ignore.ts +45 -0
  40. package/src/core/src/tools/builtin/patch.ts +269 -0
  41. package/src/core/src/tools/builtin/patch.txt +7 -0
  42. package/src/core/src/tools/builtin/plan.ts +58 -0
  43. package/src/core/src/tools/builtin/plan.txt +6 -0
  44. package/src/core/src/tools/builtin/progress.ts +55 -0
  45. package/src/core/src/tools/builtin/progress.txt +7 -0
  46. package/src/core/src/tools/builtin/ripgrep.ts +102 -0
  47. package/src/core/src/tools/builtin/ripgrep.txt +7 -0
  48. package/src/core/src/tools/builtin/websearch.ts +219 -0
  49. package/src/core/src/tools/builtin/websearch.txt +12 -0
  50. package/src/core/src/tools/loader.ts +398 -0
  51. package/src/core/src/types/index.ts +11 -0
  52. package/src/core/src/types/types.ts +4 -0
  53. package/src/index.ts +57 -58
  54. package/src/prompts/src/agents/build.txt +5 -0
  55. package/src/prompts/src/agents/general.txt +6 -0
  56. package/src/prompts/src/agents/plan.txt +13 -0
  57. package/src/prompts/src/base.txt +14 -0
  58. package/src/prompts/src/debug.ts +104 -0
  59. package/src/prompts/src/index.ts +1 -0
  60. package/src/prompts/src/modes/oneshot.txt +9 -0
  61. package/src/prompts/src/providers/anthropic.txt +151 -0
  62. package/src/prompts/src/providers/anthropicSpoof.txt +1 -0
  63. package/src/prompts/src/providers/default.txt +310 -0
  64. package/src/prompts/src/providers/google.txt +155 -0
  65. package/src/prompts/src/providers/openai.txt +310 -0
  66. package/src/prompts/src/providers.ts +116 -0
  67. package/src/providers/src/authorization.ts +17 -0
  68. package/src/providers/src/catalog.ts +4201 -0
  69. package/src/providers/src/env.ts +26 -0
  70. package/src/providers/src/index.ts +12 -0
  71. package/src/providers/src/pricing.ts +135 -0
  72. package/src/providers/src/utils.ts +24 -0
  73. package/src/providers/src/validate.ts +39 -0
  74. package/src/types/src/auth.ts +26 -0
  75. package/src/types/src/config.ts +40 -0
  76. package/src/types/src/index.ts +14 -0
  77. package/src/types/src/provider.ts +28 -0
  78. package/src/global.d.ts +0 -4
  79. package/src/tools/builtin/fs.ts +0 -1
  80. package/src/tools/builtin/git.ts +0 -1
  81. package/src/web-ui.ts +0 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agi-cli/sdk",
3
- "version": "0.1.53",
3
+ "version": "0.1.55",
4
4
  "description": "AI agent SDK for building intelligent assistants - tree-shakable and comprehensive",
5
5
  "author": "ntishxyz",
6
6
  "license": "MIT",
@@ -16,46 +16,70 @@
16
16
  "type": "module",
17
17
  "main": "./src/index.ts",
18
18
  "types": "./src/index.ts",
19
- "files": [
20
- "src",
21
- "README.md",
22
- "LICENSE"
23
- ],
24
19
  "exports": {
25
20
  ".": {
26
21
  "import": "./src/index.ts",
27
22
  "types": "./src/index.ts"
28
23
  },
29
24
  "./tools/builtin/fs": {
30
- "import": "./src/tools/builtin/fs.ts",
31
- "types": "./src/tools/builtin/fs.ts"
25
+ "import": "./src/core/src/tools/builtin/fs/index.ts",
26
+ "types": "./src/core/src/tools/builtin/fs/index.ts"
32
27
  },
33
28
  "./tools/builtin/git": {
34
- "import": "./src/tools/builtin/git.ts",
35
- "types": "./src/tools/builtin/git.ts"
29
+ "import": "./src/core/src/tools/builtin/git.ts",
30
+ "types": "./src/core/src/tools/builtin/git.ts"
31
+ },
32
+ "./tools/builtin/bash": {
33
+ "import": "./src/core/src/tools/builtin/bash.ts",
34
+ "types": "./src/core/src/tools/builtin/bash.ts"
35
+ },
36
+ "./tools/builtin/edit": {
37
+ "import": "./src/core/src/tools/builtin/edit.ts",
38
+ "types": "./src/core/src/tools/builtin/edit.ts"
39
+ },
40
+ "./tools/builtin/finish": {
41
+ "import": "./src/core/src/tools/builtin/finish.ts",
42
+ "types": "./src/core/src/tools/builtin/finish.ts"
43
+ },
44
+ "./tools/builtin/grep": {
45
+ "import": "./src/core/src/tools/builtin/grep.ts",
46
+ "types": "./src/core/src/tools/builtin/grep.ts"
47
+ },
48
+ "./tools/builtin/patch": {
49
+ "import": "./src/core/src/tools/builtin/patch.ts",
50
+ "types": "./src/core/src/tools/builtin/patch.ts"
36
51
  },
37
- "./web-ui": {
38
- "import": "./src/web-ui.ts",
39
- "types": "./src/web-ui.ts"
40
- }
52
+ "./tools/builtin/plan": {
53
+ "import": "./src/core/src/tools/builtin/plan.ts",
54
+ "types": "./src/core/src/tools/builtin/plan.ts"
55
+ },
56
+ "./tools/builtin/progress": {
57
+ "import": "./src/core/src/tools/builtin/progress.ts",
58
+ "types": "./src/core/src/tools/builtin/progress.ts"
59
+ },
60
+ "./tools/builtin/ripgrep": {
61
+ "import": "./src/core/src/tools/builtin/ripgrep.ts",
62
+ "types": "./src/core/src/tools/builtin/ripgrep.ts"
63
+ },
64
+ "./tools/builtin/websearch": {
65
+ "import": "./src/core/src/tools/builtin/websearch.ts",
66
+ "types": "./src/core/src/tools/builtin/websearch.ts"
67
+ },
68
+ "./prompts/*": "./src/prompts/src/*"
41
69
  },
70
+ "files": [
71
+ "src",
72
+ "README.md",
73
+ "LICENSE"
74
+ ],
42
75
  "scripts": {
43
76
  "dev": "bun run src/index.ts",
44
- "build": "bun run build.ts",
45
77
  "test": "bun test",
46
- "typecheck": "tsc --noEmit",
47
- "prepublishOnly": "bun run build"
78
+ "typecheck": "tsc --noEmit"
48
79
  },
49
80
  "dependencies": {
50
- "@agi-cli/auth": "0.1.0",
51
- "@agi-cli/config": "0.1.0",
52
- "@agi-cli/core": "0.1.0",
53
- "@agi-cli/database": "0.1.0",
54
- "@agi-cli/web-ui": "0.1.53",
55
- "@agi-cli/providers": "0.1.0",
56
- "@agi-cli/prompts": "0.1.0",
57
- "@agi-cli/server": "0.1.0",
58
- "@agi-cli/types": "0.1.0",
81
+ "@openauthjs/openauth": "^0.4.3",
82
+ "opencode-anthropic-auth": "^0.0.2",
59
83
  "ai": "^5.0.43",
60
84
  "@ai-sdk/anthropic": "^2.0.16",
61
85
  "@ai-sdk/openai": "^2.0.30",
@@ -63,7 +87,8 @@
63
87
  "@openrouter/ai-sdk-provider": "^1.2.0",
64
88
  "@ai-sdk/openai-compatible": "^1.0.18",
65
89
  "hono": "^4.9.7",
66
- "zod": "^4.1.8"
90
+ "zod": "^4.1.8",
91
+ "fast-glob": "^3.3.2"
67
92
  },
68
93
  "devDependencies": {
69
94
  "@types/bun": "latest",
@@ -1,4 +1,4 @@
1
- import type { ProviderName } from '@agi-cli/core';
1
+ import type { ProviderName } from '../core/src/index.ts';
2
2
 
3
3
  export type AgentConfig = {
4
4
  name: string;
@@ -0,0 +1,70 @@
1
+ import { getSecureAuthPath, ensureDir } from '../../config/src/paths.ts';
2
+ import type { ProviderId, AuthInfo, AuthFile } from '../../types/src/index.ts';
3
+
4
+ export type {
5
+ ProviderId,
6
+ ApiAuth,
7
+ OAuth,
8
+ AuthInfo,
9
+ } from '../../types/src/index.ts';
10
+
11
+ function globalAuthPath(): string {
12
+ return getSecureAuthPath();
13
+ }
14
+
15
+ export async function getAllAuth(_projectRoot?: string): Promise<AuthFile> {
16
+ const globalFile = Bun.file(globalAuthPath());
17
+ const globalData = (await globalFile.json().catch(() => ({}))) as AuthFile;
18
+ return { ...globalData };
19
+ }
20
+
21
+ export async function getAuth(
22
+ provider: ProviderId,
23
+ projectRoot?: string,
24
+ ): Promise<AuthInfo | undefined> {
25
+ const all = await getAllAuth(projectRoot);
26
+ return all[provider];
27
+ }
28
+
29
+ export async function setAuth(
30
+ provider: ProviderId,
31
+ info: AuthInfo,
32
+ _projectRoot?: string,
33
+ _scope: 'global' | 'local' = 'global',
34
+ ) {
35
+ const path = globalAuthPath();
36
+ const f = Bun.file(path);
37
+ const existing = ((await f.json().catch(() => ({}))) || {}) as AuthFile;
38
+ const next: AuthFile = { ...existing, [provider]: info };
39
+ const base = path.slice(0, path.lastIndexOf('/')) || '.';
40
+ await ensureDir(base);
41
+ await Bun.write(path, JSON.stringify(next, null, 2));
42
+ try {
43
+ const { promises: fs } = await import('node:fs');
44
+ await fs.chmod(path, 0o600).catch(() => {});
45
+ } catch {}
46
+ }
47
+
48
+ export async function removeAuth(
49
+ provider: ProviderId,
50
+ _projectRoot?: string,
51
+ _scope: 'global' | 'local' = 'global',
52
+ ) {
53
+ const path = globalAuthPath();
54
+ const f = Bun.file(path);
55
+ const existing = ((await f.json().catch(() => ({}))) || {}) as AuthFile;
56
+ delete existing[provider];
57
+ await Bun.write(path, JSON.stringify(existing, null, 2));
58
+ try {
59
+ const { promises: fs } = await import('node:fs');
60
+ await fs.chmod(path, 0o600).catch(() => {});
61
+ } catch {}
62
+ }
63
+
64
+ export {
65
+ authorize,
66
+ exchange,
67
+ refreshToken,
68
+ openAuthUrl,
69
+ createApiKey,
70
+ } from './oauth.ts';
@@ -0,0 +1,172 @@
1
+ import { spawn } from 'node:child_process';
2
+ import { randomBytes, createHash } from 'node:crypto';
3
+
4
+ const CLIENT_ID = '9d1c250a-e61b-44d9-88ed-5944d1962f5e';
5
+
6
+ type Mode = 'max' | 'console';
7
+
8
+ // Custom PKCE implementation using synchronous crypto
9
+ function generatePKCE() {
10
+ // Generate random verifier (43-128 characters, base64url encoded)
11
+ const verifier = randomBytes(32)
12
+ .toString('base64')
13
+ .replace(/\+/g, '-')
14
+ .replace(/\//g, '_')
15
+ .replace(/=/g, '');
16
+
17
+ // Generate challenge from verifier (SHA-256 hash, base64url encoded)
18
+ const challenge = createHash('sha256')
19
+ .update(verifier)
20
+ .digest('base64')
21
+ .replace(/\+/g, '-')
22
+ .replace(/\//g, '_')
23
+ .replace(/=/g, '');
24
+
25
+ return { verifier, challenge };
26
+ }
27
+
28
+ async function openBrowser(url: string) {
29
+ const platform = process.platform;
30
+ let command: string;
31
+
32
+ switch (platform) {
33
+ case 'darwin':
34
+ command = `open "${url}"`;
35
+ break;
36
+ case 'win32':
37
+ command = `start "${url}"`;
38
+ break;
39
+ default:
40
+ command = `xdg-open "${url}"`;
41
+ break;
42
+ }
43
+
44
+ return new Promise<void>((resolve, reject) => {
45
+ const child = spawn(command, [], { shell: true });
46
+ child.on('error', reject);
47
+ child.on('exit', (code) => {
48
+ if (code === 0) resolve();
49
+ else reject(new Error(`Failed to open browser (exit code ${code})`));
50
+ });
51
+ });
52
+ }
53
+
54
+ export async function authorize(mode: Mode) {
55
+ const pkce = generatePKCE();
56
+
57
+ const url = new URL(
58
+ `https://${mode === 'console' ? 'console.anthropic.com' : 'claude.ai'}/oauth/authorize`,
59
+ );
60
+ url.searchParams.set('code', 'true');
61
+ url.searchParams.set('client_id', CLIENT_ID);
62
+ url.searchParams.set('response_type', 'code');
63
+ url.searchParams.set(
64
+ 'redirect_uri',
65
+ 'https://console.anthropic.com/oauth/code/callback',
66
+ );
67
+ url.searchParams.set(
68
+ 'scope',
69
+ 'org:create_api_key user:profile user:inference',
70
+ );
71
+ url.searchParams.set('code_challenge', pkce.challenge);
72
+ url.searchParams.set('code_challenge_method', 'S256');
73
+ url.searchParams.set('state', pkce.verifier);
74
+
75
+ return {
76
+ url: url.toString(),
77
+ verifier: pkce.verifier,
78
+ };
79
+ }
80
+
81
+ export async function exchange(code: string, verifier: string) {
82
+ const splits = code.split('#');
83
+ const result = await fetch('https://console.anthropic.com/v1/oauth/token', {
84
+ method: 'POST',
85
+ headers: {
86
+ 'Content-Type': 'application/json',
87
+ },
88
+ body: JSON.stringify({
89
+ code: splits[0],
90
+ state: splits[1],
91
+ grant_type: 'authorization_code',
92
+ client_id: CLIENT_ID,
93
+ redirect_uri: 'https://console.anthropic.com/oauth/code/callback',
94
+ code_verifier: verifier,
95
+ }),
96
+ });
97
+
98
+ if (!result.ok) {
99
+ const error = await result.text();
100
+ throw new Error(`Token exchange failed: ${error}`);
101
+ }
102
+
103
+ const json = (await result.json()) as {
104
+ refresh_token: string;
105
+ access_token: string;
106
+ expires_in: number;
107
+ };
108
+ return {
109
+ refresh: json.refresh_token,
110
+ access: json.access_token,
111
+ expires: Date.now() + json.expires_in * 1000,
112
+ };
113
+ }
114
+
115
+ export async function refreshToken(refreshToken: string) {
116
+ const response = await fetch('https://console.anthropic.com/v1/oauth/token', {
117
+ method: 'POST',
118
+ headers: {
119
+ 'Content-Type': 'application/json',
120
+ },
121
+ body: JSON.stringify({
122
+ grant_type: 'refresh_token',
123
+ refresh_token: refreshToken,
124
+ client_id: CLIENT_ID,
125
+ }),
126
+ });
127
+
128
+ if (!response.ok) {
129
+ throw new Error('Failed to refresh token');
130
+ }
131
+
132
+ const json = (await response.json()) as {
133
+ refresh_token: string;
134
+ access_token: string;
135
+ expires_in: number;
136
+ };
137
+ return {
138
+ refresh: json.refresh_token,
139
+ access: json.access_token,
140
+ expires: Date.now() + json.expires_in * 1000,
141
+ };
142
+ }
143
+
144
+ export async function openAuthUrl(url: string) {
145
+ try {
146
+ await openBrowser(url);
147
+ return true;
148
+ } catch {
149
+ return false;
150
+ }
151
+ }
152
+
153
+ export async function createApiKey(accessToken: string) {
154
+ const result = await fetch(
155
+ 'https://api.anthropic.com/api/oauth/claude_cli/create_api_key',
156
+ {
157
+ method: 'POST',
158
+ headers: {
159
+ 'Content-Type': 'application/json',
160
+ authorization: `Bearer ${accessToken}`,
161
+ },
162
+ },
163
+ );
164
+
165
+ if (!result.ok) {
166
+ const error = await result.text();
167
+ throw new Error(`Failed to create API key: ${error}`);
168
+ }
169
+
170
+ const json = (await result.json()) as { raw_key: string };
171
+ return json.raw_key;
172
+ }
@@ -0,0 +1,120 @@
1
+ import {
2
+ getGlobalConfigPath,
3
+ getLocalDataDir,
4
+ ensureDir,
5
+ fileExists,
6
+ joinPath,
7
+ } from './paths.ts';
8
+ import type { AGIConfig } from '../../types/src/index.ts';
9
+
10
+ export type { ProviderConfig, AGIConfig } from '../../types/src/index.ts';
11
+
12
+ const DEFAULTS: {
13
+ defaults: AGIConfig['defaults'];
14
+ providers: AGIConfig['providers'];
15
+ } = {
16
+ defaults: {
17
+ agent: 'general',
18
+ provider: 'openai',
19
+ model: 'gpt-4o-mini',
20
+ },
21
+ providers: {
22
+ openai: { enabled: true },
23
+ anthropic: { enabled: true },
24
+ google: { enabled: true },
25
+ openrouter: { enabled: false },
26
+ opencode: { enabled: false },
27
+ },
28
+ };
29
+
30
+ export async function loadConfig(
31
+ projectRootInput?: string,
32
+ ): Promise<AGIConfig> {
33
+ const projectRoot = projectRootInput
34
+ ? String(projectRootInput)
35
+ : process.cwd();
36
+
37
+ const dataDir = getLocalDataDir(projectRoot);
38
+ const dbPath = joinPath(dataDir, 'agi.sqlite');
39
+ const projectConfigPath = joinPath(dataDir, 'config.json');
40
+ const globalConfigPath = getGlobalConfigPath();
41
+
42
+ const projectCfg = await readJsonOptional(projectConfigPath);
43
+ const globalCfg = await readJsonOptional(globalConfigPath);
44
+
45
+ const merged = deepMerge(DEFAULTS, globalCfg, projectCfg);
46
+
47
+ await ensureDir(dataDir);
48
+
49
+ return {
50
+ projectRoot,
51
+ defaults: merged.defaults as AGIConfig['defaults'],
52
+ providers: merged.providers as AGIConfig['providers'],
53
+ paths: {
54
+ dataDir,
55
+ dbPath,
56
+ projectConfigPath: (await fileExists(projectConfigPath))
57
+ ? projectConfigPath
58
+ : null,
59
+ globalConfigPath: (await fileExists(globalConfigPath))
60
+ ? globalConfigPath
61
+ : null,
62
+ },
63
+ } satisfies AGIConfig;
64
+ }
65
+
66
+ type JsonObject = Record<string, unknown>;
67
+
68
+ async function readJsonOptional(file: string): Promise<JsonObject | undefined> {
69
+ const f = Bun.file(file);
70
+ if (!(await f.exists())) return undefined;
71
+ try {
72
+ const buf = await f.text();
73
+ const parsed = JSON.parse(buf);
74
+ if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
75
+ return parsed as JsonObject;
76
+ }
77
+ return undefined;
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ }
82
+
83
+ function deepMerge<T extends JsonObject>(
84
+ ...objects: Array<JsonObject | undefined>
85
+ ): T {
86
+ const result: JsonObject = {};
87
+ for (const obj of objects) {
88
+ if (!obj) continue;
89
+ mergeInto(result, obj);
90
+ }
91
+ return result as T;
92
+ }
93
+
94
+ function mergeInto(target: JsonObject, source: JsonObject): JsonObject {
95
+ for (const key of Object.keys(source)) {
96
+ const sv = source[key];
97
+ const tv = target[key];
98
+ if (sv && typeof sv === 'object' && !Array.isArray(sv)) {
99
+ const svObj = sv as JsonObject;
100
+ const nextTarget =
101
+ tv && typeof tv === 'object' && !Array.isArray(tv)
102
+ ? (tv as JsonObject)
103
+ : {};
104
+ target[key] = mergeInto(nextTarget, svObj);
105
+ } else {
106
+ target[key] = sv;
107
+ }
108
+ }
109
+ return target;
110
+ }
111
+
112
+ export type { Scope } from './manager.ts';
113
+ export {
114
+ read,
115
+ isAuthorized,
116
+ ensureEnv,
117
+ writeDefaults,
118
+ writeAuth,
119
+ removeAuth,
120
+ } from './manager.ts';
@@ -0,0 +1,102 @@
1
+ import { loadConfig } from './index.ts';
2
+ import {
3
+ getAllAuth,
4
+ setAuth as setAuthFile,
5
+ removeAuth as removeAuthFile,
6
+ type ProviderId,
7
+ type AuthInfo,
8
+ } from '../../auth/src/index.ts';
9
+ import { getGlobalConfigDir, getGlobalConfigPath } from './paths.ts';
10
+ import {
11
+ providerIds,
12
+ readEnvKey,
13
+ setEnvKey,
14
+ } from '../../providers/src/index.ts';
15
+
16
+ export type Scope = 'global' | 'local';
17
+
18
+ export async function read(projectRoot?: string) {
19
+ const cfg = await loadConfig(projectRoot);
20
+ const auth = await getAllAuth(projectRoot);
21
+ return { cfg, auth };
22
+ }
23
+
24
+ export async function isAuthorized(
25
+ provider: ProviderId,
26
+ projectRoot?: string,
27
+ ): Promise<boolean> {
28
+ if (!providerIds.includes(provider)) return false;
29
+ if (readEnvKey(provider)) return true;
30
+ const { auth } = await read(projectRoot);
31
+ const info = auth[provider];
32
+ if (info?.type === 'api' && info.key) return true;
33
+ if (info?.type === 'oauth' && info.refresh && info.access) return true;
34
+ return false;
35
+ }
36
+
37
+ export async function ensureEnv(
38
+ provider: ProviderId,
39
+ projectRoot?: string,
40
+ ): Promise<void> {
41
+ if (!providerIds.includes(provider)) return;
42
+ if (readEnvKey(provider)) return;
43
+ const { auth } = await read(projectRoot);
44
+ const stored = auth[provider];
45
+ const key = stored?.type === 'api' ? stored.key : undefined;
46
+ if (key) setEnvKey(provider, key);
47
+ }
48
+
49
+ export async function writeDefaults(
50
+ scope: Scope,
51
+ updates: Partial<{ agent: string; provider: ProviderId; model: string }>,
52
+ projectRoot?: string,
53
+ ) {
54
+ const { cfg } = await read(projectRoot);
55
+ if (scope === 'local') {
56
+ const next = {
57
+ projectRoot: cfg.projectRoot,
58
+ defaults: {
59
+ agent: updates.agent ?? cfg.defaults.agent,
60
+ provider: (updates.provider ?? cfg.defaults.provider) as ProviderId,
61
+ model: updates.model ?? cfg.defaults.model,
62
+ },
63
+ providers: cfg.providers,
64
+ paths: cfg.paths,
65
+ };
66
+ const path = `${cfg.paths.dataDir}/config.json`;
67
+ await Bun.write(path, JSON.stringify(next, null, 2));
68
+ return;
69
+ }
70
+ const base = getGlobalConfigDir();
71
+ const path = getGlobalConfigPath();
72
+ const next = {
73
+ defaults: {
74
+ agent: updates.agent ?? cfg.defaults.agent,
75
+ provider: (updates.provider ?? cfg.defaults.provider) as ProviderId,
76
+ model: updates.model ?? cfg.defaults.model,
77
+ },
78
+ providers: cfg.providers,
79
+ };
80
+ try {
81
+ const { promises: fs } = await import('node:fs');
82
+ await fs.mkdir(base, { recursive: true }).catch(() => {});
83
+ } catch {}
84
+ await Bun.write(path, JSON.stringify(next, null, 2));
85
+ }
86
+
87
+ export async function writeAuth(
88
+ provider: ProviderId,
89
+ info: AuthInfo,
90
+ scope: Scope = 'global',
91
+ projectRoot?: string,
92
+ ) {
93
+ await setAuthFile(provider, info, projectRoot, scope);
94
+ }
95
+
96
+ export async function removeAuth(
97
+ provider: ProviderId,
98
+ scope: Scope = 'global',
99
+ projectRoot?: string,
100
+ ) {
101
+ await removeAuthFile(provider, projectRoot, scope);
102
+ }
@@ -0,0 +1,98 @@
1
+ // Utilities for resolving AGI config/data paths consistently
2
+ // Uses XDG base directory spec for global config: ~/.config/agi by default
3
+
4
+ // Minimal path join to avoid node:path; ensures forward slashes
5
+ function joinPath(...parts: string[]) {
6
+ return parts
7
+ .filter(Boolean)
8
+ .map((p) => p.replace(/\\/g, '/'))
9
+ .join('/')
10
+ .replace(/\/+\/+/g, '/');
11
+ }
12
+
13
+ export function getHomeDir(): string {
14
+ return (process.env.HOME || process.env.USERPROFILE || '').replace(
15
+ /\\/g,
16
+ '/',
17
+ );
18
+ }
19
+
20
+ export function getConfigHomeDir(): string {
21
+ const cfgHome = process.env.XDG_CONFIG_HOME;
22
+ if (cfgHome?.trim()) return cfgHome.replace(/\\/g, '/');
23
+ return joinPath(getHomeDir(), '.config');
24
+ }
25
+
26
+ export function getGlobalConfigDir(): string {
27
+ return joinPath(getConfigHomeDir(), 'agi');
28
+ }
29
+
30
+ export function getGlobalConfigPath(): string {
31
+ return joinPath(getGlobalConfigDir(), 'config.json');
32
+ }
33
+
34
+ export function getGlobalAuthPath(): string {
35
+ return joinPath(getGlobalConfigDir(), 'auth.json');
36
+ }
37
+
38
+ // Secure location for auth secrets (not in config dir or project)
39
+ // - Linux: $XDG_STATE_HOME/agi/auth.json or ~/.local/state/agi/auth.json
40
+ // - macOS: ~/Library/Application Support/agi/auth.json
41
+ // - Windows: %APPDATA%\agi\auth.json
42
+ export function getSecureAuthPath(): string {
43
+ const platform = process.platform;
44
+ if (platform === 'darwin') {
45
+ return joinPath(
46
+ getHomeDir(),
47
+ 'Library',
48
+ 'Application Support',
49
+ 'agi',
50
+ 'auth.json',
51
+ );
52
+ }
53
+ if (platform === 'win32') {
54
+ const appData = (process.env.APPDATA || '').replace(/\\/g, '/');
55
+ const base = appData || joinPath(getHomeDir(), 'AppData', 'Roaming');
56
+ return joinPath(base, 'agi', 'auth.json');
57
+ }
58
+ const stateHome = (process.env.XDG_STATE_HOME || '').replace(/\\/g, '/');
59
+ const base = stateHome || joinPath(getHomeDir(), '.local', 'state');
60
+ return joinPath(base, 'agi', 'auth.json');
61
+ }
62
+
63
+ // Global content under config dir
64
+ export function getGlobalAgentsJsonPath(): string {
65
+ return joinPath(getGlobalConfigDir(), 'agents.json');
66
+ }
67
+
68
+ export function getGlobalAgentsDir(): string {
69
+ return joinPath(getGlobalConfigDir(), 'agents');
70
+ }
71
+
72
+ export function getGlobalToolsDir(): string {
73
+ return joinPath(getGlobalConfigDir(), 'tools');
74
+ }
75
+
76
+ export function getGlobalCommandsDir(): string {
77
+ return joinPath(getGlobalConfigDir(), 'commands');
78
+ }
79
+
80
+ export function getLocalDataDir(projectRoot: string): string {
81
+ return joinPath(projectRoot, '.agi');
82
+ }
83
+
84
+ export async function ensureDir(dir: string) {
85
+ try {
86
+ // Attempt to create a marker file to ensure directory exists
87
+ await Bun.write(joinPath(dir, '.keep'), '');
88
+ } catch {
89
+ const { promises: fs } = await import('node:fs');
90
+ await fs.mkdir(dir, { recursive: true }).catch(() => {});
91
+ }
92
+ }
93
+
94
+ export async function fileExists(p: string) {
95
+ return await Bun.file(p).exists();
96
+ }
97
+
98
+ export { joinPath };