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.
Files changed (3) hide show
  1. package/README.md +127 -43
  2. package/dist/index.js +1254 -897
  3. 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 __commonJS = (cb, mod) => function __require() {
9
- return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
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
- // package.json
34
- var require_package = __commonJS({
35
- "package.json"(exports2, module2) {
36
- module2.exports = {
37
- name: "sarangai-cli",
38
- version: "1.0.14",
39
- description: "CLI resmi SarangAI \u2014 Gateway multi-model AI langsung dari terminal Anda",
40
- main: "dist/index.js",
41
- bin: {
42
- sarang: "dist/index.js",
43
- sarangai: "dist/index.js"
44
- },
45
- files: [
46
- "dist"
47
- ],
48
- scripts: {
49
- build: "tsup && chmod +x dist/index.js",
50
- prepublishOnly: "npm run build"
51
- },
52
- keywords: [
53
- "sarangai",
54
- "cli",
55
- "ai",
56
- "llm",
57
- "openai",
58
- "deepseek",
59
- "claude",
60
- "gateway"
61
- ],
62
- author: "SarangAI Team",
63
- license: "MIT",
64
- dependencies: {
65
- chalk: "^4.1.2",
66
- clipboardy: "^5.3.2",
67
- commander: "^12.1.0",
68
- ink: "^6.8.0",
69
- ora: "^5.4.1",
70
- prompts: "^2.4.2",
71
- react: "^19.3.0",
72
- "react-dom": "^19.3.0",
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 import_chalk = __toESM(require("chalk"));
77
+ var import_chalk4 = __toESM(require("chalk"));
96
78
  var import_readline = __toESM(require("readline"));
97
- var import_child_process = require("child_process");
98
- var import_crypto = __toESM(require("crypto"));
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 import_clipboardy = __toESM(require("clipboardy"));
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
- return JSON.parse(raw);
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/index.ts
128
- var program = new import_commander.Command();
129
- var SARANG_BANNER = [
130
- "\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",
131
- "\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",
132
- "\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",
133
- "\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",
134
- "\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",
135
- "\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"
136
- ];
137
- var borderActive = import_chalk.default.hex("#a855f7");
138
- var borderMuted = import_chalk.default.hex("#334155");
139
- var violetText = import_chalk.default.hex("#c084fc");
140
- var greenDot = import_chalk.default.hex("#4ade80");
141
- var grayMuted = import_chalk.default.hex("#94a3b8");
142
- var CODING_MODELS = [
143
- {
144
- id: "anthropic/claude-sonnet-4.6",
145
- name: "Claude Sonnet 4.6",
146
- desc: "Anthropic \u2022 Best coding & agentic workflow",
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(baseUrl2, apiKey) {
189
+ async function fetchUserMeta(baseUrl, token) {
195
190
  try {
196
- const res = await fetch(`${baseUrl2}/api/gateway/v1/balance`, {
197
- headers: { Authorization: `Bearer ${apiKey}` }
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 ?? 0,
202
+ balance: typeof d.balance === "number" ? Math.floor(d.balance) : parseInt(d.balance, 10) || 0,
203
203
  tier: d.tier || "DEVELOPER",
204
- accountId: d.accountId ? `SA-${d.accountId}` : "SA-\u2014"
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 refreshUserMeta() {
212
- const fresh = await fetchUserMeta(baseUrl, cfg?.apiKey || "");
213
- if (!fresh) return;
214
- const changed = !userMeta || fresh.balance !== userMeta.balance || fresh.tier !== userMeta.tier || fresh.accountId !== userMeta.accountId;
215
- userMeta = fresh;
216
- if (!changed) return;
217
- if (inWorkspace) {
218
- if (isExecuting) drawSessionScreen(lastTurn);
219
- else if (sessionScreen) drawSessionScreen(lastTurn, { inputMode: "typing" });
220
- else drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
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
- function redrawWithFreshBalance() {
226
- drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
227
- refreshUserMeta();
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
- function getDisplayDir() {
230
- const cwd = process.cwd();
231
- const home = process.env.HOME || "/root";
232
- if (cwd.startsWith(home)) {
233
- return "~" + cwd.slice(home.length);
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
- return cwd;
371
+ walk(dir, prefix);
372
+ return { tree, totalFiles };
236
373
  }
237
- function getCenterPad(contentWidth = 72) {
238
- const termCols = process.stdout.columns || 80;
239
- const padLen = Math.max(2, Math.floor((termCols - contentWidth) / 2));
240
- return " ".repeat(padLen);
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 renderCard(model, isFocused, cardWidth, pad) {
243
- const borderFn = isFocused ? borderActive : borderMuted;
244
- const prefix = isFocused ? import_chalk.default.cyan.bold("\u203A ") : " ";
245
- const innerW = cardWidth - 2;
246
- const rawL1 = ` ${model.name} \u2022 ${model.desc}`;
247
- const padL1 = Math.max(0, innerW - rawL1.length);
248
- const line1 = `${prefix}${import_chalk.default.bold.white(model.name)} \u2022 ${grayMuted(model.desc)}${" ".repeat(padL1)}`;
249
- const rawL2 = ` ${model.rate}`;
250
- const padL2 = Math.max(0, innerW - rawL2.length);
251
- const line2 = ` ${violetText.bold(model.rate)}${" ".repeat(padL2)}`;
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
- pad + borderFn("\u250C" + "\u2500".repeat(innerW) + "\u2510"),
254
- pad + borderFn("\u2502") + line1 + borderFn("\u2502"),
255
- pad + borderFn("\u2502") + line2 + borderFn("\u2502"),
256
- pad + borderFn("\u2514" + "\u2500".repeat(innerW) + "\u2518")
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 drawSelectionModal(activeModel2, userMeta2, expanded2, cursorIdx2, copiedNotice2 = false) {
260
- const cardWidth = 72;
261
- const pad = getCenterPad(cardWidth);
262
- const lines = [];
263
- const banner = [
264
- "\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",
265
- "\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",
266
- "\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",
267
- "\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",
268
- "\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",
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 drawWorkspaceScreen(model, timeLeftSec, userInput) {
324
- const boxWidth = 72;
325
- const pad = getCenterPad(boxWidth);
326
- const banner = [
327
- "\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",
328
- "\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",
329
- "\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",
330
- "\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",
331
- "\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",
332
- "\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"
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
- var inWorkspace = false;
365
- var isExecuting = false;
366
- var expanded = false;
367
- var cursorIdx = 0;
368
- var copiedNotice = false;
369
- var activeModel;
370
- var userMeta = null;
371
- var sessionTimeLeft = 3600;
372
- var timerInterval = null;
373
- var currentTaskInput = "";
374
- var cfg;
375
- var baseUrl;
376
- var respQuery = "";
377
- var respText = "";
378
- var respError;
379
- var respStreaming = false;
380
- var respScroll = null;
381
- var respTotalLines = 0;
382
- var respViewRows = 12;
383
- var conversationHistory = [];
384
- var sessionScreen = false;
385
- var respLastNotes = [];
386
- var keyBuffer = "";
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 triggerCopyLink() {
393
- copyToClipboard("https://idshop.or.id/user/dashboard");
394
- copiedNotice = true;
395
- drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
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 leaveWorkspace() {
398
- inWorkspace = false;
399
- isExecuting = false;
400
- sessionScreen = false;
401
- if (timerInterval) clearInterval(timerInterval);
402
- currentTaskInput = "";
403
- respScroll = null;
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
- function enterWorkspace() {
409
- inWorkspace = true;
410
- isExecuting = false;
411
- currentTaskInput = "";
412
- sessionScreen = true;
413
- redrawInputScreen();
414
- if (timerInterval) clearInterval(timerInterval);
415
- timerInterval = setInterval(() => {
416
- if (sessionTimeLeft > 0) {
417
- sessionTimeLeft--;
418
- if (inWorkspace && !isExecuting) {
419
- redrawInputScreen();
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
- }, 1e3);
536
+ } catch {
537
+ }
538
+ return res;
423
539
  }
424
- function visibleLength(s) {
425
- return s.replace(/\x1b\[[0-9;]*m/g, "").length;
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 wrapWithStyles(input, width) {
428
- const out = [];
429
- let inCode = false;
430
- const text = input.replace(/\r\n?/g, "\n");
431
- for (const raw of text.split("\n")) {
432
- const trimmed = raw.trimStart();
433
- let style = "plain";
434
- if (trimmed.startsWith("```")) {
435
- inCode = !inCode;
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
- if (raw === "") {
443
- out.push({ text: "", style });
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
- let remaining = raw;
447
- while (remaining.length > width) {
448
- let cut = remaining.lastIndexOf(" ", width);
449
- if (cut < Math.floor(width * 0.5)) cut = width;
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
- out.push({ text: remaining, style });
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 out;
608
+ return results;
456
609
  }
457
- function styleBodyLine(l) {
458
- if (l.style === "heading") return import_chalk.default.bold.hex("#c084fc")(l.text);
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
- var EXT_BY_LANG = {
464
- python: "py",
465
- py: "py",
466
- javascript: "js",
467
- js: "js",
468
- typescript: "ts",
469
- ts: "ts",
470
- tsx: "tsx",
471
- jsx: "jsx",
472
- go: "go",
473
- rust: "rs",
474
- rs: "rs",
475
- java: "java",
476
- c: "c",
477
- cpp: "cpp",
478
- "c++": "cpp",
479
- csharp: "cs",
480
- "c#": "cs",
481
- cs: "cs",
482
- php: "php",
483
- ruby: "rb",
484
- rb: "rb",
485
- swift: "swift",
486
- kotlin: "kt",
487
- kt: "kt",
488
- bash: "sh",
489
- sh: "sh",
490
- shell: "sh",
491
- zsh: "sh",
492
- powershell: "ps1",
493
- ps1: "ps1",
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
- function sanitizeFilename(name) {
528
- const base = import_path2.default.basename(name.trim());
529
- if (!base || base === "." || base === "..") return null;
530
- if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(base)) return null;
531
- if (base.includes("..")) return null;
532
- return base;
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 defaultFilename(lang) {
535
- if (lang === "dockerfile") return "Dockerfile";
536
- if (lang === "makefile") return "Makefile";
537
- const ext = EXT_BY_LANG[lang];
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 extractAllFiles(text) {
541
- const extracted = extractFilesFromMarkdown(text);
542
- if (extracted.length) return extracted;
543
- const fence2 = /^[ \t]*```[ \t]*([A-Za-z0-9_#+.-]+)[ \t]*\n([\s\S]*?)^[ \t]*```[ \t]*$/gm;
544
- let best = null;
545
- let m;
546
- while ((m = fence2.exec(text)) !== null) {
547
- const lang = (m[1] || "").toLowerCase();
548
- if (!EXT_BY_LANG.hasOwnProperty(lang) || lang === "text") continue;
549
- const content = m[2];
550
- if (content.trim().split("\n").length < 2) continue;
551
- if (!best || content.length > best.content.length) {
552
- best = { filename: defaultFilename(lang) || "snippet.txt", content, lang };
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 writeExtractedFiles(files) {
558
- const notes = [];
559
- for (const f of files) {
560
- const safe = sanitizeFilename(f.filename);
561
- if (!safe) {
562
- notes.push(import_chalk.default.red(`\u2715 Skip nama file tidak valid: ${f.filename}`));
563
- continue;
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
- var AGENT_SYSTEM_PROMPT = [
577
- "Anda adalah asisten coding agent SarangAI yang berjalan di terminal pengguna.",
578
- `Direktori kerja aktif: ${process.cwd()}.`,
579
- "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",
580
- "Gunakan SATU file per code block dan jangan menambah teks setelah baris penutup ``` milik block tersebut.",
581
- "Untuk file konfigurasi tanpa ekstensi gunakan ```dockerfile:Dockerfile atau ```makefile:Makefile.",
582
- "Jelaskan secara singkat sebelum/sesudah code block, tanpa membungkus nama file di backtick atau bold."
583
- ].join("\n");
584
- var lastTurn = { query: "", text: "", streaming: false, notes: [] };
585
- var activeAbort = null;
586
- function buildTranscript(width) {
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
- for (let i = 0; i + 1 < conversationHistory.length; i += 2) {
589
- const q = conversationHistory[i]?.content ?? "";
590
- const a = conversationHistory[i + 1]?.content ?? "";
591
- out.push({ text: `\u203A Anda: ${q}`, style: "query" });
592
- if (a && a !== "(error)") out.push(...wrapWithStyles(a, width));
593
- out.push({ text: "", style: "plain" });
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
- function drawSessionScreen(turn, opts = { inputMode: "idle" }) {
598
- const boxWidth = 72;
599
- const termRows = process.stdout.rows || 30;
600
- const pad = getCenterPad(boxWidth);
601
- const innerW = boxWidth - 2;
602
- const contentW = innerW - 4;
603
- const lines = [];
604
- SARANG_BANNER.forEach((l) => lines.push(pad + import_chalk.default.white.bold(l)));
605
- lines.push("");
606
- const mm = Math.floor(sessionTimeLeft / 60);
607
- const ss = sessionTimeLeft % 60;
608
- lines.push(pad + import_chalk.default.gray(`${conversationHistory.length} pesan \u2022 ${activeModel.name} \u2022 ${mm}m ${ss < 10 ? "0" : ""}${ss}s left`));
609
- lines.push("");
610
- const transcript = buildTranscript(contentW);
611
- if (!turn.streaming) {
612
- transcript.push({ text: `\u203A Anda: ${turn.query}`, style: "query" });
613
- if (turn.error) {
614
- transcript.push(...wrapWithStyles(turn.error, contentW).map((l) => ({ ...l, style: "error" })));
615
- } else if (turn.text) {
616
- transcript.push(...wrapWithStyles(turn.text, contentW));
617
- }
618
- for (const note of turn.notes) transcript.push({ text: note, style: "plain" });
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
- respTotalLines = transcript.length;
621
- const headerRows = lines.length;
622
- const footerRows = opts.inputMode === "typing" ? 8 : 4;
623
- const viewRows = Math.max(4, termRows - headerRows - footerRows);
624
- const maxStart = Math.max(0, respTotalLines - viewRows);
625
- const start = turn.streaming ? maxStart : Math.min(respScroll ?? maxStart, maxStart);
626
- const visible = transcript.slice(start, start + viewRows);
627
- respViewRows = viewRows;
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
- for (let i = visible.length; i < viewRows; i++) {
635
- lines.push(pad + import_chalk.default.gray("\u2502 ") + " ".repeat(contentW) + import_chalk.default.gray(" \u2502"));
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
- lines.push(pad + import_chalk.default.gray("\u2514" + "\u2500".repeat(innerW) + "\u2518"));
638
- if (opts.inputMode === "typing") {
639
- lines.push("");
640
- const scrollTag = start > 0 || start < maxStart ? import_chalk.default.gray(`[\u2191/\u2193/mouse scroll \u2022 ${start + 1}-${Math.min(respTotalLines, start + viewRows)}/${respTotalLines}] `) : "";
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
- function scrollSession(delta) {
661
- const maxStart = Math.max(0, respTotalLines - respViewRows);
662
- const current = respScroll ?? maxStart;
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
- } catch (err) {
771
- if (err?.name !== "AbortError") {
772
- respError = err.message;
773
- repaint(false, true);
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
- respText += "\n[ dibatalkan oleh pengguna ]";
850
+ this.stopTick();
776
851
  }
852
+ this.requestRender(false);
777
853
  }
778
- let notes = [];
779
- if (respText && !respError) {
780
- notes = writeExtractedFiles(extractAllFiles(respText));
854
+ clearContent() {
855
+ this.history = [];
856
+ this.scrollOffset = 0;
857
+ this.requestRender(true);
781
858
  }
782
- respStreaming = false;
783
- lastTurn = { query, text: respText, streaming: false, error: respError, notes };
784
- if (respError && !isRetry) {
785
- conversationHistory.push({ role: "assistant", content: respText || "(error)" });
786
- } else if (respText) {
787
- conversationHistory.push({ role: "assistant", content: respText });
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
- finishTurnToInput();
790
- }
791
- async function handleAutoAuth(baseUrl2) {
792
- const sessionCode = "SA-" + import_crypto.default.randomBytes(4).toString("hex").toUpperCase();
793
- const authUrl = `${baseUrl2}/auth/cli?code=${sessionCode}`;
794
- console.clear();
795
- const pad = getCenterPad(72);
796
- console.log("\n" + pad + violetText.bold("\u{1F510} Sesi Otorisasi CLI Diperlukan\n"));
797
- copyToClipboard(authUrl);
798
- console.log(pad + import_chalk.default.white("Buka URL berikut untuk mengizinkan:"));
799
- console.log(pad + violetText.underline(authUrl));
800
- console.log(pad + import_chalk.default.green("\u2714 Link otomatis disalin ke clipboard!\n"));
801
- console.log(pad + import_chalk.default.gray(`Menunggu verifikasi web browser (Kode: ${sessionCode})...
802
- `));
803
- for (let i = 0; i < 60; i++) {
804
- await new Promise((r) => setTimeout(r, 2e3));
805
- try {
806
- const pollRes = await fetch(`${baseUrl2}/api/auth/cli/poll?code=${sessionCode}`);
807
- if (pollRes.ok) {
808
- const payload = await pollRes.json();
809
- if (payload.apiKey) {
810
- saveConfig({ apiKey: payload.apiKey });
811
- return payload.apiKey;
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
- } catch {
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
- return null;
818
- }
819
- async function startInteractiveSession() {
820
- cfg = getConfig();
821
- baseUrl = resolveBaseUrl();
822
- const MODEL_MIGRATIONS = {
823
- "anthropic/claude-3.7-sonnet": "anthropic/claude-sonnet-4.6"
824
- };
825
- if (cfg.defaultModel && MODEL_MIGRATIONS[cfg.defaultModel]) {
826
- cfg.defaultModel = MODEL_MIGRATIONS[cfg.defaultModel];
827
- saveConfig({ defaultModel: cfg.defaultModel });
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
- if (!cfg.apiKey) {
830
- const key = await handleAutoAuth(baseUrl);
831
- if (!key) {
832
- console.log("Login dibatalkan.");
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
- userMeta = await fetchUserMeta(baseUrl, cfg.apiKey || "");
838
- activeModel = CODING_MODELS.find((m) => m.id === cfg.defaultModel) || CODING_MODELS[0];
839
- process.stdout.write("\x1B[?1049h\x1B[2J\x1B[3J\x1B[H\x1B[?1000h\x1B[?1006h");
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
- drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
845
- process.stdout.on("resize", () => {
846
- if (inWorkspace) {
847
- if (isExecuting) {
848
- drawSessionScreen(lastTurn);
849
- } else {
850
- redrawInputScreen();
851
- }
852
- } else {
853
- drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
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
- process.stdin.on("data", (buf) => {
857
- const s = buf.toString("utf8");
858
- if (!inWorkspace) return;
859
- let delta = 0;
860
- const m = s.match(/^\x1b\[(?:M([\s\S]{3})|<(?:0|64|65);(\d+);(\d+)[Mm])/);
861
- if (m) {
862
- if (m[1]) {
863
- const b = m[1].charCodeAt(0) - 32;
864
- delta = b === 64 ? -3 : b === 65 ? 3 : 0;
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
- delta = s.includes("<64") ? -3 : s.includes("<65") ? 3 : 0;
1024
+ emit("");
867
1025
  }
868
- } else if (s === "\x1B[A") delta = -3;
869
- else if (s === "\x1B[B") delta = 3;
870
- if (delta !== 0 && !isExecuting && sessionScreen) {
871
- scrollSession(delta);
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 (delta !== 0) return;
875
- });
876
- process.stdin.on("keypress", (str, key) => {
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 (inWorkspace) {
882
- if (isExecuting) {
883
- if (key.name === "escape" && activeAbort) {
884
- activeAbort.abort();
885
- return;
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
- if (key.name === "escape") {
935
- leaveWorkspace();
936
- return;
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
- if (key.name === "return") {
939
- const query = currentTaskInput.trim();
940
- if (!query) return;
941
- if (query === "/exit" || query === "exit" || query === "quit" || query === ":q") {
942
- leaveWorkspace();
943
- return;
944
- }
945
- if (query === "/model" || query === "/back") {
946
- leaveWorkspace();
947
- return;
948
- }
949
- if (query === "/new" || query === "/reset") {
950
- conversationHistory = [];
951
- respLastNotes = [];
952
- enterWorkspace();
953
- return;
954
- }
955
- conversationHistory.push({ role: "user", content: query });
956
- runPromptStream(query);
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
- if (key.name === "backspace") {
960
- currentTaskInput = currentTaskInput.slice(0, -1);
961
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
962
- return;
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
- if (str && !key.ctrl && !key.meta && !str.startsWith("\x1B")) {
965
- currentTaskInput += str;
966
- drawWorkspaceScreen(activeModel, sessionTimeLeft, currentTaskInput);
967
- return;
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.name === "escape" || str === "q") {
972
- cleanupAndExit();
1106
+ if (key === "\x1B[H" || key === "\x1B[1~") {
1107
+ this.cursor = 0;
1108
+ this.requestRender(false);
973
1109
  return;
974
1110
  }
975
- copiedNotice = false;
976
- if (key.name === "down") {
977
- if (!expanded) {
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.name === "up") {
992
- if (!expanded) {
993
- cursorIdx = (cursorIdx - 1 + 4) % 4;
994
- } else {
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 (str === "c" || str === "C") {
1007
- cursorIdx = 3;
1008
- triggerCopyLink();
1123
+ if (key === "\x1B") {
1009
1124
  return;
1010
1125
  }
1011
- if (key.name === "return" || key.name === "space") {
1012
- if (!expanded) {
1013
- if (cursorIdx === 0 || cursorIdx === 2) {
1014
- enterWorkspace();
1015
- } else if (cursorIdx === 1) {
1016
- expanded = true;
1017
- cursorIdx = 10;
1018
- drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
1019
- } else if (cursorIdx === 3) {
1020
- triggerCopyLink();
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
- if (cursorIdx === 20) {
1024
- expanded = false;
1025
- cursorIdx = 1;
1026
- drawSelectionModal(activeModel, userMeta, expanded, cursorIdx, copiedNotice);
1027
- } else if (cursorIdx >= 10) {
1028
- const selected = CODING_MODELS[cursorIdx - 10];
1029
- if (selected) {
1030
- activeModel = selected;
1031
- saveConfig({ defaultModel: activeModel.id });
1032
- expanded = false;
1033
- enterWorkspace();
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
- return;
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.name("sarang").description("SarangAI CLI \u2014 Gateway AI Coding Workspace").version(require_package().version).action(() => {
1042
- startInteractiveSession();
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.parse(process.argv);
1045
- // Annotate the CommonJS export names for ESM import in node:
1046
- 0 && (module.exports = {
1047
- CODING_MODELS
1402
+ program.action(async () => {
1403
+ await startWorkspace("glm");
1048
1404
  });
1405
+ program.parse(process.argv);