sarangai-cli 1.0.13 → 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 +1279 -854
- 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,953 +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
|
-
return
|
|
209
|
+
return null;
|
|
210
210
|
}
|
|
211
|
-
function
|
|
212
|
-
const
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
return
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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).");
|
|
221
|
+
}
|
|
222
|
+
return { token, stats };
|
|
223
|
+
}
|
|
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;
|
|
318
|
+
}
|
|
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
|
+
}
|
|
370
|
+
}
|
|
371
|
+
walk(dir, prefix);
|
|
372
|
+
return { tree, totalFiles };
|
|
373
|
+
}
|
|
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
|
+
}
|
|
389
|
+
}
|
|
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) {
|
|
234
431
|
return [
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
];
|
|
432
|
+
"",
|
|
433
|
+
`You are running as the ${model.name} profile (${model.id}).`,
|
|
434
|
+
`Specialization: ${model.role}. ${model.description}`
|
|
435
|
+
].join("\n");
|
|
240
436
|
}
|
|
241
|
-
function
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
"
|
|
247
|
-
"
|
|
248
|
-
"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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");
|
|
447
|
+
}
|
|
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");
|
|
459
|
+
}
|
|
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");
|
|
483
|
+
}
|
|
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)
|
|
284
491
|
});
|
|
285
|
-
lines.push("");
|
|
286
|
-
const balanceStr = `${Number(userMeta2.balance).toLocaleString()} Credits`;
|
|
287
|
-
lines.push(
|
|
288
|
-
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)
|
|
289
|
-
);
|
|
290
|
-
lines.push("");
|
|
291
|
-
const isFewerActive = cursorIdx2 === 20;
|
|
292
|
-
const fewerPrefix = isFewerActive ? import_chalk.default.cyan.bold("\u203A ") : " ";
|
|
293
|
-
const fewerText = isFewerActive ? import_chalk.default.bold.cyan.underline("\u2191 Show fewer") : import_chalk.default.hex("#a855f7")("\u2191 Show fewer");
|
|
294
|
-
lines.push(pad + fewerPrefix + fewerText);
|
|
295
492
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
493
|
+
return messages;
|
|
494
|
+
}
|
|
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);
|
|
299
500
|
}
|
|
300
|
-
|
|
301
|
-
lines.push(pad + import_chalk.default.gray("\u2500".repeat(cardWidth)));
|
|
302
|
-
lines.push(pad + import_chalk.default.gray("Navigasi: [\u2191/\u2193 Panah] \u2022 [Enter/Space] Pilih \u2022 [c] Copy Link \u2022 [q] Keluar"));
|
|
303
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H\x1B[?25l" + lines.join("\n"));
|
|
304
|
-
}
|
|
305
|
-
function drawWorkspaceScreen(model, timeLeftSec, userInput) {
|
|
306
|
-
const boxWidth = 72;
|
|
307
|
-
const pad = getCenterPad(boxWidth);
|
|
308
|
-
const banner = [
|
|
309
|
-
"\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",
|
|
310
|
-
"\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",
|
|
311
|
-
"\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",
|
|
312
|
-
"\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",
|
|
313
|
-
"\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",
|
|
314
|
-
"\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"
|
|
315
|
-
];
|
|
316
|
-
const lines = [];
|
|
317
|
-
banner.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
|
|
318
|
-
lines.push("");
|
|
319
|
-
lines.push(pad + import_chalk.default.white("SarangAI will run commands on your behalf to help you build."));
|
|
320
|
-
lines.push("");
|
|
321
|
-
lines.push(pad + import_chalk.default.bold.white("Directory ") + import_chalk.default.gray(getDisplayDir()));
|
|
322
|
-
lines.push("");
|
|
323
|
-
const m = Math.floor(timeLeftSec / 60);
|
|
324
|
-
const s = timeLeftSec % 60;
|
|
325
|
-
const timeFormatted = `${m}m ${s < 10 ? "0" : ""}${s}s left`;
|
|
326
|
-
const statusLeft = ` ${model.name} \u2022 ${timeFormatted}`;
|
|
327
|
-
const statusRight = `[Esc] End session `;
|
|
328
|
-
const spaceBetween = Math.max(2, boxWidth - statusLeft.length - statusRight.length);
|
|
329
|
-
const statusBar = import_chalk.default.bgHex("#1e293b").white.bold(statusLeft + " ".repeat(spaceBetween) + import_chalk.default.gray(statusRight));
|
|
330
|
-
lines.push(pad + statusBar);
|
|
331
|
-
const innerWidth = boxWidth - 2;
|
|
332
|
-
const borderTop = "\u250C" + "\u2500".repeat(innerWidth) + "\u2510";
|
|
333
|
-
const borderBottom = "\u2514" + "\u2500".repeat(innerWidth) + "\u2518";
|
|
334
|
-
const textContent = userInput ? userInput : import_chalk.default.gray("Enter a coding task or / for commands");
|
|
335
|
-
const visibleLen = userInput ? userInput.length : 37;
|
|
336
|
-
const paddingRight = Math.max(0, innerWidth - 2 - visibleLen);
|
|
337
|
-
const middleLine = `\u2502 ${textContent}${" ".repeat(paddingRight)}\u2502`;
|
|
338
|
-
lines.push(pad + import_chalk.default.gray(borderTop));
|
|
339
|
-
const inputRowNumber = lines.length + 1;
|
|
340
|
-
lines.push(pad + import_chalk.default.gray(middleLine));
|
|
341
|
-
lines.push(pad + import_chalk.default.gray(borderBottom));
|
|
342
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
|
|
343
|
-
const cursorCol = pad.length + 3 + userInput.length + 1;
|
|
344
|
-
process.stdout.write(`\x1B[${inputRowNumber};${cursorCol}H\x1B[?25h`);
|
|
345
|
-
}
|
|
346
|
-
var inWorkspace = false;
|
|
347
|
-
var isExecuting = false;
|
|
348
|
-
var expanded = false;
|
|
349
|
-
var cursorIdx = 0;
|
|
350
|
-
var copiedNotice = false;
|
|
351
|
-
var activeModel;
|
|
352
|
-
var userMeta;
|
|
353
|
-
var sessionTimeLeft = 3600;
|
|
354
|
-
var timerInterval = null;
|
|
355
|
-
var currentTaskInput = "";
|
|
356
|
-
var cfg;
|
|
357
|
-
var baseUrl;
|
|
358
|
-
var respQuery = "";
|
|
359
|
-
var respText = "";
|
|
360
|
-
var respError;
|
|
361
|
-
var respStreaming = false;
|
|
362
|
-
var respScroll = null;
|
|
363
|
-
var respTotalLines = 0;
|
|
364
|
-
var conversationHistory = [];
|
|
365
|
-
var sessionScreen = false;
|
|
366
|
-
var respLastNotes = [];
|
|
367
|
-
function cleanupAndExit() {
|
|
368
|
-
if (timerInterval) clearInterval(timerInterval);
|
|
369
|
-
process.stdout.write("\x1B[?1049l\x1B[?25h\n");
|
|
370
|
-
process.exit(0);
|
|
501
|
+
return slice;
|
|
371
502
|
}
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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);
|
|
398
533
|
}
|
|
534
|
+
if (res.length >= max) break;
|
|
399
535
|
}
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
return s.replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
536
|
+
} catch {
|
|
537
|
+
}
|
|
538
|
+
return res;
|
|
404
539
|
}
|
|
405
|
-
function
|
|
406
|
-
const
|
|
407
|
-
|
|
408
|
-
const
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
+
);
|
|
423
560
|
}
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
+
};
|
|
570
|
+
}
|
|
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```$/, "");
|
|
430
580
|
}
|
|
431
|
-
|
|
581
|
+
const writeRes = safeWriteFile(filePath, content);
|
|
582
|
+
results.push({ path: filePath, success: writeRes.success });
|
|
432
583
|
}
|
|
433
|
-
return
|
|
584
|
+
return results;
|
|
434
585
|
}
|
|
435
|
-
function
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
typescript: "ts",
|
|
447
|
-
ts: "ts",
|
|
448
|
-
tsx: "tsx",
|
|
449
|
-
jsx: "jsx",
|
|
450
|
-
go: "go",
|
|
451
|
-
rust: "rs",
|
|
452
|
-
rs: "rs",
|
|
453
|
-
java: "java",
|
|
454
|
-
c: "c",
|
|
455
|
-
cpp: "cpp",
|
|
456
|
-
"c++": "cpp",
|
|
457
|
-
csharp: "cs",
|
|
458
|
-
"c#": "cs",
|
|
459
|
-
cs: "cs",
|
|
460
|
-
php: "php",
|
|
461
|
-
ruby: "rb",
|
|
462
|
-
rb: "rb",
|
|
463
|
-
swift: "swift",
|
|
464
|
-
kotlin: "kt",
|
|
465
|
-
kt: "kt",
|
|
466
|
-
bash: "sh",
|
|
467
|
-
sh: "sh",
|
|
468
|
-
shell: "sh",
|
|
469
|
-
zsh: "sh",
|
|
470
|
-
powershell: "ps1",
|
|
471
|
-
ps1: "ps1",
|
|
472
|
-
html: "html",
|
|
473
|
-
css: "css",
|
|
474
|
-
scss: "scss",
|
|
475
|
-
sass: "sass",
|
|
476
|
-
less: "less",
|
|
477
|
-
sql: "sql",
|
|
478
|
-
json: "json",
|
|
479
|
-
yaml: "yaml",
|
|
480
|
-
yml: "yml",
|
|
481
|
-
toml: "toml",
|
|
482
|
-
xml: "xml",
|
|
483
|
-
markdown: "md",
|
|
484
|
-
md: "md",
|
|
485
|
-
dockerfile: "",
|
|
486
|
-
makefile: "",
|
|
487
|
-
ini: "ini",
|
|
488
|
-
env: "",
|
|
489
|
-
txt: "txt",
|
|
490
|
-
text: "txt",
|
|
491
|
-
csv: "csv"
|
|
492
|
-
};
|
|
493
|
-
function extractFilesFromMarkdown(text) {
|
|
494
|
-
const files = [];
|
|
495
|
-
const fence = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]*)(?::[ \t]*([^\n`]+))?[^\n]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
|
|
496
|
-
let m;
|
|
497
|
-
while ((m = fence.exec(text)) !== null) {
|
|
498
|
-
const lang = (m[1] || "").toLowerCase().trim();
|
|
499
|
-
const fname = (m[2] || "").trim().replace(/^["']|["']$/g, "");
|
|
500
|
-
if (!fname) continue;
|
|
501
|
-
files.push({ filename: fname, content: m[3], lang: lang || "text" });
|
|
502
|
-
}
|
|
503
|
-
return files;
|
|
504
|
-
}
|
|
505
|
-
function sanitizeFilename(name) {
|
|
506
|
-
const base = import_path2.default.basename(name.trim());
|
|
507
|
-
if (!base || base === "." || base === "..") return null;
|
|
508
|
-
if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(base)) return null;
|
|
509
|
-
if (base.includes("..")) return null;
|
|
510
|
-
return base;
|
|
511
|
-
}
|
|
512
|
-
function defaultFilename(lang) {
|
|
513
|
-
if (lang === "dockerfile") return "Dockerfile";
|
|
514
|
-
if (lang === "makefile") return "Makefile";
|
|
515
|
-
const ext = EXT_BY_LANG[lang];
|
|
516
|
-
return ext === void 0 ? null : `snippet.${ext || "txt"}`;
|
|
517
|
-
}
|
|
518
|
-
function extractAllFiles(text) {
|
|
519
|
-
const extracted = extractFilesFromMarkdown(text);
|
|
520
|
-
if (extracted.length) return extracted;
|
|
521
|
-
const fence2 = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]+)[ \t]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
|
|
522
|
-
let best = null;
|
|
523
|
-
let m;
|
|
524
|
-
while ((m = fence2.exec(text)) !== null) {
|
|
525
|
-
const lang = (m[1] || "").toLowerCase();
|
|
526
|
-
if (!EXT_BY_LANG.hasOwnProperty(lang) || lang === "text") continue;
|
|
527
|
-
const content = m[2];
|
|
528
|
-
if (content.trim().split("\n").length < 2) continue;
|
|
529
|
-
if (!best || content.length > best.content.length) {
|
|
530
|
-
best = { filename: defaultFilename(lang) || "snippet.txt", content, lang };
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
return best ? [best] : [];
|
|
534
|
-
}
|
|
535
|
-
function writeExtractedFiles(files) {
|
|
536
|
-
const notes = [];
|
|
537
|
-
for (const f of files) {
|
|
538
|
-
const safe = sanitizeFilename(f.filename);
|
|
539
|
-
if (!safe) {
|
|
540
|
-
notes.push(import_chalk.default.red(`\u2715 Skip nama file tidak valid: ${f.filename}`));
|
|
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 });
|
|
541
597
|
continue;
|
|
542
598
|
}
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
return notes;
|
|
553
|
-
}
|
|
554
|
-
var AGENT_SYSTEM_PROMPT = [
|
|
555
|
-
"Anda adalah asisten coding agent SarangAI yang berjalan di terminal pengguna.",
|
|
556
|
-
`Direktori kerja aktif: ${process.cwd()}.`,
|
|
557
|
-
"Jika permintaan pengguna melibatkan pembuatan atau perubahan file, SELALU output kode dalam fenced code block dengan format ````bahasa:nama-file` pada baris pembuka, contoh: ```python:binary_search.py",
|
|
558
|
-
"Gunakan SATU file per code block dan jangan menambah teks setelah baris penutup ``` milik block tersebut.",
|
|
559
|
-
"Untuk file konfigurasi tanpa ekstensi gunakan ```dockerfile:Dockerfile atau ```makefile:Makefile.",
|
|
560
|
-
"Jelaskan secara singkat sebelum/sesudah code block, tanpa membungkus nama file di backtick atau bold."
|
|
561
|
-
].join("\n");
|
|
562
|
-
var lastTurn = { query: "", text: "", streaming: false, notes: [] };
|
|
563
|
-
var activeAbort = null;
|
|
564
|
-
function drawSessionScreen(turn, opts = { inputMode: "idle" }) {
|
|
565
|
-
const boxWidth = 72;
|
|
566
|
-
const termRows = process.stdout.rows || 30;
|
|
567
|
-
const pad = getCenterPad(boxWidth);
|
|
568
|
-
const innerW = boxWidth - 2;
|
|
569
|
-
const contentW = innerW - 4;
|
|
570
|
-
const lines = [];
|
|
571
|
-
SARANG_BANNER.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
|
|
572
|
-
lines.push("");
|
|
573
|
-
const mm = Math.floor(sessionTimeLeft / 60);
|
|
574
|
-
const ss = sessionTimeLeft % 60;
|
|
575
|
-
lines.push(pad + import_chalk.default.gray(`${conversationHistory.length} pesan \u2022 ${activeModel.name} \u2022 ${mm}m ${ss < 10 ? "0" : ""}${ss}s left`));
|
|
576
|
-
lines.push("");
|
|
577
|
-
const qLabel = turn.query.length > 64 ? turn.query.slice(0, 61) + "\u2026" : turn.query;
|
|
578
|
-
lines.push(pad + import_chalk.default.bold.hex("#c084fc")(`\u203A Task: ${qLabel}`));
|
|
579
|
-
const headerRows = lines.length + 1;
|
|
580
|
-
const footerRows = opts.inputMode === "typing" ? 7 : 3;
|
|
581
|
-
const viewRows = Math.max(4, termRows - headerRows - footerRows);
|
|
582
|
-
const body = turn.error ? wrapWithStyles(turn.error, contentW).map((l) => ({ ...l, style: "error" })) : wrapWithStyles(turn.text, contentW);
|
|
583
|
-
respTotalLines = body.length;
|
|
584
|
-
const visible = turn.streaming ? body.slice(0, viewRows) : body.slice(Math.max(0, body.length - viewRows));
|
|
585
|
-
lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
|
|
586
|
-
for (const l of visible) {
|
|
587
|
-
const styled = styleBodyLine(l);
|
|
588
|
-
const fill = Math.max(0, contentW - visibleLength(styled));
|
|
589
|
-
lines.push(pad + import_chalk.default.gray("\u2502 ") + styled + " ".repeat(fill) + import_chalk.default.gray(" \u2502"));
|
|
590
|
-
}
|
|
591
|
-
for (let i = visible.length; i < viewRows; i++) {
|
|
592
|
-
lines.push(pad + import_chalk.default.gray("\u2502 ") + " ".repeat(contentW) + import_chalk.default.gray(" \u2502"));
|
|
593
|
-
}
|
|
594
|
-
lines.push(pad + import_chalk.default.gray("\u2514" + "\u2500".repeat(innerW) + "\u2518"));
|
|
595
|
-
for (const note of turn.notes) lines.push(pad + " " + note);
|
|
596
|
-
if (opts.inputMode === "typing") {
|
|
597
|
-
lines.push("");
|
|
598
|
-
const textContent = currentTaskInput ? currentTaskInput : import_chalk.default.gray("Ketik follow-up\u2026 (/new = sesi baru, exit = keluar)");
|
|
599
|
-
const visibleLen = currentTaskInput ? currentTaskInput.length : 55;
|
|
600
|
-
const paddingRight = Math.max(0, innerW - 2 - visibleLen);
|
|
601
|
-
lines.push(pad + import_chalk.default.gray("\u250C" + "\u2500".repeat(innerW) + "\u2510"));
|
|
602
|
-
const inputRowNumber = lines.length + 1;
|
|
603
|
-
lines.push(pad + import_chalk.default.gray(`\u2502 ${textContent}${" ".repeat(paddingRight)}\u2502`));
|
|
604
|
-
lines.push(pad + import_chalk.default.gray("\u2514" + "\u2500".repeat(innerW) + "\u2518"));
|
|
605
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
|
|
606
|
-
const cursorCol = pad.length + 5 + currentTaskInput.length;
|
|
607
|
-
process.stdout.write(`\x1B[${inputRowNumber};${cursorCol}H\x1B[?25h`);
|
|
608
|
-
} else {
|
|
609
|
-
lines.push("");
|
|
610
|
-
const status = turn.streaming ? import_chalk.default.gray(`[Streaming \u2022 ${activeModel.name} \u2022 ${respTotalLines} baris] Esc = batalkan`) : turn.error ? import_chalk.default.red("\u2715 Error") + import_chalk.default.gray(" \u2022 tekan tombol untuk lanjut") : import_chalk.default.green("\u2714 Selesai") + import_chalk.default.gray(` \u2022 ${respTotalLines} baris \u2022 ketik follow-up di bawah \u2022 Esc = menu`);
|
|
611
|
-
lines.push(pad + status);
|
|
612
|
-
process.stdout.write("\x1B[2J\x1B[3J\x1B[H" + lines.join("\n"));
|
|
599
|
+
const current = existing.content;
|
|
600
|
+
if (!current.includes(find)) {
|
|
601
|
+
results.push({ path: filePath, success: false });
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const updated = current.replace(find, () => replaceWith);
|
|
605
|
+
const writeRes = safeWriteFile(filePath, updated);
|
|
606
|
+
results.push({ path: filePath, success: writeRes.success });
|
|
613
607
|
}
|
|
608
|
+
return results;
|
|
614
609
|
}
|
|
615
|
-
function
|
|
616
|
-
|
|
617
|
-
sessionScreen = true;
|
|
618
|
-
drawSessionScreen(lastTurn, { inputMode: "typing" });
|
|
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();
|
|
619
612
|
}
|
|
620
|
-
function
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
}
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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
|
+
}
|
|
629
637
|
}
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
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 };
|
|
644
|
+
}
|
|
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);
|
|
658
|
+
}
|
|
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 };
|
|
663
|
+
}
|
|
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" };
|
|
634
682
|
}
|
|
635
|
-
|
|
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" };
|
|
693
|
+
}
|
|
694
|
+
}
|
|
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;
|
|
702
|
+
}
|
|
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));
|
|
636
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;
|
|
637
721
|
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
...conversationHistory
|
|
670
|
-
],
|
|
671
|
-
stream: true
|
|
672
|
-
})
|
|
673
|
-
});
|
|
674
|
-
if (!res.ok) {
|
|
675
|
-
let detail = res.statusText || `HTTP ${res.status}`;
|
|
676
|
-
try {
|
|
677
|
-
const errBody = await res.json();
|
|
678
|
-
const msg = errBody?.error?.message || errBody?.error || errBody?.message;
|
|
679
|
-
if (msg) detail = typeof msg === "string" ? msg : JSON.stringify(msg);
|
|
680
|
-
} catch {
|
|
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];
|
|
741
|
+
const out = [];
|
|
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;
|
|
681
753
|
}
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
try {
|
|
688
|
-
saveConfig({ apiKey: void 0 });
|
|
689
|
-
} catch {
|
|
690
|
-
}
|
|
691
|
-
cfg = getConfig();
|
|
692
|
-
const newKey = await handleAutoAuth(baseUrl);
|
|
693
|
-
if (newKey) {
|
|
694
|
-
cfg = getConfig();
|
|
695
|
-
userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "");
|
|
696
|
-
return runPromptStream(query, true);
|
|
697
|
-
}
|
|
698
|
-
respError = "Otorisasi ulang dibatalkan.";
|
|
699
|
-
repaint(false, true);
|
|
754
|
+
const chunk = rest[0];
|
|
755
|
+
if (currentWidth + 1 > width) {
|
|
756
|
+
out.push(current + (pendingAnsi ? "" : ""));
|
|
757
|
+
current = pendingAnsi;
|
|
758
|
+
currentWidth = 0;
|
|
700
759
|
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
if (reader) {
|
|
705
|
-
let sseBuffer = "";
|
|
706
|
-
while (true) {
|
|
707
|
-
const { done, value } = await reader.read();
|
|
708
|
-
if (done) break;
|
|
709
|
-
sseBuffer += decoder.decode(value, { stream: true });
|
|
710
|
-
const nlIdx = sseBuffer.lastIndexOf("\n");
|
|
711
|
-
if (nlIdx === -1) continue;
|
|
712
|
-
const complete = sseBuffer.slice(0, nlIdx);
|
|
713
|
-
sseBuffer = sseBuffer.slice(nlIdx + 1);
|
|
714
|
-
for (const l of complete.split("\n")) {
|
|
715
|
-
if (l.startsWith("data: ") && l !== "data: [DONE]") {
|
|
716
|
-
try {
|
|
717
|
-
const p = JSON.parse(l.slice(6));
|
|
718
|
-
const delta = p.choices?.[0]?.delta?.content;
|
|
719
|
-
if (delta) respText += delta;
|
|
720
|
-
} catch {
|
|
721
|
-
}
|
|
722
|
-
}
|
|
723
|
-
}
|
|
724
|
-
repaint(true);
|
|
725
|
-
}
|
|
726
|
-
repaint(false, true);
|
|
760
|
+
if (chunk === " " && currentWidth === 0) {
|
|
761
|
+
rest = rest.slice(1);
|
|
762
|
+
continue;
|
|
727
763
|
}
|
|
764
|
+
current += pendingAnsi + chunk;
|
|
765
|
+
pendingAnsi = "";
|
|
766
|
+
currentWidth += 1;
|
|
767
|
+
rest = rest.slice(1);
|
|
728
768
|
}
|
|
729
|
-
}
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
769
|
+
}
|
|
770
|
+
if (current.length > 0 || out.length === 0) out.push(current);
|
|
771
|
+
return out;
|
|
772
|
+
}
|
|
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);
|
|
810
|
+
}
|
|
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);
|
|
819
|
+
}
|
|
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");
|
|
828
|
+
}
|
|
829
|
+
// ------------------------------------------------------------- public state
|
|
830
|
+
setHeader(stats, model) {
|
|
831
|
+
this.header = { stats, model };
|
|
832
|
+
this.requestRender(true);
|
|
833
|
+
}
|
|
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);
|
|
838
|
+
}
|
|
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();
|
|
733
849
|
} else {
|
|
734
|
-
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
850
|
+
this.stopTick();
|
|
851
|
+
}
|
|
852
|
+
this.requestRender(false);
|
|
853
|
+
}
|
|
854
|
+
clearContent() {
|
|
855
|
+
this.history = [];
|
|
856
|
+
this.scrollOffset = 0;
|
|
857
|
+
this.requestRender(true);
|
|
858
|
+
}
|
|
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);
|
|
869
|
+
}
|
|
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;
|
|
773
895
|
}
|
|
774
|
-
|
|
896
|
+
case "cancel":
|
|
897
|
+
this.closeModelMenu();
|
|
898
|
+
break;
|
|
899
|
+
case "abort":
|
|
900
|
+
this.close();
|
|
901
|
+
abortStartupSelection();
|
|
902
|
+
break;
|
|
903
|
+
case "ignore":
|
|
904
|
+
break;
|
|
775
905
|
}
|
|
776
906
|
}
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
}
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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);
|
|
922
|
+
}
|
|
923
|
+
stopTick() {
|
|
924
|
+
if (this.tickTimer) {
|
|
925
|
+
clearInterval(this.tickTimer);
|
|
926
|
+
this.tickTimer = null;
|
|
927
|
+
}
|
|
928
|
+
}
|
|
929
|
+
// ------------------------------------------------------------------ layout
|
|
930
|
+
width() {
|
|
931
|
+
return Math.max(20, this.out.columns || 80);
|
|
932
|
+
}
|
|
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}`;
|
|
956
|
+
}
|
|
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] ?? "");
|
|
809
1023
|
} else {
|
|
810
|
-
|
|
1024
|
+
emit("");
|
|
811
1025
|
}
|
|
812
|
-
} else {
|
|
813
|
-
drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
|
|
814
1026
|
}
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
if (
|
|
818
|
-
|
|
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);
|
|
819
1041
|
return;
|
|
820
1042
|
}
|
|
821
|
-
if (
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
}
|
|
833
|
-
if (key.name === "return") {
|
|
834
|
-
const query = currentTaskInput.trim();
|
|
835
|
-
if (!query) return;
|
|
836
|
-
if (query === "/exit" || query === "exit" || query === "quit" || query === ":q") {
|
|
837
|
-
leaveWorkspace();
|
|
838
|
-
return;
|
|
839
|
-
}
|
|
840
|
-
if (query === "/model" || query === "/back") {
|
|
841
|
-
leaveWorkspace();
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
|
-
if (query === "/new" || query === "/reset") {
|
|
845
|
-
conversationHistory = [];
|
|
846
|
-
respLastNotes = [];
|
|
847
|
-
enterWorkspace();
|
|
848
|
-
return;
|
|
849
|
-
}
|
|
850
|
-
conversationHistory.push({ role: "user", content: query });
|
|
851
|
-
runPromptStream(query);
|
|
852
|
-
return;
|
|
853
|
-
}
|
|
854
|
-
if (key.name === "backspace") {
|
|
855
|
-
currentTaskInput = currentTaskInput.slice(0, -1);
|
|
856
|
-
redrawInputScreen();
|
|
857
|
-
return;
|
|
858
|
-
}
|
|
859
|
-
if (str && !key.ctrl && !key.meta && !str.startsWith("\x1B")) {
|
|
860
|
-
currentTaskInput += str;
|
|
861
|
-
redrawInputScreen();
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
1043
|
+
if (key === "") {
|
|
1044
|
+
this.close();
|
|
1045
|
+
this.onExit("ctrl-c");
|
|
1046
|
+
return;
|
|
1047
|
+
}
|
|
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);
|
|
864
1054
|
return;
|
|
865
1055
|
}
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
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);
|
|
869
1067
|
}
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
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] ?? "";
|
|
890
1087
|
}
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
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);
|
|
895
1096
|
}
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (key === "\x1B[D") {
|
|
1100
|
+
if (this.cursor > 0) {
|
|
1101
|
+
this.cursor -= 1;
|
|
1102
|
+
this.requestRender(false);
|
|
900
1103
|
}
|
|
901
1104
|
return;
|
|
902
1105
|
}
|
|
903
|
-
if (key
|
|
904
|
-
|
|
1106
|
+
if (key === "\x1B[H" || key === "\x1B[1~") {
|
|
1107
|
+
this.cursor = 0;
|
|
1108
|
+
this.requestRender(false);
|
|
905
1109
|
return;
|
|
906
1110
|
}
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
cursorIdx = (cursorIdx + 1) % 4;
|
|
911
|
-
} else {
|
|
912
|
-
if (cursorIdx >= 10 && cursorIdx < 10 + CODING_MODELS.length - 1) {
|
|
913
|
-
cursorIdx++;
|
|
914
|
-
} else if (cursorIdx === 10 + CODING_MODELS.length - 1) {
|
|
915
|
-
cursorIdx = 20;
|
|
916
|
-
} else {
|
|
917
|
-
cursorIdx = 10;
|
|
918
|
-
}
|
|
919
|
-
}
|
|
920
|
-
drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
|
|
1111
|
+
if (key === "\x1B[F" || key === "\x1B[4~") {
|
|
1112
|
+
this.cursor = this.input.length;
|
|
1113
|
+
this.requestRender(false);
|
|
921
1114
|
return;
|
|
922
1115
|
}
|
|
923
|
-
if (key
|
|
924
|
-
if (
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
if (cursorIdx === 20) {
|
|
928
|
-
cursorIdx = 10 + CODING_MODELS.length - 1;
|
|
929
|
-
} else if (cursorIdx > 10) {
|
|
930
|
-
cursorIdx--;
|
|
931
|
-
} else {
|
|
932
|
-
cursorIdx = 20;
|
|
933
|
-
}
|
|
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);
|
|
934
1120
|
}
|
|
935
|
-
drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
|
|
936
1121
|
return;
|
|
937
1122
|
}
|
|
938
|
-
if (
|
|
939
|
-
cursorIdx = 3;
|
|
940
|
-
triggerCopyLink();
|
|
1123
|
+
if (key === "\x1B") {
|
|
941
1124
|
return;
|
|
942
1125
|
}
|
|
943
|
-
if (key.
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
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);
|
|
954
1288
|
} else {
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
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]);
|
|
967
1346
|
}
|
|
968
1347
|
}
|
|
969
|
-
|
|
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);
|
|
970
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);
|
|
971
1396
|
});
|
|
972
1397
|
}
|
|
973
|
-
program.
|
|
974
|
-
|
|
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);
|
|
975
1401
|
});
|
|
976
|
-
program.
|
|
977
|
-
|
|
978
|
-
0 && (module.exports = {
|
|
979
|
-
CODING_MODELS
|
|
1402
|
+
program.action(async () => {
|
|
1403
|
+
await startWorkspace("glm");
|
|
980
1404
|
});
|
|
1405
|
+
program.parse(process.argv);
|