march-cli 0.1.31 → 0.1.33

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 (36) hide show
  1. package/package.json +1 -1
  2. package/src/agent/output/binary-output-sink.mjs +17 -0
  3. package/src/agent/output/send-binary-tool.mjs +84 -0
  4. package/src/agent/tools.mjs +3 -0
  5. package/src/cli/args.mjs +3 -1
  6. package/src/cli/commands/help-command.mjs +1 -1
  7. package/src/cli/commands/mode-command.mjs +21 -0
  8. package/src/cli/repl-loop.mjs +1 -0
  9. package/src/cli/slash-commands.mjs +8 -0
  10. package/src/cli/startup/configured-command.mjs +17 -0
  11. package/src/cli/startup/gateway-daemon-command.mjs +21 -0
  12. package/src/cli/tui/markdown-renderer.mjs +37 -23
  13. package/src/config/loader.mjs +31 -0
  14. package/src/context/system-core/base.md +6 -2
  15. package/src/gateway/command-router.mjs +44 -0
  16. package/src/gateway/command.mjs +107 -0
  17. package/src/gateway/config.mjs +62 -0
  18. package/src/gateway/daemon.mjs +41 -0
  19. package/src/gateway/handler.mjs +29 -0
  20. package/src/gateway/message.mjs +37 -0
  21. package/src/gateway/platform-registry.mjs +38 -0
  22. package/src/gateway/platforms/telegram.mjs +241 -0
  23. package/src/gateway/runner-bridge.mjs +55 -0
  24. package/src/gateway/runtime/queue.mjs +46 -0
  25. package/src/gateway/session-store.mjs +46 -0
  26. package/src/gateway/setup/command.mjs +150 -0
  27. package/src/gateway/workspace-command.mjs +40 -0
  28. package/src/image-gen/tool.mjs +16 -9
  29. package/src/lsp/client.mjs +1 -0
  30. package/src/lsp/managed-node-server.mjs +1 -0
  31. package/src/lsp/server-definitions.mjs +8 -0
  32. package/src/main.mjs +6 -9
  33. package/src/memory/markdown/markdown-format.mjs +0 -9
  34. package/src/memory/markdown-store.mjs +3 -4
  35. package/src/memory/markdown-tools.mjs +1 -1
  36. package/src/platform/open-file.mjs +9 -10
@@ -0,0 +1,40 @@
1
+ export function parseWorkspaceCommand(input) {
2
+ const trimmed = String(input ?? "").trim();
3
+ if (trimmed === "/workspace") return { type: "show" };
4
+ if (trimmed === "/workspaces") return { type: "list" };
5
+ const match = trimmed.match(/^\/workspace\s+set\s+(\S+)$/);
6
+ if (match) return { type: "set", alias: match[1] };
7
+ if (trimmed.startsWith("/workspace ")) return { type: "error", message: "Usage: /workspace, /workspaces, or /workspace set <alias>" };
8
+ return { type: "none" };
9
+ }
10
+
11
+ export function handleWorkspaceCommand(command, { session, sessionStore }) {
12
+ if (command.type === "error") return [`Error: ${command.message}`];
13
+ if (command.type === "show") return [formatCurrentWorkspace(session)];
14
+ if (command.type === "list") return formatWorkspaceList(sessionStore.listWorkspaces(), session.workspaceAlias);
15
+ if (command.type === "set") {
16
+ try {
17
+ sessionStore.setWorkspace(session, command.alias);
18
+ return [`Workspace: ${session.workspaceAlias} (${session.workspaceRoot})`];
19
+ } catch (err) {
20
+ return [`Error: ${err.message}`];
21
+ }
22
+ }
23
+ return [];
24
+ }
25
+
26
+ function formatCurrentWorkspace(session) {
27
+ if (!session.workspaceAlias || !session.workspaceRoot) return "Workspace: not configured";
28
+ return `Workspace: ${session.workspaceAlias} (${session.workspaceRoot})`;
29
+ }
30
+
31
+ function formatWorkspaceList(workspaces, currentAlias) {
32
+ if (workspaces.length === 0) return ["No gateway workspaces configured."];
33
+ return [
34
+ "Gateway workspaces:",
35
+ ...workspaces.map((workspace) => {
36
+ const marker = workspace.alias === currentAlias ? "*" : " ";
37
+ return `${marker} ${workspace.alias}: ${workspace.root}`;
38
+ }),
39
+ ];
40
+ }
@@ -1,14 +1,15 @@
1
1
  import { defineTool } from "@earendil-works/pi-coding-agent";
2
2
  import { Type } from "typebox";
3
+ import { basename } from "node:path";
4
+ import { sendBinaryOutput } from "../agent/output/binary-output-sink.mjs";
3
5
  import { toolText } from "../agent/tool-result.mjs";
4
- import { openFileWithDefaultApp } from "../platform/open-file.mjs";
5
6
  import { generateImage } from "./provider.mjs";
6
7
 
7
8
  export function createImageGenTool({
8
9
  authStorage,
9
10
  projectMarchDir,
10
11
  generateImageImpl = generateImage,
11
- openFileImpl = openFileWithDefaultApp,
12
+ sendBinary = sendBinaryOutput,
12
13
  }) {
13
14
  return defineTool({
14
15
  name: "image_generate",
@@ -48,7 +49,7 @@ export function createImageGenTool({
48
49
  try {
49
50
  const { prompt, quality = "medium", aspectRatio = "1:1", auto_open: autoOpen = true } = params;
50
51
  const image = await generateImageImpl({ prompt, quality, aspectRatio, authStorage, projectMarchDir });
51
- const openResult = autoOpen ? await openGeneratedImage(image.filePath, openFileImpl) : { opened: false };
52
+ const outputResult = autoOpen ? await deliverGeneratedImage(image, sendBinary) : { opened: false, delivered: false };
52
53
  return toolJson({
53
54
  success: true,
54
55
  image: image.marker,
@@ -57,8 +58,8 @@ export function createImageGenTool({
57
58
  prompt,
58
59
  aspectRatio,
59
60
  quality,
60
- ...openResult,
61
- }, { ...image, ...openResult });
61
+ ...outputResult,
62
+ }, { ...image, ...outputResult });
62
63
  } catch (err) {
63
64
  return toolJson({
64
65
  success: false,
@@ -70,12 +71,18 @@ export function createImageGenTool({
70
71
  });
71
72
  }
72
73
 
73
- async function openGeneratedImage(filePath, openFileImpl) {
74
+ async function deliverGeneratedImage(image, sendBinary) {
75
+ const binary = {
76
+ type: "image",
77
+ path: image.filePath,
78
+ filename: basename(image.filePath),
79
+ mimeType: image.mimeType,
80
+ };
74
81
  try {
75
- await openFileImpl(filePath);
76
- return { opened: true };
82
+ const sink = await sendBinary(binary);
83
+ return { opened: sink?.opened === true, delivered: true, sink };
77
84
  } catch (err) {
78
- return { opened: false, openError: err.message };
85
+ return { opened: false, delivered: false, openError: err.message };
79
86
  }
80
87
  }
81
88
 
@@ -47,6 +47,7 @@ const LANGUAGE_IDS = {
47
47
  ".sass": "sass",
48
48
  ".scss": "scss",
49
49
  ".sh": "shellscript",
50
+ ".sql": "sql",
50
51
  ".svelte": "svelte",
51
52
  ".tf": "terraform",
52
53
  ".tfvars": "terraform-vars",
@@ -12,6 +12,7 @@ const MANAGED_PACKAGES = {
12
12
  "vscode-json-language-server": ["vscode-langservers-extracted"],
13
13
  "vscode-html-language-server": ["vscode-langservers-extracted"],
14
14
  "vscode-css-language-server": ["vscode-langservers-extracted"],
15
+ "sql-language-server": ["sql-language-server"],
15
16
  "docker-langserver": ["dockerfile-language-server-nodejs"],
16
17
  };
17
18
 
@@ -88,6 +88,14 @@ export function createLspServerDefinitions({ resolveTypeScriptProjectRoot, resol
88
88
  managedCommand: "vscode-json-language-server",
89
89
  args: ["--stdio"],
90
90
  },
91
+ {
92
+ id: "sql",
93
+ extensions: [".sql"],
94
+ rootMarkers: [".sqllsrc.json", ".sqllsrc", "package.json", ".git"],
95
+ command: ["sql-language-server"],
96
+ managedCommand: "sql-language-server",
97
+ args: ["up", "--method", "stdio"],
98
+ },
91
99
  {
92
100
  id: "html",
93
101
  extensions: [".html", ".htm"],
package/src/main.mjs CHANGED
@@ -38,7 +38,8 @@ import { installNetworkEnvironment } from "./network/environment.mjs";
38
38
  import { runMemoryCommand } from "./memory/command.mjs";
39
39
  import { normalizeRemoteMemorySources } from "./memory/remote/config.mjs";
40
40
  import { resolveMemoryRoot } from "./memory/root.mjs";
41
- import { runBrowserCommand } from "./browser/cli/command.mjs";
41
+ import { runConfiguredCliCommand } from "./cli/startup/configured-command.mjs";
42
+ import { maybeRunGatewayDaemonCommand } from "./cli/startup/gateway-daemon-command.mjs";
42
43
  import { ensureBrowserDaemon } from "./browser/client/lifecycle.mjs";
43
44
  export async function run(argv) {
44
45
  const cwd = process.cwd();
@@ -77,14 +78,8 @@ export async function run(argv) {
77
78
  args.memoryRoot = resolveMemoryRoot(config.memoryRoot, stateRoot);
78
79
  return await runMemoryCommand(args, { homeDir: homedir() });
79
80
  }
80
- if (args.command?.name === "browser") {
81
- try {
82
- return await runBrowserCommand(args, { stateRoot });
83
- } catch (err) {
84
- process.stderr.write(`Error: ${err.message}\n`);
85
- return 1;
86
- }
87
- }
81
+ const configuredCommand = await runConfiguredCliCommand(args, { config, cwd, stateRoot });
82
+ if (configuredCommand.handled) return configuredCommand.code;
88
83
  if (!existsSync(stateRoot)) mkdirSync(stateRoot, { recursive: true });
89
84
  await ensureBrowserDaemon({ stateRoot }).catch(() => {});
90
85
  const logger = createLogger({ logDir: join(stateRoot, "logs") });
@@ -243,6 +238,8 @@ export async function run(argv) {
243
238
  ui,
244
239
  });
245
240
  refreshStatusBar();
241
+ const gatewayDaemonCommand = await maybeRunGatewayDaemonCommand(args, { config, cwd, runner, currentProject, memoryStore, ui, logger });
242
+ if (gatewayDaemonCommand.handled) return gatewayDaemonCommand.code;
246
243
 
247
244
  if (args.prompt) {
248
245
  turnRunning = true;
@@ -55,15 +55,6 @@ export function generateMemoryId() {
55
55
  return `mem_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
56
56
  }
57
57
 
58
- export function slugify(value) {
59
- return String(value ?? "memory")
60
- .trim()
61
- .toLowerCase()
62
- .replace(/[^a-z0-9\u4e00-\u9fff]+/g, "-")
63
- .replace(/^-+|-+$/g, "")
64
- .slice(0, 80) || "memory";
65
- }
66
-
67
58
  export function walkMarkdownFiles(root) {
68
59
  const out = [];
69
60
  if (!existsSync(root)) return out;
@@ -8,7 +8,6 @@ import {
8
8
  normalizeText,
9
9
  parseMemoryMarkdown,
10
10
  quoteFtsTerm,
11
- slugify,
12
11
  walkMarkdownFiles,
13
12
  } from "./markdown/markdown-format.mjs";
14
13
  import { scoreEntry, toHint } from "./markdown/markdown-recall.mjs";
@@ -176,7 +175,7 @@ export class MarkdownMemoryStore {
176
175
  if (!nextDescription) throw new Error("description is required");
177
176
  if (!existing && body == null) throw new Error("body is required");
178
177
  const nextBody = body ?? (existing ? parseMemoryMarkdown(readFileSync(existing.path, "utf8")).body : "");
179
- const nextPath = existing?.path ?? this.#newMemoryPath(now, nextName);
178
+ const nextPath = existing?.path ?? this.#newMemoryPath(now, nextId);
180
179
  mkdirSync(dirname(nextPath), { recursive: true });
181
180
  const content = formatMemoryMarkdown({
182
181
  frontmatter: {
@@ -248,11 +247,11 @@ export class MarkdownMemoryStore {
248
247
  }
249
248
  }
250
249
 
251
- #newMemoryPath(isoDate, name) {
250
+ #newMemoryPath(isoDate, id) {
252
251
  const date = isoDate.slice(0, 10);
253
252
  const [year, month, day] = date.split("-");
254
253
  const week = `week${Math.ceil(Number(day) / 7)}`;
255
- return join(this.root, year, month, week, `${date}-${slugify(name)}.md`);
254
+ return join(this.root, year, month, week, `${date}-${id}.md`);
256
255
  }
257
256
 
258
257
  #resolveMemoryPath(raw) {
@@ -70,7 +70,7 @@ export function createMarkdownMemoryTools(store, { remoteSources = [] } = {}) {
70
70
  name: "memory_save",
71
71
  label: "Memory Save",
72
72
  description:
73
- "Create a Markdown memory or update whole fields on an existing memory. For targeted edits to an existing memory body or frontmatter, use memory_open to get the path, then edit_file. Before creating a new memory, merge related updates into an existing memory when they share the same topic or decision thread. New memories require name, description, body, and at least one tag because recall hints only use tags. When updating by id, omitted fields keep their existing values; passing tags replaces the full tag list.",
73
+ "Create a Markdown memory or update whole fields on an existing memory. Local memory filenames are id-based storage paths; frontmatter name is the user-visible title. For targeted edits to an existing memory body or frontmatter, use memory_open to get the path, then edit_file. Before creating a new memory, merge related updates into an existing memory when they share the same topic or decision thread. New memories require name, description, body, and at least one tag because recall hints only use tags. When updating by id, omitted fields keep their existing values; passing tags replaces the full tag list.",
74
74
  parameters: Type.Object({
75
75
  id: Type.Optional(Type.String({ description: "Existing memory id to update. Omit to create a new memory." })),
76
76
  name: Type.Optional(Type.String({ description: "Memory name. Required when creating." })),
@@ -1,9 +1,9 @@
1
1
  import { spawn } from "node:child_process";
2
2
 
3
- export function openFileWithDefaultApp(filePath) {
3
+ export function openFileWithDefaultApp(filePath, { spawnFn = spawn } = {}) {
4
4
  return new Promise((resolve, reject) => {
5
- const { command, args } = openCommand(filePath);
6
- const child = spawn(command, args, { detached: true, stdio: "ignore" });
5
+ const { command, args, options } = openCommand(filePath);
6
+ const child = spawnFn(command, args, { ...options, detached: true, stdio: "ignore" });
7
7
  child.once("error", reject);
8
8
  child.once("spawn", () => {
9
9
  child.unref();
@@ -12,15 +12,14 @@ export function openFileWithDefaultApp(filePath) {
12
12
  });
13
13
  }
14
14
 
15
- function openCommand(filePath) {
16
- if (process.platform === "win32") {
17
- return {
18
- command: "powershell.exe",
19
- args: ["-NoProfile", "-Command", "Start-Process -LiteralPath $args[0]", filePath],
20
- };
15
+ export function openCommand(filePath, { platform = process.platform } = {}) {
16
+ if (platform === "win32") {
17
+ // cmd.exe start delegates to the user's shell association more reliably than
18
+ // PowerShell Start-Process for media files on Windows.
19
+ return { command: "cmd.exe", args: ["/c", "start", "", filePath] };
21
20
  }
22
21
 
23
- if (process.platform === "darwin") {
22
+ if (platform === "darwin") {
24
23
  return { command: "open", args: [filePath] };
25
24
  }
26
25