@ericsanchezok/synergy-util 2.4.4

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/dist/path.js ADDED
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Extract the last path segment (filename / directory name).
3
+ * Handles both forward slash and backslash separators.
4
+ */
5
+ export function getFilename(path) {
6
+ if (!path)
7
+ return "";
8
+ const trimmed = path.replace(/[\/\\]+$/, "");
9
+ const parts = trimmed.split(/[\/\\]/);
10
+ return parts[parts.length - 1] ?? "";
11
+ }
12
+ /**
13
+ * Extract the parent directory of a path, with a trailing separator.
14
+ * Handles both forward slash and backslash — backslashes are normalized to forward slashes in output.
15
+ *
16
+ * "/home/user/projects" → "/home/user/"
17
+ * "C:\\Users\\projects" → "C:/Users/"
18
+ * "~" → ""
19
+ * "/" → "/"
20
+ */
21
+ export function getDirectory(path) {
22
+ if (!path)
23
+ return "";
24
+ const normalized = path.replace(/\\/g, "/");
25
+ const parts = normalized.split("/");
26
+ if (parts.length <= 1)
27
+ return normalized === "/" ? "/" : "";
28
+ return parts.slice(0, parts.length - 1).join("/") + "/";
29
+ }
30
+ /**
31
+ * Return the file extension (last dot-segment), or the full path if no dot exists.
32
+ */
33
+ export function getFileExtension(path) {
34
+ if (!path)
35
+ return "";
36
+ const parts = path.split(".");
37
+ return parts[parts.length - 1];
38
+ }
39
+ /**
40
+ * Parse a user-typed path string into a parent-directory + search-query pair.
41
+ *
42
+ * Rules (in order):
43
+ * 1. Empty input → homeDir with no query
44
+ * 2. Expand "~" and "~/..." using homeDir
45
+ * 3. Normalize backslashes to forward slashes
46
+ * 4. Absolute paths (Unix `/`, Windows `C:/`, UNC `//`) → split on last `/`; Windows drive roots stay rooted at `C:/`
47
+ * 5. Relative paths → homeDir is the parent, the whole input is the query
48
+ *
49
+ * resolvePathInput("~/projects/myapp", "/home/user") → { path: "/home/user/projects", query: "myapp" }
50
+ * resolvePathInput("C:\\Users\\me", "/home/user") → { path: "C:/Users", query: "me" }
51
+ * resolvePathInput("D:\\data", "/home/user") → { path: "D:/", query: "data" }
52
+ * resolvePathInput("D:", "/home/user") → { path: "D:/", query: "" }
53
+ * resolvePathInput("myproject", "/home/user") → { path: "/home/user", query: "myproject" }
54
+ */
55
+ export function resolvePathInput(input, homeDir) {
56
+ const trimmed = input.trim();
57
+ if (!trimmed)
58
+ return { path: homeDir || "/", query: "" };
59
+ // Bare tilde: browse home directory itself
60
+ if (trimmed === "~")
61
+ return { path: homeDir || "/", query: "" };
62
+ let expanded = trimmed;
63
+ // Tilde-prefixed path expansion
64
+ if (expanded.startsWith("~/")) {
65
+ expanded = (homeDir || "") + expanded.slice(1);
66
+ }
67
+ // Normalize all backslashes to forward slashes
68
+ expanded = expanded.replace(/\\/g, "/");
69
+ // Bare Windows drive letters are treated as drive roots in browser-style path inputs.
70
+ const driveRoot = expanded.match(/^([A-Za-z]):(?:\/)?$/);
71
+ if (driveRoot)
72
+ return { path: `${driveRoot[1]}:/`, query: "" };
73
+ const isAbsolute = expanded.startsWith("/") || /^[A-Za-z]:\//.test(expanded);
74
+ if (isAbsolute) {
75
+ const lastSlash = expanded.lastIndexOf("/");
76
+ if (/^[A-Za-z]:\//.test(expanded) && lastSlash <= 2) {
77
+ return { path: `${expanded[0]}:/`, query: expanded.slice(3) };
78
+ }
79
+ const parentDir = expanded.slice(0, lastSlash) || "/";
80
+ const query = expanded.slice(lastSlash + 1);
81
+ return { path: parentDir, query };
82
+ }
83
+ return { path: homeDir || "/", query: expanded };
84
+ }
@@ -0,0 +1,100 @@
1
+ import { type SynergyCapabilityManifest } from "./capability";
2
+ export type PluginSource = "local" | "official" | "npm" | "git" | "url" | "builtin";
3
+ export type RuntimeMode = "in-process" | "worker" | "process";
4
+ export type TrustTier = "declarative" | "trusted-import" | "sandbox";
5
+ export type PolicyRisk = "low" | "medium" | "high";
6
+ export interface PluginPolicyManifest extends SynergyCapabilityManifest {
7
+ runtime?: {
8
+ mode?: RuntimeMode;
9
+ resources?: unknown;
10
+ };
11
+ trust?: {
12
+ requestedTier?: TrustTier;
13
+ };
14
+ }
15
+ export interface PluginTrustDecision {
16
+ tier: TrustTier;
17
+ source: PluginSource;
18
+ userTrusted: boolean;
19
+ verifiedIntegrity: boolean;
20
+ reason: string;
21
+ }
22
+ export interface RuntimeLimits {
23
+ startupTimeoutMs: number;
24
+ toolInvocationTimeoutMs: number;
25
+ hookInvocationTimeoutMs: number;
26
+ bridgeRequestTimeoutMs: number;
27
+ taskRunTimeoutMs: number;
28
+ shutdownGraceMs: number;
29
+ maxConcurrentRequests: number;
30
+ maxLogBytesPerMinute: number;
31
+ memoryMb: number;
32
+ memoryPollIntervalMs: number;
33
+ heartbeatIntervalMs: number;
34
+ heartbeatMissesBeforeKill: number;
35
+ }
36
+ export type RuntimeLimitOverrides = Partial<RuntimeLimits>;
37
+ export interface PluginRuntimePolicyInput {
38
+ thirdPartyDefaultMode?: "process" | "worker";
39
+ highRiskRequiresProcess?: boolean;
40
+ allowThirdPartyInProcess?: boolean;
41
+ allowWorkerMode?: boolean;
42
+ allowLocalInProcess?: boolean;
43
+ }
44
+ export interface PluginPolicyDecision {
45
+ source: PluginSource;
46
+ capabilities: string[];
47
+ risk: PolicyRisk;
48
+ trust: PluginTrustDecision;
49
+ runtimeMode: RuntimeMode;
50
+ }
51
+ export declare const DEFAULT_PLUGIN_RUNTIME_POLICY: Required<PluginRuntimePolicyInput>;
52
+ export declare const DEFAULT_PLUGIN_RUNTIME_LIMITS: RuntimeLimits;
53
+ export declare function isTrustedPluginSource(source: PluginSource): boolean;
54
+ export interface ResolveRuntimeModeInput {
55
+ source: PluginSource;
56
+ manifestMode?: RuntimeMode;
57
+ devMode?: boolean;
58
+ userTrusted?: boolean;
59
+ risk?: PolicyRisk;
60
+ forceProcess?: boolean;
61
+ policy?: PluginRuntimePolicyInput;
62
+ }
63
+ export interface PolicyCheckResult {
64
+ type: "pass" | "warn" | "error";
65
+ message: string;
66
+ }
67
+ export declare function resolveRuntimeMode(input: ResolveRuntimeModeInput): RuntimeMode;
68
+ export declare function resolveRuntimeLimits(...overrides: Array<RuntimeLimitOverrides | undefined>): RuntimeLimits;
69
+ export declare function decideTrust(input: {
70
+ source: PluginSource;
71
+ userTrusted: boolean;
72
+ verifiedIntegrity: boolean;
73
+ devMode: boolean;
74
+ }): PluginTrustDecision;
75
+ export declare function defaultPluginTrustDecision(input: {
76
+ source: PluginSource;
77
+ userTrusted?: boolean;
78
+ verifiedIntegrity?: boolean;
79
+ devMode?: boolean;
80
+ }): PluginTrustDecision;
81
+ export declare function resolvePluginPolicyDecision(input: {
82
+ manifest: PluginPolicyManifest;
83
+ source: PluginSource;
84
+ userTrusted?: boolean;
85
+ verifiedIntegrity?: boolean;
86
+ devMode?: boolean;
87
+ policy?: PluginRuntimePolicyInput;
88
+ forceProcess?: boolean;
89
+ risk?: PolicyRisk;
90
+ }): PluginPolicyDecision;
91
+ export declare function trustReason(decision: PluginTrustDecision): string;
92
+ export declare function trustSummary(decision: PluginTrustDecision): string;
93
+ export declare function validateRuntimePolicy(input: {
94
+ manifest: PluginPolicyManifest;
95
+ source: PluginSource;
96
+ trustTier: TrustTier;
97
+ risk: PolicyRisk;
98
+ userTrusted?: boolean;
99
+ policy?: PluginRuntimePolicyInput;
100
+ }): PolicyCheckResult[];
@@ -0,0 +1,240 @@
1
+ import { baseCapabilities, pluginRisk } from "./capability";
2
+ export const DEFAULT_PLUGIN_RUNTIME_POLICY = {
3
+ thirdPartyDefaultMode: "process",
4
+ highRiskRequiresProcess: true,
5
+ allowThirdPartyInProcess: false,
6
+ allowWorkerMode: true,
7
+ allowLocalInProcess: true,
8
+ };
9
+ export const DEFAULT_PLUGIN_RUNTIME_LIMITS = {
10
+ startupTimeoutMs: 5_000,
11
+ toolInvocationTimeoutMs: 120_000,
12
+ hookInvocationTimeoutMs: 120_000,
13
+ bridgeRequestTimeoutMs: 120_000,
14
+ taskRunTimeoutMs: 120_000,
15
+ shutdownGraceMs: 1_500,
16
+ maxConcurrentRequests: 8,
17
+ maxLogBytesPerMinute: 128_000,
18
+ memoryMb: 256,
19
+ memoryPollIntervalMs: 10_000,
20
+ heartbeatIntervalMs: 5_000,
21
+ heartbeatMissesBeforeKill: 3,
22
+ };
23
+ const TRUSTED_SOURCES = new Set(["builtin", "official", "local"]);
24
+ const THIRD_PARTY_SOURCES = new Set(["npm", "git", "url"]);
25
+ export function isTrustedPluginSource(source) {
26
+ return TRUSTED_SOURCES.has(source);
27
+ }
28
+ function runtimePolicy(input) {
29
+ return { ...DEFAULT_PLUGIN_RUNTIME_POLICY, ...(input ?? {}) };
30
+ }
31
+ export function resolveRuntimeMode(input) {
32
+ const policy = runtimePolicy(input.policy);
33
+ const source = input.source;
34
+ const manifestMode = input.manifestMode;
35
+ const userTrusted = input.userTrusted ?? false;
36
+ const risk = input.risk ?? "low";
37
+ if (input.forceProcess)
38
+ return "process";
39
+ if (policy.highRiskRequiresProcess && risk === "high")
40
+ return "process";
41
+ if (manifestMode === "process")
42
+ return "process";
43
+ if (manifestMode === "worker") {
44
+ if (policy.allowWorkerMode && userTrusted)
45
+ return "worker";
46
+ return THIRD_PARTY_SOURCES.has(source) ? "process" : defaultTrustedMode(source, policy);
47
+ }
48
+ if (manifestMode === "in-process") {
49
+ if (TRUSTED_SOURCES.has(source))
50
+ return defaultTrustedMode(source, policy);
51
+ return policy.allowThirdPartyInProcess ? "in-process" : "process";
52
+ }
53
+ if (THIRD_PARTY_SOURCES.has(source)) {
54
+ if (policy.thirdPartyDefaultMode === "worker" && policy.allowWorkerMode && userTrusted)
55
+ return "worker";
56
+ return "process";
57
+ }
58
+ return defaultTrustedMode(source, policy);
59
+ }
60
+ export function resolveRuntimeLimits(...overrides) {
61
+ const resolved = { ...DEFAULT_PLUGIN_RUNTIME_LIMITS };
62
+ for (const override of overrides) {
63
+ if (!override)
64
+ continue;
65
+ for (const [key, value] of Object.entries(override)) {
66
+ if (typeof value !== "number" || !Number.isFinite(value) || value <= 0)
67
+ continue;
68
+ resolved[key] = Math.round(value);
69
+ }
70
+ }
71
+ return resolved;
72
+ }
73
+ export function decideTrust(input) {
74
+ const { source, userTrusted, verifiedIntegrity, devMode } = input;
75
+ let tier;
76
+ let reason;
77
+ switch (source) {
78
+ case "builtin":
79
+ tier = "trusted-import";
80
+ reason = "builtin plugin is always trusted";
81
+ break;
82
+ case "official":
83
+ tier = "trusted-import";
84
+ reason = "official registry plugin is trusted";
85
+ break;
86
+ case "local":
87
+ tier = "trusted-import";
88
+ reason = devMode ? "local plugin in dev mode" : "local plugin";
89
+ break;
90
+ case "npm":
91
+ if (userTrusted && verifiedIntegrity) {
92
+ tier = "trusted-import";
93
+ reason = "user-trusted npm plugin with verified integrity";
94
+ }
95
+ else {
96
+ tier = "sandbox";
97
+ reason = "npm plugin requires explicit user trust and verified integrity";
98
+ }
99
+ break;
100
+ case "git":
101
+ if (userTrusted) {
102
+ tier = "trusted-import";
103
+ reason = "user-trusted git plugin";
104
+ }
105
+ else {
106
+ tier = "sandbox";
107
+ reason = "git plugin requires explicit user trust";
108
+ }
109
+ break;
110
+ case "url":
111
+ tier = "sandbox";
112
+ reason = "URL-sourced plugins always run in sandbox";
113
+ break;
114
+ }
115
+ return {
116
+ tier,
117
+ source,
118
+ userTrusted,
119
+ verifiedIntegrity,
120
+ reason,
121
+ };
122
+ }
123
+ export function defaultPluginTrustDecision(input) {
124
+ return decideTrust({
125
+ source: input.source,
126
+ userTrusted: input.userTrusted ?? isTrustedPluginSource(input.source),
127
+ verifiedIntegrity: input.verifiedIntegrity ?? false,
128
+ devMode: input.devMode ?? false,
129
+ });
130
+ }
131
+ export function resolvePluginPolicyDecision(input) {
132
+ const userTrusted = input.userTrusted ?? isTrustedPluginSource(input.source);
133
+ const verifiedIntegrity = input.verifiedIntegrity ?? input.source === "official";
134
+ const capabilities = baseCapabilities(input.manifest);
135
+ const risk = input.risk ?? pluginRisk(input.manifest, { scope: "install" });
136
+ const trust = decideTrust({
137
+ source: input.source,
138
+ userTrusted,
139
+ verifiedIntegrity,
140
+ devMode: input.devMode ?? false,
141
+ });
142
+ const runtimeMode = resolveRuntimeMode({
143
+ source: input.source,
144
+ manifestMode: input.manifest.runtime?.mode,
145
+ devMode: input.devMode,
146
+ userTrusted,
147
+ risk,
148
+ forceProcess: input.forceProcess,
149
+ policy: input.policy,
150
+ });
151
+ return {
152
+ source: input.source,
153
+ capabilities,
154
+ risk,
155
+ trust,
156
+ runtimeMode,
157
+ };
158
+ }
159
+ export function trustReason(decision) {
160
+ const factors = [];
161
+ if (decision.userTrusted)
162
+ factors.push("user-trusted");
163
+ if (decision.verifiedIntegrity)
164
+ factors.push("integrity verified");
165
+ const factorText = factors.length > 0 ? ` (${factors.join(", ")})` : "";
166
+ return `${decision.reason} -> ${decision.tier}${factorText}`;
167
+ }
168
+ export function trustSummary(decision) {
169
+ const metadata = [];
170
+ if (decision.userTrusted)
171
+ metadata.push("trusted");
172
+ if (decision.verifiedIntegrity)
173
+ metadata.push("verified");
174
+ const meta = metadata.length > 0 ? ` [${metadata.join(", ")}]` : "";
175
+ return `${decision.source} -> ${decision.tier}${meta}`;
176
+ }
177
+ function defaultTrustedMode(source, policy) {
178
+ if (source === "local" && !policy.allowLocalInProcess)
179
+ return "process";
180
+ return "in-process";
181
+ }
182
+ export function validateRuntimePolicy(input) {
183
+ const results = [];
184
+ const requestedMode = input.manifest.runtime?.mode;
185
+ const effectiveMode = resolveRuntimeMode({
186
+ source: input.source,
187
+ manifestMode: requestedMode,
188
+ userTrusted: input.userTrusted,
189
+ risk: input.risk,
190
+ policy: input.policy,
191
+ });
192
+ if (requestedMode && requestedMode !== effectiveMode) {
193
+ results.push({
194
+ type: "warn",
195
+ message: `Plugin requested runtime mode "${requestedMode}" but Synergy will use "${effectiveMode}" for source=${input.source} and risk=${input.risk}.`,
196
+ });
197
+ }
198
+ const requestedTier = input.manifest.trust?.requestedTier;
199
+ if (input.trustTier === "sandbox" && requestedTier !== "sandbox") {
200
+ results.push({
201
+ type: "warn",
202
+ message: `Plugin has sandbox trust tier; effective runtime mode is "${effectiveMode}". Use process mode with explicit resource limits for strongest isolation.`,
203
+ });
204
+ }
205
+ if (requestedTier !== undefined && requestedTier !== input.trustTier) {
206
+ results.push({
207
+ type: "warn",
208
+ message: `Plugin requested trust tier "${requestedTier}" but was assigned "${input.trustTier}" (source=${input.source}). Runtime mode is "${effectiveMode}".`,
209
+ });
210
+ }
211
+ if (effectiveMode === "worker") {
212
+ const tools = input.manifest.permissions?.tools;
213
+ const hasShell = tools?.shell ?? false;
214
+ const hasFileWrite = tools?.filesystem === "write";
215
+ const hasMcpSpawn = tools?.mcp === "spawn";
216
+ const contributedTools = input.manifest.contributes?.tools ?? [];
217
+ const toolShell = contributedTools.some((tool) => tool.capabilities?.shell);
218
+ const toolFileWrite = contributedTools.some((tool) => tool.capabilities?.filesystem === "write");
219
+ if (hasShell || toolShell || hasFileWrite || toolFileWrite || hasMcpSpawn) {
220
+ const unsupported = [];
221
+ if (hasShell || toolShell)
222
+ unsupported.push("shell");
223
+ if (hasFileWrite || toolFileWrite)
224
+ unsupported.push("file_write");
225
+ if (hasMcpSpawn)
226
+ unsupported.push("mcp_spawn");
227
+ results.push({
228
+ type: "warn",
229
+ message: `Worker mode does not fully support: ${unsupported.join(", ")}. Use process mode for these capabilities.`,
230
+ });
231
+ }
232
+ }
233
+ if (effectiveMode === "process" && !input.manifest.runtime?.resources) {
234
+ results.push({
235
+ type: "warn",
236
+ message: "Process mode used without resource limits. Specify runtime.resources to prevent resource exhaustion.",
237
+ });
238
+ }
239
+ return results;
240
+ }
@@ -0,0 +1,8 @@
1
+ export interface RetryOptions {
2
+ attempts?: number;
3
+ delay?: number;
4
+ factor?: number;
5
+ maxDelay?: number;
6
+ retryIf?: (error: unknown) => boolean;
7
+ }
8
+ export declare function retry<T>(fn: () => Promise<T>, options?: RetryOptions): Promise<T>;
package/dist/retry.js ADDED
@@ -0,0 +1,33 @@
1
+ const TRANSIENT_MESSAGES = [
2
+ "load failed",
3
+ "network connection was lost",
4
+ "network request failed",
5
+ "failed to fetch",
6
+ "econnreset",
7
+ "econnrefused",
8
+ "etimedout",
9
+ "socket hang up",
10
+ ];
11
+ function isTransientError(error) {
12
+ if (!error)
13
+ return false;
14
+ const message = String(error instanceof Error ? error.message : error).toLowerCase();
15
+ return TRANSIENT_MESSAGES.some((m) => message.includes(m));
16
+ }
17
+ export async function retry(fn, options = {}) {
18
+ const { attempts = 3, delay = 500, factor = 2, maxDelay = 10000, retryIf = isTransientError } = options;
19
+ let lastError;
20
+ for (let attempt = 0; attempt < attempts; attempt++) {
21
+ try {
22
+ return await fn();
23
+ }
24
+ catch (error) {
25
+ lastError = error;
26
+ if (attempt === attempts - 1 || !retryIf(error))
27
+ throw error;
28
+ const wait = Math.min(delay * Math.pow(factor, attempt), maxDelay);
29
+ await new Promise((resolve) => setTimeout(resolve, wait));
30
+ }
31
+ }
32
+ throw lastError;
33
+ }
package/package.json ADDED
@@ -0,0 +1,22 @@
1
+ {
2
+ "name": "@ericsanchezok/synergy-util",
3
+ "version": "2.4.4",
4
+ "type": "module",
5
+ "license": "MIT",
6
+ "exports": {
7
+ "./*": {
8
+ "import": "./dist/*.js",
9
+ "types": "./dist/*.d.ts"
10
+ }
11
+ },
12
+ "scripts": {
13
+ "typecheck": "tsc --noEmit",
14
+ "build": "rm -rf dist && tsc -p tsconfig.build.json"
15
+ },
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "dependencies": {
20
+ "zod": "4.1.8"
21
+ }
22
+ }