open-multi-agent-kit 0.90.8 → 0.90.9
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 +22 -0
- package/README.md +7 -1
- package/dist/cli/args.d.ts.map +1 -1
- package/dist/cli/args.js +4 -0
- package/dist/cli/args.js.map +1 -1
- package/dist/commands/doctor-provider-cli.d.ts +33 -0
- package/dist/commands/doctor-provider-cli.d.ts.map +1 -0
- package/dist/commands/doctor-provider-cli.js +215 -0
- package/dist/commands/doctor-provider-cli.js.map +1 -0
- package/dist/commands/doctor-provider-transport.d.ts +35 -0
- package/dist/commands/doctor-provider-transport.d.ts.map +1 -0
- package/dist/commands/doctor-provider-transport.js +185 -0
- package/dist/commands/doctor-provider-transport.js.map +1 -0
- package/dist/commands/doctor-provider.d.ts +121 -7
- package/dist/commands/doctor-provider.d.ts.map +1 -1
- package/dist/commands/doctor-provider.js +1295 -102
- package/dist/commands/doctor-provider.js.map +1 -1
- package/dist/commands/session-doctor-cli.d.ts +43 -0
- package/dist/commands/session-doctor-cli.d.ts.map +1 -0
- package/dist/commands/session-doctor-cli.js +494 -0
- package/dist/commands/session-doctor-cli.js.map +1 -0
- package/dist/commands/verify-ci.d.ts +21 -0
- package/dist/commands/verify-ci.d.ts.map +1 -0
- package/dist/commands/verify-ci.js +66 -0
- package/dist/commands/verify-ci.js.map +1 -0
- package/dist/core/agent-session-services.d.ts +3 -0
- package/dist/core/agent-session-services.d.ts.map +1 -1
- package/dist/core/agent-session-services.js +3 -0
- package/dist/core/agent-session-services.js.map +1 -1
- package/dist/core/agent-session.d.ts +113 -2
- package/dist/core/agent-session.d.ts.map +1 -1
- package/dist/core/agent-session.js +993 -72
- package/dist/core/agent-session.js.map +1 -1
- package/dist/core/agent-tool-settings.d.ts +35 -0
- package/dist/core/agent-tool-settings.d.ts.map +1 -0
- package/dist/core/agent-tool-settings.js +155 -0
- package/dist/core/agent-tool-settings.js.map +1 -0
- package/dist/core/atomic-session-file.d.ts +14 -0
- package/dist/core/atomic-session-file.d.ts.map +1 -0
- package/dist/core/atomic-session-file.js +104 -0
- package/dist/core/atomic-session-file.js.map +1 -0
- package/dist/core/compaction/compaction.d.ts +6 -0
- package/dist/core/compaction/compaction.d.ts.map +1 -1
- package/dist/core/compaction/compaction.js +12 -1
- package/dist/core/compaction/compaction.js.map +1 -1
- package/dist/core/compaction/hysteresis.d.ts +27 -0
- package/dist/core/compaction/hysteresis.d.ts.map +1 -0
- package/dist/core/compaction/hysteresis.js +84 -0
- package/dist/core/compaction/hysteresis.js.map +1 -0
- package/dist/core/compaction/index.d.ts +2 -0
- package/dist/core/compaction/index.d.ts.map +1 -1
- package/dist/core/compaction/index.js +2 -0
- package/dist/core/compaction/index.js.map +1 -1
- package/dist/core/compaction/transaction.d.ts +172 -0
- package/dist/core/compaction/transaction.d.ts.map +1 -0
- package/dist/core/compaction/transaction.js +824 -0
- package/dist/core/compaction/transaction.js.map +1 -0
- package/dist/core/context-budget-governor-v2.d.ts +2 -1
- package/dist/core/context-budget-governor-v2.d.ts.map +1 -1
- package/dist/core/context-budget-governor-v2.js +2 -1
- package/dist/core/context-budget-governor-v2.js.map +1 -1
- package/dist/core/context-budget-reserved-tokens.d.ts +110 -0
- package/dist/core/context-budget-reserved-tokens.d.ts.map +1 -0
- package/dist/core/context-budget-reserved-tokens.js +188 -0
- package/dist/core/context-budget-reserved-tokens.js.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts +133 -0
- package/dist/core/context-budget-v2-cache-invalidation.d.ts.map +1 -0
- package/dist/core/context-budget-v2-cache-invalidation.js +452 -0
- package/dist/core/context-budget-v2-cache-invalidation.js.map +1 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts +3 -0
- package/dist/core/context-budget-v2-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-keys.js +6 -0
- package/dist/core/context-budget-v2-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.d.ts +4 -0
- package/dist/core/context-budget-v2-cache-provider.d.ts.map +1 -1
- package/dist/core/context-budget-v2-cache-provider.js +19 -0
- package/dist/core/context-budget-v2-cache-provider.js.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.d.ts.map +1 -1
- package/dist/core/context-budget-v2-plan-cache-keys.js +1 -0
- package/dist/core/context-budget-v2-plan-cache-keys.js.map +1 -1
- package/dist/core/context-budget-v2-planner.d.ts.map +1 -1
- package/dist/core/context-budget-v2-planner.js +1 -0
- package/dist/core/context-budget-v2-planner.js.map +1 -1
- package/dist/core/context-budget-v2-types.d.ts +4 -0
- package/dist/core/context-budget-v2-types.d.ts.map +1 -1
- package/dist/core/context-budget-v2-types.js.map +1 -1
- package/dist/core/durable-file-directory.d.ts +4 -0
- package/dist/core/durable-file-directory.d.ts.map +1 -0
- package/dist/core/durable-file-directory.js +76 -0
- package/dist/core/durable-file-directory.js.map +1 -0
- package/dist/core/durable-file-identity.d.ts +26 -0
- package/dist/core/durable-file-identity.d.ts.map +1 -0
- package/dist/core/durable-file-identity.js +118 -0
- package/dist/core/durable-file-identity.js.map +1 -0
- package/dist/core/durable-file-io.d.ts +29 -0
- package/dist/core/durable-file-io.d.ts.map +1 -0
- package/dist/core/durable-file-io.js +179 -0
- package/dist/core/durable-file-io.js.map +1 -0
- package/dist/core/durable-file-lock-cleanup.d.ts +12 -0
- package/dist/core/durable-file-lock-cleanup.d.ts.map +1 -0
- package/dist/core/durable-file-lock-cleanup.js +39 -0
- package/dist/core/durable-file-lock-cleanup.js.map +1 -0
- package/dist/core/durable-file-lock-owner.d.ts +18 -0
- package/dist/core/durable-file-lock-owner.d.ts.map +1 -0
- package/dist/core/durable-file-lock-owner.js +78 -0
- package/dist/core/durable-file-lock-owner.js.map +1 -0
- package/dist/core/durable-file-lock-storage.d.ts +35 -0
- package/dist/core/durable-file-lock-storage.d.ts.map +1 -0
- package/dist/core/durable-file-lock-storage.js +263 -0
- package/dist/core/durable-file-lock-storage.js.map +1 -0
- package/dist/core/durable-file-mode.d.ts +8 -0
- package/dist/core/durable-file-mode.d.ts.map +1 -0
- package/dist/core/durable-file-mode.js +41 -0
- package/dist/core/durable-file-mode.js.map +1 -0
- package/dist/core/extensions/loader.d.ts +12 -0
- package/dist/core/extensions/loader.d.ts.map +1 -1
- package/dist/core/extensions/loader.js +46 -37
- package/dist/core/extensions/loader.js.map +1 -1
- package/dist/core/index.d.ts +6 -2
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -1
- package/dist/core/model-resolver.d.ts.map +1 -1
- package/dist/core/model-resolver.js +1 -1
- package/dist/core/model-resolver.js.map +1 -1
- package/dist/core/redaction.d.ts +8 -0
- package/dist/core/redaction.d.ts.map +1 -0
- package/dist/core/redaction.js +31 -0
- package/dist/core/redaction.js.map +1 -0
- package/dist/core/resource-loader.d.ts.map +1 -1
- package/dist/core/resource-loader.js +36 -0
- package/dist/core/resource-loader.js.map +1 -1
- package/dist/core/run-journal-store.d.ts +53 -0
- package/dist/core/run-journal-store.d.ts.map +1 -0
- package/dist/core/run-journal-store.js +278 -0
- package/dist/core/run-journal-store.js.map +1 -0
- package/dist/core/run-journal.d.ts +172 -0
- package/dist/core/run-journal.d.ts.map +1 -0
- package/dist/core/run-journal.js +1053 -0
- package/dist/core/run-journal.js.map +1 -0
- package/dist/core/sdk.d.ts +7 -0
- package/dist/core/sdk.d.ts.map +1 -1
- package/dist/core/sdk.js +94 -3
- package/dist/core/sdk.js.map +1 -1
- package/dist/core/session-doctor-plan.d.ts +128 -0
- package/dist/core/session-doctor-plan.d.ts.map +1 -0
- package/dist/core/session-doctor-plan.js +833 -0
- package/dist/core/session-doctor-plan.js.map +1 -0
- package/dist/core/session-file-compaction.d.ts +6 -0
- package/dist/core/session-file-compaction.d.ts.map +1 -0
- package/dist/core/session-file-compaction.js +169 -0
- package/dist/core/session-file-compaction.js.map +1 -0
- package/dist/core/session-file-head.d.ts +10 -0
- package/dist/core/session-file-head.d.ts.map +1 -0
- package/dist/core/session-file-head.js +78 -0
- package/dist/core/session-file-head.js.map +1 -0
- package/dist/core/session-file-persistence.d.ts +30 -0
- package/dist/core/session-file-persistence.d.ts.map +1 -0
- package/dist/core/session-file-persistence.js +106 -0
- package/dist/core/session-file-persistence.js.map +1 -0
- package/dist/core/session-integrity.d.ts +43 -0
- package/dist/core/session-integrity.d.ts.map +1 -0
- package/dist/core/session-integrity.js +420 -0
- package/dist/core/session-integrity.js.map +1 -0
- package/dist/core/session-manager.d.ts +24 -4
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +242 -125
- package/dist/core/session-manager.js.map +1 -1
- package/dist/core/session-owner-lease.d.ts +22 -0
- package/dist/core/session-owner-lease.d.ts.map +1 -0
- package/dist/core/session-owner-lease.js +176 -0
- package/dist/core/session-owner-lease.js.map +1 -0
- package/dist/core/session-path-policy.d.ts +132 -0
- package/dist/core/session-path-policy.d.ts.map +1 -0
- package/dist/core/session-path-policy.js +780 -0
- package/dist/core/session-path-policy.js.map +1 -0
- package/dist/core/session-repair-plan.d.ts +49 -0
- package/dist/core/session-repair-plan.d.ts.map +1 -0
- package/dist/core/session-repair-plan.js +202 -0
- package/dist/core/session-repair-plan.js.map +1 -0
- package/dist/core/session-termination.d.ts +100 -0
- package/dist/core/session-termination.d.ts.map +1 -0
- package/dist/core/session-termination.js +403 -0
- package/dist/core/session-termination.js.map +1 -0
- package/dist/core/settings-manager.d.ts +22 -0
- package/dist/core/settings-manager.d.ts.map +1 -1
- package/dist/core/settings-manager.js +37 -5
- package/dist/core/settings-manager.js.map +1 -1
- package/dist/core/system-prompt.d.ts.map +1 -1
- package/dist/core/system-prompt.js +1 -1
- package/dist/core/system-prompt.js.map +1 -1
- package/dist/core/verified-bash-adapter.d.ts +36 -0
- package/dist/core/verified-bash-adapter.d.ts.map +1 -0
- package/dist/core/verified-bash-adapter.js +90 -0
- package/dist/core/verified-bash-adapter.js.map +1 -0
- package/dist/guardrails/command-redaction.d.ts +32 -0
- package/dist/guardrails/command-redaction.d.ts.map +1 -0
- package/dist/guardrails/command-redaction.js +776 -0
- package/dist/guardrails/command-redaction.js.map +1 -0
- package/dist/guardrails/evidence-attestation.d.ts +31 -0
- package/dist/guardrails/evidence-attestation.d.ts.map +1 -0
- package/dist/guardrails/evidence-attestation.js +178 -0
- package/dist/guardrails/evidence-attestation.js.map +1 -0
- package/dist/guardrails/evidence-receipt-store.d.ts +33 -0
- package/dist/guardrails/evidence-receipt-store.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt-store.js +446 -0
- package/dist/guardrails/evidence-receipt-store.js.map +1 -0
- package/dist/guardrails/evidence-receipt.d.ts +58 -0
- package/dist/guardrails/evidence-receipt.d.ts.map +1 -0
- package/dist/guardrails/evidence-receipt.js +389 -0
- package/dist/guardrails/evidence-receipt.js.map +1 -0
- package/dist/guardrails/evidence-system.d.ts +39 -15
- package/dist/guardrails/evidence-system.d.ts.map +1 -1
- package/dist/guardrails/evidence-system.js +255 -97
- package/dist/guardrails/evidence-system.js.map +1 -1
- package/dist/guardrails/replay-ledger-io.d.ts +6 -0
- package/dist/guardrails/replay-ledger-io.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-io.js +42 -0
- package/dist/guardrails/replay-ledger-io.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts +32 -0
- package/dist/guardrails/replay-ledger-lock-owner.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock-owner.js +138 -0
- package/dist/guardrails/replay-ledger-lock-owner.js.map +1 -0
- package/dist/guardrails/replay-ledger-lock.d.ts +24 -0
- package/dist/guardrails/replay-ledger-lock.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-lock.js +135 -0
- package/dist/guardrails/replay-ledger-lock.js.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts +24 -0
- package/dist/guardrails/replay-ledger-mutation-gate.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js +205 -0
- package/dist/guardrails/replay-ledger-mutation-gate.js.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts +2 -0
- package/dist/guardrails/replay-ledger-quarantine.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-quarantine.js +19 -0
- package/dist/guardrails/replay-ledger-quarantine.js.map +1 -0
- package/dist/guardrails/replay-ledger-store.d.ts +32 -0
- package/dist/guardrails/replay-ledger-store.d.ts.map +1 -0
- package/dist/guardrails/replay-ledger-store.js +211 -0
- package/dist/guardrails/replay-ledger-store.js.map +1 -0
- package/dist/guardrails/shell-command-name-glob.d.ts +3 -0
- package/dist/guardrails/shell-command-name-glob.d.ts.map +1 -0
- package/dist/guardrails/shell-command-name-glob.js +233 -0
- package/dist/guardrails/shell-command-name-glob.js.map +1 -0
- package/dist/guardrails/shell-command-option.d.ts +7 -0
- package/dist/guardrails/shell-command-option.d.ts.map +1 -0
- package/dist/guardrails/shell-command-option.js +24 -0
- package/dist/guardrails/shell-command-option.js.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts +29 -0
- package/dist/guardrails/shell-command-tokenizer.d.ts.map +1 -0
- package/dist/guardrails/shell-command-tokenizer.js +145 -0
- package/dist/guardrails/shell-command-tokenizer.js.map +1 -0
- package/dist/guardrails/shell-command-wrapper.d.ts +18 -0
- package/dist/guardrails/shell-command-wrapper.d.ts.map +1 -0
- package/dist/guardrails/shell-command-wrapper.js +42 -0
- package/dist/guardrails/shell-command-wrapper.js.map +1 -0
- package/dist/guardrails/verified-executor.d.ts +65 -0
- package/dist/guardrails/verified-executor.d.ts.map +1 -0
- package/dist/guardrails/verified-executor.js +171 -0
- package/dist/guardrails/verified-executor.js.map +1 -0
- package/dist/guardrails/workspace-fingerprint.d.ts +23 -0
- package/dist/guardrails/workspace-fingerprint.d.ts.map +1 -0
- package/dist/guardrails/workspace-fingerprint.js +634 -0
- package/dist/guardrails/workspace-fingerprint.js.map +1 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -1
- package/dist/index.js.map +1 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +12 -0
- package/dist/main.js.map +1 -1
- package/dist/modes/interactive/interactive-mode.d.ts +3 -0
- package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-mode.js +59 -9
- package/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/dist/modes/print-mode.d.ts.map +1 -1
- package/dist/modes/print-mode.js +43 -2
- package/dist/modes/print-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-mode.js +23 -5
- package/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +3 -0
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/dist/types/evidence.d.ts +211 -1
- package/dist/types/evidence.d.ts.map +1 -1
- package/dist/types/evidence.js.map +1 -1
- package/dist/verify-ci.d.ts +3 -0
- package/dist/verify-ci.d.ts.map +1 -0
- package/dist/verify-ci.js +30 -0
- package/dist/verify-ci.js.map +1 -0
- package/docs/compaction.md +38 -4
- package/docs/extensions.md +2 -0
- package/docs/sdk.md +238 -1
- package/docs/settings.md +50 -2
- package/docs/usage.md +38 -0
- package/examples/extensions/custom-provider-anthropic/package-lock.json +5 -5
- package/examples/extensions/custom-provider-anthropic/package.json +1 -1
- package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
- package/examples/extensions/gondolin/package-lock.json +2 -2
- package/examples/extensions/gondolin/package.json +1 -1
- package/examples/extensions/sandbox/package-lock.json +2 -2
- package/examples/extensions/sandbox/package.json +1 -1
- package/examples/extensions/with-deps/package-lock.json +4 -4
- package/examples/extensions/with-deps/package.json +1 -1
- package/npm-shrinkwrap.json +12 -12
- package/package.json +4 -4
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, lstatSync, mkdirSync, rmdirSync } from "node:fs";
|
|
3
|
+
import { hostname, tmpdir, userInfo } from "node:os";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { ensureDurableDirectorySync, fsyncDirectorySync, writeExclusiveFileDurablySync } from "./durable-file-io.js";
|
|
6
|
+
import { createDurableFileLockRetirement, retireDurableFileLockSync, } from "./durable-file-lock-cleanup.js";
|
|
7
|
+
import { fileErrorCode, readDurableFileLockOwnerSync, sameDurableFileLockOwner, } from "./durable-file-lock-owner.js";
|
|
8
|
+
export class DurableFileLockBusyError extends Error {
|
|
9
|
+
name = "DurableFileLockBusyError";
|
|
10
|
+
constructor() {
|
|
11
|
+
super("Durable file lock is held by another live process");
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export class DurableFileLockOwnershipError extends Error {
|
|
15
|
+
name = "DurableFileLockOwnershipError";
|
|
16
|
+
constructor() {
|
|
17
|
+
super("Durable file lock ownership changed before retirement");
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const USER = userInfo();
|
|
21
|
+
const USER_KEY = USER.uid >= 0 ? String(USER.uid) : createHash("sha256").update(USER.username).digest("hex").slice(0, 16);
|
|
22
|
+
const ROOT = join(tmpdir(), `omk-persistence-${USER_KEY}`);
|
|
23
|
+
const WAITER = new Int32Array(new SharedArrayBuffer(4));
|
|
24
|
+
const retryableLocks = [];
|
|
25
|
+
export function getDurableFileLockRoot() {
|
|
26
|
+
let value;
|
|
27
|
+
try {
|
|
28
|
+
value = lstatSync(ROOT);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
if (fileErrorCode(error) !== "ENOENT")
|
|
32
|
+
throw error;
|
|
33
|
+
ensureDurableDirectorySync(ROOT);
|
|
34
|
+
value = lstatSync(ROOT);
|
|
35
|
+
}
|
|
36
|
+
if (!value.isDirectory() || value.isSymbolicLink())
|
|
37
|
+
throw new Error("Persistence lock root is not a real directory");
|
|
38
|
+
if (typeof process.getuid === "function" && value.uid !== process.getuid()) {
|
|
39
|
+
throw new Error("Persistence lock root is not owned by the current user");
|
|
40
|
+
}
|
|
41
|
+
if ((value.mode & 0o7777) !== 0o700) {
|
|
42
|
+
chmodSync(ROOT, 0o700);
|
|
43
|
+
value = lstatSync(ROOT);
|
|
44
|
+
if ((value.mode & 0o7777) !== 0o700)
|
|
45
|
+
throw new Error("Persistence lock root mode is not private");
|
|
46
|
+
}
|
|
47
|
+
return ROOT;
|
|
48
|
+
}
|
|
49
|
+
function ownerPath(path) {
|
|
50
|
+
return join(path, "owner.json");
|
|
51
|
+
}
|
|
52
|
+
function processIsLive(pid) {
|
|
53
|
+
try {
|
|
54
|
+
process.kill(pid, 0);
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
if (!(error instanceof Error))
|
|
59
|
+
throw error;
|
|
60
|
+
return fileErrorCode(error) !== "ESRCH";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
function removeOwnedLock(lock) {
|
|
64
|
+
if (lock.retirement.retiredPath === undefined) {
|
|
65
|
+
let current;
|
|
66
|
+
try {
|
|
67
|
+
current = readDurableFileLockOwnerSync(lock.path);
|
|
68
|
+
}
|
|
69
|
+
catch (error) {
|
|
70
|
+
if (fileErrorCode(error) === "ENOENT")
|
|
71
|
+
throw new DurableFileLockOwnershipError();
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
if (!sameDurableFileLockOwner(current, lock.expectedOwner))
|
|
75
|
+
throw new DurableFileLockOwnershipError();
|
|
76
|
+
}
|
|
77
|
+
retireDurableFileLockSync(lock.path, lock.retirement);
|
|
78
|
+
}
|
|
79
|
+
export function releaseDurableFileLockPathsSync(held, selected) {
|
|
80
|
+
for (const lock of [...selected].reverse()) {
|
|
81
|
+
removeOwnedLock(lock);
|
|
82
|
+
const index = held.indexOf(lock);
|
|
83
|
+
if (index >= 0)
|
|
84
|
+
held.splice(index, 1);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
export function retainRetryableDurableFileLocks(locks) {
|
|
88
|
+
for (const lock of locks)
|
|
89
|
+
if (!retryableLocks.includes(lock))
|
|
90
|
+
retryableLocks.push(lock);
|
|
91
|
+
}
|
|
92
|
+
export function assertDurableFileLockPathsOwnedSync(locks) {
|
|
93
|
+
for (const lock of locks) {
|
|
94
|
+
if (retryableLocks.includes(lock) || lock.retirement.retiredPath !== undefined)
|
|
95
|
+
throw new DurableFileLockOwnershipError();
|
|
96
|
+
let current;
|
|
97
|
+
try {
|
|
98
|
+
current = readDurableFileLockOwnerSync(lock.path);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (fileErrorCode(error) === "ENOENT")
|
|
102
|
+
throw new DurableFileLockOwnershipError();
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
if (!sameDurableFileLockOwner(current, lock.expectedOwner))
|
|
106
|
+
throw new DurableFileLockOwnershipError();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
export function drainRetryableDurableFileLocksSync() {
|
|
110
|
+
releaseDurableFileLockPathsSync(retryableLocks, retryableLocks);
|
|
111
|
+
}
|
|
112
|
+
function ownerObservation(lockPath) {
|
|
113
|
+
try {
|
|
114
|
+
return readDurableFileLockOwnerSync(lockPath);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
if (!(error instanceof Error))
|
|
118
|
+
throw error;
|
|
119
|
+
if (fileErrorCode(error) === "ENOENT") {
|
|
120
|
+
try {
|
|
121
|
+
lstatSync(lockPath);
|
|
122
|
+
}
|
|
123
|
+
catch (pathError) {
|
|
124
|
+
if (!(pathError instanceof Error))
|
|
125
|
+
throw pathError;
|
|
126
|
+
if (fileErrorCode(pathError) === "ENOENT")
|
|
127
|
+
return { status: "absent" };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
return { status: "unknown" };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function reclaimGatePath(lockPath, observed) {
|
|
134
|
+
const ownerKey = createHash("sha256")
|
|
135
|
+
.update(`${observed.owner.token}\0${observed.dev}\0${observed.ino}`)
|
|
136
|
+
.digest("hex")
|
|
137
|
+
.slice(0, 24);
|
|
138
|
+
return `${lockPath}.reclaim-${ownerKey}.lock`;
|
|
139
|
+
}
|
|
140
|
+
export function durableFileLockPath(scope, key) {
|
|
141
|
+
if (!/^[a-z-]+$/u.test(scope))
|
|
142
|
+
throw new TypeError("Durable file lock scope is invalid");
|
|
143
|
+
return join(getDurableFileLockRoot(), `${scope}-${key}.lock`);
|
|
144
|
+
}
|
|
145
|
+
export function acquireDurableFileLockPathSync(key, path, deadline) {
|
|
146
|
+
while (true) {
|
|
147
|
+
const token = randomUUID();
|
|
148
|
+
try {
|
|
149
|
+
mkdirSync(path, { mode: 0o700 });
|
|
150
|
+
}
|
|
151
|
+
catch (error) {
|
|
152
|
+
if (fileErrorCode(error) !== "EEXIST")
|
|
153
|
+
throw error;
|
|
154
|
+
if (inspectDurableFileLockPathSync(path, deadline).status === "absent")
|
|
155
|
+
continue;
|
|
156
|
+
if (Date.now() >= deadline)
|
|
157
|
+
throw new DurableFileLockBusyError();
|
|
158
|
+
Atomics.wait(WAITER, 0, 0, Math.min(10, deadline - Date.now()));
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
const directory = lstatSync(path, { bigint: true });
|
|
162
|
+
if (!directory.isDirectory() || directory.isSymbolicLink())
|
|
163
|
+
throw new DurableFileLockOwnershipError();
|
|
164
|
+
const expectedOwner = Object.freeze({
|
|
165
|
+
owner: Object.freeze({ pid: process.pid, host: hostname(), token }),
|
|
166
|
+
dev: directory.dev.toString(),
|
|
167
|
+
ino: directory.ino.toString(),
|
|
168
|
+
});
|
|
169
|
+
const lock = {
|
|
170
|
+
key,
|
|
171
|
+
path,
|
|
172
|
+
token,
|
|
173
|
+
expectedOwner,
|
|
174
|
+
retirement: createDurableFileLockRetirement(),
|
|
175
|
+
};
|
|
176
|
+
let ownerWritten = false;
|
|
177
|
+
try {
|
|
178
|
+
fsyncDirectorySync(dirname(path));
|
|
179
|
+
writeExclusiveFileDurablySync(ownerPath(path), Buffer.from(JSON.stringify(expectedOwner.owner), "utf8"));
|
|
180
|
+
ownerWritten = true;
|
|
181
|
+
assertDurableFileLockPathsOwnedSync([lock]);
|
|
182
|
+
return lock;
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
let cleanupError;
|
|
186
|
+
try {
|
|
187
|
+
if (ownerWritten)
|
|
188
|
+
releaseDurableFileLockPathsSync([lock], [lock]);
|
|
189
|
+
else {
|
|
190
|
+
rmdirSync(path);
|
|
191
|
+
fsyncDirectorySync(dirname(path));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (failure) {
|
|
195
|
+
cleanupError = failure instanceof Error ? failure : new Error(String(failure));
|
|
196
|
+
if (ownerWritten && !(cleanupError instanceof DurableFileLockOwnershipError))
|
|
197
|
+
retainRetryableDurableFileLocks([lock]);
|
|
198
|
+
}
|
|
199
|
+
if (cleanupError)
|
|
200
|
+
throw new AggregateError([error, cleanupError], "Durable file lock creation cleanup failed");
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
function reclaim(lockPath, observed, deadline) {
|
|
206
|
+
const gate = acquireDurableFileLockPathSync(`reclaim:${observed.owner.token}`, reclaimGatePath(lockPath, observed), deadline);
|
|
207
|
+
const stale = {
|
|
208
|
+
key: `stale:${observed.owner.token}`,
|
|
209
|
+
path: lockPath,
|
|
210
|
+
token: observed.owner.token,
|
|
211
|
+
expectedOwner: observed,
|
|
212
|
+
retirement: createDurableFileLockRetirement(),
|
|
213
|
+
};
|
|
214
|
+
let failure;
|
|
215
|
+
let retired = false;
|
|
216
|
+
try {
|
|
217
|
+
const current = readDurableFileLockOwnerSync(lockPath);
|
|
218
|
+
if (sameDurableFileLockOwner(current, observed)) {
|
|
219
|
+
removeOwnedLock(stale);
|
|
220
|
+
retired = true;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (fileErrorCode(error) === "ENOENT") {
|
|
225
|
+
try {
|
|
226
|
+
lstatSync(lockPath);
|
|
227
|
+
}
|
|
228
|
+
catch (pathError) {
|
|
229
|
+
if (fileErrorCode(pathError) === "ENOENT")
|
|
230
|
+
retired = true;
|
|
231
|
+
else
|
|
232
|
+
failure = pathError instanceof Error ? pathError : new Error(String(pathError));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (!retired && failure === undefined)
|
|
236
|
+
failure = error instanceof Error ? error : new Error(String(error));
|
|
237
|
+
if (stale.retirement.retiredPath !== undefined)
|
|
238
|
+
retainRetryableDurableFileLocks([stale]);
|
|
239
|
+
}
|
|
240
|
+
try {
|
|
241
|
+
releaseDurableFileLockPathsSync([gate], [gate]);
|
|
242
|
+
}
|
|
243
|
+
catch (error) {
|
|
244
|
+
retainRetryableDurableFileLocks([gate]);
|
|
245
|
+
const cleanupError = error instanceof Error ? error : new Error(String(error));
|
|
246
|
+
failure = failure ? new AggregateError([failure, cleanupError], "Durable lock reclaim failed") : cleanupError;
|
|
247
|
+
}
|
|
248
|
+
if (failure)
|
|
249
|
+
throw failure;
|
|
250
|
+
return retired;
|
|
251
|
+
}
|
|
252
|
+
export function inspectDurableFileLockPathSync(lockPath, deadline) {
|
|
253
|
+
const observed = ownerObservation(lockPath);
|
|
254
|
+
if ("status" in observed)
|
|
255
|
+
return observed;
|
|
256
|
+
if (observed.owner.host !== hostname())
|
|
257
|
+
return { status: "unknown" };
|
|
258
|
+
if (processIsLive(observed.owner.pid)) {
|
|
259
|
+
return { status: "live", pid: observed.owner.pid, host: observed.owner.host };
|
|
260
|
+
}
|
|
261
|
+
return reclaim(lockPath, observed, deadline) ? { status: "absent" } : { status: "unknown" };
|
|
262
|
+
}
|
|
263
|
+
//# sourceMappingURL=durable-file-lock-storage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-file-lock-storage.js","sourceRoot":"","sources":["../../src/core/durable-file-lock-storage.ts"],"names":[],"mappings":"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","sourcesContent":["import { createHash, randomUUID } from \"node:crypto\";\nimport { chmodSync, lstatSync, mkdirSync, rmdirSync } from \"node:fs\";\nimport { hostname, tmpdir, userInfo } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport { ensureDurableDirectorySync, fsyncDirectorySync, writeExclusiveFileDurablySync } from \"./durable-file-io.ts\";\nimport {\n\tcreateDurableFileLockRetirement,\n\ttype DurableFileLockRetirement,\n\tretireDurableFileLockSync,\n} from \"./durable-file-lock-cleanup.ts\";\nimport {\n\ttype DurableFileLockOwnerSnapshot,\n\tfileErrorCode,\n\treadDurableFileLockOwnerSync,\n\tsameDurableFileLockOwner,\n} from \"./durable-file-lock-owner.ts\";\n\nexport type DurableFileLockPathObservation =\n\t| { readonly status: \"absent\" }\n\t| { readonly status: \"live\"; readonly pid: number; readonly host: string }\n\t| { readonly status: \"unknown\" };\n\nexport interface HeldDurableFileLock {\n\treadonly key: string;\n\treadonly path: string;\n\treadonly token: string;\n\treadonly expectedOwner: DurableFileLockOwnerSnapshot;\n\treadonly retirement: DurableFileLockRetirement;\n}\n\nexport class DurableFileLockBusyError extends Error {\n\toverride readonly name = \"DurableFileLockBusyError\";\n\tconstructor() {\n\t\tsuper(\"Durable file lock is held by another live process\");\n\t}\n}\n\nexport class DurableFileLockOwnershipError extends Error {\n\toverride readonly name = \"DurableFileLockOwnershipError\";\n\tconstructor() {\n\t\tsuper(\"Durable file lock ownership changed before retirement\");\n\t}\n}\n\nconst USER = userInfo();\nconst USER_KEY =\n\tUSER.uid >= 0 ? String(USER.uid) : createHash(\"sha256\").update(USER.username).digest(\"hex\").slice(0, 16);\nconst ROOT = join(tmpdir(), `omk-persistence-${USER_KEY}`);\nconst WAITER = new Int32Array(new SharedArrayBuffer(4));\nconst retryableLocks: HeldDurableFileLock[] = [];\n\nexport function getDurableFileLockRoot(): string {\n\tlet value: ReturnType<typeof lstatSync>;\n\ttry {\n\t\tvalue = lstatSync(ROOT);\n\t} catch (error) {\n\t\tif (fileErrorCode(error) !== \"ENOENT\") throw error;\n\t\tensureDurableDirectorySync(ROOT);\n\t\tvalue = lstatSync(ROOT);\n\t}\n\tif (!value.isDirectory() || value.isSymbolicLink()) throw new Error(\"Persistence lock root is not a real directory\");\n\tif (typeof process.getuid === \"function\" && value.uid !== process.getuid()) {\n\t\tthrow new Error(\"Persistence lock root is not owned by the current user\");\n\t}\n\tif ((value.mode & 0o7777) !== 0o700) {\n\t\tchmodSync(ROOT, 0o700);\n\t\tvalue = lstatSync(ROOT);\n\t\tif ((value.mode & 0o7777) !== 0o700) throw new Error(\"Persistence lock root mode is not private\");\n\t}\n\treturn ROOT;\n}\n\nfunction ownerPath(path: string): string {\n\treturn join(path, \"owner.json\");\n}\n\nfunction processIsLive(pid: number): boolean {\n\ttry {\n\t\tprocess.kill(pid, 0);\n\t\treturn true;\n\t} catch (error) {\n\t\tif (!(error instanceof Error)) throw error;\n\t\treturn fileErrorCode(error) !== \"ESRCH\";\n\t}\n}\n\nfunction removeOwnedLock(lock: HeldDurableFileLock): void {\n\tif (lock.retirement.retiredPath === undefined) {\n\t\tlet current: DurableFileLockOwnerSnapshot;\n\t\ttry {\n\t\t\tcurrent = readDurableFileLockOwnerSync(lock.path);\n\t\t} catch (error) {\n\t\t\tif (fileErrorCode(error) === \"ENOENT\") throw new DurableFileLockOwnershipError();\n\t\t\tthrow error;\n\t\t}\n\t\tif (!sameDurableFileLockOwner(current, lock.expectedOwner)) throw new DurableFileLockOwnershipError();\n\t}\n\tretireDurableFileLockSync(lock.path, lock.retirement);\n}\n\nexport function releaseDurableFileLockPathsSync(\n\theld: HeldDurableFileLock[],\n\tselected: readonly HeldDurableFileLock[],\n): void {\n\tfor (const lock of [...selected].reverse()) {\n\t\tremoveOwnedLock(lock);\n\t\tconst index = held.indexOf(lock);\n\t\tif (index >= 0) held.splice(index, 1);\n\t}\n}\n\nexport function retainRetryableDurableFileLocks(locks: readonly HeldDurableFileLock[]): void {\n\tfor (const lock of locks) if (!retryableLocks.includes(lock)) retryableLocks.push(lock);\n}\n\nexport function assertDurableFileLockPathsOwnedSync(locks: readonly HeldDurableFileLock[]): void {\n\tfor (const lock of locks) {\n\t\tif (retryableLocks.includes(lock) || lock.retirement.retiredPath !== undefined)\n\t\t\tthrow new DurableFileLockOwnershipError();\n\t\tlet current: DurableFileLockOwnerSnapshot;\n\t\ttry {\n\t\t\tcurrent = readDurableFileLockOwnerSync(lock.path);\n\t\t} catch (error) {\n\t\t\tif (fileErrorCode(error) === \"ENOENT\") throw new DurableFileLockOwnershipError();\n\t\t\tthrow error;\n\t\t}\n\t\tif (!sameDurableFileLockOwner(current, lock.expectedOwner)) throw new DurableFileLockOwnershipError();\n\t}\n}\n\nexport function drainRetryableDurableFileLocksSync(): void {\n\treleaseDurableFileLockPathsSync(retryableLocks, retryableLocks);\n}\n\nfunction ownerObservation(lockPath: string): DurableFileLockOwnerSnapshot | DurableFileLockPathObservation {\n\ttry {\n\t\treturn readDurableFileLockOwnerSync(lockPath);\n\t} catch (error) {\n\t\tif (!(error instanceof Error)) throw error;\n\t\tif (fileErrorCode(error) === \"ENOENT\") {\n\t\t\ttry {\n\t\t\t\tlstatSync(lockPath);\n\t\t\t} catch (pathError) {\n\t\t\t\tif (!(pathError instanceof Error)) throw pathError;\n\t\t\t\tif (fileErrorCode(pathError) === \"ENOENT\") return { status: \"absent\" };\n\t\t\t}\n\t\t}\n\t\treturn { status: \"unknown\" };\n\t}\n}\n\nfunction reclaimGatePath(lockPath: string, observed: DurableFileLockOwnerSnapshot): string {\n\tconst ownerKey = createHash(\"sha256\")\n\t\t.update(`${observed.owner.token}\\0${observed.dev}\\0${observed.ino}`)\n\t\t.digest(\"hex\")\n\t\t.slice(0, 24);\n\treturn `${lockPath}.reclaim-${ownerKey}.lock`;\n}\n\nexport function durableFileLockPath(scope: string, key: string): string {\n\tif (!/^[a-z-]+$/u.test(scope)) throw new TypeError(\"Durable file lock scope is invalid\");\n\treturn join(getDurableFileLockRoot(), `${scope}-${key}.lock`);\n}\n\nexport function acquireDurableFileLockPathSync(key: string, path: string, deadline: number): HeldDurableFileLock {\n\twhile (true) {\n\t\tconst token = randomUUID();\n\t\ttry {\n\t\t\tmkdirSync(path, { mode: 0o700 });\n\t\t} catch (error) {\n\t\t\tif (fileErrorCode(error) !== \"EEXIST\") throw error;\n\t\t\tif (inspectDurableFileLockPathSync(path, deadline).status === \"absent\") continue;\n\t\t\tif (Date.now() >= deadline) throw new DurableFileLockBusyError();\n\t\t\tAtomics.wait(WAITER, 0, 0, Math.min(10, deadline - Date.now()));\n\t\t\tcontinue;\n\t\t}\n\t\tconst directory = lstatSync(path, { bigint: true });\n\t\tif (!directory.isDirectory() || directory.isSymbolicLink()) throw new DurableFileLockOwnershipError();\n\t\tconst expectedOwner: DurableFileLockOwnerSnapshot = Object.freeze({\n\t\t\towner: Object.freeze({ pid: process.pid, host: hostname(), token }),\n\t\t\tdev: directory.dev.toString(),\n\t\t\tino: directory.ino.toString(),\n\t\t});\n\t\tconst lock: HeldDurableFileLock = {\n\t\t\tkey,\n\t\t\tpath,\n\t\t\ttoken,\n\t\t\texpectedOwner,\n\t\t\tretirement: createDurableFileLockRetirement(),\n\t\t};\n\t\tlet ownerWritten = false;\n\t\ttry {\n\t\t\tfsyncDirectorySync(dirname(path));\n\t\t\twriteExclusiveFileDurablySync(ownerPath(path), Buffer.from(JSON.stringify(expectedOwner.owner), \"utf8\"));\n\t\t\townerWritten = true;\n\t\t\tassertDurableFileLockPathsOwnedSync([lock]);\n\t\t\treturn lock;\n\t\t} catch (error) {\n\t\t\tlet cleanupError: unknown;\n\t\t\ttry {\n\t\t\t\tif (ownerWritten) releaseDurableFileLockPathsSync([lock], [lock]);\n\t\t\t\telse {\n\t\t\t\t\trmdirSync(path);\n\t\t\t\t\tfsyncDirectorySync(dirname(path));\n\t\t\t\t}\n\t\t\t} catch (failure) {\n\t\t\t\tcleanupError = failure instanceof Error ? failure : new Error(String(failure));\n\t\t\t\tif (ownerWritten && !(cleanupError instanceof DurableFileLockOwnershipError))\n\t\t\t\t\tretainRetryableDurableFileLocks([lock]);\n\t\t\t}\n\t\t\tif (cleanupError) throw new AggregateError([error, cleanupError], \"Durable file lock creation cleanup failed\");\n\t\t\tthrow error;\n\t\t}\n\t}\n}\n\nfunction reclaim(lockPath: string, observed: DurableFileLockOwnerSnapshot, deadline: number): boolean {\n\tconst gate = acquireDurableFileLockPathSync(\n\t\t`reclaim:${observed.owner.token}`,\n\t\treclaimGatePath(lockPath, observed),\n\t\tdeadline,\n\t);\n\tconst stale: HeldDurableFileLock = {\n\t\tkey: `stale:${observed.owner.token}`,\n\t\tpath: lockPath,\n\t\ttoken: observed.owner.token,\n\t\texpectedOwner: observed,\n\t\tretirement: createDurableFileLockRetirement(),\n\t};\n\tlet failure: unknown;\n\tlet retired = false;\n\ttry {\n\t\tconst current = readDurableFileLockOwnerSync(lockPath);\n\t\tif (sameDurableFileLockOwner(current, observed)) {\n\t\t\tremoveOwnedLock(stale);\n\t\t\tretired = true;\n\t\t}\n\t} catch (error) {\n\t\tif (fileErrorCode(error) === \"ENOENT\") {\n\t\t\ttry {\n\t\t\t\tlstatSync(lockPath);\n\t\t\t} catch (pathError) {\n\t\t\t\tif (fileErrorCode(pathError) === \"ENOENT\") retired = true;\n\t\t\t\telse failure = pathError instanceof Error ? pathError : new Error(String(pathError));\n\t\t\t}\n\t\t}\n\t\tif (!retired && failure === undefined) failure = error instanceof Error ? error : new Error(String(error));\n\t\tif (stale.retirement.retiredPath !== undefined) retainRetryableDurableFileLocks([stale]);\n\t}\n\ttry {\n\t\treleaseDurableFileLockPathsSync([gate], [gate]);\n\t} catch (error) {\n\t\tretainRetryableDurableFileLocks([gate]);\n\t\tconst cleanupError = error instanceof Error ? error : new Error(String(error));\n\t\tfailure = failure ? new AggregateError([failure, cleanupError], \"Durable lock reclaim failed\") : cleanupError;\n\t}\n\tif (failure) throw failure;\n\treturn retired;\n}\n\nexport function inspectDurableFileLockPathSync(lockPath: string, deadline: number): DurableFileLockPathObservation {\n\tconst observed = ownerObservation(lockPath);\n\tif (\"status\" in observed) return observed;\n\tif (observed.owner.host !== hostname()) return { status: \"unknown\" };\n\tif (processIsLive(observed.owner.pid)) {\n\t\treturn { status: \"live\", pid: observed.owner.pid, host: observed.owner.host };\n\t}\n\treturn reclaim(lockPath, observed, deadline) ? { status: \"absent\" } : { status: \"unknown\" };\n}\n"]}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export declare class DurableFileModeError extends Error {
|
|
2
|
+
readonly name = "DurableFileModeError";
|
|
3
|
+
readonly path: string;
|
|
4
|
+
constructor(path: string, cause: unknown);
|
|
5
|
+
}
|
|
6
|
+
export declare function enforcePrivateFileDescriptorModeSync(fd: number, path: string): void;
|
|
7
|
+
export declare function enforcePrivateFileModeSync(path: string): void;
|
|
8
|
+
//# sourceMappingURL=durable-file-mode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-file-mode.d.ts","sourceRoot":"","sources":["../../src/core/durable-file-mode.ts"],"names":[],"mappings":"AAIA,qBAAa,oBAAqB,SAAQ,KAAK;IAC9C,SAAkB,IAAI,0BAA0B;IAChD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB,YAAY,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAMvC;CACD;AAED,wBAAgB,oCAAoC,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,IAAI,CAWnF;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAQ7D","sourcesContent":["import { closeSync, fchmodSync, fstatSync, openSync } from \"fs\";\n\nconst PRIVATE_FILE_MODE = 0o600;\n\nexport class DurableFileModeError extends Error {\n\toverride readonly name = \"DurableFileModeError\";\n\treadonly path: string;\n\n\tconstructor(path: string, cause: unknown) {\n\t\tconst code = cause instanceof Error && \"code\" in cause ? String(Reflect.get(cause, \"code\")) : undefined;\n\t\tsuper(`Cannot enforce private mode 0600 for ${path} on ${process.platform}${code ? ` (${code})` : \"\"}`, {\n\t\t\tcause,\n\t\t});\n\t\tthis.path = path;\n\t}\n}\n\nexport function enforcePrivateFileDescriptorModeSync(fd: number, path: string): void {\n\tif (process.platform === \"win32\") return;\n\ttry {\n\t\tconst current = fstatSync(fd).mode & 0o7777;\n\t\tif (current === PRIVATE_FILE_MODE) return;\n\t\tfchmodSync(fd, PRIVATE_FILE_MODE);\n\t\tconst actual = fstatSync(fd).mode & 0o7777;\n\t\tif (actual !== PRIVATE_FILE_MODE) throw new Error(`mode remained ${actual.toString(8)}`);\n\t} catch (error) {\n\t\tthrow new DurableFileModeError(path, error);\n\t}\n}\n\nexport function enforcePrivateFileModeSync(path: string): void {\n\tif (process.platform === \"win32\") return;\n\tconst fd = openSync(path, \"r\");\n\ttry {\n\t\tenforcePrivateFileDescriptorModeSync(fd, path);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n}\n"]}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { closeSync, fchmodSync, fstatSync, openSync } from "fs";
|
|
2
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
3
|
+
export class DurableFileModeError extends Error {
|
|
4
|
+
name = "DurableFileModeError";
|
|
5
|
+
path;
|
|
6
|
+
constructor(path, cause) {
|
|
7
|
+
const code = cause instanceof Error && "code" in cause ? String(Reflect.get(cause, "code")) : undefined;
|
|
8
|
+
super(`Cannot enforce private mode 0600 for ${path} on ${process.platform}${code ? ` (${code})` : ""}`, {
|
|
9
|
+
cause,
|
|
10
|
+
});
|
|
11
|
+
this.path = path;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export function enforcePrivateFileDescriptorModeSync(fd, path) {
|
|
15
|
+
if (process.platform === "win32")
|
|
16
|
+
return;
|
|
17
|
+
try {
|
|
18
|
+
const current = fstatSync(fd).mode & 0o7777;
|
|
19
|
+
if (current === PRIVATE_FILE_MODE)
|
|
20
|
+
return;
|
|
21
|
+
fchmodSync(fd, PRIVATE_FILE_MODE);
|
|
22
|
+
const actual = fstatSync(fd).mode & 0o7777;
|
|
23
|
+
if (actual !== PRIVATE_FILE_MODE)
|
|
24
|
+
throw new Error(`mode remained ${actual.toString(8)}`);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
throw new DurableFileModeError(path, error);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export function enforcePrivateFileModeSync(path) {
|
|
31
|
+
if (process.platform === "win32")
|
|
32
|
+
return;
|
|
33
|
+
const fd = openSync(path, "r");
|
|
34
|
+
try {
|
|
35
|
+
enforcePrivateFileDescriptorModeSync(fd, path);
|
|
36
|
+
}
|
|
37
|
+
finally {
|
|
38
|
+
closeSync(fd);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=durable-file-mode.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"durable-file-mode.js","sourceRoot":"","sources":["../../src/core/durable-file-mode.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,IAAI,CAAC;AAEhE,MAAM,iBAAiB,GAAG,KAAK,CAAC;AAEhC,MAAM,OAAO,oBAAqB,SAAQ,KAAK;IAC5B,IAAI,GAAG,sBAAsB,CAAC;IACvC,IAAI,CAAS;IAEtB,YAAY,IAAY,EAAE,KAAc,EAAE;QACzC,MAAM,IAAI,GAAG,KAAK,YAAY,KAAK,IAAI,MAAM,IAAI,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACxG,KAAK,CAAC,wCAAwC,IAAI,OAAO,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE;YACvG,KAAK;SACL,CAAC,CAAC;QACH,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IAAA,CACjB;CACD;AAED,MAAM,UAAU,oCAAoC,CAAC,EAAU,EAAE,IAAY,EAAQ;IACpF,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO;IACzC,IAAI,CAAC;QACJ,MAAM,OAAO,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,MAAM,CAAC;QAC5C,IAAI,OAAO,KAAK,iBAAiB;YAAE,OAAO;QAC1C,UAAU,CAAC,EAAE,EAAE,iBAAiB,CAAC,CAAC;QAClC,MAAM,MAAM,GAAG,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,MAAM,CAAC;QAC3C,IAAI,MAAM,KAAK,iBAAiB;YAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;IAC1F,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QAChB,MAAM,IAAI,oBAAoB,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;IAC7C,CAAC;AAAA,CACD;AAED,MAAM,UAAU,0BAA0B,CAAC,IAAY,EAAQ;IAC9D,IAAI,OAAO,CAAC,QAAQ,KAAK,OAAO;QAAE,OAAO;IACzC,MAAM,EAAE,GAAG,QAAQ,CAAC,IAAI,EAAE,GAAG,CAAC,CAAC;IAC/B,IAAI,CAAC;QACJ,oCAAoC,CAAC,EAAE,EAAE,IAAI,CAAC,CAAC;IAChD,CAAC;YAAS,CAAC;QACV,SAAS,CAAC,EAAE,CAAC,CAAC;IACf,CAAC;AAAA,CACD","sourcesContent":["import { closeSync, fchmodSync, fstatSync, openSync } from \"fs\";\n\nconst PRIVATE_FILE_MODE = 0o600;\n\nexport class DurableFileModeError extends Error {\n\toverride readonly name = \"DurableFileModeError\";\n\treadonly path: string;\n\n\tconstructor(path: string, cause: unknown) {\n\t\tconst code = cause instanceof Error && \"code\" in cause ? String(Reflect.get(cause, \"code\")) : undefined;\n\t\tsuper(`Cannot enforce private mode 0600 for ${path} on ${process.platform}${code ? ` (${code})` : \"\"}`, {\n\t\t\tcause,\n\t\t});\n\t\tthis.path = path;\n\t}\n}\n\nexport function enforcePrivateFileDescriptorModeSync(fd: number, path: string): void {\n\tif (process.platform === \"win32\") return;\n\ttry {\n\t\tconst current = fstatSync(fd).mode & 0o7777;\n\t\tif (current === PRIVATE_FILE_MODE) return;\n\t\tfchmodSync(fd, PRIVATE_FILE_MODE);\n\t\tconst actual = fstatSync(fd).mode & 0o7777;\n\t\tif (actual !== PRIVATE_FILE_MODE) throw new Error(`mode remained ${actual.toString(8)}`);\n\t} catch (error) {\n\t\tthrow new DurableFileModeError(path, error);\n\t}\n}\n\nexport function enforcePrivateFileModeSync(path: string): void {\n\tif (process.platform === \"win32\") return;\n\tconst fd = openSync(path, \"r\");\n\ttry {\n\t\tenforcePrivateFileDescriptorModeSync(fd, path);\n\t} finally {\n\t\tcloseSync(fd);\n\t}\n}\n"]}
|
|
@@ -4,11 +4,22 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { type EventBus } from "../event-bus.ts";
|
|
6
6
|
import type { Extension, ExtensionFactory, ExtensionRuntime, LoadExtensionsResult } from "./types.ts";
|
|
7
|
+
type PackageJsonResolver = (id: string) => string;
|
|
8
|
+
type ExistsSync = (path: string) => boolean;
|
|
9
|
+
/** Internal test seam for omk-agent-core/node resolution branches. */
|
|
10
|
+
export declare function resolveOmkAgentCoreNodeEntryForTests(options: {
|
|
11
|
+
packagesRoot: string;
|
|
12
|
+
startDir: string;
|
|
13
|
+
resolvePackageJson: PackageJsonResolver;
|
|
14
|
+
existsSync: ExistsSync;
|
|
15
|
+
}): string;
|
|
7
16
|
/**
|
|
8
17
|
* Create a runtime with throwing stubs for action methods.
|
|
9
18
|
* Runner.bindCore() replaces these with real implementations.
|
|
10
19
|
*/
|
|
11
20
|
export declare function createExtensionRuntime(): ExtensionRuntime;
|
|
21
|
+
/** Internal test seam for loading extensions through Bun binary virtual modules. */
|
|
22
|
+
export declare function loadExtensionModuleWithVirtualModulesForTests(extensionPath: string): Promise<ExtensionFactory | undefined>;
|
|
12
23
|
/**
|
|
13
24
|
* Create an Extension from an inline factory function.
|
|
14
25
|
*/
|
|
@@ -21,4 +32,5 @@ export declare function loadExtensions(paths: string[], cwd: string, eventBus?:
|
|
|
21
32
|
* Discover and load extensions from standard locations.
|
|
22
33
|
*/
|
|
23
34
|
export declare function discoverAndLoadExtensions(configuredPaths: string[], cwd: string, agentDir?: string, eventBus?: EventBus): Promise<LoadExtensionsResult>;
|
|
35
|
+
export {};
|
|
24
36
|
//# sourceMappingURL=loader.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAuBH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EACX,SAAS,EAET,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EAKpB,MAAM,YAAY,CAAC;AA+FpB;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CAgDzD;AA8PD;;GAEG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAa,GACxB,OAAO,CAAC,SAAS,CAAC,CAMpB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAyBrH;AA6GD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA2C/B","sourcesContent":["/**\n * Extension loader - loads TypeScript extension modules using jiti.\n *\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti/static\";\nimport * as _bundledOmkAgentCore from \"omk-agent-core\";\nimport * as _bundledOmkAi from \"omk-ai\";\nimport * as _bundledOmkAiOauth from \"omk-ai/oauth\";\nimport type { KeyId } from \"omk-tui\";\nimport * as _bundledOmkTui from \"omk-tui\";\n// Static imports of packages that extensions may use.\n// These MUST be static so Bun bundles them into the compiled binary.\n// The virtualModules option then makes them available to extensions.\nimport * as _bundledTypebox from \"typebox\";\nimport * as _bundledTypeboxCompile from \"typebox/compile\";\nimport * as _bundledTypeboxValue from \"typebox/value\";\nimport { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from \"../../config.ts\";\n// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n// avoiding a circular dependency. Extensions can import from open-multi-agent-kit.\nimport * as _bundledOmkCodingAgent from \"../../index.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { createEventBus, type EventBus } from \"../event-bus.ts\";\nimport type { ExecOptions } from \"../exec.ts\";\nimport { execCommand } from \"../exec.ts\";\nimport { createSyntheticSourceInfo } from \"../source-info.ts\";\nimport type {\n\tExtension,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionRuntime,\n\tLoadExtensionsResult,\n\tMessageRenderer,\n\tProviderConfig,\n\tRegisteredCommand,\n\tToolDefinition,\n} from \"./types.ts\";\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary) */\nconst VIRTUAL_MODULES: Record<string, unknown> = {\n\ttypebox: _bundledTypebox,\n\t\"typebox/compile\": _bundledTypeboxCompile,\n\t\"typebox/value\": _bundledTypeboxValue,\n\t\"@sinclair/typebox\": _bundledTypebox,\n\t\"@sinclair/typebox/compile\": _bundledTypeboxCompile,\n\t\"@sinclair/typebox/value\": _bundledTypeboxValue,\n\t\"omk-agent-core\": _bundledOmkAgentCore,\n\t\"omk-tui\": _bundledOmkTui,\n\t\"omk-ai\": _bundledOmkAi,\n\t\"omk-ai/oauth\": _bundledOmkAiOauth,\n\t\"open-multi-agent-kit\": _bundledOmkCodingAgent,\n};\n\nconst require = createRequire(import.meta.url);\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nlet _aliases: Record<string, string> | null = null;\n\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\tconst typeboxEntry = require.resolve(\"typebox\");\n\tconst typeboxCompileEntry = require.resolve(\"typebox/compile\");\n\tconst typeboxValueEntry = require.resolve(\"typebox/value\");\n\n\tconst packagesRoot = path.resolve(__dirname, \"../../../../\");\n\tconst findPackageJsonPath = (specifier: string): string => {\n\t\ttry {\n\t\t\treturn require.resolve(`${specifier}/package.json`);\n\t\t} catch {\n\t\t\tlet current = __dirname;\n\t\t\twhile (true) {\n\t\t\t\tconst candidate = path.join(current, \"node_modules\", ...specifier.split(\"/\"), \"package.json\");\n\t\t\t\tif (fs.existsSync(candidate)) {\n\t\t\t\t\treturn candidate;\n\t\t\t\t}\n\t\t\t\tconst parent = path.dirname(current);\n\t\t\t\tif (parent === current) {\n\t\t\t\t\tthrow new Error(`Unable to resolve package.json for ${specifier}`);\n\t\t\t\t}\n\t\t\t\tcurrent = parent;\n\t\t\t}\n\t\t}\n\t};\n\tconst resolvePackageFile = (specifier: string, packageRelativePath: string): string => {\n\t\tconst packageJsonPath = findPackageJsonPath(specifier);\n\t\treturn path.join(path.dirname(packageJsonPath), packageRelativePath);\n\t};\n\tconst resolveWorkspaceOrPackageFile = (\n\t\tworkspaceRelativePath: string,\n\t\tspecifier: string,\n\t\tpackageRelativePath: string,\n\t): string => {\n\t\tconst workspacePath = path.join(packagesRoot, workspaceRelativePath);\n\t\tif (fs.existsSync(workspacePath)) {\n\t\t\treturn workspacePath;\n\t\t}\n\t\treturn resolvePackageFile(specifier, packageRelativePath);\n\t};\n\n\tconst omkCodingAgentEntry = packageIndex;\n\tconst omkAgentCoreEntry = resolveWorkspaceOrPackageFile(\"agent/dist/index.js\", \"omk-agent-core\", \"dist/index.js\");\n\tconst omkTuiEntry = resolveWorkspaceOrPackageFile(\"tui/dist/index.js\", \"omk-tui\", \"dist/index.js\");\n\tconst omkAiEntry = resolveWorkspaceOrPackageFile(\"ai/dist/index.js\", \"omk-ai\", \"dist/index.js\");\n\tconst omkAiOauthEntry = resolveWorkspaceOrPackageFile(\"ai/dist/oauth.js\", \"omk-ai\", \"dist/oauth.js\");\n\n\t_aliases = {\n\t\t\"open-multi-agent-kit\": omkCodingAgentEntry,\n\t\t\"omk-agent-core\": omkAgentCoreEntry,\n\t\t\"omk-tui\": omkTuiEntry,\n\t\t\"omk-ai\": omkAiEntry,\n\t\t\"omk-ai/oauth\": omkAiOauthEntry,\n\t\ttypebox: typeboxEntry,\n\t\t\"typebox/compile\": typeboxCompileEntry,\n\t\t\"typebox/value\": typeboxValueEntry,\n\t\t\"@sinclair/typebox\": typeboxEntry,\n\t\t\"@sinclair/typebox/compile\": typeboxCompileEntry,\n\t\t\"@sinclair/typebox/value\": typeboxValueEntry,\n\t};\n\n\treturn _aliases;\n}\n\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Create a runtime with throwing stubs for action methods.\n * Runner.bindCore() replaces these with real implementations.\n */\nexport function createExtensionRuntime(): ExtensionRuntime {\n\tconst notInitialized = () => {\n\t\tthrow new Error(\"Extension runtime not initialized. Action methods cannot be called during extension loading.\");\n\t};\n\tconst state: { staleMessage?: string } = {};\n\tconst assertActive = () => {\n\t\tif (state.staleMessage) {\n\t\t\tthrow new Error(state.staleMessage);\n\t\t}\n\t};\n\n\tconst runtime: ExtensionRuntime = {\n\t\tsendMessage: notInitialized,\n\t\tsendUserMessage: notInitialized,\n\t\tappendEntry: notInitialized,\n\t\tsetSessionName: notInitialized,\n\t\tgetSessionName: notInitialized,\n\t\tsetLabel: notInitialized,\n\t\tgetActiveTools: notInitialized,\n\t\tgetAllTools: notInitialized,\n\t\tsetActiveTools: notInitialized,\n\t\t// registerTool() is valid during extension load; refresh is only needed post-bind.\n\t\trefreshTools: () => {},\n\t\tgetCommands: notInitialized,\n\t\tsetModel: () => Promise.reject(new Error(\"Extension runtime not initialized\")),\n\t\tgetThinkingLevel: notInitialized,\n\t\tsetThinkingLevel: notInitialized,\n\t\t// Optional; replaced by bindCore when the session provides a handler.\n\t\tcallMcpTool: undefined,\n\t\tflagValues: new Map(),\n\t\tpendingProviderRegistrations: [],\n\t\tassertActive,\n\t\tinvalidate: (message) => {\n\t\t\tstate.staleMessage ??=\n\t\t\t\tmessage ??\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured extension API or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\";\n\t\t},\n\t\t// Pre-bind: queue registrations so bindCore() can flush them once the\n\t\t// model registry is available. bindCore() replaces both with direct calls.\n\t\tregisterProvider: (name, config, extensionPath = \"<unknown>\") => {\n\t\t\truntime.pendingProviderRegistrations.push({ name, config, extensionPath });\n\t\t},\n\t\tunregisterProvider: (name) => {\n\t\t\truntime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);\n\t\t},\n\t};\n\n\treturn runtime;\n}\n\n/**\n * Create the ExtensionAPI for an extension.\n * Registration methods write to the extension object.\n * Action methods delegate to the shared runtime.\n */\nfunction createExtensionAPI(\n\textension: Extension,\n\truntime: ExtensionRuntime,\n\tcwd: string,\n\teventBus: EventBus,\n): ExtensionAPI {\n\tconst api = {\n\t\t// Registration methods - write to extension\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\truntime.assertActive();\n\t\t\tconst list = extension.handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\textension.handlers.set(event, list);\n\t\t},\n\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\truntime.assertActive();\n\t\t\textension.tools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t\truntime.refreshTools();\n\t\t},\n\n\t\tregisterCommand(name: string, options: Omit<RegisteredCommand, \"name\" | \"sourceInfo\">): void {\n\t\t\truntime.assertActive();\n\t\t\textension.commands.set(name, {\n\t\t\t\tname,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t...options,\n\t\t\t});\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.ts\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });\n\t\t},\n\n\t\tregisterFlag(\n\t\t\tname: string,\n\t\t\toptions: { description?: string; type: \"boolean\" | \"string\"; default?: boolean | string },\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.flags.set(name, { name, extensionPath: extension.path, ...options });\n\t\t\tif (options.default !== undefined && !runtime.flagValues.has(name)) {\n\t\t\t\truntime.flagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.messageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\t// Flag access - checks extension registered it, reads from runtime\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\truntime.assertActive();\n\t\t\tif (!extension.flags.has(name)) return undefined;\n\t\t\treturn runtime.flagValues.get(name);\n\t\t},\n\n\t\t// Action methods - delegate to shared runtime\n\t\tsendMessage(message, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendMessage(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendUserMessage(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.appendEntry(customType, data);\n\t\t},\n\n\t\tsetSessionName(name: string): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setSessionName(name);\n\t\t},\n\n\t\tgetSessionName(): string | undefined {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getSessionName();\n\t\t},\n\n\t\tsetLabel(entryId: string, label: string | undefined): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setLabel(entryId, label);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\truntime.assertActive();\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\t// Always expose the method so load-time factories can capture it (typeof === \"function\").\n\t\t// The bound handler is resolved at call time after bindCore; until then callers get a clear error.\n\t\tasync callMcpTool(server: string, name: string, args: Record<string, unknown>): Promise<unknown> {\n\t\t\truntime.assertActive();\n\t\t\tconst handler = runtime.callMcpTool;\n\t\t\tif (typeof handler !== \"function\") {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Extension callMcpTool is not bound. The session must provide a callMcpTool handler via bindCore.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn handler(server, name, args);\n\t\t},\n\n\t\tisMcpToolBound(): boolean {\n\t\t\truntime.assertActive();\n\t\t\treturn typeof runtime.callMcpTool === \"function\";\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getActiveTools();\n\t\t},\n\n\t\tgetAllTools() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getAllTools();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setActiveTools(toolNames);\n\t\t},\n\n\t\tgetCommands() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getCommands();\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.setModel(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getThinkingLevel();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\truntime.assertActive();\n\t\t\truntime.setThinkingLevel(level);\n\t\t},\n\n\t\tregisterProvider(name: string, config: ProviderConfig) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerProvider(name, config, extension.path);\n\t\t},\n\n\t\tunregisterProvider(name: string) {\n\t\t\truntime.assertActive();\n\t\t\truntime.unregisterProvider(name, extension.path);\n\t\t},\n\n\t\t// Track extension event subscriptions so they can be\n\t\t// unsubscribed on reload instead of stacking duplicate listeners\n\t\t// on the shared event bus.\n\t\tevents: {\n\t\t\temit: (channel, data) => {\n\t\t\t\teventBus.emit(channel, data);\n\t\t\t},\n\t\t\ton: (channel, handler) => {\n\t\t\t\tconst unsubscribe = eventBus.on(channel, handler);\n\t\t\t\textension.eventBusSubscriptions?.push(unsubscribe);\n\t\t\t\treturn unsubscribe;\n\t\t\t},\n\t\t},\n\t} as ExtensionAPI;\n\n\treturn api;\n}\n\nasync function loadExtensionModule(extensionPath: string) {\n\tconst jiti = createJiti(import.meta.url, {\n\t\tmoduleCache: false,\n\t\t// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)\n\t\t// Also disable tryNative so jiti handles ALL imports (not just the entry point)\n\t\t// In Node.js/dev: use aliases to resolve to node_modules paths\n\t\t...(isBunBinary ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),\n\t});\n\n\tconst module = await jiti.import(extensionPath, { default: true });\n\tconst factory = module as ExtensionFactory;\n\treturn typeof factory !== \"function\" ? undefined : factory;\n}\n\n/**\n * Create an Extension object with empty collections.\n */\nfunction createExtension(extensionPath: string, resolvedPath: string): Extension {\n\tconst source =\n\t\textensionPath.startsWith(\"<\") && extensionPath.endsWith(\">\")\n\t\t\t? extensionPath.slice(1, -1).split(\":\")[0] || \"temporary\"\n\t\t\t: \"local\";\n\tconst baseDir = extensionPath.startsWith(\"<\") ? undefined : path.dirname(resolvedPath);\n\n\treturn {\n\t\tpath: extensionPath,\n\t\tresolvedPath,\n\t\tsourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),\n\t\thandlers: new Map(),\n\t\ttools: new Map(),\n\t\tmessageRenderers: new Map(),\n\t\tcommands: new Map(),\n\t\tflags: new Map(),\n\t\tshortcuts: new Map(),\n\t\teventBusSubscriptions: [],\n\t};\n}\n\nasync function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n): Promise<{ extension: Extension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });\n\n\ttry {\n\t\tconst factory = await loadExtensionModule(resolvedPath);\n\t\tif (!factory) {\n\t\t\treturn { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };\n\t\t}\n\n\t\tconst extension = createExtension(extensionPath, resolvedPath);\n\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\tawait factory(api);\n\n\t\treturn { extension, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\n/**\n * Create an Extension from an inline factory function.\n */\nexport async function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\textensionPath = \"<inline>\",\n): Promise<Extension> {\n\tconst extension = createExtension(extensionPath, extensionPath);\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);\n\tawait factory(api);\n\treturn extension;\n}\n\n/**\n * Load extensions from paths.\n */\nexport async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {\n\tconst extensions: Extension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst runtime = createExtensionRuntime();\n\n\tfor (const extPath of paths) {\n\t\tconst { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, runtime);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\truntime,\n\t};\n}\n\ninterface OmkManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n}\n\nfunction readOmkManifest(packageJsonPath: string): OmkManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content) as { omk?: unknown };\n\t\tconst manifest = pkg.omk;\n\t\treturn manifest && typeof manifest === \"object\" ? (manifest as OmkManifest) : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with \"omk.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\t// Check for package.json with \"omk\" field first\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readOmkManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = path.resolve(dir, extPath);\n\t\t\t\tif (fs.existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with OMK manifest → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push(entryPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedAgentDir = resolvePath(agentDir);\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/\n\tconst localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 2. Global extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(resolvedAgentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true });\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\t// Check for package.json with omk manifest or index.ts\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// No explicit entries - discover individual files in directory\n\t\t\taddPaths(discoverExtensionsInDir(resolved));\n\t\t\tcontinue;\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\treturn loadExtensions(allPaths, resolvedCwd, eventBus);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"loader.d.ts","sourceRoot":"","sources":["../../../src/core/extensions/loader.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAwBH,OAAO,EAAkB,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAIhE,OAAO,KAAK,EACX,SAAS,EAET,gBAAgB,EAChB,gBAAgB,EAChB,oBAAoB,EAKpB,MAAM,YAAY,CAAC;AAsBpB,KAAK,mBAAmB,GAAG,CAAC,EAAE,EAAE,MAAM,KAAK,MAAM,CAAC;AAClD,KAAK,UAAU,GAAG,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC;AA6C5C,sEAAsE;AACtE,wBAAgB,oCAAoC,CAAC,OAAO,EAAE;IAC7D,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,kBAAkB,EAAE,mBAAmB,CAAC;IACxC,UAAU,EAAE,UAAU,CAAC;CACvB,GAAG,MAAM,CAUT;AAoFD;;;GAGG;AACH,wBAAgB,sBAAsB,IAAI,gBAAgB,CAgDzD;AA4MD,oFAAoF;AACpF,wBAAsB,6CAA6C,CAAC,aAAa,EAAE,MAAM,yCAExF;AAmDD;;GAEG;AACH,wBAAsB,wBAAwB,CAC7C,OAAO,EAAE,gBAAgB,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,QAAQ,EAClB,OAAO,EAAE,gBAAgB,EACzB,aAAa,SAAa,GACxB,OAAO,CAAC,SAAS,CAAC,CAMpB;AAED;;GAEG;AACH,wBAAsB,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC,oBAAoB,CAAC,CAyBrH;AA6GD;;GAEG;AACH,wBAAsB,yBAAyB,CAC9C,eAAe,EAAE,MAAM,EAAE,EACzB,GAAG,EAAE,MAAM,EACX,QAAQ,GAAE,MAAsB,EAChC,QAAQ,CAAC,EAAE,QAAQ,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA2C/B","sourcesContent":["/**\n * Extension loader - loads TypeScript extension modules using jiti.\n *\n */\n\nimport * as fs from \"node:fs\";\nimport { createRequire } from \"node:module\";\nimport * as path from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createJiti } from \"jiti/static\";\nimport * as _bundledOmkAgentCore from \"omk-agent-core\";\nimport * as _bundledOmkAgentCoreNode from \"omk-agent-core/node\";\nimport * as _bundledOmkAi from \"omk-ai\";\nimport * as _bundledOmkAiOauth from \"omk-ai/oauth\";\nimport type { KeyId } from \"omk-tui\";\nimport * as _bundledOmkTui from \"omk-tui\";\n// Static imports of packages that extensions may use.\n// These MUST be static so Bun bundles them into the compiled binary.\n// The virtualModules option then makes them available to extensions.\nimport * as _bundledTypebox from \"typebox\";\nimport * as _bundledTypeboxCompile from \"typebox/compile\";\nimport * as _bundledTypeboxValue from \"typebox/value\";\nimport { CONFIG_DIR_NAME, getAgentDir, isBunBinary } from \"../../config.ts\";\n// NOTE: This import works because loader.ts exports are NOT re-exported from index.ts,\n// avoiding a circular dependency. Extensions can import from open-multi-agent-kit.\nimport * as _bundledOmkCodingAgent from \"../../index.ts\";\nimport { resolvePath } from \"../../utils/paths.ts\";\nimport { createEventBus, type EventBus } from \"../event-bus.ts\";\nimport type { ExecOptions } from \"../exec.ts\";\nimport { execCommand } from \"../exec.ts\";\nimport { createSyntheticSourceInfo } from \"../source-info.ts\";\nimport type {\n\tExtension,\n\tExtensionAPI,\n\tExtensionFactory,\n\tExtensionRuntime,\n\tLoadExtensionsResult,\n\tMessageRenderer,\n\tProviderConfig,\n\tRegisteredCommand,\n\tToolDefinition,\n} from \"./types.ts\";\n\n/** Modules available to extensions via virtualModules (for compiled Bun binary) */\nconst VIRTUAL_MODULES: Record<string, unknown> = {\n\ttypebox: _bundledTypebox,\n\t\"typebox/compile\": _bundledTypeboxCompile,\n\t\"typebox/value\": _bundledTypeboxValue,\n\t\"@sinclair/typebox\": _bundledTypebox,\n\t\"@sinclair/typebox/compile\": _bundledTypeboxCompile,\n\t\"@sinclair/typebox/value\": _bundledTypeboxValue,\n\t\"omk-agent-core\": _bundledOmkAgentCore,\n\t\"omk-agent-core/node\": _bundledOmkAgentCoreNode,\n\t\"omk-tui\": _bundledOmkTui,\n\t\"omk-ai\": _bundledOmkAi,\n\t\"omk-ai/oauth\": _bundledOmkAiOauth,\n\t\"open-multi-agent-kit\": _bundledOmkCodingAgent,\n};\n\nconst require = createRequire(import.meta.url);\n\nlet _aliases: Record<string, string> | null = null;\n\ntype PackageJsonResolver = (id: string) => string;\ntype ExistsSync = (path: string) => boolean;\n\nfunction findPackageJsonPath(\n\tspecifier: string,\n\tstartDir: string,\n\tresolvePackageJson: PackageJsonResolver = require.resolve,\n\texistsSync: ExistsSync = fs.existsSync,\n): string {\n\ttry {\n\t\treturn resolvePackageJson(`${specifier}/package.json`);\n\t} catch {\n\t\tlet current = startDir;\n\t\twhile (true) {\n\t\t\tconst candidate = path.join(current, \"node_modules\", ...specifier.split(\"/\"), \"package.json\");\n\t\t\tif (existsSync(candidate)) {\n\t\t\t\treturn candidate;\n\t\t\t}\n\t\t\tconst parent = path.dirname(current);\n\t\t\tif (parent === current) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t`Unable to resolve package.json for ${specifier}. Install ${specifier} or run OMK from a workspace containing its built package.`,\n\t\t\t\t);\n\t\t\t}\n\t\t\tcurrent = parent;\n\t\t}\n\t}\n}\n\nfunction resolveWorkspaceOrPackageFile(\n\tworkspaceRelativePath: string,\n\tspecifier: string,\n\tpackageRelativePath: string,\n\tpackagesRoot: string,\n\tresolvePackageJson: PackageJsonResolver,\n\texistsSync: ExistsSync,\n\tstartDir: string,\n): string {\n\tconst workspacePath = path.join(packagesRoot, workspaceRelativePath);\n\tif (existsSync(workspacePath)) {\n\t\treturn workspacePath;\n\t}\n\tconst packageJsonPath = findPackageJsonPath(specifier, startDir, resolvePackageJson, existsSync);\n\treturn path.join(path.dirname(packageJsonPath), packageRelativePath);\n}\n\n/** Internal test seam for omk-agent-core/node resolution branches. */\nexport function resolveOmkAgentCoreNodeEntryForTests(options: {\n\tpackagesRoot: string;\n\tstartDir: string;\n\tresolvePackageJson: PackageJsonResolver;\n\texistsSync: ExistsSync;\n}): string {\n\treturn resolveWorkspaceOrPackageFile(\n\t\t\"agent/dist/node.js\",\n\t\t\"omk-agent-core\",\n\t\t\"dist/node.js\",\n\t\toptions.packagesRoot,\n\t\toptions.resolvePackageJson,\n\t\toptions.existsSync,\n\t\toptions.startDir,\n\t);\n}\n\n/**\n * Get aliases for jiti (used in Node.js/development mode).\n * In Bun binary mode, virtualModules is used instead.\n */\nfunction getAliases(): Record<string, string> {\n\tif (_aliases) return _aliases;\n\n\tconst __dirname = path.dirname(fileURLToPath(import.meta.url));\n\tconst packageIndex = path.resolve(__dirname, \"../..\", \"index.js\");\n\n\tconst typeboxEntry = require.resolve(\"typebox\");\n\tconst typeboxCompileEntry = require.resolve(\"typebox/compile\");\n\tconst typeboxValueEntry = require.resolve(\"typebox/value\");\n\n\tconst packagesRoot = path.resolve(__dirname, \"../../../../\");\n\tconst omkCodingAgentEntry = packageIndex;\n\tconst omkAgentCoreEntry = resolveWorkspaceOrPackageFile(\n\t\t\"agent/dist/index.js\",\n\t\t\"omk-agent-core\",\n\t\t\"dist/index.js\",\n\t\tpackagesRoot,\n\t\trequire.resolve,\n\t\tfs.existsSync,\n\t\t__dirname,\n\t);\n\tconst omkAgentCoreNodeEntry = resolveWorkspaceOrPackageFile(\n\t\t\"agent/dist/node.js\",\n\t\t\"omk-agent-core\",\n\t\t\"dist/node.js\",\n\t\tpackagesRoot,\n\t\trequire.resolve,\n\t\tfs.existsSync,\n\t\t__dirname,\n\t);\n\tconst omkTuiEntry = resolveWorkspaceOrPackageFile(\n\t\t\"tui/dist/index.js\",\n\t\t\"omk-tui\",\n\t\t\"dist/index.js\",\n\t\tpackagesRoot,\n\t\trequire.resolve,\n\t\tfs.existsSync,\n\t\t__dirname,\n\t);\n\tconst omkAiEntry = resolveWorkspaceOrPackageFile(\n\t\t\"ai/dist/index.js\",\n\t\t\"omk-ai\",\n\t\t\"dist/index.js\",\n\t\tpackagesRoot,\n\t\trequire.resolve,\n\t\tfs.existsSync,\n\t\t__dirname,\n\t);\n\tconst omkAiOauthEntry = resolveWorkspaceOrPackageFile(\n\t\t\"ai/dist/oauth.js\",\n\t\t\"omk-ai\",\n\t\t\"dist/oauth.js\",\n\t\tpackagesRoot,\n\t\trequire.resolve,\n\t\tfs.existsSync,\n\t\t__dirname,\n\t);\n\n\t_aliases = {\n\t\t\"open-multi-agent-kit\": omkCodingAgentEntry,\n\t\t\"omk-agent-core\": omkAgentCoreEntry,\n\t\t\"omk-agent-core/node\": omkAgentCoreNodeEntry,\n\t\t\"omk-tui\": omkTuiEntry,\n\t\t\"omk-ai\": omkAiEntry,\n\t\t\"omk-ai/oauth\": omkAiOauthEntry,\n\t\ttypebox: typeboxEntry,\n\t\t\"typebox/compile\": typeboxCompileEntry,\n\t\t\"typebox/value\": typeboxValueEntry,\n\t\t\"@sinclair/typebox\": typeboxEntry,\n\t\t\"@sinclair/typebox/compile\": typeboxCompileEntry,\n\t\t\"@sinclair/typebox/value\": typeboxValueEntry,\n\t};\n\n\treturn _aliases;\n}\n\ntype HandlerFn = (...args: unknown[]) => Promise<unknown>;\n\n/**\n * Create a runtime with throwing stubs for action methods.\n * Runner.bindCore() replaces these with real implementations.\n */\nexport function createExtensionRuntime(): ExtensionRuntime {\n\tconst notInitialized = () => {\n\t\tthrow new Error(\"Extension runtime not initialized. Action methods cannot be called during extension loading.\");\n\t};\n\tconst state: { staleMessage?: string } = {};\n\tconst assertActive = () => {\n\t\tif (state.staleMessage) {\n\t\t\tthrow new Error(state.staleMessage);\n\t\t}\n\t};\n\n\tconst runtime: ExtensionRuntime = {\n\t\tsendMessage: notInitialized,\n\t\tsendUserMessage: notInitialized,\n\t\tappendEntry: notInitialized,\n\t\tsetSessionName: notInitialized,\n\t\tgetSessionName: notInitialized,\n\t\tsetLabel: notInitialized,\n\t\tgetActiveTools: notInitialized,\n\t\tgetAllTools: notInitialized,\n\t\tsetActiveTools: notInitialized,\n\t\t// registerTool() is valid during extension load; refresh is only needed post-bind.\n\t\trefreshTools: () => {},\n\t\tgetCommands: notInitialized,\n\t\tsetModel: () => Promise.reject(new Error(\"Extension runtime not initialized\")),\n\t\tgetThinkingLevel: notInitialized,\n\t\tsetThinkingLevel: notInitialized,\n\t\t// Optional; replaced by bindCore when the session provides a handler.\n\t\tcallMcpTool: undefined,\n\t\tflagValues: new Map(),\n\t\tpendingProviderRegistrations: [],\n\t\tassertActive,\n\t\tinvalidate: (message) => {\n\t\t\tstate.staleMessage ??=\n\t\t\t\tmessage ??\n\t\t\t\t\"This extension ctx is stale after session replacement or reload. Do not use a captured extension API or command ctx after ctx.newSession(), ctx.fork(), ctx.switchSession(), or ctx.reload(). For newSession, fork, and switchSession, move post-replacement work into withSession and use the ctx passed to withSession. For reload, do not use the old ctx after await ctx.reload().\";\n\t\t},\n\t\t// Pre-bind: queue registrations so bindCore() can flush them once the\n\t\t// model registry is available. bindCore() replaces both with direct calls.\n\t\tregisterProvider: (name, config, extensionPath = \"<unknown>\") => {\n\t\t\truntime.pendingProviderRegistrations.push({ name, config, extensionPath });\n\t\t},\n\t\tunregisterProvider: (name) => {\n\t\t\truntime.pendingProviderRegistrations = runtime.pendingProviderRegistrations.filter((r) => r.name !== name);\n\t\t},\n\t};\n\n\treturn runtime;\n}\n\n/**\n * Create the ExtensionAPI for an extension.\n * Registration methods write to the extension object.\n * Action methods delegate to the shared runtime.\n */\nfunction createExtensionAPI(\n\textension: Extension,\n\truntime: ExtensionRuntime,\n\tcwd: string,\n\teventBus: EventBus,\n): ExtensionAPI {\n\tconst api = {\n\t\t// Registration methods - write to extension\n\t\ton(event: string, handler: HandlerFn): void {\n\t\t\truntime.assertActive();\n\t\t\tconst list = extension.handlers.get(event) ?? [];\n\t\t\tlist.push(handler);\n\t\t\textension.handlers.set(event, list);\n\t\t},\n\n\t\tregisterTool(tool: ToolDefinition): void {\n\t\t\truntime.assertActive();\n\t\t\textension.tools.set(tool.name, {\n\t\t\t\tdefinition: tool,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t});\n\t\t\truntime.refreshTools();\n\t\t},\n\n\t\tregisterCommand(name: string, options: Omit<RegisteredCommand, \"name\" | \"sourceInfo\">): void {\n\t\t\truntime.assertActive();\n\t\t\textension.commands.set(name, {\n\t\t\t\tname,\n\t\t\t\tsourceInfo: extension.sourceInfo,\n\t\t\t\t...options,\n\t\t\t});\n\t\t},\n\n\t\tregisterShortcut(\n\t\t\tshortcut: KeyId,\n\t\t\toptions: {\n\t\t\t\tdescription?: string;\n\t\t\t\thandler: (ctx: import(\"./types.ts\").ExtensionContext) => Promise<void> | void;\n\t\t\t},\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.shortcuts.set(shortcut, { shortcut, extensionPath: extension.path, ...options });\n\t\t},\n\n\t\tregisterFlag(\n\t\t\tname: string,\n\t\t\toptions: { description?: string; type: \"boolean\" | \"string\"; default?: boolean | string },\n\t\t): void {\n\t\t\truntime.assertActive();\n\t\t\textension.flags.set(name, { name, extensionPath: extension.path, ...options });\n\t\t\tif (options.default !== undefined && !runtime.flagValues.has(name)) {\n\t\t\t\truntime.flagValues.set(name, options.default);\n\t\t\t}\n\t\t},\n\n\t\tregisterMessageRenderer<T>(customType: string, renderer: MessageRenderer<T>): void {\n\t\t\truntime.assertActive();\n\t\t\textension.messageRenderers.set(customType, renderer as MessageRenderer);\n\t\t},\n\n\t\t// Flag access - checks extension registered it, reads from runtime\n\t\tgetFlag(name: string): boolean | string | undefined {\n\t\t\truntime.assertActive();\n\t\t\tif (!extension.flags.has(name)) return undefined;\n\t\t\treturn runtime.flagValues.get(name);\n\t\t},\n\n\t\t// Action methods - delegate to shared runtime\n\t\tsendMessage(message, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendMessage(message, options);\n\t\t},\n\n\t\tsendUserMessage(content, options): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.sendUserMessage(content, options);\n\t\t},\n\n\t\tappendEntry(customType: string, data?: unknown): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.appendEntry(customType, data);\n\t\t},\n\n\t\tsetSessionName(name: string): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setSessionName(name);\n\t\t},\n\n\t\tgetSessionName(): string | undefined {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getSessionName();\n\t\t},\n\n\t\tsetLabel(entryId: string, label: string | undefined): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setLabel(entryId, label);\n\t\t},\n\n\t\texec(command: string, args: string[], options?: ExecOptions) {\n\t\t\truntime.assertActive();\n\t\t\treturn execCommand(command, args, options?.cwd ?? cwd, options);\n\t\t},\n\n\t\t// Always expose the method so load-time factories can capture it (typeof === \"function\").\n\t\t// The bound handler is resolved at call time after bindCore; until then callers get a clear error.\n\t\tasync callMcpTool(server: string, name: string, args: Record<string, unknown>): Promise<unknown> {\n\t\t\truntime.assertActive();\n\t\t\tconst handler = runtime.callMcpTool;\n\t\t\tif (typeof handler !== \"function\") {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Extension callMcpTool is not bound. The session must provide a callMcpTool handler via bindCore.\",\n\t\t\t\t);\n\t\t\t}\n\t\t\treturn handler(server, name, args);\n\t\t},\n\n\t\tisMcpToolBound(): boolean {\n\t\t\truntime.assertActive();\n\t\t\treturn typeof runtime.callMcpTool === \"function\";\n\t\t},\n\n\t\tgetActiveTools(): string[] {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getActiveTools();\n\t\t},\n\n\t\tgetAllTools() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getAllTools();\n\t\t},\n\n\t\tsetActiveTools(toolNames: string[]): void {\n\t\t\truntime.assertActive();\n\t\t\truntime.setActiveTools(toolNames);\n\t\t},\n\n\t\tgetCommands() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getCommands();\n\t\t},\n\n\t\tsetModel(model) {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.setModel(model);\n\t\t},\n\n\t\tgetThinkingLevel() {\n\t\t\truntime.assertActive();\n\t\t\treturn runtime.getThinkingLevel();\n\t\t},\n\n\t\tsetThinkingLevel(level) {\n\t\t\truntime.assertActive();\n\t\t\truntime.setThinkingLevel(level);\n\t\t},\n\n\t\tregisterProvider(name: string, config: ProviderConfig) {\n\t\t\truntime.assertActive();\n\t\t\truntime.registerProvider(name, config, extension.path);\n\t\t},\n\n\t\tunregisterProvider(name: string) {\n\t\t\truntime.assertActive();\n\t\t\truntime.unregisterProvider(name, extension.path);\n\t\t},\n\n\t\t// Track extension event subscriptions so they can be\n\t\t// unsubscribed on reload instead of stacking duplicate listeners\n\t\t// on the shared event bus.\n\t\tevents: {\n\t\t\temit: (channel, data) => {\n\t\t\t\teventBus.emit(channel, data);\n\t\t\t},\n\t\t\ton: (channel, handler) => {\n\t\t\t\tconst unsubscribe = eventBus.on(channel, handler);\n\t\t\t\textension.eventBusSubscriptions?.push(unsubscribe);\n\t\t\t\treturn unsubscribe;\n\t\t\t},\n\t\t},\n\t} as ExtensionAPI;\n\n\treturn api;\n}\n\nasync function loadExtensionModule(extensionPath: string, binaryMode = isBunBinary) {\n\tconst jiti = createJiti(import.meta.url, {\n\t\tmoduleCache: false,\n\t\t// In Bun binary: use virtualModules for bundled packages (no filesystem resolution)\n\t\t// Also disable tryNative so jiti handles ALL imports (not just the entry point)\n\t\t// In Node.js/dev: use aliases to resolve to node_modules paths\n\t\t...(binaryMode ? { virtualModules: VIRTUAL_MODULES, tryNative: false } : { alias: getAliases() }),\n\t});\n\n\tconst module = await jiti.import(extensionPath, { default: true });\n\tconst factory = module as ExtensionFactory;\n\treturn typeof factory !== \"function\" ? undefined : factory;\n}\n/** Internal test seam for loading extensions through Bun binary virtual modules. */\nexport async function loadExtensionModuleWithVirtualModulesForTests(extensionPath: string) {\n\treturn loadExtensionModule(extensionPath, true);\n}\n\n/**\n * Create an Extension object with empty collections.\n */\nfunction createExtension(extensionPath: string, resolvedPath: string): Extension {\n\tconst source =\n\t\textensionPath.startsWith(\"<\") && extensionPath.endsWith(\">\")\n\t\t\t? extensionPath.slice(1, -1).split(\":\")[0] || \"temporary\"\n\t\t\t: \"local\";\n\tconst baseDir = extensionPath.startsWith(\"<\") ? undefined : path.dirname(resolvedPath);\n\n\treturn {\n\t\tpath: extensionPath,\n\t\tresolvedPath,\n\t\tsourceInfo: createSyntheticSourceInfo(extensionPath, { source, baseDir }),\n\t\thandlers: new Map(),\n\t\ttools: new Map(),\n\t\tmessageRenderers: new Map(),\n\t\tcommands: new Map(),\n\t\tflags: new Map(),\n\t\tshortcuts: new Map(),\n\t\teventBusSubscriptions: [],\n\t};\n}\n\nasync function loadExtension(\n\textensionPath: string,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n): Promise<{ extension: Extension | null; error: string | null }> {\n\tconst resolvedPath = resolvePath(extensionPath, cwd, { normalizeUnicodeSpaces: true });\n\n\ttry {\n\t\tconst factory = await loadExtensionModule(resolvedPath);\n\t\tif (!factory) {\n\t\t\treturn { extension: null, error: `Extension does not export a valid factory function: ${extensionPath}` };\n\t\t}\n\n\t\tconst extension = createExtension(extensionPath, resolvedPath);\n\t\tconst api = createExtensionAPI(extension, runtime, cwd, eventBus);\n\t\tawait factory(api);\n\n\t\treturn { extension, error: null };\n\t} catch (err) {\n\t\tconst message = err instanceof Error ? err.message : String(err);\n\t\treturn { extension: null, error: `Failed to load extension: ${message}` };\n\t}\n}\n\n/**\n * Create an Extension from an inline factory function.\n */\nexport async function loadExtensionFromFactory(\n\tfactory: ExtensionFactory,\n\tcwd: string,\n\teventBus: EventBus,\n\truntime: ExtensionRuntime,\n\textensionPath = \"<inline>\",\n): Promise<Extension> {\n\tconst extension = createExtension(extensionPath, extensionPath);\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst api = createExtensionAPI(extension, runtime, resolvedCwd, eventBus);\n\tawait factory(api);\n\treturn extension;\n}\n\n/**\n * Load extensions from paths.\n */\nexport async function loadExtensions(paths: string[], cwd: string, eventBus?: EventBus): Promise<LoadExtensionsResult> {\n\tconst extensions: Extension[] = [];\n\tconst errors: Array<{ path: string; error: string }> = [];\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedEventBus = eventBus ?? createEventBus();\n\tconst runtime = createExtensionRuntime();\n\n\tfor (const extPath of paths) {\n\t\tconst { extension, error } = await loadExtension(extPath, resolvedCwd, resolvedEventBus, runtime);\n\n\t\tif (error) {\n\t\t\terrors.push({ path: extPath, error });\n\t\t\tcontinue;\n\t\t}\n\n\t\tif (extension) {\n\t\t\textensions.push(extension);\n\t\t}\n\t}\n\n\treturn {\n\t\textensions,\n\t\terrors,\n\t\truntime,\n\t};\n}\n\ninterface OmkManifest {\n\textensions?: string[];\n\tthemes?: string[];\n\tskills?: string[];\n\tprompts?: string[];\n}\n\nfunction readOmkManifest(packageJsonPath: string): OmkManifest | null {\n\ttry {\n\t\tconst content = fs.readFileSync(packageJsonPath, \"utf-8\");\n\t\tconst pkg = JSON.parse(content) as { omk?: unknown };\n\t\tconst manifest = pkg.omk;\n\t\treturn manifest && typeof manifest === \"object\" ? (manifest as OmkManifest) : null;\n\t} catch {\n\t\treturn null;\n\t}\n}\n\nfunction isExtensionFile(name: string): boolean {\n\treturn name.endsWith(\".ts\") || name.endsWith(\".js\");\n}\n\n/**\n * Resolve extension entry points from a directory.\n *\n * Checks for:\n * 1. package.json with \"omk.extensions\" field -> returns declared paths\n * 2. index.ts or index.js -> returns the index file\n *\n * Returns resolved paths or null if no entry points found.\n */\nfunction resolveExtensionEntries(dir: string): string[] | null {\n\t// Check for package.json with \"omk\" field first\n\tconst packageJsonPath = path.join(dir, \"package.json\");\n\tif (fs.existsSync(packageJsonPath)) {\n\t\tconst manifest = readOmkManifest(packageJsonPath);\n\t\tif (manifest?.extensions?.length) {\n\t\t\tconst entries: string[] = [];\n\t\t\tfor (const extPath of manifest.extensions) {\n\t\t\t\tconst resolvedExtPath = path.resolve(dir, extPath);\n\t\t\t\tif (fs.existsSync(resolvedExtPath)) {\n\t\t\t\t\tentries.push(resolvedExtPath);\n\t\t\t\t}\n\t\t\t}\n\t\t\tif (entries.length > 0) {\n\t\t\t\treturn entries;\n\t\t\t}\n\t\t}\n\t}\n\n\t// Check for index.ts or index.js\n\tconst indexTs = path.join(dir, \"index.ts\");\n\tconst indexJs = path.join(dir, \"index.js\");\n\tif (fs.existsSync(indexTs)) {\n\t\treturn [indexTs];\n\t}\n\tif (fs.existsSync(indexJs)) {\n\t\treturn [indexJs];\n\t}\n\n\treturn null;\n}\n\n/**\n * Discover extensions in a directory.\n *\n * Discovery rules:\n * 1. Direct files: `extensions/*.ts` or `*.js` → load\n * 2. Subdirectory with index: `extensions/* /index.ts` or `index.js` → load\n * 3. Subdirectory with package.json: `extensions/* /package.json` with OMK manifest → load what it declares\n *\n * No recursion beyond one level. Complex packages must use package.json manifest.\n */\nfunction discoverExtensionsInDir(dir: string): string[] {\n\tif (!fs.existsSync(dir)) {\n\t\treturn [];\n\t}\n\n\tconst discovered: string[] = [];\n\n\ttry {\n\t\tconst entries = fs.readdirSync(dir, { withFileTypes: true });\n\n\t\tfor (const entry of entries) {\n\t\t\tconst entryPath = path.join(dir, entry.name);\n\n\t\t\t// 1. Direct files: *.ts or *.js\n\t\t\tif ((entry.isFile() || entry.isSymbolicLink()) && isExtensionFile(entry.name)) {\n\t\t\t\tdiscovered.push(entryPath);\n\t\t\t\tcontinue;\n\t\t\t}\n\n\t\t\t// 2 & 3. Subdirectories\n\t\t\tif (entry.isDirectory() || entry.isSymbolicLink()) {\n\t\t\t\tconst entries = resolveExtensionEntries(entryPath);\n\t\t\t\tif (entries) {\n\t\t\t\t\tdiscovered.push(...entries);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} catch {\n\t\treturn [];\n\t}\n\n\treturn discovered;\n}\n\n/**\n * Discover and load extensions from standard locations.\n */\nexport async function discoverAndLoadExtensions(\n\tconfiguredPaths: string[],\n\tcwd: string,\n\tagentDir: string = getAgentDir(),\n\teventBus?: EventBus,\n): Promise<LoadExtensionsResult> {\n\tconst resolvedCwd = resolvePath(cwd);\n\tconst resolvedAgentDir = resolvePath(agentDir);\n\tconst allPaths: string[] = [];\n\tconst seen = new Set<string>();\n\n\tconst addPaths = (paths: string[]) => {\n\t\tfor (const p of paths) {\n\t\t\tconst resolved = path.resolve(p);\n\t\t\tif (!seen.has(resolved)) {\n\t\t\t\tseen.add(resolved);\n\t\t\t\tallPaths.push(p);\n\t\t\t}\n\t\t}\n\t};\n\n\t// 1. Project-local extensions: cwd/${CONFIG_DIR_NAME}/extensions/\n\tconst localExtDir = path.join(resolvedCwd, CONFIG_DIR_NAME, \"extensions\");\n\taddPaths(discoverExtensionsInDir(localExtDir));\n\n\t// 2. Global extensions: agentDir/extensions/\n\tconst globalExtDir = path.join(resolvedAgentDir, \"extensions\");\n\taddPaths(discoverExtensionsInDir(globalExtDir));\n\n\t// 3. Explicitly configured paths\n\tfor (const p of configuredPaths) {\n\t\tconst resolved = resolvePath(p, resolvedCwd, { normalizeUnicodeSpaces: true });\n\t\tif (fs.existsSync(resolved) && fs.statSync(resolved).isDirectory()) {\n\t\t\t// Check for package.json with omk manifest or index.ts\n\t\t\tconst entries = resolveExtensionEntries(resolved);\n\t\t\tif (entries) {\n\t\t\t\taddPaths(entries);\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\t// No explicit entries - discover individual files in directory\n\t\t\taddPaths(discoverExtensionsInDir(resolved));\n\t\t\tcontinue;\n\t\t}\n\n\t\taddPaths([resolved]);\n\t}\n\n\treturn loadExtensions(allPaths, resolvedCwd, eventBus);\n}\n"]}
|