march-cli 0.1.11 → 0.1.13

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 (37) hide show
  1. package/package.json +1 -1
  2. package/src/agent/command-exec-tool.mjs +42 -8
  3. package/src/agent/runner/runner-utils.mjs +6 -0
  4. package/src/agent/runner.mjs +16 -16
  5. package/src/agent/runtime/ipc/ipc-peer.mjs +99 -0
  6. package/src/agent/runtime/ipc/process-ipc-transport.mjs +16 -0
  7. package/src/agent/runtime/remote-runner-client.mjs +73 -0
  8. package/src/agent/runtime/remote-ui-client.mjs +19 -0
  9. package/src/agent/runtime/runner-ipc-target.mjs +126 -0
  10. package/src/agent/runtime/runner-process-client.mjs +47 -0
  11. package/src/agent/runtime/runner-process-entry.mjs +93 -0
  12. package/src/agent/runtime/ui-event-bridge.mjs +85 -0
  13. package/src/agent/tool-names.mjs +1 -1
  14. package/src/agent/tool-summary.mjs +112 -0
  15. package/src/agent/tools.mjs +0 -3
  16. package/src/agent/turn/turn-events.mjs +46 -0
  17. package/src/agent/turn/turn-runner.mjs +2 -1
  18. package/src/cli/commands/copy-command.mjs +16 -2
  19. package/src/cli/commands/status-command.mjs +7 -4
  20. package/src/cli/commands/thinking-command.mjs +10 -3
  21. package/src/cli/repl-loop.mjs +3 -1
  22. package/src/cli/startup/create-runtime-runner.mjs +61 -0
  23. package/src/cli/startup/startup-banner.mjs +64 -10
  24. package/src/cli/tui/layout/main-pane-layout.mjs +16 -7
  25. package/src/cli/tui/selection-screen.mjs +83 -34
  26. package/src/cli/tui/status/status-bar.mjs +154 -18
  27. package/src/cli/tui/syntax/highlighting.mjs +7 -24
  28. package/src/cli/tui/syntax/languages.mjs +1 -1
  29. package/src/cli/tui/tool-rendering.mjs +3 -113
  30. package/src/cli/tui/tui-handlers.mjs +1 -1
  31. package/src/cli/tui/ui-theme.mjs +14 -5
  32. package/src/cli/ui.mjs +1 -1
  33. package/src/context/engine.mjs +10 -9
  34. package/src/context/profiles.mjs +39 -0
  35. package/src/main.mjs +35 -29
  36. package/src/agent/find-tool.mjs +0 -112
  37. package/src/context/center-memory.mjs +0 -14
@@ -17,6 +17,7 @@ const brightRed = (s) => `\x1b[91m${s}${R}`;
17
17
  const brightGreen = (s) => `\x1b[92m${s}${R}`;
18
18
  const orange = (s) => `\x1b[38;2;245;167;66m${s}${R}`;
19
19
  const softGreen = (s) => `\x1b[38;2;127;216;143m${s}${R}`;
20
+ const violet = (s) => `\x1b[38;2;232;91;226m${s}${R}`;
20
21
 
21
22
  // ── Formatters ───────────────────────────────────────────────────────
22
23
  const bold = (s) => `${B}${s}${R}`;
@@ -79,9 +80,16 @@ const message = {
79
80
  };
80
81
 
81
82
  const statusBar = {
82
- background: bg256(236),
83
- text: fg256(250),
84
- accent: cyan,
83
+ muted: brightBlack,
84
+ cwd: (s) => `${D}\x1b[38;5;244m${s}${R}`,
85
+ prompt: fg256(250),
86
+ accent: violet,
87
+ };
88
+
89
+ const modeLabel = {
90
+ do: orange,
91
+ discuss: green,
92
+ fallback: orange,
85
93
  };
86
94
 
87
95
  const shell = {
@@ -105,7 +113,7 @@ const selectList = {
105
113
 
106
114
  // ── Editor theme (consumed by pi-tui Editor component) ──────────────
107
115
  const EDITOR_THEME = {
108
- borderColor: border.default,
116
+ borderColor: fg256(238),
109
117
  selectList,
110
118
  };
111
119
 
@@ -127,7 +135,7 @@ export {
127
135
  R, B, D,
128
136
  black, red, green, yellow, blue, magenta, cyan, white,
129
137
  brightBlack, brightRed, brightGreen,
130
- orange, softGreen,
138
+ orange, softGreen, violet,
131
139
  bold, dim, inverse,
132
140
  fg256, bg256,
133
141
  // Semantic
@@ -140,6 +148,7 @@ export {
140
148
  tool,
141
149
  message,
142
150
  statusBar,
151
+ modeLabel,
143
152
  shell,
144
153
  spinner,
145
154
  selectList,
package/src/cli/ui.mjs CHANGED
@@ -41,7 +41,7 @@ export function createTuiUI({
41
41
  const tui = new TUI(terminal);
42
42
  const output = new OutputBuffer();
43
43
  const shellDrawer = new ShellDrawer({ shellRuntime });
44
- const statusBar = new StatusBar();
44
+ const statusBar = new StatusBar(undefined, { cwd });
45
45
  const editor = new Editor(tui, EDITOR_THEME, { paddingX: 1 });
46
46
  const selection = new ScreenSelection();
47
47
  const mainPane = new MainPaneLayout({ output, statusBar, editor, terminal, selection });
@@ -3,14 +3,14 @@ import { buildSessionIdentity } from "./session-status.mjs";
3
3
  import { buildSystemCore, resolveSystemCorePromptKey } from "./system-core.mjs";
4
4
  import { buildInjectionsLayer } from "./injections.mjs";
5
5
  import { buildProjectContext } from "./project-context.mjs";
6
- import { buildCenterMemory } from "./center-memory.mjs";
6
+ import { buildProfileLayers } from "./profiles.mjs";
7
7
  import { formatRecallHints } from "../memory/markdown-store.mjs";
8
8
 
9
9
  export class ContextEngine {
10
- constructor({ cwd, modelId, provider = "deepseek", thinkingLevel = "medium", namespace = "", memoryRoot = null, centerMemoryPath = null, shellRuntime = null, lspService = null, injections = [], maxTurns, trimBatch }) {
10
+ constructor({ cwd, modelId, provider = "deepseek", thinkingLevel = "medium", namespace = "", memoryRoot = null, profilePaths = null, shellRuntime = null, lspService = null, injections = [], maxTurns, trimBatch }) {
11
11
  this.cwd = cwd;
12
12
  this.memoryRoot = memoryRoot;
13
- this.centerMemoryPath = centerMemoryPath;
13
+ this.profilePaths = profilePaths;
14
14
  this.modelId = modelId;
15
15
  this.provider = provider;
16
16
  this.thinkingLevel = thinkingLevel;
@@ -62,19 +62,19 @@ export class ContextEngine {
62
62
  const projectCtx = buildProjectContext(this.cwd);
63
63
  if (projectCtx) layers.push({ name: "project_context", text: projectCtx });
64
64
 
65
- const centerMemory = buildCenterMemory(this.centerMemoryPath);
66
- if (centerMemory) layers.push({ name: "center_memory", text: centerMemory });
65
+ layers.push(...buildProfileLayers(this.profilePaths));
67
66
 
68
67
  layers.push({ name: "recent_chat", text: this.#buildRecentChat() });
69
68
 
70
69
  return layers;
71
70
  }
72
71
 
73
- recordTurn({ userMessage, assistantMessage, userRecallHints = [], assistantRecallHints = [] }) {
72
+ recordTurn({ userMessage, assistantMessage, assistantContext = "", userRecallHints = [], assistantRecallHints = [] }) {
74
73
  this.turns.push({
75
74
  index: this.turns.length + 1,
76
75
  userMessage,
77
76
  assistantMessage: assistantMessage ?? "",
77
+ assistantContext: assistantContext ?? "",
78
78
  userRecallHints,
79
79
  assistantRecallHints,
80
80
  });
@@ -167,9 +167,10 @@ export class ContextEngine {
167
167
  `[user]\n${String(turn.userMessage ?? "")}\n`;
168
168
  const userRecall = formatRecallHints("user", turn.userRecallHints ?? []);
169
169
  if (userRecall) block += `\n${userRecall}\n`;
170
- block += `\n[March]\n`;
171
- if (turn.assistantMessage) {
172
- block += `\n${String(turn.assistantMessage ?? "")}\n`;
170
+ block += `\n[assistant]\n`;
171
+ const assistantText = turn.assistantContext || turn.assistantMessage;
172
+ if (assistantText) {
173
+ block += `\n${String(assistantText ?? "")}\n`;
173
174
  }
174
175
  const assistantRecall = formatRecallHints("assistant", turn.assistantRecallHints ?? []);
175
176
  if (assistantRecall) block += `\n${assistantRecall}\n`;
@@ -0,0 +1,39 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export function defaultProfilePaths() {
6
+ const root = join(homedir(), ".march", "memory", "profiles");
7
+ return {
8
+ agent: join(root, "agent.md"),
9
+ user: join(root, "user.md"),
10
+ };
11
+ }
12
+
13
+ export function ensureProfileFiles(paths = defaultProfilePaths()) {
14
+ for (const [kind, path] of Object.entries(paths)) {
15
+ if (!path || existsSync(path)) continue;
16
+ mkdirSync(dirname(path), { recursive: true });
17
+ writeFileSync(path, defaultProfileContent(kind), "utf8");
18
+ }
19
+ }
20
+
21
+ export function buildProfileLayers(paths) {
22
+ if (!paths) return [];
23
+ return [
24
+ buildProfileLayer("agent_profile", paths.agent),
25
+ buildProfileLayer("user_profile", paths.user),
26
+ ].filter(Boolean);
27
+ }
28
+
29
+ function buildProfileLayer(name, path) {
30
+ if (!path || !existsSync(path)) return null;
31
+ const content = readFileSync(path, "utf8").trimEnd();
32
+ if (!content.trim()) return null;
33
+ return { name, text: `[${name}]\n--- ${path} ---\n${content}` };
34
+ }
35
+
36
+ function defaultProfileContent(kind) {
37
+ const title = kind === "agent" ? "Agent Profile" : "User Profile";
38
+ return `# ${title}\n\n`;
39
+ }
package/src/main.mjs CHANGED
@@ -15,7 +15,7 @@ import { createStatusLineUpdater } from "./cli/status-line-updater.mjs";
15
15
  import { wireTuiHandlers } from "./cli/tui/tui-handlers.mjs";
16
16
  import { createMarchAuthStorage } from "./auth/storage.mjs";
17
17
  import { runLoginCommand } from "./auth/login-command.mjs";
18
- import { createRunner } from "./agent/runner.mjs";
18
+ import { createRuntimeRunner } from "./cli/startup/create-runtime-runner.mjs";
19
19
  import { createCliShellRuntime } from "./shell/cli-runtime.mjs";
20
20
  import { MarkdownMemoryStore } from "./memory/markdown-store.mjs";
21
21
  import { createMarkdownMemoryTools } from "./memory/markdown-tools.mjs";
@@ -23,14 +23,13 @@ import { loadDotEnv } from "./config/dotenv.mjs";
23
23
  import { loadConfig } from "./config/loader.mjs";
24
24
  import { discoverProjectExtensionPaths } from "./extensions/discovery.mjs";
25
25
  import { loadProjectLifecycleHookManifests } from "./extensions/lifecycle-manifest.mjs";
26
- import { resolvePiSessionManager } from "./session/pi-manager.mjs";
27
26
  import { loadOrCreateProjectId, resumeStartupSession } from "./cli/startup/startup-session.mjs";
28
27
  import { formatStartupBanner } from "./cli/startup/startup-banner.mjs";
29
28
  import { initializeMcp } from "./mcp/index.mjs";
30
29
  import { createWebToolsFromConfig } from "./web/tools.mjs";
31
30
  import { createModelContextDumper } from "./debug/model-context-dumper.mjs";
32
31
  import { createLogger, installProcessLogHandlers } from "./debug/logger.mjs";
33
- import { defaultCenterMemoryPath } from "./context/center-memory.mjs";
32
+ import { defaultProfilePaths, ensureProfileFiles } from "./context/profiles.mjs";
34
33
  import { runProviderConfigCommand } from "./provider/config-command.mjs";
35
34
  import { runWebSearchConfigCommand } from "./web/config-command.mjs";
36
35
  import { createDesktopTurnNotifier } from "./notification/desktop-notifier.mjs";
@@ -49,6 +48,7 @@ export async function run(argv) {
49
48
  }
50
49
 
51
50
  const config = loadConfig(cwd);
51
+ const useRuntimeProcess = process.env.MARCH_RUNTIME_PROCESS !== "0";
52
52
  installNetworkEnvironment(config.network);
53
53
  if (args.command?.name === "login") {
54
54
  try {
@@ -103,14 +103,16 @@ export async function run(argv) {
103
103
  const modeState = createModeState();
104
104
  const namespace = loadOrCreateProjectId(projectMarchDir);
105
105
  const memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
106
- const centerMemoryPath = defaultCenterMemoryPath();
106
+ const profilePaths = defaultProfilePaths();
107
+ ensureProfileFiles(profilePaths);
107
108
  const memoryStore = new MarkdownMemoryStore({ root: memoryRoot });
108
109
  const memoryTools = createMarkdownMemoryTools(memoryStore);
109
110
  const currentProject = basename(cwd);
110
111
  const shellRuntime = args.shellRuntime ? createCliShellRuntime({ cwd }) : null;
111
112
 
112
- // MCP: connect to configured MCP servers
113
- const mcpInit = await initializeMcp({ projectDir: cwd });
113
+ const mcpInit = useRuntimeProcess
114
+ ? { clientManager: null, mcpTools: [], mcpInjections: [], errors: [] }
115
+ : await initializeMcp({ projectDir: cwd });
114
116
  const { clientManager: mcpClientManager, mcpTools, mcpInjections } = mcpInit;
115
117
  for (const { server, error } of mcpInit.errors) {
116
118
  if (args.json) {
@@ -159,17 +161,33 @@ export async function run(argv) {
159
161
  let turnRunning = false;
160
162
  let refreshStatusBar = null;
161
163
 
162
- const runner = await createRunner({
164
+ const runnerOptions = {
163
165
  cwd,
164
166
  modelId: model,
165
167
  provider,
166
168
  serviceTier,
167
-
168
169
  providers: config.providers,
170
+ config,
169
171
  stateRoot,
170
- ui,
171
172
  memoryRoot,
172
- centerMemoryPath,
173
+ profilePaths,
174
+ namespace,
175
+ projectMarchDir,
176
+ extensionPaths,
177
+ permissionMode,
178
+ shellRuntime: Boolean(shellRuntime),
179
+ lifecycleHooks: lifecycleManifests.hooks,
180
+ lifecycleDiagnostics: lifecycleManifests.diagnostics,
181
+ modelContextDumper: {
182
+ enabled: args.dumpContext,
183
+ rootDir: contextDumpRoot,
184
+ },
185
+ };
186
+
187
+ const runner = await createRuntimeRunner({
188
+ useRuntimeProcess,
189
+ runnerOptions,
190
+ ui,
173
191
  memoryStore,
174
192
  memoryTools,
175
193
  shellRuntime,
@@ -177,29 +195,14 @@ export async function run(argv) {
177
195
  mcpInjections,
178
196
  mcpClientManager,
179
197
  webTools,
180
- namespace,
181
- projectMarchDir,
182
- extensionPaths,
183
- sessionManager: resolvePiSessionManager({
184
- cwd,
185
- projectMarchDir,
186
- enabled: usePiSessions,
187
- }),
188
- useRuntimeHost: usePiRuntimeHost,
189
- syncPiSidecar: usePiSessions || usePiRuntimeHost,
190
- lifecycleHooks: lifecycleManifests.hooks,
191
- lifecycleDiagnostics: lifecycleManifests.diagnostics,
198
+ usePiSessions,
199
+ usePiRuntimeHost,
192
200
  authStorage: authConfig.authStorage,
193
- maxTurns: config.maxTurns ?? undefined,
194
- trimBatch: config.trimBatch ?? undefined,
195
- hostedTools: config.hostedTools,
196
201
  permissionController,
197
202
  modelContextDumper,
198
203
  turnNotifier,
199
204
  logger,
200
- onModelPayload: ({ estimatedTokens }) => {
201
- refreshStatusBar?.({ contextTokens: estimatedTokens });
202
- },
205
+ refreshStatusBar: (...args) => refreshStatusBar?.(...args),
203
206
  });
204
207
 
205
208
  refreshStatusBar = createStatusLineUpdater({
@@ -209,7 +212,10 @@ export async function run(argv) {
209
212
  sessionSource,
210
213
  getMode: () => modeState.get(),
211
214
  });
212
- refreshStatusBar();
215
+ const initialContextTokens = typeof runner.estimateContextTokens === "function"
216
+ ? await runner.estimateContextTokens("")
217
+ : null;
218
+ refreshStatusBar(initialContextTokens ? { contextTokens: initialContextTokens } : undefined);
213
219
 
214
220
  wireTuiHandlers({
215
221
  ui,
@@ -1,112 +0,0 @@
1
- import { readdirSync, statSync } from "node:fs";
2
- import { isAbsolute, relative, resolve } from "node:path";
3
- import { defineTool } from "@earendil-works/pi-coding-agent";
4
- import { Type } from "typebox";
5
- import { toolText } from "./tool-result.mjs";
6
-
7
- const DEFAULT_LIMIT = 1000;
8
- const DEFAULT_IGNORES = new Set([".git", "node_modules"]);
9
-
10
- export function createFindTool({ cwd }) {
11
- return defineTool({
12
- name: "find",
13
- label: "Find Files",
14
- description: "Find files by glob pattern. Pattern is matched relative to the search directory. Basename-only patterns like '*.mjs' search recursively, so find('*.mjs', path:'src') and find('src/**/*.mjs') both work.",
15
- parameters: Type.Object({
16
- pattern: Type.String({ description: "Glob pattern to match files, e.g. '*.mjs', '**/*.json', or 'src/**/*.test.mjs'" }),
17
- path: Type.Optional(Type.String({ description: "Directory to search in (default: current directory)" })),
18
- limit: Type.Optional(Type.Number({ description: "Maximum number of results (default 1000)" })),
19
- }),
20
- execute: async (_toolCallId, params) => executeFind({ cwd, ...params }),
21
- });
22
- }
23
-
24
- export function executeFind({ cwd, pattern, path = ".", limit = DEFAULT_LIMIT }) {
25
- const searchRoot = resolveSearchRoot(cwd, path);
26
- const trimmedPattern = String(pattern ?? "").trim().replaceAll("\\", "/");
27
- if (!trimmedPattern) return toolText("Error: pattern is required", { error: true });
28
- const effectivePattern = normalizePattern(trimmedPattern);
29
-
30
- const max = Math.max(1, Number(limit) || DEFAULT_LIMIT);
31
- let files;
32
- try {
33
- files = listFiles(searchRoot);
34
- } catch (err) {
35
- return toolText(`Error: ${err.message}`, { error: true });
36
- }
37
-
38
- const matches = [];
39
- for (const file of files) {
40
- const rel = toPosix(relative(searchRoot, file));
41
- if (!matchesGlob(effectivePattern, rel)) continue;
42
- matches.push(rel);
43
- if (matches.length >= max) break;
44
- }
45
-
46
- if (matches.length === 0) return toolText("No files found matching pattern", { pattern: trimmedPattern, effectivePattern, path: searchRoot, count: 0 });
47
- const limitHint = matches.length >= max ? `\n\n[Results truncated to ${max}. Increase limit or refine pattern.]` : "";
48
- return toolText(`${matches.join("\n")}${limitHint}`, {
49
- pattern: trimmedPattern,
50
- effectivePattern: effectivePattern === trimmedPattern ? undefined : effectivePattern,
51
- path: searchRoot,
52
- count: matches.length,
53
- resultLimitReached: matches.length >= max ? max : undefined,
54
- });
55
- }
56
-
57
- function normalizePattern(pattern) {
58
- if (pattern.includes("/") || pattern.includes("**")) return pattern;
59
- return `**/${pattern}`;
60
- }
61
-
62
- function resolveSearchRoot(cwd, path) {
63
- const raw = String(path || ".");
64
- return isAbsolute(raw) ? raw : resolve(cwd, raw);
65
- }
66
-
67
- function listFiles(root) {
68
- const out = [];
69
- walk(root, out);
70
- return out.sort((a, b) => toPosix(a).localeCompare(toPosix(b)));
71
- }
72
-
73
- function walk(dir, out) {
74
- for (const entry of readdirSync(dir, { withFileTypes: true })) {
75
- if (entry.isDirectory() && DEFAULT_IGNORES.has(entry.name)) continue;
76
- const path = resolve(dir, entry.name);
77
- if (entry.isDirectory()) walk(path, out);
78
- else if (entry.isFile()) out.push(path);
79
- }
80
- }
81
-
82
- function matchesGlob(pattern, candidate) {
83
- return matchSegments(splitGlob(pattern), splitGlob(candidate));
84
- }
85
-
86
- function matchSegments(patternSegments, candidateSegments) {
87
- if (patternSegments.length === 0) return candidateSegments.length === 0;
88
- const [head, ...tail] = patternSegments;
89
- if (head === "**") {
90
- if (matchSegments(tail, candidateSegments)) return true;
91
- return candidateSegments.length > 0 && matchSegments(patternSegments, candidateSegments.slice(1));
92
- }
93
- if (candidateSegments.length === 0) return false;
94
- return matchSegment(head, candidateSegments[0]) && matchSegments(tail, candidateSegments.slice(1));
95
- }
96
-
97
- function matchSegment(pattern, candidate) {
98
- const regex = new RegExp(`^${escapeRegex(pattern).replaceAll("\\*", "[^/]*").replaceAll("\\?", "[^/]")}$`);
99
- return regex.test(candidate);
100
- }
101
-
102
- function splitGlob(value) {
103
- return String(value).split("/").filter(Boolean);
104
- }
105
-
106
- function toPosix(value) {
107
- return String(value).replaceAll("\\", "/");
108
- }
109
-
110
- function escapeRegex(value) {
111
- return String(value).replace(/[|\\{}()[\]^$+*?.]/g, "\\$&");
112
- }
@@ -1,14 +0,0 @@
1
- import { existsSync, readFileSync } from "node:fs";
2
- import { homedir } from "node:os";
3
- import { join } from "node:path";
4
-
5
- export function defaultCenterMemoryPath() {
6
- return join(homedir(), ".march", "memory", "center.md");
7
- }
8
-
9
- export function buildCenterMemory(path = defaultCenterMemoryPath()) {
10
- if (!path || !existsSync(path)) return null;
11
- const content = readFileSync(path, "utf8").trimEnd();
12
- if (!content.trim()) return null;
13
- return `[center_memory]\n--- ${path} ---\n${content}`;
14
- }