jeopi-cli 16.4.2 → 16.4.3
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 +74 -0
- package/dist/cli.js +3179 -3140
- package/dist/types/advisor/config.d.ts +4 -3
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/update-cli.d.ts +4 -1
- package/dist/types/cli/usage-error.d.ts +4 -0
- package/dist/types/commit/agentic/agent.d.ts +1 -0
- package/dist/types/config/model-resolver.d.ts +7 -1
- package/dist/types/config/settings-schema.d.ts +3 -3
- package/dist/types/eval/py/tool-bridge.d.ts +1 -0
- package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
- package/dist/types/extensibility/extensions/runner.d.ts +3 -1
- package/dist/types/extensibility/extensions/types.d.ts +3 -0
- package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
- package/dist/types/internal-urls/types.d.ts +7 -0
- package/dist/types/irc/bus.d.ts +4 -0
- package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
- package/dist/types/modes/components/move-overlay.d.ts +1 -1
- package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
- package/dist/types/modes/components/welcome.d.ts +4 -0
- package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
- package/dist/types/modes/orchestrate.d.ts +1 -1
- package/dist/types/modes/ultrathink.d.ts +1 -1
- package/dist/types/modes/workflow.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +5 -0
- package/dist/types/session/auth-broker-config.d.ts +2 -2
- package/dist/types/tools/bash-skill-urls.d.ts +3 -2
- package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
- package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
- package/dist/types/tools/browser/launch.d.ts +1 -0
- package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
- package/dist/types/tools/browser/run-output.d.ts +25 -0
- package/dist/types/tools/browser/tab-worker.d.ts +22 -0
- package/dist/types/utils/changelog.d.ts +35 -1
- package/package.json +12 -12
- package/src/advisor/__tests__/advisor.test.ts +13 -0
- package/src/advisor/__tests__/config.test.ts +36 -0
- package/src/advisor/config.ts +11 -8
- package/src/autolearn/controller.ts +14 -2
- package/src/cli/args.ts +12 -0
- package/src/cli/flag-tables.ts +21 -7
- package/src/cli/update-cli.ts +63 -13
- package/src/cli/usage-error.ts +7 -0
- package/src/commands/acp.ts +11 -2
- package/src/commands/launch.ts +12 -3
- package/src/commit/agentic/agent.ts +4 -0
- package/src/commit/agentic/index.ts +46 -21
- package/src/config/model-resolver.ts +24 -6
- package/src/config/settings-schema.ts +3 -3
- package/src/edit/index.ts +10 -76
- package/src/edit/renderer.ts +13 -10
- package/src/eval/__tests__/agent-bridge.test.ts +19 -14
- package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
- package/src/eval/executor-base.ts +84 -7
- package/src/eval/js/shared/rewrite-imports.ts +31 -13
- package/src/eval/py/tool-bridge.ts +13 -14
- package/src/exec/non-interactive-env.ts +0 -1
- package/src/export/ttsr.ts +0 -3
- package/src/extensibility/custom-tools/loader.ts +70 -37
- package/src/extensibility/custom-tools/types.ts +3 -0
- package/src/extensibility/extensions/runner.ts +3 -0
- package/src/extensibility/extensions/types.ts +3 -0
- package/src/extensibility/plugins/manager.ts +21 -0
- package/src/internal-urls/docs-index.generated.txt +1 -1
- package/src/internal-urls/memory-protocol.ts +13 -8
- package/src/internal-urls/skill-protocol.ts +1 -1
- package/src/internal-urls/types.ts +7 -0
- package/src/irc/bus.ts +48 -18
- package/src/lsp/client.ts +7 -1
- package/src/lsp/deferred-diagnostics.ts +66 -0
- package/src/main.ts +13 -13
- package/src/mcp/tool-bridge.ts +57 -6
- package/src/mcp/transports/stdio.test.ts +45 -1
- package/src/mcp/transports/stdio.ts +6 -3
- package/src/modes/acp/acp-agent.ts +73 -2
- package/src/modes/acp/acp-event-mapper.ts +5 -0
- package/src/modes/acp/acp-mode.ts +11 -0
- package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
- package/src/modes/components/advisor-config.ts +15 -7
- package/src/modes/components/move-overlay.ts +2 -3
- package/src/modes/components/plan-review-overlay.ts +7 -0
- package/src/modes/components/tips.txt +1 -1
- package/src/modes/components/welcome.ts +13 -14
- package/src/modes/controllers/command-controller.ts +8 -10
- package/src/modes/controllers/event-controller.ts +17 -30
- package/src/modes/controllers/input-controller.ts +9 -6
- package/src/modes/controllers/selector-controller.ts +3 -2
- package/src/modes/controllers/tan-command-controller.ts +40 -1
- package/src/modes/interactive-mode.ts +13 -0
- package/src/modes/magic-keyword-boundary.ts +23 -0
- package/src/modes/orchestrate.ts +6 -5
- package/src/modes/print-mode.ts +9 -0
- package/src/modes/ultrathink.ts +6 -5
- package/src/modes/workflow.ts +5 -4
- package/src/prompts/system/eager-task.md +2 -2
- package/src/prompts/system/tan-context-switch.md +17 -0
- package/src/prompts/tools/browser.md +5 -5
- package/src/sdk.ts +3 -0
- package/src/session/agent-session.ts +172 -25
- package/src/session/auth-broker-config.ts +4 -4
- package/src/session/snapcompact-inline.ts +3 -19
- package/src/session/streaming-output.ts +40 -12
- package/src/slash-commands/builtin-registry.ts +8 -8
- package/src/task/executor.ts +8 -0
- package/src/tools/bash-skill-urls.ts +7 -4
- package/src/tools/bash.ts +30 -22
- package/src/tools/browser/cmux/cmux-tab.ts +49 -50
- package/src/tools/browser/cmux/rpc.ts +50 -0
- package/src/tools/browser/launch.ts +8 -4
- package/src/tools/browser/run-cancellation.ts +44 -5
- package/src/tools/browser/run-output.ts +76 -0
- package/src/tools/browser/tab-worker.ts +280 -133
- package/src/tools/eval-render.ts +16 -14
- package/src/tools/glob.ts +20 -6
- package/src/tools/grep.ts +1 -0
- package/src/tools/image-gen.ts +3 -4
- package/src/tools/irc.ts +27 -13
- package/src/tools/read.ts +14 -7
- package/src/tools/write.ts +22 -5
- package/src/tools/yield.ts +29 -1
- package/src/utils/changelog.ts +106 -9
- package/src/utils/git.ts +0 -1
- package/src/utils/title-generator.ts +68 -7
- package/src/web/search/index.ts +14 -6
- package/src/web/search/providers/perplexity.ts +18 -2
|
@@ -55,6 +55,27 @@ describe("discoverAdvisorConfigs", () => {
|
|
|
55
55
|
expect(sharedInstructions).toBe("Shared baseline for all advisors.");
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
it("distinguishes omitted tools, explicit no-tools, and invalid-only lists", async () => {
|
|
59
|
+
const yaml = [
|
|
60
|
+
"advisors:",
|
|
61
|
+
" - name: No Tools",
|
|
62
|
+
" tools: []",
|
|
63
|
+
" - name: Default Tools",
|
|
64
|
+
" - name: Invalid Only",
|
|
65
|
+
" tools: [reed]",
|
|
66
|
+
].join("\n");
|
|
67
|
+
await Bun.write(path.join(tmp, "WATCHDOG.yml"), yaml);
|
|
68
|
+
|
|
69
|
+
const { advisors } = await discoverAdvisorConfigs(tmp, agentDir);
|
|
70
|
+
const noTools = advisors.find(a => a.name === "No Tools");
|
|
71
|
+
const defaultTools = advisors.find(a => a.name === "Default Tools");
|
|
72
|
+
const invalidOnly = advisors.find(a => a.name === "Invalid Only");
|
|
73
|
+
|
|
74
|
+
expect(noTools?.tools).toEqual([]);
|
|
75
|
+
expect(defaultTools?.tools).toBeUndefined();
|
|
76
|
+
expect(invalidOnly?.tools).toBeUndefined();
|
|
77
|
+
});
|
|
78
|
+
|
|
58
79
|
it("ignores a malformed YAML file without throwing", async () => {
|
|
59
80
|
await Bun.write(path.join(tmp, "WATCHDOG.yml"), "advisors: [unclosed bracket");
|
|
60
81
|
const result = await discoverAdvisorConfigs(tmp, agentDir);
|
|
@@ -122,6 +143,21 @@ describe("WATCHDOG.yml file round-trip", () => {
|
|
|
122
143
|
expect(sharedInstructions).toContain("Shared baseline.");
|
|
123
144
|
});
|
|
124
145
|
|
|
146
|
+
it("round-trips an explicit empty tools list without collapsing it into the default", async () => {
|
|
147
|
+
const file = path.join(tmp, "WATCHDOG.yml");
|
|
148
|
+
const explicitNoToolsDoc: WatchdogConfigDoc = {
|
|
149
|
+
advisors: [{ name: "No Tools", tools: [] }, { name: "Default Tools" }],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
await saveWatchdogConfigFile(file, explicitNoToolsDoc);
|
|
153
|
+
const serializedDoc = await loadWatchdogConfigFile(file);
|
|
154
|
+
expect(serializedDoc).toEqual(explicitNoToolsDoc);
|
|
155
|
+
|
|
156
|
+
const { advisors } = await discoverAdvisorConfigs(tmp, tmp);
|
|
157
|
+
expect(advisors.find(a => a.name === "No Tools")?.tools).toEqual([]);
|
|
158
|
+
expect(advisors.find(a => a.name === "Default Tools")?.tools).toBeUndefined();
|
|
159
|
+
});
|
|
160
|
+
|
|
125
161
|
it("removes the file when the doc is empty so legacy discovery resumes", async () => {
|
|
126
162
|
const file = path.join(tmp, "WATCHDOG.yml");
|
|
127
163
|
await saveWatchdogConfigFile(file, doc);
|
package/src/advisor/config.ts
CHANGED
|
@@ -12,9 +12,10 @@ import { collectConfigCandidates } from "./watchdog";
|
|
|
12
12
|
* with an optional `:level` thinking suffix (e.g. `x-ai/grok-code-fast:high`),
|
|
13
13
|
* resolved exactly like any other model override; `tools` is a subset of
|
|
14
14
|
* `BUILTIN_TOOL_NAMES` — any built-in name, including mutating tools such as
|
|
15
|
-
* `edit`/`write`/`bash` (the advisor is a full agent). Omitted
|
|
16
|
-
*
|
|
17
|
-
* advisor's specialization, appended to the shared
|
|
15
|
+
* `edit`/`write`/`bash` (the advisor is a full agent). Omitted falls back to
|
|
16
|
+
* the default `read`/`grep`/`glob` subset; an explicit empty list grants no
|
|
17
|
+
* tools. `instructions` is the advisor's specialization, appended to the shared
|
|
18
|
+
* baseline.
|
|
18
19
|
*/
|
|
19
20
|
export interface AdvisorConfig {
|
|
20
21
|
name: string;
|
|
@@ -65,11 +66,13 @@ const KNOWN_TOOL_NAMES = new Set<string>(BUILTIN_TOOL_NAMES);
|
|
|
65
66
|
/**
|
|
66
67
|
* Keep only valid tool names from an advisor's `tools` list, dropping unknowns
|
|
67
68
|
* with a warning. The advisor is a full agent, so any built tool may be granted;
|
|
68
|
-
* the runtime further filters to what's actually available this session.
|
|
69
|
-
*
|
|
69
|
+
* the runtime further filters to what's actually available this session.
|
|
70
|
+
* `undefined` means "use the default subset" (read/grep/glob); only an explicit
|
|
71
|
+
* raw empty list means "no tools".
|
|
70
72
|
*/
|
|
71
73
|
function filterAdvisorTools(tools: string[] | undefined, sourcePath: string): string[] | undefined {
|
|
72
|
-
if (
|
|
74
|
+
if (tools === undefined) return undefined;
|
|
75
|
+
if (tools.length === 0) return [];
|
|
73
76
|
// Normalize legacy aliases (search→grep, find→glob) and dedupe before validating.
|
|
74
77
|
const filtered = normalizeToolNames(tools).filter(name => {
|
|
75
78
|
if (KNOWN_TOOL_NAMES.has(name)) return true;
|
|
@@ -210,7 +213,7 @@ export async function loadWatchdogConfigFile(filePath: string): Promise<Watchdog
|
|
|
210
213
|
advisors: (result.advisors ?? []).map(a => ({
|
|
211
214
|
name: a.name,
|
|
212
215
|
model: a.model?.trim() || undefined,
|
|
213
|
-
tools: a.tools
|
|
216
|
+
tools: a.tools === undefined ? undefined : [...a.tools],
|
|
214
217
|
instructions: a.instructions?.trim() ? a.instructions : undefined,
|
|
215
218
|
})),
|
|
216
219
|
};
|
|
@@ -229,7 +232,7 @@ export function serializeWatchdogConfig(doc: WatchdogConfigDoc): string {
|
|
|
229
232
|
out.advisors = doc.advisors.map(a => {
|
|
230
233
|
const entry: AdvisorConfig = { name: a.name };
|
|
231
234
|
if (a.model?.trim()) entry.model = a.model;
|
|
232
|
-
if (a.tools
|
|
235
|
+
if (a.tools !== undefined) entry.tools = [...a.tools];
|
|
233
236
|
if (a.instructions?.trim()) entry.instructions = a.instructions;
|
|
234
237
|
return entry;
|
|
235
238
|
});
|
|
@@ -84,11 +84,11 @@ export class AutoLearnController {
|
|
|
84
84
|
return;
|
|
85
85
|
}
|
|
86
86
|
if (event.type === "agent_end") {
|
|
87
|
-
this.#onAgentEnd();
|
|
87
|
+
this.#onAgentEnd(event);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
-
#onAgentEnd(): void {
|
|
91
|
+
#onAgentEnd(event: Extract<AgentSessionEvent, { type: "agent_end" }>): void {
|
|
92
92
|
// Snapshot and reset every turn: the counter describes only the
|
|
93
93
|
// just-finished turn, so below-threshold, disabled, and plan-mode stops
|
|
94
94
|
// must not let tool calls accumulate into a later turn.
|
|
@@ -107,6 +107,18 @@ export class AutoLearnController {
|
|
|
107
107
|
this.#suppressNext = false;
|
|
108
108
|
return;
|
|
109
109
|
}
|
|
110
|
+
// Never nudge a turn that ended in an abort (ESC, cancel, etc.). The
|
|
111
|
+
// abort flag on the session is unreliable by the time agent_end is
|
|
112
|
+
// deferred to subscribers; read stopReason from the event messages.
|
|
113
|
+
for (let i = event.messages.length - 1; i >= 0; i--) {
|
|
114
|
+
const message = event.messages[i];
|
|
115
|
+
if (message && typeof message === "object" && "role" in message && message.role === "assistant") {
|
|
116
|
+
if ("stopReason" in message && message.stopReason === "aborted") {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
110
122
|
// Honor a live opt-out: the subscription outlives the setting, so re-check
|
|
111
123
|
// the current flag rather than trusting install-time state.
|
|
112
124
|
if (!this.#settings.get("autolearn.enabled")) return;
|
package/src/cli/args.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
STRING_SETTERS,
|
|
15
15
|
STRING_VALUE_FLAGS,
|
|
16
16
|
} from "./flag-tables";
|
|
17
|
+
import { CliUsageError } from "./usage-error";
|
|
17
18
|
|
|
18
19
|
export type Mode = "text" | "json" | "rpc" | "acp" | "rpc-ui";
|
|
19
20
|
|
|
@@ -299,6 +300,17 @@ export function reportUnrecognizedFlags(
|
|
|
299
300
|
return true;
|
|
300
301
|
}
|
|
301
302
|
|
|
303
|
+
/** Emit a clean CLI usage error without an internal stack trace. */
|
|
304
|
+
export function reportCliUsageError(
|
|
305
|
+
error: unknown,
|
|
306
|
+
write: (text: string) => void = text => process.stderr.write(text),
|
|
307
|
+
): boolean {
|
|
308
|
+
if (!(error instanceof CliUsageError)) return false;
|
|
309
|
+
write(`${chalk.red(`Error: ${error.message}`)}\n`);
|
|
310
|
+
write(`Run \`${APP_NAME} --help\` for available flags.\n`);
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
302
314
|
export function getExtraHelpText(): string {
|
|
303
315
|
return `${chalk.bold("Environment Variables:")}
|
|
304
316
|
${chalk.dim("# Core Providers")}
|
package/src/cli/flag-tables.ts
CHANGED
|
@@ -32,6 +32,7 @@
|
|
|
32
32
|
|
|
33
33
|
import type { ConfiguredThinkingLevel } from "../thinking";
|
|
34
34
|
import type { Args } from "./args";
|
|
35
|
+
import { CliUsageError } from "./usage-error";
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
38
|
* Runtime dependencies injected into setters that need to validate input or
|
|
@@ -85,6 +86,24 @@ const setResume: OptionalSetter = (result, value) => {
|
|
|
85
86
|
result.resume = value !== undefined ? value : true;
|
|
86
87
|
};
|
|
87
88
|
|
|
89
|
+
const MAX_TIME_DURATION_RE = /^(\d+(?:\.\d+)?)([smh])$/;
|
|
90
|
+
|
|
91
|
+
function maxTimeMultiplier(unit: string | undefined): number {
|
|
92
|
+
if (unit === "h") return 3600;
|
|
93
|
+
if (unit === "m") return 60;
|
|
94
|
+
return 1;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function parseMaxTimeSeconds(value: string): number {
|
|
98
|
+
const trimmed = value.trim();
|
|
99
|
+
const duration = MAX_TIME_DURATION_RE.exec(trimmed);
|
|
100
|
+
const seconds = duration ? Number(duration[1]) * maxTimeMultiplier(duration[2]) : Number(trimmed);
|
|
101
|
+
if (Number.isFinite(seconds) && seconds > 0) return seconds;
|
|
102
|
+
throw new CliUsageError(
|
|
103
|
+
`Invalid --max-time value: ${JSON.stringify(value)}. Expected a positive number of seconds or duration like "5s", "10m", "1h".`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
88
107
|
/**
|
|
89
108
|
* Setters for flags with string values. Most built-ins consume the next argv
|
|
90
109
|
* token even when it starts with `-`; flags listed in
|
|
@@ -121,13 +140,8 @@ export const STRING_SETTERS: Record<string, StringSetter> = {
|
|
|
121
140
|
"--plan": (result, value) => {
|
|
122
141
|
result.plan = value;
|
|
123
142
|
},
|
|
124
|
-
"--max-time": (result, value
|
|
125
|
-
|
|
126
|
-
if (Number.isFinite(seconds) && seconds > 0) {
|
|
127
|
-
result.maxTime = seconds;
|
|
128
|
-
} else {
|
|
129
|
-
deps.logger.warn("Invalid seconds passed to --max-time", { value });
|
|
130
|
-
}
|
|
143
|
+
"--max-time": (result, value) => {
|
|
144
|
+
result.maxTime = parseMaxTimeSeconds(value);
|
|
131
145
|
},
|
|
132
146
|
"--api-key": (result, value) => {
|
|
133
147
|
result.apiKey = value;
|
package/src/cli/update-cli.ts
CHANGED
|
@@ -112,6 +112,19 @@ async function getBunGlobalBinDir(): Promise<string | undefined> {
|
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
async function getNpmGlobalBinDir(): Promise<string | undefined> {
|
|
116
|
+
if (!$which("npm")) return undefined;
|
|
117
|
+
try {
|
|
118
|
+
const result = await $`npm prefix -g`.quiet().nothrow();
|
|
119
|
+
if (result.exitCode !== 0) return undefined;
|
|
120
|
+
const prefix = result.text().trim();
|
|
121
|
+
if (prefix.length === 0) return undefined;
|
|
122
|
+
return process.platform === "win32" ? prefix : path.join(prefix, "bin");
|
|
123
|
+
} catch {
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
115
128
|
async function getHomebrewFormulaPrefix(): Promise<string | undefined> {
|
|
116
129
|
if (!$which("brew")) return undefined;
|
|
117
130
|
for (const formula of [HOMEBREW_FORMULA, APP_NAME]) {
|
|
@@ -192,26 +205,36 @@ function isPathInDirectory(filePath: string, directoryPath: string): boolean {
|
|
|
192
205
|
return isPathInDirectoryLexical(resolvedFile, dirReal);
|
|
193
206
|
}
|
|
194
207
|
|
|
195
|
-
type UpdateMethod = "brew" | "mise" | "bun" | "binary";
|
|
208
|
+
type UpdateMethod = "brew" | "mise" | "bun" | "npm" | "binary";
|
|
196
209
|
|
|
197
210
|
interface UpdateMethodResolutionOptions {
|
|
198
211
|
homebrewPrefix?: string;
|
|
199
212
|
miseBinDirs?: readonly string[];
|
|
200
213
|
miseDataDir?: string;
|
|
214
|
+
npmBinDir?: string;
|
|
201
215
|
}
|
|
202
216
|
|
|
203
|
-
type UpdateTarget =
|
|
217
|
+
type UpdateTarget =
|
|
218
|
+
| { method: "brew" }
|
|
219
|
+
| { method: "mise" }
|
|
220
|
+
| { method: "bun" }
|
|
221
|
+
| { method: "npm" }
|
|
222
|
+
| { method: "binary"; path: string };
|
|
204
223
|
|
|
205
224
|
function resolveUpdateMethod(
|
|
206
225
|
jeopiPath: string,
|
|
207
226
|
bunBinDir: string | undefined,
|
|
208
227
|
options: UpdateMethodResolutionOptions = {},
|
|
209
228
|
): UpdateMethod {
|
|
210
|
-
const { homebrewPrefix, miseBinDirs = [], miseDataDir } = options;
|
|
229
|
+
const { homebrewPrefix, miseBinDirs = [], miseDataDir, npmBinDir } = options;
|
|
230
|
+
const launcherExtension = path.extname(jeopiPath).toLowerCase();
|
|
231
|
+
const isWindowsScriptLauncher =
|
|
232
|
+
launcherExtension === ".cmd" || launcherExtension === ".ps1" || launcherExtension === ".bat";
|
|
211
233
|
if (homebrewPrefix && isPathInDirectory(jeopiPath, path.join(homebrewPrefix, "bin"))) return "brew";
|
|
212
234
|
if (miseBinDirs.some(dir => isPathInDirectory(jeopiPath, dir))) return "mise";
|
|
213
235
|
if (miseDataDir && isPathInDirectory(jeopiPath, path.join(miseDataDir, "shims"))) return "mise";
|
|
214
236
|
if (bunBinDir && isPathInDirectory(jeopiPath, bunBinDir)) return "bun";
|
|
237
|
+
if ((npmBinDir && isPathInDirectory(jeopiPath, npmBinDir)) || isWindowsScriptLauncher) return "npm";
|
|
215
238
|
return "binary";
|
|
216
239
|
}
|
|
217
240
|
|
|
@@ -224,6 +247,7 @@ export function resolveUpdateMethodForTest(
|
|
|
224
247
|
}
|
|
225
248
|
async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
226
249
|
const bunBinDir = await getBunGlobalBinDir();
|
|
250
|
+
const npmBinDir = await getNpmGlobalBinDir();
|
|
227
251
|
const homebrewPrefix = await getHomebrewFormulaPrefix();
|
|
228
252
|
const miseAvailable = $which("mise") !== undefined;
|
|
229
253
|
const miseBinDirs = miseAvailable ? await getMiseBinDirs() : [];
|
|
@@ -231,7 +255,7 @@ async function resolveUpdateTarget(): Promise<UpdateTarget> {
|
|
|
231
255
|
const jeopiPath = resolveJeopiPath();
|
|
232
256
|
|
|
233
257
|
if (jeopiPath) {
|
|
234
|
-
const method = resolveUpdateMethod(jeopiPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir });
|
|
258
|
+
const method = resolveUpdateMethod(jeopiPath, bunBinDir, { homebrewPrefix, miseBinDirs, miseDataDir, npmBinDir });
|
|
235
259
|
if (method === "binary") return { method, path: jeopiPath };
|
|
236
260
|
return { method };
|
|
237
261
|
}
|
|
@@ -874,6 +898,15 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
|
|
|
874
898
|
}
|
|
875
899
|
}
|
|
876
900
|
|
|
901
|
+
function buildVersionedPackageInstallArgs(expectedVersion: string, nativeTag: string): string[] {
|
|
902
|
+
const args = [`${NPM_PACKAGE}@${expectedVersion}`, `${NATIVES_PACKAGE}@${expectedVersion}`];
|
|
903
|
+
const leafPackage = NATIVE_LEAF_PACKAGES.get(nativeTag);
|
|
904
|
+
if (leafPackage) {
|
|
905
|
+
args.push(`${leafPackage}@${expectedVersion}`);
|
|
906
|
+
}
|
|
907
|
+
return args;
|
|
908
|
+
}
|
|
909
|
+
|
|
877
910
|
/**
|
|
878
911
|
* Build the bun argv used to globally install a specific jeopi version.
|
|
879
912
|
*
|
|
@@ -905,19 +938,23 @@ export async function replaceBinaryForUpdate(options: BinaryReplacementOptions):
|
|
|
905
938
|
* See #1824.
|
|
906
939
|
*/
|
|
907
940
|
export function buildBunInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
|
|
908
|
-
|
|
941
|
+
return [
|
|
909
942
|
"install",
|
|
910
943
|
"-g",
|
|
911
944
|
"--no-cache",
|
|
912
945
|
`--registry=${NPM_REGISTRY}`,
|
|
913
|
-
|
|
914
|
-
|
|
946
|
+
...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
|
|
947
|
+
];
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
/** Build the npm argv used to update npm-managed global installs. */
|
|
951
|
+
export function buildNpmInstallArgs(expectedVersion: string, nativeTag: string = currentNativeTag()): string[] {
|
|
952
|
+
return [
|
|
953
|
+
"install",
|
|
954
|
+
"-g",
|
|
955
|
+
`--registry=${NPM_REGISTRY}`,
|
|
956
|
+
...buildVersionedPackageInstallArgs(expectedVersion, nativeTag),
|
|
915
957
|
];
|
|
916
|
-
const leafPackage = NATIVE_LEAF_PACKAGES.get(nativeTag);
|
|
917
|
-
if (leafPackage) {
|
|
918
|
-
args.push(`${leafPackage}@${expectedVersion}`);
|
|
919
|
-
}
|
|
920
|
-
return args;
|
|
921
958
|
}
|
|
922
959
|
|
|
923
960
|
export function buildHomebrewUpdateArgs(force: boolean): string[] {
|
|
@@ -933,7 +970,7 @@ export function buildMiseForceInstallArgs(expectedVersion: string): string[] {
|
|
|
933
970
|
}
|
|
934
971
|
|
|
935
972
|
/**
|
|
936
|
-
* Update via
|
|
973
|
+
* Update via package manager.
|
|
937
974
|
*/
|
|
938
975
|
async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
939
976
|
console.log(chalk.dim("Updating via bun..."));
|
|
@@ -954,6 +991,17 @@ async function updateViaBun(expectedVersion: string): Promise<void> {
|
|
|
954
991
|
}
|
|
955
992
|
}
|
|
956
993
|
|
|
994
|
+
async function updateViaNpm(expectedVersion: string): Promise<void> {
|
|
995
|
+
console.log(chalk.dim("Updating via npm..."));
|
|
996
|
+
const args = buildNpmInstallArgs(expectedVersion);
|
|
997
|
+
const result = await $`npm ${args}`.nothrow();
|
|
998
|
+
if (result.exitCode !== 0) {
|
|
999
|
+
throw new Error(`npm install failed with exit code ${result.exitCode}`);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
await printVerification(expectedVersion);
|
|
1003
|
+
}
|
|
1004
|
+
|
|
957
1005
|
async function updateViaHomebrew(expectedVersion: string, force: boolean): Promise<void> {
|
|
958
1006
|
console.log(chalk.dim("Updating Homebrew formulae..."));
|
|
959
1007
|
const update = await $`brew update`.nothrow();
|
|
@@ -1089,6 +1137,8 @@ export async function runUpdateCommand(opts: { force: boolean; check: boolean })
|
|
|
1089
1137
|
await updateViaMise(release.version, opts.force);
|
|
1090
1138
|
} else if (target.method === "bun") {
|
|
1091
1139
|
await updateViaBun(release.version);
|
|
1140
|
+
} else if (target.method === "npm") {
|
|
1141
|
+
await updateViaNpm(release.version);
|
|
1092
1142
|
} else {
|
|
1093
1143
|
await updateViaBinaryAt(target.path, release.version);
|
|
1094
1144
|
}
|
package/src/commands/acp.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* ACP terminal-auth flag asks the same command to open the interactive TUI.
|
|
6
6
|
*/
|
|
7
7
|
import { Command } from "jeopi-utils/cli";
|
|
8
|
-
import { parseArgs } from "../cli/args";
|
|
8
|
+
import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args";
|
|
9
9
|
import { runRootCommand } from "../main";
|
|
10
10
|
import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
|
|
11
11
|
|
|
@@ -15,7 +15,16 @@ export default class Acp extends Command {
|
|
|
15
15
|
|
|
16
16
|
async run(): Promise<void> {
|
|
17
17
|
const { args, terminalAuth } = prepareAcpTerminalAuthArgs(this.argv);
|
|
18
|
-
|
|
18
|
+
let parsed: ParsedArgs;
|
|
19
|
+
try {
|
|
20
|
+
parsed = parseArgs(args);
|
|
21
|
+
} catch (error) {
|
|
22
|
+
if (reportCliUsageError(error)) {
|
|
23
|
+
process.exitCode = 2;
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
19
28
|
if (!terminalAuth) {
|
|
20
29
|
parsed.mode = "acp";
|
|
21
30
|
}
|
package/src/commands/launch.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import { APP_NAME } from "jeopi-utils";
|
|
6
6
|
import { Args, Command, Flags } from "jeopi-utils/cli";
|
|
7
|
-
import { parseArgs } from "../cli/args";
|
|
7
|
+
import { type Args as ParsedArgs, parseArgs, reportCliUsageError } from "../cli/args";
|
|
8
8
|
import { runRootCommand } from "../main";
|
|
9
9
|
import { prepareAcpTerminalAuthArgs } from "../modes/acp/terminal-auth";
|
|
10
10
|
import { CLI_THINKING_LEVELS } from "../thinking";
|
|
@@ -140,7 +140,7 @@ export default class Index extends Command {
|
|
|
140
140
|
description: "Include thinking blocks in print mode text output",
|
|
141
141
|
}),
|
|
142
142
|
"max-time": Flags.string({
|
|
143
|
-
description: "Stop the session after this
|
|
143
|
+
description: "Stop the session after this duration (e.g., 600, 10m, 1h)",
|
|
144
144
|
}),
|
|
145
145
|
// `--auto-approve` / `--yolo`: declared here so oclif's auto-generated `--help` lists it.
|
|
146
146
|
// Runtime parsing happens in `cli/args.ts parseArgs` (line 176 in that file) — `runRootCommand`
|
|
@@ -176,7 +176,16 @@ export default class Index extends Command {
|
|
|
176
176
|
|
|
177
177
|
async run(): Promise<void> {
|
|
178
178
|
const { args } = prepareAcpTerminalAuthArgs(this.argv);
|
|
179
|
-
|
|
179
|
+
let parsed: ParsedArgs;
|
|
180
|
+
try {
|
|
181
|
+
parsed = parseArgs(args);
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (reportCliUsageError(error)) {
|
|
184
|
+
process.exitCode = 2;
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
180
189
|
await runRootCommand(parsed, args);
|
|
181
190
|
}
|
|
182
191
|
}
|
|
@@ -29,6 +29,7 @@ export interface CommitAgentInput {
|
|
|
29
29
|
requireChangelog: boolean;
|
|
30
30
|
diffText?: string;
|
|
31
31
|
existingChangelogEntries?: ExistingChangelogEntries[];
|
|
32
|
+
onComplete?: (state: CommitAgentState) => Promise<void> | void;
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
export interface ExistingChangelogEntries {
|
|
@@ -175,6 +176,9 @@ export async function runCommitAgentSession(input: CommitAgentInput): Promise<Co
|
|
|
175
176
|
});
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
if (input.onComplete) {
|
|
180
|
+
await input.onComplete(state);
|
|
181
|
+
}
|
|
178
182
|
return state;
|
|
179
183
|
} finally {
|
|
180
184
|
unsubscribe();
|
|
@@ -6,7 +6,7 @@ import { detectChangelogBoundaries } from "../../commit/changelog/detect";
|
|
|
6
6
|
import { parseUnreleasedSection } from "../../commit/changelog/parse";
|
|
7
7
|
import { formatCommitMessage } from "../../commit/message";
|
|
8
8
|
import { resolvePrimaryModel, resolveSmolModel } from "../../commit/model-selection";
|
|
9
|
-
import type { CommitCommandArgs, ConventionalAnalysis } from "../../commit/types";
|
|
9
|
+
import type { CommitCommandArgs, ConventionalAnalysis, NumstatEntry } from "../../commit/types";
|
|
10
10
|
import { ModelRegistry } from "../../config/model-registry";
|
|
11
11
|
import { Settings } from "../../config/settings";
|
|
12
12
|
import { discoverAuthStorage, discoverContextFiles } from "../../sdk";
|
|
@@ -121,11 +121,10 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
process.stdout.write("● Starting commit agent...\n");
|
|
124
|
-
let
|
|
125
|
-
let usedFallback = false;
|
|
124
|
+
let agentSessionCompleted = false;
|
|
126
125
|
|
|
127
126
|
try {
|
|
128
|
-
|
|
127
|
+
await runCommitAgentSession({
|
|
129
128
|
cwd,
|
|
130
129
|
model: agentModel,
|
|
131
130
|
thinkingLevel: agentThinkingLevel,
|
|
@@ -138,42 +137,67 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
138
137
|
requireChangelog: !args.noChangelog && changelogTargets.length > 0,
|
|
139
138
|
diffText: diff,
|
|
140
139
|
existingChangelogEntries,
|
|
140
|
+
onComplete: async commitState => {
|
|
141
|
+
agentSessionCompleted = true;
|
|
142
|
+
await completeAgentCommitState(commitState, {
|
|
143
|
+
cwd,
|
|
144
|
+
dryRun: args.dryRun,
|
|
145
|
+
push: args.push,
|
|
146
|
+
noChangelog: args.noChangelog,
|
|
147
|
+
changelogTargets,
|
|
148
|
+
numstat,
|
|
149
|
+
});
|
|
150
|
+
},
|
|
141
151
|
});
|
|
152
|
+
return;
|
|
142
153
|
} catch (error) {
|
|
154
|
+
if (agentSessionCompleted) {
|
|
155
|
+
throw error;
|
|
156
|
+
}
|
|
143
157
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
144
158
|
process.stderr.write(`Agent error: ${errorMessage}\n`);
|
|
145
159
|
if (error instanceof Error && error.stack && $env.DEBUG) {
|
|
146
160
|
process.stderr.write(`${error.stack}\n`);
|
|
147
161
|
}
|
|
148
162
|
process.stdout.write("● Using fallback commit generation...\n");
|
|
149
|
-
|
|
150
|
-
|
|
163
|
+
const fallbackProposal = generateFallbackProposal(numstat);
|
|
164
|
+
await runSingleCommit(fallbackProposal, { cwd, dryRun: args.dryRun, push: args.push });
|
|
165
|
+
return;
|
|
151
166
|
}
|
|
167
|
+
}
|
|
152
168
|
|
|
153
|
-
|
|
169
|
+
async function completeAgentCommitState(
|
|
170
|
+
commitState: CommitAgentState,
|
|
171
|
+
ctx: CommitExecutionContext & {
|
|
172
|
+
noChangelog: boolean;
|
|
173
|
+
changelogTargets: string[];
|
|
174
|
+
numstat: NumstatEntry[];
|
|
175
|
+
},
|
|
176
|
+
): Promise<void> {
|
|
177
|
+
let usedFallback = false;
|
|
178
|
+
if (!commitState.proposal && !commitState.splitProposal) {
|
|
154
179
|
if ($env.PI_COMMIT_NO_FALLBACK?.toLowerCase() !== "true") {
|
|
155
180
|
process.stdout.write("● Agent did not provide proposal, using fallback...\n");
|
|
156
|
-
commitState.proposal = generateFallbackProposal(numstat);
|
|
181
|
+
commitState.proposal = generateFallbackProposal(ctx.numstat);
|
|
157
182
|
usedFallback = true;
|
|
158
183
|
}
|
|
159
184
|
}
|
|
160
185
|
|
|
161
186
|
let updatedChangelogFiles: string[] = [];
|
|
162
|
-
if (!
|
|
187
|
+
if (!ctx.noChangelog && ctx.changelogTargets.length > 0 && !usedFallback) {
|
|
163
188
|
if (!commitState.changelogProposal) {
|
|
164
|
-
|
|
165
|
-
return;
|
|
189
|
+
throw new Error("Commit agent did not provide changelog entries.");
|
|
166
190
|
}
|
|
167
191
|
process.stdout.write("● Applying changelog entries...\n");
|
|
168
192
|
const updated = await applyChangelogProposals({
|
|
169
|
-
cwd,
|
|
193
|
+
cwd: ctx.cwd,
|
|
170
194
|
proposals: commitState.changelogProposal.entries,
|
|
171
|
-
dryRun:
|
|
195
|
+
dryRun: ctx.dryRun,
|
|
172
196
|
onProgress: message => {
|
|
173
197
|
process.stdout.write(` ├─ ${message}\n`);
|
|
174
198
|
},
|
|
175
199
|
});
|
|
176
|
-
updatedChangelogFiles = updated.map(filePath => path.relative(cwd, filePath));
|
|
200
|
+
updatedChangelogFiles = updated.map(filePath => path.relative(ctx.cwd, filePath));
|
|
177
201
|
if (updated.length > 0) {
|
|
178
202
|
for (const filePath of updated) {
|
|
179
203
|
process.stdout.write(` └─ ${filePath}\n`);
|
|
@@ -184,21 +208,21 @@ export async function runAgenticCommit(args: CommitCommandArgs): Promise<void> {
|
|
|
184
208
|
}
|
|
185
209
|
|
|
186
210
|
if (commitState.proposal) {
|
|
187
|
-
await runSingleCommit(commitState.proposal,
|
|
211
|
+
await runSingleCommit(commitState.proposal, ctx);
|
|
188
212
|
return;
|
|
189
213
|
}
|
|
190
214
|
|
|
191
215
|
if (commitState.splitProposal) {
|
|
192
216
|
await runSplitCommit(commitState.splitProposal, {
|
|
193
|
-
cwd,
|
|
194
|
-
dryRun:
|
|
195
|
-
push:
|
|
217
|
+
cwd: ctx.cwd,
|
|
218
|
+
dryRun: ctx.dryRun,
|
|
219
|
+
push: ctx.push,
|
|
196
220
|
additionalFiles: updatedChangelogFiles,
|
|
197
221
|
});
|
|
198
222
|
return;
|
|
199
223
|
}
|
|
200
224
|
|
|
201
|
-
|
|
225
|
+
throw new Error("Commit agent did not provide a proposal.");
|
|
202
226
|
}
|
|
203
227
|
|
|
204
228
|
async function runSingleCommit(proposal: CommitProposal, ctx: CommitExecutionContext): Promise<void> {
|
|
@@ -211,6 +235,7 @@ async function runSingleCommit(proposal: CommitProposal, ctx: CommitExecutionCon
|
|
|
211
235
|
process.stdout.write(`${commitMessage}\n`);
|
|
212
236
|
return;
|
|
213
237
|
}
|
|
238
|
+
process.stdout.write("● Creating commit...\n");
|
|
214
239
|
await git.commit(ctx.cwd, commitMessage);
|
|
215
240
|
process.stdout.write("Commit created.\n");
|
|
216
241
|
if (ctx.push) {
|
|
@@ -233,8 +258,7 @@ async function runSplitCommit(
|
|
|
233
258
|
const plannedFiles = new Set(plan.commits.flatMap(commit => commit.changes.map(change => change.path)));
|
|
234
259
|
const missingFiles = stagedFiles.filter(file => !plannedFiles.has(file));
|
|
235
260
|
if (missingFiles.length > 0) {
|
|
236
|
-
|
|
237
|
-
return;
|
|
261
|
+
throw new Error(`Split commit plan missing staged files: ${missingFiles.join(", ")}`);
|
|
238
262
|
}
|
|
239
263
|
|
|
240
264
|
if (ctx.dryRun) {
|
|
@@ -266,6 +290,7 @@ async function runSplitCommit(
|
|
|
266
290
|
throw new Error(order.error);
|
|
267
291
|
}
|
|
268
292
|
|
|
293
|
+
process.stdout.write("● Creating split commits...\n");
|
|
269
294
|
const stagedDiff = await git.diff(ctx.cwd, { cached: true });
|
|
270
295
|
await git.stage.reset(ctx.cwd);
|
|
271
296
|
for (const commitIndex of order) {
|