@lazyingart/agintiflow 0.17.2 → 0.17.3
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 +1 -1
- package/docs/self-development-supervision.md +95 -0
- package/package.json +1 -1
- package/scripts/smoke-auth.js +24 -2
- package/src/auth-onboarding.js +159 -13
- package/src/project.js +47 -0
package/README.md
CHANGED
|
@@ -79,7 +79,7 @@ For larger repositories, use `--profile large-codebase` or choose **Large codeba
|
|
|
79
79
|
|
|
80
80
|
AgInTiFlow can also spend cheap DeepSeek calls on parallel scout notes before the main executor starts a complicated task. It first writes a bounded project map to `.aginti/codebase-map.json`, then runs scouts for architecture, implementation, review, research, context mapping, tests, git workflow, integration, symbol tracing, and dependency risks. A coordinator Swarm Board is injected for the main agent and saved as `artifacts/scout-blackboard.json` in the session. The executor still does the real file/shell/browser work itself. Disable with `--no-parallel-scouts` or set `--scout-count 1..10`.
|
|
81
81
|
|
|
82
|
-
The next productive-agent roadmap is tracked in [docs/productive-agent-roadmap.md](docs/productive-agent-roadmap.md): durable codebase maps, stronger scout blackboards, long-run checkpoints, LSP/symbol tools, test triage, and release automation. Runtime choices, Docker persistence, host full-access tradeoffs, tmux sessions, and rolling-plan autonomy are documented in [docs/runtime-modes-and-autonomy.md](docs/runtime-modes-and-autonomy.md).
|
|
82
|
+
The next productive-agent roadmap is tracked in [docs/productive-agent-roadmap.md](docs/productive-agent-roadmap.md): durable codebase maps, stronger scout blackboards, long-run checkpoints, LSP/symbol tools, test triage, and release automation. Runtime choices, Docker persistence, host full-access tradeoffs, tmux sessions, and rolling-plan autonomy are documented in [docs/runtime-modes-and-autonomy.md](docs/runtime-modes-and-autonomy.md). The supervised self-development protocol is in [docs/self-development-supervision.md](docs/self-development-supervision.md).
|
|
83
83
|
|
|
84
84
|
For current docs, install errors, package/toolchain setup, and source discovery, the agent has a guarded `web_search` tool. It returns compact search results without browser search-engine loops and respects configured domain allowlists. Disable with `--no-web-search`.
|
|
85
85
|
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# Self-Development Supervision
|
|
2
|
+
|
|
3
|
+
This document defines how to let AgInTiFlow work on its own source code under Codex supervision.
|
|
4
|
+
|
|
5
|
+
## Readiness Verdict
|
|
6
|
+
|
|
7
|
+
AgInTiFlow is ready for supervised self-development on small and medium tasks. It has the required primitives:
|
|
8
|
+
|
|
9
|
+
- Project-local sessions and web/CLI sync.
|
|
10
|
+
- DeepSeek flash/pro routing with mock fallback.
|
|
11
|
+
- Workspace file tools, deterministic `apply_patch`, and compact diffs.
|
|
12
|
+
- Docker workspace mode for package installs and checks.
|
|
13
|
+
- Host tmux tools for durable monitored sessions.
|
|
14
|
+
- Guardrails for secrets, npm publishing, `.git`, `node_modules`, and destructive commands.
|
|
15
|
+
- Full test, smoke, and pack dry-run scripts.
|
|
16
|
+
|
|
17
|
+
It is not ready for fully unsupervised release or system-level work. Codex should remain the supervisor for commits, pushes, npm publishing, high-risk host commands, and ambiguous git states.
|
|
18
|
+
|
|
19
|
+
## Recommended Launch
|
|
20
|
+
|
|
21
|
+
Start AgInTiFlow in a separate tmux session from the source repo:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
tmux new-session -d -s agintiflow-selfdev -c /home/lachlan/ProjectsLFS/Agent/AgInTiFlow
|
|
25
|
+
tmux send-keys -t agintiflow-selfdev 'aginti --profile large-codebase --parallel-scouts --scout-count 5' Enter
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
For a single supervised task:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
aginti --profile large-codebase --parallel-scouts --scout-count 5 \
|
|
32
|
+
"inspect this repo, implement the requested change, run focused checks, and stop before commit"
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Use `aginti web --port 3221` from the same folder if a browser UI is preferred. CLI and web will share `.sessions/`.
|
|
36
|
+
|
|
37
|
+
## Supervisor Duties
|
|
38
|
+
|
|
39
|
+
Codex should supervise by:
|
|
40
|
+
|
|
41
|
+
1. Capturing tmux output before sending input.
|
|
42
|
+
2. Keeping the task scoped and testable.
|
|
43
|
+
3. Reviewing diffs before commit.
|
|
44
|
+
4. Running or verifying `npm test`, `npm run pack:dry-run`, and `git diff --check` when package behavior changes.
|
|
45
|
+
5. Rejecting changes that weaken guardrails without explicit reasoning.
|
|
46
|
+
6. Handling commit, push, and publish steps itself unless Lachlan explicitly delegates them to AgInTiFlow.
|
|
47
|
+
|
|
48
|
+
## Safe Task Types
|
|
49
|
+
|
|
50
|
+
Good first self-development tasks:
|
|
51
|
+
|
|
52
|
+
- Add or improve docs.
|
|
53
|
+
- Add smoke tests around existing behavior.
|
|
54
|
+
- Improve prompt wording or task profiles.
|
|
55
|
+
- Make small CLI/web UI polish changes.
|
|
56
|
+
- Add capability reports or non-invasive diagnostics.
|
|
57
|
+
|
|
58
|
+
Use stronger supervision for:
|
|
59
|
+
|
|
60
|
+
- Model-client tool schemas.
|
|
61
|
+
- Guardrails and command policy.
|
|
62
|
+
- Docker runtime changes.
|
|
63
|
+
- Npm publishing workflow.
|
|
64
|
+
- Git automation.
|
|
65
|
+
- Session persistence and database changes.
|
|
66
|
+
|
|
67
|
+
## Stop Conditions
|
|
68
|
+
|
|
69
|
+
AgInTiFlow should stop and ask for supervision when it sees:
|
|
70
|
+
|
|
71
|
+
- Dirty unrelated files.
|
|
72
|
+
- Failing tests it cannot explain.
|
|
73
|
+
- Git conflicts, divergence, rebase/merge choices, or reset/checkout suggestions.
|
|
74
|
+
- Requests to publish npm, push tags, delete files, rotate secrets, or run host sudo.
|
|
75
|
+
- A plan that exceeds the current step budget without a completed checkpoint.
|
|
76
|
+
|
|
77
|
+
## Housekeeping Before Each Self-Dev Session
|
|
78
|
+
|
|
79
|
+
Run:
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
git status --short
|
|
83
|
+
aginti doctor
|
|
84
|
+
aginti doctor --capabilities
|
|
85
|
+
npm run check
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
For larger changes also run:
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
npm test
|
|
92
|
+
npm run pack:dry-run
|
|
93
|
+
git diff --check
|
|
94
|
+
```
|
|
95
|
+
|
package/package.json
CHANGED
package/scripts/smoke-auth.js
CHANGED
|
@@ -6,10 +6,13 @@ import path from "node:path";
|
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { authProviderKeyHelp, authProviderKeyUrl, normalizeAuthProvider } from "../src/auth-onboarding.js";
|
|
8
8
|
import { getProviderDefaults } from "../src/model-routing.js";
|
|
9
|
-
import { providerKeyStatus, setProviderKey } from "../src/project.js";
|
|
9
|
+
import { maskProviderKey, providerKeyPreview, providerKeyStatus, setProviderKey } from "../src/project.js";
|
|
10
10
|
|
|
11
11
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
12
12
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auth-"));
|
|
13
|
+
const envKeys = ["DEEPSEEK_API_KEY", "OPENAI_API_KEY", "LLM_API_KEY", "QWEN_API_KEY", "GRSAI", "GRSAI_API_KEY"];
|
|
14
|
+
const originalEnv = Object.fromEntries(envKeys.map((key) => [key, process.env[key]]));
|
|
15
|
+
for (const key of envKeys) delete process.env[key];
|
|
13
16
|
|
|
14
17
|
function assert(condition, message) {
|
|
15
18
|
if (!condition) throw new Error(message);
|
|
@@ -55,6 +58,8 @@ try {
|
|
|
55
58
|
assert(normalizeAuthProvider("qwen") === "qwen", "qwen provider did not normalize");
|
|
56
59
|
assert(authProviderKeyUrl("deepseek") === "https://platform.deepseek.com/api_keys", "DeepSeek key URL is missing");
|
|
57
60
|
assert(authProviderKeyHelp("openai").includes("https://platform.openai.com/api-keys"), "OpenAI key help is missing");
|
|
61
|
+
assert(maskProviderKey("short") === "s…t (5 chars)", "short key mask was not compact");
|
|
62
|
+
assert(maskProviderKey("test-openai-key") === "test…-key (15 chars)", "long key mask did not preserve prefix/suffix");
|
|
58
63
|
const qwenDefaults = getProviderDefaults("qwen");
|
|
59
64
|
assert(qwenDefaults.provider === "qwen" && qwenDefaults.model, "qwen provider defaults are not available");
|
|
60
65
|
|
|
@@ -62,11 +67,16 @@ try {
|
|
|
62
67
|
let status = providerKeyStatus(tempRoot);
|
|
63
68
|
assert(status.qwen, "qwen key status was not detected");
|
|
64
69
|
assert(status.envVars.qwen.includes("QWEN_API_KEY"), "qwen env var name was not reported");
|
|
70
|
+
const qwenPreview = providerKeyPreview(tempRoot, "qwen");
|
|
71
|
+
assert(qwenPreview.available && qwenPreview.preview === "test…-key (13 chars)", "qwen key preview was not masked correctly");
|
|
65
72
|
|
|
66
73
|
await runCli(["keys", "set", "openai", "--stdin"], "test-openai-key");
|
|
67
74
|
await runCli(["keys", "set", "grsai", "--stdin"], "test-grsai-key");
|
|
68
75
|
status = providerKeyStatus(tempRoot);
|
|
69
76
|
assert(status.openai && status.grsai && status.qwen, "stored auth keys were not detected");
|
|
77
|
+
const openaiPreview = providerKeyPreview(tempRoot, "openai");
|
|
78
|
+
assert(openaiPreview.preview === "test…-key (15 chars)", "openai key preview was not masked correctly");
|
|
79
|
+
assert(openaiPreview.preview !== "test-openai-key", "openai key preview leaked raw key");
|
|
70
80
|
|
|
71
81
|
const keysOutput = await runCli(["keys", "status"]);
|
|
72
82
|
assert(keysOutput.includes("qwen=available"), "keys status did not include qwen");
|
|
@@ -77,12 +87,24 @@ try {
|
|
|
77
87
|
{
|
|
78
88
|
ok: true,
|
|
79
89
|
projectRoot: tempRoot,
|
|
80
|
-
checks: [
|
|
90
|
+
checks: [
|
|
91
|
+
"normalize-auth-provider",
|
|
92
|
+
"provider-key-links",
|
|
93
|
+
"provider-key-mask",
|
|
94
|
+
"provider-key-preview",
|
|
95
|
+
"qwen-defaults",
|
|
96
|
+
"qwen-key-status",
|
|
97
|
+
"cli-key-status-redacted",
|
|
98
|
+
],
|
|
81
99
|
},
|
|
82
100
|
null,
|
|
83
101
|
2
|
|
84
102
|
)
|
|
85
103
|
);
|
|
86
104
|
} finally {
|
|
105
|
+
for (const key of envKeys) {
|
|
106
|
+
if (originalEnv[key] === undefined) delete process.env[key];
|
|
107
|
+
else process.env[key] = originalEnv[key];
|
|
108
|
+
}
|
|
87
109
|
await fs.rm(tempRoot, { recursive: true, force: true });
|
|
88
110
|
}
|
package/src/auth-onboarding.js
CHANGED
|
@@ -2,7 +2,24 @@ import readline from "node:readline/promises";
|
|
|
2
2
|
import { emitKeypressEvents } from "node:readline";
|
|
3
3
|
import { stdin as input, stdout as output } from "node:process";
|
|
4
4
|
import { Writable } from "node:stream";
|
|
5
|
-
import { providerKeyStatus, setProviderKey } from "./project.js";
|
|
5
|
+
import { providerKeyPreview, providerKeyStatus, setProviderKey } from "./project.js";
|
|
6
|
+
|
|
7
|
+
const useColor = Boolean(input.isTTY && output.isTTY && process.env.AGINTIFLOW_NO_COLOR !== "1");
|
|
8
|
+
const ansi = {
|
|
9
|
+
reset: "\x1b[0m",
|
|
10
|
+
bold: "\x1b[1m",
|
|
11
|
+
dim: "\x1b[2m",
|
|
12
|
+
cyan: "\x1b[36m",
|
|
13
|
+
green: "\x1b[32m",
|
|
14
|
+
yellow: "\x1b[33m",
|
|
15
|
+
selected: "\x1b[48;5;31m\x1b[38;5;231m",
|
|
16
|
+
border: "\x1b[38;5;45m",
|
|
17
|
+
muted: "\x1b[38;5;245m",
|
|
18
|
+
inputBg: "\x1b[48;5;236m\x1b[38;5;231m",
|
|
19
|
+
clearLine: "\x1b[2K",
|
|
20
|
+
cursorHide: "\x1b[?25l",
|
|
21
|
+
cursorShow: "\x1b[?25h",
|
|
22
|
+
};
|
|
6
23
|
|
|
7
24
|
export const MAIN_AUTH_PROVIDERS = [
|
|
8
25
|
{
|
|
@@ -57,6 +74,68 @@ function providerLabel(provider = "") {
|
|
|
57
74
|
return match?.label || provider;
|
|
58
75
|
}
|
|
59
76
|
|
|
77
|
+
function color(value, ...codes) {
|
|
78
|
+
if (!useColor || codes.length === 0) return String(value);
|
|
79
|
+
return `${codes.join("")}${value}${ansi.reset}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stripAnsi(value) {
|
|
83
|
+
return String(value || "").replace(/\x1b\[[0-9;?]*[ -/]*[@-~]/g, "");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function visibleLength(value) {
|
|
87
|
+
return stripAnsi(value).length;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function terminalWidth() {
|
|
91
|
+
return Math.max(Number(output.columns) || 80, 50);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function padVisible(value, width) {
|
|
95
|
+
return `${value}${" ".repeat(Math.max(width - visibleLength(value), 0))}`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function compactLine(value = "", limit = 92) {
|
|
99
|
+
const text = String(value || "").replace(/\s+/g, " ").trim();
|
|
100
|
+
return text.length <= limit ? text : `${text.slice(0, Math.max(limit - 1, 1))}…`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function boxedLine(content, width) {
|
|
104
|
+
return `${color("│", ansi.border)} ${padVisible(content, width - 4)} ${color("│", ansi.border)}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function secretInputDisplay({ value, selectedPreview, placeholder }) {
|
|
108
|
+
if (value) return color(`${"•".repeat(Math.min(value.length, 24))} (${value.length} chars)`, ansi.inputBg);
|
|
109
|
+
if (selectedPreview) return `${color(` ${selectedPreview} `, ansi.selected, ansi.bold)} ${color("selected", ansi.yellow)}`;
|
|
110
|
+
return color(placeholder || "paste key here", ansi.dim);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function renderSecretBox({ title, helpText, statusText, currentPreview, value, selected, actionText }) {
|
|
114
|
+
const width = Math.min(Math.max(terminalWidth() - 2, 58), 96);
|
|
115
|
+
const inner = width - 4;
|
|
116
|
+
const selectedPreview = selected && currentPreview ? currentPreview : "";
|
|
117
|
+
const lines = [
|
|
118
|
+
`${color("╭", ansi.border)}${color("─", ansi.border).repeat(width - 2)}${color("╮", ansi.border)}`,
|
|
119
|
+
boxedLine(color(compactLine(title, inner), ansi.bold, ansi.cyan), width),
|
|
120
|
+
helpText ? boxedLine(`${color("key", ansi.muted)} ${compactLine(helpText, inner - 6)}`, width) : "",
|
|
121
|
+
statusText ? boxedLine(`${color("status", ansi.muted)} ${compactLine(statusText, inner - 8)}`, width) : "",
|
|
122
|
+
boxedLine(`${color("input", ansi.muted)} ${secretInputDisplay({ value, selectedPreview, placeholder: "hidden input" })}`, width),
|
|
123
|
+
boxedLine(color(compactLine(actionText, inner), ansi.dim), width),
|
|
124
|
+
`${color("╰", ansi.border)}${color("─", ansi.border).repeat(width - 2)}${color("╯", ansi.border)}`,
|
|
125
|
+
].filter(Boolean);
|
|
126
|
+
return lines.join("\n");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function clearRenderedLines(count) {
|
|
130
|
+
if (!count) return;
|
|
131
|
+
output.write(`\x1b[${count - 1}A`);
|
|
132
|
+
for (let index = 0; index < count; index += 1) {
|
|
133
|
+
output.write(`\r${ansi.clearLine}`);
|
|
134
|
+
if (index < count - 1) output.write("\x1b[1B");
|
|
135
|
+
}
|
|
136
|
+
output.write(`\x1b[${count - 1}A\r`);
|
|
137
|
+
}
|
|
138
|
+
|
|
60
139
|
export function authProviderKeyUrl(provider = "") {
|
|
61
140
|
const normalized = normalizeAuthProvider(provider, "");
|
|
62
141
|
const match = [...MAIN_AUTH_PROVIDERS, AUXILIARY_AUTH_PROVIDER].find((item) => item.id === normalized);
|
|
@@ -83,7 +162,7 @@ class MutedWritable extends Writable {
|
|
|
83
162
|
}
|
|
84
163
|
}
|
|
85
164
|
|
|
86
|
-
export async function promptSecret(promptText, { allowEscape = true } = {}) {
|
|
165
|
+
export async function promptSecret(promptText, { allowEscape = true, box = null } = {}) {
|
|
87
166
|
if (!input.isTTY || !output.isTTY) return { value: "", skipped: true };
|
|
88
167
|
|
|
89
168
|
if (typeof input.setRawMode === "function") {
|
|
@@ -91,21 +170,38 @@ export async function promptSecret(promptText, { allowEscape = true } = {}) {
|
|
|
91
170
|
emitKeypressEvents(input);
|
|
92
171
|
const wasRaw = Boolean(input.isRaw);
|
|
93
172
|
let value = "";
|
|
173
|
+
let selectedExisting = Boolean(box?.currentPreview);
|
|
174
|
+
let renderedLines = 0;
|
|
175
|
+
|
|
176
|
+
const render = () => {
|
|
177
|
+
if (!box) return;
|
|
178
|
+
clearRenderedLines(renderedLines);
|
|
179
|
+
const text = renderSecretBox({
|
|
180
|
+
...box,
|
|
181
|
+
value,
|
|
182
|
+
selected: selectedExisting,
|
|
183
|
+
});
|
|
184
|
+
renderedLines = text.split("\n").length;
|
|
185
|
+
output.write(`${ansi.cursorHide}${text}`);
|
|
186
|
+
};
|
|
94
187
|
|
|
95
188
|
const cleanup = () => {
|
|
96
189
|
input.off("keypress", handler);
|
|
97
190
|
if (typeof input.setRawMode === "function") input.setRawMode(wasRaw);
|
|
98
191
|
input.pause();
|
|
192
|
+
output.write(ansi.cursorShow);
|
|
99
193
|
};
|
|
100
194
|
|
|
101
195
|
const finish = (result) => {
|
|
196
|
+
if (box) clearRenderedLines(renderedLines);
|
|
102
197
|
cleanup();
|
|
103
|
-
output.write("\n");
|
|
198
|
+
if (!box) output.write("\n");
|
|
104
199
|
resolve(result);
|
|
105
200
|
};
|
|
106
201
|
|
|
107
202
|
const handler = (str = "", key = {}) => {
|
|
108
203
|
if (key.ctrl && key.name === "c") {
|
|
204
|
+
if (box) clearRenderedLines(renderedLines);
|
|
109
205
|
cleanup();
|
|
110
206
|
reject(Object.assign(new Error("Interrupted by ctrl-c."), { name: "AbortError", code: "ABORT_ERR" }));
|
|
111
207
|
return;
|
|
@@ -119,14 +215,34 @@ export async function promptSecret(promptText, { allowEscape = true } = {}) {
|
|
|
119
215
|
return;
|
|
120
216
|
}
|
|
121
217
|
if (key.name === "backspace" || key.name === "delete") {
|
|
122
|
-
|
|
218
|
+
if (selectedExisting) {
|
|
219
|
+
selectedExisting = false;
|
|
220
|
+
value = "";
|
|
221
|
+
} else {
|
|
222
|
+
value = value.slice(0, -1);
|
|
223
|
+
}
|
|
224
|
+
render();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
if (key.ctrl && key.name === "u") {
|
|
228
|
+
selectedExisting = false;
|
|
229
|
+
value = "";
|
|
230
|
+
render();
|
|
123
231
|
return;
|
|
124
232
|
}
|
|
125
233
|
if (key.ctrl || key.meta || key.sequence?.startsWith("\x1b")) return;
|
|
126
|
-
if (str)
|
|
234
|
+
if (str) {
|
|
235
|
+
if (selectedExisting) {
|
|
236
|
+
selectedExisting = false;
|
|
237
|
+
value = "";
|
|
238
|
+
}
|
|
239
|
+
value += str.replace(/\r|\n/g, "");
|
|
240
|
+
render();
|
|
241
|
+
}
|
|
127
242
|
};
|
|
128
243
|
|
|
129
|
-
|
|
244
|
+
if (box) render();
|
|
245
|
+
else output.write(promptText);
|
|
130
246
|
input.resume();
|
|
131
247
|
input.setRawMode(true);
|
|
132
248
|
input.on("keypress", handler);
|
|
@@ -313,10 +429,23 @@ export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
|
|
|
313
429
|
const skipped = [];
|
|
314
430
|
|
|
315
431
|
if (mainProvider) {
|
|
316
|
-
const
|
|
432
|
+
const preview = providerKeyPreview(projectRoot, mainProvider);
|
|
433
|
+
const current = preview.available ? `available: ${preview.preview}` : "missing";
|
|
317
434
|
const keyHelp = authProviderKeyHelp(mainProvider);
|
|
318
|
-
const
|
|
319
|
-
const
|
|
435
|
+
const title = `${providerLabel(mainProvider)} main API key`;
|
|
436
|
+
const prompt = `${keyHelp ? `${keyHelp}\n` : ""}${title} (${current}) [hidden]: `;
|
|
437
|
+
const secret = await promptSecret(prompt, {
|
|
438
|
+
allowEscape: true,
|
|
439
|
+
box: {
|
|
440
|
+
title,
|
|
441
|
+
helpText: keyHelp || "Paste the provider API key.",
|
|
442
|
+
statusText: preview.available ? `Existing ${preview.keyName}: ${preview.preview}` : `Missing ${preview.keyName || "provider key"}`,
|
|
443
|
+
currentPreview: preview.preview,
|
|
444
|
+
actionText: preview.available
|
|
445
|
+
? "Existing key is selected. Type to replace, Enter/Esc to keep existing."
|
|
446
|
+
: "Paste key and Enter to save, or Esc/Enter to skip.",
|
|
447
|
+
},
|
|
448
|
+
});
|
|
320
449
|
if (secret.value) {
|
|
321
450
|
const result = await setProviderKey(projectRoot, mainProvider, secret.value);
|
|
322
451
|
saved.push(result);
|
|
@@ -328,10 +457,18 @@ export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
|
|
|
328
457
|
}
|
|
329
458
|
|
|
330
459
|
if (options.includeAuxiliary !== false && directProvider !== "grsai") {
|
|
331
|
-
const
|
|
332
|
-
const current =
|
|
333
|
-
const
|
|
460
|
+
const preview = providerKeyPreview(projectRoot, "grsai");
|
|
461
|
+
const current = preview.available ? `available: ${preview.preview}` : "optional";
|
|
462
|
+
const title = `${AUXILIARY_AUTH_PROVIDER.label} auxiliary image key`;
|
|
463
|
+
const secret = await promptSecret(`${title} (${current}) [hidden]: `, {
|
|
334
464
|
allowEscape: true,
|
|
465
|
+
box: {
|
|
466
|
+
title,
|
|
467
|
+
helpText: "Optional image generation key for GRS AI / Nano Banana.",
|
|
468
|
+
statusText: preview.available ? `Existing ${preview.keyName}: ${preview.preview}` : "Optional; no auxiliary key saved.",
|
|
469
|
+
currentPreview: preview.preview,
|
|
470
|
+
actionText: preview.available ? "Existing key is selected. Type to replace, Enter/Esc to keep existing." : "Paste key and Enter to save, or Esc/Enter to skip.",
|
|
471
|
+
},
|
|
335
472
|
});
|
|
336
473
|
if (secret.value) {
|
|
337
474
|
const result = await setProviderKey(projectRoot, "grsai", secret.value);
|
|
@@ -340,8 +477,17 @@ export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
|
|
|
340
477
|
skipped.push({ provider: "grsai", reason: "skipped" });
|
|
341
478
|
}
|
|
342
479
|
} else if (directProvider === "grsai") {
|
|
343
|
-
const
|
|
480
|
+
const preview = providerKeyPreview(projectRoot, "grsai");
|
|
481
|
+
const title = `${AUXILIARY_AUTH_PROVIDER.label} auxiliary image key`;
|
|
482
|
+
const secret = await promptSecret(`${title} [hidden]: `, {
|
|
344
483
|
allowEscape: true,
|
|
484
|
+
box: {
|
|
485
|
+
title,
|
|
486
|
+
helpText: "Optional image generation key for GRS AI / Nano Banana.",
|
|
487
|
+
statusText: preview.available ? `Existing ${preview.keyName}: ${preview.preview}` : "Missing auxiliary key.",
|
|
488
|
+
currentPreview: preview.preview,
|
|
489
|
+
actionText: preview.available ? "Existing key is selected. Type to replace, Enter/Esc to keep existing." : "Paste key and Enter to save, or Esc/Enter to skip.",
|
|
490
|
+
},
|
|
345
491
|
});
|
|
346
492
|
if (secret.value) saved.push(await setProviderKey(projectRoot, "grsai", secret.value));
|
|
347
493
|
else skipped.push({ provider: "grsai", reason: "skipped" });
|
package/src/project.js
CHANGED
|
@@ -16,10 +16,18 @@ const LOCAL_ENV_KEYS = new Set([
|
|
|
16
16
|
"DEEPSEEK_FAST_MODEL",
|
|
17
17
|
"DEEPSEEK_PRO_MODEL",
|
|
18
18
|
"OPENAI_DEFAULT_MODEL",
|
|
19
|
+
"QWEN_API_KEY",
|
|
19
20
|
"GRSAI",
|
|
20
21
|
"GRSAI_API_KEY",
|
|
21
22
|
]);
|
|
22
23
|
|
|
24
|
+
const PROVIDER_KEY_CANDIDATES = {
|
|
25
|
+
openai: ["OPENAI_API_KEY", "LLM_API_KEY"],
|
|
26
|
+
deepseek: ["DEEPSEEK_API_KEY", "LLM_API_KEY"],
|
|
27
|
+
qwen: ["QWEN_API_KEY"],
|
|
28
|
+
grsai: ["GRSAI", "GRSAI_API_KEY"],
|
|
29
|
+
};
|
|
30
|
+
|
|
23
31
|
export function resolveProjectRoot(input = process.cwd()) {
|
|
24
32
|
return path.resolve(input || process.cwd());
|
|
25
33
|
}
|
|
@@ -267,6 +275,45 @@ export function providerKeyStatus(projectRoot = process.cwd()) {
|
|
|
267
275
|
};
|
|
268
276
|
}
|
|
269
277
|
|
|
278
|
+
export function maskProviderKey(value = "") {
|
|
279
|
+
const text = String(value || "").trim();
|
|
280
|
+
if (!text) return "";
|
|
281
|
+
if (text.length <= 8) return `${text.slice(0, 1)}…${text.slice(-1)} (${text.length} chars)`;
|
|
282
|
+
return `${text.slice(0, 4)}…${text.slice(-4)} (${text.length} chars)`;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function providerKeyPreview(projectRoot = process.cwd(), provider = "") {
|
|
286
|
+
loadProjectEnv(projectRoot);
|
|
287
|
+
const normalized = String(provider || "").trim().toLowerCase();
|
|
288
|
+
const aliases = {
|
|
289
|
+
auxiliary: "grsai",
|
|
290
|
+
auxilliary: "grsai",
|
|
291
|
+
image: "grsai",
|
|
292
|
+
imagegen: "grsai",
|
|
293
|
+
};
|
|
294
|
+
const canonical = aliases[normalized] || normalized;
|
|
295
|
+
const keys = PROVIDER_KEY_CANDIDATES[canonical] || [];
|
|
296
|
+
for (const keyName of keys) {
|
|
297
|
+
const value = process.env[keyName];
|
|
298
|
+
if (value) {
|
|
299
|
+
return {
|
|
300
|
+
available: true,
|
|
301
|
+
provider: canonical,
|
|
302
|
+
keyName,
|
|
303
|
+
preview: maskProviderKey(value),
|
|
304
|
+
length: String(value).trim().length,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
available: false,
|
|
310
|
+
provider: canonical,
|
|
311
|
+
keyName: keys[0] || "",
|
|
312
|
+
preview: "",
|
|
313
|
+
length: 0,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
270
317
|
export async function setProviderKey(projectRoot, provider, value) {
|
|
271
318
|
const normalizedProvider = String(provider || "").toLowerCase();
|
|
272
319
|
const aliases = {
|