abelworkflow 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -182
- package/bin/abelworkflow.mjs +5 -3
- package/extensions/pi-gpt-responses-compat/index.ts +1 -1
- package/lib/cli/args.mjs +76 -0
- package/lib/cli/main.mjs +385 -0
- package/lib/cli/prompts.mjs +106 -0
- package/lib/config/dotenv.mjs +102 -0
- package/lib/config/jsonc.mjs +80 -0
- package/lib/config/store.mjs +310 -0
- package/lib/config/toml.mjs +638 -0
- package/lib/installer/assets.mjs +324 -0
- package/lib/installer/install.mjs +153 -0
- package/lib/installer/links.mjs +427 -0
- package/lib/installer/state.mjs +163 -0
- package/lib/paths.mjs +99 -0
- package/lib/providers/claude.mjs +197 -0
- package/lib/providers/codex.mjs +433 -0
- package/lib/providers/pi.mjs +577 -0
- package/lib/providers/skills.mjs +105 -0
- package/lib/templates/codex/agents/default.toml +7 -0
- package/lib/templates/codex/agents/explorer.toml +7 -0
- package/lib/templates/codex/agents/planner.toml +7 -0
- package/lib/templates/codex/agents/reviewer.toml +7 -0
- package/lib/templates/codex/agents/worker.toml +7 -0
- package/lib/templates/codex/config-base.toml +11 -3
- package/lib/templates/workflow/AGENTS.md +50 -0
- package/lib/templates/workflow/commands/abel-design.md +175 -0
- package/{commands → lib/templates/workflow/commands}/abel-diagnose.md +5 -5
- package/lib/templates/workflow/commands/abel-implement.md +170 -0
- package/{commands → lib/templates/workflow/commands}/abel-init.md +3 -5
- package/{.gitignore → lib/templates/workflow/gitignore.template} +0 -2
- package/lib/tools/cli-installer.mjs +277 -0
- package/package.json +37 -10
- package/skills/context7-auto-research/SKILL.md +5 -5
- package/skills/context7-auto-research/{context7-api.js → context7-api.cjs} +5 -8
- package/skills/dev-browser/SKILL.md +53 -33
- package/skills/dev-browser/dist/scripts/start.d.ts +2 -0
- package/skills/dev-browser/dist/scripts/start.d.ts.map +1 -0
- package/skills/dev-browser/dist/scripts/start.js +118 -0
- package/skills/dev-browser/dist/scripts/start.js.map +1 -0
- package/skills/dev-browser/dist/src/client.d.ts +93 -0
- package/skills/dev-browser/dist/src/client.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/client.js +310 -0
- package/skills/dev-browser/dist/src/client.js.map +1 -0
- package/skills/dev-browser/dist/src/entrypoint.d.ts +29 -0
- package/skills/dev-browser/dist/src/entrypoint.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/entrypoint.js +106 -0
- package/skills/dev-browser/dist/src/entrypoint.js.map +1 -0
- package/skills/dev-browser/dist/src/index.d.ts +4 -0
- package/skills/dev-browser/dist/src/index.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/index.js +2 -0
- package/skills/dev-browser/dist/src/index.js.map +1 -0
- package/skills/dev-browser/dist/src/page-api.d.ts +25 -0
- package/skills/dev-browser/dist/src/page-api.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/page-api.js +104 -0
- package/skills/dev-browser/dist/src/page-api.js.map +1 -0
- package/skills/dev-browser/dist/src/relay.d.ts +27 -0
- package/skills/dev-browser/dist/src/relay.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/relay.js +521 -0
- package/skills/dev-browser/dist/src/relay.js.map +1 -0
- package/skills/dev-browser/dist/src/runtime.d.ts +45 -0
- package/skills/dev-browser/dist/src/runtime.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/runtime.js +54 -0
- package/skills/dev-browser/dist/src/runtime.js.map +1 -0
- package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts +23 -0
- package/skills/dev-browser/dist/src/snapshot/browser-script.d.ts.map +1 -0
- package/skills/dev-browser/{src/snapshot/browser-script.ts → dist/src/snapshot/browser-script.js} +21 -30
- package/skills/dev-browser/dist/src/snapshot/browser-script.js.map +1 -0
- package/skills/dev-browser/dist/src/snapshot/index.d.ts +14 -0
- package/skills/dev-browser/dist/src/snapshot/index.d.ts.map +1 -0
- package/skills/dev-browser/{src/snapshot/index.ts → dist/src/snapshot/index.js} +2 -2
- package/skills/dev-browser/dist/src/snapshot/index.js.map +1 -0
- package/skills/dev-browser/dist/src/snapshot/inject.d.ts +13 -0
- package/skills/dev-browser/dist/src/snapshot/inject.d.ts.map +1 -0
- package/skills/dev-browser/{src/snapshot/inject.ts → dist/src/snapshot/inject.js} +2 -2
- package/skills/dev-browser/dist/src/snapshot/inject.js.map +1 -0
- package/skills/dev-browser/dist/src/standalone.d.ts +32 -0
- package/skills/dev-browser/dist/src/standalone.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/standalone.js +174 -0
- package/skills/dev-browser/dist/src/standalone.js.map +1 -0
- package/skills/dev-browser/dist/src/startup.d.ts +55 -0
- package/skills/dev-browser/dist/src/startup.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/startup.js +81 -0
- package/skills/dev-browser/dist/src/startup.js.map +1 -0
- package/skills/dev-browser/dist/src/target-registry.d.ts +29 -0
- package/skills/dev-browser/dist/src/target-registry.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/target-registry.js +135 -0
- package/skills/dev-browser/dist/src/target-registry.js.map +1 -0
- package/skills/dev-browser/dist/src/types.d.ts +27 -0
- package/skills/dev-browser/dist/src/types.d.ts.map +1 -0
- package/skills/dev-browser/dist/src/types.js +2 -0
- package/skills/dev-browser/dist/src/types.js.map +1 -0
- package/skills/dev-browser/package-lock.json +67 -1510
- package/skills/dev-browser/package.json +18 -14
- package/skills/dev-browser/references/scraping.md +94 -105
- package/skills/grok-search/.env.example +3 -3
- package/skills/grok-search/SKILL.md +1 -1
- package/skills/grok-search/defaults.json +3 -0
- package/skills/grok-search/gitignore.template +4 -0
- package/skills/grok-search/scripts/_dotenv.py +5 -1
- package/skills/grok-search/scripts/groksearch_cli.py +15 -33
- package/skills/prompt-enhancer/SKILL.md +2 -27
- package/skills/prompt-enhancer/TEMPLATE.md +0 -4
- package/.skill-lock.json +0 -29
- package/AGENTS.md +0 -45
- package/commands/abel-implement.md +0 -157
- package/commands/abel-plan.md +0 -88
- package/commands/abel-research.md +0 -126
- package/lib/cli/logic.mjs +0 -213
- package/lib/cli.mjs +0 -3016
- package/skills/confidence-check/SKILL.md +0 -34
- package/skills/confidence-check/confidence.ts +0 -276
- package/skills/dev-browser/bun.lock +0 -460
- package/skills/dev-browser/scripts/start.ts +0 -280
- package/skills/dev-browser/src/client.ts +0 -474
- package/skills/dev-browser/src/entrypoint.ts +0 -157
- package/skills/dev-browser/src/index.ts +0 -289
- package/skills/dev-browser/src/relay.ts +0 -731
- package/skills/dev-browser/src/runtime.test.ts +0 -60
- package/skills/dev-browser/src/runtime.ts +0 -171
- package/skills/dev-browser/src/startup.test.ts +0 -95
- package/skills/dev-browser/src/startup.ts +0 -153
- package/skills/dev-browser/src/types.ts +0 -35
- package/skills/dev-browser/tsconfig.json +0 -36
- package/skills/dev-browser/vitest.config.ts +0 -12
- package/skills/prompt-enhancer/.env.example +0 -12
- package/skills/prompt-enhancer/ADVANCED.md +0 -103
- package/skills/prompt-enhancer/requirements.txt +0 -2
- package/skills/prompt-enhancer/scripts/_dotenv.py +0 -28
- package/skills/prompt-enhancer/scripts/enhance.py +0 -191
- package/skills/prompt-enhancer/scripts/prompt_enhancer_entry.py +0 -253
- package/skills/sequential-think/SKILL.md +0 -198
- package/skills/sequential-think/scripts/.env.example +0 -5
- package/skills/sequential-think/scripts/sequential_think_cli.py +0 -253
|
@@ -0,0 +1,577 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { piInsecureTlsHeader } from "../../extensions/pi-gpt-responses-compat/tls-fetch.mjs";
|
|
5
|
+
import {
|
|
6
|
+
readJsonFileSafe,
|
|
7
|
+
readJsoncFileSafe,
|
|
8
|
+
updateLockedJson,
|
|
9
|
+
writeJsonFileWithBackup,
|
|
10
|
+
writeText
|
|
11
|
+
} from "../config/store.mjs";
|
|
12
|
+
import { defaultPaths, maskSecret, pathToLabel } from "../paths.mjs";
|
|
13
|
+
|
|
14
|
+
const minimumPiVersion = [0, 80, 0];
|
|
15
|
+
const piRpcRequestId = "abelworkflow-provider";
|
|
16
|
+
const piRpcArgs = [
|
|
17
|
+
"--mode", "rpc",
|
|
18
|
+
"--no-session",
|
|
19
|
+
"--offline",
|
|
20
|
+
"--no-context-files",
|
|
21
|
+
"--no-skills",
|
|
22
|
+
"--no-prompt-templates",
|
|
23
|
+
"--no-themes"
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
function requirePiProviderId(value) {
|
|
27
|
+
const providerId = typeof value === "string" ? value.trim() : "";
|
|
28
|
+
if (!providerId) {
|
|
29
|
+
throw new Error("未检测到 Pi 当前有效 Provider;请先在 Pi 中配置可用模型。");
|
|
30
|
+
}
|
|
31
|
+
return providerId;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function updatePiAuthFile(path, providerId, apiKey) {
|
|
35
|
+
const targetProviderId = requirePiProviderId(providerId);
|
|
36
|
+
return updateLockedJson(path, (auth) => {
|
|
37
|
+
if (!auth || typeof auth !== "object" || Array.isArray(auth)) {
|
|
38
|
+
throw new TypeError("Pi auth.json must contain a JSON object");
|
|
39
|
+
}
|
|
40
|
+
return buildPiAuthConfig(auth, targetProviderId, apiKey);
|
|
41
|
+
}, {
|
|
42
|
+
sensitive: true,
|
|
43
|
+
retries: {
|
|
44
|
+
retries: 10,
|
|
45
|
+
factor: 2,
|
|
46
|
+
minTimeout: 100,
|
|
47
|
+
maxTimeout: 10000,
|
|
48
|
+
randomize: true
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function parsePiVersion(value) {
|
|
54
|
+
const match = String(value || "").match(/(?:^|\D)v?(\d+)\.(\d+)\.(\d+)(?:\D|$)/u);
|
|
55
|
+
return match ? match.slice(1).map(Number) : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function assertSupportedPiVersion(value) {
|
|
59
|
+
const version = parsePiVersion(value);
|
|
60
|
+
if (!version) {
|
|
61
|
+
throw new Error("无法检测 Pi 版本(可能尚未安装);请先安装 Pi 0.80.0 或更高版本。");
|
|
62
|
+
}
|
|
63
|
+
for (let index = 0; index < minimumPiVersion.length; index += 1) {
|
|
64
|
+
if (version[index] > minimumPiVersion[index]) return version;
|
|
65
|
+
if (version[index] < minimumPiVersion[index]) {
|
|
66
|
+
throw new Error(`当前 Pi ${version.join(".")} 不支持 auth-only 自定义 Provider;请先升级到 Pi 0.80.0 或更高版本。`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return version;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function detectPiVersion() {
|
|
73
|
+
const result = spawnSync("pi", ["--version"], {
|
|
74
|
+
encoding: "utf8",
|
|
75
|
+
shell: process.platform === "win32",
|
|
76
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
77
|
+
});
|
|
78
|
+
return result.status === 0 ? `${result.stdout || ""} ${result.stderr || ""}`.trim() : undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parsePiRpcEffectiveModel(value) {
|
|
82
|
+
for (const line of String(value || "").split(/\r?\n/u)) {
|
|
83
|
+
let payload;
|
|
84
|
+
try {
|
|
85
|
+
payload = JSON.parse(line);
|
|
86
|
+
} catch {
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const model = payload?.type === "response"
|
|
90
|
+
&& payload.command === "get_state"
|
|
91
|
+
&& payload.success === true
|
|
92
|
+
? payload.data?.model
|
|
93
|
+
: undefined;
|
|
94
|
+
const provider = typeof model?.provider === "string" ? model.provider.trim() : "";
|
|
95
|
+
const id = typeof model?.id === "string" ? model.id.trim() : "";
|
|
96
|
+
if (!provider || !id) continue;
|
|
97
|
+
return {
|
|
98
|
+
provider,
|
|
99
|
+
id,
|
|
100
|
+
api: typeof model.api === "string" ? model.api.trim() : "",
|
|
101
|
+
baseUrl: typeof model.baseUrl === "string" ? model.baseUrl.trim() : ""
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function runPiRpcCommand(command, args, {
|
|
107
|
+
input = "",
|
|
108
|
+
maxBuffer = 1024 * 1024,
|
|
109
|
+
shell = process.platform === "win32",
|
|
110
|
+
start = spawn,
|
|
111
|
+
timeout = 20000
|
|
112
|
+
} = {}) {
|
|
113
|
+
return new Promise((resolve) => {
|
|
114
|
+
let child;
|
|
115
|
+
let stdout = "";
|
|
116
|
+
let settled = false;
|
|
117
|
+
let timer;
|
|
118
|
+
const finish = (status) => {
|
|
119
|
+
if (settled) return;
|
|
120
|
+
settled = true;
|
|
121
|
+
clearTimeout(timer);
|
|
122
|
+
child?.stdin?.destroy();
|
|
123
|
+
child?.stdout?.destroy();
|
|
124
|
+
child?.stderr?.destroy();
|
|
125
|
+
if (child?.exitCode === null && child.signalCode === null && !child.killed) child.kill();
|
|
126
|
+
child?.unref();
|
|
127
|
+
resolve({ status, stdout });
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const env = { ...process.env };
|
|
132
|
+
delete env.NODE_TEST_CONTEXT;
|
|
133
|
+
child = start(command, args, {
|
|
134
|
+
env,
|
|
135
|
+
shell,
|
|
136
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
137
|
+
windowsHide: true
|
|
138
|
+
});
|
|
139
|
+
} catch {
|
|
140
|
+
finish(null);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
timer = setTimeout(() => finish(null), timeout);
|
|
145
|
+
child.on("error", () => finish(null));
|
|
146
|
+
child.on("exit", (code) => finish(code));
|
|
147
|
+
child.stdout.setEncoding("utf8");
|
|
148
|
+
child.stdout.on("data", (chunk) => {
|
|
149
|
+
stdout += chunk;
|
|
150
|
+
if (Buffer.byteLength(stdout, "utf8") > maxBuffer) {
|
|
151
|
+
finish(null);
|
|
152
|
+
} else if (parsePiRpcEffectiveModel(stdout)) {
|
|
153
|
+
finish(0);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
child.stdin.on("error", () => finish(null));
|
|
157
|
+
child.stdin.write(input, (error) => {
|
|
158
|
+
if (error) finish(null);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async function detectPiEffectiveModel(run = runPiRpcCommand) {
|
|
164
|
+
let result;
|
|
165
|
+
try {
|
|
166
|
+
result = await run("pi", piRpcArgs, {
|
|
167
|
+
encoding: "utf8",
|
|
168
|
+
input: `${JSON.stringify({ id: piRpcRequestId, type: "get_state" })}\n`,
|
|
169
|
+
maxBuffer: 1024 * 1024,
|
|
170
|
+
shell: process.platform === "win32",
|
|
171
|
+
timeout: 20000
|
|
172
|
+
});
|
|
173
|
+
} catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
if (result?.error || result?.status !== 0) return undefined;
|
|
177
|
+
return parsePiRpcEffectiveModel(result.stdout);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function parsePiModelIds(value) {
|
|
181
|
+
return [...new Set(String(value || "")
|
|
182
|
+
.split(/[\n,]+/u)
|
|
183
|
+
.map((item) => item.trim())
|
|
184
|
+
.filter(Boolean))];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function getOpenAiUrlPathname(value) {
|
|
188
|
+
const input = String(value || "").trim();
|
|
189
|
+
try {
|
|
190
|
+
return new URL(input).pathname.replace(/\/+$/u, "");
|
|
191
|
+
} catch {
|
|
192
|
+
return input.split(/[?#]/u)[0].replace(/\/+$/u, "");
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function inferPiApiFromBaseUrl(value) {
|
|
197
|
+
const pathname = getOpenAiUrlPathname(value);
|
|
198
|
+
if (pathname.endsWith("/v1/chat/completions")) {
|
|
199
|
+
return "openai-completions";
|
|
200
|
+
}
|
|
201
|
+
if (pathname.endsWith("/v1/responses")) {
|
|
202
|
+
return "openai-responses";
|
|
203
|
+
}
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function normalizeOpenAiBaseUrl(value) {
|
|
208
|
+
const input = String(value || "").trim();
|
|
209
|
+
try {
|
|
210
|
+
const url = new URL(input);
|
|
211
|
+
let pathname = url.pathname.replace(/\/+$/u, "")
|
|
212
|
+
.replace(/\/v1\/(?:chat\/completions|responses)$/u, "/v1");
|
|
213
|
+
if (!pathname.endsWith("/v1")) {
|
|
214
|
+
pathname = `${pathname}/v1`;
|
|
215
|
+
}
|
|
216
|
+
url.pathname = pathname;
|
|
217
|
+
return url.toString();
|
|
218
|
+
} catch {
|
|
219
|
+
const baseUrl = input.replace(/\/+$/u, "")
|
|
220
|
+
.replace(/\/v1\/(?:chat\/completions|responses)$/u, "/v1");
|
|
221
|
+
return baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function getPiApiPromptOptions() {
|
|
226
|
+
return [
|
|
227
|
+
{ value: "openai-completions", label: "OpenAI Chat Completions(推荐)" },
|
|
228
|
+
{ value: "openai-responses", label: "OpenAI Responses API" }
|
|
229
|
+
];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveExistingPiApiConfig(modelsConfig = {}, settings = {}, auth = {}, effectiveModel) {
|
|
233
|
+
const savedProviderId = typeof settings.defaultProvider === "string" ? settings.defaultProvider.trim() : "";
|
|
234
|
+
const savedModelId = typeof settings.defaultModel === "string" ? settings.defaultModel.trim() : "";
|
|
235
|
+
const effectiveProviderId = typeof effectiveModel?.provider === "string" ? effectiveModel.provider.trim() : "";
|
|
236
|
+
const effectiveModelId = typeof effectiveModel?.id === "string" ? effectiveModel.id.trim() : "";
|
|
237
|
+
const savedProvider = savedProviderId
|
|
238
|
+
&& modelsConfig.providers?.[savedProviderId]
|
|
239
|
+
&& typeof modelsConfig.providers[savedProviderId] === "object"
|
|
240
|
+
? modelsConfig.providers[savedProviderId]
|
|
241
|
+
: undefined;
|
|
242
|
+
const savedModels = Array.isArray(savedProvider?.models) ? savedProvider.models : [];
|
|
243
|
+
const savedTargetExists = savedProviderId
|
|
244
|
+
&& savedModelId
|
|
245
|
+
&& savedModels.some((model) => model?.id === savedModelId);
|
|
246
|
+
const providerId = effectiveProviderId && effectiveModelId
|
|
247
|
+
? effectiveProviderId
|
|
248
|
+
: savedTargetExists ? savedProviderId : "";
|
|
249
|
+
const defaultModel = effectiveProviderId && effectiveModelId
|
|
250
|
+
? effectiveModelId
|
|
251
|
+
: savedTargetExists ? savedModelId : "";
|
|
252
|
+
const provider = providerId
|
|
253
|
+
&& modelsConfig.providers?.[providerId]
|
|
254
|
+
&& typeof modelsConfig.providers[providerId] === "object"
|
|
255
|
+
? modelsConfig.providers[providerId]
|
|
256
|
+
: {};
|
|
257
|
+
const credential = providerId && auth[providerId] && typeof auth[providerId] === "object"
|
|
258
|
+
? auth[providerId]
|
|
259
|
+
: {};
|
|
260
|
+
const authApiKey = credential.type === "api_key" && typeof credential.key === "string"
|
|
261
|
+
? credential.key
|
|
262
|
+
: "";
|
|
263
|
+
const models = Array.isArray(provider.models) ? provider.models.filter((model) => model?.id) : [];
|
|
264
|
+
const model = models.find((item) => item.id === defaultModel) || {};
|
|
265
|
+
const runtimeMatches = Boolean(effectiveProviderId
|
|
266
|
+
&& effectiveModelId
|
|
267
|
+
&& effectiveProviderId === providerId
|
|
268
|
+
&& effectiveModelId === defaultModel);
|
|
269
|
+
return {
|
|
270
|
+
providerId,
|
|
271
|
+
baseUrl: runtimeMatches && typeof effectiveModel.baseUrl === "string"
|
|
272
|
+
? effectiveModel.baseUrl
|
|
273
|
+
: typeof model.baseUrl === "string" ? model.baseUrl
|
|
274
|
+
: typeof provider.baseUrl === "string" ? provider.baseUrl : "",
|
|
275
|
+
api: runtimeMatches && typeof effectiveModel.api === "string"
|
|
276
|
+
? effectiveModel.api
|
|
277
|
+
: typeof model.api === "string" ? model.api
|
|
278
|
+
: typeof provider.api === "string" ? provider.api : "",
|
|
279
|
+
apiKey: authApiKey || (typeof provider.apiKey === "string" ? provider.apiKey : ""),
|
|
280
|
+
modelIds: models.map((model) => model.id),
|
|
281
|
+
defaultModel
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function assertConfigurablePiProvider(modelsConfig = {}, configuration = {}) {
|
|
286
|
+
const { providerId, defaultModel, api } = configuration;
|
|
287
|
+
const provider = modelsConfig.providers?.[providerId];
|
|
288
|
+
const models = Array.isArray(provider?.models) ? provider.models : [];
|
|
289
|
+
if (!provider || typeof provider !== "object" || !models.some((model) => model?.id === defaultModel)) {
|
|
290
|
+
throw new Error(`Pi 当前有效 Provider ${providerId || "未知"} 不是 models.json 中的自定义 Provider;为避免覆盖内置模型,已停止配置。`);
|
|
291
|
+
}
|
|
292
|
+
const supportedApis = new Set(getPiApiPromptOptions().map((option) => option.value));
|
|
293
|
+
if (api && !supportedApis.has(api)) {
|
|
294
|
+
throw new Error(`Pi 当前有效 Provider ${providerId} 使用不受支持的 API 类型 ${api};此配置器仅支持 OpenAI-compatible API。`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function buildPiModelConfig(modelId, existingModel = {}) {
|
|
299
|
+
const model = { ...existingModel };
|
|
300
|
+
delete model.baseUrl;
|
|
301
|
+
delete model.api;
|
|
302
|
+
return {
|
|
303
|
+
...model,
|
|
304
|
+
id: modelId,
|
|
305
|
+
name: model.name || modelId,
|
|
306
|
+
reasoning: model.reasoning ?? true,
|
|
307
|
+
input: Array.isArray(model.input) ? model.input : ["text", "image"],
|
|
308
|
+
contextWindow: model.contextWindow ?? 262144,
|
|
309
|
+
maxTokens: model.maxTokens ?? 64000
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function hasPiInsecureTlsSetting(modelsConfig = {}, providerId) {
|
|
314
|
+
const headers = modelsConfig.providers?.[requirePiProviderId(providerId)]?.headers;
|
|
315
|
+
return headers && typeof headers === "object"
|
|
316
|
+
? Object.keys(headers).some((key) => key.toLowerCase() === piInsecureTlsHeader)
|
|
317
|
+
: false;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function buildPiModelsConfig(modelsConfig = {}, {
|
|
321
|
+
providerId,
|
|
322
|
+
baseUrl,
|
|
323
|
+
api,
|
|
324
|
+
modelIds,
|
|
325
|
+
insecureTls = false
|
|
326
|
+
}) {
|
|
327
|
+
const targetProviderId = requirePiProviderId(providerId);
|
|
328
|
+
const providers = modelsConfig.providers && typeof modelsConfig.providers === "object" ? modelsConfig.providers : {};
|
|
329
|
+
const currentProvider = providers[targetProviderId] && typeof providers[targetProviderId] === "object"
|
|
330
|
+
? providers[targetProviderId]
|
|
331
|
+
: {};
|
|
332
|
+
const headers = currentProvider.headers && typeof currentProvider.headers === "object"
|
|
333
|
+
? { ...currentProvider.headers }
|
|
334
|
+
: {};
|
|
335
|
+
for (const key of Object.keys(headers)) {
|
|
336
|
+
if (key.toLowerCase() === piInsecureTlsHeader) {
|
|
337
|
+
delete headers[key];
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (insecureTls) {
|
|
341
|
+
headers[piInsecureTlsHeader] = new URL(baseUrl).origin;
|
|
342
|
+
}
|
|
343
|
+
const existingModels = new Map(
|
|
344
|
+
(Array.isArray(currentProvider.models) ? currentProvider.models : [])
|
|
345
|
+
.filter((model) => model?.id)
|
|
346
|
+
.map((model) => [model.id, model])
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const provider = {
|
|
350
|
+
...currentProvider,
|
|
351
|
+
baseUrl,
|
|
352
|
+
api,
|
|
353
|
+
compat: {
|
|
354
|
+
...(currentProvider.compat && typeof currentProvider.compat === "object" ? currentProvider.compat : {}),
|
|
355
|
+
supportsDeveloperRole: false
|
|
356
|
+
},
|
|
357
|
+
models: modelIds.map((modelId) => buildPiModelConfig(modelId, existingModels.get(modelId)))
|
|
358
|
+
};
|
|
359
|
+
delete provider.apiKey;
|
|
360
|
+
if (Object.keys(headers).length) {
|
|
361
|
+
provider.headers = headers;
|
|
362
|
+
} else {
|
|
363
|
+
delete provider.headers;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
...modelsConfig,
|
|
368
|
+
providers: {
|
|
369
|
+
...providers,
|
|
370
|
+
[targetProviderId]: provider
|
|
371
|
+
}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
function buildPiAuthConfig(auth = {}, providerId, apiKey) {
|
|
376
|
+
const targetProviderId = requirePiProviderId(providerId);
|
|
377
|
+
const credential = auth[targetProviderId] && typeof auth[targetProviderId] === "object"
|
|
378
|
+
? auth[targetProviderId]
|
|
379
|
+
: {};
|
|
380
|
+
return {
|
|
381
|
+
...auth,
|
|
382
|
+
[targetProviderId]: {
|
|
383
|
+
...credential,
|
|
384
|
+
type: "api_key",
|
|
385
|
+
key: apiKey
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function buildPiSettingsConfig(settings = {}, providerId, defaultModel) {
|
|
391
|
+
return {
|
|
392
|
+
...settings,
|
|
393
|
+
defaultProvider: requirePiProviderId(providerId),
|
|
394
|
+
defaultModel,
|
|
395
|
+
defaultThinkingLevel: settings.defaultThinkingLevel || "high",
|
|
396
|
+
enableSkillCommands: settings.enableSkillCommands ?? true
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function readExistingPiConfiguration(paths, operations = {}) {
|
|
401
|
+
const readAuth = operations.readAuth ?? ((path) => readJsonFileSafe(path, {}, { sensitive: true }));
|
|
402
|
+
const readModels = operations.readModels ?? ((path) => readJsoncFileSafe(path, {}, { sensitive: true }));
|
|
403
|
+
const readSettings = operations.readSettings ?? ((path) => readJsonFileSafe(path, {}));
|
|
404
|
+
const auth = await readAuth(paths.piAuthPath);
|
|
405
|
+
const models = await readModels(paths.piModelsPath);
|
|
406
|
+
const settings = await readSettings(paths.piSettingsPath);
|
|
407
|
+
return { auth, models, settings };
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function buildPiConfiguration({ auth, models, settings }, {
|
|
411
|
+
providerId,
|
|
412
|
+
apiKey,
|
|
413
|
+
baseUrl,
|
|
414
|
+
api,
|
|
415
|
+
modelIds,
|
|
416
|
+
insecureTls,
|
|
417
|
+
defaultModel
|
|
418
|
+
}) {
|
|
419
|
+
const targetProviderId = requirePiProviderId(providerId);
|
|
420
|
+
return {
|
|
421
|
+
providerId: targetProviderId,
|
|
422
|
+
apiKey,
|
|
423
|
+
auth: buildPiAuthConfig(auth, targetProviderId, apiKey),
|
|
424
|
+
models: buildPiModelsConfig(models, {
|
|
425
|
+
providerId: targetProviderId,
|
|
426
|
+
baseUrl,
|
|
427
|
+
api,
|
|
428
|
+
modelIds,
|
|
429
|
+
insecureTls
|
|
430
|
+
}),
|
|
431
|
+
settings: buildPiSettingsConfig(settings, targetProviderId, defaultModel)
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
async function persistPiConfiguration(paths, configuration, operations = {}) {
|
|
436
|
+
const updateAuth = operations.updateAuth ?? updatePiAuthFile;
|
|
437
|
+
const writeModels = operations.writeModels ?? ((path, value) => writeText(
|
|
438
|
+
path,
|
|
439
|
+
`${JSON.stringify(value, null, 2)}\n`,
|
|
440
|
+
{ sensitive: true }
|
|
441
|
+
));
|
|
442
|
+
const writeSettings = operations.writeSettings ?? ((path, value) => writeJsonFileWithBackup(path, value));
|
|
443
|
+
await updateAuth(paths.piAuthPath, configuration.providerId, configuration.apiKey);
|
|
444
|
+
await writeModels(paths.piModelsPath, configuration.models);
|
|
445
|
+
await writeSettings(paths.piSettingsPath, configuration.settings);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function configurePiApi(paths = defaultPaths, ensurePiResourcesLinked = async () => {}, promptApi, runtime = {}) {
|
|
449
|
+
const piVersion = await (runtime.getPiVersion ?? detectPiVersion)();
|
|
450
|
+
try {
|
|
451
|
+
assertSupportedPiVersion(piVersion);
|
|
452
|
+
} catch (error) {
|
|
453
|
+
(runtime.log ?? p.log).warn(error.message || String(error));
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
456
|
+
const {
|
|
457
|
+
assertNotCancelled,
|
|
458
|
+
confirmOrCancel,
|
|
459
|
+
passwordPromptOptions,
|
|
460
|
+
required,
|
|
461
|
+
requiredUnlessExisting,
|
|
462
|
+
resolvePasswordValue,
|
|
463
|
+
selectOrCancel
|
|
464
|
+
} = promptApi;
|
|
465
|
+
const {
|
|
466
|
+
auth,
|
|
467
|
+
models: modelsConfig,
|
|
468
|
+
settings
|
|
469
|
+
} = await readExistingPiConfiguration(paths);
|
|
470
|
+
const detectionSpinner = p.spinner();
|
|
471
|
+
detectionSpinner.start("正在识别 Pi 当前有效模型");
|
|
472
|
+
let effectiveModel;
|
|
473
|
+
try {
|
|
474
|
+
effectiveModel = await (runtime.getPiEffectiveModel ?? detectPiEffectiveModel)();
|
|
475
|
+
} finally {
|
|
476
|
+
detectionSpinner.stop(effectiveModel
|
|
477
|
+
? `已识别 ${effectiveModel.provider}/${effectiveModel.id}`
|
|
478
|
+
: "未识别到 Pi 当前有效模型");
|
|
479
|
+
}
|
|
480
|
+
const existing = resolveExistingPiApiConfig(modelsConfig, settings, auth, effectiveModel);
|
|
481
|
+
const providerId = requirePiProviderId(existing.providerId);
|
|
482
|
+
assertConfigurablePiProvider(modelsConfig, existing);
|
|
483
|
+
if (effectiveModel
|
|
484
|
+
&& (settings.defaultProvider !== existing.providerId || settings.defaultModel !== existing.defaultModel)) {
|
|
485
|
+
p.log.warn(`Pi 保存的默认模型 ${settings.defaultProvider || "未知"}/${settings.defaultModel || "未知"} 与当前有效模型 ${existing.providerId}/${existing.defaultModel} 不同;将配置当前有效模型。`);
|
|
486
|
+
}
|
|
487
|
+
const providerLabel = `Pi ${providerId}`;
|
|
488
|
+
|
|
489
|
+
const baseUrlInput = await p.text({
|
|
490
|
+
message: `${providerLabel} Base URL`,
|
|
491
|
+
initialValue: existing.baseUrl,
|
|
492
|
+
validate: required()
|
|
493
|
+
});
|
|
494
|
+
assertNotCancelled(baseUrlInput);
|
|
495
|
+
const inferredApi = inferPiApiFromBaseUrl(baseUrlInput);
|
|
496
|
+
const baseUrl = normalizeOpenAiBaseUrl(baseUrlInput);
|
|
497
|
+
|
|
498
|
+
const insecureTls = await confirmOrCancel({
|
|
499
|
+
message: `是否仅为 ${providerLabel} 中转请求跳过 TLS 证书校验?仅证书无法修复时启用`,
|
|
500
|
+
initialValue: hasPiInsecureTlsSetting(modelsConfig, providerId)
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
const piApiOptions = getPiApiPromptOptions();
|
|
504
|
+
const initialApi = piApiOptions.some((option) => option.value === existing.api)
|
|
505
|
+
? existing.api
|
|
506
|
+
: undefined;
|
|
507
|
+
const api = inferredApi || await selectOrCancel({
|
|
508
|
+
message: `${providerLabel} API 类型`,
|
|
509
|
+
options: piApiOptions,
|
|
510
|
+
...(initialApi ? { initialValue: initialApi } : {})
|
|
511
|
+
});
|
|
512
|
+
|
|
513
|
+
const apiKey = await p.password(passwordPromptOptions(
|
|
514
|
+
`${providerLabel} API Key`,
|
|
515
|
+
existing.apiKey,
|
|
516
|
+
requiredUnlessExisting(existing.apiKey, "API Key 不能为空")
|
|
517
|
+
));
|
|
518
|
+
assertNotCancelled(apiKey);
|
|
519
|
+
const finalApiKey = resolvePasswordValue(apiKey, existing.apiKey);
|
|
520
|
+
|
|
521
|
+
const modelIdsText = await p.text({
|
|
522
|
+
message: `${providerLabel} 模型 ID(多个用逗号分隔)`,
|
|
523
|
+
initialValue: (existing.modelIds.length
|
|
524
|
+
? existing.modelIds
|
|
525
|
+
: existing.defaultModel ? [existing.defaultModel] : []).join(","),
|
|
526
|
+
validate: (value) => parsePiModelIds(value).length ? undefined : "至少需要一个模型 ID"
|
|
527
|
+
});
|
|
528
|
+
assertNotCancelled(modelIdsText);
|
|
529
|
+
const modelIds = parsePiModelIds(modelIdsText);
|
|
530
|
+
|
|
531
|
+
const defaultModel = await p.text({
|
|
532
|
+
message: "Pi 默认模型",
|
|
533
|
+
initialValue: modelIds.includes(existing.defaultModel) ? existing.defaultModel : modelIds[0],
|
|
534
|
+
validate: (value) => modelIds.includes(String(value || "").trim()) ? undefined : "默认模型必须在模型 ID 列表中"
|
|
535
|
+
});
|
|
536
|
+
assertNotCancelled(defaultModel);
|
|
537
|
+
|
|
538
|
+
const finalDefaultModel = String(defaultModel).trim();
|
|
539
|
+
const configuration = buildPiConfiguration({ auth, models: modelsConfig, settings }, {
|
|
540
|
+
providerId,
|
|
541
|
+
apiKey: finalApiKey,
|
|
542
|
+
baseUrl,
|
|
543
|
+
api,
|
|
544
|
+
modelIds,
|
|
545
|
+
insecureTls,
|
|
546
|
+
defaultModel: finalDefaultModel
|
|
547
|
+
});
|
|
548
|
+
await ensurePiResourcesLinked(paths);
|
|
549
|
+
await persistPiConfiguration(paths, configuration);
|
|
550
|
+
|
|
551
|
+
p.log.step(`已更新 ${pathToLabel(paths.piModelsPath, paths.homeDir)} (${providerId}, ${baseUrl})`);
|
|
552
|
+
p.log.step(`已更新 ${pathToLabel(paths.piSettingsPath, paths.homeDir)} (默认模型: ${finalDefaultModel})`);
|
|
553
|
+
p.log.step(`已更新 ${pathToLabel(paths.piAuthPath, paths.homeDir)} (${maskSecret(finalApiKey)})`);
|
|
554
|
+
p.log.step(`已链接 Pi 扩展到 ${pathToLabel(join(paths.piAgentDir, "extensions"), paths.homeDir)}`);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
export {
|
|
558
|
+
assertConfigurablePiProvider,
|
|
559
|
+
assertSupportedPiVersion,
|
|
560
|
+
buildPiAuthConfig,
|
|
561
|
+
buildPiConfiguration,
|
|
562
|
+
buildPiModelsConfig,
|
|
563
|
+
buildPiSettingsConfig,
|
|
564
|
+
configurePiApi,
|
|
565
|
+
detectPiEffectiveModel,
|
|
566
|
+
getPiApiPromptOptions,
|
|
567
|
+
hasPiInsecureTlsSetting,
|
|
568
|
+
inferPiApiFromBaseUrl,
|
|
569
|
+
normalizeOpenAiBaseUrl,
|
|
570
|
+
parsePiRpcEffectiveModel,
|
|
571
|
+
parsePiModelIds,
|
|
572
|
+
persistPiConfiguration,
|
|
573
|
+
readExistingPiConfiguration,
|
|
574
|
+
resolveExistingPiApiConfig,
|
|
575
|
+
runPiRpcCommand,
|
|
576
|
+
updatePiAuthFile
|
|
577
|
+
};
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import * as p from "@clack/prompts";
|
|
4
|
+
import { readDotenvFile, updateDotenvFile } from "../config/store.mjs";
|
|
5
|
+
import { pathToLabel } from "../paths.mjs";
|
|
6
|
+
|
|
7
|
+
const grokDefaults = Object.freeze(JSON.parse(readFileSync(
|
|
8
|
+
new URL("../../skills/grok-search/defaults.json", import.meta.url),
|
|
9
|
+
"utf8"
|
|
10
|
+
)));
|
|
11
|
+
|
|
12
|
+
function readSkillEnvFile(path) {
|
|
13
|
+
return readDotenvFile(path, { sensitive: true });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function updateSkillEnvFile(path, updates) {
|
|
17
|
+
return updateDotenvFile(path, updates, { sensitive: true });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function configureGrokSearchEnv(paths, ensureSkillPresent = async () => {}, promptApi) {
|
|
21
|
+
const {
|
|
22
|
+
assertNotCancelled,
|
|
23
|
+
confirmOrCancel,
|
|
24
|
+
passwordPromptOptions,
|
|
25
|
+
required,
|
|
26
|
+
requiredUnlessExisting,
|
|
27
|
+
resolvePasswordValue
|
|
28
|
+
} = promptApi;
|
|
29
|
+
await ensureSkillPresent(paths);
|
|
30
|
+
const envPath = join(paths.agentsDir, "skills", "grok-search", ".env");
|
|
31
|
+
const existing = await readSkillEnvFile(envPath);
|
|
32
|
+
const baseUrl = await p.text({
|
|
33
|
+
message: "Grok API URL",
|
|
34
|
+
initialValue: existing.GROK_API_URL || "https://api.x.ai/v1",
|
|
35
|
+
validate: required()
|
|
36
|
+
});
|
|
37
|
+
assertNotCancelled(baseUrl);
|
|
38
|
+
|
|
39
|
+
const apiKey = await p.password(passwordPromptOptions(
|
|
40
|
+
"Grok API Key",
|
|
41
|
+
existing.GROK_API_KEY,
|
|
42
|
+
requiredUnlessExisting(existing.GROK_API_KEY, "Grok API Key 不能为空")
|
|
43
|
+
));
|
|
44
|
+
assertNotCancelled(apiKey);
|
|
45
|
+
const finalApiKey = resolvePasswordValue(apiKey, existing.GROK_API_KEY);
|
|
46
|
+
|
|
47
|
+
const model = await p.text({
|
|
48
|
+
message: "Grok 默认模型",
|
|
49
|
+
initialValue: existing.GROK_MODEL || grokDefaults.model,
|
|
50
|
+
validate: required()
|
|
51
|
+
});
|
|
52
|
+
assertNotCancelled(model);
|
|
53
|
+
|
|
54
|
+
const useTavily = await confirmOrCancel({
|
|
55
|
+
message: "是否同时配置 Tavily 作为额外搜索源?",
|
|
56
|
+
initialValue: Boolean(existing.TAVILY_API_KEY)
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const tavilyKey = useTavily
|
|
60
|
+
? await p.password(passwordPromptOptions(
|
|
61
|
+
"Tavily API Key",
|
|
62
|
+
existing.TAVILY_API_KEY,
|
|
63
|
+
requiredUnlessExisting(existing.TAVILY_API_KEY, "Tavily API Key 不能为空")
|
|
64
|
+
))
|
|
65
|
+
: "";
|
|
66
|
+
if (useTavily) assertNotCancelled(tavilyKey);
|
|
67
|
+
const finalTavilyKey = useTavily ? resolvePasswordValue(tavilyKey, existing.TAVILY_API_KEY) : null;
|
|
68
|
+
|
|
69
|
+
await updateSkillEnvFile(envPath, {
|
|
70
|
+
GROK_API_URL: baseUrl,
|
|
71
|
+
GROK_API_KEY: finalApiKey,
|
|
72
|
+
GROK_MODEL: model,
|
|
73
|
+
TAVILY_API_KEY: finalTavilyKey,
|
|
74
|
+
TAVILY_ENABLED: useTavily ? "true" : null
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
p.log.step(`已写入 ${pathToLabel(envPath)}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function configureContext7Env(paths, ensureSkillPresent = async () => {}, promptApi) {
|
|
81
|
+
const { assertNotCancelled, passwordPromptOptions, resolvePasswordValue } = promptApi;
|
|
82
|
+
await ensureSkillPresent(paths);
|
|
83
|
+
const envPath = join(paths.agentsDir, "skills", "context7-auto-research", ".env");
|
|
84
|
+
const existing = await readSkillEnvFile(envPath);
|
|
85
|
+
const apiKey = await p.password(passwordPromptOptions(
|
|
86
|
+
"Context7 API Key(可选)",
|
|
87
|
+
existing.CONTEXT7_API_KEY
|
|
88
|
+
));
|
|
89
|
+
assertNotCancelled(apiKey);
|
|
90
|
+
const finalApiKey = resolvePasswordValue(apiKey, existing.CONTEXT7_API_KEY);
|
|
91
|
+
|
|
92
|
+
await updateSkillEnvFile(envPath, {
|
|
93
|
+
CONTEXT7_API_KEY: finalApiKey
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
p.log.step(`已写入 ${pathToLabel(envPath)}`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
configureContext7Env,
|
|
101
|
+
configureGrokSearchEnv,
|
|
102
|
+
grokDefaults,
|
|
103
|
+
readSkillEnvFile,
|
|
104
|
+
updateSkillEnvFile
|
|
105
|
+
};
|
|
@@ -21,6 +21,13 @@ You are the general-purpose fallback sub-agent. Be useful quickly, stay within
|
|
|
21
21
|
scope, and avoid impersonating a specialist role when the task clearly belongs
|
|
22
22
|
to one.
|
|
23
23
|
|
|
24
|
+
## Development Context
|
|
25
|
+
|
|
26
|
+
Development context: work is currently in the development phase, and this is a development repository.
|
|
27
|
+
Do not retain runtime user state in the repository or shipped artifacts, and do not preserve compatibility layers for unreleased behavior.
|
|
28
|
+
Prefer deleting obsolete code paths; keep code and prompts concise and avoid over-engineering.
|
|
29
|
+
This context does not by itself authorize destructive changes to user files or credentials outside the repository.
|
|
30
|
+
|
|
24
31
|
## Priorities (in order)
|
|
25
32
|
|
|
26
33
|
1. Role fit — identify whether the request truly belongs to default
|