nomarmy 0.1.0-alpha.10 ā 0.1.0-alpha.11
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.
- package/bin/nomarmy.mjs +50 -12
- package/lib/connect.mjs +111 -6
- package/package.json +1 -1
package/bin/nomarmy.mjs
CHANGED
|
@@ -18,7 +18,7 @@ import { buildConfigProposal } from "../lib/propose.mjs";
|
|
|
18
18
|
import { detectHardware } from "../lib/hardware.mjs";
|
|
19
19
|
import { readGGUFMetadata, resolveModelPath, totalSplitBytes } from "../lib/gguf.mjs";
|
|
20
20
|
import { recommend, customRecommendation, evaluateConfig, bytesPerKvElementForCacheTypes, MIN_CONTEXT_PER_NOM } from "../lib/sizing.mjs";
|
|
21
|
-
import { connectClaude, connectCodex, connectCursor, cursorAlreadyConnected, deriveWorkerModelEnv, defaultInstallDir } from "../lib/connect.mjs";
|
|
21
|
+
import { connectClaude, connectCodex, connectCursor, cursorAlreadyConnected, deriveWorkerModelEnv, defaultInstallDir, installMcpCopy, SCOPES, claudeUserScoped, portableServerLaunch } from "../lib/connect.mjs";
|
|
22
22
|
import { compareVersions, readPackageVersion, readInstallVersions, copyIsStale } from "../lib/install-freshness.mjs";
|
|
23
23
|
import { ID_RE, AUTH_ENV_NAME_RE, OPENCLAW_PROVIDER_ID_RE, openclawProviderId, isNativeProviderType } from "../lib/dispatch-schema.mjs";
|
|
24
24
|
import { loadAgents, readAgentsFile, writeAgentsFile, agentsConfigPath, apiAgentAsPoolEntry, describeAgent as describeAgentLabel, agentRunsToolsOnHost, agentProviderId, AGENT_KINDS, API_PROVIDER_TYPES, RESERVED_AGENT_NAMES, BUILTIN_LOCAL_AGENT } from "../lib/agents.mjs";
|
|
@@ -40,10 +40,10 @@ import { SUBSCRIPTION_VENDORS, parseOpenclawVersion, versionAtLeast, parseCatalo
|
|
|
40
40
|
// "already connected, so resync" detection if it has no CLI to probe.
|
|
41
41
|
const KNOWN_TARGETS = ["claude", "codex", "cursor"];
|
|
42
42
|
|
|
43
|
-
function connectTarget(target, { nomarmyRoot, run }) {
|
|
44
|
-
if (target === "claude") return connectClaude({ nomarmyRoot, run });
|
|
45
|
-
if (target === "codex") return connectCodex({ nomarmyRoot, run });
|
|
46
|
-
if (target === "cursor") return connectCursor({ nomarmyRoot, run });
|
|
43
|
+
function connectTarget(target, { nomarmyRoot, run, scope = "user", projectDir = null }) {
|
|
44
|
+
if (target === "claude") return connectClaude({ nomarmyRoot, run, scope, projectDir });
|
|
45
|
+
if (target === "codex") return connectCodex({ nomarmyRoot, run, scope });
|
|
46
|
+
if (target === "cursor") return connectCursor({ nomarmyRoot, run, scope, projectDir });
|
|
47
47
|
throw new Error(`unknown connect target: ${target}`);
|
|
48
48
|
}
|
|
49
49
|
|
|
@@ -214,10 +214,16 @@ Usage: nomarmy <command> [options]
|
|
|
214
214
|
statusline the one-line summary Claude Code's status line shows
|
|
215
215
|
(installed by \`nomarmy connect claude\` when no status
|
|
216
216
|
line is set); reads the session JSON on stdin
|
|
217
|
-
connect [claude] [codex] [cursor]
|
|
217
|
+
connect [claude] [codex] [cursor] [--scope user|local|project]
|
|
218
218
|
(Re-)register the MCP server with one or more coordinators.
|
|
219
219
|
With no target and not --json, prompts an interactive
|
|
220
|
-
multi-select instead.
|
|
220
|
+
multi-select instead. --scope user (default) registers it
|
|
221
|
+
for every project; local for this repository, only you;
|
|
222
|
+
project for this repository, committed for the team
|
|
223
|
+
(.mcp.json or .cursor/mcp.json, running \`nomarmy mcp\`).
|
|
224
|
+
Codex has only the user scope.
|
|
225
|
+
mcp Start nomArmy's MCP server on stdio with this machine's
|
|
226
|
+
settings. What a --scope project registration runs.
|
|
221
227
|
sandbox The Podman VM every sandbox shares (macOS, Windows): its
|
|
222
228
|
memory, disk and images. --memory <GiB> resizes it (stops,
|
|
223
229
|
sets, restarts; refused while jobs run); --prune removes
|
|
@@ -1709,6 +1715,13 @@ async function cmdUpdate() {
|
|
|
1709
1715
|
// this process loaded. Returns the targets reconnected.
|
|
1710
1716
|
function reconnectCoordinators() {
|
|
1711
1717
|
const targets = connectedTargets();
|
|
1718
|
+
// Per-repo registrations run the installed copy (or `nomarmy mcp`), so a
|
|
1719
|
+
// refreshed copy is all they need; re-registering them at user scope would
|
|
1720
|
+
// add nomArmy to every project.
|
|
1721
|
+
if (!targets.length) {
|
|
1722
|
+
execFileSync(process.execPath, [path.join(nomarmyRoot, "bin", "nomarmy.mjs"), "connect", "--copy-only", ...(json ? ["--json"] : [])], { stdio: json ? "ignore" : "inherit" });
|
|
1723
|
+
return [];
|
|
1724
|
+
}
|
|
1712
1725
|
if (targets.length) {
|
|
1713
1726
|
if (!json) console.log(`\nReconnecting ${targets.join(", ")}...`);
|
|
1714
1727
|
execFileSync(process.execPath, [path.join(nomarmyRoot, "bin", "nomarmy.mjs"), "connect", ...targets, ...(json ? ["--json"] : [])], { stdio: json ? "ignore" : "inherit" });
|
|
@@ -1717,7 +1730,7 @@ function reconnectCoordinators() {
|
|
|
1717
1730
|
}
|
|
1718
1731
|
|
|
1719
1732
|
function connectedTargets() {
|
|
1720
|
-
return [commandExists("claude") && "claude", commandExists("codex") && "codex", cursorAlreadyConnected() && "cursor"].filter(Boolean);
|
|
1733
|
+
return [commandExists("claude") && claudeUserScoped() && "claude", commandExists("codex") && "codex", cursorAlreadyConnected() && "cursor"].filter(Boolean);
|
|
1721
1734
|
}
|
|
1722
1735
|
|
|
1723
1736
|
async function updateFromNpm() {
|
|
@@ -1788,7 +1801,20 @@ async function cmdConnect() {
|
|
|
1788
1801
|
// scan the whole argv), and `nomarmy connect --json claude` once broke
|
|
1789
1802
|
// that promise by reading argv[1] directly -- --json landed in target's
|
|
1790
1803
|
// slot instead. Multiple bare tokens are now allowed too, for multi-select.
|
|
1791
|
-
const
|
|
1804
|
+
const flagValues = new Set(["--scope", "--repo"].map((name) => argv.indexOf(name)).filter((i) => i >= 0).map((i) => i + 1));
|
|
1805
|
+
const requested = argv.slice(1).filter((a, i) => !a.startsWith("--") && !flagValues.has(i + 1));
|
|
1806
|
+
const scope = value("scope", "user");
|
|
1807
|
+
if (!SCOPES.includes(scope)) throw new Error(`--scope must be one of ${SCOPES.join(", ")}, got "${scope}".`);
|
|
1808
|
+
let projectDir = null;
|
|
1809
|
+
if (scope !== "user") {
|
|
1810
|
+
try { projectDir = execFileSync("git", ["rev-parse", "--show-toplevel"], { cwd: repoDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim(); }
|
|
1811
|
+
catch { throw new Error(`--scope ${scope} registers nomArmy for one repository, and ${repoDir} isn't inside a git repository. Run it from the repository (or pass --repo <dir>).`); }
|
|
1812
|
+
}
|
|
1813
|
+
if (flag("copy-only")) {
|
|
1814
|
+
// `nomarmy update` with only per-repo registrations: refresh the copy they run.
|
|
1815
|
+
installMcpCopy({ nomarmyRoot, installDir: defaultInstallDir(), run: (cmd, args, opts = {}) => execFileSync(cmd, args, { stdio: json ? "ignore" : "inherit", ...opts }) });
|
|
1816
|
+
return json ? out({ refreshed: defaultInstallDir() }) : console.log(c.green(`ā Refreshed the nomArmy copy in ${defaultInstallDir()}.`));
|
|
1817
|
+
}
|
|
1792
1818
|
let targets;
|
|
1793
1819
|
if (requested.length > 0) {
|
|
1794
1820
|
const unknown = requested.filter((t) => !KNOWN_TARGETS.includes(t));
|
|
@@ -1813,9 +1839,12 @@ async function cmdConnect() {
|
|
|
1813
1839
|
}
|
|
1814
1840
|
try {
|
|
1815
1841
|
if (!json) console.log(c.bold(`\nšŖ Connecting nomArmy to ${target}...`));
|
|
1816
|
-
const result = connectTarget(target, { nomarmyRoot, run });
|
|
1842
|
+
const result = connectTarget(target, { nomarmyRoot, run, scope, projectDir });
|
|
1817
1843
|
results.push({ target, connected: true, ...result });
|
|
1818
|
-
if (!json) console.log(c.green(`ā Registered nomarmy-local-worker with ${target}.`));
|
|
1844
|
+
if (!json) console.log(c.green(`ā Registered nomarmy-local-worker with ${target}${scope === "user" ? "." : scope === "local" ? ` for ${projectDir} only (not committed).` : ` in ${path.relative(projectDir, result.configPath ?? path.join(projectDir, ".mcp.json"))}, for everyone who clones this repository.`}`));
|
|
1845
|
+
if (!json && scope === "local" && result?.excluded?.length) console.log(c.dim(` Kept out of git (.git/info/exclude): ${result.excluded.join(", ")}`));
|
|
1846
|
+
if (!json && scope === "project") console.log(c.dim(` Commit it along with the playbook in ${path.relative(projectDir, result.commands.dir)}. Each teammate needs nomArmy installed and set up (npm install -g nomarmy@alpha, then nomarmy setup); the registration runs \`nomarmy mcp\`, which uses their own settings.`));
|
|
1847
|
+
if (!json && scope !== "user" && target === "claude" && result?.userScoped) console.log(c.yellow(` nomArmy is also registered for all your projects (user scope). To use it only where you register it per repository: claude mcp remove nomarmy-local-worker -s user`));
|
|
1819
1848
|
if (!json && result?.commands?.installed?.length) console.log(c.green(`ā Playbooks: ${result.commands.installed.join(", ")}`) + c.dim(` in ${result.commands.dir} (restart ${target} to pick up a new one)`));
|
|
1820
1849
|
if (!json && result?.notifier?.status === "built") console.log(c.green("ā Notifications: nomArmy.app, with nomArmy's icon") + c.dim(" (macOS asks once whether to allow it)"));
|
|
1821
1850
|
if (!json && result?.notifier?.status === "failed") console.log(c.yellow(`ā Couldn't build nomArmy.app (${result.notifier.reason}); notifications still work, with Script Editor's icon. Xcode's command-line tools provide swiftc: xcode-select --install`));
|
|
@@ -2617,7 +2646,16 @@ async function cmdStatusline() {
|
|
|
2617
2646
|
process.stdout.write(`${statusLineText({ session })}\n`);
|
|
2618
2647
|
}
|
|
2619
2648
|
|
|
2620
|
-
|
|
2649
|
+
// `nomarmy mcp`: what a --scope project registration runs. Nothing goes to
|
|
2650
|
+
// stdout but the server's own protocol.
|
|
2651
|
+
function cmdMcp() {
|
|
2652
|
+
const { serverPath, env } = portableServerLaunch({ nomarmyRoot });
|
|
2653
|
+
const child = spawn(process.execPath, [serverPath], { stdio: "inherit", env });
|
|
2654
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) process.on(signal, () => child.kill(signal));
|
|
2655
|
+
child.on("exit", (code, signal) => { if (signal) process.kill(process.pid, signal); else process.exit(code ?? 1); });
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
const commands = { mcp: cmdMcp, scan: cmdScan, validate: cmdValidate, sizing: cmdSizing, init: cmdInit, setup: cmdSetup, install: cmdInstall, model: cmdModel, agents: cmdAgents, army: cmdArmy, jobs: cmdJobs, statusline: cmdStatusline, health: cmdHealth, config: cmdConfig, update: cmdUpdate, connect: cmdConnect, sandbox: cmdSandbox, start: cmdStart, stop: cmdStop, uninstall: cmdUninstall, help: () => usage(0) };
|
|
2621
2659
|
// doctor command
|
|
2622
2660
|
async function cmdDoctor() {
|
|
2623
2661
|
// Import lazily to avoid circular dependencies
|
package/lib/connect.mjs
CHANGED
|
@@ -261,6 +261,50 @@ export function deriveWorkerModelEnv(nomarmyRoot) {
|
|
|
261
261
|
|
|
262
262
|
const SERVER_NAME = "nomarmy-local-worker";
|
|
263
263
|
|
|
264
|
+
/**
|
|
265
|
+
* Where a coordinator registers nomArmy. user: every project (the default).
|
|
266
|
+
* local: this repository, only you. project: this repository, committed for
|
|
267
|
+
* the team. A committed registration can't carry one machine's install path
|
|
268
|
+
* or settings, so it runs `nomarmy mcp`, which starts nomArmy with each
|
|
269
|
+
* person's own.
|
|
270
|
+
*/
|
|
271
|
+
export const SCOPES = Object.freeze(["user", "local", "project"]);
|
|
272
|
+
export const PORTABLE_SERVER = Object.freeze({ command: "nomarmy", args: ["mcp"] });
|
|
273
|
+
|
|
274
|
+
/** Keep repo-local files out of git without touching the committed .gitignore. */
|
|
275
|
+
export function excludeFromGit(projectDir, relPaths, run = defaultRun) {
|
|
276
|
+
let excludeFile;
|
|
277
|
+
try { excludeFile = String(run("git", ["rev-parse", "--git-path", "info/exclude"], { cwd: projectDir, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" })).trim(); } catch { return []; }
|
|
278
|
+
if (!excludeFile) return [];
|
|
279
|
+
excludeFile = path.resolve(projectDir, excludeFile);
|
|
280
|
+
const existing = fs.existsSync(excludeFile) ? fs.readFileSync(excludeFile, "utf8") : "";
|
|
281
|
+
const lines = new Set(existing.split("\n").map((l) => l.trim()));
|
|
282
|
+
const added = relPaths.map((p) => `/${p}`).filter((p) => !lines.has(p));
|
|
283
|
+
if (added.length) {
|
|
284
|
+
fs.mkdirSync(path.dirname(excludeFile), { recursive: true });
|
|
285
|
+
fs.appendFileSync(excludeFile, `${existing && !existing.endsWith("\n") ? "\n" : ""}# nomArmy, registered for this repository only\n${added.join("\n")}\n`);
|
|
286
|
+
}
|
|
287
|
+
return added;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Undo excludeFromGit for these paths: a project registration is meant to be committed. */
|
|
291
|
+
export function includeInGit(projectDir, relPaths, run = defaultRun) {
|
|
292
|
+
let excludeFile;
|
|
293
|
+
try { excludeFile = path.resolve(projectDir, String(run("git", ["rev-parse", "--git-path", "info/exclude"], { cwd: projectDir, stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" })).trim()); } catch { return []; }
|
|
294
|
+
if (!fs.existsSync(excludeFile)) return [];
|
|
295
|
+
const drop = new Set(relPaths.map((p) => `/${p}`));
|
|
296
|
+
const lines = fs.readFileSync(excludeFile, "utf8").split("\n");
|
|
297
|
+
const removed = lines.filter((l) => drop.has(l.trim()));
|
|
298
|
+
if (removed.length) fs.writeFileSync(excludeFile, lines.filter((l) => !drop.has(l.trim())).join("\n"));
|
|
299
|
+
return removed.map((l) => l.trim());
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function repoScoped(scope, projectDir) {
|
|
303
|
+
if (!SCOPES.includes(scope)) throw new Error(`unknown scope "${scope}"; use one of ${SCOPES.join(", ")}`);
|
|
304
|
+
if (scope !== "user" && !projectDir) throw new Error(`--scope ${scope} registers nomArmy for one repository; run it inside that repository`);
|
|
305
|
+
return scope !== "user";
|
|
306
|
+
}
|
|
307
|
+
|
|
264
308
|
/**
|
|
265
309
|
* Every distinct auth_env name declared across config/providers.yml's
|
|
266
310
|
* pools, mapped to a placeholder value -- NEVER the real credential, which
|
|
@@ -302,7 +346,8 @@ export function derivePoolAuthEnvPlaceholders(configDir = globalConfigDir()) {
|
|
|
302
346
|
/**
|
|
303
347
|
* @param {{ nomarmyRoot: string, installDir?: string, run?: Function, extraEnv?: Record<string,string>, configDir?: string }} input
|
|
304
348
|
*/
|
|
305
|
-
export function connectClaude({ nomarmyRoot, installDir = defaultInstallDir(), run = defaultRun, extraEnv = {}, configDir = globalConfigDir(), commandsDir = defaultCommandDirs().claude, settingsPath = defaultClaudeSettingsPath() }) {
|
|
349
|
+
export function connectClaude({ nomarmyRoot, installDir = defaultInstallDir(), run = defaultRun, extraEnv = {}, configDir = globalConfigDir(), commandsDir = defaultCommandDirs().claude, settingsPath = defaultClaudeSettingsPath(), scope = "user", projectDir = null }) {
|
|
350
|
+
if (repoScoped(scope, projectDir)) return connectClaudeToRepo({ nomarmyRoot, installDir, run, extraEnv, configDir, scope, projectDir });
|
|
306
351
|
installMcpCopy({ nomarmyRoot, installDir, run });
|
|
307
352
|
const commands = installPlaybooks({ nomarmyRoot, target: "claude", dir: commandsDir });
|
|
308
353
|
const notifier = buildNotifierApp({ nomarmyRoot });
|
|
@@ -340,13 +385,62 @@ export function connectClaude({ nomarmyRoot, installDir = defaultInstallDir(), r
|
|
|
340
385
|
run("claude", ["mcp", "add", SERVER_NAME, ...envArgs, "--", "node", serverPath]);
|
|
341
386
|
}
|
|
342
387
|
run("claude", ["mcp", "get", SERVER_NAME]);
|
|
343
|
-
return { installDir, serverPath, preservedEnv: finalEnv, commands, statusLine, notifier };
|
|
388
|
+
return { installDir, serverPath, preservedEnv: finalEnv, commands, statusLine, notifier, scope: "user" };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// Claude Code's local scope (this repo, only you) or project scope (.mcp.json,
|
|
392
|
+
// committed). The /feature playbook goes in the repo's .claude/commands, so it
|
|
393
|
+
// shows up only where nomArmy is registered; no status line, which is a
|
|
394
|
+
// user-wide setting.
|
|
395
|
+
function connectClaudeToRepo({ nomarmyRoot, installDir, run, extraEnv, configDir, scope, projectDir }) {
|
|
396
|
+
installMcpCopy({ nomarmyRoot, installDir, run });
|
|
397
|
+
const commands = installPlaybooks({ nomarmyRoot, target: "claude", dir: path.join(projectDir, ".claude", "commands") });
|
|
398
|
+
const playbookPaths = commands.installed.map((f) => path.relative(projectDir, path.join(commands.dir, f)));
|
|
399
|
+
const excluded = scope === "local" ? excludeFromGit(projectDir, playbookPaths, run) : [];
|
|
400
|
+
if (scope === "project") includeInGit(projectDir, playbookPaths, run);
|
|
401
|
+
const serverPath = path.join(installDir, "mcp", "server.mjs");
|
|
402
|
+
const opts = { cwd: projectDir };
|
|
403
|
+
quietRun(run, "claude", ["mcp", "remove", SERVER_NAME, "--scope", scope], opts);
|
|
404
|
+
let env = {};
|
|
405
|
+
if (scope === "local") {
|
|
406
|
+
env = { ...derivePoolAuthEnvPlaceholders(configDir), ...extraEnv, ...deriveWorkerModelEnv(nomarmyRoot) };
|
|
407
|
+
const envArgs = Object.entries(env).flatMap(([k, v]) => ["-e", `${k}=${v}`]);
|
|
408
|
+
run("claude", ["mcp", "add", "--scope", "local", SERVER_NAME, ...envArgs, "--", "node", serverPath], opts);
|
|
409
|
+
} else {
|
|
410
|
+
run("claude", ["mcp", "add", "--scope", "project", SERVER_NAME, "--", PORTABLE_SERVER.command, ...PORTABLE_SERVER.args], opts);
|
|
411
|
+
}
|
|
412
|
+
run("claude", ["mcp", "get", SERVER_NAME], opts);
|
|
413
|
+
return { installDir, serverPath: scope === "local" ? serverPath : null, preservedEnv: env, commands, excluded, scope, projectDir,
|
|
414
|
+
configPath: scope === "project" ? path.join(projectDir, ".mcp.json") : null, userScoped: claudeUserScoped(run) };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* What `nomarmy mcp` runs: the installed copy when there is one (the code
|
|
419
|
+
* `nomarmy update` keeps current), else this package's own server, with the
|
|
420
|
+
* env connect would register (worker model, api-key placeholders) under
|
|
421
|
+
* whatever the caller already set.
|
|
422
|
+
*/
|
|
423
|
+
export function portableServerLaunch({ nomarmyRoot, installDir = defaultInstallDir(), configDir = globalConfigDir(), env = process.env }) {
|
|
424
|
+
const installed = path.join(installDir, "mcp", "server.mjs");
|
|
425
|
+
const serverPath = fs.existsSync(installed) ? installed : path.join(nomarmyRoot, "mcp", "server.mjs");
|
|
426
|
+
return { serverPath, env: { ...derivePoolAuthEnvPlaceholders(configDir), ...deriveWorkerModelEnv(nomarmyRoot), ...env } };
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** Whether Claude Code also has nomArmy registered for every project. */
|
|
430
|
+
export function claudeUserScoped(run = defaultRun) {
|
|
431
|
+
try {
|
|
432
|
+
const out = String(run("claude", ["mcp", "get", SERVER_NAME], { cwd: os.homedir(), stdio: ["ignore", "pipe", "ignore"], encoding: "utf8" }));
|
|
433
|
+
return /Scope:\s*User/i.test(out);
|
|
434
|
+
} catch { return false; }
|
|
344
435
|
}
|
|
345
436
|
|
|
346
437
|
/**
|
|
347
438
|
* @param {{ nomarmyRoot: string, installDir?: string, run?: Function }} input
|
|
348
439
|
*/
|
|
349
|
-
export function connectCodex({ nomarmyRoot, installDir = defaultInstallDir(), run = defaultRun, configDir = globalConfigDir(), skillsDir = defaultCommandDirs().codex, legacySkillsDir = path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "skills") }) {
|
|
440
|
+
export function connectCodex({ nomarmyRoot, installDir = defaultInstallDir(), run = defaultRun, configDir = globalConfigDir(), skillsDir = defaultCommandDirs().codex, legacySkillsDir = path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "skills"), scope = "user" }) {
|
|
441
|
+
// `codex mcp add` has no scope: Codex registers MCP servers only in
|
|
442
|
+
// ~/.codex/config.toml, for every project.
|
|
443
|
+
if (scope !== "user") throw new Error(`Codex registers MCP servers only for every project (~/.codex/config.toml), so --scope ${scope} isn't available for it; connect Codex without --scope, or use Claude Code or Cursor for a per-repo registration`);
|
|
350
444
|
installMcpCopy({ nomarmyRoot, installDir, run });
|
|
351
445
|
const commands = installPlaybooks({ nomarmyRoot, target: "codex", dir: skillsDir });
|
|
352
446
|
removeLegacyCodexSkill(legacySkillsDir, skillsDir);
|
|
@@ -410,7 +504,13 @@ export function readCursorConfig(configPath) {
|
|
|
410
504
|
/**
|
|
411
505
|
* @param {{ nomarmyRoot: string, installDir?: string, configPath?: string }} input
|
|
412
506
|
*/
|
|
413
|
-
export function connectCursor({ nomarmyRoot, installDir = defaultInstallDir(), configPath = defaultCursorConfigPath(), run = defaultRun, configDir = globalConfigDir(), commandsDir = defaultCommandDirs().cursor }) {
|
|
507
|
+
export function connectCursor({ nomarmyRoot, installDir = defaultInstallDir(), configPath = defaultCursorConfigPath(), run = defaultRun, configDir = globalConfigDir(), commandsDir = defaultCommandDirs().cursor, scope = "user", projectDir = null }) {
|
|
508
|
+
// Cursor reads a project's own .cursor/mcp.json beside the user one. local
|
|
509
|
+
// keeps it out of git; project commits it, with the portable command.
|
|
510
|
+
if (repoScoped(scope, projectDir)) {
|
|
511
|
+
configPath = path.join(projectDir, ".cursor", "mcp.json");
|
|
512
|
+
commandsDir = path.join(projectDir, ".cursor", "commands");
|
|
513
|
+
}
|
|
414
514
|
installMcpCopy({ nomarmyRoot, installDir, run });
|
|
415
515
|
const commands = installPlaybooks({ nomarmyRoot, target: "cursor", dir: commandsDir });
|
|
416
516
|
const notifier = buildNotifierApp({ nomarmyRoot });
|
|
@@ -427,10 +527,15 @@ export function connectCursor({ nomarmyRoot, installDir = defaultInstallDir(), c
|
|
|
427
527
|
// Cursor starts a global MCP server in the home folder, not the open
|
|
428
528
|
// project; ${workspaceFolder} is Cursor's own placeholder for the latter.
|
|
429
529
|
const preservedEnv = { ...derivePoolAuthEnvPlaceholders(configDir), ...((existing && typeof existing.env === "object" && existing.env) || {}), ...deriveWorkerModelEnv(nomarmyRoot), NOMARMY_PROJECT_DIR: "${workspaceFolder}" };
|
|
430
|
-
config.mcpServers[SERVER_NAME] =
|
|
530
|
+
config.mcpServers[SERVER_NAME] = scope === "project"
|
|
531
|
+
? { command: PORTABLE_SERVER.command, args: [...PORTABLE_SERVER.args], env: { NOMARMY_PROJECT_DIR: "${workspaceFolder}" } }
|
|
532
|
+
: { command: "node", args: [serverPath], env: preservedEnv };
|
|
431
533
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
432
534
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + "\n");
|
|
433
|
-
|
|
535
|
+
const repoPaths = scope === "user" ? [] : [path.relative(projectDir, configPath), ...commands.installed.map((f) => path.relative(projectDir, path.join(commands.dir, f)))];
|
|
536
|
+
const excluded = scope === "local" ? excludeFromGit(projectDir, repoPaths, run) : [];
|
|
537
|
+
if (scope === "project") includeInGit(projectDir, repoPaths, run);
|
|
538
|
+
return { installDir, serverPath, configPath, preservedEnv: scope === "project" ? config.mcpServers[SERVER_NAME].env : preservedEnv, commands, notifier, scope, excluded };
|
|
434
539
|
}
|
|
435
540
|
|
|
436
541
|
/** True when Cursor's config file already has a nomArmy entry -- used by
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "Every byte verified: a harness for AI coding workers whose claims are never trusted. Your coding assistant stays in charge while workers implement and test in sandboxes, and nomArmy checks every change before it is committed.",
|
|
4
4
|
"author": "Rayson Technologies",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
|
-
"version": "0.1.0-alpha.
|
|
6
|
+
"version": "0.1.0-alpha.11",
|
|
7
7
|
"private": false,
|
|
8
8
|
"type": "module",
|
|
9
9
|
"engines": {
|