sarangai-cli 1.0.14 → 1.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +127 -43
- package/dist/index.js +1254 -897
- package/package.json +20 -15
package/dist/index.js
CHANGED
|
@@ -5,8 +5,8 @@ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
|
5
5
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
6
|
var __getProtoOf = Object.getPrototypeOf;
|
|
7
7
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
-
var
|
|
9
|
-
return
|
|
8
|
+
var __esm = (fn, res) => function __init() {
|
|
9
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
10
10
|
};
|
|
11
11
|
var __export = (target, all) => {
|
|
12
12
|
for (var name in all)
|
|
@@ -28,1021 +28,1378 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
28
28
|
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
29
29
|
mod
|
|
30
30
|
));
|
|
31
|
-
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
32
31
|
|
|
33
|
-
//
|
|
34
|
-
var
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
"readline-sync": "^1.4.10"
|
|
74
|
-
},
|
|
75
|
-
devDependencies: {
|
|
76
|
-
"@types/clipboardy": "^1.1.0",
|
|
77
|
-
"@types/node": "^20.14.9",
|
|
78
|
-
"@types/prompts": "^2.4.9",
|
|
79
|
-
"@types/react": "^19.3.0",
|
|
80
|
-
"@types/readline-sync": "^1.4.8",
|
|
81
|
-
tsup: "^8.5.1",
|
|
82
|
-
typescript: "^5.5.2"
|
|
83
|
-
}
|
|
84
|
-
};
|
|
32
|
+
// src/ui/banner.ts
|
|
33
|
+
var banner_exports = {};
|
|
34
|
+
__export(banner_exports, {
|
|
35
|
+
SARANGAI_ASCII: () => SARANGAI_ASCII,
|
|
36
|
+
bannerLines: () => bannerLines,
|
|
37
|
+
formatAccountLine: () => formatAccountLine,
|
|
38
|
+
printBanner: () => printBanner
|
|
39
|
+
});
|
|
40
|
+
function formatAccountLine(stats) {
|
|
41
|
+
if (!stats) {
|
|
42
|
+
return `${violetLight("\u25CF Account:")} ${import_chalk.default.white("\u2014")} | ${violetLight("Tier:")} ${import_chalk.default.cyan("\u2014")} | ${violetLight("\u26A1 Balance:")} ${import_chalk.default.bold.green("\u2014")}`;
|
|
43
|
+
}
|
|
44
|
+
const formattedBalance = new Intl.NumberFormat("en-US").format(stats.balance);
|
|
45
|
+
return `${violetLight("\u25CF Account:")} ${import_chalk.default.white(stats.accountId)} | ${violetLight("Tier:")} ${import_chalk.default.cyan(stats.tier)} | ${violetLight("\u26A1 Balance:")} ${import_chalk.default.bold.green(formattedBalance)}`;
|
|
46
|
+
}
|
|
47
|
+
function bannerLines(stats) {
|
|
48
|
+
const lines = SARANGAI_ASCII.split("\n");
|
|
49
|
+
lines.push(violetBorder("\u2501".repeat(68)));
|
|
50
|
+
lines.push(formatAccountLine(stats));
|
|
51
|
+
lines.push(violetBorder("\u2501".repeat(68)));
|
|
52
|
+
return lines;
|
|
53
|
+
}
|
|
54
|
+
function printBanner(stats) {
|
|
55
|
+
console.log("\n" + bannerLines(stats).join("\n"));
|
|
56
|
+
}
|
|
57
|
+
var import_chalk, violetDark, violetBorder, violetLight, SARANGAI_ASCII;
|
|
58
|
+
var init_banner = __esm({
|
|
59
|
+
"src/ui/banner.ts"() {
|
|
60
|
+
import_chalk = __toESM(require("chalk"));
|
|
61
|
+
violetDark = import_chalk.default.hex("#7c3aed");
|
|
62
|
+
violetBorder = import_chalk.default.hex("#5b21b6");
|
|
63
|
+
violetLight = import_chalk.default.hex("#a78bfa");
|
|
64
|
+
SARANGAI_ASCII = `
|
|
65
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557
|
|
66
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D \u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551
|
|
67
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551
|
|
68
|
+
\u255A\u2550\u2550\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2551\u2588\u2588\u2551
|
|
69
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551
|
|
70
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D
|
|
71
|
+
`.trim();
|
|
85
72
|
}
|
|
86
73
|
});
|
|
87
74
|
|
|
88
75
|
// src/index.ts
|
|
89
|
-
var index_exports = {};
|
|
90
|
-
__export(index_exports, {
|
|
91
|
-
CODING_MODELS: () => CODING_MODELS
|
|
92
|
-
});
|
|
93
|
-
module.exports = __toCommonJS(index_exports);
|
|
94
76
|
var import_commander = require("commander");
|
|
95
|
-
var
|
|
77
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
96
78
|
var import_readline = __toESM(require("readline"));
|
|
97
|
-
|
|
98
|
-
|
|
79
|
+
|
|
80
|
+
// src/constants.ts
|
|
81
|
+
var LOCKED_CODING_MODELS = {
|
|
82
|
+
glm: {
|
|
83
|
+
id: "z-ai/glm-5.3-flash",
|
|
84
|
+
alias: "glm",
|
|
85
|
+
name: "GLM 5.3 Flash",
|
|
86
|
+
role: "Rapid Scaffolder",
|
|
87
|
+
description: "Instant execution for boilerplate scaffolding, refactoring, and automation scripting.",
|
|
88
|
+
maxTokens: 4096
|
|
89
|
+
},
|
|
90
|
+
sonnet: {
|
|
91
|
+
id: "anthropic/claude-sonnet-4.6",
|
|
92
|
+
alias: "sonnet",
|
|
93
|
+
name: "Claude Sonnet 4.6",
|
|
94
|
+
role: "Deep Architect & Refactor",
|
|
95
|
+
description: "Architecture design, systematic refactoring, and enterprise-grade clean code.",
|
|
96
|
+
maxTokens: 8192
|
|
97
|
+
},
|
|
98
|
+
luna: {
|
|
99
|
+
id: "openai/gpt-5.6-luna",
|
|
100
|
+
alias: "luna",
|
|
101
|
+
name: "GPT-5.6 Luna",
|
|
102
|
+
role: "Fast Precision & Fixer",
|
|
103
|
+
description: "Multi-file analysis, edge-case bug fixing, and strict logic verification.",
|
|
104
|
+
maxTokens: 8192
|
|
105
|
+
},
|
|
106
|
+
deepseek: {
|
|
107
|
+
id: "deepseek/deepseek-v3.2",
|
|
108
|
+
alias: "deepseek",
|
|
109
|
+
name: "DeepSeek V3.2",
|
|
110
|
+
role: "Logic & Algorithm Engineer",
|
|
111
|
+
description: "Performance optimization, high-precision syntax, and algorithm efficiency.",
|
|
112
|
+
maxTokens: 8192
|
|
113
|
+
},
|
|
114
|
+
mimo: {
|
|
115
|
+
id: "xiaomi/mimo-v2.5-pro",
|
|
116
|
+
alias: "mimo",
|
|
117
|
+
name: "MiMo V2.5 Pro",
|
|
118
|
+
role: "Fullstack & Multimodal Agent",
|
|
119
|
+
description: "Terminal log inspection, stack trace analysis, and build script automation.",
|
|
120
|
+
maxTokens: 4096
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
var DEFAULT_CODING_MODEL = LOCKED_CODING_MODELS.glm.id;
|
|
124
|
+
var MODEL_ALIAS_LIST = Object.keys(LOCKED_CODING_MODELS).join(", ");
|
|
125
|
+
function findModel(input) {
|
|
126
|
+
if (!input) return null;
|
|
127
|
+
const q = input.trim().toLowerCase();
|
|
128
|
+
if (!q) return null;
|
|
129
|
+
return Object.values(LOCKED_CODING_MODELS).find(
|
|
130
|
+
(m) => m.alias.toLowerCase() === q || m.name.toLowerCase() === q || m.id.toLowerCase() === q
|
|
131
|
+
) ?? null;
|
|
132
|
+
}
|
|
133
|
+
function resolveModel(input) {
|
|
134
|
+
return findModel(input) ?? LOCKED_CODING_MODELS.glm;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/core/auth.ts
|
|
99
138
|
var import_fs2 = __toESM(require("fs"));
|
|
100
139
|
var import_path2 = __toESM(require("path"));
|
|
101
|
-
var
|
|
140
|
+
var import_os2 = __toESM(require("os"));
|
|
102
141
|
|
|
103
142
|
// src/config.ts
|
|
104
143
|
var import_fs = __toESM(require("fs"));
|
|
105
144
|
var import_path = __toESM(require("path"));
|
|
106
145
|
var import_os = __toESM(require("os"));
|
|
107
146
|
var CONFIG_FILE = import_path.default.join(import_os.default.homedir(), ".sarangairc");
|
|
147
|
+
var DEFAULT_CONFIG = {
|
|
148
|
+
baseUrl: "https://sarangai.id",
|
|
149
|
+
defaultModel: "glm"
|
|
150
|
+
};
|
|
108
151
|
function getConfig() {
|
|
109
152
|
try {
|
|
110
153
|
if (import_fs.default.existsSync(CONFIG_FILE)) {
|
|
111
154
|
const raw = import_fs.default.readFileSync(CONFIG_FILE, "utf-8");
|
|
112
|
-
|
|
155
|
+
const parsed = JSON.parse(raw);
|
|
156
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
157
|
+
return parsed;
|
|
158
|
+
}
|
|
113
159
|
}
|
|
114
160
|
} catch {
|
|
115
161
|
}
|
|
116
|
-
return {
|
|
117
|
-
baseUrl: "https://idshop.or.id",
|
|
118
|
-
defaultModel: "deepseek/deepseek-chat"
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
function saveConfig(newConfig) {
|
|
122
|
-
const current = getConfig();
|
|
123
|
-
const merged = { ...current, ...newConfig };
|
|
124
|
-
import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), "utf-8");
|
|
162
|
+
return { ...DEFAULT_CONFIG };
|
|
125
163
|
}
|
|
126
164
|
|
|
127
|
-
// src/
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
rate: "12,000 Credits/hr"
|
|
148
|
-
},
|
|
149
|
-
{
|
|
150
|
-
id: "openai/gpt-4o",
|
|
151
|
-
name: "GPT-4o",
|
|
152
|
-
desc: "OpenAI \u2022 Fast full-stack & complex logic",
|
|
153
|
-
rate: "5,000 Credits/hr"
|
|
154
|
-
},
|
|
155
|
-
{
|
|
156
|
-
id: "deepseek/deepseek-r1",
|
|
157
|
-
name: "DeepSeek-R1",
|
|
158
|
-
desc: "DeepSeek \u2022 Deep reasoning & hard algorithms",
|
|
159
|
-
rate: "1,500 Credits/hr"
|
|
160
|
-
},
|
|
161
|
-
{
|
|
162
|
-
id: "qwen/qwen-2.5-coder-32b-instruct",
|
|
163
|
-
name: "Qwen 2.5 Coder",
|
|
164
|
-
desc: "Alibaba \u2022 Multi-language syntax specialist",
|
|
165
|
-
rate: "700 Credits/hr"
|
|
166
|
-
},
|
|
167
|
-
{
|
|
168
|
-
id: "google/gemini-2.5-flash",
|
|
169
|
-
name: "Gemini 2.5 Flash",
|
|
170
|
-
desc: "Google \u2022 1M context & full repo analysis",
|
|
171
|
-
rate: "500 Credits/hr"
|
|
172
|
-
},
|
|
173
|
-
{
|
|
174
|
-
id: "meta-llama/llama-3.3-70b-instruct",
|
|
175
|
-
name: "Llama 3.3 70B",
|
|
176
|
-
desc: "Meta \u2022 Robust open-source coding engine",
|
|
177
|
-
rate: "600 Credits/hr"
|
|
178
|
-
}
|
|
179
|
-
];
|
|
180
|
-
function resolveBaseUrl() {
|
|
181
|
-
const cfg2 = getConfig();
|
|
182
|
-
return process.env.SARANGAI_BASE_URL || cfg2.baseUrl || "https://idshop.or.id";
|
|
183
|
-
}
|
|
184
|
-
function copyToClipboard(text) {
|
|
185
|
-
try {
|
|
186
|
-
import_clipboardy.default.writeSync(text);
|
|
187
|
-
} catch {
|
|
188
|
-
try {
|
|
189
|
-
(0, import_child_process.execSync)(`echo -n "${text}" | xclip -selection clipboard 2>/dev/null || echo -n "${text}" | pbcopy 2>/dev/null || echo | set /p="${text}" | clip 2>/dev/null`);
|
|
190
|
-
} catch {
|
|
165
|
+
// src/core/auth.ts
|
|
166
|
+
function detectLocalSessionToken() {
|
|
167
|
+
if (process.env.SARANGAI_TOKEN) return process.env.SARANGAI_TOKEN;
|
|
168
|
+
if (process.env.SARANGAI_API_KEY) return process.env.SARANGAI_API_KEY;
|
|
169
|
+
const cfg = getConfig();
|
|
170
|
+
if (cfg.apiKey) return cfg.apiKey;
|
|
171
|
+
const candidatePaths = [
|
|
172
|
+
import_path2.default.join(import_os2.default.homedir(), ".sarangairc"),
|
|
173
|
+
import_path2.default.join(import_os2.default.homedir(), ".sarangai", "session.json"),
|
|
174
|
+
import_path2.default.join(import_os2.default.homedir(), ".config", "sarangai", "auth.json")
|
|
175
|
+
];
|
|
176
|
+
for (const p of candidatePaths) {
|
|
177
|
+
if (import_fs2.default.existsSync(p)) {
|
|
178
|
+
try {
|
|
179
|
+
const raw = import_fs2.default.readFileSync(p, "utf-8");
|
|
180
|
+
const json = JSON.parse(raw);
|
|
181
|
+
const t = json.apiKey || json.token || json.sessionToken || json.jwt;
|
|
182
|
+
if (t) return t;
|
|
183
|
+
} catch {
|
|
184
|
+
}
|
|
191
185
|
}
|
|
192
186
|
}
|
|
187
|
+
return "";
|
|
193
188
|
}
|
|
194
|
-
async function fetchUserMeta(
|
|
189
|
+
async function fetchUserMeta(baseUrl, token) {
|
|
195
190
|
try {
|
|
196
|
-
const
|
|
197
|
-
|
|
191
|
+
const cleanBase = baseUrl.replace(/\/+$/, "");
|
|
192
|
+
const res = await fetch(`${cleanBase}/api/gateway/v1/balance`, {
|
|
193
|
+
headers: {
|
|
194
|
+
"Authorization": `Bearer ${token}`,
|
|
195
|
+
"Accept": "application/json"
|
|
196
|
+
}
|
|
198
197
|
});
|
|
199
198
|
if (res.ok) {
|
|
200
199
|
const d = await res.json();
|
|
200
|
+
const rawId = d.accountId != null ? String(d.accountId) : "";
|
|
201
201
|
return {
|
|
202
|
-
balance: d.balance
|
|
202
|
+
balance: typeof d.balance === "number" ? Math.floor(d.balance) : parseInt(d.balance, 10) || 0,
|
|
203
203
|
tier: d.tier || "DEVELOPER",
|
|
204
|
-
accountId:
|
|
204
|
+
accountId: rawId ? rawId.startsWith("SA-") ? rawId : `SA-${rawId}` : "SA-USER"
|
|
205
205
|
};
|
|
206
206
|
}
|
|
207
207
|
} catch {
|
|
208
208
|
}
|
|
209
209
|
return null;
|
|
210
210
|
}
|
|
211
|
-
async function
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
} else {
|
|
222
|
-
drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
|
|
211
|
+
async function ensureAuthenticated() {
|
|
212
|
+
const cfg = getConfig();
|
|
213
|
+
const baseUrl = cfg.baseUrl || "https://sarangai.id";
|
|
214
|
+
const token = detectLocalSessionToken();
|
|
215
|
+
if (!token) {
|
|
216
|
+
throw new Error("API Key not found in ~/.sarangairc.");
|
|
217
|
+
}
|
|
218
|
+
const stats = await fetchUserMeta(baseUrl, token);
|
|
219
|
+
if (!stats) {
|
|
220
|
+
throw new Error("Failed to fetch real-time balance from the SarangAI gateway (/api/gateway/v1/balance).");
|
|
223
221
|
}
|
|
222
|
+
return { token, stats };
|
|
224
223
|
}
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
|
|
225
|
+
// src/core/workspace.ts
|
|
226
|
+
var import_fs5 = __toESM(require("fs"));
|
|
227
|
+
var import_path5 = __toESM(require("path"));
|
|
228
|
+
|
|
229
|
+
// src/core/gateway.ts
|
|
230
|
+
async function streamChatCompletion({
|
|
231
|
+
model,
|
|
232
|
+
messages,
|
|
233
|
+
onReasoningProgress,
|
|
234
|
+
onContentToken,
|
|
235
|
+
signal
|
|
236
|
+
}) {
|
|
237
|
+
const config = getConfig();
|
|
238
|
+
const apiKey = config.apiKey || process.env.SARANGAI_API_KEY;
|
|
239
|
+
if (!apiKey) {
|
|
240
|
+
throw new Error("API Key is not configured in ~/.sarangairc");
|
|
241
|
+
}
|
|
242
|
+
const baseUrl = config.baseUrl || "https://sarangai.id";
|
|
243
|
+
const url = `${baseUrl.replace(/\/+$/, "")}/api/gateway/v1/chat/completions`;
|
|
244
|
+
const response = await fetch(url, {
|
|
245
|
+
method: "POST",
|
|
246
|
+
headers: {
|
|
247
|
+
"Content-Type": "application/json",
|
|
248
|
+
Authorization: `Bearer ${apiKey}`
|
|
249
|
+
},
|
|
250
|
+
body: JSON.stringify({
|
|
251
|
+
model: model.id,
|
|
252
|
+
messages,
|
|
253
|
+
stream: true,
|
|
254
|
+
max_tokens: model.maxTokens || 4096
|
|
255
|
+
}),
|
|
256
|
+
signal
|
|
257
|
+
});
|
|
258
|
+
if (!response.ok) {
|
|
259
|
+
const errorText = await response.text();
|
|
260
|
+
let errorMessage = `Gateway HTTP ${response.status}`;
|
|
261
|
+
try {
|
|
262
|
+
const errJson = JSON.parse(errorText);
|
|
263
|
+
if (errJson.error?.message) {
|
|
264
|
+
errorMessage += `: ${errJson.error.message}`;
|
|
265
|
+
} else if (errJson.message) {
|
|
266
|
+
errorMessage += `: ${errJson.message}`;
|
|
267
|
+
}
|
|
268
|
+
} catch {
|
|
269
|
+
if (errorText) errorMessage += `: ${errorText.slice(0, 200)}`;
|
|
270
|
+
}
|
|
271
|
+
throw new Error(errorMessage);
|
|
272
|
+
}
|
|
273
|
+
if (!response.body) {
|
|
274
|
+
throw new Error("Empty response body from gateway.");
|
|
275
|
+
}
|
|
276
|
+
const reader = response.body.getReader();
|
|
277
|
+
const decoder = new TextDecoder("utf-8");
|
|
278
|
+
let fullOutput = "";
|
|
279
|
+
let fullReasoning = "";
|
|
280
|
+
let buffer = "";
|
|
281
|
+
while (true) {
|
|
282
|
+
const { done, value } = await reader.read();
|
|
283
|
+
if (done) break;
|
|
284
|
+
buffer += decoder.decode(value, { stream: true });
|
|
285
|
+
const lines = buffer.split("\n");
|
|
286
|
+
buffer = lines.pop() || "";
|
|
287
|
+
for (const rawLine of lines) {
|
|
288
|
+
const line = rawLine.trim();
|
|
289
|
+
if (!line || line.startsWith(":")) continue;
|
|
290
|
+
if (line.startsWith("data:")) {
|
|
291
|
+
const payload = line.slice(5).trim();
|
|
292
|
+
if (payload === "[DONE]" || payload.includes("[DONE]")) {
|
|
293
|
+
return fullOutput;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const parsed = JSON.parse(payload);
|
|
297
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
298
|
+
const thought = delta?.reasoning_content || delta?.reasoning || "";
|
|
299
|
+
if (thought) {
|
|
300
|
+
fullReasoning += thought;
|
|
301
|
+
if (onReasoningProgress) {
|
|
302
|
+
onReasoningProgress(thought);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const content = delta?.content || "";
|
|
306
|
+
if (content) {
|
|
307
|
+
fullOutput += content;
|
|
308
|
+
if (onContentToken) {
|
|
309
|
+
onContentToken(content);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
} catch {
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return fullOutput;
|
|
228
318
|
}
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
319
|
+
|
|
320
|
+
// src/core/scanner.ts
|
|
321
|
+
var import_fs3 = __toESM(require("fs"));
|
|
322
|
+
var import_path3 = __toESM(require("path"));
|
|
323
|
+
var IGNORED_DIRS = /* @__PURE__ */ new Set([
|
|
324
|
+
"node_modules",
|
|
325
|
+
".git",
|
|
326
|
+
".next",
|
|
327
|
+
"dist",
|
|
328
|
+
"build",
|
|
329
|
+
"coverage",
|
|
330
|
+
".cache",
|
|
331
|
+
"baileys_auth"
|
|
332
|
+
]);
|
|
333
|
+
var IGNORED_FILES = /* @__PURE__ */ new Set([
|
|
334
|
+
"package-lock.json",
|
|
335
|
+
"yarn.lock",
|
|
336
|
+
"pnpm-lock.yaml",
|
|
337
|
+
".DS_Store",
|
|
338
|
+
"Thumbs.db"
|
|
339
|
+
]);
|
|
340
|
+
function scanProjectTree(dir, prefix = "", maxFiles = 100) {
|
|
341
|
+
const tree = [];
|
|
342
|
+
let totalFiles = 0;
|
|
343
|
+
function walk(currentDir, currentPrefix) {
|
|
344
|
+
if (totalFiles >= maxFiles) return;
|
|
345
|
+
let entries = [];
|
|
346
|
+
try {
|
|
347
|
+
entries = import_fs3.default.readdirSync(currentDir, { withFileTypes: true });
|
|
348
|
+
} catch {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
entries.sort((a, b) => {
|
|
352
|
+
if (a.isDirectory() && !b.isDirectory()) return -1;
|
|
353
|
+
if (!a.isDirectory() && b.isDirectory()) return 1;
|
|
354
|
+
return a.name.localeCompare(b.name);
|
|
355
|
+
});
|
|
356
|
+
for (const entry of entries) {
|
|
357
|
+
if (totalFiles >= maxFiles) break;
|
|
358
|
+
const fullPath = import_path3.default.join(currentDir, entry.name);
|
|
359
|
+
const isDir = entry.isDirectory();
|
|
360
|
+
if (isDir) {
|
|
361
|
+
if (IGNORED_DIRS.has(entry.name) || entry.name.startsWith(".")) continue;
|
|
362
|
+
tree.push(`${currentPrefix}\u{1F4C1} ${entry.name}/`);
|
|
363
|
+
walk(fullPath, `${currentPrefix} `);
|
|
364
|
+
} else {
|
|
365
|
+
if (IGNORED_FILES.has(entry.name) || entry.name.endsWith(".png") || entry.name.endsWith(".jpg")) continue;
|
|
366
|
+
tree.push(`${currentPrefix}\u{1F4C4} ${entry.name}`);
|
|
367
|
+
totalFiles++;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
234
370
|
}
|
|
235
|
-
|
|
371
|
+
walk(dir, prefix);
|
|
372
|
+
return { tree, totalFiles };
|
|
236
373
|
}
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
374
|
+
|
|
375
|
+
// src/core/fs-agent.ts
|
|
376
|
+
var import_fs4 = __toESM(require("fs"));
|
|
377
|
+
var import_path4 = __toESM(require("path"));
|
|
378
|
+
function safeReadFile(relativeOrFullPath) {
|
|
379
|
+
try {
|
|
380
|
+
const fullPath = import_path4.default.resolve(process.cwd(), relativeOrFullPath);
|
|
381
|
+
if (!import_fs4.default.existsSync(fullPath)) {
|
|
382
|
+
return { success: false, message: `File not found: ${relativeOrFullPath}`, filePath: fullPath };
|
|
383
|
+
}
|
|
384
|
+
const content = import_fs4.default.readFileSync(fullPath, "utf-8");
|
|
385
|
+
return { success: true, message: "Berhasil membaca file", filePath: fullPath, content };
|
|
386
|
+
} catch (err) {
|
|
387
|
+
return { success: false, message: `Failed to read file: ${err.message}`, filePath: relativeOrFullPath };
|
|
388
|
+
}
|
|
241
389
|
}
|
|
242
|
-
function
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
390
|
+
function safeWriteFile(relativeOrFullPath, content) {
|
|
391
|
+
try {
|
|
392
|
+
const fullPath = import_path4.default.resolve(process.cwd(), relativeOrFullPath);
|
|
393
|
+
const dir = import_path4.default.dirname(fullPath);
|
|
394
|
+
if (!import_fs4.default.existsSync(dir)) {
|
|
395
|
+
import_fs4.default.mkdirSync(dir, { recursive: true });
|
|
396
|
+
}
|
|
397
|
+
import_fs4.default.writeFileSync(fullPath, content, "utf-8");
|
|
398
|
+
return { success: true, message: "File written successfully", filePath: fullPath, content };
|
|
399
|
+
} catch (err) {
|
|
400
|
+
return { success: false, message: `Failed to write file: ${err.message}`, filePath: relativeOrFullPath };
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
// src/core/prompt-builder.ts
|
|
405
|
+
var GATHER_CONTEXT_PREAMBLE = "Gather all the relevant context and then think carefully about how to implement the following:";
|
|
406
|
+
var CONTEXT_HEADING = [
|
|
407
|
+
"Project context (read-only), for tailoring the work:",
|
|
408
|
+
"The file tree and file snippets below are the user\u2019s real working copy. Use them ONLY to fit the task to this project \u2014 its framework, file layout, names and conventions. They are NOT a competing request, and they do not narrow or replace the task \u2014 if none of them is relevant, carry out the task as written."
|
|
409
|
+
].join("\n");
|
|
410
|
+
var EXECUTION_RULES = [
|
|
411
|
+
"# How to carry it out here:",
|
|
412
|
+
"",
|
|
413
|
+
"- File operations are EXECUTED TO DISK by the harness. To create or fully rewrite a file, emit exactly:",
|
|
414
|
+
' <<<WRITE_FILE path="relative/or/absolute/path.ext">>>',
|
|
415
|
+
' ...complete final file content, no placeholders, no "..." omissions...',
|
|
416
|
+
" <<<END_FILE>>>",
|
|
417
|
+
" The path attribute is mandatory and quoted. The content between the tags must be the COMPLETE file \u2014 never a diff, never a fragment.",
|
|
418
|
+
"- To edit an existing file without rewriting it whole, emit exactly:",
|
|
419
|
+
' <<<REPLACE path="path.ext">>>',
|
|
420
|
+
" ...exact existing text to find (must match the file byte-for-byte, including indentation)...",
|
|
421
|
+
" <<<WITH>>>",
|
|
422
|
+
" ...replacement text...",
|
|
423
|
+
" <<<END_REPLACE>>>",
|
|
424
|
+
" Use one such block per edit; several blocks may follow each other for the same file.",
|
|
425
|
+
"- NEVER paste full file contents outside these tags. Everything outside the tags is user-facing prose: a concise report of what you did, the files touched, and recommended next steps.",
|
|
426
|
+
"- Never print internal reasoning, thought monologue, or meta commentary about these rules.",
|
|
427
|
+
"- Leave the user\u2019s changes and unrelated files untouched; modify only what the task needs.",
|
|
428
|
+
"- No promotions, sponsors, or third-party advertising of any kind in the report."
|
|
429
|
+
].join("\n");
|
|
430
|
+
function modelSpecialization(model) {
|
|
252
431
|
return [
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
];
|
|
432
|
+
"",
|
|
433
|
+
`You are running as the ${model.name} profile (${model.id}).`,
|
|
434
|
+
`Specialization: ${model.role}. ${model.description}`
|
|
435
|
+
].join("\n");
|
|
258
436
|
}
|
|
259
|
-
function
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
"
|
|
265
|
-
"
|
|
266
|
-
"
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
"\u255A\u2550\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u255D\u255A\u2550\u255D"
|
|
270
|
-
];
|
|
271
|
-
banner.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
|
|
272
|
-
lines.push("");
|
|
273
|
-
lines.push(
|
|
274
|
-
pad + import_chalk.default.bold.white("Start coding for SarangAI") + " " + greenDot("\u25CF\u25CF\u25CF\u25CF\u25CF\u25CB") + " ".repeat(30) + import_chalk.default.gray("\u2715")
|
|
275
|
-
);
|
|
276
|
-
lines.push("");
|
|
277
|
-
if (!expanded2) {
|
|
278
|
-
lines.push(...renderCard(activeModel2, cursorIdx2 === 0, cardWidth, pad));
|
|
279
|
-
lines.push("");
|
|
280
|
-
const balanceStr = `${Number(userMeta2?.balance ?? 0).toLocaleString()} Credits`;
|
|
281
|
-
lines.push(
|
|
282
|
-
pad + import_chalk.default.bold.white(userMeta2.tier) + import_chalk.default.gray(" \u2022 ") + import_chalk.default.yellow.bold(balanceStr) + import_chalk.default.gray(" remaining \u2022 ") + import_chalk.default.hex("#a855f7")(userMeta2.accountId)
|
|
283
|
-
);
|
|
284
|
-
lines.push("");
|
|
285
|
-
const isSeeAllActive = cursorIdx2 === 1;
|
|
286
|
-
const seeAllPrefix = isSeeAllActive ? import_chalk.default.cyan.bold("\u203A ") : " ";
|
|
287
|
-
const seeAllText = isSeeAllActive ? import_chalk.default.bold.cyan.underline(`\u2193 See all ${CODING_MODELS.length} models`) : import_chalk.default.hex("#a855f7")(`\u2193 See all ${CODING_MODELS.length} models`);
|
|
288
|
-
lines.push(pad + seeAllPrefix + seeAllText);
|
|
289
|
-
lines.push("");
|
|
290
|
-
const isStartActive = cursorIdx2 === 2;
|
|
291
|
-
lines.push(pad + (isStartActive ? import_chalk.default.green.bold("\u203A [\u{1F680} Start Coding Workspace \u21B5]") : import_chalk.default.white(" [\u{1F680} Start Coding Workspace \u21B5]")));
|
|
292
|
-
lines.push("");
|
|
293
|
-
lines.push(pad + import_chalk.default.gray("\u2726 Refer friends \u2192 manage credits:"));
|
|
294
|
-
lines.push("");
|
|
295
|
-
const isCopyActive = cursorIdx2 === 3;
|
|
296
|
-
const copyPrefix = isCopyActive ? import_chalk.default.cyan.bold("\u203A ") : " ";
|
|
297
|
-
const copyLabel = isCopyActive ? import_chalk.default.bold.cyan.underline("\u{1F4CB} Copy invite / dashboard link") : import_chalk.default.white("\u{1F4CB} Copy invite / dashboard link");
|
|
298
|
-
lines.push(pad + copyPrefix + copyLabel);
|
|
299
|
-
} else {
|
|
300
|
-
CODING_MODELS.forEach((m, idx) => {
|
|
301
|
-
lines.push(...renderCard(m, cursorIdx2 === idx + 10, cardWidth, pad));
|
|
302
|
-
});
|
|
303
|
-
lines.push("");
|
|
304
|
-
const balanceStr = `${Number(userMeta2?.balance ?? 0).toLocaleString()} Credits`;
|
|
305
|
-
lines.push(
|
|
306
|
-
pad + import_chalk.default.bold.white(userMeta2.tier) + import_chalk.default.gray(" \u2022 ") + import_chalk.default.yellow.bold(balanceStr) + import_chalk.default.gray(" remaining \u2022 ") + import_chalk.default.hex("#a855f7")(userMeta2.accountId)
|
|
307
|
-
);
|
|
308
|
-
lines.push("");
|
|
309
|
-
const isFewerActive = cursorIdx2 === 20;
|
|
310
|
-
const fewerPrefix = isFewerActive ? import_chalk.default.cyan.bold("\u203A ") : " ";
|
|
311
|
-
const fewerText = isFewerActive ? import_chalk.default.bold.cyan.underline("\u2191 Show fewer") : import_chalk.default.hex("#a855f7")("\u2191 Show fewer");
|
|
312
|
-
lines.push(pad + fewerPrefix + fewerText);
|
|
313
|
-
}
|
|
314
|
-
if (copiedNotice2) {
|
|
315
|
-
lines.push("");
|
|
316
|
-
lines.push(pad + import_chalk.default.green("\u2714 Dashboard link copied to clipboard: https://idshop.or.id/user/dashboard"));
|
|
317
|
-
}
|
|
318
|
-
lines.push("");
|
|
319
|
-
lines.push(pad + import_chalk.default.gray("\u2500".repeat(cardWidth)));
|
|
320
|
-
lines.push(pad + import_chalk.default.gray("Navigasi: [\u2191/\u2193 Panah] \u2022 [Enter/Space] Pilih \u2022 [c] Copy Link \u2022 [q] Keluar"));
|
|
321
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H\x1B[?25l" + lines.join("\n"));
|
|
437
|
+
function buildSystemPrompt(model) {
|
|
438
|
+
return [
|
|
439
|
+
"You are SarangAI, an autonomous coding agent working directly in the user\u2019s project directory from the terminal.",
|
|
440
|
+
"You write production-grade code and execute file edits yourself; you do not just describe them.",
|
|
441
|
+
modelSpecialization(model),
|
|
442
|
+
"",
|
|
443
|
+
"For every task: " + GATHER_CONTEXT_PREAMBLE,
|
|
444
|
+
"",
|
|
445
|
+
EXECUTION_RULES
|
|
446
|
+
].join("\n");
|
|
322
447
|
}
|
|
323
|
-
function
|
|
324
|
-
const
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
"
|
|
330
|
-
|
|
331
|
-
"
|
|
332
|
-
|
|
333
|
-
];
|
|
334
|
-
const lines = [];
|
|
335
|
-
banner.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
|
|
336
|
-
lines.push("");
|
|
337
|
-
lines.push(pad + import_chalk.default.white("SarangAI will run commands on your behalf to help you build."));
|
|
338
|
-
lines.push("");
|
|
339
|
-
lines.push(pad + import_chalk.default.bold.white("Directory ") + import_chalk.default.gray(getDisplayDir()));
|
|
340
|
-
lines.push("");
|
|
341
|
-
const m = Math.floor(timeLeftSec / 60);
|
|
342
|
-
const s = timeLeftSec % 60;
|
|
343
|
-
const timeFormatted = `${m}m ${s < 10 ? "0" : ""}${s}s left`;
|
|
344
|
-
const statusLeft = ` ${model.name} \u2022 ${timeFormatted}`;
|
|
345
|
-
const statusRight = `[Esc] End session `;
|
|
346
|
-
const spaceBetween = Math.max(2, boxWidth - statusLeft.length - statusRight.length);
|
|
347
|
-
const statusBar = import_chalk.default.bgHex("#1e293b").white.bold(statusLeft + " ".repeat(spaceBetween) + import_chalk.default.gray(statusRight));
|
|
348
|
-
lines.push(pad + statusBar);
|
|
349
|
-
const innerWidth = boxWidth - 2;
|
|
350
|
-
const borderTop = "\u250C" + "\u2500".repeat(innerWidth) + "\u2510";
|
|
351
|
-
const borderBottom = "\u2514" + "\u2500".repeat(innerWidth) + "\u2518";
|
|
352
|
-
const textContent = userInput ? userInput : import_chalk.default.gray("Enter a coding task or / for commands");
|
|
353
|
-
const visibleLen = userInput ? userInput.length : 37;
|
|
354
|
-
const paddingRight = Math.max(0, innerWidth - 2 - visibleLen);
|
|
355
|
-
const middleLine = `\u2502 ${textContent}${" ".repeat(paddingRight)}\u2502`;
|
|
356
|
-
lines.push(pad + import_chalk.default.gray(borderTop));
|
|
357
|
-
const inputRowNumber = lines.length + 1;
|
|
358
|
-
lines.push(pad + import_chalk.default.gray(middleLine));
|
|
359
|
-
lines.push(pad + import_chalk.default.gray(borderBottom));
|
|
360
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
|
|
361
|
-
const cursorCol = pad.length + 3 + userInput.length + 1;
|
|
362
|
-
process.stdout.write(`\x1B[${inputRowNumber};${cursorCol}H\x1B[?25h`);
|
|
448
|
+
function buildUserMessage(userPrompt, projectContext, isEmptyProject) {
|
|
449
|
+
const contextSection = isEmptyProject ? `${CONTEXT_HEADING}
|
|
450
|
+
(empty directory \u2014 scaffold from scratch)` : `${CONTEXT_HEADING}
|
|
451
|
+
${projectContext}`;
|
|
452
|
+
return [
|
|
453
|
+
GATHER_CONTEXT_PREAMBLE,
|
|
454
|
+
"",
|
|
455
|
+
userPrompt.trim(),
|
|
456
|
+
"",
|
|
457
|
+
contextSection
|
|
458
|
+
].join("\n");
|
|
363
459
|
}
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
var
|
|
367
|
-
var
|
|
368
|
-
var
|
|
369
|
-
var
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
function cleanupAndExit() {
|
|
388
|
-
if (timerInterval) clearInterval(timerInterval);
|
|
389
|
-
process.stdout.write("\x1B[?1000l\x1B[?1006l\x1B[?1049l\x1B[?25h\n");
|
|
390
|
-
process.exit(0);
|
|
460
|
+
|
|
461
|
+
// src/core/memory.ts
|
|
462
|
+
var MAX_USER_CHARS = 4e3;
|
|
463
|
+
var MAX_ASSISTANT_CHARS = 2e3;
|
|
464
|
+
var MAX_HISTORY_TURNS = 6;
|
|
465
|
+
var MAX_HISTORY_CHARS = 12e3;
|
|
466
|
+
function truncateOnLineBoundary(text, maxChars) {
|
|
467
|
+
const clean = text.trim();
|
|
468
|
+
if (clean.length <= maxChars) return clean;
|
|
469
|
+
const head = clean.slice(0, maxChars);
|
|
470
|
+
const lastNewline = head.lastIndexOf("\n");
|
|
471
|
+
const sliced = lastNewline > maxChars / 2 ? head.slice(0, lastNewline) : head;
|
|
472
|
+
return `${sliced.trimEnd()}
|
|
473
|
+
\u2026(truncated)`;
|
|
474
|
+
}
|
|
475
|
+
function summarizeResult(explanation, writtenFiles, editedFiles = []) {
|
|
476
|
+
const parts = [];
|
|
477
|
+
const okWritten = writtenFiles.filter((f) => f.success).map((f) => f.path);
|
|
478
|
+
const okEdited = editedFiles.filter((f) => f.success).map((f) => f.path);
|
|
479
|
+
if (okWritten.length > 0) parts.push(`Files written: ${okWritten.join(", ")}`);
|
|
480
|
+
if (okEdited.length > 0) parts.push(`Files edited: ${okEdited.join(", ")}`);
|
|
481
|
+
if (explanation.trim()) parts.push(explanation.trim());
|
|
482
|
+
return parts.join("\n");
|
|
391
483
|
}
|
|
392
|
-
function
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
484
|
+
function historyToMessages(history) {
|
|
485
|
+
const messages = [];
|
|
486
|
+
for (const turn of history) {
|
|
487
|
+
messages.push({ role: "user", content: truncateOnLineBoundary(turn.user, MAX_USER_CHARS) });
|
|
488
|
+
messages.push({
|
|
489
|
+
role: "assistant",
|
|
490
|
+
content: truncateOnLineBoundary(turn.summary, MAX_ASSISTANT_CHARS)
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
return messages;
|
|
396
494
|
}
|
|
397
|
-
function
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
conversationHistory = [];
|
|
405
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H");
|
|
406
|
-
redrawWithFreshBalance();
|
|
495
|
+
function boundedHistory(history) {
|
|
496
|
+
let slice = history.slice(-MAX_HISTORY_TURNS);
|
|
497
|
+
const sizeOf = (turns) => turns.reduce((acc, t) => acc + t.user.length + t.summary.length, 0);
|
|
498
|
+
while (slice.length > 1 && sizeOf(slice) > MAX_HISTORY_CHARS) {
|
|
499
|
+
slice = slice.slice(1);
|
|
500
|
+
}
|
|
501
|
+
return slice;
|
|
407
502
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
503
|
+
|
|
504
|
+
// src/core/workspace.ts
|
|
505
|
+
var IGNORED = [
|
|
506
|
+
"node_modules",
|
|
507
|
+
".git",
|
|
508
|
+
"dist",
|
|
509
|
+
".next",
|
|
510
|
+
"build",
|
|
511
|
+
"out",
|
|
512
|
+
"package-lock.json",
|
|
513
|
+
"yarn.lock",
|
|
514
|
+
"pnpm-lock.yaml",
|
|
515
|
+
".DS_Store"
|
|
516
|
+
];
|
|
517
|
+
var MAX_TREE_LINES = 60;
|
|
518
|
+
var MAX_SNIPPET_FILES = 4;
|
|
519
|
+
var MAX_SNIPPET_CHARS = 1200;
|
|
520
|
+
function scanFiles(dir, base = dir, max = 25) {
|
|
521
|
+
let res = [];
|
|
522
|
+
try {
|
|
523
|
+
const list = import_fs5.default.readdirSync(dir);
|
|
524
|
+
for (const item of list) {
|
|
525
|
+
if (IGNORED.includes(item)) continue;
|
|
526
|
+
const full = import_path5.default.join(dir, item);
|
|
527
|
+
const rel = import_path5.default.relative(base, full);
|
|
528
|
+
const stat = import_fs5.default.statSync(full);
|
|
529
|
+
if (stat.isDirectory()) {
|
|
530
|
+
res = res.concat(scanFiles(full, base, max));
|
|
531
|
+
} else if (/\.(ts|tsx|js|jsx|json|md|py|go|rs)$/i.test(item)) {
|
|
532
|
+
res.push(rel);
|
|
420
533
|
}
|
|
534
|
+
if (res.length >= max) break;
|
|
421
535
|
}
|
|
422
|
-
}
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
return res;
|
|
423
539
|
}
|
|
424
|
-
function
|
|
425
|
-
|
|
540
|
+
function assembleProjectContext() {
|
|
541
|
+
const cwd = process.cwd();
|
|
542
|
+
const files = scanFiles(cwd);
|
|
543
|
+
const isEmpty = files.length === 0;
|
|
544
|
+
if (isEmpty) {
|
|
545
|
+
return { context: "", isEmpty: true };
|
|
546
|
+
}
|
|
547
|
+
const scanRes = scanProjectTree(cwd, "", MAX_TREE_LINES);
|
|
548
|
+
const tree = Array.isArray(scanRes) ? scanRes : scanRes?.tree || [];
|
|
549
|
+
const treeText = tree.join("\n");
|
|
550
|
+
const snippets = [];
|
|
551
|
+
for (const f of files.slice(0, MAX_SNIPPET_FILES)) {
|
|
552
|
+
const r = safeReadFile(f);
|
|
553
|
+
if (r.success && r.content) {
|
|
554
|
+
snippets.push(
|
|
555
|
+
`[FILE: ${f}]
|
|
556
|
+
\`\`\`
|
|
557
|
+
${r.content.slice(0, MAX_SNIPPET_CHARS)}
|
|
558
|
+
\`\`\``
|
|
559
|
+
);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
const snippetText = snippets.length > 0 ? `
|
|
563
|
+
Key file snippets:
|
|
564
|
+
${snippets.join("\n\n")}` : "";
|
|
565
|
+
return {
|
|
566
|
+
context: `File tree:
|
|
567
|
+
${treeText}${snippetText}`,
|
|
568
|
+
isEmpty: false
|
|
569
|
+
};
|
|
426
570
|
}
|
|
427
|
-
function
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
let
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
style = "code";
|
|
437
|
-
} else if (inCode) {
|
|
438
|
-
style = "code";
|
|
439
|
-
} else if (trimmed.startsWith("#")) {
|
|
440
|
-
style = "heading";
|
|
571
|
+
function executeFileWrites(rawResponse) {
|
|
572
|
+
const fileRegex = /<<<WRITE_FILE\s+path=["']([^"']+)["']>>>([\s\S]*?)<<<END_FILE>>>/g;
|
|
573
|
+
const results = [];
|
|
574
|
+
let match;
|
|
575
|
+
while ((match = fileRegex.exec(rawResponse)) !== null) {
|
|
576
|
+
const filePath = match[1].trim();
|
|
577
|
+
let content = match[2].trim();
|
|
578
|
+
if (content.startsWith("```")) {
|
|
579
|
+
content = content.replace(/^```[a-zA-Z]*\n/, "").replace(/\n```$/, "");
|
|
441
580
|
}
|
|
442
|
-
|
|
443
|
-
|
|
581
|
+
const writeRes = safeWriteFile(filePath, content);
|
|
582
|
+
results.push({ path: filePath, success: writeRes.success });
|
|
583
|
+
}
|
|
584
|
+
return results;
|
|
585
|
+
}
|
|
586
|
+
function executeFileReplaces(rawResponse) {
|
|
587
|
+
const replaceRegex = /<<<REPLACE\s+path=["']([^"']+)["']>>>([\s\S]*?)<<<WITH>>>([\s\S]*?)<<<END_REPLACE>>>/g;
|
|
588
|
+
const results = [];
|
|
589
|
+
let match;
|
|
590
|
+
while ((match = replaceRegex.exec(rawResponse)) !== null) {
|
|
591
|
+
const filePath = match[1].trim();
|
|
592
|
+
const find = match[2].trim();
|
|
593
|
+
const replaceWith = match[3].trim();
|
|
594
|
+
const existing = safeReadFile(filePath);
|
|
595
|
+
if (!existing.success || existing.content == null) {
|
|
596
|
+
results.push({ path: filePath, success: false });
|
|
444
597
|
continue;
|
|
445
598
|
}
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
out.push({ text: remaining.slice(0, cut), style });
|
|
451
|
-
remaining = remaining.slice(cut).replace(/^ /, "");
|
|
599
|
+
const current = existing.content;
|
|
600
|
+
if (!current.includes(find)) {
|
|
601
|
+
results.push({ path: filePath, success: false });
|
|
602
|
+
continue;
|
|
452
603
|
}
|
|
453
|
-
|
|
604
|
+
const updated = current.replace(find, () => replaceWith);
|
|
605
|
+
const writeRes = safeWriteFile(filePath, updated);
|
|
606
|
+
results.push({ path: filePath, success: writeRes.success });
|
|
454
607
|
}
|
|
455
|
-
return
|
|
608
|
+
return results;
|
|
456
609
|
}
|
|
457
|
-
function
|
|
458
|
-
|
|
459
|
-
if (l.style === "code") return import_chalk.default.hex("#94a3b8")(l.text);
|
|
460
|
-
if (l.style === "error") return import_chalk.default.red(l.text);
|
|
461
|
-
return import_chalk.default.hex("#e2e8f0")(l.text);
|
|
610
|
+
function stripFileBlocks(rawResponse) {
|
|
611
|
+
return rawResponse.replace(/<<<WRITE_FILE\s+path=["'][^"']+["']>>>[\s\S]*?<<<END_FILE>>>/g, "").replace(/<<<REPLACE\s+path=["'][^"']+["']>>>[\s\S]*?<<<END_REPLACE>>>/g, "").replace(/<<<WITH>>>/g, "").trim();
|
|
462
612
|
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
html: "html",
|
|
495
|
-
css: "css",
|
|
496
|
-
scss: "scss",
|
|
497
|
-
sass: "sass",
|
|
498
|
-
less: "less",
|
|
499
|
-
sql: "sql",
|
|
500
|
-
json: "json",
|
|
501
|
-
yaml: "yaml",
|
|
502
|
-
yml: "yml",
|
|
503
|
-
toml: "toml",
|
|
504
|
-
xml: "xml",
|
|
505
|
-
markdown: "md",
|
|
506
|
-
md: "md",
|
|
507
|
-
dockerfile: "",
|
|
508
|
-
makefile: "",
|
|
509
|
-
ini: "ini",
|
|
510
|
-
env: "",
|
|
511
|
-
txt: "txt",
|
|
512
|
-
text: "txt",
|
|
513
|
-
csv: "csv"
|
|
514
|
-
};
|
|
515
|
-
function extractFilesFromMarkdown(text) {
|
|
516
|
-
const files = [];
|
|
517
|
-
const fence = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]*)(?::[ \t]*([^\n`]+))?[^\n]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
|
|
518
|
-
let m;
|
|
519
|
-
while ((m = fence.exec(text)) !== null) {
|
|
520
|
-
const lang = (m[1] || "").toLowerCase().trim();
|
|
521
|
-
const fname = (m[2] || "").trim().replace(/^["']|["']$/g, "");
|
|
522
|
-
if (!fname) continue;
|
|
523
|
-
files.push({ filename: fname, content: m[3], lang: lang || "text" });
|
|
524
|
-
}
|
|
525
|
-
return files;
|
|
613
|
+
async function runAutonomousAgent({ model, userPrompt, history, onStatus }) {
|
|
614
|
+
const { context, isEmpty } = assembleProjectContext();
|
|
615
|
+
const messages = [
|
|
616
|
+
{ role: "system", content: buildSystemPrompt(model) },
|
|
617
|
+
// Riwayat multi-giliran: konteks baca-saja agar tugas lanjutan tersambung.
|
|
618
|
+
...historyToMessages(boundedHistory(history ?? [])),
|
|
619
|
+
// Task-first: tugas user di atas, konteks sebagai background read-only.
|
|
620
|
+
{ role: "user", content: buildUserMessage(userPrompt, context, isEmpty) }
|
|
621
|
+
];
|
|
622
|
+
onStatus?.(`[${model.name}] Reading context files...`);
|
|
623
|
+
let rawResponse = "";
|
|
624
|
+
let writeNotified = false;
|
|
625
|
+
await streamChatCompletion({
|
|
626
|
+
model,
|
|
627
|
+
messages,
|
|
628
|
+
// Reasoning monologue TIDAK PERNAH dicetak ke layar.
|
|
629
|
+
onReasoningProgress: () => {
|
|
630
|
+
},
|
|
631
|
+
onContentToken: (token) => {
|
|
632
|
+
rawResponse += token;
|
|
633
|
+
if (!writeNotified && (rawResponse.includes("<<<WRITE_FILE") || rawResponse.includes("<<<REPLACE"))) {
|
|
634
|
+
writeNotified = true;
|
|
635
|
+
onStatus?.(`[${model.name}] Applying code changes...`);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
const writtenFiles = executeFileWrites(rawResponse);
|
|
640
|
+
const editedFiles = executeFileReplaces(rawResponse);
|
|
641
|
+
const cleanExplanation = stripFileBlocks(rawResponse);
|
|
642
|
+
onStatus?.(`[${model.name}] Done`);
|
|
643
|
+
return { explanation: cleanExplanation, writtenFiles, editedFiles, rawLength: rawResponse.length };
|
|
526
644
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
645
|
+
|
|
646
|
+
// src/ui/tui.ts
|
|
647
|
+
var import_chalk3 = __toESM(require("chalk"));
|
|
648
|
+
init_banner();
|
|
649
|
+
|
|
650
|
+
// src/ui/model-menu.ts
|
|
651
|
+
var import_chalk2 = __toESM(require("chalk"));
|
|
652
|
+
var BORDER_COLOR = import_chalk2.default.hex("#5b21b6");
|
|
653
|
+
var ACCENT = import_chalk2.default.hex("#c084fc");
|
|
654
|
+
var DIM = import_chalk2.default.gray;
|
|
655
|
+
var MENU_FOOTER = "\u2191/\u2193 or j/k: navigate 1-5: quick pick Enter: confirm Esc/q: cancel";
|
|
656
|
+
function menuModels() {
|
|
657
|
+
return Object.values(LOCKED_CODING_MODELS);
|
|
533
658
|
}
|
|
534
|
-
function
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
return ext === void 0 ? null : `snippet.${ext || "txt"}`;
|
|
659
|
+
function createModelMenuState(mode, activeAlias) {
|
|
660
|
+
const items = menuModels();
|
|
661
|
+
const cursor = activeAlias ? Math.max(0, items.findIndex((m) => m.alias === activeAlias)) : 0;
|
|
662
|
+
return { items, cursor, activeAlias: activeAlias ?? null, mode };
|
|
539
663
|
}
|
|
540
|
-
function
|
|
541
|
-
const
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
664
|
+
function resolveMenuKey(key, state) {
|
|
665
|
+
const n = state.items.length;
|
|
666
|
+
switch (key) {
|
|
667
|
+
case "\x1B[A":
|
|
668
|
+
// up arrow
|
|
669
|
+
case "k":
|
|
670
|
+
return { kind: "move", cursor: (state.cursor - 1 + n) % n };
|
|
671
|
+
case "\x1B[B":
|
|
672
|
+
// down arrow
|
|
673
|
+
case "j":
|
|
674
|
+
return { kind: "move", cursor: (state.cursor + 1) % n };
|
|
675
|
+
case "1":
|
|
676
|
+
case "2":
|
|
677
|
+
case "3":
|
|
678
|
+
case "4":
|
|
679
|
+
case "5": {
|
|
680
|
+
const index = Number(key) - 1;
|
|
681
|
+
return index < n ? { kind: "select", index } : { kind: "ignore" };
|
|
553
682
|
}
|
|
683
|
+
case "\r":
|
|
684
|
+
case "\n":
|
|
685
|
+
return { kind: "select", index: state.cursor };
|
|
686
|
+
case "\x1B":
|
|
687
|
+
// escape
|
|
688
|
+
case "q":
|
|
689
|
+
case "":
|
|
690
|
+
return state.mode === "startup" ? { kind: "abort" } : { kind: "cancel" };
|
|
691
|
+
default:
|
|
692
|
+
return { kind: "ignore" };
|
|
554
693
|
}
|
|
555
|
-
return best ? [best] : [];
|
|
556
694
|
}
|
|
557
|
-
function
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
try {
|
|
566
|
-
const target = import_path2.default.join(process.cwd(), safe);
|
|
567
|
-
import_fs2.default.writeFileSync(target, f.content, "utf-8");
|
|
568
|
-
const bytes = Buffer.byteLength(f.content, "utf-8");
|
|
569
|
-
notes.push(import_chalk.default.green(`\u2714 File created: ${safe}`) + import_chalk.default.gray(` (${bytes} bytes)`));
|
|
570
|
-
} catch (err) {
|
|
571
|
-
notes.push(import_chalk.default.red(`\u2715 Gagal menulis ${safe}: ${err.message}`));
|
|
572
|
-
}
|
|
573
|
-
}
|
|
574
|
-
return notes;
|
|
695
|
+
function formatModelMenuItem(m, selected, active) {
|
|
696
|
+
const radio = selected ? import_chalk2.default.green("[\u25CF]") : import_chalk2.default.gray("[ ]");
|
|
697
|
+
const activeMark = active ? import_chalk2.default.green(" \u25C9") : "";
|
|
698
|
+
const name = `${m.alias.padEnd(10)}- ${m.name.padEnd(22)}(${m.role})`;
|
|
699
|
+
const suffix = m.alias === "glm" ? import_chalk2.default.yellow(" [Default Recommendation]") : "";
|
|
700
|
+
const line = `${radio} ${ACCENT(name)}${activeMark}${suffix}`;
|
|
701
|
+
return selected ? import_chalk2.default.bold(line) : line;
|
|
575
702
|
}
|
|
576
|
-
|
|
577
|
-
"
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
]
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
703
|
+
function buildModelMenuLines(state) {
|
|
704
|
+
const title = state.mode === "startup" ? " Select a Model " : " Switch Model ";
|
|
705
|
+
const lines = [
|
|
706
|
+
"",
|
|
707
|
+
BORDER_COLOR(`\u250C${"\u2500".repeat(66)}\u2510`),
|
|
708
|
+
BORDER_COLOR("\u2502") + import_chalk2.default.bold.hex("#c084fc")(title.padEnd(66, "\u2500")) + BORDER_COLOR("\u2502"),
|
|
709
|
+
BORDER_COLOR(`\u251C${"\u2500".repeat(66)}\u2524`)
|
|
710
|
+
];
|
|
711
|
+
state.items.forEach((m, i) => {
|
|
712
|
+
const selected = i === state.cursor;
|
|
713
|
+
const active = state.activeAlias === m.alias;
|
|
714
|
+
lines.push(formatModelMenuItem(m, selected, active));
|
|
715
|
+
});
|
|
716
|
+
lines.push(BORDER_COLOR(`\u251C${"\u2500".repeat(66)}\u2524`));
|
|
717
|
+
lines.push(BORDER_COLOR("\u2502") + DIM(` ${MENU_FOOTER}`.padEnd(66)) + BORDER_COLOR("\u2502"));
|
|
718
|
+
lines.push(BORDER_COLOR(`\u2514${"\u2500".repeat(66)}\u2518`));
|
|
719
|
+
lines.push("");
|
|
720
|
+
return lines;
|
|
721
|
+
}
|
|
722
|
+
function abortStartupSelection() {
|
|
723
|
+
process.stdout.write("\x1B[?25h\x1B[?1049l");
|
|
724
|
+
console.log("Session aborted. Goodbye!");
|
|
725
|
+
process.exit(0);
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/ui/tui.ts
|
|
729
|
+
var BORDER = import_chalk3.default.hex("#5b21b6");
|
|
730
|
+
var VIOLET = import_chalk3.default.hex("#a78bfa");
|
|
731
|
+
var DIM2 = import_chalk3.default.gray;
|
|
732
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
733
|
+
var HINT_LINE = "Enter your coding task. /model: switch model /clear: clear context exit: quit";
|
|
734
|
+
var ANSI_RE = /\x1b\[[0-9;]*[A-Za-z]/g;
|
|
735
|
+
function visibleWidth(line) {
|
|
736
|
+
return line.replace(ANSI_RE, "").length;
|
|
737
|
+
}
|
|
738
|
+
function wrapAnsiLine(line, width) {
|
|
739
|
+
if (line.length === 0) return [""];
|
|
740
|
+
if (visibleWidth(line) <= width) return [line];
|
|
587
741
|
const out = [];
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
742
|
+
let current = "";
|
|
743
|
+
let currentWidth = 0;
|
|
744
|
+
let pendingAnsi = "";
|
|
745
|
+
for (const token of line.split(/(\s+)/)) {
|
|
746
|
+
let rest = token;
|
|
747
|
+
while (rest.length > 0) {
|
|
748
|
+
const ansiMatch = rest.match(/^\x1b\[[0-9;]*[A-Za-z]/);
|
|
749
|
+
if (ansiMatch) {
|
|
750
|
+
pendingAnsi += ansiMatch[0];
|
|
751
|
+
rest = rest.slice(ansiMatch[0].length);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
const chunk = rest[0];
|
|
755
|
+
if (currentWidth + 1 > width) {
|
|
756
|
+
out.push(current + (pendingAnsi ? "" : ""));
|
|
757
|
+
current = pendingAnsi;
|
|
758
|
+
currentWidth = 0;
|
|
759
|
+
}
|
|
760
|
+
if (chunk === " " && currentWidth === 0) {
|
|
761
|
+
rest = rest.slice(1);
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
current += pendingAnsi + chunk;
|
|
765
|
+
pendingAnsi = "";
|
|
766
|
+
currentWidth += 1;
|
|
767
|
+
rest = rest.slice(1);
|
|
768
|
+
}
|
|
594
769
|
}
|
|
770
|
+
if (current.length > 0 || out.length === 0) out.push(current);
|
|
595
771
|
return out;
|
|
596
772
|
}
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
773
|
+
var TerminalTui = class {
|
|
774
|
+
out;
|
|
775
|
+
inp;
|
|
776
|
+
onSubmit;
|
|
777
|
+
onExit;
|
|
778
|
+
onModelPick;
|
|
779
|
+
header;
|
|
780
|
+
history = [];
|
|
781
|
+
scrollOffset = 0;
|
|
782
|
+
// 0 = pinned ke bawah
|
|
783
|
+
input = "";
|
|
784
|
+
cursor = 0;
|
|
785
|
+
inputHistory = [];
|
|
786
|
+
inputHistoryIdx = -1;
|
|
787
|
+
status = { kind: "idle", label: "", startedAt: null };
|
|
788
|
+
spinnerIdx = 0;
|
|
789
|
+
tickTimer = null;
|
|
790
|
+
renderQueued = false;
|
|
791
|
+
lastFullFrame = "";
|
|
792
|
+
active = false;
|
|
793
|
+
/**
|
|
794
|
+
* Menu model aktif (startup maupun session): fokus keyboard penuh di menu,
|
|
795
|
+
* kotak input di bawah tetap digambar (dim) tanpa pergeseran layout.
|
|
796
|
+
*/
|
|
797
|
+
menu = null;
|
|
798
|
+
keyHandler;
|
|
799
|
+
resizeHandler;
|
|
800
|
+
constructor(opts, header) {
|
|
801
|
+
this.out = process.stdout;
|
|
802
|
+
this.inp = process.stdin;
|
|
803
|
+
this.onSubmit = opts.onSubmit;
|
|
804
|
+
this.onExit = opts.onExit;
|
|
805
|
+
this.onModelPick = opts.onModelPick;
|
|
806
|
+
this.header = header;
|
|
807
|
+
this.keyHandler = (chunk) => this.handleKey(chunk);
|
|
808
|
+
this.resizeHandler = () => this.requestRender(true);
|
|
809
|
+
this.out.on("resize", this.resizeHandler);
|
|
619
810
|
}
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
|
|
629
|
-
for (const l of visible) {
|
|
630
|
-
const styled = l.style === "query" ? import_chalk.default.bold.hex("#c084fc")(l.text) : styleBodyLine(l);
|
|
631
|
-
const fill = Math.max(0, contentW - visibleLength(styled));
|
|
632
|
-
lines.push(pad + import_chalk.default.gray("\u2502 ") + styled + " ".repeat(fill) + import_chalk.default.gray(" \u2502"));
|
|
811
|
+
// ---------------------------------------------------------------- lifecycle
|
|
812
|
+
start() {
|
|
813
|
+
this.active = true;
|
|
814
|
+
this.out.write("\x1B[?1049h\x1B[?25l");
|
|
815
|
+
this.inp.setRawMode(true);
|
|
816
|
+
this.inp.resume();
|
|
817
|
+
this.inp.on("data", this.keyHandler);
|
|
818
|
+
this.requestRender(true);
|
|
633
819
|
}
|
|
634
|
-
|
|
635
|
-
|
|
820
|
+
close() {
|
|
821
|
+
if (!this.active) return;
|
|
822
|
+
this.active = false;
|
|
823
|
+
this.stopTick();
|
|
824
|
+
this.inp.removeListener("data", this.keyHandler);
|
|
825
|
+
this.inp.setRawMode(false);
|
|
826
|
+
this.inp.pause();
|
|
827
|
+
this.out.write("\x1B[?25h\x1B[?1049l");
|
|
636
828
|
}
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
lines.push(pad + scrollTag + import_chalk.default.gray("(/new = sesi baru \u2022 exit = keluar \u2022 Esc = menu)"));
|
|
642
|
-
const textContent = currentTaskInput ? currentTaskInput : import_chalk.default.gray("Ketik follow-up\u2026");
|
|
643
|
-
const visibleLen = currentTaskInput ? currentTaskInput.length : 17;
|
|
644
|
-
const paddingRight = Math.max(0, innerW - 2 - visibleLen);
|
|
645
|
-
lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
|
|
646
|
-
const inputRowNumber = lines.length + 1;
|
|
647
|
-
lines.push(pad + import_chalk.default.gray(`\u2502 ${textContent}${" ".repeat(paddingRight)}\u2502`));
|
|
648
|
-
lines.push(pad + import_chalk.default.gray("\u2514" + "\u2500".repeat(innerW) + "\u2518"));
|
|
649
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
|
|
650
|
-
const cursorCol = pad.length + 5 + currentTaskInput.length;
|
|
651
|
-
process.stdout.write(`\x1B[${inputRowNumber};${cursorCol}H\x1B[?25h`);
|
|
652
|
-
} else {
|
|
653
|
-
lines.push("");
|
|
654
|
-
const range = start > 0 || start < maxStart ? ` \u2022 ${start + 1}-${Math.min(respTotalLines, start + viewRows)}/${respTotalLines}` : ` \u2022 ${respTotalLines} baris`;
|
|
655
|
-
const status = turn.streaming ? import_chalk.default.gray(`[Streaming \u2022 ${activeModel.name}${range}] Esc = batalkan`) : turn.error ? import_chalk.default.red("\u2715 Error") + import_chalk.default.gray(`${range} \u2022 tekan tombol untuk lanjut`) : import_chalk.default.green("\u2714 Selesai") + import_chalk.default.gray(`${range} \u2022 ketik follow-up di bawah \u2022 Esc = menu`);
|
|
656
|
-
lines.push(pad + status);
|
|
657
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
|
|
829
|
+
// ------------------------------------------------------------- public state
|
|
830
|
+
setHeader(stats, model) {
|
|
831
|
+
this.header = { stats, model };
|
|
832
|
+
this.requestRender(true);
|
|
658
833
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
respScroll = Math.max(0, Math.min(maxStart, current + delta));
|
|
664
|
-
drawSessionScreen(lastTurn, { inputMode: "typing" });
|
|
665
|
-
}
|
|
666
|
-
function redrawInputScreen() {
|
|
667
|
-
if (sessionScreen) drawSessionScreen(lastTurn, { inputMode: "typing" });
|
|
668
|
-
else drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
|
|
669
|
-
}
|
|
670
|
-
function finishTurnToInput() {
|
|
671
|
-
refreshUserMeta();
|
|
672
|
-
isExecuting = false;
|
|
673
|
-
sessionScreen = true;
|
|
674
|
-
currentTaskInput = keyBuffer;
|
|
675
|
-
keyBuffer = "";
|
|
676
|
-
respScroll = null;
|
|
677
|
-
drawSessionScreen(lastTurn, { inputMode: "typing" });
|
|
678
|
-
}
|
|
679
|
-
async function runPromptStream(query, isRetry = false) {
|
|
680
|
-
isExecuting = true;
|
|
681
|
-
respStreaming = true;
|
|
682
|
-
respQuery = query;
|
|
683
|
-
respText = "";
|
|
684
|
-
respError = void 0;
|
|
685
|
-
respScroll = null;
|
|
686
|
-
activeAbort = new AbortController();
|
|
687
|
-
process.stdout.write("\x1B[?25l");
|
|
688
|
-
lastTurn = { query, text: "", streaming: true, notes: [] };
|
|
689
|
-
drawSessionScreen(lastTurn);
|
|
690
|
-
let lastPaint = 0;
|
|
691
|
-
const repaint = (streaming, force = false) => {
|
|
692
|
-
const now = Date.now();
|
|
693
|
-
if (!force && now - lastPaint < 80) return;
|
|
694
|
-
lastPaint = now;
|
|
695
|
-
lastTurn = { query, text: respText, streaming, error: respError, notes: lastTurn.notes };
|
|
696
|
-
drawSessionScreen(lastTurn);
|
|
697
|
-
};
|
|
698
|
-
try {
|
|
699
|
-
const res = await fetch(`${baseUrl}/api/gateway/v1/chat/completions`, {
|
|
700
|
-
method: "POST",
|
|
701
|
-
headers: {
|
|
702
|
-
"Content-Type": "application/json",
|
|
703
|
-
Authorization: `Bearer ${cfg.apiKey}`
|
|
704
|
-
},
|
|
705
|
-
signal: activeAbort.signal,
|
|
706
|
-
body: JSON.stringify({
|
|
707
|
-
model: activeModel.id,
|
|
708
|
-
messages: [
|
|
709
|
-
{ role: "system", content: AGENT_SYSTEM_PROMPT },
|
|
710
|
-
...conversationHistory
|
|
711
|
-
],
|
|
712
|
-
stream: true
|
|
713
|
-
})
|
|
714
|
-
});
|
|
715
|
-
if (!res.ok) {
|
|
716
|
-
let detail = res.statusText || `HTTP ${res.status}`;
|
|
717
|
-
try {
|
|
718
|
-
const errBody = await res.json();
|
|
719
|
-
const msg = errBody?.error?.message || errBody?.error || errBody?.message;
|
|
720
|
-
if (msg) detail = typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
721
|
-
} catch {
|
|
722
|
-
}
|
|
723
|
-
respError = `(${res.status}) ${detail}`;
|
|
724
|
-
repaint(false, true);
|
|
725
|
-
if (res.status === 401 && !isRetry) {
|
|
726
|
-
respError = "API key tidak valid atau sudah di-revoke. Memulai ulang otorisasi...";
|
|
727
|
-
repaint(false, true);
|
|
728
|
-
try {
|
|
729
|
-
saveConfig({ apiKey: void 0 });
|
|
730
|
-
} catch {
|
|
731
|
-
}
|
|
732
|
-
cfg = getConfig();
|
|
733
|
-
const newKey = await handleAutoAuth(baseUrl);
|
|
734
|
-
if (newKey) {
|
|
735
|
-
cfg = getConfig();
|
|
736
|
-
userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "") ?? userMeta;
|
|
737
|
-
return runPromptStream(query, true);
|
|
738
|
-
}
|
|
739
|
-
respError = "Otorisasi ulang dibatalkan.";
|
|
740
|
-
repaint(false, true);
|
|
741
|
-
}
|
|
742
|
-
} else {
|
|
743
|
-
const reader = res.body?.getReader();
|
|
744
|
-
const decoder = new TextDecoder();
|
|
745
|
-
if (reader) {
|
|
746
|
-
let sseBuffer = "";
|
|
747
|
-
while (true) {
|
|
748
|
-
const { done, value } = await reader.read();
|
|
749
|
-
if (done) break;
|
|
750
|
-
sseBuffer += decoder.decode(value, { stream: true });
|
|
751
|
-
const nlIdx = sseBuffer.lastIndexOf("\n");
|
|
752
|
-
if (nlIdx === -1) continue;
|
|
753
|
-
const complete = sseBuffer.slice(0, nlIdx);
|
|
754
|
-
sseBuffer = sseBuffer.slice(nlIdx + 1);
|
|
755
|
-
for (const l of complete.split("\n")) {
|
|
756
|
-
if (l.startsWith("data: ") && l !== "data: [DONE]") {
|
|
757
|
-
try {
|
|
758
|
-
const p = JSON.parse(l.slice(6));
|
|
759
|
-
const delta = p.choices?.[0]?.delta?.content;
|
|
760
|
-
if (delta) respText += delta;
|
|
761
|
-
} catch {
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
repaint(true);
|
|
766
|
-
}
|
|
767
|
-
repaint(false, true);
|
|
768
|
-
}
|
|
834
|
+
appendLines(lines) {
|
|
835
|
+
for (const line of lines) {
|
|
836
|
+
const wrapped = wrapAnsiLine(line, Math.max(10, this.width() - 3));
|
|
837
|
+
this.history.push(...wrapped);
|
|
769
838
|
}
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
839
|
+
if (this.history.length > 5e3) {
|
|
840
|
+
this.history = this.history.slice(-4e3);
|
|
841
|
+
}
|
|
842
|
+
this.scrollOffset = 0;
|
|
843
|
+
this.requestRender(true);
|
|
844
|
+
}
|
|
845
|
+
setStatus(kind, label, startedAt = null) {
|
|
846
|
+
this.status = { kind, label, startedAt };
|
|
847
|
+
if (kind === "busy") {
|
|
848
|
+
this.startTick();
|
|
774
849
|
} else {
|
|
775
|
-
|
|
850
|
+
this.stopTick();
|
|
776
851
|
}
|
|
852
|
+
this.requestRender(false);
|
|
777
853
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
854
|
+
clearContent() {
|
|
855
|
+
this.history = [];
|
|
856
|
+
this.scrollOffset = 0;
|
|
857
|
+
this.requestRender(true);
|
|
781
858
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
859
|
+
// ------------------------------------------------------------- menu /model
|
|
860
|
+
/**
|
|
861
|
+
* Buka menu pemilihan model: FOKUS KEYBOARD dialihkan ke menu. Selama menu
|
|
862
|
+
* terbuka, panah atas/bawah TIDAK menggeser kursor/riwayat kotak input.
|
|
863
|
+
* Mode 'startup': Escape/q/Ctrl+C mengakhiri sesi (terminal dipulihkan).
|
|
864
|
+
*/
|
|
865
|
+
openModelMenu(mode, activeAlias) {
|
|
866
|
+
this.menu = createModelMenuState(mode, activeAlias);
|
|
867
|
+
this.inputHistoryIdx = -1;
|
|
868
|
+
this.requestRender(true);
|
|
788
869
|
}
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
870
|
+
/** Tutup menu dan kembalikan fokus ke kotak input bawah. */
|
|
871
|
+
closeModelMenu() {
|
|
872
|
+
this.menu = null;
|
|
873
|
+
this.requestRender(true);
|
|
874
|
+
}
|
|
875
|
+
get isModelMenuOpen() {
|
|
876
|
+
return this.menu !== null;
|
|
877
|
+
}
|
|
878
|
+
get isStartupSelection() {
|
|
879
|
+
return this.menu?.mode === "startup";
|
|
880
|
+
}
|
|
881
|
+
/** Proses satu tombol menu. */
|
|
882
|
+
handleMenuKey(key) {
|
|
883
|
+
if (!this.menu) return;
|
|
884
|
+
const action = resolveMenuKey(key, this.menu);
|
|
885
|
+
switch (action.kind) {
|
|
886
|
+
case "move":
|
|
887
|
+
this.menu.cursor = action.cursor;
|
|
888
|
+
this.requestRender(true);
|
|
889
|
+
break;
|
|
890
|
+
case "select": {
|
|
891
|
+
const idx = action.index;
|
|
892
|
+
this.closeModelMenu();
|
|
893
|
+
this.onModelPick?.(idx);
|
|
894
|
+
break;
|
|
813
895
|
}
|
|
814
|
-
|
|
896
|
+
case "cancel":
|
|
897
|
+
this.closeModelMenu();
|
|
898
|
+
break;
|
|
899
|
+
case "abort":
|
|
900
|
+
this.close();
|
|
901
|
+
abortStartupSelection();
|
|
902
|
+
break;
|
|
903
|
+
case "ignore":
|
|
904
|
+
break;
|
|
815
905
|
}
|
|
816
906
|
}
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
}
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
907
|
+
/** Baris menu modal — rendering diserahkan ke modul pure model-menu.ts. */
|
|
908
|
+
modelMenuLines() {
|
|
909
|
+
if (!this.menu) return [];
|
|
910
|
+
return buildModelMenuLines(this.menu);
|
|
911
|
+
}
|
|
912
|
+
get isActive() {
|
|
913
|
+
return this.active;
|
|
914
|
+
}
|
|
915
|
+
// -------------------------------------------------------------- timer kecil
|
|
916
|
+
startTick() {
|
|
917
|
+
if (this.tickTimer) return;
|
|
918
|
+
this.tickTimer = setInterval(() => {
|
|
919
|
+
this.spinnerIdx = (this.spinnerIdx + 1) % SPINNER_FRAMES.length;
|
|
920
|
+
this.requestRender(false);
|
|
921
|
+
}, 120);
|
|
828
922
|
}
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
process.exit(1);
|
|
923
|
+
stopTick() {
|
|
924
|
+
if (this.tickTimer) {
|
|
925
|
+
clearInterval(this.tickTimer);
|
|
926
|
+
this.tickTimer = null;
|
|
834
927
|
}
|
|
835
|
-
cfg = getConfig();
|
|
836
928
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
import_readline.default.emitKeypressEvents(process.stdin);
|
|
841
|
-
if (process.stdin.isTTY) {
|
|
842
|
-
process.stdin.setRawMode(true);
|
|
929
|
+
// ------------------------------------------------------------------ layout
|
|
930
|
+
width() {
|
|
931
|
+
return Math.max(20, this.out.columns || 80);
|
|
843
932
|
}
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
933
|
+
height() {
|
|
934
|
+
return Math.max(12, this.out.rows || 24);
|
|
935
|
+
}
|
|
936
|
+
headerLines() {
|
|
937
|
+
const w = this.width();
|
|
938
|
+
const lines = SARANGAI_ASCII.split("\n");
|
|
939
|
+
lines.push(BORDER("\u2501".repeat(Math.min(w, 68))));
|
|
940
|
+
lines.push(formatAccountLine(this.header.stats));
|
|
941
|
+
lines.push(BORDER("\u2501".repeat(Math.min(w, 68))));
|
|
942
|
+
lines.push(
|
|
943
|
+
DIM2(`[Agent: ${VIOLET.bold(this.header.model.name)} \u2014 ${this.header.model.role}]`)
|
|
944
|
+
);
|
|
945
|
+
lines.push(DIM2(HINT_LINE));
|
|
946
|
+
lines.push(BORDER("\u2501".repeat(Math.min(w, 68))));
|
|
947
|
+
return lines;
|
|
948
|
+
}
|
|
949
|
+
statusLine() {
|
|
950
|
+
const { kind, label, startedAt } = this.status;
|
|
951
|
+
if (kind === "busy") {
|
|
952
|
+
const elapsed = startedAt ? Math.floor((Date.now() - startedAt) / 1e3) : 0;
|
|
953
|
+
const frame = SPINNER_FRAMES[this.spinnerIdx];
|
|
954
|
+
const time = elapsed > 0 ? ` ${import_chalk3.default.gray(`${elapsed}s`)}` : "";
|
|
955
|
+
return `${VIOLET(frame)} ${import_chalk3.default.hex("#c084fc")(label)}${time}`;
|
|
854
956
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
957
|
+
if (kind === "info") {
|
|
958
|
+
return `${import_chalk3.default.green("\u2714")} ${import_chalk3.default.gray(label)}`;
|
|
959
|
+
}
|
|
960
|
+
return DIM2(`${this.header.model.alias} ready \u2014 awaiting instructions`);
|
|
961
|
+
}
|
|
962
|
+
inputBoxLines() {
|
|
963
|
+
const w = Math.min(this.width(), 80);
|
|
964
|
+
const inner = w - 4;
|
|
965
|
+
const prompt = `sarang(${this.header.model.alias})`;
|
|
966
|
+
const prefix = `${prompt}> `;
|
|
967
|
+
const effectiveInput = this.menu?.mode === "startup" ? this.input || "Select a model first..." : this.input;
|
|
968
|
+
const dimAll = this.menu?.mode === "startup";
|
|
969
|
+
const before = effectiveInput.slice(0, this.cursor);
|
|
970
|
+
const at = effectiveInput[this.cursor] ?? " ";
|
|
971
|
+
const after = effectiveInput.slice(this.cursor + 1);
|
|
972
|
+
const shown = (before + at + after).slice(0, inner - prefix.length - 1);
|
|
973
|
+
const bodyBase = import_chalk3.default.hex("#c084fc")(prefix) + shown + " ".repeat(Math.max(0, inner - prefix.length - visibleWidth(shown)));
|
|
974
|
+
const body = dimAll ? DIM2(bodyBase) : bodyBase;
|
|
975
|
+
return [
|
|
976
|
+
import_chalk3.default.hex("#5b21b6")(`\u256D${"\u2500".repeat(w - 2)}\u256E`),
|
|
977
|
+
import_chalk3.default.hex("#5b21b6")("\u2502 ") + body + import_chalk3.default.hex("#5b21b6")(" \u2502"),
|
|
978
|
+
import_chalk3.default.hex("#5b21b6")(`\u2570${"\u2500".repeat(w - 2)}\u256F`)
|
|
979
|
+
];
|
|
980
|
+
}
|
|
981
|
+
// ------------------------------------------------------------------ render
|
|
982
|
+
requestRender(force) {
|
|
983
|
+
if (!this.active || this.renderQueued) return;
|
|
984
|
+
if (!force) {
|
|
985
|
+
this.renderQueued = true;
|
|
986
|
+
setImmediate(() => {
|
|
987
|
+
this.renderQueued = false;
|
|
988
|
+
this.render();
|
|
989
|
+
});
|
|
990
|
+
return;
|
|
991
|
+
}
|
|
992
|
+
this.renderQueued = true;
|
|
993
|
+
setImmediate(() => {
|
|
994
|
+
this.renderQueued = false;
|
|
995
|
+
this.render();
|
|
996
|
+
});
|
|
997
|
+
}
|
|
998
|
+
render() {
|
|
999
|
+
if (!this.active) return;
|
|
1000
|
+
const w = this.width();
|
|
1001
|
+
const h = this.height();
|
|
1002
|
+
const header = this.headerLines();
|
|
1003
|
+
const status = [this.statusLine()];
|
|
1004
|
+
const modal = this.menu ? this.modelMenuLines() : [];
|
|
1005
|
+
const inputBox = this.inputBoxLines();
|
|
1006
|
+
const fixedRows = header.length + status.length + inputBox.length;
|
|
1007
|
+
const viewportHeight = Math.max(1, h - fixedRows);
|
|
1008
|
+
const start = Math.max(0, this.history.length - viewportHeight - this.scrollOffset);
|
|
1009
|
+
const visible = this.history.slice(start, start + viewportHeight);
|
|
1010
|
+
const parts = [];
|
|
1011
|
+
let row = 1;
|
|
1012
|
+
const emit = (text) => {
|
|
1013
|
+
parts.push(`\x1B[${row};1H\x1B[K${text}`);
|
|
1014
|
+
row += 1;
|
|
1015
|
+
};
|
|
1016
|
+
for (const line of header) emit(line);
|
|
1017
|
+
for (let i = 0; i < viewportHeight; i++) {
|
|
1018
|
+
const contentIdx = start + i;
|
|
1019
|
+
if (this.menu && i < modal.length) {
|
|
1020
|
+
emit(modal[i]);
|
|
1021
|
+
} else if (contentIdx < this.history.length) {
|
|
1022
|
+
emit(this.history[contentIdx] ?? "");
|
|
865
1023
|
} else {
|
|
866
|
-
|
|
1024
|
+
emit("");
|
|
867
1025
|
}
|
|
868
|
-
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
1026
|
+
}
|
|
1027
|
+
for (const line of status) emit(line);
|
|
1028
|
+
for (const line of inputBox) emit(line);
|
|
1029
|
+
if (row <= h) parts.push(`\x1B[${row};1H\x1B[J`);
|
|
1030
|
+
const frame = parts.join("");
|
|
1031
|
+
if (frame !== this.lastFullFrame) {
|
|
1032
|
+
this.out.write(frame);
|
|
1033
|
+
this.lastFullFrame = frame;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
// ------------------------------------------------------------------- input
|
|
1037
|
+
handleKey(chunk) {
|
|
1038
|
+
const key = chunk.toString("utf8");
|
|
1039
|
+
if (this.menu) {
|
|
1040
|
+
this.handleMenuKey(key);
|
|
872
1041
|
return;
|
|
873
1042
|
}
|
|
874
|
-
if (
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
if (key.ctrl && key.name === "c") {
|
|
878
|
-
cleanupAndExit();
|
|
1043
|
+
if (key === "") {
|
|
1044
|
+
this.close();
|
|
1045
|
+
this.onExit("ctrl-c");
|
|
879
1046
|
return;
|
|
880
1047
|
}
|
|
881
|
-
if (
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
if (str && !key.ctrl && !key.meta && !str.startsWith("\x1B")) {
|
|
888
|
-
keyBuffer += str;
|
|
889
|
-
}
|
|
890
|
-
return;
|
|
891
|
-
}
|
|
892
|
-
if (sessionScreen) {
|
|
893
|
-
if (key.name === "escape") {
|
|
894
|
-
leaveWorkspace();
|
|
895
|
-
return;
|
|
896
|
-
}
|
|
897
|
-
if (key.name === "up" || key.name === "down") {
|
|
898
|
-
scrollSession(key.name === "up" ? -3 : 3);
|
|
899
|
-
return;
|
|
900
|
-
}
|
|
901
|
-
if (key.name === "return") {
|
|
902
|
-
const query = currentTaskInput.trim();
|
|
903
|
-
if (!query) return;
|
|
904
|
-
if (query === "/exit" || query === "exit" || query === "quit" || query === ":q") {
|
|
905
|
-
leaveWorkspace();
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
|
-
if (query === "/model" || query === "/back") {
|
|
909
|
-
leaveWorkspace();
|
|
910
|
-
return;
|
|
911
|
-
}
|
|
912
|
-
if (query === "/new" || query === "/reset") {
|
|
913
|
-
conversationHistory = [];
|
|
914
|
-
respLastNotes = [];
|
|
915
|
-
enterWorkspace();
|
|
916
|
-
return;
|
|
917
|
-
}
|
|
918
|
-
conversationHistory.push({ role: "user", content: query });
|
|
919
|
-
runPromptStream(query);
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
if (key.name === "backspace") {
|
|
923
|
-
currentTaskInput = currentTaskInput.slice(0, -1);
|
|
924
|
-
redrawInputScreen();
|
|
925
|
-
return;
|
|
926
|
-
}
|
|
927
|
-
if (str && !key.ctrl && !key.meta && !str.startsWith("\x1B")) {
|
|
928
|
-
currentTaskInput += str;
|
|
929
|
-
redrawInputScreen();
|
|
930
|
-
return;
|
|
931
|
-
}
|
|
1048
|
+
if (key === "\r" || key === "\n") {
|
|
1049
|
+
const text = this.input.trim();
|
|
1050
|
+
this.input = "";
|
|
1051
|
+
this.cursor = 0;
|
|
1052
|
+
if (text.length === 0) {
|
|
1053
|
+
this.requestRender(false);
|
|
932
1054
|
return;
|
|
933
1055
|
}
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
1056
|
+
this.inputHistory.push(text);
|
|
1057
|
+
this.inputHistoryIdx = -1;
|
|
1058
|
+
this.appendLines([import_chalk3.default.hex("#7c3aed")(`\u276F ${text}`)]);
|
|
1059
|
+
this.onSubmit(text);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
if (key === "\x7F" || key === "\b") {
|
|
1063
|
+
if (this.cursor > 0) {
|
|
1064
|
+
this.input = this.input.slice(0, this.cursor - 1) + this.input.slice(this.cursor);
|
|
1065
|
+
this.cursor -= 1;
|
|
1066
|
+
this.requestRender(false);
|
|
937
1067
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
return;
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
if (key === "\x1B[A") {
|
|
1071
|
+
if (this.inputHistory.length === 0) return;
|
|
1072
|
+
if (this.inputHistoryIdx === -1) this.inputHistoryIdx = this.inputHistory.length - 1;
|
|
1073
|
+
else this.inputHistoryIdx = Math.max(0, this.inputHistoryIdx - 1);
|
|
1074
|
+
this.input = this.inputHistory[this.inputHistoryIdx] ?? "";
|
|
1075
|
+
this.cursor = this.input.length;
|
|
1076
|
+
this.requestRender(false);
|
|
1077
|
+
return;
|
|
1078
|
+
}
|
|
1079
|
+
if (key === "\x1B[B") {
|
|
1080
|
+
if (this.inputHistoryIdx === -1) return;
|
|
1081
|
+
this.inputHistoryIdx += 1;
|
|
1082
|
+
if (this.inputHistoryIdx >= this.inputHistory.length) {
|
|
1083
|
+
this.inputHistoryIdx = -1;
|
|
1084
|
+
this.input = "";
|
|
1085
|
+
} else {
|
|
1086
|
+
this.input = this.inputHistory[this.inputHistoryIdx] ?? "";
|
|
958
1087
|
}
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1088
|
+
this.cursor = this.input.length;
|
|
1089
|
+
this.requestRender(false);
|
|
1090
|
+
return;
|
|
1091
|
+
}
|
|
1092
|
+
if (key === "\x1B[C") {
|
|
1093
|
+
if (this.cursor < this.input.length) {
|
|
1094
|
+
this.cursor += 1;
|
|
1095
|
+
this.requestRender(false);
|
|
963
1096
|
}
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (key === "\x1B[D") {
|
|
1100
|
+
if (this.cursor > 0) {
|
|
1101
|
+
this.cursor -= 1;
|
|
1102
|
+
this.requestRender(false);
|
|
968
1103
|
}
|
|
969
1104
|
return;
|
|
970
1105
|
}
|
|
971
|
-
if (key
|
|
972
|
-
|
|
1106
|
+
if (key === "\x1B[H" || key === "\x1B[1~") {
|
|
1107
|
+
this.cursor = 0;
|
|
1108
|
+
this.requestRender(false);
|
|
973
1109
|
return;
|
|
974
1110
|
}
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
cursorIdx = (cursorIdx + 1) % 4;
|
|
979
|
-
} else {
|
|
980
|
-
if (cursorIdx >= 10 && cursorIdx < 10 + CODING_MODELS.length - 1) {
|
|
981
|
-
cursorIdx++;
|
|
982
|
-
} else if (cursorIdx === 10 + CODING_MODELS.length - 1) {
|
|
983
|
-
cursorIdx = 20;
|
|
984
|
-
} else {
|
|
985
|
-
cursorIdx = 10;
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
|
|
1111
|
+
if (key === "\x1B[F" || key === "\x1B[4~") {
|
|
1112
|
+
this.cursor = this.input.length;
|
|
1113
|
+
this.requestRender(false);
|
|
989
1114
|
return;
|
|
990
1115
|
}
|
|
991
|
-
if (key
|
|
992
|
-
if (
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
if (cursorIdx === 20) {
|
|
996
|
-
cursorIdx = 10 + CODING_MODELS.length - 1;
|
|
997
|
-
} else if (cursorIdx > 10) {
|
|
998
|
-
cursorIdx--;
|
|
999
|
-
} else {
|
|
1000
|
-
cursorIdx = 20;
|
|
1001
|
-
}
|
|
1116
|
+
if (key === "\x1B[3~") {
|
|
1117
|
+
if (this.cursor < this.input.length) {
|
|
1118
|
+
this.input = this.input.slice(0, this.cursor) + this.input.slice(this.cursor + 1);
|
|
1119
|
+
this.requestRender(false);
|
|
1002
1120
|
}
|
|
1003
|
-
drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
|
|
1004
1121
|
return;
|
|
1005
1122
|
}
|
|
1006
|
-
if (
|
|
1007
|
-
cursorIdx = 3;
|
|
1008
|
-
triggerCopyLink();
|
|
1123
|
+
if (key === "\x1B") {
|
|
1009
1124
|
return;
|
|
1010
1125
|
}
|
|
1011
|
-
if (key.
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1126
|
+
if (key.includes("\r") || key.includes("\n")) {
|
|
1127
|
+
const segments = key.split(/\r\n|\r|\n/);
|
|
1128
|
+
const remainder = segments.pop() ?? "";
|
|
1129
|
+
const linesToSubmit = [];
|
|
1130
|
+
for (let i = 0; i < segments.length; i++) {
|
|
1131
|
+
linesToSubmit.push((i === 0 ? this.input : "") + (segments[i] ?? ""));
|
|
1132
|
+
}
|
|
1133
|
+
this.input = remainder.replace(/[\r\n]/g, "");
|
|
1134
|
+
this.cursor = this.input.length;
|
|
1135
|
+
for (const line of linesToSubmit) {
|
|
1136
|
+
const text = line.trim();
|
|
1137
|
+
if (text.length === 0) continue;
|
|
1138
|
+
this.inputHistory.push(text);
|
|
1139
|
+
this.inputHistoryIdx = -1;
|
|
1140
|
+
this.appendLines([import_chalk3.default.hex("#7c3aed")(`\u276F ${text}`)]);
|
|
1141
|
+
this.onSubmit(text);
|
|
1142
|
+
}
|
|
1143
|
+
this.requestRender(false);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (!key.includes("\x1B") && key.length > 0) {
|
|
1147
|
+
this.input = this.input.slice(0, this.cursor) + key + this.input.slice(this.cursor);
|
|
1148
|
+
this.cursor += key.length;
|
|
1149
|
+
this.requestRender(false);
|
|
1150
|
+
}
|
|
1151
|
+
}
|
|
1152
|
+
};
|
|
1153
|
+
function createTui(opts, header) {
|
|
1154
|
+
if (!process.stdout.isTTY || !process.stdin.isTTY) return null;
|
|
1155
|
+
return new TerminalTui(opts, header);
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
// src/index.ts
|
|
1159
|
+
var program = new import_commander.Command();
|
|
1160
|
+
program.name("sarang").description("SarangAI Autonomous Coding Agent CLI").version("1.1.0");
|
|
1161
|
+
async function refreshStats() {
|
|
1162
|
+
try {
|
|
1163
|
+
const { stats } = await ensureAuthenticated();
|
|
1164
|
+
return stats;
|
|
1165
|
+
} catch {
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
function renderResultLines(result, model) {
|
|
1170
|
+
const lines = [];
|
|
1171
|
+
lines.push(import_chalk4.default.bold.hex("#a78bfa")(`\u2714 [${model.name}] Work Summary & Fixes:`));
|
|
1172
|
+
if (result.writtenFiles.length > 0) {
|
|
1173
|
+
lines.push("");
|
|
1174
|
+
lines.push(import_chalk4.default.bold.green("\u{1F4C1} Files Written/Updated:"));
|
|
1175
|
+
for (const f of result.writtenFiles) {
|
|
1176
|
+
const mark = f.success ? import_chalk4.default.green("\u2714") : import_chalk4.default.red("\u2716");
|
|
1177
|
+
lines.push(` ${mark} ${import_chalk4.default.white.bold(f.path)}`);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
if (result.editedFiles.length > 0) {
|
|
1181
|
+
lines.push("");
|
|
1182
|
+
lines.push(import_chalk4.default.bold.green("\u270F\uFE0F Files Edited (str_replace):"));
|
|
1183
|
+
for (const f of result.editedFiles) {
|
|
1184
|
+
const mark = f.success ? import_chalk4.default.green("\u2714") : import_chalk4.default.red("\u2716");
|
|
1185
|
+
lines.push(` ${mark} ${import_chalk4.default.white.bold(f.path)}`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (result.explanation) {
|
|
1189
|
+
lines.push("");
|
|
1190
|
+
lines.push(...result.explanation.split("\n"));
|
|
1191
|
+
}
|
|
1192
|
+
return lines;
|
|
1193
|
+
}
|
|
1194
|
+
function modelListLines(active) {
|
|
1195
|
+
const lines = [import_chalk4.default.yellow("Available Models (Locked):")];
|
|
1196
|
+
for (const m of Object.values(LOCKED_CODING_MODELS)) {
|
|
1197
|
+
const mark = m.alias === active.alias ? import_chalk4.default.green("\u25CF") : import_chalk4.default.gray("\u25CB");
|
|
1198
|
+
lines.push(` ${mark} ${import_chalk4.default.hex("#a78bfa")(m.alias.padEnd(10))} ${m.name.padEnd(22)} [${m.role}]`);
|
|
1199
|
+
}
|
|
1200
|
+
return lines;
|
|
1201
|
+
}
|
|
1202
|
+
function applyModel(session, next) {
|
|
1203
|
+
session.model = next;
|
|
1204
|
+
session.tui?.setHeader(session.stats, next);
|
|
1205
|
+
return import_chalk4.default.green(`\u2714 Switched to: ${next.name}`);
|
|
1206
|
+
}
|
|
1207
|
+
function exitCleanly() {
|
|
1208
|
+
console.log(import_chalk4.default.gray("\nGoodbye! Workspace closed.\n"));
|
|
1209
|
+
process.exit(0);
|
|
1210
|
+
}
|
|
1211
|
+
async function executeTask(session, input) {
|
|
1212
|
+
const { tui, model } = session;
|
|
1213
|
+
const startedAt = Date.now();
|
|
1214
|
+
session.busy = true;
|
|
1215
|
+
tui?.setStatus("busy", `[${model.name}] Analyzing requirements & designing solution...`, startedAt);
|
|
1216
|
+
try {
|
|
1217
|
+
const result = await runAutonomousAgent({
|
|
1218
|
+
model,
|
|
1219
|
+
userPrompt: input,
|
|
1220
|
+
history: session.history,
|
|
1221
|
+
onStatus: (label) => tui?.setStatus("busy", label, startedAt)
|
|
1222
|
+
});
|
|
1223
|
+
session.history.push({
|
|
1224
|
+
user: input,
|
|
1225
|
+
summary: summarizeResult(result.explanation, result.writtenFiles, result.editedFiles)
|
|
1226
|
+
});
|
|
1227
|
+
const lines = renderResultLines(result, model);
|
|
1228
|
+
if (tui) {
|
|
1229
|
+
tui.appendLines(lines);
|
|
1230
|
+
tui.setStatus("info", `[${model.name}] Done \u2014 ${result.writtenFiles.length} file(s) written`);
|
|
1231
|
+
setTimeout(() => {
|
|
1232
|
+
if (tui.isActive && !session.busy) tui.setStatus("idle", "");
|
|
1233
|
+
}, 2500);
|
|
1234
|
+
} else {
|
|
1235
|
+
console.log(lines.join("\n"));
|
|
1236
|
+
}
|
|
1237
|
+
const fresh = await refreshStats();
|
|
1238
|
+
if (fresh) {
|
|
1239
|
+
session.stats = fresh;
|
|
1240
|
+
tui?.setHeader(fresh, session.model);
|
|
1241
|
+
}
|
|
1242
|
+
} catch (err) {
|
|
1243
|
+
const msg = `\u2716 Error: ${err?.message ?? err}`;
|
|
1244
|
+
if (tui) {
|
|
1245
|
+
tui.appendLines([import_chalk4.default.red(msg)]);
|
|
1246
|
+
} else {
|
|
1247
|
+
console.error(import_chalk4.default.red(msg));
|
|
1248
|
+
}
|
|
1249
|
+
} finally {
|
|
1250
|
+
session.busy = false;
|
|
1251
|
+
tui?.setStatus("idle", "");
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
async function processInput(session, raw) {
|
|
1255
|
+
const input = raw.trim();
|
|
1256
|
+
if (!input) return "handled";
|
|
1257
|
+
const { tui } = session;
|
|
1258
|
+
const lower = input.toLowerCase();
|
|
1259
|
+
if (["exit", "quit", "/exit", "/quit"].includes(lower)) {
|
|
1260
|
+
tui?.close();
|
|
1261
|
+
exitCleanly();
|
|
1262
|
+
}
|
|
1263
|
+
if (input === "/clear") {
|
|
1264
|
+
tui?.clearContent();
|
|
1265
|
+
session.history = [];
|
|
1266
|
+
const cleared = import_chalk4.default.green("Session memory cleared.");
|
|
1267
|
+
if (tui) tui.appendLines([cleared]);
|
|
1268
|
+
else console.log(cleared);
|
|
1269
|
+
void refreshStats().then((fresh) => {
|
|
1270
|
+
if (fresh) {
|
|
1271
|
+
session.stats = fresh;
|
|
1272
|
+
tui?.setHeader(fresh, session.model);
|
|
1273
|
+
}
|
|
1274
|
+
});
|
|
1275
|
+
return "handled";
|
|
1276
|
+
}
|
|
1277
|
+
if (input.startsWith("/model") || input.startsWith("/switch")) {
|
|
1278
|
+
const parts = input.split(/\s+/);
|
|
1279
|
+
const arg = parts.length > 1 ? parts[1].trim() : "";
|
|
1280
|
+
if (arg) {
|
|
1281
|
+
const found = findModel(arg);
|
|
1282
|
+
if (!found) {
|
|
1283
|
+
const line = import_chalk4.default.red(
|
|
1284
|
+
`\u2716 Model '${arg}' not found. Available: ${MODEL_ALIAS_LIST}.`
|
|
1285
|
+
);
|
|
1286
|
+
if (tui) tui.appendLines([line]);
|
|
1287
|
+
else console.log(line);
|
|
1022
1288
|
} else {
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1289
|
+
const line = applyModel(session, found);
|
|
1290
|
+
if (tui) tui.appendLines([line]);
|
|
1291
|
+
else console.log(line);
|
|
1292
|
+
}
|
|
1293
|
+
} else if (tui) {
|
|
1294
|
+
tui.openModelMenu("session", session.model.alias);
|
|
1295
|
+
} else {
|
|
1296
|
+
const lines = modelListLines(session.model);
|
|
1297
|
+
console.log(lines.join("\n"));
|
|
1298
|
+
}
|
|
1299
|
+
return "handled";
|
|
1300
|
+
}
|
|
1301
|
+
if (session.busy) {
|
|
1302
|
+
session.queue.push(input);
|
|
1303
|
+
const line = import_chalk4.default.gray(`\u23F3 Added to queue (position ${session.queue.length}).`);
|
|
1304
|
+
if (tui) tui.appendLines([line]);
|
|
1305
|
+
else console.log(line);
|
|
1306
|
+
return "handled";
|
|
1307
|
+
}
|
|
1308
|
+
await executeTask(session, input);
|
|
1309
|
+
while (session.queue.length > 0) {
|
|
1310
|
+
const next = session.queue.shift();
|
|
1311
|
+
if (next) await executeTask(session, next);
|
|
1312
|
+
}
|
|
1313
|
+
return "handled";
|
|
1314
|
+
}
|
|
1315
|
+
async function startWorkspace(modelAlias = "glm", initialPrompt) {
|
|
1316
|
+
let stats = null;
|
|
1317
|
+
try {
|
|
1318
|
+
({ stats } = await ensureAuthenticated());
|
|
1319
|
+
} catch (err) {
|
|
1320
|
+
console.error(import_chalk4.default.red(`\u2716 ${err.message}`));
|
|
1321
|
+
process.exit(1);
|
|
1322
|
+
}
|
|
1323
|
+
const session = {
|
|
1324
|
+
tui: null,
|
|
1325
|
+
stats,
|
|
1326
|
+
model: resolveModel(modelAlias),
|
|
1327
|
+
busy: false,
|
|
1328
|
+
queue: [],
|
|
1329
|
+
history: []
|
|
1330
|
+
};
|
|
1331
|
+
const tui = createTui(
|
|
1332
|
+
{
|
|
1333
|
+
onSubmit: (text) => void processInput(session, text),
|
|
1334
|
+
onExit: () => exitCleanly(),
|
|
1335
|
+
onModelPick: (index) => {
|
|
1336
|
+
const models = Object.values(LOCKED_CODING_MODELS);
|
|
1337
|
+
const next = models[index];
|
|
1338
|
+
if (!next) return;
|
|
1339
|
+
const line = applyModel(session, next);
|
|
1340
|
+
if (tui.isStartupSelection) {
|
|
1341
|
+
tui.appendLines([
|
|
1342
|
+
import_chalk4.default.green(`\u2714 Active model: ${next.name} (${next.role})`)
|
|
1343
|
+
]);
|
|
1344
|
+
} else {
|
|
1345
|
+
tui.appendLines([line]);
|
|
1035
1346
|
}
|
|
1036
1347
|
}
|
|
1037
|
-
|
|
1348
|
+
},
|
|
1349
|
+
{ stats, model: session.model }
|
|
1350
|
+
);
|
|
1351
|
+
session.tui = tui;
|
|
1352
|
+
if (tui) {
|
|
1353
|
+
tui.start();
|
|
1354
|
+
const skipSelector = Boolean(initialPrompt?.trim()) || !process.stdin.isTTY;
|
|
1355
|
+
if (!skipSelector) {
|
|
1356
|
+
tui.openModelMenu("startup", session.model.alias);
|
|
1357
|
+
}
|
|
1358
|
+
if (initialPrompt && initialPrompt.trim()) {
|
|
1359
|
+
void processInput(session, initialPrompt.trim());
|
|
1360
|
+
}
|
|
1361
|
+
return;
|
|
1362
|
+
}
|
|
1363
|
+
const rl = import_readline.default.createInterface({ input: process.stdin, terminal: false });
|
|
1364
|
+
const banner = await Promise.resolve().then(() => (init_banner(), banner_exports));
|
|
1365
|
+
console.log(banner.bannerLines(stats).join("\n"));
|
|
1366
|
+
console.log(import_chalk4.default.gray(`[Agent: ${import_chalk4.default.bold.hex("#a78bfa")(session.model.name)} \u2014 ${session.model.role}]`));
|
|
1367
|
+
console.log(import_chalk4.default.gray("Enter your coding task. /model: switch model /clear: clear context exit: quit\n"));
|
|
1368
|
+
const pending = [];
|
|
1369
|
+
let draining = false;
|
|
1370
|
+
let inputClosed = false;
|
|
1371
|
+
const drain = async () => {
|
|
1372
|
+
if (draining) return;
|
|
1373
|
+
draining = true;
|
|
1374
|
+
while (pending.length > 0) {
|
|
1375
|
+
const line = pending.shift();
|
|
1376
|
+
if (!line) continue;
|
|
1377
|
+
await processInput(session, line);
|
|
1038
1378
|
}
|
|
1379
|
+
draining = false;
|
|
1380
|
+
if (inputClosed) exitCleanly();
|
|
1381
|
+
process.stdout.write(import_chalk4.default.bold.hex("#c084fc")(`sarang(${session.model.alias})> `));
|
|
1382
|
+
};
|
|
1383
|
+
rl.on("line", (line) => {
|
|
1384
|
+
pending.push(line.trim());
|
|
1385
|
+
void drain();
|
|
1386
|
+
});
|
|
1387
|
+
rl.on("close", () => {
|
|
1388
|
+
if (!draining && pending.length === 0 && !session.busy) exitCleanly();
|
|
1389
|
+
inputClosed = true;
|
|
1390
|
+
const wait = setInterval(() => {
|
|
1391
|
+
if (!draining && pending.length === 0 && !session.busy) {
|
|
1392
|
+
clearInterval(wait);
|
|
1393
|
+
exitCleanly();
|
|
1394
|
+
}
|
|
1395
|
+
}, 150);
|
|
1039
1396
|
});
|
|
1040
1397
|
}
|
|
1041
|
-
program.
|
|
1042
|
-
|
|
1398
|
+
program.command("run [prompt...]").alias("workspace").description("Mulai sesi autonomous coding (REPL interaktif)").option("-m, --model <alias>", "Model: glm | sonnet | luna | deepseek | mimo", "glm").action(async (promptParts, options) => {
|
|
1399
|
+
const initialPrompt = promptParts && promptParts.length > 0 ? promptParts.join(" ") : void 0;
|
|
1400
|
+
await startWorkspace(options.model, initialPrompt);
|
|
1043
1401
|
});
|
|
1044
|
-
program.
|
|
1045
|
-
|
|
1046
|
-
0 && (module.exports = {
|
|
1047
|
-
CODING_MODELS
|
|
1402
|
+
program.action(async () => {
|
|
1403
|
+
await startWorkspace("glm");
|
|
1048
1404
|
});
|
|
1405
|
+
program.parse(process.argv);
|