@jameslovespancakes/pi-plus 1.0.0 → 1.0.1
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/LICENSE +21 -21
- package/README.md +190 -190
- package/config/pi-plus.example.json +60 -60
- package/config/skills/model-routing/SKILL.md +86 -86
- package/images/pi-plus.svg +10 -10
- package/package.json +67 -67
- package/server/board-server.mjs +641 -641
- package/server/package.json +17 -17
- package/src/core/accounts/registry.ts +93 -93
- package/src/core/anthropic/client-identity.ts +241 -241
- package/src/core/catalog/quality.ts +314 -314
- package/src/core/config.ts +169 -169
- package/src/core/env.ts +58 -58
- package/src/core/exec/process.ts +146 -146
- package/src/core/exec/ssh-config.ts +157 -157
- package/src/core/policy/policy.ts +183 -183
- package/src/core/quota/pool.ts +64 -64
- package/src/core/quota/usage-source.ts +289 -289
- package/src/core/store.ts +43 -43
- package/src/domains/agents/board-setup.ts +409 -409
- package/src/domains/agents/index.ts +462 -462
- package/src/domains/models/catalog-tool.ts +361 -361
- package/src/domains/models/index.ts +14 -14
- package/src/domains/models/policy-gate.ts +169 -169
- package/src/domains/models/provider-picker.ts +207 -207
- package/src/domains/remote/config-path.ts +41 -41
- package/src/domains/remote/index.ts +866 -866
- package/src/domains/remote/setup.ts +425 -425
- package/src/domains/setup/index.ts +220 -220
- package/src/domains/subscriptions/accounts.ts +242 -242
- package/src/domains/subscriptions/footer.ts +182 -182
- package/src/domains/subscriptions/index.ts +42 -42
- package/src/domains/subscriptions/provider.ts +219 -219
- package/src/domains/subscriptions/providers/anthropic.ts +149 -149
- package/src/domains/subscriptions/providers/codex.ts +148 -148
- package/src/domains/subscriptions/routing.ts +72 -72
- package/src/services/usage-service.ts +186 -186
- package/src/ui/format.ts +73 -73
- package/src/ui/usage-bars.ts +154 -154
- package/src/vendor/anthropic.ts +109 -109
package/src/core/config.ts
CHANGED
|
@@ -1,169 +1,169 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { agentPath, readJson, writeJson } from "./store.ts";
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* One file for everything pi-plus owns: `~/.pi/agent/pi-plus.json`.
|
|
6
|
-
*
|
|
7
|
-
* ```json
|
|
8
|
-
* {
|
|
9
|
-
* "env": { "ARTIFICIAL_ANALYSIS_API_KEY": "aa_…", "AGENT_BOARD_URL": "…" },
|
|
10
|
-
* "policy": { "autoApprove": [], "requireApproval": [], "deny": [] },
|
|
11
|
-
* "remote": { "injectStatus": true, "defaults": {}, "workers": [] }
|
|
12
|
-
* }
|
|
13
|
-
* ```
|
|
14
|
-
*
|
|
15
|
-
* Pi's own `settings.json` and `models.json` are deliberately NOT absorbed:
|
|
16
|
-
* pi reads those from fixed paths and folding them in here would break it.
|
|
17
|
-
*
|
|
18
|
-
* Every legacy file is migrated on first read and left on disk, so downgrading
|
|
19
|
-
* keeps working.
|
|
20
|
-
*/
|
|
21
|
-
|
|
22
|
-
export const CONFIG_FILE = "pi-plus.json";
|
|
23
|
-
|
|
24
|
-
export interface PolicySection {
|
|
25
|
-
autoApprove: string[];
|
|
26
|
-
requireApproval: string[];
|
|
27
|
-
deny: string[];
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
export interface RemoteSection {
|
|
31
|
-
injectStatus?: boolean;
|
|
32
|
-
defaults?: Record<string, unknown>;
|
|
33
|
-
workers: Record<string, unknown>[];
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export interface PiPlusConfig {
|
|
37
|
-
env: Record<string, string>;
|
|
38
|
-
policy: PolicySection;
|
|
39
|
-
remote: RemoteSection;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const DEFAULTS: PiPlusConfig = {
|
|
43
|
-
env: {},
|
|
44
|
-
policy: {
|
|
45
|
-
autoApprove: ["anthropic/*", "openai-codex/*"],
|
|
46
|
-
requireApproval: ["openrouter/*", "google/*", "openai/*", "xai/*"],
|
|
47
|
-
deny: [],
|
|
48
|
-
},
|
|
49
|
-
remote: { injectStatus: true, workers: [] },
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/** Legacy file -> section, applied only when that section is still absent. */
|
|
53
|
-
const MIGRATIONS: { file: string; apply: (raw: any, into: PiPlusConfig) => boolean }[] = [
|
|
54
|
-
{
|
|
55
|
-
file: "pi-plus.env.json",
|
|
56
|
-
apply: (raw, into) => {
|
|
57
|
-
if (Object.keys(into.env).length > 0 || !raw || typeof raw !== "object") return false;
|
|
58
|
-
for (const [key, value] of Object.entries(raw)) {
|
|
59
|
-
if (typeof value === "string" && value.trim()) into.env[key] = value.trim();
|
|
60
|
-
}
|
|
61
|
-
return true;
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
{
|
|
65
|
-
file: "model-quality-key.json",
|
|
66
|
-
apply: (raw, into) => {
|
|
67
|
-
if (into.env.ARTIFICIAL_ANALYSIS_API_KEY || typeof raw?.artificialAnalysis !== "string") return false;
|
|
68
|
-
into.env.ARTIFICIAL_ANALYSIS_API_KEY = raw.artificialAnalysis;
|
|
69
|
-
return true;
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
{
|
|
73
|
-
file: "agent-board.json",
|
|
74
|
-
apply: (raw, into) => {
|
|
75
|
-
if (into.env.AGENT_BOARD_URL || typeof raw?.url !== "string" || typeof raw?.token !== "string") return false;
|
|
76
|
-
into.env.AGENT_BOARD_URL = raw.url;
|
|
77
|
-
into.env.AGENT_BOARD_TOKEN = raw.token;
|
|
78
|
-
if (typeof raw.adminName === "string") into.env.AGENT_BOARD_NAME ??= raw.adminName;
|
|
79
|
-
return true;
|
|
80
|
-
},
|
|
81
|
-
},
|
|
82
|
-
{
|
|
83
|
-
file: "model-policy.json",
|
|
84
|
-
apply: (raw, into) => {
|
|
85
|
-
if (!raw || typeof raw !== "object" || !Array.isArray(raw.requireApproval)) return false;
|
|
86
|
-
into.policy = {
|
|
87
|
-
autoApprove: Array.isArray(raw.autoApprove) ? raw.autoApprove : DEFAULTS.policy.autoApprove,
|
|
88
|
-
requireApproval: raw.requireApproval,
|
|
89
|
-
deny: Array.isArray(raw.deny) ? raw.deny : [],
|
|
90
|
-
};
|
|
91
|
-
return true;
|
|
92
|
-
},
|
|
93
|
-
},
|
|
94
|
-
{
|
|
95
|
-
file: "remote.json",
|
|
96
|
-
apply: (raw, into) => {
|
|
97
|
-
if (into.remote.workers.length > 0 || !Array.isArray(raw?.workers)) return false;
|
|
98
|
-
into.remote = { injectStatus: raw.injectStatus !== false, defaults: raw.defaults, workers: raw.workers };
|
|
99
|
-
return true;
|
|
100
|
-
},
|
|
101
|
-
},
|
|
102
|
-
{
|
|
103
|
-
file: "remote-workers.json",
|
|
104
|
-
apply: (raw, into) => {
|
|
105
|
-
if (into.remote.workers.length > 0 || !Array.isArray(raw?.workers)) return false;
|
|
106
|
-
into.remote = { injectStatus: raw.injectStatus !== false, defaults: raw.defaults, workers: raw.workers };
|
|
107
|
-
return true;
|
|
108
|
-
},
|
|
109
|
-
},
|
|
110
|
-
];
|
|
111
|
-
|
|
112
|
-
let cache: PiPlusConfig | undefined;
|
|
113
|
-
|
|
114
|
-
export function configPath(): string {
|
|
115
|
-
return process.env.PI_PLUS_CONFIG ?? agentPath(CONFIG_FILE);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
function normalize(raw: Partial<PiPlusConfig> | undefined): PiPlusConfig {
|
|
119
|
-
return {
|
|
120
|
-
env: raw?.env && typeof raw.env === "object" ? { ...raw.env } : {},
|
|
121
|
-
policy: {
|
|
122
|
-
autoApprove: Array.isArray(raw?.policy?.autoApprove) ? raw.policy.autoApprove : DEFAULTS.policy.autoApprove,
|
|
123
|
-
requireApproval: Array.isArray(raw?.policy?.requireApproval)
|
|
124
|
-
? raw.policy.requireApproval
|
|
125
|
-
: DEFAULTS.policy.requireApproval,
|
|
126
|
-
deny: Array.isArray(raw?.policy?.deny) ? raw.policy.deny : [],
|
|
127
|
-
},
|
|
128
|
-
remote: {
|
|
129
|
-
injectStatus: raw?.remote?.injectStatus !== false,
|
|
130
|
-
defaults: raw?.remote?.defaults,
|
|
131
|
-
workers: Array.isArray(raw?.remote?.workers) ? raw.remote.workers : [],
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export function readConfig(): PiPlusConfig {
|
|
137
|
-
if (cache) return cache;
|
|
138
|
-
|
|
139
|
-
const path = configPath();
|
|
140
|
-
const exists = existsSync(path);
|
|
141
|
-
const config = normalize(exists ? readJson<Partial<PiPlusConfig>>(path, {}) : undefined);
|
|
142
|
-
|
|
143
|
-
// Only consider legacy sources when the unified file is absent or partial.
|
|
144
|
-
let migrated = false;
|
|
145
|
-
for (const migration of MIGRATIONS) {
|
|
146
|
-
const legacyPath = agentPath(migration.file);
|
|
147
|
-
if (!existsSync(legacyPath)) continue;
|
|
148
|
-
const raw = readJson<any>(legacyPath, undefined);
|
|
149
|
-
if (raw === undefined) continue;
|
|
150
|
-
if (migration.apply(raw, config)) migrated = true;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
if (!exists || migrated) writeJson(path, config, true);
|
|
154
|
-
cache = config;
|
|
155
|
-
return config;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/** Read-modify-write of the whole file, so sections never clobber each other. */
|
|
159
|
-
export function updateConfig(mutate: (config: PiPlusConfig) => void): boolean {
|
|
160
|
-
const config = readConfig();
|
|
161
|
-
mutate(config);
|
|
162
|
-
cache = config;
|
|
163
|
-
return writeJson(configPath(), config, true);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
/** Drops the in-memory copy. Used by tests and after external edits. */
|
|
167
|
-
export function resetConfigCache(): void {
|
|
168
|
-
cache = undefined;
|
|
169
|
-
}
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { agentPath, readJson, writeJson } from "./store.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* One file for everything pi-plus owns: `~/.pi/agent/pi-plus.json`.
|
|
6
|
+
*
|
|
7
|
+
* ```json
|
|
8
|
+
* {
|
|
9
|
+
* "env": { "ARTIFICIAL_ANALYSIS_API_KEY": "aa_…", "AGENT_BOARD_URL": "…" },
|
|
10
|
+
* "policy": { "autoApprove": [], "requireApproval": [], "deny": [] },
|
|
11
|
+
* "remote": { "injectStatus": true, "defaults": {}, "workers": [] }
|
|
12
|
+
* }
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Pi's own `settings.json` and `models.json` are deliberately NOT absorbed:
|
|
16
|
+
* pi reads those from fixed paths and folding them in here would break it.
|
|
17
|
+
*
|
|
18
|
+
* Every legacy file is migrated on first read and left on disk, so downgrading
|
|
19
|
+
* keeps working.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
export const CONFIG_FILE = "pi-plus.json";
|
|
23
|
+
|
|
24
|
+
export interface PolicySection {
|
|
25
|
+
autoApprove: string[];
|
|
26
|
+
requireApproval: string[];
|
|
27
|
+
deny: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RemoteSection {
|
|
31
|
+
injectStatus?: boolean;
|
|
32
|
+
defaults?: Record<string, unknown>;
|
|
33
|
+
workers: Record<string, unknown>[];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PiPlusConfig {
|
|
37
|
+
env: Record<string, string>;
|
|
38
|
+
policy: PolicySection;
|
|
39
|
+
remote: RemoteSection;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const DEFAULTS: PiPlusConfig = {
|
|
43
|
+
env: {},
|
|
44
|
+
policy: {
|
|
45
|
+
autoApprove: ["anthropic/*", "openai-codex/*"],
|
|
46
|
+
requireApproval: ["openrouter/*", "google/*", "openai/*", "xai/*"],
|
|
47
|
+
deny: [],
|
|
48
|
+
},
|
|
49
|
+
remote: { injectStatus: true, workers: [] },
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Legacy file -> section, applied only when that section is still absent. */
|
|
53
|
+
const MIGRATIONS: { file: string; apply: (raw: any, into: PiPlusConfig) => boolean }[] = [
|
|
54
|
+
{
|
|
55
|
+
file: "pi-plus.env.json",
|
|
56
|
+
apply: (raw, into) => {
|
|
57
|
+
if (Object.keys(into.env).length > 0 || !raw || typeof raw !== "object") return false;
|
|
58
|
+
for (const [key, value] of Object.entries(raw)) {
|
|
59
|
+
if (typeof value === "string" && value.trim()) into.env[key] = value.trim();
|
|
60
|
+
}
|
|
61
|
+
return true;
|
|
62
|
+
},
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
file: "model-quality-key.json",
|
|
66
|
+
apply: (raw, into) => {
|
|
67
|
+
if (into.env.ARTIFICIAL_ANALYSIS_API_KEY || typeof raw?.artificialAnalysis !== "string") return false;
|
|
68
|
+
into.env.ARTIFICIAL_ANALYSIS_API_KEY = raw.artificialAnalysis;
|
|
69
|
+
return true;
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
file: "agent-board.json",
|
|
74
|
+
apply: (raw, into) => {
|
|
75
|
+
if (into.env.AGENT_BOARD_URL || typeof raw?.url !== "string" || typeof raw?.token !== "string") return false;
|
|
76
|
+
into.env.AGENT_BOARD_URL = raw.url;
|
|
77
|
+
into.env.AGENT_BOARD_TOKEN = raw.token;
|
|
78
|
+
if (typeof raw.adminName === "string") into.env.AGENT_BOARD_NAME ??= raw.adminName;
|
|
79
|
+
return true;
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
file: "model-policy.json",
|
|
84
|
+
apply: (raw, into) => {
|
|
85
|
+
if (!raw || typeof raw !== "object" || !Array.isArray(raw.requireApproval)) return false;
|
|
86
|
+
into.policy = {
|
|
87
|
+
autoApprove: Array.isArray(raw.autoApprove) ? raw.autoApprove : DEFAULTS.policy.autoApprove,
|
|
88
|
+
requireApproval: raw.requireApproval,
|
|
89
|
+
deny: Array.isArray(raw.deny) ? raw.deny : [],
|
|
90
|
+
};
|
|
91
|
+
return true;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
file: "remote.json",
|
|
96
|
+
apply: (raw, into) => {
|
|
97
|
+
if (into.remote.workers.length > 0 || !Array.isArray(raw?.workers)) return false;
|
|
98
|
+
into.remote = { injectStatus: raw.injectStatus !== false, defaults: raw.defaults, workers: raw.workers };
|
|
99
|
+
return true;
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
file: "remote-workers.json",
|
|
104
|
+
apply: (raw, into) => {
|
|
105
|
+
if (into.remote.workers.length > 0 || !Array.isArray(raw?.workers)) return false;
|
|
106
|
+
into.remote = { injectStatus: raw.injectStatus !== false, defaults: raw.defaults, workers: raw.workers };
|
|
107
|
+
return true;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
];
|
|
111
|
+
|
|
112
|
+
let cache: PiPlusConfig | undefined;
|
|
113
|
+
|
|
114
|
+
export function configPath(): string {
|
|
115
|
+
return process.env.PI_PLUS_CONFIG ?? agentPath(CONFIG_FILE);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function normalize(raw: Partial<PiPlusConfig> | undefined): PiPlusConfig {
|
|
119
|
+
return {
|
|
120
|
+
env: raw?.env && typeof raw.env === "object" ? { ...raw.env } : {},
|
|
121
|
+
policy: {
|
|
122
|
+
autoApprove: Array.isArray(raw?.policy?.autoApprove) ? raw.policy.autoApprove : DEFAULTS.policy.autoApprove,
|
|
123
|
+
requireApproval: Array.isArray(raw?.policy?.requireApproval)
|
|
124
|
+
? raw.policy.requireApproval
|
|
125
|
+
: DEFAULTS.policy.requireApproval,
|
|
126
|
+
deny: Array.isArray(raw?.policy?.deny) ? raw.policy.deny : [],
|
|
127
|
+
},
|
|
128
|
+
remote: {
|
|
129
|
+
injectStatus: raw?.remote?.injectStatus !== false,
|
|
130
|
+
defaults: raw?.remote?.defaults,
|
|
131
|
+
workers: Array.isArray(raw?.remote?.workers) ? raw.remote.workers : [],
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function readConfig(): PiPlusConfig {
|
|
137
|
+
if (cache) return cache;
|
|
138
|
+
|
|
139
|
+
const path = configPath();
|
|
140
|
+
const exists = existsSync(path);
|
|
141
|
+
const config = normalize(exists ? readJson<Partial<PiPlusConfig>>(path, {}) : undefined);
|
|
142
|
+
|
|
143
|
+
// Only consider legacy sources when the unified file is absent or partial.
|
|
144
|
+
let migrated = false;
|
|
145
|
+
for (const migration of MIGRATIONS) {
|
|
146
|
+
const legacyPath = agentPath(migration.file);
|
|
147
|
+
if (!existsSync(legacyPath)) continue;
|
|
148
|
+
const raw = readJson<any>(legacyPath, undefined);
|
|
149
|
+
if (raw === undefined) continue;
|
|
150
|
+
if (migration.apply(raw, config)) migrated = true;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!exists || migrated) writeJson(path, config, true);
|
|
154
|
+
cache = config;
|
|
155
|
+
return config;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Read-modify-write of the whole file, so sections never clobber each other. */
|
|
159
|
+
export function updateConfig(mutate: (config: PiPlusConfig) => void): boolean {
|
|
160
|
+
const config = readConfig();
|
|
161
|
+
mutate(config);
|
|
162
|
+
cache = config;
|
|
163
|
+
return writeJson(configPath(), config, true);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Drops the in-memory copy. Used by tests and after external edits. */
|
|
167
|
+
export function resetConfigCache(): void {
|
|
168
|
+
cache = undefined;
|
|
169
|
+
}
|
package/src/core/env.ts
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
import { readConfig, resetConfigCache, updateConfig } from "./config.ts";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Credentials and endpoints, stored in the `env` section of `pi-plus.json`.
|
|
5
|
-
*
|
|
6
|
-
* Precedence is `process.env` first, then the file. That keeps CI and one-off
|
|
7
|
-
* shell overrides working exactly as people expect, while giving the TUI a
|
|
8
|
-
* single place to read and write.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
export type EnvKey =
|
|
12
|
-
| "ARTIFICIAL_ANALYSIS_API_KEY"
|
|
13
|
-
| "AGENT_BOARD_URL"
|
|
14
|
-
| "AGENT_BOARD_TOKEN"
|
|
15
|
-
| "AGENT_BOARD_NAME"
|
|
16
|
-
| "AGENT_BOARD_MODE"
|
|
17
|
-
| "AGENT_BOARD_SSH";
|
|
18
|
-
|
|
19
|
-
export const ENV_KEYS: { key: EnvKey; label: string; secret: boolean }[] = [
|
|
20
|
-
{ key: "ARTIFICIAL_ANALYSIS_API_KEY", label: "Artificial Analysis API key", secret: true },
|
|
21
|
-
{ key: "AGENT_BOARD_URL", label: "Agent board URL", secret: false },
|
|
22
|
-
{ key: "AGENT_BOARD_TOKEN", label: "Agent board token", secret: true },
|
|
23
|
-
{ key: "AGENT_BOARD_NAME", label: "Agent board display name", secret: false },
|
|
24
|
-
{ key: "AGENT_BOARD_MODE", label: "Agent board deployment (local|remote|external)", secret: false },
|
|
25
|
-
{ key: "AGENT_BOARD_SSH", label: "Agent board SSH host, when remote", secret: false },
|
|
26
|
-
];
|
|
27
|
-
|
|
28
|
-
/** Reads a setting: real environment first, then the config file. */
|
|
29
|
-
export function env(key: EnvKey): string | undefined {
|
|
30
|
-
const fromProcess = process.env[key];
|
|
31
|
-
if (typeof fromProcess === "string" && fromProcess.trim()) return fromProcess.trim();
|
|
32
|
-
const value = readConfig().env[key];
|
|
33
|
-
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** Writes a setting. Passing undefined or blank removes it. */
|
|
37
|
-
export function setEnv(key: EnvKey, value: string | undefined): boolean {
|
|
38
|
-
return updateConfig((config) => {
|
|
39
|
-
if (value === undefined || value.trim() === "") delete config.env[key];
|
|
40
|
-
else config.env[key] = value.trim();
|
|
41
|
-
});
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/** True when the real environment supplies this key, so the file is ignored. */
|
|
45
|
-
export function isFromProcessEnv(key: EnvKey): boolean {
|
|
46
|
-
const value = process.env[key];
|
|
47
|
-
return typeof value === "string" && value.trim() !== "";
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/** `aa_vaoi…rXcC`: enough to recognise, not enough to use. */
|
|
51
|
-
export function maskSecret(value: string): string {
|
|
52
|
-
if (value.length <= 10) return "•".repeat(value.length);
|
|
53
|
-
return `${value.slice(0, 5)}…${value.slice(-4)}`;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function resetEnvCache(): void {
|
|
57
|
-
resetConfigCache();
|
|
58
|
-
}
|
|
1
|
+
import { readConfig, resetConfigCache, updateConfig } from "./config.ts";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Credentials and endpoints, stored in the `env` section of `pi-plus.json`.
|
|
5
|
+
*
|
|
6
|
+
* Precedence is `process.env` first, then the file. That keeps CI and one-off
|
|
7
|
+
* shell overrides working exactly as people expect, while giving the TUI a
|
|
8
|
+
* single place to read and write.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export type EnvKey =
|
|
12
|
+
| "ARTIFICIAL_ANALYSIS_API_KEY"
|
|
13
|
+
| "AGENT_BOARD_URL"
|
|
14
|
+
| "AGENT_BOARD_TOKEN"
|
|
15
|
+
| "AGENT_BOARD_NAME"
|
|
16
|
+
| "AGENT_BOARD_MODE"
|
|
17
|
+
| "AGENT_BOARD_SSH";
|
|
18
|
+
|
|
19
|
+
export const ENV_KEYS: { key: EnvKey; label: string; secret: boolean }[] = [
|
|
20
|
+
{ key: "ARTIFICIAL_ANALYSIS_API_KEY", label: "Artificial Analysis API key", secret: true },
|
|
21
|
+
{ key: "AGENT_BOARD_URL", label: "Agent board URL", secret: false },
|
|
22
|
+
{ key: "AGENT_BOARD_TOKEN", label: "Agent board token", secret: true },
|
|
23
|
+
{ key: "AGENT_BOARD_NAME", label: "Agent board display name", secret: false },
|
|
24
|
+
{ key: "AGENT_BOARD_MODE", label: "Agent board deployment (local|remote|external)", secret: false },
|
|
25
|
+
{ key: "AGENT_BOARD_SSH", label: "Agent board SSH host, when remote", secret: false },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/** Reads a setting: real environment first, then the config file. */
|
|
29
|
+
export function env(key: EnvKey): string | undefined {
|
|
30
|
+
const fromProcess = process.env[key];
|
|
31
|
+
if (typeof fromProcess === "string" && fromProcess.trim()) return fromProcess.trim();
|
|
32
|
+
const value = readConfig().env[key];
|
|
33
|
+
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Writes a setting. Passing undefined or blank removes it. */
|
|
37
|
+
export function setEnv(key: EnvKey, value: string | undefined): boolean {
|
|
38
|
+
return updateConfig((config) => {
|
|
39
|
+
if (value === undefined || value.trim() === "") delete config.env[key];
|
|
40
|
+
else config.env[key] = value.trim();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** True when the real environment supplies this key, so the file is ignored. */
|
|
45
|
+
export function isFromProcessEnv(key: EnvKey): boolean {
|
|
46
|
+
const value = process.env[key];
|
|
47
|
+
return typeof value === "string" && value.trim() !== "";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** `aa_vaoi…rXcC`: enough to recognise, not enough to use. */
|
|
51
|
+
export function maskSecret(value: string): string {
|
|
52
|
+
if (value.length <= 10) return "•".repeat(value.length);
|
|
53
|
+
return `${value.slice(0, 5)}…${value.slice(-4)}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function resetEnvCache(): void {
|
|
57
|
+
resetConfigCache();
|
|
58
|
+
}
|