@lazyingart/agintiflow 0.17.2 → 0.17.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/docs/self-development-supervision.md +95 -0
- package/package.json +1 -1
- package/public/app.js +17 -1
- package/public/styles.css +14 -0
- package/scripts/smoke-auth.js +24 -2
- package/scripts/smoke-cli-chat.js +37 -2
- package/src/auth-onboarding.js +159 -13
- package/src/interactive-cli.js +32 -0
- 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/public/app.js
CHANGED
|
@@ -901,7 +901,7 @@ function renderWorkspacePanel(workspace = lastWorkspace, activity = lastWorkspac
|
|
|
901
901
|
const label = blocked ? t("blockedLabel") : t("changedLabel");
|
|
902
902
|
const path = escapeHtml(item.path || "");
|
|
903
903
|
const reason = blocked ? `<div class="subtle">${escapeHtml(item.reason || "")}</div>` : "";
|
|
904
|
-
const diff = item.diff ? `<pre class="change-diff">${
|
|
904
|
+
const diff = item.diff ? `<pre class="change-diff">${renderDiffHtml(item.diff, 1200)}</pre>` : "";
|
|
905
905
|
const hashes =
|
|
906
906
|
item.beforeHash || item.afterHash
|
|
907
907
|
? `<div class="change-meta"><span>before=${escapeHtml((item.beforeHash || "new").slice(0, 10))}</span><span>after=${escapeHtml((item.afterHash || "").slice(0, 10))}</span></div>`
|
|
@@ -1123,6 +1123,22 @@ function escapeHtml(value) {
|
|
|
1123
1123
|
);
|
|
1124
1124
|
}
|
|
1125
1125
|
|
|
1126
|
+
function renderDiffHtml(value, maxChars = 1200) {
|
|
1127
|
+
return String(value || "")
|
|
1128
|
+
.slice(0, maxChars)
|
|
1129
|
+
.split(/\r?\n/)
|
|
1130
|
+
.map((line) => {
|
|
1131
|
+
const escaped = escapeHtml(line);
|
|
1132
|
+
if (/^\+(?!\+\+)/.test(line)) return `<span class="diff-add">${escaped}</span>`;
|
|
1133
|
+
if (/^-(?!--)/.test(line)) return `<span class="diff-del">${escaped}</span>`;
|
|
1134
|
+
if (/^\+\+\+/.test(line)) return `<span class="diff-file-add">${escaped}</span>`;
|
|
1135
|
+
if (/^---/.test(line)) return `<span class="diff-file-del">${escaped}</span>`;
|
|
1136
|
+
if (/^@@/.test(line)) return `<span class="diff-hunk">${escaped}</span>`;
|
|
1137
|
+
return escaped;
|
|
1138
|
+
})
|
|
1139
|
+
.join("\n");
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1126
1142
|
function safeLinkHref(value) {
|
|
1127
1143
|
const raw = String(value || "").trim();
|
|
1128
1144
|
try {
|
package/public/styles.css
CHANGED
|
@@ -481,6 +481,20 @@ button.danger {
|
|
|
481
481
|
overflow-wrap: anywhere;
|
|
482
482
|
}
|
|
483
483
|
|
|
484
|
+
.change-diff .diff-add,
|
|
485
|
+
.change-diff .diff-file-add {
|
|
486
|
+
color: #86efac;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.change-diff .diff-del,
|
|
490
|
+
.change-diff .diff-file-del {
|
|
491
|
+
color: #fca5a5;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.change-diff .diff-hunk {
|
|
495
|
+
color: #67e8f9;
|
|
496
|
+
}
|
|
497
|
+
|
|
484
498
|
.sandbox-card h2 {
|
|
485
499
|
margin: 0 0 4px;
|
|
486
500
|
font-size: 1.05rem;
|
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
|
}
|
|
@@ -4,7 +4,7 @@ import fs from "node:fs/promises";
|
|
|
4
4
|
import os from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
|
-
import { buildPromptLayout, classifyEscapeAction, stripMarkdown } from "../src/interactive-cli.js";
|
|
7
|
+
import { buildPromptLayout, classifyEscapeAction, formatWorkspaceChange, stripMarkdown } from "../src/interactive-cli.js";
|
|
8
8
|
|
|
9
9
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
10
10
|
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-cli-chat-"));
|
|
@@ -85,6 +85,21 @@ try {
|
|
|
85
85
|
if (!renderedDiff.includes("-old") || !renderedDiff.includes("+new")) {
|
|
86
86
|
throw new Error("terminal markdown renderer dropped patch diff lines");
|
|
87
87
|
}
|
|
88
|
+
const renderedPatchEvent = formatWorkspaceChange({
|
|
89
|
+
toolName: "apply_patch",
|
|
90
|
+
path: "test_cli.py",
|
|
91
|
+
beforeHash: "aaaaaaaa11111111",
|
|
92
|
+
afterHash: "bbbbbbbb22222222",
|
|
93
|
+
diff: ["--- a/test_cli.py", "+++ b/test_cli.py", "@@ line 10 @@", "-old line", "+new line"].join("\n"),
|
|
94
|
+
});
|
|
95
|
+
if (
|
|
96
|
+
renderedPatchEvent.label !== "patch" ||
|
|
97
|
+
!renderedPatchEvent.summary.includes("test_cli.py") ||
|
|
98
|
+
!renderedPatchEvent.lines.join("\n").includes("-old line") ||
|
|
99
|
+
!renderedPatchEvent.lines.join("\n").includes("+new line")
|
|
100
|
+
) {
|
|
101
|
+
throw new Error("workspace patch event formatter did not preserve red/green diff content");
|
|
102
|
+
}
|
|
88
103
|
|
|
89
104
|
const promptLayout = buildPromptLayout(`${"x".repeat(180)}\nsecond line`, 95, 80, 24);
|
|
90
105
|
const promptText = promptLayout.renderedRows
|
|
@@ -185,7 +200,27 @@ try {
|
|
|
185
200
|
{
|
|
186
201
|
ok: true,
|
|
187
202
|
projectRoot: tempRoot,
|
|
188
|
-
checks: [
|
|
203
|
+
checks: [
|
|
204
|
+
"markdown-render",
|
|
205
|
+
"markdown-table-no-duplicate",
|
|
206
|
+
"patch-diff-render",
|
|
207
|
+
"workspace-patch-event-render",
|
|
208
|
+
"prompt-layout",
|
|
209
|
+
"user-prompt-label",
|
|
210
|
+
"escape-policy",
|
|
211
|
+
"live-input-status-layout",
|
|
212
|
+
"agent-response-gutter",
|
|
213
|
+
"aginti-md",
|
|
214
|
+
"instructions-command",
|
|
215
|
+
"skills-command",
|
|
216
|
+
"slash-prefix-autoselect",
|
|
217
|
+
"instructions-chat-edit",
|
|
218
|
+
"interactive-chat",
|
|
219
|
+
"mock-file-write",
|
|
220
|
+
"run-status",
|
|
221
|
+
"resume-latest",
|
|
222
|
+
"resume-history-full",
|
|
223
|
+
],
|
|
189
224
|
},
|
|
190
225
|
null,
|
|
191
226
|
2
|
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/interactive-cli.js
CHANGED
|
@@ -356,6 +356,36 @@ function printAgentMessage(text) {
|
|
|
356
356
|
for (const line of lines) outputLine(`${responsePrefix()}${line}`);
|
|
357
357
|
}
|
|
358
358
|
|
|
359
|
+
export function formatWorkspaceChange(change = {}) {
|
|
360
|
+
const toolName = String(change.toolName || change.action || "change");
|
|
361
|
+
const path = String(change.path || "");
|
|
362
|
+
const summary = [
|
|
363
|
+
toolName,
|
|
364
|
+
path,
|
|
365
|
+
change.created ? "created" : "",
|
|
366
|
+
change.beforeHash ? `before=${String(change.beforeHash).slice(0, 8)}` : "before=new",
|
|
367
|
+
change.afterHash ? `after=${String(change.afterHash).slice(0, 8)}` : "",
|
|
368
|
+
]
|
|
369
|
+
.filter(Boolean)
|
|
370
|
+
.join(" ");
|
|
371
|
+
const diff = String(change.diff || "").trim();
|
|
372
|
+
const renderedDiff = diff ? stripMarkdown(`Diff:\n${diff}`).split("\n") : [];
|
|
373
|
+
return {
|
|
374
|
+
label: toolName === "apply_patch" || toolName.startsWith("apply_patch") ? "patch" : "write",
|
|
375
|
+
summary,
|
|
376
|
+
lines: renderedDiff,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function printWorkspaceChange(change = {}) {
|
|
381
|
+
if (!change?.diff) return;
|
|
382
|
+
const formatted = formatWorkspaceChange(change);
|
|
383
|
+
const bg = formatted.label === "patch" ? ansi.magenta : ansi.systemBg;
|
|
384
|
+
outputLine(`${label(formatted.label, bg)} ${compactLine(formatted.summary, 92)}`);
|
|
385
|
+
const gutter = `${color(" | ", bg)} `;
|
|
386
|
+
for (const line of formatted.lines) outputLine(`${gutter}${line}`);
|
|
387
|
+
}
|
|
388
|
+
|
|
359
389
|
function printPreviewBlock(role, text, { time = "", bg = ansi.systemBg, maxLines = 5 } = {}) {
|
|
360
390
|
const header = [label(role, bg).trimEnd(), time ? color(time, ansi.dim) : ""].filter(Boolean).join(" ");
|
|
361
391
|
outputLine(header);
|
|
@@ -1795,6 +1825,8 @@ async function runPrompt(prompt, state, packageDir) {
|
|
|
1795
1825
|
printStatusEvent(state, "tool", data.toolName || "unknown");
|
|
1796
1826
|
} else if (type === "tool.completed") {
|
|
1797
1827
|
printStatusEvent(state, "tool_done", data.toolName || "unknown");
|
|
1828
|
+
} else if (type === "file.changed") {
|
|
1829
|
+
printWorkspaceChange(data);
|
|
1798
1830
|
} else if (type === "tool.blocked") {
|
|
1799
1831
|
printStatusEvent(state, "tool_blocked", data.toolName || data.reason || "unknown");
|
|
1800
1832
|
} else if (type === "loop.guard") {
|
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 = {
|