jeo-code 0.5.12 → 0.5.14
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 +19 -0
- package/README.ja.md +2 -2
- package/README.ko.md +2 -2
- package/README.md +2 -2
- package/README.zh.md +2 -2
- package/package.json +3 -2
- package/src/agent/engine.ts +16 -3
- package/src/agent/loop.ts +2 -0
- package/src/agent/tool-schemas.ts +132 -0
- package/src/agent/tools.ts +9 -3
- package/src/ai/model-manager.ts +1 -0
- package/src/ai/providers/anthropic.ts +60 -3
- package/src/ai/providers/antigravity.ts +31 -1
- package/src/ai/providers/openai-responses.ts +55 -0
- package/src/ai/providers/openai.ts +46 -3
- package/src/ai/types.ts +19 -0
- package/src/cli/runner.ts +9 -0
- package/src/commands/launch.ts +207 -256
- package/src/commands/update.ts +12 -0
- package/src/commands/whats-new.ts +3 -2
- package/src/skills/catalog.ts +34 -70
- package/src/tui/app.ts +43 -61
- package/src/tui/components/autocomplete.ts +2 -8
- package/src/tui/components/slash.ts +1 -2
- package/src/util/whats-new.ts +4 -1
package/src/commands/launch.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
2
|
import { emitKeypressEvents } from "node:readline";
|
|
3
3
|
import { PassThrough } from "node:stream";
|
|
4
|
-
import { runAgentLoop,
|
|
4
|
+
import { runAgentLoop, DEFAULT_TOOLS, TOOL_PROTOCOL, WORKING_DISCIPLINE, OUTPUT_DISCIPLINE, type AgentLoopEvents } from "../agent/engine";
|
|
5
5
|
import { createOpikTracer, wrapEvents } from "../agent/opik-tracer";
|
|
6
6
|
import { initialDynamicStepLimit } from "../agent/step-budget";
|
|
7
7
|
import { memoryPromptSection, spawnDetachedDistill } from "../agent/memory";
|
|
@@ -10,11 +10,11 @@ import { createSubagentTool, SUBAGENT_TOOL_PROTOCOL_LINE } from "../agent/subage
|
|
|
10
10
|
import { SubagentRegistry } from "../agent/subagent-registry";
|
|
11
11
|
import { createTodoTool, TODO_TOOL_PROTOCOL_LINE } from "../agent/todo-tool";
|
|
12
12
|
import { LaunchTui } from "../tui/app";
|
|
13
|
-
import { runDeepInterviewEngine } from "./deep-interview";
|
|
14
|
-
import { runRalplanEngine } from "./ralplan";
|
|
15
|
-
import { runTeamEngine } from "./team";
|
|
16
|
-
import { runUltragoalEngine } from "./ultragoal";
|
|
17
|
-
import { skillsPromptSection, loadSkills,
|
|
13
|
+
import { runDeepInterviewEngine, type DeepInterviewEngineOptions } from "./deep-interview";
|
|
14
|
+
import { runRalplanEngine, type RalplanEngineOptions } from "./ralplan";
|
|
15
|
+
import { runTeamEngine, type TeamEngineOptions } from "./team";
|
|
16
|
+
import { runUltragoalEngine, type UltragoalEngineOptions } from "./ultragoal";
|
|
17
|
+
import { skillsPromptSection, loadSkills, buildSkillTask, workflowSkillsForPrompt, parseSkillInvocation, parseSkillChain, looksLikeSkillEcho, skillInvocationCard, type SkillDoc, type SkillInvocation } from "../skills/catalog";
|
|
18
18
|
import { formatForgeBox } from "../tui/components/forge";
|
|
19
19
|
import { interactiveOAuthLogin } from "./auth";
|
|
20
20
|
import { logoutOAuth } from "../auth";
|
|
@@ -54,7 +54,7 @@ import {
|
|
|
54
54
|
formatCapabilityLine,
|
|
55
55
|
} from "../tui/components/config-panel";
|
|
56
56
|
import { liveModelPicker, renderLiveModelPicker, type ModelAssignmentBadge } from "../tui/components/live-model-picker";
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
import { providerPicker, renderProviderPicker } from "../tui/components/provider-picker";
|
|
59
59
|
import { detectLanguage, languageLabel, parseLineRange, sliceLines, formatCodeBlock, formatDiff, sanitizeForTerminal } from "../tui/components/code-view";
|
|
60
60
|
import { categoryBadge } from "../tui/components/category-index";
|
|
@@ -509,6 +509,18 @@ export function isStandaloneBackspace(chunk: string): boolean {
|
|
|
509
509
|
return chunk.length > 0 && /^[\x7f\b]+$/.test(chunk);
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
+
/** gjc-parity slash-command aliases, applied once before dispatch so each real command
|
|
513
|
+
* keeps a SINGLE handler: `/login`→`/provider login`, `/settings`→`/config`,
|
|
514
|
+
* `/subagent(s)`→`/agents`. Pure rewrite that preserves any trailing args. */
|
|
515
|
+
export function normalizeSlashAlias(input: string): string {
|
|
516
|
+
if (input === "/login" || input.startsWith("/login ")) return `/provider login${input.slice("/login".length)}`;
|
|
517
|
+
if (input === "/settings") return "/config";
|
|
518
|
+
if (input === "/subagent" || input.startsWith("/subagent ")) return `/agents${input.slice("/subagent".length)}`;
|
|
519
|
+
if (input === "/subagents" || input.startsWith("/subagents ")) return `/agents${input.slice("/subagents".length)}`;
|
|
520
|
+
if (input === "/resume" || input.startsWith("/resume ")) return `/session resume${input.slice("/resume".length)}`;
|
|
521
|
+
return input;
|
|
522
|
+
}
|
|
523
|
+
|
|
512
524
|
export interface PromptInputQueue {
|
|
513
525
|
pendingLines: string[];
|
|
514
526
|
partial: string;
|
|
@@ -1013,6 +1025,28 @@ function resolveWorktree(cwd: string, wt: string): string {
|
|
|
1013
1025
|
return abs;
|
|
1014
1026
|
}
|
|
1015
1027
|
|
|
1028
|
+
/** The bundled workflow skills that run through a dedicated engine (deep-interview /
|
|
1029
|
+
* ralplan / team / ultragoal), not the ordinary agent loop. Single source of truth —
|
|
1030
|
+
* the menu listing, the dispatch guards, and the engine switch all read from here. */
|
|
1031
|
+
export const WORKFLOW_NAMES = ["deep-interview", "ralplan", "team", "ultragoal"] as const;
|
|
1032
|
+
|
|
1033
|
+
/** True when a skill name is one of the bundled workflow engines. */
|
|
1034
|
+
export function isWorkflowSkill(name: string): boolean {
|
|
1035
|
+
return (WORKFLOW_NAMES as readonly string[]).includes(name);
|
|
1036
|
+
}
|
|
1037
|
+
|
|
1038
|
+
/** Dispatch a bundled workflow by name to its engine. Keeps the name→engine mapping in
|
|
1039
|
+
* ONE place so the one-shot and interactive skill runners can't drift apart. */
|
|
1040
|
+
export function runWorkflowEngine(
|
|
1041
|
+
name: string,
|
|
1042
|
+
opts: DeepInterviewEngineOptions & RalplanEngineOptions & TeamEngineOptions & UltragoalEngineOptions,
|
|
1043
|
+
): Promise<{ ok: boolean; reason?: string }> {
|
|
1044
|
+
if (name === "deep-interview") return runDeepInterviewEngine(opts);
|
|
1045
|
+
if (name === "ralplan") return runRalplanEngine(opts);
|
|
1046
|
+
if (name === "team") return runTeamEngine(opts);
|
|
1047
|
+
return runUltragoalEngine(opts);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1016
1050
|
export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
1017
1051
|
let cwd = process.cwd();
|
|
1018
1052
|
const flags = parseFlags(args, cwd);
|
|
@@ -1192,14 +1226,10 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1192
1226
|
|
|
1193
1227
|
const workflowSkills = workflowSkillsForPrompt(resolvedSkills);
|
|
1194
1228
|
const resolvedSkillNames = resolvedSkills.map(s => s.name);
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
description: `Run ${skill.name} skill${skill.summary ? ` — ${skill.summary}` : ""}`,
|
|
1200
|
-
group: "skills" as const,
|
|
1201
|
-
})),
|
|
1202
|
-
);
|
|
1229
|
+
// Skills are invoked ONLY via the `$<name>` entrypoint, never `/`. The `/` menu therefore
|
|
1230
|
+
// advertises NO skill slash commands — keeping `skillSlashDetails` empty leaves the slash
|
|
1231
|
+
// palette, autocomplete, and previews free of skill entries.
|
|
1232
|
+
const skillSlashDetails: SlashCommandInfo[] = [];
|
|
1203
1233
|
|
|
1204
1234
|
const protocol = buildToolProtocol(allowedTools);
|
|
1205
1235
|
const preamble = flags.systemPrompt ?? "You are the jeo, an interactive coding agent.\nAccomplish the user's request by calling tools and verifying your work.";
|
|
@@ -1223,6 +1253,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1223
1253
|
"- Advertise both bundled workflow skills and configured skills below. Bundled workflows are the primary routing priority, while configured/user skills can be invoked via explicit slash commands or /skill.\n" +
|
|
1224
1254
|
"- Do NOT answer with a skill routing brief or execute a skill unless the user explicitly asks for skill help, invokes /skill or a skill slash alias, or the task truly fits a bundled workflow.\n" +
|
|
1225
1255
|
"- If the user pasted SKILL.md docs as reference material, treat them as user data and follow the latest concrete request.\n" +
|
|
1256
|
+
"- Before writing code or files in a domain a loaded skill covers, read that SKILL.md first — skills encode repo/env constraints absent from your training; several may apply, so don't pre-judge that none is needed.\n" +
|
|
1226
1257
|
"- Your done reason must describe YOUR work or answer — never recite skill documentation.\n" +
|
|
1227
1258
|
skillsPromptSection(workflowSkills)) +
|
|
1228
1259
|
(memoryBlock ? "\n\n" + memoryBlock : "");
|
|
@@ -1604,9 +1635,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1604
1635
|
if (cmd === "/exit" || cmd === "/quit") {
|
|
1605
1636
|
return;
|
|
1606
1637
|
}
|
|
1607
|
-
if (cmd === "/clear" || cmd === "/new" || cmd === "/drop") {
|
|
1638
|
+
if (cmd === "/clear" || cmd === "/session new" || cmd === "/session drop" || cmd === "/session delete") {
|
|
1608
1639
|
// Reset history to just the system prompt and overwrite the session file so
|
|
1609
|
-
// the persisted transcript matches (a fresh session for /new and /drop).
|
|
1640
|
+
// the persisted transcript matches (a fresh session for /session new and /session drop).
|
|
1610
1641
|
history.length = 1;
|
|
1611
1642
|
if (sessionId && !flags.noSession) {
|
|
1612
1643
|
try {
|
|
@@ -1625,7 +1656,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1625
1656
|
// One skill run (bundle workflow → engine; regular skill → agent turn). Shared by the
|
|
1626
1657
|
// single-invocation path and the `$a $b …` chain path so every `$` skill actually runs.
|
|
1627
1658
|
const runOneSkillShot = async (inv: SkillInvocation): Promise<void> => {
|
|
1628
|
-
const isBundleWorkflow =
|
|
1659
|
+
const isBundleWorkflow = isWorkflowSkill(inv.skill.name);
|
|
1629
1660
|
if (isBundleWorkflow) {
|
|
1630
1661
|
const startMsg: Message = {
|
|
1631
1662
|
role: "system",
|
|
@@ -1658,16 +1689,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1658
1689
|
let ok = false;
|
|
1659
1690
|
let reason: string | undefined;
|
|
1660
1691
|
try {
|
|
1661
|
-
|
|
1662
|
-
if (inv.skill.name === "deep-interview") {
|
|
1663
|
-
res = await runDeepInterviewEngine(opts);
|
|
1664
|
-
} else if (inv.skill.name === "ralplan") {
|
|
1665
|
-
res = await runRalplanEngine(opts);
|
|
1666
|
-
} else if (inv.skill.name === "team") {
|
|
1667
|
-
res = await runTeamEngine(opts);
|
|
1668
|
-
} else {
|
|
1669
|
-
res = await runUltragoalEngine(opts);
|
|
1670
|
-
}
|
|
1692
|
+
const res: { ok: boolean; reason?: string } = await runWorkflowEngine(inv.skill.name, opts);
|
|
1671
1693
|
ok = res.ok;
|
|
1672
1694
|
reason = res.reason;
|
|
1673
1695
|
} catch (err: any) {
|
|
@@ -1807,7 +1829,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1807
1829
|
);
|
|
1808
1830
|
logLines(card);
|
|
1809
1831
|
}
|
|
1810
|
-
const isBundleWorkflow =
|
|
1832
|
+
const isBundleWorkflow = isWorkflowSkill(skill.name);
|
|
1811
1833
|
if (isBundleWorkflow) {
|
|
1812
1834
|
const startMsg: Message = {
|
|
1813
1835
|
role: "system",
|
|
@@ -1857,16 +1879,7 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
1857
1879
|
let ok = false;
|
|
1858
1880
|
let reason: string | undefined;
|
|
1859
1881
|
try {
|
|
1860
|
-
|
|
1861
|
-
if (skill.name === "deep-interview") {
|
|
1862
|
-
res = await runDeepInterviewEngine(opts);
|
|
1863
|
-
} else if (skill.name === "ralplan") {
|
|
1864
|
-
res = await runRalplanEngine(opts);
|
|
1865
|
-
} else if (skill.name === "team") {
|
|
1866
|
-
res = await runTeamEngine(opts);
|
|
1867
|
-
} else {
|
|
1868
|
-
res = await runUltragoalEngine(opts);
|
|
1869
|
-
}
|
|
1882
|
+
const res: { ok: boolean; reason?: string } = await runWorkflowEngine(skill.name, opts);
|
|
1870
1883
|
ok = res.ok;
|
|
1871
1884
|
reason = res.reason;
|
|
1872
1885
|
} catch (err: any) {
|
|
@@ -2816,54 +2829,6 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
2816
2829
|
return true;
|
|
2817
2830
|
};
|
|
2818
2831
|
|
|
2819
|
-
const pickSkillFromList = async (skills: SkillDoc[]): Promise<SkillDoc | undefined> => {
|
|
2820
|
-
if (!process.stdin.isTTY || skills.length === 0) return undefined;
|
|
2821
|
-
const list = skillPicker(skills);
|
|
2822
|
-
let chosen: SkillDoc | undefined;
|
|
2823
|
-
await runSelectPicker(
|
|
2824
|
-
(cols, rows) =>
|
|
2825
|
-
renderSkillPicker(list, {
|
|
2826
|
-
cols,
|
|
2827
|
-
rows: Math.max(4, Math.min(rows, 12)),
|
|
2828
|
-
unicode: true,
|
|
2829
|
-
color: true,
|
|
2830
|
-
}),
|
|
2831
|
-
(ch, key) => {
|
|
2832
|
-
if (key?.name === "up") {
|
|
2833
|
-
list.up();
|
|
2834
|
-
return false;
|
|
2835
|
-
}
|
|
2836
|
-
if (key?.name === "down") {
|
|
2837
|
-
list.down();
|
|
2838
|
-
return false;
|
|
2839
|
-
}
|
|
2840
|
-
if (key?.name === "pageup") {
|
|
2841
|
-
list.page(-1, 6);
|
|
2842
|
-
return false;
|
|
2843
|
-
}
|
|
2844
|
-
if (key?.name === "pagedown") {
|
|
2845
|
-
list.page(1, 6);
|
|
2846
|
-
return false;
|
|
2847
|
-
}
|
|
2848
|
-
if (key?.name === "backspace") {
|
|
2849
|
-
list.backspace();
|
|
2850
|
-
return false;
|
|
2851
|
-
}
|
|
2852
|
-
if (key?.name === "escape" || (key?.ctrl && key.name === "c")) {
|
|
2853
|
-
return true;
|
|
2854
|
-
}
|
|
2855
|
-
if (key?.name === "return" || key?.name === "enter") {
|
|
2856
|
-
chosen = list.selected()?.value;
|
|
2857
|
-
return true;
|
|
2858
|
-
}
|
|
2859
|
-
if (ch && ch >= " " && !key?.ctrl && !key?.meta) {
|
|
2860
|
-
list.typeChar(ch);
|
|
2861
|
-
}
|
|
2862
|
-
return false;
|
|
2863
|
-
},
|
|
2864
|
-
);
|
|
2865
|
-
return chosen;
|
|
2866
|
-
};
|
|
2867
2832
|
|
|
2868
2833
|
const pickCloudProvider = async (statuses: Awaited<ReturnType<typeof describeAllProviders>>): Promise<AuthProvider | undefined> => {
|
|
2869
2834
|
const cloud = new Set(["anthropic", "openai", "gemini", "antigravity"]);
|
|
@@ -3114,13 +3079,9 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3114
3079
|
let input = pendingSelection && trigger && pendingSelection.startsWith(trigger.token)
|
|
3115
3080
|
? raw.slice(0, trigger.start) + pendingSelection
|
|
3116
3081
|
: raw;
|
|
3117
|
-
// gjc-parity command aliases (full behavior reuse, no duplicated handlers)
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
// `/subagent`(s) → the /agents roster/editor (view + change the current
|
|
3121
|
-
// subagent composition: per-role model · thinking · steps).
|
|
3122
|
-
else if (input === "/subagent" || input.startsWith("/subagent ")) input = `/agents${input.slice("/subagent".length)}`;
|
|
3123
|
-
else if (input === "/subagents" || input.startsWith("/subagents ")) input = `/agents${input.slice("/subagents".length)}`;
|
|
3082
|
+
// gjc-parity command aliases (full behavior reuse, no duplicated handlers):
|
|
3083
|
+
// /login→/provider login, /settings→/config, /subagent(s)→/agents.
|
|
3084
|
+
input = normalizeSlashAlias(input);
|
|
3124
3085
|
pendingSelection = undefined;
|
|
3125
3086
|
navMatches = [];
|
|
3126
3087
|
navIdx = -1;
|
|
@@ -3185,156 +3146,169 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
3185
3146
|
}
|
|
3186
3147
|
continue;
|
|
3187
3148
|
}
|
|
3188
|
-
if (input === "/sessions") {
|
|
3189
|
-
const sessions = await listSessions(cwd);
|
|
3190
|
-
if (sessions.length === 0) console.log("(no saved sessions)");
|
|
3191
|
-
for (const s of sessions) {
|
|
3192
|
-
const marker = s.id === sessionId ? "*" : " ";
|
|
3193
|
-
const title = s.title ? `[${s.title}] ` : "";
|
|
3194
|
-
console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${title}${s.preview}`);
|
|
3195
|
-
}
|
|
3196
|
-
continue;
|
|
3197
|
-
}
|
|
3198
|
-
// ---- gjc-parity session management ------------------------------------
|
|
3199
|
-
const startFreshSession = async (verb: string): Promise<void> => {
|
|
3200
|
-
history.length = 1;
|
|
3201
|
-
if (!flags.noSession) {
|
|
3202
|
-
sessionId = (await createSession(cwd)).id;
|
|
3203
|
-
advanceSessionBoxColor(); // distinct input-box hue per newly opened session
|
|
3204
|
-
console.log(`(${verb} — new session ${sessionId})`);
|
|
3205
|
-
} else {
|
|
3206
|
-
sessionId = undefined;
|
|
3207
|
-
console.log(`(${verb} — sessions disabled)`);
|
|
3208
|
-
}
|
|
3209
|
-
};
|
|
3210
|
-
if (input === "/new") {
|
|
3211
|
-
await startFreshSession("started fresh");
|
|
3212
|
-
continue;
|
|
3213
|
-
}
|
|
3214
|
-
if (input === "/drop") {
|
|
3215
|
-
if (sessionId) {
|
|
3216
|
-
const removed = await deleteSession(sessionId, cwd);
|
|
3217
|
-
console.log(removed ? `(deleted session ${sessionId})` : `(session ${sessionId} already gone)`);
|
|
3218
|
-
}
|
|
3219
|
-
await startFreshSession("dropped");
|
|
3220
|
-
continue;
|
|
3221
|
-
}
|
|
3222
3149
|
if (input === "/session" || input.startsWith("/session ")) {
|
|
3223
|
-
const
|
|
3224
|
-
|
|
3225
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3150
|
+
const tokens = input.substring(8).trim().split(/\s+/).filter(Boolean);
|
|
3151
|
+
const sub = (tokens[0] ?? "").toLowerCase();
|
|
3152
|
+
|
|
3153
|
+
const startFreshSession = async (verb: string): Promise<void> => {
|
|
3154
|
+
history.length = 1;
|
|
3155
|
+
if (!flags.noSession) {
|
|
3156
|
+
sessionId = (await createSession(cwd)).id;
|
|
3157
|
+
advanceSessionBoxColor(); // distinct input-box hue per newly opened session
|
|
3158
|
+
console.log(`(${verb} — new session ${sessionId})`);
|
|
3159
|
+
} else {
|
|
3160
|
+
sessionId = undefined;
|
|
3161
|
+
console.log(`(${verb} — sessions disabled)`);
|
|
3228
3162
|
}
|
|
3229
|
-
|
|
3230
|
-
|
|
3231
|
-
|
|
3163
|
+
};
|
|
3164
|
+
|
|
3165
|
+
if (sub === "new") {
|
|
3166
|
+
await startFreshSession("started fresh");
|
|
3232
3167
|
continue;
|
|
3233
3168
|
}
|
|
3234
|
-
if (sub
|
|
3235
|
-
|
|
3169
|
+
if (sub === "drop" || sub === "delete") {
|
|
3170
|
+
if (sessionId) {
|
|
3171
|
+
const removed = await deleteSession(sessionId, cwd);
|
|
3172
|
+
console.log(removed ? `(deleted session ${sessionId})` : `(session ${sessionId} already gone)`);
|
|
3173
|
+
}
|
|
3174
|
+
await startFreshSession("dropped");
|
|
3236
3175
|
continue;
|
|
3237
3176
|
}
|
|
3238
|
-
if (
|
|
3239
|
-
|
|
3177
|
+
if (sub === "rename") {
|
|
3178
|
+
const title = tokens.slice(1).join(" ").trim();
|
|
3179
|
+
if (!title) {
|
|
3180
|
+
console.log("Usage: /session rename <title>");
|
|
3181
|
+
continue;
|
|
3182
|
+
}
|
|
3183
|
+
if (!sessionId) {
|
|
3184
|
+
console.log("(sessions are disabled — nothing to rename)");
|
|
3185
|
+
continue;
|
|
3186
|
+
}
|
|
3187
|
+
try {
|
|
3188
|
+
await renameSession(sessionId, title, cwd);
|
|
3189
|
+
console.log(`(session renamed to '${title}')`);
|
|
3190
|
+
} catch (err) {
|
|
3191
|
+
console.log(`! rename failed: ${(err as Error).message}`);
|
|
3192
|
+
}
|
|
3240
3193
|
continue;
|
|
3241
3194
|
}
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
|
|
3247
|
-
|
|
3248
|
-
|
|
3249
|
-
|
|
3250
|
-
|
|
3251
|
-
|
|
3252
|
-
|
|
3253
|
-
|
|
3254
|
-
|
|
3255
|
-
|
|
3256
|
-
|
|
3195
|
+
if (sub === "resume") {
|
|
3196
|
+
const arg = tokens.slice(1).join(" ").trim();
|
|
3197
|
+
const applyResume = async (rid: string): Promise<void> => {
|
|
3198
|
+
try {
|
|
3199
|
+
const { messages } = await loadSession(rid, cwd);
|
|
3200
|
+
history.length = 1;
|
|
3201
|
+
for (const m of messages) history.push(m);
|
|
3202
|
+
sessionId = rid;
|
|
3203
|
+
// Seed /retry + reply marker from the last user/assistant turn.
|
|
3204
|
+
lastUserInput = ""; lastReply = "";
|
|
3205
|
+
for (let k = history.length - 1; k >= 1; k--) {
|
|
3206
|
+
if (history[k]!.role === "user" && !lastUserInput) lastUserInput = String(history[k]!.content ?? "");
|
|
3207
|
+
if (history[k]!.role === "assistant" && !lastReply) lastReply = String(history[k]!.content ?? "");
|
|
3208
|
+
if (lastUserInput && lastReply) break;
|
|
3209
|
+
}
|
|
3210
|
+
// Seed readline's input history so ↑ in the prompt recalls THIS session's
|
|
3211
|
+
// prior prompts (not just lines typed in the current run). readline history
|
|
3212
|
+
// is newest-first; unshift in chronological order so the session's newest
|
|
3213
|
+
// prompt lands at the front (first ↑). Skip injected/framed messages.
|
|
3214
|
+
const rli = rl as unknown as { history?: string[] };
|
|
3215
|
+
if (Array.isArray(rli.history)) {
|
|
3216
|
+
const priorPrompts = history
|
|
3217
|
+
.filter(m => m.role === "user")
|
|
3218
|
+
.map(m => String(m.content ?? "").trim())
|
|
3219
|
+
.filter(c => c && !c.startsWith("Tool [") && !c.startsWith("[mid-turn steering") && !c.startsWith("[Earlier conversation summary]"));
|
|
3220
|
+
for (const p of priorPrompts) {
|
|
3221
|
+
if (rli.history[0] !== p) rli.history.unshift(p);
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
const sep = "─".repeat(Math.min(48, Math.max(20, (process.stdout.columns ?? 80) - 1)));
|
|
3225
|
+
logLines([
|
|
3226
|
+
sep,
|
|
3227
|
+
`resumed session ${rid} · ${messages.length} message(s) (/history all for the full transcript)`,
|
|
3228
|
+
sep,
|
|
3229
|
+
...formatTranscript(history, { maxTurns: 6, color: true, unicode: true }),
|
|
3230
|
+
sep,
|
|
3231
|
+
]);
|
|
3232
|
+
} catch (err) {
|
|
3233
|
+
console.log(`! ${(err as Error).message}`);
|
|
3234
|
+
}
|
|
3235
|
+
};
|
|
3236
|
+
if (arg) { await applyResume(arg); continue; }
|
|
3237
|
+
// No id → only sessions with a real conversation are resumable (every launch
|
|
3238
|
+
// creates an empty session; those are noise).
|
|
3239
|
+
const sessions = (await listSessions(cwd)).filter(s => s.messageCount > 0);
|
|
3240
|
+
if (sessions.length === 0) {
|
|
3241
|
+
console.log("(no saved sessions with history)");
|
|
3242
|
+
continue;
|
|
3243
|
+
}
|
|
3244
|
+
// Interactive arrow-key picker on a TTY: ↑↓ to move, Enter to resume, Esc cancels.
|
|
3245
|
+
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
3246
|
+
const items: SelectItem<string>[] = sessions.slice(0, 50).map(s => ({
|
|
3247
|
+
value: s.id,
|
|
3248
|
+
label: `${s.title ? `[${s.title}] ` : ""}${(s.preview || s.id).replace(/\s+/g, " ")}`.slice(0, 76) || s.id,
|
|
3249
|
+
hint: `${s.messageCount} msgs${s.id === sessionId ? " · current" : ""}`,
|
|
3250
|
+
}));
|
|
3251
|
+
const picked = await pickFromOptions("Resume a session ↑↓ move · Enter resume · Esc cancel", items);
|
|
3252
|
+
if (picked) await applyResume(picked);
|
|
3253
|
+
else console.log("(resume cancelled)");
|
|
3254
|
+
continue;
|
|
3255
|
+
}
|
|
3256
|
+
// Non-TTY fallback: static list (resume with /session resume <id>).
|
|
3257
|
+
console.log("Saved sessions — resume with /session resume <id>:");
|
|
3258
|
+
for (const s of sessions.slice(0, 15)) {
|
|
3259
|
+
const marker = s.id === sessionId ? "*" : " ";
|
|
3260
|
+
console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${s.title ? `[${s.title}] ` : ""}${s.preview}`);
|
|
3261
|
+
}
|
|
3257
3262
|
continue;
|
|
3258
3263
|
}
|
|
3259
|
-
if (
|
|
3260
|
-
|
|
3264
|
+
if (sub === "list") {
|
|
3265
|
+
const sessions = await listSessions(cwd);
|
|
3266
|
+
if (sessions.length === 0) console.log("(no saved sessions)");
|
|
3267
|
+
for (const s of sessions) {
|
|
3268
|
+
const marker = s.id === sessionId ? "*" : " ";
|
|
3269
|
+
const title = s.title ? `[${s.title}] ` : "";
|
|
3270
|
+
console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${title}${s.preview}`);
|
|
3271
|
+
}
|
|
3261
3272
|
continue;
|
|
3262
3273
|
}
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3267
|
-
console.log(`! rename failed: ${(err as Error).message}`);
|
|
3268
|
-
}
|
|
3269
|
-
continue;
|
|
3270
|
-
}
|
|
3271
|
-
if (input === "/resume" || input.startsWith("/resume ")) {
|
|
3272
|
-
const arg = input.substring(7).trim();
|
|
3273
|
-
// Load a session into history and print its transcript so the resume is visible.
|
|
3274
|
-
const applyResume = async (rid: string): Promise<void> => {
|
|
3275
|
-
try {
|
|
3276
|
-
const { messages } = await loadSession(rid, cwd);
|
|
3277
|
-
history.length = 1;
|
|
3278
|
-
for (const m of messages) history.push(m);
|
|
3279
|
-
sessionId = rid;
|
|
3280
|
-
// Seed /retry + reply marker from the last user/assistant turn.
|
|
3281
|
-
lastUserInput = ""; lastReply = "";
|
|
3282
|
-
for (let k = history.length - 1; k >= 1; k--) {
|
|
3283
|
-
if (history[k]!.role === "user" && !lastUserInput) lastUserInput = String(history[k]!.content ?? "");
|
|
3284
|
-
if (history[k]!.role === "assistant" && !lastReply) lastReply = String(history[k]!.content ?? "");
|
|
3285
|
-
if (lastUserInput && lastReply) break;
|
|
3286
|
-
}
|
|
3287
|
-
// Seed readline's input history so ↑ in the prompt recalls THIS session's
|
|
3288
|
-
// prior prompts (not just lines typed in the current run). readline history
|
|
3289
|
-
// is newest-first; unshift in chronological order so the session's newest
|
|
3290
|
-
// prompt lands at the front (first ↑). Skip injected/framed messages.
|
|
3291
|
-
const rli = rl as unknown as { history?: string[] };
|
|
3292
|
-
if (Array.isArray(rli.history)) {
|
|
3293
|
-
const priorPrompts = history
|
|
3294
|
-
.filter(m => m.role === "user")
|
|
3295
|
-
.map(m => String(m.content ?? "").trim())
|
|
3296
|
-
.filter(c => c && !c.startsWith("Tool [") && !c.startsWith("[mid-turn steering") && !c.startsWith("[Earlier conversation summary]"));
|
|
3297
|
-
for (const p of priorPrompts) {
|
|
3298
|
-
if (rli.history[0] !== p) rli.history.unshift(p);
|
|
3299
|
-
}
|
|
3300
|
-
}
|
|
3301
|
-
const sep = "─".repeat(Math.min(48, Math.max(20, (process.stdout.columns ?? 80) - 1)));
|
|
3302
|
-
logLines([
|
|
3303
|
-
sep,
|
|
3304
|
-
`resumed session ${rid} · ${messages.length} message(s) (/history all for the full transcript)`,
|
|
3305
|
-
sep,
|
|
3306
|
-
...formatTranscript(history, { maxTurns: 6, color: true, unicode: true }),
|
|
3307
|
-
sep,
|
|
3308
|
-
]);
|
|
3309
|
-
} catch (err) {
|
|
3310
|
-
console.log(`! ${(err as Error).message}`);
|
|
3274
|
+
if (sub === "info") {
|
|
3275
|
+
if (!sessionId) {
|
|
3276
|
+
console.log("Session: disabled (--no-session)");
|
|
3277
|
+
continue;
|
|
3311
3278
|
}
|
|
3312
|
-
|
|
3313
|
-
|
|
3314
|
-
|
|
3315
|
-
|
|
3316
|
-
|
|
3317
|
-
|
|
3318
|
-
console.log("(
|
|
3279
|
+
const all = await listSessions(cwd);
|
|
3280
|
+
const current = all.find(s => s.id === sessionId);
|
|
3281
|
+
console.log("Session info:");
|
|
3282
|
+
console.log(` id ${sessionId}`);
|
|
3283
|
+
if (current?.title) console.log(` title ${current.title}`);
|
|
3284
|
+
console.log(` file ${sessionPath(sessionId, cwd)}`);
|
|
3285
|
+
console.log(` started ${current?.timestamp ?? "(this run)"}`);
|
|
3286
|
+
console.log(` messages ${current?.messageCount ?? Math.max(0, history.length - 1)} persisted · ${history.length - 1} in context`);
|
|
3287
|
+
console.log(` workspace ${cwd}`);
|
|
3319
3288
|
continue;
|
|
3320
3289
|
}
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
const items: SelectItem<string>[] = sessions.slice(0, 50).map(s => ({
|
|
3324
|
-
value: s.id,
|
|
3325
|
-
label: `${s.title ? `[${s.title}] ` : ""}${(s.preview || s.id).replace(/\s+/g, " ")}`.slice(0, 76) || s.id,
|
|
3326
|
-
hint: `${s.messageCount} msgs${s.id === sessionId ? " · current" : ""}`,
|
|
3327
|
-
}));
|
|
3328
|
-
const picked = await pickFromOptions("Resume a session ↑↓ move · Enter resume · Esc cancel", items);
|
|
3329
|
-
if (picked) await applyResume(picked);
|
|
3330
|
-
else console.log("(resume cancelled)");
|
|
3290
|
+
if (sub && sub !== "info") {
|
|
3291
|
+
console.log("Usage: /session [list|info|new|drop|rename <title>|resume [id]]");
|
|
3331
3292
|
continue;
|
|
3332
3293
|
}
|
|
3333
|
-
|
|
3334
|
-
|
|
3335
|
-
|
|
3336
|
-
|
|
3337
|
-
console.log(
|
|
3294
|
+
|
|
3295
|
+
// Default: list sessions AND show current session info
|
|
3296
|
+
const sessions = await listSessions(cwd);
|
|
3297
|
+
if (sessions.length === 0) {
|
|
3298
|
+
console.log("(no saved sessions)");
|
|
3299
|
+
} else {
|
|
3300
|
+
console.log("Saved sessions:");
|
|
3301
|
+
for (const s of sessions) {
|
|
3302
|
+
const marker = s.id === sessionId ? "*" : " ";
|
|
3303
|
+
const title = s.title ? `[${s.title}] ` : "";
|
|
3304
|
+
console.log(` ${marker}${s.id} (${s.messageCount} msgs) ${title}${s.preview}`);
|
|
3305
|
+
}
|
|
3306
|
+
}
|
|
3307
|
+
if (sessionId) {
|
|
3308
|
+
const current = sessions.find(s => s.id === sessionId);
|
|
3309
|
+
console.log(`\nCurrent session: ${sessionId}${current?.title ? ` [${current.title}]` : ""}`);
|
|
3310
|
+
} else {
|
|
3311
|
+
console.log("\nCurrent session: disabled (--no-session)");
|
|
3338
3312
|
}
|
|
3339
3313
|
continue;
|
|
3340
3314
|
}
|
|
@@ -4235,46 +4209,23 @@ export async function runLaunchCommand(args: string[]): Promise<void> {
|
|
|
4235
4209
|
console.log(res.success ? (res.output || "(no matches)") : `! ${res.error}`);
|
|
4236
4210
|
continue;
|
|
4237
4211
|
}
|
|
4212
|
+
|
|
4238
4213
|
const skillEntrypoint = input.startsWith("/skill:") ? "/skill:" : input.startsWith("/skill") && (input === "/skill" || input[6] === " ") ? "/skill" : "";
|
|
4239
4214
|
if (skillEntrypoint) {
|
|
4240
4215
|
if (flags.noSkills) {
|
|
4241
4216
|
console.log("Skills are disabled.");
|
|
4242
4217
|
continue;
|
|
4243
4218
|
}
|
|
4244
|
-
|
|
4219
|
+
// `/` never LOADS or RUNS a skill file — skills are invoked ONLY via `$<name>`.
|
|
4220
|
+
// `/skill[:...]` is a read-only listing that points the user at the `$` entrypoint.
|
|
4245
4221
|
let skills = await loadSkills(cwd);
|
|
4246
4222
|
if (flags.skills) {
|
|
4247
4223
|
const patterns = flags.skills.split(",").map(p => p.trim()).filter(Boolean);
|
|
4248
4224
|
skills = skills.filter(s => patterns.some(p => matchSkillGlob(p, s.name)));
|
|
4249
4225
|
}
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
if (!picked) {
|
|
4254
|
-
console.log("(cancelled)");
|
|
4255
|
-
continue;
|
|
4256
|
-
}
|
|
4257
|
-
await runSkillInvocation(picked, "");
|
|
4258
|
-
continue;
|
|
4259
|
-
}
|
|
4260
|
-
console.log("Skills (bundled + configured docs) — run with /skill <name> [intent] or a skill slash alias:");
|
|
4261
|
-
for (const s of skills) {
|
|
4262
|
-
const aliases = skillSlashAliases(s);
|
|
4263
|
-
console.log(` ${s.name.padEnd(16)} ${s.summary}${aliases.length ? ` (${aliases.join(", ")})` : ""}`);
|
|
4264
|
-
}
|
|
4265
|
-
continue;
|
|
4266
|
-
}
|
|
4267
|
-
const [nm, ...intentParts] = rest.split(/\s+/);
|
|
4268
|
-
const skill = getSkillFrom(skills, nm);
|
|
4269
|
-
if (!skill) {
|
|
4270
|
-
console.log(`Unknown skill: ${nm}. Available: ${skills.map(s => s.name).join(", ")}`);
|
|
4271
|
-
continue;
|
|
4272
|
-
}
|
|
4273
|
-
const intent = intentParts.join(" ").trim();
|
|
4274
|
-
try {
|
|
4275
|
-
await runSkillInvocation(skill, intent);
|
|
4276
|
-
} catch (err) {
|
|
4277
|
-
console.log(`! ${(err as Error).message}`);
|
|
4226
|
+
console.log("Skills are invoked with $<name> [intent] (e.g. $team build auth). Available:");
|
|
4227
|
+
for (const s of skills) {
|
|
4228
|
+
console.log(` $${s.name.padEnd(16)} ${s.summary}`);
|
|
4278
4229
|
}
|
|
4279
4230
|
continue;
|
|
4280
4231
|
}
|
package/src/commands/update.ts
CHANGED
|
@@ -36,6 +36,8 @@ export interface UpdateDeps {
|
|
|
36
36
|
fetchJson: (url: string, options?: { signal?: AbortSignal }) => Promise<any>;
|
|
37
37
|
localVersion: () => string;
|
|
38
38
|
install: () => Promise<{ success: boolean; stdout?: string; stderr?: string }>;
|
|
39
|
+
/** Display release notes after a successful self-update (best-effort, no-op in tests). */
|
|
40
|
+
showWhatsNew?: () => void;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
43
|
export const defaultDeps: UpdateDeps = {
|
|
@@ -63,6 +65,15 @@ export const defaultDeps: UpdateDeps = {
|
|
|
63
65
|
});
|
|
64
66
|
return { success: proc.success };
|
|
65
67
|
}
|
|
68
|
+
,
|
|
69
|
+
showWhatsNew: () => {
|
|
70
|
+
try {
|
|
71
|
+
// Spawn the freshly-installed binary so it reads the NEW bundled CHANGELOG.
|
|
72
|
+
Bun.spawnSync(["jeo", "whats-new"], { stdout: "inherit", stderr: "inherit" });
|
|
73
|
+
} catch {
|
|
74
|
+
// Notes are a courtesy; a spawn failure must never fail the update.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
66
77
|
};
|
|
67
78
|
|
|
68
79
|
export async function runUpdateCommand(args: string[] = []): Promise<void> {
|
|
@@ -172,6 +183,7 @@ export async function runUpdateCommandWith(args: string[], deps: UpdateDeps): Pr
|
|
|
172
183
|
}));
|
|
173
184
|
} else {
|
|
174
185
|
console.log(`Successfully installed jeo-code@${latest}`);
|
|
186
|
+
deps.showWhatsNew?.();
|
|
175
187
|
}
|
|
176
188
|
} else {
|
|
177
189
|
if (hasJson) {
|