jeo-code 0.6.4 → 0.6.5

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/CHANGELOG.md CHANGED
@@ -6,6 +6,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
6
6
 
7
7
  The README mirrors the latest 5 entries — regenerate with `bun run changelog:sync`.
8
8
 
9
+ ## [0.6.5] - 2026-06-16
10
+ _macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules._
11
+
12
+ ### Added
13
+ - **macOS / fixterms combo keys in the boxed prompt.** Option+Left/Right (word jump) and Cmd+Left/Right (line start/end), plus Option/Cmd+Backspace, are normalized to the canonical control bytes Bun's readline already acts on — so the keys macOS users actually reach for now move the caret instead of doing nothing. Readline stays the single owner of the cursor; the box just repaints.
14
+ - **Fresh-start screen clear at launch.** The banner opens atop a cleared screen (erase screen + scrollback) on a TTY — never mid-turn, so tmux scrollback is never flooded.
15
+
16
+ ### Changed
17
+ - **Welcome banner uses a natural, proportional hero width** instead of stretching into a mostly-empty rectangle on wide terminals; it shrinks gracefully (grand→compact claw) on narrow ones so the art keeps its shape and never clips.
18
+ - **`launch.ts` split into focused submodules** under `src/commands/launch/` (`flags`, `input`, `tmux`, `stream`, `workflow`) — a ~1000-line maintainability refactor with no behavior change; the public surface is re-exported unchanged.
19
+
20
+ ### Fixed
21
+ - **Renderer relayouts on height change**, not just width — a terminal that grows/shrinks vertically now repaints correctly.
22
+ - **Pickers no longer leave typed filter text behind.** A `/model` · `/agents` picker that read keystrokes directly no longer queues its leftover filter text as the next prompt.
23
+
9
24
  ## [0.6.4] - 2026-06-16
10
25
  _Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery._
11
26
 
package/README.ja.md CHANGED
@@ -162,11 +162,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
162
162
  ## 変更履歴 (Changelog)
163
163
 
164
164
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
165
+ - **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
165
166
  - **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
166
167
  - **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
167
168
  - **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
168
169
  - **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
169
- - **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
170
170
 
171
171
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
172
172
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -162,11 +162,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
162
162
  ## 변경 이력 (Changelog)
163
163
 
164
164
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
165
+ - **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
165
166
  - **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
166
167
  - **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
167
168
  - **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
168
169
  - **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
169
- - **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
170
170
 
171
171
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
172
172
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -162,11 +162,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
162
162
  ## Changelog
163
163
 
164
164
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
165
+ - **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
165
166
  - **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
166
167
  - **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
167
168
  - **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
168
169
  - **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
169
- - **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
170
170
 
171
171
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
172
172
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -162,11 +162,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
162
162
  ## 更新日志 (Changelog)
163
163
 
164
164
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
165
+ - **[0.6.5]** (2026-06-16) — macOS combo-key editing in the boxed prompt, a fresh-start screen clear at launch, a proportional welcome banner, height-aware relayout — and `launch.ts` split into focused submodules.
165
166
  - **[0.6.4]** (2026-06-16) — Branding, a responsive-resize fix, `/provider` realignment, and engine repeat-spin recovery.
166
167
  - **[0.6.3]** (2026-06-16) — OAuth loopback reliability fix.
167
168
  - **[0.6.2]** (2026-06-16) — Interactive `/provider` picker, clearer animated status + labeled block/prose boundaries, and a transient empty-response retry.
168
169
  - **[0.6.1]** (2026-06-16) — Live reasoning progress (no more frozen "calling model"), thinking-level fixes for Anthropic/Antigravity, and input-box/Ctrl+O TUI fixes.
169
- - **[0.6.0]** (2026-06-16) — TUI quality of life: durable input history (↑ recalls past queries across launches), clean `/resume` rendering, and a scrollable mid-turn Ctrl+O panel.
170
170
 
171
171
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
172
172
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -0,0 +1,282 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ import { type ProviderName, type ModelRole, type ThinkLevel, catalogMetadata } from "../../ai";
4
+
5
+ export interface LaunchFlags {
6
+ list: boolean;
7
+ resume: boolean;
8
+ resumeId?: string;
9
+ noSession: boolean;
10
+ noTui: boolean;
11
+ /** Explicit step cap from --max-steps; 0 = dynamic (process-driven budget that
12
+ * keeps extending while the turn shows progress — no hardcoded step ceiling). */
13
+ maxSteps: number;
14
+ message: string;
15
+ tmux: boolean;
16
+ worktree?: string;
17
+ model?: string;
18
+ provider?: ProviderName;
19
+ modelRole?: ModelRole;
20
+ thinking?: ThinkLevel;
21
+ errors: string[];
22
+ print?: boolean;
23
+ appendSystemPromptRaw?: string;
24
+ appendSystemPrompt?: string;
25
+ noSkills: boolean;
26
+ skills?: string;
27
+ noTools: boolean;
28
+ tools?: string;
29
+ systemPromptRaw?: string;
30
+ systemPrompt?: string;
31
+ }
32
+
33
+ function takeValue(args: string[], index: number, inlinePrefix: string): { value?: string; nextIndex: number } {
34
+ const current = args[index]!;
35
+ if (current.startsWith(inlinePrefix)) return { value: current.slice(inlinePrefix.length), nextIndex: index };
36
+ const next = args[index + 1];
37
+ if (next && !next.startsWith("-")) return { value: next, nextIndex: index + 1 };
38
+ return { nextIndex: index };
39
+ }
40
+
41
+ export function isProviderName(input: string | undefined): input is ProviderName {
42
+ return input === "anthropic" || input === "openai" || input === "gemini" || input === "antigravity" || input === "ollama";
43
+ }
44
+
45
+ export function isThinkingLevel(input: string | undefined): input is ThinkLevel {
46
+ return input === "minimal" || input === "low" || input === "medium" || input === "high" || input === "xhigh";
47
+ }
48
+
49
+
50
+ export function fastThinkingLevelForModel(modelId: string): ThinkLevel | undefined {
51
+ const supported = catalogMetadata(modelId)?.thinking ?? [];
52
+ if (supported.includes("minimal")) return "minimal";
53
+ if (supported.includes("low")) return "low";
54
+ if (/gemini-(2\.5|[3-9])/.test(modelId.toLowerCase())) return "minimal";
55
+ return undefined;
56
+ }
57
+
58
+ export function parseFlags(args: string[], cwd: string = process.cwd()): LaunchFlags {
59
+ const flags: LaunchFlags = { list: false, resume: false, noSession: false, noTui: false, maxSteps: 0, message: "", tmux: false, errors: [], print: false, noSkills: false, noTools: false };
60
+ const rest: string[] = [];
61
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
62
+ for (let i = 0; i < args.length; i++) {
63
+ const a = args[i];
64
+ if (a === "--") {
65
+ rest.push(...args.slice(i + 1));
66
+ break;
67
+ }
68
+ if (a === "--list") {
69
+ flags.list = true;
70
+ } else if (a === "-p" || a === "--print") {
71
+ flags.print = true;
72
+ flags.noTui = true;
73
+ } else if (a === "--tmux") {
74
+ flags.tmux = true;
75
+ } else if (a === "--worktree") {
76
+ const next = args[i + 1];
77
+ if (next && !next.startsWith("-")) {
78
+ flags.worktree = next;
79
+ i++;
80
+ }
81
+ } else if (a.startsWith("--worktree=")) {
82
+ flags.worktree = a.slice("--worktree=".length);
83
+ } else if (a === "--no-session") {
84
+ flags.noSession = true;
85
+ } else if (a === "--no-tui") {
86
+ flags.noTui = true;
87
+ } else if (a === "--max-steps") {
88
+ const n = parseInt(args[i + 1] ?? "", 10);
89
+ if (Number.isFinite(n) && n > 0) {
90
+ flags.maxSteps = n;
91
+ i++;
92
+ }
93
+ } else if (a.startsWith("--max-steps=")) {
94
+ const n = parseInt(a.slice(12), 10);
95
+ if (Number.isFinite(n) && n > 0) flags.maxSteps = n;
96
+ } else if (a === "--model") {
97
+ const { value, nextIndex } = takeValue(args, i, "--model=");
98
+ if (value) flags.model = value;
99
+ else flags.errors.push("--model requires a value");
100
+ i = nextIndex;
101
+ } else if (a.startsWith("--model=")) {
102
+ const { value } = takeValue(args, i, "--model=");
103
+ if (value) flags.model = value;
104
+ else flags.errors.push("--model requires a value");
105
+ } else if (a === "--provider") {
106
+ const { value, nextIndex } = takeValue(args, i, "--provider=");
107
+ const normalized = value?.toLowerCase();
108
+ if (isProviderName(normalized)) flags.provider = normalized;
109
+ else flags.errors.push("--provider must be one of: anthropic, openai, gemini, ollama");
110
+ i = nextIndex;
111
+ } else if (a.startsWith("--provider=")) {
112
+ const { value } = takeValue(args, i, "--provider=");
113
+ const normalized = value?.toLowerCase();
114
+ if (isProviderName(normalized)) flags.provider = normalized;
115
+ else flags.errors.push("--provider must be one of: anthropic, openai, gemini, ollama");
116
+ } else if (a === "--thinking") {
117
+ const { value, nextIndex } = takeValue(args, i, "--thinking=");
118
+ const normalized = value?.toLowerCase();
119
+ if (isThinkingLevel(normalized)) flags.thinking = normalized;
120
+ else flags.errors.push("--thinking must be one of: minimal, low, medium, high, xhigh");
121
+ i = nextIndex;
122
+ } else if (a.startsWith("--thinking=")) {
123
+ const { value } = takeValue(args, i, "--thinking=");
124
+ const normalized = value?.toLowerCase();
125
+ if (isThinkingLevel(normalized)) flags.thinking = normalized;
126
+ else flags.errors.push("--thinking must be one of: minimal, low, medium, high, xhigh");
127
+ } else if (a === "--smol" || a === "--slow" || a === "--plan") {
128
+ flags.modelRole = a.slice(2) as ModelRole;
129
+ } else if (a === "--resume" || a === "--continue" || a === "-c") {
130
+ flags.resume = true;
131
+ const next = args[i + 1];
132
+ if (next && UUID_REGEX.test(next)) {
133
+ flags.resumeId = next;
134
+ i++;
135
+ }
136
+ } else if (a.startsWith("--resume=") || a.startsWith("--continue=") || a.startsWith("-c=")) {
137
+ flags.resume = true;
138
+ const eqIdx = a.indexOf("=");
139
+ const val = a.slice(eqIdx + 1);
140
+ if (UUID_REGEX.test(val)) {
141
+ flags.resumeId = val;
142
+ } else {
143
+ rest.push(val);
144
+ }
145
+ } else if (a === "--append-system-prompt") {
146
+ const { value, nextIndex } = takeValue(args, i, "--append-system-prompt=");
147
+ if (value) {
148
+ flags.appendSystemPromptRaw = value;
149
+ } else {
150
+ flags.errors.push("--append-system-prompt requires a value");
151
+ }
152
+ i = nextIndex;
153
+ } else if (a.startsWith("--append-system-prompt=")) {
154
+ const { value } = takeValue(args, i, "--append-system-prompt=");
155
+ if (value) {
156
+ flags.appendSystemPromptRaw = value;
157
+ } else {
158
+ flags.errors.push("--append-system-prompt requires a value");
159
+ }
160
+ } else if (a === "--no-skills") {
161
+ flags.noSkills = true;
162
+ } else if (a === "--skills") {
163
+ const { value, nextIndex } = takeValue(args, i, "--skills=");
164
+ if (value) flags.skills = value;
165
+ else flags.errors.push("--skills requires a value");
166
+ i = nextIndex;
167
+ } else if (a.startsWith("--skills=")) {
168
+ const { value } = takeValue(args, i, "--skills=");
169
+ if (value) flags.skills = value;
170
+ else flags.errors.push("--skills requires a value");
171
+ } else if (a === "--no-tools") {
172
+ flags.noTools = true;
173
+ } else if (a === "--tools") {
174
+ const { value, nextIndex } = takeValue(args, i, "--tools=");
175
+ if (value) flags.tools = value;
176
+ else flags.errors.push("--tools requires a value");
177
+ i = nextIndex;
178
+ } else if (a.startsWith("--tools=")) {
179
+ const { value } = takeValue(args, i, "--tools=");
180
+ if (value) flags.tools = value;
181
+ else flags.errors.push("--tools requires a value");
182
+ } else if (a === "--system-prompt") {
183
+ const { value, nextIndex } = takeValue(args, i, "--system-prompt=");
184
+ if (value) flags.systemPromptRaw = value;
185
+ else flags.errors.push("--system-prompt requires a value");
186
+ i = nextIndex;
187
+ } else if (a.startsWith("--system-prompt=")) {
188
+ const { value } = takeValue(args, i, "--system-prompt=");
189
+ if (value) flags.systemPromptRaw = value;
190
+ else flags.errors.push("--system-prompt requires a value");
191
+ } else {
192
+ rest.push(a);
193
+ }
194
+ }
195
+ flags.message = rest.join(" ").trim();
196
+
197
+ if (flags.print && !flags.message) {
198
+ flags.errors.push("-p/--print requires a message argument");
199
+ }
200
+
201
+ if (flags.appendSystemPromptRaw) {
202
+ if (flags.appendSystemPromptRaw.startsWith("@")) {
203
+ const filePath = flags.appendSystemPromptRaw.slice(1);
204
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
205
+ try {
206
+ flags.appendSystemPrompt = fs.readFileSync(absPath, "utf8");
207
+ } catch (err) {
208
+ flags.errors.push(`failed to read system prompt file: ${(err as Error).message}`);
209
+ }
210
+ } else {
211
+ flags.appendSystemPrompt = flags.appendSystemPromptRaw;
212
+ }
213
+ }
214
+ if (flags.systemPromptRaw) {
215
+ if (flags.systemPromptRaw.startsWith("@")) {
216
+ const filePath = flags.systemPromptRaw.slice(1);
217
+ const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
218
+ try {
219
+ flags.systemPrompt = fs.readFileSync(absPath, "utf8");
220
+ } catch (err) {
221
+ flags.errors.push(`failed to read system prompt file: ${(err as Error).message}`);
222
+ }
223
+ } else {
224
+ flags.systemPrompt = flags.systemPromptRaw;
225
+ }
226
+ }
227
+
228
+ return flags;
229
+ }
230
+
231
+ export function matchSkillGlob(pattern: string, name: string): boolean {
232
+ const p = pattern.toLowerCase();
233
+ const n = name.toLowerCase();
234
+ if (!p.includes("*")) {
235
+ return p === n;
236
+ }
237
+ const escaped = p.replace(/[.+^${}()|[\\\]]/g, "\\$&");
238
+ const regexStr = "^" + escaped.replace(/\*/g, ".*") + "$";
239
+ const regex = new RegExp(regexStr);
240
+ return regex.test(n);
241
+ }
242
+
243
+ export function filterToolMap(
244
+ tools: Record<string, any>,
245
+ allowlist: string[]
246
+ ): Record<string, any> {
247
+ const result: Record<string, any> = {};
248
+ for (const name of allowlist) {
249
+ if (name in tools) {
250
+ result[name] = tools[name];
251
+ }
252
+ }
253
+ return result;
254
+ }
255
+
256
+ export const TOOL_DESCRIPTIONS: Record<string, string> = {
257
+ read: "read {filePath, lineRange?, raw?} — read a file; lines are prefixed `LINEhh|` (hh = 2-char content anchor; the | is a separator, not file bytes)",
258
+ write: "write {filePath, content} — create/overwrite a file",
259
+ edit: "edit {filePath, editBlock} — ≔A..B replace lines (append read anchors for safety: ≔12ab..15cd — rejected with fresh content if the lines changed); ≔A+ insert after line A; ≔$ append EOF (payload on next line). NEVER copy the `LINEhh|` prefixes into SEARCH blocks or payloads",
260
+ bash: "bash {command, timeoutMs?, cwd?, env?} — run a shell command (cwd: subdir; env: extra vars)",
261
+ find: "find {globPattern} — find files by name",
262
+ search: "search {pattern, globPattern?, ignoreCase?, context?, maxMatches?} — grep (context: N lines around each match)",
263
+ ls: "ls {dirPath} — list a directory's entries (dirs first)",
264
+ };
265
+
266
+ export function buildToolProtocol(allowedTools: Set<string>): string {
267
+ const lines: string[] = ["You have these tools (call exactly ONE per step):"];
268
+ let num = 1;
269
+ for (const name of ["read", "write", "edit", "bash", "find", "search", "ls"]) {
270
+ if (allowedTools.has(name)) {
271
+ lines.push(`${num}. ${TOOL_DESCRIPTIONS[name]}`);
272
+ num++;
273
+ }
274
+ }
275
+ lines.push(`${num}. done {reason?} — call when the task is fully implemented AND verified`);
276
+ lines.push("");
277
+ lines.push("Reply with STRICT JSON only — no code fences. You MAY include an optional leading");
278
+ lines.push('"reasoning" string (one short sentence on your plan, shown live to the user) before "tool":');
279
+ lines.push('{ "reasoning": "<one short sentence>", "tool": "<name>", "arguments": { ... } }');
280
+ lines.push("Tool calibration: scale calls to difficulty — one for a known fact, a few for a normal task, more only when evidence is genuinely missing. Locate before you open: search/find first, then read the hit, instead of guessing paths.");
281
+ return lines.join("\n");
282
+ }