chorus-cli 0.4.4 → 0.4.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +26 -18
- package/package.json +1 -1
- package/tools/__pycache__/coder.cpython-314.pyc +0 -0
- package/tools/coder.py +214 -85
package/index.js
CHANGED
|
@@ -18,41 +18,49 @@ const execPromise = util.promisify(exec);
|
|
|
18
18
|
const execFilePromise = util.promisify(execFile);
|
|
19
19
|
const fs = require('fs').promises;
|
|
20
20
|
|
|
21
|
-
// Returns a
|
|
21
|
+
// Returns a SHA-256 hash of this machine's hardware UUID (or a persistent random fallback).
|
|
22
|
+
// The raw UUID never leaves the device — only the hash is sent to the server.
|
|
22
23
|
async function getMachineId() {
|
|
24
|
+
const { createHash, randomUUID } = require('crypto');
|
|
25
|
+
let rawId;
|
|
26
|
+
|
|
23
27
|
try {
|
|
24
28
|
if (process.platform === 'darwin') {
|
|
25
29
|
const { stdout } = await execPromise(
|
|
26
30
|
"ioreg -rd1 -c IOPlatformExpertDevice | awk -F'\"' '/IOPlatformUUID/{print $4}'"
|
|
27
31
|
);
|
|
28
|
-
if (stdout.trim())
|
|
32
|
+
if (stdout.trim()) rawId = stdout.trim();
|
|
29
33
|
} else if (process.platform === 'linux') {
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
if (id) return id;
|
|
34
|
+
const id = (await fs.readFile('/etc/machine-id', 'utf8')).trim();
|
|
35
|
+
if (id) rawId = id;
|
|
33
36
|
} else if (process.platform === 'win32') {
|
|
34
37
|
const { stdout } = await execPromise('wmic csproduct get UUID');
|
|
35
38
|
const lines = stdout.trim().split('\n');
|
|
36
39
|
if (lines.length > 1) {
|
|
37
40
|
const uuid = lines[1].trim();
|
|
38
|
-
if (uuid && uuid !== 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF')
|
|
41
|
+
if (uuid && uuid !== 'FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF') rawId = uuid;
|
|
39
42
|
}
|
|
40
43
|
}
|
|
41
44
|
} catch { /* fall through to persistent fallback */ }
|
|
42
45
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
46
|
+
if (!rawId) {
|
|
47
|
+
// Persistent fallback: generate and cache a random UUID
|
|
48
|
+
const configDir = path.join(os.homedir(), '.config', 'chorus');
|
|
49
|
+
const idPath = path.join(configDir, 'machine-id');
|
|
50
|
+
try {
|
|
51
|
+
const existing = await fs.readFile(idPath, 'utf8');
|
|
52
|
+
if (existing.trim()) rawId = existing.trim();
|
|
53
|
+
} catch { /* no file yet */ }
|
|
54
|
+
|
|
55
|
+
if (!rawId) {
|
|
56
|
+
rawId = randomUUID();
|
|
57
|
+
const configDir2 = path.join(os.homedir(), '.config', 'chorus');
|
|
58
|
+
await fs.mkdir(configDir2, { recursive: true });
|
|
59
|
+
await fs.writeFile(path.join(configDir2, 'machine-id'), rawId + '\n');
|
|
60
|
+
}
|
|
61
|
+
}
|
|
50
62
|
|
|
51
|
-
|
|
52
|
-
const newId = randomUUID();
|
|
53
|
-
await fs.mkdir(configDir, { recursive: true });
|
|
54
|
-
await fs.writeFile(idPath, newId + '\n');
|
|
55
|
-
return newId;
|
|
63
|
+
return createHash('sha256').update(rawId).digest('hex');
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
// Run coder.py with real-time stderr streaming so progress is visible
|
package/package.json
CHANGED
|
Binary file
|
package/tools/coder.py
CHANGED
|
@@ -49,47 +49,183 @@ def is_token_limit_error(err):
|
|
|
49
49
|
msg = str(err)
|
|
50
50
|
return "token limit exceeded" in msg or "rate_limit_error" in msg
|
|
51
51
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
52
|
+
|
|
53
|
+
# ── Shared formatting rules (included in all prompts) ─────────────────────
|
|
54
|
+
_FORMAT_RULES = """\
|
|
55
|
+
OUTPUT FORMAT
|
|
56
|
+
|
|
57
|
+
Your output is displayed raw in a terminal. Never use markdown.
|
|
58
|
+
No headings with #, no **bold**, no *italic*, no `backticks`, no [links](url),
|
|
59
|
+
no bullet characters like - or *.
|
|
60
|
+
Use blank lines and indentation for structure. Use CAPS for section labels.
|
|
61
|
+
Use plain numbered lists (1. 2. 3.) when listing things.
|
|
62
|
+
Refer to code identifiers by name directly (e.g. myFunction not `myFunction`).
|
|
63
|
+
No greetings, preambles, encouragement, or sign-offs.
|
|
64
|
+
No "Great question!", "Let me", "Sure!", "I'll now", or similar filler.
|
|
65
|
+
State what you are doing, then do it. After completing work, state what changed."""
|
|
66
|
+
|
|
67
|
+
# ── Phase 1: Planning prompt (used once at the start of headless mode) ────
|
|
68
|
+
PLAN_PROMPT = """\
|
|
69
|
+
You are a coding agent. You receive a GitHub/Azure DevOps issue, a codebase map, \
|
|
70
|
+
and optionally a QA conversation with clarified requirements. Your job is to \
|
|
71
|
+
plan the implementation.
|
|
72
|
+
|
|
73
|
+
Working directory: {cwd}
|
|
74
|
+
|
|
75
|
+
""" + _FORMAT_RULES + """
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
TASK
|
|
79
|
+
|
|
80
|
+
Read the issue, QA conversation (if provided), and codebase map. Then produce a
|
|
81
|
+
written plan with exactly these sections:
|
|
82
|
+
|
|
83
|
+
UNDERSTANDING:
|
|
84
|
+
What the issue is asking for in one sentence.
|
|
85
|
+
|
|
86
|
+
QUESTIONS STILL OPEN:
|
|
87
|
+
Anything unresolved from QA. If --skip-qa was used, list assumptions you are making
|
|
88
|
+
and flag them clearly. Prefer the conservative/conventional choice for each.
|
|
89
|
+
|
|
90
|
+
FILES TO READ:
|
|
91
|
+
The minimum set of existing files you need to examine to understand the patterns,
|
|
92
|
+
interfaces, and conventions. Maximum 8 files. Justify each.
|
|
93
|
+
|
|
94
|
+
FILES TO CREATE:
|
|
95
|
+
New files with their paths and a one-line description of contents.
|
|
96
|
+
|
|
97
|
+
FILES TO MODIFY:
|
|
98
|
+
Existing files with a one-line description of what changes.
|
|
99
|
+
|
|
100
|
+
APPROACH:
|
|
101
|
+
How you will implement this in 2-4 sentences. Reference specific patterns
|
|
102
|
+
from the codebase map.
|
|
103
|
+
|
|
104
|
+
ON AMBIGUITY
|
|
105
|
+
|
|
106
|
+
When requirements are unclear and no QA conversation was provided:
|
|
107
|
+
1. State your assumption explicitly.
|
|
108
|
+
2. Choose the most conventional/standard approach.
|
|
109
|
+
3. Flag it so the reviewer can catch disagreements early.
|
|
110
|
+
Do not guess ambitiously. Guess conservatively. A correct simple implementation beats
|
|
111
|
+
a broken ambitious one.
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
# ── Phase 2: Execution prompt (used on every turn of the tool loop) ───────
|
|
115
|
+
EXEC_PROMPT = """\
|
|
116
|
+
You are a coding agent executing a plan. Implement the changes described in the
|
|
117
|
+
plan below. Do not re-plan or re-analyze. Execute.
|
|
118
|
+
|
|
55
119
|
Working directory: {cwd}
|
|
56
120
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
121
|
+
""" + _FORMAT_RULES + """
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
TOOL USAGE
|
|
125
|
+
|
|
126
|
+
Always use read_file before editing a file.
|
|
127
|
+
If edit_file fails with "old_string not found", re-read the file with read_file to
|
|
128
|
+
get the actual current content before retrying. Never guess at file contents.
|
|
129
|
+
Use edit_file for targeted changes. Use write_file for new files or complete rewrites.
|
|
130
|
+
Prefer editing existing files over creating new ones.
|
|
131
|
+
Use read_file, list_files, and search_files instead of bash with cat, find, or grep.
|
|
132
|
+
|
|
133
|
+
EXECUTION RULES
|
|
134
|
+
|
|
135
|
+
Reading files:
|
|
136
|
+
Only use read_file on files listed in the plan. If you discover you need another
|
|
137
|
+
file, note why -- but if you have read more than 12 files total, stop and
|
|
138
|
+
re-evaluate. You are exploring, not implementing.
|
|
139
|
+
The one exception to reading a file again: if edit_file fails because old_string
|
|
140
|
+
was not found, re-read that file to get its current content before retrying.
|
|
141
|
+
|
|
142
|
+
Writing code:
|
|
143
|
+
Follow existing patterns exactly. Match the conventions you observed for: naming,
|
|
144
|
+
exports, file structure, import ordering, indentation, and comment style.
|
|
145
|
+
Write complete files. Do not leave TODOs, placeholders, or "implement this" comments.
|
|
146
|
+
If the issue asks for a tool/script, it must work non-interactively (accept arguments
|
|
147
|
+
or config, not interactive prompts) unless the issue explicitly requires interactivity.
|
|
148
|
+
|
|
149
|
+
Shell commands (bash tool):
|
|
150
|
+
You may run bash for: installing dependencies, running the project's existing
|
|
151
|
+
linter, running the project's existing tests, checking TypeScript compilation.
|
|
152
|
+
You may not run bash for: exploratory searching beyond what was in the plan,
|
|
153
|
+
reading files you did not plan to read, testing scripts with improvised piped input.
|
|
154
|
+
Maximum 10 bash commands total. If you are approaching this limit, you are doing
|
|
155
|
+
too much exploration or too much debugging. Ship what you have.
|
|
156
|
+
|
|
157
|
+
BUDGET DISCIPLINE
|
|
158
|
+
|
|
159
|
+
Front-load the high-value work. Write the actual implementation code early. File
|
|
160
|
+
exploration is not progress -- committed code is progress.
|
|
161
|
+
|
|
162
|
+
Do not retry the same failing approach. If something fails twice, choose a different
|
|
163
|
+
approach or simplify. Do not iterate more than twice on the same problem.
|
|
164
|
+
|
|
165
|
+
If something fundamental is broken, stop. Produce what you have, note what is
|
|
166
|
+
incomplete, and let the human finish. A partial, clean implementation is more
|
|
167
|
+
valuable than a complete, broken one.
|
|
168
|
+
|
|
169
|
+
No yak-shaving. Deliver the core ask. Do not create demo scripts, markdown docs,
|
|
170
|
+
bash wrappers, or sample outputs unless the issue asks for them.
|
|
171
|
+
|
|
172
|
+
WHAT NOT TO DO
|
|
173
|
+
|
|
174
|
+
Do not explore the filesystem to "understand the project." The codebase map and your
|
|
175
|
+
plan already cover that. Read specific files for specific reasons.
|
|
176
|
+
|
|
177
|
+
Do not overuse list_files, search_files, or bash for exploration. If you need more
|
|
178
|
+
than 3 exploratory calls, your plan was insufficient.
|
|
179
|
+
|
|
180
|
+
Do not create interactive scripts that require stdin.
|
|
181
|
+
Do not create documentation or READMEs unless the issue asks for them.
|
|
182
|
+
Do not modify package.json, CI configs, or project infrastructure unless the issue
|
|
183
|
+
specifically requires it.
|
|
184
|
+
Do not keep going when stuck. After 2 failed attempts at the same problem, note the
|
|
185
|
+
issue, deliver what works, and move on.
|
|
186
|
+
|
|
187
|
+
WHEN YOU ARE DONE
|
|
188
|
+
|
|
189
|
+
End with a verify block:
|
|
190
|
+
|
|
191
|
+
1. List every file you created or modified.
|
|
192
|
+
2. For each, confirm it is syntactically valid (ran linter or compiler if available).
|
|
193
|
+
3. If tests exist for the area you changed, run them and confirm they pass.
|
|
194
|
+
4. If you wrote a script/CLI tool, show the --help output or a dry-run invocation.
|
|
195
|
+
5. Write a summary in this exact format:
|
|
196
|
+
|
|
197
|
+
PR_TITLE: conventional commit format (feat:, fix:, chore:, etc.)
|
|
198
|
+
PR_BODY: what changed, why, any assumptions made, anything the reviewer should
|
|
199
|
+
look at carefully.
|
|
200
|
+
|
|
201
|
+
Do not output anything after the summary.
|
|
85
202
|
"""
|
|
86
203
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
204
|
+
# ── Interactive REPL prompt (conversational, no phased workflow) ──────────
|
|
205
|
+
REPL_PROMPT = """\
|
|
206
|
+
You are a coding agent running in an interactive terminal session.
|
|
207
|
+
Working directory: {cwd}
|
|
208
|
+
|
|
209
|
+
""" + _FORMAT_RULES + """
|
|
210
|
+
|
|
211
|
+
|
|
212
|
+
TOOL USAGE
|
|
213
|
+
|
|
214
|
+
Always use read_file before editing a file.
|
|
215
|
+
If edit_file fails with "old_string not found", re-read the file with read_file to
|
|
216
|
+
get the actual current content before retrying. Never guess at file contents.
|
|
217
|
+
Use edit_file for targeted changes. Use write_file for new files or complete rewrites.
|
|
218
|
+
Prefer editing existing files over creating new ones.
|
|
219
|
+
Use read_file, list_files, and search_files instead of bash with cat, find, or grep.
|
|
92
220
|
|
|
221
|
+
GUIDELINES
|
|
222
|
+
|
|
223
|
+
Be direct and concise. State what you will do, then do it.
|
|
224
|
+
Always use your tools. Never guess when you can look.
|
|
225
|
+
Do not write new unit tests unless the project already has substantive test coverage.
|
|
226
|
+
Do not attempt to build or compile the project unless asked.
|
|
227
|
+
Do not add unnecessary comments, docstrings, or type annotations.
|
|
228
|
+
For bash commands, prefer non-interactive commands.
|
|
93
229
|
"""
|
|
94
230
|
|
|
95
231
|
# ── Tool Definitions ────────────────────────────────────────────────────────
|
|
@@ -689,18 +825,19 @@ def stream_response(client, messages, system):
|
|
|
689
825
|
|
|
690
826
|
# ── Headless Prompt Mode ────────────────────────────────────────────────────
|
|
691
827
|
|
|
692
|
-
def run_prompt(client, prompt,
|
|
828
|
+
def run_prompt(client, prompt, plan_system, exec_system):
|
|
693
829
|
"""Run a single prompt non-interactively. Returns a JSON-serializable dict."""
|
|
694
|
-
|
|
695
|
-
# PHASE 1: Planning -
|
|
830
|
+
|
|
831
|
+
# PHASE 1: Planning - separate API call with planning prompt
|
|
696
832
|
print(f"\n{C.BOLD}{C.BLUE}📝 PLANNING PHASE{C.RESET}", file=sys.stderr, flush=True)
|
|
697
833
|
print(f"{C.DIM}Understanding the issue and creating a plan...{C.RESET}\n", file=sys.stderr, flush=True)
|
|
698
|
-
|
|
834
|
+
|
|
835
|
+
plan_text = ""
|
|
699
836
|
plan_messages = [
|
|
700
|
-
{"role": "system", "content":
|
|
701
|
-
{"role": "user", "content": f"{prompt}\n\
|
|
837
|
+
{"role": "system", "content": plan_system},
|
|
838
|
+
{"role": "user", "content": f"{prompt}\n\nProduce the plan using the exact sections: UNDERSTANDING, QUESTIONS STILL OPEN, FILES TO READ, FILES TO CREATE, FILES TO MODIFY, APPROACH. Do NOT write any code yet."}
|
|
702
839
|
]
|
|
703
|
-
|
|
840
|
+
|
|
704
841
|
try:
|
|
705
842
|
plan_response = client.chat.completions.create(
|
|
706
843
|
model=MODEL,
|
|
@@ -708,23 +845,26 @@ def run_prompt(client, prompt, system):
|
|
|
708
845
|
messages=plan_messages,
|
|
709
846
|
)
|
|
710
847
|
plan_text = plan_response.choices[0].message.content.strip()
|
|
711
|
-
|
|
848
|
+
|
|
712
849
|
# Print the plan with formatting
|
|
713
850
|
print(f"{C.CYAN}{'─' * 60}{C.RESET}", file=sys.stderr, flush=True)
|
|
714
851
|
for line in plan_text.split('\n'):
|
|
715
852
|
print(f"{C.CYAN} {line}{C.RESET}", file=sys.stderr, flush=True)
|
|
716
853
|
print(f"{C.CYAN}{'─' * 60}{C.RESET}\n", file=sys.stderr, flush=True)
|
|
717
|
-
|
|
854
|
+
|
|
718
855
|
except Exception as e:
|
|
719
856
|
print(f"{C.YELLOW}Could not generate plan: {e}{C.RESET}", file=sys.stderr, flush=True)
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
# PHASE 2: Execution - proceed with the actual coding
|
|
857
|
+
|
|
858
|
+
# PHASE 2: Execution - inject plan into the conversation so the agent can reference it
|
|
723
859
|
print(f"{C.BOLD}{C.GREEN}🔨 EXECUTING PLAN{C.RESET}\n", file=sys.stderr, flush=True)
|
|
724
|
-
|
|
860
|
+
|
|
861
|
+
exec_user_content = prompt
|
|
862
|
+
if plan_text:
|
|
863
|
+
exec_user_content = f"{prompt}\n\nHere is your plan. Follow it.\n\n{plan_text}"
|
|
864
|
+
|
|
725
865
|
messages = [
|
|
726
|
-
{"role": "system", "content":
|
|
727
|
-
{"role": "user", "content":
|
|
866
|
+
{"role": "system", "content": exec_system},
|
|
867
|
+
{"role": "user", "content": exec_user_content}
|
|
728
868
|
]
|
|
729
869
|
files_modified = set()
|
|
730
870
|
files_created = set()
|
|
@@ -867,37 +1007,22 @@ def run_prompt(client, prompt, system):
|
|
|
867
1007
|
final_text = msg.content
|
|
868
1008
|
break
|
|
869
1009
|
|
|
870
|
-
#
|
|
1010
|
+
# Parse PR_TITLE / PR_BODY from the agent's verify output (if present)
|
|
871
1011
|
summary = final_text.strip()
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
model=MODEL,
|
|
887
|
-
max_tokens=1024,
|
|
888
|
-
messages=summary_messages,
|
|
889
|
-
)
|
|
890
|
-
|
|
891
|
-
if hasattr(summary_response, "usage") and summary_response.usage:
|
|
892
|
-
total_input_tokens += summary_response.usage.prompt_tokens
|
|
893
|
-
total_output_tokens += summary_response.usage.completion_tokens
|
|
894
|
-
|
|
895
|
-
summary = summary_response.choices[0].message.content.strip()
|
|
896
|
-
except Exception as e:
|
|
897
|
-
if is_token_limit_error(e):
|
|
898
|
-
errors.append(str(e))
|
|
899
|
-
else:
|
|
900
|
-
raise
|
|
1012
|
+
pr_title_match = re.search(r'PR_TITLE:\s*(.+)', final_text)
|
|
1013
|
+
pr_body_match = re.search(r'PR_BODY:\s*([\s\S]+?)$', final_text)
|
|
1014
|
+
if pr_title_match:
|
|
1015
|
+
pr_title = pr_title_match.group(1).strip()
|
|
1016
|
+
pr_body = pr_body_match.group(1).strip() if pr_body_match else ""
|
|
1017
|
+
summary = f"{pr_title}\n\n{pr_body}".strip() if pr_body else pr_title
|
|
1018
|
+
elif not summary:
|
|
1019
|
+
# Fallback: build a minimal summary from file lists
|
|
1020
|
+
parts = []
|
|
1021
|
+
if files_created:
|
|
1022
|
+
parts.append(f"Created: {', '.join(sorted(files_created))}")
|
|
1023
|
+
if files_modified:
|
|
1024
|
+
parts.append(f"Modified: {', '.join(sorted(files_modified))}")
|
|
1025
|
+
summary = ". ".join(parts) if parts else "No changes produced."
|
|
901
1026
|
|
|
902
1027
|
result = {
|
|
903
1028
|
"completed": len(errors) == 0,
|
|
@@ -935,24 +1060,27 @@ def main():
|
|
|
935
1060
|
if machine_id:
|
|
936
1061
|
client_kwargs["default_headers"] = {"X-Machine-Id": machine_id}
|
|
937
1062
|
client = OpenAI(**client_kwargs)
|
|
938
|
-
|
|
1063
|
+
cwd = os.getcwd()
|
|
939
1064
|
|
|
940
1065
|
# Load codebase map if available
|
|
1066
|
+
map_suffix = ""
|
|
941
1067
|
map_file = Path.cwd() / ".coder" / "map.md"
|
|
942
1068
|
if map_file.exists():
|
|
943
1069
|
try:
|
|
944
1070
|
map_content = map_file.read_text(encoding="utf-8").strip()
|
|
945
1071
|
if len(map_content) > 20000:
|
|
946
|
-
map_content = map_content[:20000] + "\n\n... (map truncated
|
|
947
|
-
|
|
1072
|
+
map_content = map_content[:20000] + "\n\n... (map truncated -- use list_files to explore further)"
|
|
1073
|
+
map_suffix = f"\n\n{map_content}"
|
|
948
1074
|
print(f"{C.DIM}Loaded codebase map ({map_content.count(chr(10))} lines){C.RESET}", file=sys.stderr)
|
|
949
1075
|
except OSError:
|
|
950
1076
|
pass
|
|
951
1077
|
|
|
952
1078
|
# ── Headless prompt mode ────────────────────────────────────────────
|
|
953
1079
|
if args.prompt:
|
|
1080
|
+
plan_system = PLAN_PROMPT.format(cwd=cwd) + map_suffix
|
|
1081
|
+
exec_system = EXEC_PROMPT.format(cwd=cwd) + map_suffix
|
|
954
1082
|
try:
|
|
955
|
-
result = run_prompt(client, args.prompt,
|
|
1083
|
+
result = run_prompt(client, args.prompt, plan_system, exec_system)
|
|
956
1084
|
print(json.dumps(result, indent=2))
|
|
957
1085
|
sys.exit(0 if result["completed"] else 1)
|
|
958
1086
|
except Exception as e:
|
|
@@ -977,6 +1105,7 @@ def main():
|
|
|
977
1105
|
sys.exit(130)
|
|
978
1106
|
|
|
979
1107
|
# ── Interactive REPL mode ───────────────────────────────────────────
|
|
1108
|
+
system = REPL_PROMPT.format(cwd=cwd) + map_suffix
|
|
980
1109
|
messages = []
|
|
981
1110
|
|
|
982
1111
|
mode_label = f" {C.YELLOW}(safe mode){C.RESET}" if SAFE_MODE else ""
|