march-cli 0.1.36 → 0.1.38

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 (58) hide show
  1. package/package.json +1 -1
  2. package/src/agent/code-search/tool.mjs +1 -1
  3. package/src/agent/runner/runner-utils.mjs +20 -0
  4. package/src/agent/runner.mjs +16 -18
  5. package/src/agent/runtime/remote-ui-client.mjs +0 -1
  6. package/src/agent/runtime/runner-process-client.mjs +7 -0
  7. package/src/agent/runtime/runner-process-factory.mjs +7 -3
  8. package/src/agent/runtime/runner-runtime-host.mjs +2 -2
  9. package/src/agent/runtime/ui-event-bridge.mjs +0 -2
  10. package/src/agent/session/session-options.mjs +2 -2
  11. package/src/agent/tools.mjs +5 -23
  12. package/src/agent/turn/turn-events.mjs +41 -0
  13. package/src/agent/turn/turn-runner.mjs +5 -2
  14. package/src/cli/args.mjs +0 -3
  15. package/src/cli/commands/registry/slash-command-registry.mjs +2 -0
  16. package/src/cli/fallback-ui.mjs +0 -2
  17. package/src/cli/input/history-store.mjs +65 -3
  18. package/src/cli/input/mode-state.mjs +1 -1
  19. package/src/cli/repl-loop.mjs +75 -25
  20. package/src/cli/startup/app-runtime.mjs +72 -31
  21. package/src/cli/startup/create-runtime-runner.mjs +5 -46
  22. package/src/cli/startup/startup-session.mjs +3 -13
  23. package/src/cli/tui/input/history-navigation-controller.mjs +56 -0
  24. package/src/cli/turn/turn-input-preparer.mjs +0 -1
  25. package/src/cli/ui.mjs +9 -6
  26. package/src/cli/workspace/command.mjs +147 -0
  27. package/src/cli/workspace/output-router.mjs +108 -0
  28. package/src/cli/workspace/project-runtime.mjs +92 -0
  29. package/src/config/features.mjs +0 -1
  30. package/src/context/engine.mjs +4 -2
  31. package/src/context/system-core/base.md +4 -1
  32. package/src/extensions/lifecycle-adapter.mjs +1 -1
  33. package/src/history/runner.mjs +11 -0
  34. package/src/history/store.mjs +129 -0
  35. package/src/history/tool.mjs +39 -0
  36. package/src/lsp/client.mjs +12 -5
  37. package/src/lsp/service.mjs +15 -3
  38. package/src/main.mjs +5 -2
  39. package/src/notification/desktop-notifier.mjs +16 -8
  40. package/src/web-ui/command.mjs +2 -2
  41. package/src/web-ui/dist/assets/index-BQtl1uQs.css +1 -0
  42. package/src/web-ui/dist/assets/index-DrlJis_D.js +1845 -0
  43. package/src/web-ui/dist/index.html +13 -0
  44. package/src/web-ui/runtime-host.mjs +5 -25
  45. package/src/web-ui/session-manager.mjs +2 -2
  46. package/src/web-ui/src/components/timeline/TimelineBlocks.tsx +2 -10
  47. package/src/web-ui/src/mockData.ts +1 -8
  48. package/src/web-ui/src/model.ts +0 -2
  49. package/src/web-ui/src/runtime/client.ts +0 -1
  50. package/src/web-ui/src/runtime/runtimeTimeline.ts +1 -3
  51. package/src/web-ui/src/styles/shell.css +1 -2
  52. package/src/web-ui/src/timelineAdapter.ts +1 -2
  53. package/src/workspace/project-id.mjs +14 -0
  54. package/src/workspace/project-registry.mjs +74 -0
  55. package/src/workspace/session-index.mjs +75 -0
  56. package/src/workspace/supervisor.mjs +172 -0
  57. package/src/cli/permissions.mjs +0 -103
  58. package/src/cli/tui/permission-request-ui.mjs +0 -18
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "march-cli",
3
- "version": "0.1.36",
3
+ "version": "0.1.38",
4
4
  "description": "March CLI — terminal-native coding agent with context reconstruction",
5
5
  "type": "module",
6
6
  "main": "./src/main.mjs",
@@ -13,7 +13,7 @@ export function createCodeSearchTool({ engine, stateRoot = null }) {
13
13
  return defineTool({
14
14
  name: "code_search",
15
15
  label: "Code Search",
16
- description: "Native code-aware search over the workspace. Use it to locate relevant code snippets before reading full files; use grep for exact string confirmation.",
16
+ description: "Native semantic/symbol code search over the workspace. Use it first for unknown entry points, cross-module flows, responsibility boundaries, and related implementations. Use grep/read afterward for exact confirmation before editing or making claims.",
17
17
  parameters: Type.Object({
18
18
  query: Type.Optional(Type.String({ description: "Natural-language or symbol query" })),
19
19
  path: Type.Optional(Type.String({ description: "Relative or absolute workspace path to search; default current workspace" })),
@@ -1,3 +1,13 @@
1
+ import { installCodexTransportCompression } from "./codex-transport-compression.mjs";
2
+ import { installCodexWebSocketEventDebug } from "./codex-websocket-event-debug.mjs";
3
+ import { installCodexLargeContextGuard } from "./codex-large-context-guard.mjs";
4
+
5
+ export function installRunnerProcessGuards() {
6
+ installCodexLargeContextGuard();
7
+ installCodexTransportCompression();
8
+ installCodexWebSocketEventDebug();
9
+ }
10
+
1
11
  export function providerContextToPayload(providerContext) {
2
12
  return {
3
13
  messages: [
@@ -22,3 +32,13 @@ export function notifyTurnEndDetached(turnNotifier, event, onResult = () => {})
22
32
  pending.then(onResult, () => {});
23
33
  return pending;
24
34
  }
35
+
36
+
37
+ export function buildNotificationActivation({ notificationContext, sessionStats }) {
38
+ if (!notificationContext?.projectId) return null;
39
+ return {
40
+ type: "workspace-session",
41
+ projectId: notificationContext.projectId,
42
+ sessionId: sessionStats?.sessionId ?? null,
43
+ };
44
+ }
@@ -11,11 +11,9 @@ import { runRunnerCleanup } from "./runner/runner-cleanup.mjs";
11
11
  import { createRunnerRuntimeHost } from "./runtime/runner-runtime-host.mjs";
12
12
  import { createRuntimeUiBridge } from "./runtime/ui-event-bridge.mjs";
13
13
  import { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
14
- import { notifyTurnEndBestEffort, notifyTurnEndDetached, providerContextToPayload } from "./runner/runner-utils.mjs";
14
+ import { buildNotificationActivation, installRunnerProcessGuards, notifyTurnEndBestEffort, notifyTurnEndDetached, providerContextToPayload } from "./runner/runner-utils.mjs";
15
15
  import { dumpCodexTransportDebug, getCodexTransportDebugSnapshot } from "./runner/codex-transport-debug.mjs";
16
- import { installCodexWebSocketEventDebug } from "./runner/codex-websocket-event-debug.mjs";
17
- import { installCodexTransportCompression } from "./runner/codex-transport-compression.mjs";
18
- import { applyCodexLargeContextGuardToPayload, installCodexLargeContextGuard } from "./runner/codex-large-context-guard.mjs";
16
+ import { applyCodexLargeContextGuardToPayload } from "./runner/codex-large-context-guard.mjs";
19
17
  import { resolveRunnerSessionOptions } from "./session/session-options.mjs";
20
18
  import { createSessionBinding } from "./session/session-binding.mjs";
21
19
  import { maybeAutoNameSession } from "./session/session-auto-name.mjs";
@@ -28,19 +26,14 @@ import { registerCustomProviders } from "../provider/custom-provider.mjs";
28
26
  import { injectHostedTools } from "../provider/hosted-tools.mjs";
29
27
  import { createRunnerLifecycle } from "./lifecycle/runner-lifecycle.mjs";
30
28
  import { createRunnerProviderQuotaRuntime } from "./runner/provider-quota-runtime.mjs";
29
+ import { appendRunnerTurnHistory, createRunnerHistoryStore } from "../history/runner.mjs";
31
30
  export { MARCH_BASE_TOOL_NAMES, installModelPayloadDumper };
32
31
  export { createDefaultSessionManager, resolveRunnerSessionManager } from "./runner/runner-init.mjs";
33
32
  export { getRunnerSessionStats, syncEngineSessionState } from "./runner/runner-session-state.mjs";
34
- export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], remoteMemorySources = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, permissionController = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {} }) {
35
- installCodexLargeContextGuard();
36
- installCodexTransportCompression();
37
- installCodexWebSocketEventDebug();
38
- if (!useRuntimeHost && extensionPaths.length > 0) {
39
- throw new Error("--extension requires the default pi runtime host path");
40
- }
41
- const authConfig = authStorage
42
- ? { authStorage, hasAuth: true }
43
- : createMarchAuthStorage({ provider: provider ?? "deepseek", providers, cwd });
33
+ export async function createRunner({ cwd, modelId = null, provider = null, providers = {}, stateRoot, ui, memoryRoot = null, profilePaths = null, memoryStore = null, memoryTools = [], remoteMemorySources = [], shellRuntime = null, mcpTools = [], mcpInjections = [], mcpClientManager = null, webTools = [], namespace = "", sessionManager = null, useRuntimeHost = false, projectMarchDir = null, syncPiSidecar = false, extensionPaths = [], lifecycleHooks = [], lifecycleDiagnostics = [], authStorage = null, modelContextDumper = null, turnNotifier = null, logger = null, onModelPayload = null, onLspStatusChange = null, createAgentSessionImpl = createAgentSession, createAgentSessionRuntimeImpl, createRuntimeServices, createRuntimeSessionFromServices, maxTurns, trimBatch, serviceTier = null, hostedTools = {}, notificationContext = null }) {
34
+ installRunnerProcessGuards();
35
+ if (!useRuntimeHost && extensionPaths.length > 0) throw new Error("--extension requires the default pi runtime host path");
36
+ const authConfig = authStorage ? { authStorage, hasAuth: true } : createMarchAuthStorage({ provider: provider ?? "deepseek", providers, cwd });
44
37
  if (!authConfig.hasAuth) throw new Error("No providers configured. Run: march provider --config");
45
38
  const resolvedAuth = authConfig.authStorage;
46
39
  const modelRegistry = ModelRegistry.create(resolvedAuth);
@@ -55,8 +48,9 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
55
48
  retry: { enabled: true, maxRetries: 3, baseDelayMs: 2000 },
56
49
  });
57
50
  const { ui: runtimeUi, eventBus: runtimeUiEvents, detach: detachRuntimeUi } = createRuntimeUiBridge(ui);
58
- const lspService = new LspService({ cwd, onEvent: (event) => runtimeUi.status?.(formatLspServiceEvent(event)) });
51
+ const lspService = new LspService({ cwd, onEvent: (event) => runtimeUi.status?.(formatLspServiceEvent(event)), onStatusChange: (event) => onLspStatusChange?.(event) });
59
52
  const engine = new ContextEngine({ cwd, modelId, provider, namespace, memoryRoot, profilePaths, remoteMemorySources, shellRuntime, lspService, injections: mcpInjections, maxTurns, trimBatch });
53
+ const historyStore = createRunnerHistoryStore({ stateRoot, cwd });
60
54
  const resolvedSessionManager = resolveRunnerSessionManager(cwd, sessionManager);
61
55
  const sessionBinding = createSessionBinding(null);
62
56
  let currentModelCallKind = "model", currentTurnId = null, currentPromptForContext = "";
@@ -72,8 +66,8 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
72
66
  providers,
73
67
  sessionManager: resolvedSessionManager, sessionBinding, engine, ui: runtimeUi,
74
68
  projectMarchDir,
75
- memoryTools, memoryStore, shellRuntime, lspService, mcpTools, webTools,
76
- lifecycle, permissionController, extensionPaths, hostedTools,
69
+ memoryTools, memoryStore, historyStore, shellRuntime, lspService, mcpTools, webTools,
70
+ lifecycle, extensionPaths, hostedTools,
77
71
  onRebind: (session) => {
78
72
  installModelPayloadDumper(session, modelContextDumper, () => currentModelCallKind, onLoggedModelPayload, injectMarchSystemContext);
79
73
  syncEngineSessionState(engine, session);
@@ -85,7 +79,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
85
79
  } else {
86
80
  const sessionOptions = resolveRunnerSessionOptions({
87
81
  cwd, stateRoot, provider, modelId, modelRegistry, engine, ui: runtimeUi,
88
- memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController,
82
+ memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle,
89
83
  authStorage: resolvedAuth, projectMarchDir,
90
84
  getCurrentModel: () => sessionBinding.get()?.model ?? selectedModel,
91
85
  });
@@ -134,12 +128,14 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
134
128
  syncCurrentPiSidecar,
135
129
  autoNameSession,
136
130
  contextMode,
131
+ recordHistory: (turn) => appendRunnerTurnHistory({ store: historyStore, turn, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost), modelId: engine.modelId, provider: engine.provider }),
137
132
  });
138
133
  notifyTurnEndDetached(turnNotifier, {
139
134
  status: "success",
140
135
  sessionName: engine.sessionName,
141
136
  draft: result?.draft ?? "",
142
137
  durationMs: Date.now() - turnStartedAt,
138
+ activation: buildNotificationActivation({ notificationContext, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost) }),
143
139
  }, (notificationResult) => { lastNotificationResult = notificationResult; });
144
140
  const lifecycleAction = lifecycle.takePendingAction();
145
141
  if (lifecycleAction) result.lifecycleAction = lifecycleAction;
@@ -151,6 +147,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
151
147
  sessionName: engine.sessionName,
152
148
  errorMessage: err?.message ?? String(err),
153
149
  durationMs: Date.now() - turnStartedAt,
150
+ activation: buildNotificationActivation({ notificationContext, sessionStats: getRunnerSessionStats(sessionBinding.get(), runtimeHost) }),
154
151
  }, (notificationResult) => { lastNotificationResult = notificationResult; });
155
152
  turnLog.endError(err);
156
153
  throw err;
@@ -258,6 +255,7 @@ export async function createRunner({ cwd, modelId = null, provider = null, provi
258
255
  ]);
259
256
  },
260
257
  };
258
+ return runner;
261
259
  function syncCurrentPiSidecar() {
262
260
  return syncPiSessionSidecar({
263
261
  enabled: syncPiSidecar, projectMarchDir, engine,
@@ -15,6 +15,5 @@ export function createRemoteRuntimeUiClient(peer) {
15
15
  debugLines: (lines) => peer.notify("uiEvent", { type: "debug_lines", lines }),
16
16
  recall: ({ source, hints }) => peer.notify("uiEvent", { type: "recall", source, hints }),
17
17
  editDiff: (path, diffLines) => peer.notify("uiEvent", { type: "edit_diff", path, diffLines }),
18
- requestPermission: (request) => peer.call("uiRequest", { type: "permission_request", ...request }),
19
18
  };
20
19
  }
@@ -10,6 +10,8 @@ export async function createRunnerProcessClient({
10
10
  runnerOptions,
11
11
  ui,
12
12
  onModelPayload = null,
13
+ onLspStatusChange = null,
14
+ onNotificationActivation = null,
13
15
  entry = fileURLToPath(DEFAULT_ENTRY),
14
16
  forkImpl = fork,
15
17
  timeoutMs = 0,
@@ -46,6 +48,11 @@ export async function createRunnerProcessClient({
46
48
  target: {
47
49
  ...createRuntimeUiEventTarget(ui),
48
50
  modelPayload: (event) => onModelPayload?.(event),
51
+ lspStatusChange: async (event) => {
52
+ await active?.runner?.refreshState?.();
53
+ onLspStatusChange?.(event);
54
+ },
55
+ notificationActivation: (activation) => onNotificationActivation?.(activation),
49
56
  },
50
57
  timeoutMs,
51
58
  });
@@ -8,7 +8,6 @@ import { createMarkdownMemoryTools } from "../../memory/markdown-tools.mjs";
8
8
  import { normalizeRemoteMemorySources } from "../../memory/remote/config.mjs";
9
9
  import { initializeMcp } from "../../mcp/index.mjs";
10
10
  import { createWebToolsFromConfig } from "../../web/tools.mjs";
11
- import { createPermissionController } from "../../cli/permissions.mjs";
12
11
  import { resolvePiSessionManager } from "../../session/pi-manager.mjs";
13
12
  import { createModelContextDumper } from "../../debug/model-context-dumper.mjs";
14
13
  import { createLogger, installProcessLogHandlers } from "../../debug/logger.mjs";
@@ -23,7 +22,6 @@ const DEFAULT_DEPS = {
23
22
  createMarkdownMemoryTools,
24
23
  initializeMcp,
25
24
  createWebToolsFromConfig,
26
- createPermissionController,
27
25
  resolvePiSessionManager,
28
26
  createModelContextDumper,
29
27
  createMarchAuthStorage,
@@ -85,14 +83,16 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
85
83
  maxTurns: options.config?.maxTurns ?? undefined,
86
84
  trimBatch: options.config?.trimBatch ?? undefined,
87
85
  hostedTools: options.config?.hostedTools,
88
- permissionController: d.createPermissionController({ mode: options.permissionMode }),
86
+ notificationContext: options.notificationContext,
89
87
  modelContextDumper: d.createModelContextDumper(options.modelContextDumper ?? { enabled: false }),
90
88
  turnNotifier: d.createDesktopTurnNotifier({
91
89
  enabled: Boolean(options.config?.notifications?.turnEnd),
92
90
  config: options.config?.notifications,
91
+ onActivation: (activation) => d.peer.notify("notificationActivation", activation),
93
92
  }),
94
93
  logger,
95
94
  onModelPayload: (event) => d.peer.notify("modelPayload", pickModelPayloadEvent(event)),
95
+ onLspStatusChange: (event) => d.peer.notify("lspStatusChange", pickLspStatusEvent(event)),
96
96
  });
97
97
 
98
98
  const originalDispose = runner.dispose;
@@ -109,3 +109,7 @@ export async function createIsolatedRunner(options = {}, deps = {}) {
109
109
  function pickModelPayloadEvent({ estimatedTokens, provider, model, kind, turnId } = {}) {
110
110
  return { estimatedTokens, provider, model, kind, turnId };
111
111
  }
112
+
113
+ function pickLspStatusEvent({ id, root, status, reason, managed } = {}) {
114
+ return { id, root, status, reason, managed };
115
+ }
@@ -20,12 +20,12 @@ export async function createRunnerRuntimeHost({
20
20
  ui,
21
21
  projectMarchDir = null,
22
22
  memoryTools = [],
23
+ historyStore = null,
23
24
  shellRuntime = null,
24
25
  lspService = null,
25
26
  mcpTools = [],
26
27
  webTools = [],
27
28
  lifecycle = null,
28
- permissionController = null,
29
29
  extensionPaths = [],
30
30
  hostedTools = {},
31
31
  onRebind = null,
@@ -56,12 +56,12 @@ export async function createRunnerRuntimeHost({
56
56
  engine,
57
57
  ui,
58
58
  memoryTools,
59
+ historyStore,
59
60
  shellRuntime,
60
61
  lspService,
61
62
  mcpTools,
62
63
  webTools,
63
64
  lifecycle,
64
- permissionController,
65
65
  authStorage,
66
66
  projectMarchDir,
67
67
  hostedTools,
@@ -53,7 +53,6 @@ export function createRuntimeUiClient(eventBus) {
53
53
  recall: ({ source, hints }) => eventBus.emit({ type: "recall", source, hints }),
54
54
  providerQuotaSnapshot: (snapshot) => eventBus.emit({ type: "provider_quota_snapshot", snapshot }),
55
55
  editDiff: (path, diffLines) => eventBus.emit({ type: "edit_diff", path, diffLines }),
56
- requestPermission: (request) => eventBus.request({ type: "permission_request", ...request }),
57
56
  };
58
57
  }
59
58
 
@@ -75,7 +74,6 @@ export function dispatchRuntimeUiEvent(ui, event) {
75
74
  case "recall": return ui.recall?.({ source: event.source, hints: event.hints });
76
75
  case "provider_quota_snapshot": return ui.providerQuotaSnapshot?.(event.snapshot);
77
76
  case "edit_diff": return ui.editDiff?.(event.path, event.diffLines);
78
- case "permission_request": return ui.requestPermission?.({ toolName: event.toolName, params: event.params, category: event.category });
79
77
  default: return undefined;
80
78
  }
81
79
  }
@@ -10,12 +10,12 @@ export function resolveRunnerSessionOptions({
10
10
  engine,
11
11
  ui,
12
12
  memoryTools = [],
13
+ historyStore = null,
13
14
  shellRuntime = null,
14
15
  lspService = null,
15
16
  mcpTools = [],
16
17
  webTools = [],
17
18
  lifecycle = null,
18
- permissionController = null,
19
19
  authStorage = null,
20
20
  projectMarchDir = null,
21
21
  stateRoot = null,
@@ -31,7 +31,7 @@ export function resolveRunnerSessionOptions({
31
31
  ?? (provider && modelId ? getModel(provider, modelId) : null);
32
32
  if (!model) throw new Error(`Model not found: ${provider}/${modelId}`);
33
33
 
34
- const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, shellRuntime, lspService, mcpTools, webTools, lifecycle, permissionController, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
34
+ const customTools = createMarchCustomTools({ cwd, engine, ui, memoryTools, historyStore, shellRuntime, lspService, mcpTools, webTools, lifecycle, authStorage, projectMarchDir, stateRoot, getCurrentModel: () => getCurrentModel?.() ?? model });
35
35
  const customToolNames = customTools.map((tool) => tool.name);
36
36
  const tools = [
37
37
  ...customToolNames.filter((name) => name === "read"),
@@ -7,17 +7,18 @@ import { createReadImageTool } from "./file-tools/read-image-tool.mjs";
7
7
  import { createSendBinaryTool } from "./output/send-binary-tool.mjs";
8
8
  import { createScreenTool } from "./screen-tools/screen-tool.mjs";
9
9
  import { createListWindowsTool } from "./screen-tools/list-windows-tool.mjs";
10
- import { toolText } from "./tool-result.mjs";
11
10
  import { createShellTools } from "../shell/tools.mjs";
12
11
  import { initImageGen } from "../image-gen/index.mjs";
13
12
  import { createSuperGrokTool } from "../supergrok/tool.mjs";
14
13
  import { createBrowserTools } from "../browser/tools/index.mjs";
15
14
  import { createRuntimeRestartTool } from "./lifecycle/runtime-restart-tool.mjs";
15
+ import { createHistorySearchTool } from "../history/tool.mjs";
16
16
 
17
- export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, permissionController = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
17
+ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], historyStore = null, shellRuntime = null, lspService = null, mcpTools = [], webTools = [], lifecycle = null, authStorage = null, projectMarchDir = null, stateRoot = null, getCurrentModel = null }) {
18
18
  const commandExecTool = createCommandExecTool({ cwd });
19
19
  const codeSearchTool = createCodeSearchTool({ engine, stateRoot });
20
20
  const contextStatsTool = createContextStatsTool({ engine });
21
+ const historySearchTool = createHistorySearchTool({ store: historyStore });
21
22
  const editFileTool = createEditFileTool({ engine, ui, lspService });
22
23
  const readFileTool = createReadFileTool({ engine });
23
24
  const readImageTool = createReadImageTool({ engine, getCurrentModel });
@@ -36,6 +37,7 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
36
37
  commandExecTool,
37
38
  editFileTool,
38
39
  ...createShellTools(shellRuntime),
40
+ ...(historySearchTool ? [historySearchTool] : []),
39
41
  ...memoryTools,
40
42
  ...mcpTools,
41
43
  ...webTools,
@@ -44,25 +46,5 @@ export function createMarchCustomTools({ cwd, engine, ui, memoryTools = [], shel
44
46
  ...(authStorage ? [createSuperGrokTool({ authStorage, projectMarchDir })] : []),
45
47
  ...(authStorage ? initImageGen({ authStorage, projectMarchDir }) : []),
46
48
  ];
47
-
48
- if (!permissionController) return tools;
49
-
50
- return tools.map((tool) => {
51
- const execute = tool.execute;
52
- if (!execute) return tool;
53
- const wrapped = async (toolCallId, params, signal, onUpdate) => {
54
- const decision = await permissionController.requestApproval(
55
- tool.name,
56
- params,
57
- ui.requestPermission
58
- ? (ctx) => ui.requestPermission(ctx)
59
- : null,
60
- );
61
- if (decision.behavior === "deny") {
62
- return toolText(`Permission denied: ${decision.message}`, { error: true, permissionDenied: true });
63
- }
64
- return execute(toolCallId, params, signal, onUpdate);
65
- };
66
- return { ...tool, execute: wrapped };
67
- });
49
+ return tools;
68
50
  }
@@ -1,5 +1,7 @@
1
1
  import { formatToolStartLine, formatToolSuccessSummary } from "../tool-summary.mjs";
2
2
 
3
+ const TOOL_ERROR_EXCERPT_LIMIT = 4000;
4
+
3
5
  export function createTurnEventState() {
4
6
  return {
5
7
  draft: "",
@@ -9,6 +11,7 @@ export function createTurnEventState() {
9
11
  assistantReplyOpen: false,
10
12
  assistantContextParts: [],
11
13
  activeToolContextPart: null,
14
+ toolCalls: [],
12
15
  };
13
16
  }
14
17
 
@@ -19,10 +22,12 @@ export function handleRunnerSessionEvent(event, { ui, engine, state }) {
19
22
  if (event.type === "tool_execution_start") {
20
23
  closeAssistantReply({ ui, state });
21
24
  appendToolStartContext(state, event.toolName, event.args);
25
+ recordToolStart(state, event.toolName, event.args);
22
26
  ui.toolStart(event.toolName, event.args);
23
27
  }
24
28
  if (event.type === "tool_execution_end") {
25
29
  updateToolEndContext(state, event.toolName, event.isError, event.result);
30
+ recordToolEnd(state, event.toolName, event.isError, event.result);
26
31
  ui.toolEnd(event.toolName, event.isError, event.result);
27
32
  }
28
33
  if (event.type === "auto_retry_start") {
@@ -109,3 +114,39 @@ function updateToolEndContext(state, name, isError, result) {
109
114
  if (summary && summary !== "done") part.text = `${part.text.trimEnd()} (${summary})\n`;
110
115
  state.activeToolContextPart = null;
111
116
  }
117
+
118
+ function recordToolStart(state, name, args) {
119
+ state.toolCalls.push({ name, args: cloneJson(args), status: "running" });
120
+ }
121
+
122
+ function recordToolEnd(state, name, isError, result) {
123
+ const call = [...state.toolCalls].reverse().find((item) => item.name === name && item.status === "running");
124
+ if (!call) return;
125
+ call.status = isError ? "failed" : "success";
126
+ if (!isError) return;
127
+ const output = extractToolOutput(result);
128
+ call.error = {
129
+ message: output.split(/\r?\n/).find(Boolean) ?? "Tool call failed",
130
+ details: cloneJson(result?.details ?? null),
131
+ excerpt: truncate(output, TOOL_ERROR_EXCERPT_LIMIT),
132
+ };
133
+ }
134
+
135
+ function extractToolOutput(result) {
136
+ const content = result?.content;
137
+ if (!Array.isArray(content)) return typeof result === "string" ? result : "";
138
+ return content.filter((item) => item?.type === "text").map((item) => item.text ?? "").join("\n");
139
+ }
140
+
141
+ function cloneJson(value) {
142
+ try {
143
+ return JSON.parse(JSON.stringify(value ?? null));
144
+ } catch {
145
+ return null;
146
+ }
147
+ }
148
+
149
+ function truncate(text, limit) {
150
+ const value = String(text ?? "");
151
+ return value.length > limit ? `${value.slice(0, limit)}\n[truncated]` : value;
152
+ }
@@ -17,6 +17,7 @@ export async function runRunnerTurn({
17
17
  syncCurrentPiSidecar,
18
18
  autoNameSession,
19
19
  contextMode = "rebuild",
20
+ recordHistory = null,
20
21
  }) {
21
22
  const {
22
23
  userRecallHints = [],
@@ -80,6 +81,7 @@ export async function runRunnerTurn({
80
81
  midTurnRecallHints,
81
82
  syncCurrentPiSidecar,
82
83
  autoNameSession,
84
+ recordHistory,
83
85
  });
84
86
  return { draft: turnState.draft };
85
87
  } finally {
@@ -127,19 +129,20 @@ function logSessionEvent(logger, event) {
127
129
  });
128
130
  }
129
131
 
130
- function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession }) {
132
+ function finalizeTurn({ prompt, userMessage, userRecallHints, currentProject, memoryStore, engine, ui, turnState, midTurnRecallHints, syncCurrentPiSidecar, autoNameSession, recordHistory }) {
131
133
  closeAssistantReply({ ui, state: turnState });
132
134
  const assistantRecallHints = flushAssistantRecall({ memoryStore, engine, turnState, currentProject });
133
135
  engine.setPendingAssistantRecallHints?.(assistantRecallHints);
134
136
  const recordedAssistantRecallHints = uniqueHints([...midTurnRecallHints, ...assistantRecallHints]);
135
137
 
136
- engine.recordTurn({
138
+ const turn = engine.recordTurn({
137
139
  userMessage: userMessage ?? prompt.slice(0, 300),
138
140
  assistantMessage: turnState.draft,
139
141
  assistantContext: compactAssistantContext(turnState),
140
142
  userRecallHints,
141
143
  assistantRecallHints: recordedAssistantRecallHints,
142
144
  });
145
+ recordHistory?.({ ...turn, thinking: assistantThinkingText(turnState), toolCalls: turnState.toolCalls });
143
146
 
144
147
  autoNameSession?.();
145
148
  syncCurrentPiSidecar();
package/src/cli/args.mjs CHANGED
@@ -17,7 +17,6 @@ export function parseCliArgs(argv) {
17
17
  "pi-runtime-host": { type: "boolean" },
18
18
  "shell-runtime": { type: "boolean" },
19
19
  "no-shell-runtime": { type: "boolean" },
20
- "permission-mode": { type: "string" },
21
20
  host: { type: "string" },
22
21
  port: { type: "string" },
23
22
  "api-port": { type: "string" },
@@ -47,7 +46,6 @@ export function parseCliArgs(argv) {
47
46
  piSessions: values["pi-sessions"] ?? false,
48
47
  piRuntimeHost: values["pi-runtime-host"] ?? false,
49
48
  shellRuntime: values["no-shell-runtime"] ? false : true,
50
- permissionMode: values["permission-mode"] ?? "bypassPermissions",
51
49
  host: values.host ?? null,
52
50
  port: values.port ?? null,
53
51
  apiPort: values["api-port"] ?? null,
@@ -97,7 +95,6 @@ Options:
97
95
  --pi-runtime-host Force pi AgentSessionRuntime host path
98
96
  --shell-runtime Enable interactive PTY shell tools (default)
99
97
  --no-shell-runtime Disable interactive PTY shell tools and shell pane
100
- --permission-mode <mode> Permission mode: default, bypassPermissions, dontAsk (default: bypassPermissions)
101
98
  -e, --extension <path>
102
99
  Load a pi extension path in the default runtime host (repeatable)
103
100
  --host <host> With memory serve/web, bind host (default: 127.0.0.1)
@@ -12,6 +12,7 @@ import { handleSessionNameCommand, parseSessionNameCommand } from "../../session
12
12
  import { handleShellCommand, parseShellCommand } from "../../shell/shell-command.mjs";
13
13
  import { handleProviderCommand, parseProviderCommand } from "../provider-command.mjs";
14
14
  import { handleModeCommand, parseModeCommand } from "../mode-command.mjs";
15
+ import { WORKSPACE_SLASH_COMMANDS } from "../../workspace/command.mjs";
15
16
 
16
17
  export const SLASH_COMMANDS = [
17
18
  exactCommand({
@@ -94,6 +95,7 @@ export const SLASH_COMMANDS = [
94
95
  parse: parseThinkingCommand,
95
96
  run: async (ctx, command) => writeLines(ctx.ui, await handleThinkingCommand(command, { runner: ctx.runner, ui: ctx.ui })),
96
97
  }),
98
+ ...WORKSPACE_SLASH_COMMANDS,
97
99
  exactCommand({
98
100
  name: "status",
99
101
  description: "Show runtime status",
@@ -48,7 +48,6 @@ export function createJsonUI() {
48
48
  editDiff: (path, diffLines) => {
49
49
  stdout.write(JSON.stringify({ type: "edit_diff", path, diff: diffLines }) + "\n");
50
50
  },
51
- requestPermission: async () => true,
52
51
  setEscapeHandler: () => {},
53
52
  setCtrlCHandler: () => {},
54
53
  setShiftTabHandler: () => {},
@@ -137,7 +136,6 @@ export function createPlainUI() {
137
136
  else stdout.write(`${dim(` ${d.text}`)}\n`);
138
137
  }
139
138
  },
140
- requestPermission: async () => true,
141
139
  setEscapeHandler: () => {},
142
140
  setCtrlCHandler: () => {},
143
141
  setShiftTabHandler: () => {},
@@ -1,8 +1,10 @@
1
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
1
+ import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
 
4
4
  const HISTORY_VERSION = 1;
5
5
  const MAX_HISTORY_ITEMS = 100;
6
+ const LOCK_WAIT_MS = 2000;
7
+ const LOCK_STALE_MS = 5000;
6
8
 
7
9
  export function createInputHistoryStore({ path, maxItems = MAX_HISTORY_ITEMS } = {}) {
8
10
  return {
@@ -20,8 +22,10 @@ export function createInputHistoryStore({ path, maxItems = MAX_HISTORY_ITEMS } =
20
22
  save(items) {
21
23
  if (!path) return;
22
24
  const normalized = normalizeItems(items, maxItems);
23
- mkdirSync(dirname(path), { recursive: true });
24
- writeFileSync(path, `${JSON.stringify({ version: HISTORY_VERSION, items: normalized }, null, 2)}\n`, "utf8");
25
+ withHistoryLock(path, () => {
26
+ const merged = mergeItems(normalized, this.load(), maxItems);
27
+ writeHistoryFile(path, merged);
28
+ });
25
29
  },
26
30
  };
27
31
  }
@@ -33,3 +37,61 @@ function normalizeItems(items, maxItems) {
33
37
  .filter(Boolean)
34
38
  .slice(0, maxItems);
35
39
  }
40
+
41
+ function mergeItems(primaryItems, existingItems, maxItems) {
42
+ const merged = [];
43
+ const seen = new Set();
44
+ for (const item of [...primaryItems, ...existingItems]) {
45
+ if (seen.has(item)) continue;
46
+ seen.add(item);
47
+ merged.push(item);
48
+ if (merged.length >= maxItems) break;
49
+ }
50
+ return merged;
51
+ }
52
+
53
+ function writeHistoryFile(path, items) {
54
+ mkdirSync(dirname(path), { recursive: true });
55
+ const tmpPath = `${path}.${process.pid}.${Date.now()}.tmp`;
56
+ const payload = `${JSON.stringify({ version: HISTORY_VERSION, items }, null, 2)}\n`;
57
+ writeFileSync(tmpPath, payload, "utf8");
58
+ renameSync(tmpPath, path);
59
+ }
60
+
61
+ function withHistoryLock(path, fn) {
62
+ mkdirSync(dirname(path), { recursive: true });
63
+ const lockPath = `${path}.lock`;
64
+ const fd = acquireLock(lockPath);
65
+ try {
66
+ return fn();
67
+ } finally {
68
+ closeSync(fd);
69
+ try { unlinkSync(lockPath); } catch {}
70
+ }
71
+ }
72
+
73
+ function acquireLock(lockPath) {
74
+ const start = Date.now();
75
+ while (true) {
76
+ try {
77
+ const fd = openSync(lockPath, "wx");
78
+ writeFileSync(fd, `${process.pid}\n`, "utf8");
79
+ return fd;
80
+ } catch (err) {
81
+ if (err?.code !== "EEXIST") throw err;
82
+ removeStaleLock(lockPath);
83
+ if (Date.now() - start >= LOCK_WAIT_MS) throw err;
84
+ sleepSync(25);
85
+ }
86
+ }
87
+ }
88
+
89
+ function removeStaleLock(lockPath) {
90
+ try {
91
+ if (Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS) unlinkSync(lockPath);
92
+ } catch {}
93
+ }
94
+
95
+ function sleepSync(ms) {
96
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
97
+ }
@@ -34,7 +34,7 @@ export function formatModeReminder(mode = MODES.DO) {
34
34
  "</mode>";
35
35
  }
36
36
  return "<mode>\n" +
37
- "You are in do mode. You may implement changes when the user asks for execution, following normal permissions and project rules.\n" +
37
+ "You are in do mode. You may implement changes when the user asks for execution, following normal project rules.\n" +
38
38
  "</mode>";
39
39
  }
40
40