jeo-code 0.5.6 → 0.5.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.ja.md CHANGED
@@ -150,11 +150,11 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
150
150
  ## 変更履歴 (Changelog)
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
153
154
  - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
154
155
  - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
155
156
  - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
156
157
  - **[0.5.3]** (2026-06-15) — `$` chains multiple skills in one line (all run, in order), plus multi-line prompt input — paste-merge and gated Shift+Enter.
157
- - **[0.5.2]** (2026-06-14) — `$skill` prompt invocation with prefix/fuzzy suggestions, and a per-session input-box hue (amber in cmd-mode).
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/README.ko.md CHANGED
@@ -150,11 +150,11 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
150
150
  ## 변경 이력 (Changelog)
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
153
154
  - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
154
155
  - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
155
156
  - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
156
157
  - **[0.5.3]** (2026-06-15) — `$` chains multiple skills in one line (all run, in order), plus multi-line prompt input — paste-merge and gated Shift+Enter.
157
- - **[0.5.2]** (2026-06-14) — `$skill` prompt invocation with prefix/fuzzy suggestions, and a per-session input-box hue (amber in cmd-mode).
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/README.md CHANGED
@@ -150,11 +150,11 @@ Required npm token permissions (repository secret `NPM_TOKEN`):
150
150
  ## Changelog
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
153
154
  - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
154
155
  - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
155
156
  - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
156
157
  - **[0.5.3]** (2026-06-15) — `$` chains multiple skills in one line (all run, in order), plus multi-line prompt input — paste-merge and gated Shift+Enter.
157
- - **[0.5.2]** (2026-06-14) — `$skill` prompt invocation with prefix/fuzzy suggestions, and a per-session input-box hue (amber in cmd-mode).
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/README.zh.md CHANGED
@@ -150,11 +150,11 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
150
150
  ## 更新日志 (Changelog)
151
151
 
152
152
  <!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
153
+ - **[0.5.7]** (2026-06-15) — `/model` picker is default-only, `/clear` resets to the initial screen, ESC clears the input box, and a launch process-listener leak is fixed.
153
154
  - **[0.5.6]** (2026-06-15) — `/model` sets only the default thinking; per-role reasoning moved to `/agents`.
154
155
  - **[0.5.5]** (2026-06-15) — Full multi-line visibility — the input box scrolls to the caret and the submitted card shows every line.
155
156
  - **[0.5.4]** (2026-06-15) — Reliable multi-line input is ON by default — a paste fills the box and submits as one message.
156
157
  - **[0.5.3]** (2026-06-15) — `$` chains multiple skills in one line (all run, in order), plus multi-line prompt input — paste-merge and gated Shift+Enter.
157
- - **[0.5.2]** (2026-06-14) — `$skill` prompt invocation with prefix/fuzzy suggestions, and a per-session input-box hue (amber in cmd-mode).
158
158
 
159
159
  See [CHANGELOG.md](CHANGELOG.md) for the full history.
160
160
  <!-- CHANGELOG:END -->
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jeo-code",
3
- "version": "0.5.6",
3
+ "version": "0.5.7",
4
4
  "description": "Clean, highly optimized AI coding agent using spec-first loop",
5
5
  "type": "module",
6
6
  "main": "src/cli.ts",
@@ -1931,6 +1931,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1931
1931
  const multilineInput = !!process.stdin.isTTY && jeoEnv("NO_MULTILINE") !== "1";
1932
1932
  const loneLfShiftEnter = jeoEnv("MULTILINE") === "1";
1933
1933
  const expandSentinel = (s: string): string => (multilineInput ? s.split(SENTINEL).join("\n") : s);
1934
+ // Prompt-scoped process listeners (stdin data/keypress, stdout resize). Registered
1935
+ // once per launch but previously anonymous and never removed — benign for a single
1936
+ // CLI run, but repeated launch() (test harness) accumulated them past Node's
1937
+ // 10-listener default → MaxListenersExceededWarning + a real leak. Track each remover
1938
+ // and drain it on every exit path so the process listener set returns to baseline.
1939
+ const promptListenerCleanups: Array<() => void> = [];
1940
+ const drainPromptListeners = () => {
1941
+ for (const off of promptListenerCleanups.splice(0)) { try { off(); } catch { /* best effort */ } }
1942
+ };
1934
1943
  let keyFilter: PassThrough | undefined;
1935
1944
  if (multilineInput) {
1936
1945
  const kf = new PassThrough();
@@ -1951,7 +1960,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1951
1960
  // off) and the xterm "\x1b[27;2;13~" / kitty "\x1b[13;2u" sequences. Enter ("\r")
1952
1961
  // passes through and submits.
1953
1962
  let kfInPaste = false;
1954
- process.stdin.on("data", (chunk: Buffer) => {
1963
+ const kfDataHandler = (chunk: Buffer) => {
1955
1964
  const data = chunk.toString("utf8");
1956
1965
  let out = "";
1957
1966
  let i = 0;
@@ -1972,7 +1981,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
1972
1981
  out += data[i]; i += 1;
1973
1982
  }
1974
1983
  kf.write(out);
1975
- });
1984
+ };
1985
+ process.stdin.on("data", kfDataHandler);
1986
+ promptListenerCleanups.push(() => process.stdin.off("data", kfDataHandler));
1976
1987
  keyFilter = kf;
1977
1988
  // readline now decodes keypresses on `keyFilter`; keep process.stdin emitting
1978
1989
  // 'keypress' too so the footer-redraw / paste-marker / picker listeners (registered
@@ -2032,13 +2043,15 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2032
2043
  const pasteMerge: { buf: string[]; endWaiters: Array<() => void> } = { buf: [], endWaiters: [] };
2033
2044
  let pasteLineFired = false; // the line that resolved rl.question came from inside a paste
2034
2045
  if (process.stdin.isTTY) {
2035
- process.stdin.on("keypress", (_ch: string, key: { name?: string } | undefined) => {
2046
+ const pasteKeypressHandler = (_ch: string, key: { name?: string } | undefined) => {
2036
2047
  if (key?.name === "paste-start") { promptPasteActive = true; pasteMerge.buf = []; }
2037
2048
  else if (key?.name === "paste-end") {
2038
2049
  promptPasteActive = false;
2039
2050
  for (const w of pasteMerge.endWaiters.splice(0)) w();
2040
2051
  }
2041
- });
2052
+ };
2053
+ process.stdin.on("keypress", pasteKeypressHandler);
2054
+ promptListenerCleanups.push(() => process.stdin.off("keypress", pasteKeypressHandler));
2042
2055
  // Enable bracketed paste for the REPL lifetime (restored on exit below):
2043
2056
  // terminals only wrap pastes in the 200~/201~ markers once the app opts in.
2044
2057
  process.stdout.write("\x1b[?2004h");
@@ -2540,28 +2553,13 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2540
2553
  const notReadyWarning = (st: { name: string; label: string }): string =>
2541
2554
  ` ! ${st.name} is not call-ready yet (${st.label}) — run /provider login antigravity before the first turn.`;
2542
2555
 
2543
- const CORE_MODEL_ACTION_ROLE_ORDER = ["executor", "architect", "planner", "critic"] as const;
2556
+
2544
2557
  const MODEL_BADGE_ROLE_ORDER = ["planner", "architect", "executor", "critic"] as const;
2545
2558
 
2546
2559
  const roleBadgeColor = (roleId: string): ModelAssignmentBadge["color"] =>
2547
2560
  roleId === "executor" || roleId === "architect" || roleId === "planner" || roleId === "critic" ? roleId : "critic";
2548
2561
 
2549
- const orderedModelRoles = (config: Awaited<ReturnType<typeof readGlobalConfig>>) => {
2550
- const roles = allSubagentRoles(config);
2551
- const emitted = new Set<string>();
2552
- const out: ReturnType<typeof allSubagentRoles> = [];
2553
- for (const id of CORE_MODEL_ACTION_ROLE_ORDER) {
2554
- const role = roles.find(r => r.id === id);
2555
- if (role) {
2556
- emitted.add(role.id);
2557
- out.push(role);
2558
- }
2559
- }
2560
- for (const role of roles) {
2561
- if (!emitted.has(role.id)) out.push(role);
2562
- }
2563
- return out;
2564
- };
2562
+
2565
2563
 
2566
2564
  const modelPickerAssignments = async (): Promise<ModelAssignmentBadge[]> => {
2567
2565
  const cfg = await readGlobalConfig();
@@ -2732,7 +2730,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2732
2730
  choices.push({
2733
2731
  value: "heading:default",
2734
2732
  label: "Set as DEFAULT (Default)",
2735
- hint: `${config.defaultModel} (${currentDefaultThinking})`,
2733
+ hint: `${config.defaultModel} (${currentDefaultThinking}) · roles → /agents`,
2736
2734
  disabled: true,
2737
2735
  });
2738
2736
  appendChildren([
@@ -2744,73 +2742,21 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2744
2742
  })),
2745
2743
  ]);
2746
2744
 
2747
- for (const role of orderedModelRoles(config)) {
2748
- const roleThinking = resolveSubagentThinking(role.id, config) ?? "inherit";
2749
- choices.push({
2750
- value: `heading:${role.id}`,
2751
- label: `Set as ${role.title.toUpperCase()} (${role.title})`,
2752
- hint: `${resolveSubagentModel(role.id, config)} (${roleThinking})`,
2753
- disabled: true,
2754
- });
2755
- appendChildren([
2756
- { value: `${role.id}:keep`, label: "Set model only", hint: `keep thinking ${roleThinking} · set via /agents edit` },
2757
- ]);
2758
- }
2759
-
2760
- choices.push({
2761
- value: "preset:openai-codex",
2762
- label: "Apply OpenAI Codex role preset",
2763
- hint: "Default medium · Executor low · Architect xhigh · Planner medium · Critic high",
2764
- });
2765
2745
  return choices;
2766
2746
  };
2767
2747
 
2768
- const applyOpenAiCodexRolePreset = async (target: string, cfgForPick: Awaited<ReturnType<typeof readGlobalConfig>>): Promise<void> => {
2769
- const roleThinking: Record<(typeof CORE_MODEL_ACTION_ROLE_ORDER)[number], ThinkLevel> = {
2770
- executor: "low",
2771
- architect: "xhigh",
2772
- planner: "medium",
2773
- critic: "high",
2774
- };
2775
- await saveConfigPatch(raw => {
2776
- let subagents = raw.subagents ?? {};
2777
- for (const roleId of CORE_MODEL_ACTION_ROLE_ORDER) {
2778
- subagents = withSubagentSetting({ subagents }, roleId, { model: target, thinking: roleThinking[roleId] });
2779
- }
2780
- return {
2781
- ...rememberModelPatch(raw, target),
2782
- thinkingLevel: "medium",
2783
- subagents,
2784
- };
2785
- });
2786
- sessionModel = target;
2787
- sessionThinking = "medium";
2788
- const { resolved, provider } = await describeModel(target);
2789
- const st = (await describeAllProviders(cfgForPick)).find(s => s.name === provider);
2790
- console.log(`OpenAI Codex role preset applied to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })} — Default medium, Executor low, Architect xhigh, Planner medium, Critic high`);
2791
- };
2748
+
2792
2749
 
2793
2750
 
2794
2751
  const applyPickedModelWithTarget = async (target: string): Promise<boolean> => {
2795
2752
  if (!process.stdin.isTTY || !process.stdout.isTTY) return false;
2796
2753
  const cfgForPick = await readGlobalConfig();
2754
+ // `/model` only assigns the DEFAULT model + (optionally) the default thinking.
2755
+ // Per-role model and thinking are configured in /agents (and /agents edit).
2797
2756
  const choice = await pickFromOptions(`Model Name: ${displayModelName(target)}\n\nAction for: ${target}`, modelActionChoices(cfgForPick)) ?? "default:keep";
2798
- if (choice === "preset:openai-codex") {
2799
- await applyOpenAiCodexRolePreset(target, cfgForPick);
2800
- return true;
2801
- }
2802
- const [applyTo, action = "keep"] = choice.split(":", 2);
2803
- if (applyTo === "heading") return false;
2804
- const roleTarget = applyTo !== "default" ? getSubagentRole(applyTo, cfgForPick) : undefined;
2757
+ const [, action = "keep"] = choice.split(":", 2);
2805
2758
  const { resolved, provider } = await describeModel(target);
2806
2759
  const st = (await describeAllProviders(cfgForPick)).find(s => s.name === provider);
2807
- if (roleTarget) {
2808
- const thinkPatch = action === "inherit" ? { thinking: undefined } : isThinkingLevel(action) ? { thinking: action } : {};
2809
- await saveConfigPatch(raw => ({ subagents: withSubagentSetting(raw, roleTarget.id, { model: target, ...thinkPatch }) }));
2810
- const thinkNote = action !== "keep" ? ` · thinking ${action}` : "";
2811
- console.log(`Subagent '${roleTarget.id}' model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })}${thinkNote} — saved (change anytime via /model or /agents)`);
2812
- return true;
2813
- }
2814
2760
  sessionModel = target;
2815
2761
  const defaultThinking = isThinkingLevel(action) ? action : undefined;
2816
2762
  if (defaultThinking) {
@@ -2820,7 +2766,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2820
2766
  ...rememberModelPatch(raw, target),
2821
2767
  ...(defaultThinking ? { thinkingLevel: defaultThinking } : {}),
2822
2768
  }));
2823
- console.log(`Model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })}${defaultThinking ? ` · thinking ${defaultThinking}` : ""} — saved as default`);
2769
+ console.log(`Model set to ${formatModelLine({ label: target, resolved, provider, ready: st?.ready })}${defaultThinking ? ` · thinking ${defaultThinking}` : ""} — saved as default. Role models/thinking: /agents`);
2824
2770
  return true;
2825
2771
  };
2826
2772
 
@@ -2925,7 +2871,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
2925
2871
 
2926
2872
  if (previewEnabled) {
2927
2873
  process.once("exit", () => out.write("\x1b[?25h")); // safety net: never leave the cursor hidden
2928
- process.stdin.on("keypress", (_ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
2874
+ const footerKeypressHandler = (_ch: string, key: { name?: string; ctrl?: boolean; meta?: boolean } | undefined) => {
2929
2875
  if (key?.ctrl && key.name === "c") {
2930
2876
  forceExitFromCtrlC();
2931
2877
  return;
@@ -3040,18 +2986,22 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3040
2986
  drawFooter(previewLines(typedLine));
3041
2987
  } catch { /* ignore render races */ }
3042
2988
  });
3043
- });
2989
+ };
2990
+ process.stdin.on("keypress", footerKeypressHandler);
2991
+ promptListenerCleanups.push(() => process.stdin.off("keypress", footerKeypressHandler));
3044
2992
  // Idle-prompt resize: re-reserve the footer at the new terminal height so the
3045
2993
  // fixed reservation stays accurate (otherwise the next paint would target the
3046
2994
  // old row count and either over-shoot or under-paint the reserved region).
3047
- process.stdout.on("resize", () => {
2995
+ const idleResizeHandler = () => {
3048
2996
  if (!previewArmed) return;
3049
2997
  try {
3050
2998
  disarmPreview();
3051
2999
  armPreview();
3052
3000
  drawFooter(promptHistoryLines ? historyPreviewLines(promptHistoryLines) : previewLines(typedLine, navIdx));
3053
3001
  } catch { /* ignore resize render races */ }
3054
- });
3002
+ };
3003
+ process.stdout.on("resize", idleResizeHandler);
3004
+ promptListenerCleanups.push(() => process.stdout.off("resize", idleResizeHandler));
3055
3005
  }
3056
3006
 
3057
3007
  while (true) {
@@ -3162,7 +3112,14 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3162
3112
  }
3163
3113
  if (input === "/clear") {
3164
3114
  history.length = 1;
3165
- console.log("(history cleared)");
3115
+ // Back to the initial screen: wipe the conversation, clear the terminal +
3116
+ // scrollback, and re-render the welcome banner so /clear looks like a fresh launch.
3117
+ if (process.stdout.isTTY) {
3118
+ disarmPreview();
3119
+ process.stdout.write("\x1b[2J\x1b[3J\x1b[H"); // clear screen + scrollback + cursor home
3120
+ console.log(renderWelcome(welcomeData).join("\n"));
3121
+ }
3122
+ console.log("(history cleared — back to the start screen)");
3166
3123
  continue;
3167
3124
  }
3168
3125
  if (input === "/compact") {
@@ -3281,6 +3238,20 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
3281
3238
  if (history[k]!.role === "assistant" && !lastReply) lastReply = String(history[k]!.content ?? "");
3282
3239
  if (lastUserInput && lastReply) break;
3283
3240
  }
3241
+ // Seed readline's input history so ↑ in the prompt recalls THIS session's
3242
+ // prior prompts (not just lines typed in the current run). readline history
3243
+ // is newest-first; unshift in chronological order so the session's newest
3244
+ // prompt lands at the front (first ↑). Skip injected/framed messages.
3245
+ const rli = rl as unknown as { history?: string[] };
3246
+ if (Array.isArray(rli.history)) {
3247
+ const priorPrompts = history
3248
+ .filter(m => m.role === "user")
3249
+ .map(m => String(m.content ?? "").trim())
3250
+ .filter(c => c && !c.startsWith("Tool [") && !c.startsWith("[mid-turn steering") && !c.startsWith("[Earlier conversation summary]"));
3251
+ for (const p of priorPrompts) {
3252
+ if (rli.history[0] !== p) rli.history.unshift(p);
3253
+ }
3254
+ }
3284
3255
  const sep = "─".repeat(Math.min(48, Math.max(20, (process.stdout.columns ?? 80) - 1)));
3285
3256
  logLines([
3286
3257
  sep,
@@ -4359,6 +4330,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4359
4330
  } catch { /* best effort */ }
4360
4331
  process.removeListener("SIGINT", forceExitFromCtrlC);
4361
4332
  process.stdin.off("data", forceExitOnCtrlCByte);
4333
+ drainPromptListeners();
4362
4334
  restorePromptRawMode();
4363
4335
  process.exit(130);
4364
4336
  }
@@ -4374,6 +4346,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
4374
4346
  if (sessionId && !flags.noSession) console.log(formatResumeHint(sessionId));
4375
4347
  process.removeListener("SIGINT", forceExitFromCtrlC);
4376
4348
  process.stdin.off("data", forceExitOnCtrlCByte);
4349
+ drainPromptListeners();
4377
4350
  restorePromptRawMode();
4378
4351
  gracefulReadlineClose = true;
4379
4352
  rl.close();