jeo-code 0.6.3 → 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.
@@ -0,0 +1,102 @@
1
+ import chalk from "chalk";
2
+ import { type AgentLoopEvents } from "../../agent/engine";
3
+ import { type TaskSubEvent } from "../../agent/task-tool";
4
+ import { categoryBadge } from "../../tui/components/category-index";
5
+ import { summarizeForgeInvocation } from "../../tui/components/forge";
6
+ import { formatDuration, formatUsage } from "../../tui/components/duration";
7
+ import { LaunchTui } from "../../tui/app";
8
+
9
+ const GATED_OUTPUT_METHODS = new Set(["write", "cursorTo", "moveCursor", "clearLine", "clearScreenDown", "_write", "_writev"]);
10
+ export function gatedStdout(real: NodeJS.WriteStream, gated: () => boolean): NodeJS.WriteStream {
11
+ return new Proxy(real, {
12
+ get(target, prop, _receiver) {
13
+ if (typeof prop === "string" && GATED_OUTPUT_METHODS.has(prop)) {
14
+ return (...args: any[]) => {
15
+ if (gated()) {
16
+ const cb = args[args.length - 1]; // honor readline's write callback so it never stalls
17
+ if (typeof cb === "function") cb();
18
+ return true;
19
+ }
20
+ return (target as any)[prop](...args);
21
+ };
22
+ }
23
+ const value = Reflect.get(target, prop, target);
24
+ return typeof value === "function" ? value.bind(target) : value;
25
+ },
26
+ }) as unknown as NodeJS.WriteStream;
27
+ }
28
+
29
+ function firstOutputLine(output: string | undefined): string {
30
+ if (!output) return "";
31
+ const line = String(output)
32
+ .split("\n")
33
+ .map(l => l.trim())
34
+ .find(l => l.length > 0);
35
+ return line ? line.replace(/\s+/g, " ").slice(0, 140) : "";
36
+ }
37
+
38
+ function streamResultSuffix(tool: string, ok: boolean, output: string | undefined): string {
39
+ const summary = firstOutputLine(output);
40
+ if (!summary) return "";
41
+ if (!ok || tool === "task") return ` — ${summary}`;
42
+ return "";
43
+ }
44
+
45
+ export function formatTaskSubEvent(e: TaskSubEvent): string {
46
+ const role = e.role || "subagent";
47
+ const roleLabel = e.index && e.total ? `${role.toUpperCase()}[${e.index}/${e.total}]` : role.toUpperCase();
48
+ const tokTag = e.tokens ? ` (${e.tokens.input + e.tokens.output} tok)` : "";
49
+ const detail = firstOutputLine(e.detail);
50
+ const summary = e.summary ? ` — ${e.summary}` : "";
51
+ const badge = categoryBadge("subagent");
52
+ if (e.kind === "start") return `${badge} ${chalk.magenta(`▸ ${roleLabel}`)} · ${detail}`.slice(0, 240);
53
+ if (e.kind === "step") return ` ${badge} ${chalk.cyan(`├─ ${roleLabel}`)} · ${detail || "working"}`;
54
+ if (e.kind === "tool") return ` ${badge} ${e.success === false ? chalk.red("├─") : chalk.green("├─")} ${roleLabel} ${e.success === false ? chalk.red("✗") : chalk.green("✓")} ${detail || "tool"}${summary}`;
55
+ if (e.kind === "error") return ` ${badge} ${chalk.red("├─")} ${roleLabel} ${chalk.red("✗")} ${detail || "error"}`;
56
+ return `${badge} ${e.success === false ? chalk.red("└─") : chalk.green("└─")} ${roleLabel} done${tokTag}${e.success === false ? " (incomplete)" : ""}${detail ? `: ${detail}` : ""}`;
57
+ }
58
+
59
+ export function logTaskSubEvent(e: TaskSubEvent, log: (line: string) => void = (s: string) => console.log(s)): void {
60
+ log(formatTaskSubEvent(e));
61
+ }
62
+
63
+ export function createStreamEvents(
64
+ _maxSteps: number,
65
+ log: (line: string) => void = (s: string) => console.log(s),
66
+ now: () => number = Date.now,
67
+ ): AgentLoopEvents {
68
+ let pending = "";
69
+ let latestUsage: { inputTokens: number; outputTokens: number } | undefined;
70
+ const startTime = now();
71
+
72
+ return {
73
+ onStep: () => {},
74
+ onAssistant: (_raw: string, invocation: { tool?: string; arguments?: unknown } | null) => {
75
+ const tool = typeof invocation?.tool === "string" ? invocation.tool.trim() : "";
76
+ if (!tool || tool === "done") return;
77
+ pending = summarizeForgeInvocation(tool, invocation?.arguments).title;
78
+ const elapsedMs = now() - startTime;
79
+ let suffix = "";
80
+ if (elapsedMs >= 1000) suffix += ` · ${formatDuration(elapsedMs)}`;
81
+ if (latestUsage) suffix += ` · ${formatUsage(latestUsage)}`;
82
+ log(`${categoryBadge("progress")} ${pending}${suffix ? chalk.dim(suffix) : ""}`);
83
+ },
84
+ onToolResult: (tool: string, ok: boolean, output?: string) => {
85
+ const label = pending || tool;
86
+ const mark = ok ? chalk.green("✓") : chalk.red("✗");
87
+ log(` ${categoryBadge(ok ? "done" : "error")} ${mark} ${label}${streamResultSuffix(tool, ok, output)}`);
88
+ pending = "";
89
+ },
90
+ onNotice: (msg: string) => log(` ${categoryBadge("progress")} ${chalk.yellow(msg)}`),
91
+ onBudget: (_limit: number, reason: string) => {
92
+ log(` ${categoryBadge("progress")} ${chalk.yellow(reason)}`);
93
+ },
94
+ onUsage: (usage: { inputTokens: number; outputTokens: number }) => {
95
+ latestUsage = usage;
96
+ },
97
+ };
98
+ }
99
+
100
+ export function shouldUseOneShotTui(noTui: boolean): boolean {
101
+ return LaunchTui.usable(noTui);
102
+ }
@@ -0,0 +1,227 @@
1
+ import * as path from "node:path";
2
+ import * as fs from "node:fs";
3
+ import { type LaunchFlags } from "./flags";
4
+
5
+ function hashString(input: string): string {
6
+ let hash = 2166136261;
7
+ for (let i = 0; i < input.length; i++) {
8
+ hash ^= input.charCodeAt(i);
9
+ hash = Math.imul(hash, 16777619);
10
+ }
11
+ return (hash >>> 0).toString(36).padStart(6, "0").slice(0, 6);
12
+ }
13
+
14
+ function tmuxSafeNamePart(input: string, max = 32): string {
15
+ const safe = input.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || "value";
16
+ if (safe.length <= max) return safe;
17
+ return `${safe.slice(0, Math.max(1, max - 7))}-${hashString(input)}`;
18
+ }
19
+
20
+ function tmuxRuntimeSuffix(flags: LaunchFlags): string {
21
+ const parts: string[] = [];
22
+ if (flags.provider) parts.push(`provider-${flags.provider}`);
23
+ if (flags.model) parts.push(`model-${tmuxSafeNamePart(flags.model)}`);
24
+ else if (flags.modelRole) parts.push(flags.modelRole);
25
+ if (flags.thinking) parts.push(`think-${flags.thinking}`);
26
+ // Only an EXPLICIT --max-steps cap names the session; the dynamic default (0) adds nothing.
27
+ if (flags.maxSteps > 0) parts.push(`steps-${flags.maxSteps}`);
28
+ if (parts.length === 0) return "";
29
+ const joined = parts.join("-");
30
+ const suffix = joined.length <= 72 ? joined : `${joined.slice(0, 65)}-${hashString(joined)}`;
31
+ return `-${suffix}`;
32
+ }
33
+
34
+ /**
35
+ * Base tmux session name for `jeo --tmux`. Keyed on the working DIRECTORY (not just the
36
+ * git branch) so two different projects/worktrees on the same branch (e.g. `main`)
37
+ * never share a base. {@link uniqueTmuxSessionName} then makes each concurrent invocation
38
+ * fully independent, so a second `jeo --tmux` never attaches to (and mirrors) the first.
39
+ */
40
+ export function tmuxSessionName(cwd: string, branch: string, flags: LaunchFlags): string {
41
+ const dirTag = `${tmuxSafeNamePart(path.basename(cwd) || "root", 16)}-${hashString(cwd)}`;
42
+ const base = branch ? `jeo-${branch}-${dirTag}` : `jeo-${dirTag}`;
43
+ return base + tmuxRuntimeSuffix(flags);
44
+ }
45
+
46
+ /**
47
+ * Count uncommitted git entries for the `⑂ <branch> ?N` footer dirty flag (gjc parity).
48
+ * One `git status --porcelain` spawn per CALL; callers invoke it once per turn start, not
49
+ * per render. Returns undefined when not a repo / git absent / clean.
50
+ */
51
+ export function gitDirtyCount(cwd: string): number | undefined {
52
+ try {
53
+ const res = Bun.spawnSync(["git", "status", "--porcelain"], { cwd, stdout: "pipe", stderr: "ignore" });
54
+ if (res.exitCode !== 0) return undefined;
55
+ const n = res.stdout.toString().split("\n").filter(l => l.trim().length > 0).length;
56
+ return n || undefined;
57
+ } catch {
58
+ return undefined;
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Allocate + create an INDEPENDENT tmux session from a base name. Each separate,
64
+ * concurrent `jeo --tmux` invocation gets its OWN session instead of attaching to (and
65
+ * mirroring) one another process already created: try `base`, then `base-2`, `base-3`, …
66
+ * The create itself is the guard, so this is race-safe — two processes starting at the
67
+ * same instant can't both win `base`. `tryCreate` must attempt to create the named session
68
+ * and return `"ok"` (created — it's ours), `"taken"` (name already live / lost the race →
69
+ * try the next suffix), or `"error:<msg>"` (a real failure → abort). Sessions die with
70
+ * their jeo process, so a sequential re-run reuses the clean base; only live overlap is
71
+ * suffixed.
72
+ */
73
+ export type TmuxCreateResult = "ok" | "taken" | `error:${string}`;
74
+ export function allocateTmuxSession(
75
+ base: string,
76
+ tryCreate: (name: string) => TmuxCreateResult,
77
+ ): { name: string } | { error: string } {
78
+ for (let n = 1; n <= 1000; n++) {
79
+ const candidate = n === 1 ? base : `${base}-${n}`;
80
+ const result = tryCreate(candidate);
81
+ if (result === "ok") return { name: candidate };
82
+ if (result === "taken") continue;
83
+ return { error: result.slice("error:".length) };
84
+ }
85
+ return { error: `could not allocate a free tmux session name for ${base} (1000 already live?)` };
86
+ }
87
+ export function shellQuote(arg: string): string {
88
+ return `'${arg.replace(/'/g, `'\\''`)}'`;
89
+ }
90
+
91
+
92
+
93
+
94
+ /**
95
+ * True when `jeo --tmux` runs INSIDE an existing tmux session and should enable
96
+ * session-scoped mouse mode for the CURRENT session: no jeo-owned session is created
97
+ * on this path, so without `mouse on` tmux ignores the wheel entirely and the
98
+ * mid-turn scrollback (ledger lines flushed above the live frame) is unreachable.
99
+ * Skipped for jeo-spawned sessions (JEO_TMUX_LAUNCHED=1 — the creator already set
100
+ * it) and when JEO_TMUX_MOUSE=0 opts out.
101
+ */
102
+ export function shouldEnableCurrentTmuxMouse(env: Record<string, string | undefined>): boolean {
103
+ return !!env.TMUX
104
+ && (env.JEO_TMUX_LAUNCHED ?? env.JEO_TMUX_LAUNCHED) !== "1"
105
+ && (env.JEO_TMUX_MOUSE ?? env.JEO_TMUX_MOUSE) !== "0";
106
+ }
107
+
108
+ /**
109
+ * The runnable command for the INNER `jeo launch` a `--tmux` session executes.
110
+ * Pure — testable. Three runtime shapes:
111
+ * - compiled standalone binary: `argv[1]` is a Bun VIRTUAL path (`/$bunfs/…`)
112
+ * that does not exist on disk; the binary itself (`execPath`) is the
113
+ * entrypoint. Passing the virtual path made the inner command crash on
114
+ * spawn, so `tmux new-session` died instantly and the follow-up attach
115
+ * failed with "can't find session".
116
+ * - source run (`bun src/cli.ts`): re-run the script through the runtime.
117
+ * - anything else (a shim/binary path on disk): run it directly.
118
+ */
119
+ export function tmuxLaunchCommand(argv1: string | undefined, execPath: string, cwd: string): string[] {
120
+ const entrypoint = argv1 ?? "";
121
+ if (entrypoint === "" || entrypoint.startsWith("/$bunfs/") || entrypoint.startsWith("B:\\~BUN\\")) {
122
+ return [execPath];
123
+ }
124
+ const resolved = path.isAbsolute(entrypoint) ? entrypoint : path.resolve(cwd, entrypoint);
125
+ if (/\.(ts|js|mjs)$/.test(entrypoint)) return [execPath, resolved];
126
+ return [resolved];
127
+ }
128
+
129
+ /** One tmux configuration step applied to a jeo-owned session after creation. */
130
+ export interface TmuxProfileCommand {
131
+ description: string;
132
+ args: string[];
133
+ }
134
+
135
+ /**
136
+ * gjc-parity tmux profile for jeo-OWNED sessions (mirrors gjc's
137
+ * `buildGjcTmuxProfileCommands`). Applied right after `new-session`, before attach:
138
+ * - `mouse on` (session-scoped): wheel-up enters copy-mode over the REAL pane
139
+ * history — this is what makes the mid-turn scrollback (ledger lines flushed
140
+ * above the inline live frame) reachable with the mouse wheel. Wheel-down at
141
+ * the bottom drops back out. `JEO_TMUX_MOUSE=0` opts out.
142
+ * - ownership/identity markers (`@jeo-profile`, `@jeo-branch`, `@jeo-project`):
143
+ * lets tooling tell jeo-owned sessions apart from user sessions (gjc parity
144
+ * with `@gjc-*`). Never applied to foreign sessions.
145
+ * - `set-clipboard on` + a readable copy-mode `mode-style`: text selected while
146
+ * wheel-scrolled back is visibly highlighted and lands on the system clipboard
147
+ * (OSC52). `JEO_TMUX_PROFILE=0` opts out of these cosmetic extras while keeping
148
+ * mouse + markers.
149
+ */
150
+ export function tmuxProfileCommands(
151
+ target: string,
152
+ env: Record<string, string | undefined>,
153
+ meta: { branch?: string; project?: string } = {},
154
+ ): TmuxProfileCommand[] {
155
+ const t = `=${target}:`;
156
+ const commands: TmuxProfileCommand[] = [];
157
+ if ((env.JEO_TMUX_MOUSE ?? env.JEO_TMUX_MOUSE) !== "0") {
158
+ commands.push({
159
+ description: "enable tmux mouse scrolling (wheel-up → copy-mode over real history)",
160
+ args: ["set-option", "-t", t, "mouse", "on"],
161
+ });
162
+ }
163
+ commands.push({
164
+ description: "mark jeo tmux ownership",
165
+ args: ["set-option", "-t", t, "@jeo-profile", "1"],
166
+ });
167
+ if (meta.branch) {
168
+ commands.push({
169
+ description: "record jeo branch identity",
170
+ args: ["set-option", "-t", t, "@jeo-branch", meta.branch],
171
+ });
172
+ }
173
+ if (meta.project) {
174
+ commands.push({
175
+ description: "record jeo project identity",
176
+ args: ["set-option", "-t", t, "@jeo-project", meta.project],
177
+ });
178
+ }
179
+ if ((env.JEO_TMUX_PROFILE ?? env.JEO_TMUX_PROFILE) !== "0") {
180
+ commands.push(
181
+ {
182
+ description: "enable tmux clipboard integration",
183
+ args: ["set-option", "-t", t, "set-clipboard", "on"],
184
+ },
185
+ {
186
+ description: "make copy-mode selection readable",
187
+ args: ["set-window-option", "-t", t, "mode-style", "fg=colour231,bg=colour60"],
188
+ },
189
+ );
190
+ }
191
+ return commands;
192
+ }
193
+
194
+ /**
195
+ * Resolve a git worktree path (gjc `--worktree <path>` parity). If the path
196
+ * already exists it is reused as-is; otherwise a new worktree is created on a
197
+ * branch derived from the path basename. Returns the absolute worktree path.
198
+ */
199
+ export function resolveWorktree(cwd: string, wt: string): string {
200
+ const abs = path.isAbsolute(wt) ? wt : path.resolve(cwd, wt);
201
+ if (fs.existsSync(abs)) return abs;
202
+ if (!Bun.which("git")) {
203
+ console.error("error: --worktree requires git on PATH");
204
+ process.exit(1);
205
+ }
206
+ const branch = (path.basename(abs).replace(/[^a-zA-Z0-9_-]/g, "-") || "jeo-wt");
207
+ const withBranch = Bun.spawnSync(["git", "worktree", "add", "-b", branch, abs], {
208
+ cwd,
209
+ stdout: "pipe",
210
+ stderr: "pipe",
211
+ });
212
+ if (withBranch.exitCode !== 0) {
213
+ // Branch may already exist; retry attaching the existing branch.
214
+ const plain = Bun.spawnSync(["git", "worktree", "add", abs], {
215
+ cwd,
216
+ stdout: "pipe",
217
+ stderr: "pipe",
218
+ });
219
+ if (plain.exitCode !== 0) {
220
+ console.error(
221
+ `error: failed to create git worktree at ${abs}: ${withBranch.stderr.toString().trim()}`,
222
+ );
223
+ process.exit(1);
224
+ }
225
+ }
226
+ return abs;
227
+ }
@@ -0,0 +1,26 @@
1
+ import { runDeepInterviewEngine, type DeepInterviewEngineOptions } from "../deep-interview";
2
+ import { runRalplanEngine, type RalplanEngineOptions } from "../ralplan";
3
+ import { runTeamEngine, type TeamEngineOptions } from "../team";
4
+ import { runUltragoalEngine, type UltragoalEngineOptions } from "../ultragoal";
5
+
6
+ /** The bundled workflow skills that run through a dedicated engine (deep-interview /
7
+ * ralplan / team / ultragoal), not the ordinary agent loop. Single source of truth —
8
+ * the menu listing, the dispatch guards, and the engine switch all read from here. */
9
+ export const WORKFLOW_NAMES = ["deep-interview", "ralplan", "team", "ultragoal"] as const;
10
+
11
+ /** True when a skill name is one of the bundled workflow engines. */
12
+ export function isWorkflowSkill(name: string): boolean {
13
+ return (WORKFLOW_NAMES as readonly string[]).includes(name);
14
+ }
15
+
16
+ /** Dispatch a bundled workflow by name to its engine. Keeps the name→engine mapping in
17
+ * ONE place so the one-shot and interactive skill runners can't drift apart. */
18
+ export function runWorkflowEngine(
19
+ name: string,
20
+ opts: DeepInterviewEngineOptions & RalplanEngineOptions & TeamEngineOptions & UltragoalEngineOptions,
21
+ ): Promise<{ ok: boolean; reason?: string }> {
22
+ if (name === "deep-interview") return runDeepInterviewEngine(opts);
23
+ if (name === "ralplan") return runRalplanEngine(opts);
24
+ if (name === "team") return runTeamEngine(opts);
25
+ return runUltragoalEngine(opts);
26
+ }