jeo-code 0.4.6 → 0.4.8

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.
@@ -1,4 +1,6 @@
1
1
  import chalk from "chalk";
2
+ import { padLineTo } from "./layout";
3
+ import { visibleWidth } from "./color";
2
4
 
3
5
  export interface TodoCardItem {
4
6
  title: string;
@@ -8,40 +10,69 @@ export interface TodoCardItem {
8
10
  export interface TodoCardOptions {
9
11
  unicode?: boolean;
10
12
  color?: boolean;
13
+ /** Faint card background tint painter — gives the card a panel look so it reads
14
+ * as a distinct block. Identity when absent (or colorless). */
15
+ fill?: (s: string) => string;
16
+ /** Muted foreground painter for secondary text (done/pending labels, count,
17
+ * tree connectors). Replaces `chalk.dim`, which washes out on dark terminals. */
18
+ muted?: (s: string) => string;
19
+ /** Accent painter for the in_progress (active) item — the theme's highlight hue,
20
+ * applied bold. Defaults to cyan so the active row matches the rest of the theme
21
+ * instead of a hardcoded color that clashes on warm/green/red palettes. */
22
+ accent?: (s: string) => string;
23
+ /** Panel width: lines pad to this so the fill spans a clean rectangle. Clamped
24
+ * to [20,120]; defaults to the longest content row + 2. */
25
+ width?: number;
11
26
  }
12
27
 
13
28
  /**
14
29
  * jeo-ref "Todo Write" scrollback card: a ✓-led header with a tree-connector
15
30
  * checklist — done items get ☑ + strikethrough, the active item highlights,
16
- * pending stays dim. Pure `string[]`; the caller flushes it into the ledger so
17
- * the plan's evolution (items checking off turn by turn) reads as transcript
18
- * history, exactly like the reference TUI.
31
+ * pending stays muted. A faint background tint (when a `fill` painter is given)
32
+ * makes the whole block read as a panel; secondary text uses a real muted hue
33
+ * instead of ANSI dim so completed rows stay legible. Pure `string[]`; the caller
34
+ * flushes it into the ledger so the plan's evolution reads as transcript history.
19
35
  */
20
36
  export function formatTodoWriteCard(items: TodoCardItem[], opts: TodoCardOptions = {}): string[] {
21
37
  if (items.length === 0) return [];
22
38
  const unicode = opts.unicode !== false;
23
39
  const color = opts.color !== false;
40
+ const fill = opts.fill ?? ((s: string) => s);
41
+ const muted = color ? (opts.muted ?? chalk.dim) : (s: string) => s;
42
+ const active = color ? (opts.accent ? (s: string) => chalk.bold(opts.accent!(s)) : chalk.cyan.bold) : (s: string) => s;
24
43
  const check = unicode ? "✓" : "v";
25
44
  const boxDone = unicode ? "☑" : "[x]";
26
45
  const boxOpen = unicode ? "☐" : "[ ]";
27
46
  const tee = unicode ? "├─" : "|-";
28
47
  const ell = unicode ? "└─" : "`-";
29
48
  const count = `${items.length} task${items.length === 1 ? "" : "s"}`;
30
- const head = color
31
- ? `${chalk.green(check)} ${chalk.bold("Todo Write")} ${chalk.dim(count)}`
32
- : `${check} Todo Write ${count}`;
33
- const lines = [head];
49
+
50
+ const rows: string[] = [];
51
+ rows.push(
52
+ color
53
+ ? `${chalk.green(check)} ${chalk.bold("Todo Write")} ${muted(count)}`
54
+ : `${check} Todo Write ${count}`,
55
+ );
34
56
  items.forEach((item, i) => {
35
- const conn = i === items.length - 1 ? ell : tee;
57
+ const conn = color ? muted(i === items.length - 1 ? ell : tee) : i === items.length - 1 ? ell : tee;
36
58
  if (item.status === "done") {
37
59
  const box = color ? chalk.green(boxDone) : boxDone;
38
- const label = color ? chalk.dim.strikethrough(item.title) : item.title;
39
- lines.push(` ${conn} ${box} ${label}`);
60
+ const label = color ? chalk.strikethrough(muted(item.title)) : item.title;
61
+ rows.push(` ${conn} ${box} ${label}`);
40
62
  } else if (item.status === "in_progress") {
41
- lines.push(` ${conn} ${boxOpen} ${color ? chalk.cyan.bold(item.title) : item.title}`);
63
+ rows.push(` ${conn} ${boxOpen} ${active(item.title)}`);
42
64
  } else {
43
- lines.push(` ${conn} ${boxOpen} ${color ? chalk.dim(item.title) : item.title}`);
65
+ rows.push(` ${conn} ${boxOpen} ${color ? muted(item.title) : item.title}`);
44
66
  }
45
67
  });
46
- return lines;
68
+
69
+ // Panel: pad each row (with a 1-col left gutter) to a common width and tint the
70
+ // whole rectangle — only when the caller opts in via `fill`/`width`; otherwise the
71
+ // bare tree-checklist rows return unchanged (back-compat). visibleWidth ignores
72
+ // ANSI, and every painter above closes with a TARGETED reset (never `\x1b[0m`), so
73
+ // the background spans the full row.
74
+ if (!opts.fill && opts.width === undefined) return rows;
75
+ const contentWidth = rows.reduce((m, r) => Math.max(m, visibleWidth(r)), 0) + 2;
76
+ const panelWidth = Math.max(20, Math.min(120, opts.width ?? contentWidth));
77
+ return rows.map(r => fill(padLineTo(` ${r}`, panelWidth, "left")));
47
78
  }
@@ -134,6 +134,57 @@ export function truncateToWidth(s: string, cols: number): string {
134
134
  return out;
135
135
  }
136
136
 
137
+ /**
138
+ * Strip control bytes that would corrupt the live differential frame. KEEPS SGR color
139
+ * escapes (`\x1b[…m`); DROPS every other CSI (cursor moves, EL/ED erase, etc.), OSC
140
+ * sequences, other escapes, and bare C0/C1 control bytes except tab/newline; and DROPS
141
+ * an INCOMPLETE trailing escape (a chunk that ends mid-sequence) so it can never eat the
142
+ * next line's `\x1b[2K`. Used to sanitize raw child stdout (e.g. a streaming `bun test`
143
+ * with `\r\x1b[2K` progress lines) before it enters the frame — the torn-escape /
144
+ * cursor-hijack class of screen corruption.
145
+ */
146
+ export function sanitizeForFrame(s: string): string {
147
+ if (!s.includes("\x1b") && !/[\x00-\x08\x0b-\x1f\x7f]/.test(s)) return s; // fast path
148
+ let out = "";
149
+ let i = 0;
150
+ const n = s.length;
151
+ while (i < n) {
152
+ const ch = s[i]!;
153
+ if (ch === "\x1b") {
154
+ if (s[i + 1] === "[") {
155
+ // CSI: ESC [ params (0-9;:?<>= space) final (@-~)
156
+ let j = i + 2;
157
+ while (j < n && /[0-9;:?<>= ]/.test(s[j]!)) j++;
158
+ if (j < n && /[@-~]/.test(s[j]!)) {
159
+ const seq = s.slice(i, j + 1);
160
+ if (seq.endsWith("m")) out += seq; // keep SGR color, drop all other CSI
161
+ i = j + 1;
162
+ continue;
163
+ }
164
+ break; // incomplete CSI at the chunk tail → drop the rest
165
+ }
166
+ if (s[i + 1] === "]") {
167
+ // OSC: ESC ] … (BEL | ST = ESC \)
168
+ let j = i + 2;
169
+ while (j < n && s[j] !== "\x07" && !(s[j] === "\x1b" && s[j + 1] === "\\")) j++;
170
+ if (j >= n) break; // incomplete OSC → drop
171
+ i = s[j] === "\x07" ? j + 1 : j + 2;
172
+ continue;
173
+ }
174
+ i += i + 1 < n ? 2 : 1; // other ESC x → drop ESC (+ its single byte)
175
+ continue;
176
+ }
177
+ const code = ch.charCodeAt(0);
178
+ if ((code < 0x20 && code !== 0x09 && code !== 0x0a) || code === 0x7f) {
179
+ i++; // strip bare control bytes (CR/BS/…), keep tab + newline
180
+ continue;
181
+ }
182
+ out += ch;
183
+ i++;
184
+ }
185
+ return out;
186
+ }
187
+
137
188
  /**
138
189
  * Hard-wrap text to `cols` display columns, breaking long words and preserving
139
190
  * existing newlines. SGR-aware (escapes don't consume width). Returns the wrapped
@@ -51,17 +51,22 @@ export class Renderer {
51
51
  this.prevCols = currentCols;
52
52
 
53
53
  const next = lines.map(line => truncate(line, currentCols));
54
+ // Rows physically occupied by the prior frame — or recorded by reset() when the
55
+ // baseline was dropped WITHOUT clearing the screen. The diff below EL-clears any
56
+ // of these that the new (possibly shorter) frame does not cover, and the reserve
57
+ // block below uses it so a post-reset repaint does not spuriously re-scroll.
58
+ const occupied = Math.max(this.prev.length, this.coverRows);
54
59
  const maxLen = Math.max(this.prev.length, next.length, this.coverRows);
55
60
  this.coverRows = 0;
56
61
  let cursorRow = 0;
57
62
  let out = "";
58
63
 
59
- if (this.reserve && next.length > this.prev.length && next.length <= Math.max(1, size().rows)) {
64
+ if (this.reserve && next.length > occupied && next.length <= Math.max(1, size().rows)) {
60
65
  // The cursor rests on the frame's first row (the anchor). Walk to the last
61
66
  // currently-occupied row, emit one newline per missing row (scrolling the
62
67
  // viewport when at the bottom margin), then hop back up to the — possibly
63
68
  // shifted — anchor so the diff below paints at stable relative positions.
64
- const have = Math.max(this.prev.length, 1);
69
+ const have = Math.max(occupied, 1);
65
70
  out += cursorDown(have - 1) + "\n".repeat(next.length - have) + cursorUp(next.length - 1) + toColumn(1);
66
71
  }
67
72
 
@@ -92,16 +97,17 @@ export class Renderer {
92
97
  }
93
98
  out += toColumn(1);
94
99
 
95
- // Close the synchronized update opened by insertAbove() now that the full
96
- // repaint is in the same buffered stream the terminal presents the overwritten
97
- // first row(s), the flushed ledger line, and the repainted frame as ONE atomic update.
98
- if (this.syncOpen) {
99
- out += END_SYNC;
100
- this.syncOpen = false;
101
- }
102
-
100
+ // Atomic present (DECSET-2026 synchronized update): wrap the WHOLE repaint so the
101
+ // terminal never shows a half-painted frameno torn row, no transient duplicate
102
+ // bar a mid-repaint snapshot could catch. An insertAbove() may have already opened
103
+ // the update (syncOpen); otherwise this render opens its own. Exactly one BSU/ESU
104
+ // pair is emitted per write.
103
105
  if (out.length > 0) {
104
- this.write(out);
106
+ this.write((this.syncOpen ? "" : BEGIN_SYNC) + out + END_SYNC);
107
+ this.syncOpen = false;
108
+ } else if (this.syncOpen) {
109
+ this.write(END_SYNC);
110
+ this.syncOpen = false;
105
111
  }
106
112
 
107
113
  this.prev = next;
@@ -137,7 +143,13 @@ export class Renderer {
137
143
  // clamped cursor-down desynced the row bookkeeping — each subsequent frame then
138
144
  // painted one row higher, devouring the flushed scrollback content above (the
139
145
  // "truncated card" corruption).
140
- const stale = this.prev.length - written;
146
+ // Use the same occupancy measure the reserve block uses (max of prev.length and
147
+ // coverRows). A reset() between frames drops prev but records coverRows; ignoring it
148
+ // here left the old frame's lower rows uncleared and the cursor below the true
149
+ // anchor, so the next render's cursorDown crossed the bottom margin and clamped —
150
+ // the persistent off-by-one that duplicated the model bar.
151
+ const occupied = Math.max(this.prev.length, this.coverRows);
152
+ const stale = occupied - written;
141
153
  if (stale > 0) {
142
154
  for (let i = 0; i < stale; i++) {
143
155
  out += toColumn(1) + clearLine() + (i < stale - 1 ? cursorDown(1) : "");
@@ -146,6 +158,7 @@ export class Renderer {
146
158
  }
147
159
  this.write(out);
148
160
  this.prev = [];
161
+ this.coverRows = 0; // consumed: the frame below is now the single source of truth
149
162
  }
150
163
 
151
164
  /** Clear the live frame. Inline (reserve) mode walks the known frame rows with
@@ -174,6 +187,19 @@ export class Renderer {
174
187
  }
175
188
 
176
189
  reset(): void {
190
+ // Drop the diff baseline so the next render() repaints every line — but REMEMBER
191
+ // how many rows are physically on screen so that repaint also EL-clears any the
192
+ // new (possibly shorter) frame doesn't cover. Without this, a self-heal reset on a
193
+ // frame that just shrank left stale rows behind (duplicate model bars / orphaned
194
+ // borders — the live-analysis screen corruption).
195
+ this.coverRows = Math.max(this.coverRows, this.prev.length);
177
196
  this.prev = [];
197
+ // Close any synchronized update opened by a preceding insertAbove() so a reset()
198
+ // landing between insertAbove() and the next render() cannot strand an open BSU
199
+ // window (which times out ~150ms later and flashes a partial frame).
200
+ if (this.syncOpen) {
201
+ this.write(END_SYNC);
202
+ this.syncOpen = false;
203
+ }
178
204
  }
179
205
  }
@@ -1,5 +1,9 @@
1
1
  import pkg from "../../package.json";
2
2
  import { compareVersions } from "../commands/update";
3
+ import * as os from "node:os";
4
+ import * as path from "node:path";
5
+ import { readFile, writeFile, mkdir } from "node:fs/promises";
6
+ import { jeoEnv } from "./env";
3
7
 
4
8
  export interface UpdateCheckResult {
5
9
  current: string;
@@ -62,3 +66,52 @@ export async function checkForUpdate(deps?: UpdateCheckDeps): Promise<UpdateChec
62
66
  return null;
63
67
  }
64
68
  }
69
+
70
+ // ---- Update-check disk cache ------------------------------------------------
71
+ // The live npm check often loses the startup race, so the "New version" banner
72
+ // rarely shows even when an update exists. Persisting the last-known-latest lets
73
+ // the NEXT launch render the banner INSTANTLY from disk (and offline), while a
74
+ // background refresh keeps the cache fresh. Mirrors gjc / npm update notices.
75
+
76
+ interface CachedUpdateCheck {
77
+ latest: string;
78
+ checkedAt: number;
79
+ }
80
+
81
+ function updateCacheDir(): string {
82
+ return jeoEnv("CONFIG_DIR") || path.join(os.homedir(), ".jeo");
83
+ }
84
+
85
+ function updateCachePath(): string {
86
+ return path.join(updateCacheDir(), "update-check.json");
87
+ }
88
+
89
+ /** Last-known-latest from disk, re-evaluated against the CURRENT local version
90
+ * (so an interim upgrade clears the banner). Null when no/invalid cache. */
91
+ export async function readUpdateCache(localVersion: string = pkg.version): Promise<UpdateCheckResult | null> {
92
+ if (typeof localVersion !== "string" || !localVersion) return null;
93
+ try {
94
+ const raw = await readFile(updateCachePath(), "utf-8");
95
+ const data = JSON.parse(raw) as Partial<CachedUpdateCheck>;
96
+ if (!data || typeof data.latest !== "string" || !data.latest) return null;
97
+ return {
98
+ current: localVersion,
99
+ latest: data.latest,
100
+ updateAvailable: compareVersions(localVersion, data.latest) < 0,
101
+ };
102
+ } catch {
103
+ return null;
104
+ }
105
+ }
106
+
107
+ /** Persist the latest version for the next launch (best-effort; never throws). */
108
+ export async function writeUpdateCache(latest: string): Promise<void> {
109
+ if (typeof latest !== "string" || !latest) return;
110
+ try {
111
+ await mkdir(updateCacheDir(), { recursive: true, mode: 0o700 });
112
+ const payload: CachedUpdateCheck = { latest, checkedAt: Date.now() };
113
+ await writeFile(updateCachePath(), JSON.stringify(payload, null, 2), { encoding: "utf-8", mode: 0o600 });
114
+ } catch {
115
+ // Cache is an optimization; a write failure must never break launch.
116
+ }
117
+ }
@@ -1,52 +0,0 @@
1
- import { runAgentLoop, executorSystemPrompt, DEFAULT_TOOLS } from "../agent/engine";
2
- import { loadSkills, buildSkillTask, getSkillFrom } from "../skills/catalog";
3
- import { readGlobalConfig, isDevMode } from "../agent/state";
4
- import { runPostImplementationHooks } from "../agent/hooks";
5
-
6
- export async function runGjcCommand(args: string[]): Promise<void> {
7
- const intent = args.join(" ").trim();
8
- const config = await readGlobalConfig();
9
-
10
- if (intent.toLowerCase().includes("self-improve") && !isDevMode()) {
11
- console.error("Error: Self-improvement tasks are only allowed in JEO_DEV_MODE=1.");
12
- process.exit(1);
13
- }
14
-
15
- const model = config.defaultModel || "fast";
16
- const skills = await loadSkills();
17
- const gjcSkill = getSkillFrom(skills, "gjc");
18
-
19
- if (!gjcSkill) {
20
- console.error("Error: gjc skill not found.");
21
- process.exit(1);
22
- }
23
-
24
- // Correct signature: executorSystemPrompt(role?, protocol?, verificationDirective?)
25
- const systemPrompt = executorSystemPrompt();
26
- const task = buildSkillTask(gjcSkill, intent);
27
-
28
- await runAgentLoop([{ role: "user", content: task }], {
29
- cwd: process.cwd(),
30
- systemPrompt,
31
- model,
32
- tools: DEFAULT_TOOLS,
33
- maxSteps: 50,
34
- });
35
-
36
- console.log("\n[jeo] Verifying implementation...");
37
- const verify = await runPostImplementationHooks(process.cwd(), intent);
38
-
39
- if (!verify.success) {
40
- console.error("\n[jeo] Verification FAILED. Auto-repairing...");
41
- const repairTask = `Previous implementation failed verification.\nErrors:\n${verify.output}\n\nPlease fix.`;
42
- await runAgentLoop([{ role: "user", content: repairTask }], {
43
- cwd: process.cwd(),
44
- systemPrompt,
45
- model,
46
- tools: DEFAULT_TOOLS,
47
- maxSteps: 30,
48
- });
49
- } else {
50
- console.log("\n[jeo] Verification SUCCESSFUL.");
51
- }
52
- }
@@ -1,31 +0,0 @@
1
- <!-- Parent: ../AGENTS.md -->
2
- <!-- Generated: 2026-06-11 | Updated: 2026-06-11 -->
3
-
4
- # gjc
5
-
6
- ## Purpose
7
- Bundled SKILL.md for the `gjc` workflow.
8
-
9
- ## Key Files
10
- | File | Description |
11
- |------|-------------|
12
- | `SKILL.md` | The primary skill definition |
13
-
14
- ## Subdirectories
15
- *(None)*
16
-
17
- ## For AI Agents
18
-
19
- ### Working In This Directory
20
- - Do not modify this without understanding the broader workflow implications.
21
-
22
- ### Testing Requirements
23
- - N/A
24
-
25
- ### Common Patterns
26
- *(None)*
27
-
28
- ## Dependencies
29
- *(None)*
30
-
31
- <!-- MANUAL: -->
@@ -1,15 +0,0 @@
1
- ---
2
- description: Main implementation process using gjc spec-first workflow.
3
- command: jeo gjc "<request>"
4
- when: When you need to perform significant code changes, refactoring, or feature development using the core gjc process.
5
- ---
6
-
7
- # gjc (Gajae-Code Process)
8
-
9
- Executes the primary code implementation workflow by leveraging the underlying `gjc` engine (jeo-claw).
10
- This process manages the heavy lifting of code transformation, while the surrounding `jeo-code` ecosystem handles loop-level orchestration.
11
-
12
- 1. **Mutation Guard**: Ensures safe code writes by blocking operations until ambiguity is low (≤ 20%).
13
- 2. **Role Delegation**: Utilizes specialized subagents (architect, planner, executor, critic) for focused tasks.
14
- 3. **Loop Control**: Maintains a tight feedback loop between planning and execution.
15
- 4. **Verification**: Automatically runs verification steps after implementation to ensure correctness.