@aiwg/cli 2026.10.1 → 2026.10.3
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/agentic/code/providers/capability-matrix.yaml +270 -0
- package/agentic/code/providers/model-capabilities.v1.json +3091 -24
- package/bin/aiwg.mjs +86 -21
- package/dist/src/a2a/codecs.js +9 -0
- package/dist/src/artifacts/corpus-tools/vision-extract.js +105 -15
- package/dist/src/artifacts/repair.js +9 -4
- package/dist/src/channel/manager.mjs +59 -10
- package/dist/src/cli/child-process-result.js +40 -0
- package/dist/src/cli/env.js +38 -16
- package/dist/src/cli/flag-parsing.js +236 -0
- package/dist/src/cli/handlers/decision.js +210 -4
- package/dist/src/cli/handlers/diagnose.js +9 -2
- package/dist/src/cli/handlers/feedback.js +14 -5
- package/dist/src/cli/handlers/init.js +245 -30
- package/dist/src/cli/handlers/mc.js +23 -0
- package/dist/src/cli/handlers/models.js +69 -6
- package/dist/src/cli/handlers/ralph.js +3 -0
- package/dist/src/cli/handlers/refresh.js +16 -0
- package/dist/src/cli/handlers/regenerate.js +14 -0
- package/dist/src/cli/handlers/runtime-info.js +24 -9
- package/dist/src/cli/handlers/script-runner.js +16 -6
- package/dist/src/cli/handlers/session.js +396 -62
- package/dist/src/cli/handlers/sessions.js +146 -40
- package/dist/src/cli/handlers/setup-manifest.js +439 -39
- package/dist/src/cli/handlers/setup.js +139 -39
- package/dist/src/cli/handlers/subcommands.js +3 -1
- package/dist/src/cli/handlers/team.js +18 -8
- package/dist/src/cli/handlers/use.js +13 -0
- package/dist/src/cli/handlers/utilities.js +106 -23
- package/dist/src/cli/prompt-utils.js +72 -0
- package/dist/src/cli/provider-resolution.js +11 -0
- package/dist/src/config/aiwg-config.js +268 -4
- package/dist/src/config/cli.js +146 -19
- package/dist/src/config/dispatch-thresholds-runtime.mjs +38 -0
- package/dist/src/config/project-artifacts-health.mjs +14 -3
- package/dist/src/config/user-config.js +36 -10
- package/dist/src/decision/adapters/jev.js +75 -27
- package/dist/src/decision/driver.js +3 -1
- package/dist/src/decision/entry.js +38 -6
- package/dist/src/decision/heldout/collector.js +10 -1
- package/dist/src/decision/index.js +1 -0
- package/dist/src/decision/interop.js +60 -8
- package/dist/src/decision/ledger.js +839 -0
- package/dist/src/decision/validate.js +6 -3
- package/dist/src/effects/dispatch-verifier.js +252 -0
- package/dist/src/effects/index.js +1 -0
- package/dist/src/features/installer.js +2 -2
- package/dist/src/flow/graph-metadata.js +5 -0
- package/dist/src/gates/providers/loader.js +3 -1
- package/dist/src/installation/manager-command.mjs +1 -1
- package/dist/src/installation/manager.mjs +17 -2
- package/dist/src/lint/runner.js +2 -2
- package/dist/src/mcp/adapters/codex-runtime.js +360 -56
- package/dist/src/mcp/cli.mjs +587 -77
- package/dist/src/mcp/config-file.mjs +279 -0
- package/dist/src/mcp/config-layers.mjs +348 -0
- package/dist/src/mcp/credentials.mjs +484 -0
- package/dist/src/mcp/grok-build-config.mjs +16 -26
- package/dist/src/mcp/helpers.mjs +106 -2
- package/dist/src/mcp/omp-config.mjs +50 -29
- package/dist/src/mcp/profiles.js +97 -23
- package/dist/src/mcp/profiles.mjs +99 -27
- package/dist/src/mcp/registry.js +264 -57
- package/dist/src/mcp/registry.mjs +198 -45
- package/dist/src/mcp/server.mjs +403 -20
- package/dist/src/mcp/toml-string.mjs +9 -0
- package/dist/src/mcp/toml-strip-mcp.mjs +135 -0
- package/dist/src/mcp/tool-filters.mjs +357 -0
- package/dist/src/mcp/tools/agentic-sandbox.mjs +3 -1
- package/dist/src/mcp/tools/decision.mjs +21 -3
- package/dist/src/mcp/tools/orchestration.mjs +38 -0
- package/dist/src/mcp/tools/subsystems.mjs +39 -8
- package/dist/src/mcp/tools/worker-invoke.mjs +1539 -0
- package/dist/src/mcp/tools/worker-shell-control.mjs +258 -0
- package/dist/src/mcp/tools/worker-stdin-runtime.mjs +79 -0
- package/dist/src/memory/project-registry.js +2 -2
- package/dist/src/mission-protocol/codecs.js +639 -1
- package/dist/src/mission-protocol/types.js +110 -0
- package/dist/src/mission-protocol/worker-status.js +2 -0
- package/dist/src/models/config-loader.js +70 -13
- package/dist/src/models/grok-build-models.js +93 -41
- package/dist/src/models/model-capabilities.v1.json +3091 -24
- package/dist/src/models/model-discovery.js +862 -63
- package/dist/src/models/provider-policy.js +625 -11
- package/dist/src/models/qualification-digest.mjs +17 -0
- package/dist/src/models/resolver.js +259 -0
- package/dist/src/models/worker-direct-readiness.js +529 -0
- package/dist/src/models/worker-dispatch-offer.js +169 -0
- package/dist/src/models/worker-health-readiness.js +131 -0
- package/dist/src/models/worker-installation.mjs +879 -0
- package/dist/src/models/wrapper-route.js +18 -0
- package/dist/src/ops/registry.js +9 -14
- package/dist/src/plugins/standalone-packager.js +4 -3
- package/dist/src/process/bounded-child-process.mjs +387 -0
- package/dist/src/process/cli-command.mjs +75 -0
- package/dist/src/providers/capability-matrix.js +8 -0
- package/dist/src/providers/capability-matrix.yaml +270 -0
- package/dist/src/providers/grok-build-headless.js +43 -21
- package/dist/src/providers/provider-definitions.js +9 -1
- package/dist/src/providers/provider-definitions.mjs +9 -2
- package/dist/src/serve/executor-registry.js +16 -24
- package/dist/src/serve/fleet-mission-conductor.js +9 -0
- package/dist/src/serve/mission-conductor.js +9 -0
- package/dist/src/serve/shared-host-scheduler.js +2 -0
- package/dist/src/serve/worker-invocation.js +538 -0
- package/dist/src/serve/worker-stdin-frame.mjs +107 -0
- package/dist/src/sessions/adapter-registry.js +45 -0
- package/dist/src/sessions/adapters/generic.js +37 -3
- package/dist/src/sessions/adapters/grokbot-locator.js +111 -0
- package/dist/src/sessions/adapters/grokbot.js +423 -24
- package/dist/src/sessions/adapters/muse-workspace-scan.js +108 -0
- package/dist/src/sessions/adapters/muse.js +68 -29
- package/dist/src/sessions/adapters/pi.js +1 -0
- package/dist/src/sessions/batch-contracts.js +13 -1
- package/dist/src/sessions/batch-import.js +23 -42
- package/dist/src/sessions/candidates.js +47 -11
- package/dist/src/sessions/contracts.js +35 -0
- package/dist/src/sessions/coverage-preflight.js +134 -0
- package/dist/src/sessions/discovery.js +3 -0
- package/dist/src/sessions/grok-build-cli.js +67 -5
- package/dist/src/sessions/importer.js +24 -8
- package/dist/src/sessions/index.js +2 -0
- package/dist/src/sessions/repository.js +54 -13
- package/dist/src/sessions/workspace-discovery.js +99 -37
- package/dist/src/skills/adapters/clawhub.js +10 -8
- package/dist/src/skills/adapters/openclaw.js +10 -7
- package/dist/src/smiths/context-pipeline/parallelism-section.js +192 -3
- package/dist/src/smiths/context-pipeline/workspace-context.js +116 -9
- package/dist/src/smiths/hook-bridge/codex-translator.js +6 -5
- package/dist/src/smiths/mcpsmith/example.js +1 -1
- package/dist/src/smiths/toolsmith/runtime-discovery.mjs +10 -14
- package/dist/src/storage/backends/logseq.js +68 -7
- package/dist/src/storage/fortemi-qualification-receipt.js +2 -2
- package/dist/src/testing/generators/test-code-generator.js +57 -30
- package/dist/src/uhp/client.js +11 -1
- package/dist/src/uhp/mission.js +5 -0
- package/dist/src/update/checker.mjs +5 -5
- package/dist/src/update/notifier.mjs +54 -2
- package/dist/src/writing/fidelity.js +18 -1
- package/package.json +18 -1
- package/schemas/mission-v1.schema.json +2429 -0
- package/schemas/models/model-catalog.v1.schema.json +301 -0
- package/schemas/models/model-config-compatibility.v1.schema.json +281 -0
- package/schemas/models/model-policy.v1.schema.json +14 -0
- package/schemas/models/provider-model-capabilities.v1.schema.json +1066 -0
- package/tools/_resolve-impl.mjs +39 -0
- package/tools/agents/deploy-agents.mjs +15 -2
- package/tools/agents/providers/base.mjs +49 -32
- package/tools/agents/providers/grokbot.mjs +11 -12
- package/tools/agents/providers/hermes.mjs +6 -3
- package/tools/agents/providers/omp.mjs +5 -2
- package/tools/agents/providers/openclaw-translator.mjs +19 -5
- package/tools/providers/deepseek-harness-transport.mjs +358 -0
- package/tools/providers/omp-transport.mjs +192 -0
- package/tools/ralph-external/lib/claude-adapter.mjs +389 -0
- package/tools/ralph-external/lib/codex-adapter.mjs +432 -0
- package/tools/ralph-external/lib/deepseek-harness-adapter.mjs +397 -0
- package/tools/ralph-external/lib/factory-adapter.mjs +503 -0
- package/tools/ralph-external/lib/muse-adapter.mjs +364 -0
- package/tools/ralph-external/lib/omp-adapter.mjs +315 -0
- package/tools/ralph-external/lib/opencode-adapter.mjs +575 -0
- package/tools/ralph-external/lib/openhuman-adapter.mjs +75 -0
- package/tools/ralph-external/lib/pi-adapter.mjs +333 -0
- package/tools/ralph-external/lib/provider-adapter.mjs +690 -0
- package/tools/ralph-external/lib/worker-health.mjs +439 -0
- package/vscode-extension/schemas/aiwg.config.v1.json +1953 -0
package/dist/src/mcp/cli.mjs
CHANGED
|
@@ -7,20 +7,145 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { startServer, createServer } from './server.mjs';
|
|
10
|
+
import { assertConfigDestination, assertConfigObject, assertProjectCredentials, isPathInsideDir, classifyMcpDestination, projectCredentialRemedy, writeConfigAtomic, writeConfigTransaction } from './config-file.mjs';
|
|
10
11
|
import fs from 'fs/promises';
|
|
11
12
|
import path from 'path';
|
|
12
|
-
import
|
|
13
|
+
import os from 'os';
|
|
14
|
+
import { existsSync as nodeExistsSync, readFileSync as nodeReadFileSync } from 'fs';
|
|
15
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
13
16
|
import {
|
|
14
17
|
McpServerRegistry,
|
|
18
|
+
buildServerConfig,
|
|
15
19
|
injectServers,
|
|
16
20
|
SUPPORTED_PROVIDERS,
|
|
17
21
|
getProviderConfigPath,
|
|
18
22
|
} from './registry.mjs';
|
|
19
23
|
import { McpProfileRegistry } from './profiles.mjs';
|
|
24
|
+
import {
|
|
25
|
+
applyJsonToolFilterPlan,
|
|
26
|
+
hasToolFilters,
|
|
27
|
+
planToolFilters,
|
|
28
|
+
prepareClaudePermissions,
|
|
29
|
+
resolveToolFilters,
|
|
30
|
+
} from './tool-filters.mjs';
|
|
31
|
+
import { assertCredentialPolicy, CREDENTIAL_POLICIES, installPolicyCheckedServer, redactCredentialArgs, redactCredentialText, redactCredentialRecord, resolveCredentialPolicy } from './credentials.mjs';
|
|
20
32
|
import { getMcpInjectionDefinition } from '../providers/provider-definitions.mjs';
|
|
21
33
|
import { manageOmpMcp } from './omp-config.mjs';
|
|
22
34
|
import { unmanageGrokBuildMcp } from './grok-build-config.mjs';
|
|
23
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Thrown when a recognized flag is present but has no usable value
|
|
38
|
+
* following it (#3062 review item 11). Mirrors `src/cli/flag-parsing.ts`'s
|
|
39
|
+
* `MissingFlagValueError`/`requireFlagValue`, duplicated in plain JS rather
|
|
40
|
+
* than imported: this file is exercised both compiled (`dist/src/mcp/
|
|
41
|
+
* cli.mjs`, where a `../cli/flag-parsing.js` sibling import would resolve)
|
|
42
|
+
* and directly from source in tests (`src/mcp/cli.mjs`, where it would
|
|
43
|
+
* not — there is no compiled `.js` sibling under `src/` itself).
|
|
44
|
+
*/
|
|
45
|
+
class MissingFlagValueError extends Error {
|
|
46
|
+
constructor(flagName) {
|
|
47
|
+
super(`${flagName} requires a value`);
|
|
48
|
+
this.name = 'MissingFlagValueError';
|
|
49
|
+
this.flagName = flagName;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Single-pass, every-occurrence-validated flag lookup (#3062 second
|
|
55
|
+
* review, finding 4 — mirrors `src/cli/flag-parsing.ts`'s
|
|
56
|
+
* `scanFlagOccurrences`/`requireFlagValue`, duplicated here for the same
|
|
57
|
+
* source/dist reason as the class above): stops at a bare `--`
|
|
58
|
+
* terminator; accepts `--flag value` and `--flag=value`; a space-
|
|
59
|
+
* separated value starting with `--` is treated as missing (use `=` for a
|
|
60
|
+
* value that must start with `--`); every occurrence is checked, not just
|
|
61
|
+
* the first; `options.rejectEmpty` additionally rejects an empty-string
|
|
62
|
+
* value. `options.precedence` picks which occurrence wins when the flag
|
|
63
|
+
* repeats — `'first'` (default) here matches this file's own pre-existing
|
|
64
|
+
* `parseFlag()` convention (first match wins).
|
|
65
|
+
*/
|
|
66
|
+
function requireFlagValue(args, options, ...names) {
|
|
67
|
+
const occurrences = [];
|
|
68
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
69
|
+
const token = args[i];
|
|
70
|
+
if (token === undefined) continue;
|
|
71
|
+
if (token === '--') break;
|
|
72
|
+
|
|
73
|
+
if (token.startsWith('--')) {
|
|
74
|
+
const eq = token.indexOf('=');
|
|
75
|
+
if (eq !== -1) {
|
|
76
|
+
const flag = token.slice(0, eq);
|
|
77
|
+
const value = token.slice(eq + 1);
|
|
78
|
+
if (names.includes(flag)) {
|
|
79
|
+
if (options.rejectEmpty && value === '') throw new MissingFlagValueError(flag);
|
|
80
|
+
occurrences.push(value);
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (names.includes(token)) {
|
|
87
|
+
const next = args[i + 1];
|
|
88
|
+
if (next === undefined || next.startsWith('--')) throw new MissingFlagValueError(token);
|
|
89
|
+
if (options.rejectEmpty && next === '') throw new MissingFlagValueError(token);
|
|
90
|
+
occurrences.push(next);
|
|
91
|
+
i += 1;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (occurrences.length === 0) return undefined;
|
|
95
|
+
return options.precedence === 'last' ? occurrences[occurrences.length - 1] : occurrences[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Find the AIWG package root by walking up from THIS file's own location
|
|
100
|
+
* (never from `AIWG_ROOT`/`process.cwd()`, which name the *served project*,
|
|
101
|
+
* not necessarily where this module's own package lives). Depth-independent
|
|
102
|
+
* on purpose: this file ships at `src/mcp/cli.mjs` in a dev checkout and at
|
|
103
|
+
* `dist/src/mcp/cli.mjs` in the published package. Mirrors
|
|
104
|
+
* `src/mcp/tools/worker-invoke.mjs`'s own `findOwnPackageRoot` (and
|
|
105
|
+
* `tools/_resolve-impl.mjs`'s `findPackageRoot`).
|
|
106
|
+
*/
|
|
107
|
+
function findOwnPackageRoot() {
|
|
108
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
109
|
+
for (let i = 0; i < 12; i += 1) {
|
|
110
|
+
const pkgPath = path.join(dir, 'package.json');
|
|
111
|
+
if (nodeExistsSync(pkgPath)) {
|
|
112
|
+
try {
|
|
113
|
+
const pkg = JSON.parse(nodeReadFileSync(pkgPath, 'utf8'));
|
|
114
|
+
if (pkg.name === 'aiwg' || pkg.name === '@aiwg/cli') return dir;
|
|
115
|
+
} catch { /* keep walking */ }
|
|
116
|
+
}
|
|
117
|
+
const parent = path.dirname(dir);
|
|
118
|
+
if (parent === dir) break;
|
|
119
|
+
dir = parent;
|
|
120
|
+
}
|
|
121
|
+
throw new Error('Could not locate the AIWG package root from mcp/cli.mjs');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Load the compiled `loginInProfile()` adapter (#3062 second review,
|
|
126
|
+
* finding 3). The first round routed this through `tools/_resolve-impl.mjs`,
|
|
127
|
+
* which falls back to importing the TypeScript *source* when no compiled
|
|
128
|
+
* `dist/src/` exists — but that source module's own internal imports
|
|
129
|
+
* (e.g. `../../cli/env.js`) have no `.js` sibling under `src/` either, so
|
|
130
|
+
* the fallback itself fails deep inside the adapter with an unrelated-
|
|
131
|
+
* looking `ERR_MODULE_NOT_FOUND`, on every Node version, not just ones
|
|
132
|
+
* without native TypeScript support. This loads the compiled module
|
|
133
|
+
* directly — never TypeScript source — and fails fast with one actionable
|
|
134
|
+
* line when it is missing, instead of attempting (and failing inside) a
|
|
135
|
+
* source fallback.
|
|
136
|
+
*/
|
|
137
|
+
async function loadCodexRuntimeAdapter() {
|
|
138
|
+
const root = findOwnPackageRoot();
|
|
139
|
+
const compiledPath = path.join(root, 'dist', 'src', 'mcp', 'adapters', 'codex-runtime.js');
|
|
140
|
+
if (!nodeExistsSync(compiledPath)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'aiwg mcp profile login requires the built CLI (missing dist/src/mcp/adapters/codex-runtime.js). ' +
|
|
143
|
+
'Use an installed `aiwg` for this command; a source checkout must be built first (see docs/contributing).',
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
return import(pathToFileURL(compiledPath).href);
|
|
147
|
+
}
|
|
148
|
+
|
|
24
149
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
25
150
|
|
|
26
151
|
/**
|
|
@@ -42,6 +167,8 @@ Usage:
|
|
|
42
167
|
aiwg mcp inject [opts] Inject servers into provider configs
|
|
43
168
|
aiwg mcp uninject [opts] Remove unchanged AIWG-owned OMP server entries
|
|
44
169
|
aiwg mcp profile <sub> Manage MCP profiles (named server subsets)
|
|
170
|
+
aiwg mcp credential-policy [literal|references|none]
|
|
171
|
+
Show or set the registry's default credential policy
|
|
45
172
|
|
|
46
173
|
Server Options (for add/update):
|
|
47
174
|
--url <url> Server URL (for http/sse types)
|
|
@@ -52,6 +179,8 @@ Server Options (for add/update):
|
|
|
52
179
|
--headers <K=V,...> HTTP headers (comma-separated K=V pairs)
|
|
53
180
|
--header-env <K=ENV,...>
|
|
54
181
|
Resolve HTTP header values from environment variables
|
|
182
|
+
--env-from <K=ENV,...>
|
|
183
|
+
Resolve stdio server variables from environment variables
|
|
55
184
|
--description <text> Optional description
|
|
56
185
|
|
|
57
186
|
Inject Options:
|
|
@@ -60,6 +189,10 @@ Inject Options:
|
|
|
60
189
|
--all Inject into all previously configured providers
|
|
61
190
|
--servers <a,b,...> Only inject specific servers (comma-separated names)
|
|
62
191
|
--dry-run Show what would change without writing
|
|
192
|
+
--strict-credentials Refuse servers with literal env/headers values; render
|
|
193
|
+
only --header-env/--env-from references
|
|
194
|
+
--no-credentials Refuse servers with any credential-bearing field,
|
|
195
|
+
references included
|
|
63
196
|
|
|
64
197
|
Serve Options:
|
|
65
198
|
--transport <type> Transport type: stdio (default), http
|
|
@@ -70,6 +203,8 @@ Examples:
|
|
|
70
203
|
aiwg mcp add fortemi --url https://memory.s9.internal/mcp --type http
|
|
71
204
|
aiwg mcp add fortemi-enterprise --url https://memory.example.internal/mcp --type http \
|
|
72
205
|
--header-env Authorization=AIWG_FORTEMI_TOKEN
|
|
206
|
+
aiwg mcp add github --type stdio --command github-mcp-server \
|
|
207
|
+
--env-from GITHUB_PERSONAL_ACCESS_TOKEN=GITHUB_TOKEN
|
|
73
208
|
aiwg mcp add gitea --url https://mcp-gitea.integrolabs.net/mcp
|
|
74
209
|
aiwg mcp add mytools --type stdio --command npx --args mcp-server-mytools
|
|
75
210
|
|
|
@@ -96,20 +231,20 @@ Examples:
|
|
|
96
231
|
/**
|
|
97
232
|
* Generate MCP client configuration
|
|
98
233
|
*/
|
|
99
|
-
async function generateConfig(target, projectDir = '.') {
|
|
234
|
+
async function generateConfig(target, projectDir = '.', scope = 'project') {
|
|
100
235
|
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
101
236
|
|
|
102
237
|
const configs = {
|
|
103
238
|
claude: {
|
|
104
|
-
path: path.join(
|
|
239
|
+
path: scope === 'user' ? path.join(homeDir, '.claude.json') : path.join(projectDir, '.mcp.json'),
|
|
240
|
+
userScope: scope === 'user',
|
|
105
241
|
content: {
|
|
106
242
|
mcpServers: {
|
|
107
243
|
aiwg: {
|
|
108
244
|
command: 'aiwg',
|
|
109
245
|
args: ['mcp', 'serve'],
|
|
110
|
-
env
|
|
111
|
-
AIWG_ROOT: process.env.AIWG_ROOT
|
|
112
|
-
}
|
|
246
|
+
...(scope === 'user' && process.env.AIWG_ROOT
|
|
247
|
+
? { env: { AIWG_ROOT: process.env.AIWG_ROOT } } : {})
|
|
113
248
|
}
|
|
114
249
|
}
|
|
115
250
|
}
|
|
@@ -133,6 +268,7 @@ async function generateConfig(target, projectDir = '.') {
|
|
|
133
268
|
})
|
|
134
269
|
},
|
|
135
270
|
factory: {
|
|
271
|
+
userScope: projectDir === '.' || projectDir === 'global',
|
|
136
272
|
// Factory stores MCP config at user level in ~/.factory/mcp.json
|
|
137
273
|
// or project level in .factory/mcp.json
|
|
138
274
|
path: projectDir === '.' || projectDir === 'global'
|
|
@@ -158,7 +294,8 @@ async function generateConfig(target, projectDir = '.') {
|
|
|
158
294
|
},
|
|
159
295
|
codex: {
|
|
160
296
|
// Codex stores config in ~/.codex/config.toml (TOML format)
|
|
161
|
-
path: path.join(homeDir, '.codex
|
|
297
|
+
path: path.resolve(process.env.CODEX_HOME || path.join(homeDir, '.codex'), 'config.toml'),
|
|
298
|
+
userScope: true,
|
|
162
299
|
// We generate TOML snippet to append, not JSON
|
|
163
300
|
content: null,
|
|
164
301
|
toml: `
|
|
@@ -185,8 +322,8 @@ enabled_tools = [
|
|
|
185
322
|
let existing = '';
|
|
186
323
|
try {
|
|
187
324
|
existing = await fs.readFile(configPath, 'utf-8');
|
|
188
|
-
} catch {
|
|
189
|
-
|
|
325
|
+
} catch (error) {
|
|
326
|
+
if (error.code !== 'ENOENT') throw error;
|
|
190
327
|
}
|
|
191
328
|
|
|
192
329
|
// Check if AIWG MCP already configured
|
|
@@ -198,8 +335,7 @@ enabled_tools = [
|
|
|
198
335
|
// Append TOML config
|
|
199
336
|
const updated = existing.trimEnd() + '\n' + tomlContent.trim() + '\n';
|
|
200
337
|
|
|
201
|
-
await
|
|
202
|
-
await fs.writeFile(configPath, updated);
|
|
338
|
+
await writeConfigAtomic(configPath, updated, { userScope: true });
|
|
203
339
|
console.log(`MCP configuration appended to: ${configPath}`);
|
|
204
340
|
console.log(`\nTo use AIWG MCP server with Codex:`);
|
|
205
341
|
console.log(` 1. Restart Codex CLI`);
|
|
@@ -209,12 +345,14 @@ enabled_tools = [
|
|
|
209
345
|
},
|
|
210
346
|
openai: {
|
|
211
347
|
// Alias for codex
|
|
212
|
-
path: path.join(homeDir, '.codex
|
|
348
|
+
path: path.resolve(process.env.CODEX_HOME || path.join(homeDir, '.codex'), 'config.toml'),
|
|
349
|
+
userScope: true,
|
|
213
350
|
alias: 'codex'
|
|
214
351
|
},
|
|
215
352
|
windsurf: {
|
|
216
353
|
// Windsurf stores MCP config at ~/.codeium/windsurf/mcp_config.json
|
|
217
354
|
path: path.join(homeDir, '.codeium/windsurf/mcp_config.json'),
|
|
355
|
+
userScope: true,
|
|
218
356
|
content: {
|
|
219
357
|
mcpServers: {
|
|
220
358
|
aiwg: {
|
|
@@ -312,17 +450,21 @@ enabled_tools = [
|
|
|
312
450
|
|
|
313
451
|
// Find existing config
|
|
314
452
|
for (const loc of locations) {
|
|
453
|
+
await assertConfigDestination(loc, projectRoot);
|
|
315
454
|
try {
|
|
316
455
|
const rawContent = await fs.readFile(loc, 'utf-8');
|
|
317
|
-
// Strip JSONC comments
|
|
318
|
-
const jsonContent = rawContent
|
|
319
|
-
|
|
320
|
-
.
|
|
456
|
+
// Strip JSONC comments while preserving strings such as URLs.
|
|
457
|
+
const jsonContent = rawContent.replace(
|
|
458
|
+
/"(?:\\.|[^"\\])*"|\/\/[^\r\n]*|\/\*[\s\S]*?\*\//g,
|
|
459
|
+
match => match.startsWith('"') ? match : ' ',
|
|
460
|
+
);
|
|
321
461
|
existing = JSON.parse(jsonContent);
|
|
462
|
+
assertConfigObject(existing, 'mcp');
|
|
322
463
|
targetPath = loc;
|
|
323
464
|
break;
|
|
324
|
-
} catch {
|
|
325
|
-
|
|
465
|
+
} catch (error) {
|
|
466
|
+
if (error instanceof SyntaxError) throw new Error(`Refusing to overwrite malformed MCP config ${loc}: invalid JSON`);
|
|
467
|
+
if (error.code !== 'ENOENT') throw error;
|
|
326
468
|
}
|
|
327
469
|
}
|
|
328
470
|
|
|
@@ -335,8 +477,7 @@ enabled_tools = [
|
|
|
335
477
|
// Merge configuration
|
|
336
478
|
const merged = mergeFunc(existing, content);
|
|
337
479
|
|
|
338
|
-
await
|
|
339
|
-
await fs.writeFile(targetPath, JSON.stringify(merged, null, 2));
|
|
480
|
+
await writeConfigAtomic(targetPath, JSON.stringify(merged, null, 2), { userScope, projectRoot });
|
|
340
481
|
console.log(`MCP configuration written to: ${targetPath}`);
|
|
341
482
|
console.log(`\nTo use AIWG MCP server with OpenCode:`);
|
|
342
483
|
console.log(` 1. Restart OpenCode`);
|
|
@@ -358,21 +499,61 @@ enabled_tools = [
|
|
|
358
499
|
config = configs[config.alias];
|
|
359
500
|
}
|
|
360
501
|
|
|
502
|
+
// Destination-derived, not scope-derived (and never from a static
|
|
503
|
+
// `config.userScope` flag): a target whose resolved path still lands inside
|
|
504
|
+
// the project directory gets project containment, whatever scope was
|
|
505
|
+
// requested. Providers with no real user-scope destination (Cursor,
|
|
506
|
+
// opencode) refuse `--scope user` instead of silently writing the project
|
|
507
|
+
// file. `config.path` is null only for providers with no config file at all
|
|
508
|
+
// (Warp, UI-configured), which never reach a destination check.
|
|
509
|
+
const projectBase = target === 'opencode' && projectDir === 'global' ? process.cwd() : projectDir;
|
|
510
|
+
const { userScope, projectRoot } = config.path
|
|
511
|
+
? classifyMcpDestination(target, scope, config.path, projectBase)
|
|
512
|
+
: { userScope: Boolean(config.userScope), projectRoot: undefined };
|
|
513
|
+
if (config.path) await assertConfigDestination(config.path, projectRoot);
|
|
514
|
+
|
|
361
515
|
// Handle custom handler (for TOML configs like Codex, or OpenCode JSON)
|
|
362
516
|
if (config.handler) {
|
|
363
517
|
return await config.handler(config.path, config.toml, config.content, config.merge);
|
|
364
518
|
}
|
|
365
519
|
|
|
366
|
-
// Ensure directory exists
|
|
367
|
-
await fs.mkdir(path.dirname(config.path), { recursive: true });
|
|
368
|
-
|
|
369
520
|
// Check if file exists and merge
|
|
370
521
|
let existing = {};
|
|
371
522
|
try {
|
|
372
523
|
const content = await fs.readFile(config.path, 'utf-8');
|
|
373
524
|
existing = JSON.parse(content);
|
|
374
|
-
} catch {
|
|
375
|
-
|
|
525
|
+
} catch (error) {
|
|
526
|
+
if (error instanceof SyntaxError) throw new Error(`Refusing to overwrite malformed MCP config ${config.path}: invalid JSON`);
|
|
527
|
+
if (error.code !== 'ENOENT') throw error;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
const installServerMapKey = Object.keys(config.content)[0];
|
|
531
|
+
assertConfigObject(existing, installServerMapKey);
|
|
532
|
+
|
|
533
|
+
// Install-generated server definitions go through the same resolved
|
|
534
|
+
// credential policy as `aiwg mcp inject`, before any write. AIWG_ROOT (the
|
|
535
|
+
// only literal env value any install target's fixed content can ever
|
|
536
|
+
// carry, and only on the built-in "aiwg" entry) is a named, narrow
|
|
537
|
+
// exemption - installPolicyCheckedServer/INSTALL_POLICY_EXEMPT_ENV - not
|
|
538
|
+
// an unexamined bypass: any other literal env/header is still refused.
|
|
539
|
+
const installRegistry = new McpServerRegistry();
|
|
540
|
+
assertCredentialPolicy(
|
|
541
|
+
Object.entries(config.content[installServerMapKey] || {})
|
|
542
|
+
.map(([name, server]) => installPolicyCheckedServer(name, server)),
|
|
543
|
+
resolveCredentialPolicy({
|
|
544
|
+
registryPolicy: await installRegistry.getCredentialPolicy(),
|
|
545
|
+
floorPolicy: await installRegistry.getCredentialPolicyFloor(),
|
|
546
|
+
floorSource: await installRegistry.getCredentialPolicyFloorSource(),
|
|
547
|
+
}),
|
|
548
|
+
);
|
|
549
|
+
|
|
550
|
+
// Only the claude target's install content can ever embed a literal credential
|
|
551
|
+
// (`AIWG_ROOT`, gated to `scope === 'user'` above, and exempted above); every
|
|
552
|
+
// other target's `configs[...]` entry above is a fixed `aiwg mcp serve`
|
|
553
|
+
// stdio/TOML stanza with no env/headers field, so there is nothing further
|
|
554
|
+
// for assertProjectCredentials to check there.
|
|
555
|
+
if (target === 'claude' && scope !== 'user') {
|
|
556
|
+
assertProjectCredentials(Object.entries(config.content.mcpServers).map(([name, server]) => ({ name, ...server })), config.path);
|
|
376
557
|
}
|
|
377
558
|
|
|
378
559
|
// Merge configuration using custom merge function if available
|
|
@@ -387,7 +568,7 @@ enabled_tools = [
|
|
|
387
568
|
}
|
|
388
569
|
};
|
|
389
570
|
|
|
390
|
-
await
|
|
571
|
+
await writeConfigAtomic(config.path, JSON.stringify(merged, null, 2), { userScope, projectRoot });
|
|
391
572
|
console.log(`MCP configuration written to: ${config.path}`);
|
|
392
573
|
console.log(`\nTo use AIWG MCP server with ${target}:`);
|
|
393
574
|
console.log(` 1. Restart ${target}`);
|
|
@@ -477,6 +658,16 @@ function parseFlag(args, flag) {
|
|
|
477
658
|
return args[idx + 1];
|
|
478
659
|
}
|
|
479
660
|
|
|
661
|
+
/** Skip option values when locating a server name, regardless of flag order. */
|
|
662
|
+
function serverName(args) {
|
|
663
|
+
const valueFlags = new Set(['--type', '--url', '--command', '--args', '--env',
|
|
664
|
+
'--headers', '--header-env', '--env-from', '--description']);
|
|
665
|
+
for (let i = 0; i < args.length; i++) {
|
|
666
|
+
if (valueFlags.has(args[i])) { i++; continue; }
|
|
667
|
+
if (!args[i].startsWith('--')) return args[i];
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
480
671
|
/**
|
|
481
672
|
* Parse comma-separated key=value pairs into an object
|
|
482
673
|
*/
|
|
@@ -495,8 +686,7 @@ function parseKVPairs(str) {
|
|
|
495
686
|
* Handle `aiwg mcp add <name> [opts]`
|
|
496
687
|
*/
|
|
497
688
|
async function handleAdd(args) {
|
|
498
|
-
const
|
|
499
|
-
const name = positional[0];
|
|
689
|
+
const name = serverName(args);
|
|
500
690
|
|
|
501
691
|
if (!name) {
|
|
502
692
|
console.error('Usage: aiwg mcp add <name> --url <url> [--type http|stdio|sse] [--command <cmd>] [--args <a,b>]');
|
|
@@ -510,6 +700,7 @@ async function handleAdd(args) {
|
|
|
510
700
|
const envStr = parseFlag(args, '--env');
|
|
511
701
|
const headersStr = parseFlag(args, '--headers');
|
|
512
702
|
const headerEnvStr = parseFlag(args, '--header-env');
|
|
703
|
+
const envFromStr = parseFlag(args, '--env-from');
|
|
513
704
|
const description = parseFlag(args, '--description');
|
|
514
705
|
|
|
515
706
|
if (type === 'stdio' && !command) {
|
|
@@ -531,13 +722,17 @@ async function handleAdd(args) {
|
|
|
531
722
|
env: parseKVPairs(envStr),
|
|
532
723
|
headers: parseKVPairs(headersStr),
|
|
533
724
|
headerEnv: parseKVPairs(headerEnvStr),
|
|
725
|
+
envFrom: parseKVPairs(envFromStr),
|
|
534
726
|
description,
|
|
535
727
|
});
|
|
536
728
|
|
|
537
729
|
console.log(`Added MCP server: ${name}`);
|
|
538
|
-
if (url) console.log(` URL: ${url}`);
|
|
539
|
-
if (command) console.log(` Command: ${command}`);
|
|
730
|
+
if (url) console.log(` URL: ${redactUrlUserinfo(url)}`);
|
|
731
|
+
if (command) console.log(` Command: ${redactCredentialText(command)}`);
|
|
540
732
|
console.log(` Type: ${type}`);
|
|
733
|
+
for (const [key, values] of [['env', parseKVPairs(envStr)], ['headers', parseKVPairs(headersStr)]]) {
|
|
734
|
+
if (values) console.log(` ${key}: ${Object.keys(values).join(', ')}`);
|
|
735
|
+
}
|
|
541
736
|
console.log(`\nUse "aiwg mcp inject --provider <name>" to inject into a provider config.`);
|
|
542
737
|
}
|
|
543
738
|
|
|
@@ -562,8 +757,7 @@ async function handleRemove(args) {
|
|
|
562
757
|
* Handle `aiwg mcp update <name> [opts]`
|
|
563
758
|
*/
|
|
564
759
|
async function handleUpdate(args) {
|
|
565
|
-
const
|
|
566
|
-
const name = positional[0];
|
|
760
|
+
const name = serverName(args);
|
|
567
761
|
|
|
568
762
|
if (!name) {
|
|
569
763
|
console.error('Usage: aiwg mcp update <name> --url <url> [--type <type>] ...');
|
|
@@ -578,6 +772,7 @@ async function handleUpdate(args) {
|
|
|
578
772
|
const envStr = parseFlag(args, '--env');
|
|
579
773
|
const headersStr = parseFlag(args, '--headers');
|
|
580
774
|
const headerEnvStr = parseFlag(args, '--header-env');
|
|
775
|
+
const envFromStr = parseFlag(args, '--env-from');
|
|
581
776
|
const description = parseFlag(args, '--description');
|
|
582
777
|
|
|
583
778
|
if (url !== undefined) updates.url = url;
|
|
@@ -587,6 +782,7 @@ async function handleUpdate(args) {
|
|
|
587
782
|
if (envStr !== undefined) updates.env = parseKVPairs(envStr);
|
|
588
783
|
if (headersStr !== undefined) updates.headers = parseKVPairs(headersStr);
|
|
589
784
|
if (headerEnvStr !== undefined) updates.headerEnv = parseKVPairs(headerEnvStr);
|
|
785
|
+
if (envFromStr !== undefined) updates.envFrom = parseKVPairs(envFromStr);
|
|
590
786
|
if (description !== undefined) updates.description = description;
|
|
591
787
|
|
|
592
788
|
if (Object.keys(updates).length === 0) {
|
|
@@ -598,17 +794,24 @@ async function handleUpdate(args) {
|
|
|
598
794
|
await registry.update(name, updates);
|
|
599
795
|
console.log(`Updated MCP server: ${name}`);
|
|
600
796
|
for (const [key, value] of Object.entries(updates)) {
|
|
601
|
-
|
|
797
|
+
const display = key === 'url' ? redactUrlUserinfo(value)
|
|
798
|
+
: key === 'args' ? JSON.stringify(redactCredentialArgs(value))
|
|
799
|
+
: ['env', 'headers', 'headerEnv', 'envFrom'].includes(key) ? Object.keys(value || {}).join(', ')
|
|
800
|
+
: typeof value === 'object' ? JSON.stringify(redactCredentialRecord(value)) : redactCredentialText(value);
|
|
801
|
+
console.log(` ${key}: ${display}`);
|
|
602
802
|
}
|
|
603
803
|
console.log(`\nRe-run "aiwg mcp inject --all" to propagate changes to provider configs.`);
|
|
604
804
|
}
|
|
605
805
|
|
|
806
|
+
// Refusal and all display paths share the scanner in credentials.mjs.
|
|
807
|
+
const redactUrlUserinfo = redactCredentialText;
|
|
808
|
+
|
|
606
809
|
/**
|
|
607
810
|
* Handle `aiwg mcp list` (managed servers from registry)
|
|
608
811
|
*/
|
|
609
812
|
async function handleList() {
|
|
610
813
|
const registry = new McpServerRegistry();
|
|
611
|
-
const servers = await registry.list();
|
|
814
|
+
const servers = redactCredentialRecord(await registry.list());
|
|
612
815
|
|
|
613
816
|
if (servers.length === 0) {
|
|
614
817
|
console.log('No MCP servers registered.');
|
|
@@ -621,13 +824,17 @@ async function handleList() {
|
|
|
621
824
|
for (const server of servers) {
|
|
622
825
|
console.log(` ${server.name}`);
|
|
623
826
|
console.log(` Type: ${server.type}`);
|
|
624
|
-
if (server.url) console.log(` URL: ${server.url}`);
|
|
625
|
-
if (server.command) console.log(` Command: ${server.command}${server.args ? ' ' + server.args.join(' ') : ''}`);
|
|
827
|
+
if (server.url) console.log(` URL: ${redactUrlUserinfo(server.url)}`);
|
|
828
|
+
if (server.command) console.log(` Command: ${server.command}${server.args ? ' ' + redactCredentialArgs(server.args).join(' ') : ''}`);
|
|
626
829
|
if (server.headerEnv) {
|
|
627
830
|
const refs = Object.entries(server.headerEnv).map(([header, envName]) => `${header}←${envName}`);
|
|
628
831
|
console.log(` Credential refs: ${refs.join(', ')}`);
|
|
629
832
|
}
|
|
630
|
-
if (server.
|
|
833
|
+
if (server.envFrom) {
|
|
834
|
+
const refs = Object.entries(server.envFrom).map(([key, envName]) => `${key}←${envName}`);
|
|
835
|
+
console.log(` Env refs: ${refs.join(', ')}`);
|
|
836
|
+
}
|
|
837
|
+
if (server.description) console.log(` Description: ${redactCredentialText(server.description)}`);
|
|
631
838
|
if (server.injectedProviders && server.injectedProviders.length > 0) {
|
|
632
839
|
console.log(` Injected into: ${server.injectedProviders.join(', ')}`);
|
|
633
840
|
}
|
|
@@ -660,6 +867,12 @@ async function handleInject(args) {
|
|
|
660
867
|
const profileName = parseFlag(args, '--profile');
|
|
661
868
|
const ephemeral = args.includes('--ephemeral');
|
|
662
869
|
const outPath = parseFlag(args, '--out');
|
|
870
|
+
const strictCredentials = args.includes('--strict-credentials');
|
|
871
|
+
const noCredentials = args.includes('--no-credentials');
|
|
872
|
+
if (strictCredentials && noCredentials) {
|
|
873
|
+
console.error('Error: --strict-credentials and --no-credentials are mutually exclusive.');
|
|
874
|
+
process.exit(1);
|
|
875
|
+
}
|
|
663
876
|
|
|
664
877
|
if (!provider && !injectAll) {
|
|
665
878
|
console.error('Usage: aiwg mcp inject --provider <name> [--profile <p>] [--ephemeral] [--servers a,b] [--dry-run]');
|
|
@@ -669,12 +882,19 @@ async function handleInject(args) {
|
|
|
669
882
|
}
|
|
670
883
|
|
|
671
884
|
const registry = new McpServerRegistry();
|
|
885
|
+
const credentialPolicy = resolveCredentialPolicy({
|
|
886
|
+
flag: noCredentials ? 'none' : strictCredentials ? 'references' : undefined,
|
|
887
|
+
registryPolicy: await registry.getCredentialPolicy(),
|
|
888
|
+
floorPolicy: await registry.getCredentialPolicyFloor(),
|
|
889
|
+
floorSource: await registry.getCredentialPolicyFloorSource(),
|
|
890
|
+
});
|
|
672
891
|
|
|
673
892
|
// Resolve server filter: --profile takes precedence over --servers
|
|
674
893
|
let serverFilter;
|
|
894
|
+
let profile;
|
|
675
895
|
if (profileName) {
|
|
676
896
|
const profiles = new McpProfileRegistry();
|
|
677
|
-
|
|
897
|
+
profile = await profiles.resolve(profileName);
|
|
678
898
|
if (!profile) {
|
|
679
899
|
const all = await profiles.list();
|
|
680
900
|
console.error(`Profile "${profileName}" not found.`);
|
|
@@ -707,6 +927,20 @@ async function handleInject(args) {
|
|
|
707
927
|
providers = [normalized];
|
|
708
928
|
}
|
|
709
929
|
|
|
930
|
+
if (injectAll && !ephemeral) {
|
|
931
|
+
const selected = (await registry.list()).filter(server => !serverFilter || serverFilter.includes(server.name));
|
|
932
|
+
for (const p of providers) {
|
|
933
|
+
const configPath = getProviderConfigPath(p, projectDir, { scope });
|
|
934
|
+
const { projectRoot } = classifyMcpDestination(p, scope, configPath, projectDir);
|
|
935
|
+
await assertConfigDestination(configPath, projectRoot);
|
|
936
|
+
// Every destination inside the project directory is meant to be committed
|
|
937
|
+
// to version control; home destinations are private.
|
|
938
|
+
if (projectRoot) {
|
|
939
|
+
assertProjectCredentials(selected, configPath, projectCredentialRemedy(p, projectDir));
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
710
944
|
if (ephemeral) {
|
|
711
945
|
for (const p of providers) {
|
|
712
946
|
const mcpDefinition = getMcpInjectionDefinition(p);
|
|
@@ -723,23 +957,27 @@ async function handleInject(args) {
|
|
|
723
957
|
|
|
724
958
|
let totalInjected = 0;
|
|
725
959
|
|
|
960
|
+
const printWarnings = (warnings = []) => {
|
|
961
|
+
for (const warning of warnings) console.error(` WARNING ${warning}`);
|
|
962
|
+
};
|
|
963
|
+
|
|
726
964
|
for (const p of providers) {
|
|
965
|
+
const toolFilters = profile ? resolveToolFilters(profile, p) : undefined;
|
|
727
966
|
if (ephemeral) {
|
|
728
967
|
// Generate a standalone ephemeral config file
|
|
729
|
-
const targetPath = outPath ?? path.join(
|
|
730
|
-
process.env.TMPDIR || '/tmp',
|
|
731
|
-
`aiwg-mcp-${profileName ?? 'custom'}-${p}-${Date.now()}.json`,
|
|
732
|
-
);
|
|
733
|
-
|
|
734
968
|
const allServers = await registry.list();
|
|
735
969
|
const servers = serverFilter
|
|
736
970
|
? allServers.filter(s => serverFilter.includes(s.name))
|
|
737
971
|
: allServers;
|
|
972
|
+
const toolPlan = hasToolFilters(toolFilters)
|
|
973
|
+
? planToolFilters(p, servers.map(server => server.name), toolFilters)
|
|
974
|
+
: null;
|
|
738
975
|
|
|
739
976
|
if (servers.length === 0) {
|
|
740
977
|
console.error(` ${p}: no servers to write`);
|
|
741
978
|
continue;
|
|
742
979
|
}
|
|
980
|
+
assertCredentialPolicy(servers, credentialPolicy);
|
|
743
981
|
|
|
744
982
|
// Build ephemeral config in provider's format
|
|
745
983
|
const mcpDefinition = getMcpInjectionDefinition(p);
|
|
@@ -752,32 +990,59 @@ async function handleInject(args) {
|
|
|
752
990
|
console.log(` Use "aiwg session --provider codex --profile ${profileName}" instead.`);
|
|
753
991
|
continue;
|
|
754
992
|
}
|
|
755
|
-
|
|
756
|
-
if (server.type === 'stdio') {
|
|
757
|
-
cfg.command = server.command;
|
|
758
|
-
cfg.args = server.args || [];
|
|
759
|
-
if (server.env) cfg.env = server.env;
|
|
760
|
-
} else {
|
|
761
|
-
cfg.url = server.url;
|
|
762
|
-
if (server.headers) cfg.headers = server.headers;
|
|
763
|
-
}
|
|
764
|
-
mcpBlock[server.name] = cfg;
|
|
993
|
+
mcpBlock[server.name] = buildServerConfig(server, p);
|
|
765
994
|
}
|
|
766
995
|
|
|
767
996
|
if (Object.keys(mcpBlock).length === 0) continue;
|
|
768
997
|
|
|
769
998
|
const config = { [mcpKey]: mcpBlock };
|
|
999
|
+
if (toolPlan) applyJsonToolFilterPlan(config, mcpKey, toolPlan);
|
|
1000
|
+
const tempDir = outPath || dryRun
|
|
1001
|
+
? undefined
|
|
1002
|
+
: await fs.mkdtemp(path.join(os.tmpdir(), 'aiwg-mcp-'));
|
|
1003
|
+
const targetPath = outPath ?? path.join(
|
|
1004
|
+
tempDir ?? path.join(os.tmpdir(), 'aiwg-mcp-<random>'),
|
|
1005
|
+
`${profileName ?? 'custom'}-${p}.json`,
|
|
1006
|
+
);
|
|
1007
|
+
|
|
1008
|
+
// Claude Code reads permission rules from settings, not from --mcp-config.
|
|
1009
|
+
const settingsPath = toolPlan?.claudePermissions ? targetPath.replace(/(\.json)?$/, '.settings.json') : null;
|
|
1010
|
+
// Explicit --out paths may be outside the project. Check every parent from
|
|
1011
|
+
// the filesystem root so neither destination can traverse a symlink.
|
|
1012
|
+
const outputRoot = path.parse(path.resolve(targetPath)).root;
|
|
1013
|
+
await assertConfigDestination(targetPath, outputRoot);
|
|
1014
|
+
const realProject = await fs.realpath(path.resolve(projectDir)).catch(error => {
|
|
1015
|
+
if (error.code !== 'ENOENT') throw error;
|
|
1016
|
+
return path.resolve(projectDir);
|
|
1017
|
+
});
|
|
1018
|
+
if (isPathInsideDir(targetPath, projectDir) || isPathInsideDir(targetPath, realProject)) {
|
|
1019
|
+
assertProjectCredentials(servers, targetPath, projectCredentialRemedy(p, projectDir));
|
|
1020
|
+
}
|
|
1021
|
+
const preparedPermissions = settingsPath
|
|
1022
|
+
? await prepareClaudePermissions(settingsPath, toolPlan.claudePermissions, {
|
|
1023
|
+
projectRoot: outputRoot,
|
|
1024
|
+
userScope: true,
|
|
1025
|
+
managedDir: path.dirname(registry.getPath()),
|
|
1026
|
+
sidecar: true,
|
|
1027
|
+
mcpPath: targetPath,
|
|
1028
|
+
})
|
|
1029
|
+
: null;
|
|
770
1030
|
|
|
771
1031
|
if (!dryRun) {
|
|
772
|
-
await
|
|
773
|
-
|
|
1032
|
+
await writeConfigTransaction([
|
|
1033
|
+
{ file: targetPath, content: JSON.stringify(config, null, 2) + '\n',
|
|
1034
|
+
options: { userScope: true, projectRoot: outputRoot } },
|
|
1035
|
+
...(preparedPermissions?.writes || []),
|
|
1036
|
+
]);
|
|
774
1037
|
}
|
|
775
1038
|
|
|
776
1039
|
const prefix = dryRun ? '[DRY RUN] ' : '';
|
|
777
1040
|
console.log(`${prefix}${p}: ${targetPath}`);
|
|
778
1041
|
console.log(` ${prefix}Servers: ${Object.keys(mcpBlock).join(', ')}`);
|
|
1042
|
+
if (settingsPath) console.log(` ${prefix}Tool permissions: ${settingsPath}`);
|
|
1043
|
+
printWarnings([...(toolPlan?.warnings || []), ...(preparedPermissions?.warnings || [])]);
|
|
779
1044
|
if (!dryRun && (p === 'claude-code' || p === 'claude')) {
|
|
780
|
-
console.log(` Launch with: claude --mcp-config ${targetPath}`);
|
|
1045
|
+
console.log(` Launch with: claude --mcp-config ${targetPath}${settingsPath ? ` --settings ${settingsPath}` : ''}`);
|
|
781
1046
|
}
|
|
782
1047
|
totalInjected += Object.keys(mcpBlock).length;
|
|
783
1048
|
continue;
|
|
@@ -789,6 +1054,8 @@ async function handleInject(args) {
|
|
|
789
1054
|
projectDir,
|
|
790
1055
|
dryRun,
|
|
791
1056
|
scope,
|
|
1057
|
+
...(hasToolFilters(toolFilters) ? { toolFilters } : {}),
|
|
1058
|
+
credentialPolicy,
|
|
792
1059
|
});
|
|
793
1060
|
|
|
794
1061
|
if (result.error) {
|
|
@@ -806,6 +1073,8 @@ async function handleInject(args) {
|
|
|
806
1073
|
if (result.alreadyPresent.length > 0) {
|
|
807
1074
|
console.log(` ${prefix}Updated in place: ${result.alreadyPresent.join(', ')}`);
|
|
808
1075
|
}
|
|
1076
|
+
if (result.settingsPath) console.log(` ${prefix}Tool permissions: ${result.settingsPath}`);
|
|
1077
|
+
printWarnings(result.warnings);
|
|
809
1078
|
}
|
|
810
1079
|
|
|
811
1080
|
if (!dryRun && totalInjected > 0 && !ephemeral) {
|
|
@@ -825,14 +1094,18 @@ function printProfileUsage() {
|
|
|
825
1094
|
aiwg mcp profile — MCP server profiles (named server subsets)
|
|
826
1095
|
|
|
827
1096
|
Usage:
|
|
828
|
-
aiwg mcp profile add <name> --servers a,b,c [--description "..."]
|
|
1097
|
+
aiwg mcp profile add <name> --servers a,b,c [--extends base,...] [--description "..."]
|
|
1098
|
+
[--provider <p|*>] [--tool-deny s__t,...] [--tool-allow s__t,...]
|
|
829
1099
|
aiwg mcp profile list
|
|
830
|
-
aiwg mcp profile show <name>
|
|
1100
|
+
aiwg mcp profile show <name> [--json]
|
|
831
1101
|
aiwg mcp profile edit <name> [--add-server x] [--remove-server y] [--description "..."]
|
|
1102
|
+
[--provider <p|*>] [--tool-deny s__t,...] [--tool-allow s__t,...]
|
|
1103
|
+
[--clear-tool-filters]
|
|
832
1104
|
aiwg mcp profile remove <name>
|
|
833
1105
|
aiwg mcp profile import <file>
|
|
834
1106
|
aiwg mcp profile export <name> [--out <file>]
|
|
835
1107
|
aiwg mcp profile init-presets
|
|
1108
|
+
aiwg mcp profile login <name> [--api-key-stdin | --api-key-file <path> | --api-key-env <ENV_VAR_NAME>]
|
|
836
1109
|
|
|
837
1110
|
Profiles let you define named subsets of your registered MCP servers:
|
|
838
1111
|
aiwg mcp profile add dev --servers git-gitea,memory-fortemi --description "Dev work"
|
|
@@ -840,11 +1113,60 @@ Profiles let you define named subsets of your registered MCP servers:
|
|
|
840
1113
|
aiwg mcp inject --provider claude --profile dev --ephemeral
|
|
841
1114
|
aiwg session --provider claude --profile dev
|
|
842
1115
|
|
|
1116
|
+
Layered configuration (organisation base, identity overlay):
|
|
1117
|
+
AIWG_CONFIG_LAYERS=/etc/aiwg/org:/home/me/.aiwg/acme aiwg mcp inject \
|
|
1118
|
+
--provider claude --profile acme-dev --ephemeral --out /tmp/acme.json
|
|
1119
|
+
Directories are listed lowest precedence first (path delimiter separated).
|
|
1120
|
+
A server or profile in a later layer replaces one of the same name in an
|
|
1121
|
+
earlier layer. Writes go to the last layer. --extends lets a profile inherit
|
|
1122
|
+
another profile's servers and tool filters from any layer.
|
|
1123
|
+
|
|
1124
|
+
Tool filters name tools as <server>__<tool>; <tool> may contain *. --provider
|
|
1125
|
+
defaults to *, which applies to every provider. inject renders them into each
|
|
1126
|
+
provider's own setting and warns about any filter the provider cannot express:
|
|
1127
|
+
aiwg mcp profile edit dev --tool-deny git-gitea__delete_repo
|
|
1128
|
+
aiwg mcp profile edit dev --provider codex --tool-allow git-gitea__list_repos
|
|
1129
|
+
|
|
843
1130
|
Preset profiles (minimal, dev, ops, research, incident, full):
|
|
844
1131
|
aiwg mcp profile init-presets
|
|
1132
|
+
|
|
1133
|
+
Logging in to a Codex profile's isolated runtime home (#3062):
|
|
1134
|
+
aiwg mcp profile login ops interactive (requires a terminal)
|
|
1135
|
+
vault read -field=key secret/openai |
|
|
1136
|
+
aiwg mcp profile login ops --api-key-stdin headless, most preferred — key never
|
|
1137
|
+
touches an environment variable or argv
|
|
1138
|
+
aiwg mcp profile login ops --api-key-file ~/.openai-key
|
|
1139
|
+
headless — file must be owned by you,
|
|
1140
|
+
mode 600, not a symlink
|
|
1141
|
+
aiwg mcp profile login ops --api-key-env OPENAI_KEY
|
|
1142
|
+
headless, least preferred — reads
|
|
1143
|
+
$OPENAI_KEY; visible to every other
|
|
1144
|
+
child process this shell starts
|
|
1145
|
+
In every case the key reaches codex over stdin only; it never appears on
|
|
1146
|
+
argv, in logs, or in error messages. Exactly one of the three may be
|
|
1147
|
+
given. Without a terminal and without any of the three, this errors
|
|
1148
|
+
naming all three. See "Supplying credentials safely" in the docs:
|
|
1149
|
+
docs/mcp/codex-profiles.md#supplying-credentials-safely
|
|
845
1150
|
`);
|
|
846
1151
|
}
|
|
847
1152
|
|
|
1153
|
+
/**
|
|
1154
|
+
* Parse --provider / --tool-deny / --tool-allow into a providerOverrides map.
|
|
1155
|
+
*/
|
|
1156
|
+
function parseToolFilterFlags(args) {
|
|
1157
|
+
const deny = parseFlag(args, '--tool-deny');
|
|
1158
|
+
const allow = parseFlag(args, '--tool-allow');
|
|
1159
|
+
if (!deny && !allow) return undefined;
|
|
1160
|
+
const split = value => value.split(',').map(s => s.trim()).filter(Boolean);
|
|
1161
|
+
const provider = parseFlag(args, '--provider') || '*';
|
|
1162
|
+
return {
|
|
1163
|
+
[provider]: {
|
|
1164
|
+
...(deny ? { toolDeny: split(deny) } : {}),
|
|
1165
|
+
...(allow ? { toolAllow: split(allow) } : {}),
|
|
1166
|
+
},
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
|
|
848
1170
|
/**
|
|
849
1171
|
* Handle `aiwg mcp profile add <name> [opts]`
|
|
850
1172
|
*/
|
|
@@ -858,17 +1180,26 @@ async function handleProfileAdd(args) {
|
|
|
858
1180
|
|
|
859
1181
|
const serversStr = parseFlag(args, '--servers');
|
|
860
1182
|
const description = parseFlag(args, '--description');
|
|
1183
|
+
const extendsStr = parseFlag(args, '--extends');
|
|
1184
|
+
const bases = extendsStr ? extendsStr.split(',').map(s => s.trim()).filter(Boolean) : undefined;
|
|
861
1185
|
|
|
862
|
-
if (!serversStr && name !== 'minimal') {
|
|
1186
|
+
if (!serversStr && !bases && name !== 'minimal') {
|
|
863
1187
|
console.error('Warning: no --servers specified. Profile will start empty.');
|
|
864
1188
|
}
|
|
865
1189
|
|
|
866
1190
|
const servers = serversStr ? serversStr.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
1191
|
+
const providerOverrides = parseToolFilterFlags(args);
|
|
867
1192
|
|
|
868
1193
|
const profiles = new McpProfileRegistry();
|
|
869
1194
|
const registry = new McpServerRegistry();
|
|
870
1195
|
|
|
871
|
-
await profiles.add({
|
|
1196
|
+
await profiles.add({
|
|
1197
|
+
name,
|
|
1198
|
+
description,
|
|
1199
|
+
servers,
|
|
1200
|
+
...(bases ? { extends: bases } : {}),
|
|
1201
|
+
...(providerOverrides ? { providerOverrides } : {}),
|
|
1202
|
+
}, registry);
|
|
872
1203
|
|
|
873
1204
|
console.log(`Profile added: ${name}`);
|
|
874
1205
|
if (description) console.log(` Description: ${description}`);
|
|
@@ -881,7 +1212,7 @@ async function handleProfileAdd(args) {
|
|
|
881
1212
|
*/
|
|
882
1213
|
async function handleProfileList() {
|
|
883
1214
|
const profiles = new McpProfileRegistry();
|
|
884
|
-
const all = await profiles.list();
|
|
1215
|
+
const all = redactCredentialRecord(await profiles.list());
|
|
885
1216
|
|
|
886
1217
|
if (all.length === 0) {
|
|
887
1218
|
console.log('No profiles defined.');
|
|
@@ -911,7 +1242,7 @@ async function handleProfileShow(args) {
|
|
|
911
1242
|
}
|
|
912
1243
|
|
|
913
1244
|
const profiles = new McpProfileRegistry();
|
|
914
|
-
const profile = await profiles.get(name);
|
|
1245
|
+
const profile = redactCredentialRecord(await profiles.get(name));
|
|
915
1246
|
|
|
916
1247
|
if (!profile) {
|
|
917
1248
|
const all = await profiles.list();
|
|
@@ -922,8 +1253,18 @@ async function handleProfileShow(args) {
|
|
|
922
1253
|
process.exit(1);
|
|
923
1254
|
}
|
|
924
1255
|
|
|
1256
|
+
if (args.includes('--json')) {
|
|
1257
|
+
console.log(JSON.stringify(redactCredentialRecord(profile), null, 2));
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
|
|
925
1261
|
console.log(`Profile: ${profile.name}`);
|
|
926
1262
|
if (profile.description) console.log(`Description: ${profile.description}`);
|
|
1263
|
+
if (profile.extends?.length) {
|
|
1264
|
+
console.log(`Extends: ${profile.extends.join(', ')}`);
|
|
1265
|
+
const resolved = await profiles.resolve(name);
|
|
1266
|
+
console.log(`Resolved servers: ${resolved.servers.join(', ') || '(none)'}`);
|
|
1267
|
+
}
|
|
927
1268
|
console.log(`\nServers (${profile.servers.length}):`);
|
|
928
1269
|
|
|
929
1270
|
if (profile.servers.length === 0) {
|
|
@@ -934,11 +1275,11 @@ async function handleProfileShow(args) {
|
|
|
934
1275
|
// Resolve server configs
|
|
935
1276
|
const registry = new McpServerRegistry();
|
|
936
1277
|
for (const serverName of profile.servers) {
|
|
937
|
-
const server = await registry.get(serverName);
|
|
1278
|
+
const server = redactCredentialRecord(await registry.get(serverName));
|
|
938
1279
|
if (server) {
|
|
939
1280
|
const detail = server.type === 'stdio'
|
|
940
|
-
? `stdio ${server.command}${server.args ? ' ' + server.args.join(' ') : ''}`
|
|
941
|
-
: `${server.type} ${server.url}`;
|
|
1281
|
+
? `stdio ${server.command}${server.args ? ' ' + redactCredentialArgs(server.args).join(' ') : ''}`
|
|
1282
|
+
: `${server.type} ${redactUrlUserinfo(server.url)}`;
|
|
942
1283
|
console.log(` ${serverName.padEnd(24)} ${detail}`);
|
|
943
1284
|
if (server.description) console.log(` ${''.padEnd(24)} ${server.description}`);
|
|
944
1285
|
} else {
|
|
@@ -975,14 +1316,18 @@ async function handleProfileEdit(args) {
|
|
|
975
1316
|
const removeServer = parseFlag(args, '--remove-server');
|
|
976
1317
|
const description = parseFlag(args, '--description');
|
|
977
1318
|
|
|
1319
|
+
const providerOverrides = parseToolFilterFlags(args);
|
|
1320
|
+
const clearToolFilters = args.includes('--clear-tool-filters');
|
|
978
1321
|
const changes = {
|
|
979
1322
|
description,
|
|
980
1323
|
addServers: addServer ? addServer.split(',').map(s => s.trim()) : undefined,
|
|
981
1324
|
removeServers: removeServer ? removeServer.split(',').map(s => s.trim()) : undefined,
|
|
1325
|
+
providerOverrides,
|
|
1326
|
+
clearToolFilters: clearToolFilters ? (parseFlag(args, '--provider') || '*') : undefined,
|
|
982
1327
|
};
|
|
983
1328
|
|
|
984
|
-
if (!description && !addServer && !removeServer) {
|
|
985
|
-
console.error('No changes specified. Use --add-server, --remove-server, or --
|
|
1329
|
+
if (!description && !addServer && !removeServer && !providerOverrides && !clearToolFilters) {
|
|
1330
|
+
console.error('No changes specified. Use --add-server, --remove-server, --description, --tool-deny, --tool-allow or --clear-tool-filters.');
|
|
986
1331
|
process.exit(1);
|
|
987
1332
|
}
|
|
988
1333
|
|
|
@@ -1055,6 +1400,135 @@ async function handleProfileInitPresets() {
|
|
|
1055
1400
|
}
|
|
1056
1401
|
}
|
|
1057
1402
|
|
|
1403
|
+
/**
|
|
1404
|
+
* Read all of stdin and return it as one trimmed string (go/no-go review,
|
|
1405
|
+
* blocker 1C's `--api-key-stdin`). Mirrors `src/cli/handlers/decision.ts`'s
|
|
1406
|
+
* own private `readStdin()` + `.trim()` for `--token-stdin` — duplicated
|
|
1407
|
+
* rather than imported for the same source/dist reason documented on
|
|
1408
|
+
* `requireFlagValue` above (this file runs both compiled and directly from
|
|
1409
|
+
* source). Input is expected to be a single line; a value containing
|
|
1410
|
+
* additional lines is not specially rejected, matching that precedent.
|
|
1411
|
+
*/
|
|
1412
|
+
async function readStdinAll() {
|
|
1413
|
+
const chunks = [];
|
|
1414
|
+
for await (const chunk of process.stdin) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk)));
|
|
1415
|
+
return Buffer.concat(chunks).toString('utf8');
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
/**
|
|
1419
|
+
* Handle `aiwg mcp profile login <name> [--api-key-env <ENV_VAR_NAME> |
|
|
1420
|
+
* --api-key-stdin | --api-key-file <path>]` (#3062, #3069, go/no-go review
|
|
1421
|
+
* blocker 1C).
|
|
1422
|
+
*
|
|
1423
|
+
* Wires `loginInProfile()` (src/mcp/adapters/codex-runtime.ts), which
|
|
1424
|
+
* already implements the full headless-safe behavior this command needs:
|
|
1425
|
+
* with a terminal attached and none of the three given, it runs the
|
|
1426
|
+
* interactive `codex login` OAuth flow. Exactly one credential source may
|
|
1427
|
+
* be given; more than one is a usage error naming the flags given. In
|
|
1428
|
+
* every case the key reaches `loginInProfile()` only as an in-memory value
|
|
1429
|
+
* (or, for `--api-key-env`, a variable *name*) — never on argv, in logs, or
|
|
1430
|
+
* in an error message — and travels to `codex` only on its stdin (see
|
|
1431
|
+
* `spawnCodexLoginWithApiKey()`'s own doc comment). Without a terminal and
|
|
1432
|
+
* without any of the three, `loginInProfile()` itself refuses, naming all
|
|
1433
|
+
* three flags.
|
|
1434
|
+
*
|
|
1435
|
+
* `--api-key-env` reads the key from that named environment variable, as
|
|
1436
|
+
* before. `--api-key-stdin` reads one line from this process's own stdin
|
|
1437
|
+
* (refusing if stdin is a terminal, or if it is empty) — the least
|
|
1438
|
+
* observable source, since the key then never exists in any environment or
|
|
1439
|
+
* on any command line. `--api-key-file <path>` reads the key from a file
|
|
1440
|
+
* that `readApiKeyFromFile()` requires to be a non-symlink, single-hard-
|
|
1441
|
+
* link, regular file owned by the current user with no group/other
|
|
1442
|
+
* permission bits on POSIX — refused otherwise, without ever printing the
|
|
1443
|
+
* path's contents. `--api-key-env` remains the least preferred of the
|
|
1444
|
+
* three (see "Supplying credentials safely" in the docs): the named
|
|
1445
|
+
* variable is visible to every other child process this shell starts.
|
|
1446
|
+
*/
|
|
1447
|
+
async function handleProfileLogin(args) {
|
|
1448
|
+
let apiKeyEnv;
|
|
1449
|
+
let apiKeyFilePath;
|
|
1450
|
+
try {
|
|
1451
|
+
// rejectEmpty: an explicitly-given-but-empty variable name/path can
|
|
1452
|
+
// never name a real environment variable or file (#3062 second review,
|
|
1453
|
+
// finding 4; go/no-go review blocker 1C).
|
|
1454
|
+
apiKeyEnv = requireFlagValue(args, { rejectEmpty: true }, '--api-key-env');
|
|
1455
|
+
apiKeyFilePath = requireFlagValue(args, { rejectEmpty: true }, '--api-key-file');
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
if (err instanceof MissingFlagValueError) {
|
|
1458
|
+
console.error(`${err.message}.`);
|
|
1459
|
+
process.exit(2);
|
|
1460
|
+
}
|
|
1461
|
+
throw err;
|
|
1462
|
+
}
|
|
1463
|
+
const useStdin = args.includes('--api-key-stdin');
|
|
1464
|
+
|
|
1465
|
+
const sourcesGiven = [
|
|
1466
|
+
apiKeyEnv !== undefined ? '--api-key-env' : null,
|
|
1467
|
+
useStdin ? '--api-key-stdin' : null,
|
|
1468
|
+
apiKeyFilePath !== undefined ? '--api-key-file' : null,
|
|
1469
|
+
].filter(Boolean);
|
|
1470
|
+
if (sourcesGiven.length > 1) {
|
|
1471
|
+
console.error(`Only one credential source may be given; got ${sourcesGiven.join(', ')}.`);
|
|
1472
|
+
process.exit(2);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
// Skip --api-key-env's/--api-key-file's own value when hunting for the
|
|
1476
|
+
// positional profile name, so e.g. `login --api-key-env FOO` (missing
|
|
1477
|
+
// name) never misreads FOO as the name (mirrors this file's own
|
|
1478
|
+
// `serverName()` helper). Stops at a bare `--` the same way
|
|
1479
|
+
// requireFlagValue() does.
|
|
1480
|
+
const positional = [];
|
|
1481
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
1482
|
+
const token = args[i];
|
|
1483
|
+
if (token === '--') break;
|
|
1484
|
+
if (token === '--api-key-env' || token === '--api-key-file') { i += 1; continue; }
|
|
1485
|
+
if (!token.startsWith('--')) positional.push(token);
|
|
1486
|
+
}
|
|
1487
|
+
const name = positional[0];
|
|
1488
|
+
if (!name) {
|
|
1489
|
+
console.error('Usage: aiwg mcp profile login <name> [--api-key-env <ENV_VAR_NAME> | --api-key-stdin | --api-key-file <path>]');
|
|
1490
|
+
process.exit(1);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
let apiKey;
|
|
1494
|
+
if (useStdin) {
|
|
1495
|
+
if (process.stdin.isTTY) {
|
|
1496
|
+
console.error(
|
|
1497
|
+
'--api-key-stdin requires piped input, not a terminal; run ' +
|
|
1498
|
+
`\`aiwg mcp profile login ${name}\` with no credential flag for the interactive login instead.`,
|
|
1499
|
+
);
|
|
1500
|
+
process.exit(2);
|
|
1501
|
+
}
|
|
1502
|
+
apiKey = (await readStdinAll()).trim();
|
|
1503
|
+
if (!apiKey) {
|
|
1504
|
+
console.error('--api-key-stdin received no key (stdin was empty).');
|
|
1505
|
+
process.exit(2);
|
|
1506
|
+
}
|
|
1507
|
+
} else if (apiKeyFilePath !== undefined) {
|
|
1508
|
+
const { readApiKeyFromFile } = await loadCodexRuntimeAdapter();
|
|
1509
|
+
try {
|
|
1510
|
+
apiKey = await readApiKeyFromFile(apiKeyFilePath);
|
|
1511
|
+
} catch (err) {
|
|
1512
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1513
|
+
process.exit(2);
|
|
1514
|
+
}
|
|
1515
|
+
if (!apiKey) {
|
|
1516
|
+
console.error(`--api-key-file "${apiKeyFilePath}" is empty.`);
|
|
1517
|
+
process.exit(2);
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
try {
|
|
1522
|
+
const { loginInProfile } = await loadCodexRuntimeAdapter();
|
|
1523
|
+
const loginOptions = apiKey !== undefined ? { apiKey } : apiKeyEnv ? { apiKeyEnv } : {};
|
|
1524
|
+
await loginInProfile(name, loginOptions);
|
|
1525
|
+
console.log(`\nLogged in to profile "${name}".`);
|
|
1526
|
+
} catch (err) {
|
|
1527
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
1528
|
+
process.exit(1);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1058
1532
|
/**
|
|
1059
1533
|
* Route `aiwg mcp profile <subcommand>`
|
|
1060
1534
|
*/
|
|
@@ -1080,6 +1554,9 @@ async function handleProfile(args) {
|
|
|
1080
1554
|
case 'rm':
|
|
1081
1555
|
await handleProfileRemove(subArgs);
|
|
1082
1556
|
break;
|
|
1557
|
+
case 'login':
|
|
1558
|
+
await handleProfileLogin(subArgs);
|
|
1559
|
+
break;
|
|
1083
1560
|
case 'import':
|
|
1084
1561
|
await handleProfileImport(subArgs);
|
|
1085
1562
|
break;
|
|
@@ -1137,14 +1614,21 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1137
1614
|
if (!['project', 'user'].includes(scope)) throw new Error('Scope must be project or user');
|
|
1138
1615
|
const projectDir = parseFlag(args, '--project') || (args[2] && !args[2].startsWith('--') ? args[2] : '.');
|
|
1139
1616
|
const configPath = getProviderConfigPath('omp', projectDir, { scope });
|
|
1140
|
-
|
|
1617
|
+
// Destination-derived, not scope-derived: a provider home-directory
|
|
1618
|
+
// override (e.g. PI_CODING_AGENT_DIR) that still resolves inside the
|
|
1619
|
+
// project must get project containment, whatever scope was requested.
|
|
1620
|
+
const { userScope, projectRoot } = classifyMcpDestination('omp', scope, configPath, projectDir);
|
|
1621
|
+
await assertConfigDestination(configPath, projectRoot);
|
|
1622
|
+
const result = await manageOmpMcp(configPath, [{ name: 'aiwg', type: 'stdio', command: 'aiwg', args: ['mcp', 'serve'] }], { dryRun: args.includes('--dry-run'), userScope, projectRoot });
|
|
1141
1623
|
console.log(JSON.stringify(result, null, 2));
|
|
1142
1624
|
break;
|
|
1143
1625
|
}
|
|
1144
1626
|
// Parse install arguments (skip flags)
|
|
1145
|
-
const
|
|
1627
|
+
const scope = parseFlag(args, '--scope') || 'project';
|
|
1628
|
+
if (!['project', 'user'].includes(scope)) throw new Error('Scope must be project or user');
|
|
1629
|
+
const installArgs = args.slice(1).filter((a, index, rest) => !a.startsWith('--') && !['--scope', '--project'].includes(rest[index - 1]));
|
|
1146
1630
|
const target = installArgs[0] || 'claude';
|
|
1147
|
-
const projectDir = installArgs[1] || '.';
|
|
1631
|
+
const projectDir = parseFlag(args, '--project') || installArgs[1] || '.';
|
|
1148
1632
|
|
|
1149
1633
|
// Check for --dry-run flag
|
|
1150
1634
|
if (args.includes('--dry-run')) {
|
|
@@ -1152,13 +1636,13 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1152
1636
|
console.log(`[DRY RUN] Would generate MCP config for: ${target}`);
|
|
1153
1637
|
console.log(`[DRY RUN] Target directory: ${projectDir}`);
|
|
1154
1638
|
const configPaths = {
|
|
1155
|
-
claude: '.claude
|
|
1639
|
+
claude: scope === 'user' ? path.join(homeDir, '.claude.json') : path.join(projectDir, '.mcp.json'),
|
|
1156
1640
|
cursor: '.cursor/mcp.json',
|
|
1157
1641
|
factory: (projectDir === '.' || projectDir === 'global')
|
|
1158
1642
|
? path.join(homeDir, '.factory/mcp.json')
|
|
1159
1643
|
: path.join(projectDir, '.factory/mcp.json'),
|
|
1160
|
-
codex: path.join(homeDir, '.codex
|
|
1161
|
-
openai: path.join(homeDir, '.codex
|
|
1644
|
+
codex: path.resolve(process.env.CODEX_HOME || path.join(homeDir, '.codex'), 'config.toml'),
|
|
1645
|
+
openai: path.resolve(process.env.CODEX_HOME || path.join(homeDir, '.codex'), 'config.toml'),
|
|
1162
1646
|
vscode: '.vscode/mcp.json',
|
|
1163
1647
|
copilot: '.vscode/mcp.json',
|
|
1164
1648
|
opencode: (projectDir === '.' || projectDir === 'global')
|
|
@@ -1171,7 +1655,7 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1171
1655
|
break;
|
|
1172
1656
|
}
|
|
1173
1657
|
|
|
1174
|
-
await generateConfig(target, projectDir);
|
|
1658
|
+
if (!await generateConfig(target, projectDir, scope)) process.exitCode = 1;
|
|
1175
1659
|
break;
|
|
1176
1660
|
}
|
|
1177
1661
|
|
|
@@ -1208,9 +1692,9 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1208
1692
|
if (!['project', 'user'].includes(scope)) throw new Error('Scope must be project or user');
|
|
1209
1693
|
const remove = (parseFlag(subArgs, '--servers') || '').split(',').map(s => s.trim()).filter(Boolean);
|
|
1210
1694
|
if (!remove.length) throw new Error('uninject requires --servers name[,name]');
|
|
1211
|
-
const
|
|
1695
|
+
const projectDir = parseFlag(subArgs, '--project') || '.';
|
|
1696
|
+
const configPath = getProviderConfigPath(provider, projectDir, { scope });
|
|
1212
1697
|
if (provider === 'grok-build') {
|
|
1213
|
-
const projectDir = parseFlag(subArgs, '--project') || '.';
|
|
1214
1698
|
const result = await unmanageGrokBuildMcp(configPath, remove, {
|
|
1215
1699
|
dryRun: subArgs.includes('--dry-run'),
|
|
1216
1700
|
root: scope === 'user' ? path.dirname(path.dirname(configPath)) : path.resolve(projectDir),
|
|
@@ -1218,7 +1702,17 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1218
1702
|
console.log(JSON.stringify(result, null, 2));
|
|
1219
1703
|
break;
|
|
1220
1704
|
}
|
|
1221
|
-
|
|
1705
|
+
// Destination-derived, not scope-derived: a provider home-directory
|
|
1706
|
+
// override that still resolves inside the project must get project
|
|
1707
|
+
// containment, whatever scope was requested.
|
|
1708
|
+
const { userScope: ompUserScope, projectRoot: ompProjectRoot } =
|
|
1709
|
+
classifyMcpDestination(provider, scope, configPath, projectDir);
|
|
1710
|
+
const result = await manageOmpMcp(configPath, [], {
|
|
1711
|
+
remove,
|
|
1712
|
+
dryRun: subArgs.includes('--dry-run'),
|
|
1713
|
+
userScope: ompUserScope,
|
|
1714
|
+
projectRoot: ompProjectRoot,
|
|
1715
|
+
});
|
|
1222
1716
|
console.log(JSON.stringify(result, null, 2));
|
|
1223
1717
|
break;
|
|
1224
1718
|
}
|
|
@@ -1227,6 +1721,22 @@ export async function main(args = process.argv.slice(2)) {
|
|
|
1227
1721
|
await handleProfile(subArgs);
|
|
1228
1722
|
break;
|
|
1229
1723
|
|
|
1724
|
+
case 'credential-policy': {
|
|
1725
|
+
const registry = new McpServerRegistry();
|
|
1726
|
+
const value = subArgs.find(a => !a.startsWith('--'));
|
|
1727
|
+
if (!value) {
|
|
1728
|
+
console.log(await registry.getCredentialPolicy() || 'literal');
|
|
1729
|
+
break;
|
|
1730
|
+
}
|
|
1731
|
+
if (!CREDENTIAL_POLICIES.includes(value)) {
|
|
1732
|
+
console.error(`Unknown credential policy "${value}". Use one of: ${CREDENTIAL_POLICIES.join(', ')}`);
|
|
1733
|
+
process.exit(1);
|
|
1734
|
+
}
|
|
1735
|
+
await registry.setCredentialPolicy(value);
|
|
1736
|
+
console.log(`Credential policy: ${value}`);
|
|
1737
|
+
break;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1230
1740
|
case '--help':
|
|
1231
1741
|
case '-h':
|
|
1232
1742
|
case 'help':
|