jeo-code 0.11.0 → 0.11.1
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 +15 -0
- package/README.ja.md +1 -1
- package/README.ko.md +1 -1
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/package.json +1 -1
- package/src/agent/notify/session-endpoint.ts +15 -0
- package/src/commands/launch.ts +222 -211
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,21 @@ The README mirrors the latest 5 entries — regenerate with `bun run changelog:s
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.11.1] - 2026-08-25
|
|
13
|
+
_Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited._
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- **A one-shot run never exited when `notifications.enabled` was set** (`src/agent/notify/session-endpoint.ts`, `src/commands/launch.ts`) — `startSessionNotifyEndpoint` binds a `Bun.serve` socket plus a snapshot-poll `setInterval` for the whole launch lifetime, but its teardown lived only on the interactive REPL's exit path. A one-shot run leaves through one of ~17 early `return`s, none of which reach it, so the listening socket and the ref'd timer kept the event loop alive indefinitely. Fixed on both sides: the server and poll timer are now `unref`'d so the endpoint can never be the reason the process stays up, and the entire one-shot block is wrapped in a `try/finally` that stops the endpoint — which also removes the session discovery file, so the Telegram daemon stops dialling a dead socket after a scripted run.
|
|
17
|
+
- This was invisible for two compounding reasons: it only reproduces on a machine with notifications actually configured, and Bun timers/servers do not appear in `process._getActiveHandles()` — so the hung process reported **zero** active handles while refusing to exit. It was found by monkey-patching `setInterval`/`setTimeout` to record creation sites and dumping the still-ref'd ones after `dispatch()` returned.
|
|
18
|
+
- **The `bashTool` orphan-reaping assertions compared raw command output to the string `"0"`** (`test/tools-fs.test.ts`) — `bashTool` merges stdout and stderr, so any warning `pgrep` wrote to stderr false-failed a run in which zero strays actually existed. The count is now parsed rather than string-matched, and the orphan check is gated on process enumeration actually working (hardened sandboxes answer `pgrep: Cannot get process list`). The abort **contract** itself (`success:false` / `"Command aborted"`) is still asserted unconditionally, so a restricted host keeps covering the behaviour that matters.
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- `test/notify-endpoint-exit.test.ts` — pins the exit contract deterministically by pointing `JEO_CONFIG_DIR` at a temp config with notifications ON, rather than depending on the developer's real `~/.jeo/config.json`. Covers both halves independently: the `finally` teardown (one-shot exits, no leftover discovery file) and the `unref` guard (any future path that forgets `stop()` still cannot pin the process open).
|
|
22
|
+
|
|
23
|
+
### Verified
|
|
24
|
+
- `bun run typecheck` clean; `bun test` **3348 pass, 0 fail** — down from 22 failures two releases ago and 6 at 0.11.0. Suite wall time also dropped ~148s → ~88s, since the hung specs no longer burn their full 25s/5s timeouts.
|
|
25
|
+
- The unref guard was verified by reverting it and confirming the new unit test fails, then restoring it.
|
|
26
|
+
|
|
12
27
|
## [0.11.0] - 2026-08-25
|
|
13
28
|
_One bad boundary check was corrupting agent context, subagent fan-out, and the Telegram daemon's kill safety at the same time — and none of the three looked related from the outside._
|
|
14
29
|
|
package/README.ja.md
CHANGED
|
@@ -319,10 +319,10 @@ CI は `.github/workflows/npm-publish.yml` で公開します — GitHub リリ
|
|
|
319
319
|
|
|
320
320
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
321
321
|
- **[Unreleased]**
|
|
322
|
+
- **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
|
|
322
323
|
- **[0.11.0]** (2026-08-25) — One bad boundary check was corrupting agent context, subagent fan-out, and the Telegram daemon's kill safety at the same time — and none of the three looked related from the outside.
|
|
323
324
|
- **[0.10.0]** (2026-08-25) — jeo could only be pointed at ONE user-supplied endpoint (`config.openaiBaseUrl`), and doing so rebound the built-in `openai` provider: it stole the `openai/` routing prefix, collided with real OpenAI model ids, and could not speak the Anthropic Messages protocol at all. There was no way to run a company LiteLLM proxy and a self-hosted vLLM box at the same time, or to reach either from a script.
|
|
324
325
|
- **[0.9.17]** (2026-08-18) — Live model discovery already existed (`listProviderModels`/`discoverModels`), but only `jeo auth login openai` ever called it — logging into Anthropic or Antigravity left the account pinned to the maintained static catalog snapshot until the next unrelated live-discovery call happened to run.
|
|
325
|
-
- **[0.9.16]** (2026-08-18) — Routing looked broken while it was actually working: the idle status bar and `/compact`/`/handoff` never reflected which model `routePrompt` actually routed to.
|
|
326
326
|
|
|
327
327
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
328
328
|
<!-- CHANGELOG:END -->
|
package/README.ko.md
CHANGED
|
@@ -320,10 +320,10 @@ CI는 `.github/workflows/npm-publish.yml`로 배포합니다 — GitHub 릴리
|
|
|
320
320
|
|
|
321
321
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
322
322
|
- **[Unreleased]**
|
|
323
|
+
- **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
|
|
323
324
|
- **[0.11.0]** (2026-08-25) — One bad boundary check was corrupting agent context, subagent fan-out, and the Telegram daemon's kill safety at the same time — and none of the three looked related from the outside.
|
|
324
325
|
- **[0.10.0]** (2026-08-25) — jeo could only be pointed at ONE user-supplied endpoint (`config.openaiBaseUrl`), and doing so rebound the built-in `openai` provider: it stole the `openai/` routing prefix, collided with real OpenAI model ids, and could not speak the Anthropic Messages protocol at all. There was no way to run a company LiteLLM proxy and a self-hosted vLLM box at the same time, or to reach either from a script.
|
|
325
326
|
- **[0.9.17]** (2026-08-18) — Live model discovery already existed (`listProviderModels`/`discoverModels`), but only `jeo auth login openai` ever called it — logging into Anthropic or Antigravity left the account pinned to the maintained static catalog snapshot until the next unrelated live-discovery call happened to run.
|
|
326
|
-
- **[0.9.16]** (2026-08-18) — Routing looked broken while it was actually working: the idle status bar and `/compact`/`/handoff` never reflected which model `routePrompt` actually routed to.
|
|
327
327
|
|
|
328
328
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
329
329
|
<!-- CHANGELOG:END -->
|
package/README.md
CHANGED
|
@@ -327,10 +327,10 @@ Huge thanks to [gajae-code](https://github.com/Yeachan-Heo/gajae-code) for the i
|
|
|
327
327
|
|
|
328
328
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
329
329
|
- **[Unreleased]**
|
|
330
|
+
- **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
|
|
330
331
|
- **[0.11.0]** (2026-08-25) — One bad boundary check was corrupting agent context, subagent fan-out, and the Telegram daemon's kill safety at the same time — and none of the three looked related from the outside.
|
|
331
332
|
- **[0.10.0]** (2026-08-25) — jeo could only be pointed at ONE user-supplied endpoint (`config.openaiBaseUrl`), and doing so rebound the built-in `openai` provider: it stole the `openai/` routing prefix, collided with real OpenAI model ids, and could not speak the Anthropic Messages protocol at all. There was no way to run a company LiteLLM proxy and a self-hosted vLLM box at the same time, or to reach either from a script.
|
|
332
333
|
- **[0.9.17]** (2026-08-18) — Live model discovery already existed (`listProviderModels`/`discoverModels`), but only `jeo auth login openai` ever called it — logging into Anthropic or Antigravity left the account pinned to the maintained static catalog snapshot until the next unrelated live-discovery call happened to run.
|
|
333
|
-
- **[0.9.16]** (2026-08-18) — Routing looked broken while it was actually working: the idle status bar and `/compact`/`/handoff` never reflected which model `routePrompt` actually routed to.
|
|
334
334
|
|
|
335
335
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
336
336
|
<!-- CHANGELOG:END -->
|
package/README.zh.md
CHANGED
|
@@ -317,10 +317,10 @@ CI 通过 `.github/workflows/npm-publish.yml` 发布 — GitHub 发布 release
|
|
|
317
317
|
|
|
318
318
|
<!-- CHANGELOG:START (auto-generated from CHANGELOG.md — run `bun run changelog:sync`) -->
|
|
319
319
|
- **[Unreleased]**
|
|
320
|
+
- **[0.11.1]** (2026-08-25) — Every non-interactive `jeo` run hung forever once Telegram notifications were configured — `echo "..." | jeo`, `jeo -p "..."` in CI, any scripted use. The work completed and the command returned; the process just never exited.
|
|
320
321
|
- **[0.11.0]** (2026-08-25) — One bad boundary check was corrupting agent context, subagent fan-out, and the Telegram daemon's kill safety at the same time — and none of the three looked related from the outside.
|
|
321
322
|
- **[0.10.0]** (2026-08-25) — jeo could only be pointed at ONE user-supplied endpoint (`config.openaiBaseUrl`), and doing so rebound the built-in `openai` provider: it stole the `openai/` routing prefix, collided with real OpenAI model ids, and could not speak the Anthropic Messages protocol at all. There was no way to run a company LiteLLM proxy and a self-hosted vLLM box at the same time, or to reach either from a script.
|
|
322
323
|
- **[0.9.17]** (2026-08-18) — Live model discovery already existed (`listProviderModels`/`discoverModels`), but only `jeo auth login openai` ever called it — logging into Anthropic or Antigravity left the account pinned to the maintained static catalog snapshot until the next unrelated live-discovery call happened to run.
|
|
323
|
-
- **[0.9.16]** (2026-08-18) — Routing looked broken while it was actually working: the idle status bar and `/compact`/`/handoff` never reflected which model `routePrompt` actually routed to.
|
|
324
324
|
|
|
325
325
|
See [CHANGELOG.md](CHANGELOG.md) for the full history.
|
|
326
326
|
<!-- CHANGELOG:END -->
|
package/package.json
CHANGED
|
@@ -229,7 +229,22 @@ export class SessionNotifyEndpoint {
|
|
|
229
229
|
},
|
|
230
230
|
},
|
|
231
231
|
});
|
|
232
|
+
// Same reasoning as the poll timer below: a listening socket also keeps the Bun
|
|
233
|
+
// event loop alive. This server exists to MIRROR a session, never to outlive it,
|
|
234
|
+
// so it must not be able to decide the process stays up. `stop()` still tears it
|
|
235
|
+
// down explicitly on the paths that have one — this only removes its veto over exit.
|
|
236
|
+
(this.server as { unref?: () => void }).unref?.();
|
|
232
237
|
this.pollTimer = setInterval(() => this.broadcastIfChanged(), SNAPSHOT_POLL_MS);
|
|
238
|
+
// Unref the poll timer so it can NEVER be the reason the process stays alive.
|
|
239
|
+
// This endpoint mirrors an already-running session; it has no work of its own
|
|
240
|
+
// worth holding the event loop open for. Left ref'd, any code path that starts
|
|
241
|
+
// the endpoint but does not reach an explicit `stop()` hangs the process
|
|
242
|
+
// forever — which is exactly what happened to every non-interactive invocation
|
|
243
|
+
// (`echo "…" | jeo`, `jeo -p "…"` in CI): the one-shot branch returns long
|
|
244
|
+
// before the interactive REPL's exit teardown runs. Bun timers do not appear in
|
|
245
|
+
// `process._getActiveHandles()`, so that hang presented as a process with zero
|
|
246
|
+
// active handles refusing to exit.
|
|
247
|
+
(this.pollTimer as { unref?: () => void }).unref?.();
|
|
233
248
|
await fs.mkdir(notifySessionsDir(), { recursive: true, mode: 0o700 });
|
|
234
249
|
const payload = {
|
|
235
250
|
url: `ws://127.0.0.1:${this.server.port}`,
|
package/src/commands/launch.ts
CHANGED
|
@@ -1974,239 +1974,250 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1974
1974
|
};
|
|
1975
1975
|
|
|
1976
1976
|
if (isOneShot) {
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
// a slow, flaky, and semantically wrong round-trip. These no-arg session/system
|
|
1989
|
-
// commands have a meaningful one-shot effect (or are simple no-ops) and exit.
|
|
1990
|
-
{
|
|
1991
|
-
const cmd = messageContent.trim();
|
|
1992
|
-
if (cmd === "/exit" || cmd === "/quit") {
|
|
1993
|
-
return;
|
|
1994
|
-
}
|
|
1995
|
-
if (cmd === "/clear" || cmd === "/session new" || cmd === "/session drop" || cmd === "/session delete") {
|
|
1996
|
-
// Reset history to just the system prompt and overwrite the session file so
|
|
1997
|
-
// the persisted transcript matches (a fresh session for /session new and /session drop).
|
|
1998
|
-
history.length = 1;
|
|
1999
|
-
if (sessionId && !flags.noSession) {
|
|
2000
|
-
try {
|
|
2001
|
-
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
2002
|
-
} catch { /* best-effort: in-memory clear already done */ }
|
|
2003
|
-
}
|
|
2004
|
-
console.log("(history cleared)");
|
|
2005
|
-
return;
|
|
2006
|
-
}
|
|
2007
|
-
if (cmd === "/" || cmd === "/?" || cmd === "/help") {
|
|
2008
|
-
for (const line of formatSlashCommandList("/", skillSlashDetails)) console.log(line);
|
|
2009
|
-
console.log("Tools: read / write / edit / bash / find / search. Sessions persist to .jeo/sessions/.");
|
|
2010
|
-
return;
|
|
2011
|
-
}
|
|
2012
|
-
if (cmd === "/usage") {
|
|
2013
|
-
const cfg = await readGlobalConfig();
|
|
2014
|
-
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
2015
|
-
const result = handleUsage(ctx, sessionUsage);
|
|
2016
|
-
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
2017
|
-
return;
|
|
2018
|
-
}
|
|
2019
|
-
if (cmd === "/context") {
|
|
2020
|
-
const cfg = await readGlobalConfig();
|
|
2021
|
-
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
2022
|
-
const result = await handleContext(ctx);
|
|
2023
|
-
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
2024
|
-
return;
|
|
2025
|
-
}
|
|
2026
|
-
if (cmd === "/tools") {
|
|
2027
|
-
const cfg = await readGlobalConfig();
|
|
2028
|
-
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
2029
|
-
const result = await handleTools(ctx);
|
|
2030
|
-
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
2031
|
-
return;
|
|
2032
|
-
}
|
|
2033
|
-
if (cmd === "/hotkeys") {
|
|
2034
|
-
const cfg = await readGlobalConfig();
|
|
2035
|
-
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
2036
|
-
const result = handleHotkeys(ctx);
|
|
2037
|
-
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
2038
|
-
return;
|
|
1977
|
+
// A one-shot run has ~17 exit points below. The session notify endpoint (a
|
|
1978
|
+
// listening socket + a discovery file the Telegram daemon polls) is created for
|
|
1979
|
+
// the WHOLE launch lifetime up at `startSessionNotifyEndpoint`, but its teardown
|
|
1980
|
+
// lives on the interactive REPL's exit path — which none of those 17 returns
|
|
1981
|
+
// reach. The endpoint's socket/timer are unref'd so a leak can no longer HANG the
|
|
1982
|
+
// process, but a one-shot run would still leave a stale discovery file behind for
|
|
1983
|
+
// the daemon to keep dialling. One `finally` covers every exit point.
|
|
1984
|
+
try {
|
|
1985
|
+
let messageContent = joinedArgs;
|
|
1986
|
+
if (!process.stdin.isTTY && joinedArgs.length === 0) {
|
|
1987
|
+
messageContent = (await Bun.stdin.text()).trim();
|
|
2039
1988
|
}
|
|
2040
|
-
if (
|
|
2041
|
-
|
|
2042
|
-
const cfg = await readGlobalConfig();
|
|
2043
|
-
const activeModel = sessionModel || defaultModel;
|
|
2044
|
-
// `Config` (state.ts) predates config-schema.ts's `compaction` field and is
|
|
2045
|
-
// out of this change's scope — read the passthrough-preserved value via a
|
|
2046
|
-
// narrow structural cast instead of widening the shared interface.
|
|
2047
|
-
const focus = parsed.focus ?? (cfg as { compaction?: { handoffFocus?: string } }).compaction?.handoffFocus;
|
|
2048
|
-
const res = await buildHandoffDocument(history, { model: activeModel, focus });
|
|
2049
|
-
for (const line of handoffLines(res, process.stdout.columns)) console.log(line);
|
|
1989
|
+
if (!messageContent) {
|
|
1990
|
+
console.log("No input provided.");
|
|
2050
1991
|
return;
|
|
2051
1992
|
}
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
1993
|
+
// One-shot piped/`-p` input that is a control slash command must be handled
|
|
1994
|
+
// HERE — never forwarded to the model. `echo "/clear" | jeo` (and Ctrl-D after
|
|
1995
|
+
// a piped command) previously sent the literal "/clear" to the LLM as a prompt:
|
|
1996
|
+
// a slow, flaky, and semantically wrong round-trip. These no-arg session/system
|
|
1997
|
+
// commands have a meaningful one-shot effect (or are simple no-ops) and exit.
|
|
1998
|
+
{
|
|
1999
|
+
const cmd = messageContent.trim();
|
|
2000
|
+
if (cmd === "/exit" || cmd === "/quit") {
|
|
2001
|
+
return;
|
|
2061
2002
|
}
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
delete process.env.JEO_WIKI_ROOT;
|
|
2074
|
-
sessionWikiRoot = undefined;
|
|
2075
|
-
await refreshSessionMemory("");
|
|
2076
|
-
} else if (decision.kind === "set") {
|
|
2077
|
-
await saveConfigPatch(() => ({ wikiRoot: decision.persistArg }));
|
|
2078
|
-
process.env.JEO_WIKI_ROOT = decision.root;
|
|
2079
|
-
sessionWikiRoot = decision.root;
|
|
2080
|
-
await refreshSessionMemory("");
|
|
2003
|
+
if (cmd === "/clear" || cmd === "/session new" || cmd === "/session drop" || cmd === "/session delete") {
|
|
2004
|
+
// Reset history to just the system prompt and overwrite the session file so
|
|
2005
|
+
// the persisted transcript matches (a fresh session for /session new and /session drop).
|
|
2006
|
+
history.length = 1;
|
|
2007
|
+
if (sessionId && !flags.noSession) {
|
|
2008
|
+
try {
|
|
2009
|
+
sessionId = (await createSession(cwd, undefined, sessionModel)).id;
|
|
2010
|
+
} catch { /* best-effort: in-memory clear already done */ }
|
|
2011
|
+
}
|
|
2012
|
+
console.log("(history cleared)");
|
|
2013
|
+
return;
|
|
2081
2014
|
}
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
return;
|
|
2087
|
-
}
|
|
2088
|
-
if (cmd === "/evolve") {
|
|
2089
|
-
await runEvolveSimulation(animateAsciiArt);
|
|
2090
|
-
return;
|
|
2091
|
-
}
|
|
2092
|
-
}
|
|
2093
|
-
// One skill run (bundle workflow → engine; regular skill → agent turn). Shared by the
|
|
2094
|
-
// single-invocation path and the `$a $b …` chain path so every `$` skill actually runs.
|
|
2095
|
-
const runOneSkillShot = async (inv: SkillInvocation): Promise<void> => {
|
|
2096
|
-
const isBundleWorkflow = isWorkflowSkill(inv.skill.name);
|
|
2097
|
-
if (isBundleWorkflow) {
|
|
2098
|
-
const startMsg: Message = {
|
|
2099
|
-
role: "system",
|
|
2100
|
-
content: `[workflow:${inv.skill.name}:start]${inv.intent ? ` intent: ${inv.intent}` : ""}`
|
|
2101
|
-
};
|
|
2102
|
-
history.push(startMsg);
|
|
2103
|
-
if (sessionId) {
|
|
2104
|
-
await appendMessage(sessionId, startMsg, cwd);
|
|
2015
|
+
if (cmd === "/" || cmd === "/?" || cmd === "/help") {
|
|
2016
|
+
for (const line of formatSlashCommandList("/", skillSlashDetails)) console.log(line);
|
|
2017
|
+
console.log("Tools: read / write / edit / bash / find / search. Sessions persist to .jeo/sessions/.");
|
|
2018
|
+
return;
|
|
2105
2019
|
}
|
|
2020
|
+
if (cmd === "/usage") {
|
|
2021
|
+
const cfg = await readGlobalConfig();
|
|
2022
|
+
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
2023
|
+
const result = handleUsage(ctx, sessionUsage);
|
|
2024
|
+
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
if (cmd === "/context") {
|
|
2028
|
+
const cfg = await readGlobalConfig();
|
|
2029
|
+
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
2030
|
+
const result = await handleContext(ctx);
|
|
2031
|
+
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
2032
|
+
return;
|
|
2033
|
+
}
|
|
2034
|
+
if (cmd === "/tools") {
|
|
2035
|
+
const cfg = await readGlobalConfig();
|
|
2036
|
+
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
2037
|
+
const result = await handleTools(ctx);
|
|
2038
|
+
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
if (cmd === "/hotkeys") {
|
|
2042
|
+
const cfg = await readGlobalConfig();
|
|
2043
|
+
const ctx: SlashContext = { history, sessionModel, sessionId, cwd, config: cfg };
|
|
2044
|
+
const result = handleHotkeys(ctx);
|
|
2045
|
+
if (result && "lines" in result) for (const line of result.lines) console.log(line);
|
|
2046
|
+
return;
|
|
2047
|
+
}
|
|
2048
|
+
if (cmd === "/handoff" || cmd.startsWith("/handoff ")) {
|
|
2049
|
+
const parsed = parseHandoffCommand(cmd) ?? {};
|
|
2050
|
+
const cfg = await readGlobalConfig();
|
|
2051
|
+
const activeModel = sessionModel || defaultModel;
|
|
2052
|
+
// `Config` (state.ts) predates config-schema.ts's `compaction` field and is
|
|
2053
|
+
// out of this change's scope — read the passthrough-preserved value via a
|
|
2054
|
+
// narrow structural cast instead of widening the shared interface.
|
|
2055
|
+
const focus = parsed.focus ?? (cfg as { compaction?: { handoffFocus?: string } }).compaction?.handoffFocus;
|
|
2056
|
+
const res = await buildHandoffDocument(history, { model: activeModel, focus });
|
|
2057
|
+
for (const line of handoffLines(res, process.stdout.columns)) console.log(line);
|
|
2058
|
+
return;
|
|
2059
|
+
}
|
|
2060
|
+
if (cmd === "/theme" || cmd.startsWith("/theme ")) {
|
|
2061
|
+
const decision = decideThemeSlash(cmd, listThemes(), resolveTheme().name);
|
|
2062
|
+
for (const line of decision.lines) console.log(line);
|
|
2063
|
+
if (decision.kind === "set") {
|
|
2064
|
+
process.env.JEO_TUI_THEME = decision.themeName!;
|
|
2065
|
+
await saveConfigPatch(raw => ({ theme: decision.themeName }));
|
|
2066
|
+
// No refreshUiTheme() here — one-shot mode returns immediately after this
|
|
2067
|
+
// block, so there is no live TUI paint state to re-resolve (and
|
|
2068
|
+
// refreshUiTheme is declared later in this closure, out of scope here).
|
|
2069
|
+
}
|
|
2070
|
+
return;
|
|
2071
|
+
}
|
|
2072
|
+
if (cmd === "/wiki" || cmd.startsWith("/wiki ")) {
|
|
2073
|
+
const decision = decideWikiSlash(
|
|
2074
|
+
cmd,
|
|
2075
|
+
resolveWikiRoot(await readGlobalConfig()),
|
|
2076
|
+
!!jeoEnv("WIKI_ROOT"),
|
|
2077
|
+
);
|
|
2078
|
+
for (const line of decision.lines) console.log(line);
|
|
2079
|
+
if (decision.kind === "clear") {
|
|
2080
|
+
await saveConfigPatch(() => ({ wikiRoot: undefined }));
|
|
2081
|
+
delete process.env.JEO_WIKI_ROOT;
|
|
2082
|
+
sessionWikiRoot = undefined;
|
|
2083
|
+
await refreshSessionMemory("");
|
|
2084
|
+
} else if (decision.kind === "set") {
|
|
2085
|
+
await saveConfigPatch(() => ({ wikiRoot: decision.persistArg }));
|
|
2086
|
+
process.env.JEO_WIKI_ROOT = decision.root;
|
|
2087
|
+
sessionWikiRoot = decision.root;
|
|
2088
|
+
await refreshSessionMemory("");
|
|
2089
|
+
}
|
|
2090
|
+
return;
|
|
2091
|
+
}
|
|
2092
|
+
if (cmd === "/config" || cmd === "/settings") {
|
|
2093
|
+
for (const line of await buildConfigPanelLines({ sessionModel, sessionThinking, sessionId })) console.log(line);
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
if (cmd === "/evolve") {
|
|
2097
|
+
await runEvolveSimulation(animateAsciiArt);
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
// One skill run (bundle workflow → engine; regular skill → agent turn). Shared by the
|
|
2102
|
+
// single-invocation path and the `$a $b …` chain path so every `$` skill actually runs.
|
|
2103
|
+
const runOneSkillShot = async (inv: SkillInvocation): Promise<void> => {
|
|
2104
|
+
const isBundleWorkflow = isWorkflowSkill(inv.skill.name);
|
|
2105
|
+
if (isBundleWorkflow) {
|
|
2106
|
+
const startMsg: Message = {
|
|
2107
|
+
role: "system",
|
|
2108
|
+
content: `[workflow:${inv.skill.name}:start]${inv.intent ? ` intent: ${inv.intent}` : ""}`
|
|
2109
|
+
};
|
|
2110
|
+
history.push(startMsg);
|
|
2111
|
+
if (sessionId) {
|
|
2112
|
+
await appendMessage(sessionId, startMsg, cwd);
|
|
2113
|
+
}
|
|
2106
2114
|
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2115
|
+
const harness = createInFlightAbortHarness({
|
|
2116
|
+
captureEsc: false,
|
|
2117
|
+
onAbortNotice: msg => console.log(msg),
|
|
2118
|
+
onHardExit: () => forceExitFromCtrlC(),
|
|
2119
|
+
});
|
|
2120
|
+
const ac = harness.controller;
|
|
2113
2121
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2122
|
+
const opts = {
|
|
2123
|
+
cwd,
|
|
2124
|
+
signal: ac.signal,
|
|
2125
|
+
onProgress: (e: { skill: string; phase: string; detail?: string }) => console.log(`[workflow:${e.skill}] ${e.phase}${e.detail ? ` — ${e.detail}` : ""}`),
|
|
2126
|
+
io: {
|
|
2127
|
+
output: (line: string) => {
|
|
2128
|
+
console.log(line);
|
|
2129
|
+
},
|
|
2130
|
+
// This `$a $b …` one-shot chain runs BEFORE the interactive REPL's
|
|
2131
|
+
// `promptInput`/`rl` exist (this whole dispatch is reached and returns
|
|
2132
|
+
// long before the `const promptInput = …` further down the function
|
|
2133
|
+
// executes) — referencing `promptInput` here throws a TDZ
|
|
2134
|
+
// ReferenceError ("Cannot access 'promptInput' before initialization")
|
|
2135
|
+
// the instant the workflow engine asks its first question. That error
|
|
2136
|
+
// is caught and stashed into session history (never printed to the
|
|
2137
|
+
// terminal), so the visible symptom is just a frozen prompt forever
|
|
2138
|
+
// (the "jeo --tmux $deep-interview hangs" / "$deep-interview hangs"
|
|
2139
|
+
// report). Omitting `input` here is safe and correct: no other
|
|
2140
|
+
// readline exists yet at this point, so deep-interview.ts's own
|
|
2141
|
+
// fallback `createInterface({input: process.stdin, ...})` (used
|
|
2142
|
+
// whenever `opts.io?.input` is absent) has nothing to fight over raw
|
|
2143
|
+
// mode with. The INTERACTIVE `runSkillInvocation` sibling path below
|
|
2144
|
+
// runs AFTER `promptInput` is declared and correctly wires it — do not
|
|
2145
|
+
// "fix" this block by copying that wiring here again.
|
|
2121
2146
|
},
|
|
2122
|
-
// This `$a $b …` one-shot chain runs BEFORE the interactive REPL's
|
|
2123
|
-
// `promptInput`/`rl` exist (this whole dispatch is reached and returns
|
|
2124
|
-
// long before the `const promptInput = …` further down the function
|
|
2125
|
-
// executes) — referencing `promptInput` here throws a TDZ
|
|
2126
|
-
// ReferenceError ("Cannot access 'promptInput' before initialization")
|
|
2127
|
-
// the instant the workflow engine asks its first question. That error
|
|
2128
|
-
// is caught and stashed into session history (never printed to the
|
|
2129
|
-
// terminal), so the visible symptom is just a frozen prompt forever
|
|
2130
|
-
// (the "jeo --tmux $deep-interview hangs" / "$deep-interview hangs"
|
|
2131
|
-
// report). Omitting `input` here is safe and correct: no other
|
|
2132
|
-
// readline exists yet at this point, so deep-interview.ts's own
|
|
2133
|
-
// fallback `createInterface({input: process.stdin, ...})` (used
|
|
2134
|
-
// whenever `opts.io?.input` is absent) has nothing to fight over raw
|
|
2135
|
-
// mode with. The INTERACTIVE `runSkillInvocation` sibling path below
|
|
2136
|
-
// runs AFTER `promptInput` is declared and correctly wires it — do not
|
|
2137
|
-
// "fix" this block by copying that wiring here again.
|
|
2138
|
-
},
|
|
2139
2147
|
|
|
2140
|
-
|
|
2148
|
+
args: inv.skill.name === "deep-interview" ? (inv.intent ? inv.intent.split(/\s+/) : []) : undefined
|
|
2141
2149
|
|
|
2142
|
-
|
|
2150
|
+
};
|
|
2143
2151
|
|
|
2144
|
-
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2152
|
+
let ok = false;
|
|
2153
|
+
let reason: string | undefined;
|
|
2154
|
+
try {
|
|
2155
|
+
const res: { ok: boolean; reason?: string } = await runWorkflowEngine(inv.skill.name, opts);
|
|
2156
|
+
ok = res.ok;
|
|
2157
|
+
reason = res.reason;
|
|
2158
|
+
} catch (err: any) {
|
|
2159
|
+
ok = false;
|
|
2160
|
+
reason = err.message;
|
|
2161
|
+
} finally {
|
|
2162
|
+
harness.dispose();
|
|
2163
|
+
}
|
|
2156
2164
|
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
+
const endMsg: Message = {
|
|
2166
|
+
role: "system",
|
|
2167
|
+
content: ok
|
|
2168
|
+
? `[workflow:${inv.skill.name}:finish]`
|
|
2169
|
+
: `[workflow:${inv.skill.name}:abort]${reason ? ` reason: ${reason}` : ""}`
|
|
2170
|
+
};
|
|
2171
|
+
if (sessionId) {
|
|
2172
|
+
await appendMessage(sessionId, endMsg, cwd);
|
|
2173
|
+
}
|
|
2174
|
+
return;
|
|
2165
2175
|
}
|
|
2166
|
-
return;
|
|
2167
|
-
}
|
|
2168
2176
|
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
|
|
2177
|
-
|
|
2177
|
+
const useOneShotTui = shouldUseOneShotTui(flags.noTui);
|
|
2178
|
+
if (!useOneShotTui) {
|
|
2179
|
+
console.log(`▶ Running skill: ${inv.skill.name}${inv.intent ? ` — ${inv.intent}` : ""}`);
|
|
2180
|
+
}
|
|
2181
|
+
const task = buildSkillTask(inv.skill, inv.intent, inv.invokedAs);
|
|
2182
|
+
const { reply, rendered, usage } = await runTurn(task, useOneShotTui, undefined, { userCard: null });
|
|
2183
|
+
if (!rendered) console.log(stripMarkdown(renderMarkdownTables(reply)) + usage);
|
|
2184
|
+
else if (usage) console.log(usage.trim());
|
|
2185
|
+
};
|
|
2178
2186
|
|
|
2179
|
-
|
|
2180
|
-
|
|
2181
|
-
|
|
2182
|
-
|
|
2183
|
-
|
|
2187
|
+
// `$a $b … [intent]` — run every resolved skill in order; a lone `$skill` is a chain of 1.
|
|
2188
|
+
const skillChain = parseSkillChain(messageContent, resolvedSkills);
|
|
2189
|
+
if (skillChain && skillChain.invocations.length) {
|
|
2190
|
+
if (skillChain.unresolved.length) {
|
|
2191
|
+
console.log(`(skipping unknown skill${skillChain.unresolved.length > 1 ? "s" : ""}: ${skillChain.unresolved.map(u => `$${u}`).join(", ")})`);
|
|
2192
|
+
}
|
|
2193
|
+
if (skillChain.invocations.length > 1) {
|
|
2194
|
+
console.log(`▶ Chaining ${skillChain.invocations.length} skills: ${skillChain.invocations.map(i => `$${i.skill.name}`).join(" → ")}`);
|
|
2195
|
+
}
|
|
2196
|
+
for (const inv of skillChain.invocations) {
|
|
2197
|
+
await runOneSkillShot(inv);
|
|
2198
|
+
}
|
|
2199
|
+
await maybeDistillOneShot();
|
|
2200
|
+
return;
|
|
2184
2201
|
}
|
|
2185
|
-
|
|
2186
|
-
|
|
2202
|
+
// A `$skill` invocation resolves to a single skill (by name, declared alias, or unique prefix).
|
|
2203
|
+
const skillInvocation = parseSkillInvocation(messageContent, resolvedSkills);
|
|
2204
|
+
if (skillInvocation) {
|
|
2205
|
+
await runOneSkillShot(skillInvocation);
|
|
2206
|
+
await maybeDistillOneShot();
|
|
2207
|
+
return;
|
|
2187
2208
|
}
|
|
2188
|
-
|
|
2189
|
-
await
|
|
2209
|
+
try {
|
|
2210
|
+
const { reply, rendered, usage } = await runTurn(messageContent, shouldUseOneShotTui(flags.noTui));
|
|
2211
|
+
if (!rendered) console.log(stripMarkdown(renderMarkdownTables(reply)) + usage);
|
|
2212
|
+
else if (usage) console.log(usage.trim());
|
|
2213
|
+
} catch (err) {
|
|
2214
|
+
console.log(`! ${friendlyProviderError(err)}`);
|
|
2190
2215
|
}
|
|
2191
2216
|
await maybeDistillOneShot();
|
|
2192
2217
|
return;
|
|
2218
|
+
} finally {
|
|
2219
|
+
await sessionNotifyEndpoint?.stop().catch(() => {});
|
|
2193
2220
|
}
|
|
2194
|
-
// A `$skill` invocation resolves to a single skill (by name, declared alias, or unique prefix).
|
|
2195
|
-
const skillInvocation = parseSkillInvocation(messageContent, resolvedSkills);
|
|
2196
|
-
if (skillInvocation) {
|
|
2197
|
-
await runOneSkillShot(skillInvocation);
|
|
2198
|
-
await maybeDistillOneShot();
|
|
2199
|
-
return;
|
|
2200
|
-
}
|
|
2201
|
-
try {
|
|
2202
|
-
const { reply, rendered, usage } = await runTurn(messageContent, shouldUseOneShotTui(flags.noTui));
|
|
2203
|
-
if (!rendered) console.log(stripMarkdown(renderMarkdownTables(reply)) + usage);
|
|
2204
|
-
else if (usage) console.log(usage.trim());
|
|
2205
|
-
} catch (err) {
|
|
2206
|
-
console.log(`! ${friendlyProviderError(err)}`);
|
|
2207
|
-
}
|
|
2208
|
-
await maybeDistillOneShot();
|
|
2209
|
-
return;
|
|
2210
2221
|
}
|
|
2211
2222
|
|
|
2212
2223
|
// INTERACTIVE mode
|