march-cli 0.1.13 → 0.1.15

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.
@@ -26,13 +26,13 @@ export async function resumePiSessionById(id, { runner, sessions, projectMarchDi
26
26
  }
27
27
 
28
28
  let result;
29
+ const restoreState = toContextSessionState(sidecar.state);
29
30
  try {
30
- result = await runner.switchPiSession(session.path);
31
+ result = await runner.switchPiSession(session.path, restoreState);
31
32
  } catch (err) {
32
33
  return [`Error: failed to switch pi session ${session.id}: ${err.message}`];
33
34
  }
34
35
  if (result?.cancelled) return [`Resume pi session cancelled: ${session.id}`];
35
- runner.engine.restoreSession(toContextSessionState(sidecar.state), null, { replace: true });
36
36
  return [`Resumed pi session: ${session.id}`];
37
37
  }
38
38
 
@@ -75,7 +75,8 @@ export class StatusBar {
75
75
  const mode = formatModeLabel(parts.mode || DEFAULT_STATUS_TEXT);
76
76
  const activity = parts.activity ? statusBar.muted(`${parts.activity} · `) : "";
77
77
  const right = [parts.model, parts.thinking].filter(Boolean).join(" • ");
78
- const line = composeMetaLine({ left: `${activity}${mode}`, right, width: innerWidth, muteLeft: false });
78
+ const paintWidth = inputPaintWidth(innerWidth);
79
+ const line = composeMetaLine({ left: `${activity}${mode}`, right, width: paintWidth, muteLeft: false });
79
80
  return ["", `${insetLeft}${line}${insetRight}`];
80
81
  }
81
82
  }
@@ -38,6 +38,17 @@ export function upsertProviderProfile({ path = globalConfigJsonPath(), id, type,
38
38
  return config;
39
39
  }
40
40
 
41
+ export function upsertSharedProviderProfile({ path = globalConfigJsonPath(), id, provider }) {
42
+ const config = readConfigJson(path);
43
+ const providers = config.providers && typeof config.providers === "object" && !Array.isArray(config.providers)
44
+ ? config.providers
45
+ : {};
46
+ providers[id] = provider;
47
+ config.providers = providers;
48
+ writeConfigJson(path, config);
49
+ return config;
50
+ }
51
+
41
52
  export function upsertModelSelection({ path = globalConfigJsonPath(), provider, model, serviceTier }) {
42
53
  const config = readConfigJson(path);
43
54
  config.provider = provider;
@@ -53,7 +53,8 @@ The user primarily asks for software engineering work: fixing bugs, adding behav
53
53
  </git_contract>
54
54
 
55
55
  <memory_system>
56
- - [memory_hint source="..."] blocks in recent_chat show memory hints matched from your thinking output. Use memory_open(id) to read the full content.
56
+ - [memory_hint source="..."] blocks in recent_chat are lightweight recall hints matched from prior thinking output. Treat them as possibly relevant pointers, not as complete facts.
57
+ - If a memory hint may help the current task, use memory_open(id) to read the full memory before relying on it. Ignore hints that are clearly unrelated or too low-value for the task.
57
58
  - Use memory_search(query) for full-text search across all memories.
58
59
  - To edit an existing memory, use memory_open(id) to get its path, then edit_file with mode="patch" for targeted edits.
59
60
  - Use memory_save() to create memories or update whole fields. Before creating a new memory, first search/open related memories and merge updates into an existing memory when they share the same topic, project, or decision thread; prefer modifying the existing memory file over creating a scattered new one. Tags are the primary retrieval key for future recall. Prefer lowercase kebab-case tags like 'march-cli', 'tooling', 'permissions'.
@@ -1,23 +1,76 @@
1
1
  import { pathToFileURL } from "node:url";
2
2
  import { spawnCommand } from "../platform/spawn-command.mjs";
3
- import { extname } from "node:path";
3
+ import { basename, extname } from "node:path";
4
4
  import { readFileSync } from "node:fs";
5
5
 
6
6
  const INITIALIZE_TIMEOUT_MS = 15000;
7
7
  const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2;
8
8
 
9
9
  const LANGUAGE_IDS = {
10
+ ".astro": "astro",
11
+ ".bash": "shellscript",
12
+ ".c": "c",
13
+ ".c++": "cpp",
14
+ ".cc": "cpp",
15
+ ".cjs": "javascript",
16
+ ".cpp": "cpp",
17
+ ".css": "css",
18
+ ".cts": "typescript",
19
+ ".cxx": "cpp",
20
+ ".dart": "dart",
21
+ ".dockerfile": "dockerfile",
22
+ ".go": "go",
23
+ ".h": "c",
24
+ ".h++": "cpp",
25
+ ".hh": "cpp",
26
+ ".hpp": "cpp",
27
+ ".htm": "html",
28
+ ".html": "html",
29
+ ".hxx": "cpp",
10
30
  ".js": "javascript",
31
+ ".json": "json",
32
+ ".jsonc": "jsonc",
11
33
  ".jsx": "javascriptreact",
12
- ".ts": "typescript",
13
- ".tsx": "typescriptreact",
34
+ ".ksh": "shellscript",
35
+ ".less": "less",
36
+ ".lua": "lua",
37
+ ".markdown": "markdown",
38
+ ".md": "markdown",
39
+ ".mdx": "mdx",
14
40
  ".mjs": "javascript",
15
- ".cjs": "javascript",
16
41
  ".mts": "typescript",
17
- ".cts": "typescript",
42
+ ".php": "php",
43
+ ".prisma": "prisma",
44
+ ".py": "python",
45
+ ".pyi": "python",
46
+ ".rs": "rust",
47
+ ".sass": "sass",
48
+ ".scss": "scss",
49
+ ".sh": "shellscript",
50
+ ".svelte": "svelte",
51
+ ".tf": "terraform",
52
+ ".tfvars": "terraform-vars",
53
+ ".toml": "toml",
54
+ ".ts": "typescript",
55
+ ".tsx": "typescriptreact",
18
56
  ".vue": "vue",
57
+ ".yaml": "yaml",
58
+ ".yml": "yaml",
59
+ ".zig": "zig",
60
+ ".zon": "zig",
61
+ ".zsh": "shellscript",
62
+ };
63
+
64
+ const FILENAME_LANGUAGE_IDS = {
65
+ containerfile: "dockerfile",
66
+ dockerfile: "dockerfile",
19
67
  };
20
68
 
69
+ export function languageIdForPath(path) {
70
+ const name = basename(path).toLowerCase();
71
+ return FILENAME_LANGUAGE_IDS[name] ?? LANGUAGE_IDS[extname(path).toLowerCase()] ?? "plaintext";
72
+ }
73
+
21
74
  export class LspClient {
22
75
  constructor({ serverId, command, args = [], cwd, initialization = {}, store }) {
23
76
  this.serverId = serverId;
@@ -94,7 +147,7 @@ export class LspClient {
94
147
  this.#notify("textDocument/didOpen", {
95
148
  textDocument: {
96
149
  uri,
97
- languageId: LANGUAGE_IDS[extname(path).toLowerCase()] ?? "plaintext",
150
+ languageId: languageIdForPath(path),
98
151
  version: 0,
99
152
  text,
100
153
  },
@@ -8,6 +8,11 @@ const DEFAULT_CACHE_ROOT = join(homedir(), ".march", "lsp", "node");
8
8
  const MANAGED_PACKAGES = {
9
9
  "typescript-language-server": ["typescript-language-server", "typescript"],
10
10
  "vue-language-server": ["@vue/language-server", "typescript"],
11
+ "pyright-langserver": ["pyright"],
12
+ "vscode-json-language-server": ["vscode-langservers-extracted"],
13
+ "vscode-html-language-server": ["vscode-langservers-extracted"],
14
+ "vscode-css-language-server": ["vscode-langservers-extracted"],
15
+ "docker-langserver": ["dockerfile-language-server-nodejs"],
11
16
  };
12
17
 
13
18
  const installs = new Map();
@@ -0,0 +1,188 @@
1
+ const NODE_ROOT_MARKERS = ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock", "package.json"];
2
+
3
+ export function createLspServerDefinitions({ resolveTypeScriptProjectRoot, resolveTypeScriptSdk, resolveTypeScriptServer }) {
4
+ return [
5
+ {
6
+ id: "vue",
7
+ extensions: [".vue"],
8
+ rootMarkers: NODE_ROOT_MARKERS,
9
+ command: ["vue-language-server"],
10
+ managedCommand: "vue-language-server",
11
+ args: ["--stdio"],
12
+ initialization: ({ root, workspaceRoot }) => {
13
+ const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
14
+ return tsdk ? { typescript: { tsdk } } : null;
15
+ },
16
+ managedTypeScript: true,
17
+ missingInitialization: "missing project typescript SDK",
18
+ },
19
+ {
20
+ id: "typescript",
21
+ extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
22
+ rootMarkers: NODE_ROOT_MARKERS,
23
+ projectRoot: resolveTypeScriptProjectRoot,
24
+ command: ["typescript-language-server"],
25
+ managedCommand: "typescript-language-server",
26
+ args: ["--stdio"],
27
+ initialization: ({ root, workspaceRoot }) => {
28
+ const tsserver = resolveTypeScriptServer({ root, workspaceRoot });
29
+ return tsserver ? { tsserver: { path: tsserver } } : null;
30
+ },
31
+ managedTypeScript: true,
32
+ missingInitialization: "missing project typescript/tsserver.js",
33
+ },
34
+ {
35
+ id: "python",
36
+ extensions: [".py", ".pyi"],
37
+ rootMarkers: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"],
38
+ command: ["pyright-langserver"],
39
+ managedCommand: "pyright-langserver",
40
+ args: ["--stdio"],
41
+ },
42
+ {
43
+ id: "go",
44
+ extensions: [".go"],
45
+ rootMarkers: ["go.work", "go.mod", "go.sum"],
46
+ command: ["gopls"],
47
+ args: [],
48
+ },
49
+ {
50
+ id: "rust",
51
+ extensions: [".rs"],
52
+ rootMarkers: ["Cargo.toml", "Cargo.lock"],
53
+ command: ["rust-analyzer"],
54
+ args: [],
55
+ },
56
+ {
57
+ id: "clangd",
58
+ extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
59
+ rootMarkers: ["compile_commands.json", "compile_flags.txt", ".clangd"],
60
+ command: ["clangd"],
61
+ args: ["--background-index", "--clang-tidy"],
62
+ },
63
+ {
64
+ id: "svelte",
65
+ extensions: [".svelte"],
66
+ rootMarkers: NODE_ROOT_MARKERS,
67
+ command: ["svelteserver", "svelte-language-server"],
68
+ args: ["--stdio"],
69
+ initialization: () => ({}),
70
+ },
71
+ {
72
+ id: "astro",
73
+ extensions: [".astro"],
74
+ rootMarkers: NODE_ROOT_MARKERS,
75
+ command: ["astro-ls", "@astrojs/language-server"],
76
+ args: ["--stdio"],
77
+ initialization: ({ root, workspaceRoot }) => {
78
+ const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
79
+ return tsdk ? { typescript: { tsdk } } : null;
80
+ },
81
+ missingInitialization: "missing project typescript SDK",
82
+ },
83
+ {
84
+ id: "json",
85
+ extensions: [".json", ".jsonc"],
86
+ rootMarkers: NODE_ROOT_MARKERS,
87
+ command: ["vscode-json-language-server"],
88
+ managedCommand: "vscode-json-language-server",
89
+ args: ["--stdio"],
90
+ },
91
+ {
92
+ id: "html",
93
+ extensions: [".html", ".htm"],
94
+ rootMarkers: NODE_ROOT_MARKERS,
95
+ command: ["vscode-html-language-server"],
96
+ managedCommand: "vscode-html-language-server",
97
+ args: ["--stdio"],
98
+ },
99
+ {
100
+ id: "css",
101
+ extensions: [".css", ".scss", ".sass", ".less"],
102
+ rootMarkers: NODE_ROOT_MARKERS,
103
+ command: ["vscode-css-language-server"],
104
+ managedCommand: "vscode-css-language-server",
105
+ args: ["--stdio"],
106
+ },
107
+ {
108
+ id: "yaml",
109
+ extensions: [".yaml", ".yml"],
110
+ rootMarkers: NODE_ROOT_MARKERS,
111
+ command: ["yaml-language-server"],
112
+ args: ["--stdio"],
113
+ },
114
+ {
115
+ id: "bash",
116
+ extensions: [".sh", ".bash", ".zsh", ".ksh"],
117
+ rootMarkers: [],
118
+ command: ["bash-language-server"],
119
+ args: ["start"],
120
+ },
121
+ {
122
+ id: "lua",
123
+ extensions: [".lua"],
124
+ rootMarkers: [".luarc.json", ".luarc.jsonc", ".luacheckrc", ".stylua.toml", "stylua.toml", "selene.toml", "selene.yml"],
125
+ command: ["lua-language-server"],
126
+ args: [],
127
+ },
128
+ {
129
+ id: "zig",
130
+ extensions: [".zig", ".zon"],
131
+ rootMarkers: ["build.zig"],
132
+ command: ["zls"],
133
+ args: [],
134
+ },
135
+ {
136
+ id: "dart",
137
+ extensions: [".dart"],
138
+ rootMarkers: ["pubspec.yaml", "analysis_options.yaml"],
139
+ command: ["dart"],
140
+ args: ["language-server", "--lsp"],
141
+ },
142
+ {
143
+ id: "php",
144
+ extensions: [".php"],
145
+ rootMarkers: ["composer.json", "composer.lock", ".php-version"],
146
+ command: ["intelephense"],
147
+ args: ["--stdio"],
148
+ initialization: () => ({ telemetry: { enabled: false } }),
149
+ },
150
+ {
151
+ id: "dockerfile",
152
+ extensions: [".dockerfile"],
153
+ filenames: ["dockerfile", "containerfile"],
154
+ rootMarkers: ["Dockerfile", "docker-compose.yml", "docker-compose.yaml", "compose.yml", "compose.yaml"],
155
+ command: ["docker-langserver"],
156
+ managedCommand: "docker-langserver",
157
+ args: ["--stdio"],
158
+ },
159
+ {
160
+ id: "markdown",
161
+ extensions: [".md", ".mdx", ".markdown"],
162
+ rootMarkers: NODE_ROOT_MARKERS,
163
+ command: ["marksman"],
164
+ args: ["server"],
165
+ },
166
+ {
167
+ id: "toml",
168
+ extensions: [".toml"],
169
+ rootMarkers: ["Cargo.toml", "pyproject.toml", "taplo.toml", ".taplo.toml"],
170
+ command: ["taplo"],
171
+ args: ["lsp", "stdio"],
172
+ },
173
+ {
174
+ id: "terraform",
175
+ extensions: [".tf", ".tfvars"],
176
+ rootMarkers: [".terraform", ".terraform.lock.hcl", "terraform.tfstate"],
177
+ command: ["terraform-ls"],
178
+ args: ["serve"],
179
+ },
180
+ {
181
+ id: "prisma",
182
+ extensions: [".prisma"],
183
+ rootMarkers: ["schema.prisma", "prisma/schema.prisma", "prisma"],
184
+ command: ["prisma"],
185
+ args: ["language-server"],
186
+ },
187
+ ];
188
+ }
@@ -1,140 +1,11 @@
1
1
  import { existsSync } from "node:fs";
2
- import { dirname, join, resolve } from "node:path";
2
+ import { basename, dirname, join, resolve } from "node:path";
3
3
  import { createRequire } from "node:module";
4
4
  import { ensureManagedNodeCommand, ensureManagedTypeScript, findManagedTypeScriptSdk, findManagedTypeScriptServer } from "./managed-node-server.mjs";
5
+ import { createLspServerDefinitions } from "./server-definitions.mjs";
5
6
  import { resolveTypeScriptProjectRoot } from "./typescript-project-resolver.mjs";
6
7
 
7
- const NODE_ROOT_MARKERS = ["package-lock.json", "bun.lockb", "bun.lock", "pnpm-lock.yaml", "yarn.lock", "package.json"];
8
-
9
- const LSP_SERVERS = [
10
- {
11
- id: "vue",
12
- extensions: [".vue"],
13
- rootMarkers: NODE_ROOT_MARKERS,
14
- command: ["vue-language-server"],
15
- managedCommand: "vue-language-server",
16
- args: ["--stdio"],
17
- initialization: ({ root, workspaceRoot }) => {
18
- const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
19
- return tsdk ? { typescript: { tsdk } } : null;
20
- },
21
- managedTypeScript: true,
22
- missingInitialization: "missing project typescript SDK",
23
- },
24
- {
25
- id: "typescript",
26
- extensions: [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs", ".mts", ".cts"],
27
- rootMarkers: NODE_ROOT_MARKERS,
28
- projectRoot: resolveTypeScriptProjectRoot,
29
- command: ["typescript-language-server"],
30
- managedCommand: "typescript-language-server",
31
- args: ["--stdio"],
32
- initialization: ({ root, workspaceRoot }) => {
33
- const tsserver = resolveTypeScriptServer({ root, workspaceRoot });
34
- return tsserver ? { tsserver: { path: tsserver } } : null;
35
- },
36
- managedTypeScript: true,
37
- missingInitialization: "missing project typescript/tsserver.js",
38
- },
39
- {
40
- id: "python",
41
- extensions: [".py", ".pyi"],
42
- rootMarkers: ["pyproject.toml", "setup.py", "setup.cfg", "requirements.txt", "Pipfile", "pyrightconfig.json"],
43
- command: ["pyright-langserver"],
44
- args: ["--stdio"],
45
- },
46
- {
47
- id: "go",
48
- extensions: [".go"],
49
- rootMarkers: ["go.work", "go.mod", "go.sum"],
50
- command: ["gopls"],
51
- args: [],
52
- },
53
- {
54
- id: "rust",
55
- extensions: [".rs"],
56
- rootMarkers: ["Cargo.toml", "Cargo.lock"],
57
- command: ["rust-analyzer"],
58
- args: [],
59
- },
60
- {
61
- id: "clangd",
62
- extensions: [".c", ".cpp", ".cc", ".cxx", ".c++", ".h", ".hpp", ".hh", ".hxx", ".h++"],
63
- rootMarkers: ["compile_commands.json", "compile_flags.txt", ".clangd"],
64
- command: ["clangd"],
65
- args: ["--background-index", "--clang-tidy"],
66
- },
67
- {
68
- id: "svelte",
69
- extensions: [".svelte"],
70
- rootMarkers: NODE_ROOT_MARKERS,
71
- command: ["svelteserver", "svelte-language-server"],
72
- args: ["--stdio"],
73
- initialization: () => ({}),
74
- },
75
- {
76
- id: "astro",
77
- extensions: [".astro"],
78
- rootMarkers: NODE_ROOT_MARKERS,
79
- command: ["astro-ls", "@astrojs/language-server"],
80
- args: ["--stdio"],
81
- initialization: ({ root, workspaceRoot }) => {
82
- const tsdk = resolveTypeScriptSdk({ root, workspaceRoot });
83
- return tsdk ? { typescript: { tsdk } } : null;
84
- },
85
- missingInitialization: "missing project typescript SDK",
86
- },
87
- {
88
- id: "yaml",
89
- extensions: [".yaml", ".yml"],
90
- rootMarkers: NODE_ROOT_MARKERS,
91
- command: ["yaml-language-server"],
92
- args: ["--stdio"],
93
- },
94
- {
95
- id: "bash",
96
- extensions: [".sh", ".bash", ".zsh", ".ksh"],
97
- rootMarkers: [],
98
- command: ["bash-language-server"],
99
- args: ["start"],
100
- },
101
- {
102
- id: "lua",
103
- extensions: [".lua"],
104
- rootMarkers: [".luarc.json", ".luarc.jsonc", ".luacheckrc", ".stylua.toml", "stylua.toml", "selene.toml", "selene.yml"],
105
- command: ["lua-language-server"],
106
- args: [],
107
- },
108
- {
109
- id: "zig",
110
- extensions: [".zig", ".zon"],
111
- rootMarkers: ["build.zig"],
112
- command: ["zls"],
113
- args: [],
114
- },
115
- {
116
- id: "dart",
117
- extensions: [".dart"],
118
- rootMarkers: ["pubspec.yaml", "analysis_options.yaml"],
119
- command: ["dart"],
120
- args: ["language-server", "--lsp"],
121
- },
122
- {
123
- id: "php",
124
- extensions: [".php"],
125
- rootMarkers: ["composer.json", "composer.lock", ".php-version"],
126
- command: ["intelephense"],
127
- args: ["--stdio"],
128
- initialization: () => ({ telemetry: { enabled: false } }),
129
- },
130
- {
131
- id: "prisma",
132
- extensions: [".prisma"],
133
- rootMarkers: ["schema.prisma", "prisma/schema.prisma", "prisma"],
134
- command: ["prisma"],
135
- args: ["language-server"],
136
- },
137
- ];
8
+ const LSP_SERVERS = createLspServerDefinitions({ resolveTypeScriptProjectRoot, resolveTypeScriptSdk, resolveTypeScriptServer });
138
9
 
139
10
  export async function resolveLspServer({ filePath, workspaceRoot, onEvent = null } = {}) {
140
11
  const result = await resolveLspServerStatus({ filePath, workspaceRoot, onEvent });
@@ -143,7 +14,7 @@ export async function resolveLspServer({ filePath, workspaceRoot, onEvent = null
143
14
 
144
15
  export async function resolveLspServerStatus({ filePath, workspaceRoot, onEvent = null } = {}) {
145
16
  const ext = extensionOf(filePath);
146
- const def = LSP_SERVERS.find((server) => server.extensions.includes(ext));
17
+ const def = LSP_SERVERS.find((server) => matchesServer(server, filePath, ext));
147
18
  if (!def) return { status: "unsupported", extension: ext };
148
19
 
149
20
  const root = resolveServerRoot(def, { filePath, workspaceRoot });
@@ -165,7 +36,13 @@ export async function resolveLspServerStatus({ filePath, workspaceRoot, onEvent
165
36
  }
166
37
 
167
38
  export function listLspServerDefinitions() {
168
- return LSP_SERVERS.map(({ id, extensions, rootMarkers, command, args }) => ({ id, extensions, rootMarkers, command, args }));
39
+ return LSP_SERVERS.map(({ id, extensions, filenames, rootMarkers, command, args }) => ({ id, extensions, filenames, rootMarkers, command, args }));
40
+ }
41
+
42
+ function matchesServer(server, filePath, ext) {
43
+ if (server.extensions.includes(ext)) return true;
44
+ const name = basename(filePath).toLowerCase();
45
+ return server.filenames?.includes(name) ?? false;
169
46
  }
170
47
 
171
48
  async function resolveCommand(def, { root, workspaceRoot, onEvent }) {
package/src/main.mjs CHANGED
@@ -30,7 +30,7 @@ import { createWebToolsFromConfig } from "./web/tools.mjs";
30
30
  import { createModelContextDumper } from "./debug/model-context-dumper.mjs";
31
31
  import { createLogger, installProcessLogHandlers } from "./debug/logger.mjs";
32
32
  import { defaultProfilePaths, ensureProfileFiles } from "./context/profiles.mjs";
33
- import { runProviderConfigCommand } from "./provider/config-command.mjs";
33
+ import { runProviderCommand } from "./provider/command.mjs";
34
34
  import { runWebSearchConfigCommand } from "./web/config-command.mjs";
35
35
  import { createDesktopTurnNotifier } from "./notification/desktop-notifier.mjs";
36
36
  import { registerSuperGrokOAuthProvider } from "./supergrok/oauth-provider.mjs";
@@ -60,11 +60,12 @@ export async function run(argv) {
60
60
  return 1;
61
61
  }
62
62
  }
63
-
64
- if (args.command?.name === "provider" || args.command?.name === "websearch") {
65
- const command = args.command.name === "provider" ? runProviderConfigCommand : runWebSearchConfigCommand;
66
- if (args.providerConfig) return await command({ homeDir: homedir() });
67
- process.stderr.write(`Usage: march ${args.command.name} --config\n`);
63
+ if (args.command?.name === "provider") {
64
+ return await runProviderCommand(args);
65
+ }
66
+ if (args.command?.name === "websearch") {
67
+ if (args.providerConfig) return await runWebSearchConfigCommand({ homeDir: homedir() });
68
+ process.stderr.write("Usage: march websearch --config\n");
68
69
  return 1;
69
70
  }
70
71
 
@@ -0,0 +1,89 @@
1
+ import { globalConfigJsonPath, readConfigJson, upsertSharedProviderProfile } from "../config/config-json.mjs";
2
+ import { selectWithKeyboard } from "../cli/input/select-with-keyboard.mjs";
3
+ import { hasApiKey, parseProviderShareToken } from "./share-payload.mjs";
4
+
5
+ export async function runProviderAcceptCommand({
6
+ homeDir,
7
+ token,
8
+ input = process.stdin,
9
+ output = process.stdout,
10
+ select = selectWithKeyboard,
11
+ } = {}) {
12
+ if (!token) {
13
+ output.write("Usage: march provider accept <march-provider-v1-token>\n");
14
+ return 1;
15
+ }
16
+
17
+ let payload;
18
+ try {
19
+ payload = parseProviderShareToken(token);
20
+ } catch (error) {
21
+ output.write(`Invalid provider share token: ${error.message}\n`);
22
+ return 1;
23
+ }
24
+
25
+ output.write(formatImportPreview(payload));
26
+ const path = globalConfigJsonPath(homeDir);
27
+ const config = readConfigJson(path);
28
+ const providers = config.providers && typeof config.providers === "object" && !Array.isArray(config.providers) ? config.providers : {};
29
+ if (providers[payload.providerId]) {
30
+ const action = await select({
31
+ input,
32
+ output,
33
+ message: `Provider "${payload.providerId}" already exists`,
34
+ items: [
35
+ { label: "Overwrite existing provider", value: "overwrite" },
36
+ { label: "Cancel", value: "cancel" },
37
+ ],
38
+ });
39
+ if (action !== "overwrite") {
40
+ output.write("Provider import cancelled.\n");
41
+ return 1;
42
+ }
43
+ } else {
44
+ const action = await select({
45
+ input,
46
+ output,
47
+ message: "Import provider?",
48
+ items: [
49
+ { label: "Import", value: "import" },
50
+ { label: "Cancel", value: "cancel" },
51
+ ],
52
+ });
53
+ if (action !== "import") {
54
+ output.write("Provider import cancelled.\n");
55
+ return 1;
56
+ }
57
+ }
58
+
59
+ upsertSharedProviderProfile({ path, id: payload.providerId, provider: payload.provider });
60
+ output.write(`Imported provider: ${payload.providerId}\n`);
61
+ output.write(`Config: ${path}\n`);
62
+ return 0;
63
+ }
64
+
65
+ export function formatImportPreview(payload) {
66
+ const provider = payload.provider;
67
+ const models = Array.isArray(provider.models) ? provider.models : [];
68
+ const lines = [
69
+ "Provider to import:",
70
+ ` Id: ${payload.providerId}`,
71
+ ` Name: ${typeof provider.name === "string" && provider.name ? provider.name : "-"}`,
72
+ ` Type: ${provider.type}`,
73
+ ];
74
+ if (typeof provider.baseUrl === "string" && provider.baseUrl) lines.push(` Base URL: ${provider.baseUrl}`);
75
+ if (typeof provider.api === "string" && provider.api) lines.push(` API: ${provider.api}`);
76
+ if (models.length) {
77
+ lines.push(` Models: ${models.length}`);
78
+ for (const model of models.slice(0, 5)) lines.push(` - ${formatModel(model)}`);
79
+ if (models.length > 5) lines.push(` ... ${models.length - 5} more`);
80
+ }
81
+ lines.push(` API key: ${hasApiKey(provider) ? "included" : "not included"}`);
82
+ lines.push("");
83
+ return `${lines.join("\n")}\n`;
84
+ }
85
+
86
+ function formatModel(model) {
87
+ if (model && typeof model === "object" && !Array.isArray(model)) return model.name || model.id || "<unnamed>";
88
+ return String(model);
89
+ }
@@ -0,0 +1,21 @@
1
+ import { homedir } from "node:os";
2
+ import { runProviderConfigCommand } from "./config-command.mjs";
3
+ import { runProviderShareCommand } from "./share-command.mjs";
4
+ import { runProviderAcceptCommand } from "./accept-command.mjs";
5
+
6
+ export async function runProviderCommand(args, { homeDir = homedir(), stderr = process.stderr } = {}) {
7
+ if (args.providerConfig) return await runProviderConfigCommand({ homeDir });
8
+ if (args.command.args[0] === "share") {
9
+ return await runProviderShareCommand({
10
+ homeDir,
11
+ providerId: args.command.args[1],
12
+ includeKey: args.includeKey,
13
+ profileOnly: args.profileOnly,
14
+ });
15
+ }
16
+ if (args.command.args[0] === "accept") {
17
+ return await runProviderAcceptCommand({ homeDir, token: args.command.args[1] });
18
+ }
19
+ stderr.write("Usage: march provider --config | march provider share [id] | march provider accept <token>\n");
20
+ return 1;
21
+ }