@ericsanchezok/synergy-util 3.0.23 → 3.1.0

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.
Files changed (74) hide show
  1. package/LICENSE.codex +201 -0
  2. package/NOTICE +9 -0
  3. package/dist/activity.d.ts +9 -4
  4. package/dist/activity.js +20 -19
  5. package/dist/asset-reference.d.ts +15 -0
  6. package/dist/asset-reference.js +98 -0
  7. package/dist/atomic-file.d.ts +9 -0
  8. package/dist/atomic-file.js +86 -0
  9. package/dist/attachment-presentation.d.ts +26 -0
  10. package/dist/attachment-presentation.js +11 -0
  11. package/dist/capability.d.ts +1 -1
  12. package/dist/capability.js +5 -19
  13. package/dist/cli-command.d.ts +12 -0
  14. package/dist/cli-command.js +3 -0
  15. package/dist/encode.d.ts +3 -0
  16. package/dist/encode.js +7 -0
  17. package/dist/error.d.ts +36 -56
  18. package/dist/feature-config.d.ts +4 -0
  19. package/dist/feature-config.js +23 -0
  20. package/dist/features.d.ts +134 -0
  21. package/dist/features.js +195 -0
  22. package/dist/file-patch.d.ts +31 -0
  23. package/dist/file-patch.js +218 -0
  24. package/dist/filesystem-identity.d.ts +28 -0
  25. package/dist/filesystem-identity.js +71 -0
  26. package/dist/fn.d.ts +1 -1
  27. package/dist/fs-lock.d.ts +5 -1
  28. package/dist/fs-lock.js +14 -6
  29. package/dist/input-assistance.d.ts +51 -0
  30. package/dist/input-assistance.js +61 -0
  31. package/dist/installed-launcher.d.ts +18 -0
  32. package/dist/installed-launcher.js +39 -0
  33. package/dist/io-retry.d.ts +11 -0
  34. package/dist/io-retry.js +36 -0
  35. package/dist/json-value.d.ts +4 -0
  36. package/dist/json-value.js +12 -0
  37. package/dist/markdown-assets.d.ts +6 -0
  38. package/dist/markdown-assets.js +29 -0
  39. package/dist/markdown-link.d.ts +2 -0
  40. package/dist/markdown-link.js +8 -0
  41. package/dist/native-assets.d.ts +7 -0
  42. package/dist/native-assets.js +28 -0
  43. package/dist/network-error.d.ts +7 -0
  44. package/dist/network-error.js +111 -0
  45. package/dist/plugin-policy.d.ts +1 -1
  46. package/dist/process-group.d.ts +45 -0
  47. package/dist/process-group.js +156 -0
  48. package/dist/process-identity.js +5 -2
  49. package/dist/projection-patch.d.ts +9 -0
  50. package/dist/projection-patch.js +74 -0
  51. package/dist/reasoning-item.d.ts +1 -0
  52. package/dist/reasoning-item.js +16 -0
  53. package/dist/render-artifact.d.ts +352 -0
  54. package/dist/render-artifact.js +115 -0
  55. package/dist/render-ui.d.ts +202 -0
  56. package/dist/render-ui.js +208 -0
  57. package/dist/resource-reference.d.ts +73 -0
  58. package/dist/resource-reference.js +227 -0
  59. package/dist/retry.d.ts +4 -0
  60. package/dist/retry.js +56 -23
  61. package/dist/runtime-startup.d.ts +240 -2
  62. package/dist/runtime-startup.js +108 -9
  63. package/dist/terminal.d.ts +217 -0
  64. package/dist/terminal.js +111 -0
  65. package/dist/tool-display.d.ts +10 -0
  66. package/dist/tool-display.js +22 -1
  67. package/dist/tool-timeout.d.ts +21 -0
  68. package/dist/tool-timeout.js +1 -0
  69. package/dist/tool.d.ts +6 -7
  70. package/dist/workbench-view.d.ts +692 -0
  71. package/dist/workbench-view.js +90 -0
  72. package/dist/worker-plan.d.ts +39 -0
  73. package/dist/worker-plan.js +33 -0
  74. package/package.json +28 -4
@@ -0,0 +1,12 @@
1
+ import { z } from "zod";
2
+ const json = z.json();
3
+ // hono-openapi 1.1.2 loses nested recursive $defs. JSON payloads need no recursive
4
+ // OpenAPI restriction, but persistence still validates every nested value.
5
+ export const JsonValue = z.unknown().refine((value) => {
6
+ try {
7
+ return json.safeParse(value).success;
8
+ }
9
+ catch {
10
+ return false;
11
+ }
12
+ }, "Expected a JSON value");
@@ -0,0 +1,6 @@
1
+ export declare const MARKDOWN_ASSET_REFERENCE_CHARS = 262144;
2
+ export declare function markdownAssetReferences(text: string): string[];
3
+ export declare function attachmentSuppression(outputs: readonly {
4
+ id: string;
5
+ references?: readonly string[];
6
+ }[], inline: readonly string[]): Record<string, string[]>;
@@ -0,0 +1,29 @@
1
+ import { Marked } from "marked";
2
+ import { ResourceReference } from "./resource-reference";
3
+ const markdown = new Marked();
4
+ export const MARKDOWN_ASSET_REFERENCE_CHARS = 262144;
5
+ export function markdownAssetReferences(text) {
6
+ const source = text.slice(0, MARKDOWN_ASSET_REFERENCE_CHARS);
7
+ if (!source.includes("asset://"))
8
+ return [];
9
+ const references = new Set();
10
+ markdown.walkTokens(markdown.lexer(source), (token) => {
11
+ if (references.size >= 32 || (token.type !== "image" && token.type !== "link"))
12
+ return;
13
+ if (token.href.length > 256)
14
+ return;
15
+ const reference = ResourceReference.parse(token.href);
16
+ if (reference.kind === "asset")
17
+ references.add(reference.url);
18
+ });
19
+ return [...references];
20
+ }
21
+ export function attachmentSuppression(outputs, inline) {
22
+ const seen = new Set(inline);
23
+ return Object.fromEntries(outputs.map((output) => {
24
+ const hidden = [...new Set(output.references?.filter((reference) => seen.has(reference)))];
25
+ for (const reference of output.references ?? [])
26
+ seen.add(reference);
27
+ return [output.id, hidden];
28
+ }));
29
+ }
@@ -0,0 +1,2 @@
1
+ export declare function bareUrlEnd(value: string): number;
2
+ export declare function isBareUrlBoundary(value: string): boolean;
@@ -0,0 +1,8 @@
1
+ const punctuation = /[\u3001\u3002\uff0c\uff1b\uff01\uff1f\uff1a\u201c\u201d\u2018\u2019\u300c\u300d\u300e\u300f\u300a\u300b\u3010\u3011\uff08\uff09]/;
2
+ export function bareUrlEnd(value) {
3
+ const boundary = value.search(punctuation);
4
+ return boundary < 0 ? value.length : boundary;
5
+ }
6
+ export function isBareUrlBoundary(value) {
7
+ return punctuation.test(value);
8
+ }
@@ -0,0 +1,7 @@
1
+ export declare function nativeLibc(): "glibc" | "musl";
2
+ export declare function nativePackageName(options?: {
3
+ platform?: string;
4
+ arch?: string;
5
+ libc?: string;
6
+ }): string;
7
+ export declare function nativeAsset(filename: string, owner: string | URL): string | undefined;
@@ -0,0 +1,28 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ export function nativeLibc() {
4
+ if (process.platform !== "linux")
5
+ return "glibc";
6
+ if (typeof SYNERGY_LIBC === "string")
7
+ return SYNERGY_LIBC === "musl" ? "musl" : "glibc";
8
+ return readFileSync("/proc/self/maps", "utf8").includes("ld-musl-") ? "musl" : "glibc";
9
+ }
10
+ export function nativePackageName(options = {}) {
11
+ const platform = options.platform ?? process.platform;
12
+ const arch = options.arch ?? process.arch;
13
+ const abi = platform === "linux" ? `-${options.libc ?? nativeLibc()}` : "";
14
+ return `@ericsanchezok/synergy-native-${platform}-${arch}${abi}`;
15
+ }
16
+ export function nativeAsset(filename, owner) {
17
+ if (!/^[a-zA-Z0-9_.-]+$/.test(filename) || filename === "." || filename === "..")
18
+ throw new Error("Native asset must be a package-local filename");
19
+ try {
20
+ const resolved = createRequire(owner).resolve(`${nativePackageName()}/${filename}`);
21
+ return existsSync(resolved) ? resolved : undefined;
22
+ }
23
+ catch (error) {
24
+ if (error.code === "MODULE_NOT_FOUND")
25
+ return undefined;
26
+ throw error;
27
+ }
28
+ }
@@ -0,0 +1,7 @@
1
+ export declare function classifyNetworkError(error: unknown): {
2
+ kind: "transient" | "permanent" | "aborted" | "indeterminate";
3
+ code?: string;
4
+ syscall?: string;
5
+ category?: string;
6
+ } | undefined;
7
+ export declare function isRetryableHttpStatus(status: number): boolean;
@@ -0,0 +1,111 @@
1
+ // Provenance: https://nodejs.org/docs/latest-v22.x/api/errors.html#common-system-errors
2
+ // https://github.com/nodejs/undici/blob/v6.21.3/docs/docs/api/Errors.md
3
+ // https://nodejs.org/docs/latest-v22.x/api/dns.html#dnslookuphostname-options-callback
4
+ // Local adaptation: classify bounded error graphs across runtimes; callers own replay safety and retry budgets.
5
+ const TRANSIENT_CODES = new Set([
6
+ "CONNECTIONREFUSED",
7
+ "CONNECTIONCLOSED",
8
+ "FAILEDTOOPENSOCKET",
9
+ "ETIMEOUT",
10
+ "ETIMEDOUT",
11
+ "ENOTFOUND",
12
+ "EAI_AGAIN",
13
+ "ESERVFAIL",
14
+ "ECONNRESET",
15
+ "ECONNREFUSED",
16
+ "ECONNABORTED",
17
+ "EPIPE",
18
+ "ENETUNREACH",
19
+ "EHOSTUNREACH",
20
+ "ENETDOWN",
21
+ "UND_ERR_CONNECT_TIMEOUT",
22
+ "UND_ERR_HEADERS_TIMEOUT",
23
+ "UND_ERR_BODY_TIMEOUT",
24
+ "UND_ERR_SOCKET",
25
+ ]);
26
+ const PERMANENT_CODES = new Set([
27
+ "ENONAME",
28
+ "EAI_NONAME",
29
+ "ENODATA",
30
+ "EBADNAME",
31
+ "ERR_INVALID_URL",
32
+ "ERR_CAUSE_TRUNCATED",
33
+ "CERT_HAS_EXPIRED",
34
+ "CERT_NOT_YET_VALID",
35
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
36
+ "SELF_SIGNED_CERT_IN_CHAIN",
37
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
38
+ "UNABLE_TO_GET_ISSUER_CERT",
39
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
40
+ "UND_ERR_INVALID_ARG",
41
+ "UND_ERR_REQ_CONTENT_LENGTH_MISMATCH",
42
+ "UND_ERR_RES_EXCEEDED_MAX_SIZE",
43
+ ]);
44
+ const ABORT_CODES = new Set(["ABORT_ERR", "UND_ERR_ABORT", "UND_ERR_ABORTED", "ECANCELLED"]);
45
+ // Provenance: https://www.rfc-editor.org/rfc/rfc5280
46
+ // Local adaptation: certificate wording without a reason code is reported as a distinct indeterminate
47
+ // kind so callers can apply a narrower retry budget than a proven transient failure.
48
+ const CERTIFICATE_MESSAGE = /\bcertificate\b/i;
49
+ const VERIFICATION_MESSAGE = /\bverif/i;
50
+ export function classifyNetworkError(error) {
51
+ const seen = new Set();
52
+ let remaining = 32;
53
+ function visit(value, depth) {
54
+ if (depth > 8 || --remaining < 0)
55
+ return { kind: "permanent" };
56
+ if (!value || (typeof value !== "object" && typeof value !== "string"))
57
+ return undefined;
58
+ if (typeof value === "object") {
59
+ if (seen.has(value))
60
+ return undefined;
61
+ seen.add(value);
62
+ }
63
+ const record = typeof value === "object" ? value : undefined;
64
+ const message = typeof value === "string" ? value : typeof record?.message === "string" ? record.message : "";
65
+ const code = typeof record?.code === "string" ? record.code : undefined;
66
+ const token = code?.toUpperCase() ?? message.match(/\b(?:E[A-Z_]+|UND_ERR_[A-Z_]+)\b/)?.[0] ?? message.trim().toUpperCase();
67
+ const syscall = typeof record?.syscall === "string" ? record.syscall : undefined;
68
+ const name = record?.name;
69
+ let result;
70
+ if (name === "AbortError" || (token && ABORT_CODES.has(token)))
71
+ result = { kind: "aborted", code, syscall };
72
+ else if ((token && (PERMANENT_CODES.has(token) || /^(ERR_TLS_|ERR_SSL_)/.test(token))) ||
73
+ /certificate (?:has expired|verify failed|verification failed)|self[- ]signed certificate|unable to verify (?:the )?first certificate/i.test(message)) {
74
+ result = { kind: "permanent", code, syscall };
75
+ }
76
+ else if (!code && CERTIFICATE_MESSAGE.test(message) && VERIFICATION_MESSAGE.test(message)) {
77
+ result = { kind: "indeterminate", code, syscall, category: "tls-verification" };
78
+ }
79
+ else if (token && TRANSIENT_CODES.has(token))
80
+ result = { kind: "transient", code: code ?? token, syscall };
81
+ else if (name === "TimeoutError" ||
82
+ /^(?:(?:TypeError|Error): )?(?:fetch failed(?:: failed to fetch)?|failed to fetch|load failed|network error|connection error|(?:the )?network request failed|(?:the )?network connection was lost|socket hang up|NetworkError when attempting to fetch resource|The Internet connection appears to be offline)\.?$/i.test(message) ||
83
+ /^The socket connection was closed unexpectedly\./i.test(message) ||
84
+ /unable to connect\. is the computer able to access the url\?/i.test(message)) {
85
+ result = { kind: "transient", code, syscall };
86
+ }
87
+ const children = [record?.cause, ...(Array.isArray(record?.errors) ? record.errors.slice(0, 33) : [])];
88
+ for (const child of children) {
89
+ if (child === undefined)
90
+ continue;
91
+ const nested = visit(child, depth + 1);
92
+ if (!nested)
93
+ continue;
94
+ if (nested.kind === "aborted")
95
+ return nested;
96
+ if (result?.kind === "aborted")
97
+ return result;
98
+ if (nested.kind === "permanent" ||
99
+ !result ||
100
+ (result.kind === "transient" && (nested.code || nested.kind === "indeterminate")))
101
+ result = nested;
102
+ }
103
+ return result;
104
+ }
105
+ return visit(error, 0);
106
+ }
107
+ // Provenance: https://www.rfc-editor.org/rfc/rfc9110.html#section-15
108
+ // Local adaptation: transient HTTP reads exclude unsupported server capabilities; provider-specific conflicts stay with the SDK.
109
+ export function isRetryableHttpStatus(status) {
110
+ return status === 408 || status === 429 || (status >= 500 && status <= 599 && status !== 501 && status !== 505);
111
+ }
@@ -37,7 +37,7 @@ export interface PolicyCheckResult {
37
37
  }
38
38
  export declare function resolveRuntimeMode(input: ResolveRuntimeModeInput): RuntimeMode;
39
39
  export declare function resolveRuntimeLimits(...overrides: Array<RuntimeLimitOverrides | undefined>): RuntimeLimits;
40
- export declare function isTrustedPluginSource(source: PluginSource): source is "local" | "official" | "builtin";
40
+ export declare function isTrustedPluginSource(source: PluginSource): source is "builtin" | "local" | "official";
41
41
  export declare function decideTrust(input: {
42
42
  source: PluginSource;
43
43
  userTrusted: boolean;
@@ -0,0 +1,45 @@
1
+ interface ProcessHandle {
2
+ readonly pid?: number;
3
+ stop?(): Promise<void>;
4
+ kill(signal?: NodeJS.Signals | number): boolean;
5
+ }
6
+ export declare namespace ProcessGroup {
7
+ interface TaskkillProcess {
8
+ once(event: "exit", listener: (code: number | null) => void): this;
9
+ once(event: "error", listener: () => void): this;
10
+ kill(): boolean;
11
+ }
12
+ export interface KillTreeRuntimeForTest {
13
+ platform: NodeJS.Platform;
14
+ taskkill(pid: number): TaskkillProcess;
15
+ isPidAlive(pid: number): boolean;
16
+ signalProcessGroup?(pid: number, signal: NodeJS.Signals | number): void;
17
+ taskkillTimeoutMs: number;
18
+ }
19
+ export type ProcessInvocation = {
20
+ command: string;
21
+ args: string[];
22
+ };
23
+ /**
24
+ * Wrap an invocation so it leads its own process group and keeps that group
25
+ * alive briefly after exit, without inserting a process between the caller
26
+ * and `invocation.command`.
27
+ *
28
+ * `exec` is load-bearing: callers correlate kernel-side facts with
29
+ * `child.pid` — the macOS sandbox audit record that names a denied path and
30
+ * access is attributed to the exec'd image — and an intermediate shell would
31
+ * be a different pid, silently detaching that evidence from the child. The
32
+ * ownership anchor therefore cannot be a fixed `sleep` started up front; it
33
+ * polls the pid it shares with the exec'd image and anchors the group only
34
+ * once that image has exited.
35
+ */
36
+ export function prepareOwnedProcessGroup(invocation: ProcessInvocation, platform?: NodeJS.Platform): ProcessInvocation;
37
+ export function releaseOwnedProcessGroup(proc: ProcessHandle): void;
38
+ export function killTree(proc: ProcessHandle, opts?: {
39
+ exited?: () => boolean;
40
+ allowExitedParent?: boolean;
41
+ runtime?: KillTreeRuntimeForTest;
42
+ }): Promise<void>;
43
+ export {};
44
+ }
45
+ export {};
@@ -0,0 +1,156 @@
1
+ import { spawn } from "node:child_process";
2
+ import { setTimeout as sleep } from "node:timers/promises";
3
+ const SIGKILL_TIMEOUT_MS = 200;
4
+ const TASKKILL_TIMEOUT_MS = 2_000;
5
+ export var ProcessGroup;
6
+ (function (ProcessGroup) {
7
+ /**
8
+ * Wrap an invocation so it leads its own process group and keeps that group
9
+ * alive briefly after exit, without inserting a process between the caller
10
+ * and `invocation.command`.
11
+ *
12
+ * `exec` is load-bearing: callers correlate kernel-side facts with
13
+ * `child.pid` — the macOS sandbox audit record that names a denied path and
14
+ * access is attributed to the exec'd image — and an intermediate shell would
15
+ * be a different pid, silently detaching that evidence from the child. The
16
+ * ownership anchor therefore cannot be a fixed `sleep` started up front; it
17
+ * polls the pid it shares with the exec'd image and anchors the group only
18
+ * once that image has exited.
19
+ */
20
+ function prepareOwnedProcessGroup(invocation, platform = process.platform) {
21
+ if (platform === "win32")
22
+ return invocation;
23
+ return {
24
+ command: "/bin/sh",
25
+ args: [
26
+ "-c",
27
+ '(while kill -0 $$ 2>/dev/null; do sleep 0.2; done; sleep 5) </dev/null >/dev/null 2>&1 & exec "$0" "$@"',
28
+ invocation.command,
29
+ ...invocation.args,
30
+ ],
31
+ };
32
+ }
33
+ ProcessGroup.prepareOwnedProcessGroup = prepareOwnedProcessGroup;
34
+ function releaseOwnedProcessGroup(proc) {
35
+ if (process.platform === "win32" || !proc.pid)
36
+ return;
37
+ try {
38
+ process.kill(-proc.pid, "SIGTERM");
39
+ }
40
+ catch { }
41
+ }
42
+ ProcessGroup.releaseOwnedProcessGroup = releaseOwnedProcessGroup;
43
+ async function killTree(proc, opts) {
44
+ try {
45
+ if (proc.stop)
46
+ await proc.stop();
47
+ else
48
+ await killTreeOnce(proc, opts);
49
+ }
50
+ catch { }
51
+ }
52
+ ProcessGroup.killTree = killTree;
53
+ async function killTreeOnce(proc, opts) {
54
+ const pid = proc.pid;
55
+ if (!pid || (!opts?.allowExitedParent && didExit(opts?.exited)))
56
+ return;
57
+ const runtime = opts?.runtime ?? killTreeRuntime;
58
+ if (runtime.platform === "win32") {
59
+ if (didExit(opts?.exited))
60
+ return;
61
+ const succeeded = await runTaskkill(runtime, pid);
62
+ if (didExit(opts?.exited) || (succeeded && !isPidAlive(runtime, pid)))
63
+ return;
64
+ try {
65
+ proc.kill("SIGKILL");
66
+ }
67
+ catch { }
68
+ return;
69
+ }
70
+ const signalProcessGroup = runtime.signalProcessGroup ?? ((target, signal) => process.kill(-target, signal));
71
+ try {
72
+ signalProcessGroup(pid, "SIGTERM");
73
+ }
74
+ catch (_e) {
75
+ if (didExit(opts?.exited))
76
+ return;
77
+ try {
78
+ proc.kill("SIGTERM");
79
+ }
80
+ catch { }
81
+ await sleep(SIGKILL_TIMEOUT_MS);
82
+ if (!didExit(opts?.exited)) {
83
+ try {
84
+ proc.kill("SIGKILL");
85
+ }
86
+ catch { }
87
+ }
88
+ return;
89
+ }
90
+ await sleep(SIGKILL_TIMEOUT_MS);
91
+ try {
92
+ signalProcessGroup(pid, 0);
93
+ signalProcessGroup(pid, "SIGKILL");
94
+ }
95
+ catch { }
96
+ }
97
+ function didExit(exited) {
98
+ try {
99
+ return exited?.() ?? false;
100
+ }
101
+ catch {
102
+ return false;
103
+ }
104
+ }
105
+ function isPidAlive(runtime, pid) {
106
+ try {
107
+ return runtime.isPidAlive(pid);
108
+ }
109
+ catch {
110
+ return true;
111
+ }
112
+ }
113
+ const killTreeRuntime = {
114
+ platform: process.platform,
115
+ taskkill: (pid) => spawn("taskkill", ["/pid", String(pid), "/f", "/t"], { stdio: "ignore" }),
116
+ isPidAlive: (pid) => {
117
+ try {
118
+ process.kill(pid, 0);
119
+ return true;
120
+ }
121
+ catch (error) {
122
+ return error.code === "EPERM";
123
+ }
124
+ },
125
+ taskkillTimeoutMs: TASKKILL_TIMEOUT_MS,
126
+ };
127
+ function runTaskkill(runtime, pid) {
128
+ return new Promise((resolve) => {
129
+ let killer;
130
+ try {
131
+ killer = runtime.taskkill(pid);
132
+ }
133
+ catch {
134
+ resolve(false);
135
+ return;
136
+ }
137
+ let settled = false;
138
+ const finish = (succeeded) => {
139
+ if (settled)
140
+ return;
141
+ settled = true;
142
+ clearTimeout(timer);
143
+ resolve(succeeded);
144
+ };
145
+ const timer = setTimeout(() => {
146
+ try {
147
+ killer.kill();
148
+ }
149
+ catch { }
150
+ finish(false);
151
+ }, runtime.taskkillTimeoutMs);
152
+ killer.once("exit", (code) => finish(code === 0));
153
+ killer.once("error", () => finish(false));
154
+ });
155
+ }
156
+ })(ProcessGroup || (ProcessGroup = {}));
@@ -20,8 +20,11 @@ export async function processStartIdentity(pid) {
20
20
  if (pid === process.pid) {
21
21
  // Cache the promise, not the value: concurrent first acquisitions must
22
22
  // share a single query instead of spawning one subprocess each.
23
- ownIdentityPromise ??= queryProcessStartIdentity(pid);
24
- return ownIdentityPromise;
23
+ const pending = (ownIdentityPromise ??= queryProcessStartIdentity(pid));
24
+ const identity = await pending;
25
+ if (identity === undefined && ownIdentityPromise === pending)
26
+ ownIdentityPromise = undefined;
27
+ return identity;
25
28
  }
26
29
  return queryProcessStartIdentity(pid);
27
30
  }
@@ -0,0 +1,9 @@
1
+ import { z } from "zod";
2
+ export declare const ProjectionPatch: z.ZodObject<{
3
+ path: z.ZodArray<z.ZodString>;
4
+ value: z.ZodOptional<z.ZodType<z.JSONType, unknown, z.core.$ZodTypeInternals<z.JSONType, unknown>>>;
5
+ remove: z.ZodOptional<z.ZodLiteral<true>>;
6
+ }, z.core.$strip>;
7
+ export type ProjectionPatch = z.infer<typeof ProjectionPatch>;
8
+ export declare function projectionPatch(previous: unknown, next: unknown): ProjectionPatch[];
9
+ export declare function applyProjectionPatch(previous: unknown, changes: ProjectionPatch[]): unknown;
@@ -0,0 +1,74 @@
1
+ import { z } from "zod";
2
+ import { JsonValue } from "./json-value";
3
+ export const ProjectionPatch = z.object({
4
+ path: z.array(z.string()).max(64),
5
+ value: JsonValue.optional(),
6
+ remove: z.literal(true).optional(),
7
+ });
8
+ const object = (value) => value !== null && typeof value === "object";
9
+ const safe = (key) => !["__proto__", "prototype", "constructor"].includes(key);
10
+ export function projectionPatch(previous, next) {
11
+ const changes = [];
12
+ const visit = (before, after, path) => {
13
+ if (before === after)
14
+ return;
15
+ if (!object(before) || !object(after) || Array.isArray(before) !== Array.isArray(after)) {
16
+ changes.push(after === undefined ? { path, remove: true } : { path, value: after });
17
+ return;
18
+ }
19
+ for (const key of Object.keys(after)) {
20
+ if (!safe(key))
21
+ throw new Error("Invalid projection key");
22
+ visit(before[key], after[key], [...path, key]);
23
+ }
24
+ if (Array.isArray(after)) {
25
+ if (before.length !== after.length)
26
+ changes.push({ path: [...path, "length"], value: after.length });
27
+ return;
28
+ }
29
+ for (const key of Object.keys(before))
30
+ if (!(key in after))
31
+ changes.push({ path: [...path, key], remove: true });
32
+ };
33
+ visit(previous, next, []);
34
+ return changes;
35
+ }
36
+ export function applyProjectionPatch(previous, changes) {
37
+ let value = previous;
38
+ const copies = new WeakMap();
39
+ const copy = (input) => {
40
+ if (!object(input))
41
+ throw new Error("Invalid projection parent");
42
+ let result = copies.get(input);
43
+ if (result)
44
+ return result;
45
+ result = (Array.isArray(input) ? [...input] : { ...input });
46
+ copies.set(input, result);
47
+ copies.set(result, result);
48
+ return result;
49
+ };
50
+ for (const change of changes) {
51
+ if (!change.path.every(safe))
52
+ throw new Error("Invalid projection path");
53
+ if (!change.path.length) {
54
+ value = change.remove ? undefined : structuredClone(change.value);
55
+ continue;
56
+ }
57
+ value = copy(value);
58
+ let parent = value;
59
+ for (const key of change.path.slice(0, -1)) {
60
+ if (!object(parent) || !Object.hasOwn(parent, key))
61
+ throw new Error("Missing projection parent");
62
+ parent[key] = copy(parent[key]);
63
+ parent = parent[key];
64
+ }
65
+ if (!object(parent))
66
+ throw new Error("Invalid projection parent");
67
+ const key = change.path.at(-1);
68
+ if (change.remove)
69
+ delete parent[key];
70
+ else
71
+ parent[key] = structuredClone(change.value);
72
+ }
73
+ return value;
74
+ }
@@ -0,0 +1 @@
1
+ export declare function reasoningItemKey(metadata: unknown): string | undefined;
@@ -0,0 +1,16 @@
1
+ export function reasoningItemKey(metadata) {
2
+ if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
3
+ return;
4
+ let identity;
5
+ for (const [provider, value] of Object.entries(metadata)) {
6
+ if (!value || typeof value !== "object" || Array.isArray(value) || !("itemId" in value))
7
+ continue;
8
+ const itemID = value.itemId;
9
+ if (typeof itemID !== "string" || !itemID.trim() || itemID.length > 1024 || provider.length > 128)
10
+ continue;
11
+ if (identity)
12
+ return;
13
+ identity = JSON.stringify([provider, itemID]);
14
+ }
15
+ return identity;
16
+ }