agentlas 1.0.64 → 1.0.66
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/CHANGELOG.md +87 -0
- package/README.md +8 -6
- package/bin/agentlas.cjs +341 -16
- package/engine/acp/server.cjs +27 -1
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +29 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +10 -3
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/bootstrap-schema.sql +4 -4
- package/engine/browser/cdp.cjs +315 -32
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +80 -28
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +308 -113
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +67 -34
- package/engine/commands/build.cjs +42 -12
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +40 -12
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +8 -2
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +29 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +230 -61
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +15 -5
- package/engine/mcp/plan.cjs +5 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +212 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +187 -24
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
package/engine/mcp/contract.cjs
CHANGED
|
@@ -18,6 +18,7 @@ const MAX_JSON_BYTES = 2 * 1024 * 1024;
|
|
|
18
18
|
const MAX_APPROVED_MCP_PER_BUILD = 8;
|
|
19
19
|
const STATE_LOCK_STALE_MS = 30_000;
|
|
20
20
|
const STATE_LOCK_WAIT_MS = 2_000;
|
|
21
|
+
const STATE_LOCK_OWNER_MAX_BYTES = 512;
|
|
21
22
|
|
|
22
23
|
// Public contract text must be compact, value-free, and instruction-safe.
|
|
23
24
|
const UNSAFE_TEXT_PATTERNS = [
|
|
@@ -92,14 +93,109 @@ function safeDisplayName(value, fallback) {
|
|
|
92
93
|
return text;
|
|
93
94
|
}
|
|
94
95
|
|
|
96
|
+
function fileIdentityEqual(left, right) {
|
|
97
|
+
return Boolean(left && right) && left.dev === right.dev && left.ino === right.ino && left.nlink === right.nlink && left.mode === right.mode;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function fileTimesEqual(left, right) {
|
|
101
|
+
return Boolean(left && right) && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function assertRegularJsonFile(stat, label) {
|
|
105
|
+
if (!stat || !stat.isFile() || stat.isSymbolicLink() || stat.nlink !== 1) {
|
|
106
|
+
throw new Error(`${label} must be a regular file (symlinks are not accepted)`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function assertJsonFileSize(stat, label) {
|
|
111
|
+
if (stat.size <= 0 || stat.size > MAX_JSON_BYTES) throw new Error(`${label} has an invalid size`);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function openReadOnlyNoFollow(filePath) {
|
|
115
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
116
|
+
try {
|
|
117
|
+
return fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// Node does not expose a no-follow open flag on every Windows filesystem.
|
|
120
|
+
// Keep the lstat/fstat/path identity checks below as the fallback there;
|
|
121
|
+
// never silently drop O_NOFOLLOW on POSIX.
|
|
122
|
+
if (process.platform === "win32" && noFollow && ["EINVAL", "ENOTSUP"].includes(error && error.code)) {
|
|
123
|
+
return fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
124
|
+
}
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function readBoundedDescriptor(fd, maxBytes) {
|
|
130
|
+
const chunks = [];
|
|
131
|
+
let total = 0;
|
|
132
|
+
while (total <= maxBytes) {
|
|
133
|
+
const capacity = Math.min(64 * 1024, maxBytes + 1 - total);
|
|
134
|
+
if (capacity <= 0) break;
|
|
135
|
+
const chunk = Buffer.allocUnsafe(capacity);
|
|
136
|
+
const result = fs.readSync(fd, chunk, 0, capacity, null);
|
|
137
|
+
const bytesRead = typeof result === "number" ? result : result.bytesRead;
|
|
138
|
+
if (!Number.isInteger(bytesRead) || bytesRead < 0) throw new Error("descriptor read returned an invalid byte count");
|
|
139
|
+
if (bytesRead === 0) break;
|
|
140
|
+
chunks.push(chunk.subarray(0, bytesRead));
|
|
141
|
+
total += bytesRead;
|
|
142
|
+
}
|
|
143
|
+
return { bytes: Buffer.concat(chunks, total), byteLength: total };
|
|
144
|
+
}
|
|
145
|
+
|
|
95
146
|
function readJsonFile(filePath, label) {
|
|
96
147
|
const absolute = path.resolve(filePath);
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
148
|
+
let fd;
|
|
149
|
+
let listed;
|
|
150
|
+
try {
|
|
151
|
+
listed = fs.lstatSync(absolute);
|
|
152
|
+
assertRegularJsonFile(listed, label);
|
|
153
|
+
assertJsonFileSize(listed, label);
|
|
154
|
+
fd = openReadOnlyNoFollow(absolute);
|
|
155
|
+
} catch (error) {
|
|
156
|
+
if (error && error.code === "ELOOP") {
|
|
157
|
+
throw new Error(`${label} must be a regular file (symlinks are not accepted)`);
|
|
158
|
+
}
|
|
159
|
+
throw error;
|
|
160
|
+
}
|
|
161
|
+
try {
|
|
162
|
+
const before = fs.fstatSync(fd);
|
|
163
|
+
assertRegularJsonFile(before, label);
|
|
164
|
+
assertJsonFileSize(before, label);
|
|
165
|
+
if (!fileIdentityEqual(listed, before) || !fileTimesEqual(listed, before) || listed.size !== before.size) {
|
|
166
|
+
throw new Error(`${label} changed while it was being read`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let captured;
|
|
170
|
+
try {
|
|
171
|
+
captured = readBoundedDescriptor(fd, MAX_JSON_BYTES);
|
|
172
|
+
} catch (error) {
|
|
173
|
+
throw new Error(`${label} is not valid JSON: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
const after = fs.fstatSync(fd);
|
|
176
|
+
let current;
|
|
177
|
+
try { current = fs.lstatSync(absolute); }
|
|
178
|
+
catch { throw new Error(`${label} changed while it was being read`); }
|
|
179
|
+
if (!current.isFile() || current.isSymbolicLink() || current.nlink !== 1) {
|
|
180
|
+
throw new Error(`${label} changed while it was being read`);
|
|
181
|
+
}
|
|
182
|
+
if (captured.byteLength <= 0 || captured.byteLength > MAX_JSON_BYTES || after.size <= 0 || after.size > MAX_JSON_BYTES) {
|
|
183
|
+
throw new Error(`${label} has an invalid size`);
|
|
184
|
+
}
|
|
185
|
+
if (
|
|
186
|
+
!fileIdentityEqual(before, after) || !fileTimesEqual(before, after) || after.size !== before.size ||
|
|
187
|
+
captured.byteLength !== after.size || !fileIdentityEqual(before, current) ||
|
|
188
|
+
!fileTimesEqual(before, current) || current.size !== after.size
|
|
189
|
+
) {
|
|
190
|
+
throw new Error(`${label} changed while it was being read`);
|
|
191
|
+
}
|
|
192
|
+
let value;
|
|
193
|
+
try { value = JSON.parse(captured.bytes.toString("utf8")); }
|
|
194
|
+
catch (error) { throw new Error(`${label} is not valid JSON: ${error.message}`); }
|
|
195
|
+
return { absolute, value };
|
|
196
|
+
} finally {
|
|
197
|
+
try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
198
|
+
}
|
|
103
199
|
}
|
|
104
200
|
|
|
105
201
|
function writePrivateJsonAtomic(filePath, value) {
|
|
@@ -122,43 +218,152 @@ function waitSync(milliseconds) {
|
|
|
122
218
|
Atomics.wait(signal, 0, 0, milliseconds);
|
|
123
219
|
}
|
|
124
220
|
|
|
221
|
+
function processIsAlive(pid) {
|
|
222
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
223
|
+
try {
|
|
224
|
+
process.kill(pid, 0);
|
|
225
|
+
return true;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error && error.code === "ESRCH") return false;
|
|
228
|
+
// EPERM and unknown failures mean the process may still be alive. Never
|
|
229
|
+
// reclaim a stale-looking lock when liveness cannot be disproved.
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function readLockOwner(lockPath, labels) {
|
|
235
|
+
const ownerPath = path.join(lockPath, "owner.json");
|
|
236
|
+
let fd;
|
|
237
|
+
try {
|
|
238
|
+
const listed = fs.lstatSync(ownerPath);
|
|
239
|
+
if (!listed.isFile() || listed.isSymbolicLink() || listed.nlink !== 1) throw new Error(labels.unsafe);
|
|
240
|
+
fd = openReadOnlyNoFollow(ownerPath);
|
|
241
|
+
const before = fs.fstatSync(fd);
|
|
242
|
+
if (!before.isFile() || before.isSymbolicLink() || before.nlink !== 1 || before.size <= 0 || before.size > STATE_LOCK_OWNER_MAX_BYTES) {
|
|
243
|
+
throw new Error(labels.unsafe);
|
|
244
|
+
}
|
|
245
|
+
// 정상 release/다음 owner 획득과 겹친 교체는 unsafe 파일이 아니라 재시도 신호다.
|
|
246
|
+
if (!fileIdentityEqual(listed, before) || !fileTimesEqual(listed, before)) return null;
|
|
247
|
+
const captured = readBoundedDescriptor(fd, STATE_LOCK_OWNER_MAX_BYTES);
|
|
248
|
+
const after = fs.fstatSync(fd);
|
|
249
|
+
let current;
|
|
250
|
+
try { current = fs.lstatSync(ownerPath); }
|
|
251
|
+
catch (error) {
|
|
252
|
+
if (error && error.code === "ENOENT") return null;
|
|
253
|
+
throw new Error(labels.unsafe);
|
|
254
|
+
}
|
|
255
|
+
if (
|
|
256
|
+
captured.byteLength !== before.size || !fileIdentityEqual(before, after) || !fileTimesEqual(before, after) ||
|
|
257
|
+
after.size !== before.size || !fileIdentityEqual(before, current) || !fileTimesEqual(before, current) ||
|
|
258
|
+
current.size !== after.size
|
|
259
|
+
) return null;
|
|
260
|
+
let owner;
|
|
261
|
+
try { owner = JSON.parse(captured.bytes.toString("utf8")); }
|
|
262
|
+
catch { throw new Error(labels.unsafe); }
|
|
263
|
+
if (
|
|
264
|
+
!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0 ||
|
|
265
|
+
typeof owner.nonce !== "string" || !/^[a-f0-9]{32}$/.test(owner.nonce)
|
|
266
|
+
) throw new Error(labels.unsafe);
|
|
267
|
+
return owner;
|
|
268
|
+
} catch (error) {
|
|
269
|
+
if (error && error.code === "ENOENT") return null;
|
|
270
|
+
if (error && error.code === "ELOOP") throw new Error(labels.unsafe);
|
|
271
|
+
throw error;
|
|
272
|
+
} finally {
|
|
273
|
+
if (fd !== undefined) try { fs.closeSync(fd); } catch { /* best effort */ }
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
125
277
|
/**
|
|
126
|
-
* <stateFile>.lock
|
|
127
|
-
*
|
|
278
|
+
* <stateFile>.lock directory 기반 상호배제. 크래시 잔류 락은 owner PID가
|
|
279
|
+
* 실제로 죽었다고 확인될 때만 고유 quarantine 경로로 회수한다. 디렉터리
|
|
280
|
+
* rename이 소유권 hand-off 경계이므로 이전 소유자의 cleanup이 successor를
|
|
281
|
+
* unlink할 수 없다.
|
|
128
282
|
*/
|
|
129
283
|
function withPrivateStateLock(stateFile, labels, action) {
|
|
130
284
|
const dir = path.dirname(stateFile);
|
|
131
285
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
132
286
|
try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
|
|
133
|
-
const
|
|
287
|
+
const lockPath = `${stateFile}.lock`;
|
|
134
288
|
const deadline = Date.now() + STATE_LOCK_WAIT_MS;
|
|
135
|
-
let
|
|
136
|
-
|
|
289
|
+
let acquiredOwner = null;
|
|
290
|
+
let acquired = false;
|
|
291
|
+
while (!acquired) {
|
|
137
292
|
try {
|
|
138
|
-
|
|
139
|
-
fs.writeFileSync(descriptor, `${process.pid}\n${new Date().toISOString()}\n`, "utf8");
|
|
293
|
+
fs.mkdirSync(lockPath, { mode: 0o700 });
|
|
140
294
|
} catch (error) {
|
|
141
|
-
if (!error || error.code !== "EEXIST") throw error;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
fs.unlinkSync(lockFile);
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
} catch (statError) {
|
|
295
|
+
if (!error || (error.code !== "EEXIST" && error.code !== "ENOENT")) throw error;
|
|
296
|
+
if (error.code === "ENOENT") continue;
|
|
297
|
+
let stat;
|
|
298
|
+
try { stat = fs.lstatSync(lockPath); }
|
|
299
|
+
catch (statError) {
|
|
150
300
|
if (statError && statError.code === "ENOENT") continue;
|
|
151
301
|
throw statError;
|
|
152
302
|
}
|
|
303
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(labels.unsafe);
|
|
304
|
+
let owner;
|
|
305
|
+
try { owner = readLockOwner(lockPath, labels); }
|
|
306
|
+
catch (ownerError) { throw ownerError; }
|
|
307
|
+
if (Date.now() - stat.mtimeMs > STATE_LOCK_STALE_MS) {
|
|
308
|
+
// Missing or malformed owner identity is not evidence of a dead PID.
|
|
309
|
+
// Stay fail-closed rather than stealing a possibly live lock.
|
|
310
|
+
if (!owner || processIsAlive(owner.pid)) {
|
|
311
|
+
if (Date.now() >= deadline) throw new Error(labels.busy);
|
|
312
|
+
waitSync(25);
|
|
313
|
+
continue;
|
|
314
|
+
}
|
|
315
|
+
const quarantine = `${lockPath}.stale-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
316
|
+
try {
|
|
317
|
+
fs.renameSync(lockPath, quarantine);
|
|
318
|
+
fs.rmSync(quarantine, { recursive: true, force: true });
|
|
319
|
+
continue;
|
|
320
|
+
} catch (reclaimError) {
|
|
321
|
+
if (reclaimError && reclaimError.code === "ENOENT") continue;
|
|
322
|
+
throw reclaimError;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
153
325
|
if (Date.now() >= deadline) throw new Error(labels.busy);
|
|
154
326
|
waitSync(25);
|
|
327
|
+
// 기존 소유자가 정상적으로 보유 중이다. 같은 디렉터리의 owner.json에
|
|
328
|
+
// 쓰기를 시도하면 EEXIST 처리 중 활성 잠금을 abandoned로 오인해 지울 수 있다.
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
try {
|
|
332
|
+
acquiredOwner = {
|
|
333
|
+
pid: process.pid,
|
|
334
|
+
nonce: crypto.randomBytes(16).toString("hex"),
|
|
335
|
+
createdAt: new Date().toISOString(),
|
|
336
|
+
};
|
|
337
|
+
// owner.json 자체도 원자적으로 공개한다. mkdir 직후의 부분 쓰기를 다른
|
|
338
|
+
// contender가 size=0/malformed unsafe lock으로 관측하면 정상 경합이 실패한다.
|
|
339
|
+
writePrivateJsonAtomic(path.join(lockPath, "owner.json"), acquiredOwner);
|
|
340
|
+
acquired = true;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
const abandoned = `${lockPath}.abandoned-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
343
|
+
try {
|
|
344
|
+
fs.renameSync(lockPath, abandoned);
|
|
345
|
+
fs.rmSync(abandoned, { recursive: true, force: true });
|
|
346
|
+
} catch { /* original owner-write error remains authoritative */ }
|
|
347
|
+
throw error;
|
|
155
348
|
}
|
|
156
349
|
}
|
|
157
350
|
try {
|
|
158
351
|
return action();
|
|
159
352
|
} finally {
|
|
160
|
-
|
|
161
|
-
try {
|
|
353
|
+
let ownsLock = false;
|
|
354
|
+
try {
|
|
355
|
+
const owner = readLockOwner(lockPath, labels);
|
|
356
|
+
ownsLock = Boolean(owner && acquiredOwner && owner.pid === acquiredOwner.pid && owner.nonce === acquiredOwner.nonce);
|
|
357
|
+
} catch { /* do not remove an unverified lock */ }
|
|
358
|
+
if (ownsLock) {
|
|
359
|
+
const cleanup = `${lockPath}.done-${process.pid}-${crypto.randomBytes(6).toString("hex")}`;
|
|
360
|
+
try {
|
|
361
|
+
fs.renameSync(lockPath, cleanup);
|
|
362
|
+
fs.rmSync(cleanup, { recursive: true, force: true });
|
|
363
|
+
} catch (error) {
|
|
364
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
162
367
|
}
|
|
163
368
|
}
|
|
164
369
|
|
package/engine/mcp/inventory.cjs
CHANGED
|
@@ -13,6 +13,7 @@ const crypto = require("node:crypto");
|
|
|
13
13
|
const fs = require("node:fs");
|
|
14
14
|
const os = require("node:os");
|
|
15
15
|
const path = require("node:path");
|
|
16
|
+
const { userDataDir: defaultUserDataDir } = require("../core/paths.cjs");
|
|
16
17
|
const {
|
|
17
18
|
hasSensitiveRuntimeArgument,
|
|
18
19
|
mcpRuntimeHome,
|
|
@@ -40,6 +41,10 @@ const TOKEN_BUDGET = Object.freeze({
|
|
|
40
41
|
experienceRetrievalMaxItems: 8,
|
|
41
42
|
});
|
|
42
43
|
|
|
44
|
+
function resolvedMcpUserDataDir(value) {
|
|
45
|
+
return typeof value === "string" && value.trim() ? value : defaultUserDataDir();
|
|
46
|
+
}
|
|
47
|
+
|
|
43
48
|
const MCP_REQUIREMENT_REQUIRED = [
|
|
44
49
|
"schemaVersion", "kind", "requirementId", "catalogId", "reason", "capabilities",
|
|
45
50
|
"required", "requiresKey", "priority", "permissions", "alternatives", "unavailablePolicy",
|
|
@@ -144,8 +149,12 @@ function validateMcpPolicy(value) {
|
|
|
144
149
|
|
|
145
150
|
function loadProjectMcpPolicy(cwd) {
|
|
146
151
|
const file = path.join(cwd || process.cwd(), ".agentlas", "mcp-policy.json");
|
|
147
|
-
|
|
148
|
-
|
|
152
|
+
let value;
|
|
153
|
+
try { ({ value } = readJsonFile(file, "MCP policy")); }
|
|
154
|
+
catch (error) {
|
|
155
|
+
if (error && error.code === "ENOENT") return null;
|
|
156
|
+
throw error;
|
|
157
|
+
}
|
|
149
158
|
return validateMcpPolicy(value);
|
|
150
159
|
}
|
|
151
160
|
|
|
@@ -154,9 +163,10 @@ function loadProjectMcpPolicy(cwd) {
|
|
|
154
163
|
* 소스: 프로세스 env + userData/credentials.env + ~/.agentlas/credentials.env.
|
|
155
164
|
*/
|
|
156
165
|
function readCredentialNames(userDataDir, env = process.env) {
|
|
166
|
+
const resolvedUserDataDir = resolvedMcpUserDataDir(userDataDir);
|
|
157
167
|
const names = new Set(Object.keys(env || {}).filter((key) => ENV_RE.test(key) && env[key]));
|
|
158
168
|
const files = [
|
|
159
|
-
path.join(
|
|
169
|
+
path.join(resolvedUserDataDir, "credentials.env"),
|
|
160
170
|
path.join(os.homedir(), ".agentlas", "credentials.env"),
|
|
161
171
|
];
|
|
162
172
|
for (const file of files) {
|
|
@@ -193,7 +203,7 @@ function collectSystemMcpInventory(db, options = {}) {
|
|
|
193
203
|
rows = [];
|
|
194
204
|
registryStatus = "unavailable";
|
|
195
205
|
}
|
|
196
|
-
const credentialNames = readCredentialNames(options.userDataDir
|
|
206
|
+
const credentialNames = readCredentialNames(options.userDataDir, options.env || process.env);
|
|
197
207
|
const inventory = [];
|
|
198
208
|
const seen = new Set();
|
|
199
209
|
for (const row of rows) {
|
|
@@ -279,7 +289,7 @@ function materializeTrustedSystemMcpServer(row, options = {}) {
|
|
|
279
289
|
});
|
|
280
290
|
if (options.createRuntimeHome !== false) {
|
|
281
291
|
Object.defineProperty(server, "mcpRuntimeHome", {
|
|
282
|
-
value: mcpRuntimeHome(options.userDataDir, `${catalogId}\u0000${row.id}`),
|
|
292
|
+
value: mcpRuntimeHome(resolvedMcpUserDataDir(options.userDataDir), `${catalogId}\u0000${row.id}`),
|
|
283
293
|
enumerable: false,
|
|
284
294
|
});
|
|
285
295
|
}
|
package/engine/mcp/plan.cjs
CHANGED
|
@@ -58,7 +58,11 @@ function lexicalRequirementHintIds(request, inventory) {
|
|
|
58
58
|
const text = String(request || "").toLowerCase();
|
|
59
59
|
const ids = [];
|
|
60
60
|
for (const item of inventory) {
|
|
61
|
-
const
|
|
61
|
+
const catalogId = String(item.catalogId || "").trim().toLowerCase();
|
|
62
|
+
const name = String(item.name || "").trim().toLowerCase();
|
|
63
|
+
// String#includes("") is always true. A registry row whose optional display
|
|
64
|
+
// name is empty must not become a hint for every unrelated build request.
|
|
65
|
+
const direct = Boolean((catalogId && text.includes(catalogId)) || (name && text.includes(name)));
|
|
62
66
|
const heuristic = HEURISTIC_GROUPS.some(([nameRe, taskRe]) => nameRe.test(`${item.catalogId} ${item.name}`) && taskRe.test(text));
|
|
63
67
|
if (direct || heuristic) ids.push(item.catalogId);
|
|
64
68
|
}
|
package/engine/mcp/probe.cjs
CHANGED
|
@@ -30,6 +30,7 @@ function probeSystemMcpServerConnection(server, options = {}) {
|
|
|
30
30
|
let abortHandler = null;
|
|
31
31
|
let forceKillTimer = null;
|
|
32
32
|
let childClosed = false;
|
|
33
|
+
let writeMessage = () => false;
|
|
33
34
|
const terminateChild = (signal) => {
|
|
34
35
|
const pid = Number(child?.pid);
|
|
35
36
|
// detached 자식은 프로세스 그룹(-pid)째로 종료해야 패키지 매니저가 띄운
|
|
@@ -63,12 +64,8 @@ function probeSystemMcpServerConnection(server, options = {}) {
|
|
|
63
64
|
if (message.id === 1) {
|
|
64
65
|
if (message.error || !message.result) return finish(false, "initialize_failed");
|
|
65
66
|
initialized = true;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
child.stdin.write(`${JSON.stringify({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} })}\n`);
|
|
69
|
-
} catch {
|
|
70
|
-
finish(false, "connection_failed");
|
|
71
|
-
}
|
|
67
|
+
if (!writeMessage({ jsonrpc: "2.0", method: "notifications/initialized", params: {} })) return;
|
|
68
|
+
writeMessage({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
72
69
|
} else if (message.id === 2 && initialized) {
|
|
73
70
|
finish(
|
|
74
71
|
!message.error && Boolean(message.result),
|
|
@@ -100,8 +97,13 @@ function probeSystemMcpServerConnection(server, options = {}) {
|
|
|
100
97
|
}
|
|
101
98
|
};
|
|
102
99
|
const timer = setTimeout(() => finish(false, "connection_timeout"), timeoutMs);
|
|
100
|
+
const runtimeArgs = parseRuntimeServerArgs(server.args_json);
|
|
101
|
+
if (!runtimeArgs) {
|
|
102
|
+
finish(false, "invalid_runtime_args");
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
103
105
|
try {
|
|
104
|
-
child = spawnImpl(server.command,
|
|
106
|
+
child = spawnImpl(server.command, runtimeArgs, {
|
|
105
107
|
cwd: options.cwd || process.cwd(),
|
|
106
108
|
env: buildMcpChildEnv(options.env || process.env, server.credentialKeyNames || [], {
|
|
107
109
|
runtimeHome: server.mcpRuntimeHome || mcpRuntimeHome(options.userDataDir, server.catalog_id || server.id || server.command),
|
|
@@ -110,6 +112,7 @@ function probeSystemMcpServerConnection(server, options = {}) {
|
|
|
110
112
|
stdio: ["pipe", "pipe", "ignore"],
|
|
111
113
|
});
|
|
112
114
|
child.once("error", () => finish(false, "connection_failed"));
|
|
115
|
+
child.stdin.on?.("error", () => finish(false, "connection_failed"));
|
|
113
116
|
child.once("close", () => {
|
|
114
117
|
childClosed = true;
|
|
115
118
|
if (forceKillTimer) clearTimeout(forceKillTimer);
|
|
@@ -122,7 +125,19 @@ function probeSystemMcpServerConnection(server, options = {}) {
|
|
|
122
125
|
buffer = Buffer.concat([buffer, Buffer.from(chunk)]);
|
|
123
126
|
drain();
|
|
124
127
|
});
|
|
125
|
-
|
|
128
|
+
writeMessage = (message) => {
|
|
129
|
+
if (settled) return false;
|
|
130
|
+
try {
|
|
131
|
+
child.stdin.write(`${JSON.stringify(message)}\n`, (error) => {
|
|
132
|
+
if (error) finish(false, "connection_failed");
|
|
133
|
+
});
|
|
134
|
+
return true;
|
|
135
|
+
} catch {
|
|
136
|
+
finish(false, "connection_failed");
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
writeMessage({
|
|
126
141
|
jsonrpc: "2.0",
|
|
127
142
|
id: 1,
|
|
128
143
|
method: "initialize",
|
|
@@ -131,7 +146,7 @@ function probeSystemMcpServerConnection(server, options = {}) {
|
|
|
131
146
|
capabilities: {},
|
|
132
147
|
clientInfo: { name: "agentlas-terminal-build", version: "1" },
|
|
133
148
|
},
|
|
134
|
-
})
|
|
149
|
+
});
|
|
135
150
|
if (options.signal) {
|
|
136
151
|
abortHandler = () => finish(false, "connection_timeout");
|
|
137
152
|
if (options.signal.aborted) abortHandler();
|
|
@@ -19,6 +19,11 @@ const fs = require("node:fs");
|
|
|
19
19
|
const path = require("node:path");
|
|
20
20
|
const { tableExists, columnExists } = require("../core/db.cjs");
|
|
21
21
|
|
|
22
|
+
const MAX_MEMORY_EVENTS = 32;
|
|
23
|
+
const MAX_MEMORY_CONTENT_CHARS = 900;
|
|
24
|
+
const MAX_MEMORY_LOG_BYTES = 16 * 1024 * 1024;
|
|
25
|
+
const MAX_MEMORY_LOG_RECORD_BYTES = 16 * 1024;
|
|
26
|
+
|
|
22
27
|
// 앱과 동일한 컴파일된 manifest — 빌트인 에이전트 + 메모리 아키텍처 상수.
|
|
23
28
|
let _arch = null;
|
|
24
29
|
function loadArch() {
|
|
@@ -45,13 +50,43 @@ const SECRET_RE = [/\b(?:sk|pk|rk)-[A-Za-z0-9]{16,}/, /AKIA[0-9A-Z]{16}/, /ghp_[
|
|
|
45
50
|
* 이 모듈이 위장하지 않는다.
|
|
46
51
|
*/
|
|
47
52
|
function logCli(projectPath, rec) {
|
|
48
|
-
if (!projectPath) return;
|
|
53
|
+
if (!projectPath) return false;
|
|
54
|
+
let fd = null;
|
|
49
55
|
try {
|
|
50
56
|
const arch = loadArch();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
fs.
|
|
54
|
-
|
|
57
|
+
// Memory curation is not project initialization. Only an existing private
|
|
58
|
+
// .agentlas directory may receive the best-effort decision log.
|
|
59
|
+
const root = fs.realpathSync(path.resolve(projectPath));
|
|
60
|
+
const dir = path.join(root, arch.memoryDir || ".agentlas");
|
|
61
|
+
const dirStat = fs.lstatSync(dir);
|
|
62
|
+
if (!dirStat.isDirectory() || dirStat.isSymbolicLink()) return false;
|
|
63
|
+
if (process.platform !== "win32" && (dirStat.mode & 0o077) !== 0) return false;
|
|
64
|
+
|
|
65
|
+
const line = Buffer.from(JSON.stringify(rec) + "\n", "utf8");
|
|
66
|
+
if (line.length <= 1 || line.length > MAX_MEMORY_LOG_RECORD_BYTES) return false;
|
|
67
|
+
const file = path.join(dir, arch.logFile || "memory-log.jsonl");
|
|
68
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
69
|
+
fd = fs.openSync(
|
|
70
|
+
file,
|
|
71
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND | noFollow,
|
|
72
|
+
0o600,
|
|
73
|
+
);
|
|
74
|
+
const opened = fs.fstatSync(fd);
|
|
75
|
+
const listed = fs.lstatSync(file);
|
|
76
|
+
if (
|
|
77
|
+
!opened.isFile() || opened.nlink !== 1 ||
|
|
78
|
+
!listed.isFile() || listed.isSymbolicLink() || listed.nlink !== 1 ||
|
|
79
|
+
opened.dev !== listed.dev || opened.ino !== listed.ino ||
|
|
80
|
+
opened.size + line.length > MAX_MEMORY_LOG_BYTES
|
|
81
|
+
) return false;
|
|
82
|
+
const written = fs.writeSync(fd, line, 0, line.length, null);
|
|
83
|
+
if (written !== line.length) return false;
|
|
84
|
+
fs.fsyncSync(fd);
|
|
85
|
+
fs.fchmodSync(fd, 0o600);
|
|
86
|
+
const after = fs.fstatSync(fd);
|
|
87
|
+
return after.isFile() && after.nlink === 1 && after.dev === opened.dev && after.ino === opened.ino;
|
|
88
|
+
} catch { return false; }
|
|
89
|
+
finally { if (fd != null) try { fs.closeSync(fd); } catch { /* best effort */ } }
|
|
55
90
|
}
|
|
56
91
|
|
|
57
92
|
function coerceText(v, max) {
|
|
@@ -128,9 +163,9 @@ function curateCliReply(db, text, ctx) {
|
|
|
128
163
|
if (!ctx || !Array.isArray(ctx.curatedMemories) || !memory) return;
|
|
129
164
|
if (!ctx.curatedMemories.some((item) => item.id === memory.id)) ctx.curatedMemories.push(memory);
|
|
130
165
|
};
|
|
131
|
-
for (const ev of events) {
|
|
166
|
+
for (const ev of events.slice(0, MAX_MEMORY_EVENTS)) {
|
|
132
167
|
const content = ev && typeof ev.content === "string" ? ev.content.trim() : "";
|
|
133
|
-
if (!content) continue;
|
|
168
|
+
if (!content || content.length > MAX_MEMORY_CONTENT_CHARS) continue;
|
|
134
169
|
if (ev.sensitivity === "secret" || SECRET_RE.some((re) => re.test(content))) continue;
|
|
135
170
|
const kind = arch.kinds.includes(ev.memory_kind) ? ev.memory_kind : "fact";
|
|
136
171
|
let scope = ev.suggested_scope === "agent_team"
|
|
@@ -146,17 +181,37 @@ function curateCliReply(db, text, ctx) {
|
|
|
146
181
|
const scopedAgentId = scope === "agent_repo" ? (ctx.agentId || null) : null;
|
|
147
182
|
const requestContext = normalizeRequestContext(ev, ctx, ppath);
|
|
148
183
|
try {
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
184
|
+
const memoryId = randomUUID();
|
|
185
|
+
const confidence = ["low", "medium", "high"].includes(ev.confidence) ? ev.confidence : "medium";
|
|
186
|
+
const sensitivity = ["public", "internal", "private"].includes(ev.sensitivity) ? ev.sensitivity : "internal";
|
|
187
|
+
const evidence = Array.isArray(ev.evidence_refs)
|
|
188
|
+
? ev.evidence_refs.filter((item) => typeof item === "string").slice(0, 16).map((item) => item.slice(0, 240))
|
|
189
|
+
: [];
|
|
190
|
+
// One statement owns both the duplicate observation and insertion. A
|
|
191
|
+
// SELECT followed by INSERT allowed two Terminal processes to observe
|
|
192
|
+
// absence and persist the same memory under different UUIDs.
|
|
193
|
+
const inserted = db.prepare(`
|
|
194
|
+
INSERT INTO memory_entries
|
|
195
|
+
(id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at)
|
|
196
|
+
SELECT ?,?,?,?,?,?,?,?,?,?,?,?,NULL,?
|
|
197
|
+
WHERE NOT EXISTS (
|
|
198
|
+
SELECT 1 FROM memory_entries
|
|
199
|
+
WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL
|
|
200
|
+
AND (project_path IS ? OR project_path=?)
|
|
201
|
+
AND (agent_id IS ? OR agent_id=?)
|
|
202
|
+
)
|
|
203
|
+
`).run(
|
|
204
|
+
memoryId, scope, kind, content, ctx.projectId || null, ppath, scopedAgentId, null,
|
|
205
|
+
confidence, sensitivity, JSON.stringify(evidence), JSON.stringify(requestContext), now,
|
|
206
|
+
scope, kind, content.toLowerCase(), ppath, ppath, scopedAgentId, scopedAgentId,
|
|
207
|
+
);
|
|
208
|
+
if ((inserted.changes ?? inserted.rowsAffected ?? 0) > 0) {
|
|
209
|
+
rememberCurated({ id: memoryId, scope, kind, content, confidence, sensitivity, requestContext });
|
|
210
|
+
logCli(ctx.projectPath, { action: "written", scope, kind, content, request_context: requestContext, at: now });
|
|
152
211
|
continue;
|
|
153
212
|
}
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
const sensitivity = ev.sensitivity || "internal";
|
|
157
|
-
db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath, scopedAgentId, null, confidence, sensitivity, JSON.stringify(Array.isArray(ev.evidence_refs) ? ev.evidence_refs : []), JSON.stringify(requestContext), now);
|
|
158
|
-
rememberCurated({ id: memoryId, scope, kind, content, confidence, sensitivity, requestContext });
|
|
159
|
-
logCli(ctx.projectPath, { action: "written", scope, kind, content, request_context: requestContext, at: now });
|
|
213
|
+
const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) AND (agent_id IS ? OR agent_id=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath, scopedAgentId, scopedAgentId);
|
|
214
|
+
if (dup) rememberCurated({ ...dup, requestContext });
|
|
160
215
|
} catch { /* ignore */ }
|
|
161
216
|
}
|
|
162
217
|
return style.sanitizeAssistantText(cleaned);
|
|
@@ -170,4 +225,7 @@ module.exports = {
|
|
|
170
225
|
normalizeRequestContext,
|
|
171
226
|
parseMemoryEventsCli,
|
|
172
227
|
curateCliReply,
|
|
228
|
+
MAX_MEMORY_EVENTS,
|
|
229
|
+
MAX_MEMORY_CONTENT_CHARS,
|
|
230
|
+
MAX_MEMORY_LOG_BYTES,
|
|
173
231
|
};
|
package/engine/oberon/common.cjs
CHANGED
|
@@ -21,22 +21,47 @@ function fail(msg, code) {
|
|
|
21
21
|
throw new OberonFail(String(msg ?? ""), code);
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
function parseFlags(args) {
|
|
24
|
+
// 타입 스키마가 있으면 알 수 없는/중복/값 누락 플래그를 fail-closed 한다.
|
|
25
|
+
// `--key=value`와 위치 인자용 `--` sentinel도 지원한다.
|
|
26
|
+
function parseFlags(args, schema = null) {
|
|
27
27
|
const flags = {};
|
|
28
28
|
const rest = [];
|
|
29
29
|
for (let i = 0; i < args.length; i++) {
|
|
30
|
-
const a = args[i];
|
|
31
|
-
if (a
|
|
32
|
-
|
|
30
|
+
const a = String(args[i]);
|
|
31
|
+
if (a === "--") {
|
|
32
|
+
rest.push(...args.slice(i + 1).map(String));
|
|
33
|
+
break;
|
|
34
|
+
}
|
|
35
|
+
if (!a.startsWith("--")) { rest.push(a); continue; }
|
|
36
|
+
const equals = a.indexOf("=");
|
|
37
|
+
const key = a.slice(2, equals >= 0 ? equals : undefined);
|
|
38
|
+
const inlineValue = equals >= 0 ? a.slice(equals + 1) : null;
|
|
39
|
+
if (!key) fail("Oberon option name is empty");
|
|
40
|
+
if (Object.prototype.hasOwnProperty.call(flags, key)) fail(`Duplicate Oberon option: --${key}`);
|
|
41
|
+
const type = schema && schema[key];
|
|
42
|
+
if (schema && !type) fail(`Unknown Oberon option: --${key}`);
|
|
43
|
+
if (!schema) {
|
|
33
44
|
const next = args[i + 1];
|
|
34
|
-
if (
|
|
35
|
-
else
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
45
|
+
if (inlineValue != null) flags[key] = inlineValue;
|
|
46
|
+
else if (next === undefined || String(next).startsWith("--")) flags[key] = true;
|
|
47
|
+
else { flags[key] = String(next); i += 1; }
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
if (type === "boolean") {
|
|
51
|
+
if (inlineValue != null) fail(`Boolean Oberon option does not take a value: --${key}`);
|
|
52
|
+
flags[key] = true;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (type !== "value") fail(`Invalid Oberon option schema: --${key}`);
|
|
56
|
+
if (inlineValue != null) {
|
|
57
|
+
if (!inlineValue) fail(`Oberon option requires a value: --${key}`);
|
|
58
|
+
flags[key] = inlineValue;
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const next = args[i + 1];
|
|
62
|
+
if (next === undefined || String(next).startsWith("--")) fail(`Oberon option requires a value: --${key}`);
|
|
63
|
+
flags[key] = String(next);
|
|
64
|
+
i += 1;
|
|
40
65
|
}
|
|
41
66
|
return { flags, rest };
|
|
42
67
|
}
|