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.
Files changed (125) hide show
  1. package/CHANGELOG.md +74 -0
  2. package/dist/cli.js +3179 -3140
  3. package/dist/types/advisor/config.d.ts +4 -3
  4. package/dist/types/cli/args.d.ts +2 -0
  5. package/dist/types/cli/update-cli.d.ts +4 -1
  6. package/dist/types/cli/usage-error.d.ts +4 -0
  7. package/dist/types/commit/agentic/agent.d.ts +1 -0
  8. package/dist/types/config/model-resolver.d.ts +7 -1
  9. package/dist/types/config/settings-schema.d.ts +3 -3
  10. package/dist/types/eval/py/tool-bridge.d.ts +1 -0
  11. package/dist/types/extensibility/custom-tools/types.d.ts +3 -0
  12. package/dist/types/extensibility/extensions/runner.d.ts +3 -1
  13. package/dist/types/extensibility/extensions/types.d.ts +3 -0
  14. package/dist/types/internal-urls/memory-protocol.d.ts +6 -6
  15. package/dist/types/internal-urls/types.d.ts +7 -0
  16. package/dist/types/irc/bus.d.ts +4 -0
  17. package/dist/types/lsp/deferred-diagnostics.d.ts +11 -0
  18. package/dist/types/modes/components/move-overlay.d.ts +1 -1
  19. package/dist/types/modes/components/plan-review-overlay.d.ts +2 -0
  20. package/dist/types/modes/components/welcome.d.ts +4 -0
  21. package/dist/types/modes/magic-keyword-boundary.d.ts +9 -0
  22. package/dist/types/modes/orchestrate.d.ts +1 -1
  23. package/dist/types/modes/ultrathink.d.ts +1 -1
  24. package/dist/types/modes/workflow.d.ts +1 -1
  25. package/dist/types/session/agent-session.d.ts +5 -0
  26. package/dist/types/session/auth-broker-config.d.ts +2 -2
  27. package/dist/types/tools/bash-skill-urls.d.ts +3 -2
  28. package/dist/types/tools/browser/cmux/cmux-tab.d.ts +3 -1
  29. package/dist/types/tools/browser/cmux/rpc.d.ts +20 -0
  30. package/dist/types/tools/browser/launch.d.ts +1 -0
  31. package/dist/types/tools/browser/run-cancellation.d.ts +17 -2
  32. package/dist/types/tools/browser/run-output.d.ts +25 -0
  33. package/dist/types/tools/browser/tab-worker.d.ts +22 -0
  34. package/dist/types/utils/changelog.d.ts +35 -1
  35. package/package.json +12 -12
  36. package/src/advisor/__tests__/advisor.test.ts +13 -0
  37. package/src/advisor/__tests__/config.test.ts +36 -0
  38. package/src/advisor/config.ts +11 -8
  39. package/src/autolearn/controller.ts +14 -2
  40. package/src/cli/args.ts +12 -0
  41. package/src/cli/flag-tables.ts +21 -7
  42. package/src/cli/update-cli.ts +63 -13
  43. package/src/cli/usage-error.ts +7 -0
  44. package/src/commands/acp.ts +11 -2
  45. package/src/commands/launch.ts +12 -3
  46. package/src/commit/agentic/agent.ts +4 -0
  47. package/src/commit/agentic/index.ts +46 -21
  48. package/src/config/model-resolver.ts +24 -6
  49. package/src/config/settings-schema.ts +3 -3
  50. package/src/edit/index.ts +10 -76
  51. package/src/edit/renderer.ts +13 -10
  52. package/src/eval/__tests__/agent-bridge.test.ts +19 -14
  53. package/src/eval/__tests__/bridge-timeout.test.ts +60 -0
  54. package/src/eval/executor-base.ts +84 -7
  55. package/src/eval/js/shared/rewrite-imports.ts +31 -13
  56. package/src/eval/py/tool-bridge.ts +13 -14
  57. package/src/exec/non-interactive-env.ts +0 -1
  58. package/src/export/ttsr.ts +0 -3
  59. package/src/extensibility/custom-tools/loader.ts +70 -37
  60. package/src/extensibility/custom-tools/types.ts +3 -0
  61. package/src/extensibility/extensions/runner.ts +3 -0
  62. package/src/extensibility/extensions/types.ts +3 -0
  63. package/src/extensibility/plugins/manager.ts +21 -0
  64. package/src/internal-urls/docs-index.generated.txt +1 -1
  65. package/src/internal-urls/memory-protocol.ts +13 -8
  66. package/src/internal-urls/skill-protocol.ts +1 -1
  67. package/src/internal-urls/types.ts +7 -0
  68. package/src/irc/bus.ts +48 -18
  69. package/src/lsp/client.ts +7 -1
  70. package/src/lsp/deferred-diagnostics.ts +66 -0
  71. package/src/main.ts +13 -13
  72. package/src/mcp/tool-bridge.ts +57 -6
  73. package/src/mcp/transports/stdio.test.ts +45 -1
  74. package/src/mcp/transports/stdio.ts +6 -3
  75. package/src/modes/acp/acp-agent.ts +73 -2
  76. package/src/modes/acp/acp-event-mapper.ts +5 -0
  77. package/src/modes/acp/acp-mode.ts +11 -0
  78. package/src/modes/components/__tests__/move-overlay.test.ts +16 -1
  79. package/src/modes/components/advisor-config.ts +15 -7
  80. package/src/modes/components/move-overlay.ts +2 -3
  81. package/src/modes/components/plan-review-overlay.ts +7 -0
  82. package/src/modes/components/tips.txt +1 -1
  83. package/src/modes/components/welcome.ts +13 -14
  84. package/src/modes/controllers/command-controller.ts +8 -10
  85. package/src/modes/controllers/event-controller.ts +17 -30
  86. package/src/modes/controllers/input-controller.ts +9 -6
  87. package/src/modes/controllers/selector-controller.ts +3 -2
  88. package/src/modes/controllers/tan-command-controller.ts +40 -1
  89. package/src/modes/interactive-mode.ts +13 -0
  90. package/src/modes/magic-keyword-boundary.ts +23 -0
  91. package/src/modes/orchestrate.ts +6 -5
  92. package/src/modes/print-mode.ts +9 -0
  93. package/src/modes/ultrathink.ts +6 -5
  94. package/src/modes/workflow.ts +5 -4
  95. package/src/prompts/system/eager-task.md +2 -2
  96. package/src/prompts/system/tan-context-switch.md +17 -0
  97. package/src/prompts/tools/browser.md +5 -5
  98. package/src/sdk.ts +3 -0
  99. package/src/session/agent-session.ts +172 -25
  100. package/src/session/auth-broker-config.ts +4 -4
  101. package/src/session/snapcompact-inline.ts +3 -19
  102. package/src/session/streaming-output.ts +40 -12
  103. package/src/slash-commands/builtin-registry.ts +8 -8
  104. package/src/task/executor.ts +8 -0
  105. package/src/tools/bash-skill-urls.ts +7 -4
  106. package/src/tools/bash.ts +30 -22
  107. package/src/tools/browser/cmux/cmux-tab.ts +49 -50
  108. package/src/tools/browser/cmux/rpc.ts +50 -0
  109. package/src/tools/browser/launch.ts +8 -4
  110. package/src/tools/browser/run-cancellation.ts +44 -5
  111. package/src/tools/browser/run-output.ts +76 -0
  112. package/src/tools/browser/tab-worker.ts +280 -133
  113. package/src/tools/eval-render.ts +16 -14
  114. package/src/tools/glob.ts +20 -6
  115. package/src/tools/grep.ts +1 -0
  116. package/src/tools/image-gen.ts +3 -4
  117. package/src/tools/irc.ts +27 -13
  118. package/src/tools/read.ts +14 -7
  119. package/src/tools/write.ts +22 -5
  120. package/src/tools/yield.ts +29 -1
  121. package/src/utils/changelog.ts +106 -9
  122. package/src/utils/git.ts +0 -1
  123. package/src/utils/title-generator.ts +68 -7
  124. package/src/web/search/index.ts +14 -6
  125. package/src/web/search/providers/perplexity.ts +18 -2
@@ -15,6 +15,7 @@ export interface PyToolBridgeEntry {
15
15
  toolSession: ToolSession;
16
16
  signal?: AbortSignal;
17
17
  emitStatus?: (event: JsStatusEvent) => void;
18
+ abortRequested?: () => boolean;
18
19
  }
19
20
 
20
21
  export interface PyToolBridgeInfo {
@@ -31,23 +32,21 @@ const registrations = new Map<string, PyToolBridgeEntry>();
31
32
  let serverPromise: Promise<BridgeServer> | null = null;
32
33
 
33
34
  /**
34
- * Forward a bridge call to {@link callSessionTool}, but resolve the HTTP request
35
- * the instant the cell's signal aborts instead of waiting for the tool/subagent
36
- * to fully tear down.
35
+ * Forward a bridge call to {@link callSessionTool} while respecting eval abort
36
+ * shielding.
37
37
  *
38
- * The kernel invokes this bridge with a *blocking* `urllib` request from a
39
- * worker thread (each `agent()` / `tool.*` call). When the cell is interrupted,
40
- * `parallel()`'s `ThreadPoolExecutor.__exit__` joins those worker threads
41
- * (`shutdown(wait=True)`), so they cannot unwind until their `urllib` call
42
- * returns — i.e. until this handler responds. A host-side `agent()` teardown
43
- * (aborting nested LLM streams + tools across a wide fan-out) routinely exceeds
44
- * the kernel's SIGINT escalation window, so the kernel was hard-killed and its
45
- * persistent state lost while the subagents were still winding down. Responding
46
- * immediately on abort lets the kernel raise through the blocked call and settle
47
- * cleanly (preserving state); the already-signaled call keeps tearing down in
48
- * the background, its eventual result/rejection swallowed.
38
+ * Python invokes this bridge with blocking `urllib` requests from worker threads
39
+ * (each `agent()` / `tool.*` call). The base executor defers the registered
40
+ * signal while a bridge call is already paused so in-flight subagents can finish
41
+ * and persist output instead of being orphaned. Once an abort has been requested,
42
+ * later bridge calls are rejected before starting; once the shielded signal
43
+ * finally aborts, this handler still resolves the HTTP request promptly so the
44
+ * kernel can unwind without being hard-killed.
49
45
  */
50
46
  async function callSessionToolPromptOnAbort(name: string, args: unknown, entry: PyToolBridgeEntry): Promise<unknown> {
47
+ if (entry.abortRequested?.()) {
48
+ throw new Error(`bridge call ${JSON.stringify(name)} aborted: eval cell was interrupted`);
49
+ }
51
50
  const call = callSessionTool(name, args, {
52
51
  session: entry.toolSession,
53
52
  signal: entry.signal,
@@ -15,7 +15,6 @@ export const NON_INTERACTIVE_ENV: Readonly<Record<string, string>> = {
15
15
  LESS: "FRX",
16
16
  // Disable terminal features that can block the process.
17
17
  TERM: "dumb",
18
- GPG_TTY: "not a tty",
19
18
  NO_COLOR: "1",
20
19
  PYTHONUNBUFFERED: "1",
21
20
  // Disable editor and terminal credential prompts.
@@ -447,9 +447,6 @@ export class TtsrManager {
447
447
  strictness: AstMatchStrictness.Smart,
448
448
  limit: 1,
449
449
  });
450
- if (result.parseErrors && result.parseErrors.length > 0) {
451
- logger.debug("TTSR ast match reported parse errors", { parseErrors: result.parseErrors });
452
- }
453
450
  return result.totalMatches > 0;
454
451
  } catch (error) {
455
452
  logger.warn("TTSR ast match failed, treating as no match", {
@@ -24,23 +24,53 @@ import type { CustomToolAPI, CustomToolFactory, LoadedCustomTool, ToolLoadError
24
24
  /**
25
25
  * Load a single tool module using native Bun import.
26
26
  */
27
+ interface LoadToolResult {
28
+ tools: LoadedCustomTool[];
29
+ errors: ToolLoadError[];
30
+ }
31
+
32
+ function isLoadableCustomTool(value: unknown): value is LoadedCustomTool["tool"] {
33
+ return (
34
+ typeof value === "object" &&
35
+ value !== null &&
36
+ "name" in value &&
37
+ typeof value.name === "string" &&
38
+ value.name.length > 0 &&
39
+ "description" in value &&
40
+ typeof value.description === "string" &&
41
+ "parameters" in value &&
42
+ "execute" in value &&
43
+ typeof value.execute === "function"
44
+ );
45
+ }
46
+
47
+ function invalidToolError(path: string, index: number, source: ToolLoadError["source"]): ToolLoadError {
48
+ return {
49
+ path,
50
+ error: `Tool factory returned invalid tool at index ${index}: expected object with string name, string description, parameters, and execute function`,
51
+ source,
52
+ };
53
+ }
54
+
27
55
  async function loadTool(
28
56
  toolPath: string,
29
57
  cwd: string,
30
58
  sharedApi: CustomToolAPI,
31
59
  source?: { provider: string; providerName: string; level: "user" | "project" },
32
- ): Promise<{ tools: LoadedCustomTool[] | null; error: ToolLoadError | null }> {
60
+ ): Promise<LoadToolResult> {
33
61
  const resolvedPath = resolvePath(toolPath, cwd);
34
62
 
35
63
  // Skip declarative tool files (.md, .json) - these are metadata only, not executable modules
36
64
  if (resolvedPath.endsWith(".md") || resolvedPath.endsWith(".json")) {
37
65
  return {
38
- tools: null,
39
- error: {
40
- path: toolPath,
41
- error: "Declarative tool files (.md, .json) cannot be loaded as executable modules",
42
- source,
43
- },
66
+ tools: [],
67
+ errors: [
68
+ {
69
+ path: toolPath,
70
+ error: "Declarative tool files (.md, .json) cannot be loaded as executable modules",
71
+ source,
72
+ },
73
+ ],
44
74
  };
45
75
  }
46
76
 
@@ -49,23 +79,32 @@ async function loadTool(
49
79
  const factory = (module.default ?? module) as CustomToolFactory;
50
80
 
51
81
  if (typeof factory !== "function") {
52
- return { tools: null, error: { path: toolPath, error: "Tool must export a default function", source } };
82
+ return { tools: [], errors: [{ path: toolPath, error: "Tool must export a default function", source }] };
53
83
  }
54
84
 
55
- const toolResult = await factory(sharedApi);
85
+ const toolResult: unknown = await factory(sharedApi);
56
86
  const toolsArray = Array.isArray(toolResult) ? toolResult : [toolResult];
57
87
 
58
- const loadedTools: LoadedCustomTool[] = toolsArray.map(tool => ({
59
- path: toolPath,
60
- resolvedPath,
61
- tool,
62
- source,
63
- }));
88
+ const loadedTools: LoadedCustomTool[] = [];
89
+ const errors: ToolLoadError[] = [];
90
+ for (const [index, tool] of toolsArray.entries()) {
91
+ if (!isLoadableCustomTool(tool)) {
92
+ errors.push(invalidToolError(toolPath, index, source));
93
+ continue;
94
+ }
95
+
96
+ loadedTools.push({
97
+ path: toolPath,
98
+ resolvedPath,
99
+ tool,
100
+ source,
101
+ });
102
+ }
64
103
 
65
- return { tools: loadedTools, error: null };
104
+ return { tools: loadedTools, errors };
66
105
  } catch (err) {
67
106
  const message = err instanceof Error ? err.message : String(err);
68
- return { tools: null, error: { path: toolPath, error: `Failed to load tool: ${message}`, source } };
107
+ return { tools: [], errors: [{ path: toolPath, error: `Failed to load tool: ${message}`, source }] };
69
108
  }
70
109
  }
71
110
 
@@ -129,28 +168,22 @@ export class CustomToolLoader {
129
168
 
130
169
  async load(pathsWithSources: ToolPathWithSource[]): Promise<void> {
131
170
  for (const { path: toolPath, source } of pathsWithSources) {
132
- const { tools: loadedTools, error } = await loadTool(toolPath, this.#sharedApi.cwd, this.#sharedApi, source);
171
+ const { tools: loadedTools, errors } = await loadTool(toolPath, this.#sharedApi.cwd, this.#sharedApi, source);
172
+ this.errors.push(...errors);
133
173
 
134
- if (error) {
135
- this.errors.push(error);
136
- continue;
137
- }
138
-
139
- if (loadedTools) {
140
- for (const loadedTool of loadedTools) {
141
- // Check for name conflicts
142
- if (this.#seenNames.has(loadedTool.tool.name)) {
143
- this.errors.push({
144
- path: toolPath,
145
- error: `Tool name "${loadedTool.tool.name}" conflicts with existing tool`,
146
- source,
147
- });
148
- continue;
149
- }
150
-
151
- this.#seenNames.add(loadedTool.tool.name);
152
- this.tools.push(loadedTool);
174
+ for (const loadedTool of loadedTools) {
175
+ // Check for name conflicts
176
+ if (this.#seenNames.has(loadedTool.tool.name)) {
177
+ this.errors.push({
178
+ path: toolPath,
179
+ error: `Tool name "${loadedTool.tool.name}" conflicts with existing tool`,
180
+ source,
181
+ });
182
+ continue;
153
183
  }
184
+
185
+ this.#seenNames.add(loadedTool.tool.name);
186
+ this.tools.push(loadedTool);
154
187
  }
155
188
  }
156
189
  }
@@ -24,6 +24,7 @@ import type { Settings } from "../../config/settings";
24
24
  import type { ExecOptions, ExecResult } from "../../exec/exec";
25
25
  import type { HookUIContext } from "../../extensibility/hooks/types";
26
26
  import type * as PiCodingAgent from "../../index";
27
+ import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
27
28
  import type { Theme } from "../../modes/theme/theme";
28
29
  import type { ReadonlySessionManager } from "../../session/session-manager";
29
30
  import type { TodoItem } from "../../tools/todo";
@@ -96,6 +97,8 @@ export interface CustomToolContext {
96
97
  settings?: Settings;
97
98
  /** Fetch implementation for outbound HTTP; defaults to global fetch when omitted. */
98
99
  fetch?: FetchImpl;
100
+ /** Calling session's `local://` root mapping for tools that bridge out of the OMP process. */
101
+ localProtocolOptions?: LocalProtocolOptions;
99
102
  /** Whether to auto-approve all destructive tool operations (--auto-approve CLI flag) */
100
103
  autoApprove?: boolean;
101
104
  }
@@ -7,6 +7,7 @@ import type { KeyId } from "jeopi-tui";
7
7
  import { logger } from "jeopi-utils";
8
8
  import type { ModelRegistry } from "../../config/model-registry";
9
9
  import type { Settings } from "../../config/settings";
10
+ import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
10
11
  import type { MemoryRuntimeContext } from "../../memory-backend";
11
12
  import { type Theme, theme } from "../../modes/theme/theme";
12
13
  import type { SessionManager } from "../../session/session-manager";
@@ -255,6 +256,7 @@ export class ExtensionRunner {
255
256
  private readonly modelRegistry: ModelRegistry,
256
257
  getMemory?: () => MemoryRuntimeContext | undefined,
257
258
  private readonly settings?: Settings,
259
+ private readonly localProtocolOptions?: LocalProtocolOptions,
258
260
  ) {
259
261
  this.#uiContext = noOpUIContext;
260
262
  this.#getMemoryFn = getMemory;
@@ -537,6 +539,7 @@ export class ExtensionRunner {
537
539
  hasPendingMessages: () => this.#hasPendingMessagesFn(),
538
540
  shutdown: () => this.#shutdownHandler(),
539
541
  getSystemPrompt: () => this.#getSystemPromptFn(),
542
+ localProtocolOptions: this.localProtocolOptions,
540
543
  memory: this.#getMemoryFn?.(),
541
544
  };
542
545
  }
@@ -42,6 +42,7 @@ import type { PythonResult } from "../../eval/py/executor";
42
42
  import type { BashResult } from "../../exec/bash-executor";
43
43
  import type { ExecOptions, ExecResult } from "../../exec/exec";
44
44
  import type * as PiCodingAgent from "../../index";
45
+ import type { LocalProtocolOptions } from "../../internal-urls/local-protocol";
45
46
  import type { MemoryRuntimeContext } from "../../memory-backend";
46
47
  import type { CustomEditor } from "../../modes/components/custom-editor";
47
48
  import type { Theme } from "../../modes/theme/theme";
@@ -346,6 +347,8 @@ export interface ExtensionContext {
346
347
  sessionManager: ReadonlySessionManager;
347
348
  /** Model registry for API key resolution */
348
349
  modelRegistry: ModelRegistry;
350
+ /** Calling session's `local://` root mapping for external tool bridges. */
351
+ localProtocolOptions?: LocalProtocolOptions;
349
352
  /** Current model (may be undefined) */
350
353
  model: Model | undefined;
351
354
  /** Read-only model query facade: list / current / resolve / family. */
@@ -212,6 +212,16 @@ export class PluginManager {
212
212
  }
213
213
  }
214
214
 
215
+ async #removeDependencyEntry(pkgJsonPath: string, name: string): Promise<void> {
216
+ const pkgJson: { dependencies?: Record<string, string>; [key: string]: unknown } =
217
+ await Bun.file(pkgJsonPath).json();
218
+ if (!pkgJson.dependencies || !(name in pkgJson.dependencies)) {
219
+ return;
220
+ }
221
+ delete pkgJson.dependencies[name];
222
+ await Bun.write(pkgJsonPath, JSON.stringify(pkgJson, null, 2));
223
+ }
224
+
215
225
  #collectInstalledNames(deps: Record<string, string>, config: PluginRuntimeConfig): Set<string> {
216
226
  const installedNames = new Set<string>();
217
227
  for (const name of Object.keys(deps)) {
@@ -451,6 +461,17 @@ export class PluginManager {
451
461
  // validation throws.
452
462
  let actualName: string | undefined;
453
463
  try {
464
+ // Bun treats a dependency replacement from `repo#old-ref` to the same
465
+ // package at `repo`/`repo#new-ref` as a self-edge and bails with
466
+ // DependencyLoop. Remove only the stale manifest edge; rollback restores
467
+ // the original package.json and node_modules snapshot on failure.
468
+ if (gitSource && existingActualName) {
469
+ const installedSource = parseGitUrl(depsBefore[existingActualName] ?? "");
470
+ if (installedSource && installedSource.ref !== gitSource.ref) {
471
+ await this.#removeDependencyEntry(pkgJsonPath, existingActualName);
472
+ }
473
+ }
474
+
454
475
  // Step 1: write the spec into plugins/package.json + node_modules.
455
476
  const installProc = Bun.spawn(["bun", "install", packageInstallSpec], {
456
477
  cwd: getPluginsDir(),