@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/server.mjs
CHANGED
|
@@ -16,6 +16,8 @@ import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mc
|
|
|
16
16
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
17
17
|
import { z } from 'zod';
|
|
18
18
|
import fs from 'fs/promises';
|
|
19
|
+
import { constants } from 'node:fs';
|
|
20
|
+
import { randomUUID } from 'node:crypto';
|
|
19
21
|
import path from 'path';
|
|
20
22
|
import { fileURLToPath } from 'url';
|
|
21
23
|
import { registerWriterProfileResources } from './tools/writer-profiles.mjs';
|
|
@@ -33,10 +35,321 @@ const FRAMEWORKS = {
|
|
|
33
35
|
'media-marketing-kit': path.join(AIWG_ROOT, 'agentic/code/frameworks/media-marketing-kit')
|
|
34
36
|
};
|
|
35
37
|
|
|
38
|
+
function assertArtifactContained(root, target) {
|
|
39
|
+
const relative = path.relative(root, target);
|
|
40
|
+
if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
41
|
+
throw new Error('Artifact path must stay beneath the artifact root');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function rootedArtifactAccess() {
|
|
46
|
+
return process.platform === 'linux' && constants.O_NOFOLLOW && constants.O_DIRECTORY;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A root pinned as missing must be created by us, never adopted later. */
|
|
50
|
+
async function createArtifactRoot(store) {
|
|
51
|
+
let parent;
|
|
52
|
+
let directory;
|
|
53
|
+
let verified = false;
|
|
54
|
+
try {
|
|
55
|
+
let entry = store.configured;
|
|
56
|
+
if (rootedArtifactAccess()) {
|
|
57
|
+
parent = await fs.open(store.projectRoot, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
58
|
+
if (await fs.realpath(`/proc/self/fd/${parent.fd}`) !== store.projectRoot) {
|
|
59
|
+
throw new Error('Project root changed during artifact creation');
|
|
60
|
+
}
|
|
61
|
+
entry = `/proc/self/fd/${parent.fd}/.aiwg`;
|
|
62
|
+
}
|
|
63
|
+
// EEXIST is a refusal, including a symlink or directory installed since pinning.
|
|
64
|
+
await fs.mkdir(entry);
|
|
65
|
+
const created = await fs.lstat(entry);
|
|
66
|
+
if (created.isSymbolicLink() || !created.isDirectory()) throw new Error('Artifact root changed during creation');
|
|
67
|
+
// Windows cannot open directory handles through fs.open; its fallback
|
|
68
|
+
// uses static identity checks, as do the subsequent artifact operations.
|
|
69
|
+
if (process.platform !== 'win32') {
|
|
70
|
+
directory = await fs.open(entry, constants.O_RDONLY | (constants.O_DIRECTORY || 0) | (constants.O_NOFOLLOW || 0));
|
|
71
|
+
}
|
|
72
|
+
const opened = directory ? await directory.stat() : await fs.stat(entry);
|
|
73
|
+
const current = await fs.lstat(entry);
|
|
74
|
+
if (current.isSymbolicLink() || !opened.isDirectory()
|
|
75
|
+
|| opened.dev !== created.dev || opened.ino !== created.ino
|
|
76
|
+
|| opened.dev !== current.dev || opened.ino !== current.ino
|
|
77
|
+
|| await fs.realpath(store.configured) !== store.root) {
|
|
78
|
+
throw new Error('Artifact root changed during creation');
|
|
79
|
+
}
|
|
80
|
+
store.identity = opened;
|
|
81
|
+
verified = true;
|
|
82
|
+
} finally {
|
|
83
|
+
const closed = await Promise.allSettled([...(directory ? [directory.close()] : []), ...(parent ? [parent.close()] : [])]);
|
|
84
|
+
const failure = closed.find(result => result.status === 'rejected');
|
|
85
|
+
if (verified && failure) throw failure.reason;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Pin an existing store once; only a missing local default may be bootstrapped. */
|
|
90
|
+
async function pinArtifactRoot(configured, projectRoot) {
|
|
91
|
+
try {
|
|
92
|
+
const root = await fs.realpath(configured);
|
|
93
|
+
const identity = await fs.stat(root);
|
|
94
|
+
const store = { configured, projectRoot, root, identity };
|
|
95
|
+
await verifyArtifactRoot(store);
|
|
96
|
+
return store;
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error.code === 'ENOENT' && configured === path.join(projectRoot, '.aiwg')) {
|
|
99
|
+
const existing = await fs.lstat(configured).catch(missing => {
|
|
100
|
+
if (missing.code !== 'ENOENT') throw missing;
|
|
101
|
+
});
|
|
102
|
+
if (!existing) return { configured, projectRoot, root: configured, identity: null };
|
|
103
|
+
// A symlink (or other unexpected node) present where the default root
|
|
104
|
+
// should be created must not become the containment root later.
|
|
105
|
+
return {
|
|
106
|
+
configured,
|
|
107
|
+
projectRoot,
|
|
108
|
+
root: configured,
|
|
109
|
+
error: new Error('Default artifact root is not a creatable directory'),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
// Keep discovery available, but never adopt a store that was unavailable
|
|
113
|
+
// at startup (including a dangling alias) on a later request.
|
|
114
|
+
return { configured, projectRoot, root: configured, error };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async function verifyArtifactRoot(store) {
|
|
119
|
+
if (store.error) throw store.error;
|
|
120
|
+
const canonical = await fs.realpath(store.configured);
|
|
121
|
+
const current = await fs.stat(store.configured);
|
|
122
|
+
if (canonical !== store.root || !current.isDirectory()
|
|
123
|
+
|| current.dev !== store.identity.dev || current.ino !== store.identity.ino) {
|
|
124
|
+
throw new Error('Artifact root changed since it was pinned');
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function pinnedArtifactRoot(store, write) {
|
|
129
|
+
if (store.error) throw store.error;
|
|
130
|
+
if (!store.identity) {
|
|
131
|
+
if (!write) throw new Error('Default artifact root has not been created');
|
|
132
|
+
// Serialize first-write initialization so concurrent requests share one
|
|
133
|
+
// identity. Creation still rejects symlinks and uses a project descriptor.
|
|
134
|
+
store.initialization ||= createArtifactRoot(store);
|
|
135
|
+
await store.initialization;
|
|
136
|
+
}
|
|
137
|
+
await verifyArtifactRoot(store);
|
|
138
|
+
return store.root;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Resolve existing aliases before creating anything; dangling symlinks fail closed. */
|
|
142
|
+
async function resolveArtifactPath(store, artifactPath, { write = false } = {}) {
|
|
143
|
+
if (!artifactPath || path.isAbsolute(artifactPath) || path.win32.isAbsolute(artifactPath)
|
|
144
|
+
|| /^[A-Za-z]:/.test(artifactPath)
|
|
145
|
+
|| (process.platform === 'win32' && artifactPath.split(/[\\/]/).some(part => part !== '.'
|
|
146
|
+
&& (/[<>:"|?*\x00-\x1f]/.test(part) || /[. ]$/.test(part)
|
|
147
|
+
|| /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part))))
|
|
148
|
+
|| artifactPath.split(/[\\/]/).includes('..')) {
|
|
149
|
+
throw new Error('Artifact path must be relative and contain no .. segments');
|
|
150
|
+
}
|
|
151
|
+
if (write && artifactPath.split(/[\\/]/).some(part => /^\.aiwg-location[. ]*(?::.*)?$/i.test(part))) {
|
|
152
|
+
throw new Error('Refusing to write an artifact location pointer');
|
|
153
|
+
}
|
|
154
|
+
const root = await pinnedArtifactRoot(store, write);
|
|
155
|
+
const target = path.resolve(root, artifactPath);
|
|
156
|
+
assertArtifactContained(root, target);
|
|
157
|
+
if (!rootedArtifactAccess()) {
|
|
158
|
+
let componentPath = root;
|
|
159
|
+
for (const component of path.relative(root, target).split(path.sep)) {
|
|
160
|
+
componentPath = path.join(componentPath, component);
|
|
161
|
+
const info = await fs.lstat(componentPath).catch(error => {
|
|
162
|
+
if (error.code !== 'ENOENT') throw error;
|
|
163
|
+
});
|
|
164
|
+
if (info?.isSymbolicLink()) throw new Error('Refusing artifact symlink component');
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
let ancestor = path.dirname(target);
|
|
168
|
+
const missing = [];
|
|
169
|
+
while (true) {
|
|
170
|
+
try {
|
|
171
|
+
// lstat distinguishes a missing directory from a dangling symlink.
|
|
172
|
+
await fs.lstat(ancestor);
|
|
173
|
+
break;
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error.code !== 'ENOENT') throw error;
|
|
176
|
+
missing.unshift(path.basename(ancestor));
|
|
177
|
+
ancestor = path.dirname(ancestor);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const realAncestor = await fs.realpath(ancestor);
|
|
181
|
+
if (realAncestor !== root) assertArtifactContained(root, realAncestor);
|
|
182
|
+
const fullPath = path.resolve(realAncestor, ...missing, path.basename(target));
|
|
183
|
+
assertArtifactContained(root, fullPath);
|
|
184
|
+
let info;
|
|
185
|
+
try {
|
|
186
|
+
info = await fs.lstat(fullPath);
|
|
187
|
+
} catch (error) {
|
|
188
|
+
if (error.code !== 'ENOENT') throw error;
|
|
189
|
+
}
|
|
190
|
+
if (info) {
|
|
191
|
+
if (write && info.isSymbolicLink()) throw new Error('Refusing to write through an artifact symlink');
|
|
192
|
+
const realTarget = await fs.realpath(fullPath);
|
|
193
|
+
assertArtifactContained(root, realTarget);
|
|
194
|
+
if (!write) return { root, fullPath: realTarget };
|
|
195
|
+
}
|
|
196
|
+
return { root, fullPath };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Linux procfs descriptor paths anchor each lookup to an opened directory.
|
|
201
|
+
* Other platforms use static containment checks. Writes use a fresh inode;
|
|
202
|
+
* failed verification closes handles without unsafe pathname cleanup.
|
|
203
|
+
* Concurrent same-user relocation after verification is not a sandbox boundary.
|
|
204
|
+
*/
|
|
205
|
+
async function openArtifact(store, artifactPath, { write = false } = {}) {
|
|
206
|
+
const { root, fullPath } = await resolveArtifactPath(store, artifactPath, { write });
|
|
207
|
+
if (!rootedArtifactAccess()) return openStaticArtifact(store, root, fullPath, { write });
|
|
208
|
+
const directories = [];
|
|
209
|
+
let file;
|
|
210
|
+
let transferred = false;
|
|
211
|
+
const descriptorPath = handle => `/proc/self/fd/${handle.fd}`;
|
|
212
|
+
async function verify(handle, { rootHandle = false, directory = false, entry } = {}) {
|
|
213
|
+
const actual = await fs.realpath(descriptorPath(handle));
|
|
214
|
+
if (rootHandle) {
|
|
215
|
+
if (actual !== root) throw new Error('Artifact root changed during access');
|
|
216
|
+
} else {
|
|
217
|
+
assertArtifactContained(root, actual);
|
|
218
|
+
}
|
|
219
|
+
const [opened, current] = await Promise.all([handle.stat(), fs.lstat(entry || actual)]);
|
|
220
|
+
if (rootHandle) {
|
|
221
|
+
await verifyArtifactRoot(store);
|
|
222
|
+
if (opened.dev !== store.identity.dev || opened.ino !== store.identity.ino) {
|
|
223
|
+
throw new Error('Artifact root changed since it was pinned');
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
if (current.isSymbolicLink() || opened.dev !== current.dev || opened.ino !== current.ino
|
|
227
|
+
|| (directory ? !opened.isDirectory() : !opened.isFile())) {
|
|
228
|
+
throw new Error('Artifact changed during access or is not a regular file/directory');
|
|
229
|
+
}
|
|
230
|
+
if (!directory && (opened.nlink > 1 || current.nlink > 1)) {
|
|
231
|
+
throw new Error('Refusing multiply-linked artifact file');
|
|
232
|
+
}
|
|
233
|
+
return actual;
|
|
234
|
+
}
|
|
235
|
+
async function close() {
|
|
236
|
+
const closed = await Promise.allSettled([...(file ? [file.close()] : []), ...directories.map(handle => handle.close())]);
|
|
237
|
+
const failure = closed.find(result => result.status === 'rejected');
|
|
238
|
+
if (failure) throw failure.reason;
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
let parent = await fs.open(root, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
242
|
+
directories.push(parent);
|
|
243
|
+
await verify(parent, { rootHandle: true, directory: true });
|
|
244
|
+
const components = path.relative(root, fullPath).split(path.sep);
|
|
245
|
+
const name = components.pop();
|
|
246
|
+
for (const component of components) {
|
|
247
|
+
const entry = path.join(descriptorPath(parent), component);
|
|
248
|
+
let child;
|
|
249
|
+
try {
|
|
250
|
+
child = await fs.open(entry, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
251
|
+
} catch (error) {
|
|
252
|
+
if (!write || error.code !== 'ENOENT') throw error;
|
|
253
|
+
await verify(parent, { rootHandle: directories.length === 1, directory: true });
|
|
254
|
+
try {
|
|
255
|
+
await fs.mkdir(entry);
|
|
256
|
+
} catch (mkdirError) {
|
|
257
|
+
if (mkdirError.code !== 'EEXIST') throw mkdirError;
|
|
258
|
+
}
|
|
259
|
+
child = await fs.open(entry, constants.O_RDONLY | constants.O_DIRECTORY | constants.O_NOFOLLOW);
|
|
260
|
+
}
|
|
261
|
+
directories.push(child);
|
|
262
|
+
await verify(child, { directory: true });
|
|
263
|
+
parent = child;
|
|
264
|
+
}
|
|
265
|
+
const parentOptions = { rootHandle: directories.length === 1, directory: true };
|
|
266
|
+
await verify(parent, parentOptions);
|
|
267
|
+
const destination = path.join(descriptorPath(parent), name);
|
|
268
|
+
async function verifyDestination() {
|
|
269
|
+
const info = await fs.lstat(destination).catch(error => {
|
|
270
|
+
if (error.code !== 'ENOENT') throw error;
|
|
271
|
+
});
|
|
272
|
+
if (info && (info.isSymbolicLink() || !info.isFile())) {
|
|
273
|
+
throw new Error('Refusing to replace an artifact symlink or nonregular file');
|
|
274
|
+
}
|
|
275
|
+
return info;
|
|
276
|
+
}
|
|
277
|
+
const targetInfo = write ? await verifyDestination() : undefined;
|
|
278
|
+
const finalMode = targetInfo ? targetInfo.mode & 0o7777 : 0o666 & ~process.umask();
|
|
279
|
+
const entry = write ? path.join(descriptorPath(parent), `.aiwg-write-${randomUUID()}.tmp`) : destination;
|
|
280
|
+
file = await fs.open(entry,
|
|
281
|
+
(write ? constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL : constants.O_RDONLY)
|
|
282
|
+
| constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
|
|
283
|
+
await verify(file, { entry });
|
|
284
|
+
transferred = true;
|
|
285
|
+
return {
|
|
286
|
+
handle: file,
|
|
287
|
+
fullPath,
|
|
288
|
+
close,
|
|
289
|
+
async commit() {
|
|
290
|
+
await verify(directories[0], { rootHandle: true, directory: true });
|
|
291
|
+
await verify(file, { entry });
|
|
292
|
+
await verify(parent, parentOptions);
|
|
293
|
+
await verifyDestination();
|
|
294
|
+
await file.chmod(finalMode);
|
|
295
|
+
await fs.rename(entry, destination);
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
} finally {
|
|
299
|
+
if (!transferred) await Promise.allSettled([close()]);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/** Static checks for platforms without Linux descriptor paths. */
|
|
304
|
+
async function openStaticArtifact(store, root, fullPath, { write }) {
|
|
305
|
+
// Inspect the lexical path as well as the resolved path: contained aliases
|
|
306
|
+
// must not hide symlink components on this fallback.
|
|
307
|
+
async function check(target, { allowMissing = false } = {}) {
|
|
308
|
+
const relative = path.relative(root, target);
|
|
309
|
+
assertArtifactContained(root, target);
|
|
310
|
+
let current = root;
|
|
311
|
+
for (const component of relative.split(path.sep)) {
|
|
312
|
+
current = path.join(current, component);
|
|
313
|
+
const info = await fs.lstat(current).catch(error => {
|
|
314
|
+
if (!allowMissing || error.code !== 'ENOENT') throw error;
|
|
315
|
+
});
|
|
316
|
+
if (!info) continue;
|
|
317
|
+
if (info.isSymbolicLink()) throw new Error('Refusing artifact symlink component');
|
|
318
|
+
assertArtifactContained(root, await fs.realpath(current));
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
await check(fullPath, { allowMissing: write });
|
|
322
|
+
if (write) await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
323
|
+
const target = await fs.lstat(fullPath).catch(error => {
|
|
324
|
+
if (!write || error.code !== 'ENOENT') throw error;
|
|
325
|
+
});
|
|
326
|
+
if (target && (!target.isFile() || target.isSymbolicLink())) throw new Error('Artifact must be a regular file');
|
|
327
|
+
if (!write && target.nlink > 1) throw new Error('Refusing multiply-linked artifact file');
|
|
328
|
+
const entry = write ? path.join(path.dirname(fullPath), `.aiwg-write-${randomUUID()}.tmp`) : fullPath;
|
|
329
|
+
const finalMode = target ? target.mode & 0o7777 : 0o666 & ~process.umask();
|
|
330
|
+
const handle = await fs.open(entry, write ? constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL : constants.O_RDONLY, 0o600);
|
|
331
|
+
try {
|
|
332
|
+
const opened = await handle.stat();
|
|
333
|
+
if (!opened.isFile() || opened.nlink > 1) throw new Error('Refusing nonregular or multiply-linked artifact file');
|
|
334
|
+
return {
|
|
335
|
+
handle, fullPath, close: () => handle.close(),
|
|
336
|
+
async commit() {
|
|
337
|
+
await verifyArtifactRoot(store);
|
|
338
|
+
await check(fullPath, { allowMissing: true });
|
|
339
|
+
await handle.chmod(finalMode);
|
|
340
|
+
await fs.rename(entry, fullPath);
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
} catch (error) {
|
|
344
|
+
await handle.close().catch(() => {});
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
36
349
|
/**
|
|
37
350
|
* Create and configure the AIWG MCP Server
|
|
38
351
|
*/
|
|
39
|
-
export function createServer() {
|
|
352
|
+
export async function createServer() {
|
|
40
353
|
const server = new McpServer({
|
|
41
354
|
name: 'aiwg',
|
|
42
355
|
version: '1.0.0'
|
|
@@ -70,11 +383,72 @@ export function createServer() {
|
|
|
70
383
|
// Opt-in subsystem toolsets (#1322-#1332)
|
|
71
384
|
//
|
|
72
385
|
// Enabled via AIWG_MCP_TOOLSETS env or `aiwg mcp serve --toolsets=`.
|
|
73
|
-
// Known: flows, missions, memory, kb, research, activity-log, index, ralph, mc, ops, sandbox, all
|
|
386
|
+
// Known: flows, missions, memory, kb, research, activity-log, index, ralph, mc, ops, sandbox, decision, all
|
|
387
|
+
// `workers` (#2892 direct worker run/status/cancel) needs an EXPLICIT name;
|
|
388
|
+
// `all` deliberately does not reach it (src/mcp/tools/subsystems.mjs).
|
|
74
389
|
// Default: none (core only — discovery + command-run)
|
|
75
390
|
// ============================================
|
|
76
391
|
const requested = parseToolsets(process.env.AIWG_MCP_TOOLSETS || '');
|
|
77
|
-
|
|
392
|
+
// #2892 B1: the `workers` toolset's served project root is resolved ONCE,
|
|
393
|
+
// here, at registration time — never lazily from `process.cwd()` inside
|
|
394
|
+
// the tool handler on first request. A project root that cannot be found
|
|
395
|
+
// (not running inside an AIWG project) means `workers` is simply not
|
|
396
|
+
// registered; `registerWorkerInvokeToolset` refuses rather than falling
|
|
397
|
+
// back to an implicit cwd.
|
|
398
|
+
let projectRoot;
|
|
399
|
+
try {
|
|
400
|
+
projectRoot = await findProjectRoot();
|
|
401
|
+
} catch {
|
|
402
|
+
projectRoot = undefined;
|
|
403
|
+
}
|
|
404
|
+
await registerOptInToolsets(server, requested, { projectRoot });
|
|
405
|
+
|
|
406
|
+
// Resolve operator configuration once per trusted project, then pin it.
|
|
407
|
+
// Artifact tools default to the server-start project (the launch directory),
|
|
408
|
+
// not an ancestor discovered by walking up from it: a fresh launch directory
|
|
409
|
+
// inside another marked tree (for example a stray `.aiwg` in the temp dir)
|
|
410
|
+
// must pin its own missing default at startup, never adopt the ancestor's
|
|
411
|
+
// store. Callers select other projects explicitly via `project_dir`.
|
|
412
|
+
const artifactEnv = { ...process.env };
|
|
413
|
+
const trustedProjectRoot = await fs.realpath(process.cwd());
|
|
414
|
+
const trustedArtifactRoot = await resolveProjectAiwgDir(trustedProjectRoot, { env: artifactEnv });
|
|
415
|
+
const startupStore = await pinArtifactRoot(trustedArtifactRoot, trustedProjectRoot);
|
|
416
|
+
const trustedStore = startupStore.root;
|
|
417
|
+
const projects = new Map();
|
|
418
|
+
async function trustProject(requested, store) {
|
|
419
|
+
const canonical = await fs.realpath(requested);
|
|
420
|
+
const identity = await fs.stat(requested);
|
|
421
|
+
if (!identity.isDirectory()) throw new Error('project_dir must be a real directory');
|
|
422
|
+
if (canonical !== trustedProjectRoot) {
|
|
423
|
+
const inStore = path.relative(trustedStore, canonical);
|
|
424
|
+
if (canonical.split(path.sep).includes('.aiwg') || !inStore
|
|
425
|
+
|| (inStore !== '..' && !inStore.startsWith(`..${path.sep}`) && !path.isAbsolute(inStore))) {
|
|
426
|
+
throw new Error('project_dir must not select a project inside an artifact store');
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
const configured = store ? undefined : await resolveProjectAiwgDir(canonical, { env: { ...process.env } });
|
|
430
|
+
return { canonical, identity, store: store || await pinArtifactRoot(configured, canonical) };
|
|
431
|
+
}
|
|
432
|
+
const startupProject = Promise.resolve(await trustProject(trustedProjectRoot, startupStore));
|
|
433
|
+
projects.set(trustedProjectRoot, startupProject);
|
|
434
|
+
const canonicalProjects = new Map([[trustedProjectRoot, startupProject]]);
|
|
435
|
+
async function artifactRoots(explicit) {
|
|
436
|
+
const requested = explicit ? path.resolve(trustedProjectRoot, explicit) : trustedProjectRoot;
|
|
437
|
+
// Key by the supplied project path, so a replacement cannot acquire fresh trust.
|
|
438
|
+
if (!projects.has(requested)) {
|
|
439
|
+
const canonical = await fs.realpath(requested);
|
|
440
|
+
if (!canonicalProjects.has(canonical)) canonicalProjects.set(canonical, trustProject(requested));
|
|
441
|
+
projects.set(requested, canonicalProjects.get(canonical));
|
|
442
|
+
}
|
|
443
|
+
const pinned = await projects.get(requested);
|
|
444
|
+
const canonical = await fs.realpath(requested);
|
|
445
|
+
const current = await fs.stat(requested);
|
|
446
|
+
if (canonical !== pinned.canonical || !current.isDirectory()
|
|
447
|
+
|| current.dev !== pinned.identity.dev || current.ino !== pinned.identity.ino) {
|
|
448
|
+
throw new Error('Project root changed since it was pinned');
|
|
449
|
+
}
|
|
450
|
+
return pinned.store;
|
|
451
|
+
}
|
|
78
452
|
|
|
79
453
|
// Tool: artifact-read
|
|
80
454
|
server.registerTool('artifact-read', {
|
|
@@ -82,14 +456,21 @@ export function createServer() {
|
|
|
82
456
|
description: 'Read artifact from .aiwg/ directory',
|
|
83
457
|
inputSchema: {
|
|
84
458
|
path: z.string().describe('Path relative to .aiwg/ (e.g., "requirements/UC-001.md")'),
|
|
85
|
-
project_dir: z.string().optional().describe('
|
|
459
|
+
project_dir: z.string().optional().describe('Real project directory outside artifact trees (defaults to server-start project; artifact store pinned on first use)')
|
|
86
460
|
}
|
|
87
461
|
}, async ({ path: artifactPath, project_dir }) => {
|
|
88
462
|
try {
|
|
89
|
-
|
|
90
|
-
const
|
|
91
|
-
|
|
92
|
-
|
|
463
|
+
const store = await artifactRoots(project_dir);
|
|
464
|
+
const { handle, close } = await openArtifact(store, artifactPath);
|
|
465
|
+
let content;
|
|
466
|
+
let completed = false;
|
|
467
|
+
try {
|
|
468
|
+
content = await handle.readFile('utf-8');
|
|
469
|
+
completed = true;
|
|
470
|
+
} finally {
|
|
471
|
+
const [closed] = await Promise.allSettled([close()]);
|
|
472
|
+
if (completed && closed.status === 'rejected') throw closed.reason;
|
|
473
|
+
}
|
|
93
474
|
return {
|
|
94
475
|
content: [{
|
|
95
476
|
type: 'text',
|
|
@@ -114,20 +495,22 @@ export function createServer() {
|
|
|
114
495
|
inputSchema: {
|
|
115
496
|
path: z.string().describe('Path relative to .aiwg/'),
|
|
116
497
|
content: z.string().describe('Artifact content'),
|
|
117
|
-
project_dir: z.string().optional().describe('
|
|
498
|
+
project_dir: z.string().optional().describe('Real project directory outside artifact trees (defaults to server-start project; artifact store pinned on first use)')
|
|
118
499
|
}
|
|
119
500
|
}, async ({ path: artifactPath, content, project_dir }) => {
|
|
120
501
|
try {
|
|
121
|
-
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
502
|
+
const store = await artifactRoots(project_dir);
|
|
503
|
+
const { handle, fullPath, commit, close } = await openArtifact(store, artifactPath, { write: true });
|
|
504
|
+
let completed = false;
|
|
505
|
+
try {
|
|
506
|
+
await handle.writeFile(content, 'utf-8');
|
|
507
|
+
await commit();
|
|
508
|
+
completed = true;
|
|
509
|
+
} finally {
|
|
510
|
+
// Close every owned descriptor; preserve any write/verification error.
|
|
511
|
+
const [closed] = await Promise.allSettled([close()]);
|
|
512
|
+
if (completed && closed.status === 'rejected') throw closed.reason;
|
|
513
|
+
}
|
|
131
514
|
|
|
132
515
|
return {
|
|
133
516
|
content: [{
|
|
@@ -611,7 +994,7 @@ async function scanDirectory(dir, type) {
|
|
|
611
994
|
* Start the MCP server with stdio transport
|
|
612
995
|
*/
|
|
613
996
|
export async function startServer() {
|
|
614
|
-
const server = createServer();
|
|
997
|
+
const server = await createServer();
|
|
615
998
|
const transport = new StdioServerTransport();
|
|
616
999
|
|
|
617
1000
|
console.error('[AIWG MCP] Starting server...');
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Encode a complete TOML basic string using valid Unicode scalar values. */
|
|
2
|
+
export function tomlString(value) {
|
|
3
|
+
if (typeof value !== 'string' || [...value].some(char => {
|
|
4
|
+
const point = char.codePointAt(0);
|
|
5
|
+
return point >= 0xd800 && point <= 0xdfff;
|
|
6
|
+
})) throw new Error('TOML values must be strings containing valid Unicode scalar values');
|
|
7
|
+
// JSON escapes align with TOML basic strings except DEL must also be escaped.
|
|
8
|
+
return JSON.stringify(value).replace(/\u007f/g, '\\u007f');
|
|
9
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { parseTOML } from 'toml-eslint-parser';
|
|
2
|
+
|
|
3
|
+
/** Remove the root MCP subtree without interpreting strings as TOML structure. */
|
|
4
|
+
export function stripMcpServers(text) {
|
|
5
|
+
const fail = () => { throw new Error('Refusing to copy base Codex config: cannot safely classify TOML (malformed or unterminated structure)'); };
|
|
6
|
+
// Reuse the existing persistent-editor validator (no new dependency). The
|
|
7
|
+
// scanner below owns subtree removal; validation also catches ambiguous
|
|
8
|
+
// scalar syntax and duplicate definitions, beyond balanced delimiters.
|
|
9
|
+
try { parseTOML(text, { tomlVersion: '1.1.0' }); } catch { fail(); }
|
|
10
|
+
// A key path may use bare, literal, or escaped basic quoted components.
|
|
11
|
+
const keyPath = source => {
|
|
12
|
+
const parts = [];
|
|
13
|
+
let rest = source.trim();
|
|
14
|
+
while (rest) {
|
|
15
|
+
const match = /^(?:([A-Za-z0-9_-]+)|'([^'\r\n]*)'|"((?:[^"\\\r\n]|\\(?:[btnfr"\\]|u[\da-fA-F]{4}|U[\da-fA-F]{8}))*)")/.exec(rest);
|
|
16
|
+
if (!match) fail();
|
|
17
|
+
let key = match[1] ?? match[2];
|
|
18
|
+
if (key === undefined) {
|
|
19
|
+
key = match[3].replace(/\\(u[\da-fA-F]{4}|U[\da-fA-F]{8}|[btnfr"\\])/g, (_, escape) => {
|
|
20
|
+
if (escape[0] === 'u' || escape[0] === 'U') {
|
|
21
|
+
const code = Number.parseInt(escape.slice(1), 16);
|
|
22
|
+
if (code > 0x10ffff || (code >= 0xd800 && code <= 0xdfff)) fail();
|
|
23
|
+
return String.fromCodePoint(code);
|
|
24
|
+
}
|
|
25
|
+
return { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' }[escape];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
parts.push(key);
|
|
29
|
+
rest = rest.slice(match[0].length).trim();
|
|
30
|
+
if (!rest) break;
|
|
31
|
+
if (!rest.startsWith('.')) fail();
|
|
32
|
+
rest = rest.slice(1).trim();
|
|
33
|
+
if (!rest) fail();
|
|
34
|
+
}
|
|
35
|
+
if (!parts.length) fail();
|
|
36
|
+
return parts;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let table = [];
|
|
40
|
+
let quote = null;
|
|
41
|
+
let multiline = false;
|
|
42
|
+
const brackets = [];
|
|
43
|
+
let record = '';
|
|
44
|
+
let drop = false;
|
|
45
|
+
const kept = [];
|
|
46
|
+
for (const line of text.split(/(?<=\n)/)) {
|
|
47
|
+
let start = 0;
|
|
48
|
+
if (!record) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (!trimmed || trimmed.startsWith('#')) {
|
|
51
|
+
if (table[0] !== 'mcp_servers') kept.push(line);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (trimmed.startsWith('[')) {
|
|
55
|
+
// Header strings are single-line key components; scan their quotes so
|
|
56
|
+
// a bracket or hash inside a quoted name cannot terminate the header.
|
|
57
|
+
let headerQuote = null;
|
|
58
|
+
let end = line.length;
|
|
59
|
+
for (let i = 0; i < line.length; i++) {
|
|
60
|
+
const char = line[i];
|
|
61
|
+
if (headerQuote) {
|
|
62
|
+
if (headerQuote === '"' && char === '\\') { i++; continue; }
|
|
63
|
+
if (char === headerQuote) headerQuote = null;
|
|
64
|
+
} else if (char === '"' || char === "'") headerQuote = char;
|
|
65
|
+
else if (char === '#') { end = i; break; }
|
|
66
|
+
}
|
|
67
|
+
if (headerQuote) fail();
|
|
68
|
+
const header = line.slice(0, end).trim();
|
|
69
|
+
const array = header.startsWith('[[');
|
|
70
|
+
const width = array ? 2 : 1;
|
|
71
|
+
if (!header.endsWith(array ? ']]' : ']')) fail();
|
|
72
|
+
table = keyPath(header.slice(width, -width));
|
|
73
|
+
if (table[0] !== 'mcp_servers') kept.push(line);
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
let keyQuote = null;
|
|
77
|
+
let equals = -1;
|
|
78
|
+
for (let i = 0; i < line.length; i++) {
|
|
79
|
+
const char = line[i];
|
|
80
|
+
if (keyQuote) {
|
|
81
|
+
if (keyQuote === '"' && char === '\\') { i++; continue; }
|
|
82
|
+
if (char === keyQuote) keyQuote = null;
|
|
83
|
+
} else if (char === '"' || char === "'") keyQuote = char;
|
|
84
|
+
else if (char === '=') { equals = i; break; }
|
|
85
|
+
else if (char === '#') break;
|
|
86
|
+
}
|
|
87
|
+
if (equals < 0 || keyQuote) fail();
|
|
88
|
+
const key = keyPath(line.slice(0, equals));
|
|
89
|
+
drop = table[0] === 'mcp_servers' || (table.length === 0 && key[0] === 'mcp_servers');
|
|
90
|
+
start = equals + 1;
|
|
91
|
+
if (!line.slice(start).split('#')[0].trim()) fail();
|
|
92
|
+
}
|
|
93
|
+
record += line;
|
|
94
|
+
for (let i = start; i < line.length; i++) {
|
|
95
|
+
const char = line[i];
|
|
96
|
+
if (quote) {
|
|
97
|
+
if (quote === '"' && char === '\\') {
|
|
98
|
+
// A backslash escapes the next character (including a multiline
|
|
99
|
+
// continuation); it cannot conceal a closing delimiter.
|
|
100
|
+
if (i + 1 >= line.length) fail();
|
|
101
|
+
i++;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === quote) {
|
|
105
|
+
if (!multiline) quote = null;
|
|
106
|
+
else if (line.slice(i, i + 3) === quote.repeat(3)) {
|
|
107
|
+
let run = 3;
|
|
108
|
+
while (line[i + run] === quote) run++;
|
|
109
|
+
if (run > 5) fail();
|
|
110
|
+
i += run - 1;
|
|
111
|
+
quote = null;
|
|
112
|
+
multiline = false;
|
|
113
|
+
}
|
|
114
|
+
} else if (!multiline && (char === '\n' || char === '\r')) fail();
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
if (char === '#') break;
|
|
118
|
+
if (char === '"' || char === "'") {
|
|
119
|
+
quote = char;
|
|
120
|
+
multiline = line.slice(i, i + 3) === char.repeat(3);
|
|
121
|
+
if (multiline) i += 2;
|
|
122
|
+
} else if (char === '[' || char === '{') brackets.push(char);
|
|
123
|
+
else if (char === ']' || char === '}') {
|
|
124
|
+
if (brackets.pop() !== (char === ']' ? '[' : '{')) fail();
|
|
125
|
+
} else if (char === '=' && !brackets.includes('{')) fail();
|
|
126
|
+
}
|
|
127
|
+
if (quote && !multiline) fail();
|
|
128
|
+
if (!quote && brackets.length === 0) {
|
|
129
|
+
if (!drop) kept.push(record);
|
|
130
|
+
record = '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (quote || brackets.length || record) fail();
|
|
134
|
+
return kept.join('');
|
|
135
|
+
}
|